diff --git a/.github/DISCUSSION_TEMPLATE/game-bring-up.yml b/.github/DISCUSSION_TEMPLATE/game-bring-up.yml new file mode 100644 index 0000000000..afb5f3d3d3 --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/game-bring-up.yml @@ -0,0 +1,81 @@ +title: "Game name (PPSA00000)" +body: + - type: markdown + attributes: + value: | + One thread per title, for every platform: search this category for the title ID first. If a thread exists, reply there, also when your OS or GPU differs from the one it reports. + Name the discussion `Game name (title ID)`, for example `DOOM Shareware (PPSA99666)`, without the platform. + Bug reports and implementation work belong in issues and pull requests; link them here as the blocker. + Per-game progress is also tracked on the [blocker map](https://alxcbtw.github.io/anyps5-roadmap/), built from public GitHub data. Only share material you have the right to distribute. + - type: checkboxes + id: searched + attributes: + label: Existing thread + options: + - label: I searched this category for the title ID and no thread exists for this title on any platform. + required: true + - type: input + id: title + attributes: + label: Title + description: Name and title ID from `sce_sys/param.json`. + placeholder: DOOM Shareware (PPSA99666) + validations: + required: true + - type: markdown + attributes: + value: | + ### First report + The fields below describe one run. Other systems and content versions are added to the thread as replies. + - type: input + id: version + attributes: + label: Content version tested + description: The content version from `sce_sys/param.json`. + placeholder: 01.001.001 + validations: + required: true + - type: input + id: commit + attributes: + label: AnyPS5 commit + description: The `main` commit that was built, or the release that was used, and any pull requests merged on top. + validations: + required: true + - type: input + id: system + attributes: + label: System tested + description: OS, GPU and driver version, and the compiler if you built it yourself. + placeholder: Arch Linux, GCC 16.2.1, RTX 5080 (NVIDIA 615.78.08) + validations: + required: true + - type: dropdown + id: reached + attributes: + label: Furthest point + options: + - Does not convert + - Stops before the first frame + - Logos or loading screen + - Menu + - In game + validations: + required: true + - type: textarea + id: seen + attributes: + label: What is shown + description: The last screen that was seen, the frame rate there, and what works or is missing (picture, audio, input). + validations: + required: true + - type: textarea + id: blocker + attributes: + label: Current blocker + description: The line where it stops, the issue that tracks it, and the open pull requests and local changes the title still needs. + - type: textarea + id: steps + attributes: + label: How it was run + description: Commands and environment variables, and what the title needed beyond the usage guide. diff --git a/.github/ISSUE_TEMPLATE/performance.yml b/.github/ISSUE_TEMPLATE/performance.yml new file mode 100644 index 0000000000..417fc4be2a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/performance.yml @@ -0,0 +1,65 @@ +name: Performance report +description: A slow scene or a change in speed, measured with a performance trace so that reports can be compared. +title: "perf: " +body: + - type: markdown + attributes: + value: | + Capture the scene with `APS5_TRACE_FRAMES=:` (presents; `APS5_TRACE_SECONDS=:` also works), as described under [Performance trace](https://github.com/boykopovar/AnyPS5/blob/main/docs/dev/BUILD.md#performance-trace). It writes `aps5-trace--.json.gz` and appends one line to `aps5-summary.jsonl`. + + - Run the scene once and restart the title, so the shader and pipeline caches are warm. + - Use the same scene, window, resolution and settings for every capture you compare. + - Set no other `APS5_TRACE_*` or `APS5_PROFILE_*` switch and do not redirect the log; they cost time. + - Capture the base and the change on the same machine in the same session. + + A trace holds timings, scope and thread names and counts, no game data. The summary line names the GPU, driver and OS. + - type: input + id: scene + attributes: + label: Title and scene + description: Title ID and what the window covers (menu, level, position, cutscene). + placeholder: PPSA01325, main menu after the intro + validations: + required: true + - type: textarea + id: observed + attributes: + label: What is slow or what changed + description: What you see (frame time, stutter, a phase that grew) and, for a comparison, the two commits. + validations: + required: true + - type: textarea + id: summary + attributes: + label: Summary lines + description: The line of aps5-summary.jsonl for each capture. It holds the commit, the APS5_* switches, GPU, driver, OS, resolution and frame times. + render: json + validations: + required: true + - type: textarea + id: trace + attributes: + label: Traces + description: Attach each aps5-trace--.json.gz (drag it into this field). They open in ui.perfetto.dev. + validations: + required: true + - type: textarea + id: digest + attributes: + label: Digest + description: The output of `python3 tools/trace_digest.py ` (with `--stacks .collapsed.txt` if you sampled the worker). + - type: textarea + id: stacks + attributes: + label: Worker stacks + description: Optional. The collapsed-stack file of APS5_SAMPLE_WORKER (.collapsed.txt), attached. It opens in speedscope.app. + - type: checkboxes + id: protocol + attributes: + label: Capture + options: + - label: The caches were warm (the scene ran once and the title was restarted) + required: true + - label: No other APS5_TRACE_* or APS5_PROFILE_* switch was set and the log was not redirected + required: true + - label: Captures compared here come from the same machine, scene and settings diff --git a/.github/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 fec7af47a1..c084976f5c 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,16 +1,19 @@ ### What ### Tested - + ### Checklist - [ ] Based on current `main`; no other open PR implements the same functions - [ ] One topic per PR; follow-ups go in a new PR - [ ] No comments in code except [technical debt](https://github.com/boykopovar/AnyPS5/blob/main/docs/dev/CONVENTIONS.md) -- [ ] Unimplemented paths throw (`NotImplemented_nid_no_patch`); silent stubs are listed in [TechnicalDebt](https://github.com/boykopovar/AnyPS5/blob/main/docs/dev/TechnicalDebt.md#silent-stubs) +- [ ] Unimplemented paths throw (`NotImplemented_nid_no_patch`); silent stubs are listed under Silent stubs in their library's [technical debt](https://github.com/boykopovar/AnyPS5/tree/main/docs/dev/debt) file - [ ] No notes, investigation `.md` files or images added to the repository (attach them to this PR) - [ ] 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 e64aaef4bf..f76171b143 100644 --- a/.github/workflows/area-labels.yml +++ b/.github/workflows/area-labels.yml @@ -14,18 +14,34 @@ jobs: contents: read pull-requests: write steps: + - uses: actions/cache/restore@v6 + with: + path: ${{ runner.temp }}/area-state.json + key: area-labels-${{ github.run_id }} + restore-keys: area-labels- - id: prs env: GH_TOKEN: ${{ github.token }} run: | + state=$RUNNER_TEMP/area-state.json + [ -s "$state" ] || echo '{}' > "$state" + config=$(gh api "repos/$GITHUB_REPOSITORY/contents/.github/labeler.yml" --jq .sha) + gh pr list -R "$GITHUB_REPOSITORY" --base "${{ github.event.repository.default_branch }}" -L 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" \ + 'with_entries(select(.key | in($open[0]))) + ($open[0] | with_entries(select(.key | IN($todo | split("\n")[]))))' "$state" > "$RUNNER_TEMP/area-next.json" { echo 'numbers<> "$GITHUB_OUTPUT" - - if: steps.prs.outputs.numbers != '' - uses: actions/checkout@v7 - if: steps.prs.outputs.numbers != '' uses: actions/labeler@v7 with: pr-number: ${{ steps.prs.outputs.numbers }} + - run: mv "$RUNNER_TEMP/area-next.json" "$RUNNER_TEMP/area-state.json" + - uses: actions/cache/save@v6 + with: + path: ${{ runner.temp }}/area-state.json + key: area-labels-${{ github.run_id }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b9963ffe59..cab59565e5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,9 +9,24 @@ on: workflow_dispatch: inputs: pr: - description: Pull request number to build + description: Pull request numbers to build together, separated by spaces or commas (up to 40) required: true type: string + linux: + description: Also build and test on Linux + 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 @@ -39,28 +54,132 @@ jobs: pr: ${{ steps.gate.outputs.pr }} head: ${{ steps.gate.outputs.head }} relinker: ${{ steps.gate.outputs.relinker }} + 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('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 }); @@ -68,45 +187,84 @@ jobs: throw new Error('Only collaborators with write access can start or rerun builds'); } } - if (!/^[1-9][0-9]*$/.test(process.env.PR_NUMBER)) { - throw new Error('Enter a positive pull request number'); + if (!/^[1-9][0-9]*([ ,]+[1-9][0-9]*){0,39}$/.test(process.env.PR_NUMBER.trim())) { + throw new Error('Enter up to 40 positive pull request numbers separated by spaces or commas'); } } - const pull_number = manual ? Number(process.env.PR_NUMBER) : context.payload.pull_request.number; - if (!Number.isSafeInteger(pull_number) || pull_number < 1) { + const numbers = manual + ? [...new Set(process.env.PR_NUMBER.trim().split(/[ ,]+/).map(Number))] + : [context.payload.pull_request.number]; + if (numbers.some((pull_number) => !Number.isSafeInteger(pull_number) || pull_number < 1)) { throw new Error('Invalid pull request number'); } - let { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number }); - for (let attempt = 0; pr.mergeable === null && attempt < 10; ++attempt) { - await new Promise((resolve) => setTimeout(resolve, 3000)); - ({ data: pr } = await github.rest.pulls.get({ owner, repo, pull_number })); + const pulls = []; + const paths = []; + let truncated = false; + for (const pull_number of numbers) { + let { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number }); + for (let attempt = 0; pr.mergeable === null && attempt < 10; ++attempt) { + await new Promise((resolve) => setTimeout(resolve, 3000)); + ({ data: pr } = await github.rest.pulls.get({ owner, repo, pull_number })); + } + if (!manual && pr.state !== 'open') { + await core.summary.addRaw(`PR #${pull_number} was ${pr.merged ? 'merged' : 'closed'} before this run started: nothing to build`).write(); + return; + } + if (pr.state !== 'open' || pr.base.ref !== 'main') { + throw new Error(`Pull request #${pull_number} must be open and target main`); + } + if (!manual && pr.head.sha !== context.payload.pull_request.head.sha) { + throw new Error('The pull request has newer commits; use the latest run'); + } + if (pr.mergeable === false) { + throw new Error(`Pull request #${pull_number} does not merge cleanly into main; rebase it first`); + } + 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 }); } - if (pr.state !== 'open' || pr.base.ref !== 'main') { - throw new Error('The pull request must be open and target main'); - } - if (!manual && pr.head.sha !== context.payload.pull_request.head.sha) { - throw new Error('The pull request has newer commits; use the latest run'); - } - if (pr.mergeable === false) { - throw new Error('The pull request does not merge cleanly into main; rebase it first'); - } - let relinker = manual; - if (!manual) { + 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: pr.user.login, per_page: 1 + 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', String(pull_number)); - core.setOutput('head', pr.head.sha); + core.setOutput('pr', pulls.map((pull) => pull.number).join(' ')); + core.setOutput('head', pulls.map((pull) => pull.head).join(' ')); core.setOutput('relinker', relinker); - core.setOutput('full', manual); - core.info(`PR #${pull_number} at ${pr.head.sha} merged into main at ${main.commit.sha}`); + core.setOutput('register-context', registerContext); + core.setOutput('write-watch', writeWatch); + 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( - `PR #${pull_number} at ${pr.head.sha} merged into main at ${main.commit.sha}: full builds ${manual ? 'enabled' : 'disabled'}, ` + - `relinker ${relinker ? 'enabled' : 'skipped because 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(); relinker: @@ -117,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 @@ -142,9 +294,15 @@ jobs: PR_NUMBER: ${{ needs.eligibility.outputs.pr }} PR_HEAD: ${{ needs.eligibility.outputs.head }} run: | - git fetch --no-tags --unshallow origin "refs/pull/$PR_NUMBER/head" - if [ "$(git rev-parse FETCH_HEAD)" != "$PR_HEAD" ]; then echo "::error::The pull request has newer commits; start a new build"; exit 1; fi - if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit FETCH_HEAD; then echo "::error::The pull request does not merge cleanly into main; rebase it first"; exit 1; fi + read -ra numbers <<< "$PR_NUMBER" + read -ra heads <<< "$PR_HEAD" + refspecs=() + for number in "${numbers[@]}"; do refspecs+=("+refs/pull/$number/head:refs/remotes/pull/$number"); done + git fetch --no-tags --unshallow origin "${refspecs[@]}" + for i in "${!numbers[@]}"; do + if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi + if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi + done - if: runner.os == 'Windows' id: toolchain uses: actions/cache@v6 @@ -157,11 +315,44 @@ 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 + if: ${{ needs.eligibility.outputs.register-context == 'true' }} strategy: fail-fast: false matrix: @@ -169,23 +360,11 @@ jobs: configuration: [Debug, Release] runs-on: ${{ matrix.os }} timeout-minutes: 5 - steps: - - uses: actions/checkout@v7 - - 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 - - linux: - needs: eligibility - if: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && needs.eligibility.outputs.full == 'true' }} - runs-on: ubuntu-latest - timeout-minutes: 30 steps: - uses: actions/checkout@v7 with: ref: ${{ needs.eligibility.outputs.sha }} persist-credentials: false - submodules: true - name: Merge the pull request into main if: ${{ needs.eligibility.outputs.head != '' }} shell: bash @@ -193,27 +372,181 @@ jobs: PR_NUMBER: ${{ needs.eligibility.outputs.pr }} PR_HEAD: ${{ needs.eligibility.outputs.head }} run: | - git fetch --no-tags --unshallow origin "refs/pull/$PR_NUMBER/head" - if [ "$(git rev-parse FETCH_HEAD)" != "$PR_HEAD" ]; then echo "::error::The pull request has newer commits; start a new build"; exit 1; fi - if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit FETCH_HEAD; then echo "::error::The pull request does not merge cleanly into main; rebase it first"; exit 1; fi - git submodule update --init --recursive - - uses: hendrikmuhs/ccache-action@v1.2.24 + 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 + + write-watch: + needs: eligibility + if: ${{ needs.eligibility.outputs.write-watch == 'true' }} + strategy: + fail-fast: false + matrix: + os: [macos-15-intel, macos-15] + runs-on: ${{ matrix.os }} + timeout-minutes: 5 + steps: + - uses: actions/checkout@v7 with: - key: linux - save: false - evict-old-files: job - - name: Install lavapipe for the GPU execution tests - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libvulkan1 mesa-vulkan-drivers - - run: cmake -S . -B build $CONFIGURE -DANYPS5_ENABLE_SPIRV_TOOLS=ON + 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 + + linux: + needs: eligibility + if: ${{ (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.linux)) && needs.eligibility.outputs.full == 'true' }} + 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: + ref: ${{ needs.eligibility.outputs.sha }} + persist-credentials: false + submodules: recursive + - 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 + 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 + - name: Prepare persistent caches + run: | + 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 + - 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 - run: python3 tools/check_skips.py build/ctest.xml + - name: Write the test graph of main + if: ${{ !cancelled() && github.event_name == 'push' && steps.build.outcome == 'success' && steps.libs.outcome == 'success' }} + continue-on-error: true + run: python3 tools/select_tests.py dump build test-graph.json + - if: ${{ !cancelled() && github.event_name == 'push' && hashFiles('test-graph.json') != '' }} + continue-on-error: true + uses: actions/cache/save@v6 + with: + path: test-graph.json + key: test-graph-${{ runner.os }}-${{ github.sha }} + - name: Report compiler cache statistics + if: ${{ !cancelled() }} + run: ccache --show-stats - name: Report tests slower than 30 s if: ${{ !cancelled() }} shell: bash @@ -230,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 @@ -239,7 +571,6 @@ jobs: with: ref: ${{ needs.eligibility.outputs.sha }} persist-credentials: false - submodules: true - name: Merge the pull request into main if: ${{ needs.eligibility.outputs.head != '' }} shell: bash @@ -247,10 +578,44 @@ jobs: PR_NUMBER: ${{ needs.eligibility.outputs.pr }} PR_HEAD: ${{ needs.eligibility.outputs.head }} run: | - git fetch --no-tags --unshallow origin "refs/pull/$PR_NUMBER/head" - if [ "$(git rev-parse FETCH_HEAD)" != "$PR_HEAD" ]; then echo "::error::The pull request has newer commits; start a new build"; exit 1; fi - if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit FETCH_HEAD; then echo "::error::The pull request does not merge cleanly into main; rebase it first"; exit 1; fi - git submodule update --init --recursive + 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 + - 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: @@ -263,21 +628,74 @@ jobs: - run: echo 'C:\winlibs\mingw64\bin' >> "$GITHUB_PATH" - uses: hendrikmuhs/ccache-action@v1.2.24 with: - key: windows - save: false + 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% + 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 @@ -285,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/conflict-label.yml b/.github/workflows/conflict-label.yml index d6183c9eb8..0231a79c0d 100644 --- a/.github/workflows/conflict-label.yml +++ b/.github/workflows/conflict-label.yml @@ -9,6 +9,10 @@ on: permissions: {} +concurrency: + group: conflict-label + cancel-in-progress: true + jobs: label: runs-on: ubuntu-latest diff --git a/.github/workflows/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 7ac7a0c109..299adbcdb8 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -24,10 +24,13 @@ jobs: force_label() { gh label create "$1" --color "$2" --description "$3" --force; } force_label 'needs rebase' b60205 'Conflicts with main, rebase needed' - force_label 'intake paused' d93f0b 'Closed while PR intake is limited to authors with at least 20 PRs merged into main' + force_label 'open PR limit' d93f0b 'Closed over the open PR limit: 1 without merged PRs, 20 with at least 1, none from 20' 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,3 +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 new file mode 100644 index 0000000000..0dc28284b0 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,56 @@ +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] + workflow_run: + workflows: [Release] + types: [completed] + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + pages: + if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/checkout@v7 + with: + ref: main + fetch-depth: 0 + - name: Download matching release converter and libraries + env: + GITHUB_TOKEN: ${{ github.token }} + 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 + with: + path: build-wasm/web + - id: deployment + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/pr-intake.yml b/.github/workflows/pr-intake.yml index a632bd61c9..3225af812b 100644 --- a/.github/workflows/pr-intake.yml +++ b/.github/workflows/pr-intake.yml @@ -3,7 +3,7 @@ name: PR intake on: pull_request_target: branches: [main] - types: [opened] + types: [opened, reopened] permissions: {} @@ -27,7 +27,27 @@ jobs: if (pr.state !== 'open' || pr.base.ref !== 'main') { return; } - const minimumMergedPRs = 20; + 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 @@ -35,10 +55,23 @@ jobs: if (mergedPRs.incomplete_results || !Number.isSafeInteger(mergedPRs.total_count) || mergedPRs.total_count < 0) { throw new Error('Could not reliably count merged pull requests'); } - if (mergedPRs.total_count >= minimumMergedPRs) { + const merged = mergedPRs.total_count; + if (merged >= 20) { return; } - const name = 'intake paused'; + const allowedOpen = merged >= 1 ? 20 : 1; + const openPRs = await github.paginate(github.rest.pulls.list, { + owner, repo, state: 'open', base: 'main', per_page: 100 + }); + 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; + } + const name = 'open PR limit'; try { await github.rest.issues.getLabel({ owner, repo, name }); } catch (error) { @@ -48,7 +81,7 @@ jobs: try { await github.rest.issues.createLabel({ owner, repo, name, color: 'd93f0b', - description: 'Closed while PR intake is limited to authors with at least 20 PRs merged into main' + description: 'Closed over the open PR limit: 1 without merged PRs, 20 with at least 1, none from 20' }); } catch (createError) { if (createError.status !== 422) { @@ -58,7 +91,7 @@ jobs: } } await github.rest.issues.addLabels({ owner, repo, issue_number: pull_number, labels: [name] }); - const body = 'Thank you for your interest in the project! We are sorry, but we cannot accept more PRs while we work through the existing backlog. This PR has been automatically closed and labeled "intake paused". This is not a judgment of the quality of your changes.'; + const body = 'Thank you for your contribution! Authors with no pull requests merged into `main` can have 1 open pull request at a time, authors with at least 1 merged can have 20, and from 20 merged there is no limit. This PR has been automatically closed and labeled "open PR limit" because that limit was reached. This is not a judgment of the quality of your changes: reopen it once one of your open pull requests is merged or closed.'; const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: pull_number, per_page: 100 }); 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 1e181a5f65..40a008035f 100644 --- a/.github/workflows/pr-overlap.yml +++ b/.github/workflows/pr-overlap.yml @@ -22,20 +22,36 @@ jobs: - uses: actions/checkout@v7 with: fetch-depth: 0 + - uses: actions/cache/restore@v6 + with: + path: ${{ runner.temp }}/overlap-state.json + key: pr-overlap-${{ github.run_id }} + restore-keys: pr-overlap- - env: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} run: | out=$RUNNER_TEMP/overlap + state=$RUNNER_TEMP/overlap-state.json python3 tools/pr_overlap.py --base "${{ github.event.repository.default_branch }}" --out "$out" + [ -s "$state" ] || echo '{}' > "$state" + record() { jq --arg pr "$1" --arg h "$2" '.[$pr] = $h' "$state" > "$state.new" && mv "$state.new" "$state"; } + shopt -s nullglob marker='' + 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=$(unpinned < "$file" | sha256sum | cut -d' ' -f1) + prev= + [ "$(jq -r --arg pr "$pr" '.[$pr] // empty' "$state")" = "$hash" ] && continue + prev=$pr comment=$(gh api "repos/$GH_REPO/issues/$pr/comments" --paginate \ --jq ".[] | select(.user.login == \"github-actions[bot]\" and (.body | startswith(\"$marker\"))) | [.id, .body] | @json" | head -n1) id=$(jq -r '.[0] // empty' <<< "${comment:-null}") 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 @@ -45,3 +61,11 @@ jobs: gh api -X DELETE "repos/$GH_REPO/issues/comments/$id" fi done + [ -z "$prev" ] || record "$prev" "$hash" + reports=$(for file in "$out"/*.md; do basename "$file" .md; done | jq -R . | jq -sc .) + jq -c --argjson keep "$reports" 'with_entries(select(.key | IN($keep[])))' "$state" > "$state.new" && mv "$state.new" "$state" + - if: always() + uses: actions/cache/save@v6 + with: + path: ${{ runner.temp }}/overlap-state.json + key: pr-overlap-${{ github.run_id }} diff --git a/.github/workflows/progress.yml b/.github/workflows/progress.yml deleted file mode 100644 index 93e9f06000..0000000000 --- a/.github/workflows/progress.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Progress - -on: - push: - branches: [main] - workflow_dispatch: - -permissions: - contents: read - pages: write - id-token: write - -concurrency: - group: pages - cancel-in-progress: false - -jobs: - progress: - runs-on: ubuntu-latest - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - steps: - - uses: actions/checkout@v7 - - run: python3 tools/progress.py out - - uses: actions/upload-pages-artifact@v5 - with: - path: out - - id: deployment - uses: actions/deploy-pages@v5 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 522ada2fdb..cff3998f8c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,7 +41,17 @@ jobs: steps: - uses: actions/checkout@v7 with: - submodules: true + 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 @@ -58,6 +68,9 @@ jobs: with: key: ${{ matrix.platform }} save: false + - name: Install lavapipe for the GPU execution tests and the audio development files + if: matrix.platform == 'linux' + run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libvulkan1 mesa-vulkan-drivers libpipewire-0.3-dev libpulse-dev libasound2-dev libxrandr-dev libwayland-dev libxkbcommon-dev libegl-dev libdecor-0-dev - name: Configure Linux if: matrix.platform == 'linux' run: cmake -S . -B build $CONFIGURE @@ -74,7 +87,10 @@ 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 - name: Run tests (Windows) if: matrix.platform == 'windows' shell: cmd @@ -95,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: @@ -107,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-* @@ -115,12 +162,17 @@ 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 chmod +x "dist/relinker-$RELEASE_TAG" + - name: Prepare release notes + shell: bash + run: | + gh api --method POST "repos/$GH_REPO/releases/generate-notes" -f tag_name="$RELEASE_TAG" > "$RUNNER_TEMP/release-notes.json" + python tools/prepare_release_notes.py --input "$RUNNER_TEMP/release-notes.json" --body "$RUNNER_TEMP/release-body.md" --full dist/release-notes.md - name: Create draft and upload files - run: gh release create "$RELEASE_TAG" dist/* --verify-tag --draft --title "$RELEASE_TAG" --generate-notes + run: gh release create "$RELEASE_TAG" dist/* --verify-tag --draft --title "$RELEASE_TAG" --notes-file "$RUNNER_TEMP/release-body.md" - name: Publish complete release run: gh release edit "$RELEASE_TAG" --draft=false diff --git a/.github/workflows/stale-conflicts.yml b/.github/workflows/stale-conflicts.yml new file mode 100644 index 0000000000..00150e1b70 --- /dev/null +++ b/.github/workflows/stale-conflicts.yml @@ -0,0 +1,36 @@ +name: Stale conflicts + +on: + schedule: + - cron: '30 3 * * *' + workflow_dispatch: + +permissions: {} + +jobs: + warn: + runs-on: ubuntu-latest + permissions: + 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 + days-before-issue-stale: -1 + days-before-issue-close: -1 + days-before-pr-stale: 7 + days-before-pr-close: -1 + stale-pr-label: stale + stale-pr-message: 'This pull request has conflicted with `main` and had no activity for 7 days. Rebase it on current `main` so it can be reviewed and merged. It will not be closed automatically.' + remove-pr-stale-when-updated: true + exempt-draft-pr: true + operations-per-run: 200 diff --git a/.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/.gitmodules b/.gitmodules index fc2e48631c..64a98a2f94 100644 --- a/.gitmodules +++ b/.gitmodules @@ -28,6 +28,27 @@ [submodule "3rdparty/stb"] path = 3rdparty/stb url = https://github.com/nothings/stb.git +[submodule "3rdparty/tracy"] + path = 3rdparty/tracy + url = https://github.com/wolfpld/tracy.git +[submodule "3rdparty/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 +[submodule "3rdparty/libusb-cmake"] + path = 3rdparty/libusb-cmake + url = https://github.com/libusb/libusb-cmake.git +[submodule "3rdparty/mbedtls"] + path = 3rdparty/mbedtls + url = https://github.com/Mbed-TLS/mbedtls.git +[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 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/libusb-cmake b/3rdparty/libusb-cmake new file mode 160000 index 0000000000..bea6567b63 --- /dev/null +++ b/3rdparty/libusb-cmake @@ -0,0 +1 @@ +Subproject commit bea6567b63796ab27c1c33257d230284fb2d8316 diff --git a/3rdparty/mbedtls b/3rdparty/mbedtls new file mode 160000 index 0000000000..068ff080b3 --- /dev/null +++ b/3rdparty/mbedtls @@ -0,0 +1 @@ +Subproject commit 068ff080b369adfac81509f9b57b2afabaf82dc5 diff --git a/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/tracy b/3rdparty/tracy new file mode 160000 index 0000000000..30997d5ca6 --- /dev/null +++ b/3rdparty/tracy @@ -0,0 +1 @@ +Subproject commit 30997d5ca6bb632cc10807a1da8a6d3de0aeeb3c diff --git a/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/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 4d9ff089fc..9eaa66c8db 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,21 +1,52 @@ cmake_minimum_required(VERSION 3.20) +if(CMAKE_HOST_APPLE AND NOT ANYPS5_RELINKER_ONLY) + set(CMAKE_APPLE_SILICON_PROCESSOR x86_64 CACHE STRING "Host processor CMake reports on Apple silicon") + set(CMAKE_OSX_ARCHITECTURES x86_64 CACHE STRING "Architectures built on macOS") +endif() project(Relinker CXX) set(ANYPS5_TEST_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tests") +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) option(ANYPS5_RELINKER_ONLY "Build the relinker without system libraries or third-party dependencies" OFF) +option(ANYPS5_BUILD_WASM "Build the browser relinker" OFF) +if(ANYPS5_BUILD_WASM) + if(NOT EMSCRIPTEN) + message(FATAL_ERROR "ANYPS5_BUILD_WASM requires emcmake and Emscripten") + endif() + if(BUILD_TESTING) + message(FATAL_ERROR "Use the native build for C++ tests and the browser tests for WASM") + endif() + set(ANYPS5_RELINKER_CLI OFF CACHE BOOL "Build the native relinker command line" FORCE) + add_subdirectory(core/relinker) + return() +endif() + if(BUILD_TESTING) enable_testing() if(MINGW) 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}") @@ -24,37 +55,212 @@ if(BUILD_TESTING) endif() option(ANYPS5_ENABLE_SPIRV_TOOLS "Enable SPIRV-Tools validation and optimization" OFF) +option(ANYPS5_ENABLE_ASAN "Build Linux targets with AddressSanitizer and use an ASan-compatible guest arena range" OFF) option(APS5_ENABLE_TIMING_LOG "Write frame timings asynchronously to frame-timing.log" OFF) +option(APS5_ENABLE_TRACY "Build the Tracy profiler client and report performance timer scopes as Tracy zones" OFF) +option(ANYPS5_USE_SYSTEM_DEPS "Resolve SDL2, freetype, Vulkan-Headers, SPIRV-Headers, glslang and SPIRV-Tools from the system instead of the bundled submodules" OFF) add_compile_definitions(APS5_ENABLE_TIMING_LOG=$) +include(cmake/SystemDeps.cmake) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -if(NOT APPLE) +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") + endif() + if(NOT CMAKE_CXX_COMPILER_ID MATCHES "^(GNU|Clang)$") + message(FATAL_ERROR "ANYPS5_ENABLE_ASAN requires GCC or Clang") + endif() + add_compile_options(-fsanitize=address -fno-omit-frame-pointer) + add_link_options(-fsanitize=address) + add_compile_definitions(ANYPS5_ENABLE_ASAN=1) +elseif(NOT APPLE) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static -static-libgcc -static-libstdc++") endif() +if(APPLE AND NOT ANYPS5_RELINKER_ONLY) + add_compile_options($<$:-fexperimental-library>) + add_link_options(-fexperimental-library) +endif() + if(BUILD_TESTING) + add_test_executable(shader_cache_file_read_tests + ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/tests/CacheFileReadTests.cpp) + target_include_directories(shader_cache_file_read_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler) + add_test(NAME shader_cache_file_read COMMAND shader_cache_file_read_tests) + set_tests_properties(shader_cache_file_read PROPERTIES TIMEOUT 20) + add_test_executable(request_serializer_tests + ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/tests/RequestSerializerTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp) + target_include_directories(request_serializer_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler + ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/ControlFlow/include) + add_test(NAME request_serializer COMMAND request_serializer_tests) + set_tests_properties(request_serializer PROPERTIES TIMEOUT 20) find_package(Python3 QUIET COMPONENTS Interpreter) + 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} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress.py) add_test(NAME progress_encoding COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress_encoding.py) + add_test(NAME progress_render_sliver COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress_render_sliver.py) + add_test(NAME progress_wrappers COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress_wrappers.py) + add_test(NAME progress_read_encoding COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress_read_encoding.py) add_test(NAME import_audit COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_import_audit.py) - add_test(NAME conventions_function_bodies COMMAND ${Python3_EXECUTABLE} - ${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} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_debt_split.py) + add_test(NAME stubgen_stdout_newlines COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_stubgen_stdout_newlines.py) add_test(NAME hardware_oracle COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/hw-oracle/test_hw_oracle.py) + add_test(NAME 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() @@ -63,13 +269,17 @@ if(ANYPS5_RELINKER_ONLY) return() endif() +if(APPLE AND NOT CMAKE_OSX_ARCHITECTURES STREQUAL "x86_64") + message(FATAL_ERROR "The full build on macOS is x86-64 only; configure without CMAKE_OSX_ARCHITECTURES or set it to x86_64") +endif() + set(SDL_SHARED OFF CACHE BOOL "" FORCE) set(SDL_STATIC ON CACHE BOOL "" FORCE) set(SDL_TEST OFF CACHE BOOL "" FORCE) set(SDL_VIDEO ON CACHE BOOL "" FORCE) set(SDL_RENDER OFF CACHE BOOL "" FORCE) set(SDL_X11 ON CACHE BOOL "" FORCE) -set(SDL_WAYLAND OFF CACHE BOOL "" FORCE) +set(SDL_WAYLAND ON CACHE BOOL "" FORCE) set(SDL_OPENGL OFF CACHE BOOL "" FORCE) set(SDL_OPENGLES OFF CACHE BOOL "" FORCE) set(SDL_VULKAN ON CACHE BOOL "" FORCE) @@ -78,10 +288,28 @@ set(SDL_HAPTIC OFF CACHE BOOL "" FORCE) set(SDL_HIDAPI ON CACHE BOOL "" FORCE) set(SDL_SENSOR OFF CACHE BOOL "" FORCE) set(SDL_STATIC_PIC ON CACHE BOOL "" FORCE) -set(ANYPS5_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") -set(CMAKE_EXE_LINKER_FLAGS "") -add_subdirectory(3rdparty/SDL2) -set(CMAKE_EXE_LINKER_FLAGS "${ANYPS5_EXE_LINKER_FLAGS}") +if(NOT ANYPS5_USE_SYSTEM_DEPS) + set(ANYPS5_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") + set(CMAKE_EXE_LINKER_FLAGS "") + add_subdirectory(3rdparty/SDL2) + set(CMAKE_EXE_LINKER_FLAGS "${ANYPS5_EXE_LINKER_FLAGS}") + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + get_directory_property(ANYPS5_SDL_X11_XRANDR DIRECTORY 3rdparty/SDL2 DEFINITION HAVE_X11_XRANDR) + if(NOT ANYPS5_SDL_X11_XRANDR) + message(FATAL_ERROR "SDL has no XRandR support, so X11 display refresh rates cannot be queried. " + "Install libxrandr-dev (Debian, Ubuntu) or libXrandr-devel (Fedora), " + "enable SDL_X11_XRANDR, then rerun configuration.") + endif() + get_directory_property(ANYPS5_SDL_PIPEWIRE DIRECTORY 3rdparty/SDL2 DEFINITION HAVE_PIPEWIRE) + get_directory_property(ANYPS5_SDL_PULSEAUDIO DIRECTORY 3rdparty/SDL2 DEFINITION HAVE_PULSEAUDIO) + get_directory_property(ANYPS5_SDL_ALSA DIRECTORY 3rdparty/SDL2 DEFINITION HAVE_ALSA) + if(NOT ANYPS5_SDL_PIPEWIRE AND NOT ANYPS5_SDL_PULSEAUDIO AND NOT ANYPS5_SDL_ALSA) + message(FATAL_ERROR "SDL found no PipeWire, PulseAudio or ALSA development files, so audio output would be silent. " + "Install libpipewire-0.3-dev, libpulse-dev and libasound2-dev (Debian, Ubuntu) " + "or pipewire-devel, pulseaudio-libs-devel and alsa-lib-devel (Fedora), then rerun configuration.") + endif() + endif() +endif() set(ARCHITECTURE x86_64) add_subdirectory(3rdparty/ffmpeg-core) if(MINGW) @@ -90,18 +318,50 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") target_link_options(ffmpeg INTERFACE LINKER:--exclude-libs,ALL) endif() +if(APS5_ENABLE_TRACY) + include(cmake/Tracy.cmake) +endif() + set(FT_DISABLE_ZLIB ON CACHE BOOL "" FORCE) set(FT_DISABLE_BZIP2 ON CACHE BOOL "" FORCE) set(FT_DISABLE_PNG ON CACHE BOOL "" FORCE) set(FT_DISABLE_HARFBUZZ ON CACHE BOOL "" FORCE) set(FT_DISABLE_BROTLI ON CACHE BOOL "" FORCE) set(SKIP_INSTALL_ALL ON CACHE BOOL "" FORCE) -add_subdirectory(3rdparty/freetype EXCLUDE_FROM_ALL) -set_target_properties(freetype PROPERTIES POSITION_INDEPENDENT_CODE ON) +if(NOT ANYPS5_USE_SYSTEM_DEPS) + add_subdirectory(3rdparty/freetype EXCLUDE_FROM_ALL) + set_target_properties(freetype PROPERTIES POSITION_INDEPENDENT_CODE ON) + if(WIN32) + get_target_property(freetypeSources freetype SOURCES) + list(FILTER freetypeSources EXCLUDE REGEX "ftver\\.rc$") + set_target_properties(freetype PROPERTIES SOURCES "${freetypeSources}") + endif() +endif() + +set(LIBUSB_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) +set(LIBUSB_INSTALL_TARGETS OFF CACHE BOOL "" FORCE) +set(LIBUSB_BUILD_TESTING OFF CACHE BOOL "" FORCE) +set(LIBUSB_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) +set(LIBUSB_ENABLE_UDEV OFF CACHE BOOL "" FORCE) +add_subdirectory(3rdparty/libusb-cmake EXCLUDE_FROM_ALL) +set_target_properties(usb-1.0 PROPERTIES POSITION_INDEPENDENT_CODE ON) + +include(cmake/ZArchive.cmake) if(WIN32) - get_target_property(freetypeSources freetype SOURCES) - list(FILTER freetypeSources EXCLUDE REGEX "ftver\\.rc$") - set_target_properties(freetype PROPERTIES SOURCES "${freetypeSources}") + 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) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8fb6826275..f6668eb80b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,13 +5,14 @@ - 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). -- A silent stub is allowed only when it unblocks a title and only affects the UI; add it to [silent stubs](docs/dev/TechnicalDebt.md#silent-stubs). +- 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. +- 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. - Don't add tests that only check that a symbol is exported: a missing export already fails at startup. - Each test finishes in 30 s on the CI runners, Vulkan tests included (Linux runs them on lavapipe), counting the first run with no shader cache. ctest runs in parallel there, so a slow test competes for the cores and times out on some runs and not others. Split or shrink a test that gets close instead of raising its timeout. CI reports every test over 30 s as a warning on the pull request. @@ -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`; full builds never run automatically for pull requests. 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 [TechnicalDebt](docs/dev/TechnicalDebt.md), 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 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: ``` python3 tools/check_conventions.py --base origin/main @@ -50,13 +53,22 @@ python3 tools/check_conventions.py --base origin/main The [pull request template](.github/pull_request_template.md) is the checklist for these rules. - One branch per topic, based on current `main`. Follow-up work goes in a new pull request, not into an open one. +- Open pull requests per author are limited by how many of theirs are merged into `main`: 1 with none merged, 20 with at least 1, no limit from 20. A pull request opened or reopened over the limit is closed automatically; reopen it once another one is merged or closed. - Before starting, check that no open pull request already implements the same functions. - Keep the branch up to date with `main` and resolve conflicts yourself; rebasing and force-pushing is fine. -- If a pull request needs another one first, say so in the description (`Depends on #N`). +- A pull request that conflicts with `main` and has no activity for 7 days is labeled `stale` with a reminder. It is not closed. +- If a pull request needs another one first, say so on the checklist line (`Depends on: #N`; `none` otherwise). +- If a pull request fixes a reported issue, write `Fixes #N` on its checklist line (`none` otherwise). GitHub closes the issue on merge, and the `fix error` label is added within the hour. +- If a pull request fixes a stop or a wrong result seen on a title, name it on the checklist line with the title ID and its [Game bring-up](https://github.com/boykopovar/AnyPS5/discussions/categories/game-bring-up) discussion (`Unblocks: PPSA01325 #3232`; `none` otherwise). Reviewers review these first. 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. 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. +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 5e57cb94ff..5777022fb4 100644 --- a/README.md +++ b/README.md @@ -2,29 +2,33 @@ 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/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 -[![libraries](https://boykopovar.github.io/AnyPS5/badge-libraries.svg)](https://boykopovar.github.io/AnyPS5/) [![shaders](https://boykopovar.github.io/AnyPS5/badge-shaders.svg)](https://boykopovar.github.io/AnyPS5/) +[![libraries](https://boykopovar.github.io/AnyPS5/badge-libraries.svg)](https://boykopovar.github.io/AnyPS5/progress.html) [![shaders](https://boykopovar.github.io/AnyPS5/badge-shaders.svg)](https://boykopovar.github.io/AnyPS5/progress.html) -[![progress map](https://boykopovar.github.io/AnyPS5/progress.svg)](https://boykopovar.github.io/AnyPS5/) +[![progress map](https://boykopovar.github.io/AnyPS5/progress.svg)](https://boykopovar.github.io/AnyPS5/progress.html) + +[Progress history](https://boykopovar.github.io/AnyPS5/history.html) * System libraries: percentage of the functions known to the project so far (declared in [core/libs/prx](core/libs/prx)), not of every PS5 system function. The total grows as more functions are declared. -[List of verified games](docs/user/COMPATIBILITY.md) - -Dreaming Sarah (2D platformer) runs at a stable 60 fps on a GTX 1050 Ti / i5-7500 3.4GHz. - Unsupported or unexpected states strictly throw `std::runtime_error`. `what()` is printed to stderr and the process terminates. The [shader recompiler](core/shader/recompiler/Recompiler.cpp) successfully produces SPIR-V (validated via [Spirv-Tools](3rdparty/SPIRV-Tools) when built with `ANYPS5_ENABLE_SPIRV_TOOLS`). ## Compatibility -See the [game compatibility list](docs/user/COMPATIBILITY.md) for tested games and known issues. +Per-game progress (stage reached, open blockers, libraries and GPU coverage) is tracked on the [blocker map](https://alxcbtw.github.io/anyps5-roadmap/), a community project built from public GitHub data ([source](https://github.com/AlxcBTW/anyps5-roadmap)). Test reports go in the [Game bring-up](https://github.com/boykopovar/AnyPS5/discussions/categories/game-bring-up) discussions, one thread per title. ## Input mapping diff --git a/cmake/SystemDeps.cmake b/cmake/SystemDeps.cmake new file mode 100644 index 0000000000..c679aaab3c --- /dev/null +++ b/cmake/SystemDeps.cmake @@ -0,0 +1,118 @@ +set(ANYPS5_VULKAN_HEADERS_INCLUDE_DIR "${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include") +set(ANYPS5_SPIRV_HEADERS_INCLUDE_DIR "${CMAKE_SOURCE_DIR}/3rdparty/SPIRV-Headers/include") +set(ANYPS5_SDL2_INCLUDE_DIR "${CMAKE_SOURCE_DIR}/3rdparty/SDL2/include") +set(ANYPS5_GLSLANG_INCLUDE_DIRS "${CMAKE_SOURCE_DIR}/3rdparty/glslang") +set(ANYPS5_SPIRV_HEADERS_SOURCE_DIR "${CMAKE_SOURCE_DIR}/3rdparty/SPIRV-Headers") +set(ANYPS5_GLSLANG_SOURCE_DIR "${CMAKE_SOURCE_DIR}/3rdparty/glslang") + +if(NOT ANYPS5_USE_SYSTEM_DEPS) + return() +endif() + +include(CheckCXXSourceCompiles) + +find_package(SDL2 CONFIG REQUIRED) +if(NOT TARGET SDL2::SDL2) + message(FATAL_ERROR "ANYPS5_USE_SYSTEM_DEPS: SDL2 was found but does not provide SDL2::SDL2") +endif() +add_library(SDL2-static INTERFACE) +target_link_libraries(SDL2-static INTERFACE SDL2::SDL2) +get_target_property(ANYPS5_SDL2_INCLUDE_DIR SDL2::SDL2 INTERFACE_INCLUDE_DIRECTORIES) +if(NOT ANYPS5_SDL2_INCLUDE_DIR) + message(FATAL_ERROR "ANYPS5_USE_SYSTEM_DEPS: SDL2::SDL2 exposes no INTERFACE_INCLUDE_DIRECTORIES") +endif() +set(ANYPS5_SDL2_PROBE_INCLUDES ${ANYPS5_SDL2_INCLUDE_DIR}) +foreach(_dir IN LISTS ANYPS5_SDL2_INCLUDE_DIR) + list(APPEND ANYPS5_SDL2_PROBE_INCLUDES "${_dir}/SDL2") +endforeach() +list(REMOVE_DUPLICATES ANYPS5_SDL2_PROBE_INCLUDES) +set(CMAKE_REQUIRED_INCLUDES ${ANYPS5_SDL2_PROBE_INCLUDES}) +check_cxx_source_compiles("#include \"SDL_config.h\"\n#ifndef SDL_JOYSTICK_HIDAPI\n#error SDL_JOYSTICK_HIDAPI is not defined\n#endif\nint main() { return 0; }" ANYPS5_SDL2_HAS_HIDAPI) +unset(CMAKE_REQUIRED_INCLUDES) +if(NOT ANYPS5_SDL2_HAS_HIDAPI) + message(FATAL_ERROR "ANYPS5_USE_SYSTEM_DEPS: the system SDL2 does not define SDL_JOYSTICK_HIDAPI, so a DualSense gets no light bar, touchpad or trigger effects and the disk audio driver does not preserve the requested format; sdl2-compat (SDL3 underneath) and SDL2 builds with HIDAPI disabled both fail the pad and audio tests; install a real SDL2 2.x built with HIDAPI") +endif() + +find_package(Freetype REQUIRED) +if(NOT TARGET Freetype::Freetype) + message(FATAL_ERROR "ANYPS5_USE_SYSTEM_DEPS: freetype was found but does not provide Freetype::Freetype") +endif() +add_library(freetype INTERFACE) +target_link_libraries(freetype INTERFACE Freetype::Freetype) + +find_package(VulkanHeaders CONFIG REQUIRED) +if(NOT TARGET Vulkan::Headers) + message(FATAL_ERROR "ANYPS5_USE_SYSTEM_DEPS: Vulkan-Headers was found but does not provide Vulkan::Headers") +endif() +get_target_property(ANYPS5_VULKAN_HEADERS_INCLUDE_DIR Vulkan::Headers INTERFACE_INCLUDE_DIRECTORIES) +if(NOT ANYPS5_VULKAN_HEADERS_INCLUDE_DIR) + message(FATAL_ERROR "ANYPS5_USE_SYSTEM_DEPS: Vulkan::Headers exposes no INTERFACE_INCLUDE_DIRECTORIES") +endif() +set(CMAKE_REQUIRED_INCLUDES ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) +check_cxx_source_compiles("#include \nint main() { VkPhysicalDeviceMaintenance8FeaturesKHR features{}; (void)features; return 0; }" ANYPS5_VULKAN_HAS_MAINTENANCE8) +unset(CMAKE_REQUIRED_INCLUDES) +if(NOT ANYPS5_VULKAN_HAS_MAINTENANCE8) + message(FATAL_ERROR "ANYPS5_USE_SYSTEM_DEPS: the system Vulkan-Headers do not provide VkPhysicalDeviceMaintenance8FeaturesKHR (VK_KHR_maintenance8), which the agc driver uses; install Vulkan-Headers 1.4.310 or newer") +endif() + +find_package(SPIRV-Headers CONFIG REQUIRED) +if(NOT TARGET SPIRV-Headers::SPIRV-Headers) + message(FATAL_ERROR "ANYPS5_USE_SYSTEM_DEPS: SPIRV-Headers was found but does not provide SPIRV-Headers::SPIRV-Headers") +endif() +get_target_property(ANYPS5_SPIRV_HEADERS_INCLUDE_DIR SPIRV-Headers::SPIRV-Headers INTERFACE_INCLUDE_DIRECTORIES) +if(NOT ANYPS5_SPIRV_HEADERS_INCLUDE_DIR) + message(FATAL_ERROR "ANYPS5_USE_SYSTEM_DEPS: SPIRV-Headers::SPIRV-Headers exposes no INTERFACE_INCLUDE_DIRECTORIES") +endif() + +find_package(glslang CONFIG REQUIRED) +if(NOT TARGET glslang::glslang OR NOT TARGET glslang::SPIRV) + message(FATAL_ERROR "ANYPS5_USE_SYSTEM_DEPS: glslang was found but does not provide glslang::glslang and glslang::SPIRV") +endif() +add_library(glslang INTERFACE) +target_link_libraries(glslang INTERFACE glslang::glslang) +add_library(SPIRV INTERFACE) +target_link_libraries(SPIRV INTERFACE glslang::SPIRV) +find_path(ANYPS5_GLSLANG_PUBLIC_INCLUDE_DIR NAMES glslang/Public/ShaderLang.h) +if(NOT ANYPS5_GLSLANG_PUBLIC_INCLUDE_DIR) + message(FATAL_ERROR "ANYPS5_USE_SYSTEM_DEPS: glslang/Public/ShaderLang.h was not found; the glslang headers are incomplete") +endif() +set(ANYPS5_GLSLANG_SPIRV_INCLUDE_DIR "${ANYPS5_GLSLANG_PUBLIC_INCLUDE_DIR}/glslang") +if(NOT EXISTS "${ANYPS5_GLSLANG_SPIRV_INCLUDE_DIR}/SPIRV/GlslangToSpv.h") + message(FATAL_ERROR "ANYPS5_USE_SYSTEM_DEPS: ${ANYPS5_GLSLANG_SPIRV_INCLUDE_DIR}/SPIRV/GlslangToSpv.h was not found") +endif() +set(ANYPS5_GLSLANG_INCLUDE_DIRS "${ANYPS5_GLSLANG_PUBLIC_INCLUDE_DIR};${ANYPS5_GLSLANG_SPIRV_INCLUDE_DIR}") + +find_program(ANYPS5_GLSLANG_STANDALONE_EXECUTABLE NAMES glslang glslangValidator) +if(NOT ANYPS5_GLSLANG_STANDALONE_EXECUTABLE) + message(FATAL_ERROR "ANYPS5_USE_SYSTEM_DEPS: the glslang compiler (glslang or glslangValidator) was not found; install the glslang tools package") +endif() +add_executable(glslang-standalone IMPORTED GLOBAL) +set_target_properties(glslang-standalone PROPERTIES IMPORTED_LOCATION "${ANYPS5_GLSLANG_STANDALONE_EXECUTABLE}") + +if(ANYPS5_ENABLE_SPIRV_TOOLS) + find_package(SPIRV-Tools-opt CONFIG REQUIRED) + if(NOT TARGET SPIRV-Tools-opt) + message(FATAL_ERROR "ANYPS5_USE_SYSTEM_DEPS: SPIRV-Tools was found but does not provide SPIRV-Tools-opt") + endif() + if(NOT TARGET SPIRV-Tools-static) + if(TARGET SPIRV-Tools) + add_library(SPIRV-Tools-static INTERFACE) + target_link_libraries(SPIRV-Tools-static INTERFACE SPIRV-Tools) + else() + message(FATAL_ERROR "ANYPS5_USE_SYSTEM_DEPS: SPIRV-Tools was found but provides neither SPIRV-Tools-static nor SPIRV-Tools") + endif() + endif() +endif() + +set(ANYPS5_SYSTEM_DEPS_STAMP "") +list(APPEND ANYPS5_SYSTEM_DEPS_PACKAGES VulkanHeaders SPIRV-Headers glslang) +if(ANYPS5_ENABLE_SPIRV_TOOLS) + list(APPEND ANYPS5_SYSTEM_DEPS_PACKAGES SPIRV-Tools-opt) +endif() +foreach(_package IN LISTS ANYPS5_SYSTEM_DEPS_PACKAGES) + set(_version "unknown") + if(DEFINED ${_package}_VERSION) + set(_version "${${_package}_VERSION}") + endif() + string(APPEND ANYPS5_SYSTEM_DEPS_STAMP " ${_package}=${_version}") +endforeach() diff --git a/cmake/Tracy.cmake b/cmake/Tracy.cmake new file mode 100644 index 0000000000..d108607a06 --- /dev/null +++ b/cmake/Tracy.cmake @@ -0,0 +1,21 @@ +set(TRACY_ENABLE ON CACHE BOOL "" FORCE) +set(TRACY_STATIC OFF CACHE BOOL "" FORCE) +set(TRACY_ON_DEMAND ON CACHE BOOL "" FORCE) +set(TRACY_ONLY_LOCALHOST ON CACHE BOOL "" FORCE) +set(TRACY_NO_CRASH_HANDLER ON CACHE BOOL "" FORCE) +set(TRACY_NO_CODE_TRANSFER ON CACHE BOOL "" FORCE) +set(TRACY_NO_SAMPLING ON CACHE BOOL "" FORCE) +set(TRACY_NO_SYSTEM_TRACING ON CACHE BOOL "" FORCE) +set(TRACY_NO_CALLSTACK ON CACHE BOOL "" FORCE) +set(TRACY_NO_FRAME_IMAGE ON CACHE BOOL "" FORCE) +set(TRACY_NO_VERIFY ON CACHE BOOL "" FORCE) +set(CMAKE_DISABLE_FIND_PACKAGE_rocprofiler-sdk ON) +add_subdirectory(${CMAKE_SOURCE_DIR}/3rdparty/tracy ${CMAKE_BINARY_DIR}/3rdparty/tracy EXCLUDE_FROM_ALL) + +if(MINGW) + target_sources(TracyClient PRIVATE ${CMAKE_CURRENT_LIST_DIR}/TracyThreadExit.cpp) + target_compile_definitions(TracyClient PRIVATE TRACY_DELAYED_INIT) + get_target_property(tracyInterfaceDefinitions TracyClient INTERFACE_COMPILE_DEFINITIONS) + list(FILTER tracyInterfaceDefinitions EXCLUDE REGEX "^(WINVER|_WIN32_WINNT)=") + set_target_properties(TracyClient PROPERTIES INTERFACE_COMPILE_DEFINITIONS "${tracyInterfaceDefinitions}") +endif() diff --git a/cmake/TracyThreadExit.cpp b/cmake/TracyThreadExit.cpp new file mode 100644 index 0000000000..704dce3e4f --- /dev/null +++ b/cmake/TracyThreadExit.cpp @@ -0,0 +1,3 @@ +extern "C" int __cxa_thread_atexit(void (*)(void*), void*, void*) { + return 0; +} diff --git a/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/CMakeLists.txt b/core/Decoder/Jpeg/CMakeLists.txt index fcb338b8c0..f16c5df8a6 100644 --- a/core/Decoder/Jpeg/CMakeLists.txt +++ b/core/Decoder/Jpeg/CMakeLists.txt @@ -16,6 +16,12 @@ function(add_jpeg_decoder target runtimeTarget) set(jpeg_turbo_library "${jpeg_turbo_prefix}/lib/libturbojpeg.a") endif() file(MAKE_DIRECTORY "${jpeg_turbo_prefix}/include") + set(jpeg_turbo_platform_args) + if(APPLE) + list(APPEND jpeg_turbo_platform_args + -DCMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES} + -DCMAKE_APPLE_SILICON_PROCESSOR=${CMAKE_APPLE_SILICON_PROCESSOR}) + endif() ExternalProject_Add(jpeg_turbo_external SOURCE_DIR "${PROJECT_SOURCE_DIR}/3rdparty/libjpeg-turbo" BINARY_DIR "${CMAKE_BINARY_DIR}/libjpeg-turbo-build" @@ -38,6 +44,7 @@ function(add_jpeg_decoder target runtimeTarget) -DCMAKE_MAKE_PROGRAM=${CMAKE_MAKE_PROGRAM} -DCMAKE_C_COMPILER_LAUNCHER=${CMAKE_C_COMPILER_LAUNCHER} -DCMAKE_CXX_COMPILER_LAUNCHER=${CMAKE_CXX_COMPILER_LAUNCHER} + ${jpeg_turbo_platform_args} BUILD_BYPRODUCTS "${jpeg_turbo_library}" ) diff --git a/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp b/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp index 13c1d8accc..ed59467ac3 100644 --- a/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp +++ b/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp @@ -35,6 +35,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..e8e6aa19c3 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; @@ -97,12 +135,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 d137df164c..434a589d6c 100644 --- a/core/Decoder/Png/src/Png.cpp +++ b/core/Decoder/Png/src/Png.cpp @@ -138,9 +138,9 @@ std::optional
ParseHeader(std::span png) { while (png.size() - offset >= CHUNK_OVERHEAD) { const std::uint32_t length = readBigEndian32(&png[offset]); const std::uint8_t* type = &png[offset + 4]; + if (length > png.size() - offset - CHUNK_OVERHEAD) break; if (isChunkType(type, "tRNS")) header.hasTransparency = true; if (isChunkType(type, "tRNS") || isChunkType(type, "IDAT") || isChunkType(type, "IEND")) break; - if (length > png.size() - offset - CHUNK_OVERHEAD) break; offset += CHUNK_OVERHEAD + length; } return header; @@ -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/Decoder/Png/tests/Png.cpp b/core/Decoder/Png/tests/Png.cpp index 7287ed5b3f..0c1ac81d1e 100644 --- a/core/Decoder/Png/tests/Png.cpp +++ b/core/Decoder/Png/tests/Png.cpp @@ -153,6 +153,12 @@ int main() { Require(paletteHeader.has_value()); Require(paletteHeader->width == 2 && paletteHeader->height == 2 && paletteHeader->bitDepth == 2); Require(paletteHeader->colorType == Decoder::Png::ColorType::Palette && paletteHeader->hasTransparency); + for (const std::size_t size : {std::size_t{69}, std::size_t{71}}) { + const auto truncated = Decoder::Png::ParseHeader(std::span(PALETTE_TRNS, size)); + Require(truncated.has_value() && !truncated->hasTransparency); + } + const auto completeTransparency = Decoder::Png::ParseHeader(std::span(PALETTE_TRNS, 73)); + Require(completeTransparency.has_value() && completeTransparency->hasTransparency); const auto palette = Decoder::Png::Decode(PALETTE_TRNS); Require(palette.has_value()); Require(palette->pixels == std::vector({255, 0, 0, 255, 0, 255, 0, 0, 0, 0, 255, 255, 255, 255, 0, 128})); diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 08d0155567..2854a5fa08 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -1,8 +1,22 @@ 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() +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + include(CheckPIESupported) + check_pie_supported(LANGUAGES C CXX) + set(CMAKE_POSITION_INDEPENDENT_CODE ON) +endif() + +if(APPLE) + set(ANYPS5_PRX_RPATH "@loader_path") +else() + set(ANYPS5_PRX_RPATH "$ORIGIN") +endif() + add_executable(nid_patcher ${CMAKE_CURRENT_SOURCE_DIR}/nid/src/Sha1.cpp ${CMAKE_CURRENT_SOURCE_DIR}/nid/src/NidCompute.cpp @@ -10,6 +24,7 @@ add_executable(nid_patcher ${CMAKE_CURRENT_SOURCE_DIR}/nid/src/ExportExclusions.cpp ${CMAKE_CURRENT_SOURCE_DIR}/nid/src/ElfNidPatcher.cpp ${CMAKE_CURRENT_SOURCE_DIR}/nid/src/PeNidPatcher.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/nid/src/MachONidPatcher.cpp ${CMAKE_CURRENT_SOURCE_DIR}/nid/src/BinaryPatcherFactory.cpp ${CMAKE_CURRENT_SOURCE_DIR}/nid/src/main.cpp ) @@ -18,7 +33,14 @@ target_include_directories(nid_patcher PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/nid/i target_compile_features(nid_patcher PRIVATE cxx_std_20) if(MINGW) - target_link_options(nid_patcher PRIVATE -static -static-libgcc -static-libstdc++) + target_link_options(nid_patcher PRIVATE -municode -static -static-libgcc -static-libstdc++) + include(CheckCXXSourceCompiles) + set(CMAKE_REQUIRED_FLAGS -fno-asynchronous-unwind-tables) + check_cxx_source_compiles("#ifdef __USING_SJLJ_EXCEPTIONS__\n#error\n#endif\nint main() { return 0; }" ANYPS5_MINGW_DWARF_EXCEPTIONS) + 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.") + endif() add_compile_options(-fno-asynchronous-unwind-tables) endif() @@ -38,6 +60,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) @@ -45,7 +82,14 @@ 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) + target_link_options(libc INTERFACE LINKER:--no-as-needed) +endif() set(UNPATCHED_DIR ${CMAKE_CURRENT_BINARY_DIR}/libs/unpatched) set(PATCHED_DIR ${CMAKE_CURRENT_BINARY_DIR}/libs) @@ -58,11 +102,19 @@ set_target_properties(libc PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${UNPATCHED_DIR} ARCHIVE_OUTPUT_DIRECTORY ${IMPLIB_DIR} BUILD_WITH_INSTALL_RPATH ON - INSTALL_RPATH "$ORIGIN" + INSTALL_RPATH "${ANYPS5_PRX_RPATH}" ) set(LIBC_UNPATCHED ${UNPATCHED_DIR}/libc.prx) 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} @@ -90,10 +142,16 @@ 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} PRIVATE libc) + target_link_libraries(${LIB_NAME} PUBLIC libc) else() - target_link_libraries(${LIB_NAME} PRIVATE libkernel libc) + target_link_libraries(${LIB_NAME} PRIVATE libkernel PUBLIC libc) + endif() + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_link_options(${LIB_NAME} PRIVATE LINKER:-z,now) endif() if(WIN32 AND LIB_NAME STREQUAL "libSceNet") target_link_libraries(${LIB_NAME} PRIVATE ws2_32) @@ -110,7 +168,7 @@ foreach(LIB_DIR ${LIBS_DIRS}) LIBRARY_OUTPUT_DIRECTORY ${UNPATCHED_DIR} ARCHIVE_OUTPUT_DIRECTORY ${IMPLIB_DIR} BUILD_WITH_INSTALL_RPATH ON - INSTALL_RPATH "$ORIGIN" + INSTALL_RPATH "${ANYPS5_PRX_RPATH}" ) set(UNPATCHED_FILE ${UNPATCHED_DIR}/${LIB_NAME}.prx) @@ -119,8 +177,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}" ) @@ -128,35 +186,344 @@ foreach(LIB_DIR ${LIBS_DIRS}) endif() endforeach() +if(APS5_ENABLE_TRACY) + set_target_properties(TracyClient PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${UNPATCHED_DIR} + LIBRARY_OUTPUT_DIRECTORY ${UNPATCHED_DIR} + ARCHIVE_OUTPUT_DIRECTORY ${IMPLIB_DIR} + ) + set(TRACY_CLIENT_COPIED ${CMAKE_CURRENT_BINARY_DIR}/TracyClient.copied) + add_custom_command( + OUTPUT ${TRACY_CLIENT_COPIED} + COMMAND ${CMAKE_COMMAND} -E make_directory ${PATCHED_DIR} + COMMAND ${CMAKE_COMMAND} -E copy $ ${PATCHED_DIR} + COMMAND ${CMAKE_COMMAND} -E touch ${TRACY_CLIENT_COPIED} + DEPENDS TracyClient + COMMENT "Copying the Tracy client next to the patched libraries" + ) + list(APPEND PATCHED_OUTPUTS ${TRACY_CLIENT_COPIED}) +endif() + +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 -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) + add_test_executable(guest_posix_scalar_io_tests tests/GuestPosixScalarIo.cpp) + target_include_directories(guest_posix_scalar_io_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_posix_scalar_io_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_posix_scalar_io_tests) + add_test(NAME guest_posix_scalar_io COMMAND guest_posix_scalar_io_tests) + if(WIN32) + set_tests_properties(guest_posix_scalar_io PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(nid_buffer_validation_tests nid/tests/BufferValidation.cpp) + target_include_directories(nid_buffer_validation_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/nid/include) + target_compile_features(nid_buffer_validation_tests PRIVATE cxx_std_20) + if(MINGW) + target_compile_options(nid_buffer_validation_tests PRIVATE -fasynchronous-unwind-tables) + endif() + add_test(NAME nid_buffer_validation COMMAND nid_buffer_validation_tests) + set_tests_properties(nid_buffer_validation PROPERTIES TIMEOUT 10) + + 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} + ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_native_paths.py $) + set_tests_properties(nid_patcher_native_paths PROPERTIES TIMEOUT 20) + endif() add_test_executable(patched_libraries_tests tests/PatchedLibraries.cpp) add_dependencies(patched_libraries_tests libs) + if(Python3_Interpreter_FOUND) + add_test(NAME nid_pe_references COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_pe_references.py $) + endif() if(MINGW) target_compile_options(patched_libraries_tests PRIVATE -fasynchronous-unwind-tables) target_link_options(patched_libraries_tests PRIVATE -static -static-libgcc -static-libstdc++) 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 $) + endif() add_test_executable(guest_kernel_sys_tests tests/GuestKernelSys.cpp) target_include_directories(guest_kernel_sys_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_kernel_sys_tests PRIVATE libkernel_sys libkernel libc) configure_windows_unwind(guest_kernel_sys_tests) add_test(NAME guest_kernel_sys COMMAND guest_kernel_sys_tests) + if(Python3_Interpreter_FOUND) + add_test(NAME nid_verneed COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_verneed.py $) + endif() if(WIN32) set_tests_properties(guest_kernel_sys PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") 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) + target_include_directories(windows_write_watch_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_compile_definitions(windows_write_watch_tests PRIVATE _WIN32_WINNT=0x0A00) + if(MINGW) + target_compile_options(windows_write_watch_tests PRIVATE -fasynchronous-unwind-tables) + endif() + add_test(NAME windows_write_watch COMMAND windows_write_watch_tests) + set_tests_properties(windows_write_watch PROPERTIES TIMEOUT 20) + endif() + + if(WIN32) + add_test_executable(undeliverable_host_exception_tests tests/UndeliverableHostException.cpp) + add_dependencies(undeliverable_host_exception_tests libc) + add_test(NAME undeliverable_host_exception COMMAND undeliverable_host_exception_tests $) + set_tests_properties(undeliverable_host_exception PROPERTIES + PASS_REGULAR_EXPRESSION "C\\+\\+ exception St9bad_alloc raised on thread [0-9]+ cannot be delivered: 0x[0-9a-f]+ undeliverable_host_exception_tests\\.exe\\+0x[0-9a-f]+ has no unwind data") + endif() + + add_test_executable(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_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) configure_windows_unwind(guest_offline_imports_tests) add_test(NAME guest_offline_imports COMMAND guest_offline_imports_tests) + if(Python3_Interpreter_FOUND) + add_test(NAME nid_shared_strings COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_shared_strings.py $) + endif() if(WIN32) set_tests_properties(guest_offline_imports PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") 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) + configure_windows_unwind(guest_signin_dialog_tests) + add_test(NAME guest_signin_dialog COMMAND guest_signin_dialog_tests) + if(WIN32) + set_tests_properties(guest_signin_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "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 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) + configure_windows_unwind(guest_netctl_ap_dialog_tests) + add_test(NAME guest_netctl_ap_dialog COMMAND guest_netctl_ap_dialog_tests) + if(WIN32) + set_tests_properties(guest_netctl_ap_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(guest_np_auth_tests tests/GuestNpAuth.cpp) + target_include_directories(guest_np_auth_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_np_auth_tests PRIVATE libSceNpAuth libc) + configure_windows_unwind(guest_np_auth_tests) + add_test(NAME guest_np_auth COMMAND guest_np_auth_tests) + if(WIN32) + set_tests_properties(guest_np_auth PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(guest_rudp_status_tests tests/GuestRudpStatus.cpp) + target_include_directories(guest_rudp_status_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_rudp_status_tests PRIVATE libSceRudp libc) + configure_windows_unwind(guest_rudp_status_tests) + add_test(NAME guest_rudp_status COMMAND guest_rudp_status_tests) + if(WIN32) + set_tests_properties(guest_rudp_status PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(guest_rudp_context_queries_tests tests/GuestRudpContextQueries.cpp) + target_include_directories(guest_rudp_context_queries_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_rudp_context_queries_tests PRIVATE libSceRudp libc) + configure_windows_unwind(guest_rudp_context_queries_tests) + add_test(NAME guest_rudp_context_queries COMMAND guest_rudp_context_queries_tests) + if(WIN32) + set_tests_properties(guest_rudp_context_queries PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(guest_rudp_pump_tests tests/GuestRudpPump.cpp) + target_include_directories(guest_rudp_pump_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_rudp_pump_tests PRIVATE libSceRudp libc) + configure_windows_unwind(guest_rudp_pump_tests) + add_test(NAME guest_rudp_pump COMMAND guest_rudp_pump_tests) + if(WIN32) + set_tests_properties(guest_rudp_pump PROPERTIES ENVIRONMENT_MODIFICATION + "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) + configure_windows_unwind(guest_memory_pool_stats_tests) + add_test(NAME guest_memory_pool_stats COMMAND guest_memory_pool_stats_tests) + if(WIN32) + set_tests_properties(guest_memory_pool_stats PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(guest_system_gesture_tests tests/GuestSystemGesture.cpp) + target_include_directories(guest_system_gesture_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_system_gesture_tests PRIVATE libSceSystemGesture libc) + configure_windows_unwind(guest_system_gesture_tests) + add_test(NAME guest_system_gesture COMMAND guest_system_gesture_tests) + if(WIN32) + set_tests_properties(guest_system_gesture PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + if(UNIX AND (CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64)$" OR CMAKE_OSX_ARCHITECTURES STREQUAL "x86_64")) add_subdirectory(tests/register_context) endif() @@ -187,6 +554,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}) @@ -198,6 +577,21 @@ if(BUILD_TESTING) endif() add_test(NAME host_thread_local COMMAND host_thread_local_tests) set_tests_properties(host_thread_local PROPERTIES TIMEOUT 15) + if(MINGW) + add_test(NAME prx_thread_local_destructors COMMAND ${CMAKE_COMMAND} -DNM=${CMAKE_NM} -DOBJDUMP=${CMAKE_OBJDUMP} + -DMODULES=$ -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/ThreadLocalDestructors.cmake) + 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(savedata_core_write_replacement_tests tests/SaveDataWriteReplacement.cpp) target_include_directories(savedata_core_write_replacement_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) @@ -217,6 +611,16 @@ if(BUILD_TESTING) configure_windows_unwind(savedata_replace_file_failure_tests) add_test(NAME savedata_replace_file_failure COMMAND savedata_replace_file_failure_tests) + add_test_executable(guest_crt_tests tests/GuestCrt.cpp) + target_include_directories(guest_crt_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_crt_tests PRIVATE libSceAmpr libc) + configure_windows_unwind(guest_crt_tests) + add_test(NAME guest_crt COMMAND guest_crt_tests) + if(WIN32) + set_tests_properties(guest_crt PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + if(MINGW) target_compile_options(savedata_core_write_replacement_tests PRIVATE -fasynchronous-unwind-tables) target_compile_options(savedata_native_write_replacement_tests PRIVATE -fasynchronous-unwind-tables) @@ -228,4 +632,54 @@ if(BUILD_TESTING) set_tests_properties(savedata_native_write_replacement 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) + configure_windows_unwind(guest_posix_random_tests) + add_test(NAME guest_posix_random COMMAND guest_posix_random_tests) + if(WIN32) + set_tests_properties(guest_posix_random PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(MemoryPoolTest + tests/MemoryPoolTest.cpp + prx/libkernel/DirectMemory/MemoryPool.cpp + ) + target_include_directories(MemoryPoolTest PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + if(MINGW) + target_compile_options(MemoryPoolTest PRIVATE -fasynchronous-unwind-tables) + endif() + add_test(NAME MemoryPoolTest COMMAND MemoryPoolTest) + + add_test_executable(guest_acm_tests tests/GuestAcm.cpp) + target_include_directories(guest_acm_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_acm_tests PRIVATE libSceAcm libc) + configure_windows_unwind(guest_acm_tests) + add_test(NAME guest_acm COMMAND guest_acm_tests) + if(WIN32) + 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() endif() diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 99211616a5..4f8d8e8fd7 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -16,6 +16,23 @@ struct alignas(16) __m128 { float v[4]; }; using Bool = std::uint8_t; +struct SonyLconv { + char* monetary[7]; + char chars[14]; + char pad[2]; + char* decimal_point; + char* grouping; + char* thousands_sep; + char* frac_grouping; + char* frac_sep; + char* false_text; + char* true_text; + char* no_text; + char* yes_text; +}; +static_assert(offsetof(SonyLconv, decimal_point) == 0x48); +static_assert(sizeof(SonyLconv) == 0x90); + using KernelModule = std::int32_t; using KernelCpumask = std::uint64_t; using KernelUseconds = unsigned int; @@ -78,6 +95,14 @@ struct KernelAioRwRequest { std::int32_t fd; }; +struct KernelAioSchedulingParam { + std::int32_t schedulingWindowSize; + std::int32_t delayedCountLimit; + std::uint32_t enableSplit; + std::uint32_t splitSize; + std::uint32_t splitChunkSize; +}; + struct KernelBatchMapEntry { void* start; std::uint64_t offset; @@ -135,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; @@ -255,6 +285,12 @@ struct ModuleInfoForUnwind { std::uint64_t seg0_size; }; +struct InternalSegmentInfo { + std::uint64_t address; + std::uint64_t size; +}; +static_assert(sizeof(InternalSegmentInfo) == 0x10); + struct MallocReplace { std::uint64_t size = sizeof(MallocReplace); void* malloc_initialize = nullptr; @@ -521,6 +557,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; @@ -644,10 +721,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]; @@ -882,6 +962,14 @@ struct VoicePortInfo { std::uint16_t reserved; }; +struct VoiceResourceInfo { + std::uint16_t max_voice_input_ports; + std::uint16_t max_voice_output_ports; + std::uint16_t max_device_input_ports; + std::uint16_t max_device_output_ports; + std::uint16_t max_ports; +}; + struct VoiceStartParam { void* container; std::uint32_t mem_size; @@ -1067,8 +1155,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* out_text, std::uint32_t* out_text_length, const char16_t* source_text, std::uint32_t source_text_length); +using ImeExtKeyboardFilter = int (APS5_VABI *)(const ImeKeycode* source_keycode, std::uint16_t* out_keycode, std::uint32_t* out_status, void* reserved); struct ImeExtendedParam { std::uint32_t option; @@ -1150,7 +1238,7 @@ struct ImeEvent { ImeEventParam param; }; -using EventHandler = void (*)(void* arg, const ImeEvent* event); +using EventHandler = void (APS5_VABI *)(void* arg, const ImeEvent* event); struct Param { std::int32_t user_id; @@ -1234,9 +1322,16 @@ struct NetCtlNatInfo { std::uint8_t opaque[128]; }; union NetCtlInfo { std::uint8_t opaque[256]; }; -using NetCtlCallback = void (*)(int, void*); +struct NetCtlIfStat { + std::uint32_t device; + std::uint64_t tx_bytes; + std::uint64_t rx_bytes; + std::uint32_t reserved[8]; +}; -struct HttpEpoll {}; +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*); @@ -1278,6 +1373,18 @@ struct NpTitleId { char data[13]; char pad[3]; }; struct NpTitleSecret { std::uint8_t data[128]; }; struct NpContentRestriction { std::uint8_t opaque[128]; }; struct NpOnlineId { char data[17]; char pad[3]; }; + +struct SharePlayConnectionInfoA { + std::int32_t status; + std::int32_t mode; + NpOnlineId hostOnlineId; + NpOnlineId visitorOnlineId; + std::uint64_t hostAccountId; + std::uint64_t visitorAccountId; + std::int32_t hostUserId; + std::int32_t visitorUserId; +}; +static_assert(sizeof(SharePlayConnectionInfoA) == 72 && offsetof(SharePlayConnectionInfoA, hostAccountId) == 48 && offsetof(SharePlayConnectionInfoA, hostUserId) == 64); struct NpId { NpOnlineId online_id; std::uint8_t opaque[4]; }; struct NpCreateAsyncRequestParameter { std::uint8_t opaque[64]; }; struct NpCheckPremiumParameter { std::uint8_t opaque[64]; }; @@ -1314,10 +1421,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; @@ -1336,6 +1439,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; @@ -1352,6 +1460,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; @@ -1411,8 +1539,19 @@ struct SaveDataMountInfo { struct SceSaveDataTitleId { char data[10]; char pad[2]; }; struct SceSaveDataDirName { char data[32]; }; -struct SaveDataSearchInfo { std::uint8_t opaque[128]; }; -struct SaveDataMemoryData { void* buf; std::size_t buf_size; std::size_t offset; }; +struct SaveDataSearchInfo { + std::uint64_t blocks; + std::uint64_t free_blocks; + std::uint8_t reserved[32]; +}; +static_assert(sizeof(SaveDataSearchInfo) == 48); +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; @@ -1534,6 +1673,13 @@ struct SaveDataBackup { std::uint8_t reserved[32]; }; +struct SaveDataConvert { + std::int32_t user_id; + std::int32_t pad; + const SceSaveDataDirName* dir_name; + std::uint8_t unknown[48]; +}; + struct AppContentInitParam { char reserved[32]; }; struct AppContentBootParam { @@ -1553,8 +1699,50 @@ struct ContentExportInitParam2 { std::int64_t reserved1; }; +struct ContentExportParam { + char title[257]; + char comment[257]; + char content_type[65]; +}; +static_assert(sizeof(ContentExportParam) == 579); + +struct ContentExportCallbackParam { + std::size_t content_length; + void* func; + void* user_data; +}; + struct ContentSearchInitParam { std::size_t memory_size; }; +struct ContentSearchColumnValue { + std::uint32_t size; + std::int64_t value; +}; + +struct ContentSearchContentColumn { + std::int32_t column_type; + std::int32_t column_operator; + const ContentSearchColumnValue* value; + std::int32_t logical_connector; +}; + +struct ContentSearchContentColumnSet { + const ContentSearchContentColumn* columns; + std::int32_t columns_length; + std::int32_t logical_connector; +}; + +struct ContentSearchContentOrderByCondition { + std::int32_t column_type; + std::int32_t sort_type; +}; + +static_assert(sizeof(ContentSearchColumnValue) == 16); +static_assert(sizeof(ContentSearchContentColumn) == 24); +static_assert(offsetof(ContentSearchContentColumn, logical_connector) == 16); +static_assert(sizeof(ContentSearchContentColumnSet) == 16); +static_assert(sizeof(ContentSearchContentOrderByCondition) == 8); + struct PngEncCreateParam { std::uint32_t this_size; std::uint32_t attribute; @@ -1646,23 +1834,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; }; @@ -1670,8 +1864,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 { @@ -1722,6 +1921,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; @@ -1762,6 +1970,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]; }; @@ -1839,6 +2054,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; @@ -1867,6 +2118,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..8e1ee93454 --- /dev/null +++ b/core/libs/allocator/PeImportFirst.cpp @@ -0,0 +1,93 @@ +#include "PeImportFirst.hpp" + +#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::string& path, const std::string& dll) { + std::vector bytes; + { + std::ifstream input(path, std::ios::binary); + if (!input) throw std::runtime_error("cannot open " + path); + 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); +} + +} diff --git a/core/libs/allocator/PeImportFirst.hpp b/core/libs/allocator/PeImportFirst.hpp new file mode 100644 index 0000000000..30aa18cf7d --- /dev/null +++ b/core/libs/allocator/PeImportFirst.hpp @@ -0,0 +1,14 @@ +#ifndef CORE_LIBS_ALLOCATOR_PEIMPORTFIRST_HPP +#define CORE_LIBS_ALLOCATOR_PEIMPORTFIRST_HPP + +#include +#include + +namespace Allocator { + +void MoveImportFirst(std::vector& image, const std::string& dll); +void MoveImportFirstInFile(const std::string& 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..02ace8e22e --- /dev/null +++ b/core/libs/allocator/PeImportFirstMain.cpp @@ -0,0 +1,18 @@ +#include "PeImportFirst.hpp" + +#include +#include + +int main(int argc, char** argv) { + if (argc != 3) { + std::fprintf(stderr, "usage: pe_import_first \n"); + return 2; + } + try { + Allocator::MoveImportFirstInFile(argv[1], argv[2]); + } catch (const std::exception& error) { + std::fprintf(stderr, "pe_import_first: %s: %s\n", argv[1], error.what()); + return 1; + } + return 0; +} 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..4df8aceed5 --- /dev/null +++ b/core/libs/allocator/tests/PeImportFirstTests.cpp @@ -0,0 +1,156 @@ +#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.string(), "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.string(), "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() { + const auto directory = std::filesystem::temp_directory_path() / ("aps5-pe-import-first-" + 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/include/nid/MachONidPatcher.hpp b/core/libs/nid/include/nid/MachONidPatcher.hpp new file mode 100644 index 0000000000..f49732cd88 --- /dev/null +++ b/core/libs/nid/include/nid/MachONidPatcher.hpp @@ -0,0 +1,30 @@ +#ifndef CORE_LIBS_NID_MACHONIDPATCHER_HPP +#define CORE_LIBS_NID_MACHONIDPATCHER_HPP + +#include +#include +#include +#include + +namespace Nid { + +constexpr std::uint32_t kMachOMagic64 = 0xfeedfacfu; + +struct MachOExport { + std::string Name; + std::uint64_t Flags = 0; + std::uint64_t Address = 0; + std::uint64_t Other = 0; + std::string ImportName; +}; + +std::vector ReadMachOExports(const std::vector& binary); + +class MachONidPatcher final : public IBinaryPatcher { +public: + void PatchNids(std::vector& binary, const std::string& libraryName, const std::unordered_set& excludedExports) const override; +}; + +} + +#endif diff --git a/core/libs/nid/include/nid/NativePath.hpp b/core/libs/nid/include/nid/NativePath.hpp new file mode 100644 index 0000000000..994be15dfa --- /dev/null +++ b/core/libs/nid/include/nid/NativePath.hpp @@ -0,0 +1,15 @@ +#ifndef NID_NATIVEPATH_HPP +#define NID_NATIVEPATH_HPP + +#include +#include + +namespace Nid::Internal { + +inline std::filesystem::path NativePath(const std::string& path) { + return std::filesystem::path(std::u8string(path.begin(), path.end())); +} + +} + +#endif diff --git a/core/libs/nid/include/nid/NidPatcherUtils.hpp b/core/libs/nid/include/nid/NidPatcherUtils.hpp index f467756124..33ecd14f8b 100644 --- a/core/libs/nid/include/nid/NidPatcherUtils.hpp +++ b/core/libs/nid/include/nid/NidPatcherUtils.hpp @@ -61,7 +61,7 @@ inline std::string StripNidPostfix(const std::string& name) { template T Read(const std::vector& buf, std::size_t offset) { - if (offset + sizeof(T) > buf.size()) throw std::runtime_error("read out of bounds"); + if (offset > buf.size() || sizeof(T) > buf.size() - offset) throw std::runtime_error("read out of bounds"); T v; std::memcpy(&v, buf.data() + offset, sizeof(T)); return v; @@ -69,7 +69,7 @@ T Read(const std::vector& buf, std::size_t offset) { template void Write(std::vector& buf, std::size_t offset, const T& v) { - if (offset + sizeof(T) > buf.size()) throw std::runtime_error("write out of bounds"); + if (offset > buf.size() || sizeof(T) > buf.size() - offset) throw std::runtime_error("write out of bounds"); std::memcpy(buf.data() + offset, &v, sizeof(T)); } @@ -78,6 +78,7 @@ inline std::string ReadCStr(const std::vector& buf, std::size_t of std::string s; while (offset < buf.size() && buf[offset] != 0u) s.push_back(static_cast(buf[offset++])); + if (offset == buf.size()) throw std::runtime_error("unterminated cstr"); return s; } diff --git a/core/libs/nid/src/BinaryPatcherFactory.cpp b/core/libs/nid/src/BinaryPatcherFactory.cpp index c11cd304fd..a8ff6f0079 100644 --- a/core/libs/nid/src/BinaryPatcherFactory.cpp +++ b/core/libs/nid/src/BinaryPatcherFactory.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -10,6 +11,8 @@ std::unique_ptr MakePatcher(const std::vector& bin return std::make_unique(); if (binary.size() >= 2 && binary[0] == 'M' && binary[1] == 'Z') return std::make_unique(); + if (binary.size() >= 4 && binary[0] == 0xcf && binary[1] == 0xfa && binary[2] == 0xed && binary[3] == 0xfe) + return std::make_unique(); throw std::runtime_error("unrecognized binary format"); } diff --git a/core/libs/nid/src/ElfNidPatcher.cpp b/core/libs/nid/src/ElfNidPatcher.cpp index 4f341bbd26..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 @@ -15,6 +16,11 @@ namespace Nid { namespace { +void RequireElfRange(const std::vector& elf, std::uint64_t offset, std::uint64_t size, const char* table) { + if (offset > elf.size() || size > elf.size() - offset) + throw std::runtime_error(std::string("invalid ELF ") + table + " range"); +} + std::uint32_t GnuHash(const std::string& name) { std::uint32_t h = 5381u; for (const unsigned char c : name) @@ -22,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; @@ -36,6 +80,18 @@ GnuHashLayout ReadGnuHashLayout(const std::vector& elf, std::size_ return layout; } +void ValidateGnuHash(const std::vector& elf, std::size_t offset, std::size_t size, std::size_t symCount) { + if (offset > elf.size() || size > elf.size() - offset || size < 16u) + throw std::runtime_error("invalid gnu.hash section range"); + const auto layout = ReadGnuHashLayout(elf, offset); + if (layout.SymOffset > symCount || !layout.BloomSize || (layout.BloomSize & (layout.BloomSize - 1u)) || + layout.BloomShift >= 32u || (!layout.NBuckets && layout.SymOffset != symCount)) + throw std::runtime_error("invalid gnu.hash geometry"); + const std::uint64_t required = 16u + std::uint64_t{layout.BloomSize} * 8u + + std::uint64_t{layout.NBuckets} * 4u + (symCount - layout.SymOffset) * 4u; + if (required > size) throw std::runtime_error("gnu.hash section too small to rebuild in place"); +} + void ReorderDynSymForGnuHash( std::vector& elf, std::size_t dynSymOffset, @@ -204,49 +260,65 @@ void RemapDynamicNeededOffsets( } } +struct VersionNameField { + std::size_t Offset; + std::uint32_t Name; +}; + +std::vector ReadVersionNameFields(const std::vector& elf, const Elf64_Ehdr& ehdr) { + using namespace Internal; + std::vector fields; + for (std::uint16_t i = 0u; i < ehdr.e_shnum; ++i) { + const auto section = Read(elf, static_cast(ehdr.e_shoff) + i * sizeof(Elf64_Shdr)); + if (section.sh_type != kShtGnuVerneed || section.sh_size == 0u) continue; + if (section.sh_offset > elf.size() || section.sh_size > elf.size() - section.sh_offset) + throw std::runtime_error("invalid .gnu.version_r section range"); + const auto location = [&](std::uint64_t relative, std::size_t size) { + if (relative > section.sh_size || size > section.sh_size - relative) + throw std::runtime_error(".gnu.version_r record outside section"); + return static_cast(section.sh_offset + relative); + }; + const auto advance = [&](std::uint64_t current, std::uint32_t step, std::size_t size) { + if (step < size || step > section.sh_size - current) + throw std::runtime_error("invalid .gnu.version_r record link"); + return current + step; + }; + std::uint64_t current = 0u; + while (true) { + const auto offset = location(current, sizeof(Elf64_Verneed)); + const auto need = Read(elf, offset); + if (need.vn_version != 1u) throw std::runtime_error("unsupported .gnu.version_r structure version"); + fields.push_back({offset + offsetof(Elf64_Verneed, vn_file), need.vn_file}); + if (need.vn_cnt != 0u) { + auto auxiliary = advance(current, need.vn_aux, sizeof(Elf64_Verneed)); + for (std::uint16_t index = 0u; index < need.vn_cnt; ++index) { + const auto auxOffset = location(auxiliary, sizeof(Elf64_Vernaux)); + const auto aux = Read(elf, auxOffset); + fields.push_back({auxOffset + offsetof(Elf64_Vernaux, vna_name), aux.vna_name}); + if (index + 1u == need.vn_cnt) { + if (aux.vna_next != 0u) throw std::runtime_error(".gnu.version_r auxiliary count mismatch"); + } else { + auxiliary = advance(auxiliary, aux.vna_next, sizeof(Elf64_Vernaux)); + } + } + } + if (need.vn_next == 0u) break; + current = advance(current, need.vn_next, sizeof(Elf64_Verneed)); + } + } + return fields; +} + void RemapVerneedOffsets( std::vector& elf, const Elf64_Ehdr& ehdr, const std::vector>& oldToNewOffset ) { using namespace Internal; - - for (std::uint16_t i = 0u; i < ehdr.e_shnum; ++i) { - const std::size_t shOffset = static_cast(ehdr.e_shoff) + i * sizeof(Elf64_Shdr); - const auto shdr = Read(elf, shOffset); - if (shdr.sh_type != kShtGnuVerneed) continue; - - const std::size_t verneedOffset = static_cast(shdr.sh_offset); - std::size_t entryOffset = verneedOffset; - - while (true) { - const auto verneed = Read(elf, entryOffset); - - const auto mappedFile = std::find_if( - oldToNewOffset.begin(), oldToNewOffset.end(), - [&](const auto& entry) { return entry.first == verneed.vn_file; } - ); - if (mappedFile != oldToNewOffset.end()) - Write(elf, entryOffset + offsetof(Elf64_Verneed, vn_file), mappedFile->second); - - std::size_t auxOffset = entryOffset + verneed.vn_aux; - for (std::uint16_t a = 0u; a < verneed.vn_cnt; ++a) { - const auto vernaux = Read(elf, auxOffset); - - const auto mappedName = std::find_if( - oldToNewOffset.begin(), oldToNewOffset.end(), - [&](const auto& entry) { return entry.first == vernaux.vna_name; } - ); - if (mappedName != oldToNewOffset.end()) - Write(elf, auxOffset + offsetof(Elf64_Vernaux, vna_name), mappedName->second); - - if (vernaux.vna_next == 0u) break; - auxOffset += vernaux.vna_next; - } - - if (verneed.vn_next == 0u) break; - entryOffset += verneed.vn_next; - } + for (const auto& field : ReadVersionNameFields(elf, ehdr)) { + const auto mapped = std::find_if(oldToNewOffset.begin(), oldToNewOffset.end(), + [&](const auto& entry) { return entry.first == field.Name; }); + if (mapped != oldToNewOffset.end()) Write(elf, field.Offset, mapped->second); } } @@ -275,35 +347,8 @@ std::vector CollectDynamicNameOffsets(const std::vector CollectVerneedNameOffsets(const std::vector& elf, const Elf64_Ehdr& ehdr) { - using namespace Internal; - std::vector offsets; - - for (std::uint16_t i = 0u; i < ehdr.e_shnum; ++i) { - const std::size_t shOffset = static_cast(ehdr.e_shoff) + i * sizeof(Elf64_Shdr); - const auto shdr = Read(elf, shOffset); - if (shdr.sh_type != kShtGnuVerneed) continue; - - std::size_t entryOffset = static_cast(shdr.sh_offset); - - while (true) { - const auto verneed = Read(elf, entryOffset); - offsets.push_back(verneed.vn_file); - - std::size_t auxOffset = entryOffset + verneed.vn_aux; - for (std::uint16_t a = 0u; a < verneed.vn_cnt; ++a) { - const auto vernaux = Read(elf, auxOffset); - offsets.push_back(vernaux.vna_name); - - if (vernaux.vna_next == 0u) break; - auxOffset += vernaux.vna_next; - } - - if (verneed.vn_next == 0u) break; - entryOffset += verneed.vn_next; - } - } - + for (const auto& field : ReadVersionNameFields(elf, ehdr)) offsets.push_back(field.Name); return offsets; } @@ -316,9 +361,14 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& if (elf[4] != 2) throw std::runtime_error("not ELF64"); const auto ehdr = Read(elf, 0u); + if (ehdr.e_ident[5] != 1 || ehdr.e_shentsize != sizeof(Elf64_Shdr)) + throw std::runtime_error("unsupported ELF byte order or section header size"); + RequireElfRange(elf, ehdr.e_shoff, std::uint64_t{ehdr.e_shnum} * sizeof(Elf64_Shdr), "section table"); std::size_t dynSymOffset = 0u, dynSymSize = 0u; std::size_t gnuHashOffset = 0u, gnuHashSize = 0u; + 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; @@ -326,6 +376,9 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& const std::size_t shOffset = static_cast(ehdr.e_shoff) + i * sizeof(Elf64_Shdr); const auto shdr = Read(elf, shOffset); if (shdr.sh_type == kShtDynsym) { + if (shdr.sh_entsize != sizeof(Elf64_Sym) || shdr.sh_size % sizeof(Elf64_Sym) || shdr.sh_link >= ehdr.e_shnum) + throw std::runtime_error("invalid ELF dynsym layout"); + RequireElfRange(elf, shdr.sh_offset, shdr.sh_size, "dynsym"); dynSymOffset = static_cast(shdr.sh_offset); dynSymSize = static_cast(shdr.sh_size); dynSymSectionIndex = i; @@ -333,7 +386,11 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& if (shdr.sh_type == kShtGnuHash) { gnuHashOffset = static_cast(shdr.sh_offset); gnuHashSize = static_cast(shdr.sh_size); - gnuHashSymOffset = Read(elf, gnuHashOffset + 4u); + } + if (shdr.sh_type == 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); @@ -345,6 +402,14 @@ 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); + } const std::size_t dynStrSectionLink = [&]() -> std::size_t { for (std::uint16_t i = 0u; i < ehdr.e_shnum; ++i) { @@ -359,6 +424,8 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& { const std::size_t shOffset = static_cast(ehdr.e_shoff) + dynStrSectionLink * sizeof(Elf64_Shdr); const auto shdr = Read(elf, shOffset); + if (shdr.sh_type != 3) throw std::runtime_error("invalid ELF dynstr section type"); + RequireElfRange(elf, shdr.sh_offset, shdr.sh_size, "dynstr"); dynStrOffset = static_cast(shdr.sh_offset); dynStrSize = static_cast(shdr.sh_size); } @@ -391,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); @@ -407,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); @@ -427,7 +498,7 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& const auto addName = [&](std::uint32_t oldOffset, std::string value) { const auto existing = std::find_if( names.begin(), names.end(), - [&](const auto& entry) { return entry.first == oldOffset; } + [&](const auto& entry) { return entry.first == oldOffset && entry.second == value; } ); if (existing == names.end()) names.emplace_back(oldOffset, std::move(value)); }; @@ -463,8 +534,9 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& newDynStr.push_back(0u); } - std::vector> oldToNewOffset; - for (const auto& name : names) oldToNewOffset.emplace_back(name.first, valueOffsets.at(name.second)); + std::vector> preservedToNewOffset; + for (const auto offset : preservedOffsets) + preservedToNewOffset.emplace_back(offset, valueOffsets.at(ReadCStr(origDynStr, offset))); if (newDynStr.size() > dynStrSize) { throw std::runtime_error( @@ -475,25 +547,15 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& newDynStr.resize(dynStrSize, 0u); std::vector newSymbolNames(symCount); - for (std::size_t i = 1u; i < symCount; ++i) { - const std::size_t symOffset = dynSymOffset + i * sizeof(Elf64_Sym); - const auto sym = Read(elf, symOffset); - if (sym.st_name == 0u) continue; - - const auto mapped = std::find_if( - oldToNewOffset.begin(), oldToNewOffset.end(), - [&](const auto& entry) { return entry.first == sym.st_name; } - ); - if (mapped == oldToNewOffset.end()) continue; - - newSymbolNames[i] = ReadCStr(newDynStr, mapped->second); - Write(elf, symOffset + offsetof(Elf64_Sym, st_name), mapped->second); + for (const auto& use : uses) { + newSymbolNames[use.symIndex] = use.newValue; + Write(elf, dynSymOffset + use.symIndex * sizeof(Elf64_Sym) + offsetof(Elf64_Sym, st_name), valueOffsets.at(use.newValue)); } std::memcpy(elf.data() + dynStrOffset, newDynStr.data(), newDynStr.size()); - RemapDynamicNeededOffsets(elf, ehdr, oldToNewOffset); - RemapVerneedOffsets(elf, ehdr, oldToNewOffset); + RemapDynamicNeededOffsets(elf, ehdr, preservedToNewOffset); + RemapVerneedOffsets(elf, ehdr, preservedToNewOffset); if (gnuHashOffset != 0u) { const auto bucketCount = Read(elf, gnuHashOffset); @@ -502,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 64d8fa7472..273f517d3b 100644 --- a/core/libs/nid/src/ExportExclusions.cpp +++ b/core/libs/nid/src/ExportExclusions.cpp @@ -1,11 +1,16 @@ #include #include #include +#include #include +#include +#include #include #include #include +#include #include +#include namespace Nid { @@ -48,7 +53,12 @@ std::unordered_set ReadPeExports(const std::vector& b if (!directory.VirtualAddress || directory.Size < sizeof(PeExportDirectory)) throw std::runtime_error("missing reference PE exports"); const auto sections = optional + optionalSize; RequireRange(binary, sections, std::uint64_t{sectionCount} * sizeof(PeSectionHeader)); + const auto headersSize = ReadValue(binary, optional + 60); const auto mapRva = [&](std::uint32_t rva, std::uint64_t size) -> std::uint64_t { + if (rva < headersSize && size <= headersSize - rva) { + RequireRange(binary, rva, size); + return rva; + } for (std::uint16_t index = 0; index < sectionCount; ++index) { const auto section = ReadValue(binary, sections + std::uint64_t{index} * sizeof(PeSectionHeader)); if (rva < section.VirtualAddress) continue; @@ -63,14 +73,20 @@ std::unordered_set ReadPeExports(const std::vector& b const auto table = ReadValue(binary, mapRva(directory.VirtualAddress, sizeof(PeExportDirectory))); if (!table.NumberOfNames) throw std::runtime_error("reference PE has no named exports"); const auto names = mapRva(table.AddressOfNames, std::uint64_t{table.NumberOfNames} * 4); + const auto ordinals = mapRva(table.AddressOfNameOrdinals, std::uint64_t{table.NumberOfNames} * 2); + const auto functions = mapRva(table.AddressOfFunctions, std::uint64_t{table.NumberOfFunctions} * 4); std::unordered_set result; for (std::uint32_t index = 0; index < table.NumberOfNames; ++index) { + const auto ordinal = ReadValue(binary, ordinals + std::uint64_t{index} * 2); + if (ordinal >= table.NumberOfFunctions) throw std::runtime_error("invalid reference PE export ordinal"); + if (!ReadValue(binary, functions + std::uint64_t{ordinal} * 4)) continue; const auto rva = ReadValue(binary, names + std::uint64_t{index} * 4); const auto offset = mapRva(rva, 1); auto name = ReadName(binary, offset, binary.size() - offset); mapRva(rva, name.size() + 1); if (!result.insert(name).second) throw std::runtime_error("duplicate reference export: " + name); } + if (result.empty()) throw std::runtime_error("reference PE has no named exports"); return result; } @@ -100,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) { @@ -109,7 +143,7 @@ std::string NormalizeExportName(const std::string& name) { std::unordered_set ReadExportExclusions(const std::string& path) { if constexpr (std::endian::native != std::endian::little) throw std::runtime_error("export references require a little-endian host"); - std::ifstream input(path, std::ios::binary | std::ios::ate); + std::ifstream input(Internal::NativePath(path), std::ios::binary | std::ios::ate); if (!input) throw std::runtime_error("cannot open export reference: " + path); const auto size = input.tellg(); if (size <= 0) throw std::runtime_error("empty or unreadable export reference: " + path); @@ -121,6 +155,12 @@ std::unordered_set ReadExportExclusions(const std::string& path) { exports = ReadPeExports(binary); } else if (binary.size() >= 4 && binary[0] == 0x7f && binary[1] == 'E' && binary[2] == 'L' && binary[3] == 'F') { exports = ReadElfExports(binary); + } else if (binary.size() >= 4 && binary[0] == 0xcf && binary[1] == 0xfa && binary[2] == 0xed && binary[3] == 0xfe) { + for (const auto& entry : ReadMachOExports(binary)) { + if (entry.Name.size() < 2 || entry.Name[0] != '_') throw std::runtime_error("invalid reference Mach-O export: " + entry.Name); + if (!exports.insert(entry.Name.substr(1)).second) throw std::runtime_error("duplicate reference export: " + entry.Name); + } + if (exports.empty()) throw std::runtime_error("reference Mach-O has no exports"); } else { throw std::runtime_error("unrecognized export reference format: " + path); } @@ -134,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/MachONidPatcher.cpp b/core/libs/nid/src/MachONidPatcher.cpp new file mode 100644 index 0000000000..45bdee2735 --- /dev/null +++ b/core/libs/nid/src/MachONidPatcher.cpp @@ -0,0 +1,493 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Nid { + +namespace { + +using Internal::Read; +using Internal::Write; + +constexpr std::uint32_t kLcSegment64 = 0x19; +constexpr std::uint32_t kLcCodeSignature = 0x1d; +constexpr std::uint32_t kLcDyldInfo = 0x22; +constexpr std::uint32_t kLcDyldInfoOnly = 0x80000022u; +constexpr std::uint32_t kLcDyldExportsTrie = 0x80000033u; +constexpr std::uint32_t kLcDyldChainedFixups = 0x80000034u; +constexpr std::uint32_t kLcLoadDylib = 0x0c; +constexpr std::uint32_t kLcLoadWeakDylib = 0x80000018u; +constexpr std::uint32_t kLcReexportDylib = 0x8000001fu; +constexpr std::uint32_t kLcLazyLoadDylib = 0x20; +constexpr std::uint32_t kLcLoadUpwardDylib = 0x80000023u; + +constexpr std::uint64_t kExportWeakDefinition = 0x04; +constexpr std::uint64_t kExportReexport = 0x08; +constexpr std::uint64_t kExportStubAndResolver = 0x10; + +constexpr std::uint32_t kImportFormatPlain = 1; +constexpr std::uint32_t kImportFormatAddend = 2; +constexpr std::uint32_t kImportFormatAddend64 = 3; + +constexpr std::uint64_t kPageSize = 0x1000; + +struct MachHeader64 { + std::uint32_t Magic; + std::uint32_t CpuType; + std::uint32_t CpuSubtype; + std::uint32_t FileType; + std::uint32_t CommandCount; + std::uint32_t CommandBytes; + std::uint32_t Flags; + std::uint32_t Reserved; +}; + +struct LoadCommand { + std::uint32_t Cmd; + std::uint32_t Size; +}; + +struct SegmentCommand64 { + std::uint32_t Cmd; + std::uint32_t Size; + char Name[16]; + std::uint64_t VmAddress; + std::uint64_t VmSize; + std::uint64_t FileOffset; + std::uint64_t FileSize; + std::uint32_t MaxProtection; + std::uint32_t InitProtection; + std::uint32_t SectionCount; + std::uint32_t Flags; +}; + +struct LinkeditDataCommand { + std::uint32_t Cmd; + std::uint32_t Size; + std::uint32_t DataOffset; + std::uint32_t DataSize; +}; + +struct DyldInfoCommand { + std::uint32_t Cmd; + std::uint32_t Size; + std::uint32_t RebaseOffset; + std::uint32_t RebaseSize; + std::uint32_t BindOffset; + std::uint32_t BindSize; + std::uint32_t WeakBindOffset; + std::uint32_t WeakBindSize; + std::uint32_t LazyBindOffset; + std::uint32_t LazyBindSize; + std::uint32_t ExportOffset; + std::uint32_t ExportSize; +}; + +struct ChainedFixupsHeader { + std::uint32_t Version; + std::uint32_t StartsOffset; + std::uint32_t ImportsOffset; + std::uint32_t SymbolsOffset; + std::uint32_t ImportsCount; + std::uint32_t ImportsFormat; + std::uint32_t SymbolsFormat; +}; + +void RequireRange(const std::vector& binary, std::uint64_t offset, std::uint64_t size) { + if (offset > binary.size() || size > binary.size() - offset) throw std::runtime_error("Mach-O range out of bounds"); +} + +std::uint64_t ReadUleb(const std::vector& data, std::size_t& cursor, std::size_t end) { + std::uint64_t value = 0; + unsigned shift = 0; + for (;;) { + if (cursor >= end) throw std::runtime_error("truncated uleb128 in export trie"); + const auto byte = data[cursor++]; + if (shift >= 64 || (shift == 63 && (byte & 0x7e) != 0)) throw std::runtime_error("uleb128 overflow in export trie"); + value |= std::uint64_t{byte & 0x7fu} << shift; + if ((byte & 0x80) == 0) return value; + shift += 7; + } +} + +void WriteUleb(std::vector& out, std::uint64_t value) { + do { + std::uint8_t byte = value & 0x7f; + value >>= 7; + if (value != 0) byte |= 0x80; + out.push_back(byte); + } while (value != 0); +} + +std::size_t UlebSize(std::uint64_t value) { + std::size_t size = 1; + while (value >>= 7) ++size; + return size; +} + +std::string ReadTrieString(const std::vector& data, std::size_t& cursor, std::size_t end) { + const auto start = cursor; + while (cursor < end && data[cursor] != 0) ++cursor; + if (cursor >= end) throw std::runtime_error("unterminated string in export trie"); + std::string text(reinterpret_cast(data.data() + start), cursor - start); + ++cursor; + return text; +} + +struct Image { + MachHeader64 Header {}; + std::vector Commands; +}; + +Image ParseImage(const std::vector& binary) { + Image image; + image.Header = Read(binary, 0); + if (image.Header.Magic != kMachOMagic64) throw std::runtime_error("not a 64-bit little-endian Mach-O image"); + RequireRange(binary, sizeof(MachHeader64), image.Header.CommandBytes); + std::size_t cursor = sizeof(MachHeader64); + const auto end = cursor + image.Header.CommandBytes; + for (std::uint32_t index = 0; index < image.Header.CommandCount; ++index) { + if (cursor + sizeof(LoadCommand) > end) throw std::runtime_error("load command out of bounds"); + const auto command = Read(binary, cursor); + if (command.Size < sizeof(LoadCommand) || command.Size % 8 != 0 || command.Size > end - cursor) throw std::runtime_error("invalid load command size"); + image.Commands.push_back(cursor); + cursor += command.Size; + } + return image; +} + +struct TrieLocation { + std::size_t OffsetField; + std::size_t SizeField; +}; + +std::optional FindTrie(const std::vector& binary, const Image& image) { + std::optional found; + for (const auto offset : image.Commands) { + const auto cmd = Read(binary, offset).Cmd; + std::optional here; + if (cmd == kLcDyldExportsTrie) here = TrieLocation{offset + offsetof(LinkeditDataCommand, DataOffset), offset + offsetof(LinkeditDataCommand, DataSize)}; + else if (cmd == kLcDyldInfo || cmd == kLcDyldInfoOnly) { + if (Read(binary, offset).ExportSize != 0) + here = TrieLocation{offset + offsetof(DyldInfoCommand, ExportOffset), offset + offsetof(DyldInfoCommand, ExportSize)}; + } + if (!here) continue; + if (found) throw std::runtime_error("Mach-O image has more than one export trie"); + found = here; + } + return found; +} + +void ParseTrieNode(const std::vector& data, std::size_t start, std::size_t end, std::size_t node, std::string& prefix, std::set& visited, std::vector& out) { + if (node >= end - start) throw std::runtime_error("export trie node out of bounds"); + if (!visited.insert(node).second) throw std::runtime_error("export trie has a cycle"); + std::size_t cursor = start + node; + const auto terminalSize = ReadUleb(data, cursor, end); + if (terminalSize > end - cursor) throw std::runtime_error("export trie terminal out of bounds"); + const auto children = cursor + terminalSize; + if (terminalSize != 0) { + MachOExport entry; + entry.Name = prefix; + entry.Flags = ReadUleb(data, cursor, children); + if (entry.Flags & kExportReexport) { + entry.Other = ReadUleb(data, cursor, children); + entry.ImportName = ReadTrieString(data, cursor, children); + } else { + entry.Address = ReadUleb(data, cursor, children); + if (entry.Flags & kExportStubAndResolver) entry.Other = ReadUleb(data, cursor, children); + } + out.push_back(std::move(entry)); + } + cursor = children; + if (cursor >= end) throw std::runtime_error("export trie child count out of bounds"); + const auto childCount = data[cursor++]; + for (unsigned index = 0; index < childCount; ++index) { + const auto label = ReadTrieString(data, cursor, end); + const auto child = ReadUleb(data, cursor, end); + const auto length = prefix.size(); + prefix += label; + ParseTrieNode(data, start, end, static_cast(child), prefix, visited, out); + prefix.resize(length); + } +} + +std::vector ParseTrie(const std::vector& binary, std::uint32_t offset, std::uint32_t size) { + RequireRange(binary, offset, size); + std::vector exports; + if (size == 0) return exports; + std::string prefix; + std::set visited; + ParseTrieNode(binary, offset, std::size_t{offset} + size, 0, prefix, visited, exports); + return exports; +} + +class TrieBuilder { +public: + explicit TrieBuilder(const std::vector& exports) { + for (const auto& entry : exports) Insert(entry); + } + + std::vector Build() { + std::vector order; + Collect(&root, order); + for (bool changed = true; changed;) { + changed = false; + std::uint64_t offset = 0; + for (auto* node : order) { + if (node->Offset != offset) { + node->Offset = offset; + changed = true; + } + offset += NodeSize(*node); + } + } + std::vector out; + for (auto* node : order) { + if (out.size() != node->Offset) throw std::runtime_error("export trie layout mismatch"); + const auto terminal = TerminalBytes(*node); + WriteUleb(out, terminal.size()); + out.insert(out.end(), terminal.begin(), terminal.end()); + if (node->Children.size() > 255) throw std::runtime_error("export trie node has too many children"); + out.push_back(static_cast(node->Children.size())); + for (const auto& [label, child] : node->Children) { + out.insert(out.end(), label.begin(), label.end()); + out.push_back(0); + WriteUleb(out, child->Offset); + } + } + while (out.size() % 8 != 0) out.push_back(0); + return out; + } + +private: + struct Node { + std::vector>> Children; + std::optional Entry; + std::uint64_t Offset = 0; + }; + + Node root; + + void Insert(const MachOExport& entry) { + Node* node = &root; + std::string_view rest = entry.Name; + while (!rest.empty()) { + bool descended = false; + for (auto& [label, child] : node->Children) { + std::size_t common = 0; + while (common < label.size() && common < rest.size() && label[common] == rest[common]) ++common; + if (common == 0) continue; + if (common < label.size()) { + auto middle = std::make_unique(); + middle->Children.emplace_back(label.substr(common), std::move(child)); + label.resize(common); + child = std::move(middle); + } + node = child.get(); + rest.remove_prefix(common); + descended = true; + break; + } + if (!descended) { + node->Children.emplace_back(std::string(rest), std::make_unique()); + node = node->Children.back().second.get(); + rest = {}; + } + } + if (node->Entry) throw std::runtime_error("duplicate patched export name: " + entry.Name); + node->Entry = entry; + } + + static void Collect(Node* node, std::vector& order) { + order.push_back(node); + for (auto& [label, child] : node->Children) Collect(child.get(), order); + } + + static std::vector TerminalBytes(const Node& node) { + std::vector bytes; + if (!node.Entry) return bytes; + const auto& entry = *node.Entry; + WriteUleb(bytes, entry.Flags); + if (entry.Flags & kExportReexport) { + WriteUleb(bytes, entry.Other); + bytes.insert(bytes.end(), entry.ImportName.begin(), entry.ImportName.end()); + bytes.push_back(0); + } else { + WriteUleb(bytes, entry.Address); + if (entry.Flags & kExportStubAndResolver) WriteUleb(bytes, entry.Other); + } + return bytes; + } + + static std::uint64_t NodeSize(const Node& node) { + const auto terminal = TerminalBytes(node).size(); + std::uint64_t size = UlebSize(terminal) + terminal + 1; + for (const auto& [label, child] : node.Children) size += label.size() + 1 + UlebSize(child->Offset); + return size; + } +}; + +std::string StripUnderscore(const std::string& name) { + return !name.empty() && name[0] == '_' ? name.substr(1) : name; +} + +bool IsGuestFacingImport(const std::string& name) { + using namespace Internal; + const bool hasNidPostfix = name.size() >= kNidPostfixLen && name.compare(name.size() - kNidPostfixLen, kNidPostfixLen, kNidPostfix) == 0; + const bool hasScePrefix = name.size() >= 3u && + std::tolower(static_cast(name[0])) == 's' && + std::tolower(static_cast(name[1])) == 'c' && + std::tolower(static_cast(name[2])) == 'e'; + return hasNidPostfix || hasScePrefix; +} + +std::vector DylibOrdinalsArePrx(const std::vector& binary, const Image& image) { + std::vector result; + for (const auto offset : image.Commands) { + const auto command = Read(binary, offset); + if (command.Cmd != kLcLoadDylib && command.Cmd != kLcLoadWeakDylib && command.Cmd != kLcReexportDylib && command.Cmd != kLcLazyLoadDylib && command.Cmd != kLcLoadUpwardDylib) continue; + const auto nameOffset = Read(binary, offset + 8); + if (nameOffset >= command.Size) throw std::runtime_error("dylib name out of bounds"); + const auto name = Internal::ReadCStr(binary, offset + nameOffset); + result.push_back(name.size() >= 4 && name.compare(name.size() - 4, 4, ".prx") == 0); + } + return result; +} + +void PatchChainedImports(std::vector& binary, const Image& image) { + std::optional fixups; + for (const auto offset : image.Commands) { + const auto cmd = Read(binary, offset).Cmd; + if (cmd == kLcDyldChainedFixups) fixups = Read(binary, offset); + if ((cmd == kLcDyldInfo || cmd == kLcDyldInfoOnly)) { + const auto info = Read(binary, offset); + if (info.BindSize != 0 || info.LazyBindSize != 0 || info.WeakBindSize != 0) throw std::runtime_error("Mach-O bind opcodes are not supported; link with chained fixups"); + } + } + if (!fixups || fixups->DataSize == 0) return; + RequireRange(binary, fixups->DataOffset, fixups->DataSize); + const auto base = std::size_t{fixups->DataOffset}; + const auto header = Read(binary, base); + if (header.Version != 0) throw std::runtime_error("unsupported chained fixups version"); + if (header.SymbolsFormat != 0) throw std::runtime_error("compressed chained fixup symbols are not supported"); + const std::size_t entrySize = header.ImportsFormat == kImportFormatPlain ? 4 : header.ImportsFormat == kImportFormatAddend ? 8 : header.ImportsFormat == kImportFormatAddend64 ? 16 : 0; + if (entrySize == 0) throw std::runtime_error("unsupported chained import format"); + if (header.ImportsOffset > fixups->DataSize || std::uint64_t{header.ImportsCount} * entrySize > fixups->DataSize - header.ImportsOffset || header.SymbolsOffset > fixups->DataSize) + throw std::runtime_error("chained imports out of bounds"); + const auto symbols = base + header.SymbolsOffset; + const auto symbolsEnd = base + fixups->DataSize; + const auto prx = DylibOrdinalsArePrx(binary, image); + + struct Import { std::uint32_t NameOffset; std::uint32_t Ordinal; }; + std::vector imports; + for (std::uint32_t index = 0; index < header.ImportsCount; ++index) { + const auto at = base + header.ImportsOffset + index * entrySize; + if (header.ImportsFormat == kImportFormatAddend64) { + const auto raw = Read(binary, at); + imports.push_back({static_cast(raw >> 32), static_cast(raw & 0xffff)}); + } else { + const auto raw = Read(binary, at); + imports.push_back({raw >> 9, raw & 0xff}); + } + } + std::set nameOffsets; + for (const auto& item : imports) nameOffsets.insert(item.NameOffset); + std::set rewritten; + for (const auto& item : imports) { + const bool special = header.ImportsFormat == kImportFormatAddend64 ? item.Ordinal >= 0xfff0 : item.Ordinal >= 0xf0; + if (item.Ordinal == 0 || special || item.Ordinal > prx.size() || !prx[item.Ordinal - 1]) continue; + if (rewritten.contains(item.NameOffset)) continue; + const auto at = symbols + item.NameOffset; + if (at >= symbolsEnd) throw std::runtime_error("chained import name out of bounds"); + const auto name = Internal::ReadCStr(binary, at); + if (at + name.size() >= symbolsEnd) throw std::runtime_error("unterminated chained import name"); + const auto bare = StripUnderscore(name); + if (bare == name || !IsGuestFacingImport(bare)) continue; + const auto patched = "_" + ResolveOneName(bare); + if (patched.size() > name.size()) throw std::runtime_error("import NID longer than original name: " + name); + const auto inside = nameOffsets.upper_bound(item.NameOffset); + if (inside != nameOffsets.end() && *inside <= item.NameOffset + name.size()) throw std::runtime_error("chained import names share storage: " + name); + std::memcpy(binary.data() + at, patched.data(), patched.size()); + std::fill(binary.begin() + static_cast(at + patched.size()), binary.begin() + static_cast(at + name.size() + 1), 0); + rewritten.insert(item.NameOffset); + } +} + +void WriteTrie(std::vector& binary, const Image& image, const TrieLocation& location, const std::vector& trie) { + const auto oldOffset = Read(binary, location.OffsetField); + const auto oldSize = Read(binary, location.SizeField); + if (trie.size() <= oldSize) { + std::copy(trie.begin(), trie.end(), binary.begin() + oldOffset); + std::fill(binary.begin() + oldOffset + static_cast(trie.size()), binary.begin() + oldOffset + oldSize, 0); + Write(binary, location.SizeField, static_cast(trie.size())); + return; + } + std::optional linkedit; + for (const auto offset : image.Commands) { + const auto cmd = Read(binary, offset).Cmd; + if (cmd == kLcCodeSignature) throw std::runtime_error("signed Mach-O images cannot grow their export trie"); + if (cmd != kLcSegment64) continue; + const auto segment = Read(binary, offset); + if (std::strncmp(segment.Name, "__LINKEDIT", sizeof(segment.Name)) == 0) linkedit = offset; + } + if (!linkedit) throw std::runtime_error("Mach-O image has no __LINKEDIT segment"); + auto segment = Read(binary, *linkedit); + if (segment.FileOffset + segment.FileSize != binary.size()) throw std::runtime_error("__LINKEDIT does not end the file"); + const auto newOffset = (binary.size() + 7) & ~std::size_t{7}; + if (newOffset + trie.size() > 0xffffffffu) throw std::runtime_error("export trie offset overflow"); + binary.resize(newOffset, 0); + binary.insert(binary.end(), trie.begin(), trie.end()); + std::fill(binary.begin() + oldOffset, binary.begin() + oldOffset + oldSize, 0); + segment.FileSize = binary.size() - segment.FileOffset; + segment.VmSize = std::max(segment.VmSize, (segment.FileSize + kPageSize - 1) & ~(kPageSize - 1)); + Write(binary, *linkedit, segment); + Write(binary, location.OffsetField, static_cast(newOffset)); + Write(binary, location.SizeField, static_cast(trie.size())); +} + +} + +std::vector ReadMachOExports(const std::vector& binary) { + const auto image = ParseImage(binary); + const auto location = FindTrie(binary, image); + if (!location) return {}; + return ParseTrie(binary, Read(binary, location->OffsetField), Read(binary, location->SizeField)); +} + +void MachONidPatcher::PatchNids(std::vector& binary, const std::string& libraryName, const std::unordered_set& excludedExports) const { + const auto image = ParseImage(binary); + const auto location = FindTrie(binary, image); + if (!location) throw std::runtime_error("no export trie"); + auto exports = ParseTrie(binary, Read(binary, location->OffsetField), Read(binary, location->SizeField)); + if (exports.empty()) throw std::runtime_error("no exported names"); + + std::vector names; + names.reserve(exports.size()); + for (const auto& entry : exports) { + if (entry.Name.empty() || entry.Name[0] != '_') throw std::runtime_error("exported name without a C underscore: " + entry.Name); + names.push_back(entry.Name.substr(1)); + } + const auto nidMap = ResolveNids(names, libraryName, excludedExports); + for (std::size_t index = 0; index < exports.size(); ++index) { + const auto& name = names[index]; + const bool marked = name.ends_with(Internal::kNidPostfix) || Internal::IsNidNoPatch(name) || Internal::IsNidNoPatchCut(name); + if ((exports[index].Flags & kExportWeakDefinition) != 0 && !marked) continue; + const auto found = nidMap.find(name); + if (found == nidMap.end()) throw std::runtime_error("symbol not in nid map: " + name); + exports[index].Name = "_" + found->second; + } + WriteTrie(binary, image, *location, TrieBuilder(exports).Build()); + PatchChainedImports(binary, ParseImage(binary)); +} + +} 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 2f91413fd6..722c765e7c 100644 --- a/core/libs/nid/src/main.cpp +++ b/core/libs/nid/src/main.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -9,25 +10,42 @@ namespace { std::vector _readFile(const std::string& path) { - std::ifstream f(path, std::ios::binary); + std::ifstream f(Nid::Internal::NativePath(path), std::ios::binary); if (!f) throw std::runtime_error("cannot open: " + path); return {std::istreambuf_iterator(f), {}}; } void _writeFile(const std::string& path, const std::vector& data) { - std::ofstream f(path, std::ios::binary); + std::ofstream f(Nid::Internal::NativePath(path), std::ios::binary); if (!f) throw std::runtime_error("cannot write: " + path); f.write(reinterpret_cast(data.data()), static_cast(data.size())); + f.close(); + if (!f) throw std::runtime_error("cannot write: " + path); } } -int main(int argc, char* argv[]) { +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; @@ -35,10 +53,10 @@ int main(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); @@ -65,3 +83,27 @@ int main(int argc, char* argv[]) { return 0; } + +#ifdef _WIN32 +int wmain(int argc, wchar_t* argv[]) { + try { + std::vector values; + values.reserve(static_cast(argc)); + for (int index = 0; index < argc; ++index) { + const auto utf8 = std::filesystem::path(argv[index]).u8string(); + values.emplace_back(utf8.begin(), utf8.end()); + } + std::vector arguments; + arguments.reserve(values.size()); + for (auto& value : values) arguments.push_back(value.data()); + return Run(argc, arguments.data()); + } catch (const std::exception& error) { + std::cerr << "FAIL: " << error.what() << '\n'; + return 2; + } +} +#else +int main(int argc, char* argv[]) { + return Run(argc, argv); +} +#endif diff --git a/core/libs/nid/tests/BufferValidation.cpp b/core/libs/nid/tests/BufferValidation.cpp new file mode 100644 index 0000000000..a006371d89 --- /dev/null +++ b/core/libs/nid/tests/BufferValidation.cpp @@ -0,0 +1,60 @@ +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition) { + if (!condition) throw std::runtime_error("NID buffer validation failed"); +} + +template +void RequireRejected(TAction action) { + try { action(); } + catch (const std::runtime_error&) { return; } + throw std::runtime_error("Malformed NID buffer was accepted"); +} + +} + +int main() { + try { + using namespace Nid::Internal; + const std::vector unterminated{'n', 'a', 'm', 'e'}; + RequireRejected([&] { ReadCStr(unterminated, 0); }); + RequireRejected([&] { ReadCStr(unterminated, 3); }); + const std::vector strings{'a', 0, 'b', 'c', 0}; + Require(ReadCStr(strings, 0) == "a"); + Require(ReadCStr(strings, 1).empty()); + Require(ReadCStr(strings, 2) == "bc"); + Require(ReadCStr(strings, 4).empty()); + RequireRejected([&] { ReadCStr(strings, strings.size()); }); + RequireRejected([&] { ReadCStr({}, 0); }); + + std::vector bytes(8, 0xa5); + const std::uint64_t value = 0x1020304050607080; + Write(bytes, 0, value); + Require(Read(bytes, 0) == value); + Write(bytes, 4, 0x12345678); + Require(Read(bytes, 4) == 0x12345678); + const auto before = bytes; + for (const auto offset : {std::size_t{5}, std::size_t{8}, std::size_t{9}, + std::numeric_limits::max(), std::numeric_limits::max() - 1, + std::numeric_limits::max() - 3}) { + RequireRejected([&] { Read(bytes, offset); }); + RequireRejected([&] { Write(bytes, offset, 0); }); + Require(bytes == before); + } + RequireRejected([&] { Read(bytes, 1); }); + std::vector empty; + RequireRejected([&] { Read(empty, 0); }); + RequireRejected([&] { Write(empty, 0, 0); }); + std::cout << "NID buffer validation passed\n"; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/nid/tests/test_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_elf_layout.py b/core/libs/nid/tests/test_elf_layout.py new file mode 100644 index 0000000000..703ac46db3 --- /dev/null +++ b/core/libs/nid/tests/test_elf_layout.py @@ -0,0 +1,48 @@ +import struct +import sys +import unittest +from pathlib import Path + +from test_symbol_versions import build_image, symbols, patch, SHOFF + + +class ElfLayoutTests(unittest.TestCase): + def test_declared_layout_must_match_supported_tables(self): + changes = ((5, "B", 2), (0x3a, "H", 32), + (SHOFF + 64 + 56, "Q", 8), + (SHOFF + 64 + 32, "Q", len(symbols(False)) * 24 + 1), + (SHOFF + 128 + 4, "I", 1)) + for offset, kind, value in changes: + with self.subTest(offset=offset, value=value): + image = build_image(symbols(False), False) + struct.pack_into("<" + kind, image, offset, value) + self.reject(image) + + def test_table_ranges_and_links_are_checked(self): + changes = ((0x28, "Q", 0x10000), + (SHOFF + 64 + 24, "Q", 0xff0), + (SHOFF + 64 + 40, "I", 100), + (SHOFF + 128 + 24, "Q", 0xff0), + (SHOFF + 128 + 32, "Q", 0xffffffffffffffff)) + for offset, kind, value in changes: + with self.subTest(offset=offset, value=value): + image = build_image(symbols(False), False) + struct.pack_into("<" + kind, image, offset, value) + self.reject(image) + + def reject(self, image): + result, after = patch(PATCHER, bytes(image), "layout.so") + self.assertEqual(result.returncode, 2, (result.returncode, result.stderr)) + self.assertRegex(result.stderr.decode(), "ELF|dynsym|dynstr") + self.assertEqual(after, image) + + def test_valid_supported_layout_still_patches(self): + image = build_image(symbols(False), False) + result, after = patch(PATCHER, bytes(image), "valid-layout.so") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertNotEqual(after, image) + + +if __name__ == "__main__": + PATCHER = Path(sys.argv.pop(1)).resolve() + unittest.main() diff --git a/core/libs/nid/tests/test_gnu_hash.py b/core/libs/nid/tests/test_gnu_hash.py new file mode 100644 index 0000000000..2e46a70f32 --- /dev/null +++ b/core/libs/nid/tests/test_gnu_hash.py @@ -0,0 +1,157 @@ +import struct +import sys +import unittest +from pathlib import Path + +from test_symbol_versions import build_image, symbols, patch, HASH_OFFSET, SHOFF, read_dynsym, gnu_hash, 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("> shift) % 64))) + start = struct.unpack_from("> 18) & 63] + BASE64[(triple >> 12) & 63] + BASE64[(triple >> 6) & 63] + BASE64[triple & 63] + tail = (reversed_bytes[6] << 16) | (reversed_bytes[7] << 8) + return text + BASE64[(tail >> 18) & 63] + BASE64[(tail >> 12) & 63] + BASE64[(tail >> 6) & 63] + + +def uleb(value): + out = bytearray() + while True: + byte = value & 0x7F + value >>= 7 + out.append(byte | (0x80 if value else 0)) + if not value: + return bytes(out) + + +def read_uleb(data, position): + value = shift = 0 + while True: + byte = data[position] + position += 1 + value |= (byte & 0x7F) << shift + if not byte & 0x80: + return value, position + shift += 7 + + +def build_trie(entries): + offsets = [0] * len(entries) + for _ in range(8): + root = bytearray(b"\0") + bytes([len(entries)]) + for (name, _, _), offset in zip(entries, offsets): + root += name.encode() + b"\0" + uleb(offset) + nodes = [] + for name, flags, address in entries: + terminal = uleb(flags) + uleb(address) + nodes.append(uleb(len(terminal)) + terminal + b"\0") + position, expected = len(root), [] + for node in nodes: + expected.append(position) + position += len(node) + if expected == offsets: + break + offsets = expected + trie = bytes(root) + b"".join(nodes) + return trie + bytes(-len(trie) % 8) + + +def parse_trie(data, start, size): + found = {} + + def walk(node, prefix): + position = start + node + terminal, position = read_uleb(data, position) + children = position + terminal + if terminal: + flags, position = read_uleb(data, position) + address, position = read_uleb(data, position) + found[prefix] = (flags, address) + count = data[children] + position = children + 1 + for _ in range(count): + end = data.index(0, position) + label = data[position:end].decode() + child, position = read_uleb(data, end + 1) + walk(child, prefix + label) + + if size: + walk(0, "") + return found + + +def build_fixups(imports): + symbols = bytearray(b"\0") + entries = [] + for name, ordinal in imports: + entries.append(ordinal | (len(symbols) << 9)) + symbols += name.encode() + b"\0" + imports_offset = 32 + header = struct.pack("<7I", 0, 28, imports_offset, imports_offset + 4 * len(imports), len(imports), 1, 0) + blob = header + struct.pack("> 9) + result.append((data[start:data.index(0, start)].decode(), raw & 0xFF)) + return result + + +def patch(patcher, directory, name, image): + path = Path(directory) / name + path.write_bytes(image) + result = subprocess.run([str(patcher), "libtest", str(path)], capture_output=True, text=True, timeout=30) + return result, path + + +def exports_of(data): + offset = command_fields(data)[LC_DYLD_EXPORTS_TRIE][0] + start, size = struct.unpack_from("= filesize and len(data) > len(original), (fileoff, filesize, len(data)) + trie_offset, trie_size = struct.unpack_from("= len(original) and trie_offset + trie_size <= len(data) and trie_offset % 8 == 0, (trie_offset, trie_size) + + +def check_preserved(patcher, directory): + reference = Path(directory) / "reference.dylib" + reference.write_bytes(build_dylib([("_puts", 0, 0x100)], [])) + path = Path(directory) / "preserve.dylib" + path.write_bytes(build_dylib([("_puts_nid_postfix", 0, 0x100), ("_exit_nid_postfix", 0, 0x108)], [])) + result = subprocess.run([str(patcher), "libtest", "--preserve-exports", str(reference), str(path)], capture_output=True, text=True, timeout=30) + assert result.returncode == 0, (result.stdout, result.stderr) + assert exports_of(path.read_bytes()) == {"_puts_nid_postfix": (0, 0x100), "_" + nid("exit"): (0, 0x108)}, exports_of(path.read_bytes()) + + +def write_export_list(patcher, reference, listing): + return subprocess.run([str(patcher), "--write-export-list", str(reference), str(listing)], capture_output=True, text=True, timeout=30) + + +def check_export_list(patcher, directory): + reference = Path(directory) / "listed.dylib" + listing = Path(directory) / "listed.exports" + reference.write_bytes(build_dylib([("_puts_nid_postfix", 0, 0x100), ("_exit", 0, 0x108)], [])) + result = write_export_list(patcher, reference, listing) + assert result.returncode == 0, (result.stdout, result.stderr) + assert listing.read_bytes() == b"exit\nputs\n", listing.read_bytes() + + os.utime(listing, ns=(1_000_000_000_000_000_000, 1_000_000_000_000_000_000)) + reference.write_bytes(build_dylib([("_exit", 0, 0x110), ("_puts_nid_postfix", 0, 0x118)], [])) + result = write_export_list(patcher, reference, listing) + assert result.returncode == 0, (result.stdout, result.stderr) + assert listing.stat().st_mtime_ns == 1_000_000_000_000_000_000, "an unchanged export list was rewritten" + + reference.write_bytes(build_dylib([("_puts", 0, 0x100)], [])) + result = write_export_list(patcher, reference, listing) + assert result.returncode == 0, (result.stdout, result.stderr) + assert listing.read_bytes() == b"puts\n", listing.read_bytes() + assert listing.stat().st_mtime_ns != 1_000_000_000_000_000_000, "a changed export list kept its time" + + path = Path(directory) / "listed-preserve.dylib" + path.write_bytes(build_dylib([("_puts_nid_postfix", 0, 0x100), ("_exit_nid_postfix", 0, 0x108)], [])) + result = subprocess.run([str(patcher), "libtest", "--preserve-export-list", str(listing), str(path)], capture_output=True, text=True, timeout=30) + assert result.returncode == 0, (result.stdout, result.stderr) + assert exports_of(path.read_bytes()) == {"_puts_nid_postfix": (0, 0x100), "_" + nid("exit"): (0, 0x108)}, exports_of(path.read_bytes()) + + malformed_lists = [ + ("unterminated", b"puts", "malformed export list"), + ("blank", b"puts\n\nexit\n", "empty name in export list"), + ("twice", b"puts\nputs\n", "duplicate name in export list"), + ("empty", b"", "malformed export list"), + ] + for name, content, message in malformed_lists: + malformed = Path(directory) / (name + ".exports") + malformed.write_bytes(content) + target = Path(directory) / (name + ".dylib") + target.write_bytes(build_dylib([("_puts_nid_postfix", 0, 0x100)], [])) + result = subprocess.run([str(patcher), "libtest", "--preserve-export-list", str(malformed), str(target)], capture_output=True, text=True, timeout=30) + assert result.returncode != 0 and message in result.stdout + result.stderr, (name, result.stdout, result.stderr) + result = subprocess.run([str(patcher), "libtest", "--preserve-exports", str(reference), "--preserve-export-list", str(listing), str(path)], capture_output=True, text=True, timeout=30) + assert result.returncode != 0 and "exactly one reference" in result.stdout + result.stderr, (result.stdout, result.stderr) + + +def check_refusals(patcher, directory): + growing = [("_ab", 0, 0x100), ("_cd", 0, 0x108)] + result, _ = patch(patcher, directory, "signed.dylib", build_dylib(growing, [], signed=True)) + assert result.returncode != 0 and "signed Mach-O images cannot grow their export trie" in result.stdout + result.stderr, (result.stdout, result.stderr) + result, _ = patch(patcher, directory, "opcodes.dylib", build_dylib([("_puts_nid_postfix", 0, 0x100)], [], bind_opcodes=True)) + assert result.returncode != 0 and "bind opcodes are not supported" in result.stdout + result.stderr, (result.stdout, result.stderr) + result, _ = patch(patcher, directory, "long.dylib", build_dylib([("_puts_nid_postfix", 0, 0x100)], [("_sceXy", 1)])) + assert result.returncode != 0 and "import NID longer than original name" in result.stdout + result.stderr, (result.stdout, result.stderr) + + +def main(): + patcher = Path(sys.argv[1]).resolve() + with tempfile.TemporaryDirectory(prefix="anyps5-macho-nid-") as directory: + check_shrinking(patcher, directory) + check_growing(patcher, directory) + check_preserved(patcher, directory) + check_export_list(patcher, directory) + check_refusals(patcher, directory) + print("NID patcher Mach-O tests passed") + + +if __name__ == "__main__": + main() diff --git a/core/libs/nid/tests/test_native_paths.py b/core/libs/nid/tests/test_native_paths.py new file mode 100644 index 0000000000..ccfd9e6b8c --- /dev/null +++ b/core/libs/nid/tests/test_native_paths.py @@ -0,0 +1,83 @@ +import subprocess +import signal +import sys +import tempfile +import unittest +from pathlib import Path + +from test_macho_nids import build_dylib, exports_of, nid + +try: + import resource +except ImportError: + resource = None + +PATCHER = Path(sys.argv.pop(1)).resolve() + + +class NativePathTests(unittest.TestCase): + def invoke(self, *arguments): + return subprocess.run([str(PATCHER), "libtest", *map(str, arguments)], + capture_output=True, encoding="utf-8", errors="replace", timeout=10) + + def test_non_ascii_directory_and_filename_patch_the_intended_file(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) / "\u6e2c\u8a66" + root.mkdir() + file = root / "\u043c\u043e\u0434\u0443\u043b\u044c.prx" + file.write_bytes(build_dylib([("_puts_nid_postfix", 0, 0x100)], [])) + result = self.invoke(file) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual(exports_of(file.read_bytes()), {"_" + nid("puts"): (0, 0x100)}) + self.assertIn(str(file), result.stdout) + self.assertEqual(list(root.iterdir()), [file]) + + def test_non_ascii_export_reference_preserves_the_named_export(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + reference = root / "\u6e2c\u8a66-reference.prx" + original = build_dylib([("_puts", 0, 0x100)], []) + reference.write_bytes(original) + file = root / "target.prx" + file.write_bytes(build_dylib([("_puts_nid_postfix", 0, 0x100), ("_exit_nid_postfix", 0, 0x108)], [])) + result = self.invoke("--preserve-exports", reference, file) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual(exports_of(file.read_bytes()), {"_puts_nid_postfix": (0, 0x100), "_" + nid("exit"): (0, 0x108)}) + self.assertEqual(reference.read_bytes(), original) + + def test_missing_unicode_input_does_not_modify_another_file(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + existing = root / "existing.prx" + existing.write_bytes(b"unchanged") + missing = root / "\u6e2c\u8a66-missing.prx" + result = self.invoke(missing) + self.assertEqual(result.returncode, 2) + self.assertIn(str(missing), result.stderr) + self.assertFalse(missing.exists()) + self.assertEqual(existing.read_bytes(), b"unchanged") + + @unittest.skipUnless(resource is not None and hasattr(signal, "SIGXFSZ"), "requires POSIX file size limits") + def test_failed_file_write_is_reported_without_success(self): + for limit in (0, 1024): + with self.subTest(limit=limit), tempfile.TemporaryDirectory() as temporary: + file = Path(temporary) / "library.prx" + original = build_dylib([("_puts_nid_postfix", 0, 0x100)], []) + file.write_bytes(original) + + def restrict_output(): + signal.signal(signal.SIGXFSZ, signal.SIG_IGN) + resource.setrlimit(resource.RLIMIT_FSIZE, (limit, limit)) + + result = subprocess.run([str(PATCHER), "libtest", str(file)], capture_output=True, + encoding="utf-8", errors="replace", timeout=10, + preexec_fn=restrict_output) + self.assertEqual(file.stat().st_size, limit) + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + self.assertIn("cannot write", result.stderr) + self.assertIn(str(file), result.stderr) + self.assertNotIn("OK:", result.stdout) + + +if __name__ == "__main__": + unittest.main() diff --git a/core/libs/nid/tests/test_pe_references.py b/core/libs/nid/tests/test_pe_references.py new file mode 100644 index 0000000000..2e6eed7f40 --- /dev/null +++ b/core/libs/nid/tests/test_pe_references.py @@ -0,0 +1,246 @@ +import struct +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +from test_macho_nids import build_dylib, exports_of, nid + + +def reference(*, headers=False, address=0x2000, ordinal=0): + image = bytearray(1280) + image[:2] = b"MZ" + struct.pack_into(" 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" { - -int APS5_VABI ErrorDialogClose(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI ErrorDialogOpen(const void* param) { - (void)param; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI ShareGetCurrentStatus(uint32_t feature_flag, ShareCurrentStatus* status) { - (void)feature_flag; - (void)status; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI ShareInitialize(size_t heap_size, int thread_priority, uint64_t affinity_mask) { - (void)heap_size; - (void)thread_priority; - (void)affinity_mask; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI ShareTerminate(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SystemServiceParamGetInt(int param_id, int* value) { - (void)param_id; - (void)value; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SystemServiceParamGetString(int param_id, char* buf, size_t buf_size) { - (void)param_id; - (void)buf; - (void)buf_size; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -} diff --git a/core/libs/prx/libReactNative.Modules.Vsh/Export.cpp b/core/libs/prx/libReactNative.Modules.Vsh/Export.cpp deleted file mode 100644 index b488eb139b..0000000000 --- a/core/libs/prx/libReactNative.Modules.Vsh/Export.cpp +++ /dev/null @@ -1,35 +0,0 @@ -#include -#include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" - -extern "C" { - -int APS5_VABI RemotePlayGetConnectionStatus(int user_id, int* status) { - (void)user_id; - (void)status; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI ShareGetCurrentStatus(uint32_t feature_flag, ShareCurrentStatus* status) { - (void)feature_flag; - (void)status; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI ShareInitialize(size_t heap_size, int thread_priority, uint64_t affinity_mask) { - (void)heap_size; - (void)thread_priority; - (void)affinity_mask; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI ShareTerminate(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -} diff --git a/core/libs/prx/libSceAbstractLocal/Export.cpp b/core/libs/prx/libSceAbstractLocal/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceAbstractLocal/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/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..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceAc3Enc/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/libSceAcm/Export.cpp b/core/libs/prx/libSceAcm/Export.cpp index e62e5934dc..325afaa575 100644 --- a/core/libs/prx/libSceAcm/Export.cpp +++ b/core/libs/prx/libSceAcm/Export.cpp @@ -1,64 +1,113 @@ #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) { return 0; } -int APS5_VABI sceAcm_FFT() { +int APS5_VABI sceAcm_FFT(void) { + return 0; +} + +int APS5_VABI sceAcm_Panner(void) { + return 0; +} + +int APS5_VABI sceAcmBatchJobPriority(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAcm_Panner() { +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 de20362b0e..567bfd5e18 100644 --- a/core/libs/prx/libSceAgc/Acb/src/Control.cpp +++ b/core/libs/prx/libSceAgc/Acb/src/Control.cpp @@ -4,17 +4,34 @@ #include "prx/libSceAgc/Command/include/Memory.hpp" #include "prx/libSceAgc/Command/include/Packet.hpp" #include "prx/libSceAgc/DcbState/include/Marker.hpp" +#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" +#include "prx/libSceAgcDriver/Resource/include/Registration.hpp" #include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +std::uint32_t* RejectUnregisteredWorkloadStream(CommandBuffer* buf, std::uint32_t streamId, const char* function) { + if (buf == nullptr || !AgcDriverWorkloadStreamRegistered_nid_postfix(streamId)) return nullptr; + NotImplemented_nid_no_patch(function); + return nullptr; +} + +} + extern "C" { -// unknown signature -void* APS5_VABI sceAgcAcbAtomicGds_0900(void) { - NotImplemented_nid_no_patch(__func__); - return nullptr; +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) { + 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 sceAgcAcbJump(CommandBuffer* buf, std::uint8_t cachePolicy, const std::uint32_t* target, std::uint32_t sizeInDwords) { @@ -46,21 +63,38 @@ std::uint32_t APS5_VABI sceAgcAcbRewindGetSize() { } std::uint32_t* APS5_VABI sceAgcAcbWaitUntilSafeForRendering(CommandBuffer* buf, std::uint32_t videoOutHandle, std::uint32_t displayBufferIndex) { - (void)buf; - (void)videoOutHandle; - (void)displayBufferIndex; - NotImplemented_nid_no_patch(__func__); - return nullptr; + auto* packet = Agc::Command::Allocate(buf, AgcDriver::RenderingWaitPacketWords, __func__); + packet[0] = AgcDriver::RenderingWaitPacketHeader; + packet[1] = videoOutHandle; + packet[2] = displayBufferIndex; + packet[3] = 0; + return packet; } 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) { + (void)workloadIds; + (void)workloadCount; + return RejectUnregisteredWorkloadStream(buf, streamId, __func__); +} + +std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadComplete(CommandBuffer* buf, std::uint32_t streamId, std::uint32_t workloadId) { + (void)workloadId; + return RejectUnregisteredWorkloadStream(buf, streamId, __func__); +} + +std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadStreamInactive(CommandBuffer* buf, std::uint32_t streamId) { + return RejectUnregisteredWorkloadStream(buf, streamId, __func__); } uint32_t* APS5_VABI sceAgcAcbPushMarker(CommandBuffer* buf, const char* str, uint32_t color) { 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 b6f691fb47..a411cacdb2 100644 --- a/core/libs/prx/libSceAgc/CMakeLists.txt +++ b/core/libs/prx/libSceAgc/CMakeLists.txt @@ -29,9 +29,11 @@ add_library(libSceAgc SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/DcbState/src/Predication.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DcbState/src/Registers.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DcbState/src/Workload.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/Debug.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/PacketInfo.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/Platform.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/RegisterDefaults.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/Semaphore.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/ShaderFusion.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/Suspend.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Patch/src/CondExec.cpp @@ -46,6 +48,7 @@ add_library(libSceAgc SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Predication/src/Predication.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Shader/src/CreateShader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Shader/src/GsOversubscription.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Shader/src/GsPrimPayload.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Shader/src/InterpolantMapping.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Shader/src/LinkShaders.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Shader/src/PrimState.cpp @@ -67,7 +70,7 @@ endforeach() add_test_executable(agc_command_tests EXCLUDE_FROM_ALL tests/Commands.cpp) target_include_directories(agc_command_tests PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(agc_command_tests PRIVATE libSceAgc libc) +target_link_libraries(agc_command_tests PRIVATE libSceAgc libSceAgcDriver libc) configure_windows_unwind(agc_command_tests) if(BUILD_TESTING) set_target_properties(agc_command_tests PROPERTIES EXCLUDE_FROM_ALL OFF) @@ -78,6 +81,19 @@ 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_platform_tests EXCLUDE_FROM_ALL tests/Platform.cpp) @@ -93,6 +109,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_semaphore_tests EXCLUDE_FROM_ALL tests/Semaphore.cpp) +target_include_directories(agc_semaphore_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_semaphore_tests PRIVATE libSceAgc libc) +configure_windows_unwind(agc_semaphore_tests) +if(BUILD_TESTING) + set_target_properties(agc_semaphore_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME agc_semaphore_tests COMMAND agc_semaphore_tests) + if(WIN32) + set_tests_properties(agc_semaphore_tests PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_packet_info_tests EXCLUDE_FROM_ALL tests/PacketInfo.cpp) target_include_directories(agc_packet_info_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(agc_packet_info_tests PRIVATE libSceAgc libc) @@ -209,7 +238,7 @@ if(BUILD_TESTING) endif() add_test_executable(agc_shader_fusion_tests EXCLUDE_FROM_ALL tests/ShaderFusion.cpp) -target_include_directories(agc_shader_fusion_tests 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_include_directories(agc_shader_fusion_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} ${ANYPS5_SPIRV_HEADERS_INCLUDE_DIR}) target_link_libraries(agc_shader_fusion_tests PRIVATE libSceAgc libSceAgcDriver libc) set_target_properties(agc_shader_fusion_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_shader_fusion_tests) @@ -290,6 +319,22 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_gs_prim_payload_tests EXCLUDE_FROM_ALL + tests/GsPrimPayload.cpp + Shader/src/GsPrimPayload.cpp +) +target_include_directories(agc_gs_prim_payload_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_gs_prim_payload_tests PRIVATE libc) +configure_windows_unwind(agc_gs_prim_payload_tests) +if(BUILD_TESTING) + set_target_properties(agc_gs_prim_payload_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME agc_gs_prim_payload_tests COMMAND agc_gs_prim_payload_tests) + if(WIN32) + set_tests_properties(agc_gs_prim_payload_tests PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_dma_data_tests EXCLUDE_FROM_ALL tests/DmaData.cpp Command/src/Packet.cpp @@ -383,6 +428,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 4a4e9e3184..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 @@ -85,7 +87,7 @@ void ValidatePacket(const std::uint32_t* packet, std::uint32_t opcode, std::uint } std::uint32_t* WriteNop(CommandBuffer* buffer, std::uint32_t count, const char* function) { - const auto header = Header(0x10u, count); + const auto header = count == 1 ? 0xffff1000u : Header(0x10u, count); auto* packet = Allocate(buffer, count, function); packet[0] = header; std::fill_n(packet + 1, count - 1u, 0u); @@ -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/Instancing.cpp b/core/libs/prx/libSceAgc/DcbDraw/src/Instancing.cpp index 35a5fcba87..21ec62d119 100644 --- a/core/libs/prx/libSceAgc/DcbDraw/src/Instancing.cpp +++ b/core/libs/prx/libSceAgc/DcbDraw/src/Instancing.cpp @@ -20,7 +20,7 @@ uint32_t* APS5_VABI sceAgcDcbSetBaseIndirectArgs(CommandBuffer* buf, uint32_t sh // SET_BASE with base index 1; header bit 1 selects the dispatch (compute) base over the draw base. Agc::Command::CheckBits(shader_type, 1, __func__); const auto address = reinterpret_cast(indirect_base_addr); - Agc::Command::CheckAddress(address, 8, __func__); + if (address != 0) Agc::Command::CheckAddress(address, 8, __func__); Agc::Command::CheckBits(address, 0xffffffffffffull, __func__); auto* packet = Agc::Command::Allocate(buf, 4, __func__); packet[0] = Agc::Command::Header(0x11u, 4, shader_type != 0 ? 2u : 0u); 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 bb68d80d84..987ef180f3 100644 --- a/core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp +++ b/core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp @@ -12,11 +12,19 @@ extern "C" { -// unknown signature -APS5_EXPORT("zARR5aCmkoY", sceAgcDcbA_zARR5aCmkoY); -void* APS5_VABI sceAgcDcbA_zARR5aCmkoY(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); } @@ -35,8 +43,7 @@ std::uint32_t* APS5_VABI sceAgcDcbResetQueue(CommandBuffer* buf, std::uint32_t o } std::uint32_t* APS5_VABI sceAgcDcbClearState(CommandBuffer* buf, std::uint32_t command) { - Agc::Command::CheckBits(command, 0xfu, __func__); - return Agc::Command::Emit(buf, 0x12u, {command}, __func__); + return Agc::Command::Emit(buf, 0x12u, {command & 0xfu}, __func__); } uint32_t* APS5_VABI sceAgcDcbRewind(CommandBuffer* buf, uint32_t initial_state) { 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/DcbFlow/src/Sync.cpp b/core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp index e7271811c2..96c45f0d3c 100644 --- a/core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp +++ b/core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp @@ -36,7 +36,6 @@ std::uint32_t* APS5_VABI sceAgcDcbEventWrite(CommandBuffer* buf, std::uint8_t ev Agc::Command::CheckAddress(guestAddress, 8, __func__); return Agc::Command::Emit(buf, 0x46u, {0x100u | eventType, static_cast(guestAddress), static_cast(guestAddress >> 32u)}, __func__); } - Agc::Command::Require(address == nullptr, __func__, "this event does not use an address"); const auto eventIndex = eventType == 7 || eventType == 15 || eventType == 16 ? 0x400u : 0u; return Agc::Command::Emit(buf, 0x46u, {eventIndex | eventType}, __func__); } diff --git a/core/libs/prx/libSceAgc/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 7ad33c6d90..f3acef34c5 100644 --- a/core/libs/prx/libSceAgc/DcbState/src/Init.cpp +++ b/core/libs/prx/libSceAgc/DcbState/src/Init.cpp @@ -9,16 +9,16 @@ 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 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/DcbState/src/Registers.cpp b/core/libs/prx/libSceAgc/DcbState/src/Registers.cpp index f9b0b4c992..0949b2c36b 100644 --- a/core/libs/prx/libSceAgc/DcbState/src/Registers.cpp +++ b/core/libs/prx/libSceAgc/DcbState/src/Registers.cpp @@ -29,7 +29,7 @@ std::uint32_t* APS5_VABI sceAgcDcbSetCxRegistersIndirect(CommandBuffer* buf, con } std::uint32_t APS5_VABI sceAgcDcbSetCxRegistersIndirectGetSize(std::uint32_t numRegs) { - Agc::Command::CheckBits(numRegs, 0x3fffu, __func__); + static_cast(numRegs); return 20; } @@ -46,7 +46,7 @@ std::uint32_t* APS5_VABI sceAgcDcbSetShRegistersIndirect(CommandBuffer* buf, con } std::uint32_t APS5_VABI sceAgcDcbSetShRegistersIndirectGetSize(std::uint32_t numRegs) { - Agc::Command::CheckBits(numRegs, 0x3fffu, __func__); + static_cast(numRegs); return 20; } @@ -63,7 +63,7 @@ std::uint32_t* APS5_VABI sceAgcDcbSetUcRegistersIndirect(CommandBuffer* buf, con } std::uint32_t APS5_VABI sceAgcDcbSetUcRegistersIndirectGetSize(std::uint32_t numRegs) { - Agc::Command::CheckBits(numRegs, 0x3fffu, __func__); + static_cast(numRegs); return 20; } diff --git a/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp b/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp index 2ce29cfe2d..0f739d9055 100644 --- a/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp +++ b/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp @@ -1,26 +1,62 @@ #include "prx/libSceAgc/DcbState/include/Workload.hpp" #include "prx/libSceAgc/Command/include/Packet.hpp" +#include "prx/libSceAgcDriver/Resource/include/Registration.hpp" #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr std::uint32_t OpcodeNop = 0x10; +constexpr std::uint32_t ActivePacketWords = 18; +constexpr std::uint32_t CompletePacketWords = 12; +constexpr std::uint32_t MaxWorkloadId = 63; +constexpr std::uint32_t MaxActiveCount = 63; + +uint32_t* NewPacket(CommandBuffer* buf, std::uint32_t words, const char* function) { + auto* packet = Agc::Command::Allocate(buf, words, function); + std::memset(packet, 0, words * sizeof(std::uint32_t)); + packet[0] = Agc::Command::Header(OpcodeNop, words, 0); + return packet; +} + +} + extern "C" { uint32_t* APS5_VABI sceAgcDcbSetWorkloadComplete(CommandBuffer* buf, uint32_t stream_id, uint32_t workload_id) { - (void)buf; - (void)stream_id; - (void)workload_id; - NotImplemented_nid_no_patch(__func__); - return nullptr; + if (buf == nullptr || workload_id > MaxWorkloadId || !AgcDriverWorkloadStreamRegistered_nid_postfix(stream_id)) return nullptr; + const std::uint64_t clearMask = ~(std::uint64_t{1} << workload_id); + auto* packet = NewPacket(buf, CompletePacketWords, __func__); + packet[1] = stream_id; + packet[2] = workload_id; + packet[3] = static_cast(clearMask); + packet[4] = static_cast(clearMask >> 32); + return packet; } uint32_t* APS5_VABI sceAgcDcbSetWorkloadsActive(CommandBuffer* buf, uint32_t stream_id, const uint32_t* workload_ids, uint32_t workload_count) { - (void)buf; - (void)stream_id; - (void)workload_ids; - (void)workload_count; + if (buf == nullptr || workload_ids == nullptr || workload_count == 0 || workload_count > MaxActiveCount || !AgcDriverWorkloadStreamRegistered_nid_postfix(stream_id)) return nullptr; + std::uint64_t mask = 0; + for (std::uint32_t index = 0; index < workload_count; ++index) { + const auto id = workload_ids[index]; + if (id > MaxWorkloadId) return nullptr; + const std::uint64_t bit = std::uint64_t{1} << id; + if ((mask & bit) != 0) return nullptr; + mask |= bit; + } + auto* packet = NewPacket(buf, ActivePacketWords, __func__); + packet[1] = stream_id; + packet[2] = static_cast(mask); + packet[3] = static_cast(mask >> 32); + return packet; +} + +uint32_t* APS5_VABI sceAgcDcbSetWorkloadStreamInactive(CommandBuffer* buf, uint32_t streamId) { + if (buf == nullptr || !AgcDriverWorkloadStreamRegistered_nid_postfix(streamId)) return nullptr; NotImplemented_nid_no_patch(__func__); return nullptr; } diff --git a/core/libs/prx/libSceAgc/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/Debug.hpp b/core/libs/prx/libSceAgc/Misc/include/Debug.hpp new file mode 100644 index 0000000000..58286eb85c --- /dev/null +++ b/core/libs/prx/libSceAgc/Misc/include/Debug.hpp @@ -0,0 +1,4 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_DEBUG_HPP +#define CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_DEBUG_HPP + +#endif 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/Semaphore.hpp b/core/libs/prx/libSceAgc/Misc/include/Semaphore.hpp new file mode 100644 index 0000000000..834dcdd962 --- /dev/null +++ b/core/libs/prx/libSceAgc/Misc/include/Semaphore.hpp @@ -0,0 +1,4 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_SEMAPHORE_HPP +#define CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_SEMAPHORE_HPP + +#endif diff --git a/core/libs/prx/libSceAgc/Misc/src/Debug.cpp b/core/libs/prx/libSceAgc/Misc/src/Debug.cpp new file mode 100644 index 0000000000..8a321110ab --- /dev/null +++ b/core/libs/prx/libSceAgc/Misc/src/Debug.cpp @@ -0,0 +1,14 @@ +#include "prx/libSceAgc/Misc/include/Debug.hpp" + +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceAgcDebugRaiseException(std::uint32_t exceptionId) { + APS5_LOG_OUT("sceAgcDebugRaiseException exceptionId=0x%08x", static_cast(exceptionId)); + return 0; +} + +} 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 new file mode 100644 index 0000000000..7a6c802690 --- /dev/null +++ b/core/libs/prx/libSceAgc/Misc/src/Semaphore.cpp @@ -0,0 +1,61 @@ +#include "prx/libSceAgc/Misc/include/Semaphore.hpp" +#include "prx/libSceAgc/Command/include/Packet.hpp" + +#include +#include +#include +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +namespace { + +constexpr std::uint64_t semaphoreMemoryAlignment = 0x4000u; +constexpr std::uint64_t semaphoreLabelBytes = 32u; +constexpr std::uint64_t semaphoreCounterBytes = 8u; + +std::mutex semaphoreMutex; +std::uint8_t* semaphoreMemory = nullptr; +std::uint64_t semaphoreBytes = 0; + +bool hasOnlyZeroLabelTails(const std::uint8_t* memory, std::uint64_t sizeInBytes) { + for (std::uint64_t label = 0; label < sizeInBytes; label += semaphoreLabelBytes) { + for (std::uint64_t byte = semaphoreCounterBytes; byte < semaphoreLabelBytes; ++byte) { + if (memory[label + byte] != 0) return false; + } + } + return true; +} + +} + +extern "C" { + +int APS5_VABI sceAgcSetAmmSemaphoreMemory(void* memory, std::uint64_t sizeInBytes) { + const auto address = reinterpret_cast(memory); + std::lock_guard lock(semaphoreMutex); + Agc::Command::Require(semaphoreMemory == nullptr, __func__, "the AMM semaphore memory is already registered; the console's error is unknown"); + Agc::Command::Require(memory != nullptr && sizeInBytes != 0, __func__, "null or empty AMM semaphore memory; the console's error is unknown"); + Agc::Command::Require(((address | sizeInBytes) & (semaphoreMemoryAlignment - 1u)) == 0, __func__, "AMM semaphore memory or size is not 16 KiB aligned; the console's error is unknown"); + Agc::Command::Require(sizeInBytes - 1u <= std::numeric_limits::max() - address, __func__, "AMM semaphore memory wraps the address space"); + auto* labels = static_cast(memory); + Agc::Command::Require(hasOnlyZeroLabelTails(labels, sizeInBytes), __func__, "AMM semaphore labels hold data past their first 8 bytes; whether registration clears it is unknown"); + for (std::uint64_t label = 0; label < sizeInBytes; label += semaphoreLabelBytes) { + std::memset(labels + label, 0, semaphoreCounterBytes); + } + semaphoreMemory = labels; + semaphoreBytes = sizeInBytes; + return 0; +} + +int APS5_VABI sceAgcGetSemaphoreLabel(std::uint32_t index, void** label) { + std::lock_guard lock(semaphoreMutex); + Agc::Command::Require(semaphoreMemory != nullptr, __func__, "no AMM semaphore memory is registered; the console's error is unknown"); + Agc::Command::Require(index < semaphoreBytes / semaphoreLabelBytes, __func__, "the semaphore label lies outside the AMM semaphore memory; the console's error is unknown"); + Agc::Command::Require(label != nullptr, __func__, "null semaphore label output; the console's error is unknown"); + *label = semaphoreMemory + static_cast(index) * semaphoreLabelBytes; + return 0; +} + +} diff --git a/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp b/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp index 499217f13b..ad00d374ce 100644 --- a/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp +++ b/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include #include #include @@ -29,12 +31,12 @@ constexpr std::uint32_t SCodeEnd = 0xbf9f0000u; constexpr std::uint32_t SNop = 0xbf800000u; constexpr std::uint32_t SSetpcS6 = 0xbe802006u; constexpr std::uint32_t SSwappcNullS6 = 0xbefd2106u; -constexpr std::uint8_t FusedCodeAlignmentLog2 = 8; -constexpr std::size_t FusedCodeAlignment = std::size_t{1} << FusedCodeAlignmentLog2; +constexpr std::size_t FusedCodeAlignment = 256; constexpr char TrailerMagic[8] = {'b', 'a', 'r', 'e', 'f', 'o', 'o', 't'}; constexpr std::size_t TrailerCodeBytesOffset = sizeof(TrailerMagic) + 3 * sizeof(std::uint32_t); constexpr std::uint32_t Rsrc1VgprMask = 0x3fu; 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) { @@ -126,8 +128,8 @@ std::uint32_t MergeRsrc1(std::uint32_t back, std::uint32_t front) { } 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); + if ((front & ~(Rsrc2UserSgprMask | Rsrc2OffchipLdsEnable) & ~back) != 0) Fail(function, "front half requires hardware state the back half does not enable"); + return (back & ~Rsrc2UserSgprMask) | (front & (Rsrc2UserSgprMask | Rsrc2OffchipLdsEnable)); } const ShaderRegister* FindHalfRegister(const Shader* shader, std::uint32_t offset) { @@ -191,14 +193,45 @@ void SetProgramAddress(std::vector& regs, std::uint32_t loOffset throw std::runtime_error("sceAgcFuseShaderHalves_0200: back half has no program address register pair"); } -int FuseGeometryHalves(Shader* fused_result, const Shader* front, const Shader* back, void* scratch_mem) { - constexpr auto fn = "sceAgcFuseShaderHalves_0200"; - if (scratch_mem == nullptr) Fail(fn, "geometry halves need fused shader memory"); - auto layout = ComputeGeometryLayout(fn, front, back); - const auto scratch = reinterpret_cast(scratch_mem); - const auto base = AlignUp(scratch, FusedCodeAlignment); +struct FusedProgram { + std::vector storage; + Shader* header; +}; + +std::mutex FusedProgramsMutex; +std::map, FusedProgram> FusedPrograms; + +void AppendBytes(std::vector& bytes, const void* data, std::size_t size) { + if (size == 0) return; + const auto* begin = static_cast(data); + bytes.insert(bytes.end(), begin, begin + size); +} + +std::vector FusionKey(const Shader* front, const Shader* back, const GeometryLayout& layout) { + std::vector key; + constexpr auto headerBytes = offsetof(Shader, num_sh_registers) + sizeof(Shader::num_sh_registers); + AppendBytes(key, front, headerBytes); + AppendBytes(key, back, headerBytes); + AppendBytes(key, const_cast(front->code), layout.frontBytes); + AppendBytes(key, const_cast(back->code), back->shader_size); + AppendBytes(key, layout.sh.data(), layout.sh.size() * sizeof(ShaderRegister)); + AppendBytes(key, layout.cx.data(), layout.cx.size() * sizeof(ShaderRegister)); + AppendBytes(key, back->specials, back->special_sizes_bytes); + AppendBytes(key, front->input_semantics, front->num_input_semantics * sizeof(ShaderSemantic)); + AppendBytes(key, back->output_semantics, back->num_output_semantics * sizeof(ShaderSemantic)); + if (back->user_data == nullptr) return key; + AppendBytes(key, back->user_data, offsetof(ShaderUserData, sharp_resource_count) + sizeof(ShaderUserData::sharp_resource_count)); + AppendBytes(key, back->user_data->direct_resource_offset, back->user_data->direct_resource_count * sizeof(std::uint16_t)); + for (std::size_t i = 0; i < layout.sharpOffsets.size(); ++i) AppendBytes(key, back->user_data->sharp_resource_offset[i], back->user_data->sharp_resource_count[i] * sizeof(ShaderSharp)); + return key; +} + +FusedProgram BuildFusedProgram(const char* fn, const Shader* front, const Shader* back, GeometryLayout& layout) { + FusedProgram program{std::vector(layout.totalBytes + FusedCodeAlignment - 1), nullptr}; + const auto storage = reinterpret_cast(program.storage.data()); + const auto base = AlignUp(storage, FusedCodeAlignment); if ((base & SHADER_BASE_ALIGN_MASK) != 0) Fail(fn, "fused shader memory is not a valid program address"); - auto* memory = static_cast(scratch_mem) + (base - scratch); + auto* memory = program.storage.data() + (base - storage); std::memcpy(memory, const_cast(front->code), layout.frontBytes); for (std::size_t offset = layout.frontBytes; offset < layout.backOffset; offset += sizeof(SNop)) std::memcpy(memory + offset, &SNop, sizeof(SNop)); std::memcpy(memory + layout.backOffset, const_cast(back->code), back->shader_size); @@ -241,10 +274,24 @@ int FuseGeometryHalves(Shader* fused_result, const Shader* front, const Shader* fused.type = static_cast(ShaderBinaryType::Gs); fused.num_sh_registers = static_cast(layout.sh.size()); fused.num_cx_registers = static_cast(layout.cx.size()); - auto* header = reinterpret_cast(memory + layout.headerOffset); - *header = fused; + program.header = reinterpret_cast(memory + layout.headerOffset); + *program.header = fused; + AgcDriverRegisterShader_nid_postfix(program.header); + return program; +} + +int FuseGeometryHalves(Shader* fused_result, const Shader* front, const Shader* back, void* scratch_mem) { + constexpr auto fn = "sceAgcFuseShaderHalves_0200"; + if (scratch_mem == nullptr) Fail(fn, "geometry halves need fused shader memory"); + auto layout = ComputeGeometryLayout(fn, front, back); + auto key = FusionKey(front, back, layout); + std::lock_guard lock(FusedProgramsMutex); + auto found = FusedPrograms.find(key); + if (found == FusedPrograms.end()) found = FusedPrograms.emplace(std::move(key), BuildFusedProgram(fn, front, back, layout)).first; + Shader fused = *found->second.header; + std::memcpy(scratch_mem, fused.sh_registers, std::size_t{fused.num_sh_registers} * sizeof(ShaderRegister)); + fused.sh_registers = static_cast(scratch_mem); *fused_result = fused; - AgcDriverRegisterShader_nid_postfix(header); return 0; } @@ -306,8 +353,8 @@ int APS5_VABI sceAgcGetFusedShaderSize_0080(SizeAlign* dst, const Shader* front, if (!ValidHalves(front, back)) return GRAPHICS5_ERROR_INVALID_SHADER_HALVES; if (GeometryHalves(front)) { const auto layout = ComputeGeometryLayout("sceAgcGetFusedShaderSize_0080", front, back); - dst->m_size = layout.totalBytes + FusedCodeAlignment - 1; - dst->m_align = FusedCodeAlignmentLog2; + dst->m_size = layout.sh.size() * sizeof(ShaderRegister); + dst->m_align = 4; return 0; } dst->m_size = std::uint64_t{back->num_sh_registers} * sizeof(ShaderRegister); diff --git a/core/libs/prx/libSceAgc/Patch/src/Branch.cpp b/core/libs/prx/libSceAgc/Patch/src/Branch.cpp index b2f7916153..1be1907980 100644 --- a/core/libs/prx/libSceAgc/Patch/src/Branch.cpp +++ b/core/libs/prx/libSceAgc/Patch/src/Branch.cpp @@ -19,6 +19,12 @@ void PatchBranchTarget(std::uint32_t* cmd, std::uint32_t field, const volatile s cmd[field + 2u] = (cmd[field + 2u] & ~0xfffffu) | sizeInDwords; } +void PatchBranchTarget(std::uint32_t* cmd, std::uint32_t field, std::uint8_t cachePolicy, const volatile std::uint32_t* target, std::uint32_t sizeInDwords, const char* function) { + Agc::Command::CheckBits(cachePolicy, 3, function); + PatchBranchTarget(cmd, field, target, sizeInDwords, function); + cmd[field + 2u] = (cmd[field + 2u] & ~(3u << 28u)) | (static_cast(cachePolicy) << 28u); +} + } extern "C" { @@ -42,4 +48,14 @@ int APS5_VABI sceAgcBranchPatchSetElseTarget(std::uint32_t* cmd, const volatile return 0; } +int APS5_VABI sceAgcBranchPatchSetThenTarget_0300(std::uint32_t* cmd, std::uint8_t cachePolicy, const volatile std::uint32_t* target, std::uint32_t sizeInDwords) { + PatchBranchTarget(cmd, 8, cachePolicy, target, sizeInDwords, __func__); + return 0; +} + +int APS5_VABI sceAgcBranchPatchSetElseTarget_0300(std::uint32_t* cmd, std::uint8_t cachePolicy, const volatile std::uint32_t* target, std::uint32_t sizeInDwords) { + PatchBranchTarget(cmd, 11, cachePolicy, target, sizeInDwords, __func__); + return 0; +} + } diff --git a/core/libs/prx/libSceAgc/Shader/include/ShaderConstants.hpp b/core/libs/prx/libSceAgc/Shader/include/ShaderConstants.hpp index 78a0805876..4af5275789 100644 --- a/core/libs/prx/libSceAgc/Shader/include/ShaderConstants.hpp +++ b/core/libs/prx/libSceAgc/Shader/include/ShaderConstants.hpp @@ -19,11 +19,13 @@ constexpr std::uint32_t VGT_SHADER_STAGES_EN = 0x2D5u; constexpr std::uint32_t VGT_PRIMITIVE_TYPE = 0x242u; constexpr std::uint32_t GE_CNTL = 0x25Bu; +constexpr std::uint32_t GE_USER_VGPR1 = 0x25Cu; constexpr std::uint32_t GE_USER_VGPR_EN = 0x262u; constexpr std::uint32_t GE_PC_ALLOC = 0x260u; constexpr std::uint32_t SPI_SHADER_PGM_RSRC4_GS = 0x081u; constexpr std::uint32_t SPI_VS_OUT_CONFIG = 0x1B1u; +constexpr std::uint32_t SPI_SHADER_IDX_FORMAT = 0x1C2u; constexpr std::uint32_t GE_MAX_OUTPUT_PER_SUBGROUP = 0x1FFu; constexpr std::uint32_t PA_CL_VS_OUT_CNTL = 0x207u; constexpr std::uint32_t VGT_GS_ONCHIP_CNTL = 0x291u; @@ -78,7 +80,7 @@ enum class PrimitiveType : std::uint32_t { }; constexpr std::uint32_t VGT_SHADER_STAGES_GS_BIT = 0x20u; -constexpr std::uint32_t VGT_SHADER_STAGES_NGG_BIT = 0x04u; +constexpr std::uint32_t VGT_SHADER_STAGES_HS_BIT = 0x04u; enum class AgcDirectResourceType : std::uint32_t { GdsCounterRange = 0, diff --git a/core/libs/prx/libSceAgc/Shader/src/GsPrimPayload.cpp b/core/libs/prx/libSceAgc/Shader/src/GsPrimPayload.cpp new file mode 100644 index 0000000000..722ade98c6 --- /dev/null +++ b/core/libs/prx/libSceAgc/Shader/src/GsPrimPayload.cpp @@ -0,0 +1,39 @@ +#include +#include +#include +#include + +#include "SceShaders.hpp" +#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp" + +namespace { + +constexpr std::uint32_t IDX0_EXPORT_FORMAT_MASK = 0xFu; +constexpr std::uint32_t SPI_SHADER_2COMP = 2u; +constexpr std::uint32_t TwoComponentPayloadBytes = 8u; + +} + +extern "C" { + +int APS5_VABI sceAgcGetGsPrimPayload(std::uint32_t* payload, const Shader* gs) { + if (payload == nullptr || gs == nullptr) { + throw std::runtime_error(std::string(__func__) + ": payload or gs is null"); + } + if (gs->num_cx_registers != 0 && gs->cx_registers == nullptr) { + throw std::runtime_error(std::string(__func__) + ": gs->cx_registers is null but num_cx_registers != 0"); + } + + std::uint32_t bytes = 0; + for (std::uint32_t i = 0; i < gs->num_cx_registers; ++i) { + const ShaderRegister& reg = gs->cx_registers[i]; + if (reg.offset == ShaderRegs::SPI_SHADER_IDX_FORMAT && (reg.value & IDX0_EXPORT_FORMAT_MASK) == SPI_SHADER_2COMP) { + bytes = TwoComponentPayloadBytes; + break; + } + } + *payload = bytes; + return 0; +} + +} diff --git a/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp b/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp index b3b0da8f42..8e56d4c0c8 100644 --- a/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp +++ b/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp @@ -53,26 +53,38 @@ std::uint32_t CreateInterpolantValueSplitF16(std::uint32_t psWord, const ShaderS : CreateInterpolantValue(psWord, gsSemantic); } -int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* gs, const Shader* ps, CreateInterpolantValueFn createValue) { +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); +} + +int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* gs, const Shader* ps, CreateInterpolantValueFn createValue, bool fillIdentityTail) { if (regs == nullptr) { throw std::runtime_error(std::string(fn) + ": regs is null"); } + if (ps == nullptr && !fillIdentityTail) { + throw std::runtime_error(std::string(fn) + ": ps is null"); + } if (ps != nullptr && ps->num_input_semantics > 32) { 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(); if (ps == nullptr || ps->num_input_semantics == 0) { - FillIdentityInterpolants(regs, 0); + const std::uint32_t count = fillIdentityTail ? InterpolantRegisterCount : 0; + if (fillIdentityTail) FillIdentityInterpolants(regs, 0); if (ps != nullptr) { - AgcDriverResolveShaderAbi_nid_postfix(ps, {regs, 32}, {}); + AgcDriverResolveShaderAbi_nid_postfix(ps, {regs, count}, {}); if (gs != nullptr) AgcDriverResolveGraphicsAbi_nid_postfix(gs, ps, 0); } transaction.Commit(); - std::copy(values.begin(), values.end(), output); + std::copy_n(values.begin(), count, output); return 0; } @@ -98,11 +110,12 @@ int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* SetInterpolantRegister(regs, i, createValue(ShaderSemanticWord(psSemantic), gsSemantic)); } - FillIdentityInterpolants(regs, ps->num_input_semantics); - AgcDriverResolveShaderAbi_nid_postfix(ps, {regs, 32}, {}); + const std::uint32_t count = fillIdentityTail ? InterpolantRegisterCount : ps->num_input_semantics; + if (fillIdentityTail) FillIdentityInterpolants(regs, ps->num_input_semantics); + AgcDriverResolveShaderAbi_nid_postfix(ps, {regs, count}, {}); AgcDriverResolveGraphicsAbi_nid_postfix(gs, ps, 0); transaction.Commit(); - std::copy(values.begin(), values.end(), output); + std::copy_n(values.begin(), count, output); return 0; } @@ -111,12 +124,15 @@ int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* extern "C" { int APS5_VABI sceAgcCreateInterpolantMapping_0100(ShaderRegister* regs, const Shader* gs, const Shader* ps) { - return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValue); + return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValue, true); } -APS5_EXPORT("dbOlWdppb4o", sceAgcUnknownCreateInterpolantMapping); -int APS5_VABI sceAgcUnknownCreateInterpolantMapping(ShaderRegister* regs, const Shader* gs, const Shader* ps) { - return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValueSplitF16); +int APS5_VABI sceAgcCreateInterpolantMapping_10000(ShaderRegister* regs, const Shader* gs, const Shader* ps) { + return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValueSplitF16, true); +} + +int APS5_VABI sceAgcUpdateInterpolantMapping_10000(ShaderRegister* regs, const Shader* gs, const Shader* ps) { + return CreateInterpolantMapping(__func__, regs, gs, ps, UpdateInterpolantValue, false); } } diff --git a/core/libs/prx/libSceAgc/Shader/src/LinkShaders.cpp b/core/libs/prx/libSceAgc/Shader/src/LinkShaders.cpp index 88a8f599f2..59a43354c8 100644 --- a/core/libs/prx/libSceAgc/Shader/src/LinkShaders.cpp +++ b/core/libs/prx/libSceAgc/Shader/src/LinkShaders.cpp @@ -22,11 +22,15 @@ 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); sceAgcCreateInterpolantMapping_0100(contextValues.data() + 2, vertex, pixel); + if (pixel != nullptr) { + const std::array stages{vertex, pixel}; + AgcDriverResolveGraphicsStagesAbi_nid_postfix(stages, contextValues, primitiveValues); + } AgcDriverResolveGraphicsAbi_nid_postfix(vertex, pixel, primitiveType); transaction.Commit(); std::copy(contextValues.begin(), contextValues.end(), context); diff --git a/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp b/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp index 8d3564c765..b62666315c 100644 --- a/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp +++ b/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp @@ -8,9 +8,22 @@ #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" +namespace { + +void RequireSpecialOffset(const ShaderRegister& reg, std::uint32_t expected, const char* name) { + if (reg.offset != expected) { + char message[160]; + std::snprintf(message, sizeof(message), "sceAgcCreatePrimState: %s special register at offset 0x%x, expected 0x%x", name, static_cast(reg.offset), static_cast(expected)); + throw std::runtime_error(message); + } +} + +} + extern "C" { int APS5_VABI sceAgcCreatePrimState(ShaderRegister* cx_regs, ShaderRegister* uc_regs, const Shader* hs, const Shader* gs, std::uint32_t prim_type) { @@ -25,31 +38,36 @@ int APS5_VABI sceAgcCreatePrimState(ShaderRegister* cx_regs, ShaderRegister* uc_ throw std::runtime_error(std::string(__func__) + ": invalid shader type or missing special registers"); } (void)GraphicsPrimTypeToGsOut(prim_type); - const auto valid = [](const Shader* shader) { return shader->specials->vgt_shader_stages_en.offset == ShaderRegs::VGT_SHADER_STAGES_EN && shader->specials->vgt_gs_out_prim_type.offset == ShaderRegs::VGT_GS_OUT_PRIM_TYPE; }; - if (!valid(gs) || (hs != nullptr && !valid(hs))) { - throw std::runtime_error(std::string(__func__) + ": invalid context register offsets"); - } - if (gs->specials->ge_cntl.offset != ShaderRegs::GE_CNTL || gs->specials->ge_user_vgpr_en.offset != ShaderRegs::GE_USER_VGPR_EN || (hs != nullptr && hs->specials->ge_user_vgpr_en.offset != ShaderRegs::GE_USER_VGPR_EN)) { - throw std::runtime_error(std::string(__func__) + ": invalid user configuration register offsets"); - } + RequireSpecialOffset(gs->specials->vgt_shader_stages_en, ShaderRegs::VGT_SHADER_STAGES_EN, "VGT_SHADER_STAGES_EN"); + if (hs != nullptr) RequireSpecialOffset(hs->specials->vgt_shader_stages_en, ShaderRegs::VGT_SHADER_STAGES_EN, "VGT_SHADER_STAGES_EN"); + RequireSpecialOffset(gs->specials->ge_cntl, ShaderRegs::GE_CNTL, "GE_CNTL"); std::array contextValues{gs->specials->vgt_shader_stages_en, gs->specials->vgt_gs_out_prim_type}; - if ((contextValues[0].value & ShaderRegs::VGT_SHADER_STAGES_GS_BIT) == 0) { - contextValues[1] = {ShaderRegs::VGT_GS_OUT_PRIM_TYPE, GraphicsPrimTypeToGsOut(prim_type)}; - } if (hs != nullptr) { contextValues[0].value |= hs->specials->vgt_shader_stages_en.value; - if ((contextValues[0].value & ShaderRegs::VGT_SHADER_STAGES_GS_BIT) == 0) contextValues[1] = hs->specials->vgt_gs_out_prim_type; + } + if ((contextValues[0].value & ShaderRegs::VGT_SHADER_STAGES_GS_BIT) != 0) { + RequireSpecialOffset(gs->specials->vgt_gs_out_prim_type, ShaderRegs::VGT_GS_OUT_PRIM_TYPE, "VGT_GS_OUT_PRIM_TYPE"); + } else if (hs != nullptr) { + RequireSpecialOffset(hs->specials->vgt_gs_out_prim_type, ShaderRegs::VGT_GS_OUT_PRIM_TYPE, "VGT_GS_OUT_PRIM_TYPE"); + contextValues[1] = hs->specials->vgt_gs_out_prim_type; + } else { + contextValues[1] = {ShaderRegs::VGT_GS_OUT_PRIM_TYPE, GraphicsPrimTypeToGsOut(prim_type)}; } std::array primitiveValues{gs->specials->ge_cntl, hs != nullptr ? hs->specials->ge_user_vgpr_en : gs->specials->ge_user_vgpr_en, ShaderRegister{ShaderRegs::VGT_PRIMITIVE_TYPE, prim_type}}; const std::array stages{hs, gs}; - AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span(stages).subspan(hs == nullptr ? 1u : 0u), contextValues, primitiveValues); + 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); return 0; } int APS5_VABI sceAgcUpdatePrimState(ShaderRegister* cx_regs, ShaderRegister* uc_regs, std::uint32_t prim_type) { - if (cx_regs != nullptr && (cx_regs[0].value & (ShaderRegs::VGT_SHADER_STAGES_GS_BIT | ShaderRegs::VGT_SHADER_STAGES_NGG_BIT)) == 0) { + if (cx_regs != nullptr && (cx_regs[0].value & (ShaderRegs::VGT_SHADER_STAGES_GS_BIT | ShaderRegs::VGT_SHADER_STAGES_HS_BIT)) == 0) { cx_regs[1].value &= ~0x7u; cx_regs[1].value |= GraphicsPrimTypeToGsOut(prim_type); } 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 4841b8d7ad..7a7d7c52cb 100644 --- a/core/libs/prx/libSceAgc/Unimplemented.cpp +++ b/core/libs/prx/libSceAgc/Unimplemented.cpp @@ -15,21 +15,6 @@ int APS5_VABI sceAgcAcbSetMarkerSpan() { return 0; } -int APS5_VABI sceAgcAcbSetWorkloadComplete() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAgcAcbSetWorkloadStreamInactive() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAgcAcbSetWorkloadsActive() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAgcAcquireMemSetEngine() { NotImplemented_nid_no_patch(__func__); return 0; @@ -45,31 +30,6 @@ int APS5_VABI sceAgcDcbSetMarkerSpan() { return 0; } -int APS5_VABI sceAgcDcbSetWorkloadStreamInactive() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAgcDebugRaiseException() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAgcGetDefaultCxStateFlat() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAgcGetSemaphoreLabel() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAgcSetAmmSemaphoreMemory() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAgcSetSemaphoreMemory() { NotImplemented_nid_no_patch(__func__); return 0; @@ -80,16 +40,6 @@ int APS5_VABI sceAgcCbMemsetExclusive() { return 0; } -int APS5_VABI sceAgcBranchPatchSetThenTarget_0300() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAgcGetGsPrimPayload() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAgcSetShaderInstrumentation() { NotImplemented_nid_no_patch(__func__); return 0; @@ -100,15 +50,4 @@ int APS5_VABI sceAgcGetShaderInstrumentation() { return 0; } -int APS5_VABI sceAgcBranchPatchSetElseTarget_0300() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -APS5_EXPORT("vieBRwlh1Lw", sceAgcUnknown_vieBRwlh1Lw); -int APS5_VABI sceAgcUnknown_vieBRwlh1Lw(void) { - 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/BranchPatch.cpp b/core/libs/prx/libSceAgc/tests/BranchPatch.cpp index 2382676ee6..58afe9b1c9 100644 --- a/core/libs/prx/libSceAgc/tests/BranchPatch.cpp +++ b/core/libs/prx/libSceAgc/tests/BranchPatch.cpp @@ -13,6 +13,8 @@ std::uint32_t APS5_VABI sceAgcCbBranchGetSize(); int APS5_VABI sceAgcBranchPatchSetCompareAddress(std::uint32_t*, const volatile std::uint64_t*); int APS5_VABI sceAgcBranchPatchSetThenTarget(std::uint32_t*, const volatile std::uint32_t*, std::uint32_t); int APS5_VABI sceAgcBranchPatchSetElseTarget(std::uint32_t*, const volatile std::uint32_t*, std::uint32_t); +int APS5_VABI sceAgcBranchPatchSetThenTarget_0300(std::uint32_t*, std::uint8_t, const volatile std::uint32_t*, std::uint32_t); +int APS5_VABI sceAgcBranchPatchSetElseTarget_0300(std::uint32_t*, std::uint8_t, const volatile std::uint32_t*, std::uint32_t); } namespace { @@ -107,6 +109,31 @@ void testTarget(TSetter setter, std::size_t field, std::uint32_t cachePolicyBits check(storage.words == expected, name); } +template +void testTargetWithCachePolicy(TSetter setter, std::size_t field, const char* name) { + Storage storage; + auto expected = storage.words; + expected[field] = 0x89abcdecu; + expected[field + 1] = 0x4567u; + expected[field + 2] = 0x30000004u; + check(setter(storage.packet, 3, targetAt(0x0000456789abcdecull), 4) == 0, name); + check(storage.words == expected, name); + expected[field] = 0x1000u; + expected[field + 1] = 0; + expected[field + 2] = 0xfffffu; + check(setter(storage.packet, 0, targetAt(0x1000u), 0xfffffu) == 0, name); + check(storage.words == expected, name); + expectFailure([&] { setter(storage.packet, 4, targetAt(0x2000u), 1); }); + expectFailure([&] { setter(storage.packet, 1, nullptr, 1); }); + expectFailure([&] { setter(storage.packet, 1, targetAt(0x1002u), 1); }); + expectFailure([&] { setter(storage.packet, 1, targetAt(0x2000u), 0x100000u); }); + expectFailure([&] { setter(nullptr, 1, targetAt(0x2000u), 1); }); + storage.packet[0] = 0xc00b3f00u; + expected[0] = 0xc00b3f00u; + expectFailure([&] { setter(storage.packet, 1, targetAt(0x2000u), 1); }); + check(storage.words == expected, name); +} + void testMalformedPackets() { for (const auto header : {0xc0023f00u, 0xc00b3f00u, 0xc00c3f01u, 0xc00c2200u}) { Storage storage; @@ -128,6 +155,8 @@ int main() { testCompareAddress(); testTarget(sceAgcBranchPatchSetThenTarget, 8, 0x10000000u, "then target setter wrote the wrong words"); testTarget(sceAgcBranchPatchSetElseTarget, 11, 0x20000000u, "else target setter wrote the wrong words"); + testTargetWithCachePolicy(sceAgcBranchPatchSetThenTarget_0300, 8, "3.00 then target setter wrote the wrong words"); + testTargetWithCachePolicy(sceAgcBranchPatchSetElseTarget_0300, 11, "3.00 else target setter wrote the wrong words"); testMalformedPackets(); std::puts("AGC branch patch tests passed"); return 0; diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index 58b9f10262..960dbb36cc 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -10,14 +10,20 @@ #include #include +extern "C" std::uint32_t* APS5_VABI sceAgcDcbEventWrite(CommandBuffer* buf, std::uint8_t eventType, const volatile void* address); +extern "C" std::uint64_t APS5_VABI sceAgcDcbEventWriteGetSize(std::uint8_t eventType); extern "C" std::uint32_t* APS5_VABI sceAgcDcbResetQueue(CommandBuffer* buf, std::uint32_t op, std::uint32_t state); extern "C" std::uint32_t* APS5_VABI sceAgcDcbClearState(CommandBuffer* buf, std::uint32_t command); extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetFlip(CommandBuffer* buf, std::uint32_t handle, std::int32_t index, std::uint32_t mode, std::int64_t argument); +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); @@ -29,11 +35,23 @@ extern "C" std::uint32_t* APS5_VABI sceAgcCbSetShRegisterRangeDirect(CommandBuff extern "C" std::uint32_t* APS5_VABI sceAgcDcbContextStateOp_0100(CommandBuffer* buf, std::uint32_t operation); extern "C" std::uint32_t* APS5_VABI sceAgcDcbPushMarker(CommandBuffer* buf, const char* str, std::uint32_t color); extern "C" std::uint32_t* APS5_VABI sceAgcDcbPopMarker(CommandBuffer* buf); +extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetWorkloadComplete(CommandBuffer* buf, std::uint32_t stream_id, std::uint32_t workload_id); +extern "C" int APS5_VABI sceAgcDriverRegisterWorkloadStream(std::uint32_t stream_id, const void* stream); +extern "C" int APS5_VABI sceAgcDriverUnregisterWorkloadStream(std::uint32_t stream_id); +extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetWorkloadsActive(CommandBuffer* buf, std::uint32_t stream_id, const std::uint32_t* workload_ids, std::uint32_t workload_count); +extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetWorkloadStreamInactive(CommandBuffer* buf, std::uint32_t streamId); +extern "C" std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadsActive(CommandBuffer* buf, std::uint32_t streamId, const std::uint32_t* workloadIds, std::uint32_t workloadCount); +extern "C" std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadComplete(CommandBuffer* buf, std::uint32_t streamId, std::uint32_t workloadId); +extern "C" std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadStreamInactive(CommandBuffer* buf, std::uint32_t streamId); extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetMarker(CommandBuffer* buf, const char* str, std::uint32_t color); extern "C" std::uint32_t* APS5_VABI sceAgcAcbPushMarker(CommandBuffer* buf, const char* str, std::uint32_t color); extern "C" std::uint32_t* APS5_VABI sceAgcAcbPopMarker(CommandBuffer* buf); 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 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); +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); namespace { @@ -109,6 +127,21 @@ void testPackets() { check(noDown.cursor_up == scratch.data() + 6, "allocation past the top of a buffer without a down cursor advanced its cursor"); } +void testNop() { + Storage storage; + storage.words.fill(0xdeadbeefu); + auto* single = sceAgcSetNop(&storage.buffer, 1); + auto* pair = sceAgcSetNop(&storage.buffer, 2); + auto* triple = sceAgcSetNop(&storage.buffer, 3); + const std::array expected{0xffff1000u, 0xc0001000u, 0, 0xc0011000u, 0, 0, 0xdeadbeefu}; + check(single == storage.words.data() && pair == single + 1 && triple == pair + 2, "incorrect NOP placement"); + check(std::equal(expected.begin(), expected.end(), storage.words.begin()), "incorrect NOP packet"); + check(storage.buffer.cursor_up == storage.words.data() + 6, "incorrect NOP cursor advance"); + expectFailure([&] { sceAgcSetNop(&storage.buffer, 0); }); + expectFailure([&] { sceAgcSetNop(&storage.buffer, 0x4002u); }); + check(storage.buffer.cursor_up == storage.words.data() + 6, "invalid NOP advanced the cursor"); +} + void testClearState() { Storage storage; storage.words.fill(0xdeadbeefu); @@ -116,12 +149,14 @@ void testClearState() { auto* packet = sceAgcDcbClearState(&storage.buffer, command); check(packet == storage.words.data() + command * 2 && packet[0] == 0xc0001200u && packet[1] == command, "incorrect CLEAR_STATE packet"); } - check(storage.buffer.cursor_up == storage.words.data() + 32 && storage.words[32] == 0xdeadbeefu, "incorrect CLEAR_STATE cursor advance"); + auto* maskedZero = sceAgcDcbClearState(&storage.buffer, 0x10u); + check(maskedZero == storage.words.data() + 32 && maskedZero[0] == 0xc0001200u && maskedZero[1] == 0, "CLEAR_STATE did not mask the command to four bits"); + auto* maskedF = sceAgcDcbClearState(&storage.buffer, 0xffffffffu); + check(maskedF == storage.words.data() + 34 && maskedF[0] == 0xc0001200u && maskedF[1] == 0xfu, "CLEAR_STATE did not mask the command to four bits"); + check(storage.buffer.cursor_up == storage.words.data() + 36 && storage.words[36] == 0xdeadbeefu, "incorrect CLEAR_STATE cursor advance"); const auto before = storage.words; - expectFailure([&] { sceAgcDcbClearState(&storage.buffer, 0x10u); }); - expectFailure([&] { sceAgcDcbClearState(&storage.buffer, 0xffffffffu); }); expectFailure([] { sceAgcDcbClearState(nullptr, 0); }); - check(storage.words == before && storage.buffer.cursor_up == storage.words.data() + 32, "invalid CLEAR_STATE modified the buffer"); + check(storage.words == before && storage.buffer.cursor_up == storage.words.data() + 36, "invalid CLEAR_STATE modified the buffer"); } struct ContextGrowth { @@ -186,6 +221,91 @@ void testMarkers() { expectFailure([] { sceAgcAcbSetMarker(nullptr, "frame", 0); }); } +void testWorkloads() { + Storage dcb; + const std::uint32_t ids[2] = {3, 4}; + check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 1, ids, 2) == nullptr, "workloads active packet for an unregistered stream"); + check(sceAgcDcbSetWorkloadComplete(&dcb.buffer, 1, 3) == nullptr, "workload complete packet for an unregistered stream"); + check(dcb.buffer.cursor_up == dcb.words.data(), "unregistered stream advanced the cursor"); + const std::uint32_t record[8] = {}; + check(sceAgcDriverRegisterWorkloadStream(1, record) == 0, "workload stream registration failed"); + const auto* active = sceAgcDcbSetWorkloadsActive(&dcb.buffer, 1, ids, 2); + check(active == dcb.words.data() && active[0] == Agc::Command::Header(0x10, 18, 0) && active[1] == 1 && active[2] == 0x18 && active[3] == 0, "workloads active packet"); + for (std::uint32_t word = 4; word < 18; ++word) check(active[word] == 0, "workloads active packet tail is not zero"); + const auto* complete = sceAgcDcbSetWorkloadComplete(&dcb.buffer, 1, 33); + check(complete == dcb.words.data() + 18 && complete[0] == Agc::Command::Header(0x10, 12, 0) && complete[1] == 1 && complete[2] == 33 && complete[3] == 0xffffffffu && complete[4] == 0xfffffffdu, "workload complete packet"); + for (std::uint32_t word = 5; word < 12; ++word) check(complete[word] == 0, "workload complete packet tail is not zero"); + check(dcb.buffer.cursor_up == dcb.words.data() + 30, "incorrect workload cursor advance"); + const std::uint32_t duplicate[2] = {5, 5}; + const std::uint32_t outOfRange[1] = {64}; + const std::uint32_t top[1] = {63}; + check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 1, duplicate, 2) == nullptr, "duplicate workload ids accepted"); + check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 1, outOfRange, 1) == nullptr, "workload id 64 accepted"); + check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 1, top, 1) != nullptr, "workload id 63 rejected"); + check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 1, ids, 0) == nullptr, "workload count 0 accepted"); + check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 1, nullptr, 2) == nullptr, "null workload ids accepted"); + check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 0, ids, 2) == nullptr, "stream id 0 accepted"); + check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 32, ids, 2) == nullptr, "stream id 32 accepted"); + check(sceAgcDcbSetWorkloadComplete(&dcb.buffer, 1, 64) == nullptr, "workload complete id 64 accepted"); + check(sceAgcDcbSetWorkloadsActive(nullptr, 1, ids, 2) == nullptr, "null command buffer accepted"); + check(sceAgcDcbSetWorkloadComplete(nullptr, 1, 3) == nullptr, "null command buffer accepted for complete"); + check(sceAgcDriverUnregisterWorkloadStream(1) == 0, "workload stream unregistration failed"); + check(sceAgcDcbSetWorkloadComplete(&dcb.buffer, 1, 3) == nullptr, "workload complete packet after unregistration"); +} + +void testWorkloadStreams() { + struct Vector { + std::uint32_t streamId; + std::uintptr_t workloadIds; + std::uint32_t workloadCount; + std::uint32_t workloadId; + }; + const std::array vectors{{ + {0x11111111u, 0x22222222u, 0x33333333u, 0x22222222u}, + {1, 2, 3, 2}, + {0x22222222u, static_cast(0x3333333344444444ull), 0, 0x44444444u}}}; + for (const auto& vector : vectors) { + Storage storage; + storage.words.fill(0xdeadbeefu); + const auto before = storage.words; + const auto* ids = reinterpret_cast(vector.workloadIds); + check(sceAgcDcbSetWorkloadsActive(&storage.buffer, vector.streamId, ids, vector.workloadCount) == nullptr, "DCB workloads active returned a packet for an unregistered stream"); + check(sceAgcDcbSetWorkloadComplete(&storage.buffer, vector.streamId, vector.workloadId) == nullptr, "DCB workload complete returned a packet for an unregistered stream"); + check(sceAgcDcbSetWorkloadStreamInactive(&storage.buffer, vector.streamId) == nullptr, "DCB workload stream inactive returned a packet for an unregistered stream"); + check(sceAgcAcbSetWorkloadsActive(&storage.buffer, vector.streamId, ids, vector.workloadCount) == nullptr, "ACB workloads active returned a packet for an unregistered stream"); + check(sceAgcAcbSetWorkloadComplete(&storage.buffer, vector.streamId, vector.workloadId) == nullptr, "ACB workload complete returned a packet for an unregistered stream"); + check(sceAgcAcbSetWorkloadStreamInactive(&storage.buffer, vector.streamId) == nullptr, "ACB workload stream inactive returned a packet for an unregistered stream"); + check(storage.words == before && storage.buffer.cursor_up == storage.words.data(), "a workload call for an unregistered stream wrote to the buffer"); + } + + const std::uint32_t ids[2] = {3, 4}; + Storage exhausted; + exhausted.buffer.cursor_down = exhausted.words.data() + 3; + const auto before = exhausted.words; + check(sceAgcDcbSetWorkloadStreamInactive(&exhausted.buffer, 1) == nullptr && sceAgcAcbSetWorkloadsActive(&exhausted.buffer, 1, ids, 2) == nullptr && + sceAgcAcbSetWorkloadComplete(&exhausted.buffer, 1, 3) == nullptr && sceAgcAcbSetWorkloadStreamInactive(&exhausted.buffer, 1) == nullptr, + "a workload call for an unregistered stream on an exhausted buffer returned a packet"); + check(exhausted.words == before && exhausted.buffer.cursor_up == exhausted.words.data(), "a workload call for an unregistered stream modified an exhausted buffer"); + check(sceAgcDcbSetWorkloadStreamInactive(nullptr, 1) == nullptr && sceAgcAcbSetWorkloadsActive(nullptr, 1, ids, 2) == nullptr && + sceAgcAcbSetWorkloadComplete(nullptr, 1, 3) == nullptr && sceAgcAcbSetWorkloadStreamInactive(nullptr, 1) == nullptr, + "a workload call with a null command buffer returned a packet"); + + const std::uint32_t record[8] = {}; + check(sceAgcDriverRegisterWorkloadStream(1, record) == 0, "workload stream registration failed"); + Storage registered; + registered.words.fill(0xdeadbeefu); + const auto untouched = registered.words; + expectFailure([&] { sceAgcDcbSetWorkloadStreamInactive(®istered.buffer, 1); }); + expectFailure([&] { sceAgcAcbSetWorkloadsActive(®istered.buffer, 1, ids, 2); }); + expectFailure([&] { sceAgcAcbSetWorkloadComplete(®istered.buffer, 1, 3); }); + expectFailure([&] { sceAgcAcbSetWorkloadStreamInactive(®istered.buffer, 1); }); + check(registered.words == untouched && registered.buffer.cursor_up == registered.words.data(), "a workload call that threw modified the buffer"); + check(sceAgcDcbSetWorkloadStreamInactive(nullptr, 1) == nullptr && sceAgcAcbSetWorkloadsActive(nullptr, 1, ids, 2) == nullptr && + sceAgcAcbSetWorkloadComplete(nullptr, 1, 3) == nullptr && sceAgcAcbSetWorkloadStreamInactive(nullptr, 1) == nullptr, + "a workload call with a null command buffer for a registered stream returned a packet"); + check(sceAgcDriverUnregisterWorkloadStream(1) == 0, "workload stream unregistration failed"); +} + void testIndexBuffer() { Storage storage; alignas(4) std::uint16_t indices[2]{}; @@ -205,6 +325,19 @@ void testIndexBuffer() { throw std::runtime_error("misaligned index buffer was accepted"); } +void testIndirectBase() { + Storage storage; + alignas(8) std::uint32_t arguments[4]{}; + const auto address = reinterpret_cast(arguments); + const auto* bound = sceAgcDcbSetBaseIndirectArgs(&storage.buffer, 0, arguments); + check(bound[1] == 1 && bound[2] == static_cast(address) && bound[3] == static_cast(address >> 32u), "indirect base address mismatch"); + const auto* cleared = sceAgcDcbSetBaseIndirectArgs(&storage.buffer, 1, nullptr); + check(cleared == bound + 4 && (cleared[0] & 2u) != 0 && cleared[1] == 1 && cleared[2] == 0 && cleared[3] == 0, "indirect base was not cleared"); + const auto before = storage.words; + expectFailure([&] { sceAgcDcbSetBaseIndirectArgs(&storage.buffer, 0, reinterpret_cast(address + 4)); }); + check(storage.words == before, "misaligned indirect base modified packet memory"); +} + void testContextState() { const std::array, 4> sizes{{{5}, {5, 8, 9, 3, 2}, {3, 5, 8, 9, 2}, {5, 8, 9, 3, 2, 5}}}; const std::array, 4> reservations{{{5}, {22, 3, 2}, {3, 22, 2}, {22, 3, 2, 5}}}; @@ -283,6 +416,27 @@ void testFlip() { exhausted.buffer.cursor_down = exhausted.words.data() + 5; expectFailure([&] { sceAgcDcbSetFlip(&exhausted.buffer, 1, 0, 1, 0); }); check(exhausted.buffer.cursor_up == exhausted.words.data(), "failed flip allocation advanced cursor"); + Storage acb; + acb.words.fill(0xdeadbeefu); + auto* wait = sceAgcAcbWaitUntilSafeForRendering(&acb.buffer, 0xfedcba98u, 2); + const std::array expectedWait{0xc0021018u, 0xfedcba98u, 2, 0}; + check(wait == acb.words.data() && std::equal(expectedWait.begin(), expectedWait.end(), wait), "compute rendering wait packet mismatch"); + check(acb.buffer.cursor_up == wait + 4 && wait[4] == 0xdeadbeefu, "compute rendering wait overran allocation"); + expectFailure([] { sceAgcAcbWaitUntilSafeForRendering(nullptr, 1, 0); }); + Storage full; + 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"); } @@ -320,8 +474,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() { @@ -371,15 +531,122 @@ void testMemory() { check(storage.words == beforeMask, "invalid mask patch modified packet memory"); } +struct AtomicGdsAllocation { + Storage storage; + std::uint32_t capacity = 15; + std::uint32_t calls = 0; + bool accepted = true; +}; + +bool APS5_VABI growAtomicGds(CommandBuffer* buffer, std::uint32_t count, void* userData) { + auto& allocation = *static_cast(userData); + check(count == 15, "atomic GDS allocation omitted packet or reserved words"); + ++allocation.calls; + if (!allocation.accepted) return false; + buffer->bottom = allocation.storage.words.data(); + buffer->top = buffer->bottom + allocation.capacity; + buffer->cursor_up = buffer->bottom; + buffer->cursor_down = buffer->top; + return true; +} + +void testAtomicGds() { + Storage storage; + storage.words.fill(0xdeadbeefu); + storage.buffer.cursor_up += 2; + auto* packet = sceAgcAcbAtomicGds_0900(&storage.buffer, 0xffffffffu, 1, 0xffffffffu, 0xffffffffu, + 0xabcd, 0xfedc, 0x12345678u, 0x87654321u, 0xfedcba9876543210ull, 0x0123456789abcdefull); + const std::array expected{ + 0xc0091d00u, 0x001500ffu, 0x13fu, 0xabcdu, 0xfedcu, 0x00340078u, + 0x87654321u, 0x76543210u, 0xfedcba98u, 0x89abcdefu, 0x01234567u}; + check(packet == storage.words.data() + 2, "atomic GDS returned the wrong packet address"); + check(std::equal(expected.begin(), expected.end(), packet), "atomic GDS packet or guest ABI mismatch"); + check(storage.buffer.cursor_up == packet + expected.size(), "atomic GDS did not advance by eleven words"); + check(storage.words[0] == 0xdeadbeefu && storage.words[1] == 0xdeadbeefu && + std::all_of(storage.words.begin() + 13, storage.words.end(), [](auto word) { return word == 0xdeadbeefu; }), + "atomic GDS wrote outside its packet"); + + const std::array, 9> controls{{ + {0, 0, 0}, {0, 1, 0x00020000u}, {0, 2, 0}, {0, 0xffffffffu, 0}, + {0x20u, 1, 0x00040020u}, {0x40u, 1, 0x00120040u}, + {0x200u, 1, 0x00030000u}, {0xfffffd00u, 0, 0}, {0xffu, 1, 0x001400ffu}}}; + for (const auto& entry : controls) { + Storage controlStorage; + auto* controlPacket = sceAgcAcbAtomicGds_0900(&controlStorage.buffer, entry[0], entry[1], + 0xfffffffeu, 0xffffffc0u, 0, 0, 0xff00ff00u, 0, 0, 0); + std::array controlExpected{}; + controlExpected[0] = 0xc0091d00u; + controlExpected[1] = entry[2]; + check(std::equal(controlExpected.begin(), controlExpected.end(), controlPacket), "atomic GDS control or field masking mismatch"); + } + + auto emit = [](CommandBuffer* buffer) { + return sceAgcAcbAtomicGds_0900(buffer, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + }; + Storage exact; + exact.buffer.cursor_down = exact.words.data() + 15; + exact.buffer.reserved_dw = 4; + check(emit(&exact.buffer) == exact.words.data() && exact.buffer.cursor_up == exact.words.data() + 11, + "atomic GDS rejected an exact fit with reserved space"); + const auto exactBefore = exact.words; + expectFailure([&] { emit(&exact.buffer); }); + check(exact.words == exactBefore && exact.buffer.cursor_up == exact.words.data() + 11, + "atomic GDS overwrote reserved space"); + Storage exhausted; + exhausted.buffer.cursor_down = exhausted.words.data() + 10; + const auto before = exhausted.words; + expectFailure([&] { emit(&exhausted.buffer); }); + check(exhausted.words == before && exhausted.buffer.cursor_up == exhausted.words.data(), + "failed atomic GDS allocation modified the buffer"); + expectFailure([&] { emit(nullptr); }); + + Storage noDown; + noDown.buffer.top = noDown.words.data() + 11; + noDown.buffer.cursor_down = nullptr; + check(emit(&noDown.buffer) == noDown.words.data() && noDown.buffer.cursor_up == noDown.buffer.top, + "atomic GDS did not honor a buffer with no down cursor"); + + AtomicGdsAllocation allocation; + CommandBuffer growing{nullptr, nullptr, nullptr, nullptr, growAtomicGds, &allocation, 4}; + check(emit(&growing) == allocation.storage.words.data() && allocation.calls == 1 && + growing.cursor_up == allocation.storage.words.data() + 11, "atomic GDS did not use the guest allocation callback"); + const std::array zeroPacket{0xc0091d00u}; + check(std::equal(zeroPacket.begin(), zeroPacket.end(), allocation.storage.words.begin()), + "atomic GDS callback allocation produced the wrong packet"); + for (const bool accepted : {false, true}) { + AtomicGdsAllocation failure; + failure.accepted = accepted; + failure.capacity = 14; + const auto failureBefore = failure.storage.words; + CommandBuffer failed{nullptr, nullptr, nullptr, nullptr, growAtomicGds, &failure, 4}; + expectFailure([&] { emit(&failed); }); + check(failure.calls == 1 && failure.storage.words == failureBefore && + failed.cursor_up == (accepted ? failure.storage.words.data() : nullptr), + "atomic GDS wrote a packet after failed or insufficient allocation"); + } +} + +void testDebugRaiseException() { + int result = -1; + try { + result = sceAgcDebugRaiseException(0x1234abcdu); + } catch (const std::exception& error) { + throw std::runtime_error(std::string("debug exception stopped the caller: ") + error.what()); + } + check(result == 0, "debug exception did not return 0"); +} + void testDefaults() { std::uint32_t state = 0x12345678; 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"); - expectFailure([] { sceAgcInit(14); }); - expectFailure([] { sceAgcInit(0xffffffffu); }); + check(sceAgcInit(13) == 0, "AGC version 13 initialization failed"); + 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__); @@ -389,32 +656,151 @@ 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)); + check(samplePacket == storage.words.data(), "incorrect event 0x38 return pointer"); + check(storage.words[0] == 0xc0024600u, "incorrect event 0x38 packet header"); + check(storage.words[1] == (0x100u | 0x38u), "incorrect event 0x38 first payload word"); + check(storage.words[2] == 0x1000u, "incorrect event 0x38 low address word"); + check(storage.words[3] == 0u, "incorrect event 0x38 high address word"); + check(sceAgcDcbEventWriteGetSize(0x38) == 16, "incorrect event 0x38 size"); + check(sceAgcDcbEventWriteGetSize(0x39) == 16, "incorrect event 0x39 size"); + + Storage sample39; + sceAgcDcbEventWrite(&sample39.buffer, 0x39, reinterpret_cast(0x2008)); + check(sample39.words[0] == 0xc0024600u && sample39.words[1] == (0x100u | 0x39u) && sample39.words[2] == 0x2008u && sample39.words[3] == 0u, "incorrect event 0x39 packet"); + + expectFailure([&] { sceAgcDcbEventWrite(&storage.buffer, 0x38, reinterpret_cast(0x1001)); }); + expectFailure([&] { sceAgcDcbEventWrite(&storage.buffer, 0x40, nullptr); }); + + Storage partialFlush; + sceAgcDcbEventWrite(&partialFlush.buffer, 7, nullptr); + check(partialFlush.words[0] == 0xc0004600u && partialFlush.words[1] == 0x407u, "incorrect event 7 partial flush packet"); + check(sceAgcDcbEventWriteGetSize(7) == 8, "incorrect event 7 size"); + + Storage partialFlush15; + sceAgcDcbEventWrite(&partialFlush15.buffer, 15, nullptr); + check(partialFlush15.words[0] == 0xc0004600u && partialFlush15.words[1] == 0x40fu, "incorrect event 15 partial flush packet"); + + Storage partialFlush16; + sceAgcDcbEventWrite(&partialFlush16.buffer, 16, nullptr); + check(partialFlush16.words[0] == 0xc0004600u && partialFlush16.words[1] == 0x410u, "incorrect event 16 partial flush packet"); + + Storage cacheFlushNull; + sceAgcDcbEventWrite(&cacheFlushNull.buffer, 0x16, nullptr); + check(cacheFlushNull.words[0] == 0xc0004600u && cacheFlushNull.words[1] == 0x16u, "incorrect event 0x16 null address packet"); + check(sceAgcDcbEventWriteGetSize(0x16) == 8, "incorrect event 0x16 size"); + + Storage cacheFlushWithAddress; + sceAgcDcbEventWrite(&cacheFlushWithAddress.buffer, 0x16, reinterpret_cast(0x2d2a10000ull)); + check(cacheFlushWithAddress.words[0] == 0xc0004600u && cacheFlushWithAddress.words[1] == 0x16u, "incorrect event 0x16 with address packet"); +} + } int main(int argc, char** argv) { try { if (argc == 2 && std::strcmp(argv[1], "defaults") == 0) { testDefaults(); + testDefaultsTypes(); LibcRunShutdown_nid_postfix(); std::puts("AGC initialization tests passed"); return 0; } testPackets(); + testEventWrite(); + testNop(); testClearState(); testIndexedIndirectDraws(); testMarkers(); + testWorkloads(); + testWorkloadStreams(); testIndexBuffer(); + testIndirectBase(); testContextState(); testFlip(); testRegisters(); testRegisterRange(); testPacketPayloadAddress(); testMemory(); + 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/GsPrimPayload.cpp b/core/libs/prx/libSceAgc/tests/GsPrimPayload.cpp new file mode 100644 index 0000000000..60f3d29b77 --- /dev/null +++ b/core/libs/prx/libSceAgc/tests/GsPrimPayload.cpp @@ -0,0 +1,119 @@ +#include "SceShaders.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp" + +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI sceAgcGetGsPrimPayload(std::uint32_t* payload, const Shader* gs); + +namespace { + +constexpr std::uint32_t Sentinel = 0xccccccccu; + +void check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +template +void expectFailure(TAction action, const char* message) { + try { + action(); + } catch (const std::exception& error) { + check(error.what()[0] != '\0', "empty exception message"); + return; + } + throw std::runtime_error(message); +} + +Shader makeGs(std::vector& cx) { + Shader gs{}; + gs.cx_registers = cx.empty() ? nullptr : cx.data(); + gs.num_cx_registers = static_cast(cx.size()); + return gs; +} + +void expectPayload(const Shader& gs, std::uint32_t expected, const char* message) { + std::array out{Sentinel, Sentinel}; + check(sceAgcGetGsPrimPayload(out.data(), &gs) == 0, message); + check(out[0] == expected, message); + check(out[1] == Sentinel, "the payload write went past four bytes"); +} + +void testNoRegisters() { + std::vector cx; + expectPayload(makeGs(cx), 0, "a shader without context registers reported a payload"); +} + +void testTwoComponentIndexFormat() { + std::vector cx{{ShaderRegs::SPI_SHADER_IDX_FORMAT, 2u}}; + expectPayload(makeGs(cx), 8, "a two-component index export did not report 8 bytes"); +} + +void testOnlyTheLowNibbleCounts() { + std::vector high{{ShaderRegs::SPI_SHADER_IDX_FORMAT, 0xfffffff2u}}; + expectPayload(makeGs(high), 8, "bits above IDX0_EXPORT_FORMAT changed the payload"); + for (const std::uint32_t format : {0u, 1u, 3u, 4u, 0x20u}) { + std::vector cx{{ShaderRegs::SPI_SHADER_IDX_FORMAT, format}}; + expectPayload(makeGs(cx), 0, "an index export format other than two components reported a payload"); + } +} + +void testOtherRegistersAreIgnored() { + std::vector cx{{ShaderRegs::SPI_VS_OUT_CONFIG, 2u}, {ShaderRegs::GE_MAX_OUTPUT_PER_SUBGROUP, 2u}}; + expectPayload(makeGs(cx), 0, "a register other than SPI_SHADER_IDX_FORMAT reported a payload"); +} + +void testSearchContinuesPastAMismatch() { + std::vector cx{ + {ShaderRegs::SPI_VS_OUT_CONFIG, 0u}, + {ShaderRegs::SPI_SHADER_IDX_FORMAT, 4u}, + {ShaderRegs::SPI_SHADER_IDX_FORMAT, 2u}, + }; + expectPayload(makeGs(cx), 8, "a non-matching SPI_SHADER_IDX_FORMAT entry stopped the search"); +} + +void testCountBoundsTheSearch() { + std::vector cx{{ShaderRegs::SPI_VS_OUT_CONFIG, 0u}, {ShaderRegs::SPI_SHADER_IDX_FORMAT, 2u}}; + Shader gs = makeGs(cx); + gs.num_cx_registers = 1; + expectPayload(gs, 0, "an entry past num_cx_registers was read"); +} + +void testRejections() { + std::vector cx{{ShaderRegs::SPI_SHADER_IDX_FORMAT, 2u}}; + const Shader gs = makeGs(cx); + std::uint32_t out = Sentinel; + expectFailure([&] { sceAgcGetGsPrimPayload(nullptr, &gs); }, "a null payload pointer was accepted"); + expectFailure([&] { sceAgcGetGsPrimPayload(&out, nullptr); }, "a null shader was accepted"); + Shader missing{}; + missing.num_cx_registers = 1; + expectFailure([&] { sceAgcGetGsPrimPayload(&out, &missing); }, "a register count without a register array was accepted"); + check(out == Sentinel, "a rejected call wrote the payload"); +} + +} + +int main() { + try { + testNoRegisters(); + testTwoComponentIndexFormat(); + testOnlyTheLowNibbleCounts(); + testOtherRegistersAreIgnored(); + testSearchContinuesPastAMismatch(); + testCountBoundsTheSearch(); + testRejections(); + LibcRunShutdown_nid_postfix(); + std::puts("AGC GS primitive payload tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + try { LibcRunShutdown_nid_postfix(); } + catch (const std::exception& shutdown) { std::fprintf(stderr, "shutdown: %s\n", shutdown.what()); } + return 1; + } +} diff --git a/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp b/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp index 1ea1d6ca24..652eacfea7 100644 --- a/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp +++ b/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp @@ -12,13 +12,15 @@ #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 sceAgcCreateInterpolantMapping_10000(ShaderRegister* regs, const Shader* gs, const Shader* ps); +extern "C" int APS5_VABI sceAgcUpdateInterpolantMapping_10000(ShaderRegister* regs, const Shader* gs, const Shader* ps); namespace { using Registers = std::array; const Shader* mappedPixel = nullptr; Registers preparedMapping{}; +std::size_t preparedCount = 0; unsigned mappings = 0; unsigned links = 0; @@ -59,12 +61,17 @@ 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(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"); } void testMapping() { @@ -87,7 +94,7 @@ void testMapping() { ps.num_input_semantics = static_cast(inputs.size()); auto split = filled(); - check(sceAgcUnknownCreateInterpolantMapping(split.data(), &gs, &ps) == 0, "split f16 mapping failed"); + check(sceAgcCreateInterpolantMapping_10000(split.data(), &gs, &ps) == 0, "split f16 mapping failed"); check(std::memcmp(split.data(), preparedMapping.data(), sizeof(split)) == 0, "prepared split mapping differs from published registers"); for (std::uint32_t i = 0; i < inputs.size(); ++i) { check(split[i].offset == ShaderRegs::SPI_PS_INPUT_CNTL_0 + i, "interpolant register offset changed"); @@ -99,6 +106,15 @@ void testMapping() { check(split[4].value == 0x03180002u, "full f16 interpolant mapping changed"); checkIdentity(split, static_cast(inputs.size()), "unused interpolants are not the identity"); + auto update = filled(); + const auto untouched = filled(); + const std::size_t written = inputs.size() * sizeof(ShaderRegister); + 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"); + check(std::memcmp(update.data() + inputs.size(), untouched.data(), sizeof(update) - written) == 0, "update wrote past its inputs"); + auto regular = filled(); check(sceAgcCreateInterpolantMapping_0100(regular.data(), &gs, &ps) == 0, "mapping failed"); check(std::memcmp(regular.data(), preparedMapping.data(), sizeof(regular)) == 0, "prepared mapping differs from published registers"); @@ -106,6 +122,51 @@ void testMapping() { checkIdentity(regular, static_cast(inputs.size()), "unused interpolants are not the identity"); } +void testUpdateMapping() { + std::array inputs{semantic(1, 0, 0), semantic(2, 0, 1), semantic(3, 0, 2), semantic(4, 0, 3)}; + inputs[0].is_custom = 1; + inputs[0].default_value = 2; + inputs[1].default_value = 1; + inputs[1].default_value_hi = 1; + inputs[2].default_value_hi = 2; + inputs[3].is_custom = 1; + inputs[3].default_value = 3; + inputs[3].default_value_hi = 3; + std::array outputs{semantic(1, 5, 0), semantic(2, 7, 1), semantic(4, 9, 3)}; + 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()); + + auto update = filled(); + const auto untouched = filled(); + 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"); + check(update[3].value == 0x03680709u, "full f16 interpolant update changed"); + for (std::uint32_t i = 0; i < inputs.size(); ++i) { + check(update[i].offset == ShaderRegs::SPI_PS_INPUT_CNTL_0 + i, "updated register offset changed"); + } + const std::size_t written = inputs.size() * sizeof(ShaderRegister); + check(std::memcmp(update.data() + inputs.size(), untouched.data(), sizeof(update) - written) == 0, "update wrote past its inputs"); + + auto create = filled(); + 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([&] { sceAgcUpdateInterpolantMapping_10000(update.data(), &gs, &ps); }); + check(std::memcmp(update.data(), untouched.data(), sizeof(update)) == 0, "rejected update wrote registers"); + inputs[index].default_value = inputs[index].default_value_hi; + } +} + void testRejections() { std::array inputs{}; Shader gs{}; @@ -116,19 +177,24 @@ 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([&] { 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([&] { 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); }); } } extern "C" void AgcDriverResolveShaderAbi_nid_postfix(const Shader* shader, std::span context, std::span primitive) { - check(shader != nullptr && context.size() == preparedMapping.size() && primitive.empty(), "invalid mapping preparation call"); + check(shader != nullptr && context.size() <= preparedMapping.size() && primitive.empty(), "invalid mapping preparation call"); mappedPixel = shader; std::copy(context.begin(), context.end(), preparedMapping.begin()); + preparedCount = context.size(); ++mappings; } @@ -141,8 +207,9 @@ int main() { try { testIdentity(); testMapping(); + testUpdateMapping(); testRejections(); - check(mappings == 3u && links == 2u, "mapping preparation call count changed"); + check(mappings == 7u && links == 5u, "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 65343c02df..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}; @@ -116,8 +144,8 @@ void testIndirect() { check(storage.words == expected, "count setter modified unrelated packet data"); } const auto before = storage.words; - for (const auto count : {0x4000u, 0xffffffffu}) { - expectFailure([&] { sizes[i](count); }); + for (const auto count : {0x4000u, 0xffffffffu, 0xf5f5039cu}) { + check(sizes[i](count) == 20, "indirect size depends on the count"); expectFailure([&] { setters[i](packet, count); }); } expectFailure([&] { setters[i](nullptr, 0); }); @@ -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 new file mode 100644 index 0000000000..021eb3e7c2 --- /dev/null +++ b/core/libs/prx/libSceAgc/tests/Semaphore.cpp @@ -0,0 +1,217 @@ +#include "prx/libSceAgc/Command/include/Packet.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/Shutdown.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI sceAgcSetAmmSemaphoreMemory(void* memory, std::uint64_t sizeInBytes); +extern "C" int APS5_VABI sceAgcGetSemaphoreLabel(std::uint32_t index, void** label); +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 { + +constexpr std::uint64_t blockBytes = 0x4000u; +constexpr std::uint64_t labelBytes = 32u; +constexpr std::uint64_t counterBytes = 8u; +constexpr std::uint64_t memoryBytes = 2u * blockBytes; +constexpr std::uint64_t storageBytes = 4u * blockBytes; +constexpr std::uint32_t labelCount = static_cast(memoryBytes / labelBytes); +constexpr std::uint8_t guardByte = 0xa5u; +constexpr std::uint8_t counterByte = 0x5au; + +struct AlignedRelease { + void operator()(std::uint8_t* memory) const { ::operator delete(memory, std::align_val_t{blockBytes}); } +}; + +std::unique_ptr storage; + +void check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +template +void expectFailure(TAction action) { + try { + action(); + } catch (const std::runtime_error& error) { + check(error.what()[0] != '\0', "empty exception message"); + return; + } + throw std::runtime_error("expected an exception"); +} + +std::uint8_t* memory() { + return storage.get() + blockBytes; +} + +std::vector snapshot() { + return {storage.get(), storage.get() + storageBytes}; +} + +void checkUnchanged(const std::vector& before, const char* message) { + check(std::equal(before.begin(), before.end(), storage.get()), message); +} + +void fillStorage() { + std::fill(storage.get(), storage.get() + storageBytes, guardByte); + for (std::uint64_t label = 0; label < memoryBytes; label += labelBytes) { + std::fill(memory() + label, memory() + label + counterBytes, counterByte); + std::fill(memory() + label + counterBytes, memory() + label + labelBytes, std::uint8_t{0}); + } +} + +void testUnregistered() { + std::array labels{storage.get(), storage.get(), storage.get()}; + expectFailure([&] { sceAgcGetSemaphoreLabel(0, &labels[1]); }); + expectFailure([] { sceAgcGetSemaphoreLabel(0, nullptr); }); + check(labels[0] == storage.get() && labels[1] == storage.get() && labels[2] == storage.get(), "a rejected semaphore label changed the output"); +} + +void testRegistrationRejections() { + const auto before = snapshot(); + expectFailure([] { sceAgcSetAmmSemaphoreMemory(nullptr, memoryBytes); }); + expectFailure([] { sceAgcSetAmmSemaphoreMemory(nullptr, 0); }); + expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), 0); }); + expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory() + 1, memoryBytes); }); + expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory() + labelBytes, blockBytes); }); + expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), blockBytes - 1u); }); + expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), blockBytes + labelBytes); }); + expectFailure([] { + const auto top = std::numeric_limits::max() & ~static_cast(blockBytes - 1u); + sceAgcSetAmmSemaphoreMemory(reinterpret_cast(top), memoryBytes); + }); + checkUnchanged(before, "a rejected registration changed memory"); + testUnregistered(); +} + +void testUnsettledClearing() { + for (const std::uint64_t offset : {5u * labelBytes + counterBytes, memoryBytes - 1u}) { + memory()[offset] = 0x77u; + const auto before = snapshot(); + expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes); }); + checkUnchanged(before, "a registration with unsettled clearing changed memory"); + testUnregistered(); + memory()[offset] = 0; + } +} + +void testRegistration() { + const auto before = snapshot(); + check(sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes) == 0, "registering the AMM semaphore memory failed"); + check(std::all_of(memory(), memory() + memoryBytes, [](std::uint8_t byte) { return byte == 0; }), "registration did not clear the semaphore counters"); + check(std::equal(before.begin(), before.begin() + blockBytes, storage.get()), "registration wrote before the semaphore memory"); + check(std::equal(before.begin() + blockBytes + memoryBytes, before.end(), memory() + memoryBytes), "registration wrote past the semaphore memory"); +} + +void testLabels() { + for (const std::uint32_t index : {0u, 1u, 7u, labelCount / 2u, labelCount - 1u}) { + std::array labels{storage.get(), nullptr, storage.get()}; + check(sceAgcGetSemaphoreLabel(index, &labels[1]) == 0, "a semaphore label lookup failed"); + check(labels[1] == memory() + index * labelBytes, "incorrect semaphore label address"); + check(labels[0] == storage.get() && labels[2] == storage.get(), "a semaphore label lookup wrote past its output"); + } +} + +void testLabelRejections() { + for (const std::uint32_t index : {labelCount, labelCount + 1u, std::numeric_limits::max()}) { + void* label = storage.get(); + expectFailure([&] { sceAgcGetSemaphoreLabel(index, &label); }); + check(label == storage.get(), "a rejected semaphore label changed the output"); + } + expectFailure([] { sceAgcGetSemaphoreLabel(0, nullptr); }); +} + +void testRepeatedRegistration() { + std::fill(memory(), memory() + memoryBytes, std::uint8_t{0x3c}); + const auto before = snapshot(); + expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes); }); + expectFailure([] { sceAgcSetAmmSemaphoreMemory(storage.get(), blockBytes); }); + expectFailure([] { sceAgcSetAmmSemaphoreMemory(nullptr, 0); }); + checkUnchanged(before, "a repeated registration changed memory"); + void* label = nullptr; + check(sceAgcGetSemaphoreLabel(labelCount - 1u, &label) == 0 && label == memory() + memoryBytes - labelBytes, "a repeated registration replaced the semaphore memory"); +} + +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() { + try { + storage.reset(static_cast(::operator new(storageBytes, std::align_val_t{blockBytes}))); + fillStorage(); + testUnregistered(); + testRegistrationRejections(); + testUnsettledClearing(); + testRegistration(); + testLabels(); + testMemSemaphore(); + testLabelRejections(); + testRepeatedRegistration(); + storage.reset(); + LibcRunShutdown_nid_postfix(); + std::puts("AGC semaphore tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + try { LibcRunShutdown_nid_postfix(); } + catch (const std::exception& shutdown) { std::fprintf(stderr, "shutdown: %s\n", shutdown.what()); } + return 1; + } +} diff --git a/core/libs/prx/libSceAgc/tests/ShaderFusion.cpp b/core/libs/prx/libSceAgc/tests/ShaderFusion.cpp index c0a6a4eda6..2f451acf50 100644 --- a/core/libs/prx/libSceAgc/tests/ShaderFusion.cpp +++ b/core/libs/prx/libSceAgc/tests/ShaderFusion.cpp @@ -129,21 +129,20 @@ void testFusion(std::size_t misalignment, std::uint32_t terminator = SSetpcS6) { Halves halves(terminator); SizeAlign size{}; check(sceAgcGetFusedShaderSize_0080(&size, &halves.front, &halves.back) == 0, "fused size query failed"); - check(size.m_align == 8, "fused shaders are not 256-byte aligned"); - check(size.m_size > 256 + 255 + halves.back.shader_size + sizeof(Shader), "fused size does not cover the alignment padding, code and header"); + check(size.m_size == 7 * sizeof(ShaderRegister) && size.m_align == 4, "fused size is not the fused register list"); std::vector storage(size.m_size + 512, 0xcd); auto* scratch = aligned(storage, 256) + misalignment; Shader fused{}; check(sceAgcFuseShaderHalves_0200(&fused, &halves.front, &halves.back, scratch) == 0, "fusion failed"); for (std::size_t i = 0; i < 16; ++i) check(scratch[size.m_size + i] == 0xcd, "fusion wrote past the reported size"); - auto* memory = scratch + (256 - misalignment) % 256; - const auto base = reinterpret_cast(memory); - const auto scratchBase = reinterpret_cast(scratch); + const auto base = reinterpret_cast(fused.code); + const auto* memory = reinterpret_cast(base); check(fused.type == static_cast(ShaderRegs::ShaderBinaryType::Gs), "fused shader is not a GS"); - check(fused.code == memory && fused.shader_size == 256 + halves.back.shader_size, "fused code placement is wrong"); + check(base % 256 == 0 && fused.shader_size == 256 + halves.back.shader_size, "fused code placement is wrong"); check(word(memory, 0) == SMovS0 && word(memory, 1) == VMovV0, "front program was not copied"); for (std::size_t i = 2; i < 64; ++i) check(word(memory, i) == SNop, "front program does not fall through to the back half"); check(std::memcmp(memory + 256, halves.backCode.data(), halves.back.shader_size) == 0, "back program was not copied"); + check(reinterpret_cast(fused.sh_registers) == reinterpret_cast(scratch), "fused registers are not in the caller's memory"); const auto* backLo = find(fused, 0x088u); const auto* frontLo = find(fused, 0x0C8u); check(backLo != nullptr && backLo->value == static_cast((base + 256) >> 8u), "back program address was not patched"); @@ -158,17 +157,69 @@ void testFusion(std::size_t misalignment, std::uint32_t terminator = SSetpcS6) { check(fused.num_output_semantics == 1 && fused.output_semantics[0].semantic == 3, "back outputs were not kept"); check(fused.scratch_size_dw_per_thread == 16, "scratch size is not the larger half's"); check(fused.specials != nullptr && fused.specials != halves.back.specials && fused.specials->ge_cntl.value == 0x77u, "back specials were not copied"); - const auto inside = [&](const void* pointer) { - const auto address = reinterpret_cast(pointer); - return address >= scratchBase && address < scratchBase + size.m_size; + const auto inside = [&](std::uintptr_t address) { + const auto begin = reinterpret_cast(storage.data()); + return address >= begin && address < begin + storage.size(); }; - check(inside(fused.sh_registers) && inside(fused.cx_registers) && inside(fused.specials) && inside(fused.input_semantics) && inside(fused.output_semantics), "fused tables are not in the fused memory"); - check(inside(fused.user_data) && inside(fused.user_data->direct_resource_offset) && inside(fused.user_data->sharp_resource_offset[1]), "fused user data is not in the fused memory"); + const auto program = [&](const void* pointer) { + const auto address = reinterpret_cast(pointer); + return !inside(address) && address >= base + fused.shader_size && address < base + fused.shader_size + fused.header_size + alignof(Shader); + }; + check(!inside(base) && program(fused.cx_registers) && program(fused.specials) && program(fused.input_semantics) && program(fused.output_semantics), "fused tables are not beside the fused code"); + check(program(fused.user_data) && program(fused.user_data->direct_resource_offset) && program(fused.user_data->sharp_resource_offset[1]), "fused user data is not beside the fused code"); check(fused.user_data->direct_resource_count == 2 && fused.user_data->direct_resource_offset[1] == 9u && fused.user_data->sharp_resource_offset[1][0].offset_dw == 12 && fused.user_data->sharp_resource_offset[0] == nullptr && fused.user_data->eud_size_dw == 4, "fused user data is wrong"); - const auto headerOffset = static_cast(size.m_size) - 255 - fused.header_size; - check(headerOffset % alignof(Shader) == 0 && headerOffset >= fused.shader_size, "fused header is misplaced"); - const auto* header = reinterpret_cast(memory + headerOffset); - check(header->code == fused.code && header->sh_registers == fused.sh_registers && header->user_data == fused.user_data && header->header_size == fused.header_size && header->shader_size == fused.shader_size && header->type == fused.type && header->num_sh_registers == fused.num_sh_registers, "fused memory has no self-contained header"); +} + +void testScratchReuse() { + Halves first; + Halves second; + second.backCode[0] = VMovV0; + SizeAlign size{}; + check(sceAgcGetFusedShaderSize_0080(&size, &first.front, &first.back) == 0, "fused size query failed"); + std::vector scratch(size.m_size); + Shader firstFused{}; + Shader secondFused{}; + check(sceAgcFuseShaderHalves_0200(&firstFused, &first.front, &first.back, scratch.data()) == 0, "first fusion failed"); + const auto firstCode = reinterpret_cast(firstFused.code); + check(sceAgcFuseShaderHalves_0200(&secondFused, &second.front, &second.back, scratch.data()) == 0, "second fusion failed"); + const auto secondCode = reinterpret_cast(secondFused.code); + check(firstCode != secondCode, "two fused programs share one code address"); + check(word(reinterpret_cast(firstCode), 64) == SMovS0, "fusing into reused memory overwrote the first fused program"); + check(word(reinterpret_cast(secondCode), 64) == VMovV0, "second fused program has the wrong back half"); + Shader again{}; + check(sceAgcFuseShaderHalves_0200(&again, &first.front, &first.back, scratch.data()) == 0, "repeated fusion failed"); + check(reinterpret_cast(again.code) == firstCode, "the same halves were fused into a second program"); + first.backCode[0] = VMovV0; + Shader changed{}; + check(sceAgcFuseShaderHalves_0200(&changed, &first.front, &first.back, scratch.data()) == 0, "fusion of changed halves failed"); + check(reinterpret_cast(changed.code) != firstCode && word(reinterpret_cast(firstCode), 64) == SMovS0, "changed halves reused the earlier fused program"); +} + +void 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 | backEnable), "fusion lost offchip LDS enable or changed the back-half state or front user SGPRs"); + } + } + 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 testRejections() { @@ -252,6 +303,8 @@ int main() { testFusion(0xa0); testFusion(4); testFusion(0, SSwappcNullS6); + testScratchReuse(); + testOffchipLdsEnable(); testRejections(); testHullHalves(); LibcRunShutdown_nid_postfix(); diff --git a/core/libs/prx/libSceAgc/tests/ShaderHelpers.cpp b/core/libs/prx/libSceAgc/tests/ShaderHelpers.cpp index 07759d178d..46f7c3c8e8 100644 --- a/core/libs/prx/libSceAgc/tests/ShaderHelpers.cpp +++ b/core/libs/prx/libSceAgc/tests/ShaderHelpers.cpp @@ -2,11 +2,13 @@ #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 #include #include +#include #include extern "C" int APS5_VABI sceAgcLinkShaders(ShaderRegister*, ShaderRegister*, const void*, const Shader*, const Shader*, std::uint32_t); @@ -17,6 +19,9 @@ namespace { std::array preparedContext; std::array preparedPrimitive; std::size_t stageCount = 0; +std::array linkedStages{}; +std::size_t linkedStageCount = 0; +std::size_t linkedContextCount = 0; unsigned preparations = 0; unsigned mappings = 0; unsigned links = 0; @@ -26,13 +31,63 @@ const Shader* mappedPixel = nullptr; void Require(bool condition) { if (!condition) throw std::runtime_error("shader helper preparation mismatch"); } + +void OptionalGeometryOutputRegister(ShaderSpecialRegs& special, const Shader& vertex, const Shader& hull) { + using namespace ShaderRegs; + special.vgt_gs_out_prim_type = {}; + std::array noGeometryContext{}; + Require(sceAgcCreatePrimState(noGeometryContext.data(), nullptr, nullptr, &vertex, 7) == 0); + Require(noGeometryContext[0].offset == VGT_SHADER_STAGES_EN && noGeometryContext[1].offset == VGT_GS_OUT_PRIM_TYPE && noGeometryContext[1].value == static_cast(GsOutputPrimitiveType::Rectangle2D)); + bool rejectedMissingHullOutput = false; + try { static_cast(sceAgcCreatePrimState(noGeometryContext.data(), nullptr, &hull, &vertex, 7)); } + catch (const std::runtime_error&) { rejectedMissingHullOutput = true; } + Require(rejectedMissingHullOutput); + special.vgt_shader_stages_en.value = VGT_SHADER_STAGES_GS_BIT; + bool rejectedMissingGeometryOutput = false; + try { static_cast(sceAgcCreatePrimState(noGeometryContext.data(), nullptr, nullptr, &vertex, 7)); } + catch (const std::runtime_error&) { rejectedMissingGeometryOutput = true; } + Require(rejectedMissingGeometryOutput); + special.vgt_shader_stages_en.value = 0; + special.vgt_gs_out_prim_type = {VGT_GS_OUT_PRIM_TYPE, 2}; +} + +void HandBuiltUserVgprRegister() { + using namespace ShaderRegs; + ShaderSpecialRegs handBuilt{}; + handBuilt.ge_cntl = {GE_CNTL, 0x100u}; + handBuilt.vgt_shader_stages_en = {VGT_SHADER_STAGES_EN, 0x2412010u}; + handBuilt.vgt_gs_out_prim_type = {0x2ceu, 2}; + handBuilt.ge_user_vgpr_en = {GE_USER_VGPR1, 0}; + Shader preRaster{}; + preRaster.type = static_cast(ShaderBinaryType::Gs); + preRaster.specials = &handBuilt; + std::array context{}; + std::array primitive{}; + preparations = 0; + Require(sceAgcCreatePrimState(context.data(), primitive.data(), nullptr, &preRaster, 4) == 0); + Require(context[0].offset == VGT_SHADER_STAGES_EN && context[0].value == 0x2412010u && context[1].offset == VGT_GS_OUT_PRIM_TYPE && context[1].value == static_cast(GsOutputPrimitiveType::Triangles)); + Require(primitive[0].offset == GE_CNTL && primitive[0].value == 0x100u && primitive[1].offset == GE_USER_VGPR1 && primitive[1].value == 0 && primitive[2].offset == VGT_PRIMITIVE_TYPE && primitive[2].value == 4); + Require(preparations == 1 && stageCount == 1 && preparedPrimitive[1].offset == GE_USER_VGPR1 && preparedPrimitive[1].value == 0); + handBuilt.ge_cntl.offset = 0x25cu; + std::string message; + try { static_cast(sceAgcCreatePrimState(context.data(), primitive.data(), nullptr, &preRaster, 4)); } + catch (const std::runtime_error& error) { message = error.what(); } + Require(message.find("GE_CNTL") != std::string::npos && message.find("0x25c") != std::string::npos); +} } extern "C" void AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span stages, std::span context, std::span primitive) { - Require(context.size() == 2 && primitive.size() == 3); - std::copy(context.begin(), context.end(), preparedContext.begin()); - std::copy(primitive.begin(), primitive.end(), preparedPrimitive.begin()); - stageCount = stages.size(); + Require((context.size() == 2 || context.size() == 34) && primitive.size() == 3); + if (context.size() == 2) { + std::copy(context.begin(), context.end(), preparedContext.begin()); + std::copy(primitive.begin(), primitive.end(), preparedPrimitive.begin()); + stageCount = stages.size(); + } else { + Require(stages.size() == 2); + std::copy(stages.begin(), stages.end(), linkedStages.begin()); + linkedStageCount = stages.size(); + linkedContextCount = context.size(); + } ++preparations; } @@ -91,6 +146,19 @@ int main() { } } } + preparations = 0; + std::array patchContext{}; + std::array patchPrimitive{}; + Require(sceAgcCreatePrimState(patchContext.data(), patchPrimitive.data(), nullptr, &vertex, 9) == 0); + Require(preparations == 0 && patchContext[0].offset == VGT_SHADER_STAGES_EN && patchContext[0].value == 0 && patchPrimitive[2].offset == VGT_PRIMITIVE_TYPE && patchPrimitive[2].value == 9); + ShaderSpecialRegs hullSpecial = special; + hullSpecial.vgt_shader_stages_en.value = VGT_SHADER_STAGES_HS_BIT; + hull.specials = &hullSpecial; + Require(sceAgcCreatePrimState(patchContext.data(), patchPrimitive.data(), &hull, &vertex, 4) == 0); + Require(preparations == 0 && patchContext[0].value == VGT_SHADER_STAGES_HS_BIT && patchPrimitive[2].value == 4); + Require(sceAgcCreatePrimState(patchContext.data(), patchPrimitive.data(), &hull, &vertex, 9) == 0); + Require(preparations == 1 && stageCount == 2 && preparedContext[0].value == VGT_SHADER_STAGES_HS_BIT && preparedPrimitive[2].value == 9 && patchPrimitive[2].value == 9); + hull.specials = &special; std::array linkedContext{}; std::array linkedPrimitive{}; for (auto& value : linkedContext) value.value = 0xdeadbeef; @@ -116,14 +184,22 @@ int main() { Require(rejected); for (const auto value : interpolants) Require(value.value == 0xdeadbeef); pixel.num_input_semantics = 1; + preparations = mappings = links = 0; + linkedStageCount = linkedContextCount = 0; Require(sceAgcLinkShaders(linkedContext.data(), linkedPrimitive.data(), nullptr, &vertex, &pixel, 7) == 0); + Require(preparations == 2 && linkedStageCount == 2 && linkedContextCount == linkedContext.size()); + Require(linkedStages[0] == &vertex && linkedStages[1] == &pixel); preparations = mappings = links = 0; Require(sceAgcCreatePrimState(nullptr, nullptr, nullptr, nullptr, 7) == 0); Require(sceAgcCreateInterpolantMapping_0100(interpolants.data(), &vertex, nullptr) == 0); Require(preparations == 0 && mappings == 0 && links == 0); + OptionalGeometryOutputRegister(special, vertex, hull); + HandBuiltUserVgprRegister(); + LibcRunShutdown_nid_postfix(); std::cout << "shader helper preparation tests passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; + try { LibcRunShutdown_nid_postfix(); } catch (...) {} return 1; } } diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 262959ff01..198d4a0865 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -45,8 +45,40 @@ add_custom_command( DEPENDS "${agcTextureDetileShaderSource}" glslang-standalone VERBATIM ) +set(agcTextureDetileImageShaderSpv ${agcTextureDetileGeneratedDir}/TextureDetileImage.spv) +set(agcTextureDetileImageShaderHeader ${agcTextureDetileGeneratedDir}/TextureDetileImage_spv.h) +add_custom_command( + OUTPUT "${agcTextureDetileImageShaderHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}" + COMMAND "$" -V --target-env vulkan1.1 -DLINEAR_IMAGE=1 -o "${agcTextureDetileImageShaderSpv}" "${agcTextureDetileShaderSource}" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcTextureDetileImageShaderSpv} -DOUTPUT=${agcTextureDetileImageShaderHeader} + -DSYMBOL=TEXTURE_DETILE_IMAGE_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${agcTextureDetileShaderSource}" glslang-standalone + VERBATIM +) 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(agcMeshArgumentsShaderSpv ${agcTextureDetileGeneratedDir}/MeshArguments.spv) set(agcMeshArgumentsShaderHeader ${agcTextureDetileGeneratedDir}/MeshArguments_spv.h) add_custom_command( @@ -59,6 +91,45 @@ add_custom_command( VERBATIM ) +set(agcCmaskClearShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/CmaskClear.comp) +set(agcCmaskClearShaderSpv ${agcTextureDetileGeneratedDir}/CmaskClear.spv) +set(agcCmaskClearShaderHeader ${agcTextureDetileGeneratedDir}/CmaskClear_spv.h) +add_custom_command( + OUTPUT "${agcCmaskClearShaderHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}" + COMMAND "$" -V --target-env vulkan1.1 -o "${agcCmaskClearShaderSpv}" "${agcCmaskClearShaderSource}" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcCmaskClearShaderSpv} -DOUTPUT=${agcCmaskClearShaderHeader} + -DSYMBOL=CMASK_CLEAR_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${agcCmaskClearShaderSource}" glslang-standalone + 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(agcSampleCounterShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/SampleCounter.comp) set(agcSampleCounterShaderSpv ${agcTextureDetileGeneratedDir}/SampleCounter.spv) set(agcSampleCounterShaderHeader ${agcTextureDetileGeneratedDir}/SampleCounter_spv.h) @@ -72,14 +143,83 @@ add_custom_command( VERBATIM ) +set(agcSampleDumpsShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/SampleDumps.comp) +set(agcSampleDumpsShaderSpv ${agcTextureDetileGeneratedDir}/SampleDumps.spv) +set(agcSampleDumpsShaderHeader ${agcTextureDetileGeneratedDir}/SampleDumps_spv.h) +add_custom_command( + OUTPUT "${agcSampleDumpsShaderHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}" + COMMAND "$" -V --target-env vulkan1.1 -o "${agcSampleDumpsShaderSpv}" "${agcSampleDumpsShaderSource}" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcSampleDumpsShaderSpv} -DOUTPUT=${agcSampleDumpsShaderHeader} + -DSYMBOL=SAMPLE_DUMPS_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${agcSampleDumpsShaderSource}" glslang-standalone + VERBATIM +) +foreach(agcPresentationStage IN ITEMS Vert Frag) + string(TOLOWER "${agcPresentationStage}" agcPresentationExtension) + string(TOUPPER "${agcPresentationStage}" agcPresentationSymbol) + set(agcPresentationShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/PresentationPass.${agcPresentationExtension}) + set(agcPresentationShaderSpv ${agcTextureDetileGeneratedDir}/PresentationPass${agcPresentationStage}.spv) + set(agcPresentationShaderHeader ${agcTextureDetileGeneratedDir}/PresentationPass${agcPresentationStage}_spv.h) + add_custom_command( + OUTPUT "${agcPresentationShaderHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}" + COMMAND "$" -V --target-env vulkan1.1 -o "${agcPresentationShaderSpv}" "${agcPresentationShaderSource}" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcPresentationShaderSpv} -DOUTPUT=${agcPresentationShaderHeader} + -DSYMBOL=PRESENTATION_PASS_${agcPresentationSymbol}_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${agcPresentationShaderSource}" "${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/PqToSdr.glsl" glslang-standalone + VERBATIM + ) + list(APPEND agcPresentationShaderHeaders ${agcPresentationShaderHeader}) +endforeach() +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 ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Dispatch/Dispatch.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Dispatch/DispatchAhead.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Dispatch/DispatchCache.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Dispatch/DispatchCacheStatistics.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Dispatch/DispatchInsert.cpp @@ -88,13 +228,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 @@ -114,6 +257,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 @@ -123,23 +267,38 @@ 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 ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/ShaderMemory.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/BdaFeatures.cpp ${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 ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ColorTargetLayout.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ColorTargetTransfer.cpp ${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 + ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ParallelCompare.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ShaderInputState.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ShaderResources.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ShaderAddressResources.cpp @@ -154,12 +313,24 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/DccMetadata.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/TextureTiling.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/TextureDetiler.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/MultisampleReadback.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} + ${agcCmaskClearShaderHeader} ${agcMeshArgumentsShaderHeader} + ${agcTextureDetileImageShaderHeader} + ${agcTextureRepackShaderHeader} + ${agcDispatchArgumentsShaderHeader} ${agcSampleCounterShaderHeader} + ${agcSampleDumpsShaderHeader} + ${agcPresentationShaderHeaders} ${CMAKE_CURRENT_SOURCE_DIR}/Eq/src/Event.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Eq/src/Query.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Resource/src/Lifecycle.cpp @@ -171,15 +342,29 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Submit/src/Dcb.cpp ) +find_package(Git QUIET) +set(agcTraceRevisionHeader ${agcTextureDetileGeneratedRoot}/prx/libSceAgcDriver/TraceRevision.hpp) +add_custom_target(agc_trace_revision + COMMAND ${CMAKE_COMMAND} -DGIT=${GIT_EXECUTABLE} -DSOURCE=${CMAKE_SOURCE_DIR} -DOUTPUT=${agcTraceRevisionHeader} -P ${CMAKE_CURRENT_SOURCE_DIR}/trace_revision.cmake + BYPRODUCTS ${agcTraceRevisionHeader} + VERBATIM +) +set(agcZlibIncludeDir ${CMAKE_SOURCE_DIR}/3rdparty/libjpeg-turbo/src/spng/zlib) +set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/TraceFile.cpp PROPERTIES + INCLUDE_DIRECTORIES ${agcZlibIncludeDir} +) + add_library(libSceAgcDriver SHARED EXCLUDE_FROM_ALL ${agcDriverSources} ) +add_dependencies(libSceAgcDriver agc_trace_revision) target_include_directories(libSceAgcDriver PRIVATE ${LIBS_INCLUDE_DIR} ${agcTextureDetileGeneratedRoot} - ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include - ${CMAKE_SOURCE_DIR}/3rdparty/SPIRV-Headers/include + ${CMAKE_SOURCE_DIR}/core/shader/recompiler/RdnaDecoder/include + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} + ${ANYPS5_SPIRV_HEADERS_INCLUDE_DIR} ) file(GLOB_RECURSE agcShaderCacheGuardSources CONFIGURE_DEPENDS RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" @@ -195,15 +380,58 @@ add_shader_recompiler(shader_recompiler_agc_driver libc target_link_libraries(libSceAgcDriver PRIVATE shader_recompiler_agc_driver + decoder_png SDL2-static libc libkernel + jpeg_turbo + ${CMAKE_DL_LIBS} ) +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_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_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) @@ -224,16 +452,116 @@ if(BUILD_TESTING) set_tests_properties(agc_profile_output PROPERTIES ENVIRONMENT_MODIFICATION "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) + configure_windows_unwind(agc_stack_walk_tests) + add_test(NAME agc_stack_walk COMMAND agc_stack_walk_tests) + set_tests_properties(agc_stack_walk PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(agc_stack_walk PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(agc_performance_trace_tests tests/PerformanceTrace.cpp Execution/src/TraceFile.cpp) + target_include_directories(agc_performance_trace_tests PRIVATE ${LIBS_INCLUDE_DIR} ${agcZlibIncludeDir}) + target_link_libraries(agc_performance_trace_tests PRIVATE libc jpeg_turbo) + configure_windows_unwind(agc_performance_trace_tests) + add_test(NAME agc_performance_trace COMMAND agc_performance_trace_tests) + set_tests_properties(agc_performance_trace PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(agc_performance_trace PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + 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) - set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/GuestMemory.cpp PROPERTIES COMPILE_DEFINITIONS _WIN32_WINNT=0x0602) + set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/GuestMemory.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Lifecycle.cpp PROPERTIES COMPILE_DEFINITIONS _WIN32_WINNT=0x0602) endif() foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") endforeach() +add_test_executable(agc_driver_register_key_tests EXCLUDE_FROM_ALL tests/RegisterKey.cpp) +target_include_directories(agc_driver_register_key_tests PRIVATE + ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/core/shader/recompiler + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} +) +target_link_libraries(agc_driver_register_key_tests PRIVATE + libSceAgcDriver + libc + libkernel +) +set_target_properties(agc_driver_register_key_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_driver_register_key_tests) +if(BUILD_TESTING) + set_target_properties(agc_driver_register_key_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_driver_register_key COMMAND agc_driver_register_key_tests) + set_tests_properties(agc_driver_register_key PROPERTIES TIMEOUT 30 ENVIRONMENT "APS5_VERIFY_REGISTER_KEYS=1") + if(WIN32) + set_tests_properties(agc_driver_register_key PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + +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 ) @@ -241,8 +569,8 @@ add_test_executable(agc_driver_tests EXCLUDE_FROM_ALL target_include_directories(agc_driver_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler - ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include - ${CMAKE_SOURCE_DIR}/3rdparty/SPIRV-Headers/include + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} + ${ANYPS5_SPIRV_HEADERS_INCLUDE_DIR} ) target_link_libraries(agc_driver_tests PRIVATE @@ -254,16 +582,73 @@ target_link_libraries(agc_driver_tests PRIVATE set_target_properties(agc_driver_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_driver_tests) +add_test_executable(agc_driver_wait_shutdown_tests EXCLUDE_FROM_ALL tests/WaitShutdown.cpp) +target_include_directories(agc_driver_wait_shutdown_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_driver_wait_shutdown_tests PRIVATE libSceAgcDriver libkernel libc) +set_target_properties(agc_driver_wait_shutdown_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_driver_wait_shutdown_tests) +if(BUILD_TESTING) + set_target_properties(agc_driver_wait_shutdown_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_driver_wait_shutdown COMMAND agc_driver_wait_shutdown_tests) + set_tests_properties(agc_driver_wait_shutdown PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_GPU_WAIT_TIMEOUT_MS=20" + TIMEOUT 10 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_driver_resolution_shutdown COMMAND agc_driver_wait_shutdown_tests --resolution-shutdown) + set_tests_properties(agc_driver_resolution_shutdown PROPERTIES TIMEOUT 10 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_wait_shutdown agc_driver_resolution_shutdown PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + 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) @@ -273,6 +658,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) @@ -283,21 +669,122 @@ 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() +add_test_executable(agc_thread_priority_tests EXCLUDE_FROM_ALL + tests/ThreadPriority.cpp + Execution/src/ThreadPriority.cpp +) +target_include_directories(agc_thread_priority_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_thread_priority_tests PRIVATE libc ${CMAKE_DL_LIBS}) +set_target_properties(agc_thread_priority_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_thread_priority_tests) +if(BUILD_TESTING) + set_target_properties(agc_thread_priority_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_thread_priority COMMAND agc_thread_priority_tests) + if(WIN32) + set_tests_properties(agc_thread_priority PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + else() + foreach(mode rt high) + add_test(NAME agc_thread_priority_refused_${mode} COMMAND agc_thread_priority_tests refused) + set_tests_properties(agc_thread_priority_refused_${mode} PROPERTIES ENVIRONMENT + "APS5_THREAD_PRIORITY=${mode};DBUS_SYSTEM_BUS_ADDRESS=unix:path=/nonexistent/aps5-no-rtkit") + endforeach() + endif() +endif() + add_test_executable(agc_structurizer_tests EXCLUDE_FROM_ALL tests/Structurizer.cpp) target_link_libraries(agc_structurizer_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_structurizer_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_structurizer_tests) +add_test_executable(agc_fragment_barycentric_tests EXCLUDE_FROM_ALL tests/FragmentBarycentric.cpp) +target_include_directories(agc_fragment_barycentric_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) +set_target_properties(agc_fragment_barycentric_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_fragment_barycentric_tests) +if(BUILD_TESTING) + set_target_properties(agc_fragment_barycentric_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + 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) +configure_windows_unwind(agc_pipeline_link_optimization_tests) +if(BUILD_TESTING) + set_target_properties(agc_pipeline_link_optimization_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_pipeline_link_optimization COMMAND agc_pipeline_link_optimization_tests) +endif() + +add_test_executable(agc_robust_buffer_access_tests EXCLUDE_FROM_ALL tests/RobustBufferAccess.cpp) +target_include_directories(agc_robust_buffer_access_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) +set_target_properties(agc_robust_buffer_access_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_robust_buffer_access_tests) +if(BUILD_TESTING) + set_target_properties(agc_robust_buffer_access_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_robust_buffer_access COMMAND agc_robust_buffer_access_tests) +endif() + +add_test_executable(agc_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 ) -target_include_directories(agc_subgroup_clock_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include) +target_include_directories(agc_subgroup_clock_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) set_target_properties(agc_subgroup_clock_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_subgroup_clock_tests) if(BUILD_TESTING) @@ -333,6 +820,10 @@ set_target_properties(agc_spirv_narrow_constant_store_tests PROPERTIES CXX_EXTEN configure_windows_unwind(agc_spirv_narrow_constant_store_tests) if(ANYPS5_ENABLE_SPIRV_TOOLS) target_link_libraries(agc_spirv_narrow_constant_store_tests PRIVATE SPIRV-Tools-static) + add_test_executable(agc_spirv13_image_tests EXCLUDE_FROM_ALL tests/Spirv13Image.cpp) + target_link_libraries(agc_spirv13_image_tests PRIVATE shader_recompiler_agc_driver libc) + set_target_properties(agc_spirv13_image_tests PROPERTIES CXX_EXTENSIONS OFF) + configure_windows_unwind(agc_spirv13_image_tests) add_test_executable(agc_spirv_optimizer_tests EXCLUDE_FROM_ALL tests/SpirvOptimizer.cpp) target_link_libraries(agc_spirv_optimizer_tests PRIVATE shader_recompiler_agc_driver libc SPIRV-Tools-static) set_target_properties(agc_spirv_optimizer_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -352,6 +843,84 @@ 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) +configure_windows_unwind(agc_spirv_branch_targets_tests) +if(BUILD_TESTING) + set_target_properties(agc_spirv_branch_targets_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_spirv_branch_targets COMMAND agc_spirv_branch_targets_tests) + set_tests_properties(agc_spirv_branch_targets PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1") + if(WIN32) + set_tests_properties(agc_spirv_branch_targets PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + +add_test_executable(agc_spirv_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) +configure_windows_unwind(agc_spirv_lds_atomic_lock_tests) +if(BUILD_TESTING) + set_target_properties(agc_spirv_lds_atomic_lock_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_spirv_lds_atomic_lock COMMAND agc_spirv_lds_atomic_lock_tests) + set_tests_properties(agc_spirv_lds_atomic_lock PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1") + if(WIN32) + set_tests_properties(agc_spirv_lds_atomic_lock PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + 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) @@ -362,6 +931,44 @@ target_link_libraries(agc_masked_select_tests PRIVATE shader_recompiler_agc_driv set_target_properties(agc_masked_select_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_masked_select_tests) +add_test_executable(agc_ir_reclaim_tests EXCLUDE_FROM_ALL tests/IrReclaim.cpp) +target_link_libraries(agc_ir_reclaim_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_ir_reclaim_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_ir_reclaim_tests) + +add_test_executable(agc_vector_window_tests EXCLUDE_FROM_ALL tests/VectorWindow.cpp) +target_link_libraries(agc_vector_window_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_vector_window_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_vector_window_tests) + +add_test_executable(agc_constant_bit_test_tests EXCLUDE_FROM_ALL tests/ConstantBitTest.cpp) +target_link_libraries(agc_constant_bit_test_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_constant_bit_test_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_constant_bit_test_tests) + +add_test_executable(agc_integer_narrowing_tests EXCLUDE_FROM_ALL tests/IntegerNarrowing.cpp) +target_link_libraries(agc_integer_narrowing_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_integer_narrowing_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_integer_narrowing_tests) + +add_test_executable(agc_scalar_select_mask_tests EXCLUDE_FROM_ALL tests/ScalarSelectMask.cpp) +target_link_libraries(agc_scalar_select_mask_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_scalar_select_mask_tests PROPERTIES CXX_EXTENSIONS OFF) +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) @@ -376,13 +983,111 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_denormal_flush_elimination_tests EXCLUDE_FROM_ALL tests/DenormalFlushElimination.cpp) +target_link_libraries(agc_denormal_flush_elimination_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_denormal_flush_elimination_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_denormal_flush_elimination_tests) +if(BUILD_TESTING) + set_target_properties(agc_denormal_flush_elimination_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_denormal_flush_elimination COMMAND agc_denormal_flush_elimination_tests) + if(WIN32) + set_tests_properties(agc_denormal_flush_elimination PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + +add_test_executable(agc_redundant_value_elimination_tests EXCLUDE_FROM_ALL tests/RedundantValueElimination.cpp) +target_link_libraries(agc_redundant_value_elimination_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_redundant_value_elimination_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_redundant_value_elimination_tests) +if(BUILD_TESTING) + set_target_properties(agc_redundant_value_elimination_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_redundant_value_elimination COMMAND agc_redundant_value_elimination_tests) + if(WIN32) + set_tests_properties(agc_redundant_value_elimination PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + +add_test_executable(agc_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) +configure_windows_unwind(agc_vop2_encoding_decode_tests) +if(BUILD_TESTING) + set_target_properties(agc_vop2_encoding_decode_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_vop2_encoding_decode COMMAND agc_vop2_encoding_decode_tests) + if(WIN32) + set_tests_properties(agc_vop2_encoding_decode PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + +add_test_executable(agc_vop3_interp_decode_tests EXCLUDE_FROM_ALL tests/Vop3InterpDecode.cpp) +target_link_libraries(agc_vop3_interp_decode_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_vop3_interp_decode_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_vop3_interp_decode_tests) +if(BUILD_TESTING) + set_target_properties(agc_vop3_interp_decode_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_vop3_interp_decode COMMAND agc_vop3_interp_decode_tests) + if(WIN32) + set_tests_properties(agc_vop3_interp_decode PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + 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 ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include) +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) set_target_properties(agc_dcc_key_sharing_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_dcc_key_sharing_tests) @@ -396,21 +1101,42 @@ if(BUILD_TESTING) endif() add_test_executable(agc_buffer_pool_tests EXCLUDE_FROM_ALL tests/BufferPool.cpp) -target_include_directories(agc_buffer_pool_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include) +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) 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() + +add_test_executable(agc_device_memory_fallback_tests EXCLUDE_FROM_ALL tests/DeviceMemoryFallback.cpp) +target_include_directories(agc_device_memory_fallback_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) +target_link_libraries(agc_device_memory_fallback_tests PRIVATE libSceAgcDriver libc) +set_target_properties(agc_device_memory_fallback_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_device_memory_fallback_tests) +if(BUILD_TESTING) + set_target_properties(agc_device_memory_fallback_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_device_memory_fallback COMMAND agc_device_memory_fallback_tests) + if(WIN32) + set_tests_properties(agc_device_memory_fallback PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() add_test_executable(agc_display_buffer_dcc_tests EXCLUDE_FROM_ALL tests/DisplayBufferDcc.cpp) -target_include_directories(agc_display_buffer_dcc_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include) +target_include_directories(agc_display_buffer_dcc_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) target_link_libraries(agc_display_buffer_dcc_tests PRIVATE libSceAgcDriver libc) set_target_properties(agc_display_buffer_dcc_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_display_buffer_dcc_tests) @@ -423,19 +1149,84 @@ 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) configure_windows_unwind(agc_validate_program_tests) +add_test_executable(agc_formatted_buffer_once_tests EXCLUDE_FROM_ALL tests/FormattedBufferOnce.cpp) +target_link_libraries(agc_formatted_buffer_once_tests PRIVATE shader_recompiler_agc_driver libc) +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) +configure_windows_unwind(agc_rect_list_position_invariant_tests) +if(BUILD_TESTING) + set_target_properties(agc_rect_list_position_invariant_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_rect_list_position_invariant COMMAND agc_rect_list_position_invariant_tests) + if(WIN32) + set_tests_properties(agc_rect_list_position_invariant PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + +add_test_executable(agc_formatted_buffer_byte_stores_tests EXCLUDE_FROM_ALL tests/FormattedBufferByteStores.cpp) +target_link_libraries(agc_formatted_buffer_byte_stores_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_formatted_buffer_byte_stores_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_formatted_buffer_byte_stores_tests) + +add_test_executable(agc_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) set_target_properties(agc_shader_disk_cache_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_shader_disk_cache_tests) +add_test_executable(agc_placeholder_images_tests EXCLUDE_FROM_ALL tests/PlaceholderImages.cpp) +target_include_directories(agc_placeholder_images_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include) +target_link_libraries(agc_placeholder_images_tests PRIVATE shader_recompiler_agc_driver libc) +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} ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include) +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) set_target_properties(agc_shader_device_profile_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_shader_device_profile_tests) @@ -448,12 +1239,19 @@ target_link_libraries(agc_shader_control_flow_tests PRIVATE shader_recompiler_ag set_target_properties(agc_shader_control_flow_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_shader_control_flow_tests) +add_test_executable(agc_shader_mode_register_tests EXCLUDE_FROM_ALL tests/ShaderModeRegisters.cpp) +target_include_directories(agc_shader_mode_register_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_shader_mode_register_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_shader_mode_register_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_shader_mode_register_tests) + add_test_executable(agc_driver_flip_tests EXCLUDE_FROM_ALL tests/Flip.cpp ) target_include_directories(agc_driver_flip_tests PRIVATE ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include ) target_link_libraries(agc_driver_flip_tests PRIVATE @@ -507,19 +1305,72 @@ foreach(agcTriangleStage IN ITEMS vert frag) list(APPEND agcTriangleHeaders "${agcTriangleHeader}") endforeach() +foreach(agcCoverStage IN ITEMS vert frag) + string(TOUPPER "${agcCoverStage}" agcCoverSymbol) + set(agcCoverShaderSpv ${agcTestShaderGeneratedRoot}/Cover_${agcCoverStage}.spv) + set(agcCoverShaderHeader ${agcTestShaderGeneratedRoot}/Cover_${agcCoverStage}_spv.h) + add_custom_command( + OUTPUT "${agcCoverShaderHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTestShaderGeneratedRoot}" + COMMAND "$" -V --target-env vulkan1.1 -o "${agcCoverShaderSpv}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/Cover.${agcCoverStage}" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcCoverShaderSpv} -DOUTPUT=${agcCoverShaderHeader} + -DSYMBOL=COVER_${agcCoverSymbol}_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/Cover.${agcCoverStage}" glslang-standalone + VERBATIM + ) + list(APPEND agcCoverShaderHeaders ${agcCoverShaderHeader}) +endforeach() + +add_test_executable(agc_driver_cmask_fast_clear_tests EXCLUDE_FROM_ALL + tests/CmaskFastClear.cpp + ${agcCoverShaderHeaders} +) + +target_include_directories(agc_driver_cmask_fast_clear_tests PRIVATE + ${LIBS_INCLUDE_DIR} + ${agcTestShaderGeneratedRoot} + ${CMAKE_SOURCE_DIR}/core/shader/recompiler + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} +) + +target_link_libraries(agc_driver_cmask_fast_clear_tests PRIVATE + libSceAgcDriver + SDL2-static + libc +) + +set_target_properties(agc_driver_cmask_fast_clear_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_driver_cmask_fast_clear_tests) + +set(agcBufferUpdateSource ${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/BufferUpdate.comp) +set(agcBufferUpdateSpv ${agcTestShaderGeneratedRoot}/BufferUpdate.spv) +set(agcBufferUpdateHeader ${agcTestShaderGeneratedRoot}/BufferUpdate_spv.h) +add_custom_command( + OUTPUT "${agcBufferUpdateHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTestShaderGeneratedRoot}" + COMMAND "$" -V --target-env vulkan1.1 -o "${agcBufferUpdateSpv}" "${agcBufferUpdateSource}" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcBufferUpdateSpv} -DOUTPUT=${agcBufferUpdateHeader} + -DSYMBOL=BUFFER_UPDATE_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${agcBufferUpdateSource}" glslang-standalone + VERBATIM +) + add_test_executable(agc_driver_recorder_tests EXCLUDE_FROM_ALL tests/Recorder.cpp + tests/HostImportLifetime.cpp tests/ResidentPresent.cpp ${agcTriangleHeaders} ${agcSampleLodShaderHeader} ${agcSampleArrayShaderHeader} + ${agcCoverShaderHeaders} + ${agcBufferUpdateHeader} ) target_include_directories(agc_driver_recorder_tests PRIVATE ${LIBS_INCLUDE_DIR} ${agcTestShaderGeneratedRoot} ${CMAKE_SOURCE_DIR}/core/shader/recompiler - ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} ) target_link_libraries(agc_driver_recorder_tests PRIVATE @@ -533,15 +1384,235 @@ 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_pipeline_cache COMMAND agc_driver_recorder_tests --pipeline-cache-only) + 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_pipeline_cache PROPERTIES ENVIRONMENT_MODIFICATION + 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) + set_tests_properties(agc_pipeline_library_off PROPERTIES ENVIRONMENT "APS5_NO_GPL=1") + add_test(NAME agc_pipeline_library_fast_only COMMAND agc_driver_recorder_tests --pipeline-library-only) + set_tests_properties(agc_pipeline_library_fast_only PROPERTIES ENVIRONMENT "APS5_NO_PIPELINE_LTO=1" SKIP_RETURN_CODE 77) + if(WIN32) + 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 agc_pipeline_cache_file PROPERTIES SKIP_RETURN_CODE 77) + if(WIN32) + 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_trimmed_import COMMAND agc_driver_recorder_tests --trimmed-import-only) + set_tests_properties(agc_reimport_bench agc_trimmed_import PROPERTIES SKIP_RETURN_CODE 77) + set_tests_properties(agc_reimport_bench agc_trimmed_import PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test(NAME agc_unchanged_cpu_stamps COMMAND agc_driver_recorder_tests --unchanged-cpu-stamps-only) + set_tests_properties(agc_unchanged_cpu_stamps PROPERTIES SKIP_RETURN_CODE 77) + if(WIN32) + set_tests_properties(agc_unchanged_cpu_stamps PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + 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) + set_tests_properties(agc_storage_integer_clear PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test(NAME agc_storage_upload_lifetime COMMAND agc_driver_recorder_tests --storage-upload-lifetime-only) + add_test(NAME agc_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) + set_tests_properties(agc_storage_upload_lifetime agc_storage_upload_failure PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + set_tests_properties(agc_storage_baseline_settled PROPERTIES SKIP_RETURN_CODE 77) + if(WIN32) + 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") + if(WIN32) + add_test(NAME agc_storage_cpu_snapshot COMMAND agc_driver_recorder_tests --storage-cpu-snapshot-only) + set_tests_properties(agc_storage_cpu_snapshot PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 SKIP_RETURN_CODE 77 + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + add_test(NAME agc_descriptor_pool_recycling COMMAND agc_driver_recorder_tests --descriptor-pool-only) + set_tests_properties(agc_descriptor_pool_recycling PROPERTIES SKIP_RETURN_CODE 77) + add_test(NAME agc_draw_snapshot_cap COMMAND agc_driver_recorder_tests --draw-snapshot-cap-only) + 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) + add_test(NAME agc_pending_write_snapshot COMMAND agc_driver_recorder_tests --pending-write-snapshot-only) + set_tests_properties(agc_pending_write_snapshot PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_pending_write_snapshot PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + set_tests_properties(agc_draw_inputs_in_place PROPERTIES SKIP_RETURN_CODE 77) + add_test(NAME agc_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_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_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 + "PATH=path_list_prepend:$") + endif() + add_test(NAME agc_pending_key_stores COMMAND agc_driver_recorder_tests --pending-key-stores-only) + set_tests_properties(agc_pending_key_stores PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_shadowed_key_stores COMMAND agc_driver_recorder_tests --shadowed-key-stores-only) + set_tests_properties(agc_shadowed_key_stores PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_sample_dumps COMMAND agc_driver_recorder_tests --sample-dumps-only) + set_tests_properties(agc_sample_dumps PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_draw_descriptors COMMAND agc_driver_recorder_tests --draw-descriptors-only) + set_tests_properties(agc_draw_descriptors PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60) + if(WIN32) + set_tests_properties(agc_pending_key_stores agc_shadowed_key_stores agc_sample_dumps agc_draw_descriptors PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test(NAME agc_driver_host_import_lifetime COMMAND agc_driver_recorder_tests --host-import-lifetime-only) + add_test(NAME agc_driver_host_import_mutation_race COMMAND agc_driver_recorder_tests --host-import-mutation-race-only) + set_tests_properties(agc_driver_host_import_lifetime agc_driver_host_import_mutation_race PROPERTIES + ENVIRONMENT "APS5_PIN_WAIT_MS=2000" SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_driver_unit_shadow COMMAND agc_driver_recorder_tests --unit-shadow-only) + add_test(NAME agc_driver_unit_shadow_budget COMMAND agc_driver_recorder_tests --unit-shadow-only) + set_tests_properties(agc_driver_unit_shadow PROPERTIES + ENVIRONMENT "APS5_PIN_WAIT_MS=2000" SKIP_RETURN_CODE 77 TIMEOUT 30) + set_tests_properties(agc_driver_unit_shadow_budget PROPERTIES + ENVIRONMENT "APS5_PIN_WAIT_MS=2000;APS5_UNIT_SHADOW_MIB=8" SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_driver_host_import_lifetime agc_driver_host_import_mutation_race agc_driver_unit_shadow agc_driver_unit_shadow_budget PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + set_property(TEST agc_driver_host_import_lifetime APPEND PROPERTY ENVIRONMENT_MODIFICATION + "APS5_RELEASE_UNDER_LOCK=unset:" "APS5_RELEASE_ON_UNLOCK=unset:") endif() add_test_executable(agc_recorder_kept_bytes_tests EXCLUDE_FROM_ALL tests/RecorderKeptBytes.cpp) -target_include_directories(agc_recorder_kept_bytes_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include) +target_include_directories(agc_recorder_kept_bytes_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) target_link_libraries(agc_recorder_kept_bytes_tests PRIVATE libSceAgcDriver libc) set_target_properties(agc_recorder_kept_bytes_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_recorder_kept_bytes_tests) @@ -554,6 +1625,126 @@ 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) + if(WIN32) + set_tests_properties(agc_host_import_retire PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + +add_test_executable(agc_host_import_sparse_tests EXCLUDE_FROM_ALL tests/HostImportSparseDmaBuf.cpp) +target_include_directories(agc_host_import_sparse_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) +target_link_libraries(agc_host_import_sparse_tests PRIVATE libSceAgcDriver libc) +set_target_properties(agc_host_import_sparse_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_host_import_sparse_tests) +if(BUILD_TESTING) + set_target_properties(agc_host_import_sparse_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_host_import_sparse COMMAND agc_host_import_sparse_tests) + set_tests_properties(agc_host_import_sparse PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_host_import_sparse PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + +add_test_executable(agc_driver_presentation_pass_tests EXCLUDE_FROM_ALL + tests/PresentationPass.cpp +) + +target_include_directories(agc_driver_presentation_pass_tests PRIVATE + ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/core/shader/recompiler + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} +) + +target_link_libraries(agc_driver_presentation_pass_tests PRIVATE + libSceAgcDriver + SDL2-static + libc +) + +set_target_properties(agc_driver_presentation_pass_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_driver_presentation_pass_tests) +if(BUILD_TESTING) + set_target_properties(agc_driver_presentation_pass_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_driver_presentation_pass COMMAND agc_driver_presentation_pass_tests) + set_tests_properties(agc_driver_presentation_pass PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_presentation_pass PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + +add_test_executable(agc_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 ) @@ -564,6 +1755,7 @@ target_include_directories(agc_driver_pm4_tests PRIVATE target_link_libraries(agc_driver_pm4_tests PRIVATE libSceAgcDriver + shader_recompiler_agc_driver libc libkernel ) @@ -583,128 +1775,424 @@ add_test_executable(agc_driver_graphics_tests EXCLUDE_FROM_ALL tests/GuestSamplerResource.cpp tests/TextureDetiler.cpp tests/DepthSurfaceReuse.cpp - Graphics/src/Draw.cpp - Graphics/src/DepthSurface.cpp - Graphics/src/Pipeline.cpp - Graphics/src/State.cpp - Graphics/src/ColorTargetLayout.cpp - Graphics/src/ColorTargetTransfer.cpp - Graphics/src/Resources.cpp - Graphics/src/ShaderResources.cpp - Graphics/src/ShaderAddressResources.cpp - Graphics/src/GuestBufferMemory.cpp - Graphics/src/BdaResources.cpp - Graphics/src/BufferAddress.cpp - Graphics/src/ShaderValidation.cpp - Graphics/src/GuestTextureResource.cpp - Graphics/src/GuestSamplerResource.cpp - Graphics/src/TextureFormat.cpp - Graphics/src/DccMetadata.cpp - Graphics/src/TextureTiling.cpp - Graphics/src/TextureDetiler.cpp - Graphics/src/Texture.cpp - Graphics/src/UnitShadow.cpp - Graphics/src/Sampler.cpp - Graphics/src/Recorder.cpp - Graphics/src/ShaderInputState.cpp - ${agcTextureDetileShaderHeader} - ${agcMeshArgumentsShaderHeader} - ${agcSampleCounterShaderHeader} - Execution/src/GuestMemory.cpp - Execution/src/Pm4.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} + $ ${agcTextureDetileGeneratedRoot} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${CMAKE_SOURCE_DIR}/core/shader/recompiler/IntermediateRepresentation/include ${CMAKE_SOURCE_DIR}/core/shader/recompiler/ControlFlow/include ${CMAKE_SOURCE_DIR}/core/shader/recompiler/RdnaDecoder/include - ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include - ${CMAKE_SOURCE_DIR}/3rdparty/SPIRV-Headers/include + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} + ${ANYPS5_SPIRV_HEADERS_INCLUDE_DIR} ) target_link_libraries(agc_driver_graphics_tests PRIVATE - shader_recompiler_agc_driver + libSceAgcDriver libc 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) +foreach(agcVertexRangeShader IN ITEMS VertexRange.vert VertexRangeProducer.frag) + string(REPLACE "." "_" agcVertexRangeName "${agcVertexRangeShader}") + string(TOUPPER "${agcVertexRangeName}_SPV" agcVertexRangeSymbol) + set(agcVertexRangeSpv ${agcTestShaderGeneratedRoot}/${agcVertexRangeName}.spv) + set(agcVertexRangeHeader ${agcTestShaderGeneratedRoot}/${agcVertexRangeName}_spv.h) + add_custom_command( + OUTPUT "${agcVertexRangeHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTestShaderGeneratedRoot}" + COMMAND "$" -V --target-env vulkan1.1 -o "${agcVertexRangeSpv}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/${agcVertexRangeShader}" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcVertexRangeSpv} -DOUTPUT=${agcVertexRangeHeader} + -DSYMBOL=${agcVertexRangeSymbol} -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/${agcVertexRangeShader}" glslang-standalone + VERBATIM + ) + list(APPEND agcVertexRangeHeaders ${agcVertexRangeHeader}) +endforeach() + +target_sources(agc_driver_recorder_tests PRIVATE ${agcVertexRangeHeaders}) + +if(BUILD_TESTING) + add_test(NAME agc_driver_vertex_target_range_copied COMMAND agc_driver_recorder_tests --vertex-target-range-copied) + add_test(NAME agc_driver_vertex_target_range_imported COMMAND agc_driver_recorder_tests --vertex-target-range-imported) + set_tests_properties(agc_driver_vertex_target_range_copied agc_driver_vertex_target_range_imported PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_driver_vertex_target_range_copied agc_driver_vertex_target_range_imported PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_driver_bda_device_tests EXCLUDE_FROM_ALL tests/BdaDevice.cpp tests/BdaShader.cpp tests/BdaExecution.cpp tests/BdaContracts.cpp - Execution/src/BdaFeatures.cpp - Graphics/src/Resources.cpp - Graphics/src/BufferAddress.cpp + tests/HostImportFailure.cpp ) target_include_directories(agc_driver_bda_device_tests PRIVATE ${LIBS_INCLUDE_DIR} + $ ${CMAKE_SOURCE_DIR}/core/shader/recompiler - ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} ) -target_link_libraries(agc_driver_bda_device_tests PRIVATE SDL2-static libc shader_recompiler_agc_driver) +target_link_libraries(agc_driver_bda_device_tests PRIVATE libSceAgcDriver SDL2-static libc) set_target_properties(agc_driver_bda_device_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_driver_bda_device_tests) +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 - ${agcDriverSources} -) +add_test_executable(agc_driver_mesh_tests EXCLUDE_FROM_ALL tests/Mesh.cpp) target_include_directories(agc_driver_mesh_tests PRIVATE ${LIBS_INCLUDE_DIR} - ${agcTextureDetileGeneratedRoot} - ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include - ${CMAKE_SOURCE_DIR}/3rdparty/SPIRV-Headers/include + ${CMAKE_SOURCE_DIR}/core/shader/recompiler + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} + ${ANYPS5_SPIRV_HEADERS_INCLUDE_DIR} ) -target_link_libraries(agc_driver_mesh_tests PRIVATE shader_recompiler_agc_driver SDL2-static libc libkernel) +target_link_libraries(agc_driver_mesh_tests PRIVATE libSceAgcDriver SDL2-static libc) set_target_properties(agc_driver_mesh_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_driver_mesh_tests) +set(ANYPS5_AGC_EXECUTION_TESTS "" CACHE STRING "tests/execution sources to build, by file name without extension; the others build a stub that reports a skip. Empty builds all") file(GLOB agcExecutionTestSources CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/tests/execution/*.cpp) set(agcExecutionTests) foreach(agcExecutionTestSource IN LISTS agcExecutionTestSources) get_filename_component(agcExecutionTestName ${agcExecutionTestSource} NAME_WE) + if(ANYPS5_AGC_EXECUTION_TESTS AND NOT agcExecutionTestName IN_LIST ANYPS5_AGC_EXECUTION_TESTS) + set(agcExecutionTestSource ${CMAKE_CURRENT_SOURCE_DIR}/tests/ExecutionTestNotBuilt.cpp) + endif() string(REGEX REPLACE "([a-z0-9])([A-Z])" "\\1_\\2" agcExecutionTestName "${agcExecutionTestName}") string(TOLOWER "agc_driver_${agcExecutionTestName}_tests" agcExecutionTest) add_test_executable(${agcExecutionTest} EXCLUDE_FROM_ALL ${agcExecutionTestSource}) target_include_directories(${agcExecutionTest} PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler - ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include - ${CMAKE_SOURCE_DIR}/3rdparty/SPIRV-Headers/include + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} + ${ANYPS5_SPIRV_HEADERS_INCLUDE_DIR} ) target_link_libraries(${agcExecutionTest} PRIVATE libSceAgcDriver SDL2-static libc) set_target_properties(${agcExecutionTest} PROPERTIES CXX_EXTENSIONS OFF) 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}") + if(WIN32) + set_tests_properties(agc_driver_store_pending_identity PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + +if(BUILD_TESTING) + add_test(NAME agc_driver_native_msaa_vertex_input COMMAND agc_driver_sample_multisample_target_tests --native-vertex-input) + set_tests_properties(agc_driver_native_msaa_vertex_input PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_native_msaa_vertex_input PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test(NAME agc_driver_undithered_alpha_to_coverage_state COMMAND agc_driver_graphics_tests --undithered-alpha-to-coverage-state) + add_test(NAME agc_driver_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 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 ${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) + set(agcProvokingShaderHeader ${agcTestShaderGeneratedRoot}/ProvokingVertex_${stage}_spv.h) + string(TOUPPER "PROVOKING_VERTEX_${stage}_SPV" agcProvokingShaderSymbol) + add_custom_command( + OUTPUT "${agcProvokingShaderHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTestShaderGeneratedRoot}" + COMMAND "$" -V --target-env vulkan1.1 -o "${agcProvokingShaderSpv}" "${agcProvokingShaderSource}" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcProvokingShaderSpv} -DOUTPUT=${agcProvokingShaderHeader} + -DSYMBOL=${agcProvokingShaderSymbol} -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${agcProvokingShaderSource}" glslang-standalone + VERBATIM + ) + target_sources(agc_driver_provoking_vertex_tests PRIVATE ${agcProvokingShaderHeader}) +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 $) target_link_libraries(agc_driver_interpolation_modes_tests PRIVATE shader_recompiler_agc_driver) +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 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) + set_tests_properties(agc_shader_mode_register PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 20) + if(WIN32) + set_tests_properties(agc_shader_mode_register PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + 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_bindless PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + 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() - 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_vop1_control_decode_tests agc_validate_program_tests agc_shader_disk_cache_tests agc_shader_device_profile_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 ${agcExecutionTests}) - if(agcTest STREQUAL "agc_driver_constant_store_tests") + 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}") + add_test(NAME agc_shader_layout_pair COMMAND agc_shader_memory_tests --layout-pair) + add_test(NAME agc_shader_layout_pair_off COMMAND agc_shader_memory_tests --layout-pair) + set_tests_properties(agc_shader_layout_pair PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_tests_properties(agc_shader_layout_pair_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_LAYOUT_PAIR=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + 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_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_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() string(REGEX REPLACE "_tests$" "" agcTestName "${agcTest}") @@ -716,28 +2204,473 @@ 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) + 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" + SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_tests_properties(agc_driver_emulated_color_compare agc_driver_emulated_color_compare_srgb_decode PROPERTIES TIMEOUT 30) + if(WIN32) + 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) + add_test(NAME agc_shared_write_partial_collect_two_pass COMMAND agc_shared_write_tests --partial-collect) + set_tests_properties(agc_shared_write_partial_collect_two_pass PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_SINGLE_PASS_COLLECT=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + set_tests_properties(agc_shared_write_partial_collect agc_shared_write_partial_collect_two_pass PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + set_target_properties(agc_driver_pipeline_optimized_readback_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + foreach(pipelineReadbackCase IN ITEMS d32 d16 color32 color64) + foreach(pipelineReadbackMode IN ITEMS default fast) + set(pipelineReadbackTest agc_driver_pipeline_optimized_readback_${pipelineReadbackCase}) + set(pipelineReadbackEnvironment "ANYPS5_NO_SHADER_CACHE=1") + if(pipelineReadbackMode STREQUAL "fast") + string(APPEND pipelineReadbackTest "_fast") + list(APPEND pipelineReadbackEnvironment "APS5_NO_PIPELINE_LTO=1") + endif() + add_test(NAME ${pipelineReadbackTest} COMMAND agc_driver_pipeline_optimized_readback_tests ${pipelineReadbackCase}) + set_tests_properties(${pipelineReadbackTest} PROPERTIES ENVIRONMENT "${pipelineReadbackEnvironment}" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(${pipelineReadbackTest} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + endforeach() + endforeach() + add_test(NAME agc_vector_window_stage_inputs COMMAND agc_vector_window_tests --stage-inputs-only) + set_tests_properties(agc_vector_window_stage_inputs PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_tests_properties(agc_vector_window_stage_inputs agc_driver_relative_stage_inputs PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_vector_window_stage_inputs PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + add_test(NAME agc_driver_layered_volume_vertex COMMAND agc_driver_layered_volume_draw_tests --vertex-only) + add_test(NAME agc_driver_layered_volume_vertex_no_gpl COMMAND agc_driver_layered_volume_draw_tests --vertex-only) + set_tests_properties(agc_driver_layered_volume_vertex agc_driver_layered_volume_vertex_no_gpl PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_property(TEST agc_driver_layered_volume_vertex_no_gpl APPEND PROPERTY ENVIRONMENT "APS5_NO_GPL=1") + if(WIN32) + set_tests_properties(agc_driver_layered_volume_vertex agc_driver_layered_volume_vertex_no_gpl PROPERTIES + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + add_test(NAME agc_driver_f64_denorm_preserve_spirv COMMAND agc_driver_f64_fma_subnormals_tests --verify-spv-denorm-mode) + set_tests_properties(agc_driver_f64_denorm_preserve_spirv PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_f64_denorm_preserve_spirv PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + add_test(NAME agc_driver_f16_to_f32_unpack_spirv COMMAND agc_driver_f16_to_f32_unpack_tests --verify-spirv) + set_tests_properties(agc_driver_f16_to_f32_unpack_spirv PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_f16_to_f32_unpack_spirv PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + add_test(NAME agc_structurizer_intersection_off COMMAND agc_structurizer_tests) + set_tests_properties(agc_structurizer_intersection_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_DOM_INTERSECTION=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_structurizer_iteration_selection_off COMMAND agc_structurizer_tests) + set_tests_properties(agc_structurizer_iteration_selection_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_ITERATION_SELECTION=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_shader_control_flow_iteration_selection_off COMMAND agc_shader_control_flow_tests) + set_tests_properties(agc_shader_control_flow_iteration_selection_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_ITERATION_SELECTION=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_structurizer_forced_fallback COMMAND agc_structurizer_tests) + set_tests_properties(agc_structurizer_forced_fallback PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_FORCE_STRUCTURIZER_FALLBACK=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_shader_control_flow_forced_fallback COMMAND agc_shader_control_flow_tests) + set_tests_properties(agc_shader_control_flow_forced_fallback PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_FORCE_STRUCTURIZER_FALLBACK=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_structurizer_intersection_off agc_structurizer_iteration_selection_off agc_shader_control_flow_iteration_selection_off agc_structurizer_forced_fallback agc_shader_control_flow_forced_fallback PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + add_test(NAME agc_driver_gated_dispatch_capture_short_timeout COMMAND agc_driver_gated_dispatch_capture_tests) + set_tests_properties(agc_driver_gated_dispatch_capture_short_timeout PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_GPU_WAIT_TIMEOUT_MS=1;APS5_NO_PIPELINE_LTO=1" + SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_gated_dispatch_capture_short_timeout PROPERTIES ENVIRONMENT_MODIFICATION + "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) + add_test(NAME agc_driver_flat_apertures_${flatAperturesPart} COMMAND agc_driver_flat_apertures_tests --part ${flatAperturesPart}/14) + set_tests_properties(agc_driver_flat_apertures_${flatAperturesPart} PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_flat_apertures_${flatAperturesPart} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + 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_driver_sampled_cache_budget_memory PROPERTIES ENVIRONMENT_MODIFICATION + 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) + 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_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 + # stage, which must include PA_CL_VS_OUT_CNTL. + add_test(NAME agc_driver_vertex_clip_cull_draw_no_key COMMAND agc_driver_vertex_clip_cull_draw_tests no-key) + set_tests_properties(agc_driver_vertex_clip_cull_draw_no_key PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_DRAW_KEY=1" SKIP_RETURN_CODE 77) + if(WIN32) + set_tests_properties(agc_driver_vertex_clip_cull_draw_no_key PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_driver_pm4_failure COMMAND agc_driver_pm4_tests failure) - set_tests_properties(agc_driver_pm4_failure PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77) + 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) - set_tests_properties(agc_driver_pm4_failure PROPERTIES ENVIRONMENT_MODIFICATION + 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_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) + add_test(NAME agc_driver_pm4_priority_refused COMMAND agc_driver_pm4_tests priority-refused) + set_tests_properties(agc_driver_pm4_priority_refused PROPERTIES ENVIRONMENT + "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 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) + 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 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() - foreach(agcTest IN ITEMS agc_spirv_narrow_constant_store_tests agc_spirv_optimizer_tests agc_driver_constant_store_tests) + 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() endif() @@ -755,6 +2688,9 @@ if(BUILD_TESTING) ${agcSpirvEnvironment} "$") set_tests_properties(${agcTestName} PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 60 SKIP_RETURN_CODE 77) + if(agcTest STREQUAL "agc_spirv13_image_tests") + set_tests_properties(${agcTestName} PROPERTIES TIMEOUT 30) + endif() if(agcTest STREQUAL "agc_driver_constant_store_tests") set_tests_properties(${agcTestName} PROPERTIES RUN_SERIAL TRUE) endif() @@ -764,10 +2700,21 @@ if(BUILD_TESTING) endif() endforeach() endforeach() - foreach(preparedMode IN ITEMS indirect uncached debug debug_failure) + 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") add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --fail-before-registration) set_tests_properties(${preparedTest} PROPERTIES WILL_FAIL TRUE TIMEOUT 30) @@ -775,8 +2722,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() @@ -784,6 +2740,26 @@ if(BUILD_TESTING) set_tests_properties(${preparedTest} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endforeach() + add_test(NAME agc_driver_shutdown_threads_joined COMMAND agc_driver_shutdown_threads_tests --joined) + add_test(NAME agc_driver_shutdown_threads_compare_helpers COMMAND agc_driver_shutdown_threads_tests --compare-helpers) + add_test(NAME agc_driver_shutdown_threads_draw_committer COMMAND agc_driver_shutdown_threads_tests --draw-committer) + add_test(NAME agc_driver_shutdown_threads_shader_cache_writer COMMAND agc_driver_shutdown_threads_tests --shader-cache-writer) + add_test(NAME agc_driver_shutdown_threads_late_shader_cache COMMAND agc_driver_shutdown_threads_tests --late-shader-cache) + add_test(NAME agc_driver_shutdown_threads_async_log_held COMMAND agc_driver_shutdown_threads_tests --async-log-held) + add_test(NAME agc_driver_shutdown_threads_exit_after_start COMMAND agc_driver_shutdown_threads_tests --exit-after-start) + set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer agc_driver_shutdown_threads_late_shader_cache agc_driver_shutdown_threads_async_log_held agc_driver_shutdown_threads_exit_after_start PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer agc_driver_shutdown_threads_late_shader_cache agc_driver_shutdown_threads_async_log_held agc_driver_shutdown_threads_exit_after_start PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + add_test(NAME agc_driver_compute_user_data_calls_registered COMMAND agc_driver_compute_user_data_calls_tests --registered) + set_tests_properties(agc_driver_compute_user_data_calls_registered PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_compute_user_data_calls_registered PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + set_tests_properties(agc_driver_prepared_shaders + agc_driver_prepared_shaders_indirect agc_driver_prepared_shaders_uncached + agc_driver_prepared_shaders_debug agc_driver_prepared_shaders_debug_failure + PROPERTIES RESOURCE_LOCK shader_failure_capture) endif() option(AGC_BUILD_VISUAL_TEST "Enable the standalone AGC SPIR-V visual test" OFF) @@ -826,16 +2802,19 @@ if(AGC_BUILD_VISUAL_TEST) ${CMAKE_SOURCE_DIR}/core/shader/recompiler/IntermediateRepresentation/include ${CMAKE_SOURCE_DIR}/core/shader/recompiler/ControlFlow/include ${CMAKE_SOURCE_DIR}/core/shader/recompiler/RdnaDecoder/include - ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include - ${CMAKE_SOURCE_DIR}/3rdparty/SPIRV-Headers/include + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} + ${ANYPS5_SPIRV_HEADERS_INCLUDE_DIR} ) target_link_libraries(agc_driver_visual_test PRIVATE shader_recompiler_agc_driver + decoder_png SDL2-static libc libkernel + jpeg_turbo ) + add_dependencies(agc_driver_visual_test agc_trace_revision) set_target_properties(agc_driver_visual_test PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_driver_visual_test) @@ -887,9 +2866,147 @@ 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} + ${CMAKE_SOURCE_DIR}/core/shader/recompiler + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} + ${ANYPS5_SPIRV_HEADERS_INCLUDE_DIR} +) +target_link_libraries(agc_compute_pipeline_replay PRIVATE libSceAgcDriver libc) +set_target_properties(agc_compute_pipeline_replay PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_compute_pipeline_replay) + +if(BUILD_TESTING) + add_test_executable(agc_compute_pipeline_capture_tests tests/ComputePipelineCapture.cpp Execution/src/ComputePipelineCapture.cpp) + target_include_directories(agc_compute_pipeline_capture_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) + target_link_libraries(agc_compute_pipeline_capture_tests PRIVATE libc) + set_target_properties(agc_compute_pipeline_capture_tests PROPERTIES CXX_EXTENSIONS OFF) + configure_windows_unwind(agc_compute_pipeline_capture_tests) + add_test(NAME agc_compute_descriptor_layout_flags COMMAND agc_driver_graphics_tests --descriptor-layout-flags) + set_tests_properties(agc_compute_descriptor_layout_flags PROPERTIES TIMEOUT 30) + add_test(NAME agc_driver_covered_centroid_state COMMAND agc_driver_graphics_tests --covered-centroid-state) + set_tests_properties(agc_driver_covered_centroid_state PROPERTIES TIMEOUT 30) + 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() +endif() + +if(BUILD_TESTING AND ANYPS5_ENABLE_SPIRV_TOOLS) + add_test_executable(agc_compute_pipeline_replay_tests tests/ComputePipelineReplay.cpp) + target_include_directories(agc_compute_pipeline_replay_tests PRIVATE + ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/core/shader/recompiler + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} + ${ANYPS5_SPIRV_HEADERS_INCLUDE_DIR} + ) + target_link_libraries(agc_compute_pipeline_replay_tests PRIVATE libSceAgcDriver libc) + set_target_properties(agc_compute_pipeline_replay_tests PROPERTIES CXX_EXTENSIONS OFF) + configure_windows_unwind(agc_compute_pipeline_replay_tests) + add_test(NAME agc_compute_pipeline_replay COMMAND agc_compute_pipeline_replay_tests) + set_tests_properties(agc_compute_pipeline_replay PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77) + add_dependencies(agc_compute_pipeline_replay_tests agc_compute_pipeline_replay agc_driver_branch_past_endpgm_tests) + add_test(NAME agc_compute_pipeline_capture_run COMMAND ${CMAKE_COMMAND} + -DFIXTURE=$ + -DREPLAY=$ + -DOUTPUT_ROOT=${CMAKE_CURRENT_BINARY_DIR}/capture-tests + -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/ComputePipelineCaptureRun.cmake) + set_tests_properties(agc_compute_pipeline_capture_run PROPERTIES TIMEOUT 30 SKIP_REGULAR_EXPRESSION "compute pipeline capture run: skipped, no usable Vulkan device") + if(WIN32) + set_tests_properties(agc_compute_pipeline_replay agc_compute_pipeline_capture_run PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + if(BUILD_TESTING) add_test_executable(texture_format_lookup_tests ${LIBS_INCLUDE_DIR}/tests/TextureFormatLookup.cpp) - target_include_directories(texture_format_lookup_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include) + target_include_directories(texture_format_lookup_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) target_link_libraries(texture_format_lookup_tests PRIVATE libSceAgcDriver libc) configure_windows_unwind(texture_format_lookup_tests) add_test(NAME texture_format_lookup COMMAND texture_format_lookup_tests) @@ -898,7 +3015,7 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() add_test_executable(linear_display_buffer_tests ${LIBS_INCLUDE_DIR}/tests/LinearDisplayBuffer.cpp) - target_include_directories(linear_display_buffer_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_include_directories(linear_display_buffer_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) target_link_libraries(linear_display_buffer_tests PRIVATE libSceAgcDriver libc) configure_windows_unwind(linear_display_buffer_tests) add_test(NAME linear_display_buffer COMMAND linear_display_buffer_tests) @@ -915,6 +3032,15 @@ if(BUILD_TESTING) set_tests_properties(guest_agc_driver_resources PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_agc_driver_debug_unavailable_tests ${LIBS_INCLUDE_DIR}/tests/GuestAgcDriverDebugUnavailable.cpp) + target_include_directories(guest_agc_driver_debug_unavailable_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_agc_driver_debug_unavailable_tests PRIVATE libSceAgcDriver libc) + configure_windows_unwind(guest_agc_driver_debug_unavailable_tests) + add_test(NAME guest_agc_driver_debug_unavailable COMMAND guest_agc_driver_debug_unavailable_tests) + if(WIN32) + set_tests_properties(guest_agc_driver_debug_unavailable PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_rendering_wait_tests ${LIBS_INCLUDE_DIR}/tests/GuestRenderingWait.cpp) target_include_directories(guest_rendering_wait_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_rendering_wait_tests PRIVATE libSceAgcDriver libc) @@ -988,6 +3114,14 @@ if(BUILD_TESTING) set_tests_properties(vector_fused_multiply_add PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(vector_constant_factor_tininess_tests ${LIBS_INCLUDE_DIR}/tests/VectorConstantFactorTininess.cpp) + target_link_libraries(vector_constant_factor_tininess_tests PRIVATE shader_recompiler_agc_driver libc) + configure_windows_unwind(vector_constant_factor_tininess_tests) + add_test(NAME vector_constant_factor_tininess COMMAND vector_constant_factor_tininess_tests) + if(WIN32) + set_tests_properties(vector_constant_factor_tininess PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(scalar_saveexec_tests ${LIBS_INCLUDE_DIR}/tests/ScalarSaveexec.cpp) target_link_libraries(scalar_saveexec_tests PRIVATE shader_recompiler_agc_driver libc) configure_windows_unwind(scalar_saveexec_tests) @@ -1012,6 +3146,14 @@ if(BUILD_TESTING) set_tests_properties(scalar_bitcount64 PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(bitfield_mask_tests ${LIBS_INCLUDE_DIR}/tests/BitfieldMask.cpp) + target_link_libraries(bitfield_mask_tests PRIVATE shader_recompiler_agc_driver libc) + configure_windows_unwind(bitfield_mask_tests) + add_test(NAME bitfield_mask COMMAND bitfield_mask_tests) + if(WIN32) + set_tests_properties(bitfield_mask PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(scalar_scratch_load_tests ${LIBS_INCLUDE_DIR}/tests/ScalarScratchLoad.cpp) target_link_libraries(scalar_scratch_load_tests PRIVATE shader_recompiler_agc_driver libc) configure_windows_unwind(scalar_scratch_load_tests) @@ -1020,6 +3162,14 @@ if(BUILD_TESTING) set_tests_properties(scalar_scratch_load PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(aperture_sources_tests ${LIBS_INCLUDE_DIR}/tests/ApertureSources.cpp) + target_link_libraries(aperture_sources_tests PRIVATE shader_recompiler_agc_driver libc) + configure_windows_unwind(aperture_sources_tests) + add_test(NAME aperture_sources COMMAND aperture_sources_tests) + if(WIN32) + set_tests_properties(aperture_sources PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(scalar_call_tests ${LIBS_INCLUDE_DIR}/tests/ScalarCall.cpp) target_link_libraries(scalar_call_tests PRIVATE shader_recompiler_agc_driver libc) configure_windows_unwind(scalar_call_tests) @@ -1044,6 +3194,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) @@ -1055,6 +3213,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_embedded_vertex_fetch_tests EXCLUDE_FROM_ALL tests/EmbeddedVertexFetch.cpp) +target_link_libraries(agc_embedded_vertex_fetch_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_embedded_vertex_fetch_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_embedded_vertex_fetch_tests) +if(BUILD_TESTING) + set_target_properties(agc_embedded_vertex_fetch_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_embedded_vertex_fetch COMMAND agc_embedded_vertex_fetch_tests) + if(WIN32) + set_tests_properties(agc_embedded_vertex_fetch PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_rdna_truncated_modifier_tests EXCLUDE_FROM_ALL tests/RdnaTruncatedModifier.cpp) target_link_libraries(agc_rdna_truncated_modifier_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_rdna_truncated_modifier_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -1081,6 +3252,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) @@ -1108,6 +3293,63 @@ 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) +configure_windows_unwind(agc_position_invariance_tests) + +add_test_executable(agc_vertex_layer_export_tests EXCLUDE_FROM_ALL tests/VertexLayerExport.cpp) +target_link_libraries(agc_vertex_layer_export_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_vertex_layer_export_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_vertex_layer_export_tests) +if(BUILD_TESTING) + set_target_properties(agc_vertex_layer_export_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_vertex_layer_export COMMAND agc_vertex_layer_export_tests) + set_tests_properties(agc_vertex_layer_export PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1") + if(WIN32) + set_tests_properties(agc_vertex_layer_export PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + +if(BUILD_TESTING) + set_target_properties(agc_position_invariance_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_position_invariance COMMAND agc_position_invariance_tests) + set_tests_properties(agc_position_invariance PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1") + if(WIN32) + set_tests_properties(agc_position_invariance PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + +add_test_executable(agc_vertex_barrier_tests EXCLUDE_FROM_ALL tests/VertexBarrier.cpp) +target_link_libraries(agc_vertex_barrier_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_vertex_barrier_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_vertex_barrier_tests) +if(BUILD_TESTING) + set_target_properties(agc_vertex_barrier_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_vertex_barrier COMMAND agc_vertex_barrier_tests) + set_tests_properties(agc_vertex_barrier PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1") + if(WIN32) + set_tests_properties(agc_vertex_barrier PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + if(BUILD_TESTING) add_test_executable(image_by_tests ${LIBS_INCLUDE_DIR}/tests/ImageBy.cpp) target_link_libraries(image_by_tests PRIVATE shader_recompiler_agc_driver libc) @@ -1135,3 +3377,48 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +add_test_executable(agc_embedded_vertex_fetch_aliasing_tests EXCLUDE_FROM_ALL tests/EmbeddedVertexFetchAliasing.cpp) +target_link_libraries(agc_embedded_vertex_fetch_aliasing_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_embedded_vertex_fetch_aliasing_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_embedded_vertex_fetch_aliasing_tests) +if(BUILD_TESTING) + set_target_properties(agc_embedded_vertex_fetch_aliasing_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_embedded_vertex_fetch_aliasing COMMAND agc_embedded_vertex_fetch_aliasing_tests) + if(WIN32) + set_tests_properties(agc_embedded_vertex_fetch_aliasing PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + +add_test_executable(agc_constant_phi_tests EXCLUDE_FROM_ALL tests/ConstantPhi.cpp) +target_link_libraries(agc_constant_phi_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_constant_phi_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_constant_phi_tests) +if(BUILD_TESTING) + set_target_properties(agc_constant_phi_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_constant_phi COMMAND agc_constant_phi_tests) + if(WIN32) + set_tests_properties(agc_constant_phi PROPERTIES ENVIRONMENT_MODIFICATION + "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 new file mode 100644 index 0000000000..da6858faff --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/ComputePipelineCapture.hpp @@ -0,0 +1,79 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_COMPUTEPIPELINECAPTURE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_COMPUTEPIPELINECAPTURE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct ComputeDescriptorBinding { + std::uint32_t binding = 0; + VkDescriptorType type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + std::uint32_t count = 1; + VkShaderStageFlags stages = VK_SHADER_STAGE_COMPUTE_BIT; + VkDescriptorBindingFlags flags = 0; +}; + +struct ComputeDescriptorSetLayout { + VkDescriptorSetLayoutCreateFlags flags = 0; + std::vector bindings; +}; + +struct ComputeSpecialization { + std::uint32_t id = 0; + std::uint32_t value = 0; +}; + +struct ComputePipelineCapture { + static constexpr std::uint32_t Version = 1; + static constexpr std::uint32_t MaxSpirvWords = 8u * 1024u * 1024u; + static constexpr std::size_t MaxFileBytes = 36u * 1024u * 1024u; + std::vector spirv; + std::vector sets; + std::vector pushConstants; + std::vector specialization; + std::uint32_t requiredSubgroupSize = 0; + VkPipelineCreateFlags flags = 0; + VkPipelineShaderStageCreateFlags stageFlags = 0; + std::string deviceName; + std::uint32_t vendorId = 0; + std::uint32_t deviceId = 0; + std::uint32_t driverVersion = 0; + std::uint32_t apiVersion = 0; + std::array pipelineCacheUuid{}; + + void Validate() const; + std::vector Encode() const; + 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; +}; + +struct ComputePipelineReplayResult { + double moduleMilliseconds = 0; + double layoutMilliseconds = 0; + double pipelineMilliseconds = 0; + double cacheMilliseconds = 0; + double totalMilliseconds = 0; + std::string deviceName; + std::uint32_t vendorId = 0; + std::uint32_t deviceId = 0; + std::uint32_t driverVersion = 0; + std::uint32_t apiVersion = 0; + std::array pipelineCacheUuid{}; + std::size_t initialCacheBytes = 0; + std::vector cache; +}; + +} + +#endif 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 24a7be45ba..685976d255 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp @@ -22,11 +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 fc18cff573..ff879da824 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp @@ -3,16 +3,26 @@ #define VK_NO_PROTOTYPES #include +#include #include +#include +#include namespace AgcDriver { VkFormat DisplayTexelFormat(std::uint64_t pixelFormat); bool DisplayTenBit(std::uint64_t pixelFormat); bool DisplayRedLow(std::uint64_t pixelFormat); +bool DisplayHalfFloat(std::uint64_t pixelFormat); +bool DisplayPq(std::uint64_t pixelFormat); +std::uint32_t DisplayTexelBytes(std::uint64_t pixelFormat); enum class ResidentPresent : std::uint8_t { None, Blit, Convert }; ResidentPresent ResidentPresentPath(VkFormat storage, std::uint64_t pixelFormat, bool blitSource); +std::optional 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 16ef18fbef..52582c1143 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" @@ -12,10 +13,13 @@ void Submit(const Packet* packet, std::uint32_t queue); } extern "C" void AgcDriverWaitIdle_nid_postfix(); +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); 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 3c0093fbe8..39bb6c35c9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp @@ -3,11 +3,25 @@ #include "prx/libSceAgcDriver/Execution/include/QueueState.hpp" #include +#include +#include namespace AgcDriver::DriverDetail { 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/Dispatch/IndirectDispatch.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/IndirectDispatch.hpp index af38e7fab7..5d8257c476 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/IndirectDispatch.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/IndirectDispatch.hpp @@ -5,7 +5,7 @@ namespace AgcDriver::DriverDetail { -enum IndirectPath { IndirectGpu = 0, IndirectPendingImage = 1, IndirectCopiedWrite = 2, IndirectNotImported = 3, IndirectThreadDimensions = 4, IndirectFillKernel = 5, IndirectMisaligned = 6, IndirectDisabled = 7, IndirectPaths = 8 }; +enum IndirectPath { IndirectGpu = 0, IndirectPendingImage = 1, IndirectCopiedWrite = 2, IndirectNotImported = 3, IndirectThreadDimensions = 4, IndirectFillKernel = 5, IndirectMisaligned = 6, IndirectDisabled = 7, IndirectWorkgroupMemory = 8, IndirectPaths = 9 }; } 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 16d7a286ea..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 { @@ -23,6 +26,13 @@ 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 { @@ -32,14 +42,22 @@ 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; + ShaderRecompiler::ShaderStage stage = ShaderRecompiler::ShaderStage::Vertex; + std::uint32_t pushOffset = 0; + 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; @@ -54,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 }; @@ -69,18 +95,71 @@ 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(); }; +class DrawKeySightings { +public: + static constexpr std::size_t Ways = 4; + explicit DrawKeySightings(std::size_t keys); + bool SeenBefore(std::uint64_t key); + +private: + std::vector keys; + std::vector filled; + std::vector next; + std::size_t bucketMask = 0; +}; + enum class DrawVerdict { Drawn, Nothing, Rejected }; struct StageCapture { 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); + } #endif 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 new file mode 100644 index 0000000000..cddc9abc90 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp @@ -0,0 +1,20 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_DRIVER_DRAW_DRAWREGISTERKEY_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_DRIVER_DRAW_DRAWREGISTERKEY_HPP + +#include "prx/libSceAgcDriver/Execution/include/QueueState.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" + +namespace AgcDriver::DriverDetail { + +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); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index 2f12dae2e5..3fb9f473b3 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 @@ -44,13 +46,17 @@ namespace AgcDriver::DriverDetail { class Driver { public: static Driver& Get(); + static std::uint64_t GpuMapImportThreads(); ~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); @@ -60,6 +66,21 @@ 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 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; @@ -74,11 +95,21 @@ private: static bool copySegment(Submission& submission, const std::uint32_t* guest, std::size_t words, std::size_t& budget); static void readRegisterLists(Submission& submission); void waitForFlipRoom(const Submission& submission); + void holdFlipBehindWorker(const Submission& submission); + bool awaitsTitle(std::uint64_t awaited) const; + void noteAwaitingTitle(std::uint32_t queue, std::uint64_t awaited); + void releaseFlipHold(); void reserveOutputs(Submission& submission); void executeRewindTail(const Submission& stalled); 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; @@ -87,6 +118,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(); @@ -100,46 +132,63 @@ 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); + 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); - ShaderRecompiler::RecompileResult materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector>& vertexInfos, std::vector& memory, const std::vector& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr& localDevice, ShaderMemory& shaderMemory, std::vector& stageCaptures, std::vector& recompiled, bool drawHit, const std::vector>& matched, const std::vector>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array& phaseMs, std::string& rejected); - 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); - std::optional classifyIndirectDraw(const ShaderRecompiler::RecompileResult& result, const Graphics::State& graphics, const DrawProgram& frontProgram, const std::shared_ptr& localDevice, Pm4::DrawParameters& drawParameters, bool traceIndirect); DrawVerdict draw(QueueState& queue, std::span packet, const Submission& submission, std::string& rejected); void addDriverPhases(DispatchClass which, const std::array& ms, bool hit, bool validated); static PendingDispatchPhases& pendingDispatchPhases(); static std::chrono::steady_clock::time_point& packetStartedAt(); static PendingDrawPhases& pendingDrawPhases(); void addDrawPhases(const std::array& ms, bool drawn, std::uint64_t captures); - void noteLabelStore(std::uint64_t address, std::span bytes, std::uint64_t stamp); - bool storedSince(std::span packet, std::uint64_t address, std::size_t bytes, std::uint64_t received); + void noteLabelStore(std::uint64_t address, std::span bytes, std::uint64_t stamp, std::uint32_t queue); + bool storedSince(std::span packet, std::uint64_t address, std::size_t bytes, std::uint64_t received, std::uint32_t* writer = nullptr); static void traceLabel(std::span packet, std::uint32_t queue); static std::array& writeHistory(); static std::size_t& writeCursor(); static void dumpPackets(std::span commands, const std::uint32_t* guest = nullptr); static void validate(const Submission& submission, const std::uint32_t* guest = nullptr); static void reportSkip(const char* kind, const std::string& what); - static std::string dumpRequest(std::uint64_t address, const ShaderRecompiler::RecompileRequest& request); static bool matchesFillKernel(std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute); 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); @@ -151,7 +200,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(); @@ -161,6 +221,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(); @@ -177,8 +238,16 @@ private: template static void forEachWrittenBuffer(const ShaderRecompiler::RecompileResult& compiled, TVisit&& visit); void noteWrittenBuffers(std::uint64_t program, std::uint32_t queue, const ShaderRecompiler::RecompileResult& compiled); + 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); @@ -187,20 +256,58 @@ 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(); static bool pollTryEach(); static bool traceLateLabels(); - void waitMemory(std::span packet, std::uint32_t queue, const PacketHistory& context, std::uint64_t received, bool heldAtSubmit); + void waitMemory(std::span packet, std::uint32_t queue, const PacketHistory& context, std::uint64_t received, bool heldAtSubmit, bool requireMemory = false); + struct PendingWait { + std::array words; + std::size_t count; + std::uint64_t received; + }; + static std::vector& pendingWaits(); + static void notePendingWait(std::span packet, std::uint32_t queue, std::uint64_t received); + void landPendingWaits(std::uint32_t queue); + struct ResolvedDispatch { + std::uint64_t address = 0, key = 0, indirectArguments = 0; + std::array packet{}; + std::shared_ptr registeredShader; + std::shared_ptr compiledResult; + std::shared_ptr keepVariant, attachVariant; + std::shared_ptr shaderMemory; + std::vector captured; + std::vector liveWords; + std::shared_ptr capture; + bool dataHit = false, cached = false, validated = false; + 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, argumentMismatches = 0; + double resolveMs = 0, resolveMaxMs = 0, ageMs = 0, ageMaxMs = 0; + std::chrono::steady_clock::time_point reported = std::chrono::steady_clock::now(); + }; + static std::map& resolvedAhead(); + static GroupCaptureStats& groupCaptureStats(); + static std::size_t& currentPacketOffset(); + static bool& resolvingAhead(); + static bool& gateOpened(); + void resolveGroupAhead(const Submission& submission, const QueueState& live, std::size_t from); + static void clearResolvedAhead(); + static void reportGroupCapture(std::uint32_t queue); static PollStats& pollStats(); static WaitOutcomes& waitOutcomes(); 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(); @@ -223,6 +330,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(); @@ -233,13 +342,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}; @@ -260,13 +372,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; @@ -279,6 +399,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; @@ -299,12 +425,18 @@ 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}; std::atomic queue0Awaited{0}; + std::atomic flipsAhead{0}; + std::atomic flipHolders{0}; + std::atomic queue0AwaitsTitle{false}; 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 new file mode 100644 index 0000000000..404207df81 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DwordPatternFill.hpp @@ -0,0 +1,32 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_MEMORY_DWORDPATTERNFILL_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_MEMORY_DWORDPATTERNFILL_HPP + +#include "Recompiler.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::DriverDetail { + +struct DwordPatternFill { + std::uint64_t base = 0; + std::size_t bytes = 0; + std::array pattern{}; +}; + +std::optional MatchDwordPatternFill(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); + +} + +#endif 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 1b15bcfe9f..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 { @@ -29,6 +30,7 @@ struct Submission { std::map> registerLists; bool suspend = false; bool waitFree = false; + bool holdsFlip = false; std::uint64_t received = 0; std::vector labelWrites; 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 8467298689..203940d7c3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp @@ -5,19 +5,123 @@ #include "prx/libSceAgcDriver/Execution/include/QueueState.hpp" #include #include +#include #include +#include #include #include +#include +#include +#include +#include #include #include #include #include +#include #include +struct Shader; + +namespace ShaderRecompiler { +class ShaderPreparationContext; +} + +namespace AgcDriver { +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 { @@ -46,9 +150,15 @@ struct PreparedShaderState { std::vector rectangleProgress; std::vector> fragments; bool rectangleRequested = false; + bool deferred = false; }; struct PreparedShaders : PreparedShaderState { std::mutex mutex; + std::condition_variable settled; + std::exception_ptr failure; + std::atomic demanded{false}; + bool pending = false; + std::vector convertedSamples; }; struct RegisteredShaderState { @@ -65,21 +175,50 @@ struct ShaderSnapshot { std::vector header; std::shared_ptr prepared = std::make_shared(); std::shared_ptr registeredState; + std::shared_ptr> workgroupUse = std::make_shared>(0u); + std::shared_ptr> positionVectorUse = std::make_shared>(0u); + std::shared_ptr> rawGraphicsNoted = std::make_shared>(false); + std::optional userDataTailRegister; }; -std::shared_ptr ReadRawComputeShader(std::uint64_t address); +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(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. +std::uint32_t PositionExportLayout(const ShaderSnapshot* snapshot, std::size_t codeOffset, std::span code, std::uint32_t paClVsOutCntl, const ShaderSnapshot* continuation = nullptr); +bool VertexWorkgroupRequired(const QueueState& queue, const ShaderRegistry& registry); + +std::shared_ptr PrepareShaderWithDiagnostics(const ShaderRecompiler::RecompileRequest& request, ShaderRecompiler::ShaderPreparationContext* preparation = nullptr); 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 0644622d3a..d807612d5d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp @@ -12,7 +12,7 @@ struct SubmissionCosts { std::atomic submissions{0}, validateNs{0}, copyNs{0}, dequeueNs{0}, completeNs{0}, suspendNs{0}, suspends{0}, suspendsSkipped{0}, endSubmits{0}, endSkipped{0}, notifiesSkipped{0}; }; -extern std::atomic gpuLabels, completionLabels, notImportedLabels, noOpLabels, unlockedDrains; +extern std::atomic gpuLabels, completionLabels, noOpLabels, unlockedDrains; extern std::atomic labelFallbacks[5]; @@ -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; +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 5ff14c7f7b..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}; @@ -18,6 +17,7 @@ inline constexpr std::chrono::microseconds PollTryInterval{1000}; struct LabelStore { std::uint64_t stamp = 0; std::uint32_t value = 0; + std::uint32_t queue = 0xffffffffu; }; struct WriteRecord { 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/FragmentBarycentric.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/FragmentBarycentric.hpp new file mode 100644 index 0000000000..361aadb063 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/FragmentBarycentric.hpp @@ -0,0 +1,14 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_FRAGMENTBARYCENTRIC_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_FRAGMENTBARYCENTRIC_HPP + +#include + +namespace AgcDriver { + +inline bool FragmentShaderBarycentricUsable(VkDriverId driver, bool reported) { + return reported && driver != VK_DRIVER_ID_MOLTENVK; +} + +} + +#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 ba201ca151..efa0e9d031 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp @@ -26,10 +26,12 @@ 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. void ReadCommitted(std::uint64_t address, std::span destination); +void ReadCommittedUnsynced(std::uint64_t address, std::span destination); bool EqualsCommitted(std::uint64_t address, std::span bytes); // EqualsCommitted without the flush hook: the caller has decided that the recorded GPU work // writing the range need not be waited for (the dispatch-cache validation in Driver.cpp). @@ -59,7 +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); -bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function& import); +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); @@ -71,12 +79,15 @@ struct UnchangedQuery { }; bool WrittenSince(std::uint64_t address, std::size_t bytes, std::uint64_t generation); bool UnchangedSinceAll(std::span queries); +std::uint64_t CollectCommittedWrites(std::uint64_t address, std::size_t bytes, std::vector>& ranges); +bool UnchangedCommittedSince(std::span> ranges, std::uint64_t generation); // Returns the generation the blocks were stamped with (0 when the arena is not write-watched): // UnchangedSince(range, it) holds until the next store over the range. std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes); std::uint64_t StoreOwnBytes(std::uint64_t address, std::size_t bytes, const std::function& 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 @@ -93,14 +104,19 @@ 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 // tracker mutex, so every collect that bumped before it has finished its walk and every later // collect stamps newer. std::uint64_t TrackerGeneration(); +std::uint64_t NewestStamp(std::uint64_t address, std::size_t bytes); +std::uint64_t UntrackedHint(std::uint64_t address, std::size_t bytes); // Whether no CPU write touched the range since `generation`: an uncached resetting collect of the // range's pages first (a game store not collected yet becomes a block stamp), then the compare over // the stamps collects make (dirty pages), not the MarkWritten stamps of the driver's own GPU label @@ -114,7 +130,6 @@ bool UnchangedSinceCollected(std::uint64_t address, std::size_t bytes, std::uint // watched arena, or for a generation of 0, a block reads as changed. constexpr std::uint8_t BlockUnchanged = 0; constexpr std::uint8_t BlockWritten = 1; -constexpr std::uint8_t BlockMaybeWritten = 2; bool ChangedBlocks(std::uint64_t address, std::size_t bytes, std::span generations, std::span changed, std::span cpu = {}); // Serializes device work: draws, dispatches, presentation and the deferred write-backs below. The @@ -149,6 +164,7 @@ GpuMutexType& GpuMutex(); // work the hold deferred to run without it (the recorder's release of the objects completed batches // kept). One hook, set once; it must not take GpuMutex itself. void SetGpuUnlockHook(void (*hook)()); +std::uint64_t GpuHoldSerial(); // Debug aid (APS5_ASSERT_GPU_LOCK=1): aborts with a [lock] line naming `where` when the calling // thread does not hold GpuMutex. Recording, submits, command batches and the detiler's pools must // only be reached under it (a resource build's stage A runs without it, see ShaderResources). @@ -183,6 +199,8 @@ unsigned long long CodeOffset(const void* address); // every read and store through this interface calls first. Code that reads guest memory directly // 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 @@ -236,5 +254,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/PerformanceTimer.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp index 1f6bb75a0b..28ccc5c2df 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp @@ -1,6 +1,8 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PERFORMANCETIMER_HPP #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PERFORMANCETIMER_HPP +#include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp" +#include "prx/libSceAgcDriver/Execution/include/TracyZone.hpp" #include #include #include @@ -258,11 +260,11 @@ private: class PerformanceTimer { public: - explicit PerformanceTimer(const char* scope) : frame(PerformanceContext::Current()), scope(scope) { + explicit PerformanceTimer(const char* scope) : frame(PerformanceContext::Current()), trace(PerformanceTrace::Recording() ? PerformanceTrace::Thread() : nullptr), scope(scope) { if (frame != nullptr) { total = frame->Get(scope, "total"); } - if (frame != nullptr) start = Clock::now(); + if (frame != nullptr || trace != nullptr) start = Clock::now(); previous = start; } @@ -274,8 +276,12 @@ public: } void Finish() { - if (frame == nullptr) return; + zone.End(); + if (frame == nullptr && trace == nullptr) return; const auto end = Clock::now(); + if (trace != nullptr) trace->Add(scope, nullptr, start, end); + trace = nullptr; + if (frame == nullptr) return; if (tail != nullptr) frame->Add(tail, end - previous); frame->Add(total, end - start); frame = nullptr; @@ -287,10 +293,13 @@ public: } void Mark(const char* stage, std::uint64_t bytes = 0) { - if (frame == nullptr) return; + if (frame == nullptr && trace == nullptr) return; const auto now = Clock::now(); - if (tail == nullptr) tail = frame->Get(scope, "tail"); - frame->Add(frame->Get(scope, stage), now - previous, bytes); + if (trace != nullptr) trace->Add(scope, stage, previous, now); + if (frame != nullptr) { + if (tail == nullptr) tail = frame->Get(scope, "tail"); + frame->Add(frame->Get(scope, stage), now - previous, bytes); + } previous = now; } @@ -298,7 +307,9 @@ private: using Clock = FrameTiming::Clock; FrameTiming* frame; + TraceThread* trace; const char* scope; + [[no_unique_address]] TracyZone zone{scope}; FrameTiming::Metric* total = nullptr; FrameTiming::Metric* tail = nullptr; Clock::time_point start{}; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp new file mode 100644 index 0000000000..e683bc7695 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp @@ -0,0 +1,780 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PERFORMANCETRACE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PERFORMANCETRACE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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; + +class TraceWindow { +public: + using Clock = std::chrono::steady_clock; + enum class Edge { None, Open, Close }; + + static std::optional Parse(const char* frames, const char* seconds) { + if (frames == nullptr && seconds == nullptr) return std::nullopt; + if (frames != nullptr && seconds != nullptr) throw std::invalid_argument("APS5_TRACE_FRAMES and APS5_TRACE_SECONDS cannot be set together"); + const std::string_view name = frames != nullptr ? "APS5_TRACE_FRAMES" : "APS5_TRACE_SECONDS"; + const std::string_view text = frames != nullptr ? frames : seconds; + const auto colon = text.find(':'); + std::uint64_t start = 0; + std::uint64_t length = 0; + if (colon == std::string_view::npos || !number(text.substr(0, colon), start) || !number(text.substr(colon + 1), length) || length == 0) + throw std::invalid_argument(std::string(name) + "=" + std::string(text) + " is not : with a length above 0"); + return TraceWindow(frames != nullptr, start, length); + } + + Edge Present(std::uint64_t present, Clock::duration elapsed) { + if (state == State::Closed) return Edge::None; + if (state == State::Waiting) { + if (frames ? present < start : wholeSeconds(elapsed) < start) return Edge::None; + state = State::Open; + openedPresent = present; + openedAt = elapsed; + return Edge::Open; + } + if (frames ? present - openedPresent < length : wholeSeconds(elapsed - openedAt) < length) return Edge::None; + state = State::Closed; + return Edge::Close; + } + + std::string Describe() const { + return std::string(frames ? "frames " : "seconds ") + std::to_string(start) + ":" + std::to_string(length); + } + +private: + enum class State { Waiting, Open, Closed }; + + TraceWindow(bool frames, std::uint64_t start, std::uint64_t length) : frames(frames), start(start), length(length) {} + + static bool number(std::string_view text, std::uint64_t& value) { + const auto end = text.data() + text.size(); + const auto [last, error] = std::from_chars(text.data(), end, value); + return !text.empty() && error == std::errc{} && last == end; + } + + static std::uint64_t wholeSeconds(Clock::duration elapsed) { + return static_cast(std::chrono::duration_cast(elapsed).count()); + } + + bool frames; + std::uint64_t start; + std::uint64_t length; + State state = State::Waiting; + std::uint64_t openedPresent = 0; + Clock::duration openedAt{}; +}; + +class TraceInterval { +public: + using Clock = std::chrono::steady_clock; + + static std::optional Parse(const char* seconds) { + if (seconds == nullptr) return std::nullopt; + const std::string_view text = seconds; + std::uint64_t length = 0; + const auto end = text.data() + text.size(); + const auto [last, error] = std::from_chars(text.data(), end, length); + if (text.empty() || error != std::errc{} || last != end || length == 0 || length > 31536000) + throw std::invalid_argument("APS5_TRACE_SUMMARY_EVERY=" + std::string(text) + " is not a whole number of seconds above 0"); + return TraceInterval(length); + } + + bool Due(Clock::duration elapsed) const { + return elapsed - begun >= std::chrono::seconds(length); + } + + void Restart(Clock::duration elapsed) { + begun = elapsed; + } + + Clock::duration Begun() const { + return begun; + } + + std::string Describe() const { + return "summary_every " + std::to_string(length); + } + +private: + explicit TraceInterval(std::uint64_t length) : length(length) {} + + std::uint64_t length; + Clock::duration begun{}; +}; + +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 { + const char* scope; + const char* stage; + Clock::duration length; + std::uint64_t count; + }; + + struct Snapshot { + std::string name; + std::span slices; + std::vector totals; + bool dropped = false; + std::uint64_t eventsLost = 0; + std::optional eventsFullAt; + bool totalsFull = false; + }; + + inline static const std::atomic AlwaysBuffering{true}; + + TraceThread(std::string name, const std::atomic& presents, std::size_t capacity = DefaultCapacity, const std::atomic& buffering = AlwaysBuffering) + : name(std::move(name)), presents(presents), buffering(buffering), slices(std::make_unique(capacity)), capacity(capacity) {} + + TraceThread(const TraceThread&) = delete; + TraceThread& operator=(const TraceThread&) = delete; + + void Add(const char* scope, const char* stage, Clock::time_point begin, Clock::time_point end) { + const auto length = end - begin; + account(scope, stage, length); + if (!buffering.load(std::memory_order_relaxed)) return; + const auto present = presents.load(std::memory_order_relaxed); + if (!sampled || present != sampledPresent) { + if (sampled) append({nullptr, nullptr, sampleBegin, folded}); + sampled = true; + sampledPresent = present; + sampleBegin = begin; + folded = {}; + } + if (length < MinimumSlice) folded += length; + else append({scope, stage, begin, length, TraceBatchTag}); + } + + Snapshot Collect() const { + Snapshot snapshot{name, {slices.get(), used.load(std::memory_order_acquire)}, {}, false}; + for (const auto& entry : entries) { + const auto* scope = entry.scope.load(std::memory_order_acquire); + if (scope == nullptr) continue; + snapshot.totals.push_back({scope, entry.stage.load(std::memory_order_relaxed), Clock::duration(entry.length.load(std::memory_order_relaxed)), entry.count.load(std::memory_order_relaxed)}); + } + snapshot.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; + } + +private: + struct Entry { + std::atomic scope{nullptr}; + std::atomic stage{nullptr}; + std::atomic length{0}; + std::atomic count{0}; + }; + + void append(const Slice& slice) { + const auto index = used.load(std::memory_order_relaxed); + if (index == capacity) { + 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; + used.store(index + 1, std::memory_order_release); + } + + void account(const char* scope, const char* stage, Clock::duration length) { + const auto key = reinterpret_cast(scope) ^ (reinterpret_cast(stage) >> 3); + const auto hash = static_cast((static_cast(key) * 0x9e3779b97f4a7c15ull) >> 55); + for (std::size_t probe = 0; probe < entries.size(); ++probe) { + auto& entry = entries[(hash + probe) % entries.size()]; + const auto* current = entry.scope.load(std::memory_order_relaxed); + if (current == nullptr) { + entry.stage.store(stage, std::memory_order_relaxed); + entry.scope.store(scope, std::memory_order_release); + } else if (current != scope || entry.stage.load(std::memory_order_relaxed) != stage) { + continue; + } + entry.length.store(entry.length.load(std::memory_order_relaxed) + length.count(), std::memory_order_relaxed); + entry.count.store(entry.count.load(std::memory_order_relaxed) + 1, std::memory_order_relaxed); + return; + } + totalsFull.store(true, std::memory_order_relaxed); + } + + const std::string name; + const std::atomic& presents; + const std::atomic& buffering; + const std::unique_ptr slices; + const std::size_t capacity; + std::atomic used{0}; + std::atomic lost{0}; + std::atomic firstLost{0}; + std::atomic totalsFull{false}; + std::array entries; + bool sampled = false; + std::uint64_t sampledPresent = 0; + Clock::time_point sampleBegin{}; + 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; + std::vector> environment; + std::string gpu; + std::string driver; + std::string os; + std::uint32_t width = 0; + std::uint32_t height = 0; + std::string window; + std::uint64_t firstPresent = 0; + double gpuBusyMs = 0; + std::string trace; + 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 { + double gpuNs = 0; + std::chrono::steady_clock::time_point cpu{}; +}; + +class TraceFormat { +public: + using Clock = std::chrono::steady_clock; + static constexpr std::array WorkerBusyScopes{"Driver.Packet", "Driver.Suspend", "Driver.EndSubmission", "Driver.Rewind", "Driver.Completion"}; + static constexpr std::string_view WorkerWaitScope = "Driver.WorkerWait"; + static constexpr std::string_view WorkerPrefix = "queue worker "; + static constexpr std::size_t TopPhases = 8; + + static void AppendString(std::string& output, std::string_view text) { + output += '"'; + for (const char character : text) { + const auto byte = static_cast(character); + if (character == '"' || character == '\\') { + output += '\\'; + output += character; + } else if (byte < 0x20 || byte >= 0x7f) { + char escaped[8]; + std::snprintf(escaped, sizeof escaped, "\\u%04x", static_cast(byte)); + output += escaped; + } else { + output += character; + } + } + output += '"'; + } + + static std::string Name(const char* scope, const char* stage) { + return stage == nullptr ? std::string(scope) : std::string(scope) + "." + stage; + } + + static std::string Events(std::uint64_t process, Clock::time_point origin, std::span threads, std::size_t presenter, std::span presents, std::uint64_t firstPresent) { + std::string output = "{\"traceEvents\":[\n"; + bool first = true; + const auto event = [&](std::string_view name, const char* phase, Clock::time_point at, std::size_t tid) { + if (!first) output += ",\n"; + first = false; + output += "{\"name\":"; + AppendString(output, name); + char text[96]; + std::snprintf(text, sizeof text, ",\"ph\":\"%s\",\"ts\":%.3f,\"pid\":%llu,\"tid\":%llu", phase, microseconds(at - origin), static_cast(process), static_cast(tid)); + output += text; + }; + event("process_name", "M", origin, 0); + output += ",\"args\":{\"name\":\"AnyPS5\"}}"; + for (std::size_t index = 0; index < threads.size(); ++index) { + const auto& thread = threads[index]; + const auto tid = index + 1; + event("thread_name", "M", origin, tid); + output += ",\"args\":{\"name\":"; + AppendString(output, thread.name); + output += "}}"; + std::vector slices(thread.slices.begin(), thread.slices.end()); + std::stable_sort(slices.begin(), slices.end(), [](const auto& a, const auto& b) { return a.begin != b.begin ? a.begin < b.begin : a.length > b.length; }); + const auto folded = thread.name + " folded"; + for (const auto& slice : slices) { + char text[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); + 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); + output += ",\"s\":\"g\",\"args\":{\"present\":" + std::to_string(firstPresent + index) + "}}"; + } + output += "\n]}\n"; + return output; + } + + static std::vector GpuSlices(std::span ranges, const TraceClockSample& first, const TraceClockSample& last, Clock::time_point origin) { + const double gpuSpan = last.gpuNs - first.gpuNs; + const double scale = gpuSpan > 0 ? std::chrono::duration(last.cpu - first.cpu).count() / gpuSpan : 1.0; + const auto map = [&](double gpuNs) { return first.cpu + std::chrono::duration_cast(std::chrono::duration((gpuNs - first.gpuNs) * scale)); }; + std::vector slices; + for (const auto& range : ranges) { + const auto begin = map(range.beginNs); + const auto end = map(range.endNs); + if (end <= begin || begin < origin) continue; + slices.push_back({range.name, nullptr, begin, end - begin, range.batch}); + } + return slices; + } + + static std::vector Since(std::span now, std::span before) { + std::vector result; + for (const auto& total : now) { + auto delta = total; + for (const auto& old : before) { + if (old.scope != total.scope || old.stage != total.stage) continue; + delta.length -= old.length; + delta.count -= old.count; + break; + } + if (delta.count != 0) result.push_back(delta); + } + return result; + } + + static std::string Summary(const TraceSummary& summary, std::span threads, std::span presents) { + const auto count = presents.empty() ? std::size_t{0} : presents.size() - 1; + const auto span = presents.empty() ? Clock::duration{} : presents.back() - presents.front(); + std::vector frames; + for (std::size_t index = 1; index < presents.size(); ++index) frames.push_back(milliseconds(presents[index] - presents[index - 1])); + std::sort(frames.begin(), frames.end()); + const auto percentile = [&](double fraction) { return frames.empty() ? 0.0 : frames[std::min(frames.size() - 1, static_cast(fraction * static_cast(frames.size())))]; }; + const double perPresent = count != 0 ? 1.0 / static_cast(count) : 0.0; + + std::string output = "{\"commit\":"; + AppendString(output, summary.commit); + output += ",\"dirty\":"; + output += summary.dirty.has_value() ? (*summary.dirty ? "true" : "false") : "null"; + output += ",\"env\":{"; + for (std::size_t index = 0; index < summary.environment.size(); ++index) { + if (index != 0) output += ','; + AppendString(output, summary.environment[index].first); + output += ':'; + AppendString(output, summary.environment[index].second); + } + output += "},\"gpu\":"; + AppendString(output, summary.gpu); + output += ",\"driver\":"; + AppendString(output, summary.driver); + output += ",\"os\":"; + AppendString(output, summary.os); + AppendMachine(output, summary); + output += ",\"resolution\":"; + AppendString(output, std::to_string(summary.width) + "x" + std::to_string(summary.height)); + output += ",\"window\":{\"spec\":"; + AppendString(output, summary.window); + output += ",\"first_present\":" + std::to_string(summary.firstPresent); + output += ",\"ms\":" + number(milliseconds(span)) + "}"; + if (summary.interval) output += ",\"interval_start_s\":" + number(summary.interval->first) + ",\"interval_end_s\":" + number(summary.interval->second); + output += ",\"presents\":" + std::to_string(count); + output += ",\"frame_ms\":{\"p50\":" + number(percentile(0.5)) + ",\"p90\":" + number(percentile(0.9)) + ",\"p99\":" + number(percentile(0.99)) + ",\"max\":" + number(frames.empty() ? 0.0 : frames.back()) + "}"; + output += ",\"gpu_busy_ms_per_present\":" + number(summary.gpuBusyMs * perPresent); + + output += ",\"worker_busy_pct\":{"; + 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; + if (total.stage == nullptr && std::find(WorkerBusyScopes.begin(), WorkerBusyScopes.end(), scope) != WorkerBusyScopes.end()) busy += total.length; + if (scope != WorkerWaitScope) phases[Name(total.scope, total.stage)] += total.length; + } + if (!thread.name.starts_with(WorkerPrefix)) continue; + if (!firstWorker) output += ','; + firstWorker = false; + AppendString(output, thread.name); + output += ':' + number(span != Clock::duration{} ? 100.0 * static_cast(busy.count()) / static_cast(span.count()) : 0.0); + } + output += "},\"top_phases_ms_per_frame\":{"; + std::vector> top(phases.begin(), phases.end()); + std::stable_sort(top.begin(), top.end(), [](const auto& a, const auto& b) { return a.second > b.second; }); + for (std::size_t index = 0; index < top.size() && index < TopPhases; ++index) { + if (index != 0) output += ','; + AppendString(output, top[index].first); + output += ':' + number(milliseconds(top[index].second) * perPresent); + } + output += "},\"trace_dropped\":"; + output += dropped ? "true" : "false"; + 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(); + } + + static double milliseconds(Clock::duration elapsed) { + return std::chrono::duration(elapsed).count(); + } + + static std::string number(double value) { + char text[32]; + std::snprintf(text, sizeof text, "%.3f", value); + return text; + } +}; + +class PerformanceTrace { +public: + static bool Configured() { + static const bool configured = std::getenv("APS5_TRACE_FRAMES") != nullptr || std::getenv("APS5_TRACE_SECONDS") != nullptr || 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, std::uint64_t batch = 0) { + if (!buffering.load(std::memory_order_relaxed)) return; + std::lock_guard lock(gpuMutex); + if (gpuRanges.size() == gpuRanges.capacity()) { + ++gpuLost; + return; + } + gpuRanges.push_back({name, beginNs, endNs, batch}); + } + + static bool Recording() { + return AgcDriverTraceRecording_nid_postfix(); + } + + static TraceThread* Thread() { + 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); + + static std::atomic recording; + inline static std::atomic buffering{false}; + inline static std::atomic presentCount{0}; + static thread_local TraceThread* local; + inline static std::mutex gpuMutex; + inline static std::vector gpuRanges; + inline static std::uint64_t gpuLost = 0; +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PipelineLinkOptimization.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PipelineLinkOptimization.hpp new file mode 100644 index 0000000000..b66b563598 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PipelineLinkOptimization.hpp @@ -0,0 +1,14 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PIPELINELINKOPTIMIZATION_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PIPELINELINKOPTIMIZATION_HPP + +#include + +namespace AgcDriver { + +inline bool PipelineLinkTimeOptimizationUsable(VkDriverId driver) { + return driver != VK_DRIVER_ID_NVIDIA_PROPRIETARY; +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp index e6d2cbe13e..909b403e26 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp @@ -78,17 +78,19 @@ 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. -inline bool FillerPacket(std::uint32_t header) { return (header >> 30u) == 2u; } +inline bool FillerPacket(std::uint32_t header) { return (header >> 30u) == 2u || header == 0xffff1000u; } inline std::size_t PacketWords(std::uint32_t header) { return FillerPacket(header) ? 1u : static_cast((header >> 16u) & 0x3fffu) + 2u; } std::string_view UnsupportedReason(std::uint32_t header); 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. @@ -140,12 +142,63 @@ struct MemoryCopy { std::size_t bytes; }; 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); DrawParameters ResolveDraw(std::span packet, const QueueState& queue); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Pm4Opcodes.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Pm4Opcodes.hpp index e78777f28f..3786ec368b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Pm4Opcodes.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Pm4Opcodes.hpp @@ -65,6 +65,7 @@ inline constexpr Opcode Opcodes[] = { {0x88, "WAIT_ON_DE_COUNTER_DIFF"}, {0x8D, "DISPATCH_DRAW"}, {0x8E, "GET_LOD_STATS"}, + {0x91, "INDEX_ATTRIBUTES_INDIRECT"}, {0x93, "WAIT_REG_MEM_64"}, {0x9F, "SET_CONTEXT_REG_INDIRECT"}, }; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PqToSdr.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PqToSdr.hpp new file mode 100644 index 0000000000..eca1e61a9e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PqToSdr.hpp @@ -0,0 +1,78 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PQTOSDR_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PQTOSDR_HPP + +#include +#include +#include +#include +#include "prx/libSceAgcDriver/Execution/include/ToneMapLuminance.hpp" + +namespace AgcDriver { + +inline constexpr float PqReferenceWhiteNits = 203.0f; +inline constexpr float PqToneMapKnee = 0.75f; +inline constexpr std::uint32_t PqCodes = 1024; + +inline const std::array& PqEotfNits() { + static const auto table = [] { + std::array nits{}; + constexpr double m1 = 2610.0 / 16384.0; + constexpr double m2 = 2523.0 / 4096.0 * 128.0; + constexpr double c1 = 3424.0 / 4096.0; + constexpr double c2 = 2413.0 / 4096.0 * 32.0; + constexpr double c3 = 2392.0 / 4096.0 * 32.0; + for (std::uint32_t code = 0; code < PqCodes; ++code) { + const double e = std::pow(code / 1023.0, 1.0 / m2); + nits[code] = static_cast(10000.0 * std::pow(std::max(e - c1, 0.0) / (c2 - c3 * e), 1.0 / m1)); + } + return nits; + }(); + return table; +} + +inline float PqReferenceWhite(float whiteNits) { + return std::min(PqReferenceWhiteNits, whiteNits); +} + +inline float PqToneMap(float luminance, float relativeWhite) { + if (luminance <= PqToneMapKnee) return luminance; + const float t = (luminance - PqToneMapKnee) / (1.0f - PqToneMapKnee); + const float w = (relativeWhite - PqToneMapKnee) / (1.0f - PqToneMapKnee); + return PqToneMapKnee + (1.0f - PqToneMapKnee) * t * (1.0f + t / (w * w)) / (1.0f + t); +} + +inline std::array PqToSdrLinear(float red, float green, float blue, float whiteNits) { + const float referenceWhite = PqReferenceWhite(whiteNits); + const float r2020 = red / referenceWhite; + const float g2020 = green / referenceWhite; + const float b2020 = blue / referenceWhite; + std::array rgb{ + std::max(1.6604910f * r2020 - 0.5876411f * g2020 - 0.0728499f * b2020, 0.0f), + std::max(-0.1245505f * r2020 + 1.1328999f * g2020 - 0.0083494f * b2020, 0.0f), + std::max(-0.0181508f * r2020 - 0.1005789f * g2020 + 1.1187297f * b2020, 0.0f)}; + const float luminance = 0.2126f * rgb[0] + 0.7152f * rgb[1] + 0.0722f * rgb[2]; + if (luminance > 0.0f) { + const float scale = PqToneMap(luminance, whiteNits / referenceWhite) / luminance; + for (auto& channel : rgb) channel = std::min(channel * scale, 1.0f); + } + return rgb; +} + +inline std::uint8_t SrgbEncode8(float linear) { + const float v = std::clamp(linear, 0.0f, 1.0f); + const float encoded = v <= 0.0031308f ? 12.92f * v : 1.055f * std::pow(v, 1.0f / 2.4f) - 0.055f; + return static_cast(std::floor(encoded * 255.0f + 0.5f)); +} + +inline std::array PqToSdrPixel(std::uint32_t word, bool redLow, float whiteNits) { + const auto& nits = PqEotfNits(); + const float low = nits[word & 0x3ffu]; + const float green = nits[(word >> 10u) & 0x3ffu]; + const float high = nits[(word >> 20u) & 0x3ffu]; + const auto rgb = PqToSdrLinear(redLow ? low : high, green, redLow ? high : low, whiteNits); + return {SrgbEncode8(rgb[2]), SrgbEncode8(rgb[1]), SrgbEncode8(rgb[0]), 0xff}; +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PresentMode.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PresentMode.hpp new file mode 100644 index 0000000000..cb448bbc44 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PresentMode.hpp @@ -0,0 +1,27 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PRESENTMODE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PRESENTMODE_HPP + +#include +#include +#include +#include +#include + +namespace AgcDriver { + +inline VkPresentModeKHR SelectPresentMode(const char* requested, std::span supported) { + VkPresentModeKHR mode = VK_PRESENT_MODE_FIFO_KHR; + if (requested != nullptr) { + const std::string_view name(requested); + if (name == "mailbox") mode = VK_PRESENT_MODE_MAILBOX_KHR; + else if (name == "immediate") mode = VK_PRESENT_MODE_IMMEDIATE_KHR; + else if (name == "fifo_latest_ready") mode = VK_PRESENT_MODE_FIFO_LATEST_READY_KHR; + else if (name != "fifo") throw std::runtime_error("Vulkan presentation: ANYPS5_PRESENT_MODE must be fifo, fifo_latest_ready, mailbox or immediate"); + } + if (std::find(supported.begin(), supported.end(), mode) == supported.end()) throw std::runtime_error("Vulkan presentation: the surface does not support the requested present mode"); + return mode; +} + +} + +#endif 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 new file mode 100644 index 0000000000..337bfb0805 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PresentationPass.hpp @@ -0,0 +1,75 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PRESENTATIONPASS_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PRESENTATIONPASS_HPP + +#define VK_NO_PROTOTYPES +#include +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include +#include +#include +#include +#include + +namespace AgcDriver { + +namespace Graphics { +class Buffer; +} + +class PresentationPass { +public: + PresentationPass(const Graphics::Context& context, std::size_t slots); + ~PresentationPass(); + PresentationPass(const PresentationPass&) = delete; + PresentationPass& operator=(const PresentationPass&) = delete; + + void Bind(std::size_t slot, VkImage image, std::uint32_t width, std::uint32_t height, std::uint64_t pixelFormat, float whiteNits, std::uint32_t layer = 0); + void RecordPresent(VkCommandBuffer commands, std::size_t slot, VkImage destination, VkFormat format, 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); + static VkExtent2D DumpExtent(std::uint32_t width, std::uint32_t height, std::uint32_t stride); + +private: + struct Target { + VkRenderPass renderPass = VK_NULL_HANDLE; + VkPipeline pipeline = VK_NULL_HANDLE; + }; + struct Slot { + VkDescriptorSet set = VK_NULL_HANDLE; + VkImageView source = VK_NULL_HANDLE; + std::uint32_t width = 0; + std::uint32_t height = 0; + std::uint32_t mode = 0; + float whiteNits = 0.0f; + std::vector> transient; + VkImage dumpImage = VK_NULL_HANDLE; + VkDeviceMemory dumpMemory = VK_NULL_HANDLE; + VkImageView dumpView = VK_NULL_HANDLE; + VkFramebuffer dumpFramebuffer = VK_NULL_HANDLE; + VkExtent2D dumpExtent{}; + }; + + const Target& target(VkFormat format, VkImageLayout finalLayout); + VkImageView createView(VkImage image, VkFormat format, VkImageUsageFlags usage, std::uint32_t layer = 0) const; + VkFramebuffer createFramebuffer(VkRenderPass renderPass, VkImageView view, VkExtent2D extent) const; + void ensureDumpTarget(Slot& slot, VkExtent2D extent); + void releaseDumpTarget(Slot& slot) noexcept; + void draw(VkCommandBuffer commands, const Slot& slot, const Target& target, VkFramebuffer framebuffer, VkExtent2D extent, const std::int32_t (¶meters)[9]) const; + void release() noexcept; + + Graphics::Context context; + VkSampler sampler = VK_NULL_HANDLE; + VkDescriptorSetLayout descriptorLayout = VK_NULL_HANDLE; + VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; + VkDescriptorPool descriptorPool = VK_NULL_HANDLE; + VkShaderModule vertex = VK_NULL_HANDLE; + VkShaderModule fragment = VK_NULL_HANDLE; + std::unique_ptr pqNits; + std::map, Target> targets; + std::vector slots; +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp index 35e8ed497f..b0e88120aa 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp @@ -3,8 +3,10 @@ #define VK_NO_PROTOTYPES #include +#include "prx/libSceAgcDriver/Execution/include/AspectFit.hpp" #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" #include +#include namespace AgcDriver { @@ -22,14 +24,16 @@ public: // Fills the source image from another image of the same extent (any blittable format, layout // unchanged), leaving the source image ready to blit or read back. void RecordBlitInto(VkCommandBuffer commands, VkImage image, VkImageLayout layout, VkFilter filter); - void RecordBlitInto(VkCommandBuffer commands, VkImage image, VkImageLayout layout, VkFilter filter, std::uint32_t width, std::uint32_t height); + void RecordBlitInto(VkCommandBuffer commands, VkImage image, VkImageLayout layout, VkFilter filter, std::uint32_t width, std::uint32_t height, std::uint32_t layer = 0); // Copies the source image's texels (tightly packed rows of its format) into a host-readable buffer // and makes them visible to the host once the submission completes. void RecordReadback(VkCommandBuffer commands, VkBuffer destination); void RecordBlit(VkCommandBuffer commands, VkImage destinationImage, std::uint32_t destinationWidth, std::uint32_t destinationHeight); // The aspect-fitting blit of RecordBlit from any image: presenting a resident render target needs // no copy into the source image first. - static void RecordBlitFrom(const Graphics::Context& context, VkCommandBuffer commands, VkImage image, VkImageLayout layout, std::uint32_t width, std::uint32_t height, VkFilter filter, VkImage destinationImage, std::uint32_t destinationWidth, std::uint32_t destinationHeight); + static void RecordBlitFrom(const Graphics::Context& context, VkCommandBuffer commands, VkImage image, VkImageLayout layout, std::uint32_t width, std::uint32_t height, VkFilter filter, VkImage destinationImage, std::uint32_t destinationWidth, std::uint32_t destinationHeight, std::uint32_t layer = 0); + void RecordLetterbox(VkCommandBuffer commands, std::uint32_t width, std::uint32_t height, VkImage destinationImage, std::uint32_t destinationWidth, std::uint32_t destinationHeight); + static std::vector LetterboxBands(std::uint32_t width, std::uint32_t height, std::uint32_t destinationWidth, std::uint32_t destinationHeight); std::uint32_t SourceWidth() const { return sourceWidth; } std::uint32_t SourceHeight() const { return sourceHeight; } @@ -42,6 +46,8 @@ private: std::uint32_t sourceHeight = 0; VkImage sourceImage = VK_NULL_HANDLE; VkDeviceMemory sourceMemory = VK_NULL_HANDLE; + VkImage blackImage = VK_NULL_HANDLE; + VkDeviceMemory blackMemory = VK_NULL_HANDLE; }; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp index 6daec3ba86..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; @@ -57,6 +57,8 @@ public: return true; } + void Print(const char* text) { Print("%s", text); } + template void Print(const char* format, TArgs... args) { std::array local{}; @@ -67,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 ff24be8250..3954c462a3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp @@ -9,8 +9,10 @@ #include #include #include +#include #include #include +#include #include #include @@ -22,6 +24,29 @@ public: using mapped_type = std::uint32_t; using value_type = std::pair; + class Mask { + public: + explicit Mask(std::vector bits) : bits(std::move(bits)) {} + bool Contains(std::uint32_t offset) const { return offset / 64 < bits.size() && ((bits[offset / 64] >> (offset % 64)) & 1u) != 0; } + private: + std::vector bits; + }; + + class Reference { + public: + Reference(Registers& owner, std::uint32_t offset) : owner(owner), offset(offset) {} + operator std::uint32_t() const { return owner.values[offset]; } + Reference& operator=(std::uint32_t value) { owner.insert_or_assign(offset, value); return *this; } + Reference& operator=(const Reference& other) { return *this = static_cast(other); } + Reference& operator|=(std::uint32_t value) { return *this = static_cast(*this) | value; } + Reference& operator&=(std::uint32_t value) { return *this = static_cast(*this) & value; } + Reference& operator^=(std::uint32_t value) { return *this = static_cast(*this) ^ value; } + Reference& operator+=(std::uint32_t value) { return *this = static_cast(*this) + value; } + private: + Registers& owner; + std::uint32_t offset; + }; + class const_iterator { public: using iterator_category = std::forward_iterator_tag; @@ -56,6 +81,18 @@ public: using iterator = const_iterator; Registers() = default; + Registers(const Registers&) = default; + Registers& operator=(const Registers&) = default; + Registers(Registers&& other) noexcept : values(std::move(other.values)), present(std::move(other.present)), entries(std::exchange(other.entries, 0)), mask(std::move(other.mask)), fingerprint(std::exchange(other.fingerprint, 0)) {} + Registers& operator=(Registers&& other) noexcept { + if (this == &other) return *this; + values = std::move(other.values); + present = std::move(other.present); + entries = std::exchange(other.entries, 0); + mask = std::move(other.mask); + fingerprint = std::exchange(other.fingerprint, 0); + return *this; + } Registers(std::initializer_list> entries) { for (const auto& [offset, value] : entries) emplace(offset, value); } @@ -67,33 +104,48 @@ public: const_iterator upper_bound(std::uint32_t offset) const { return {this, nextPresent(static_cast(offset) + 1)}; } bool contains(std::uint32_t offset) const { return offset < values.size() && ((present[offset / 64] >> (offset % 64)) & 1u) != 0; } std::size_t count(std::uint32_t offset) const { return contains(offset) ? 1 : 0; } + template + void ForEach(TVisit&& visit) const { + for (std::size_t word = 0; word < present.size(); ++word) { + for (auto bits = present[word]; bits != 0; bits &= bits - 1) { + const auto offset = word * 64 + static_cast(std::countr_zero(bits)); + visit(static_cast(offset), values[offset]); + } + } + } std::size_t size() const { return entries; } bool empty() const { return entries == 0; } void clear() { values.clear(); present.clear(); entries = 0; + fingerprint = 0; } std::pair emplace(std::uint32_t offset, std::uint32_t value) { if (contains(offset)) return {const_iterator{this, offset}, false}; mark(offset) = value; + changeFingerprint(offset, value); return {const_iterator{this, offset}, true}; } std::pair insert_or_assign(std::uint32_t offset, std::uint32_t value) { if (contains(offset)) { + if (values[offset] == value) return {const_iterator{this, offset}, false}; + changeFingerprint(offset, values[offset]); values[offset] = value; + changeFingerprint(offset, value); return {const_iterator{this, offset}, false}; } mark(offset) = value; + changeFingerprint(offset, value); return {const_iterator{this, offset}, true}; } - std::uint32_t& operator[](std::uint32_t offset) { - if (contains(offset)) return values[offset]; - return mark(offset) = 0; + Reference operator[](std::uint32_t offset) { + if (!contains(offset)) emplace(offset, 0); + return {*this, offset}; } - std::uint32_t& at(std::uint32_t offset) { + Reference at(std::uint32_t offset) { if (!contains(offset)) throw std::out_of_range("register is not set"); - return values[offset]; + return {*this, offset}; } const std::uint32_t& at(std::uint32_t offset) const { if (!contains(offset)) throw std::out_of_range("register is not set"); @@ -101,6 +153,7 @@ public: } std::size_t erase(std::uint32_t offset) { if (!contains(offset)) return 0; + changeFingerprint(offset, values[offset]); present[offset / 64] &= ~(std::uint64_t{1} << (offset % 64)); --entries; return 1; @@ -113,8 +166,31 @@ public: } return a == end() && b == other.end(); } + std::uint64_t Fingerprint(const std::shared_ptr& selected) const { + if (mask != selected) { + fingerprint = RecomputeFingerprint(*selected); + mask = selected; + } + return fingerprint; + } + std::uint64_t RecomputeFingerprint(const Mask& selected) const { + std::uint64_t result = 0; + for (const auto [offset, value] : *this) { + if (selected.Contains(offset)) result ^= contribution(offset, value); + } + return result; + } private: + static std::uint64_t contribution(std::uint32_t offset, std::uint32_t value) { + auto hash = ((static_cast(offset) << 32u) | value) + 0x9e3779b97f4a7c15ull; + hash = (hash ^ (hash >> 30u)) * 0xbf58476d1ce4e5b9ull; + hash = (hash ^ (hash >> 27u)) * 0x94d049bb133111ebull; + return hash ^ (hash >> 31u); + } + void changeFingerprint(std::uint32_t offset, std::uint32_t value) { + if (mask != nullptr && mask->Contains(offset)) fingerprint ^= contribution(offset, value); + } static constexpr std::size_t End = ~std::size_t{0}; std::uint32_t& mark(std::uint32_t offset) { if (offset >= values.size()) { @@ -137,9 +213,11 @@ private: std::vector values; std::vector present; std::size_t entries = 0; + mutable std::shared_ptr mask; + mutable std::uint64_t fingerprint = 0; }; -inline Registers InitialContextRegisters() { +inline Registers BuildInitialContextRegisters() { Registers result{ {0x200, 0}, {0x201, 0}, {0x202, 0xcc0010}, {0x203, 0}, {0x204, 0}, {0x205, 0}, {0x206, 1087}, {0x207, 0}, @@ -148,19 +226,22 @@ inline Registers InitialContextRegisters() { {0xc, 0}, {0xd, 0x40004000}, {0x81, 0x80000000}, {0x82, 0x40004000}, {0x90, 0x80000000}, {0x91, 0x40004000}, {0x105, 0}, {0x106, 0}, {0x107, 0}, {0x108, 0}, + {0x10b, 0}, {0x10c, 0}, {0x10d, 0}, {0x1b1, 0}, {0x1b3, 0}, {0x1b4, 0}, {0x1b6, 0}, {0x1c3, 0}, {0x1c4, 0}, {0x1c5, 0}, - {0x1ff, 0}, {0x292, 2}, {0x293, 0}, {0x29b, 0}, + {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}, {0x318, 0}, {0x31b, 0}, {0x31c, 0}, {0x31d, 0}, {0x390, 0}, {0x3b0, 0}, {0x3b8, 0} }; + for (std::uint32_t i = 0; i < 32; ++i) result.emplace(0x191 + i, 0); for (std::uint32_t i = 0; i < 8; ++i) { result.emplace(0x1e0 + i, 0x20010001); result.emplace(0x31c + 0xfu * i, 0); } for (std::uint32_t i = 0; i < 16; ++i) { + result.emplace(0x2fe + i, 0); result.emplace(0x94 + 2 * i, 0x80000000); result.emplace(0x95 + 2 * i, 0x40004000); result.emplace(0xb4 + 2 * i, 0); @@ -170,6 +251,11 @@ inline Registers InitialContextRegisters() { return result; } +inline Registers InitialContextRegisters() { + static const Registers& initial = *new Registers(BuildInitialContextRegisters()); + return initial; +} + struct Predication { std::uint64_t address = 0; std::uint32_t operation = 0; @@ -177,11 +263,12 @@ struct Predication { }; struct QueueState { + static constexpr std::size_t ConstantRamWords = 0x3000; Registers shader; Registers context = InitialContextRegisters(); Registers userConfig{{0x24a, 0}, {0x24b, 0}}; std::optional savedContext; - std::array constantRam{}; + std::vector constantRam; std::uint64_t indexBase = 0; std::uint64_t drawIndirectBase = 0; std::uint64_t dispatchIndirectBase = 0; @@ -194,6 +281,17 @@ struct QueueState { void ClearContext() { context = InitialContextRegisters(); } + + std::span ConstantRam() const { + static const std::array unwritten{}; + if (constantRam.empty()) return unwritten; + return constantRam; + } + + std::span WritableConstantRam() { + if (constantRam.empty()) constantRam.resize(ConstantRamWords); + return constantRam; + } }; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Recipe.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Recipe.hpp index 51c74e1a0a..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,8 +82,9 @@ 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/RobustBufferAccess.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/RobustBufferAccess.hpp new file mode 100644 index 0000000000..a180b23104 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/RobustBufferAccess.hpp @@ -0,0 +1,14 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_ROBUSTBUFFERACCESS_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_ROBUSTBUFFERACCESS_HPP + +#include + +namespace AgcDriver { + +inline bool RobustBufferAccess2Usable(VkDriverId driver) { + return driver != VK_DRIVER_ID_AMD_PROPRIETARY; +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/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/ShaderPreparation.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp index 57ce88164a..98fd3cbadd 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp @@ -16,6 +16,7 @@ public: ShaderPreparationTransaction& operator=(const ShaderPreparationTransaction&) = delete; PreparedShaderState& Edit(const ShaderSnapshot& snapshot); const PreparedShaderState& Read(const ShaderSnapshot& snapshot) const; + void Hold(); void Commit(); private: 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/StackWalk.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/StackWalk.hpp new file mode 100644 index 0000000000..d7ae5782ca --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/StackWalk.hpp @@ -0,0 +1,155 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_STACKWALK_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_STACKWALK_HPP + +#include "prx/libc/include/exceptions/Unwind.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +class DwarfFrames { +public: + using Byte = LibcUnwind::Byte; + using Word = LibcUnwind::Word; + using Registers = std::array; + enum class Step { Stepped, NoRecord, Failed }; + + DwarfFrames(Word base, Word end, const Byte* frames, const Byte* framesEnd) : base(base), end(end) { + for (const Byte* p = frames; p != nullptr && framesEnd - p >= 8;) { + const Byte* record = p; + const auto length = LibcUnwind::Read(p); + if (length == 0) continue; + if (length == 0xffffffff || length < 4 || static_cast(framesEnd - p) < length) break; + const Byte* next = p + length; + if (LibcUnwind::Read(p) != 0) { + LibcUnwind::Frame frame{}; + const Byte* cursor = nullptr; + const char* augmentation = nullptr; + Byte lsdaEncoding = 0; + if (LibcUnwind::DecodeRange(frame, LibcUnwind::Lookup{0, record}, cursor, augmentation, lsdaEncoding)) index.emplace_back(frame.start, record); + } + p = next; + } + std::sort(index.begin(), index.end()); + } + + Word Base() const { + return base; + } + + bool Contains(Word pc) const { + return pc >= base && pc < end; + } + + bool Covers(Word pc) const { + LibcUnwind::Frame frame{}; + return find(pc, frame) == Step::Stepped; + } + + template + Step Unwind(Registers& registers, bool leaf, TRead&& read) const { + const Word pc = registers[16] - (leaf ? 0 : 1); + LibcUnwind::Frame frame{}; + if (const auto found = find(pc, frame); found != Step::Stepped) return found; + LibcUnwind::Rules initial; + if (!LibcUnwind::Instructions(frame.cieBegin, frame.cieEnd, frame, ~Word(0), initial, {})) return Step::Failed; + if constexpr (LibcUnwind::RuleCount > 32) { + if (frame.returnRegister == 32) { + initial.registers[16] = initial.registers[32]; + initial.registers[32] = {}; + frame.returnRegister = 16; + } + } + LibcUnwind::Rules rules = initial; + if (!LibcUnwind::Instructions(frame.begin, frame.end, frame, pc, rules, initial)) return Step::Failed; + if (rules.cfaExpression != nullptr || rules.cfaRegister >= 16 || frame.returnRegister >= 17) return Step::Failed; + const Word cfa = registers[rules.cfaRegister] + rules.cfaOffset; + if (cfa <= registers[7]) return Step::Failed; + Registers next = registers; + for (unsigned column = 0; column < next.size(); ++column) { + const auto& rule = rules.registers[column]; + if (rule.kind == 0) continue; + if (rule.kind == 1) { + if (!read(cfa + rule.value, next[column])) return Step::Failed; + } else if (rule.kind == 2 && rule.value >= 0 && rule.value < 17) { + next[column] = registers[rule.value]; + } else if (rule.kind == 4) { + next[column] = cfa + rule.value; + } else if (rule.kind == 5) { + next[column] = 0; + } else { + return Step::Failed; + } + } + next[7] = cfa; + next[16] = next[frame.returnRegister]; + if (next[16] == 0 || (next[7] == registers[7] && next[16] == registers[16])) return Step::Failed; + registers = next; + return Step::Stepped; + } + +private: + Step find(Word pc, LibcUnwind::Frame& frame) const { + const auto found = std::upper_bound(index.begin(), index.end(), pc, [](Word value, const auto& entry) { return value < entry.first; }); + if (found == index.begin()) return Step::NoRecord; + const Byte* p = nullptr; + const char* augmentation = nullptr; + Byte lsdaEncoding = 0; + if (!LibcUnwind::DecodeRange(frame, LibcUnwind::Lookup{pc, std::prev(found)->second}, p, augmentation, lsdaEncoding)) return Step::Failed; + if (pc < frame.start || pc - frame.start >= frame.length) return Step::NoRecord; + if (*augmentation == 'z') { + const auto size = LibcUnwind::Uleb(p); + p += size; + } + frame.begin = p; + return Step::Stepped; + } + + Word base; + Word end; + std::vector> index; +}; + +class CollapsedStacks { +public: + void Add(std::span leafFirst, bool truncated) { + std::vector stack(leafFirst.begin(), leafFirst.end()); + if (truncated) { + stack.push_back(0); + ++truncatedStacks; + } + ++stacks[std::move(stack)]; + } + + std::uint64_t Truncated() const { + return truncatedStacks; + } + + template + std::string Format(TName&& name) const { + std::string output; + for (const auto& [stack, count] : stacks) { + for (auto frame = stack.rbegin(); frame != stack.rend(); ++frame) { + if (frame != stack.rbegin()) output += ';'; + output += *frame == 0 ? std::string("[truncated]") : name(*frame); + } + output += ' ' + std::to_string(count) + '\n'; + } + return output; + } + +private: + std::map, std::uint64_t> stacks; + std::uint64_t truncatedStacks = 0; +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/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/SubgroupClock.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/SubgroupClock.hpp index 6c1c742095..db97b2d580 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/SubgroupClock.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/SubgroupClock.hpp @@ -2,11 +2,12 @@ #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SUBGROUPCLOCK_HPP #include +#include #include namespace AgcDriver { -bool NarrowSubgroupClock(VkDriverId driver, std::string_view deviceName); +bool NarrowSubgroupClock(VkDriverId driver, std::uint32_t deviceId, std::string_view deviceName); } 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/ThreadPriority.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/ThreadPriority.hpp new file mode 100644 index 0000000000..6a38c4f777 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/ThreadPriority.hpp @@ -0,0 +1,34 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_THREADPRIORITY_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_THREADPRIORITY_HPP + +#include +#include +#include + +namespace AgcDriver { + +enum class ThreadPriorityMode { Off, High, Realtime }; + +struct RttimeLimit { + std::uint64_t soft = 0; + std::uint64_t hard = 0; + bool operator==(const RttimeLimit&) const = default; +}; + +struct RtkitLimits { + std::int64_t maxPriority = 0; + std::int64_t maxRttimeUs = 0; +}; + +struct RealtimePlan { + std::string refusal; + std::optional limit; +}; + +ThreadPriorityMode ParseThreadPriorityMode(const char* text); +RealtimePlan PlanRealtime(std::uint32_t priority, const std::optional& rtkit, RttimeLimit current, std::uint64_t requestedUs); +void RaiseWorkerThreadPriority(const char* role); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/ToneMapLuminance.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/ToneMapLuminance.hpp new file mode 100644 index 0000000000..b00601533e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/ToneMapLuminance.hpp @@ -0,0 +1,10 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_TONEMAPLUMINANCE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_TONEMAPLUMINANCE_HPP + +namespace AgcDriver { + +inline constexpr float SdrToneMapLuminanceNits = 100.0f; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/TraceFile.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/TraceFile.hpp new file mode 100644 index 0000000000..ed7bb2a11d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/TraceFile.hpp @@ -0,0 +1,18 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_TRACEFILE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_TRACEFILE_HPP + +#include +#include + +namespace AgcDriver { + +class TraceFile { +public: + static void WriteGzip(const std::filesystem::path& path, std::string_view text); + static void ReplaceGzip(const std::filesystem::path& path, std::string_view text); + static void AppendLine(const std::filesystem::path& path, std::string_view line); +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/TracyZone.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/TracyZone.hpp new file mode 100644 index 0000000000..f763218099 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/TracyZone.hpp @@ -0,0 +1,81 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_TRACYZONE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_TRACYZONE_HPP + +#ifdef TRACY_ENABLE +#include +#include +#include +#include +#include +#include +#include +#endif + +namespace AgcDriver { + +class TracyZone { +public: + explicit TracyZone([[maybe_unused]] const char* name) { +#ifdef TRACY_ENABLE + context = ___tracy_emit_zone_begin(location(name), 1); +#endif + } + + TracyZone(const TracyZone&) = delete; + TracyZone& operator=(const TracyZone&) = delete; + + ~TracyZone() { + End(); + } + + void End() { +#ifdef TRACY_ENABLE + if (context.active == 0) return; + ___tracy_emit_zone_end(context); + context.active = 0; +#endif + } + + static void MarkFrame() { +#ifdef TRACY_ENABLE + ___tracy_emit_frame_mark(nullptr); +#endif + } + + static void NameThread([[maybe_unused]] const char* name) { +#ifdef TRACY_ENABLE + ___tracy_set_thread_name(name); +#endif + } + +private: +#ifdef TRACY_ENABLE + static const ___tracy_source_location_data* location(const char* name) { + static constexpr std::size_t capacity = 256; + static std::array, capacity> names{}; + static std::array<___tracy_source_location_data, capacity> locations{}; + static std::mutex mutex; + const auto start = static_cast(reinterpret_cast(name) >> 4); + for (std::size_t probe = 0; probe < capacity; ++probe) { + const auto index = (start + probe) % capacity; + const auto* known = names[index].load(std::memory_order_acquire); + if (known == name) return &locations[index]; + if (known != nullptr) continue; + std::lock_guard lock(mutex); + known = names[index].load(std::memory_order_relaxed); + if (known == name) return &locations[index]; + if (known != nullptr) continue; + locations[index] = {name, name, "", 0, 0}; + names[index].store(name, std::memory_order_release); + return &locations[index]; + } + throw std::runtime_error("Tracy: too many zone names"); + } + + TracyCZoneCtx context; +#endif +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/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 6622cc2433..924a2701dd 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -7,8 +7,12 @@ #include "prx/libSceAgcDriver/Execution/include/Presentation.hpp" #include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp" #include "prx/libSceAgcDriver/Execution/include/Recipe.hpp" +#include "prx/libSceAgcDriver/Execution/include/ComputePipelineCapture.hpp" #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 @@ -22,20 +26,43 @@ 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: explicit VulkanDevice(const PresentationWindow* window = nullptr); + 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 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; } @@ -46,12 +73,15 @@ 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 // that). A reap runs completion actions, and a write-back can wait for a later batch under the // mutex through the flush hook, so a thread submitting on another queue's behalf (a WAIT_REG_MEM // poller, a compute worker's between-packet flush) passes false: it must only submit, never wait. void SubmitRecorded(bool reapFirst = true); + void StoreIdleResults(bool reapFirst = true); // Unlocked drain: SubmitAndEpoch (under the mutex) returns the serial covering every recorded // batch (0: nothing in flight); WaitRecorded(serial) waits for it WITHOUT the mutex (the caller // holds a shared_ptr to this device); ReapRecorded(serial) (under the mutex) runs the completions @@ -66,11 +96,9 @@ public: // once that work completed; the batch is submitted by the queue worker's flush rules (or at once // with APS5_LABEL_SUBMIT_NOW=1). `stamp` is the record-order stamp and `queue` the recording // queue for the recorder's pending-label table. Returns 0 when recorded on the GPU, 5 when kept - // as a completion action behind pending write-backs and 6 when kept as one because the memory is - // not host-imported so the GPU cannot store it (both land when their batch is reaped), else why - // the CPU must write it: + // as a completion action behind pending write-backs, else why the CPU must write it: // 1 nothing recorded (the write is already ordered), 2 write-backs pending and 3 memory not - // imported (both only with APS5_DRAIN_COMPLETION_LABELS=1), 4 unsuitable size or alignment. + // imported, 4 unsuitable size or alignment. // `reapFirst` retires finished batches before the checks; a caller recording a group of labels // under one lock passes it for the first label only. int WriteLabelOnGpu(std::uint64_t address, std::span bytes, std::uint64_t stamp, std::uint32_t queue, bool reapFirst = true); @@ -78,6 +106,9 @@ 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, // recorded behind the open batch; false when the range is not imported (the caller stores it). // Recorded GPU stores over the range are ordered before the fill by its barrier; finished @@ -140,6 +171,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 @@ -155,14 +187,26 @@ 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; bool SamplerFilterMinmax() const; bool ConservativeRasterization() const; VkShaderStageFlags SubgroupStages() const; + bool ProvokingVertexLast() const; + bool GraphicsPipelineLibraries() const; // A presentation is a few steps so the presenter holds GuestMemory::GpuMutex only while it // touches the queue. Presentations are slots (FlipInFlight() + 1, each with its own command // buffer, fence, kept resident image and dump buffer): RetirePresents(keep) (no mutex) retires @@ -206,10 +250,14 @@ public: // blit-submit times after the flip packet; noted in-place reads checked // (APS5_FLIP_READ_CHECK=1) and found overwritten before the blit's fence. struct PresentStatistics { - std::uint64_t presents, gpuUnread, batchesAheadOfBlit, batchesAfterFlip, readsChecked, readsOverwritten; + std::uint64_t presents, gpuUnread, batchesAheadOfBlit, batchesAfterFlip, readsChecked, readsOverwritten, acquireDrops; double gpuBusyMs, gpuGapMs, lastSubmitAfterFlipMs, blitSubmitAfterFlipMs; }; static PresentStatistics PresentCounts(); + 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 @@ -217,7 +265,7 @@ 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::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); @@ -225,21 +273,11 @@ public: // 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); - // A dispatch whose group counts are the three dwords at `arguments` in guest memory - // (DISPATCH_INDIRECT): the GPU reads them in place from the host import, ordered after everything - // recorded before, so the CPU never waits for the shader that wrote them. When the GPU could not - // see the current bytes the counts are read on the CPU instead (through the flush hook, as the - // driver resolved every indirect dispatch before) and the dispatch is recorded as a direct one: - // cpuReason 1 storage-image results were pending over them, 2 a recorded dispatch writes them - // through a copied buffer (its CPU write-back lands only when the batch is reaped) or a label - // over those dwords is pending, 3 the memory is not host-imported; 0 when recorded GPU-side. argumentReadMs is - // what that CPU read (its sync) took. The device limit on group counts is not checked GPU-side. - // Debug aid: APS5_NO_GPU_INDIRECT=1 (in the driver) keeps every indirect dispatch on the CPU path. 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 @@ -261,7 +299,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(); @@ -281,6 +319,7 @@ 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); // A draw-cache hit recorded from its recipe (Graphics::DrawWithRecipe), under the mutex after @@ -296,24 +335,43 @@ public: }; IndirectDrawSupport DrawIndirectSupport() const; 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: +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 = {}); // 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); + 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 new file mode 100644 index 0000000000..4009901feb --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp @@ -0,0 +1,347 @@ +#include "prx/libSceAgcDriver/Execution/include/ComputePipelineCapture.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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) { + hash ^= std::to_integer(value); + hash *= 1099511628211ull; + } + std::ostringstream result; + result << std::hex << std::setfill('0') << std::setw(16) << hash; + return result.str(); +} + +class Encoder { +public: + void Word(std::uint32_t value) { + for (unsigned shift = 0; shift < 32; shift += 8) bytes.push_back(static_cast((value >> shift) & 0xffu)); + } + void String(const std::string& value) { + Word(static_cast(value.size())); + for (const auto character : value) bytes.push_back(static_cast(static_cast(character))); + } + std::vector bytes; +}; + +class Decoder { +public: + explicit Decoder(std::span bytes) : bytes(bytes) {} + std::uint32_t Word() { + RequireCapture(bytes.size() - cursor >= 4, "truncated integer"); + std::uint32_t value = 0; + for (unsigned shift = 0; shift < 32; shift += 8) value |= std::to_integer(bytes[cursor++]) << shift; + return value; + } + std::uint32_t Count(std::uint32_t maximum, std::size_t minimumBytes) { + const auto count = Word(); + RequireCapture(count <= maximum && count <= (bytes.size() - cursor) / minimumBytes, "count exceeds record bounds"); + return count; + } + std::string String() { + const auto count = Count(VK_MAX_PHYSICAL_DEVICE_NAME_SIZE - 1, 1); + std::string result; + for (std::uint32_t index = 0; index < count; ++index) result.push_back(static_cast(std::to_integer(bytes[cursor++]))); + return result; + } + bool Finished() const { return cursor == bytes.size(); } +private: + std::span bytes; + std::size_t cursor = 0; +}; + +std::vector ReadBytes(const std::filesystem::path& path) { + const auto size = CaptureFileSize(path); + RequireCapture(size <= ComputePipelineCapture::MaxFileBytes, "file exceeds 36 MiB"); + std::ifstream input(path, std::ios::binary); + RequireCaptureIo(input.good(), "cannot open capture"); + std::vector bytes(static_cast(size)); + input.read(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + RequireCaptureIo(input.good() && input.peek() == std::char_traits::eof(), "capture changed while reading"); + return bytes; +} + +} + +void ComputePipelineCapture::Validate() const { + RequireCapture(spirv.size() >= 5 && spirv.size() <= MaxSpirvWords, "SPIR-V word count out of bounds"); + RequireCapture(spirv[0] == 0x07230203u && spirv[1] >= 0x00010000u && spirv[1] <= 0x00010600u && (spirv[1] & 0xffu) == 0 && spirv[3] != 0 && spirv[3] <= 0x003fffffu && spirv[4] == 0, "invalid SPIR-V header"); + bool mainEntry = false; + for (std::size_t cursor = 5; cursor < spirv.size();) { + const auto count = spirv[cursor] >> 16u; + RequireCapture(count != 0 && count <= spirv.size() - cursor, "truncated SPIR-V instruction"); + if ((spirv[cursor] & 0xffffu) == 15u) { + RequireCapture(count >= 5 && spirv[cursor + 1] == 5u && spirv[cursor + 2] != 0 && spirv[cursor + 2] < spirv[3] && spirv[cursor + 3] == 0x6e69616du && spirv[cursor + 4] == 0, "only a compute main entry point is supported"); + RequireCapture(!mainEntry, "multiple entry points are unsupported"); + mainEntry = true; + } + cursor += count; + } + 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 || (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; + for (const auto& layout : sets) { + RequireCapture((layout.flags & ~VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT) == 0, "unsupported descriptor layout flags"); + RequireCapture(layout.bindings.size() <= 4096, "too many descriptor bindings"); + totalBindings += layout.bindings.size(); + std::set used; + const auto highest = layout.bindings.empty() ? 0u : std::ranges::max(layout.bindings, {}, &ComputeDescriptorBinding::binding).binding; + for (const auto& binding : layout.bindings) { + RequireCapture(used.insert(binding.binding).second, "duplicate descriptor binding"); + RequireCapture(binding.type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER || binding.type == VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE || binding.type == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE || binding.type == VK_DESCRIPTOR_TYPE_SAMPLER, "unsupported descriptor type"); + RequireCapture(binding.count != 0 && binding.count <= 1048576, "invalid descriptor count"); + 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 || 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"); + } + } + RequireCapture(totalBindings <= 4096, "too many total descriptor bindings"); + RequireCapture(pushConstants.size() <= 1, "too many compute push constant ranges"); + for (const auto& range : pushConstants) RequireCapture(range.stageFlags == VK_SHADER_STAGE_COMPUTE_BIT && range.size != 0 && range.size <= 4096 && range.offset <= 4096 - range.size && (range.offset % 4) == 0 && (range.size % 4) == 0, "invalid push constant range"); + RequireCapture(specialization.size() <= 4096, "too many specialization values"); + std::set specializationIds; + for (const auto& value : specialization) RequireCapture(specializationIds.insert(value.id).second, "duplicate specialization ID"); +} + +std::vector ComputePipelineCapture::Encode() const { + Validate(); + Encoder output; + output.Word(0x35535041u); + output.Word(0x504d4f43u); + output.Word(Version); + output.Word(flags); + output.Word(stageFlags); + output.Word(requiredSubgroupSize); + output.Word(vendorId); + output.Word(deviceId); + output.Word(driverVersion); + output.Word(apiVersion); + for (const auto value : pipelineCacheUuid) output.Word(value); + output.String(deviceName); + output.Word(static_cast(sets.size())); + for (const auto& layout : sets) { + output.Word(layout.flags); + output.Word(static_cast(layout.bindings.size())); + for (const auto& binding : layout.bindings) { + output.Word(binding.binding); + output.Word(static_cast(binding.type)); + output.Word(binding.count); + output.Word(binding.stages); + output.Word(binding.flags); + } + } + output.Word(static_cast(pushConstants.size())); + for (const auto& range : pushConstants) { + output.Word(range.stageFlags); + output.Word(range.offset); + output.Word(range.size); + } + output.Word(static_cast(specialization.size())); + for (const auto& value : specialization) { + output.Word(value.id); + output.Word(value.value); + } + output.Word(static_cast(spirv.size())); + for (const auto word : spirv) output.Word(word); + RequireCapture(output.bytes.size() <= MaxFileBytes, "encoded capture exceeds size bound"); + return std::move(output.bytes); +} + +ComputePipelineCapture ComputePipelineCapture::Decode(std::span bytes) { + RequireCapture(bytes.size() <= MaxFileBytes, "record exceeds 36 MiB"); + Decoder input(bytes); + RequireCapture(input.Word() == 0x35535041u && input.Word() == 0x504d4f43u, "invalid file signature"); + RequireCapture(input.Word() == Version, "unsupported file version"); + ComputePipelineCapture result; + result.flags = input.Word(); + result.stageFlags = input.Word(); + result.requiredSubgroupSize = input.Word(); + result.vendorId = input.Word(); + result.deviceId = input.Word(); + result.driverVersion = input.Word(); + result.apiVersion = input.Word(); + for (auto& value : result.pipelineCacheUuid) { + const auto word = input.Word(); + RequireCapture(word <= 255, "invalid cache UUID byte"); + value = static_cast(word); + } + result.deviceName = input.String(); + result.sets.resize(input.Count(8, 8)); + std::size_t totalBindings = 0; + for (auto& layout : result.sets) { + layout.flags = input.Word(); + const auto count = input.Count(4096, 20); + totalBindings += count; + RequireCapture(totalBindings <= 4096, "too many total descriptor bindings"); + layout.bindings.resize(count); + for (auto& binding : layout.bindings) { + binding.binding = input.Word(); + binding.type = static_cast(input.Word()); + binding.count = input.Word(); + binding.stages = input.Word(); + binding.flags = input.Word(); + } + } + result.pushConstants.resize(input.Count(1, 12)); + for (auto& range : result.pushConstants) { + range.stageFlags = input.Word(); + range.offset = input.Word(); + range.size = input.Word(); + } + result.specialization.resize(input.Count(4096, 8)); + for (auto& value : result.specialization) { + value.id = input.Word(); + value.value = input.Word(); + } + result.spirv.resize(input.Count(MaxSpirvWords, 4)); + for (auto& word : result.spirv) word = input.Word(); + RequireCapture(input.Finished(), "trailing bytes after capture"); + result.Validate(); + return result; +} + +ComputePipelineCapture ComputePipelineCapture::Read(const std::filesystem::path& path) { + return Decode(ReadBytes(path)); +} + +std::string ComputePipelineCapture::Identity() const { + return Hash(Encode()); +} + +std::filesystem::path ComputePipelineCapture::Write(const std::filesystem::path& directory) const { + const auto bytes = Encode(); + const auto recordDirectory = directory / ("compute-" + Hash(bytes)); + const auto path = recordDirectory / "capture.aps5cp"; + static std::mutex mutex; + std::lock_guard lock(mutex); + 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; + 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"; + 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"); + } + 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); + RequireCaptureIo(output.good(), "cannot create capture"); + output.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + output.close(); + 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 8bc53af7df..6f7dd42ade 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp @@ -2,7 +2,9 @@ #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/HostHdr.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" +#include "prx/libSceAgcDriver/Graphics/include/HalfFloatScanout.hpp" #include #include #include @@ -33,7 +35,38 @@ std::uint32_t tileOffset(std::uint32_t x, std::uint32_t y) { return ((y << 4u) & 0x0070u) ^ ((y << 5u) & 0x0f00u) ^ ((y << 9u) & 0x1000u) ^ ((y << 8u) & 0x4000u) ^ ((x << 2u) & 0x000cu) ^ ((x << 5u) & 0x0380u) ^ ((x << 4u) & 0x0400u) ^ ((x << 6u) & 0x0800u) ^ ((x << 9u) & 0xa000u); } -void decodeTexel(const std::byte* texel, std::byte* pixel, bool rgba, bool tenBit) { +std::uint32_t tileOffset64(std::uint32_t x, std::uint32_t y) { + return ((x << 3u) & 0x0008u) ^ ((x << 4u) & 0x0460u) ^ ((x << 5u) & 0x0300u) ^ ((x << 6u) & 0x0800u) ^ ((x << 9u) & 0x8000u) ^ ((x << 10u) & 0x2000u) ^ ((y << 4u) & 0x0010u) ^ ((y << 6u) & 0x0080u) ^ ((y << 5u) & 0x0f00u) ^ ((y << 10u) & 0x5000u); +} + +std::size_t texelOffset(const DisplayBuffer& buffer, std::uint32_t x, std::uint32_t y, std::uint32_t texelBytes) { + if (buffer.tilingMode == 1) { + const auto pitch = buffer.pitchInPixel == 0 ? buffer.width : buffer.pitchInPixel; + return (static_cast(y) * pitch + x) * texelBytes; + } + const auto blocksPerRow = (buffer.width + 127u) / 128u; + const auto blockHeight = 65536u / 128u / texelBytes; + const auto block = static_cast(y / blockHeight) * blocksPerRow + x / 128u; + return block * 65536u + (texelBytes == 8u ? tileOffset64(x, y) : tileOffset(x, y)); +} + +void decodeHalfFloatTexel(const std::byte* texel, std::byte* pixel, bool redLow) { + std::array halves{}; + std::memcpy(halves.data(), texel, sizeof(halves)); + pixel[0] = static_cast(Graphics::HalfFloatScanoutCode(halves[redLow ? 2 : 0], false)); + pixel[1] = static_cast(Graphics::HalfFloatScanoutCode(halves[1], false)); + pixel[2] = static_cast(Graphics::HalfFloatScanoutCode(halves[redLow ? 0 : 2], false)); + pixel[3] = static_cast(Graphics::HalfFloatScanoutCode(halves[3], true)); +} + +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, whiteNits); + for (std::size_t i = 0; i < converted.size(); ++i) pixel[i] = static_cast(converted[i]); + return; + } if (tenBit) { std::uint32_t word = 0; std::memcpy(&word, texel, 4); @@ -90,30 +123,52 @@ void traceDisplayKeys(const DisplayBuffer& buffer, const std::uint8_t* keys, std std::size_t DisplayBufferSize(const DisplayBuffer& buffer) { require(buffer.width != 0 && buffer.height != 0 && buffer.width <= 16384 && buffer.height <= 16384, "VideoOut: invalid display buffer dimensions"); - if (baseFormat(buffer.pixelFormat) != PixelFormatB8G8R8A8 && baseFormat(buffer.pixelFormat) != PixelFormatR8G8B8A8) { + const bool halfFloat = DisplayHalfFloat(buffer.pixelFormat); + if (!halfFloat && !DisplayPq(buffer.pixelFormat) && baseFormat(buffer.pixelFormat) != PixelFormatB8G8R8A8 && baseFormat(buffer.pixelFormat) != PixelFormatR8G8B8A8) { char message[96]; std::snprintf(message, sizeof(message), "VideoOut: unsupported display pixel format 0x%016llx", static_cast(buffer.pixelFormat)); throw std::runtime_error(message); } - require(buffer.address != 0 && (buffer.address & 65535u) == 0, "VideoOut: display buffer requires 64 KiB alignment"); + require(!halfFloat || buffer.dccAddress == 0, "VideoOut: DCC metadata on a 16-bit float display buffer is not implemented"); + 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"); require(buffer.dccAddress != 0 || buffer.dccClearColor == 0, "VideoOut: a DCC clear color needs DCC metadata"); const auto pitch = buffer.pitchInPixel == 0 ? buffer.width : buffer.pitchInPixel; require(pitch >= buffer.width && pitch <= 16384, "VideoOut: invalid linear display pitch"); - const auto size = buffer.tilingMode == 1 ? static_cast(pitch) * buffer.height * 4u - : static_cast((buffer.width + 127u) / 128u) * ((buffer.height + 127u) / 128u) * 65536u; + const auto texelBytes = DisplayTexelBytes(buffer.pixelFormat); + const auto blockHeight = 65536u / 128u / texelBytes; + const auto size = buffer.tilingMode == 1 ? static_cast(pitch) * buffer.height * texelBytes + : static_cast((buffer.width + 127u) / 128u) * ((buffer.height + blockHeight - 1u) / blockHeight) * 65536u; require(size <= std::numeric_limits::max() && size <= std::numeric_limits::max() - buffer.address, "VideoOut: display buffer range overflow"); return static_cast(size); } +std::size_t DisplayBufferOffset(const DisplayBuffer& buffer, std::uint32_t x, std::uint32_t y) { + static_cast(DisplayBufferSize(buffer)); + require(x < buffer.width && y < buffer.height, "VideoOut: display buffer coordinate out of range"); + return texelOffset(buffer, x, y, DisplayTexelBytes(buffer.pixelFormat)); +} + std::vector DecodeDisplayBuffer(const DisplayBuffer& buffer, std::span source) { PerformanceTimer timing("DisplayBuffer.Decode"); require(source.size() == DisplayBufferSize(buffer), "VideoOut: invalid display buffer size"); std::vector pixels(static_cast(buffer.width) * buffer.height * 4); + if (DisplayHalfFloat(buffer.pixelFormat)) { + const bool redLow = DisplayRedLow(buffer.pixelFormat); + timing.Mark("validate_allocate"); + for (std::uint32_t y = 0; y < buffer.height; ++y) { + for (std::uint32_t x = 0; x < buffer.width; ++x) decodeHalfFloatTexel(source.data() + texelOffset(buffer, x, y, 8u), pixels.data() + (static_cast(y) * buffer.width + x) * 4, redLow); + } + timing.Mark("detile_convert"); + return pixels; + } const auto blocksPerRow = (buffer.width + 127u) / 128u; - const bool rgba = baseFormat(buffer.pixelFormat) == PixelFormatR8G8B8A8; + const bool rgba = DisplayRedLow(buffer.pixelFormat); + const bool pq = DisplayPq(buffer.pixelFormat); + 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; @@ -124,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); + 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{}; @@ -148,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(), baseFormat(buffer.pixelFormat) == PixelFormatR8G8B8A8, (buffer.pixelFormat & PixelFormatUnormBit) != 0); + decodeTexel(texel.data(), pixel.data(), DisplayRedLow(buffer.pixelFormat) && !DisplayHalfFloat(buffer.pixelFormat), (buffer.pixelFormat & PixelFormatUnormBit) != 0, DisplayPq(buffer.pixelFormat), PqToSdrWhiteNits(AgcDriver::CurrentHostHdr())); return pixel; } @@ -190,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 7a013fdbe4..914f5961e6 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/DisplayFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/DisplayFormat.cpp @@ -1,10 +1,17 @@ #include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" +#include +#include +#include namespace AgcDriver { namespace { constexpr std::uint64_t PixelFormatUnormBit = 0x0100000000000000ull; constexpr std::uint64_t PixelFormatR8G8B8A8 = 0x8000000022000000ull; +constexpr std::uint64_t PixelFormatR16G16B16A16Float = 0xc001000622000000ull; +constexpr std::uint64_t PixelFormatB16G16R16A16Float = 0xc001000600000000ull; +constexpr std::uint64_t PixelFormatR10G10B10A2Bt2100Pq = 0x8100070422000000ull; +constexpr std::uint64_t PixelFormatB10G10R10A2Bt2100Pq = 0x8100070400000000ull; } @@ -13,16 +20,30 @@ bool DisplayTenBit(std::uint64_t pixelFormat) { } bool DisplayRedLow(std::uint64_t pixelFormat) { - return (pixelFormat & ~PixelFormatUnormBit) == PixelFormatR8G8B8A8; + return (pixelFormat & ~PixelFormatUnormBit) == PixelFormatR8G8B8A8 || pixelFormat == PixelFormatR16G16B16A16Float || pixelFormat == PixelFormatR10G10B10A2Bt2100Pq; +} + +bool DisplayPq(std::uint64_t pixelFormat) { + return pixelFormat == PixelFormatR10G10B10A2Bt2100Pq || pixelFormat == PixelFormatB10G10R10A2Bt2100Pq; +} + +bool DisplayHalfFloat(std::uint64_t pixelFormat) { + return pixelFormat == PixelFormatR16G16B16A16Float || pixelFormat == PixelFormatB16G16R16A16Float; +} + +std::uint32_t DisplayTexelBytes(std::uint64_t pixelFormat) { + return DisplayHalfFloat(pixelFormat) ? 8u : 4u; } VkFormat DisplayTexelFormat(std::uint64_t pixelFormat) { + if (DisplayHalfFloat(pixelFormat)) return VK_FORMAT_R16G16B16A16_SFLOAT; const bool redLow = DisplayRedLow(pixelFormat); if (DisplayTenBit(pixelFormat)) return redLow ? VK_FORMAT_A2B10G10R10_UNORM_PACK32 : VK_FORMAT_A2R10G10B10_UNORM_PACK32; return redLow ? VK_FORMAT_R8G8B8A8_UNORM : VK_FORMAT_B8G8R8A8_UNORM; } ResidentPresent ResidentPresentPath(VkFormat storage, std::uint64_t pixelFormat, bool blitSource) { + if (DisplayHalfFloat(pixelFormat)) return storage == VK_FORMAT_R16G16B16A16_SFLOAT ? ResidentPresent::Convert : ResidentPresent::None; const bool tenBit = DisplayTenBit(pixelFormat); if (!tenBit && blitSource && storage == DisplayTexelFormat(pixelFormat)) return ResidentPresent::Blit; switch (storage) { @@ -31,12 +52,45 @@ 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; + if (offset % displayBytes != 0 || offset / displayBytes >= slices) return std::nullopt; + return static_cast(offset / displayBytes); +} + } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp index 59d038112b..b94858e431 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp @@ -1,6 +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 { @@ -9,6 +14,28 @@ double TraceMs() { return std::chrono::duration(std::chrono::steady_clock::now() - origin).count(); } +std::uint64_t SkippedShader() { + static const std::uint64_t skipped = [] { const char* text = std::getenv("APS5_SKIP_SHADER"); return text ? std::strtoull(text, nullptr, 16) : 0ull; }(); + return skipped; +} + +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 975e68edb4..7bba71169d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -1,26 +1,71 @@ #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/Recorder.hpp" #include "Optimization/ResourceProgram.hpp" +#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); - auto it = submission.shaders->upper_bound(address); - std::shared_ptr registeredShader; - if (it != submission.shaders->begin()) { - --it; - if (address - it->second->codeAddress < it->second->code.size() * sizeof(std::uint32_t)) registeredShader = it->second; - } - if (!registeredShader) registeredShader = ReadRawComputeShader(address); + if (address == SkippedShader() && address != 0) return; + auto registeredShader = RegisteredProgram(*submission.shaders, address, {0}); + if (!registeredShader) registeredShader = ReadRawShader(address); const auto& snapshot = *registeredShader; require(snapshot.type == 0, "compute program refers to a non-compute shader"); const auto userCount = (readRegister(queue.shader, 0x213) >> 1u) & 0x1fu; @@ -29,6 +74,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, userData.push_back(readUserData(queue.shader, 0x240 + i)); } auto compute = Graphics::DecodeComputeStageInfo(queue.shader, snapshot.header); + const bool resolving = resolvingAhead(); std::vector memory{{snapshot.codeAddress, std::as_bytes(std::span(snapshot.code))}}; if (!snapshot.header.empty()) memory.push_back({snapshot.headerAddress, snapshot.header}); @@ -42,22 +88,57 @@ void Driver::dispatch(QueueState& queue, std::span packet, } const auto codeOffset = static_cast((address - snapshot.codeAddress) / sizeof(std::uint32_t)); std::array resolved{}; - if (indirectArguments != 0 && matchesFillKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute)) { - - recordQueuedLabelsBeforeRead(submission.queue); + ResolvedDispatch ahead; + bool fromAhead = false; + auto& groupStats = groupCaptureStats(); + if (!resolving) { + auto& parked = resolvedAhead(); + if (const auto found = parked.find(currentPacketOffset()); found != parked.end()) { + ahead = std::move(found->second); + parked.erase(found); + fromAhead = ahead.address == address; + if (!fromAhead) ++groupStats.addressMismatches; + } + } + if (fromAhead) { + resolved = ahead.packet; + packet = resolved; + indirectArguments = ahead.indirectArguments; + } 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; } - if (fillBuffer(queue, submission.queue, packet, std::span(snapshot.code).subspan(codeOffset), userData, compute, localDevice)) { - pendingDispatchPhases().outcome = DispatchOutcome::FillHle; - return; + std::array partialGroups{}; + 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)}); } - if (indirectArguments == 0 && copyBuffer(queue, submission.queue, packet, std::span(snapshot.code).subspan(codeOffset), userData, compute, localDevice, address)) { - pendingDispatchPhases().outcome = DispatchOutcome::CopyHle; - return; + const bool pipelineCandidate = !resolving && !fromAhead && DispatchItemsEnabled() && DrawPipeline::Active() && indirectArguments == 0 && deferredLabels().labels.empty() && !matchesCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute) && !MatchesStreamCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute); + const bool partialLastGroup = partialGroups != std::array{}; + if (resolving) { + if (!partialLastGroup && matchesFillKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute)) return; + 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 (indirectArguments == 0 && (packet[4] & 0x20u) == 0) { + if (const auto fill = Graphics::MatchMemoryValueFill(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads, {packet[1], packet[2], packet[3]})) Graphics::NoteMemoryValueFill(*fill); + } + if (!partialLastGroup && fillBuffer(queue, submission.queue, packet, std::span(snapshot.code).subspan(codeOffset), userData, compute, localDevice)) { + pendingDispatchPhases().outcome = DispatchOutcome::FillHle; + return; + } + 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); + Graphics::NoteHtileDepthClear(htile, static_cast(threads * 16u), nativeBytes); + } + if (!partialLastGroup && indirectArguments == 0 && copyBuffer(queue, submission.queue, packet, std::span(snapshot.code).subspan(codeOffset), userData, compute, localDevice, address)) { + pendingDispatchPhases().outcome = DispatchOutcome::CopyHle; + return; + } } if (indirectArguments == 0 && (packet[4] & 0x20u) != 0) { const std::array threads{packet[1], packet[2], packet[3]}; @@ -65,11 +146,17 @@ void Driver::dispatch(QueueState& queue, std::span packet, if (threads[axis] % compute.numThreads[axis] != 0) compute.partialThreads = threads; } } + 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; @@ -180,27 +267,84 @@ void Driver::dispatch(QueueState& queue, std::span packet, } mix(reinterpret_cast(registeredShader.get())); + if (fromAhead && key != ahead.key) { + fromAhead = false; + ++groupStats.keyMismatches; + } phaseTiming.Phase(PhaseKey); timing.Mark("decode_key_device_setup"); - lookupDispatch(address, submission, key, noDispatchCache, traceCache, profile, memory, phaseTiming, phaseMs, compiledResult, keepVariant, captured, liveWords, dataHit, cached, validated, missedEntry, missedDiffering); + 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); + attachVariant = std::move(ahead.attachVariant); + shaderMemory = std::move(ahead.shaderMemory); + captured = std::move(ahead.captured); + liveWords = std::move(ahead.liveWords); + capture = std::move(ahead.capture); + dataHit = ahead.dataHit; + 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); + phaseTiming.Phase(PhaseLookup); + } else { + 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 { - shaderMemory = std::make_shared(memory, &queryPendingWrite, &observePendingWrite, hookWaitCounter()); + } else if (!fromAhead) { + 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; @@ -220,7 +364,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); @@ -233,6 +383,29 @@ void Driver::dispatch(QueueState& queue, std::span packet, phaseTiming.Phase(PhaseRecompile); insertDispatch(address, key, noDispatchCache || !CacheableResult(*compiledResult), profile, registeredShader, forgetAtCapture, memory, shaderMemory, captured, capture, compiledResult, missedEntry, missedDiffering, attachVariant, phaseTiming); } + if (resolving) { + auto& parked = resolvedAhead()[currentPacketOffset()]; + parked = ResolvedDispatch{}; + parked.address = address; + parked.key = key; + parked.indirectArguments = indirectArguments; + std::copy_n(packet.begin(), std::min(packet.size(), parked.packet.size()), parked.packet.begin()); + parked.registeredShader = registeredShader; + parked.compiledResult = compiledResult; + parked.keepVariant = keepVariant; + parked.attachVariant = attachVariant; + parked.shaderMemory = shaderMemory; + parked.captured = std::move(captured); + parked.liveWords = std::move(liveWords); + parked.capture = capture; + parked.dataHit = dataHit; + parked.cached = cached; + parked.validated = validated; + parked.resolvedAt = std::chrono::steady_clock::now(); + ++groupStats.resolved; + if (cached) ++groupStats.resolvedHits; + return; + } if (verifyDataHits() && dataHit) verifyDataHit(snapshot, codeOffset, request, memory, address, *keepVariant, liveWords, *compiledResult); if (recordQueuedLabelsAfterCapture(submission.queue, captured)) { @@ -251,6 +424,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, groups[axis] = (groups[axis] + threads - 1) / threads; } } + const std::uint64_t dispatchThreads = indirectArguments != 0 ? 0 : std::uint64_t{groups[0]} * groups[1] * groups[2] * compute.numThreads[0] * compute.numThreads[1] * compute.numThreads[2]; static const bool traceIo = std::getenv("APS5_TRACE_DISPATCH_IO") != nullptr; if (traceIo) { @@ -262,16 +436,79 @@ 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) { + if (fromAhead) { + ++groupStats.deviceRejects; + dispatch(queue, packet, submission, indirectArguments); + return true; + } 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] { + DrawPipeline::FollowEpoch(epoch); + GuestMemory::SetCurrentPacket(0x15, queueId); + const auto& compiledCommit = *result; + const bool noteWrites = writeEvidenceEnabled() || traceCapSync(); + try { + std::shared_ptr recipeHit = localDevice->PrepareRecipe(recipe, false); + std::shared_ptr builtRecipe; + bool writersNoted = false; + for (;;) { + std::shared_ptr prepared; + if (recipeHit == nullptr || VulkanDevice::VerifyRecipes()) prepared = localDevice->PrepareDispatch(compiledCommit, snapshots); + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Dispatch); + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + recordLabelsForPacket(localDevice.get(), queueId); + if (noteWrites && writerKeyedEvidence() && !writersNoted) { + noteWrittenBuffers(address, queueId, compiledCommit); + writersNoted = true; + } + if (recipeHit != nullptr) { + VulkanDevice::IndirectOutcome outcome{0, 0}; + const auto recipeResult = localDevice->DispatchRecipe(compiledCommit, groups[0], groups[1], groups[2], 0, address, recipeHit, outcome, VulkanDevice::VerifyRecipes() ? prepared : nullptr, dataHit); + if (recipeResult == RecipeOutcome::Rebuild) { + recipeHit = nullptr; + VulkanDevice::NoteRecipe(VulkanDevice::RecipeEvent::Restart, false); + itemRebuilds.fetch_add(1, std::memory_order_relaxed); + continue; + } + } else { + localDevice->Dispatch(compiledCommit, groups[0], groups[1], groups[2], snapshots, address, std::move(prepared), &builtRecipe); + } + break; + } + if (noteWrites && !writerKeyedEvidence()) noteWrittenBuffers(address, queueId, compiledCommit); + if (builtRecipe != nullptr) { + if (dataHit) { + auto own = std::make_shared(*builtRecipe); + own->dataWordsHash = Graphics::ShaderResources::DataWordsHash({ShaderRecompiler::ShaderStage::Compute, keepVariant->compiled.get(), 0}); + builtRecipe = std::move(own); + } + keepVariant->recipe.store(std::move(builtRecipe), std::memory_order_release); + VulkanDevice::NoteRecipe(VulkanDevice::RecipeEvent::Attach, false); + } + } catch (const std::exception& error) { + char where[64]; + std::snprintf(where, sizeof(where), "compute shader 0x%llx: ", static_cast(address)); + throw std::runtime_error(where + std::string(error.what())); + } + }, std::move(itemWrites)); + ReportDispatchItems(profile); + return; + } + std::shared_ptr recipeHit; if (cached && keepVariant != nullptr && !stampValidate()) { recipeHit = localDevice->PrepareRecipe(keepVariant->recipe.load(std::memory_order_acquire), indirectArguments != 0); @@ -282,15 +519,30 @@ 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); + prepared = localDevice->PrepareDispatch(compiled, snapshots, dispatchThreads, address); } catch (const std::exception& error) { - rethrow(error); + if (rethrow(error)) return; } if (profile) { const auto now = std::chrono::steady_clock::now(); @@ -320,10 +572,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; @@ -332,13 +586,17 @@ 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); } } catch (const std::exception& error) { - rethrow(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 new file mode 100644 index 0000000000..0b6f1f0eb0 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp @@ -0,0 +1,218 @@ +#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" +#include "ThreadOwned.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp" +#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" +#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" +#include +#include +#include + +namespace AgcDriver::DriverDetail { + +std::map& Driver::resolvedAhead() { + static thread_local std::map* resolved = nullptr; + return ShaderRecompiler::ThreadOwned(resolved); +} + +Driver::GroupCaptureStats& Driver::groupCaptureStats() { + static thread_local GroupCaptureStats stats; + return stats; +} + +std::size_t& Driver::currentPacketOffset() { + static thread_local std::size_t offset = 0; + return offset; +} + +bool& Driver::resolvingAhead() { + static thread_local bool resolving = false; + return resolving; +} + +bool& Driver::gateOpened() { + static thread_local bool opened = false; + 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; + groupCaptureStats().stale += resolved.size(); + resolved.clear(); +} + +namespace { + +struct RunLabel { + std::uint64_t address; + std::size_t size; + std::uint64_t value; +}; + +bool InCode(const ShaderRecompiler::MemoryRegion& region, const std::shared_ptr& code) { + if (code == nullptr) return false; + if (region.guestAddress >= code->codeAddress && region.guestAddress < code->codeAddress + code->code.size() * sizeof(std::uint32_t)) return true; + return !code->header.empty() && region.guestAddress >= code->headerAddress && region.guestAddress < code->headerAddress + code->header.size(); +} + +bool OverlapsRange(std::span regions, const std::vector>& ranges, const std::shared_ptr& code) { + for (const auto& region : regions) { + if (InCode(region, code)) continue; + for (const auto& [begin, end] : ranges) { + if (begin < region.guestAddress + region.bytes.size() && region.guestAddress < end) return true; + } + } + return false; +} + +bool WrittenByRun(std::span wait, const std::vector& labels) { + const std::uint64_t awaited = wait[2] | (static_cast(wait[3]) << 32u); + const auto bytes = Pm4::WaitAwaitedBytes(wait); + for (auto it = labels.rbegin(); it != labels.rend(); ++it) { + if (it->address != awaited || it->size < bytes) continue; + return Pm4::WaitComparesValue(wait, bytes == 8 ? it->value : (it->value & 0xffffffffu)); + } + return false; +} + +void NoteLabel(std::vector& labels, std::uint64_t address, std::span bytes) { + if (bytes.size() != 4 && bytes.size() != 8) return; + std::uint64_t value = 0; + std::memcpy(&value, bytes.data(), bytes.size()); + labels.push_back({address, bytes.size(), value}); +} + +bool WritesThroughPointers(const ShaderRecompiler::RecompileResult& compiled) { + return std::any_of(compiled.bindings.begin(), compiled.bindings.end(), [](const ShaderRecompiler::DescriptorBinding& binding) { return binding.role == ShaderRecompiler::DescriptorRole::BdaPagetable; }); +} + +bool OverlapsLabel(std::span regions, const std::vector& labels) { + for (const auto& label : labels) { + for (const auto& region : regions) { + if (label.address < region.guestAddress + region.bytes.size() && region.guestAddress < label.address + label.size) return true; + } + } + return false; +} + +} + +void Driver::resolveGroupAhead(const Submission& submission, const QueueState& live, std::size_t from) { + static const bool allQueues = std::getenv("APS5_GROUP_CAPTURE_ALL_QUEUES") != nullptr; + if (!allQueues && submission.queue == 0) return; + auto& resolved = resolvedAhead(); + if (resolved.contains(from) || device.Load() == nullptr) return; + auto& stats = groupCaptureStats(); + const auto begin = std::chrono::steady_clock::now(); + ++stats.runs; + QueueState scratch = live; + std::vector labels; + std::vector> writes; + bool unknownWrites = false; + for (const auto& label : deferredLabels().labels) NoteLabel(labels, label.address, std::span(label.bytes).first(label.size)); + struct Resolving { + std::size_t offset; + Resolving() : offset(currentPacketOffset()) { resolvingAhead() = true; } + ~Resolving() { + resolvingAhead() = false; + currentPacketOffset() = offset; + } + } resolving; + const auto& commands = submission.commands; + for (std::size_t cursor = from; cursor < commands.size();) { + const auto header = commands[cursor]; + if (Pm4::FillerPacket(header)) { + ++cursor; + continue; + } + const auto count = Pm4::PacketWords(header); + if (cursor + count > commands.size()) break; + const auto packet = std::span(commands).subspan(cursor, count); + const auto opcode = (header >> 8u) & 0xffu; + const auto next = cursor + count; + if (header == FlipPacketHeader || header == RenderingWaitPacketHeader || opcode == 0x22 || (Pm4::Predicated(header) && scratch.predication.operation != 0)) break; + if (opcode == 0x3c || opcode == 0x93) { + if (!Pm4::WaitSatisfiedUnchecked(packet) && !WrittenByRun(packet, labels)) break; + } else if (opcode == 0x15 || opcode == 0x16) { + if (resolved.contains(cursor)) break; + if (opcode == 0x16) { + std::uint64_t arguments = 0; + try { + arguments = Pm4::DispatchArgumentAddress(packet, scratch); + } catch (const std::exception&) { + break; + } + 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) && indirectArgumentsPath(packet, arguments) != IndirectGpu) { + ++stats.writtenBefore; + break; + } + } + currentPacketOffset() = cursor; + try { + if (opcode == 0x15) dispatch(scratch, packet, submission); + else dispatchIndirect(scratch, packet, submission); + Pm4::NoteDispatchRegisters(packet, scratch); + } catch (const std::exception&) { + ++stats.failures; + break; + } + if (const auto found = resolved.find(cursor); found != resolved.end()) { + if (OverlapsLabel(found->second.captured, labels)) { + resolved.erase(found); + ++stats.labelOverlaps; + break; + } + if (OverlapsRange(found->second.captured, writes, found->second.registeredShader)) { + resolved.erase(found); + ++stats.writtenBefore; + break; + } + if (found->second.compiledResult != nullptr) appendWrittenRanges(*found->second.compiledResult, writes); + if (found->second.compiledResult == nullptr || WritesThroughPointers(*found->second.compiledResult)) unknownWrites = true; + } else { + unknownWrites = true; + } + } else if (opcode == 0x37 || opcode == 0x49) { + const auto label = Pm4::DecodeLabelWrite(packet); + if (!label) break; + NoteLabel(labels, label->address, label->Bytes()); + writes.emplace_back(label->address, label->address + label->Bytes().size()); + } else if (opcode != 0x3f && opcode != 0x42 && opcode != 0x46 && opcode != 0x58) { + if (Pm4::AccessesMemory(header)) break; + try { + Pm4::Execute(packet, scratch); + } catch (const std::exception&) { + break; + } + } + cursor = next; + } + const auto ms = std::chrono::duration(std::chrono::steady_clock::now() - begin).count(); + stats.resolveMs += ms; + stats.resolveMaxMs = std::max(stats.resolveMaxMs, ms); + reportGroupCapture(submission.queue); +} + +void Driver::reportGroupCapture(std::uint32_t queue) { + static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; + if (!profile) return; + 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, %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.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 42efc40f83..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,22 +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); 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]), 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) { - recordQueuedLabelsBeforeRead(submission.queue); const auto readStart = std::chrono::steady_clock::now(); - const auto direct = Pm4::ReadDispatchArguments(arguments, initiator); + const auto direct = readDispatchArguments(submission.queue, arguments, initiator); countIndirect(path, std::chrono::duration(std::chrono::steady_clock::now() - readStart).count()); + if (!resolvingAhead()) { + auto& resolved = resolvedAhead(); + if (const auto found = resolved.find(currentPacketOffset()); found != resolved.end() && found->second.indirectArguments == 0 && found->second.packet[0] == 0xc0031500u) { + if (found->second.packet == direct) { + scope.source.ahead = AheadArguments::Matched; + } else { + resolved.erase(found); + ++groupCaptureStats().argumentMismatches; + scope.source.ahead = AheadArguments::Differed; + } + } + } 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 bf6ca03d18..acc9d7ea26 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -1,10 +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 { @@ -28,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); @@ -64,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(); @@ -79,17 +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; - const auto& pixel = decode->pixel; - std::vector programs = decode->programs; + auto pixel = decode->pixel; + 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); @@ -97,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; @@ -120,10 +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]; - memory.insert(memory.end(), program.memory.begin(), program.memory.end()); linked.push_back({roles[i], program.binary, program.userDataBase, program.firstUserSgpr, program.userData}); } timing.Mark("prepare"); @@ -135,16 +249,22 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe const auto& program = programs[i]; 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]); + 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]->depthRemapMode = graphics.depthRemap.mode; + } }; 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()); std::vector stages; - results.reserve(programs.size() + (graphics.rectList ? 2u : 0u)); + results.reserve(programs.size() + 2u); stages.reserve(programs.size()); std::uint32_t pushCursorBytes = 0; @@ -157,9 +277,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) { @@ -168,6 +314,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}; @@ -179,53 +330,117 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe phaseTiming.Phase(DrawRowDecode); } - static const bool indxOffsetSkipFold = std::getenv("APS5_INDX_OFFSET_SKIP_FOLD") != nullptr; - static const bool indexedOffsetFold = std::getenv("APS5_NO_INDEXED_OFFSET_FOLD") == nullptr; - const auto fold = [&](const ShaderRecompiler::RecompileResult& main, Pm4::DrawParameters& parameters) { - if (parameters.indexed && !indexedOffsetFold) return; - if (main.vertexOffsetSgpr >= 0 && (parameters.firstVertex == 0 || !indxOffsetSkipFold)) { - const auto offset = drawUserWord(programs.front(), main.vertexOffsetSgpr); - require(offset <= std::numeric_limits::max() - parameters.firstVertex, "draw vertex offset overflow"); - parameters.firstVertex += offset; - } - if (main.instanceOffsetSgpr >= 0) parameters.firstInstance = drawUserWord(programs.front(), main.instanceOffsetSgpr); - }; + 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; const auto& program = programs[i]; + pushCursorBytes = Graphics::StagePushOffset(pushCursorBytes, program.binary.stage, localDevice->GraphicsPipelineLibraries()); 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 { - resultIndex[i] = results.size(); - results.push_back(materializeDrawStage(i, pushCursorBytes, queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected)); - if (!rejected.empty()) return DrawVerdict::Rejected; - programResults[i] = &results.back(); + 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); + } + if (dataEntry != nullptr && reused[i]) ++dataStagesReused; + else if (dataEntry != nullptr) ++dataStagesCompiled; } const auto& result = *programResults[i]; if (i == 0 && drawParameters.indirect) { - indirectCpu = classifyIndirectDraw(result, graphics, programs.front(), localDevice, drawParameters, traceIndirect); + indirectCpu = ClassifyIndirectDraw(result, graphics, programs.front(), localDevice, drawParameters, traceIndirect); } else if (i == 0) { fold(result, drawParameters); } - require(result.pushConstants.size() <= Graphics::PipelinePushConstantBytes - pushCursorBytes, "stage push constants exceed the pipeline push constant block"); + const auto slotEnd = (pushCursorBytes / Graphics::PipelinePushSlotBytes + 1u) * Graphics::PipelinePushSlotBytes; + require(result.pushConstants.size() <= slotEnd - pushCursorBytes, "stage push constants exceed the pipeline push constant block"); stages.push_back({program.binary.stage, &result, result.pushConstants.empty() ? 0u : pushCursorBytes}); 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; @@ -234,25 +449,56 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe const auto buildRectList = [&] { phaseTiming.Phase(DrawRowVectors); require(programs.size() == 2 && programResults[0] != nullptr && programResults[1] != nullptr, "rect-list requires vertex and fragment programs"); - auto rectangle = PreparedRectangle(*programs[0].snapshot, programResults[0]->variantId, programResults[1]->variantId); + 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 && 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"); + const auto geometryLimits = localDevice->GeometryLimits(); + if (geometryLimits || !localDevice->Target().tessellation) { + results.push_back(ShaderRecompiler::BuildBarycentricGeometryShader(*programResults[0], *programResults[1], localDevice->Target(), geometryLimits)); + stages.insert(stages.begin() + 1, Graphics::CompiledShader{Stage::Geometry, &results.back(), 0}); + } else { + require(graphics.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST && !graphics.primitiveRestart, "a pixel shader that reads barycentrics without VK_KHR_fragment_shader_barycentric or geometry shaders needs a triangle list without primitive restart"); + auto patches = ShaderRecompiler::BuildBarycentricTessellationShaders(*programResults[0], *programResults[1], localDevice->Target(), localDevice->MaxFragmentInputComponents()); + const auto first = results.size(); + results.push_back(std::move(patches.control)); + results.push_back(std::move(patches.evaluation)); + stages.insert(stages.begin() + 1, {{Stage::TessellationControl, &results[first], 0}, {Stage::TessellationEvaluation, &results[first + 1], 0}}); + } + } std::vector snapshots; const auto snapshot = [&] { snapshots.clear(); + snapshots.reserve(memory.size()); for (const auto& region : memory) snapshots.push_back({region.guestAddress, region.bytes}); }; snapshot(); @@ -277,17 +523,14 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe if (drawParameters.indirect && indirectCpu) { const auto indirect = *drawParameters.indirect; - if (drawHit) { - - for (std::size_t i = 0; i < programs.size(); ++i) { - if (programResults[i] == nullptr) continue; - resultIndex[i] = results.size(); - results.push_back(ShaderRecompiler::RecompileResult(*programResults[i])); - for (auto& stage : stages) { - if (stage.program == programResults[i]) stage.program = &results[resultIndex[i]]; - } - programResults[i] = &results[resultIndex[i]]; + for (std::size_t i = 0; i < programs.size(); ++i) { + if (programResults[i] == nullptr) continue; + resultIndex[i] = results.size(); + results.push_back(ShaderRecompiler::RecompileResult(*programResults[i])); + for (auto& stage : stages) { + if (stage.program == programResults[i]) stage.program = &results[resultIndex[i]]; } + programResults[i] = &results[resultIndex[i]]; } recordQueuedLabelsBeforeRead(submission.queue); const auto readStart = std::chrono::steady_clock::now(); @@ -295,22 +538,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) { @@ -329,7 +583,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"); } @@ -340,6 +594,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); @@ -351,7 +623,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(); @@ -367,6 +639,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); @@ -387,4 +678,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 bcd1a44fab..1d76a6a338 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp @@ -1,5 +1,10 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #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 { @@ -21,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; @@ -55,7 +75,55 @@ void Driver::accountDrawVariant(const DispatchVariant& variant, bool added) { } } -void Driver::insertDrawEntry(std::uint64_t key, std::vector>& fresh, std::shared_ptr decode) { +DrawKeySightings::DrawKeySightings(std::size_t keyCount) { + Graphics::Require(keyCount >= Ways, "draw key sightings need at least one bucket"); + const auto buckets = std::bit_ceil(keyCount / Ways); + keys.assign(buckets * Ways, 0); + filled.assign(buckets, 0); + next.assign(buckets, 0); + bucketMask = buckets - 1; +} + +bool DrawKeySightings::SeenBefore(std::uint64_t key) { + auto mixed = key; + mixed = (mixed ^ (mixed >> 30u)) * 0xbf58476d1ce4e5b9ull; + mixed = (mixed ^ (mixed >> 27u)) * 0x94d049bb133111ebull; + mixed ^= mixed >> 31u; + const auto bucket = static_cast(mixed) & bucketMask; + auto* ways = keys.data() + bucket * Ways; + if (std::find(ways, ways + filled[bucket], key) != ways + filled[bucket]) return true; + ways[next[bucket]] = key; + next[bucket] = static_cast((next[bucket] + 1u) % Ways); + if (filled[bucket] < Ways) ++filled[bucket]; + return false; +} + +bool Driver::admitDrawKey(std::uint64_t key, std::uint64_t shape) { + std::lock_guard cacheLock(drawCacheMutex); + if (drawCache.contains(key)) return true; + if (!drawKeySightings) drawKeySightings.emplace(drawCacheEntries() * DrawKeySightings::Ways); + 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, std::uint64_t shape) { std::lock_guard cacheLock(drawCacheMutex); auto& counters = drawEntryCounters; ++counters.inserts; @@ -63,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; @@ -71,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; @@ -87,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(); @@ -101,12 +171,34 @@ 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; } } +void Driver::eraseDrawEntry(std::uint64_t key, const std::shared_ptr& expected) { + const auto found = drawCache.find(key); + if (found == drawCache.end() || found->second != expected) return; + 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); +} + +bool DecodeProgramsCurrent(const DrawDecode& decode, const ShaderRegistry&) { + return std::all_of(decode.programs.begin(), decode.programs.end(), [&](const DrawProgram& program) { + if (!program.snapshot->header.empty()) return true; + return ReadRawShader(program.snapshot->codeAddress + program.codeOffset * sizeof(std::uint32_t)) == program.snapshot && + (program.tailContinuation == nullptr || ReadRawShader(program.tailContinuation->codeAddress) == program.tailContinuation); + }); +} + std::shared_ptr Driver::findDrawRecipe(std::uint64_t key, const std::vector>& stages) { std::shared_ptr> records; { 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 9d82d8550e..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,21 +1,71 @@ #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{}; } +DrawEntryCounters Driver::DrawCacheCounters() { + std::lock_guard cacheLock(drawCacheMutex); + 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 ad07dcdc66..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 { -ShaderRecompiler::RecompileResult Driver::materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector>& vertexInfos, std::vector& memory, const std::vector& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr& localDevice, ShaderMemory& shaderMemory, std::vector& stageCaptures, std::vector& recompiled, bool drawHit, const std::vector>& matched, const std::vector>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array& phaseMs, std::string& rejected) { +std::shared_ptr Driver::materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector>& vertexInfos, std::vector& memory, const std::vector& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr& localDevice, ShaderMemory& shaderMemory, std::vector& stageCaptures, std::vector& recompiled, bool drawHit, const std::vector>& matched, const std::vector>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array& phaseMs, std::string& rejected, const std::optional& meshBarycentrics) try { PerformanceTimer timing("Shader.DrawStage"); using Stage = ShaderRecompiler::ShaderStage; phaseTiming.Phase(DrawRowVectors); @@ -20,28 +24,48 @@ ShaderRecompiler::RecompileResult Driver::materializeDrawStage(std::size_t i, st program.binary, {waveSize, program.firstUserSgpr, program.userData, std::nullopt, program.binary.stage == Stage::Fragment ? std::optional(pixel) : std::nullopt, vertexInfos[i], memory, RegisteredFloatMode(*program.snapshot)}, localDevice->Target(), - {0, 0, pushOffset, (graphics.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : Graphics::PipelinePushConstantBytes) - pushOffset}, - 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,38 +99,116 @@ ShaderRecompiler::RecompileResult Driver::materializeDrawStage(std::size_t i, st 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"); - ShaderRecompiler::RecompileResult result = *stageCapture.compiled; + 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 result; + 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)) { +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()); @@ -120,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 fac2d2bdc6..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; @@ -28,31 +88,24 @@ void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const const auto prepare = [&](std::uint64_t address, std::uint8_t type, Stage stage, std::uint32_t rsrc2, std::uint32_t userDataBase) { const bool nullPixel = stage == Stage::Fragment && (address == 0 || pixelSkipped); if (nullPixel) address = NullPixelProgramAddress(); - auto it = registry.upper_bound(address); - require(it != registry.begin(), "graphics program does not belong to a registered shader"); - --it; - const auto& snapshot = *it->second; - require(address - snapshot.codeAddress < snapshot.code.size() * sizeof(std::uint32_t), "graphics program is outside registered shader code"); + const auto program = ProgramSnapshot(registry, address, {type}); + const auto& snapshot = *program; require((address - snapshot.codeAddress) % sizeof(std::uint32_t) == 0, "graphics entry point is not dword aligned"); - require(snapshot.type == type, "graphics program refers to an incompatible shader binary type"); - Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, rsrc2); - const auto resources = nullPixel && !queue.shader.contains(rsrc2) ? 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"); + require(snapshot.header.empty() || snapshot.type == type, "graphics program refers to an incompatible shader binary type"); + 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}}}, - it->second, + 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))); - } + result.resourceRegister = rsrc2; + result.nullPixel = nullPixel; + ReadUserWords(queue, result, staticAbi); return result; }; { @@ -65,37 +118,29 @@ 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) { - 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); - auto snapshot = registry.upper_bound(frontAddress); - require(snapshot != registry.begin(), "geometry front program is not registered"); - --snapshot; - const auto type = snapshot->second->type; + const auto front = ProgramSnapshot(registry, frontAddress, {2, 4}); + 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); @@ -106,27 +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); + 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 8d42a6ae60..6bfa9b1fb4 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,11 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include namespace AgcDriver::DriverDetail { -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 +22,13 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(tess.breakAtInstance)}) mix(value); + mix(tess.mergedHull); } mix(graphics.rectList); mix(programs.size()); @@ -48,6 +50,7 @@ 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; @@ -87,21 +107,37 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptrGraphicsPipelineLibraries()); const auto& variants = entry->stages[i]; auto outcome = EntryOutcome::Differing; bool anyLayout = false; for (std::size_t rank = 0; rank < variants.size(); ++rank) { const auto& variant = variants[rank]; if (variant->pushOffset != 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; @@ -122,6 +158,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; @@ -159,6 +198,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(*miss)]; if (registerKey && entry->decode != nullptr) ++counters.decodePartial; @@ -170,9 +210,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 e75ed53eea..a6787296d4 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,7 @@ std::optional Driver::precheckDraw(const QueueState& queue, const S localDevice->ColorMetadataPass(*pass); return DrawVerdict::Drawn; } + if (Graphics::DepthResummarizeDrawsNothing(queue)) return DrawVerdict::Nothing; rejected = Graphics::DepthMaintenanceRejection(queue); if (!rejected.empty()) return DrawVerdict::Rejected; static const bool traceIndirectEnabled = std::getenv("APS5_TRACE_INDIRECT_DRAWS") != nullptr; @@ -35,8 +38,6 @@ std::optional Driver::precheckDraw(const QueueState& queue, const S if (!colorWrites && !queue.shader.contains(0x8)) { const auto word = [&](std::uint32_t offset) { const auto it = queue.context.find(offset); return it == queue.context.end() ? 0u : it->second; }; if ((word(0x200) & 3u) == 0 || ((word(0x010) & 3u) == 0 && (word(0x011) & 1u) == 0)) return DrawVerdict::Nothing; - rejected = "AGC graphics: depth/stencil-only draws without a pixel shader are not implemented"; - return DrawVerdict::Rejected; } } if (drawParameters.indexed) { @@ -46,7 +47,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 29284743d4..fbbe6c5d79 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -1,41 +1,172 @@ #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 { -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; +namespace { + +bool runtimeRegister(std::uint32_t offset) { + return offset - 0x00cu < 32u || offset - 0x08cu < 32u || offset - 0x10cu < 32u || offset == 0x082u || offset == 0x083u || offset == 0x102u || offset == 0x103u; +} + +const auto& registerMasks() { + static const auto masks = [] { + std::array, 3> bits; + for (const auto& range : Graphics::DrawKeyRegisters) { + auto& bank = bits[static_cast(range.bank)]; + bank.resize(std::max(bank.size(), (static_cast(range.first) + range.count + 63) / 64)); + for (auto offset = range.first; offset < range.first + range.count; ++offset) { + if (range.bank == Graphics::RegisterBank::Shader && runtimeRegister(offset)) continue; + bank[offset / 64] |= std::uint64_t{1} << (offset % 64); + } + } + std::array, 3> result; + for (std::size_t i = 0; i < bits.size(); ++i) result[i] = std::make_shared(std::move(bits[i])); + return result; + }(); + return masks; +} + +template +void forEachKeyedUserWord(const QueueState& queue, bool allUserWords, TVisit&& visit) { + const auto words = [&](std::uint32_t base, std::uint32_t count) { + for (auto it = queue.shader.lower_bound(base); it != queue.shader.end() && it->first < base + count; ++it) visit(it->first, it->second); + }; + for (const auto base : {0x00cu, 0x08cu, 0x10cu}) { + const auto resources = queue.shader.find(base - 1); + const auto count = resources == queue.shader.end() ? 0u : ((resources->second >> 1u) & 0x1fu) | (((resources->second >> 27u) & 1u) << 5u); + words(base, allUserWords ? 32u : std::min(count, 32u)); + } + words(0x082u, 2); + words(0x102u, 2); +} + +} + +std::uint64_t RegisterStateKey(const QueueState& queue, bool allUserWords, bool incremental, std::uint64_t* shapeKey, RegisterStateKeyParts* parts) { + 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 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; }; - const auto userEnd = [&](std::uint32_t base) { - if (allUserWords) return base + 32u; - const auto resources = queue.shader.find(base - 1); - if (resources == queue.shader.end()) return base; - const auto count = ((resources->second >> 1u) & 0x1fu) | (((resources->second >> 27u) & 1u) << 5u); - return base + std::min(count, 32u); - }; - const std::array, 3> users{{{0x00cu, userEnd(0x00cu)}, {0x08cu, userEnd(0x08cu)}, {0x10cu, userEnd(0x10cu)}}}; - const auto unread = [&](std::uint32_t offset) { - return std::any_of(users.begin(), users.end(), [&](const auto& user) { return offset >= user.second && offset < user.first + 32u; }); - }; - mix(deviceSerial); - for (const auto& range : Graphics::DrawKeyRegisters) { - const auto& bank = range.bank == Graphics::RegisterBank::Context ? queue.context : range.bank == Graphics::RegisterBank::Shader ? queue.shader : queue.userConfig; - mix((static_cast(range.bank) << 32u) | range.first); - const auto end = range.first + range.count; - const bool shader = range.bank == Graphics::RegisterBank::Shader; - for (auto it = bank.lower_bound(range.first); it != bank.end() && it->first < end; ++it) { - if (shader && unread(it->first)) continue; - mix(it->first); - mix(it->second); + for (const auto base : {0x008u, 0x088u, 0x0c8u, 0x108u, 0x148u}) { + const auto low = queue.shader.find(base); + const auto high = queue.shader.find(base + 1); + 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); const auto high = queue.shader.find(base + 1); @@ -50,14 +181,40 @@ std::uint64_t Driver::drawRegisterKey(const QueueState& queue, const ShaderRegis continue; } --it; - mix(reinterpret_cast(it->second.get())); - mix(address - it->second->codeAddress); + 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) return false; + if (a.resourcesNum != b.resourcesNum || a.fetchAttribReg != b.fetchAttribReg || a.fetchBufferReg != b.fetchBufferReg || a.fetchEmbedded != b.fetchEmbedded || a.paClVsOutCntl != b.paClVsOutCntl) return false; + 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]; @@ -79,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; + 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; @@ -94,17 +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.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.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) return false; + if (x.binary.stage != y.binary.stage || x.binary.codeAddress != y.binary.codeAddress || x.userDataBase != y.userDataBase || x.firstUserSgpr != y.firstUserSgpr || x.userData != y.userData || x.snapshot != y.snapshot || x.codeOffset != y.codeOffset || x.tailContinuation != y.tailContinuation) return false; } + if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.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.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 a9ebfa0253..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 @@ -12,17 +13,29 @@ std::uint32_t Driver::drawUserWord(const DrawProgram& program, std::int32_t sgpr return program.userData[index]; } -std::optional Driver::classifyIndirectDraw(const ShaderRecompiler::RecompileResult& result, const Graphics::State& graphics, const DrawProgram& frontProgram, const std::shared_ptr& localDevice, Pm4::DrawParameters& drawParameters, bool traceIndirect) { +void Driver::FoldDrawOffsets(const ShaderRecompiler::RecompileResult& result, const DrawProgram& program, Pm4::DrawParameters& parameters) { + if (result.vertexOffsetSgpr >= 0) { + const auto offset = drawUserWord(program, result.vertexOffsetSgpr); + require(offset <= std::numeric_limits::max() - parameters.firstVertex, "draw vertex offset overflow"); + parameters.firstVertex += offset; + } + if (result.instanceOffsetSgpr >= 0) parameters.firstInstance = drawUserWord(program, result.instanceOffsetSgpr); +} + +std::optional Driver::ClassifyIndirectDraw(const ShaderRecompiler::RecompileResult& result, const Graphics::State& graphics, const DrawProgram& frontProgram, const std::shared_ptr& localDevice, Pm4::DrawParameters& drawParameters, bool traceIndirect) { std::optional indirectCpu; auto& indirect = *drawParameters.indirect; using Rule = Pm4::DrawParameters::IndirectDraw::Rule; 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 526c7ab37a..04a1f72a02 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp @@ -1,9 +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(); @@ -55,6 +59,10 @@ void ReportFailure(std::exception_ptr error) { } +extern "C" void AgcDriverRaiseWorkerThreadPriority_nid_postfix(const char* role) { + AgcDriver::RaiseWorkerThreadPriority(role); +} + extern "C" void AgcDriverWaitIdle_nid_postfix() try { AgcDriver::WaitIdle(); } catch (const ProcessShutdown&) { @@ -78,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&) { @@ -90,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); } @@ -118,13 +144,19 @@ extern "C" void AgcDriverReportFailure_nid_postfix(std::exception_ptr error) { AgcDriver::ReportFailure(error); } -extern "C" void AgcDriverResolveShaderAbi_nid_postfix(const Shader* shader, std::span context, std::span primitive) { +extern "C" void AgcDriverResolveShaderAbi_nid_postfix(const Shader* shader, std::span context, std::span primitive) try { AgcDriver::DriverDetail::Driver::Get().ResolveShaderAbi(shader, context, primitive); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -extern "C" void AgcDriverResolveGraphicsAbi_nid_postfix(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType) { +extern "C" void AgcDriverResolveGraphicsAbi_nid_postfix(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType) try { AgcDriver::DriverDetail::Driver::Get().ResolveGraphicsAbi(vertex, pixel, primitiveType); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -extern "C" void AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span stages, std::span context, std::span primitive) { +extern "C" void AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span stages, std::span context, std::span primitive) try { AgcDriver::DriverDetail::Driver::Get().ResolveGraphicsStagesAbi(stages, context, primitive); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp index ebe7891130..71c479e41e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp @@ -1,18 +1,138 @@ +#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 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#include +#endif namespace AgcDriver::DriverDetail { +namespace { + +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; + ULONG_PTR high = 0; + GetCurrentThreadStackLimits(&low, &high); +#else + struct Bounds { + std::uintptr_t low = 0; + std::uintptr_t high = 0; + Bounds() { + pthread_attr_t attributes; + if (pthread_getattr_np(pthread_self(), &attributes) != 0) return; + void* base = nullptr; + std::size_t size = 0; + if (pthread_attr_getstack(&attributes, &base, &size) == 0 && base != nullptr) { + low = reinterpret_cast(base); + high = low + size; + } + pthread_attr_destroy(&attributes); + } + }; + thread_local const Bounds bounds; + const auto low = bounds.low; + const auto high = bounds.high; +#endif + const auto frame = reinterpret_cast(__builtin_frame_address(0)); + return frame >= low && frame < high && frame - low >= InlineImportStackBytes; +} + +} + +std::uint64_t Driver::GpuMapImportThreads() { + return gpuMapImportThreads.load(std::memory_order_relaxed); +} + Driver& Driver::Get() { static Driver driver; return driver; } Driver::~Driver() { + GuestAllocations::GuestAllocationsSetGpuMapObserver_nid_postfix(nullptr); stop(); } @@ -30,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(); @@ -40,16 +161,33 @@ 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 { LibcRegisterShutdown_nid_postfix([] { Driver::Get().Shutdown(); }); @@ -57,6 +195,31 @@ Driver::Driver() { stop(); throw; } + GuestAllocations::GuestAllocationsSetGpuMapObserver_nid_postfix([](const GuestAllocations::Mapped& ranges, std::uint64_t generation) { + const auto import = [&] { + try { + const auto current = Driver::Get().device.Load(); + if (current == nullptr || current->ImportGuestMemory(ranges, generation, false)) return; + std::lock_guard lock(GuestMemory::GpuMutex()); + if (Driver::Get().device.Load() == current) current->ImportGuestMemory(ranges, generation, true); + } catch (const std::exception& error) { + std::fprintf(stderr, "[gpu] import of GPU memory at its mapping failed: %s\n", error.what()); + } + }; + if (GuestMemory::GpuMutex().HeldByThisThread() || StackHoldsImport()) { + import(); + return; + } + std::thread host; + try { + host = std::thread(import); + } catch (const std::system_error& error) { + std::fprintf(stderr, "[gpu] import of GPU memory at its mapping failed: no host thread to run it on: %s\n", error.what()); + return; + } + gpuMapImportThreads.fetch_add(1, std::memory_order_relaxed); + host.join(); + }); } void Driver::WaitIdle() { @@ -80,9 +243,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) { @@ -93,6 +258,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(); } @@ -113,3 +279,15 @@ bool& Driver::onWorkerThread() { } } + +namespace AgcDriver { + +void FlushAsyncLog_nid_no_patch() { + AsyncLog::Get().Flush(); +} + +std::unique_lock HoldAsyncLog() { + return AsyncLog::Get().Hold(); +} + +} 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 fb3a799ed1..e410cb88d3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp @@ -2,20 +2,57 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferFill.hpp" #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 namespace AgcDriver::DriverDetail { +namespace { + +bool matchesBufferPatternFillKernel(std::span code) { + static constexpr std::array kernel{0xd7460004u, 0x04010c08u, 0xf4280302u, 0xfa000000u, 0xbf8cc07fu, 0x7e00020cu, 0x7e02020du, 0x7e04020eu, 0x7e06020fu, 0xe01c2000u, 0x80000004u, 0xbf810000u}; + return code.size() >= kernel.size() && std::equal(kernel.begin(), kernel.end(), code.begin()); +} + +bool 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) { static const bool enabled = std::getenv("APS5_NO_FILL_HLE") == nullptr; static const bool patternEnabled = std::getenv("APS5_NO_PATTERN_FILL_HLE") == nullptr; if (!enabled || userData.size() < 8 || compute.numThreads[0] != 64 || compute.numThreads[1] != 1 || compute.numThreads[2] != 1) return false; if (patternEnabled && MatchesPatternFillKernel(code, userData, compute)) return true; static constexpr std::array fillKernel{0xd7460004u, 0x04010c08u, 0x7e000204u, 0x7e020205u, 0x7e040206u, 0x7e060207u, 0xe01c2000u, 0x80000004u, 0xbf810000u}; - if (code.size() < fillKernel.size() || !std::equal(fillKernel.begin(), fillKernel.end(), code.begin())) return false; + if (!matchesBufferPatternFillKernel(code) && (code.size() < fillKernel.size() || !std::equal(fillKernel.begin(), fillKernel.end(), code.begin()))) return false; const auto stride = (userData[1] >> 16u) & 0x3fffu; const bool swizzled = ((userData[1] >> 31u) & 1u) != 0; @@ -26,6 +63,114 @@ bool Driver::matchesFillKernel(std::span code, const std::v return type == 0 && stride == 16 && !swizzled && !addTid && dstSel == 0xfacu && format == 0x4bu; } +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; + 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; + const auto type = userData[3] >> 30u; + const auto format = (userData[3] >> 12u) & 0x7fu; + const auto period = userData[9]; + if (type != 0 || stride != 4 || swizzled || addTid || format < 20u || format > 22u || (period != 1u && period != 2u && period != 4u) || (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]}); + const auto base = userData[0] | (static_cast(userData[1] & 0xffffu) << 32u); + if (dwords == 0 || dwords % 4u != 0 || base % 16u != 0) return std::nullopt; + DwordPatternFill fill{base, static_cast(dwords * 4u), {}}; + for (std::uint32_t i = 0; i < 4; ++i) fill.pattern[i] = userData[4 + i % period]; + return fill; +} + +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, 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; + const auto outOfBounds = (userData[3] >> 28u) & 3u; + const auto format = (userData[3] >> 12u) & 0x7fu; + const auto type = userData[3] >> 30u; + if (type != 0 || stride != 4 || swizzled || addTid || outOfBounds != 0 || format < 20u || format > 22u || (packet[4] & 0x20u) != 0 || packet[2] != 1 || packet[3] != 1) return std::nullopt; + const auto dwords = std::min(static_cast(packet[1]) * 64u, userData[2]); + const auto base = userData[0] | (static_cast(userData[1] & 0xffffu) << 32u); + if (dwords == 0 || dwords % 4u != 0 || base % 16u != 0) return std::nullopt; + 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) { + static const bool enabled = std::getenv("APS5_NO_FILL_HLE") == nullptr; + if (!enabled || userData.size() != 8 || packet.size() < 5 || (packet[4] & 0x20u) != 0 || !compute.groupIdEnable[0]) return std::nullopt; + const auto fill = Graphics::MatchMemoryValueFill(code, userData, compute.numThreads, {packet[1], packet[2], packet[3]}); + if (!fill) return std::nullopt; + if (fill->address % 16u != 0 || fill->bytes % 16u != 0) return std::nullopt; + return fill; +} + +bool IsByteFill(const std::array& pattern) { + return std::all_of(pattern.begin(), pattern.end(), [&](std::uint32_t word) { return word == (pattern[0] & 0xffu) * 0x01010101u; }); +} + +std::optional KeysFillMemo(const std::array& pattern, bool coversStorageKeys, bool memoryValueFill) { + if (!IsByteFill(pattern) || !(coversStorageKeys || memoryValueFill)) return std::nullopt; + switch (pattern[0] & 0xffu) { + case 0x00: return Graphics::DccKeys::Clear0000; + case 0x40: return Graphics::DccKeys::Clear0001; + case 0x80: return Graphics::DccKeys::Clear1110; + case 0xc0: return Graphics::DccKeys::Clear1111; + case 0x20: return Graphics::DccKeys::ClearRegister; + case 0xff: return Graphics::DccKeys::Uncompressed; + default: return std::nullopt; + } +} + +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; @@ -90,16 +235,50 @@ 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) { - if (!matchesFillKernel(code, userData, compute)) return false; + auto dwordFill = MatchDwordPatternFill(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; std::uint64_t base; std::size_t bytes; std::array pattern; std::unique_lock inputLock(GuestMemory::GpuMutex(), std::defer_lock); - if (MatchesPatternFillKernel(code, userData, compute)) { + if (dwordFill) { + base = dwordFill->base; + bytes = dwordFill->bytes; + pattern = dwordFill->pattern; + } else if (memoryFill) { + drainPendingInputWrites(queueId, {{memoryFill->valueAddress, 4}}); + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Fill); + inputLock.lock(); + recordLabelsForPacket(localDevice.get(), queueId); + if (!GuestMemory::Accessible(reinterpret_cast(memoryFill->valueAddress), 4)) return false; + GuestMemory::Read(memoryFill->valueAddress, std::as_writable_bytes(std::span(pattern).first(1)), 4); + std::fill(pattern.begin() + 1, pattern.end(), pattern[0]); + base = memoryFill->address; + bytes = memoryFill->bytes; + } else if (MatchesPatternFillKernel(code, userData, compute)) { const auto range = DecodePatternFillRange(userData, packet); if (!range) return false; if (range->invocations == 0) return true; + drainPendingInputWrites(queueId, {{range->control, 8}, {range->source, 4}}); GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Fill); inputLock.lock(); recordLabelsForPacket(localDevice.get(), queueId); @@ -127,8 +306,25 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span(static_cast(groups[0]) * 64u, numRecords); base = userData[0] | (static_cast(userData[1] & 0xffffu) << 32u); bytes = static_cast(records * 16u); - pattern = {userData[4], userData[5], userData[6], userData[7]}; + if (matchesBufferPatternFillKernel(code)) { + const auto source = userData[4] | (static_cast(userData[5] & 0xffffu) << 32u); + drainPendingInputWrites(queueId, {{source, sizeof(pattern)}}); + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Fill); + inputLock.lock(); + recordLabelsForPacket(localDevice.get(), queueId); + if (!GuestMemory::Accessible(reinterpret_cast(source), sizeof(pattern))) return false; + GuestMemory::Read(source, std::as_writable_bytes(std::span(pattern)), 4); + } else { + pattern = {userData[4], userData[5], userData[6], userData[7]}; + } } + if (queueId == 0 && DrawPipeline::Active() && inputLock.owns_lock()) inputLock.unlock(); + storeBufferFill(queueId, base, bytes, pattern, localDevice, memoryFill.has_value(), true); + return true; +} + + +void Driver::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) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; static std::atomic fills{0}, filledBytes{0}, cpuFills{0}; @@ -139,11 +335,12 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span fillPhaseMs{}; static std::array holdByCoverMs{}; static std::array holdByCover{}; - static std::uint64_t flushes = 0, flushed = 0; + static std::uint64_t flushes = 0, flushed = 0, absorbedFills = 0, absorbedBytes = 0; static auto fillReport = std::chrono::steady_clock::now(); ++fills; filledBytes += bytes; - if (bytes != 0) { + if (bytes == 0) return; + const auto store = [this, queueId, base, bytes, pattern, localDevice, memoryValueFill, noteMetadata] { GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Fill); std::lock_guard gpuLock(GuestMemory::GpuMutex()); const auto holdStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; @@ -155,103 +352,111 @@ 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 && std::all_of(pattern.begin(), pattern.end(), [&](std::uint32_t word) { return word == (pattern[0] & 0xffu) * 0x01010101u; }); - 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); - - if (!cleared) { - ++flushes; - - if (Graphics::StorageTexture::FlushPending(base, bytes, nullptr, "buffer fill", Graphics::PublishScope::PartialUnits)) ++flushed; - } - phase(FillFlush); - const bool stored = cleared || localDevice->FillBuffer(base, bytes, pattern); - phase(FillDevice); - if (uniformKeysFill && stored && !cleared) { - Graphics::DccKeys filled = Graphics::DccKeys::Mixed; - switch (pattern[0] & 0xffu) { - case 0x00: filled = Graphics::DccKeys::Clear0000; break; - case 0x40: filled = Graphics::DccKeys::Clear0001; break; - case 0x80: filled = Graphics::DccKeys::Clear1110; break; - case 0xc0: filled = Graphics::DccKeys::Clear1111; break; - case 0x20: filled = Graphics::DccKeys::ClearRegister; break; - case 0xff: filled = Graphics::DccKeys::Uncompressed; break; - default: break; - } - if (filled != Graphics::DccKeys::Mixed) Graphics::NoteKeysFillOnGpu(base, bytes, filled); - } - if (profile && stored && !cleared) { - const bool uniform = pattern[0] == pattern[1] && pattern[1] == pattern[2] && pattern[2] == pattern[3]; - ++(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 (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; - } - 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 b9b65463ff..8b0687ac4f 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,17 @@ 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); }); +} + void Driver::noteWrittenBuffers(std::uint64_t program, std::uint32_t queue, const ShaderRecompiler::RecompileResult& compiled) { std::lock_guard lock(writtenBuffersMutex); const auto serial = ++writtenBufferSerial; @@ -29,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); @@ -42,6 +92,108 @@ 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) || Graphics::VertexFetchOutOfRange(attribute)) continue; + const auto address = attribute.resource.fields[0] | (static_cast(attribute.resource.fields[1] & 0xffffu) << 32u); + std::uint64_t bytes = 0; + try { + 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 3a2b722698..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,10 +118,36 @@ 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) { auto* submissionTiming = includeTimingSubmission(submission, true); + struct FlipHold { + Driver& driver; + bool held; + void Release() { + if (!held) return; + held = false; + driver.releaseFlipHold(); + } + ~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"); @@ -44,8 +161,9 @@ void Driver::execute(const Submission& submission) { const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush); std::lock_guard gpuLock(GuestMemory::GpuMutex()); - if (const auto localDevice = device.Load()) { - recordDeferredLabels(localDevice.get(), submission.queue); + const auto localDevice = device.Load(); + recordDeferredLabels(localDevice.get(), submission.queue); + if (localDevice != nullptr) { if (suspendDrain) localDevice->WaitIdle(); else localDevice->SubmitRecorded(submission.queue == 0); } @@ -96,8 +214,18 @@ 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 { + ~ClearAhead() { clearResolvedAhead(); } + } clearAhead; for (std::size_t cursor = 0; cursor < submission.commands.size();) { if (packetEpoch()) bumpEpoch(&EpochBumps::packets); CheckFailure(); @@ -110,13 +238,43 @@ 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)) { cursor = opcode == 0x3f ? submission.conditionalEnds.at(cursor) : nextCursor; @@ -185,9 +343,18 @@ 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; if (!drains) { GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush); @@ -195,35 +362,51 @@ void Driver::execute(const Submission& submission) { const auto localDevice = device.Load(); if (!recordLabelsForPacket(localDevice.get(), submission.queue) && localDevice != nullptr) localDevice->SubmitRecorded(submission.queue == 0); - if (localDevice != nullptr) localDevice->FlipBatches(batchesAtFlip, unsignaledAtFlip); + if (localDevice != nullptr) { + localDevice->FlipBatches(batchesAtFlip, unsignaledAtFlip); + const auto queueId = submission.queue; + flipInterruptDeferred = localDevice->AfterRecordedWork([this, queueId] { deliverFlipEopInterrupt(queueId); }, false); + } } + 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) { - timed(&WorkerProfile::dispatchMs, [&] { dispatch(queue, packet, submission); }); + } else if (opcode == 0x15 || opcode == 0x16) { + packetCounters.CountDispatch(); + landPendingWaits(submission.queue); + currentPacketOffset() = cursor; + if (gateOpened()) { + gateOpened() = false; + 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(false); - } else if (opcode == 0x16) { - timed(&WorkerProfile::dispatchMs, [&] { dispatchIndirect(queue, packet, submission); }); - Graphics::Recorder::CountRecordedWork(); - finishDispatchPacket(true); + finishDispatchPacket(opcode == 0x16); } else if (opcode == 0x3c || opcode == 0x93) { static const bool traceGpu = std::getenv("APS5_TRACE_GPU") != nullptr; const auto waitStart = std::chrono::steady_clock::now(); timed(&WorkerProfile::waitMs, [&] { waitMemory(packet, submission.queue, recent, submission.received, submission.heldAtSubmit.contains(cursor)); }); + gateOpened() = true; if (traceGpu && std::chrono::steady_clock::now() - waitStart > std::chrono::milliseconds(200)) { for (std::size_t next = cursor + count, shown = 0; next < submission.commands.size() && shown < 48; ++shown) { @@ -237,6 +420,9 @@ 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; @@ -260,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); @@ -268,21 +458,59 @@ void Driver::execute(const Submission& submission) { }); finishDrawPacket(drawn); } else if (opcode == 0x22) { + landPendingWaits(submission.queue); recordQueuedLabelsBeforeRead(submission.queue); const auto condition = Pm4::ReadCondition(packet); if (condition == 0) nextCursor = submission.conditionalEnds.at(cursor); if (traceGpu) std::fprintf(stderr, "[gpu] %.1f queue 0x%x COND_EXEC at DWORD %zu reads 0x%x at 0x%llx: %s %zu dwords\n", TraceMs(), submission.queue, cursor, condition, static_cast(packet[1] | (static_cast(packet[2]) << 32u)), condition == 0 ? "skips" : "executes", submission.conditionalEnds.at(cursor) - cursor - count); + } else if (opcode == 0x91) { + landPendingWaits(submission.queue); + recordQueuedLabelsBeforeRead(submission.queue); + Pm4::Execute(packet, queue); } else if (sampleDump && !wroteOnGpu) { - dumpSampleCounters(packet[2] | (static_cast(packet[3]) << 32u)); + 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); + 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"); @@ -294,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 21da6d27f5..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,10 +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/Synchronization/SynchronizationStatistics.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/Eq/include/Event.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#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 @@ -37,8 +45,8 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue const auto stamp = ++eventSerial; reason = localDevice != nullptr ? localDevice->WriteLabelOnGpu(label->address, bytes, stamp, submission.queue) : 4; if (reason == 1) GuestMemory::Write(label->address, bytes, 4); - if (reason == 0 || reason == 1 || reason == 5 || reason == 6) { - noteLabelStore(label->address, bytes, stamp); + if (reason == 0 || reason == 1 || reason == 5) { + noteLabelStore(label->address, bytes, stamp, submission.queue); Graphics::Recorder::CloseLabelGroup(GuestMemory::TrackerGeneration()); } countLabelOutcome(reason); @@ -46,9 +54,9 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue } else { ++noOpLabels; } - if (reason == 0 || reason == 5 || reason == 6) { + 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) { @@ -56,6 +64,14 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue } } } + if (opcode == 0x50 && packet.size() >= 7) { + if (Pm4::DmaSource(packet) == 0 || Pm4::DmaSource(packet) == 3) Graphics::RequireNoMultisampleTarget(packet[2] | (static_cast(packet[3]) << 32u), packet[6] & 0x3ffffffu, "a DMA copy"); + if (Pm4::DmaSource(packet) == 2 && Pm4::DmaDestination(packet) != 1) { + const auto address = packet[4] | (static_cast(packet[5]) << 32u); + Graphics::NoteDepthMetadataFill(address, packet[6] & 0x3ffffffu, packet[2]); + Graphics::NoteColorMetadataFill(address, packet[6] & 0x3ffffffu, packet[2]); + } + } if (!drainAll && !endOfPipeInterrupt && (opcode == 0x49 || opcode == 0x37)) { if (const auto label = Pm4::DecodeLabelWrite(packet)) { const auto bytes = label->Bytes(); @@ -77,13 +93,13 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue recordDeferredLabels(localDevice.get(), submission.queue); const auto stamp = ++eventSerial; const auto reason = localDevice != nullptr ? localDevice->WriteLabelOnGpu(label->address, bytes, stamp, submission.queue) : 4; - wroteOnGpu = reason == 0 || reason == 5 || reason == 6; + wroteOnGpu = reason == 0 || reason == 5; if (reason == 1) { GuestMemory::Write(label->address, bytes, 4); wroteOnGpu = true; } - if (wroteOnGpu) noteLabelStore(label->address, bytes, stamp); + if (wroteOnGpu) noteLabelStore(label->address, bytes, stamp, submission.queue); Graphics::Recorder::CloseLabelGroup(GuestMemory::TrackerGeneration()); countLabelOutcome(reason); ++immediateLabels; @@ -100,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()) { @@ -117,8 +155,8 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue recordDeferredLabels(localDevice.get(), submission.queue); const auto stamp = ++eventSerial; const auto reason = localDevice != nullptr ? localDevice->WriteLabelOnGpu(store->address, bytes, stamp, submission.queue) : 4; - if (reason == 0 || reason == 1 || reason == 5 || reason == 6) noteLabelStore(store->address, bytes, stamp); - if (reason == 0 || reason == 5 || reason == 6) { + if (reason == 0 || reason == 1 || reason == 5) noteLabelStore(store->address, bytes, stamp, submission.queue); + if (reason == 0 || reason == 5) { if (reason == 0) ++storesOnGpu; else ++storesBehindCompletions; wroteOnGpu = true; @@ -133,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()); @@ -143,25 +180,49 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue if (outcome.path == 1 || outcome.path == 3) { wroteOnGpu = true; drained = false; + } else if (outcome.path == 2 && !Graphics::RegisteredReadableCovers(copy->destination, copy->bytes) && !localDevice->StoresPendingOver(copy->destination, copy->bytes)) { + Graphics::StorageTexture::FlushPending(copy->source, copy->bytes, nullptr, "buffer copy source", Graphics::PublishScope::Whole); + if (auto* recorder = Graphics::Recorder::Active()) recorder->SyncThrough(copy->source, copy->bytes); + ++copiesToHostMemory; + orderedAlready = true; + drained = false; } } } } + 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))); @@ -223,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 e1c00238f5..d071d049fc 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp @@ -4,10 +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); @@ -20,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); } @@ -37,7 +55,7 @@ void Driver::AttachWindow(const PresentationWindow& window) { } void Driver::Present(const PresentationWindow& window, const DisplayBuffer* buffer, bool opaque, void (*gpuReady)(void*), void* context) { - + if (PerformanceTrace::Configured()) PerformanceTrace::Present(window.width, window.height, device.Load().get()); PerformanceContext timingContext(window.timing.get()); PerformanceTimer timing("Driver.Present"); static thread_local bool pinned = false; @@ -71,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"); @@ -127,6 +146,7 @@ void Driver::Present(const PresentationWindow& window, const DisplayBuffer* buff } gpuReady(context); timing.Mark("release_and_callback"); + TracyZone::MarkFrame(); if (profile) reportPresents(waitedMs, inFlight); CheckFailure(); } catch (const ProcessShutdown&) { @@ -152,10 +172,12 @@ void Driver::reportPresents(double waitedMs, std::size_t inFlight) { const auto counts = VulkanDevice::PresentCounts(); const auto presents = counts.presents - previous.presents; const double perPresent = presents != 0 ? 1.0 / static_cast(presents) : 0.0; + const auto dropped = counts.acquireDrops - previous.acquireDrops; + const double perFrame = presents + dropped != 0 ? 1.0 / static_cast(presents + dropped) : 0.0; const auto flips = flipsCounted.load() - previousFlips; const double perFlip = flips != 0 ? 1.0 / static_cast(flips) : 0.0; const auto serial = flipSerial.load(); - AgcDriver::ProfilePrint_nid_no_patch( "[present] %llu presents over 10 s (%zu may trail on the GPU); %s p50 %.2f ms, p90 %.2f ms, max %.2f ms over %zu; per present: GPU busy %.1f ms, idle gaps %.1f ms (%llu batches without a completion record); per flip: %.1f batches recorded (%.1f unsignaled at the flip); per present: %.1f batches ahead of the blit, %.1f after it; after the flip packet: last submit %.1f ms, blit submit %.1f ms; in-place reads overwritten before execution: %llu of %llu checked\n", static_cast(presents), inFlight, inFlight != 0 ? "inflight_wait" : "render_fence_wait", percentile(0.5), percentile(0.9), waits.empty() ? 0.0 : waits.back(), waits.size(), (counts.gpuBusyMs - previous.gpuBusyMs) * perPresent, (counts.gpuGapMs - previous.gpuGapMs) * perPresent, static_cast(counts.gpuUnread - previous.gpuUnread), static_cast(serial - windowSerial) * perFlip, static_cast(flipBatchesUnsignaled.load() - previousUnsignaled) * perFlip, static_cast(counts.batchesAheadOfBlit - previous.batchesAheadOfBlit) * perPresent, static_cast(counts.batchesAfterFlip - previous.batchesAfterFlip) * perPresent, (counts.lastSubmitAfterFlipMs - previous.lastSubmitAfterFlipMs) * perPresent, (counts.blitSubmitAfterFlipMs - previous.blitSubmitAfterFlipMs) * perPresent, static_cast(counts.readsOverwritten - previous.readsOverwritten), static_cast(counts.readsChecked - previous.readsChecked)); + AgcDriver::ProfilePrint_nid_no_patch( "[present] %llu presents over 10 s (%llu dropped: no free swapchain image; %zu may trail on the GPU); %s p50 %.2f ms, p90 %.2f ms, max %.2f ms over %zu; per frame, presented or dropped: GPU busy %.1f ms, idle gaps %.1f ms (%llu batches without a completion record); per flip: %.1f batches recorded (%.1f unsignaled at the flip); per present: %.1f batches ahead of the blit, %.1f after it; after the flip packet: last submit %.1f ms, blit submit %.1f ms; in-place reads overwritten before execution: %llu of %llu checked\n", static_cast(presents), static_cast(dropped), inFlight, inFlight != 0 ? "inflight_wait" : "render_fence_wait", percentile(0.5), percentile(0.9), waits.empty() ? 0.0 : waits.back(), waits.size(), (counts.gpuBusyMs - previous.gpuBusyMs) * perFrame, (counts.gpuGapMs - previous.gpuGapMs) * perFrame, static_cast(counts.gpuUnread - previous.gpuUnread), static_cast(serial - windowSerial) * perFlip, static_cast(flipBatchesUnsignaled.load() - previousUnsignaled) * perFlip, static_cast(counts.batchesAheadOfBlit - previous.batchesAheadOfBlit) * perPresent, static_cast(counts.batchesAfterFlip - previous.batchesAfterFlip) * perPresent, (counts.lastSubmitAfterFlipMs - previous.lastSubmitAfterFlipMs) * perPresent, (counts.blitSubmitAfterFlipMs - previous.blitSubmitAfterFlipMs) * perPresent, static_cast(counts.readsOverwritten - previous.readsOverwritten), static_cast(counts.readsChecked - previous.readsChecked)); previous = counts; previousFlips = flipsCounted.load(); previousUnsignaled = flipBatchesUnsignaled.load(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp index 9d9232a0a9..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,15 +1,26 @@ #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" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" +#include +#include #include namespace AgcDriver::DriverDetail { +namespace { + +constexpr auto IdleResultsQuietPeriod = std::chrono::milliseconds(50); + +} + FrameTiming* Driver::frameTiming() { if (!APS5_ENABLE_TIMING_LOG) return nullptr; if (pendingFrameTiming == nullptr) pendingFrameTiming = std::make_shared(++frameSerial, true); @@ -46,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; @@ -66,11 +132,9 @@ void Driver::reapCompletionLabels() { void Driver::run(std::uint32_t id) noexcept { onWorkerThread() = true; - { - char role[32]; - std::snprintf(role, sizeof role, "queue worker 0x%x", id); - PinWorkerThread(role); - } + char role[32]; + std::snprintf(role, sizeof role, "queue worker 0x%x", id); + PinWorkerThread(role); GuestMemory::TagGpuLockThread(id); if (id == 0) StartWorkerSampler(); @@ -78,8 +142,27 @@ void Driver::run(std::uint32_t id) noexcept { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; auto& costs = submissionCosts(id); try { + if (PerformanceTrace::Configured()) PerformanceTrace::BindThread(role); for (;;) { submission = Submission{}; + struct Running { + std::atomic& count; + std::atomic& idleEpoch; + std::condition_variable& changed; + bool active = false; + Running(std::atomic& count, std::atomic& idleEpoch, std::condition_variable& changed) + : count(count), idleEpoch(idleEpoch), changed(changed) {} + void Start() { + count.fetch_add(1, std::memory_order_acq_rel); + active = true; + } + ~Running() { + if (active && count.fetch_sub(1, std::memory_order_acq_rel) == 1) { + idleEpoch.fetch_add(1, std::memory_order_release); + changed.notify_all(); + } + } + } running{runningWorkers, idleEpoch, changed}; static const bool traceGpu = std::getenv("APS5_TRACE_GPU") != nullptr; { PerformanceContext timingContext(id == 0 ? frameTiming() : FrameTiming::Async()); @@ -91,6 +174,13 @@ void Driver::run(std::uint32_t id) noexcept { workerQueued() = &worker.queued; if (traceGpu && pending.empty()) std::fprintf(stderr, "[gpu] %.1f idle queue=0x%x\n", TraceMs(), id); const auto ready = [&] { return failure || stopping || !pending.empty(); }; + const auto allQueuesIdle = [&] { + return runningWorkers.load(std::memory_order_acquire) == 0 + && std::all_of(workers.begin(), workers.end(), [](const auto& item) { return item.second.pending.empty(); }); + }; + const auto idleResultsPending = [&] { + return id == 0 && Graphics::StorageTexture::AnyPending() && allQueuesIdle() && !completionsPending(); + }; const auto poll = [&](const auto& done) { #ifdef _WIN32 lock.unlock(); @@ -104,9 +194,35 @@ void Driver::run(std::uint32_t id) noexcept { while (!ready()) { if (!completionsPending() || (id != 0 && Graphics::Recorder::PendingCompletionLabels() == 0)) { + const auto idleEpochAtCheck = idleEpoch.load(std::memory_order_acquire); + const bool storeIdleResults = idleResultsPending(); + if (storeIdleResults) { + queue0Dormant.store(true, std::memory_order_relaxed); + const auto quietUntil = std::chrono::steady_clock::now() + IdleResultsQuietPeriod; + const auto idleEpochAtWait = idleEpoch.load(std::memory_order_acquire); + changed.wait_until(lock, quietUntil, [&] { + return ready() || idleEpoch.load(std::memory_order_acquire) != idleEpochAtWait; + }); + queue0Dormant.store(false, std::memory_order_relaxed); + if (ready()) break; + if (idleEpoch.load(std::memory_order_acquire) != idleEpochAtWait) { + continue; + } + if (!idleResultsPending()) continue; + } + lock.unlock(); + if (storeIdleResults) { + if (const auto localDevice = device.Load()) localDevice->StoreIdleResults(); + } + lock.lock(); + if (ready()) break; + if (id == 0 && allQueuesIdle() && !completionsPending() + && ((!storeIdleResults && Graphics::StorageTexture::AnyPending()) || idleEpoch.load(std::memory_order_acquire) != idleEpochAtCheck)) continue; if (id == 0) queue0Dormant.store(true, std::memory_order_relaxed); - changed.wait(lock, ready); + const auto idleEpochAtWait = idleEpoch.load(std::memory_order_acquire); + 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 || completionsPending())) continue; break; } if (poll(ready)) break; @@ -124,25 +240,27 @@ 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(); timing.Mark("dequeue_order"); worker.queued.fetch_sub(1, std::memory_order_acq_rel); + running.Start(); if (profile && submission.enqueuedAt != std::chrono::steady_clock::time_point{}) costs.dequeueNs += static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - submission.enqueuedAt).count()); } - { - struct Running { - std::atomic& count; - explicit Running(std::atomic& count) : count(count) { count.fetch_add(1, std::memory_order_acq_rel); } - ~Running() { count.fetch_sub(1, std::memory_order_acq_rel); } - } running{runningWorkers}; - execute(submission); - } + execute(submission); if (traceGpu) std::fprintf(stderr, "[gpu] %.1f done serial=%llu queue=0x%x\n", TraceMs(), static_cast(submission.serial), id); const auto completeStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; PerformanceContext completionContext(id == 0 ? frameTiming() : FrameTiming::Async()); @@ -151,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; @@ -165,8 +285,8 @@ void Driver::run(std::uint32_t id) noexcept { submission = Submission{}; } catch (...) { const auto error = std::current_exception(); - ReportFailure(error); for (const auto& [offset, flip] : submission.flips) flip->Fail(error); + ReportFailure(error); } } 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 3df687bf65..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,15 +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(); @@ -131,7 +193,45 @@ void Driver::waitForFlipRoom(const Submission& submission) { } } +bool Driver::awaitsTitle(std::uint64_t awaited) const { + return runningWorkers.load(std::memory_order_acquire) == 0 && orderHolders.load(std::memory_order_acquire) == 0 && !completionsPending() && !Graphics::Recorder::SnapshotWriteOverlaps(awaited, 4); +} + +void Driver::noteAwaitingTitle(std::uint32_t queue, std::uint64_t awaited) { + if (queue != 0 || flipHolders.load(std::memory_order_acquire) == 0 || queue0AwaitsTitle.load(std::memory_order_acquire) || !awaitsTitle(awaited)) return; + std::lock_guard lock(mutex); + queue0AwaitsTitle.store(true, std::memory_order_release); + changed.notify_all(); +} + +void Driver::holdFlipBehindWorker(const Submission& submission) { + if (submission.queue != 0 || onWorkerThread()) 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); +} + +void Driver::releaseFlipHold() { + std::lock_guard lock(mutex); + flipsAhead.fetch_sub(1, std::memory_order_acq_rel); + if (flipHolders.load(std::memory_order_acquire) != 0) changed.notify_all(); +} + 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) { @@ -141,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]); @@ -152,27 +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); @@ -180,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(); @@ -188,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"); @@ -218,37 +358,62 @@ void Driver::Submit(const Packet* packet, std::uint32_t queue) { readRegisterLists(submission); 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()); } if (APS5_ENABLE_TIMING_LOG) submission.enqueuedAt = std::chrono::steady_clock::now(); + if (queue == 0 && !submission.flips.empty()) { + submission.holdsFlip = true; + flipsAhead.fetch_add(1, std::memory_order_acq_rel); + } enqueue(std::move(submission)); ++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(); @@ -260,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) { @@ -278,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) { @@ -296,9 +466,14 @@ bool Driver::orderReleased(std::uint32_t queue, std::uint64_t received) const { } void Driver::noteWaitBlocked(std::uint32_t queue, std::uint64_t awaited, bool blocked) { - if (blocked) runningWorkers.fetch_sub(1, std::memory_order_acq_rel); - else runningWorkers.fetch_add(1, std::memory_order_acq_rel); + if (blocked) { + if (runningWorkers.fetch_sub(1, std::memory_order_acq_rel) == 1) { + idleEpoch.fetch_add(1, std::memory_order_release); + changed.notify_all(); + } + } else runningWorkers.fetch_add(1, std::memory_order_acq_rel); if (queue == 0) queue0Awaited.store(blocked ? awaited : 0, std::memory_order_release); + if (queue == 0 && !blocked) queue0AwaitsTitle.store(false, std::memory_order_release); if (blocked && orderHolders.load(std::memory_order_acquire) != 0) { std::lock_guard lock(mutex); changed.notify_all(); @@ -312,7 +487,17 @@ void Driver::enqueue(Submission submission) { for (const auto dword : submission.labelWrites) ++worker.unfinishedWrites[dword]; worker.pending.push_back(std::move(submission)); worker.queued.fetch_add(1, std::memory_order_acq_rel); - if (!worker.thread.joinable()) worker.thread = std::thread([this, queue] { run(queue); }); + if (!worker.thread.joinable()) worker.thread = std::thread([this, queue] { + char role[32]; + std::snprintf(role, sizeof(role), "queue 0x%x worker", queue); + try { + RaiseWorkerThreadPriority(role); + } catch (...) { + ReportFailure(std::current_exception()); + return; + } + run(queue); + }); } void Driver::noteHeldAtSubmit(Submission& submission, std::size_t cursor) { @@ -329,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) { @@ -344,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 d9d04c893a..792b920c92 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/WorkerAffinity.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/WorkerAffinity.cpp @@ -1,6 +1,8 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Queues/WorkerAffinity.hpp" +#include "prx/libSceAgcDriver/Execution/include/TracyZone.hpp" #include "prx/libc/include/CpuTopology.hpp" +#include "prx/libc/include/general/ThreadName.hpp" #include namespace AgcDriver::DriverDetail { @@ -20,6 +22,11 @@ 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 109b381a7f..b69bccd8fe 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -1,8 +1,8 @@ #include "prx/libc/include/general/LogMacros.hpp" #include "ControlFlow/GraphBuilder.hpp" -#include "ControlFlow/Structurizer.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" @@ -10,16 +10,78 @@ #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 { -std::shared_ptr ReadRawComputeShader(std::uint64_t address) { +std::shared_ptr PrepareShaderWithDiagnostics(const ShaderRecompiler::RecompileRequest& request, ShaderRecompiler::ShaderPreparationContext* preparation) { + std::exception_ptr failure; + std::string dump; + try { + auto handle = ShaderRecompiler::PrepareShader(request, preparation); + return handle; + } catch (...) { + failure = std::current_exception(); + try { + if (std::getenv("APS5_DUMP_SHADERS") != nullptr) dump = Driver::dumpRequest(request.shader.codeAddress, request, ".prepare"); + } catch (...) {} + } + try { + std::rethrow_exception(failure); + } catch (const std::exception& error) { + std::ostringstream where; + where << "shader 0x" << std::hex << request.shader.codeAddress << " stage " << static_cast(request.shader.stage); + if (!dump.empty()) where << " (" << dump << ")"; + where << ": "; + throw std::runtime_error(where.str() + error.what()); + } +} + +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; @@ -32,6 +94,7 @@ std::shared_ptr ReadRawComputeShader(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); @@ -52,10 +115,12 @@ std::shared_ptr ReadRawComputeShader(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 { const auto decoded = ShaderRecompiler::RdnaInstructionDecoder{}.Decode(snapshot.code); + if (const auto tail = ShaderRecompiler::FindUserDataTail(decoded, 0u, 106u)) snapshot.userDataTailRegister = tail->userDataRegister; const auto& last = decoded.instructions.back(); snapshot.code.resize(last.programCounter / sizeof(std::uint32_t) + last.wordCount); auto result = std::make_shared(std::move(snapshot)); @@ -82,20 +147,167 @@ std::shared_ptr ReadRawComputeShader(std::uint64_t address if (snapshot.code.size() == available) break; } } - throw std::runtime_error("AGC driver: raw compute program has no reachable end within mapped code or the size limit"); + throw std::runtime_error("AGC driver: raw shader program has no reachable end within mapped code or the size limit"); } +std::shared_ptr ReadUserDataTailContinuation(std::span code, std::uint32_t userDataBase, std::span userData) { + const auto program = ShaderRecompiler::RdnaInstructionDecoder{}.Decode(code); + const auto tail = ShaderRecompiler::FindUserDataTail(program, userDataBase, static_cast(userData.size())); + if (!tail) return nullptr; + const auto slot = tail->userDataRegister - userDataBase; + const auto 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; + return ReadRawShader(address); +} + +namespace { +std::atomic rawGraphicsDrawn{false}; +std::mutex rawGraphicsMutex; +std::map rawGraphicsRanges; +} + +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); +} + +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) { + if (prepared.pending) PrioritizeRegistrationPreparation(prepared); + prepared.settled.wait(lock, [&] { return !prepared.pending; }); + if (prepared.failure != nullptr) std::rethrow_exception(prepared.failure); +} + +bool CommittedAbi(const ShaderSnapshot& snapshot, std::span key) { + std::unique_lock lock(snapshot.prepared->mutex); + AwaitRegisteredPreparation(*snapshot.prepared, lock); + return std::ranges::any_of(snapshot.prepared->registeredAbis, [&](const auto& abi) { return std::ranges::equal(abi, key); }); +} + +} + +bool ProgramUsesWorkgroup(const ShaderSnapshot& snapshot, std::size_t codeOffset) { + constexpr std::uint8_t Unknown = 0, Unused = 1, Used = 2; + if (codeOffset == 0) { + const auto cached = snapshot.workgroupUse->load(std::memory_order_acquire); + if (cached != Unknown) return cached == Used; + } + const auto decoded = ShaderRecompiler::RdnaInstructionDecoder{}.Decode(std::span(snapshot.code).subspan(codeOffset)); + const bool used = std::any_of(decoded.instructions.begin(), decoded.instructions.end(), [](const auto& instruction) { + return instruction.family == ShaderRecompiler::RdnaInstructionFamily::DS || instruction.op == ShaderRecompiler::RdnaOpcode::SBarrier; + }); + if (codeOffset == 0) snapshot.workgroupUse->store(used ? Used : Unused, std::memory_order_release); + return used; +} + +std::uint32_t PositionExportLayout(const ShaderSnapshot* snapshot, std::size_t codeOffset, std::span code, std::uint32_t paClVsOutCntl, const ShaderSnapshot* continuation) { + constexpr std::uint8_t Unknown = 0, Unused = 1, Used = 2; + const auto bits = paClVsOutCntl & Graphics::PositionExportLayoutBits; + if (bits == 0) return 0; + if (continuation != nullptr && PositionExportLayout(continuation, 0, continuation->code, bits) != 0) return bits; + const bool cached = snapshot != nullptr && codeOffset == 0; + if (cached) { + const auto use = snapshot->positionVectorUse->load(std::memory_order_acquire); + if (use != Unknown) return use == Used ? bits : 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; +} + + +bool VertexWorkgroupRequired(const QueueState& queue, const ShaderRegistry& registry) { + Graphics::NoteRegisterRead(Graphics::RegisterBank::Context, 0x2d5); + const auto routing = queue.context.find(0x2d5); + if (routing == queue.context.end() || (routing->second & 0x02002024u) != 0x2000u) return false; + Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, 0xc8); + Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, 0xc9); + const auto low = queue.shader.find(0xc8); + const auto high = queue.shader.find(0xc9); + if (low == queue.shader.end() || high == queue.shader.end()) return false; + require((high->second & ~0xffu) == 0, "reserved graphics program address bits are set"); + const auto address = (static_cast(low->second) << 8u) | (static_cast(high->second) << 40u); + const auto snapshot = ProgramSnapshot(registry, address, {2}); + require((address - snapshot->codeAddress) % sizeof(std::uint32_t) == 0, "graphics entry point is not dword aligned"); + const auto offset = (address - snapshot->codeAddress) / sizeof(std::uint32_t); + if (!snapshot->header.empty()) return snapshot->type == 2 && ProgramUsesWorkgroup(*snapshot, offset); + if (!snapshot->userDataTailRegister) return ProgramUsesWorkgroup(*snapshot, offset); + Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, 0x8b); + const auto resources = queue.shader.find(0x8b); + const auto rsrc2 = resources == queue.shader.end() ? 0u : resources->second; + const auto count = ((rsrc2 >> 1u) & 0x1fu) | (((rsrc2 >> 27u) & 1u) << 5u); + require(count <= 32, "graphics user SGPR count exceeds the register bank"); + std::vector userData; + for (std::uint32_t index = 0; index < count; ++index) { + Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, 0x8c + index); + const auto found = queue.shader.find(0x8c + index); + userData.push_back(found == queue.shader.end() ? 0u : found->second); + } + const auto continuation = ReadUserDataTailContinuation(std::span(snapshot->code).subspan(offset), 8u, userData); + return ProgramUsesWorkgroup(*snapshot, offset) || (continuation != nullptr && ProgramUsesWorkgroup(*continuation, 0u)); +} struct ShaderPreparationTransaction::State { struct Change { std::shared_ptr destination; PreparedShaderState prepared; + bool edited = false; + std::size_t baseEntries = 0; + std::size_t baseAbis = 0; }; static std::mutex writers; static thread_local State* active; - std::unique_lock lock{writers}; + std::unique_lock lock{writers, std::defer_lock}; std::map changes; bool failed = false; + + void Hold() { + if (!lock.owns_lock()) lock.lock(); + } }; std::mutex ShaderPreparationTransaction::State::writers; @@ -116,17 +328,32 @@ ShaderPreparationTransaction::~ShaderPreparationTransaction() { PreparedShaderState& ShaderPreparationTransaction::Edit(const ShaderSnapshot& snapshot) { require(!committed, "shader preparation transaction is already committed"); + root->Hold(); auto& changes = root->changes; const auto found = changes.find(snapshot.prepared.get()); - if (found != changes.end()) return found->second.prepared; - std::lock_guard lock(snapshot.prepared->mutex); - return changes.emplace(snapshot.prepared.get(), State::Change{snapshot.prepared, *snapshot.prepared}).first->second.prepared; + if (found != changes.end()) { + found->second.edited = true; + return found->second.prepared; + } + std::unique_lock lock(snapshot.prepared->mutex); + AwaitRegisteredPreparation(*snapshot.prepared, lock); + return changes.emplace(snapshot.prepared.get(), State::Change{snapshot.prepared, *snapshot.prepared, true, snapshot.prepared->entries.size(), snapshot.prepared->registeredAbis.size()}).first->second.prepared; } const PreparedShaderState& ShaderPreparationTransaction::Read(const ShaderSnapshot& snapshot) const { require(!committed, "shader preparation transaction is already committed"); - const auto found = root->changes.find(snapshot.prepared.get()); - return found != root->changes.end() ? found->second.prepared : *snapshot.prepared; + root->Hold(); + auto& changes = root->changes; + const auto found = changes.find(snapshot.prepared.get()); + if (found != changes.end()) return found->second.prepared; + std::unique_lock lock(snapshot.prepared->mutex); + AwaitRegisteredPreparation(*snapshot.prepared, lock); + return changes.emplace(snapshot.prepared.get(), State::Change{snapshot.prepared, *snapshot.prepared, false, snapshot.prepared->entries.size(), snapshot.prepared->registeredAbis.size()}).first->second.prepared; +} + +void ShaderPreparationTransaction::Hold() { + require(!committed, "shader preparation transaction is already committed"); + root->Hold(); } void ShaderPreparationTransaction::Commit() { @@ -136,7 +363,16 @@ void ShaderPreparationTransaction::Commit() { locks.reserve(state->changes.size()); for (const auto& [key, change] : state->changes) locks.emplace_back(change.destination->mutex); for (auto& [key, change] : state->changes) { + if (!change.edited) continue; auto& destination = *change.destination; + for (auto index = std::min(change.baseEntries, destination.entries.size()); index < destination.entries.size(); ++index) { + const auto& entry = destination.entries[index]; + if (std::ranges::none_of(change.prepared.entries, [&](const auto& existing) { return existing.codeOffset == entry.codeOffset && existing.handle == entry.handle; })) change.prepared.entries.push_back(entry); + } + for (auto index = std::min(change.baseAbis, destination.registeredAbis.size()); index < destination.registeredAbis.size(); ++index) { + const auto& abi = destination.registeredAbis[index]; + if (std::ranges::find(change.prepared.registeredAbis, abi) == change.prepared.registeredAbis.end()) change.prepared.registeredAbis.push_back(abi); + } destination.entries.swap(change.prepared.entries); destination.registeredAbis.swap(change.prepared.registeredAbis); destination.graphicsAbis.swap(change.prepared.graphicsAbis); @@ -144,51 +380,259 @@ 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; } +bool FusedHullHeader(const ShaderSnapshot& snapshot, Shader used, const Shader& registered) { + if (used.type != 3u || registered.type != 7u) return false; + const auto* bound = used.sh_registers; + used.type = registered.type; + used.sh_registers = registered.sh_registers; + if (std::memcmp(&used, ®istered, offsetof(Shader, num_sh_registers) + sizeof(Shader::num_sh_registers)) != 0) return false; + const auto address = reinterpret_cast(registered.sh_registers); + const std::size_t bytes = registered.num_sh_registers * sizeof(ShaderRegister); + if (bytes == 0 || bound == nullptr || address < snapshot.headerAddress || address - snapshot.headerAddress > snapshot.header.size() || snapshot.header.size() - (address - snapshot.headerAddress) < bytes) return false; + GuestMemory::CheckRange(bound, bytes, alignof(ShaderRegister)); + for (std::size_t index = 0; index < registered.num_sh_registers; ++index) { + ShaderRegister expected; + std::memcpy(&expected, snapshot.header.data() + (address - snapshot.headerAddress) + index * sizeof(ShaderRegister), sizeof(ShaderRegister)); + const auto reg = bound[index]; + if (reg.offset != expected.offset) return false; + const bool fused = reg.offset == 0x100u || reg.offset == 0x10au || reg.offset == 0x10bu || reg.offset == 0x148u || reg.offset == 0x149u; + if (!fused && reg.value != expected.value) return false; + } + return true; +} + +static Shader GuestShaderFields(const Shader* shader) { + GuestMemory::CheckRange(shader, sizeof(Shader), 1); + Shader fields; + std::memcpy(&fields, static_cast(shader), sizeof(Shader)); + return fields; +} + +static bool RelocatedRegisters(const ShaderSnapshot& snapshot, Shader used, const Shader& registered) { + const auto* bound = used.sh_registers; + used.sh_registers = registered.sh_registers; + if (std::memcmp(&used, ®istered, offsetof(Shader, num_sh_registers) + sizeof(Shader::num_sh_registers)) != 0) return false; + const auto address = reinterpret_cast(registered.sh_registers); + const std::size_t bytes = registered.num_sh_registers * sizeof(ShaderRegister); + if (bytes == 0 || bound == nullptr || address < snapshot.headerAddress || address - snapshot.headerAddress > snapshot.header.size() || snapshot.header.size() - (address - snapshot.headerAddress) < bytes) return false; + GuestMemory::CheckRange(bound, bytes, alignof(ShaderRegister)); + return std::memcmp(static_cast(bound), snapshot.header.data() + (address - snapshot.headerAddress), bytes) == 0; +} + +// 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; + GuestMemory::CheckRange(shader, sizeof(Shader), 1); + Shader used; + Shader registered; + 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) || 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) { + const auto found = registry.find(reinterpret_cast(const_cast(GuestShaderFields(shader).code))); + if (found == registry.end()) return {}; + for (const auto& snapshot : found->second) { + if (snapshot->headerAddress == reinterpret_cast(shader)) return snapshot; + } + for (const auto& snapshot : found->second) { + if (SameHeader(*snapshot, shader)) return snapshot; + } + return {}; +} + +std::shared_ptr RegisteredProgram(const ShaderRegistry& registry, std::uint64_t address, std::initializer_list types) { + auto found = registry.upper_bound(address); + if (found == registry.begin()) return {}; + --found; + std::shared_ptr result; + bool registered = false; + for (const auto& snapshot : found->second) { + if (address - snapshot->codeAddress >= snapshot->code.size() * sizeof(std::uint32_t)) continue; + registered = true; + if (std::ranges::find(types, snapshot->type) == types.end()) continue; + require(result == nullptr || result->type == snapshot->type, "registered program has ambiguous shader binary types"); + result = snapshot; + } + require(!registered || result != nullptr, "registered program refers to an incompatible shader binary type"); + return result; +} + void PublishRegisteredShader(std::shared_ptr& registry, const std::shared_ptr& snapshot) { ShaderPreparationTransaction transaction; + transaction.Hold(); + auto published = snapshot; if (registry != nullptr) { const auto found = registry->find(snapshot->codeAddress); if (found != registry->end()) { - const auto& current = *found->second; - if (current.headerAddress == snapshot->headerAddress && current.type == snapshot->type && current.code == snapshot->code && current.header == snapshot->header) { - transaction.Commit(); - return; + for (const auto& current : found->second) { + if (current->headerAddress == snapshot->headerAddress && current->type == snapshot->type && current->code == snapshot->code && current->header == snapshot->header) { + if (current == found->second.back()) { + transaction.Commit(); + return; + } + published = current; + break; + } } } } auto next = registry != nullptr ? std::make_shared(*registry) : std::make_shared(); - next->insert_or_assign(snapshot->codeAddress, snapshot); + auto& headers = (*next)[snapshot->codeAddress]; + std::erase_if(headers, [&](const auto& current) { return current->headerAddress == snapshot->headerAddress || current->code != snapshot->code; }); + headers.push_back(published); registry = std::move(next); transaction.Commit(); } +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; + } + if (request.context.vertex && (request.context.vertex->paClVsOutCntl & (1u << 18u)) != 0) { + APS5_LOG_OUT_DEBUG("Shader 0x%llx is drawn into a layered target; preparing its layered variant at draw", static_cast(snapshot.codeAddress)); + return; + } + if (!snapshot.header.empty()) { + if (!snapshot.prepared->deferred) APS5_LOG_ERR("Shader 0x%llx stage %u has no artifact prepared at registration for the state of this draw or dispatch; preparing it at use", static_cast(request.shader.codeAddress), static_cast(request.shader.stage)); + return; + } + if (request.shader.stage == ShaderRecompiler::ShaderStage::Compute) { + if (snapshot.type != 0) throw std::runtime_error("AGC driver: unregistered program is not a compute shader"); + APS5_LOG_ERR("Compute shader 0x%llx was not registered; preparing its artifact at dispatch", static_cast(snapshot.codeAddress)); + return; + } + APS5_LOG_ERR("Graphics shader 0x%llx was not registered; preparing its artifact at first use", static_cast(snapshot.codeAddress)); +} + +} + std::shared_ptr SourceHandleFor(const ShaderSnapshot& snapshot, std::size_t codeOffset, const ShaderRecompiler::RecompileRequest& request) { + require(codeOffset < snapshot.code.size(), "prepared shader code offset is outside the snapshot"); + require(std::ranges::equal(request.shader.code, std::span(snapshot.code).subspan(codeOffset)), "prepared shader request does not refer to registered code"); struct PreparedKeyStorage {}; auto& key = HostThreadLocal, PreparedKeyStorage>(); ShaderRecompiler::BuildPreparedShaderKey(request, key); - std::lock_guard lock(snapshot.prepared->mutex); + std::unique_lock lock(snapshot.prepared->mutex); + AwaitRegisteredPreparation(*snapshot.prepared, lock); for (const auto& entry : snapshot.prepared->entries) { if (entry.codeOffset == codeOffset && ShaderRecompiler::MatchesPreparedShader(request, *entry.handle, key)) return entry.handle; } - if (snapshot.header.empty()) { - if (snapshot.type != 0 || request.shader.stage != ShaderRecompiler::ShaderStage::Compute) throw std::runtime_error("AGC driver: unregistered program is not a compute shader"); - APS5_LOG_ERR("Compute shader 0x%llx was not registered; preparing its artifact at dispatch", static_cast(snapshot.codeAddress)); - auto handle = ShaderRecompiler::PrepareShader(request); - snapshot.prepared->entries.push_back({codeOffset, handle}); - return handle; - } - std::string layouts; - for (const auto& entry : snapshot.prepared->entries) { - if (entry.codeOffset != codeOffset || entry.handle == nullptr || entry.handle->artifact == nullptr) continue; - const auto& layout = entry.handle->artifact->layout; - layouts += " [" + std::to_string(layout.pushConstantOffsetBytes) + "," + std::to_string(layout.pushConstantSizeBytes) + "]"; - } - throw std::runtime_error("AGC driver: prepared shader artifact is missing for the requested static ABI: address=" + std::to_string(request.shader.codeAddress) + " stage=" + std::to_string(static_cast(request.shader.stage)) + " wave=" + std::to_string(request.context.waveSize) + " pushOffset=" + std::to_string(request.layout.pushConstantOffsetBytes) + " pushCapacity=" + std::to_string(request.layout.pushConstantSizeBytes) + " preparedLayouts=" + layouts); + ReportPreparedAtUse(snapshot, request); + auto handle = PrepareShaderWithDiagnostics(request); + snapshot.prepared->entries.push_back({codeOffset, handle}); + return handle; } ShaderRecompiler::RectListShaders PreparedRectangle(const ShaderSnapshot& snapshot, std::uint64_t vertexId, std::uint64_t fragmentId) { @@ -202,56 +646,199 @@ ShaderRecompiler::RectListShaders PreparedRectangle(const ShaderSnapshot& snapsh throw std::runtime_error("AGC driver: prepared rectangle artifacts are missing"); } +ShaderRecompiler::RectListShaders DrawRectangle(const ShaderSnapshot& front, const std::shared_ptr& fragment, std::uint64_t vertexId, std::uint64_t fragmentId, const ShaderRecompiler::SpirvTarget& target) { + const auto prepared = [&] { + std::lock_guard lock(front.prepared->mutex); + return std::ranges::any_of(front.prepared->rectangles, [&](const auto& entry) { return entry.vertexId == vertexId && entry.fragmentId == fragmentId; }); + }; + if (prepared()) return PreparedRectangle(front, vertexId, fragmentId); + APS5_LOG_ERR("Rect-list draw of shader 0x%llx has no rectangle for fragment shader 0x%llx; preparing it at draw", static_cast(front.codeAddress), static_cast(fragment->codeAddress)); + ResolvePreparedGraphics(front, fragment, 17, target); + return PreparedRectangle(front, vertexId, fragmentId); +} + +namespace { + +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 {}; + const auto continuation = ReadUserDataTailContinuation(request.shader.code, request.context.userDataBaseRegister, request.context.userData); + return ShaderRecompiler::LinkUserDataTail(program, *tail, ShaderRecompiler::RdnaInstructionDecoder{}.Decode(continuation->code)); +} + +std::string hexText(std::uint64_t value) { + char text[24]; + std::snprintf(text, sizeof(text), "0x%llx", static_cast(value)); + return text; +} + +std::vector ReadUserDataCallee(std::uint64_t address, std::uint32_t linkRegister) { + constexpr std::uint64_t page = 4096u; + constexpr std::size_t limitWords = 16384u; + if (address % 4u != 0u) throw std::runtime_error("its callee address " + hexText(address) + " is not dword aligned"); + std::vector words; + while (words.size() < limitWords) { + const auto next = address + words.size() * 4u; + const auto bytes = static_cast(page - next % page); + if (!GuestMemory::Accessible(reinterpret_cast(static_cast(next)), bytes)) throw std::runtime_error("its callee at " + hexText(address) + " is not readable at " + hexText(next)); + const auto old = words.size(); + words.resize(old + bytes / 4u); + GuestMemory::Read(next, std::as_writable_bytes(std::span(words).subspan(old))); + if (const auto length = ShaderRecompiler::UserDataCalleeLength(words, linkRegister)) { + words.resize(*length); + return words; + } + } + throw std::runtime_error("its callee at " + hexText(address) + " does not return within " + std::to_string(limitWords * 4u) + " bytes"); +} + +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()), request.context.waveSize}; + const auto calls = ShaderRecompiler::FindUserDataCalls(program, swappc); + 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; + const auto address = static_cast(request.context.userData[slot]) | static_cast(request.context.userData[slot + 1u]) << 32u; + try { + callees.push_back(ReadUserDataCallee(address, call.linkRegister)); + } catch (const std::exception& error) { + throw std::runtime_error("AGC driver: compute shader " + hexText(request.shader.codeAddress) + " calls user data s[" + std::to_string(call.userDataRegister) + ":" + + std::to_string(call.userDataRegister + 1u) + "] at program counter " + hexText(call.programCounter) + ", but " + error.what()); + } + } + 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); +} + +} + ShaderRecompiler::PreparedShaderInvocation InvocationFor(const ShaderSnapshot& snapshot, std::size_t codeOffset, const ShaderRecompiler::RecompileRequest& request) { require(codeOffset < snapshot.code.size(), "prepared shader code offset is outside the snapshot"); const auto code = std::span(snapshot.code).subspan(codeOffset); require(request.shader.code.data() == code.data() && request.shader.code.size() == code.size(), "prepared invocation does not refer to registered code"); - auto invocationRequest = request; + auto linked = LinkUserDataCallees(request); + if (linked.code.empty()) linked.code = LinkGraphicsUserDataTail(request, snapshot); + auto source = request; + 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(request, key); - std::lock_guard lock(snapshot.prepared->mutex); + ShaderRecompiler::BuildPreparedShaderKey(source, key); for (const auto& entry : snapshot.prepared->entries) { if (entry.codeOffset != codeOffset) continue; invocationRequest.shader.code = ShaderRecompiler::GetPreparedCode(*entry.handle); + 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); } - if (snapshot.header.empty()) { - if (snapshot.type != 0 || request.shader.stage != ShaderRecompiler::ShaderStage::Compute) throw std::runtime_error("AGC driver: unregistered program is not a compute shader"); - APS5_LOG_ERR("Compute shader 0x%llx was not registered; preparing its artifact at dispatch", static_cast(snapshot.codeAddress)); - auto handle = ShaderRecompiler::PrepareShader(request); - invocationRequest = request; - invocationRequest.shader.code = ShaderRecompiler::GetPreparedCode(*handle); - auto invocation = ShaderRecompiler::PreparedShaderInvocation::TryCreate(invocationRequest, handle, key); - if (!invocation.has_value()) throw std::runtime_error("AGC driver: raw compute artifact does not match its invocation"); - snapshot.prepared->entries.push_back({codeOffset, std::move(handle)}); - return std::move(*invocation); - } - std::string layouts; - for (const auto& entry : snapshot.prepared->entries) { - if (entry.codeOffset != codeOffset || entry.handle == nullptr || entry.handle->artifact == nullptr) continue; - const auto& layout = entry.handle->artifact->layout; - layouts += " [" + std::to_string(layout.pushConstantOffsetBytes) + "," + std::to_string(layout.pushConstantSizeBytes) + "]"; - } - throw std::runtime_error("AGC driver: prepared shader artifact is missing for the requested static ABI: address=" + std::to_string(request.shader.codeAddress) + " stage=" + std::to_string(static_cast(request.shader.stage)) + " wave=" + std::to_string(request.context.waveSize) + " pushOffset=" + std::to_string(request.layout.pushConstantOffsetBytes) + " pushCapacity=" + std::to_string(request.layout.pushConstantSizeBytes) + " preparedLayouts=" + layouts); + ReportPreparedAtUse(snapshot, source); + auto handle = PrepareShaderWithDiagnostics(source); + invocationRequest = source; + invocationRequest.shader.code = ShaderRecompiler::GetPreparedCode(*handle); + auto invocation = ShaderRecompiler::PreparedShaderInvocation::TryCreate(invocationRequest, handle, key); + if (!invocation.has_value()) throw std::runtime_error("AGC driver: an artifact prepared at use does not match its invocation"); + 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 @@ -272,9 +859,8 @@ Shader ReadHeader(const ShaderSnapshot& snapshot) { return header; } -RegisteredShaderState DecodeRegisteredState(const ShaderSnapshot& snapshot) { +RegisteredShaderState DecodeRegisteredState(const ShaderSnapshot& snapshot, RegisteredShaderState state = {{}, InitialContextRegisters(), {{0x24a, 0}, {0x24b, 0}}}) { const auto header = ReadHeader(snapshot); - RegisteredShaderState state{{}, InitialContextRegisters(), {{0x24a, 0}, {0x24b, 0}}}; for (const auto reg : ReadHeaderArray(snapshot, header.sh_registers, header.num_sh_registers)) { state.shader.insert_or_assign(reg.offset, reg.value); } @@ -284,7 +870,7 @@ RegisteredShaderState DecodeRegisteredState(const ShaderSnapshot& snapshot) { if (header.specials != nullptr) { const auto special = ReadHeaderArray(snapshot, header.specials, 1).front(); state.context[special.vgt_shader_stages_en.offset] = special.vgt_shader_stages_en.value; - state.context[special.vgt_gs_out_prim_type.offset] = special.vgt_gs_out_prim_type.value; + if (special.vgt_gs_out_prim_type.offset != 0) state.context[special.vgt_gs_out_prim_type.offset] = special.vgt_gs_out_prim_type.value; state.userConfig[special.ge_cntl.offset] = special.ge_cntl.value; state.userConfig[special.ge_user_vgpr_en.offset] = special.ge_user_vgpr_en.value; } @@ -303,11 +889,22 @@ void BuildRegisteredAbiKey(const QueueState& state, const VulkanDevice& device, key.insert(key.end(), {device.Serial(), ShaderRecompiler::DebugProbeActive(), ShaderRecompiler::RayTracingStrict(), ShaderRecompiler::RayTracingMiss()}); for (const auto* registers : {&state.shader, &state.context, &state.userConfig}) { key.push_back(registers->size()); - for (const auto& [offset, value] : *registers) key.push_back((static_cast(offset) << 32u) | value); + registers->ForEach([&](std::uint32_t offset, std::uint32_t value) { key.push_back((static_cast(offset) << 32u) | value); }); } } -std::vector PrepareRegistered(const ShaderSnapshot& snapshot, const VulkanDevice& device, const QueueState& state, bool registration) { +struct RegisteredPreparation { + std::size_t codeOffset = 0; + bool registration = false; + ShaderRecompiler::SwappcInfo swappc{}; + std::vector userData; + std::array memory{}; + std::optional request; + std::vector abiKey; + std::shared_ptr prebuildDevice; +}; + +std::unique_ptr PlanRegistered(const ShaderSnapshot& snapshot, const VulkanDevice& device, const QueueState& state, bool registration, bool* deferred = nullptr) { using Stage = ShaderRecompiler::ShaderStage; const auto header = ReadHeader(snapshot); std::uint32_t programRegister; @@ -319,18 +916,38 @@ std::vector PrepareRegistered(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: stage = Stage::Local; programRegister = 0x148; resourceRegister = 0x10b; firstUser = 8; break; + 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: throw std::runtime_error("AGC driver: unsupported registered shader type"); + default: + if (registration) { + APS5_LOG_ERR("Shader 0x%llx has unsupported registered shader type %u; it is not prepared at registration", static_cast(snapshot.codeAddress), static_cast(snapshot.type)); + return nullptr; + } + throw std::runtime_error("AGC driver: unsupported registered shader type " + std::to_string(snapshot.type)); } + const auto defer = [&](const char* reason) { + APS5_LOG_ERR("Shader 0x%llx could not be prepared at registration (%s); preparing it when it is used", static_cast(snapshot.codeAddress), reason); + *deferred = true; + return std::unique_ptr{}; + }; + if (deferred != nullptr && (!state.shader.contains(programRegister) || !state.shader.contains(programRegister + 1))) return defer("its program address is not in the shader registers"); const auto high = RegisterValue(state.shader, programRegister + 1); if ((high & ~0xffu) != 0) throw std::runtime_error("AGC driver: invalid registered program address"); const auto address = (static_cast(RegisterValue(state.shader, programRegister)) << 8u) | (static_cast(high) << 40u); if (address < snapshot.codeAddress || address - snapshot.codeAddress >= snapshot.code.size() * 4u) throw std::runtime_error("AGC driver: registered entry point is outside shader code"); const auto codeOffset = static_cast((address - snapshot.codeAddress) / 4u); const auto code = std::span(snapshot.code).subspan(codeOffset); - const auto decoded = ShaderRecompiler::RdnaInstructionDecoder{}.Decode(code); + auto plan = std::make_unique(); + plan->codeOffset = codeOffset; + ShaderRecompiler::RdnaProgram decoded; + plan->registration = registration; + try { + decoded = ShaderRecompiler::RdnaInstructionDecoder{}.Decode(code); + } catch (const ShaderRecompiler::UnsupportedInstructionError& error) { + if (deferred == nullptr) throw; + return defer(error.what()); + } std::optional compute; std::optional pixel; std::optional vertex; @@ -342,17 +959,30 @@ std::vector PrepareRegistered(const ShaderSnapshot& snap if (userCount > 32) throw std::runtime_error("AGC driver: static user SGPR count exceeds the register bank"); if (stage == Stage::Compute) { compute = Graphics::DecodeComputeStageInfo(state.shader, snapshot.header); + if (header.specials == nullptr) { + if (deferred != nullptr) return defer("its header has no specials to give its wave size"); + return {}; + } const auto special = ReadHeaderArray(snapshot, header.specials, 1).front(); wave = (special.dispatch_modifier & 0x8000u) != 0 ? 32u : 64u; } else if (stage == Stage::Fragment) { const auto count = RegisterValue(state.context, 0x1b6) & 0x3fu; - if (registration && count != 0) return {}; + 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 ((routing & 0x20u) != 0 || stage == Stage::Mesh || (routing & 4u) != 0) { + 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; stageState.context[0x1b6] = 0; @@ -363,39 +993,408 @@ std::vector PrepareRegistered(const ShaderSnapshot& snap } if (firstUser == 0) userCount += 8; } - std::vector userData(userCount); - if (stage != Stage::Compute && stage != Stage::Fragment && snapshot.type != 6) vertex = Graphics::DecodeVertexStageInfo(snapshot.header, snapshot.headerAddress, userData, nullptr, true); - const ShaderRecompiler::SwappcInfo swappc{vertex.has_value(), firstUser, userCount}; - auto graph = ShaderRecompiler::GraphBuilder{}.Build(decoded, &swappc); - ShaderRecompiler::Structurizer{}.Structurize(graph); - const std::array memory{{{snapshot.codeAddress, std::as_bytes(std::span(snapshot.code))}, {snapshot.headerAddress, snapshot.header}}}; - ShaderRecompiler::RecompileRequest request{{stage, address, code, snapshot.headerAddress, snapshot.header}, {wave, firstUser, userData, compute, pixel, vertex, memory, RegisteredFloatMode(snapshot)}, stage == Stage::Compute ? device.ComputeTarget(wave) : device.Target(), {0, 0, 0, 128}, graphics}; + plan->userData.resize(userCount); + if (stage != Stage::Compute && stage != Stage::Fragment && snapshot.type != 6) { + vertex = Graphics::DecodeVertexStageInfo(snapshot.header, snapshot.headerAddress, plan->userData, nullptr, true, 8u - firstUser); + const auto outputs = state.context.find(0x207u); + if (vertex && outputs != state.context.end()) vertex->paClVsOutCntl = PositionExportLayout(&snapshot, codeOffset, std::span(snapshot.code).subspan(codeOffset), outputs->second); + } + plan->swappc = {vertex.has_value(), firstUser, userCount}; + if (deferred != nullptr) { + if (const auto counter = ShaderRecompiler::UnresolvableSwappcTarget(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); + } + } + plan->memory = {{{snapshot.codeAddress, std::as_bytes(std::span(snapshot.code))}, {snapshot.headerAddress, snapshot.header}}}; + auto& request = plan->request.emplace(ShaderRecompiler::RecompileRequest{{stage, address, code, snapshot.headerAddress, snapshot.header}, {wave, firstUser, plan->userData, compute, pixel, vertex, plan->memory, RegisteredFloatMode(snapshot)}, stage == Stage::Compute ? device.ComputeTarget(wave) : device.Target(), {0, 0, 0, 128}, graphics}); if (graphics && graphics->mesh) request.layout.pushConstantSizeBytes = ShaderRecompiler::MeshDrawPushOffsetBytes; + if (graphics && graphics->tessellation) request.layout.pushConstantSizeBytes = ShaderRecompiler::TessellationLevelPushOffsetBytes; + if (pixel && device.GraphicsPipelineLibraries()) request.layout.pushConstantOffsetBytes = Graphics::FixedPushOffset(Stage::Fragment); + return plan; +} + +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 (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 { + static_cast(device->PrebuildGraphicsLibraries(stages, shaders)); + } catch (const std::exception& error) { + APS5_LOG_OUT("shader library prebuild skipped: %s", error.what()); + } + }); +} + +std::vector PrepareRegistered(RegisteredPreparation& plan) { + auto& request = *plan.request; std::vector entries; + ShaderRecompiler::ShaderPreparationContext preparation; const auto append = [&] { PerformanceTimer timing("Shader.PrepareArtifact"); - entries.push_back({codeOffset, ShaderRecompiler::PrepareShader(request)}); + entries.push_back({plan.codeOffset, PrepareShaderWithDiagnostics(request, &preparation)}); }; append(); - if (compute) { + 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) { + APS5_LOG_OUT("shader library prebuild skipped: %s", error.what()); + } + } + if (request.context.compute) { request.context.compute->partialThreads = {1, 1, 1}; append(); - } else if (pixel) { + } else if (request.context.pixel) { request.layout.pushConstantSizeBytes = 0; append(); } + if (plan.registration) { + for (const auto& entry : entries) { + if (entry.handle != nullptr) ShaderRecompiler::ReleaseUndrawnResourcePlan(*entry.handle); + } + } return entries; } +std::vector PrepareRegistered(const ShaderSnapshot& snapshot, const VulkanDevice& device, const QueueState& state, bool registration, bool* deferred = nullptr) { + const auto plan = PlanRegistered(snapshot, device, state, registration, deferred); + return plan != nullptr ? PrepareRegistered(*plan) : std::vector{}; } -std::vector PrepareGraphicsStages(const DrawDecode& decoded, const ShaderRecompiler::SpirvTarget& target) { +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 = [] { + std::lock_guard lifecycle(Lifecycle()); + auto* created = new RegistrationPreparePool(Closing() ? 0u : std::max(2u, std::thread::hardware_concurrency() / 2u)); + PreparationSlot().store(created); + return created; + }(); + return *pool; + } + + static RegistrationPreparePool* Existing() { + return PreparationSlot().load(); + } + + static RegistrationPreparePool& Prebuild() { + static auto* pool = [] { + const char* text = std::getenv("APS5_PREBUILD_THREADS"); + 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; + }(); + return *pool; + } + + static RegistrationPreparePool* ExistingPrebuild() { + return PrebuildSlot().load(); + } + + static unsigned LiveWorkers() { + return liveWorkers.load(std::memory_order_acquire); + } + + static void CloseAll() { + std::array pools{}; + { + std::lock_guard lifecycle(Lifecycle()); + Closing() = true; + pools = {Existing(), ExistingPrebuild()}; + } + for (auto* pool : pools) { + if (pool != nullptr) pool->Close(); + } + } + +private: + static std::mutex& Lifecycle() { + static std::mutex lifecycle; + return lifecycle; + } + + static bool& Closing() { + static bool closing = false; + return closing; + } + + static std::atomic& PreparationSlot() { + static std::atomic slot{nullptr}; + return slot; + } + + static std::atomic& PrebuildSlot() { + static std::atomic slot{nullptr}; + return slot; + } + +public: + + void Submit(std::function task, const PreparedShaders* prepared = nullptr) { + { + std::unique_lock lock(mutex); + if (threads != 0 && !closed) { + 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; + } + } + task(); + } + + void Prioritize(const PreparedShaders& prepared) { + std::lock_guard lock(mutex); + const auto found = std::ranges::find_if(tasks, [&](const auto& task) { return task.prepared == &prepared; }); + if (found == tasks.end()) return; + if (std::getenv("APS5_TRACE_PREPARATION_WAIT") != nullptr) std::fprintf(stderr, "[shader-prepare] demanded queued task at position %zu of %zu\n", static_cast(std::distance(tasks.begin(), found)), tasks.size()); + std::rotate(tasks.begin(), found, std::next(found)); + } + + void AwaitSubmitted() { + std::unique_lock lock(mutex); + const auto ticket = submitted; + idle.wait(lock, [&] { return unfinished.empty() || *unfinished.begin() > ticket; }); + } + + void Close() { + std::vector joined; + { + std::unique_lock lock(mutex); + closed = true; + idle.wait(lock, [&] { return tasks.empty() && running == 0; }); + joined.swap(workers); + } + ready.notify_all(); + for (const auto& worker : joined) 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, StackBytes); + for (unsigned index = 0; index < count; ++index) { + pthread_t thread; + if (pthread_create(&thread, &attributes, &RegistrationPreparePool::Main, this) == 0) { + 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); + for (;;) { + Task task; + { + std::unique_lock lock(pool.mutex); + 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; + pool.unfinished.erase(sequence); + drained = pool.tasks.empty() && pool.running == 0; + pool.idle.notify_all(); + } +#ifdef __GLIBC__ + if (drained) trimFreedMemory(); +#endif + } + return nullptr; + } + + 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; +}; + +bool AsyncRegistrationPrepare() { + static const bool sync = std::getenv("APS5_SYNC_SHADER_PREPARE") != nullptr; + return !sync; +} + +void PrepareRegisteredInBackground(std::shared_ptr snapshot, std::shared_ptr plan) { + const auto* preparedKey = snapshot->prepared.get(); + SubmitRegistrationPreparation([snapshot = std::move(snapshot), plan = std::move(plan)] { + PerformanceContext timingContext(FrameTiming::Preparation()); + std::vector entries; + std::exception_ptr failure; + try { + entries = PrepareRegistered(*plan); + } catch (const std::exception& error) { + APS5_LOG_ERR("Shader 0x%llx could not be prepared after registration: %s", static_cast(snapshot->codeAddress), error.what()); + failure = std::current_exception(); + } catch (...) { + failure = std::current_exception(); + } + auto& prepared = *snapshot->prepared; + { + std::lock_guard lock(prepared.mutex); + prepared.entries.insert(prepared.entries.end(), std::make_move_iterator(entries.begin()), std::make_move_iterator(entries.end())); + if (failure == nullptr && !plan->abiKey.empty()) prepared.registeredAbis.push_back(std::move(plan->abiKey)); + prepared.failure = failure; + prepared.pending = false; + } + prepared.settled.notify_all(); + }, preparedKey); +} + +} + +void SubmitRegistrationPreparation(std::function task, const PreparedShaders* prepared) { + RegistrationPreparePool::Instance().Submit(std::move(task), prepared); +} + +void PrioritizeRegistrationPreparation(PreparedShaders& prepared) { + prepared.demanded.store(true); + if (auto* pool = RegistrationPreparePool::Existing()) pool->Prioritize(prepared); +} + +void AwaitRegistrationPreparation() { + if (auto* pool = RegistrationPreparePool::Existing()) pool->AwaitSubmitted(); +} + +void SubmitLibraryPrebuild(std::function task) { + RegistrationPreparePool::Prebuild().Submit(std::move(task)); +} + +void AwaitLibraryPrebuilds() { + if (auto* pool = RegistrationPreparePool::ExistingPrebuild()) pool->AwaitSubmitted(); +} + +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"); std::vector expected; - if (decoded.state.stages.path == Graphics::ShaderPath::Tessellation) expected = {ShaderRecompiler::ProgramRole::Local, ShaderRecompiler::ProgramRole::Hull, ShaderRecompiler::ProgramRole::Domain}; - else { + if (decoded.state.stages.path == Graphics::ShaderPath::Tessellation) { + if (!decoded.roles.empty() && decoded.roles.front() == ShaderRecompiler::ProgramRole::Local) expected.push_back(ShaderRecompiler::ProgramRole::Local); + expected.insert(expected.end(), {ShaderRecompiler::ProgramRole::Hull, ShaderRecompiler::ProgramRole::Domain}); + } else { expected.push_back(ShaderRecompiler::ProgramRole::Main); if (decoded.state.stages.path == Graphics::ShaderPath::Geometry && !decoded.programs.empty() && decoded.programs.front().snapshot->type == 4) expected.push_back(ShaderRecompiler::ProgramRole::GeometryBack); } @@ -403,25 +1402,41 @@ 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 auto capacity = decoded.state.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : Graphics::PipelinePushConstantBytes; + const bool fixedSlots = target.fixedPushSlots; + 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; + 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; - if (!fragment) vertex = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, nullptr, true); - 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, pushOffset, capacity - pushOffset}, ShaderRecompiler::GraphicsCompileContext{program.firstUserSgpr, linked, decoded.state.stages.mesh, decoded.state.stages.tessellation, {}}}; + 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; + 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); std::shared_ptr handle; @@ -433,18 +1448,42 @@ std::vector PrepareGraphicsStages(const DrawDecode& decod } if (handle == nullptr) { PerformanceTimer timing("Shader.PrepareArtifact"); - handle = ShaderRecompiler::PrepareShader(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 <= capacity - pushOffset, "prepared graphics stages exceed the push constant block"); - prepared.push_back({program.snapshot, {program.codeOffset, handle}}); + require(bytes <= room, "prepared graphics stages exceed the push constant block"); + std::shared_ptr registration; + if (target.registrationModules && !LibraryPrebuildDisabled()) { + try { + registration = ShaderRecompiler::RegistrationResult(request, *handle); + } catch (const std::exception& error) { + APS5_LOG_OUT("shader library prebuild skipped: %s", error.what()); + } + } + prepared.push_back({program.snapshot, {program.codeOffset, handle}, program.binary.stage, offset, std::move(registration)}); pushOffset += bytes; } transaction.Commit(); 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; @@ -453,27 +1492,41 @@ 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; for (const auto* shader : stages) { - GuestMemory::CheckRange(shader, sizeof(Shader), alignof(Shader)); - const auto address = reinterpret_cast(const_cast(shader->code)); + const auto fields = GuestShaderFields(shader); + const auto address = reinterpret_cast(const_cast(fields.code)); require(registry != nullptr && registry->contains(address), "graphics ABI refers to an unregistered shader"); - if (owner == nullptr) owner = registry->at(address); - const auto& snapshot = *registry->at(address); - require(snapshot.headerAddress == reinterpret_cast(shader), "graphics ABI refers to a replaced shader header"); + const auto selected = RegisteredHeader(*registry, shader); + require(selected != nullptr, "graphics ABI refers to a replaced shader header"); + selectedStages.push_back(selected); + const auto& snapshot = *selected; require(snapshot.registeredState != nullptr, "registered shader state is missing"); - const auto& registered = *snapshot.registeredState; - for (const auto& [offset, value] : registered.shader) state.shader.insert_or_assign(offset, value); - for (const auto& [offset, value] : registered.context) state.context.insert_or_assign(offset, value); - for (const auto& [offset, value] : registered.userConfig) state.userConfig.insert_or_assign(offset, value); + const auto registered = DecodeRegisteredState(snapshot, {}); + registered.shader.ForEach([&](std::uint32_t offset, std::uint32_t value) { state.shader.insert_or_assign(offset, value); }); + registered.context.ForEach([&](std::uint32_t offset, std::uint32_t value) { state.context.insert_or_assign(offset, value); }); + registered.userConfig.ForEach([&](std::uint32_t offset, std::uint32_t value) { state.userConfig.insert_or_assign(offset, value); }); + if (fields.type != snapshot.type) { + const std::size_t count = fields.num_sh_registers; + GuestMemory::CheckRange(fields.sh_registers, count * sizeof(ShaderRegister), alignof(ShaderRegister)); + for (std::size_t index = 0; index < count; ++index) state.shader.insert_or_assign(fields.sh_registers[index].offset, fields.sh_registers[index].value); + } } } for (const auto reg : context) state.context.insert_or_assign(reg.offset, reg.value); 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); @@ -483,23 +1536,35 @@ 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); - 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) { + if (stage.registration != nullptr) PrebuildStageLibrary(localDevice, decoded.state.stages, stage.stage, stage.registration, stage.pushOffset); + } } PreparedShaderState::GraphicsAbi abi{key, registry, {}}; for (const auto& stage : prepared) abi.stages.push_back(stage.snapshot); @@ -510,12 +1575,13 @@ 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; CheckFailure(); - GuestMemory::CheckRange(shader, sizeof(Shader), alignof(Shader)); - if (shader->type != 0 && shader->type != 1) { + const auto fields = GuestShaderFields(shader); + if (fields.type != 0 && fields.type != 1) { const std::array stages{shader}; ResolveGraphicsStagesAbi(stages, context, primitive); transaction.Commit(); @@ -524,10 +1590,10 @@ void Driver::ResolveShaderAbi(const Shader* shader, std::span snapshot; { std::lock_guard lock(mutex); - const auto address = reinterpret_cast(const_cast(shader->code)); + const auto address = reinterpret_cast(const_cast(fields.code)); require(shaders != nullptr && shaders->contains(address), "static ABI refers to an unregistered shader"); - snapshot = shaders->at(address); - require(snapshot->headerAddress == reinterpret_cast(shader), "static ABI refers to a replaced shader header"); + snapshot = RegisteredHeader(*shaders, shader); + require(snapshot != nullptr, "static ABI refers to a replaced shader header"); } require(snapshot->registeredState != nullptr, "registered shader state is missing"); QueueState state{}; @@ -538,20 +1604,39 @@ void Driver::ResolveShaderAbi(const Shader* shader, std::span, RegisteredAbiKeyStorage>(); BuildRegisteredAbiKey(state, *localDevice, key); + std::optional deferral; + const auto prepare = [&] { + 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(); const auto& registeredAbis = transaction.Read(*snapshot).registeredAbis; if (std::ranges::find(registeredAbis, key) != registeredAbis.end()) { transaction.Commit(); return; } - auto entries = PrepareRegistered(*snapshot, *localDevice, state, false); + if (committedAbi) entries = prepare(); auto& prepared = transaction.Edit(*snapshot); for (auto& entry : entries) { const auto duplicate = std::ranges::any_of(prepared.entries, [&](const auto& existing) { return existing.handle->artifact == 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(); } @@ -573,23 +1658,22 @@ 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; std::shared_ptr fragment; { std::lock_guard lock(mutex); - GuestMemory::CheckRange(vertex, sizeof(Shader), alignof(Shader)); - GuestMemory::CheckRange(pixel, sizeof(Shader), alignof(Shader)); const auto lookup = [&](const Shader* shader) { - const auto address = reinterpret_cast(const_cast(shader->code)); + const auto address = reinterpret_cast(const_cast(GuestShaderFields(shader).code)); require(shaders != nullptr && shaders->contains(address), "rectangle ABI refers to an unregistered shader"); - const auto snapshot = shaders->at(address); - require(snapshot->headerAddress == reinterpret_cast(shader), "rectangle ABI refers to a replaced shader header"); + const auto snapshot = RegisteredHeader(*shaders, shader); + require(snapshot != nullptr, "rectangle ABI refers to a replaced shader header"); return snapshot; }; front = lookup(vertex); @@ -598,13 +1682,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); @@ -630,27 +1734,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; }); @@ -659,7 +1773,33 @@ void ResolvePreparedGraphics(const ShaderSnapshot& front, const std::shared_ptr< transaction.Commit(); } +ShaderSnapshot PrepareNullPixelProgram(const VulkanDevice& device) { + ShaderSnapshot null{NullPixelProgramAddress(), reinterpret_cast(&NullPixelShader), NullPixelShader.type, {}, {}}; + null.code.assign(std::begin(NullPixelCode), std::end(NullPixelCode)); + null.header.resize(sizeof(Shader)); + std::memcpy(null.header.data(), &NullPixelShader, sizeof(Shader)); + auto nullRegisteredState = DecodeRegisteredState(null); + nullRegisteredState.shader.insert_or_assign(0x008u, static_cast(null.codeAddress >> 8u)); + nullRegisteredState.shader.insert_or_assign(0x009u, static_cast(null.codeAddress >> 40u)); + nullRegisteredState.shader.insert_or_assign(0x00bu, 0u); + nullRegisteredState.context.insert_or_assign(0x1b3u, 0x2u); + nullRegisteredState.context.insert_or_assign(0x1b4u, 0x2u); + null.registeredState = std::make_shared(std::move(nullRegisteredState)); + QueueState nullState{}; + nullState.shader = null.registeredState->shader; + nullState.context = null.registeredState->context; + nullState.userConfig = null.registeredState->userConfig; + null.prepared->entries = PrepareRegistered(null, device, nullState, true); + 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; @@ -673,22 +1813,27 @@ 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); snapshot.header.resize(fields.header_size); std::memcpy(snapshot.header.data(), static_cast(shader), fields.header_size); + transaction.Hold(); { std::lock_guard lock(mutex); rethrowFailure(); if (shaders != nullptr) { const auto found = shaders->find(snapshot.codeAddress); if (found != shaders->end()) { - const auto& current = *found->second; - if (current.headerAddress == snapshot.headerAddress && current.type == snapshot.type && current.code == snapshot.code && current.header == snapshot.header) { - transaction.Commit(); - return; + for (const auto& current : found->second) { + if (current->headerAddress == snapshot.headerAddress && current->type == snapshot.type && current->code == snapshot.code && current->header == snapshot.header) { + const auto identical = current; + PublishRegisteredShader(shaders, identical); + transaction.Commit(); + return; + } } } } @@ -705,11 +1850,23 @@ void Driver::RegisterShader(const Shader* shader) { registered.shader = snapshot.registeredState->shader; registered.context = snapshot.registeredState->context; registered.userConfig = snapshot.registeredState->userConfig; - snapshot.prepared->entries = PrepareRegistered(snapshot, *localDevice, registered, true); - if ((snapshot.type == 0 || snapshot.type == 1) && !snapshot.prepared->entries.empty()) { - std::vector key; - BuildRegisteredAbiKey(registered, *localDevice, key); - snapshot.prepared->registeredAbis.push_back(std::move(key)); + std::shared_ptr preparation; + 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); + snapshot.prepared->pending = preparation != nullptr; + } else { + snapshot.prepared->entries = PrepareRegistered(snapshot, *localDevice, registered, true, &snapshot.prepared->deferred); + if ((snapshot.type == 0 || snapshot.type == 1) && !snapshot.prepared->entries.empty()) { + std::vector key; + BuildRegisteredAbiKey(registered, *localDevice, key); + snapshot.prepared->registeredAbis.push_back(std::move(key)); + } } static const char* traceRegs = std::getenv("APS5_TRACE_SHADER_REGS"); if (traceRegs != nullptr && (std::string(traceRegs) == "all" || std::strtoull(traceRegs, nullptr, 16) == snapshot.codeAddress)) { @@ -726,34 +1883,21 @@ void Driver::RegisterShader(const Shader* shader) { print(fields.sh_registers, fields.num_sh_registers); std::fprintf(stderr, "\n"); } + const auto published = std::make_shared(std::move(snapshot)); std::lock_guard lock(mutex); rethrowFailure(); - PublishRegisteredShader(shaders, std::make_shared(std::move(snapshot))); + PublishRegisteredShader(shaders, published); if (shaders->find(NullPixelProgramAddress()) == shaders->end()) { - ShaderSnapshot null{NullPixelProgramAddress(), reinterpret_cast(&NullPixelShader), NullPixelShader.type, {}, {}}; - null.code.assign(std::begin(NullPixelCode), std::end(NullPixelCode)); - null.header.resize(sizeof(Shader)); - std::memcpy(null.header.data(), &NullPixelShader, sizeof(Shader)); - auto nullRegisteredState = DecodeRegisteredState(null); - nullRegisteredState.shader.emplace(0x008u, static_cast(null.codeAddress >> 8u)); - nullRegisteredState.shader.emplace(0x009u, static_cast(null.codeAddress >> 40u)); - nullRegisteredState.shader.emplace(0x00bu, 0u); - nullRegisteredState.context.emplace(0x1b3u, 0x2u); - nullRegisteredState.context.emplace(0x1b4u, 0x2u); - null.registeredState = std::make_shared(std::move(nullRegisteredState)); - QueueState nullState{}; - nullState.shader = null.registeredState->shader; - nullState.context = null.registeredState->context; - nullState.userConfig = null.registeredState->userConfig; - null.prepared->entries = PrepareRegistered(null, *localDevice, nullState, true); - PublishRegisteredShader(shaders, std::make_shared(std::move(null))); + PublishRegisteredShader(shaders, std::make_shared(PrepareNullPixelProgram(*localDevice))); } transaction.Commit(); + if (preparation != nullptr) PrepareRegisteredInBackground(published, std::move(preparation)); } } -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/DeferredLabels.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp index 395c2ef2e6..858cada5e4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp @@ -117,11 +117,11 @@ void Driver::recordDeferredLabels(VulkanDevice* localDevice, std::uint32_t queue const int reason = localDevice != nullptr ? localDevice->WriteLabelOnGpu(label.address, bytes, stamp, queue, first) : 4; first = false; countLabelOutcome(reason); - if (reason != 0 && reason != 5 && reason != 6) { + if (reason != 0 && reason != 5) { if (reason != 1 && localDevice != nullptr) localDevice->WaitIdle(); GuestMemory::Write(label.address, bytes, 4); } - noteLabelStore(label.address, bytes, stamp); + noteLabelStore(label.address, bytes, stamp, queue); } ++labelGroups; static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; 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 f599ec8361..2720b91265 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/LabelHistory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/LabelHistory.cpp @@ -6,18 +6,19 @@ namespace AgcDriver::DriverDetail { -void Driver::noteLabelStore(std::uint64_t address, std::span bytes, std::uint64_t stamp) { - if (bytes.empty() || bytes.size() > 64 || bytes.size() % 4 != 0 || address % 4 != 0) return; +void Driver::noteLabelStore(std::uint64_t address, std::span bytes, std::uint64_t stamp, std::uint32_t queue) { + 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]; std::shift_right(history.begin(), history.end(), 1); history[0].stamp = stamp; + history[0].queue = queue; std::memcpy(&history[0].value, bytes.data() + offset, 4); } } -bool Driver::storedSince(std::span packet, std::uint64_t address, std::size_t bytes, std::uint64_t received) { +bool Driver::storedSince(std::span packet, std::uint64_t address, std::size_t bytes, std::uint64_t received, std::uint32_t* writer) { std::lock_guard lock(labelStoresMutex); const auto low = labelStores.find(address); if (low == labelStores.end()) return false; @@ -31,7 +32,10 @@ bool Driver::storedSince(std::span packet, std::uint64_t ad if (upper == high->second.end()) continue; value |= static_cast(upper->value) << 32u; } - if (Pm4::WaitComparesValue(packet, value)) return true; + if (Pm4::WaitComparesValue(packet, value)) { + if (writer != nullptr) *writer = store.queue; + return true; + } } return false; } @@ -46,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 dfb31f4b74..5b1e22624b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/SynchronizationStatistics.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/SynchronizationStatistics.cpp @@ -5,7 +5,7 @@ namespace AgcDriver::DriverDetail { -std::atomic gpuLabels{0}, completionLabels{0}, notImportedLabels{0}, noOpLabels{0}, unlockedDrains{0}; +std::atomic gpuLabels{0}, completionLabels{0}, noOpLabels{0}, unlockedDrains{0}; std::atomic labelFallbacks[5] = {}; @@ -21,7 +21,7 @@ std::atomic packetLockRecordUs{0}, packetLockSubmitUs{0}; std::atomic triesFailed[TrySites] = {}; -std::atomic storesOnGpu{0}, storesBehindCompletions{0}, storesOnCpu{0}, storesDrained{0}; +std::atomic storesOnGpu{0}, storesBehindCompletions{0}, storesOnCpu{0}, storesDrained{0}, dmaFills{0}, copiesToHostMemory{0}, copiesBehindPendingSources{0}; std::map drainCounts; @@ -39,13 +39,12 @@ 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 deferred not imported, %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)\n", static_cast(drainTotal), report.c_str(), static_cast(unlockedDrains.load()), static_cast(gpuLabels.load()), static_cast(completionLabels.load()), static_cast(notImportedLabels.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())); + 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) { if (reason == 0) ++gpuLabels; else if (reason == 5) ++completionLabels; - else if (reason == 6) ++notImportedLabels; else ++labelFallbacks[std::min(reason, 4)]; } 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 9a6ee823d4..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,9 +1,12 @@ +#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" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include #include #include @@ -11,6 +14,9 @@ #include #endif #include +#include +#include +#include namespace AgcDriver::DriverDetail { @@ -49,18 +55,70 @@ bool Driver::traceLateLabels() { return trace; } -void Driver::waitMemory(std::span packet, std::uint32_t queue, const PacketHistory& context, std::uint64_t received, bool heldAtSubmit) { +std::vector& Driver::pendingWaits() { + struct WaitsTag {}; + return HostThreadLocal, WaitsTag>(); +} + +void Driver::notePendingWait(std::span packet, std::uint32_t queue, std::uint64_t received) { + if (packet.size() > std::tuple_size_v) return; + static const bool allQueues = std::getenv("APS5_LAND_WAITS_ALL_QUEUES") != nullptr; + if (!allQueues && queue == 0) return; + if (Pm4::WaitSatisfiedUnchecked(packet)) return; + auto& waits = pendingWaits(); + for (const auto& wait : waits) { + if (wait.count == packet.size() && std::equal(packet.begin(), packet.end(), wait.words.begin())) return; + } + PendingWait wait{}; + std::copy(packet.begin(), packet.end(), wait.words.begin()); + wait.count = packet.size(); + wait.received = received; + waits.push_back(wait); +} + +void Driver::landPendingWaits(std::uint32_t queue) { + auto& waits = pendingWaits(); + if (waits.empty()) return; + static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; + struct LandStats { + std::uint64_t lands = 0, waited = 0; + double waitedMs = 0, maxMs = 0; + std::chrono::steady_clock::time_point reported = std::chrono::steady_clock::now(); + }; + static thread_local LandStats stats; + const PacketHistory none{}; + for (const auto& wait : waits) { + const auto packet = std::span(wait.words).first(wait.count); + ++stats.lands; + if (Pm4::WaitSatisfiedUnchecked(packet)) continue; + const auto started = std::chrono::steady_clock::now(); + waitMemory(packet, queue, none, wait.received, false, true); + const auto ms = std::chrono::duration(std::chrono::steady_clock::now() - started).count(); + ++stats.waited; + stats.waitedMs += ms; + stats.maxMs = std::max(stats.maxMs, ms); + } + waits.clear(); + if (profile && std::chrono::steady_clock::now() - stats.reported > std::chrono::seconds(10)) { + AgcDriver::ProfilePrint_nid_no_patch("[landwait] queue 0x%x (10 s): %llu waits landed before a guest read, %llu had to wait for the GPU: %.1f ms in total, max %.2f ms\n", queue, static_cast(stats.lands), static_cast(stats.waited), stats.waitedMs, stats.maxMs); + stats = LandStats{}; + } +} + +void Driver::waitMemory(std::span packet, std::uint32_t queue, const PacketHistory& context, std::uint64_t received, bool heldAtSubmit, bool requireMemory) { auto start = std::chrono::steady_clock::now(); auto lastDone = packetsDone.load(); 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() { @@ -68,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 { @@ -82,9 +140,15 @@ void Driver::waitMemory(std::span packet, std::uint32_t que ++outcomes.atEntry; return; } - if (heldAtSubmit || storedSince(packet, awaited, awaitedBytes, received)) return; + if (heldAtSubmit) return; + if (std::uint32_t writer = queue; !requireMemory && storedSince(packet, awaited, awaitedBytes, received, &writer)) { + if (writer != queue) notePendingWait(packet, queue, received); + else epochPoint.bump = false; + return; + } static const bool labelShortcut = std::getenv("APS5_NO_LABEL_SHORTCUT") == nullptr; + const bool labelTable = labelShortcut && !requireMemory; static const bool overlapSubmit = std::getenv("APS5_NO_WAIT_OVERLAP_SUBMIT") == nullptr; static const bool waitLock = std::getenv("APS5_WAIT_LOCK") != nullptr; @@ -121,6 +185,8 @@ void Driver::waitMemory(std::span packet, std::uint32_t que if (hit->queue == queue) { ++outcomes.fromRecorderSameQueue; if (!hit->late) epochPoint.bump = false; + } else { + notePendingWait(packet, queue, received); } return true; }; @@ -129,7 +195,7 @@ void Driver::waitMemory(std::span packet, std::uint32_t que const bool unlocked = !waitLock && overlapSubmit; if (unlocked) { - if (labelShortcut) { + if (labelTable) { Graphics::Recorder::LabelRefusal refusal{}; if (takeLabel(Graphics::Recorder::LookupLabel(awaited, awaitedBytes, received, &refusal), refusal, false, true)) return; } @@ -152,7 +218,7 @@ void Driver::waitMemory(std::span packet, std::uint32_t que } if (const auto localDevice = gpuLock.owns_lock() ? device.Load() : std::shared_ptr{}) { - if (labelShortcut) { + if (labelTable) { Graphics::Recorder::LabelRefusal refusal{}; if (takeLabel(localDevice->PendingLabel(awaited, awaitedBytes, received, &refusal), refusal, false, false)) return; } @@ -182,7 +248,7 @@ void Driver::waitMemory(std::span packet, std::uint32_t que if (now - lastTry < PollTryInterval) return false; lastTry = now; } - if (changed && unlocked && labelShortcut) { + if (changed && unlocked && labelTable) { Graphics::Recorder::LabelRefusal refusal{}; if (takeLabel(Graphics::Recorder::LookupLabel(awaited, awaitedBytes, received, &refusal), refusal, true, true)) { @@ -220,7 +286,7 @@ void Driver::waitMemory(std::span packet, std::uint32_t que if (changed) { seenGeneration = generation; recheck = false; - if (labelShortcut) { + if (labelTable) { Graphics::Recorder::LabelRefusal refusal{}; if (takeLabel(localDevice->PendingLabel(awaited, awaitedBytes, received, &refusal), refusal, true, false)) { if (pollProfile) ++poll.tableHits; @@ -263,12 +329,18 @@ 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 (storedSince(packet, awaited, awaitedBytes, received)) return; + if (std::uint32_t writer = queue; !requireMemory && storedSince(packet, awaited, awaitedBytes, received, &writer)) { + if (writer != queue) notePendingWait(packet, queue, received); + else epochPoint.bump = false; + return; + } ++polls; if (spinning) { _mm_pause(); @@ -281,10 +353,20 @@ void Driver::waitMemory(std::span packet, std::uint32_t que } CheckFailure(); if (pollService(!spinning)) return; - if (const auto done = packetsDone.load(); done != lastDone || packetsInFlight.load() != 0) { + 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); + if (settleLock.owns_lock()) { + if (const auto localDevice = device.Load(); localDevice != nullptr && localDevice->RecordedWritesSettled(awaited, awaitedBytes)) return; + } + } + 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; @@ -295,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) return; + 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); - std::fprintf(stderr, "[gpu] queue 0x%x WAIT_REG_MEM%s at 0x%llx timed out after %dms (function %u ref 0x%llx mask 0x%llx value 0x%llx)\n", queue, wide ? "_64" : "", + 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)); @@ -321,7 +403,6 @@ void Driver::waitMemory(std::span packet, std::uint32_t que } context.Print(stderr, "[gpu] preceding packet %s\n"); } - if (warned) return; } ++outcomes.polled; } 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 fa5dc7530c..6e7a43a575 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -1,5 +1,7 @@ #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" #include "prx/libc/include/GuestAllocations.hpp" @@ -10,6 +12,8 @@ #include #include #include +#include +#include #include #include #include @@ -31,39 +35,13 @@ #include #include #include +#include #include #include #include +#include #include -#if __has_include() -#include -#endif -#if !defined(PROCMAP_QUERY) -struct procmap_query { - std::uint64_t size; - std::uint64_t query_flags; - std::uint64_t query_addr; - std::uint64_t vma_start; - std::uint64_t vma_end; - std::uint64_t vma_flags; - std::uint64_t vma_page_size; - std::uint64_t vma_offset; - std::uint64_t inode; - std::uint32_t dev_major; - std::uint32_t dev_minor; - std::uint32_t vma_name_size; - std::uint32_t build_id_size; - std::uint64_t vma_name_addr; - std::uint64_t build_id_addr; -}; -static_assert(sizeof(procmap_query) == 104); -enum : std::uint64_t { - PROCMAP_QUERY_VMA_READABLE = 0x01, - PROCMAP_QUERY_VMA_WRITABLE = 0x02, - PROCMAP_QUERY_COVERING_OR_NEXT_VMA = 0x10, -}; -#define PROCMAP_QUERY _IOWR('f', 17, struct procmap_query) -#endif +#include "prx/libc/include/specifics/linux/HostMapping.hpp" #endif namespace AgcDriver::GuestMemory { @@ -91,6 +69,15 @@ bool readableProtection(DWORD protection) { bool writableProtection(DWORD protection) { return protection == PAGE_READWRITE || protection == PAGE_WRITECOPY || protection == PAGE_EXECUTE_READWRITE || protection == PAGE_EXECUTE_WRITECOPY; } + +std::uintptr_t userAddressLimit() { + static const auto limit = [] { + SYSTEM_INFO info{}; + GetSystemInfo(&info); + return reinterpret_cast(info.lpMaximumApplicationAddress); + }(); + return limit; +} #endif } @@ -367,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 @@ -376,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; @@ -401,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) { @@ -414,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); @@ -434,6 +420,7 @@ struct PageSpan { struct PageStates { std::once_flag once; + std::atomic ready{false}; PageSpan arena; PageSpan image; @@ -449,6 +436,29 @@ struct PageStates { arena.forget(address, bytes); image.forget(address, bytes); } + + 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 (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 || 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); + } + } + } }; PageStates& Pages() { @@ -480,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(); @@ -501,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 { @@ -513,22 +559,6 @@ struct PageRun { bool writable; }; -#if !defined(_WIN32) -int ProcMapsQueryFd() { - static const int fd = [] { - if (std::getenv("APS5_NO_PROCMAP_QUERY") != nullptr) return -1; - const int opened = open("/proc/self/maps", O_RDONLY | O_CLOEXEC); - if (opened < 0) return -1; - procmap_query probe{}; - probe.size = sizeof(probe); - probe.query_flags = PROCMAP_QUERY_COVERING_OR_NEXT_VMA; - if (ioctl(opened, PROCMAP_QUERY, &probe) == 0 || errno == ENOENT) return opened; - close(opened); - return -1; - }(); - return fd; -} -#endif // Calls `emit` with consecutive runs of uniform accessibility covering [address, address + bytes) in // order, stopping early when it returns false. Returns false when the address space cannot be queried. @@ -541,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; } @@ -556,6 +586,11 @@ 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; + } if (VirtualQuery(reinterpret_cast(cursor), &memory, sizeof(memory)) != sizeof(memory)) return false; // Debug aid: APS5_TRACE_QUERY prints every 16th page query with its cost. static const bool traceQuery = std::getenv("APS5_TRACE_QUERY") != nullptr; @@ -575,19 +610,18 @@ 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); - for (PageSpan* span : {&pages.arena, &pages.image}) { - if (!readable || span->size == 0 || regionEnd <= span->base || base >= span->base + span->size) continue; - const std::uint8_t value = PageReadable | (writable ? PageWritable : 0u); - 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 ((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 - if (const int fd = ProcMapsQueryFd(); fd >= 0) { + 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, access); + }; + if (const int fd = HostMapping::QueryFd(); fd >= 0) { procmap_query query{}; query.size = sizeof(query); query.query_flags = PROCMAP_QUERY_COVERING_OR_NEXT_VMA; @@ -596,13 +630,16 @@ 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; } const bool readable = (query.vma_flags & PROCMAP_QUERY_VMA_READABLE) != 0; + const bool writable = readable && (query.vma_flags & PROCMAP_QUERY_VMA_WRITABLE) != 0; const auto next = std::min(end, query.vma_end); - if (!emit(PageRun{cursor, next, readable, readable && (query.vma_flags & PROCMAP_QUERY_VMA_WRITABLE) != 0})) return true; + recordRun(next, readable, writable); + if (!emit(PageRun{cursor, next, readable, writable})) return true; cursor = next; continue; } @@ -626,13 +663,17 @@ 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; break; } + const bool readable = permissions[0] == 'r'; + const bool writable = readable && permissions[1] == 'w'; const auto next = std::min(end, last); - if (!emit(PageRun{cursor, next, permissions[0] == 'r', permissions[0] == 'r' && permissions[1] == 'w'})) return true; + recordRun(next, readable, writable); + if (!emit(PageRun{cursor, next, readable, writable})) return true; cursor = next; found = true; break; @@ -671,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) { @@ -747,7 +811,22 @@ constexpr std::size_t WriteBlockBytes = 65536; constexpr std::size_t WritePageBytes = 4096; constexpr std::size_t WritePagesPerBlock = WriteBlockBytes / WritePageBytes; -enum class StampKind : std::uint8_t { Cpu, Driver, ImportWindow }; +enum class StampKind : std::uint8_t { Cpu, Driver, ImportWindow, Fresh }; + +constexpr std::array ZeroPage{}; + +struct WindowCopy { + static constexpr std::uint32_t Zero = std::numeric_limits::max(); + std::uint64_t first; + std::span bytes; + std::span slots; + + bool Holds(std::uintptr_t page) const { + const auto slot = slots[(page - first) / WritePageBytes]; + const auto* expected = slot == Zero ? ZeroPage.data() : bytes.data() + static_cast(slot) * WritePageBytes; + return std::memcmp(reinterpret_cast(page), expected, WritePageBytes) == 0; + } +}; #ifdef _WIN32 void watchPrivateMapping(std::uintptr_t address, std::size_t bytes, std::uint64_t generation); @@ -757,26 +836,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; - std::vector writtenBlocks; - WriteWatchCoverage coverage; -#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 writtenBlocks{}; + 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 @@ -808,9 +884,7 @@ 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); - writtenBlocks.assign(size / WriteBlockBytes + 1, 0); + leaves.resize((size / WriteBlockBytes + LeafBlocks) / LeafBlocks); coverage.Initialize(base, size); pages.resize(1u << 16); GuestArena::GuestArenaSetPrivateMappingObserver_nid_postfix(&watchPrivateMapping); @@ -822,7 +896,7 @@ struct WriteTracker { bool covers(std::uint64_t address, std::size_t bytes) const { #ifdef _WIN32 - return coverage.Covers(address, bytes); + return coverage.Covers(address, bytes) && !GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(static_cast(address), bytes); #else return address + bytes <= LeafCount * LeafBlocks * WriteBlockBytes && GuestWriteWatch::GuestWriteWatchCovers_nid_postfix(static_cast(address), bytes); #endif @@ -844,6 +918,35 @@ struct WriteTracker { #endif } +#ifdef _WIN32 + void startHints(std::uint64_t address, std::size_t bytes) { + const auto first = std::max(address, base); + const auto stop = std::min(address + bytes, base + size); + if (first >= stop) return; + bool started = false; + for (auto block = blockOf(first); block <= blockOf(stop - 1); ++block) { + if (!coverage.Covers(blockBegin(block), static_cast(std::min(WriteBlockBytes, base + size - blockBegin(block))))) continue; + if (!started) ++generation; + started = true; + 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 (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, hintOf(block)); + return newest; + } +#endif + struct DriverPiece { std::uint32_t generation = 0; std::uint32_t begin = 0; @@ -854,6 +957,20 @@ struct WriteTracker { std::uint32_t dropped = 0; std::uint32_t next = 0; std::array pieces{}; + + 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; @@ -861,13 +978,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); @@ -875,15 +1001,40 @@ struct WriteTracker { } } + bool blockPartWatched(std::uint64_t first, std::uint64_t stop) const { +#ifdef _WIN32 + return covers(first, 1) || covers(stop - 1, 1); +#else + return GuestWriteWatch::GuestWriteWatchOverlaps_nid_postfix(static_cast(first), static_cast(stop - first)); +#endif + } + + void stampWatchedPart(std::uint64_t address, std::uint64_t end, std::uint32_t stampGeneration) { +#ifdef _WIN32 + address = std::max(address, base); + end = std::min(end, base + size); + if (address >= end) return; +#else + end = std::min(end, LeafCount * LeafBlocks * WriteBlockBytes); + if (address >= end || !GuestWriteWatch::GuestWriteWatchOverlaps_nid_postfix(static_cast(address), static_cast(end - address))) return; +#endif + for (auto block = blockOf(address); block <= blockOf(end - 1); ++block) { + const auto first = std::max(address, blockBegin(block)); + const auto stop = std::min(end, blockBegin(block) + WriteBlockBytes); + if (!blockPartWatched(first, stop)) continue; + stamp(block, stampGeneration, StampKind::Driver); + noteDriverStore(block, address, end, stampGeneration); + } + } + void noteDriverStore(std::uint64_t block, std::uint64_t address, std::uint64_t end, std::uint32_t stampGeneration) { const auto begin = blockBegin(block); const auto from = static_cast(std::max(address, begin) - begin); 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]; + entry = DriverPieces{}; + entry.whole = stampGeneration; return; } auto& entry = driverPieces[block]; @@ -892,66 +1043,136 @@ struct WriteTracker { slot = {stampGeneration, from, to}; } - std::uint32_t stampOf(std::uint64_t block) const { + Leaf& leafFor(std::uint64_t block) { + auto& leaf = leaves[block / LeafBlocks]; + if (leaf == nullptr) leaf = std::make_unique(); + return *leaf; + } + #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; + } + + static constexpr std::size_t UnstampedSlots = 1u << 16; + std::vector unstampedStores; + + void noteUnstampedStore(std::uint64_t address, std::uint64_t end) { + if (unstampedStores.empty()) unstampedStores.assign(UnstampedSlots, 0); + const auto first = address / WriteBlockBytes; + const auto last = (end - 1) / WriteBlockBytes; + if (last - first >= UnstampedSlots) { + std::fill(unstampedStores.begin(), unstampedStores.end(), generation); + return; + } + for (auto block = first; block <= last; ++block) unstampedStores[block % UnstampedSlots] = generation; + } + + std::uint32_t newestStamp(std::uint64_t address, std::uint64_t end) const { + std::uint32_t newest = 0; + if (!unstampedStores.empty()) { + const auto first = address / WriteBlockBytes; + const auto last = (end - 1) / WriteBlockBytes; + if (last - first >= UnstampedSlots) newest = *std::max_element(unstampedStores.begin(), unstampedStores.end()); + else for (auto block = first; block <= last; ++block) newest = std::max(newest, unstampedStores[block % UnstampedSlots]); + } + if (!watched) return newest; +#ifdef _WIN32 + address = std::max(address, base); + end = std::min(end, base + size); +#else + end = std::min(end, LeafCount * LeafBlocks * WriteBlockBytes); #endif + if (address >= end) return newest; + for (auto block = blockOf(address); block <= blockOf(end - 1); ++block) newest = std::max(newest, stampOf(block)); + 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 } - std::uint32_t writtenStampOf(std::uint64_t block) const { -#ifdef _WIN32 - return writtenBlocks[block]; -#else - const auto& leaf = leaves[block / LeafBlocks]; - return leaf != nullptr ? leaf->writtenBlocks[block % LeafBlocks] : 0; -#endif + 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) return true; + for (const auto& piece : entry.pieces) { + 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::Driver; - const bool written = kind != StampKind::ImportWindow; -#ifdef _WIN32 - blocks[block] = stampGeneration; - if (cpu) cpuBlocks[block] = stampGeneration; - if (written) writtenBlocks[block] = stampGeneration; -#else - auto& leaf = leaves[block / LeafBlocks]; - if (leaf == nullptr) leaf = std::make_unique(); - leaf->blocks[block % LeafBlocks] = stampGeneration; - if (cpu) leaf->cpuBlocks[block % LeafBlocks] = stampGeneration; - if (written) leaf->writtenBlocks[block % LeafBlocks] = stampGeneration; -#endif + const bool cpu = kind == StampKind::Cpu || kind == StampKind::ImportWindow; + const bool stored = kind != StampKind::ImportWindow; + 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; } }; @@ -981,8 +1202,10 @@ void watchPrivateMapping(std::uintptr_t address, std::size_t bytes, std::uint64_ if (!tracker.watched || bytes == 0 || bytes > tracker.size || address < tracker.base || address - tracker.base > tracker.size - bytes) return; tracker.coverage.Restore(address, bytes, generation); ++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); @@ -996,6 +1219,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; @@ -1017,82 +1241,104 @@ 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; } -#ifndef _WIN32 struct StampRuns { WriteTracker& tracker; StampKind kind; + std::size_t stamped = 0; + const WindowCopy* window = nullptr; }; void stampWrittenRun(void* context, std::uintptr_t begin, std::uintptr_t end) { - auto& [tracker, kind] = *static_cast(context); + auto& runs = *static_cast(context); + auto& tracker = runs.tracker; + const auto kind = runs.kind; if (end <= begin) return; + ++runs.stamped; const auto generation = tracker.generation.load(std::memory_order_relaxed); if (kind != StampKind::Driver) tracker.noteCpuStore(begin, end, generation); - for (auto block = tracker.blockOf(begin); block <= tracker.blockOf(end - 1); ++block) tracker.stamp(block, generation, kind); + 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 { + for (auto block = tracker.blockOf(begin); block <= tracker.blockOf(end - 1); ++block) tracker.stamp(block, generation, kind); + } +#ifndef _WIN32 collectDirtyRuns.fetch_add(1, std::memory_order_relaxed); +#endif +} + +#ifdef _WIN32 +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) { + auto cursor = first; + while (cursor < stop) { + std::size_t count = tracker.pages.size(); + if (!collect(cursor, static_cast(stop - cursor), tracker.pages.data(), &count, clear)) return false; + for (std::size_t i = 0; i < count; ++i) { + const auto page = reinterpret_cast(tracker.pages[i]); + stampWrittenRun(&runs, page, page + WritePageBytes); + } + if (count < tracker.pages.size()) break; + cursor = reinterpret_cast(tracker.pages[count - 1]) + WritePageBytes; + } + return true; + }; + if (!walk(singlePass || fresh)) return false; + if (runs.stamped != 0) collectDirty.fetch_add(1, std::memory_order_relaxed); + return fresh || singlePass || runs.stamped == 0 || walk(true); } #endif -bool walkWrites(WriteTracker& tracker, std::uint64_t first, std::uint64_t stop, StampKind kind) { +bool walkWrites(WriteTracker& tracker, std::uint64_t first, std::uint64_t stop, StampKind kind, bool armedOnly = false, const WindowCopy* window = nullptr) { ++tracker.generation; + StampRuns runs{tracker, kind, 0, window}; #ifdef _WIN32 - constexpr std::uint64_t page = 4096; - // One resetting walk: the kernel reports and clears a page's dirty bit together, a write landing - // after the walk passed a page is reported by the next walk, and a clean page is not touched by the - // reset, so a probe pass first (which doubled the walk of every dirty range) buys nothing. - // APS5_NO_SINGLE_PASS_COLLECT=1 restores the probe pass followed by a resetting pass when dirty. - static const bool singlePass = std::getenv("APS5_NO_SINGLE_PASS_COLLECT") == nullptr; - auto cursor = first; - bool dirty = false; - while (cursor < stop) { - std::size_t count = tracker.pages.size(); - DWORD granularity = 4096; - if (!GuestArena::GuestArenaCollectWrites_nid_postfix(cursor, static_cast(stop - cursor), tracker.pages.data(), &count, singlePass)) { - // Uncommitted pages inside the range make the call fail; such ranges are compared instead. - return false; - } - if (count != 0) dirty = true; - for (ULONG_PTR i = 0; i < count; ++i) { - const auto page = reinterpret_cast(tracker.pages[i]); - tracker.stamp(tracker.blockOf(page), tracker.generation, kind); - if (kind != StampKind::Driver) tracker.noteCpuStore(page, page + WritePageBytes, tracker.generation); - } - if (count < tracker.pages.size()) break; - cursor = reinterpret_cast(tracker.pages[count - 1]) + (granularity != 0 ? granularity : page); - } - if (dirty) collectDirty.fetch_add(1, std::memory_order_relaxed); - if (dirty && !singlePass) { - // The resetting pass reports pages again, so a write racing the first pass is stamped too. - cursor = first; - while (cursor < stop) { - std::size_t count = tracker.pages.size(); - DWORD granularity = 4096; - if (!GuestArena::GuestArenaCollectWrites_nid_postfix(cursor, static_cast(stop - cursor), tracker.pages.data(), &count, true)) return false; - for (ULONG_PTR i = 0; i < count; ++i) { - const auto page = reinterpret_cast(tracker.pages[i]); - tracker.stamp(tracker.blockOf(page), tracker.generation, kind); - if (kind != StampKind::Driver) tracker.noteCpuStore(page, page + WritePageBytes, tracker.generation); - } - if (count < tracker.pages.size()) break; - cursor = reinterpret_cast(tracker.pages[count - 1]) + (granularity != 0 ? granularity : page); - } - } - return true; + return collectWindowsWrites(runs, first, stop, armedOnly); #else const auto dirtyRuns = collectDirtyRuns.load(std::memory_order_relaxed); - StampRuns runs{tracker, kind}; - const bool complete = GuestWriteWatch::GuestWriteWatchCollect_nid_postfix(static_cast(first), static_cast(stop - first), &stampWrittenRun, &runs); + const auto collect = armedOnly ? &GuestWriteWatch::GuestWriteWatchCollectArmed_nid_postfix : &GuestWriteWatch::GuestWriteWatchCollect_nid_postfix; + const bool complete = collect(static_cast(first), static_cast(stop - first), &stampWrittenRun, &runs); if (collectDirtyRuns.load(std::memory_order_relaxed) != dirtyRuns) collectDirty.fetch_add(1, std::memory_order_relaxed); return complete; #endif } std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoized) { + if (bytes == 0 || bytes > std::numeric_limits::max() - address) return 0; auto& tracker = Tracker(); // Whole pages, so a page shared with the next range is collected with either. constexpr std::uint64_t page = 4096; @@ -1106,6 +1352,13 @@ std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoi 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 && 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) { // The current generation, not the memoized one: blocks stamped since (MarkWritten, an @@ -1144,7 +1397,13 @@ std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoi 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}; + else { + threadCollectMemo.entries[threadCollectMemo.next++ % threadCollectMemo.entries.size()] = {first, stop, epoch, serial}; + if (epochCollectMapEnabled()) { + auto& entry = epochCollectMap(epoch).ranges[first]; + if (entry.unwatched != serial || entry.end < stop) entry = {first, stop, epoch, serial}; + } + } } return tracker.generation; } @@ -1161,9 +1420,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); } @@ -1174,9 +1448,30 @@ std::uint64_t CollectEpoch() { namespace { +std::atomic unwatchHook{nullptr}; + +void beforeUnwatch(std::uint64_t address, std::size_t 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) { if (!tracker.watched) return; #ifdef _WIN32 + tracker.startHints(address, bytes); if (tracker.coverage.Exclude(address, bytes, GuestArena::GuestArenaCommitGeneration_nid_postfix())) unwatchSerial.fetch_add(1, std::memory_order_release); #else if (GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(reinterpret_cast(address), bytes)) unwatchSerial.fetch_add(1, std::memory_order_release); @@ -1185,16 +1480,57 @@ void unwatchLocked(WriteTracker& tracker, std::uint64_t address, std::size_t byt } +void SetUnwatchHook(void (*hook)(std::uint64_t, std::size_t)) { + unwatchHook.store(hook, std::memory_order_release); +} + +#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); auto& tracker = Tracker(); const auto lock = lockTracker(tracker); tracker.initialize(); unwatchLocked(tracker, address, bytes); } -bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function& import) { +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) { + constexpr std::uint64_t page = 4096; + const auto first = address & ~(page - 1); + beforeUnwatch(first, static_cast(((address + bytes + page - 1) & ~(page - 1)) - first)); + } auto& tracker = Tracker(); auto lock = lockTracker(tracker); tracker.initialize(); @@ -1206,9 +1542,55 @@ 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)); - const bool before = covered && walkWrites(tracker, first, stop, StampKind::Cpu); - if (!import()) return false; - if (!before || !walkWrites(tracker, first, stop, StampKind::ImportWindow)) unwatchLocked(tracker, first, static_cast(stop - first)); + const auto runImport = [&] { +#ifdef __APPLE__ + const GuestArena::HostWrite open(reinterpret_cast(first), static_cast(stop - first)); +#endif + 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; + } + 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 slots(static_cast((stop - first) / page), WindowCopy::Zero); + std::size_t kept = 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) { + if (std::memcmp(reinterpret_cast(cursor), ZeroPage.data(), page) != 0) slots[static_cast((cursor - first) / page)] = static_cast(kept++); + } + } + std::vector saved; + saved.reserve(kept * page); + const auto* source = reinterpret_cast(first); + for (std::size_t index = 0; index < slots.size(); ++index) { + if (slots[index] != WindowCopy::Zero) saved.insert(saved.end(), source + index * page, source + (index + 1) * page); + } + 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, saved, slots}; + 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; } @@ -1220,6 +1602,7 @@ bool WriteWatched() { } bool Watched(std::uint64_t address, std::size_t bytes) { + if (bytes == 0 || bytes > std::numeric_limits::max() - address) return false; auto& tracker = Tracker(); const auto lock = lockTracker(tracker); tracker.initialize(); @@ -1227,13 +1610,12 @@ bool Watched(std::uint64_t address, std::size_t bytes) { } bool UnchangedSince(std::uint64_t address, std::size_t bytes, std::uint64_t generation) { + if (bytes == 0 || bytes > std::numeric_limits::max() - address) return false; auto& tracker = Tracker(); 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) { @@ -1243,9 +1625,31 @@ 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; +} + +std::uint64_t CollectCommittedWrites(std::uint64_t address, std::size_t bytes, std::vector>& ranges) { + ranges.clear(); + if (bytes == 0 || bytes > std::numeric_limits::max() - address || !WriteWatched()) return 0; + auto committed = CommittedRanges(address, bytes); + auto generation = TrackerGeneration(); + for (const auto& [begin, end] : committed) { + generation = CollectWrites(begin, static_cast(end - begin)); + if (generation == 0) return 0; + } + ranges = std::move(committed); + return generation; +} + +bool UnchangedCommittedSince(std::span> ranges, std::uint64_t generation) { + auto& tracker = Tracker(); + const auto lock = lockTracker(tracker); + tracker.initialize(); + if (!tracker.watched || generation == 0) return false; + for (const auto& [begin, end] : ranges) { + if (end <= begin || !tracker.covers(begin, static_cast(end - begin)) || tracker.newestStamp(begin, end) > generation) return false; } return true; } @@ -1255,7 +1659,11 @@ std::uint64_t storeOwn(std::uint64_t address, std::size_t bytes, const std::func const auto stampStored = [](WriteTracker& tracker, std::pair stored) -> std::uint64_t { if (stored.second <= stored.first) return 0; ++tracker.generation; - if (!tracker.watched || !tracker.covers(stored.first, static_cast(stored.second - stored.first))) return 0; + if (!tracker.watched || !tracker.covers(stored.first, static_cast(stored.second - stored.first))) { + tracker.noteUnstampedStore(stored.first, stored.second); + if (tracker.watched) tracker.stampWatchedPart(stored.first, stored.second, tracker.generation); + return 0; + } for (auto block = tracker.blockOf(stored.first); block <= tracker.blockOf(stored.second - 1); ++block) { tracker.stamp(block, tracker.generation, StampKind::Driver); tracker.noteDriverStore(block, stored.first, stored.second, tracker.generation); @@ -1269,9 +1677,6 @@ std::uint64_t storeOwn(std::uint64_t address, std::size_t bytes, const std::func constexpr std::uint64_t page = 4096; const auto first = address & ~(page - 1); const auto stop = (address + bytes + page - 1) & ~(page - 1); -#ifdef _WIN32 - if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(first, static_cast(stop - first))) return stampStored(tracker, store()); -#endif if (!walkWrites(tracker, first, stop, StampKind::Cpu)) return stampStored(tracker, store()); const auto stored = store(); walkWrites(tracker, first, stop, StampKind::Driver); @@ -1296,11 +1701,34 @@ std::uint64_t StoreOwnBytes(std::uint64_t address, std::span so std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes) { 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)) return 0; + if (!tracker.watched || !tracker.covers(address, bytes)) { + tracker.noteUnstampedStore(address, address + bytes); + if (tracker.watched) tracker.stampWatchedPart(address, address + bytes, tracker.generation); + return 0; + } + { + const auto blockFirst = address & ~(std::uint64_t{WriteBlockBytes} - 1); + const auto blockStop = (address + bytes + WriteBlockBytes - 1) & ~(std::uint64_t{WriteBlockBytes} - 1); + StampRuns runs{tracker, StampKind::Fresh}; +#ifdef _WIN32 + const bool complete = collectWindowsWrites(runs, blockFirst, blockStop, false); +#else + const bool complete = GuestWriteWatch::GuestWriteWatchCollectFresh_nid_postfix(static_cast(blockFirst), static_cast(blockStop - blockFirst), &stampWrittenRun, &runs); +#endif + if (!complete) { + lock.unlock(); + beforeUnwatch(blockFirst, static_cast(blockStop - blockFirst)); + lock.lock(); + unwatchLocked(tracker, blockFirst, static_cast(blockStop - blockFirst)); + tracker.noteUnstampedStore(address, address + bytes); + return 0; + } + if (runs.stamped != 0) ++tracker.generation; + } const auto first = tracker.blockOf(address); const auto last = tracker.blockOf(address + bytes - 1); for (auto block = first; block <= last; ++block) { @@ -1315,33 +1743,37 @@ bool StoredOver(std::uint64_t address, std::size_t bytes, std::uint64_t generati const auto lock = lockTracker(tracker); tracker.initialize(); if (!tracker.watched || generation == 0 || bytes == 0 || !tracker.covers(address, bytes)) return true; +#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.writtenStampOf(block) <= generation) continue; - if (tracker.cpuStampOf(block) > generation) { - const auto begin = tracker.blockBegin(block); - const auto from = std::max(address, begin) - begin; - const auto to = std::min(end, begin + WriteBlockBytes) - begin; - const auto found = tracker.cpuPages.find(block); - if (found == tracker.cpuPages.end()) return true; - for (auto page = from / WritePageBytes; page <= (to - 1) / WritePageBytes; ++page) { - if (found->second.pages[page] > 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() { @@ -1352,6 +1784,67 @@ std::uint64_t TrackerGeneration() { return tracker.generation; } +std::uint64_t NewestStamp(std::uint64_t address, std::size_t bytes) { + auto& tracker = Tracker(); + const auto lock = lockTracker(tracker); + tracker.initialize(); + return bytes == 0 ? 0 : tracker.newestStamp(address, address + bytes); +} + +#ifdef _WIN32 +namespace { + +thread_local ThreadCollectMemo threadHintMemo; + +bool walkHints(WriteTracker& tracker, std::uint64_t first, std::uint64_t stop) { + auto cursor = first; + bool marked = false; + while (cursor < stop) { + std::size_t count = tracker.pages.size(); + if (!GuestArena::GuestArenaCollectWrites_nid_postfix(static_cast(cursor), static_cast(stop - cursor), tracker.pages.data(), &count, true)) return false; + if (count != 0 && !marked) ++tracker.generation; + marked = marked || count != 0; + for (std::size_t i = 0; i < count; ++i) tracker.stampHint(tracker.blockOf(reinterpret_cast(tracker.pages[i]))); + if (count < tracker.pages.size()) break; + cursor = reinterpret_cast(tracker.pages[count - 1]) + WritePageBytes; + } + if (marked) collectDirty.fetch_add(1, std::memory_order_relaxed); + return true; +} + +} +#endif + +std::uint64_t UntrackedHint(std::uint64_t address, std::size_t bytes) { +#ifdef _WIN32 + if (bytes == 0 || bytes > std::numeric_limits::max() - address) return 0; + constexpr std::uint64_t page = 4096; + const auto first = address & ~(page - 1); + const auto stop = (address + bytes + page - 1) & ~(page - 1); + const auto epoch = currentCollectEpoch(); + auto& tracker = Tracker(); + const auto lock = lockTracker(tracker); + tracker.initialize(); + if (!tracker.watched || !tracker.hinted(first, stop)) return 0; + const auto unwatched = unwatchSerial.load(std::memory_order_relaxed); + const bool memoized = collectMemoEnabled() && std::any_of(threadHintMemo.entries.begin(), threadHintMemo.entries.end(), [&](const WriteTracker::Memo& entry) { + return entry.epoch == epoch && entry.unwatched == unwatched && entry.begin <= first && stop <= entry.end; + }); + if (memoized) { + collectMemoHits.fetch_add(1, std::memory_order_relaxed); + } else { + const TimedAccess timed(CounterCollect, bytes); + if (!walkHints(tracker, first, stop)) return 0; + if (collectMemoEnabled()) threadHintMemo.entries[threadHintMemo.next++ % threadHintMemo.entries.size()] = {first, stop, epoch, unwatched}; + } + return tracker.newestHint(first, stop); +#else + static_cast(address); + static_cast(bytes); + return 0; +#endif +} + bool UnchangedSinceCollected(std::uint64_t address, std::size_t bytes, std::uint64_t generation) { if (CollectWritesUncached(address, bytes) == 0) return false; auto& tracker = Tracker(); @@ -1359,8 +1852,18 @@ bool UnchangedSinceCollected(std::uint64_t address, std::size_t bytes, std::uint 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); + const auto end = address + bytes; for (auto block = first; block <= last; ++block) { - if (tracker.cpuStampOf(block) > generation) return false; + 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 false; + } + } else if (tracker.cpuStampOf(block) > generation) { + return false; + } } return true; } @@ -1373,7 +1876,7 @@ bool WrittenSince(std::uint64_t address, std::size_t bytes, std::uint64_t genera const auto first = tracker.blockOf(address); const auto last = tracker.blockOf(address + bytes - 1); for (auto block = first; block <= last; ++block) { - if (tracker.writtenStampOf(block) > generation) return true; + if (tracker.stampOf(block) > generation) return true; } return false; } @@ -1391,7 +1894,7 @@ bool ChangedBlocks(std::uint64_t address, std::size_t bytes, std::span= generations.size() || k >= changed.size()) break; const auto generation = generations[k]; - changed[k] = generation == 0 || tracker.writtenStampOf(block) > generation ? BlockWritten : tracker.stampOf(block) > generation ? BlockMaybeWritten : BlockUnchanged; + changed[k] = generation == 0 || tracker.stampOf(block) > generation ? BlockWritten : BlockUnchanged; if (k < cpu.size()) cpu[k] = generation == 0 || tracker.cpuStampOf(block) > generation ? 1 : 0; } return true; @@ -1545,11 +2048,13 @@ const void* ThreadToken() { thread_local char token; return &token; } + +std::atomic gpuHoldSerial{0}; } void GpuMutexType::acquired() { owner.store(ThreadToken(), std::memory_order_relaxed); - ++depth; + if (++depth == 1) gpuHoldSerial.fetch_add(1, std::memory_order_relaxed); } bool GpuMutexType::try_lock() { @@ -1580,6 +2085,10 @@ void SetGpuUnlockHook(void (*hook)()) { gpuUnlockHook.store(hook, std::memory_order_release); } +std::uint64_t GpuHoldSerial() { + return gpuHoldSerial.load(std::memory_order_relaxed); +} + void GpuMutexType::unlock() { const bool outermost = --depth == 0; if (outermost) { @@ -1613,7 +2122,17 @@ void AssertGpuLockHeld(const char* where) { void GpuMutexType::lock() { if (!GpuLockProfiled()) { - mutex.lock(); + auto* trace = PerformanceTrace::Recording() ? PerformanceTrace::Thread() : nullptr; + if (trace == nullptr) { + 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(); + LockSpinning(mutex, HandoffSpinWindow()); + trace->Add("GpuMutex.wait", GpuLockSiteNames[site], start, std::chrono::steady_clock::now()); + } + } acquired(); return; } @@ -1628,9 +2147,10 @@ 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); if (holders) { HolderMatrixValues after{}; Holders().Snapshot(after); @@ -1704,7 +2224,7 @@ void GpuMutexType::lock() { void TagGpuLockSite(GpuLockSite site) { // Off the profiled path the tag is never read, so the thread-local is not touched either. - if (!GpuLockProfiled()) return; + if (!GpuLockProfiled() && !PerformanceTrace::Recording()) return; LockStats().nextSite = site; } @@ -1743,10 +2263,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); +} + +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) { @@ -1755,6 +2291,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; + GuardRead(address, destination.size()); FlushGpuWrites(address, destination.size()); const TimedAccess timed(CounterRead, destination.size()); CountReadCaller(__builtin_return_address(0)); @@ -1766,14 +2303,29 @@ 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()); } -void ReadCommitted(std::uint64_t address, std::span destination) { - if (destination.empty()) return; - FlushGpuWrites(address, destination.size()); - const TimedAccess timed(CounterRead, destination.size()); - CountReadCaller(__builtin_return_address(0)); +#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) { if (Accessible(reinterpret_cast(address), destination.size())) { std::memcpy(destination.data(), reinterpret_cast(address), destination.size()); return; @@ -1782,6 +2334,25 @@ void ReadCommitted(std::uint64_t address, std::span destination) { for (const auto& [begin, end] : CommittedRanges(address, destination.size())) std::memcpy(destination.data() + (begin - address), reinterpret_cast(begin), static_cast(end - begin)); } +} + +void ReadCommitted(std::uint64_t address, std::span destination) { + if (destination.empty()) return; + GuardRead(address, destination.size()); + FlushGpuWrites(address, destination.size()); + const TimedAccess timed(CounterRead, destination.size()); + CountReadCaller(__builtin_return_address(0)); + copyCommitted(address, destination); +} + +void ReadCommittedUnsynced(std::uint64_t address, std::span destination) { + if (destination.empty()) return; + GuardRead(address, destination.size()); + const TimedAccess timed(CounterRead, destination.size()); + CountReadCaller(__builtin_return_address(0)); + copyCommitted(address, destination); +} + bool EqualsCommitted(std::uint64_t address, std::span bytes) { if (bytes.empty()) return true; FlushGpuWrites(address, bytes.size()); @@ -1846,28 +2417,29 @@ void WriteChanged(std::uint64_t address, std::span current, std const TimedAccess timed(CounterChangedWrite, current.size()); auto* destination = reinterpret_cast(address); CheckRange(destination, current.size(), 1, true); - constexpr std::size_t block = 256; const auto size = current.size(); - const auto differs = [&](std::size_t at) { - const auto length = std::min(block, size - at); - return std::memcmp(current.data() + at, original.data() + at, length) != 0; - }; // Stamped like a GPU write: a collect memoized for this packet would not see the page fault. storeOwn(address, size, [&] { std::size_t firstChanged = size; std::size_t lastChanged = 0; - for (std::size_t at = 0; at < size; at += block) { - if (!differs(at)) continue; - const auto blockEnd = std::min(at + block, size); - for (std::size_t run = at; run < blockEnd;) { - if (current[run] == original[run]) { ++run; continue; } - auto runEnd = run + 1; - while (runEnd < blockEnd && current[runEnd] != original[runEnd]) ++runEnd; - std::memcpy(destination + run, current.data() + run, runEnd - run); - firstChanged = std::min(firstChanged, run); - lastChanged = std::max(lastChanged, runEnd); - run = runEnd; - } + std::size_t at = 0; + for (; at + 16 <= size; at += 16) { + const auto bytes = _mm_loadu_si128(reinterpret_cast(current.data() + at)); + const auto same = _mm_cmpeq_epi8(bytes, _mm_loadu_si128(reinterpret_cast(original.data() + at))); + const auto equal = static_cast(_mm_movemask_epi8(same)); + if (equal == 0xffffu) continue; + if (equal == 0) _mm_storeu_si128(reinterpret_cast<__m128i*>(destination + at), bytes); + else _mm_maskmoveu_si128(bytes, _mm_xor_si128(same, _mm_set1_epi8(-1)), reinterpret_cast(destination + at)); + const auto changed = ~equal & 0xffffu; + firstChanged = std::min(firstChanged, at + static_cast(std::countr_zero(changed))); + lastChanged = std::max(lastChanged, at + 32 - static_cast(std::countl_zero(changed))); + } + _mm_sfence(); + for (; at < size; ++at) { + if (current[at] == original[at]) continue; + destination[at] = current[at]; + firstChanged = std::min(firstChanged, at); + lastChanged = std::max(lastChanged, at + 1); } return firstChanged < lastChanged ? std::pair{address + firstChanged, address + lastChanged} : std::pair{0, 0}; }); @@ -1892,3 +2464,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 new file mode 100644 index 0000000000..e9578a2e7c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp @@ -0,0 +1,389 @@ +#include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp" +#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" +#include "prx/libSceAgcDriver/Execution/include/TraceFile.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/TraceRevision.hpp" +#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#include +#include +#endif +#if defined(_MSC_VER) && !defined(__clang__) +#include +#elif defined(__x86_64__) || defined(__i386__) +#include +#endif + +#ifndef _WIN32 +extern char** environ; +#endif + +namespace AgcDriver { + +namespace { + +using Clock = std::chrono::steady_clock; +constexpr std::size_t GpuRangeCapacity = TraceThread::DefaultCapacity; + +struct Session { + std::mutex mutex; + std::vector> threads; + bool started = false; + std::optional window; + bool windowOpen = false; + std::vector> windowBase; + std::optional interval; + bool intervalOpen = false; + std::uint64_t intervalFirstPresent = 0; + double intervalGpuBusy = 0; + std::vector> intervalBase; + std::vector intervalPresents; + Clock::time_point first{}; + std::uint64_t present = 0; + std::uint64_t firstPresent = 0; + double gpuBusyAtOpen = 0; + std::optional gpuClock; + std::vector presents; + bool bench = false; + TraceBenchIntervals benchIntervals; + std::uint64_t benchDropped = 0; +}; + +Session& session() { + static auto& state = *new Session(); + return state; +} + +std::unique_ptr& writer() { + static std::unique_ptr output; + return output; +} + +std::unique_ptr& summaryWriter() { + static std::unique_ptr output; + 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 : "."); +} + +std::uint64_t processId() { +#ifdef _WIN32 + return GetCurrentProcessId(); +#else + return static_cast(::getpid()); +#endif +} + +std::string operatingSystem() { +#ifdef _WIN32 + using QueryVersion = LONG(WINAPI*)(PRTL_OSVERSIONINFOW); + static const auto query = reinterpret_cast(reinterpret_cast(::GetProcAddress(::GetModuleHandleW(L"ntdll.dll"), "RtlGetVersion"))); + RTL_OSVERSIONINFOW version{}; + version.dwOSVersionInfoSize = sizeof(version); + if (query == nullptr || query(&version) != 0) throw std::runtime_error("RtlGetVersion failed"); + return "Windows " + std::to_string(version.dwMajorVersion) + "." + std::to_string(version.dwMinorVersion) + "." + std::to_string(version.dwBuildNumber); +#else + utsname name{}; + if (::uname(&name) != 0) throw std::system_error(errno, std::generic_category(), "uname"); + return std::string(name.sysname) + " " + name.release; +#endif +} + +std::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; +#else + char** entries = environ; +#endif + std::vector> result; + for (; entries != nullptr && *entries != nullptr; ++entries) { + const std::string_view entry = *entries; + const auto equals = entry.find('='); + if (!entry.starts_with("APS5_") || equals == std::string_view::npos) continue; + std::string value(entry.substr(equals + 1)); + if (value.find_first_of("/\\") != std::string::npos) value = ""; + result.emplace_back(std::string(entry.substr(0, equals)), std::move(value)); + } + std::sort(result.begin(), result.end()); + return result; +} + +TraceSummary identity(std::uint32_t width, std::uint32_t height, const VulkanDevice* device) { + TraceSummary summary; + summary.commit = APS5_TRACE_COMMIT; + if (APS5_TRACE_DIRTY >= 0) summary.dirty = APS5_TRACE_DIRTY != 0; + summary.environment = environment(); + summary.gpu = device != nullptr ? device->DeviceName() : std::string(); + summary.driver = device != nullptr ? device->DriverInfo() : std::string(); + summary.os = operatingSystem(); + summary.width = width; + summary.height = height; + summary.host = host(); + static const auto running = title(); + summary.title = running; + return summary; +} + +std::vector> totalsOf(const std::vector>& threads) { + std::vector> result; + for (const auto& thread : threads) result.push_back(thread->Collect().totals); + return result; +} + +double seconds(Clock::duration elapsed) { + return std::chrono::duration(elapsed).count(); +} + +} + +void PerformanceTrace::BindThread(const char* name, std::size_t capacity) { + if (!Configured() || local != nullptr) return; + 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); + local = bound.get(); + state.threads.push_back(std::move(bound)); +} + +void PerformanceTrace::OpenInterval(Clock::time_point now, std::uint64_t present) { + auto& state = session(); + state.interval->Restart(now - state.first); + state.intervalOpen = true; + state.intervalFirstPresent = present; + state.intervalGpuBusy = VulkanDevice::PresentCounts().gpuBusyMs; + state.intervalPresents.assign(1, now); + if (summaryWriter()) return; + summaryWriter() = std::make_unique([directory = traceDirectory()](std::string_view text) { + try { + std::filesystem::create_directories(directory); + TraceFile::AppendLine(directory / "aps5-summary.jsonl", text); + } catch (const std::exception& error) { + std::fprintf(stderr, "[trace] appending to aps5-summary.jsonl failed: %s\n", error.what()); + std::fflush(stderr); + throw; + } + }, std::numeric_limits::max()); + ProfilePrint_nid_no_patch("[trace] summarizing %s from present %llu\n", state.interval->Describe().c_str(), static_cast(present)); +} + +void PerformanceTrace::FlushInterval(Clock::time_point now, std::uint32_t width, std::uint32_t height, const VulkanDevice* device) { + auto& state = session(); + state.intervalBase.resize(state.threads.size()); + std::vector snapshots; + for (std::size_t index = 0; index < state.threads.size(); ++index) { + auto snapshot = state.threads[index]->Collect(); + auto totals = std::move(snapshot.totals); + snapshot.slices = {}; + snapshot.totals = TraceFormat::Since(totals, state.intervalBase[index]); + state.intervalBase[index] = std::move(totals); + snapshots.push_back(std::move(snapshot)); + } + auto summary = identity(width, height, device); + summary.window = state.interval->Describe(); + summary.firstPresent = state.intervalFirstPresent; + summary.gpuBusyMs = VulkanDevice::PresentCounts().gpuBusyMs - state.intervalGpuBusy; + summary.interval = std::make_pair(seconds(state.intervalPresents.front() - state.first), seconds(now - state.first)); + summaryWriter()->Write(TraceFormat::Summary(summary, snapshots, state.intervalPresents)); + 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) { + const auto now = Clock::now(); + BindThread("presenter"); + auto& state = session(); + std::lock_guard lock(state.mutex); + 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; + } + const auto present = state.present++; + const auto edge = state.window ? state.window->Present(present, now - state.first) : TraceWindow::Edge::None; + if (state.interval) { + if (!state.intervalOpen) { + OpenInterval(now, present); + } else { + state.intervalPresents.push_back(now); + if (state.interval->Due(now - state.first)) { + FlushInterval(now, width, height, device); + OpenInterval(now, present); + } + } + } + if (edge == TraceWindow::Edge::Open) { + state.windowOpen = true; + state.windowBase = totalsOf(state.threads); + state.firstPresent = present; + state.gpuBusyAtOpen = VulkanDevice::PresentCounts().gpuBusyMs; + TraceClockSample clock; + if (device != nullptr && device->CalibrateClock(clock.gpuNs, clock.cpu)) state.gpuClock = clock; + { + std::lock_guard gpu(gpuMutex); + gpuRanges.clear(); + gpuLost = 0; + gpuRanges.reserve(GpuRangeCapacity); + } + state.presents.push_back(now); + presentCount.fetch_add(1, std::memory_order_relaxed); + buffering.store(true, std::memory_order_relaxed); + recording.store(true, std::memory_order_relaxed); + ProfilePrint_nid_no_patch("[trace] recording %s from present %llu%s\n", state.window->Describe().c_str(), static_cast(present), state.gpuClock ? "" : "; no GPU track: the device has no calibrated timestamps"); + return; + } + if (!state.windowOpen) { + if (!state.interval) return; + presentCount.fetch_add(1, std::memory_order_relaxed); + recording.store(true, std::memory_order_relaxed); + return; + } + state.presents.push_back(now); + presentCount.fetch_add(1, std::memory_order_relaxed); + if (edge != TraceWindow::Edge::Close) return; + state.windowOpen = false; + buffering.store(false, std::memory_order_relaxed); + recording.store(state.interval.has_value(), std::memory_order_relaxed); + + std::vector snapshots; + std::size_t presenter = 0; + state.windowBase.resize(state.threads.size()); + for (std::size_t index = 0; index < state.threads.size(); ++index) { + if (state.threads[index].get() == local) presenter = index; + snapshots.push_back(state.threads[index]->Collect()); + snapshots.back().totals = TraceFormat::Since(snapshots.back().totals, state.windowBase[index]); + } + std::vector gpuSlices; + TraceClockSample clock; + 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, {}, gpuLost != 0, gpuLost}); + } + const auto process = processId(); + auto summary = identity(width, height, device); + summary.window = state.window->Describe(); + summary.firstPresent = state.firstPresent; + summary.gpuBusyMs = VulkanDevice::PresentCounts().gpuBusyMs - state.gpuBusyAtOpen; + summary.trace = "aps5-trace-" + summary.commit + "-" + std::to_string(process) + ".json.gz"; + auto events = TraceFormat::Events(process, state.presents.front(), snapshots, presenter, state.presents, state.firstPresent); + auto& output = writer(); + output = std::make_unique([directory = traceDirectory(), name = summary.trace, line = TraceFormat::Summary(summary, snapshots, state.presents)](std::string_view text) { + try { + std::filesystem::create_directories(directory); + TraceFile::WriteGzip(directory / name, text); + TraceFile::AppendLine(directory / "aps5-summary.jsonl", line); + std::fprintf(stderr, "[trace] wrote %s and its line in aps5-summary.jsonl\n", name.c_str()); + std::fflush(stderr); + } catch (const std::exception& error) { + std::fprintf(stderr, "[trace] writing %s failed: %s\n", name.c_str(), error.what()); + std::fflush(stderr); + throw; + } + }, std::numeric_limits::max()); + ProfilePrint_nid_no_patch("[trace] closed at present %llu, writing %s (%zu bytes of JSON)\n", static_cast(present), summary.trace.c_str(), events.size()); + output->Write(std::move(events)); +} + +void PerformanceTrace::StopOutput() { + if (writer()) writer()->Stop(); + 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 14072414bc..f4a9dbeca6 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/PreciseWait.hpp" #include #include #include @@ -71,7 +72,7 @@ bool IndirectDrawsDisabled() { // nowhere) or a user-data register of the vertex-side stage banks (0x8c.. for the vertex / geometry // programs, 0x10c.. for the local / hull programs). bool drawLocation(std::uint32_t value) { - return value == 0x280u || value - 0x8cu < 32u || value - 0x10cu < 32u; + return value == 0 || value == 0x280u || value - 0x8cu < 32u || value - 0x10cu < 32u; } Registers& registersFor(QueueState& queue, std::uint32_t opcode) { @@ -113,15 +114,14 @@ std::vector copySource(std::uint64_t source, std::size_t bytes, bool return data; } -constexpr std::uint32_t DmaSelectGds = 1; 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); } @@ -131,16 +131,42 @@ 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; +constexpr std::uint32_t UserConfigRegisterBase = 0xc000; + +std::uint32_t copyDataCount(std::span packet) { + return (packet[1] & 0x10000u) != 0 ? 2u : 1u; +} 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)); @@ -149,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) { @@ -168,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; } @@ -187,7 +218,7 @@ std::string_view UnsupportedReason(std::uint32_t header) { case 0x11: case 0x12: case 0x13: case 0x15: case 0x16: case 0x20: case 0x22: case 0x26: case 0x27: case 0x2a: case 0x2d: case 0x2f: case 0x35: case 0x37: case 0x40: case 0x42: case 0x45: case 0x46: case 0x50: case 0x58: case 0x63: case 0x64: case 0x69: case 0x76: case 0x79: case 0x7a: - case 0x81: case 0x83: case 0x9f: return {}; + case 0x81: case 0x83: case 0x91: case 0x9f: case 0x28: return {}; case 0x24: case 0x25: case 0x2c: case 0x38: if (IndirectDrawsDisabled()) return "graphics draw, shader stages and guest render-target materialization are not implemented"; return {}; @@ -204,7 +235,7 @@ std::string_view UnsupportedReason(std::uint32_t header) { case 0x43: case 0x47: case 0x48: return "guest cache actions, GPU events and interrupt delivery are not implemented"; case 0x8e: return "GPU LOD statistics are not implemented; synthetic results are forbidden"; - case 0x28: case 0x41: case 0x68: case 0x78: + case 0x41: case 0x68: case 0x78: return "opcode is named but has no handler in the reference dispatch table"; default: return "opcode is not known in the reference"; } @@ -238,14 +269,14 @@ void Validate(std::span packet, std::uint32_t queue) { require((packet[1] & 0xffff0000u) != 0x68750000u, "typed user-data and legacy flip markers are not implemented"); break; case 0x09: size(2); break; - case 0x06: graphics(); size(4); require(packet[3] == 0, "unsupported rendering wait mode"); break; + case 0x06: size(4); require(packet[3] == 0, "unsupported rendering wait mode"); break; case 0x0b: { const auto data = std::as_bytes(packet.subspan(1)); require(std::find(data.begin(), data.end(), std::byte{}) != data.end(), "unterminated marker text"); 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"); @@ -259,13 +290,13 @@ void Validate(std::span packet, std::uint32_t queue) { // neither changes what the packet writes. const auto flags = header & 0xffu; const bool registerWrite = opcode == 0x69 || opcode == 0x76 || opcode == 0x79 || opcode == 0x7a; - auto allowedFlags = opcode == 0x11 ? 2u : registerWrite ? 6u : opcode == 0x3c || opcode == 0x93 ? 2u : 0u; + auto allowedFlags = opcode == 0x11 || opcode == 0x15 || opcode == 0x16 ? 2u : registerWrite ? 6u : opcode == 0x3c || opcode == 0x93 ? 2u : 0u; if (opcode != 0x20 && opcode != 0x22) allowedFlags |= 1u; if ((flags & ~allowedFlags) != 0) throw std::runtime_error("PM4 header flags 0x" + ToHex(flags) + " are not implemented"); switch (opcode) { case 0x11: size(4); - require(packet[1] == 1 && (packet[2] & 7u) == 0 && packet[3] <= 0xffffu, "unsupported indirect base index, alignment or address bits"); + require(packet[1] == 1 && (packet[2] & 3u) == 0 && packet[3] <= 0xffffu, "unsupported indirect base index, alignment or address bits"); if ((header & 2u) == 0) graphics(); break; case 0x12: graphics(); size(2); require((packet[1] & ~0xfu) == 0, "unsupported CLEAR_STATE payload bits"); break; @@ -290,6 +321,13 @@ void Validate(std::span packet, std::uint32_t queue) { require((packet[5] & ~0x20u) == 0, "unsupported indexed draw flags"); break; case 0x2a: graphics(); size(2); require(packet[1] <= 3, "unsupported index-type modifiers"); break; + case 0x91: + graphics(); + size(4); + require((packet[1] & 1u) == 0, "INDEX_ATTRIBUTES_INDIRECT indirect mode (addr_offset) is not implemented"); + require((packet[1] & 0xeu) == 0, "INDEX_ATTRIBUTES_INDIRECT attribute base is not 16-byte aligned"); + require((packet[3] >> 16u) == 0, "INDEX_ATTRIBUTES_INDIRECT reserved attribute index bits are set"); + break; case 0x2d: graphics(); size(3); @@ -321,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] & ~0xa020u) != 0x41u) 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() & ~0xa020u) == 0x41u, "indirect dispatch modifiers are not implemented"); + require((packet.back() & ~0xa06cu) == 0x1u, "indirect dispatch modifiers are not implemented"); break; case 0x22: size(5); @@ -334,10 +372,17 @@ void Validate(std::span packet, std::uint32_t queue) { require((packet[4] & ~ConditionalWordsMask) == 0, "COND_EXEC reserved count bits are not implemented"); break; case 0x42: size(2); require(packet[1] == 0, "unsupported PFP_SYNC_ME payload"); break; + case 0x28: + graphics(); + size(3); + 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); @@ -349,6 +394,20 @@ void Validate(std::span packet, std::uint32_t queue) { size(2); require(eventIndex == 0 || eventIndex == 7, "invalid cache-flush event index"); break; + case 0x19: case 0x1a: case 0x26: + size(2); + require(eventIndex == 0, "invalid pipeline statistics or SQ event index"); + break; + case 0x24: + graphics(); + 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); @@ -364,11 +423,11 @@ void Validate(std::span packet, std::uint32_t queue) { const auto controlMask = packet.size() == 8 ? 0x86287fc3u : 0xfeecfffbu; require((packet[1] & ~controlMask) == 0, "unsupported ACQUIRE_MEM control flags"); require(queue == 0 || (packet[1] & 0x06287fc3u) == 0, "graphics cache operation in compute queue"); - require(packet[3] == 0 && packet[5] == 0, "ACQUIRE_MEM ranges above 40 bits are not implemented"); + require(packet[3] <= 0xffu && packet[5] <= 0xffu, "invalid ACQUIRE_MEM range high bits"); require(packet[6] <= 0xffffu, "invalid ACQUIRE_MEM poll interval"); - const auto base = static_cast(packet[4]) << 8u; - const auto bytes = static_cast(packet[2]) << 8u; - require(bytes <= (1ull << 40u) - base, "ACQUIRE_MEM range exceeds 40-bit address space"); + const auto base = ((static_cast(packet[5]) << 32u) | packet[4]) << 8u; + const auto bytes = ((static_cast(packet[3]) << 32u) | packet[2]) << 8u; + require(bytes <= (1ull << 48u) - base, "ACQUIRE_MEM range exceeds 48-bit address space"); if (packet.size() == 8) { require((packet[7] & ~0x3ffffu) == 0, "unsupported ACQUIRE_MEM GCR flags"); require((packet[7] & 0x2000u) == 0, "ACQUIRE_MEM cache discard is not implemented"); @@ -378,13 +437,13 @@ void Validate(std::span packet, std::uint32_t queue) { case 0x63: case 0x64: case 0x9f: if (opcode != 0x63) graphics(); size(5); - require((packet[1] & 3u) == 0 && packet[3] == 0x80000000u && packet[4] <= 0x3fffu, "unsupported indirect-register address or control fields"); + require((packet[1] & 3u) == 0 && packet[4] <= 0x3fffu && (packet[3] == 0x80000000u || ((packet[3] & ~0xffffu) == 0 && packet[4] <= 0x10000u - packet[3])), "unsupported indirect-register address or control fields"); break; case 0x69: case 0x76: case 0x79: case 0x7a: { if (IsTagMarker(packet)) break; if (opcode != 0x76) graphics(); require(packet.size() >= 3, "register packet has no values"); - if (opcode == 0x7a) require((packet[1] & 0xf0000000u) == 0 || (packet.size() == 3 && packet[1] == 0x20000243u), "indexed register bank selection is not implemented"); + if (opcode == 0x7a) require((packet[1] & 0xf0000000u) == 0 || (packet.size() == 3 && (packet[1] == 0x20000243u || packet[1] == 0x10000242u)), "indexed register bank selection is not implemented"); const auto offset = registerOffset(packet[1]); require(packet.size() - 2 <= 0x10000u - offset, "register range overflow"); break; @@ -397,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: { @@ -428,28 +486,41 @@ 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; - require(destination == 1 || destination == 2 || (queue != 0 && destination == 5), "WRITE_DATA register or GDS destination is not implemented"); + 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; } 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; - require(destination == 2 || destination == 4, "COPY_DATA register or GDS 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"); + 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 || 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, "DMA_DATA register or prefetch destination 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"); } @@ -481,20 +552,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); } @@ -503,11 +581,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; @@ -546,6 +619,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. @@ -562,36 +636,125 @@ 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) 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; - 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 > queue.constantRam.size() || packet[2] > queue.constantRam.size() - packet[1] / 4) return std::nullopt; - const auto words = std::span(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; } } +std::uint32_t DmaSource(std::span packet) { + return dmaSource(packet); +} + +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; @@ -606,7 +769,7 @@ bool UsesGpuCacheBarrier(std::span packet) { bool AccessesMemory(std::uint32_t header) { switch ((header >> 8u) & 0xffu) { - case 0x22: case 0x27: case 0x3c: case 0x93: case 0x49: case 0x16: case 0x2d: case 0x35: case 0x24: case 0x25: case 0x2c: case 0x38: case 0x37: case 0x40: case 0x45: case 0x50: case 0x63: case 0x64: case 0x83: case 0x9f: return true; + case 0x22: case 0x27: case 0x3c: case 0x93: case 0x49: case 0x16: case 0x2d: case 0x35: case 0x24: case 0x25: case 0x2c: case 0x38: case 0x37: case 0x40: case 0x45: case 0x50: case 0x63: case 0x64: case 0x83: case 0x91: case 0x9f: return true; default: return false; } } @@ -618,6 +781,26 @@ std::uint64_t DispatchArgumentAddress(std::span packet, con return queue.dispatchIndirectBase + packet[1]; } +std::array PartialGroupThreads(const std::array& groups, const std::array& numThreads) { + std::array threads{}; + bool partial = false; + bool empty = false; + for (std::size_t axis = 0; axis < 3; ++axis) { + const auto full = numThreads[axis] & 0xffffu; + const auto last = numThreads[axis] >> 16u; + require(last != 0 && last <= full, "COMPUTE_NUM_THREAD partial group size is zero or exceeds the full group"); + partial |= last != full; + if (groups[axis] == 0) { + empty = true; + continue; + } + const auto total = static_cast(groups[axis] - 1) * full + last; + require(total <= std::numeric_limits::max(), "partial thread group dispatch size exceeds 32 bits"); + threads[axis] = static_cast(total); + } + return partial && !empty ? threads : std::array{}; +} + std::array ReadDispatchArguments(std::uint64_t arguments, std::uint32_t initiator) { std::array result{0xc0031500u, 0, 0, 0, initiator}; // Named for the [hooksync] attribution (the read goes through the flush hook). @@ -655,9 +838,10 @@ DrawParameters resolveIndirectDraw(std::span packet, const indirect.count = multi ? packet[5] : 1; indirect.countIndirect = multi && (packet[4] & (1u << 30u)) != 0; indirect.countAddress = multi ? address(packet[6], packet[7]) : 0; - indirect.baseVertexLocation = packet[2]; - indirect.startInstanceLocation = packet[3]; - indirect.drawIndexLocation = multi ? packet[4] & 0xffffu : 0x280u; + const auto location = [](std::uint32_t value) { return value == 0 ? 0x280u : value; }; + indirect.baseVertexLocation = location(packet[2]); + indirect.startInstanceLocation = location(packet[3]); + indirect.drawIndexLocation = multi ? location(packet[4] & 0xffffu) : 0x280u; indirect.drawIndexEnabled = multi && (packet[4] >> 31u) != 0; indirect.indxOffset = 0; require(indirect.count <= (std::numeric_limits::max() - indirect.arguments - indirect.recordBytes) / indirect.stride, "indirect draw record range overflow"); @@ -669,13 +853,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; @@ -735,26 +916,94 @@ DrawParameters ResolveDraw(std::span packet, const QueueSta std::vector ReadIndirectRegisters(std::span packet) { require(packet.size() == 5 && IndirectRegisterOpcode((packet[0] >> 8u) & 0xffu), "expected indirect register packet"); - std::vector pairs(static_cast(packet[4]) * 2); // Named for the [hooksync] attribution (the read goes through the flush hook). const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::Registers); + if (packet[3] != 0x80000000u) { + std::vector values(packet[4]); + GuestMemory::Read(address(packet[1], packet[2]), std::as_writable_bytes(std::span(values)), 4); + std::vector pairs; + pairs.reserve(values.size() * 2); + for (std::size_t i = 0; i < values.size(); ++i) { + pairs.push_back(packet[3] + static_cast(i)); + pairs.push_back(values[i]); + } + return pairs; + } + std::vector pairs(static_cast(packet[4]) * 2); GuestMemory::Read(address(packet[1], packet[2]), std::as_writable_bytes(std::span(pairs)), 4); 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"); } } +namespace { + +constexpr std::uint64_t IndexAttributesBytes = 16; + +struct IndexAttributes { + std::uint64_t address; + std::array words; +}; + +IndexAttributes readIndexAttributes(std::span packet) { + require(packet.size() == 4 && ((packet[0] >> 8u) & 0xffu) == 0x91, "expected INDEX_ATTRIBUTES_INDIRECT packet"); + require((packet[1] & 0xfu) == 0, "INDEX_ATTRIBUTES_INDIRECT indirect mode or reserved base bits are not implemented"); + const auto base = address(packet[1], packet[2]); + const auto offset = static_cast(packet[3] & 0xffffu) * IndexAttributesBytes; + require(offset <= std::numeric_limits::max() - base, "INDEX_ATTRIBUTES_INDIRECT record address overflow"); + IndexAttributes attributes{base + offset, {}}; + const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::IndirectArguments); + GuestMemory::Read(attributes.address, std::as_writable_bytes(std::span(attributes.words)), IndexAttributesBytes); + return attributes; +} + +void executeIndexAttributes(const IndexAttributes& attributes, QueueState& queue) { + if (attributes.words[3] > 3) { + char what[160]; + std::snprintf(what, sizeof(what), "INDEX_ATTRIBUTES_INDIRECT record at 0x%llx has index type 0x%x (index-type modifiers are not implemented)", static_cast(attributes.address), attributes.words[3]); + throw std::runtime_error(what); + } + queue.indexBase = address(attributes.words[0], attributes.words[1]); + queue.indexBufferSize = attributes.words[2]; + queue.indexType = attributes.words[3]; +} + +} + void Execute(std::span packet, QueueState& queue) { const auto opcode = (packet[0] >> 8u) & 0xffu; switch (opcode) { @@ -764,8 +1013,7 @@ void Execute(std::span packet, QueueState& queue) { case 0x09: queue = std::move(*std::make_unique()); return; case 0x0b: queue.markers.emplace_back(reinterpret_cast(packet.data() + 1)); return; case 0x0c: - require(!queue.markers.empty(), "marker stack underflow"); - queue.markers.pop_back(); + if (!queue.markers.empty()) queue.markers.pop_back(); return; case 0x1a: if (TraceContextState()) std::fprintf(stderr, "[context] op %u: %zu registers, saved %d, cb0 %x info %x mask %x\n", packet[1], queue.context.size(), queue.savedContext.has_value() ? 1 : 0, queue.context.contains(0x318) ? queue.context.at(0x318) : 0u, queue.context.contains(0x31c) ? queue.context.at(0x31c) : 0u, queue.context.contains(0x8e) ? queue.context.at(0x8e) : 0u); @@ -802,6 +1050,7 @@ void Execute(std::span packet, QueueState& queue) { case 0x3f: throw std::runtime_error(std::string(UnsupportedReason(packet[0]))); case 0x26: queue.indexBase = address(packet[1], packet[2]); return; case 0x2a: queue.indexType = packet[1]; return; + case 0x91: executeIndexAttributes(readIndexAttributes(packet), queue); return; case 0x2f: queue.instanceCount = packet[1]; return; case 0x63: case 0x64: case 0x9f: ExecuteIndirectRegisters(packet, ReadIndirectRegisters(packet), queue); @@ -812,11 +1061,11 @@ void Execute(std::span packet, QueueState& queue) { const auto start = std::chrono::steady_clock::now(); bool warned = false; while (!WaitSatisfied(packet)) { - std::this_thread::sleep_for(std::chrono::microseconds(50)); + PreciseSleepUs(50); 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; @@ -841,21 +1090,29 @@ void Execute(std::span packet, QueueState& queue) { return; } case 0x81: - std::copy(packet.begin() + 2, packet.end(), queue.constantRam.begin() + packet[1] / 4); + 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(std::span(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); @@ -863,15 +1120,24 @@ void Execute(std::span packet, QueueState& queue) { return; } case 0x40: { + if (CopyDataToRegister(packet)) { + std::array values{}; + const auto count = copyDataCount(packet); + const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::Registers); + GuestMemory::Read(address(packet[2], packet[3]), std::as_writable_bytes(std::span(values).first(count)), 4); + for (std::uint32_t i = 0; i < count; ++i) writeRegister(queue, 0x69, packet[4] - ContextRegisterBase + i, values[i]); + 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) 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 new file mode 100644 index 0000000000..e42e4e2b03 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp @@ -0,0 +1,364 @@ +#include "prx/libSceAgcDriver/Execution/include/PresentationPass.hpp" +#include "prx/libSceAgcDriver/Execution/include/AspectFit.hpp" +#include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" +#include "prx/libSceAgcDriver/Execution/include/PqToSdr.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Graphics/shaders/PresentationPassFrag_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/PresentationPassVert_spv.h" +#include +#include +#include +#include + +namespace AgcDriver { +namespace { + +constexpr std::uint32_t ModeTenBit = 1; +constexpr std::uint32_t ModeRedLow = 2; +constexpr std::uint32_t ModeKeepTenBit = 4; +constexpr std::uint32_t ModeSubsample = 8; +constexpr std::uint32_t ModePq = 16; + +} + +PresentationPass::PresentationPass(const Graphics::Context& context, std::size_t slotCount) : context(context), slots(slotCount) { + Graphics::Require(slotCount != 0, "presentation pass needs at least one slot"); + try { + VkSamplerCreateInfo samplerInfo{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO}; + samplerInfo.magFilter = VK_FILTER_NEAREST; + samplerInfo.minFilter = VK_FILTER_NEAREST; + samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; + samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + Graphics::Check(context.Function("vkCreateSampler")(context.device, &samplerInfo, nullptr, &sampler), "vkCreateSampler presentation pass"); + const std::array bindings{{{0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_FRAGMENT_BIT, &sampler}, {1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_FRAGMENT_BIT, nullptr}}}; + VkDescriptorSetLayoutCreateInfo descriptorInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + descriptorInfo.bindingCount = static_cast(bindings.size()); + descriptorInfo.pBindings = bindings.data(); + Graphics::Check(context.Function("vkCreateDescriptorSetLayout")(context.device, &descriptorInfo, nullptr, &descriptorLayout), "vkCreateDescriptorSetLayout presentation pass"); + const VkPushConstantRange push{VK_SHADER_STAGE_FRAGMENT_BIT, 0, 36}; + VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layoutInfo.setLayoutCount = 1; + layoutInfo.pSetLayouts = &descriptorLayout; + layoutInfo.pushConstantRangeCount = 1; + layoutInfo.pPushConstantRanges = &push; + Graphics::Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &pipelineLayout), "vkCreatePipelineLayout presentation pass"); + const std::array sizes{{{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, static_cast(slotCount)}, {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, static_cast(slotCount)}}}; + VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + poolInfo.maxSets = static_cast(slotCount); + poolInfo.poolSizeCount = static_cast(sizes.size()); + poolInfo.pPoolSizes = sizes.data(); + Graphics::Check(context.Function("vkCreateDescriptorPool")(context.device, &poolInfo, nullptr, &descriptorPool), "vkCreateDescriptorPool presentation pass"); + for (auto& slot : slots) { + VkDescriptorSetAllocateInfo allocation{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + allocation.descriptorPool = descriptorPool; + allocation.descriptorSetCount = 1; + allocation.pSetLayouts = &descriptorLayout; + Graphics::Check(context.Function("vkAllocateDescriptorSets")(context.device, &allocation, &slot.set), "vkAllocateDescriptorSets presentation pass"); + } + const auto& luminance = PqEotfNits(); + pqNits = std::make_unique(context, sizeof(luminance), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + std::memcpy(pqNits->Bytes().data(), luminance.data(), sizeof(luminance)); + const VkDescriptorBufferInfo tableInfo{pqNits->Handle(), 0, sizeof(luminance)}; + std::vector tableWrites(slots.size(), VkWriteDescriptorSet{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}); + for (std::size_t i = 0; i < slots.size(); ++i) { + tableWrites[i].dstSet = slots[i].set; + tableWrites[i].dstBinding = 1; + tableWrites[i].descriptorCount = 1; + tableWrites[i].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + tableWrites[i].pBufferInfo = &tableInfo; + } + context.Function("vkUpdateDescriptorSets")(context.device, static_cast(tableWrites.size()), tableWrites.data(), 0, nullptr); + 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; + } +} + +PresentationPass::~PresentationPass() { + release(); +} + +void PresentationPass::release() noexcept { + for (std::size_t i = 0; i < slots.size(); ++i) { + Release(i); + releaseDumpTarget(slots[i]); + } + for (const auto& [key, entry] : targets) { + if (entry.pipeline) context.Function("vkDestroyPipeline")(context.device, entry.pipeline, nullptr); + if (entry.renderPass) context.Function("vkDestroyRenderPass")(context.device, entry.renderPass, nullptr); + } + targets.clear(); + if (fragment) context.Function("vkDestroyShaderModule")(context.device, fragment, nullptr); + if (vertex) context.Function("vkDestroyShaderModule")(context.device, vertex, nullptr); + if (descriptorPool) context.Function("vkDestroyDescriptorPool")(context.device, descriptorPool, nullptr); + if (pipelineLayout) context.Function("vkDestroyPipelineLayout")(context.device, pipelineLayout, nullptr); + if (descriptorLayout) context.Function("vkDestroyDescriptorSetLayout")(context.device, descriptorLayout, nullptr); + if (sampler) context.Function("vkDestroySampler")(context.device, sampler, nullptr); + pqNits.reset(); + fragment = vertex = VK_NULL_HANDLE; + descriptorPool = VK_NULL_HANDLE; + pipelineLayout = VK_NULL_HANDLE; + descriptorLayout = VK_NULL_HANDLE; + sampler = VK_NULL_HANDLE; +} + +bool PresentationPass::TenBitFormat(VkFormat format) { + return format == VK_FORMAT_A2B10G10R10_UNORM_PACK32 || format == VK_FORMAT_A2R10G10B10_UNORM_PACK32; +} + +VkExtent2D PresentationPass::DumpExtent(std::uint32_t width, std::uint32_t height, std::uint32_t stride) { + Graphics::Require(stride != 0, "presentation dump stride must be non-zero"); + return {(width + stride - 1) / stride, (height + stride - 1) / stride}; +} + +void PresentationPass::Release(std::size_t index) noexcept { + if (index >= slots.size()) return; + auto& slot = slots[index]; + for (const auto& [view, framebuffer] : slot.transient) { + if (framebuffer) context.Function("vkDestroyFramebuffer")(context.device, framebuffer, nullptr); + if (view) context.Function("vkDestroyImageView")(context.device, view, nullptr); + } + slot.transient.clear(); + if (slot.source) context.Function("vkDestroyImageView")(context.device, slot.source, nullptr); + slot.source = VK_NULL_HANDLE; +} + +VkImageView PresentationPass::createView(VkImage image, VkFormat format, VkImageUsageFlags usage, std::uint32_t layer) const { + VkImageViewUsageCreateInfo usageInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; + usageInfo.usage = usage; + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.pNext = &usageInfo; + viewInfo.image = image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = format; + viewInfo.components = {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY}; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, layer, 1}; + VkImageView view = VK_NULL_HANDLE; + Graphics::Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView presentation pass"); + return view; +} + +VkFramebuffer PresentationPass::createFramebuffer(VkRenderPass renderPass, VkImageView view, VkExtent2D extent) const { + VkFramebufferCreateInfo info{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; + info.renderPass = renderPass; + info.attachmentCount = 1; + info.pAttachments = &view; + info.width = extent.width; + info.height = extent.height; + info.layers = 1; + VkFramebuffer framebuffer = VK_NULL_HANDLE; + Graphics::Check(context.Function("vkCreateFramebuffer")(context.device, &info, nullptr, &framebuffer), "vkCreateFramebuffer presentation pass"); + return framebuffer; +} + +const PresentationPass::Target& PresentationPass::target(VkFormat format, VkImageLayout finalLayout) { + const auto key = std::pair{format, finalLayout}; + if (const auto found = targets.find(key); found != targets.end()) return found->second; + VkFormatProperties properties{}; + context.formatProperties(context.physical, format, &properties); + Graphics::Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0, "presentation pass format is not renderable"); + Target created; + try { + VkAttachmentDescription attachment{}; + attachment.format = format; + attachment.samples = VK_SAMPLE_COUNT_1_BIT; + attachment.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attachment.finalLayout = finalLayout; + const VkAttachmentReference reference{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL}; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &reference; + const bool transfer = finalLayout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + const VkSubpassDependency dependencies[2] = { + {VK_SUBPASS_EXTERNAL, 0, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, 0}, + {0, VK_SUBPASS_EXTERNAL, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, transfer ? VK_PIPELINE_STAGE_TRANSFER_BIT : VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, transfer ? VK_ACCESS_TRANSFER_READ_BIT : VkAccessFlags{0}, 0}, + }; + VkRenderPassCreateInfo renderPassInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + renderPassInfo.attachmentCount = 1; + renderPassInfo.pAttachments = &attachment; + renderPassInfo.subpassCount = 1; + renderPassInfo.pSubpasses = &subpass; + renderPassInfo.dependencyCount = 2; + renderPassInfo.pDependencies = dependencies; + Graphics::Check(context.Function("vkCreateRenderPass")(context.device, &renderPassInfo, nullptr, &created.renderPass), "vkCreateRenderPass presentation pass"); + + VkPipelineShaderStageCreateInfo stages[2]{{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}, {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}}; + stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; + stages[0].module = vertex; + stages[0].pName = "main"; + stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; + stages[1].module = fragment; + stages[1].pName = "main"; + VkPipelineVertexInputStateCreateInfo vertexInput{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO}; + VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO}; + assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + VkPipelineViewportStateCreateInfo viewport{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO}; + viewport.viewportCount = 1; + viewport.scissorCount = 1; + VkPipelineRasterizationStateCreateInfo rasterization{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO}; + rasterization.polygonMode = VK_POLYGON_MODE_FILL; + rasterization.cullMode = VK_CULL_MODE_NONE; + rasterization.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + rasterization.lineWidth = 1.0f; + VkPipelineMultisampleStateCreateInfo multisample{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; + multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + VkPipelineColorBlendAttachmentState blendAttachment{}; + blendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; + blend.attachmentCount = 1; + blend.pAttachments = &blendAttachment; + const VkDynamicState dynamicStates[2] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; + VkPipelineDynamicStateCreateInfo dynamic{VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO}; + dynamic.dynamicStateCount = 2; + dynamic.pDynamicStates = dynamicStates; + VkGraphicsPipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO}; + pipelineInfo.stageCount = 2; + pipelineInfo.pStages = stages; + pipelineInfo.pVertexInputState = &vertexInput; + pipelineInfo.pInputAssemblyState = &assembly; + pipelineInfo.pViewportState = &viewport; + pipelineInfo.pRasterizationState = &rasterization; + pipelineInfo.pMultisampleState = &multisample; + pipelineInfo.pColorBlendState = &blend; + pipelineInfo.pDynamicState = &dynamic; + pipelineInfo.layout = pipelineLayout; + pipelineInfo.renderPass = created.renderPass; + Graphics::Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &created.pipeline), "vkCreateGraphicsPipelines presentation pass"); + } catch (...) { + if (created.renderPass) context.Function("vkDestroyRenderPass")(context.device, created.renderPass, nullptr); + throw; + } + return targets.emplace(key, created).first->second; +} + +void PresentationPass::Bind(std::size_t index, VkImage image, std::uint32_t width, std::uint32_t height, std::uint64_t pixelFormat, float whiteNits, std::uint32_t layer) { + Graphics::Require(index < slots.size(), "presentation pass slot is out of range"); + Graphics::Require(image != VK_NULL_HANDLE && width != 0 && height != 0, "presentation pass source is unavailable"); + Graphics::Require(!DisplayHalfFloat(pixelFormat), "presentation pass does not decode 16-bit float display buffers"); + Graphics::Require(std::isfinite(whiteNits) && whiteNits >= 1.0f, "presentation pass white level must be at least 1 nit"); + Release(index); + auto& slot = slots[index]; + slot.source = createView(image, VK_FORMAT_R32_UINT, VK_IMAGE_USAGE_SAMPLED_BIT, layer); + slot.width = width; + slot.height = height; + slot.mode = (DisplayTenBit(pixelFormat) ? ModeTenBit : 0u) | (DisplayRedLow(pixelFormat) ? ModeRedLow : 0u) | (DisplayPq(pixelFormat) ? ModePq : 0u); + slot.whiteNits = whiteNits; + const VkDescriptorImageInfo imageInfo{sampler, slot.source, VK_IMAGE_LAYOUT_GENERAL}; + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstSet = slot.set; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write.pImageInfo = &imageInfo; + context.Function("vkUpdateDescriptorSets")(context.device, 1, &write, 0, nullptr); +} + +void PresentationPass::draw(VkCommandBuffer commands, const Slot& slot, const Target& entry, VkFramebuffer framebuffer, VkExtent2D extent, const std::int32_t (¶meters)[9]) const { + VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + begin.renderPass = entry.renderPass; + begin.framebuffer = framebuffer; + begin.renderArea = {{0, 0}, extent}; + context.Function("vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); + context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, entry.pipeline); + const VkViewport viewport{0.0f, 0.0f, static_cast(extent.width), static_cast(extent.height), 0.0f, 1.0f}; + const VkRect2D scissor{{0, 0}, extent}; + context.Function("vkCmdSetViewport")(commands, 0, 1, &viewport); + context.Function("vkCmdSetScissor")(commands, 0, 1, &scissor); + context.Function("vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &slot.set, 0, nullptr); + context.Function("vkCmdPushConstants")(commands, pipelineLayout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(parameters), parameters); + context.Function("vkCmdDraw")(commands, 3, 1, 0, 0); + context.Function("vkCmdEndRenderPass")(commands); +} + +void PresentationPass::RecordPresent(VkCommandBuffer commands, std::size_t index, VkImage destination, VkFormat format, 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]; + const auto& entry = target(format, finalLayout); + const auto view = createView(destination, format, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | (finalLayout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL ? VK_IMAGE_USAGE_TRANSFER_SRC_BIT : 0u)); + VkFramebuffer framebuffer = VK_NULL_HANDLE; + try { + framebuffer = createFramebuffer(entry.renderPass, view, extent); + } catch (...) { + context.Function("vkDestroyImageView")(context.device, view, nullptr); + throw; + } + slot.transient.emplace_back(view, framebuffer); + const auto rect = ComputeContainRect_nid_postfix(slot.width, slot.height, extent.width, extent.height); + const 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); +} + +void PresentationPass::releaseDumpTarget(Slot& slot) noexcept { + if (slot.dumpFramebuffer) context.Function("vkDestroyFramebuffer")(context.device, slot.dumpFramebuffer, nullptr); + if (slot.dumpView) context.Function("vkDestroyImageView")(context.device, slot.dumpView, nullptr); + if (slot.dumpImage) context.Function("vkDestroyImage")(context.device, slot.dumpImage, nullptr); + if (slot.dumpMemory) context.Function("vkFreeMemory")(context.device, slot.dumpMemory, nullptr); + slot.dumpFramebuffer = VK_NULL_HANDLE; + slot.dumpView = VK_NULL_HANDLE; + slot.dumpImage = VK_NULL_HANDLE; + slot.dumpMemory = VK_NULL_HANDLE; + slot.dumpExtent = {}; +} + +void PresentationPass::ensureDumpTarget(Slot& slot, VkExtent2D extent) { + if (slot.dumpImage != VK_NULL_HANDLE && slot.dumpExtent.width == extent.width && slot.dumpExtent.height == extent.height) return; + releaseDumpTarget(slot); + try { + VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + imageInfo.imageType = VK_IMAGE_TYPE_2D; + imageInfo.format = VK_FORMAT_B8G8R8A8_UNORM; + imageInfo.extent = {extent.width, extent.height, 1u}; + imageInfo.mipLevels = 1; + imageInfo.arrayLayers = 1; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imageInfo.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Graphics::Check(context.Function("vkCreateImage")(context.device, &imageInfo, nullptr, &slot.dumpImage), "vkCreateImage presentation dump"); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, slot.dumpImage, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + Graphics::Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &slot.dumpMemory), "vkAllocateMemory presentation dump"); + Graphics::Check(context.Function("vkBindImageMemory")(context.device, slot.dumpImage, slot.dumpMemory, 0), "vkBindImageMemory presentation dump"); + slot.dumpView = createView(slot.dumpImage, VK_FORMAT_B8G8R8A8_UNORM, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + slot.dumpFramebuffer = createFramebuffer(target(VK_FORMAT_B8G8R8A8_UNORM, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL).renderPass, slot.dumpView, extent); + slot.dumpExtent = extent; + } catch (...) { + releaseDumpTarget(slot); + throw; + } +} + +void PresentationPass::RecordDump(VkCommandBuffer commands, std::size_t index, std::uint32_t stride, VkBuffer destination) { + Graphics::Require(index < slots.size() && slots[index].source != VK_NULL_HANDLE, "presentation pass slot has no source"); + Graphics::Require(destination != VK_NULL_HANDLE, "presentation dump buffer is unavailable"); + auto& slot = slots[index]; + const auto extent = DumpExtent(slot.width, slot.height, stride); + ensureDumpTarget(slot, extent); + const auto& entry = target(VK_FORMAT_B8G8R8A8_UNORM, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + const std::int32_t parameters[9] = {0, 0, static_cast(extent.width), static_cast(extent.height), static_cast(slot.width), static_cast(slot.height), static_cast(stride), static_cast(slot.mode | ModeSubsample), std::bit_cast(slot.whiteNits)}; + draw(commands, slot, entry, slot.dumpFramebuffer, extent, parameters); + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {extent.width, extent.height, 1u}; + context.Function("vkCmdCopyImageToBuffer")(commands, slot.dumpImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destination, 1, ©); + Graphics::RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp index 354b2f0cbd..d7c88d7b7d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp @@ -17,6 +17,8 @@ PresentationScaler::PresentationScaler(const Graphics::Context& context, VkForma PresentationScaler::~PresentationScaler() { release(); + if (blackImage != VK_NULL_HANDLE) context.Function("vkDestroyImage")(context.device, blackImage, nullptr); + if (blackMemory != VK_NULL_HANDLE) context.Function("vkFreeMemory")(context.device, blackMemory, nullptr); } void PresentationScaler::release() noexcept { @@ -102,7 +104,7 @@ void PresentationScaler::RecordBlitInto(VkCommandBuffer commands, VkImage image, RecordBlitInto(commands, image, layout, filter, sourceWidth, sourceHeight); } -void PresentationScaler::RecordBlitInto(VkCommandBuffer commands, VkImage image, VkImageLayout layout, VkFilter filter, std::uint32_t width, std::uint32_t height) { +void PresentationScaler::RecordBlitInto(VkCommandBuffer commands, VkImage image, VkImageLayout layout, VkFilter filter, std::uint32_t width, std::uint32_t height, std::uint32_t layer) { Graphics::Require(sourceImage != VK_NULL_HANDLE && image != VK_NULL_HANDLE, "presentation image is unavailable"); Graphics::Require(width != 0 && height != 0, "presentation blit extent must be non-zero"); const auto pipelineBarrier = context.Function("vkCmdPipelineBarrier"); @@ -116,9 +118,9 @@ void PresentationScaler::RecordBlitInto(VkCommandBuffer commands, VkImage image, barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; pipelineBarrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); VkImageBlit blit{}; - blit.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + blit.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, layer, 1}; blit.srcOffsets[1] = {static_cast(width), static_cast(height), 1}; - blit.dstSubresource = blit.srcSubresource; + blit.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; blit.dstOffsets[1] = {static_cast(sourceWidth), static_cast(sourceHeight), 1}; context.Function("vkCmdBlitImage")(commands, image, layout, sourceImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, filter); barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; @@ -142,11 +144,11 @@ void PresentationScaler::RecordBlit(VkCommandBuffer commands, VkImage destinatio RecordBlitFrom(context, commands, sourceImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, sourceWidth, sourceHeight, VK_FILTER_LINEAR, destinationImage, destinationWidth, destinationHeight); } -void PresentationScaler::RecordBlitFrom(const Graphics::Context& context, VkCommandBuffer commands, VkImage image, VkImageLayout layout, std::uint32_t width, std::uint32_t height, VkFilter filter, VkImage destinationImage, std::uint32_t destinationWidth, std::uint32_t destinationHeight) { +void PresentationScaler::RecordBlitFrom(const Graphics::Context& context, VkCommandBuffer commands, VkImage image, VkImageLayout layout, std::uint32_t width, std::uint32_t height, VkFilter filter, VkImage destinationImage, std::uint32_t destinationWidth, std::uint32_t destinationHeight, std::uint32_t layer) { Graphics::Require(image != VK_NULL_HANDLE && width != 0 && height != 0, "presentation blit source is unavailable"); const auto rect = ComputeContainRect_nid_postfix(width, height, destinationWidth, destinationHeight); VkImageBlit blit{}; - blit.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + blit.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, layer, 1}; blit.srcOffsets[0] = {0, 0, 0}; blit.srcOffsets[1] = {static_cast(width), static_cast(height), 1}; blit.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; @@ -155,4 +157,82 @@ void PresentationScaler::RecordBlitFrom(const Graphics::Context& context, VkComm context.Function("vkCmdBlitImage")(commands, image, layout, destinationImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, filter); } +std::vector PresentationScaler::LetterboxBands(std::uint32_t width, std::uint32_t height, std::uint32_t destinationWidth, std::uint32_t destinationHeight) { + const auto rect = ComputeContainRect_nid_postfix(width, height, destinationWidth, destinationHeight); + const auto right = rect.x + static_cast(rect.width); + const auto bottom = rect.y + static_cast(rect.height); + std::vector bands; + if (rect.y > 0) bands.push_back({0, 0, destinationWidth, static_cast(rect.y)}); + if (bottom < static_cast(destinationHeight)) bands.push_back({0, bottom, destinationWidth, destinationHeight - static_cast(bottom)}); + if (rect.x > 0) bands.push_back({0, rect.y, static_cast(rect.x), rect.height}); + if (right < static_cast(destinationWidth)) bands.push_back({right, rect.y, destinationWidth - static_cast(right), rect.height}); + return bands; +} + +void PresentationScaler::RecordLetterbox(VkCommandBuffer commands, std::uint32_t width, std::uint32_t height, VkImage destinationImage, std::uint32_t destinationWidth, std::uint32_t destinationHeight) { + const auto bands = LetterboxBands(width, height, destinationWidth, destinationHeight); + if (bands.empty()) return; + const auto pipelineBarrier = context.Function("vkCmdPipelineBarrier"); + if (blackImage == VK_NULL_HANDLE) { + VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + imageInfo.imageType = VK_IMAGE_TYPE_2D; + imageInfo.format = VK_FORMAT_B8G8R8A8_UNORM; + imageInfo.extent = {1u, 1u, 1u}; + imageInfo.mipLevels = 1; + imageInfo.arrayLayers = 1; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + Graphics::Check(context.Function("vkCreateImage")(context.device, &imageInfo, nullptr, &image), "vkCreateImage letterbox"); + try { + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + Graphics::Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory letterbox"); + Graphics::Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory letterbox"); + } catch (...) { + context.Function("vkDestroyImage")(context.device, image, nullptr); + if (memory != VK_NULL_HANDLE) context.Function("vkFreeMemory")(context.device, memory, nullptr); + throw; + } + blackImage = image; + blackMemory = memory; + VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = blackImage; + barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + pipelineBarrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + VkClearColorValue black{}; + black.float32[3] = 1.0f; + context.Function("vkCmdClearColorImage")(commands, blackImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &barrier.subresourceRange); + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + pipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + } + std::vector regions; + regions.reserve(bands.size()); + for (const auto& band : bands) { + VkImageBlit blit{}; + blit.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + blit.srcOffsets[1] = {1, 1, 1}; + blit.dstSubresource = blit.srcSubresource; + blit.dstOffsets[0] = {band.x, band.y, 0}; + blit.dstOffsets[1] = {band.x + static_cast(band.width), band.y + static_cast(band.height), 1}; + regions.push_back(blit); + } + context.Function("vkCmdBlitImage")(commands, blackImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destinationImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast(regions.size()), regions.data(), VK_FILTER_NEAREST); +} + } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Screenshot.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Screenshot.cpp new file mode 100644 index 0000000000..6a930d4e30 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Screenshot.cpp @@ -0,0 +1,77 @@ +#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() { + const char* configured = std::getenv("ANYPS5_SCREENSHOTS"); + 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 5eec16c82a..660a9c007c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp @@ -30,11 +30,12 @@ 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"); Require(bytes != nullptr && bytes->storageBuffer8BitAccess == VK_TRUE, "shader runtime requires enabled storageBuffer8BitAccess"); - Require(nullDescriptors, "shader runtime requires enabled nullDescriptor"); Require(core.vertexPipelineStoresAndAtomics == VK_TRUE && core.fragmentStoresAndAtomics == VK_TRUE, "shader runtime requires enabled graphics stores and atomics"); Require(limits.maxPushConstantsSize >= ShaderRecompiler::RuntimeAbi::PushConstantDwords * sizeof(std::uint32_t) && limits.maxBoundDescriptorSets > ShaderRecompiler::RuntimeAbi::DescriptorSet, "shader runtime ABI exceeds device limits"); Require(limits.maxStorageBufferRange >= sizeof(ShaderRecompiler::RuntimeAbi::ShaderData), "shader runtime ShaderData exceeds device limits"); @@ -46,9 +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/SubgroupClock.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/SubgroupClock.cpp index ac24b51d0f..f586b7f207 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/SubgroupClock.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/SubgroupClock.cpp @@ -15,7 +15,8 @@ constexpr std::array NarrowClockFamilies{ } -bool NarrowSubgroupClock(VkDriverId driver, std::string_view deviceName) { +bool NarrowSubgroupClock(VkDriverId driver, std::uint32_t deviceId, std::string_view deviceName) { + if (driver == VK_DRIVER_ID_AMD_PROPRIETARY && (deviceId == 0x1114u || deviceId == 0x744cu || deviceId == 0x747eu)) return true; if (driver != VK_DRIVER_ID_MESA_RADV) return false; constexpr std::string_view prefix = "(RADV "; const auto start = deviceName.rfind(prefix); 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/ThreadPriority.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp new file mode 100644 index 0000000000..2db74fc43c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp @@ -0,0 +1,309 @@ +#include "prx/libSceAgcDriver/Execution/include/ThreadPriority.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#include +#include +#include +#include +#endif + +namespace AgcDriver { + +namespace { + +ThreadPriorityMode RequestedMode() { + static const ThreadPriorityMode mode = ParseThreadPriorityMode(std::getenv("APS5_THREAD_PRIORITY")); + return mode; +} + +int EnvInt(const char* name, int fallback) { + const char* text = std::getenv(name); + return text != nullptr && *text != '\0' ? std::atoi(text) : fallback; +} + +void LogOnce(const char* role, const std::string& text) { + static std::mutex mutex; + static std::set seen; + std::lock_guard lock(mutex); + if (!seen.insert(std::string(role) + "|" + text).second) return; + std::fprintf(stderr, "[thread-priority] %s: %s\n", role, text.c_str()); +} + +#ifndef _WIN32 + +struct DBusConnection; +struct DBusMessage; +struct DBusError { + const char* name; + const char* message; + unsigned int dummy1 : 1, dummy2 : 1, dummy3 : 1, dummy4 : 1, dummy5 : 1; + void* padding1; +}; +constexpr int DbusBusSystem = 1; +constexpr int DbusTypeInvalid = 0; +constexpr int DbusTypeInt32 = static_cast('i'); +constexpr int DbusTypeUint32 = static_cast('u'); +constexpr int DbusTypeUint64 = static_cast('t'); +constexpr int DbusTypeInt64 = static_cast('x'); +constexpr int DbusTypeString = static_cast('s'); +constexpr int DbusTypeVariant = static_cast('v'); + +struct DBusMessageIter { + void* dummy1; + void* dummy2; + std::uint32_t dummy3; + int dummy4, dummy5, dummy6, dummy7, dummy8, dummy9, dummy10, dummy11; + int pad1; + void* pad2; + void* pad3; +}; + +struct Dbus { + void* handle = nullptr; + DBusConnection* connection = nullptr; + int (*threadsInitDefault)() = nullptr; + void (*errorInit)(DBusError*) = nullptr; + void (*errorFree)(DBusError*) = nullptr; + int (*errorIsSet)(const DBusError*) = nullptr; + DBusConnection* (*busGet)(int, DBusError*) = nullptr; + DBusMessage* (*messageNewMethodCall)(const char*, const char*, const char*, const char*) = nullptr; + int (*messageAppendArgs)(DBusMessage*, int, ...) = nullptr; + DBusMessage* (*sendWithReplyAndBlock)(DBusConnection*, DBusMessage*, int, DBusError*) = nullptr; + void (*messageUnref)(DBusMessage*) = nullptr; + int (*iterInit)(DBusMessage*, DBusMessageIter*) = nullptr; + int (*iterGetArgType)(DBusMessageIter*) = nullptr; + void (*iterRecurse)(DBusMessageIter*, DBusMessageIter*) = nullptr; + void (*iterGetBasic)(DBusMessageIter*, void*) = nullptr; + std::string failure; +}; + +Dbus& LoadDbus() { + static Dbus dbus = [] { + Dbus loaded; + loaded.handle = dlopen("libdbus-1.so.3", RTLD_NOW | RTLD_LOCAL); + if (loaded.handle == nullptr) { + loaded.failure = "libdbus-1.so.3 is not available"; + return loaded; + } + const auto resolve = [&](auto& function, const char* name) { + function = reinterpret_cast>(dlsym(loaded.handle, name)); + if (function == nullptr && loaded.failure.empty()) loaded.failure = std::string("libdbus lacks ") + name; + }; + resolve(loaded.threadsInitDefault, "dbus_threads_init_default"); + resolve(loaded.errorInit, "dbus_error_init"); + resolve(loaded.errorFree, "dbus_error_free"); + resolve(loaded.errorIsSet, "dbus_error_is_set"); + resolve(loaded.busGet, "dbus_bus_get"); + resolve(loaded.messageNewMethodCall, "dbus_message_new_method_call"); + resolve(loaded.messageAppendArgs, "dbus_message_append_args"); + resolve(loaded.sendWithReplyAndBlock, "dbus_connection_send_with_reply_and_block"); + resolve(loaded.messageUnref, "dbus_message_unref"); + resolve(loaded.iterInit, "dbus_message_iter_init"); + resolve(loaded.iterGetArgType, "dbus_message_iter_get_arg_type"); + resolve(loaded.iterRecurse, "dbus_message_iter_recurse"); + resolve(loaded.iterGetBasic, "dbus_message_iter_get_basic"); + if (!loaded.failure.empty()) return loaded; + loaded.threadsInitDefault(); + DBusError error{}; + loaded.errorInit(&error); + loaded.connection = loaded.busGet(DbusBusSystem, &error); + if (loaded.connection == nullptr) { + loaded.failure = std::string("system bus: ") + (loaded.errorIsSet(&error) && error.message != nullptr ? error.message : "unavailable"); + loaded.errorFree(&error); + } + return loaded; + }(); + return dbus; +} + +DBusMessage* RtkitSend(DBusMessage* message, const char* method, std::string& failure) { + auto& dbus = LoadDbus(); + DBusError error{}; + dbus.errorInit(&error); + auto* reply = dbus.sendWithReplyAndBlock(dbus.connection, message, 5000, &error); + dbus.messageUnref(message); + if (reply == nullptr) { + failure = std::string(method) + ": " + (dbus.errorIsSet(&error) && error.message != nullptr ? error.message : "no reply"); + dbus.errorFree(&error); + } + return reply; +} + +bool RtkitCall(const char* method, std::uint64_t tid, int type, const void* value, std::string& failure) { + auto& dbus = LoadDbus(); + if (dbus.connection == nullptr) { + failure = dbus.failure; + return false; + } + static std::mutex mutex; + std::lock_guard lock(mutex); + auto* message = dbus.messageNewMethodCall("org.freedesktop.RealtimeKit1", "/org/freedesktop/RealtimeKit1", "org.freedesktop.RealtimeKit1", method); + if (message == nullptr) { + failure = "dbus message allocation failed"; + return false; + } + const std::uint64_t pid = static_cast(getpid()); + bool ok = false; + if (type == DbusTypeInt32) ok = dbus.messageAppendArgs(message, DbusTypeUint64, &pid, DbusTypeUint64, &tid, DbusTypeInt32, value, DbusTypeInvalid) != 0; + else ok = dbus.messageAppendArgs(message, DbusTypeUint64, &pid, DbusTypeUint64, &tid, DbusTypeUint32, value, DbusTypeInvalid) != 0; + if (!ok) { + dbus.messageUnref(message); + failure = "dbus argument append failed"; + return false; + } + auto* reply = RtkitSend(message, method, failure); + if (reply == nullptr) return false; + dbus.messageUnref(reply); + return true; +} + +bool RtkitProperty(const char* name, std::int64_t& value, std::string& failure) { + auto& dbus = LoadDbus(); + if (dbus.connection == nullptr) { + failure = dbus.failure; + return false; + } + auto* message = dbus.messageNewMethodCall("org.freedesktop.RealtimeKit1", "/org/freedesktop/RealtimeKit1", "org.freedesktop.DBus.Properties", "Get"); + if (message == nullptr) { + failure = "dbus message allocation failed"; + return false; + } + const char* interfaceName = "org.freedesktop.RealtimeKit1"; + if (dbus.messageAppendArgs(message, DbusTypeString, &interfaceName, DbusTypeString, &name, DbusTypeInvalid) == 0) { + dbus.messageUnref(message); + failure = "dbus argument append failed"; + return false; + } + auto* reply = RtkitSend(message, name, failure); + if (reply == nullptr) return false; + DBusMessageIter iter{}; + DBusMessageIter variant{}; + bool ok = false; + if (dbus.iterInit(reply, &iter) != 0 && dbus.iterGetArgType(&iter) == DbusTypeVariant) { + dbus.iterRecurse(&iter, &variant); + const int type = dbus.iterGetArgType(&variant); + if (type == DbusTypeInt32) { + std::int32_t number = 0; + dbus.iterGetBasic(&variant, &number); + value = number; + ok = true; + } else if (type == DbusTypeInt64) { + std::int64_t number = 0; + dbus.iterGetBasic(&variant, &number); + value = number; + ok = true; + } + } + dbus.messageUnref(reply); + if (!ok) failure = std::string(name) + ": unexpected reply type"; + return ok; +} + +bool PrepareRealtime(std::uint32_t priority, const char* role, std::string& failure) { + std::int64_t maxPriority = 0; + std::int64_t maxRttime = 0; + std::string rtkitFailure; + std::optional rtkit; + if (RtkitProperty("MaxRealtimePriority", maxPriority, rtkitFailure) && RtkitProperty("RTTimeUSecMax", maxRttime, rtkitFailure)) rtkit = RtkitLimits{maxPriority, maxRttime}; + static std::mutex mutex; + std::lock_guard lock(mutex); + rlimit limit{}; + if (getrlimit(RLIMIT_RTTIME, &limit) != 0) { + failure = "getrlimit(RLIMIT_RTTIME) failed"; + return false; + } + const auto requested = static_cast(std::max(1, EnvInt("APS5_THREAD_RTTIME_US", 200000))); + const auto plan = PlanRealtime(priority, rtkit, {limit.rlim_cur, limit.rlim_max}, requested); + if (!plan.refusal.empty()) { + failure = rtkit.has_value() ? plan.refusal : rtkitFailure; + return false; + } + sched_param param{}; + if (sched_getparam(0, ¶m) != 0 || sched_setscheduler(0, sched_getscheduler(0) | SCHED_RESET_ON_FORK, ¶m) != 0) { + failure = "SCHED_RESET_ON_FORK could not be set"; + return false; + } + if (!plan.limit.has_value()) return true; + const rlimit wanted{plan.limit->soft, plan.limit->hard}; + if (setrlimit(RLIMIT_RTTIME, &wanted) != 0) { + failure = "setrlimit(RLIMIT_RTTIME) failed"; + return false; + } + if (wanted.rlim_max != limit.rlim_max) { + const auto text = [](rlim_t value) { return value == RLIM_INFINITY ? std::string("unlimited") : std::to_string(value); }; + LogOnce(role, "RLIMIT_RTTIME hard limit lowered from " + text(limit.rlim_max) + " to " + text(wanted.rlim_max) + " us for the process"); + } + return true; +} + +void Raise(const char* role) { + const auto tid = static_cast(syscall(SYS_gettid)); + std::string failure; + if (RequestedMode() == ThreadPriorityMode::Realtime) { + const std::uint32_t priority = static_cast(std::max(1, std::min(99, EnvInt("APS5_THREAD_RT", 10)))); + if (!PrepareRealtime(priority, role, failure) || !RtkitCall("MakeThreadRealtimeWithPID", tid, DbusTypeUint32, &priority, failure)) { + throw std::runtime_error(std::string("APS5_THREAD_PRIORITY=rt: ") + role + " realtime refused: " + failure); + } + const int policy = sched_getscheduler(0) & ~SCHED_RESET_ON_FORK; + LogOnce(role, std::string(policy == SCHED_FIFO ? "SCHED_FIFO " : "SCHED_RR ") + std::to_string(priority) + " via rtkit"); + return; + } + const std::int32_t nice = static_cast(std::max(-20, std::min(19, EnvInt("APS5_THREAD_NICE", -10)))); + if (!RtkitCall("MakeThreadHighPriorityWithPID", tid, DbusTypeInt32, &nice, failure)) { + throw std::runtime_error(std::string("APS5_THREAD_PRIORITY=high: ") + role + " nice " + std::to_string(nice) + " refused: " + failure); + } + LogOnce(role, "nice " + std::to_string(nice) + " via rtkit"); +} + +#else + +void Raise(const char* role) { + const int priority = RequestedMode() == ThreadPriorityMode::Realtime ? THREAD_PRIORITY_TIME_CRITICAL : THREAD_PRIORITY_HIGHEST; + if (!SetThreadPriority(GetCurrentThread(), priority)) { + throw std::runtime_error(std::string("APS5_THREAD_PRIORITY: ") + role + " SetThreadPriority failed (error " + std::to_string(GetLastError()) + ")"); + } + LogOnce(role, priority == THREAD_PRIORITY_TIME_CRITICAL ? "time-critical" : "highest"); +} + +#endif + +} + +ThreadPriorityMode ParseThreadPriorityMode(const char* text) { + if (text == nullptr || *text == '\0' || std::strcmp(text, "off") == 0) return ThreadPriorityMode::Off; + if (std::strcmp(text, "high") == 0) return ThreadPriorityMode::High; + if (std::strcmp(text, "rt") == 0) return ThreadPriorityMode::Realtime; + throw std::invalid_argument(std::string("APS5_THREAD_PRIORITY=") + text + " is not off, high or rt"); +} + +RealtimePlan PlanRealtime(std::uint32_t priority, const std::optional& rtkit, RttimeLimit current, std::uint64_t requestedUs) { + if (!rtkit.has_value()) return {"rtkit's limits are unknown", std::nullopt}; + if (static_cast(priority) > rtkit->maxPriority) return {"priority " + std::to_string(priority) + " is above rtkit's MaxRealtimePriority " + std::to_string(rtkit->maxPriority), std::nullopt}; + if (rtkit->maxRttimeUs <= 0) return {"rtkit reports RTTimeUSecMax " + std::to_string(rtkit->maxRttimeUs), std::nullopt}; + const auto ceiling = std::min(requestedUs, static_cast(rtkit->maxRttimeUs)); + auto wanted = current; + if (wanted.hard > ceiling) wanted.hard = ceiling; + if (wanted.soft > wanted.hard / 2) wanted.soft = wanted.hard / 2; + if (wanted == current) return {}; + return {{}, wanted}; +} + +void RaiseWorkerThreadPriority(const char* role) { + if (RequestedMode() == ThreadPriorityMode::Off) return; + Raise(role); +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/TraceFile.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/TraceFile.cpp new file mode 100644 index 0000000000..b8263c881e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/TraceFile.cpp @@ -0,0 +1,81 @@ +#include "prx/libSceAgcDriver/Execution/include/TraceFile.hpp" +#include "zlib.h" +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#endif + +namespace AgcDriver { + +void TraceFile::WriteGzip(const std::filesystem::path& path, std::string_view text) { + z_stream stream{}; + if (deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -MAX_WBITS, 8, Z_DEFAULT_STRATEGY) != Z_OK) throw std::runtime_error("deflateInit2 failed"); + struct End { + z_stream& stream; + ~End() { deflateEnd(&stream); } + } end{stream}; + std::ofstream output; + output.exceptions(std::ios::badbit | std::ios::failbit); + output.open(path, std::ios::binary | std::ios::trunc); + const std::array header{0x1f, 0x8b, 8, 0, 0, 0, 0, 0, 0, 0xff}; + output.write(reinterpret_cast(header.data()), header.size()); + uLong crc = crc32(0, Z_NULL, 0); + std::vector chunk(1 << 16); + std::size_t offset = 0; + int status = Z_OK; + while (status != Z_STREAM_END) { + if (stream.avail_in == 0 && offset < text.size()) { + const auto piece = static_cast(std::min(text.size() - offset, std::size_t{1} << 30)); + stream.next_in = reinterpret_cast(const_cast(text.data() + offset)); + stream.avail_in = piece; + crc = crc32(crc, stream.next_in, piece); + offset += piece; + } + stream.next_out = chunk.data(); + stream.avail_out = static_cast(chunk.size()); + status = deflate(&stream, offset == text.size() ? Z_FINISH : Z_NO_FLUSH); + if (status != Z_OK && status != Z_STREAM_END) throw std::runtime_error("deflate failed with " + std::to_string(status)); + output.write(reinterpret_cast(chunk.data()), static_cast(chunk.size() - stream.avail_out)); + } + std::array trailer{}; + const auto size = static_cast(text.size()); + for (unsigned index = 0; index < 4; ++index) { + trailer[index] = static_cast((crc >> (8 * index)) & 0xff); + trailer[4 + index] = static_cast((size >> (8 * index)) & 0xff); + } + output.write(reinterpret_cast(trailer.data()), trailer.size()); + output.close(); +} + +void TraceFile::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); + output.open(path, std::ios::binary | std::ios::app); + output.write(line.data(), static_cast(line.size())); + output.close(); +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 38a2eff399..05d51f142a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -1,17 +1,40 @@ #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/RobustBufferAccess.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/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" @@ -32,9 +55,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -42,6 +67,7 @@ #include #include #include +#include #include #include #include @@ -58,6 +84,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)); } @@ -67,6 +94,52 @@ void require(bool condition, const char* reason) { if (!condition) throw std::runtime_error(std::string("Vulkan presentation: ") + reason); } +bool CoreVulkanCapability(std::uint32_t capability) { + switch (capability) { + case spv::CapabilityMatrix: + case spv::CapabilityShader: + case spv::CapabilityInputAttachment: + case spv::CapabilitySampled1D: + case spv::CapabilityImage1D: + case spv::CapabilitySampledBuffer: + case spv::CapabilityImageBuffer: + case spv::CapabilityImageQuery: + case spv::CapabilityDerivativeControl: + case spv::CapabilityStorageImageExtendedFormats: + return true; + default: + return false; + } +} + +void CreateComputePipeline(VkDevice device, PFN_vkCreateComputePipelines create, VkPipelineCache cache, VkShaderModule module, VkPipelineLayout layout, VkPipelineCreateFlags flags, VkPipelineShaderStageCreateFlags stageFlags, const VkSpecializationInfo* specialization, std::uint32_t subgroupSize, VkPipeline* pipeline, Graphics::PipelineCacheSynchronization* synchronization = nullptr) { + VkComputePipelineCreateInfo info{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; + info.flags = flags; + info.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + info.stage.flags = stageFlags; + info.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; + info.stage.module = module; + info.stage.pName = "main"; + info.stage.pSpecializationInfo = specialization; + VkPipelineShaderStageRequiredSubgroupSizeCreateInfoEXT subgroup{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_REQUIRED_SUBGROUP_SIZE_CREATE_INFO_EXT}; + subgroup.requiredSubgroupSize = subgroupSize; + if (subgroupSize != 0) info.stage.pNext = &subgroup; + info.layout = layout; + auto cacheLock = synchronization != nullptr ? synchronization->LockForPipelineUse() : Graphics::PipelineCacheSynchronization::PipelineUseLock{}; + check(create(device, cache, 1, &info, nullptr, pipeline), "vkCreateComputePipelines"); + PerformanceCounters::CountPipeline(); +} + +bool CaptureComputePipeline(std::size_t words) { + const char* minimum = std::getenv("APS5_DUMP_COMPUTE_MIN_WORDS"); + if (minimum == nullptr) return true; + std::uint32_t threshold = 0; + const auto end = minimum + std::strlen(minimum); + const auto parsed = std::from_chars(minimum, end, threshold); + require(parsed.ec == std::errc{} && parsed.ptr == end && threshold <= ComputePipelineCapture::MaxSpirvWords, "APS5_DUMP_COMPUTE_MIN_WORDS must be an integer from 0 through 8388608"); + return words >= threshold; +} + // The ShaderResources content cache dispatches share with recorded draws: Graphics::ResourceCache, // one process-wide instance (see SharedResourceCache) that the State references so this file keeps // its Find/Insert/Remove/Clear calls; the device clears it at teardown before its descriptor caches @@ -75,6 +148,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: @@ -87,23 +161,30 @@ public: if (worker.joinable()) worker.join(); } - void Enqueue(int index, std::uint32_t width, std::uint32_t height, std::uint32_t scale, std::vector pixels) { + void Enqueue(int index, std::uint32_t width, std::uint32_t height, std::uint32_t scale, bool screenshot, std::vector pixels) { std::lock_guard lock(mutex); require(!stopping, "frame dump writer has stopped"); require(pixels.size() <= maxBytes - queuedBytes, "frame dump queue exceeded 64 MiB"); + if (!screenshot && !frameDumpOutput.Enabled_nid_no_patch()) return; if (!worker.joinable()) worker = std::thread([this] { run(); }); const auto bytes = pixels.size(); - pending.push_back({index, width, height, scale, std::move(pixels)}); + pending.push_back({index, width, height, scale, screenshot, std::move(pixels)}); queuedBytes += bytes; changed.notify_one(); } + bool Idle() { + std::lock_guard lock(mutex); + return queuedBytes == 0; + } + private: struct Frame { int index; std::uint32_t width; std::uint32_t height; std::uint32_t scale; + bool screenshot; std::vector pixels; }; @@ -118,7 +199,8 @@ 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(); } @@ -153,14 +235,43 @@ 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 { + VkDevice device = VK_NULL_HANDLE; + PFN_vkDestroyDescriptorPool destroyPool = nullptr; + std::unique_ptr buffer; + VkDescriptorPool pool = VK_NULL_HANDLE; + VkDescriptorSet set = VK_NULL_HANDLE; + ~WorkgroupMemory() { + buffer.reset(); + if (pool != VK_NULL_HANDLE) destroyPool(device, pool, nullptr); + } +}; + struct VulkanDevice::State { + bool compileOnly = false; void* library = nullptr; PFN_vkGetInstanceProcAddr instanceProc = nullptr; PFN_vkGetDeviceProcAddr deviceProc = nullptr; @@ -175,6 +286,7 @@ struct VulkanDevice::State { VkExtent2D extent{}; std::vector images; VkFence acquireFence = VK_NULL_HANDLE; + VkPresentModeKHR presentMode = VK_PRESENT_MODE_FIFO_KHR; struct RetiredSwapchain { VkSwapchainKHR swapchain = VK_NULL_HANDLE; std::vector rendered; @@ -187,6 +299,12 @@ struct VulkanDevice::State { void* uploadMapping = nullptr; 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}; std::vector spirvExtensions; @@ -194,40 +312,85 @@ struct VulkanDevice::State { bool tessellationShader = false; bool meshShader = false; bool fragmentShaderBarycentric = false; + bool shaderTerminateInvocation = false; bool geometryShader = false; bool sampleRateShading = false; + bool storageImageMultisample = false; + bool shaderClipDistance = false; + bool shaderCullDistance = false; bool shaderClock = false; 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; VkPhysicalDeviceDescriptorIndexingPropertiesEXT descriptorIndexingProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_PROPERTIES_EXT}; bool imageInt64Atomics = false; bool bufferInt64Atomics = false; + bool viewportIndexLayer = false; bool primitiveListRestart = false; bool depthClipControl = false; + bool graphicsPipelineLibrary = false; + bool pipelineLinkTimeOptimization = true; bool imageViewMinLod = false; bool pipelineExecutableInfo = false; bool maintenance8 = 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; bool fragmentShaderPixelInterlock = false; bool conservativeRasterization = false; + bool provokingVertexLast = false; + bool provokingVertexModePerPipeline = false; // VK_KHR_timeline_semaphore enabled: the recorder's unlocked waits are available. bool timelineSemaphores = false; 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; @@ -244,17 +407,26 @@ struct VulkanDevice::State { // Device-local buffers of a repeated 16-byte fill pattern (FillBuffer), most recently used // last; each is filled once by a doubling chain in device memory. std::vector, std::shared_ptr>> patternBuffers; + VkDescriptorSetLayout workgroupMemoryLayout = VK_NULL_HANDLE; + std::shared_ptr workgroupMemory; // Compute pipeline objects by variant (and push-constant use), under their own mutex: the // 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; + std::unique_ptr placeholderImages; 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 @@ -265,6 +437,17 @@ struct VulkanDevice::State { std::shared_ptr>> copiedWriters = std::make_shared>>(); VkPhysicalDeviceMeshShaderPropertiesEXT meshLimits{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MESH_SHADER_PROPERTIES_EXT}; std::unique_ptr scaler; + std::unique_ptr presentation; + 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; @@ -283,6 +466,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; @@ -314,6 +498,7 @@ struct VulkanDevice::State { bool queuePending = false; std::uint32_t queueIndex = 0; int nextDumpIndex = 0; + bool nextDumpScreenshot = false; FrameDumpWriter dumpWriter; template @@ -335,10 +520,56 @@ struct VulkanDevice::State { return function; } + void CreateWorkgroupMemoryLayout() { + const VkDescriptorSetLayoutBinding binding{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}; + VkDescriptorSetLayoutCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + info.bindingCount = 1; + info.pBindings = &binding; + check(DeviceFunction("vkCreateDescriptorSetLayout")(device, &info, nullptr, &workgroupMemoryLayout), "vkCreateDescriptorSetLayout workgroup memory"); + } + + std::shared_ptr WorkgroupMemoryFor(const Graphics::Context& context, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint32_t dwords) { + const std::uint64_t range = properties.limits.maxStorageBufferRange; + const auto groupBytes = static_cast(dwords) * 4u; + std::uint64_t groups = 1; + for (const std::uint64_t count : {x, y, z}) { + if (count != 0u && groups > range / groupBytes / count) { + throw std::runtime_error("Vulkan dispatch: the LDS of " + std::to_string(x) + "x" + std::to_string(y) + "x" + std::to_string(z) + " workgroups of " + std::to_string(groupBytes) + " bytes each is over maxStorageBufferRange " + std::to_string(range)); + } + groups *= count; + } + const auto bytes = std::max(groups, 1u) * groupBytes; + if (workgroupMemory != nullptr && workgroupMemory->buffer->Size() >= bytes) return workgroupMemory; + const auto size = std::min((bytes + WorkgroupMemoryGranule - 1u) / WorkgroupMemoryGranule * WorkgroupMemoryGranule, range); + auto memory = std::make_shared(); + memory->device = device; + memory->destroyPool = DeviceFunction("vkDestroyDescriptorPool"); + memory->buffer = std::make_unique(context, static_cast(size), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + const VkDescriptorPoolSize poolSize{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1}; + VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + poolInfo.maxSets = 1; + poolInfo.poolSizeCount = 1; + poolInfo.pPoolSizes = &poolSize; + check(DeviceFunction("vkCreateDescriptorPool")(device, &poolInfo, nullptr, &memory->pool), "vkCreateDescriptorPool workgroup memory"); + VkDescriptorSetAllocateInfo allocation{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + allocation.descriptorPool = memory->pool; + allocation.descriptorSetCount = 1; + allocation.pSetLayouts = &workgroupMemoryLayout; + check(DeviceFunction("vkAllocateDescriptorSets")(device, &allocation, &memory->set), "vkAllocateDescriptorSets workgroup memory"); + const VkDescriptorBufferInfo described{memory->buffer->Handle(), 0, size}; + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstSet = memory->set; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + write.pBufferInfo = &described; + DeviceFunction("vkUpdateDescriptorSets")(device, 1, &write, 0, nullptr); + workgroupMemory = std::move(memory); + return workgroupMemory; + } + void Upload(std::span pixels) { - APS5_LOG_OUT("Upload pixels=%zu uploadSize=%llu buffer=%p memory=%p mapping=%p", pixels.size(), static_cast(uploadSize), reinterpret_cast(uploadBuffer), reinterpret_cast(uploadMemory), uploadMapping); if (uploadSize < pixels.size()) { - APS5_LOG_OUT("Upload reallocating oldSize=%llu newSize=%zu", static_cast(uploadSize), pixels.size()); if (uploadMapping) DeviceFunction("vkUnmapMemory")(device, uploadMemory); uploadMapping = nullptr; if (uploadBuffer) DeviceFunction("vkDestroyBuffer")(device, uploadBuffer, nullptr); @@ -362,7 +593,6 @@ struct VulkanDevice::State { } } require(memoryType < memoryProperties.memoryTypeCount, "coherent host upload memory is unavailable"); - APS5_LOG_OUT("Upload requirements size=%llu alignment=%llu typeBits=0x%x memoryType=%u", static_cast(requirements.size), static_cast(requirements.alignment), requirements.memoryTypeBits, memoryType); VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; allocation.allocationSize = requirements.size; allocation.memoryTypeIndex = memoryType; @@ -370,10 +600,8 @@ struct VulkanDevice::State { check(DeviceFunction("vkBindBufferMemory")(device, uploadBuffer, uploadMemory, 0), "vkBindBufferMemory display upload"); check(DeviceFunction("vkMapMemory")(device, uploadMemory, 0, pixels.size(), 0, &uploadMapping), "vkMapMemory display upload"); uploadSize = pixels.size(); - APS5_LOG_OUT("Upload allocation complete buffer=%p memory=%p mapping=%p size=%llu", reinterpret_cast(uploadBuffer), reinterpret_cast(uploadMemory), uploadMapping, static_cast(uploadSize)); } std::memcpy(uploadMapping, pixels.data(), pixels.size()); - APS5_LOG_OUT("Upload memcpy complete bytes=%zu", pixels.size()); } bool SharedSlotInFlight() { @@ -384,7 +612,7 @@ struct VulkanDevice::State { // Whether a recorded dispatch's or draw's copied write-back (listed until it ran) stores into // the range on the CPU; under GpuMutex. bool CopiedWriterOverlaps(std::uint64_t address, std::size_t bytes) const { - const auto overlaps = [&](const auto& writer) { return writer->WritesOverlap(address, bytes); }; + const auto overlaps = [&](const auto& writer) { return writer->CopiedWritesOverlap(address, bytes); }; return std::any_of(copiedWriters->begin(), copiedWriters->end(), overlaps) || std::any_of(Graphics::DrawCopiedWriters()->begin(), Graphics::DrawCopiedWriters()->end(), overlaps); } @@ -405,6 +633,7 @@ struct VulkanDevice::State { // and its statistics taken (under presentMutex). void RetireSlot(PresentSlot& slot) { slot.inFlight = false; + if (presentation) presentation->Release(static_cast(&slot - presentSlots.data())); slot.kept.reset(); if (slot.dumpRecorded) { slot.dumpRecorded = false; @@ -466,13 +695,24 @@ struct VulkanDevice::State { void WriteFrameDump(const PresentSlot& slot) { require(slot.dumpBuffer != nullptr, "frame dump was not recorded"); - std::vector full(slot.dumpBuffer->Bytes().begin(), slot.dumpBuffer->Bytes().end()); + const auto full = slot.dumpBuffer->Bytes(); const auto index = slot.dumpIndex; - const auto width = slot.dumpWidth; - const auto height = slot.dumpHeight; + 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; + const auto height = (slot.dumpHeight + scale - 1) / scale; + std::vector pixels(static_cast(width) * height * 4); + for (std::uint32_t y = 0; y < height; ++y) { + for (std::uint32_t x = 0; x < width; ++x) std::memcpy(pixels.data() + (static_cast(y) * width + x) * 4, full.data() + (static_cast(y) * scale * slot.dumpWidth + static_cast(x) * scale) * 4, 4); + } 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, width, height, std::chrono::duration(std::chrono::steady_clock::now() - start).count()); - dumpWriter.Enqueue(index, width, height, slot.dumpScale, std::move(full)); + 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, false, std::move(pixels)); } // Blocks on the fence, or polls it every 50 us with APS5_PRESENT_POLL_FENCE=1 (the difference @@ -494,7 +734,21 @@ 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; + } if (device != VK_NULL_HANDLE) { const auto idle = reinterpret_cast(deviceProc(device, "vkDeviceWaitIdle"))(device); if (idle != VK_SUCCESS && idle != VK_ERROR_DEVICE_LOST) std::terminate(); @@ -526,25 +780,36 @@ struct VulkanDevice::State { // belong to this device and must be destroyed while it lives. 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(); @@ -569,6 +834,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) { @@ -581,7 +847,164 @@ struct VulkanDevice::State { } }; -VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_unique()), serial([] { static std::atomic serials{0}; return serials.fetch_add(1, std::memory_order_relaxed) + 1; }()) { +ComputePipelineReplayResult VulkanDevice::ReplayComputePipeline(const ComputePipelineCapture& capture, std::span initialCache) { + require(state->compileOnly, "pipeline replay requires a compile-only device"); + const auto start = std::chrono::steady_clock::now(); + const auto elapsed = [](auto since) { return std::chrono::duration(std::chrono::steady_clock::now() - since).count(); }; + capture.Validate(); + require(initialCache.size() <= 64u * 1024u * 1024u, "replay pipeline cache exceeds 64 MiB"); + if (!initialCache.empty()) { + require(initialCache.size() >= 32, "replay pipeline cache header is truncated"); + const auto word = [&](std::size_t offset) { + std::uint32_t result = 0; + for (unsigned shift = 0; shift < 32; shift += 8) result |= std::to_integer(initialCache[offset++]) << shift; + return result; + }; + require(word(0) >= 32 && word(0) <= initialCache.size() && word(4) == VK_PIPELINE_CACHE_HEADER_VERSION_ONE, "unsupported replay pipeline cache header"); + require(word(8) == state->properties.vendorID && word(12) == state->properties.deviceID, "replay pipeline cache belongs to another device"); + for (std::size_t index = 0; index < VK_UUID_SIZE; ++index) require(std::to_integer(initialCache[16 + index]) == state->properties.pipelineCacheUUID[index], "replay pipeline cache UUID differs from the driver"); + } + require((capture.flags & VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR) == 0 || state->pipelineExecutableInfo, "captured pipeline statistics feature is not enabled"); + if (capture.requiredSubgroupSize != 0) { + 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(); + require(capture.spirv[1] <= target.spirvVersion, "captured SPIR-V version exceeds enabled device support"); + for (std::size_t cursor = 5; cursor < capture.spirv.size(); cursor += capture.spirv[cursor] >> 16u) { + const auto count = capture.spirv[cursor] >> 16u; + if ((capture.spirv[cursor] & 0xffffu) == spv::OpCapability) { + require(count == 2 && (CoreVulkanCapability(capture.spirv[cursor + 1]) || std::ranges::find(target.supportedCapabilities, capture.spirv[cursor + 1]) != target.supportedCapabilities.end()), "captured SPIR-V capability is not enabled"); + } + } +#if ANYPS5_ENABLE_SPIRV_TOOLS + static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(capture.spirv, target.vulkanVersion, target.spirvVersion, target.nonConstantImageOffsets, false)); +#else + throw std::runtime_error("compute pipeline replay requires ANYPS5_ENABLE_SPIRV_TOOLS=ON"); +#endif + ShaderRecompiler::RecompileResult shader; + shader.spirv = capture.spirv; + for (const auto& value : capture.specialization) shader.specialization.push_back({value.id, value.value}); + const Graphics::PipelineSpecialization specialization(shader); + const auto& limits = state->properties.limits; + require(capture.sets.size() <= limits.maxBoundDescriptorSets, "captured descriptor sets exceed device limits"); + for (const auto& range : capture.pushConstants) require(range.offset + range.size <= limits.maxPushConstantsSize, "captured push constants exceed device limits"); + struct ReplayObjects { + VkDevice device; + PFN_vkDestroyDescriptorSetLayout destroySetLayout; + PFN_vkDestroyPipelineCache destroyCache; + std::vector sets; + VkPipelineCache cache = VK_NULL_HANDLE; + ~ReplayObjects() { + for (auto layout : sets) destroySetLayout(device, layout, nullptr); + if (cache != VK_NULL_HANDLE) destroyCache(device, cache, nullptr); + } + } replay{state->device, state->DeviceFunction("vkDestroyDescriptorSetLayout"), state->DeviceFunction("vkDestroyPipelineCache"), {}}; + ComputePipelineObjects objects; + objects.device = state->device; + objects.destroyModule = state->DeviceFunction("vkDestroyShaderModule"); + objects.destroyLayout = state->DeviceFunction("vkDestroyPipelineLayout"); + objects.destroyPipeline = state->DeviceFunction("vkDestroyPipeline"); + ComputePipelineReplayResult result; + result.deviceName = state->properties.deviceName; + result.vendorId = state->properties.vendorID; + result.deviceId = state->properties.deviceID; + result.driverVersion = state->properties.driverVersion; + result.apiVersion = state->properties.apiVersion; + std::copy_n(state->properties.pipelineCacheUUID, VK_UUID_SIZE, result.pipelineCacheUuid.begin()); + result.initialCacheBytes = initialCache.size(); + auto phase = std::chrono::steady_clock::now(); + VkPipelineCacheCreateInfo cacheInfo{VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO}; + cacheInfo.initialDataSize = initialCache.size(); + cacheInfo.pInitialData = initialCache.data(); + check(state->DeviceFunction("vkCreatePipelineCache")(state->device, &cacheInfo, nullptr, &replay.cache), "vkCreatePipelineCache replay"); + result.cacheMilliseconds = elapsed(phase); + phase = std::chrono::steady_clock::now(); + 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{}; + bool hasUpdateAfterBindLayout = false; + 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 || 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); + const std::size_t kind = binding.type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? 0 : binding.type == VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE ? 1 : binding.type == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE ? 2 : 3; + descriptorCounts[updateAfterBind ? 1 : 0][kind] += binding.count; + } + VkDescriptorSetLayoutBindingFlagsCreateInfo bindingFlags{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_BINDING_FLAGS_CREATE_INFO}; + bindingFlags.bindingCount = static_cast(flags.size()); + bindingFlags.pBindingFlags = flags.data(); + VkDescriptorSetLayoutCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + info.pNext = std::ranges::any_of(flags, [](auto value) { return value != 0; }) ? &bindingFlags : nullptr; + info.flags = layout.flags; + info.bindingCount = static_cast(bindings.size()); + info.pBindings = bindings.data(); + VkDescriptorSetVariableDescriptorCountLayoutSupport variableSupport{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_VARIABLE_DESCRIPTOR_COUNT_LAYOUT_SUPPORT}; + VkDescriptorSetLayoutSupport support{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_SUPPORT, variableCount != 0 ? &variableSupport : nullptr}; + state->DeviceFunction("vkGetDescriptorSetLayoutSupport")(state->device, &info, &support); + require(support.supported == VK_TRUE, "captured descriptor set layout is unsupported"); + require(variableCount <= variableSupport.maxVariableDescriptorCount, "captured variable descriptor count exceeds layout support"); + VkDescriptorSetLayout handle = VK_NULL_HANDLE; + check(state->DeviceFunction("vkCreateDescriptorSetLayout")(state->device, &info, nullptr, &handle), "vkCreateDescriptorSetLayout replay"); + replay.sets.push_back(handle); + } + const std::array regularLimits{std::min(limits.maxDescriptorSetStorageBuffers, limits.maxPerStageDescriptorStorageBuffers), std::min(limits.maxDescriptorSetSampledImages, limits.maxPerStageDescriptorSampledImages), std::min(limits.maxDescriptorSetStorageImages, limits.maxPerStageDescriptorStorageImages), std::min(limits.maxDescriptorSetSamplers, limits.maxPerStageDescriptorSamplers)}; + const auto& indexed = state->descriptorIndexingProperties; + const std::array indexedLimits{std::min(indexed.maxDescriptorSetUpdateAfterBindStorageBuffers, indexed.maxPerStageDescriptorUpdateAfterBindStorageBuffers), std::min(indexed.maxDescriptorSetUpdateAfterBindSampledImages, indexed.maxPerStageDescriptorUpdateAfterBindSampledImages), std::min(indexed.maxDescriptorSetUpdateAfterBindStorageImages, indexed.maxPerStageDescriptorUpdateAfterBindStorageImages), std::min(indexed.maxDescriptorSetUpdateAfterBindSamplers, indexed.maxPerStageDescriptorUpdateAfterBindSamplers)}; + std::uint64_t regularResources = 0; + std::uint64_t indexedResources = 0; + for (std::size_t kind = 0; kind < regularLimits.size(); ++kind) { + require(descriptorCounts[0][kind] <= regularLimits[kind], "captured pipeline descriptors exceed device limits"); + require(!hasUpdateAfterBindLayout || descriptorCounts[0][kind] + descriptorCounts[1][kind] <= indexedLimits[kind], "captured update-after-bind pipeline descriptors exceed device limits"); + if (kind != 3) { + regularResources += descriptorCounts[0][kind]; + indexedResources += descriptorCounts[0][kind] + descriptorCounts[1][kind]; + } + } + require(regularResources <= limits.maxPerStageResources && (!hasUpdateAfterBindLayout || indexedResources <= indexed.maxPerStageUpdateAfterBindResources), "captured pipeline resources exceed device limits"); + VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layoutInfo.setLayoutCount = static_cast(replay.sets.size()); + layoutInfo.pSetLayouts = replay.sets.data(); + layoutInfo.pushConstantRangeCount = static_cast(capture.pushConstants.size()); + layoutInfo.pPushConstantRanges = capture.pushConstants.data(); + check(state->DeviceFunction("vkCreatePipelineLayout")(state->device, &layoutInfo, nullptr, &objects.layout), "vkCreatePipelineLayout replay"); + result.layoutMilliseconds = elapsed(phase); + phase = std::chrono::steady_clock::now(); + CreateComputePipeline(state->device, state->DeviceFunction("vkCreateComputePipelines"), replay.cache, objects.module, objects.layout, capture.flags, capture.stageFlags, specialization.Info(), capture.requiredSubgroupSize, &objects.pipeline); + result.pipelineMilliseconds = elapsed(phase); + phase = std::chrono::steady_clock::now(); + const auto getCache = state->DeviceFunction("vkGetPipelineCacheData"); + std::size_t cacheBytes = 0; + check(getCache(state->device, replay.cache, &cacheBytes, nullptr), "vkGetPipelineCacheData size replay"); + require(cacheBytes <= 64u * 1024u * 1024u, "exported replay pipeline cache exceeds 64 MiB"); + result.cache.resize(cacheBytes); + check(getCache(state->device, replay.cache, &cacheBytes, result.cache.data()), "vkGetPipelineCacheData replay"); + result.cache.resize(cacheBytes); + result.cacheMilliseconds += elapsed(phase); + result.totalMilliseconds = elapsed(start); + return result; +} + +VulkanDevice::VulkanDevice(const PresentationWindow* window) : VulkanDevice(window, false) {} + +VulkanDevice::VulkanDevice(CompileOnly) : VulkanDevice(nullptr, true) {} + +VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state(std::make_unique()), serial([] { static std::atomic serials{0}; return serials.fetch_add(1, std::memory_order_relaxed) + 1; }()) { + state->compileOnly = compileOnly; APS5_LOG_OUT("VulkanDevice constructor window=%p", static_cast(window)); #ifdef _WIN32 state->library = SDL_LoadObject("vulkan-1.dll"); @@ -621,6 +1044,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u 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(); } @@ -723,6 +1150,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u 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); @@ -739,6 +1167,16 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u 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); require(hasExtension(VK_KHR_SHADER_FLOAT_CONTROLS_EXTENSION_NAME), "VK_KHR_shader_float_controls is unavailable"); @@ -763,6 +1201,18 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &barycentricFeatures}; state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &features); state->fragmentShaderBarycentric = barycentricFeatures.fragmentShaderBarycentric == VK_TRUE; + if (state->fragmentShaderBarycentric && hasExtension(VK_KHR_DRIVER_PROPERTIES_EXTENSION_NAME)) { + VkPhysicalDeviceDriverProperties driver{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES}; + VkPhysicalDeviceProperties2 driverProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &driver}; + state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &driverProperties); + state->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)) { @@ -782,21 +1232,49 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u VkPhysicalDeviceDriverProperties driver{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES}; VkPhysicalDeviceProperties2 driverProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &driver}; state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &driverProperties); - state->narrowSubgroupClock = NarrowSubgroupClock(driver.driverID, state->properties.deviceName); + 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); state->spirvExtensions.push_back("SPV_EXT_fragment_shader_interlock"); } + if (hasExtension(VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME)) { + 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); state->capabilities.push_back(spv::CapabilityShaderClockKHR); @@ -827,6 +1305,17 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u state->imageInt64Atomics = true; state->spirvExtensions.push_back("SPV_EXT_shader_image_int64"); } + if (hasExtension(VK_EXT_SHADER_VIEWPORT_INDEX_LAYER_EXTENSION_NAME)) { + deviceExtensions.push_back(VK_EXT_SHADER_VIEWPORT_INDEX_LAYER_EXTENSION_NAME); + state->capabilities.push_back(spv::CapabilityShaderViewportIndexLayerEXT); + state->spirvExtensions.push_back("SPV_EXT_shader_viewport_index_layer"); + state->viewportIndexLayer = true; + } + 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"); @@ -845,6 +1334,13 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u state->capabilities.push_back(spv::CapabilityRoundingModeRTE); state->capabilities.push_back(spv::CapabilityDenormPreserve); } + state->float16DenormPreserve = floatControls.shaderDenormPreserveFloat16 == VK_TRUE && + floatControls.denormBehaviorIndependence == VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_ALL; + 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); @@ -854,6 +1350,37 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u state->spirvExtensions.push_back("SPV_KHR_physical_storage_buffer"); state->depthRangeUnrestricted = hasExtension(VK_EXT_DEPTH_RANGE_UNRESTRICTED_EXTENSION_NAME); if (state->depthRangeUnrestricted) deviceExtensions.push_back(VK_EXT_DEPTH_RANGE_UNRESTRICTED_EXTENSION_NAME); + if (hasExtension(VK_EXT_SAMPLE_LOCATIONS_EXTENSION_NAME)) { + VkPhysicalDeviceSampleLocationsPropertiesEXT locations{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SAMPLE_LOCATIONS_PROPERTIES_EXT}; + VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &locations}; + 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"); + 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); + 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); + } + const char* fifoLatestReadyExtension = hasExtension(VK_KHR_PRESENT_MODE_FIFO_LATEST_READY_EXTENSION_NAME) ? VK_KHR_PRESENT_MODE_FIFO_LATEST_READY_EXTENSION_NAME : hasExtension("VK_EXT_present_mode_fifo_latest_ready") ? "VK_EXT_present_mode_fifo_latest_ready" : nullptr; + const char* requestedPresentMode = std::getenv("ANYPS5_PRESENT_MODE"); + VkPhysicalDevicePresentModeFifoLatestReadyFeaturesKHR fifoLatestReadyFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_MODE_FIFO_LATEST_READY_FEATURES_KHR}; + bool fifoLatestReady = false; + if (requestedPresentMode != nullptr && std::strcmp(requestedPresentMode, "fifo_latest_ready") == 0 && fifoLatestReadyExtension != nullptr) { + VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &fifoLatestReadyFeatures}; + state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &features); + fifoLatestReady = fifoLatestReadyFeatures.presentModeFifoLatestReady == VK_TRUE; + if (fifoLatestReady) deviceExtensions.push_back(fifoLatestReadyExtension); + } // Guest dispatches cover whole thread groups past the edge of small images; robust image access // drops those writes instead of faulting the device. const bool imageRobustness = hasExtension(VK_EXT_IMAGE_ROBUSTNESS_EXTENSION_NAME); @@ -885,9 +1412,16 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u 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. - if (const char* value = std::getenv("APS5_WORKING_SET_MIB")) { + if (const char* value = std::getenv("APS5_WORKING_SET_MIB"); value != nullptr && !compileOnly) { const SIZE_T bytes = static_cast(std::strtoull(value, nullptr, 10)) << 20u; if (!SetProcessWorkingSetSizeEx(GetCurrentProcess(), bytes, bytes * 2, QUOTA_LIMITS_HARDWS_MIN_DISABLE | QUOTA_LIMITS_HARDWS_MAX_DISABLE)) std::fprintf(stderr, "[gpu] SetProcessWorkingSetSizeEx failed: %lu\n", GetLastError()); } @@ -910,6 +1444,21 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u } listRestartFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRIMITIVE_TOPOLOGY_LIST_RESTART_FEATURES_EXT}; listRestartFeatures.primitiveTopologyListRestart = state->primitiveListRestart ? VK_TRUE : VK_FALSE; + VkPhysicalDeviceProvokingVertexFeaturesEXT provokingFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROVOKING_VERTEX_FEATURES_EXT}; + if (hasExtension(VK_EXT_PROVOKING_VERTEX_EXTENSION_NAME)) { + VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &provokingFeatures}; + state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &features); + state->provokingVertexLast = provokingFeatures.provokingVertexLast == VK_TRUE; + if (state->provokingVertexLast) { + VkPhysicalDeviceProvokingVertexPropertiesEXT provokingProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROVOKING_VERTEX_PROPERTIES_EXT}; + VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &provokingProperties}; + state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &properties); + state->provokingVertexModePerPipeline = provokingProperties.provokingVertexModePerPipeline == VK_TRUE; + deviceExtensions.push_back(VK_EXT_PROVOKING_VERTEX_EXTENSION_NAME); + } + } + provokingFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROVOKING_VERTEX_FEATURES_EXT}; + provokingFeatures.provokingVertexLast = state->provokingVertexLast ? VK_TRUE : VK_FALSE; VkPhysicalDeviceImageViewMinLodFeaturesEXT minLodFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_VIEW_MIN_LOD_FEATURES_EXT}; if (hasExtension(VK_EXT_IMAGE_VIEW_MIN_LOD_EXTENSION_NAME)) { VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &minLodFeatures}; @@ -935,6 +1484,62 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u state->depthClipControl = depthClipFeatures.depthClipControl == VK_TRUE; if (state->depthClipControl) deviceExtensions.push_back(VK_EXT_DEPTH_CLIP_CONTROL_EXTENSION_NAME); } + VkPhysicalDeviceNonSeamlessCubeMapFeaturesEXT nonSeamlessCubeFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_NON_SEAMLESS_CUBE_MAP_FEATURES_EXT}; + if (hasExtension(VK_EXT_NON_SEAMLESS_CUBE_MAP_EXTENSION_NAME)) { + VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &nonSeamlessCubeFeatures}; + state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &features); + state->nonSeamlessCubeMap = nonSeamlessCubeFeatures.nonSeamlessCubeMap == VK_TRUE; + if (state->nonSeamlessCubeMap) deviceExtensions.push_back(VK_EXT_NON_SEAMLESS_CUBE_MAP_EXTENSION_NAME); + } + 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 (state->mergedCoverageSampleCounts != 0) { + deviceExtensions.push_back(VK_NV_FRAMEBUFFER_MIXED_SAMPLES_EXTENSION_NAME); + deviceExtensions.push_back(VK_NV_COVERAGE_REDUCTION_MODE_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}; + 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}; + state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &features); + 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); + 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}; + state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &driverProperties); + state->pipelineLinkTimeOptimization = PipelineLinkTimeOptimizationUsable(driver.driverID); + } + } + 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}; if (state->meshShader) { deviceExtensions.insert(deviceExtensions.end(), meshExtensions.begin(), meshExtensions.end()); state->capabilities.push_back(5283); @@ -945,6 +1550,37 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u 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; @@ -955,18 +1591,73 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u enabled.shaderInt64 = VK_TRUE; enabled.shaderFloat64 = available.shaderFloat64 && floatControls.shaderSignedZeroInfNanPreserveFloat64; if (enabled.shaderFloat64) state->capabilities.push_back(spv::CapabilityFloat64); + state->float64DenormPreserve = enabled.shaderFloat64 && floatControls.shaderDenormPreserveFloat64 == VK_TRUE && + floatControls.denormBehaviorIndependence == VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_ALL; + if (state->float64DenormPreserve && std::find(state->capabilities.begin(), state->capabilities.end(), static_cast(spv::CapabilityDenormPreserve)) == state->capabilities.end()) state->capabilities.push_back(spv::CapabilityDenormPreserve); enabled.vertexPipelineStoresAndAtomics = VK_TRUE; enabled.fragmentStoresAndAtomics = VK_TRUE; enabled.tessellationShader = available.tessellationShader; state->tessellationShader = enabled.tessellationShader == VK_TRUE; if (state->tessellationShader) state->capabilities.push_back(3); - APS5_LOG_OUT("Vulkan features tessellationAvailable=%u mesh=%u depthClip=%u depthRangeUnrestricted=%u", static_cast(state->tessellationShader), static_cast(state->meshShader), static_cast(state->depthClipControl), static_cast(state->depthRangeUnrestricted)); + 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; enabled.textureCompressionBC = VK_TRUE; // 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[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"); + getFamilies(selected, &families, nullptr); + std::vector queues(families); + getFamilies(selected, &families, queues.data()); + const auto binds = [&](std::uint32_t index) { return index != family && queues[index].queueCount != 0 && (queues[index].queueFlags & VK_QUEUE_SPARSE_BINDING_BIT) != 0; }; + std::uint32_t sparseFamily = families; + for (std::uint32_t i = 0; i < families && sparseFamily == families; ++i) { + if (binds(i) && queues[i].queueFlags == VK_QUEUE_SPARSE_BINDING_BIT) sparseFamily = i; + } + for (std::uint32_t i = 0; i < families && sparseFamily == families; ++i) { + if (binds(i)) sparseFamily = i; + } + if (sparseFamily != families) { + queueInfos[1].queueFamilyIndex = sparseFamily; + queueInfos[1].queueCount = 1; + queueInfos[1].pQueuePriorities = &priority; + deviceInfo.queueCreateInfoCount = 2; + state->sparseFamily = sparseFamily; + state->sparseBinding = true; + } else { + state->sparseBinding = family < families && (queues[family].queueFlags & VK_QUEUE_SPARSE_BINDING_BIT) != 0; + } + enabled.sparseBinding = state->sparseBinding ? VK_TRUE : VK_FALSE; + } + deviceInfo.pQueueCreateInfos = queueInfos; + 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; @@ -979,10 +1670,13 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u state->depthBounds = enabled.depthBounds == VK_TRUE; enabled.depthBiasClamp = available.depthBiasClamp; state->depthBiasClamp = enabled.depthBiasClamp == VK_TRUE; + enabled.dualSrcBlend = available.dualSrcBlend; + state->dualSrcBlend = enabled.dualSrcBlend == VK_TRUE; enabled.occlusionQueryPrecise = available.occlusionQueryPrecise; 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; @@ -996,10 +1690,17 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u enabled.geometryShader = available.geometryShader; state->geometryShader = enabled.geometryShader == 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; + state->shaderClipDistance = enabled.shaderClipDistance == VK_TRUE; + state->shaderCullDistance = enabled.shaderCullDistance == VK_TRUE; + if (enabled.shaderClipDistance) state->capabilities.push_back(spv::CapabilityClipDistance); + if (enabled.shaderCullDistance) state->capabilities.push_back(spv::CapabilityCullDistance); if (enabled.shaderStorageImageWriteWithoutFormat) state->capabilities.push_back(spv::CapabilityStorageImageWriteWithoutFormat); if (enabled.shaderStorageImageReadWithoutFormat) state->capabilities.push_back(spv::CapabilityStorageImageReadWithoutFormat); + state->storageImageWithoutFormat = enabled.shaderStorageImageWriteWithoutFormat == VK_TRUE && enabled.shaderStorageImageReadWithoutFormat == VK_TRUE; // Bindless image tables index an image array with a wave-uniform runtime slot. enabled.shaderSampledImageArrayDynamicIndexing = available.shaderSampledImageArrayDynamicIndexing; enabled.shaderStorageImageArrayDynamicIndexing = available.shaderStorageImageArrayDynamicIndexing; @@ -1011,13 +1712,17 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &features); state->descriptorIndexing = descriptorIndexingFeatures.shaderSampledImageArrayNonUniformIndexing == VK_TRUE && descriptorIndexingFeatures.shaderStorageImageArrayNonUniformIndexing == VK_TRUE; state->storageBufferUpdateAfterBind = descriptorIndexingFeatures.descriptorBindingStorageBufferUpdateAfterBind == 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) 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; @@ -1027,9 +1732,19 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u 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; + if (state->runtimeDescriptorArray) { + state->capabilities.push_back(spv::CapabilityRuntimeDescriptorArray); + if (!state->descriptorIndexing) state->spirvExtensions.push_back("SPV_EXT_descriptor_indexing"); + } state->samplerAnisotropy = true; state->textureCompressionBC = true; deviceInfo.pEnabledFeatures = &enabled; + const char* calibratedTimestamps = !PerformanceTrace::Configured() ? nullptr : hasExtension(VK_KHR_CALIBRATED_TIMESTAMPS_EXTENSION_NAME) ? VK_KHR_CALIBRATED_TIMESTAMPS_EXTENSION_NAME : hasExtension(VK_EXT_CALIBRATED_TIMESTAMPS_EXTENSION_NAME) ? VK_EXT_CALIBRATED_TIMESTAMPS_EXTENSION_NAME : nullptr; + if (calibratedTimestamps != nullptr) deviceExtensions.push_back(calibratedTimestamps); deviceInfo.enabledExtensionCount = static_cast(deviceExtensions.size()); deviceInfo.ppEnabledExtensionNames = deviceExtensions.data(); if (state->meshShader) { @@ -1040,23 +1755,50 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u depthClipFeatures.pNext = const_cast(deviceInfo.pNext); deviceInfo.pNext = &depthClipFeatures; } + if (libraryDevice) { + renderingFeatures.pNext = const_cast(deviceInfo.pNext); + deviceInfo.pNext = &libraryFeatures; + } if (state->primitiveListRestart) { listRestartFeatures.pNext = const_cast(deviceInfo.pNext); deviceInfo.pNext = &listRestartFeatures; } + if (state->provokingVertexLast) { + provokingFeatures.pNext = const_cast(deviceInfo.pNext); + deviceInfo.pNext = &provokingFeatures; + } if (state->imageViewMinLod) { minLodFeatures.pNext = const_cast(deviceInfo.pNext); deviceInfo.pNext = &minLodFeatures; } + fifoLatestReadyFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_MODE_FIFO_LATEST_READY_FEATURES_KHR}; + fifoLatestReadyFeatures.presentModeFifoLatestReady = VK_TRUE; + if (fifoLatestReady) { + fifoLatestReadyFeatures.pNext = const_cast(deviceInfo.pNext); + deviceInfo.pNext = &fifoLatestReadyFeatures; + } if (state->maintenance8) { maintenance8Features.pNext = const_cast(deviceInfo.pNext); deviceInfo.pNext = &maintenance8Features; } + if (state->nonSeamlessCubeMap) { + nonSeamlessCubeFeatures.pNext = const_cast(deviceInfo.pNext); + deviceInfo.pNext = &nonSeamlessCubeFeatures; + } + if (state->mergedCoverageSampleCounts != 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; @@ -1077,15 +1819,23 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u 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) { + 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). @@ -1104,17 +1854,25 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &sizeProperties); state->computeWave32 = subgroupSizeFeatures.subgroupSizeControl == VK_TRUE && subgroupSize.minSubgroupSize <= 32u && subgroupSize.maxSubgroupSize >= 32u && (subgroupSize.requiredSubgroupSizeStages & VK_SHADER_STAGE_COMPUTE_BIT) != 0; + 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) { + 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(); subgroupSizeFeatures.pNext = byteFeatures.pNext; byteFeatures.pNext = &subgroupSizeFeatures; - APS5_LOG_OUT("Compute wave32 programs run on subgroups of %u", 32u); + 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; @@ -1141,29 +1899,103 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u 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}; - Graphics::Require(hasExtension(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME), "shader runtime requires VK_EXT_robustness2"); - VkPhysicalDeviceFeatures2 queried{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &robustness2}; - state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &queried); - Graphics::Require(robustness2.nullDescriptor == VK_TRUE, "shader runtime requires nullDescriptor"); - robustness2 = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT}; - robustness2.nullDescriptor = VK_TRUE; - robustness2.pNext = bdaFeatures.pNext; - bdaFeatures.pNext = &robustness2; - deviceExtensions.push_back(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME); - deviceInfo.enabledExtensionCount = static_cast(deviceExtensions.size()); - deviceInfo.ppEnabledExtensionNames = deviceExtensions.data(); + if (hasExtension(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME)) { + VkPhysicalDeviceFeatures2 queried{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &robustness2}; + state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &queried); + } + VkDriverId robustnessDriver = VK_DRIVER_ID_MAX_ENUM; + if (hasExtension(VK_KHR_DRIVER_PROPERTIES_EXTENSION_NAME)) { + VkPhysicalDeviceDriverProperties driver{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES}; + VkPhysicalDeviceProperties2 driverProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &driver}; + state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &driverProperties); + robustnessDriver = driver.driverID; + } + state->robustBufferAccess2 = robustness2.robustBufferAccess2 == VK_TRUE && enabled.robustBufferAccess == VK_TRUE && RobustBufferAccess2Usable(robustnessDriver); + if (robustness2.nullDescriptor == VK_TRUE || state->robustBufferAccess2) { + const bool nullDescriptor = robustness2.nullDescriptor == VK_TRUE; + robustness2 = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT}; + robustness2.nullDescriptor = 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); + deviceInfo.enabledExtensionCount = static_cast(deviceExtensions.size()); + deviceInfo.ppEnabledExtensionNames = deviceExtensions.data(); + } deviceInfo.pNext = &bdaFeatures; auto shaderProfile = std::make_unique(buildTarget(), deviceInfo, state->properties.limits); check(state->InstanceFunction("vkCreateDevice")(selected, &deviceInfo, nullptr, &state->device), "vkCreateDevice"); state->shaderProfile = std::move(shaderProfile); + if (compileOnly) return; + if (calibratedTimestamps != nullptr) { + const bool khr = std::strcmp(calibratedTimestamps, VK_KHR_CALIBRATED_TIMESTAMPS_EXTENSION_NAME) == 0; + const auto domains = reinterpret_cast(state->instanceProc(state->instance, khr ? "vkGetPhysicalDeviceCalibrateableTimeDomainsKHR" : "vkGetPhysicalDeviceCalibrateableTimeDomainsEXT")); + std::uint32_t domainCount = 0; + std::vector available; + if (domains != nullptr && domains(selected, &domainCount, nullptr) == VK_SUCCESS) { + available.resize(domainCount); + if (domains(selected, &domainCount, available.data()) != VK_SUCCESS) available.clear(); + } +#ifdef _WIN32 + const auto host = VK_TIME_DOMAIN_QUERY_PERFORMANCE_COUNTER_KHR; +#else + const auto host = VK_TIME_DOMAIN_CLOCK_MONOTONIC_KHR; +#endif + if (std::ranges::find(available, VK_TIME_DOMAIN_DEVICE_KHR) != available.end() && std::ranges::find(available, host) != available.end()) { + state->hostTimeDomain = host; + state->calibratedTimestamps = state->DeviceFunction(khr ? "vkGetCalibratedTimestampsKHR" : "vkGetCalibratedTimestampsEXT"); + } + } state->DeviceFunction("vkGetDeviceQueue")(state->device, family, 0, &state->queue); + 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); @@ -1172,12 +2004,19 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u 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()) { + std::unique_lock gpuLock(GuestMemory::GpuMutex(), std::defer_lock); + if (!GuestMemory::GpuMutex().HeldByThisThread()) gpuLock.lock(); + state->placeholderImages = std::make_unique(graphicsContext()); + } state->recorder = std::make_unique(graphicsContext(), state->timelineSemaphores); state->recorder->Activate(); state->context = buildContext(); @@ -1203,24 +2042,29 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u 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"); + std::vector presentModes(presentModeCount); + check(getPresentModes(selected, state->surface, &presentModeCount, presentModes.data()), "vkGetPhysicalDeviceSurfacePresentModesKHR"); + presentModes.resize(presentModeCount); + state->presentMode = SelectPresentMode(std::getenv("ANYPS5_PRESENT_MODE"), presentModes); + require((surface.supportedUsageFlags & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) != 0, "surface does not support color attachment images"); VkSwapchainCreateInfoKHR swapchain{VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR}; swapchain.surface = state->surface; swapchain.minImageCount = surface.minImageCount; - swapchain.imageFormat = VK_FORMAT_B8G8R8A8_UNORM; - swapchain.imageColorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; + swapchain.imageFormat = state->surfaceFormat; + swapchain.imageColorSpace = state->surfaceColorSpace; swapchain.imageExtent = state->extent; swapchain.imageArrayLayers = 1; - swapchain.imageUsage = VK_IMAGE_USAGE_TRANSFER_DST_BIT; + swapchain.imageUsage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; swapchain.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; swapchain.preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR; swapchain.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; - swapchain.presentMode = VK_PRESENT_MODE_FIFO_KHR; + swapchain.presentMode = state->presentMode; swapchain.clipped = VK_FALSE; check(state->DeviceFunction("vkCreateSwapchainKHR")(state->device, &swapchain, nullptr, &state->swapchain), "vkCreateSwapchainKHR"); std::uint32_t imageCount = 0; @@ -1243,19 +2087,102 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u check(createFence(state->device, &fence, nullptr, &slot.fence), "vkCreateFence present"); check(state->DeviceFunction("vkAllocateCommandBuffers")(state->device, &allocation, &slot.commands), "vkAllocateCommandBuffers"); } - state->scaler = std::make_unique(graphicsContext(), VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_B8G8R8A8_UNORM); + state->scaler = std::make_unique(graphicsContext(), VK_FORMAT_B8G8R8A8_UNORM, state->surfaceFormat); + state->presentation = std::make_unique(graphicsContext(), state->presentSlots.size()); } + 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); +} + 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() { @@ -1280,6 +2207,15 @@ bool OpportunisticReap() { } +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); +} + void VulkanDevice::SubmitRecorded(bool reapFirst) { if (!state->recorder) return; // Finished batches are retired first (a non-blocking fence status check on the front of the @@ -1326,7 +2262,26 @@ 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; + if (!recorder.PendingWriteSettled(address, bytes)) return false; + return Graphics::Recorder::PendingCompletionLabels() == 0 || !recorder.CompletionLabelIn(address, bytes); +} + +bool VulkanDevice::StoresPendingOver(std::uint64_t address, std::size_t bytes) const { + const std::array, 1> range{{{address, address + bytes}}}; + if (Graphics::StorageTexture::AnyPendingOverlaps(range) || Graphics::AnyShadowedOverlaps(range) || state->CopiedWriterOverlaps(address, bytes)) return true; + return state->recorder && (state->recorder->PendingWriteOverlaps(address, bytes) || state->recorder->PendingLabelIn(address, bytes)); +} + 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 @@ -1341,13 +2296,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 @@ -1376,10 +2325,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(); @@ -1411,9 +2362,9 @@ bool VulkanDevice::DumpSamplesOnGpu(std::uint64_t address) { const auto* import = Graphics::HostImportFor(context, address, bytes); if (import == nullptr || import->address == 0) return false; if (Graphics::AnyShadowedOverlaps(address, bytes)) Graphics::PublishShadow(address, bytes, Graphics::PublishScope::PartialUnits, Graphics::PublishReason::Label); - recorder.FlushStoresOverlapping(address, bytes); + if (!recorder.QueuesSampleDumps()) recorder.FlushStoresOverlapping(address, bytes); recorder.FlushKeyStoresOverlapping(address, bytes); - if (!recorder.DumpSamples(import->address + (address - import->base))) return false; + if (!recorder.DumpSamples(import->address + (address - import->base), address)) return false; recorder.NotePendingWrite(address, bytes); GuestMemory::MarkWritten(address, bytes); return true; @@ -1467,8 +2418,7 @@ bool VulkanDevice::FillBuffer(std::uint64_t address, std::size_t bytes, std::spa if (recorder.PendingWriteOverlaps(address, bytes)) { // Recorded draws with copied writes keep their own registry (Graphics::DrawCopiedWriters): // their CPU write-back would land after a fill recorded on the GPU, so they count too. - bool sync = alwaysSync || std::any_of(state->copiedWriters->begin(), state->copiedWriters->end(), [&](const auto& writer) { return writer->WritesOverlap(address, bytes); }) - || std::any_of(Graphics::DrawCopiedWriters()->begin(), Graphics::DrawCopiedWriters()->end(), [&](const auto& writer) { return writer->WritesOverlap(address, bytes); }); + bool sync = alwaysSync || state->CopiedWriterOverlaps(address, bytes); if (!sync && Graphics::Recorder::PendingCompletionLabels() != 0) { // A label the CPU will store after a completion must not be overwritten by the fill: // only a label inside the filled range matters, whatever the range's size. @@ -1493,7 +2443,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; @@ -1557,7 +2507,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]); @@ -1595,7 +2545,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); @@ -1611,70 +2561,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 || std::any_of(state->copiedWriters->begin(), state->copiedWriters->end(), [&](const auto& writer) { return writer->WritesOverlap(address, bytes); }) - || std::any_of(Graphics::DrawCopiedWriters()->begin(), Graphics::DrawCopiedWriters()->end(), [&](const auto& writer) { return writer->WritesOverlap(address, bytes); })) return true; - return Graphics::Recorder::PendingCompletionLabels() != 0 && recorder.PendingLabelIn(address, bytes); - }; + 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. @@ -1752,8 +2742,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(); @@ -1767,12 +2757,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; } @@ -1816,7 +2804,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. @@ -1826,6 +2814,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 @@ -1851,7 +2841,10 @@ void VulkanDevice::Resize(std::uint32_t width, std::uint32_t height) { return; } if (state->extent.width == width && state->extent.height == height) return; - WaitIdle(); + 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 @@ -1860,25 +2853,32 @@ void VulkanDevice::Resize(std::uint32_t width, std::uint32_t height) { width = surface.currentExtent.width; height = surface.currentExtent.height; if (width == 0 || height == 0) { + WaitIdle(); 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 = VK_FORMAT_B8G8R8A8_UNORM; - create.imageColorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; + create.imageFormat = state->surfaceFormat; + create.imageColorSpace = state->surfaceColorSpace; create.imageExtent = {width, height}; create.imageArrayLayers = 1; - create.imageUsage = VK_IMAGE_USAGE_TRANSFER_DST_BIT; + create.imageUsage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; create.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; create.preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR; create.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; - create.presentMode = VK_PRESENT_MODE_FIFO_KHR; + create.presentMode = state->presentMode; create.oldSwapchain = state->swapchain; state->retiredSwapchains.reserve(state->retiredSwapchains.size() + 1); VkSwapchainKHR replacement = VK_NULL_HANDLE; @@ -1893,9 +2893,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; } @@ -1915,20 +2998,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; @@ -1956,6 +3029,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); @@ -1964,26 +3038,15 @@ 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) { +ResidentPresent ResidentPresentable(const Graphics::Context& context, const Graphics::StorageTexture& image, const DisplayBuffer& buffer, VkFilter& filter, std::uint32_t& layer) { const auto& descriptor = image.Descriptor(); - if (descriptor.width != buffer.width || descriptor.height != buffer.height || descriptor.mipCount != 1 || image.ImageLayers() != 1 || image.ImageDepth() != 1) return ResidentPresent::None; - if (image.GuestBytes() != DisplayBufferSize(buffer)) return ResidentPresent::None; + if (descriptor.width != buffer.width || descriptor.height != buffer.height || descriptor.mipCount != 1 || image.ImageDepth() != 1) return ResidentPresent::None; + const auto slice = ResidentPresentSlice(descriptor.baseAddress, image.GuestBytes(), image.ImageLayers(), buffer.address, DisplayBufferSize(buffer)); + if (!slice) return ResidentPresent::None; + layer = *slice; const auto format = Graphics::StorageFormatForGuest(context, descriptor.format); VkFormatProperties properties{}; context.formatProperties(context.physical, format, &properties); @@ -1992,12 +3055,13 @@ ResidentPresent ResidentPresentable(const Graphics::Context& context, const Grap return ResidentPresentPath(format, buffer.pixelFormat, blitSource); } -std::shared_ptr PresentableResident(const Graphics::Context& context, const DisplayBuffer& buffer, VkFilter& filter, bool& pending, bool& convert) { - auto resident = Graphics::StorageTexture::FindPending(buffer.address, DisplayBufferSize(buffer)); +std::shared_ptr PresentableResident(const Graphics::Context& context, const DisplayBuffer& buffer, VkFilter& filter, bool& pending, bool& convert, std::uint32_t& layer) { + auto resident = Graphics::StorageTexture::FindPendingSlice(buffer.address, DisplayBufferSize(buffer)); pending = resident != nullptr; convert = false; + layer = 0; if (resident == nullptr) return resident; - const auto path = ResidentPresentable(context, *resident, buffer, filter); + const auto path = ResidentPresentable(context, *resident, buffer, filter, layer); if (path == ResidentPresent::None) resident.reset(); convert = path == ResidentPresent::Convert; return resident; @@ -2024,6 +3088,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); @@ -2031,6 +3103,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; @@ -2062,9 +3141,35 @@ FrameDumps& Dumps() { } bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { + static const bool storeAtFlip = Graphics::StorageTexture::StoreAtFlipRequested(std::getenv("APS5_STORE_AT_FLIP")); + if (storeAtFlip) Graphics::StorageTexture::FlushAllPending("flip"); + else Graphics::StorageTexture::FlushDumpSelected("flip"); + auto& dumps = Dumps(); + 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); - return present(buffer.width, buffer.height, true, pixels); + if (passthrough) ensureHdrScaler(DisplayTexelFormat(buffer.pixelFormat)); + const auto pixels = passthrough ? ReadDisplayBufferTexels(buffer) : ReadDisplayBuffer(buffer); + bool dumpFrame = false; + 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; + } + } + 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; // The display buffer is usually a resident render target whose results are still on the GPU: it @@ -2077,9 +3182,10 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { std::shared_ptr resident; VkFilter filter = VK_FILTER_LINEAR; bool convert = false; + std::uint32_t layer = 0; if (!NoResidentPresent()) { bool pending = false; - resident = PresentableResident(graphicsContext(), buffer, filter, pending, convert); + resident = PresentableResident(graphicsContext(), buffer, filter, pending, convert, layer); if (resident != nullptr && buffer.dccAddress != 0 && !ResidentServesDisplay(*resident, buffer)) { resident.reset(); convert = false; @@ -2094,6 +3200,8 @@ 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(); @@ -2103,13 +3211,10 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { } } const auto cleared = buffer.dccAddress != 0 && resident == nullptr ? CompressedClearPixel(buffer, bytes) : std::nullopt; - auto& dumps = Dumps(); bool dumpFrame = false; - if (dumps.dumped < dumps.limit && ++dumps.presents % static_cast(dumps.every) == 0) { - if (dumps.cpu) { - 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; @@ -2118,22 +3223,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)) { + 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"); @@ -2146,11 +3264,15 @@ bool VulkanDevice::AcquireImage() { std::uint32_t index = 0; timing.Mark("fence_reset"); // An out-of-date swapchain (the window changed) drops this frame; the next Resize recreates it. - const auto acquired = state->DeviceFunction("vkAcquireNextImageKHR")(state->device, state->swapchain, 5'000'000'000ULL, VK_NULL_HANDLE, state->acquireFence, &index); + const auto acquired = state->DeviceFunction("vkAcquireNextImageKHR")(state->device, state->swapchain, 0, VK_NULL_HANDLE, state->acquireFence, &index); if (acquired == VK_ERROR_OUT_OF_DATE_KHR) { state->extent = {0, 0}; return false; } + if (acquired == VK_NOT_READY || acquired == VK_TIMEOUT) { + ++presentCounters.acquireDrops; + return false; + } if (acquired != VK_SUBOPTIMAL_KHR) check(acquired, "vkAcquireNextImageKHR"); timing.Mark("acquire_image"); APS5_LOG_OUT_DEBUG("vkAcquireNextImageKHR index=%u imageCount=%zu", index, state->images.size()); @@ -2171,7 +3293,7 @@ bool VulkanDevice::AcquireImage() { return true; } -bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaque, std::span pixels, const DisplayBuffer* display, const std::shared_ptr& resident, VkFilter residentFilter, bool dumpFrame, bool residentConvert, const VkClearColorValue* uniform) { +bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaque, std::span pixels, const DisplayBuffer* display, const std::shared_ptr& resident, VkFilter residentFilter, bool dumpFrame, bool residentConvert, const VkClearColorValue* uniform, std::uint32_t residentLayer, 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"); @@ -2189,19 +3311,25 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu require(!residentConvert || (resident != nullptr && display != nullptr), "a converted resident presentation needs its image and display buffer"); require(uniform == nullptr || (pixels.empty() && display == nullptr && resident == nullptr), "a uniform presentation has no other source"); const bool direct = resident != nullptr && !residentConvert; + const bool drawn = resident != nullptr && residentConvert && !DisplayHalfFloat(display->pixelFormat); + 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 // (PresentWaitsForSlots); this is the fallback. - const bool shared = !clearOnly && !direct; - if ((shared || (!clearOnly && (state->scaler == nullptr || state->scaler->SourceWidth() != width || state->scaler->SourceHeight() != height))) && state->SharedSlotInFlight()) RetirePresents(0); + const bool shared = !clearOnly && !direct && !drawn; + if (shared && state->SharedSlotInFlight()) RetirePresents(0); // The next slot is the oldest; a caller that did not retire it first (PresentPixels) waits here. if (state->presentSlots[state->presentCursor].inFlight) RetirePresents(state->presentSlots.size() - 1); auto& slot = state->presentSlots[state->presentCursor]; 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); + state->colorTransfer->Upload(display->address, width, height, Graphics::ColorTileMode::RenderTarget, DisplayTexelBytes(display->pixelFormat)); } if (!pixels.empty()) state->Upload(pixels); // The frame's recorded work (and a refresh of the resident image) must reach the queue before the @@ -2243,8 +3371,10 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu barrier.image = state->images[index]; barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; auto pipelineBarrier = state->DeviceFunction("vkCmdPipelineBarrier"); - pipelineBarrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); - Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); + if (!drawn) { + pipelineBarrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); + } if (clearOnly) { APS5_LOG_OUT_DEBUG("Recording swapchain clear opaque=%u image=%p", static_cast(opaque), reinterpret_cast(barrier.image)); VkClearColorValue clear{}; @@ -2252,84 +3382,92 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu state->DeviceFunction("vkCmdClearColorImage")(commands, barrier.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clear, 1, &barrier.subresourceRange); } else { APS5_LOG_OUT_DEBUG("Recording swapchain scaled blit width=%u height=%u bytes=%zu buffer=%p image=%p", width, height, pixels.size(), reinterpret_cast(state->uploadBuffer), reinterpret_cast(barrier.image)); - require(state->scaler != nullptr, "presentation scaler is unavailable"); - state->scaler->EnsureSourceImage(width, height); + require(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; + const VkAccessFlags residentRead = drawn ? VK_ACCESS_SHADER_READ_BIT : VK_ACCESS_TRANSFER_READ_BIT; if (resident != nullptr) { // The draws and dispatches that produced the image were submitted before this command // buffer; their writes become visible to the blit here. The layout stays GENERAL, the // one the storage image is tracked in. residentBarrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT; - residentBarrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + residentBarrier.dstAccessMask = residentRead; residentBarrier.oldLayout = VK_IMAGE_LAYOUT_GENERAL; residentBarrier.newLayout = VK_IMAGE_LAYOUT_GENERAL; residentBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; residentBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; residentBarrier.image = resident->Image(); residentBarrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, VK_REMAINING_MIP_LEVELS, 0, VK_REMAINING_ARRAY_LAYERS}; - pipelineBarrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &residentBarrier); + pipelineBarrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, residentStage, 0, 0, nullptr, 0, nullptr, 1, &residentBarrier); Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); - if (residentConvert) { - state->colorTransfer->DetileImage(commands, resident->Image(), VK_IMAGE_LAYOUT_GENERAL, width, height, Graphics::ColorTileMode::RenderTarget, DisplayRedLow(display->pixelFormat), DisplayTenBit(display->pixelFormat)); - state->scaler->RecordUpload(commands, state->colorTransfer->LinearBuffer()); + 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); + scaler->RecordUpload(commands, state->colorTransfer->LinearBuffer()); } } else { - if (display != nullptr) { - state->colorTransfer->Detile(commands, DisplayRedLow(display->pixelFormat), DisplayTenBit(display->pixelFormat)); + 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(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); } - VkClearColorValue letterbox{}; - letterbox.float32[3] = 1.0f; - state->DeviceFunction("vkCmdClearColorImage")(commands, barrier.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &letterbox, 1, &barrier.subresourceRange); - VkImageMemoryBarrier letterboxBarrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; - letterboxBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - letterboxBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - letterboxBarrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - letterboxBarrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - letterboxBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - letterboxBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - letterboxBarrier.image = barrier.image; - letterboxBarrier.subresourceRange = barrier.subresourceRange; - pipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &letterboxBarrier); - Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); - if (direct) PresentationScaler::RecordBlitFrom(graphicsContext(), commands, resident->Image(), VK_IMAGE_LAYOUT_GENERAL, width, height, residentFilter, barrier.image, state->extent.width, state->extent.height); - else state->scaler->RecordBlit(commands, barrier.image, state->extent.width, state->extent.height); - if (dumpFrame && direct) { - const auto scale = DumpScale(); - slot.dumpWidth = (width + scale - 1) / scale; - slot.dumpHeight = (height + scale - 1) / scale; - slot.dumpScale = 1; - if (!slot.dumpScaler) slot.dumpScaler = std::make_unique(graphicsContext(), VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_B8G8R8A8_UNORM); - slot.dumpScaler->EnsureSourceImage(slot.dumpWidth, slot.dumpHeight); - slot.dumpScaler->RecordBlitInto(commands, resident->Image(), VK_IMAGE_LAYOUT_GENERAL, VK_FILTER_NEAREST, width, height); - } - if (resident != nullptr) { - // Later batches write the image again (the next frame into this buffer, a refresh): they - // start after the blit has read it. - residentBarrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; - residentBarrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT; - pipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &residentBarrier); - Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); + if (drawn) { + state->presentation->Bind(slotIndex, resident->Image(), width, height, display->pixelFormat, 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 { + 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 scaler->RecordBlit(commands, barrier.image, state->extent.width, state->extent.height); } if (dumpFrame) { - if (!direct) { - slot.dumpWidth = state->scaler->SourceWidth(); - slot.dumpHeight = state->scaler->SourceHeight(); - slot.dumpScale = 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 = scaler->SourceWidth(); + slot.dumpHeight = scaler->SourceHeight(); + slot.dumpScale = scale; } const auto dumpBytes = static_cast(slot.dumpWidth) * slot.dumpHeight * 4; if (!slot.dumpBuffer || slot.dumpBuffer->Bytes().size() != dumpBytes) slot.dumpBuffer = std::make_unique(graphicsContext(), dumpBytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); - (direct ? slot.dumpScaler : state->scaler)->RecordReadback(commands, slot.dumpBuffer->Handle()); + if (drawn) { + state->presentation->RecordDump(commands, slotIndex, scale, slot.dumpBuffer->Handle()); + } else if (direct) { + if (!slot.dumpScaler) slot.dumpScaler = std::make_unique(graphicsContext(), VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_B8G8R8A8_UNORM); + slot.dumpScaler->EnsureSourceImage(slot.dumpWidth, slot.dumpHeight); + slot.dumpScaler->RecordBlitInto(commands, resident->Image(), VK_IMAGE_LAYOUT_GENERAL, VK_FILTER_NEAREST, width, height, residentLayer); + slot.dumpScaler->RecordReadback(commands, slot.dumpBuffer->Handle()); + } else { + scaler->RecordReadback(commands, slot.dumpBuffer->Handle()); + } + } + if (resident != nullptr) { + residentBarrier.srcAccessMask = residentRead; + residentBarrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT; + pipelineBarrier(commands, residentStage | VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &residentBarrier); + Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); } } - barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - barrier.dstAccessMask = 0; - barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - barrier.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; - pipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); - Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); + if (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; + barrier.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + pipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); + } if (slot.queries != VK_NULL_HANDLE) state->DeviceFunction("vkCmdWriteTimestamp")(commands, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, slot.queries, 1); check(state->DeviceFunction("vkEndCommandBuffer")(commands), "vkEndCommandBuffer"); APS5_LOG_CHARS_OUT_DEBUG("Presentation command buffer recorded"); @@ -2355,6 +3493,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; @@ -2404,8 +3543,8 @@ bool VulkanDevice::PresentWaitsForSlots(const DisplayBuffer* buffer) const { VkFilter filter = VK_FILTER_LINEAR; bool pending = false; bool convert = false; - if (PresentableResident(graphicsContext(), *buffer, filter, pending, convert) == nullptr || convert) return true; - return state->scaler == nullptr || state->scaler->SourceWidth() != buffer->width || state->scaler->SourceHeight() != buffer->height; + std::uint32_t layer = 0; + return PresentableResident(graphicsContext(), *buffer, filter, pending, convert, layer) == nullptr; } void VulkanDevice::FlipBatches(std::uint64_t& submissions, std::uint64_t& unsignaled) const { @@ -2479,9 +3618,53 @@ std::string VulkanDevice::DeviceName() const { return state->properties.deviceName; } +std::string VulkanDevice::DriverInfo() const { + return state->driverInfo.empty() ? std::to_string(state->properties.driverVersion) : state->driverInfo; +} + +bool VulkanDevice::CalibrateClock(double& gpuNs, std::chrono::steady_clock::time_point& cpu) const { + if (state->calibratedTimestamps == nullptr) return false; + const std::array domains{VkCalibratedTimestampInfoKHR{VK_STRUCTURE_TYPE_CALIBRATED_TIMESTAMP_INFO_KHR, nullptr, VK_TIME_DOMAIN_DEVICE_KHR}, VkCalibratedTimestampInfoKHR{VK_STRUCTURE_TYPE_CALIBRATED_TIMESTAMP_INFO_KHR, nullptr, state->hostTimeDomain}}; + std::array stamps{}; + std::uint64_t deviation = 0; + if (state->calibratedTimestamps(state->device, static_cast(domains.size()), domains.data(), stamps.data(), &deviation) != VK_SUCCESS) return false; +#ifdef _WIN32 + LARGE_INTEGER counter{}; + LARGE_INTEGER frequency{}; + QueryPerformanceCounter(&counter); + const auto now = std::chrono::steady_clock::now(); + QueryPerformanceFrequency(&frequency); + const double sinceNs = static_cast(static_cast(counter.QuadPart) - stamps[1]) * 1e9 / static_cast(frequency.QuadPart); +#else + timespec monotonic{}; + clock_gettime(CLOCK_MONOTONIC, &monotonic); + const auto now = std::chrono::steady_clock::now(); + const double sinceNs = static_cast(static_cast(monotonic.tv_sec) * 1000000000ull + static_cast(monotonic.tv_nsec) - stamps[1]); +#endif + gpuNs = static_cast(stamps[0]) * state->properties.limits.timestampPeriod; + cpu = now - std::chrono::duration_cast(std::chrono::duration(sinceNs)); + return true; +} + +const std::uint32_t* VulkanDevice::WorkGroupCountLimit() const { + return state->properties.limits.maxComputeWorkGroupCount; +} + +std::uint32_t VulkanDevice::VendorId() const { + return state->properties.vendorID; +} + 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 { @@ -2490,7 +3673,16 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const { target.fragmentShaderBarycentricEnabled = state->fragmentShaderBarycentric; target.nonConstantImageOffsets = state->maintenance8; target.narrowSubgroupClock = state->narrowSubgroupClock; - target.srgbDecodeFormats = state->srgbDecodeFormats; + 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.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; target.mesh = ShaderRecompiler::MeshTargetLimits{{mesh.maxMeshWorkGroupSize[0], mesh.maxMeshWorkGroupSize[1], mesh.maxMeshWorkGroupSize[2]}, mesh.maxMeshWorkGroupInvocations, std::min(mesh.maxMeshSharedMemorySize, mesh.maxMeshPayloadAndSharedMemorySize), mesh.maxMeshOutputVertices, mesh.maxMeshOutputPrimitives, mesh.maxMeshOutputComponents, std::min(mesh.maxMeshOutputMemorySize, mesh.maxMeshPayloadAndOutputMemorySize), mesh.meshOutputPerVertexGranularity, mesh.meshOutputPerPrimitiveGranularity}; @@ -2499,10 +3691,24 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const { return target; } +std::optional VulkanDevice::GeometryLimits() const { + if (!state->geometryShader) return std::nullopt; + const auto& limits = state->properties.limits; + 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; } +bool VulkanDevice::DualSrcBlend() const { + return state->dualSrcBlend; +} + bool VulkanDevice::SamplerFilterMinmax() const { return state->samplerFilterMinmax; } @@ -2515,6 +3721,14 @@ VkShaderStageFlags VulkanDevice::SubgroupStages() const { return state->subgroup.supportedStages; } +bool VulkanDevice::ProvokingVertexLast() const { + return state->provokingVertexLast; +} + +bool VulkanDevice::GraphicsPipelineLibraries() const { + return state->graphicsPipelineLibrary; +} + Graphics::Context VulkanDevice::graphicsContext() const { static const bool noCache = std::getenv("APS5_NO_CONTEXT_CACHE") != nullptr; if (state->contextReady && !noCache) return state->context; @@ -2550,16 +3764,37 @@ Graphics::Context VulkanDevice::buildContext() const { state->depthClamp, 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; + context.dualSrcBlend = state->dualSrcBlend; context.depthBounds = state->depthBounds; context.depthBiasClamp = state->depthBiasClamp; context.samplerFilterMinmax = state->samplerFilterMinmax; + context.nonSeamlessCubeMap = state->nonSeamlessCubeMap; + context.mergedCoverageSampleCounts = state->mergedCoverageSampleCounts; 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; context.emptyBuffer = state->emptyBuffer ? state->emptyBuffer->Handle() : VK_NULL_HANDLE; @@ -2568,15 +3803,38 @@ 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.sampleRateShading = state->sampleRateShading; + context.storageImageMultisample = state->storageImageMultisample; + context.shaderClipDistance = state->shaderClipDistance; + context.shaderCullDistance = state->shaderCullDistance; + context.shaderTerminateInvocation = state->shaderTerminateInvocation; context.nullDescriptors = state->shaderProfile != nullptr && state->shaderProfile->NullDescriptors(); context.bufferInt64Atomics = state->bufferInt64Atomics; + context.placeholderImages = state->placeholderImages.get(); + context.viewportIndexLayer = state->viewportIndexLayer; + 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.memoryProperties2 = state->memoryBudget ? state->InstanceFunction("vkGetPhysicalDeviceMemoryProperties2") : nullptr; + context.srgbCopyFormats = state->srgbCopyFormats; + context.sampleLocationSampleCounts = state->sampleLocationSampleCounts; + 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; } @@ -2592,7 +3850,46 @@ void VulkanDevice::ColorMetadataPass(const Graphics::ColorMetadataPass& pass) { Graphics::RunColorMetadataPass(graphicsContext(), pass); } +bool VulkanDevice::PrebuildGraphicsLibraries(const Graphics::ShaderStages& stages, std::span shaders) { + PerformanceTimer timing("Vulkan.GraphicsPipelinePrebuild"); + const auto context = graphicsContext(); + if (!context.graphicsPipelineLibrary || context.descriptorCache == nullptr) return false; + Graphics::State state{}; + state.stages = stages; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.color.format = VK_FORMAT_R8G8B8A8_UNORM; + state.color.exportIndex = 0; + state.colors.push_back(state.color); + state.hasColorTarget = true; + state.blend.colorWriteMask = 15; + state.blends.push_back(state.blend); + state.renderExtent = {1, 1}; + std::vector group(shaders.begin(), shaders.end()); + if (group.size() == 1u) { + const bool vertex = group.front().stage == ShaderRecompiler::ShaderStage::Fragment; + static const auto stub = [](bool vertexStub) { + auto result = std::make_shared(); + const std::uint32_t model = vertexStub ? 0u : 4u; + std::vector words{0x07230203u, 0x00010000u, 0u, 6u, 0u, 0x00020011u, 1u, 0x0003000eu, 0u, 1u, 0x0005000fu, model, 4u, 0x6e69616du, 0u}; + if (!vertexStub) words.insert(words.end(), {0x00030010u, 4u, 7u}); + words.insert(words.end(), {0x00020013u, 2u, 0x00030021u, 3u, 2u, 0x00050036u, 2u, 4u, 0u, 3u, 0x000200f8u, 5u, 0x000100fdu, 0x00010038u}); + result->spirv = std::move(words); + result->variantId = vertexStub ? ~std::uint64_t{0} - 1u : ~std::uint64_t{0} - 2u; + result->runtimeAbiVersion = ShaderRecompiler::RuntimeAbi::Version; + return result; + }; + static const auto vertexStub = stub(true); + static const auto fragmentStub = stub(false); + if (vertex) group.insert(group.begin(), {ShaderRecompiler::ShaderStage::Vertex, vertexStub.get(), 0u}); + else group.push_back({ShaderRecompiler::ShaderStage::Fragment, fragmentStub.get(), Graphics::PipelinePushSlotBytes}); + } + const auto layouts = Graphics::DescribeSetLayouts(context, group); + return Graphics::Pipeline::Prebuild(context, state, Graphics::VertexInputLayout{}, layouts.View(), group); +} + 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); @@ -2606,6 +3903,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, @@ -2625,11 +3926,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)); @@ -2660,14 +3974,18 @@ 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; + std::uint64_t dispatchThreads = 0; enum PreparePhase : std::size_t { PrepareKey, PrepareFind, PreparePrecollect, PreparePresync, PrepareStageA, PreparePhaseCount }; std::array phaseMs{}; // 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 { @@ -2689,6 +4007,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}; }; @@ -2854,6 +4174,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(); @@ -3004,12 +4381,27 @@ struct RecordedDispatch { // 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; @@ -3071,23 +4463,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(); } @@ -3096,17 +4496,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) { +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; @@ -3116,6 +4525,7 @@ std::shared_ptr VulkanDevice::PrepareDispatch(const ShaderReco const Graphics::CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &shader, 0}; const auto context = graphicsContext(); auto prepared = std::make_shared(); + prepared->dispatchThreads = dispatchThreads; // A cached build (the map locks itself) is revalidated under the mutex by the dispatch, which // looks it up again by the key made here; the object stays local, only its surfaces matter // below. Anything inserted between here and the dispatch is simply replaced by this build. @@ -3131,11 +4541,20 @@ 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); } if (cached == nullptr) { - prepared->resources = std::make_shared(context, compute, snapshots, true); + prepared->resources = std::make_shared(context, compute, snapshots, true, dispatchThreads); phase(PreparedDispatch::PrepareStageA); } if (cached != nullptr && CachedPrecollect()) { @@ -3159,6 +4578,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; } @@ -3211,8 +4642,8 @@ void VulkanDevice::Dispatch(const ShaderRecompiler::RecompileResult& shader, std static_cast(dispatch(shader, x, y, z, 0, 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) { - 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) { @@ -3220,7 +4651,7 @@ void VulkanDevice::decideIndirect(RecordedDispatch& record, IndirectOutcome& out auto& timer = *record.timer; const auto& context = *record.context; const auto arguments = record.arguments; - const auto* limit = state->properties.limits.maxComputeWorkGroupCount; + 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 @@ -3243,10 +4674,12 @@ void VulkanDevice::decideIndirect(RecordedDispatch& record, IndirectOutcome& out outcome.cpuReason = 1; Graphics::Recorder::CountSync(2); recorder.Sync(); - } else if (labelPending() || std::any_of(state->copiedWriters->begin(), state->copiedWriters->end(), [&](const auto& writer) { return writer->WritesOverlap(arguments, 12); }) - || std::any_of(Graphics::DrawCopiedWriters()->begin(), Graphics::DrawCopiedWriters()->end(), [&](const auto& writer) { return writer->WritesOverlap(arguments, 12); })) { + } else if (record.shader->program->workgroupMemoryDwords != 0) { + 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; @@ -3259,22 +4692,26 @@ 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. record.arguments = 0; timer.indirect = false; std::snprintf(groupsText, 40, "%ux%ux%u", record.x, record.y, record.z); - if (record.x > limit[0] || record.y > limit[1] || record.z > limit[2]) throw std::runtime_error("Vulkan dispatch: workgroup count exceeds device limits"); } void VulkanDevice::recordDispatch(RecordedDispatch& record) { @@ -3315,7 +4752,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 @@ -3333,6 +4776,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) { @@ -3364,14 +4825,71 @@ 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); + recorder.Keep(std::move(memory)); + } if (record.pushStages != 0) { - context.Resolved(&Graphics::DeviceFunctions::cmdPushConstants, "vkCmdPushConstants")(commands, record.objects->layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, Graphics::PipelinePushConstantBytes, record.pushBytes->data()); + 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 (argumentImport != nullptr) context.Resolved(&Graphics::DeviceFunctions::cmdDispatchIndirect, "vkCmdDispatchIndirect")(commands, argumentImport->buffer, arguments - argumentImport->base); - else context.Resolved(&Graphics::DeviceFunctions::cmdDispatch, "vkCmdDispatch")(commands, record.x, record.y, record.z); + if (state->checkpointMarker != nullptr) state->checkpointMarker(commands, reinterpret_cast(static_cast(record.programAddress))); + if (argumentImport != nullptr) { + 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 { + 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); + if (record.shader->program->workgroupMemoryDwords != 0) { + 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); + } + 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); + } + 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); + bindPipeline(commands, VK_PIPELINE_BIND_POINT_COMPUTE, splitComputePipeline(context, *record.shader->program, *record.objects, {xBase, yBase, zBase})); + } + firstChunk = false; + dispatchChunk(commands, std::min(limit[0], record.x - xBase), std::min(limit[1], record.y - yBase), std::min(limit[2], record.z - zBase)); + } + } + } + } + } recorder.EndGpuTiming(gpuTiming); const auto trailingTiming = recorder.BeginGpuTiming(CommandClass::DispatchTrailing); constexpr VkAccessFlags dispatchedAccess = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_READ_BIT; @@ -3384,7 +4902,10 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { 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()) { + if (resources.NeedsCompletion() && resources.Reusable()) { + auto kept = record.resources; + recorder.OnComplete([kept] { kept->CheckFaults(); }); + } else if (resources.NeedsCompletion()) { // Listed for DispatchIndirect until the write-back ran (a non-reusable object, so once). auto writers = state->copiedWriters; auto kept = record.resources; @@ -3394,12 +4915,148 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { kept->WriteBackBuffers(); }); // Listed after the registration: a throw there leaves nothing that would pin the CPU path forever. - writers->push_back(kept); + if (resources.HasCopiedWrites()) writers->push_back(kept); } 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) { + 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"); if (recipeOut != nullptr) *recipeOut = nullptr; @@ -3413,20 +5070,14 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec } const std::array shaders{{{ShaderRecompiler::ShaderStage::Compute, &shader, 0}}}; const auto pushStages = Graphics::PushConstantStages(shaders); - if (pushStages != 0 && state->properties.limits.maxPushConstantsSize < Graphics::PipelinePushConstantBytes) { + if (pushStages != 0 && state->properties.limits.maxPushConstantsSize < Graphics::ComputePushConstantBytes) { throw std::runtime_error("Vulkan dispatch: compute push constant range exceeds device limit"); } auto pushBytes = Graphics::AssemblePushConstants(shaders); const auto context = graphicsContext(); - const auto* limit = state->properties.limits.maxComputeWorkGroupCount; - if (arguments == 0 && (x > limit[0] || y > limit[1] || z > limit[2])) { - throw std::runtime_error("Vulkan dispatch: workgroup count exceeds device limits"); - } - // 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; } @@ -3444,7 +5095,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. @@ -3464,6 +5117,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 = [&] { @@ -3474,7 +5132,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(); @@ -3489,6 +5150,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); @@ -3511,6 +5173,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) { @@ -3518,17 +5181,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) { @@ -3539,10 +5248,11 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec } if (resources == nullptr) { const auto buildStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; - resources = std::make_shared(context, shaders[0], snapshots); + resources = std::make_shared(context, shaders[0], snapshots, false, prepared != nullptr ? prepared->dispatchThreads : 0); 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(); } } @@ -3562,53 +5272,23 @@ 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 auto setLayout = resources->Layout(); - const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, Graphics::PipelinePushConstantBytes}; - VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; - layoutInfo.setLayoutCount = 1; - layoutInfo.pSetLayouts = &setLayout; - layoutInfo.pushConstantRangeCount = pushStages != 0 ? 1 : 0; - layoutInfo.pPushConstantRanges = pushStages != 0 ? &push : nullptr; - check(state->DeviceFunction("vkCreatePipelineLayout")(state->device, &layoutInfo, nullptr, &objects->layout), "vkCreatePipelineLayout"); - VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; - pipelineInfo.flags = context.pipelineExecutableInfo ? VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR : 0; - pipelineInfo.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; - pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; - pipelineInfo.stage.module = objects->module; - pipelineInfo.stage.pName = "main"; - const Graphics::PipelineSpecialization specialization(shader); - pipelineInfo.stage.pSpecializationInfo = specialization.Info(); - VkPipelineShaderStageRequiredSubgroupSizeCreateInfoEXT requiredSubgroup{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_REQUIRED_SUBGROUP_SIZE_CREATE_INFO_EXT}; - requiredSubgroup.requiredSubgroupSize = std::min(shader.hostSubgroupSize, state->maxComputeSubgroupSize); - if (state->computeWave32 && (shader.hostSubgroupSize == 32u || shader.hostSubgroupSize > state->maxComputeSubgroupSize)) pipelineInfo.stage.pNext = &requiredSubgroup; - pipelineInfo.layout = objects->layout; - 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)); - check(state->DeviceFunction("vkCreateComputePipelines")(state->device, context.pipelineCache, 1, &pipelineInfo, nullptr, &objects->pipeline), "vkCreateComputePipelines"); - Graphics::LogPipelineStatistics_nid_no_patch(context, objects->pipeline); - 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; + if (verifySetLayouts && context.descriptorCache != nullptr) { + const auto described = Graphics::DescribeSetLayouts(context, shaders); + require(described.layouts.main == resources->Layout() && !described.layouts.split, "the set layout described from the compute shader differs from the dispatch's"); } + const auto layouts = resources->SetLayouts(); + objects = computePipeline(context, shader, layouts.main, layouts.mainKey, pushStages, programAddress); + timing.Mark("pipeline_create"); timer.phase(PhasePipeline); } // Recorded into the device's open batch: the CPU moves on to the next command while the GPU @@ -3617,6 +5297,8 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec IndirectOutcome outcome{0, 0}; RecordedDispatch record{&context, &shaders[0], resources, objects, pushStages, &pushBytes, x, y, z, arguments, nullptr, programAddress, fromCache && TemplateDataRefresh() ? RecordedDispatch::DataRefresh::Words : RecordedDispatch::DataRefresh::None, 0, revalidateMs, &timer}; record.resourceKeyMatched = fromCache; + record.bindings = overlay; + 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; @@ -3670,7 +5352,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() && 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; @@ -3691,7 +5373,8 @@ 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"); outcome = {0, 0}; @@ -3703,10 +5386,6 @@ RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResu auto& d = Dispatches(); const std::array shaders{{{ShaderRecompiler::ShaderStage::Compute, &shader, 0}}}; const auto context = graphicsContext(); - const auto* limit = state->properties.limits.maxComputeWorkGroupCount; - if (arguments == 0 && (x > limit[0] || y > limit[1] || z > limit[2])) { - throw std::runtime_error("Vulkan dispatch: workgroup count exceeds device limits"); - } if (recipe.needsCompletion || recipe.holdsLease || !hit->resources->Reusable()) throw std::runtime_error("Vulkan dispatch: recipe over a non-reusable template"); // Without the refresh the template holds the words it was keyed with: a data-only hit takes // the ordinary path, whose word-keyed lookup serves or builds the object for the live words. @@ -3738,11 +5417,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; @@ -3774,6 +5454,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; @@ -3788,6 +5469,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 e48bdbbbc1..7bbca06017 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp @@ -3,9 +3,11 @@ #include #include #ifdef _WIN32 +#include "prx/libSceAgcDriver/Execution/include/StackWalk.hpp" #include #include #include +#include #include #include #include @@ -25,47 +27,139 @@ 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. struct Sampler { + static constexpr int ProcessDepth = 12; + static constexpr int WorkerDepth = 64; + HANDLE target = nullptr; std::string path; + int depth = ProcessDepth; + bool keepStacks = false; + std::uint64_t stackLow = 0; + std::uint64_t stackHigh = 0; + std::vector modules; + std::uint64_t pendingModule = 0; std::mutex mutex; std::map> counts; + CollapsedStacks stacks; std::uint64_t samples = 0; + std::int64_t suspendedTicks = 0; + std::int64_t longestSuspendedTicks = 0; ~Sampler() noexcept(false) { if (target != nullptr && !CloseHandle(target)) throw std::system_error(GetLastError(), std::system_category(), "Closing sampler thread handle"); } - void sample() { - CONTEXT context{}; - context.ContextFlags = CONTEXT_FULL; - if (SuspendThread(target) == static_cast(-1)) return; - const bool captured = GetThreadContext(target, &context) != 0; - std::vector frames; - if (captured) { - // Unwinding reads the target's stack while it is suspended; frames without unwind data - // (JIT/guest code) end the walk. - for (int depth = 0; depth < 12 && context.Rip != 0; ++depth) { - frames.push_back(context.Rip); - DWORD64 imageBase = 0; - auto* entry = RtlLookupFunctionEntry(context.Rip, &imageBase, nullptr); - if (entry == nullptr) break; + bool readStack(std::uint64_t address, std::uint64_t& value) const { + if (address < stackLow || address > stackHigh - sizeof(value)) return false; + std::memcpy(&value, reinterpret_cast(address), sizeof(value)); + return true; + } + + const DwarfFrames* moduleOf(std::uint64_t pc) const { + const auto found = std::upper_bound(modules.begin(), modules.end(), pc, [](std::uint64_t value, const DwarfFrames& module) { return value < module.Base(); }); + return found != modules.begin() && std::prev(found)->Contains(pc) ? &*std::prev(found) : nullptr; + } + + bool hasUnwindData(std::uint64_t pc) const { + DWORD64 imageBase = 0; + const auto* module = moduleOf(pc - 1); + return RtlLookupFunctionEntry(pc, &imageBase, nullptr) != nullptr || (module != nullptr && module->Covers(pc - 1)); + } + + DwarfFrames::Step stepDwarf(CONTEXT& context, bool leaf) { + const auto* module = moduleOf(context.Rip - (leaf ? 0 : 1)); + if (module == nullptr) { + MEMORY_BASIC_INFORMATION memory{}; + if (VirtualQuery(reinterpret_cast(context.Rip), &memory, sizeof(memory)) != 0 && memory.Type == MEM_IMAGE) pendingModule = reinterpret_cast(memory.AllocationBase); + return DwarfFrames::Step::Failed; + } + DwarfFrames::Registers registers{context.Rax, context.Rdx, context.Rcx, context.Rbx, context.Rsi, context.Rdi, context.Rbp, context.Rsp, context.R8, context.R9, context.R10, context.R11, context.R12, context.R13, context.R14, context.R15, context.Rip}; + const auto step = module->Unwind(registers, leaf, [&](std::uint64_t address, std::uint64_t& value) { return readStack(address, value); }); + if (step != DwarfFrames::Step::Stepped) return step; + DWORD64* columns[] = {&context.Rax, &context.Rdx, &context.Rcx, &context.Rbx, &context.Rsi, &context.Rdi, &context.Rbp, &context.Rsp, &context.R8, &context.R9, &context.R10, &context.R11, &context.R12, &context.R13, &context.R14, &context.R15, &context.Rip}; + for (std::size_t column = 0; column < registers.size(); ++column) *columns[column] = registers[column]; + return step; + } + + int walk(CONTEXT& context, std::array& frames) { + const bool dwarf = stackHigh != 0 && context.Rsp >= stackLow && context.Rsp < stackHigh; + int count = 0; + while (count < depth && context.Rip != 0) { + frames[count++] = context.Rip; + DWORD64 imageBase = 0; + if (auto* entry = RtlLookupFunctionEntry(context.Rip, &imageBase, nullptr)) { PVOID handler = nullptr; DWORD64 establisher = 0; RtlVirtualUnwind(UNW_FLAG_NHANDLER, imageBase, context.Rip, entry, &context, &handler, &establisher, nullptr); + continue; } + if (!dwarf) break; + const auto step = stepDwarf(context, count == 1); + if (step == DwarfFrames::Step::Stepped) continue; + std::uint64_t returnAddress = 0; + if (step == DwarfFrames::Step::Failed || count != 1 || !readStack(context.Rsp, returnAddress) || !hasUnwindData(returnAddress)) break; + context.Rip = returnAddress; + context.Rsp += sizeof(returnAddress); } + return count; + } + + void indexModule() { + const auto base = std::exchange(pendingModule, 0); + if (moduleOf(base) != nullptr) return; + const auto* dos = reinterpret_cast(base); + const auto* nt = reinterpret_cast(base + dos->e_lfanew); + const auto* sections = IMAGE_FIRST_SECTION(nt); + const DwarfFrames::Byte* frames = nullptr; + const DwarfFrames::Byte* framesEnd = nullptr; + for (unsigned index = 0; index < nt->FileHeader.NumberOfSections; ++index) { + if (std::memcmp(sections[index].Name, ".ehfram", 8) != 0) continue; + frames = reinterpret_cast(base + sections[index].VirtualAddress); + framesEnd = frames + sections[index].Misc.VirtualSize; + } + const auto at = std::upper_bound(modules.begin(), modules.end(), base, [](std::uint64_t value, const DwarfFrames& module) { return value < module.Base(); }); + modules.emplace(at, base, base + nt->OptionalHeader.SizeOfImage, frames, framesEnd); + } + + void sample() { + CONTEXT context{}; + context.ContextFlags = CONTEXT_FULL; + std::array frames; + int count = 0; + LARGE_INTEGER suspended{}; + LARGE_INTEGER resumed{}; + QueryPerformanceCounter(&suspended); + if (SuspendThread(target) == static_cast(-1)) return; + if (GetThreadContext(target, &context) != 0) count = walk(context, frames); ResumeThread(target); + QueryPerformanceCounter(&resumed); + if (pendingModule != 0) indexModule(); std::lock_guard lock(mutex); ++samples; - for (std::size_t i = 0; i < frames.size(); ++i) { - auto& count = counts[frames[i]]; - if (i == 0) ++count.first; - ++count.second; + suspendedTicks += resumed.QuadPart - suspended.QuadPart; + longestSuspendedTicks = std::max(longestSuspendedTicks, resumed.QuadPart - suspended.QuadPart); + for (int i = 0; i < count; ++i) { + auto& entry = counts[frames[i]]; + if (i == 0) ++entry.first; + ++entry.second; } + if (keepStacks && count != 0) stacks.Add({frames.data(), static_cast(count)}, context.Rip != 0); + } + + static std::string frameName(std::uint64_t address, std::map& loaded) { + HMODULE module = nullptr; + char name[MAX_PATH] = "?"; + if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(address), &module)) GetModuleFileNameA(module, name, sizeof(name)); + const char* base = std::strrchr(name, '\\'); + if (module != nullptr) loaded.emplace(reinterpret_cast(module), base ? base + 1 : name); + char frame[MAX_PATH + 32]; + std::snprintf(frame, sizeof(frame), "%s+0x%llx", base ? base + 1 : name, static_cast(address - reinterpret_cast(module))); + return frame; } void write() { @@ -81,6 +175,34 @@ struct Sampler { std::fprintf(file, "%s 0x%llx %llu %llu\n", base ? base + 1 : name, static_cast(address - reinterpret_cast(module)), static_cast(count.first), static_cast(count.second)); } std::fclose(file); + if (keepStacks) writeStacks(); + } + + void writeStacks() { + const auto dot = path.rfind('.'); + const auto slash = path.find_last_of("\\/"); + const auto stem = dot != std::string::npos && (slash == std::string::npos || dot > slash) ? path.substr(0, dot) : path; + const auto collapsed = stem + ".collapsed.txt"; + const auto temporary = collapsed + ".tmp"; + std::map names; + std::map loaded; + for (const auto& [address, count] : counts) names.emplace(address, frameName(address, loaded)); + std::FILE* file = std::fopen(temporary.c_str(), "w"); + if (file == nullptr) return; + LARGE_INTEGER frequency{}; + QueryPerformanceFrequency(&frequency); + const double tickUs = 1e6 / static_cast(frequency.QuadPart); + std::fprintf(file, "# samples=%llu depth=%d truncated=%llu suspended_us_mean=%.1f suspended_us_max=%.1f\n", static_cast(samples), depth, static_cast(stacks.Truncated()), + samples != 0 ? static_cast(suspendedTicks) * tickUs / static_cast(samples) : 0.0, static_cast(longestSuspendedTicks) * tickUs); + for (const auto& [base, name] : loaded) { + const auto* dos = reinterpret_cast(base); + const auto* nt = reinterpret_cast(base + dos->e_lfanew); + std::fprintf(file, "# module %s 0x%lx 0x%lx\n", name.c_str(), static_cast(nt->OptionalHeader.SizeOfImage), static_cast(nt->FileHeader.TimeDateStamp)); + } + const auto text = stacks.Format([&](std::uint64_t address) { return names.at(address); }); + const bool written = std::fwrite(text.data(), 1, text.size(), file) == text.size(); + if (std::fclose(file) != 0 || !written) return; + MoveFileExA(temporary.c_str(), collapsed.c_str(), MOVEFILE_REPLACE_EXISTING); } }; @@ -164,16 +286,20 @@ 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; + sampler->depth = Sampler::WorkerDepth; + sampler->keepStacks = true; + const auto* block = reinterpret_cast(NtCurrentTeb()); + MEMORY_BASIC_INFORMATION stack{}; + if (VirtualQuery(block->StackLimit, &stack, sizeof(stack)) != 0) { + sampler->stackLow = reinterpret_cast(stack.AllocationBase); + sampler->stackHigh = reinterpret_cast(block->StackBase); + } if (!DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), &sampler->target, THREAD_ALL_ACCESS, FALSE, 0)) throw std::system_error(GetLastError(), std::system_category(), "Duplicating sampler thread handle"); - workerSamplerThread = std::jthread([sampler = std::move(sampler)](std::stop_token token) { + 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)); @@ -187,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 f73ee10c21..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 @@ -18,10 +20,19 @@ struct BufferAllocation { void* mapping; VkDeviceAddress address; VkDeviceSize allocationBytes; - // Size the VkBuffer was created with (see BufferPool::Capacity), not the size a user asked for. std::size_t bytes; VkBufferUsageFlags usage; VkMemoryPropertyFlags properties = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + VkDeviceSize offset = 0; + bool slab = false; + std::size_t bufferBytes = 0; + std::uint32_t memoryType = 0; +}; + +struct SlabSlot { + VkDeviceMemory memory; + VkDeviceSize offset; + void* mapping; }; // Released buffer allocations kept for reuse, since creating, binding and mapping one costs tens of @@ -43,37 +54,61 @@ struct BufferAllocation { // the 256-byte copied-region buffers to fit, and those then missed on every build (81869 misses // and 80850 evictions per run, each a Vulkan create or destroy under the pool mutex, 3.5 s per // run). APS5_BUFFER_POOL_SHARED=1 keeps one tier as before. -// -// Device-local allocations (the detiler's scratch buffers and the staging shadows of written guest -// buffers, see GuestBufferMemory) are retained in a third tier with a budget of their own, video -// memory instead of pinned host memory: APS5_STAGING_POOL_MIB (default 512); 0 keeps them in the -// two host tiers as before. class BufferPool { public: explicit BufferPool(const Context& context); + 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, 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); + static VkDeviceSize SlabBlockBytes(std::size_t capacity); + SlabSlot TakeSlot(const Context& context, std::uint32_t memoryType, std::size_t capacity, bool addressable); + VkDeviceSize PutSlot(VkDeviceMemory memory, VkDeviceSize offset) noexcept; + std::size_t SlabBlocks(); private: + struct SlabBlock { + VkDeviceMemory memory = VK_NULL_HANDLE; + std::byte* mapping = nullptr; + VkDeviceSize slotBytes = 0; + std::vector free; + std::uint32_t used = 0; + std::uint64_t slab = 0; + }; + 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); + void freeBlock(SlabBlock& block) noexcept; 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 @@ -91,28 +126,50 @@ private: // The tier a buffer of `capacity` and `properties` is retained in (the large one for everything // when shared; the device tier for device-local memory while it has a budget). Tier& tierFor(std::size_t capacity, VkMemoryPropertyFlags properties); - // The device tier's budget (APS5_STAGING_POOL_MIB), read once. - static VkDeviceSize DeviceBudget(); + static bool DeviceTierEnabled(); + static VkDeviceSize deviceBudget(const Context& context); static bool DeviceTiered(VkMemoryPropertyFlags properties); static std::size_t NextCapacity(std::size_t capacity); - void destroy(const BufferAllocation& allocation) noexcept; + VkDeviceSize destroy(const BufferAllocation& allocation) noexcept; + VkDeviceSize trim(); // Moves the tier's least recently used slot (the oldest front of its lists) to `evicted`; the // 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; + std::unordered_map> slabBlocks; static constexpr VkDeviceSize budget = 512ull * 1024 * 1024; // The small tier's own budget (slots of at most half a MiB each): pinned host memory the large // tier's budget does not count. @@ -124,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/ColorTargetLayout.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp index 78112575aa..f8acde495e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp @@ -2,6 +2,7 @@ #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_COLORTARGETLAYOUT_HPP #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include #include #include @@ -9,25 +10,40 @@ namespace AgcDriver::Graphics { +struct TextureSwizzleEquation; + enum class ColorTileMode : std::uint32_t { Linear = 0, Standard4KB = 5, Standard64KB = 9, + S4KBX = 0x15, + D4KBX = 0x16, + Z64KBX = 0x18, + D64KBX = 0x1a, RenderTarget = 0x1b }; +constexpr bool ColorTileModeIsXor(ColorTileMode mode) { + return mode == ColorTileMode::S4KBX || mode == ColorTileMode::D4KBX || mode == ColorTileMode::Z64KBX || mode == ColorTileMode::D64KBX || mode == ColorTileMode::RenderTarget; +} + +constexpr std::uint32_t ColorTileModeBlockBytes(ColorTileMode mode) { + return mode == ColorTileMode::Linear ? 256u : mode == ColorTileMode::Standard4KB || mode == ColorTileMode::S4KBX || mode == ColorTileMode::D4KBX ? 4096u : 65536u; +} + constexpr TextureTileMode ColorTextureTileMode(ColorTileMode mode) { - return mode == ColorTileMode::Linear ? TextureTileMode::kLinear : mode == ColorTileMode::Standard4KB ? TextureTileMode::kStandard4KB : mode == ColorTileMode::Standard64KB ? TextureTileMode::kStandard64KB : TextureTileMode::kR64KBX; + return mode == ColorTileMode::Linear ? TextureTileMode::kLinear : mode == ColorTileMode::Standard4KB ? TextureTileMode::kStandard4KB : mode == ColorTileMode::Standard64KB ? TextureTileMode::kStandard64KB : mode == ColorTileMode::S4KBX ? TextureTileMode::kS4KBX : mode == ColorTileMode::D4KBX ? TextureTileMode::kD4KBX : mode == ColorTileMode::D64KBX ? TextureTileMode::kD64KBX : mode == ColorTileMode::Z64KBX ? TextureTileMode::kZ64KBX : TextureTileMode::kR64KBX; } ColorTileMode DecodeColorTileMode(std::uint32_t attrib3); +TileMipLayout ComputeColorTargetMipLayout(ColorTileMode mode, std::uint32_t bytesPerElement, std::uint32_t surfaceWidth, std::uint32_t surfaceHeight, std::uint32_t mipCount, std::uint32_t mipLevel); class ColorTargetLayout { public: - ColorTargetLayout(std::uint32_t width, std::uint32_t height, ColorTileMode mode, std::uint32_t bytesPerElement = 4); + ColorTargetLayout(std::uint32_t width, std::uint32_t height, ColorTileMode mode, std::uint32_t bytesPerElement = 4, std::uint32_t pipeBankXor = 0); std::size_t Bytes() const { return bytes; } std::size_t LinearBytes() const { return static_cast(width) * height * elementBytes; } - std::size_t Alignment() const { return mode == ColorTileMode::Linear ? 256u : mode == ColorTileMode::Standard4KB ? 4096u : 65536u; } + std::size_t Alignment() const { return ColorTileModeBlockBytes(mode); } std::uint32_t BlocksPerRow() const { return pitch / blockWidth; } std::size_t Offset(std::uint32_t x, std::uint32_t y) const; void Detile(std::span source, std::span destination) const; @@ -43,9 +59,28 @@ private: std::uint32_t elementBytes; std::uint32_t blockWidth = 1; std::uint32_t blockHeight = 1; + std::uint32_t pipeBankXor = 0; // The XOR swizzle is linear over GF(2), so a block offset is xOffsets[x] ^ yOffsets[y]. const std::uint32_t* xOffsets = nullptr; const std::uint32_t* yOffsets = nullptr; + const TextureSwizzleEquation* equation = nullptr; +}; + +class CmaskLayout { +public: + static constexpr std::size_t Alignment = 4096; + CmaskLayout(std::uint32_t width, std::uint32_t height); + std::size_t Bytes() const { return bytes; } + std::uint32_t BlocksPerRow() const { return blocksPerRow; } + std::uint32_t TilesX() const { return (width + 7u) / 8u; } + std::uint32_t TilesY() const { return (height + 7u) / 8u; } + std::size_t Nibble(std::uint32_t tileX, std::uint32_t tileY) const; + +private: + std::uint32_t width; + std::uint32_t height; + std::uint32_t blocksPerRow; + std::size_t bytes; }; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index ed079a89b7..15f785bde7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -4,15 +4,24 @@ #ifndef VK_NO_PROTOTYPES #define VK_NO_PROTOTYPES #endif +#include #include +#include +#include #include +#include +#include #include +#include +#include +#include #include #include #include namespace AgcDriver::Graphics { + class TextureDetiler; class GpuColorTransfer; class BufferPool; @@ -24,19 +33,70 @@ class Recorder; class DescriptorCache; class SamplerCache; class ShaderResources; +class PlaceholderImages; +class VertexInputCache; + +class PipelineCacheSynchronization { +public: + using PipelineUseLock = std::shared_lock; + using SnapshotLock = std::unique_lock; + + PipelineUseLock LockForPipelineUse() const { return PipelineUseLock(mutex); } + SnapshotLock LockForSnapshot() const { return SnapshotLock(mutex); } + SnapshotLock TryLockForSnapshot() const { return SnapshotLock(mutex, std::try_to_lock); } + +private: + mutable std::shared_mutex mutex; +}; inline void Require(bool condition, const std::string& reason) { if (!condition) throw std::runtime_error("AGC graphics: " + reason); } +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). @@ -52,10 +112,23 @@ struct DeviceFunctions { PFN_vkCmdDispatchIndirect cmdDispatchIndirect = nullptr; PFN_vkCmdBeginRenderPass cmdBeginRenderPass = nullptr; PFN_vkCmdEndRenderPass cmdEndRenderPass = nullptr; + PFN_vkCmdBeginRenderingKHR cmdBeginRendering = nullptr; + PFN_vkCmdEndRenderingKHR cmdEndRendering = nullptr; PFN_vkCmdSetViewport cmdSetViewport = nullptr; PFN_vkCmdSetScissor cmdSetScissor = nullptr; PFN_vkCmdSetDepthBounds cmdSetDepthBounds = nullptr; PFN_vkCmdSetDepthBias cmdSetDepthBias = nullptr; + PFN_vkCmdSetCullModeEXT cmdSetCullMode = nullptr; + PFN_vkCmdSetFrontFaceEXT cmdSetFrontFace = nullptr; + PFN_vkCmdSetDepthTestEnableEXT cmdSetDepthTestEnable = nullptr; + PFN_vkCmdSetDepthWriteEnableEXT cmdSetDepthWriteEnable = nullptr; + PFN_vkCmdSetDepthCompareOpEXT cmdSetDepthCompareOp = nullptr; + PFN_vkCmdSetDepthBoundsTestEnableEXT cmdSetDepthBoundsTestEnable = nullptr; + PFN_vkCmdSetStencilTestEnableEXT cmdSetStencilTestEnable = nullptr; + PFN_vkCmdSetStencilOpEXT cmdSetStencilOp = nullptr; + PFN_vkCmdSetStencilCompareMask cmdSetStencilCompareMask = nullptr; + PFN_vkCmdSetStencilWriteMask cmdSetStencilWriteMask = nullptr; + PFN_vkCmdSetStencilReference cmdSetStencilReference = nullptr; PFN_vkCmdBindVertexBuffers cmdBindVertexBuffers = nullptr; PFN_vkCmdBindIndexBuffer cmdBindIndexBuffer = nullptr; PFN_vkCmdDraw cmdDraw = nullptr; @@ -106,8 +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; @@ -125,8 +209,12 @@ struct Context { bool occlusionQueryPrecise = false; bool depthBounds = false; bool depthBiasClamp = false; + bool dualSrcBlend = false; bool samplerFilterMinmax = false; + bool nonSeamlessCubeMap = false; + VkSampleCountFlags mergedCoverageSampleCounts = 0; bool conservativeRasterization = false; + bool meshWave32 = false; VkBuffer emptyBuffer = VK_NULL_HANDLE; // The device's list of recorded dispatches whose copied written buffers await a CPU write-back // (VulkanDevice's State::copiedWriters; the draw counterpart is DrawCopiedWriters): an indirect @@ -139,14 +227,47 @@ 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 sampleRateShading = false; + bool storageImageMultisample = 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 { @@ -170,8 +291,82 @@ struct Context { } throw std::runtime_error("AGC graphics: required Vulkan memory type is unavailable"); } + + std::uint32_t BufferMemoryType(std::uint32_t mask, VkMemoryPropertyFlags flags, VkDeviceSize bytes) const { + constexpr VkMemoryPropertyFlags coherent = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + auto type = MemoryType(mask, flags); + if (flags == coherent) { + for (std::uint32_t i = 0; i < memory.memoryTypeCount; ++i) { + if ((mask & (1u << i)) != 0 && (memory.memoryTypes[i].propertyFlags & (coherent | VK_MEMORY_PROPERTY_HOST_CACHED_BIT)) == (coherent | VK_MEMORY_PROPERTY_HOST_CACHED_BIT)) { + type = i; + break; + } + } + } + if ((flags & coherent) != coherent || memoryProperties2 == nullptr || physical == VK_NULL_HANDLE) return type; + VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2, &reported}; + memoryProperties2(physical, &properties); + const auto heap = memory.memoryTypes[type].heapIndex; + const auto fits = [&](std::uint32_t candidate) { + return reported.heapBudget[candidate] != 0 && reported.heapUsage[candidate] < reported.heapBudget[candidate] && bytes <= reported.heapBudget[candidate] - reported.heapUsage[candidate]; + }; + if (reported.heapBudget[heap] == 0 || fits(heap)) return type; + for (std::uint32_t i = 0; i < memory.memoryTypeCount; ++i) { + const auto& candidate = memory.memoryTypes[i]; + if ((mask & (1u << i)) != 0 && (candidate.propertyFlags & flags) == flags && candidate.heapIndex != heap && fits(candidate.heapIndex)) return i; + } + return type; + } + + std::uint32_t DeviceMemoryType(std::uint32_t mask, VkDeviceSize bytes, const char* what) const { + const auto type = MemoryType(mask, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + if (memoryProperties2 == nullptr || physical == VK_NULL_HANDLE) return type; + 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); + const auto heap = memory.memoryTypes[type].heapIndex; + 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) return i; + } + 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 109a02b39c..7f05d121a2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp @@ -19,7 +19,7 @@ struct Context; // uncompressed here, so the keys that matter are the fast-clear codes a title writes into the metadata // (the surface then reads as a constant whatever its texels hold) and "uncompressed", which the driver // stores after it writes a surface so later reads see the texels. -enum class DccKeys { Uncompressed, Clear0000, Clear0001, Clear1110, Clear1111, ClearRegister, Mixed, Unreadable }; +enum class DccKeys { Uncompressed, Clear0000, Clear0001, Clear1110, Clear1111, ClearRegister, ClearSingle, Mixed, Unreadable }; const char* DccKeysName(DccKeys keys); std::size_t DccKeyBytes(std::uint64_t surfaceBytes); @@ -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); @@ -83,12 +86,31 @@ struct DccKeyProofCounts { std::uint64_t proved; std::uint64_t scanned; std::uint64_t unstable; + std::uint64_t rangeProved; + std::uint64_t rangeScanned; }; DccKeyProofCounts KeyProofCounts(); +struct DccRangeProof { + std::uint64_t address = 0; + std::uint64_t count = 0; + DccKeys keys = DccKeys::Uncompressed; + std::uint64_t generation = 0; +}; +DccKeys ProvedCurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes, DccRangeProof& proof); // A surface's texels as a read sees them: the guest bytes, or the clear value of fast-cleared keys. 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 2e8ed728c8..521c60721b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -2,20 +2,112 @@ #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); +std::vector> FlushDepthPlanes(std::uint64_t address, std::size_t bytes); +bool DepthPlanesStale(); +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); -std::shared_ptr DepthSurfaceTexture(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components); +bool DepthSurfaceOverlaps(VkDevice device, std::uint64_t address, std::uint64_t bytes); +bool DepthSurfaceAt(const Context& context, std::span words, const GuestTextureResource& resource); +std::uint64_t HtileDepthClearAddress(std::span code, std::span userData, const std::array& numThreads); +std::size_t HtileDepthClearBytes_nid_no_patch(std::span code, std::span userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, std::span packet, std::uint64_t indirectArguments); +void NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes, std::size_t nativeBytes = 0u); +struct MemoryValueFill { + std::uint64_t address; + std::size_t bytes; + std::uint64_t valueAddress; +}; +std::optional MatchMemoryValueFill(std::span code, std::span userData, const std::array& numThreads, const std::array& groups); +void NoteMemoryValueFill(const MemoryValueFill& fill); +bool DepthSurfaceHolds(const Context& context, const GuestTextureResource& resource, const Texture* texture); +void NoteDepthMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern); +VkImageAspectFlags HtileFillClears(std::uint32_t pattern, bool stencilInHtile); +std::uint64_t HtileMetadataBytes(VkExtent2D extent); +bool HtileFillCovers(std::uint64_t htile, VkExtent2D extent, std::uint64_t address, std::size_t bytes); +std::shared_ptr DepthSurfaceTexture(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components, 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); + +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 078e05bb29..8e7623ea87 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp @@ -12,11 +12,14 @@ namespace AgcDriver::Graphics { +struct HostImport; + // `recipe`, when given, receives the DrawRecipe a recorded, cacheable, reusable, non-indirect draw // built for its draw-cache entry (design_cpu_final M8; null otherwise, and always under // APS5_NO_DRAW_RECIPE=1). void Draw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span shaders, std::span snapshots = {}, std::shared_ptr* recipe = nullptr); std::optional KnownValidationFailure(const Context& context, std::span shaders, const State& state); +std::uint64_t DrawRenderPassKey(const Context& context, const State& state, std::span targetViews); struct DrawInputCopy { std::shared_ptr buffer; @@ -26,7 +29,14 @@ 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, 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, bool* behindGpuWrite = nullptr); +struct InPlaceIndices { + std::optional highest; + bool reused = false; +}; +InPlaceIndices InPlaceIndexRange(Recorder& recorder, std::uint64_t address, std::size_t bytes, std::uint32_t indexSize, bool skipRestart); std::array MeshIndexBufferDescriptor(const Pm4::DrawParameters& draw); @@ -81,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 diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp index 3d1aaf2ba5..672a396493 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include +#include namespace AgcDriver::Graphics { @@ -12,18 +13,18 @@ public: ~GpuColorTransfer(); GpuColorTransfer(const GpuColorTransfer&) = delete; GpuColorTransfer& operator=(const GpuColorTransfer&) = delete; - void Upload(std::uint64_t address, std::uint32_t width, std::uint32_t height, ColorTileMode mode); - void Detile(VkCommandBuffer commands, bool swapRedBlue = false, bool tenBit = false); - void DetileImage(VkCommandBuffer commands, VkImage image, VkImageLayout layout, std::uint32_t width, std::uint32_t height, ColorTileMode mode, bool swapRedBlue, bool tenBit); + void Upload(std::uint64_t address, std::uint32_t width, std::uint32_t height, ColorTileMode mode, std::uint32_t elementBytes = 4); + void Detile(VkCommandBuffer commands, bool swapRedBlue = false, bool tenBit = false, bool halfFloat = false, std::optional pqWhiteNits = std::nullopt); + void DetileImage(VkCommandBuffer commands, VkImage image, VkImageLayout layout, std::uint32_t width, std::uint32_t height, ColorTileMode mode, bool swapRedBlue, bool tenBit, bool halfFloat = false, std::uint32_t layer = 0); void Tile(VkCommandBuffer commands); void WriteBack(std::uint64_t address); VkBuffer LinearBuffer() const; RenderTarget& Target(const ColorTarget& color, bool blending); private: - void prepare(std::uint32_t width, std::uint32_t height, ColorTileMode mode); - void convert(VkCommandBuffer commands, bool toTiled, bool swapRedBlue, bool tenBit = false); - void dispatch(VkCommandBuffer commands, bool toTiled, bool swapRedBlue, bool tenBit, bool tiledSource); + void prepare(std::uint32_t width, std::uint32_t height, ColorTileMode mode, std::uint32_t elementBytes); + void convert(VkCommandBuffer commands, bool toTiled, bool swapRedBlue, bool tenBit = false, bool halfFloat = false, std::optional pqWhiteNits = std::nullopt); + void dispatch(VkCommandBuffer commands, bool toTiled, bool swapRedBlue, bool tenBit, bool tiledSource, bool halfFloat, std::optional pqWhiteNits); void release() noexcept; Context context; VkDescriptorSetLayout descriptorLayout = VK_NULL_HANDLE; @@ -35,9 +36,12 @@ private: std::unique_ptr tiled; std::unique_ptr tiledDevice; std::unique_ptr linear; + std::unique_ptr thresholds; + std::unique_ptr pqNits; std::uint32_t width = 0; std::uint32_t height = 0; ColorTileMode mode = ColorTileMode::Linear; + std::uint32_t elementBytes = 4; std::unique_ptr target; VkExtent2D targetExtent{}; VkFormat targetFormat = VK_FORMAT_UNDEFINED; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index e489002301..ceec04cf3e 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 @@ -27,13 +33,27 @@ struct HostImport { VkDeviceMemory memory; VkDeviceAddress address; void* alias = nullptr; - std::weak_ptr range {}; + std::shared_ptr range {}; + std::vector> ranges {}; // Identity for the life of this import (see HostImportSerial); 0 until first asked for. std::uint64_t serial = 0; bool unwatched = false; bool dmaBuf = false; + 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 { @@ -42,7 +62,11 @@ 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; }; ImportProbe ProbeImportWriteProtection(const Context& context); @@ -50,24 +74,90 @@ 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(); +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 // imported); see GuestBufferMemory.cpp. 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 @@ -108,6 +198,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; @@ -152,8 +243,12 @@ struct MirrorStats { std::uint64_t heapRefills = 0; std::uint64_t sweeps = 0; std::uint64_t heapChecks = 0; + 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); @@ -182,7 +277,7 @@ public: // (DescriptorBinding::bufferWritten): the CPU never has to wait for it. `atomic` marks an // element the shader updates atomically (DescriptorBinding::bufferAtomic), staged in device // memory whatever its size (see AllowDeviceStaging). - void AddWritable(std::uint64_t address, std::size_t bytes, bool atomic = false); + void AddWritable(std::uint64_t address, std::size_t bytes, bool atomic = false, bool swept = false); void AddReadable(std::uint64_t address, std::size_t bytes); // Device-local staging of written and atomic elements inside host imports (see // GuestBufferMemory.cpp): allowed only for a build whose every use records its work and then @@ -224,6 +319,7 @@ public: struct AddressSpace; void WriteBack(); bool WritesOverlap(std::uint64_t address, std::size_t bytes) const; + bool CopiedWritesOverlap(std::uint64_t address, std::size_t bytes) const; // Guest ranges the shader may write through descriptors, as [begin, end). const std::vector>& Writes() const { return writes; } // Whether any written range lives in a copied buffer, so a write-back must run once the GPU is done. @@ -242,15 +338,34 @@ public: // Whether registered allocations are pinned until write-back (address-based shaders): by this // build's own lease, or by the cached address space it holds. bool HoldsLease() const { return !lease.empty() || space != nullptr; } + std::size_t CopiedBytes() const { + std::size_t bytes = 0; + for (const auto& region : regions) if (region.buffer != nullptr) bytes += static_cast((region.bufferEnd != 0 ? region.bufferEnd : region.end) - region.begin); + return bytes; + } // Every uploaded region as [begin, end) when all of them are served by host imports, in place or // 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 { @@ -271,7 +386,11 @@ private: // Set when the region covers uncommitted pages: only the `backed` parts (possibly none) are guest // 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; // Set when the region is served by an image mirror: nothing is copied; writable mirrors are // written back by comparing with the mirror's shadow. Kept alive here for recorded work. std::shared_ptr mirror {}; @@ -293,6 +412,7 @@ private: bool copiedBack = false; // An element the shader updates atomically lies inside (AddWritable's `atomic`). bool atomic = false; + bool swept = false; // The gpuCopy buffer is a device-local staging shadow (see stagingEligible): no host // mapping, so nothing is ever stored from it by the CPU, and the region is taken even when // the import could bind it in place. @@ -309,9 +429,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 { @@ -322,11 +443,16 @@ private: void addCopiedRange(const CopiedRange& range); // 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 // of the two did. - void addDescriptorRegion(std::uint64_t address, std::size_t bytes, bool atomic); + void addDescriptorRegion(std::uint64_t address, std::size_t bytes, bool atomic, bool swept, bool readOnly); + static bool servesView(const Region& region, std::uint64_t view); + static bool sparseHeapCandidate(const Region& region); + bool bindSparseMirror(Region& region, const GuestAllocations::Lease& lease); // Gives a region a buffer of its own with its bytes (guest memory for host-backed and writable // ranges, plus the write-back's reference copy for a range a descriptor writes; else its snapshot). void copyRegion(Region& region, bool addressable); @@ -351,16 +477,19 @@ 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; + std::uint64_t importFloor = 0; std::vector regions; // Whether `regions` is in ascending address order (true right after AcquireRegistered, whose // regions follow the registry's order), so AddSnapshot can search instead of scanning. bool regionsSorted = false; + bool regionsOverlap = false; std::vector> writes; + std::vector> readsOverBase; std::vector>> heapReferences; // UploadPrepare ran (regions are frozen); `uploaded` once UploadFinish ran. bool prepared = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp index e85a1a6f89..55442662ba 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp @@ -6,6 +6,8 @@ #endif #include #include +#include +#include #include namespace AgcDriver::Graphics { @@ -28,9 +30,11 @@ struct GuestSamplerResource { bool compareEnable = false; VkCompareOp compareOp = VK_COMPARE_OP_NEVER; bool unnormalizedCoordinates = false; + bool nonSeamlessCube = false; }; -GuestSamplerResource DecodeSamplerResource(std::span words, bool unnormalizedProven = false, bool forceDegammaPaired = false); +GuestSamplerResource DecodeSamplerResource(std::span words, bool unnormalizedProven = false, bool forceDegammaPaired = false, bool integerBorder = false); +std::optional> SingleLevelSamplerWords(std::span words, bool singleLevelImage, bool mipmappedImage); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp index aa0765d12f..6f40129b56 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 { @@ -76,20 +78,28 @@ struct GuestTextureResource { std::uint8_t dstSelY; std::uint8_t dstSelZ; std::uint8_t dstSelW; + std::uint32_t pipeBankXor = 0; + std::uint8_t bcSwizzle = 0; // Last mip level the view exposes; the surface itself holds mipCount levels. std::uint32_t lastLevel = 0; // DCC metadata of a compressed surface, or 0 (see DccMetadata.hpp). 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; }; 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/HalfFloatScanout.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/HalfFloatScanout.hpp new file mode 100644 index 0000000000..8a586a948a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/HalfFloatScanout.hpp @@ -0,0 +1,60 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_HALFFLOATSCANOUT_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_HALFFLOATSCANOUT_HPP + +#include +#include +#include + +namespace AgcDriver::Graphics { + +inline constexpr std::uint32_t HalfFloatScanoutCodes = 255; +inline constexpr std::uint32_t HalfFloatPositiveInfinity = 0x7c00u; + +inline const std::array& HalfFloatScanoutThresholds() { + static const auto thresholds = [] { + std::array table{}; + const auto finiteValue = [](std::uint32_t bits) { + const auto exponent = static_cast(bits >> 10u); + const auto mantissa = static_cast(bits & 0x3ffu); + return exponent == 0 ? std::ldexp(mantissa, -24) : std::ldexp(1024.0 + mantissa, exponent - 25); + }; + const auto firstAtLeast = [&](double value) { + std::uint32_t low = 0; + std::uint32_t high = HalfFloatPositiveInfinity; + while (low < high) { + const auto middle = (low + high) / 2u; + if (finiteValue(middle) >= value) high = middle; + else low = middle + 1u; + } + return low; + }; + for (std::uint32_t code = 1; code <= HalfFloatScanoutCodes; ++code) { + const double encoded = (code - 0.5) / 255.0; + table[code - 1u] = firstAtLeast(encoded <= 0.04045 ? encoded / 12.92 : std::pow((encoded + 0.055) / 1.055, 2.4)); + table[HalfFloatScanoutCodes + code - 1u] = firstAtLeast(encoded); + } + return table; + }(); + return thresholds; +} + +inline std::uint8_t HalfFloatScanoutCode(std::uint16_t bits, bool alpha) { + static std::array, 2> codes{}; + static const bool filled = [] { + const auto& thresholds = HalfFloatScanoutThresholds(); + for (std::uint32_t kind = 0; kind < 2u; ++kind) { + for (std::uint32_t half = 0; half <= HalfFloatPositiveInfinity; ++half) { + std::uint32_t code = 0; + while (code < HalfFloatScanoutCodes && thresholds[kind * HalfFloatScanoutCodes + code] <= half) ++code; + codes[kind][half] = static_cast(code); + } + } + return true; + }(); + static_cast(filled); + return codes[alpha ? 1u : 0u][bits]; +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp new file mode 100644 index 0000000000..6d5385bfdf --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -0,0 +1,79 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_MULTISAMPLETARGET_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_MULTISAMPLETARGET_HPP + +#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 + +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 SeedCode(std::optional code); + void CheckAddress(std::uint64_t cmaskAddress) const; + bool TakeClear(); + DccKeys Keys() const { return keys; } + const std::string& Refusal() const { return refusal; } + +private: + std::uint64_t address = 0; + std::uint64_t bytes = 0; + DccKeys keys = DccKeys::Uncompressed; + std::string refusal; +}; + +class MultisampledColorFill { +public: + explicit MultisampledColorFill(const ColorTarget& target); + void NoteFill(std::uint64_t fillAddress, std::size_t fillBytes, std::uint32_t pattern); + std::optional TakeFill(); + void Discard(); + +private: + std::uint64_t address = 0; + std::uint64_t bytes = 0; + std::optional filled; + std::string refusal; +}; + +std::uint64_t MultisampledColorBytes(const ColorTarget& target); +std::size_t MultisampledDccBytes(const ColorTarget& target); +VkClearColorValue MultisampledDccClear(const ColorTarget& target, std::span dcc, std::span cmask); +bool MultisampledFillClearColor(const ColorTarget& target, std::uint32_t fill, VkClearColorValue& clear); +bool TakeMultisampledClear(MultisampledCmask& cmask, MultisampledColorFill& colorFill, const ColorTarget& use, VkClearColorValue& clear); +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); +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); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp new file mode 100644 index 0000000000..483b2f45cc --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp @@ -0,0 +1,24 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_PARALLELCOMPARE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_PARALLELCOMPARE_HPP + +#include +#include +#include + +namespace AgcDriver::Graphics { + +struct CompareSpan { + const void* first; + const void* second; + std::size_t bytes; +}; + +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(); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PassHazards.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PassHazards.hpp new file mode 100644 index 0000000000..298ca9c49e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PassHazards.hpp @@ -0,0 +1,71 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_PASSHAZARDS_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_PASSHAZARDS_HPP + +#ifndef VK_NO_PROTOTYPES +#define VK_NO_PROTOTYPES +#endif +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::Graphics { + +using GuestRangeList = std::span>; + +struct PassAccess { + GuestRangeList reads; + GuestRangeList writes; + std::span> images; + std::span> attachments; + bool anyReads = false; + bool anyWrites = false; + GuestRangeList writeOnly = {}; +}; + +enum class PassHazard : std::uint8_t { None, ReadAfterWrite, WriteAfterWrite, WriteAfterRead, Image, UnknownRead, UnknownWrite }; + +class GuestRangeSet { +public: + void Insert(std::uint64_t begin, std::uint64_t end); + 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: + std::vector> ranges; +}; + +class PassHazards { +public: + PassHazard Check(const PassAccess& access, bool samePass) const; + void Add(const PassAccess& access); + void BeginPass() { ++pass; } + void SealWriteOnly(); + void Clear(); + bool Empty() const; + +private: + struct Attachment { + VkImage image; + bool written; + std::uint64_t pass; + }; + 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; + bool anyReads = false; + bool anyWrites = false; +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index 5e83e444d2..1ee631c131 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -2,6 +2,7 @@ #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_PIPELINE_HPP #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" #include namespace AgcDriver::Graphics { @@ -14,17 +15,19 @@ void LogPipelineStatistics_nid_no_patch(const Context& context, VkPipeline pipel // the batch completes, so a recorded draw keeps the object (Recorder::Keep) like its pipeline. class Framebuffer { public: - Framebuffer(const Context& context, VkRenderPass renderPass, std::span targets, VkExtent2D extent); + Framebuffer(const Context& context, VkRenderPass renderPass, std::span targets, VkExtent2D extent, std::uint32_t layers = 1); ~Framebuffer(); Framebuffer(const Framebuffer&) = delete; Framebuffer& operator=(const Framebuffer&) = delete; VkFramebuffer Handle() const { return framebuffer; } + std::span Views() const { return views; } // Forgets the handle without destroying it (the device it belongs to is already gone). void Abandon() noexcept; private: Context context; VkFramebuffer framebuffer = VK_NULL_HANDLE; + std::vector views; }; // The shader modules, layout, render pass and VkPipeline of one draw configuration. Viewport, @@ -38,16 +41,27 @@ public: // before, during and after the pass (GENERAL for resident targets, which then need no // transitions). Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); ~Pipeline(); + static bool Prebuild(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders); 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::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 // construction: the attachment formats alone decide). void Continue(VkCommandBuffer commands, const State& state) const; + bool SplitsFaces() const { return backFaces != VK_NULL_HANDLE; } + bool Optimized() const { return optimized != nullptr && optimized->handle.load() != VK_NULL_HANDLE; } + 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; @@ -55,41 +69,87 @@ 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; + void setLibraryState(VkCommandBuffer commands, const State& state) const; Context context; std::vector _modules; VkPipelineLayout layout = VK_NULL_HANDLE; VkRenderPass renderPass = VK_NULL_HANDLE; VkPipeline pipeline = VK_NULL_HANDLE; + VkPipeline backFaces = VK_NULL_HANDLE; + VkPipeline secondChannel = VK_NULL_HANDLE; std::size_t attachments = 0; std::size_t colorAttachments = 0; bool depthBounds = false; bool depthBias = false; + bool libraries = false; + std::shared_ptr optimized; + bool dynamicRendering = false; + VkFormat depthFormat = VK_FORMAT_UNDEFINED; + 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); +// `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; + std::vector fragmentKey; + PipelineSetLayouts View() const { return {layouts.main, layouts.fragment, layouts.split, mainKey, fragmentKey}; } +}; +DescribedSetLayouts DescribeSetLayouts(const Context& context, std::span shaders); +bool SplitsSets(const Context& context, std::span shaders); // Destroys the cached pipelines of a device; to be called before the device goes away. Without it, // entries of a gone device are recognised by their buffer pool (made and reset with the device, so // it tells device instances apart when the loader reuses a VkDevice handle) and forgotten unused. void ClearCachedPipelines(VkDevice device); +bool HasStencil(VkFormat format); // Device limit checks of the viewport, which is dynamic state and so no longer checked by Pipeline. 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); +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 54727e9459..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,23 +18,48 @@ 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; VkPhysicalDeviceProperties properties{}; 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 new file mode 100644 index 0000000000..fcd6104569 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp @@ -0,0 +1,64 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_PIPELINELIBRARY_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_PIPELINELIBRARY_HPP + +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::Graphics { + +struct PipelineLibraryKeys { + std::vector vertexInput; + std::vector preRasterization; + std::vector fragmentShader; + std::vector fragmentOutput; + std::vector renderPass; + std::vector layout; + std::vector preRasterizationLayout; + std::vector fragmentLayout; + std::uint32_t preRasterizationSets = 0; + std::uint32_t fragmentSets = 0; +}; + +std::span PipelineLibraryDynamicStates(); + +struct OptimizedPipeline { + VkDevice device = VK_NULL_HANDLE; + 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 = {}); + +void WaitForOptimizedPipelines(VkDevice device); + +void ClearPipelineLibraries(VkDevice device); + +void StopPipelineLibraryWorker(); +unsigned PipelineLibraryWorkers(); + +struct PipelineLibraryCounters { + std::array built{}; + std::uint64_t linked = 0; + std::uint64_t optimized = 0; +}; + +PipelineLibraryCounters PipelineLibraryCountersOf(VkDevice device); + +} + +#endif 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 new file mode 100644 index 0000000000..71575f622a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp @@ -0,0 +1,82 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_PLACEHOLDERIMAGES_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_PLACEHOLDERIMAGES_HPP + +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include "RuntimeAbi.hpp" +#include +#include +#include + +namespace AgcDriver::Graphics { + +inline constexpr std::uint32_t PlaceholderDimensionsPerClass = (ShaderRecompiler::RuntimeAbi::FirstComparisonImageBinding - ShaderRecompiler::RuntimeAbi::FirstImageBinding) / 3u; +static_assert(ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding == ShaderRecompiler::RuntimeAbi::FirstComparisonImageBinding + PlaceholderDimensionsPerClass); +static_assert(ShaderRecompiler::RuntimeAbi::ImageBindingCount == 8u * PlaceholderDimensionsPerClass && PlaceholderDimensionsPerClass == 7u); + +struct PlaceholderImageShape { + VkFormat format; + VkFormatFeatureFlags features; + VkImageUsageFlags usage; + VkImageAspectFlags aspect; + VkImageType type; + VkImageViewType viewType; + VkSampleCountFlagBits samples; +}; + +constexpr PlaceholderImageShape PlaceholderImageShapeOf(std::uint32_t heap) { + constexpr std::array formats{VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_R32_UINT, VK_FORMAT_R32_SINT, VK_FORMAT_D16_UNORM, + VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_R32_UINT, VK_FORMAT_R32_UINT, VK_FORMAT_R64_UINT}; + constexpr std::array types{VK_IMAGE_TYPE_1D, VK_IMAGE_TYPE_1D, VK_IMAGE_TYPE_2D, VK_IMAGE_TYPE_2D, + VK_IMAGE_TYPE_2D, VK_IMAGE_TYPE_2D, VK_IMAGE_TYPE_3D}; + constexpr std::array views{VK_IMAGE_VIEW_TYPE_1D, VK_IMAGE_VIEW_TYPE_1D_ARRAY, VK_IMAGE_VIEW_TYPE_2D, + VK_IMAGE_VIEW_TYPE_2D_ARRAY, VK_IMAGE_VIEW_TYPE_2D, VK_IMAGE_VIEW_TYPE_2D_ARRAY, + VK_IMAGE_VIEW_TYPE_3D}; + constexpr std::array samples{VK_SAMPLE_COUNT_1_BIT, VK_SAMPLE_COUNT_1_BIT, VK_SAMPLE_COUNT_1_BIT, + VK_SAMPLE_COUNT_1_BIT, VK_SAMPLE_COUNT_4_BIT, VK_SAMPLE_COUNT_4_BIT, + VK_SAMPLE_COUNT_1_BIT}; + const auto index = heap - ShaderRecompiler::RuntimeAbi::FirstImageBinding; + const auto kind = index / PlaceholderDimensionsPerClass; + const auto format = formats[kind]; + const auto dimension = index % PlaceholderDimensionsPerClass; + const bool storage = heap >= ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding; + const bool atomic = kind >= 6u; + const VkFormatFeatureFlags access = atomic ? VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT | VK_FORMAT_FEATURE_STORAGE_IMAGE_ATOMIC_BIT + : storage ? VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT + : VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT; + return {format, + static_cast(access | VK_FORMAT_FEATURE_TRANSFER_DST_BIT), + static_cast((storage ? VK_IMAGE_USAGE_STORAGE_BIT : VK_IMAGE_USAGE_SAMPLED_BIT) | VK_IMAGE_USAGE_TRANSFER_DST_BIT), + static_cast(format == VK_FORMAT_D16_UNORM ? VK_IMAGE_ASPECT_DEPTH_BIT : VK_IMAGE_ASPECT_COLOR_BIT), + types[dimension], + views[dimension], + 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); + ~PlaceholderImages(); + PlaceholderImages(const PlaceholderImages&) = delete; + PlaceholderImages& operator=(const PlaceholderImages&) = delete; + VkImageView View(std::uint32_t heap) const; + +private: + struct Image { + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + VkImageView view = VK_NULL_HANDLE; + }; + void release() noexcept; + Context context; + std::array images{}; +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index 08afd6140e..6ef4b5cf08 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -1,7 +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 @@ -16,6 +19,7 @@ #include #include #include +#include #include #include @@ -58,16 +62,14 @@ public: // destination access mask. void MarkCovered(VkAccessFlags access); void MarkShaderReadsCovered() { MarkCovered(VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); } - // A recorded draw's render pass (Draw.cpp) is left open after the draw: the next draw of the - // same attachments (`key`: the views and the extent) continues it when nothing was recorded in - // between and the earlier draw allowed it (`continuable`: it wrote nothing but its - // attachments, so no barrier is owed inside the pass), and anything else recorded first ends - // it (vkCmdEndRenderPass, one trailing barrier for the pass, the end of the draw class range - // `timing`): Commands(), RecordStore and Submit end it; CommandsInRenderPass hands a - // continuing draw the command buffer without ending it. - bool ContinuesRenderPass(std::uint64_t key) const; - VkCommandBuffer CommandsInRenderPass(); - void LeaveRenderPassOpen(std::uint64_t key, std::uint32_t timing, bool continuable, std::function afterPass = {}); + struct DrawPassStart { + VkCommandBuffer commands = VK_NULL_HANDLE; + bool continued = false; + bool barrier = true; + }; + 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 @@ -84,22 +86,43 @@ 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); + void KeepThroughHold(std::shared_ptr object); + void KeepBytes(const void* owner, std::size_t bytes); + std::size_t OpenKeptBytes() const { return open != nullptr ? open->keptBytes : 0; } + void FlushDeferredReleases(); static constexpr std::size_t KeptBytesBudget = std::size_t{512} << 20u; void BoundKeptBytes(); std::size_t InFlightKeptBytes() const { return inFlightKeptBytes; } - enum class SnapshotUse : std::uint8_t { Storage, Vertex, Index16, Index32 }; + 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 ReusableDrawSnapshot(std::uint64_t address, std::size_t bytes, SnapshotUse use = SnapshotUse::Storage, std::uint32_t* derived = nullptr); + 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); + void KeepHighestIndex(std::uint64_t address, std::size_t bytes, SnapshotUse use, std::uint64_t generation, std::uint64_t registryGeneration, std::optional highest); 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; @@ -114,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 @@ -124,10 +149,13 @@ public: // on the open batch, no snapshot: the only reader holds the mutex. `kind` names the reader for // the [recorder] hit counters. APS5_COPY_READ_TRACKING=0 notes nothing (ReadTracking() is then // false and the CPU copy falls back to Idle()). - enum class ReadKind : std::uint8_t { DispatchElement = 0, GpuCopy, AddressBased, Indirect, StorageUpload, CopySource, Count }; + enum class ReadKind : std::uint8_t { DispatchElement = 0, GpuCopy, AddressBased, Indirect, StorageUpload, CopySource, DrawInput, Count }; 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. @@ -152,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 @@ -168,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. @@ -184,8 +215,26 @@ public: void CountSamples(); bool RecordMeshArguments(VkCommandBuffer commands, VkDeviceAddress record, VkDeviceAddress arguments, std::span rules); std::uint64_t SamplesTotal(); - bool DumpSamples(VkDeviceAddress target); - void NoteSampledDraw(); + bool DumpSamples(VkDeviceAddress target, std::uint64_t address = 0); + void NoteSampledDraw(VkCommandBuffer commands); + bool QueuesSampleDumps() const; + void PrepareSampleSlot(); + void EndPassSamples(); + struct SampleDumpStatistics { + std::uint64_t queued; + std::uint64_t queuedInPass; + std::uint64_t recordedAtOnce; + std::uint64_t segments; + std::uint64_t folds; + }; + static SampleDumpStatistics SampleDumpCounts(); + struct QueuedWrite { + std::uint64_t begin; + 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 // submission order, so completions still run in order. Debug aid: APS5_NO_SYNC_THROUGH=1 syncs all. @@ -364,6 +413,7 @@ public: // The recorder of the current device, for code that only has guest addresses (the flush hook) // and for helpers that must not recycle resources the recorded work still uses. static Recorder* Active(); + VkDevice Device() const { return context.device; } // Whether the calling thread runs inside a batch's completion action (finish()): a store made // there must not have a publish recorded for it (UnitShadow), see SyncThrough. static bool InCompletion(); @@ -436,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 @@ -496,10 +548,12 @@ private: VkFence fence = VK_NULL_HANDLE; std::vector> kept; std::size_t keptBytes = 0; + std::unordered_set keptOwners; std::vector> completions; 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; @@ -507,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). @@ -519,6 +580,12 @@ private: std::shared_ptr samplePool; bool sampleActive = false; bool samplesDrawn = false; + VkQueryPool segments = VK_NULL_HANDLE; + std::shared_ptr segmentPool; + std::uint32_t segmentNext = 0; + std::uint32_t segmentSlot = 0; + bool segmentActive = false; + std::uint32_t queuedDumps = 0; std::vector timedKeys; std::vector timedBytes; // The whole-batch timed range (BatchTimingKey) and its stamps once read (see Completed). @@ -560,6 +627,9 @@ private: VkDeviceSize offset; std::vector bytes; std::uint64_t address; + VkDeviceAddress dumpTarget = 0; + std::uint32_t dumpSegments = 0; + std::uint64_t End() const; }; std::vector queued; } run; @@ -581,14 +651,14 @@ private: }; std::vector images; } tracker; - // The render pass a recorded draw left open (see ContinuesRenderPass). struct RenderPass { bool open = false; - bool continuable = false; + bool fenced = false; std::uint64_t key = 0; std::uint32_t timing = NoTiming; std::function afterPass; } renderPass; + PassHazards passHazards; // A pass ended in this batch: Submit records the host-read barrier its draws left out. bool hostReadOwed = false; // Queued DCC key stores (see QueueKeyStore). @@ -622,9 +692,21 @@ private: bool gpuSampleCounter(); void endSamples(Batch& batch); void foldSamples(Batch& batch, VkDeviceAddress target); + bool segmentMode() const; + bool segmentSlotReady(const Batch& batch) const; + void takeSegmentPool(Batch& batch); + void beginSegment(Batch& batch); + void endSegment(Batch& batch); + void copySegments(VkCommandBuffer commands, std::uint32_t count); + void retireSegments(std::uint32_t count); + void recordQueuedDumps(VkCommandBuffer commands, std::span writes); + void dispatchDumps(VkCommandBuffer commands, std::uint32_t firstDump, std::uint32_t dumps, std::uint32_t firstSegment, std::uint32_t lastSegment); + void foldSegments(); + void foldQueuedDumps(VkCommandBuffer commands, std::span writes, std::size_t first, std::size_t end, std::uint32_t& dumpIndex, std::uint32_t& consumed); struct SampleSegment { std::shared_ptr pool; VkQueryPool handle; + std::uint32_t slot = 0; }; std::vector pendingSamples; bool countingSamples = false; @@ -636,6 +718,12 @@ private: VkPipelineLayout sampleLayout = VK_NULL_HANDLE; VkPipeline samplePipeline = VK_NULL_HANDLE; std::shared_ptr samplePools; + 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); @@ -666,6 +754,7 @@ private: void recordBarrier(VkCommandBuffer commands, VkPipelineStageFlags sourceStage, VkPipelineStageFlags destinationStage, VkAccessFlags sourceAccess, VkAccessFlags destinationAccess) const; // Opens a batch when none is open, without closing a store run (Commands() does both). void ensureOpen(); + void dropStaleHoldKept(); // Records the run's pending store (vkCmdUpdateBuffer), if any. void flushPendingStore(); // Records the pending store and the run's trailing barrier; the next store starts a new run. @@ -673,7 +762,7 @@ private: // form, toward the host only); returns whether the batch's host-read barrier was included. bool closeStoreRun(bool atSubmit = false); // Ends the render pass a draw left open (vkCmdEndRenderPass, the pass's trailing barrier). - void endOpenRenderPass(); + void endOpenRenderPass(bool barrier = true); // Records the queued key stores as one run (`forWriter`: before a command writing, reading or // labelling over one, not at Submit). void recordKeyStores(bool forWriter); @@ -684,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() const; + void publishPendingWrites(std::span> additions = {}) const; // Appends one range to the open batch; returns whether the snapshot must be rebuilt for it. // `ownLabel`: the range is a label's own store (NoteLabel, AfterCompletions), which does not // overwrite the table entries it covers; any other range flags them (table mutex, briefly). - 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); @@ -716,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; @@ -733,13 +828,18 @@ private: // table is empty (most of the time between label groups). std::atomic recordedLabels{0}; std::unique_ptr open; + std::vector> holdKept; + std::uint64_t holdKeptSerial = 0; std::deque> inFlight; // 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; @@ -761,8 +861,28 @@ 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 { + std::uint64_t generation; + std::uint64_t registryGeneration; + std::list::iterator recent; + std::optional highest; + }; + std::map indexRanges; + std::list indexRangeRecency; + bool refreshDrawSnapshot(DrawSnapshot& entry, std::uint64_t address, std::size_t bytes); + mutable std::weak_ptr publishedWriteRanges; + mutable bool publishedWriteRangesValid = false; }; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Resources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Resources.hpp index ba43520988..674cff0c47 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Resources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Resources.hpp @@ -29,12 +29,13 @@ private: VkDeviceMemory memory = VK_NULL_HANDLE; void* mapping = nullptr; std::size_t size; - // The VkBuffer's own size, `size` rounded up to its pool class (see BufferPool::Capacity). std::size_t capacity; // Fully made (or taken from the pool), so release returns it to the pool instead of destroying // the handles a failed construction left. bool ready = false; VkDeviceSize allocationBytes = 0; + VkDeviceSize offset = 0; + bool slab = false; VkBufferUsageFlags usage; VkMemoryPropertyFlags properties; std::shared_ptr cache; @@ -62,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 a98203b7cb..ad2f347b21 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp @@ -22,6 +22,10 @@ public: bool ForcesDegamma() const; VkSampler Handle() const; 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; @@ -30,6 +34,8 @@ private: bool forcesDegamma = false; VkSampler sampler = VK_NULL_HANDLE; bool requiresFilterMinmax = false; + bool opaqueBlackBorder = false; + bool requiresNonSeamlessCube = false; }; // One VkSampler per distinct S# (its 4 words plus the shader's depth-compare use and the @@ -43,7 +49,7 @@ public: explicit SamplerCache(std::size_t capacity = 1024); SamplerCache(const SamplerCache&) = delete; SamplerCache& operator=(const SamplerCache&) = delete; - std::shared_ptr Get(const Context& context, std::span words, bool compareEnable, bool unnormalizedProven = false, bool forceDegammaPaired = false); + std::shared_ptr Get(const Context& context, std::span words, bool compareEnable, bool unnormalizedProven = false, bool forceDegammaPaired = false, bool integerBorder = false); // APS5_PROFILE_DRAW counters: lookups served by an existing sampler, and samplers created. std::uint64_t Hits() const { return hits; } std::uint64_t Misses() const { return misses; } @@ -62,7 +68,9 @@ private: }; void RequireFilterMinmax(const Context& context, VkFormat format, std::uint32_t samplerMask, std::span> samplers); -void RequireDegammaFormat(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span> samplers); +bool ForcesDegammaOn(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span> samplers); +void RequireBorderSwizzle(std::uint32_t bcSwizzle, std::uint32_t samplerMask, std::span> samplers); +void RequireNonSeamlessCube(bool cube, std::uint32_t samplerMask, std::span> samplers); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp index 6719373065..d555d389a5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp @@ -11,14 +11,18 @@ 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); +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 47474d8a6e..c0d65d1f74 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,17 +29,25 @@ 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); +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. @@ -50,15 +61,21 @@ bool StorageImageServesKeys(const StorageTexture& image, std::uint64_t dccAddres // a storage image other than `except` has results pending in [address, address + bytes). bool PendingStorageOverlaps(std::uint64_t address, std::size_t bytes, const StorageTexture* except); -// Per-device descriptor objects shared by ShaderResources builds: set layouts by their binding list -// (immutable; kept until the device is torn down, which Vulkan allows even for pipeline layouts made -// from them) and a chain of descriptor pools that sets are freed back to, so a build creates and -// destroys no layout or pool of its own. A set lives exactly as long as its ShaderResources, which -// its batch keeps until the GPU completed. The cache locks itself: stage A of a dispatch build -// (ShaderResources::buildPrepare, from VulkanDevice::PrepareDispatch) takes layouts and sets from it -// without GuestMemory::GpuMutex, while other threads free and update sets of the same pools under -// that lock (Vulkan synchronizes the pool for allocate/free and the set for update, so that is -// legal). APS5_NO_LAYOUT_CACHE=1 / APS5_NO_POOL_CACHE=1 restore the per-build objects. +struct PipelineSetLayouts { + VkDescriptorSetLayout main = VK_NULL_HANDLE; + VkDescriptorSetLayout fragment = VK_NULL_HANDLE; + bool split = false; + std::span mainKey; + 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); @@ -68,13 +85,21 @@ public: // The layout for `key` (binding, type, count, stage flags per binding, as ShaderResources builds // it from `bindings`), created on first use. - VkDescriptorSetLayout Layout(std::span key, std::span bindings, bool updateAfterBind = false); + VkDescriptorSetLayout Layout(std::span key, std::span bindings, bool updateAfterBind = false, std::span flags = {}); struct SetAllocation { VkDescriptorSet set = VK_NULL_HANDLE; 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; @@ -82,6 +107,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; @@ -89,32 +116,65 @@ private: Context context; PFN_vkDestroyDescriptorSetLayout destroyLayout; PFN_vkDestroyDescriptorPool destroyPool; - PFN_vkFreeDescriptorSets freeSets; + PFN_vkResetDescriptorPool resetPool; + PFN_vkFreeDescriptorSets freeDescriptorSets = nullptr; mutable std::mutex mutex; std::map, VkDescriptorSetLayout> layouts; - std::vector pools; - std::vector updateAfterBindPools; + struct Pool { + VkDescriptorPool handle = VK_NULL_HANDLE; + std::size_t liveSets = 0; + bool exhausted = false; + }; + 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); - ShaderResources(const Context& context, std::span shaders, const ColorTarget& target, std::uint64_t indexAddress, std::size_t indexBytes, std::span snapshots = {}); + 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 = {}); ShaderResources(const Context& context, const CompiledShader& compute, std::span snapshots = {}); // Two-stage build for dispatches (see build): with `deferred` the constructor runs stage A only, // which needs no device lock, and Complete() runs stage B under GuestMemory::GpuMutex; until // then no other member may be used. `compute` and `snapshots` must outlive Complete(). An // address-based shader (BDA tables) builds entirely in Complete(): its lease acquisition // reconciles imports and refreshes mirrors, which needs the lock. - ShaderResources(const Context& context, const CompiledShader& compute, std::span snapshots, bool deferred); + ShaderResources(const Context& context, const CompiledShader& compute, std::span snapshots, bool deferred, std::uint64_t dispatchThreads = 0); void Complete(); bool Completed() const { return completed; } ~ShaderResources(); ShaderResources(const ShaderResources&) = delete; ShaderResources& operator=(const ShaderResources&) = delete; VkDescriptorSetLayout Layout() const; + std::vector LayoutBindings() const; + bool LayoutUpdateAfterBind() const; + std::span LayoutBindingFlags() const { return layoutBindingFlags; } + VkDescriptorSetLayout FragmentLayout() const { return fragment.layout; } + bool SplitSets() const { return splitSets; } + PipelineSetLayouts SetLayouts() const { return {_layout, fragment.layout, splitSets, layoutKey, fragmentLayoutKey}; } void Bind(VkCommandBuffer commands, VkPipelineBindPoint bindPoint, VkPipelineLayout layout) const; struct DrawBindings { DrawBindings() = default; @@ -123,8 +183,14 @@ 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; ~DrawBindings(); @@ -133,10 +199,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 @@ -144,25 +220,32 @@ 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); } + bool CopiedWritesOverlap(std::uint64_t address, std::size_t bytes) const { return guestMemory.CopiedWritesOverlap(address, bytes); } // Whether a region the recorded work reads in place through a host import overlaps the range. bool ReadsOverlap(std::uint64_t address, std::size_t bytes) const; // The recorder's hazard tracker inputs (Recorder::NoteAccess): the written elements' guest // 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; - // Whether a use writes guest memory beyond a draw's attachments (storage images, written or - // copied buffers, an address-based build's unknown writes), or reads `image` (a view of it - // sampled, or the image itself bound): a recorded draw's render pass may only be continued by - // a draw for which neither holds (Draw.cpp). - bool WritesMemory() 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. std::string Describe() const; @@ -178,6 +261,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 @@ -187,6 +272,14 @@ 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 // and fragment stages, or one compute stage): their bindings are walked like the build did. // How a Revalidate proved (or refused) the object, for the [recipe] line: the proof path taken @@ -214,7 +307,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, 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 }; @@ -238,11 +331,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; @@ -252,6 +348,7 @@ private: std::uint32_t format = 0; int tileMode = 0; std::uint64_t dccAddress = 0; + bool imageTable = false; }; std::vector describedRanges; struct Allocation { @@ -262,6 +359,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; @@ -280,13 +378,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 @@ -295,15 +425,16 @@ private: // GuestMemory::GpuMutex): the texture and storage image lookups (they refresh, upload and flush // through the recorder), the rest of the upload, the BDA objects, the descriptor writes and the // reusability record. build runs both. - void build(std::span shaders, const ColorTarget* target, std::uint64_t indexAddress, std::size_t indexBytes); - void buildPrepare(std::span shaders, const ColorTarget* target, std::uint64_t indexAddress, std::size_t indexBytes); + void build(std::span shaders, const ColorTarget* target, std::uint32_t colorAttachments, std::uint64_t indexAddress, std::size_t indexBytes); + void buildPrepare(std::span shaders, const ColorTarget* target, std::uint32_t colorAttachments, std::uint64_t indexAddress, std::size_t indexBytes); void buildComplete(); // APS5_PROFILE_DRAW: closes the current sub-phase of the build into the [resources] totals. double phase(BuildPhase which); // `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); + 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). @@ -336,12 +467,20 @@ private: std::uint64_t entryGeneration = 0; }; std::vector imageRecords; + struct PreviousSampled { + std::array words{}; + bool depthCompare = false; + VkImageViewType multisampleViewType = VK_IMAGE_VIEW_TYPE_2D; + std::shared_ptr texture; + }; + PreviousSampled previousSampled; // The next record resolveImageBinding consumes (records follow the deferredImages order). std::size_t nextImageRecord = 0; // Stage B: the record's texture when the fastRevalidate predicate proves it current under the // lock and the cache still holds it; null sends the element to cachedTexture. std::shared_ptr fastTexture(const ImageRecord& record); void resolveImageBinding(const ShaderRecompiler::DescriptorBinding& binding, Binding& item, std::span> shaderSamplers); + void applyAnisoOverride(); void forgetDeferredInputs(); void release() noexcept; void prepareAddressBindings(std::span shaders, std::span snapshots); @@ -365,6 +504,7 @@ private: std::uint64_t collected = 0; // Storage-sourced textures: the image the view follows (kept alive by the texture). const StorageTexture* source = nullptr; + bool depth = false; bool valid = false; }; void captureValidation(); @@ -404,24 +544,68 @@ private: Context context; std::vector layoutKey; 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; VkDescriptorSet _set = VK_NULL_HANDLE; VkDescriptorPool pool = VK_NULL_HANDLE; // The cache pool the set was allocated from, freed back to it on release. - VkDescriptorPool cachePool = VK_NULL_HANDLE; + DescriptorCache::SetAllocation cacheAllocation; + struct SetObjects { + VkDescriptorSetLayout layout = VK_NULL_HANDLE; + bool ownsLayout = false; + VkDescriptorSet set = VK_NULL_HANDLE; + VkDescriptorPool pool = VK_NULL_HANDLE; + DescriptorCache::SetAllocation cacheAllocation; + }; + 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; std::vector textureFirstLayer; + std::vector textureBaseLevel; + std::vector textureSrgb; std::vector> storageTextures; + std::vector multisampleStorageViews; std::vector storageMips; std::vector storageKeys; std::vector storageFirstLayer; @@ -430,6 +614,15 @@ private: std::vector storageAtomic64; std::vector> samplers; std::shared_ptr paddingSampler; + struct SamplerSource { + std::array words{}; + bool compareEnable = false; + bool unnormalized = false; + bool integerBorder = false; + bool singleLevelImage = false; + bool mipmappedImage = false; + }; + std::vector samplerSources; bool reusable = false; std::vector directRegions; std::vector validatedTextures; @@ -450,6 +643,8 @@ private: // still to look up (index into `bindings`; the DescriptorBinding lives in the compiled shader), // the descriptor counts the set was sized for, and the compute stage of a deferred build. std::vector bindings; + std::vector layoutBindingFlags; + std::vector fragmentBindingFlags; std::vector refreshResourceKey; struct DeferredImages { const ShaderRecompiler::DescriptorBinding* binding; @@ -500,6 +695,7 @@ public: // With `object`, erases the entry only while it still holds that object (a replacement made // meanwhile by another worker stays). void Remove(const Key& key, const ShaderResources* object = nullptr); + void RemoveImage(const StorageTexture* image, std::vector>* evicted = nullptr); // Moves the entry to the front of the LRU (a recipe hit uses its template without Find, and a // hot template must not age out under the recipes that depend on it); false when no entry. bool Touch(const Key& key); @@ -508,10 +704,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; @@ -522,6 +728,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 307dbde3ca..2ddac45e80 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Shaders.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Shaders.hpp @@ -9,7 +9,29 @@ namespace AgcDriver::Graphics { -inline constexpr std::uint32_t PipelinePushConstantBytes = 128; +inline constexpr std::uint32_t PipelinePushConstantBytes = 256; +inline constexpr std::uint32_t PipelinePushSlotBytes = 128; +inline constexpr std::uint32_t ComputePushConstantBytes = 128; + +inline std::uint32_t PushBlockBytes(bool libraries) { + return libraries ? PipelinePushConstantBytes : PipelinePushSlotBytes; +} + +inline VkShaderStageFlags PreRasterizationPushStages(bool meshShader, bool tessellation, bool geometry) { + VkShaderStageFlags stages = VK_SHADER_STAGE_VERTEX_BIT; + if (tessellation) stages |= VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT | VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT; + if (geometry) stages |= VK_SHADER_STAGE_GEOMETRY_BIT; + if (meshShader) stages |= VK_SHADER_STAGE_MESH_BIT_EXT; + return stages; +} + +inline std::uint32_t FixedPushOffset(ShaderRecompiler::ShaderStage stage) { + return stage == ShaderRecompiler::ShaderStage::Fragment ? PipelinePushSlotBytes : 0u; +} + +inline std::uint32_t StagePushOffset(std::uint32_t cursor, ShaderRecompiler::ShaderStage stage, bool fixedSlots) { + return fixedSlots && stage == ShaderRecompiler::ShaderStage::Fragment ? PipelinePushSlotBytes : cursor; +} struct CompiledShader { ShaderRecompiler::ShaderStage stage; @@ -23,6 +45,7 @@ inline VkShaderStageFlagBits VulkanStage(ShaderRecompiler::ShaderStage stage) { case ShaderRecompiler::ShaderStage::Local: return VK_SHADER_STAGE_VERTEX_BIT; case ShaderRecompiler::ShaderStage::TessellationControl: return VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT; case ShaderRecompiler::ShaderStage::TessellationEvaluation: return VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT; + case ShaderRecompiler::ShaderStage::Geometry: return VK_SHADER_STAGE_GEOMETRY_BIT; case ShaderRecompiler::ShaderStage::Mesh: return VK_SHADER_STAGE_MESH_BIT_EXT; case ShaderRecompiler::ShaderStage::Fragment: return VK_SHADER_STAGE_FRAGMENT_BIT; case ShaderRecompiler::ShaderStage::Compute: return VK_SHADER_STAGE_COMPUTE_BIT; @@ -37,6 +60,7 @@ inline VkPipelineStageFlags PipelineStages(std::span shade case VK_SHADER_STAGE_VERTEX_BIT: result |= VK_PIPELINE_STAGE_VERTEX_SHADER_BIT; break; case VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT: result |= VK_PIPELINE_STAGE_TESSELLATION_CONTROL_SHADER_BIT; break; case VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT: result |= VK_PIPELINE_STAGE_TESSELLATION_EVALUATION_SHADER_BIT; break; + case VK_SHADER_STAGE_GEOMETRY_BIT: result |= VK_PIPELINE_STAGE_GEOMETRY_SHADER_BIT; break; case VK_SHADER_STAGE_MESH_BIT_EXT: result |= VK_PIPELINE_STAGE_MESH_SHADER_BIT_EXT; break; case VK_SHADER_STAGE_FRAGMENT_BIT: result |= VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; break; default: throw std::runtime_error("AGC graphics: invalid graphics pipeline stage"); @@ -49,7 +73,7 @@ inline VkShaderStageFlags PushConstantStages(std::span sha VkShaderStageFlags result = 0; for (const auto& shader : shaders) { Require(shader.program != nullptr, "missing compiled shader"); - if (!shader.program->pushConstants.empty() || shader.stage == ShaderRecompiler::ShaderStage::Mesh) result |= VulkanStage(shader.stage); + if (!shader.program->pushConstants.empty() || shader.program->readsDepthRemapPush || shader.stage == ShaderRecompiler::ShaderStage::Mesh || shader.stage == ShaderRecompiler::ShaderStage::TessellationControl) result |= VulkanStage(shader.stage); } return result; } @@ -63,6 +87,7 @@ inline std::array AssemblePushConstants(st if (bytes.empty()) continue; Require(bytes.size() % 4 == 0 && shader.pushConstantOffset % 4 == 0, "shader push constant range is not DWORD aligned"); Require(shader.pushConstantOffset < PipelinePushConstantBytes && bytes.size() <= PipelinePushConstantBytes - shader.pushConstantOffset, "shader push constant range lies outside the pipeline push constant block"); + Require(bytes.size() <= PipelinePushSlotBytes - shader.pushConstantOffset % PipelinePushSlotBytes, "shader push constant range crosses a push constant slot"); for (std::size_t i = 0; i < bytes.size(); ++i) { const auto position = shader.pushConstantOffset + i; Require(!occupied[position], "shader push constant ranges of different stages overlap"); 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 704b9ca7d8..00960ee917 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -3,9 +3,11 @@ #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" #include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" #include "prx/libSceAgcDriver/Execution/include/QueueState.hpp" #include #include +#include #include #include #include @@ -28,6 +30,8 @@ struct ShaderStages { std::uint32_t fragmentWaveSize; std::optional mesh; std::optional tessellation; + float minTessellationLevel = 1.0f; + float maxTessellationLevel = 64.0f; }; struct ColorTarget { @@ -38,12 +42,14 @@ struct ColorTarget { std::uint8_t componentMapping; ColorTileMode tileMode = ColorTileMode::Linear; std::uint32_t elementBytes = 4; + ShaderRecompiler::ColorExportPacking packing = ShaderRecompiler::ColorExportPacking::None; // DCC metadata of a compressed target (CB_COLOR_INFO DCC_ENABLE), or 0 (see DccMetadata.hpp). std::uint64_t dccAddress = 0; bool dccAlphaOnMsb = false; bool dccPipeAligned = false; std::uint64_t cmaskAddress = 0; std::size_t cmaskBytes = 0; + bool cmaskFastClear = false; std::uint64_t surfaceAddress = 0; VkExtent2D surfaceExtent{}; std::uint32_t mipCount = 1; @@ -53,8 +59,15 @@ struct ColorTarget { std::uint32_t slot = 0; std::uint32_t depth = 1; std::uint32_t depthSlice = 0; + std::uint64_t arrayAddress = 0; + std::uint32_t arrayLastSlice = 0; + std::uint32_t arraySlice = 0; + std::uint32_t layers = 1; std::uint32_t exportIndex = 0; bool uintExport = false; + std::uint32_t pipeBankXor = 0; + std::uint32_t samples = 1; + std::uint32_t dccControl = 0; }; struct DepthTarget { @@ -64,6 +77,17 @@ struct DepthTarget { VkFormat format; float clearDepth; std::uint8_t clearStencil; + 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 { @@ -79,42 +103,105 @@ struct State { float depthBiasConstant = 0.0f; float depthBiasSlope = 0.0f; float depthBiasClamp = 0.0f; + bool depthBiasPerFace = false; + float backDepthBiasConstant = 0.0f; + float backDepthBiasSlope = 0.0f; 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; VkPrimitiveTopology topology; bool primitiveRestart = false; 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; VkConservativeRasterizationModeEXT conservativeRasterization = VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT; VkRect2D scissor; VkCullModeFlags cullMode; VkFrontFace frontFace; + VkProvokingVertexModeEXT provokingVertexMode = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT; VkPipelineColorBlendAttachmentState blend; std::array blendConstants; + std::uint32_t samples = 1; + std::array sampleLocations{}; + bool customSampleLocations = false; + bool sampleLocationGrid = false; + std::uint32_t paClVsOutCntl = 0; + std::array quadSampleMask{0xffffu, 0xffffu, 0xffffu, 0xffffu}; + bool pixelEarlyZ = false; }; -ShaderStages DecodeShaderStages(const QueueState& queue); -State DecodeState(const QueueState& queue); +// PA_CL_VS_OUT_CNTL bits that place position exports (clip and cull distances, the misc and the two +// distance vectors); a vertex program that exports pos1..pos3 depends on them. +inline constexpr std::uint32_t PositionExportLayoutBits = 0xffffu | (1u << 21u) | (1u << 22u) | (1u << 23u); + +ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup = false); +State DecodeState(const QueueState& queue, bool vertexWorkgroup = false); +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); -ColorTarget DecodeColorBuffer(const Registers& context, std::uint32_t slot); +std::array ExportPackings(const State& state); +ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state); +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 }; + enum class Mode { EliminateFastClear, DccDecompress, Resolve, FmaskDecompress }; Mode mode; std::vector targets; + std::optional source; }; std::optional DecodeColorMetadataPass(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 // this precheck covers, evaluated without exceptions before the draw is decoded; empty when they // pass (DecodeState still checks everything). A register a rule needs that is absent is no verdict. @@ -133,7 +220,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) { @@ -152,17 +259,17 @@ struct DrawKeyRange { std::uint32_t first; std::uint32_t count; }; -inline constexpr std::array DrawKeyRegisters{{ - {RegisterBank::Context, 0x000, 1}, {RegisterBank::Context, 0x002, 1}, {RegisterBank::Context, 0x007, 7}, {RegisterBank::Context, 0x010, 6}, {RegisterBank::Context, 0x01a, 4}, +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, 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, 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, 0x3a8, 0x18}, + {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 // address; the vertex/geometry-front RSRC1/RSRC2 and user words; the vertex program address; // the hull user pointer, program address, RSRC2 and user words; the local program address. @@ -170,6 +277,9 @@ inline constexpr std::array DrawKeyRegisters{{ // GE_PRIM_TYPE, GE_MULTI_PRIM_IB_RESET_EN, the geometry subgroup sizes. {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 a14c015ef8..ae4f6dcaec 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -13,6 +13,8 @@ #include #include #include +#include +#include #include #include #include @@ -20,6 +22,7 @@ namespace AgcDriver::Graphics { +class Buffer; class ResidentColor; class CommandBatch; class StorageTexture; @@ -31,10 +34,11 @@ struct HostImport; VkFormat StorageFormatForGuest(const Context& context, std::uint32_t guestFormat); // Whether StorageFormatForGuest would succeed, without throwing (unknown guest formats included). bool StorageFormatAvailable(const Context& context, std::uint32_t guestFormat); -// Whether a storage image of the guest format takes DCC clear `keys` as a GPU clear (see -// StorageTexture::upload); false for integer formats and non-clear keys. +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); +bool ClearColorForTexel(VkFormat format, std::uint32_t elementBytes, std::span pattern, VkClearColorValue& clear); // A sampled texture's own VkImage with its memory, shared with the recorder while a recorded upload // still writes it (see the snapshot constructor), so the texture may go before the batch completes. @@ -42,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 { @@ -59,7 +63,27 @@ public: // compute pass writing it and the next pass sampling it share one image and copy nothing. // CanCopyFrom says whether the two descriptors address the same surface compatibly. Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components); - Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components); + Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D, 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 { + std::uint64_t textures; + std::uint64_t refreshes; + }; + static DepthCopyCounts TakeDepthCopyCounts(); static bool CanCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor); ~Texture(); Texture(const Texture&) = delete; @@ -67,6 +91,9 @@ public: VkImageView View() const; VkImageView FirstLayerView() const { return firstLayerView; } + VkImageView BaseLevelView() const; + bool HasSrgbView() const { return srgbInfo.has_value(); } + VkImageView SrgbView(bool firstLayer, bool baseLevel) const; struct ViewRange { VkImageViewType type; std::uint32_t levels; @@ -85,10 +112,20 @@ public: // the source image's own (snapshots, views over other metadata); one per cache entry, so every // object binding the texture shares it. Under GuestMemory::GpuMutex only. DccKeyProof& KeyProof() const { return keyProof; } + enum class PerUseCopy : std::uint8_t { None, RefreshedOnReuse, RemadePerUse }; + PerUseCopy CopyKind() const { return perUseCopy; } + bool RefreshedPerUse() const { return perUseCopy == PerUseCopy::RemadePerUse; } + void MarkRefreshedPerUse() { perUseCopy = PerUseCopy::RemadePerUse; } + 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); // Held by value: cached textures outlive the Context of the draw that created them. Context context; @@ -100,6 +137,13 @@ private: VkImageView firstLayerView = VK_NULL_HANDLE; ViewRange viewRange{}; ViewRange firstLayerRange{}; + std::optional baseLevelInfo; + mutable std::mutex baseLevelMutex; + mutable VkImageView baseLevelView = VK_NULL_HANDLE; + std::optional srgbInfo; + float srgbMinLod = 0.0f; + mutable std::mutex srgbMutex; + mutable std::array srgbViews{}; VkImageLayout layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; VkDeviceSize allocationBytes = 0; VkDeviceSize countedBytes = 0; @@ -107,9 +151,20 @@ private: std::shared_ptr source; 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; }; VkFormat AttachmentProxyFormat(const Context& context, VkFormat format); +VkFormat SrgbTwinFormat(const Context& context, VkFormat format); // A guest texture a shader writes through a storage image. It is uploaded like a sampled texture; // after the GPU work completes its results are stored to guest memory (retiled, changed bytes only), @@ -118,7 +173,7 @@ VkFormat AttachmentProxyFormat(const Context& context, VkFormat format); // or it leaves the cache. Dispatches reusing the image meanwhile skip the round trip entirely. class StorageTexture : public std::enable_shared_from_this { public: - StorageTexture(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, std::uint32_t mipLevel); + static std::shared_ptr Create(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, std::uint32_t mipLevel); ~StorageTexture(); StorageTexture(const StorageTexture&) = delete; StorageTexture& operator=(const StorageTexture&) = delete; @@ -129,19 +184,23 @@ public: VkImageView View(std::uint32_t mip); VkImageView FirstLayerView(std::uint32_t mip); VkImageView StorageView(std::uint32_t mip, bool firstLayer); + VkImageView ElementView(); VkImageView AtomicView(std::uint32_t mip, bool firstLayer); VkImageView Atomic64View(std::uint32_t mip, bool firstLayer); // Render targets live in the same images: draws attach mip 0 through a view of the color // buffer's format and mark the image dirty like a storage write. bool Attachable() const { return attachable; } - VkImageView AttachmentView(VkFormat format, std::uint32_t mip = 0, std::uint32_t depthSlice = 0); + VkImageView AttachmentView(VkFormat format, std::uint32_t mip = 0, std::uint32_t depthSlice = 0, std::uint32_t layers = 1, std::uint32_t layer = 0); VkImageView AttachmentProxyView(); void RecordAttachmentProxyLoad(VkCommandBuffer commands, VkImageLayout attachmentLayout) const; void RecordAttachmentProxyStore(VkCommandBuffer commands, VkImageLayout attachmentLayout) const; void WriteBack(); // Deferred write-back (APS5_EAGER_WRITEBACK=1 stores at once instead). void MarkDirty(); + void MarkWholeWritten(); + 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 @@ -151,12 +210,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 @@ -223,6 +291,7 @@ public: // contains a descriptor of its first mips), if any; the caller decides whether the geometry fits // (Texture::CanCopyFrom, ResidentPresentable). static std::shared_ptr FindPending(std::uint64_t address, std::uint64_t bytes); + static std::shared_ptr FindPendingSlice(std::uint64_t address, std::uint64_t bytes); // How a buffer fill of [address, address + bytes) (Driver.cpp's fill HLE) meets the storage // images alive: none of them; a whole surface (Exact) or one array layer of a surface (Layer), // `image` and `layer` naming it, whatever other images lie over the range (`others`, of which @@ -257,7 +326,7 @@ public: // captures, snapshots) takes the bytes through the flush hook, and this image alone is current. // False, naming why in `refusal`, when the fill must be stored as before. static constexpr std::uint32_t WholeImage = ~0u; - bool FillClear(std::span pattern, std::uint32_t layer, const char*& refusal); + bool FillClear(std::span pattern, std::uint32_t layer, const char*& refusal, bool allowRegisterClear = false); // The cached, live image whose surface is exactly [address, address + bytes), if any (the newest // of several: see ClassifyFill). static std::shared_ptr FindLive(std::uint64_t address, std::uint64_t bytes); @@ -271,6 +340,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; } @@ -285,23 +355,36 @@ public: // GuestMemory::GpuMutex only, as Refresh is; never from a build's stage A. DccKeys UploadedKeys() const { return uploadedKeys; } DccKeys FilledKeys() const { return filledKeys; } + bool GuestSnapshotValid() const { return originalHolds(descriptor.baseAddress, static_cast(guestBytes)); } DccKeyProof& KeyProof() const { return keyProof; } + DccRangeProof& TargetKeyProof() const { return targetKeyProof; } DccKeys ProvedKeys() const; bool ServesKeysAt(std::uint64_t dccAddress) const; // Brings the image up to date with guest memory before another use; returns whether its content // was still current (nothing uploaded). // Keeps the image current with guest memory (see GuestMemory::CollectWrites). bool Refresh(); + 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; } 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); void captureGuestBytes(const std::vector* layers); + bool originalHolds(std::uint64_t address, std::size_t bytes) const; bool compareUntracked(std::uint64_t address, std::size_t bytes, std::span changed, bool memoize = false) const; // Stores the pending tracked layers overlapping [address, address + bytes) to guest memory; in // each, 64 KiB blocks the CPU wrote since the layer's generation keep the CPU's bytes. Block @@ -310,6 +393,25 @@ private: // APS5_BLOCK_WRITEBACK_EACH=1 stores the touched units only). void writeBack(std::uint64_t address, std::size_t bytes); void writeBackLayers(const std::vector& layers); + bool unchangedSinceBaseline(std::uint64_t from, std::uint64_t to) const; + void saveBeforeUnwatch(); + void forgetKeptAtUnwatch(std::uint32_t unit); + bool keptAtUnwatchHolds(std::uint64_t from, std::uint32_t unit) const; + // Drops the saved guest bytes of the units over [address, address + bytes) (another image's fill + // or copy supersedes them there); the other units' bytes stay valid. + void forgetSavedOver(std::uint64_t address, std::size_t bytes); + // 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 // layer, a CPU write or another image's store then costs the blocks it touched, moved through @@ -354,13 +456,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 @@ -369,6 +478,9 @@ private: std::uint64_t layerBegin(std::uint32_t layer) const { return descriptor.baseAddress + static_cast(layer) * trackedLayerBytes; } bool anyLayerPending() const; void refreshGeneration(); + bool refreshProved(); + void takeRefreshProof(bool aliased); + bool otherPendingOverlaps() const; // Marks `count` tracked layers from `first` pending and registers the image (MarkDirty's // registration; APS5_EAGER_WRITEBACK=1 stores at once instead). void markLayersPending(std::uint32_t first, std::uint32_t count); @@ -397,6 +509,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); @@ -426,6 +540,10 @@ private: bool keysFillMatches(std::uint64_t address, std::size_t bytes, bool overlapped) const; bool pendingUnitInside(std::uint64_t address, std::size_t bytes) const; VkImageView createView(std::uint32_t mip, bool firstLayer, VkFormat format) const; + bool singlePass(); + VkImageView elementLayerView(std::uint32_t level, std::uint32_t layer); + void recordDirectUploadBarrier(VkCommandBuffer commands, bool discard); + void recordDirectUploadDone(VkCommandBuffer commands); void release() noexcept; Context context; @@ -437,11 +555,20 @@ private: std::uint64_t sliceLinearBytes = 0; SurfaceGeometry geometry; std::vector original; - mutable std::array comparedGuestBytes{}; + mutable std::array comparedGuestBytes{}; + std::vector generationBaseline; // DCC keys the image content was uploaded under: a fast-cleared surface starts as its clear value. DccKeys uploadedKeys = DccKeys::Uncompressed; mutable DccKeys filledKeys = DccKeys::Uncompressed; mutable DccKeyProof keyProof; + mutable DccRangeProof targetKeyProof; + struct RefreshProof { + std::uint64_t generation = 0; + std::uint64_t pendingSerial = 0; + std::uint64_t keyGeneration = 0; + DccKeys keys = DccKeys::Uncompressed; + }; + RefreshProof refreshProof; struct ForeignKeyProof { std::uint64_t dccAddress = 0; DccKeyProof proof; @@ -464,6 +591,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. @@ -471,14 +600,18 @@ 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; std::map firstLayerViews; std::map, VkImageView> atomicViews; std::map, VkImageView> uintViews; + VkImageView elementView = VK_NULL_HANDLE; + std::map elementLayerViews; + std::int8_t singlePassState = -1; bool attachable = false; - std::map, VkImageView> attachmentViews; + std::map, VkImageView> attachmentViews; VkImage proxyImage = VK_NULL_HANDLE; VkDeviceMemory proxyMemory = VK_NULL_HANDLE; VkImageView proxyView = VK_NULL_HANDLE; @@ -486,8 +619,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. - bool originalValid = true; + 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; @@ -516,7 +654,7 @@ private: // element (a fast miss is followed by a full lookup); the refresh, upload, DCC scan and pending // flush rows lie inside the storage and sampled rows. struct LookupOutcomes { - enum Kind : std::size_t { SampledFast, SampledFastMiss, SampledHitView, SampledHitClearedView, SampledHitSnapshot, SampledMadeView, SampledMadeSnapshot, StorageHit, StorageMade, RefreshUnchanged, RefreshCompared, UploadDirect, UploadCpu, UploadClear, DccScan, PendingFlush, Count }; + enum Kind : std::size_t { SampledFast, SampledFastMiss, SampledHitView, SampledHitClearedView, SampledHitSnapshot, SampledMadeView, SampledMadeSnapshot, StorageHit, StorageMade, RefreshUnchanged, RefreshCompared, RefreshProved, UploadDirect, UploadCpu, UploadClear, DccScan, PendingFlush, Count }; static const char* Name(Kind kind); static bool Profiled(); // Charges the time since `start` to `kind` on this thread and returns now. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp index 7ffb40425c..cc70a270e8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp @@ -5,12 +5,16 @@ #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include #include +#include #include #include namespace AgcDriver::Graphics { + class Buffer; + // The part of a mip one Dispatch moves: only elements whose tiled offset (relative to the mip's // tiled base; the linear offset of a linear surface) lies in [rangeBegin, rangeEnd), with the // tiled buffer holding the mip from tiledBase and the linear buffer holding `linearBytes` of @@ -29,6 +33,7 @@ namespace AgcDriver::Graphics { std::uint32_t columnEnd = 0; std::uint32_t rowBegin = 0; std::uint32_t rowEnd = 0; + std::uint32_t pipeBankXor = 0; }; class TextureDetiler { @@ -43,21 +48,52 @@ namespace AgcDriver::Graphics { // restricts the move to part of the mip (a partial upload or write-back, StorageTexture), // 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); private: - VkPipeline pipeline(TextureTileMode tileMode, std::uint32_t elementBytes, bool retile, bool thick); + VkPipeline multisampleReadbackPipeline(std::uint32_t elementBytes, bool depth); + VkPipeline pipeline(TextureTileMode tileMode, std::uint32_t elementBytes, bool retile, bool thick, bool image = false); void release() noexcept; - VkDescriptorSet allocateSet(); + 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; VkDescriptorSetLayout descriptorLayout = VK_NULL_HANDLE; 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; + DescriptorRing cmaskDescriptorSets; + std::unique_ptr cmaskErrors; + VkDescriptorSetLayout imageDescriptorLayout = VK_NULL_HANDLE; + VkPipelineLayout imagePipelineLayout = VK_NULL_HANDLE; + VkShaderModule imageModule = VK_NULL_HANDLE; + 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; }; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp index 47552e4dc0..a2625c2e6b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp @@ -13,13 +13,19 @@ namespace AgcDriver::Graphics { struct Context; 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 3f1b18e3ff..d408819b49 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp @@ -84,11 +84,21 @@ 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}}, {0x119u, 8u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x01000000u, 0x00001000u, 0x00000002u, 0x02000000u, 0x20002020u, 0x10010004u, 0x04008010u, 0x08004008u, 0x00000008u, 0x08000000u, 0x00008000u, 0x00000010u}}, {0x119u, 16u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x01000000u, 0x00001000u, 0x00000001u, 0x02000000u, 0x20002010u, 0x10010002u, 0x04008008u, 0x08004004u, 0x00000004u, 0x08000000u, 0x00008000u, 0x00000008u}}, + {0x11au, 1u, {0x00000001u, 0x00000002u, 0x01000000u, 0x00001000u, 0x00002000u, 0x02000000u, 0x00000004u, 0x04000000u, 0x00008008u, 0x00010010u, 0x00024040u, 0x00040020u, 0x08000000u, 0x00008000u, 0x00000010u, 0x10000000u}}, + {0x11au, 2u, {0x00000000u, 0x00000001u, 0x01000000u, 0x00001000u, 0x00000002u, 0x02000000u, 0x00002000u, 0x04000000u, 0x00008008u, 0x00010010u, 0x08020040u, 0x00044020u, 0x00000004u, 0x10000000u, 0x00008000u, 0x00000010u}}, + {0x11au, 4u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x00000002u, 0x01000000u, 0x00002000u, 0x02000000u, 0x00008008u, 0x00010010u, 0x04020040u, 0x00044020u, 0x00000004u, 0x08000000u, 0x00008000u, 0x00000010u}}, + {0x11au, 8u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x01000000u, 0x00000002u, 0x02000000u, 0x00008008u, 0x00010010u, 0x04020040u, 0x00042020u, 0x00000004u, 0x08000000u, 0x00004000u, 0x00000008u}}, + {0x11au, 16u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x01000000u, 0x00001000u, 0x02000000u, 0x00008008u, 0x00010012u, 0x04020040u, 0x00042020u, 0x00000004u, 0x08000000u, 0x00004000u, 0x00000008u}}, }; inline const TextureSwizzleEquation* FindTextureSwizzleEquation(std::uint32_t swizzleMode, std::uint32_t elementBytes) { 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/UnitShadow.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp index 7748a0e9dd..4c8124f513 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp @@ -141,7 +141,7 @@ void MarkShadowed(const HostImport& import, std::span range // The import retires because its registration vanished or changed size, so the rest of its memory // belongs to the title again and is dropped (counted `dropped on retire`); `lost on retire` counts // the units nothing could record. -void RetireShadow(const Context& context, const HostImport& import, const std::function& registered); +void RetireShadow(VkDevice device, const HostImport& import, const std::function& registered); // Teardown: every shadow of the context's device is published; DestroyShadows then drops them. void PublishAllShadows(const Context& context, PublishReason reason); void DestroyShadows(VkDevice device); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp index 1964f46beb..a8d93a9a85 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp @@ -3,7 +3,13 @@ #include "prx/libSceAgcDriver/Graphics/include/Shaders.hpp" #include +#include +#include +#include +#include +#include #include +#include #include #include #include @@ -18,8 +24,17 @@ struct VertexFormat { const char* scalar; }; +inline bool NullVertexDescriptor(const ShaderRecompiler::VertexAttribute& attribute) { + const auto& fields = attribute.resource.fields; + return fields[0] == 0 && fields[1] == 0 && fields[2] == 0 && fields[3] == 0; +} + inline VertexFormat DecodeVertexFormat(const ShaderRecompiler::VertexAttribute& attribute) { Require(attribute.components >= 1 && attribute.components <= 4, "invalid vertex attribute component count"); + if (NullVertexDescriptor(attribute)) { + const std::array formats{VK_FORMAT_R32_SFLOAT, VK_FORMAT_R32G32_SFLOAT, VK_FORMAT_R32G32B32_SFLOAT, VK_FORMAT_R32G32B32A32_SFLOAT}; + return {formats[attribute.components - 1], attribute.components * 4u, 4u, "f32"}; + } const auto components = attribute.formatComponents == 0u ? attribute.components : attribute.formatComponents; Require(components >= 1u && components <= 4u, "invalid vertex format component count"); const auto format = (attribute.resource.fields[3] >> 12u) & 0x7fu; @@ -108,7 +123,7 @@ inline VertexInputLayout BuildVertexInputLayout(const Context& context, std::spa Require((fields[1] & 0x80000000u) == 0 && (fields[3] & 0x00800000u) == 0 && (fields[3] >> 30u) == 0, "unsupported vertex buffer descriptor flags"); const auto stride = (fields[1] >> 16u) & 0x3fffu; const auto address = fields[0] | (static_cast(fields[1] & 0xffffu) << 32u); - Require(address != 0 && address % format.alignment == 0 && stride % format.alignment == 0, "unaligned vertex buffer"); + Require(NullVertexDescriptor(attribute) || (address != 0 && address % format.alignment == 0 && stride % format.alignment == 0), "unaligned vertex buffer"); Require(stride <= context.limits.maxVertexInputBindingStride, "vertex stride exceeds device limits"); Require(context.formatProperties != nullptr, "missing vertex format property query"); VkFormatProperties properties{}; @@ -121,13 +136,116 @@ 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; + const auto select = VertexBufferOutOfBoundsSelect(attribute); + return stride == 0 && (select == 2u || select == 3u) && attribute.resource.fields[2] == 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; +} + inline std::size_t VertexBufferExtent(const ShaderRecompiler::VertexAttribute& attribute) { + if (NullVertexDescriptor(attribute)) return DecodeVertexFormat(attribute).bytes; 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 ? static_cast(records) : 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"); @@ -137,20 +255,28 @@ inline std::size_t VertexBufferExtent(const ShaderRecompiler::VertexAttribute& a inline std::size_t VertexBufferReadSize(const ShaderRecompiler::VertexAttribute& attribute, std::uint32_t maxIndex, std::uint32_t instances, std::uint32_t firstInstance = 0) { Require(instances != 0, "vertex input requires nonzero instance count"); Require(firstInstance <= std::numeric_limits::max() - (instances - 1u), "vertex input instance range overflow"); + if (NullVertexDescriptor(attribute)) return DecodeVertexFormat(attribute).bytes; 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 index = attribute.fetchIndex == 0 ? maxIndex : firstInstance + instances - 1u; const auto bytes = DecodeVertexFormat(attribute).bytes; - Require(stride == 0 || index < records, "vertex fetch exceeds descriptor record count"); - const auto available = stride == 0 ? static_cast(records) : static_cast(records) * stride; + Require(stride == 0 || VertexBufferOutOfBoundsSelect(attribute) <= 1u || index < records, "vertex fetch exceeds descriptor record count"); const auto required = static_cast(stride) * index + bytes; - Require(required <= available && required <= std::numeric_limits::max(), "vertex fetch exceeds descriptor byte range"); + Require(stride != 0 || required <= ZeroStrideAvailableBytes(attribute), "vertex fetch exceeds descriptor byte range"); + Require(required <= std::numeric_limits::max(), "vertex fetch exceeds addressable byte range"); const auto address = attribute.resource.fields[0] | (static_cast(attribute.resource.fields[1] & 0xffffu) << 32u); Require(address != 0 && required <= std::numeric_limits::max() - address, "invalid vertex buffer address range"); return static_cast(required); } +inline std::size_t VertexBufferReadOffset(const ShaderRecompiler::VertexAttribute& attribute, std::uint32_t firstVertex, std::uint32_t firstInstance) { + Require(attribute.fetchIndex <= 1, "unsupported vertex fetch index"); + const auto stride = (attribute.resource.fields[1] >> 16u) & 0x3fffu; + const auto index = attribute.fetchIndex == 0 ? firstVertex : firstInstance; + return static_cast(static_cast(stride) * index); +} + struct VertexFetch { std::uint64_t begin; std::uint64_t end; @@ -163,6 +289,7 @@ struct VertexCopyPlan { std::vector> copies; std::vector copyOf; std::vector offsets; + std::vector alignments; }; inline VertexCopyPlan PlanVertexCopies(std::span fetches) { @@ -186,7 +313,9 @@ inline VertexCopyPlan PlanVertexCopies(std::span fetches) { if (!joins) { lead = i; plan.copies.emplace_back(fetch.begin, fetch.end); + plan.alignments.push_back(fetch.alignment); } + plan.alignments.back() = std::max(plan.alignments.back(), fetch.alignment); auto& copy = plan.copies.back(); copy.second = std::max(copy.second, fetch.end); plan.copyOf[i] = plan.copies.size() - 1; @@ -195,7 +324,111 @@ inline VertexCopyPlan PlanVertexCopies(std::span fetches) { return plan; } +inline std::optional ZeroFetchVertex(std::span attributes) { + std::uint32_t zeroFrom = 0; + for (const auto& attribute : attributes) { + const auto stride = (attribute.resource.fields[1] >> 16u) & 0x3fffu; + if (NullVertexDescriptor(attribute) || attribute.fetchIndex != 0 || stride == 0) continue; + if (VertexBufferOutOfBoundsSelect(attribute) > 1u) return std::nullopt; + zeroFrom = std::max(zeroFrom, attribute.resource.fields[2]); + } + return zeroFrom; +} + +inline std::optional ClampDrawIndices(std::span indices, std::uint32_t indexSize, bool skipRestart, std::uint32_t limit) { + 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; + if (index > limit) { + index = limit; + if (indexSize == 2) { + const auto value = static_cast(index); + std::memcpy(indices.data() + offset, &value, sizeof(value)); + } else { + std::memcpy(indices.data() + offset, &index, sizeof(index)); + } + } + highest = std::max(highest.value_or(0u), index); + } + 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"); + return indexSize == 2 ? HighestDrawIndexOf(indices, skipRestart) : HighestDrawIndexOf(indices, skipRestart); +} + +inline std::vector MeshRestartTable(std::span indices, std::uint32_t indexSize) { + Require(indexSize == 2 || indexSize == 4, "unsupported index size"); + const auto restartIndex = indexSize == 2 ? 0xffffu : 0xffffffffu; + std::vector starts(indices.size() / indexSize); + std::uint32_t stripStart = 0; + bool restarted = false; + for (std::size_t position = 0; position < starts.size(); ++position) { + std::uint32_t index = 0; + if (indexSize == 2) { + std::uint16_t value = 0; + std::memcpy(&value, indices.data() + position * 2u, sizeof(value)); + index = value; + } else { + std::memcpy(&index, indices.data() + position * 4u, sizeof(index)); + } + if (index == restartIndex) { + restarted = true; + stripStart = static_cast(position) + 1u; + } + starts[position] = stripStart; + } + if (!restarted) starts.clear(); + return starts; +} + +inline std::vector SoloZeroPaddedFetchIndices(const std::vector& fetches, const std::vector& fetchValid) { + Require(fetchValid.size() == fetches.size(), "fetch validity does not match the fetch count"); + std::vector solo; + for (std::size_t i = 0; i < fetches.size(); ++i) { + if (fetchValid[i] < fetches[i].end - fetches[i].begin) solo.push_back(i); + } + return solo; +} + } #endif - diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/CmaskClear.comp b/core/libs/prx/libSceAgcDriver/Graphics/shaders/CmaskClear.comp new file mode 100644 index 0000000000..935a0e81af --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/CmaskClear.comp @@ -0,0 +1,38 @@ +#version 450 + +layout(local_size_x = 8, local_size_y = 8) in; + +layout(set = 0, binding = 0, std430) readonly buffer Cmask { uint words[]; } cmask; +layout(set = 0, binding = 1, std430) buffer Errors { uint codes; } errors; +layout(set = 0, binding = 2) writeonly uniform uimage2D target; + +layout(push_constant) uniform Push { + uint base; + uint width; + uint height; + uint blocksPerRow; + uint clear0; + uint clear1; + uint write; +} push; + +const uint xMasks[13] = uint[](0x008u, 0x000u, 0x040u, 0x000u, 0x080u, 0x000u, 0x100u, 0x000u, 0x200u, 0x008u, 0x010u, 0x040u, 0x020u); +const uint yMasks[13] = uint[](0x000u, 0x010u, 0x000u, 0x040u, 0x000u, 0x080u, 0x000u, 0x100u, 0x000u, 0x008u, 0x010u, 0x020u, 0x040u); + +void main() { + uvec2 pixel = gl_GlobalInvocationID.xy; + if (pixel.x >= push.width || pixel.y >= push.height) return; + uint x = pixel.x & ~7u; + uint y = pixel.y & ~7u; + uint offset = 0u; + for (uint bit = 0u; bit < 13u; ++bit) offset |= (uint(bitCount((x & xMasks[bit]) ^ (y & yMasks[bit]))) & 1u) << bit; + uint nibble = ((y / 512u) * push.blocksPerRow + x / 1024u) * 8192u + offset; + uint byteAddress = push.base + nibble / 2u; + uint code = (cmask.words[byteAddress / 4u] >> ((byteAddress % 4u) * 8u + (nibble % 2u) * 4u)) & 0xfu; + if (code != 0u) return; + if (push.write != 0u) { + imageStore(target, ivec2(pixel), uvec4(push.clear0, push.clear1, 0u, 0u)); + return; + } + if ((pixel.x & 7u) == 0u && (pixel.y & 7u) == 0u) atomicOr(errors.codes, 1u); +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/ColorTransfer.comp b/core/libs/prx/libSceAgcDriver/Graphics/shaders/ColorTransfer.comp index d9ae43c660..7e787cb840 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/shaders/ColorTransfer.comp +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/ColorTransfer.comp @@ -1,15 +1,30 @@ #version 450 +#extension GL_GOOGLE_include_directive : require + +#include "PqToSdr.glsl" layout(local_size_x = 8, local_size_y = 8) in; layout(set = 0, binding = 0, std430) buffer TiledPixels { uint tiled[]; }; layout(set = 0, binding = 1, std430) buffer LinearPixels { uint linear[]; }; +layout(set = 0, binding = 2, std430) readonly buffer ScanoutThresholds { uint thresholds[]; }; +layout(set = 0, binding = 3, std430) readonly buffer PqLuminance { float pqNits[]; }; layout(push_constant) uniform Parameters { uint width; uint height; uint blocksPerRow; uint mode; + float whiteNits; } parameters; +uint scanoutCode(uint bits, uint base) { + if (bits > 0x7c00u) return 0u; + uint code = 0u; + for (uint step = 128u; step != 0u; step >>= 1u) { + if (code + step <= 255u && thresholds[base + code + step - 1u] <= bits) code += step; + } + return code; +} + void main() { uint x = gl_GlobalInvocationID.x; uint y = gl_GlobalInvocationID.y; @@ -21,7 +36,27 @@ void main() { ^ ((x << 2u) & 0x000cu) ^ ((x << 5u) & 0x0380u) ^ ((x << 4u) & 0x0400u) ^ ((x << 6u) & 0x0800u) ^ ((x << 9u) & 0xa000u); tiledIndex = ((y / 128u) * parameters.blocksPerRow + x / 128u) * 16384u + offset / 4u; } - uint pixel = (parameters.mode & 1u) != 0u ? linear[linearIndex] : tiled[tiledIndex]; + uint pixel; + if ((parameters.mode & 16u) != 0u) { + uint texel = linearIndex * 2u; + if ((parameters.mode & 4u) != 0u) { + uint offset = ((x << 3u) & 0x0008u) ^ ((x << 4u) & 0x0460u) ^ ((x << 5u) & 0x0300u) ^ ((x << 6u) & 0x0800u) ^ ((x << 9u) & 0x8000u) ^ ((x << 10u) & 0x2000u) + ^ ((y << 4u) & 0x0010u) ^ ((y << 6u) & 0x0080u) ^ ((y << 5u) & 0x0f00u) ^ ((y << 10u) & 0x5000u); + texel = (((y / 64u) * parameters.blocksPerRow + x / 128u) * 65536u + offset) / 4u; + } + uint first = tiled[texel]; + uint second = tiled[texel + 1u]; + pixel = scanoutCode(first & 0xffffu, 0u) | (scanoutCode(first >> 16u, 0u) << 8u) | (scanoutCode(second & 0xffffu, 0u) << 16u) | (scanoutCode(second >> 16u, 255u) << 24u); + } else { + pixel = (parameters.mode & 1u) != 0u ? linear[linearIndex] : tiled[tiledIndex]; + } + if ((parameters.mode & 32u) != 0u) { + vec3 nits = vec3(pqNits[pixel & 0x3ffu], pqNits[(pixel >> 10u) & 0x3ffu], pqNits[(pixel >> 20u) & 0x3ffu]); + vec3 encoded = srgbEncode(pqToSdrLinear((parameters.mode & 2u) != 0u ? nits : nits.zyx, parameters.whiteNits)); + uvec3 codes = uvec3(floor(encoded * 255.0 + 0.5)); + linear[linearIndex] = codes.b | (codes.g << 8u) | (codes.r << 16u) | 0xff000000u; + return; + } // Mode 8: A2B10G10R10 source (red in the low bits) presented as 8-bit RGBA. if ((parameters.mode & 8u) != 0u) pixel = ((pixel >> 2u) & 0xffu) | (((pixel >> 12u) & 0xffu) << 8u) | (((pixel >> 22u) & 0xffu) << 16u) | 0xff000000u; if ((parameters.mode & 2u) != 0u) pixel = (pixel & 0xff00ff00u) | ((pixel & 0xffu) << 16u) | ((pixel >> 16u) & 0xffu); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/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/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/PqToSdr.glsl b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PqToSdr.glsl new file mode 100644 index 0000000000..4a35119dea --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PqToSdr.glsl @@ -0,0 +1,28 @@ +float pqToneMap(float luminance, float relativeWhite) { + const float knee = 0.75; + if (luminance <= knee) return luminance; + float t = (luminance - knee) / (1.0 - knee); + float w = (relativeWhite - knee) / (1.0 - knee); + return knee + (1.0 - knee) * t * (1.0 + t / (w * w)) / (1.0 + t); +} + +vec3 pqToSdrLinear(vec3 nits, float whiteNits) { + float referenceWhite = min(203.0, whiteNits); + vec3 bt2020 = nits / referenceWhite; + vec3 rgb = max(vec3( + 1.6604910 * bt2020.r - 0.5876411 * bt2020.g - 0.0728499 * bt2020.b, + -0.1245505 * bt2020.r + 1.1328999 * bt2020.g - 0.0083494 * bt2020.b, + -0.0181508 * bt2020.r - 0.1005789 * bt2020.g + 1.1187297 * bt2020.b), vec3(0.0)); + float luminance = 0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b; + if (luminance > 0.0) rgb = min(rgb * (pqToneMap(luminance, whiteNits / referenceWhite) / luminance), vec3(1.0)); + return rgb; +} + +float srgbEncode(float linear) { + float v = clamp(linear, 0.0, 1.0); + return v <= 0.0031308 ? 12.92 * v : 1.055 * pow(v, 1.0 / 2.4) - 0.055; +} + +vec3 srgbEncode(vec3 linear) { + return vec3(srgbEncode(linear.r), srgbEncode(linear.g), srgbEncode(linear.b)); +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/PresentationPass.frag b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PresentationPass.frag new file mode 100644 index 0000000000..ccd35c25ad --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PresentationPass.frag @@ -0,0 +1,62 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require + +#include "PqToSdr.glsl" + +layout(set = 0, binding = 0) uniform usampler2D source; +layout(set = 0, binding = 1, std430) readonly buffer PqLuminance { float pqNits[]; }; +layout(push_constant) uniform Parameters { + ivec2 origin; + ivec2 size; + ivec2 sourceSize; + int stride; + uint mode; + float whiteNits; +} parameters; +layout(location = 0) out vec4 color; + +vec4 decode(uint word) { + if ((parameters.mode & 16u) != 0u) { + vec3 nits = vec3(pqNits[word & 0x3ffu], pqNits[(word >> 10u) & 0x3ffu], pqNits[(word >> 20u) & 0x3ffu]); + return vec4(srgbEncode(pqToSdrLinear((parameters.mode & 2u) != 0u ? nits : nits.zyx, parameters.whiteNits)), 1.0); + } + uvec4 channels; + float levels = 255.0; + if ((parameters.mode & 1u) != 0u) { + channels = uvec4(word & 0x3ffu, (word >> 10u) & 0x3ffu, (word >> 20u) & 0x3ffu, 0u); + if ((parameters.mode & 4u) != 0u) levels = 1023.0; + else channels >>= 2u; + channels.w = uint(levels); + } else { + channels = uvec4(word & 0xffu, (word >> 8u) & 0xffu, (word >> 16u) & 0xffu, word >> 24u); + } + vec4 value = vec4(channels) / levels; + return (parameters.mode & 2u) != 0u ? value : value.zyxw; +} + +vec4 texel(ivec2 position) { + return decode(texelFetch(source, clamp(position, ivec2(0), parameters.sourceSize - 1), 0).x); +} + +void main() { + ivec2 pixel = ivec2(gl_FragCoord.xy) - parameters.origin; + if (any(lessThan(pixel, ivec2(0))) || any(greaterThanEqual(pixel, parameters.size))) { + color = vec4(0.0, 0.0, 0.0, 1.0); + return; + } + if ((parameters.mode & 8u) != 0u) { + color = texel(pixel * parameters.stride); + return; + } + if (parameters.size == parameters.sourceSize) { + color = texel(pixel); + return; + } + vec2 position = (vec2(pixel) + 0.5) * vec2(parameters.sourceSize) / vec2(parameters.size) - 0.5; + vec2 base = floor(position); + vec2 weight = position - base; + ivec2 corner = ivec2(base); + vec4 top = mix(texel(corner), texel(corner + ivec2(1, 0)), weight.x); + vec4 bottom = mix(texel(corner + ivec2(0, 1)), texel(corner + ivec2(1, 1)), weight.x); + color = mix(top, bottom, weight.y); +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/PresentationPass.vert b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PresentationPass.vert new file mode 100644 index 0000000000..8b3c0059dd --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PresentationPass.vert @@ -0,0 +1,6 @@ +#version 450 + +void main() { + vec2 corner = vec2(float((gl_VertexIndex << 1) & 2), float(gl_VertexIndex & 2)); + gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0); +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/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 new file mode 100644 index 0000000000..1549983e45 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleDumps.comp @@ -0,0 +1,43 @@ +#version 450 +#extension GL_EXT_buffer_reference : require +#extension GL_EXT_buffer_reference_uvec2 : require + +layout(local_size_x = 1) in; +layout(buffer_reference, std430, buffer_reference_align = 8) buffer Words { uint words[]; }; +layout(push_constant) uniform Parameters { + Words counter; + Words dumps; + uint firstDump; + uint dumpCount; + uint firstSegment; + uint lastSegment; + uint phase; +} parameters; + +void main() { + uint low = parameters.counter.words[0]; + uint high = parameters.counter.words[1]; + uint segment = parameters.firstSegment; + for (uint dump = parameters.firstDump; dump < parameters.firstDump + parameters.dumpCount; ++dump) { + uint end = parameters.dumps.words[dump * 4u + 2u]; + for (; segment < end; ++segment) { + uint carry; + low = uaddCarry(low, parameters.counter.words[4u + 2u * segment], carry); + high = high + parameters.counter.words[5u + 2u * segment] + carry; + } + Words target = Words(uvec2(parameters.dumps.words[dump * 4u], parameters.dumps.words[dump * 4u + 1u])); + for (uint db = 0u; db < 16u; ++db) { + 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); + high = high + parameters.counter.words[5u + 2u * segment] + carry; + } + parameters.counter.words[0] = low; + parameters.counter.words[1] = high; +} 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/TextureDetile.comp b/core/libs/prx/libSceAgcDriver/Graphics/shaders/TextureDetile.comp index 38b17aeae2..fcff04497b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/shaders/TextureDetile.comp +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/TextureDetile.comp @@ -1,4 +1,7 @@ #version 450 +#ifdef LINEAR_IMAGE +#extension GL_EXT_shader_image_load_formatted : require +#endif layout(constant_id = 0) const uint ELEMENT_BYTES = 4u; layout(constant_id = 1) const uint BLOCK_BYTES = 256u; @@ -29,8 +32,13 @@ layout(constant_id = 21) const uint BLOCK_HEIGHT = 0u; layout(local_size_x = 8, local_size_y = 8) in; +#ifdef LINEAR_IMAGE +layout(set = 0, binding = 0, std430) buffer Tiled { uint data[]; } tiledBuffer; +layout(set = 0, binding = 1) uniform uimage2D linearImage; +#else layout(set = 0, binding = 0, std430) readonly buffer Input { uint data[]; } inputBuffer; layout(set = 0, binding = 1, std430) buffer Output { uint data[]; } outputBuffer; +#endif layout(push_constant) uniform Push { uint srcBase; @@ -55,6 +63,7 @@ layout(push_constant) uniform Push { // whole mip). uint columnBegin; uint rowBegin; + uint pipeBankXor; } params; uint equationBit(uint mask, uvec2 p) { @@ -117,7 +126,7 @@ uint standard64Extra(uint x, uint y) { uint blockOffset(uvec2 p) { if (TILE_FAMILY == 2u) { - return equationOffset(p); + return equationOffset(p) ^ params.pipeBankXor; } uint offset = standardOffset(p.x, p.y); if (BLOCK_BYTES > 4096u) { @@ -141,6 +150,43 @@ uvec2 blockExtent() { return uvec2(width, BLOCK_BYTES / (width * ELEMENT_BYTES)); } +#ifdef LINEAR_IMAGE +void storeElement(uint src, uvec2 p) { + uvec4 value = uvec4(0u); + uint word = src >> 2; + if (ELEMENT_BYTES == 1u) { + value.x = (tiledBuffer.data[word] >> ((src & 3u) * 8u)) & 0xffu; + } else if (ELEMENT_BYTES == 2u) { + value.x = (tiledBuffer.data[word] >> ((src & 3u) * 8u)) & 0xffffu; + } else { + value.x = tiledBuffer.data[word]; + if (ELEMENT_BYTES >= 8u) value.y = tiledBuffer.data[word + 1u]; + if (ELEMENT_BYTES >= 16u) { + value.z = tiledBuffer.data[word + 2u]; + value.w = tiledBuffer.data[word + 3u]; + } + } + imageStore(linearImage, ivec2(p), value); +} + +void loadElement(uvec2 p, uint dst) { + uvec4 value = imageLoad(linearImage, ivec2(p)); + uint word = dst >> 2; + if (ELEMENT_BYTES >= 4u) { + tiledBuffer.data[word] = value.x; + if (ELEMENT_BYTES >= 8u) tiledBuffer.data[word + 1u] = value.y; + if (ELEMENT_BYTES >= 16u) { + tiledBuffer.data[word + 2u] = value.z; + tiledBuffer.data[word + 3u] = value.w; + } + } else { + uint mask = ELEMENT_BYTES == 1u ? 0xffu : 0xffffu; + uint shift = (dst & 3u) * 8u; + atomicAnd(tiledBuffer.data[word], ~(mask << shift)); + atomicOr(tiledBuffer.data[word], (value.x & mask) << shift); + } +} +#else void copyElement(uint src, uint dst) { if (ELEMENT_BYTES >= 4u) { for (uint i = 0u; i < ELEMENT_BYTES; i += 4u) { @@ -154,6 +200,7 @@ void copyElement(uint src, uint dst) { atomicOr(outputBuffer.data[dst >> 2], value << shift); } } +#endif void main() { uvec2 p = gl_GlobalInvocationID.xy + uvec2(params.columnBegin, params.rowBegin); @@ -169,11 +216,19 @@ void main() { if (linearOffset < params.rangeBegin || linearOffset >= params.rangeEnd) { return; } +#ifdef LINEAR_IMAGE + if (RETILE != 0u) { + loadElement(p, params.dstBase + linearOffset - params.tiledBase); + } else { + storeElement(params.srcBase + linearOffset - params.tiledBase, p); + } +#else if (RETILE != 0u) { copyElement(params.srcBase + linearOffset - params.linearBase, params.dstBase + linearOffset - params.tiledBase); } else { copyElement(params.srcBase + linearOffset - params.tiledBase, params.dstBase + linearOffset - params.linearBase); } +#endif return; } @@ -193,10 +248,18 @@ void main() { tiledOffset -= params.tiledBase; linearOffset -= params.linearBase; +#ifdef LINEAR_IMAGE + if (RETILE != 0u) { + loadElement(p, params.dstBase + tiledOffset); + } else { + storeElement(params.srcBase + tiledOffset, p); + } +#else if (RETILE != 0u) { copyElement(params.srcBase + linearOffset, params.dstBase + tiledOffset); } else { copyElement(params.srcBase + tiledOffset, params.dstBase + linearOffset); } +#endif } 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 605809a503..1da043f87f 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 { @@ -124,7 +126,8 @@ BdaResources::BdaResources(const Context& context) { static_assert(std::endian::native == std::endian::little); 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, fault->Bytes().size()); + 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,8 @@ 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}; std::memcpy(table->Bytes().data(), &header, sizeof(header)); @@ -240,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. @@ -252,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(); @@ -266,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()); } @@ -273,18 +306,55 @@ 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) + " pages stored to through the BDA table in one use are not implemented"); - bool any = false; - for (std::uint32_t slot = 0; slot < Abi::WrittenPageSlots; ++slot) { - const auto page = words[Abi::WrittenSlotsWord + slot]; - if (page == 0) continue; - GuestMemory::MarkWritten(static_cast(page - 1u) << Abi::WrittenPageShift, std::size_t{1} << Abi::WrittenPageShift); - any = true; + 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 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::MarkWritten(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; + words[Abi::WrittenSlotsWord + 2 * slot + 1] = 0; } - if (any) std::memset(words + Abi::WrittenSlotsWord, 0, Abi::WrittenPageSlots * sizeof(std::uint32_t)); + words[Abi::WrittenCountWord] = 0; } } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp index d466793d3e..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")), 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 = DeviceBudget(); + deviceTier.budget = deviceBudget(context); + memoryProperties = context.memory; + physical = context.physical; + memoryProperties2 = context.memoryProperties2; + imageBudget = ImageBudget(deviceTier.budget); } BufferPool::~BufferPool() { @@ -38,25 +54,316 @@ BufferPool::~BufferPool() { for (const auto& slot : slots) destroy(slot.allocation); } } + for (auto& [memory, block] : slabBlocks) freeBlock(*block); } -VkDeviceSize BufferPool::DeviceBudget() { - static const VkDeviceSize deviceBudget = [] { +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; + 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; + result = allocateMemory(device, &allocation, nullptr, &memory); + ++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 > 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[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); + const auto atomOk = atom <= 1 || (std::has_single_bit(atom) && atom <= capacity); + return alignmentOk && atomOk; +} + +VkDeviceSize BufferPool::SlabBlockBytes(std::size_t capacity) { + return std::max(VkDeviceSize{2} << 20u, static_cast(capacity) * 8u); +} + +std::uint64_t BufferPool::SlabKey(std::uint32_t memoryType, std::size_t capacity, bool addressable) { + return (static_cast(memoryType) << 40u) | (static_cast(addressable ? 1u : 0u) << 39u) | static_cast(capacity); +} + +SlabSlot BufferPool::TakeSlot(const Context& context, std::uint32_t memoryType, std::size_t capacity, bool addressable) { + const auto key = SlabKey(memoryType, capacity, addressable); + const auto takeFrom = [&](SlabBlock& block, Slab& slab) { + const auto index = block.free.back(); + block.free.pop_back(); + if (block.used++ == 0) --slab.emptyBlocks; + if (block.free.empty()) slab.available.erase(std::find(slab.available.begin(), slab.available.end(), &block)); + const auto offset = static_cast(index) * block.slotBytes; + return SlabSlot{block.memory, offset, block.mapping != nullptr ? block.mapping + offset : nullptr}; + }; + { + std::lock_guard lock(slabMutex); + auto& slab = slabs[key]; + if (!slab.available.empty()) return takeFrom(*slab.available.back(), slab); + } + const auto blockBytes = SlabBlockBytes(capacity); + 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}; + const VkMemoryAllocateFlagsInfo flags{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_FLAGS_INFO, nullptr, VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT, 0}; + if (addressable) allocation.pNext = &flags; + allocation.allocationSize = blockBytes; + allocation.memoryTypeIndex = memoryType; + block->memory = AllocateMemory(allocation, "vkAllocateMemory buffer slab"); + 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); + Check(mapped, "vkMapMemory buffer slab"); + block->mapping = static_cast(mapping); + } + const auto slots = static_cast(blockBytes / capacity); + block->free.reserve(slots); + 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; + 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); +} + +void BufferPool::freeBlock(SlabBlock& block) noexcept { + if (block.mapping != nullptr) unmap(device, block.memory); + freeMemory(device, block.memory, nullptr); +} + +VkDeviceSize BufferPool::PutSlot(VkDeviceMemory memory, VkDeviceSize offset) noexcept { + std::unique_ptr released; + { + std::lock_guard lock(slabMutex); + const auto found = slabBlocks.find(memory); + if (found == slabBlocks.end()) return 0; + auto& block = *found->second; + auto& slab = slabs[block.slab]; + if (block.free.empty()) slab.available.push_back(&block); + block.free.push_back(static_cast(offset / block.slotBytes)); + if (--block.used != 0) return 0; + if (slab.emptyBlocks == 0) { + ++slab.emptyBlocks; + return 0; + } + slab.available.erase(std::find(slab.available.begin(), slab.available.end(), &block)); + --slab.blocks; + released = std::move(found->second); + slabBlocks.erase(found); + } + freeBlock(*released); + return SlabBlockBytes(released->slotBytes); +} + +std::size_t BufferPool::SlabBlocks() { + std::lock_guard lock(slabMutex); + return slabBlocks.size(); +} + +bool BufferPool::DeviceTierEnabled() { + static const bool enabled = [] { const char* value = std::getenv("APS5_STAGING_POOL_MIB"); - return (value != nullptr ? std::strtoull(value, nullptr, 10) : 512ull) << 20u; + return value == nullptr || std::strtoull(value, nullptr, 10) != 0; }(); - return deviceBudget; + return enabled; } -void BufferPool::destroy(const BufferAllocation& allocation) noexcept { +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; + 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 || memory.memoryHeaps[heap].size > memory.memoryHeaps[*largest].size)) largest = heap; + } + 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 { + if (allocation.slab) { + destroyBuffer(device, allocation.buffer, nullptr); + return PutSlot(allocation.memory, allocation.offset); + } // 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; } bool BufferPool::DeviceTiered(VkMemoryPropertyFlags properties) { - return (properties & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0 && (properties & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) == 0 && DeviceBudget() != 0; + return (properties & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0 && (properties & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) == 0 && DeviceTierEnabled(); } std::size_t BufferPool::Capacity(std::size_t bytes, VkMemoryPropertyFlags properties) { @@ -77,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; } @@ -91,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); @@ -116,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. @@ -133,6 +465,48 @@ std::size_t BufferPool::MaxSlots() { return slots; } +VkDeviceSize BufferPool::trim() { + Tier retiredSmall; + Tier retiredLarge; + Tier retiredDevice; + { + std::lock_guard lock(mutex); + for (const auto& [tier, retired] : {std::pair{&smallTier, &retiredSmall}, std::pair{&largeTier, &retiredLarge}, std::pair{&deviceTier, &retiredDevice}}) { + tier->free.swap(retired->free); + tier->evictions += tier->slots; + tier->retainedBytes = 0; + tier->slots = 0; + } + imageEvictions += imageSlots; + imageBytes = 0; + imageSlots = 0; + imageOrder.clear(); + } + VkDeviceSize bytes = 0; + for (const auto* tier : {&retiredSmall, &retiredLarge, &retiredDevice}) { + for (const auto& [key, slots] : tier->free) { + for (const auto& slot : slots) bytes += destroy(slot.allocation); + } + } + for (;;) { + std::unique_ptr released; + { + std::lock_guard lock(slabMutex); + const auto found = std::find_if(slabBlocks.begin(), slabBlocks.end(), [](const auto& entry) { return entry.second->used == 0; }); + 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); + } + freeBlock(*released); + bytes += SlabBlockBytes(released->slotBytes); + } + return bytes; +} + void BufferPool::Put(const BufferAllocation& allocation) noexcept { // Evicted allocations are destroyed after the mutex is released (see evictOldest). The vector // may throw on growth; a Put that cannot retain simply destroys, as noexcept requires. @@ -144,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; } @@ -159,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/ColorTargetLayout.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp index 95cbf8470a..6d1f9ec2fe 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp @@ -1,10 +1,13 @@ #include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp" +#include #include #include #include #include #include +#include namespace AgcDriver::Graphics { namespace { @@ -41,14 +44,28 @@ std::uint32_t standard64Extra(std::uint32_t x, std::uint32_t y, std::uint32_t el ColorTileMode DecodeColorTileMode(std::uint32_t attrib3) { const auto resourceType = (attrib3 >> 24u) & 3u; - require((attrib3 & 0x80002000u) == 0 && ((resourceType == 1 && (attrib3 & 0x1fffu) == 0) || resourceType == 2) && ((attrib3 >> 27u) & 7u) == 1, "AGC graphics: unsupported color depth, dimension, resource level or metadata mode"); + require((attrib3 & 0x80002000u) == 0 && ((resourceType == 0 && (attrib3 & 0x1fffu) == 0) || resourceType == 1 || resourceType == 2) && ((attrib3 >> 27u) & 7u) == 1, "AGC graphics: unsupported color depth, dimension, resource level or metadata mode"); const auto mode = (attrib3 >> 14u) & 0x1fu; const auto fmaskMode = (attrib3 >> 19u) & 0x1fu; - require(fmaskMode == 0 || fmaskMode == 0x18, "AGC graphics: unsupported color FMASK swizzle mode"); - require(mode == 0 || mode == 5 || mode == 9 || mode == 0x1b, "AGC graphics: unsupported color tile mode"); + require(mode == 0 || mode == 5 || mode == 9 || mode == 0x15 || mode == 0x16 || mode == 0x18 || mode == 0x1a || mode == 0x1b, "AGC graphics: unsupported color tile mode"); + require(fmaskMode == 0 || fmaskMode == 0x18 || fmaskMode == (mode & ~3u), "AGC graphics: unsupported color FMASK swizzle mode"); + require(resourceType != 0 || mode == 0 || mode == 0x1b, "AGC graphics: 1D color targets with a standard swizzle mode are invalid"); return static_cast(mode); } +TileMipLayout ComputeColorTargetMipLayout(ColorTileMode mode, std::uint32_t bytesPerElement, std::uint32_t surfaceWidth, std::uint32_t surfaceHeight, std::uint32_t mipCount, std::uint32_t mipLevel) { + auto mips = ComputeElementMipLayout(ColorTextureTileMode(mode), bytesPerElement, surfaceWidth, surfaceHeight, mipCount); + require(mipLevel < mips.size(), "AGC graphics: color target mip is outside its surface"); + auto mip = mips[mipLevel]; + const ColorTargetLayout target(mip.width, mip.height, mode, bytesPerElement); + require(mip.tiledSize == target.Bytes(), "AGC graphics: color target mip tiling differs from its surface layout"); + mip.tiledOffset = 0; + mip.linearOffset = 0; + mip.pitchBytes = mip.width * bytesPerElement; + mip.linearSize = target.LinearBytes(); + return mip; +} + namespace { struct SwizzleTables { @@ -56,14 +73,15 @@ struct SwizzleTables { std::vector y; }; -const SwizzleTables& renderTargetTables(std::uint32_t bytesPerElement, std::uint32_t blockWidth, std::uint32_t blockHeight) { - static std::once_flag once[5]; - static SwizzleTables tables[5]; +const SwizzleTables& equationTables(ColorTileMode mode, std::uint32_t bytesPerElement, std::uint32_t blockWidth, std::uint32_t blockHeight) { + static std::once_flag once[5][5]; + static SwizzleTables tables[5][5]; + const auto modeIndex = mode == ColorTileMode::S4KBX ? 0u : mode == ColorTileMode::D4KBX ? 1u : mode == ColorTileMode::D64KBX ? 2u : mode == ColorTileMode::Z64KBX ? 4u : 3u; const auto index = static_cast(std::countr_zero(bytesPerElement)); - std::call_once(once[index], [&] { - const auto* equation = FindTextureSwizzleEquation(27u, bytesPerElement); - require(equation != nullptr, "AGC graphics: no SW_64KB_R_X equation for the color element size"); - auto& table = tables[index]; + std::call_once(once[modeIndex][index], [&] { + const auto* equation = FindTextureSwizzleEquation(static_cast(mode), bytesPerElement); + require(equation != nullptr, "AGC graphics: no XOR swizzle equation for the color tile mode and element size"); + auto& table = tables[modeIndex][index]; table.x.resize(blockWidth); table.y.resize(blockHeight); for (std::uint32_t x = 0; x < blockWidth; ++x) { @@ -77,7 +95,7 @@ const SwizzleTables& renderTargetTables(std::uint32_t bytesPerElement, std::uint table.y[y] = offset; } }); - return tables[index]; + return tables[modeIndex][index]; } const SwizzleTables& standardTables(std::uint32_t bytesPerElement, std::uint32_t blockWidth, std::uint32_t blockHeight, bool block64KB) { @@ -96,9 +114,11 @@ const SwizzleTables& standardTables(std::uint32_t bytesPerElement, std::uint32_t } -ColorTargetLayout::ColorTargetLayout(std::uint32_t width, std::uint32_t height, ColorTileMode mode, std::uint32_t bytesPerElement) : width(width), height(height), pitch(width), mode(mode), bytes(0), elementBytes(bytesPerElement) { +ColorTargetLayout::ColorTargetLayout(std::uint32_t width, std::uint32_t height, ColorTileMode mode, std::uint32_t bytesPerElement, std::uint32_t pipeBankXor) : width(width), height(height), pitch(width), mode(mode), bytes(0), elementBytes(bytesPerElement), pipeBankXor(pipeBankXor) { require(width != 0 && height != 0 && width <= 16384 && height <= 16384, "AGC graphics: invalid color surface extent"); require(std::has_single_bit(bytesPerElement) && bytesPerElement <= 16u, "AGC graphics: unsupported color element size"); + require(pipeBankXor == 0 || (ColorTileModeIsXor(mode) && pipeBankXor < ColorTileModeBlockBytes(mode) && pipeBankXor % 256u == 0), "AGC graphics: a color pipe/bank XOR applies only to whole 256-byte units of XOR swizzle blocks"); + require(mode != ColorTileMode::D64KBX || (pipeBankXor & ~0xcf00u) == 0, "AGC graphics: unsupported display color pipe/bank XOR column bits"); std::uint32_t paddedHeight = height; switch (mode) { case ColorTileMode::Linear: { @@ -106,16 +126,20 @@ ColorTargetLayout::ColorTargetLayout(std::uint32_t width, std::uint32_t height, pitch = (width + pitchAlignment - 1u) / pitchAlignment * pitchAlignment; break; } + case ColorTileMode::S4KBX: + case ColorTileMode::D4KBX: + case ColorTileMode::Z64KBX: + case ColorTileMode::D64KBX: case ColorTileMode::RenderTarget: { - // SW_64KB_R_X: 64 KiB blocks of 2^(16 - log2(bpe)) elements, wider than tall for odd powers. - const auto log2Elements = 16u - static_cast(std::countr_zero(bytesPerElement)); + const auto log2Elements = static_cast(std::countr_zero(ColorTileModeBlockBytes(mode))) - static_cast(std::countr_zero(bytesPerElement)); blockWidth = 1u << ((log2Elements + 1u) / 2u); blockHeight = 1u << (log2Elements / 2u); pitch = (width + blockWidth - 1u) / blockWidth * blockWidth; paddedHeight = (height + blockHeight - 1u) / blockHeight * blockHeight; - const auto& tables = renderTargetTables(bytesPerElement, blockWidth, blockHeight); + const auto& tables = equationTables(mode, bytesPerElement, blockWidth, blockHeight); xOffsets = tables.x.data(); yOffsets = tables.y.data(); + if (mode == ColorTileMode::RenderTarget || mode == ColorTileMode::Z64KBX) equation = FindTextureSwizzleEquation(static_cast(mode), bytesPerElement); break; } case ColorTileMode::Standard4KB: @@ -142,7 +166,16 @@ ColorTargetLayout::ColorTargetLayout(std::uint32_t width, std::uint32_t height, std::size_t ColorTargetLayout::offset(std::uint32_t x, std::uint32_t y) const { if (mode == ColorTileMode::Linear) return (static_cast(y) * pitch + x) * elementBytes; const auto block = static_cast(y / blockHeight) * (pitch / blockWidth) + x / blockWidth; - return block * Alignment() + (xOffsets[x % blockWidth] ^ yOffsets[y % blockHeight]); + auto inBlock = xOffsets[x % blockWidth] ^ yOffsets[y % blockHeight]; + if (equation != nullptr) { + const auto highX = (x & ~(blockWidth - 1u)) & 0xfffu; + const auto highY = ((y & ~(blockHeight - 1u)) << 12u) & 0xfff000u; + for (std::uint32_t bit = 0; bit < 16u; ++bit) { + const auto coordinates = (highX & equation->bits[bit]) ^ (highY & equation->bits[bit]); + inBlock ^= (parity(coordinates) << bit); + } + } + return block * Alignment() + (inBlock ^ pipeBankXor); } std::size_t ColorTargetLayout::Offset(std::uint32_t x, std::uint32_t y) const { @@ -166,4 +199,23 @@ void ColorTargetLayout::Tile(std::span source, std::span(blocksPerRow) * ((height + 511u) / 512u) * Alignment; +} + +std::size_t CmaskLayout::Nibble(std::uint32_t tileX, std::uint32_t tileY) const { + require(tileX < TilesX() && tileY < TilesY(), "AGC graphics: CMASK tile out of range"); + static constexpr std::array, 13> equation{{ + {0x008u, 0x000u}, {0x000u, 0x010u}, {0x040u, 0x000u}, {0x000u, 0x040u}, {0x080u, 0x000u}, {0x000u, 0x080u}, {0x100u, 0x000u}, + {0x000u, 0x100u}, {0x200u, 0x000u}, {0x008u, 0x008u}, {0x010u, 0x010u}, {0x040u, 0x020u}, {0x020u, 0x040u}, + }}; + const auto x = tileX * 8u; + const auto y = tileY * 8u; + std::size_t offset = 0; + for (std::size_t bit = 0; bit < equation.size(); ++bit) offset |= static_cast(parity((x & equation[bit].first) ^ (y & equation[bit].second))) << bit; + const auto block = static_cast(y / 512u) * blocksPerRow + x / 1024u; + return block * Alignment * 2u + offset; +} + } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetTransfer.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetTransfer.cpp index 36690adc43..7639649679 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetTransfer.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetTransfer.cpp @@ -7,7 +7,7 @@ namespace AgcDriver::Graphics { void ReadColorTarget(const ColorTarget& target, std::span destination) { PerformanceTimer timing("ColorTarget.Read"); - const ColorTargetLayout layout(target.extent.width, target.extent.height, target.tileMode, target.elementBytes); + const ColorTargetLayout layout(target.extent.width, target.extent.height, target.tileMode, target.elementBytes, target.pipeBankXor); Require(target.bytes == layout.Bytes() && destination.size() == layout.LinearBytes(), "color target transfer size mismatch"); if (target.tileMode == ColorTileMode::Linear && layout.Bytes() == layout.LinearBytes()) { GuestMemory::Read(target.address, destination, layout.Alignment()); @@ -23,7 +23,7 @@ void ReadColorTarget(const ColorTarget& target, std::span destination void WriteColorTarget(const ColorTarget& target, std::span source) { PerformanceTimer timing("ColorTarget.Write"); - const ColorTargetLayout layout(target.extent.width, target.extent.height, target.tileMode, target.elementBytes); + const ColorTargetLayout layout(target.extent.width, target.extent.height, target.tileMode, target.elementBytes, target.pipeBankXor); Require(target.bytes == layout.Bytes() && source.size() == layout.LinearBytes(), "color target transfer size mismatch"); if (target.tileMode == ColorTileMode::Linear && layout.Bytes() == layout.LinearBytes()) { GuestMemory::Write(target.address, source, layout.Alignment()); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp index 5e4729b1ff..eee078cba1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp @@ -51,11 +51,24 @@ DccKeys ByteKeys(std::uint8_t key) { case 0x80: return DccKeys::Clear1110; case 0xc0: return DccKeys::Clear1111; case 0x20: return DccKeys::ClearRegister; + case 0x10: return DccKeys::ClearSingle; case 0xff: return DccKeys::Uncompressed; default: return DccKeys::Mixed; } } +bool SingleKeysReadable(const GuestTextureResource& resource) { + return KeyBytes % BytesPerElement(resource.format) == 0; +} + +void ExpandSingleBlocks(std::span bytes, std::size_t elementBytes) { + for (std::size_t block = 0; block < bytes.size(); block += KeyBytes) { + const auto blockBytes = std::min(KeyBytes, bytes.size() - block); + auto* first = bytes.data() + block; + for (auto filled = std::min(elementBytes, blockBytes); filled < blockBytes; filled *= 2) std::memcpy(first + filled, first, std::min(filled, blockBytes - filled)); + } +} + // APS5_PROFILE_DRAW: key scans, their bytes and time, printed as [dcc] every 10 s, with the hits of // the GPU-store memo below and the "uncompressed" key stores by where they were made. Flush syncs // stay 0 (no key read syncs the recorder); the field is kept so the line's shape does not change. @@ -82,6 +95,8 @@ struct ProofCounters { std::atomic proved{0}; std::atomic scanned{0}; std::atomic unstable{0}; + std::atomic rangeProved{0}; + std::atomic rangeScanned{0}; }; ProofCounters& Proofs() { @@ -219,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); @@ -278,6 +293,48 @@ void NoteKeysFillOnGpu(std::uint64_t begin, std::size_t count, DccKeys keys) { namespace { +struct LandedKeys { + int value = -1; + bool mixed = false; + bool unknown = false; +}; + +LandedKeys KeysAfterPendingWrites(const Recorder& recorder, std::uint64_t begin, std::size_t count) { + auto writes = recorder.PendingWritesOver(begin, count); + std::sort(writes.begin(), writes.end(), [](const Recorder::PendingWrite& a, const Recorder::PendingWrite& b) { return a.note > b.note; }); + std::vector> unwritten{{begin, begin + count}}; + LandedKeys landed; + const auto see = [&](int key) { + if (landed.value < 0) landed.value = key; + else if (landed.value != key) landed.mixed = true; + }; + for (const auto& write : writes) { + if (landed.mixed || unwritten.empty()) break; + std::vector> rest; + bool covered = false; + for (const auto& [from, to] : unwritten) { + if (write.end <= from || to <= write.begin) { + rest.emplace_back(from, to); + continue; + } + covered = true; + if (from < write.begin) rest.emplace_back(from, write.begin); + if (write.end < to) rest.emplace_back(write.end, to); + } + unwritten = std::move(rest); + if (!covered) continue; + if (write.value >= 0) see(write.value); + else landed.unknown = true; + } + for (const auto& [from, to] : unwritten) { + if (landed.mixed) break; + const auto* keys = reinterpret_cast(from); + if (AllKeysEqual(keys + 1, static_cast(to - from) - 1, keys[0])) see(keys[0]); + else landed.mixed = true; + } + return landed; +} + std::optional PendingStoreKeys(Recorder& recorder, std::uint64_t begin, std::size_t count) { const auto end = begin + count; std::uint64_t note = 0; @@ -293,8 +350,16 @@ std::optional PendingStoreKeys(Recorder& recorder, std::uint64_t begin, break; } } - if (note == 0 || AnyShadowedOverlaps(begin, count) || recorder.NewestWriteNote(begin, count) != note) return std::nullopt; - return keys; + if (AnyShadowedOverlaps(begin, count)) { + PublishShadow(begin, count, PublishScope::Whole, PublishReason::Keys); + if (AnyShadowedOverlaps(begin, count)) return std::nullopt; + } + if (note != 0 && recorder.NewestWriteNote(begin, count) == note) return keys; + if (!GuestMemory::Accessible(reinterpret_cast(begin), count)) return std::nullopt; + const auto landed = KeysAfterPendingWrites(recorder, begin, count); + if (landed.mixed) return DccKeys::Mixed; + if (landed.unknown || landed.value < 0) return std::nullopt; + return ByteKeys(static_cast(landed.value)); } // Whether the keys already read as uncompressed: scans the same bytes as ReadDccKeys, so it counts @@ -341,6 +406,7 @@ bool LayoutFor(VkFormat format, Layout& layout) { case VK_FORMAT_R16G16_UINT: layout = {2, {16, 16}, Kind::Uint}; return true; case VK_FORMAT_R16G16_SINT: layout = {2, {16, 16}, Kind::Sint}; return true; case VK_FORMAT_R16G16_SFLOAT: layout = {2, {16, 16}, Kind::Float}; return true; + case VK_FORMAT_B10G11R11_UFLOAT_PACK32: layout = {3, {11, 11, 10}, Kind::Float}; return true; case VK_FORMAT_A2B10G10R10_UNORM_PACK32: case VK_FORMAT_A2R10G10B10_UNORM_PACK32: layout = {4, {10, 10, 10, 2}, Kind::Unorm}; return true; case VK_FORMAT_A2B10G10R10_UINT_PACK32: layout = {4, {10, 10, 10, 2}, Kind::Uint}; return true; case VK_FORMAT_R8G8B8A8_UNORM: case VK_FORMAT_R8G8B8A8_SRGB: case VK_FORMAT_B8G8R8A8_UNORM: case VK_FORMAT_B8G8R8A8_SRGB: layout = {4, {8, 8, 8, 8}, Kind::Unorm}; return true; @@ -367,7 +433,14 @@ std::uint64_t One(Kind kind, std::uint32_t bits) { switch (kind) { case Kind::Unorm: case Kind::Uint: return bits >= 64 ? ~0ull : (1ull << bits) - 1u; case Kind::Snorm: case Kind::Sint: return (1ull << (bits - 1u)) - 1u; - case Kind::Float: return bits == 16 ? 0x3c00u : 0x3f800000u; + case Kind::Float: + switch (bits) { + case 10: return 0x1e0u; + case 11: return 0x3c0u; + case 16: return 0x3c00u; + case 32: return 0x3f800000u; + default: throw std::runtime_error("AGC graphics: DCC clear of a " + std::to_string(bits) + "-bit float channel is not implemented"); + } } return 0; } @@ -382,6 +455,7 @@ const char* DccKeysName(DccKeys keys) { case DccKeys::Clear1110: return "1110"; case DccKeys::Clear1111: return "1111"; case DccKeys::ClearRegister: return "register"; + case DccKeys::ClearSingle: return "single"; case DccKeys::Mixed: return "mixed"; case DccKeys::Unreadable: return "unreadable"; } @@ -428,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); @@ -441,11 +515,15 @@ DccKeys readDccKeys(std::uint64_t metaAddress, std::size_t count, bool& memoized return uniform ? ByteKeys(first) : DccKeys::Mixed; } +DccKeys currentDccKeys(std::uint64_t metaAddress, std::size_t count, bool& memoized); + DccKeys textureClearKeys(const GuestTextureResource& resource, std::uint64_t guestBytes, bool& memoized) { memoized = false; if (resource.dccAddress == 0) return DccKeys::Uncompressed; - const auto keys = readDccKeys(resource.dccAddress, DccKeyBytes(guestBytes), memoized); - if (keys == DccKeys::Uncompressed) return keys; + DccKeys keys; + if (const auto known = WaitForKeyWriters(resource, guestBytes)) keys = *known; + else keys = currentDccKeys(resource.dccAddress, DccKeyBytes(guestBytes), memoized); + if (keys == DccKeys::Uncompressed || (keys == DccKeys::ClearSingle && SingleKeysReadable(resource))) return keys; if (IsConvertedTextureFormat(resource.format) && (keys == DccKeys::Clear0001 || keys == DccKeys::Clear1110)) throw std::runtime_error(std::string("AGC graphics: DCC clear code ") + DccKeysName(keys) + " of converted texture format " + std::to_string(resource.format) + " is not implemented"); std::byte probe[16]{}; if (!IsDccClear(keys) || !FillDccClear(ResolveTextureFormat(resource.format), keys, resource.dccAlphaOnMsb, std::span(probe, std::min(sizeof(probe), BytesPerElement(resource.format))))) { @@ -458,15 +536,18 @@ DccKeys textureClearKeys(const GuestTextureResource& resource, std::uint64_t gue return keys; } -DccKeys currentDccKeys(std::uint64_t metaAddress, std::size_t count) { +DccKeys currentDccKeys(std::uint64_t metaAddress, std::size_t count, bool& memoized) { + memoized = false; if (metaAddress != 0 && count != 0 && GuestMemory::GpuMutex().HeldByThisThread()) { if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->PendingWriteOverlaps(metaAddress, count)) { - if (const auto keys = PendingStoreKeys(*recorder, metaAddress, count)) return *keys; + if (const auto keys = PendingStoreKeys(*recorder, metaAddress, count)) { + memoized = true; + return *keys; + } Recorder::CountSync(2); recorder->SyncThrough(metaAddress, count); } } - bool memoized = false; return readDccKeys(metaAddress, count, memoized); } @@ -489,18 +570,63 @@ DccKeys ReadDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes) { } DccKeys CurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes) { - return currentDccKeys(metaAddress, DccKeyBytes(surfaceBytes)); + bool memoized = false; + return currentDccKeys(metaAddress, DccKeyBytes(surfaceBytes), memoized); } DccKeys CurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes, std::size_t keyCount) { Require(keyCount >= DccKeyBytes(surfaceBytes), "a DCC key range covers fewer keys than one per 256 surface bytes"); - return currentDccKeys(metaAddress, keyCount); + bool memoized = false; + 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)); } @@ -618,19 +744,44 @@ DccKeys ProvedClearKeys(const GuestTextureResource& resource, std::uint64_t gues DccKeyProofCounts KeyProofCounts() { const auto& counters = Proofs(); - return {counters.proved.load(std::memory_order_relaxed), counters.scanned.load(std::memory_order_relaxed), counters.unstable.load(std::memory_order_relaxed)}; + return {counters.proved.load(std::memory_order_relaxed), counters.scanned.load(std::memory_order_relaxed), counters.unstable.load(std::memory_order_relaxed), counters.rangeProved.load(std::memory_order_relaxed), counters.rangeScanned.load(std::memory_order_relaxed)}; +} + +DccKeys ProvedCurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes, DccRangeProof& proof) { + const auto count = DccKeyBytes(surfaceBytes); + if (metaAddress == 0 || count == 0 || !KeyFastPath() || !GuestMemory::GpuMutex().HeldByThisThread()) return CurrentDccKeys(metaAddress, surfaceBytes); + auto& counters = Proofs(); + const auto collected = GuestMemory::CollectWrites(metaAddress, count); + if (proof.generation != 0 && proof.address == metaAddress && proof.count == count && collected != 0 && GuestMemory::UnchangedSince(metaAddress, count, proof.generation)) { + counters.rangeProved.fetch_add(1, std::memory_order_relaxed); + return proof.keys; + } + bool memoized = false; + const auto keys = currentDccKeys(metaAddress, count, memoized); + bool stable = collected != 0 && !memoized; + if (stable) { + if (auto* recorder = Recorder::Active(); recorder != nullptr) { + const auto info = recorder->DescribePendingWrite(metaAddress, count); + stable = !info.has_value() || info->signaled; + } + } + proof = stable ? DccRangeProof{metaAddress, count, keys, collected} : DccRangeProof{}; + counters.rangeScanned.fetch_add(1, std::memory_order_relaxed); + return keys; } void ReadTextureSurface(const GuestTextureResource& resource, DccKeys keys, std::span bytes) { // Named for the [hooksync] attribution: the read goes through the flush hook. const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::TextureRead); - if (keys == DccKeys::Uncompressed) GuestMemory::ReadCommitted(resource.baseAddress, bytes); + if (keys == DccKeys::Uncompressed || keys == DccKeys::ClearSingle) GuestMemory::ReadCommitted(resource.baseAddress, bytes); else FillDccClear(ResolveTextureFormat(resource.format), keys, resource.dccAlphaOnMsb, bytes); + if (keys == DccKeys::ClearSingle) ExpandSingleBlocks(bytes, BytesPerElement(resource.format)); } namespace { DccKeys EncodedKeys(const GuestTextureResource& resource, DccKeys keys) { + if (keys == DccKeys::ClearSingle && SingleKeysReadable(resource)) return keys; std::byte probe[16]{}; if (!IsDccClear(keys) || !FillDccClear(ResolveTextureFormat(resource.format), keys, resource.dccAlphaOnMsb, std::span(probe, std::min(sizeof(probe), BytesPerElement(resource.format))))) return DccKeys::Uncompressed; return keys; @@ -639,49 +790,68 @@ DccKeys EncodedKeys(const GuestTextureResource& resource, DccKeys keys) { bool PendingFillsLeaveUncompressed(const Recorder& recorder, const GuestTextureResource& resource, std::size_t count) { const auto begin = resource.dccAddress; if (!GuestMemory::Accessible(reinterpret_cast(begin), count) || AnyShadowedOverlaps(begin, count)) return false; - auto writes = recorder.PendingWritesOver(begin, count); - std::sort(writes.begin(), writes.end(), [](const Recorder::PendingWrite& a, const Recorder::PendingWrite& b) { return a.note > b.note; }); - std::vector> unwritten{{begin, begin + count}}; - int value = -1; - bool mixed = false; - const auto see = [&](int key) { - if (value < 0) value = key; - else if (value != key) mixed = true; - }; - for (const auto& write : writes) { - if (mixed || unwritten.empty()) break; - std::vector> rest; - bool covered = false; - for (const auto& [from, to] : unwritten) { - if (write.end <= from || to <= write.begin) { - rest.emplace_back(from, to); - continue; - } - covered = true; - if (from < write.begin) rest.emplace_back(from, write.begin); - if (write.end < to) rest.emplace_back(write.end, to); - } - unwritten = std::move(rest); - if (covered && write.value >= 0) see(write.value); - } - for (const auto& [from, to] : unwritten) { - if (mixed) break; - const auto* keys = reinterpret_cast(from); - if (AllKeysEqual(keys + 1, static_cast(to - from) - 1, keys[0])) see(keys[0]); - else mixed = true; - } - return mixed || (value >= 0 && EncodedKeys(resource, ByteKeys(static_cast(value))) == DccKeys::Uncompressed); + const auto landed = KeysAfterPendingWrites(recorder, begin, count); + return landed.mixed || (landed.value >= 0 && EncodedKeys(resource, ByteKeys(static_cast(landed.value))) == DccKeys::Uncompressed); } } +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 d1cc7c90ab..3c31c3ab57 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -1,41 +1,274 @@ #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 "RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp" #include #include +#include +#include +#include +#include +#include #include #include #include #include +#include +#include +#include #include +#include #include 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_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; +} + +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) { + 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, target.format, &properties); - Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) != 0, "depth/stencil format " + std::to_string(target.format) + " cannot be an attachment on this device"); - Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0, "depth/stencil format " + std::to_string(target.format) + " cannot be sampled on this device"); + context.formatProperties(context.physical, hostFormat, &properties); + Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) != 0, "depth/stencil format " + std::to_string(hostFormat) + " cannot be an attachment on this device"); + Require(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 { VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + if (SampleLocationsCompatibleDepth(context, hostFormat, target.samples)) info.flags = VK_IMAGE_CREATE_SAMPLE_LOCATIONS_COMPATIBLE_DEPTH_BIT_EXT; info.imageType = VK_IMAGE_TYPE_2D; - info.format = target.format; + info.format = hostFormat; info.extent = {target.extent.width, target.extent.height, 1}; info.mipLevels = 1; info.arrayLayers = 1; - info.samples = VK_SAMPLE_COUNT_1_BIT; + info.samples = static_cast(target.samples); info.tiling = VK_IMAGE_TILING_OPTIMAL; - info.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + 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"); @@ -44,15 +277,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 = target.format; + viewInfo.format = hostFormat; viewInfo.subresourceRange = {aspects, 0, 1, 0, 1}; Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView depth"); - auto* recorder = Recorder::Active(); + 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(); @@ -76,61 +312,965 @@ public: throw; } } + ~DepthSurface() { release(); } DepthSurface(const DepthSurface&) = delete; DepthSurface& operator=(const DepthSurface&) = delete; - std::shared_ptr Sampled(std::span words, const GuestTextureResource& resource, VkComponentMapping components) { - std::array key{}; + 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); + 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; + if (recorder == nullptr) batch = std::make_unique(context); + const auto commands = recorder != nullptr ? recorder->Commands() : batch->Handle(); + 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{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); + recorder->Keep(owned); + } + } + + 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; + 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); - if ((format != expected && !depthBits) || resource.dimension != TextureDimension::k2D || resource.width != target.extent.width || resource.height != target.extent.height || resource.baseLevel != 0 || resource.lastLevel != 0 || resource.baseArray != 0) { + 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); } - auto texture = std::make_shared(context, image, target.format, stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT, components); + 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) { + ForgetClear(aspect); + auto* recorder = RecorderFor(context); + std::unique_ptr batch; + if (recorder == nullptr) batch = std::make_unique(context); + const auto commands = recorder != nullptr ? recorder->Commands() : batch->Handle(); + constexpr VkAccessFlags all = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, all, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + VkImageCopy region{}; + region.srcSubresource = {static_cast(aspect), 0, 0, 1}; + region.dstSubresource = region.srcSubresource; + region.extent = {target.extent.width, target.extent.height, 1}; + context.Function("vkCmdCopyImage")(commands, source.image, VK_IMAGE_LAYOUT_GENERAL, image, VK_IMAGE_LAYOUT_GENERAL, 1, ®ion); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, all); + if (batch) batch->SubmitAndWait(); + else Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); + } + + bool Holds(const Texture* texture) const { + 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; }); + } + + std::uint64_t written = ++depthWrites; + 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; + 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); + return ResolveTextureFormat(resource.format) == expected || depthBits; + } + + 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) { + char text[224]; + std::snprintf(text, sizeof(text), "AGC graphics: storage image access to the depth plane of depth surface 0x%llx (vk format %d) stored as vk format %d is not implemented", static_cast(target.address), static_cast(target.format), static_cast(hostFormat)); + throw std::runtime_error(text); + } + } + + std::shared_ptr Storage(std::span words, const GuestTextureResource& resource, std::uint32_t mip) { + if (!StorageViewMatches(words, resource)) return nullptr; + const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress; + const bool oneLayer = resource.dimension == TextureDimension::k2DArray && resource.baseArray == 0 && resource.depthOrLastArray == 0; + if (resource.dimension != TextureDimension::k2D && !oneLayer) { + char text[256]; + std::snprintf(text, sizeof(text), "AGC graphics: storage image access to depth/stencil surface 0x%llx is not implemented (storage=%ux%u guest format=%u mip=%u dimension=%d tile=%d)", static_cast(resource.baseAddress), resource.width, resource.height, resource.format, mip, static_cast(resource.dimension), static_cast(resource.tileMode)); + throw std::runtime_error(text); + } + RequireStorable(resource); + 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) { + std::erase_if(plane.replaced, [](const auto& old) { return old.expired(); }); + if (plane.storage != nullptr) plane.replaced.push_back(plane.storage); + plane.storage = StorageTexture::Create(context, *context.detiler, resource, 0); + } + 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) { + if (stencil || !StoreOverwrite(candidate)) copyPlane(plane, plane.storage, stencil, false); + written = ++depthWrites; + return true; + } + if (std::none_of(plane.replaced.begin(), plane.replaced.end(), [&](const auto& old) { return old.lock().get() == &candidate; })) continue; + char text[288]; + std::snprintf(text, sizeof(text), "AGC graphics: storage image writes to the %s plane of depth surface 0x%llx through a view that another view of the plane (guest format %u, tile mode %d) replaced are not implemented", stencil ? "stencil" : "depth", static_cast(target.address), plane.storage->Descriptor().format, static_cast(plane.storage->Descriptor().tileMode)); + throw std::runtime_error(text); + } + return false; + } + + 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; + 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 StoreDepthPlane(const GuestTextureResource& resource) { + ApplyFastClear(); + if (mirrorPlane.storage == nullptr) { + mirrorPlane.storage = StorageTexture::Create(context, *context.detiler, resource, 0); + ++mirrorEpoch; + } + copyPlane(mirrorPlane, mirrorPlane.storage, false, true, true); + return mirrorPlane.storage; + } + + bool SyncFromMemory(const GuestTextureResource& resource) { + const auto bytes = static_cast(DescribeSurface(resource).guestBytes); + GuestMemory::CollectWrites(resource.baseAddress, bytes); + if (syncedGeneration == 0 || (pendingClear & VK_IMAGE_ASPECT_DEPTH_BIT) != 0 || !GuestMemory::WrittenSince(resource.baseAddress, bytes, syncedGeneration)) { + syncedGeneration = GuestMemory::TrackerGeneration(); + return false; + } + memoryStale = false; + if (mirrorPlane.storage == nullptr) { + mirrorPlane.storage = StorageTexture::Create(context, *context.detiler, resource, 0); + ++mirrorEpoch; + } + else static_cast(mirrorPlane.storage->Refresh()); + syncedGeneration = GuestMemory::TrackerGeneration(); + copyPlane(mirrorPlane, mirrorPlane.storage, false, false, true); + written = ++depthWrites; + return true; + } + + void DropMirror() { + mirrorPlane = {}; + } + + bool memoryStale = false; + std::uint64_t scannedEpoch = 0; + std::uint64_t syncedGeneration = 0; + const Context context; const DepthTarget target; + const VkFormat hostFormat; VkImage image = VK_NULL_HANDLE; VkImageView view = VK_NULL_HANDLE; VkDeviceMemory memory = VK_NULL_HANDLE; + std::shared_ptr> active = std::make_shared>(true); + bool sampleable = true; + bool samplingActive = false; + DepthSamplingState sampling; + std::uint64_t clearGeneration = 0u; + std::shared_ptr owned; + std::shared_ptr sampleShadow; + bool useSampleShadow = false; + std::uint64_t sampleShadowWritten = 0u; private: - std::map, std::shared_ptr> textures; + struct PlaneStorage { + std::shared_ptr storage; + std::vector> replaced; + std::unique_ptr staging; + }; + struct StencilCopy { + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + std::unique_ptr staging; + std::map, std::shared_ptr> textures; + std::uint64_t written = 0; + bool filled = false; + }; + std::map, std::shared_ptr> textures; + PlaneStorage depthPlane; + PlaneStorage stencilPlane; + std::uint64_t depthMemory = 0; + std::uint64_t stencilMemory = 0; + StencilCopy stencilCopy; + + 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; + 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) { + 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 auto bytes = static_cast(target.extent.width) * target.extent.height * (stencil ? 1u : d16 ? 2u : 4u); + if (plane.staging == nullptr) plane.staging = std::make_unique(context, bytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); + const auto commands = recorder != nullptr ? recorder->Commands() : batch->Handle(); + if (recorder != nullptr) recorder->Keep(storage); + const std::uint32_t timing = recorder != nullptr ? recorder->BeginGpuTiming(Recorder::CommandClass::Copy) : 0u; + 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 : 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); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function("vkCmdCopyBufferToImage")(commands, plane.staging->Handle(), destinationImage, VK_IMAGE_LAYOUT_GENERAL, 1, toStorage ? &colorRegion : &planeRegion); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT); + if (batch) { + batch->SubmitAndWait(); + return; + } + Recorder::CountBarriers(Recorder::CommandClass::Copy, 3); + recorder->EndGpuTiming(timing, bytes); + } void release() noexcept { + depthPlane = {}; + stencilPlane = {}; + releaseStencilCopy(); + mirrorPlane = {}; textures.clear(); 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; } }; -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; +struct DepthPlaneSlice { + VkImage image = VK_NULL_HANDLE; + std::uint64_t written = 0; +}; + +class DepthPlaneCopy { +public: + DepthPlaneCopy(const Context& context, VkExtent2D extent, VkFormat format, std::uint32_t layers) : context(context), extent(extent), format(format), layers(layers), guestLayers(layers), surfaceLayers(layers) { + this->context.bufferPool.reset(); + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.flags = layers % 6u == 0 && extent.width == extent.height ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u; + info.imageType = VK_IMAGE_TYPE_2D; + info.format = format; + info.extent = {extent.width, extent.height, 1}; + info.mipLevels = 1; + info.arrayLayers = layers; + info.samples = VK_SAMPLE_COUNT_1_BIT; + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + try { + Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage depth plane copy"); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory depth plane copy"); + Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory depth plane copy"); + 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; + } + } + ~DepthPlaneCopy() { release(); } + DepthPlaneCopy(const DepthPlaneCopy&) = delete; + DepthPlaneCopy& operator=(const DepthPlaneCopy&) = delete; + + std::shared_ptr Refresh(std::span slices, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType viewType, std::uint32_t baseLayer) { + Require(slices.size() == layers, "depth plane copy slices do not match its layers"); + const auto geometry = DescribeSurface(resource); + 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; + for (std::uint32_t layer = 0; layer < layers; ++layer) { + auto& held = guestLayers[layer]; + auto& surface = surfaceLayers[layer]; + if (slices[layer].image != VK_NULL_HANDLE) { + held = {}; + if (slices[layer].written != 0 && surface.image == slices[layer].image && surface.written == slices[layer].written) continue; + surface = slices[layer]; + copiedLayers.push_back(layer); + continue; + } + surface = {}; + const GuestLayer current{resource.baseAddress + geometry.GuestLayerOffset(layer), geometry.layerBytes, resource.tileMode, geometry.thick, GuestMemory::CollectWrites(resource.baseAddress + geometry.GuestLayerOffset(layer), static_cast(geometry.layerBytes))}; + if (held.generation != 0 && held.address == current.address && held.bytes == current.bytes && held.tileMode == current.tileMode && held.thick == current.thick && !Recorder::SnapshotWriteOverlaps(current.address, static_cast(current.bytes)) && GuestMemory::UnchangedSince(current.address, static_cast(current.bytes), held.generation)) { + held.generation = current.generation; + continue; + } + auto upload = std::make_shared(context, static_cast(geometry.layerBytes), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + GuestMemory::ReadCommitted(current.address, upload->Bytes().first(static_cast(geometry.layerBytes))); + uploads.push_back(std::move(upload)); + uploadLayers.push_back(layer); + held = current; + } + if (!copiedLayers.empty() || !uploads.empty()) record(slices, resource, geometry, copiedLayers, uploads, uploadLayers); + const std::array key{static_cast(components.r), static_cast(components.g), static_cast(components.b), static_cast(components.a), static_cast(viewType), baseLayer}; + auto& texture = textures[key]; + if (texture == nullptr) { + texture = std::make_shared(context, image, format, aspect(), components, viewType, baseLayer); + texture->MarkRefreshedPerUse(); + } + return texture; + } + + const Context context; + const VkExtent2D extent; + const VkFormat format; + const std::uint32_t layers; + +private: + struct GuestLayer { + std::uint64_t address = 0; + std::uint64_t bytes = 0; + TextureTileMode tileMode = TextureTileMode::kLinear; + bool thick = false; + std::uint64_t generation = 0; + }; + + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + std::unique_ptr staging; + std::size_t stagingSize = 0; + std::vector> retiredStaging; + std::map, std::shared_ptr> textures; + std::vector guestLayers; + std::vector surfaceLayers; + bool filled = false; + + void record(std::span slices, const GuestTextureResource& resource, const SurfaceGeometry& geometry, std::span copiedLayers, std::span> uploads, std::span uploadLayers) { + if (!uploads.empty()) { + Require(context.detiler != nullptr, "depth plane copy requires a texture detiler"); + context.detiler->BeginBatch(); + } + auto* recorder = RecorderFor(context); + std::unique_ptr batch; + if (recorder == nullptr) batch = std::make_unique(context); + const auto commands = recorder != nullptr ? recorder->Commands() : batch->Handle(); + const auto barrier = context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier"); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + std::vector regions; + const auto& mip = geometry.mips.front(); + const auto region = [&](std::uint32_t layer, VkImageAspectFlags aspectMask, std::uint32_t imageLayer) { + VkBufferImageCopy copy{}; + 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; + }; + for (const auto layer : copiedLayers) { + const auto copy = region(layer, VK_IMAGE_ASPECT_DEPTH_BIT, 0); + context.Function("vkCmdCopyImageToBuffer")(commands, slices[layer].image, VK_IMAGE_LAYOUT_GENERAL, staging->Handle(), 1, ©); + regions.push_back(region(layer, aspect(), layer)); + } + if (!uploads.empty()) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_HOST_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_HOST_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + for (std::size_t upload = 0; upload < uploads.size(); ++upload) { + const auto layer = uploadLayers[upload]; + context.detiler->Dispatch(commands, resource.tileMode, format == VK_FORMAT_D32_SFLOAT ? 4u : 2u, uploads[upload]->Handle(), 0, staging->Handle(), geometry.LinearLayerOffset(layer), geometry.mips.front(), false, layer, geometry.thick); + regions.push_back(region(layer, aspect(), layer)); + } + if (recorder != nullptr) { + for (const auto& upload : uploads) recorder->Keep(upload, upload->Bytes().size()); + } + } + VkImageMemoryBarrier toTransfer{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + toTransfer.srcAccessMask = VK_ACCESS_SHADER_READ_BIT; + toTransfer.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toTransfer.oldLayout = filled ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_UNDEFINED; + toTransfer.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + toTransfer.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toTransfer.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toTransfer.image = image; + toTransfer.subresourceRange = {aspect(), 0, 1, 0, layers}; + const VkBufferMemoryBarrier staged{VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER, nullptr, VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, staging->Handle(), 0, VK_WHOLE_SIZE}; + barrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &staged, 1, &toTransfer); + context.Function("vkCmdCopyBufferToImage")(commands, staging->Handle(), image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast(regions.size()), regions.data()); + VkImageMemoryBarrier toGeneral = toTransfer; + toGeneral.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toGeneral.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + toGeneral.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL; + barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); + filled = true; + if (batch) batch->SubmitAndWait(); + else Recorder::CountBarriers(Recorder::CommandClass::Draw, 3); + } + + VkDeviceSize sliceBytes() const { + return static_cast(extent.width) * extent.height * (format == VK_FORMAT_D32_SFLOAT ? 4u : 2u); + } + + VkImageAspectFlags aspect() const { + return format == VK_FORMAT_R16_UINT ? VK_IMAGE_ASPECT_COLOR_BIT : VK_IMAGE_ASPECT_DEPTH_BIT; + } + + void release() noexcept { + textures.clear(); + staging.reset(); + if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); + if (memory) context.Function("vkFreeMemory")(context.device, memory, nullptr); + image = VK_NULL_HANDLE; + memory = VK_NULL_HANDLE; + } +}; + +std::map, std::unique_ptr>& planeCopies() { + static auto* copies = new std::map, std::unique_ptr>(); + return *copies; } -std::mutex& surfacesMutex() { - static std::mutex mutex; +struct LayeredSlice { + DepthSurface* surface; + std::uint32_t layer; +}; + +class LayeredDepthStorage { +public: + 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); + Require(storage->ImageLayers() == layers, "a layered depth storage view's image does not hold its layers"); + staging = std::make_unique(context, static_cast(sliceBytes()) * layers, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); + } + + void Copy(std::span slices, bool toStorage) { + if (slices.empty()) return; + auto* recorder = Recorder::Active(); + Require(recorder != nullptr, "depth surface storage copies need an active recorder"); + const auto commands = recorder->Commands(); + recorder->Keep(storage); + const auto timing = recorder->BeginGpuTiming(Recorder::CommandClass::Copy); + std::vector storageRegions; + const auto region = [&](std::uint32_t layer, VkImageAspectFlags aspect, std::uint32_t imageLayer) { + VkBufferImageCopy copy{}; + copy.bufferOffset = sliceBytes() * layer; + copy.imageSubresource = {aspect, 0, imageLayer, 1}; + copy.imageExtent = {extent.width, extent.height, 1}; + return copy; + }; + for (const auto& slice : slices) storageRegions.push_back(region(slice.layer, VK_IMAGE_ASPECT_COLOR_BIT, slice.layer)); + const auto copyToBuffer = context.Function("vkCmdCopyImageToBuffer"); + const auto copyToImage = context.Function("vkCmdCopyBufferToImage"); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + if (toStorage) { + for (const auto& slice : slices) { + const auto copy = region(slice.layer, VK_IMAGE_ASPECT_DEPTH_BIT, 0); + copyToBuffer(commands, slice.surface->image, VK_IMAGE_LAYOUT_GENERAL, staging->Handle(), 1, ©); + } + } else { + copyToBuffer(commands, storage->Image(), VK_IMAGE_LAYOUT_GENERAL, staging->Handle(), static_cast(storageRegions.size()), storageRegions.data()); + } + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + if (toStorage) { + copyToImage(commands, staging->Handle(), storage->Image(), VK_IMAGE_LAYOUT_GENERAL, static_cast(storageRegions.size()), storageRegions.data()); + } else { + for (const auto& slice : slices) { + const auto copy = region(slice.layer, VK_IMAGE_ASPECT_DEPTH_BIT, 0); + copyToImage(commands, staging->Handle(), slice.surface->image, VK_IMAGE_LAYOUT_GENERAL, 1, ©); + } + } + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT); + Recorder::CountBarriers(Recorder::CommandClass::Copy, 3); + recorder->EndGpuTiming(timing, sliceBytes() * slices.size()); + } + + const Context context; + const DepthTarget base; + const VkExtent2D extent; + const std::uint32_t bytesPerTexel; + const std::uint32_t firstLayer; + const std::uint32_t layers; + std::shared_ptr storage; + +private: + std::unique_ptr staging; + + VkDeviceSize sliceBytes() const { + return static_cast(extent.width) * extent.height * bytesPerTexel; + } +}; + +std::map, std::unique_ptr>& layeredStorages() { + static auto* storages = new std::map, std::unique_ptr>(); + return *storages; +} + +bool sameSurface(const DepthTarget& a, const DepthTarget& b) { + return a.address == b.address && a.stencilAddress == b.stencilAddress && a.extent.width == b.extent.width && a.extent.height == b.extent.height && a.format == b.format && a.samples == b.samples && (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::recursive_mutex& surfacesMutex() { + static std::recursive_mutex mutex; return mutex; } @@ -139,51 +1279,971 @@ std::vector>& surfaces() { return *list; } +struct PlaneOwner { + DepthSurface* surface = nullptr; + std::uint64_t serial = 0; +}; + +using PlaneOwners = std::map, PlaneOwner>; + +std::uint64_t planeBinds = 0; + +PlaneOwners& depthOwners() { + static auto* owners = new PlaneOwners(); + return *owners; +} + +PlaneOwners& stencilOwners() { + static auto* owners = new PlaneOwners(); + return *owners; +} + +void takePlane(DepthSurface& surface, PlaneOwners& owners, std::uint64_t address, VkImageAspectFlagBits aspect, bool created, VkImageAspectFlags clearAspects) { + auto& entry = owners[{surface.context.device, address}]; + auto* owner = entry.surface; + if (owner != nullptr && owner->active->load(std::memory_order_acquire) && owner != &surface && owner->target.format == surface.target.format && owner->target.samples == surface.target.samples && owner->target.extent.width == surface.target.extent.width && owner->target.extent.height == surface.target.extent.height && (!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, 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 (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->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() && 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->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::map& clearedHtiles() { + static auto* cleared = new std::map(); + return *cleared; +} + +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.memoryStale) return; + surface.memoryStale = false; + const auto plane = stalePlaneRange(surface); + if (std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& other) { return other->memoryStale && stalePlaneRange(*other) == plane; })) return; + auto& planes = stalePlanes(); + std::lock_guard lock(planes.mutex); + std::erase(planes.ranges, plane); + publishStaleHull(planes); +} +} + +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; } -VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) { - std::lock_guard lock(surfacesMutex()); - for (const auto& surface : surfaces()) { - if (surface->context.device == context.device && sameSurface(surface->target, target)) return surface->view; +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"); + std::shared_lock lock(depthGuardMutex()); + for (const auto& range : depthGuards()) Require(address >= range.address + range.bytes || range.address >= address + bytes, "sample-preserving guest publication of native multisample depth is not implemented"); +} + +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); } - surfaces().push_back(std::make_unique(context, target)); - return surfaces().back()->view; + 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 == 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); + if (result == VK_ERROR_FORMAT_NOT_SUPPORTED) return false; + Check(result, "vkGetPhysicalDeviceImageFormatProperties sample locations depth"); + return (properties.sampleCounts & samples) != 0; +} + +VkFormat HostDepthFormat(const Context& context, VkFormat format) { + if (format != VK_FORMAT_D16_UNORM_S8_UINT) return format; + static std::mutex mutex; + static std::map chosen; + std::lock_guard lock(mutex); + if (const auto found = chosen.find(context.physical); found != chosen.end()) return found->second; + constexpr VkFormatFeatureFlags required = VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT; + auto host = format; + for (const auto candidate : {VK_FORMAT_D16_UNORM_S8_UINT, VK_FORMAT_D24_UNORM_S8_UINT, VK_FORMAT_D32_SFLOAT_S8_UINT}) { + VkFormatProperties properties{}; + context.formatProperties(context.physical, candidate, &properties); + if ((properties.optimalTilingFeatures & required) != required) continue; + host = candidate; + break; + } + chosen.emplace(context.physical, host); + return host; +} + +float HostDepthBiasConstant(const Context& context, VkFormat format, float constant) { + const auto host = HostDepthFormat(context, format); + if (constant == 0.0f || host == format) return constant; + Require(host == VK_FORMAT_D24_UNORM_S8_UINT, "a constant depth bias on a 16-bit depth target with stencil that the device stores as 32-bit float depth is not implemented"); + return constant * 256.0f; +} + +VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target, bool depthWritten) { + const auto htileCleared = RoutesToNativeDepthOwner(target) ? VkImageAspectFlags{0} : storeBackClearedHtile(context, target); + std::lock_guard gpu(GuestMemory::GpuMutex()); + std::unique_lock lock(surfacesMutex()); + const bool cleared = takeClearedHtile(target); + DepthSurface* surface = nullptr; + for (const auto& candidate : surfaces()) { + 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) { + 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}}}); + anyDepthGuard().store(true, std::memory_order_release); + InstallResidentImageReadGuard_nid_no_patch(); + fresh.samplingActive = true; + } + surface = surfaces().back().get(); + } + 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, 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)) static_cast(surface->SyncFromMemory(mirrorResource(target))); + surface->ApplyFastClear(); + 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->memoryStale = false; + other->DropMirror(); + } + } + if ((depthWritten || cleared) && mirrorable(target)) { + surface->memoryStale = true; + const auto plane = target.address; + noteStalePlane(plane, plane + DescribeSurface(mirrorResource(target)).guestBytes); + } + 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) { + 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 {}; + flushing = true; + struct Reset { + ~Reset() { flushing = false; } + } reset; + 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->memoryStale) continue; + const auto resource = mirrorResource(surface->target); + const auto plane = surface->target.address; + const auto planeBytes = DescribeSurface(resource).guestBytes; + if (plane >= end || address >= plane + planeBytes) { + remaining.emplace_back(plane, plane + planeBytes); + continue; + } + surface->memoryStale = false; + if (surface->SyncFromMemory(resource)) continue; + stored.emplace_back(surface.get(), surface->StoreDepthPlane(resource)); + ranges.emplace_back(plane, planeBytes); + } + auto& planes = stalePlanes(); + std::lock_guard staleLock(planes.mutex); + planes.ranges = std::move(remaining); + publishStaleHull(planes); + } + for (const auto& entry : stored) entry.second->MarkWholeWritten(); + for (const auto& [plane, planeBytes] : ranges) StorageTexture::FlushPending(plane, static_cast(planeBytes), nullptr, "depth plane store"); + const auto generation = GuestMemory::TrackerGeneration(); + std::lock_guard lock(surfacesMutex()); + for (const auto& entry : stored) { + if (std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& candidate) { return candidate.get() == entry.first; })) { + entry.first->syncedGeneration = generation; + entry.first->NoteMemory(); + } + } + return ranges; +} + +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; + if (code.size() < htileMaskKernel.size() || !std::equal(htileMaskKernel.begin(), htileMaskKernel.end(), code.begin())) return 0; + if ((userData[5] & 0xfu) != 0 || (userData[4] & 0xfu) != 0) return 0; + return userData[2] | (static_cast(userData[3]) << 32u); +} + +std::optional MatchMemoryValueFill(std::span code, std::span userData, const std::array& numThreads, const std::array& groups) { + static constexpr std::array kernel{0xd7460000u, 0x04010c08u, 0xf4201a82u, 0xfa000000u, 0xbf8cc07fu, 0x7e02026au, 0xe0102000u, 0x80000100u, 0xbf810000u}; + if (userData.size() < 8 || numThreads != std::array{64u, 1u, 1u} || groups[1] != 1u || groups[2] != 1u) return std::nullopt; + if (code.size() < kernel.size() || !std::equal(kernel.begin(), kernel.end(), code.begin())) return std::nullopt; + const auto stride = (userData[1] >> 16u) & 0x3fffu; + const bool swizzled = ((userData[1] >> 31u) & 1u) != 0; + const bool addTid = ((userData[3] >> 23u) & 1u) != 0; + const auto type = userData[3] >> 30u; + const auto format = (userData[3] >> 12u) & 0x7fu; + const auto valueStride = (userData[5] >> 16u) & 0x3fffu; + const auto valueBytes = valueStride == 0 ? static_cast(userData[6]) : static_cast(userData[6]) * valueStride; + if (type != 0 || stride != 4 || swizzled || addTid || format < 20u || format > 22u || valueBytes < 4u) return std::nullopt; + const auto dwords = std::min(static_cast(groups[0]) * 64u, userData[2]); + const auto address = userData[0] | (static_cast(userData[1] & 0xffffu) << 32u); + const auto valueAddress = userData[4] | (static_cast(userData[5] & 0xffffu) << 32u); + if (dwords == 0 || valueAddress == 0 || (valueAddress & 3u) != 0) return std::nullopt; + return MemoryValueFill{address, static_cast(dwords * 4u), valueAddress}; +} + +void NoteMemoryValueFill(const MemoryValueFill& fill) { + std::uint32_t value = 0; + GuestMemory::Read(fill.valueAddress, std::as_writable_bytes(std::span(&value, 1)), 4); + NoteDepthMetadataFill(fill.address, fill.bytes, value); +} + +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; +} + +void NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes, std::size_t nativeBytes) { + std::lock_guard lock(surfacesMutex()); + forgetHtileReads(htileAddress, bytes); + bool existing = false; + for (const auto& surface : surfaces()) { + 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->clearGeneration != 0u) surface->sampling.Refuse("overlapping native multisample depth clear transitions are not implemented"); + else surface->clearGeneration = GuestMemory::CollectWritesUncached(metadata.address, metadata.bytes); + 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()[htileAddress] = bytes; } 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->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()); + std::lock_guard lock(surfacesMutex()); + auto* found = planeSurface(context.device, resource.baseAddress); + if (found == nullptr) return nullptr; + const auto& base = found->target; + 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; + const bool cube = resource.dimension == TextureDimension::kCube; + const bool layered = cube || (resource.dimension == TextureDimension::k2DArray && resource.depthOrLastArray != 0); + 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()) { + retire(*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 (!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 + geometry.GuestLayerOffset(layer); + auto* slice = depthSlice(context.device, address, base); + if (slice != nullptr) slice->ApplyFastClear(); + if (slice == nullptr) slices.push_back({}); + else slices.push_back({slice->image, slice == drawnDepth ? 0u : slice->written}); + } + const auto imageFormat = integer ? VK_FORMAT_R16_UINT : d16 ? VK_FORMAT_D16_UNORM : VK_FORMAT_D32_SFLOAT; + auto& copy = planeCopies()[{context.device, base.address, base.extent.width, base.extent.height, imageFormat, layers}]; + if (copy == nullptr) { + copy = std::make_unique(context, base.extent, imageFormat, layers); + } + return copy->Refresh(slices, resource, components, cube || resource.dimension == TextureDimension::k2DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D, layered && !cube ? resource.baseArray : 0u); +} + +bool DepthSurfaceHolds(const Context& context, const GuestTextureResource& resource, const Texture* texture) { + 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; + std::lock_guard lock(surfacesMutex()); + 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) { + VkImageAspectFlags cleared = (pattern & 0xfu) == 0 ? VK_IMAGE_ASPECT_DEPTH_BIT : 0u; + if (stencilInHtile && ((pattern >> 8u) & 0x3u) == 0) cleared |= VK_IMAGE_ASPECT_STENCIL_BIT; + return cleared; +} + +std::uint64_t HtileMetadataBytes(VkExtent2D extent) { + const auto blocks = static_cast((extent.width + 1023u) / 1024u) * ((extent.height + 511u) / 512u); + return blocks * 32768u; +} + +bool HtileFillCovers(std::uint64_t htile, VkExtent2D extent, std::uint64_t address, std::size_t bytes) { + if (htile == 0 || htile < address || htile >= address + bytes) return false; + 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 (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); + } + } +} + +bool DepthSurfaceAt(const Context& context, std::span words, const GuestTextureResource& resource) { + std::lock_guard lock(surfacesMutex()); + const auto* found = planeSurface(context.device, resource.baseAddress); + if (found == nullptr) return false; + const auto& target = found->target; + if (resource.width != target.extent.width || resource.height != target.extent.height) return false; + const bool stencil = target.stencilAddress != 0 && target.stencilAddress == resource.baseAddress; + const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; + 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); + return ResolveTextureFormat(resource.format) == expected || depthBits; +} + +namespace { + +std::vector layeredSlices(const Context& context, const LayeredDepthStorage& storage) { + const auto& base = storage.base; + std::vector slices; + const auto& list = surfaces(); + for (std::uint32_t layer = 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->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; +} + +std::shared_ptr layeredDepthStorage(const Context& context, DepthSurface& surface, const GuestTextureResource& resource, std::uint32_t mip) { + const auto& target = surface.target; + const auto layers = resource.depthOrLastArray + 1u; + const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; + const auto bytesPerTexel = d16 ? 2u : 4u; + 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), 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(); + entry->Copy(slices, true); + return entry->storage; +} + +} + +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; - const auto& target = (*found)->target; - if (resource.width != target.extent.width || resource.height != target.extent.height) return nullptr; - const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress; - if (!stencil) { - const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; - const bool depthBits = words.size() >= 4 && ShaderRecompiler::DepthBitsTextureWidth(words[1], words[3]) == (d16 ? 16u : 32u); - if (ResolveTextureFormat(resource.format) != (d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT) && !depthBits) return nullptr; + auto& surface = **found; + const bool stencil = surface.target.stencilAddress != 0 && surface.target.stencilAddress == resource.baseAddress; + if (stencil || resource.dimension != TextureDimension::k2DArray || (resource.baseArray == 0 && resource.depthOrLastArray == 0)) return surface.Storage(words, resource, mip); + if (!surface.StorageViewMatches(words, resource)) return nullptr; + return layeredDepthStorage(context, surface, resource, mip); +} + +bool DepthSurfaceStoreBack(const Context& context, const StorageTexture& storage) { + std::lock_guard lock(surfacesMutex()); + for (auto& [key, entry] : layeredStorages()) { + if (std::get<0>(key) != context.device || entry->storage.get() != &storage) continue; + const auto slices = layeredSlices(context, *entry); + entry->Copy(slices, false); + for (const auto& slice : slices) slice.surface->written = ++depthWrites; + return true; } - return (*found)->Sampled(words, resource, components); + for (const auto& surface : surfaces()) { + 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 e36586389c..2438c18aa2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/ColorTargetTransfer.hpp" #include "prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp" #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" @@ -7,13 +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 @@ -29,40 +35,45 @@ #include #include #include +#include #include namespace AgcDriver::Graphics { -namespace { - -std::uint32_t GuestFormatFor(VkFormat format, std::uint32_t elementBytes) { - if (const auto guest = FindGuestColorTargetFormat(format, elementBytes)) return *guest; - throw std::runtime_error("AGC graphics: no guest texture format matches the color buffer format " + std::to_string(static_cast(format))); +std::uint64_t DrawRenderPassKey(const Context& context, const State& state, std::span targetViews) { + std::uint64_t key = 14695981039346656037ull; + const auto mix = [&](std::uint64_t value) { + key ^= value; + key *= 1099511628211ull; + }; + for (const auto view : targetViews) mix(reinterpret_cast(view)); + if (state.blends.size() != state.colors.size()) { + mix(state.blends.size()); + for (const auto& color : state.colors) mix(color.exportIndex); + } + mix(state.renderExtent.width); + mix(state.renderExtent.height); + mix(state.samples); + mix(state.mergedCoverage); + mix(state.customSampleLocations); + if (state.customSampleLocations) { + mix(state.sampleLocationGrid); + for (const auto location : state.sampleLocations) mix(location); + } + if (!context.provokingVertexModePerPipeline) mix(state.provokingVertexMode); + return key; } -// The color buffer as a single-mip 2D surface descriptor (tile mode SW_64KB_R_X). -GuestTextureResource SurfaceForTarget(const ColorTarget& color) { - Require(color.tileMode != ColorTileMode::Linear, "linear color targets are not resident"); - const bool chain = color.mipCount > 1; - GuestTextureResource surface{}; - surface.baseAddress = chain ? color.surfaceAddress : color.address; - surface.width = chain ? color.surfaceExtent.width : color.extent.width; - surface.height = chain ? color.surfaceExtent.height : color.extent.height; - surface.depthOrLastArray = color.depth - 1u; - surface.baseArray = 0; - surface.mipCount = color.mipCount; - surface.baseLevel = 0; - surface.lastLevel = color.mipCount - 1; - surface.tileMode = ColorTextureTileMode(color.tileMode); - surface.dimension = color.depth > 1 ? TextureDimension::k3D : TextureDimension::k2D; - surface.format = GuestFormatFor(color.format, color.elementBytes); - surface.dstSelX = 4; - surface.dstSelY = 5; - surface.dstSelZ = 6; - surface.dstSelW = 7; - surface.dccAddress = color.dccAddress; - surface.dccAlphaOnMsb = color.dccAlphaOnMsb; - surface.dccPipeAligned = color.dccPipeAligned; +namespace { + +GuestTextureResource ArraySurfaceForTarget(const ColorTarget& color) { + Require(color.arrayLastSlice != 0, "the color target is not a view of a 2D array"); + auto surface = SurfaceForTarget(color); + surface.baseAddress = color.arrayAddress; + surface.width = color.surfaceExtent.width; + surface.height = color.surfaceExtent.height; + surface.depthOrLastArray = color.arrayLastSlice; + surface.dimension = TextureDimension::k2DArray; return surface; } @@ -88,12 +99,12 @@ std::array clearTexel(const ColorTarget& color, DccKeys keys) { return texel; } -bool clearToTexel(StorageTexture& image, const std::array& texel, std::uint32_t elementBytes, const char*& refusal) { +bool clearToTexel(StorageTexture& image, const std::array& texel, std::uint32_t elementBytes, const char*& refusal, bool allowRegisterClear = false) { std::array repeated{}; for (std::size_t offset = 0; offset + elementBytes <= repeated.size(); offset += elementBytes) std::memcpy(repeated.data() + offset, texel.data(), elementBytes); std::array pattern{}; std::memcpy(pattern.data(), repeated.data(), repeated.size()); - return image.FillClear(std::span(pattern), StorageTexture::WholeImage, refusal); + return image.FillClear(std::span(pattern), StorageTexture::WholeImage, refusal, allowRegisterClear); } void writeTexels(const ColorTarget& color, const std::array& texel) { @@ -111,12 +122,32 @@ void storeClearTexels(const Context& context, const ColorTarget& color, const st MarkDccUncompressed(context, color.dccAddress, color.bytes, colorKeyCount(color, color.bytes)); } +void storeSingleTexels(const Context& context, const ColorTarget& color) { + Require(color.tileMode != ColorTileMode::Linear, "comp-to-single DCC keys of a linear color target"); + const ColorTargetLayout surface(color.extent.width, color.extent.height, color.tileMode, color.elementBytes, color.pipeBankXor); + Require(surface.Bytes() == color.bytes, "comp-to-single DCC keys over a color target whose layout differs from its surface"); + StorageTexture::FlushPending(color.address, color.bytes, nullptr, "comp-to-single expansion"); + std::vector texels(color.bytes); + GuestMemory::Read(color.address, texels, surface.Alignment()); + constexpr std::size_t BlockBytes = 256; + for (std::uint32_t y = 0; y < color.extent.height; ++y) { + for (std::uint32_t x = 0; x < color.extent.width; ++x) { + const auto offset = surface.Offset(x, y); + const auto first = offset / BlockBytes * BlockBytes; + if (offset != first) std::memcpy(texels.data() + offset, texels.data() + first, color.elementBytes); + } + } + GuestMemory::Write(color.address, texels, surface.Alignment()); + MarkDccUncompressed(context, color.dccAddress, color.bytes, colorKeyCount(color, color.bytes)); +} + void materializeRegisterClear(const Context& context, const ColorTarget& color, StorageTexture& resident) { if (color.dccAddress == 0 || resident.Descriptor().dccAddress != color.dccAddress) return; - if (CurrentDccKeys(color.dccAddress, color.bytes) != DccKeys::ClearRegister) return; + 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)) { @@ -134,25 +165,147 @@ void materializeRegisterClear(const Context& context, const ColorTarget& color, resident.Refresh(); } -bool materializeCmaskClear(const Context& context, const ColorTarget& color, StorageTexture* resident) { - if (color.cmaskAddress == 0) return false; - const auto metadataBytes = static_cast(color.cmaskBytes) * 256u; - const auto state = CurrentDccKeys(color.cmaskAddress, metadataBytes); - if (state == DccKeys::Uncompressed) return false; - Require(color.dccAddress == 0, std::string("CMASK of a DCC color target that is not all expanded is not modeled (") + DccKeysName(state) + ")"); - Require(state == DccKeys::Clear0000, std::string("CMASK whose tiles are not all fast-cleared or all expanded is not modeled (") + DccKeysName(state) + ")"); - const auto texel = clearTexel(color, DccKeys::ClearRegister); - const char* refusal = nullptr; - if (resident == nullptr || !clearToTexel(*resident, texel, color.elementBytes, refusal)) { - StorageTexture::FlushPending(color.address, color.bytes, nullptr, "fast-clear materialization"); - writeTexels(color, texel); - if (resident != nullptr) resident->Refresh(); - } - MarkDccUncompressed(context, color.cmaskAddress, metadataBytes); - return true; +enum class CmaskState { Expanded, Cleared, Partial }; + +struct CmaskProof { + std::size_t bytes = 0; + std::uint64_t generation = 0; +}; + +std::mutex& cmaskProofMutex() { + static std::mutex mutex; + return mutex; } -void imageBarrier(const Context& context, VkCommandBuffer commands, VkImage image, VkImageLayout oldLayout, VkImageLayout newLayout, VkPipelineStageFlags sourceStage, VkPipelineStageFlags destinationStage, VkAccessFlags sourceAccess, VkAccessFlags destinationAccess) { +std::unordered_map& cmaskProofs() { + static std::unordered_map proofs; + return proofs; +} + +bool cmaskProved(std::uint64_t address, std::size_t bytes) { + if (GuestMemory::CollectWrites(address, bytes) == 0) return false; + std::lock_guard lock(cmaskProofMutex()); + const auto found = cmaskProofs().find(address); + return found != cmaskProofs().end() && found->second.bytes == bytes && !GuestMemory::StoredOver(address, bytes, found->second.generation); +} + +void proveCmask(std::uint64_t address, std::size_t bytes, std::uint64_t generation) { + std::lock_guard lock(cmaskProofMutex()); + if (generation == 0) cmaskProofs().erase(address); + else cmaskProofs()[address] = {bytes, generation}; +} + +void requireCmaskCodes(std::uint32_t codes) { + Require((codes & 1u) == 0, "a CMASK fast clear of a DCC color target, whose clear belongs in its DCC keys, was recorded"); +} + +CmaskState readCmask(const ColorTarget& color, const CmaskLayout& layout, std::vector& cleared) { + const auto address = color.cmaskAddress; + const auto bytes = layout.Bytes(); + const bool locked = GuestMemory::GpuMutex().HeldByThisThread(); + auto* recorder = Recorder::Active(); + if (locked && recorder != nullptr && recorder->PendingWriteOverlaps(address, bytes)) { + Recorder::CountSync(2); + recorder->SyncThrough(address, bytes); + } + if (AnyShadowedOverlaps(address, bytes)) { + Require(locked, "a CMASK under a unit shadow must be read under the device lock"); + PublishShadow(address, bytes, PublishScope::Whole, PublishReason::Keys); + if (recorder != nullptr) { + Recorder::CountSync(2); + recorder->Sync(); + } + } + std::vector mask(bytes); + GuestMemory::Read(address, mask, CmaskLayout::Alignment); + const auto uniform = [&](std::byte value) { return std::all_of(mask.begin(), mask.end(), [&](std::byte entry) { return entry == value; }); }; + if (uniform(std::byte{0xff})) return CmaskState::Expanded; + if (uniform(std::byte{0})) return CmaskState::Cleared; + for (std::uint32_t tileY = 0; tileY < layout.TilesY(); ++tileY) { + for (std::uint32_t tileX = 0; tileX < layout.TilesX(); ++tileX) { + const auto nibble = layout.Nibble(tileX, tileY); + const auto code = (std::to_integer(mask[nibble / 2u]) >> ((nibble % 2u) * 4u)) & 0xfu; + if (code == 0u) cleared.push_back(tileY * layout.TilesX() + tileX); + } + } + if (cleared.empty()) return CmaskState::Expanded; + if (cleared.size() == static_cast(layout.TilesX()) * layout.TilesY()) return CmaskState::Cleared; + return CmaskState::Partial; +} + +void storeCmaskClearTexels(const ColorTarget& color, const CmaskLayout& layout, const std::vector* tiles, const std::array& texel) { + StorageTexture::FlushPending(color.address, color.bytes, nullptr, "fast-clear materialization"); + const ColorTargetLayout surface(color.extent.width, color.extent.height, color.tileMode, color.elementBytes); + Require(surface.Bytes() == color.bytes, "CMASK fast clear over a color target whose layout differs from its surface"); + std::vector texels(surface.Bytes()); + GuestMemory::Read(color.address, texels, surface.Alignment()); + const auto fill = [&](std::uint32_t tile) { + const auto x0 = (tile % layout.TilesX()) * 8u; + const auto y0 = (tile / layout.TilesX()) * 8u; + for (auto y = y0; y < std::min(y0 + 8u, color.extent.height); ++y) { + for (auto x = x0; x < std::min(x0 + 8u, color.extent.width); ++x) std::memcpy(texels.data() + surface.Offset(x, y), texel.data(), color.elementBytes); + } + }; + if (tiles != nullptr) { + for (const auto tile : *tiles) fill(tile); + } else { + for (std::uint32_t tile = 0; tile < layout.TilesX() * layout.TilesY(); ++tile) fill(tile); + } + GuestMemory::Write(color.address, texels, surface.Alignment()); +} + +void recordCmaskClear(const Context& context, Recorder& recorder, const HostImport& import, const ColorTarget& color, const CmaskLayout& layout, const std::shared_ptr& resident, VkImageView view) { + const auto address = color.cmaskAddress; + const auto bytes = layout.Bytes(); + StorageTexture::FlushPending(address, bytes, nullptr, "fast-clear materialization"); + 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, 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)); +} + +bool materializeCmaskClear(const Context& context, const ColorTarget& color, const std::shared_ptr& resident) { + if (color.cmaskAddress == 0) return false; + if (context.detiler != nullptr) requireCmaskCodes(context.detiler->CmaskErrors()); + const CmaskLayout layout(color.extent.width, color.extent.height); + const auto bytes = layout.Bytes(); + Require(color.cmaskBytes == bytes, "CMASK size differs from the color target's CMASK layout"); + if (cmaskProved(color.cmaskAddress, bytes)) return false; + auto* recorder = Recorder::Active(); + if (resident != nullptr && context.detiler != nullptr && recorder != nullptr && GuestMemory::GpuMutex().HeldByThisThread()) { + const auto* import = HostImportFor(context, color.cmaskAddress, bytes); + const auto view = import != nullptr ? resident->ElementView() : VK_NULL_HANDLE; + if (view != VK_NULL_HANDLE) { + recordCmaskClear(context, *recorder, *import, color, layout, resident, view); + return true; + } + } + const auto collected = GuestMemory::CollectWrites(color.cmaskAddress, bytes); + std::vector cleared; + const auto state = readCmask(color, layout, cleared); + if (state == CmaskState::Expanded) { + proveCmask(color.cmaskAddress, bytes, collected); + return false; + } + 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 && 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(); + } + const std::vector expanded(bytes, std::byte{0xff}); + GuestMemory::Write(color.cmaskAddress, expanded, CmaskLayout::Alignment); + proveCmask(color.cmaskAddress, bytes, GuestMemory::CollectWrites(color.cmaskAddress, bytes)); + return filled; +} + +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; @@ -161,7 +314,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); } @@ -194,7 +347,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 { @@ -205,6 +358,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; @@ -277,6 +433,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. @@ -347,6 +504,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; @@ -464,7 +629,7 @@ bool ValidationKey(const Context& context, std::span shade for (const auto& shader : shaders) { Require(shader.program != nullptr, "missing compiled shader"); const auto& program = *shader.program; - const bool generated = state.rectList && (shader.stage == Stage::TessellationControl || shader.stage == Stage::TessellationEvaluation); + const bool generated = (state.rectList && (shader.stage == Stage::TessellationControl || shader.stage == Stage::TessellationEvaluation)) || shader.stage == Stage::Geometry; if (!generated && program.PipelineVariantId() == 0) { keyed = false; break; @@ -495,6 +660,11 @@ bool ValidationKey(const Context& context, std::span shade add(context.subgroup.supportedStages); add(context.subgroup.supportedOperations); add(context.fragmentShaderBarycentric); + add(context.shaderClipDistance); + add(context.shaderCullDistance); + add(context.viewportIndexLayer); + add(context.shaderTerminateInvocation); + add(state.paClVsOutCntl); add(state.stages.mesh.has_value()); if (state.stages.mesh) { const auto& mesh = *state.stages.mesh; @@ -506,6 +676,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) { @@ -515,6 +686,9 @@ bool ValidationKey(const Context& context, std::span shade add(tessellation.domain); add(tessellation.partitioning); add(tessellation.outputTopology); + add(tessellation.patchesPerGroup); + add(tessellation.primitiveGroupSize); + add(tessellation.breakAtInstance ? 1u : 0u); } } return keyed; @@ -530,12 +704,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()); @@ -544,15 +725,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); + 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; } @@ -560,7 +744,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; } @@ -581,8 +765,10 @@ bool MovableBuffers() { return enabled; } -ResourceCache::Key DrawResourceKey(const Context& context, std::span shaders, const ColorTarget& target, std::uint64_t indexAddress, std::uint64_t indexBytes, bool ranges) { - ResourceCache::Key key{0xffffffffu}; +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; + 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)); @@ -590,12 +776,14 @@ 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) { - append64(target.address); + append64(target.address | target.pipeBankXor); append64(target.bytes); append64(indexAddress); append64(indexBytes); @@ -616,10 +804,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"); } @@ -641,7 +830,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}; } @@ -676,6 +865,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; @@ -753,11 +954,70 @@ 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, 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(); + 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; +} + void KeepDrawInput(Recorder* recorder, std::uint64_t address, const DrawInputCopy& copy, Recorder::SnapshotUse use, std::uint32_t derived) { if (recorder == nullptr || copy.reused || copy.generation == 0 || copy.buffer == nullptr) return; 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, bool* behindGpuWrite) { + auto* recorder = context.recorder; + 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 || HostImportOffset(*import, address) % alignment != 0) return nullptr; + recorder->NotePendingRead(address, bytes, Recorder::ReadKind::DrawInput); + if (behindGpuWrite != nullptr && behind) *behindGpuWrite = true; + return import; +} + +InPlaceIndices InPlaceIndexRange(Recorder& recorder, std::uint64_t address, std::size_t bytes, std::uint32_t indexSize, bool skipRestart) { + const std::span range(reinterpret_cast(address), bytes); + InPlaceIndices indices; + if (bytes < Recorder::KeptIndexRangeIndices * indexSize) { + indices.highest = HighestDrawIndex(range, indexSize, skipRestart); + return indices; + } + const auto use = indexSize == 2 ? (skipRestart ? Recorder::SnapshotUse::Index16Restart : Recorder::SnapshotUse::Index16) : (skipRestart ? Recorder::SnapshotUse::Index32Restart : Recorder::SnapshotUse::Index32); + const auto registryGeneration = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + const auto generation = GuestMemory::CollectWrites(address, bytes); + if (generation != 0 && recorder.KnownHighestIndex(address, bytes, use, indices.highest)) { + indices.reused = true; + return indices; + } + indices.highest = HighestDrawIndex(range, indexSize, skipRestart); + recorder.KeepHighestIndex(address, bytes, use, generation, registryGeneration, indices.highest); + return indices; +} + namespace { // The draw's inputs before its resources (prepareDrawInputs): the validated parameters, the index @@ -768,16 +1028,29 @@ struct DrawInputs { bool nothing = false; std::uint64_t indexBytes = 0; std::shared_ptr indices; + VkBuffer indexHandle = VK_NULL_HANDLE; + VkDeviceSize indexOffset = 0; std::uint32_t maxIndex = 0; VertexInputLayout vertexInput; + std::shared_ptr retainedVertexInput; std::vector> vertexBuffers; std::vector vertexHandles; std::vector vertexOffsets; - std::set fragmentOutputs; + std::vector> inPlaceReads; + bool vertexInputBehindGpuWrite = false; + std::uint32_t fragmentOutputs = 0; VkPipelineStageFlags shaderStages = 0; std::uint32_t meshGroups = 0; + std::vector restartTable; + const VertexInputLayout& VertexLayout() const { return retainedVertexInput != nullptr ? *retainedVertexInput : vertexInput; } }; +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}); + return zero; +} + // Draw's validation, index and vertex phases. With `recipe` the fragment outputs, the pipeline // stages and the vertex input layout are the recipe's (derived from the same compiled stages) // instead of computed. @@ -805,8 +1078,12 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P Require(draw.firstInstance <= std::numeric_limits::max() - (draw.instanceCount - 1u), "auto draw instance range overflow"); } } - if (args == nullptr) Require(draw.indexCount != 0 && draw.instanceCount != 0, "zero-count indexed draws are unsupported"); - else Require((!state.stages.mesh || draw.indexed) && !state.stages.tessellation && !state.rectList, "indirect draw on a non-vertex path must be resolved by the driver"); + if (args == nullptr) { + if (draw.indexCount == 0 || draw.instanceCount == 0) { + inputs.nothing = true; + return inputs; + } + } else Require((!state.stages.mesh || draw.indexed) && !state.stages.tessellation && !state.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)); @@ -814,7 +1091,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 (state.rectList) Require(draw.indexCount % 3 == 0, "incomplete rect-list primitive"); + 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; @@ -830,7 +1107,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; @@ -843,65 +1125,245 @@ 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) { - const auto use = draw.indexSize == 2 ? Recorder::SnapshotUse::Index16 : Recorder::SnapshotUse::Index32; - auto copy = CopyDrawInput(context, context.recorder, draw.indexAddress, static_cast(indexBytes), draw.indexSize, use); - std::uint32_t highest = copy.derived; - if (!copy.reused) { - highest = 0; - const auto bytes = copy.buffer->Bytes(); - for (std::size_t offset = 0; offset < indexBytes; offset += draw.indexSize) { - std::uint32_t index = 0; + if (state.quadList) { + Require(!draw.indirect, "indirect quad-list draw is unsupported"); + const std::uint32_t quadCount = draw.indexCount / 4u; + if (quadCount == 0u || draw.instanceCount == 0u) { + inputs.nothing = true; + return inputs; + } + const std::uint32_t outputIndexCount = quadCount * 6u; + const std::size_t quadIndexBytes = static_cast(outputIndexCount) * sizeof(std::uint32_t); + auto indexCopy = std::make_shared(context, quadIndexBytes, 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; + 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 value = 0; - std::memcpy(&value, bytes.data() + offset, sizeof(value)); - index = value; + 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::memcpy(&index, bytes.data() + offset, sizeof(index)); + std::uint32_t v = 0; + GuestMemory::Read(draw.indexAddress + static_cast(idx) * 4u, std::as_writable_bytes(std::span(&v, 1)), 4); + return v; } - highest = std::max(highest, index); + }; + for (std::uint32_t q = 0; q < quadCount; ++q) { + const auto i0 = readIndex(q * 4u + 0u); + const auto i1 = readIndex(q * 4u + 1u); + const auto i2 = readIndex(q * 4u + 2u); + const auto i3 = readIndex(q * 4u + 3u); + maxIdx = std::max({maxIdx, i0, i1, i2, i3}); + if (lastProvoking) { + outIndices[q * 6u + 0u] = i0; + outIndices[q * 6u + 1u] = i1; + outIndices[q * 6u + 2u] = i3; + outIndices[q * 6u + 3u] = i1; + outIndices[q * 6u + 4u] = i2; + outIndices[q * 6u + 5u] = i3; + } else { + outIndices[q * 6u + 0u] = i0; + outIndices[q * 6u + 1u] = i1; + outIndices[q * 6u + 2u] = i2; + outIndices[q * 6u + 3u] = i0; + outIndices[q * 6u + 4u] = i2; + outIndices[q * 6u + 5u] = i3; + } + } + Require(draw.firstVertex <= std::numeric_limits::max() - maxIdx, "indexed draw vertex range overflow"); + inputs.maxIndex = maxIdx + draw.firstVertex; + } else { + maxIdx = quadCount * 4u - 1u; + Require(draw.firstVertex <= std::numeric_limits::max() - maxIdx, "auto draw vertex range overflow"); + for (std::uint32_t q = 0; q < quadCount; ++q) { + const auto i0 = q * 4u + 0u; + const auto i1 = q * 4u + 1u; + const auto i2 = q * 4u + 2u; + const auto i3 = q * 4u + 3u; + if (lastProvoking) { + outIndices[q * 6u + 0u] = i0; + outIndices[q * 6u + 1u] = i1; + outIndices[q * 6u + 2u] = i3; + outIndices[q * 6u + 3u] = i1; + outIndices[q * 6u + 4u] = i2; + outIndices[q * 6u + 5u] = i3; + } else { + outIndices[q * 6u + 0u] = i0; + outIndices[q * 6u + 1u] = i1; + outIndices[q * 6u + 2u] = i2; + outIndices[q * 6u + 3u] = i0; + outIndices[q * 6u + 4u] = i2; + outIndices[q * 6u + 5u] = i3; + } + } + 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.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; + const auto use = draw.indexSize == 2 ? (skipRestart ? Recorder::SnapshotUse::Index16Restart : Recorder::SnapshotUse::Index16) : (skipRestart ? Recorder::SnapshotUse::Index32Restart : Recorder::SnapshotUse::Index32); + const auto* import = InPlaceDrawInput(context, draw.indexAddress, static_cast(indexBytes), draw.indexSize); + DrawInputCopy copy; + if (import == nullptr) copy = CopyDrawInput(context, context.recorder, draw.indexAddress, static_cast(indexBytes), draw.indexSize, use); + else GuestMemory::CheckRange(reinterpret_cast(draw.indexAddress), static_cast(indexBytes), draw.indexSize); + std::optional highest; + if (import != nullptr) { + highest = InPlaceIndexRange(*context.recorder, draw.indexAddress, static_cast(indexBytes), draw.indexSize, skipRestart).highest; + } else if (!copy.reused) { + highest = HighestDrawIndex(copy.buffer->Bytes().first(static_cast(indexBytes)), draw.indexSize, skipRestart); + KeepDrawInput(context.recorder, draw.indexAddress, copy, use, skipRestart ? (highest ? *highest + 1u : 0u) : highest.value_or(0u)); + } else if (!skipRestart) { + highest = copy.derived; + } else if (copy.derived != 0) { + highest = copy.derived - 1u; + } + if (!highest) { + inputs.nothing = true; + return inputs; + } + constexpr std::uint32_t ZeroFetchSlack = 0x10000u; + if (const auto zeroFrom = state.stages.mesh ? std::nullopt : ZeroFetchVertex(shaders.front().program->vertexAttributes)) { + const auto limit = *zeroFrom > draw.firstVertex ? *zeroFrom - draw.firstVertex : 0u; + if (*highest >= limit && *highest - limit >= ZeroFetchSlack) { + GuestMemory::FlushGpuWrites(draw.indexAddress, static_cast(indexBytes)); + auto clamped = std::make_shared(context, static_cast(indexBytes), VK_BUFFER_USAGE_INDEX_BUFFER_BIT); + const auto bytes = clamped->Bytes().first(static_cast(indexBytes)); + GuestMemory::Read(draw.indexAddress, bytes, draw.indexSize); + highest = ClampDrawIndices(bytes, draw.indexSize, skipRestart, limit); + import = nullptr; + copy = DrawInputCopy{}; + copy.buffer = std::move(clamped); + if (!highest) { + inputs.nothing = true; + return inputs; + } } - KeepDrawInput(context.recorder, draw.indexAddress, copy, use, highest); + } + Require(*highest <= context.limits.maxDrawIndexedIndexValue, "index exceeds the device's indexed draw limit"); + Require(!fanGeometry || !state.primitiveRestart || *highest != (draw.indexSize == 2 ? 0xffffu : 0xffffffffu), "primitive restart in a triangle fan geometry draw is unsupported"); + inputs.maxIndex = *highest; + if (skipRestart && state.stages.mesh && state.stages.mesh->inputPrimitive == 6) { + const auto indexSpan = import != nullptr ? std::span(reinterpret_cast(draw.indexAddress), static_cast(indexBytes)) : copy.buffer->Bytes().first(static_cast(indexBytes)); + inputs.restartTable = MeshRestartTable(indexSpan, draw.indexSize); + } + if (import != nullptr) { + inputs.indexHandle = import->buffer; + inputs.indexOffset = HostImportOffset(*import, draw.indexAddress); + inputs.inPlaceReads.emplace_back(draw.indexAddress, draw.indexAddress + indexBytes); + } else { + inputs.indices = std::move(copy.buffer); + inputs.indexHandle = inputs.indices->Handle(); } - Require(highest <= context.limits.maxDrawIndexedIndexValue, "index exceeds the device's indexed draw limit"); - Require(!state.stages.mesh || state.stages.mesh->inputPrimitive != 5 || !state.primitiveRestart || highest != (draw.indexSize == 2 ? 0xffffu : 0xffffffffu), "primitive restart in a triangle fan geometry draw is unsupported"); - inputs.maxIndex = highest; - inputs.indices = std::move(copy.buffer); } 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"); inputs.maxIndex += draw.firstVertex; } std::vector fetches; + std::vector fetchValid; fetches.reserve(attributes.size()); - for (const auto& attribute : attributes) { + fetchValid.reserve(attributes.size()); + std::vector fetchOf(attributes.size(), 0); + std::vector> zeroed(attributes.size()); + for (std::size_t i = 0; i < attributes.size(); ++i) { + const auto& attribute = attributes[i]; + if (NullVertexDescriptor(attribute) || VertexFetchOutOfRange(attribute)) { + zeroed[i] = zeroVertexBuffer(context, attribute); + continue; + } // An indirect draw's counts are unknown here: the descriptor's whole range is copied. const auto bytes = args != nullptr ? VertexBufferExtent(attribute) : VertexBufferReadSize(attribute, inputs.maxIndex, draw.instanceCount, draw.firstInstance); const auto& fields = attribute.resource.fields; const auto address = fields[0] | (static_cast(fields[1] & 0xffffu) << 32u); - Require(!state.hasColorTarget || address + bytes <= state.color.address || state.color.address + state.color.bytes <= address, "vertex buffer aliases the render target"); - fetches.push_back({address, address + bytes, (fields[1] >> 16u) & 0x3fffu, attribute.fetchIndex, DecodeVertexFormat(attribute).alignment}); + const auto firstByte = args == nullptr ? VertexBufferReadOffset(attribute, draw.firstVertex, draw.firstInstance) : 0; + Require(!state.hasColorTarget || address + bytes <= state.color.address || state.color.address + state.color.bytes <= address + firstByte, "vertex buffer aliases the render target"); + const auto stride = (fields[1] >> 16u) & 0x3fffu; + fetchOf[i] = fetches.size(); + fetches.push_back({address, address + bytes, stride, attribute.fetchIndex, DecodeVertexFormat(attribute).alignment}); + const auto recordBytes = static_cast(fields[2]) * stride; + fetchValid.push_back(args == nullptr && stride != 0 ? static_cast(std::min(recordBytes, static_cast(bytes))) : bytes); } - const auto plan = PlanVertexCopies(fetches); - for (const auto& [begin, end] : plan.copies) { + const auto soloFetches = SoloZeroPaddedFetchIndices(fetches, fetchValid); + std::vector isSolo(fetches.size(), false); + for (const auto f : soloFetches) isSolo[f] = true; + std::vector plannedFetches; + plannedFetches.reserve(fetches.size()); + std::vector plannedOf(fetches.size(), 0); + for (std::size_t f = 0; f < fetches.size(); ++f) { + if (isSolo[f]) continue; + plannedOf[f] = plannedFetches.size(); + plannedFetches.push_back(fetches[f]); + } + const auto plan = PlanVertexCopies(plannedFetches); + std::vector rangeHandles; + std::vector rangeOffsets; + rangeHandles.reserve(plan.copies.size()); + rangeOffsets.reserve(plan.copies.size()); + for (std::size_t range = 0; range < plan.copies.size(); ++range) { + const auto [begin, end] = plan.copies[range]; const auto bytes = static_cast(end - begin); GuestMemory::CheckRange(reinterpret_cast(begin), bytes, 1); + 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(HostImportOffset(*import, begin)); + inputs.inPlaceReads.emplace_back(begin, end); + continue; + } auto copy = CopyDrawInput(context, context.recorder, begin, bytes, 1, Recorder::SnapshotUse::Vertex); KeepDrawInput(context.recorder, begin, copy, Recorder::SnapshotUse::Vertex, 0); + rangeHandles.push_back(copy.buffer->Handle()); + rangeOffsets.push_back(0); + inputs.vertexBuffers.push_back(std::move(copy.buffer)); + } + std::vector soloHandles(fetches.size(), VK_NULL_HANDLE); + 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], fetches[f].stride); + soloHandles[f] = copy.buffer->Handle(); inputs.vertexBuffers.push_back(std::move(copy.buffer)); } for (std::size_t i = 0; i < attributes.size(); ++i) { - inputs.vertexHandles.push_back(inputs.vertexBuffers[plan.copyOf[i]]->Handle()); - inputs.vertexOffsets[i] = plan.offsets[i]; + if (zeroed[i] != nullptr) { + inputs.vertexHandles.push_back(zeroed[i]->Handle()); + inputs.vertexOffsets[i] = 0; + inputs.vertexBuffers.push_back(std::move(zeroed[i])); + continue; + } + const auto f = fetchOf[i]; + if (isSolo[f]) { + inputs.vertexHandles.push_back(soloHandles[f]); + inputs.vertexOffsets[i] = 0; + continue; + } + const auto j = plannedOf[f]; + inputs.vertexHandles.push_back(rangeHandles[plan.copyOf[j]]); + inputs.vertexOffsets[i] = rangeOffsets[plan.copyOf[j]] + plan.offsets[j]; } timer.phase(PhaseVertex); return inputs; @@ -924,7 +1386,7 @@ std::shared_ptr refreshResidentTarget(const Context& context, co std::lock_guard lock(reportMutex); if (reported.insert(color.address).second) std::fprintf(stderr, "[gpu] color target 0x%llx stays non-resident: %s\n", static_cast(color.address), error.what()); } - if (resident != nullptr) materializeCmaskClear(context, color, resident.get()); + if (resident != nullptr) materializeCmaskClear(context, color, resident); if (profile) { const auto lookupUs = std::chrono::duration(std::chrono::steady_clock::now() - lookupStart).count(); ++outcome.targetLookups; @@ -964,9 +1426,9 @@ ResolvedResources resolveDrawResources(const Context& context, const State& stat // The render target and index ranges stay out of the key (see DrawResourceKey); a hit repeats // the alias checks instead. Debug aid: APS5_NO_DRAW_KEY_TRIM=1 keys them as before. static const bool trimKey = std::getenv("APS5_NO_DRAW_KEY_TRIM") == nullptr; - resolved.cacheable = recordable && !noDrawResourceCache && !noTextureCache && std::all_of(shaders.begin(), shaders.end(), [](const CompiledShader& shader) { return shader.program != nullptr && shader.program->PipelineVariantId() != 0; }); + resolved.cacheable = recordable && !noDrawResourceCache && !noTextureCache && std::all_of(shaders.begin(), shaders.end(), [](const CompiledShader& shader) { return shader.program != nullptr && shader.program->PipelineVariantId() != 0; }) && !ShaderResources::NeverReusable(shaders); if (resolved.cacheable) { - resolved.contentKey = DrawResourceKey(context, shaders, state.color, draw.indexAddress, indexBytes, !trimKey); + resolved.contentKey = DrawResourceKey(context, shaders, state.color, static_cast(state.colors.size()), draw.indexAddress, indexBytes, !trimKey); if (auto cached = SharedResourceCache().Find(resolved.contentKey)) { const bool valid = cached->Revalidate(shaders); auto* recorder = Recorder::Active(); @@ -990,7 +1452,7 @@ ResolvedResources resolveDrawResources(const Context& context, const State& stat } timer.phase(PhaseLookup); if (resolved.resources == nullptr) { - resolved.resources = std::make_shared(context, shaders, state.color, draw.indexAddress, static_cast(indexBytes), snapshots); + resolved.resources = std::make_shared(context, shaders, state.color, static_cast(state.colors.size()), draw.indexAddress, static_cast(indexBytes), snapshots); resolved.built = &resolved.resources->Timing(); outcome.addressBased = resolved.resources->HoldsLease(); outcome.kind = outcome.addressBased ? KindBda : KindBuild; @@ -1015,8 +1477,8 @@ 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) { - if (context.recorder != nullptr) context.recorder->NoteSampledDraw(); +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) { context.Function("vkCmdDrawMeshTasksIndirectEXT")(commands, argumentBuffer, argumentOffset, 1, sizeof(VkDrawMeshTasksIndirectCommandEXT)); @@ -1028,14 +1490,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 (draw.indexed) context.Resolved(&DeviceFunctions::cmdBindIndexBuffer, "vkCmdBindIndexBuffer")(commands, inputs.indices->Handle(), 0, draw.indexSize == 2 ? VK_INDEX_TYPE_UINT16 : VK_INDEX_TYPE_UINT32); + if (state.quadList) { + context.Resolved(&DeviceFunctions::cmdBindIndexBuffer, "vkCmdBindIndexBuffer")(commands, inputs.indexHandle, inputs.indexOffset, VK_INDEX_TYPE_UINT32); + context.Resolved(&DeviceFunctions::cmdDrawIndexed, "vkCmdDrawIndexed")(commands, (draw.indexCount / 4u) * 6u, 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); @@ -1083,7 +1550,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()); @@ -1104,10 +1571,30 @@ bool recordIndirectArguments(const Context& context, VkCommandBuffer commands, R return true; } -std::shared_ptr recordMeshArguments(const Context& context, VkCommandBuffer commands, Recorder* recorder, bool recorded, const State& state, const Pm4::DrawParameters& draw, const IndirectRecord& indirect, const std::function& countBarrier) { +std::shared_ptr meshArgumentBuffer(const Context& context, std::span restartTable) { + const auto tableBytes = restartTable.empty() ? 0u : sizeof(std::uint32_t) * (restartTable.size() + 1u); + auto arguments = std::make_shared(context, ShaderRecompiler::MeshArgumentBytes + tableBytes, VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); + if (!restartTable.empty()) { + Require(restartTable.size() <= std::numeric_limits::max(), "mesh restart table is too large"); + const auto length = static_cast(restartTable.size()); + auto* bytes = arguments->Bytes().data() + ShaderRecompiler::MeshArgumentBytes; + std::memcpy(bytes, &length, sizeof(length)); + std::memcpy(bytes + sizeof(length), restartTable.data(), restartTable.size_bytes()); + } + return arguments; +} + +std::shared_ptr directMeshArguments(const Context& context, const Pm4::DrawParameters& draw, std::span restartTable) { + auto arguments = meshArgumentBuffer(context, restartTable); + const MeshArguments resolved{0u, 0u, 0u, draw.indexCount, 0u}; + std::memcpy(arguments->Bytes().data(), &resolved, sizeof(resolved)); + return arguments; +} + +std::shared_ptr recordMeshArguments(const Context& context, VkCommandBuffer commands, Recorder* recorder, bool recorded, const State& state, const Pm4::DrawParameters& draw, const IndirectRecord& indirect, const std::function& countBarrier, std::span restartTable) { const auto* args = indirect.args; const auto rules = MeshArgumentRulesFor(context, *state.stages.mesh, draw.indexCount); - auto arguments = std::make_shared(context, ShaderRecompiler::MeshArgumentBytes, VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); + auto arguments = meshArgumentBuffer(context, restartTable); if (indirect.path == IndirectDrawPath::Gpu && recorder != nullptr && indirect.argumentImport->address != 0) { const std::array words{rules.indexCount, rules.inputSize, rules.step, rules.primitivesPerGroup, rules.maxGroups, rules.maxInstances, rules.maxTotal}; if (recorder->RecordMeshArguments(commands, indirect.argumentImport->address + (args->arguments - indirect.argumentImport->base), arguments->DeviceAddress(), words)) { @@ -1163,7 +1650,7 @@ struct Kept { // The completion side of a recorded draw: the kept objects, the record check, the lease outcome, // the GPU write notes, the copied-buffer write-back (listed in DrawCopiedWriters or run as a // completion action) and the targets marked dirty. -void keepRecordedDraw(Recorder& recorder, const std::shared_ptr& resources, std::shared_ptr pipeline, std::shared_ptr framebuffer, DrawInputs& inputs, std::vector> targets, std::unique_ptr scratch, std::function checkRecords, bool listed, bool completion, const DrawOutcome& outcome) { +void keepRecordedDraw(Recorder& recorder, const std::shared_ptr& resources, std::shared_ptr pipeline, std::shared_ptr framebuffer, DrawInputs& inputs, std::vector> targets, std::vector> slices, std::unique_ptr scratch, std::function checkRecords, bool listed, bool completion, const DrawOutcome& outcome) { auto kept = std::make_shared(); kept->resources = resources; kept->pipeline = std::move(pipeline); @@ -1173,7 +1660,12 @@ 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)); // Counted for the [address-sync] line: a lease released by the completion (the pin waiter // finishes the recorder up to the open batch, which holds the kept resources and gets the @@ -1183,7 +1675,7 @@ void keepRecordedDraw(Recorder& recorder, const std::shared_ptr resources->MarkGpuWrites(recorder); // The write-back (fault check, copied buffers) runs when the batch completed; a fault is // reported by the recorder ("deferred write-back failed") instead of thrown out of the draw. - if (listed) { + if (listed && resources->HasCopiedWrites()) { // As VulkanDevice::dispatch lists its copied writers: delisted before the write-back // (one that fails must not keep indirect dispatches on the CPU), listed after the // registration (a throw there leaves nothing behind). @@ -1194,10 +1686,17 @@ void keepRecordedDraw(Recorder& recorder, const std::shared_ptr }); writers->push_back(resources); } else if (completion) { - recorder.OnComplete([resources] { resources->WriteBackBuffers(); }); + if (resources->Reusable()) recorder.OnComplete([resources] { resources->CheckFaults(); }); + else recorder.OnComplete([resources] { resources->WriteBackBuffers(); }); + } + for (std::size_t index = 0; index < residents.size(); ++index) { + if (residents[index] == nullptr) continue; + 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->MarkDirty(); + if (resident == nullptr || resident->Descriptor().tileMode != TextureTileMode::kLinear) continue; + StorageTexture::FlushPending(resident->Descriptor().baseAddress, static_cast(resident->GuestBytes()), nullptr, "linear target"); } } @@ -1212,7 +1711,10 @@ struct RecordedDraw { std::shared_ptr framebuffer; std::span targetViews; std::vector> targets; + std::vector> targetSlices; + std::vector> multisampleTargets; std::vector> proxies; + std::vector multisampleImages; const IndirectRecord* indirect = nullptr; std::span moved; bool listed = false; @@ -1220,22 +1722,81 @@ struct RecordedDraw { bool waited = false; const std::array* pushBytes = nullptr; VkShaderStageFlags pushStages = 0; + std::vector>> stencilReplay; + std::vector reordered; + SampleOrderPass reorderPass; + std::optional depthLocations; }; -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) { +// 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}; + static_assert(ShaderRecompiler::TessellationLevelPushOffsetBytes + ShaderRecompiler::TessellationLevelPushBytes == PipelinePushSlotBytes); + std::memcpy(block.data() + ShaderRecompiler::TessellationLevelPushOffsetBytes, levels.data(), sizeof(levels)); + } if (!state.stages.mesh) { pipeline.PushConstants(commands, stages, block); return; } const auto firstVertex = draw.indirect ? draw.indirect->vertexConstant : draw.firstVertex; const auto firstInstance = draw.indirect ? draw.indirect->instanceConstant : draw.firstInstance; - const std::array words{draw.indexCount, firstVertex, firstInstance, draw.indexed ? draw.indexSize : 0u, static_cast(meshArguments), static_cast(meshArguments >> 32u)}; - static_assert(ShaderRecompiler::MeshDrawPushOffsetBytes + ShaderRecompiler::MeshDrawPushBytes == PipelinePushConstantBytes); + const std::array words{draw.indexCount, firstVertex, firstInstance, draw.indexed ? draw.indexSize | (restartTable ? ShaderRecompiler::MeshIndexRestartTable : 0u) : 0u, static_cast(meshArguments), static_cast(meshArguments >> 32u)}; + static_assert(ShaderRecompiler::MeshDrawPushOffsetBytes + ShaderRecompiler::MeshDrawPushBytes == PipelinePushSlotBytes); std::memcpy(block.data() + ShaderRecompiler::MeshDrawPushOffsetBytes, words.data(), sizeof(words)); pipeline.PushConstants(commands, stages | VK_SHADER_STAGE_MESH_BIT_EXT, block); } @@ -1277,22 +1838,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; @@ -1324,6 +1885,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; @@ -1335,18 +1901,7 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam // within the open batch) and the extent. The previous draw's pass is continued only when this // draw neither reads its attachments (a barrier would be owed, which no pass allows) nor // records anything outside a pass (an indirect draw's argument barrier and scratch copies). - std::uint64_t passKey = 14695981039346656037ull; - const auto mix = [&](std::uint64_t value) { - passKey ^= value; - passKey *= 1099511628211ull; - }; - for (const auto view : record.targetViews) mix(reinterpret_cast(view)); - if (state.blends.size() != state.colors.size()) { - mix(state.blends.size()); - for (const auto& color : state.colors) mix(color.exportIndex); - } - mix(state.renderExtent.width); - mix(state.renderExtent.height); + const auto passKey = DrawRenderPassKey(context, state, record.targetViews); const bool readsTarget = std::any_of(record.targets.begin(), record.targets.end(), [&](const std::shared_ptr& target) { return resources.ReadsImage(target.get()); }); // A queued DCC key store over memory the draw writes or reads in place (unknown for an // address-based build), or over its GPU-side records, lands before it, as before a dispatch @@ -1357,28 +1912,87 @@ 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 continued = !capture && !readsTarget && !gpuIndirect && !meshIndirect && recorder->ContinuesRenderPass(passKey); + const bool addressBased = resources.HoldsLease(); + 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(); + 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; - const auto commands = continued ? recorder->CommandsInRenderPass() : recorder->Commands(); + const auto commands = start.commands; + if (!continued) recorder->PrepareSampleSlot(); if (capture) captureInputs(context, *recorder, commands, resources, drawBindings, record.targets.empty() || record.targets.front() == nullptr ? 0 : record.targets.front()->Descriptor().baseAddress); // The draw's [gputime] class range: from its first barrier to the pass's trailing barrier (a // continued draw lies inside its pass's range). const auto drawTiming = !continued ? recorder->BeginGpuTiming(CommandClass::Draw) : Recorder::NoTiming; if (!continued && Recorder::BarrierValidate()) { auto reads = resources.InPlaceReads(); + reads.insert(reads.end(), inputs.inPlaceReads.begin(), inputs.inPlaceReads.end()); if (gpuIndirect) { reads.emplace_back(args->arguments, args->arguments + args->RangeBytes()); if (args->countIndirect) reads.emplace_back(args->countAddress, args->countAddress + 4); } auto images = resources.StorageImages(); - for (const auto& target : record.targets) images.emplace_back(target->Image(), true); + for (const auto& target : record.targets) { + if (target != nullptr) images.emplace_back(target->Image(), true); + } recorder->NoteAccess(CommandClass::Draw, Recorder::Access{reads, resources.GpuWrites(), images, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_VERTEX_INPUT_BIT | inputs.shaderStages, resources.HoldsLease()}); } std::unique_ptr scratch; @@ -1386,6 +2000,7 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam VkBuffer argumentBuffer = VK_NULL_HANDLE; VkDeviceSize argumentOffset = 0; bool rewritten = false; + if (!meshIndirect && !inputs.restartTable.empty()) meshArguments = directMeshArguments(context, draw, inputs.restartTable); if (continued) { record.pipeline->Continue(commands, state); } else { @@ -1393,51 +2008,83 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam proxy->RecordAttachmentProxyLoad(commands, VK_IMAGE_LAYOUT_GENERAL); countBarrier(2); } - const VkMemoryBarrier before{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, 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_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_INDEX_READ_BIT | VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT | VK_ACCESS_UNIFORM_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT}; - context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_VERTEX_INPUT_BIT | inputs.shaderStages, 0, 1, &before, 0, nullptr, 0, nullptr); - countBarrier(1); + if (start.barrier) { + const VkMemoryBarrier before{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, 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_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_INDEX_READ_BIT | VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT | VK_ACCESS_UNIFORM_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT}; + context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT, 0, 1, &before, 0, nullptr, 0, nullptr); + 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); + meshArguments = recordMeshArguments(context, commands, recorder, true, state, draw, *record.indirect, countBarrier, inputs.restartTable); argumentBuffer = meshArguments->Handle(); } else if (gpuIndirect) { rewritten = recordIndirectArguments(context, commands, recorder, true, *record.indirect, scratch, argumentBuffer, argumentOffset, countBarrier); } // 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); } APS5_LOG_CHARS_OUT_DEBUG("Pipeline Begin OK"); if (drawBindings != nullptr) { const auto set = drawBindings->allocation.set; - context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, record.pipeline->Layout(), 0, 1, &set, 0, nullptr); + const auto fragmentSet = drawBindings->fragmentAllocation.set; + if (set != VK_NULL_HANDLE) context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, record.pipeline->Layout(), 0, 1, &set, 0, nullptr); + if (fragmentSet != VK_NULL_HANDLE) context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, record.pipeline->Layout(), ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet, 1, &fragmentSet, 0, nullptr); } else { resources.Bind(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, record.pipeline->Layout()); } APS5_LOG_CHARS_OUT_DEBUG("Resources bound"); - pushDrawConstants(*record.pipeline, commands, state, draw, shaders, resources, record.pushBytes, record.pushStages, meshArguments != nullptr ? meshArguments->DeviceAddress() : 0); + 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); + } + if (record.pipeline->SplitsSecondChannel()) { + record.pipeline->ContinueSecondChannel(commands); + recordDrawCommands(context, commands, state, draw, inputs, record.indirect, argumentBuffer, argumentOffset); + } if (meshArguments != nullptr) recorder->Keep(meshArguments); if (args != nullptr) CountIndirectDraw(record.indirect->path, record.indirect->readMs, rewritten); auto checkRecords = indirectRecordCheck(record.indirect); APS5_LOG_CHARS_OUT_DEBUG("Draw recorded"); - // The pass stays open for the next draw of these attachments; the recorder ends it (and the - // draw class range) before anything else is recorded. A draw that wrote memory owes the next - // one a barrier, so its pass cannot be continued. 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, !resources.WritesMemory(), std::move(storeProxies)); + recorder->LeaveRenderPassOpen(passKey, drawTiming, fenced, passAccess, std::move(storeProxies)); timer.phase(PhaseRecord); - keepRecordedDraw(*recorder, record.resources, std::move(record.pipeline), std::move(record.framebuffer), inputs, std::move(record.targets), std::move(scratch), std::move(checkRecords), record.listed, record.completion, outcome); + keepRecordedDraw(*recorder, record.resources, std::move(record.pipeline), std::move(record.framebuffer), inputs, std::move(record.targets), std::move(record.targetSlices), std::move(scratch), std::move(checkRecords), record.listed, record.completion, outcome); timer.phase(PhaseKeep); if (record.waited) { // The wait the dispatch path makes for such work (source 3, "address-based"): the @@ -1454,16 +2101,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() { @@ -1484,7 +2139,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(); @@ -1493,6 +2148,31 @@ std::optional KnownValidationFailure(const Context& context, std::s } void Draw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span shaders, std::span snapshots, std::shared_ptr* recipeOut) { + std::vector 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) recipeOut = nullptr; + if (context.recorder != nullptr) context.recorder->BoundKeptBytes(); PerformanceTimer timing("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; @@ -1534,6 +2214,9 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& std::vector original; TileMipLayout mip{}; std::unique_ptr target; + bool multisampled = false; + VkImage multisampleImage = VK_NULL_HANDLE; + std::shared_ptr multisampleOwner; // APS5_DUMP_TARGETS: the rendered linear pixels, read back for target_
_.raw. std::unique_ptr dump; }; @@ -1547,13 +2230,24 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& binding.color = state.colors[index]; const auto& color = binding.color; binding.gpuTiling = color.tileMode == ColorTileMode::RenderTarget && context.detiler != nullptr; + if (color.samples > 1) { + binding.multisampled = true; + timer.phase(PhaseSetup); + 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())); + 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 @@ -1561,6 +2255,18 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& // APS5_PACKET_EPOCH=1); the key scan runs on every lookup regardless. Lookups of a // millisecond or more are counted apart; the [texture] line says how many uploaded. binding.resident = refreshResidentTarget(context, state, color, outcome, profile, [&] { + 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); + // 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 = CachedStorageSurface(context, array); + Require(resident->Attachable(), "storage format cannot be a color attachment"); + return resident; + } auto resident = CachedStorageSurface(context, SurfaceForTarget(color)); Require(resident->Attachable(), "storage format cannot be a color attachment"); Require(color.mipCount > 1 || color.depth > 1 || resident->GuestBytes() == colorLayout.Bytes(), "resident image layout differs from the color layout"); @@ -1570,14 +2276,16 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& if (binding.resident != nullptr) { timer.phase(PhaseReadTarget); binding.proxied = AttachmentProxyFormat(context, color.format) != VK_FORMAT_UNDEFINED; - targetViews.push_back(binding.proxied ? binding.resident->AttachmentProxyView() : binding.resident->AttachmentView(color.format, color.mip, color.depthSlice)); + targetViews.push_back(binding.proxied ? binding.resident->AttachmentProxyView() : binding.resident->AttachmentView(color.format, color.mip, color.depthSlice, color.layers, color.arraySlice)); continue; } materializeCmaskClear(context, color, nullptr); - Require(!color.mipTail, "rendering into a packed mip tail needs the resident image of its surface"); + 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 = ColorTargetMip(color, colorLayout); + binding.mip = color.mipTail ? ComputeColorTargetMipLayout(color.tileMode, color.elementBytes, color.surfaceExtent.width, color.surfaceExtent.height, color.mipCount, color.mip) : ColorTargetMip(color, colorLayout); binding.tiled = std::make_unique(context, colorLayout.Bytes(), copies); binding.tiledDevice = std::make_unique(context, colorLayout.Bytes(), copies | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); binding.linearDevice = std::make_unique(context, colorLayout.LinearBytes(), copies | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); @@ -1608,13 +2316,39 @@ 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)); 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; }); + 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& 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; @@ -1641,7 +2375,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& return IndirectDrawPath::PendingImage; } if (recorder != nullptr && recorder->PendingLabelIn(address, bytes)) return IndirectDrawPath::PendingLabelOrCopy; - const auto overlaps = [&](const auto& writer) { return writer->WritesOverlap(address, bytes); }; + const auto overlaps = [&](const auto& writer) { return writer->CopiedWritesOverlap(address, bytes); }; if (context.copiedWriters != nullptr && std::any_of(context.copiedWriters->begin(), context.copiedWriters->end(), overlaps)) return IndirectDrawPath::PendingLabelOrCopy; if (std::any_of(DrawCopiedWriters()->begin(), DrawCopiedWriters()->end(), overlaps)) return IndirectDrawPath::PendingLabelOrCopy; if ((import = HostImportFor(context, address, bytes)) == nullptr) return IndirectDrawPath::NotImported; @@ -1723,10 +2457,16 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& // 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; }); + 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, draw.indexAddress, static_cast(indexBytes), snapshots); + 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); @@ -1738,13 +2478,45 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& masked->blends.at(state.colors[index].exportIndex).colorWriteMask &= VK_COLOR_COMPONENT_R_BIT; } const State& pipelineState = masked.has_value() ? *masked : state; - auto pipeline = CachedPipeline(context, pipelineState, inputs.vertexInput, *resources, shaders, lean ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + static const bool verifySetLayouts = std::getenv("APS5_VERIFY_SET_LAYOUTS") != nullptr; + if (verifySetLayouts && context.descriptorCache != nullptr) { + const auto described = DescribeSetLayouts(context, shaders); + 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"); + } + std::vector>> 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 = 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; owners.reserve(targets.size()); - for (const auto& binding : targets) owners.push_back(binding.resident); - auto framebuffer = pipeline->AcquireFramebuffer(targetViews, owners, state.renderExtent); + std::vector> framebufferOwners; + framebufferOwners.reserve(targets.size()); + for (const auto& binding : targets) { + owners.push_back(binding.resident); + framebufferOwners.push_back(binding.multisampled ? binding.multisampleOwner : binding.resident); + } + auto framebuffer = pipeline->AcquireFramebuffer(targetViews, framebufferOwners, state.renderExtent, state.renderLayers); + std::vector reordered; + SampleOrderPass 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"); + } + } + const auto depthLocations = passDepthLocations(context, pipelineState, *pipeline); timer.phase(PhasePipeline); APS5_LOG_CHARS_OUT_DEBUG("Pipeline created"); if (lean) { @@ -1756,19 +2528,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.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() && !state.depth && resolved.moved.empty()) { + 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; @@ -1777,12 +2558,8 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& recipe->framebuffer = framebuffer; for (const auto& owner : owners) recipe->targets.emplace_back(owner); recipe->targetViews = targetViews; - std::uint64_t passKey = 14695981039346656037ull; - for (const auto view : targetViews) passKey = (passKey ^ reinterpret_cast(view)) * 1099511628211ull; - passKey = (passKey ^ state.renderExtent.width) * 1099511628211ull; - passKey = (passKey ^ state.renderExtent.height) * 1099511628211ull; - recipe->passKey = passKey; - recipe->vertexInput = inputs.vertexInput; + recipe->passKey = DrawRenderPassKey(context, state, targetViews); + recipe->vertexInput = inputs.VertexLayout(); recipe->pushStages = PushConstantStages(shaders); if (recipe->pushStages != 0) { recipe->pushBytes = AssemblePushConstants(shaders); @@ -1791,6 +2568,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& recipe->masked = masked; recipe->fragmentOutputs = inputs.fragmentOutputs; recipe->shaderStages = inputs.shaderStages; + recipe->hasDepth = state.depth.has_value(); *recipeOut = std::move(recipe); } timing.Mark("draw_and_resource_release"); @@ -1820,11 +2598,13 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& // A queued label store over such memory likewise (Recorder::RecordStore). if (recorded && recorder->HasQueuedStores() && (resources->HoldsLease() || recorder->AnyQueuedStore(touches))) recorder->FlushStores(); const auto commands = recorded ? recorder->Commands() : batch->Handle(); + if (recorded) recorder->PrepareSampleSlot(); APS5_LOG_CHARS_OUT_DEBUG("CommandBatch created"); // The draw's [gputime] class range: from its first barrier to the download barrier. const auto drawTiming = recorded ? recorder->BeginGpuTiming(CommandClass::Draw) : Recorder::NoTiming; if (recorded && Recorder::BarrierValidate()) { auto reads = resources->InPlaceReads(); + reads.insert(reads.end(), inputs.inPlaceReads.begin(), inputs.inPlaceReads.end()); if (gpuIndirect) { reads.emplace_back(args->arguments, args->arguments + args->RangeBytes()); if (args->countIndirect) reads.emplace_back(args->countAddress, args->countAddress + 4); @@ -1849,10 +2629,16 @@ 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); + meshArguments = recordMeshArguments(context, commands, recorder, recorded, state, draw, indirect, countBarrier, inputs.restartTable); argumentBuffer = meshArguments->Handle(); + } else if (!inputs.restartTable.empty()) { + meshArguments = directMeshArguments(context, draw, inputs.restartTable); } else if (gpuIndirect) { rewritten = recordIndirectArguments(context, commands, recorder, recorded, indirect, scratch, argumentBuffer, argumentOffset, countBarrier); } @@ -1863,7 +2649,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; } @@ -1875,26 +2661,50 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& if (binding.gpuTiling) { CopyBuffer(context, commands, binding.tiled->Handle(), 0, binding.tiledDevice->Handle(), 0, binding.original.size()); memoryBarrier(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); - context.detiler->Dispatch(commands, TextureTileMode::kR64KBX, binding.color.elementBytes, binding.tiledDevice->Handle(), 0, binding.linearDevice->Handle(), 0, binding.mip); + context.detiler->Dispatch(commands, TextureTileMode::kR64KBX, binding.color.elementBytes, binding.tiledDevice->Handle(), 0, binding.linearDevice->Handle(), 0, binding.mip, false, 0, false, {.pipeBankXor = binding.color.pipeBankXor}); memoryBarrier(context, commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); } imageBarrier(context, commands, binding.target->Image(), 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.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"); resources->Bind(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline->Layout()); APS5_LOG_CHARS_OUT_DEBUG("Resources bound"); - pushDrawConstants(*pipeline, commands, state, draw, shaders, *resources, nullptr, 0, meshArguments != nullptr ? meshArguments->DeviceAddress() : 0); + 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); + } + if (pipeline->SplitsSecondChannel()) { + pipeline->ContinueSecondChannel(commands); + recordDrawCommands(context, commands, state, draw, inputs, args != nullptr ? &indirect : nullptr, argumentBuffer, argumentOffset); + } if (meshArguments != nullptr && recorded) recorder->Keep(meshArguments); if (args != nullptr) CountIndirectDraw(indirect.path, indirect.readMs, rewritten); auto checkRecords = indirectRecordCheck(args != nullptr ? &indirect : nullptr); APS5_LOG_CHARS_OUT_DEBUG("Draw recorded"); - context.Resolved(&DeviceFunctions::cmdEndRenderPass, "vkCmdEndRenderPass")(commands); + 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) { @@ -1903,7 +2713,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; } @@ -1931,7 +2741,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& CopyBuffer(context, commands, binding.linearDevice->Handle(), 0, binding.dump->Handle(), 0, binding.linearDevice->Size()); } } - context.detiler->Dispatch(commands, TextureTileMode::kR64KBX, binding.color.elementBytes, binding.linearDevice->Handle(), 0, binding.tiledDevice->Handle(), 0, binding.mip, true); + context.detiler->Dispatch(commands, TextureTileMode::kR64KBX, binding.color.elementBytes, binding.linearDevice->Handle(), 0, binding.tiledDevice->Handle(), 0, binding.mip, true, 0, false, {.pipeBankXor = binding.color.pipeBankXor}); memoryBarrier(context, commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); CopyBuffer(context, commands, binding.tiledDevice->Handle(), 0, binding.tiled->Handle(), 0, binding.original.size()); } @@ -1946,7 +2756,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& APS5_LOG_CHARS_OUT_DEBUG("SubmitAndWait"); timer.phase(PhaseRecord); if (recorded) { - keepRecordedDraw(*recorder, resources, pipeline, framebuffer, inputs, owners, std::move(scratch), std::move(checkRecords), listed, completion, outcome); + keepRecordedDraw(*recorder, resources, pipeline, framebuffer, inputs, owners, [&] { std::vector> slices; for (const auto& binding : targets) slices.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 @@ -1965,6 +2775,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(); @@ -1991,7 +2802,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& if (skipTargetWrite) break; if (binding.resident != nullptr) { // The results stay on the GPU until something reads the target's memory. - binding.resident->MarkDirty(); + binding.resident->MarkSliceDirty(binding.color.arraySlice, binding.color.arrayLastSlice != 0 ? binding.color.layers : 1u); continue; } if (binding.gpuTiling) GuestMemory::WriteChanged(binding.color.address, binding.tiled->Bytes(), binding.original); @@ -2033,6 +2844,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& } 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); PerformanceTimer timing("Graphics.DrawWithRecipe"); static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; @@ -2053,8 +2865,10 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con }; Require(!draw.indirect, "a draw recipe covers direct draws only"); auto* recorder = Recorder::Active(); - if (!RecordDraws() || recorder == nullptr || DumpTargetLimit() != 0) return miss(DrawRecipeMiss::NotRecordable); - Require(recipe.targets.size() == state.colors.size() && recipe.targetViews.size() == state.colors.size(), "draw recipe targets do not match the state"); + if (state.quadList || !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"); auto inputs = prepareDrawInputs(context, state, draw, shaders, outcome, timer, &recipe); if (inputs.nothing) { result.recorded = true; @@ -2080,6 +2894,12 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con if (resident == nullptr) return miss(DrawRecipeMiss::TargetGone); targets.push_back(std::move(resident)); } + if (recipe.hasDepth) { + timer.phase(PhaseSetup); + const auto depthView = DepthSurfaceView(context, *state.depth, state.depthWrite); + timer.phase(PhaseReadTarget); + if (recipe.targetViews.back() != depthView) return miss(DrawRecipeMiss::TargetGone); + } timer.phase(PhasePrepare); // The template's proof (rules R6/R7): ProveCurrent, T1 included, the alias checks the trimmed // key leaves to a hit repeated; a failure removes the template from the cache (the batch keeps @@ -2093,6 +2913,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); @@ -2101,7 +2922,7 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con SharedResourceCache().Touch(recipe.key); countCache(&DrawProfile::cacheHits); timer.phase(PhaseLookup); - outcome.completion = false; + outcome.completion = resources->ChecksFaults(); outcome.recorded = true; // The pipeline is the recipe's while the pipeline store still holds it (its device's teardown // or its eviction bound drops it: a miss). The framebuffer is the recipe's while the pipeline's @@ -2110,18 +2931,24 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con auto pipeline = recipe.pipeline.lock(); if (pipeline == nullptr) return miss(DrawRecipeMiss::ObjectsGone); auto framebuffer = recipe.framebuffer.lock(); - if (framebuffer == nullptr) framebuffer = pipeline->AcquireFramebuffer(recipe.targetViews, targets, state.renderExtent); + if (framebuffer == nullptr) { + 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; record.recorder = recorder; + record.completion = outcome.completion; record.resources = std::move(resources); record.pipeline = std::move(pipeline); record.framebuffer = std::move(framebuffer); record.targetViews = recipe.targetViews; record.targets = std::move(targets); + 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(); @@ -2139,32 +2966,46 @@ namespace { std::shared_ptr metadataPassResident(const Context& context, const ColorTarget& color) { if (color.tileMode == ColorTileMode::Linear || context.detiler == nullptr) return nullptr; - std::shared_ptr resident; - try { - resident = CachedStorageSurface(context, SurfaceForTarget(color)); - } catch (const std::exception&) { - return nullptr; - } + const auto resident = CachedStorageSurface(context, SurfaceForTarget(color)); return resident != nullptr && resident->GuestBytes() == color.bytes ? resident : nullptr; } } 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::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); - if (materializeCmaskClear(context, color, resident.get()) && resident != nullptr) resident->MarkDirty(); + if (materializeCmaskClear(context, color, resident) && resident != nullptr) resident->MarkDirty(); } if (color.dccAddress == 0) continue; auto keys = CurrentDccKeys(color.dccAddress, color.bytes); if (keys == DccKeys::Uncompressed) continue; + if (keys == DccKeys::ClearSingle) { + Require(pass.mode == ColorMetadataPass::Mode::DccDecompress, "CB fast-clear eliminate over comp-to-single DCC keys (whether it expands them is not modeled)"); + storeSingleTexels(context, color); + if (const auto resident = metadataPassResident(context, color)) resident->Refresh(); + continue; + } Require(IsDccClear(keys), std::string("CB metadata pass over DCC keys that are ") + DccKeysName(keys) + " (per-block metadata is not modeled)"); const auto texel = clearTexel(color, keys); const auto resident = metadataPassResident(context, color); if (resident != nullptr) { const char* refusal = nullptr; - const bool current = keys == DccKeys::ClearRegister ? clearToTexel(*resident, texel, color.elementBytes, refusal) : StorageTexture::FindPending(color.address, color.bytes) == resident || resident->UploadedKeys() == keys; + const bool current = keys == DccKeys::ClearRegister ? clearToTexel(*resident, texel, color.elementBytes, refusal, true) : StorageTexture::FindPending(color.address, color.bytes) == resident || resident->UploadedKeys() == keys; if (current) { resident->MarkDirty(); MarkDccUncompressed(context, color.dccAddress, color.bytes, colorKeyCount(color, color.bytes)); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp index 346ba590ae..9881ba2473 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp @@ -1,14 +1,19 @@ #include "prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp" #include "prx/libSceAgcDriver/Graphics/shaders/ColorTransfer_spv.h" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/PqToSdr.hpp" +#include "prx/libSceAgcDriver/Graphics/include/HalfFloatScanout.hpp" #include +#include +#include +#include namespace AgcDriver::Graphics { GpuColorTransfer::GpuColorTransfer(const Context& context) : context(context) { VkShaderModule module = VK_NULL_HANDLE; try { - std::array bindings{}; + std::array bindings{}; for (std::uint32_t i = 0; i < bindings.size(); ++i) { bindings[i] = {i, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}; } @@ -16,27 +21,27 @@ GpuColorTransfer::GpuColorTransfer(const Context& context) : context(context) { descriptorInfo.bindingCount = static_cast(bindings.size()); descriptorInfo.pBindings = bindings.data(); Check(context.Function("vkCreateDescriptorSetLayout")(context.device, &descriptorInfo, nullptr, &descriptorLayout), "vkCreateDescriptorSetLayout color transfer"); - const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, 16}; + const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, 20}; VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; layoutInfo.setLayoutCount = 1; layoutInfo.pSetLayouts = &descriptorLayout; 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; pipelineInfo.stage.module = module; pipelineInfo.stage.pName = "main"; pipelineInfo.layout = pipelineLayout; - Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateComputePipelines color transfer"); + { + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateComputePipelines color transfer"); + } context.Function("vkDestroyShaderModule")(context.device, module, nullptr); module = VK_NULL_HANDLE; - const VkDescriptorPoolSize size{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2}; + const VkDescriptorPoolSize size{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 4}; VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; poolInfo.maxSets = 1; poolInfo.poolSizeCount = 1; @@ -47,6 +52,23 @@ GpuColorTransfer::GpuColorTransfer(const Context& context) : context(context) { allocation.descriptorSetCount = 1; allocation.pSetLayouts = &descriptorLayout; Check(context.Function("vkAllocateDescriptorSets")(context.device, &allocation, &descriptorSet), "vkAllocateDescriptorSets color transfer"); + const auto& table = HalfFloatScanoutThresholds(); + thresholds = std::make_unique(context, sizeof(table), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + std::memcpy(thresholds->Bytes().data(), table.data(), sizeof(table)); + const auto& luminance = AgcDriver::PqEotfNits(); + pqNits = std::make_unique(context, sizeof(luminance), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + std::memcpy(pqNits->Bytes().data(), luminance.data(), sizeof(luminance)); + const std::array tableInfo{{{thresholds->Handle(), 0, sizeof(table)}, {pqNits->Handle(), 0, sizeof(luminance)}}}; + std::array tableWrites{}; + for (std::uint32_t i = 0; i < tableWrites.size(); ++i) { + tableWrites[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + tableWrites[i].dstSet = descriptorSet; + tableWrites[i].dstBinding = 2 + i; + tableWrites[i].descriptorCount = 1; + tableWrites[i].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + tableWrites[i].pBufferInfo = &tableInfo[i]; + } + context.Function("vkUpdateDescriptorSets")(context.device, static_cast(tableWrites.size()), tableWrites.data(), 0, nullptr); } catch (...) { if (module) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); release(); @@ -65,16 +87,18 @@ void GpuColorTransfer::release() noexcept { if (descriptorLayout) context.Function("vkDestroyDescriptorSetLayout")(context.device, descriptorLayout, nullptr); } -void GpuColorTransfer::prepare(std::uint32_t newWidth, std::uint32_t newHeight, ColorTileMode newMode) { - const ColorTargetLayout layout(newWidth, newHeight, newMode); - Require(layout.Bytes() <= context.limits.maxStorageBufferRange && layout.LinearBytes() <= context.limits.maxStorageBufferRange, "color transfer exceeds storage buffer limits"); +void GpuColorTransfer::prepare(std::uint32_t newWidth, std::uint32_t newHeight, ColorTileMode newMode, std::uint32_t newElementBytes) { + Require(newElementBytes == 4u || (newElementBytes == 8u && newMode == ColorTileMode::RenderTarget), "color transfer supports 8-byte texels only in the render target layout"); + const ColorTargetLayout layout(newWidth, newHeight, newMode, newElementBytes); + const auto linearBytes = static_cast(newWidth) * newHeight * 4u; + Require(layout.Bytes() <= context.limits.maxStorageBufferRange && linearBytes <= context.limits.maxStorageBufferRange, "color transfer exceeds storage buffer limits"); Require((newWidth + 7u) / 8u <= context.limits.maxComputeWorkGroupCount[0] && (newHeight + 7u) / 8u <= context.limits.maxComputeWorkGroupCount[1], "color transfer exceeds workgroup limits"); - if (tiled && width == newWidth && height == newHeight && mode == newMode) return; + if (tiled && width == newWidth && height == newHeight && mode == newMode && elementBytes == newElementBytes) return; constexpr VkBufferUsageFlags copies = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; auto newTiled = std::make_unique(context, layout.Bytes(), copies); auto newTiledDevice = std::make_unique(context, layout.Bytes(), copies | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); - auto newLinear = std::make_unique(context, layout.LinearBytes(), copies | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); - const std::array buffers{{{newTiledDevice->Handle(), 0, layout.Bytes()}, {newLinear->Handle(), 0, layout.LinearBytes()}}}; + auto newLinear = std::make_unique(context, linearBytes, copies | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + const std::array buffers{{{newTiledDevice->Handle(), 0, layout.Bytes()}, {newLinear->Handle(), 0, linearBytes}}}; std::array writes{}; for (std::uint32_t i = 0; i < writes.size(); ++i) { writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; @@ -91,33 +115,37 @@ void GpuColorTransfer::prepare(std::uint32_t newWidth, std::uint32_t newHeight, width = newWidth; height = newHeight; mode = newMode; + elementBytes = newElementBytes; } -void GpuColorTransfer::Upload(std::uint64_t address, std::uint32_t newWidth, std::uint32_t newHeight, ColorTileMode newMode) { - prepare(newWidth, newHeight, newMode); - const ColorTargetLayout layout(width, height, mode); +void GpuColorTransfer::Upload(std::uint64_t address, std::uint32_t newWidth, std::uint32_t newHeight, ColorTileMode newMode, std::uint32_t newElementBytes) { + prepare(newWidth, newHeight, newMode, newElementBytes); + const ColorTargetLayout layout(width, height, mode, elementBytes); GuestMemory::Read(address, tiled->Bytes(), layout.Alignment()); } -void GpuColorTransfer::convert(VkCommandBuffer commands, bool toTiled, bool swapRedBlue, bool tenBit) { +void GpuColorTransfer::convert(VkCommandBuffer commands, bool toTiled, bool swapRedBlue, bool tenBit, bool halfFloat, std::optional pqWhiteNits) { Require(commands != VK_NULL_HANDLE && tiled && tiledDevice && linear, "color transfer is not prepared"); + Require(halfFloat == (elementBytes == 8u), "color transfer source does not match the prepared texel size"); + Require(!toTiled || !halfFloat, "color transfer cannot write 16-bit float texels back to the guest"); // The guest bytes move to video memory with one DMA copy; the shader's scattered accesses stay local. RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_HOST_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); CopyBuffer(context, commands, tiled->Handle(), 0, tiledDevice->Handle(), 0, tiledDevice->Size()); - dispatch(commands, toTiled, swapRedBlue, tenBit, mode == ColorTileMode::RenderTarget); + dispatch(commands, toTiled, swapRedBlue, tenBit, mode == ColorTileMode::RenderTarget, halfFloat, pqWhiteNits); if (toTiled) { CopyBuffer(context, commands, tiledDevice->Handle(), 0, tiled->Handle(), 0, tiledDevice->Size()); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); } } -void GpuColorTransfer::dispatch(VkCommandBuffer commands, bool toTiled, bool swapRedBlue, bool tenBit, bool tiledSource) { +void GpuColorTransfer::dispatch(VkCommandBuffer commands, bool toTiled, bool swapRedBlue, bool tenBit, bool tiledSource, bool halfFloat, std::optional pqWhiteNits) { + Require(!pqWhiteNits || (!toTiled && !tenBit && !halfFloat && elementBytes == 4u && std::isfinite(*pqWhiteNits) && *pqWhiteNits >= 1.0f), "color transfer converts PQ texels of 4 bytes to an 8-bit linear output with a white level of at least 1 nit"); VkMemoryBarrier before{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; before.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT; before.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT; const auto barrier = context.Function("vkCmdPipelineBarrier"); barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 1, &before, 0, nullptr, 0, nullptr); - const std::array push{width, height, (width + 127u) / 128u, (toTiled ? 1u : 0u) | (swapRedBlue ? 2u : 0u) | (tiledSource ? 4u : 0u) | (tenBit ? 8u : 0u)}; + const std::array push{width, height, (width + 127u) / 128u, (toTiled ? 1u : 0u) | (swapRedBlue ? 2u : 0u) | (tiledSource ? 4u : 0u) | (tenBit ? 8u : 0u) | (halfFloat ? 16u : 0u) | (pqWhiteNits ? 32u : 0u), std::bit_cast(pqWhiteNits.value_or(0.0f))}; context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); context.Function("vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, pipelineLayout, 0, 1, &descriptorSet, 0, nullptr); context.Function("vkCmdPushConstants")(commands, pipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(push), push.data()); @@ -128,20 +156,20 @@ void GpuColorTransfer::dispatch(VkCommandBuffer commands, bool toTiled, bool swa barrier(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &after, 0, nullptr, 0, nullptr); } -void GpuColorTransfer::DetileImage(VkCommandBuffer commands, VkImage image, VkImageLayout layout, std::uint32_t newWidth, std::uint32_t newHeight, ColorTileMode newMode, bool swapRedBlue, bool tenBit) { +void GpuColorTransfer::DetileImage(VkCommandBuffer commands, VkImage image, VkImageLayout layout, std::uint32_t newWidth, std::uint32_t newHeight, ColorTileMode newMode, bool swapRedBlue, bool tenBit, bool halfFloat, std::uint32_t layer) { Require(commands != VK_NULL_HANDLE && image != VK_NULL_HANDLE, "color transfer image source is unavailable"); - prepare(newWidth, newHeight, newMode); - Require(tiledDevice->Size() >= static_cast(width) * height * 4u, "color transfer buffer is smaller than the image"); + prepare(newWidth, newHeight, newMode, halfFloat ? 8u : 4u); + Require(tiledDevice->Size() >= static_cast(width) * height * elementBytes, "color transfer buffer is smaller than the image"); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); VkBufferImageCopy copy{}; - copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, layer, 1}; copy.imageExtent = {width, height, 1}; context.Function("vkCmdCopyImageToBuffer")(commands, image, layout, tiledDevice->Handle(), 1, ©); - dispatch(commands, false, swapRedBlue, tenBit, false); + dispatch(commands, false, swapRedBlue, tenBit, false, halfFloat, std::nullopt); } -void GpuColorTransfer::Detile(VkCommandBuffer commands, bool swapRedBlue, bool tenBit) { - convert(commands, false, swapRedBlue, tenBit); +void GpuColorTransfer::Detile(VkCommandBuffer commands, bool swapRedBlue, bool tenBit, bool halfFloat, std::optional pqWhiteNits) { + convert(commands, false, swapRedBlue, tenBit, halfFloat, pqWhiteNits); } void GpuColorTransfer::Tile(VkCommandBuffer commands) { @@ -150,7 +178,8 @@ void GpuColorTransfer::Tile(VkCommandBuffer commands) { void GpuColorTransfer::WriteBack(std::uint64_t address) { Require(tiled != nullptr, "color transfer is not prepared for writeback"); - const ColorTargetLayout layout(width, height, mode); + Require(elementBytes == 4u, "color transfer cannot write 16-bit float texels back to the guest"); + const ColorTargetLayout layout(width, height, mode, elementBytes); GuestMemory::Write(address, tiled->Bytes(), layout.Alignment()); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 29a657f70f..610c437369 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -1,15 +1,18 @@ #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" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #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 @@ -17,22 +20,30 @@ #include #include #include +#include #include #endif +#include #include +#include +#include #include #include #include #include +#include #include #include +#include #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -54,6 +65,9 @@ struct ImageMirror { std::uint64_t bytes = 0; bool writable = false; bool heap = false; + bool sparse = false; + std::uint64_t reserved = 0; + std::vector> committed; std::vector generations; std::weak_ptr range; std::shared_ptr buffer; @@ -62,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 @@ -95,18 +110,28 @@ struct GuestBufferMemory::AddressSpace { std::vector writableMirrors; std::vector heapMirrors; std::vector heapRuns; + std::vector sparseMirrors; std::unique_ptr[]> verified; 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 { +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}; @@ -132,6 +157,73 @@ bool addressSpaceCacheEnabled() { return !disabled; } +struct ImportFailure { + std::uint64_t base; + std::uint64_t bytes; + 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 { @@ -140,7 +232,16 @@ struct HostImports { PFN_vkDestroyBuffer destroyBuffer = nullptr; PFN_vkFreeMemory freeMemory = nullptr; std::map imports; - std::set failed; + std::map trimmed; + std::map>> parked; + std::uint64_t liveBytes = 0; + std::map failed; + std::map failedJoins; + 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 @@ -149,6 +250,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() { @@ -156,6 +285,57 @@ HostImports& Imports() { return imports; } +#ifdef _WIN32 +void waitSettleBatch(const ImportSettle& settle, VkDevice device, const SettleBatch& batch) { + if (batch.busy && settle.waitForFences != nullptr) static_cast(settle.waitForFences(device, 1, &batch.fence, VK_TRUE, 10'000'000'000ull)); +} + +bool settleBatchRunning(const ImportSettle& settle, VkDevice device, const SettleBatch& batch) { + return batch.busy && settle.waitForFences != nullptr && settle.waitForFences(device, 1, &batch.fence, VK_TRUE, 0) == VK_TIMEOUT; +} + +SettleBatch* forgetSettle(HostImports& state, std::uint64_t base) { + const auto found = state.pendingSettles.find(base); + if (found == state.pendingSettles.end()) return nullptr; + SettleBatch* running = nullptr; + if (const auto settle = state.settles.find(found->second.device); found->second.phase == SettlePhase::Submitted && settle != state.settles.end()) { + auto& batch = settle->second.batches[found->second.batch]; + if (settleBatchRunning(settle->second, settle->first, batch)) running = &batch; + } + state.pendingSettles.erase(found); + return running; +} + +void drainSettles(HostImports& state) { + for (auto& [device, settle] : state.settles) { + for (auto& batch : settle.batches) { + waitSettleBatch(settle, device, batch); + for (const auto alias : batch.aliases) settle.settleDestroyBuffer(device, alias, nullptr); + batch.aliases.clear(); + batch.retired.clear(); + batch.bases.clear(); + batch.busy = false; + } + } + state.pendingSettles.clear(); + state.settleWanted.clear(); + state.settleReady.clear(); +} + +void releaseSettleBatches(VkDevice device, ImportSettle& settle) { + for (auto& batch : settle.batches) { + waitSettleBatch(settle, device, batch); + for (const auto alias : batch.aliases) settle.settleDestroyBuffer(device, alias, nullptr); + if (batch.fence != VK_NULL_HANDLE) settle.destroyFence(device, batch.fence, nullptr); + batch = SettleBatch{}; + } + if (settle.batchTarget != VK_NULL_HANDLE) settle.settleDestroyBuffer(device, settle.batchTarget, nullptr); + if (settle.batchMemory != VK_NULL_HANDLE) settle.settleFreeMemory(device, settle.batchMemory, nullptr); + settle.batchTarget = VK_NULL_HANDLE; + settle.batchMemory = VK_NULL_HANDLE; +} +#endif + void destroyImport(const Context& context, const HostImport& entry) { context.Function("vkDestroyBuffer")(context.device, entry.buffer, nullptr); context.Function("vkFreeMemory")(context.device, entry.memory, nullptr); @@ -164,37 +344,191 @@ void destroyImport(const Context& context, const HostImport& entry) { #endif } +using ImportNode = std::map::node_type; + // Frees a dropped import's Vulkan objects when it is released. Never copied: a copy would destroy the // same handles twice (and a temporary would destroy them at once). struct RetiredImport { - RetiredImport(const Context& context, const HostImport& entry) : context(context), entry(entry) {} + RetiredImport(VkDevice device, PFN_vkDestroyBuffer destroyBuffer, PFN_vkFreeMemory freeMemory, ImportNode node) + : device(device), destroyBuffer(destroyBuffer), freeMemory(freeMemory), node(std::move(node)) {} RetiredImport(const RetiredImport&) = delete; RetiredImport& operator=(const RetiredImport&) = delete; - ~RetiredImport() { destroyImport(context, entry); } - Context context; - HostImport entry; + ~RetiredImport() { + const auto& entry = node.mapped(); + destroyBuffer(device, entry.buffer, nullptr); + freeMemory(device, entry.memory, nullptr); +#ifdef _WIN32 + GuestArena::GuestArenaUnmapAlias_nid_postfix(entry.alias); +#endif + } + VkDevice device; + PFN_vkDestroyBuffer destroyBuffer; + PFN_vkFreeMemory freeMemory; + ImportNode node; }; -// Drops an import. Recorded batches (dispatches, recorded draws, GPU label writes) may still read it, -// so the objects live until the batch open now completed; with no batches in flight all GPU work that -// used it was synchronous and it is destroyed at once. const GuestAllocations::Range* containingRange(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end); +bool runCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end); +bool joinedCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end); -void retireImport(const Context& context, HostImports& state, std::map::iterator it, const GuestAllocations::Lease& lease) { +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(context, it->second, [&lease](std::uint64_t begin, std::uint64_t end) { return containingRange(lease, begin, end) != nullptr; }); - auto holder = std::make_shared(context, it->second); - if (auto* recorder = Recorder::Active(); recorder != nullptr && !recorder->Idle()) recorder->Keep(std::move(holder)); + 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.imports.erase(it); + 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)); + else if (!recorder->Idle()) recorder->Keep(std::move(holder)); +} + +bool releaseParkedImports(HostImports& state, std::uint64_t begin, std::uint64_t end) { + std::vector> released; + { + std::lock_guard lock(state.mutex); + for (auto device = state.parked.begin(); device != state.parked.end();) { + auto& list = device->second; + for (auto it = list.begin(); it != list.end();) { + if ((*it)->node.mapped().base < end && begin < (*it)->node.mapped().base + (*it)->node.mapped().bytes) { + released.push_back(std::move(*it)); + it = list.erase(it); + } else { + ++it; + } + } + device = list.empty() ? state.parked.erase(device) : std::next(device); + } + } + return !released.empty(); +} + +#ifdef _WIN32 +bool releaseSettleRetired(HostImports& state, std::uint64_t begin, std::uint64_t end) { + std::vector> released; + { + std::lock_guard lock(state.mutex); + for (auto& [device, settle] : state.settles) { + for (auto& batch : settle.batches) { + const bool overlaps = std::any_of(batch.retired.begin(), batch.retired.end(), [&](const std::shared_ptr& holder) { return holder->node.mapped().base < end && begin < holder->node.mapped().base + holder->node.mapped().bytes; }); + if (!overlaps) continue; + if (settleBatchRunning(settle, device, batch)) { + ++state.settleUnmapWaits; + waitSettleBatch(settle, device, batch); + } + for (auto& holder : batch.retired) released.push_back(std::move(holder)); + batch.retired.clear(); + } + } + } + return !released.empty(); +} +#endif + +#ifndef _WIN32 +struct ImportFamilies { + std::array indices{}; + std::uint32_t count = 0; +}; + +// A dma-buf import's buffer is used on the main queue, on the settle queue (settleImport) and, when +// sparse, on the sparse queue. +void shareDmaBufImport(const Context& context, VkBufferCreateInfo& info, ImportFamilies& families, bool sparse) { + const auto add = [&](std::uint32_t family) { + if (std::find(families.indices.begin(), families.indices.begin() + families.count, family) == families.indices.begin() + families.count) families.indices[families.count++] = family; + }; + add(context.queueFamily); + if (context.settleQueue != VK_NULL_HANDLE) add(context.settleFamily); + if (sparse && context.sparseQueue != VK_NULL_HANDLE) add(context.sparseFamily); + const bool concurrent = families.count > 1; + info.sharingMode = concurrent ? VK_SHARING_MODE_CONCURRENT : VK_SHARING_MODE_EXCLUSIVE; + info.queueFamilyIndexCount = concurrent ? families.count : 0u; + info.pQueueFamilyIndices = concurrent ? families.indices.data() : nullptr; +} +#endif + +void releaseTrimmed(HostImports& state, ImportNode node) { + state.liveBytes -= node.mapped().pinnedBytes; + auto holder = std::make_shared(state.device, state.destroyBuffer, state.freeMemory, std::move(node)); + auto* recorder = Recorder::Active(); + if (recorder != nullptr) recorder->KeepThroughHold(std::move(holder)); +} + +void dropTrimmed(HostImports& state, std::uint64_t begin, std::uint64_t end) { + for (auto it = state.trimmed.begin(); it != state.trimmed.end();) { + const auto next = std::next(it); + if (it->first < end && begin < it->first + it->second.bytes) releaseTrimmed(state, state.trimmed.extract(it)); + it = next; + } +} + +void retireOrTrim(VkDevice device, HostImports& state, std::map::iterator it, const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) { + const auto& entry = it->second; + const auto entryEnd = entry.base + entry.bytes; + std::uint64_t first = 0; + std::uint64_t last = 0; + if (begin <= entry.base && end > entry.base && end < entryEnd) { + first = end; + last = entryEnd; + } else if (begin > entry.base && begin < entryEnd && end >= entryEnd) { + first = entry.base; + last = begin; + } else if (begin > entry.base && begin < end && end < entryEnd) { + const bool tail = entryEnd - end >= begin - entry.base; + first = tail ? end : entry.base; + last = tail ? entryEnd : begin; + } + const bool plain = entry.alias == nullptr && !entry.dmaBuf && entry.chunk == nullptr && !entry.joined && entry.ranges.size() == 1 && entry.backingSerial != 0 && entry.memoryType != std::numeric_limits::max() && entry.base == entry.range->address && entry.bytes == entry.range->bytes; +#ifdef _WIN32 + const bool settling = state.pendingSettles.contains(it->first); +#else + const bool settling = false; +#endif + if (first == last || !plain || settling || state.trimmed.contains(first)) { + retireImport(device, state, it, lease, false); + return; + } + RetireShadow(device, entry, [&lease](std::uint64_t from, std::uint64_t to) { return runCovers(lease, from, to); }); + ++state.epoch; + auto node = state.imports.extract(it); + auto& kept = node.mapped(); + std::shared_ptr holder(nullptr, [device = state.device, destroy = state.destroyBuffer, buffer = std::exchange(kept.buffer, VK_NULL_HANDLE)](void*) { destroy(device, buffer, nullptr); }); + kept.range.reset(); + kept.ranges.clear(); + kept.base = first; + kept.bytes = last - first; + kept.address = 0; + kept.serial = 0; + node.key() = first; + state.trimmed.insert(std::move(node)); + auto* recorder = Recorder::Active(); + if (recorder != nullptr && !recorder->Idle()) recorder->Keep(std::move(holder)); +} + +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; @@ -205,9 +539,15 @@ const char* bindImport(const Context& context, HostImport& entry, VkExternalMemo VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, &external}; info.size = entry.bytes; // 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; + 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; @@ -218,6 +558,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); @@ -239,7 +580,15 @@ const char* createHostPointerImport(const Context& context, HostImport& entry, V #ifndef _WIN32 int udmabufDevice() { - static const int device = open("/dev/udmabuf", O_RDWR | O_CLOEXEC); + static const int device = [] { + const int result = open("/dev/udmabuf", O_RDWR | O_CLOEXEC); + if (result < 0) { + const int error = errno; + std::fprintf(stderr, "[gpu] open /dev/udmabuf: %s; shared direct memory is copied instead of imported, and GPU stores through FLAT/GLOBAL addresses do not reach it%s\n", + std::strerror(error), error == EACCES ? " (give the user read-write access to /dev/udmabuf, for example through the kvm group)" : ""); + } + return result; + }(); return device; } @@ -271,12 +620,279 @@ const char* createDmaBufImport(const Context& context, HostImport& entry, int fi } #endif +#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; + PFN_vkFreeMemory freeMemory = nullptr; + VkDeviceMemory memory = VK_NULL_HANDLE; + std::uint32_t memoryType = 0; + ImportChunk() = default; + ImportChunk(const ImportChunk&) = delete; + ImportChunk& operator=(const ImportChunk&) = delete; + ~ImportChunk() { + if (memory != VK_NULL_HANDLE) freeMemory(device, memory, nullptr); + } +}; + +struct ImportChunks { + std::mutex mutex; + VkDevice device = VK_NULL_HANDLE; + std::map, std::weak_ptr> chunks; +}; + +ImportChunks& Chunks() { + static ImportChunks chunks; + return chunks; +} + +std::shared_ptr importChunk(const Context& context, int file, std::uint64_t index, std::uint32_t bufferTypes, const char*& step, VkResult& failure) { + struct stat info {}; + if (fstat(file, &info) != 0) { + failure = VK_ERROR_INVALID_EXTERNAL_HANDLE; + step = "fstat of the shared backing"; + return nullptr; + } + auto& chunks = Chunks(); + std::lock_guard lock(chunks.mutex); + if (chunks.device != context.device) { + chunks.chunks.clear(); + chunks.device = context.device; + } + const auto key = std::make_tuple(info.st_dev, info.st_ino, index); + if (const auto found = chunks.chunks.find(key); found != chunks.chunks.end()) { + if (auto chunk = found->second.lock()) return chunk; + chunks.chunks.erase(found); + } + if (static_cast(info.st_size) < (index + 1) * ImportChunkBytes) { + failure = VK_ERROR_INVALID_EXTERNAL_HANDLE; + step = "a chunk past the end of the shared backing"; + return nullptr; + } + udmabuf_create request{}; + request.memfd = static_cast(file); + request.flags = UDMABUF_FLAGS_CLOEXEC; + request.offset = index * ImportChunkBytes; + request.size = ImportChunkBytes; + const int device = udmabufDevice(); + const int buffer = device < 0 ? -1 : ioctl(device, UDMABUF_CREATE, &request); + if (buffer < 0) { + failure = VK_ERROR_INVALID_EXTERNAL_HANDLE; + step = device < 0 ? "open /dev/udmabuf" : "UDMABUF_CREATE"; + return nullptr; + } + VkMemoryFdPropertiesKHR properties{VK_STRUCTURE_TYPE_MEMORY_FD_PROPERTIES_KHR}; + if (const auto result = context.Function("vkGetMemoryFdPropertiesKHR")(context.device, VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, buffer, &properties); result != VK_SUCCESS) { + close(buffer); + failure = result; + step = "vkGetMemoryFdPropertiesKHR"; + return nullptr; + } + const auto types = properties.memoryTypeBits & bufferTypes; + if (types == 0) { + close(buffer); + failure = VK_ERROR_FORMAT_NOT_SUPPORTED; + step = "memory type selection"; + return nullptr; + } + const VkImportMemoryFdInfoKHR import{VK_STRUCTURE_TYPE_IMPORT_MEMORY_FD_INFO_KHR, nullptr, VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, buffer}; + const VkMemoryAllocateFlagsInfo flags{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_FLAGS_INFO, &import, VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT, 0}; + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, &flags}; + allocation.allocationSize = ImportChunkBytes; + allocation.memoryTypeIndex = static_cast(std::countr_zero(types)); + auto chunk = std::make_shared(); + if (const auto result = context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &chunk->memory); result != VK_SUCCESS) { + close(buffer); + failure = result; + step = "vkAllocateMemory"; + return nullptr; + } + chunk->device = context.device; + chunk->freeMemory = context.Function("vkFreeMemory"); + chunk->memoryType = allocation.memoryTypeIndex; + for (auto it = chunks.chunks.begin(); it != chunks.chunks.end();) it = it->second.expired() ? chunks.chunks.erase(it) : std::next(it); + chunks.chunks[key] = chunk; + return chunk; +} + +const char* createChunkedDmaBufImport(const Context& context, HostImport& entry, int file, std::uint64_t offset, bool& fits, VkResult& failure) { + const auto index = offset / ImportChunkBytes; + const auto within = offset - index * ImportChunkBytes; + fits = entry.bytes != 0 && within + entry.bytes <= ImportChunkBytes; + if (!fits) return nullptr; + const VkExternalMemoryBufferCreateInfo external{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO, nullptr, VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT}; + VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, &external}; + info.size = entry.bytes; + info.usage = ChunkImportUsage; + 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; + return "vkCreateBuffer"; + } + const auto destroy = [&] { context.Function("vkDestroyBuffer")(context.device, buffer, nullptr); }; + VkMemoryRequirements requirements{}; + context.Function("vkGetBufferMemoryRequirements")(context.device, buffer, &requirements); + if (requirements.alignment == 0 || within % requirements.alignment != 0 || within + requirements.size > ImportChunkBytes) { + destroy(); + fits = false; + return nullptr; + } + const char* step = nullptr; + auto chunk = importChunk(context, file, index, requirements.memoryTypeBits, step, failure); + if (chunk == nullptr) { + destroy(); + return step; + } + if (((requirements.memoryTypeBits >> chunk->memoryType) & 1u) == 0) { + destroy(); + fits = false; + return nullptr; + } + if (const auto result = context.Function("vkBindBufferMemory")(context.device, buffer, chunk->memory, within); result != VK_SUCCESS) { + destroy(); + failure = result; + return "vkBindBufferMemory"; + } + const VkBufferDeviceAddressInfo addressInfo{VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO, nullptr, buffer}; + entry.buffer = buffer; + entry.memory = VK_NULL_HANDLE; + entry.address = context.Function("vkGetBufferDeviceAddressKHR")(context.device, &addressInfo); + entry.chunk = std::move(chunk); + entry.dmaBuf = true; + return nullptr; +} + +constexpr VkBufferUsageFlags SparseImportUsage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT; + +[[noreturn]] void throwSparseImport(const HostImport& entry, const char* step, VkResult result) { + char text[256]; + std::snprintf(text, sizeof(text), "AGC graphics: sparse dma-buf import of 0x%llx+0x%llx failed at %s (%d)", static_cast(entry.base), static_cast(entry.bytes), step, static_cast(result)); + throw std::runtime_error(text); +} + +VkMemoryRequirements dmaBufBufferRequirements(const Context& context, const HostImport& entry, VkBufferCreateFlags flags, VkBufferUsageFlags usage, VkDeviceSize size, VkBuffer* kept) { + const VkExternalMemoryBufferCreateInfo external{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO, nullptr, VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT}; + VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, &external}; + info.flags = flags; + info.size = size; + info.usage = usage; + 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{}; + context.Function("vkGetBufferMemoryRequirements")(context.device, buffer, &requirements); + if (kept != nullptr) { + *kept = buffer; + } else { + context.Function("vkDestroyBuffer")(context.device, buffer, nullptr); + } + return requirements; +} + +bool createSparseDmaBufImport(const Context& context, HostImport& entry, int file, std::uint64_t offset) { + const 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, 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); + const auto fail = [&](const char* step, VkResult result) { + context.Function("vkDestroyBuffer")(context.device, buffer, nullptr); + throwSparseImport(entry, step, result); + }; + if (!chosen) { + context.Function("vkDestroyBuffer")(context.device, buffer, nullptr); + return false; + } + auto chunks = std::make_shared>>(); + std::vector binds; + for (VkDeviceSize resource = 0; resource < requirements.size;) { + const auto position = offset + resource; + const auto index = position / ImportChunkBytes; + const auto within = position - index * ImportChunkBytes; + const auto size = std::min(ImportChunkBytes - within, requirements.size - resource); + const char* step = nullptr; + VkResult failure = VK_SUCCESS; + auto chunk = importChunk(context, file, index, requirements.memoryTypeBits, step, failure); + if (chunk == nullptr) fail(step, failure); + if (((requirements.memoryTypeBits >> chunk->memoryType) & 1u) == 0) fail("the memory type of a chunk import", VK_ERROR_FORMAT_NOT_SUPPORTED); + binds.push_back({resource, size, chunk->memory, within, 0}); + chunks->push_back(std::move(chunk)); + resource += size; + } + const VkSparseBufferMemoryBindInfo bufferBinds{buffer, static_cast(binds.size()), binds.data()}; + VkBindSparseInfo bind{VK_STRUCTURE_TYPE_BIND_SPARSE_INFO}; + bind.bufferBindCount = 1; + bind.pBufferBinds = &bufferBinds; + const VkFenceCreateInfo fenceInfo{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO}; + VkFence fence = VK_NULL_HANDLE; + if (const auto result = context.Function("vkCreateFence")(context.device, &fenceInfo, nullptr, &fence); result != VK_SUCCESS) fail("vkCreateFence", result); + VkResult bound = VK_SUCCESS; + VkResult waited = VK_SUCCESS; + { + std::unique_lock queueLock; + if (ownQueue) queueLock = std::unique_lock(*context.sparseQueueMutex); + bound = context.Function("vkQueueBindSparse")(ownQueue ? context.sparseQueue : context.queue, 1, &bind, fence); + if (bound == VK_SUCCESS) waited = context.Function("vkWaitForFences")(context.device, 1, &fence, VK_TRUE, UINT64_MAX); + } + context.Function("vkDestroyFence")(context.device, fence, nullptr); + if (bound != VK_SUCCESS) fail("vkQueueBindSparse", bound); + if (waited != VK_SUCCESS) fail("vkWaitForFences", waited); + const VkBufferDeviceAddressInfo addressInfo{VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO, nullptr, buffer}; + const auto address = context.Function("vkGetBufferDeviceAddressKHR")(context.device, &addressInfo); + if (address == 0) fail("vkGetBufferDeviceAddressKHR", VK_ERROR_UNKNOWN); + entry.buffer = buffer; + entry.memory = VK_NULL_HANDLE; + entry.address = address + lead; + entry.chunk = std::move(chunks); + entry.dmaBuf = true; + entry.lead = lead; + return true; +} +#endif + +#ifndef _WIN32 +struct BackingFile { + BackingFile() = default; + BackingFile(const BackingFile&) = delete; + BackingFile& operator=(const BackingFile&) = delete; + ~BackingFile() { + if (file >= 0) close(file); + } + int file = -1; + std::uint64_t offset = 0; +}; + +bool sharedBacking(const Context& context, const HostImport& entry, BackingFile& backing) { + return context.dmaBufImport && GuestArena::GuestArenaSharedBacking_nid_postfix(static_cast(entry.base), static_cast(entry.bytes), &backing.file, &backing.offset); +} + +const char* createBackingImport(const Context& context, HostImport& entry, BackingFile& backing, VkResult& failure) { + bool fits = false; + VkResult chunkFailure = VK_SUCCESS; + if (createChunkedDmaBufImport(context, entry, backing.file, backing.offset, fits, chunkFailure) == nullptr && fits) return nullptr; + if (!fits && createSparseDmaBufImport(context, entry, backing.file, backing.offset)) return nullptr; + return createDmaBufImport(context, entry, std::exchange(backing.file, -1), backing.offset, failure); +} +#endif + const char* createImport(const Context& context, HostImport& entry, VkResult& failure) { const char* step = createHostPointerImport(context, entry, failure); #ifndef _WIN32 - int file = -1; - std::uint64_t offset = 0; - if (step != nullptr && context.dmaBufImport && GuestArena::GuestArenaSharedBacking_nid_postfix(static_cast(entry.base), static_cast(entry.bytes), &file, &offset)) step = createDmaBufImport(context, entry, file, offset, failure); + BackingFile backing; + if (step != nullptr && sharedBacking(context, entry, backing)) step = createBackingImport(context, entry, backing, failure); #endif return step; } @@ -296,13 +912,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; @@ -324,8 +1242,8 @@ void decideImportWatch(const Context& context, HostImports& state) { state.unwatchImports = true; std::fprintf(stderr, "[write-watch] host imports resolve write protection: unknown (probe failed at %s, %d); imported ranges are compared\n", probe.failure, static_cast(probe.result)); } else { - state.unwatchImports = probe.writtenAfterSubmit != 0; - std::fprintf(stderr, "[write-watch] host imports resolve write protection: %s (%u of %u scratch pages written after a GPU read, %u after the import); imported ranges %s\n", state.unwatchImports ? "yes" : "no", probe.writtenAfterSubmit, probe.pages, probe.writtenAtImport, state.unwatchImports ? "are compared" : "stay watched"); + state.unwatchImports = probe.writtenAfterSubmit != 0 || probe.writtenAfterFill != 0; + std::fprintf(stderr, "[write-watch] host imports resolve write protection: %s (%u of %u scratch pages written after a GPU read, %u after a GPU write, %u after the import); imported ranges %s\n", state.unwatchImports ? "yes" : "no", probe.writtenAfterSubmit, probe.pages, probe.writtenAfterFill, probe.writtenAtImport, state.unwatchImports ? "are compared" : "stay watched"); } state.unwatchDmaBufImports = state.unwatchImports; if (!context.dmaBufImport) return; @@ -335,8 +1253,8 @@ void decideImportWatch(const Context& context, HostImports& state) { std::fprintf(stderr, "[write-watch] dma-buf imports keep write protection: unknown (probe failed at %s, %d); imported ranges are compared\n", dmaBuf.failure, static_cast(dmaBuf.result)); return; } - state.unwatchDmaBufImports = dmaBuf.writtenAfterSubmit != 0 || dmaBuf.writtenByCpu == 0; - std::fprintf(stderr, "[write-watch] dma-buf imports keep write protection: %s (%u of %u scratch pages written after a GPU read, %u after the import, %u seen after a CPU store); imported ranges %s\n", state.unwatchDmaBufImports ? "no" : "yes", dmaBuf.writtenAfterSubmit, dmaBuf.pages, dmaBuf.writtenAtImport, dmaBuf.writtenByCpu, state.unwatchDmaBufImports ? "are compared" : "stay watched"); + state.unwatchDmaBufImports = dmaBuf.writtenAfterSubmit != 0 || dmaBuf.writtenAfterFill != 0 || dmaBuf.writtenByCpu == 0; + std::fprintf(stderr, "[write-watch] dma-buf imports keep write protection: %s (%u of %u scratch pages written after a GPU read, %u after a GPU write, %u after the import, %u seen after a CPU store); imported ranges %s\n", state.unwatchDmaBufImports ? "no" : "yes", dmaBuf.writtenAfterSubmit, dmaBuf.pages, dmaBuf.writtenAfterFill, dmaBuf.writtenAtImport, dmaBuf.writtenByCpu, state.unwatchDmaBufImports ? "are compared" : "stay watched"); #endif } @@ -345,52 +1263,522 @@ std::shared_ptr leasedRangeOwner(const GuestAlloc return found != lease.end() && (*found)->address == base ? *found : nullptr; } -bool sameRange(const HostImport& entry, const GuestAllocations::Lease& lease) { - const auto current = leasedRangeOwner(lease, entry.base); - return current != nullptr && !entry.range.owner_before(current) && !current.owner_before(entry.range); +std::shared_ptr leasedRangeOwner(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) { + const auto found = std::upper_bound(lease.begin(), lease.end(), begin, [](std::uint64_t value, const auto& range) { return value < range->address; }); + if (found == lease.begin()) return nullptr; + const auto& range = *std::prev(found); + return begin >= range->address && end <= range->address + range->bytes ? range : nullptr; } -const HostImport* importAllocation(const Context& context, HostImports& state, std::uint64_t base, std::uint64_t bytes, const GuestAllocations::Lease& lease) { - if (const auto found = state.imports.find(base); found != state.imports.end()) { - if (found->second.bytes == bytes && sameRange(found->second, lease)) return &found->second; - retireImport(context, state, found, lease); +struct ImportRun { + std::uint64_t base = 0; + std::uint64_t bytes = 0; + std::vector> ranges; +}; + +bool continuesRun(const GuestAllocations::Range& before, const GuestAllocations::Range& after) { + return before.address + before.bytes == after.address && before.readable && after.readable && before.writable == after.writable && before.gpu == after.gpu; +} + +std::size_t leasedIndexContaining(const GuestAllocations::Lease& lease, std::uint64_t address) { + const auto found = std::upper_bound(lease.begin(), lease.end(), address, [](std::uint64_t value, const auto& range) { return value < range->address; }); + if (found == lease.begin()) return lease.size(); + const auto index = static_cast(std::prev(found) - lease.begin()); + const auto& range = *lease[index]; + return range.readable && address < range.address + range.bytes ? index : lease.size(); +} + +std::atomic importWindow{0}; + +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; } - const auto alignment = context.hostImportAlignment; - if (alignment == 0 || base % alignment != 0 || bytes % alignment != 0 || state.failed.contains(base)) return nullptr; - // Pinned imports count against the driver's system memory budget; past it ordinary host - // allocations fail, so imports stop at APS5_HOST_IMPORT_MIB (default 6 GiB, which covers the - // registered memory of address-based shaders; past it they copy gigabytes per dispatch). - static const std::uint64_t budget = [] { - const char* value = std::getenv("APS5_HOST_IMPORT_MIB"); - return (value ? std::strtoull(value, nullptr, 10) : 6144ull) << 20u; - }(); - std::uint64_t live = 0; - for (const auto& [address, existing] : state.imports) live += existing.bytes; - if (live + bytes > budget) { - // A refused import turns every later use of the range into CPU copies, so say so. - static std::uint64_t refused = 0, refusedBytes = 0; - static auto lastReport = std::chrono::steady_clock::now() - std::chrono::seconds(60); - ++refused; - refusedBytes += bytes; - if (std::chrono::steady_clock::now() - lastReport > std::chrono::seconds(10)) { - lastReport = std::chrono::steady_clock::now(); - std::fprintf(stderr, "[gpu] host import budget: %llu MiB live of %llu MiB (APS5_HOST_IMPORT_MIB); %llu imports (%llu MiB) refused so far, last 0x%llx+0x%llx\n", static_cast(live >> 20u), static_cast(budget >> 20u), static_cast(refused), static_cast(refusedBytes >> 20u), static_cast(base), static_cast(bytes)); + 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(); + if (window != 0) { + low = address / window * window; + high = low + window; + 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; + 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) { + const auto run = importRun(lease, begin); + return !run.ranges.empty() && end <= run.base + run.bytes; +} + +ImportRun crossedRun(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) { + auto last = leasedIndexContaining(lease, begin); + if (last == lease.size()) return {}; + const auto first = last; + while (lease[last]->address + lease[last]->bytes < end) { + if (last + 1 == lease.size() || !continuesRun(*lease[last], *lease[last + 1])) return {}; + ++last; + } + if (first == last) return {}; + ImportRun run{lease[first]->address, lease[last]->address + lease[last]->bytes - lease[first]->address, {}}; + run.ranges.assign(lease.begin() + static_cast(first), lease.begin() + static_cast(last) + 1); + return run; +} + +ImportRun joinedRun(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) { + const auto window = importWindow.load(std::memory_order_relaxed); + if (window == 0) return {}; + auto run = crossedRun(lease, begin, end); + if (run.ranges.empty()) return run; + if (std::all_of(run.ranges.begin(), run.ranges.end(), [window](const auto& range) { return range->bytes > window; })) return run; + if (begin == run.base && end == run.base + run.bytes) return run; + return {}; +} + +bool joinedCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) { + return !crossedRun(lease, begin, end).ranges.empty(); +} + +template +bool sameRun(const Entry& entry, const ImportRun& run) { + if (run.base != entry.base || run.bytes != entry.bytes || run.ranges.size() != entry.ranges.size()) return false; + for (std::size_t index = 0; index < run.ranges.size(); ++index) { + if (entry.ranges[index].owner_before(run.ranges[index]) || run.ranges[index].owner_before(entry.ranges[index])) return false; + } + return true; +} + +bool wouldRetireImport(const HostImports& state, const ImportRun& run) { + if (run.ranges.empty()) return false; + for (const auto& [base, entry] : state.trimmed) { + if (base < run.base + run.bytes && run.base < base + entry.bytes) return true; + } + const auto next = state.imports.lower_bound(run.base); + if (next != state.imports.end()) { + if (next->first == run.base && sameRun(next->second, run)) return false; + if (next->first < run.base + run.bytes) return true; + } + if (next == state.imports.begin()) return false; + const auto& previous = std::prev(next)->second; + return previous.base + previous.bytes > run.base; +} + +bool sameRange(const 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, 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 effectiveUse(state, entry) >= state.buildFloor; +} + +constexpr std::uint64_t ProtectedUses = 256; + +bool evictable(const HostImports& state, const HostImport& entry) { + 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) { + std::uint64_t freed = 0; + while (freed < wanted) { + auto oldest = state.imports.end(); + for (auto it = state.imports.begin(); it != state.imports.end(); ++it) { + if (evictable(state, it->second) && (oldest == state.imports.end() || effectiveUse(state, it->second) < effectiveUse(state, oldest->second))) oldest = it; } + if (oldest == state.imports.end()) break; + freed += oldest->second.pinnedBytes; + retireImport(context.device, state, oldest, lease); + } + return freed; +} + +std::uint64_t physicalMemoryBytes() { +#ifdef _WIN32 + MEMORYSTATUSEX status{}; + status.dwLength = sizeof(status); + return GlobalMemoryStatusEx(&status) ? status.ullTotalPhys : 0; +#else + const auto pages = sysconf(_SC_PHYS_PAGES); + const auto pageSize = sysconf(_SC_PAGESIZE); + return pages > 0 && pageSize > 0 ? static_cast(pages) * static_cast(pageSize) : 0; +#endif +} + +std::atomic budgetOverride{0}; +std::atomic addressCopyLimitOverride{0}; + +// 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; + }(); + 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; + } + 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}; + +// The import of an access to a registered range larger than the range window: the cells the access touches, grown to +// the union of the range's imports it overlaps, so accesses across neighbouring cell edges share one import instead of +// replacing each other's. +ImportRun windowRun(const HostImports& state, ImportRun run, std::uint64_t begin, std::uint64_t end, std::uint64_t alignment) { + const auto window = rangeImportWindow.load(std::memory_order_relaxed); + if (run.ranges.size() != 1 || window == 0 || alignment == 0 || window % alignment != 0) return run; + const auto& range = *run.ranges.front(); + if (range.bytes <= window || begin < range.address || end > range.address + range.bytes || end <= begin) return run; + const auto lastCell = range.bytes / window - 1; + const auto cell = [&](std::uint64_t address) { return std::min((address - range.address) / window, lastCell); }; + auto windowBegin = range.address + cell(begin) * window; + const auto endCell = cell(end - 1); + auto windowEnd = endCell == lastCell ? range.address + range.bytes : range.address + (endCell + 1) * window; + for (bool grown = true; grown;) { + grown = false; + auto it = state.imports.upper_bound(windowBegin); + if (it != state.imports.begin()) --it; + for (; it != state.imports.end() && it->first < windowEnd; ++it) { + const auto importEnd = it->second.base + it->second.bytes; + if (importEnd <= windowBegin || it->second.base < range.address || importEnd > range.address + range.bytes) continue; + if (it->second.base < windowBegin || importEnd > windowEnd) grown = true; + windowBegin = std::min(windowBegin, it->second.base); + windowEnd = std::max(windowEnd, importEnd); + } + } + if (windowBegin % alignment != 0 || (windowEnd - windowBegin) % alignment != 0) return run; + run.base = windowBegin; + run.bytes = windowEnd - windowBegin; + return run; +} + +const HostImport* 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); +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& 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) 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; +} + +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 = peekImport(state, address, address + 1); current != nullptr && current->joined && sameRange(*current, lease)) { + current->lastUse = ++state.useCounter; + return current; + } + } + 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; } - HostImport entry{base, bytes, VK_NULL_HANDLE, VK_NULL_HANDLE, 0}; - entry.range = leasedRangeOwner(lease, base); + 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) { + if (const auto* current = findImport(state, address, run.ranges.empty() ? address + 1 : run.base + run.bytes)) return current; + } + if (end != 0) run = windowRun(state, std::move(run), address, end, context.hostImportAlignment); + if (run.ranges.empty()) return nullptr; + return importRunAt(context, state, std::move(run), false, address, lease, end, evict, 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 = 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, 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, 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)) { + 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); !stepDown && failed != failures.end()) { + if (sameRun(failed->second, run)) return nullptr; + failures.erase(failed); + } + dropTrimmed(state, base, base + bytes); + 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) { + 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 stepDown ? importAllocation(context, state, address, lease, requester, end, evict, bytes / 2) : nullptr; + replaced += it->second.pinnedBytes; + } + const auto live = state.liveBytes - replaced; + 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 auto lastReport = std::chrono::steady_clock::now() - std::chrono::seconds(60); + ++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::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;) { + const auto next = std::next(it); + retireImport(context.device, state, it, lease); + it = next; + } + if (state.liveBytes + bytes > budget) retireLeastRecent(context, state, lease, state.liveBytes + bytes - budget); + 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) { - state.failed.insert(base); - 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); @@ -399,42 +1787,79 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s if (!pageWritable) writable = false; cursor = reinterpret_cast(info.BaseAddress) + info.RegionSize; } - if (!writable && readOnly) { - state.failed.insert(base); - return nullptr; - } + if (!writable && readOnly) return failedImport(); if (!writable) { if (refused.BaseAddress != nullptr) { char text[256]; std::snprintf(text, sizeof(text), "AGC graphics: host import of 0x%llx+0x%llx: memory at 0x%llx (type 0x%lx, protection 0x%lx) is neither read-write, read-only nor a shared mapping", static_cast(base), static_cast(bytes), reinterpret_cast(refused.BaseAddress), refused.Type, refused.Protect); throw std::runtime_error(text); } - entry.alias = GuestArena::GuestArenaMapAlias_nid_postfix(static_cast(base), static_cast(bytes)); + try { + entry.alias = GuestArena::GuestArenaMapAlias_nid_postfix(static_cast(base), static_cast(bytes)); + } catch (const std::runtime_error&) { + if (entry.ranges.size() == 1) throw; + return joined ? failedImport() : failedBlockImport(); + } + if (entry.alias == nullptr) return nullptr; } #else - if (!GuestMemory::Accessible(reinterpret_cast(base), static_cast(bytes), true)) { - state.failed.insert(base); - 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; - GuestMemory::ImportWatched(base, bytes, [&] { - step = createImport(context, entry, result); - return step == nullptr; - }); - entry.unwatched = step == nullptr && (entry.dmaBuf ? state.unwatchDmaBufImports : state.unwatchImports); #ifdef _WIN32 - if (entry.alias != nullptr) entry.unwatched = false; + const bool 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; + const bool watchedDmaBuf = !state.unwatchDmaBufImports; #endif - if (entry.unwatched) GuestMemory::Unwatch(base, bytes); + GuestMemory::ImportWatched(base, bytes, [&] { + step = watchedDmaBuf == watchedHostPointer ? createImport(context, entry, result) : createHostPointerImport(context, entry, result); +#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 + if (BackingFile backing; step != nullptr && watchedDmaBuf != watchedHostPointer && sharedBacking(context, entry, backing)) { + GuestMemory::ImportWatched(base, bytes, [&] { + step = createBackingImport(context, entry, backing, result); + return step == nullptr; + }, watchedDmaBuf); + } + 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 - state.failed.insert(base); - 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;) { @@ -457,21 +1882,34 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s 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; - std::uint64_t liveBytes = bytes; - for (const auto& [address, existing] : state.imports) liveBytes += existing.bytes; - if (trace) std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx ok (%zu live, %.1f MiB live, %.1f MiB ever)\n", static_cast(base), static_cast(bytes), state.imports.size() + 1, liveBytes / 1048576.0, importedBytes / 1048576.0); - return &state.imports.emplace(base, entry).first->second; -} - -// The lease is in address order (it is built from the registry map), so a range is found by binary search. -const GuestAllocations::Range* leasedRangeAt(const GuestAllocations::Lease& lease, std::uint64_t base) { - const auto found = std::lower_bound(lease.begin(), lease.end(), base, [](const auto& range, std::uint64_t value) { return range->address < value; }); - return found != lease.end() && (*found)->address == base ? found->get() : nullptr; + state.liveBytes += bytes; + 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; +#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. @@ -482,52 +1920,142 @@ const GuestAllocations::Range* containingRange(const GuestAllocations::Lease& le return range->readable && begin >= range->address && end <= range->address + range->bytes ? range.get() : nullptr; } +bool reattachTrimmed(const Context& context, HostImports& state, HostImport& entry, const GuestAllocations::Lease& lease) { + const auto first = entry.base; + const auto last = entry.base + entry.bytes; + std::shared_ptr kept; + for (auto it = std::lower_bound(lease.begin(), lease.end(), first, [](const auto& range, std::uint64_t value) { return range->address < value; }); it != lease.end() && (*it)->address < last; ++it) { + if ((*it)->backingSerial == entry.backingSerial && (*it)->address + (*it)->bytes <= last && (kept == nullptr || (*it)->bytes > kept->bytes)) kept = *it; + } + if (kept == nullptr) return false; + const auto next = state.imports.lower_bound(kept->address); + if (next != state.imports.end() && next->first < kept->address + kept->bytes) return false; + if (next != state.imports.begin() && std::prev(next)->second.base + std::prev(next)->second.bytes > kept->address) return false; + HostImport candidate{kept->address, kept->bytes, VK_NULL_HANDLE, VK_NULL_HANDLE, 0}; + candidate.range = kept; + candidate.ranges = {kept}; + if (!sameRange(candidate, lease)) return false; + const auto offset = kept->address - entry.memoryBase; + VkBuffer buffer = VK_NULL_HANDLE; + if (createImportBuffer(context, VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT, kept->bytes, &buffer) != VK_SUCCESS) return false; + const auto refuse = [&] { + context.Function("vkDestroyBuffer")(context.device, buffer, nullptr); + return false; + }; + VkMemoryRequirements requirements{}; + context.Function("vkGetBufferMemoryRequirements")(context.device, buffer, &requirements); + if (((requirements.memoryTypeBits >> entry.memoryType) & 1u) == 0 || requirements.alignment == 0 || offset % requirements.alignment != 0 || requirements.size > entry.pinnedBytes - offset) return refuse(); + if (context.Function("vkBindBufferMemory")(context.device, buffer, entry.memory, offset) != VK_SUCCESS) return refuse(); + const VkBufferDeviceAddressInfo addressInfo{VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO, nullptr, buffer}; + const auto address = context.Function("vkGetBufferDeviceAddressKHR")(context.device, &addressInfo); + if (address == 0) return refuse(); + entry.base = kept->address; + entry.bytes = kept->bytes; + entry.buffer = buffer; + entry.address = address; + entry.range = kept; + entry.ranges = {kept}; + entry.serial = 0; + entry.lastUse = ++state.useCounter; + return true; +} + // Drops imports whose registered range changed or disappeared: their pages may no longer back the // 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) { - for (const auto& [address, entry] : state.imports) { - if (state.device != VK_NULL_HANDLE && state.destroyBuffer != nullptr && state.freeMemory != nullptr) { - state.destroyBuffer(state.device, entry.buffer, nullptr); - state.freeMemory(state.device, entry.memory, nullptr); - } #ifdef _WIN32 - GuestArena::GuestArenaUnmapAlias_nid_postfix(entry.alias); + drainSettles(state); #endif + for (auto* held : {&state.imports, &state.trimmed}) { + while (!held->empty()) { + auto node = held->extract(held->begin()); + if (state.device != VK_NULL_HANDLE && state.destroyBuffer != nullptr && state.freeMemory != nullptr) { + state.parked[state.device].push_back(std::make_shared(state.device, state.destroyBuffer, state.freeMemory, std::move(node))); + continue; + } +#ifdef _WIN32 + GuestArena::GuestArenaUnmapAlias_nid_postfix(node.mapped().alias); +#endif + } } - state.imports.clear(); + 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"); state.refreshedGeneration = 0; ++state.epoch; } + if (state.destroyBuffer == nullptr) state.destroyBuffer = context.Function("vkDestroyBuffer"); + if (state.freeMemory == nullptr) state.freeMemory = context.Function("vkFreeMemory"); 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); }); + while (!state.trimmed.empty()) { + auto node = state.trimmed.extract(state.trimmed.begin()); + if (!reattachTrimmed(context, state, node.mapped(), lease)) { + releaseTrimmed(state, std::move(node)); + continue; + } + node.key() = node.mapped().base; + ++state.epoch; + state.imports.insert(std::move(node)); + } + const auto budget = effectiveBudget(context, state); for (auto it = state.imports.begin(); it != state.imports.end();) { - const auto* range = leasedRangeAt(lease, it->first); - if (range != nullptr && range->bytes == it->second.bytes && sameRange(it->second, lease)) { + if (sameRange(it->second, lease, budget)) { if (it->second.unwatched) GuestMemory::Unwatch(it->first, it->second.bytes); ++it; continue; } static const bool trace = std::getenv("APS5_TRACE_HOST_IMPORT") != nullptr; - if (trace && range != nullptr && range->bytes == it->second.bytes) std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx retired: the range was mapped again\n", static_cast(it->first), static_cast(it->second.bytes)); + if (trace && leasedRangeOwner(lease, it->first, it->first + it->second.bytes) != nullptr) std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx retired: the range was mapped again\n", static_cast(it->first), static_cast(it->second.bytes)); const auto next = std::next(it); - retireImport(context, state, it, lease); + retireImport(context.device, state, it, lease); it = next; } - for (auto it = state.failed.begin(); it != state.failed.end();) it = leasedRangeAt(lease, *it) != nullptr ? 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; - return begin >= entry.base && end <= entry.base + entry.bytes ? &entry : nullptr; + return begin < entry.base || end > entry.base + entry.bytes ? nullptr : &entry; +} + +const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint64_t end) { + const auto* entry = peekImport(state, begin, end); + if (entry != nullptr) entry->lastUse = ++state.useCounter; + return entry; +} + +const HostImport* importAccess(const Context& context, HostImports& state, std::uint64_t begin, std::uint64_t end, const GuestAllocations::Lease& lease, ImportRequester requester) { + if (!runCovers(lease, begin, end)) return findImport(state, begin, end); + const auto* entry = importAllocation(context, state, begin, lease, requester, end); + return entry != nullptr && findImport(state, begin, end) == entry ? entry : nullptr; } // Whether the imports need reconciling before a lookup: the registry changed since the last walk. @@ -552,6 +2080,7 @@ struct ImageMirrors { std::uint64_t heapImported = 0; std::uint64_t heapImportedBytes = 0; std::uint64_t subranges = 0; + std::uint64_t sparseBinds = 0; std::uint64_t rebuilds = 0; std::uint64_t refreshes = 0; std::uint64_t blocksCompared = 0; @@ -604,6 +2133,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; @@ -647,12 +2234,22 @@ struct RefreshBlock { std::uint64_t address; std::size_t length; std::uint64_t generation = 0; + std::uint8_t fill = 0; }; +constexpr std::uint8_t SparseCopy = 1; +constexpr std::uint8_t SparseZero = 2; + // Compares a block's guest bytes with the mirror's shadow and copies a changed block into the device bool compareBlock(const RefreshBlock& block) { const auto offset = static_cast(block.address - block.mirror->base); const auto* guest = reinterpret_cast(block.address); + if (block.fill != 0) { + auto* target = block.mirror->buffer->Bytes().data() + offset; + if (block.fill == SparseZero) std::memset(target, 0, block.length); + else std::memcpy(target, guest, block.length); + return true; + } if (block.mirror->heap) { std::memcpy(block.mirror->buffer->Bytes().data() + offset, guest, block.length); block.mirror->generations[static_cast(block.address / 65536 - block.mirror->base / 65536)] = block.generation; @@ -761,10 +2358,7 @@ private: // completes first, so its results reach the guest and the shadow before the compare and no batch // writes a block being replaced; GPU reads of the mirror by earlier recorded work (an address-based // dispatch whose lease is released at completion) see the newer bytes, as they would on hardware. -void prepareRange(std::uint64_t address, std::uint64_t bytes) { - // The compare reads the pages directly; a mirror is only reused while its Range exists, but a - // lease of a build in flight can keep a replaced Range alive past a protection change. - Require(GuestMemory::Accessible(reinterpret_cast(address), static_cast(bytes)), "image mirror range became inaccessible"); +void syncRange(std::uint64_t address, std::uint64_t bytes) { // Named for the [hooksync] attribution: the flush goes through the hook, which waits when // recorded work notes a write inside the range (a descriptor written inside a writable exe // range; the lease itself maps the range read-only). The access is the whole range. @@ -779,6 +2373,11 @@ void prepareRange(std::uint64_t address, std::uint64_t bytes) { ++state.refreshes; } +void prepareRange(std::uint64_t address, std::uint64_t bytes) { + Require(GuestMemory::Accessible(reinterpret_cast(address), static_cast(bytes)), "image mirror range became inaccessible"); + syncRange(address, bytes); +} + bool prepareRefresh(ImageMirror& mirror, std::uint64_t address, std::uint64_t bytes) { if ((!mirror.writable && !mirror.heap) || bytes == 0) return false; prepareRange(address, bytes); @@ -834,6 +2433,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();) { @@ -843,12 +2460,112 @@ 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; } } +bool sparseMirrorsEnabled() { + static const bool disabled = std::getenv("APS5_NO_SPARSE_HEAP_MIRROR") != nullptr; + 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) { + constexpr std::uint64_t granule = std::uint64_t{1} << 20u; + const auto used = pieces.empty() ? 0 : pieces.back().second - base; + const auto wanted = used + used / 8; + return std::min(reserved, std::max(granule, (wanted + granule - 1) / granule * granule)); +} + +std::uint64_t collectPieces(const Pieces& pieces) { + std::uint64_t generation = 0; + for (const auto& [first, last] : pieces) { + generation = GuestMemory::CollectWrites(first, static_cast(last - first)); + if (generation == 0) return 0; + } + return generation != 0 ? generation : GuestMemory::TrackerGeneration(); +} + +bool covers(const Pieces& pieces, std::uint64_t address) { + return std::any_of(pieces.begin(), pieces.end(), [&](const auto& piece) { return piece.first <= address && address < piece.second; }); +} + +void forgetCommitChanges(ImageMirror& mirror, const Pieces& pieces) { + if (pieces == mirror.committed) return; + std::vector edges; + for (const auto* list : std::array{&pieces, &mirror.committed}) { + for (const auto& [first, last] : *list) { + edges.push_back(first); + edges.push_back(last); + } + } + std::sort(edges.begin(), edges.end()); + edges.erase(std::unique(edges.begin(), edges.end()), edges.end()); + constexpr std::uint64_t block = 65536; + const auto end = mirror.base + mirror.bytes; + for (std::size_t at = 0; at + 1 < edges.size(); ++at) { + const auto from = std::max(edges[at], mirror.base); + const auto to = std::min(edges[at + 1], end); + if (from >= to || covers(pieces, from) == covers(mirror.committed, from)) continue; + for (auto index = from / block - mirror.base / block; index <= (to - 1) / block - mirror.base / block; ++index) mirror.generations[static_cast(index)] = 0; + } +} + +void appendSparseBlocks(ImageMirror& mirror, Pieces pieces, std::uint64_t generation, std::vector& blocks) { + constexpr std::uint64_t block = 65536; + thread_local std::vector* changedSlot = nullptr; + auto& changed = ShaderRecompiler::ThreadOwned(changedSlot); + forgetCommitChanges(mirror, pieces); + ++Mirrors().heapChecks; + changed.assign(mirror.generations.size(), 0); + GuestMemory::ChangedBlocks(mirror.base, static_cast(mirror.bytes), mirror.generations, changed); + const auto aligned = mirror.base / block * block; + const auto end = mirror.base + mirror.bytes; + bool refilled = false; + std::size_t piece = 0; + for (std::size_t at = 0; at < changed.size(); ++at) { + if (changed[at] == 0) continue; + refilled = true; + mirror.generations[at] = generation; + const auto from = std::max(mirror.base, aligned + at * block); + const auto to = std::min(end, aligned + (at + 1) * block); + auto cursor = from; + while (piece < pieces.size() && pieces[piece].second <= from) ++piece; + for (auto next = piece; next < pieces.size() && pieces[next].first < to; ++next) { + const auto first = std::max(pieces[next].first, from); + const auto last = std::min(pieces[next].second, to); + if (cursor < first) blocks.push_back({&mirror, cursor, static_cast(first - cursor), 0, SparseZero}); + blocks.push_back({&mirror, first, static_cast(last - first), 0, SparseCopy}); + cursor = last; + } + if (cursor < to) blocks.push_back({&mirror, cursor, static_cast(to - cursor), 0, SparseZero}); + } + if (refilled) ++Mirrors().heapRefills; + mirror.committed = std::move(pieces); +} + +Pieces clipPieces(const Pieces& pieces, std::uint64_t begin, std::uint64_t end) { + Pieces result; + for (const auto& [first, last] : pieces) { + if (std::max(first, begin) < std::min(last, end)) result.emplace_back(std::max(first, begin), std::min(last, end)); + } + return result; +} + +bool refreshSparseMirror(ImageMirror& mirror, std::vector& blocks) { + auto pieces = GuestMemory::DescribeCommitted(mirror.base, static_cast(mirror.reserved)).ranges; + if (!pieces.empty() && pieces.back().second > mirror.base + mirror.bytes) return false; + syncRange(mirror.base, mirror.bytes); + const auto generation = collectPieces(pieces); + if (generation == 0) markUnwatched(mirror); + appendSparseBlocks(mirror, std::move(pieces), generation, blocks); + return true; +} + void planSpaceMirrors(GuestBufferMemory::AddressSpace& space) { for (std::size_t index = 0; index < space.base.size(); ++index) { const auto& region = space.base[index]; @@ -859,6 +2576,10 @@ void planSpaceMirrors(GuestBufferMemory::AddressSpace& space) { continue; } auto* mirror = region.mirror.get(); + if (mirror->sparse) { + space.sparseMirrors.push_back(mirror); + continue; + } if (space.heapRuns.empty() || mirror->base != space.heapMirrors.back()->base + space.heapMirrors.back()->bytes) space.heapRuns.push_back({mirror->base, 0, space.heapMirrors.size(), 0}); space.heapMirrors.push_back(mirror); auto& run = space.heapRuns.back(); @@ -875,14 +2596,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 @@ -900,36 +2632,62 @@ std::shared_ptr acquireMirror(const Context& context, const std::sh } if (state.failed.contains(range->address)) return nullptr; const auto found = state.entries.find(range->address); - if (found != state.entries.end() && sameRange(found->second->range, range) && found->second->bytes == range->bytes && found->second->writable == range->writable && found->second->heap == heap) mirror = found->second; + if (found != state.entries.end() && sameRange(found->second->range, range) && found->second->reserved == range->bytes && found->second->writable == range->writable && found->second->heap == heap) mirror = found->second; } if (mirror != nullptr) { - if (!heap && prepareRefresh(*mirror, range->address, range->bytes)) appendBlocks(blocks, *mirror, range->address, range->bytes); - return mirror; + if (!mirror->sparse) { + if (!heap && prepareRefresh(*mirror, range->address, range->bytes)) appendBlocks(blocks, *mirror, range->address, range->bytes); + return mirror; + } + if (refreshSparseMirror(*mirror, blocks)) return mirror; } - if (!GuestMemory::Accessible(reinterpret_cast(range->address), range->bytes, range->writable && !heap)) return nullptr; + Pieces pieces; + if (!GuestMemory::Accessible(reinterpret_cast(range->address), range->bytes, range->writable && !heap)) { + if (!heap || !sparseMirrorsEnabled()) return nullptr; + pieces = GuestMemory::DescribeCommitted(range->address, range->bytes).ranges; + if (pieces.empty()) return nullptr; + } + const bool sparse = !pieces.empty(); mirror = std::make_shared(); mirror->base = range->address; - mirror->bytes = range->bytes; + mirror->bytes = sparse ? sparseExtent(range->address, range->bytes, pieces) : range->bytes; + mirror->reserved = range->bytes; + mirror->sparse = sparse; mirror->writable = range->writable; mirror->heap = heap; mirror->range = range; if (heap) { - prepareRefresh(*mirror, range->address, range->bytes); - const auto generation = GuestMemory::CollectWrites(range->address, range->bytes); + 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); + } else { + prepareRefresh(*mirror, range->address, range->bytes); + generation = GuestMemory::CollectWrites(range->address, range->bytes); + } constexpr std::uint64_t block = 65536; - mirror->generations.assign(static_cast(((range->address + range->bytes + block - 1) / block) - range->address / block), generation); + 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; } - if (heapMirrorsBeside(*range) + range->bytes > heapMirrorBudget()) { + mirror->watchedSerial.store(serial, std::memory_order_relaxed); + if (heapMirrorsBeside(*range) + mirror->bytes > heapMirrorBudget()) { sweepMirrors(); - if (const auto held = heapMirrorsBeside(*range); held + range->bytes > heapMirrorBudget()) throw std::runtime_error(heapMirrorError(*range, held, "past the budget")); + if (const auto held = heapMirrorsBeside(*range); held + mirror->bytes > heapMirrorBudget()) throw std::runtime_error(heapMirrorError(*range, held, "past the budget")); } } try { - mirror->buffer = std::make_shared(context, range->bytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); + mirror->buffer = std::make_shared(context, static_cast(mirror->bytes), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); } catch (const std::runtime_error& error) { if (heap) throw std::runtime_error(heapMirrorError(*range, heapMirrorsBeside(*range), error.what())); std::fprintf(stderr, "[gpu] image mirror of 0x%llx+0x%llx failed: %s; falling back to copies\n", static_cast(range->address), static_cast(range->bytes), error.what()); @@ -941,7 +2699,17 @@ std::shared_ptr acquireMirror(const Context& context, const std::sh // named for the [hooksync] attribution like the refresh's flush. { const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::MirrorRefresh); - GuestMemory::Read(range->address, mirror->buffer->Bytes()); + const auto target = mirror->buffer->Bytes().first(static_cast(mirror->bytes)); + if (!sparse) GuestMemory::Read(range->address, target); + std::size_t cursor = 0; + for (const auto& [first, last] : pieces) { + const auto at = static_cast(first - mirror->base); + std::memset(target.data() + cursor, 0, at - cursor); + GuestMemory::Read(first, target.subspan(at, static_cast(last - first))); + cursor = static_cast(last - mirror->base); + } + if (sparse) std::memset(target.data() + cursor, 0, target.size() - cursor); + mirror->committed = std::move(pieces); } if (range->writable && !heap) mirror->shadow.assign(mirror->buffer->Bytes().begin(), mirror->buffer->Bytes().end()); std::lock_guard lock(state.mutex); @@ -1032,7 +2800,12 @@ 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 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.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); + 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 @@ -1071,6 +2844,7 @@ LeaseState& Leases() { struct SnapshotStats { std::atomic checked{0}; std::atomic skipped{0}; + std::atomic mismatched{0}; }; SnapshotStats& Snapshots() { @@ -1078,10 +2852,11 @@ SnapshotStats& Snapshots() { return stats; } -bool WaitForLeases() 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; // 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 @@ -1097,34 +2872,110 @@ bool WaitForLeases() noexcept { state.stats.contentionMs += std::chrono::duration(std::chrono::steady_clock::now() - start).count(); return true; } + if (Recorder::InCompletion()) throw std::runtime_error("guest allocation mutation cannot wait for an active GPU completion"); if (GuestMemory::GpuMutex().HeldByThisThread()) { - std::this_thread::yield(); + 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); + retireOrTrim(imports.device, imports, it, lease, address, end); + 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 { - 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. + 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->Submit(); - recorder->FinishUpTo(target); - state.finishedLeaseSerial = target; + recorder->Sync(); synced = true; - } else if (recorder != nullptr && !recorder->Idle()) { + 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); + retireOrTrim(imports.device, imports, it, lease, address, end); + it = next; + retiredImport = true; + } + } + if (retiredImport && 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(); } - } 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); @@ -1132,12 +2983,12 @@ bool WaitForLeases() noexcept { if (synced) ++state.stats.contentionSyncs; if (drained) ++state.stats.contentionDrains; state.stats.contentionMs += std::chrono::duration(std::chrono::steady_clock::now() - start).count(); - return synced; + return synced || retiredImport; } void ensurePinWaiter() { static const bool registered = [] { - GuestAllocations::GuestAllocationsSetPinWaiter_nid_postfix(&WaitForLeases); + GuestAllocations::GuestAllocationsSetPinWaiter_nid_postfix(&WaitForLeasesAndImports); return true; }(); static_cast(registered); @@ -1179,6 +3030,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); @@ -1210,12 +3072,37 @@ std::string AddressCopyOverflow(std::vector copies, std::uint64_t l MirrorStats MirrorCounters() { auto& state = Mirrors(); std::lock_guard lock(state.mutex); - MirrorStats stats{0, state.heapBytes, state.rebuilds, state.blocksCopied, state.heapRefills, state.sweeps, state.heapChecks}; + MirrorStats stats{0, state.heapBytes, state.rebuilds, state.blocksCopied, state.heapRefills, state.sweeps, state.heapChecks, state.sparseBinds}; for (const auto& [base, mirror] : state.entries) stats.heapMirrors += mirror->heap ? 1 : 0; 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) { + Spaces().current.compare_exchange_strong(space, std::shared_ptr{}); + } auto& state = Mirrors(); std::map> entries; { @@ -1226,12 +3113,26 @@ void ClearImageMirrors(VkDevice device) { state.heapBytes = 0; state.device = VK_NULL_HANDLE; } - Spaces().current.store(nullptr); } 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); @@ -1240,28 +3141,33 @@ void ClearHostImports(VkDevice device) { GuestArena::GuestArenaUnmapAlias_nid_postfix(entry.alias); #endif } + for (const auto& [address, entry] : state.trimmed) state.freeMemory(state.device, entry.memory, nullptr); state.imports.clear(); + state.trimmed.clear(); + 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 = 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"; @@ -1269,6 +3175,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; @@ -1285,24 +3196,36 @@ void runImportProbe(const Context& context, std::uint64_t base, std::uint64_t by VkCommandBufferAllocateInfo allocate{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO}; allocate.commandPool = context.pool; allocate.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; - allocate.commandBufferCount = 1; - if ((probe.result = context.Function("vkAllocateCommandBuffers")(context.device, &allocate, &commands)) != VK_SUCCESS) return "vkAllocateCommandBuffers"; + 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, &begin)) != VK_SUCCESS) return "vkBeginCommandBuffer"; + if ((probe.result = context.Function("vkBeginCommandBuffer")(commands[0], &begin)) != VK_SUCCESS) return "vkBeginCommandBuffer"; const VkBufferCopy region{0, 0, bytes}; - context.Function("vkCmdCopyBuffer")(commands, import.buffer, destination, 1, ®ion); - if ((probe.result = context.Function("vkEndCommandBuffer")(commands)) != VK_SUCCESS) return "vkEndCommandBuffer"; + 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"; const VkFenceCreateInfo fenceInfo{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO}; if ((probe.result = context.Function("vkCreateFence")(context.device, &fenceInfo, nullptr, &fence)) != VK_SUCCESS) return "vkCreateFence"; - VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO}; - submit.commandBufferCount = 1; - submit.pCommandBuffers = &commands; - if ((probe.result = context.Function("vkQueueSubmit")(context.queue, 1, &submit, fence)) != VK_SUCCESS) return "vkQueueSubmit"; - submitted = true; - if ((probe.result = context.Function("vkWaitForFences")(context.device, 1, &fence, VK_TRUE, 10'000'000'000ull)) != VK_SUCCESS) return "vkWaitForFences"; - submitted = false; + const auto execute = [&](VkCommandBuffer buffer) -> const char* { + if ((probe.result = context.Function("vkResetFences")(context.device, 1, &fence)) != VK_SUCCESS) return "vkResetFences"; + VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO}; + submit.commandBufferCount = 1; + submit.pCommandBuffers = &buffer; + if ((probe.result = context.Function("vkQueueSubmit")(context.queue, 1, &submit, fence)) != VK_SUCCESS) return "vkQueueSubmit"; + submitted = true; + if ((probe.result = context.Function("vkWaitForFences")(context.device, 1, &fence, VK_TRUE, 10'000'000'000ull)) != VK_SUCCESS) return "vkWaitForFences"; + submitted = false; + return nullptr; + }; + 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; if (!collect(probe.writtenByCpu)) return "the collect after a CPU store"; return nullptr; @@ -1310,21 +3233,53 @@ void runImportProbe(const Context& context, std::uint64_t base, std::uint64_t by probe.failure = run(); if (submitted) context.Function("vkQueueWaitIdle")(context.queue); if (fence != VK_NULL_HANDLE) context.Function("vkDestroyFence")(context.device, fence, nullptr); - if (commands != VK_NULL_HANDLE) context.Function("vkFreeCommandBuffers")(context.device, context.pool, 1, &commands); + if (commands[0] != VK_NULL_HANDLE) context.Function("vkFreeCommandBuffers")(context.device, context.pool, 2, commands); 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) { @@ -1347,7 +3302,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 @@ -1387,6 +3344,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) { @@ -1394,7 +3352,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; @@ -1416,24 +3375,203 @@ void SetImportWatch(const Context& context, ImportWatch watch) { state.unwatchDmaBufImports = state.unwatchImports; } -const HostImport* HostImportFor(const Context& context, std::uint64_t address, std::size_t bytes) { - if (context.hostImportAlignment == 0 || bytes == 0 || bytes > std::numeric_limits::max() - address) return nullptr; +void SetHostImportWindow(std::uint64_t bytes) { + importWindow.store(bytes, std::memory_order_relaxed); +} + +void SetHostImportRangeWindow(std::uint64_t bytes) { + rangeImportWindow.store(bytes, std::memory_order_relaxed); +} + +void SetHostImportBudget(std::uint64_t bytes) { + budgetOverride.store(bytes, std::memory_order_relaxed); 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() { + auto& state = Imports(); + std::lock_guard lock(state.mutex); + return state.liveBytes; +} + +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)) return entry; + if (const auto* entry = findImport(state, address, address + bytes)) { + entry->lastUse = ++state.useCounter; + return entry; + } } const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); refreshImports(context, state, lease); - if (const auto* range = containingRange(lease, address, address + bytes)) return importAllocation(context, state, range->address, range->bytes, lease); - return nullptr; + return importAccess(context, state, address, address + bytes, lease, requester); } -bool RegisteredReadableCovers(std::uint64_t address, std::size_t bytes) { +const HostImport* ExistingHostImportFor(const Context& context, std::uint64_t address, std::size_t bytes) { + if (context.hostImportAlignment == 0 || bytes == 0 || bytes > std::numeric_limits::max() - address) return nullptr; + auto& state = Imports(); + std::lock_guard lock(state.mutex); + if (importsStale(context, state)) return nullptr; + return findImport(state, address, address + bytes); +} + +bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& ranges, std::uint64_t generation, bool adoptDevice) { + if (context.hostImportAlignment == 0) return true; + ensurePinWaiter(); + auto& state = Imports(); + std::lock_guard lock(state.mutex); + if (!adoptDevice && (state.device != context.device || state.watchDevice != context.device)) return false; + const bool adopting = state.device != context.device; + GuestAllocations::Lease current; + if (adopting) { + current = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + refreshImports(context, state, current); + } else if (state.refreshedGeneration > generation) { + return true; + } + if (!adopting) current = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + for (const auto& mapped : ranges) { + const auto range = mapped.lock(); + if (range == nullptr || leasedRangeOwner(current, range->address) != range) continue; + try { + if (!GuestMemory::GpuMutex().HeldByThisThread()) { + 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; + } + 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()); + } + } + return true; +} + +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) { + if (address == std::numeric_limits::max()) return 0; + const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + const auto* range = containingRange(lease, address, address + 1); + return range != nullptr ? range->address + range->bytes : 0; } bool HostImportCovers(const Context& context, std::uint64_t address, std::size_t bytes) { @@ -1442,7 +3580,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) {} @@ -1510,11 +3648,44 @@ 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; + 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); + } + } + } + if (hit) { + for (auto* mirror : current->sparseMirrors) { + if (refreshSparseMirror(*mirror, blocks)) continue; + hit = false; + blocks.clear(); + spaces.current.compare_exchange_strong(current, std::shared_ptr{}); + break; + } } if (hit) { spaces.hits.fetch_add(1, std::memory_order_relaxed); @@ -1544,6 +3715,8 @@ void GuestBufferMemory::AcquireRegistered() { auto& state = Imports(); std::lock_guard lock(state.mutex); if (importable) refreshImports(context, state, lease); + if (importFloor == 0) importFloor = state.useCounter + 1; + state.buildFloor = importFloor; // Taken before the pass: the imports it takes are the registry's ones (just reconciled, so // none is retired by a make below), and a mirror's preparation may sync the recorder, whose // completions can retire an import already taken; that moves the epoch, and Upload then @@ -1557,15 +3730,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) continue; + 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, range->bytes, lease); + 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) { @@ -1581,7 +3764,12 @@ void GuestBufferMemory::AcquireRegistered() { lap(timing.mirrorsUs, at); if (region.mirror != nullptr) { mirrored = true; - if (region.mirror->heap) heaps.push_back(region.mirror.get()); + if (region.mirror->sparse) { + region.sparse = true; + region.backed = region.mirror->committed; + } else if (region.mirror->heap) { + heaps.push_back(region.mirror.get()); + } regions.push_back(std::move(region)); continue; } @@ -1602,7 +3790,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(); @@ -1622,6 +3811,20 @@ void GuestBufferMemory::AcquireRegistered() { publish = false; spaces.unpublished.fetch_add(1, std::memory_order_relaxed); } else { + std::size_t kept = 0; + for (std::size_t index = 0; index < regions.size(); ++index) { + auto& region = regions[index]; + if (kept != 0) { + auto& previous = regions[kept - 1]; + if (region.direct != nullptr && previous.direct == region.direct && previous.end == region.begin && previous.writable == region.writable && region.mirror == nullptr && previous.mirror == nullptr && !region.sparse && !previous.sparse && region.end <= region.direct->base + region.direct->bytes) { + previous.end = region.end; + continue; + } + } + if (kept != index) regions[kept] = std::move(region); + ++kept; + } + regions.erase(regions.begin() + static_cast(kept), regions.end()); for (const auto& region : regions) { if (region.direct != nullptr || region.mirror != nullptr) ranges.push_back(addressRange(region)); } @@ -1647,6 +3850,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); @@ -1713,9 +3923,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())); @@ -1732,27 +3945,57 @@ 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; } -void GuestBufferMemory::addDescriptorRegion(std::uint64_t address, std::size_t bytes, bool atomic) { +namespace { + +template +bool mirrorOverlaps(const Regions& base, std::uint64_t begin, std::uint64_t end) { + auto found = std::upper_bound(base.begin(), base.end(), begin, [](std::uint64_t value, const auto& region) { return value < region.begin; }); + if (found != base.begin()) --found; + for (; found != base.end() && found->begin < end; ++found) { + if (found->end > begin && found->mirror != nullptr) return true; + } + return false; +} + +} + +void GuestBufferMemory::addDescriptorRegion(std::uint64_t address, std::size_t bytes, bool atomic, bool swept, bool readOnly) { validate(address, bytes); const auto begin = address & ~std::uint64_t{3}; const auto end = begin == address ? address + bytes : (address + bytes + 3) & ~std::uint64_t{3}; - switch (baseOverlap(begin, end, nullptr)) { + const Region* inside = nullptr; + switch (baseOverlap(begin, end, &inside)) { case BaseOverlap::Inside: + if (!servesView(*inside, begin)) { + dissolveSpace(false); + break; + } return; case BaseOverlap::Partial: + if (readOnly && !mirrorOverlaps(space->base, begin, end) && !WritesOverlap(begin, static_cast(end - begin))) { + readsOverBase.emplace_back(begin, end); + break; + } Spaces().dissolvedOverlap.fetch_add(1, std::memory_order_relaxed); dissolveSpace(false); break; @@ -1763,18 +4006,66 @@ void GuestBufferMemory::addDescriptorRegion(std::uint64_t address, std::size_t b // the shader stores to them; what is written back is decided by Writes() alone. Region region{begin, end, true, {}, nullptr}; region.atomic = atomic; + region.swept = swept; + region.lastView = begin; auto committed = GuestMemory::DescribeCommitted(begin, static_cast(end - begin)); if (!committed.whole) { // A GPU heap bound whole while the guest commits its pages on demand, or a descriptor left region.sparse = true; + region.sparseHeap = true; region.backed = std::move(committed.ranges); } regions.push_back(std::move(region)); regionsSorted = false; } -void GuestBufferMemory::AddWritable(std::uint64_t address, std::size_t bytes, bool atomic) { - addDescriptorRegion(address, bytes, atomic); +bool GuestBufferMemory::servesView(const Region& region, std::uint64_t view) { + return region.mirror == nullptr || view + 4 <= region.mirror->base + region.mirror->bytes; +} + +bool GuestBufferMemory::sparseHeapCandidate(const Region& region) { + return region.sparseHeap && region.sparse && region.mirror == nullptr && region.direct == nullptr && (region.hostBacked || region.writable) && mirrorsEnabled() && sparseMirrorsEnabled(); +} + +bool GuestBufferMemory::bindSparseMirror(Region& region, const GuestAllocations::Lease& lease) { + const auto backedEnd = region.backed.empty() ? region.begin : region.backed.back().second; + const auto neededEnd = std::min(region.end, std::max(backedEnd, std::max(region.lastView, region.begin) + 4u)); + const auto* range = containingRange(lease, region.begin, neededEnd); + if (range == nullptr || !range->releasable) return false; + std::vector blocks; + std::shared_ptr mirror; + try { + mirror = acquireMirror(context, leasedRangeOwner(lease, range->address), blocks, true); + } catch (const std::runtime_error& error) { + static std::atomic reported{0}; + if (reported.fetch_add(1, std::memory_order_relaxed) < 4) std::fprintf(stderr, "[gpu] %s; copying the sparse heap instead\n", error.what()); + return false; + } + if (mirror != nullptr && mirror->heap && !mirror->sparse) { + std::vector whole{mirror.get()}; + refreshHeapMirrors(whole, blocks); + } + compareBlocks(blocks); + if (mirror == nullptr || !mirror->heap || region.begin < mirror->base || (region.begin - mirror->base) % context.limits.minStorageBufferOffsetAlignment != 0) return false; + region.mirror = std::move(mirror); + if (!servesView(region, std::max(region.lastView, region.begin))) { + region.mirror = nullptr; + return false; + } + if (region.mirror->sparse) region.backed = clipPieces(region.mirror->committed, region.begin, region.end); + else region.backed = clipPieces({{region.mirror->base, region.mirror->base + region.mirror->bytes}}, region.begin, region.end); + region.snapshot.clear(); + std::lock_guard lock(Mirrors().mutex); + ++Mirrors().sparseBinds; + return true; +} + +void GuestBufferMemory::AddWritable(std::uint64_t address, std::size_t bytes, bool atomic, bool swept) { + if (space != nullptr && std::any_of(readsOverBase.begin(), readsOverBase.end(), [&](const auto& range) { return address < range.second && range.first < address + bytes; })) { + Spaces().dissolvedOverlap.fetch_add(1, std::memory_order_relaxed); + dissolveSpace(false); + } + addDescriptorRegion(address, bytes, atomic, swept, false); writes.emplace_back(address, address + bytes); } @@ -1782,7 +4073,7 @@ void GuestBufferMemory::AddReadable(std::uint64_t address, std::size_t bytes) { // Not in `writes`: no reference copy (copyRegion), no write-back, no pending-write note, no // direct-write mark, and UploadPrepare copies it without the device lock. A written descriptor // overlapping the range still covers it through its own Writes() entry. - addDescriptorRegion(address, bytes, false); + addDescriptorRegion(address, bytes, false, false, true); } void GuestBufferMemory::AddSnapshot(const GuestMemorySnapshot& snapshot) { @@ -1831,7 +4122,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; } @@ -1905,6 +4213,8 @@ VkBufferUsageFlags gpuCopyUsage(bool addressable) { // APS5_WRITTEN_SHADOW_MIN_KIB / APS5_WRITTEN_SHADOW_MAX_KIB (16 / 2048) bound the written window // (a kernel streaming once through a large buffer, the engine's memcpy kernel over 4-8 MiB video // frames, would only gain the two copies), APS5_ATOMIC_STAGE_MAX_KIB (1024) the atomic one. +constexpr std::uint64_t SweptShadowMax = std::uint64_t{16} << 20u; + bool writtenShadowEnabled() { static const bool disabled = std::getenv("APS5_NO_WRITTEN_SHADOW") != nullptr; return !disabled; @@ -2045,13 +4355,15 @@ bool GuestBufferMemory::stagingEligible(const Region& region, bool addressable) const auto bytes = region.end - region.begin; if (!WritesOverlap(region.begin, static_cast(bytes))) return false; if (region.atomic && atomicStagingEnabled() && bytes <= atomicStageMax()) return true; - return writtenShadowEnabled() && bytes >= writtenShadowMin() && bytes <= writtenShadowMax(); + return writtenShadowEnabled() && bytes >= writtenShadowMin() && bytes <= (region.swept ? std::max(writtenShadowMax(), SweptShadowMax) : writtenShadowMax()); } void GuestBufferMemory::UploadPrepare(bool addressable) { Require(!prepared && !uploaded, "guest memory was already uploaded"); prepared = true; - std::sort(regions.begin(), regions.end(), [](const Region& left, const Region& right) { return left.begin < right.begin; }); + std::vector order(regions.size()); + std::iota(order.begin(), order.end(), 0u); + std::sort(order.begin(), order.end(), [&](std::uint32_t left, std::uint32_t right) { return regions[left].begin < regions[right].begin; }); // Merged regions stay sparse when either part covered uncommitted pages. const auto mergeBacked = [](Region& into, const Region& from) { if (!into.sparse && !from.sparse) return; @@ -2069,7 +4381,9 @@ void GuestBufferMemory::UploadPrepare(bool addressable) { if (!into.sparse) into.backed.clear(); }; std::vector merged; - for (auto& region : regions) { + merged.reserve(regions.size()); + for (const auto index : order) { + auto& region = regions[index]; if (!merged.empty() && region.begin < merged.back().end) { auto& previous = merged.back(); if (previous.mirror != nullptr || region.mirror != nullptr) { @@ -2079,13 +4393,17 @@ void GuestBufferMemory::UploadPrepare(bool addressable) { // crossing the mirror's end cannot be served by it: this build copies the range. if (previous.mirror == nullptr) std::swap(previous, region); Require(region.mirror == nullptr, "image mirrors overlap"); - if (region.begin >= previous.begin && region.end <= previous.end) continue; + if (region.begin >= previous.begin && region.end <= previous.end && servesView(previous, std::max(region.lastView, region.begin))) continue; + if (previous.mirror->sparse) previous.backed = clipPieces(previous.mirror->committed, previous.begin, previous.end); previous.mirror = nullptr; previous.hostBacked = true; } mergeBacked(previous, region); + previous.lastView = std::max({previous.lastView, region.lastView, region.begin}); if (region.end > previous.end) previous.direct = nullptr; previous.atomic = previous.atomic || region.atomic; + previous.swept = previous.swept || region.swept; + previous.sparseHeap = previous.sparseHeap || region.sparseHeap; // A range starting before the mirror (only possible after the swap) keeps its prefix; the // earlier merged region ends at or before it, so the merged list stays sorted. previous.begin = std::min(previous.begin, region.begin); @@ -2136,9 +4454,15 @@ void GuestBufferMemory::UploadPrepare(bool addressable) { if (context.hostImportAlignment != 0) { auto& state = Imports(); std::lock_guard lock(state.mutex); + if (importFloor == 0) importFloor = state.useCounter + 1; const bool stale = importsStale(context, state); for (auto& region : regions) { if (region.mirror != nullptr) continue; + if (sparseHeapCandidate(region)) { + region.direct = nullptr; + region.pending = true; + continue; + } // An import found when the lease was acquired is reused while none was dropped since. const HostImport* entry = region.direct != nullptr && state.epoch == importsEpoch ? region.direct : nullptr; region.direct = nullptr; @@ -2159,7 +4483,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)); @@ -2195,7 +4519,7 @@ void GuestBufferMemory::UploadPrepare(bool addressable) { // GuestMemory::Read stores pending GPU results first (the flush hook locks for itself). for (auto& region : regions) { if (region.pending || region.mirror != nullptr || region.direct != nullptr) continue; - if (region.writable && WritesOverlap(region.begin, static_cast(region.end - region.begin))) { + if (sparseHeapCandidate(region) || (region.writable && WritesOverlap(region.begin, static_cast(region.end - region.begin)))) { region.pending = true; continue; } @@ -2203,11 +4527,39 @@ 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"); - uploaded = true; static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; const auto started = std::chrono::steady_clock::now(); + for (const auto& region : regions) CompletePendingCpuWrites(context, region.begin, static_cast(region.end - region.begin)); + if (space != nullptr) 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(); + std::lock_guard lock(state.mutex); + state.buildFloor = importFloor != 0 ? importFloor : state.useCounter + 1; + } // Registered ranges to import from. Address-based shaders hold their lease until write-back; // descriptor-only uploads acquire one only when the registry changed or an import must be made // (a lease copies every registered range's shared_ptr under the registry lock), so the guest can @@ -2237,6 +4589,17 @@ void GuestBufferMemory::UploadFinish(bool addressable) { ++Mirrors().subranges; } bool importsRefreshed = false; + if (context.hostImportAlignment != 0) { + auto& state = Imports(); + std::lock_guard lock(state.mutex); + if (importsStale(context, state)) refreshImports(context, state, importRanges()); + importsRefreshed = true; + for (const auto& region : regions) { + if (!region.pending || region.mirror != nullptr || region.direct != nullptr || sparseHeapCandidate(region)) continue; + if (findImport(state, region.begin, region.end) != nullptr || runCovers(importRanges(), region.begin, region.end)) continue; + static_cast(importJoined(context, state, region.begin, region.end, importRanges())); + } + } if (space != nullptr && context.hostImportAlignment != 0) { // The space's regions were confirmed against the imports when it was built; one check that // none was retired since (a mirror refresh or a flush between the stages can reconcile @@ -2273,6 +4636,12 @@ 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(); @@ -2287,14 +4656,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) { - if (const auto* range = containingRange(importRanges(), region.begin, region.end)) entry = importAllocation(context, state, range->address, range->bytes, 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. @@ -2308,7 +4704,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) { @@ -2378,39 +4774,27 @@ void GuestBufferMemory::copyRegion(Region& region, bool addressable) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; const auto bytes = region.end - region.begin; const auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | (addressable ? VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT : 0u); - if (profile) { - // Why the region is copied rather than bound in place, totalled every 1000 uploads (under a - // mutex: prepare stages of several builds copy at once). - static std::mutex reasonsMutex; - static std::uint64_t uploads = 0, noImport = 0, noImportBytes = 0, misaligned = 0, misalignedBytes = 0, snapshotOnly = 0, snapshotBytes = 0; - static std::map> outside; - bool inImport = false; - if (context.hostImportAlignment != 0) { - auto& state = Imports(); - std::lock_guard lock(state.mutex); - inImport = findImport(state, region.begin, region.end) != nullptr; - } - std::lock_guard lock(reasonsMutex); - if (!region.hostBacked && !region.writable) { ++snapshotOnly; snapshotBytes += bytes; } - else if (inImport) { ++misaligned; misalignedBytes += bytes; } - else { - ++noImport; - noImportBytes += bytes; - auto& bucket = outside[region.begin >> 28u]; - ++bucket.first; - bucket.second += bytes; - } - if (++uploads % 1000 == 0) { - // 'misaligned in imports' are the ones the GPU did not copy: over APS5_GPU_COPY_MAX_KIB, - // sparse, APS5_CPU_COPIES, no recorder, or an import gone by UploadFinish. - AgcDriver::ProfilePrint_nid_no_patch("[buffers] %llu copied regions on the CPU: %llu snapshots (%.0f MiB), %llu misaligned in imports (%.0f MiB), %llu outside imports (%.0f MiB):", static_cast(uploads), static_cast(snapshotOnly), snapshotBytes / 1048576.0, static_cast(misaligned), misalignedBytes / 1048576.0, static_cast(noImport), noImportBytes / 1048576.0); - for (const auto& [granule, counts] : outside) AgcDriver::ProfilePrint_nid_no_patch(" 0x%llx0000000:%llu/%.0fMiB", static_cast(granule), static_cast(counts.first), counts.second / 1048576.0); - AgcDriver::ProfilePrint_nid_no_patch("\n"); - } + std::uint64_t readBytes = 0; + bool inImport = false; + if (profile && context.hostImportAlignment != 0) { + auto& state = Imports(); + std::lock_guard lock(state.mutex); + inImport = 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)) { + const auto backedEnd = region.backed.empty() ? region.begin : region.backed.back().second; + const auto viewEnd = std::max(region.lastView, region.begin) + 4u; + constexpr std::uint64_t granule = 256; + const auto extent = std::max(backedEnd, viewEnd) - region.begin; + const auto end = region.begin + std::min((extent + granule - 1) / granule * granule, bytes); + region.bufferEnd = end < region.end ? end : 0; + } + 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(bytes), 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. @@ -2421,19 +4805,54 @@ void GuestBufferMemory::copyRegion(Region& region, bool addressable) { const bool written = region.writable && WritesOverlap(region.begin, static_cast(bytes)); if (!region.sparse) { GuestMemory::Read(region.begin, region.buffer->Bytes()); + readBytes += region.buffer->Bytes().size(); if (written) region.uploaded.assign(region.buffer->Bytes().begin(), region.buffer->Bytes().end()); } else { // `uploaded` holds the backed pieces back to back. + const auto target = region.buffer->Bytes().first(static_cast(allocated)); + std::size_t cursor = 0; for (const auto& [first, last] : region.backed) { - const auto piece = region.buffer->Bytes().subspan(static_cast(first - region.begin), static_cast(last - first)); + const auto at = static_cast(first - region.begin); + if (at > cursor) std::memset(target.data() + cursor, 0, at - cursor); + const auto piece = target.subspan(at, static_cast(last - first)); GuestMemory::Read(first, piece); + readBytes += piece.size(); if (written) region.uploaded.insert(region.uploaded.end(), piece.begin(), piece.end()); + cursor = at + piece.size(); } + if (cursor < target.size()) std::memset(target.data() + cursor, 0, target.size() - cursor); } } - else std::memcpy(region.buffer->Bytes().data(), region.snapshot.data(), region.snapshot.size()); + else { + if (!shared) std::memcpy(region.buffer->Bytes().data(), region.snapshot.data(), region.snapshot.size()); + readBytes += region.snapshot.size(); + } region.snapshot.clear(); if (profile) readUs.fetch_add(microsecondsSince(readStart), std::memory_order_relaxed); + if (profile) { + static std::mutex reasonsMutex; + static std::uint64_t uploads = 0, noImport = 0, noImportBytes = 0, misaligned = 0, misalignedBytes = 0, snapshotOnly = 0, snapshotBytes = 0; + static std::uint64_t logicalBytes = 0, readTotalBytes = 0; + static std::map> outside; + std::lock_guard lock(reasonsMutex); + const auto bufferBytes = region.buffer->Bytes().size(); + logicalBytes += bytes; + readTotalBytes += readBytes; + if (!region.hostBacked && !region.writable) { ++snapshotOnly; snapshotBytes += bufferBytes; } + else if (inImport) { ++misaligned; misalignedBytes += bufferBytes; } + else { + ++noImport; + noImportBytes += bufferBytes; + auto& bucket = outside[region.begin >> 28u]; + ++bucket.first; + bucket.second += bufferBytes; + } + if (++uploads % 1000 == 0) { + AgcDriver::ProfilePrint_nid_no_patch("[buffers] %llu completed CPU-copy operations: logical spans %.0f MiB, buffer spans %.0f MiB, bytes read %.0f MiB; %llu snapshots (%.0f MiB), %llu misaligned in imports (%.0f MiB), %llu outside imports (%.0f MiB):", static_cast(uploads), logicalBytes / 1048576.0, (snapshotBytes + misalignedBytes + noImportBytes) / 1048576.0, readTotalBytes / 1048576.0, static_cast(snapshotOnly), snapshotBytes / 1048576.0, static_cast(misaligned), misalignedBytes / 1048576.0, static_cast(noImport), noImportBytes / 1048576.0); + for (const auto& [granule, counts] : outside) AgcDriver::ProfilePrint_nid_no_patch(" 0x%llx0000000:%llu/%.0fMiB", static_cast(granule), static_cast(counts.first), counts.second / 1048576.0); + AgcDriver::ProfilePrint_nid_no_patch("\n"); + } + } } void GuestBufferMemory::recordGpuCopies(std::span copies, bool addressable) { @@ -2447,15 +4866,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) { @@ -2486,7 +4909,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) { @@ -2566,11 +4989,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 @@ -2604,25 +5031,38 @@ 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; - Require(address >= region.begin && address + bytes <= region.end && (region.buffer != nullptr || region.direct != nullptr || region.mirror != nullptr), "guest buffer view exceeds its GPU owner"); - const auto base = region.direct != nullptr ? region.direct->base : region.mirror != nullptr ? region.mirror->base : region.begin; + const auto describe = [&](const char* what) { + char text[256]; + std::snprintf(text, sizeof(text), "guest buffer view 0x%llx+0x%llx %s its GPU owner 0x%llx-0x%llx (buffer %d, direct %d, mirror %d)", static_cast(address), static_cast(bytes), what, static_cast(region.begin), static_cast(region.end), region.buffer != nullptr, region.direct != nullptr, region.mirror != nullptr); + 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.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)); - const auto range = ViewBytes(bytes, adjustment); - const auto end = region.direct != nullptr ? region.direct->base + region.direct->bytes : region.end; - Require(address - adjustment + range <= end, "guest buffer view exceeds its GPU owner"); + 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 || 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)); + } + if (address - adjustment + range > end) Require(false, describe("rounded up to its view range exceeds")); Require(range <= context.limits.maxStorageBufferRange, "guest buffer view exceeds descriptor range limit"); const auto handle = region.direct != nullptr ? region.direct->buffer : region.mirror != nullptr ? region.mirror->buffer->Handle() : region.buffer->Handle(); return {handle, offset - adjustment, range}; } 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) { @@ -2630,19 +5070,81 @@ 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.end, address, permissions, 0}; + return {region.begin, region.direct != nullptr ? inPlaceEnd(region) : servedEnd(region), address, permissions, 0}; +} + +std::uint64_t GuestBufferMemory::servedEnd(const Region& region) { + if (region.mirror != nullptr) return std::min(region.end, region.mirror->base + region.mirror->bytes); + 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; @@ -2669,7 +5171,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; } @@ -2686,27 +5188,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; @@ -2722,9 +5247,24 @@ bool GuestBufferMemory::HasCopiedWrites() const { return false; } +bool GuestBufferMemory::CopiedWritesOverlap(std::uint64_t address, std::size_t bytes) const { + if (bytes == 0) return false; + for (const auto& [begin, end] : writes) { + if (!(address < end && begin < address + bytes)) continue; + const auto* found = owner(begin, end); + if (found == nullptr) continue; + const auto& region = *found; + if (region.direct != nullptr) continue; + if (region.gpuCopy && (region.copiedBack || region.deviceLocal)) continue; + if (region.mirror != nullptr && !region.mirror->writable) continue; + return true; + } + return false; +} + void GuestBufferMemory::MarkDirectWrites() const { for (const auto& [begin, end] : writes) { - const auto* found = owner(begin); + 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)); @@ -2757,6 +5297,25 @@ void storeChanged(std::uint64_t address, std::span current, std if (cursor < address + current.size()) unchanged(cursor, address + current.size()); } +void storeSparse(ImageMirror& mirror, std::uint64_t address, std::span current, std::span original) { + constexpr std::uint64_t block = 65536; + const auto end = address + current.size(); + const auto forget = [&](std::uint64_t from, std::uint64_t to) { + const auto at = static_cast(from - address); + if (from >= to || std::memcmp(current.data() + at, original.data() + at, static_cast(to - from)) == 0) return; + for (auto index = from / block - mirror.base / block; index <= (to - 1) / block - mirror.base / block; ++index) mirror.generations[static_cast(index)] = 0; + }; + auto cursor = address; + for (const auto& [first, last] : GuestMemory::DescribeCommitted(address, current.size(), true).ranges) { + forget(cursor, first); + const auto at = static_cast(first - address); + const auto length = static_cast(last - first); + GuestMemory::WriteChanged(first, current.subspan(at, length), original.subspan(at, length)); + cursor = last; + } + forget(cursor, end); +} + std::vector> mergeWrites(std::vector> writes) { std::sort(writes.begin(), writes.end()); std::vector> merged; @@ -2772,11 +5331,13 @@ 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; - const auto bytes = mirror.buffer->Bytes().subspan(static_cast(begin - mirror.base), static_cast(end - begin)); + const auto served = std::min(end, servedEnd(*found)); + if (begin >= served) continue; + const auto bytes = mirror.buffer->Bytes().subspan(static_cast(begin - mirror.base), static_cast(served - begin)); heapReferences.emplace_back(begin, std::vector(bytes.begin(), bytes.end())); } } @@ -2787,7 +5348,7 @@ void GuestBufferMemory::WriteBack() { // The CPU keeps running while the GPU works, so storing whole ranges would roll back its writes to // bytes the shader never touched. Only runs that differ from the uploaded copy are stored. for (const auto& [begin, end] : merged) { - const auto* found = owner(begin); + const auto* found = owner(begin, end); Require(found != nullptr, "write-back range has no GPU owner"); const auto& region = *found; // Every branch below but the direct, copied-back and staged ones stores from the CPU. @@ -2804,12 +5365,15 @@ void GuestBufferMemory::WriteBack() { // refresh does not mistake the GPU's results for a guest change. auto& mirror = *region.mirror; const auto offset = static_cast(begin - mirror.base); - const auto length = static_cast(end - begin); + const auto served = std::min(end, servedEnd(region)); + if (mirror.heap && begin >= served) continue; + const auto length = static_cast((mirror.heap ? served : end) - begin); const auto current = mirror.buffer->Bytes().subspan(offset, length); if (mirror.heap) { const auto reference = std::find_if(heapReferences.begin(), heapReferences.end(), [&](const auto& entry) { return entry.first == begin; }); Require(reference != heapReferences.end() && reference->second.size() == length, "heap mirror write-back has no reference"); - storeChanged(begin, current, reference->second); + if (mirror.sparse) storeSparse(mirror, begin, current, reference->second); + else storeChanged(begin, current, reference->second); continue; } GuestMemory::WriteChanged(begin, current, std::span(mirror.shadow).subspan(offset, length)); @@ -2873,6 +5437,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 @@ -2884,27 +5451,73 @@ 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::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); + } + } + 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 58581a5588..e08b9e47fd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp @@ -54,7 +54,7 @@ float toSignedLodBias(std::uint32_t raw) { } -GuestSamplerResource DecodeSamplerResource(std::span words, bool unnormalizedProven, bool forceDegammaPaired) { +GuestSamplerResource DecodeSamplerResource(std::span words, bool unnormalizedProven, bool forceDegammaPaired, bool integerBorder) { Require(words.size() == 4, "guest sampler descriptor must contain 4 dwords"); const auto clampX = (words[0] >> 0u) & 0x7u; @@ -90,35 +90,30 @@ 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); - Require(!disableCubeWrap, "guest sampler descriptor disables seamless cube filtering which is not implemented"); 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"); - Require(!anisoOverride, "guest sampler descriptor uses an anisotropy override which is not implemented"); + static_cast(anisoOverride); Require(!blendZeroPrt, "guest sampler descriptor uses PRT blend-zero which is not implemented"); if (mipFilter > 2u) Require(false, "guest sampler descriptor uses an unknown mip filter " + std::to_string(mipFilter)); 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)); @@ -136,12 +131,12 @@ GuestSamplerResource DecodeSamplerResource(std::span words, VkBorderColor border; switch (borderColorType) { - case 0: border = VK_BORDER_COLOR_INT_TRANSPARENT_BLACK; break; - case 1: border = VK_BORDER_COLOR_INT_OPAQUE_BLACK; break; - case 2: border = VK_BORDER_COLOR_INT_OPAQUE_WHITE; break; + case 0: border = integerBorder ? VK_BORDER_COLOR_INT_TRANSPARENT_BLACK : VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK; break; + case 1: border = integerBorder ? VK_BORDER_COLOR_INT_OPAQUE_BLACK : VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK; break; + case 2: border = integerBorder ? VK_BORDER_COLOR_INT_OPAQUE_WHITE : VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE; break; default: if (readsBorderColor(clampX) || readsBorderColor(clampY) || readsBorderColor(clampZ)) throw std::runtime_error("AGC graphics: guest sampler descriptor uses a border color table which is not implemented"); - border = VK_BORDER_COLOR_INT_TRANSPARENT_BLACK; + border = integerBorder ? VK_BORDER_COLOR_INT_TRANSPARENT_BLACK : VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK; break; } @@ -162,9 +157,13 @@ GuestSamplerResource DecodeSamplerResource(std::span words, const std::array compareOps{VK_COMPARE_OP_NEVER, VK_COMPARE_OP_LESS, VK_COMPARE_OP_EQUAL, VK_COMPARE_OP_LESS_OR_EQUAL, VK_COMPARE_OP_GREATER, VK_COMPARE_OP_NOT_EQUAL, VK_COMPARE_OP_GREATER_OR_EQUAL, VK_COMPARE_OP_ALWAYS}; result.compareOp = compareOps.at(depthCompareFunc); result.forceDegamma = forceSrgb; + 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; @@ -174,4 +173,11 @@ GuestSamplerResource DecodeSamplerResource(std::span words, return result; } +std::optional> SingleLevelSamplerWords(std::span words, bool singleLevelImage, bool mipmappedImage) { + const auto filters = words[2]; + if (((filters >> 29u) & 1u) == 0 || !singleLevelImage || (!isAnisoFilter((filters >> 20u) & 3u) && !isAnisoFilter((filters >> 22u) & 3u))) return std::nullopt; + Require(!mipmappedImage, "guest sampler with ANISO_OVERRIDE is paired with both single-level and mipmapped images in one draw, which is not implemented"); + return std::array{words[0], words[1], filters & ~((2u << 20u) | (2u << 22u)), words[3]}; +} + } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index f97d82c064..3c81c9c97e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -2,6 +2,8 @@ #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" #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 @@ -43,17 +45,25 @@ 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 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) { 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 baseAddress = base40 << 8u; + auto baseAddress = base40 << 8u; Require(baseAddress != 0, "guest texture descriptor has a null base address"); const auto minLod = (words[1] >> 8u) & 0xfffu; @@ -75,14 +85,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; @@ -110,7 +120,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 @@ -123,10 +133,17 @@ GuestTextureResource DecodeTextureResource(std::span words) std::fprintf(stderr, "[gpu] texture DCC color transform is ignored (word6=0x%08x word7=0x%08x)\n", words[6], words[7]); } } - Require(bcSwizzle == 0, "guest texture descriptor uses a BC swizzle which is not implemented"); + Require(bcSwizzle <= 5u, "guest texture descriptor uses a reserved BC swizzle"); 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(!metaCompress && !writeCompress, "FMASK CMASK compression is not implemented"); + 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; @@ -137,19 +154,39 @@ 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); - // XOR swizzles fold a pipe/bank XOR into the low address bits; only unmodified 64 KiB bases are modeled. - Require(XorSwizzleMode(tileMode) == 0 || (baseAddress & (tileMode == TextureTileMode::kS4KBX || tileMode == TextureTileMode::kD4KBX ? 0xfffu : 0xffffu)) == 0, "guest texture descriptor combines an XOR swizzle with a pipe/bank XOR base which is not implemented"); + std::uint32_t pipeBankXor = 0; + if (XorSwizzleMode(tileMode) != 0) { + pipeBankXor = static_cast(baseAddress & (tileMode == TextureTileMode::kS4KBX || tileMode == TextureTileMode::kD4KBX ? 0xfffu : 0xffffu)); + baseAddress -= pipeBankXor; + Require(baseAddress != 0, "guest texture descriptor has a null base address"); + } switch (dimension) { case TextureDimension::k1D: Require(height == 1 && depth == 0 && baseArray == 0, "guest 1D texture descriptor has a nonzero height, depth or base array"); break; case TextureDimension::k2D: + if (depth != 0 && tileMode == TextureTileMode::kLinear) { + const auto elementsPerPitchUnit = 256u / BytesPerElement(format); + const auto defaultPitch = (width + elementsPerPitchUnit - 1u) / elementsPerPitchUnit * elementsPerPitchUnit; + Require(!IsBlockCompressed(format) && maxMip == 0 && baseArray == 0 && depth + 1u == defaultPitch, "guest 2D linear texture descriptor sets a custom pitch other than its default pitch, which is not implemented" + describe()); + depth = 0; + } 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"); @@ -166,8 +203,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; @@ -175,6 +212,7 @@ GuestTextureResource DecodeTextureResource(std::span words) GuestTextureResource result{}; result.baseAddress = baseAddress; + result.pipeBankXor = pipeBankXor; result.width = width; result.height = height; result.depthOrLastArray = depth; @@ -185,15 +223,18 @@ 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.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()); result.mipCount = lastLevel + 1u; @@ -206,6 +247,19 @@ float EffectiveMinLod(const GuestTextureResource& resource) { return std::min(static_cast(resource.minLod) / 256.0f, static_cast(resource.lastLevel)); } +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; @@ -214,8 +268,17 @@ 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"); } +bool ArrayLayersServeView(const GuestTextureResource& surface, const GuestTextureResource& view) { + if (view.dimension == surface.dimension && view.depthOrLastArray == surface.depthOrLastArray) return true; + if (surface.dimension != TextureDimension::k2DArray) return false; + if (view.dimension == TextureDimension::k2D) return view.baseArray == 0; + return view.dimension == TextureDimension::k2DArray && view.depthOrLastArray <= surface.depthOrLastArray; +} + } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/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 new file mode 100644 index 0000000000..f1504e9fc6 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -0,0 +1,1048 @@ +#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/Execution/include/GuestMemory.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include +#include +#include +#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); +} + +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; +} + +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{}; + for (std::size_t offset = 0; offset < texels.size(); offset += target.elementBytes) std::memcpy(texels.data() + offset, target.clearWords.data(), target.elementBytes); + std::array pattern{}; + std::memcpy(pattern.data(), texels.data(), texels.size()); + VkClearColorValue clear{}; + Require(ClearColorForTexel(target.format, target.elementBytes, pattern, clear), "the fast-clear color of a multisampled target has no exact clear value in its format"); + return clear; +} + +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 : 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"); + 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{}; + 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; + try { + const auto created = allocate(usage, orderFlags); + image = created.image; + memory = created.memory; + view = created.view; + allocationBytes = created.bytes; + recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { + 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); + }); + } catch (...) { + release(); + throw; + } + } + ~MultisampleTarget() { release(); } + 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; + 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({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)); + } + + 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{}; + 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}; + context.Resolved(&DeviceFunctions::cmdClearColorImage, "vkCmdClearColorImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &clear, 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); + }); + } + + void Recreate(const ColorTarget& use) { + SettleStorageWrites(); + target = use; + cmask = MultisampledCmask(use.cmaskAddress, use.cmaskBytes); + colorFill = MultisampledColorFill(use); + fromTexture = false; + dccRanges.clear(); + 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()}; + } + + VkRenderPass ResolvePass() { + if (resolvePass != VK_NULL_HANDLE) return resolvePass; + std::array attachments{}; + attachments[0].format = target.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].samples = VK_SAMPLE_COUNT_1_BIT; + attachments[1].loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_STORE; + for (auto& attachment : attachments) { + attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachment.initialLayout = VK_IMAGE_LAYOUT_GENERAL; + attachment.finalLayout = VK_IMAGE_LAYOUT_GENERAL; + } + const VkAttachmentReference color{0, VK_IMAGE_LAYOUT_GENERAL}; + const VkAttachmentReference resolve{1, VK_IMAGE_LAYOUT_GENERAL}; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &color; + subpass.pResolveAttachments = &resolve; + VkRenderPassCreateInfo info{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + info.attachmentCount = static_cast(attachments.size()); + info.pAttachments = attachments.data(); + info.subpassCount = 1; + info.pSubpasses = &subpass; + Check(context.Function("vkCreateRenderPass")(context.device, &info, nullptr, &resolvePass), "vkCreateRenderPass resolve"); + return resolvePass; + } + + 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; + 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; + std::vector retired; + VkImageView storageView = VK_NULL_HANDLE; + bool storageWritten = false; + VkDeviceSize allocationBytes = 0; + 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"); + } + } + std::vector dcc(dccRanges[1].bytes); + GuestMemory::Read(use.dccAddress, dcc); + std::vector cmask(use.cmaskBytes); + if (!cmask.empty()) GuestMemory::Read(use.cmaskAddress, cmask); + const auto clear = MultisampledDccClear(use, dcc, cmask); + Require(GuestMemory::UnchangedSinceAll(dccRanges), "initial multisampled DCC contents changed during import"); + return clear; + } + + void 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" : "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"); + } + } + + void release() noexcept { + textures.clear(); + retiredTextures.clear(); + if (resolvePass) context.Function("vkDestroyRenderPass")(context.device, resolvePass, nullptr); + if (orderView) context.Function("vkDestroyImageView")(context.device, orderView, nullptr); + if (storageView) context.Function("vkDestroyImageView")(context.device, storageView, nullptr); + destroy({image, memory, view}); + for (const auto& allocation : retired) destroy(allocation); + } +}; + +struct GuardedRange { + VkDevice device; + std::uint64_t address; + std::uint64_t bytes; + std::uint32_t samples; + std::shared_ptr> active; + std::shared_ptr> metadataValid; +}; + +class PrimedSharedMutex : public std::shared_mutex { +public: + PrimedSharedMutex() { + lock(); + unlock(); + } +}; + +std::shared_mutex& guardedMutex() { + static PrimedSharedMutex mutex; + return mutex; +} + +std::vector& guarded() { + static std::vector ranges; + return ranges; +} + +std::atomic& anyGuarded() { + static std::atomic any{false}; + return any; +} + +void guardReads(std::uint64_t address, std::size_t bytes) { + RequireNoMultisampleTarget(address, bytes, "a CPU read"); +} + +void retireUnmapped(std::uintptr_t address, std::size_t bytes) { + RetireUnmappedDepth_nid_no_patch(address, bytes); + std::unique_lock lock(guardedMutex()); + std::erase_if(guarded(), [&](const auto& range) { + if (range.metadataValid && address < range.address + range.bytes && range.address < address + bytes) range.metadataValid->store(false, std::memory_order_release); + if (range.address < address || range.address - address > bytes || range.bytes > bytes - (range.address - address)) return false; + 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, 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 (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; + const auto blockWidth = 1u << log2Width; + const auto blockHeight = 1u << (log2Pixels - log2Width); + const auto align = [](std::uint32_t extent, std::uint32_t block) { return (static_cast(extent) + block - 1u) / block * block; }; + bytes = align(target.extent.width, blockWidth) * align(target.extent.height, blockHeight) * target.elementBytes * target.samples; + } + std::unique_lock lock(guardedMutex()); + guarded().push_back({context.device, target.address, bytes, target.samples, active}); + if (target.dccAddress != 0) { + guarded().push_back({context.device, target.dccAddress, MultisampledDccBytes(target), target.samples, active, metadataValid}); + if (target.cmaskAddress != 0) guarded().push_back({context.device, target.cmaskAddress, target.cmaskBytes, target.samples, active, metadataValid}); + } + anyGuarded().store(true, std::memory_order_release); + InstallResidentImageReadGuard_nid_no_patch(); +} + +std::mutex& targetsMutex() { + static std::mutex mutex; + return mutex; +} + +std::vector>& targets() { + static std::vector> list; + return list; +} + +void discardRetiredTargets() { + std::erase_if(targets(), [](const auto& target) { return !target->active->load(std::memory_order_acquire); }); +} + +MultisampleTarget& acquire(const Context& context, const ColorTarget& use) { + 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 (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; + } + 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); + 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) || 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) { + if (address == 0 || fillAddress >= address + bytes || address >= fillAddress + fillBytes) return; + if (fillAddress > address || fillAddress + fillBytes < address + bytes) { + char text[192]; + std::snprintf(text, sizeof(text), "a fill of 0x%llx+0x%zx partly covers the CMASK 0x%llx+0x%llx of a multisampled color target, leaving its tiles mixed", static_cast(fillAddress), fillBytes, static_cast(address), static_cast(bytes)); + refusal = text; + return; + } + refusal.clear(); + const auto code = static_cast(pattern); + if (const auto filled = MultisampledCmaskKeys(code); filled && pattern == code * 0x01010101u) { + keys = *filled; + } else { + 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::SeedCode(std::optional code) { + if (const auto seeded = code ? MultisampledCmaskKeys(*code) : std::nullopt) { + keys = *seeded; + return; + } + char text[192]; + 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; +} + +void MultisampledCmask::CheckAddress(std::uint64_t cmaskAddress) const { + if (cmaskAddress == address) return; + char text[192]; + std::snprintf(text, sizeof(text), "a multisampled color target used CMASK 0x%llx after its first use with CMASK 0x%llx, which is not modeled", static_cast(cmaskAddress), static_cast(address)); + Require(false, std::string(text)); +} + +bool MultisampledCmask::TakeClear() { + Require(refusal.empty(), refusal); + if (keys != DccKeys::Clear0000) return false; + keys = DccKeys::Uncompressed; + return true; +} + +std::uint64_t MultisampledColorBytes(const ColorTarget& target) { + 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; + const auto widthLog2 = (elementsLog2 + ((samplesLog2 & 1u) == 0 ? 1u : 0u)) / 2u; + const std::uint64_t blockWidth = std::uint64_t{1} << widthLog2; + const std::uint64_t blockHeight = std::uint64_t{1} << (elementsLog2 - widthLog2); + return ((target.extent.width + blockWidth - 1) / blockWidth) * ((target.extent.height + blockHeight - 1) / blockHeight) << blockLog2; +} + +std::size_t MultisampledDccBytes(const ColorTarget& target) { + Require(target.samples == 2 || target.samples == 4 || target.samples == 8, "unsupported multisampled DCC fragment count"); + Require(target.tileMode == ColorTileMode::RenderTarget && target.dccPipeAligned, "multisampled DCC requires pipe-aligned SW_64KB_R_X"); + Require(std::has_single_bit(target.elementBytes) && target.elementBytes <= 16, "unsupported multisampled DCC element size"); + Require(target.extent.width != 0 && target.extent.width <= 16384 && target.extent.height != 0 && target.extent.height <= 16384, "invalid multisampled DCC extent"); + Require(target.mipCount == 1 && target.mip == 0 && !target.mipTail && target.depth == 1 && target.depthSlice == 0 && target.arrayLastSlice == 0 && target.arraySlice == 0 && target.layers == 1, "multisampled DCC array, volume or mip contents are not implemented"); + const auto bits = 19u - static_cast(std::countr_zero(target.elementBytes)); + const std::uint64_t width = std::uint64_t{1} << ((bits + 1u) / 2u); + const std::uint64_t height = std::uint64_t{1} << (bits / 2u); + return static_cast(((target.extent.width + width - 1) / width) * ((target.extent.height + height - 1) / height) * 4096u); +} + +VkClearColorValue MultisampledDccClear(const ColorTarget& target, std::span dcc, std::span cmask) { + Require(target.dccAddress != 0 && (target.dccControl & 0x3ff80u) == 0 && (target.pipeBankXor & ~0xcf00u) == 0, "multisampled DCC address, color transform, lossy mode or XOR is not implemented"); + Require(dcc.size() == MultisampledDccBytes(target), "multisampled DCC metadata snapshot size mismatch"); + Require(std::ranges::all_of(dcc, [&](std::byte key) { return key == dcc.front(); }), "mixed multisampled DCC metadata is not implemented"); + Require(target.cmaskAddress == 0 ? cmask.empty() && target.cmaskBytes == 0 : cmask.size() == target.cmaskBytes && target.cmaskBytes == CmaskBytes(target.extent.width, target.extent.height), "multisampled DCC CMASK snapshot size mismatch"); + if (!cmask.empty()) { + const auto code = std::to_integer(cmask.front()); + const auto expanded = target.samples == 2 ? 0xddu : target.samples == 4 ? 0xeeu : 0xffu; + Require(std::ranges::all_of(cmask, [&](std::byte key) { return key == cmask.front(); }) && (code == 0xccu || code == expanded), "multisampled DCC CMASK compression or register clear is not implemented"); + } + DccKeys keys; + switch (std::to_integer(dcc.front())) { + case 0x00u: keys = DccKeys::Clear0000; break; + case 0x40u: keys = DccKeys::Clear0001; break; + case 0x80u: keys = DccKeys::Clear1110; break; + case 0xc0u: keys = DccKeys::Clear1111; break; + default: throw std::runtime_error("initial multisampled DCC contents are not a supported uniform fast clear"); + } + std::array texels{}; + Require(FillDccClear(target.format, keys, target.dccAlphaOnMsb, texels), "multisampled DCC clear format is not implemented"); + std::array pattern{}; + std::memcpy(pattern.data(), texels.data(), texels.size()); + VkClearColorValue clear{}; + Require(ClearColorForTexel(target.format, target.elementBytes, pattern, clear), "multisampled DCC fast clear has no exact host value"); + return clear; +} + +bool MultisampledFillClearColor(const ColorTarget& target, std::uint32_t fill, VkClearColorValue& clear) { + const std::array pattern{fill, fill, fill, fill}; + VkFormat unorm = VK_FORMAT_UNDEFINED; + switch (target.format) { + case VK_FORMAT_R8G8B8A8_SRGB: unorm = VK_FORMAT_R8G8B8A8_UNORM; break; + case VK_FORMAT_B8G8R8A8_SRGB: unorm = VK_FORMAT_B8G8R8A8_UNORM; break; + case VK_FORMAT_A8B8G8R8_SRGB_PACK32: unorm = VK_FORMAT_A8B8G8R8_UNORM_PACK32; break; + default: return ClearColorForTexel(target.format, target.elementBytes, pattern, clear); + } + if (!ClearColorForTexel(unorm, target.elementBytes, pattern, clear)) return false; + return std::all_of(clear.float32, clear.float32 + 3, [](float value) { return value == 0.0f || value == 1.0f; }); +} + +MultisampledColorFill::MultisampledColorFill(const ColorTarget& target) : address(target.address), bytes(MultisampledColorBytes(target)) {} + +void MultisampledColorFill::NoteFill(std::uint64_t fillAddress, std::size_t fillBytes, std::uint32_t pattern) { + if (bytes == 0 ? fillAddress > address || address >= fillAddress + fillBytes : fillAddress >= address + bytes || address >= fillAddress + fillBytes) return; + char text[192]; + if (bytes == 0) { + std::snprintf(text, sizeof(text), "a fill of 0x%llx+0x%zx covers the color memory at 0x%llx of a multisampled target whose footprint is not modeled", static_cast(fillAddress), fillBytes, static_cast(address)); + refusal = text; + return; + } + if (fillAddress > address || fillAddress + fillBytes < address + bytes) { + std::snprintf(text, sizeof(text), "a fill of 0x%llx+0x%zx partly covers the color memory 0x%llx+0x%llx of a multisampled target, leaving its samples mixed", static_cast(fillAddress), fillBytes, static_cast(address), static_cast(bytes)); + refusal = text; + return; + } + refusal.clear(); + filled = pattern; +} + +std::optional MultisampledColorFill::TakeFill() { + Require(refusal.empty(), refusal); + return std::exchange(filled, std::nullopt); +} + +void MultisampledColorFill::Discard() { + refusal.clear(); + filled.reset(); +} + +bool TakeMultisampledClear(MultisampledCmask& cmask, MultisampledColorFill& colorFill, const ColorTarget& use, VkClearColorValue& clear) { + if (cmask.TakeClear()) { + colorFill.Discard(); + clear = fastClearColor(use); + return true; + } + const auto fill = colorFill.TakeFill(); + if (!fill) return false; + Require(MultisampledFillClearColor(use, *fill, clear), "a fill over the color memory of a multisampled target has no exact clear value in its format"); + return true; +} + +SampleOrderTarget MultisampleTargetOrderTarget(const Context& context, const ColorTarget& target) { + std::lock_guard lock(targetsMutex()); + 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()); + const auto& acquired = acquire(context, target); + if (image != nullptr) *image = acquired.image; + return acquired.view; +} + +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; +} + +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); + target.Keep(); + const std::array views{target.view, destinationView}; + auto framebuffer = std::make_shared(context, target.ResolvePass(), 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.framebuffer = framebuffer->Handle(); + begin.renderArea = {{0, 0}, source.extent}; + context.Resolved(&DeviceFunctions::cmdBeginRenderPass, "vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); + context.Resolved(&DeviceFunctions::cmdEndRenderPass, "vkCmdEndRenderPass")(commands); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_COLOR_ATTACHMENT_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); + if (recorder != nullptr) { + Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); + recorder->Keep(framebuffer); + } + }); + destination.MarkDirty(); +} + +bool MultisampledColorFillAbsorbs(std::uint64_t address, std::size_t bytes, std::uint32_t pattern) { + if (bytes == 0) return false; + std::lock_guard lock(targetsMutex()); + return std::any_of(targets().begin(), targets().end(), [&](const auto& target) { + VkClearColorValue clear{}; + return target->active->load(std::memory_order_acquire) && target->target.address == address && MultisampledColorBytes(target->target) == bytes && MultisampledFillClearColor(target->target, pattern, clear); + }); +} + +void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern) { + std::lock_guard lock(targetsMutex()); + for (const auto& target : targets()) { + if (!target->active->load(std::memory_order_acquire)) continue; + target->colorFill.NoteFill(address, bytes, pattern); + 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; + static std::atomic reports{0}; + if (reports.fetch_add(1) < 8) std::fprintf(stderr, "[gpu] multisampled color target 0x%llx fast-cleared through its CMASK 0x%llx (fill 0x%llx+0x%zx)\n", static_cast(target->target.surfaceAddress), static_cast(target->target.cmaskAddress), static_cast(address), bytes); + } +} + +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); + 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()) { + if (address >= range.address + range.bytes || range.address >= address + bytes) continue; + char text[256]; + std::snprintf(text, sizeof(text), "AGC graphics: %s of 0x%llx+0x%zx overlaps multisampled color target 0x%llx+0x%llx (%u samples), whose samples are only on the host: guest access to it is not implemented", access, static_cast(address), bytes, static_cast(range.address), static_cast(range.bytes), range.samples); + throw std::runtime_error(text); + } +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ParallelCompare.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ParallelCompare.cpp new file mode 100644 index 0000000000..7f83917b88 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ParallelCompare.cpp @@ -0,0 +1,165 @@ +#include "prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::Graphics { +namespace { + +std::atomic runningHelpers{0}; + +class ComparePool { +public: + explicit ComparePool(unsigned count) { + 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) { + std::unique_lock caller(callers, std::try_to_lock); + 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; + next.store(0, std::memory_order_relaxed); + done.store(0, std::memory_order_relaxed); + ++generation; + } + wake.notify_all(); + drain(spans, equal); + std::unique_lock lock(mutex); + finished.wait(lock, [&] { return working == 0 && done.load(std::memory_order_acquire) == spans.size(); }); + jobSpans = {}; + jobEqual = {}; + 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; + for (auto index = next.fetch_add(1, std::memory_order_relaxed); index < spans.size(); index = next.fetch_add(1, std::memory_order_relaxed)) { + const auto& span = spans[index]; + equal[index] = std::memcmp(span.first, span.second, span.bytes) == 0 ? 1 : 0; + ++compared; + } + if (compared != 0) done.fetch_add(compared, std::memory_order_acq_rel); + } + + void work() { + std::uint64_t seen = 0; + std::unique_lock lock(mutex); + for (;;) { + wake.wait(lock, [&] { return generation != seen || stopping; }); + if (stopping) break; + seen = generation; + const auto spans = jobSpans; + const auto equal = jobEqual; + ++working; + lock.unlock(); + drain(spans, equal); + lock.lock(); + --working; + if (working == 0) finished.notify_all(); + } + runningHelpers.fetch_sub(1, std::memory_order_acq_rel); + } + + std::mutex callers; + std::mutex mutex; + std::condition_variable wake; + std::condition_variable finished; + std::uint64_t generation = 0; + unsigned working = 0; + std::span jobSpans; + 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 = [] { + std::lock_guard lifecycle(poolLifecycle()); + auto* created = new ComparePool(poolStopped() ? 0u : CompareHelpers()); + createdPool().store(created, std::memory_order_release); + return created; + }(); + return pool; +} + +} + +unsigned CompareHelpers() { + static const unsigned helpers = std::clamp(std::thread::hardware_concurrency() / 8u, 1u, 4u); + return helpers; +} + +void CompareSpansFrom(std::span spans, std::span equal, std::size_t parallelBytes) { + std::size_t total = 0; + for (const auto& span : spans) total += span.bytes; + if (spans.size() > 1 && total >= parallelBytes) { + if (sharedPool()->Run(spans, equal)) return; + } + for (std::size_t index = 0; index < spans.size(); ++index) equal[index] = std::memcmp(spans[index].first, spans[index].second, spans[index].bytes) == 0 ? 1 : 0; +} + +void StopCompareHelpers() { + ComparePool* pool = nullptr; + { + std::lock_guard lifecycle(poolLifecycle()); + poolStopped() = true; + pool = createdPool().load(std::memory_order_acquire); + } + if (pool != nullptr) pool->Stop(); +} + +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 new file mode 100644 index 0000000000..58673799a3 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PassHazards.cpp @@ -0,0 +1,99 @@ +#include "prx/libSceAgcDriver/Graphics/include/PassHazards.hpp" +#include + +namespace AgcDriver::Graphics { + +void GuestRangeSet::Insert(std::uint64_t begin, std::uint64_t end) { + if (begin >= end) return; + auto first = std::lower_bound(ranges.begin(), ranges.end(), begin, [](const auto& range, std::uint64_t value) { return range.second < value; }); + auto last = first; + while (last != ranges.end() && last->first <= end) { + begin = std::min(begin, last->first); + end = std::max(end, last->second); + ++last; + } + if (first == last) { + ranges.insert(first, {begin, end}); + return; + } + *first = {begin, end}; + ranges.erase(first + 1, last); +} + +bool GuestRangeSet::Overlaps(std::uint64_t begin, std::uint64_t end) const { + if (begin >= end) return false; + const auto it = std::upper_bound(ranges.begin(), ranges.end(), begin, [](std::uint64_t value, const auto& range) { return value < range.second; }); + return it != ranges.end() && it->first < end; +} + +bool PassHazards::overlapsAny(const GuestRangeSet& set, GuestRangeList ranges) const { + if (set.Empty()) return false; + return std::any_of(ranges.begin(), ranges.end(), [&](const auto& range) { return set.Overlaps(range.first, range.second); }); +} + +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) || nonEmpty(use.writeOnly); + }; + const bool earlierTouches = anyReads || anyWrites || !reads.Empty() || !writes.Empty() || !writeOnly.Empty(); + if ((access.anyWrites && earlierTouches) || (anyWrites && touchesMemory(access))) return PassHazard::UnknownWrite; + 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; + if (std::any_of(images.begin(), images.end(), [&](const auto& seen) { return seen.first == image && conflicts(seen.second, written); })) return PassHazard::Image; + if (std::any_of(attachments.begin(), attachments.end(), [&](const Attachment& seen) { return seen.image == image && conflicts(seen.written, written); })) return PassHazard::Image; + } + for (const auto& [image, written] : access.attachments) { + if (image == VK_NULL_HANDLE) continue; + if (std::any_of(images.begin(), images.end(), [&](const auto& seen) { return seen.first == image && conflicts(seen.second, written); })) return PassHazard::Image; + if (std::any_of(attachments.begin(), attachments.end(), [&](const Attachment& seen) { return seen.image == image && !(samePass && seen.pass == pass) && conflicts(seen.written, written); })) return PassHazard::Image; + } + return PassHazard::None; +} + +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; }); + if (it == images.end()) images.emplace_back(image, written); + else it->second = it->second || written; + } + for (const auto& [image, written] : access.attachments) { + if (image == VK_NULL_HANDLE) continue; + const auto it = std::find_if(attachments.begin(), attachments.end(), [&](const Attachment& seen) { return seen.image == image && seen.pass == pass; }); + if (it == attachments.end()) attachments.push_back({image, written, pass}); + else it->written = it->written || written; + } + anyReads = anyReads || access.anyReads; + 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() && 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 3cd41ef7d2..b03bb53e22 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -1,10 +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 @@ -63,17 +70,24 @@ void LogPipelineStatistics_nid_no_patch(const Context& context, VkPipeline pipel } } -Framebuffer::Framebuffer(const Context& context, VkRenderPass renderPass, std::span targets, VkExtent2D extent) : context(context) { +bool HasStencil(VkFormat format) { + return format == VK_FORMAT_D16_UNORM_S8_UINT || format == VK_FORMAT_D24_UNORM_S8_UINT || format == VK_FORMAT_D32_SFLOAT_S8_UINT || format == VK_FORMAT_S8_UINT; +} + +Framebuffer::Framebuffer(const Context& context, VkRenderPass renderPass, std::span targets, VkExtent2D extent, std::uint32_t layers) : context(context) { // Cached objects outlive their device's teardown; they must not keep its buffer pool alive past it. this->context.bufferPool.reset(); Require(extent.width != 0 && extent.height != 0 && extent.width <= context.limits.maxFramebufferWidth && extent.height <= context.limits.maxFramebufferHeight, "framebuffer extent exceeds device limits"); + Require(layers != 0 && layers <= context.limits.maxFramebufferLayers, "framebuffer layers exceed device limits"); + views.assign(targets.begin(), targets.end()); + if (renderPass == VK_NULL_HANDLE) return; VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; framebufferInfo.renderPass = renderPass; framebufferInfo.attachmentCount = static_cast(targets.size()); framebufferInfo.pAttachments = targets.empty() ? nullptr : targets.data(); framebufferInfo.width = extent.width; framebufferInfo.height = extent.height; - framebufferInfo.layers = 1; + framebufferInfo.layers = layers; Check(context.Function("vkCreateFramebuffer")(context.device, &framebufferInfo, nullptr, &framebuffer), "vkCreateFramebuffer"); } @@ -85,6 +99,40 @@ 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) { + 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.flatP10; }), "v_interp_mov p10 of a flat input with the first provoking vertex is not measured"); + } + return; + } + Require(context.provokingVertexLast, "last provoking vertex requires VK_EXT_provoking_vertex with provokingVertexLast enabled"); + const bool meshPrimitives = state.stages.path == ShaderPath::Geometry && state.stages.mesh; + Require(!BarycentricPatches(state, shaders) && !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_STRIP && state.topology != VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN), "last provoking vertex is unsupported for triangle strips or 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.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.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; +} + void ValidateViewport(const Context& context, const VkViewport& viewport) { Require(std::isfinite(viewport.minDepth) && std::isfinite(viewport.maxDepth), "non-finite viewport depth range"); Require(context.depthRangeUnrestricted || (viewport.minDepth >= 0 && viewport.minDepth <= 1 && viewport.maxDepth >= 0 && viewport.maxDepth <= 1), "viewport depth outside [0, 1] requires VK_EXT_depth_range_unrestricted"); @@ -94,19 +142,203 @@ void ValidateViewport(const Context& context, const VkViewport& viewport) { Require(std::min(viewport.y, viewport.y + viewport.height) >= context.limits.viewportBoundsRange[0] && std::max(viewport.y, viewport.y + viewport.height) <= context.limits.viewportBoundsRange[1], "viewport Y exceeds device bounds"); } -Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& 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) { +bool ShadersWriteMemory(std::span shaders) { + const auto any = [](const std::vector& written, std::uint32_t count) { return written.size() < count || std::ranges::any_of(written, [](bool element) { return element; }); }; + for (const auto& shader : shaders) { + if (shader.program == nullptr) continue; + for (const auto& binding : shader.program->bindings) { + switch (binding.role) { + case ShaderRecompiler::DescriptorRole::GuestBuffers: + if (binding.kind != ShaderRecompiler::DescriptorKind::UniformBuffer && binding.kind != ShaderRecompiler::DescriptorKind::UniformTexelBuffer && any(binding.bufferWritten, binding.count)) return true; + break; + case ShaderRecompiler::DescriptorRole::GuestImages: + if (binding.kind == ShaderRecompiler::DescriptorKind::StorageImage && any(binding.imageWritten, binding.count)) return true; + break; + case ShaderRecompiler::DescriptorRole::Gds: + case ShaderRecompiler::DescriptorRole::BdaPagetable: + return true; + case ShaderRecompiler::DescriptorRole::GuestSamplers: + case ShaderRecompiler::DescriptorRole::FaultBuffer: + case ShaderRecompiler::DescriptorRole::FlattenedSrt: + case ShaderRecompiler::DescriptorRole::ShaderData: + break; + } + } + } + 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; +thread_local bool prebuilt = false; + +VkShaderStageFlags pushStagesOf(const Context& context) { + return PreRasterizationPushStages(context.meshShader, context.tessellationShader, context.geometryShader); +} + +template +void appendKey(std::vector& key, const TValue& value) { + static_assert(std::is_trivially_copyable_v); + const auto bytes = std::as_bytes(std::span(&value, 1)); + key.insert(key.end(), bytes.begin(), bytes.end()); +} + +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 || BarycentricPatches(state, shaders) || SplitsAlternateAlpha(state)) return false; + keys = {}; + for (const auto& shader : shaders) { + 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, identity.low); + appendKey(key, identity.high); + appendKey(key, shader.pushConstantOffset); + if (resources.split) { + auto& sets = shader.stage == Stage::Fragment ? keys.fragmentSets : keys.preRasterizationSets; + for (const auto& binding : shader.program->bindings) { + Require(binding.descriptorSet < 2u, "split pipeline library descriptor set exceeds two slots"); + sets |= 1u << binding.descriptorSet; + } + } + } + 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); + appendKey(keys.vertexInput, binding.inputRate); + } + appendKey(keys.vertexInput, input.attributes.size()); + for (const auto& attribute : input.attributes) { + appendKey(keys.vertexInput, attribute.location); + appendKey(keys.vertexInput, attribute.binding); + appendKey(keys.vertexInput, attribute.format); + appendKey(keys.vertexInput, attribute.offset); + } + appendKey(keys.vertexInput, state.topology); + appendKey(keys.vertexInput, state.primitiveRestart); + appendKey(keys.preRasterization, state.provokingVertexMode); + appendKey(keys.preRasterization, state.negativeOneToOne); + appendKey(keys.preRasterization, state.depthClamp && context.depthClamp); + appendKey(keys.preRasterization, state.conservativeRasterization); + appendKey(keys.preRasterization, state.depth.has_value() && state.depthBias); + appendKey(keys.preRasterization, state.stages.tessellation.has_value()); + 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); + 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) { + 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); + for (const auto value : state.blendConstants) appendKey(keys.fragmentOutput, value); + appendKey(keys.renderPass, state.blends.size()); + for (const auto& color : state.colors) { + appendKey(keys.renderPass, color.format); + appendKey(keys.renderPass, color.exportIndex); + } + 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); + keys.preRasterizationLayout = keys.fragmentLayout = keys.layout; + return true; + } + const auto splitKey = [&](std::vector& key, std::uint32_t sets) { + appendKey(key, ~0u); + appendKey(key, sets); + appendKey(key, pushStagesOf(context)); + if ((sets & 1u) != 0u) { + appendKey(key, resources.mainKey.size()); + for (const auto word : resources.mainKey) appendKey(key, word); + } + if ((sets & 2u) != 0u) { + appendKey(key, resources.fragmentKey.size()); + for (const auto word : resources.fragmentKey) appendKey(key, word); + } + }; + splitKey(keys.layout, 3u); + splitKey(keys.preRasterizationLayout, keys.preRasterizationSets); + splitKey(keys.fragmentLayout, keys.fragmentSets); + return true; +} + +} + +Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout) : Pipeline(context, state, vertexInput, resources.SetLayouts(), shaders, attachmentLayout) {} + +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(); Require(state.blends.size() == (state.colors.empty() ? 0u : state.colors.back().exportIndex + 1u) && state.colors.size() <= state.blends.size(), "blend states do not match decoded color state"); Require(state.blends.size() <= context.limits.maxColorAttachments, "color targets exceed device attachment limits"); - Require(state.hasColorTarget || (context.limits.framebufferNoAttachmentsSampleCounts & VK_SAMPLE_COUNT_1_BIT) != 0, "device does not support single-sample rendering without attachments"); + Require(state.hasColorTarget || (context.limits.framebufferNoAttachmentsSampleCounts & static_cast(state.samples)) != 0, "device does not support rendering without attachments at the draw's sample count"); + Require(state.samples == 1 || !state.depth.has_value() || (context.limits.framebufferDepthSampleCounts & static_cast(state.samples)) != 0, "device does not support depth targets with the draw's sample count"); 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.conservativeRasterization == VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT || context.conservativeRasterization, "conservative rasterization requires VK_EXT_conservative_rasterization with at most 1/256 pixel of overestimation and degenerate triangles rasterized"); + ValidateProvokingVertex(context, state, shaders); if (state.rectList) Require(context.tessellationShader && context.limits.maxTessellationPatchSize >= 4, "rect-list requires tessellation with four output control points"); + const bool barycentricPatches = BarycentricPatches(state, shaders); + if (barycentricPatches) Require(context.tessellationShader && state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST && !state.primitiveRestart, "emulated barycentrics through tessellation require a triangle list without primitive restart"); + if (std::any_of(shaders.begin(), shaders.end(), [](const CompiledShader& shader) { return shader.stage == ShaderRecompiler::ShaderStage::Geometry; })) Require(context.geometryShader, "device does not support geometry shaders"); if (state.stages.tessellation) { Require(context.tessellationShader, "device does not support tessellation shaders"); Require(state.stages.tessellation->inputControlPoints <= context.limits.maxTessellationPatchSize && state.stages.tessellation->outputControlPoints <= context.limits.maxTessellationPatchSize, "tessellation patch exceeds device limits"); @@ -119,41 +351,51 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput Require(mesh.maxVertices <= context.meshLimits.maxMeshOutputVertices && mesh.maxPrimitives <= context.meshLimits.maxMeshOutputPrimitives && static_cast(mesh.ldsSizeDwords) * 4 <= context.meshLimits.maxMeshSharedMemorySize, "mesh output or LDS exceeds device limits"); } const auto pushStages = PushConstantStages(shaders); - Require(pushStages == 0 || context.limits.maxPushConstantsSize >= PipelinePushConstantBytes, "graphics push constant range exceeds device limit"); + Require(pushStages == 0 || context.limits.maxPushConstantsSize >= PushBlockBytes(dynamicRendering), "graphics push constant range exceeds device limit"); + if (dynamicRendering) { + for (const auto& shader : shaders) Require(shader.program->pushConstants.empty() || (shader.pushConstantOffset >= PipelinePushSlotBytes) == (shader.stage == ShaderRecompiler::ShaderStage::Fragment), "a stage's push constants lie outside its push constant slot"); + } try { std::vector stages(shaders.size()); std::vector specializations; specializations.reserve(shaders.size()); + 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; stages[i].module = _modules[i]; stages[i].pName = "main"; + if (shaders[i].stage == ShaderRecompiler::ShaderStage::Mesh && context.meshWave32 && shader.hostSubgroupSize == 32u) stages[i].pNext = &meshSubgroup; 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 auto setLayout = resources.Layout(); - const VkPushConstantRange push{pushStages, 0, PipelinePushConstantBytes}; + const std::array setLayouts{resources.main, resources.fragment}; + const VkPushConstantRange push{pushStages, 0, PipelinePushSlotBytes}; + const std::array slots{{{pushStagesOf(context), 0, PipelinePushSlotBytes}, {VK_SHADER_STAGE_FRAGMENT_BIT, PipelinePushSlotBytes, PipelinePushSlotBytes}}}; VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; - layoutInfo.setLayoutCount = 1; - layoutInfo.pSetLayouts = &setLayout; - layoutInfo.pushConstantRangeCount = pushStages != 0 ? 1 : 0; - layoutInfo.pPushConstantRanges = pushStages != 0 ? &push : nullptr; + layoutInfo.flags = resources.split ? VK_PIPELINE_LAYOUT_CREATE_INDEPENDENT_SETS_BIT_EXT : 0; + layoutInfo.setLayoutCount = resources.split ? 2u : 1u; + layoutInfo.pSetLayouts = setLayouts.data(); + layoutInfo.pushConstantRangeCount = dynamicRendering ? static_cast(slots.size()) : pushStages != 0 ? 1 : 0; + layoutInfo.pPushConstantRanges = dynamicRendering ? slots.data() : pushStages != 0 ? &push : nullptr; Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &layout), "vkCreatePipelineLayout graphics"); std::vector colors; std::vector references(state.blends.size(), VkAttachmentReference{VK_ATTACHMENT_UNUSED, attachmentLayout}); for (std::uint32_t index = 0; index < state.colors.size(); ++index) { VkAttachmentDescription color{}; color.format = state.colors[index].format; - color.samples = VK_SAMPLE_COUNT_1_BIT; + color.samples = state.mergedCoverage ? 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; @@ -170,8 +412,8 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput const VkAttachmentReference depthReference{static_cast(colors.size()), VK_IMAGE_LAYOUT_GENERAL}; if (state.depth) { VkAttachmentDescription depth{}; - depth.format = state.depth->format; - depth.samples = VK_SAMPLE_COUNT_1_BIT; + depth.format = HostDepthFormat(context, state.depth->format); + depth.samples = static_cast(state.samples); depth.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; depth.storeOp = VK_ATTACHMENT_STORE_OP_STORE; depth.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD; @@ -181,19 +423,39 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput colors.push_back(depth); subpass.pDepthStencilAttachment = &depthReference; } + std::vector renderingColors(state.blends.size(), VK_FORMAT_UNDEFINED); + for (const auto& color : state.colors) renderingColors.at(color.exportIndex) = color.format; + 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.mergedCoverage) rendering.pNext = &attachmentSamples; + if (state.depth) { + rendering.depthAttachmentFormat = HostDepthFormat(context, state.depth->format); + if (HasStencil(state.depth->format)) rendering.stencilAttachmentFormat = rendering.depthAttachmentFormat; + } + if (dynamicRendering) { + depthFormat = state.depth ? HostDepthFormat(context, state.depth->format) : VK_FORMAT_UNDEFINED; + colorSlots.assign(state.blends.size(), ~0u); + for (std::uint32_t index = 0; index < state.colors.size(); ++index) colorSlots.at(state.colors[index].exportIndex) = index; + this->attachmentLayout = attachmentLayout; + } VkRenderPassCreateInfo passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; passInfo.attachmentCount = static_cast(colors.size()); passInfo.pAttachments = colors.empty() ? nullptr : colors.data(); passInfo.subpassCount = 1; passInfo.pSubpasses = &subpass; - Check(context.Function("vkCreateRenderPass")(context.device, &passInfo, nullptr, &renderPass), "vkCreateRenderPass"); + if (!dynamicRendering) Check(context.Function("vkCreateRenderPass")(context.device, &passInfo, nullptr, &renderPass), "vkCreateRenderPass"); VkPipelineVertexInputStateCreateInfo input{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO}; input.vertexBindingDescriptionCount = static_cast(vertexInput.bindings.size()); input.pVertexBindingDescriptions = vertexInput.bindings.data(); 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 @@ -204,24 +466,49 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput if (state.negativeOneToOne) viewports.pNext = &depthClip; viewports.viewportCount = 1; viewports.scissorCount = 1; + 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()); VkPipelineDynamicStateCreateInfo dynamic{VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO}; 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; raster.depthClampEnable = state.depthClamp && context.depthClamp ? VK_TRUE : VK_FALSE; - raster.cullMode = state.cullMode; + 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; raster.frontFace = state.frontFace; raster.depthBiasEnable = depthBias; raster.lineWidth = 1; VkPipelineRasterizationConservativeStateCreateInfoEXT conservative{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_CONSERVATIVE_STATE_CREATE_INFO_EXT}; conservative.conservativeRasterizationMode = state.conservativeRasterization; if (state.conservativeRasterization != VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT) raster.pNext = &conservative; + VkPipelineRasterizationProvokingVertexStateCreateInfoEXT provoking{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_PROVOKING_VERTEX_STATE_CREATE_INFO_EXT}; + provoking.provokingVertexMode = state.provokingVertexMode; + if (context.provokingVertexLast) { + provoking.pNext = raster.pNext; + raster.pNext = &provoking; + } VkPipelineMultisampleStateCreateInfo samples{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; - samples.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + 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; + 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 = VK_COVERAGE_REDUCTION_MODE_MERGE_NV; + if (state.mergedCoverage) { + 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; @@ -230,17 +517,39 @@ 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}; - blend.attachmentCount = static_cast(state.blends.size()); - blend.pAttachments = state.blends.empty() ? nullptr : state.blends.data(); + const bool alternateAlpha = SplitsAlternateAlpha(state); + std::vector firstChannels(state.blends.begin(), state.blends.end()); + std::vector secondChannels(state.blends.begin(), state.blends.end()); + if (alternateAlpha) { + const auto keepsStencil = [](const VkStencilOpState& face) { return face.writeMask == 0 || (face.failOp == VK_STENCIL_OP_KEEP && face.passOp == VK_STENCIL_OP_KEEP && face.depthFailOp == VK_STENCIL_OP_KEEP); }; + Require(!(state.depth && (state.depthWrite || (state.stencilTest && (!keepsStencil(state.stencilFront) || !keepsStencil(state.stencilBack))))) && !state.depthBiasPerFace, "blending into an 8_8 color target with the alternate component order and a separate alpha blend from a draw that writes depth or stencil is not implemented"); + Require(!ShadersWriteMemory(shaders), "blending into an 8_8 color target with the alternate component order and a separate alpha blend from a draw whose shaders write memory is not implemented"); + for (std::size_t index = 0; index < secondChannels.size(); ++index) { + const bool split = index < state.colors.size() && state.colors[index].componentMapping == 0xecu && state.blends[index].blendEnable; + if (!split) { + secondChannels[index].colorWriteMask = 0; + continue; + } + firstChannels[index].colorWriteMask &= ~VK_COLOR_COMPONENT_G_BIT; + secondChannels[index].colorWriteMask &= VK_COLOR_COMPONENT_G_BIT; + secondChannels[index].srcColorBlendFactor = state.blends[index].srcAlphaBlendFactor; + secondChannels[index].dstColorBlendFactor = state.blends[index].dstAlphaBlendFactor; + secondChannels[index].colorBlendOp = state.blends[index].alphaBlendOp; + } + } + blend.attachmentCount = static_cast(firstChannels.size()); + blend.pAttachments = firstChannels.empty() ? nullptr : firstChannels.data(); std::copy(state.blendConstants.begin(), state.blendConstants.end(), blend.blendConstants); VkGraphicsPipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO}; pipelineInfo.flags = context.pipelineExecutableInfo ? VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR : 0; 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; @@ -253,9 +562,39 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput pipelineInfo.pDynamicState = &dynamic; pipelineInfo.layout = layout; pipelineInfo.renderPass = renderPass; + if (dynamicRendering) pipelineInfo.pNext = &rendering; timing.Mark("modules_and_state"); - Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines"); + if (prebuilding && !libraries) { + for (const auto module : _modules) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); + _modules.clear(); + return; + } + if (prebuilding) { + static_cast(LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, true, _modules)); + for (const auto module : _modules) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); + _modules.clear(); + prebuilt = true; + return; + } + if (libraries) pipeline = LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, false, _modules); + else { + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines"); + } + if (alternateAlpha) { + blend.pAttachments = secondChannels.data(); + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &secondChannel), "vkCreateGraphicsPipelines"); + } + if (state.depthBiasPerFace) { + raster.cullMode = VK_CULL_MODE_FRONT_BIT; + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &backFaces), "vkCreateGraphicsPipelines"); + } + for (const auto module : _modules) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); + _modules.clear(); timing.Mark("create"); + PerformanceCounters::CountPipeline(); LogPipelineStatistics_nid_no_patch(context, pipeline); } catch (...) { release(); @@ -263,28 +602,48 @@ 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; } + } scope; + const Pipeline pipeline(context, state, vertexInput, resources, shaders); + return prebuilt; +} + Pipeline::~Pipeline() { release(); } void Pipeline::release() noexcept { framebuffers.clear(); + if (optimized != nullptr) optimized->Release(); + optimized.reset(); if (pipeline) context.Function("vkDestroyPipeline")(context.device, pipeline, nullptr); + if (backFaces) context.Function("vkDestroyPipeline")(context.device, backFaces, nullptr); + if (secondChannel) context.Function("vkDestroyPipeline")(context.device, secondChannel, nullptr); if (renderPass) context.Function("vkDestroyRenderPass")(context.device, renderPass, nullptr); if (layout) context.Function("vkDestroyPipelineLayout")(context.device, layout, nullptr); for (auto module : _modules) { if (module) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); } pipeline = VK_NULL_HANDLE; + backFaces = VK_NULL_HANDLE; + secondChannel = VK_NULL_HANDLE; renderPass = VK_NULL_HANDLE; layout = VK_NULL_HANDLE; _modules.clear(); } void Pipeline::Abandon() noexcept { + if (optimized != nullptr) optimized->released.store(true); + optimized.reset(); for (auto& entry : framebuffers) entry.framebuffer->Abandon(); framebuffers.clear(); pipeline = VK_NULL_HANDLE; + backFaces = VK_NULL_HANDLE; + secondChannel = VK_NULL_HANDLE; renderPass = VK_NULL_HANDLE; layout = VK_NULL_HANDLE; _modules.clear(); @@ -298,27 +657,34 @@ VkPipelineLayout Pipeline::Layout() const { return layout; } -std::shared_ptr Pipeline::AcquireFramebuffer(std::span targets, std::span> owners, VkExtent2D extent) { +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"); - const bool resident = std::all_of(owners.begin(), owners.end(), [](const auto& owner) { return owner != nullptr; }); + bool resident = true; + 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) { - return entry.framebuffer.use_count() == 1 && std::any_of(entry.owners.begin(), entry.owners.end(), [](const auto& owner) { return owner.expired(); }); + 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 || !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; - for (std::size_t i = 0; i < owners.size() && same; ++i) same = it->owners[i].lock().get() == owners[i].get(); + for (std::size_t i = 0; i < owners.size() && same; ++i) same = it->owned[i] == (owners[i] != nullptr) && it->owners[i].lock().get() == owners[i].get(); if (!same) continue; std::rotate(it, std::next(it), framebuffers.end()); return framebuffers.back().framebuffer; } } - auto framebuffer = std::make_shared(context, renderPass, targets, extent); + auto framebuffer = std::make_shared(context, renderPass, targets, extent, layers); if (!resident) return framebuffer; // Beyond the bound the least recently used unreferenced entry goes. constexpr std::size_t bound = 8; @@ -330,33 +696,123 @@ std::shared_ptr Pipeline::AcquireFramebuffer(std::span colors(colorSlots.size(), VkRenderingAttachmentInfoKHR{VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO_KHR}); + for (std::size_t slot = 0; slot < colorSlots.size(); ++slot) { + if (colorSlots[slot] == ~0u) continue; + colors[slot].imageView = views[colorSlots[slot]]; + colors[slot].imageLayout = attachmentLayout; + colors[slot].loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + colors[slot].storeOp = VK_ATTACHMENT_STORE_OP_STORE; + } + VkRenderingAttachmentInfoKHR depth{VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO_KHR}; + if (depthFormat != VK_FORMAT_UNDEFINED) { + depth.imageView = views.back(); + depth.imageLayout = VK_IMAGE_LAYOUT_GENERAL; + depth.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + depth.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + } + VkRenderingInfoKHR rendering{VK_STRUCTURE_TYPE_RENDERING_INFO_KHR}; + rendering.renderArea = {{0, 0}, extent}; + rendering.layerCount = state.renderLayers; + rendering.colorAttachmentCount = static_cast(colors.size()); + rendering.pColorAttachments = colors.empty() ? nullptr : colors.data(); + if (depthFormat != VK_FORMAT_UNDEFINED) rendering.pDepthAttachment = &depth; + if (depthFormat != VK_FORMAT_UNDEFINED && HasStencil(depthFormat)) rendering.pStencilAttachment = &depth; + context.Resolved(&DeviceFunctions::cmdBeginRendering, "vkCmdBeginRenderingKHR")(commands, &rendering); + Continue(commands, state); + return; + } VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; begin.renderPass = renderPass; begin.framebuffer = framebuffer.Handle(); begin.renderArea = {{0, 0}, extent}; + const auto locations = SampleLocations(state); + 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}; + sampleLocations.attachmentInitialSampleLocationsCount = state.depth ? 1 : 0; + sampleLocations.pAttachmentInitialSampleLocations = state.depth ? &initial : nullptr; + sampleLocations.postSubpassSampleLocationsCount = 1; + sampleLocations.pPostSubpassSampleLocations = &post; + if (sampleLocationsEnabled) begin.pNext = &sampleLocations; context.Resolved(&DeviceFunctions::cmdBeginRenderPass, "vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); Continue(commands, state); } void Pipeline::Continue(VkCommandBuffer commands, const State& state) const { - context.Resolved(&DeviceFunctions::cmdBindPipeline, "vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + 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 (depthBias) context.Resolved(&DeviceFunctions::cmdSetDepthBias, "vkCmdSetDepthBias")(commands, state.depthBiasConstant, state.depthBiasClamp, state.depthBiasSlope); + if (state.stencilReplayReference) context.Resolved(&DeviceFunctions::cmdSetStencilReference, "vkCmdSetStencilReference")(commands, VK_STENCIL_FACE_FRONT_AND_BACK, *state.stencilReplayReference); + if (libraries) { + setLibraryState(commands, state); + return; + } + if (depthBias) context.Resolved(&DeviceFunctions::cmdSetDepthBias, "vkCmdSetDepthBias")(commands, HostDepthBiasConstant(context, state.depth->format, state.depthBiasConstant), state.depthBiasClamp, state.depthBiasSlope); if (!depthBounds) return; context.Resolved(&DeviceFunctions::cmdSetDepthBounds, "vkCmdSetDepthBounds")(commands, state.minDepthBounds, state.maxDepthBounds); } +void Pipeline::setLibraryState(VkCommandBuffer commands, const State& state) const { + const bool depth = state.depth.has_value(); + context.Resolved(&DeviceFunctions::cmdSetCullMode, "vkCmdSetCullModeEXT")(commands, state.cullMode); + context.Resolved(&DeviceFunctions::cmdSetFrontFace, "vkCmdSetFrontFaceEXT")(commands, state.frontFace); + context.Resolved(&DeviceFunctions::cmdSetDepthBias, "vkCmdSetDepthBias")(commands, depthBias ? HostDepthBiasConstant(context, state.depth->format, state.depthBiasConstant) : 0.0f, depthBias ? state.depthBiasClamp : 0.0f, depthBias ? state.depthBiasSlope : 0.0f); + context.Resolved(&DeviceFunctions::cmdSetDepthBounds, "vkCmdSetDepthBounds")(commands, depthBounds ? state.minDepthBounds : 0.0f, depthBounds ? state.maxDepthBounds : 1.0f); + context.Resolved(&DeviceFunctions::cmdSetDepthTestEnable, "vkCmdSetDepthTestEnableEXT")(commands, depth && state.depthTest); + context.Resolved(&DeviceFunctions::cmdSetDepthWriteEnable, "vkCmdSetDepthWriteEnableEXT")(commands, depth && state.depthWrite); + context.Resolved(&DeviceFunctions::cmdSetDepthCompareOp, "vkCmdSetDepthCompareOpEXT")(commands, depth ? state.depthCompare : VK_COMPARE_OP_ALWAYS); + context.Resolved(&DeviceFunctions::cmdSetDepthBoundsTestEnable, "vkCmdSetDepthBoundsTestEnableEXT")(commands, depthBounds); + context.Resolved(&DeviceFunctions::cmdSetStencilTestEnable, "vkCmdSetStencilTestEnableEXT")(commands, depth && state.stencilTest); + const auto stencil = [&](VkStencilFaceFlags face, const VkStencilOpState& op) { + context.Resolved(&DeviceFunctions::cmdSetStencilOp, "vkCmdSetStencilOpEXT")(commands, face, op.failOp, op.passOp, op.depthFailOp, op.compareOp); + context.Resolved(&DeviceFunctions::cmdSetStencilCompareMask, "vkCmdSetStencilCompareMask")(commands, face, op.compareMask); + context.Resolved(&DeviceFunctions::cmdSetStencilWriteMask, "vkCmdSetStencilWriteMask")(commands, face, op.writeMask); + context.Resolved(&DeviceFunctions::cmdSetStencilReference, "vkCmdSetStencilReference")(commands, face, op.reference); + }; + stencil(VK_STENCIL_FACE_FRONT_BIT, depth ? state.stencilFront : VkStencilOpState{}); + stencil(VK_STENCIL_FACE_BACK_BIT, depth ? state.stencilBack : VkStencilOpState{}); +} + +void Pipeline::ContinueSecondChannel(VkCommandBuffer commands) const { + Require(secondChannel != VK_NULL_HANDLE, "the pipeline blends every channel in one pass"); + context.Resolved(&DeviceFunctions::cmdBindPipeline, "vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, secondChannel); +} + +void Pipeline::ContinueBackFaces(VkCommandBuffer commands, const State& state) const { + Require(backFaces != VK_NULL_HANDLE, "the pipeline draws both faces in one pass"); + context.Resolved(&DeviceFunctions::cmdBindPipeline, "vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, backFaces); + context.Resolved(&DeviceFunctions::cmdSetViewport, "vkCmdSetViewport")(commands, 0, 1, &state.viewport); + context.Resolved(&DeviceFunctions::cmdSetScissor, "vkCmdSetScissor")(commands, 0, 1, &state.scissor); + context.Resolved(&DeviceFunctions::cmdSetDepthBias, "vkCmdSetDepthBias")(commands, HostDepthBiasConstant(context, state.depth->format, state.backDepthBiasConstant), state.depthBiasClamp, state.backDepthBiasSlope); + if (depthBounds) context.Resolved(&DeviceFunctions::cmdSetDepthBounds, "vkCmdSetDepthBounds")(commands, state.minDepthBounds, state.maxDepthBounds); +} + void Pipeline::PushConstants(VkCommandBuffer commands, VkShaderStageFlags stages, std::span bytes) const { if (stages == 0) return; - context.Resolved(&DeviceFunctions::cmdPushConstants, "vkCmdPushConstants")(commands, layout, stages, 0, PipelinePushConstantBytes, bytes.data()); + if (dynamicRendering) { + context.Resolved(&DeviceFunctions::cmdPushConstants, "vkCmdPushConstants")(commands, layout, pushStagesOf(context), 0, PipelinePushSlotBytes, bytes.data()); + context.Resolved(&DeviceFunctions::cmdPushConstants, "vkCmdPushConstants")(commands, layout, VK_SHADER_STAGE_FRAGMENT_BIT, PipelinePushSlotBytes, PipelinePushSlotBytes, bytes.data() + PipelinePushSlotBytes); + return; + } + context.Resolved(&DeviceFunctions::cmdPushConstants, "vkCmdPushConstants")(commands, layout, stages, 0, PipelinePushSlotBytes, bytes.data()); } namespace { @@ -368,25 +824,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. -void pipelineKey(std::vector& key, const Context& context, const State& state, const VertexInputLayout& input, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout) { - using Stage = ShaderRecompiler::ShaderStage; +// 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) { 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); - 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); } @@ -404,36 +862,57 @@ void pipelineKey(std::vector& key, const Context& context, const Stat append(key, attribute.format); append(key, attribute.offset); } - append(key, resources.LayoutKey().size()); - for (const auto word : resources.LayoutKey()) append(key, word); + append(key, resources.mainKey.size()); + for (const auto word : resources.mainKey) append(key, word); + append(key, resources.fragmentKey.size()); + for (const auto word : resources.fragmentKey) append(key, word); append(key, state.hasColorTarget); append(key, state.rectList); append(key, state.topology); append(key, state.primitiveRestart); append(key, state.cullMode); append(key, state.frontFace); + append(key, state.provokingVertexMode); append(key, state.negativeOneToOne); append(key, state.depthClamp && context.depthClamp); append(key, state.conservativeRasterization); append(key, state.blends.size()); 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.customSampleLocations); + 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) { @@ -446,6 +925,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) { @@ -455,7 +935,15 @@ void pipelineKey(std::vector& key, const Context& context, const Stat append(key, tessellation.domain); append(key, tessellation.partitioning); append(key, tessellation.outputTopology); + append(key, tessellation.patchesPerGroup); + 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) { @@ -468,12 +956,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; @@ -481,6 +969,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(); }; @@ -493,8 +982,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; } @@ -506,6 +999,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; @@ -520,56 +1018,120 @@ void reportPipelines(PipelineStore& store) { } std::shared_ptr CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout) { + 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, 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); @@ -581,6 +1143,7 @@ void ClearCachedPipelines(VkDevice device) { store.index.erase(it->hash); it = store.entries.erase(it); } + ClearPipelineLibraries(device); } } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp index b3f1de78cb..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,38 +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; - if (getData(context.device, cache, &size, nullptr) != VK_SUCCESS || size == 0 || size == savedBytes) return; - std::vector file(sizeof(FileHeader) + size); - const auto result = getData(context.device, cache, &size, file.data() + sizeof(FileHeader)); - if (result != VK_SUCCESS && result != VK_INCOMPLETE) return; - file.resize(sizeof(FileHeader) + size); + std::vector file; + { + auto synchronizationLock = 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, 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..d97bd373b3 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -0,0 +1,1154 @@ +#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) == 784, "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 = 5u; +constexpr std::uint32_t ModuleRecord = 1u; +constexpr std::uint32_t GraphicsRecord = 2u; +constexpr std::uint32_t ComputeRecord = 3u; +constexpr std::uint64_t RecordAlignment = 8u; + +struct FileHeader { + std::uint32_t magic; + std::uint32_t format; + std::uint32_t runtimeAbi; + std::uint32_t reserved; + std::uint64_t sourceVersion; + std::uint64_t device; +}; +static_assert(sizeof(FileHeader) == 32 && std::is_trivially_copyable_v); + +struct RecordHeader { + std::uint32_t kind; + std::uint32_t reserved; + std::uint64_t payloadBytes; + std::uint64_t payloadHash; +}; +static_assert(sizeof(RecordHeader) == 24 && std::is_trivially_copyable_v); + +class Writer { +public: + explicit Writer(std::vector& out) : out(out) {} + + template + void Raw(const TValue& value) requires std::is_trivially_copyable_v { + const auto offset = out.size(); + out.resize(offset + sizeof(value)); + std::memcpy(out.data() + offset, &value, sizeof(value)); + } + + void Bool(bool value) { Raw(value ? 1u : 0u); } + void U32(std::uint32_t value) { Raw(value); } + void U64(std::uint64_t value) { Raw(value); } + + template + void Raws(std::span values) requires std::is_trivially_copyable_v { + U64(values.size()); + for (const auto& value : values) Raw(value); + } + +private: + std::vector& out; +}; + +class Reader { +public: + explicit Reader(std::span bytes) : bytes(bytes) {} + + [[nodiscard]] bool Ok() const { return ok; } + [[nodiscard]] bool Done() const { return ok && position == bytes.size(); } + void Fail() { ok = false; } + + template + [[nodiscard]] TValue Raw() requires std::is_trivially_copyable_v { + TValue value{}; + if (!ok || bytes.size() - position < sizeof(value)) { + ok = false; + return value; + } + std::memcpy(&value, bytes.data() + position, sizeof(value)); + position += sizeof(value); + return value; + } + + [[nodiscard]] bool Bool() { + const auto value = Raw(); + if (value > 1u) ok = false; + return ok && value == 1u; + } + + [[nodiscard]] std::uint32_t U32() { return Raw(); } + [[nodiscard]] std::uint64_t U64() { return Raw(); } + + template + void Raws(std::vector& values, std::size_t limit) requires std::is_trivially_copyable_v { + const auto count = U64(); + if (!ok || count > limit || count > (bytes.size() - position) / sizeof(TValue)) { + ok = false; + values.clear(); + return; + } + values.resize(static_cast(count)); + for (auto& value : values) value = Raw(); + } + +private: + std::span bytes; + std::size_t position = 0; + bool ok = true; +}; + +template +void encodeOptional(Writer& writer, const std::optional& value, void (*encode)(Writer&, const TValue&)) { + writer.Bool(value.has_value()); + if (value) encode(writer, *value); +} + +void encodeMesh(Writer& writer, const ShaderRecompiler::MeshConfiguration& mesh) { + for (const auto value : {mesh.inputPrimitive, mesh.primitivesPerGroup, mesh.verticesPerGroup, mesh.maxVertices, mesh.maxPrimitives, mesh.threadsPerGroup, mesh.ldsSizeDwords, mesh.provokingVertex, mesh.esgsItemSize, mesh.outputPrimitiveSize}) writer.U32(value); +} + +void decodeMesh(Reader& reader, ShaderRecompiler::MeshConfiguration& mesh) { + for (auto* value : {&mesh.inputPrimitive, &mesh.primitivesPerGroup, &mesh.verticesPerGroup, &mesh.maxVertices, &mesh.maxPrimitives, &mesh.threadsPerGroup, &mesh.ldsSizeDwords, &mesh.provokingVertex, &mesh.esgsItemSize, &mesh.outputPrimitiveSize}) *value = reader.U32(); +} + +void encodeTessellation(Writer& writer, const ShaderRecompiler::TessellationConfiguration& tessellation) { + for (const auto value : {tessellation.inputControlPoints, tessellation.outputControlPoints, tessellation.domain, tessellation.partitioning, tessellation.outputTopology, tessellation.patchesPerGroup, tessellation.primitiveGroupSize}) writer.U32(value); + writer.Bool(tessellation.breakAtInstance); + writer.U32(tessellation.instancePatches); + writer.Bool(tessellation.drawParameters); +} + +void decodeTessellation(Reader& reader, ShaderRecompiler::TessellationConfiguration& tessellation) { + for (auto* value : {&tessellation.inputControlPoints, &tessellation.outputControlPoints, &tessellation.domain, &tessellation.partitioning, &tessellation.outputTopology, &tessellation.patchesPerGroup, &tessellation.primitiveGroupSize}) *value = reader.U32(); + tessellation.breakAtInstance = reader.Bool(); + tessellation.instancePatches = reader.U32(); + tessellation.drawParameters = reader.Bool(); +} + +void encodeState(Writer& writer, const State& state) { + writer.U32(static_cast(state.stages.path)); + writer.U32(state.stages.vertexWaveSize); + writer.U32(state.stages.fragmentWaveSize); + encodeOptional(writer, state.stages.mesh, encodeMesh); + encodeOptional(writer, state.stages.tessellation, encodeTessellation); + writer.Bool(state.depth.has_value()); + if (state.depth) { + writer.U32(static_cast(state.depth->format)); + writer.U32(state.depth->samples); + } + writer.Bool(state.depthTest); + writer.Bool(state.depthWrite); + writer.U32(static_cast(state.depthCompare)); + writer.Bool(state.depthBoundsTest); + writer.Bool(state.depthBias); + writer.Raw(state.depthBiasClamp); + writer.Bool(state.depthBiasPerFace); + writer.Bool(state.stencilTest); + writer.Raw(state.stencilFront); + writer.Raw(state.stencilBack); + writer.Bool(state.stencilReplayReference.has_value()); + writer.U64(state.colors.size()); + for (const auto& color : state.colors) { + writer.U32(static_cast(color.format)); + writer.U32(color.exportIndex); + writer.Raw(color.componentMapping); + } + writer.Raws(std::span(state.blends)); + for (const auto value : state.blendConstants) writer.Raw(value); + writer.Bool(state.hasColorTarget); + writer.Bool(state.rectList); + writer.Bool(state.dualSourceBlend); + writer.U32(state.reversedBlend); + writer.U32(static_cast(state.topology)); + writer.Bool(state.primitiveRestart); + writer.Bool(state.negativeOneToOne); + writer.Bool(state.depthClamp); + writer.U32(static_cast(state.conservativeRasterization)); + writer.U32(state.cullMode); + writer.U32(static_cast(state.frontFace)); + writer.U32(static_cast(state.provokingVertexMode)); + writer.U32(state.samples); + for (const auto value : state.sampleLocations) writer.Raw(value); + writer.Bool(state.customSampleLocations); + writer.Bool(state.sampleLocationGrid); + writer.Bool(state.perSampleShading); + writer.Bool(state.mergedCoverage); + for (const auto mask : state.quadSampleMask) writer.Raw(mask); + writer.U32(state.depthRemap.mode); +} + +void decodeState(Reader& reader, State& state) { + state = State{}; + state.stages.path = static_cast(reader.U32()); + state.stages.vertexWaveSize = reader.U32(); + state.stages.fragmentWaveSize = reader.U32(); + if (reader.Bool()) decodeMesh(reader, state.stages.mesh.emplace()); + if (reader.Bool()) decodeTessellation(reader, state.stages.tessellation.emplace()); + if (reader.Bool()) { + auto& depth = state.depth.emplace(); + depth.format = static_cast(reader.U32()); + depth.samples = reader.U32(); + } + state.depthTest = reader.Bool(); + state.depthWrite = reader.Bool(); + state.depthCompare = static_cast(reader.U32()); + state.depthBoundsTest = reader.Bool(); + state.depthBias = reader.Bool(); + state.depthBiasClamp = reader.Raw(); + state.depthBiasPerFace = reader.Bool(); + state.stencilTest = reader.Bool(); + state.stencilFront = reader.Raw(); + state.stencilBack = reader.Raw(); + if (reader.Bool()) state.stencilReplayReference = 0u; + const auto colors = reader.U64(); + if (!reader.Ok() || colors > 8u) { + reader.Fail(); + state = State{}; + return; + } + for (std::uint64_t index = 0; index < colors; ++index) { + ColorTarget color{}; + color.format = static_cast(reader.U32()); + color.exportIndex = reader.U32(); + color.componentMapping = reader.Raw(); + state.colors.push_back(color); + } + reader.Raws(state.blends, 8u); + for (auto& value : state.blendConstants) value = reader.Raw(); + state.hasColorTarget = reader.Bool(); + state.rectList = reader.Bool(); + state.dualSourceBlend = reader.Bool(); + state.reversedBlend = reader.U32(); + state.topology = static_cast(reader.U32()); + state.primitiveRestart = reader.Bool(); + state.negativeOneToOne = reader.Bool(); + state.depthClamp = reader.Bool(); + state.conservativeRasterization = static_cast(reader.U32()); + state.cullMode = reader.U32(); + state.frontFace = static_cast(reader.U32()); + state.provokingVertexMode = static_cast(reader.U32()); + state.samples = reader.U32(); + for (auto& value : state.sampleLocations) value = reader.Raw(); + state.customSampleLocations = reader.Bool(); + state.sampleLocationGrid = reader.Bool(); + state.perSampleShading = reader.Bool(); + state.mergedCoverage = reader.Bool(); + for (auto& mask : state.quadSampleMask) mask = reader.Raw(); + state.depthRemap.mode = reader.U32(); +} + +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.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.quadSampleMask = state.quadSampleMask; + canonical.depthRemap.mode = state.depthRemap.mode; + 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 new file mode 100644 index 0000000000..7670cde694 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp @@ -0,0 +1,374 @@ +#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 +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::Graphics { + +namespace { + +constexpr std::array dynamicStates{ + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR, + VK_DYNAMIC_STATE_DEPTH_BIAS, + VK_DYNAMIC_STATE_DEPTH_BOUNDS, + VK_DYNAMIC_STATE_STENCIL_COMPARE_MASK, + VK_DYNAMIC_STATE_STENCIL_WRITE_MASK, + VK_DYNAMIC_STATE_STENCIL_REFERENCE, + VK_DYNAMIC_STATE_CULL_MODE_EXT, + VK_DYNAMIC_STATE_FRONT_FACE_EXT, + VK_DYNAMIC_STATE_DEPTH_TEST_ENABLE_EXT, + VK_DYNAMIC_STATE_DEPTH_WRITE_ENABLE_EXT, + VK_DYNAMIC_STATE_DEPTH_COMPARE_OP_EXT, + VK_DYNAMIC_STATE_DEPTH_BOUNDS_TEST_ENABLE_EXT, + VK_DYNAMIC_STATE_STENCIL_TEST_ENABLE_EXT, + VK_DYNAMIC_STATE_STENCIL_OP_EXT, +}; + +using Key = std::vector; + +struct DeviceLibraries { + PFN_vkDestroyPipeline destroyPipeline = nullptr; + PFN_vkDestroyPipelineLayout destroyLayout = nullptr; + PFN_vkDestroyShaderModule destroyModule = nullptr; + std::vector modules; + std::map layouts; + std::array, 4> parts; + PipelineLibraryCounters counters; +}; + +struct LibraryStore { + std::mutex mutex; + std::map devices; + std::condition_variable idle; + std::condition_variable work; + std::deque>> jobs; + std::map pending; + std::map activeBuilds; + std::thread worker; + bool stopping = false; +}; + +std::atomic runningWorkers{0}; + +LibraryStore& Libraries() { + static auto* store = new LibraryStore(); + return *store; +} + +void runJobs(LibraryStore& store) { + std::unique_lock lock(store.mutex); + for (;;) { + 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(); + job(); + 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.stopping) return; + if (!store.worker.joinable()) { + store.worker = std::thread(runJobs, std::ref(store)); + runningWorkers.fetch_add(1, std::memory_order_acq_rel); + } + auto& pending = store.pending[device]; + store.jobs.emplace_back(device, std::move(job)); + ++pending; + store.work.notify_one(); +} + +Key Combined(const Key& part, const Key& context) { + Key key; + const auto append = [&](const Key& value) { + const auto size = value.size(); + const auto bytes = std::as_bytes(std::span(&size, 1)); + key.insert(key.end(), bytes.begin(), bytes.end()); + key.insert(key.end(), value.begin(), value.end()); + }; + append(part); + append(context); + return key; +} + +} + +std::span PipelineLibraryDynamicStates() { + return dynamicStates; +} + +VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr* optimized, bool partsOnly, std::span modules) { + Require(info.renderPass == VK_NULL_HANDLE && info.pDynamicState != nullptr, "pipeline library parts need dynamic rendering and the library dynamic states"); + const bool split = (layout.flags & VK_PIPELINE_LAYOUT_CREATE_INDEPENDENT_SETS_BIT_EXT) != 0; + Require(!split || (layout.setLayoutCount == 2u && layout.pSetLayouts != nullptr && ((keys.preRasterizationSets | keys.fragmentSets) & ~3u) == 0u), "split pipeline library layout requires two descriptor set slots and a valid mask"); + auto& store = Libraries(); + std::unique_lock lock(store.mutex); + ++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"); + device.destroyLayout = context.Function("vkDestroyPipelineLayout"); + device.destroyModule = context.Function("vkDestroyShaderModule"); + } + const auto layoutFor = [&](const Key& key, std::optional mask) { + auto found = device.layouts.find(key); + if (found != device.layouts.end()) return found->second; + auto info = layout; + std::array sets{VK_NULL_HANDLE, VK_NULL_HANDLE}; + if (mask.has_value()) { + for (std::uint32_t set = 0; set < sets.size(); ++set) { + if ((*mask & (1u << set)) != 0u) sets[set] = layout.pSetLayouts[set]; + } + info.pSetLayouts = sets.data(); + } + VkPipelineLayout handle = VK_NULL_HANDLE; + Check(context.Function("vkCreatePipelineLayout")(context.device, &info, nullptr, &handle), "vkCreatePipelineLayout library"); + return device.layouts.emplace(key, handle).first->second; + }; + const auto fullLayout = layoutFor(keys.layout, std::nullopt); + const std::array partLayouts{VK_NULL_HANDLE, split ? layoutFor(keys.preRasterizationLayout, keys.preRasterizationSets) : fullLayout, split ? layoutFor(keys.fragmentLayout, keys.fragmentSets) : fullLayout, VK_NULL_HANDLE}; + std::vector preRasterization; + const VkPipelineShaderStageCreateInfo* fragment = nullptr; + bool mesh = false; + for (std::uint32_t index = 0; index < info.stageCount; ++index) { + const auto& stage = info.pStages[index]; + if (stage.stage == VK_SHADER_STAGE_FRAGMENT_BIT) fragment = &stage; + else preRasterization.push_back(stage); + mesh |= stage.stage == VK_SHADER_STAGE_MESH_BIT_EXT; + } + Require(fragment != nullptr && !preRasterization.empty(), "a pipeline built from libraries needs a fragment and a pre-rasterization stage"); + constexpr std::array flags{ + VK_GRAPHICS_PIPELINE_LIBRARY_VERTEX_INPUT_INTERFACE_BIT_EXT, + VK_GRAPHICS_PIPELINE_LIBRARY_PRE_RASTERIZATION_SHADERS_BIT_EXT, + VK_GRAPHICS_PIPELINE_LIBRARY_FRAGMENT_SHADER_BIT_EXT, + VK_GRAPHICS_PIPELINE_LIBRARY_FRAGMENT_OUTPUT_INTERFACE_BIT_EXT, + }; + const std::array partKeys{&keys.vertexInput, &keys.preRasterization, &keys.fragmentShader, &keys.fragmentOutput}; + const Key none; + VkPipelineRenderingCreateInfoKHR shaderRendering{VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR}; + shaderRendering.viewMask = rendering.viewMask; + shaderRendering.depthAttachmentFormat = rendering.depthAttachmentFormat; + shaderRendering.stencilAttachmentFormat = rendering.stencilAttachmentFormat; + const std::array attachments{shaderRendering.viewMask, static_cast(shaderRendering.depthAttachmentFormat), static_cast(shaderRendering.stencilAttachmentFormat)}; + const auto attachmentBytes = std::as_bytes(std::span(attachments)); + const Key attachmentKey(attachmentBytes.begin(), attachmentBytes.end()); + const auto preRasterizationContext = Combined(keys.preRasterizationLayout, attachmentKey); + const auto fragmentContext = Combined(keys.fragmentLayout, attachmentKey); + const std::array contextKeys{&none, &preRasterizationContext, &fragmentContext, &keys.renderPass}; + VkPipelineDepthStencilStateCreateInfo dynamicDepth{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; + static const bool backgroundLinking = std::getenv("APS5_NO_PIPELINE_LTO") == nullptr; + const bool optimize = backgroundLinking && context.pipelineLinkTimeOptimization; + std::vector libraries; + for (std::size_t part = 0; part < flags.size(); ++part) { + if (part == 0 && mesh) continue; + const auto key = Combined(*partKeys[part], *contextKeys[part]); + auto& cache = device.parts[part]; + if (const auto found = cache.find(key); found != cache.end()) { + libraries.push_back(found->second); + continue; + } + PerformanceTimer timing("Vulkan.GraphicsPipelineLibrary"); + VkGraphicsPipelineLibraryCreateInfoEXT library{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_LIBRARY_CREATE_INFO_EXT, flags[part] == VK_GRAPHICS_PIPELINE_LIBRARY_FRAGMENT_OUTPUT_INTERFACE_BIT_EXT ? &rendering : &shaderRendering}; + library.flags = flags[part]; + VkGraphicsPipelineCreateInfo create{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, &library}; + create.flags = VK_PIPELINE_CREATE_LIBRARY_BIT_KHR | VK_PIPELINE_CREATE_RETAIN_LINK_TIME_OPTIMIZATION_INFO_BIT_EXT; + create.pDynamicState = info.pDynamicState; + create.layout = partLayouts[part]; + switch (flags[part]) { + case VK_GRAPHICS_PIPELINE_LIBRARY_VERTEX_INPUT_INTERFACE_BIT_EXT: + create.pVertexInputState = info.pVertexInputState; + create.pInputAssemblyState = info.pInputAssemblyState; + create.layout = VK_NULL_HANDLE; + break; + case VK_GRAPHICS_PIPELINE_LIBRARY_PRE_RASTERIZATION_SHADERS_BIT_EXT: + create.stageCount = static_cast(preRasterization.size()); + create.pStages = preRasterization.data(); + create.pViewportState = info.pViewportState; + create.pRasterizationState = info.pRasterizationState; + create.pTessellationState = info.pTessellationState; + break; + case VK_GRAPHICS_PIPELINE_LIBRARY_FRAGMENT_SHADER_BIT_EXT: + create.stageCount = 1; + create.pStages = fragment; + create.pMultisampleState = info.pMultisampleState; + create.pDepthStencilState = info.pDepthStencilState != nullptr ? info.pDepthStencilState : &dynamicDepth; + break; + default: + create.pMultisampleState = info.pMultisampleState; + create.pColorBlendState = info.pColorBlendState; + create.layout = VK_NULL_HANDLE; + break; + } + VkPipeline handle = VK_NULL_HANDLE; + // 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); + } + lock.lock(); + Check(created, "vkCreateGraphicsPipelines library"); + if (const auto raced = cache.find(key); raced != cache.end()) { + device.destroyPipeline(context.device, handle, nullptr); + libraries.push_back(raced->second); + continue; + } + cache.emplace(key, handle); + if (optimize) { + for (std::uint32_t stage = 0; stage < create.stageCount; ++stage) { + for (auto& module : modules) { + if (module == VK_NULL_HANDLE || module != create.pStages[stage].module) continue; + device.modules.push_back(module); + module = VK_NULL_HANDLE; + } + } + } + ++device.counters.built[part]; + libraries.push_back(handle); + } + if (partsOnly) return VK_NULL_HANDLE; + PerformanceTimer timing("Vulkan.GraphicsPipelineLink"); + 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}; + 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(); + slot->device = context.device; + slot->destroy = device.destroyPipeline; + *optimized = slot; + const auto createPipelines = context.Function("vkCreateGraphicsPipelines"); + const auto cache = context.pipelineCache; + const auto cacheSynchronization = context.pipelineCacheSynchronization; + const auto libraryLayout = fullLayout; + std::function relink = [slot, createPipelines, cache, cacheSynchronization, libraryLayout, libraries, &store, devicePointer = &device] { + if (slot->released.load()) return; + VkPipelineLibraryCreateInfoKHR linked{VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR}; + linked.libraryCount = static_cast(libraries.size()); + linked.pLibraries = libraries.data(); + VkGraphicsPipelineCreateInfo create{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, &linked}; + create.flags = VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT; + create.layout = libraryLayout; + VkPipeline result = VK_NULL_HANDLE; + const auto pipelineCreationResult = [&] { + auto pipelineCacheLock = cacheSynchronization != nullptr ? cacheSynchronization->LockForPipelineUse() : PipelineCacheSynchronization::PipelineUseLock{}; + return createPipelines(slot->device, cache, 1, &create, nullptr, &result); + }(); + if (pipelineCreationResult != VK_SUCCESS) return; + slot->handle.store(result); + if (slot->released.load()) { + if (const auto handle = slot->handle.exchange(VK_NULL_HANDLE)) slot->destroy(slot->device, handle, nullptr); + } + 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; +} + +void OptimizedPipeline::Release() noexcept { + released.store(true); + 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); + store.idle.wait(lock, [&] { return store.pending[device] == 0; }); +} + +void ClearPipelineLibraries(VkDevice device) { + auto& store = Libraries(); + std::unique_lock lock(store.mutex); + 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; + for (auto& part : libraries.parts) { + for (const auto& [key, handle] : part) libraries.destroyPipeline(device, handle, nullptr); + } + for (const auto& [key, handle] : libraries.layouts) libraries.destroyLayout(device, handle, nullptr); + for (const auto module : libraries.modules) libraries.destroyModule(device, module, nullptr); + 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); + const auto found = store.devices.find(device); + return found == store.devices.end() ? PipelineLibraryCounters{} : found->second.counters; +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp new file mode 100644 index 0000000000..68487401c7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp @@ -0,0 +1,111 @@ +#include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include +#include + +namespace AgcDriver::Graphics { +namespace { + +bool Supported(const Context& context, const PlaceholderImageShape& shape) { + VkFormatProperties properties{}; + context.formatProperties(context.physical, shape.format, &properties); + if ((properties.optimalTilingFeatures & shape.features) != shape.features) return false; + VkImageFormatProperties limits{}; + if (context.imageFormatProperties(context.physical, shape.format, shape.type, VK_IMAGE_TILING_OPTIMAL, shape.usage, 0, &limits) != VK_SUCCESS) return false; + return (limits.sampleCounts & shape.samples) != 0; +} + +} + +PlaceholderImages::PlaceholderImages(const Context& context) : context(context) { + try { + std::vector toClear; + for (std::uint32_t index = 0; index < images.size(); ++index) { + 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.extent = {1, 1, 1}; + info.mipLevels = 1; + info.arrayLayers = 1; + info.samples = shape->samples; + info.tiling = VK_IMAGE_TILING_OPTIMAL; + 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"); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, image.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, &image.memory), "vkAllocateMemory placeholder"); + 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}; + 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; + barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = image.image; + barrier.subresourceRange = viewInfo.subresourceRange; + toClear.push_back(barrier); + } + if (toClear.empty()) return; + CommandBatch batch(context); + const auto commands = batch.Handle(); + const auto pipelineBarrier = context.Function("vkCmdPipelineBarrier"); + pipelineBarrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, static_cast(toClear.size()), toClear.data()); + std::vector toGeneral; + for (const auto& barrier : toClear) { + if (barrier.subresourceRange.aspectMask == VK_IMAGE_ASPECT_DEPTH_BIT) { + const VkClearDepthStencilValue zero{0.0f, 0}; + context.Function("vkCmdClearDepthStencilImage")(commands, barrier.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &zero, 1, &barrier.subresourceRange); + } else { + const VkClearColorValue zero{}; + context.Function("vkCmdClearColorImage")(commands, barrier.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &zero, 1, &barrier.subresourceRange); + } + auto general = barrier; + general.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + general.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT; + general.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + general.newLayout = VK_IMAGE_LAYOUT_GENERAL; + toGeneral.push_back(general); + } + pipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, static_cast(toGeneral.size()), toGeneral.data()); + batch.SubmitAndWait(); + } catch (...) { + release(); + throw; + } +} + +PlaceholderImages::~PlaceholderImages() { + release(); +} + +void PlaceholderImages::release() noexcept { + for (auto& image : images) { + if (image.view) context.Function("vkDestroyImageView")(context.device, image.view, nullptr); + if (image.image) context.Function("vkDestroyImage")(context.device, image.image, nullptr); + if (image.memory) context.Function("vkFreeMemory")(context.device, image.memory, nullptr); + image = {}; + } +} + +VkImageView PlaceholderImages::View(std::uint32_t heap) const { + const auto index = heap - ShaderRecompiler::RuntimeAbi::FirstImageBinding; + Require(heap >= ShaderRecompiler::RuntimeAbi::FirstImageBinding && index < images.size() && images[index].view != VK_NULL_HANDLE, + "the device has no placeholder image for typed image heap " + std::to_string(heap)); + return images[index].view; +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index f3a40cafc9..9f03343a67 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -1,16 +1,22 @@ +#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" #include "prx/libSceAgcDriver/Graphics/shaders/MeshArguments_spv.h" #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 @@ -27,6 +33,8 @@ #include #include #include +#include +#include "prx/libc/include/general/AtomicSharedPtr.hpp" namespace AgcDriver::Graphics { @@ -174,6 +182,13 @@ std::atomic nextRecorderId{1}; std::atomic samplesPassed{0}; constexpr std::uint32_t SampleFoldCapacity = 64; constexpr std::uint32_t SampleCounterPushBytes = 24; +constexpr std::uint32_t SegmentSlots = 128; +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 = 40; +std::atomic dumpsQueued{0}, dumpsQueuedInPass{0}, dumpsAtOnce{0}, segmentsClosed{0}, dumpFolds{0}; struct SamplePoolCache { VkDevice device; @@ -494,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}; @@ -624,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. @@ -646,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, @@ -1037,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 { @@ -1062,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); @@ -1108,6 +1184,8 @@ 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; pendingWrites.store(nullptr, std::memory_order_release); @@ -1150,10 +1228,15 @@ Recorder::~Recorder() { if (sampleCounterState > 0) { samplesPassed.store(SamplesTotal(), std::memory_order_release); if (samplePools != nullptr) std::static_pointer_cast(samplePools)->close(); + if (segmentPools != nullptr) std::static_pointer_cast(segmentPools)->close(); context.Function("vkDestroyPipeline")(context.device, samplePipeline, nullptr); context.Function("vkDestroyPipelineLayout")(context.device, sampleLayout, nullptr); + context.Function("vkDestroyPipeline")(context.device, dumpPipeline, nullptr); + 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); @@ -1360,16 +1443,27 @@ void Recorder::MarkCovered(VkAccessFlags access) { if (open != nullptr) open->coveredAccess = access; } -bool Recorder::ContinuesRenderPass(std::uint64_t key) const { - return open != nullptr && open->renderPass.open && open->renderPass.continuable && open->renderPass.key == key; +Recorder::DrawPassStart Recorder::StartDrawPass(std::uint64_t key, bool forced, bool fenced, const PassAccess& access) { + ensureOpen(); + auto& pass = open->renderPass; + if (!pass.open || forced || (countingSamples && segmentMode() && !segmentSlotReady(*open))) { + const auto commands = Commands(); + open->passHazards.Clear(); + open->passHazards.BeginPass(); + return {commands, false, true}; + } + const auto hazard = open->passHazards.Check(access, pass.key == key); + if (hazard == PassHazard::None && pass.key == key) return {open->commands, true, false}; + const bool merge = MergeBarriers() && !fenced && !pass.fenced && !pass.afterPass; + const bool elide = merge && hazard == PassHazard::None; + endOpenRenderPass(!merge); + const auto commands = Commands(); + if (!elide) open->passHazards.Clear(); + open->passHazards.BeginPass(); + return {commands, false, !elide}; } -VkCommandBuffer Recorder::CommandsInRenderPass() { - Require(open != nullptr && open->renderPass.open, "no render pass is open in the recorder"); - return open->commands; -} - -void Recorder::LeaveRenderPassOpen(std::uint64_t key, std::uint32_t timing, bool continuable, std::function afterPass) { +void Recorder::LeaveRenderPassOpen(std::uint64_t key, std::uint32_t timing, bool fenced, const PassAccess& access, std::function afterPass) { Require(open != nullptr, "no batch is open for the render pass"); auto& pass = open->renderPass; if (!pass.open) { @@ -1378,19 +1472,27 @@ void Recorder::LeaveRenderPassOpen(std::uint64_t key, std::uint32_t timing, bool } pass.open = true; pass.key = key; - pass.continuable = continuable; + pass.fenced = pass.fenced || fenced; + open->passHazards.Add(access); } -void Recorder::endOpenRenderPass() { +void Recorder::SealPassWriteOnly() { + if (open != nullptr) open->passHazards.SealWriteOnly(); +} + +void Recorder::endOpenRenderPass(bool barrier) { auto& pass = open->renderPass; - context.Resolved(&DeviceFunctions::cmdEndRenderPass, "vkCmdEndRenderPass")(open->commands); + if (open->segmentActive) endSegment(*open); + EndRenderPass(context, open->commands); // The pass's attachment and shader writes are visible to everything recorded after it (the // host sees them at the batch's fence). - 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); - CountBarriers(CommandClass::Draw); + if (barrier) { + recordBarrier(open->commands, PassEndBarrier.srcStage, PassEndBarrier.dstStage, PassEndBarrier.srcAccess, PassEndBarrier.dstAccess); + CountBarriers(CommandClass::Draw); + } if (pass.afterPass) pass.afterPass(open->commands); EndGpuTiming(pass.timing); - open->coveredAccess = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT; + open->coveredAccess = barrier ? VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT : 0; open->hostReadOwed = true; pass = {}; } @@ -1488,8 +1590,15 @@ void Recorder::ensureOpen() { throw; } batch->queue = GuestMemory::GpuLockThreadTag(); - if (countingSamples) beginSamples(*batch); + if (countingSamples && !segmentMode()) beginSamples(*batch); + 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); @@ -1593,43 +1702,88 @@ bool Recorder::closeStoreRun(bool atSubmit) { if (LabelRunsPerBatch()) { auto stores = std::move(run.queued); run.queued.clear(); + open->queuedDumps = 0; if (stores.empty()) return false; // 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)); if (BarrierValidate()) { std::vector> ranges; - for (const auto& store : stores) ranges.emplace_back(store.address, store.address + store.bytes.size()); + for (const auto& store : stores) ranges.emplace_back(store.address, store.End()); NoteAccess(CommandClass::LabelRun, Access{{}, ranges, {}, VK_PIPELINE_STAGE_TRANSFER_BIT}); } // 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"); - for (const auto& store : stores) { - const VkDeviceSize end = store.offset + store.bytes.size(); - // Two transfers to the same bytes have no order of their own within the run. - if (std::any_of(run.recorded.begin(), run.recorded.end(), [&](const auto& earlier) { return std::get<0>(earlier) == store.buffer && store.offset < std::get<2>(earlier) && std::get<1>(earlier) < end; })) { - recordBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); - ++barriers; - storeWawBarriers.fetch_add(1, std::memory_order_relaxed); + 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]); } - update(commands, store.buffer, store.offset, store.bytes.size(), store.bytes.data()); - run.recorded.emplace_back(store.buffer, store.offset, end); - storedBytes += store.bytes.size(); + 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; + std::uint32_t dumpIndex = 0, consumed = 0; + for (const auto end : QueuedWriteGroups(writes)) { + if (first != 0) { + recordBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + ++barriers; + run.recorded.clear(); + } + for (std::size_t i = first; i < end; ++i) { + const auto& store = stores[i]; + if (store.dumpTarget != 0) continue; + const VkDeviceSize storeEnd = store.offset + store.bytes.size(); + if (std::any_of(run.recorded.begin(), run.recorded.end(), [&](const auto& earlier) { return std::get<0>(earlier) == store.buffer && store.offset < std::get<2>(earlier) && std::get<1>(earlier) < storeEnd; })) { + recordBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + ++barriers; + storeWawBarriers.fetch_add(1, std::memory_order_relaxed); + } + update(commands, store.buffer, store.offset, store.bytes.size(), store.bytes.data()); + run.recorded.emplace_back(store.buffer, store.offset, storeEnd); + storedBytes += store.bytes.size(); + } + const auto before = dumpIndex; + foldQueuedDumps(commands, stores, first, end, dumpIndex, consumed); + if (dumpIndex != before) barriers += 3; + first = end; + } + if (dumps) { + retireSegments(consumed); + dumpFolds.fetch_add(1, std::memory_order_relaxed); + } + const VkPipelineStageFlags written = VK_PIPELINE_STAGE_TRANSFER_BIT | (dumps ? VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT : 0u); + const VkAccessFlags writtenAccess = VK_ACCESS_TRANSFER_WRITE_BIT | (dumps ? VK_ACCESS_SHADER_WRITE_BIT : 0u); if (atSubmit) { - recordBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + recordBarrier(commands, written, VK_PIPELINE_STAGE_HOST_BIT, writtenAccess, VK_ACCESS_HOST_READ_BIT); open->coveredAccess = 0; storeRunsAtSubmit.fetch_add(1, std::memory_order_relaxed); } else { - recordBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT | VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + recordBarrier(commands, written, VK_PIPELINE_STAGE_HOST_BIT | VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, writtenAccess, VK_ACCESS_HOST_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); open->coveredAccess = VK_ACCESS_HOST_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT; storeRunsForced.fetch_add(1, std::memory_order_relaxed); } @@ -1655,12 +1809,12 @@ bool Recorder::closeStoreRun(bool atSubmit) { bool Recorder::QueuedStoreOverlaps(std::uint64_t address, std::size_t bytes) const { if (open == nullptr || bytes == 0) return false; const auto end = address + bytes; - return std::any_of(open->run.queued.begin(), open->run.queued.end(), [&](const Batch::StoreRun::Queued& store) { return address < store.address + store.bytes.size() && store.address < end; }); + return std::any_of(open->run.queued.begin(), open->run.queued.end(), [&](const Batch::StoreRun::Queued& store) { return address < store.End() && store.address < end; }); } bool Recorder::AnyQueuedStore(const std::function& overlaps) const { if (open == nullptr) return false; - return std::any_of(open->run.queued.begin(), open->run.queued.end(), [&](const Batch::StoreRun::Queued& store) { return overlaps(store.address, store.address + store.bytes.size()); }); + return std::any_of(open->run.queued.begin(), open->run.queued.end(), [&](const Batch::StoreRun::Queued& store) { return overlaps(store.address, store.End()); }); } void Recorder::FlushStores() { @@ -1744,6 +1898,7 @@ void reportBarriers() { AgcDriver::ProfilePrint_nid_no_patch("; validate: would emit %llu (%s) + %llu batch ends, would skip %llu; emitted/skipped by class:%s", static_cast(emitted), kinds.c_str() + 1, static_cast(validateBatchEnds.exchange(0, std::memory_order_relaxed)), static_cast(skipped), classes.c_str()); } AgcDriver::ProfilePrint_nid_no_patch("\n"); + AgcDriver::ProfilePrint_nid_no_patch("[samples] occlusion dumps (cumulative): %llu queued (%llu inside an open pass), %llu recorded at once; %llu segments closed; %llu folds of queued dumps\n", static_cast(dumpsQueued.load(std::memory_order_relaxed)), static_cast(dumpsQueuedInPass.load(std::memory_order_relaxed)), static_cast(dumpsAtOnce.load(std::memory_order_relaxed)), static_cast(segmentsClosed.load(std::memory_order_relaxed)), static_cast(dumpFolds.load(std::memory_order_relaxed))); } } @@ -1754,7 +1909,7 @@ bool Recorder::GpuTimingEnabled() { } bool Recorder::BatchStampsEnabled() { - return GpuTimingEnabled() || DrawProfiled(); + return GpuTimingEnabled() || DrawProfiled() || PerformanceTrace::Configured(); } void Recorder::CountBarriers(CommandClass which, std::uint32_t count) { @@ -1767,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); @@ -1846,7 +2012,7 @@ std::uint32_t Recorder::BeginGpuTiming(std::uint64_t key) { std::uint32_t Recorder::beginTiming(std::uint64_t key) { const bool full = GpuTimingEnabled(); - if (!full && !(key == BatchTimingKey && DrawProfiled())) return NoTiming; + if (!full && !(key == BatchTimingKey && BatchStampsEnabled())) return NoTiming; const auto commands = open->commands; const std::uint32_t queryCount = full ? MaxTimedRanges * 2 : 2; if (open->queries == VK_NULL_HANDLE) { @@ -1883,22 +2049,70 @@ 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(); countingSamples = true; - if (open == nullptr || open->samples != VK_NULL_HANDLE) return; + if (segmentMode() || open == nullptr || open->samples != VK_NULL_HANDLE) return; if (open->renderPass.open) endOpenRenderPass(); beginSamples(*open); } std::uint64_t Recorder::SamplesTotal() { if (sampleCounterState <= 0) return samplesPassed.load(std::memory_order_acquire); + if (segmentMode() && !Idle()) Sync(); std::uint64_t total = 0; std::memcpy(&total, sampleCounter->Bytes().data(), sizeof(total)); for (const auto& segment : pendingSamples) { std::uint64_t samples = 0; - Check(context.Function("vkGetQueryPoolResults")(context.device, segment.handle, 0, 1, sizeof(samples), &samples, sizeof(samples), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT), "vkGetQueryPoolResults occlusion"); + Check(context.Function("vkGetQueryPoolResults")(context.device, segment.handle, segment.slot, 1, sizeof(samples), &samples, sizeof(samples), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT), "vkGetQueryPoolResults occlusion"); total += samples; } pendingSamples.clear(); @@ -1906,8 +2120,13 @@ std::uint64_t Recorder::SamplesTotal() { return total; } -void Recorder::NoteSampledDraw() { - if (open != nullptr && open->sampleActive) open->samplesDrawn = true; +void Recorder::NoteSampledDraw(VkCommandBuffer commands) { + if (open == nullptr) return; + if (segmentMode()) { + if (countingSamples && commands == open->commands && !open->segmentActive) beginSegment(*open); + return; + } + if (open->sampleActive) open->samplesDrawn = true; } bool Recorder::gpuSampleCounter() { @@ -1916,7 +2135,7 @@ bool Recorder::gpuSampleCounter() { if (!context.bufferDeviceAddress) return false; VkShaderModule module = VK_NULL_HANDLE; try { - sampleCounter = std::make_unique(context, 16 + 8 * SampleFoldCapacity, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); + sampleCounter = std::make_unique(context, DumpListOffset + 16 * DumpFoldCapacity, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); std::memset(sampleCounter->Bytes().data(), 0, sampleCounter->Bytes().size()); const auto initial = samplesPassed.load(std::memory_order_acquire); std::memcpy(sampleCounter->Bytes().data(), &initial, sizeof(initial)); @@ -1925,21 +2144,38 @@ 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; pipelineInfo.stage.module = module; pipelineInfo.stage.pName = "main"; pipelineInfo.layout = sampleLayout; - Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &samplePipeline), "vkCreateComputePipelines sample counter"); + { + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &samplePipeline), "vkCreateComputePipelines sample counter"); + } context.Function("vkDestroyShaderModule")(context.device, module, nullptr); + module = VK_NULL_HANDLE; + const VkPushConstantRange dumpPush{VK_SHADER_STAGE_COMPUTE_BIT, 0, SampleDumpPushBytes}; + layoutInfo.pPushConstantRanges = &dumpPush; + Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &dumpLayout), "vkCreatePipelineLayout sample dumps"); + module = CreateShaderModule(context, SAMPLE_DUMPS_SPV, sizeof(SAMPLE_DUMPS_SPV), "vkCreateShaderModule sample dumps"); + pipelineInfo.stage.module = module; + pipelineInfo.layout = dumpLayout; + { + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &dumpPipeline), "vkCreateComputePipelines sample dumps"); + } + context.Function("vkDestroyShaderModule")(context.device, module, nullptr); + module = VK_NULL_HANDLE; } catch (const std::exception& error) { if (module != VK_NULL_HANDLE) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); + if (samplePipeline != VK_NULL_HANDLE) context.Function("vkDestroyPipeline")(context.device, samplePipeline, nullptr); + if (dumpLayout != VK_NULL_HANDLE) context.Function("vkDestroyPipelineLayout")(context.device, dumpLayout, nullptr); if (sampleLayout != VK_NULL_HANDLE) context.Function("vkDestroyPipelineLayout")(context.device, sampleLayout, nullptr); + samplePipeline = VK_NULL_HANDLE; + dumpLayout = VK_NULL_HANDLE; sampleLayout = VK_NULL_HANDLE; sampleCounter.reset(); std::fprintf(stderr, "[gpu] occlusion counter dumps drain the device: %s\n", error.what()); @@ -1949,6 +2185,229 @@ bool Recorder::gpuSampleCounter() { return true; } +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; + for (std::size_t i = 0; i < writes.size(); ++i) { + const auto& write = writes[i]; + if (!write.computed && !computed.empty()) { + ends.push_back(i); + computed.clear(); + } + if (write.computed) computed.emplace_back(write.begin, write.end); + } + if (!writes.empty()) ends.push_back(writes.size()); + return ends; +} + +bool Recorder::segmentMode() const { + return sampleCounterState > 0; +} + +bool Recorder::QueuesSampleDumps() const { + return segmentMode() && LabelRunsPerBatch(); +} + +Recorder::SampleDumpStatistics Recorder::SampleDumpCounts() { + return SampleDumpStatistics{dumpsQueued.load(std::memory_order_relaxed), dumpsQueuedInPass.load(std::memory_order_relaxed), dumpsAtOnce.load(std::memory_order_relaxed), segmentsClosed.load(std::memory_order_relaxed), dumpFolds.load(std::memory_order_relaxed)}; +} + +bool Recorder::segmentSlotReady(const Batch& batch) const { + return batch.segmentActive || (batch.segments != VK_NULL_HANDLE && batch.segmentNext < SegmentSlots); +} + +void Recorder::PrepareSampleSlot() { + GuestMemory::AssertGpuLockHeld("Recorder::PrepareSampleSlot"); + if (!countingSamples || !segmentMode()) return; + ensureOpen(); + Require(!open->renderPass.open, "a sample slot is prepared inside a render pass"); + if (!segmentSlotReady(*open)) takeSegmentPool(*open); +} + +void Recorder::EndPassSamples() { + if (open != nullptr && open->segmentActive) endSegment(*open); +} + +void Recorder::takeSegmentPool(Batch& batch) { + auto cache = std::static_pointer_cast(segmentPools); + if (cache == nullptr) { + cache = std::make_shared(); + cache->device = context.device; + cache->destroy = context.Function("vkDestroyQueryPool"); + segmentPools = cache; + } + if (batch.segmentPool != nullptr) batch.kept.push_back(std::move(batch.segmentPool)); + batch.segmentPool.reset(); + batch.segments = cache->take(); + if (batch.segments == VK_NULL_HANDLE) { + VkQueryPoolCreateInfo info{VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO}; + info.queryType = VK_QUERY_TYPE_OCCLUSION; + info.queryCount = SegmentSlots; + Check(context.Function("vkCreateQueryPool")(context.device, &info, nullptr, &batch.segments), "vkCreateQueryPool occlusion segments"); + } + try { + batch.segmentPool = std::make_shared(cache, batch.segments); + } catch (...) { + cache->put(batch.segments); + batch.segments = VK_NULL_HANDLE; + throw; + } + batch.segmentNext = 0; + function(cmdResetQueryPool, "vkCmdResetQueryPool")(batch.commands, batch.segments, 0, SegmentSlots); +} + +void Recorder::beginSegment(Batch& batch) { + Require(batch.segments != VK_NULL_HANDLE && batch.segmentNext < SegmentSlots, "an occlusion segment begins without a reset query slot"); + batch.segmentSlot = batch.segmentNext++; + function(cmdBeginQuery, "vkCmdBeginQuery")(batch.commands, batch.segments, batch.segmentSlot, context.occlusionQueryPrecise ? VK_QUERY_CONTROL_PRECISE_BIT : 0u); + batch.segmentActive = true; +} + +void Recorder::endSegment(Batch& batch) { + function(cmdEndQuery, "vkCmdEndQuery")(batch.commands, batch.segments, batch.segmentSlot); + batch.segmentActive = false; + pendingSamples.push_back({batch.segmentPool, batch.segments, batch.segmentSlot}); + segmentsClosed.fetch_add(1, std::memory_order_relaxed); +} + +void Recorder::copySegments(VkCommandBuffer commands, std::uint32_t count) { + Require(count <= pendingSamples.size() && count <= SegmentFoldCapacity, "occlusion segments exceed the fold capacity"); + const auto copy = function(cmdCopyQueryPoolResults, "vkCmdCopyQueryPoolResults"); + for (std::uint32_t i = 0; i < count;) { + const auto& first = pendingSamples[i]; + std::uint32_t run = 1; + while (i + run < count && pendingSamples[i + run].handle == first.handle && pendingSamples[i + run].slot == first.slot + run) ++run; + copy(commands, first.handle, first.slot, run, sampleCounter->Handle(), 16 + 8 * static_cast(i), 8, VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT); + i += run; + } +} + +void Recorder::retireSegments(std::uint32_t count) { + for (std::uint32_t i = 0; i < count; ++i) { + if (pendingSamples[i].pool != nullptr) open->kept.push_back(std::move(pendingSamples[i].pool)); + } + pendingSamples.erase(pendingSamples.begin(), pendingSamples.begin() + count); +} + +void Recorder::recordQueuedDumps(VkCommandBuffer commands, std::span writes) { + std::uint32_t segments = 0; + std::vector list; + for (const auto& write : writes) { + if (write.dumpTarget == 0) continue; + segments = write.dumpSegments; + list.insert(list.end(), {static_cast(write.dumpTarget), static_cast(write.dumpTarget >> 32u), write.dumpSegments, 0u}); + } + Require(list.size() <= 4u * DumpFoldCapacity, "queued occlusion dumps exceed the fold capacity"); + copySegments(commands, segments); + function(cmdUpdateBuffer, "vkCmdUpdateBuffer")(commands, sampleCounter->Handle(), DumpListOffset, list.size() * sizeof(std::uint32_t), list.data()); +} + +void Recorder::dispatchDumps(VkCommandBuffer commands, std::uint32_t firstDump, std::uint32_t dumps, std::uint32_t firstSegment, std::uint32_t lastSegment) { + recordBarrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + function(cmdBindPipeline, "vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, dumpPipeline); + struct { + VkDeviceAddress counter; + VkDeviceAddress list; + std::uint32_t firstDump; + std::uint32_t dumps; + std::uint32_t firstSegment; + std::uint32_t lastSegment; + std::uint32_t phase; + } parameters{sampleCounter->DeviceAddress(), sampleCounter->DeviceAddress() + DumpListOffset, firstDump, dumps, firstSegment, lastSegment, 0u}; + static_assert(sizeof(parameters) == SampleDumpPushBytes); + 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) { + std::uint32_t count = 0, segments = consumed; + for (std::size_t i = first; i < end; ++i) { + if (writes[i].dumpTarget == 0) continue; + ++count; + segments = writes[i].dumpSegments; + } + if (count == 0) return; + dispatchDumps(commands, dumpIndex, count, consumed, segments); + dumpIndex += count; + consumed = segments; +} + +void Recorder::foldSegments() { + const auto commands = open->commands; + const auto count = static_cast(std::min(pendingSamples.size(), SegmentFoldCapacity)); + 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_TRANSFER_WRITE_BIT); + copySegments(commands, count); + dispatchDumps(commands, 0, 0, 0, count); + 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_HOST_READ_BIT); + CountBarriers(CommandClass::Copy, 3); + retireSegments(count); +} + void Recorder::endSamples(Batch& batch) { if (!batch.sampleActive) return; function(cmdEndQuery, "vkCmdEndQuery")(batch.commands, batch.samples, 0); @@ -1969,7 +2428,7 @@ void Recorder::foldSamples(Batch& batch, VkDeviceAddress target) { if (done != 0) recordBarrier(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); for (std::uint32_t i = 0; i < count; ++i) { const auto& segment = pendingSamples[done + i]; - copy(commands, segment.handle, 0, 1, sampleCounter->Handle(), 16 + 8 * i, 8, VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT); + copy(commands, segment.handle, segment.slot, 1, sampleCounter->Handle(), 16 + 8 * i, 8, VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT); if (segment.pool != nullptr) batch.kept.push_back(segment.pool); } done += count; @@ -1985,16 +2444,46 @@ 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(); } -bool Recorder::DumpSamples(VkDeviceAddress target) { +bool Recorder::DumpSamples(VkDeviceAddress target, std::uint64_t address) { GuestMemory::AssertGpuLockHeld("Recorder::DumpSamples"); if (target == 0 || !gpuSampleCounter()) return false; countingSamples = true; + if (segmentMode()) { + ensureOpen(); + if (address != 0 && LabelRunsPerBatch() && pendingSamples.size() < SegmentFoldCapacity && open->queuedDumps < DumpFoldCapacity) { + if (open->segmentActive) endSegment(*open); + if (open->renderPass.open) dumpsQueuedInPass.fetch_add(1, std::memory_order_relaxed); + if (open->run.queued.empty()) storeRuns.fetch_add(1, std::memory_order_relaxed); + auto& queued = open->run.queued.emplace_back(); + queued.address = address; + queued.dumpTarget = target; + queued.dumpSegments = static_cast(pendingSamples.size()); + ++open->queuedDumps; + dumpsQueued.fetch_add(1, std::memory_order_relaxed); + return true; + } + FlushStores(); + Commands(); + foldSamples(*open, target); + dumpsAtOnce.fetch_add(1, std::memory_order_relaxed); + return true; + } Commands(); endSamples(*open); foldSamples(*open, target); @@ -2016,17 +2505,17 @@ 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; pipelineInfo.stage.module = module; pipelineInfo.stage.pName = "main"; pipelineInfo.layout = meshArgumentLayout; - Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &meshArgumentPipeline), "vkCreateComputePipelines mesh arguments"); + { + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &meshArgumentPipeline), "vkCreateComputePipelines mesh arguments"); + } context.Function("vkDestroyShaderModule")(context.device, module, nullptr); meshArgumentState = 1; } catch (const std::exception& error) { @@ -2091,13 +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, batch.traceId); } return; } @@ -2113,10 +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, 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, batch.traceId); auto& totals = timingByKey[key]; ++totals.count; totals.ms += ns / 1e6; @@ -2195,6 +2688,26 @@ void Recorder::Keep(std::shared_ptr object, std::size_t bytes) { open->keptBytes += bytes; } +void Recorder::KeepThroughHold(std::shared_ptr object) { + GuestMemory::AssertGpuLockHeld("Recorder::KeepThroughHold"); + if (const auto hold = GuestMemory::GpuHoldSerial(); hold != holdKeptSerial) { + holdKept.clear(); + holdKeptSerial = hold; + } + Keep(object); + holdKept.push_back(std::move(object)); +} + +void Recorder::dropStaleHoldKept() { + if (!holdKept.empty() && holdKeptSerial != GuestMemory::GpuHoldSerial() && GuestMemory::GpuMutex().HeldByThisThread()) holdKept.clear(); +} + +void Recorder::KeepBytes(const void* owner, std::size_t bytes) { + if (owner == nullptr || bytes == 0) return; + ensureOpen(); + if (open->keptOwners.insert(owner).second) open->keptBytes += bytes; +} + void Recorder::BoundKeptBytes() { if (!GuestMemory::GpuMutex().HeldByThisThread()) return; if (open != nullptr && open->keptBytes >= KeptBytesBudget) Submit(); @@ -2219,12 +2732,53 @@ void Recorder::eraseDrawSnapshot(std::map::iterat drawSnapshots.erase(entry); } -std::shared_ptr Recorder::ReusableDrawSnapshot(std::uint64_t address, std::size_t bytes, SnapshotUse use, std::uint32_t* derived) { +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 {}; - if (found->second.registryGeneration != GuestAllocations::GuestAllocationsGeneration_nid_postfix() || !GuestMemory::UnchangedSince(address, bytes, found->second.generation)) { - eraseDrawSnapshot(found); - return {}; + const bool sameRegistry = found->second.registryGeneration == GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + if (!sameRegistry || !GuestMemory::UnchangedSince(address, bytes, found->second.generation)) { + if (!sameRegistry || generation == 0 || use != SnapshotUse::Storage || !refreshDrawSnapshot(found->second, address, bytes)) { + eraseDrawSnapshot(found); + return {}; + } + found->second.generation = generation; } auto& recency = drawSnapshotPools[SnapshotPool(use)].recency; recency.splice(recency.end(), recency, found->second.recent); @@ -2232,6 +2786,53 @@ std::shared_ptr Recorder::ReusableDrawSnapshot(std::uint64_t address, st return found->second.buffer; } +bool Recorder::refreshDrawSnapshot(DrawSnapshot& entry, std::uint64_t address, std::size_t bytes) { + const auto held = entry.buffer->Bytes(); + if (held.size() != bytes || bytes == 0) return false; + constexpr std::uint64_t block = 65536; + const auto aligned = address / block * block; + const auto end = address + bytes; + const auto count = static_cast((end - 1) / block - address / block + 1); + struct Scratch { + std::vector generations; + std::vector changed; + std::vector> differing; + std::vector spans; + std::vector equal; + }; + auto& scratch = HostThreadLocal(); + auto& generations = scratch.generations; + auto& changed = scratch.changed; + auto& differing = scratch.differing; + auto& spans = scratch.spans; + auto& equal = scratch.equal; + generations.assign(count, entry.generation); + changed.assign(count, GuestMemory::BlockWritten); + differing.clear(); + spans.clear(); + const bool sole = entry.buffer.use_count() == 1; + GuestMemory::ChangedBlocks(address, bytes, generations, changed); + for (std::size_t k = 0; k < count; ++k) { + if (changed[k] == GuestMemory::BlockUnchanged) continue; + const auto from = std::max(address, aligned + k * block); + const auto to = std::min(end, aligned + (k + 1) * block); + const auto offset = static_cast(from - address); + spans.push_back({held.data() + offset, reinterpret_cast(from), static_cast(to - from)}); + } + equal.assign(spans.size(), 0); + CompareSpans(spans, equal); + for (std::size_t k = 0; k < spans.size(); ++k) { + if (equal[k] != 0) continue; + const auto offset = static_cast(static_cast(spans[k].first) - held.data()); + const auto length = spans[k].bytes; + if (!sole) return false; + if (!differing.empty() && differing.back().first + differing.back().second == offset) differing.back().second += length; + else differing.emplace_back(offset, length); + } + for (const auto& [offset, length] : differing) std::memcpy(held.data() + offset, reinterpret_cast(address + offset), length); + return true; +} + void Recorder::KeepDrawSnapshot(std::uint64_t address, std::size_t bytes, std::uint64_t generation, std::uint64_t registryGeneration, std::shared_ptr buffer, SnapshotUse use, std::uint32_t derived) { const bool storage = use == SnapshotUse::Storage; const auto budget = storage ? DrawSnapshotBudget : DrawInputBudget; @@ -2255,6 +2856,40 @@ void Recorder::KeepDrawSnapshot(std::uint64_t address, std::size_t bytes, std::u pool.bytes += bytes; } +bool Recorder::KnownHighestIndex(std::uint64_t address, std::size_t bytes, SnapshotUse use, std::optional& highest) { + const auto found = indexRanges.find({address, use, bytes}); + if (found == indexRanges.end()) return false; + if (found->second.registryGeneration != GuestAllocations::GuestAllocationsGeneration_nid_postfix() || !GuestMemory::UnchangedSince(address, bytes, found->second.generation)) { + indexRangeRecency.erase(found->second.recent); + indexRanges.erase(found); + return false; + } + indexRangeRecency.splice(indexRangeRecency.end(), indexRangeRecency, found->second.recent); + highest = found->second.highest; + return true; +} + +void Recorder::KeepHighestIndex(std::uint64_t address, std::size_t bytes, SnapshotUse use, std::uint64_t generation, std::uint64_t registryGeneration, std::optional highest) { + Require(use != SnapshotUse::Storage && use != SnapshotUse::Vertex, "a highest index belongs to an index buffer"); + if (generation == 0) return; + const DrawSnapshotKey key{address, use, bytes}; + if (const auto found = indexRanges.find(key); found != indexRanges.end()) { + indexRangeRecency.erase(found->second.recent); + indexRanges.erase(found); + } + while (indexRanges.size() >= IndexRangeEntries) { + indexRanges.erase(indexRangeRecency.front()); + indexRangeRecency.pop_front(); + } + indexRangeRecency.push_back(key); + try { + indexRanges.emplace(key, IndexRange{generation, registryGeneration, std::prev(indexRangeRecency.end()), highest}); + } catch (...) { + indexRangeRecency.pop_back(); + throw; + } +} + void Recorder::OnComplete(std::function action) { ensureOpen(); open->completions.push_back(std::move(action)); @@ -2262,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); @@ -2286,7 +2923,8 @@ void Recorder::noteWriteOn(Batch& batch, std::uint64_t address, std::size_t byte if (bytes == 0) return; if (&batch == open.get()) { if (!noteWrite(address, bytes, ownLabel)) return; - publishPendingWrites(); + const std::array ranges{std::pair{address, address + bytes}}; + publishPendingWrites(ranges); std::atomic_thread_fence(std::memory_order_seq_cst); return; } @@ -2295,10 +2933,15 @@ 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)) publishPendingWrites(); + if (!SnapshotCovers(address, address + bytes)) { + const std::array ranges{std::pair{address, address + bytes}}; + publishPendingWrites(ranges); + } std::atomic_thread_fence(std::memory_order_seq_cst); } @@ -2311,15 +2954,25 @@ void Recorder::markOverwritten(std::uint64_t address, std::uint64_t end) { void Recorder::NotePendingWrite(std::uint64_t address, std::size_t bytes) { if (!noteWrite(address, bytes)) return; - publishPendingWrites(); + const std::array ranges{std::pair{address, address + bytes}}; + publishPendingWrites(ranges); // The note precedes this thread's vkQueueSubmit and the label another queue polls for; the // fence makes that order hold without relying on x86 store ordering. std::atomic_thread_fence(std::memory_order_seq_cst); } void Recorder::NotePendingFill(std::uint64_t address, std::size_t bytes, std::uint8_t value) { - if (!noteWrite(address, bytes, false, value)) return; - publishPendingWrites(); + 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); } @@ -2329,26 +2982,56 @@ void Recorder::NotePendingWrites(std::span begin && noteWrite(begin, static_cast(end - begin))) publish = true; } if (!publish) return; - publishPendingWrites(); + publishPendingWrites(ranges); std::atomic_thread_fence(std::memory_order_seq_cst); } -void Recorder::publishPendingWrites() const { - // Only the active recorder owns the snapshot: one torn down after its successor was activated, - // or one a worker still dispatches into after device.reset(), must not publish its ranges. - if (activeRecorder != this) return; - const auto started = DrawProfiled() ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; - auto merged = std::make_shared(); - if (open != nullptr) merged->insert(merged->end(), open->writes.begin(), open->writes.end()); - for (const auto& batch : inFlight) merged->insert(merged->end(), batch->writes.begin(), batch->writes.end()); - for (const auto* batch : finishing) merged->insert(merged->end(), batch->writes.begin(), batch->writes.end()); - std::sort(merged->begin(), merged->end()); - std::size_t out = 0; - for (const auto& [begin, end] : *merged) { - if (out != 0 && begin <= (*merged)[out - 1].second) (*merged)[out - 1].second = std::max((*merged)[out - 1].second, end); - else (*merged)[out++] = {begin, end}; +void Recorder::publishPendingWrites(std::span> additions) const { + if (activeRecorder != this) { + publishedWriteRangesValid = false; + return; } - merged->resize(out); + const auto started = DrawProfiled() ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; + const auto previous = pendingWrites.load(std::memory_order_acquire); + const auto owned = publishedWriteRanges.lock(); + const bool extend = !additions.empty() && previous != nullptr && previous == owned && publishedWriteRangesValid && + std::all_of(additions.begin(), additions.end(), [](const auto& range) { return range.second > range.first; }); + publishedWriteRangesValid = false; + auto merged = std::make_shared(); + if (extend && additions.size() == 1) { + auto range = additions.front(); + const auto first = std::partition_point(previous->begin(), previous->end(), [&](const auto& entry) { return entry.second < range.first; }); + auto last = first; + while (last != previous->end() && last->first <= range.second) { + range.first = std::min(range.first, last->first); + range.second = std::max(range.second, last->second); + ++last; + } + merged->reserve(previous->size() - static_cast(last - first) + 1); + merged->insert(merged->end(), previous->begin(), first); + merged->push_back(range); + merged->insert(merged->end(), last, previous->end()); + } else { + if (extend) { + merged->reserve(previous->size() + additions.size()); + merged->insert(merged->end(), previous->begin(), previous->end()); + merged->insert(merged->end(), additions.begin(), additions.end()); + } else { + if (open != nullptr) merged->insert(merged->end(), open->writes.begin(), open->writes.end()); + for (const auto& batch : inFlight) merged->insert(merged->end(), batch->writes.begin(), batch->writes.end()); + for (const auto* batch : finishing) merged->insert(merged->end(), batch->writes.begin(), batch->writes.end()); + } + std::sort(merged->begin(), merged->end()); + std::size_t out = 0; + for (const auto& [begin, end] : *merged) { + if (out != 0 && begin <= (*merged)[out - 1].second) (*merged)[out - 1].second = std::max((*merged)[out - 1].second, end); + else (*merged)[out++] = {begin, end}; + } + merged->resize(out); + } + publishedWriteRangesValid = std::all_of(merged->begin(), merged->end(), [](const auto& range) { return range.second > range.first; }); + publishedWriteRanges = merged; + published = merged; pendingWrites.store(std::move(merged), std::memory_order_release); publishGeneration.fetch_add(1, std::memory_order_release); ++snapshotRebuilds; @@ -2364,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) { @@ -2376,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; } @@ -2416,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); @@ -2424,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(); } @@ -2446,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 { @@ -2458,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; @@ -2481,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; } @@ -2761,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 @@ -2768,13 +3524,18 @@ void Recorder::Submit() { // count behind, and the callers that act on it would otherwise take the mutex for nothing at // every packet or submission end until a real batch is next submitted. if (activeRecorder == this) workSinceSubmit.store(0, std::memory_order_relaxed); + dropStaleHoldKept(); if (open == nullptr) return; GuestMemory::AssertGpuLockHeld("Recorder::Submit"); if (!open->keyStores.empty()) recordKeyStores(false); if (open->renderPass.open) endOpenRenderPass(); - endSamples(*open); - if (pendingSamples.size() >= SampleFoldCapacity) foldSamples(*open, 0); + const bool segments = segmentMode(); + if (!segments) { + endSamples(*open); + if (pendingSamples.size() >= SampleFoldCapacity) foldSamples(*open, 0); + } const bool hostReadCovered = (open->run.open || !open->run.queued.empty()) && closeStoreRun(true); + if (segments && pendingSamples.size() >= SampleFoldCapacity) foldSegments(); if (BarrierValidate()) validateBatchEnds.fetch_add(1, std::memory_order_relaxed); if (open->hostReadOwed && !hostReadCovered) { // A fence makes nothing visible to the host: a lean draw's writes (no download barrier of @@ -2787,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; @@ -2798,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(); @@ -2822,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); @@ -2917,6 +3707,11 @@ void Recorder::Sync() { activeSyncSite = previousSite; } +void Recorder::FlushDeferredReleases() { + holdKept.clear(); + ReleaseDeferredKeeps(); +} + void Recorder::SyncThrough(std::uint64_t address, std::size_t bytes, bool waitUnlocked) { if (bytes == 0) return; const auto end = address + bytes; @@ -2931,9 +3726,6 @@ void Recorder::SyncThrough(std::uint64_t address, std::size_t bytes, bool waitUn // later batch's own ordering against this write-back is its recorder's business at record // time (FillBuffer and DispatchIndirect consult copiedWriters; a later copied writer's // write-back runs after this one in finish order; a label behind completions lands after). - // Known gap, not introduced here: a later batch writing the same range GPU-direct in place - // (a writable region bound in place, or a GPU copy-back) is not ordered against this - // write-back by the resource build; it went from deterministically stale to racy. announcedSource = 4; announcedSite = nullptr; ++holdCounters.completionSyncsSkipped; @@ -3176,6 +3968,7 @@ bool Recorder::syncThroughUnlocked(std::uint64_t address, std::uint64_t end, int bool Recorder::Reap() { PerformanceTimer frameTiming("Recorder.Reap"); + dropStaleHoldKept(); const bool profile = DrawProfiled(); const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; std::uint64_t retired = 0; @@ -3250,7 +4043,7 @@ void Recorder::finish(std::unique_ptr batch, bool wait, int source, bool AgcDriver::ProfilePrint_nid_no_patch("[recorder] completion label stores: %llu run, %llu skipped (no CPU write-back overlapped them); %llu counted pending at a write-back; %llu write-backs over a tracked label; %llu write-back completions pending\n", static_cast(completionStoresRun.load()), static_cast(completionStoresSkipped.load()), static_cast(completionLabelsCountedLate.load()), static_cast(writeBacksOverLabels.load()), static_cast(writeBackCompletions.load())); AgcDriver::ProfilePrint_nid_no_patch("[recorder] submits %llu, vkQueueSubmit mean %.1f us, max %.1f us\n", static_cast(submitCount), submitCount != 0 ? submitUs / static_cast(submitCount) : 0.0, submitMaxUs); const auto reads = Recorder::ReadCounts(); - AgcDriver::ProfilePrint_nid_no_patch("[recorder] in-place reads: %llu noted, %llu queries, hits by reader: dispatch element %llu, gpu copy %llu, address-based %llu, indirect %llu, storage upload %llu, copy source %llu; %llu hits on signaled batches ignored\n", static_cast(reads.noted), static_cast(reads.queries), static_cast(reads.hits[0]), static_cast(reads.hits[1]), static_cast(reads.hits[2]), static_cast(reads.hits[3]), static_cast(reads.hits[4]), static_cast(reads.hits[5]), static_cast(reads.staleIgnored)); + AgcDriver::ProfilePrint_nid_no_patch("[recorder] in-place reads: %llu noted, %llu queries, hits by reader: dispatch element %llu, gpu copy %llu, address-based %llu, indirect %llu, storage upload %llu, copy source %llu, draw input %llu; %llu hits on signaled batches ignored\n", static_cast(reads.noted), static_cast(reads.queries), static_cast(reads.hits[0]), static_cast(reads.hits[1]), static_cast(reads.hits[2]), static_cast(reads.hits[3]), static_cast(reads.hits[4]), static_cast(reads.hits[5]), static_cast(reads.hits[6]), static_cast(reads.staleIgnored)); } } } report{profile, source, waitStart, *this, signaledAtStart}; @@ -3283,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. @@ -3299,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 @@ -3332,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 { @@ -3361,6 +4181,8 @@ void Recorder::release(Batch& batch) noexcept { if (batch.samples != VK_NULL_HANDLE && batch.samplePool == nullptr) context.Function("vkDestroyQueryPool")(context.device, batch.samples, nullptr); batch.samples = VK_NULL_HANDLE; batch.samplePool.reset(); + batch.segments = VK_NULL_HANDLE; + batch.segmentPool.reset(); // A completed (or never submitted) batch's objects are kept for reuse: the fence is signaled or // untouched, so resetting it cannot block, and the command buffer is no longer pending. if (batch.commands != VK_NULL_HANDLE && batch.fence != VK_NULL_HANDLE && spare.size() < 64 && function(resetFences, "vkResetFences")(context.device, 1, &batch.fence) == VK_SUCCESS) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp index 336cbc301b..4f30207def 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp @@ -3,10 +3,40 @@ #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include #include namespace AgcDriver::Graphics { +namespace { + +VkDeviceSize ClassAllocationBytes(const VkMemoryRequirements& requirements, std::size_t capacity) { + const auto alignment = std::max(requirements.alignment, 1); + return std::max(requirements.size, (static_cast(capacity) + alignment - 1) / alignment * alignment); +} + +VkBuffer BindExactBuffer(const Context& context, std::size_t size, VkBufferUsageFlags usage, VkDeviceMemory memory, VkDeviceSize offset, VkDeviceSize availableBytes) { + VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; + info.size = size; + info.usage = usage; + info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + VkBuffer buffer = VK_NULL_HANDLE; + Check(context.Function("vkCreateBuffer")(context.device, &info, nullptr, &buffer), "vkCreateBuffer pooled"); + try { + VkMemoryRequirements requirements{}; + context.Function("vkGetBufferMemoryRequirements")(context.device, buffer, &requirements); + Require(requirements.size <= availableBytes, "pooled buffer memory is smaller than the requested buffer"); + Require(requirements.alignment == 0 || offset % requirements.alignment == 0, "pooled buffer offset breaks the requested buffer's alignment"); + Check(context.Function("vkBindBufferMemory")(context.device, buffer, memory, offset), "vkBindBufferMemory pooled"); + } catch (...) { + context.Function("vkDestroyBuffer")(context.device, buffer, nullptr); + throw; + } + return buffer; +} + +} + Buffer::Buffer(const Context& context, std::size_t size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties) : context(context), size(size), capacity(BufferPool::Capacity(size, properties)), usage(BufferPool::Usage(usage, properties)), properties(properties) { Require(size != 0, "zero-sized GPU buffer"); const bool addressable = (usage & VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT) != 0; @@ -19,12 +49,26 @@ Buffer::Buffer(const Context& context, std::size_t size, VkBufferUsageFlags usag deviceAddress = allocation->address; allocationBytes = allocation->allocationBytes; capacity = allocation->bytes; + offset = allocation->offset; + slab = allocation->slab; + if (allocation->bufferBytes != size) { + context.Function("vkDestroyBuffer")(context.device, buffer, nullptr); + buffer = VK_NULL_HANDLE; + deviceAddress = 0; + try { + buffer = BindExactBuffer(context, size, this->usage, memory, offset, allocationBytes); + initializeAddress(usage); + } catch (...) { + release(); + throw; + } + } ready = true; return; } try { VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; - info.size = capacity; + info.size = size; info.usage = this->usage; info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; Check(context.Function("vkCreateBuffer")(context.device, &info, nullptr, &buffer), "vkCreateBuffer"); @@ -33,21 +77,25 @@ Buffer::Buffer(const Context& context, std::size_t size, VkBufferUsageFlags usag VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; const VkMemoryAllocateFlagsInfo flags{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_FLAGS_INFO, nullptr, VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT, 0}; if (addressable) allocation.pNext = &flags; - allocation.allocationSize = requirements.size; - allocationBytes = requirements.size; + allocation.allocationSize = ClassAllocationBytes(requirements, capacity); + allocationBytes = allocation.allocationSize; // The CPU reads most of these buffers back (write-back, diffs), which is very slow from // write-combined memory, so the default host properties prefer cached host memory. - constexpr VkMemoryPropertyFlags hostDefault = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; - if (properties == hostDefault) { - try { - allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, hostDefault | VK_MEMORY_PROPERTY_HOST_CACHED_BIT); - } catch (const std::runtime_error&) { - allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, hostDefault); - } - } else { - allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, properties); + const bool useSlab = (properties & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0 && BufferPool::SlabEligible(capacity, requirements.alignment, requirements.size, context.limits.nonCoherentAtomSize); + allocation.memoryTypeIndex = context.BufferMemoryType(requirements.memoryTypeBits, properties, useSlab ? BufferPool::SlabBlockBytes(capacity) : allocation.allocationSize); + if (useSlab) { + const auto slot = cache->TakeSlot(context, allocation.memoryTypeIndex, capacity, addressable); + memory = slot.memory; + offset = slot.offset; + slab = true; + allocationBytes = capacity; + Check(context.Function("vkBindBufferMemory")(context.device, buffer, memory, offset), "vkBindBufferMemory slab"); + initializeAddress(usage); + mapping = slot.mapping; + ready = true; + return; } - Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory buffer"); + memory = cache->AllocateMemory(allocation, "vkAllocateMemory buffer"); Check(context.Function("vkBindBufferMemory")(context.device, buffer, memory, 0), "vkBindBufferMemory"); initializeAddress(usage); if ((properties & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0) Check(context.Function("vkMapMemory")(context.device, memory, 0, VK_WHOLE_SIZE, 0, &mapping), "vkMapMemory"); @@ -64,7 +112,12 @@ Buffer::~Buffer() { void Buffer::release() noexcept { if (ready && cache) { - cache->Put({buffer, memory, mapping, deviceAddress, allocationBytes, capacity, usage, properties}); + cache->Put({buffer, memory, mapping, deviceAddress, allocationBytes, capacity, usage, properties, offset, slab, size}); + return; + } + if (slab) { + if (buffer) context.Function("vkDestroyBuffer")(context.device, buffer, nullptr); + cache->PutSlot(memory, offset); return; } if (mapping) context.Function("vkUnmapMemory")(context.device, memory); @@ -85,7 +138,8 @@ void Buffer::Invalidate() { Require(mapping != nullptr, "cannot invalidate an unmapped GPU buffer"); VkMappedMemoryRange range{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE}; range.memory = memory; - range.size = VK_WHOLE_SIZE; + range.offset = offset; + range.size = slab ? static_cast(capacity) : VK_WHOLE_SIZE; Check(context.Function("vkInvalidateMappedMemoryRanges")(context.device, 1, &range), "vkInvalidateMappedMemoryRanges"); } @@ -97,22 +151,39 @@ DeviceBuffer::DeviceBuffer(const Context& context, std::size_t size, VkBufferUsa memory = allocation->memory; allocationBytes = allocation->allocationBytes; capacity = allocation->bytes; + if (allocation->bufferBytes == size) { + ready = true; + return; + } + context.Function("vkDestroyBuffer")(context.device, buffer, nullptr); + buffer = VK_NULL_HANDLE; + try { + buffer = BindExactBuffer(context, size, this->usage, memory, 0, allocationBytes); + } catch (...) { + release(); + throw; + } + ready = true; return; } try { VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; - info.size = capacity; + info.size = size; info.usage = this->usage; info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; Check(context.Function("vkCreateBuffer")(context.device, &info, nullptr, &buffer), "vkCreateBuffer device"); VkMemoryRequirements requirements{}; context.Function("vkGetBufferMemoryRequirements")(context.device, buffer, &requirements); VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; - allocation.allocationSize = requirements.size; - allocationBytes = requirements.size; - allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory device buffer"); + allocation.allocationSize = ClassAllocationBytes(requirements, capacity); + allocationBytes = allocation.allocationSize; + 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; @@ -124,8 +195,8 @@ DeviceBuffer::~DeviceBuffer() { } void DeviceBuffer::release() noexcept { - if (buffer && memory && cache) { - cache->Put({buffer, memory, nullptr, 0, allocationBytes, capacity, usage, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT}); + 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; } if (buffer) context.Function("vkDestroyBuffer")(context.device, buffer, nullptr); @@ -166,6 +237,21 @@ void FillDeviceFunctions(const Context& context, DeviceFunctions& functions) { functions.cmdSetScissor = context.Function("vkCmdSetScissor"); functions.cmdSetDepthBounds = context.Function("vkCmdSetDepthBounds"); functions.cmdSetDepthBias = context.Function("vkCmdSetDepthBias"); + if (context.graphicsPipelineLibrary) { + functions.cmdBeginRendering = context.Function("vkCmdBeginRenderingKHR"); + functions.cmdEndRendering = context.Function("vkCmdEndRenderingKHR"); + functions.cmdSetCullMode = context.Function("vkCmdSetCullModeEXT"); + functions.cmdSetFrontFace = context.Function("vkCmdSetFrontFaceEXT"); + functions.cmdSetDepthTestEnable = context.Function("vkCmdSetDepthTestEnableEXT"); + functions.cmdSetDepthWriteEnable = context.Function("vkCmdSetDepthWriteEnableEXT"); + functions.cmdSetDepthCompareOp = context.Function("vkCmdSetDepthCompareOpEXT"); + functions.cmdSetDepthBoundsTestEnable = context.Function("vkCmdSetDepthBoundsTestEnableEXT"); + functions.cmdSetStencilTestEnable = context.Function("vkCmdSetStencilTestEnableEXT"); + functions.cmdSetStencilOp = context.Function("vkCmdSetStencilOpEXT"); + functions.cmdSetStencilCompareMask = context.Function("vkCmdSetStencilCompareMask"); + functions.cmdSetStencilWriteMask = context.Function("vkCmdSetStencilWriteMask"); + functions.cmdSetStencilReference = context.Function("vkCmdSetStencilReference"); + } functions.cmdBindVertexBuffers = context.Function("vkCmdBindVertexBuffers"); functions.cmdBindIndexBuffer = context.Function("vkCmdBindIndexBuffer"); functions.cmdDraw = context.Function("vkCmdDraw"); @@ -208,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; @@ -250,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"); @@ -294,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 6bbe3bf7b8..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,7 +41,12 @@ namespace AgcDriver::Graphics { info.minLod = descriptor.minLod; info.maxLod = descriptor.maxLod; info.borderColor = descriptor.borderColor; + 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; + else requiresNonSeamlessCube = true; + } Check(context.Function("vkCreateSampler")(context.device, &info, nullptr, &sampler), "vkCreateSampler"); } @@ -55,11 +66,20 @@ namespace AgcDriver::Graphics { return requiresFilterMinmax; } + bool Sampler::ReadsOpaqueBlackBorder() const { + 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) { + std::shared_ptr SamplerCache::Get(const Context& context, std::span words, bool compareEnable, bool unnormalizedProven, bool forceDegammaPaired, bool integerBorder) { Require(words.size() == 4, "guest sampler descriptor must contain 4 dwords"); - const std::array key{words[0], words[1], words[2], words[3], (compareEnable ? 1u : 0u) | (unnormalizedProven ? 2u : 0u)}; + const std::array key{words[0], words[1], words[2], words[3], (compareEnable ? 1u : 0u) | (unnormalizedProven ? 2u : 0u) | (integerBorder ? 4u : 0u)}; std::lock_guard lock(mutex); ++clock; if (const auto found = entries.find(key); found != entries.end()) { @@ -69,7 +89,7 @@ namespace AgcDriver::Graphics { return found->second.sampler; } ++misses; - auto resource = DecodeSamplerResource(words, unnormalizedProven, forceDegammaPaired); + auto resource = DecodeSamplerResource(words, unnormalizedProven, forceDegammaPaired, integerBorder); resource.compareEnable = compareEnable; auto sampler = std::make_shared(context, resource); // The cap keeps live samplers well below the device's limit (NVIDIA: ~4000); a set in flight @@ -99,9 +119,33 @@ namespace AgcDriver::Graphics { return forcesDegamma; } - void RequireDegammaFormat(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span> samplers) { + bool Sampler::RequiresNonSeamlessCube() const { + return requiresNonSeamlessCube; + } + + bool ForcesDegammaOn(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span> samplers) { + 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() && !IsSrgbTextureFormat(guestFormat)) Require(false, "a texture of guest format " + std::to_string(guestFormat) + ", which is not sRGB, is sampled through a sampler that forces sRGB decoding, which is not implemented"); + if (((samplerMask >> element) & 1u) != 0u && samplers[element]->ForcesDegamma()) return true; + } + return false; + } + + void RequireBorderSwizzle(std::uint32_t bcSwizzle, std::uint32_t samplerMask, std::span> samplers) { + constexpr std::uint32_t BcSwizzleXyzw = 0u; + constexpr std::uint32_t BcSwizzleZyxw = 4u; + if (bcSwizzle == BcSwizzleXyzw || bcSwizzle == BcSwizzleZyxw) return; + for (std::uint32_t element = 0; element < 32u; ++element) { + if (((samplerMask >> element) & 1u) == 0u) continue; + if (element >= samplers.size()) Require(false, "a sampled texture is paired with sampler element " + std::to_string(element) + ", which its shader does not bind"); + if (samplers[element]->ReadsOpaqueBlackBorder()) Require(false, "a sampled texture whose BC swizzle " + std::to_string(bcSwizzle) + " moves the alpha channel is sampled through an opaque black border, which is not implemented"); + } + } + + void RequireNonSeamlessCube(bool cube, std::uint32_t samplerMask, std::span> samplers) { + if (!cube) return; + for (std::uint32_t element = 0; element < 32u && element < samplers.size(); ++element) { + if (((samplerMask >> element) & 1u) != 0u && samplers[element]->RequiresNonSeamlessCube()) Require(false, "a cube texture is sampled through a sampler that disables seamless cube filtering, which needs VK_EXT_non_seamless_cube_map"); } } 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 2d7b65e3c4..3859fc17d0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp @@ -5,6 +5,7 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include #include +#include #include #include #include @@ -23,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; @@ -38,14 +40,14 @@ std::uint32_t read(const Registers& registers, std::uint32_t offset, RegisterBan return it->second; } -template T _readHeaderPod(std::span header, std::uint64_t headerAddress, const void* pointer) { +template T _readHeaderPod(std::span header, std::uint64_t headerAddress, const void* pointer, std::size_t bytes = sizeof(T)) { if (pointer == nullptr) throw std::runtime_error("AGC graphics: null AGC header pointer"); const auto address = reinterpret_cast(pointer); if (address < headerAddress) throw std::runtime_error("AGC graphics: AGC header pointer precedes the shader header"); const auto offset = address - headerAddress; - if (offset + sizeof(T) > header.size()) throw std::runtime_error("AGC graphics: AGC header pointer is outside the registered shader header"); - T value; - std::memcpy(&value, header.data() + offset, sizeof(T)); + if (offset > header.size() || bytes > header.size() - offset) throw std::runtime_error("AGC graphics: AGC header pointer is outside the registered shader header"); + T value{}; + std::memcpy(&value, header.data() + offset, bytes); return value; } @@ -63,9 +65,9 @@ template void _readHeaderArray(std::span header, s } ShaderRecompiler::ShaderComputeStageInfo DecodeComputeStageInfo(const Registers& shader, std::span header) { - const auto numThreadX = read(shader, computeNumThreadX, RegisterBank::Shader); - const auto numThreadY = read(shader, computeNumThreadY, RegisterBank::Shader); - const auto numThreadZ = read(shader, computeNumThreadZ, RegisterBank::Shader); + const auto numThreadX = read(shader, computeNumThreadX, RegisterBank::Shader) & 0xffffu; + const auto numThreadY = read(shader, computeNumThreadY, RegisterBank::Shader) & 0xffffu; + const auto numThreadZ = read(shader, computeNumThreadZ, RegisterBank::Shader) & 0xffffu; if (numThreadX == 0 || numThreadY == 0 || numThreadZ == 0) { throw std::runtime_error("AGC graphics: COMPUTE_NUM_THREAD_X/Y/Z must be nonzero"); } @@ -92,6 +94,16 @@ ShaderRecompiler::ShaderComputeStageInfo DecodeComputeStageInfo(const Registers& }; } +ShaderRecompiler::ShaderPixelStageInfo DefaultUnexportedInputs(ShaderRecompiler::ShaderPixelStageInfo pixel, std::span parameterExports) { + for (std::uint32_t input = 0; input < pixel.interpolatorCount && input < pixel.interpolatorSettings.size(); ++input) { + auto& setting = pixel.interpolatorSettings[input]; + if ((setting & 0x20u) != 0u || std::ranges::find(parameterExports, setting & 0x1fu) != parameterExports.end()) continue; + setting = (setting & ~0x700u) | 0x20u; + if ((setting & 0x80000u) != 0u) setting = (setting & ~0x600000u) | 0x100000u; + } + return pixel; +} + ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& context, const std::array& exportMappings, bool nullProgram) { const auto inControl = nullProgram ? 0u : read(context, spiPsInControl, RegisterBank::Context); const auto inputNum = inControl & 0x3Fu; @@ -106,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::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); @@ -131,8 +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 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, @@ -145,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), @@ -152,22 +176,37 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con .pixelKillEnable = pixelKillEnable, .depthExportEnable = depthExportEnable, .sampleMaskExportEnable = sampleMaskExportEnable, - .earlyZ = zOrder == 1u && !pixelKillEnable && !depthExportEnable && !sampleMaskExportEnable, + .earlyZ = PixelEarlyZ(shaderControl), .executeOnNoop = ((shaderControl >> 10u) & 0x1u) != 0, .conservativeZExport = static_cast(conservativeZExport), .orderedPixelShader = ((shaderControl >> 16u) & 0x1u) != 0, .targetOutputMode = targetOutputMode, - .targetExportMapping = exportMappings + .targetExportMapping = nullProgram ? std::array{} : exportMappings, + .stencilRefExportEnable = stencilRefExportEnable, + .stencilOperationValueExport = stencilOperationValue, + .positionLocation = (positionControl & 0x100000u) != 0u ? ShaderRecompiler::PixelPositionLocation::UpperLeft : static_cast(floatLocation) }; } -ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span header, std::uint64_t headerAddress, std::span userData, std::vector* reads, bool staticAbi) { +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"); Shader shader; std::memcpy(&shader, header.data(), sizeof(Shader)); ShaderRecompiler::ShaderVertexStageInfo info{}; if (shader.user_data == nullptr) throw std::runtime_error("AGC graphics: missing AGC user-data header"); - const auto userDataHeader = _readHeaderPod(header, headerAddress, shader.user_data); + constexpr auto userDataBytes = offsetof(ShaderUserData, sharp_resource_count) + sizeof(ShaderUserData{}.sharp_resource_count); + const auto userDataHeader = _readHeaderPod(header, headerAddress, shader.user_data, userDataBytes); if (userDataHeader.direct_resource_count > ShaderRegs::AGC_DIRECT_RESOURCE_TYPE_COUNT) throw std::runtime_error("AGC graphics: AGC direct-resource count exceeds the known resource domain"); std::array directOffsets{}; directOffsets.fill(ShaderRegs::AGC_ILLEGAL_DIRECT_OFFSET); @@ -182,19 +221,29 @@ ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span(vertexBufferReg) + 1u >= userData.size()) throw std::runtime_error("AGC graphics: vertex buffer table pointer exceeds the user-SGPR domain"); - if (static_cast(vertexAttribReg) + 1u >= userData.size()) throw std::runtime_error("AGC graphics: vertex attribute table pointer exceeds the user-SGPR domain"); + const auto bufferWord = static_cast(vertexBufferReg) + hiddenUserWords; + const auto attribWord = static_cast(vertexAttribReg) + hiddenUserWords; + if (bufferWord + 1u >= userData.size()) throw std::runtime_error("AGC graphics: vertex buffer table pointer exceeds the user-SGPR domain"); + if (attribWord + 1u >= userData.size()) throw std::runtime_error("AGC graphics: vertex attribute table pointer exceeds the user-SGPR domain"); if (shader.num_input_semantics == 0 || shader.num_input_semantics > ShaderRecompiler::ShaderVertexStageInfo::MaxResources) throw std::runtime_error("AGC graphics: vertex semantic count is outside the supported domain"); if (shader.input_semantics == nullptr) throw std::runtime_error("AGC graphics: missing vertex input semantics"); std::array semantics{}; _readHeaderArray(header, headerAddress, shader.input_semantics, shader.num_input_semantics, semantics.data()); - const auto attribTableAddr = static_cast(userData[static_cast(vertexAttribReg)]) | (static_cast(userData[static_cast(vertexAttribReg) + 1u]) << 32u); - const auto bufferTableAddr = static_cast(userData[static_cast(vertexBufferReg)]) | (static_cast(userData[static_cast(vertexBufferReg) + 1u]) << 32u); + const auto attribTableAddr = static_cast(userData[attribWord]) | (static_cast(userData[attribWord + 1u]) << 32u); + const auto bufferTableAddr = static_cast(userData[bufferWord]) | (static_cast(userData[bufferWord + 1u]) << 32u); if (!staticAbi && attribTableAddr == 0) throw std::runtime_error("AGC graphics: null vertex attribute table address"); if (!staticAbi && bufferTableAddr == 0) throw std::runtime_error("AGC graphics: null vertex buffer table address"); info.fetchEmbedded = true; info.fetchAttribReg = static_cast(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"); @@ -208,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; @@ -219,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 fabc7134cc..277eea93c5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -2,17 +2,25 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "ThreadOwned.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include #include #include #include +#include #include #include #include #include +#include +#include #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" @@ -21,6 +29,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 @@ -29,6 +39,8 @@ #include #include #include +#include +#include #include "prx/libc/include/GuestAllocations.hpp" namespace AgcDriver::Graphics { @@ -51,8 +63,9 @@ 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}; - return {ComponentSwizzleFor(resource.dstSelX), ComponentSwizzleFor(resource.dstSelY), ComponentSwizzleFor(resource.dstSelZ), ComponentSwizzleFor(resource.dstSelW)}; + 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)}; } // Sampled textures are reused across draws and dispatches while their guest bytes are unchanged; a @@ -83,6 +96,8 @@ TextureKey MakeTextureKey(VkDevice device, std::span words, TextureKey key{device, {}, {static_cast(components.r), static_cast(components.g), static_cast(components.b), static_cast(components.a)}}; key.depthCompare = depthCompare; std::copy(words.begin(), words.end(), key.words.begin()); + key.words[5] &= ~((0xfffu << 8u) | (1u << 25u)); + key.words[6] &= ~0xffu; return key; } @@ -100,6 +115,12 @@ struct CachedTexture { std::shared_ptr source; std::uint64_t sourceVersion = 0; 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 @@ -111,11 +132,25 @@ struct TextureCache { std::unordered_map::iterator, TextureKeyHash> index; std::unordered_multimap::iterator> views; std::uint64_t bytes = 0; + std::size_t passed = 0; + std::chrono::steady_clock::time_point passedSince{}; + std::uint64_t firstUsesPresent = 0; + std::unordered_set firstUses; + struct Counts { + std::uint64_t inserted = 0; + std::uint64_t uncached = 0; + std::uint64_t evicted = 0; + std::uint64_t passed = 0; + std::uint64_t stale = 0; + } counts; + std::chrono::steady_clock::time_point countsReported = std::chrono::steady_clock::now(); VkPhysicalDevice budgetDevice = VK_NULL_HANDLE; 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; }; TextureCache& Textures() { @@ -123,6 +158,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; @@ -141,6 +196,21 @@ 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; +} + bool TextureHashEnabled() { static const bool disabled = std::getenv("APS5_NO_TEXTURE_HASH") != nullptr; return !disabled; @@ -167,8 +237,10 @@ 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; } void dropUncachedViews(TextureCache& cache) { @@ -190,6 +262,101 @@ void dropUncachedViews(TextureCache& cache) { // Moves an entry to the front (most recently used). void touchTexture(TextureCache& cache, std::list::iterator it) { cache.entries.splice(cache.entries.begin(), cache.entries, it); + it->present = Recorder::Presents(); +} + +bool admitTexture(TextureCache& cache, const TextureKey& key, std::uint64_t incoming, std::uint64_t budget) { + if (cache.bytes + incoming <= budget) return true; + if (const auto now = std::chrono::steady_clock::now(); now - cache.passedSince >= std::chrono::seconds(1)) { + cache.passed = 0; + cache.passedSince = now; + } + const auto present = Recorder::Presents(); + if (cache.firstUsesPresent != present) { + cache.firstUses.clear(); + cache.firstUsesPresent = present; + } + 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()); + 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 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); + ++cache.passed; + ++cache.counts.passed; + } else if (keep && !over) { + break; + } else { + eraseTexture(cache, back); + ++cache.counts.evicted; + } + } + return true; +} + +void reportCacheCounts(TextureCache& cache, std::uint64_t budget) { + static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; + if (!profile) return; + const auto now = std::chrono::steady_clock::now(); + 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 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 { + VkDevice device = VK_NULL_HANDLE; + std::array words{}; + GuestTextureResource resource{}; + std::uint64_t guestBytes = 0; + VkComponentMapping components{}; + bool hasEntry = false; + std::list::iterator entry{}; + std::uint64_t erasures = 0; +}; + +constexpr std::size_t ImageMemoSlots = 1024; +struct ImageMemoTag {}; + +ImageMemoEntry& imageMemoSlot(std::span words) { + auto& imageMemo = HostThreadLocal>, ImageMemoTag>(); + if (imageMemo == nullptr) imageMemo = std::make_unique>(); + const auto hash = hashWords(14695981039346656037ull, words.data(), words.size()); + return (*imageMemo)[(hash ^ (hash >> 29u)) % ImageMemoSlots]; +} + +const ImageMemoEntry& decodedImage(VkDevice device, std::span words) { + auto& slot = imageMemoSlot(words); + if (slot.device == device && std::equal(words.begin(), words.end(), slot.words.begin())) return slot; + slot.device = VK_NULL_HANDLE; + slot.resource = DecodeTextureResource(words); + slot.guestBytes = DescribeSurface(slot.resource).guestBytes; + slot.components = ViewComponents(slot.resource); + std::copy(words.begin(), words.end(), slot.words.begin()); + slot.hasEntry = false; + slot.device = device; + return slot; +} + +std::list::iterator findSampledEntry(TextureCache& cache, VkDevice device, const std::array& words, VkComponentMapping components) { + auto& slot = imageMemoSlot(words); + const bool same = slot.device == device && slot.words == words && slot.components.r == components.r && slot.components.g == components.g && slot.components.b == components.b && slot.components.a == components.a; + if (same && slot.hasEntry && slot.erasures == cache.erasures) return slot.entry; + const auto it = findTexture(cache, MakeTextureKey(device, words, components)); + if (same) { + slot.hasEntry = it != cache.entries.end(); + slot.entry = it; + slot.erasures = cache.erasures; + } + return it; } // APS5_PROFILE_DRAW: what the sampled-texture and storage-image lookups did, printed as [textures] @@ -210,6 +377,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}; }; @@ -226,7 +402,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, @@ -241,27 +417,54 @@ struct LookupRecord { DccKeys keys; std::uint64_t generation; const StorageTexture* source; + bool depth = false; }; 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; +}; + +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::uint64_t heldBytes(const StorageTexture& image) { - return std::max(image.AllocationBytes(), image.GuestBytes()); + return image.BudgetBytes(); } bool MetadataMoved(const StorageTexture& image, const GuestTextureResource& resource) { @@ -291,6 +494,14 @@ bool ClearedViewEnabled() { return !disabled; } +std::uint64_t DepthCopyMinBytes() { + static const std::uint64_t bytes = [] { + const char* text = std::getenv("APS5_DEPTH_COPY_MIN_KIB"); + return (text != nullptr ? std::strtoull(text, nullptr, 10) : 1024ull) << 10u; + }(); + return bytes; +} + // Surfaces whose storage image could not be made (no writable committed pages, say): remembered // so the sampled lookup does not throw and fall back on every use. A failure is keyed by the surface // (address and size: the heap reuses addresses) and holds only while the guest mappings are what @@ -343,7 +554,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; @@ -353,7 +567,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) { @@ -369,11 +601,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; @@ -382,16 +620,76 @@ 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; + } 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(); - if (auto depth = DepthSurfaceTexture(context, words, resource, components)) return depth; + if (auto depth = DepthSurfaceTexture(context, words, resource, components)) { + logLookup({depth.get(), resource, 0, DccKeys::Uncompressed, 0, nullptr, true}); + return depth; + } const auto depthBitsWidth = words.size() >= 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) { @@ -399,6 +697,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{}; @@ -408,15 +721,20 @@ 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); - if (source != nullptr && (depthCompare || !Texture::CanCopyFrom(*source, resource) || MetadataMoved(*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; if (source != nullptr && resource.dccAddress != 0 && IsDccClear(source->FilledKeys())) { @@ -434,18 +752,21 @@ std::shared_ptr cachedTexture(const Context& context, std::span cachedTexture(const Context& context, std::span cachedTexture(const Context& context, std::span(context, source, resource, components, Texture::DepthCopyTag{}); + texture->RefreshDepthCopy(); + return texture; + } + if (source != nullptr && blockCopy) { + auto texture = std::make_shared(context, source, resource, components, Texture::BlockCopyTag{}); + texture->RefreshDepthCopy(); + 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); return std::make_shared(context, *context.detiler, resource, components, snapshot, depthCompare); } + auto snapshotGeneration = generation; + std::optional>> committed; + if (source == nullptr && generation == 0) { + std::vector> ranges; + if (const auto partial = GuestMemory::CollectCommittedWrites(address, bytes, ranges); partial != 0) { + snapshotGeneration = partial; + committed = std::move(ranges); + } + } + const auto untracked = source == nullptr && generation == 0 && !committed.has_value() && *keys == DccKeys::Uncompressed ? untrackedKey(address, bytes) : std::array{}; 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 (!stagedFlushed && (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 (it->texture->IsDepthCopy()) it->texture->RefreshDepthCopy(); it->keys = *keys; touchTexture(cache, it); logLookup({it->texture.get(), resource, guestBytes, *keys, 0, it->source.get()}); @@ -503,17 +866,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); }; - if (*keys != DccKeys::Uncompressed || GuestMemory::UnchangedSince(address, it->bytes.size(), it->generation) || equalsCommitted()) { - it->generation = generation; + const auto unchanged = [&] { + if (it->committed != committed) return false; + return committed.has_value() ? !Recorder::SnapshotWriteOverlaps(address, static_cast(it->surfaceBytes)) && GuestMemory::UnchangedCommittedSince(*committed, it->generation) : GuestMemory::UnchangedSince(address, static_cast(it->surfaceBytes), it->generation); + }; + 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(); @@ -522,18 +898,37 @@ std::shared_ptr cachedTexture(const Context& context, std::span(source != nullptr ? 0u : bytes), nullptr, *keys, generation}; + CachedTexture entry{key, address, std::vector(source != nullptr ? 0u : bytes), nullptr, *keys, snapshotGeneration}; + entry.committed = std::move(committed); + entry.surfaceBytes = bytes; if (source != nullptr) { entry.source = source; entry.sourceVersion = source->Version(); - entry.texture = std::make_shared(context, source, resource, components); + if (depthCompare) { + entry.texture = std::make_shared(context, source, resource, components, Texture::DepthCopyTag{}); + entry.texture->RefreshDepthCopy(); + } else if (blockCopy) { + entry.texture = std::make_shared(context, source, resource, components, Texture::BlockCopyTag{}); + entry.texture->RefreshDepthCopy(); + } else 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); + } entry.accounted = source->Cached() ? 0u : heldBytes(*source); counters.fromStorage.fetch_add(1, std::memory_order_relaxed); } else { // Snapshot before the upload so a write racing with it is caught by the next comparison. - if (*keys == DccKeys::Uncompressed && Recorder::SnapshotWriteOverlaps(address, bytes)) counters.pendingReads.fetch_add(1, std::memory_order_relaxed); + const bool pending = *keys == DccKeys::Uncompressed && Recorder::SnapshotWriteOverlaps(address, bytes); + if (pending) counters.pendingReads.fetch_add(1, std::memory_order_relaxed); ReadTextureSurface(resource, *keys, entry.bytes); + if (!pending) entry.untracked = untracked; static const bool traceTextures = std::getenv("APS5_TRACE_TEXTURES") != nullptr; if (traceTextures) { std::size_t nonzero = 0; @@ -542,16 +937,28 @@ std::shared_ptr 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); - while (!cache.entries.empty() && cache.bytes + entry.accounted > budget) eraseTexture(cache, std::prev(cache.entries.end())); - cache.bytes += entry.accounted; 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(); + if (profile) LookupOutcomes::Add(source != nullptr ? LookupOutcomes::SampledMadeView : LookupOutcomes::SampledMadeSnapshot, start); + return texture; + } + cache.bytes += entry.accounted; + entry.present = Recorder::Presents(); + ++cache.counts.inserted; logLookup({texture.get(), resource, guestBytes, *keys, generation, source.get()}); cache.entries.push_front(std::move(entry)); cache.index[key] = cache.entries.begin(); if (source != nullptr) cache.views.emplace(source.get(), cache.entries.begin()); + reportCacheCounts(cache, budget); reportTextureCounters(); if (profile) LookupOutcomes::Add(source != nullptr ? LookupOutcomes::SampledMadeView : LookupOutcomes::SampledMadeSnapshot, start); return texture; @@ -619,7 +1026,8 @@ std::list::iterator findStorageByImage(StorageTextureCache } // Evicts an entry: its pending results go to guest memory first (the image may die with the entry). -void evictStorage(StorageTextureCache& cache, std::list::iterator it) { +std::shared_ptr evictStorage(StorageTextureCache& cache, std::list::iterator it) { + auto texture = it->texture; it->texture->SetCached(false); noteUncached(it->texture); it->texture->Flush(); @@ -627,13 +1035,14 @@ void evictStorage(StorageTextureCache& cache, std::list::i cache.index.erase(it->key); cache.byImage.erase(it->texture.get()); cache.entries.erase(it); + return texture; } // Storage images are shared by every descriptor of one surface (address, extent, layers, format, tile // mode): the image holds the whole mip chain, and render targets in the same memory attach to it. std::array SurfaceKey(const Context& context, const GuestTextureResource& resource) { // Guest formats that store in the same Vulkan format share the image (views carry the difference). - return {static_cast(resource.baseAddress), static_cast(resource.baseAddress >> 32u), resource.width, resource.height, (resource.depthOrLastArray << 16u) | (resource.mipCount & 0xffffu), (static_cast(resource.tileMode) << 12u) | (static_cast(resource.dimension) << 20u), resource.baseArray, static_cast(StorageFormatForGuest(context, resource.format))}; + return {static_cast(resource.baseAddress | resource.pipeBankXor), static_cast(resource.baseAddress >> 32u), resource.width, resource.height, (resource.depthOrLastArray << 16u) | (resource.mipCount & 0xffffu), (static_cast(resource.tileMode) << 12u) | (static_cast(resource.dimension) << 20u), resource.baseArray, static_cast(StorageFormatForGuest(context, resource.format))}; } struct ExtendedSurfaces { @@ -672,15 +1081,55 @@ 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) { - if (DepthSurfaceAt(viewed.baseAddress)) { - char text[112]; - std::snprintf(text, sizeof(text), "AGC graphics: storage image access to depth/stencil surface 0x%llx is not implemented", static_cast(viewed.baseAddress)); - throw std::runtime_error(text); +std::shared_ptr cachedStorageTexture(const Context& context, std::span words, const GuestTextureResource& requested, std::uint32_t mip, std::uint64_t guestBytes, bool written) { + 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))); + 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 std::make_shared(context, *context.detiler, viewed, mip); + if (disabled) return StorageTexture::Create(context, *context.detiler, viewed, mip); static_cast(words); if (auto* recorder = Recorder::Active(); recorder != nullptr) recorder->BoundKeptBytes(); const bool profile = LookupOutcomes::Profiled(); @@ -689,47 +1138,62 @@ std::shared_ptr cachedStorageTexture(const Context& context, std const auto resource = StorageSurface(context, viewed); const StorageKey key{context.device, SurfaceKey(context, resource)}; auto& cache = StorageTextures(); - std::lock_guard lock(cache.mutex); - if (resource.mipCount > AllocatedLevels(resource)) { - auto allocated = resource; - allocated.mipCount = AllocatedLevels(resource); - if (const auto found = findStorage(cache, {context.device, SurfaceKey(context, allocated)}); found != cache.entries.end()) evictStorage(cache, found); + std::vector> evicted; + 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); + if (const auto found = findStorage(cache, {context.device, SurfaceKey(context, allocated)}); found != cache.entries.end()) evicted.push_back(evictStorage(cache, found)); + } + auto it = findStorage(cache, key); + if (it != cache.entries.end() && MetadataMoved(*it->texture, resource)) { + evicted.push_back(evictStorage(cache, it)); + it = cache.entries.end(); + } + if (it != cache.entries.end()) { + it->texture->Refresh(); + cache.entries.splice(cache.entries.begin(), cache.entries, it); + texture = it->texture; + counters.storageHits.fetch_add(1, std::memory_order_relaxed); + if (profile) LookupOutcomes::Add(LookupOutcomes::StorageHit, start); + } else { + if (guestBytes == 0) guestBytes = DescribeSurface(resource).guestBytes; + if (StorageTexture::FlushPending(resource.baseAddress, static_cast(guestBytes), nullptr, "storage image creation", PublishScope::None) && profile) start = LookupOutcomes::Add(LookupOutcomes::PendingFlush, start); + CachedStorageTexture entry{key, mip, StorageTexture::Create(context, *context.detiler, resource, mip)}; + static const bool keepNew = std::getenv("APS5_NO_KEEP_NEW_STORAGE") == nullptr; + if (auto* recorder = Recorder::Active(); keepNew && recorder != nullptr && GuestMemory::GpuMutex().HeldByThisThread() && recorder->Recording()) recorder->Keep(entry.texture); + entry.accounted = heldBytes(*entry.texture); + const auto budget = storageBudget(context); + while (!cache.entries.empty() && cache.bytes + entry.accounted > budget) evicted.push_back(evictStorage(cache, std::prev(cache.entries.end()))); + cache.bytes += entry.accounted; + texture = entry.texture; + cache.entries.push_front(std::move(entry)); + cache.index[key] = cache.entries.begin(); + cache.byImage[texture.get()] = cache.entries.begin(); + texture->SetCached(true); + counters.storageCreated.fetch_add(1, std::memory_order_relaxed); + if (profile) LookupOutcomes::Add(LookupOutcomes::StorageMade, start); + } } - auto it = findStorage(cache, key); - if (it != cache.entries.end() && MetadataMoved(*it->texture, resource)) { - evictStorage(cache, it); - it = cache.entries.end(); + for (const auto& image : evicted) { + std::vector> resources; + SharedResourceCache().RemoveImage(image.get(), &resources); + if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->Recording()) { + 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 (it != cache.entries.end()) { - it->texture->Refresh(); - cache.entries.splice(cache.entries.begin(), cache.entries, it); - counters.storageHits.fetch_add(1, std::memory_order_relaxed); - if (profile) LookupOutcomes::Add(LookupOutcomes::StorageHit, start); - return it->texture; + if (!evicted.empty()) { + auto& sampled = Textures(); + std::lock_guard lock(sampled.mutex); + dropUncachedViews(sampled); } - // Results other images hold over this memory reach it before the new image reads it: a hit's - // Refresh flushes them, the constructor's upload does not, and a GPU-direct upload reads the - // import buffer without the flush hook. The flush is recorded ahead of the upload in the batch. - if (guestBytes == 0) guestBytes = DescribeSurface(resource).guestBytes; - if (StorageTexture::FlushPending(resource.baseAddress, static_cast(guestBytes), nullptr, "storage image creation", PublishScope::None) && profile) start = LookupOutcomes::Add(LookupOutcomes::PendingFlush, start); - CachedStorageTexture entry{key, mip, std::make_shared(context, *context.detiler, resource, mip)}; - // The constructor's upload may have recorded into the open batch (a GPU clear, a direct - // detile) before the image could keep itself (no weak_from_this yet): the batch keeps it here, - // so an eviction or a failed view before it ran cannot destroy a referenced image. - // APS5_NO_KEEP_NEW_STORAGE=1 leaves the image to its cache entry alone, as before. - static const bool keepNew = std::getenv("APS5_NO_KEEP_NEW_STORAGE") == nullptr; - if (auto* recorder = Recorder::Active(); keepNew && recorder != nullptr && GuestMemory::GpuMutex().HeldByThisThread() && recorder->Recording()) recorder->Keep(entry.texture); - entry.accounted = heldBytes(*entry.texture); - const auto budget = storageBudget(context); - while (!cache.entries.empty() && cache.bytes + entry.accounted > budget) evictStorage(cache, std::prev(cache.entries.end())); - cache.bytes += entry.accounted; - auto texture = entry.texture; - cache.entries.push_front(std::move(entry)); - cache.index[key] = cache.entries.begin(); - cache.byImage[texture.get()] = cache.entries.begin(); - texture->SetCached(true); - counters.storageCreated.fetch_add(1, std::memory_order_relaxed); - if (profile) LookupOutcomes::Add(LookupOutcomes::StorageMade, start); return texture; } @@ -740,6 +1204,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); @@ -754,6 +1228,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; { @@ -773,9 +1259,17 @@ 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) { + Require(first.size() == 8 && second.size() == 8, "sampled texture keys take eight descriptor words"); + const VkComponentMapping components{VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_A}; + const auto firstKey = MakeTextureKey(VK_NULL_HANDLE, first, components); + const auto secondKey = MakeTextureKey(VK_NULL_HANDLE, second, components); + return firstKey == secondKey && TextureKeyHash{}(firstKey) == TextureKeyHash{}(secondKey); } void FlushCachedTextures(VkDevice device) { @@ -799,19 +1293,117 @@ void ClearCachedTextures(VkDevice device) { } } auto& storage = StorageTextures(); - std::lock_guard lock(storage.mutex); - for (auto it = storage.entries.begin(); it != storage.entries.end();) { - if (it->key.device != device) { - ++it; - continue; + std::vector> cleared; + { + std::lock_guard lock(storage.mutex); + for (auto it = storage.entries.begin(); it != storage.entries.end();) { + if (it->key.device != device) { + ++it; + continue; + } + cleared.push_back(it->texture); + it->texture->SetCached(false); + noteUncached(it->texture); + storage.bytes -= it->accounted; + storage.index.erase(it->key); + storage.byImage.erase(it->texture.get()); + it = storage.entries.erase(it); } - it->texture->SetCached(false); - noteUncached(it->texture); - storage.bytes -= it->accounted; - storage.index.erase(it->key); - storage.byImage.erase(it->texture.get()); - it = storage.entries.erase(it); } + for (const auto& image : cleared) { + std::vector> resources; + SharedResourceCache().RemoveImage(image.get(), &resources); + if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->Recording()) { + 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) { @@ -862,6 +1454,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"); } @@ -896,12 +1492,22 @@ 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, indexAddress, indexBytes) {} +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) {} -ShaderResources::ShaderResources(const Context& context, std::span shaders, const ColorTarget& target, std::uint64_t indexAddress, std::size_t indexBytes, std::span snapshots) : context(context), guestMemory(context) { +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); - build(shaders, &target, indexAddress, indexBytes); + build(shaders, &target, colorAttachments, indexAddress, indexBytes); } ShaderResources::ShaderResources(const Context& context, const CompiledShader& compute, std::span snapshots) : ShaderResources(context, compute, snapshots, false) {} @@ -916,7 +1522,7 @@ bool UsesAddressTables(const CompiledShader& compute) { } -ShaderResources::ShaderResources(const Context& context, const CompiledShader& compute, std::span snapshots, bool deferred) : context(context), guestMemory(context), deferredCompute(compute), deferredSnapshots(snapshots) { +ShaderResources::ShaderResources(const Context& context, const CompiledShader& compute, std::span snapshots, bool deferred, std::uint64_t dispatchThreads) : context(context), guestMemory(context), dispatchThreads(dispatchThreads), deferredCompute(compute), deferredSnapshots(snapshots) { Require(compute.stage == ShaderRecompiler::ShaderStage::Compute, "compute resources require a compute shader"); // Every use of a compute build is a recorded dispatch that calls MarkGpuWrites, which staged // buffers need (a synchronous draw's use would not). @@ -924,7 +1530,7 @@ ShaderResources::ShaderResources(const Context& context, const CompiledShader& c const std::span shaders(&deferredCompute, 1); if (!deferred) { prepareAddressBindings(shaders, snapshots); - build(shaders, nullptr, 0, 0); + build(shaders, nullptr, 0, 0, 0); forgetDeferredInputs(); return; } @@ -936,7 +1542,7 @@ ShaderResources::ShaderResources(const Context& context, const CompiledShader& c if (lockedBuild) return; unlockedPrepare = true; prepareAddressBindings(shaders, snapshots); - buildPrepare(shaders, nullptr, 0, 0); + buildPrepare(shaders, nullptr, 0, 0, 0); } void ShaderResources::Complete() { @@ -944,7 +1550,7 @@ void ShaderResources::Complete() { const std::span shaders(&deferredCompute, 1); if (lockedBuild) { prepareAddressBindings(shaders, deferredSnapshots); - buildPrepare(shaders, nullptr, 0, 0); + buildPrepare(shaders, nullptr, 0, 0, 0); } buildComplete(); forgetDeferredInputs(); @@ -958,21 +1564,20 @@ void ShaderResources::forgetDeferredInputs() { deferredSnapshots = {}; } -void ShaderResources::build(std::span shaders, const ColorTarget* target, std::uint64_t indexAddress, std::size_t indexBytes) { - buildPrepare(shaders, target, indexAddress, indexBytes); +void ShaderResources::build(std::span shaders, const ColorTarget* target, std::uint32_t colorAttachments, std::uint64_t indexAddress, std::size_t indexBytes) { + buildPrepare(shaders, target, colorAttachments, indexAddress, indexBytes); buildComplete(); } namespace { -VkDescriptorSetLayout CreateSetLayout(const Context& context, std::span bindings, bool updateAfterBind) { - std::vector flags; - for (const auto& binding : bindings) flags.push_back(updateAfterBind && binding.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0); +VkDescriptorSetLayout CreateSetLayout(const Context& context, std::span bindings, bool updateAfterBind, std::span flags) { + Require(flags.size() == bindings.size(), "descriptor binding flag count differs from layout"); VkDescriptorSetLayoutBindingFlagsCreateInfo bindingFlags{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_BINDING_FLAGS_CREATE_INFO}; bindingFlags.bindingCount = static_cast(flags.size()); bindingFlags.pBindingFlags = flags.data(); VkDescriptorSetLayoutCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; - info.pNext = updateAfterBind ? &bindingFlags : nullptr; + info.pNext = std::ranges::any_of(flags, [](auto value) { return value != 0; }) ? &bindingFlags : nullptr; info.flags = updateAfterBind ? VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT : 0; info.bindingCount = static_cast(bindings.size()); info.pBindings = bindings.data(); @@ -981,6 +1586,14 @@ VkDescriptorSetLayout CreateSetLayout(const Context& context, std::span PoolSizes(std::span bindings) { + std::map counts; + for (const auto& binding : bindings) counts[binding.descriptorType] += binding.descriptorCount; + std::vector sizes; + for (const auto& [type, count] : counts) sizes.push_back({type, count}); + return sizes; +} + VkDescriptorPool CreateSetPool(const Context& context, std::span sizes, bool updateAfterBind) { VkDescriptorPoolCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; info.flags = updateAfterBind ? VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT : 0; @@ -1072,8 +1685,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; @@ -1085,12 +1837,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) { @@ -1124,7 +1877,7 @@ void ValidateRuntimeResources(const CompiledShader& shader, std::span shaders, const ColorTarget* target, std::uint64_t indexAddress, std::size_t indexBytes) { +void ShaderResources::buildPrepare(std::span shaders, const ColorTarget* target, std::uint32_t colorAttachments, std::uint64_t indexAddress, std::size_t indexBytes) { PerformanceTimer frameTiming("Resources.Prepare"); const auto stageStart = std::chrono::steady_clock::now(); phaseStart = stageStart; @@ -1139,9 +1892,15 @@ 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 = 0; + std::uint64_t stageResources = shader.stage == ShaderRecompiler::ShaderStage::Fragment ? colorAttachments : 0; + Require(stageResources <= context.limits.maxPerStageResources, "shader descriptors exceed per-stage limits"); std::vector offsetsInData; std::int64_t shaderData = -1; const auto firstSampler = samplers.size(); @@ -1153,8 +1912,39 @@ void ShaderResources::buildPrepare(std::span shaders, cons updateAfterBind = true; }; for (const auto& binding : shader.program->bindings) { - Require(binding.descriptorSet == 0, "unexpected descriptor set: every shader resource must use descriptor set zero"); + 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; @@ -1180,7 +1970,11 @@ void ShaderResources::buildPrepare(std::span shaders, cons // vector) counts as written, like imageWritten below. const bool written = element >= binding.bufferWritten.size() || binding.bufferWritten[element]; const bool atomic = element < binding.bufferAtomic.size() && binding.bufferAtomic[element]; - const auto index = addGuestBuffer(std::span(binding.guestDescriptor).subspan(static_cast(element) * 4, 4), target, indexAddress, indexBytes, written, atomic); + const bool read = element < binding.bufferRead.size() && binding.bufferRead[element]; + 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; @@ -1217,6 +2011,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"); @@ -1229,43 +2058,126 @@ void ShaderResources::buildPrepare(std::span shaders, cons if (drawBuild && !usesBda && std::all_of(allocations.begin(), allocations.end(), [&](const Allocation& allocation) { return !allocation.guest || allocation.pushByte >= 0 || (allocation.dataAllocation >= 0 && allocation.dataByte < allocations[static_cast(allocation.dataAllocation)].size); })) guestMemory.AllowAdjustedRegions(); guestMemory.UploadPrepare(usesBda); timing.uploadMs = phase(BuildPhase::Upload); - std::vector description; + splitSets = SplitsSets(context, shaders); + std::array, 2> descriptions; + bool tablesUpdateAfterBind = false; for (const auto& binding : bindings) { - description.push_back(binding.layout); - layoutKey.insert(layoutKey.end(), {binding.layout.binding, static_cast(binding.layout.descriptorType), binding.layout.descriptorCount, binding.layout.stageFlags}); + const auto set = setOf(binding); + descriptions[set].push_back(binding.layout); + (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); } - if (updateAfterBind) layoutKey.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; - if (context.descriptorCache != nullptr && !noLayoutCache) { - _layout = context.descriptorCache->Layout(layoutKey, description, updateAfterBind); - } else { + 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); - _layout = CreateSetLayout(context, description, updateAfterBind); - ownsLayout = true; + owns = true; + return CreateSetLayout(context, description, updateAfterBind, flags); + }; + _layout = createLayout(layoutKey, descriptions[0], layoutBindingFlags, ownsLayout); + if (splitSets) { + fragment.layout = createLayout(fragmentLayoutKey, descriptions[1], fragmentBindingFlags, fragment.ownsLayout); + { + const auto sizes = splitPoolSizes(1); + const auto variableCount = tableVariableCount(1); + if (context.descriptorCache != nullptr && !noPoolCache) { + 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); + fragment.set = allocateVariableSet(fragment.pool, fragment.layout, variableCount); + } + } } - if (!bindings.empty()) { + if (splitSets) { + const auto sizes = splitPoolSizes(0); + const auto variableCount = tableVariableCount(0); + if (context.descriptorCache != nullptr && !noPoolCache) { + cacheAllocation = context.descriptorCache->Allocate(_layout, sizes, updateAfterBind, variableCount); + _set = cacheAllocation.set; + } + if (_set == VK_NULL_HANDLE) { + pool = CreateSetPool(context, sizes, updateAfterBind); + _set = allocateVariableSet(pool, _layout, variableCount); + } + } + if (!splitSets && !bindings.empty()) { // The set is sized from the plan: every image element becomes exactly one descriptor // 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; - cachePool = allocated.pool; + 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(); @@ -1277,16 +2189,100 @@ void ShaderResources::buildPrepare(std::span shaders, cons } } +bool SplitsSets(const Context& context, std::span shaders) { + const auto fragmentSet = [](const CompiledShader& shader) { return shader.program != nullptr && std::any_of(shader.program->bindings.begin(), shader.program->bindings.end(), [](const auto& binding) { return binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet; }); }; + const auto fragmentStage = [](const CompiledShader& shader) { return shader.stage == ShaderRecompiler::ShaderStage::Fragment; }; + return std::any_of(shaders.begin(), shaders.end(), fragmentSet) || (context.graphicsPipelineLibrary && std::any_of(shaders.begin(), shaders.end(), fragmentStage)); +} + +DescribedSetLayouts DescribeSetLayouts(const Context& context, std::span shaders) { + Require(context.descriptorCache != nullptr, "described set layouts need the descriptor cache"); + DescribedSetLayouts result; + std::array, 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"); + const VkShaderStageFlags flags = VulkanStage(shader.stage); + std::uint64_t stageStorageBuffers = 0; + std::uint64_t stageResources = 0; + for (const auto& binding : shader.program->bindings) { + 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; + if (type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER) { + stageStorageBuffers += binding.count; + storageBuffers += binding.count; + } + if (stageStorageBuffers > context.limits.maxPerStageDescriptorStorageBuffers || stageResources > context.limits.maxPerStageResources) updateAfterBind = true; + 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, 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; try { // The lookups run in plan order: Revalidate walks the bindings the same way, and consecutive // storage elements of one mip chain share the previous element's image. + previousSampled = {}; + for (auto& source : samplerSources) source.singleLevelImage = source.mipmappedImage = false; for (const auto& deferred : deferredImages) resolveImageBinding(*deferred.binding, bindings[deferred.index], std::span>(samplers).subspan(deferred.firstSampler, deferred.samplerCount)); + previousSampled = {}; deferredImages.clear(); + applyAnisoOverride(); // The records served their purpose: each holds the cache entry's objects as of stage A, // which would otherwise keep a replaced texture or an evicted storage image (and its device // memory) alive, outside the caches' budgets, for as long as this object is cached. @@ -1294,50 +2290,107 @@ 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) { + 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 // pointer into them stays valid until the call. std::size_t bufferCount = 0; 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; } - std::vector buffers; - std::vector images; + auto& buffers = descriptorBuffers; + auto& images = descriptorImages; buffers.reserve(bufferCount); images.reserve(imageCount); - std::vector writes; + 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 = _set; + write.dstSet = setOf(binding) == 0 ? _set : fragment.set; write.dstBinding = binding.layout.binding; write.descriptorCount = binding.layout.descriptorCount; write.descriptorType = binding.layout.descriptorType; switch (binding.layout.descriptorType) { case VK_DESCRIPTOR_TYPE_STORAGE_BUFFER: write.pBufferInfo = buffers.data() + buffers.size(); - for (const auto index : binding.allocations) buffers.push_back(descriptor(allocations[index])); + for (const auto index : binding.allocations) { + buffers.push_back(descriptor(allocations[index])); + } break; case VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE: write.pImageInfo = images.data() + images.size(); for (const auto index : binding.imageAllocations) { - if (index == std::numeric_limits::max()) images.push_back({VK_NULL_HANDLE, VK_NULL_HANDLE, VK_IMAGE_LAYOUT_GENERAL}); - else images.push_back({VK_NULL_HANDLE, textureFirstLayer[index] ? textures[index]->FirstLayerView() : textures[index]->View(), textures[index]->Layout()}); + if (index == std::numeric_limits::max()) images.push_back(absentImage(binding.layout.binding)); + else images.push_back({VK_NULL_HANDLE, textureSrgb[index] ? textures[index]->SrgbView(textureFirstLayer[index], textureBaseLevel[index]) : textureBaseLevel[index] ? textures[index]->BaseLevelView() : textureFirstLayer[index] ? textures[index]->FirstLayerView() : textures[index]->View(), textures[index]->Layout()}); } Require(binding.imageAllocations.size() == binding.layout.descriptorCount, "descriptor allocations disagree with compact binding"); break; case VK_DESCRIPTOR_TYPE_STORAGE_IMAGE: write.pImageInfo = images.data() + images.size(); for (const auto index : binding.imageAllocations) { - if (index == std::numeric_limits::max()) images.push_back({VK_NULL_HANDLE, VK_NULL_HANDLE, VK_IMAGE_LAYOUT_GENERAL}); + 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"); @@ -1351,7 +2404,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; @@ -1410,6 +2478,16 @@ 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}; + +} + +namespace { + +bool LoopGuardEnabled() { + static const bool enabled = std::getenv("APS5_LOOP_GUARD") != nullptr; + return enabled; +} } @@ -1418,8 +2496,9 @@ void ShaderResources::noteReusable() { captureValidation(); reusable = false; directRegions.clear(); - if (NeedsCompletion() || HoldsLease()) return; - if (TemplateDataRefresh() && std::any_of(allocations.begin(), allocations.end(), [](const Allocation& allocation) { return allocation.buffer != nullptr && !allocation.guest && allocation.size > MaxRefreshBytes; })) 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.role != ShaderRecompiler::DescriptorRole::ImageTableMap && allocation.size > MaxRefreshBytes; })) return; const auto regions = guestMemory.DirectRegions(); if (!regions.has_value()) return; for (const auto& [begin, end] : *regions) { @@ -1432,9 +2511,27 @@ 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) { + return shader.program != nullptr && std::any_of(shader.program->bindings.begin(), shader.program->bindings.end(), [loopGuard](const ShaderRecompiler::DescriptorBinding& binding) { return binding.role == ShaderRecompiler::DescriptorRole::BdaPagetable || (loopGuard && binding.role == ShaderRecompiler::DescriptorRole::FaultBuffer); }); + }); +} + // APS5_PROFILE_DRAW: the per-device descriptor caches' counters, every 10 s. void ShaderResources::reportDescriptorCaches() const { static std::mutex reportMutex; @@ -1487,23 +2584,138 @@ void VisitContentKey(const CompiledShader& shader, bool dataWords, bool movableB packBits(binding.imageDepthCompare); 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 // must not serve a build with another (the variant implies it, this makes it explicit). 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; - key.reserve(8u + shader.program->bindings.size() * 12u); - VisitContentKey(shader, dataWords, movableBuffers, [&](std::uint32_t word) { key.push_back(word); }); + 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(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.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); }); +} + +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) { std::size_t cursor = 0; bool equal = true; @@ -1521,7 +2733,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"}; +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"}; @@ -1637,10 +2849,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); } @@ -1649,7 +2861,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 @@ -1659,6 +2871,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 @@ -1673,11 +2898,15 @@ void ShaderResources::captureValidation() { }; validatedTextures.assign(textures.size(), {}); for (std::size_t i = 0; i < textures.size(); ++i) { - if (const auto* found = record(textures[i].get())) validatedTextures[i] = {found->resource, found->bytes, found->keys, found->generation, 0, found->source, true}; + if (const auto* found = record(textures[i].get())) validatedTextures[i] = {found->resource, found->bytes, found->keys, found->generation, 0, found->source, found->depth, true}; } 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 @@ -1729,8 +2958,14 @@ 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; @@ -1784,6 +3019,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 @@ -1847,8 +3086,14 @@ bool ShaderResources::fastRevalidateEach() { for (std::size_t i = 0; i < textures.size(); ++i) { auto& surface = validatedTextures[i]; if (!surface.valid) return false; + if (surface.depth) { + 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; @@ -1883,6 +3128,10 @@ bool ShaderResources::fastRevalidateEach() { if (own.dccAddress != 0) return false; const auto address = own.baseAddress; const auto bytes = static_cast(image->GuestBytes()); + if (DepthSurfaceOverlaps(context.device, address, bytes)) { + depthGuardRefusals.fetch_add(1, std::memory_order_relaxed); + return false; + } if (GuestMemory::CollectWrites(address, bytes) == 0) return false; if (PendingStorageOverlaps(address, bytes, image)) return false; if (!StorageImageCached(context, image)) return false; @@ -1953,7 +3202,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); @@ -1963,7 +3212,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; @@ -1972,16 +3221,67 @@ 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; } +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 false; + const auto drawWriters = DrawCopiedWriters(); + const bool labels = Recorder::PendingCompletionLabels() != 0; + 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->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) { if (report != nullptr) *report = {ProofPath::Full, ProofFailure::Other}; if (!reusable || shaders.empty()) return false; static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; + const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; + for (const auto& region : directRegions) { + 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)); + } + for (const auto& image : storageTextures) { + CompletePendingCpuWrites(context, image->Descriptor().baseAddress, static_cast(image->GuestBytes())); + if (image->Descriptor().dccAddress != 0) CompletePendingCpuWrites(context, image->Descriptor().dccAddress, DccKeyBytes(image->GuestBytes())); + } // APS5_NO_FAST_REVALIDATE=1 always repeats the lookups. static const bool noFast = std::getenv("APS5_NO_FAST_REVALIDATE") != nullptr; - const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; const auto finish = [&](bool fast, bool ok, ProofFailure failure = ProofFailure::Other) { if (profile) countRevalidate(fast, ok, start); if (report != nullptr) report->failure = ok ? ProofFailure::None : failure; @@ -1995,7 +3295,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) { @@ -2005,27 +3307,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 @@ -2061,6 +3401,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); @@ -2068,7 +3413,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; @@ -2111,15 +3456,20 @@ 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; }; if (EpochRevalidate()) { - if (!directRegions.empty() && !(pendingSerialSeen != 0 && pendingSerialSeen == StorageTexture::PendingSerial())) { + if ((!directRegions.empty() || !guestMemory.Writes().empty()) && !(pendingSerialSeen != 0 && pendingSerialSeen == StorageTexture::PendingSerial())) { thread_local std::vector* regionsSlot = nullptr; auto& regions = ShaderRecompiler::ThreadOwned(regionsSlot); regions.clear(); for (const auto& region : directRegions) regions.push_back({region.begin, region.end, nullptr, nullptr, false}); + for (const auto& [begin, end] : guestMemory.Writes()) regions.push_back({begin, end, nullptr, nullptr, false}); StorageTexture::ScanPending(regions); // A region over a unit shadow's fresh results (a retile bumped the serial) is // published by the flush, as one over a pending image is stored. @@ -2135,6 +3485,7 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR } } else { for (const auto& region : directRegions) StorageTexture::FlushPending(region.begin, static_cast(region.end - region.begin), nullptr, "imported buffer region"); + for (const auto& [begin, end] : guestMemory.Writes()) StorageTexture::FlushPending(begin, static_cast(end - begin), nullptr, "written buffer range"); if (!serialLoop()) return finish(fast, false, ProofFailure::Imports); } // (3) Buffers staged in device memory (GuestBufferMemory::AllowDeviceStaging) are copied in @@ -2149,6 +3500,9 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR } } pendingSerialSeen = EpochRevalidate() && StorageTexture::PendingSerial() == serialBefore ? serialBefore : 0; + for (const auto& texture : textures) { + if (texture != nullptr && texture->IsDepthCopy()) texture->RefreshDepthCopy(); + } return finish(fast, true); } @@ -2158,6 +3512,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{}; @@ -2166,6 +3528,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. @@ -2174,9 +3537,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; @@ -2232,16 +3607,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). @@ -2262,6 +3708,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; @@ -2282,6 +3730,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(); @@ -2306,12 +3767,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); + 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); @@ -2319,19 +3794,33 @@ void ResourceCache::Insert(const Key& key, std::shared_ptr reso } entries.emplace_front(key, std::move(resources)); index.emplace(key, entries.begin()); - // Entries pin their textures and storage images past the texture caches' budgets, so the bound - // stays modest: APS5_RESOURCE_CACHE_ENTRIES (default 1024) covers several frames of distinct - // dispatch and draw content. - 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) { @@ -2343,12 +3832,39 @@ void ResourceCache::Remove(const Key& key, const ShaderResources* object) { erase(key); } +void ResourceCache::RemoveImage(const StorageTexture* image, std::vector>* evicted) { + std::lock_guard lock(mutex); + for (auto it = entries.begin(); it != entries.end();) { + if (!it->second->ReadsImage(image)) { + ++it; + continue; + } + if (evicted != nullptr) evicted->push_back(std::move(it->second)); + index.erase(it->first); + it = entries.erase(it); + } +} + +void ResourceCache::dropReleasedTextures(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; } @@ -2364,6 +3880,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 { @@ -2371,6 +3902,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; @@ -2383,36 +3935,66 @@ ResourceCache& SharedResourceCache() { return *cache; } -DescriptorCache::DescriptorCache(const Context& context) : context(context), destroyLayout(context.Function("vkDestroyDescriptorSetLayout")), destroyPool(context.Function("vkDestroyDescriptorPool")), freeSets(context.Function("vkFreeDescriptorSets")) {} +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, nullptr); - for (const auto pool : updateAfterBindPools) destroyPool(context.device, pool, nullptr); + 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); } -VkDescriptorSetLayout DescriptorCache::Layout(std::span key, std::span bindings, bool updateAfterBind) { +VkDescriptorSetLayout DescriptorCache::Layout(std::span key, std::span bindings, bool updateAfterBind, std::span flags) { + std::vector defaults; + if (flags.empty()) { + for (const auto& binding : bindings) defaults.push_back(updateAfterBind && binding.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0); + flags = defaults; + } + Require(flags.size() == bindings.size(), "descriptor binding flag count differs from layout"); std::lock_guard lock(mutex); std::vector keyCopy(key.begin(), key.end()); + keyCopy.push_back(static_cast(flags.size())); + keyCopy.insert(keyCopy.end(), flags.begin(), flags.end()); if (const auto found = layouts.find(keyCopy); found != layouts.end()) { ++stats.layoutHits; return found->second; } ValidateDescriptorLayout(context, bindings, updateAfterBind); ++stats.layoutMisses; - const auto layout = CreateSetLayout(context, bindings, updateAfterBind); - layouts.emplace(std::move(keyCopy), layout); + const auto layout = CreateSetLayout(context, bindings, updateAfterBind, flags); + 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)); } } @@ -2423,21 +4005,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; @@ -2449,46 +4047,96 @@ 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, {}}; } - // Newest pool first: it has the most room; a full or fragmented pool is left for its sets to - // drain and tried again later. auto& chain = updateAfterBind ? updateAfterBindPools : pools; - for (auto it = chain.rbegin(); it != chain.rend(); ++it) { - allocation.descriptorPool = *it; + for (std::size_t index = chain.size(); index != 0; --index) { + auto& pool = chain[index - 1]; + if (pool.exhausted) continue; + allocation.descriptorPool = pool.handle; VkDescriptorSet set = VK_NULL_HANDLE; const auto result = allocate(context.device, &allocation, &set); if (result == VK_SUCCESS) { ++stats.sets; - return {set, *it}; + ++pool.liveSets; + 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 = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT | (updateAfterBind ? VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT : 0); + 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); + chain.push_back({pool}); ++stats.pools; allocation.descriptorPool = pool; VkDescriptorSet set = VK_NULL_HANDLE; Check(allocate(context.device, &allocation, &set), "vkAllocateDescriptorSets"); + chain.back().liveSets = 1; ++stats.sets; - return {set, pool}; + 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; } - static_cast(freeSets(context.device, allocation.pool, 1, &allocation.set)); + 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; } DescriptorCache::Stats DescriptorCache::Counters() const { @@ -2496,26 +4144,50 @@ 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) { +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; } Require(byteSize <= context.limits.maxStorageBufferRange, "shader buffer exceeds descriptor range limit"); Require(byteSize <= std::numeric_limits::max(), "shader buffer size exceeds host address space"); - const auto size = static_cast(byteSize); + auto size = static_cast(byteSize); + 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; - if (written) guestMemory.AddWritable(address, size, atomic); + 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); ++readOnlyBuffers; @@ -2592,7 +4264,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()); @@ -2606,7 +4278,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); } } @@ -2668,6 +4340,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); @@ -2696,7 +4393,7 @@ void ShaderResources::addImageBinding(const ShaderRecompiler::DescriptorBinding& const bool sampledHeap = localBinding >= ShaderRecompiler::RuntimeAbi::FirstImageBinding && localBinding < ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding; const bool storageHeap = localBinding >= ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding && localBinding < static_cast(ShaderRecompiler::RuntimeAbi::Binding::Samplers); Require((binding.kind == ShaderRecompiler::DescriptorKind::SampledImage && sampledHeap) || (binding.kind == ShaderRecompiler::DescriptorKind::StorageImage && storageHeap) || (binding.kind == ShaderRecompiler::DescriptorKind::Sampler && heap == ShaderRecompiler::RuntimeAbi::Binding::Samplers), "resource class disagrees with typed descriptor heap"); - Require(binding.kind == ShaderRecompiler::DescriptorKind::Sampler || context.nullDescriptors, "typed image heaps require enabled nullDescriptor"); + Require(binding.kind == ShaderRecompiler::DescriptorKind::Sampler || context.nullDescriptors || context.placeholderImages != nullptr, "typed image heaps require nullDescriptor or placeholder images"); if (binding.kind == ShaderRecompiler::DescriptorKind::StorageImage) { // Storage images are guest textures the shader writes (looked up in stage B). Require(binding.role == ShaderRecompiler::DescriptorRole::GuestImages, "storage image binding has a non-image role"); @@ -2727,21 +4424,29 @@ 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]; - static const bool noSamplerCache = std::getenv("APS5_NO_SAMPLER_CACHE") != nullptr; - if (context.samplerCache != nullptr && !noSamplerCache) { - samplers.push_back(context.samplerCache->Get(context, words, compareEnable, unnormalized, true)); + const bool integerBorder = element < binding.samplerIntegerBorder.size() && binding.samplerIntegerBorder[element]; + if (context.samplerCache != nullptr && !SamplerCacheDisabled()) { + samplers.push_back(context.samplerCache->Get(context, words, compareEnable, unnormalized, true, integerBorder)); } else { - auto resource = DecodeSamplerResource(words, unnormalized, true); + auto resource = DecodeSamplerResource(words, unnormalized, true, integerBorder); resource.compareEnable = compareEnable; samplers.push_back(std::make_shared(context, resource)); } + samplerSources.push_back({{words[0], words[1], words[2], words[3]}, compareEnable, unnormalized, integerBorder}); item.imageAllocations.push_back(samplers.size() - 1); } Require(samplers.size() <= context.limits.maxDescriptorSetSamplers, "pipeline sampler descriptors exceed device limits"); @@ -2791,21 +4496,39 @@ 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))) { + ImageRecord repeated; + repeated.sampled = true; + repeated.decoded = previous.decoded; + repeated.words = previous.words; + repeated.resource = previous.resource; + repeated.guestBytes = previous.guestBytes; + repeated.components = previous.components; + imageRecords.push_back(repeated); + continue; + } + } ImageRecord record; - record.sampled = binding.kind == ShaderRecompiler::DescriptorKind::SampledImage; - if (words[0] != 0u || (words[1] & 0xffu) != 0u) { - record.resource = DecodeTextureResource(words); - record.guestBytes = DescribeSurface(record.resource).guestBytes; + record.sampled = sampled; + 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; record.generation = GuestMemory::CollectWrites(record.resource.baseAddress, static_cast(record.guestBytes)); record.decoded = true; if (record.sampled && !noRecords && words.size() == 8 && (binding.imageDepthCompare.empty() || !binding.imageDepthCompare.at(element))) { std::copy(words.begin(), words.end(), record.words.begin()); - record.components = ViewComponents(record.resource); + record.components = memo->components; record.keys = TextureClearKeys(record.resource, record.guestBytes); auto& cache = Textures(); std::lock_guard lock(cache.mutex); - if (const auto it = findTexture(cache, MakeTextureKey(context.device, words, record.components)); it != cache.entries.end()) { + if (const auto it = findSampledEntry(cache, context.device, record.words, record.components); it != cache.entries.end()) { record.texture = it->texture; record.source = it->source; record.entryKeys = it->keys; @@ -2821,7 +4544,7 @@ bool ShaderResources::precollectImages() { } std::shared_ptr ShaderResources::fastTexture(const ImageRecord& record) { - if (record.texture == nullptr) return nullptr; + if (record.texture == nullptr || record.texture->RefreshedPerUse() || record.texture->IsDepthCopy()) return nullptr; struct Outcome { bool profile; std::chrono::steady_clock::time_point start; @@ -2840,6 +4563,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); + 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; @@ -2859,15 +4584,15 @@ std::shared_ptr ShaderResources::fastTexture(const ImageRecord& record) // Under fast-clear keys the view holds only while its image's results are still pending over // the surface; flushed, the clear the image cannot see makes the lookup take a snapshot. if (keys != DccKeys::Uncompressed && StorageTexture::FindPending(address, record.guestBytes) != record.source) return nullptr; - } else if (keys == DccKeys::Uncompressed && !GuestMemory::UnchangedSince(address, bytes, record.entryGeneration)) { + } else if ((keys == DccKeys::Uncompressed || keys == DccKeys::ClearSingle) && !GuestMemory::UnchangedSince(address, bytes, record.entryGeneration)) { return nullptr; } // The entry must still be the cache's, holding this object (an evicted object is not reused: the // next lookup would make another), and it takes the stage-A generation like a hit would. auto& cache = Textures(); std::lock_guard lock(cache.mutex); - const auto it = findTexture(cache, MakeTextureKey(context.device, record.words, record.components)); - if (it == cache.entries.end() || it->texture != record.texture) return nullptr; + const auto it = findSampledEntry(cache, context.device, record.words, record.components); + if (it == cache.entries.end() || it->texture != record.texture || it->committed.has_value()) return nullptr; if (it->source == nullptr) it->generation = record.generation; touchTexture(cache, it); logLookup({record.texture.get(), record.resource, record.guestBytes, keys, record.source != nullptr ? 0 : record.generation, record.source.get()}); @@ -2875,6 +4600,22 @@ std::shared_ptr ShaderResources::fastTexture(const ImageRecord& record) return record.texture; } +void ShaderResources::applyAnisoOverride() { + for (std::size_t index = 0; index < samplerSources.size(); ++index) { + const auto& source = samplerSources[index]; + const auto variant = SingleLevelSamplerWords(source.words, source.singleLevelImage, source.mipmappedImage); + if (!variant.has_value()) continue; + const auto& words = *variant; + if (context.samplerCache != nullptr && !SamplerCacheDisabled()) { + samplers[index] = context.samplerCache->Get(context, words, source.compareEnable, source.unnormalized, true, source.integerBorder); + } else { + auto resource = DecodeSamplerResource(words, source.unnormalized, true, source.integerBorder); + resource.compareEnable = source.compareEnable; + samplers[index] = std::make_shared(context, resource); + } + } +} + void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBinding& binding, Binding& item, std::span> shaderSamplers) { auto& counters = TextureCounts(); // The element's stage-A record, when the pass ran (records follow the plan order exactly). @@ -2883,33 +4624,66 @@ 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; } const auto resource = record != nullptr && record->decoded ? record->resource : DecodeTextureResource(words); - RequireDegammaFormat(resource.format, 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 forcedSrgb = ForcesDegammaOn(resource.format, binding.imageSamplers[element], shaderSamplers); + RequireNonSeamlessCube(resource.dimension == TextureDimension::kCube, binding.imageSamplers[element], shaderSamplers); + 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; - if (record != nullptr && record->texture != nullptr) { + 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, !binding.imageDepthCompare.empty() && binding.imageDepthCompare.at(element)); + 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); - const bool singleLevel = range.levels == 1u && range.layers == 1u && resource.baseLevel == 0u && EffectiveMinLod(resource) == 0.0f; - if (!singleLevel || (range.type != VK_IMAGE_VIEW_TYPE_1D && range.type != VK_IMAGE_VIEW_TYPE_2D)) throw std::runtime_error("AGC graphics: guest texture sampled with unnormalized coordinates is not a single-level, single-layer 1D or 2D view starting at mip 0, which is not implemented (base level " + std::to_string(resource.baseLevel) + ", levels " + std::to_string(range.levels) + ", layers " + std::to_string(range.layers) + ", view type " + std::to_string(static_cast(range.type)) + ")"); + const bool exactAtBase = element < binding.imageUnnormalizedBaseLevel.size() && binding.imageUnnormalizedBaseLevel[element]; + const bool singleLevel = range.levels == 1u && resource.baseLevel == 0u; + const bool supported = range.layers == 1u && EffectiveMinLod(resource) == 0.0f && (singleLevel || exactAtBase); + if (!supported || (range.type != VK_IMAGE_VIEW_TYPE_1D && range.type != VK_IMAGE_VIEW_TYPE_2D)) throw std::runtime_error("AGC graphics: guest texture sampled with unnormalized coordinates is not a single-layer 1D or 2D view without a minimum LOD clamp, and is neither a single-level view at mip 0 nor read only by image_sample_lz through an S# without minimum LOD or bias, which is not implemented (base level " + std::to_string(resource.baseLevel) + ", levels " + std::to_string(range.levels) + ", layers " + std::to_string(range.layers) + ", view type " + std::to_string(static_cast(range.type)) + ")"); + baseLevel = range.levels > 1u; } RequireFilterMinmax(context, texture->ViewFormat(), binding.imageSamplers[element], shaderSamplers); + RequireBorderSwizzle(resource.bcSwizzle, binding.imageSamplers[element], shaderSamplers); + if (forcedSrgb && !texture->HasSrgbView()) Require(false, "a texture of guest format " + std::to_string(resource.format) + ", which is not sRGB and has no sRGB view, is sampled through a sampler that forces sRGB decoding, which is not implemented"); + if (!shaderSamplers.empty()) { + const bool singleLevel = DescriptorSingleLevel(words); + const auto firstSampler = static_cast(shaderSamplers.data() - samplers.data()); + for (std::uint32_t sampler = 0; sampler < shaderSamplers.size() && sampler < 32u; ++sampler) { + if (((binding.imageSamplers[element] >> sampler) & 1u) == 0) continue; + auto& source = samplerSources.at(firstSampler + sampler); + (singleLevel ? source.singleLevelImage : source.mipmappedImage) = true; + } + } + 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)); textureFirstLayer.push_back(firstLayer); + textureBaseLevel.push_back(baseLevel); + textureSrgb.push_back(forcedSrgb); describedRanges.push_back({"texture", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast(resource.tileMode), resource.dccAddress}); item.imageAllocations.push_back(textures.size() - 1); } @@ -2917,6 +4691,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind reportTextureCounters(); return; } + previousSampled = {}; // Consecutive identical storage descriptors address successive mips of one texture (dynamic-mip // storage writes). std::uint32_t mipOffset = 0; @@ -2926,7 +4701,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; @@ -2934,17 +4709,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}); @@ -2952,15 +4745,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 @@ -2976,28 +5158,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; } @@ -3005,28 +5205,80 @@ 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 (cachePool && context.descriptorCache != nullptr) context.descriptorCache->Free({_set, cachePool}); + 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); - cachePool = VK_NULL_HANDLE; + 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 = {}; pool = VK_NULL_HANDLE; _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; } VkDescriptorSetLayout ShaderResources::Layout() const { return _layout; } +std::vector ShaderResources::LayoutBindings() const { + std::vector result; + for (const auto& binding : bindings) result.push_back(binding.layout); + return result; +} + +bool ShaderResources::LayoutUpdateAfterBind() const { + return updateAfterBind; +} + ShaderResources::DrawBindings::~DrawBindings() { - if (cache != nullptr && allocation.set != VK_NULL_HANDLE) cache->Free(allocation); + 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 { std::vector moved; - if (_set == VK_NULL_HANDLE || usesBda) return moved; + if ((_set == VK_NULL_HANDLE && fragment.set == VK_NULL_HANDLE) || usesBda) return moved; for (const auto& shader : shaders) { if (shader.program == nullptr) return std::nullopt; for (const auto& binding : shader.program->bindings) { @@ -3040,6 +5292,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; } @@ -3065,6 +5318,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)}); } } @@ -3072,22 +5326,117 @@ std::optional> ShaderResources::MovedR return moved; } -std::shared_ptr ShaderResources::PrepareDrawBindings(Recorder& recorder, std::span moved) const { - if (_set == VK_NULL_HANDLE || usesBda) return {}; - const auto reads = guestMemory.InPlaceReads(); +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::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 {}; + 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; @@ -3096,71 +5445,120 @@ 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; } const auto begin = address - item.adjustment; 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); + 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::map counts; - for (const auto& binding : bindings) counts[binding.layout.descriptorType] += binding.layout.descriptorCount; - std::vector sizes; - for (const auto& [type, count] : counts) sizes.push_back({type, count}); - result->cache = context.descriptorCache; - result->allocation = result->cache->Allocate(_layout, sizes); - Require(result->allocation.set != VK_NULL_HANDLE, "draw snapshot descriptor allocation failed"); - std::vector copies; + 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) { - VkCopyDescriptorSet copy{VK_STRUCTURE_TYPE_COPY_DESCRIPTOR_SET}; - copy.srcSet = _set; - copy.srcBinding = binding.layout.binding; - copy.dstSet = result->allocation.set; - copy.dstBinding = binding.layout.binding; - copy.descriptorCount = binding.layout.descriptorCount; - copies.push_back(copy); + 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}); } - const auto update = context.Resolved(&DeviceFunctions::updateDescriptorSets, "vkUpdateDescriptorSets"); - update(context.device, 0, nullptr, static_cast(copies.size()), copies.data()); - std::vector infos; - infos.reserve(selected.size()); - for (const auto& snapshot : result->snapshots) infos.push_back({snapshot.buffer->Handle(), 0, snapshot.buffer->Bytes().size()}); - std::vector writes; + 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 (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 (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"); + } + 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; + auto nextWrite = writes.begin(); for (const auto& binding : bindings) { - for (std::size_t element = 0; element < binding.allocations.size(); ++element) { - const auto found = std::find(selected.begin(), selected.end(), binding.allocations[element]); - if (found == selected.end()) continue; - VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; - write.dstSet = result->allocation.set; - write.dstBinding = binding.layout.binding; - write.dstArrayElement = static_cast(element); - write.descriptorCount = 1; - write.descriptorType = binding.layout.descriptorType; - write.pBufferInfo = &infos[static_cast(found - selected.begin())]; - writes.push_back(write); + 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}; + } } } - update(context.device, static_cast(writes.size()), writes.data(), 0, nullptr); + 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()); return result; } void ShaderResources::Bind(VkCommandBuffer commands, VkPipelineBindPoint bindPoint, VkPipelineLayout layout) const { - if (_set == VK_NULL_HANDLE) return; - context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, bindPoint, layout, 0, 1, &_set, 0, nullptr); + if (_set != VK_NULL_HANDLE) context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, bindPoint, layout, 0, 1, &_set, 0, nullptr); + if (fragment.set != VK_NULL_HANDLE) context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, bindPoint, layout, ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet, 1, &fragment.set, 0, nullptr); } namespace { @@ -3172,21 +5570,38 @@ bool SkipWriteBack() { } void ShaderResources::WriteBack() { - WriteBackBuffers(); - if (SkipWriteBack()) return; - for (std::size_t index = 0; index < storageTextures.size(); ++index) { - if (storageWritten[index]) storageTextures[index]->MarkDirty(); + 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]) storageTextures[index]->MarkDirty(); + if (!storageWritten[index] || storageTextures[index] == nullptr) continue; + storageTextures[index]->MarkDirty(); + static_cast(DepthSurfaceStoreBack(context, *storageTextures[index])); } // Written sub-ranges of buffers the GPU copied out of a host import go back into it by the GPU, // recorded here after the work: those regions then need no CPU write-back (HasCopiedWrites), @@ -3206,17 +5621,24 @@ 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 { + if (bda) bda->CheckFault(); } void ShaderResources::WriteBackBuffers() { - if (bda) bda->CheckFault(); - if (SkipWriteBack()) return; - guestMemory.WriteBack(); -} - -bool ShaderResources::WritesMemory() const { - return HoldsLease() || NeedsCompletion() || !guestMemory.Writes().empty() || std::any_of(storageWritten.begin(), storageWritten.end(), [](bool written) { return written; }); + 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 { @@ -3235,6 +5657,15 @@ std::vector> ShaderResources::StorageImages() const { return images; } +bool ShaderResources::HasCachedStorageImages() const { + if (std::any_of(storageTextures.begin(), storageTextures.end(), [](const auto& image) { return image != nullptr && !image->Cached(); })) return false; + 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 21636daece..40f39f6a74 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" #include #include +#include #include #include #include @@ -19,7 +20,9 @@ struct Decoration { std::optional set; std::optional binding; std::optional stride; + std::optional index; bool block = false; + bool aliased = false; bool patch = false; bool perPrimitive = false; bool perVertex = false; @@ -35,14 +38,21 @@ 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; std::map inputs; std::map outputs; + std::optional secondSource; bool position = false; - bool primitiveIndices = false; + std::uint32_t clipDistances = 0; + std::uint32_t cullDistances = 0; + std::uint32_t primitiveIndices = 0; bool fragmentBarycentric = false; + bool layerOutput = false; + bool meshCapability = false; + std::uint32_t primitiveInputs = 0; std::map> modes; const std::vector& Type(std::uint32_t id) const { @@ -67,17 +77,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; } @@ -102,6 +112,20 @@ struct Module { Require(input == (stage == Stage::TessellationEvaluation), "invalid tessellation level direction"); return; } + if (stage == Stage::Geometry) { + Require(value == spv::BuiltInPosition && Signature(type) == "f32x4" && (input || !position), "unsupported geometry built-in"); + if (!input) position = true; + 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::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"); + count = static_cast(signature[4] - '0'); + Require(clipDistances + cullDistances <= 8, "more than eight clip and cull distances in one shader"); + return; + } if (stage == Stage::TessellationControl || stage == Stage::TessellationEvaluation || stage == Stage::Mesh) { const auto signature = Signature(type); if (!input && value == spv::BuiltInPosition) { @@ -110,26 +134,29 @@ 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"), "unsupported mesh built-in"); + if (!input && value == spv::BuiltInPrimitiveLineIndicesEXT) primitiveIndices = 2u; + if (!input && value == spv::BuiltInPrimitiveTriangleIndicesEXT) primitiveIndices = 3u; + Require((input && ((value == spv::BuiltInWorkgroupId || value == spv::BuiltInLocalInvocationId || value == spv::BuiltInGlobalInvocationId || value == spv::BuiltInNumWorkgroups) && signature == "u32x3")) || (input && value == spv::BuiltInLocalInvocationIndex && signature == "u32") || (!input && value == spv::BuiltInPrimitiveLineIndicesEXT && signature == "u32x2") || (!input && value == spv::BuiltInPrimitiveTriangleIndicesEXT && signature == "u32x3") || (!input && value == spv::BuiltInCullPrimitiveEXT && signature == "bool") || (!input && value == spv::BuiltInLayer && signature == "u32"), "unsupported mesh built-in"); return; } const auto signature = Signature(type); 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; 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::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) { +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; @@ -140,6 +167,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi const bool mesh = stage == Stage::Mesh; const bool control = stage == Stage::TessellationControl; const bool evaluation = stage == Stage::TessellationEvaluation; + const bool geometry = stage == Stage::Geometry; const bool rectListControl = state.rectList && control; if (rectListControl) { Require(shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version, "incompatible rect-list fault ABI version"); @@ -147,7 +175,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi const auto& binding = shader.bindings.front(); Require(binding.role == ShaderRecompiler::DescriptorRole::FaultBuffer && binding.kind == ShaderRecompiler::DescriptorKind::StorageBuffer && binding.descriptorSet == 0 && binding.count == 1 && binding.guestDescriptor.empty() && !binding.readOnly, "invalid rect-list fault buffer contract"); } - const auto model = mesh ? spv::ExecutionModelMeshEXT : control ? spv::ExecutionModelTessellationControl : evaluation ? spv::ExecutionModelTessellationEvaluation : vertex ? spv::ExecutionModelVertex : spv::ExecutionModelFragment; + const auto model = mesh ? spv::ExecutionModelMeshEXT : control ? spv::ExecutionModelTessellationControl : evaluation ? spv::ExecutionModelTessellationEvaluation : geometry ? spv::ExecutionModelGeometry : vertex ? spv::ExecutionModelVertex : spv::ExecutionModelFragment; const auto& words = shader.spirv; Require(words.size() >= 5, "SPIR-V header is truncated: " + std::to_string(words.size()) + " words"); @@ -162,6 +190,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi std::uint32_t entryPoint = 0; std::set executionModeTargets; bool upperLeft = false; + bool terminateExtension = false; for (std::size_t cursor = 5; cursor < words.size();) { const auto count = words[cursor] >> 16u; Require(count != 0 && count <= words.size() - cursor, "truncated SPIR-V instruction"); @@ -178,6 +207,11 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi Require(fragmentShaderBarycentric, "device does not support enabled VK_KHR_fragment_shader_barycentric with fragmentShaderBarycentric"); module.fragmentBarycentric = true; } + const bool isLayerCapability = capability == spv::CapabilityShaderViewportIndexLayerEXT; + if (isLayerCapability) { + Require(viewportIndexLayer, "device does not support enabled VK_EXT_shader_viewport_index_layer"); + module.layerOutput = true; + } VkSubgroupFeatureFlags subgroupOperations = 0; switch (capability) { case spv::CapabilityGroupNonUniform: subgroupOperations = VK_SUBGROUP_FEATURE_BASIC_BIT; break; @@ -206,24 +240,43 @@ 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 = + geometry && + capability == spv::CapabilityGeometry; + const bool isInterlockCapability = fragment && capability == spv::CapabilityFragmentShaderPixelInterlockEXT; + const bool isStencilExportCapability = + fragment && + capability == spv::CapabilityStencilExportEXT; + + 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 + ")"); + } + // Enabled unconditionally or by the device setup in VulkanDevice. const bool isFeatureCapability = capability == spv::CapabilitySampled1D || capability == spv::CapabilityImage1D || capability == spv::CapabilityImageGatherExtended || capability == spv::CapabilityImageQuery || + ((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) || capability == spv::CapabilityStorageImageWriteWithoutFormat || capability == spv::CapabilityStorageImageReadWithoutFormat || capability == spv::CapabilityInt64 || @@ -231,19 +284,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 || @@ -252,11 +308,14 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi isBdaCapability || isTessellationCapability || isMeshCapability || + isGeometryCapability || isInterlockCapability || + isStencilExportCapability || isFeatureCapability || isDescriptorIndexingCapability || ((imageInt64Atomics || bufferInt64Atomics) && capability == spv::CapabilityInt64Atomics) || - (imageInt64Atomics && capability == spv::CapabilityInt64ImageEXT), + (imageInt64Atomics && capability == spv::CapabilityInt64ImageEXT) || + isLayerCapability, std::string("SPIR-V requires unsupported device capability ") + std::to_string(static_cast(capability))); @@ -273,13 +332,27 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi Require(fragment, "SPV_EXT_fragment_shader_interlock requires a fragment shader"); break; } + if (extension == "SPV_EXT_shader_stencil_export") { + Require(fragment, "SPV_EXT_shader_stencil_export requires a fragment shader"); + break; + } if (extension == "SPV_KHR_fragment_shader_barycentric") { Require(fragment && fragmentShaderBarycentric, "SPV_KHR_fragment_shader_barycentric requires enabled fragmentShaderBarycentric in a fragment shader"); break; } - Require(extension == "SPV_KHR_float_controls" || (imageInt64Atomics && extension == "SPV_EXT_shader_image_int64") || (mesh && (extension == "SPV_EXT_mesh_shader" || extension == "SPV_KHR_physical_storage_buffer")) || (descriptorIndexing && extension == "SPV_EXT_descriptor_indexing") || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (extension == "SPV_KHR_physical_storage_buffer" || extension == "SPV_KHR_8bit_storage")), "unsupported SPIR-V extension"); + 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: @@ -305,7 +378,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; @@ -320,13 +393,13 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi Require(count >= 3, "malformed SPIR-V decoration"); auto& decoration = module.decorations[instruction[1]]; const auto kind = static_cast(instruction[2]); - if (kind == spv::DecorationLocation || kind == spv::DecorationBuiltIn || kind == spv::DecorationDescriptorSet || kind == spv::DecorationBinding || kind == spv::DecorationArrayStride) { + if (kind == spv::DecorationLocation || kind == spv::DecorationBuiltIn || kind == spv::DecorationDescriptorSet || kind == spv::DecorationBinding || kind == spv::DecorationArrayStride || kind == spv::DecorationIndex) { Require(count == 4, "malformed SPIR-V literal decoration"); - auto* field = kind == spv::DecorationLocation ? &decoration.location : kind == spv::DecorationBuiltIn ? &decoration.builtin : kind == spv::DecorationDescriptorSet ? &decoration.set : kind == spv::DecorationBinding ? &decoration.binding : &decoration.stride; + auto* field = kind == spv::DecorationLocation ? &decoration.location : kind == spv::DecorationBuiltIn ? &decoration.builtin : kind == spv::DecorationDescriptorSet ? &decoration.set : kind == spv::DecorationBinding ? &decoration.binding : kind == spv::DecorationIndex ? &decoration.index : &decoration.stride; Require(!field->has_value(), "duplicate SPIR-V decoration"); *field = instruction[3]; } - Require(kind != spv::DecorationComponent && kind != spv::DecorationIndex && kind != spv::DecorationStream && kind != spv::DecorationXfbBuffer && kind != spv::DecorationXfbStride, "unsupported shader interface packing or transform feedback"); + Require(kind != spv::DecorationComponent && kind != spv::DecorationStream && kind != spv::DecorationXfbBuffer && kind != spv::DecorationXfbStride, "unsupported shader interface packing or transform feedback"); if (kind == spv::DecorationPatch) decoration.patch = true; if (kind == spv::DecorationPerPrimitiveEXT) decoration.perPrimitive = true; if (kind == spv::DecorationPerVertexKHR) { @@ -334,6 +407,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi decoration.perVertex = true; } if (kind == spv::DecorationBlock) decoration.block = true; + if (kind == spv::DecorationAliased) decoration.aliased = true; break; } case spv::OpMemberDecorate: @@ -343,6 +417,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: @@ -374,6 +449,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; @@ -382,21 +458,37 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi mode(spv::ExecutionModeLocalSize, {paired ? config.threadsPerGroup / 2u : config.threadsPerGroup, 1, 1}); mode(spv::ExecutionModeOutputVertices, {config.maxVertices}); mode(spv::ExecutionModeOutputPrimitivesEXT, {config.maxPrimitives}); - mode(spv::ExecutionModeOutputTrianglesEXT, {}); + mode(config.outputPrimitiveSize == 2u ? spv::ExecutionModeOutputLinesEXT : spv::ExecutionModeOutputTrianglesEXT, {}); Require(module.modes.size() == 4, "unsupported mesh execution mode"); + } else if (barycentricPatches) { + if (control) mode(spv::ExecutionModeOutputVertices, {3u}); + mode(spv::ExecutionModeTriangles, {}); + mode(spv::ExecutionModeSpacingEqual, {}); + mode(spv::ExecutionModeVertexOrderCw, {}); + Require(module.modes.size() == (control ? 4u : 3u), "unsupported generated tessellation execution mode"); } else if (control) { Require(state.rectList || state.stages.tessellation.has_value(), "tessellation configuration is missing"); mode(spv::ExecutionModeOutputVertices, {state.rectList ? 4u : state.stages.tessellation->outputControlPoints}); Require(module.modes.size() == 1, "unsupported tessellation-control execution mode"); } else if (evaluation) { - mode(state.rectList ? spv::ExecutionModeQuads : spv::ExecutionModeTriangles, {}); - mode(state.rectList ? spv::ExecutionModeSpacingEqual : spv::ExecutionModeSpacingFractionalOdd, {}); - mode(spv::ExecutionModeVertexOrderCw, {}); + Require(state.rectList || state.stages.tessellation.has_value(), "tessellation configuration is missing"); + const bool quads = state.rectList || state.stages.tessellation->domain == 2u; + const bool counterClockwise = !state.rectList && state.stages.tessellation->outputTopology == 3u; + mode(quads ? spv::ExecutionModeQuads : spv::ExecutionModeTriangles, {}); + mode(state.rectList || 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) { + mode(spv::ExecutionModeTriangles, {}); + mode(spv::ExecutionModeInvocations, {1u}); + mode(spv::ExecutionModeOutputTriangleStrip, {}); + mode(spv::ExecutionModeOutputVertices, {3u}); + Require(module.modes.size() == 4, "unsupported geometry execution mode"); } else if (fragment) { - for (const auto& [name, operands] : module.modes) Require(operands.empty() && (name == spv::ExecutionModeOriginUpperLeft || name == spv::ExecutionModeEarlyFragmentTests || name == spv::ExecutionModeDepthReplacing || name == spv::ExecutionModeDepthLess || name == spv::ExecutionModeDepthGreater || name == spv::ExecutionModePixelInterlockOrderedEXT), "unsupported fragment execution mode"); + for (const auto& [name, operands] : module.modes) Require(operands.empty() && (name == spv::ExecutionModeOriginUpperLeft || name == spv::ExecutionModeEarlyFragmentTests || name == spv::ExecutionModeDepthReplacing || name == spv::ExecutionModeDepthLess || name == spv::ExecutionModeDepthGreater || name == spv::ExecutionModePixelInterlockOrderedEXT || name == spv::ExecutionModeStencilRefReplacingEXT), "unsupported fragment execution mode"); } else Require(module.modes.empty(), "unsupported vertex execution mode"); std::set> descriptors; + std::map, bool> bufferViews; bool push = false; for (const auto& [id, variable] : module.variables) { if (variable.storage == spv::StorageClassFunction || variable.storage == spv::StorageClassPrivate) continue; @@ -407,7 +499,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi const bool input = variable.storage == spv::StorageClassInput; const bool output = variable.storage == spv::StorageClassOutput; const bool tessellationLevels = decoration.builtin && (*decoration.builtin == spv::BuiltInTessLevelOuter || *decoration.builtin == spv::BuiltInTessLevelInner); - const bool vertexArray = !tessellationLevels && ((control && (input || output)) || (evaluation && input) || (mesh && output)) && !decoration.patch; + const bool vertexArray = !tessellationLevels && ((control && (input || output)) || (evaluation && input) || (geometry && input) || (mesh && output)) && !decoration.patch; const auto& outer = module.Type(typeId); if (decoration.perVertex) { Require(fragment && input && module.fragmentBarycentric && !decoration.patch && !decoration.perPrimitive && !decoration.builtin && decoration.location.has_value(), "PerVertexKHR requires a fragment location input with FragmentBarycentricKHR"); @@ -421,23 +513,32 @@ 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::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 || 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.location) { + if (decoration.index) { + Require(!module.secondSource, "duplicate second dual-source blend color"); + 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 { @@ -449,15 +550,15 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi } } } else if (variable.storage == spv::StorageClassPushConstant) { - Require(!push && (!shader.pushConstants.empty() || mesh) && (type[0] & 0xffffu) == spv::OpTypeStruct, "invalid push constant interface"); + Require(!push && (!shader.pushConstants.empty() || 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}); - Require(offset != module.offsets.end() && offset->second == 0, "push constant member must have offset zero"); + Require(offset != module.offsets.end() && offset->second % PipelinePushSlotBytes == 0 && offset->second < PipelinePushConstantBytes, "push constant member must start at a stage slot"); const auto& array = module.Type(type[2]); Require(array.size() == 4 && (array[0] & 0xffffu) == spv::OpTypeArray && module.Signature(array[2]) == "u32", "push constant member must be an array of u32"); const auto length = module.constants.find(array[3]); - Require(length != module.constants.end() && length->second == PipelinePushConstantBytes / 4, "push constant array must contain 32 elements"); + Require(length != module.constants.end() && length->second == PipelinePushSlotBytes / 4, "push constant array must contain 32 elements"); const auto stride = module.decorations[type[2]].stride; Require(stride.has_value() && *stride == 4, "push constant array must have an ArrayStride of 4"); } else if (variable.storage == spv::StorageClassWorkgroup) { @@ -465,10 +566,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); @@ -477,13 +582,14 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi } else { Require(variable.storage == spv::StorageClassStorageBuffer && decoration.set && decoration.binding, "unsupported or unbound shader resource"); const auto key = std::make_pair(*decoration.set, *decoration.binding); - Require(descriptors.insert(key).second, "duplicate SPIR-V resource binding"); const auto binding = std::find_if(shader.bindings.begin(), shader.bindings.end(), [&](const auto& item) { return item.descriptorSet == key.first && item.binding == key.second; }); + const bool array = binding != shader.bindings.end() && binding->role == ShaderRecompiler::DescriptorRole::GuestBuffers; + const auto [view, first] = bufferViews.emplace(key, decoration.aliased); + Require(descriptors.insert(key).second || (!first && view->second && decoration.aliased && array), "duplicate SPIR-V resource binding"); Require(binding != shader.bindings.end(), "SPIR-V resource is absent from recompiler binding metadata"); Require(binding->kind == ShaderRecompiler::DescriptorKind::StorageBuffer, "SPIR-V descriptor type disagrees with recompiler binding metadata"); Require(!binding->readOnly, "read-only descriptor metadata is unsupported because the recompiler emits no NonWritable decoration"); - const bool array = binding->role == ShaderRecompiler::DescriptorRole::GuestBuffers; - Require(array || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (binding->role == ShaderRecompiler::DescriptorRole::BdaPagetable || binding->role == ShaderRecompiler::DescriptorRole::FaultBuffer)) || binding->role == ShaderRecompiler::DescriptorRole::ShaderData || binding->role == ShaderRecompiler::DescriptorRole::FlattenedSrt || binding->role == ShaderRecompiler::DescriptorRole::Gds, "SPIR-V descriptor role is unsupported"); + 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"); @@ -496,7 +602,10 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi const auto& block = module.Type(blockId); Require(block.size() == 3 && (block[0] & 0xffffu) == spv::OpTypeStruct && module.decorations[blockId].block, "descriptor must be a Block struct with exactly one member"); const auto& words = module.Type(block[2]); - Require(words.size() == 3 && (words[0] & 0xffffu) == spv::OpTypeRuntimeArray && module.Signature(words[2]) == "u32", "descriptor block member must be a runtime array of u32"); + Require(words.size() == 3 && (words[0] & 0xffffu) == spv::OpTypeRuntimeArray, "descriptor block member must be a runtime array"); + const auto& element = module.Type(words[2]); + const bool unsignedInteger = element.size() == 4 && (element[0] & 0xffffu) == spv::OpTypeInt && element[3] == 0u; + Require(unsignedInteger && (element[2] == 32u || (array && decoration.aliased && element[2] == 64u)), "descriptor block member must be a runtime array of u32, or of u64 in an aliased guest buffer view"); const auto member = module.offsets.find({blockId, 0}); Require(member != module.offsets.end() && member->second == 0, "descriptor block member must have offset zero"); } @@ -513,40 +622,82 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi Require(shader.vertexAttributes.empty(), "vertex attribute metadata is invalid for this stage"); } Require(descriptors.size() == shader.bindings.size(), "recompiler binding metadata contains undeclared resources"); - Require(push == (!shader.pushConstants.empty() || mesh), "recompiler push constant metadata disagrees with SPIR-V"); + Require(push == (!shader.pushConstants.empty() || 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 (stage == Stage::Vertex || mesh || evaluation) Require(module.position, "vertex shader does not export position"); + 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 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"); - Require(shaders.size() == (tessellation || state.rectList ? 4u : 2u), "incorrect graphics stage count"); + const bool geometry = state.stages.path == ShaderPath::Vertex && !state.rectList && shaders.size() == 3u; + const bool patches = BarycentricPatches(state, shaders) && shaders.back().program != nullptr && shaders.back().program->barycentricEmulation.active; + Require(geometry || shaders.size() == (tessellation || state.rectList || patches ? 4u : 2u), "incorrect graphics stage count"); + if (patches) Require(state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, "emulated barycentrics through tessellation require a triangle list"); + if (geometry) { + Require(shaders.back().program != nullptr && shaders.back().program->barycentricEmulation.active, "a geometry stage is generated only for emulated barycentrics"); + Require(state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, "emulated barycentrics require triangle primitives"); + } 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] : i == 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 == 0, "graphics resource uses a descriptor set other than zero"); - const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics); + for (const auto& binding : shaders[i].program->bindings) Require(binding.descriptorSet == ShaderRecompiler::RuntimeAbi::DescriptorSet || (shaders[i].stage == ShaderRecompiler::ShaderStage::Fragment && binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet), "graphics resource uses a descriptor set other than its stage's"); + const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics, shaderClipDistance, shaderCullDistance, viewportIndexLayer, runtimeDescriptorArray, demoteToHelperInvocation, shaderTerminateInvocation, primitiveLocations); if (i != 0) { for (const auto& [location, signature] : current.inputs) { const auto output = previous.outputs.find(location); if (output == previous.outputs.end() || output->second != signature) { + std::string exports; + for (const auto value : shaders[i - 1].program->parameterExports) exports += " " + std::to_string(value); + std::string parameters; + for (const auto& parameter : shaders[i].program->fragmentParameters) parameters += " " + std::to_string(parameter.location) + "<-" + std::to_string(parameter.sourceLocation); + std::string outputs; + for (const auto& [index, kind] : previous.outputs) outputs += " " + std::to_string(index) + ":" + kind; Require(false, "graphics interfaces disagree at location " + std::to_string(location) + " (stage " + std::to_string(i - 1) + " output " + (output == previous.outputs.end() ? "missing" : output->second) + - ", stage " + std::to_string(i) + " input " + signature + ")"); + ", stage " + std::to_string(i) + " input " + signature + "; previous outputs:" + outputs + "; parameter exports:" + exports + "; fragment parameters:" + parameters + ")"); } } } @@ -558,9 +709,11 @@ std::set ValidateShaders(std::span shaders, if (location >= attachments) continue; 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; - Require(signature == (uintExport ? "vertex:u32x4" : "vertex:f32x4"), uintExport ? "fragment shader must export uint4 colors to its unsigned integer attachments" : "fragment shader must export float4 colors to its attachments"); + const bool packed = color != state.colors.end() && color->packing != ShaderRecompiler::ColorExportPacking::None; + 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 && 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 6506f69d1a..ce8af4edff 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1,8 +1,11 @@ #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" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libc/include/General.hpp" #include @@ -11,9 +14,20 @@ #include #include #include +#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) { @@ -39,12 +53,37 @@ 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; } +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)); @@ -58,7 +97,7 @@ std::string vteMessage(std::uint32_t viewportControl) { std::string conservativeMessage(std::uint32_t control) { std::ostringstream message; - message << "AGC graphics: PA_SC_CONSERVATIVE_RASTERIZATION_CNTL=0x" << std::hex << control << ": only 0x6000 (off) and 0x6001 (overestimation) are supported"; + message << "AGC graphics: PA_SC_CONSERVATIVE_RASTERIZATION_CNTL=0x" << std::hex << control << ": only off (over- and underestimation disabled, with at most the inner-to-normal overrides, NULL_SQUAD_AA_MASK_ENABLE and the uncertainty-region mode and edge rules set) and 0x6001 (overestimation) are supported"; return message.str(); } @@ -72,7 +111,7 @@ void requireConservativeTriangles(bool triangles, const char* primitive, const c VkConservativeRasterizationModeEXT decodeConservativeRasterization(const QueueState& queue) { const auto& cx = queue.context; const auto control = read(cx, 0x313); - if (control == 0x6000u) return VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT; + if ((control & ~0x001f6000u) == 0) return VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT; if (control != 0x6001u) throw std::runtime_error(conservativeMessage(control)); const auto stages = read(cx, 0x2d5); if ((stages & 0x20u) != 0) { @@ -87,28 +126,77 @@ 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; } // The register rules DecodeState and DrawRejection share (the precheck must reject exactly what // DecodeState would): the masks whose set bits are unsupported, and the depth-control verdict. -// Render target index, viewport index and the misc export vector that carries them are accepted but -// not routed: color targets are single-layer, so layered draws land in layer 0. constexpr std::uint32_t LayerExports = (1u << 18u) | (1u << 19u) | (1u << 21u) | (1u << 24u); +constexpr std::uint32_t ClipCullExports = 0xffffu | (1u << 22u) | (1u << 23u); + +std::uint32_t inactiveVertexPointSize(const QueueState& queue) { + const auto stages = find(queue.context, 0x2d5); + const auto primitive = find(queue.userConfig, 0x242, RegisterBank::UserConfig); + const auto raster = find(queue.context, 0x205); + if (stages == queue.context.end() || primitive == queue.userConfig.end() || raster == queue.context.end()) return 0; + if ((stages->second & 0x24u) != 0) return 0; + if ((raster->second & 8u) != 0 && (raster->second & 0x7e0u) != 0x240u) return 0; + switch (primitive->second) { + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 17: + case 21: + case 19: + return 1u << 16u; + default: + return 0; + } +} + constexpr std::uint32_t DepthControlMask = ~0x007007f0u; // EXEC_ON_HIER_FAIL / EXEC_ON_NOOP / EXEC_IF_OVERLAPPED (bits 9, 10, 17) only force the pixel shader // to run, which it always does here. 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; + +bool alphaToCoverageDisabled(const Registers& cx) { + const auto control = find(cx, 0x203); + return control != cx.end() && (control->second & 0x800u) != 0; +} + +bool ditheredAlphaSamplesSupported(const Registers& cx) { + const auto config = find(cx, 0x2f8); + const auto eqaa = find(cx, 0x201); + return config != cx.end() && (config->second & 0x700007u) == 0x300003u && eqaa != cx.end() && (eqaa->second & 0x207777u) == 0x3303u; +} + +bool ditheredAlphaLocationsSupported(const Registers& cx) { + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + const auto first = find(cx, 0x2fe + pixel * 4u); + const auto second = find(cx, 0x2ff + pixel * 4u); + if (first == cx.end() || second == cx.end() || first->second != 0xbd153fd1u || second->second != 0x9773f95bu) return false; + } + return true; +} constexpr std::uint32_t ScanModeMask = ~0x22u; -constexpr std::uint32_t ScanControlMask = ~0x06023fffu; +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; @@ -116,9 +204,12 @@ 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 | stencilOpValExportEnable); if (zFormat == 9u) mask &= ~maskExportEnable; return mask; } @@ -137,13 +228,21 @@ bool depthSurfaceBound(const Registers& cx) { return (z != cx.end() && (z->second & 3u) != 0) || (stencil != cx.end() && (stencil->second & 1u) != 0); } +std::uint32_t planeTests(const Registers& cx, std::uint32_t depthControl) { + const auto z = find(cx, 0x010); + const auto stencil = find(cx, 0x011); + if (z != cx.end() && (z->second & 3u) == 0) depthControl &= ~2u; + if (stencil != cx.end() && (stencil->second & 1u) == 0) depthControl &= ~1u; + return depthControl & 0xbu; +} + bool depthPlanesAbsent(const Registers& cx) { const auto z = find(cx, 0x010); const auto stencil = find(cx, 0x011); 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) { +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; @@ -154,13 +253,25 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint auto fixed = compare == 0 || compare == 7 ? 0u : face.compareMask; const auto convert = [&](std::uint32_t op) { 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; case 2: case 3: case 4: { + if (testValExport) { + 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) { 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); @@ -189,15 +300,18 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint face.passOp = convert((ops >> 4u) & 0xfu); face.depthFailOp = convert((ops >> 8u) & 0xfu); 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, State& result) { - zero(cx, 0x000, 0x00001f9du, "depth clear, 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, 0x000f100cu, "multisampled, partially resident or mipmapped depth (DB_Z_INFO)"); + zero(cx, 0x010, 0x000f1000u, "partially resident or mipmapped depth (DB_Z_INFO)"); 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; @@ -211,11 +325,28 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result) 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 || !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) { @@ -225,6 +356,11 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result) 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) + static_cast(view & 0x1fffu) * HtileMetadataBytes(depth.extent); + depth.htileStencil = stencil && !depth.htileDepthOnly; + } result.depth = depth; result.depthTest = (depthControl & 2u) != 0; result.depthWrite = result.depthTest && (depthControl & 4u) != 0 && !depthReadOnly; @@ -232,8 +368,14 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result) 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; + result.depthWrite = true; + result.depthCompare = VK_COMPARE_OP_ALWAYS; + result.depthBoundsTest = false; } result.stencilTest = (depthControl & 1u) != 0; if (stencilClear) { @@ -250,8 +392,8 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result) result.stencilBack = clear; } else if (result.stencilTest) { const auto ops = read(cx, 0x10b); - result.stencilFront = stencilFace((depthControl >> 8u) & 7u, ops, read(cx, 0x10c), stencilReadOnly); - result.stencilBack = (depthControl & 0x80u) != 0 ? stencilFace((depthControl >> 20u) & 7u, ops >> 12u, read(cx, 0x10d), stencilReadOnly) : 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; } } @@ -261,15 +403,19 @@ void decodeDepthBias(const Registers& cx, std::uint32_t raster, State& result) { const bool frontBias = (raster & 0x800u) != 0; const bool backBias = (raster & 0x1000u) != 0; if (!(front && frontBias) && !(back && backBias)) return; - if (front && back && (frontBias != backBias || read(cx, 0x2e0) != read(cx, 0x2e2) || read(cx, 0x2e1) != read(cx, 0x2e3))) throw std::runtime_error("AGC graphics: " + zeroMessage(0x205, raster, "depth bias differing between front and back faces")); + const bool perFace = front && back && (frontBias != backBias || read(cx, 0x2e0) != read(cx, 0x2e2) || read(cx, 0x2e1) != read(cx, 0x2e3)); const bool d16 = result.depth->format == VK_FORMAT_D16_UNORM || result.depth->format == VK_FORMAT_D16_UNORM_S8_UINT; const auto format = find(cx, 0x2de) == cx.end() ? (d16 ? 0xf0u : 0x1e9u) : read(cx, 0x2de); if (format != (d16 ? 0xf0u : 0x1e9u)) throw std::runtime_error("AGC graphics: " + zeroMessage(0x2de, format, "depth bias in units other than the depth format")); const auto scale = front && frontBias ? 0x2e0u : 0x2e2u; result.depthBias = true; - result.depthBiasSlope = readFloat(cx, scale) / 16.0f; - result.depthBiasConstant = readFloat(cx, scale + 1u); + result.depthBiasSlope = frontBias || !perFace ? readFloat(cx, scale) / 16.0f : 0.0f; + result.depthBiasConstant = frontBias || !perFace ? readFloat(cx, scale + 1u) : 0.0f; result.depthBiasClamp = readFloat(cx, 0x2df); + if (!perFace) return; + result.depthBiasPerFace = true; + result.backDepthBiasSlope = backBias ? readFloat(cx, 0x2e2) / 16.0f : 0.0f; + result.backDepthBiasConstant = backBias ? readFloat(cx, 0x2e3) : 0.0f; } bool depthPassThrough(std::uint32_t depthControl) { @@ -285,6 +431,170 @@ std::uint32_t effectiveDepthControl(std::uint32_t depthControl) { return (depthControl & 3u) == 0 ? depthControl & ~4u : depthControl; } +constexpr std::array, 2> StandardLocations2{{{4, 4}, {-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 { + const auto it = find(registers, offset, bank); + if (it == registers.end()) return std::nullopt; + return it->second; + }; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + const auto offset = 0x2feu + pixel * 4u; + if (const auto locations = word(cx, offset); locations && (*locations & 0xffu) != 0) return zeroMessage(offset, *locations, "MSAA_ENABLE on a single-sample target whose sample is off the pixel center"); + } + if (const auto alphaToMask = word(cx, 0x2dc); alphaToMask && (*alphaToMask & 1u) != 0 && !(*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"); + 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; + const auto ena = find(queue.context, 0x1b3); + const auto addr = find(queue.context, 0x1b4); + return ena != queue.context.end() && addr != queue.context.end() && (ena->second & addr->second & CentroidInputs) == 0; +} + +std::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"); + const auto log2 = config & 7u; + if (log2 == 0) { + Require(((config >> 20u) & 7u) == 0, "exposed samples without multisampling are unsupported"); + return 1; + } + Require(log2 <= 3u, "more than eight samples are unsupported"); + Require(((config >> 20u) & 7u) == log2, "exposing fewer samples than are rasterized is unsupported"); + return 1u << log2; +} + +void decodeSampleLocations(const Registers& cx, State& state) { + const auto samples = state.samples; + if (samples == 1) return; + const std::span> standard = samples == 2 ? std::span>(StandardLocations2) : samples == 4 ? std::span>(StandardLocations4) : std::span>(StandardLocations8); + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + for (std::uint32_t sample = 0; sample < samples; ++sample) { + const auto word = read(cx, 0x2fe + pixel * 4u + sample / 4u); + 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; + 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); @@ -312,12 +622,37 @@ VkBlendFactor blendFactor(std::uint32_t value) { case 10: return VK_BLEND_FACTOR_SRC_ALPHA_SATURATE; case 13: return VK_BLEND_FACTOR_CONSTANT_COLOR; case 14: return VK_BLEND_FACTOR_ONE_MINUS_CONSTANT_COLOR; + case 15: return VK_BLEND_FACTOR_SRC1_COLOR; + case 16: return VK_BLEND_FACTOR_ONE_MINUS_SRC1_COLOR; + case 17: return VK_BLEND_FACTOR_SRC1_ALPHA; + case 18: return VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA; case 19: return VK_BLEND_FACTOR_CONSTANT_ALPHA; case 20: return VK_BLEND_FACTOR_ONE_MINUS_CONSTANT_ALPHA; default: throw std::runtime_error("AGC graphics: unsupported blend factor " + std::to_string(value)); } } +VkBlendFactor colorFactorWithOpaqueDestination(VkBlendFactor factor) { + switch (factor) { + case VK_BLEND_FACTOR_DST_ALPHA: return VK_BLEND_FACTOR_ONE; + case VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA: return VK_BLEND_FACTOR_ZERO; + case VK_BLEND_FACTOR_SRC_ALPHA_SATURATE: return VK_BLEND_FACTOR_ZERO; + default: return factor; + } +} + +VkBlendFactor alphaFactorWithOpaqueDestination(VkBlendFactor factor) { + switch (factor) { + case VK_BLEND_FACTOR_DST_ALPHA: case VK_BLEND_FACTOR_DST_COLOR: return VK_BLEND_FACTOR_ONE; + case VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA: case VK_BLEND_FACTOR_ONE_MINUS_DST_COLOR: return VK_BLEND_FACTOR_ZERO; + default: return factor; + } +} + +bool secondSource(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; +} + VkBlendOp blendOp(std::uint32_t value) { switch (value) { case 0: return VK_BLEND_OP_ADD; @@ -329,12 +664,153 @@ 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; std::uint8_t componentMapping = 0xe4u; + 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) { @@ -345,7 +821,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); @@ -359,11 +835,14 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, if (number == floating) return single(VK_FORMAT_R16_SFLOAT, 2); if (number == uint) return single(VK_FORMAT_R16_UINT, 2); return fail(); - case 3: - if (swap != 0) return fail(); - if (number == unorm) return {VK_FORMAT_R8G8_UNORM, 2}; - if (number == snorm) return {VK_FORMAT_R8G8_SNORM, 2}; + case 3: { + if (swap > 1) return fail(); + 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: if (number == floating) return single(VK_FORMAT_R32_SFLOAT, 4); if (number == uint) return single(VK_FORMAT_R32_UINT, 4); @@ -378,8 +857,12 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, return fail(); case 6: // COLOR_10_11_11: red in the low 11 bits, the Vulkan B10G11R11 packing. - if (swap != 0 || number != floating) return fail(); + if (swap != 0 || (number != floating && number != unorm)) return fail(); + if (number == unorm) return {VK_FORMAT_R32_UINT, 4, 0xe4u, ShaderRecompiler::ColorExportPacking::Unorm10_11_11}; return {VK_FORMAT_B10G11R11_UFLOAT_PACK32, 4}; + case 7: + if (swap != 2 || number != floating) return fail(); + return {VK_FORMAT_R32_UINT, 4, 0xc6u, ShaderRecompiler::ColorExportPacking::Float11_11_10}; case 9: // COLOR_2_10_10_10 keeps red in the low bits, the Vulkan A2B10G10R10 packing. if (number != unorm) return fail(); @@ -389,6 +872,7 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, if (number == unorm) return {alternate ? VK_FORMAT_B8G8R8A8_UNORM : VK_FORMAT_R8G8B8A8_UNORM, 4, reversed}; if (number == snorm) return {alternate ? VK_FORMAT_B8G8R8A8_SNORM : VK_FORMAT_R8G8B8A8_SNORM, 4, reversed}; if (number == srgb) return {alternate ? VK_FORMAT_B8G8R8A8_SRGB : VK_FORMAT_R8G8B8A8_SRGB, 4, reversed}; + if (number == uint && !alternate) return {VK_FORMAT_R8G8B8A8_UINT, 4, reversed}; return fail(); } case 11: @@ -397,8 +881,7 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, if (number == uint) return {VK_FORMAT_R32G32_UINT, 8}; return fail(); case 12: { - if (swap == 1) return fail(); - const auto reversed = static_cast(swap == 2 ? 0x1bu : swap == 3 ? 0x93u : 0xe4u); + const auto reversed = static_cast(alternate ? 0xc6u : swap == 2 ? 0x1bu : swap == 3 ? 0x93u : 0xe4u); if (number == floating) return {VK_FORMAT_R16G16B16A16_SFLOAT, 8, reversed}; if (number == unorm) return {VK_FORMAT_R16G16B16A16_UNORM, 8, reversed}; if (number == snorm) return {VK_FORMAT_R16G16B16A16_SNORM, 8, reversed}; @@ -410,6 +893,12 @@ 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}; default: return fail(); } @@ -423,7 +912,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); @@ -434,7 +922,99 @@ void intersect(VkRect2D& result, const Registers& registers, std::uint32_t offse } -ShaderStages DecodeShaderStages(const QueueState& queue) { +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{}; + 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}; + } + } + 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; +} + +ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) { const auto value = read(queue.context, 0x2d5); const auto validate = [&](bool condition, const char* reason) { if (condition) return; @@ -446,7 +1026,8 @@ ShaderStages DecodeShaderStages(const QueueState& queue) { validate((value & 3u) != 3u && ((value >> 3u) & 3u) != 3u && ((value >> 6u) & 3u) != 3u, "reserved LS_EN, ES_EN or VS_EN encoding"); const auto primitive = read(queue.userConfig, 0x242, RegisterBank::UserConfig); const bool tessellation = primitive == 9; - const bool geometry = (value & 0x20u) != 0; + validate(!vertexWorkgroup || ((value & 0x02002020u) == 0x2000u && !tessellation), "a vertex program that needs a workgroup requires PRIMGEN_EN without GS_EN, PRIMGEN_PASSTHRU_EN or tessellation"); + const bool geometry = (value & 0x20u) != 0 || vertexWorkgroup; validate(tessellation == ((value & 4u) != 0), "Patch topology and HS_EN disagree"); validate(!tessellation || !geometry, "combined tessellation and geometry is unsupported by the reference path"); const auto path = tessellation ? ShaderPath::Tessellation : geometry ? ShaderPath::Geometry : ShaderPath::Vertex; @@ -454,56 +1035,162 @@ ShaderStages DecodeShaderStages(const QueueState& queue) { 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 & ~0x0007ed0du) == 0 && (value & 3u) == 1u && ((value >> 3u) & 3u) == 1u, "unsupported tessellation routing"); + validate((value & ~0x0207ed0du) == 0 && (value & 0x02002000u) == 0x02002000u && (value & 3u) == 1u && ((value >> 3u) & 3u) == 1u, "unsupported tessellation routing"); const auto config = read(queue.context, 0x2d6); const auto parameters = read(queue.context, 0x2db); - ShaderRecompiler::TessellationConfiguration tess{(config >> 8u) & 0x3fu, (config >> 14u) & 0x3fu, parameters & 3u, (parameters >> 2u) & 3u, (parameters >> 5u) & 3u}; + const auto primitiveGroup = find(queue.userConfig, 0x25b, RegisterBank::UserConfig); + ShaderRecompiler::TessellationConfiguration tess{(config >> 8u) & 0x3fu, (config >> 14u) & 0x3fu, parameters & 3u, (parameters >> 2u) & 7u, (parameters >> 5u) & 7u, config & 0xffu, primitiveGroup == queue.userConfig.end() ? 0u : primitiveGroup->second & 0x1ffu, primitiveGroup != queue.userConfig.end() && (primitiveGroup->second & 0x40000u) != 0u}; validate(tess.inputControlPoints != 0 && tess.inputControlPoints <= 32 && tess.outputControlPoints != 0 && tess.outputControlPoints <= 32, "invalid tessellation control-point counts"); - validate(tess.domain == 1 && tess.partitioning == 2 && tess.outputTopology == 2, "only triangular, fractional-odd, clockwise tessellation is supported by the reference path"); + 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")); + 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"); result.tessellation = tess; } else { - validate((value & ~0x0047ec30u) == 0, "unsupported geometry routing, fast launch or wave-ID state"); + const bool fastLaunch = ((value >> 19u) & 3u) == 1u; + validate((value & ~(fastLaunch ? 0x004fec30u : 0x0047ec30u)) == 0, "unsupported geometry routing, fast launch or wave-ID state"); const auto group = read(queue.userConfig, 0x25b, RegisterBank::UserConfig); const auto vertices = (group >> 9u) & 0x1ffu; const auto primitives = group & 0x1ffu; + validate(!fastLaunch || (!vertexWorkgroup && primitive == 1 && vertices == 1 && primitives == 1), "fast launch other than one point per subgroup is unsupported"); const auto maxVertices = read(queue.context, 0x1ff); - const auto verticesPerPrimitive = 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 verticesPerPrimitive = vertexWorkgroup ? 3u : read(queue.context, 0x2ce); + const auto outputType = read(queue.context, 0x29b); + validate(outputType == 2 || (outputType == 1 && !vertexWorkgroup), "VGT_GS_OUT_PRIM_TYPE other than a triangle strip, or a line strip from a geometry program, is unsupported"); + const auto outputSize = outputType == 1 ? 2u : 3u; + validate((primitive == 1 || primitive == 2 || primitive == 4 || primitive == 5 || primitive == 6) && verticesPerPrimitive >= outputSize, "unsupported geometry input or output assembly"); const auto inputSize = primitive == 1 ? 1u : primitive == 2 ? 2u : 3u; - validate(vertices >= inputSize && maxVertices != 0 && maxVertices <= 256 && verticesPerPrimitive <= 256, "invalid geometry subgroup output"); + 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; - const auto groupPrimitives = std::min({primitives, (vertices - inputSize) / inputStep + 1u, maxVertices / verticesPerPrimitive}); + const auto outputLimit = vertexWorkgroup ? (maxVertices - inputSize) / inputStep + 1u : maxVertices / verticesPerPrimitive; + 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(((read(queue.shader, 0x8a, RegisterBank::Shader) >> 29u) & 3u) == 3 && ((resources >> 16u) & 3u) == 3, "unsupported geometry VGPR allocation"); const auto esgsItemSize = read(queue.context, 0x2ab); validate(esgsItemSize != 0 && esgsItemSize * vertices <= 0xffffu, "invalid VGT_ESGS_RING_ITEMSIZE"); - const auto threads = std::max({(groupPrimitives - 1u) * inputStep + inputSize, primitives, maxVertices, primitives * (verticesPerPrimitive - 2u)}); - result.mesh = ShaderRecompiler::MeshConfiguration{primitive, groupPrimitives, (groupPrimitives - 1u) * inputStep + inputSize, maxVertices, primitives * (verticesPerPrimitive - 2u), ((threads + result.vertexWaveSize - 1u) / result.vertexWaveSize) * result.vertexWaveSize, ((resources >> 19u) & 0xffu) * 128u, 0, esgsItemSize}; + const auto amplification = fastLaunch ? read(queue.context, 0x2d3) & 0x1ffu : verticesPerPrimitive - outputSize + 1u; + validate(amplification != 0, "fast launch subgroup amplifies to no primitives"); + 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}); + 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; } +bool DepthResummarizeDrawsNothing(const QueueState& queue) { + const auto& cx = queue.context; + const auto control = find(cx, 0x000); + const auto depthControl = find(cx, 0x200); + const auto zInfo = find(cx, 0x010); + const auto stencilInfo = find(cx, 0x011); + const auto targetMask = find(cx, 0x8e); + const auto shaderMask = find(cx, 0x8f); + if (control == cx.end() || control->second != 0x10u || depthControl == cx.end() || zInfo == cx.end() || stencilInfo == cx.end() || targetMask == cx.end() || shaderMask == cx.end()) return false; + const auto depth = depthControl->second; + const auto format = zInfo->second & 3u; + return (depth & 2u) != 0 && (depth & 0xc000000cu) == 0 && depthPassThrough(depth) && (format == 1u || format == 3u) && (stencilInfo->second & 1u) == 0 && ColorWriteMask(cx) == 0 && PixelProgramSkipped(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 & ~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"); } -State DecodeState(const QueueState& queue) { +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; State result{}; result.conservativeRasterization = decodeConservativeRasterization(queue); - result.stages = DecodeShaderStages(queue); + result.stages = DecodeShaderStages(queue, 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 2: result.topology = VK_PRIMITIVE_TOPOLOGY_LINE_LIST; break; + case 3: result.topology = VK_PRIMITIVE_TOPOLOGY_LINE_STRIP; break; case 7: case 17: Require(result.stages.path == ShaderPath::Vertex, "rect-list requires vertex routing"); @@ -512,27 +1199,34 @@ State DecodeState(const QueueState& queue) { 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; - if ((read(cx, 0x207) & LayerExports) != 0) { - static bool reported = false; - if (!reported) { - reported = true; - std::fprintf(stderr, "[gpu] layer/viewport index vertex exports are ignored (PA_CL_VS_OUT_CNTL=0x%08x)\n", read(cx, 0x207)); - } - } - zero(cx, 0x207, ~LayerExports, "clip distances, layer, viewport or auxiliary vertex exports"); + 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 | inactiveVertexPointSize(queue)), "layer, viewport or auxiliary vertex exports"); + Require(std::popcount(read(cx, 0x207) & 0xffffu) <= 8, "more than eight clip and cull distances are unsupported"); + result.paClVsOutCntl = read(cx, 0x207); + result.samples = decodeSamples(cx); + Require(coveredCentroidSupported(queue), "COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs is not implemented"); + decodeSampleLocations(cx, result); { const auto depthControl = read(cx, 0x200); const auto renderControl = find(cx, 0x000); - const bool stencilClear = renderControl != cx.end() && (renderControl->second & 2u) != 0; - if (stencilClear || ((depthControl & 0xbu) != 0 && depthSurfaceBound(cx))) { - decodeDepth(cx, depthControl, result); - } else if (depthPassThrough(depthControl) || ((depthControl & 3u) != 0 && depthPlanesAbsent(cx))) { + 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), (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) { reported = true; @@ -547,20 +1241,64 @@ State DecodeState(const QueueState& queue) { } else { if ((effectiveDepthControl(depthControl) & DepthControlMask) != 0) zero(cx, 0x200, DepthControlMask, "depth, stencil or conditional color writes"); } - Require((depthControl & 8u) == 0 || result.depth.has_value(), "depth bounds without a depth surface"); + Require((depthControl & 8u) == 0 || result.depth.has_value() || depthPlanesAbsent(cx), "depth bounds without a depth surface"); Require((depthControl & 0xc0000000u) == 0, "depth-conditional color writes are unsupported"); } zero(cx, 0x203, shaderControlMask(read(cx, 0x1c4)), "depth export, shader coverage or ordered fragment execution"); - zero(cx, 0x2dc, AlphaToCoverageMask, "alpha-to-coverage"); - zero(cx, 0x2f8, ~0u, "multisampling or coverage conversion"); - zero(cx, 0x292, ScanModeMask, "scan conversion mode"); - zero(cx, 0x293, ScanControlMask, "sample iteration, primitive discard or out-of-order rasterization"); + const auto alpha = read(cx, 0x2dc); + const bool alphaDisabled = alpha == AlphaToCoverageDithered && alphaToCoverageDisabled(cx); + result.alphaToCoverage = alpha == AlphaToCoverageUndithered || (!alphaDisabled && alpha == AlphaToCoverageDithered); + result.ditheredAlphaToCoverage = result.alphaToCoverage && alpha == AlphaToCoverageDithered; + if (!result.alphaToCoverage) zero(cx, 0x2dc, alphaDisabled ? AlphaToCoverageMask & ~1u : AlphaToCoverageMask, "dithered or rounded alpha-to-coverage"); + if (result.ditheredAlphaToCoverage) { + Require(ditheredAlphaSamplesSupported(cx), "dithered alpha-to-coverage requires eight matched raster/DB_EQAA samples and one pixel iteration"); + Require(!result.customSampleLocations, "dithered alpha-to-coverage with custom sample locations is unsupported"); + } + const bool depthKeepsSampleZero = result.depth && result.depth->samples == 1 && result.samples > 1; + zero(cx, 0x292, ScanModeMask & ~1u, "scan conversion mode"); + 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) || ((read(cx, 0x293) & PixelShaderIteratesSamples) == 0 && 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, PS_ITER_SAMPLE, 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) { + const bool filled = result.topology != VK_PRIMITIVE_TOPOLOGY_POINT_LIST && result.topology != VK_PRIMITIVE_TOPOLOGY_LINE_LIST && result.topology != VK_PRIMITIVE_TOPOLOGY_LINE_STRIP; + Require(filled && !result.alphaToCoverage, "multisampled targets rasterized without MSAA_ENABLE are unsupported for points, lines 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, "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"); @@ -569,12 +1307,15 @@ State DecodeState(const QueueState& queue) { 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 auto rasterMode = raster & ~0x7u & ~(1u << 24u) & ~0x1800u; - if (rasterMode != 0 && rasterMode != 0x240u && rasterMode != 0x248u) throw std::runtime_error("AGC graphics: " + zeroMessage(0x205, raster, "polygon mode, depth bias, provoking vertex or nonstandard rasterization")); + 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")); 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; 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; APS5_LOG_OUT_DEBUG("Raster=0x%x cullMode=0x%x frontFace=%u negativeOneToOne=%u", raster, static_cast(result.cullMode), static_cast(result.frontFace), result.negativeOneToOne ? 1u : 0u); const auto shaderMask = read(cx, 0x8f); // Channels of targets the pixel shader does not export are never written, so the target mask only @@ -612,6 +1353,22 @@ State DecodeState(const QueueState& queue) { 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); + } + 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); + Require((color.samples == result.samples && !result.mergedCoverage) || merged, "a color target's sample count differs from the rasterizer's"); + result.mergedCoverage = merged; 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; @@ -622,15 +1379,29 @@ State DecodeState(const QueueState& queue) { } if (result.hasColorTarget) { result.color = result.colors.front(); + result.renderLayers = result.color.layers; + for (const auto& color : result.colors) Require(color.layers == result.renderLayers, "color targets with different layer counts are unsupported"); + if (result.renderLayers > 1) { + Require(!result.depth, "layered rendering with a depth target is unsupported"); + result.layerExports = read(cx, 0x207) & (1u << 18u); + } if (result.depth) result.renderExtent = {std::min(result.renderExtent.width, result.depth->extent.width), std::min(result.renderExtent.height, result.depth->extent.height)}; } else if (result.depth) { result.renderExtent = result.depth->extent; } 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) { + reported = true; + std::fprintf(stderr, "[gpu] layer/viewport index vertex exports are ignored (PA_CL_VS_OUT_CNTL=0x%08x)\n", read(cx, 0x207)); + } } const auto xs = readFloat(cx, 0x10f); const auto xo = readFloat(cx, 0x110); @@ -641,21 +1412,35 @@ State DecodeState(const QueueState& queue) { const auto minDepth = result.negativeOneToOne ? zo - zs : zo; const auto maxDepth = zo + zs; APS5_LOG_OUT_DEBUG("Viewport transform scale=(%f,%f,%f) offset=(%f,%f,%f) depth=(%f,%f)", xs, ys, zs, xo, yo, zo, minDepth, maxDepth); - if (!(xs > 0 && ys != 0 && std::isfinite(minDepth) && std::isfinite(maxDepth))) { + const bool areaFree = result.stages.path == ShaderPath::Vertex && (result.rectList || result.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || result.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN || result.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP); + const bool collapsed = (xs == 0 || ys == 0) && xs >= 0 && areaFree && std::isfinite(xo) && std::isfinite(yo) && std::isfinite(minDepth) && std::isfinite(maxDepth); + if (!collapsed && !(xs > 0 && ys != 0 && std::isfinite(minDepth) && std::isfinite(maxDepth))) { std::ostringstream message; message << "AGC graphics: unsupported viewport transform: scale=(" << xs << ", " << ys << ", " << zs << "), offset=(" << xo << ", " << yo << ", " << zo << "), depth=(" << minDepth << ", " << maxDepth << "), negativeOneToOne=" << result.negativeOneToOne; throw std::runtime_error(message.str()); } Require(readFloat(cx, 0xb4) <= readFloat(cx, 0xb5), "inverted viewport depth clamp bounds"); - result.viewport = {xo - xs, yo - ys, 2 * xs, 2 * ys, minDepth, maxDepth}; + 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; + result.viewport.maxDepth = result.depth->clearDepth; + result.depthBias = false; + } APS5_LOG_OUT_DEBUG("Viewport x=%f y=%f w=%f h=%f minDepth=%f maxDepth=%f", result.viewport.x, result.viewport.y, result.viewport.width, result.viewport.height, result.viewport.minDepth, result.viewport.maxDepth); result.scissor = {{0, 0}, result.renderExtent}; intersect(result.scissor, cx, 0xc, true); intersect(result.scissor, cx, 0x81, false); intersect(result.scissor, cx, 0x90, false); if ((read(cx, 0x292) & 2u) != 0) intersect(result.scissor, cx, 0x94, false); + if (collapsed) result.scissor.extent = {0, 0}; APS5_LOG_OUT_DEBUG("Scissor offset=(%d,%d) extent=%ux%u", result.scissor.offset.x, result.scissor.offset.y, result.scissor.extent.width, result.scissor.extent.height); result.blends.assign(exportCount, VkPipelineColorBlendAttachmentState{}); + std::array requiredBlendConstants{}; + std::uint32_t requiredBlendMask = 0; for (const auto& color : result.colors) { const auto slot = color.slot; const auto blend = read(cx, 0x1e0 + slot); @@ -667,32 +1452,156 @@ State DecodeState(const QueueState& queue) { for (std::uint32_t component = 0; component < 4; ++component) { if (((exportedMask >> ((mapping >> (2u * component)) & 3u)) & 1u) != 0) state.colorWriteMask |= 1u << component; } - state.blendEnable = (blend >> 30u) & 1u; - 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"); + state.blendEnable = ((blend >> 30u) & 1u) != 0 && (read(cx, 0x31c + slot * 0xfu) & 0x10000u) == 0; Require(!state.blendEnable || !color.uintExport, "blending into an unsigned integer target is unsupported"); + const bool reversed = mapping == 0x1bu || mapping == 0x93u; if (state.blendEnable) { - Require((read(cx, 0x31c + slot * 0xfu) & 0x10000u) == 0, "blend bypass conflicts with enabled blending"); - state.srcColorBlendFactor = blendFactor(blend & 0x1fu); - state.dstColorBlendFactor = blendFactor((blend >> 8u) & 0x1fu); - state.colorBlendOp = blendOp((blend >> 5u) & 7u); - 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; + 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; }; + if ((mapping == 0xe4u || mapping == 0xc6u) && state.colorBlendOp != VK_BLEND_OP_MIN && state.colorBlendOp != VK_BLEND_OP_MAX && (constantColor(state.srcColorBlendFactor) || constantColor(state.dstColorBlendFactor))) { + for (std::uint32_t i = 0; i < 3; ++i) { + if ((state.colorWriteMask & (1u << i)) == 0) continue; + const auto value = result.blendConstants[(mapping >> (2u * i)) & 3u]; + Require((requiredBlendMask & (1u << i)) == 0 || requiredBlendConstants[i] == value, "color targets require conflicting blend constants after component swap"); + requiredBlendConstants[i] = value; + requiredBlendMask |= 1u << i; + } + } + if (mapping == 0xecu) { + const auto destinationAlpha = [](VkBlendFactor factor) { return factor == VK_BLEND_FACTOR_DST_ALPHA || factor == VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA || factor == VK_BLEND_FACTOR_SRC_ALPHA_SATURATE; }; + if (destinationAlpha(state.srcColorBlendFactor) || destinationAlpha(state.dstColorBlendFactor) || destinationAlpha(state.srcAlphaBlendFactor) || destinationAlpha(state.dstAlphaBlendFactor)) throw std::runtime_error("AGC graphics: blending into an 8_8 color target with the alternate component order with a factor of the destination alpha, which the target keeps in its second channel, is not implemented"); + const auto weighted = [](VkBlendOp operation) { return operation != VK_BLEND_OP_MIN && operation != VK_BLEND_OP_MAX; }; + if ((weighted(state.colorBlendOp) && (constantColor(state.srcColorBlendFactor) || constantColor(state.dstColorBlendFactor))) || (weighted(state.alphaBlendOp) && (constantColor(state.srcAlphaBlendFactor) || constantColor(state.dstAlphaBlendFactor)))) throw std::runtime_error("AGC graphics: blending into an 8_8 color target with the alternate component order with a constant color factor, which Vulkan applies to the second channel with the green blend constant instead of the alpha one, is not implemented"); + } + } + 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 " + packed + " target is unsupported"); + Require(!state.blendEnable, "blending into " + packed + " color target is unsupported"); + Require((exportedMask & 7u) == 7u, "partial writes of " + packed + " color target are unsupported"); + } + if (state.blendEnable && !reversed && (secondSource(state.srcColorBlendFactor) || secondSource(state.dstColorBlendFactor) || secondSource(state.srcAlphaBlendFactor) || secondSource(state.dstAlphaBlendFactor))) { + const auto addition = [](VkBlendOp op) { return op == VK_BLEND_OP_ADD || op == VK_BLEND_OP_SUBTRACT || op == VK_BLEND_OP_REVERSE_SUBTRACT; }; + Require(addition(state.colorBlendOp) && addition(state.alphaBlendOp), "dual-source blending with a MIN or MAX operation is unsupported"); + result.dualSourceBlend = true; + } + if (const auto info = read(cx, 0x31c + slot * 0xfu); state.blendEnable && (info & 0x8000u) == 0 && ((info >> 8u) & 7u) <= 1u) { + char detail[80]; + std::snprintf(detail, sizeof(detail), " (CB_COLOR_INFO 0x%08x, CB_BLEND%u_CONTROL 0x%08x)", info, slot, blend); + throw std::runtime_error(std::string("AGC graphics: blending unclamped normalized color is not implemented") + detail); } result.blends[color.exportIndex] = state; } + for (std::uint32_t i = 0; i < 3; ++i) { + 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"); + Require(((exportFormat >> 4u) & 0xfu) == (exportFormat & 0xfu), "dual-source blending needs the MRT1 export in the MRT0 export format"); + Require(((shaderMask >> 4u) & 0xfu) == (shaderMask & 0xfu), "dual-source blending needs the MRT1 export with the MRT0 shader mask"); + } + if (result.alphaToCoverage) { + const auto mrt0 = std::find_if(result.colors.begin(), result.colors.end(), [](const ColorTarget& color) { return color.exportIndex == 0; }); + const auto format = read(cx, 0x1c5) & 0xfu; + Require(format == 3u || format == 4u || format == 5u || format == 6u || format == 9u, "alpha-to-coverage needs an MRT0 export format with a floating-point alpha"); + Require(!result.ditheredAlphaToCoverage || format == 4u || format == 9u, "dithered alpha-to-coverage needs an FP16_ABGR or 32_ABGR export"); + Require(mrt0 == result.colors.end() || (mrt0->componentMapping == 0xe4u && mrt0->packing == ShaderRecompiler::ColorExportPacking::None), "alpha-to-coverage into a component-swapped or packed MRT0 target is unsupported"); + } APS5_LOG_OUT_DEBUG("DecodeState done colorTarget=%u render=%ux%u topology=%u", result.hasColorTarget ? 1u : 0u, result.renderExtent.width, result.renderExtent.height, static_cast(result.topology)); return result; } +std::array ExportPackings(const State& state) { + std::array packings{}; + for (const auto& color : state.colors) { + if (color.exportIndex < packings.size()) packings[color.exportIndex] = color.packing; + } + 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 == 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; + case VK_PRIMITIVE_TOPOLOGY_LINE_LIST: + case VK_PRIMITIVE_TOPOLOGY_LINE_STRIP: return ShaderRecompiler::RasterPrimitive::Line; + default: return ShaderRecompiler::RasterPrimitive::Triangle; + } +} + std::array ExportMappings(const State& state) { std::array mappings{}; mappings.fill(0xe4u); @@ -703,10 +1612,7 @@ std::array ExportMappings(const State& state) { } std::size_t CmaskBytes(std::uint32_t width, std::uint32_t height) { - constexpr std::size_t metablockWidth = 1024; - constexpr std::size_t metablockHeight = 512; - constexpr std::size_t metablockBytes = 4096; - return ((width + metablockWidth - 1) / metablockWidth) * ((height + metablockHeight - 1) / metablockHeight) * metablockBytes; + return CmaskLayout(width, height).Bytes(); } std::uint32_t ColorWriteMask(const Registers& context) { @@ -720,7 +1626,43 @@ std::uint32_t ColorWriteMask(const Registers& context) { return mask; } -ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { +namespace { + +std::uint32_t GuestFormatFor(VkFormat format, std::uint32_t elementBytes) { + if (const auto guest = FindGuestColorTargetFormat(format, elementBytes)) return *guest; + throw std::runtime_error("AGC graphics: no guest texture format matches the color buffer format " + std::to_string(static_cast(format))); +} + +} + +GuestTextureResource SurfaceForTarget(const ColorTarget& color) { + 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; + surface.pipeBankXor = color.pipeBankXor; + surface.width = chain ? color.surfaceExtent.width : color.extent.width; + surface.height = chain ? color.surfaceExtent.height : color.extent.height; + surface.depthOrLastArray = color.depth - 1u; + surface.baseArray = 0; + surface.mipCount = color.mipCount; + surface.baseLevel = 0; + surface.lastLevel = color.mipCount - 1; + surface.tileMode = ColorTextureTileMode(color.tileMode); + surface.dimension = color.depth > 1 ? TextureDimension::k3D : TextureDimension::k2D; + surface.format = GuestFormatFor(color.format, color.elementBytes); + surface.dstSelX = 4; + surface.dstSelY = 5; + surface.dstSelZ = 6; + surface.dstSelW = 7; + surface.dccAddress = color.dccAddress; + surface.dccAlphaOnMsb = color.dccAlphaOnMsb; + surface.dccPipeAligned = color.dccPipeAligned; + return surface; +} + +ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allowMultisampleDcc) { const auto stride = slot * 0xfu; ColorTarget color{}; color.slot = slot; @@ -730,26 +1672,53 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { APS5_LOG_OUT_DEBUG("Color %u info=0x%x number=%u swap=%u", slot, info, number, swap); const auto format = (info >> 2u) & 0x1fu; const auto decoded = DecodeColorFormat(format, number, swap); - // ROUND_MODE (bit 18) only affects unorm rounding. With DCC_ENABLE (bit 28) the target is written - if ((info & ~(0x00039f7cu | 0x00040000u | 0x10000000u | 0x00002000u)) != 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"); + const auto attrib = read(cx, 0x31d + stride); + zero(cx, 0x31d + stride, ~0x3f000u, "color destination alpha override or other CB_COLOR_ATTRIB modes"); + color.samples = 1u << ((attrib >> 12u) & 7u); + 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; + // 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 <= 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; - Require(slice == ((view >> 13u) & 0x1fffu), "color views of several array slices are unsupported"); + const auto lastSlice = (view >> 13u) & 0x1fffu; + Require(lastSlice >= slice, "the color view ends before its first slice"); const auto viewMip = (view >> 26u) & 0xfu; - zero(cx, 0x31d + stride, ~0u, "color samples, fragments or destination alpha override"); const auto attrib2 = read(cx, 0x3b0 + slot); const auto maxMip = attrib2 >> 28u; + Require(color.samples == 1 || maxMip == 0, "multisampled color mips are unsupported"); Require(viewMip <= maxMip, "color view mip exceeds the surface"); const auto attrib3 = read(cx, 0x3b8 + slot); color.tileMode = DecodeColorTileMode(attrib3); const bool volume = ((attrib3 >> 24u) & 3u) == 2u; + Require(((attrib3 >> 24u) & 3u) != 0u || (attrib2 & 0x3fffu) == 0u, "1D color targets taller than one row are unsupported"); if (volume) { color.depth = (attrib3 & 0x1fffu) + 1u; - Require(maxMip == 0 && (info & 0x10000000u) == 0, "mipmapped or DCC 3D color targets are unsupported"); - Require(slice < color.depth, "the color view slice is beyond the 3D surface"); + 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; + } else if ((attrib3 & 0x1fffu) != 0) { + Require(lastSlice <= (attrib3 & 0x1fffu), "the color view slice is beyond the array surface"); + color.arrayLastSlice = attrib3 & 0x1fffu; + color.arraySlice = slice; + } + if (!volume && lastSlice != slice) { + const auto exports = find(cx, 0x207); + 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; @@ -757,34 +1726,87 @@ 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); Require((high & ~0xffu) == 0, "invalid color address extension"); color.surfaceAddress = (static_cast(high) << 40u) | (static_cast(read(cx, 0x318 + stride)) << 8u); - if (slice != 0 && !volume) color.surfaceAddress += slice * ComputeSurfaceSize(ComputeElementMipLayout(ColorTextureTileMode(color.tileMode), color.elementBytes, color.surfaceExtent.width, color.surfaceExtent.height, color.mipCount), 1); + if (ColorTileModeIsXor(color.tileMode)) { + color.pipeBankXor = static_cast(color.surfaceAddress & (ColorTileModeBlockBytes(color.tileMode) - 1u)); + color.surfaceAddress -= color.pipeBankXor; + } + Require(color.tileMode != ColorTileMode::D64KBX || (color.pipeBankXor & ~0xcf00u) == 0, "unsupported display color pipe/bank XOR column bits"); + color.arrayAddress = color.arrayLastSlice != 0 ? color.surfaceAddress : 0; + if (slice != 0 && !volume) { + color.surfaceAddress += slice * ComputeSurfaceSize(ComputeElementMipLayout(ColorTextureTileMode(color.tileMode), color.elementBytes, color.surfaceExtent.width, color.surfaceExtent.height, color.mipCount), 1); + if (color.arrayLastSlice == 0 && ColorTileModeIsXor(color.tileMode)) { + const auto* equation = FindTextureSwizzleEquation(static_cast(color.tileMode), color.elementBytes); + Require(equation != nullptr, "a sliced color target has no swizzle equation"); + const auto coordinates = slice << 24u; + for (std::uint32_t bit = 0; bit < equation->bits.size(); ++bit) { + color.pipeBankXor ^= (std::popcount(equation->bits[bit] & coordinates) & 1u) << bit; + } + } + } color.address = color.surfaceAddress + mipOffset; color.bytes = colorLayout.Bytes(); GuestMemory::CheckRange(reinterpret_cast(color.address), color.bytes, colorLayout.Alignment(), true); color.format = decoded.format; color.componentMapping = decoded.componentMapping; + color.packing = decoded.packing; + 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) { - Require(maxMip == 0 && !volume && slice == 0, "CMASK fast clears of a mipmapped, 3D or array color target are unsupported"); + if (((info & 0x2000u) != 0 || (color.samples > 1 && (info & 0x4000u) != 0)) && (color.samples > 1 || viewMip == 0)) { + 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 (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); - color.cmaskAddress = ((cmaskHigh == cx.end() ? 0ull : static_cast(cmaskHigh->second & 0xffu)) << 40u) | (static_cast(read(cx, 0x31f + stride)) << 8u); - Require(color.cmaskAddress != 0, "CMASK fast clears without a CMASK address are unsupported"); - color.cmaskBytes = CmaskBytes(color.extent.width, color.extent.height); + 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) { + 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(cmask), totalCmaskBytes, CmaskLayout::Alignment, true); + } + color.cmaskFastClear = (info & 0x2000u) != 0; } if ((info & 0x10000000u) != 0) { if (maxMip == 0) { @@ -792,6 +1814,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) { @@ -800,6 +1827,10 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { } } } + if (volume && (color.tileMode == ColorTileMode::RenderTarget || color.tileMode == ColorTileMode::Standard4KB)) { + color.bytes = DescribeSurface(SurfaceForTarget(color)).guestBytes; + GuestMemory::CheckRange(reinterpret_cast(color.address), color.bytes, colorLayout.Alignment(), true); + } return color; } @@ -808,11 +1839,31 @@ std::optional DecodeColorMetadataPass(const QueueState& queue const auto control = find(cx, 0x202); if (control == cx.end()) return std::nullopt; const auto mode = (control->second >> 4u) & 7u; - if (mode != 2u && mode != 6u) return std::nullopt; - ColorMetadataPass pass{mode == 2u ? ColorMetadataPass::Mode::EliminateFastClear : ColorMetadataPass::Mode::DccDecompress, {}}; + const auto aaConfig = find(cx, 0x2f8); + const bool rasterizerSamples = aaConfig != cx.end() && (aaConfig->second & 7u) != 0; + const auto sourceAttrib = find(cx, 0x31d); + const bool sourceSamples = sourceAttrib != cx.end() && ((sourceAttrib->second >> 12u) & 7u) != 0; + const bool resolve = mode == 3u && (rasterizerSamples || sourceSamples); + 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"); - Require((read(cx, 0x200) & 0xfu) == 0 && (read(cx, 0x0) & 0xfu) == 0, "CB metadata pass with depth or stencil work"); - zero(cx, 0x2f8, ~0u, "multisampling or coverage conversion"); + const auto value = [&](std::uint32_t offset) { + const auto found = find(cx, offset); + return found == cx.end() ? 0u : found->second; + }; + const bool planes = !depthPlanesAbsent(cx); + const auto depthControl = planes ? read(cx, 0x200) : 0u; + const bool depthWork = (depthControl & 0xfu) != 0 && ((depthControl & 8u) != 0 || !depthPassThrough(depthControl)); + if (depthWork || (planes && (read(cx, 0x0) & 0xfu) != 0)) { + char text[192]; + std::snprintf(text, sizeof(text), "CB metadata pass with depth or stencil work (DB_DEPTH_CONTROL 0x%08x, DB_RENDER_CONTROL 0x%08x, DB_Z_INFO 0x%08x, DB_STENCIL_INFO 0x%08x)", value(0x200), value(0x0), value(0x010), value(0x011)); + throw std::runtime_error(text); + } + const auto 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)); @@ -825,16 +1876,38 @@ std::optional DecodeColorMetadataPass(const QueueState& queue intersect(covered, cx, 0x81, false); intersect(covered, cx, 0x90, false); if ((read(cx, 0x292) & 2u) != 0) intersect(covered, cx, 0x94, false); - 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((read(cx, 0x31c + slot * 0xfu) & 0x10000000u) == 0 || target.dccAddress != 0, "CB metadata pass over a mipmapped DCC color target, whose keys are not modeled"); + 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"); + }; + if (resolve) { + const auto source = DecodeColorBuffer(cx, 0); + const auto destination = DecodeColorBuffer(cx, 1); + Require(samples == 1 || source.samples == samples, "CB resolve from a target whose sample count differs from the rasterizer's"); + Require(destination.samples == 1, "CB resolve into a multisampled target is unsupported"); + Require(destination.cmaskAddress == 0, "CB resolve into a fast-clear color target is unsupported (the resolve does not update its CMASK)"); + Require(source.format == destination.format && source.extent.width == destination.extent.width && source.extent.height == destination.extent.height, "CB resolve between different formats or extents is unsupported"); + 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); + return pass; + } + 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, 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); } return pass; @@ -860,37 +1933,66 @@ 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, "clip distances, layer, viewport or auxiliary vertex exports"); !reason.empty()) return reason; + if (auto reason = nonzero(cx, 0x207, ~(LayerExports | ClipCullExports | inactiveVertexPointSize(queue)), "layer, viewport or auxiliary vertex exports"); !reason.empty()) return reason; + if (value(cx, 0x207, word) && std::popcount(word & 0xffffu) > 8) return require(false, "more than eight clip and cull distances are unsupported"); if (value(cx, 0x200, word)) { const auto renderControl = find(cx, 0x000); + const bool depthClear = renderControl != cx.end() && (renderControl->second & 1u) != 0; const bool stencilClear = renderControl != cx.end() && (renderControl->second & 2u) != 0; - if (stencilClear) { + if (stencilClear && !depthPlanesAbsent(cx)) { std::uint32_t stencil = 0, view = 0; if ((value(cx, 0x011, stencil) && (stencil & 1u) == 0) || (value(cx, 0x002, view) && (view & 0x02000000u) != 0)) return require(false, "stencil clear requires a writable stencil plane"); } - const bool surface = ((word & 0xbu) != 0 || stencilClear) && depthSurfaceBound(cx); - if (!surface && !((word & 3u) != 0 && depthPlanesAbsent(cx)) && !depthPassThrough(word) && !IgnoreDepthTest() && (effectiveDepthControl(word) & DepthControlMask) != 0) return zeroMessage(0x200, word, "depth, stencil or conditional color writes"); - if (auto reason = require((word & 8u) == 0 || surface, "depth bounds without a depth surface"); !reason.empty()) return reason; + 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"); + } + const auto tests = planeTests(cx, word); + const bool surface = (tests != 0 || depthClear || stencilClear) && depthSurfaceBound(cx); + if (!surface && !((word & 0xbu) != 0 && (tests == 0 || depthPlanesAbsent(cx))) && !depthPassThrough(word) && !IgnoreDepthTest() && (effectiveDepthControl(word) & DepthControlMask) != 0) return zeroMessage(0x200, word, "depth, stencil or conditional color writes"); + if (auto reason = require((word & 8u) == 0 || surface || depthPlanesAbsent(cx), "depth bounds without a depth surface"); !reason.empty()) return reason; if (auto reason = require((word & 0xc0000000u) == 0, "depth-conditional color writes are unsupported"); !reason.empty()) return reason; } std::uint32_t zFormat = 0; static_cast(value(cx, 0x1c4, zFormat)); if (auto reason = nonzero(cx, 0x203, shaderControlMask(zFormat), "depth export, shader coverage or ordered fragment execution"); !reason.empty()) return reason; - if (auto reason = nonzero(cx, 0x2dc, AlphaToCoverageMask, "alpha-to-coverage"); !reason.empty()) return reason; - if (auto reason = nonzero(cx, 0x2f8, ~0u, "multisampling or coverage conversion"); !reason.empty()) return reason; - if (auto reason = nonzero(cx, 0x292, ScanModeMask, "scan conversion mode"); !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 (value(cx, 0x2dc, word)) { + const bool alphaDisabled = word == AlphaToCoverageDithered && alphaToCoverageDisabled(cx); + if ((word != AlphaToCoverageUndithered && word != AlphaToCoverageDithered) || alphaDisabled) { + if (auto reason = nonzero(cx, 0x2dc, alphaDisabled ? AlphaToCoverageMask & ~1u : AlphaToCoverageMask, "dithered or rounded alpha-to-coverage"); !reason.empty()) return reason; + } + if (!alphaDisabled && word == AlphaToCoverageDithered && !ditheredAlphaSamplesSupported(cx)) return "AGC graphics: dithered alpha-to-coverage requires eight matched raster/DB_EQAA samples and one pixel iteration"; + if (!alphaDisabled && word == AlphaToCoverageDithered) { + if (!ditheredAlphaLocationsSupported(cx)) return "AGC graphics: dithered alpha-to-coverage with custom sample locations is unsupported"; + std::uint32_t format = 0; + if (value(cx, 0x1c5, format)) { + format &= 0xfu; + if (format != 3u && format != 4u && format != 5u && format != 6u && format != 9u) return "AGC graphics: alpha-to-coverage needs an MRT0 export format with a floating-point alpha"; + if (format != 4u && format != 9u) return "AGC graphics: dithered alpha-to-coverage needs an FP16_ABGR or 32_ABGR export"; + } + } + } + if (auto reason = nonzero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u | CoveredCentroidIsCenter), "coverage conversion or nonstandard multisampling"); !reason.empty()) return reason; + if (!coveredCentroidSupported(queue)) return "AGC graphics: COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs is not implemented"; + const bool multisampled = value(cx, 0x2f8, word) && (word & 7u) != 0; + if (auto reason = nonzero(cx, 0x292, ScanModeMask & ~1u, "scan conversion mode"); !reason.empty()) return reason; + if (!multisampled && value(cx, 0x292, word) && (word & 1u) != 0) { + if (auto reason = singleSampleMsaaRejection(queue); !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 != 0x6000u && 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"); + if (value(cx, 0x313, word) && (word & ~0x001f6000u) != 0 && word != 0x6001u) return conservativeMessage(word); + 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; @@ -909,21 +2011,24 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { bool PixelProgramSkipped(const QueueState& queue) { const auto low = find(queue.shader, 0x008, RegisterBank::Shader); const auto high = find(queue.shader, 0x009, RegisterBank::Shader); - if (low != queue.shader.end() && high != queue.shader.end() && low->second == 0 && high->second == 0) return true; + if ((low == queue.shader.end() || low->second == 0) && (high == queue.shader.end() || high->second == 0)) return true; 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 {}; } @@ -932,6 +2037,56 @@ 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]; + if (state.colors[index].componentMapping != 0xecu || !blend.blendEnable) continue; + if (blend.srcColorBlendFactor != blend.srcAlphaBlendFactor || blend.dstColorBlendFactor != blend.dstAlphaBlendFactor || blend.colorBlendOp != blend.alphaBlendOp) return true; + } + 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 26911542c8..a70da26def 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" @@ -11,6 +16,7 @@ #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp" #include #include #include @@ -29,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. @@ -60,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 @@ -77,15 +101,20 @@ 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 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}; +std::atomic depthCopyTextures{0}, depthCopyRefreshes{0}; +std::atomic refreshesProved{0}; +std::atomic untrackedCompares{0}; struct StorageTraffic { std::mutex mutex; @@ -126,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; @@ -206,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"); } @@ -223,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; @@ -240,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; @@ -252,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"); @@ -263,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; - imageInfo.imageType = ImageTypeFor(descriptor.dimension); + 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 = 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"); @@ -283,10 +363,20 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; allocation.allocationSize = requirements.size; allocationBytes = requirements.size; - allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &owned->memory), "vkAllocateMemory texture"); + allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, requirements.size, "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 @@ -325,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(); @@ -340,7 +431,17 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex for (std::uint32_t level = 0; level < mips.size(); ++level) { if (!geometry.HasLayer(level, layer)) continue; const auto& mip = mips[level]; - detiler.Dispatch(commands, descriptor.tileMode, elementBytes, tiled->Handle(), guestLayerOffset + mip.tiledOffset, linear->Handle(), linearLayerOffset + mip.linearOffset, mip, false, layer, geometry.thick); + 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); + } } } @@ -360,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); @@ -381,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); @@ -426,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}; @@ -442,6 +555,8 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView"); viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; createFirstLayerView(descriptor, viewInfo); + prepareBaseLevelView(descriptor, viewInfo); + prepareSrgbView(descriptor, viewInfo); if (profile) { auto& totals = Profile(); totals.view += timer.lap(); @@ -455,12 +570,450 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex } } +VkFormat Texture::DepthCopyFormat(VkFormat colorFormat) { + switch (colorFormat) { + case VK_FORMAT_R32_SFLOAT: return VK_FORMAT_D32_SFLOAT; + case VK_FORMAT_R16_UNORM: return VK_FORMAT_D16_UNORM; + default: return VK_FORMAT_UNDEFINED; + } +} + +bool Texture::CanDepthCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor) { + if (!CanCopyFrom(source, descriptor) || descriptor.dimension == TextureDimension::k3D) return false; + if (DepthCopyFormat(ResolveTextureFormat(descriptor.format)) == VK_FORMAT_UNDEFINED) return false; + if (ResolveTextureFormat(source.Descriptor().format) != ResolveTextureFormat(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; +} + +bool Texture::CanBlockCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor) { + const auto& from = source.Descriptor(); + if (!IsBlockCompressed(descriptor.format) || IsBlockCompressed(from.format) || BytesPerElement(from.format) != BytesPerElement(descriptor.format)) return false; + if (descriptor.dimension != TextureDimension::k2D && descriptor.dimension != TextureDimension::k2DArray) return false; + if (descriptor.baseAddress != from.baseAddress || descriptor.pipeBankXor != from.pipeBankXor || descriptor.dimension != from.dimension || descriptor.tileMode != from.tileMode || descriptor.depthOrLastArray != from.depthOrLastArray || descriptor.mipCount > from.mipCount) return false; + for (std::uint32_t level = 0; level < descriptor.mipCount; ++level) { + const auto width = std::max(descriptor.width >> level, 1u); + const auto height = std::max(descriptor.height >> level, 1u); + if (width % 4u != 0 || height % 4u != 0 || std::max(from.width >> level, 1u) != width / 4u || std::max(from.height >> level, 1u) != height / 4u) return false; + } + const auto geometry = DescribeSurface(descriptor); + return geometry.imageDepth == 1 && source.ImageDepth() == 1 && source.ImageLayers() >= geometry.imageLayers; +} + +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"); + blockFormat = SampledTextureFormat(context, descriptor.format); + const auto geometry = DescribeSurface(descriptor); + copyWidth = descriptor.width; + copyHeight = descriptor.height; + copyLevels = descriptor.mipCount; + copyLayers = geometry.imageLayers; + copyElementBytes = BytesPerElement(descriptor.format); + 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; + imageInfo.format = blockFormat; + imageInfo.extent = {descriptor.width, descriptor.height, 1u}; + imageInfo.mipLevels = copyLevels; + imageInfo.arrayLayers = copyLayers; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Check(context.Function("vkCreateImage")(context.device, &imageInfo, nullptr, &image), "vkCreateImage block copy"); + owned = std::make_shared(context, image, VK_NULL_HANDLE); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocationBytes = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + 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; + const auto viewLayerCount = geometry.imageLayers - descriptor.baseArray; + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = image; + viewInfo.viewType = ViewTypeFor(descriptor.dimension, viewLayerCount); + viewInfo.format = blockFormat; + viewFormat = blockFormat; + viewInfo.components = components; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, descriptor.baseLevel, viewLevelCount, descriptor.baseArray, viewLayerCount}; + VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT}; + ChainMinLod(context, descriptor, viewInfo, minLod); + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView block copy"); + viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; + createFirstLayerView(descriptor, viewInfo); + prepareBaseLevelView(descriptor, viewInfo); + } catch (...) { + release(); + throw; + } +} + +Texture::Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components, DepthCopyTag) : context(context), storageSource(source), perUseCopy(PerUseCopy::RefreshedOnReuse) { + try { + Require(source != nullptr && CanDepthCopyFrom(*source, descriptor), "storage image cannot serve a depth copy of the sampled texture"); + const auto colorFormat = ResolveTextureFormat(descriptor.format); + depthFormat = DepthCopyFormat(colorFormat); + 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 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 = depth1D ? VK_IMAGE_TYPE_2D : ImageTypeFor(descriptor.dimension); + imageInfo.format = depthFormat; + imageInfo.extent = {descriptor.width, depth1D ? 1u : 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 depth 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 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) { + Require(viewLayerCount % 6u == 0, "guest cube texture view does not contain a multiple of 6 array slices"); + } + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = image; + viewInfo.viewType = ViewTypeFor(descriptor.dimension, 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 sRGB copy"); + viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; + createFirstLayerView(descriptor, viewInfo); + prepareBaseLevelView(descriptor, viewInfo); + sourceDescriptor = descriptor; + countedBytes = allocationBytes; + SampledMemoryCounter().fetch_add(countedBytes, std::memory_order_relaxed); + } catch (...) { + release(); + throw; + } +} + +bool Texture::refreshSrgbExpand() { + auto* recorder = Recorder::Active(); + std::unique_ptr batch; + VkCommandBuffer commands = VK_NULL_HANDLE; + if (recorder != nullptr) { + commands = recorder->Commands(); + recorder->Keep(owned); + recorder->Keep(storageSource); + } else { + batch = std::make_unique(context); + commands = batch->Handle(); + } + const auto barrier = context.Function("vkCmdPipelineBarrier"); + const VkMemoryBarrier produced{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | 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 = {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}; + linearReady.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + linearReady.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + linearReady.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + linearReady.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + linearReady.buffer = linear->Handle(); + linearReady.offset = 0; + linearReady.size = VK_WHOLE_SIZE; + 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; + toShaderRead.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + toShaderRead.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + toShaderRead.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + toShaderRead.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toShaderRead.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toShaderRead.image = image; + toShaderRead.subresourceRange = toTransferDst.subresourceRange; + 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(); + if (!repackCopy) depthCopyRefreshes.fetch_add(1, std::memory_order_relaxed); + return true; +} + +bool Texture::refreshBlockCopy() { + std::vector regions; + for (std::uint32_t level = 0; level < copyLevels; ++level) { + VkImageCopy region{}; + region.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level, 0, copyLayers}; + region.dstSubresource = region.srcSubresource; + region.extent = {std::max(copyWidth >> level, 1u) / 4u, std::max(copyHeight >> level, 1u) / 4u, 1u}; + regions.push_back(region); + } + auto* recorder = Recorder::Active(); + std::unique_ptr batch; + VkCommandBuffer commands = VK_NULL_HANDLE; + if (recorder != nullptr) { + commands = recorder->Commands(); + recorder->Keep(owned); + recorder->Keep(storageSource); + } else { + batch = std::make_unique(context); + commands = batch->Handle(); + } + const auto barrier = context.Function("vkCmdPipelineBarrier"); + const VkMemoryBarrier produced{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT}; + VkImageMemoryBarrier toTransferDst{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + toTransferDst.srcAccessMask = VK_ACCESS_SHADER_READ_BIT; + toTransferDst.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toTransferDst.oldLayout = copiedOnce ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL : VK_IMAGE_LAYOUT_UNDEFINED; + toTransferDst.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + toTransferDst.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toTransferDst.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toTransferDst.image = image; + toTransferDst.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, copyLevels, 0, copyLayers}; + barrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &produced, 0, nullptr, 1, &toTransferDst); + context.Function("vkCmdCopyImage")(commands, storageSource->Image(), VK_IMAGE_LAYOUT_GENERAL, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast(regions.size()), regions.data()); + VkImageMemoryBarrier toShaderRead = toTransferDst; + toShaderRead.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toShaderRead.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + toShaderRead.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + toShaderRead.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &toShaderRead); + if (batch) batch->SubmitAndWait(); + copiedOnce = true; + copiedVersion = storageSource->Version(); + return true; +} + +Texture::DepthCopyCounts Texture::TakeDepthCopyCounts() { + return {depthCopyTextures.exchange(0, std::memory_order_relaxed), depthCopyRefreshes.exchange(0, std::memory_order_relaxed)}; +} + bool Texture::CanCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor) { const auto& from = source.Descriptor(); if (IsBlockCompressed(descriptor.format) || IsBlockCompressed(from.format)) return false; // Same memory, same layout, same texel size: the GPU copy reinterprets the texels exactly as a // guest read through the sampled descriptor would. - return descriptor.baseAddress == from.baseAddress && descriptor.width == from.width && descriptor.height == from.height && descriptor.dimension == from.dimension && descriptor.tileMode == from.tileMode && descriptor.mipCount <= from.mipCount && descriptor.depthOrLastArray == from.depthOrLastArray && BytesPerElement(descriptor.format) == BytesPerElement(from.format) && BlockWidth(descriptor.format) == BlockWidth(from.format); + 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); } Texture::Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components) : context(context), storageSource(source) { @@ -470,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(); @@ -490,6 +1045,8 @@ Texture::Texture(const Context& context, const std::shared_ptr& Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView storage view"); viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; createFirstLayerView(descriptor, viewInfo); + prepareBaseLevelView(descriptor, viewInfo); + prepareSrgbView(descriptor, viewInfo); if (profile) { auto& totals = Profile(); totals.view += timer.lap(); @@ -502,17 +1059,26 @@ Texture::Texture(const Context& context, const std::shared_ptr& } } -Texture::Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components) : context(context) { +Texture::Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType, 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 = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.viewType = viewType; viewInfo.format = depthFormat; viewFormat = depthFormat; viewInfo.components = components; - viewInfo.subresourceRange = {aspect, 0, 1, 0, 1}; + viewInfo.subresourceRange = {aspect, 0, 1, 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; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.subresourceRange.layerCount = 1; + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &firstLayerView), "vkCreateImageView depth plane first layer"); + firstLayerRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; } Texture::~Texture() { @@ -520,19 +1086,87 @@ 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"); firstLayerRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; } +void Texture::prepareBaseLevelView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo) { + if (viewInfo.subresourceRange.levelCount == 1u) 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 && !depth1D) ? VK_IMAGE_VIEW_TYPE_1D : VK_IMAGE_VIEW_TYPE_2D; + viewInfo.subresourceRange.levelCount = 1; + viewInfo.subresourceRange.layerCount = 1; + baseLevelInfo = viewInfo; +} + +void Texture::prepareSrgbView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo) { + const auto format = SrgbTwinFormat(context, viewInfo.format); + if (format == VK_FORMAT_UNDEFINED) return; + viewInfo.pNext = nullptr; + viewInfo.format = format; + srgbInfo = viewInfo; + srgbMinLod = EffectiveMinLod(descriptor); +} + +VkImageView Texture::SrgbView(bool firstLayer, bool baseLevel) const { + Require(srgbInfo.has_value(), "AGC graphics: a texture of view format " + std::to_string(viewFormat) + " has no sRGB view for a sampler that forces sRGB decoding"); + std::lock_guard lock(srgbMutex); + auto& slot = srgbViews[baseLevel ? 2u : firstLayer ? 1u : 0u]; + if (slot != VK_NULL_HANDLE) return slot; + VkImageViewCreateInfo info = *srgbInfo; + VkImageViewUsageCreateInfo usage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; + usage.usage = VK_IMAGE_USAGE_SAMPLED_BIT; + info.pNext = &usage; + VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT}; + if (baseLevel) { + info.viewType = info.viewType == VK_IMAGE_VIEW_TYPE_1D || info.viewType == VK_IMAGE_VIEW_TYPE_1D_ARRAY ? VK_IMAGE_VIEW_TYPE_1D : VK_IMAGE_VIEW_TYPE_2D; + info.subresourceRange.levelCount = 1; + info.subresourceRange.layerCount = 1; + } else { + if (firstLayer) { + info.viewType = VK_IMAGE_VIEW_TYPE_2D; + info.subresourceRange.layerCount = 1; + } + if (srgbMinLod != 0.0f) { + minLod.minLod = srgbMinLod; + minLod.pNext = info.pNext; + info.pNext = &minLod; + } + } + Check(context.Function("vkCreateImageView")(context.device, &info, nullptr, &slot), "vkCreateImageView sRGB"); + return slot; +} + +VkImageView Texture::BaseLevelView() const { + Require(baseLevelInfo.has_value(), "a base-level view needs a mipmapped 1D, 2D or 2D array texture"); + std::lock_guard lock(baseLevelMutex); + 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; +} + void Texture::release() noexcept { upload.reset(); if (view) context.Function("vkDestroyImageView")(context.device, view, nullptr); view = VK_NULL_HANDLE; if (firstLayerView) context.Function("vkDestroyImageView")(context.device, firstLayerView, nullptr); firstLayerView = VK_NULL_HANDLE; + if (baseLevelView) context.Function("vkDestroyImageView")(context.device, baseLevelView, nullptr); + baseLevelView = VK_NULL_HANDLE; + for (auto& srgbView : srgbViews) { + if (srgbView) context.Function("vkDestroyImageView")(context.device, srgbView, nullptr); + srgbView = VK_NULL_HANDLE; + } // The image and its memory go with the last holder: this texture, or the batch still uploading it. owned.reset(); image = VK_NULL_HANDLE; @@ -631,12 +1265,15 @@ bool StorageFormatAvailable(const Context& context, std::uint32_t guestFormat) { } } -// Integer formats take integer clear values; the DCC clear codes are only mapped for the others. +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: case VK_FORMAT_R8G8B8A8_UINT: case VK_FORMAT_R8G8B8A8_SINT: case VK_FORMAT_B8G8R8A8_UINT: case VK_FORMAT_B8G8R8A8_SINT: - case VK_FORMAT_A8B8G8R8_UINT_PACK32: case VK_FORMAT_A8B8G8R8_SINT_PACK32: case VK_FORMAT_A2R10G10B10_UINT_PACK32: case VK_FORMAT_A2B10G10R10_UINT_PACK32: + case VK_FORMAT_A8B8G8R8_UINT_PACK32: case VK_FORMAT_A8B8G8R8_SINT_PACK32: case VK_FORMAT_A2R10G10B10_UINT_PACK32: case VK_FORMAT_A2B10G10R10_UINT_PACK32: case VK_FORMAT_A2B10G10R10_SINT_PACK32: case VK_FORMAT_R16_UINT: case VK_FORMAT_R16_SINT: case VK_FORMAT_R16G16_UINT: case VK_FORMAT_R16G16_SINT: case VK_FORMAT_R16G16B16A16_UINT: case VK_FORMAT_R16G16B16A16_SINT: case VK_FORMAT_R32_UINT: case VK_FORMAT_R32_SINT: case VK_FORMAT_R32G32_UINT: case VK_FORMAT_R32G32_SINT: case VK_FORMAT_R32G32B32_UINT: case VK_FORMAT_R32G32B32_SINT: @@ -646,9 +1283,12 @@ bool IntegerFormat(VkFormat format) { } } -// The clear value a DCC clear code stands for, for non-integer formats. bool ClearColorFor(VkFormat format, DccKeys keys, VkClearColorValue& clear) { - if (IntegerFormat(format)) return false; + if (IntegerFormat(format)) { + if (keys != DccKeys::Clear0000) return false; + clear.uint32[0] = clear.uint32[1] = clear.uint32[2] = clear.uint32[3] = 0u; + return true; + } switch (keys) { case DccKeys::Clear0000: clear.float32[0] = clear.float32[1] = clear.float32[2] = clear.float32[3] = 0.0f; return true; case DccKeys::Clear0001: clear.float32[0] = clear.float32[1] = clear.float32[2] = 0.0f; clear.float32[3] = 1.0f; return true; @@ -667,6 +1307,23 @@ bool StorageClearAvailable(const Context& context, std::uint32_t guestFormat, Dc return IsDccClear(keys) && StorageFormatAvailable(context, guestFormat) && ClearColorFor(StorageFormatForGuest(context, guestFormat), keys, clear); } +VkFormat SrgbTwinFormat(const Context& context, VkFormat format) { + const auto decoded = [&](ShaderRecompiler::IrBufferFormat srgb) { return (context.srgbDecodeFormats & ShaderRecompiler::SrgbDecodeBit(srgb)) != 0u; }; + switch (format) { + case VK_FORMAT_R8_UNORM: return decoded(ShaderRecompiler::IrBufferFormat::Format8Srgb) ? VK_FORMAT_UNDEFINED : VK_FORMAT_R8_SRGB; + case VK_FORMAT_R8G8_UNORM: return decoded(ShaderRecompiler::IrBufferFormat::Format8_8Srgb) ? VK_FORMAT_UNDEFINED : VK_FORMAT_R8G8_SRGB; + case VK_FORMAT_R8G8B8A8_UNORM: return VK_FORMAT_R8G8B8A8_SRGB; + case VK_FORMAT_B8G8R8A8_UNORM: return VK_FORMAT_B8G8R8A8_SRGB; + case VK_FORMAT_A8B8G8R8_UNORM_PACK32: return VK_FORMAT_A8B8G8R8_SRGB_PACK32; + case VK_FORMAT_BC1_RGB_UNORM_BLOCK: return VK_FORMAT_BC1_RGB_SRGB_BLOCK; + case VK_FORMAT_BC1_RGBA_UNORM_BLOCK: return VK_FORMAT_BC1_RGBA_SRGB_BLOCK; + case VK_FORMAT_BC2_UNORM_BLOCK: return VK_FORMAT_BC2_SRGB_BLOCK; + case VK_FORMAT_BC3_UNORM_BLOCK: return VK_FORMAT_BC3_SRGB_BLOCK; + case VK_FORMAT_BC7_UNORM_BLOCK: return VK_FORMAT_BC7_SRGB_BLOCK; + default: return VK_FORMAT_UNDEFINED; + } +} + VkFormat AttachmentProxyFormat(const Context& context, VkFormat format) { if (format != VK_FORMAT_R8_SRGB) return VK_FORMAT_UNDEFINED; struct Table { @@ -689,16 +1346,43 @@ VkFormat AttachmentProxyFormat(const Context& context, VkFormat format) { return proxy; } -StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, std::uint32_t mipLevel) : context(context), detiler(detiler), descriptor(descriptor) { +std::shared_ptr StorageTexture::Create(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, std::uint32_t mipLevel) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; PhaseTimer timer; + static const bool unwatchHooked = [] { + GuestMemory::SetUnwatchHook(&StorageTexture::SaveBeforeUnwatch); + return true; + }(); + static_cast(unwatchHooked); + auto texture = std::shared_ptr(new StorageTexture(context, detiler, descriptor, mipLevel)); + uploadReason = "first"; + texture->upload(); + texture->defaultMip = mipLevel; + texture->view = texture->createView(mipLevel, false, texture->storageFormat); + { + auto& live = Live(); + std::lock_guard lock(live.mutex); + live.textures.push_back(texture.get()); + } + if (profile) { + auto& totals = Profile(); + totals.storageCreate += timer.lap(); + totals.storageBytes += texture->guestBytes; + if (++totals.storageCount % 50 == 0) std::fprintf(stderr, "[texture] %llu storage images (%.0f MiB): create %.0f ms, write-back %.0f ms (alloc %.0f, host copy %.0f, gpu %.0f, store %.0f), %llu reused, %llu direct uploads, %llu direct write-backs, clears %llu recorded / %llu waited\n", static_cast(totals.storageCount), totals.storageBytes / 1048576.0, totals.storageCreate, totals.storageWriteBack, totals.storageAlloc, totals.storageHostCopy, totals.storageGpu, totals.storageStore, static_cast(totals.storageReused), static_cast(totals.storageDirectUploads), static_cast(totals.storageDirectWriteBacks), static_cast(totals.storageRecordedClears), static_cast(totals.storageWaitedClears)); + } + return texture; +} + +StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, std::uint32_t mipLevel) : context(context), detiler(detiler), descriptor(descriptor) { try { Require(!IsBlockCompressed(descriptor.format), "block-compressed textures cannot be storage images"); 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; @@ -715,6 +1399,8 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, trackedLayerBytes = blockUnits ? 65536 : guestBytes / trackedLayers; layerGeneration.assign(trackedLayers, 0); layerPending.assign(trackedLayers, false); + layerOriginal.assign(trackedLayers, true); + savedTicket.assign(trackedLayers, 0); sliceLinearBytes = geometry.sliceLinearBytes; const auto linearBytes = sliceLinearBytes * arrayLayers; @@ -725,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; @@ -732,7 +1419,7 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, { VkFormatProperties properties{}; context.formatProperties(context.physical, vkFormat, &properties); - attachable = (descriptor.dimension == TextureDimension::k2D || descriptor.dimension == TextureDimension::k3D) && (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0; + attachable = (descriptor.dimension == TextureDimension::k2D || descriptor.dimension == TextureDimension::k3D || descriptor.dimension == TextureDimension::k2DArray) && (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0; if (attachable) imageInfo.usage |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; } imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; @@ -743,24 +1430,9 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; allocation.allocationSize = requirements.size; memoryBytes = requirements.size; - allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory storage texture"); + allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, requirements.size, "storage texture"); + memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory storage texture", &memoryType); Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory storage"); - uploadReason = "first"; - upload(); - defaultMip = mipLevel; - view = createView(mipLevel, false, storageFormat); - { - auto& live = Live(); - std::lock_guard lock(live.mutex); - live.textures.push_back(this); - } - if (profile) { - auto& totals = Profile(); - totals.storageCreate += timer.lap(); - totals.storageBytes += guestBytes; - if (++totals.storageCount % 50 == 0) std::fprintf(stderr, "[texture] %llu storage images (%.0f MiB): create %.0f ms, write-back %.0f ms (alloc %.0f, host copy %.0f, gpu %.0f, store %.0f), %llu reused, %llu direct uploads, %llu direct write-backs, clears %llu recorded / %llu waited\n", static_cast(totals.storageCount), totals.storageBytes / 1048576.0, totals.storageCreate, totals.storageWriteBack, totals.storageAlloc, totals.storageHostCopy, totals.storageGpu, totals.storageStore, static_cast(totals.storageReused), static_cast(totals.storageDirectUploads), static_cast(totals.storageDirectWriteBacks), static_cast(totals.storageRecordedClears), static_cast(totals.storageWaitedClears)); - } } catch (...) { release(); throw; @@ -772,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()); @@ -805,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() { @@ -902,7 +1598,7 @@ bool ClearLayoutFor(VkFormat format, ClearLayout& layout) { // A float of `bits` (16: IEEE half; 11 and 10: the unsigned 5-bit-exponent floats of // B10G11R11) as a float32, or nullopt for a NaN, a denormal or (unsigned) an infinity. -std::optional SmallFloat(std::uint32_t code, std::uint32_t bits) { +std::optional SmallFloat(std::uint32_t code, std::uint32_t bits, bool denormals = false) { const std::uint32_t mantissaBits = bits == 16 ? 10u : bits - 5u; const bool negative = bits == 16 && (code >> 15u) != 0; const std::uint32_t exponent = (code >> mantissaBits) & 0x1fu; @@ -912,8 +1608,9 @@ std::optional SmallFloat(std::uint32_t code, std::uint32_t bits) { return negative ? -std::numeric_limits::infinity() : std::numeric_limits::infinity(); } if (exponent == 0) { - if (mantissa != 0) return std::nullopt; - return negative ? -0.0f : 0.0f; + if (mantissa != 0 && !denormals) return std::nullopt; + const float value = std::ldexp(static_cast(mantissa) / static_cast(1u << mantissaBits), -14); + return negative ? -value : value; } const float value = std::ldexp(1.0f + static_cast(mantissa) / static_cast(1u << mantissaBits), static_cast(exponent) - 15); return negative ? -value : value; @@ -925,7 +1622,7 @@ std::optional SmallFloat(std::uint32_t code, std::uint32_t bits) { // to the same code), float ones the decoded value. Codes a clear might store differently are // refused: NaNs and denormals (an implementation may canonicalize or flush them) and the most // negative SNORM code (stored as its neighbour, the same -1.0). -bool FillClearColor(VkFormat format, std::uint32_t elementBytes, std::span pattern, VkClearColorValue& clear) { +bool FillClearColor(VkFormat format, std::uint32_t elementBytes, std::span pattern, VkClearColorValue& clear, bool denormals = false) { ClearLayout layout{}; if (elementBytes == 0 || elementBytes > 16 || 16 % elementBytes != 0 || !ClearLayoutFor(format, layout)) return false; std::array bytes{}; @@ -961,13 +1658,13 @@ bool FillClearColor(VkFormat format, std::uint32_t elementBytes, std::span(code); } else { - const auto value = SmallFloat(code, channel.bits); + const auto value = SmallFloat(code, channel.bits, denormals); if (!value) return false; clear.float32[channel.component] = *value; } break; case ClearKind::UFloat: { - const auto value = SmallFloat(code, channel.bits); + const auto value = SmallFloat(code, channel.bits, denormals); if (!value) return false; clear.float32[channel.component] = *value; break; @@ -1000,6 +1697,97 @@ bool AdjacentGenerationEnabled() { } +bool ClearKeepsDenormals(const Context& context, VkFormat format) { + static std::mutex mutex; + static std::map, bool> known; + std::lock_guard lock(mutex); + if (const auto found = known.find({context.device, format}); found != known.end()) return found->second; + ClearLayout layout{}; + bool smallFloats = ClearLayoutFor(format, layout) && (layout.kind == ClearKind::Float || layout.kind == ClearKind::UFloat); + std::array pattern{}; + VkClearColorValue clear{}; + std::uint32_t elementBits = 0; + constexpr std::array halfCodes{0x0001u, 0x83ffu, 0x0200u, 0x8001u}; + for (std::uint32_t i = 0; smallFloats && i < layout.channels; ++i) { + const auto& channel = layout.channel[i]; + if (channel.bits == 32) { + smallFloats = false; + break; + } + const auto code = channel.bits == 16 ? halfCodes[i] : 1u; + pattern[channel.offset / 32u] |= code << (channel.offset % 32u); + clear.float32[channel.component] = *SmallFloat(code, channel.bits, true); + elementBits = std::max(elementBits, channel.offset + channel.bits); + } + bool keeps = false; + if (smallFloats) { + const auto elementBytes = static_cast((elementBits + 7u) / 8u); + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + struct Release { + const Context& context; + VkImage& image; + VkDeviceMemory& memory; + ~Release() { + if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); + if (memory) context.Function("vkFreeMemory")(context.device, memory, nullptr); + } + } release{context, image, memory}; + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.imageType = VK_IMAGE_TYPE_2D; + info.format = format; + info.extent = {1, 1, 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_SRC_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 denormal clear probe"); + 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 denormal clear probe"); + Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory denormal clear probe"); + Buffer readback(context, elementBytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + CommandBatch batch(context); + const auto commands = batch.Handle(); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + VkImageMemoryBarrier toClear{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + toClear.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toClear.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + toClear.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + toClear.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toClear.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toClear.image = image; + toClear.subresourceRange = range; + context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toClear); + context.Function("vkCmdClearColorImage")(commands, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clear, 1, &range); + VkImageMemoryBarrier toCopy = toClear; + toCopy.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toCopy.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + toCopy.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + toCopy.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toCopy); + VkBufferImageCopy region{}; + region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + region.imageExtent = {1, 1, 1}; + context.Function("vkCmdCopyImageToBuffer")(commands, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback.Handle(), 1, ®ion); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + batch.SubmitAndWait(); + keeps = std::memcmp(readback.Bytes().data(), pattern.data(), elementBytes) == 0; + } + known.emplace(std::pair{context.device, format}, keeps); + return keeps; +} + +bool ClearColorForTexel(VkFormat format, std::uint32_t elementBytes, std::span pattern, VkClearColorValue& clear) { + return FillClearColor(format, elementBytes, pattern, clear); +} + VkImageView StorageTexture::createView(std::uint32_t mip, bool firstLayer, VkFormat format) const { Require(mip < descriptor.mipCount, "storage texture mip level is outside the texture"); Require(!firstLayer || descriptor.dimension == TextureDimension::k2DArray, "a first-layer storage view needs a 2D array surface"); @@ -1019,25 +1807,60 @@ VkImageView StorageTexture::createView(std::uint32_t mip, bool firstLayer, VkFor return created; } -VkImageView StorageTexture::AttachmentView(VkFormat format, std::uint32_t mip, std::uint32_t depthSlice) { +bool StorageTexture::singlePass() { + if (singlePassState < 0) { + const auto elementBytes = BytesPerElement(descriptor.format); + const auto format = TextureDetiler::ImageElementFormat(elementBytes); + bool eligible = context.singlePassStorage && format != VK_FORMAT_UNDEFINED && BlockWidth(descriptor.format) == 1 && !geometry.thick && geometry.imageDepth == 1 && (descriptor.dimension == TextureDimension::k2D || descriptor.dimension == TextureDimension::k2DArray || descriptor.dimension == TextureDimension::kCube); + if (eligible) { + VkFormatProperties properties{}; + context.formatProperties(context.physical, format, &properties); + eligible = (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0; + } + singlePassState = eligible ? 1 : 0; + } + return singlePassState == 1; +} + +VkImageView StorageTexture::elementLayerView(std::uint32_t level, std::uint32_t layer) { + const auto key = (level << 16) | layer; + if (const auto found = elementLayerViews.find(key); found != elementLayerViews.end()) return found->second; + VkImageViewUsageCreateInfo usage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; + usage.usage = VK_IMAGE_USAGE_STORAGE_BIT; + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.pNext = &usage; + viewInfo.image = image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = TextureDetiler::ImageElementFormat(BytesPerElement(descriptor.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, level, 1u, geometry.CopyLayer(layer), 1u}; + VkImageView created = VK_NULL_HANDLE; + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &created), "vkCreateImageView storage element"); + elementLayerViews.emplace(key, created); + return created; +} + +VkImageView StorageTexture::AttachmentView(VkFormat format, std::uint32_t mip, std::uint32_t depthSlice, std::uint32_t layers, std::uint32_t layer) { Require(attachable, "storage image cannot be a color attachment"); Require(mip < descriptor.mipCount, "attachment mip exceeds the storage image"); const bool volume = descriptor.dimension == TextureDimension::k3D; - Require(depthSlice == 0 || (volume && mip == 0 && depthSlice <= descriptor.depthOrLastArray), "attachment slice is outside the storage image"); - const auto found = attachmentViews.find({format, mip, depthSlice}); + 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}; usage.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.pNext = &usage; viewInfo.image = image; - viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.viewType = layers > 1 ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D; viewInfo.format = format; viewInfo.components = {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY}; - viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, mip, 1u, volume ? depthSlice : descriptor.baseArray, 1u}; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, mip, 1u, volume ? depthSlice : descriptor.baseArray + layer, layers}; VkImageView created = VK_NULL_HANDLE; Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &created), "vkCreateImageView attachment"); - attachmentViews.emplace(std::tuple{format, mip, depthSlice}, created); + attachmentViews.emplace(std::tuple{format, mip, depthSlice, layers, layer}, created); return created; } @@ -1063,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; @@ -1159,6 +1982,16 @@ VkImageView StorageTexture::StorageView(std::uint32_t mip, bool firstLayer) { return created; } +VkImageView StorageTexture::ElementView() { + if (elementView != VK_NULL_HANDLE) return elementView; + if (descriptor.dimension != TextureDimension::k2D || geometry.imageLayers != 1) return VK_NULL_HANDLE; + const auto bytes = BytesPerElement(descriptor.format); + const auto format = bytes == 1 ? VK_FORMAT_R8_UINT : bytes == 2 ? VK_FORMAT_R16_UINT : bytes == 4 ? VK_FORMAT_R32_UINT : bytes == 8 ? VK_FORMAT_R32G32_UINT : VK_FORMAT_UNDEFINED; + if (format == VK_FORMAT_UNDEFINED || StorageFormatOrUndefined(context, format) != format) return VK_NULL_HANDLE; + elementView = createView(0, false, format); + return elementView; +} + VkImageView StorageTexture::AtomicView(std::uint32_t mip, bool firstLayer) { if (storageFormat == VK_FORMAT_R32_UINT) return firstLayer ? FirstLayerView(mip) : View(mip); Require(storageFormat == VK_FORMAT_R32_SINT || storageFormat == VK_FORMAT_R32_SFLOAT, "storage image atomics need a surface of one 32-bit component"); @@ -1180,7 +2013,7 @@ VkImageView StorageTexture::Atomic64View(std::uint32_t mip, bool firstLayer) { } const char* LookupOutcomes::Name(Kind kind) { - static constexpr const char* names[Count] = {"sampled fast hit", "sampled fast miss", "sampled hit view", "sampled hit cleared view", "sampled hit snapshot", "sampled made view", "sampled made snapshot", "storage hit", "storage made", "refresh unchanged", "refresh compared", "upload direct", "upload cpu", "upload clear", "dcc scan", "pending flush"}; + static constexpr const char* names[Count] = {"sampled fast hit", "sampled fast miss", "sampled hit view", "sampled hit cleared view", "sampled hit snapshot", "sampled made view", "sampled made snapshot", "storage hit", "storage made", "refresh unchanged", "refresh compared", "refresh proved", "upload direct", "upload cpu", "upload clear", "dcc scan", "pending flush"}; return kind < Count ? names[kind] : "?"; } @@ -1205,6 +2038,15 @@ LookupOutcomes& ThreadLookupOutcomes() { bool StorageTexture::Refresh() { const bool profile = LookupOutcomes::Profiled(); auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; + CompletePendingCpuWrites(context, descriptor.baseAddress, static_cast(guestBytes)); + if (descriptor.dccAddress != 0) CompletePendingCpuWrites(context, descriptor.dccAddress, DccKeyBytes(guestBytes)); + if (refreshProved()) { + NoteProved(); + refreshesProved.fetch_add(1, std::memory_order_relaxed); + if (profile) LookupOutcomes::Add(LookupOutcomes::RefreshProved, start); + return true; + } + refreshProof = {}; struct Exempt { const StorageTexture* previous; ~Exempt() { refreshing = previous; } @@ -1227,14 +2069,17 @@ bool StorageTexture::Refresh() { if (!GuestMemory::ChangedBlocks(descriptor.baseAddress, static_cast(guestBytes), alias->layerGeneration, aliasStamped)) alias = nullptr; for (std::uint32_t unit = 0; alias != nullptr && unit < trackedLayers; ++unit) { if (!alias->layerPending[unit]) continue; - if (alias->layerGeneration[unit] == 0 || aliasStamped[unit] == GuestMemory::BlockMaybeWritten || (aliasStamped[unit] == GuestMemory::BlockUnchanged && layerPending[unit])) { + if (alias->layerGeneration[unit] == 0 || (aliasStamped[unit] == GuestMemory::BlockUnchanged && layerPending[unit])) { alias = nullptr; break; } } } // 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 ? clearHandOver() : std::nullopt; + const bool flushed = !handOverKeys.has_value() && FlushPending(descriptor.baseAddress, static_cast(guestBytes), 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 @@ -1268,11 +2113,18 @@ bool StorageTexture::Refresh() { stampedBlocks.assign(generations.size(), 0); cpuBlocks.assign(generations.size(), 0); tracked = GuestMemory::ChangedBlocks(descriptor.baseAddress, static_cast(guestBytes), generations, stampedBlocks, cpuBlocks); + if (!tracked) awaitStoredSave(nullptr); compared = !tracked && compareUntracked(descriptor.baseAddress, static_cast(guestBytes), stampedBlocks, true); - if (compared) cpuBlocks = stampedBlocks; + if (compared) { + for (const auto& [from, unitGeneration] : keptAtUnwatch) { + const auto unit = static_cast((from - descriptor.baseAddress) / trackedLayerBytes); + if (keptAtUnwatchHolds(from, unit)) stampedBlocks[static_cast(from / 65536 - descriptor.baseAddress / 65536)] = GuestMemory::BlockWritten; + } + cpuBlocks = stampedBlocks; + } }; const auto keysStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; - keys = ProvedKeys(); + keys = handOverKeys.has_value() ? *handOverKeys : ProvedKeys(); if (profile && descriptor.dccAddress != 0) LookupOutcomes::Add(LookupOutcomes::DccScan, keysStart); if (keys != uploadedKeys) { changed.assign(trackedLayers, true); @@ -1323,7 +2175,7 @@ bool StorageTexture::Refresh() { for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { if (changed[layer] && layerPending[layer]) pendingChanged = true; } - if (!unchanged && !keysChanged && !pendingChanged && originalValid && equalsOriginal()) { + if (!unchanged && !keysChanged && !pendingChanged && originalHolds(descriptor.baseAddress, static_cast(guestBytes)) && equalsOriginal()) { unchanged = true; stamped = false; } @@ -1357,6 +2209,7 @@ bool StorageTexture::Refresh() { ++Profile().storageReused; layerGeneration.assign(trackedLayers, current); refreshGeneration(); + takeRefreshProof(alias != nullptr); if (profile) LookupOutcomes::Add(stamped ? LookupOutcomes::RefreshUnchanged : LookupOutcomes::RefreshCompared, start); return true; } @@ -1365,7 +2218,7 @@ bool StorageTexture::Refresh() { static const bool traceUpload = std::getenv("APS5_TRACE_UPLOAD") != nullptr; if (traceUpload) { const auto keys = TextureClearKeys(descriptor, guestBytes); - std::fprintf(stderr, "[upload] 0x%llx+0x%llx %ux%u mips %u: %s (keys %s -> %s, originalValid %d, dirty %d)\n", static_cast(descriptor.baseAddress), static_cast(guestBytes), descriptor.width, descriptor.height, descriptor.mipCount, keys != uploadedKeys ? "DCC keys changed" : "guest memory changed", DccKeysName(uploadedKeys), DccKeysName(keys), originalValid ? 1 : 0, dirty ? 1 : 0); + std::fprintf(stderr, "[upload] 0x%llx+0x%llx %ux%u mips %u: %s (keys %s -> %s, originalValid %d, dirty %d)\n", static_cast(descriptor.baseAddress), static_cast(guestBytes), descriptor.width, descriptor.height, descriptor.mipCount, keys != uploadedKeys ? "DCC keys changed" : "guest memory changed", DccKeysName(uploadedKeys), DccKeysName(keys), originalHolds(descriptor.baseAddress, static_cast(guestBytes)) ? 1 : 0, dirty ? 1 : 0); } // Changed layers with results pending: the CPU wrote this memory while GPU results were // pending, so as with an immediate write-back followed by the CPU write, its blocks win and the @@ -1397,7 +2250,7 @@ bool StorageTexture::Refresh() { // drop rule. Debug aid: APS5_KEYFLIP_DROP=1 drops as before. static const bool keyFlipDrop = std::getenv("APS5_KEYFLIP_DROP") != nullptr; static const bool traceKeys = std::getenv("APS5_TRACE_DCC_KEYS") != nullptr; - const bool keyFlip = keysChanged && blockUnits && !keyFlipDrop && IsDccClear(uploadedKeys) && keys == DccKeys::Uncompressed && stampedBlocks.size() >= trackedLayers; + const bool keyFlip = keysChanged && blockUnits && !keyFlipDrop && (IsDccClear(uploadedKeys) || uploadedKeys == DccKeys::ClearSingle) && keys == DccKeys::Uncompressed && stampedBlocks.size() >= trackedLayers; if (traceKeys && keysChanged) { std::size_t pendingUnits = 0, stampedUnits = 0; for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) { @@ -1430,7 +2283,7 @@ bool StorageTexture::Refresh() { stored[layer] = true; anyStored = true; } - if (anyStored && !keysChanged && descriptor.dccAddress != 0 && IsDccClear(uploadedKeys)) stored.assign(trackedLayers, true); + if (anyStored && !keysChanged && descriptor.dccAddress != 0 && (IsDccClear(uploadedKeys) || uploadedKeys == DccKeys::ClearSingle)) stored.assign(trackedLayers, true); if (pendingResults) { static std::atomic reports{0}; if (reports.fetch_add(1) < 8) std::fprintf(stderr, "[gpu] storage image 0x%llx: guest memory changed while GPU results were pending; keeping the CPU's blocks\n", static_cast(descriptor.baseAddress)); @@ -1468,6 +2321,61 @@ DccKeys StorageTexture::ProvedKeys() const { return *keys; } +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); + const auto other = [&](const StorageTexture* texture) { return texture != this && texture->overlaps(descriptor.baseAddress, static_cast(guestBytes)); }; + return std::any_of(pending.textures.begin(), pending.textures.end(), other) || std::any_of(pending.flushing.begin(), pending.flushing.end(), other); +} + +bool StorageTexture::refreshProved() { + if (refreshProof.generation == 0) return false; + if (generation != refreshProof.generation || uploadedKeys != refreshProof.keys || !GuestMemory::GpuMutex().HeldByThisThread()) return false; + const auto bytes = static_cast(guestBytes); + std::array queries{}; + std::size_t used = 0; + if (GuestMemory::CollectWrites(descriptor.baseAddress, bytes) == 0) return false; + queries[used++] = {descriptor.baseAddress, bytes, generation}; + if (descriptor.dccAddress != 0) { + const auto keyCount = static_cast(guestBytes / 256); + if (keyCount == 0 || keyProof.generation != refreshProof.keyGeneration || keyProof.keys != uploadedKeys) return false; + if (GuestMemory::CollectWrites(descriptor.dccAddress, keyCount) == 0) return false; + queries[used++] = {descriptor.dccAddress, keyCount, keyProof.generation}; + } + if (!GuestMemory::UnchangedSinceAll(std::span(queries.data(), used))) return false; + const auto serial = PendingSerial(); + if (serial != refreshProof.pendingSerial) { + if (otherPendingOverlaps()) return false; + refreshProof.pendingSerial = serial; + } + return true; +} + +void StorageTexture::takeRefreshProof(bool aliased) { + refreshProof = {}; + if (aliased || generation == 0 || !GuestMemory::GpuMutex().HeldByThisThread()) return; + if (descriptor.dccAddress != 0 && (keyProof.generation == 0 || keyProof.keys != uploadedKeys)) return; + const auto serial = PendingSerial(); + if (otherPendingOverlaps()) return; + refreshProof = {generation, serial, keyProof.generation, uploadedKeys}; +} + +std::uint64_t StorageTexture::RefreshesProved() { + return refreshesProved.load(std::memory_order_relaxed); +} + +std::uint64_t StorageTexture::UntrackedCompares() { + return untrackedCompares.load(std::memory_order_relaxed); +} + bool StorageTexture::ServesKeysAt(std::uint64_t dccAddress) const { const bool locked = GuestMemory::GpuMutex().HeldByThisThread(); const auto readFollowed = [&] { @@ -1503,18 +2411,29 @@ void traceKeyStore(const char* path, const GuestTextureResource& descriptor, std } void StorageTexture::captureGuestBytes(const std::vector* layers) { + original.resize(static_cast(guestBytes)); comparedGuestBytes = {}; - const bool complete = layers == nullptr || std::all_of(layers->begin(), layers->end(), [](bool selected) { return selected; }); + std::vector selected(trackedLayers, true); + if (layers != nullptr) selected = *layers; + for (const auto& [begin, end] : unitRuns(selected)) GuestMemory::FlushGpuWrites(descriptor.baseAddress + begin, static_cast(end - begin)); for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { - if (layers != nullptr && !(*layers)[layer]) continue; + if (!selected[layer]) continue; const auto offset = static_cast(layerBegin(layer) - descriptor.baseAddress); - GuestMemory::ReadCommitted(layerBegin(layer), std::span(original).subspan(offset, static_cast(layerBytes(layer)))); + GuestMemory::ReadCommittedUnsynced(layerBegin(layer), std::span(original).subspan(offset, static_cast(layerBytes(layer)))); + layerOriginal[layer] = true; + cancelStoredSave(layer); } - originalValid = originalValid || complete; +} + +bool StorageTexture::originalHolds(std::uint64_t address, std::size_t bytes) const { + for (auto layer = (address - descriptor.baseAddress) / trackedLayerBytes; layer < trackedLayers && layer * trackedLayerBytes < address - descriptor.baseAddress + bytes; ++layer) { + if (!layerOriginal[static_cast(layer)]) return false; + } + return true; } bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes, std::span changed, bool memoize) const { - if (!originalValid) return false; + if (!originalHolds(address, bytes)) return false; struct Exempt { const StorageTexture* previous; ~Exempt() { refreshing = previous; } @@ -1523,18 +2442,22 @@ bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes, Require(address >= descriptor.baseAddress && bytes <= guestBytes && address - descriptor.baseAddress <= guestBytes - bytes, "texture comparison exceeds its snapshot"); Require(original.size() == guestBytes, "texture comparison has no complete snapshot"); GuestMemory::FlushGpuWrites(address, bytes); - if (!originalValid) return false; + if (!originalHolds(address, bytes)) return false; constexpr std::uint64_t blockBytes = 65536; const auto end = address + bytes; const auto first = address / blockBytes; Require(changed.size() == (end - 1) / blockBytes - first + 1, "texture comparison block count differs"); const auto* recorder = Recorder::Active(); - const std::array stamp{GuestMemory::CollectEpoch(), GuestMemory::TrackerGeneration(), GuestMemory::ForgetSerial(), recorder != nullptr ? recorder->NewestWriteNote(address, bytes) : 0}; - const bool cacheable = memoize && stamp[0] != 0 && (stamp[2] & 1u) == 0 && address == descriptor.baseAddress && bytes == guestBytes; + const auto epoch = GuestMemory::CollectEpoch(); + const bool whole = address == descriptor.baseAddress && bytes == guestBytes; + const auto hint = memoize && epoch != 0 && whole ? GuestMemory::UntrackedHint(address, bytes) : 0; + const std::array stamp{hint != 0, hint != 0 ? hint : epoch, GuestMemory::NewestStamp(address, bytes), GuestMemory::ForgetSerial(), recorder != nullptr ? recorder->NewestWriteNote(address, bytes) : 0}; + const bool cacheable = memoize && epoch != 0 && (stamp[3] & 1u) == 0 && whole; if (cacheable && comparedGuestBytes == stamp) { std::fill(changed.begin(), changed.end(), GuestMemory::BlockUnchanged); return true; } + untrackedCompares.fetch_add(1, std::memory_order_relaxed); const auto saved = std::span(original).subspan(static_cast(address - descriptor.baseAddress), bytes); if (GuestMemory::EqualsCommittedUnsynced(address, saved)) { std::fill(changed.begin(), changed.end(), GuestMemory::BlockUnchanged); @@ -1551,20 +2474,33 @@ bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes, return true; } +constexpr std::uint64_t GenerationBaselineBytes = 1ull << 20; + void StorageTexture::upload(const std::vector* layers) { - CaptureTrace::Log("upload image=%llx bytes=%llu generation=%llu reason=%s partial=%d", static_cast(descriptor.baseAddress), static_cast(guestBytes), static_cast(generation), uploadReason, layers != nullptr); const bool profile = LookupOutcomes::Profiled(); const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; + CompletePendingCpuWrites(context, descriptor.baseAddress, static_cast(guestBytes)); + if (descriptor.dccAddress != 0) CompletePendingCpuWrites(context, descriptor.dccAddress, DccKeyBytes(guestBytes)); + CaptureTrace::Log("upload image=%llx bytes=%llu generation=%llu reason=%s partial=%d", static_cast(descriptor.baseAddress), static_cast(guestBytes), static_cast(generation), uploadReason, layers != nullptr); const auto elementBytes = BytesPerElement(descriptor.format); const auto linearBytes = sliceLinearBytes * arrayLayers; - original.resize(static_cast(guestBytes)); // A layer selection applies to the direct path alone (the others upload everything); it names // array layers, which the tracked layers are when there are several. if (layers != nullptr && ((!blockUnits && trackedLayers != arrayLayers) || std::all_of(layers->begin(), layers->end(), [](bool selected) { return selected; }))) layers = nullptr; const auto now = GuestMemory::CollectWrites(descriptor.baseAddress, static_cast(guestBytes)); const auto stampLayers = [&](bool selectedOnly) { + const bool whole = !selectedOnly || layers == nullptr; + if (guestBytes > GenerationBaselineBytes || !guestBytesSettled()) generationBaseline.clear(); + else if (whole) { + generationBaseline.resize(static_cast(guestBytes)); + GuestMemory::ReadCommitted(descriptor.baseAddress, generationBaseline); + } for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { - if (!selectedOnly || layers == nullptr || (*layers)[layer]) layerGeneration[layer] = now; + if (!whole && !(*layers)[layer]) continue; + layerGeneration[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)))); } refreshGeneration(); }; @@ -1576,7 +2512,13 @@ void StorageTexture::upload(const std::vector* layers) { // The clear is recorded into the open batch like a direct upload (the image kept by it): a // batch of its own submitted the recorder's work first and waited for all of it, 25-40 ms // under the GPU mutex at the movie stage. APS5_NO_RECORDED_CLEAR=1 waits as before. - captureGuestBytes(nullptr); + static_cast(HostImportFor(context, descriptor.baseAddress, static_cast(guestBytes))); + 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; @@ -1587,7 +2529,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()) { @@ -1600,7 +2542,7 @@ void StorageTexture::upload(const std::vector* layers) { ++Profile().storageWaitedClears; } VkImageMemoryBarrier toClear{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; - toClear.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT; + toClear.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT; toClear.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; toClear.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; toClear.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; @@ -1612,7 +2554,7 @@ void StorageTexture::upload(const std::vector* layers) { context.Function("vkCmdClearColorImage")(commands, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clearValue, 1, &toClear.subresourceRange); VkImageMemoryBarrier toGeneral = toClear; toGeneral.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - toGeneral.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT; + toGeneral.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT; toGeneral.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL; context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); @@ -1624,8 +2566,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))) originalValid = 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 @@ -1661,7 +2607,8 @@ void StorageTexture::upload(const std::vector* layers) { if ((*layers)[layer]) uploadedBytes += trackedLayerBytes; } } - auto linear = std::make_shared(context, static_cast(linearBytes), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + const bool direct = singlePass(); + auto linear = direct ? nullptr : std::make_shared(context, static_cast(linearBytes), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT); detiler.BeginBatch(); auto* recorder = Recorder::Active(); std::unique_ptr batch; @@ -1672,12 +2619,12 @@ void StorageTexture::upload(const std::vector* layers) { recorder->FlushStoresOverlapping(descriptor.baseAddress, static_cast(guestBytes)); commands = recorder->Commands(); timing = recorder->BeginGpuTiming(Recorder::CommandClass::StorageUpload); - recorder->Keep(linear, linear->Size()); + 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, 3); + Recorder::CountBarriers(Recorder::CommandClass::StorageUpload, direct ? 2 : 3); if (Recorder::BarrierValidate()) { const std::pair read{descriptor.baseAddress, descriptor.baseAddress + guestBytes}; const std::pair written{image, true}; @@ -1687,14 +2634,34 @@ 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) { + if (layers != nullptr && !(*layers)[layer]) continue; + for (std::uint32_t level = 0; level < mips.size(); ++level) { + if (!geometry.HasLayer(level, layer)) continue; + const auto& mip = mips[level]; + detiler.DispatchImage(commands, descriptor.tileMode, elementBytes, import->buffer, importOffset + geometry.GuestLayerOffset(layer) + mip.tiledOffset, elementLayerView(level, layer), mip, false, layer, {.pipeBankXor = descriptor.pipeBankXor}); + } + } + recordDirectUploadDone(commands); + singlePassMoves.fetch_add(1, std::memory_order_relaxed); + if (batch) batch->SubmitAndWait(); + else recorder->EndGpuTiming(timing, uploadedBytes); + countStorageUpload(1, uploadedBytes); + ++Profile().storageDirectUploads; + ++version; + if (profile) LookupOutcomes::Add(LookupOutcomes::UploadDirect, start); + return; + } RecordMemoryBarrier(context, 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 | VK_ACCESS_SHADER_WRITE_BIT); for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) { if (layers != nullptr && !(*layers)[layer]) continue; for (std::uint32_t level = 0; level < mips.size(); ++level) { if (!geometry.HasLayer(level, layer)) continue; const auto& mip = mips[level]; - detiler.Dispatch(commands, descriptor.tileMode, elementBytes, import->buffer, importOffset + geometry.GuestLayerOffset(layer) + mip.tiledOffset, linear->Handle(), geometry.LinearLayerOffset(layer) + mip.linearOffset, mip, false, layer, geometry.thick); + detiler.Dispatch(commands, descriptor.tileMode, elementBytes, import->buffer, importOffset + geometry.GuestLayerOffset(layer) + mip.tiledOffset, linear->Handle(), geometry.LinearLayerOffset(layer) + mip.linearOffset, mip, false, layer, geometry.thick, {.pipeBankXor = descriptor.pipeBankXor}); } } const auto linearRead = WholeBufferBarrier(linear->Handle(), VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); @@ -1725,9 +2692,11 @@ void StorageTexture::upload(const std::vector* layers) { if (profile) LookupOutcomes::Add(LookupOutcomes::UploadDirect, start); return; } - originalValid = true; + 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); @@ -1745,7 +2714,7 @@ void StorageTexture::upload(const std::vector* layers) { for (std::uint32_t level = 0; level < mips.size(); ++level) { if (!geometry.HasLayer(level, layer)) continue; const auto& mip = mips[level]; - detiler.Dispatch(commands, descriptor.tileMode, elementBytes, tiled.Handle(), geometry.GuestLayerOffset(layer) + mip.tiledOffset, linear.Handle(), geometry.LinearLayerOffset(layer) + mip.linearOffset, mip, false, layer, geometry.thick); + detiler.Dispatch(commands, descriptor.tileMode, elementBytes, tiled.Handle(), geometry.GuestLayerOffset(layer) + mip.tiledOffset, linear.Handle(), geometry.LinearLayerOffset(layer) + mip.linearOffset, mip, false, layer, geometry.thick, {.pipeBankXor = descriptor.pipeBankXor}); } } const auto linearRead = WholeBufferBarrier(linear.Handle(), VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); @@ -1945,6 +2914,80 @@ 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}; + toWrite.srcAccessMask = discard ? 0u : VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + toWrite.dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT; + toWrite.oldLayout = discard ? VK_IMAGE_LAYOUT_UNDEFINED : VK_IMAGE_LAYOUT_GENERAL; + toWrite.newLayout = VK_IMAGE_LAYOUT_GENERAL; + toWrite.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toWrite.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toWrite.image = image; + toWrite.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, descriptor.mipCount, 0, geometry.imageLayers}; + context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 1, &sourceReady, 0, nullptr, 1, &toWrite); +} + +void StorageTexture::recordDirectUploadDone(VkCommandBuffer commands) { + VkImageMemoryBarrier written{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + written.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT; + written.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + written.oldLayout = VK_IMAGE_LAYOUT_GENERAL; + written.newLayout = VK_IMAGE_LAYOUT_GENERAL; + written.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + written.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + written.image = image; + written.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_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &written); +} + std::uint64_t StorageTexture::uploadWindows(const HostImport& import, std::span> runs, bool discard) { const auto elementBytes = BytesPerElement(descriptor.format); // Each run's pieces by source (a unit shadow's slab while fresh, else the import), the tail @@ -1984,7 +3027,8 @@ std::uint64_t StorageTexture::uploadWindows(const HostImport& import, std::span< linearTotal += (window.linearBytes + 255) & ~std::uint64_t{255}; } for (const auto& [begin, end] : runs) uploadedBytes += end - begin; - auto linear = std::make_shared(context, static_cast(linearTotal), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + 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_SRC_BIT); detiler.BeginBatch(); auto* recorder = Recorder::Active(); std::unique_ptr batch; @@ -2001,13 +3045,13 @@ std::uint64_t StorageTexture::uploadWindows(const HostImport& import, std::span< for (const auto& [begin, end] : reads) recorder->FlushStoresOverlapping(begin, static_cast(end - begin)); commands = recorder->Commands(); timing = recorder->BeginGpuTiming(Recorder::CommandClass::StorageUpload); - recorder->Keep(linear, linear->Size()); + if (linear) recorder->Keep(linear, linear->Size()); 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, 3); + Recorder::CountBarriers(Recorder::CommandClass::StorageUpload, direct ? 2 : 3); if (Recorder::BarrierValidate()) { const std::pair written{image, true}; recorder->NoteAccess(Recorder::CommandClass::StorageUpload, Recorder::Access{reads, {}, std::span(&written, 1), VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT}); @@ -2016,7 +3060,28 @@ 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) { + const auto& mip = mips[window.level]; + const auto& source = sourceOf(window); + auto detile = window.window; + detile.pipeBankXor = descriptor.pipeBankXor; + const auto view = elementLayerView(window.level, window.layer); + if (source.shadow) { + detiler.DispatchImage(commands, descriptor.tileMode, elementBytes, source.buffer, source.offset + (window.tiledBegin - source.begin), view, mip, false, window.layer, detile); + } else { + detile.tiledBase = 0; + detiler.DispatchImage(commands, descriptor.tileMode, elementBytes, import.buffer, importOffset + geometry.GuestLayerOffset(window.layer) + mip.tiledOffset, view, mip, false, window.layer, detile); + } + } + recordDirectUploadDone(commands); + singlePassMoves.fetch_add(1, std::memory_order_relaxed); + if (batch) batch->SubmitAndWait(); + else recorder->EndGpuTiming(timing, uploadedBytes); + return uploadedBytes; + } RecordMemoryBarrier(context, 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 | VK_ACCESS_SHADER_WRITE_BIT); std::vector regions; for (std::size_t i = 0; i < windows.size(); ++i) { @@ -2024,6 +3089,7 @@ std::uint64_t StorageTexture::uploadWindows(const HostImport& import, std::span< const auto& mip = mips[window.level]; const auto& source = sourceOf(window); auto detile = window.window; + detile.pipeBankXor = descriptor.pipeBankXor; if (source.shadow) { // The slab holds the piece from the window's first tiled byte: the window's tiled base // is its range begin (sliceWindows builds it so), read from the piece's slab offset. @@ -2062,7 +3128,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; @@ -2072,6 +3138,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; @@ -2100,23 +3170,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); } @@ -2124,18 +3216,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]; @@ -2146,7 +3259,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; @@ -2154,8 +3352,10 @@ std::uint64_t StorageTexture::writeBackWindows(const HostImport& import, std::sp flushBegin = std::min(flushBegin, seed.begin); flushEnd = std::max(flushEnd, seed.end); } - auto linear = 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 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); + 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; @@ -2167,12 +3367,12 @@ std::uint64_t StorageTexture::writeBackWindows(const HostImport& import, std::sp recorder->FlushStoresOverlapping(flushBegin, static_cast(flushEnd - flushBegin)); commands = recorder->Commands(&covered); timing = recorder->BeginGpuTiming(Recorder::CommandClass::StorageWriteBack); - recorder->Keep(linear, linear->Size()); - recorder->Keep(tiledScratch, tiledScratch->Size()); + if (linear) recorder->Keep(linear, linear->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, 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; @@ -2193,8 +3393,8 @@ std::uint64_t StorageTexture::writeBackWindows(const HostImport& import, std::sp toSource.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; toSource.image = image; toSource.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, descriptor.mipCount, 0, geometry.imageLayers}; - 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 (!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() || !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; @@ -2204,39 +3404,74 @@ 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()); } - std::vector regions; - for (std::size_t i = 0; i < windows.size(); ++i) { - for (auto region : windows[i].regions) { - region.bufferOffset += linearPositions[i]; - regions.push_back(region); + 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) { + VkImageMemoryBarrier toRead = toSource; + 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; + 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; + 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 { + std::vector regions; + for (std::size_t i = 0; i < windows.size(); ++i) { + for (auto region : windows[i].regions) { + region.bufferOffset += linearPositions[i]; + regions.push_back(region); + } + } + context.Function("vkCmdCopyImageToBuffer")(commands, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, linear->Handle(), static_cast(regions.size()), regions.data()); + const auto linearRead = WholeBufferBarrier(linear->Handle(), VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); + context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 1, &importReady, 1, &linearRead, 0, nullptr); + for (std::size_t i = 0; i < windows.size(); ++i) { + const auto& window = windows[i]; + auto retile = window.window; + retile.pipeBankXor = descriptor.pipeBankXor; + 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); } } - context.Function("vkCmdCopyImageToBuffer")(commands, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, linear->Handle(), static_cast(regions.size()), regions.data()); - const auto linearRead = WholeBufferBarrier(linear->Handle(), VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); - 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}; - context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 1, &importReady, 1, &linearRead, 0, nullptr); - for (std::size_t i = 0; i < windows.size(); ++i) { - const auto& window = windows[i]; - detiler.Dispatch(commands, descriptor.tileMode, elementBytes, linear->Handle(), linearPositions[i], tiledScratch->Handle(), scratchPositions[i], mips[window.level], true, window.layer, false, window.window); - } - { + 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; @@ -2247,7 +3482,7 @@ std::uint64_t StorageTexture::writeBackWindows(const HostImport& import, std::sp // merges its leading barrier. constexpr VkAccessFlags storedAccess = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_READ_BIT; const VkMemoryBarrier stored{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT, storedAccess}; - context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &stored, 0, nullptr, 1, &backToGeneral); + context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &stored, 0, nullptr, direct ? 0u : 1u, &backToGeneral); if (batch) { batch->SubmitAndWait(); } else { @@ -2302,7 +3537,7 @@ bool StorageTexture::keysFillMatches(std::uint64_t address, std::size_t bytes, b } void StorageTexture::MarkDirty() { - if (descriptor.dccAddress != 0 && IsDccClear(uploadedKeys) && !IsDccClear(filledKeys)) { + if (descriptor.dccAddress != 0 && (IsDccClear(uploadedKeys) || uploadedKeys == DccKeys::ClearSingle) && !IsDccClear(filledKeys)) { traceKeyStore("first write", descriptor, guestBytes); MarkDccUncompressed(context, descriptor.dccAddress, guestBytes, DccKeyCount(descriptor, guestBytes)); uploadedKeys = DccKeys::Uncompressed; @@ -2310,6 +3545,21 @@ void StorageTexture::MarkDirty() { markLayersPending(0, trackedLayers); } +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 * count + trackedLayerBytes - 1) / trackedLayerBytes, trackedLayers)); + if (first >= last) { + MarkDirty(); + return; + } + markLayersPending(first, last - first); +} + bool StorageTexture::anyLayerPending() const { return std::any_of(layerPending.begin(), layerPending.end(), [](bool pending) { return pending; }); } @@ -2320,7 +3570,15 @@ void StorageTexture::refreshGeneration() { void StorageTexture::markLayersPending(std::uint32_t first, std::uint32_t count) { CaptureTrace::Log("image-write image=%llx first=%u count=%u generation=%llu", static_cast(descriptor.baseAddress), first, count, static_cast(generation)); + ++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; + for (std::uint32_t layer = first; layer < first + count; ++layer) { + unsaved[layer] = !layerPending[layer] && !layerOriginal[layer] && layerGeneration[layer] == 0; + anyUnsaved = anyUnsaved || unsaved[layer]; + } + if (anyUnsaved && !GuestMemory::Watched(descriptor.baseAddress, static_cast(guestBytes))) captureGuestBytes(&unsaved); for (std::uint32_t layer = first; layer < first + count; ++layer) layerPending[layer] = true; // Results of this image now cover the alias's results borrowed into these units: this image // stores them, the alias no longer has to (its results, unchanged since the borrow, are in @@ -2337,6 +3595,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; @@ -2374,6 +3633,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(); @@ -2410,7 +3675,12 @@ bool StorageTexture::pendingUnitInside(std::uint64_t address, std::size_t bytes) } 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; @@ -2439,14 +3709,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; @@ -2454,6 +3726,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}; @@ -2463,6 +3736,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) { @@ -2485,25 +3772,288 @@ bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, cons 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; + { + auto& pending = Pending(); + std::lock_guard lock(pending.mutex); + const auto take = [&](StorageTexture* texture) { + if (!texture->overlaps(address, bytes)) return; + if (auto alive = texture->weak_from_this().lock()) overlapping.push_back(std::move(alive)); + }; + for (auto* texture : pending.textures) take(texture); + for (auto* texture : pending.flushing) take(texture); + } + if (overlapping.empty()) return; + std::lock_guard gpu(GuestMemory::GpuMutex()); + for (const auto& texture : overlapping) texture->saveBeforeUnwatch(); +} + +void StorageTexture::forgetKeptAtUnwatch(std::uint32_t unit) { + const auto begin = layerBegin(unit); + keptAtUnwatch.erase(keptAtUnwatch.lower_bound(begin), keptAtUnwatch.lower_bound(begin + layerBytes(unit))); +} + +bool StorageTexture::keptAtUnwatchHolds(std::uint64_t from, std::uint32_t unit) const { + const auto found = keptAtUnwatch.find(from); + return found != keptAtUnwatch.end() && layerOriginal[unit] && found->second == layerGeneration[unit]; +} + +void StorageTexture::forgetSavedOver(std::uint64_t address, std::size_t bytes) { + // Only these units' bytes changed under the saved copy. Units outside keep it: results still + // pending there (the flush before the fill or copy stored only the units it overlaps) are + // compared with it at their store when the memory is outside the write watch. + for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) { + if (layerBegin(unit) < address + bytes && address < layerBegin(unit) + layerBytes(unit)) { + layerOriginal[unit] = false; + 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]; + const auto runs = unitRuns(units); + if (runs.empty()) return; + original.resize(static_cast(guestBytes)); + comparedGuestBytes = {}; + for (const auto& [begin, end] : runs) GuestMemory::ReadCommittedUnsynced(descriptor.baseAddress + begin, std::span(original).subspan(static_cast(begin), static_cast(end - begin))); + for (const auto& [begin, end] : runs) GuestMemory::CollectWritesUncached(descriptor.baseAddress + begin, static_cast(end - begin)); + constexpr std::uint64_t block = 65536; + for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) { + if (!units[unit]) continue; + 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); + const auto spanBlocks = static_cast((end - spanBegin + block - 1) / block); + const std::vector generations(spanBlocks, layerGeneration[unit]); + std::vector changedBlocks(spanBlocks); + if (!GuestMemory::ChangedBlocks(begin, static_cast(end - begin), generations, changedBlocks)) continue; + forgetKeptAtUnwatch(unit); + for (auto at = spanBegin; at < end; at += block) { + const auto from = std::max(at, begin); + const auto to = std::min(at + block, end); + const bool edge = from != at || to != at + block; + if (changedBlocks[static_cast((at - spanBegin) / block)] == GuestMemory::BlockWritten && (!edge || GuestMemory::StoredOver(from, static_cast(to - from), layerGeneration[unit])) && !unchangedSinceBaseline(from, to)) keptAtUnwatch.emplace(from, layerGeneration[unit]); + } + layerOriginal[unit] = true; + 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)); + comparedGuestBytes = {}; + 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; } +namespace { + +struct StorageDumpSelector { + std::uint64_t address = 0; + std::uint32_t width = 0, height = 0, layers = 0, format = 0; + bool anyFormat = true; +}; + +const StorageDumpSelector& StorageDumpSelection() { + static const StorageDumpSelector selector = [] { + StorageDumpSelector parsed; + const char* text = std::getenv("APS5_DUMP_STORAGE"); + if (text == nullptr) return parsed; + if (const int fields = std::sscanf(text, "%ux%ux%u:%u", &parsed.width, &parsed.height, &parsed.layers, &parsed.format); fields >= 3) { + parsed.anyFormat = fields < 4; + return parsed; + } + parsed.address = std::strtoull(text, nullptr, 16); + return parsed; + }(); + return selector; +} + +bool StorageDumpSelects(const GuestTextureResource& descriptor) { + const auto& selector = StorageDumpSelection(); + if (selector.address != 0) return descriptor.baseAddress == selector.address; + return selector.width != 0 && descriptor.width == selector.width && descriptor.height == selector.height && descriptor.depthOrLastArray + 1 == selector.layers && (selector.anyFormat || descriptor.format == selector.format); +} + +} + +void StorageTexture::FlushDumpSelected(const char* reason) { + const auto& selector = StorageDumpSelection(); + if (selector.address == 0 && selector.width == 0) return; + std::vector> ranges; + { + auto& pending = Pending(); + std::lock_guard lock(pending.mutex); + for (const auto* texture : pending.textures) { + if (StorageDumpSelects(texture->descriptor)) ranges.emplace_back(texture->descriptor.baseAddress, static_cast(texture->guestBytes)); + } + } + for (const auto& [address, bytes] : ranges) static_cast(FlushPending(address, bytes, nullptr, reason)); +} + 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); + return pending.textures.begin() != pending.textures.end(); +} + +bool StorageTexture::StoreAtFlipRequested(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_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); + StorageTexture* containing = nullptr; for (auto* texture : pending.textures) { + if (texture->descriptor.baseAddress != address || texture->guestBytes < bytes) continue; + if (texture->guestBytes == bytes) return texture->weak_from_this().lock(); // Containment, not equality: a descriptor of a chain's first mips (its own guestBytes are // shorter) is served by the chain's image; CanCopyFrom then checks the geometry. - if (texture->descriptor.baseAddress == address && texture->guestBytes >= bytes) return texture->weak_from_this().lock(); + if (containing == nullptr) containing = texture; } - return nullptr; + return containing != nullptr ? containing->weak_from_this().lock() : nullptr; +} + +std::shared_ptr StorageTexture::FindPendingSlice(std::uint64_t address, std::uint64_t bytes) { + if (auto whole = FindPending(address, bytes)) return whole; + auto& pending = Pending(); + std::lock_guard lock(pending.mutex); + StorageTexture* found = nullptr; + for (auto* texture : pending.textures) { + const auto base = texture->descriptor.baseAddress; + const auto layers = texture->geometry.imageLayers; + if (texture->geometry.thick || layers <= 1 || bytes == 0 || address <= base || texture->guestBytes % layers != 0 || texture->guestBytes / layers != bytes) continue; + if ((address - base) % bytes != 0 || (address - base) / bytes >= layers) continue; + if (found != nullptr) { + char message[256]; + std::snprintf(message, sizeof(message), "AGC graphics: 0x%llx+0x%llx is a slice of the pending array images 0x%llx and 0x%llx: which one holds its results is not modeled", static_cast(address), static_cast(bytes), static_cast(found->descriptor.baseAddress), static_cast(base)); + throw std::runtime_error(message); + } + found = texture; + } + return found != nullptr ? found->weak_from_this().lock() : nullptr; } bool PendingStorageOverlaps(std::uint64_t address, std::size_t bytes, const StorageTexture* except) { @@ -2538,6 +4088,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); } @@ -2550,6 +4113,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; @@ -2677,9 +4241,11 @@ bool StorageTexture::evictStale(std::uint64_t address, std::size_t bytes, std::s // As the empty write-back leaves the layers: no results pending, the generation unchanged (the // stamps make their next Refresh re-upload), `original` no longer vouching for the bytes. for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { - if (selected(layer)) layerPending[layer] = false; + if (!selected(layer)) continue; + layerPending[layer] = false; + layerOriginal[layer] = false; + cancelStoredSave(layer); } - originalValid = false; if (!anyLayerPending()) { ++dropped; staleEvicted.fetch_add(1, std::memory_order_relaxed); @@ -2712,9 +4278,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; @@ -2739,7 +4307,8 @@ std::size_t StorageTexture::DiscardPendingInside(std::uint64_t address, std::siz // touched the bytes a CPU-path upload copied). texture->dirty = false; texture->layerPending.assign(texture->trackedLayers, false); - texture->originalValid = false; + texture->layerOriginal.assign(texture->trackedLayers, false); + texture->cancelStoredSaves(); it = pending.textures.erase(it); ++discarded; } else if (texture != exempt && texture->blockUnits && texture->overlaps(address, bytes)) { @@ -2751,13 +4320,12 @@ std::size_t StorageTexture::DiscardPendingInside(std::uint64_t address, std::siz const auto unitBegin = texture->layerBegin(unit); if (!texture->layerPending[unit] || unitBegin < address || unitBegin + texture->layerBytes(unit) > end) continue; texture->layerPending[unit] = false; + texture->layerOriginal[unit] = false; + texture->cancelStoredSave(unit); unitsDropped.fetch_add(1, std::memory_order_relaxed); dropped = true; } - if (dropped) { - texture->originalValid = false; - ++discarded; - } + if (dropped) ++discarded; if (dropped && !texture->anyLayerPending()) { texture->dirty = false; it = pending.textures.erase(it); @@ -2931,7 +4499,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}; @@ -2955,7 +4523,8 @@ bool StorageTexture::clearByKeysFill(DccKeys keys, std::uint8_t key) { toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL; context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); recorder->EndGpuTiming(timing, guestBytes); - originalValid = false; + layerOriginal.assign(trackedLayers, false); + cancelStoredSaves(); uploadedKeys = keys; filledKeys = DccKeys::Uncompressed; keyProof = {}; @@ -2967,7 +4536,7 @@ bool StorageTexture::clearByKeysFill(DccKeys keys, std::uint8_t key) { return true; } -bool StorageTexture::FillClear(std::span pattern, std::uint32_t layer, const char*& refusal) { +bool StorageTexture::FillClear(std::span pattern, std::uint32_t layer, const char*& refusal, bool allowRegisterClear) { // Debug aid (APS5_TRACE_FILL_COVER=1): the first layer covers refused, with the surface, the // pattern and the generation the refusal was memoized at (ClassifyFill's layerRefusedGeneration). static const bool trace = std::getenv("APS5_TRACE_FILL_COVER") != nullptr; @@ -2985,7 +4554,8 @@ bool StorageTexture::FillClear(std::span pattern, std::u } } traceRefusal{*this, pattern, layer, refusal}; VkClearColorValue clearValue{}; - if (!FillClearColor(storageFormat, BytesPerElement(descriptor.format), pattern, clearValue)) { + const auto elementBytes = BytesPerElement(descriptor.format); + if (!FillClearColor(storageFormat, elementBytes, pattern, clearValue) && !(ClearKeepsDenormals(context, storageFormat) && FillClearColor(storageFormat, elementBytes, pattern, clearValue, true))) { refusal = "pattern"; return false; } @@ -2997,7 +4567,7 @@ bool StorageTexture::FillClear(std::span pattern, std::u // Under a clear code reads see that value whatever the texels hold, and keys a recorded // kernel still writes are not in the bytes yet: only keys that read as uncompressed now let // the image stand for the texels. - if (Recorder::SnapshotWriteOverlaps(descriptor.dccAddress, static_cast(guestBytes / 256))) { + if (!allowRegisterClear && Recorder::SnapshotWriteOverlaps(descriptor.dccAddress, static_cast(guestBytes / 256))) { refusal = "keys pending"; return false; } @@ -3006,10 +4576,6 @@ bool StorageTexture::FillClear(std::span pattern, std::u return false; } } - if (HostImportFor(context, descriptor.baseAddress, static_cast(guestBytes)) == nullptr) { - refusal = "not imported"; - return false; - } auto* recorder = Recorder::Active(); if (recorder == nullptr) { refusal = "no recorder"; @@ -3051,7 +4617,7 @@ bool StorageTexture::FillClear(std::span pattern, std::u std::lock_guard lock(live.mutex); for (auto* texture : live.textures) { if (texture == this || texture->released || !texture->overlaps(begin, bytes)) continue; - texture->originalValid = false; + texture->forgetSavedOver(begin, bytes); if (texture->descriptor.dccAddress != 0) aliasedKeys = true; } } @@ -3068,7 +4634,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}); @@ -3100,13 +4666,22 @@ bool StorageTexture::FillClear(std::span pattern, std::u // memo hit of this submission's epoch would not consume pages the game dirtied since that // walk, the write-back's own walk would stamp them above this generation and keep their // blocks, and the fill's pattern would be lost in them. - originalValid = false; + const bool watched = GuestMemory::Watched(descriptor.baseAddress, static_cast(guestBytes)); + if (watched) { + layerOriginal.assign(trackedLayers, false); + cancelStoredSaves(); + } uploadedKeys = DccKeys::Uncompressed; if (layer != WholeImage && trackedLayers > 1) { // The cleared layer's units alone: their stamps above lie below this generation, the other // units keep theirs (their pages are consumed by their own collects). const auto first = blockUnits ? static_cast((begin - descriptor.baseAddress) / trackedLayerBytes) : layer; const auto count = blockUnits ? static_cast(bytes / trackedLayerBytes) : 1u; + if (!watched) { + std::vector filled(trackedLayers, false); + std::fill_n(filled.begin() + first, count, true); + captureGuestBytes(&filled); + } const auto now = GuestMemory::CollectWritesUncached(begin, bytes); for (auto unit = first; unit < first + count; ++unit) layerGeneration[unit] = now; refreshGeneration(); @@ -3114,6 +4689,7 @@ bool StorageTexture::FillClear(std::span pattern, std::u markLayersPending(first, count); return true; } + if (!watched) captureGuestBytes(nullptr); layerGeneration.assign(trackedLayers, GuestMemory::CollectWritesUncached(descriptor.baseAddress, static_cast(guestBytes))); refreshGeneration(); forgetBorrowed(0, trackedLayers); @@ -3144,28 +4720,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(); @@ -3178,15 +4745,15 @@ bool StorageTexture::CopyFrom(StorageTexture& source, const char*& refusal) { auto& live = Live(); std::lock_guard lock(live.mutex); for (auto* texture : live.textures) { - if (texture != this && !texture->released && texture->overlaps(descriptor.baseAddress, static_cast(guestBytes))) texture->originalValid = false; + if (texture != this && !texture->released && texture->overlaps(descriptor.baseAddress, static_cast(guestBytes))) texture->forgetSavedOver(descriptor.baseAddress, static_cast(guestBytes)); } } GuestMemory::MarkWritten(descriptor.baseAddress, static_cast(guestBytes)); 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}); @@ -3226,16 +4793,25 @@ 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). - originalValid = false; + 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))); refreshGeneration(); forgetBorrowed(0, trackedLayers); MarkDirty(); - return true; } // See AdjacentGenerationEnabled above. @@ -3307,6 +4883,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; @@ -3324,7 +4998,9 @@ std::shared_ptr StorageTexture::pendingAlias() const { const auto& theirs = candidate->descriptor; // The same texels under another format of the same size: the device copy moves them bit // for bit, as the guest bytes would. - const bool sameShape = 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 && BytesPerElement(mine.format) == BytesPerElement(theirs.format) && geometry.imageLayers == candidate->geometry.imageLayers && geometry.imageDepth == candidate->geometry.imageDepth && trackedLayers == candidate->trackedLayers; + const auto oneSlice = [](const GuestTextureResource& resource) { return resource.dimension == TextureDimension::k2D || (resource.dimension == TextureDimension::k2DArray && resource.depthOrLastArray == 0); }; + const bool sameDimension = mine.dimension == theirs.dimension || (oneSlice(mine) && oneSlice(theirs)); + const bool sameShape = mine.width == theirs.width && mine.height == theirs.height && mine.depthOrLastArray == theirs.depthOrLastArray && mine.baseArray == theirs.baseArray && mine.mipCount == theirs.mipCount && mine.tileMode == theirs.tileMode && sameDimension && BytesPerElement(mine.format) == BytesPerElement(theirs.format) && geometry.imageLayers == candidate->geometry.imageLayers && geometry.imageDepth == candidate->geometry.imageDepth && trackedLayers == candidate->trackedLayers; if (sameShape && candidate->Cached() && candidate->uploadedKeys == DccKeys::Uncompressed) return candidate; } return nullptr; @@ -3356,8 +5032,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}}}; @@ -3446,6 +5122,20 @@ void StorageTexture::writeBack(std::uint64_t address, std::size_t bytes) { writeBackLayers(layers); } +bool StorageTexture::guestBytesSettled() const { + const std::array, 1> range{{{descriptor.baseAddress, descriptor.baseAddress + guestBytes}}}; + if (AnyShadowedOverlaps(range) || AnyPendingOverlaps(range)) return false; + const auto* recorder = Recorder::Active(); + return recorder == nullptr || !recorder->PendingWriteOverlaps(descriptor.baseAddress, static_cast(guestBytes)); +} + +bool StorageTexture::unchangedSinceBaseline(std::uint64_t from, std::uint64_t to) const { + const auto& baseline = originalHolds(from, static_cast(to - from)) ? original : generationBaseline; + if (baseline.size() != guestBytes) return false; + const auto offset = static_cast(from - descriptor.baseAddress); + return GuestMemory::EqualsCommittedUnsynced(from, std::span(baseline).subspan(offset, static_cast(to - from))); +} + void StorageTexture::writeBackLayers(const std::vector& layers) { CaptureTrace::Log("writeback image=%llx generation=%llu reason=%s units=%zu", static_cast(descriptor.baseAddress), static_cast(generation), flushReason, static_cast(std::count(layers.begin(), layers.end(), true))); static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; @@ -3493,12 +5183,32 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { } std::vector changedBlocks(spanBlocks); const bool tracked = GuestMemory::ChangedBlocks(firstStored, static_cast(lastStored - firstStored), generations, changedBlocks); - const bool compared = !tracked && compareUntracked(firstStored, static_cast(lastStored - firstStored), changedBlocks); + std::vector compared(trackedLayers, false); + if (!tracked) { + 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)) { + const auto from = descriptor.baseAddress + begin; + const auto last = descriptor.baseAddress + end - 1; + if (!compareUntracked(from, static_cast(end - begin), std::span(changedBlocks).subspan(static_cast((from - spanBegin) / block), static_cast(last / block - from / block + 1)))) continue; + for (auto layer = static_cast(begin / trackedLayerBytes); layer < trackedLayers && layerBegin(layer) <= last; ++layer) compared[layer] = true; + } + } + const auto forgetStored = [&] { + for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { + if (!layers[layer]) continue; + layerOriginal[layer] = false; + cancelStoredSave(layer); + } + }; + bool unsavedOutsideWatch = false; for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { if (!layers[layer]) continue; const auto begin = layerBegin(layer); const auto end = begin + layerBytes(layer); - if (!compared && (!tracked || layerGeneration[layer] == 0)) { + if (tracked ? layerGeneration[layer] == 0 : !compared[layer]) { + unsavedOutsideWatch = unsavedOutsideWatch || !tracked; keep.emplace_back(begin, end); continue; } @@ -3507,7 +5217,7 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { const auto to = std::min(at + block, end); if (from >= to) continue; const bool edge = from != at || to != at + block; - if (changedBlocks[static_cast((at - spanBegin) / block)] == GuestMemory::BlockWritten && (compared || !edge || GuestMemory::StoredOver(from, static_cast(to - from), layerGeneration[layer]))) { + if (keptAtUnwatchHolds(from, layer) || (changedBlocks[static_cast((at - spanBegin) / block)] == GuestMemory::BlockWritten && (compared[layer] || !edge || GuestMemory::StoredOver(from, static_cast(to - from), layerGeneration[layer])) && !unchangedSinceBaseline(from, to))) { skippedAny = true; skippedLayer[layer] = true; ++skipped; @@ -3517,6 +5227,9 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { else keep.emplace_back(from, to); } } + for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { + if (layers[layer]) forgetKeptAtUnwatch(layer); + } // Debug aid: APS5_TRACE_FLUSH also names the blocks a write-back leaves to the CPU. static const bool traceKept = std::getenv("APS5_TRACE_FLUSH") != nullptr; if (traceKept && skippedAny) { @@ -3536,46 +5249,60 @@ 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] = now; } + 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 // store, the keys stay as they are, and the image is stale there (re-uploaded at its next // use, never from a matching `original`). emptyWriteBacks.fetch_add(1, std::memory_order_relaxed); - originalValid = false; + forgetStored(); settle(true); return; } - if (const auto* import = HostImportFor(context, descriptor.baseAddress, static_cast(guestBytes))) { + 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(); - originalValid = false; + forgetStored(); traceKeyStore("block write-back", descriptor, guestBytes); - if (!IsDccClear(filledKeys)) MarkDccUncompressed(context, descriptor.dccAddress, guestBytes, DccKeyCount(descriptor, guestBytes)); + if (!IsDccClear(filledKeys) && uploadedKeys != DccKeys::Uncompressed) MarkDccUncompressed(context, descriptor.dccAddress, guestBytes, DccKeyCount(descriptor, guestBytes)); uploadedKeys = DccKeys::Uncompressed; for (const auto& [from, to] : keep) GuestMemory::MarkWritten(from, static_cast(to - from)); settle(true); + commitStoredSave(saveUnits); ++Profile().storageDirectWriteBacks; return; } @@ -3590,7 +5317,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; @@ -3603,11 +5330,11 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { recorder->FlushStoresOverlapping(firstStored, static_cast(lastStored - firstStored)); commands = recorder->Commands(); timing = recorder->BeginGpuTiming(Recorder::CommandClass::StorageWriteBack); - recorder->Keep(linear, linear->Size()); + if (linear) recorder->Keep(linear, linear->Size()); 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}; @@ -3617,7 +5344,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; @@ -3648,15 +5374,26 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { for (std::uint32_t level = 0; level < mips.size(); ++level) { if (!geometry.HasLayer(level, layer)) continue; const auto& mip = mips[level]; - detiler.Dispatch(commands, descriptor.tileMode, elementBytes, linear->Handle(), geometry.LinearLayerOffset(layer) + mip.linearOffset, tiledScratch->Handle(), geometry.GuestLayerOffset(layer) + mip.tiledOffset, mip, true, layer, geometry.thick); + detiler.Dispatch(commands, descriptor.tileMode, elementBytes, linear->Handle(), geometry.LinearLayerOffset(layer) + mip.linearOffset, tiledScratch->Handle(), geometry.GuestLayerOffset(layer) + mip.tiledOffset, mip, true, layer, geometry.thick, {.pipeBankXor = descriptor.pipeBankXor}); } } { 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; @@ -3675,18 +5412,19 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { countStorageWriteBack(storedBytes, true); if (profile) Profile().storageGpu += timer.lap(); // The guest bytes now differ from `original`; other caches of the range see the write. - originalValid = false; + forgetStored(); // The keys go the same way as the texels: a fill recorded behind the retile when the // metadata is host-imported (no CPU wait for the title's key-writing kernels), else a CPU // store (APS5_CPU_DCC_KEYS=1 keeps the CPU store; see DccMetadata.hpp). traceKeyStore("layer write-back", descriptor, guestBytes); - if (!IsDccClear(filledKeys)) MarkDccUncompressed(context, descriptor.dccAddress, guestBytes, DccKeyCount(descriptor, guestBytes)); + if (!IsDccClear(filledKeys) && uploadedKeys != DccKeys::Uncompressed) MarkDccUncompressed(context, descriptor.dccAddress, guestBytes, DccKeyCount(descriptor, guestBytes)); uploadedKeys = DccKeys::Uncompressed; for (const auto& [from, to] : keep) GuestMemory::MarkWritten(from, static_cast(to - from)); // The walk covers this surface's pages only: an adjacent image's later CPU write is stamped // 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; } @@ -3695,22 +5433,49 @@ 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); - originalValid = false; + forgetStored(); for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { if (layers[layer]) layerPending[layer] = false; } return; } + original.resize(static_cast(guestBytes)); // The store below compares against the guest bytes, read now when `original` does not hold // them (a GPU clear, a dropped or kept unit). Reading them says nothing about the image: a // stale unit (dropped, or kept for the CPU) still differs from them, so `original` vouches for // the content again only where it did before, or once every unit is stored. - const bool wasValid = originalValid; - if (!wasValid) GuestMemory::ReadCommitted(descriptor.baseAddress, original); + Require(!unsavedOutsideWatch, "storing storage image results pending over memory outside the write watch without the guest bytes saved before them is not implemented"); + std::vector unsaved = layerOriginal; + unsaved.flip(); + for (const auto& [begin, end] : unitRuns(unsaved)) { + 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); @@ -3741,18 +5506,18 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { for (std::uint32_t level = 0; level < mips.size(); ++level) { if (!geometry.HasLayer(level, layer)) continue; const auto& mip = mips[level]; - detiler.Dispatch(commands, descriptor.tileMode, elementBytes, linear.Handle(), geometry.LinearLayerOffset(layer) + mip.linearOffset, tiled.Handle(), geometry.GuestLayerOffset(layer) + mip.tiledOffset, mip, true, layer, geometry.thick); + detiler.Dispatch(commands, descriptor.tileMode, elementBytes, linear.Handle(), geometry.LinearLayerOffset(layer) + mip.linearOffset, tiled.Handle(), geometry.GuestLayerOffset(layer) + mip.tiledOffset, mip, true, layer, geometry.thick, {.pipeBankXor = descriptor.pipeBankXor}); } } - // Debug aid: APS5_DUMP_STORAGE= saves that storage image's first mip after each of - // its first 8 write-backs as storage_
_.raw (u32 width, height, VkFormat, then rows). - static const std::uint64_t dumpAddress = [] { const char* text = std::getenv("APS5_DUMP_STORAGE"); return text ? std::strtoull(text, nullptr, 16) : 0ull; }(); + const bool dumpSelected = StorageDumpSelects(descriptor); static int dumps = 0; std::unique_ptr dump; - if (dumpAddress != 0 && descriptor.baseAddress == dumpAddress && dumps < 8) { - dump = std::make_unique(context, static_cast(mips[0].linearSize), VK_BUFFER_USAGE_TRANSFER_DST_BIT); + if (dumpSelected && dumps < 8) { + dump = std::make_unique(context, static_cast(mips[0].linearSize * arrayLayers), VK_BUFFER_USAGE_TRANSFER_DST_BIT); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); - CopyBuffer(context, commands, linear.Handle(), mips[0].linearOffset, dump->Handle(), 0, mips[0].linearSize); + for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) { + if (geometry.HasLayer(0, layer)) CopyBuffer(context, commands, linear.Handle(), geometry.LinearLayerOffset(layer) + mips[0].linearOffset, dump->Handle(), layer * mips[0].linearSize, mips[0].linearSize); + } } const auto toCopy = WholeBufferBarrier(tiled.Handle(), VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); VkImageMemoryBarrier backToGeneral = toSource; @@ -3790,11 +5555,15 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { // The texels now hold the whole image, so later reads must see them rather than a fast clear // (the keys may be host-imported although the texels were not: then a recorded fill, else a CPU store). traceKeyStore("cpu write-back", descriptor, guestBytes); - if (!IsDccClear(filledKeys)) MarkDccUncompressed(context, descriptor.dccAddress, guestBytes, DccKeyCount(descriptor, guestBytes)); + if (!IsDccClear(filledKeys) && uploadedKeys != DccKeys::Uncompressed) MarkDccUncompressed(context, descriptor.dccAddress, guestBytes, DccKeyCount(descriptor, guestBytes)); uploadedKeys = DccKeys::Uncompressed; // The store above is the only write to these pages, so `original` is current at a fresh // generation, unless blocks were kept for the CPU: then the image is stale there. - originalValid = !skippedAny && (wasValid || std::all_of(layers.begin(), layers.end(), [](bool selected) { return selected; })); + for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { + if (!layers[layer]) 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 // (the shared edge block included, undoing the advance), so the walk is made here and @@ -3833,6 +5602,10 @@ void StorageTexture::release() noexcept { atomicViews.clear(); for (const auto& [key, uint] : uintViews) context.Function("vkDestroyImageView")(context.device, uint, nullptr); uintViews.clear(); + if (elementView) context.Function("vkDestroyImageView")(context.device, elementView, nullptr); + elementView = VK_NULL_HANDLE; + for (const auto& [key, element] : elementLayerViews) context.Function("vkDestroyImageView")(context.device, element, nullptr); + elementLayerViews.clear(); for (const auto& [format, attachment] : attachmentViews) context.Function("vkDestroyImageView")(context.device, attachment, nullptr); attachmentViews.clear(); if (proxyView) context.Function("vkDestroyImageView")(context.device, proxyView, nullptr); @@ -3840,13 +5613,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 b065119d1a..74621bdba1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp @@ -1,11 +1,16 @@ #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp" #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/TextureDetileImage_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/TextureRepack_spv.h" #include #include #include #include +#include #include #include @@ -31,6 +36,7 @@ struct Push { std::uint32_t linearBase; std::uint32_t columnBegin; std::uint32_t rowBegin; + std::uint32_t pipeBankXor; }; std::uint32_t BlockBytesFor(TextureTileMode tileMode) { @@ -54,8 +60,8 @@ std::uint32_t BlockBytesFor(TextureTileMode tileMode) { throw std::runtime_error("AGC graphics: TextureDetiler encountered an unknown tile mode"); } -std::uint32_t PipelineKey(TextureTileMode tileMode, std::uint32_t elementBytes, bool retile, bool thick) { - return (thick ? 1u << 17 : 0u) | (retile ? 1u << 16 : 0u) | (static_cast(tileMode) << 8) | elementBytes; +std::uint32_t PipelineKey(TextureTileMode tileMode, std::uint32_t elementBytes, bool retile, bool thick, bool image) { + return (image ? 1u << 18 : 0u) | (thick ? 1u << 17 : 0u) | (retile ? 1u << 16 : 0u) | (static_cast(tileMode) << 8) | elementBytes; } } @@ -83,10 +89,11 @@ 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; + Check(context.Function("vkCreatePipelineLayout")(context.device, &pipelineLayoutInfo, nullptr, &imagePipelineLayout), "vkCreatePipelineLayout"); } catch (...) { release(); throw; @@ -98,16 +105,43 @@ TextureDetiler::~TextureDetiler() { } void TextureDetiler::release() noexcept { - for (const auto pool : descriptorPools) 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 (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 : descriptorSets.pools) context.Function("vkDestroyDescriptorPool")(context.device, pool, nullptr); + for (const auto pool : imageDescriptorSets.pools) context.Function("vkDestroyDescriptorPool")(context.device, pool, 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); if (module) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); if (pipelineLayout) context.Function("vkDestroyPipelineLayout")(context.device, pipelineLayout, nullptr); if (descriptorLayout) context.Function("vkDestroyDescriptorSetLayout")(context.device, descriptorLayout, nullptr); } -VkPipeline TextureDetiler::pipeline(TextureTileMode tileMode, std::uint32_t elementBytes, bool retile, bool thick) { +VkFormat TextureDetiler::ImageElementFormat(std::uint32_t elementBytes) { + switch (elementBytes) { + case 1: return VK_FORMAT_R8_UINT; + case 2: return VK_FORMAT_R16_UINT; + case 4: return VK_FORMAT_R32_UINT; + case 8: return VK_FORMAT_R32G32_UINT; + case 16: return VK_FORMAT_R32G32B32A32_UINT; + default: return VK_FORMAT_UNDEFINED; + } +} + +VkPipeline TextureDetiler::pipeline(TextureTileMode tileMode, std::uint32_t elementBytes, bool retile, bool thick, bool image) { Require(std::has_single_bit(elementBytes) && elementBytes <= 16u, "unsupported element size for texture detiling"); - const auto key = PipelineKey(tileMode, elementBytes, retile, thick); + const auto key = PipelineKey(tileMode, elementBytes, retile, thick, image); for (const auto& entry : pipelines) { if (entry.first == key) return entry.second; } @@ -115,8 +149,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 : 0u; - Require(thickMode != 0, "3D textures are only detiled thick from SW_4KB_S, SW_64KB_S or SW_64KB_S_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; @@ -130,6 +164,9 @@ VkPipeline TextureDetiler::pipeline(TextureTileMode tileMode, std::uint32_t elem values[2] = 2u; std::copy(equation->bits.begin(), equation->bits.end(), values.begin() + 4); } + if (image && imageModule == VK_NULL_HANDLE) { + 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}; VkSpecializationInfo specialization{}; @@ -139,27 +176,35 @@ VkPipeline TextureDetiler::pipeline(TextureTileMode tileMode, std::uint32_t elem specialization.pData = values.data(); VkPipelineShaderStageCreateInfo stage{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; - stage.module = module; + stage.module = image ? imageModule : module; stage.pName = "main"; stage.pSpecializationInfo = &specialization; VkComputePipelineCreateInfo createInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; createInfo.stage = stage; - createInfo.layout = pipelineLayout; + createInfo.layout = image ? imagePipelineLayout : pipelineLayout; VkPipeline result = VK_NULL_HANDLE; - Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &createInfo, nullptr, &result), "vkCreateComputePipelines"); + { + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &createInfo, nullptr, &result), "vkCreateComputePipelines"); + } pipelines.emplace_back(key, result); return result; } +void TextureDetiler::checkWindow(const TileMipLayout& layout, const DetileWindow& window, std::uint32_t& columnEnd, std::uint32_t& rowEnd) const { + Require(layout.width != 0 && layout.height != 0, "texture detiling requires a non-empty mip layout"); + Require(layout.tiledSize != 0 && layout.linearSize != 0, "texture detiling requires a non-empty mip layout"); + columnEnd = window.columnEnd != 0 ? std::min(window.columnEnd, layout.width) : layout.width; + rowEnd = window.rowEnd != 0 ? std::min(window.rowEnd, layout.height) : layout.height; + if (!(window.columnBegin < columnEnd && window.rowBegin < rowEnd)) Require(false, "texture detiling window lies outside the mip: columns " + std::to_string(window.columnBegin) + ".." + std::to_string(columnEnd) + ", rows " + std::to_string(window.rowBegin) + ".." + std::to_string(rowEnd) + " of " + std::to_string(layout.width) + "x" + std::to_string(layout.height)); +} + void TextureDetiler::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, std::uint32_t slice, bool thick, 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"); Require(source != VK_NULL_HANDLE && destination != VK_NULL_HANDLE, "texture detiling requires source and destination buffers"); - Require(layout.width != 0 && layout.height != 0, "texture detiling requires a non-empty mip layout"); - Require(layout.tiledSize != 0 && layout.linearSize != 0, "texture detiling requires a non-empty mip layout"); - const auto columnEnd = window.columnEnd != 0 ? std::min(window.columnEnd, layout.width) : layout.width; - const auto rowEnd = window.rowEnd != 0 ? std::min(window.rowEnd, layout.height) : layout.height; - if (!(window.columnBegin < columnEnd && window.rowBegin < rowEnd)) Require(false, "texture detiling window lies outside the mip: columns " + std::to_string(window.columnBegin) + ".." + std::to_string(columnEnd) + ", rows " + std::to_string(window.rowBegin) + ".." + std::to_string(rowEnd) + " of " + std::to_string(layout.width) + "x" + std::to_string(layout.height)); + std::uint32_t columnEnd = 0, rowEnd = 0; + checkWindow(layout, window, columnEnd, rowEnd); const auto target = pipeline(tileMode, elementBytes, retile, thick); const auto alignment = std::max(context.limits.minStorageBufferOffsetAlignment, 4); const auto sourceDescriptorOffset = sourceOffset - sourceOffset % alignment; @@ -211,10 +256,201 @@ void TextureDetiler::Dispatch(VkCommandBuffer commands, TextureTileMode tileMode push.linearBase = window.linearBase; push.columnBegin = window.columnBegin; push.rowBegin = window.rowBegin; + push.pipeBankXor = window.pipeBankXor; context.Function("vkCmdPushConstants")(commands, pipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(Push), &push); const auto groupsX = (columnEnd - window.columnBegin + 7u) / 8u; const auto groupsY = (rowEnd - window.rowBegin + 7u) / 8u; 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; + std::memcpy(&codes, cmaskErrors->Bytes().data(), sizeof(codes)); + return codes; +} + +void TextureDetiler::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) { + Require(commands != VK_NULL_HANDLE && cmask != VK_NULL_HANDLE && view != VK_NULL_HANDLE, "a CMASK clear needs a command buffer, the CMASK buffer and the target view"); + Require(elementBytes == 1 || elementBytes == 2 || elementBytes == 4 || elementBytes == 8, "a CMASK clear of this element size is not modeled"); + Require(cmaskOffset % 4 == 0 && cmaskBytes % 4 == 0 && cmaskBytes != 0, "a CMASK clear needs a word-aligned CMASK"); + if (cmaskPipeline == VK_NULL_HANDLE) { + std::array bindings{}; + for (std::uint32_t index = 0; index < bindings.size(); ++index) { + bindings[index].binding = index; + bindings[index].descriptorType = index == 2 ? VK_DESCRIPTOR_TYPE_STORAGE_IMAGE : VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + bindings[index].descriptorCount = 1; + bindings[index].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + } + 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, &cmaskDescriptorLayout), "vkCreateDescriptorSetLayout CMASK clear"); + const VkPushConstantRange pushRange{VK_SHADER_STAGE_COMPUTE_BIT, 0, 7 * sizeof(std::uint32_t)}; + VkPipelineLayoutCreateInfo pipelineLayoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + pipelineLayoutInfo.setLayoutCount = 1; + pipelineLayoutInfo.pSetLayouts = &cmaskDescriptorLayout; + pipelineLayoutInfo.pushConstantRangeCount = 1; + pipelineLayoutInfo.pPushConstantRanges = &pushRange; + Check(context.Function("vkCreatePipelineLayout")(context.device, &pipelineLayoutInfo, nullptr, &cmaskPipelineLayout), "vkCreatePipelineLayout 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; + pipelineInfo.stage.module = cmaskModule; + pipelineInfo.stage.pName = "main"; + pipelineInfo.layout = cmaskPipelineLayout; + VkResult created; + { + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + created = context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &cmaskPipeline); + } + context.Function("vkDestroyShaderModule")(context.device, cmaskModule, nullptr); + Check(created, "vkCreateComputePipelines CMASK clear"); + cmaskErrors = std::make_unique(context, 4, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + std::memset(cmaskErrors->Bytes().data(), 0, 4); + } + 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; + const VkDescriptorBufferInfo cmaskInfo{cmask, descriptorOffset, base + cmaskBytes}; + const VkDescriptorBufferInfo errorInfo{cmaskErrors->Handle(), 0, 4}; + const VkDescriptorImageInfo imageInfo{VK_NULL_HANDLE, view, 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 == 2 ? VK_DESCRIPTOR_TYPE_STORAGE_IMAGE : VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + } + writes[0].pBufferInfo = &cmaskInfo; + writes[1].pBufferInfo = &errorInfo; + writes[2].pImageInfo = &imageInfo; + 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_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_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_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::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"); + Require(tiled != VK_NULL_HANDLE && view != VK_NULL_HANDLE, "texture detiling requires a tiled buffer and an image view"); + Require(ImageElementFormat(elementBytes) != VK_FORMAT_UNDEFINED, "unsupported element size for texture detiling through an image"); + std::uint32_t columnEnd = 0, rowEnd = 0; + checkWindow(layout, window, columnEnd, rowEnd); + const auto target = pipeline(tileMode, elementBytes, retile, false, true); + const auto alignment = std::max(context.limits.minStorageBufferOffsetAlignment, 4); + const auto descriptorOffset = tiledOffset - tiledOffset % alignment; + const auto base = tiledOffset - descriptorOffset; + Require(base <= UINT32_MAX, "texture detiling buffer offset exceeds addressable range"); + const std::uint64_t tiledBytes = window.rangeEnd == 0xffffffffu ? layout.tiledSize : std::min(layout.tiledSize, window.rangeEnd) - window.tiledBase; + const auto range = (base + tiledBytes + 3) / 4 * 4; + Require(range <= context.limits.maxStorageBufferRange, "texture detiling buffer range exceeds device limits"); + const auto set = allocateSet(true); + const VkDescriptorBufferInfo bufferInfo{tiled, descriptorOffset, range}; + const VkDescriptorImageInfo imageInfo{VK_NULL_HANDLE, view, VK_IMAGE_LAYOUT_GENERAL}; + std::array writes{}; + writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[0].dstSet = set; + writes[0].dstBinding = 0; + writes[0].descriptorCount = 1; + writes[0].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + writes[0].pBufferInfo = &bufferInfo; + writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[1].dstSet = set; + writes[1].dstBinding = 1; + writes[1].descriptorCount = 1; + writes[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + writes[1].pImageInfo = &imageInfo; + context.Function("vkUpdateDescriptorSets")(context.device, static_cast(writes.size()), writes.data(), 0, nullptr); + context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, target); + context.Function("vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, imagePipelineLayout, 0, 1, &set, 0, nullptr); + Push push{}; + push.srcBase = static_cast(retile ? 0 : base); + push.dstBase = static_cast(retile ? base : 0); + push.width = layout.width; + push.height = layout.height; + push.pitchBytes = layout.pitchBytes; + push.blocksPerRow = layout.blocksPerRow; + push.tail = layout.tail ? 1u : 0u; + push.tailX = layout.tailX; + push.tailY = layout.tailY; + push.elementBytes = elementBytes; + push.slice = slice; + push.rangeBegin = window.rangeBegin; + push.rangeEnd = window.rangeEnd; + push.tiledBase = window.tiledBase; + push.linearBase = 0; + push.columnBegin = window.columnBegin; + push.rowBegin = window.rowBegin; + push.pipeBankXor = window.pipeBankXor; + context.Function("vkCmdPushConstants")(commands, imagePipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(Push), &push); + const auto groupsX = (columnEnd - window.columnBegin + 7u) / 8u; + const auto groupsY = (rowEnd - window.rowBegin + 7u) / 8u; + context.Function("vkCmdDispatch")(commands, groupsX, groupsY, 1); +} + } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetilerDescriptors.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetilerDescriptors.cpp index ed3b905e4f..5f78f5729c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetilerDescriptors.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetilerDescriptors.cpp @@ -1,42 +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; + resetRing(descriptorSets, "vkResetDescriptorPool texture detiler"); + resetRing(cmaskDescriptorSets, "vkResetDescriptorPool CMASK clear"); + resetRing(imageDescriptorSets, "vkResetDescriptorPool texture detiler"); + resetRing(readbackDescriptorSets, "vkResetDescriptorPool multisample readback"); } -VkDescriptorSet TextureDetiler::allocateSet() { +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; - const auto poolIndex = allocatedSets / setsPerPool; - if (poolIndex == descriptorPools.size()) { - const VkDescriptorPoolSize size{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, setsPerPool * 2}; - VkDescriptorPoolCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; - info.maxSets = setsPerPool; - info.poolSizeCount = 1; - info.pPoolSizes = &size; - descriptorPools.reserve(descriptorPools.size() + 1); - VkDescriptorPool pool = VK_NULL_HANDLE; - Check(context.Function("vkCreateDescriptorPool")(context.device, &info, nullptr, &pool), "vkCreateDescriptorPool texture detiler"); - descriptorPools.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 = descriptorPools.at(poolIndex); + allocation.descriptorPool = ring.pools[ring.current]; allocation.descriptorSetCount = 1; - allocation.pSetLayouts = &descriptorLayout; + allocation.pSetLayouts = &layout; VkDescriptorSet set = VK_NULL_HANDLE; - Check(context.Function("vkAllocateDescriptorSets")(context.device, &allocation, &set), "vkAllocateDescriptorSets texture detiler"); - ++allocatedSets; + 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 ad66fdbaba..e667909a5b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp @@ -20,6 +20,7 @@ struct FormatEntry { constexpr FormatEntry kFormatLookup[] = { {1, VK_FORMAT_R8_UNORM, 1, false}, + {2, VK_FORMAT_R8_SNORM, 1, false}, {5, VK_FORMAT_R8_UINT, 1, false}, {6, VK_FORMAT_R8_SINT, 1, false}, {7, VK_FORMAT_R16_UNORM, 2, false}, @@ -41,7 +42,9 @@ constexpr FormatEntry kFormatLookup[] = { {29, VK_FORMAT_R16G16_SFLOAT, 4, false}, {36, VK_FORMAT_B10G11R11_UFLOAT_PACK32, 4, false}, {50, VK_FORMAT_A2B10G10R10_UNORM_PACK32, 4, false}, + {51, VK_FORMAT_A2B10G10R10_SNORM_PACK32, 4, false}, {54, VK_FORMAT_A2B10G10R10_UINT_PACK32, 4, false}, + {55, VK_FORMAT_A2B10G10R10_SINT_PACK32, 4, false}, {56, VK_FORMAT_R8G8B8A8_UNORM, 4, false}, {57, VK_FORMAT_R8G8B8A8_SNORM, 4, false}, {60, VK_FORMAT_R8G8B8A8_UINT, 4, false}, @@ -66,7 +69,7 @@ constexpr FormatEntry kFormatLookup[] = { {132, VK_FORMAT_E5B9G9R9_UFLOAT_PACK32, 4, false}, {133, VK_FORMAT_R5G6B5_UNORM_PACK16, 2, false}, {134, VK_FORMAT_A1R5G5B5_UNORM_PACK16, 2, false}, - {136, VK_FORMAT_R4G4B4A4_UNORM_PACK16, 2, false}, + {136, VK_FORMAT_B4G4R4A4_UNORM_PACK16, 2, false}, {169, VK_FORMAT_BC1_RGBA_UNORM_BLOCK, 8, true}, {170, VK_FORMAT_BC1_RGBA_SRGB_BLOCK, 8, true}, {171, VK_FORMAT_BC2_UNORM_BLOCK, 16, true}, @@ -114,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); @@ -128,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: @@ -146,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; @@ -153,6 +185,21 @@ std::optional FindGuestTextureFormat(VkFormat format, std::uint32 return std::nullopt; } +VkComponentSwizzle TextureComponentChannel(std::uint32_t guestFormat, VkComponentSwizzle component) { + if (component < VK_COMPONENT_SWIZZLE_R || component > VK_COMPONENT_SWIZZLE_A) return component; + const auto index = static_cast(component - VK_COMPONENT_SWIZZLE_R); + constexpr std::array bgrb{VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_B}; + constexpr std::array bgra{VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_A}; + constexpr std::array argb{VK_COMPONENT_SWIZZLE_A, VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B}; + switch (guestFormat) { + case 133: return bgrb[index]; + case 134: return bgra[index]; + case 136: return argb[index]; + case 43: return bgra[index]; + default: return component; + } +} + std::uint32_t BytesPerElement(std::uint32_t guestFormat) { return findFormatEntry(guestFormat).bytesPerElement; } @@ -161,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 bf4c3f4f42..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 @@ -116,6 +117,10 @@ bool GetMipTailLayout(TextureTileMode tileMode, const BlockLayout& block, std::u case TextureTileMode::kD64KBX: case TextureTileMode::kR64KBX: out = MakeMipTailLayout(kMipTailThin64KB[index], block.blockWidth >> 1u, block.blockHeight); + if (tileMode == TextureTileMode::kZ64KBX && bytesPerElement <= 2u) { + out.widthLimit >>= 1u; + if (bytesPerElement == 1u) out.heightLimit >>= 1u; + } return true; } throw std::runtime_error("AGC graphics: GetMipTailLayout encountered an unknown tile mode"); @@ -258,10 +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: 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 and SW_64KB_S_X, not in swizzle mode " + std::to_string(EquationSwizzleMode(tileMode)) + " at " + std::to_string(bytesPerElement) + " bytes per element"); + 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_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"); } } @@ -311,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]; @@ -377,7 +384,14 @@ ThickLayout ComputeThickLayout(TextureTileMode tileMode, std::uint32_t format, s SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor) { SurfaceGeometry geometry; - if (descriptor.dimension == TextureDimension::k3D && (descriptor.tileMode == TextureTileMode::kZ64KBX || descriptor.tileMode == TextureTileMode::kD64KBX || descriptor.tileMode == TextureTileMode::kR64KBX)) { + 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); geometry.layers = depth; @@ -400,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; @@ -429,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 06a6c5c04d..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; } @@ -736,7 +739,7 @@ void MarkShadowed(const HostImport& import, std::span range StorageTexture::BumpPendingSerial(); } -void RetireShadow(const Context& context, const HostImport& import, const std::function& registered) { +void RetireShadow(VkDevice device, const HostImport& import, const std::function& registered) { if (!UnitShadowEnabled()) return; auto& registry = Registry(); std::shared_ptr shadow; @@ -766,7 +769,7 @@ void RetireShadow(const Context& context, const HostImport& import, const std::f if (dropped != 0) Stats().droppedOnRetire.fetch_add(dropped, std::memory_order_relaxed); std::size_t published = 0; if (any) { - if (GuestMemory::GpuMutex().HeldByThisThread() && shadow->context.device == context.device) { + if (GuestMemory::GpuMutex().HeldByThisThread() && shadow->context.device == device) { published = publishUnits(shadow, 0, shadow->Units() - 1, selected, PublishReason::Retire); } else { Stats().lostOnRetire.fetch_add(shadow->liveUnits, std::memory_order_relaxed); diff --git a/core/libs/prx/libSceAgcDriver/PostCompile.cmake b/core/libs/prx/libSceAgcDriver/PostCompile.cmake index 3cabf5ccd3..736f3207fd 100644 --- a/core/libs/prx/libSceAgcDriver/PostCompile.cmake +++ b/core/libs/prx/libSceAgcDriver/PostCompile.cmake @@ -6,29 +6,29 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}" COMMAND "$" -V --target-env vulkan1.1 -o "${agcColorTransferSpv}" "${agcColorTransferSource}" COMMAND ${CMAKE_COMMAND} -DINPUT=${agcColorTransferSpv} -DOUTPUT=${agcColorTransferHeader} -DSYMBOL=COLOR_TRANSFER_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" - DEPENDS "${agcColorTransferSource}" glslang-standalone + DEPENDS "${agcColorTransferSource}" "${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/PqToSdr.glsl" glslang-standalone VERBATIM ) -foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests) +foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test) if(TARGET ${agcTarget}) target_sources(${agcTarget} PRIVATE Graphics/src/BufferPool.cpp) endif() endforeach() -foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test agc_driver_graphics_tests agc_driver_mesh_tests) +foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test) if(TARGET ${agcTarget}) target_sources(${agcTarget} PRIVATE Graphics/src/TextureDetilerDescriptors.cpp Graphics/src/TextureCache.cpp) endif() endforeach() -foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests) +foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test) if(TARGET ${agcTarget}) target_sources(${agcTarget} PRIVATE Graphics/src/GpuColorTransfer.cpp ${agcColorTransferHeader}) endif() endforeach() -target_sources(agc_driver_bda_device_tests PRIVATE tests/ColorTransferTests.cpp Graphics/src/ColorTargetLayout.cpp Execution/src/GuestMemory.cpp) +target_sources(agc_driver_bda_device_tests PRIVATE tests/ColorTransferTests.cpp) target_include_directories(agc_driver_bda_device_tests PRIVATE ${agcTextureDetileGeneratedRoot}) if(TARGET agc_driver_visual_test) diff --git a/core/libs/prx/libSceAgcDriver/Resource/include/Registration.hpp b/core/libs/prx/libSceAgcDriver/Resource/include/Registration.hpp index bcf672d320..d30f27839a 100644 --- a/core/libs/prx/libSceAgcDriver/Resource/include/Registration.hpp +++ b/core/libs/prx/libSceAgcDriver/Resource/include/Registration.hpp @@ -1,4 +1,8 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_RESOURCE_INCLUDE_REGISTRATION_HPP #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_RESOURCE_INCLUDE_REGISTRATION_HPP +#include + +extern "C" bool AgcDriverWorkloadStreamRegistered_nid_postfix(std::uint32_t streamId); + #endif diff --git a/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp b/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp index ff798fca23..f3fbd0ba67 100644 --- a/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp +++ b/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp @@ -2,15 +2,27 @@ #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" // Resource registration feeds GPU debugging tools, none of which are attached; the game treats this code as benign. static constexpr int SCE_AGC_ERROR_RESOURCE_REGISTRATION_UNAVAILABLE = static_cast(0x8A6C9018); static constexpr uint32_t RESOURCE_REGISTRATION_MAX_NAME_LENGTH = 0xfc; +static constexpr uint32_t WORKLOAD_STREAM_MIN_ID = 1; +static constexpr uint32_t WORKLOAD_STREAM_MAX_ID = 31; + +static std::mutex g_workloadStreamLock; +static uint32_t g_workloadStreams = 0; extern "C" { +bool AgcDriverWorkloadStreamRegistered_nid_postfix(uint32_t streamId) { + if (streamId < WORKLOAD_STREAM_MIN_ID || streamId > WORKLOAD_STREAM_MAX_ID) return false; + std::lock_guard lock(g_workloadStreamLock); + return (g_workloadStreams & (1u << streamId)) != 0; +} + int APS5_VABI sceAgcDriverRegisterOwner(uint32_t* owner_handle, const char* name) { (void)owner_handle; (void)name; @@ -28,9 +40,19 @@ int APS5_VABI sceAgcDriverRegisterResource(uint32_t* resource_handle, uint32_t o return SCE_AGC_ERROR_RESOURCE_REGISTRATION_UNAVAILABLE; } +int APS5_VABI sceAgcDriverRegisterMultipleResources(uint32_t* resourceHandle, uint32_t ownerHandle, const void* resources, uint32_t resourceCount) { + (void)resourceHandle; + (void)ownerHandle; + (void)resources; + (void)resourceCount; + return SCE_AGC_ERROR_RESOURCE_REGISTRATION_UNAVAILABLE; +} + int APS5_VABI sceAgcDriverRegisterWorkloadStream(uint32_t stream_id, const void* stream) { - (void)stream_id; - (void)stream; + if (stream != nullptr && stream_id >= WORKLOAD_STREAM_MIN_ID && stream_id <= WORKLOAD_STREAM_MAX_ID) { + std::lock_guard lock(g_workloadStreamLock); + g_workloadStreams |= 1u << stream_id; + } return 0; } @@ -61,7 +83,10 @@ int APS5_VABI sceAgcDriverUnregisterAllResourcesForOwner(uint32_t owner_handle) } int APS5_VABI sceAgcDriverUnregisterWorkloadStream(uint32_t stream_id) { - (void)stream_id; + if (stream_id >= WORKLOAD_STREAM_MIN_ID && stream_id <= WORKLOAD_STREAM_MAX_ID) { + std::lock_guard lock(g_workloadStreamLock); + g_workloadStreams &= ~(1u << stream_id); + } return 0; } @@ -125,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/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 9344228717..be639faf70 100644 --- a/core/libs/prx/libSceAgcDriver/Unimplemented.cpp +++ b/core/libs/prx/libSceAgcDriver/Unimplemented.cpp @@ -1,48 +1,217 @@ #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; + +} + extern "C" { int APS5_VABI sceAgcDriverGetShaderDebuggingStatus() { - NotImplemented_nid_no_patch(__func__); - return 0; + return 1; } -int APS5_VABI sceAgcDriverRegisterMultipleResources() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAgcDriverRegisterDefaultOwner(uint32_t owner) { + (void)owner; + return 0; } -int APS5_VABI sceAgcDriverRegisterDefaultOwner(uint32_t* owner_handle) { - (void)owner_handle; - return static_cast(0x8A6C9018); +int APS5_VABI sceAgcDriverSetValidationErrorOutputFrequency(uint32_t frequency) { + (void)frequency; + return DEBUG_UNAVAILABLE; } -int APS5_VABI sceAgcDriverSetValidationErrorOutputFrequency() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAgcDriverSetSubmitValidationMode(uint32_t mode) { + (void)mode; + return DEBUG_UNAVAILABLE; } -int APS5_VABI sceAgcDriverSetSubmitValidationMode() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAgcDriverGetSubmitValidationConfig(void* config) { + (void)config; + return DEBUG_UNAVAILABLE; } -int APS5_VABI sceAgcDriverGetSubmitValidationConfig() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAgcDriverGetSubmitValidationMode(uint32_t* mode) { + (void)mode; + return DEBUG_UNAVAILABLE; } -int APS5_VABI sceAgcDriverGetSubmitValidationMode() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAgcDriverSetSubmitValidationConfig(const void* config) { + (void)config; + return DEBUG_UNAVAILABLE; } -int APS5_VABI sceAgcDriverSetSubmitValidationConfig() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAgcDriverGetHsOffchipParam(uint16_t* first, uint16_t* second) { + (void)first; + (void)second; + NotImplemented_nid_no_patch(__func__); + return 0; +} + + +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; +} + +int APS5_VABI sceAgcDriverGetCoredumpAddress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +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 sceAgcDriverNopDataWritePacket() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverReleaseHDRScopesACQ() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSetTargetRingForDiag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +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 3af9d345d9..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}; @@ -41,7 +178,7 @@ void RunBdaContractTests() { ValidateBdaTarget(program, target); AgcDriver::Graphics::Require(!BdaInvocationsMayStop(program), "a BDA program with barriers must keep its invocations running"); program.Resources().stage = IrShaderStage::TessellationControl; - reject([&] { ValidateBdaTarget(program, target); }, "barrier-safe"); + ValidateBdaTarget(program, target); program.Resources().stage = IrShaderStage::Compute; RecompileRequest request{}; request.target.bdaAbiVersion = BdaAbi::Version; @@ -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 8d73dada82..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,21 +232,31 @@ 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}; Require(buffer.Bytes()[255] == std::byte{0x5a}, "real BDA buffer mapping failed"); if (device->RunsOnCpu()) { - std::cout << "CPU Vulkan device: BDA execution not tested\n"; + std::cout << "CPU Vulkan device: BDA read and multi-dword write execution not tested\n"; } else { RunBdaExecutionTests(device->GetContext()); } + RunBdaStoreExecutionTests(device->GetContext()); + std::cout << "BDA store boundary tests passed\n"; RunColorTransferTests(device->GetContext()); std::cout << "Vulkan BDA allocation and execution tests passed\n"; return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp index 3361f71093..098d311465 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp @@ -1,12 +1,27 @@ #include "BdaShader.hpp" #include "BdaAbi.hpp" +#include "IntermediateRepresentation/IrMetadata/ShaderStage.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include #include +#include #include +#include #include +#include +#include +#include +#include +#include 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 { @@ -87,6 +102,306 @@ private: } +static void RunWrittenRangeDecodeTests() { + namespace Abi = ShaderRecompiler::BdaAbi; + std::vector words(Abi::FaultBufferBytes / sizeof(std::uint32_t)); + std::vector> marked; + const auto decode = [&] { + marked.clear(); + return Abi::ForEachWrittenRange(words.data(), [&](std::uint64_t address, std::size_t bytes) { marked.emplace_back(address, bytes); }); + }; + const auto claim = [&](std::uint32_t index, std::uint32_t slot, std::uint64_t address, std::uint32_t mask) { + words[Abi::WrittenListWord + index] = slot; + words[Abi::WrittenSlotsWord + 2 * slot] = Abi::WrittenChunkKey(address); + words[Abi::WrittenSlotsWord + 2 * slot + 1] = mask; + words[Abi::WrittenCountWord] = std::max(words[Abi::WrittenCountWord], index + 1); + }; + constexpr std::uint64_t page = std::uint64_t{1} << Abi::WrittenPageShift; + Require(decode() && marked.empty(), "an empty written-page record marked pages"); + constexpr std::uint64_t top = 0x7ffffffe0000ULL; + claim(0, 5, top, 0b1011u); + Require(decode() && marked == std::vector>{{top, 2 * page}, {top + 3 * page, page}}, "written pages near the top of the address space were not decoded as written"); + std::fill(words.begin(), words.end(), 0u); + constexpr std::uint64_t high = 0x200000001000ULL; + claim(0, Abi::WrittenPageSlots - 1, high, 1u << ((high >> Abi::WrittenPageShift) & (Abi::WrittenChunkPages - 1u))); + Require(decode() && marked == std::vector>{{high, page}}, "a written page above 16 TiB was decoded at another address"); + std::fill(words.begin(), words.end(), 0u); + constexpr std::uint64_t chunk = 0x3e0000000ULL; + claim(0, 7, chunk, 0xffffffffu); + claim(1, 8, chunk + (std::uint64_t{1} << Abi::WrittenChunkShift), 1u); + claim(2, 9, chunk - (std::uint64_t{1} << Abi::WrittenChunkShift), 0x80000000u); + Require(decode() && marked == std::vector>{{chunk, 32 * page}, {chunk + 32 * page, page}, {chunk - page, page}}, "whole, first-page and last-page chunks were not decoded exactly"); + words[Abi::WrittenListWord + 1] = Abi::WrittenPageSlots; + Require(!decode(), "a claim outside the slots was accepted"); + std::fill(words.begin(), words.end(), 0u); + words[Abi::WrittenCountWord] = 1; + Require(!decode(), "a claim of an empty slot was accepted"); + words[Abi::WrittenCountWord] = Abi::WrittenPageSlots + 1; + Require(!decode(), "more claims than slots were accepted"); +} + +static void RunBdaDwordWriteTests(const Context& context) { + RunWrittenRangeDecodeTests(); + namespace Abi = ShaderRecompiler::BdaAbi; + constexpr std::uint64_t guest = 0x7fff12340f80ULL; + constexpr std::size_t firstBytes = 160; + Buffer first(context, firstBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); + Buffer second(context, 16, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); + Buffer readOnly(context, 16, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); + const std::array ranges{{ + {guest, guest + firstBytes, first.DeviceAddress(), Abi::Read | Abi::Write, 0}, + {guest + firstBytes, guest + firstBytes + 16, second.DeviceAddress(), Abi::Read | Abi::Write, 0}, + {guest + firstBytes + 16, guest + firstBytes + 32, readOnly.DeviceAddress(), Abi::Read, 0}, + }}; + Buffer table(context, sizeof(Abi::Header) + sizeof(ranges), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Buffer fault(context, Abi::FaultBufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Buffer output(context, 4, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + const std::array values{0x11223344u, 0x55667788u, 0x99aabbccu, 0xddeeff00u}; + const auto bytesAt = [&](std::uint64_t address) -> std::byte* { + for (std::size_t index = 0; index < ranges.size(); ++index) { + if (address < ranges[index].begin || address >= ranges[index].end) continue; + auto& buffer = index == 0 ? first : index == 1 ? second : readOnly; + return buffer.Bytes().data() + (address - ranges[index].begin); + } + return nullptr; + }; + const auto notedBytes = [&](std::uint64_t first, std::uint64_t last) { + std::uint64_t covered = 0, total = 0; + const auto* faultWords = reinterpret_cast(fault.Bytes().data()); + Require(Abi::ForEachWrittenRange(faultWords, [&](std::uint64_t begin, std::size_t bytes) { + total += bytes; + for (const auto address : {first, last}) covered += address >= begin && address < begin + bytes ? 1u : 0u; + }), "a BDA dword write left an invalid written-page record"); + return std::pair{covered, total}; + }; + const auto write = [&](std::uint64_t address, std::uint32_t dwords, std::uint32_t storedBytes, Abi::FaultReason reason, std::uint32_t groups = 1) { + const Abi::Header header{Abi::Version, static_cast(ranges.size()), 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()); + for (auto* buffer : {&first, &second, &readOnly}) std::memset(buffer->Bytes().data(), 0xa5, buffer->Bytes().size()); + Pipeline pipeline(context, MakeBdaDwordWriteTestShader(address, dwords, values.data()), {&table, &fault, &output}); + pipeline.Run(groups); + Abi::Fault report{}; + std::memcpy(&report, fault.Bytes().data(), sizeof(report)); + for (std::uint32_t byte = 0; byte < dwords * 4u; ++byte) { + const auto* stored = bytesAt(address + byte); + const auto expected = byte < storedBytes ? static_cast(values[byte / 4u] >> ((byte % 4u) * 8u)) : std::byte{0xa5}; + Require(stored != nullptr && *stored == expected, "BDA dword write stored incorrect bytes at offset " + std::to_string(byte) + " of guest + " + std::to_string(address - guest)); + } + Require(*bytesAt(address - 1) == std::byte{0xa5}, "BDA dword write touched the byte before it"); + if (address + dwords * 4u < guest + firstBytes + 32) Require(*bytesAt(address + dwords * 4u) == std::byte{0xa5}, "BDA dword write touched the byte after it"); + if (static_cast(reason) == 0) { + Require(report.state == Abi::FaultState::Empty, "a mapped BDA dword write published a fault"); + } else { + Require(report.state == Abi::FaultState::Ready && report.reason == reason && report.instruction == 0x1234, "a BDA dword write did not publish its fault"); + } + const auto [covered, total] = notedBytes(address, address + (storedBytes != 0 ? storedBytes - 1u : 0u)); + if (storedBytes == 0) { + Require(total == 0, "a BDA dword write that stored nothing noted pages"); + } else { + const auto pages = ((address + storedBytes - 1u) >> Abi::WrittenPageShift) - (address >> Abi::WrittenPageShift) + 1u; + Require(covered == 2 && total == pages << Abi::WrittenPageShift, "a BDA dword write did not note exactly its written pages"); + const auto chunks = ((address + storedBytes - 1u) >> Abi::WrittenChunkShift) - (address >> Abi::WrittenChunkShift) + 1u; + std::uint32_t claims = 0; + std::memcpy(&claims, fault.Bytes().data() + Abi::WrittenCountWord * sizeof(std::uint32_t), sizeof(claims)); + Require(claims == chunks, "a BDA dword write claimed a written-page slot more than once per chunk"); + } + }; + write(guest + 16, 4, 16, static_cast(0)); + write(guest + 20, 4, 16, static_cast(0)); + write(guest + 8, 2, 8, static_cast(0)); + write(guest + 12, 2, 8, static_cast(0)); + write(guest + 32, 3, 12, static_cast(0)); + write(guest + 0x78, 4, 16, static_cast(0)); + write(guest + 0x7c, 2, 8, static_cast(0)); + write(guest + 41, 4, 16, static_cast(0)); + write(guest + firstBytes - 8, 4, 16, static_cast(0)); + write(guest + firstBytes + 12, 4, 4, Abi::FaultReason::Permission); + 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); @@ -206,4 +521,92 @@ 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) { + namespace Abi = ShaderRecompiler::BdaAbi; + Buffer first(context, 2, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); + Buffer second(context, 3, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); + Buffer table(context, sizeof(Abi::Header) + 2 * sizeof(Abi::Range), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Buffer fault(context, Abi::FaultBufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Buffer output(context, 4, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + constexpr std::uint64_t guest = 0x7fff1235fffdULL; + constexpr std::uint32_t writable = Abi::Read | Abi::Write; + const Abi::Header header{Abi::Version, 2, sizeof(Abi::Range), 0}; + std::array ranges{}; + bool runtime = false; + const auto run = [&](const char* name, std::uint64_t address, std::uint32_t bits, bool coherent, const std::array& expected, std::initializer_list writtenAddresses, Abi::FaultReason reason = {}, std::uint64_t faultAddress = 0, std::uint32_t faultBytes = 0) { + const std::string label = std::string(runtime ? "BDA runtime write " : "BDA store ") + name + (coherent ? " coherent: " : ": "); + std::memset(first.Bytes().data(), 0xcd, first.Bytes().size()); + std::memset(second.Bytes().data(), 0xcd, second.Bytes().size()); + 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()); + std::memset(output.Bytes().data(), 0, output.Bytes().size()); + Pipeline pipeline(context, runtime ? MakeBdaWriteTestShader(address, 0xa1b2c3d4u, bits, coherent) : MakeBdaStoreTestShader(address, 0xa1b2c3d4u, bits, coherent), {&table, &fault, &output}); + pipeline.Run(1); + std::array actual{}; + std::memcpy(actual.data(), first.Bytes().data(), 2); + std::memcpy(actual.data() + 2, second.Bytes().data(), 3); + Require(actual == expected, label + "incorrect stored bytes or writes outside the mapped span"); + 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.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.otherFaults == 1, label + "incorrect first fault metadata"); + } + std::array words{}; + std::memcpy(words.data(), fault.Bytes().data(), sizeof(words)); + Require(words[Abi::WrittenOverflowWord] == 0, label + "written-page table overflowed"); + std::set recordedPages; + Require(Abi::ForEachWrittenRange(words.data(), [&](std::uint64_t address, std::size_t bytes) { + for (std::size_t offset = 0; offset < bytes; offset += std::size_t{1} << Abi::WrittenPageShift) { + Require(recordedPages.insert((address + offset) >> Abi::WrittenPageShift).second, label + "written-page table contains duplicate pages"); + } + }), label + "invalid written-page record"); + std::set expectedPages; + std::set expectedChunks; + for (const auto writtenAddress : writtenAddresses) { + expectedPages.insert(writtenAddress >> Abi::WrittenPageShift); + expectedChunks.insert(Abi::WrittenChunkKey(writtenAddress)); + } + Require(recordedPages == expectedPages, label + "incorrect written-page set"); + Require(words[Abi::WrittenCountWord] == expectedChunks.size(), label + "incorrect written-chunk claim count"); + std::uint32_t completed = 0; + std::memcpy(&completed, output.Bytes().data(), sizeof(completed)); + Require(completed == 1, label + "store did not finish its per-byte accesses"); + }; + for (const auto& [coherent, writes] : std::initializer_list>{{false, false}, {false, true}, {true, false}, {true, true}}) { + runtime = writes; + ranges = {{{guest, guest + 2, first.DeviceAddress(), writable, 0}, {guest + 2, guest + 5, second.DeviceAddress(), writable, 0}}}; + run("byte", guest + 1, 8, coherent, {0xcd, 0xd4, 0xcd, 0xcd, 0xcd}, {guest + 1}); + run("split short", guest + 1, 16, coherent, {0xcd, 0xd4, 0xc3, 0xcd, 0xcd}, {guest + 1, guest + 2}); + run("split unaligned dword", guest, 32, coherent, {0xd4, 0xc3, 0xb2, 0xa1, 0xcd}, {guest, guest + 3}); + ranges[1].permissions = Abi::Read; + run("read-only byte", guest + 2, 8, coherent, {0xcd, 0xcd, 0xcd, 0xcd, 0xcd}, {}, Abi::FaultReason::Permission, guest + 2, 1); + run("read-only short suffix", guest + 1, 16, coherent, {0xcd, 0xd4, 0xcd, 0xcd, 0xcd}, {guest + 1}, Abi::FaultReason::Permission, guest + 2, 1); + run("read-only suffix", guest, 32, coherent, {0xd4, 0xc3, 0xcd, 0xcd, 0xcd}, {guest}, Abi::FaultReason::Permission, guest + 2, 1); + ranges[0].end = guest + 1; + ranges[1].permissions = writable; + run("unmapped middle byte", guest, 32, coherent, {0xd4, 0xcd, 0xb2, 0xa1, 0xcd}, {guest, guest + 3}, Abi::FaultReason::Unmapped, guest + 1, 1); + ranges = {{{guest + 3, guest + 5, first.DeviceAddress(), writable, 0}, {guest + 5, guest + 8, second.DeviceAddress(), writable, 0}}}; + run("split aligned dword", guest + 3, 32, coherent, {0xcd, 0xcd, 0xcd, 0xcd, 0xcd}, {}, Abi::FaultReason::Unmapped, guest + 3, 4); + } } diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index f256818ac6..ceacffde72 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -1,8 +1,12 @@ #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" #include "prx/libc/include/GuestArena.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" #ifdef _WIN32 #ifndef NOMINMAX #define NOMINMAX @@ -17,9 +21,16 @@ #include #include #include +#include +#include #include #include +#include +#ifndef _WIN32 +#include +#include +#endif namespace { using namespace AgcDriver::Graphics; @@ -35,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}); @@ -48,7 +171,7 @@ void importCrossingTests(const Context& context, const BdaTestAccess& access) { const auto registry = [&](bool add) { auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); for (auto* range : {guest, guest + half}) { - if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, half, true, false); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, half, true, false, true); else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range); } GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); @@ -66,15 +189,16 @@ void importCrossingTests(const Context& context, const BdaTestAccess& access) { const auto crossing = leased.Descriptor(second - 16, 32, adjustment); const auto crossingBytes = access.bytes(crossing.buffer); Require(crossing.offset + crossing.range <= crossingBytes.size(), "a view crossing into the next registered range is bound past its host import"); - Require(crossingBytes[crossing.offset + adjustment + 15] == std::byte{0x11} && crossingBytes[crossing.offset + adjustment + 16] == std::byte{0x22}, "a view crossing into the next registered range misses its bytes"); + Require(secondImport.base == first && secondImport.bytes == 2 * half, "adjacent registered ranges with the same access were not imported as one span"); + Require(crossing.buffer == secondImport.buffer && crossing.offset + adjustment == second - 16 - secondImport.base, "a view crossing into the next registered range is not bound in place in the spanning import"); const auto inside = leased.Descriptor(second + 64, 16, adjustment); const auto insideBytes = access.bytes(inside.buffer); - Require(inside.offset + inside.range <= insideBytes.size() && (inside.buffer == secondImport.buffer ? inside.offset + adjustment == 64 : insideBytes[inside.offset + adjustment] == std::byte{0x22}), "a view in the second registered range is bound past its buffer or misses its bytes"); + Require(inside.offset + inside.range <= insideBytes.size() && (inside.buffer == secondImport.buffer ? inside.offset + adjustment == second + 64 - secondImport.base : insideBytes[inside.offset + adjustment] == std::byte{0x22}), "a view in the second registered range is bound past its buffer or misses its bytes"); for (const auto& range : leased.AddressRanges()) { if (range.end <= first || range.begin >= second + half) continue; const auto mapped = access.addressBytes(range.deviceAddress); Require(mapped.size() >= range.end - range.begin, "a BDA range runs past the end of its host import"); - if (range.begin <= second && second < range.end && range.deviceAddress != secondImport.address) Require(mapped[second - range.begin] == std::byte{0x22}, "the BDA range over the second registered range misses its bytes"); + if (range.begin <= second && second < range.end && (range.deviceAddress < secondImport.address || range.deviceAddress >= secondImport.address + secondImport.bytes)) Require(mapped[second - range.begin] == std::byte{0x22}, "the BDA range over the second registered range misses its bytes"); } leased.WriteBack(); } @@ -83,6 +207,1850 @@ void importCrossingTests(const Context& context, const BdaTestAccess& access) { ::operator delete(block, std::align_val_t{half}); } +void importSpanTests(const Context& context) { + constexpr std::size_t piece = 65536; + void* block = ::operator new(3 * piece, std::align_val_t{piece}); + auto* guest = static_cast(block); + const auto first = reinterpret_cast(block); + auto importing = context; + importing.hostImportAlignment = piece; + const auto change = [&](std::uint8_t* range, bool add, bool writable) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, piece, true, writable, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + change(guest, true, false); + change(guest + piece, true, false); + change(guest + 2 * piece, true, true); + const auto* span = HostImportFor(importing, first + piece - 16, 32); + Require(span != nullptr && span->base == first && span->bytes == 2 * piece, "adjacent read-only ranges were not imported as one span"); + const auto* writable = HostImportFor(importing, first + 2 * piece, piece); + Require(writable != nullptr && writable->base == first + 2 * piece && writable->bytes == piece, "a writable range joined the read-only span"); + Require(HostImportFor(importing, first + 2 * piece - 16, 32) == nullptr, "a view crossing ranges of different access was imported"); + change(guest + piece, false, false); + const auto* alone = HostImportFor(importing, first, piece); + Require(alone != nullptr && alone->base == first && alone->bytes == piece, "the span outlived the removal of one of its ranges"); + Require(HostImportFor(importing, first + piece, 16) == nullptr, "the removed range is still imported"); + change(guest, false, false); + change(guest + 2 * piece, false, false); + ::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})); + auto importing = context; + importing.hostImportAlignment = piece; + const auto address = reinterpret_cast(block); + const auto add = [&](std::size_t index) { + GuestAllocations::Mutation mutation; + mutation.Add(block + index * piece, piece, true, true, true); + }; + const auto remove = [&](std::size_t index) { + GuestAllocations::Mutation mutation; + mutation.Remove(block + index * piece); + }; + SetHostImportWindow(0); + for (std::size_t replaced = 0; replaced < 2; ++replaced) { + add(0); + add(1); + std::weak_ptr owner; + { + const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + const auto found = std::find_if(lease.begin(), lease.end(), [&](const auto& range) { return range->address == address + replaced * piece; }); + Require(found != lease.end(), "failed import fixture has no registered owner"); + owner = *found; + } + 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() == 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"); + remove(replaced); + Require(owner.expired(), "a failed import kept the replaced owner alive"); + add(replaced); + access.limitMemory(std::nullopt); + const auto* imported = HostImportFor(importing, address, 2 * piece); + Require(imported != nullptr && imported->base == address && imported->bytes == 2 * piece && access.allocationAttempts() == attempts + 2, "same-address remap retained a failed import decision"); + remove(0); + remove(1); + remove(3); + ClearHostImports(importing.device); + } + add(0); + add(1); + access.limitMemory(0); + Require(HostImportFor(importing, address, 2 * piece) == nullptr, "span failure was not exercised"); + remove(1); + access.limitMemory(std::nullopt); + const auto* shortened = HostImportFor(importing, address, piece); + Require(shortened != nullptr && shortened->bytes == piece, "a failed span suppressed an import of its remaining range"); + remove(0); + ClearHostImports(importing.device); + ::operator delete(block, std::align_val_t{piece}); +} + +void spaceImportRunTests(const Context& context) { + constexpr std::size_t piece = 65536; + void* block = ::operator new(2 * piece, std::align_val_t{piece}); + const auto first = reinterpret_cast(block); + std::memset(block, 0x3c, 2 * piece); + auto importing = context; + importing.hostImportAlignment = piece; + const auto change = [&](std::uint8_t* range, bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, piece, true, false, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + auto* guest = static_cast(block); + change(guest, true); + change(guest + piece, true); + for (int build = 0; build < 2; ++build) { + const auto before = AddressSpaceCounters(); + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + leased.AddReadable(first + piece - 256, 512); + leased.Upload(true); + std::uint32_t adjustment = 0; + const auto view = leased.Descriptor(first + piece - 256, 512, adjustment); + Require(view.buffer != VK_NULL_HANDLE && view.range == 512 + adjustment, "a V# across one import's ranges has no descriptor"); + const auto ranges = leased.AddressRanges(); + Require(std::any_of(ranges.begin(), ranges.end(), [&](const auto& range) { return range.begin <= first && first + 2 * piece <= range.end; }), "one import's adjacent ranges are not one BDA table entry"); + leased.WriteBack(); + const auto after = AddressSpaceCounters(); + if (after.enabled && build == 1) Require(after.hits == before.hits + 1 && after.dissolvedOverlap == before.dissolvedOverlap, "a V# across one import's ranges dissolved the cached address space"); + } + change(guest, false); + change(guest + piece, false); + ::operator delete(block, std::align_val_t{piece}); +} + +void 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; + void* block = ::operator new(4 * piece, std::align_val_t{window}); + auto* guest = static_cast(block); + const auto first = reinterpret_cast(block); + auto importing = context; + importing.hostImportAlignment = piece; + SetHostImportWindow(window); + const auto change = [&](std::uint8_t* range, bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, piece, true, false, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + for (std::size_t index = 0; index < 3; ++index) change(guest + index * piece, true); + const auto* low = HostImportFor(importing, first + piece - 16, 32); + Require(low != nullptr && low->base == first && low->bytes == window, "adjacent ranges inside one window were not imported as one span"); + const auto lowBuffer = low->buffer; + const auto* high = HostImportFor(importing, first + window, piece); + Require(high != nullptr && high->base == first + window && high->bytes == piece, "a span crossed the import window"); + Require(HostImportFor(importing, first + window - 16, 32) == nullptr, "a view crossing the import window was imported"); + change(guest + 3 * piece, true); + const auto* grown = HostImportFor(importing, first + window, window); + Require(grown != nullptr && grown->base == first + window && grown->bytes == window, "the next window did not grow into the new range"); + const auto* kept = HostImportFor(importing, first, 16); + Require(kept != nullptr && kept->buffer == lowBuffer, "growth in one window re-imported another"); + for (std::size_t index = 0; index < 4; ++index) change(guest + index * piece, false); + SetHostImportWindow(0); + ::operator delete(block, std::align_val_t{window}); +} + +void importJoinTests(const Context& context) { + constexpr std::size_t piece = 65536; + constexpr std::size_t window = 2 * piece; + constexpr std::size_t large = 3 * piece; + void* block = ::operator new(3 * large + 5 * piece, std::align_val_t{window}); + auto* guest = static_cast(block); + std::memset(guest, 0x11, 3 * large); + const auto first = reinterpret_cast(block); + const auto second = first + large; + const auto third = second + large; + auto importing = context; + importing.hostImportAlignment = piece; + SetHostImportWindow(window); + const auto change = [&](std::uint8_t* range, std::size_t bytes, bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, bytes, true, false, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + const auto bind = [&](std::uintptr_t address, std::size_t bytes = 32) { + GuestBufferMemory plain(importing); + plain.AddReadable(address, bytes); + plain.Upload(false); + std::uint32_t adjustment = 0; + const auto view = plain.Descriptor(address, bytes, adjustment); + const auto copied = plain.CopiedBytes(); + plain.WriteBack(); + return std::pair{view.buffer, copied}; + }; + change(guest, large, true); + change(guest + large, large, true); + const auto* low = HostImportFor(importing, first, 16); + Require(low != nullptr && low->base == first && low->bytes == large, "a range larger than the import window was not imported alone"); + const auto* high = HostImportFor(importing, second, 16); + Require(high != nullptr && high->base == second && high->bytes == large, "the next range larger than the import window was not imported alone"); + Require(HostImportFor(importing, second - 16, 32) == nullptr, "a view across two imported ranges was imported before it was bound"); + { + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + leased.AddReadable(second - 16, 32); + leased.Upload(true); + const auto* joined = HostImportFor(importing, second - 16, 32); + Require(joined != nullptr && joined->base == first && joined->bytes == 2 * large, "a buffer bound across two adjacent ranges larger than the import window was not imported as one"); + std::uint32_t adjustment = 0; + const auto crossing = leased.Descriptor(second - 16, 32, adjustment); + Require(crossing.buffer == joined->buffer && crossing.offset + adjustment == second - 16 - first, "a buffer bound across the joined ranges is not bound in place"); + leased.WriteBack(); + } + const auto joinedBuffer = HostImportFor(importing, first, 16)->buffer; + const auto [again, copied] = bind(second - 16); + Require(again == joinedBuffer && copied == 0, "a later bind across the joined ranges did not use the joined import"); + auto* heap = guest + 3 * large + 2 * piece; + change(heap, piece, true); + change(heap + piece, piece, true); + Require(bind(reinterpret_cast(heap + piece) - 16).second != 0 && HostImportFor(importing, reinterpret_cast(heap + piece) - 16, 32) == nullptr, "two pieces no larger than the import window were joined"); + const auto heapBase = reinterpret_cast(heap); + const auto [piecesBuffer, piecesCopied] = bind(heapBase, 2 * piece); + const auto* pieces = HostImportFor(importing, heapBase, 2 * piece); + Require(pieces != nullptr && pieces->base == heapBase && pieces->bytes == 2 * piece && piecesBuffer == pieces->buffer && piecesCopied == 0, "a buffer bound exactly over two pieces no larger than the import window was not imported as one"); + change(heap + 2 * piece, piece, true); + static_cast(bind(heapBase + 2 * piece - 16)); + const auto* kept = HostImportFor(importing, first, 16); + Require(kept != nullptr && kept->buffer == joinedBuffer && kept->bytes == 2 * large, "a registry change elsewhere re-imported the joined ranges"); + change(guest + 2 * large, large, true); + Require(HostImportFor(importing, first, 16)->buffer == joinedBuffer, "a range registered next to the joined ranges re-imported them"); + const auto [grownBuffer, grownCopied] = bind(third - 16); + const auto* grown = HostImportFor(importing, third - 16, 32); + Require(grown != nullptr && grown->base == first && grown->bytes == 3 * large && grownBuffer == grown->buffer && grownCopied == 0, "a bind across the next range did not grow the joined import"); + Require(bind(second - 16).first == grownBuffer && HostImportFor(importing, first, 16)->buffer == grownBuffer, "two joins sharing a range retired each other"); + change(heap, piece, false); + change(heap + piece, piece, false); + change(heap + 2 * piece, piece, false); + change(guest + large, large, false); + const auto* alone = HostImportFor(importing, first, 16); + Require(alone != nullptr && alone->base == first && alone->bytes == large, "the joined import outlived one of its ranges"); + change(guest, large, false); + change(guest + 2 * large, large, false); + Require(HostImportFor(importing, first, 16) == nullptr && HostImportFor(importing, third, 16) == nullptr, "the host imports outlived their ranges"); + SetHostImportWindow(0); + ::operator delete(block, std::align_val_t{window}); +} + +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 { +public: + WindowedRange(const Context& context, std::size_t window, std::size_t cells, std::uint64_t budget = 0) : window(window), bytes(window * cells), importing(context) { + block = ::operator new(bytes, std::align_val_t{piece}); + std::memset(block, 0x33, bytes); + importing.hostImportAlignment = piece; + SetHostImportRangeWindow(window); + SetHostImportBudget(budget); + change(true); + } + ~WindowedRange() { + change(false); + SetHostImportBudget(0); + SetHostImportRangeWindow(std::uint64_t{64} << 20u); + ::operator delete(block, std::align_val_t{piece}); + } + WindowedRange(const WindowedRange&) = delete; + WindowedRange& operator=(const WindowedRange&) = delete; + + std::uintptr_t First() const { return reinterpret_cast(block); } + + static constexpr std::size_t piece = 65536; + const std::size_t window; + const std::size_t bytes; + Context importing; + +private: + void change(bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block, bytes, true, false, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + } + + void* block = nullptr; +}; + +// A registered range larger than the import window, its first window imported by an earlier access: an address-based +// build (AcquireRegistered) must import the whole range, so a view in its last window binds (Demon's Souls threw +// "guest buffer view ... exceeds its GPU owner" with the 64 MiB windows). +void importWholeRangeOverWindowTests(const Context& context) { + WindowedRange range(context, 2 * WindowedRange::piece, 2); + const auto first = range.First(); + const auto* windowed = HostImportFor(range.importing, first + 16, 32); + Require(windowed != nullptr && windowed->base == first && windowed->bytes == range.window, "an access to the first window of a large range was not imported as that window"); + GuestBufferMemory leased(range.importing); + leased.AcquireRegistered(); + leased.Upload(true); + std::uint32_t adjustment = 0; + const auto view = leased.Descriptor(first + range.bytes - WindowedRange::piece + 16, 32, adjustment); + Require(view.range == 32 + adjustment, "a view in the last window of a large registered range has no descriptor"); + const auto* whole = HostImportFor(range.importing, first, range.bytes); + Require(whole != nullptr && whole->base == first && whole->bytes == range.bytes && view.buffer == whole->buffer, "an address-based build was served by the range's first window, not an import of the whole range"); + leased.WriteBack(); +} + +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) { + WindowedRange range(context, 2 * WindowedRange::piece, 4, 3 * 2 * WindowedRange::piece); + const auto first = range.First(); + const auto* windowed = HostImportFor(range.importing, first + 16, 32); + Require(windowed != nullptr && windowed->bytes == range.window, "the first window of a range over the budget was not imported"); + const auto buffer = windowed->buffer; + for (int build = 0; build < 3; ++build) { + GuestBufferMemory leased(range.importing); + leased.AcquireRegistered(); + leased.Upload(true); + leased.WriteBack(); + Require(HostImportCovers(range.importing, first + 16, 32), "an address-based build refused its whole-range import and retired the range's window (build " + std::to_string(build) + ")"); + } + const auto* kept = HostImportFor(range.importing, first + 16, 32); + Require(kept != nullptr && kept->buffer == buffer, "the range's window was imported again after a refused whole-range import"); +} + +// Two uses across neighbouring cell edges of one range, each in a build of its own, share one import: the second grows +// the first's window to their union, and later rounds of both keep it. Before, each replaced the other's window at +// every use. +void importNeighbouringEdgesTests(const Context& context) { + WindowedRange range(context, 2 * WindowedRange::piece, 3); + const auto first = range.First(); + const auto use = [&](std::uintptr_t address) { + GuestBufferMemory leased(range.importing); + leased.AddReadable(address, 32); + leased.Upload(false); + leased.WriteBack(); + }; + const auto acrossFirst = first + range.window - 16; + const auto acrossSecond = first + 2 * range.window - 16; + use(acrossFirst); + Require(HostImportCovers(range.importing, acrossFirst, 32), "a use across the first cell edge was not imported"); + use(acrossSecond); + Require(HostImportCovers(range.importing, first, range.bytes), "a use across the second cell edge did not grow the first use's window to their union"); + const auto serial = HostImportSerial(range.importing, first, range.bytes, false); + for (int round = 0; round < 10; ++round) { + use(acrossFirst); + use(acrossSecond); + Require(HostImportSerial(range.importing, first, range.bytes, false) == serial, "a use across a cell edge replaced the shared window (round " + std::to_string(round) + ")"); + } +} + +// A range mapped while the imports are at the budget is refused at map time without retiring an import: the map +// observer holds no GPU lock, so it must not evict (before, it evicted the least recently used import to fit). +void importAtMapOverBudgetTests(const Context& context) { + constexpr std::size_t bytes = 65536; + auto importing = context; + importing.hostImportAlignment = bytes; + void* kept = ::operator new(bytes, std::align_val_t{bytes}); + void* used = ::operator new(bytes, std::align_val_t{bytes}); + void* mapped = ::operator new(bytes, std::align_val_t{bytes}); + const auto change = [&](void* block, bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block, bytes, true, false, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + change(kept, true); + change(used, true); + const auto keptAddress = reinterpret_cast(kept); + const auto usedAddress = reinterpret_cast(used); + Require(HostImportFor(importing, keptAddress, 16) != nullptr, "the import to keep was not made"); + for (int use = 0; use < 300; ++use) Require(HostImportFor(importing, usedAddress, 16) != nullptr, "the import in use was not made"); + { + GuestBufferMemory leased(importing); + leased.AddReadable(usedAddress, 16); + leased.Upload(false); + leased.WriteBack(); + } + const auto live = HostImportLiveBytes(); + SetHostImportBudget(live + bytes - 1); + change(mapped, true); + GuestAllocations::Mapped ranges; + { + const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + for (const auto& range : lease) { + if (range->address == reinterpret_cast(mapped)) ranges.push_back(range); + } + } + Require(ranges.size() == 1, "the mapped range is not registered"); + static_cast(ImportMappedRanges(importing, ranges, GuestAllocations::GuestAllocationsGeneration_nid_postfix(), true)); + SetHostImportBudget(0); + Require(HostImportLiveBytes() == live, "a range mapped at the budget retired an import at map time (an eviction without the GPU lock)"); + Require(HostImportCovers(importing, keptAddress, 16) && !HostImportCovers(importing, reinterpret_cast(mapped), 16), "a range mapped at the budget was imported, or the import to keep was retired"); + for (auto* block : {kept, used, mapped}) change(block, false); + for (auto* block : {kept, used, mapped}) ::operator delete(block, std::align_val_t{bytes}); +} + +void 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; + void* block = ::operator new(3 * piece, std::align_val_t{window}); + auto* guest = static_cast(block); + const auto first = reinterpret_cast(block); + const auto boundary = first + window; + std::memset(guest, 0x5a, window); + std::memset(guest + window, 0x6b, piece); + auto importing = context; + importing.hostImportAlignment = piece; + SetHostImportWindow(window); + const auto change = [&](std::uint8_t* range, bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, piece, true, false, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + for (std::size_t index = 0; index < 3; ++index) change(guest + index * piece, true); + for (int build = 0; build < 2; ++build) { + const auto before = AddressSpaceCounters(); + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + leased.AddReadable(boundary - 256, 512); + leased.Upload(true); + std::uint32_t adjustment = 0; + const auto view = leased.Descriptor(boundary - 256, 512, adjustment); + Require(view.buffer != VK_NULL_HANDLE && view.range == 512 + adjustment, "a read-only V# across two imports has no descriptor"); + const auto bytes = access.bytes(view.buffer); + Require(view.offset + adjustment + 512 <= bytes.size() && bytes[view.offset + adjustment] == std::byte{0x5a} && bytes[view.offset + adjustment + 256] == std::byte{0x6b}, "a read-only V# across two imports misses the bytes of either range"); + const auto ranges = leased.AddressRanges(); + Require(std::any_of(ranges.begin(), ranges.end(), [&](const auto& range) { return range.begin <= first && boundary <= range.end; }) && std::any_of(ranges.begin(), ranges.end(), [&](const auto& range) { return range.begin <= boundary && boundary + piece <= range.end; }), "an import is missing from the BDA table of a build with a read-only V# across it"); + leased.WriteBack(); + const auto after = AddressSpaceCounters(); + if (after.enabled && build == 1) Require(after.hits == before.hits + 1 && after.dissolvedOverlap == before.dissolvedOverlap, "a read-only V# across two imports dissolved the cached address space"); + } + for (std::size_t index = 0; index < 3; ++index) change(guest + index * piece, false); + SetHostImportWindow(0); + ::operator delete(block, std::align_val_t{window}); +} + +void spaceReadAcrossImportsWrittenTests(const Context& context) { + constexpr std::size_t piece = 65536; + constexpr std::size_t window = 2 * piece; + void* block = ::operator new(3 * piece, std::align_val_t{window}); + auto* guest = static_cast(block); + const auto boundary = reinterpret_cast(block) + window; + std::memset(guest, 0x5a, window); + std::memset(guest + window, 0x6b, piece); + auto importing = context; + importing.hostImportAlignment = piece; + SetHostImportWindow(window); + const auto change = [&](std::uint8_t* range, bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, piece, true, false, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + for (const bool writtenFirst : {false, true}) { + for (std::size_t index = 0; index < 3; ++index) change(guest + index * piece, true); + for (int build = 0; build < 2; ++build) { + const auto before = AddressSpaceCounters(); + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + if (writtenFirst) leased.AddWritable(boundary - 384, 256); + leased.AddReadable(boundary - 256, 512); + if (!writtenFirst) leased.AddWritable(boundary - 384, 256); + leased.Upload(true); + std::uint32_t readAdjustment = 0; + std::uint32_t writeAdjustment = 0; + const auto read = leased.Descriptor(boundary - 256, 512, readAdjustment); + const auto written = leased.Descriptor(boundary - 384, 256, writeAdjustment); + Require(read.buffer != VK_NULL_HANDLE && read.buffer == written.buffer && written.offset + writeAdjustment + 128 == read.offset + readAdjustment, "a written V# inside a read-only V# across two imports maps other bytes than it"); + leased.WriteBack(); + const auto after = AddressSpaceCounters(); + if (after.enabled && build == 0) Require(after.dissolvedOverlap == before.dissolvedOverlap + 1, "a written V# inside a read-only V# across two imports kept the cached address space"); + } + for (std::size_t index = 0; index < 3; ++index) change(guest + index * piece, false); + } + SetHostImportWindow(0); + ::operator delete(block, std::align_val_t{window}); +} + +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}; } @@ -103,7 +2071,7 @@ void importedHeapMirrorTests(const Context& context, const BdaTestAccess& access importing.hostImportAlignment = bytes; const auto registry = [&](bool add) { auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); - if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block, bytes, true, false); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block, bytes, true, false, true); else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block); GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); }; @@ -142,6 +2110,123 @@ void importedHeapMirrorTests(const Context& context, const BdaTestAccess& access GuestArena::GuestArenaRelease_nid_postfix(block, bytes); } +void importedFreshTests(const Context& context) { +#ifndef _WIN32 + if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) { + std::cout << "write watch unavailable: imported never-collected pages not tested\n"; + return; + } + constexpr std::size_t blockBytes = 65536; + void* area = mmap(nullptr, 2 * blockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(area != MAP_FAILED, "cannot map the imported block"); + const auto base = (reinterpret_cast(area) + blockBytes - 1) & ~(blockBytes - 1); + std::memset(reinterpret_cast(base), 0x55, blockBytes); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(reinterpret_cast(base), blockBytes); + auto importing = context; + importing.hostImportAlignment = blockBytes; + const auto registry = [&](bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, reinterpret_cast(base), blockBytes, true, true, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, reinterpret_cast(base)); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + registry(true); + if (HostImportFor(importing, base, blockBytes) == nullptr) { + std::cout << "the never-collected block was not imported: imported never-collected pages not tested\n"; + } else { + const auto gpuWrite = AgcDriver::GuestMemory::MarkWritten(base, 4); + Require(gpuWrite != 0, "a GPU write into an imported never-collected block was not stamped"); + reinterpret_cast(base)[5 * 4096 + 8] = 0x66; + static_cast(AgcDriver::GuestMemory::CollectWritesUncached(base, blockBytes)); + Require(AgcDriver::GuestMemory::WrittenSince(base + 5 * 4096, 4096, gpuWrite), "a CPU write to an imported page protected after the import was not seen"); + } + registry(false); + Require(HostImportFor(importing, base, blockBytes) == nullptr, "the import outlived its range"); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(reinterpret_cast(base), blockBytes); + munmap(area, 2 * blockBytes); +#else + static_cast(context); +#endif +} + +void importWatchTests() { +#ifndef _WIN32 + if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) { + std::cout << "write watch unavailable: import watch not tested\n"; + return; + } + constexpr std::size_t bytes = 4 * 65536; + void* block = mmap(nullptr, bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(block != MAP_FAILED, "cannot map the import watch block"); + auto* guest = static_cast(block); + const auto address = reinterpret_cast(block); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes); + std::memset(block, 0x22, bytes); + const auto written = [&] { + std::size_t reported = 0; + Require(GuestWriteWatch::GuestWriteWatchCollect_nid_postfix(address, bytes, [](void* context, std::uintptr_t begin, std::uintptr_t end) { *static_cast(context) += end - begin; }, &reported), "the write watch did not collect the block"); + return reported; + }; + Require(AgcDriver::GuestMemory::ImportWatched(address, bytes, [] { return true; }), "importing a block nothing had collected failed"); + Require(written() == bytes, "an import write-protected pages nothing had collected"); + guest[3 * 4096] = 0x33; + Require(AgcDriver::GuestMemory::ImportWatched(address, bytes, [] { return true; }), "importing a collected block failed"); + Require(written() == 0, "an import did not take the writes to pages a collect had protected"); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, bytes); + munmap(block, bytes); + constexpr std::size_t blockBytes = 65536; + void* area = mmap(nullptr, bytes + 2 * blockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(area != MAP_FAILED, "cannot map the GPU write block"); + const auto aligned = (reinterpret_cast(area) + blockBytes - 1) & ~(blockBytes - 1); + const auto imported = aligned + 4096; + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(reinterpret_cast(imported), bytes); + Require(AgcDriver::GuestMemory::ImportWatched(imported, bytes, [] { return true; }), "importing a fresh range failed"); + const auto gpuWrite = AgcDriver::GuestMemory::MarkWritten(imported + 4096, 4096); + Require(gpuWrite != 0, "a GPU write into the imported range was not stamped"); + static_cast(AgcDriver::GuestMemory::CollectWritesUncached(imported, bytes)); + Require(!AgcDriver::GuestMemory::WrittenSince(imported + 4096, 4096, gpuWrite), "a collect after a GPU write into an uncollected import read as a CPU write over the GPU's results"); + reinterpret_cast(imported)[4096 + 100] = 0x44; + static_cast(AgcDriver::GuestMemory::CollectWritesUncached(imported, bytes)); + Require(AgcDriver::GuestMemory::WrittenSince(imported + 4096, 4096, gpuWrite), "a CPU write after a GPU write into an import was not seen"); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(reinterpret_cast(imported), bytes); + munmap(area, bytes + 2 * blockBytes); + + void* labels = mmap(nullptr, 2 * blockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(labels != MAP_FAILED, "cannot map the label block"); + const auto label = (reinterpret_cast(labels) + blockBytes - 1) & ~(blockBytes - 1); + auto* words = reinterpret_cast(label); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(reinterpret_cast(label), blockBytes); + words[0] = 1; + static_cast(AgcDriver::GuestMemory::CollectWritesUncached(label, 4)); + const auto before = AgcDriver::GuestMemory::TrackerGeneration(); + Require(AgcDriver::GuestMemory::MarkWritten(label, 4) != 0, "a GPU write next to never-collected pages was not stamped"); + Require(AgcDriver::GuestMemory::UnchangedSinceCollected(label, 4, before), "a GPU write next to never-collected pages read as a CPU write of its block"); + Require(AgcDriver::GuestMemory::StoredOver(label + 8 * 4096, 4096, before), "a never-collected page a GPU write collected is not reported as possibly stored"); + const std::array since{before}; + std::array changed{}; + std::array cpu{}; + Require(AgcDriver::GuestMemory::ChangedBlocks(label, blockBytes, since, changed, cpu) && changed[0] == AgcDriver::GuestMemory::BlockWritten && cpu[0] == 0, "collecting never-collected pages at a GPU write stamped their block as written by the CPU"); + const auto now = AgcDriver::GuestMemory::TrackerGeneration(); + Require(AgcDriver::GuestMemory::MarkWritten(label, 4) == now + 1, "a GPU write into a block with nothing left to collect advanced the generation more than once"); + words[1] = 2; + Require(!AgcDriver::GuestMemory::UnchangedSinceCollected(label, 4, before), "a CPU write next to a GPU write was not seen"); + void* awaitedArea = mmap(nullptr, 2 * blockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(awaitedArea != MAP_FAILED, "cannot map the awaited block"); + const auto awaitedBlock = (reinterpret_cast(awaitedArea) + blockBytes - 1) & ~(blockBytes - 1); + const auto awaited = awaitedBlock + 5 * 4096; + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(reinterpret_cast(awaitedBlock), blockBytes); + static_cast(AgcDriver::GuestMemory::CollectWritesUncached(awaitedBlock, 4)); + const auto closed = AgcDriver::GuestMemory::TrackerGeneration(); + *reinterpret_cast(awaited) = 7; + Require(AgcDriver::GuestMemory::MarkWritten(awaitedBlock, 4) != 0, "a GPU write into the awaited block was not stamped"); + Require(!AgcDriver::GuestMemory::UnchangedSinceCollected(awaited, 4, closed), "a CPU store to a never-collected page that a GPU write elsewhere in its block collected was not seen"); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(reinterpret_cast(awaitedBlock), blockBytes); + munmap(awaitedArea, 2 * blockBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(reinterpret_cast(label), blockBytes); + munmap(labels, 2 * blockBytes); +#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"; @@ -157,7 +2242,7 @@ void heapMirrorTests(const Context& context, const BdaTestAccess& access) { const auto address = reinterpret_cast(block); const auto registry = [&](bool add, bool writable) { auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); - if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block, bytes, true, writable); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block, bytes, true, writable, true); else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block); GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); }; @@ -248,7 +2333,7 @@ void heapMirrorTests(const Context& context, const BdaTestAccess& access) { const auto registerPair = [&](bool add) { auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); for (auto* range : {first, second}) { - if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, half, true, false); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, half, true, false, true); else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range); } GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); @@ -301,7 +2386,7 @@ void heapMirrorTests(const Context& context, const BdaTestAccess& access) { GuestAllocations::GuestAllocationsInvalidate_nid_postfix(page, 4096); const auto registerRange = [&](bool add) { auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); - if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, raw, size, true, true); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, raw, size, true, true, true); else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, raw); GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); }; @@ -361,7 +2446,7 @@ void heapMirrorTests(const Context& context, const BdaTestAccess& access) { const auto change = [&](std::initializer_list added, std::initializer_list removed) { auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); for (const auto index : removed) GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, heaps[index]); - for (const auto index : added) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, heaps[index], sizes[index], true, false); + for (const auto index : added) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, heaps[index], sizes[index], true, false, true); GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); }; const auto mirrored = [&](std::size_t index) { @@ -419,6 +2504,671 @@ void heapMirrorTests(const Context& context, const BdaTestAccess& access) { } +void gpuMappingTests(const Context& context) { + constexpr std::size_t bytes = 65536; + auto* cpu = static_cast(::operator new(bytes, std::align_val_t{bytes})); + auto* gpu = static_cast(::operator new(bytes, std::align_val_t{bytes})); + { + GuestAllocations::Mutation mutation; + mutation.Add(cpu, bytes, true, true, false); + mutation.Add(gpu, bytes, true, true, true); + } + { + GuestBufferMemory leased(context); + leased.AcquireRegistered(); + leased.Upload(true); + const auto ranges = leased.AddressRanges(); + const auto has = [&](const std::byte* block) { return std::any_of(ranges.begin(), ranges.end(), [&](const auto& range) { return range.begin == reinterpret_cast(block); }); }; + Require(has(gpu), "a GPU-mapped range is missing from the BDA table"); + Require(!has(cpu), "a range mapped without GPU access is in the BDA table"); + leased.WriteBack(); + } + { + GuestAllocations::Mutation mutation; + mutation.Remove(cpu); + mutation.Remove(gpu); + } + ::operator delete(cpu, std::align_val_t{bytes}); + ::operator delete(gpu, std::align_val_t{bytes}); +} + +void stridedOverhangTests(const Context& context, const BdaTestAccess& access) { + constexpr std::size_t allocation = 0x8000; + constexpr std::uint32_t stride = 20; + constexpr std::uint32_t records = (allocation + stride - 1) / stride; + auto* block = static_cast(::operator new(allocation * 2, std::align_val_t{allocation})); + std::memset(block, 0x5a, allocation * 2); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, allocation, true, false, true); + } + const auto address = reinterpret_cast(block); + ShaderRecompiler::RecompileResult shader; + auto buffer = binding(Role::GuestBuffers, 6); + buffer.guestDescriptor = {static_cast(address), (static_cast(address >> 32) & 0xffffu) | (stride << 16u), records, 0x31000000u}; + buffer.bufferWritten = {false}; + shader.bindings = {buffer}; + CompiledShader compiled{ShaderRecompiler::ShaderStage::Compute, &shader, 0}; + { + ShaderResources resources(context, compiled); + Require(access.descriptor(6).range == allocation, "a strided buffer's rounded-up last record was bound past its allocation"); + resources.WriteBack(); + } + { + GuestAllocations::Mutation mutation; + mutation.Add(block + allocation, allocation, true, false, true); + } + { + ShaderResources resources(context, compiled); + Require(access.descriptor(6).range == allocation, "a strided buffer's rounded-up last record was bound into the next allocation"); + resources.WriteBack(); + } + { + GuestAllocations::Mutation mutation; + mutation.Remove(block + allocation); + } + buffer.guestDescriptor[2] = records - 1; + shader.bindings = {buffer}; + { + ShaderResources resources(context, compiled); + Require(access.descriptor(6).range == static_cast(records - 1) * stride, "a strided buffer inside its allocation was shortened"); + resources.WriteBack(); + } + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + ::operator delete(block, std::align_val_t{allocation}); +} + +std::byte* reserveSparse(std::size_t reserved, std::size_t committed) { +#ifdef _WIN32 + auto* block = static_cast(VirtualAlloc(nullptr, reserved, MEM_RESERVE, PAGE_NOACCESS)); + Require(block != nullptr && VirtualAlloc(block, committed, MEM_COMMIT, PAGE_READWRITE) != nullptr, "cannot reserve the sparse heap"); +#else + void* mapped = mmap(nullptr, reserved, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(mapped != MAP_FAILED && mprotect(mapped, committed, PROT_READ | PROT_WRITE) == 0, "cannot reserve the sparse heap"); + auto* block = static_cast(mapped); +#endif + return block; +} + +void releaseSparse(std::byte* block, std::size_t reserved) { +#ifdef _WIN32 + static_cast(reserved); + VirtualFree(block, 0, MEM_RELEASE); +#else + munmap(block, reserved); +#endif +} + +void sparseHeapTests(const Context& context, const BdaTestAccess& access) { + constexpr std::size_t reserved = 64u << 20u; + constexpr std::size_t committed = 65536; + auto* block = reserveSparse(reserved, committed); + std::memset(block, 0x5a, committed); + const auto base = reinterpret_cast(block); + access.limitMemory(4u << 20u); + { + GuestBufferMemory memory(context); + memory.AddWritable(base, reserved); + memory.Upload(false); + std::uint32_t adjustment = 0; + const auto view = memory.Descriptor(base, reserved, adjustment); + Require(adjustment == 0 && view.range == committed, "a sparse heap view is not cut at its last committed byte"); + Require(memory.CopiedBytes() == committed, "a cut sparse heap is charged at its reserved size"); + const auto bytes = access.bytes(view.buffer); + Require(bytes.size() < reserved && bytes[view.offset + committed - 1] == std::byte{0x5a}, "a sparse heap was not copied up to its last committed byte"); + bytes[view.offset + 8] = std::byte{0x77}; + memory.WriteBack(); + Require(block[8] == std::byte{0x77}, "a store to a cut sparse heap was not written back"); + } + { + constexpr std::size_t late = 1u << 20u; + GuestBufferMemory memory(context); + memory.AddWritable(base, reserved); + memory.AddReadable(base + late, 16); + memory.Upload(true); + std::uint32_t adjustment = 0; + const auto view = memory.Descriptor(base + late, 16, adjustment); + Require(view.range == 16 + adjustment, "a view in the uncommitted part of a sparse heap is not bound"); + const auto bytes = access.bytes(view.buffer); + const auto whole = memory.Descriptor(base, reserved, adjustment); + Require(whole.buffer == view.buffer && bytes.size() >= whole.offset + late + 16, "a view in the uncommitted part of a sparse heap has another owner"); + Require(bytes[whole.offset + committed - 1] == std::byte{0x5a} && std::all_of(bytes.begin() + static_cast(whole.offset + committed), bytes.begin() + static_cast(whole.offset + late + 16), [](std::byte byte) { return byte == std::byte{}; }), "the uncommitted part of a cut sparse heap does not read as zeros"); + Require(whole.range == late + 256, "a sparse heap copy does not end past its last view's start"); + Require(memory.CopiedBytes() == late + 256, "a cut sparse heap charge does not include its last view"); + const auto ranges = memory.AddressRanges(); + Require(ranges.size() == 1 && ranges[0].begin == base && ranges[0].end == base + late + 256, "the BDA range of a cut sparse heap is not clamped to its copy"); + memory.WriteBack(); + } + access.limitMemory(std::nullopt); + 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; + bool passed = true; + for (const bool writable : {false, true}) { + for (const std::string shape : {"interior", "shortened", "committed"}) { + const auto name = std::string(writable ? "writable " : "readable ") + shape; + try { + const bool whole = shape == "committed"; + const auto reserved = shape == "shortened" ? 4 * unit : copied; + struct Reservation { + std::size_t bytes; + std::byte* data; + ~Reservation() { releaseSparse(data, bytes); } + } reservation{reserved, reserveSparse(reserved, whole ? reserved : unit)}; + if (!whole) { +#ifdef _WIN32 + Require(VirtualAlloc(reservation.data + 2 * unit, unit, MEM_COMMIT, PAGE_READWRITE) != nullptr, "cannot commit the sparse heap tail"); +#else + Require(mprotect(reservation.data + 2 * unit, unit, PROT_READ | PROT_WRITE) == 0, "cannot commit the sparse heap tail"); +#endif + } + std::memset(reservation.data, 0x31, unit); + if (whole) std::memset(reservation.data + unit, 0x3c, unit); + std::memset(reservation.data + 2 * unit, 0x72, unit); + VkBuffer seeded = VK_NULL_HANDLE; + { + Buffer seed(context, copied, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + std::fill(seed.Bytes().begin(), seed.Bytes().end(), std::byte{0xa5}); + seeded = seed.Handle(); + } + const auto base = reinterpret_cast(reservation.data); + GuestBufferMemory memory(context); + if (writable) memory.AddWritable(base, reserved); + else memory.AddReadable(base, reserved); + memory.Upload(false); + std::uint32_t adjustment = 0; + const auto view = memory.Descriptor(base, reserved, adjustment); + Require(view.buffer == seeded, "the sparse upload did not reuse the poisoned buffer"); + Require(adjustment == 0 && view.offset == 0 && view.range == copied && memory.CopiedBytes() == copied, "the sparse upload has the wrong allocated span"); + auto bytes = access.bytes(view.buffer).first(copied); + const auto equals = [](std::span part, std::byte value) { + return std::all_of(part.begin(), part.end(), [value](std::byte byte) { return byte == value; }); + }; + Require(equals(bytes.first(unit), std::byte{0x31}) && equals(bytes.subspan(2 * unit, unit), std::byte{0x72}), "committed GPU bytes changed during upload"); + const bool holeZero = equals(bytes.subspan(unit, unit), whole ? std::byte{0x3c} : std::byte{}); + bytes[8] = std::byte{0x19}; + bytes[unit + 8] = std::byte{0xd4}; + bytes[2 * unit + 8] = std::byte{0x27}; + memory.WriteBack(); + Require(reservation.data[8] == (writable ? std::byte{0x19} : std::byte{0x31}) && reservation.data[2 * unit + 8] == (writable ? std::byte{0x27} : std::byte{0x72}), "sparse write-back did not preserve its writable ranges"); + if (whole) Require(reservation.data[unit + 8] == (writable ? std::byte{0xd4} : std::byte{0x3c}), "committed middle write-back changed permissions"); + Require(holeZero, "uncommitted GPU bytes retain prior buffer contents"); + std::cout << "sparse buffer holes: " << name << " passed\n"; + } catch (const std::exception& error) { + std::cerr << "sparse buffer holes: " << name << ": " << error.what() << '\n'; + passed = false; + } + } + } + return passed; +} + +void 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); + const auto address = reinterpret_cast(host.data()); + Require(!AgcDriver::GuestMemory::Watched(address, host.size()), "ordinary host heap memory was reported as write-watched"); + Require(AgcDriver::GuestMemory::CollectWritesUncached(address, host.size()) == 0, "ordinary host heap memory returned a guest write generation"); + Require(!AgcDriver::GuestMemory::UnchangedSince(address, host.size(), 1), "ordinary host heap memory was proven unchanged"); + const std::array queries{AgcDriver::GuestMemory::UnchangedQuery{address, host.size(), 1}}; + Require(!AgcDriver::GuestMemory::UnchangedSinceAll(queries), "a host heap range passed a bulk unchanged proof"); + Require(std::all_of(host.begin(), host.end(), [](std::uint8_t value) { return value == 0x31; }), "write-watch eligibility changed host bytes"); +#endif +} + +void sparseHeapMirrorTests(const Context& context, const BdaTestAccess& access) { +#ifdef _WIN32 + if (!GuestArena::GuestArenaAvailable_nid_postfix() || !GuestArena::GuestArenaWriteWatched_nid_postfix()) { + std::cout << "guest arena unavailable or not write-watched: sparse heap mirrors not tested\n"; + return; + } + constexpr std::size_t reserved = 64u << 20u; + constexpr std::size_t committed = 2 * 65536; + constexpr std::size_t extent = 1u << 20u; + constexpr std::size_t late = 512u << 10u; + constexpr std::size_t grown = 2u << 20u; + constexpr std::size_t past = 3584u << 10u; + void* block = GuestArena::GuestArenaAllocate_nid_postfix(reserved, 65536); + auto* guest = static_cast(block); + GuestArena::GuestArenaCommit_nid_postfix(block, committed, PAGE_READWRITE, 65536); + std::memset(block, 0x5a, committed); + const auto base = reinterpret_cast(block); + const auto registry = [&](bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block, reserved, true, true, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + const auto bind = [&](bool written, const std::function, const VkDescriptorBufferInfo&)>& gpu) { + GuestBufferMemory memory(context); + if (written) memory.AddWritable(base, reserved); + else memory.AddReadable(base, reserved); + memory.Upload(false); + std::uint32_t adjustment = 0; + const auto view = memory.Descriptor(base, reserved, adjustment); + Require(adjustment == 0, "a sparse heap view at its base is adjusted"); + gpu(access.bytes(view.buffer), view); + memory.WriteBack(); + return view; + }; + const auto address = [&] { + GuestBufferMemory memory(context); + memory.AcquireRegistered(); + memory.Upload(true); + const auto ranges = memory.AddressRanges(); + const auto found = std::find_if(ranges.begin(), ranges.end(), [&](const auto& range) { return range.begin == base; }); + const auto result = found != ranges.end() ? std::pair{found->end, found->deviceAddress} : std::pair{0, 0}; + memory.WriteBack(); + return result; + }; + const auto sweep = [&] { + GuestBufferMemory memory(context); + memory.AcquireRegistered(); + memory.Upload(true); + memory.WriteBack(); + }; + sweep(); + registry(true); + const auto before = MirrorCounters(); + const auto first = bind(true, [&](std::span bytes, const VkDescriptorBufferInfo& view) { + Require(view.range == extent && bytes[view.offset + committed - 1] == std::byte{0x5a} && bytes[view.offset + committed] == std::byte{}, "a sparse heap is not bound to a mirror of its committed extent"); + bytes[view.offset + 8] = std::byte{0x77}; + guest[24] = 0x55; + }); + const auto made = MirrorCounters(); + Require(made.heapMirrors == before.heapMirrors + 1 && made.heapBytes == before.heapBytes + extent && made.rebuilds == before.rebuilds + 1 && made.sparseBinds == before.sparseBinds + 1, "a sparse heap was not mirrored once"); + Require(guest[8] == 0x77 && guest[24] == 0x55, "a sparse heap mirror's write-back lost the GPU's store or rolled back the CPU's"); + const auto second = bind(false, [&](std::span bytes, const VkDescriptorBufferInfo& view) { + Require(bytes[view.offset + 8] == std::byte{0x77} && bytes[view.offset + 24] == std::byte{0x55}, "the sparse heap mirror missed the stores"); + }); + const auto again = MirrorCounters(); + Require(second.buffer == first.buffer && again.rebuilds == made.rebuilds && again.blocksCopied == made.blocksCopied + 1, "a sparse heap bound again was read again beyond its written block"); + bind(false, [](std::span, const VkDescriptorBufferInfo&) {}); + Require(MirrorCounters().blocksCopied == again.blocksCopied && MirrorCounters().rebuilds == again.rebuilds, "an unchanged sparse heap was read again"); + GuestArena::GuestArenaCommit_nid_postfix(guest + late, 65536, PAGE_READWRITE, 65536); + guest[late + 4] = 0x66; + bind(false, [&](std::span bytes, const VkDescriptorBufferInfo& view) { + Require(view.buffer == first.buffer && bytes[view.offset + late + 4] == std::byte{0x66} && bytes[view.offset + late + 65536] == std::byte{}, "a page committed after the mirror was made was not read into it"); + }); + const auto [end, device] = address(); + Require(end == base + extent && device != 0 && access.addressBytes(device)[late + 4] == std::byte{0x66} && MirrorCounters().rebuilds == again.rebuilds, "an address-based build does not map the sparse heap mirror up to its extent"); + GuestArena::GuestArenaCommit_nid_postfix(guest + grown, 65536, PAGE_READWRITE, 65536); + guest[grown + 2] = 0x44; + const auto [grownEnd, grownDevice] = address(); + Require(grownEnd == base + 3 * extent && grownDevice != device && access.addressBytes(grownDevice)[grown + 2] == std::byte{0x44} && access.addressBytes(grownDevice)[8] == std::byte{0x77}, "a sparse heap committed past its mirror was not mirrored again"); + { + GuestBufferMemory memory(context); + memory.AddReadable(base, reserved); + memory.AddReadable(base + past, 16); + memory.Upload(false); + std::uint32_t adjustment = 0; + const auto view = memory.Descriptor(base + past, 16, adjustment); + const auto whole = memory.Descriptor(base, reserved, adjustment); + Require(view.buffer == whole.buffer && access.bytes(whole.buffer)[whole.offset + grown + 2] == std::byte{0x44}, "a view past the sparse heap mirror is not copied with the heap"); + memory.WriteBack(); + } + registry(false); + sweep(); + Require(MirrorCounters().heapMirrors == before.heapMirrors && MirrorCounters().heapBytes == before.heapBytes, "the sparse heap mirror outlived its range"); + { + constexpr std::size_t whole = 1u << 20u; + GuestArena::GuestArenaReset_nid_postfix(block, reserved); + GuestArena::GuestArenaCommit_nid_postfix(guest, whole, PAGE_READWRITE, 65536); + std::memset(guest, 0x31, whole); + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block, whole, true, true, true); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + const auto start = MirrorCounters(); + const auto view = bind(true, [&](std::span bytes, const VkDescriptorBufferInfo& info) { + Require(info.range == whole, "a heap bound whole over a smaller registered range is not clamped to that range"); + Require(bytes[info.offset + whole - 1] == std::byte{0x31}, "a heap bound whole over a smaller registered range misses its last committed byte"); + bytes[info.offset + 16] = std::byte{0x12}; + }); + Require(view.range == whole && MirrorCounters().sparseBinds == start.sparseBinds + 1 && guest[16] == 0x12, "a heap bound whole over a smaller registered range was copied"); + mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + sweep(); + } + GuestArena::GuestArenaReset_nid_postfix(block, reserved); + GuestArena::GuestArenaRelease_nid_postfix(block, reserved); +#else + static_cast(context); + static_cast(access); +#endif +} + void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { alignas(64) std::array guest{}; guest[0] = 123; @@ -426,7 +3176,9 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { GuestBufferMemory memory(context); memory.AddWritable(address, sizeof(guest)); memory.AddWritable(address + 16, 16); + Require(memory.CopiedBytes() == 0, "a region counted as copied before its upload"); memory.Upload(true); + Require(memory.CopiedBytes() == sizeof(guest), "copied bytes " + std::to_string(memory.CopiedBytes()) + " do not match the one region copied"); std::uint32_t adjustment = 0; const auto first = memory.Descriptor(address, sizeof(guest), adjustment); Require(adjustment == 0, "a view at its owner's start is bound off it"); @@ -464,7 +3216,10 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { control.bindings.push_back(writable); const std::array stages{{{ShaderRecompiler::ShaderStage::TessellationControl, &control, 0}}}; const std::array unusedSnapshots{{{0, source}}}; - ShaderResources resources(disabled, stages, ColorTarget{}, 0, 0, unusedSnapshots); + ShaderResources resources(disabled, stages, ColorTarget{}, 0, 0, 0, unusedSnapshots); + Require(!ShaderResources::NeverReusable(stages), "a fault-buffer build is refused the resource cache"); + Require(!resources.Reusable(), "a fault-buffer build with copied writes is reusable"); + Require(resources.NeedsCompletion() && !resources.UsesAddressTable(), "a fault-buffer build reaches guest memory through the BDA table"); const auto fault = access.bytes(access.descriptor(5).buffer); for (const auto byte : fault) Require(byte == std::byte{}, "rect-list fault buffer was not initialized"); const ShaderRecompiler::BdaAbi::Fault report{ShaderRecompiler::BdaAbi::FaultState::Ready, ShaderRecompiler::BdaAbi::FaultReason::InvalidRectangle, 0, 0, 0, 0, 0}; @@ -476,8 +3231,27 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { resources.WriteBack(); Require(guest[1] == changed, "rect-list fault-only path lost guest buffer writes"); } + { + ShaderRecompiler::RecompileResult control; + control.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version; + control.bindings = {binding(Role::FaultBuffer, 5)}; + const std::array stages{{{ShaderRecompiler::ShaderStage::TessellationControl, &control, 0}}}; + ShaderResources resources(disabled, stages, ColorTarget{}, 0, 0, 0, {}); + Require(!ShaderResources::NeverReusable(stages) && resources.Reusable() && resources.ChecksFaults(), "a fault-only build is not reusable"); + resources.CheckFaults(); + const auto fault = access.bytes(access.descriptor(5).buffer); + const ShaderRecompiler::BdaAbi::Fault report{ShaderRecompiler::BdaAbi::FaultState::Ready, ShaderRecompiler::BdaAbi::FaultReason::InvalidRectangle, 0, 0, 0, 0, 0}; + std::memcpy(fault.data(), &report, sizeof(report)); + reject([&] { resources.CheckFaults(); }, "rect-list requires"); + } { ShaderResources resources(context, compiled, snapshots); + Require(ShaderResources::NeverReusable(std::span(&compiled, 1)) && !resources.Reusable(), "an address-based build is reusable"); + Require(resources.UsesAddressTable(), "an address-based build does not use the BDA table"); + ShaderRecompiler::RecompileResult plain; + plain.bindings = {binding(Role::GuestBuffers, 6)}; + const CompiledShader plainStage{ShaderRecompiler::ShaderStage::Compute, &plain, 0}; + Require(!ShaderResources::NeverReusable(std::span(&plainStage, 1)), "a build without address tables or a fault buffer is refused the resource cache"); const auto table = access.bytes(access.descriptor(4).buffer); ShaderRecompiler::BdaAbi::Header header{}; ShaderRecompiler::BdaAbi::Range range{}; @@ -509,6 +3283,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; @@ -534,11 +3329,38 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { tail.Upload(true); reject([&] { tail.Descriptor(address + 58, 4, adjustment); }, "exceeds its GPU owner"); } + { + void* plain = GuestHeap::GuestHeapAllocate_nid_postfix(64); + void* gpu = GuestHeap::GuestHeapAlignForGpu_nid_postfix(16, 64); + const auto lookup = [](const void* pointer) { + GuestAllocations::Mutation mutation; + return mutation.Find(pointer); + }; + Require(!lookup(plain).gpu && lookup(gpu).gpu, "a guest heap block's GPU access does not follow how it was allocated"); + gpu = GuestHeap::GuestHeapReallocate_nid_postfix(gpu, 128); + plain = GuestHeap::GuestHeapRealign_nid_postfix(plain, 128, 64); + Require(!lookup(plain).gpu && lookup(gpu).gpu, "a reallocated guest heap block changed its GPU access"); + { + GuestBufferMemory leased(context); + leased.AcquireRegistered(); + leased.Upload(true); + const auto ranges = leased.AddressRanges(); + const auto listed = [&](const void* pointer) { + const auto address = reinterpret_cast(pointer); + return std::any_of(ranges.begin(), ranges.end(), [&](const auto& range) { return range.begin <= address && address < range.end; }); + }; + Require(!listed(plain), "a guest heap block without GPU access is in an address-based build"); + Require(listed(gpu), "a guest heap block with GPU access is missing from an address-based build"); + leased.WriteBack(); + } + GuestHeap::GuestHeapFree_nid_postfix(plain); + GuestHeap::GuestHeapFree_nid_postfix(gpu); + } { // The cached address space: a second build in an unchanged registry takes the first one's // space, and a guest free of a range pinned only by the cache goes through the pin waiter's // drop (no GPU work to wait for) and empties the registry of it. - void* block = GuestHeap::GuestHeapAllocate_nid_postfix(64); + void* block = GuestHeap::GuestHeapAlignForGpu_nid_postfix(16, 64); const auto blockAddress = reinterpret_cast(block); std::memset(block, 0x5a, 64); const auto registered = [&] { @@ -568,9 +3390,67 @@ 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); heapMirrorTests(context, access); importedHeapMirrorTests(context, access); + importWatchTests(); + importedFreshTests(context); + 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 4cbfffe4c6..9ca39a5b01 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp @@ -3,15 +3,19 @@ #include "SpirvBackend/SpirvMemory/SpirvTypes.hpp" #include "SpirvBackend/SpirvMemory/SpirvConstants.hpp" #include "IntermediateRepresentation/IrBuilder.hpp" +#if ANYPS5_ENABLE_SPIRV_TOOLS +#include "SpirvBackend/SpirvOptimizer.hpp" +#endif namespace { template -std::vector MakeShader(std::uint64_t address, bool coherent, bool stops, const TEmit& emit, std::uint32_t extracted = 0) { +std::vector MakeShader(std::uint64_t address, bool coherent, bool stops, const TEmit& emit, std::uint32_t extracted = 0, bool writes = false) { using namespace ShaderRecompiler; IrProgram program; program.Resources().stage = IrShaderStage::Compute; program.Info().usesDma = true; + program.Info().bdaWrites = writes; if (coherent) { MemoryInfo memory; memory.coherent = true; @@ -39,7 +43,7 @@ std::vector MakeShader(std::uint64_t address, bool coherent, bool state.module.AddFunction(spv::OpFunction, TypeVoid(state), main, spv::FunctionControlMaskNone, TypeFunction(state)); EmitLabel(state, state.module.AllocateId()); SpirvValueEmitContext ctx(state); - auto& instruction = extracted != 0u ? program.CreateValue(IrOpcode::LoadAddressU32x4, IrType::U32x4) : program.CreateValue(IrOpcode::LoadAddressU32, IrType::U32); + auto& instruction = writes ? program.CreateValue(IrOpcode::StoreAddressU32, IrType::Void) : extracted != 0u ? program.CreateValue(IrOpcode::LoadAddressU32x4, IrType::U32x4) : program.CreateValue(IrOpcode::LoadAddressU32, IrType::U32); IrBuilder ir(program); for (std::uint32_t dword = 0; dword < 4u; ++dword) { if ((extracted & (1u << dword)) == 0u) continue; @@ -83,6 +87,21 @@ std::vector MakeBdaSpanReadTestShader(std::uint64_t address, std: }, extracted); } +std::vector MakeBdaDwordWriteTestShader(std::uint64_t address, std::uint32_t dwords, const std::uint32_t* values) { + using namespace ShaderRecompiler; + return MakeShader(address, false, true, [&](SpirvValueEmitContext& ctx, const IrValue& instruction, std::uint32_t base) { + auto& state = ctx.state; + std::vector parts; + for (std::uint32_t dword = 0; dword < dwords; ++dword) parts.push_back(ConstantU32(state, values[dword])); + const auto composite = state.module.AllocateId(); + if (dwords == 2u) state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 2u), composite, parts[0], parts[1]); + else if (dwords == 3u) state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 3u), composite, parts[0], parts[1], parts[2]); + else state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 4u), composite, parts[0], parts[1], parts[2], parts[3]); + EmitBdaDwordWrites(ctx, instruction, base, 0u, dwords, composite); + return std::vector{ConstantU32(state, 1u)}; + }, 0u, true); +} + std::vector MakeBdaDwordReadTestShader(std::uint64_t address, std::uint32_t dwords, bool coherent, bool stops) { using namespace ShaderRecompiler; return MakeShader(address, coherent, stops, [&](SpirvValueEmitContext& ctx, const IrValue& instruction, std::uint32_t base) { @@ -90,3 +109,126 @@ std::vector MakeBdaDwordReadTestShader(std::uint64_t address, std return std::vector(values.begin(), values.begin() + dwords); }); } + +std::vector MakeBdaStoreTestShader(std::uint64_t address, std::uint32_t value, std::uint32_t bits, bool coherent) { + using namespace ShaderRecompiler; + auto shader = MakeShader(address, coherent, true, [&](SpirvValueEmitContext& ctx, const IrValue& instruction, std::uint32_t base) { + EmitBdaStore(ctx, instruction, base, ConstantU32(ctx.state, value), bits); + return std::vector{ConstantU32(ctx.state, 1u)}; + }, 0u, true); +#if ANYPS5_ENABLE_SPIRV_TOOLS + shader = ValidateAndOptimizeSpirv(shader, VK_API_VERSION_1_1, shader[1], false, false); +#endif + return shader; +} + +std::vector MakeBdaWriteTestShader(std::uint64_t address, std::uint32_t value, std::uint32_t bits, bool coherent) { + using namespace ShaderRecompiler; + auto shader = MakeShader(address, coherent, true, [&](SpirvValueEmitContext& ctx, const IrValue& instruction, std::uint32_t base) { + EmitBdaWrite(ctx, instruction, base, ConstantU32(ctx.state, value), bits); + return std::vector{ConstantU32(ctx.state, 1u)}; + }, 0u, true); +#if ANYPS5_ENABLE_SPIRV_TOOLS + shader = ValidateAndOptimizeSpirv(shader, VK_API_VERSION_1_1, shader[1], false, false); +#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 cd61a127e0..e31020caf7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp @@ -2,12 +2,36 @@ #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 0d90f1c583..6aea082fff 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp @@ -12,9 +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 12edf8f22c..abdf8f0166 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp @@ -1,17 +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) { @@ -33,6 +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; @@ -41,6 +68,7 @@ VKAPI_ATTR VkResult VKAPI_CALL mockCreateBuffer(VkDevice, const VkBufferCreateIn *buffer = reinterpret_cast(static_cast(mock.next++)); mock.sizes[*buffer] = info->size; mock.usages[*buffer] = info->usage; + mock.liveBuffers.insert(*buffer); return VK_SUCCESS; } @@ -49,30 +77,55 @@ VKAPI_ATTR void VKAPI_CALL mockGetBufferMemoryRequirements(VkDevice, VkBuffer bu } VKAPI_ATTR VkResult VKAPI_CALL mockAllocateMemory(VkDevice, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks*, VkDeviceMemory* memory) { + ++mock.attempts; + auto result = VK_SUCCESS; + if (!mock.results.empty()) { + result = mock.results.front(); + mock.results.pop_front(); + } + if (result == VK_SUCCESS && mock.limit != 0 && mock.liveBytes + info->allocationSize > mock.limit) result = mock.pressureResult; + if (result != VK_SUCCESS) { + *memory = reinterpret_cast(std::uintptr_t{0xdeadbeef}); + return result; + } *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, VkDeviceMemory, VkDeviceSize) { +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, const VkAllocationCallbacks*) { +VKAPI_ATTR void VKAPI_CALL mockDestroyBuffer(VkDevice, VkBuffer buffer, const VkAllocationCallbacks*) { + Expect(mock.liveBuffers.erase(buffer) == 1, "an unknown buffer was destroyed"); ++mock.destroyedBuffers; } VKAPI_ATTR void VKAPI_CALL mockFreeMemory(VkDevice, VkDeviceMemory memory, const VkAllocationCallbacks*) { + if (!mock.memoryBytes.contains(memory)) { + Expect(false, "an undefined failed allocation output was freed"); + return; + } mock.liveBytes -= mock.memoryBytes.at(memory); mock.memoryBytes.erase(memory); mock.hostMemory.erase(memory); @@ -99,6 +152,14 @@ PFN_vkVoidFunction VKAPI_CALL mockProc(VkDevice, const char* name) { return it == table.end() ? nullptr : it->second; } +VkDeviceMemory MemoryOf(VkBuffer buffer) { + return mock.bound.at(buffer).first; +} + +VkDeviceSize ClassOf(VkBuffer buffer) { + return mock.memoryBytes.at(MemoryOf(buffer)); +} + Context mockContext() { Context context{}; context.deviceProc = mockProc; @@ -110,47 +171,182 @@ 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(); - VkBuffer first = VK_NULL_HANDLE; + VkDeviceMemory first = VK_NULL_HANDLE; { DeviceBuffer upload(context, 10 * MiB + 4096, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT); - first = upload.Handle(); - Expect(mock.sizes.at(first) == 11 * MiB, "a 10 MiB + 4 KiB device buffer was created with " + std::to_string(mock.sizes.at(first)) + " bytes, not its 11 MiB class"); + first = MemoryOf(upload.Handle()); + Expect(mock.sizes.at(upload.Handle()) == 10 * MiB + 4096, "a 10 MiB + 4 KiB device buffer was created with " + std::to_string(mock.sizes.at(upload.Handle())) + " bytes, not the requested size"); + Expect(ClassOf(upload.Handle()) == 11 * MiB, "a 10 MiB + 4 KiB device buffer has " + std::to_string(ClassOf(upload.Handle())) + " bytes of memory, not its 11 MiB class"); constexpr VkBufferUsageFlags all = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; - Expect((mock.usages.at(first) & all) == all, "a device buffer was created without storage and both transfer directions"); + Expect((mock.usages.at(upload.Handle()) & all) == all, "a device buffer was created without storage and both transfer directions"); Expect(upload.Size() == 10 * MiB + 4096, "a device buffer reports its class instead of the requested size"); } const auto made = mock.allocations; DeviceBuffer writeBack(context, 10 * MiB + 512 * 1024, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); - Expect(writeBack.Handle() == first && mock.allocations == made, "a retained 11 MiB device buffer did not serve a write-back of another size and direction in its class"); + Expect(MemoryOf(writeBack.Handle()) == first && mock.allocations == made, "a retained 11 MiB device buffer did not serve a write-back of another size and direction in its class"); Expect(writeBack.Size() == 10 * MiB + 512 * 1024, "a reused device buffer reports the retained size"); + 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(); constexpr auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; - VkBuffer twelve = VK_NULL_HANDLE; + VkDeviceMemory twelve = VK_NULL_HANDLE; { DeviceBuffer large(context, 12 * MiB, usage); - twelve = large.Handle(); + twelve = MemoryOf(large.Handle()); } { DeviceBuffer smaller(context, 7 * MiB, usage); - Expect(smaller.Handle() == twelve, "a 7 MiB request did not take the retained 12 MiB buffer"); + Expect(MemoryOf(smaller.Handle()) == twelve, "a 7 MiB request did not take the retained 12 MiB buffer"); } const auto made = mock.allocations; { DeviceBuffer tooSmall(context, 5 * MiB, usage); - Expect(tooSmall.Handle() != twelve && mock.allocations == made + 1, "a 5 MiB request took a retained buffer of more than twice its size"); + Expect(MemoryOf(tooSmall.Handle()) != twelve && mock.allocations == made + 1, "a 5 MiB request took a retained buffer of more than twice its size"); Expect(mock.sizes.at(tooSmall.Handle()) == 5 * MiB, "a 5 MiB device buffer was not created at its own class"); } DeviceBuffer again(context, 12 * MiB, usage); - Expect(again.Handle() == twelve, "a buffer served to a smaller request did not return to its own 12 MiB class"); + Expect(MemoryOf(again.Handle()) == twelve, "a buffer served to a smaller request did not return to its own 12 MiB class"); DeviceBuffer best(context, 4 * MiB + 1, usage); - Expect(best.Handle() != twelve && mock.sizes.at(best.Handle()) == 5 * MiB, "a 4 MiB + 1 request did not take the smallest retained class that fits (5 MiB)"); + Expect(MemoryOf(best.Handle()) != twelve && ClassOf(best.Handle()) == 5 * MiB, "a 4 MiB + 1 request did not take the smallest retained class that fits (5 MiB)"); } void KeptUntilFence() { @@ -181,28 +377,28 @@ void Budget() { constexpr auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; std::vector> live; for (int i = 0; i < 12; ++i) live.push_back(std::make_unique(context, (48 + static_cast(i) * 3) * MiB, usage)); - const auto first = live.front()->Handle(); + const auto first = MemoryOf(live.front()->Handle()); VkDeviceSize madeBytes = 0; - for (const auto& buffer : live) madeBytes += mock.sizes.at(buffer->Handle()); + for (const auto& buffer : live) madeBytes += ClassOf(buffer->Handle()); Expect(madeBytes > 512 * MiB, "the budget case does not exceed the budget"); for (auto& buffer : live) buffer.reset(); Expect(mock.frees != 0 && mock.liveBytes <= 512 * MiB, "the device tier retains " + std::to_string(mock.liveBytes / MiB) + " MiB after " + std::to_string(mock.frees) + " evictions, over its 512 MiB budget"); DeviceBuffer probe(context, 48 * MiB, usage); - Expect(probe.Handle() != first, "the least recently used device buffer survived the eviction"); + Expect(MemoryOf(probe.Handle()) != first, "the least recently used device buffer survived the eviction"); } void AddressAndHostUnchanged() { mock = MockDevice{}; auto context = mockContext(); - VkBuffer scratch = VK_NULL_HANDLE; + VkDeviceMemory scratch = VK_NULL_HANDLE; { DeviceBuffer buffer(context, 2 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT); - scratch = buffer.Handle(); + scratch = MemoryOf(buffer.Handle()); } Buffer addressed(context, 2 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - Expect(addressed.Handle() != scratch && addressed.DeviceAddress() != 0, "an addressable device-local buffer took a scratch buffer without a device address"); + Expect(MemoryOf(addressed.Handle()) != scratch && addressed.DeviceAddress() != 0, "an addressable device-local buffer took a scratch buffer without a device address"); Buffer shadow(context, 2 * MiB - 256, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - Expect(shadow.Handle() == scratch, "a device-local staging buffer did not share the scratch buffers' class"); + Expect(MemoryOf(shadow.Handle()) == scratch, "a device-local staging buffer did not share the scratch buffers' class"); VkBuffer host = VK_NULL_HANDLE; { Buffer copy(context, 10 * MiB + 4096, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, HostVisible); @@ -220,19 +416,19 @@ void SmallDeviceClasses() { mock = MockDevice{}; auto context = mockContext(); constexpr auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; - VkBuffer mebibyte = VK_NULL_HANDLE; - VkBuffer half = VK_NULL_HANDLE; + VkDeviceMemory mebibyte = VK_NULL_HANDLE; + VkDeviceMemory half = VK_NULL_HANDLE; { DeviceBuffer one(context, MiB, usage); - mebibyte = one.Handle(); + mebibyte = MemoryOf(one.Handle()); } { DeviceBuffer smaller(context, 300 * 1024, usage); - Expect(smaller.Handle() != mebibyte && mock.sizes.at(smaller.Handle()) == 512 * 1024, "a 300 KiB request took a 1 MiB buffer or skipped its power-of-two class"); - half = smaller.Handle(); + Expect(MemoryOf(smaller.Handle()) != mebibyte && ClassOf(smaller.Handle()) == 512 * 1024, "a 300 KiB request took a 1 MiB buffer or skipped its power-of-two class"); + half = MemoryOf(smaller.Handle()); } DeviceBuffer again(context, 260 * 1024, usage); - Expect(again.Handle() == half, "a 260 KiB request did not take the retained 512 KiB buffer"); + Expect(MemoryOf(again.Handle()) == half, "a 260 KiB request did not take the retained 512 KiB buffer"); constexpr auto indirect = VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; VkBuffer tiny = VK_NULL_HANDLE; { @@ -244,16 +440,615 @@ 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"); } -int main() { +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"); + Expect(!BufferPool::SlabEligible(512, 256, 640, 64), "a buffer needing more than its class was given a slab"); + Expect(!BufferPool::SlabEligible(512, 256, 512, 1024), "a size class below the non-coherent atom was given a slab"); + Expect(!BufferPool::SlabEligible(MiB, 256, MiB, 64), "an exact-size buffer was given a slab"); + mock = MockDevice{}; + { + auto context = mockContext(); + context.limits.nonCoherentAtomSize = 64; + constexpr VkBufferUsageFlags storage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; + { + Buffer a(context, 300, storage); + Buffer b(context, 300, storage); + Buffer c(context, 260, storage); + Expect(mock.allocations == 1, "buffers of one size class did not share one memory block"); + const auto block = mock.bound.at(a.Handle()).first; + std::set offsets; + for (auto* buffer : {&a, &b, &c}) { + const auto [memory, offset] = mock.bound.at(buffer->Handle()); + Expect(memory == block, "buffers of one size class were bound to different blocks"); + Expect(offset % 512 == 0, "a slab slot is not aligned to its size class"); + Expect(offsets.insert(offset).second, "two live buffers share a slab slot"); + Expect(buffer->Bytes().data() == mock.hostMemory.at(memory).data() + offset, "a slab buffer's mapping is not its slot"); + } + std::memset(a.Bytes().data(), 0x11, a.Bytes().size()); + std::memset(b.Bytes().data(), 0x22, b.Bytes().size()); + std::memset(c.Bytes().data(), 0x33, c.Bytes().size()); + Expect(std::all_of(a.Bytes().begin(), a.Bytes().end(), [](std::byte value) { return value == std::byte{0x11}; }), "a neighbouring slab buffer overwrote another"); + Expect(std::all_of(b.Bytes().begin(), b.Bytes().end(), [](std::byte value) { return value == std::byte{0x22}; }), "a neighbouring slab buffer overwrote another"); + Buffer larger(context, 600, storage); + Expect(mock.allocations == 2 && mock.bound.at(larger.Handle()).first != block, "another size class did not get a block of its own"); + } + Expect(mock.frees == 0, "releasing slab buffers freed device memory"); + Buffer again(context, 300, storage); + Expect(mock.allocations == 2, "a released slab buffer was not reused"); + } + Expect(mock.liveBytes == 0, "slab blocks outlived their pool"); + mock = MockDevice{}; + { + auto context = mockContext(); + BufferPool pool(context); + constexpr std::size_t slot = 512 * 1024; + const auto perBlock = static_cast(BufferPool::SlabBlockBytes(slot) / slot); + std::vector slots; + for (std::size_t i = 0; i <= perBlock; ++i) slots.push_back(pool.TakeSlot(context, 1, slot, false)); + Expect(mock.allocations == 2 && pool.SlabBlocks() == 2, "a full block did not open a second one"); + std::set> distinct; + for (const auto& taken : slots) distinct.insert({taken.memory, taken.offset}); + Expect(distinct.size() == slots.size(), "a slab handed out one slot twice"); + for (const auto& taken : slots) pool.PutSlot(taken.memory, taken.offset); + Expect(mock.frees == 1 && pool.SlabBlocks() == 1, "emptied blocks were not freed down to one spare"); + const auto reused = pool.TakeSlot(context, 1, slot, false); + Expect(mock.allocations == 2, "the spare block was not reused"); + pool.PutSlot(reused.memory, reused.offset); + } + Expect(mock.liveBytes == 0, "slab blocks outlived their pool"); +} + +void AllocationRecovery(VkResult pressureResult, bool host) { + mock = MockDevice{}; + { + auto context = mockContext(); + const auto make = [&](std::size_t bytes) -> std::shared_ptr { + if (host) return std::make_shared(context, bytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + return std::make_shared(context, bytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + }; + auto active = make(2 * MiB); + const auto activeBuffer = *mock.liveBuffers.begin(); + const auto activeMemory = mock.memoryBytes.begin()->first; + { auto idle = make(2 * MiB); } + mock.limit = 6 * MiB; + mock.pressureResult = pressureResult; + const auto before = mock.attempts; + try { + auto recovered = make(4 * MiB); + Expect(mock.attempts == before + 2, "pool pressure did not trigger exactly one allocation retry"); + Expect(mock.frees == 1 && mock.liveBytes == 6 * MiB, "recovery did not release the idle allocation"); + Expect(mock.liveBuffers.contains(activeBuffer) && mock.memoryBytes.contains(activeMemory), "recovery destroyed an active allocation"); + } catch (const std::exception& error) { + Expect(false, std::string("idle memory did not permit recovery: ") + error.what()); + } + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "recovery leaked a resource"); +} + +void SlabAllocationRecovery() { + mock = MockDevice{}; + { + auto context = mockContext(); + Buffer active(context, 300, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + const auto activeMemory = mock.bound.at(active.Handle()).first; + { DeviceBuffer idle(context, 2 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); } + mock.limit = 4 * MiB; + const auto before = mock.attempts; + try { + Buffer recovered(context, 600, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Expect(mock.attempts == before + 2 && mock.frees == 1, "a new slab block did not retry after reclaiming an idle allocation"); + Expect(context.bufferPool->SlabBlocks() == 2 && mock.memoryBytes.contains(activeMemory), "slab allocation recovery discarded its live block"); + } catch (const std::exception& error) { + Expect(false, std::string("slab allocation did not recover: ") + error.what()); + } + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "slab recovery leaked a resource"); +} + +void ReclaimSlabs() { + mock = MockDevice{}; + { + auto context = mockContext(); + Buffer active(context, 300, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + const auto activeMemory = mock.bound.at(active.Handle()).first; + std::memset(active.Bytes().data(), 0x5a, active.Bytes().size()); + { + Buffer sameBlock(context, 260, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Buffer otherBlock(context, 600, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + } + mock.limit = 6 * MiB; + const auto before = mock.attempts; + try { + Buffer recovered(context, 4 * MiB, VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + Expect(mock.attempts == before + 2 && mock.frees == 1, "reclaim did not free the unused slab block"); + Expect(context.bufferPool->SlabBlocks() == 1 && mock.memoryBytes.contains(activeMemory), "reclaim freed a slab with a live slot"); + Expect(std::all_of(active.Bytes().begin(), active.Bytes().end(), [](std::byte value) { return value == std::byte{0x5a}; }), "reclaim changed a live slab buffer"); + } catch (const std::exception& error) { + Expect(false, std::string("empty slab did not permit recovery: ") + error.what()); + } + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "slab reclaim leaked a resource"); + mock = MockDevice{}; + { + auto context = mockContext(); + BufferPool pool(context); + const auto idle = pool.TakeSlot(context, 1, 512 * 1024, false); + pool.PutSlot(idle.memory, idle.offset); + mock.limit = 4 * MiB; + const auto before = mock.attempts; + try { + const auto recovered = pool.TakeSlot(context, 1, 256 * 1024, false); + Expect(mock.attempts == before + 2 && mock.frees == 1 && pool.SlabBlocks() == 1, "a spare slab without cached buffers was not reclaimed"); + pool.PutSlot(recovered.memory, recovered.offset); + } catch (const std::exception& error) { + Expect(false, std::string("spare slab did not permit recovery: ") + error.what()); + } + } + Expect(mock.liveBytes == 0, "spare slab recovery leaked device memory"); +} + +void AllocationFailure(VkResult initial, VkResult final, bool cached) { + mock = MockDevice{}; + { + auto context = mockContext(); + if (cached) { DeviceBuffer idle(context, 2 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); } + mock.results = {initial, final}; + const auto before = mock.attempts; + std::string failure; + try { + DeviceBuffer buffer(context, 4 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + } catch (const std::exception& error) { + failure = error.what(); + } + const bool reclaimed = cached && (initial == VK_ERROR_OUT_OF_DEVICE_MEMORY || initial == VK_ERROR_OUT_OF_HOST_MEMORY); + Expect(mock.attempts == before + (reclaimed ? 2 : 1), "allocation retries were not bounded or a non-memory failure was retried"); + Expect(failure.find("Vulkan result " + std::to_string(reclaimed ? final : initial)) != std::string::npos, "allocation failure lost its final Vulkan result"); + Expect(mock.liveBuffers.size() == (cached && !reclaimed ? 1u : 0u), "a failed allocation retained its buffer"); + Expect(mock.liveBytes == (cached && !reclaimed ? 2 * MiB : 0), "a failed allocation mishandled cached memory"); + } + 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{}; + { + auto context = mockContext(); + { DeviceBuffer idle(context, 2 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); } + DeviceBuffer buffer(context, 4 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Expect(mock.attempts == 2 && mock.frees == 0 && mock.liveBytes == 6 * MiB, "a successful first allocation reclaimed its cache"); + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "normal allocation leaked a resource"); +} + +void PartialSlabDoesNotRetry() { + mock = MockDevice{}; + { + auto context = mockContext(); + Buffer active(context, 300, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + { Buffer idle(context, 260, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); } + mock.limit = 2 * MiB; + const auto before = mock.attempts; + std::string failure; + try { + Buffer buffer(context, 2 * MiB, VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + } catch (const std::exception& error) { + failure = error.what(); + } + Expect(failure.find("Vulkan result -2") != std::string::npos, "a live slab's memory was treated as reclaimable"); + Expect(mock.attempts == before + 1 && mock.frees == 0, "an allocation retried without freeing device memory"); + Expect(mock.liveBuffers.size() == 1 && mock.liveBuffers.contains(active.Handle()) && context.bufferPool->SlabBlocks() == 1, "reclaim changed a live slab allocation"); + } + 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"); + } +} + +} + +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); + AllocationRecovery(result, true); + AllocationFailure(result, result, true); + AllocationFailure(result, result, false); + } + SlabAllocationRecovery(); + 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/CmaskFastClear.cpp b/core/libs/prx/libSceAgcDriver/tests/CmaskFastClear.cpp new file mode 100644 index 0000000000..dfadd4aabe --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/CmaskFastClear.cpp @@ -0,0 +1,191 @@ +#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/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Cover_vert_spv.h" +#include "Cover_frag_spv.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace AgcDriver::Graphics; + +constexpr int VulkanTestSkipped = 77; +constexpr std::uint32_t Width = 61; +constexpr std::uint32_t Height = 13; +constexpr std::uint32_t Stale = 0x5a5a5a5au; +constexpr std::uint32_t Clear = 0x11223344u; +constexpr std::uint32_t Drawn = 0xffffffffu; + +alignas(4096) std::array SurfaceStorage{}; +const std::span Surface{reinterpret_cast((reinterpret_cast(SurfaceStorage.data()) + 65535u) & ~std::uintptr_t{65535u}), 65536}; +alignas(4096) std::array Cmask{}; + +ColorTarget target(ColorTileMode mode) { + const ColorTargetLayout layout(Width, Height, mode, 4); + ColorTarget color{}; + color.address = reinterpret_cast(Surface.data()); + color.extent = {Width, Height}; + color.format = VK_FORMAT_R8G8B8A8_UNORM; + color.bytes = layout.Bytes(); + color.componentMapping = 0xe4u; + color.tileMode = mode; + color.elementBytes = 4; + color.clearWords = {Clear, 0}; + color.surfaceAddress = color.address; + color.surfaceExtent = color.extent; + color.cmaskAddress = reinterpret_cast(Cmask.data()); + color.cmaskBytes = CmaskLayout(Width, Height).Bytes(); + return color; +} + +void prepare(const ColorTarget& color, const std::function& cleared, std::uint8_t code = 0) { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + StorageTexture::FlushPending(color.address, Surface.size(), nullptr, "test"); + std::vector stale(Surface.size()); + for (std::size_t offset = 0; offset < stale.size(); offset += 4) std::memcpy(stale.data() + offset, &Stale, 4); + AgcDriver::GuestMemory::Write(color.address, stale); + const CmaskLayout layout(Width, Height); + std::vector mask(Cmask.size(), std::byte{0xff}); + for (std::uint32_t tileY = 0; tileY < layout.TilesY(); ++tileY) { + for (std::uint32_t tileX = 0; tileX < layout.TilesX(); ++tileX) { + if (!cleared(tileX, tileY)) continue; + const auto nibble = layout.Nibble(tileX, tileY); + mask[nibble / 2u] &= std::byte{static_cast((nibble % 2u) == 0 ? 0xf0u : 0x0fu)}; + mask[nibble / 2u] |= std::byte{static_cast((nibble % 2u) == 0 ? code : code << 4u)}; + } + } + AgcDriver::GuestMemory::Write(color.cmaskAddress, mask); +} + +void draw(AgcDriver::VulkanDevice& device, const ColorTarget& color, VkRect2D scissor) { + ShaderRecompiler::RecompileResult vertex; + vertex.spirv = std::vector(std::begin(COVER_VERT_SPV), std::end(COVER_VERT_SPV)); + ShaderRecompiler::RecompileResult fragment; + fragment.spirv = std::vector(std::begin(COVER_FRAG_SPV), std::end(COVER_FRAG_SPV)); + const std::array shaders{{ + {ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, + {ShaderRecompiler::ShaderStage::Fragment, &fragment, 0} + }}; + State state{}; + state.stages = {ShaderPath::Vertex, 0u, 64u, 64u, std::nullopt, std::nullopt}; + state.color = color; + state.colors = {color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, 0, static_cast(Width), static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = scissor; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + device.Draw(state, {0, 3, 0, 1, 0, false}, shaders); + device.WaitIdle(); +} + +void expect(AgcDriver::VulkanDevice& device, const ColorTarget& color, const std::function& texel, const std::string& what) { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + StorageTexture::FlushPending(color.address, color.bytes, nullptr, "test"); + device.WaitIdle(); + const ColorTargetLayout layout(Width, Height, color.tileMode, 4); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + std::uint32_t stored = 0; + std::memcpy(&stored, Surface.data() + layout.Offset(x, y), 4); + Require(stored == texel(x, y), what + ": texel (" + std::to_string(x) + ", " + std::to_string(y) + ") holds " + std::to_string(stored)); + } + } + Require(std::all_of(Cmask.begin(), Cmask.end(), [](std::uint8_t value) { return value == 0xffu; }), what + ": the CMASK was not left expanded"); +} + +void eliminate(AgcDriver::VulkanDevice& device, const ColorTarget& color) { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + device.ColorMetadataPass({ColorMetadataPass::Mode::EliminateFastClear, {color}}); +} + +void run(AgcDriver::VulkanDevice& device, ColorTileMode mode, const std::string& name) { + const auto color = target(mode); + prepare(color, [](std::uint32_t, std::uint32_t) { return true; }); + draw(device, color, {{0, 0}, {20, Height}}); + expect(device, color, [](std::uint32_t x, std::uint32_t) { return x < 20 ? Drawn : Clear; }, name + " fast-cleared target drawn over its left part"); + draw(device, color, {{40, 0}, {Width - 40, Height}}); + expect(device, color, [](std::uint32_t x, std::uint32_t) { return x < 20 || x >= 40 ? Drawn : Clear; }, name + " fast-cleared target drawn again over its right part"); + prepare(color, [](std::uint32_t tileX, std::uint32_t tileY) { return (tileX == 0 && tileY == 0) || (tileX == 7 && tileY == 1); }); + draw(device, color, {{0, 0}, {20, Height}}); + expect(device, color, [](std::uint32_t x, std::uint32_t y) { return x < 20 ? Drawn : x >= 56 && y >= 8 ? Clear : Stale; }, name + " partly fast-cleared target drawn over its left part"); + prepare(color, [](std::uint32_t, std::uint32_t) { return true; }); + eliminate(device, color); + expect(device, color, [](std::uint32_t, std::uint32_t) { return Clear; }, name + " fast-cleared target eliminated without a draw"); + prepare(color, [](std::uint32_t tileX, std::uint32_t tileY) { return tileX == 7 && tileY == 1; }); + eliminate(device, color); + expect(device, color, [](std::uint32_t x, std::uint32_t y) { return x >= 56 && y >= 8 ? Clear : Stale; }, name + " partly fast-cleared edge tile eliminated"); + prepare(color, [](std::uint32_t tileX, std::uint32_t tileY) { return (tileX + tileY) % 2u == 0; }); + eliminate(device, color); + expect(device, color, [](std::uint32_t x, std::uint32_t y) { return (x / 8u + y / 8u) % 2u == 0 ? Clear : Stale; }, name + " checkerboard of fast-cleared tiles eliminated"); + prepare(color, [](std::uint32_t tileX, std::uint32_t tileY) { return tileX >= 3 && tileY == 1; }); + draw(device, color, {{0, 0}, {Width, 4}}); + expect(device, color, [](std::uint32_t x, std::uint32_t y) { return y < 4 ? Drawn : y >= 8 && x >= 24 ? Clear : Stale; }, name + " fast-cleared bottom edge tiles under a draw of the top rows"); +} + +void unknownCode(AgcDriver::VulkanDevice& device) { + const auto color = target(ColorTileMode::RenderTarget); + prepare(color, [](std::uint32_t tileX, std::uint32_t tileY) { return tileX == 2 && tileY == 0; }, 3); + eliminate(device, color); + expect(device, color, [](std::uint32_t, std::uint32_t) { return Stale; }, "a tile with CMASK code 3 eliminated"); + prepare(color, [](std::uint32_t, std::uint32_t) { return true; }); + eliminate(device, color); + expect(device, color, [](std::uint32_t, std::uint32_t) { return Clear; }, "fast-cleared target eliminated after a CMASK code 3"); +} + +} + +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; + } + { + GuestAllocations::Mutation mutation; + mutation.Add(SurfaceStorage.data(), SurfaceStorage.size(), true, true, true); + mutation.Add(Cmask.data(), Cmask.size(), true, true, true); + } + run(*device, ColorTileMode::Linear, "linear"); + run(*device, ColorTileMode::RenderTarget, "SW_64KB_R_X"); + unknownCode(*device); + device->WaitIdle(); + device.reset(); + { + GuestAllocations::Mutation mutation; + mutation.Remove(SurfaceStorage.data()); + mutation.Remove(Cmask.data()); + } + std::cout << "CMASK fast clear draw and eliminate 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/ColorTargetLayout.cpp b/core/libs/prx/libSceAgcDriver/tests/ColorTargetLayout.cpp index 40b69b7a4d..c77bdb157f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ColorTargetLayout.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ColorTargetLayout.cpp @@ -1,11 +1,14 @@ #include "BdaTests.hpp" #include "prx/libSceAgcDriver/Graphics/include/ColorTargetTransfer.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp" #include #include +#include #include #include #include +#include #include namespace { @@ -24,10 +27,20 @@ void reject(TAction action) { void RunColorTargetLayoutTests() { Require(DecodeColorTileMode(0x4dc6c000) == ColorTileMode::RenderTarget, "logged color descriptor was rejected"); Require(DecodeColorTileMode(0x09000000) == ColorTileMode::Linear, "linear descriptor changed"); - reject([] { DecodeColorTileMode(0x4dc6c001); }); + Require(DecodeColorTileMode(0x4dc6c001) == ColorTileMode::RenderTarget, "2D array color descriptor with MIP0_DEPTH 1 was rejected"); reject([] { DecodeColorTileMode(0x4dc6e000); }); reject([] { DecodeColorTileMode(0xcdc6c000); }); reject([] { DecodeColorTileMode(0x09004000); }); + Require(DecodeColorTileMode(0x4cc6c000) == ColorTileMode::RenderTarget, "1D color descriptor was rejected"); + reject([] { DecodeColorTileMode(0x4cc6c001); }); + reject([] { DecodeColorTileMode(0x4fc6c000); }); + Require(DecodeColorTileMode(0x08000000) == ColorTileMode::Linear, "linear 1D color descriptor was rejected"); + reject([] { DecodeColorTileMode(0x08014000); }); + reject([] { DecodeColorTileMode(0x08024000); }); + Require(DecodeColorTileMode(0x0da58000) == ColorTileMode::D4KBX, "a SW_4KB_D_X color descriptor with its Z-mode FMASK swizzle was rejected"); + Require(DecodeColorTileMode(0x0da54000) == ColorTileMode::S4KBX, "a SW_4KB_S_X color descriptor with its Z-mode FMASK swizzle was rejected"); + Require(DecodeColorTileMode(0x09058000) == ColorTileMode::D4KBX, "a SW_4KB_D_X color descriptor without an FMASK swizzle was rejected"); + reject([] { DecodeColorTileMode(0x0d458000); }); reject([] { ColorTargetLayout(0, 1, ColorTileMode::RenderTarget); }); const ColorTargetLayout padded(63, 2, ColorTileMode::Linear); Require(padded.Bytes() == 512 && padded.LinearBytes() == 504 && padded.Offset(0, 1) == 256, "linear rows are not padded to 256 bytes"); @@ -38,9 +51,16 @@ void RunColorTargetLayoutTests() { Require(layout.Offset(128, 0) == 65536 && layout.Offset(0, 128) == 3 * 65536, "block raster order is incorrect"); Require(layout.Offset(16, 0) == 0x2200 && layout.Offset(0, 8) == 0x1100, "render-target XOR addressing is incorrect"); Require(layout.Offset(256, 0) == 2 * 65536, "third block address is incorrect"); + const ColorTargetLayout r64(33, 65, ColorTileMode::RenderTarget, 8); + const ColorTargetLayout r128(129, 67, ColorTileMode::RenderTarget, 16); + Require(r64.Offset(0, 64) == 67584, "64 bpp SW_64KB_R_X dropped a cross-block y-coordinate XOR bit"); + Require(r128.Offset(64, 0) == 66560, "128 bpp SW_64KB_R_X dropped a cross-block x-coordinate XOR bit"); + Require(r64.Offset(32, 64) == 65536, "64 bpp SW_64KB_R_X did not XOR overlapping coordinate contributions"); + Require(r128.Offset(32, 64) == 3 * 65536, "128 bpp SW_64KB_R_X did not XOR overlapping coordinate contributions"); Require(DecodeColorTileMode(0x4dc14000) == ColorTileMode::Standard4KB, "4 KiB standard color descriptor was rejected"); const ColorTargetLayout standard(256, 256, ColorTileMode::Standard4KB); Require(standard.Bytes() == ComputeSurfaceSize(ComputeElementMipLayout(TextureTileMode::kStandard4KB, 4, 256, 256, 1), 1) && standard.Alignment() == 4096, "4 KiB standard color layout differs from the texture layout"); + Require(ColorTargetLayout(1920, 1080, ColorTileMode::RenderTarget, 16).Bytes() == 0x1fe0000u, "four-sample 1920x1080 color target should fit before the next 32 MiB allocation"); Require(standard.Offset(32, 0) == 4096 && standard.Offset(0, 32) == 8 * 4096 && standard.Offset(1, 0) == 4 && standard.Offset(0, 1) == 16, "4 KiB standard block or element addressing is incorrect"); std::vector standardSeen(1024); for (std::uint32_t y = 0; y < 32; ++y) { @@ -71,6 +91,33 @@ void RunColorTargetLayoutTests() { } Require(DecodeColorTileMode(0x4dc24000) == ColorTileMode::Standard64KB, "64 KiB standard color descriptor was rejected"); reject([&] { layout.Offset(257, 0); }); + Require(CmaskLayout(1, 1).Bytes() == 4096 && CmaskLayout(1024, 512).Bytes() == 4096 && CmaskLayout(1025, 513).Bytes() == 16384 && CmaskLayout(1920, 1080).Bytes() == 24576 && CmaskLayout(2432, 1368).Bytes() == 36864 && CmaskLayout(3328, 1872).Bytes() == 65536 && CmaskLayout(3840, 2160).Bytes() == 81920, "CMASK size differs from addrlib's pipe-aligned SW_64KB_Z_X CMASK"); + struct CmaskReference { + std::uint32_t width; + std::uint32_t height; + std::uint32_t tileX; + std::uint32_t tileY; + std::size_t nibble; + }; + constexpr std::array cmaskReferences{{ + {20, 12, 2, 1, 0x600u}, {61, 13, 7, 1, 0x1401u}, {61, 13, 5, 0, 0x1201u}, {1920, 1080, 239, 134, 0xb347u}, {1920, 1080, 128, 64, 0x6000u}, + {1920, 1080, 1, 1, 0x1u}, {3840, 2160, 479, 269, 0x2651du}, {3840, 2160, 200, 77, 0xb30cu}, {2432, 1368, 303, 170, 0x10acfu}, {1025, 513, 128, 64, 0x6000u}, + }}; + for (const auto& reference : cmaskReferences) Require(CmaskLayout(reference.width, reference.height).Nibble(reference.tileX, reference.tileY) == reference.nibble, "CMASK nibble address differs from addrlib's"); + for (const auto [width, height] : {std::pair{61u, 13u}, std::pair{1920u, 1080u}}) { + const CmaskLayout cmask(width, height); + std::vector seen(cmask.Bytes() * 2u); + for (std::uint32_t tileY = 0; tileY < cmask.TilesY(); ++tileY) { + for (std::uint32_t tileX = 0; tileX < cmask.TilesX(); ++tileX) { + const auto nibble = cmask.Nibble(tileX, tileY); + Require(nibble < seen.size() && !seen[nibble], "CMASK tiles alias or leave the CMASK"); + seen[nibble] = true; + } + } + } + reject([] { CmaskLayout(0, 1); }); + reject([] { CmaskLayout(61, 13).Nibble(8, 0); }); + reject([] { CmaskLayout(61, 13).Nibble(0, 2); }); std::vector tiled(layout.Bytes(), std::byte{0x5a}); std::vector linear(layout.LinearBytes()); std::vector restored(linear.size()); @@ -111,4 +158,109 @@ void RunColorTargetLayoutTests() { std::array linearReadback{}; ReadColorTarget(target, linearReadback); Require(linearReadback == linearPixels && guest[512] == std::byte{0x6b}, "linear guest color transfer changed"); + + const ColorTargetLayout plainLayout(256, 70, ColorTileMode::RenderTarget, 4); + const ColorTargetLayout xorLayout(256, 70, ColorTileMode::RenderTarget, 4, 0x5600); + for (const auto& [x, y] : {std::pair{0u, 0u}, std::pair{1u, 0u}, std::pair{0u, 1u}, std::pair{127u, 69u}, std::pair{128u, 0u}, std::pair{255u, 69u}, std::pair{200u, 33u}}) { + const auto plain = plainLayout.Offset(x, y); + Require(xorLayout.Offset(x, y) == (plain & ~std::size_t{0xffff}) + ((plain & 0xffffu) ^ 0x5600u), "a pipe/bank XOR did not apply to the offset inside the SW_64KB_R_X block"); + } + std::vector xorLinear(xorLayout.LinearBytes()); + for (std::size_t i = 0; i < xorLinear.size(); ++i) xorLinear[i] = static_cast((i * 7u) & 255u); + std::vector xorTiled(xorLayout.Bytes(), std::byte{0}); + std::vector xorRestored(xorLinear.size()); + xorLayout.Tile(xorLinear, xorTiled); + xorLayout.Detile(xorTiled, xorRestored); + Require(xorRestored == xorLinear, "color tiling round trip with a pipe/bank XOR lost pixels"); + reject([] { static_cast(ColorTargetLayout(64, 64, ColorTileMode::Standard64KB, 4, 0x100)); }); + reject([] { static_cast(ColorTargetLayout(64, 64, ColorTileMode::Linear, 4, 0x100)); }); + reject([] { static_cast(ColorTargetLayout(64, 64, ColorTileMode::RenderTarget, 4, 0x80)); }); + reject([] { static_cast(ColorTargetLayout(64, 64, ColorTileMode::RenderTarget, 4, 0x10000)); }); + + std::fill(guest.begin(), guest.end(), std::byte{0x6b}); + target = {reinterpret_cast(guest.data()), {2, 2}, VK_FORMAT_R8G8B8A8_UNORM, guest.size(), 0xe4, ColorTileMode::RenderTarget}; + target.pipeBankXor = 0x3200; + WriteColorTarget(target, pixels); + Require(guest[0x3200] == std::byte{0x32} && guest[0x3210] == std::byte{0x32} && guest[0] == std::byte{0x6b} && guest[16] == std::byte{0x6b}, "a color target with a pipe/bank XOR was written at the unswizzled offsets"); + readback.fill(std::byte{0}); + ReadColorTarget(target, readback); + Require(readback == pixels, "guest color transfer round trip with a pipe/bank XOR failed"); + + for (const auto mode : {ColorTileMode::S4KBX, ColorTileMode::D4KBX}) { + const auto* equation = FindTextureSwizzleEquation(static_cast(mode), 4u); + Require(equation != nullptr, "the 4 KiB XOR equation is missing"); + const auto expected = [&](std::uint32_t x, std::uint32_t y) { + std::size_t offset = 0; + for (std::uint32_t bit = 0; bit < 16u; ++bit) offset |= static_cast(std::popcount((x & equation->bits[bit] & 0xfffu) ^ ((y << 12u) & equation->bits[bit] & 0xfff000u)) & 1) << bit; + return offset; + }; + const ColorTargetLayout xor4Kb(40, 20, mode, 4, 0xa00); + Require(xor4Kb.Alignment() == 4096u && xor4Kb.Bytes() == 64u * 32u * 4u && xor4Kb.BlocksPerRow() == 2u, "the 4 KiB XOR color layout has the wrong block geometry"); + for (const auto& [x, y] : {std::pair{0u, 0u}, std::pair{1u, 0u}, std::pair{0u, 1u}, std::pair{31u, 19u}, std::pair{32u, 0u}, std::pair{39u, 19u}}) { + Require(xor4Kb.Offset(x, y) == (x / 32u) * 4096u + (expected(x % 32u, y) ^ 0xa00u), "a 4 KiB XOR color offset does not follow its swizzle equation and pipe/bank XOR"); + } + std::vector source(xor4Kb.LinearBytes()); + for (std::size_t i = 0; i < source.size(); ++i) source[i] = static_cast((i * 13u) & 255u); + std::vector tiled4Kb(xor4Kb.Bytes(), std::byte{0}); + std::vector back(source.size()); + xor4Kb.Tile(source, tiled4Kb); + xor4Kb.Detile(tiled4Kb, back); + Require(back == source, "4 KiB XOR color tiling round trip lost pixels"); + reject([mode] { static_cast(ColorTargetLayout(40, 20, mode, 4, 0x1000)); }); + } + + Require(DecodeColorTileMode(0x0dc68000u) == ColorTileMode::D64KBX, "the observed SW_64KB_D_X color descriptor was rejected"); + Require(DecodeColorTileMode(0x4dc60000u) == ColorTileMode::Z64KBX, "the observed SW_64KB_Z_X color descriptor was rejected"); + Require(ColorTextureTileMode(ColorTileMode::Z64KBX) == TextureTileMode::kZ64KBX, "a Z-order XOR color target did not map to its texture mode"); + Require(ColorTileModeIsXor(ColorTileMode::Z64KBX) && ColorTileModeBlockBytes(ColorTileMode::Z64KBX) == 65536u, "a Z-order XOR color target lost its block alignment or XOR flag"); + reject([] { DecodeColorTileMode((0x4dc60000u & ~(0x1fu << 19u)) | (1u << 19u)); }); + Require(ColorTextureTileMode(ColorTileMode::D64KBX) == TextureTileMode::kD64KBX, "a display XOR color target did not map to its texture mode"); + Require(ColorTileModeIsXor(ColorTileMode::D64KBX) && ColorTileModeBlockBytes(ColorTileMode::D64KBX) == 65536u, "a display XOR color target lost its block alignment or XOR flag"); + reject([] { DecodeColorTileMode(0x0cc68000u); }); + reject([] { DecodeColorTileMode((0x0dc68000u & ~(0x1fu << 19u)) | (1u << 19u)); }); + reject([] { ColorTargetLayout(64, 64, ColorTileMode::D64KBX, 4, 0x80); }); + reject([] { ColorTargetLayout(64, 64, ColorTileMode::D64KBX, 4, 0x10000); }); + for (const std::uint32_t columnBits : {0x1000u, 0x2000u, 0x3000u}) reject([columnBits] { ColorTargetLayout(64, 64, ColorTileMode::D64KBX, 4, columnBits); }); + const std::array, 5> sourceOffsets{{{27647u, 66048u}, {29694u, 212992u}, {62204u, 344064u}, {45304u, 540672u}, {110832u, 933888u}}}; + for (const std::uint32_t bpe : {1u, 2u, 4u, 8u, 16u}) { + const ColorTargetLayout sourceLayout(257, 129, ColorTileMode::D64KBX, bpe, 0x4000); + const auto& offsets = sourceOffsets[std::countr_zero(bpe)]; + for (std::uint32_t xorTag = 0; xorTag < 256u; ++xorTag) { + if ((xorTag & ~0xcfu) != 0) continue; + const ColorTargetLayout xorLayout(1, 1, ColorTileMode::D64KBX, bpe, xorTag << 8u); + Require(xorLayout.Offset(0, 0) == (xorTag << 8u), "a display XOR color origin differs from its pipe/bank bits"); + } + Require(sourceLayout.Offset(127, 63) == offsets[0] && sourceLayout.Offset(256, 128) == offsets[1], "display XOR color offsets differ from the AMD GFX10 non-RB+ source reference"); + const auto block = ThinBlockLayout(TextureTileMode::kD64KBX, bpe); + const auto width = 2u * block[1] + 1u; + const auto height = 2u * block[2] + 1u; + for (const auto mode : {ColorTileMode::Z64KBX, ColorTileMode::D64KBX, ColorTileMode::RenderTarget}) { + const auto* equation = FindTextureSwizzleEquation(static_cast(mode), bpe); + Require(equation != nullptr, "a 64 KiB color swizzle equation is missing"); + const ColorTargetLayout layout(width, height, mode, bpe, 0x4600); + Require(layout.Alignment() == block[0] && layout.BlocksPerRow() == 3u && layout.Bytes() == ComputeSurfaceSize(ComputeElementMipLayout(ColorTextureTileMode(mode), bpe, width, height, 1), 1), "a 64 KiB color layout differs from its texture geometry"); + for (const auto x : {0u, 1u, block[1] - 1u, block[1], block[1] + 1u, width - 1u}) { + for (const auto y : {0u, 1u, block[2] - 1u, block[2], block[2] + 1u, height - 1u}) { + std::size_t expected = 0; + for (std::uint32_t bit = 0; bit < 16u; ++bit) expected |= static_cast(std::popcount((x & equation->bits[bit] & 0xfffu) ^ ((y << 12u) & equation->bits[bit] & 0xfff000u)) & 1) << bit; + const auto blockIndex = (y / block[2]) * 3u + x / block[1]; + Require(layout.Offset(x, y) == blockIndex * 65536u + (expected ^ 0x4600u), "a 64 KiB color offset differs from its mode's equation across block boundaries"); + } + } + std::vector source(layout.LinearBytes()); + for (std::size_t index = 0; index < source.size(); ++index) source[index] = static_cast((index * 31u + index / 251u) & 255u); + std::vector tiled(layout.Bytes()); + std::vector restored(source.size()); + layout.Tile(source, tiled); + layout.Detile(tiled, restored); + Require(restored == source, "64 KiB display/render-target XOR color tiling lost pixels"); + } + } + std::fill(guest.begin(), guest.end(), std::byte{0x6b}); + target = {reinterpret_cast(guest.data()), {2, 2}, VK_FORMAT_R8G8B8A8_UNORM, guest.size(), 0xe4, ColorTileMode::D64KBX}; + target.pipeBankXor = 0x0200; + WriteColorTarget(target, pixels); + readback.fill(std::byte{0}); + ReadColorTarget(target, readback); + Require(readback == pixels && guest[0] == std::byte{0x6b}, "display XOR guest color transfer lost pixels or wrote the untagged block origin"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/ColorTransferTests.cpp b/core/libs/prx/libSceAgcDriver/tests/ColorTransferTests.cpp index 6a032c776a..4554351ae3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ColorTransferTests.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ColorTransferTests.cpp @@ -1,6 +1,9 @@ #include "ColorTransferTests.hpp" #include "prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp" +#include "prx/libSceAgcDriver/Graphics/include/HalfFloatScanout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp" #include +#include #include #include #include @@ -53,6 +56,125 @@ void checkConversion(const Context& context, std::uint32_t width, std::uint32_t Require(std::equal(expectedTiled.begin(), expectedTiled.end(), guest.begin()), "GPU tile changed pixels or surface padding"); } +std::size_t renderTargetOffset64(std::uint32_t blocksPerRow, std::uint32_t x, std::uint32_t y) { + const auto* equation = FindTextureSwizzleEquation(27u, 8u); + Require(equation != nullptr, "no 8-byte SW_64KB_R_X equation"); + std::size_t offset = 0; + for (std::uint32_t bit = 0; bit < 16u; ++bit) { + const auto mask = equation->bits[bit]; + offset |= static_cast(std::popcount((x & mask & 0xfffu) ^ ((y << 12u) & mask & 0xfff000u)) & 1) << bit; + } + return (static_cast(y / 64u) * blocksPerRow + x / 128u) * 65536u + offset; +} + +std::uint32_t deviceLocalType(const Context& context, std::uint32_t bits) { + for (std::uint32_t i = 0; i < context.memory.memoryTypeCount; ++i) { + if ((bits & (1u << i)) != 0 && (context.memory.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0) return i; + } + Require(false, "no device-local memory type for the test image"); + return 0; +} + +void checkHalfFloatDetile(const Context& context, std::uint32_t width, std::uint32_t height, bool redLow) { + const ColorTargetLayout layout(width, height, ColorTileMode::RenderTarget, 8); + std::vector storage(layout.Bytes() + layout.Alignment()); + void* aligned = storage.data(); + auto available = storage.size(); + Require(std::align(layout.Alignment(), layout.Bytes(), aligned, available) != nullptr, "test surface alignment failed"); + std::span guest(static_cast(aligned), layout.Bytes()); + std::fill(guest.begin(), guest.end(), std::byte{0xa5}); + std::vector texels(static_cast(width) * height * 8); + std::vector expected(static_cast(width) * height * 4); + std::uint32_t seed = redLow ? 0x2545f491u : 0x9e3779b9u; + for (std::uint32_t y = 0; y < height; ++y) { + for (std::uint32_t x = 0; x < width; ++x) { + std::array halves{}; + for (auto& half : halves) { + seed = seed * 1664525u + 1013904223u; + half = static_cast(seed >> 16u); + } + const auto index = static_cast(y) * width + x; + std::memcpy(texels.data() + index * 8, halves.data(), sizeof(halves)); + std::memcpy(guest.data() + renderTargetOffset64(layout.BlocksPerRow(), x, y), halves.data(), sizeof(halves)); + auto* pixel = expected.data() + index * 4; + pixel[0] = static_cast(HalfFloatScanoutCode(halves[redLow ? 2 : 0], false)); + pixel[1] = static_cast(HalfFloatScanoutCode(halves[1], false)); + pixel[2] = static_cast(HalfFloatScanoutCode(halves[redLow ? 0 : 2], false)); + pixel[3] = static_cast(HalfFloatScanoutCode(halves[3], true)); + } + } + VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + imageInfo.imageType = VK_IMAGE_TYPE_2D; + imageInfo.format = VK_FORMAT_R16G16B16A16_SFLOAT; + imageInfo.extent = {width, height, 1}; + imageInfo.mipLevels = 1; + imageInfo.arrayLayers = 1; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + Check(context.Function("vkCreateImage")(context.device, &imageInfo, nullptr, &image), "vkCreateImage half-float detile test"); + struct Release { + const Context& context; + VkImage image; + VkDeviceMemory& memory; + ~Release() { + context.Function("vkDestroyImage")(context.device, image, nullptr); + if (memory) context.Function("vkFreeMemory")(context.device, memory, nullptr); + } + } release{context, image, memory}; + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = deviceLocalType(context, requirements.memoryTypeBits); + Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory half-float detile test"); + Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory half-float detile test"); + Buffer upload(context, texels.size(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + std::memcpy(upload.Bytes().data(), texels.data(), texels.size()); + Buffer fromGuest(context, expected.size(), VK_BUFFER_USAGE_TRANSFER_DST_BIT); + Buffer fromImage(context, expected.size(), VK_BUFFER_USAGE_TRANSFER_DST_BIT); + GpuColorTransfer transfer(context); + const auto barrier = context.Function("vkCmdPipelineBarrier"); + const auto readback = [&](VkCommandBuffer commands, Buffer& destination) { + const VkBufferCopy copy{0, 0, expected.size()}; + context.Function("vkCmdCopyBuffer")(commands, transfer.LinearBuffer(), destination.Handle(), 1, ©); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_HOST_READ_BIT); + }; + transfer.Upload(reinterpret_cast(aligned), width, height, ColorTileMode::RenderTarget, 8); + { + CommandBatch batch(context); + const auto commands = batch.Handle(); + VkImageMemoryBarrier toTransfer{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + toTransfer.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toTransfer.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + toTransfer.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + toTransfer.srcQueueFamilyIndex = toTransfer.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toTransfer.image = image; + toTransfer.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + barrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toTransfer); + VkBufferImageCopy region{}; + region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + region.imageExtent = {width, height, 1}; + context.Function("vkCmdCopyBufferToImage")(commands, upload.Handle(), image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); + auto toGeneral = toTransfer; + toGeneral.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toGeneral.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + toGeneral.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL; + barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); + transfer.Detile(commands, redLow, false, true); + readback(commands, fromGuest); + transfer.DetileImage(commands, image, VK_IMAGE_LAYOUT_GENERAL, width, height, ColorTileMode::RenderTarget, redLow, false, true); + readback(commands, fromImage); + batch.SubmitAndWait(); + } + Require(std::equal(expected.begin(), expected.end(), fromGuest.Bytes().begin()), "GPU 16-bit float detile from guest memory differs from the reference"); + Require(std::equal(expected.begin(), expected.end(), fromImage.Bytes().begin()), "GPU 16-bit float detile from an image differs from the reference"); +} + void checkBufferReuse(const Context& context) { constexpr auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT; VkBuffer handle = VK_NULL_HANDLE; @@ -115,4 +237,6 @@ void RunColorTransferTests(const AgcDriver::Graphics::Context& context) { checkConversion(context, 130, 129, AgcDriver::Graphics::ColorTileMode::RenderTarget, false); checkConversion(context, 257, 17, AgcDriver::Graphics::ColorTileMode::RenderTarget, true); checkConversion(context, 192, 13, AgcDriver::Graphics::ColorTileMode::Linear, false); + checkHalfFloatDetile(context, 300, 130, true); + checkHalfFloatDetile(context, 129, 70, false); } 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 new file mode 100644 index 0000000000..eb6a6b104b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCapture.cpp @@ -0,0 +1,303 @@ +#include "../tools/ComputePipelineFixture.hpp" +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +template +void Reject(TAction action, const char* message) { + bool rejected = false; + try { + action(); + } catch (const std::exception&) { + rejected = true; + } + 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() { + path = std::filesystem::temp_directory_path() / ("anyps5-compute-capture-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(path), "could not create test directory"); + } + ~Directory() { + std::error_code error; + std::filesystem::remove_all(path, error); + } + std::filesystem::path path; +}; + +void CheckRoundTrip() { + auto capture = AgcDriver::ComputePipelineFixture(); + capture.deviceName = "test GPU \"name\""; + capture.vendorId = 0x10de; + capture.deviceId = 0x2b85; + capture.driverVersion = 0x12345678; + capture.apiVersion = VK_API_VERSION_1_3; + capture.pipelineCacheUuid[0] = 0x12; + capture.pipelineCacheUuid[15] = 0xab; + capture.requiredSubgroupSize = 32; + capture.flags = VK_PIPELINE_CREATE_DISPATCH_BASE_BIT; + capture.sets = {{VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT, + {{3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 5, VK_SHADER_STAGE_COMPUTE_BIT, VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT}, + {7, VK_DESCRIPTOR_TYPE_SAMPLER, 2, VK_SHADER_STAGE_COMPUTE_BIT, 0}}}}; + capture.pushConstants = {{VK_SHADER_STAGE_COMPUTE_BIT, 16, 32}}; + const auto encoded = capture.Encode(); + const auto decoded = AgcDriver::ComputePipelineCapture::Decode(encoded); + Require(decoded.Encode() == encoded, "capture round trip changed pipeline inputs"); + Require(decoded.Identity() == capture.Identity(), "capture identity changed after decoding"); + auto changed = decoded; + ++changed.specialization[0].value; + Require(changed.Identity() != capture.Identity(), "specialization did not affect capture identity"); + changed = decoded; + ++changed.sets[0].bindings[0].count; + Require(changed.Identity() != capture.Identity(), "descriptor layout did not affect capture identity"); + 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"); + Require(capture.Write(directory.path) == file, "identical capture was not deduplicated"); + std::ofstream(file, std::ios::binary | std::ios::trunc).put('x'); + Reject([&] { capture.Write(directory.path); }, "existing damaged capture was overwritten"); + Require(std::filesystem::file_size(file) == 1, "damaged capture was changed"); +} + +void CheckLargeRecord() { + auto capture = AgcDriver::ComputePipelineFixture(); + constexpr std::size_t words = 5500000; + capture.spirv.insert(capture.spirv.end() - 2, words - capture.spirv.size(), 0x00010000u); + const auto encoded = capture.Encode(); + Require(encoded.size() > 20u * 1024u * 1024u, "large fixture did not exceed the former file bound"); + Require(AgcDriver::ComputePipelineCapture::Decode(encoded).spirv == capture.spirv, "large module changed during decoding"); + Directory directory; + const auto file = capture.Write(directory.path); + Require(AgcDriver::ComputePipelineCapture::Read(file).spirv == capture.spirv, "large module changed on disk"); + capture.spirv.resize(AgcDriver::ComputePipelineCapture::MaxSpirvWords + 1u, 0x00010000u); + Reject([&] { capture.Encode(); }, "module beyond the word bound accepted"); + std::vector oversized(AgcDriver::ComputePipelineCapture::MaxFileBytes + 1u); + Reject([&] { AgcDriver::ComputePipelineCapture::Decode(oversized); }, "record beyond the file bound accepted"); + const auto oversizedFile = directory.path / "oversized.aps5cp"; + { + std::ofstream output(oversizedFile, std::ios::binary); + output.seekp(static_cast(AgcDriver::ComputePipelineCapture::MaxFileBytes)); + output.put('x'); + Require(output.good(), "cannot write oversized capture fixture"); + } + Reject([&] { AgcDriver::ComputePipelineCapture::Read(oversizedFile); }, "file beyond the file bound accepted"); +} + +void CheckBindingFlags() { + auto capture = AgcDriver::ComputePipelineFixture(); + capture.sets = {{0, {{7, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 4, VK_SHADER_STAGE_COMPUTE_BIT, VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT}, + {3, VK_DESCRIPTOR_TYPE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT}}}}; + const auto encoded = capture.Encode(); + Require(AgcDriver::ComputePipelineCapture::Decode(encoded).Encode() == encoded, "binding flags changed without update-after-bind"); + auto changed = capture; + changed.sets[0].bindings[0].flags = VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT; + Require(changed.Identity() != capture.Identity(), "binding flags did not affect capture identity"); + changed = capture; + changed.sets[0].bindings[1].flags |= VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT; + Reject([&] { changed.Encode(); }, "non-highest variable descriptor binding accepted"); + changed = capture; + changed.sets[0].bindings[0].flags |= VK_DESCRIPTOR_BINDING_UPDATE_UNUSED_WHILE_PENDING_BIT; + Reject([&] { changed.Encode(); }, "unsupported binding flags accepted"); + capture.sets[0].flags = VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT; + capture.sets[0].bindings[0].type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + capture.sets[0].bindings[0].flags |= VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT; + Require(AgcDriver::ComputePipelineCapture::Decode(capture.Encode()).Encode() == capture.Encode(), "combined binding flags changed"); +} + +void CheckMalformedRecords() { + const auto capture = AgcDriver::ComputePipelineFixture(); + const auto encoded = capture.Encode(); + for (std::size_t length = 0; length < encoded.size(); ++length) { + Reject([&] { AgcDriver::ComputePipelineCapture::Decode(std::span(encoded).first(length)); }, "truncated capture accepted"); + } + auto bytes = encoded; + bytes.push_back(std::byte{0}); + Reject([&] { AgcDriver::ComputePipelineCapture::Decode(bytes); }, "trailing capture bytes accepted"); + bytes = encoded; + bytes[0] ^= std::byte{1}; + Reject([&] { AgcDriver::ComputePipelineCapture::Decode(bytes); }, "invalid signature accepted"); + bytes = encoded; + bytes[8] = std::byte{0xff}; + Reject([&] { AgcDriver::ComputePipelineCapture::Decode(bytes); }, "unknown capture version accepted"); + bytes = encoded; + for (std::size_t index = 104; index < 108; ++index) bytes[index] = std::byte{0xff}; + Reject([&] { AgcDriver::ComputePipelineCapture::Decode(bytes); }, "unbounded device name accepted"); + const auto invalid = [&](auto change) { + auto copy = capture; + change(copy); + Reject([&] { copy.Encode(); }, "unsupported pipeline input accepted"); + }; + invalid([](auto& copy) { copy.spirv[0] = 0; }); + invalid([](auto& copy) { copy.spirv[5] = 0; }); + 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_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}}; }); + invalid([](auto& copy) { copy.pushConstants = {{VK_SHADER_STAGE_COMPUTE_BIT, 1, 4}}; }); + invalid([](auto& copy) { copy.sets = {{0, {{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 0, VK_SHADER_STAGE_COMPUTE_BIT, 0}}}}; }); + invalid([](auto& copy) { copy.sets = {{0, {{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_VERTEX_BIT, 0}}}}; }); + invalid([](auto& copy) { copy.sets = {{0, {{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT}}}}; }); + 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"); +} + +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(); + CheckLargeRecord(); + std::puts("compute pipeline capture: round trip, 5500000-word module, binding flags, immutable writes and bounded inputs passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "compute pipeline capture test: %s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCaptureRun.cmake b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCaptureRun.cmake new file mode 100644 index 0000000000..fb15007573 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCaptureRun.cmake @@ -0,0 +1,68 @@ +foreach(required IN ITEMS FIXTURE REPLAY OUTPUT_ROOT) + if(NOT DEFINED ${required}) + message(FATAL_ERROR "Missing ${required}") + endif() +endforeach() +string(RANDOM LENGTH 16 ALPHABET 0123456789abcdef runId) +set(runDirectory "${OUTPUT_ROOT}/compute-capture-${runId}") +set(captureDirectory "${runDirectory}/captures") +file(MAKE_DIRECTORY "${runDirectory}") +execute_process( + COMMAND "${CMAKE_COMMAND}" -E env "ANYPS5_NO_SHADER_CACHE=1" "APS5_PIPELINE_STATS=0" "APS5_DUMP_COMPUTE_PIPELINES=" "APS5_DUMP_COMPUTE_MIN_WORDS=0" "${FIXTURE}" + WORKING_DIRECTORY "${runDirectory}" + RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE error TIMEOUT 20 +) +file(WRITE "${runDirectory}/normal.log" "${output}\n${error}") +if(result STREQUAL "77" AND NOT DEFINED ENV{ANYPS5_REQUIRE_VULKAN}) + message(STATUS "compute pipeline capture run: skipped, no usable Vulkan device") + file(REMOVE_RECURSE "${runDirectory}") + return() +endif() +if(NOT result STREQUAL "0" OR NOT output MATCHES "branch past endpgm tests passed") + message(FATAL_ERROR "Normal compute fixture failed: ${result}\n${output}\n${error}") +endif() +execute_process( + COMMAND "${CMAKE_COMMAND}" -E env "ANYPS5_REQUIRE_VULKAN=1" "ANYPS5_NO_SHADER_CACHE=1" "APS5_PIPELINE_STATS=0" "APS5_DUMP_COMPUTE_PIPELINES=${captureDirectory}" "APS5_DUMP_COMPUTE_MIN_WORDS=0" "${FIXTURE}" + WORKING_DIRECTORY "${runDirectory}" + RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE error TIMEOUT 20 +) +file(WRITE "${runDirectory}/capture.log" "${output}\n${error}") +if(NOT result STREQUAL "0" OR NOT output MATCHES "branch past endpgm tests passed") + message(FATAL_ERROR "Captured compute fixture failed: ${result}\n${output}\n${error}") +endif() +file(GLOB captures "${captureDirectory}/compute-*/capture.aps5cp") +list(LENGTH captures count) +if(count LESS 1 OR count GREATER 12) + message(FATAL_ERROR "Expected 1 to 12 compute captures, found ${count}") +endif() +foreach(capture IN LISTS captures) + get_filename_component(recordDirectory "${capture}" DIRECTORY) + get_filename_component(recordName "${recordDirectory}" NAME) + string(REPLACE "compute-" "" identity "${recordName}") + set(report "${runDirectory}/${recordName}.json") + execute_process( + COMMAND "${CMAKE_COMMAND}" -E env "APS5_PIPELINE_STATS=0" "APS5_DUMP_COMPUTE_PIPELINES=" "${REPLAY}" "${capture}" --output "${report}" + WORKING_DIRECTORY "${runDirectory}" + RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE error TIMEOUT 20 + ) + file(WRITE "${runDirectory}/${recordName}.log" "${output}\n${error}") + if(NOT result STREQUAL "0") + message(FATAL_ERROR "Captured pipeline replay failed: ${result}\n${output}\n${error}") + endif() + file(READ "${report}" reportJson) + string(JSON reportIdentity GET "${reportJson}" capture_identity) + string(JSON words GET "${reportJson}" spirv_words) + string(JSON sets GET "${reportJson}" descriptor_sets) + string(JSON ranges GET "${reportJson}" push_constant_ranges) + string(JSON dispatches GET "${reportJson}" dispatch_count) + string(JSON policy GET "${reportJson}" application_cache_policy) + string(JSON capturedDevice GET "${reportJson}" captured_device device_id) + string(JSON replayDevice GET "${reportJson}" replay_device device_id) + string(JSON capturedDriver GET "${reportJson}" captured_device driver_version_raw) + string(JSON replayDriver GET "${reportJson}" replay_device driver_version_raw) + if(NOT reportIdentity STREQUAL identity OR words LESS 54 OR sets LESS 1 OR ranges LESS 1 OR NOT dispatches EQUAL 0 OR NOT policy STREQUAL "fresh" OR NOT capturedDevice STREQUAL replayDevice OR NOT capturedDriver STREQUAL replayDriver) + message(FATAL_ERROR "Captured pipeline metadata mismatch: ${reportJson}") + endif() +endforeach() +message(STATUS "Normal and captured compute output verified; ${count} captured pipelines replayed without dispatch") +file(REMOVE_RECURSE "${runDirectory}") diff --git a/core/libs/prx/libSceAgcDriver/tests/ComputePipelineReplay.cpp b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineReplay.cpp new file mode 100644 index 0000000000..3e1f179365 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineReplay.cpp @@ -0,0 +1,122 @@ +#include "../tools/ComputePipelineFixture.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +void CheckResult(const AgcDriver::ComputePipelineReplayResult& result) { + Require(!result.deviceName.empty() && result.vendorId != 0 && result.apiVersion != 0, "replay did not report its device"); + Require(result.cache.size() >= 32, "replay did not export a pipeline cache header"); + for (const auto duration : {result.moduleMilliseconds, result.layoutMilliseconds, result.pipelineMilliseconds, result.cacheMilliseconds, result.totalMilliseconds}) { + Require(std::isfinite(duration) && duration >= 0, "replay reported an invalid duration"); + } + Require(result.totalMilliseconds >= result.pipelineMilliseconds, "pipeline duration exceeds replay duration"); +} + +} + +int main() { + try { + auto capture = AgcDriver::ComputePipelineFixture(); + const auto encoded = capture.Encode(); + const auto decoded = AgcDriver::ComputePipelineCapture::Decode(encoded); + std::unique_ptr device; + try { + device = std::make_unique(AgcDriver::VulkanDevice::CompileOnly{}); + } catch (const std::exception& error) { + const std::string_view message(error.what()); + const bool absent = (message.starts_with("Vulkan loader: ") && message != "Vulkan loader: vkGetInstanceProcAddr missing") || message == "Vulkan: no Vulkan 1.1 graphics and compute queue" || message == "vkCreateInstance: Vulkan result -9"; + if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr || !absent) throw; + std::printf("skipped, no usable Vulkan device: %s\n", error.what()); + return 77; + } + auto sampled1D = decoded; + sampled1D.spirv.insert(sampled1D.spirv.begin() + 7, {0x00020011u, 43u}); + CheckResult(device->ReplayComputePipeline(sampled1D)); + std::puts("compute pipeline replay: core Vulkan Sampled1D capability compiled"); + for (const auto capability : {6u, 0xffffffffu}) { + auto unsupported = decoded; + unsupported.spirv.insert(unsupported.spirv.begin() + 7, {0x00020011u, capability}); + bool refused = false; + try { + device->ReplayComputePipeline(unsupported); + } catch (const std::exception& error) { + Require(std::string_view(error.what()) == "Vulkan presentation: captured SPIR-V capability is not enabled", "unsupported capability failed outside the replay capability guard"); + refused = true; + } + Require(refused, "non-Vulkan or unknown SPIR-V capability accepted"); + } + std::puts("compute pipeline replay: non-Vulkan Kernel and unknown capabilities rejected"); + const auto fresh = device->ReplayComputePipeline(decoded); + CheckResult(fresh); + Require(fresh.initialCacheBytes == 0, "fresh replay imported cache bytes"); + const auto imported = device->ReplayComputePipeline(decoded, fresh.cache); + CheckResult(imported); + Require(imported.initialCacheBytes == fresh.cache.size(), "replay did not import the supplied cache"); + Require(fresh.deviceId == imported.deviceId && fresh.driverVersion == imported.driverVersion && fresh.pipelineCacheUuid == imported.pipelineCacheUuid, "replay device identity changed"); + Require(capture.Encode() == encoded, "replay modified the captured pipeline"); + auto corrupt = fresh.cache; + corrupt[8] ^= std::byte{0xff}; + bool rejected = false; + try { + device->ReplayComputePipeline(decoded, corrupt); + } catch (const std::exception&) { + rejected = true; + } + Require(rejected, "cache from another vendor was accepted"); + corrupt = fresh.cache; + corrupt[16] ^= std::byte{0xff}; + rejected = false; + try { + device->ReplayComputePipeline(decoded, corrupt); + } catch (const std::exception&) { + rejected = true; + } + Require(rejected, "cache with another pipeline UUID was accepted"); + for (const auto flags : {VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT, VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT, static_cast(VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT)}) { + auto indexed = decoded; + indexed.sets[0].bindings[0].flags = flags; + try { + CheckResult(device->ReplayComputePipeline(indexed)); + std::printf("compute pipeline replay: descriptor binding flags 0x%x compiled without update-after-bind\n", static_cast(flags)); + } catch (const std::exception& error) { + if (std::getenv("ANYPS5_REQUIRE_DESCRIPTOR_BINDING_FLAGS") != nullptr) throw; + const std::string_view message(error.what()); + Require(message == "Vulkan presentation: captured partially-bound descriptors are unsupported" || message == "Vulkan presentation: captured variable descriptor count is unsupported", "binding flag replay failed without an explicit feature refusal"); + std::printf("compute pipeline replay: descriptor binding flags 0x%x explicitly refused: %s\n", static_cast(flags), error.what()); + } + } + auto runtimeArray = decoded; + runtimeArray.spirv.insert(runtimeArray.spirv.begin() + 7, {0x00020011u, 5302u, 0x0008000au, 0x5f565053u, 0x5f545845u, 0x63736564u, 0x74706972u, 0x695f726fu, 0x7865646eu, 0x00676e69u}); + try { + CheckResult(device->ReplayComputePipeline(runtimeArray)); + std::puts("compute pipeline replay: runtime descriptor array capability compiled"); + } catch (const std::exception& error) { + if (std::getenv("ANYPS5_REQUIRE_DESCRIPTOR_BINDING_FLAGS") != nullptr) throw; + Require(std::string_view(error.what()) == "Vulkan presentation: captured SPIR-V capability is not enabled", "runtime descriptor array replay failed without an explicit feature refusal"); + std::puts("compute pipeline replay: device explicitly refused runtime descriptor array capability"); + } + capture.requiredSubgroupSize = 32; + try { + const auto subgroup = device->ReplayComputePipeline(capture); + CheckResult(subgroup); + std::puts("compute pipeline replay: required subgroup size 32 compiled"); + } catch (const std::exception& error) { + Require(std::string_view(error.what()).find("captured required subgroup size is unsupported") != std::string_view::npos, "required subgroup replay failed without an explicit support refusal"); + std::puts("compute pipeline replay: device explicitly refused required subgroup size 32"); + } + std::printf("compute pipeline replay: fresh %.6f ms, imported %.6f ms, GPU %s; no dispatch\n", fresh.pipelineMilliseconds, imported.pipelineMilliseconds, fresh.deviceName.c_str()); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "compute pipeline replay test: %s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp b/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp new file mode 100644 index 0000000000..06d73e6578 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp @@ -0,0 +1,250 @@ +#include "IntermediateRepresentation/IrBuilder.hpp" +#include "Optimization/ConstantFolder.hpp" +#include "Optimization/DeadCodeEliminator.hpp" +#include +#include +#include + +using namespace ShaderRecompiler; + +namespace { + +bool foldsTo(std::uint32_t word, bool expected, bool maskedShift) { + 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& shift = maskedShift ? builder.Emit(IrOpcode::BitwiseAnd32, IrType::U32, {&lane, &builder.Constant(31u)}) : lane; + auto& shifted = builder.Emit(IrOpcode::ShiftRightLogical32, IrType::U32, {&builder.Constant(word), &shift}); + 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}); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + ConstantFolder().Fold(program); + const IrValue* result = user.Argument(0)->Resolve(); + if (!maskedShift || (word != 0u && word != 0xffffffffu)) return result->Opcode() == IrOpcode::INotEqual32; + 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(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + IrBuilder builder(program); + builder.SetInsertionPoint(block); + auto& reversed = builder.Emit(IrOpcode::BitReverse32, IrType::U32, {&builder.Constant(word)}); + auto& test = builder.Emit(IrOpcode::IEqual32, IrType::Bool, {&reversed, &builder.Constant(expected)}); + auto& user = builder.Emit(IrOpcode::Reference, IrType::Void, {&test}); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + ConstantFolder().Fold(program); + const IrValue* result = user.Argument(0)->Resolve(); + 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; +} + +} + +int main() { + try { + bool passed = true; + passed &= run("an all-zero word must fold the lane bit test to false", foldsTo(0u, false, true)); + 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()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/ConstantPhi.cpp b/core/libs/prx/libSceAgcDriver/tests/ConstantPhi.cpp new file mode 100644 index 0000000000..2cc4a8b370 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/ConstantPhi.cpp @@ -0,0 +1,136 @@ +#include "IntermediateRepresentation/IrBuilder.hpp" +#include "Optimization/ConstantFolder.hpp" +#include "Optimization/DeadCodeEliminator.hpp" +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +void CheckDiamond(unsigned mode) { + IrProgram program; + for (std::uint32_t id = 0; id < 4u; ++id) { + auto& block = program.CreateBlock(); + program.BlockOrder().push_back(&block); + BlockInfo info; + info.id = id; + info.terminator.kind = TerminatorKind::Return; + program.Metadata().blockInfo.push_back(info); + } + auto& entry = *program.BlockOrder()[0]; + auto& left = *program.BlockOrder()[1]; + auto& right = *program.BlockOrder()[2]; + auto& merge = *program.BlockOrder()[3]; + program.SetEntryBlock(entry); + entry.AddBranch(&left); + entry.AddBranch(&right); + left.AddBranch(&merge); + right.AddBranch(&merge); + auto& entryInfo = program.Metadata().blockInfo[0]; + entryInfo.terminator.kind = TerminatorKind::ConditionalBranch; + entryInfo.terminator.trueBlock = 1u; + entryInfo.terminator.falseBlock = 2u; + for (std::uint32_t id = 1; id < 3u; ++id) { + program.Metadata().blockInfo[id].terminator.kind = TerminatorKind::Branch; + program.Metadata().blockInfo[id].terminator.trueBlock = 3u; + } + IrBuilder builder(program); + builder.SetInsertionPoint(entry); + auto& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + auto& limit = builder.Constant(16u); + auto& condition = builder.Emit(IrOpcode::ULessThan32, IrType::Bool, {&lane, &limit}); + entryInfo.condition = &condition; + auto& one = builder.Constant(1u); + auto& two = builder.Constant(2u); + IrValue* first = &lane; + IrValue* second = &lane; + if (mode == 0u || mode == 1u) { + builder.SetInsertionPoint(left); + first = mode == 0u ? &builder.Emit(IrOpcode::SelectU32, IrType::U32, {&condition, &one, &two}) : &builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + builder.SetInsertionPoint(right); + second = mode == 0u ? &builder.Emit(IrOpcode::SelectU32, IrType::U32, {&condition, &one, &two}) : &builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + } else if (mode == 3u || mode == 4u) { + first = &one; + second = &program.CreateValue(IrOpcode::Void, IrType::U32); + second->SetImmediateU32(mode == 3u ? 1u : 2u); + } + builder.SetInsertionPoint(merge); + auto& phi = builder.Emit(IrOpcode::Phi, IrType::U32, {}); + phi.AddPhiOperand(&left, first); + phi.AddPhiOperand(&right, second); + auto& reference = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&phi}); + ValidateProgram(program, true); + ConstantFolder{}.Fold(program); + DeadCodeEliminator{}.RemoveIdentities(program); + DeadCodeEliminator{}.Eliminate(program); + ValidateProgram(program, true); + const auto* result = reference.Argument(0)->Resolve(); + if (mode == 0u || mode == 1u || mode == 4u) { + Require(result == &phi && phi.Argument(0) == first && phi.Argument(1) == second, "phi lost distinct branch definitions or constants"); + } else { + Require(result == first, "phi did not fold identical definitions or equal constants"); + } +} + +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) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} 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 14e020f130..3febba4f40 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp @@ -6,9 +6,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -43,24 +45,73 @@ void require(bool condition, const std::string& message) { if (!condition) throw std::runtime_error(message); } -std::vector constructBlocks(const ControlFlowGraph& graph, std::uint32_t header, std::uint32_t exclude) { +class StructuralDominators { +public: + explicit StructuralDominators(const ControlFlowGraph& graph) : dominators(graph.blocks.size()) { + std::vector> predecessors(graph.blocks.size()); + for (const auto& block : graph.blocks) { + for (const auto successor : block.successors) predecessors[successor].push_back(block.id); + const auto& terminator = block.terminator; + if (terminator.mergeBlock != InvalidControlFlowId) predecessors[terminator.mergeBlock].push_back(block.id); + if (terminator.loopHeader && terminator.continueBlock != InvalidControlFlowId) predecessors[terminator.continueBlock].push_back(block.id); + } + + std::vector all; + all.reserve(graph.blocks.size()); + for (const auto& block : graph.blocks) all.push_back(block.id); + for (const auto& block : graph.blocks) dominators[block.id] = block.id == graph.entryBlock ? std::vector{block.id} : all; + + bool changed = true; + while (changed) { + changed = false; + for (const auto& block : graph.blocks) { + if (block.id == graph.entryBlock) continue; + auto next = predecessors[block.id].empty() ? std::vector{} : dominators[predecessors[block.id].front()]; + for (std::size_t i = 1; i < predecessors[block.id].size(); ++i) { + std::vector intersection; + const auto& other = dominators[predecessors[block.id][i]]; + std::set_intersection(next.begin(), next.end(), other.begin(), other.end(), std::back_inserter(intersection)); + next = std::move(intersection); + } + if (!std::binary_search(next.begin(), next.end(), block.id)) { + next.insert(std::lower_bound(next.begin(), next.end(), block.id), block.id); + } + if (next != dominators[block.id]) { + dominators[block.id] = std::move(next); + changed = true; + } + } + } + } + + bool Dominates(std::uint32_t dominator, std::uint32_t block) const { + const auto& values = dominators.at(block); + return std::binary_search(values.begin(), values.end(), dominator); + } + +private: + std::vector> dominators; +}; + +std::vector constructBlocks(const ControlFlowGraph& graph, const StructuralDominators& dominators, std::uint32_t header, std::uint32_t exclude) { std::vector blocks; for (const auto& block : graph.blocks) { - if (graph.Dominates(header, block.id) && (exclude == InvalidControlFlowId || !graph.Dominates(exclude, block.id))) { + if (dominators.Dominates(header, block.id) && (exclude == InvalidControlFlowId || !dominators.Dominates(exclude, block.id))) { blocks.push_back(block.id); } } return blocks; } -bool inEnclosingContinueConstruct(const ControlFlowGraph& graph, std::uint32_t header, std::uint32_t member) { +bool inEnclosingContinueConstruct(const ControlFlowGraph& graph, const StructuralDominators& dominators, std::uint32_t header, std::uint32_t member) { return std::any_of(graph.blocks.begin(), graph.blocks.end(), [&](const BasicBlock& loop) { const auto continueBlock = loop.terminator.continueBlock; - return loop.terminator.loopHeader && graph.Dominates(loop.id, header) && !graph.Dominates(continueBlock, header) && graph.Dominates(continueBlock, member); + return loop.terminator.loopHeader && dominators.Dominates(loop.id, header) && !dominators.Dominates(continueBlock, header) && dominators.Dominates(continueBlock, member); }); } void verifyStructured(const std::string& prefix, const ControlFlowGraph& graph) { + const StructuralDominators dominators(graph); std::map mergeOwners; std::vector> loopExits; for (const auto& block : graph.blocks) { @@ -72,10 +123,10 @@ void verifyStructured(const std::string& prefix, const ControlFlowGraph& graph) for (const auto& block : graph.blocks) { const auto& terminator = block.terminator; if (terminator.loopHeader || terminator.mergeBlock == InvalidControlFlowId) continue; - for (const auto member : constructBlocks(graph, block.id, terminator.mergeBlock)) { - if (inEnclosingContinueConstruct(graph, block.id, member)) continue; + for (const auto member : constructBlocks(graph, dominators, block.id, terminator.mergeBlock)) { + if (inEnclosingContinueConstruct(graph, dominators, block.id, member)) continue; for (const auto successor : graph.FindBlock(member).successors) { - if (successor == terminator.mergeBlock || (graph.Dominates(block.id, successor) && !graph.Dominates(terminator.mergeBlock, successor))) continue; + if (successor == terminator.mergeBlock || (dominators.Dominates(block.id, successor) && !dominators.Dominates(terminator.mergeBlock, successor))) continue; const bool loopExit = std::any_of(loopExits.begin(), loopExits.end(), [&](const auto& exits) { return successor == exits.first || successor == exits.second; }); require(loopExit, prefix + "block " + std::to_string(member) + " of the selection at block " + std::to_string(block.id) + " branches to block " + std::to_string(successor) + " outside the construct"); } @@ -84,14 +135,51 @@ void verifyStructured(const std::string& prefix, const ControlFlowGraph& graph) for (const auto& loop : graph.blocks) { if (!loop.terminator.loopHeader) continue; const auto continueBlock = loop.terminator.continueBlock; - for (const auto member : constructBlocks(graph, loop.id, loop.terminator.mergeBlock)) { + for (const auto member : constructBlocks(graph, dominators, loop.id, loop.terminator.mergeBlock)) { const auto& terminator = graph.FindBlock(member).terminator; - if (terminator.loopHeader || graph.Dominates(continueBlock, member)) continue; + if (terminator.loopHeader || dominators.Dominates(continueBlock, member)) continue; require(terminator.mergeBlock != continueBlock, prefix + "the selection at block " + std::to_string(member) + " in the loop at block " + std::to_string(loop.id) + " merges at the loop's continue block " + std::to_string(continueBlock)); } } } +ControlFlowGraph makeGraph(const std::vector>& successors) { + ControlFlowGraph graph; + graph.entryBlock = 0; + for (std::uint32_t id = 0; id < successors.size(); ++id) { + BasicBlock block; + block.id = id; + block.startProgramCounter = id * 8; + block.endProgramCounter = id * 8 + 8; + block.instructionBegin = id * 2; + block.instructionEnd = id * 2 + 2; + block.successors = successors[id]; + auto& terminator = block.terminator; + if (successors[id].empty()) { + terminator.kind = TerminatorKind::Return; + } else if (successors[id].size() == 1) { + terminator.kind = TerminatorKind::Branch; + terminator.trueBlock = successors[id][0]; + } else { + terminator.kind = TerminatorKind::ConditionalBranch; + terminator.condition = BranchCondition::SccNonZero; + terminator.trueBlock = successors[id][0]; + terminator.falseBlock = successors[id][1]; + } + graph.blocks.push_back(std::move(block)); + } + for (const auto& block : graph.blocks) { + for (const auto successor : block.successors) graph.blocks[successor].predecessors.push_back(block.id); + } + return graph; +} + +void verifyLoopMergeDoesNotEnterNestedSelection() { + auto graph = makeGraph({{1, 5}, {2, 3}, {5}, {5, 4}, {1}, {}}); + Structurizer{}.Structurize(graph); + verifyStructured("loop merge structural dominance: ", graph); +} + std::uint32_t followEmptyBlocks(const std::string& prefix, const ControlFlowGraph& graph, std::uint32_t blockId, RouteState& routes) { for (std::size_t steps = 0; steps <= graph.blocks.size(); ++steps) { const auto& block = graph.FindBlock(blockId); @@ -182,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}; @@ -208,10 +296,141 @@ 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, + 0x060202ffu, 0x3f003000u, 0x060202ffu, 0x3f004000u, 0x060202ffu, 0x3f005000u, 0x060202ffu, 0x3f006000u, + 0x060202ffu, 0x3f007000u, 0x060202ffu, 0x3f008000u, 0x060202ffu, 0x3f009000u, 0x060202ffu, 0x3f00a000u, + 0x060202ffu, 0x3f00b000u, 0x060202ffu, 0x3f00c000u, 0x060202ffu, 0x3f00d000u, 0x060202ffu, 0x3f00e000u, + 0x060202ffu, 0x3f00f000u, 0x060202ffu, 0x3f010000u, 0x060202ffu, 0x3f810000u, 0x060202ffu, 0x3f000010u, + 0x060202ffu, 0x3f001010u, 0x060202ffu, 0x3f002010u, 0x060202ffu, 0x3f003010u, 0x060202ffu, 0x3f004010u, + 0x060202ffu, 0x3f005010u, 0x060202ffu, 0x3f006010u, 0x060202ffu, 0x3f007010u, 0x060202ffu, 0x3f008010u, + 0x060202ffu, 0x3f009010u, 0x060202ffu, 0x3f00a010u, 0x060202ffu, 0x3f00b010u, 0x060202ffu, 0x3f00c010u, + 0x060202ffu, 0x3f00d010u, 0x060202ffu, 0x3f00e010u, 0x060202ffu, 0x3f00f010u, 0x060202ffu, 0x3f010010u, + 0x7d880084u, 0xbf870025u, 0x060202ffu, 0x3f830000u, 0x060202ffu, 0x3f000030u, 0x060202ffu, 0x3f001030u, + 0x060202ffu, 0x3f002030u, 0x060202ffu, 0x3f003030u, 0x060202ffu, 0x3f004030u, 0x060202ffu, 0x3f005030u, + 0x060202ffu, 0x3f006030u, 0x060202ffu, 0x3f007030u, 0x060202ffu, 0x3f008030u, 0x060202ffu, 0x3f009030u, + 0x060202ffu, 0x3f00a030u, 0x060202ffu, 0x3f00b030u, 0x060202ffu, 0x3f00c030u, 0x060202ffu, 0x3f00d030u, + 0x060202ffu, 0x3f00e030u, 0x060202ffu, 0x3f00f030u, 0x060202ffu, 0x3f010030u, 0xbf820099u, 0x060202ffu, + 0x3f820000u, 0x060202ffu, 0x3f000020u, 0x060202ffu, 0x3f001020u, 0x060202ffu, 0x3f002020u, 0x060202ffu, + 0x3f003020u, 0x060202ffu, 0x3f004020u, 0x060202ffu, 0x3f005020u, 0x060202ffu, 0x3f006020u, 0x060202ffu, + 0x3f007020u, 0x060202ffu, 0x3f008020u, 0x060202ffu, 0x3f009020u, 0x060202ffu, 0x3f00a020u, 0x060202ffu, + 0x3f00b020u, 0x060202ffu, 0x3f00c020u, 0x060202ffu, 0x3f00d020u, 0x060202ffu, 0x3f00e020u, 0x060202ffu, + 0x3f00f020u, 0x060202ffu, 0x3f010020u, 0x7d880085u, 0xbf87004cu, 0x060202ffu, 0x3f850000u, 0x060202ffu, + 0x3f000050u, 0x060202ffu, 0x3f001050u, 0x060202ffu, 0x3f002050u, 0x060202ffu, 0x3f003050u, 0x060202ffu, + 0x3f004050u, 0x060202ffu, 0x3f005050u, 0x060202ffu, 0x3f006050u, 0x060202ffu, 0x3f007050u, 0x060202ffu, + 0x3f008050u, 0x060202ffu, 0x3f009050u, 0x060202ffu, 0x3f00a050u, 0x060202ffu, 0x3f00b050u, 0x060202ffu, + 0x3f00c050u, 0x060202ffu, 0x3f00d050u, 0x060202ffu, 0x3f00e050u, 0x060202ffu, 0x3f00f050u, 0x060202ffu, + 0x3f010050u, 0x7d880088u, 0xbf870001u, 0xbf82ff68u, 0x060202ffu, 0x3f870000u, 0x060202ffu, 0x3f000070u, + 0x060202ffu, 0x3f001070u, 0x060202ffu, 0x3f002070u, 0x060202ffu, 0x3f003070u, 0x060202ffu, 0x3f004070u, + 0x060202ffu, 0x3f005070u, 0x060202ffu, 0x3f006070u, 0x060202ffu, 0x3f007070u, 0x060202ffu, 0x3f008070u, + 0x060202ffu, 0x3f009070u, 0x060202ffu, 0x3f00a070u, 0x060202ffu, 0x3f00b070u, 0x060202ffu, 0x3f00c070u, + 0x060202ffu, 0x3f00d070u, 0x060202ffu, 0x3f00e070u, 0x060202ffu, 0x3f00f070u, 0x060202ffu, 0x3f010070u, + 0xbf820027u, 0x060202ffu, 0x3f840000u, 0x060202ffu, 0x3f000040u, 0x060202ffu, 0x3f001040u, 0x060202ffu, + 0x3f002040u, 0x060202ffu, 0x3f003040u, 0x060202ffu, 0x3f004040u, 0x060202ffu, 0x3f005040u, 0x060202ffu, + 0x3f006040u, 0x060202ffu, 0x3f007040u, 0x060202ffu, 0x3f008040u, 0x060202ffu, 0x3f009040u, 0x060202ffu, + 0x3f00a040u, 0x060202ffu, 0x3f00b040u, 0x060202ffu, 0x3f00c040u, 0x060202ffu, 0x3f00d040u, 0x060202ffu, + 0x3f00e040u, 0x060202ffu, 0x3f00f040u, 0x060202ffu, 0x3f010040u, 0x7d880087u, 0xbf87ff43u, 0xbf82ff8du, + 0x060202ffu, 0x3f860000u, 0x060202ffu, 0x3f000060u, 0x060202ffu, 0x3f001060u, 0x060202ffu, 0x3f002060u, + 0x060202ffu, 0x3f003060u, 0x060202ffu, 0x3f004060u, 0x060202ffu, 0x3f005060u, 0x060202ffu, 0x3f006060u, + 0x060202ffu, 0x3f007060u, 0x060202ffu, 0x3f008060u, 0x060202ffu, 0x3f009060u, 0x060202ffu, 0x3f00a060u, + 0x060202ffu, 0x3f00b060u, 0x060202ffu, 0x3f00c060u, 0x060202ffu, 0x3f00d060u, 0x060202ffu, 0x3f00e060u, + 0x060202ffu, 0x3f00f060u, 0x060202ffu, 0x3f010060u, 0xe0700000u, 0x80010100u, 0xbf810000u, + }; + const std::string name("large loop-exit shader preserves the original guarded fallback"); + static_cast(verifyGraph(name, code)); + static_cast(recompile(name, code, 32u)); + static_cast(recompile(name, code, 64u)); +} + +std::vector branchProgram(std::span> targets, std::span order) { + constexpr auto none = InvalidControlFlowId; + std::vector code; + std::vector starts(targets.size()); + std::vector> patches; + for (std::size_t at = 0; at < order.size(); ++at) { + const auto block = order[at]; + starts[block] = code.size(); + code.insert(code.end(), {0x060202ffu, 0x3f800000u + block * 0x10000u}); + for (std::uint32_t padding = 0; padding < 17; ++padding) code.insert(code.end(), {0x060202ffu, 0x3f000000u + padding * 0x1000u + block * 0x10u}); + const auto next = at + 1 < order.size() ? order[at + 1] : none; + const auto [first, second] = targets[block]; + if (first == none) { + code.insert(code.end(), {0xe0700000u, 0x80010100u, 0xbf810000u}); + continue; + } + if (second != none) { + code.push_back(0x7d880080u + block + 3u); + patches.emplace_back(code.size(), first); + code.push_back(0xbf870000u); + } + const auto branch = second != none ? second : first; + if (branch != next) { + patches.emplace_back(code.size(), branch); + code.push_back(0xbf820000u); + } + } + for (const auto& [instruction, target] : patches) { + const auto displacement = static_cast(starts[target]) - static_cast(instruction) - 1; + require(displacement >= -32768 && displacement <= 32767, "a generated test branch is out of range"); + code[instruction] |= static_cast(displacement) & 0xffffu; + } + return code; +} + +void verifyNestedExitForwarders() { + constexpr auto none = InvalidControlFlowId; + constexpr std::array, 21> targets{{ + {1, none}, {2, none}, {2, 3}, {2, 4}, {5, 6}, {1, 7}, {8, 9}, {10, none}, + {11, 12}, {13, 14}, {1, 15}, {16, none}, {9, none}, {17, 18}, {19, none}, + {16, none}, {none, none}, {16, none}, {15, 20}, {10, none}, {19, none}, + }}; + constexpr std::array order{0, 1, 2, 3, 4, 6, 9, 14, 19, 10, 15, 13, 18, 20, 17, 8, 12, 11, 5, 7, 16}; + const auto code = branchProgram(targets, order); + const std::string name("nested exit forwarders keep conditional selections structured"); + static_cast(verifyGraph(name, code)); + static_cast(recompile(name, code, 32u)); + static_cast(recompile(name, code, 64u)); +} + +void verifyIrreducibleLoopEntry() { + constexpr auto none = InvalidControlFlowId; + constexpr std::array, 4> targets{{{1, 2}, {2, 3}, {1, 3}, {none, none}}}; + constexpr std::array order{0, 1, 2, 3}; + const auto code = branchProgram(targets, order); + const std::string name("a loop entered at either of its two blocks runs behind reach flags"); + const auto result = verifyGraph(name, code); + require(result.clonedInstructions == 0 && result.routeVariables != 0, name + ": expected the reach flag fallback, got " + std::to_string(result.clonedInstructions) + " cloned instructions and " + std::to_string(result.routeVariables) + " route variables"); + static_cast(recompile(name, code, 32u)); + static_cast(recompile(name, code, 64u)); +} + void verifyProgram(const Program& program) { const std::string name(program.name); const auto result = verifyGraph(name, program.code); - if (program.split == Split::None) { + if (std::getenv("APS5_FORCE_STRUCTURIZER_FALLBACK") != nullptr) { + require(result.clonedInstructions == 0 && result.routeVariables != 0, name + ": expected the reach flag fallback, got " + std::to_string(result.clonedInstructions) + " cloned instructions and " + std::to_string(result.routeVariables) + " route variables"); + } else if (program.split == Split::None) { require(result.clonedInstructions == 0 && result.routeVariables == 0, name + ": expected no clone or routing, got " + std::to_string(result.clonedInstructions) + " cloned instructions and " + std::to_string(result.routeVariables) + " route variables"); } else if (program.split == Split::Clone) { require(result.clonedInstructions != 0 && result.routeVariables == 0, name + ": expected a clone, got " + std::to_string(result.clonedInstructions) + " cloned instructions and " + std::to_string(result.routeVariables) + " route variables"); @@ -272,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(); @@ -302,7 +546,82 @@ void verifyNullSwappc() { } +void VerifyUserDataTail() { + const RdnaInstructionDecoder decoder; + const std::vector code{0x7e000280u, 0xbe802008u, 0xbf9f0000u}; + const auto decoded = decoder.Decode(code); + const auto tail = FindUserDataTail(decoded, 8u, 2u); + require(tail && tail->programCounter == 4u && tail->userDataRegister == 8u, "unchanged user-data tail was not recognized"); + require(!FindUserDataTail(decoded, 9u, 2u) && !FindUserDataTail(decoded, 8u, 1u), "tail outside user data was accepted"); + const std::vector> refused{ + {0xbe880380u, 0xbe802008u, 0xbf9f0000u}, + {0xbe890300u, 0xbe802008u, 0xbf9f0000u}, + {0xbe880480u, 0xbe802008u, 0xbf9f0000u}, + {0xf4080200u, 0xfa000000u, 0xbe802008u, 0xbf9f0000u}, + {0xbf820000u, 0xbe802008u, 0xbf9f0000u}, + {0xbe802009u, 0xbf9f0000u}, + {0xbe802008u, 0xbf810000u}, + {0xbe802008u, 0xbf800000u, 0xbf9f0000u}, + }; + for (const auto& rejected : refused) require(!FindUserDataTail(decoder.Decode(rejected), 8u, 4u), "a modified, branching or malformed tail was accepted"); + const std::vector continuation{0xbf820001u, 0x7e000281u, 0xbf810000u}; + const auto linked = LinkUserDataTail(decoded, *tail, decoder.Decode(continuation)); + require(linked == std::vector({0x7e000280u, 0xbf820000u, 0xbf820001u, 0x7e000281u, 0xbf810000u}), "linking did not preserve relative branches and register operations"); + static_cast(GraphBuilder{}.Build(decoder.Decode(linked))); + const std::vector readsPc{0xbe8c1f00u, 0xbf810000u}; + bool refusedPc = false; + try { + static_cast(LinkUserDataTail(decoded, *tail, decoder.Decode(readsPc))); + } catch (const std::invalid_argument& error) { + refusedPc = std::string_view(error.what()).find("reads its program counter") != std::string_view::npos; + } + require(refusedPc, "a continuation reading its relocated PC was accepted"); +} + +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(); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } if (argc > 1) { int failures = 0; for (int i = 1; i < argc; ++i) { @@ -316,6 +635,38 @@ int main(int argc, char** argv) { return failures == 0 ? 0 : 1; } const std::vector programs{ + {"nested selections share a loop continuation", "Synthetic loop with two nested selections and a shared continue; the inner selection does not dominate the continuation.", + {0x060202ffu, 0x3f800000u, 0x060202ffu, 0x3f810000u, 0x7d880084u, 0xbf870001u, 0xbf820018u, 0x060202ffu, 0x3f820000u, 0x7d880085u, 0xbf87000cu, 0x060202ffu, 0x3f850000u, 0x7d880088u, 0xbf870003u, 0x060202ffu, 0x3f870000u, 0xbf82fff0u, 0x060202ffu, 0x3f860000u, 0x7d880089u, 0xbf87fff9u, 0xbf820008u, 0x060202ffu, 0x3f830000u, 0x7d880086u, 0xbf870001u, 0xbf820003u, 0x060202ffu, 0x3f840000u, 0xbf82fff0u, 0x060202ffu, 0x3f880000u, 0xe0700000u, 0x80000100u, 0xbf810000u}, Split::None}, + {"reduced shared exit junction from the public CFG fuzz report", "Public reduced word stream from #2762; synthetic regression, not title data.", + {0x060202f2u, 0x060202f4u, 0x7d880084u, 0xbf870001u, 0xbf820011u, 0x060202ffu, 0x3f820000u, 0x7d880085u, 0xbf870005u, + 0x060202ffu, 0x3f850000u, 0x7d880088u, 0xbf870009u, 0xbf82fff3u, 0x060202ffu, 0x3f840000u, 0x7d880087u, 0xbf870001u, + 0xbf820003u, 0x060202ffu, 0x3f860000u, 0xbf82ffebu, 0x060202f6u, 0xe0700000u, 0x80010100u, 0xbf810000u}, Split::None}, + {"both selection arms leave through a shared loop exit tail", R"( + v_mov_b32 v1, 0 + s_mov_b32 s8, 0 +loop: + s_cmp_lt_u32 s8, 4 + s_cbranch_scc0 done + s_cmp_eq_u32 s8, s2 + s_cbranch_scc1 right + v_cmp_gt_u32 vcc, 8, v0 + s_cbranch_vccz tail + v_add_nc_u32 v1, 1, v1 + s_branch latch +right: + v_cmp_gt_u32 vcc, 16, v0 + s_cbranch_vccz tail + v_add_nc_u32 v1, 2, v1 +latch: + s_add_u32 s8, s8, 1 + s_branch loop +tail: + v_add_nc_u32 v1, 3, v1 +done: + buffer_store_dword v1, off, s[0:3], 0 + s_endpgm)", + Store({0x7e020280u, 0xbe880380u, 0xbf0a8408u, 0xbf84000cu, 0xbf060208u, 0xbf850004u, 0x7d880088u, 0xbf860007u, 0x4a020281u, + 0xbf820003u, 0x7d880090u, 0xbf860003u, 0x4a020282u, 0x80088108u, 0xbf82fff3u, 0x4a020283u}), Split::None}, {"shared tail", R"( v_cmp_gt_u32 vcc, 16, v0 s_cbranch_vccz outer_else @@ -350,6 +701,16 @@ done: buffer_store_dword v1, off, s[0:3], 0 s_endpgm)", Store({0x7d880090u, 0xbf860004u, 0x7d880088u, 0xbf870003u, 0x4a020081u, 0xbf820005u, 0x4a020082u, 0x7d880084u, 0xbf860001u, 0x4a020285u, 0x4a020283u}), Split::Clone}, + {"memory access in a single-block loop latch", R"( + s_mov_b32 s8, 0 + v_mov_b32 v1, 0 +loop: + buffer_store_dword v1, off, s[0:3], 0 + s_add_u32 s8, s8, 1 + s_cmp_lt_u32 s8, 4 + s_cbranch_scc1 loop + s_endpgm)", + {0xbe880380u, 0x7e020280u, 0xe0700000u, 0x80000100u, 0x80088108u, 0xbf0a8408u, 0xbf85fffbu, 0xbf810000u}, Split::None}, {"shared tail in a loop", R"( s_mov_b32 s8, 0 v_mov_b32 v1, 0 @@ -371,6 +732,39 @@ latch: buffer_store_dword v1, off, s[0:3], 0 s_endpgm)", Store({0xbe880380u, 0x7e020280u, 0x7d880008u, 0xbf860004u, 0x7d880088u, 0xbf870003u, 0x4a020281u, 0xbf820002u, 0x4a020282u, 0x4a020283u, 0x80089008u, 0xbf0ac008u, 0xbf85fff5u}), Split::Clone}, + {"loop selection arms that leave the iteration rejoin before a join the enclosing selection shares", R"( + v_mov_b32 v1, 0 + s_mov_b32 s8, 0 +loop: + s_cmp_eq_u32 s8, s2 + s_cbranch_scc1 join + v_cmp_gt_u32 vcc, 8, v0 + s_cbranch_vccz arm + v_add_nc_u32 v1, 1, v1 + v_cmp_gt_u32 vcc, 16, v0 + s_cbranch_vccz away +arm: + v_add_nc_u32 v1, 2, v1 + v_cmp_gt_u32 vcc, 4, v0 + s_cbranch_vccz aside +join: + v_add_nc_u32 v1, 3, v1 + s_cmp_lt_u32 s8, 4 + s_cbranch_scc0 done + s_branch latch +away: + v_add_nc_u32 v1, 4, v1 + s_branch latch +aside: + v_add_nc_u32 v1, 5, v1 +latch: + s_add_u32 s8, s8, 1 + s_branch loop +done: + buffer_store_dword v1, off, s[0:3], 0 + s_endpgm)", + Store({0x7e020280u, 0xbe880380u, 0xbf060208u, 0xbf850008u, 0x7d880088u, 0xbf860003u, 0x4a020281u, 0x7d880090u, 0xbf860007u, 0x4a020282u, 0x7d880084u, + 0xbf860006u, 0x4a020283u, 0xbf0a8408u, 0xbf840006u, 0xbf820003u, 0x4a020284u, 0xbf820001u, 0x4a020285u, 0x80088108u, 0xbf82ffedu}), Split::None}, {"continue beside the inner merge of a nested selection in a loop", R"( s_mov_b32 s8, 0 v_mov_b32 v1, 0 @@ -394,6 +788,69 @@ done: buffer_store_dword v1, off, s[0:3], 0 s_endpgm)", Store({0xbe880380u, 0x7e020280u, 0x7d880008u, 0xbf860005u, 0x7d880088u, 0xbf870002u, 0x7d880084u, 0xbf870004u, 0x4a020281u, 0x4a020282u, 0xbf09c008u, 0xbf850002u, 0x80089008u, 0xbf82fff4u}), Split::None}, + {"nested selections in a loop whose second latch continues from inside them", R"( + s_mov_b32 s8, 0 + v_mov_b32 v1, 0 + s_branch loop +again: + v_add_nc_u32 v1, 2, v1 + s_add_u32 s8, s8, 16 +loop: + s_cmp_eq_u32 s8, s2 + s_cbranch_scc1 latch + v_cmp_gt_u32 vcc, 8, v0 + s_cbranch_vccz latch + v_cmp_gt_u32 vcc, 4, v0 + s_cbranch_vccnz again +latch: + v_add_nc_u32 v1, 1, v1 + s_add_u32 s8, s8, 16 + s_cmp_lt_u32 s8, 64 + s_cbranch_scc1 loop + buffer_store_dword v1, off, s[0:3], 0 + s_endpgm)", + Store({0xbe880380u, 0x7e020280u, 0xbf820002u, 0x4a020282u, 0x80089008u, 0xbf060208u, 0xbf850004u, 0x7d880088u, 0xbf860002u, 0x7d880084u, 0xbf87fff8u, 0x4a020281u, 0x80089008u, 0xbf0ac008u, 0xbf85fff6u}), Split::None}, + {"an inner loop that ends the program or continues its outer loop", R"( + v_mov_b32 v1, 0 + s_mov_b32 s8, 0 +outer: + v_add_nc_u32 v1, 2, v1 +inner: + v_cmp_gt_u32 vcc, 8, v0 + s_cbranch_vccz done + v_add_nc_u32 v1, 1, v1 + s_add_u32 s8, s8, 16 + s_cmp_lt_u32 s8, 64 + s_cbranch_scc1 inner + s_mov_b32 s8, 0 + s_branch outer +done: + buffer_store_dword v1, off, s[0:3], 0 + s_endpgm)", + Store({0x7e020280u, 0xbe880380u, 0x4a020282u, 0x7d880088u, 0xbf860006u, 0x4a020281u, 0x80089008u, 0xbf0ac008u, 0xbf85fffau, 0xbe880380u, 0xbf82fff7u}), Split::Route, 0}, + {"an inner loop whose body leaves both loops", R"( + v_mov_b32 v1, 0 + s_mov_b32 s8, 0 +outer: + s_cmp_lt_u32 s8, 64 + s_cbranch_scc0 done +inner: + v_cmp_gt_u32 vcc, 8, v0 + s_cbranch_vccz latch + v_add_nc_u32 v1, 1, v1 + s_add_u32 s8, s8, 16 + s_cmp_eq_u32 s8, s2 + s_cbranch_scc1 done + s_branch inner +latch: + v_add_nc_u32 v1, 2, v1 + s_add_u32 s8, s8, 16 + s_branch outer +done: + buffer_store_dword v1, off, s[0:3], 0 + s_endpgm)", + Store({0x7e020280u, 0xbe880380u, 0xbf0ac008u, 0xbf84000au, 0x7d880088u, 0xbf860005u, 0x4a020281u, 0x80089008u, 0xbf060208u, 0xbf850004u, + 0xbf82fff9u, 0x4a020282u, 0x80089008u, 0xbf82fff4u}), Split::Route, 0}, {"loop exit to the end of the program beside a kill exit", R"( v_mov_b32 v1, 0 s_mov_b64 s[20:21], exec @@ -797,6 +1254,29 @@ early_exit: {0x7e020280u, 0xbf06c002u, 0xbf850003u, 0x4a020081u, 0xbf068003u, 0xbf850006u, 0x7d880088u, 0xbf870001u, 0x4a020282u, 0xe0700000u, 0x80000100u, 0xbf810000u, 0xbefe0480u, 0xbf810000u}, Split::None}, + {"early exit arm beside a parent arm that reaches the same merge", R"( + v_mov_b32 v1, 0 + s_cmp_eq_u32 s2, 64 + s_cbranch_scc1 other + v_add_nc_u32 v1, 1, v0 + s_cmp_eq_u32 s3, 0 + s_cbranch_scc1 early_exit +join: + v_cmp_gt_u32 vcc, 8, v0 + s_cbranch_vccnz store + v_add_nc_u32 v1, 2, v1 +store: + buffer_store_dword v1, off, s[0:3], 0 + s_endpgm +other: + v_add_nc_u32 v1, 3, v0 + s_branch join +early_exit: + s_mov_b64 exec, 0 + s_endpgm)", + {0x7e020280u, 0xbf06c002u, 0xbf850009u, 0x4a020081u, 0xbf068003u, 0xbf850008u, 0x7d880088u, 0xbf870001u, 0x4a020282u, 0xe0700000u, + 0x80000100u, 0xbf810000u, 0x4a020083u, 0xbf82fff8u, 0xbefe0480u, 0xbf810000u}, + Split::None}, {"return block entered from the return block after it", R"( v_mov_b32 v1, 0 s_cmp_eq_u32 s2, 64 @@ -934,6 +1414,14 @@ done: } try { verifyNullSwappc(); + verifyScheduledJumpTable(); + VerifyUserDataTail(); + VerifyConstantHighTail(); + verifyCallLinkOverwrittenByWave64Compare(); + verifyCallLinkMaskReadByScalarInstruction(); + verifyLargeLoopExitFallback(); + verifyNestedExitForwarders(); + verifyIrreducibleLoopEntry(); } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); ++failures; diff --git a/core/libs/prx/libSceAgcDriver/tests/DenormalFlushElimination.cpp b/core/libs/prx/libSceAgcDriver/tests/DenormalFlushElimination.cpp new file mode 100644 index 0000000000..cc952f5ea9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/DenormalFlushElimination.cpp @@ -0,0 +1,379 @@ +#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; + +namespace { + +struct Builder { + IrProgram program; + IrBlock* block = nullptr; + + Builder() { + block = &newBlock(); + program.SetEntryBlock(*block); + } + + IrBlock& newBlock() { + auto& created = program.CreateBlock(); + program.BlockOrder().push_back(&created); + return created; + } + + IrValue& constant(std::uint32_t value) { + auto& created = program.CreateValue(IrOpcode::Void, IrType::U32); + created.SetImmediateU32(value); + return created; + } + + IrValue& 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& flush(IrValue& bits) { + auto& exponent = emit(IrOpcode::BitwiseAnd32, IrType::U32, {&bits, &constant(0x7f800000u)}); + auto& denormal = emit(IrOpcode::IEqual32, IrType::Bool, {&exponent, &constant(0u)}); + auto& sign = emit(IrOpcode::BitwiseAnd32, IrType::U32, {&bits, &constant(0x80000000u)}); + return emit(IrOpcode::SelectU32, IrType::U32, {&denormal, &sign, &bits}); + } + + IrValue& phi(IrBlock& block, std::initializer_list> incoming) { + auto& created = program.CreateValue(IrOpcode::Phi, IrType::U32); + block.AppendInstruction(&created); + for (const auto& [predecessor, value] : incoming) created.AddPhiOperand(predecessor, value); + return created; + } + + void keep(IrValue& value) { + (void)emit(IrOpcode::ReferenceU32, IrType::Void, {&value}); + } + + std::uint32_t eliminate() { + return DenormalFlushEliminator{}.Eliminate(program).removedFlushes; + } +}; + +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; + } +} + +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() { + bool passed = true; + + passed &= run("a flush of a flushed value is removed and the first flush stays", [] { + Builder b; + auto& first = b.flush(b.unknown()); + auto& second = b.flush(first); + b.keep(second); + return b.eliminate() == 1u && !second.HasUses() && first.HasUses(); + }); + + passed &= run("a flush of an unknown value stays", [] { + Builder b; + auto& flushed = b.flush(b.unknown()); + b.keep(flushed); + return b.eliminate() == 0u && flushed.HasUses(); + }); + + passed &= run("a sign change or absolute value of a flushed value needs no flush", [] { + Builder b; + auto& first = b.flush(b.unknown()); + auto& negated = b.flush(b.emit(IrOpcode::BitwiseXor32, IrType::U32, {&first, &b.constant(0x80000000u)})); + auto& absolute = b.flush(b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&b.constant(0x7fffffffu), &first})); + b.keep(negated); + b.keep(absolute); + return b.eliminate() == 2u && !negated.HasUses() && !absolute.HasUses(); + }); + + passed &= run("a select of flushed values needs no flush, one with an unknown arm does", [] { + Builder b; + auto& condition = b.emit(IrOpcode::ULessThan32, IrType::Bool, {&b.unknown(), &b.constant(16u)}); + auto& both = b.flush(b.emit(IrOpcode::SelectU32, IrType::U32, {&condition, &b.flush(b.unknown()), &b.flush(b.unknown())})); + auto& mixed = b.flush(b.emit(IrOpcode::SelectU32, IrType::U32, {&condition, &b.flush(b.unknown()), &b.unknown()})); + b.keep(both); + b.keep(mixed); + return b.eliminate() == 1u && !both.HasUses() && mixed.HasUses(); + }); + + passed &= run("normal and zero constants need no flush, a denormal constant does", [] { + Builder b; + auto& one = b.flush(b.constant(0x3f800000u)); + auto& negativeZero = b.flush(b.constant(0x80000000u)); + auto& denormal = b.flush(b.constant(0x00000001u)); + b.keep(one); + b.keep(negativeZero); + b.keep(denormal); + return b.eliminate() == 2u && !one.HasUses() && !negativeZero.HasUses() && denormal.HasUses(); + }); + + passed &= run("an integer or f16 converted to f32 needs no flush", [] { + Builder b; + auto& integer = b.flush(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&b.emit(IrOpcode::ConvertF32U32, IrType::F32, {&b.unknown()})})); + auto& half = b.flush(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&b.emit(IrOpcode::ConvertF32F16, IrType::F32, {&b.emit(IrOpcode::BitCastF16U16, IrType::F16, {&b.emit(IrOpcode::ConvertU16U32, IrType::U16, {&b.unknown()})})})})); + b.keep(integer); + b.keep(half); + return b.eliminate() == 2u && !integer.HasUses() && !half.HasUses(); + }); + + passed &= run("a clamped flushed value needs no flush, an arithmetic result does", [] { + Builder b; + auto& first = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&b.flush(b.unknown())}); + auto& clamped = b.flush(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&b.emit(IrOpcode::FPSaturate32, IrType::F32, {&first})})); + auto& product = b.flush(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&b.emit(IrOpcode::FPMul32, IrType::F32, {&first, &first})})); + b.keep(clamped); + b.keep(product); + return b.eliminate() == 1u && !clamped.HasUses() && product.HasUses(); + }); + + passed &= run("a min, max, min3, max3 or med3 of flushed values needs no flush, one with an unknown operand does", [] { + Builder b; + const auto value = [&](IrValue& bits) -> IrValue& { return b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&bits}); }; + const auto flushedResult = [&](IrOpcode opcode, std::initializer_list arguments) -> IrValue& { + return b.flush(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&b.emit(opcode, IrType::F32, arguments)})); + }; + auto& x = value(b.flush(b.unknown())); + auto& y = value(b.flush(b.unknown())); + auto& z = value(b.flush(b.unknown())); + auto& raw = value(b.unknown()); + IrValue* removed[] = { + &flushedResult(IrOpcode::FPMin32, {&x, &y}), + &flushedResult(IrOpcode::FPMax32, {&y, &x}), + &flushedResult(IrOpcode::FPMinTri32, {&x, &y, &z}), + &flushedResult(IrOpcode::FPMaxTri32, {&z, &y, &x}), + &flushedResult(IrOpcode::FPMedTri32, {&x, &z, &y}), + }; + auto& mixedPair = flushedResult(IrOpcode::FPMax32, {&x, &raw}); + auto& mixedMedian = flushedResult(IrOpcode::FPMedTri32, {&x, &y, &raw}); + for (auto* flushed : removed) b.keep(*flushed); + b.keep(mixedPair); + b.keep(mixedMedian); + const bool counted = b.eliminate() == 5u; + bool kept = false; + for (auto* flushed : removed) kept |= flushed->HasUses(); + return counted && !kept && mixedPair.HasUses() && mixedMedian.HasUses(); + }); + + passed &= run("a floor or ceil needs no flush, even of an unknown value", [] { + Builder b; + auto& raw = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&b.unknown()}); + auto& floored = b.flush(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&b.emit(IrOpcode::FPFloor32, IrType::F32, {&raw})})); + auto& ceiled = b.flush(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&b.emit(IrOpcode::FPCeil32, IrType::F32, {&raw})})); + b.keep(floored); + b.keep(ceiled); + return b.eliminate() == 2u && !floored.HasUses() && !ceiled.HasUses(); + }); + + passed &= run("a max carried around a loop without a flush keeps its flush and the search ends", [] { + Builder b; + IrBlock& head = *b.block; + IrBlock& body = b.newBlock(); + IrBlock& exit = b.newBlock(); + head.AddBranch(&body); + body.AddBranch(&body); + body.AddBranch(&exit); + auto& initial = b.flush(b.unknown()); + b.block = &body; + auto& carried = b.program.CreateValue(IrOpcode::Phi, IrType::U32); + body.AppendInstruction(&carried); + auto& step = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&b.flush(b.unknown())}); + auto& maximum = b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&b.emit(IrOpcode::FPMax32, IrType::F32, {&b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&carried}), &step})}); + carried.AddPhiOperand(&head, &initial); + carried.AddPhiOperand(&body, &maximum); + auto& flushed = b.flush(maximum); + b.block = &exit; + b.keep(flushed); + return b.eliminate() == 0u && flushed.HasUses(); + }); + + passed &= run("a phi of flushed values needs no flush", [] { + Builder b; + IrBlock& head = *b.block; + IrBlock& body = b.newBlock(); + IrBlock& merge = b.newBlock(); + head.AddBranch(&body); + head.AddBranch(&merge); + body.AddBranch(&merge); + auto& fromHead = b.flush(b.unknown()); + b.block = &body; + auto& fromBody = b.flush(b.unknown()); + b.block = &merge; + auto& merged = b.flush(b.phi(merge, {{&head, &fromHead}, {&body, &fromBody}})); + b.keep(merged); + return b.eliminate() == 1u && !merged.HasUses(); + }); + + passed &= run("a loop phi needs no flush when every value it takes is flushed, and keeps it otherwise", [] { + const auto loop = [](bool flushedBackEdge) { + Builder b; + IrBlock& head = *b.block; + IrBlock& body = b.newBlock(); + IrBlock& exit = b.newBlock(); + head.AddBranch(&body); + body.AddBranch(&body); + body.AddBranch(&exit); + auto& initial = b.flush(b.unknown()); + b.block = &body; + auto& carried = b.program.CreateValue(IrOpcode::Phi, IrType::U32); + body.AppendInstruction(&carried); + auto& next = b.flush(carried); + auto& backEdge = flushedBackEdge ? next : b.emit(IrOpcode::BitwiseXor32, IrType::U32, {&b.unknown(), &next}); + carried.AddPhiOperand(&head, &initial); + carried.AddPhiOperand(&body, &backEdge); + b.block = &exit; + b.keep(next); + b.eliminate(); + return !next.HasUses(); + }; + 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 b91361ceae..79878c2766 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp @@ -2,21 +2,31 @@ #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 #include +#include +#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; constexpr std::uint32_t FormatR32Uint = 20; constexpr std::uint32_t FormatR32Float = 22; constexpr std::uint32_t FormatR11G11B10Float = 36; +constexpr std::uint32_t FormatR8G8B8A8Unorm = 56; +constexpr std::uint32_t TileStandard64KB = 0x9; constexpr std::uint32_t TileDepth64KB = 0x18; constexpr std::uint32_t TileRenderTarget64KB = 0x1b; constexpr std::uint32_t Type2D = 9; @@ -29,8 +39,23 @@ THandle newHandle() { return reinterpret_cast(nextHandle += 0x10); } -VKAPI_ATTR VkResult VKAPI_CALL createImage(VkDevice, const VkImageCreateInfo*, const VkAllocationCallbacks*, VkImage* image) { +VkFormat lastImageFormat = VK_FORMAT_UNDEFINED; +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; } @@ -85,16 +110,74 @@ 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; +} + +VKAPI_ATTR void VKAPI_CALL formatPropertiesOnlyD32S8(VkPhysicalDevice, VkFormat format, VkFormatProperties* properties) { + *properties = {}; + if (format == VK_FORMAT_D32_SFLOAT_S8_UINT) properties->optimalTilingFeatures = VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT; +} + PFN_vkVoidFunction VKAPI_CALL deviceProc(VkDevice, const char* name) { static const std::map table{ {"vkCreateImage", reinterpret_cast(createImage)}, @@ -115,7 +198,16 @@ PFN_vkVoidFunction VKAPI_CALL deviceProc(VkDevice, const char* name) { {"vkDestroyImage", reinterpret_cast(destroyImage)}, {"vkFreeMemory", reinterpret_cast(freeMemory)}, {"vkCmdPipelineBarrier", reinterpret_cast(cmdPipelineBarrier)}, - {"vkCmdClearDepthStencilImage", reinterpret_cast(cmdClearDepthStencilImage)} + {"vkCmdClearDepthStencilImage", reinterpret_cast(cmdClearDepthStencilImage)}, + {"vkCmdCopyImage", reinterpret_cast(cmdCopyImage)}, + {"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; @@ -129,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) { @@ -139,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; } @@ -149,6 +245,176 @@ 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; + Context without = context; + without.physical = reinterpret_cast(&withoutTag); + without.formatProperties = formatPropertiesWithoutD16S8; + constexpr std::uint64_t d16s8 = 0x70000000; + constexpr std::uint64_t d16s8Stencil = 0x70800000; + Require(HostDepthFormat(without, VK_FORMAT_D16_UNORM_S8_UINT) == VK_FORMAT_D24_UNORM_S8_UINT, "a device without D16_UNORM_S8_UINT must store it as D24_UNORM_S8_UINT"); + Require(HostDepthFormat(without, VK_FORMAT_D32_SFLOAT_S8_UINT) == VK_FORMAT_D32_SFLOAT_S8_UINT && HostDepthFormat(without, VK_FORMAT_D16_UNORM) == VK_FORMAT_D16_UNORM, "only D16_UNORM_S8_UINT is replaced"); + DepthSurfaceView(without, {d16s8, d16s8Stencil, {256, 256}, VK_FORMAT_D16_UNORM_S8_UINT, 1.0f, 0}); + Require(lastImageFormat == VK_FORMAT_D24_UNORM_S8_UINT, "a D16_UNORM_S8_UINT surface was not created as D24_UNORM_S8_UINT on a device without it"); + Require(lookup(without, {d16s8, FormatR16Unorm, 256, 256}) != nullptr, "an R16 view of a D16 surface stored as D24 must still sample its depth plane"); + Require(lookup(without, {d16s8Stencil, FormatR8Uint, 256, 256}) != nullptr, "an 8-bit view of the stencil plane of a D16 surface stored as D24 must sample it"); + Require(HostDepthBiasConstant(without, VK_FORMAT_D16_UNORM_S8_UINT, 2.0f) == 512.0f, "a 16-bit depth bias must be scaled by 256 for D24 storage"); + Require(HostDepthBiasConstant(without, VK_FORMAT_D32_SFLOAT_S8_UINT, 2.0f) == 2.0f, "a D32 depth bias must not be scaled"); + const auto refused = [&](const View& view) { + try { + lookup(without, view); + } catch (const std::runtime_error& error) { + return std::string_view(error.what()).find("a depth plane that the device stores in another depth format is not implemented") != std::string_view::npos; + } + return false; + }; + Require(refused({d16s8, FormatR16Unorm, 256, 256, TileDepth64KB, Type2DArray, 1}), "two slices of a D16 surface stored as D24 must be refused, as the plane copy would read the wider image as 16-bit texels"); + Require(refused({d16s8, FormatR16Uint, 256, 256, TileStandard64KB}), "an R16_UINT view of a D16 surface stored as D24 must be refused"); + std::string storageError; + try { + const auto words = tsharp({d16s8, FormatR16Unorm, 256, 256}); + DepthSurfaceStorage(without, words, DecodeTextureResource(words), 0); + } catch (const std::runtime_error& error) { + storageError = error.what(); + } + Require(storageError.find("depth plane") != std::string::npos && storageError.find("stored as") != std::string::npos, "storage access to the depth plane of a D16 surface stored as D24 must throw"); + ClearDepthSurfaces(without.device); + + static int onlyTag = 0; + Context only = context; + only.physical = reinterpret_cast(&onlyTag); + only.formatProperties = formatPropertiesOnlyD32S8; + Require(HostDepthFormat(only, VK_FORMAT_D16_UNORM_S8_UINT) == VK_FORMAT_D32_SFLOAT_S8_UINT, "a device with neither D16 nor D24 stencil formats must store D16_UNORM_S8_UINT as D32_SFLOAT_S8_UINT"); + Require(HostDepthBiasConstant(only, VK_FORMAT_D16_UNORM_S8_UINT, 0.0f) == 0.0f, "a zero depth bias needs no scaling"); + bool threw = false; + try { + HostDepthBiasConstant(only, VK_FORMAT_D16_UNORM_S8_UINT, 2.0f); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw, "a constant 16-bit depth bias on D32_SFLOAT_S8_UINT storage must throw"); +} + +void 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() { @@ -161,10 +427,18 @@ void RunDepthSurfaceReuseTests() { context.memory.memoryTypes[0].propertyFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; context.limits.maxFramebufferWidth = 16384; context.limits.maxFramebufferHeight = 16384; + context.limits.maxFramebufferLayers = 1; + TestD16StencilFallback(context); + TestMultisampleDepthOwnership(context); + TestMultisampleStencilAliases(context); + TestStencilUnormView(context); constexpr std::uint64_t depth32 = 0x40000000; constexpr std::uint64_t depth16 = 0x50000000; + constexpr std::uint64_t depthStencil = 0x60000000; + constexpr std::uint64_t stencil = 0x60800000; DepthSurfaceView(context, {depth32, 0, {384, 384}, VK_FORMAT_D32_SFLOAT, 1.0f, 0}); DepthSurfaceView(context, {depth16, 0, {256, 256}, VK_FORMAT_D16_UNORM, 1.0f, 0}); + DepthSurfaceView(context, {depthStencil, stencil, {320, 180}, VK_FORMAT_D32_SFLOAT_S8_UINT, 1.0f, 0}); Require(lookup(context, {depth32, FormatR32Float, 384, 384}) != nullptr, "an R32F view of a D32 surface's own extent must sample its depth plane"); Require(lookup(context, {depth16, FormatR16Unorm, 256, 256}) != nullptr, "an R16 view of a D16 surface's own extent must sample its depth plane"); @@ -176,6 +450,80 @@ 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"); + Require(lookup(context, {stencil, FormatR32Float, 320, 180}) == nullptr, "an R32F view of a stencil plane's own extent must be read as reused memory"); + + const auto storageAliasesDepth = [&](const Context& owner, const View& view) { + const auto words = tsharp(view); + return DepthSurfaceAt(owner, words, DecodeTextureResource(words)); + }; + Require(storageAliasesDepth(context, {depth32, FormatR32Float, 384, 384}), "storage access to the actual D32 plane must remain rejected"); + Require(storageAliasesDepth(context, {depth16, FormatR16Uint, 256, 256}), "raw D16 storage access must remain rejected"); + Require(!storageAliasesDepth(context, {depth32, FormatR32Float, 128, 128}), "temporary memory reused at a different extent is not a depth storage alias"); + Require(!storageAliasesDepth(context, {depth32, FormatR11G11B10Float, 384, 384}), "temporary memory reused for a color format is not a depth storage alias"); + Require(!storageAliasesDepth(context, {depth32, FormatR32Uint, 384, 384, TileRenderTarget64KB}), "raw integer color memory without a depth layout is not a depth storage alias"); + auto otherDevice = context; + static int otherDeviceTag = 0; + otherDevice.device = reinterpret_cast(&otherDeviceTag); + Require(!storageAliasesDepth(otherDevice, {depth32, FormatR32Float, 384, 384}), "a depth surface on another device must not block a storage lookup"); + + constexpr std::uint64_t stencil16 = 0x60000000; + DepthSurfaceView(context, {depth16, stencil16, {128, 128}, VK_FORMAT_D16_UNORM_S8_UINT, 1.0f, 0}); + Require(!storageAliasesDepth(context, {depth16, FormatR16Unorm, 256, 256}), "an older depth extent at the same address must not override the newest surface"); + Require(storageAliasesDepth(context, {depth16, FormatR16Unorm, 128, 128}), "the newest depth extent must still reject actual storage access"); + Require(storageAliasesDepth(context, {stencil16, FormatR8Uint, 128, 128}), "storage access to the actual stencil plane must remain rejected"); + Require(!storageAliasesDepth(context, {stencil16, FormatR11G11B10Float, 128, 128}), "a stencil address reused for color is not a stencil storage alias"); + + constexpr std::uint64_t shared = 0x70000000; + constexpr std::uint64_t stencilA = 0x71000000; + constexpr std::uint64_t stencilB = 0x72000000; + constexpr std::uint64_t otherDepth = 0x73000000; + const auto pairing = [&](std::uint64_t depth, std::uint64_t stencil) { + const auto before = createdImages.size(); + DepthSurfaceView(context, {depth, stencil, {256, 256}, VK_FORMAT_D32_SFLOAT_S8_UINT, 1.0f, 0}); + return createdImages.size() != before ? createdImages[before] : VK_NULL_HANDLE; + }; + const auto withA = pairing(shared, stencilA); + imageCopies.clear(); + const auto withB = pairing(shared, stencilB); + Require(withA != VK_NULL_HANDLE && withB != VK_NULL_HANDLE && withA != withB, "each depth/stencil pairing must have its own image"); + Require(imageCopies.size() == 1 && imageCopies[0] == std::make_tuple(withA, withB, static_cast(VK_IMAGE_ASPECT_DEPTH_BIT)), "a depth plane paired with another stencil plane must carry its depth over"); + imageCopies.clear(); + pairing(shared, stencilB); + Require(imageCopies.empty(), "drawing again with the same pairing must not copy"); + pairing(shared, stencilA); + Require(imageCopies.size() == 1 && imageCopies[0] == std::make_tuple(withB, withA, static_cast(VK_IMAGE_ASPECT_DEPTH_BIT)), "returning to the first pairing must carry the depth written through the second"); + imageCopies.clear(); + const auto otherWithA = pairing(otherDepth, stencilA); + Require(imageCopies.size() == 1 && imageCopies[0] == std::make_tuple(withA, otherWithA, static_cast(VK_IMAGE_ASPECT_STENCIL_BIT)), "a stencil plane paired with another depth plane must carry its stencil over"); + { + 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 new file mode 100644 index 0000000000..a5ff02af5d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/DeviceMemoryFallback.cpp @@ -0,0 +1,127 @@ +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include +#include +#include +#include + +using namespace AgcDriver::Graphics; + +namespace { + +int failures = 0; + +void Expect(bool condition, const std::string& what) { + if (condition) return; + std::fprintf(stderr, "FAIL: %s\n", what.c_str()); + ++failures; +} + +constexpr std::uint32_t HostType = 0; +constexpr std::uint32_t DeviceType = 1; +constexpr std::uint64_t MiB = 1ull << 20u; + +std::uint64_t reportedBudget = 0; +std::uint64_t reportedUsage = 0; +std::uint64_t reportedHostBudget = 0; +std::uint64_t reportedHostUsage = 0; + +VKAPI_ATTR void VKAPI_CALL mockMemoryProperties2(VkPhysicalDevice, VkPhysicalDeviceMemoryProperties2* properties) { + auto* reported = static_cast(properties->pNext); + reported->heapBudget[0] = reportedBudget; + reported->heapUsage[0] = reportedUsage; + reported->heapBudget[1] = reportedHostBudget; + reported->heapUsage[1] = reportedHostUsage; +} + +Context mockContext(bool budgetExtension) { + Context context{}; + context.physical = reinterpret_cast(static_cast(1)); + context.memoryProperties2 = budgetExtension ? mockMemoryProperties2 : nullptr; + context.memory.memoryHeapCount = 2; + context.memory.memoryHeaps[0] = {4096 * MiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; + context.memory.memoryHeaps[1] = {16384 * MiB, 0}; + context.memory.memoryTypeCount = 2; + context.memory.memoryTypes[HostType] = {0, 1}; + context.memory.memoryTypes[DeviceType] = {VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 0}; + return context; +} + +void Choose(const char* what, bool budgetExtension, std::uint64_t budget, std::uint64_t usage, std::uint32_t mask, std::uint32_t expected) { + reportedBudget = budget; + reportedUsage = usage; + const auto type = mockContext(budgetExtension).DeviceMemoryType(mask, 64 * MiB, "test"); + 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); + context.memory.memoryTypeCount = 4; + context.memory.memoryTypes[0] = {coherent | VK_MEMORY_PROPERTY_HOST_CACHED_BIT | VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 0}; + context.memory.memoryTypes[1] = {coherent, 1}; + context.memory.memoryTypes[2] = {coherent | VK_MEMORY_PROPERTY_HOST_CACHED_BIT, 0}; + context.memory.memoryTypes[3] = {VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, 1}; + reportedBudget = 3000 * MiB; + reportedUsage = 2936 * MiB; + reportedHostBudget = 8000 * MiB; + reportedHostUsage = 2000 * MiB; + const auto choose = [&](const char* what, std::uint32_t mask, VkMemoryPropertyFlags flags, std::uint32_t expected) { + const auto type = context.BufferMemoryType(mask, flags, 64 * MiB); + Expect(type == expected, std::string(what) + ": chose " + std::to_string(type) + ", expected " + std::to_string(expected)); + }; + choose("coherent buffer fits exactly in preferred cached heap", 15, coherent, 0); + ++reportedUsage; + choose("coherent buffer moves before allocating from a full heap", 15, coherent, 1); + choose("buffer respects its allowed type mask", 5, coherent, 0); + choose("another type on the same full heap is not an alternative", 7, coherent | VK_MEMORY_PROPERTY_HOST_CACHED_BIT, 0); + choose("noncoherent alternative is not eligible", 9, coherent, 0); + choose("explicit device-local requirement is preserved", 15, coherent | VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 0); + choose("uncached-only coherent buffer preserves its type", 2, coherent, 1); + reportedHostUsage = reportedHostBudget - 64 * MiB + 1; + choose("alternative must have enough reported room", 15, coherent, 0); + reportedHostBudget = 0; + choose("unreported alternative budget is not assumed free", 15, coherent, 0); + reportedHostBudget = 8000 * MiB; + reportedHostUsage = 2000 * MiB; + reportedBudget = 0; + choose("unreported preferred budget retains cached preference", 15, coherent, 0); + reportedBudget = 3000 * MiB; + context.memoryProperties2 = nullptr; + choose("no budget query retains cached preference", 15, coherent, 0); + context.memoryProperties2 = mockMemoryProperties2; + context.physical = VK_NULL_HANDLE; + choose("no physical device retains cached preference", 15, coherent, 0); +} + +} + +int main() { + try { + const std::uint32_t both = (1u << HostType) | (1u << DeviceType); + Choose("no VK_EXT_memory_budget", false, 0, 0, both, DeviceType); + Choose("budget not reported", true, 0, 4000 * MiB, both, DeviceType); + Choose("room in the device-local budget", true, 3000 * MiB, 2000 * MiB, both, DeviceType); + Choose("allocation fills the budget exactly", true, 3000 * MiB, 2936 * MiB, both, DeviceType); + Choose("allocation exceeds the budget", true, 3000 * MiB, 2937 * MiB, both, HostType); + Choose("usage already over the budget", true, 2952 * MiB, 3633 * MiB, both, HostType); + Choose("no host type allowed", true, 3000 * MiB, 3000 * MiB, 1u << DeviceType, DeviceType); + BufferPlacementTests(); + RefusedFallbackTests(); + } catch (const std::exception& error) { + std::fprintf(stderr, "FAIL: %s\n", error.what()); + return 1; + } + if (failures != 0) return 1; + std::puts("device memory placement tests passed"); + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/DisplayBufferDcc.cpp b/core/libs/prx/libSceAgcDriver/tests/DisplayBufferDcc.cpp index f5f1f159ff..a257da198b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DisplayBufferDcc.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DisplayBufferDcc.cpp @@ -264,6 +264,14 @@ int main() { ExpectPixels(buffer, DccKeys::ClearRegister, {0, 0, 255, 255}, "A2B10G10R10 register clear"); buffer.pixelFormat = bgra | tenBit; ExpectPixels(buffer, DccKeys::ClearRegister, {255, 0, 0, 255}, "A2R10G10B10 register clear"); + buffer.pixelFormat = 0x8100070422000000ull; + ExpectPixels(buffer, DccKeys::Clear0000, {0, 0, 0, 255}, "R10G10B10A2 PQ 0000"); + ExpectPixels(buffer, DccKeys::Clear1111, {255, 255, 255, 255}, "R10G10B10A2 PQ 1111"); + buffer.dccClearColor = 600u | (500u << 10u) | (300u << 20u) | (3u << 30u); + ExpectPixels(buffer, DccKeys::ClearRegister, {0, 213, 255, 255}, "R10G10B10A2 PQ register clear"); + buffer.pixelFormat = 0x8100070400000000ull; + buffer.dccClearColor = 300u | (500u << 10u) | (600u << 20u) | (3u << 30u); + ExpectPixels(buffer, DccKeys::ClearRegister, {0, 213, 255, 255}, "B10G10R10A2 PQ register clear"); for (const auto keys : {DccKeys::Uncompressed, DccKeys::Mixed, DccKeys::Unreadable}) { const auto message = Rejection([&] { AgcDriver::DisplayBufferClearPixel(buffer, keys); }); Expect(message.find(AgcDriver::Graphics::DccKeysName(keys)) != std::string::npos && message.find("0x7f0000") != std::string::npos, std::string("keys without a clear value were presented as one: ") + AgcDriver::Graphics::DccKeysName(keys)); diff --git a/core/libs/prx/libSceAgcDriver/tests/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 new file mode 100644 index 0000000000..5a1fddb472 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/EmbeddedVertexFetch.cpp @@ -0,0 +1,98 @@ +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" +#include "Translation/EmbeddedVertexFetch.hpp" +#include +#include +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +namespace { + +constexpr std::uint32_t AttributeTable = 0; +constexpr std::uint32_t BufferTable = 2; + +std::int32_t FetchedAttribute(std::uint32_t source, std::uint32_t field) { + const std::array code{ + 0xbe9403ffu, source, + 0xbe9503ffu, field, + 0x93961514u, + 0xf4080405u, 0x2c000000u, + 0x02140b08u, + 0xe00c2000u, 0x80040b0au, + 0xbf810000u, + }; + const auto program = RdnaInstructionDecoder{}.Decode(code); + const auto plan = EmbeddedVertexFetchAnalyzer{}.Analyze(program, AttributeTable, BufferTable, 0, 16, 64); + return plan.loads.size() == 1 ? plan.loads.front().attributeId : -1; +} + +int Expect(const char* name, std::uint32_t source, std::uint32_t field, std::uint32_t offset) { + const auto expected = static_cast(offset / 16u); + const auto actual = FetchedAttribute(source, field); + if (actual == expected) return 0; + std::fprintf(stderr, "%s: s_bfe_u32 0x%08x, 0x%08x gives buffer slot %d, expected %d\n", name, source, field, actual, expected); + 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() { + int failures = 0; + failures += Expect("width 30 at offset 0", 0x9e000000u, 0x001e0000u, 0x1e000000u); + 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/EmbeddedVertexFetchAliasing.cpp b/core/libs/prx/libSceAgcDriver/tests/EmbeddedVertexFetchAliasing.cpp new file mode 100644 index 0000000000..3e38216dfd --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/EmbeddedVertexFetchAliasing.cpp @@ -0,0 +1,31 @@ +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" +#include "Translation/EmbeddedVertexFetch.hpp" +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +int main() { + int failures = 0; + for (const std::uint32_t waveSize : {32u, 64u}) { + for (const std::uint32_t indexRegister : {0u, 1u, 2u, 3u, 16u}) { + const std::array code{ + 0xf4000004u | (indexRegister << 6u), 0xfa00000cu, + 0xf4080005u, indexRegister << 25u, + 0x02140b08u, + 0xe00c2000u, 0x80000b0au, + 0xbf810000u, + }; + const auto decoded = RdnaInstructionDecoder{}.Decode(code); + const auto plan = EmbeddedVertexFetchAnalyzer{}.Analyze(decoded, 0, 2, 0, 16, waveSize); + if (plan.loads.size() != 1 || plan.loads.front().attributeId != 3 || plan.loads.front().componentCount != 4 || + plan.loads.front().programCounter != 20 || plan.loads.front().prologLoads != std::vector{0, 8}) { + std::fprintf(stderr, "wave%u descriptor offset s%u lost its attribute or load provenance\n", waveSize, indexRegister); + ++failures; + } + } + } + return failures == 0 ? 0 : 1; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/ExecutionTestNotBuilt.cpp b/core/libs/prx/libSceAgcDriver/tests/ExecutionTestNotBuilt.cpp new file mode 100644 index 0000000000..d385028db2 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/ExecutionTestNotBuilt.cpp @@ -0,0 +1,6 @@ +#include + +int main() { + std::puts("skipped, this build leaves the test out with ANYPS5_AGC_EXECUTION_TESTS"); + return 77; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/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 da4fb199c9..ec73dea01a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Flip.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Flip.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/PresentMode.hpp" #include "prx/libc/include/Shutdown.hpp" #include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" @@ -8,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 { @@ -35,6 +52,8 @@ struct State { bool entered = false; bool fail = false; bool checkSelfWait = false; + bool gate = false; + bool waiting = false; std::atomic alive = 0; std::atomic ready = 0; std::atomic failed = 0; @@ -80,10 +99,24 @@ private: std::shared_ptr state; }; +class RenderingGate final : public AgcDriver::IRenderingWait { +public: + explicit RenderingGate(std::shared_ptr value) : state(std::move(value)) {} + void Wait() override { + std::unique_lock lock(state->mutex); + state->waiting = true; + state->changed.notify_all(); + state->changed.wait(lock, [&] { return !state->gate; }); + } +private: + std::shared_ptr state; +}; + class Output final : public AgcDriver::IVideoOutput { public: void Fail(std::exception_ptr error) noexcept override { if (!error) std::terminate(); } std::shared_ptr state = std::make_shared(); + std::shared_ptr CaptureRenderingWait(std::uint32_t) override { return std::make_shared(state); } std::shared_ptr Reserve(const AgcDriver::FlipInfo& info) override { std::lock_guard lock(state->mutex); state->last = info; @@ -103,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; @@ -165,6 +197,142 @@ void testFlipAndBoundary() { AgcDriverUnregisterVideoOutput_nid_postfix(7, replacement); } +void testFlipHold() { + auto output = std::make_shared(); + AgcDriverRegisterVideoOutput_nid_postfix(7, output); + { + std::lock_guard lock(output->state->mutex); + output->state->gate = true; + } + struct GateRelease { + std::shared_ptr state; + ~GateRelease() { + { + std::lock_guard lock(state->mutex); + state->gate = false; + } + state->changed.notify_all(); + } + } gateRelease{output->state}; + std::array gated{0xc0021018, 7, 0, 0, 0xc004105c, 7, 0xfffffffeu, 1, 0, 0}; + Packet gatedFrame{gated.data(), static_cast(gated.size()), 0, {}}; + check(sceAgcDriverSubmitDcb(&gatedFrame) == 0, "gated flip submission failed"); + { + std::unique_lock lock(output->state->mutex); + check(output->state->changed.wait_for(lock, std::chrono::seconds(5), [&] { return output->state->waiting; }), "worker did not reach the rendering wait"); + } + auto held = std::async(std::launch::async, [] { submitFlip(); }); + check(held.wait_for(std::chrono::milliseconds(200)) == std::future_status::timeout, "a flip was accepted before the worker reached the previous one"); + check(output->state->ready == 0, "a flip passed a closed rendering wait"); + { + std::lock_guard lock(output->state->mutex); + output->state->gate = false; + } + output->state->changed.notify_all(); + check(held.wait_for(std::chrono::seconds(5)) == std::future_status::ready, "a flip stayed held after the worker reached the previous one"); + held.get(); + AgcDriverWaitIdle_nid_postfix(); + check(output->state->ready == 2, "held flips were lost"); + alignas(4) static std::atomic label = 0; + struct LabelRelease { + ~LabelRelease() { label.store(1, std::memory_order_release); } + } labelRelease; + const auto labelAddress = reinterpret_cast(&label); + std::array waiting{0xc0053c00, 0x13, static_cast(labelAddress), static_cast(static_cast(labelAddress) >> 32u), 1, 0xffffffffu, 0x19, 0xc004105c, 7, 0xfffffffeu, 1, 0, 0}; + Packet waitingFrame{waiting.data(), static_cast(waiting.size()), 0, {}}; + check(sceAgcDriverSubmitDcb(&waitingFrame) == 0, "waiting flip submission failed"); + auto released = std::async(std::launch::async, [] { submitFlip(); }); + check(released.wait_for(std::chrono::seconds(5)) == std::future_status::ready, "a flip was held while the worker waited for a label only the title stores"); + released.get(); + check(output->state->ready == 2, "a flip passed an unsatisfied wait"); + label.store(1, std::memory_order_release); + AgcDriverWaitIdle_nid_postfix(); + check(output->state->ready == 4, "flips behind a title-stored label were lost"); + { + std::lock_guard lock(output->state->mutex); + output->state->gate = true; + output->state->waiting = false; + } + alignas(4) static std::atomic later = 0; + struct LaterRelease { + ~LaterRelease() { later.store(1, std::memory_order_release); } + } laterRelease; + const auto laterAddress = reinterpret_cast(&later); + std::array gatedWait{0xc0021018, 7, 0, 0, 0xc0053c00, 0x13, static_cast(laterAddress), static_cast(static_cast(laterAddress) >> 32u), 1, 0xffffffffu, 0x19, 0xc004105c, 7, 0xfffffffeu, 1, 0, 0}; + Packet gatedWaitFrame{gatedWait.data(), static_cast(gatedWait.size()), 0, {}}; + check(sceAgcDriverSubmitDcb(&gatedWaitFrame) == 0, "gated waiting flip submission failed"); + { + std::unique_lock lock(output->state->mutex); + check(output->state->changed.wait_for(lock, std::chrono::seconds(5), [&] { return output->state->waiting; }), "worker did not reach the second rendering wait"); + } + auto progressed = std::async(std::launch::async, [] { submitFlip(); }); + check(progressed.wait_for(std::chrono::milliseconds(200)) == std::future_status::timeout, "a flip was accepted while the worker was held before a label wait"); + { + std::lock_guard lock(output->state->mutex); + output->state->gate = false; + } + output->state->changed.notify_all(); + check(progressed.wait_for(std::chrono::seconds(5)) == std::future_status::ready, "a flip stayed held after the worker went on to wait for a label only the title stores"); + progressed.get(); + check(output->state->ready == 4, "a flip passed an unsatisfied wait after the rendering wait"); + later.store(1, std::memory_order_release); + AgcDriverWaitIdle_nid_postfix(); + check(output->state->ready == 6, "flips behind a later title-stored label were lost"); + AgcDriverUnregisterVideoOutput_nid_postfix(7, output); +} + +void testComputeRenderingWait() { + auto output = std::make_shared(); + AgcDriverRegisterVideoOutput_nid_postfix(7, output); + std::array wait{0xc0021018, 8, 0, 0}; + Packet packet{wait.data(), 4, 0, {}}; + check(expectFailure([&] { sceAgcDriverSubmitAcb(0x20, &packet); }).find("unregistered video output") != std::string::npos, "a compute rendering wait on an unregistered output was accepted"); + wait = {0xc0021018, 7, 0, 1}; + check(expectFailure([&] { sceAgcDriverSubmitAcb(0x20, &packet); }).find("rendering wait mode") != std::string::npos, "a compute rendering wait with a mode was accepted"); + { + std::lock_guard lock(output->state->mutex); + output->state->gate = true; + } + struct GateRelease { + std::shared_ptr state; + ~GateRelease() { + { + std::lock_guard lock(state->mutex); + state->gate = false; + } + state->changed.notify_all(); + } + } gateRelease{output->state}; + wait = {0xc0021018, 7, 0, 0}; + check(sceAgcDriverSubmitAcb(0x20, &packet) == 0, "compute rendering wait submission failed"); + { + std::unique_lock lock(output->state->mutex); + check(output->state->changed.wait_for(lock, std::chrono::seconds(5), [&] { return output->state->waiting; }), "compute worker did not reach the rendering wait"); + } + auto idle = std::async(std::launch::async, [] { AgcDriverWaitIdle_nid_postfix(); }); + check(idle.wait_for(std::chrono::milliseconds(200)) == std::future_status::timeout, "the driver went idle past a closed compute rendering wait"); + { + std::lock_guard lock(output->state->mutex); + output->state->gate = false; + } + output->state->changed.notify_all(); + check(idle.wait_for(std::chrono::seconds(5)) == std::future_status::ready, "a compute rendering wait stayed blocked after release"); + idle.get(); + 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; @@ -182,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, {}}; @@ -198,10 +400,41 @@ void testReset(bool compute) { } +void testPresentMode() { + using AgcDriver::SelectPresentMode; + const std::array wayland{VK_PRESENT_MODE_FIFO_KHR, VK_PRESENT_MODE_IMMEDIATE_KHR}; + const std::array all{VK_PRESENT_MODE_IMMEDIATE_KHR, VK_PRESENT_MODE_MAILBOX_KHR, VK_PRESENT_MODE_FIFO_KHR}; + check(SelectPresentMode(nullptr, wayland) == VK_PRESENT_MODE_FIFO_KHR, "the default present mode is not FIFO"); + check(SelectPresentMode("fifo", wayland) == VK_PRESENT_MODE_FIFO_KHR, "fifo was not selected"); + check(SelectPresentMode("immediate", wayland) == VK_PRESENT_MODE_IMMEDIATE_KHR, "immediate was not selected"); + check(SelectPresentMode("mailbox", all) == VK_PRESENT_MODE_MAILBOX_KHR, "mailbox was not selected"); + const std::array latest{VK_PRESENT_MODE_FIFO_KHR, VK_PRESENT_MODE_FIFO_LATEST_READY_KHR}; + check(SelectPresentMode("fifo_latest_ready", latest) == VK_PRESENT_MODE_FIFO_LATEST_READY_KHR, "fifo_latest_ready was not selected"); + check(SelectPresentMode(nullptr, latest) == VK_PRESENT_MODE_FIFO_KHR, "the default changed on a surface with fifo_latest_ready"); + check(expectFailure([&] { SelectPresentMode("fifo_latest_ready", wayland); }).find("does not support") != std::string::npos, "fifo_latest_ready was accepted on a surface without it"); + check(expectFailure([&] { SelectPresentMode("mailbox", wayland); }).find("does not support") != std::string::npos, "an unsupported present mode was accepted"); + check(expectFailure([&] { SelectPresentMode("Immediate", all); }).find("must be fifo, fifo_latest_ready, mailbox or immediate") != std::string::npos, "an unknown present mode name was accepted"); + check(expectFailure([&] { SelectPresentMode("", all); }).find("must be fifo, fifo_latest_ready, mailbox or immediate") != std::string::npos, "an empty present mode name was accepted"); + const std::array none{}; + check(expectFailure([&] { SelectPresentMode(nullptr, none); }).find("does not support") != std::string::npos, "FIFO was selected on a surface without it"); +} + int main(int argc, char** argv) { try { + if (argc == 2 && std::string(argv[1]) == "present-mode") { + testPresentMode(); + 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(); 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/Float16Conversion.cpp b/core/libs/prx/libSceAgcDriver/tests/Float16Conversion.cpp index 4035a58490..baf3155a63 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Float16Conversion.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Float16Conversion.cpp @@ -42,7 +42,7 @@ std::vector recompile(std::span capabilities request.useCache = false; auto result = Recompile(request); require(!result.spirv.empty(), "v_cvt_f16_f32 did not recompile"); - return std::move(result.spirv); + return result.spirv.Copy(); } struct Usage { diff --git a/core/libs/prx/libSceAgcDriver/tests/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/FormattedBufferByteStores.cpp b/core/libs/prx/libSceAgcDriver/tests/FormattedBufferByteStores.cpp new file mode 100644 index 0000000000..0383aa0bac --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/FormattedBufferByteStores.cpp @@ -0,0 +1,119 @@ +#include "BdaAbi.hpp" +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ShaderRecompiler; + +void Check(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +SpirvTarget Target(bool byteAccess) { + static constexpr std::array withBytes{ + spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, + }; + static constexpr std::array withoutBytes{spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses}; + static constexpr std::array extensions{ + "SPV_EXT_descriptor_indexing", "SPV_KHR_8bit_storage", "SPV_KHR_float_controls", "SPV_KHR_physical_storage_buffer", + }; + return {0x00401000u, 0x00010300u, 32, BdaAbi::Version, byteAccess ? std::span(withBytes) : std::span(withoutBytes), extensions, false, + {1024, 1024, 64}, 1024, 32768, {}, {}}; +} + +struct Counts { + std::size_t compareExchanges = 0; + std::size_t byteStores = 0; + std::size_t byteTypes = 0; + bool byteCapability = false; + bool byteBuffers = false; +}; + +Counts Count(std::span words) { + Check(words.size() >= 5 && words[0] == spv::MagicNumber, "formatted buffer byte stores: missing SPIR-V module"); + Counts counts; + std::uint32_t byteType = 0; + std::vector bytePointers; + for (std::size_t offset = 5; offset < words.size();) { + const auto count = words[offset] >> spv::WordCountShift; + Check(count != 0 && count <= words.size() - offset, "formatted buffer byte stores: malformed SPIR-V instruction"); + const auto opcode = words[offset] & spv::OpCodeMask; + if (opcode == spv::OpAtomicCompareExchange) ++counts.compareExchanges; + if (opcode == spv::OpCapability && words[offset + 1] == spv::CapabilityStorageBuffer8BitAccess) counts.byteCapability = true; + if (opcode == spv::OpTypeInt && words[offset + 2] == 8u) { + ++counts.byteTypes; + byteType = words[offset + 1]; + } + if (opcode == spv::OpTypePointer && words[offset + 2] == spv::StorageClassStorageBuffer && words[offset + 3] == byteType && byteType != 0) bytePointers.push_back(words[offset + 1]); + if (opcode == spv::OpName) { + const auto name = std::string_view(reinterpret_cast(&words[offset + 2])); + if (name == "buffers_u8") counts.byteBuffers = true; + } + offset += count; + } + std::vector byteChains; + for (std::size_t offset = 5; offset < words.size();) { + const auto count = words[offset] >> spv::WordCountShift; + const auto opcode = words[offset] & spv::OpCodeMask; + if (opcode == spv::OpAccessChain) { + for (const auto pointer : bytePointers) { + if (words[offset + 1] == pointer) byteChains.push_back(words[offset + 2]); + } + } + if (opcode == spv::OpStore) { + for (const auto chain : byteChains) { + if (words[offset + 1] == chain) ++counts.byteStores; + } + } + offset += count; + } + return counts; +} + +Counts Compile(bool byteAccess, bool vertex = false) { + std::vector code{0xe0000000u, 0x80020a00u, 0xbf8c3f70u, 0xe0100000u, 0x80020a00u}; + if (vertex) code.insert(code.end(), {0xf80008cfu, 0x03020100u}); + code.push_back(0xbf810000u); + const std::span words(code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(words)}}}; + const std::array userData{0u, 0u, 0u, 0u, 0x10000000u, 0u, 0x1000u, 0x31016facu, 0x20000000u, 0u, 0x1000u, 0x31038facu}; + RecompileRequest request{}; + request.shader = {vertex ? ShaderStage::Vertex : ShaderStage::Compute, reinterpret_cast(code.data()), words, 0, {}}; + request.target = Target(byteAccess); + request.layout = {0, 0, 0, 128}; + request.context.waveSize = 32; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + if (vertex) request.context.vertex = ShaderVertexStageInfo{}; + else request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.context.memory = memory; + request.useCache = false; + return Count(Recompile(request).spirv.Words()); +} + +} + +int main() { + try { + const auto bytes = Compile(true); + Check(bytes.compareExchanges == 0, "formatted buffer byte stores: a store still patches its dword with " + std::to_string(bytes.compareExchanges) + " compare-exchange loops"); + Check(bytes.byteBuffers && bytes.byteCapability && bytes.byteTypes == 1, "formatted buffer byte stores: the byte view of the buffers is not declared under StorageBuffer8BitAccess"); + Check(bytes.byteStores >= 4, "formatted buffer byte stores: the 8-bit components are stored with " + std::to_string(bytes.byteStores) + " byte stores, expected at least 4"); + const auto dwords = Compile(false); + Check(dwords.compareExchanges > 0 && !dwords.byteBuffers && !dwords.byteCapability, "formatted buffer byte stores: without StorageBuffer8BitAccess the stores do not fall back to the dword patch"); + std::printf("formatted buffer byte stores tests passed (%zu byte stores, %zu compare-exchange loops without the capability)\n", bytes.byteStores, dwords.compareExchanges); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp b/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp new file mode 100644 index 0000000000..0eba15d628 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp @@ -0,0 +1,119 @@ +#include "BdaAbi.hpp" +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ShaderRecompiler; + +void Check(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +SpirvTarget Target() { + static constexpr std::array capabilities{ + spv::CapabilityInt64, 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 switches = 0; + std::size_t calls = 0; + std::size_t functions = 0; + std::size_t notInlined = 0; + std::size_t compareExchanges = 0; +}; + +Counts Count(std::span words) { + Check(words.size() >= 5 && words[0] == spv::MagicNumber, "formatted buffer 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, "formatted buffer once: malformed SPIR-V instruction"); + const auto opcode = words[offset] & spv::OpCodeMask; + if (opcode == spv::OpSwitch) ++counts.switches; + if (opcode == spv::OpFunctionCall) ++counts.calls; + if (opcode == spv::OpAtomicCompareExchange) ++counts.compareExchanges; + if (opcode == spv::OpFunction) { + ++counts.functions; + if (count > 3 && (words[offset + 3] & spv::FunctionControlDontInlineMask) != 0) ++counts.notInlined; + } + offset += count; + } + return counts; +} + +Counts Compile(std::size_t loads, bool formattedStores = false, bool gpu = false, bool vertex = false, bool registration = false) { + std::vector code; + if (gpu) code.push_back(0x7e160500u); + for (std::uint32_t load = 0; load < loads; ++load) code.insert(code.end(), {0xe0000000u | (load * 4u), 0x80020000u | ((10u + load) << 8u)}); + code.push_back(0xbf8c3f70u); + for (std::uint32_t load = 0; load < loads; ++load) code.insert(code.end(), {(formattedStores ? 0xe0100000u : 0xe0700000u) | (load * 4u), 0x80010000u | ((10u + load) << 8u)}); + if (vertex) code.insert(code.end(), {0xf80008cfu, 0x03020100u}); + code.push_back(0xbf810000u); + const std::span words(code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(words)}}}; + const std::array userData{0u, 0u, 0u, 0u, 0x10000000u, 0u, 0x1000u, 0x31016facu, 0x20000000u, 0u, 0x1000u, 0x31038facu}; + RecompileRequest request{}; + request.shader = {vertex ? ShaderStage::Vertex : ShaderStage::Compute, reinterpret_cast(code.data()), words, 0, {}}; + request.target = Target(); + request.target.registrationModules = registration; + request.layout = {0, 0, 0, 128}; + request.context.waveSize = 32; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + if (vertex) request.context.vertex = ShaderVertexStageInfo{}; + else request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.context.memory = memory; + request.useCache = false; + return Count(Recompile(request).spirv.Words()); +} + +} + +int main() { + try { + const auto none = Compile(0); + const auto one = Compile(1); + const auto three = Compile(3); + const auto gpuOne = Compile(1, false, true); + const auto vertexSpecialized = Compile(1, false, false, true, false); + const auto vertexRegistration = Compile(1, false, false, true, true); + Check(vertexRegistration.compareExchanges == vertexSpecialized.compareExchanges, "formatted buffer once: in registration form a store through an aligned V# kept the unaligned compare-exchange path (" + std::to_string(vertexRegistration.compareExchanges) + " compare-exchanges, " + std::to_string(vertexSpecialized.compareExchanges) + " specialized)"); + Check(vertexRegistration.words < vertexSpecialized.words + vertexSpecialized.words / 2u, "formatted buffer once: in registration form a formatted load from a known descriptor kept every format's decoder (" + std::to_string(vertexRegistration.words) + " words, " + std::to_string(vertexSpecialized.words) + " specialized)"); + Check(one.words < gpuOne.words / 2u, "formatted buffer once: a formatted load from a known descriptor kept every format's decoder (" + std::to_string(one.words) + " words, a GPU-selected descriptor needs " + std::to_string(gpuOne.words) + ")"); + Check(one.calls == none.calls + 1 && one.functions == none.functions + 1, "formatted buffer once: one formatted load emitted " + std::to_string(one.calls - none.calls) + " calls and " + std::to_string(one.functions - none.functions) + " functions more than none, expected one call to one shared function"); + Check(three.calls == one.calls + 2 && three.functions == one.functions, "formatted buffer once: two more formatted loads from the same descriptor emitted " + std::to_string(three.calls - one.calls) + " calls and " + std::to_string(three.functions - one.functions) + " functions more, expected two calls and no function"); + Check(three.switches == one.switches, "formatted buffer once: two more formatted loads from the same descriptor emitted " + std::to_string(three.switches - one.switches) + " more format switches"); + Check(one.notInlined == none.notInlined + 1, "formatted buffer once: the shared formatted access function may be inlined back into its callers"); + const auto storeOne = Compile(1, true); + 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) / 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) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/FragmentBarycentric.cpp b/core/libs/prx/libSceAgcDriver/tests/FragmentBarycentric.cpp new file mode 100644 index 0000000000..6b67d8629f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/FragmentBarycentric.cpp @@ -0,0 +1,33 @@ +#include "prx/libSceAgcDriver/Execution/include/FragmentBarycentric.hpp" +#include + +namespace { + +struct Case { + VkDriverId driver; + bool reported; + bool usable; +}; + +constexpr Case Cases[]{ + {VK_DRIVER_ID_MOLTENVK, true, false}, + {VK_DRIVER_ID_MOLTENVK, false, false}, + {VK_DRIVER_ID_MESA_RADV, true, true}, + {VK_DRIVER_ID_MESA_RADV, false, false}, + {VK_DRIVER_ID_NVIDIA_PROPRIETARY, true, true}, + {VK_DRIVER_ID_AMD_PROPRIETARY, true, true}, + {VK_DRIVER_ID_INTEL_PROPRIETARY_WINDOWS, false, false}, +}; + +} + +int main() { + int failures = 0; + for (const auto& item : Cases) { + if (AgcDriver::FragmentShaderBarycentricUsable(item.driver, item.reported) != item.usable) { + std::fprintf(stderr, "driver %d reported %d: expected %s fragment barycentric\n", static_cast(item.driver), item.reported ? 1 : 0, item.usable ? "usable" : "unusable"); + ++failures; + } + } + return failures == 0 ? 0 : 1; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/FrameDumpIo.cpp b/core/libs/prx/libSceAgcDriver/tests/FrameDumpIo.cpp new file mode 100644 index 0000000000..ec52e5f1d9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/FrameDumpIo.cpp @@ -0,0 +1,149 @@ +#include "prx/libSceAgcDriver/Execution/include/FrameDumpIo.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#include +#else +#include +#endif + +namespace { + +void Check(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "%s\n", message); + std::exit(1); + } +} + +void Close(int descriptor) { +#ifdef _WIN32 + _close(descriptor); +#else + close(descriptor); +#endif +} + +int Read(int descriptor, void* bytes, unsigned count) { +#ifdef _WIN32 + return _read(descriptor, bytes, count); +#else + return static_cast(read(descriptor, bytes, count)); +#endif +} + +std::FILE* Pipe(int& reader) { + int descriptors[2]; +#ifdef _WIN32 + Check(_pipe(descriptors, 4096, _O_BINARY) == 0, "cannot create test pipe"); + auto* file = _fdopen(descriptors[1], "wb"); +#else + Check(pipe(descriptors) == 0, "cannot create test pipe"); + auto* file = fdopen(descriptors[1], "wb"); +#endif + Check(file != nullptr, "cannot open test pipe writer"); + reader = descriptors[0]; + return file; +} + +void SuccessfulBytes() { + auto* file = std::tmpfile(); + Check(file != nullptr, "cannot create BMP test file"); + const std::array pixels{std::byte{1}, std::byte{2}, std::byte{3}, std::byte{4}}; + Check(!AgcDriver::WriteBmpStream_nid_no_patch(file, 2, 2, pixels), "valid BMP write failed"); + Check(std::fflush(file) == 0, "valid BMP flush failed"); + std::rewind(file); + std::array actual{}; + Check(std::fread(actual.data(), 1, actual.size(), file) == actual.size(), "BMP has the wrong byte count"); + const std::array header{ + 0x42, 0x4d, 70, 0, 0, 0, 0, 0, 0, 0, 54, 0, 0, 0, + 40, 0, 0, 0, 2, 0, 0, 0, 0xfe, 0xff, 0xff, 0xff, + 1, 0, 32, 0, 0, 0, 0, 0, 16, 0, 0, 0, + 0x13, 0x0b, 0, 0, 0x13, 0x0b, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + Check(std::memcmp(actual.data(), header.data(), header.size()) == 0, "BMP header changed"); + Check(std::memcmp(actual.data() + header.size(), pixels.data(), pixels.size()) == 0, "BMP pixel payload changed"); + Check(std::fclose(file) == 0, "BMP test close failed"); +} + +void HeaderFailure() { + int reader; + auto* file = Pipe(reader); + Check(std::setvbuf(file, nullptr, _IONBF, 0) == 0, "cannot disable pipe buffering"); + Close(reader); + const std::array pixels{}; + const auto failure = AgcDriver::WriteBmpAndClose_nid_no_patch(file, 2, 2, pixels); + Check(failure && std::strcmp(failure.operation, "header") == 0, "closed pipe header write was accepted"); +} + +void PixelFailure() { + int reader; + auto* file = Pipe(reader); + Check(std::setvbuf(file, nullptr, _IONBF, 0) == 0, "cannot disable pipe buffering"); + unsigned received = 0; + std::thread consumer([&] { + std::array header{}; + while (received < header.size()) { + const auto count = Read(reader, header.data() + received, static_cast(header.size() - received)); + if (count <= 0) break; + received += static_cast(count); + } + Close(reader); + }); + const std::vector pixels(512 * 512 * 4); + const auto failure = AgcDriver::WriteBmpAndClose_nid_no_patch(file, 512, 512, pixels); + consumer.join(); + Check(received == 54, "pixel failure did not follow a complete BMP header"); + Check(failure && std::strcmp(failure.operation, "pixels") == 0, "closed pipe pixel write was accepted"); +} + +void CloseFailure() { + int reader; + auto* file = Pipe(reader); + std::array buffer{}; + Check(std::setvbuf(file, buffer.data(), _IOFBF, buffer.size()) == 0, "cannot buffer test pipe"); + Close(reader); + const std::array pixels{}; + const auto failure = AgcDriver::WriteBmpAndClose_nid_no_patch(file, 2, 2, pixels); + Check(failure && std::strcmp(failure.operation, "close") == 0, "failed buffered close was accepted"); +} + +void DisableAfterFailure() { + const auto directory = std::filesystem::temp_directory_path() / ("anyps5-bmp-io-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Check(std::filesystem::create_directory(directory), "cannot create output-failure fixture"); + AgcDriver::FrameDumpOutput output; + const std::array pixels{}; + Check(output.Enabled_nid_no_patch(), "BMP output was disabled before any failure"); + const auto blocked = directory.string(); + Check(!output.Write_nid_no_patch(blocked.c_str(), 2, 2, pixels), "BMP output accepted a directory as a file"); + Check(!output.Enabled_nid_no_patch(), "failed BMP output remained enabled"); + const auto skipped = directory / "must-not-exist.bmp"; + const auto path = skipped.string(); + for (unsigned i = 0; i < 64; ++i) Check(!output.Write_nid_no_patch(path.c_str(), 2, 2, pixels), "disabled BMP output wrote another file"); + Check(!std::filesystem::exists(skipped), "disabled BMP output created a file"); + Check(std::filesystem::remove(directory), "cannot remove empty fixture directory"); +} + +} + +int main() { +#ifndef _WIN32 + const auto previous = std::signal(SIGPIPE, SIG_IGN); +#endif + SuccessfulBytes(); + HeaderFailure(); + PixelFailure(); + CloseFailure(); + DisableAfterFailure(); +#ifndef _WIN32 + std::signal(SIGPIPE, previous); +#endif + std::puts("frame dump I/O tests passed: bytes/header/pixels/close/disable"); + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index cfbe619f7f..4f7619a163 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1,24 +1,58 @@ #include "BdaTests.hpp" #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 "SceShaders.hpp" +#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp" #include "ControlFlow/RequestSerializer.hpp" #include "CacheKey.hpp" +#include "Optimization/ResourceProgram.hpp" #include "BdaAbi.hpp" +#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 @@ -27,21 +61,25 @@ #include #include #include +#include #include +extern "C" std::terminate_handler APS5_VABI _ZSt13set_terminatePFvvE_nid_postfix(std::terminate_handler handler); + namespace { using AgcDriver::Graphics::Require; alignas(256) std::array colorMemory{}; alignas(256) std::array sliceMemory{}; +alignas(256) std::array volumeKeyMemory{}; AgcDriver::QueueState makeState() { AgcDriver::QueueState queue; 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}, @@ -66,6 +104,8 @@ AgcDriver::QueueState makeState() { queue.context[0x114] = 0; queue.context[0xb4] = 0; queue.context[0xb5] = std::bit_cast(1.0f); + queue.shader[0x008] = 0x100; + queue.shader[0x009] = 0; return queue; } @@ -80,6 +120,191 @@ void expectFailure(TAction action, std::string_view reason) { throw std::runtime_error("expected graphics rejection: " + std::string(reason)); } +void inactiveVertexPointSizeTests() { + const auto pointSizeState = [] { + auto queue = makeState(); + queue.context[0x1b3] = queue.context[0x1b4] = 2; + queue.context[0x207] = 0x01210000; + return queue; + }; + for (const auto primitive : {2u, 4u, 5u, 6u, 7u, 17u, 19u, 21u}) { + auto queue = pointSizeState(); + queue.userConfig[0x242] = primitive; + for (const auto raster : {0u, 0x240u, 0x248u}) { + queue.context[0x205] = raster; + const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false); + Require(rejection.empty(), "inactive vertex point size failed the draw precheck: " + rejection); + static_cast(AgcDriver::Graphics::DecodeState(queue)); + } + } + for (const auto raster : {0u, 0x240u, 0x248u}) { + auto queue = pointSizeState(); + queue.userConfig[0x242] = 3; + queue.context[0x205] = raster; + const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false); + Require(rejection.empty(), "inactive line-strip point size failed the draw precheck: " + rejection); + const auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP && !state.primitiveRestart, "inactive point size changed line-strip decoding"); + Require(AgcDriver::Graphics::RasterPrimitiveOf(state) == ShaderRecompiler::RasterPrimitive::Line, "inactive point size changed the line primitive class"); + queue.userConfig[0x24b] = 1; + queue.context[0x103] = 0xffffffffu; + const auto indexedRejection = AgcDriver::Graphics::DrawRejection(queue, true); + Require(indexedRejection.empty(), "inactive line-strip point size with primitive restart failed the draw precheck: " + indexedRejection); + const auto indexedState = AgcDriver::Graphics::DecodeState(queue); + Require(indexedState.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP && indexedState.primitiveRestart, "inactive point size changed indexed line-strip decoding"); + } + const auto rejected = [](AgcDriver::QueueState queue) { + Require(AgcDriver::Graphics::DrawRejection(queue, false).find("auxiliary vertex exports") != std::string::npos, "unsupported vertex exports passed the draw precheck"); + try { + static_cast(AgcDriver::Graphics::DecodeState(queue)); + } catch (const std::runtime_error&) { + return; + } + throw std::runtime_error("potentially active point size passed the state decoder"); + }; + for (const auto primitive : {1u, 9u}) { + auto queue = pointSizeState(); + queue.userConfig[0x242] = primitive; + rejected(queue); + } + for (const auto stages : {0x2004u, 0x2020u}) { + auto queue = pointSizeState(); + queue.context[0x2d5] = stages; + rejected(queue); + } + for (const auto raster : {8u, 0x208u, 0x48u}) { + auto queue = pointSizeState(); + queue.context[0x205] = raster; + rejected(queue); + } + for (const auto flag : {1u << 17u, 1u << 20u}) { + auto queue = pointSizeState(); + queue.context[0x207] = 0x01210000 | flag; + rejected(queue); + } + for (const auto distances : {1u, 0x100u, 0x00c000ffu, 0x00c0ff00u}) { + auto queue = pointSizeState(); + queue.context[0x207] |= distances; + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "inactive point size rejected supported clip/cull distances"); + Require(AgcDriver::Graphics::DecodeState(queue).paClVsOutCntl == queue.context[0x207], "inactive point size discarded clip/cull distance state"); + } + { + auto queue = pointSizeState(); + queue.context[0x207] |= 0x004001ffu; + Require(AgcDriver::Graphics::DrawRejection(queue, false).find("more than eight") != std::string::npos, "inactive point size bypassed the clip/cull count limit"); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "more than eight clip and cull distances"); + } + for (const auto offset : {0x2d5u, 0x205u}) { + auto queue = pointSizeState(); + queue.context.erase(offset); + rejected(queue); + } + auto queue = pointSizeState(); + queue.userConfig.erase(0x242); + rejected(queue); +} + +void 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 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}) { + auto queue = makeState(); + queue.context[0x31c] = (queue.context[0x31c] & ~0x1f7cu) | (12u << 2u) | (number << 8u) | (1u << 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] = queue.context[0x1b4] = 2; + auto state = DecodeState(queue); + const auto expected = number == 0 ? VK_FORMAT_R16G16B16A16_UNORM : number == 1 ? VK_FORMAT_R16G16B16A16_SNORM : number == 4 ? VK_FORMAT_R16G16B16A16_UINT : VK_FORMAT_R16G16B16A16_SFLOAT; + Require(state.color.format == expected && state.color.elementBytes == 8, "alternate 16-bit color format was not decoded"); + Require(state.color.componentMapping == 0xc6u && ExportMappings(state)[0] == 0xc6u, "alternate 16-bit color export did not swap red and blue"); + Require(DecodePixelStageInfo(queue.context, ExportMappings(state)).targetExportMapping[0] == 0xc6u, "pixel export lost the alternate color mapping"); + queue.context[0x8e] = 1; + state = DecodeState(queue); + Require(state.blend.colorWriteMask == VK_COLOR_COMPONENT_B_BIT, "alternate red-only write did not select the third storage component"); + queue.context[0x8e] = 0xf; + queue.context[0x1e0] = (1u << 30u) | 13u; + for (std::uint32_t i = 0; i < 4; ++i) queue.context[0x105 + i] = std::bit_cast(float(i + 1) / 8.0f); + state = DecodeState(queue); + Require(state.blendConstants == std::array{0.375f, 0.25f, 0.125f, 0.5f}, "alternate color blend constants were not swizzled with the export"); + queue.context[0x31c] &= ~(1u << 11u); + state = DecodeState(queue); + Require(state.color.componentMapping == 0xe4u && state.blendConstants == std::array{0.125f, 0.25f, 0.375f, 0.5f}, "standard color mapping changed"); + queue.context[0x31c] |= 2u << 11u; + expectFailure([&] { DecodeState(queue); }, "reversed component order"); + } + auto queue = makeState(); + queue.context[0x31c] = (queue.context[0x31c] & ~0x1f7cu) | (12u << 2u) | (7u << 8u) | (1u << 11u); + for (const auto offset : {0x318u, 0x31bu, 0x31cu, 0x31du}) queue.context[offset + 0xfu] = queue.context.at(offset); + for (const auto offset : {0x390u, 0x3b0u, 0x3b8u}) queue.context[offset + 1u] = queue.context.at(offset); + queue.context[0x31c + 0xfu] &= ~(1u << 11u); + queue.context[0x1c5] = 0x99u; + queue.context[0x8e] = queue.context[0x8f] = 0xffu; + queue.context[0x1e0] = queue.context[0x1e1] = (1u << 30u) | 13u; + for (std::uint32_t i = 0; i < 4; ++i) queue.context[0x105 + i] = std::bit_cast(float(i + 1) / 8.0f); + expectFailure([&] { DecodeState(queue); }, "conflicting blend constants"); + queue.context[0x107] = queue.context[0x105]; + Require(DecodeState(queue).colors.size() == 2, "compatible constants rejected mixed component orders"); +} + void stateTests() { AgcDriver::QueueState initial; Require(initial.context.at(0x200) == 0 && initial.context.at(0x83) == 0xffff, "initial context state is missing"); @@ -117,15 +342,36 @@ 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("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("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; + Require(AgcDriver::Graphics::DecodeState(queue).conservativeRasterization == VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT, "disabled conservative rasterization decoded as enabled"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).find("PA_SC_CONSERVATIVE") == std::string::npos, "disabled conservative rasterization was rejected"); + } + for (const auto enabled : {0x00e00001u, 0x01e00022u, 0x000e6001u, 0x000e6020u}) { + queue.context[0x313] = enabled; + Require(AgcDriver::Graphics::DrawRejection(queue, false).find("PA_SC_CONSERVATIVE") != std::string::npos, "conservative rasterization was accepted"); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "PA_SC_CONSERVATIVE"); + } + queue.context[0x313] = 0x6000u; queue = makeState(); queue.userConfig.erase(0x24b); queue.context[0x2a5] = 0; @@ -138,6 +384,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; @@ -178,6 +430,13 @@ void stateTests() { state = AgcDriver::Graphics::DecodeState(queue); Require(!state.depthTest && !state.stencilTest, "tests on absent depth and stencil planes were kept"); Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "tests on absent depth and stencil planes were rejected"); + queue.context[0x200] = 0x007007bbu; + state = AgcDriver::Graphics::DecodeState(queue); + Require(!state.depth.has_value() && !state.depthTest && !state.depthBoundsTest && !state.stencilTest, "a depth bounds test on absent depth and stencil planes was kept"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "a depth bounds test on absent depth and stencil planes was rejected"); + queue.context[0x200] = 8u; + Require(!AgcDriver::Graphics::DecodeState(queue).depthBoundsTest && AgcDriver::Graphics::DrawRejection(queue, false).empty(), "a depth bounds test alone on absent depth and stencil planes was kept or rejected"); + queue.context[0x200] = 0x007007b3; queue.context[0x011] = 0x20000181; queue.context[0x012] = 0x00001000; queue.context[0x013] = 0x00002000; @@ -192,6 +451,31 @@ void stateTests() { queue.context[0x10d] = 0x01ffff00; state =AgcDriver::Graphics::DecodeState(queue); Require(!state.depthTest && state.stencilTest, "a depth test on an absent depth plane was kept beside a stencil plane"); + queue.context[0x200] = 0x007007bbu; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "depth bounds without a depth plane"); + queue = makeState(); + queue.context[0x010] = 0x80000183; + queue.context[0x011] = 0x20000180; + queue.context[0x012] = 0x00001000; + queue.context[0x014] = 0x00001000; + queue.context[0x007] = 0x0001001f; + queue.context[0x000] = 0; + queue.context[0x002] = 0; + queue.context[0x00a] = 0; + queue.context[0x00b] = 0x3f800000; + queue.context[0x200] = 0x007007b1; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + state = AgcDriver::Graphics::DecodeState(queue); + Require(!state.depth.has_value() && !state.stencilTest && state.renderExtent.width == state.color.extent.width && state.renderExtent.height == state.color.extent.height, "a stencil test without a stencil plane bound the depth plane it does not test"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "a stencil test without a stencil plane beside an untested depth plane was rejected"); + queue.context[0x200] = 0x00700231; + state = AgcDriver::Graphics::DecodeState(queue); + Require(!state.depth.has_value() && !state.stencilTest && state.renderExtent.width == state.color.extent.width && state.renderExtent.height == state.color.extent.height, "a failing stencil test without a stencil plane bound the depth plane it does not test"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "a failing stencil test without a stencil plane beside an untested depth plane was rejected"); + queue.context[0x200] = 0x007007b3; + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.depth.has_value() && state.depthTest && !state.stencilTest && state.renderExtent.width == 32 && state.renderExtent.height == 2, "a depth test on a bound depth plane lost its surface"); queue = makeState(); queue.context[0x10f] = 0x7fc00000; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "non-finite"); @@ -199,14 +483,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(); @@ -221,10 +509,32 @@ void stateTests() { Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), "a pixel program address was read as unset"); Require(AgcDriver::Graphics::DrawRejection(queue, true).find("missing register at DWORD 0x1b3") != std::string::npos, "a real pixel program without SPI_PS_INPUT_ENA was accepted"); expectFailure([&] { AgcDriver::Graphics::DecodePixelStageInfo(queue.context, std::array{0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u}); }, "missing register"); + AgcDriver::QueueState cleared; + cleared.context[0x1b3] = 2; + cleared.context[0x1b4] = 2; + cleared.context[0x1b6] = 2; + cleared.context[0x192] = 7; + const auto clearedPixel = AgcDriver::Graphics::DecodePixelStageInfo(cleared.context, std::array{0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u}); + Require(clearedPixel.interpolatorSettings[0] == 0 && clearedPixel.interpolatorSettings[1] == 7, "an unwritten SPI_PS_INPUT_CNTL_0 did not read as its clear-state value"); + cleared.ClearContext(); + for (std::uint32_t i = 0; i < 32; ++i) Require(cleared.context.at(0x191 + i) == 0, "CLEAR_STATE did not reset SPI_PS_INPUT_CNTL"); 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"); + Require(AgcDriver::Graphics::DrawRejection(queue, true).find("writes color") != std::string::npos, "a draw with an unwritten pixel program that writes color was accepted"); queue.context[0x8e] = 0; + Require(AgcDriver::Graphics::DrawRejection(queue, true).empty(), "a depth-only draw with an unwritten pixel program was rejected"); + queue.shader[0x008] = 0; + queue.shader[0x009] = 0; Require(AgcDriver::Graphics::DrawRejection(queue, true).empty(), "a depth-only draw without a pixel program was rejected"); state = AgcDriver::Graphics::DecodeState(queue); Require(!state.hasColorTarget, "a depth-only draw without a pixel program decoded a color target"); @@ -245,7 +555,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"); @@ -326,6 +647,296 @@ 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; + using AgcDriver::Graphics::SampleLocations; + const auto fresh = [](std::uint32_t config, std::uint32_t attributes, std::uint32_t scanMode = 3) { + auto written = makeState(); + written.context[0x2f8] = config; + written.context[0x292] = scanMode; + written.context[0x3b8] = 0x0dc00000u; + written.context[0x31d] = attributes; + written.context[0x1b3] = 2; + written.context[0x1b4] = 2; + AgcDriver::QueueState queue; + queue.shader = written.shader; + queue.userConfig = written.userConfig; + written.context.ForEach([&](std::uint32_t offset, std::uint32_t value) { queue.context[offset] = value; }); + return queue; + }; + AgcDriver::QueueState initial; + for (std::uint32_t offset = 0x2fe; offset <= 0x30d; ++offset) Require(initial.context.contains(offset) && initial.context.at(offset) == 0, "an unwritten PA_SC_AA_SAMPLE_LOCS register did not hold its clear-state value 0"); + for (const auto samples : {2u, 4u, 8u}) { + const auto log2 = static_cast(std::countr_zero(samples)); + auto queue = fresh((log2 << 20u) | log2, (log2 << 12u) | (log2 << 15u)); + const auto state = DecodeState(queue); + Require(state.samples == samples && state.colors.size() == 1 && state.colors[0].samples == samples, "a multisampled draw with unwritten sample locations did not decode its sample count"); + Require(state.customSampleLocations, "unwritten sample locations did not decode as the clear-state pattern"); + for (std::uint32_t sample = 0; sample < samples; ++sample) Require(state.sampleLocations[sample] == 0, "an unwritten sample location was not at the pixel center"); + const auto centered = SampleLocations(state); + for (std::uint32_t sample = 0; sample < samples; ++sample) Require(centered[sample].x == 0.5f && centered[sample].y == 0.5f, "unwritten sample locations did not place every sample at the pixel center"); + const auto rejection = DrawRejection(queue, false); + Require(rejection.empty(), "the precheck refused a multisampled draw with unwritten sample locations: " + rejection); + } + auto queue = fresh(0x00200002, 0x12000); + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0x622ae6ae; + auto state = DecodeState(queue); + Require(state.samples == 4 && !state.customSampleLocations, "explicit standard 4x sample locations changed"); + queue = fresh(0x00200002, 0x12000); + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0x22eea66a; + state = DecodeState(queue); + Require(state.samples == 4 && state.customSampleLocations, "explicit custom 4x sample locations changed"); + const auto custom = SampleLocations(state); + const std::array expected{{{0.125f, 0.875f}, {0.875f, 0.125f}, {0.375f, 0.375f}, {0.625f, 0.625f}}}; + for (std::size_t sample = 0; sample < expected.size(); ++sample) Require(custom[sample].x == expected[sample].x && custom[sample].y == expected[sample].y, "explicit custom 4x sample coordinates changed"); + queue.ClearContext(); + for (std::uint32_t offset = 0x2fe; offset <= 0x30d; ++offset) Require(queue.context.at(offset) == 0, "CLEAR_STATE did not reset PA_SC_AA_SAMPLE_LOCS"); + expectFailure([&] { DecodeState(fresh(0x00400004, 0x12000)); }, "more than eight samples"); + expectFailure([&] { DecodeState(fresh(0x00100002, 0x12000)); }, "exposing fewer samples"); + queue = fresh(0, 0, 3); + state = DecodeState(queue); + Require(state.samples == 1 && DrawRejection(queue, false).empty(), "MSAA_ENABLE on a single-sample target with unwritten sample locations was refused"); +} + +void 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, lines"); + queue = drawC(); + queue.userConfig[0x242] = 2; + expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE are unsupported for points, lines"); + 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, PS_ITER_SAMPLE, 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; + 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); + queue = depthTarget(color); + queue.userConfig[0x242] = 2; + 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; + const auto enabled = [] { + auto queue = makeState(); + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + queue.context[0x292] = 3; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0; + return queue; + }; + const auto refused = [](const AgcDriver::QueueState& queue, std::string_view reason) { + expectFailure([&] { DecodeState(queue); }, reason); + const auto rejection = DrawRejection(queue, false); + Require(rejection.find(reason) != std::string::npos, "the precheck did not refuse MSAA_ENABLE on a single-sample target (" + std::string(reason) + "): " + rejection); + }; + auto queue = enabled(); + auto clear = queue; + clear.context[0x292] = 2; + const auto baseline = DecodeState(clear); + for (const auto locations : {0x00000000u, 0xcc44cc00u}) { + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = locations; + const auto state = DecodeState(queue); + Require(state.samples == 1 && state.colors.size() == 1 && state.colors[0].samples == 1 && !state.alphaToCoverage, "MSAA_ENABLE on a single-sample target did not decode as single-sampled"); + Require(state.topology == baseline.topology && state.conservativeRasterization == baseline.conservativeRasterization && state.cullMode == baseline.cullMode && state.frontFace == baseline.frontFace, "MSAA_ENABLE on a single-sample target changed the rasterizer state"); + Require(state.scissor.offset.x == baseline.scissor.offset.x && state.scissor.offset.y == baseline.scissor.offset.y && state.scissor.extent.width == baseline.scissor.extent.width && state.scissor.extent.height == baseline.scissor.extent.height, "MSAA_ENABLE on a single-sample target changed the scissor"); + Require(state.colors[0].address == baseline.colors[0].address && state.colors[0].format == baseline.colors[0].format && state.blend.colorWriteMask == baseline.blend.colorWriteMask, "MSAA_ENABLE on a single-sample target changed the color target"); + const auto rejection = DrawRejection(queue, false); + Require(rejection.empty(), "the precheck refused MSAA_ENABLE on a single-sample target: " + rejection); + } + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + for (const auto location : {0x04u, 0x40u, 0xc4u}) { + queue = enabled(); + queue.context[0x2fe + pixel * 4u] = location; + refused(queue, "off the pixel center"); + } + } + queue = enabled(); + queue.context.erase(0x306); + expectFailure([&] { DecodeState(queue); }, "missing register"); + queue = enabled(); + queue.context[0x203] &= ~0x800u; + queue.context[0x2dc] = 0xaa01; + refused(queue, "MSAA_ENABLE on a single-sample target with alpha-to-coverage"); + queue = enabled(); + queue.context[0x2dc] = 0xaa01; + refused(queue, "MSAA_ENABLE on a single-sample target with alpha-to-coverage"); + queue.context[0x2dc] = 0x18701; + Require(!DecodeState(queue).alphaToCoverage && DrawRejection(queue, false).empty(), "MSAA_ENABLE on a single-sample target ignored the dithered alpha disable override"); + queue = enabled(); + queue.context[0x313] = 0x6001; + refused(queue, "MSAA_ENABLE on a single-sample target with conservative rasterization"); + for (const auto primitive : {2u, 3u}) { + queue = enabled(); + queue.userConfig[0x242] = primitive; + refused(queue, "primitives other than triangles and rectangles"); + } + queue = enabled(); + queue.userConfig[0x242] = 1; + 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 filled triangle or rectangle primitive"); + } + queue = enabled(); + queue.context[0x292] = 7; + refused(queue, "scan conversion mode"); + clear.context[0x2fe] = 0x44; + clear.context[0x203] &= ~0x800u; + clear.context[0x2dc] = 0xaa01; + Require(DecodeState(clear).alphaToCoverage && DrawRejection(clear, false).empty(), "a single-sample target without MSAA_ENABLE was refused over its sample locations or alpha-to-coverage"); +} + void ShaderStageTests() { auto queue = makeState(); for (const auto routing : {0x2000u, 0x2010u, 0x02002000u, 0x02002010u}) { @@ -340,6 +951,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; @@ -352,6 +979,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; @@ -380,6 +1013,79 @@ void ShaderStageTests() { fan.userConfig[0x242] = 3; expectFailure([&] { AgcDriver::Graphics::DecodeState(fan); }, "unsupported geometry input or output assembly"); } + { + auto fast = makeState(); + fast.userConfig[0x242] = 1; + fast.context[0x2d5] = 0x482030; + fast.userConfig[0x25b] = 0x201; + fast.context[0x1ff] = 32; + fast.context[0x2ce] = 32; + fast.context[0x2d3] = 64; + fast.context[0x29b] = 2; + fast.context[0x2ab] = 4; + fast.shader[0x8a] = 0x022c0245; + fast.shader[0x8b] = 0x9002a; + const auto state = AgcDriver::Graphics::DecodeState(fast); + Require(state.stages.path == AgcDriver::Graphics::ShaderPath::Geometry && state.stages.vertexWaveSize == 32 && state.stages.mesh, "a fast-launch draw did not decode as wave32 geometry"); + const auto& mesh = *state.stages.mesh; + Require(mesh.inputPrimitive == 1 && mesh.primitivesPerGroup == 1 && mesh.verticesPerGroup == 1 && mesh.maxVertices == 32 && mesh.maxPrimitives == 64 && mesh.threadsPerGroup == 64 && mesh.ldsSizeDwords == 128, "a fast-launch subgroup did not decode as one point amplified by PRIM_AMP_FACTOR"); + fast.context[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; + fast.userConfig[0x25b] = 0x402; + expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "fast launch other than one point per subgroup"); + fast.userConfig[0x25b] = 0x201; + fast.userConfig[0x242] = 4; + expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "fast launch other than one point per subgroup"); + fast.userConfig[0x242] = 1; + fast.context[0x2d3] = 0; + expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "amplifies to no primitives"); + } + { + 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[0x205] = raster; + lines.context[0x29b] = 1; + lines.context[0x2ce] = 1; + expectFailure([&] { AgcDriver::Graphics::DecodeState(lines); }, "unsupported geometry input or output assembly"); + lines.context[0x2ce] = 5; + for (const auto output : {0u, 3u, 0x80000001u, 0x101u}) { + lines.context[0x29b] = output; + expectFailure([&] { AgcDriver::Graphics::DecodeState(lines); }, "VGT_GS_OUT_PRIM_TYPE other than a triangle strip, or a line strip from a geometry program, is unsupported"); + } + lines.userConfig[0x242] = 4; + lines.context[0x205] = 0x2a40; + lines.context[0x29b] = 2; + Require(AgcDriver::Graphics::DecodeState(lines).stages.mesh->primitivesPerGroup == 17, "triangle input with a triangle-strip output and a triangle polygon offset did not decode"); + lines.context[0x29b] = 1; + expectFailure([&] { AgcDriver::Graphics::DecodeState(lines); }, "polygon mode, depth bias or nonstandard rasterization"); + } queue.context[0x2ab] = 0; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "invalid VGT_ESGS_RING_ITEMSIZE"); queue.context[0x2ab] = 4; @@ -389,15 +1095,65 @@ void ShaderStageTests() { queue.context[0x2d5] = 0x200d; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "Patch topology and HS_EN disagree"); queue.userConfig[0x242] = 9; - queue.context[0x2d6] = (3u << 8u) | (3u << 14u); - queue.context[0x2db] = 1u | (2u << 2u) | (2u << 5u); - stages = AgcDriver::Graphics::DecodeState(queue).stages; - Require(stages.path == AgcDriver::Graphics::ShaderPath::Tessellation && stages.tessellation && stages.tessellation->inputControlPoints == 3, "tessellation routing changed"); + queue.userConfig[0x25b] = 0x1fe55; + queue.context[0x2d6] = 0xc355; + queue.context[0x2db] = 0x40049; + queue.context[0x286] = 0x42800000; + queue.context[0x287] = 0x3f800000; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "unsupported tessellation routing"); + queue.context[0x2d5] = 0x0200010d; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "unsupported tessellation routing"); + queue.context[0x2d5] = 0x0200210d; + { + std::vector tessellationLog; + { + 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{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; + queue.userConfig[0x25b] = 0x5fe3f; + const auto quadStages = AgcDriver::Graphics::DecodeState(queue).stages; + const auto& quads = quadStages.tessellation; + Require(quads && quads->inputControlPoints == 4 && quads->outputControlPoints == 4 && quads->domain == 2 && quads->partitioning == 2 && quads->outputTopology == 3 && quadStages.minTessellationLevel == 1.0f && quadStages.maxTessellationLevel == 9.0f && quads->patchesPerGroup == 63 && quads->primitiveGroupSize == 63 && quads->breakAtInstance, "counter-clockwise quad patches with HOS levels [1, 9], 63 patches per group and BREAK_WAVE_AT_EOI decoded differently"); + queue.userConfig[0x25b] = 0x8040; + const auto shared = AgcDriver::Graphics::DecodeState(queue).stages.tessellation; + Require(shared && shared->primitiveGroupSize == 64 && !shared->breakAtInstance, "GE_CNTL 0x8040 did not decode as 64 primitives per group with BREAK_WAVE_AT_EOI clear"); + queue.userConfig.erase(0x25b); + const auto missing = AgcDriver::Graphics::DecodeState(queue).stages.tessellation; + Require(missing->primitiveGroupSize == 0 && !missing->breakAtInstance, "a draw without GE_CNTL decoded a primitive group size or BREAK_WAVE_AT_EOI"); + queue.context[0x2db] = 0x40069; + Require(AgcDriver::Graphics::DecodeState(queue).stages.tessellation->outputTopology == 3, "counter-clockwise triangle patches decoded differently"); + for (const auto [maximum, minimum, reason] : {std::tuple{0x3f800000u, 0x40000000u, "tessellation level clamps"}, std::tuple{0x41100000u, 0xbf800000u, "tessellation level clamps"}, std::tuple{0x3f000000u, 0x00000000u, "tessellation level clamps"}, std::tuple{0x42800000u, 0x42800000u, "tessellation level clamps"}, std::tuple{0x7fc00000u, 0x3f800000u, "non-finite register"}, std::tuple{0x7f800000u, 0x3f800000u, "non-finite register"}}) { + queue.context[0x286] = maximum; + queue.context[0x287] = minimum; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, reason); + } + queue.context[0x2d6] = 0xc355; + queue.context[0x2db] = 0x40049; + queue.context[0x286] = 0x42800000; + queue.context[0x287] = 0x3f800000; queue.context[0x2d5] = 0x202d; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "combined tessellation and geometry"); - queue.context[0x2d5] = 0x200d; + queue.context[0x2d5] = 0x0200210d; queue.context[0x2d6] = 0; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "control-point counts"); + queue.context[0x2d6] = 0xc300; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "NUM_PATCHES is zero"); queue = makeState(); for (const auto value : {0x2003u, 0x2018u, 0x20c0u, 0x80002000u}) { queue.context[0x2d5] = value; @@ -416,6 +1172,68 @@ 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; + queue.userConfig[0x242] = 4; + queue.userConfig[0x25b] = 0x10080; + queue.context[0x1ff] = 0x80; + queue.context[0x2ce] = 0; + queue.context[0x29b] = 2; + queue.context[0x2ab] = 1; + queue.shader[0x8a] = 0x222c0009; + queue.shader[0x8b] = 0x43001e; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + 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; + mesh = *AgcDriver::Graphics::DecodeState(queue, true).stages.mesh; + Require(mesh.primitivesPerGroup == 126 && mesh.verticesPerGroup == 128 && mesh.maxPrimitives == 128, "the NGG vertex subgroup of a triangle strip is not bounded by its vertex budget"); + queue.shader[0x8a] = 0x222c0005; + queue.shader[0x8b] = 0x400030; + Require(AgcDriver::Graphics::DecodeState(queue, true).stages.mesh.has_value(), "an NGG vertex program that loads only its vertex ID was rejected"); + queue.shader[0x8a] = 0x222c0009; + queue.shader[0x8b] = 0x43001e; + queue.context[0x1ff] = 2; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue, true); }, "invalid geometry subgroup output"); + queue.context[0x1ff] = 0x80; + for (const auto stages : {0x02012010u, 0x00012030u, 0x00010010u}) { + queue.context[0x2d5] = stages; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue, true); }, "requires PRIMGEN_EN without GS_EN, PRIMGEN_PASSTHRU_EN or tessellation"); + } + queue.context[0x2d5] = 0x12010; + queue.context[0x29b] = 1; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue, true); }, "VGT_GS_OUT_PRIM_TYPE other than a triangle strip, or a line strip from a geometry program, is unsupported"); +} + constexpr std::array IdentityExports{0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u}; std::vector pixelBuiltinsRead(std::uint32_t ena, std::uint32_t addr, std::uint32_t source) { @@ -448,14 +1266,44 @@ 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(); } -std::vector pixelNoPerspectiveLocations(bool barycentricEnabled) { +std::vector pixelNoPerspectiveLocations(bool barycentricEnabled, std::uint32_t inputs = 0x22u) { auto queue = makeState(); - queue.context[0x1b3] = 0x22u; - queue.context[0x1b4] = 0x22u; + queue.context[0x1b3] = inputs; + queue.context[0x1b4] = inputs; queue.context[0x1b6] = 2u; queue.context[0x191] = 0u; queue.context[0x192] = 1u; @@ -489,6 +1337,7 @@ std::vector pixelNoPerspectiveLocations(bool barycentricEnabled) } } if (barycentricEnabled) Require(perVertex == 2u && perspectiveBarycentrics && linearBarycentrics, "explicit interpolation must preserve both parameter vertices and both barycentric inputs"); + if (!barycentricEnabled) Require(!result.barycentricEmulation.active, "v_interp_p1/p2 of their own I/J pairs did not keep host interpolation"); std::vector result2; for (const auto id : noPerspective) result2.push_back(locations.count(id) != 0 ? locations.at(id) : 0xffffffffu); return result2; @@ -512,6 +1361,9 @@ void ComputeScratchTests() { expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeComputeStageInfo(shader, header)); }, "zero scratch size"); shader[0x213] = 0u; Require(AgcDriver::Graphics::DecodeComputeStageInfo(shader, {}).scratchDwords == 0u, "a dispatch without SCRATCH_EN got scratch"); + shader[0x207] = 0x00100040u; + Require(AgcDriver::Graphics::DecodeComputeStageInfo(shader, {}).numThreads[0] == 64u, "the partial group size leaked into the group size"); + shader[0x207] = 64u; ShaderRecompiler::RecompileRequest request{}; const std::array code{0xbf810000u}; request.shader = {ShaderRecompiler::ShaderStage::Compute, 0x30000u, code, 0, {}}; @@ -522,6 +1374,145 @@ void ComputeScratchTests() { Require(back.request.context.compute->scratchDwords == 24u, "the compute scratch size did not survive serialization"); } +void shaderUserDataTailPaddingTests() { + constexpr std::size_t userDataOffset = 288; + constexpr auto userDataBytes = offsetof(ShaderUserData, sharp_resource_count) + sizeof(ShaderUserData{}.sharp_resource_count); + static_assert(userDataBytes == 54); + std::vector header(userDataOffset + userDataBytes); + Shader shader{}; + shader.user_data = reinterpret_cast(header.data() + userDataOffset); + std::memcpy(header.data(), &shader, sizeof(shader)); + const auto headerAddress = reinterpret_cast(header.data()); + const auto info = AgcDriver::Graphics::DecodeVertexStageInfo(header, headerAddress, {}, nullptr, true); + Require(!info.fetchEmbedded, "a ShaderUserData block without trailing struct padding was rejected"); +} + +void ColorPipeBankXorTests() { + constexpr std::uint32_t side = 128; + constexpr std::size_t blockBytes = 65536; + static std::vector storage(2 * blockBytes + side * side * 4); + const auto block = (reinterpret_cast(storage.data()) + blockBytes - 1) / blockBytes * blockBytes; + auto queue = makeState(); + queue.context[0x3b8] |= static_cast(AgcDriver::Graphics::ColorTileMode::RenderTarget) << 14u; + queue.context[0x3b0] = ((side - 1u) << 14u) | (side - 1u); + for (const auto offset : {0xdu, 0x82u, 0x91u, 0x95u}) queue.context[offset] = (side << 16u) | side; + for (const std::uint32_t pipeBankXor : {0u, 0x5600u, 0xff00u}) { + const auto address = block + pipeBankXor; + queue.context[0x318] = static_cast(address >> 8u); + queue.context[0x390] = static_cast(address >> 40u); + const auto color = AgcDriver::Graphics::DecodeState(queue).color; + Require(color.address == block && color.surfaceAddress == block && color.pipeBankXor == pipeBankXor, "a SW_64KB_R_X color base did not split into its block base and pipe/bank XOR " + std::to_string(pipeBankXor)); + } + queue.context[0x3b8] = 0x9000000u | (0x16u << 14u) | (0x14u << 19u); + for (const std::uint32_t pipeBankXor : {0u, 0xa00u, 0xf00u}) { + const auto address = block + 0x3000u + pipeBankXor; + queue.context[0x318] = static_cast(address >> 8u); + queue.context[0x390] = static_cast(address >> 40u); + const auto color = AgcDriver::Graphics::DecodeState(queue).color; + Require(color.tileMode == AgcDriver::Graphics::ColorTileMode::D4KBX && color.address == block + 0x3000u && color.pipeBankXor == pipeBankXor, "a SW_4KB_D_X color base did not split into its 4 KiB block base and pipe/bank XOR " + std::to_string(pipeBankXor)); + } + queue.context[0x3b8] = 0x0dc68000u; + for (const std::uint32_t pipeBankXor : {0u, 0x4600u, 0xcf00u}) { + const auto address = block + pipeBankXor; + queue.context[0x318] = static_cast(address >> 8u); + queue.context[0x390] = static_cast(address >> 40u); + const auto color = AgcDriver::Graphics::DecodeState(queue).color; + Require(color.tileMode == AgcDriver::Graphics::ColorTileMode::D64KBX && color.address == block && color.surfaceAddress == block && color.pipeBankXor == pipeBankXor, "a SW_64KB_D_X color base did not split its block address and XOR"); + } + queue.context[0x3b8] = 0x4dc60000u; + for (const std::uint32_t pipeBankXor : {0u, 0x4600u, 0xff00u}) { + const auto address = block + pipeBankXor; + queue.context[0x318] = static_cast(address >> 8u); + queue.context[0x390] = static_cast(address >> 40u); + const auto color = AgcDriver::Graphics::DecodeState(queue).color; + Require(color.tileMode == AgcDriver::Graphics::ColorTileMode::Z64KBX && color.address == block && color.surfaceAddress == block && color.pipeBankXor == pipeBankXor, "a SW_64KB_Z_X color base did not split its block address and XOR"); + } + queue.context[0x3b8] = 0x0dc68000u; + for (const std::uint32_t columnBits : {0x1000u, 0x2000u, 0x3000u}) { + queue.context[0x318] = static_cast((block + columnBits) >> 8u); + queue.context[0x390] = static_cast((block + columnBits) >> 40u); + expectFailure([&queue] { AgcDriver::Graphics::DecodeColorBuffer(queue.context, 0); }, "unsupported display color pipe/bank XOR column bits"); + } + static std::vector mipStorage(16u * blockBytes); + const auto mipBase = (reinterpret_cast(mipStorage.data()) + blockBytes - 1u) / blockBytes * blockBytes; + const auto mips = AgcDriver::Graphics::ComputeElementMipLayout(AgcDriver::Graphics::TextureTileMode::kD64KBX, 4, 256, 128, 5); + const auto sliceBytes = AgcDriver::Graphics::ComputeSurfaceSize(mips, 1); + queue.context[0x318] = static_cast((mipBase + 0x0200u) >> 8u); + queue.context[0x390] = static_cast((mipBase + 0x0200u) >> 40u); + queue.context[0x3b0] = (4u << 28u) | (255u << 14u) | 127u; + queue.context[0x3b8] |= 1u; + queue.context[0x31b] = (2u << 26u) | 1u | (1u << 13u); + const auto mipColor = AgcDriver::Graphics::DecodeColorBuffer(queue.context, 0); + Require(mipColor.mip == 2 && mipColor.mipCount == 5 && mipColor.extent.width == 64 && mipColor.extent.height == 32 && mipColor.surfaceAddress == mipBase + sliceBytes && mipColor.address == mipBase + sliceBytes + mips[2].tiledOffset && mipColor.mipTail == mips[2].tail && mipColor.pipeBankXor == 0x0200u, "a display XOR color view lost its array slice, mip placement or XOR"); +} + +void HiddenUserWordVertexTablesTests() { + constexpr std::size_t userDataOffset = 288; + constexpr std::size_t directOffset = 352; + constexpr std::size_t semanticOffset = 384; + static std::vector header(semanticOffset + sizeof(ShaderSemantic)); + static std::array bufferTable{0x1000u, 0x20000u, 64u, 0x3000u}; + static std::array attribTable{(0x2cu << 5u)}; + std::array direct{}; + direct.fill(ShaderRegs::AGC_ILLEGAL_DIRECT_OFFSET); + direct[static_cast(ShaderRegs::AgcDirectResourceType::PtrVertexBufferTable)] = 4; + direct[static_cast(ShaderRegs::AgcDirectResourceType::PtrVertexAttribDescTable)] = 6; + std::memcpy(header.data() + directOffset, direct.data(), sizeof(direct)); + ShaderSemantic semantic{}; + semantic.size_in_elements = 1; + std::memcpy(header.data() + semanticOffset, &semantic, sizeof(semantic)); + ShaderUserData userData{}; + userData.direct_resource_offset = reinterpret_cast(header.data() + directOffset); + userData.direct_resource_count = ShaderRegs::AGC_DIRECT_RESOURCE_TYPE_COUNT; + std::memcpy(header.data() + userDataOffset, &userData, sizeof(userData)); + Shader shader{}; + shader.user_data = reinterpret_cast(header.data() + userDataOffset); + shader.input_semantics = reinterpret_cast(header.data() + semanticOffset); + shader.num_input_semantics = 1; + std::memcpy(header.data(), &shader, sizeof(shader)); + const auto headerAddress = reinterpret_cast(header.data()); + const auto low = [](const void* pointer) { return static_cast(reinterpret_cast(pointer)); }; + const auto high = [](const void* pointer) { return static_cast(reinterpret_cast(pointer) >> 32u); }; + const std::array registers{0x31016facu, 0x7efff390u, 0x100002u, 0x49u, low(bufferTable.data()), high(bufferTable.data()), low(attribTable.data()), high(attribTable.data())}; + std::vector merged(8, 0u); + merged.insert(merged.end(), registers.begin(), registers.end()); + const auto info = AgcDriver::Graphics::DecodeVertexStageInfo(header, headerAddress, merged, nullptr, false, 8); + Require(info.fetchEmbedded && info.fetchBufferReg == 4u && info.fetchAttribReg == 6u && info.resourcesNum == 1 && info.resources[0].fields[0] == 0x1000u && info.resources[0].fields[2] == 64u, "a merged program's vertex tables were not read past its eight hidden user words"); + expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeVertexStageInfo(header, headerAddress, merged, nullptr, false, 0)); }, "null vertex attribute table"); + const auto plain = AgcDriver::Graphics::DecodeVertexStageInfo(header, headerAddress, registers, nullptr, false, 0); + Require(plain.fetchEmbedded && plain.fetchBufferReg == 4u && plain.resourcesNum == 1 && plain.resources[0].fields[0] == 0x1000u, "a vertex program's tables moved"); +} + +void HeaderlessVertexStageTests() { + const std::array userData{1, 2, 3, 4}; + const auto info = AgcDriver::Graphics::DecodeVertexStageInfo({}, 0, userData); + Require(!info.fetchEmbedded && info.resourcesNum == 0, "a vertex program without an AGC header received an embedded fetch table"); + const std::array truncated{}; + expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeVertexStageInfo(truncated, 0x1000, userData)); }, "smaller than the fixed AGC header"); +} + +void UnexportedPixelInputTests() { + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.interpolatorCount = 5; + pixel.interpolatorSettings[0] = 0x000u; + pixel.interpolatorSettings[1] = 0x401u; + pixel.interpolatorSettings[2] = 0x302u; + pixel.interpolatorSettings[3] = 0x123u; + pixel.interpolatorSettings[4] = 0x680004u; + pixel.interpolatorSettings[5] = 0x005u; + const std::array exported{0u}; + const auto adjusted = AgcDriver::Graphics::DefaultUnexportedInputs(pixel, exported); + Require(adjusted.interpolatorSettings[0] == 0x000u, "an input of an exported parameter was changed"); + Require(adjusted.interpolatorSettings[1] == 0x021u, "a flat input of an unexported parameter did not read zero"); + Require(adjusted.interpolatorSettings[2] == 0x022u, "an input of an unexported parameter did not read zero"); + Require(adjusted.interpolatorSettings[3] == 0x123u, "an input that already reads its default value was changed"); + Require(adjusted.interpolatorSettings[4] == 0x180024u, "an FP16 input of an unexported parameter did not read zero in both halves"); + Require(adjusted.interpolatorSettings[5] == 0x005u, "a setting past SPI_PS_IN_CONTROL's input count was changed"); + const std::array all{0u, 1u, 2u, 4u, 5u}; + const auto unchanged = AgcDriver::Graphics::DefaultUnexportedInputs(pixel, all); + Require(std::ranges::equal(unchanged.interpolatorSettings, pixel.interpolatorSettings), "inputs of exported parameters were changed"); +} + void PixelInputLayoutTests() { using ShaderRecompiler::PixelInput; using ShaderRecompiler::PixelInputVgpr; @@ -536,11 +1527,37 @@ 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, 0x80u, 0x4000u, 0x8000u}) { - 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); + Require(PixelInputVgpr(pixel.inputAddr, PixelInput::LineStipple) == 2u && PixelInputVgpr(pixel.inputAddr, PixelInput::PositionFixedPoint) == 3u, "LINE_STIPPLE_TEX and POS_FIXED_PT did not take their VGPRs after the perspective center"); + Require(readsBuiltin(pixelBuiltinsRead(0x8082u, 0x8082u, 3u), spv::BuiltInFragCoord), "POS_FIXED_PT is not read from the fragment coordinate"); + Require(!readsBuiltin(pixelBuiltinsRead(0x82u, 0x82u, 2u), spv::BuiltInFragCoord), "LINE_STIPPLE_TEX read the fragment coordinate"); pixel = decode(0x546u, 0x7c7u); { ShaderRecompiler::RecompileRequest request{}; @@ -560,6 +1577,19 @@ void PixelInputLayoutTests() { const auto noPerspective = pixelNoPerspectiveLocations(false); Require(noPerspective.size() == 1 && noPerspective[0] == 1u, "only the parameter interpolated through the linear pair must be NoPerspective"); Require(pixelNoPerspectiveLocations(true).empty(), "explicit interpolation must not interpolate parameter arrays a second time"); + for (const auto inputs : {0x11u, 0x12u, 0x21u}) { + const auto sampleNoPerspective = pixelNoPerspectiveLocations(false, inputs); + Require(sampleNoPerspective.size() == 1 && sampleNoPerspective[0] == 1u, "a parameter interpolated through a sample I/J pair lost its interpolation mode: inputs " + std::to_string(inputs)); + Require(pixelNoPerspectiveLocations(true, inputs).empty(), "explicit interpolation through a sample I/J pair interpolated parameter arrays a second time: inputs " + std::to_string(inputs)); + } + for (const auto source : {0u, 1u}) { + const auto read = pixelBuiltinsRead(0x3u, 0x3u, source); + Require(readsBuiltin(read, spv::BuiltInBaryCoordKHR) && !readsBuiltin(read, spv::BuiltInFragCoord), "PERSP_SAMPLE does not hold the perspective barycentrics: v" + std::to_string(source)); + } + for (const auto source : {2u, 3u}) { + const auto read = pixelBuiltinsRead(0x12u, 0x12u, source); + Require(readsBuiltin(read, spv::BuiltInBaryCoordNoPerspKHR) && !readsBuiltin(read, spv::BuiltInBaryCoordKHR), "LINEAR_SAMPLE does not hold the linear barycentrics: v" + std::to_string(source)); + } auto read = pixelBuiltinsRead(0x106u, 0x106u, 4u); Require(readsBuiltin(read, spv::BuiltInFragCoord) && !readsBuiltin(read, spv::BuiltInBaryCoordKHR), "POS_X is not in v4 after the center and centroid pairs"); read = pixelBuiltinsRead(0x326u, 0x326u, 5u); @@ -572,6 +1602,77 @@ void PixelInputLayoutTests() { Require(!readsBuiltin(read, spv::BuiltInFragCoord) && !readsBuiltin(read, spv::BuiltInBaryCoordKHR), "an ADDR-only centroid pair was loaded"); } +void BlendBypassTests() { + auto queue = makeState(); + for (std::uint32_t constant = 0x105; constant < 0x109; ++constant) queue.context[constant] = 0; + queue.context[0x1e0] = (1u << 30u) | 4u | (5u << 8u); + Require(AgcDriver::Graphics::DecodeState(queue).blends.at(0).blendEnable, "enabled blending was not decoded"); + queue.context[0x31c] |= 0x10000u; + const auto bypassed = AgcDriver::Graphics::DecodeState(queue); + Require(!bypassed.blends.at(0).blendEnable && bypassed.blends.at(0).colorWriteMask != 0, "a target with BLEND_BYPASS still blends"); +} + +void 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; + queue.context[0x1e0] = (1u << 30u) | (1u << 29u) | 6u | (7u << 8u) | (8u << 16u) | (10u << 24u); + auto blend = AgcDriver::Graphics::DecodeState(queue).blend; + Require(blend.srcColorBlendFactor == VK_BLEND_FACTOR_DST_ALPHA && blend.dstColorBlendFactor == VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA, "color blend factors changed"); + Require(blend.srcAlphaBlendFactor == VK_BLEND_FACTOR_DST_COLOR && blend.dstAlphaBlendFactor == VK_BLEND_FACTOR_SRC_ALPHA_SATURATE, "alpha blend factors changed"); + queue.context[0x31d] = 0x20000u; + blend = AgcDriver::Graphics::DecodeState(queue).blend; + Require(blend.srcColorBlendFactor == VK_BLEND_FACTOR_ONE && blend.dstColorBlendFactor == VK_BLEND_FACTOR_ZERO, "an opaque destination left a destination-alpha color factor"); + Require(blend.srcAlphaBlendFactor == VK_BLEND_FACTOR_ONE && blend.dstAlphaBlendFactor == VK_BLEND_FACTOR_SRC_ALPHA_SATURATE, "an opaque destination left a destination alpha factor"); + queue.context[0x1e0] = (1u << 30u) | 10u | (9u << 8u); + blend = AgcDriver::Graphics::DecodeState(queue).blend; + Require(blend.srcColorBlendFactor == VK_BLEND_FACTOR_ZERO && blend.dstColorBlendFactor == VK_BLEND_FACTOR_ONE_MINUS_DST_COLOR, "alpha saturation with an opaque destination"); + Require(blend.dstAlphaBlendFactor == VK_BLEND_FACTOR_ZERO, "an opaque destination left a destination-color alpha factor"); + queue.context[0x31d] = 0x40000u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "destination alpha override"); +} + void DisabledColorTests() { auto queue = makeState(); queue.context[0x8e] = 0; @@ -580,7 +1681,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); @@ -608,6 +1714,11 @@ 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] = 0; for (const auto colorControl : {0x0u, 0xcc0000u}) { queue = makeState(); queue.context[0x202] = colorControl; @@ -644,7 +1755,49 @@ void TuningFieldTests() { } } +void ShadersWriteMemoryTests() { + const auto binding = [](ShaderRecompiler::DescriptorKind kind, ShaderRecompiler::DescriptorRole role, std::vector written) { + ShaderRecompiler::DescriptorBinding result{}; + result.kind = kind; + result.role = role; + result.count = 1; + if (role == ShaderRecompiler::DescriptorRole::GuestImages) result.imageWritten = std::move(written); + else result.bufferWritten = std::move(written); + return result; + }; + ShaderRecompiler::RecompileResult reads; + reads.bindings.push_back(binding(ShaderRecompiler::DescriptorKind::StorageBuffer, ShaderRecompiler::DescriptorRole::GuestBuffers, {false})); + reads.bindings.push_back(binding(ShaderRecompiler::DescriptorKind::StorageImage, ShaderRecompiler::DescriptorRole::GuestImages, {false})); + ShaderRecompiler::RecompileResult stores; + stores.bindings.push_back(binding(ShaderRecompiler::DescriptorKind::StorageBuffer, ShaderRecompiler::DescriptorRole::GuestBuffers, {true})); + using Stage = ShaderRecompiler::ShaderStage; + const auto writes = [](std::initializer_list shaders) { return AgcDriver::Graphics::ShadersWriteMemory(std::vector(shaders)); }; + Require(!writes({{Stage::Vertex, &reads, 0}, {Stage::Fragment, &reads, 0}}), "shaders that only read memory were reported as writing it"); + Require(writes({{Stage::Vertex, &reads, 0}, {Stage::Fragment, &stores, 0}}), "a pixel shader that stores to a buffer was not reported"); + Require(writes({{Stage::Vertex, &stores, 0}, {Stage::Fragment, &reads, 0}}), "a vertex shader that stores to a buffer was not reported"); + Require(writes({{Stage::Geometry, &stores, 0}, {Stage::Fragment, &reads, 0}}), "a geometry shader that stores to a buffer was not reported"); +} + void ReversedComponentOrderTests() { + { + auto queue = makeState(); + queue.context[0x31c] = (queue.context[0x31c] & ~((0x1fu << 2u) | (7u << 8u) | (3u << 11u))) | (3u << 2u) | (1u << 11u); + const auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.colors.size() == 1 && state.colors[0].format == VK_FORMAT_R8G8_UNORM && state.colors[0].componentMapping == 0xecu, "an 8_8 target with SWAP_ALT did not store the x and w exports in its two channels"); + queue.context[0x1e0] = 0x40010001u; + for (const auto offset : {0x105u, 0x106u, 0x107u, 0x108u}) queue.context[offset] = 0; + const auto blended = AgcDriver::Graphics::DecodeState(queue); + Require(blended.blends.size() == 1 && blended.blends[0].blendEnable, "blending into an 8_8 SWAP_ALT target with one equation for color and alpha was rejected"); + Require(!AgcDriver::Graphics::SplitsAlternateAlpha(blended), "one blend equation for color and alpha split an 8_8 SWAP_ALT draw"); + queue.context[0x1e0] = 0x60000001u; + Require(AgcDriver::Graphics::SplitsAlternateAlpha(AgcDriver::Graphics::DecodeState(queue)), "a separate alpha blend into an 8_8 SWAP_ALT target did not split its second channel"); + queue.context[0x1e0] = (1u << 30u) | 13u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "constant color factor"); + queue.context[0x1e0] = (1u << 30u) | (1u << 29u) | 1u | (1u << 16u) | (14u << 24u); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "constant color factor"); + queue.context[0x1e0] = (1u << 30u) | (2u << 5u) | 13u; + Require(AgcDriver::Graphics::DecodeState(queue).blends[0].colorBlendOp == VK_BLEND_OP_MIN, "a constant color factor of a MIN blend into an 8_8 SWAP_ALT target was rejected"); + } for (const auto& [swap, mapping] : {std::pair{2u, 0x1bu}, std::pair{3u, 0x93u}}) { auto queue = makeState(); queue.context[0x31c] = (queue.context[0x31c] & ~(3u << 11u)) | (swap << 11u); @@ -652,15 +1805,262 @@ 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); Require(wide.colors.size() == 1 && wide.colors[0].format == VK_FORMAT_R16G16B16A16_SFLOAT && wide.colors[0].componentMapping == mapping && wide.blends[0].colorWriteMask == 0xfu, "a 16_16_16_16 float target with a reversed component order did not map exports onto RGBA16"); } +} + +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] & ~((0x1fu << 2u) | (7u << 8u) | (3u << 11u))) | (12u << 2u) | (7u << 8u) | (1u << 11u); - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "component swap 1"); + 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); + const auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.colors.size() == 1 && state.colors[0].format == VK_FORMAT_A1R5G5B5_UNORM_PACK16 && state.colors[0].elementBytes == 2 && state.colors[0].componentMapping == 0xc6u, "a 1_5_5_5 target did not render through A1R5G5B5 with X and Z exchanged"); + Require(AgcDriver::Graphics::ExportMappings(state)[0] == 0xc6u && state.blends[0].colorWriteMask == 0xfu, "a 1_5_5_5 target did not write all four channels through its export mapping"); + queue.context[0x8e] = 0x1u; + Require(AgcDriver::Graphics::DecodeState(queue).blends[0].colorWriteMask == VK_COLOR_COMPONENT_B_BIT, "a 1_5_5_5 target writing only X did not write the A1R5G5B5 channel in its low bits"); + queue.context[0x8e] = 0xfu; + 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 1_5_5_5 target was not decoded"); + queue.context[0x1e0] = 0; + for (const auto& [number, swap] : {std::pair{1u, 0u}, std::pair{4u, 0u}, std::pair{0u, 1u}}) { + queue.context[0x31c] = (queue.context[0x31c] & ~((7u << 8u) | (3u << 11u))) | (number << 8u) | (swap << 11u); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "unsupported color format 17 number type " + std::to_string(number) + " component swap " + std::to_string(swap)); + } } void CompactedExportTests() { @@ -708,22 +2108,130 @@ void DepthStencilTests() { queue.context[0x011] = 0x20000181; for (const auto offset : {0x012u, 0x014u}) queue.context[offset] = 0x100; for (const auto offset : {0x013u, 0x015u}) queue.context[offset] = 0x200; + queue.context[0x005] = 0x300; + queue.context[0x01e] = 0x1; + { + AgcDriver::QueueState unwritten; + unwritten.shader = queue.shader; + unwritten.userConfig = queue.userConfig; + queue.context[0x200] = 0x00700791; + for (int reset = 0; reset < 2; ++reset) { + queue.context.ForEach([&](std::uint32_t offset, std::uint32_t value) { unwritten.context[offset] = value; }); + const auto state = AgcDriver::Graphics::DecodeState(unwritten); + Require(state.stencilTest && state.depth, "unwritten stencil registers rejected a stencil draw"); + for (const auto face : {state.stencilFront, state.stencilBack}) { + Require(face.failOp == VK_STENCIL_OP_KEEP && face.passOp == VK_STENCIL_OP_KEEP && face.depthFailOp == VK_STENCIL_OP_KEEP && face.compareOp == VK_COMPARE_OP_ALWAYS, "unwritten stencil operations changed"); + Require(face.compareMask == 0 && face.writeMask == 0 && face.reference == 0, "unwritten stencil reference or masks changed"); + } + Require(AgcDriver::Graphics::DrawRejection(unwritten, false).empty(), "precheck rejected unwritten stencil registers"); + unwritten.context[0x10b] = 0x00070070; + unwritten.context[0x10c] = unwritten.context[0x10d] = 0x01ffff09; + const auto explicitState = AgcDriver::Graphics::DecodeState(unwritten); + for (const auto face : {explicitState.stencilFront, explicitState.stencilBack}) Require(face.passOp == VK_STENCIL_OP_INVERT && face.compareMask == 0xff && face.writeMask == 0xff && face.reference == 9, "explicit stencil state was replaced by defaults"); + unwritten.ClearContext(); + for (const auto offset : {0x10bu, 0x10cu, 0x10du}) Require(unwritten.context.at(offset) == 0, "context clear did not reset stencil state"); + } + } queue.context[0x10b] = 0x00050050; queue.context[0x10c] = 0x01ffff01; queue.context[0x10d] = 0x01000001; 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, "depth surface decode changed"); + 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); + Require(stencilTiled.depth && stencilTiled.depth->htileStencil, "HTILE holds the stencil state without TILE_STENCIL_DISABLE"); + queue.context[0x011] |= 1u << 29u; + queue.context[0x010] &= ~(1u << 29u); + const auto untiled = AgcDriver::Graphics::DecodeState(queue); + Require(untiled.depth && untiled.depth->htileAddress == 0, "HTILE must be ignored without TILE_SURFACE_ENABLE"); + queue.context[0x010] |= 1u << 29u; + using AgcDriver::Graphics::HtileFillClears; + using AgcDriver::Graphics::HtileFillCovers; + constexpr VkImageAspectFlags both = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT; + Require(HtileFillClears(0x0003fff0u, false) == VK_IMAGE_ASPECT_DEPTH_BIT, "a depth-only HTILE fill with ZMask 0 clears the depth"); + Require(HtileFillClears(0x000000f0u, true) == both, "a depth and stencil fast clear (ZMask 0, SMem 0) clears both aspects"); + Require(HtileFillClears(0x000003f0u, true) == VK_IMAGE_ASPECT_DEPTH_BIT, "SMem 3 leaves the stencil uncleared"); + Require(HtileFillClears(0xfffff0ffu, true) == VK_IMAGE_ASPECT_STENCIL_BIT, "ZMask 0xf with SMem 0 clears only the stencil"); + Require(HtileFillClears(0xffffffffu, true) == 0 && HtileFillClears(0xffffffffu, false) == 0, "an expanded fill clears nothing"); + const VkExtent2D extent{16, 8}; + Require(HtileFillCovers(0x10000, extent, 0x10000, 0x8000) && HtileFillCovers(0x10000, extent, 0xfff0, 0x8010), "a fill over the whole 32 KiB metadata block covers the surface"); + Require(!HtileFillCovers(0x10000, extent, 0x10000, 0x7ffc) && !HtileFillCovers(0x10000, extent, 0x10004, 0x8000) && !HtileFillCovers(0, extent, 0, 0x10000), "a partial fill, one starting past the HTILE base, or no HTILE covers nothing"); + // Pipe-aligned SW_64KB_Z_X places a 16x16 surface's HTILE words across its metadata block (a valid word at offset + // 260, AMD PAL), so a fill of the words a linear layout would hold covers nothing. + Require(!HtileFillCovers(0x10000, {16, 16}, 0x10000, 16) && !HtileFillCovers(0x10000, {16, 16}, 0x10000, 264), "a fill of the linear HTILE size of a 16x16 surface misses its pipe-aligned words"); + Require(AgcDriver::Graphics::HtileMetadataBytes({1024, 512}) == 0x8000u && AgcDriver::Graphics::HtileMetadataBytes({1025, 512}) == 0x10000u && AgcDriver::Graphics::HtileMetadataBytes({3840, 2160}) == 20u * 0x8000u, "the HTILE metadata is 32 KiB per 1024x512 block"); + } Require(state.renderExtent.width == 4 && state.renderExtent.height == 2, "render extent ignores the depth surface"); Require(!state.depthTest && !state.depthWrite && state.stencilTest, "depth/stencil enables changed"); + queue.context.erase(0x005); + Require(AgcDriver::Graphics::DecodeState(queue).depth->htileAddress == 0, "an HTILE surface without DB_HTILE_DATA_BASE decoded an HTILE address"); + queue.context[0x005] = 0x300; + queue.context[0x01e] = 0x01; + log.clear(); + { + 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"); + queue.context[0x010] |= 0x20000000u; const auto& front = state.stencilFront; Require(front.compareOp == VK_COMPARE_OP_ALWAYS && front.passOp == VK_STENCIL_OP_INCREMENT_AND_CLAMP && front.failOp == VK_STENCIL_OP_KEEP && front.reference == 1 && front.writeMask == 0xff, "stencil mask pass changed"); Require(std::memcmp(&state.stencilBack, &front, sizeof(front)) == 0, "back faces without BACKFACE_ENABLE must use the front state"); @@ -749,7 +2257,15 @@ void DepthStencilTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "stencil add/subtract"); queue.context[0x10b] = 0; queue.context[0x000] = 1; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "DB_RENDER_CONTROL"); + queue.context[0x00b] = std::bit_cast(0.25f); + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.depth && state.depthTest && state.depthWrite && state.depthCompare == VK_COMPARE_OP_ALWAYS && !state.depthBoundsTest && !state.depthBias && state.viewport.minDepth == 0.25f && state.viewport.maxDepth == 0.25f, "a DEPTH_CLEAR_ENABLE draw does not store DB_DEPTH_CLEAR everywhere it covers"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "precheck rejected a depth clear"); + queue.context[0x002] = 0x01000000; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "writable depth plane"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).find("writable depth plane") != std::string::npos, "precheck accepted read-only depth clear"); + queue.context[0x002] = 0; + queue.context[0x00b] = 0; queue.context[0x000] = 0x22; state = AgcDriver::Graphics::DecodeState(queue); const auto clears = [](const VkStencilOpState& face) { @@ -790,7 +2306,15 @@ void DepthStencilTests() { state = AgcDriver::Graphics::DecodeState(queue); Require(!state.depthWrite && clears(state.stencilFront), "read-only depth prevented stencil clear"); queue.context[0x002] = 0; - for (const auto control : {1u, 4u, 8u}) { + 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 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"); } @@ -799,31 +2323,180 @@ void DepthStencilTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "writable stencil plane"); Require(AgcDriver::Graphics::DrawRejection(queue, false).find("writable stencil plane") != std::string::npos, "precheck accepted clear without a stencil plane"); queue.context[0x010] = 0; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "writable stencil plane"); + state = AgcDriver::Graphics::DecodeState(queue); + Require(!state.depth && !state.depthTest && !state.depthWrite && !state.stencilTest && state.colors.size() == 2, "clear flags without depth/stencil planes changed color-only rendering"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "precheck rejected clear flags without depth/stencil planes"); + queue = makeState(); + queue.context[0x010] = 0x80000180; + queue.context[0x011] = 0x20000180; + queue.context[0x200] = 0x776; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + const auto colorOnly = AgcDriver::Graphics::DecodeState(queue); + for (const auto control : {1u, 2u, 3u}) { + queue.context[0x000] = control; + state = AgcDriver::Graphics::DecodeState(queue); + Require(!state.depth && !state.depthTest && !state.depthWrite && !state.stencilTest, "stale clear flags enabled absent depth/stencil planes"); + Require(state.color.address == colorOnly.color.address && state.blends[0].colorWriteMask == colorOnly.blends[0].colorWriteMask && state.viewport.minDepth == colorOnly.viewport.minDepth && state.viewport.maxDepth == colorOnly.viewport.maxDepth, "stale clear flags changed color writes or viewport depth"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "precheck rejected a color-only draw with stale clear flags: " + AgcDriver::Graphics::DrawRejection(queue, false)); + } queue = makeState(); queue.context[0x31b] = 1u << 26u; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "mip exceeds"); + queue.context[0x31b] = 0; + const auto plain = AgcDriver::Graphics::DecodeState(queue); + queue.context[0x31b] = 35u << 13u; + const auto plainRange = AgcDriver::Graphics::DecodeState(queue); + Require(plainRange.color.address == plain.color.address && plainRange.color.bytes == plain.color.bytes && plainRange.color.arrayLastSlice == plain.color.arrayLastSlice, "a color view of slices 0 to 35 of a target with no array slices did not render into its only slice"); + queue.context[0x31b] = 1u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "ends before its first slice"); queue.context[0x31b] = 1u << 13u; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "several array slices"); + const auto firstSlice = AgcDriver::Graphics::DecodeState(queue); + Require(firstSlice.color.address == reinterpret_cast(colorMemory.data()), "a color view of several slices did not retain its first slice"); + queue.context[0x207] = 1u << 18u; + 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); queue.context[0x390] = static_cast(sliced >> 40u); queue.context[0x31b] = 1u | (1u << 13u); const auto slice = AgcDriver::Graphics::DecodeState(queue); Require(slice.color.address == sliced + 1024u && slice.color.bytes == 1024u, "a color view of one slice did not move the target by one slice"); + queue.context[0x3b8] = 0x09000001; + const auto arraySlice = AgcDriver::Graphics::DecodeState(queue); + Require(arraySlice.color.address == sliced + 1024u && arraySlice.color.bytes == 1024u && arraySlice.color.depth == 1u, "a color view of one slice of a 2D array target did not move the target by one slice"); + queue.context[0x31b] = 1u << 13u; + const auto range = AgcDriver::Graphics::DecodeState(queue); + Require(range.color.address == sliced && range.color.bytes == 1024u, "a color view of slices 0 to 1 of a 2D array target did not render into its first slice"); + queue.context[0x207] = 1u << 18u; + 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"); + queue.context[0x31b] = 1u << 14u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "beyond the array surface"); queue.context[0x3b8] = 0x0a000003; queue.context[0x31b] = 2u | (2u << 13u); const auto volume = AgcDriver::Graphics::DecodeState(queue); Require(volume.color.address == sliced && volume.color.depth == 4u && volume.color.depthSlice == 2u, "a color view of one 3D depth slice did not keep the surface address and select the slice"); + Require(volume.color.layers == 1 && volume.renderLayers == 1 && volume.layerExports == 0, "a color view of one 3D depth slice became layered"); + { + 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); }, "beyond the 3D surface"); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "outside the 3D surface"); + queue.context[0x31b] = 2u | (1u << 13u); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "ends before its first slice"); + queue.context[0x31b] = 1u | (3u << 13u); + queue.context[0x207] = (1u << 18u) | (1u << 21u) | (1u << 24u); + const auto layered = AgcDriver::Graphics::DecodeState(queue); + Require(layered.color.depthSlice == 1u && layered.color.layers == 3u && layered.renderLayers == 3u && layered.layerExports == (1u << 18u), "a color view of three 3D depth slices did not render three layers from the vertex layer export"); + { + auto pointSize = queue; + pointSize.context[0x1b3] = pointSize.context[0x1b4] = 2; + Require(AgcDriver::Graphics::DrawRejection(pointSize, false).empty(), "layered volume test state failed the draw precheck"); + pointSize.context[0x207] |= 1u << 16u; + const auto rejection = AgcDriver::Graphics::DrawRejection(pointSize, false); + Require(rejection.empty(), "inactive point size rejected a layered volume draw: " + rejection); + const auto decoded = AgcDriver::Graphics::DecodeState(pointSize); + Require(decoded.color.depthSlice == layered.color.depthSlice && decoded.color.layers == layered.color.layers && decoded.renderLayers == layered.renderLayers && decoded.layerExports == layered.layerExports, "inactive point size changed layered volume routing"); + Require(decoded.paClVsOutCntl == pointSize.context[0x207], "inactive point size discarded the layered export state"); + } + queue.context[0x31b] = 1u | (8u << 13u); + Require(AgcDriver::Graphics::DecodeState(queue).renderLayers == 3u, "a color view past the 3D surface was not clamped to its last slice"); + queue.context[0x207] = 0; queue.context[0x31b] = 0; queue.context[0x31c] |= 0x10000000; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "DCC 3D color targets"); + const auto volumeKeys = reinterpret_cast(volumeKeyMemory.data()); + queue.context[0x325] = static_cast(volumeKeys >> 8u); + queue.context[0x3a8] = static_cast(volumeKeys >> 40u); + Require(AgcDriver::Graphics::DecodeState(queue).color.dccAddress == volumeKeys, "a DCC 3D color target lost its keys"); +} + +void OneDimensionalColorTests() { + auto queue = makeState(); + queue.context[0x3b8] = 0x08000000; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "taller than one row"); + queue.context[0x3b0] = 63u << 14u; + const auto line = AgcDriver::Graphics::DecodeState(queue); + 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 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}; + const auto address = [&](std::span code, std::array userData, std::array threads) { + return AgcDriver::Graphics::HtileDepthClearAddress(code, userData, threads); + }; + Require(address(kernel, {0, 0, 0x0dc50000u, 0x40u, 0u, 0x00000ff0u}, wave) == 0x400dc50000ull, "RADV's HTILE depth fast clear (ZMASK to 0) was not recognized"); + Require(address(kernel, {0, 0, 0x0dc50000u, 0x40u, 0xfffff00fu, 0x00000ff0u}, wave) == 0, "an HTILE initialization to expanded (ZMASK 0xf) was taken for a clear"); + Require(address(kernel, {0, 0, 0x0dc50000u, 0x40u, 0x000003f0u, 0xfffffc0fu}, wave) == 0, "a stencil-only HTILE update that keeps ZMASK was taken for a depth clear"); + Require(address(kernel, {0, 0, 0x0dc50000u, 0x40u, 0u, 0x00000ff0u}, {32, 1, 1}) == 0, "the HTILE kernel matched with another group size"); + auto other = kernel; + other[6] ^= 1u; + Require(address(other, {0, 0, 0x0dc50000u, 0x40u, 0u, 0x00000ff0u}, wave) == 0, "a different kernel was taken for RADV's HTILE update"); + Require(address(std::span(kernel).first(16), {0, 0, 0x0dc50000u, 0x40u, 0u, 0x00000ff0u}, wave) == 0, "a truncated kernel was taken for RADV's HTILE update"); +} + +void memoryValueFillTests() { + static constexpr std::array kernel{0xd7460000u, 0x04010c08u, 0xf4201a82u, 0xfa000000u, 0xbf8cc07fu, 0x7e02026au, 0xe0102000u, 0x80000100u, 0xbf810000u}; + const std::array wave{64, 1, 1}; + const std::array userData{0x50b00000u, 0x00040020u, 0x20000u, 0x00014204u, 0x34be64e0u, 0x00100020u, 1u, 0x0004dfacu}; + const auto match = [&](std::span code, std::array words, std::array threads, std::array groups) { + return AgcDriver::Graphics::MatchMemoryValueFill(code, words, threads, groups); + }; + const auto fill = match(kernel, userData, wave, {2048, 1, 1}); + Require(fill.has_value() && fill->address == 0x2050b00000ull && fill->bytes == 0x80000u && fill->valueAddress == 0x2034be64e0ull, "Kena's HTILE fill from a value in memory was not recognized"); + const auto partial = match(kernel, userData, wave, {1024, 1, 1}); + Require(partial.has_value() && partial->bytes == 0x40000u, "a fill of half the groups did not cover half the buffer"); + auto records = userData; + records[2] = 0x100u; + const auto clipped = match(kernel, records, wave, {2048, 1, 1}); + Require(clipped.has_value() && clipped->bytes == 0x400u, "a fill past NUM_RECORDS was not clipped to it"); + Require(!match(kernel, userData, {32, 1, 1}, {2048, 1, 1}), "the memory value fill matched with another group size"); + Require(!match(kernel, userData, wave, {2048, 2, 1}), "a two-dimensional dispatch was taken for a linear fill"); + auto strided = userData; + strided[1] = 0x00080020u; + Require(!match(kernel, strided, wave, {2048, 1, 1}), "a fill with an 8-byte stride was taken for a dword fill"); + auto indexed = userData; + indexed[3] |= 1u << 23u; + Require(!match(kernel, indexed, wave, {2048, 1, 1}), "a buffer with ADD_TID_ENABLE was taken for a plain fill"); + auto nowhere = userData; + nowhere[4] = 0; + nowhere[5] = 0; + Require(!match(kernel, nowhere, wave, {2048, 1, 1}), "a fill from a null value pointer was recognized"); + auto empty = userData; + empty[6] = 0; + Require(!match(kernel, empty, wave, {2048, 1, 1}), "a fill whose value buffer has no records was taken for a fill from memory"); + auto raw = userData; + raw[5] = 0x20u; + raw[6] = 4u; + Require(match(kernel, raw, wave, {2048, 1, 1}).has_value(), "a fill from a 4-byte raw value buffer was not recognized"); + raw[6] = 3u; + Require(!match(kernel, raw, wave, {2048, 1, 1}), "a fill from a value buffer shorter than a dword was taken for a fill from memory"); + for (const std::uint32_t format : {20u, 21u, 22u}) { + auto typed = userData; + typed[3] = (typed[3] & ~(0x7fu << 12u)) | (format << 12u); + Require(match(kernel, typed, wave, {2048, 1, 1}).has_value(), "a fill through a 32-bit buffer format was not recognized"); + } + for (const std::uint32_t format : {0x27u, 0x0du, 0x4bu}) { + auto converted = userData; + converted[3] = (converted[3] & ~(0x7fu << 12u)) | (format << 12u); + Require(!match(kernel, converted, wave, {2048, 1, 1}), "a fill through a converting buffer format was taken for a raw dword fill"); + } + auto other = kernel; + other[6] ^= 1u; + Require(!match(other, userData, wave, {2048, 1, 1}), "a different kernel was taken for the memory value fill"); + Require(!match(std::span(kernel).first(8), userData, wave, {2048, 1, 1}), "a truncated kernel was taken for the memory value fill"); } void depthMaintenanceTests() { - for (const auto mode : {0x4u, 0x8u, 0x10u, 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; @@ -837,14 +2510,54 @@ 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"); } + for (const auto clear : {0u, 1u, 2u, 3u}) { + auto queue = makeState(); + queue.context[0x000] = 0x10u | clear; + queue.context[0x200] = 0; + queue.context[0x8e] = 0; + queue.context[0x8f] = 0; + Require(AgcDriver::Graphics::DepthMaintenanceRejection(queue).empty(), "a resummarize draw without depth, stencil or color work was rejected"); + queue.context[0x200] = 2; + Require(AgcDriver::Graphics::DepthMaintenanceRejection(queue).find("DB_RENDER_CONTROL") != std::string::npos, "a resummarize draw with a depth test passed"); + queue.context[0x200] = 0; + queue.context[0x8e] = 0xf; + queue.context[0x8f] = 0xf; + Require(AgcDriver::Graphics::DepthMaintenanceRejection(queue).find("DB_RENDER_CONTROL") != std::string::npos, "a resummarize draw with color writes passed"); + } auto absent = makeState(); absent.context.erase(0x000); Require(AgcDriver::Graphics::DepthMaintenanceRejection(absent).empty(), "an absent depth control produced a maintenance verdict"); + auto readOnly = makeState(); + readOnly.context[0x000] = 0x10; + readOnly.context[0x200] = 0x007007f3; + readOnly.context[0x010] = 0xa0000183; + readOnly.context[0x011] = 0x20000180; + readOnly.context[0x8e] = 0xffffffff; + readOnly.context[0x8f] = 0; + readOnly.shader.erase(0x8); + readOnly.shader.erase(0x9); + Require(AgcDriver::Graphics::DepthMaintenanceRejection(readOnly).empty(), "a resummarize draw with pass-through read-only depth and no stencil plane was rejected"); + for (const auto [offset, value] : {std::pair{0x200u, 0x007007f7u}, {0x200u, 0x007007fbu}, {0x200u, 0x007007e3u}, {0x011u, 0x20000181u}, {0x8fu, 0xfu}, {0x000u, 0x11u}, {0x010u, 0xa0000182u}}) { + auto changed = readOnly; + changed.context[offset] = value; + Require(!AgcDriver::Graphics::DepthMaintenanceRejection(changed).empty(), "a resummarize draw with surface work or unsupported depth passed"); + } + auto pixel = readOnly; + pixel.shader[0x8] = 1; + pixel.shader[0x9] = 0; + pixel.context[0x1c4] = 0; + pixel.context[0x203] = 0x40; + Require(!AgcDriver::Graphics::DepthMaintenanceRejection(pixel).empty(), "a resummarize draw with a running pixel stage passed"); + for (const auto offset : {0x200u, 0x010u, 0x011u, 0x8eu, 0x8fu}) { + auto missing = readOnly; + missing.context.erase(offset); + Require(!AgcDriver::Graphics::DepthMaintenanceRejection(missing).empty(), "a resummarize draw without a required register passed"); + } } // SPI_SHADER_Z_FORMAT (0x1c4) and the export enables of DB_SHADER_CONTROL (0x203): Z export needs a @@ -896,6 +2609,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); @@ -906,11 +2641,39 @@ void DepthBoundsBiasTests() { for (const auto offset : {0x2e1u, 0x2e3u}) queue.context[offset] = bits(4.0f); state = AgcDriver::Graphics::DecodeState(queue); Require(state.depthBias && state.depthBiasSlope == 2.0f && state.depthBiasConstant == 4.0f && state.depthBiasClamp == 0.5f, "depth bias decode changed"); + Require(!state.depthBiasPerFace, "equal front and back depth bias must draw both faces in one pass"); + queue.context[0x2e2] = bits(-64.0f); queue.context[0x2e3] = bits(8.0f); - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "differing between front and back"); + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.depthBias && state.depthBiasPerFace && state.cullMode == VK_CULL_MODE_NONE, "differing front and back depth bias must draw each face with its own bias"); + Require(state.depthBiasSlope == 2.0f && state.depthBiasConstant == 4.0f && state.backDepthBiasSlope == -4.0f && state.backDepthBiasConstant == 8.0f && state.depthBiasClamp == 0.5f, "per-face depth bias decode changed"); + queue.context[0x205] = 0x00000a48u; + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.depthBias && state.depthBiasPerFace && state.depthBiasConstant == 4.0f && state.backDepthBiasSlope == 0.0f && state.backDepthBiasConstant == 0.0f, "back faces without depth bias must draw unbiased"); + queue.context[0x205] = 0x00001248u; + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.depthBias && state.depthBiasPerFace && state.depthBiasSlope == 0.0f && state.depthBiasConstant == 0.0f && state.backDepthBiasConstant == 8.0f, "front faces without depth bias must draw unbiased"); queue.context[0x205] = 0x00001a4au; state = AgcDriver::Graphics::DecodeState(queue); - Require(state.depthBias && state.depthBiasConstant == 4.0f, "culled back faces must not constrain the front depth bias"); + Require(state.depthBias && !state.depthBiasPerFace && state.depthBiasConstant == 4.0f, "culled back faces must not constrain the front depth bias"); + queue.context[0x205] = 0x00001a49u; + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.depthBias && !state.depthBiasPerFace && state.depthBiasSlope == -4.0f && state.depthBiasConstant == 8.0f, "culled front faces must draw with the back depth bias"); + queue.context[0x205] = 0x00003a46u; + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.depthBias && state.depthBiasConstant == 4.0f && state.cullMode == VK_CULL_MODE_BACK_BIT, "a triangle draw with the point and line offset enable lost its depth bias"); + queue.context[0x205] = 0x00013a46u; + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.depthBias && state.depthBiasConstant == 4.0f && state.cullMode == VK_CULL_MODE_BACK_BIT, "the window offset enable bit changed the raster decode while the window offset is zero"); + queue.context[0x80] = 1; + 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"); + queue.userConfig[0x242] = 3; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "polygon mode, depth bias or nonstandard rasterization"); + queue.userConfig[0x242] = 4; + queue.context[0x205] = 0x00001a4au; queue.context[0x2de] = 0x1f0u; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "units other than the depth format"); queue.context[0x2de] = 0x1e9u; @@ -954,8 +2717,34 @@ void metadataPassTests() { queue.context[0x202] = 0xcc0060; queue.context[0x200] = 2; expectFailure([&] { DecodeColorMetadataPass(queue); }, "depth or stencil work"); + queue.context[0x10] = 0x80000180; + queue.context[0x11] = 0x20000180; + queue.context[0x200] = 0x700763; + queue.context[0x0] = 0x20; + Require(DecodeColorMetadataPass(queue).has_value(), "depth and stencil state without depth or stencil planes refused the pass"); + queue.context[0x10] = 0x80000183; + expectFailure([&] { DecodeColorMetadataPass(queue); }, "depth or stencil work"); + queue.context[0x10] = 0x80000180; + queue.context[0x11] = 0x20000181; + expectFailure([&] { DecodeColorMetadataPass(queue); }, "depth or stencil work"); + queue.context[0x11] = 0x20000180; + queue.context.erase(0x200); + queue.context.erase(0x0); + Require(DecodeColorMetadataPass(queue).has_value(), "unwritten depth and render control refused a pass without depth or stencil planes"); + queue.context.erase(0x10); + queue.context.erase(0x11); + queue.context[0x0] = 0; queue.context[0x200] = 0x70; Require(DecodeColorMetadataPass(queue).has_value(), "an always-pass depth function without a test refused the pass"); + auto bound = queue; + bound.context[0x010] = 0x3; + bound.context[0x011] = 0x1; + bound.context[0x200] = 0x72; + Require(DecodeColorMetadataPass(bound).has_value(), "an always-pass depth test without writes refused the pass"); + bound.context[0x200] = 0x76; + expectFailure([&] { DecodeColorMetadataPass(bound); }, "depth or stencil work"); + bound.context[0x200] = 0x7a; + expectFailure([&] { DecodeColorMetadataPass(bound); }, "depth or stencil work"); queue.context[0x91] = 0x40020; expectFailure([&] { DecodeColorMetadataPass(queue); }, "over part of a color target"); queue.context[0x91] = 0x40040; @@ -1070,13 +2859,36 @@ void metadataPassTests() { for (std::size_t offset = 0; offset < tiledBytes; offset += 4) filled = filled && std::memcmp(tiledTexels + offset, &texel, 4) == 0; Require(filled && stored == expected && std::all_of(tiledKeys, tiledKeys + expected, [](std::uint8_t value) { return value == 0xff; }), std::string("a ") + code + " fast clear eliminate of a 128x128 SW_64KB_R_X target with " + alignment + " DCC stored uncompressed keys over " + std::to_string(stored) + " of its " + std::to_string(keyCount) + " DCC key bytes, expected " + std::to_string(expected)); } + std::vector original(tiledBytes / 4); + for (std::size_t index = 0; index < original.size(); ++index) original[index] = 0x01000000u + static_cast(index); + const auto resetSingle = [&] { + std::memcpy(tiledTexels, original.data(), tiledBytes); + std::memset(tiledKeys, 0x10, keyCount); + }; + resetSingle(); + expectFailure([&] { AgcDriver::Graphics::RunColorMetadataPass(context, *tiledPass); }, "fast-clear eliminate over comp-to-single"); + Require(std::memcmp(tiledTexels, original.data(), tiledBytes) == 0 && std::all_of(tiledKeys, tiledKeys + keyCount, [](std::uint8_t value) { return value == 0x10; }), "a refused fast-clear eliminate over comp-to-single keys changed the " + alignment + " target"); + resetSingle(); + AgcDriver::Graphics::RunColorMetadataPass(context, {AgcDriver::Graphics::ColorMetadataPass::Mode::DccDecompress, tiledPass->targets}); + const AgcDriver::Graphics::ColorTargetLayout layout(tiledSide, tiledSide, AgcDriver::Graphics::ColorTileMode::RenderTarget, 4); + bool expanded = true; + for (std::uint32_t y = 0; y < tiledSide && expanded; ++y) { + for (std::uint32_t x = 0; x < tiledSide && expanded; ++x) { + std::uint32_t texel = 0; + std::memcpy(&texel, tiledTexels + layout.Offset(x, y), 4); + expanded = texel == original[layout.Offset(x & ~7u, y & ~7u) / 4]; + } + } + const auto stored = static_cast(std::count(tiledKeys, tiledKeys + keyCount, std::uint8_t{0xff})); + Require(expanded && stored == expected, "a DCC decompress of comp-to-single keys over a 128x128 SW_64KB_R_X target with " + alignment + " DCC did not give every 8x8 block its first texel (" + std::to_string(stored) + " of " + std::to_string(keyCount) + " keys uncompressed, expected " + std::to_string(expected) + ")"); } } -alignas(256) std::array cmaskMemory{}; +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()); @@ -1084,18 +2896,89 @@ void cmaskTests() { queue.context[0x398] = static_cast(cmaskAddress >> 40u); queue.context[0x323] = 0x11223344; queue.context[0x324] = 0; + queue.context[0x3b8] = 0x0dc00000u; 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; + { + 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"); + constexpr std::size_t blockBytes = 65536u; + static std::vector mipStorage(16u * blockBytes); + const auto mipBase = (reinterpret_cast(mipStorage.data()) + blockBytes - 1u) / blockBytes * blockBytes; + 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[0x3b0] |= 1u << 28u; - expectFailure([&] { DecodeColorBuffer(mipmapped.context, 0); }, "mipmapped, 3D or array"); + 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"); auto unaddressed = queue; unaddressed.context[0x31f] = 0; unaddressed.context[0x398] = 0; expectFailure([&] { DecodeColorBuffer(unaddressed.context, 0); }, "without a CMASK address"); + auto unaligned = queue; + unaligned.context[0x3b8] = 0x09c00000u; + expectFailure([&] { DecodeColorBuffer(unaligned.context, 0); }, "pipe-aligned SW_64KB_Z_X"); + unaligned.context[0x3b8] = 0x0d000000u; + expectFailure([&] { DecodeColorBuffer(unaligned.context, 0); }, "pipe-aligned SW_64KB_Z_X"); + unaligned = queue; + unaligned.context[0x31f] += 1u; + expectFailure([&] { DecodeColorBuffer(unaligned.context, 0); }, "misaligned"); + using AgcDriver::Graphics::CmaskLayout; + Require(CmaskBytes(61, 13) == CmaskLayout(61, 13).Bytes() && CmaskBytes(3840, 2160) == CmaskLayout(3840, 2160).Bytes(), "the CMASK size and layout disagree"); queue.context[0x0] = 0; queue.context[0x202] = 0xcc0020; @@ -1118,10 +3001,43 @@ void cmaskTests() { std::memset(colorMemory.data(), 0x5a, colorMemory.size()); AgcDriver::Graphics::RunColorMetadataPass(context, *pass); Require(texels(0x5a5a5a5au) && cmaskIs(0xff), "a pass over an expanded CMASK changed the texels"); + const CmaskLayout layout(64, 4); + const auto setTile = [&](std::uint32_t tileX, std::uint8_t code) { + const auto nibble = layout.Nibble(tileX, 0); + auto& entry = cmaskMemory[nibble / 2u]; + const auto shift = (nibble % 2u) * 4u; + entry = static_cast((entry & ~(0xfu << shift)) | (code << shift)); + }; + const auto tiles = [&](const auto& cleared) { + for (std::size_t offset = 0; offset < colorMemory.size(); offset += 4) { + std::uint32_t texel = 0; + std::memcpy(&texel, colorMemory.data() + offset, 4); + if (texel != (cleared((offset / 4u) % 64u / 8u) ? 0x11223344u : 0x5a5a5a5au)) return false; + } + return true; + }; + std::memset(colorMemory.data(), 0x5a, colorMemory.size()); cmaskMemory.fill(0); cmaskMemory[cmaskMemory.size() - 1] = 0xff; - expectFailure([&] { AgcDriver::Graphics::RunColorMetadataPass(context, *pass); }, "not all fast-cleared or all expanded"); - Require(texels(0x5a5a5a5au), "a refused pass changed the texels"); + AgcDriver::Graphics::RunColorMetadataPass(context, *pass); + Require(texels(0x11223344) && cmaskIs(0xff), "CMASK nibbles past the target's tiles changed what its tiles hold"); + std::memset(colorMemory.data(), 0x5a, colorMemory.size()); + cmaskMemory.fill(0xff); + setTile(1, 0); + setTile(7, 0); + AgcDriver::Graphics::RunColorMetadataPass(context, *pass); + Require(tiles([](std::uint32_t tile) { return tile == 1 || tile == 7; }) && cmaskIs(0xff), "a partly fast-cleared CMASK did not clear exactly its cleared tiles"); + std::memset(colorMemory.data(), 0x5a, colorMemory.size()); + cmaskMemory.fill(0xff); + setTile(4, 0); + AgcDriver::Graphics::RunColorMetadataPass(context, {AgcDriver::Graphics::ColorMetadataPass::Mode::DccDecompress, pass->targets}); + Require(tiles([](std::uint32_t tile) { return tile == 4; }) && cmaskIs(0xff), "a DCC decompress pass did not eliminate the CMASK fast clear"); + std::memset(colorMemory.data(), 0x5a, colorMemory.size()); + cmaskMemory.fill(0xff); + setTile(2, 0); + setTile(3, 3); + AgcDriver::Graphics::RunColorMetadataPass(context, *pass); + Require(tiles([](std::uint32_t tile) { return tile == 2; }) && cmaskIs(0xff), "a tile with a CMASK code other than 0 or 15 did not keep its texels"); queue.context[0x31c] |= 0x10000000; const auto keysAddress = reinterpret_cast(dccKeys.data()); @@ -1136,10 +3052,35 @@ void cmaskTests() { Require(texels(0x11223344) && cmaskIs(0xff) && std::ranges::all_of(dccKeys, [](std::uint8_t key) { return key == 0xff; }), "a DCC fast clear under an expanded CMASK was not eliminated through its keys"); std::memset(colorMemory.data(), 0x5a, colorMemory.size()); cmaskMemory.fill(0); - expectFailure([&] { AgcDriver::Graphics::RunColorMetadataPass(context, *pass); }, "DCC color target that is not all expanded"); + expectFailure([&] { AgcDriver::Graphics::RunColorMetadataPass(context, *pass); }, "DCC color target"); 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; + queue.context[0x31c] = (queue.context[0x31c] & ~0x1ffcu) | (10u << 2u) | (4u << 8u); + const auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.color.format == VK_FORMAT_R8G8B8A8_UINT && state.color.elementBytes == 4 && state.color.uintExport, "an 8_8_8_8 UINT color target was rejected"); + for (const std::uint32_t swap : {2u, 3u}) { + queue.context[0x31c] = (queue.context[0x31c] & ~0x1800u) | (swap << 11u); + Require(AgcDriver::Graphics::DecodeState(queue).color.format == VK_FORMAT_R8G8B8A8_UINT, "a reversed 8_8_8_8 UINT color target was rejected"); + } + queue.context[0x31c] = (queue.context[0x31c] & ~0x1800u) | (1u << 11u); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "unsupported color format 10 number type 4 component swap 1"); +} + void uint16ExportTests() { auto queue = makeState(); queue.context[0x1c5] = 7; @@ -1155,6 +3096,1063 @@ void uint16ExportTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "color export format 8"); } +VkSampleCountFlags depthSampleCounts = VK_SAMPLE_COUNT_4_BIT; +VkResult depthSampleResult = VK_SUCCESS; +std::uint32_t depthSampleQueries = 0; + +VKAPI_ATTR VkResult VKAPI_CALL sampleLocationDepthProperties(VkPhysicalDevice, VkFormat format, VkImageType type, VkImageTiling tiling, VkImageUsageFlags usage, VkImageCreateFlags flags, VkImageFormatProperties* properties) { + Require(format == VK_FORMAT_D32_SFLOAT && type == VK_IMAGE_TYPE_2D && tiling == VK_IMAGE_TILING_OPTIMAL, "sample location query changed the depth image format or type"); + Require(usage == (VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT) && flags == VK_IMAGE_CREATE_SAMPLE_LOCATIONS_COMPATIBLE_DEPTH_BIT_EXT, "sample location query differs from depth image creation"); + ++depthSampleQueries; + properties->sampleCounts = depthSampleCounts; + 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 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, lines 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 = [] { + auto queue = makeState(); + queue.context[0x2f8] = 0x00100001; + queue.context[0x292] = 3; + queue.context[0x3b8] = 0x0dc00000u; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0xcc44; + queue.context[0x31d] = 0x9000; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + return queue; + }; + const auto cmaskAddress = reinterpret_cast(cmaskMemory.data()); + const auto setCmaskAddress = [&](auto& target, std::uint32_t slot = 0u) { + const auto stride = slot * 0xfu; + target.context[0x31f + stride] = static_cast(cmaskAddress >> 8u); + target.context[0x398 + slot] = static_cast(cmaskAddress >> 40u); + }; + auto queue = multisampled(); + auto state = DecodeState(queue); + 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; + 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; + queue.context[0x31d] = 0x12000; + state = DecodeState(queue); + Require(state.samples == 4 && state.colors.size() == 1 && state.colors[0].samples == 4 && state.customSampleLocations, "single-sample scan conversion into a 4x target did not rasterize the target's samples"); + const auto centered = AgcDriver::Graphics::SampleLocations(state); + for (std::uint32_t sample = 0; sample < state.samples; ++sample) Require(centered[sample].x == 0.5f && centered[sample].y == 0.5f, "single-sample scan conversion did not place every sample at the pixel center"); + queue.context[0x292] = 3; + expectFailure([&] { DecodeState(queue); }, "MSAA_ENABLE on a single-sample target"); + queue = multisampled(); + queue.context[0x2f8] = 0x00300003; + queue.context[0x31d] = 0x1b000; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + queue.context[0x2fe + pixel * 4u] = 0xbd153fd1u; + queue.context[0x2ff + pixel * 4u] = 0x9773f95bu; + } + state = DecodeState(queue); + Require(state.samples == 8 && state.colors.size() == 1 && state.colors[0].samples == 8, "the standard 8x pattern (sample 7 at (7, -7), PPSA99902's registers) did not decode as standard"); + queue.context[0x2f8] = 0x00200002; + 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 && !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"); + AgcDriver::Graphics::Context sampleContext{}; + expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state); }, "VK_EXT_sample_locations"); + sampleContext.sampleLocationSampleCounts = VK_SAMPLE_COUNT_2_BIT; + expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state); }, "sample count"); + sampleContext.sampleLocationSampleCounts |= VK_SAMPLE_COUNT_4_BIT; + AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state); + sampleContext.imageFormatProperties = sampleLocationDepthProperties; + using AgcDriver::Graphics::SampleLocationsCompatibleDepth; + Require(SampleLocationsCompatibleDepth(sampleContext, VK_FORMAT_D32_SFLOAT, 4), "supported custom sample depth format was refused"); + state.depth = AgcDriver::Graphics::DepthTarget{}; + state.depth->format = VK_FORMAT_D32_SFLOAT; + state.depth->samples = 4; + Require(AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state), "supported custom depth sample locations were disabled"); + depthSampleCounts = VK_SAMPLE_COUNT_2_BIT; + Require(!SampleLocationsCompatibleDepth(sampleContext, VK_FORMAT_D32_SFLOAT, 4), "unsupported custom sample depth count was accepted"); + expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state); }, "compatible depth format and sample count"); + depthSampleCounts = VK_SAMPLE_COUNT_4_BIT; + depthSampleResult = VK_ERROR_FORMAT_NOT_SUPPORTED; + Require(!SampleLocationsCompatibleDepth(sampleContext, VK_FORMAT_D32_SFLOAT, 4), "unsupported custom sample depth format was accepted"); + expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state); }, "compatible depth format and sample count"); + state.customSampleLocations = false; + Require(!AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state), "standard locations did not fall back for an incompatible depth format"); + depthSampleResult = VK_ERROR_OUT_OF_HOST_MEMORY; + expectFailure([&] { SampleLocationsCompatibleDepth(sampleContext, VK_FORMAT_D32_SFLOAT, 4); }, "Vulkan result"); + depthSampleResult = VK_SUCCESS; + const auto queries = depthSampleQueries; + Require(!SampleLocationsCompatibleDepth({}, VK_FORMAT_D32_SFLOAT, 4) && !SampleLocationsCompatibleDepth(sampleContext, VK_FORMAT_D32_SFLOAT, 1), "sample location depth compatibility requires the extension and multisampling"); + Require(depthSampleQueries == queries, "ineligible sample location depth formats were queried"); + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0x622ae6ae; + state = DecodeState(queue); + Require(!state.customSampleLocations, "standard sample locations require the extension"); + const auto standard = AgcDriver::Graphics::SampleLocations(state); + const std::array expectedStandard{{{0.375f, 0.125f}, {0.875f, 0.375f}, {0.125f, 0.625f}, {0.625f, 0.875f}}}; + for (std::size_t sample = 0; sample < expectedStandard.size(); ++sample) Require(standard[sample].x == expectedStandard[sample].x && standard[sample].y == expectedStandard[sample].y, "standard sample coordinate scale or Y direction changed"); + AgcDriver::Graphics::ValidateSampleLocations({}, state); + for (const auto samples : {2u, 8u}) { + queue = multisampled(); + const auto log2 = samples == 2 ? 1u : 3u; + queue.context[0x2f8] = (log2 << 20u) | log2; + queue.context[0x31d] = (log2 << 12u) | (log2 << 15u); + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + queue.context[0x2fe + pixel * 4u] = 0x77887788; + queue.context[0x2ff + pixel * 4u] = 0x77887788; + } + state = DecodeState(queue); + Require(state.customSampleLocations && state.samples == samples, "a custom 2x or 8x pattern did not decode"); + const auto endpoints = AgcDriver::Graphics::SampleLocations(state); + for (std::uint32_t sample = 0; sample < samples; ++sample) { + const auto expected = sample % 2u == 0 ? 0.0f : 15.0f / 16.0f; + Require(endpoints[sample].x == expected && endpoints[sample].y == expected, "signed sample coordinate endpoints changed"); + } + } + queue.context[0x2f8] = 0x00300003; + queue.context[0x31d] = 0x1b000; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + queue.context[0x2fe + pixel * 4u] = 0xbd153fd1u; + queue.context[0x2ff + pixel * 4u] = 0x9773f95bu; + } + state = DecodeState(queue); + Require(state.samples == 8 && !state.customSampleLocations && state.colors.size() == 1 && state.colors[0].samples == 8, "the standard 8x pattern (sample 7 at (7, -7), PPSA99902's registers) did not decode as standard"); + AgcDriver::Graphics::ValidateSampleLocations({}, state); + for (const auto log2 : {0u, 1u, 2u, 3u}) { + queue = log2 == 0 ? makeState() : multisampled(); + queue.context[0x1b3] = 2u; + queue.context[0x1b4] = 2u; + queue.context[0x2f8] = (log2 << 20u) | log2 | 0x20000000u; + queue.context[0x31d] = (log2 << 12u) | (log2 << 15u); + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + queue.context[0x2fe + pixel * 4u] = log2 == 3 ? 0xbd153fd1u : log2 == 2 ? 0x622ae6aeu : 0xcc44u; + queue.context[0x2ff + pixel * 4u] = log2 == 3 ? 0x9773f95bu : 0u; + } + std::vector sampleReads; + std::optional sampleLog(std::in_place, &sampleReads); + const auto coveredCenter = DecodeState(queue); + const auto coveredRejection = AgcDriver::Graphics::DrawRejection(queue, false); + 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); + for (const auto centroid : {4u, 64u, 68u}) { + queue.context[0x1b3] = 2u | centroid; + queue.context[0x1b4] = 2u | centroid; + expectFailure([&] { DecodeState(queue); }, "COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).find("COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs") != std::string::npos, "the precheck accepted active covered-centroid interpolation"); + queue.context[0x1b4] = 2u; + Require(DecodeState(queue).samples == (1u << log2) && AgcDriver::Graphics::DrawRejection(queue, false).empty(), "an unaddressed centroid input changed covered-centroid mode"); + queue.context[0x1b3] = 2u; + queue.context[0x1b4] = 2u | centroid; + Require(DecodeState(queue).samples == (1u << log2) && AgcDriver::Graphics::DrawRejection(queue, false).empty(), "a disabled centroid input changed covered-centroid mode"); + } + queue.context[0x1b3] = 2u; + queue.context[0x1b4] = 2u; + for (const auto offset : {0x1b3u, 0x1b4u}) { + queue.context.erase(offset); + expectFailure([&] { DecodeState(queue); }, "COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).find("COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs") != std::string::npos, "the precheck accepted unspecified centroid inputs"); + queue.context[offset] = 2u; + } + const auto withProgram = queue; + queue.shader[0x008] = 0; + queue.context[0x8e] = 0; + queue.context.erase(0x1b3); + queue.context.erase(0x1b4); + Require(DecodeState(queue).samples == (1u << log2) && AgcDriver::Graphics::DrawRejection(queue, false).empty(), "covered-centroid mode was refused with no pixel program"); + queue = withProgram; + queue.context[0x2f8] |= 0x10u; + expectFailure([&] { DecodeState(queue); }, "coverage conversion or nonstandard multisampling"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).find("coverage conversion or nonstandard multisampling") != std::string::npos, "covered-centroid handling admitted unrelated AA bits"); + } + queue = multisampled(); + queue.context[0x31d] = 0x1000; + expectFailure([&] { DecodeState(queue); }, "fewer fragments than samples"); + queue.context[0x31d] = 0; + 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; + state = DecodeState(queue); + Require(state.samples == 2 && state.colors.size() == 1 && state.colors[0].samples == 2 && state.customSampleLocations, "a 2x target rasterized without MSAA_ENABLE did not rasterize the target's samples"); + const auto aliased = AgcDriver::Graphics::SampleLocations(state); + for (std::uint32_t sample = 0; sample < state.samples; ++sample) Require(aliased[sample].x == 0.5f && aliased[sample].y == 0.5f, "a 2x target rasterized without MSAA_ENABLE did not place every sample at the pixel center"); + queue.context[0x2dc] = 0x0000aa01u; + expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE are unsupported for points, lines 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"); + queue = multisampled(); + queue.context[0x31c] |= 0x6000; + setCmaskAddress(queue); + Require(DecodeState(queue).colors[0].cmaskAddress == cmaskAddress, "the CMASK address of a multisampled target did not decode"); + queue = makeState(); + queue.context[0x31c] |= 0x4000; + expectFailure([&] { DecodeState(queue); }, "color compression"); + queue = multisampled(); + queue.context[0x31c] |= 0x2000; + setCmaskAddress(queue); + const auto fastCleared = DecodeState(queue).colors[0]; + Require(fastCleared.cmaskAddress == cmaskAddress && fastCleared.cmaskFastClear && fastCleared.cmaskBytes == 0x1000, "a multisampled FAST_CLEAR target did not decode its CMASK at the metablock size"); + queue = multisampled(); + queue.context[0x31c] |= 0x4000; + setCmaskAddress(queue); + const auto compressed = DecodeState(queue).colors[0]; + Require(compressed.cmaskAddress == cmaskAddress && !compressed.cmaskFastClear && compressed.cmaskBytes == 0x1000, "a multisampled COMPRESSION target did not decode its CMASK at the metablock size"); + queue = multisampled(); + queue.context[0x31c] |= 0x2000; + 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; + { + 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(); + queue.context[0x0] = 0; + queue.context[0x202] = 0xcc0030; + for (const auto offset : {0x318u, 0x31bu, 0x31cu, 0x390u}) queue.context[offset + (offset >= 0x390u ? 1u : 0xfu)] = queue.context[offset]; + queue.context[0x32c] = 0; + queue.context[0x3b1] = queue.context[0x3b0]; + queue.context[0x3b9] = queue.context[0x3b8]; + setCmaskAddress(queue); + setCmaskAddress(queue, 1); + auto pass = DecodeColorMetadataPass(queue); + Require(pass.has_value() && pass->mode == ColorMetadataPass::Mode::Resolve && pass->source.has_value() && pass->source->samples == 2 && pass->targets.size() == 1 && pass->targets[0].samples == 1, "a CB resolve of a multisampled target did not decode"); + const auto rasterizedResolve = queue; + queue.context[0x2f8] = 0; + pass = DecodeColorMetadataPass(queue); + Require(pass.has_value() && pass->mode == ColorMetadataPass::Mode::Resolve && pass->source.has_value() && pass->source->samples == 2 && pass->targets.size() == 1, "a CB resolve with a single-sample rasterizer did not take its sample count from the source target"); + queue = rasterizedResolve; + queue.context[0x2f8] = 0x00200002; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0x622ae6aeu; + expectFailure([&] { DecodeColorMetadataPass(queue); }, "differs from the rasterizer's"); + queue.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; + for (const auto offset : {0x318u, 0x31bu, 0x31cu, 0x390u}) queue.context[offset + (offset >= 0x390u ? 1u : 0xfu)] = queue.context[offset]; + queue.context[0x32c] = 0; + queue.context[0x3b1] = queue.context[0x3b0]; + queue.context[0x3b9] = queue.context[0x3b8]; + queue.context[0x32b] = queue.context[0x31c] | 0x2000u; + setCmaskAddress(queue, 1); + expectFailure([&] { DecodeColorMetadataPass(queue); }, "fast-clear color target"); + queue = multisampled(); + queue.context[0x2f8] = 0; + 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; + MultisampledCmask cmask(0x10000, 0x1000); + cmask.NoteFill(0x10000, 0x1000, 0x11111111u); + expectFailure([&] { cmask.TakeClear(); }, "leaves its tiles mixed"); + cmask.NoteFill(0x10800, 0x1000, 0); + expectFailure([&] { cmask.TakeClear(); }, "partly covers"); + cmask.NoteFill(0x10000, 0x1000, 0); + Require(cmask.TakeClear() && !cmask.TakeClear(), "a zero fill of the whole CMASK did not clear once"); + cmask.NoteFill(0x10000, 0x1000, 0xffffffffu); + Require(!cmask.TakeClear(), "an expanded CMASK cleared"); + cmask.NoteFill(0x20000, 0x1000, 0x11111111u); + 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.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.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"); + + using AgcDriver::Graphics::ColorTarget; + using AgcDriver::Graphics::ColorTileMode; + using AgcDriver::Graphics::MultisampledColorBytes; + using AgcDriver::Graphics::MultisampledColorFill; + using AgcDriver::Graphics::MultisampledFillClearColor; + ColorTarget halfFloat{}; + halfFloat.address = 0x40000000; + halfFloat.extent = {960, 540}; + halfFloat.format = VK_FORMAT_R16G16B16A16_SFLOAT; + halfFloat.tileMode = ColorTileMode::RenderTarget; + halfFloat.elementBytes = 8; + halfFloat.samples = 4; + Require(MultisampledColorBytes(halfFloat) == 0xff0000, "a 4x 960x540 8-byte target is not 64x32 pixels per 64 KiB block"); + auto srgb = halfFloat; + srgb.extent = {1536, 1536}; + srgb.format = VK_FORMAT_R8G8B8A8_SRGB; + srgb.elementBytes = 4; + Require(MultisampledColorBytes(srgb) == 0x2400000, "a 4x 1536x1536 4-byte target is not 64x64 pixels per 64 KiB block"); + auto narrow = srgb; + narrow.extent = {130, 60}; + narrow.samples = 2; + Require(MultisampledColorBytes(narrow) == 0x30000, "a 2x 4-byte target is not 64x128 pixels per 64 KiB block"); + narrow.samples = 8; + Require(MultisampledColorBytes(narrow) == 0x50000, "an 8x 4-byte target is not 32x64 pixels per 64 KiB block"); + srgb.extent = {100, 100}; + Require(MultisampledColorBytes(srgb) == 0x40000, "a 4x 100x100 4-byte target does not span 2x2 64 KiB blocks"); + 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; + Require(MultisampledColorBytes(srgb) == 0, "a linear multisampled target has a footprint"); + + MultisampledColorFill fill(halfFloat); + Require(!fill.TakeFill(), "an unfilled target cleared"); + fill.NoteFill(0x40000000, 0xff0000, 0); + const auto zero = fill.TakeFill(); + Require(zero && *zero == 0 && !fill.TakeFill(), "a fill of the whole color memory did not clear once"); + fill.NoteFill(0x40ff0000, 0x10000, 0); + fill.NoteFill(0x3fff0000, 0x10000, 0); + Require(!fill.TakeFill(), "a fill next to the color memory cleared it"); + fill.NoteFill(0x40000000, 0xfe0000, 0); + expectFailure([&] { fill.TakeFill(); }, "partly covers"); + fill.NoteFill(0x3fff0000, 0x1010000, 0x3c003c00u); + const auto one = fill.TakeFill(); + Require(one && *one == 0x3c003c00u, "a fill over and around the color memory did not clear it"); + MultisampledColorFill linear(srgb); + linear.NoteFill(0x40001000, 0x1000, 0); + Require(!linear.TakeFill(), "a fill past the start of a target with no modeled footprint was taken"); + linear.NoteFill(0x40000000, 0x1000, 0); + expectFailure([&] { linear.TakeFill(); }, "footprint is not modeled"); + + VkClearColorValue clear{}; + Require(MultisampledFillClearColor(halfFloat, 0x3c003c00u, clear) && clear.float32[0] == 1.0f && clear.float32[3] == 1.0f, "a half-float fill of 1.0 did not clear to 1.0"); + Require(MultisampledFillClearColor(srgb, 0, clear) && clear.float32[0] == 0.0f && clear.float32[3] == 0.0f, "an sRGB fill of 0 did not clear to 0"); + Require(MultisampledFillClearColor(srgb, 0x80ffffffu, clear) && clear.float32[2] == 1.0f && clear.float32[3] == 128.0f / 255.0f, "an sRGB fill of white with linear alpha 0x80 did not clear to it"); + Require(!MultisampledFillClearColor(srgb, 0x00000080u, clear), "an sRGB fill whose color decodes inexactly cleared"); + + using AgcDriver::Graphics::TakeMultisampledClear; + halfFloat.clearWords = {0x3c003c00u, 0x3c003c00u}; + MultisampledCmask clearedCmask(0x50000000, 0x1000); + MultisampledColorFill pending(halfFloat); + Require(!TakeMultisampledClear(clearedCmask, pending, halfFloat, clear), "a target with no CMASK clear and no fill cleared"); + pending.NoteFill(0x40000000, 0xff0000, 0); + Require(TakeMultisampledClear(clearedCmask, pending, halfFloat, clear) && clear.float32[0] == 0.0f, "a fill of the whole color memory did not clear to its value"); + pending.NoteFill(0x40000000, 0xfe0000, 0); + expectFailure([&] { TakeMultisampledClear(clearedCmask, pending, halfFloat, clear); }, "partly covers"); + clearedCmask.NoteFill(0x50000000, 0x1000, 0); + Require(TakeMultisampledClear(clearedCmask, pending, halfFloat, clear) && clear.float32[0] == 1.0f, "a pending CMASK clear did not take precedence over a partial fill of the color memory"); + Require(!TakeMultisampledClear(clearedCmask, pending, halfFloat, clear), "a partial fill of the color memory outlived the CMASK clear over it"); + pending.NoteFill(0x40000000, 0xff0000, 0); + clearedCmask.NoteFill(0x50000000, 0x1000, 0); + Require(TakeMultisampledClear(clearedCmask, pending, halfFloat, clear) && clear.float32[0] == 1.0f && !TakeMultisampledClear(clearedCmask, pending, halfFloat, clear), "a pending CMASK clear and a whole fill did not clear once to the CMASK clear color"); +} + +void multisampleDccStateTests() { + static std::array color{}; + const auto address = (reinterpret_cast(color.data()) + 65535u) & ~std::uintptr_t{65535u}; + for (const auto log2 : {1u, 2u, 3u}) { + auto queue = makeState(); + queue.context[0x318] = static_cast(address >> 8u); + queue.context[0x390] = static_cast(address >> 40u); + queue.context[0x31c] |= 0x10000000u; + queue.context[0x31d] = (log2 << 12u) | (log2 << 15u); + queue.context[0x31e] = 0x00100038u; + queue.context[0x325] = 0x1234u; + queue.context[0x3b8] = 0x4dc6c000u; + queue.context[0x2f8] = (log2 << 20u) | log2; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + queue.context[0x2fe + pixel * 4u] = 0; + queue.context[0x2ff + pixel * 4u] = 0; + } + queue.context[0x292] = 3; + queue.context[0x1b3] = queue.context[0x1b4] = 2; + const auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.colors.size() == 1 && state.colors.front().samples == (1u << log2) && state.colors.front().dccAddress == 0x123400u && state.colors.front().dccPipeAligned, "normal-render multisampled DCC state did not decode"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "normal-render multisampled DCC state failed the draw precheck"); + auto changed = queue; + changed.context[0x31e] |= 0x80u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "color transforms or lossy"); + changed = queue; + changed.context[0x3b8] &= ~0x40000000u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "pipe-aligned single-layer"); + changed = queue; + changed.context[0x325] = 0; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "pipe-aligned single-layer"); + changed = queue; + changed.context[0x3b8] |= 1u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "pipe-aligned single-layer"); + changed = queue; + changed.context[0x3b0] |= 1u << 28u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "multisampled color mips"); + } +} + +void multisampledDepthTests() { + using AgcDriver::Graphics::DecodeState; + const auto singleSampleScan = [] { + auto queue = makeState(); + queue.context[0x2f8] = 0; + queue.context[0x292] = 2; + queue.context[0x31d] = 0x12000; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + queue.context[0x000] = 0; + queue.context[0x002] = 0; + queue.context[0x007] = (3u << 16u) | 63u; + queue.context[0x00a] = 0; + queue.context[0x00b] = 0; + queue.context[0x010] = 0x2290098b; + queue.context[0x011] = 0x20000180; + for (const auto offset : {0x012u, 0x014u}) queue.context[offset] = 0x100; + queue.context[0x200] = 0x16; + return queue; + }; + const auto requireCentered = [](const AgcDriver::Graphics::State& state, const std::string& what) { + Require(state.samples == 4 && state.depth && state.depth->samples == 4 && state.customSampleLocations && !state.sampleLocationGrid && !state.perSampleShading, what + " did not rasterize the depth target's samples"); + const auto locations = AgcDriver::Graphics::SampleLocations(state); + for (std::uint32_t sample = 0; sample < state.samples; ++sample) Require(locations[sample].x == 0.5f && locations[sample].y == 0.5f, what + " did not place every sample at the pixel center"); + }; + const auto depthOnly = [&] { + auto queue = singleSampleScan(); + queue.context[0x8e] = 0; + queue.context[0x010] = 0xa290098b; + queue.context[0x200] = 0x777; + queue.context[0x201] = 0; + return queue; + }; + auto queue = depthOnly(); + auto state = DecodeState(queue); + Require(state.colors.empty(), "a depth-only single-sample scan decoded a color target"); + requireCentered(state, "a single-sample scan into a 4x depth target (PPSA18216's registers)"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck refused a single-sample scan into a 4x depth target"); + for (const auto primitive : {5u, 6u, 7u, 17u}) { + queue = depthOnly(); + queue.userConfig[0x242] = primitive; + requireCentered(DecodeState(queue), "a single-sample scan of primitive type " + std::to_string(primitive) + " into a 4x depth target"); + } + queue = depthOnly(); + queue.context[0x000] = 1; + queue.context[0x00b] = std::bit_cast(0.25f); + state = DecodeState(queue); + requireCentered(state, "a depth clear scanned single-sampled into a 4x depth target"); + Require(state.depthWrite && state.depthCompare == VK_COMPARE_OP_ALWAYS, "a depth clear into a 4x depth target did not write every covered sample"); + queue = depthOnly(); + queue.context[0x1b3] = queue.context[0x1b4] = 2u | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary); + requireCentered(DecodeState(queue), "a single-sample scan whose pixel program is skipped"); + queue = depthOnly(); + queue.context[0x201] = 0x00040000u; + expectFailure([&] { DecodeState(queue); }, "a single-sample scan into a multisampled depth target with Z interpolation or overrasterization (DB_EQAA)"); + queue = singleSampleScan(); + state = DecodeState(queue); + Require(state.colors.size() == 1 && state.colors[0].samples == 4, "a single-sample scan into 4x depth and color targets lost its color target"); + requireCentered(state, "a single-sample scan into 4x depth and color targets"); + queue.context[0x293] = 0x10000u; + Require(!DecodeState(queue).perSampleShading, "PS_ITER_SAMPLE requested sample shading for a single-sample scan"); + queue = singleSampleScan(); + queue.context[0x31d] = 0; + expectFailure([&] { DecodeState(queue); }, "a color target's sample count differs from the rasterizer's"); + queue = singleSampleScan(); + queue.context[0x010] = 0x22900983; + expectFailure([&] { DecodeState(queue); }, "a color target's sample count differs from the rasterizer's"); + queue = singleSampleScan(); + queue.context[0x2dc] = 0x0000aa01u; + expectFailure([&] { DecodeState(queue); }, "a single-sample scan into a multisampled depth target with alpha-to-coverage"); + for (const auto primitive : {2u, 3u}) { + queue = singleSampleScan(); + queue.userConfig[0x242] = primitive; + expectFailure([&] { DecodeState(queue); }, "a single-sample scan into a multisampled depth target for primitives other than triangles and rectangles"); + } + queue = singleSampleScan(); + queue.context[0x1b3] = queue.context[0x1b4] = 2u | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary); + expectFailure([&] { DecodeState(queue); }, "a single-sample scan into a multisampled depth target with a pixel program reading the sample index"); + queue = singleSampleScan(); + queue.context[0x1b3] = queue.context[0x1b4] = 2u | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveSample); + requireCentered(DecodeState(queue), "a single-sample scan with a pixel program reading sample-interpolated inputs"); + queue = singleSampleScan(); + queue.context[0x1c4] = 9; + queue.context[0x203] |= 0x100u; + expectFailure([&] { DecodeState(queue); }, "a single-sample scan into a multisampled depth target with a sample-mask export"); + queue = singleSampleScan(); + queue.context[0x292] = 3; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0; + expectFailure([&] { DecodeState(queue); }, "the depth target's sample count differs from the rasterizer's"); + queue = singleSampleScan(); + queue.context[0x2f8] = 0x00100001; + queue.context[0x292] = 3; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0xcc44; + queue.context[0x31d] = 0x9000; + expectFailure([&] { DecodeState(queue); }, "the depth target's sample count differs from the rasterizer's"); +} + +void clipDistanceTests() { + auto queue = makeState(); + queue.context[0x207] = 0x0040000f; + Require(AgcDriver::Graphics::DecodeState(queue).paClVsOutCntl == 0x0040000fu, "DecodeState dropped the PA_CL_VS_OUT_CNTL value the validator quotes"); + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "four clip distances were rejected by the precheck"); + queue.context[0x207] = 0x004001ff; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "more than eight clip and cull distances"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).find("more than eight") != std::string::npos, "nine clip distances passed the precheck"); +} + +void framebufferLayerTests() { + AgcDriver::Graphics::Context context{}; + context.limits.maxFramebufferWidth = 64; + context.limits.maxFramebufferHeight = 64; + context.limits.maxFramebufferLayers = 3; + AgcDriver::Graphics::Framebuffer accepted(context, VK_NULL_HANDLE, {}, {64, 64}, 3); + for (const VkRenderPass pass : std::array{VK_NULL_HANDLE, reinterpret_cast(1)}) { + expectFailure([&] { AgcDriver::Graphics::Framebuffer rejected(context, pass, {}, {64, 64}, 0); }, "framebuffer layers exceed device limits"); + expectFailure([&] { AgcDriver::Graphics::Framebuffer rejected(context, pass, {}, {64, 64}, 4); }, "framebuffer layers exceed device limits"); + } +} + +void 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); @@ -1182,8 +4180,23 @@ void DepthClipTests() { queue.context[0xb4] = std::bit_cast(2.0f); expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "inverted viewport depth clamp"); queue.context[0xb4] = 0; + auto collapsedQueue = makeState(); + collapsedQueue.context[0x10f] = 0; + collapsedQueue.context[0x110] = 0; + collapsedQueue.context[0x111] = std::bit_cast(-0.0f); + collapsedQueue.context[0x112] = 0; + const auto collapsed = AgcDriver::Graphics::DecodeState(collapsedQueue); + Require(collapsed.scissor.extent.width == 0 && collapsed.scissor.extent.height == 0, "a triangle draw through a zero-scale viewport rasterizes"); + Require(collapsed.viewport.width > 0 && collapsed.viewport.height > 0 && collapsed.viewport.minDepth == 0 && collapsed.viewport.maxDepth == 1, "a zero-scale viewport did not become a valid Vulkan viewport"); + collapsedQueue.userConfig[0x242] = 2; + expectFailure([&] { AgcDriver::Graphics::DecodeState(collapsedQueue); }, "unsupported viewport transform"); + collapsedQueue.userConfig[0x242] = 4; + collapsedQueue.context[0x10f] = std::bit_cast(-0.0f); + Require(AgcDriver::Graphics::DecodeState(collapsedQueue).scissor.extent.width == 0, "a negative-zero viewport scale was not collapsed"); + 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"); @@ -1196,13 +4209,122 @@ 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"); + } } } +void UnditheredAlphaToCoverageTests() { + auto queue = makeState(); + queue.context[0x203] = 0x8010; + queue.context[0x2dc] = 0xaa01; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + for (const auto samples : {1u, 8u}) { + if (samples == 8) { + queue.context[0x201] = 0x00133303; + queue.context[0x2f8] = 0x0030e003; + queue.context[0x292] = 3; + queue.context[0x31d] = 0x1b000; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + queue.context[0x2fe + pixel * 4u] = 0xbd153fd1u; + queue.context[0x2ff + pixel * 4u] = 0x9773f95bu; + } + } + for (const auto disabled : {false, true}) { + queue.context[0x203] = 0x8010u | (disabled ? 0x800u : 0u); + const auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.alphaToCoverage && !state.ditheredAlphaToCoverage && state.samples == samples, "the existing undithered alpha-to-coverage state changed"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck changed the existing undithered alpha-to-coverage state"); + } + } +} + +void AlphaToCoverageTests() { + UnditheredAlphaToCoverageTests(); + auto dithered = makeState(); + dithered.context[0x203] = 0x8010; + dithered.context[0x201] = 0x00133303; + dithered.context[0x2dc] = 0x18701; + dithered.context[0x2f8] = 0x0030e003; + dithered.context[0x292] = 3; + dithered.context[0x31d] = 0x1b000; + dithered.context[0x1b3] = 2; + dithered.context[0x1b4] = 2; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + dithered.context[0x2fe + pixel * 4u] = 0xbd153fd1u; + dithered.context[0x2ff + pixel * 4u] = 0x9773f95bu; + } + const auto rejection = AgcDriver::Graphics::DrawRejection(dithered, false); + Require(rejection.empty(), "eight-sample default dither refused: " + rejection); + const auto ditheredState = AgcDriver::Graphics::DecodeState(dithered); + Require(ditheredState.alphaToCoverage && ditheredState.samples == 8, "default dither did not enable eight-sample coverage"); + Require(ditheredState.ditheredAlphaToCoverage, "default dither did not reach the shader state"); + for (const auto eqaa : {0x00133333u, 0x00132303u, 0x00133203u, 0x00333303u}) { + dithered.context[0x201] = eqaa; + expectFailure([&] { AgcDriver::Graphics::DecodeState(dithered); }, "DB_EQAA"); + Require(AgcDriver::Graphics::DrawRejection(dithered, false).find("DB_EQAA") != std::string::npos, "the precheck accepted unmatched dithered coverage samples"); + } + dithered.context[0x201] = 0x00133303; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) dithered.context[0x2fe + pixel * 4u] ^= 1u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(dithered); }, "custom sample locations"); + Require(AgcDriver::Graphics::DrawRejection(dithered, false).find("custom sample locations") != std::string::npos, "the precheck accepted unmeasured dithered sample locations"); + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) dithered.context[0x2fe + pixel * 4u] ^= 1u; + for (const auto format : {3u, 5u, 6u}) { + dithered.context[0x1c5] = format; + expectFailure([&] { AgcDriver::Graphics::DecodeState(dithered); }, "FP16_ABGR or 32_ABGR"); + Require(AgcDriver::Graphics::DrawRejection(dithered, false).find("FP16_ABGR or 32_ABGR") != std::string::npos, "the precheck accepted an unmeasured dithered export format"); + } + dithered.context[0x1c5] = 4; + Require(AgcDriver::Graphics::DecodeState(dithered).ditheredAlphaToCoverage, "FP16 default dither was refused"); + dithered.context[0x203] |= 0x800u; + Require(!AgcDriver::Graphics::DecodeState(dithered).alphaToCoverage, "the shader disable override was ignored"); + Require(AgcDriver::Graphics::DrawRejection(dithered, false).empty(), "the precheck ignored the shader disable override"); + dithered.context[0x2dc] |= 2u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(dithered); }, "alpha-to-coverage"); + auto queue = makeState(); + queue.context[0x203] = 0; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + Require(!AgcDriver::Graphics::DecodeState(queue).alphaToCoverage, "alpha-to-coverage decoded while disabled"); + queue.context[0x2dc] = 0xaa01; + Require(AgcDriver::Graphics::DecodeState(queue).alphaToCoverage, "undithered alpha-to-coverage was not decoded"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck rejected undithered alpha-to-coverage"); + queue.context[0x203] |= 0x800u; + Require(AgcDriver::Graphics::DecodeState(queue).alphaToCoverage, "the existing undithered single-sample state changed under the shader disable override"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck changed the existing undithered single-sample state under the shader disable override"); + queue.context[0x203] &= ~0x800u; + for (const auto control : {0x8701u, 0x1aa01u, 0xaa03u}) { + queue.context[0x2dc] = control; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "alpha-to-coverage"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).find("alpha-to-coverage") != std::string::npos, "the precheck accepted unsupported alpha-to-coverage"); + } + queue.context[0x2dc] = 0xaa01; + const std::uint32_t exportFormat = queue.context.at(0x1c5); + for (const auto format : {3u, 4u, 5u, 6u, 9u}) { + queue.context[0x1c5] = (exportFormat & ~0xfu) | format; + Require(AgcDriver::Graphics::DecodeState(queue).alphaToCoverage, "alpha-to-coverage rejected an MRT0 export format with a floating-point alpha"); + } + for (const auto format : {1u, 2u}) { + queue.context[0x1c5] = (exportFormat & ~0xfu) | format; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "floating-point alpha"); + } + const auto targetMask = queue.context.at(0x8e); + queue.context[0x8e] = 0; + queue.context[0x1c5] = (exportFormat & ~0xfu) | 3u; + const auto depthOnly = AgcDriver::Graphics::DecodeState(queue); + Require(depthOnly.alphaToCoverage && depthOnly.colors.empty(), "alpha-to-coverage without a written color target and a 32_AR MRT0 export was refused or kept a target"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck rejected alpha-to-coverage without a written color target"); + queue.context[0x1c5] = exportFormat & ~0xfu; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "floating-point alpha"); + queue.context[0x8e] = targetMask; + queue.context[0x1c5] = exportFormat; +} + void InitialContextTests() { const auto configured = makeState(); AgcDriver::QueueState queue; @@ -1219,8 +4341,9 @@ void InitialContextTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "PA_CL_VTE_CNTL=0x"); } queue.context[0x206] = 0x43f; + queue.context[0x203] &= ~0x800u; queue.context[0x2dc] |= 1; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "alpha-to-coverage"); + Require(AgcDriver::Graphics::DecodeState(queue).alphaToCoverage, "undithered alpha-to-coverage was not decoded"); queue.context.erase(0x2dc); expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "missing register"); queue.ClearContext(); @@ -1230,35 +4353,96 @@ void InitialContextTests() { struct MockDescriptorWrite { std::uint32_t binding; + std::uint32_t arrayElement; 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; std::map bufferMemory; + std::map bufferOffset; std::map> memories; std::map allocationSizes; 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 freedSets = 0; + std::uint32_t freedDescriptorSets = 0; + struct DescriptorPool { + VkDescriptorPoolCreateFlags flags; + std::uint32_t maxSets; + std::uint32_t allocated = 0; + std::uint32_t attempts = 0; + std::uint32_t resets = 0; + }; + 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; std::uint32_t boundFirst = 0; VkPipelineBindPoint boundPoint = VK_PIPELINE_BIND_POINT_MAX_ENUM; std::map pipelineLayoutPushConstantSize; + std::map> pipelineLayoutSets; + std::map pipelineLayoutFlags; + std::map> pipelineLayoutPushRanges; + std::map libraryLayouts; std::uint32_t pipelineCreateCount = 0; std::vector> pipelineSpecializations; VkPipeline boundPipeline = VK_NULL_HANDLE; @@ -1267,6 +4451,11 @@ struct MockVulkan { }; MockVulkan mock; +std::mutex pipelineCreateMutex; +std::condition_variable pipelineCreateCondition; +bool blockLibraryCreate = false; +bool libraryCreateEntered = false; +bool releaseLibraryCreate = false; template THandle makeHandle() { @@ -1284,12 +4473,14 @@ VKAPI_ATTR VkResult VKAPI_CALL mockCreateBuffer(VkDevice, const VkBufferCreateIn } VKAPI_ATTR void VKAPI_CALL mockGetBufferMemoryRequirements(VkDevice, VkBuffer buffer, VkMemoryRequirements* requirements) { - *requirements = {mock.bufferSizes.at(buffer), 1, 1}; + *requirements = {mock.bufferSizes.at(buffer), 1, mock.bufferMemoryTypeBits}; } VKAPI_ATTR VkResult VKAPI_CALL mockAllocateMemory(VkDevice, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks*, VkDeviceMemory* memory) { ++mock.allocationAttempts; + mock.allocationTypes.push_back(info->memoryTypeIndex); if (mock.memoryLimit.has_value() && mock.allocatedBytes + info->allocationSize > *mock.memoryLimit) return VK_ERROR_OUT_OF_DEVICE_MEMORY; + 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; @@ -1304,11 +4495,17 @@ VKAPI_ATTR VkResult VKAPI_CALL mockAllocateMemory(VkDevice, const VkMemoryAlloca } VKAPI_ATTR VkResult VKAPI_CALL mockBindBufferMemory(VkDevice, VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize offset) { - Require(offset == 0, "mock buffer memory must be bound at offset zero"); + Require(offset + mock.bufferSizes.at(buffer) <= mock.memories.at(memory).size(), "mock buffer memory is bound past its allocation"); mock.bufferMemory[buffer] = memory; + mock.bufferOffset[buffer] = offset; return VK_SUCCESS; } +std::span mockBufferMemory(VkBuffer buffer) { + auto& memory = mock.memories.at(mock.bufferMemory.at(buffer)); + return std::span(memory).subspan(static_cast(mock.bufferOffset.at(buffer)), static_cast(mock.bufferSizes.at(buffer))); +} + VKAPI_ATTR VkResult VKAPI_CALL mockMapMemory(VkDevice, VkDeviceMemory memory, VkDeviceSize offset, VkDeviceSize, VkMemoryMapFlags, void** data) { Require(offset == 0, "mock memory must be mapped from offset zero"); *data = mock.memories.at(memory).data(); @@ -1329,6 +4526,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); @@ -1336,11 +4550,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; } @@ -1349,30 +4567,72 @@ VKAPI_ATTR VkResult VKAPI_CALL mockCreateDescriptorPool(VkDevice, const VkDescri mock.poolSizes.assign(info->pPoolSizes, info->pPoolSizes + info->poolSizeCount); mock.poolMaxSets = info->maxSets; mock.poolFlags = info->flags; + mock.descriptorPools.emplace(*pool, MockVulkan::DescriptorPool{info->flags, std::min(info->maxSets, mock.descriptorPoolLimit)}); ++mock.live; return VK_SUCCESS; } -VKAPI_ATTR void VKAPI_CALL mockDestroyDescriptorPool(VkDevice, VkDescriptorPool, const VkAllocationCallbacks*) { +VKAPI_ATTR void VKAPI_CALL mockDestroyDescriptorPool(VkDevice, VkDescriptorPool pool, const VkAllocationCallbacks*) { + mock.descriptorPools.erase(pool); --mock.live; } VKAPI_ATTR VkResult VKAPI_CALL mockAllocateDescriptorSets(VkDevice, const VkDescriptorSetAllocateInfo* info, VkDescriptorSet* sets) { Require(info->descriptorSetCount == 1, "exactly one descriptor set must be allocated"); + auto& pool = mock.descriptorPools.at(info->descriptorPool); + ++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; } -VKAPI_ATTR VkResult VKAPI_CALL mockFreeDescriptorSets(VkDevice, VkDescriptorPool, std::uint32_t count, const VkDescriptorSet*) { - mock.freedSets += count; +VKAPI_ATTR VkResult VKAPI_CALL mockResetDescriptorPool(VkDevice, VkDescriptorPool handle, VkDescriptorPoolResetFlags) { + auto& pool = mock.descriptorPools.at(handle); + const auto result = mock.descriptorResetResult; + mock.descriptorResetResult = VK_SUCCESS; + if (result != VK_SUCCESS) return result; + pool.allocated = 0; + ++pool.resets; return VK_SUCCESS; } -VKAPI_ATTR void VKAPI_CALL mockUpdateDescriptorSets(VkDevice, std::uint32_t count, const VkWriteDescriptorSet* writes, std::uint32_t copyCount, const VkCopyDescriptorSet*) { - Require(copyCount == 0, "descriptor copies are not expected"); +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].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); } } @@ -1399,6 +4659,35 @@ VKAPI_ATTR VkDeviceAddress VKAPI_CALL mockGetBufferDeviceAddress(VkDevice, const VKAPI_ATTR VkResult VKAPI_CALL mockCreatePipelineLayout(VkDevice, const VkPipelineLayoutCreateInfo* info, const VkAllocationCallbacks*, VkPipelineLayout* layout) { *layout = makeHandle(); mock.pipelineLayoutPushConstantSize[*layout] = info->pushConstantRangeCount > 0 ? info->pPushConstantRanges[0].size : 0; + if (info->setLayoutCount != 0) mock.pipelineLayoutSets[*layout].assign(info->pSetLayouts, info->pSetLayouts + info->setLayoutCount); + mock.pipelineLayoutFlags[*layout] = info->flags; + if (info->pushConstantRangeCount != 0) mock.pipelineLayoutPushRanges[*layout].assign(info->pPushConstantRanges, info->pPushConstantRanges + info->pushConstantRangeCount); + ++mock.live; + return VK_SUCCESS; +} + +VKAPI_ATTR VkResult VKAPI_CALL mockCreateGraphicsPipelines(VkDevice, VkPipelineCache, std::uint32_t count, const VkGraphicsPipelineCreateInfo* infos, const VkAllocationCallbacks*, VkPipeline* pipelines) { + { + 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; + *pipelines = makeHandle(); ++mock.live; return VK_SUCCESS; } @@ -1413,8 +4702,8 @@ VKAPI_ATTR VkResult VKAPI_CALL mockCreateShaderModule(VkDevice, const VkShaderMo return VK_SUCCESS; } -VKAPI_ATTR void VKAPI_CALL mockDestroyShaderModule(VkDevice, VkShaderModule, const VkAllocationCallbacks*) { - --mock.live; +VKAPI_ATTR void VKAPI_CALL mockDestroyShaderModule(VkDevice, VkShaderModule module, const VkAllocationCallbacks*) { + if (module != VK_NULL_HANDLE) --mock.live; } VKAPI_ATTR VkResult VKAPI_CALL mockCreateComputePipelines(VkDevice, VkPipelineCache, std::uint32_t count, const VkComputePipelineCreateInfo* infos, const VkAllocationCallbacks*, VkPipeline* pipelines) { @@ -1450,13 +4739,54 @@ VKAPI_ATTR void VKAPI_CALL mockCmdDispatch(VkCommandBuffer, std::uint32_t x, std } VKAPI_ATTR void VKAPI_CALL mockCmdUpdateBuffer(VkCommandBuffer, VkBuffer buffer, VkDeviceSize offset, VkDeviceSize size, const void* data) { - auto& memory = mock.memories.at(mock.bufferMemory.at(buffer)); + auto memory = mockBufferMemory(buffer); Require(offset + size <= memory.size(), "a buffer update exceeds its buffer"); std::memcpy(memory.data() + offset, data, static_cast(size)); } +VKAPI_ATTR VkResult VKAPI_CALL mockAllocateCommandBuffers(VkDevice, const VkCommandBufferAllocateInfo* info, VkCommandBuffer* commands) { + for (std::uint32_t index = 0; index < info->commandBufferCount; ++index) commands[index] = makeHandle(); + mock.live += info->commandBufferCount; + return VK_SUCCESS; +} + +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.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) {} +VKAPI_ATTR void VKAPI_CALL mockCmdResetQueryPool(VkCommandBuffer, VkQueryPool, std::uint32_t, std::uint32_t) {} +VKAPI_ATTR void VKAPI_CALL mockCmdCopyQueryPoolResults(VkCommandBuffer, VkQueryPool, std::uint32_t, std::uint32_t, VkBuffer, VkDeviceSize, VkDeviceSize, VkQueryResultFlags) {} + PFN_vkVoidFunction VKAPI_CALL mockProc(VkDevice, const char* name) { static const std::map table{ + {"vkAllocateCommandBuffers", reinterpret_cast(mockAllocateCommandBuffers)}, + {"vkFreeCommandBuffers", reinterpret_cast(mockFreeCommandBuffers)}, + {"vkCreateFence", reinterpret_cast(mockCreateFence)}, + {"vkDestroyFence", reinterpret_cast(mockDestroyFence)}, + {"vkGetFenceStatus", reinterpret_cast(mockGetFenceStatus)}, + {"vkResetFences", reinterpret_cast(mockResetFences)}, + {"vkWaitForFences", reinterpret_cast(mockWaitForFences)}, + {"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)}, + {"vkCmdResetQueryPool", reinterpret_cast(mockCmdResetQueryPool)}, + {"vkCmdCopyQueryPoolResults", reinterpret_cast(mockCmdCopyQueryPoolResults)}, + {"vkGetBufferDeviceAddressKHR", reinterpret_cast(mockGetBufferDeviceAddress)}, {"vkGetMemoryHostPointerPropertiesEXT", reinterpret_cast(mockGetMemoryHostPointerProperties)}, {"vkCreateBuffer", reinterpret_cast(mockCreateBuffer)}, @@ -1473,8 +4803,12 @@ PFN_vkVoidFunction VKAPI_CALL mockProc(VkDevice, const char* name) { {"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)}, {"vkCreatePipelineLayout", reinterpret_cast(mockCreatePipelineLayout)}, {"vkDestroyPipelineLayout", reinterpret_cast(mockDestroyPipelineLayout)}, {"vkCreateShaderModule", reinterpret_cast(mockCreateShaderModule)}, @@ -1490,6 +4824,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; @@ -1502,10 +4851,349 @@ 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"); +#else + setenv("APS5_NO_LEASE_MIRROR", "1", 1); +#endif + auto context = mockContext(); + context.device = makeHandle(); + context.bufferDeviceAddress = true; + context.limits.maxStorageBufferRange = 1u << 27; + void* block = GuestHeap::GuestHeapAlignForGpu_nid_postfix(16, 64); + const auto before = AgcDriver::Graphics::LeaseCounters().cacheDrops; + { + AgcDriver::Graphics::GuestBufferMemory memory(context); + memory.AcquireRegistered(); + Require(memory.HoldsLease(), "address-based build holds no lease before device teardown"); + memory.Upload(true); + memory.WriteBack(); + Require(!memory.HoldsLease(), "completed address-based build kept its own lease"); + } + AgcDriver::Graphics::ClearImageMirrors(context.device); + GuestHeap::GuestHeapFree_nid_postfix(block); + Require(AgcDriver::Graphics::LeaseCounters().cacheDrops == before, "device teardown left the cached address space pinning guest allocations"); + Require(mock.live == 0, "address-space teardown test leaked Vulkan objects"); +} + using Role = ShaderRecompiler::DescriptorRole; + +void bufferBudgetPlacementTests() { + using namespace AgcDriver::Graphics; + for (const std::size_t bytes : {std::size_t{512}, std::size_t{3} << 20u}) { + for (const bool fail : {false, true}) { + mock = MockVulkan{}; + mock.bufferMemoryTypeBits = 3; + if (fail) mock.memoryLimit = 0; + { + auto context = mockContext(); + constexpr auto coherent = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + context.memory.memoryTypeCount = 2; + context.memory.memoryHeapCount = 2; + context.memory.memoryHeaps[0] = {4u << 20u, 0}; + context.memory.memoryHeaps[1] = {8u << 20u, 0}; + context.memory.memoryTypes[0] = {coherent | VK_MEMORY_PROPERTY_HOST_CACHED_BIT, 0}; + context.memory.memoryTypes[1] = {coherent, 1}; + context.physical = reinterpret_cast(std::uintptr_t{1}); + context.memoryProperties2 = [](VkPhysicalDevice, VkPhysicalDeviceMemoryProperties2* properties) { + auto* reported = static_cast(properties->pNext); + reported->heapBudget[0] = 1u << 20u; + reported->heapUsage[0] = reported->heapBudget[0] - 1024u; + reported->heapBudget[1] = 8u << 20u; + }; + const auto allocate = [&] { + Buffer buffer(context, bytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Require(buffer.Mapped() && buffer.Bytes().size() == bytes, "a budget-selected buffer lost its mapping or size"); + buffer.Bytes().front() = std::byte{0x5a}; + Require(mockBufferMemory(buffer.Handle()).front() == std::byte{0x5a}, "a budget-selected buffer bound the wrong allocation"); + }; + if (fail) expectFailure(allocate, "Vulkan result -2"); + else allocate(); + Require(mock.allocationTypes == std::vector{1} && mock.allocationAttempts == 1, "a coherent buffer did not select the other heap before its single allocation attempt"); + if (!fail) Require(mock.allocatedBytes == (bytes == 512 ? BufferPool::SlabBlockBytes(512) : bytes), "a coherent buffer budget check lost its full allocation extent"); + } + Require(mock.live == 0, "a budget-selected buffer leaked Vulkan objects"); + } + } +} + +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}; @@ -1540,7 +5228,7 @@ std::vector ShaderDataWords(std::initializer_list return words; } -bool sameBytes(const std::vector& memory, const void* expected, std::size_t bytes) { +bool sameBytes(std::span memory, const void* expected, std::size_t bytes) { return memory.size() >= bytes && std::memcmp(memory.data(), expected, bytes) == 0; } @@ -1558,8 +5246,8 @@ const VkDescriptorSetLayoutBinding& findLayoutBinding(std::uint32_t binding) { throw std::runtime_error("expected descriptor set layout binding is missing for binding " + std::to_string(binding)); } -const std::vector& bufferBytes(VkBuffer buffer) { - return mock.memories.at(mock.bufferMemory.at(buffer)); +std::span bufferBytes(VkBuffer buffer) { + return mockBufferMemory(buffer); } void expectResourceFailure(const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment, std::string_view reason) { @@ -1582,12 +5270,152 @@ 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{}; + { + AgcDriver::Graphics::DescriptorCache cache(mockContext()); + const VkDescriptorSetLayoutBinding binding{3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}; + const std::array key{3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2, VK_SHADER_STAGE_COMPUTE_BIT}; + const std::array flags{indexed}; + const auto layout = cache.Layout(key, std::span(&binding, 1), false, flags); + Require(mock.layoutFlags == 0 && mock.layoutBindingFlags == std::vector{indexed}, "native layout lost binding flags without update-after-bind"); + Require(cache.Layout(key, std::span(&binding, 1), false, flags) == layout, "identical binding flags missed the layout cache"); + Require(cache.Layout(key, std::span(&binding, 1)) != layout && mock.layoutBindingFlags.empty(), "layout cache ignored binding flags"); + } + Require(mock.live == 0, "binding flag layout cache leaked Vulkan objects"); + for (const bool updateAfterBind : {false, true}) { + mock = MockVulkan{}; + auto context = mockContext(); + context.emptyBuffer = reinterpret_cast(std::uintptr_t{0x1000}); + context.descriptorIndexingLimits.maxPerStageDescriptorUpdateAfterBindStorageBuffers = 17; + context.descriptorIndexingLimits.maxPerStageUpdateAfterBindResources = 128; + context.descriptorIndexingLimits.maxDescriptorSetUpdateAfterBindStorageBuffers = 32; + { + ShaderRecompiler::RecompileResult program; + const std::uint32_t count = updateAfterBind ? 17 : 1; + program.bindings.push_back(makeBinding(Role::GuestBuffers, 0, count, std::vector(4 * count))); + const AgcDriver::Graphics::CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + AgcDriver::Graphics::ShaderResources resources(context, compute); + const auto flags = resources.LayoutBindingFlags(); + Require(flags.size() == resources.LayoutBindings().size() && flags.size() == 1, "exported binding flag count differs from native layout"); + Require(flags[0] == (updateAfterBind ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0), "exported binding flags differ from native layout"); + Require(updateAfterBind ? mock.layoutBindingFlags == std::vector(flags.begin(), flags.end()) : mock.layoutBindingFlags.empty(), "native layout did not use the exported binding flag vector"); + } + Require(mock.live == 0, "exported binding flag fixture leaked Vulkan objects"); + } +} + +void descriptorPoolTests() { + using namespace AgcDriver::Graphics; + mock = MockVulkan{}; + mock.descriptorPoolLimit = 2; + { + DescriptorCache cache(mockContext()); + 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 auto layout = cache.Layout(key, std::span(&binding, 1)); + const VkDescriptorPoolSize size{binding.descriptorType, binding.descriptorCount}; + const auto allocate = [&] { return cache.Allocate(layout, std::span(&size, 1)); }; + const auto first = allocate(); + const auto second = allocate(); + Require(first.pool == second.pool, "descriptor allocations did not share a pool"); + Require(mock.descriptorPools.at(first.pool).flags == 0, "descriptor pools use the individually freed allocation path"); + const auto third = allocate(); + const auto fourth = allocate(); + const auto fifth = allocate(); + Require(third.pool == fourth.pool && third.pool != first.pool && fifth.pool != third.pool, "exhausted descriptor pools did not advance the chain"); + Require(mock.descriptorPools.at(first.pool).attempts == 3, "an exhausted descriptor pool was retried before reset"); + cache.Free(first); + Require(mock.descriptorPools.at(first.pool).resets == 0, "a descriptor pool reset while another set was live"); + cache.Free(second); + Require(mock.descriptorPools.at(first.pool).resets == 1, "an empty descriptor pool was not reset"); + const auto sixth = allocate(); + const auto seventh = allocate(); + Require(sixth.pool == fifth.pool && seventh.pool == first.pool && cache.Counters().pools == 3, "an empty descriptor pool was not recycled"); + cache.Free(third); + cache.Free(fourth); + cache.Free(fifth); + cache.Free(sixth); + cache.Free(seventh); + Require(mock.descriptorPools.at(first.pool).resets == 2 && mock.descriptorPools.at(third.pool).resets == 1 && mock.descriptorPools.at(fifth.pool).resets == 1, "descriptor pool ownership was lost after recycling"); + } + Require(mock.live == 0 && mock.descriptorPools.empty(), "descriptor pool cache leaked Vulkan objects"); +} + +void descriptorPoolResetFailureTest() { + using namespace AgcDriver::Graphics; + mock = MockVulkan{}; + DescriptorCache cache(mockContext()); + 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 auto layout = cache.Layout(key, std::span(&binding, 1)); + const VkDescriptorPoolSize size{binding.descriptorType, binding.descriptorCount}; + const auto allocation = cache.Allocate(layout, std::span(&size, 1)); + mock.descriptorResetResult = VK_ERROR_DEVICE_LOST; + const auto previous = _ZSt13set_terminatePFvvE_nid_postfix([] { + std::_Exit(mock.descriptorResetResult == VK_SUCCESS ? 0 : 1); + }); + cache.Free(allocation); + _ZSt13set_terminatePFvvE_nid_postfix(previous); +} + void pushConstantTests() { ShaderRecompiler::RecompileResult vertex; ShaderRecompiler::RecompileResult fragment; @@ -1596,11 +5424,16 @@ void pushConstantTests() { std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, 8}}}; Require(AgcDriver::Graphics::PushConstantStages(shaders) == (VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT), "push constant stage union changed"); const auto bytes = AgcDriver::Graphics::AssemblePushConstants(shaders); - Require(bytes.size() == 128 && bytes[0] == std::byte{1} && bytes[7] == std::byte{1} && bytes[8] == std::byte{2} && bytes[19] == std::byte{2} && bytes[20] == std::byte{0} && bytes[127] == std::byte{0}, "assembled push constants are misplaced"); + Require(bytes.size() == AgcDriver::Graphics::PipelinePushConstantBytes && bytes[0] == std::byte{1} && bytes[7] == std::byte{1} && bytes[8] == std::byte{2} && bytes[19] == std::byte{2} && bytes[20] == std::byte{0} && bytes[127] == std::byte{0} && bytes.back() == std::byte{0}, "assembled push constants are misplaced"); shaders[1].pushConstantOffset = 4; expectFailure([&] { AgcDriver::Graphics::AssemblePushConstants(shaders); }, "overlap"); shaders[1].pushConstantOffset = 120; + expectFailure([&] { AgcDriver::Graphics::AssemblePushConstants(shaders); }, "crosses a push constant slot"); + shaders[1].pushConstantOffset = AgcDriver::Graphics::PipelinePushConstantBytes - 4; expectFailure([&] { AgcDriver::Graphics::AssemblePushConstants(shaders); }, "outside the pipeline push constant block"); + shaders[1].pushConstantOffset = AgcDriver::Graphics::PipelinePushSlotBytes; + const auto slotted = AgcDriver::Graphics::AssemblePushConstants(shaders); + Require(slotted[128] == std::byte{2} && slotted[139] == std::byte{2} && slotted[8] == std::byte{0}, "a pixel shader in its push slot was misplaced"); shaders[1].pushConstantOffset = 2; expectFailure([&] { AgcDriver::Graphics::AssemblePushConstants(shaders); }, "DWORD aligned"); shaders[1].pushConstantOffset = 8; @@ -1609,11 +5442,224 @@ 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"); } +void descriptorSnapshotTests() { + using AgcDriver::Graphics::DescriptorCache; + using AgcDriver::Graphics::Recorder; + using AgcDriver::Graphics::ShaderResources; + for (const unsigned selected : {1u, 2u, 3u}) { + 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 vertex, fragment; + auto binding = makeBinding(Role::GuestBuffers, 0, 2, join(vsharp(guestFirst.data(), 16), vsharp(guestSecond.data(), 32))); + binding.bufferWritten = {false, false}; + vertex.bindings.push_back(binding); + fragment.bindings.push_back(makeBinding(Role::GuestBuffers, 4, 1, vsharp(guestThird.data(), 8))); + const auto state = AgcDriver::Graphics::DecodeState(makeState()); + ShaderResources resources(context, vertex, fragment, state.color, 0, 0); + 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; + 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); + Require(snapshots != nullptr && snapshots->snapshots.size() == moved.size(), "draw snapshots are missing"); + Require(mock.copies.empty(), "snapshot descriptor set must be written without descriptor copies"); + Require(mock.writes.size() == 2, "snapshot update must write every binding once"); + const auto& write = mock.writes.front(); + const auto& untouched = mock.writes.back(); + Require(untouched.binding == 4 && untouched.count == 1 && untouched.buffers[0].buffer == originalThird[0].buffer && untouched.buffers[0].offset == originalThird[0].offset && untouched.buffers[0].range == originalThird[0].range, "snapshot update changed a binding without snapshots"); + Require(write.binding == 0 && write.arrayElement == 0 && write.count == 2, "snapshot update did not start from array element zero"); + std::size_t snapshot = 0; + for (std::size_t index = 0; index < 2; ++index) { + const auto& descriptor = write.buffers[index]; + if ((selected & (1u << index)) == 0) { + Require(descriptor.buffer == original[index].buffer && descriptor.offset == original[index].offset && descriptor.range == original[index].range, "snapshot update changed an untouched buffer descriptor"); + } else { + const auto& item = snapshots->snapshots[snapshot]; + Require(descriptor.buffer == item.buffer->Handle() && descriptor.offset == 0 && descriptor.range == moved[snapshot].size, "snapshot descriptor used another element's buffer or range"); + Require(sameBytes(bufferBytes(descriptor.buffer), moved[snapshot].words.data(), moved[snapshot].size), "snapshot descriptor lost its buffer data"); + ++snapshot; + } + } + recorder.Sync(); + } + Require(mock.live == 0, "snapshot descriptor resources leaked Vulkan objects"); + } +} + +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(); @@ -1646,9 +5692,9 @@ void resourceTests() { Require(findWrite(44).buffers.size() == 1 && findWrite(44).buffers[0].range == 8 && sameBytes(bufferBytes(findWrite(44).buffers[0].buffer), guestThird.data(), 8), "fragment guest buffer is incorrect"); resources.Bind(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, VK_NULL_HANDLE); Require(mock.boundPoint == VK_PIPELINE_BIND_POINT_GRAPHICS && mock.boundFirst == 0 && mock.boundSets == 1, "exactly one descriptor set must be bound at set zero"); - auto& first = mock.memories.at(mock.bufferMemory.at(array.buffers[0].buffer)); + auto first = mockBufferMemory(array.buffers[0].buffer); std::memset(first.data(), 0xab, 16); - auto& shaderData = mock.memories.at(mock.bufferMemory.at(findWrite(5).buffers[0].buffer)); + auto shaderData = mockBufferMemory(findWrite(5).buffers[0].buffer); std::memset(shaderData.data(), 0xcd, 12); resources.WriteBack(); Require(guestFirst[0] == 0xabababab && guestFirst[3] == 0xabababab, "guest buffer was not written back"); @@ -1676,7 +5722,7 @@ void resourceTests() { resources.Bind(commands, VK_PIPELINE_BIND_POINT_COMPUTE, VK_NULL_HANDLE); Require(mock.boundPoint == VK_PIPELINE_BIND_POINT_COMPUTE && mock.boundSets == 1, "compute descriptors were bound to the wrong bind point"); guestThird = {0xaaaaaaaa, 0xbbbbbbbb}; - std::memset(mock.memories.at(mock.bufferMemory.at(findWrite(3).buffers[0].buffer)).data(), 0x5a, 8); + std::memset(mockBufferMemory(findWrite(3).buffers[0].buffer).data(), 0x5a, 8); resources.WriteBack(); Require(guestThird[0] == 0x5a5a5a5a && guestThird[1] == 0x5a5a5a5a, "compute buffer was not written back"); guestThird = {0xaaaaaaaa, 0xbbbbbbbb}; @@ -1695,7 +5741,7 @@ void resourceTests() { Require(buffer.range == sizeof(guestSecond), "strided buffer range does not cover every record"); Require(sameBytes(bufferBytes(buffer.buffer), guestSecond.data(), sizeof(guestSecond)), "strided buffer contents were not uploaded"); const std::uint32_t changed = 0x12345678u; - auto& bytes = mock.memories.at(mock.bufferMemory.at(buffer.buffer)); + auto bytes = mockBufferMemory(buffer.buffer); std::memcpy(bytes.data() + 16, &changed, sizeof(changed)); resources.WriteBack(); Require(guestSecond[4] == changed && guestSecond[0] == 1 && guestSecond[7] == 8, "strided buffer write back changed the wrong record"); @@ -1733,12 +5779,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"); @@ -1790,6 +5846,40 @@ 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; + fragment.bindings.push_back(makeBinding(Role::GuestBuffers, 0, 2, join(vsharp(guestFirst.data(), 16), vsharp(guestSecond.data(), 32)))); + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}}; + auto limited = mockContext(); + limited.limits.maxPerStageResources = 2; + const auto expectAttachments = [&](std::uint32_t attachments, bool accepted, std::string_view what) { + mock = MockVulkan{}; + if (accepted) { + AgcDriver::Graphics::ShaderResources resources(limited, shaders, state.color, attachments, 0, 0); + } else { + expectFailure([&] { AgcDriver::Graphics::ShaderResources resources(limited, shaders, state.color, attachments, 0, 0); }, "shader descriptors exceed per-stage limits"); + } + Require(mock.live == 0, std::string(what) + " leaked Vulkan objects"); + }; + expectAttachments(0, true, "a pixel stage at the per-stage limit"); + expectAttachments(1, false, "a pixel stage over the per-stage limit with its colour attachment"); + std::swap(vertex.bindings, fragment.bindings); + expectAttachments(1, true, "a vertex stage at the per-stage limit"); + expectAttachments(3, false, "colour attachments over the per-stage limit"); + } { ShaderRecompiler::RecompileResult vertex; ShaderRecompiler::RecompileResult fragment; @@ -1824,20 +5914,476 @@ 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 && mock.freedSets == 0, "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 == VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT && cache.Counters().pools == 1, "a set the chain pool holds left the chain"); + 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.freedSets == 1 && mock.live == live + 2, "a chain set was not freed back to its pool"); + 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(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); + Require(mock.descriptorPools.at(chained.pool).resets == 2, "a chain pool was not reset after the oversized set was freed"); const std::array beyondDevice{{{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 8193}}}; expectFailure([&] { cache.Allocate(large, beyondDevice); }, "descriptor set exceeds the device's per-set descriptor limit"); const std::array storageImages{{{VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1}}}; 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() { @@ -1885,6 +6431,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; @@ -1953,6 +6567,11 @@ struct ModuleShape { bool fragDepth = false; std::uint32_t sampleMaskLength = 0; std::uint32_t floatControlsWidth = 0; + std::uint32_t clipDistanceLength = 0; + std::uint32_t cullDistanceLength = 0; + bool layerOutput = false; + bool terminateInvocation = false; + bool omitTerminateExtension = false; }; void emit(std::vector& out, spv::Op op, std::initializer_list operands) { @@ -1991,6 +6610,14 @@ std::vector makeModule(const ModuleShape& shape) { emit(annotations, spv::OpDecorate, {parameter, spv::DecorationLocation, shape.parameterLocation}); extraInterface.push_back(parameter); } + if (shape.layerOutput) { + const auto pointer = id(); + const auto layer = id(); + emit(declarations, spv::OpTypePointer, {pointer, spv::StorageClassOutput, uintType}); + emit(declarations, spv::OpVariable, {pointer, layer, spv::StorageClassOutput}); + emit(annotations, spv::OpDecorate, {layer, spv::DecorationBuiltIn, spv::BuiltInLayer}); + extraInterface.push_back(layer); + } if (shape.secondTarget) { const auto target = id(); emit(declarations, spv::OpVariable, {outputPointer, target, spv::StorageClassOutput}); @@ -2052,6 +6679,30 @@ std::vector makeModule(const ModuleShape& shape) { emit(annotations, spv::OpDecorate, {variable, spv::DecorationBuiltIn, spv::BuiltInSampleMask}); extraInterface.push_back(variable); } + if (shape.clipDistanceLength != 0) { + const auto length = id(); + const auto array = id(); + const auto pointer = id(); + const auto variable = id(); + emit(declarations, spv::OpConstant, {uintType, length, shape.clipDistanceLength}); + emit(declarations, spv::OpTypeArray, {array, floatType, length}); + emit(declarations, spv::OpTypePointer, {pointer, spv::StorageClassOutput, array}); + emit(declarations, spv::OpVariable, {pointer, variable, spv::StorageClassOutput}); + emit(annotations, spv::OpDecorate, {variable, spv::DecorationBuiltIn, spv::BuiltInClipDistance}); + extraInterface.push_back(variable); + } + if (shape.cullDistanceLength != 0) { + const auto length = id(); + const auto array = id(); + const auto pointer = id(); + const auto variable = id(); + emit(declarations, spv::OpConstant, {uintType, length, shape.cullDistanceLength}); + emit(declarations, spv::OpTypeArray, {array, floatType, length}); + emit(declarations, spv::OpTypePointer, {pointer, spv::StorageClassOutput, array}); + emit(declarations, spv::OpVariable, {pointer, variable, spv::StorageClassOutput}); + emit(annotations, spv::OpDecorate, {variable, spv::DecorationBuiltIn, spv::BuiltInCullDistance}); + extraInterface.push_back(variable); + } if (shape.perVertex) { const auto length = id(); const auto array = id(); @@ -2115,12 +6766,23 @@ 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}); if (shape.sampleId) emit(words, spv::OpCapability, {spv::CapabilitySampleRateShading}); if (shape.layer) emit(words, spv::OpCapability, {spv::CapabilityGeometry}); + if (shape.clipDistanceLength != 0) emit(words, spv::OpCapability, {spv::CapabilityClipDistance}); + if (shape.cullDistanceLength != 0) emit(words, spv::OpCapability, {spv::CapabilityCullDistance}); + if (shape.layerOutput) { + emit(words, spv::OpCapability, {spv::CapabilityShaderViewportIndexLayerEXT}); + const std::string extension = "SPV_EXT_shader_viewport_index_layer"; + const auto count = (extension.size() + 4) / 4; + words.push_back((static_cast(count + 1) << 16u) | spv::OpExtension); + const auto start = words.size(); + words.resize(start + count, 0); + for (std::size_t i = 0; i < extension.size(); ++i) words[start + i / 4] |= static_cast(static_cast(extension[i])) << ((i % 4) * 8); + } if (shape.barycentric) { emit(words, spv::OpCapability, {spv::CapabilityFragmentBarycentricKHR}); const std::string extension = "SPV_KHR_fragment_shader_barycentric"; @@ -2130,6 +6792,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}); @@ -2148,6 +6818,1052 @@ 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}) { + mock = MockVulkan{}; + auto context = mockContext(); + context.graphicsPipelineLibrary = true; + context.limits.framebufferNoAttachmentsSampleCounts = VK_SAMPLE_COUNT_1_BIT; + State state{}; + ShaderRecompiler::RecompileResult vertex, fragment; + vertex.spirv = makeModule({}); + fragment.spirv = makeModule({.fragment = true}); + vertex.variantId = 1; + fragment.variantId = 2; + for (std::uint32_t set = 0; set < 2; ++set) { + ShaderRecompiler::DescriptorBinding binding{}; + binding.descriptorSet = set; + if ((mask & (1u << set)) != 0u) fragment.bindings.push_back(binding); + else vertex.bindings.push_back(binding); + } + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, PipelinePushSlotBytes}}}; + std::array mainKey{11}, fragmentKey{22}; + PipelineSetLayouts resources{makeHandle(), makeHandle(), true, mainKey, fragmentKey}; + const auto prebuild = [&] { Require(Pipeline::Prebuild(context, state, VertexInputLayout{}, resources, shaders), "graphics pipeline library prebuild was skipped"); }; + prebuild(); + for (const auto part : {VK_GRAPHICS_PIPELINE_LIBRARY_PRE_RASTERIZATION_SHADERS_BIT_EXT, VK_GRAPHICS_PIPELINE_LIBRARY_FRAGMENT_SHADER_BIT_EXT}) { + const auto layout = mock.libraryLayouts.at(part); + const auto expectedMask = part == VK_GRAPHICS_PIPELINE_LIBRARY_FRAGMENT_SHADER_BIT_EXT ? mask : mask ^ 3u; + const std::vector expected{(expectedMask & 1u) != 0u ? resources.main : VK_NULL_HANDLE, (expectedMask & 2u) != 0u ? resources.fragment : VK_NULL_HANDLE}; + Require(mock.pipelineLayoutSets.at(layout) == expected, "shader library lost a used set or retained an unused set"); + Require(mock.pipelineLayoutFlags.at(layout) == VK_PIPELINE_LAYOUT_CREATE_INDEPENDENT_SETS_BIT_EXT, "shader library lost independent sets"); + const auto& pushes = mock.pipelineLayoutPushRanges.at(layout); + Require(pushes.size() == 2 && pushes[0].offset == 0 && pushes[0].size == PipelinePushSlotBytes && pushes[0].stageFlags == VK_SHADER_STAGE_VERTEX_BIT && pushes[1].offset == PipelinePushSlotBytes && pushes[1].size == PipelinePushSlotBytes && pushes[1].stageFlags == VK_SHADER_STAGE_FRAGMENT_BIT, "shader library changed push constant slots"); + } + auto before = PipelineLibraryCountersOf(context.device).built; + prebuild(); + Require(PipelineLibraryCountersOf(context.device).built == before, "identical shader libraries were rebuilt"); + for (std::uint32_t set = 0; set < 2; ++set) { + if (set == 0) { ++mainKey[0]; resources.main = makeHandle(); } + else { ++fragmentKey[0]; resources.fragment = makeHandle(); } + prebuild(); + const auto after = PipelineLibraryCountersOf(context.device).built; + Require(after[0] == before[0] && after[3] == before[3], "descriptor layout change rebuilt interface libraries"); + Require(after[1] == before[1] + ((mask & (1u << set)) == 0u ? 1u : 0u), "pre-rasterization library reuse ignored descriptor set usage"); + Require(after[2] == before[2] + ((mask & (1u << set)) != 0u ? 1u : 0u), "fragment library reuse ignored descriptor set usage"); + before = after; + } + ShaderRecompiler::DescriptorBinding invalid{}; + invalid.descriptorSet = 32; + fragment.bindings.push_back(invalid); + expectFailure(prebuild, "descriptor set exceeds two slots"); + ClearPipelineLibraries(context.device); + Require(mock.live == 0, "shader library tests leaked Vulkan objects"); + } +} + +void 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; + const std::array parameters{{{0u, 0u, false, false}, {1u, 1u, false, true}, {2u, 2u, true, false}}}; + const auto layout = LayoutBarycentricEmulation(parameters, {true, true, true}); + Require(layout.smoothLocation == 1u && layout.linearLocation == 3u && layout.perVertexLocations == std::vector>{{1u, 4u}}, "emulated barycentrics did not take the free locations in order"); + Require(LayoutBarycentricEmulation(parameters, {}).perVertexLocations.empty() && LayoutBarycentricEmulation(parameters, {}).smoothLocation == BarycentricEmulationLayout::NoLocation, "an inactive emulation was given locations"); + std::vector crowded; + for (std::uint32_t location = 0; location < 11u; ++location) crowded.push_back({location, location, false, true}); + expectFailure([&] { static_cast(LayoutBarycentricEmulation(crowded, {true, false, false})); }, "exceed 32 locations"); + + const auto compile = [&](std::span code, bool barycentric, std::uint32_t secondInput = 0xffffffffu) { + auto queue = makeState(); + queue.context[0x1b3] = 0x2u; + queue.context[0x1b4] = 0x2u; + queue.context[0x1b6] = secondInput == 0xffffffffu ? 1u : 2u; + queue.context[0x191] = 0u; + if (secondInput != 0xffffffffu) queue.context[0x192] = secondInput; + RecompileRequest request{}; + request.shader = {ShaderStage::Fragment, 0x30000u, code, 0, {}}; + request.context.waveSize = 64; + request.context.pixel = DecodePixelStageInfo(queue.context, IdentityExports); + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 64; + request.target.fragmentShaderBarycentricEnabled = barycentric; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + return Recompile(request); + }; + const auto decorations = [](const RecompileResult& result, spv::Decoration decoration) { + const auto& words = result.spirv.Words(); + std::set inputs; + 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 + 3] == spv::StorageClassInput) inputs.insert(words[at + 2]); + } + std::vector values; + 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] == decoration && inputs.contains(words[at + 1])) values.push_back((words[at] >> 16u) > 3u ? words[at + 3] : words[at + 1]); + } + return values; + }; + const auto barycentricCapability = [](const RecompileResult& result) { + 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::OpCapability && words[at + 1] == spv::CapabilityFragmentBarycentricKHR) return true; + } + return false; + }; + static constexpr std::array readsIj{0xf800180fu, 0x01000100u, 0xbf810000u}; + static constexpr std::array interpolates{0xc8080000u, 0xc8090001u, 0xf800180fu, 0x02020202u, 0xbf810000u}; + static constexpr std::array readsVertex{0xc80a0000u, 0xf800180fu, 0x02020202u, 0xbf810000u}; + static constexpr std::array shiftsIj{0x060000f0u, 0xc8080000u, 0xc8090001u, 0xf800180fu, 0x02020202u, 0xbf810000u}; + auto fragment = compile(readsIj, false); + Require(fragment.barycentricEmulation.active && fragment.barycentricEmulation.smooth && !fragment.barycentricEmulation.linear, "reading the I/J VGPRs without the extension did not emulate barycentrics"); + const auto builtins = decorations(fragment, spv::DecorationBuiltIn); + Require(std::find(builtins.begin(), builtins.end(), static_cast(spv::BuiltInBaryCoordKHR)) == builtins.end() && !barycentricCapability(fragment), "emulated barycentrics still need VK_KHR_fragment_shader_barycentric"); + Require(decorations(fragment, spv::DecorationLocation) == std::vector{0u}, "the emulated barycentrics were not read from location 0"); + Require(!compile(readsIj, true).barycentricEmulation.active, "a device with the extension emulated barycentrics"); + Require(!compile(interpolates, false).barycentricEmulation.active, "plain v_interp_p1/p2 lost host interpolation"); + Require(compile(shiftsIj, false).barycentricEmulation.active, "v_interp_p1/p2 through a changed I lost exact barycentrics"); + static constexpr std::array overwritesI{0xc8080000u, 0xc8090001u, 0x7e0002f2u, 0xf800180fu, 0x02020002u, 0xbf810000u}; + static constexpr std::array accumulatesInI{0xc8000000u, 0xc8010001u, 0xf800180fu, 0x00000000u, 0xbf810000u}; + for (const auto& code : {std::span(overwritesI), std::span(accumulatesInI)}) { + const auto reused = compile(code, false); + const auto reusedBuiltins = decorations(reused, spv::DecorationBuiltIn); + Require(!reused.barycentricEmulation.active && !barycentricCapability(reused) && std::find(reusedBuiltins.begin(), reusedBuiltins.end(), static_cast(spv::BuiltInBaryCoordKHR)) == reusedBuiltins.end(), "reusing the I/J VGPRs after v_interp_p1/p2 lost host interpolation"); + } + static constexpr std::array widensExec{0xbefe0381u, 0xc8080000u, 0xbefe03c1u, 0xc8090001u, 0xf800180fu, 0x02020202u, 0xbf810000u}; + Require(compile(widensExec, false).barycentricEmulation.active, "v_interp_p2_f32 under a wider EXEC than its v_interp_p1_f32 lost exact barycentrics"); + static constexpr std::array sharedSlot{0xc8080000u, 0xc8090001u, 0xc80e0402u, 0xf800180fu, 0x03030202u, 0xbf810000u}; + const auto shared = compile(sharedSlot, false, 0x400u); + Require(shared.barycentricEmulation.active && !barycentricCapability(shared), "inputs sharing a slot with different interpolation did not emulate per-vertex parameters"); + fragment = compile(readsVertex, false); + Require(fragment.barycentricEmulation.active && !fragment.barycentricEmulation.smooth && fragment.fragmentParameters.size() == 1u && fragment.fragmentParameters[0].perVertex, "v_interp_mov of P10 did not emulate per-vertex parameters"); + Require(decorations(fragment, spv::DecorationLocation) == std::vector{0u} && decorations(fragment, spv::DecorationPerVertexKHR).empty() && !decorations(fragment, spv::DecorationFlat).empty() && !barycentricCapability(fragment), "the emulated per-vertex parameter was not a flat array at the first free location"); + + RecompileResult vertex; + vertex.spirv = makeModule({.parameterOutput = true}); + vertex.parameterExports = {0u}; + const std::array capabilities{spv::CapabilityShader, spv::CapabilityGeometry}; + SpirvTarget target{}; + target.vulkanVersion = VK_API_VERSION_1_1; + target.spirvVersion = 0x00010300u; + target.supportedCapabilities = capabilities; + const GeometryStageLimits limits{64u, 128u, 256u, 1024u, 128u}; + auto geometry = BuildBarycentricGeometryShader(vertex, fragment, target, limits); + const std::array shaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::Geometry, &geometry, 0}, {ShaderStage::Fragment, &fragment, 0}}}; + const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; + auto state = DecodeState(makeState()); + Require(state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, "the reference state is not a triangle list"); + ValidateShaders(shaders, state, subgroup, false, false, false, true); + state.topology = VK_PRIMITIVE_TOPOLOGY_POINT_LIST; + expectFailure([&] { ValidateShaders(shaders, state, subgroup, false, false, false, true); }, "triangle primitives"); + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; + ValidateShaders(shaders, state, subgroup, false, false, false, true); + const auto plain = compile(interpolates, false); + const std::array plainShaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::Geometry, &geometry, 0}, {ShaderStage::Fragment, &plain, 0}}}; + expectFailure([&] { ValidateShaders(plainShaders, state, subgroup, false, false, false, true); }, "only for emulated barycentrics"); + expectFailure([&] { static_cast(BuildBarycentricGeometryShader(vertex, plain, target, limits)); }, "does not emulate barycentrics"); + expectFailure([&] { static_cast(BuildBarycentricGeometryShader(vertex, fragment, target, std::nullopt)); }, "geometry shaders are unavailable"); + expectFailure([&] { static_cast(BuildBarycentricGeometryShader(vertex, fragment, target, GeometryStageLimits{64u, 12u, 256u, 1024u, 128u})); }, "exceeds device limits"); + + const std::array tessellationCapabilities{spv::CapabilityShader, spv::CapabilityTessellation}; + SpirvTarget tessellationTarget = target; + tessellationTarget.supportedCapabilities = tessellationCapabilities; + tessellationTarget.tessellation = TessellationTargetLimits{32u, 128u, 128u, 120u, 4096u, 128u, 128u}; + const auto modes = [](const RecompileResult& result) { + std::vector values; + const auto& words = result.spirv.Words(); + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + if (static_cast(words[at] & 0xffffu) == spv::OpExecutionMode) values.push_back(words[at + 2]); + } + return values; + }; + auto patches = BuildBarycentricTessellationShaders(vertex, fragment, tessellationTarget, 128u); + Require(modes(patches.control) == std::vector{spv::ExecutionModeOutputVertices, spv::ExecutionModeTriangles, spv::ExecutionModeSpacingEqual, spv::ExecutionModeVertexOrderCw}, "the generated control stage does not declare three output vertices and the triangle domain"); + Require(modes(patches.evaluation) == std::vector{spv::ExecutionModeTriangles, spv::ExecutionModeSpacingEqual, spv::ExecutionModeVertexOrderCw}, "the generated evaluation stage does not declare the triangle domain"); + const std::array patchShaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::TessellationControl, &patches.control, 0}, {ShaderStage::TessellationEvaluation, &patches.evaluation, 0}, {ShaderStage::Fragment, &fragment, 0}}}; + Require(BarycentricPatches(state, patchShaders) && !BarycentricPatches(state, shaders), "generated tessellation stages were not recognised as barycentric patches"); + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + ValidateShaders(patchShaders, state, subgroup, false); + for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, VK_PRIMITIVE_TOPOLOGY_POINT_LIST}) { + state.topology = topology; + expectFailure([&] { ValidateShaders(patchShaders, state, subgroup, false); }, "require a triangle list"); + } + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + const std::array plainPatchShaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::TessellationControl, &patches.control, 0}, {ShaderStage::TessellationEvaluation, &patches.evaluation, 0}, {ShaderStage::Fragment, &plain, 0}}}; + expectFailure([&] { ValidateShaders(plainPatchShaders, state, subgroup, false); }, "stage count"); + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, plain, tessellationTarget, 128u)); }, "does not emulate barycentrics"); + auto noTessellation = tessellationTarget; + noTessellation.tessellation.reset(); + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, noTessellation, 128u)); }, "tessellation shaders are unavailable"); + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, target, 128u)); }, "tessellation shaders are unavailable"); + auto noCapability = tessellationTarget; + noCapability.supportedCapabilities = std::span(capabilities).first(1); + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, noCapability, 128u)); }, "tessellation capability is unavailable"); + auto smallPatches = tessellationTarget; + smallPatches.tessellation->maxEvaluationOutputComponents = 4u; + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, smallPatches, 128u)); }, "exceeds device limits"); + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, tessellationTarget, 0u)); }, "exceeds device limits"); + + RecompileResult clipped; + clipped.spirv = makeModule({.parameterOutput = true, .clipDistanceLength = 2}); + clipped.parameterExports = {0u}; + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(clipped, fragment, tessellationTarget, 128u)); }, "need shaderClipDistance"); + const std::array clipCapabilities{spv::CapabilityShader, spv::CapabilityTessellation, spv::CapabilityClipDistance}; + auto clipTarget = tessellationTarget; + clipTarget.supportedCapabilities = clipCapabilities; + auto clippedPatches = BuildBarycentricTessellationShaders(clipped, fragment, clipTarget, 128u); + const auto memberBuiltIn = [](const RecompileResult& result, spv::BuiltIn builtin) { + const auto& words = result.spirv.Words(); + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + if (static_cast(words[at] & 0xffffu) == spv::OpMemberDecorate && words[at + 3] == spv::DecorationBuiltIn && words[at + 4] == builtin) return true; + } + return false; + }; + const auto variableBuiltIn = [](const RecompileResult& result, spv::BuiltIn builtin) { + const auto& words = result.spirv.Words(); + std::set variables; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + if (static_cast(words[at] & 0xffffu) == spv::OpVariable) variables.insert(words[at + 2]); + } + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + if (static_cast(words[at] & 0xffffu) == spv::OpDecorate && words[at + 2] == spv::DecorationBuiltIn && words[at + 3] == builtin && variables.contains(words[at + 1])) return true; + } + return false; + }; + Require(!memberBuiltIn(patches.control, spv::BuiltInClipDistance) && !memberBuiltIn(patches.evaluation, spv::BuiltInClipDistance), "a generated tessellation stage declares clip distances the vertex shader does not write"); + Require(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)); }, "not exported by the vertex shader"); + expectFailure([&] { static_cast(BuildBarycentricGeometryShader(vertex, fragment, target, limits)); }, "not exported by the vertex shader"); +} + +void copiedWriterIndirectOverlapTests() { + using namespace ShaderRecompiler; + using namespace AgcDriver::Graphics; + + constexpr std::size_t half = 65536; + + void* directBlock = ::operator new(half, std::align_val_t{half}); + std::memset(directBlock, 0xaa, half); + const auto directAddress = reinterpret_cast(directBlock); + + void* copiedBlock = ::operator new(half, std::align_val_t{half}); + std::memset(copiedBlock, 0xbb, half); + const auto copiedAddress = reinterpret_cast(copiedBlock); + + { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, directBlock, half, true, true, true); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + } + + mock = MockVulkan{}; + auto context = mockContext(); + context.hostImportAlignment = half; + context.limits.minStorageBufferOffsetAlignment = 16; + context.limits.maxStorageBufferRange = 1u << 27; + + { + GuestBufferMemory memory(context); + memory.AcquireRegistered(); + Require(HostImportCovers(context, directAddress, half), "direct registered range must be covered by host import"); + Require(!HostImportCovers(context, copiedAddress, half), "copied unregistered range must NOT be covered by host import"); + + memory.AddWritable(copiedAddress, 1024); + memory.AddWritable(directAddress, 1024); + memory.Upload(false); + + Require(memory.HasCopiedWrites(), "writer with one copied region must report HasCopiedWrites() == true"); + + Require(memory.WritesOverlap(directAddress, 16), "WritesOverlap must return true for direct written region"); + Require(memory.WritesOverlap(copiedAddress, 16), "WritesOverlap must return true for copied written region"); + Require(!memory.WritesOverlap(directAddress + 2048, 16), "WritesOverlap must return false for unwritten range"); + + Require(!memory.CopiedWritesOverlap(directAddress, 16), "CopiedWritesOverlap must return false for direct written region"); + Require(memory.CopiedWritesOverlap(copiedAddress, 16), "CopiedWritesOverlap must return true for copied written region"); + Require(!memory.CopiedWritesOverlap(directAddress + 2048, 16), "CopiedWritesOverlap must return false for unwritten range"); + + memory.WriteBack(); + } + + { + RecompileResult shader; + shader.runtimeAbiVersion = RuntimeAbi::Version; + auto copiedBinding = makeBinding(Role::GuestBuffers, 0, 1, vsharp(copiedBlock, 1024)); + copiedBinding.bufferWritten = {true}; + auto directBinding = makeBinding(Role::GuestBuffers, 1, 1, vsharp(directBlock, 1024)); + directBinding.bufferWritten = {true}; + shader.bindings = {copiedBinding, directBinding}; + CompiledShader compiled{ShaderStage::Compute, &shader, 0}; + auto writer = std::make_shared(context, compiled); + + Require(writer->HasCopiedWrites(), "ShaderResources with copied region must report HasCopiedWrites() == true"); + Require(writer->WritesOverlap(directAddress, 16), "ShaderResources must report WritesOverlap for direct region"); + Require(!writer->CopiedWritesOverlap(directAddress, 16), "ShaderResources must NOT report CopiedWritesOverlap for direct region"); + Require(writer->CopiedWritesOverlap(copiedAddress, 16), "ShaderResources must report CopiedWritesOverlap for copied region"); + + auto copiedWriters = DrawCopiedWriters(); + copiedWriters->push_back(writer); + + const auto overlapsMain = [&](const auto& w) { return w->WritesOverlap(directAddress, 16); }; + const auto overlapsPR = [&](const auto& w) { return w->CopiedWritesOverlap(directAddress, 16); }; + + const bool mainOverlaps = std::any_of(copiedWriters->begin(), copiedWriters->end(), overlapsMain); + const bool prOverlaps = std::any_of(copiedWriters->begin(), copiedWriters->end(), overlapsPR); + + Require(mainOverlaps, "main's check (WritesOverlap) must flag an overlap over direct arguments"); + Require(!prOverlaps, "PR's check (CopiedWritesOverlap) must disambiguate and NOT flag an overlap over direct arguments"); + + const auto copiedOverlapsPR = [&](const auto& w) { return w->CopiedWritesOverlap(copiedAddress, 16); }; + Require(std::any_of(copiedWriters->begin(), copiedWriters->end(), copiedOverlapsPR), "PR's check must flag an overlap over copied arguments"); + + copiedWriters->erase(std::remove(copiedWriters->begin(), copiedWriters->end(), writer), copiedWriters->end()); + writer->WriteBack(); + } + + { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, directBlock); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + } + ::operator delete(directBlock, std::align_val_t{half}); + ::operator delete(copiedBlock, std::align_val_t{half}); +} + +void 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; @@ -2161,6 +7877,10 @@ void rectListTests() { target.spirvVersion = 0x00010300u; target.supportedCapabilities = capabilities; target.tessellation = TessellationTargetLimits{32, 128, 128, 120, 4096, 128, 128}; + vertex.bindings = {{DescriptorKind::StorageBuffer, DescriptorRole::GuestBuffers, 0, 0, 1, {}}}; + const auto faultAuxiliary = BuildRectListShaders(vertex, fragment, target); + Require(faultAuxiliary.control.bindings.size() == 1 && faultAuxiliary.control.bindings[0].role == DescriptorRole::FaultBuffer && faultAuxiliary.control.bindings[0].binding == RuntimeAbi::BindingNumber(RuntimeAbi::Stage::TessellationControl, RuntimeAbi::Binding::FaultBuffer), "the rect-list control shader's fault buffer is not in the tessellation control binding group"); + vertex.bindings.clear(); for (const auto version : {0x00010300u, 0x00010400u}) { target.spirvVersion = version; auto auxiliary = BuildRectListShaders(vertex, fragment, target); @@ -2177,17 +7897,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"); @@ -2207,8 +7945,58 @@ void rectListTests() { queue.userConfig[0x242] = 17; const auto state = DecodeState(queue); const Context context{}; - const AgcDriver::Pm4::DrawParameters draw{0, 4, 0, 1, 0, false}; - expectFailure([&] { Draw(context, state, draw, {}); }, "incomplete rect-list"); + std::array indices{}; + const auto drawFailure = [&](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 = drawFailure(3, indexed); + for (const auto count : {4u, 5u}) Require(drawFailure(count, indexed) == complete, "a rect-list draw with a partial last rectangle was not drawn like its complete rectangles"); + } + Require(drawFailure(0, true).empty(), "an indexed draw without indices was not skipped"); + const AgcDriver::Pm4::DrawParameters noInstances{reinterpret_cast(indices.data()), 3, 2, 0, 0, true}; + Draw(context, state, noInstances, {}); +} + +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) { @@ -2333,6 +8121,46 @@ void conservativeZExportTests() { expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "unsupported fragment execution mode"); } +bool declaresEarlyFragmentTests(const ShaderRecompiler::RecompileRequest& request) { + const auto result = ShaderRecompiler::Recompile(request); + const auto& words = result.spirv.Words(); + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + if (static_cast(words[at] & 0xffffu) == spv::OpExecutionMode && words[at + 2] == spv::ExecutionModeEarlyFragmentTests) return true; + } + return false; +} + +void earlyFragmentTestsTests() { + const std::array, 9> cases{{ + {0x0000u, false}, + {0x0020u, false}, + {0x0010u, true}, + {0x0030u, true}, + {0x0050u, false}, + {0x0011u, false}, + {0x1000u, true}, + {0x1050u, true}, + {0x1031u, true} + }}; + const ShaderRecompiler::RequestSerializer serializer; + for (const auto& [shaderControl, expected] : cases) { + const auto request = depthExportRequest(shaderControl); + const auto replayed = serializer.Deserialize(serializer.Serialize(request)); + Require(declaresEarlyFragmentTests(request) == expected && declaresEarlyFragmentTests(replayed.request) == expected, "DB_SHADER_CONTROL " + std::to_string(shaderControl) + (expected ? " did not declare" : " declared") + " early fragment tests"); + } + for (const auto shaderControl : {0x0010u, 0x1000u}) { + auto request = depthExportRequest(shaderControl); + request.context.pixel->alphaToCoverage = true; + const auto replayed = serializer.Deserialize(serializer.Serialize(request)); + Require(!declaresEarlyFragmentTests(request) && !declaresEarlyFragmentTests(replayed.request), "DB_SHADER_CONTROL " + std::to_string(shaderControl) + " with alpha-to-coverage declared early fragment tests"); + 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"); + } +} + constexpr std::array OrderedPixelCode{0x7e0e02f2u, 0xbf900007u, 0xf800180fu, 0x07070707u, 0xbf810000u}; constexpr std::array OrderedBdaPixelCode{0x7e040f00u, 0x7e060280u, 0xdc208001u, 0x047d0002u, 0xdc308008u, 0x057d0002u, 0xbf8c3f70u, 0x4a080b04u, 0x7e0e0d04u, 0xf800180fu, 0x07070707u, 0xbf810000u}; @@ -2372,7 +8200,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; @@ -2390,7 +8218,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; @@ -2414,6 +8242,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); @@ -2462,19 +8291,729 @@ 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"); } +constexpr std::array StencilRefExportCode{0x7e0e02f2u, 0xf8001882u, 0x700u, 0xbf810000u}; +constexpr std::array DepthStencilRefExportCode{0x7e0e02f2u, 0xf8001883u, 0x707u, 0xbf810000u}; + +AgcDriver::QueueState stencilRefExportQueue(std::uint32_t shaderControl, std::uint32_t zFormat) { + auto queue = makeState(); + queue.context[0x1b3] = 0x300u; + queue.context[0x1b4] = 0x300u; + queue.context[0x1c4] = zFormat; + queue.context[0x203] = shaderControl; + return queue; +} + +ShaderRecompiler::RecompileRequest stencilRefExportRequest(std::uint32_t shaderControl, std::span capabilities, std::span code = StencilRefExportCode) { + const auto queue = stencilRefExportQueue(shaderControl, 2u); + ShaderRecompiler::RecompileRequest request{}; + request.shader = {ShaderRecompiler::ShaderStage::Fragment, 0x30000u, code, 0, {}}; + request.context.waveSize = 64; + request.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(queue.context, IdentityExports); + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 64; + request.target.supportedCapabilities = capabilities; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + return request; +} + +std::string stencilRefShape(std::span words) { + std::string shape; + std::uint32_t variable = 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::OpCapability && words[at + 1] == spv::CapabilityStencilExportEXT) shape += "capability "; + if (op == spv::OpExtension && std::string_view(reinterpret_cast(&words[at + 1])) == "SPV_EXT_shader_stencil_export") shape += "extension "; + if (op == spv::OpExecutionMode && words[at + 2] == spv::ExecutionModeEarlyFragmentTests) shape += "early "; + if (op == spv::OpExecutionMode && words[at + 2] == spv::ExecutionModeDepthReplacing) shape += "depth "; + if (op == spv::OpExecutionMode && words[at + 2] == spv::ExecutionModeStencilRefReplacingEXT) shape += "replacing "; + if (op == spv::OpDecorate && words[at + 2] == spv::DecorationBuiltIn && words[at + 3] == spv::BuiltInFragStencilRefEXT) { + variable = words[at + 1]; + shape += "builtin "; + } + if (op == spv::OpStore && variable != 0 && words[at + 1] == variable) shape += "store "; + } + return shape; +} + +void stencilRefExportTests() { + for (const auto shaderControl : {0x52u, 0x12u, 0x02u, 0x03u}) { + for (const auto zFormat : {2u, 9u}) { + const auto queue = stencilRefExportQueue(shaderControl, zFormat); + static_cast(AgcDriver::Graphics::DecodeState(queue)); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck rejected DB_SHADER_CONTROL " + std::to_string(shaderControl) + " with Z format " + std::to_string(zFormat)); + const auto pixel = AgcDriver::Graphics::DecodePixelStageInfo(queue.context, IdentityExports); + Require(pixel.stencilRefExportEnable, "STENCIL_TEST_VAL_EXPORT_ENABLE did not reach the pixel stage"); + Require(!pixel.earlyZ, "a pixel shader that exports the stencil reference was given early tests"); + } + 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 a stencil reference export with Z format " + std::to_string(zFormat)); + } + } + 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}; + const ShaderRecompiler::RequestSerializer serializer; + std::set> keys; + const std::array, 4> cases{{{0x52u, "capability extension replacing builtin store "}, {0x12u, "capability extension replacing builtin store "}, {0x50u, ""}, {0x10u, "early "}}}; + for (const auto& [shaderControl, expected] : cases) { + const auto request = stencilRefExportRequest(shaderControl, stencilExport); + const auto replayed = serializer.Deserialize(serializer.Serialize(request)); + Require(replayed.request.context.pixel->stencilRefExportEnable == ((shaderControl & 2u) != 0), "the serialized request lost STENCIL_TEST_VAL_EXPORT_ENABLE"); + const auto result = ShaderRecompiler::Recompile(request); + Require(stencilRefShape(result.spirv.Words()) == expected && stencilRefShape(ShaderRecompiler::Recompile(replayed.request).spirv.Words()) == expected, "DB_SHADER_CONTROL " + std::to_string(shaderControl) + " declared the wrong stencil reference export: " + stencilRefShape(result.spirv.Words())); + std::vector key; + ShaderRecompiler::RecompileCacheKey::Build(request, key); + keys.insert(key); + 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, &result, 0}}}; + AgcDriver::Graphics::ValidateShaders(shaders, state, VkPhysicalDeviceSubgroupProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}, false); + } + Require(keys.size() == cases.size(), "the recompile cache key ignores STENCIL_TEST_VAL_EXPORT_ENABLE"); + const auto both = stencilRefShape(ShaderRecompiler::Recompile(stencilRefExportRequest(0x53u, stencilExport, DepthStencilRefExportCode)).spirv.Words()); + 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); + 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; + queue.context[0x007] = (1u << 16u) | 3u; + queue.context[0x00a] = 7; + queue.context[0x00b] = 0; + queue.context[0x010] = 0x22900983; + queue.context[0x011] = 0x20000181; + for (const auto offset : {0x012u, 0x014u}) queue.context[offset] = 0x100; + for (const auto offset : {0x013u, 0x015u}) queue.context[offset] = 0x200; + queue.context[0x10b] = 0x00030030; + queue.context[0x10c] = 0x05ffff02; + queue.context[0x10d] = 0x05ffff02; + queue.context[0x200] = 0x00200211; + auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.stencilTest && state.stencilFront.compareOp == VK_COMPARE_OP_EQUAL && state.stencilFront.passOp == VK_STENCIL_OP_REPLACE, "a stencil reference export changed the REPLACE_TEST decode"); + for (const auto ops : {0x00020020u, 0x00040040u, 0x00000200u, 0x00000004u}) { + queue.context[0x10b] = ops; + expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeState(queue)); }, "exports the stencil test value"); + } + queue.context[0x10b] = 0x00050050; + queue.context[0x10c] = 0x01ffff02; + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.stencilFront.passOp == VK_STENCIL_OP_INCREMENT_AND_CLAMP, "a stencil reference export changed the increment decode"); + queue.context[0x000] = 2; + expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeState(queue)); }, "stencil clear with a pixel shader that exports the stencil test value"); + queue.context[0x203] = 0x50u; + 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) { + if (static_cast(words[at] & 0xffffu) != spv::OpName) continue; + const std::string name(reinterpret_cast(&words[at + 2])); + if (name.starts_with("buffers")) views.insert(name); + } + return views; +} + +void aliasedBufferViewTests() { + constexpr std::array capabilities{spv::CapabilityShader, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityInt64Atomics}; + constexpr std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + constexpr std::array userData{0u, 0u, 0u, 0u, 0x10000000u, 0u, 0x1000u, 0x31016facu, 0x20000000u, 0u, 0x1000u, 0x31038facu}; + const auto code = [](std::uint32_t store, std::uint32_t resource) { + return std::array{0xe0000000u, 0x80010a00u, 0xbf8c3f70u, store, resource, 0x7e0e02f2u, 0xf800180fu, 0x07070707u, 0xbf810000u}; + }; + const std::array, std::set>, 2> cases{{ + {code(0xe0104000u, 0x80010a00u), {"buffers", "buffers_coherent"}}, + {code(0xe1480000u, 0x80010a00u), {"buffers", "buffers_u64"}}, + }}; + AgcDriver::Graphics::State state{}; + state.stages.path = AgcDriver::Graphics::ShaderPath::Vertex; + ShaderRecompiler::RecompileResult vertex; + vertex.spirv = makeModule({}); + const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; + for (const auto& [words, views] : cases) { + auto request = orderedPixelRequest(0x20600u, capabilities, words); + request.target.supportedExtensions = extensions; + request.target.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + auto pixel = ShaderRecompiler::Recompile(request); + const auto declared = bufferViews(pixel.spirv.Words()); + std::string names; + for (const auto& name : declared) names += " " + name; + Require(declared == views, "the pixel shader declared the guest buffers as" + names + ", not as the expected " + std::to_string(views.size()) + " aliased variables"); + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &pixel, 0}}}; + AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, true); + auto unaliased = pixel.spirv.Copy(); + for (std::size_t at = 5; at < unaliased.size() && (unaliased[at] >> 16u) != 0; at += unaliased[at] >> 16u) { + if (static_cast(unaliased[at] & 0xffffu) == spv::OpDecorate && unaliased[at + 2] == spv::DecorationAliased) unaliased[at + 2] = spv::DecorationRestrict; + } + pixel.spirv = std::move(unaliased); + expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, true); }, "duplicate SPIR-V resource binding"); + } +} + +void LineStripTests() { + using namespace AgcDriver::Graphics; + auto queue = makeState(); + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + queue.userConfig[0x242] = 3; + auto state = DecodeState(queue); + Require(state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP && !state.rectList && !state.primitiveRestart, "primitive type 3 did not decode as a line strip"); + Require(RasterPrimitiveOf(state) == ShaderRecompiler::RasterPrimitive::Line, "a line strip draw did not get the line primitive class"); + queue.userConfig[0x24b] = 1; + queue.context[0x103] = 0xffffffffu; + state = DecodeState(queue); + Require(state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP && state.primitiveRestart, "primitive restart was lost on a line strip"); + const auto rejection = DrawRejection(queue, true); + Require(rejection.empty(), "the draw precheck rejected an indexed line strip with primitive restart: " + rejection); +} + +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; + auto queue = makeState(); + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + const auto first = DecodeState(queue); + Require(first.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT, "the default provoking vertex is not first"); + for (const auto primitive : {2u, 3u, 4u, 5u, 6u}) { + queue.userConfig[0x242] = primitive; + for (const auto last : {false, true, false}) { + queue.context[0x205] = 0x240u | (last ? lastVertex : 0u); + const auto state = DecodeState(queue); + Require(state.provokingVertexMode == (last ? VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT : VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT), "PROVOKING_VTX_LAST did not select the requested vertex for primitive " + std::to_string(primitive)); + const auto rejection = DrawRejection(queue, false); + Require(rejection.empty(), "the draw precheck rejected provoking vertex mode for primitive " + std::to_string(primitive) + ": " + rejection); + Require(state.cullMode == first.cullMode && state.frontFace == first.frontFace && state.depthBias == first.depthBias, "PROVOKING_VTX_LAST changed another rasterization field"); + } + } + queue.context[0x205] |= lastVertex; + queue.ClearContext(); + Require((queue.context.at(0x205) & lastVertex) == 0, "context reset kept last provoking vertex mode"); + Require(DrawKeyCovers({RegisterBank::Context, 0x205}), "the draw cache key does not cover provoking vertex mode"); + const Context unsupported{}; + ValidateProvokingVertex(unsupported, first); + auto last = first; + last.provokingVertexMode = VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT; + expectFailure([&] { ValidateProvokingVertex(unsupported, last); }, "last provoking vertex requires VK_EXT_provoking_vertex with provokingVertexLast enabled"); + auto supported = unsupported; + supported.provokingVertexLast = true; + ValidateProvokingVertex(supported, last); + for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_POINT_LIST, VK_PRIMITIVE_TOPOLOGY_LINE_LIST, VK_PRIMITIVE_TOPOLOGY_LINE_STRIP, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN}) { + auto state = last; + state.topology = topology; + ValidateProvokingVertex(supported, state); + } + for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_PATCH_LIST, VK_PRIMITIVE_TOPOLOGY_LINE_LIST_WITH_ADJACENCY, VK_PRIMITIVE_TOPOLOGY_LINE_STRIP_WITH_ADJACENCY, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST_WITH_ADJACENCY, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP_WITH_ADJACENCY, VK_PRIMITIVE_TOPOLOGY_MAX_ENUM}) { + auto state = last; + state.topology = topology; + expectFailure([&] { ValidateProvokingVertex(supported, state); }, "last provoking vertex is unsupported for this primitive topology"); + } + const std::array flatColor{0xc8120002u, 0xc8160102u, 0xc81a0202u, 0xc81e0302u, 0xf800180fu, 0x07060504u, 0xbf810000u}; + const auto flatPixel = recompilePixel({0x400u}, flatColor); + const auto inputs = locatedInputs(flatPixel.spirv.Words()); + Require(inputs.size() == 1 && inputs[0].location == 0 && inputs[0].flat && !inputs[0].perVertex, "flat v_interp_mov P0 did not compile to a flat input"); + Require(flatPixel.fragmentParameters.size() == 1 && flatPixel.fragmentParameters[0].flat && !flatPixel.fragmentParameters[0].perVertex, "flat v_interp_mov P0 has explicit per-vertex metadata"); + const std::array flatShaders{{{ShaderRecompiler::ShaderStage::Fragment, &flatPixel, 0}}}; + ValidateProvokingVertex(supported, last, flatShaders); + const std::array flatP10Color{0xc8120000u, 0xc8160100u, 0xc81a0200u, 0xc81e0300u, 0xf800180fu, 0x07060504u, 0xbf810000u}; + const auto flatP10Pixel = recompilePixel({0x400u}, flatP10Color); + const auto p10Inputs = locatedInputs(flatP10Pixel.spirv.Words()); + Require(p10Inputs.size() == 1 && p10Inputs[0].location == 0 && p10Inputs[0].flat && !p10Inputs[0].perVertex, "flat v_interp_mov P10 did not compile to a flat input"); + Require(flatP10Pixel.fragmentParameters.size() == 1 && flatP10Pixel.fragmentParameters[0].flatP10 && !flatP10Pixel.fragmentParameters[0].perVertex && !flatPixel.fragmentParameters[0].flatP10, "flat v_interp_mov P10 was not recorded apart from P0"); + 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"); + 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}) { + auto state = last; + state.topology = topology; + for (const auto flat : {false, true}) { + for (const auto perVertex : {false, true}) { + 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); + } + } + } + } + } + for (const auto path : {ShaderPath::Geometry, ShaderPath::Tessellation, ShaderPath::TessellationGeometry}) { + auto generated = last; + generated.stages.path = path; + expectFailure([&] { ValidateProvokingVertex(supported, generated); }, "last provoking vertex is unsupported for generated primitive pipelines"); + generated.provokingVertexMode = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT; + ValidateProvokingVertex(unsupported, generated); + } + for (std::uint32_t generated = 0; generated < 2; ++generated) { + auto state = last; + 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); + } + for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN}) { + meshDraw.topology = topology; + expectFailure([&] { ValidateProvokingVertex(supported, meshDraw, flatShaders); }, "last provoking vertex is unsupported for triangle strips or fans assembled for a geometry program"); + auto firstDraw = meshDraw; + 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"); +} + +void ProvokingVertexRenderPassTests() { + using namespace AgcDriver::Graphics; + const auto first = DecodeState(makeState()); + const std::array views{reinterpret_cast(std::uintptr_t{1})}; + const std::array modes{ + VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT, VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT, + VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT, VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT, + VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT + }; + for (const bool perPipeline : {false, true}) { + Context context{}; + context.provokingVertexLast = true; + context.provokingVertexModePerPipeline = perPipeline; + auto state = first; + const auto initialKey = DrawRenderPassKey(context, state, views); + auto previousKey = initialKey; + auto previousMode = modes.front(); + for (const auto mode : modes) { + state.provokingVertexMode = mode; + const auto key = DrawRenderPassKey(context, state, views); + const bool reusable = key == previousKey; + Require(reusable == (perPipeline || mode == previousMode), "render-pass reuse does not respect provokingVertexModePerPipeline"); + previousKey = key; + previousMode = mode; + } + Require(previousKey == initialKey, "returning to first provoking vertex changed the render-pass identity"); + state.viewport.x += 1.0f; + Require(DrawRenderPassKey(context, state, views) == initialKey, "dynamic viewport changes prevent render-pass reuse"); + const std::array differentViews{reinterpret_cast(std::uintptr_t{2})}; + Require(DrawRenderPassKey(context, state, differentViews) != initialKey, "different attachments share a render-pass identity"); + ++state.renderExtent.width; + Require(DrawRenderPassKey(context, state, views) != initialKey, "different render extents share a render-pass identity"); + } +} + void ConservativeRasterizationTests() { auto queue = makeState(); queue.context[0x1b3] = 2; 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"); @@ -2503,10 +9042,12 @@ void ConservativeRasterizationTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(geometry); }, rejected); } auto tessellation = queue; - tessellation.context[0x2d5] = 0x200d; + tessellation.context[0x2d5] = 0x0200210d; tessellation.userConfig[0x242] = 9; - tessellation.context[0x2d6] = (3u << 8u) | (3u << 14u); + tessellation.context[0x2d6] = 0xc355; tessellation.context[0x2db] = 1u | (2u << 2u) | (2u << 5u); + tessellation.context[0x286] = 0x42800000; + tessellation.context[0x287] = 0x3f800000; const auto tessellated = AgcDriver::Graphics::DecodeState(tessellation); Require(tessellated.stages.path == AgcDriver::Graphics::ShaderPath::Tessellation && tessellated.conservativeRasterization == VK_CONSERVATIVE_RASTERIZATION_MODE_OVERESTIMATE_EXT, "OVER_RAST_ENABLE did not overestimate clockwise tessellated triangles"); for (const auto& [parameters, rejected] : std::array, 3>{{{1u | (2u << 2u), "conservative rasterization of points is unsupported (VGT_TF_PARAM=0x9)"}, {1u | (2u << 2u) | (1u << 5u), "conservative rasterization of lines is unsupported (VGT_TF_PARAM=0x29)"}, {(2u << 2u) | (1u << 5u), "conservative rasterization of lines is unsupported (VGT_TF_PARAM=0x28)"}}}) { @@ -2519,7 +9060,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}) { @@ -2588,6 +9137,373 @@ void pixelParameterSlotTests() { expectFailure([&] { recompilePixel({0x423u, 0x3u}, shared); }, "passes its vertices through unchanged"); } +ShaderRecompiler::RecompileRequest packedExportRequest(const AgcDriver::QueueState& queue, const AgcDriver::Graphics::State& state, std::span code) { + ShaderRecompiler::RecompileRequest request{}; + request.shader = {ShaderRecompiler::ShaderStage::Fragment, 0x30000u, code, 0, {}}; + request.context.waveSize = 64; + request.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(queue.context, AgcDriver::Graphics::ExportMappings(state)); + request.context.pixel->targetExportPacking = AgcDriver::Graphics::ExportPackings(state); + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 64; + request.layout.pushConstantSizeBytes = 128; + request.useCache = true; + return request; +} + +void unorm10_11_11TargetTests() { + using ShaderRecompiler::ColorExportPacking; + auto queue = makeState(); + queue.context[0x31c] = (queue.context.at(0x31c) & ~0x7cu) | (6u << 2u); + 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.packing == ColorExportPacking::Unorm10_11_11, "a 10_11_11 unorm target did not decode as a packed R32_UINT attachment"); + const auto packings = AgcDriver::Graphics::ExportPackings(state); + Require(packings[0] == ColorExportPacking::Unorm10_11_11 && std::all_of(packings.begin() + 1, packings.end(), [](ColorExportPacking packing) { return packing == ColorExportPacking::None; }), "export 0 did not carry the 10_11_11 unorm packing alone"); + auto changed = queue; + changed.context[0x31c] |= 7u << 8u; + const auto floating = AgcDriver::Graphics::DecodeState(changed); + Require(floating.color.format == VK_FORMAT_B10G11R11_UFLOAT_PACK32 && floating.color.packing == ColorExportPacking::None, "a 10_11_11 float target changed"); + changed = queue; + changed.context[0x31c] |= 1u << 8u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "unsupported color format 6 number type 1"); + changed = queue; + changed.context[0x1e0] = 0x40010001u; + for (std::uint32_t i = 0; i < 4; ++i) changed.context[0x105 + i] = 0; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "blending into a 10_11_11 unorm color target"); + changed = queue; + changed.context[0x8e] = 3; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "partial writes of a 10_11_11 unorm color target"); + changed.context[0x8e] = 7; + Require(AgcDriver::Graphics::DecodeState(changed).color.packing == ColorExportPacking::Unorm10_11_11, "an RGB write of a 10_11_11 unorm 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 a 10_11_11 unorm target"); + } + changed = queue; + changed.context[0x1c5] = 4; + Require(AgcDriver::Graphics::DecodeState(changed).color.packing == ColorExportPacking::Unorm10_11_11, "an FP16_ABGR export into a 10_11_11 unorm target was refused"); + changed = queue; + changed.context[0x31c] |= 0x10000000u; + changed.context[0x325] = 0x1234; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "DCC-compressed 10_11_11 unorm color targets"); + changed = queue; + changed.context[0x31c] |= 0x40000u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "truncating (ROUND_MODE) 10_11_11 unorm color targets"); + + constexpr std::array code{0xf800180fu, 0x07060504u, 0xbf810000u}; + const auto request = packedExportRequest(queue, state, code); + auto plain = request; + plain.context.pixel->targetExportPacking.fill(ColorExportPacking::None); + const ShaderRecompiler::RequestSerializer serializer; + Require(serializer.Deserialize(serializer.Serialize(request)).request.context.pixel->targetExportPacking == request.context.pixel->targetExportPacking, "the 10_11_11 unorm packing was lost in serialization"); + std::vector key; + std::vector plainKey; + ShaderRecompiler::BuildPreparedShaderKey(request, key); + ShaderRecompiler::BuildPreparedShaderKey(plain, plainKey); + Require(key == plainKey, "the export packing entered the static shader ABI"); + const auto prepared = ShaderRecompiler::PrepareShader(plain); + Require(ShaderRecompiler::MatchesPreparedShader(request, *prepared), "a packed draw did not match the registered pixel shader"); + const auto packed = ShaderRecompiler::Recompile(request); + const auto unpacked = ShaderRecompiler::Recompile(plain); + Require(packed.variantId == unpacked.variantId && packed.variantId == ShaderRecompiler::GetPreparedArtifact(*prepared).variantId, "the packed draw did not reuse the registered artifact"); + Require(packed.PipelineVariantId() != unpacked.PipelineVariantId() && !(packed.spirv == unpacked.spirv), "the export packing did not specialize the module"); + Require(ShaderRecompiler::Recompile(request).PipelineVariantId() == packed.PipelineVariantId(), "the packed specialization was not reused"); + ShaderRecompiler::RecompileResult vertex; + vertex.spirv = makeModule({}); + 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(packed, state) == std::set{0u}, "the packed export did not reach attachment 0"); + 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 color export packing path"); +} + +void float11_11_10TargetTests() { + using ShaderRecompiler::ColorExportPacking; + auto queue = makeState(); + queue.context[0x31c] = (queue.context.at(0x31c) & ~0x1f7cu) | (7u << 2u) | (7u << 8u) | (2u << 11u) | 0x40000u; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + const auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.color.format == VK_FORMAT_R32_UINT && state.color.elementBytes == 4 && state.color.bytes == colorMemory.size() && state.color.componentMapping == 0xc6u && state.color.packing == ColorExportPacking::Float11_11_10, "an 11_11_10 float target did not decode as a packed R32_UINT attachment with blue in the low bits"); + const auto packings = AgcDriver::Graphics::ExportPackings(state); + Require(packings[0] == ColorExportPacking::Float11_11_10 && std::all_of(packings.begin() + 1, packings.end(), [](ColorExportPacking packing) { return packing == ColorExportPacking::None; }), "export 0 did not carry the 11_11_10 float packing alone"); + Require(AgcDriver::Graphics::ExportMappings(state)[0] == 0xc6u, "export 0 did not reverse its components into the 11_11_10 float target"); + auto changed = queue; + changed.context[0x31c] &= ~0x40000u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "rounding (ROUND_MODE 0) 11_11_10 float color targets"); + for (const auto swap : {0u, 1u, 3u}) { + changed = queue; + changed.context[0x31c] = (changed.context[0x31c] & ~0x1800u) | (swap << 11u); + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "unsupported color format 7 number type 7 component swap " + std::to_string(swap)); + } + for (const auto number : {0u, 4u}) { + changed = queue; + changed.context[0x31c] = (changed.context[0x31c] & ~0x700u) | (number << 8u) | 0x8000u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "unsupported color format 7 number type " + std::to_string(number) + " component swap 2"); + } + changed = queue; + changed.context[0x1e0] = 0x60010101u; + for (std::uint32_t i = 0; i < 4; ++i) changed.context[0x105 + i] = 0; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "blending into an 11_11_10 float color target"); + changed = queue; + changed.context[0x8e] = 6; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "partial writes of an 11_11_10 float color target"); + changed.context[0x8e] = 7; + Require(AgcDriver::Graphics::DecodeState(changed).color.packing == ColorExportPacking::Float11_11_10, "an RGB write of an 11_11_10 float target was refused"); + for (const auto format : {5u, 6u}) { + changed = queue; + changed.context[0x1c5] = format; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "color export format " + std::to_string(format) + " into an 11_11_10 float target"); + } + changed = queue; + changed.context[0x1c5] = 4; + Require(AgcDriver::Graphics::DecodeState(changed).color.packing == ColorExportPacking::Float11_11_10, "an FP16_ABGR export into an 11_11_10 float target was refused"); + changed = queue; + changed.context[0x31c] |= 0x10000000u; + changed.context[0x325] = 0x1234; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "DCC-compressed 11_11_10 float color targets"); + + constexpr std::array code{0xf800180fu, 0x07060504u, 0xbf810000u}; + const auto request = packedExportRequest(queue, state, code); + auto unorm = request; + unorm.context.pixel->targetExportPacking[0] = ColorExportPacking::Unorm10_11_11; + const ShaderRecompiler::RequestSerializer serializer; + Require(serializer.Deserialize(serializer.Serialize(request)).request.context.pixel->targetExportPacking == request.context.pixel->targetExportPacking, "the 11_11_10 float packing was lost in serialization"); + const auto packed = ShaderRecompiler::Recompile(request); + const auto unormPacked = ShaderRecompiler::Recompile(unorm); + Require(packed.variantId == unormPacked.variantId && packed.PipelineVariantId() != unormPacked.PipelineVariantId(), "the 11_11_10 float packing did not key its own specialization of the shared artifact"); + ShaderRecompiler::RecompileResult vertex; + vertex.spirv = makeModule({}); + const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &packed, 0}}}; + Require(AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false) == std::set{0u}, "the packed float export did not reach attachment 0"); + auto invalid = request; + invalid.context.pixel->targetExportPacking[0] = static_cast(3); + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(invalid)); }, "invalid prepared fragment export packing"); +} + +AgcDriver::QueueState dualSourceQueue() { + auto queue = makeState(); + queue.context[0x8f] = 0xffu; + queue.context[0x1c5] = 0x44u; + queue.context[0x1e0] = 0x40010f01u; + for (std::uint32_t i = 0; i < 4; ++i) queue.context[0x105 + i] = 0; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + return queue; +} + +struct OutputDecorations { + std::map locations; + std::map indices; +}; + +OutputDecorations outputDecorations(std::span words) { + OutputDecorations result; + std::set outputs; + 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 && words[at + 3] == spv::StorageClassOutput) outputs.insert(words[at + 2]); + if (op != spv::OpDecorate) continue; + if (words[at + 2] == spv::DecorationLocation) result.locations[words[at + 1]] = words[at + 3]; + if (words[at + 2] == spv::DecorationIndex) result.indices[words[at + 1]] = words[at + 3]; + } + std::erase_if(result.locations, [&](const auto& entry) { return !outputs.contains(entry.first); }); + return result; +} + +void DualSourceBlendTests() { + auto queue = dualSourceQueue(); + const auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.dualSourceBlend && state.colors.size() == 1 && state.blends.size() == 1, "an ONE + SRC1_COLOR blend did not decode as dual-source blending into one target"); + Require(state.blends[0].srcColorBlendFactor == VK_BLEND_FACTOR_ONE && state.blends[0].dstColorBlendFactor == VK_BLEND_FACTOR_SRC1_COLOR && state.blends[0].srcAlphaBlendFactor == VK_BLEND_FACTOR_ONE && state.blends[0].dstAlphaBlendFactor == VK_BLEND_FACTOR_SRC1_COLOR, "BLEND_SRC1_COLOR did not become VK_BLEND_FACTOR_SRC1_COLOR"); + const std::array, 4> factors{{{15u, VK_BLEND_FACTOR_SRC1_COLOR}, {16u, VK_BLEND_FACTOR_ONE_MINUS_SRC1_COLOR}, {17u, VK_BLEND_FACTOR_SRC1_ALPHA}, {18u, VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA}}}; + for (const auto& [value, factor] : factors) { + auto changed = queue; + changed.context[0x1e0] = 0x60000000u | (value << 24u) | (1u << 16u) | (value << 8u) | value; + const auto decoded = AgcDriver::Graphics::DecodeState(changed); + Require(decoded.dualSourceBlend && decoded.blends[0].srcColorBlendFactor == factor && decoded.blends[0].dstColorBlendFactor == factor && decoded.blends[0].srcAlphaBlendFactor == VK_BLEND_FACTOR_ONE && decoded.blends[0].dstAlphaBlendFactor == factor, "blend factor " + std::to_string(value) + " decoded wrongly"); + } + auto changed = queue; + changed.context[0x1e0] = 0x00010f01u; + Require(!AgcDriver::Graphics::DecodeState(changed).dualSourceBlend, "SRC1 factors of a disabled blend enabled dual-source blending"); + changed = queue; + changed.context[0x1e0] = 0x40010001u; + Require(!AgcDriver::Graphics::DecodeState(changed).dualSourceBlend, "a blend without SRC1 factors enabled dual-source blending"); + for (const auto operation : {2u, 3u}) { + changed.context[0x1e0] = 0x40010f01u | (operation << 5u); + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "MIN or MAX operation"); + } + changed.context[0x1e0] = 0x40010f01u | (4u << 5u); + Require(AgcDriver::Graphics::DecodeState(changed).blends[0].colorBlendOp == VK_BLEND_OP_REVERSE_SUBTRACT, "a reverse-subtract dual-source blend was refused"); + changed = queue; + changed.context[0x8f] = 0xfu; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "MRT slots 0 and 1"); + changed.context[0x8f] = 0xfffu; + changed.context[0x1c5] = 0x444u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "MRT slots 0 and 1"); + changed.context[0x8f] = 0xf000fu; + changed.context[0x1c5] = 0x44u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "MRT slots 0 and 1"); + alignas(256) static std::array slotOneMemory{}; + changed = queue; + for (const auto offset : {0x31bu, 0x31cu, 0x31du}) changed.context[offset + 0xfu] = changed.context.at(offset); + for (const auto offset : {0x3b0u, 0x3b8u}) changed.context[offset + 1u] = changed.context.at(offset); + changed.context[0x318 + 0xfu] = static_cast(reinterpret_cast(slotOneMemory.data()) >> 8u); + changed.context[0x390 + 1u] = static_cast(reinterpret_cast(slotOneMemory.data()) >> 40u); + changed.context[0x1e1] = 0; + changed.context[0x8e] = 0xffu; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "color writes to slot 0 alone"); + for (const auto format : {0x04u, 0x14u, 0x74u, 0x94u}) { + changed = queue; + changed.context[0x1c5] = format; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "MRT1 export in the MRT0 export format"); + } + changed = queue; + changed.context[0x1c5] = 0x99u; + Require(AgcDriver::Graphics::DecodeState(changed).dualSourceBlend, "a 32_ABGR MRT0 and MRT1 export pair was refused"); + for (const auto mask : {0x7fu, 0xf7u}) { + changed = queue; + changed.context[0x8f] = mask; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "MRT1 export with the MRT0 shader mask"); + } + changed = queue; + changed.context[0x31c] = 0x8804u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "component-swapped color target"); + + constexpr std::array code{0xf800000fu, 0x07060504u, 0xf800181fu, 0x0b0a0908u, 0xbf810000u}; + ShaderRecompiler::RecompileRequest request{}; + request.shader = {ShaderRecompiler::ShaderStage::Fragment, 0x30000u, code, 0, {}}; + request.context.waveSize = 64; + request.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(queue.context, AgcDriver::Graphics::ExportMappings(state)); + request.context.pixel->dualSourceBlend = state.dualSourceBlend; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 64; + request.layout.pushConstantSizeBytes = 128; + request.useCache = true; + auto plain = request; + plain.context.pixel->dualSourceBlend = false; + const ShaderRecompiler::RequestSerializer serializer; + Require(serializer.Deserialize(serializer.Serialize(request)).request.context.pixel->dualSourceBlend, "dual-source blending was lost in serialization"); + std::vector key; + std::vector plainKey; + ShaderRecompiler::BuildPreparedShaderKey(request, key); + ShaderRecompiler::BuildPreparedShaderKey(plain, plainKey); + Require(key == plainKey, "dual-source blending entered the static shader ABI"); + const auto prepared = ShaderRecompiler::PrepareShader(plain); + Require(ShaderRecompiler::MatchesPreparedShader(request, *prepared), "a dual-source draw did not match the registered pixel shader"); + const auto dual = ShaderRecompiler::Recompile(request); + const auto single = ShaderRecompiler::Recompile(plain); + Require(dual.variantId == single.variantId && dual.variantId == ShaderRecompiler::GetPreparedArtifact(*prepared).variantId, "the dual-source draw did not reuse the registered artifact"); + Require(dual.PipelineVariantId() != single.PipelineVariantId() && ShaderRecompiler::Recompile(request).PipelineVariantId() == dual.PipelineVariantId(), "dual-source blending did not key its own specialization"); + const auto dualOutputs = outputDecorations(dual.spirv.Words()); + const auto singleOutputs = outputDecorations(single.spirv.Words()); + Require(dualOutputs.locations.size() == 2 && std::all_of(dualOutputs.locations.begin(), dualOutputs.locations.end(), [](const auto& entry) { return entry.second == 0u; }) && dualOutputs.indices.size() == 1 && dualOutputs.indices.begin()->second == 1u, "the MRT1 export of a dual-source draw was not the location 0 index 1 output"); + std::set singleLocations; + for (const auto& entry : singleOutputs.locations) singleLocations.insert(entry.second); + Require(singleOutputs.indices.empty() && singleLocations == std::set{0u, 1u}, "MRT0 and MRT1 without dual-source blending did not keep locations 0 and 1"); + 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(dual, state) == std::set{0u}, "the dual-source shader did not validate against its draw"); + expectFailure([&] { validate(single, state); }, "needs a float4 second color at location 0 index 1"); + auto singleState = state; + singleState.dualSourceBlend = false; + 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; @@ -2610,6 +9526,19 @@ void validationTests() { expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, true, false); }, "unsupported device capability 35"); pixel.spirv = makeModule({.fragment = true, .sampleMaskLength = 2}); expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "unsupported fragment built-in"); + pixel.spirv = makeModule({.fragment = true}); + vertex.spirv = makeModule({.parameterOutput = true, .clipDistanceLength = 4, .cullDistanceLength = 4}); + AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, true, true); + state.paClVsOutCntl = 0x0040000fu; + vertex.spirv = makeModule({.parameterOutput = true, .clipDistanceLength = 4}); + expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, false, true); }, "PA_CL_VS_OUT_CNTL=0x0040000f"); + AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, true, false); + vertex.spirv = makeModule({.parameterOutput = true, .cullDistanceLength = 4}); + expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, true, false); }, "PA_CL_VS_OUT_CNTL=0x0040000f"); + AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, false, true); + state.paClVsOutCntl = 0; + vertex.spirv = makeModule({.parameterOutput = true, .clipDistanceLength = 9}); + expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, true, true); }, "clip or cull distance"); vertex.spirv = makeModule({.parameterOutput = true, .sampleId = true}); expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "unsupported device capability 35"); vertex.spirv = makeModule({.parameterOutput = true}); @@ -2622,6 +9551,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; @@ -2644,18 +9601,65 @@ void validationTests() { vertex.vertexAttributes[0].components = 2; expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "metadata disagrees"); Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 2, 1) == 80, "incorrect strided vertex range"); - expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 3, 1); }, "record count"); + Require(AgcDriver::Graphics::VertexBufferReadOffset(attribute, 2, 7) == 64, "vertex range includes records before the first vertex"); + Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 3, 1) == 112, "an index past the records still spans its fetch"); + { + auto wider = attribute; + wider.resource.fields[2] = 9; + auto instanced = attribute; + instanced.fetchIndex = 1; + instanced.resource.fields[2] = 1000; + auto constant = attribute; + constant.resource.fields[1] = 0; + const std::array attributes{attribute, wider, instanced, constant}; + Require(AgcDriver::Graphics::ZeroFetchVertex(attributes) == 9u, "the first vertex reading zeros is not past the largest per-vertex record count"); + auto clamped = attribute; + clamped.resource.fields[3] |= 2u << 28u; + const std::array selective{attribute, clamped}; + Require(!AgcDriver::Graphics::ZeroFetchVertex(selective).has_value(), "an attribute with out-of-bounds select 2 was taken to read zeros past its records"); + std::array indices{0xffffffffu, 2u, 0x12345u, 0xffffffffu}; + Require(AgcDriver::Graphics::ClampDrawIndices(std::as_writable_bytes(std::span(indices)), 4, true, 9u) == 9u && indices == std::array{0xffffffffu, 2u, 9u, 0xffffffffu}, "clamping kept an index past the limit or moved a restart index"); + Require(AgcDriver::Graphics::ClampDrawIndices(std::as_writable_bytes(std::span(indices)), 4, false, 9u) == 9u && indices == std::array{9u, 2u, 9u, 9u}, "clamping without restart kept a 0xffffffff index"); + std::array narrow{0xffffu, 1u}; + Require(AgcDriver::Graphics::ClampDrawIndices(std::as_writable_bytes(std::span(narrow)), 2, false, 4u) == 4u && narrow[0] == 4u && narrow[1] == 1u, "clamping 16-bit indices failed"); + } attribute.fetchIndex = 1; + Require(AgcDriver::Graphics::VertexBufferReadOffset(attribute, 2, 7) == 224, "instance range includes records before the first instance"); Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2) == 48, "instance attributes used the vertex index"); Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2, 1) == 80, "first instance was ignored"); - expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 2, 2); }, "record count"); + Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 2, 2) == 112, "a first instance past the records still spans its fetch"); expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 2, 0xffffffffu); }, "instance range overflow"); - expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 4); }, "record count"); + Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 4) == 112, "an instance range past the records still spans its fetch"); + attribute.fetchIndex = 0; + attribute.resource.fields[3] = 0x1004dfacu; + Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 3, 1) == 112, "oob_select 1 past the records still spans its fetch"); + attribute.resource.fields[3] = 0x2004dfacu; + expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 3, 1); }, "record count"); + attribute.resource.fields[3] = 0x3004dfacu; + expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 3, 1); }, "record count"); + attribute.resource.fields[3] = 77u << 12u; + attribute.fetchIndex = 1; attribute.resource.fields[1] = 0; attribute.resource.fields[2] = 16; Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2) == 16, "zero stride must repeat one value"); + Require(AgcDriver::Graphics::VertexBufferReadOffset(attribute, 100, 7) == 0, "zero-stride input skipped its repeated value"); attribute.resource.fields[2] = 8; expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1); }, "byte range"); + Require(!AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "a structured zero-stride fetch was treated as out of range"); + attribute.resource.fields[3] = (77u << 12u) | (2u << 28u); + attribute.resource.fields[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; + Require(AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 2 without records must read zero"); + attribute.resource.fields[3] = (77u << 12u) | (3u << 28u); + 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"); attribute.resource.fields[3] = 113u << 12u; expectFailure([&] { AgcDriver::Graphics::DecodeVertexFormat(attribute); }, "unsupported vertex format"); attribute.resource.fields[3] = 50u << 12u; @@ -2664,6 +9668,17 @@ void validationTests() { Require(AgcDriver::Graphics::DecodeVertexFormat(attribute).format == VK_FORMAT_A2B10G10R10_SINT_PACK32 && std::string_view(AgcDriver::Graphics::DecodeVertexFormat(attribute).scalar) == "i32", "2_10_10_10 sint vertex format was not decoded"); attribute.resource.fields[3] = 36u << 12u; Require(AgcDriver::Graphics::DecodeVertexFormat(attribute).format == VK_FORMAT_B10G11R11_UFLOAT_PACK32, "10_11_11 float vertex format was not decoded"); + for (std::uint32_t format = 1; format < 128u; ++format) { + attribute.resource.fields[3] = format << 12u; + std::string_view scalar; + try { + scalar = AgcDriver::Graphics::DecodeVertexFormat(attribute).scalar; + } catch (const std::exception&) { + continue; + } + const auto numeric = ShaderRecompiler::VertexInputNumericClass(static_cast(format)); + Require((scalar == "u32") == (numeric == ShaderRecompiler::IrTextureNumericClass::Uint) && (scalar == "i32") == (numeric == ShaderRecompiler::IrTextureNumericClass::Sint), "vertex format " + std::to_string(format) + " declares a shader input of another numeric type than its attribute"); + } attribute.resource.fields[3] = 43u << 12u; expectFailure([&] { AgcDriver::Graphics::DecodeVertexFormat(attribute); }, "unsupported vertex format"); attribute.resource.fields = {0x1000, 32u << 16u, 3, 77u << 12u}; @@ -2703,6 +9718,14 @@ void validationTests() { expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "unsupported device capability 12"); AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, true); } + { + ShaderRecompiler::RecompileResult vertex; + vertex.spirv = makeModule({.layerOutput = true}); + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}}; + const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; + expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "VK_EXT_shader_viewport_index_layer"); + AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, false, false, true); + } for (const auto capability : {spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle}) { ShaderRecompiler::RecompileResult vertex; vertex.spirv = makeModule({}); @@ -2783,7 +9806,7 @@ void validationTests() { { auto vertex = pushed({.push = true, .bufferArray = 2}); vertex.bindings.front().descriptorSet = 1; - expectFailure([&] { validate(vertex, 8); }, "descriptor set other than zero"); + expectFailure([&] { validate(vertex, 8); }, "descriptor set other than its stage's"); } { auto vertex = pushed({.push = true, .bufferArray = 2}); @@ -2842,6 +9865,39 @@ void meshIndexBufferTests() { expectFailure([] { static_cast(AgcDriver::Graphics::MeshIndexBufferDescriptor(AgcDriver::Pm4::DrawParameters{0, 3, 2, 1, 0, true})); }, "invalid mesh index buffer range"); } +void stagingPoolBudgetTests() { + if (std::getenv("APS5_STAGING_POOL_MIB") != nullptr) return; + VkPhysicalDeviceMemoryProperties memory{}; + memory.memoryHeapCount = 2; + memory.memoryHeaps[0] = {VkDeviceSize{32} << 30u, 0}; + memory.memoryHeaps[1] = {VkDeviceSize{16} << 30u, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; + Require(AgcDriver::Graphics::BufferPool::DeviceBudget(memory) == VkDeviceSize{2} << 30u, "the staging pool budget is not an eighth of the device-local heap"); + memory.memoryHeaps[1].size = VkDeviceSize{2} << 30u; + 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() { using AgcDriver::Graphics::MeshArguments; using AgcDriver::Graphics::ResolveMeshArguments; @@ -2879,6 +9935,69 @@ void debugBranchTests() { for (const auto opcode : {0x17u, 0x18u, 0x19u, 0x1au}) Require(recompilesDebugBranch(opcode), "a conditional debug branch did not recompile"); } +void highestDrawIndexTests() { + using AgcDriver::Graphics::HighestDrawIndex; + const auto bytesOf = [](const auto& values) { return std::as_bytes(std::span(values)); }; + const std::array narrow{0, 7, 0xffff, 3, 0xffff}; + Require(HighestDrawIndex(bytesOf(narrow), 2, true) == 7, "a 16-bit restart index counted as a vertex"); + Require(HighestDrawIndex(bytesOf(narrow), 2, false) == 0xffff, "a 16-bit all-ones index without restart was skipped"); + const std::array wide{9, 0xffffffffu, 0xffff, 2}; + Require(HighestDrawIndex(bytesOf(wide), 4, true) == 0xffff, "a 32-bit restart index counted as a vertex, or 0xffff was taken for it"); + Require(HighestDrawIndex(bytesOf(wide), 4, false) == 0xffffffffu, "a 32-bit all-ones index without restart was skipped"); + const std::array restartOnly{0xffff, 0xffff, 0xffff, 0xffff}; + Require(!HighestDrawIndex(bytesOf(restartOnly), 2, true).has_value(), "a draw of only restart indices reached a vertex"); + 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() { + using AgcDriver::Graphics::StorageTexture; + Require(!StorageTexture::StoreAtFlipRequested(nullptr), "an unset APS5_STORE_AT_FLIP stored at each flip"); + Require(StorageTexture::StoreAtFlipRequested("1"), "APS5_STORE_AT_FLIP=1 did not store at each flip"); + 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}; + ShaderRecompiler::VertexAttribute attribute{}; + attribute.formatComponents = 1; + for (std::uint32_t components = 1; components <= 4; ++components) { + attribute.components = components; + const auto format = DecodeVertexFormat(attribute); + Require(format.format == formats[components - 1] && format.bytes == components * 4u && std::string_view(format.scalar) == "f32", "a null V# did not read as zero floats of the attribute's width"); + Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100000, 3, 2) == format.bytes && AgcDriver::Graphics::VertexBufferExtent(attribute) == format.bytes, "a null V# was sized as a guest range"); + } + attribute.resource.fields = {0x1000, 0, 0, 0}; + expectFailure([&] { DecodeVertexFormat(attribute); }, "unsupported vertex format"); +} + void vertexCopyTests() { using AgcDriver::Graphics::PlanVertexCopies; using AgcDriver::Graphics::VertexFetch; @@ -2887,6 +10006,12 @@ void vertexCopyTests() { const auto plan = PlanVertexCopies(fetches); Require(plan.copies.size() == 1 && plan.copies[0].first == 0x1000 && plan.copies[0].second == 0x1018 + 32 * 9 + 8, "interleaved attributes were not copied as one union"); Require(plan.copyOf == std::vector{0, 0, 0} && plan.offsets == std::vector{0x18, 0, 0xc}, "interleaved attribute offsets are wrong"); + Require(plan.alignments == std::vector{4}, "the union's alignment is not its attributes'"); + } + { + const std::array fetches{{{0x4000, 0x4100, 16, 0, 2}, {0x4004, 0x4104, 16, 0, 4}}}; + const auto plan = PlanVertexCopies(fetches); + Require(plan.copies.size() == 1 && plan.alignments == std::vector{4}, "a union did not take its strictest attribute alignment"); } { const std::array fetches{{ @@ -2915,7 +10040,553 @@ void vertexCopyTests() { } } -int main() { +void vertexZeroFillTests() { + using AgcDriver::Graphics::CopyZeroPaddedDrawInput; + using AgcDriver::Graphics::VertexBufferReadSize; + constexpr std::uint32_t stride = 24; + constexpr std::uint32_t records = 3; + const ShaderRecompiler::VertexAttribute attribute{0, 4, {{0x1000u, stride << 16u, records, 77u << 12u}}, 0}; + const auto extent = VertexBufferReadSize(attribute, records, 1); + const auto valid = static_cast(stride) * records; + std::vector guest(extent); + std::fill(guest.begin(), guest.begin() + static_cast(valid), std::byte{0x7a}); + std::fill(guest.begin() + static_cast(valid), guest.end(), std::byte{0x55}); + const auto copy = CopyZeroPaddedDrawInput(mockContext(), reinterpret_cast(guest.data()), extent, valid); + const auto bytes = copy.buffer->Bytes(); + Require(bytes.size() == extent, "a zero-padded vertex fetch changed its size"); + Require(std::all_of(bytes.begin(), bytes.begin() + static_cast(valid), [](std::byte value) { return value == std::byte{0x7a}; }), "a vertex fetch did not copy the bytes within the record count"); + 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; + using AgcDriver::Graphics::VertexFetch; + const std::vector fetches{{0x1000u, 0x1000u + 88u, 24u, 0u, 4u}, {0x1010u, 0x1010u + 80u, 24u, 0u, 4u}}; + Require(PlanVertexCopies(fetches).copies.size() == 1, "interleaved attributes share one vertex copy"); + Require(SoloZeroPaddedFetchIndices(fetches, {88u, 80u}).empty(), "fully valid fetches share the straight copy"); + const auto solo = SoloZeroPaddedFetchIndices(fetches, {88u, 72u}); + Require(solo.size() == 1 && solo[0] == 1, "a fetch past the records is copied alone"); +} + +void parallelCompareTests() { + using AgcDriver::Graphics::CompareSpan; + using AgcDriver::Graphics::CompareSpansFrom; + constexpr std::size_t block = 65536; + constexpr std::size_t count = 48; + std::vector first(block * count + 100, std::byte{7}); + auto second = first; + const std::array changedAt{3 * block + 17, 20 * block, 33 * block + block - 1, 47 * block + 120}; + for (const auto at : changedAt) second[at] = std::byte{9}; + std::vector spans; + for (std::size_t k = 0; k < count; ++k) { + const auto length = k + 1 == count ? block + 100 : block; + spans.push_back({first.data() + k * block, second.data() + k * block, length}); + } + const auto expected = [&](std::size_t k) { + return std::none_of(changedAt.begin(), changedAt.end(), [&](std::size_t at) { return at >= k * block && at < k * block + spans[k].bytes; }); + }; + for (const std::size_t threshold : {std::size_t{0}, std::size_t{1} << 40u}) { + for (int round = 0; round < 20; ++round) { + std::vector equal(count, 2); + CompareSpansFrom(spans, equal, threshold); + for (std::size_t k = 0; k < count; ++k) Require((equal[k] == 1) == expected(k) && equal[k] <= 1, "parallel compare misreported a span"); + } + } + std::vector left(count, 2), right(count, 2); + std::thread other([&] { + for (int round = 0; round < 50; ++round) CompareSpansFrom(spans, right, 0); + }); + for (int round = 0; round < 50; ++round) CompareSpansFrom(spans, left, 0); + other.join(); + for (std::size_t k = 0; k < count; ++k) Require((left[k] == 1) == expected(k) && (right[k] == 1) == expected(k), "concurrent parallel compares misreported a span"); + std::vector none; + CompareSpansFrom({}, none, 0); +} + +void ResourceContentKeyTests(bool benchmark) { + using AgcDriver::Graphics::ShaderResources; + using ShaderRecompiler::DescriptorKind; + using ShaderRecompiler::DescriptorRole; + for (const auto count : {0u, 1u, 31u, 32u, 33u, 16384u}) { + ShaderRecompiler::RecompileResult program; + ShaderRecompiler::DescriptorBinding images{}; + images.kind = DescriptorKind::SampledImage; + images.role = DescriptorRole::GuestImages; + images.count = count; + images.guestDescriptor.resize(static_cast(count) * 8u); + for (std::size_t i = 0; i < images.guestDescriptor.size(); ++i) images.guestDescriptor[i] = static_cast(i * 1664525u + 1013904223u); + for (auto* bits : {&images.imageWritten, &images.samplerDepthCompare, &images.imageDepthCompare, &images.imageAtomic, &images.samplerUnnormalized, &images.samplerIntegerBorder, &images.imageUnnormalized, &images.imageUnnormalizedBaseLevel, &images.bufferWritten}) { + bits->resize(count); + for (std::size_t i = 0; i < bits->size(); ++i) (*bits)[i] = i % 3u == 0; + } + images.imageSamplers.resize(count, 7u); + images.imageShapes.resize(count, ShaderRecompiler::DescriptorImageShape::Image2D); + program.bindings.push_back(std::move(images)); + ShaderRecompiler::DescriptorBinding data{}; + data.kind = DescriptorKind::StorageBuffer; + data.role = DescriptorRole::ShaderData; + data.count = 1; + data.guestDescriptor.resize(257u, 0x12345678u); + program.bindings.push_back(std::move(data)); + ShaderRecompiler::DescriptorBinding buffers{}; + buffers.kind = DescriptorKind::StorageBuffer; + buffers.role = DescriptorRole::GuestBuffers; + buffers.count = 1; + buffers.guestDescriptor = {0x1000u, 0x10000u, 4u, 0x1234u}; + buffers.bufferWritten = {false}; + program.bindings.push_back(std::move(buffers)); + const AgcDriver::Graphics::CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + for (const bool dataWords : {false, true}) for (const bool movable : {false, true}) { + const auto original = ShaderResources::ContentKey(shader, dataWords, movable); + program.bindings[1].guestDescriptor.back() ^= 1u; + Require((ShaderResources::ContentKey(shader, dataWords, movable) != original) == (dataWords && !movable), "content key changed its data-word policy"); + program.bindings[1].guestDescriptor.back() ^= 1u; + program.bindings[2].guestDescriptor[0] += 0x1000u; + Require((ShaderResources::ContentKey(shader, dataWords, movable) != original) == !movable, "content key changed its movable-buffer policy"); + program.bindings[2].guestDescriptor[0] -= 0x1000u; + program.bindings[2].bufferWritten[0] = true; + Require(ShaderResources::ContentKey(shader, dataWords, movable) != original, "content key ignored the written-buffer mask"); + program.bindings[2].bufferWritten[0] = false; + if (count != 0) { + for (auto* bits : {&program.bindings[0].imageWritten, &program.bindings[0].samplerDepthCompare, &program.bindings[0].imageDepthCompare, &program.bindings[0].imageAtomic, &program.bindings[0].samplerUnnormalized, &program.bindings[0].samplerIntegerBorder, &program.bindings[0].imageUnnormalized, &program.bindings[0].imageUnnormalizedBaseLevel}) { + bits->back().flip(); + Require(ShaderResources::ContentKey(shader, dataWords, movable) != original, "content key ignored the last packed flag"); + bits->back().flip(); + } + ++program.bindings[0].imageSamplers.back(); + Require(ShaderResources::ContentKey(shader, dataWords, movable) != original, "content key ignored the last sampler association"); + --program.bindings[0].imageSamplers.back(); + 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; + for (const auto word : original) hash = (hash ^ word) * 1099511628211ull; + std::cout << "content-key count=" << count << " data=" << dataWords << " movable=" << movable << " words=" << original.size() << " capacity=" << original.capacity() << " hash=" << hash; + if (benchmark) { + constexpr std::size_t repeats = 256; + std::uint64_t checksum = 0; + const auto begin = std::chrono::steady_clock::now(); + for (std::size_t i = 0; i < repeats; ++i) { + const auto key = ShaderResources::ContentKey(shader, dataWords, movable); + checksum += key.size() + key.back(); + } + const auto elapsed = std::chrono::duration(std::chrono::steady_clock::now() - begin).count(); + std::cout << " us/key=" << elapsed / repeats << " checksum=" << checksum; + } + std::cout << '\n'; + } + } +} + +// 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 gridState = state; + gridState.samples = 2; + gridState.customSampleLocations = true; + gridState.sampleLocationGrid = true; + gridState.perSampleShading = true; + gridState.sampleLocations[31] = 0x5a; + GraphicsPipelineRecipe gridDecoded; + Require(DecodeGraphicsPipelineRecipe(EncodeGraphicsPipelineRecipe(MakeGraphicsPipelineRecipe(gridState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL)), gridDecoded), "the sample grid recipe does not decode"); + Require(gridDecoded.state.sampleLocationGrid && gridDecoded.state.perSampleShading && gridDecoded.state.sampleLocations[31] == 0x5a, "the recipe dropped the sample grid, per-sample shading or the last sample location"); + std::vector grid, gridReplayed; + BuildPipelineStoreKey(grid, context, gridState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + BuildPipelineStoreKey(gridReplayed, context, gridDecoded.state, gridDecoded.vertexInput, layouts.View(), shaders, gridDecoded.attachmentLayout); + Require(grid != original && grid == gridReplayed, "the decoded sample grid state keys another pipeline"); + auto lineState = state; + lineState.stages.mesh = ShaderRecompiler::MeshConfiguration{1, 2, 2, 4, 4, 32, 64, 1, 4, 2}; + GraphicsPipelineRecipe lineDecoded; + Require(DecodeGraphicsPipelineRecipe(EncodeGraphicsPipelineRecipe(MakeGraphicsPipelineRecipe(lineState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL)), lineDecoded), "the line strip mesh recipe does not decode"); + Require(lineDecoded.state.stages.mesh && lineDecoded.state.stages.mesh->outputPrimitiveSize == 2u && lineDecoded.state.stages.mesh->esgsItemSize == 4u, "the recipe dropped the mesh output primitive size"); + auto fourSampleState = state; + fourSampleState.samples = 4; + auto maskState = fourSampleState; + maskState.quadSampleMask = {0x3u, 0x3u, 0x3u, 0x3u}; + maskState.stencilReplayReference = 7u; + maskState.reversedBlend = 1u; + GraphicsPipelineRecipe maskDecoded; + Require(DecodeGraphicsPipelineRecipe(EncodeGraphicsPipelineRecipe(MakeGraphicsPipelineRecipe(maskState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL)), maskDecoded), "the sample mask recipe does not decode"); + Require(maskDecoded.state.quadSampleMask == maskState.quadSampleMask && maskDecoded.state.stencilReplayReference.has_value() && maskDecoded.state.reversedBlend == 1u, "the recipe dropped the quad sample masks, the replayed stencil reference or the reversed blend"); + std::vector fourSamples, masked, maskReplayed; + BuildPipelineStoreKey(fourSamples, context, fourSampleState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + BuildPipelineStoreKey(masked, context, maskState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + BuildPipelineStoreKey(maskReplayed, context, maskDecoded.state, maskDecoded.vertexInput, layouts.View(), shaders, maskDecoded.attachmentLayout); + Require(masked != fourSamples && masked == maskReplayed, "the decoded sample mask state keys another pipeline"); + auto 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"); _putenv_s("APS5_HEAP_MIRROR_MIB", "4"); @@ -2924,6 +10595,208 @@ int main() { 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; + { + auto context = mockContext(); + context.limits.maxStorageBufferRange = 1u << 27; + passed = RunSparseBufferHoleTests(context, { + [](VkBuffer buffer) -> std::span { return mockBufferMemory(buffer); }, {}, {}, {}, {} + }); + } + Require(mock.live == 0, "sparse buffer hole tests leaked Vulkan objects"); + std::cout << "sparse buffer hole tests " << (passed ? "passed" : "failed") << '\n'; + return passed ? 0 : 1; + } + if (argc == 2 && (std::string_view(argv[1]) == "--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"; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--buffer-budget-placement") { + 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(); + bool snapshotAcquired = false; + std::thread snapshot([&] { snapshotAcquired = synchronization.TryLockForSnapshot().owns_lock(); }); + snapshot.join(); + Require(!snapshotAcquired, "pipeline cache snapshot overlapped pipeline creation"); + pipelineUse.unlock(); + Require(synchronization.TryLockForSnapshot().owns_lock(), "pipeline cache snapshot stayed blocked after pipeline creation finished"); + } + if (argc == 2 && std::string_view(argv[1]) == "--undithered-alpha-to-coverage-state") { + UnditheredAlphaToCoverageTests(); + std::cout << "Existing undithered alpha-to-coverage states passed\n"; + return 0; + } + if (argc == 2 && (std::string_view(argv[1]) == "--resource-content-key" || std::string_view(argv[1]) == "--benchmark-resource-content-key")) { + ResourceContentKeyTests(std::string_view(argv[1]) == "--benchmark-resource-content-key"); + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--descriptor-layout-flags") { + descriptorLayoutFlagTests(); + std::cout << "Descriptor layout native and captured binding flags passed\n"; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--alpha-to-coverage-state") { + MsaaEnableSingleSampleTests(); + AlphaToCoverageTests(); + std::cout << "Alpha-to-coverage state and refusal tests passed\n"; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--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"; + return 1; + } + if (argc == 2 && std::string_view(argv[1]) == "--covered-centroid-state") { + multisampleTests(); + std::cout << "Covered-centroid and multisample state checks passed\n"; + return 0; + } + 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{}; @@ -2955,49 +10828,131 @@ int main() { AgcDriver::Graphics::ValidateDepthBounds(unrestricted, bounded); } RunGuestLeaseWaitTests(); + ResourceContentKeyTests(false); stateTests(); + pipelineStoreLazyBufferPoolTests(); + retainedPixelStageTests(); + pipelineLibrarySetTests(); + pipelineLibraryClearWaitsForBuildTests(); + framebufferOwnerTests(); + hdrComponentSwapTests(); depthMaintenanceTests(); hardwareScreenOffsetTests(); srgb8TargetTests(); DepthClipTests(); + DepthRemapTests(); + UnusedViewportDepthTests(); DepthStencilTests(); + OneDimensionalColorTests(); + htileDepthClearTests(); + 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(); cmaskTests(); uint16ExportTests(); + uint8x2TargetTests(); + uint8x4TargetTests(); + msaaEnableClearDepthTests(); + multisampleTests(); + MsaaEnableSingleSampleTests(); + multisampledDepthTests(); + clipDistanceTests(); + framebufferLayerTests(); + msaaEnableClearOneSampleTargetTests(); + UnwrittenSampleLocationTests(); + sampleLocationGridFallbackTests(); + sampleOrderTests(); + depthCopyFormatTests(); + quadPixelMaskTests(); ShaderStageTests(); + UnsupportedTessellationFieldTests(); + NggVertexWorkgroupTests(); + inactiveVertexPointSizeTests(); + PolygonPrimitiveTests(); TuningFieldTests(); PixelInputLayoutTests(); + UnexportedPixelInputTests(); + ColorPipeBankXorTests(); + HeaderlessVertexStageTests(); + HiddenUserWordVertexTablesTests(); ComputeScratchTests(); + shaderUserDataTailPaddingTests(); InitialContextTests(); + AlphaToCoverageTests(); + descriptorPoolTests(); pushConstantTests(); resourceTests(); + oversizedDescriptorTests(); + guestBufferViewBytesTests(); + descriptorSnapshotTests(); + dispatchMoveTests(); descriptorCacheTests(); + bufferBudgetPlacementTests(); + imageTableAllocationTests(); + imageTableSnapshotAllocationTests(); + samplerTableNonSeamlessCubeTests(); + samplerTableDeviceLimitTests(); + tableUpdateAfterBindTests(); misalignedShaderDataTests(); debugBranchTests(); + hostImportBudgetTests(); textureCacheBudgetTests(); sampledTextureBudgetTests(); + storageTextureBudgetTests(); sampledBudgetReportTests(); meshArgumentTests(); meshIndexBufferTests(); + stagingPoolBudgetTests(); validationTests(); vertexCopyTests(); + highestDrawIndexTests(); + storeAtFlipTests(); + storeAtLabelTests(); + nullVertexDescriptorTests(); + vertexZeroFillTests(); + vertexOverhangTests(); + vertexZeroFillMergeTests(); + parallelCompareTests(); pixelParameterSlotTests(); rectListTests(); + quadListTests(); floatControlsModeTests(); + barycentricEmulationTests(); + copiedWriterIndirectOverlapTests(); + registeredReadableCoverTests(); mock = MockVulkan{}; auto bdaContext = mockContext(); bdaContext.bufferDeviceAddress = true; bdaContext.limits.maxStorageBufferRange = 1u << 27; RunBdaResourceTests(bdaContext, { - [](VkBuffer buffer) -> std::span { return mock.memories.at(mock.bufferMemory.at(buffer)); }, + [](VkBuffer buffer) -> std::span { return mockBufferMemory(buffer); }, [](std::uint32_t binding) { for (auto it = mock.writes.rbegin(); it != mock.writes.rend(); ++it) { if (it->binding == binding) return it->buffers.at(0); @@ -3007,12 +10962,13 @@ int main() { [](VkDeviceAddress address) { const auto offset = address - 0x100000000000ULL; const auto buffer = reinterpret_cast(offset / 0x10000); - return std::span(mock.memories.at(mock.bufferMemory.at(buffer))).subspan(offset % 0x10000); + return mockBufferMemory(buffer).subspan(offset % 0x10000); }, [](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(); @@ -3063,7 +11019,13 @@ int main() { 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 b016d793e7..89d83afd29 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp @@ -1,11 +1,13 @@ #include "BdaTests.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/GuestHeap.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include #include #include +#include #include #include #include @@ -27,6 +29,70 @@ void reject(TAction action) { throw std::runtime_error("expected guest allocation ownership rejection"); } +void leaseSnapshotTests() { + std::array memory{}; + const auto address = reinterpret_cast(memory.data()); + { + GuestAllocations::Mutation mutation; + mutation.Add(memory.data(), 64, true, true, true); + } + std::weak_ptr held; + { + const auto first = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + const auto second = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + Require(!first.empty() && first.size() == second.size() && &first[0] == &second[0], "two leases taken without a registry change do not share one snapshot"); + const auto found = std::find_if(first.begin(), first.end(), [&](const auto& range) { return range->address == address; }); + Require(found != first.end(), "the shared snapshot misses a registered range"); + held = *found; + Require(held.use_count() == 2, "the shared snapshot of two leases holds a range more than once"); + { + GuestAllocations::Mutation mutation; + mutation.Add(memory.data() + 64, 64, true, true, true); + } + const auto third = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + Require(&third[0] != &first[0] && std::any_of(third.begin(), third.end(), [&](const auto& range) { return range->address == address + 64; }), "a lease taken after a registry change still shares the old snapshot"); + } + 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); +} + } void RunGuestLeaseWaitTests() { @@ -34,11 +100,13 @@ void RunGuestLeaseWaitTests() { const auto address = reinterpret_cast(memory.data()); { GuestAllocations::Mutation mutation; - mutation.Add(memory.data(), 64, true, true); - mutation.Add(memory.data() + 64, 64, true, true); + mutation.Add(memory.data(), 64, true, true, true); + mutation.Add(memory.data() + 64, 64, true, true, true); + } + GuestAllocations::Lease::Ranges lease; + for (const auto& range : GuestAllocations::GuestAllocationsAcquire_nid_postfix()) { + if (range->address == address) lease.push_back(range); } - auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); - std::erase_if(lease, [&](const auto& range) { return range->address != address; }); std::atomic mutationEntered = false; std::atomic lookupFinished = false; std::exception_ptr failure; @@ -55,8 +123,10 @@ void RunGuestLeaseWaitTests() { } catch (...) { failure = std::current_exception(); } }); mutationEntered.wait(false); - auto other = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); - std::erase_if(other, [&](const auto& range) { return range->address != address + 64; }); + GuestAllocations::Lease::Ranges other; + for (const auto& range : GuestAllocations::GuestAllocationsAcquire_nid_postfix()) { + if (range->address == address + 64) other.push_back(range); + } const bool found = other.size() == 1 && other.front()->address == address + 64; lookupFinished.store(true); lease.clear(); @@ -73,6 +143,7 @@ void RunGuestLeaseWaitTests() { } void RunGuestAllocationTests() { + leaseSnapshotTests(); void* pointer = GuestHeap::GuestHeapAllocate_nid_postfix(32); Require(reinterpret_cast(pointer) % alignof(std::max_align_t) == 0, "guest malloc is not suitably aligned"); std::memset(pointer, 0x55, 32); @@ -83,7 +154,7 @@ void RunGuestAllocationTests() { reject([&] { GuestHeap::GuestHeapReallocate_nid_postfix(pointer, 64); }); GuestAllocations::Mutation mutation; bool applied = false; - reject([&] { mutation.Protect(pointer, 32, true, false, [&] { applied = true; }); }); + reject([&] { mutation.Protect(pointer, 32, true, false, true, [&] { applied = true; }); }); Require(!applied, "pinned guest protection changed"); } pointer = GuestHeap::GuestHeapReallocate_nid_postfix(pointer, 64); @@ -102,9 +173,9 @@ void RunGuestAllocationTests() { std::array mapping{}; { GuestAllocations::Mutation mutation; - mutation.Add(mapping.data(), mapping.size(), true, true); + mutation.Add(mapping.data(), mapping.size(), true, true, true); reject([&] { mutation.RequireAvailable(mapping.data() + 32, 16); }); - mutation.Protect(mapping.data() + 32, 32, true, false, [] {}); + mutation.Protect(mapping.data() + 32, 32, true, false, true, [] {}); } { const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); @@ -120,7 +191,7 @@ void RunGuestAllocationTests() { applied = true; }); Require(applied, "partial unmap callback was not called"); - reject([&] { mutation.Protect(mapping.data(), mapping.size(), true, true, [] {}); }); + reject([&] { mutation.Protect(mapping.data(), mapping.size(), true, true, true, [] {}); }); mutation.Unmap(mapping.data(), 32, [&](const void*, std::size_t, const void* allocation, bool last) { Require(allocation == mapping.data() && !last, "first fragment released remaining mapping"); }); @@ -144,10 +215,11 @@ void RunGuestAllocationTests() { imageRangeCount = lease.size(); const auto found = std::find_if(lease.begin(), lease.end(), [&](const auto& range) { return imageAddress >= range->address && imageAddress - range->address < range->bytes; }); Require(found != lease.end() && (*found)->writable && !(*found)->releasable, "main image registration is missing or releasable"); + Require(std::none_of(lease.begin(), lease.end(), [](const auto& range) { return !range->releasable && range->gpu; }), "main image registered as GPU-mapped"); allocationAddress = (*found)->allocationAddress; GuestAllocations::Mutation mutation; bool applied = false; - reject([&] { mutation.Protect(&imageProbe, 1, true, false, [&] { applied = true; }); }); + reject([&] { mutation.Protect(&imageProbe, 1, true, false, true, [&] { applied = true; }); }); Require(!applied, "pinned image protection changed"); } { @@ -157,12 +229,12 @@ void RunGuestAllocationTests() { bool applied = false; reject([&] { mutation.Unmap(&imageProbe, 1, [&](const void*, std::size_t, const void*, bool) { applied = true; }); }); Require(!applied, "image memory was unmapped"); - reject([&] { mutation.Protect(&imageProbe, 1, true, false, [] { throw std::runtime_error("host protection failure"); }); }); + reject([&] { mutation.Protect(&imageProbe, 1, true, false, true, [] { throw std::runtime_error("host protection failure"); }); }); } Require(GuestAllocations::GuestAllocationsAcquire_nid_postfix().size() == imageRangeCount, "failed image protection changed registry ranges"); { GuestAllocations::Mutation mutation; - mutation.Protect(&imageProbe, 1, true, true, [] {}); + mutation.Protect(&imageProbe, 1, true, true, true, [] {}); bool applied = false; reject([&] { mutation.Unmap(&imageProbe, 1, [&](const void*, std::size_t, const void*, bool) { applied = true; }); }); Require(!applied, "split image memory became releasable"); @@ -186,5 +258,73 @@ 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"); + { + GuestAllocations::Mutation mutation; + mutation.Add(guest, 2 * page, true, true, true); + } + Require(GuestMemory::Accessible(guest, page, true), "a mapped arena page is not writable"); + Require(mprotect(guest, page, PROT_NONE) == 0, "cannot revoke the arena test page behind the registry"); + Require(GuestMemory::Accessible(guest, page, true), "a checked arena page was queried again instead of read from the page table"); + Require(mprotect(guest, page, PROT_READ | PROT_WRITE) == 0, "cannot restore the arena test page behind the registry"); + Require(mprotect(guest + page, page, PROT_READ) == 0, "cannot protect the arena test's second page"); + Require(!GuestMemory::Accessible(guest + page, page, true), "a check stored arena pages outside its range"); + { + GuestAllocations::Mutation mutation; + mutation.Protect(guest + page, page, true, true, true, [&] { Require(mprotect(guest + page, page, PROT_READ | PROT_WRITE) == 0, "cannot restore the arena test's second page"); }); + } + Require(GuestMemory::Accessible(guest, 2 * page, true), "mapped arena pages are not writable"); + { + GuestAllocations::Mutation mutation; + mutation.Protect(guest, page, true, false, true, [&] { Require(mprotect(guest, page, PROT_READ) == 0, "cannot protect the arena test page"); }); + } + Require(!GuestMemory::Accessible(guest, page, true) && GuestMemory::Accessible(guest, page) && GuestMemory::Accessible(guest + page, page, true), "an arena page protected through the registry kept its old access"); + { + GuestAllocations::Mutation mutation; + const auto release = [](const void* pointer, std::size_t bytes, const void*, bool) { Require(munmap(const_cast(pointer), bytes) == 0, "cannot unmap an arena test page"); }; + mutation.Unmap(guest, page, release); + 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 c27a28492b..7f5b21682c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp @@ -93,6 +93,9 @@ struct SamplerCapture { bool created = false; const void* next = nullptr; VkSamplerReductionMode reductionMode = VK_SAMPLER_REDUCTION_MODE_MAX_ENUM; + VkSamplerCreateFlags flags = 0; + bool anisotropyEnable = false; + float maxAnisotropy = 0.0f; }; SamplerCapture samplerCapture; @@ -105,6 +108,9 @@ THandle fakeHandle(std::uint64_t value) { VKAPI_ATTR VkResult VKAPI_CALL captureCreateSampler(VkDevice, const VkSamplerCreateInfo* info, const VkAllocationCallbacks*, VkSampler* sampler) { 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); @@ -172,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; @@ -211,6 +233,20 @@ void RunSamplerReductionTests(const Fields& base) { reject([&] { RequireFilterMinmax(context, VK_FORMAT_R32G32_SFLOAT, 0b011u, samplers); }, "does not support min/max filtering"); reject([&] { RequireFilterMinmax(context, VK_FORMAT_R32_SFLOAT, 0b1000u, samplers); }, "sampler element 3, which its shader does not bind"); + const auto borderSampler = [&](std::uint32_t clamp, std::uint32_t borderColorType) { + Fields fields = base; + fields.clampY = clamp; + fields.borderColorType = borderColorType; + return std::make_shared(context, DecodeSamplerResource(pack(fields))); + }; + const std::array borderSamplers{borderSampler(6u, 0u), borderSampler(6u, 1u), borderSampler(6u, 2u), borderSampler(2u, 1u)}; + Require(!borderSamplers[0]->ReadsOpaqueBlackBorder() && borderSamplers[1]->ReadsOpaqueBlackBorder() && !borderSamplers[2]->ReadsOpaqueBlackBorder() && !borderSamplers[3]->ReadsOpaqueBlackBorder(), "opaque black border detection is wrong"); + for (std::uint32_t swizzle = 0; swizzle <= 5u; ++swizzle) RequireBorderSwizzle(swizzle, 0b1101u, borderSamplers); + RequireBorderSwizzle(0u, 0b0010u, borderSamplers); + RequireBorderSwizzle(4u, 0b0010u, borderSamplers); + for (std::uint32_t swizzle : {1u, 2u, 3u, 5u}) reject([&] { RequireBorderSwizzle(swizzle, 0b0010u, borderSamplers); }, "moves the alpha channel is sampled through an opaque black border"); + reject([&] { RequireBorderSwizzle(1u, 0b10000u, borderSamplers); }, "sampler element 4, which its shader does not bind"); + const auto pointFiltered = [](const std::array& words) { return ShaderRecompiler::PointFilteredSamplerWord(words[0], words[2]); }; Require(pointFiltered(pack(base)) == 0x05000000u, "a point-filtered weighted-average sampler kept its bilinear or linear mip filter"); Require(pointFiltered(pack(minPoint)) == pack(minPoint)[2], "a point-filtered min sampler that already point-samples changed"); @@ -219,6 +255,37 @@ 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) { + Fields nonSeamless = base; + nonSeamless.disableCubeWrap = true; + const auto decoded = DecodeSamplerResource(pack(nonSeamless)); + Require(decoded.nonSeamlessCube, "DISABLE_CUBE_WRAP was not decoded as a non-seamless cube sampler"); + Require(!DecodeSamplerResource(pack(base)).nonSeamlessCube, "a sampler without DISABLE_CUBE_WRAP decoded as non-seamless"); + + Context context{}; + context.limits.maxSamplerAnisotropy = 1.0f; + context.deviceProc = captureProc; + context.nonSeamlessCubeMap = true; + Require((createSampler(context, decoded).flags & VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT) != 0u, "a non-seamless cube sampler was created without VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT"); + Require(createSampler(context, DecodeSamplerResource(pack(base))).flags == 0u, "a seamless sampler was created with flags"); + const std::array honored{std::make_shared(context, decoded)}; + Require(!honored[0]->RequiresNonSeamlessCube(), "a sampler created with the non-seamless flag still restricts cube textures"); + RequireNonSeamlessCube(true, 0b1u, honored); + + context.nonSeamlessCubeMap = false; + Require(createSampler(context, decoded).flags == 0u, "a sampler set the non-seamless flag without the device feature"); + const std::array unsupported{std::make_shared(context, DecodeSamplerResource(pack(base))), std::make_shared(context, decoded)}; + Require(unsupported[1]->RequiresNonSeamlessCube(), "a non-seamless sampler without the device feature does not restrict cube textures"); + RequireNonSeamlessCube(false, 0b11u, unsupported); + RequireNonSeamlessCube(true, 0b01u, unsupported); + reject([&] { RequireNonSeamlessCube(true, 0b10u, unsupported); }, "disables seamless cube filtering, which needs VK_EXT_non_seamless_cube_map"); } void RunSamplerCacheDegammaTests(const Fields& base) { @@ -246,7 +313,7 @@ void RunGuestSamplerResourceTests() { Require(result.addressModeU == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, "clamp mode 2 must decode to clamp-to-edge"); Require(!result.anisotropyEnable && result.maxAnisotropy == 1.0f, "non-anisotropic filter must leave anisotropy disabled"); Require(nearlyEqual(result.maxLod, 12.0f), "max LOD decoded incorrectly"); - Require(result.borderColor == VK_BORDER_COLOR_INT_TRANSPARENT_BLACK, "border color type 0 must decode to transparent black"); + Require(result.borderColor == VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK, "border color type 0 must decode to transparent black"); Fields nearest = base; nearest.xyMagFilter = 0; @@ -318,7 +385,7 @@ void RunGuestSamplerResourceTests() { requireUnnormalized(unnormalized, "captured unnormalized S#"); Require(unnormalized.magFilter == VK_FILTER_LINEAR && unnormalized.minFilter == VK_FILTER_LINEAR, "captured unnormalized S# filters decoded incorrectly"); Require(unnormalized.addressModeU == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE && unnormalized.addressModeV == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, "captured unnormalized S# clamp modes decoded incorrectly"); - Require(unnormalized.borderColor == VK_BORDER_COLOR_INT_TRANSPARENT_BLACK, "captured unnormalized S# border color decoded incorrectly"); + Require(unnormalized.borderColor == VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK, "captured unnormalized S# border color decoded incorrectly"); Require(!DecodeSamplerResource(pack(base)).unnormalizedCoordinates, "a normalized S# decoded to unnormalized coordinates"); Fields unnormalizedBase = base; @@ -329,7 +396,7 @@ void RunGuestSamplerResourceTests() { const auto point = DecodeSamplerResource(pack(unnormalizedPoint), true); requireUnnormalized(point, "unnormalized point S#"); Require(point.magFilter == VK_FILTER_NEAREST && point.minFilter == VK_FILTER_NEAREST, "unnormalized point S# filters decoded incorrectly"); - const std::array borders{VK_BORDER_COLOR_INT_TRANSPARENT_BLACK, VK_BORDER_COLOR_INT_OPAQUE_BLACK, VK_BORDER_COLOR_INT_OPAQUE_WHITE}; + const std::array borders{VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK, VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK, VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE}; for (std::uint32_t type = 0; type < borders.size(); ++type) { Fields border = unnormalizedBase; border.clampX = 6; @@ -354,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"); @@ -392,27 +475,54 @@ void RunGuestSamplerResourceTests() { truncated.anisoBias = 1; static_cast(DecodeSamplerResource(pack(truncated))); - Fields badCubeWrap = base; - badCubeWrap.disableCubeWrap = true; - rejectFields(badCubeWrap, "seamless cube filtering"); - + RunNonSeamlessCubeTests(base); RunSamplerReductionTests(base); Fields badDegamma = base; 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; rejectFields(badPreclamp, "point preclamping"); - Fields badAnisoOverride = base; - badAnisoOverride.anisoOverride = true; - rejectFields(badAnisoOverride, "anisotropy override"); + Fields anisoOverride = base; + anisoOverride.xyMagFilter = 2; + anisoOverride.xyMinFilter = 2; + anisoOverride.zFilter = 2; + anisoOverride.maxAnisoRatio = 3; + const auto withoutOverride = DecodeSamplerResource(pack(anisoOverride)); + anisoOverride.anisoOverride = true; + const auto withOverride = DecodeSamplerResource(pack(anisoOverride)); + Require(withOverride.anisotropyEnable == withoutOverride.anisotropyEnable && withOverride.maxAnisotropy == withoutOverride.maxAnisotropy && withOverride.magFilter == withoutOverride.magFilter && withOverride.minFilter == withoutOverride.minFilter, "ANISO_OVERRIDE changed the decoded sampler"); + const auto overrideWords = pack(anisoOverride); + Require(!SingleLevelSamplerWords(overrideWords, false, true).has_value(), "ANISO_OVERRIDE changed a sampler of mipmapped images"); + Require(!SingleLevelSamplerWords(overrideWords, false, false).has_value(), "ANISO_OVERRIDE changed a sampler with no paired image"); + const auto singleLevel = SingleLevelSamplerWords(overrideWords, true, false); + Require(singleLevel.has_value() && (*singleLevel)[0] == overrideWords[0] && (*singleLevel)[1] == overrideWords[1] && (*singleLevel)[3] == overrideWords[3], "ANISO_OVERRIDE did not keep the sampler's other words"); + const auto singleLevelSampler = DecodeSamplerResource(*singleLevel); + Require(!singleLevelSampler.anisotropyEnable && singleLevelSampler.maxAnisotropy == 1.0f && singleLevelSampler.magFilter == withOverride.magFilter && singleLevelSampler.minFilter == withOverride.minFilter && singleLevelSampler.mipmapMode == withOverride.mipmapMode, "ANISO_OVERRIDE left anisotropy on a single-level image's sampler"); + reject([&] { SingleLevelSamplerWords(overrideWords, true, true); }, "both single-level and mipmapped"); + Require(!SingleLevelSamplerWords(pack(base), true, false).has_value(), "a sampler without ANISO_OVERRIDE changed"); + Fields linearOverride = base; + linearOverride.anisoOverride = true; + Require(!SingleLevelSamplerWords(pack(linearOverride), true, true).has_value(), "ANISO_OVERRIDE changed a sampler without anisotropy"); + anisoOverride.xyMagFilter = 3; + anisoOverride.xyMinFilter = 3; + const auto linearPlain = DecodeSamplerResource(*SingleLevelSamplerWords(pack(anisoOverride), true, false)); + Require(!linearPlain.anisotropyEnable && linearPlain.magFilter == VK_FILTER_LINEAR && linearPlain.minFilter == VK_FILTER_LINEAR, "ANISO_OVERRIDE did not keep linear filtering"); Fields badBlendZero = base; badBlendZero.blendZeroPrt = true; @@ -443,10 +553,13 @@ void RunGuestSamplerResourceTests() { Fields opaqueBlack = base; opaqueBlack.borderColorType = 1; - Require(DecodeSamplerResource(pack(opaqueBlack)).borderColor == VK_BORDER_COLOR_INT_OPAQUE_BLACK, "border color type 1 must decode to opaque black"); + Require(DecodeSamplerResource(pack(opaqueBlack)).borderColor == VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK, "border color type 1 must decode to opaque black"); + Require(DecodeSamplerResource(pack(opaqueBlack), false, false, true).borderColor == VK_BORDER_COLOR_INT_OPAQUE_BLACK, "border color type 1 of a sampler for integer images must decode to integer opaque black"); Fields opaqueWhite = base; opaqueWhite.borderColorType = 2; - Require(DecodeSamplerResource(pack(opaqueWhite)).borderColor == VK_BORDER_COLOR_INT_OPAQUE_WHITE, "border color type 2 must decode to opaque white"); + Require(DecodeSamplerResource(pack(opaqueWhite)).borderColor == VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE, "border color type 2 must decode to opaque white"); + Require(DecodeSamplerResource(pack(opaqueWhite), false, false, true).borderColor == VK_BORDER_COLOR_INT_OPAQUE_WHITE, "border color type 2 of a sampler for integer images must decode to integer opaque white"); + Require(DecodeSamplerResource(pack(base), false, false, true).borderColor == VK_BORDER_COLOR_INT_TRANSPARENT_BLACK, "border color type 0 of a sampler for integer images must decode to integer transparent black"); Fields positiveBias = base; positiveBias.lodBiasRaw = 256; diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp index 44b30348e0..db38d183f5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -1,7 +1,9 @@ #include "GraphicsTests.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #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 @@ -95,6 +97,18 @@ void rejectFields(const Fields& f, std::string_view reason) { 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"); @@ -103,6 +117,43 @@ 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"); + 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; @@ -119,8 +170,10 @@ void RunGuestTextureResourceTests() { tileModes.tileModeRaw = 0x09; Require(DecodeTextureResource(pack(tileModes)).tileMode == TextureTileMode::kStandard64KB, "tile mode 0x09 must decode to standard 64KB"); tileModes.tileModeRaw = 0x1b; - rejectFields(tileModes, "pipe/bank XOR base"); + const auto xorBase = DecodeTextureResource(pack(tileModes)); + Require(xorBase.baseAddress == 0x12340000ull && xorBase.pipeBankXor == 0x5600u, "a 64 KiB XOR swizzle did not split its base into the block base and its pipe/bank XOR"); tileModes.base40 = 0x120000ull; + Require(DecodeTextureResource(pack(tileModes)).pipeBankXor == 0u, "an aligned 64 KiB XOR base gained a pipe/bank XOR"); Require(DecodeTextureResource(pack(tileModes)).tileMode == TextureTileMode::RenderTarget64KB, "tile mode 0x1b must decode to render target 64KB"); constexpr std::array, 7> added{{{0x02, TextureTileMode::kD256B}, {0x06, TextureTileMode::kD4KB}, {0x0a, TextureTileMode::kD64KB}, {0x11, TextureTileMode::kS64KBT}, {0x12, TextureTileMode::kD64KBT}, {0x15, TextureTileMode::kS4KBX}, {0x16, TextureTileMode::kD4KBX}}}; for (const auto& [raw, mode] : added) { @@ -131,10 +184,18 @@ void RunGuestTextureResourceTests() { tileModes.base40 = 0x120010ull; Require(DecodeTextureResource(pack(tileModes)).tileMode == TextureTileMode::kD4KBX, "a 4 KiB XOR swizzle must accept a 4 KiB aligned base"); tileModes.base40 = 0x120011ull; - rejectFields(tileModes, "pipe/bank XOR base"); + const auto xor4Kb = DecodeTextureResource(pack(tileModes)); + Require(xor4Kb.baseAddress == 0x12001000ull && xor4Kb.pipeBankXor == 0x100u, "a 4 KiB XOR swizzle did not split its base into the block base and its pipe/bank XOR"); tileModes.tileModeRaw = 0x12; tileModes.base40 = 0x120010ull; - rejectFields(tileModes, "pipe/bank XOR base"); + const auto xorT = DecodeTextureResource(pack(tileModes)); + Require(xorT.baseAddress == 0x12000000ull && xorT.pipeBankXor == 0x1000u, "a 64 KiB T swizzle did not split its base into the block base and its pipe/bank XOR"); + tileModes.tileModeRaw = 0x09; + const auto standard = DecodeTextureResource(pack(tileModes)); + Require(standard.baseAddress == 0x12001000ull && standard.pipeBankXor == 0u, "a swizzle without XOR addressing split its base"); + tileModes.tileModeRaw = 0x1b; + tileModes.base40 = 0x56ull; + rejectFields(tileModes, "null base address"); tileModes.base40 = base.base40; tileModes.tileModeRaw = 0x03; rejectFields(tileModes, "unsupported tile mode"); @@ -149,7 +210,26 @@ void RunGuestTextureResourceTests() { Fields twoD = base; twoD.depth = 1; + rejectFields(twoD, "custom pitch other than its default pitch"); + twoD.tileModeRaw = 0x09; rejectFields(twoD, "nonzero depth or base array"); + Fields pitched = base; + pitched.width = 72; + pitched.height = 72; + pitched.depth = 127; + result = DecodeTextureResource(pack(pitched)); + Require(result.dimension == TextureDimension::k2D && result.width == 72 && result.depthOrLastArray == 0, "linear 2D descriptor with its default pitch in DEPTH decoded incorrectly"); + pitched.width = 8; + pitched.height = 8; + pitched.depth = 63; + result = DecodeTextureResource(pack(pitched)); + Require(result.width == 8 && result.depthOrLastArray == 0, "linear 2D descriptor with a one-unit default pitch decoded incorrectly"); + pitched.depth = 127; + rejectFields(pitched, "custom pitch other than its default pitch"); + pitched.depth = 63; + pitched.maxMip = 1; + pitched.lastLevel = 1; + rejectFields(pitched, "custom pitch other than its default pitch"); twoD = base; twoD.baseArray = 1; rejectFields(twoD, "nonzero depth or base array"); @@ -210,10 +290,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"); @@ -285,7 +512,7 @@ void RunGuestTextureResourceTests() { Fields badMsaa = base; badMsaa.msaaDepth = true; - rejectFields(badMsaa, "MSAA"); + rejectFields(badMsaa, "ITERATE_256"); Fields blockSize = base; blockSize.maxUncompBlkSize = 1; @@ -326,9 +553,16 @@ void RunGuestTextureResourceTests() { Require(image.dccPipeAligned == pipeAligned && stored == expected && std::all_of(keys, keys + expected, [](std::uint8_t key) { return key == 0xff; }), std::string("a 1920x1080 SW_64KB_R_X storage image with ") + (pipeAligned ? "pipe-aligned" : "unaligned") + " DCC stored " + std::to_string(stored) + " uncompressed keys, expected " + std::to_string(expected)); } - Fields badSwizzle = base; - badSwizzle.bcSwizzle = 1; - rejectFields(badSwizzle, "BC swizzle"); + for (std::uint32_t swizzle = 0; swizzle <= 5u; ++swizzle) { + Fields borderSwizzle = base; + borderSwizzle.bcSwizzle = swizzle; + Require(DecodeTextureResource(pack(borderSwizzle)).bcSwizzle == swizzle, "a texture descriptor lost its BC swizzle " + std::to_string(swizzle)); + } + for (std::uint32_t swizzle : {6u, 7u}) { + Fields badSwizzle = base; + badSwizzle.bcSwizzle = swizzle; + rejectFields(badSwizzle, "reserved BC swizzle"); + } Fields badLevels = base; badLevels.baseLevel = 2; @@ -401,6 +635,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"); @@ -408,4 +647,59 @@ void RunGuestTextureResourceTests() { Require(!MatchesGuestDimension(Shape::Image3D, TextureDimension::k2D), "3D shape must never match a guest dimension"); Require(!MatchesGuestDimension(Shape::Image3D, TextureDimension::k2DArray), "3D shape must never match a guest dimension"); Require(!MatchesGuestDimension(Shape::Image3D, TextureDimension::kCube), "3D shape must never match a guest dimension"); + + const auto layered = [](TextureDimension dimension, std::uint32_t lastLayer, std::uint32_t baseArray = 0) { + GuestTextureResource resource{}; + resource.dimension = dimension; + resource.depthOrLastArray = lastLayer; + resource.baseArray = baseArray; + return resource; + }; + const auto twoSlices = layered(TextureDimension::k2DArray, 1); + Require(ArrayLayersServeView(twoSlices, layered(TextureDimension::k2D, 0)), "a 2D texture must view the first layer of a 2D array color surface (slice-0 view with MIP0_DEPTH 1)"); + Require(ArrayLayersServeView(twoSlices, twoSlices), "a 2D array must view the same 2D array"); + Require(ArrayLayersServeView(layered(TextureDimension::k2DArray, 3), twoSlices), "a 2D array must view the first layers of a larger 2D array"); + Require(!ArrayLayersServeView(twoSlices, layered(TextureDimension::k2DArray, 3)), "a 2D array view must not reach past the surface's layers"); + Require(!ArrayLayersServeView(twoSlices, layered(TextureDimension::k2D, 0, 1)), "a 2D view of a later layer must not be served by the array's first layer"); + Require(ArrayLayersServeView(layered(TextureDimension::k2D, 0), layered(TextureDimension::k2D, 0)), "a 2D texture must view the same 2D surface"); + Require(!ArrayLayersServeView(layered(TextureDimension::k2D, 0), twoSlices), "a 2D surface must not serve a 2D array view"); + Require(!ArrayLayersServeView(layered(TextureDimension::k3D, 1), layered(TextureDimension::k2D, 0)), "a 3D surface's first slice must not serve a 2D texture"); + Require(!ArrayLayersServeView(layered(TextureDimension::kCube, 5), layered(TextureDimension::k2D, 0)), "a cube's first face must not serve a 2D texture"); + + Fields streamed = base; + streamed.minLodWarn = 0xabc; + streamed.mipStatsCntEn = true; + streamed.mipStatsCntId = 0x5a; + Require(SampledTexturesShareEntry(pack(base), pack(streamed)), "T#s differing only in MIN_LOD_WARN, MIP_STATS_COUNTER_EN and MIP_STATS_COUNTER_ID must share a sampled texture entry"); + for (const auto& field : {&Fields::minLodWarn, &Fields::mipStatsCntId}) { + Fields one = base; + one.*field = 1; + Require(SampledTexturesShareEntry(pack(base), pack(one)), "a T# differing in one streaming-feedback field must share a sampled texture entry"); + } + Fields moved = streamed; + moved.base40 = base.base40 + 1; + Require(!SampledTexturesShareEntry(pack(base), pack(moved)), "T#s with different base addresses must not share a sampled texture entry"); + Fields reformatted = streamed; + reformatted.format = 57; + Require(!SampledTexturesShareEntry(pack(base), pack(reformatted)), "T#s with different formats must not share a sampled texture entry"); + 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..36d9aba45e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/HostImportLifetime.cpp @@ -0,0 +1,808 @@ +#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) { + Require(!imported->freed.load(), "an unmap inside a host import freed the memory of the side it keeps"); + 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(high->memory == imported->memory && !imported->freed.load(), "the side kept from an unmap inside the host import was imported again"); + 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->memory != imported->memory, "the smaller side of an unmap inside the host import was not imported again"); + 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 new file mode 100644 index 0000000000..8d0d9cddc7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/HostImportRetireHold.cpp @@ -0,0 +1,387 @@ +#include "HostImportFailure.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.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 +#include +#include +#include +#include +#include + +using namespace AgcDriver::Graphics; + +namespace { + +int failures = 0; + +void Expect(bool condition, const std::string& what) { + if (condition) return; + std::fprintf(stderr, "FAIL: %s\n", what.c_str()); + ++failures; +} + +constexpr std::size_t Alignment = 65536; +constexpr std::size_t RangeBytes = std::size_t{1} << 20u; + +struct MockDevice { + std::uintptr_t next = 0x1000; + std::map signaled; + std::vector submitted; + std::set buffers; + std::map sizes; + std::set memories; + std::uint64_t nextAddress = 0x500000000ull; +}; + +MockDevice mock; + +bool Live(VkBuffer buffer) { + return mock.buffers.contains(buffer); +} + +VKAPI_ATTR VkResult VKAPI_CALL mockAllocateCommandBuffers(VkDevice, const VkCommandBufferAllocateInfo*, VkCommandBuffer* commands) { + *commands = reinterpret_cast(mock.next++); + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL mockFreeCommandBuffers(VkDevice, VkCommandPool, std::uint32_t, const VkCommandBuffer*) {} + +VKAPI_ATTR VkResult VKAPI_CALL mockCreateFence(VkDevice, const VkFenceCreateInfo*, const VkAllocationCallbacks*, VkFence* fence) { + *fence = reinterpret_cast(mock.next++); + mock.signaled[*fence] = false; + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL mockDestroyFence(VkDevice, VkFence fence, const VkAllocationCallbacks*) { + mock.signaled.erase(fence); +} + +VKAPI_ATTR VkResult VKAPI_CALL mockGetFenceStatus(VkDevice, VkFence fence) { + return mock.signaled.at(fence) ? VK_SUCCESS : VK_NOT_READY; +} + +VKAPI_ATTR VkResult VKAPI_CALL mockWaitForFences(VkDevice, std::uint32_t count, const VkFence* fences, VkBool32, std::uint64_t) { + for (std::uint32_t i = 0; i < count; ++i) mock.signaled.at(fences[i]) = true; + return VK_SUCCESS; +} + +VKAPI_ATTR VkResult VKAPI_CALL mockResetFences(VkDevice, std::uint32_t count, const VkFence* fences) { + for (std::uint32_t i = 0; i < count; ++i) mock.signaled.at(fences[i]) = false; + 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 fence) { + mock.submitted.push_back(fence); + return VK_SUCCESS; +} + +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 mockCmdUpdateBuffer(VkCommandBuffer, VkBuffer, VkDeviceSize, VkDeviceSize, const void*) {} +VKAPI_ATTR void VKAPI_CALL mockCmdBeginQuery(VkCommandBuffer, VkQueryPool, std::uint32_t, VkQueryControlFlags) {} +VKAPI_ATTR void VKAPI_CALL mockCmdEndQuery(VkCommandBuffer, VkQueryPool, std::uint32_t) {} +VKAPI_ATTR void VKAPI_CALL mockCmdResetQueryPool(VkCommandBuffer, VkQueryPool, std::uint32_t, std::uint32_t) {} +VKAPI_ATTR void VKAPI_CALL mockCmdCopyQueryPoolResults(VkCommandBuffer, VkQueryPool, std::uint32_t, std::uint32_t, VkBuffer, VkDeviceSize, VkDeviceSize, VkQueryResultFlags) {} +VKAPI_ATTR void VKAPI_CALL mockCmdBindPipeline(VkCommandBuffer, VkPipelineBindPoint, VkPipeline) {} +VKAPI_ATTR void VKAPI_CALL mockCmdPushConstants(VkCommandBuffer, VkPipelineLayout, VkShaderStageFlags, std::uint32_t, std::uint32_t, const void*) {} +VKAPI_ATTR void VKAPI_CALL mockCmdDispatch(VkCommandBuffer, std::uint32_t, std::uint32_t, std::uint32_t) {} + +VKAPI_ATTR VkResult VKAPI_CALL mockGetMemoryHostPointerProperties(VkDevice, VkExternalMemoryHandleTypeFlagBits, const void*, VkMemoryHostPointerPropertiesEXT* properties) { + properties->memoryTypeBits = 1; + return VK_SUCCESS; +} + +VKAPI_ATTR VkResult VKAPI_CALL mockCreateBuffer(VkDevice, const VkBufferCreateInfo* info, const VkAllocationCallbacks*, VkBuffer* buffer) { + *buffer = reinterpret_cast(mock.next++); + mock.buffers.insert(*buffer); + mock.sizes[*buffer] = info->size; + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL mockDestroyBuffer(VkDevice, VkBuffer buffer, const VkAllocationCallbacks*) { + mock.buffers.erase(buffer); +} + +VKAPI_ATTR void VKAPI_CALL mockGetBufferMemoryRequirements(VkDevice, VkBuffer buffer, VkMemoryRequirements* requirements) { + requirements->size = mock.sizes.at(buffer); + requirements->alignment = Alignment; + requirements->memoryTypeBits = 1; +} + +VKAPI_ATTR VkResult VKAPI_CALL mockAllocateMemory(VkDevice, const VkMemoryAllocateInfo*, const VkAllocationCallbacks*, VkDeviceMemory* memory) { + *memory = reinterpret_cast(mock.next++); + mock.memories.insert(*memory); + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL mockFreeMemory(VkDevice, VkDeviceMemory memory, const VkAllocationCallbacks*) { + mock.memories.erase(memory); +} + +VKAPI_ATTR VkResult VKAPI_CALL mockBindBufferMemory(VkDevice, VkBuffer, VkDeviceMemory, VkDeviceSize) { + return VK_SUCCESS; +} + +VKAPI_ATTR VkDeviceAddress VKAPI_CALL mockGetBufferDeviceAddress(VkDevice, const VkBufferDeviceAddressInfo* info) { + const auto address = mock.nextAddress; + mock.nextAddress += (mock.sizes.at(info->buffer) + Alignment - 1) / Alignment * Alignment; + return address; +} + +PFN_vkVoidFunction VKAPI_CALL mockProc(VkDevice, const char* name) { + static const std::map table{ + {"vkAllocateCommandBuffers", reinterpret_cast(mockAllocateCommandBuffers)}, + {"vkFreeCommandBuffers", reinterpret_cast(mockFreeCommandBuffers)}, + {"vkCreateFence", reinterpret_cast(mockCreateFence)}, + {"vkDestroyFence", reinterpret_cast(mockDestroyFence)}, + {"vkGetFenceStatus", reinterpret_cast(mockGetFenceStatus)}, + {"vkWaitForFences", reinterpret_cast(mockWaitForFences)}, + {"vkResetFences", reinterpret_cast(mockResetFences)}, + {"vkBeginCommandBuffer", reinterpret_cast(mockBeginCommandBuffer)}, + {"vkEndCommandBuffer", reinterpret_cast(mockEndCommandBuffer)}, + {"vkQueueSubmit", reinterpret_cast(mockQueueSubmit)}, + {"vkCmdPipelineBarrier", reinterpret_cast(mockCmdPipelineBarrier)}, + {"vkCmdUpdateBuffer", reinterpret_cast(mockCmdUpdateBuffer)}, + {"vkCmdBeginQuery", reinterpret_cast(mockCmdBeginQuery)}, + {"vkCmdEndQuery", reinterpret_cast(mockCmdEndQuery)}, + {"vkCmdResetQueryPool", reinterpret_cast(mockCmdResetQueryPool)}, + {"vkCmdCopyQueryPoolResults", reinterpret_cast(mockCmdCopyQueryPoolResults)}, + {"vkCmdBindPipeline", reinterpret_cast(mockCmdBindPipeline)}, + {"vkCmdPushConstants", reinterpret_cast(mockCmdPushConstants)}, + {"vkCmdDispatch", reinterpret_cast(mockCmdDispatch)}, + {"vkGetMemoryHostPointerPropertiesEXT", reinterpret_cast(mockGetMemoryHostPointerProperties)}, + {"vkCreateBuffer", reinterpret_cast(mockCreateBuffer)}, + {"vkDestroyBuffer", reinterpret_cast(mockDestroyBuffer)}, + {"vkGetBufferMemoryRequirements", reinterpret_cast(mockGetBufferMemoryRequirements)}, + {"vkAllocateMemory", reinterpret_cast(mockAllocateMemory)}, + {"vkFreeMemory", reinterpret_cast(mockFreeMemory)}, + {"vkBindBufferMemory", reinterpret_cast(mockBindBufferMemory)}, + {"vkGetBufferDeviceAddressKHR", reinterpret_cast(mockGetBufferDeviceAddress)}, + }; + const auto it = table.find(name); + return it == table.end() ? nullptr : it->second; +} + +Context mockContext() { + static const auto device = reinterpret_cast(mock.next++); + static const auto queue = reinterpret_cast(mock.next++); + Context context{}; + context.device = device; + context.queue = queue; + context.deviceProc = mockProc; + context.hostImportAlignment = Alignment; + return context; +} + +struct Heap { + std::byte* base = nullptr; + bool first = false; + bool second = false; + + Heap() { +#ifdef _WIN32 + base = static_cast(VirtualAlloc(nullptr, 2 * RangeBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + base = static_cast(std::aligned_alloc(Alignment, 2 * RangeBytes)); +#endif + if (base == nullptr) throw std::runtime_error("cannot allocate the test heap"); + GuestAllocations::Mutation mutation; + mutation.Add(base, RangeBytes, true, true, true); + first = true; + } + + void Grow() { + GuestAllocations::Mutation mutation; + mutation.Add(base + RangeBytes, RangeBytes, true, true, true); + second = true; + } + + std::uint64_t Address(std::size_t offset = 0) const { + return reinterpret_cast(base) + offset; + } + + ~Heap() { + try { + GuestAllocations::Mutation mutation; + if (second) mutation.Remove(base + RangeBytes); + if (first) mutation.Remove(base); + } catch (...) { + } +#ifdef _WIN32 + VirtualFree(base, 0, MEM_RELEASE); +#else + std::free(base); +#endif + } +}; + +struct ActiveRecorder { + std::unique_ptr recorder; + + ActiveRecorder() { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + recorder = std::make_unique(mockContext()); + recorder->Activate(); + } + + ~ActiveRecorder() { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + recorder.reset(); + } +}; + +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; + 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, "the first range was not imported"); + if (taken == nullptr) return; + retired = taken->buffer; + Expect(recorder.Idle(), "the recorder is not idle before the drop"); + heap.Grow(); + const auto* grown = HostImportFor(context, heap.Address(RangeBytes), 4096); + Expect(grown != nullptr && grown->buffer != retired && grown->base == heap.Address() && grown->bytes == 2 * RangeBytes, "the grown run was not imported as one import replacing the first"); + Expect(Live(retired), "an import dropped with the recorder idle was destroyed at once, while the hold that took it can still record with it"); + if (!Live(retired)) return; + Expect(taken->buffer == retired && taken->base == heap.Address(), "the dropped import's entry changed under the hold that took it"); + recorder.Commands(); + recorder.Submit(); + recorder.Sync(); + Expect(Live(retired), "an import dropped in this hold was destroyed by a Sync inside the same hold"); + recorder.Commands(); + recorder.Submit(); + Expect(Live(retired), "an import dropped in this hold was destroyed while a batch the hold recorded afterwards is in flight"); + } + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Expect(Live(retired), "an import dropped in a hold was destroyed at the hold's unlock while a batch of that hold is in flight"); + } + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + recorder.Sync(); + } + Expect(!Live(retired), "the dropped import outlived every batch of its hold"); +} + +void BusyRetireOutlivesALaterBatch() { + Heap heap; + ActiveRecorder active; + auto& recorder = *active.recorder; + const auto context = mockContext(); + VkBuffer retired = VK_NULL_HANDLE; + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + recorder.Keep(std::make_shared(0)); + const auto* taken = HostImportFor(context, heap.Address(), 4096); + Expect(taken != nullptr, "the first range was not imported"); + if (taken == nullptr) return; + retired = taken->buffer; + heap.Grow(); + Expect(HostImportFor(context, heap.Address(RangeBytes), 4096) != nullptr, "the grown run was not imported"); + mock.submitted.clear(); + recorder.Submit(); + recorder.Commands(); + recorder.Submit(); + Expect(mock.submitted.size() == 2, "the hold did not submit the keeping batch and the later one apart"); + if (mock.submitted.size() != 2) return; + mock.signaled.at(mock.submitted[0]) = true; + recorder.Reap(); + Expect(Live(retired), "the dropped import was destroyed when the batch open at the drop completed, while a later batch of the hold that may use it is in flight"); + } + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Expect(Live(retired), "the dropped import was destroyed at the hold's unlock, while a later batch of the hold that may use it is in flight"); + mock.signaled.at(mock.submitted[1]) = true; + recorder.Reap(); + } + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + recorder.Commands(); + recorder.Sync(); + } + Expect(!Live(retired), "the dropped import outlived every batch of its hold"); +} + +} + +int main() { +#ifdef _WIN32 + _putenv_s("APS5_RELEASE_ON_UNLOCK", "1"); +#else + setenv("APS5_RELEASE_ON_UNLOCK", "1", 1); + 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; + } + if (failures != 0) { + std::fprintf(stderr, "%d host import retire checks failed\n", failures); + return 1; + } + std::printf("host import retire tests passed\n"); + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp b/core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp new file mode 100644 index 0000000000..6f127df513 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp @@ -0,0 +1,429 @@ +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#ifndef _WIN32 +#include "prx/libc/include/GuestArena.hpp" +#include +#include +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +int main() { + std::printf("skipped, udmabuf imports are Linux only\n"); + return 77; +} +#else +using namespace AgcDriver::Graphics; + +namespace { + +int failures = 0; + +void Expect(bool condition, const std::string& what) { + if (condition) return; + std::fprintf(stderr, "FAIL: %s\n", what.c_str()); + ++failures; +} + +constexpr std::uint64_t Alignment = 65536; +constexpr std::uint64_t ChunkBytes = std::uint64_t{64} << 20u; +constexpr std::uint64_t LargestTestedLimit = std::uint64_t{1} << 30u; + +struct Bind { + VkDeviceSize resourceOffset; + VkDeviceSize size; + VkDeviceMemory memory; + VkDeviceSize memoryOffset; +}; + +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 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; +} + +VKAPI_ATTR VkResult VKAPI_CALL mockGetMemoryHostPointerProperties(VkDevice, VkExternalMemoryHandleTypeFlagBits, const void*, VkMemoryHostPointerPropertiesEXT*) { + return VK_ERROR_INVALID_EXTERNAL_HANDLE; +} + +VKAPI_ATTR VkResult VKAPI_CALL mockGetMemoryFdProperties(VkDevice, VkExternalMemoryHandleTypeFlagBits, int, VkMemoryFdPropertiesKHR* properties) { + properties->memoryTypeBits = 1; + return VK_SUCCESS; +} + +VKAPI_ATTR VkResult VKAPI_CALL mockCreateBuffer(VkDevice, const VkBufferCreateInfo* info, const VkAllocationCallbacks*, VkBuffer* buffer) { + *buffer = reinterpret_cast(mock.next++); + mock.buffers.insert(*buffer); + if ((info->flags & VK_BUFFER_CREATE_SPARSE_BINDING_BIT) != 0) mock.sparse.insert(*buffer); + mock.sizes[*buffer] = info->size; + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL mockDestroyBuffer(VkDevice, VkBuffer buffer, const VkAllocationCallbacks*) { + mock.buffers.erase(buffer); +} + +VKAPI_ATTR void VKAPI_CALL mockGetBufferMemoryRequirements(VkDevice, VkBuffer buffer, VkMemoryRequirements* requirements) { + 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; +} + +VKAPI_ATTR void VKAPI_CALL mockFreeMemory(VkDevice, VkDeviceMemory memory, const VkAllocationCallbacks*) { + mock.memories.erase(memory); +} + +VKAPI_ATTR VkResult VKAPI_CALL mockBindBufferMemory(VkDevice, VkBuffer, VkDeviceMemory, VkDeviceSize) { + return VK_SUCCESS; +} + +VKAPI_ATTR VkResult VKAPI_CALL mockQueueBindSparse(VkQueue, std::uint32_t count, const VkBindSparseInfo* infos, VkFence) { + if (mock.bindResult != VK_SUCCESS) return mock.bindResult; + for (std::uint32_t i = 0; i < count; ++i) { + for (std::uint32_t b = 0; b < infos[i].bufferBindCount; ++b) { + const auto& buffer = infos[i].pBufferBinds[b]; + for (std::uint32_t m = 0; m < buffer.bindCount; ++m) mock.binds.push_back({buffer.pBinds[m].resourceOffset, buffer.pBinds[m].size, buffer.pBinds[m].memory, buffer.pBinds[m].memoryOffset}); + } + } + return VK_SUCCESS; +} + +VKAPI_ATTR VkDeviceAddress VKAPI_CALL mockGetBufferDeviceAddress(VkDevice, const VkBufferDeviceAddressInfo* info) { + const auto address = mock.nextAddress; + mock.nextAddress += (mock.sizes.at(info->buffer) + Alignment - 1) / Alignment * Alignment; + return address; +} + +PFN_vkVoidFunction VKAPI_CALL mockProc(VkDevice, const char* name) { + static const std::map table{ + {"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)}, + {"vkGetMemoryHostPointerPropertiesEXT", reinterpret_cast(mockGetMemoryHostPointerProperties)}, + {"vkGetMemoryFdPropertiesKHR", reinterpret_cast(mockGetMemoryFdProperties)}, + {"vkCreateBuffer", reinterpret_cast(mockCreateBuffer)}, + {"vkDestroyBuffer", reinterpret_cast(mockDestroyBuffer)}, + {"vkGetBufferMemoryRequirements", reinterpret_cast(mockGetBufferMemoryRequirements)}, + {"vkAllocateMemory", reinterpret_cast(mockAllocateMemory)}, + {"vkFreeMemory", reinterpret_cast(mockFreeMemory)}, + {"vkBindBufferMemory", reinterpret_cast(mockBindBufferMemory)}, + {"vkQueueBindSparse", reinterpret_cast(mockQueueBindSparse)}, + {"vkGetBufferDeviceAddressKHR", reinterpret_cast(mockGetBufferDeviceAddress)}, + }; + const auto it = table.find(name); + return it == table.end() ? nullptr : it->second; +} + +Context mockContext() { + static const auto device = reinterpret_cast(mock.next++); + static const auto queue = reinterpret_cast(mock.next++); + Context context{}; + context.device = device; + context.queue = queue; + context.deviceProc = mockProc; + context.hostImportAlignment = Alignment; + context.dmaBufImport = true; + context.sparseBinding = true; + return context; +} + +struct Backing { + int file = -1; + std::uint8_t* base = nullptr; + std::uint64_t bytes = 0; + std::uint64_t fileOffset = 0; + void* reservation = nullptr; + std::uint64_t reserved = 0; +}; + +std::vector backings; + +bool Resolve(std::uintptr_t address, std::size_t bytes, int* file, std::uint64_t* offset) { + for (const auto* backing : backings) { + const auto begin = reinterpret_cast(backing->base); + if (address < begin || address + bytes > begin + backing->bytes) continue; + *file = dup(backing->file); + *offset = backing->fileOffset + (address - begin); + return *file >= 0; + } + return false; +} + +std::uint64_t UdmabufLimitBytes() { + std::ifstream limit("/sys/module/udmabuf/parameters/size_limit_mb"); + std::uint64_t megabytes = 64; + limit >> megabytes; + return megabytes << 20u; +} + +struct SharedRange { + Backing backing; + + SharedRange(std::uint64_t bytes, std::uint64_t fileOffset, bool sealed) { + backing.bytes = bytes; + backing.fileOffset = fileOffset; + backing.file = memfd_create("anyps5-sparse-import-mock", MFD_CLOEXEC | MFD_ALLOW_SEALING); + const auto fileBytes = (fileOffset + bytes + ChunkBytes - 1) / ChunkBytes * ChunkBytes; + if (backing.file < 0 || ftruncate(backing.file, static_cast(fileBytes)) != 0) throw std::runtime_error("cannot make the backing file"); + if (sealed && fcntl(backing.file, F_ADD_SEALS, F_SEAL_SHRINK) != 0) throw std::runtime_error("cannot seal the backing file"); + backing.reserved = bytes + Alignment; + backing.reservation = mmap(nullptr, backing.reserved, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (backing.reservation == MAP_FAILED) throw std::runtime_error("cannot reserve the guest range"); + const auto aligned = (reinterpret_cast(backing.reservation) + Alignment - 1) / Alignment * Alignment; + void* mapped = mmap(reinterpret_cast(aligned), bytes, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, backing.file, static_cast(fileOffset)); + if (mapped == MAP_FAILED) throw std::runtime_error("cannot map the backing file"); + backing.base = static_cast(mapped); + backings.push_back(&backing); + GuestAllocations::Mutation mutation; + mutation.Add(backing.base, static_cast(bytes), true, true, true); + } + + ~SharedRange() { + std::erase(backings, &backing); + } + + std::uint64_t Address() const { + return reinterpret_cast(backing.base); + } +}; + +std::uint64_t RangeBytes() { + return (UdmabufLimitBytes() / ChunkBytes + 1) * ChunkBytes + (std::uint64_t{1} << 20u); +} + +void 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); + const auto context = mockContext(); + mock.binds.clear(); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + const auto* import = HostImportFor(context, range.Address(), static_cast(bytes)); + Expect(import != nullptr, "a shared range larger than udmabuf's size limit was not imported"); + if (import == nullptr) return; + Expect(import->base == range.Address() && import->bytes == bytes && import->dmaBuf, "the import does not cover the whole range as a dma-buf import"); + Expect(mock.sparse.contains(import->buffer), "the range was not imported as one sparse buffer"); + const auto chunks = (bytes + ChunkBytes - 1) / ChunkBytes; + Expect(mock.binds.size() == chunks, "the sparse buffer is not bound to one chunk import per 64 MiB of the range"); + VkDeviceSize resource = 0; + std::set memories; + for (const auto& bind : mock.binds) { + Expect(bind.resourceOffset == resource && bind.memoryOffset == 0 && bind.size == std::min(ChunkBytes, bytes - resource), "a bind does not map its chunk import to the matching part of the range"); + Expect(mock.memories.contains(bind.memory) && mock.memories.at(bind.memory) == ChunkBytes, "a bind does not use a 64 MiB chunk import"); + memories.insert(bind.memory); + resource += bind.size; + } + Expect(memories.size() == chunks, "two parts of the range are bound to the same chunk import"); +} + +void UnalignedOffsetStartsAtTheBlockBelow() { + const auto bytes = RangeBytes(); + 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); + const auto context = mockContext(); + mock.bindResult = bind; + const auto live = mock.buffers.size(); + std::string thrown; + try { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + HostImportFor(context, range.Address(), static_cast(bytes)); + } catch (const std::runtime_error& error) { + thrown = error.what(); + } + mock.bindResult = VK_SUCCESS; + Expect(thrown.find("sparse dma-buf import") != std::string::npos && thrown.find(step) != std::string::npos, std::string("a failure at ") + step + " after the sparse path was chosen did not throw (got \"" + thrown + "\")"); + Expect(mock.buffers.size() == live, std::string("the sparse buffer outlived the failure at ") + step); +} + +} + +int main() { + const int udmabuf = open("/dev/udmabuf", O_RDWR | O_CLOEXEC); + if (udmabuf < 0) { + std::printf("skipped, host imports are unavailable: /dev/udmabuf is not accessible\n"); + return 77; + } + close(udmabuf); + 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(); + 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; + } + if (failures != 0) { + std::fprintf(stderr, "%d sparse dma-buf import checks failed\n", failures); + return 1; + } + std::printf("host import sparse dma-buf tests passed\n"); + return 0; +} +#endif diff --git a/core/libs/prx/libSceAgcDriver/tests/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/IntegerNarrowing.cpp b/core/libs/prx/libSceAgcDriver/tests/IntegerNarrowing.cpp new file mode 100644 index 0000000000..cd69416957 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/IntegerNarrowing.cpp @@ -0,0 +1,118 @@ +#include "IntermediateRepresentation/IrBuilder.hpp" +#include "Optimization/ConstantFolder.hpp" +#include "Optimization/DeadCodeEliminator.hpp" +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +namespace { + +enum class Bitcast { None, F32, F16 }; + +struct RoundTrip { + const char* name; + unsigned bits; + Bitcast bitcast; +}; + +IrValue& convert(IrBuilder& builder, IrValue& source, const RoundTrip& test) { + if (test.bitcast == Bitcast::F16) { + return builder.BitCastU32FromF16(builder.BitCastF16(source)); + } + auto& input = test.bitcast == Bitcast::F32 ? builder.BitCastU32(builder.BitCastF32(source)) : source; + auto& narrowed = builder.Emit(test.bits == 16 ? IrOpcode::ConvertU16U32 : IrOpcode::ConvertU8U32, + test.bits == 16 ? IrType::U16 : IrType::U8, {&input}); + auto& widened = builder.Emit(test.bits == 16 ? IrOpcode::ConvertU32U16 : IrOpcode::ConvertU32U8, + IrType::U32, {&narrowed}); + return test.bitcast == Bitcast::F32 ? builder.BitCastU32(builder.BitCastF32(widened)) : widened; +} + +void fold(IrProgram& program, bool cleanup) { + ConstantFolder().Fold(program); + if (cleanup) { + DeadCodeEliminator().RemoveIdentities(program); + DeadCodeEliminator().Eliminate(program); + ConstantFolder().Fold(program); + DeadCodeEliminator().RemoveIdentities(program); + DeadCodeEliminator().Eliminate(program); + } + ValidateProgram(program, true); +} + +bool checkRoundTrip(const RoundTrip& test, std::uint32_t pattern, std::uint32_t laneValue, bool dynamic, bool cleanup) { + IrProgram program; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + BlockInfo info; + info.id = 0; + program.Metadata().blockInfo.push_back(info); + IrBuilder builder(program); + builder.SetInsertionPoint(block); + IrValue* lane = dynamic ? &builder.Emit(IrOpcode::LaneId, IrType::U32, {}) : nullptr; + auto& source = dynamic ? builder.Emit(IrOpcode::IAdd32, IrType::U32, {lane, &builder.Constant(pattern)}) + : builder.Constant(pattern); + auto& result = convert(builder, source, test); + auto& user = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&result}); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + ValidateProgram(program, true); + fold(program, cleanup); + if (dynamic) { + if (user.Argument(0)->Resolve()->HasImmediate() || !lane->HasUses()) { + std::fprintf(stderr, "%s: optimized result must still depend on LaneId\n", test.name); + return false; + } + lane->ReplaceUsesWith(&builder.Constant(laneValue), true); + fold(program, cleanup); + } + const auto expected = (pattern + laneValue) & (test.bits == 16 ? 0xffffu : 0xffu); + const auto* actual = user.Argument(0)->Resolve(); + if (!actual->HasImmediate() || actual->Type() != IrType::U32 || actual->ImmediateU32() != expected) { + std::fprintf(stderr, "%s: pattern=%08x lane=%u dynamic=%d cleanup=%d expected=%08x actual=%08x immediate=%d\n", + test.name, pattern, laneValue, dynamic, cleanup, expected, + actual->HasImmediate() ? actual->ImmediateU32() : 0u, actual->HasImmediate()); + return false; + } + return true; +} + +} + +int main() { + try { + constexpr RoundTrip cases[] = { + {"U16 round trip", 16, Bitcast::None}, + {"U8 round trip", 8, Bitcast::None}, + {"U16 with F32 bitcasts", 16, Bitcast::F32}, + {"U8 with F32 bitcasts", 8, Bitcast::F32}, + {"F16 bitcast round trip", 16, Bitcast::F16}, + }; + constexpr std::uint32_t patterns[] = { + 0u, 1u, 0x7fu, 0x80u, 0xffu, 0x100u, 0x7fffu, 0x8000u, + 0xffffu, 0x10000u, 0x12345678u, 0x80000000u, 0xffff0000u, 0xffffff00u, 0xffffffffu, + }; + constexpr std::uint32_t lanes[] = {0u, 1u, 7u, 31u, 32u, 63u}; + unsigned total = 0; + unsigned failures = 0; + for (const auto& test : cases) { + for (auto pattern : patterns) { + ++total; + failures += !checkRoundTrip(test, pattern, 0u, false, true); + for (auto lane : lanes) { + for (bool cleanup : {false, true}) { + ++total; + failures += !checkRoundTrip(test, pattern, lane, true, cleanup); + } + } + } + } + std::printf("Integer narrowing: %u cases, %u failures\n", total, failures); + return failures == 0 ? 0 : 1; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/IrReclaim.cpp b/core/libs/prx/libSceAgcDriver/tests/IrReclaim.cpp new file mode 100644 index 0000000000..1e855b1d1a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/IrReclaim.cpp @@ -0,0 +1,92 @@ +#include "IntermediateRepresentation/IrBuilder.hpp" +#include "IntermediateRepresentation/IrProgram.hpp" +#include "Optimization/DeadCodeEliminator.hpp" +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +int main() { + try { + IrProgram program; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + IrBuilder builder(program); + builder.SetInsertionPoint(block); + auto& value = builder.Constant(42u); + IrSsaValues table; + const auto& readTable = table; + if (readTable[0] != nullptr || table.HasStorage()) throw std::runtime_error("an empty SSA table allocated storage during a read"); + for (IrValue* entry : readTable) if (entry != nullptr) throw std::runtime_error("an empty SSA table contained a value"); + if (table.HasStorage()) throw std::runtime_error("iterating an empty SSA table allocated storage"); + table[3] = &value; + if (!table.HasStorage() || readTable[3] != &value || readTable[2] != nullptr) throw std::runtime_error("an SSA write lost zero initialization or its value"); + table.fill(nullptr); + if (table.HasStorage() || readTable[3] != nullptr) throw std::runtime_error("clearing an SSA table retained construction storage"); + const auto id = value.Id(); + static_cast(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&value})); + auto& identity = program.CreateValue(IrOpcode::Identity, IrType::U32); + identity.AddArgument(&value); + block.AppendInstruction(&identity); + DeadCodeEliminator{}.RemoveIdentities(program); + auto& first = program.CreateValue(IrOpcode::IAdd32, IrType::U32); + auto& second = program.CreateValue(IrOpcode::IAdd32, IrType::U32); + first.AddArgument(&second); + second.AddArgument(&first); + std::vector pinned; + for (std::uint32_t number = 100u; number < 112u; ++number) pinned.push_back(&builder.Constant(number)); + block.ssaScalarValues[0] = pinned[0]; + block.ssaVectorValues[0] = pinned[1]; + block.ssaThreadBitScalarValues[0] = pinned[2]; + block.ssaScalarMaskTags[0] = pinned[3]; + program.Metadata().dynamicReads.push_back(pinned[4]); + program.Metadata().blockInfo.emplace_back(); + program.Metadata().blockInfo.back().condition = pinned[5]; + program.Metadata().blockInfo.back().indirectTarget = pinned[6]; + auto& resources = program.Resources(); + resources.descriptorSources.emplace_back(); + resources.descriptorSources.back().dwords[0] = pinned[7]; + resources.srtReads.push_back({pinned[8], 0u}); + resources.controlFlow.emplace_back(); + resources.controlFlow.back().condition = pinned[9]; + resources.uniformFill.values[0] = pinned[10]; + 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"); + for (std::size_t index = 0; index < pinned.size(); ++index) { + if (pinned[index]->ImmediateU32() != 100u + index) throw std::runtime_error("a metadata or resource-owned value was reclaimed"); + } + if (program.ReclaimUnusedValues() != 0u) throw std::runtime_error("reclaiming an unchanged graph was not idempotent"); + auto& unused = program.CreateValue(IrOpcode::IAdd32, IrType::U32); + IrValue untracked(IrOpcode::Identity, IrType::U32, value.Id()); + untracked.AddArgument(&unused); + bool rejected = false; + try { + static_cast(program.ReclaimUnusedValues()); + } catch (const std::runtime_error&) { + rejected = true; + } + if (!rejected) throw std::runtime_error("an untracked external owner was accepted"); + untracked.ReplaceArgument(0, nullptr); + program.Metadata().dynamicReads.clear(); + if (program.ReclaimUnusedValues() != 2u) throw std::runtime_error("reclamation did not recover after an external-owner rejection"); + if (builder.Constant(999u).Id() <= pinned.back()->Id()) throw std::runtime_error("reclamation reused a value ID"); + IrProgram moved = std::move(program); + if (value.Id() != id || value.ImmediateU32() != 42u || moved.ReclaimUnusedValues() != 1u) throw std::runtime_error("moving a program invalidated its allocation owner"); + IrProgram assigned; + static_cast(assigned.CreateValue(IrOpcode::IAdd32, IrType::U32)); + assigned = std::move(moved); + if (value.Id() != id || value.ImmediateU32() != 42u || assigned.ReclaimUnusedValues() != 0u) throw std::runtime_error("move assignment invalidated a live allocation owner"); + if (assigned.CreateValue(IrOpcode::IAdd32, IrType::U32).Id() <= pinned.back()->Id()) throw std::runtime_error("a transferred allocation owner reused a value ID"); + std::puts("IR reclamation ownership tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/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 c7843ab418..facdaddbe3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/LiveStack.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/LiveStack.cpp @@ -39,6 +39,14 @@ void checkLiveStack(std::uintptr_t stackBottom, std::uintptr_t stackTop, std::ui void RunLiveStackAccessTests() { checkLiveStack(0, 0, 0); +#if defined(_WIN32) + for (const auto address : {std::uintptr_t{0xf00000000000ull}, std::uintptr_t{0x7fffffffffffull}}) { + Require(!GuestMemory::Accessible(reinterpret_cast(address), 4), + "an address above the user address space is reported accessible"); + const auto committed = GuestMemory::CommittedRanges(address, 4, false); + Require(committed.empty(), "an address above the user address space reports committed bytes"); + } +#endif #if defined(__linux__) const auto page = static_cast(sysconf(_SC_PAGESIZE)); const std::size_t stackBytes = 256 * 1024; @@ -67,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 119f879251..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 @@ -8,6 +10,7 @@ #include #include #include +#include using namespace ShaderRecompiler; @@ -25,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; } @@ -34,6 +38,12 @@ struct Builder { return created; } + IrValue& truth() { + auto& created = program.CreateValue(IrOpcode::Void, IrType::Bool); + created.SetImmediateBool(true); + return created; + } + IrValue& emit(IrOpcode opcode, IrType type, std::initializer_list arguments, std::uint64_t flags = 0) { auto& created = program.CreateValue(opcode, type, flags); for (auto* argument : arguments) created.AddArgument(argument); @@ -77,6 +87,22 @@ struct Builder { return threadBit(low, high); } + std::pair ballotWords(IrValue& predicate) { + auto& ballot = emit(IrOpcode::Ballot, IrType::U32x4, {&predicate}); + return {&emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&ballot, &constant(0u)}), &emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&ballot, &constant(1u)})}; + } + + IrValue& wordAnd(IrValue& left, IrValue& right) { + return emit(IrOpcode::BitwiseAnd32, IrType::U32, {&left, &right}); + } + + IrValue& phi(IrType type, IrBlock& predecessor, IrValue& incoming) { + auto& created = program.CreateValue(IrOpcode::Phi, type); + block->AppendInstruction(&created); + created.AddPhiOperand(&predecessor, &incoming); + return created; + } + void keep(IrValue& value) { (void)emit(IrOpcode::ReferenceU32, IrType::Void, {&value}); } @@ -95,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()); @@ -162,9 +207,32 @@ int main() { return b.eliminate() == 2u && removed(first) && removed(second) && !removed(sink); }); + passed &= run("a write read through thousands of lane-local values still loses its select", [] { + Builder b; + auto& exec = b.mask(16u); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + IrValue* value = &written; + for (std::uint32_t step = 0; step < 6000u; ++step) value = &b.add(*value, step); + b.keep(b.select(exec, *value, old)); + return b.eliminate() == 1u && removed(written); + }); + + passed &= run("thousands of lane-local values ending in a wider read keep the select", [] { + Builder b; + auto& wide = b.mask(32u); + auto& exec = b.logicalAnd(wide, b.mask(16u)); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + IrValue* value = &written; + for (std::uint32_t step = 0; step < 6000u; ++step) value = &b.add(*value, step); + b.keep(b.select(wide, *value, old)); + return b.eliminate() == 0u && !removed(written); + }); + passed &= run("cross-lane reads must keep the select", [] { bool ok = true; - for (const IrOpcode opcode : {IrOpcode::ReadFirstLane, IrOpcode::DppMoveU32, IrOpcode::BpermuteU32, IrOpcode::Permlane16U32, IrOpcode::SwizzleU32}) { + for (const IrOpcode opcode : {IrOpcode::DppMoveU32, IrOpcode::BpermuteU32, IrOpcode::Permlane16U32, IrOpcode::SwizzleU32}) { Builder b; auto& exec = b.mask(16u); auto& old = b.lane(); @@ -182,6 +250,132 @@ int main() { return ok && b.eliminate() == 0u && !removed(written); }); + passed &= run("a first-lane read guarded at both ends loses the source select", [] { + Builder b; + auto& exec = b.mask(16u); + auto& narrow = b.logicalAnd(exec, b.mask(8u)); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + auto& read = b.emit(IrOpcode::ReadFirstLane, IrType::U32, {&written, &narrow}); + b.keep(b.select(narrow, read, old)); + return b.eliminate() == 1u && removed(written); + }); + + passed &= run("an unguarded first-lane result keeps the source select for empty exec", [] { + Builder b; + auto& exec = b.emit(IrOpcode::INotEqual32, IrType::Bool, {&b.lane(), &b.constant(0u)}); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + auto& empty = b.logicalAnd(exec, b.mask(0u)); + b.keep(b.emit(IrOpcode::ReadFirstLane, IrType::U32, {&written, &empty})); + return b.eliminate() == 0u && !removed(written); + }); + + passed &= run("a first-lane result read under the write mask keeps the select when read exec is narrower", [] { + Builder b; + auto& exec = b.emit(IrOpcode::INotEqual32, IrType::Bool, {&b.lane(), &b.constant(0u)}); + auto& empty = b.logicalAnd(exec, b.mask(0u)); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + auto& read = b.emit(IrOpcode::ReadFirstLane, IrType::U32, {&written, &empty}); + b.keep(b.select(exec, read, old)); + return b.eliminate() == 0u && !removed(written); + }); + + passed &= run("a first-lane read under a wider exec keeps the select", [] { + Builder b; + auto& wide = b.mask(32u); + auto& exec = b.logicalAnd(wide, b.mask(16u)); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + auto& read = b.emit(IrOpcode::ReadFirstLane, IrType::U32, {&written, &wide}); + b.keep(b.select(wide, read, old)); + return b.eliminate() == 0u && !removed(written); + }); + + const auto selectedExec = [](bool negate, bool widerArm) { + Builder b; + auto& wide = b.mask(32u); + auto& exec = b.logicalAnd(wide, b.mask(16u)); + auto& empty = b.logicalAnd(exec, b.mask(0u)); + auto& selector = negate ? b.emit(IrOpcode::LogicalNot, IrType::Bool, {&exec}) : exec; + auto& later = b.emit(IrOpcode::SelectU1, IrType::Bool, {&selector, negate ? (widerArm ? &wide : &empty) : &wide, negate ? &wide : (widerArm ? &wide : &empty)}); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + b.keep(b.select(later, b.add(written, 2u), old)); + return std::pair{b.eliminate(), removed(written)}; + }; + + passed &= run("selected boolean exec implies the write mask through its selector", [&] { + for (bool negate : {false, true}) { + const auto [count, gone] = selectedExec(negate, false); + if (count != 1u || !gone) return false; + } + return true; + }); + + passed &= run("selected boolean exec with a wider unguarded arm keeps the select", [&] { + for (bool negate : {false, true}) { + const auto [count, gone] = selectedExec(negate, true); + if (count != 0u || gone) return false; + } + return true; + }); + + const auto rebuiltExec = [](bool mismatchedHigh) { + Builder b; + const auto [low, high] = b.ballotWords(b.mask(16u)); + const auto [wideLow, wideHigh] = b.ballotWords(b.mask(48u)); + auto& exec = b.threadBit(*low, *high); + auto& later = b.threadBit(*low, mismatchedHigh ? *wideHigh : *high); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + b.keep(b.select(later, b.add(written, 2u), old)); + return std::pair{b.eliminate(), removed(written)}; + }; + + passed &= run("an exec rebuilt from the same words implies the write mask", [&] { + const auto [count, gone] = rebuiltExec(false); + return count >= 1u && gone; + }); + + passed &= run("matching low mask words alone do not imply a wave64 write mask", [&] { + const auto [count, gone] = rebuiltExec(true); + return count == 0u && !gone; + }); + + const auto firstLaneInLoop = [](bool carriedResult) { + Builder b; + auto& exec = b.mask(16u); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + IrBlock& entry = *b.block; + IrBlock& loop = b.newBlock(); + entry.AddBranch(&loop); + loop.AddBranch(&loop); + b.block = &loop; + auto& loopExec = b.logicalAnd(exec, b.mask(8u)); + auto& read = b.emit(IrOpcode::ReadFirstLane, IrType::U32, {&written, &loopExec}); + if (carriedResult) { + auto& carried = b.phi(IrType::U32, entry, old); + carried.AddPhiOperand(&loop, &read); + b.keep(b.select(loopExec, carried, old)); + } else { + b.keep(b.select(loopExec, read, old)); + } + return std::pair{b.eliminate(), removed(written)}; + }; + + passed &= run("a loop-local first-lane result guarded in its own iteration loses the source select", [&] { + const auto [count, gone] = firstLaneInLoop(false); + return count == 1u && gone; + }); + + passed &= run("a first-lane result carried to another loop iteration keeps the source select", [&] { + const auto [count, gone] = firstLaneInLoop(true); + return count == 0u && !gone; + }); + passed &= run("a branch condition derived from the write keeps the select", [] { Builder b; auto& exec = b.mask(16u); @@ -275,6 +469,131 @@ int main() { return count == 0u && !gone; }); + const auto mergedExec = [](bool wideBranch, bool loopMerge) { + Builder b; + auto& wide = b.mask(48u); + auto& exec = b.logicalAnd(wide, b.mask(32u)); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + IrBlock& head = *b.block; + IrBlock& body = b.newBlock(); + IrBlock& merge = b.newBlock(); + head.AddBranch(&body); + head.AddBranch(&merge); + body.AddBranch(&merge); + b.block = &body; + auto& killed = b.logicalAnd(wideBranch ? wide : exec, b.mask(16u)); + auto& phi = b.program.CreateValue(IrOpcode::Phi, IrType::Bool); + merge.AppendInstruction(&phi); + phi.AddPhiOperand(&head, &exec); + phi.AddPhiOperand(&body, &killed); + if (loopMerge) { + IrBlock& exit = b.newBlock(); + merge.AddBranch(&merge); + merge.AddBranch(&exit); + phi.AddPhiOperand(&merge, &phi); + b.block = &exit; + } else { + b.block = &merge; + } + b.keep(b.select(phi, b.add(written, 2u), old)); + return std::pair{b.eliminate(), removed(written)}; + }; + + passed &= run("an exec merged by a phi whose every incoming exec implies the write's exec drops the select", [&] { + const auto [count, gone] = mergedExec(false, false); + return count == 1u && gone; + }); + + passed &= run("an exec merged by a phi with a wider incoming exec keeps the select", [&] { + const auto [count, gone] = mergedExec(true, false); + return count == 0u && !gone; + }); + + passed &= run("an exec carried around a loop whose every incoming exec implies the write's exec drops the select", [&] { + const auto [count, gone] = mergedExec(false, true); + return count == 1u && gone; + }); + + passed &= run("an exec carried around a loop with a wider incoming exec keeps the select", [&] { + const auto [count, gone] = mergedExec(true, true); + return count == 0u && !gone; + }); + + const auto mergedWords = [](bool wideBranch) { + Builder b; + auto& wide = b.mask(48u); + auto& exec = b.logicalAnd(wide, b.mask(40u)); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + auto& saved = b.emit(IrOpcode::Ballot, IrType::U32x4, {&exec}); + auto& savedLow = b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&saved, &b.constant(0u)}); + auto& savedHigh = b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&saved, &b.constant(1u)}); + IrBlock& head = *b.block; + IrBlock& body = b.newBlock(); + IrBlock& merge = b.newBlock(); + head.AddBranch(&body); + head.AddBranch(&merge); + body.AddBranch(&merge); + b.block = &body; + auto& survived = b.emit(IrOpcode::Ballot, IrType::U32x4, {&b.logicalAnd(wideBranch ? wide : exec, b.mask(24u))}); + auto& survivedLow = b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&survived, &b.constant(0u)}); + auto& survivedHigh = b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&survived, &b.constant(1u)}); + auto& low = b.program.CreateValue(IrOpcode::Phi, IrType::U32); + auto& high = b.program.CreateValue(IrOpcode::Phi, IrType::U32); + merge.AppendInstruction(&low); + merge.AppendInstruction(&high); + low.AddPhiOperand(&head, &savedLow); + low.AddPhiOperand(&body, &survivedLow); + high.AddPhiOperand(&head, &savedHigh); + high.AddPhiOperand(&body, &survivedHigh); + b.block = &merge; + b.keep(b.select(b.threadBit(low, high), b.add(written, 2u), old)); + return std::pair{b.eliminate(), removed(written)}; + }; + + passed &= run("an exec rebuilt from mask words merged by phis implies the write's exec", [&] { + const auto [count, gone] = mergedWords(false); + return count == 1u && gone; + }); + + passed &= run("an exec rebuilt from merged mask words with a wider incoming ballot keeps the select", [&] { + const auto [count, gone] = mergedWords(true); + return count == 0u && !gone; + }); + + const auto chained = [](bool wideRoot, std::uint32_t kills) { + Builder b; + auto& wide = b.mask(48u); + auto& exec = b.logicalAnd(wide, b.mask(32u)); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + IrValue* later = wideRoot ? &wide : &exec; + for (std::uint32_t kill = 0; kill < kills; ++kill) later = &b.logicalAnd(*later, b.mask(31u - kill % 24u)); + b.keep(b.select(*later, b.add(written, 2u), old)); + return std::pair{b.eliminate(), removed(written)}; + }; + + passed &= run("an exec narrowed by a long chain of kills still implies the write's exec", [&] { + const auto [count, gone] = chained(false, 24u); + return count == 1u && gone; + }); + + passed &= run("a long chain of kills rooted at a wider exec keeps the select", [&] { + const auto [count, gone] = chained(true, 24u); + return count == 0u && !gone; + }); + + passed &= run("an exec narrowed by hundreds of kills still implies the write's exec", [&] { + const auto [count, gone] = chained(false, 300u); + return count == 1u && gone; + }); + + passed &= run("hundreds of kills rooted at a wider exec keep the select", [&] { + const auto [count, gone] = chained(true, 300u); + return count == 0u && !gone; + }); + passed &= run("a write in a loop must keep its select", [] { Builder b; auto& exec = b.mask(16u); @@ -292,9 +611,10 @@ int main() { return b.eliminate() == 0u && !removed(written); }); - passed &= run("a write read inside a later loop must keep its select", [] { + const auto laterLoop = [](bool wideRead) { Builder b; - auto& exec = b.mask(16u); + auto& wide = b.mask(48u); + auto& exec = b.logicalAnd(wide, b.mask(16u)); auto& old = b.lane(); auto& written = b.select(exec, b.add(old, 1u), old); IrBlock& head = *b.block; @@ -302,18 +622,159 @@ int main() { head.AddBranch(&loop); loop.AddBranch(&loop); b.block = &loop; - b.keep(b.select(exec, b.add(written, 2u), old)); + b.keep(b.select(wideRead ? wide : exec, b.add(written, 2u), old)); + return std::pair{b.eliminate(), removed(written)}; + }; + + passed &= run("a write read inside a later loop under its own exec loses its select", [&] { + const auto [count, gone] = laterLoop(false); + return count == 1u && gone; + }); + + passed &= run("a write read inside a later loop under a wider exec keeps its select", [&] { + const auto [count, gone] = laterLoop(true); + return count == 0u && !gone; + }); + + enum class Latch { Narrowed, Revived }; + const auto carried = [](Latch latch, bool readInside) { + Builder b; + auto& wide = b.mask(48u); + auto& exec = b.logicalAnd(wide, b.mask(40u)); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + const auto [savedLow, savedHigh] = b.ballotWords(exec); + IrBlock& entry = *b.block; + IrBlock& header = b.newBlock(); + IrBlock& exit = b.newBlock(); + entry.AddBranch(&header); + header.AddBranch(&header); + header.AddBranch(&exit); + b.block = &header; + auto& low = b.phi(IrType::U32, entry, *savedLow); + auto& high = b.phi(IrType::U32, entry, *savedHigh); + auto& value = b.phi(IrType::U32, entry, written); + auto& loopExec = b.threadBit(low, high); + auto& next = b.select(loopExec, b.add(value, 2u), value); + if (readInside) b.keep(b.select(loopExec, b.add(value, 3u), old)); + if (latch == Latch::Narrowed) { + const auto [stayLow, stayHigh] = b.ballotWords(b.mask(24u)); + low.AddPhiOperand(&header, &b.wordAnd(low, *stayLow)); + high.AddPhiOperand(&header, &b.wordAnd(high, *stayHigh)); + } else { + const auto [revivedLow, revivedHigh] = b.ballotWords(wide); + low.AddPhiOperand(&header, revivedLow); + high.AddPhiOperand(&header, revivedHigh); + } + value.AddPhiOperand(&header, &next); + b.block = &exit; + b.keep(b.select(b.threadBit(*savedLow, *savedHigh), b.add(value, 4u), old)); + return std::pair{b.eliminate(), removed(written)}; + }; + + passed &= run("a write carried through a loop whose exec is saved, narrowed and restored loses its select", [&] { + const auto [count, gone] = carried(Latch::Narrowed, false); + return count == 1u && gone; + }); + + passed &= run("a write read inside a loop under the loop's narrowed exec loses its select", [&] { + const auto [count, gone] = carried(Latch::Narrowed, true); + return count == 1u && gone; + }); + + passed &= run("a lane that becomes active again inside a loop and reads the write keeps the select", [&] { + const auto [count, gone] = carried(Latch::Revived, true); + return count == 0u && !gone; + }); + + passed &= run("a lane that becomes active again inside a loop but never reads the write lets the select go", [&] { + const auto [count, gone] = carried(Latch::Revived, false); + return count == 1u && gone; + }); + + passed &= run("an exec proven only by assuming a loop phi is forgotten once that phi turns out wider", [] { + Builder b; + auto& wide = b.mask(48u); + auto& exec = b.logicalAnd(wide, b.mask(40u)); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + const auto [savedLow, savedHigh] = b.ballotWords(exec); + IrBlock& entry = *b.block; + IrBlock& header = b.newBlock(); + IrBlock& kill = b.newBlock(); + IrBlock& revive = b.newBlock(); + IrBlock& latch = b.newBlock(); + IrBlock& exit = b.newBlock(); + entry.AddBranch(&header); + header.AddBranch(&kill); + header.AddBranch(&revive); + kill.AddBranch(&latch); + revive.AddBranch(&latch); + latch.AddBranch(&header); + latch.AddBranch(&exit); + b.block = &header; + auto& low = b.phi(IrType::U32, entry, *savedLow); + auto& high = b.phi(IrType::U32, entry, *savedHigh); + (void)b.select(b.threadBit(low, high), written, old); + b.block = &kill; + const auto [stayLow, stayHigh] = b.ballotWords(b.mask(24u)); + auto& killedLow = b.wordAnd(low, *stayLow); + auto& killedHigh = b.wordAnd(high, *stayHigh); + b.keep(b.select(b.threadBit(killedLow, killedHigh), written, old)); + b.block = &revive; + const auto [revivedLow, revivedHigh] = b.ballotWords(wide); + b.block = &latch; + auto& mergedLow = b.phi(IrType::U32, kill, killedLow); + auto& mergedHigh = b.phi(IrType::U32, kill, killedHigh); + mergedLow.AddPhiOperand(&revive, revivedLow); + mergedHigh.AddPhiOperand(&revive, revivedHigh); + low.AddPhiOperand(&latch, &mergedLow); + high.AddPhiOperand(&latch, &mergedHigh); return b.eliminate() == 0u && !removed(written); }); - const auto sample = [](IrShaderStage stage, std::uint32_t sampleFlags) { + passed &= run("a write mask computed inside an earlier loop does not let that loop's phis stand for it", [] { + Builder b; + auto& old = b.lane(); + IrBlock& entry = *b.block; + IrBlock& header = b.newBlock(); + IrBlock& exit = b.newBlock(); + entry.AddBranch(&header); + header.AddBranch(&header); + header.AddBranch(&exit); + b.block = &header; + auto& low = b.phi(IrType::U32, entry, b.constant(0u)); + auto& high = b.phi(IrType::U32, entry, b.constant(0u)); + auto& bound = b.phi(IrType::U32, entry, b.constant(64u)); + auto& exec = b.emit(IrOpcode::ULessThan32, IrType::Bool, {&b.lane(), &bound}); + const auto [stayLow, stayHigh] = b.ballotWords(exec); + low.AddPhiOperand(&header, stayLow); + high.AddPhiOperand(&header, stayHigh); + bound.AddPhiOperand(&header, &b.emit(IrOpcode::ISub32, IrType::U32, {&bound, &b.constant(8u)})); + b.block = &exit; + auto& written = b.select(exec, b.add(old, 1u), old); + b.keep(b.select(b.threadBit(low, high), b.add(written, 2u), old)); + return b.eliminate() == 0u && !removed(written); + }); + + const auto wholeQuads = [](Builder& b, IrValue& live) -> IrValue& { + auto& ballot = b.emit(IrOpcode::Ballot, IrType::U32x4, {&live}); + auto& allLow = b.emit(IrOpcode::Ballot, IrType::U32x4, {&b.truth()}); + auto& allHigh = b.emit(IrOpcode::Ballot, IrType::U32x4, {&b.truth()}); + auto& low = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&ballot, &b.constant(0u)}), &b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&allLow, &b.constant(0u)})}); + auto& high = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&ballot, &b.constant(1u)}), &b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&allHigh, &b.constant(1u)})}); + auto& wqm = b.emit(IrOpcode::WqmU64, IrType::U64, {&b.emit(IrOpcode::CompositeConstructU64, IrType::U64, {&low, &high})}); + return b.threadBit(b.emit(IrOpcode::CompositeExtractU64, IrType::U32, {&wqm, &b.constant(0u)}), b.emit(IrOpcode::CompositeExtractU64, IrType::U32, {&wqm, &b.constant(1u)})); + }; + + const auto sample = [&](IrShaderStage stage, std::uint32_t sampleFlags, IrOpcode opcode = IrOpcode::ImageSampleRaw, bool wholeQuadExec = false) { Builder b; b.program.Resources().stage = stage; MemoryInfo memory; memory.kind = ResourceKind::Image; memory.imageSampleFlags = sampleFlags; b.program.Resources().memoryInfo.push_back(memory); - auto& exec = b.mask(16u); + auto& exec = wholeQuadExec ? wholeQuads(b, b.mask(16u)) : b.mask(16u); auto& old = b.lane(); auto& written = b.select(exec, b.add(old, 1u), old); auto& address = b.emit(IrOpcode::MakeImageAddress, IrType::ImageAddress, {&written, &old}); @@ -322,20 +783,507 @@ int main() { MemoryFlags flags{0u, 0u}; std::uint64_t bits = 0; std::memcpy(&bits, &flags, sizeof(flags)); - auto& sampled = b.emit(IrOpcode::ImageSampleRaw, IrType::U32x4, {&image, &sampler, &address}, bits); + auto& sampled = b.emit(opcode, IrType::U32x4, {&image, &sampler, &address}, bits); b.keep(b.select(exec, b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&sampled, &b.constant(0u)}), old)); b.eliminate(); 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); }); + passed &= run("an implicit-LOD pixel sample under whole quad mode reads no masked quad and drops the select", [&] { + return sample(IrShaderStage::Pixel, 0u, IrOpcode::ImageSampleRaw, true); + }); passed &= run("an explicit-LOD sample reads only its lane and drops the select", [&] { return sample(IrShaderStage::Pixel, RdnaImageSampleFlagLevelZero) && sample(IrShaderStage::Mesh, 0u); }); + passed &= run("a pixel gather without an LOD of its own takes it from the quad and keeps the select", [&] { + return !sample(IrShaderStage::Pixel, 0u, IrOpcode::ImageGatherRaw) && !sample(IrShaderStage::Pixel, RdnaImageSampleFlagBias, IrOpcode::ImageGatherRaw); + }); + + passed &= run("a gather with an explicit LOD reads only its lane and drops the select", [&] { + return sample(IrShaderStage::Pixel, RdnaImageSampleFlagLevelZero, IrOpcode::ImageGatherRaw) && sample(IrShaderStage::Pixel, RdnaImageSampleFlagLod, IrOpcode::ImageGatherRaw) && + sample(IrShaderStage::Mesh, 0u, IrOpcode::ImageGatherRaw); + }); + + passed &= run("an image size query reads only its lane's level and drops the select", [] { + Builder b; + auto& exec = b.mask(16u); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + auto& address = b.emit(IrOpcode::MakeImageAddress, IrType::ImageAddress, {&written}); + auto& size = b.emit(IrOpcode::ImageQueryDimensions, IrType::U32x4, {&b.emit(IrOpcode::GetImageResource, IrType::ImageResource, {}), &address}); + b.keep(b.select(exec, b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&size, &b.constant(0u)}), old)); + return b.eliminate() == 1u && removed(written); + }); + const auto guarded = [](IrOpcode opcode, bool sameExec, bool asActive) { Builder b; auto& wide = b.mask(48u); @@ -350,6 +1298,19 @@ int main() { case IrOpcode::LoadBufferU32: b.keep(b.select(exec, b.emit(opcode, IrType::U32, {&b.emit(IrOpcode::GetBufferResource, IrType::BufferResource, {}), &written, &b.constant(0u), &b.constant(0u), &active}), old)); break; + case IrOpcode::ImageRead: { + auto& address = b.emit(IrOpcode::MakeImageAddress, IrType::ImageAddress, {&written, &old}); + auto& image = b.emit(IrOpcode::GetImageResource, IrType::ImageResource, {}); + auto& read = b.emit(opcode, IrType::U32x4, {&image, &address, &active}); + b.keep(b.select(exec, b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&read, &b.constant(0u)}), old)); + break; + } + case IrOpcode::ImageWrite: { + auto& address = b.emit(IrOpcode::MakeImageAddress, IrType::ImageAddress, {&old, &old}); + auto& texel = b.emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&written, &old, &old, &old}); + b.emit(opcode, IrType::Void, {&b.emit(IrOpcode::GetImageResource, IrType::ImageResource, {}), &address, &texel, &active}); + break; + } default: b.emit(opcode, IrType::Void, {&b.emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&written, &old, &old, &old}), &active}); break; @@ -359,11 +1320,15 @@ int main() { }; passed &= run("a store, load or export whose exec implies the write's exec drops the select", [&] { - return guarded(IrOpcode::WriteSharedU32, true, false) && guarded(IrOpcode::LoadBufferU32, true, false) && guarded(IrOpcode::SetAttribute, true, false); + return guarded(IrOpcode::WriteSharedU32, true, false) && guarded(IrOpcode::LoadBufferU32, true, false) && guarded(IrOpcode::ImageRead, true, false) && guarded(IrOpcode::SetAttribute, true, false); }); passed &= run("a store, load or export under a wider exec keeps the select", [&] { - return !guarded(IrOpcode::WriteSharedU32, false, false) && !guarded(IrOpcode::LoadBufferU32, false, false) && !guarded(IrOpcode::SetAttribute, false, false); + return !guarded(IrOpcode::WriteSharedU32, false, false) && !guarded(IrOpcode::LoadBufferU32, false, false) && !guarded(IrOpcode::ImageRead, false, false) && !guarded(IrOpcode::SetAttribute, false, false); + }); + + passed &= run("an image write under the write's exec drops the select, under a wider exec or as its exec keeps it", [&] { + return guarded(IrOpcode::ImageWrite, true, false) && !guarded(IrOpcode::ImageWrite, false, false) && !guarded(IrOpcode::ImageWrite, true, true); }); passed &= run("a value used as a store's exec keeps the select", [&] { @@ -374,5 +1339,91 @@ int main() { return !guarded(IrOpcode::SharedAtomicIAdd32, true, false); }); + passed &= run("a write under whole quad mode read only by the lanes it was widened from drops the select in every stage", [&] { + const auto widened = [&](IrShaderStage stage) { + Builder b; + b.program.Resources().stage = stage; + auto& live = b.mask(16u); + auto& quads = wholeQuads(b, live); + auto& old = b.lane(); + auto& written = b.select(quads, b.add(old, 1u), old); + auto& next = b.select(b.logicalAnd(live, b.mask(8u)), b.add(written, 2u), old); + b.keep(next); + return b.eliminate() != 0u && removed(written) && !removed(next); + }; + return widened(IrShaderStage::Pixel) && widened(IrShaderStage::Compute); + }); + + passed &= run("a write under whole quad mode read by a lane outside the widened mask keeps the select in compute", [&] { + Builder b; + b.program.Resources().stage = IrShaderStage::Compute; + auto& live = b.mask(16u); + auto& quads = wholeQuads(b, live); + auto& old = b.lane(); + auto& written = b.select(quads, b.add(old, 1u), old); + auto& next = b.select(b.mask(32u), b.add(written, 2u), old); + b.keep(next); + return b.eliminate() == 0u && !removed(written); + }); + + passed &= run("a write under whole quad mode read only by the lanes it was widened from drops the select", [&] { + Builder b; + auto& live = b.mask(16u); + auto& quads = wholeQuads(b, live); + auto& old = b.lane(); + auto& written = b.select(quads, b.add(old, 1u), old); + auto& next = b.select(b.logicalAnd(live, b.mask(8u)), b.add(written, 2u), old); + b.keep(next); + return b.eliminate() != 0u && removed(written) && !removed(next); + }); + + passed &= run("a write under whole quad mode read by other lanes keeps the select", [&] { + Builder b; + auto& live = b.mask(16u); + auto& quads = wholeQuads(b, live); + auto& old = b.lane(); + auto& written = b.select(quads, b.add(old, 1u), old); + auto& next = b.select(b.mask(32u), b.add(written, 2u), old); + b.keep(next); + return b.eliminate() == 0u && !removed(written); + }); + + const auto shaded = [](std::uint32_t units) { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + MemoryInfo memory; + memory.kind = ResourceKind::Image; + memory.imageSampleFlags = RdnaImageSampleFlagLevelZero; + b.program.Resources().memoryInfo.push_back(memory); + MemoryFlags flags{0u, 0u}; + std::uint64_t bits = 0; + std::memcpy(&bits, &flags, sizeof(flags)); + auto& exec = b.mask(48u); + auto& image = b.emit(IrOpcode::GetImageResource, IrType::ImageResource, {}); + auto& sampler = b.emit(IrOpcode::GetSamplerResource, IrType::SamplerResource, {}); + IrValue* color = &b.lane(); + IrValue* address = nullptr; + std::uint32_t kept = 0; + std::vector compared; + for (std::uint32_t unit = 0; unit < units; ++unit) { + auto& sum = b.add(*color, unit); + auto& low = b.emit(IrOpcode::ULessThan32, IrType::Bool, {&sum, &b.constant(100u + unit)}); + compared.push_back(&b.select(low, sum, b.add(*color, 1u))); + address = &b.emit(IrOpcode::MakeImageAddress, IrType::ImageAddress, {compared.back(), color}); + auto& gathered = b.emit(IrOpcode::ImageGatherRaw, IrType::U32x4, {&image, &sampler, address}, bits); + color = &b.select(exec, b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&gathered, &b.constant(0u)}), *color); + } + b.emit(IrOpcode::ImageWrite, IrType::Void, {&image, address, &b.emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {color, color, color, color}), &exec}); + const auto stats = MaskedSelectEliminator{}.Eliminate(b.program); + for (const IrValue* select : compared) kept += removed(*select) ? 0u : 1u; + return std::pair{stats, kept}; + }; + + passed &= run("selects that each reach the rest of the program are settled in steps that grow with the program, not with its square", [&] { + const auto [small, smallKept] = shaded(500u); + const auto [large, largeKept] = shaded(4000u); + return small.removedSelects == 500u && smallKept == 500u && large.removedSelects == 4000u && largeKept == 4000u && small.steps != 0u && large.steps <= 9u * small.steps; + }); + return passed ? 0 : 1; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp index d4ce2560d0..a3c2a13217 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp @@ -1,7 +1,9 @@ #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 @@ -9,7 +11,9 @@ #include #include #include +#include #include +#include #include namespace { @@ -22,20 +26,27 @@ 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; + code[10] = 0x04190a6bu; + code[28] = 0x04250a02u; + return code; +}(); + +alignas(256) constexpr std::array FastLaunchCode{ + 0xd7650000, 0x000100c1, 0x7e000505, 0x93018300, 0x4a020001, 0xe0382000, 0x80020201, 0xe0382010, + 0x80020601, 0xb07c1003, 0xbf900009, 0xbf8c3f70, 0x7d880083, 0xbefe036a, 0xf80008cf, 0x05040302, + 0xf800020f, 0x09080706, 0x7d840080, 0xbefe036a, 0x7e1402ff, 0x00200400, 0xf8000941, 0x0000000a, + 0xbf810000, +}; + +alignas(256) constexpr std::array 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{ @@ -82,12 +93,22 @@ 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{}; alignas(256) std::array FanScrambled{}; alignas(256) std::array FanIndices{}; alignas(256) std::array FanRestart{}; +alignas(256) std::array Quads{}; +alignas(256) std::array QuadStrips{}; +alignas(256) std::array QuadStrips32{}; std::array VertexBufferDescriptor(const void* vertices, std::uint32_t count) { const auto address = reinterpret_cast(vertices); @@ -119,23 +140,49 @@ struct MeshDraw { std::array vertexBuffer; std::uint32_t geometryPushBytes = ShaderRecompiler::MeshDrawPushOffsetBytes; bool restart = false; + std::uint32_t waveSize = 64u; + VkCullModeFlags cull = VK_CULL_MODE_NONE; + std::span code = {}; + std::uint32_t stagesRegister = 0x20u; }; +bool ReadsNumWorkgroups(std::span words) { + constexpr std::uint32_t OpDecorate = 71; + constexpr std::uint32_t OpAccessChain = 65; + constexpr std::uint32_t OpLoad = 61; + constexpr std::uint32_t DecorationBuiltIn = 11; + constexpr std::uint32_t BuiltInNumWorkgroups = 24; + std::uint32_t variable = 0; + std::vector pointers; + for (std::size_t at = 5; at < words.size();) { + const auto length = words[at] >> 16u; + const auto opcode = words[at] & 0xffffu; + if (length == 0 || at + length > words.size()) return false; + if (opcode == OpDecorate && length == 4 && words[at + 2] == DecorationBuiltIn && words[at + 3] == BuiltInNumWorkgroups) variable = words[at + 1]; + if (opcode == OpAccessChain && length >= 4 && variable != 0 && words[at + 3] == variable) pointers.push_back(words[at + 2]); + if (opcode == OpLoad && length >= 4 && variable != 0 && (words[at + 3] == variable || std::find(pointers.begin(), pointers.end(), words[at + 3]) != pointers.end())) return true; + at += length; + } + return false; +} + void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) { const auto target = device.Target(); std::vector userData(12, 0u); const auto index = AgcDriver::Graphics::MeshIndexBufferDescriptor(setup.draw); std::copy(index.begin(), index.end(), userData.begin() + ShaderRecompiler::MeshIndexBufferUserWord); std::copy(setup.vertexBuffer.begin(), setup.vertexBuffer.end(), userData.begin() + 8); - const std::array geometryMemory{{{reinterpret_cast(GeometryCode.data()), std::as_bytes(std::span(GeometryCode))}}}; + const std::span code = !setup.code.empty() ? setup.code : setup.waveSize == 32u ? std::span(Wave32GeometryCode) : std::span(GeometryCode); + const std::array geometryMemory{{{reinterpret_cast(code.data()), std::as_bytes(std::span(code))}}}; ShaderRecompiler::RecompileRequest geometry{ - {ShaderStage::Mesh, reinterpret_cast(GeometryCode.data()), GeometryCode, 0, {}}, - {64, 0, userData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, geometryMemory}, + {ShaderStage::Mesh, reinterpret_cast(code.data()), code, 0, {}}, + {setup.waveSize, 0, userData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, 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}} }; const auto meshResult = ShaderRecompiler::Recompile(geometry); + Require(ReadsNumWorkgroups(meshResult.spirv), "the mesh program does not read NumWorkgroups, so RADV can keep a previous vertex pipeline's user SGPR count for its dispatch"); const auto meshPush = static_cast(meshResult.pushConstants.size()); Require((meshPush == 0) == (setup.geometryPushBytes == 0), "the geometry program's push data does not follow its layout"); @@ -149,17 +196,17 @@ void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) { {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, {64, 0, {}, std::nullopt, pixel, std::nullopt, pixelMemory}, target, - {0, 0, meshPush, ShaderRecompiler::MeshDrawPushOffsetBytes - meshPush}, + target.fixedPushSlots ? PixelPushLayout(meshPush, target) : ShaderRecompiler::BindingLayout{0, 0, meshPush, ShaderRecompiler::MeshDrawPushOffsetBytes - meshPush}, std::nullopt }; const auto pixelResult = ShaderRecompiler::Recompile(fragment); const std::array shaders{{ {ShaderStage::Mesh, &meshResult, 0}, - {ShaderStage::Fragment, &pixelResult, meshPush} + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(meshPush, target)} }}; AgcDriver::Graphics::State state{}; - state.stages = {AgcDriver::Graphics::ShaderPath::Geometry, 0x20u, 64, 64, setup.mesh, std::nullopt}; + state.stages = {AgcDriver::Graphics::ShaderPath::Geometry, setup.stagesRegister, setup.waveSize, 64, setup.mesh, std::nullopt}; state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; state.colors = {state.color}; state.hasColorTarget = true; @@ -169,7 +216,7 @@ void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) { 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 = setup.cull; state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; state.blend.colorWriteMask = 15; state.blends = {state.blend}; @@ -180,6 +227,7 @@ void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) { constexpr ShaderRecompiler::MeshConfiguration SmallSubgroup{4u, 4u, 12u, 12u, 4u, 64u, 1024u, 0u, 4u}; constexpr ShaderRecompiler::MeshConfiguration WideSubgroup{4u, 32u, 96u, 96u, 32u, 128u, 2048u, 0u, 4u}; +constexpr ShaderRecompiler::MeshConfiguration FastLaunchSubgroup{1u, 1u, 1u, 3u, 1u, 32u, 128u, 0u, 4u}; std::size_t FanPixel(float angle, float radius) { const auto x = static_cast((radius * std::cos(angle) + 1.0f) * 0.5f * Width); @@ -196,6 +244,24 @@ void CheckFan(const char* what) { Require(PixelIs(0, Background) && PixelIs(Pixels.size() - 4u, Background), std::string(what) + ": the corners changed"); } +std::size_t QuadPixel(std::uint32_t quad, float across) { + const auto x = static_cast((static_cast(quad) + across * 0.8f) * Width / Columns); + return (static_cast(Height * 3u / 4u) * Width + x) * 4u; +} + +void CheckQuads(const char* what, std::uint32_t first, std::uint32_t count) { + for (std::uint32_t quad = 0; quad < Columns; ++quad) { + const bool drawn = quad >= first && quad < first + count; + for (const float across : {0.25f, 0.75f}) { + const auto offset = QuadPixel(quad, across); + Require(PixelIs(offset, drawn ? TriangleColor(quad) : Background), std::string(what) + ": quad " + std::to_string(quad) + (across < 0.5f ? " left" : " right") + " half pixel " + PixelText(offset)); + } + const auto gap = QuadPixel(quad, 1.12f); + Require(PixelIs(gap, Background), std::string(what) + ": a triangle crossed the restart after quad " + std::to_string(quad) + ", pixel " + PixelText(gap)); + } + Require(PixelIs((static_cast(Height / 4u) * Width + Width / 2u) * 4u, Background), std::string(what) + ": the upper half changed"); +} + void CheckTriangles(const char* what) { for (std::uint32_t triangle = 0; triangle < Triangles; ++triangle) { const auto offset = CellPixel(triangle); @@ -228,6 +294,17 @@ int main() { const float y = i % 2u == 0u ? -1.0f : 0.0f; Strip[i] = {{x, y, 0.5f, 1.0f}, stripColor}; } + for (std::uint32_t strip = 0; strip < LineStrips; ++strip) { + const auto color = TriangleColor(strip); + const std::array rgba{color[0] / 255.0f, color[1] / 255.0f, color[2] / 255.0f, 1.0f}; + const float top = 1.0f - (static_cast(LineStripRow(strip)) + 0.5f) / (Height / 2.0f); + const float bottom = top - 20.0f / (Height / 2.0f); + const float left = 24.5f / (Width / 2.0f) - 1.0f; + const float right = 168.5f / (Width / 2.0f) - 1.0f; + LineStripVertices[3 * strip] = {{left, top, 0.5f, 1.0f}, rgba}; + LineStripVertices[3 * strip + 1] = {{right, top, 0.5f, 1.0f}, rgba}; + LineStripVertices[3 * strip + 2] = {{right, bottom, 0.5f, 1.0f}, rgba}; + } Fan[0] = {{0.0f, 0.0f, 0.5f, 1.0f}, {1.0f, 1.0f, 1.0f, 1.0f}}; for (std::uint32_t k = 1; k <= FanRim; ++k) { const auto color = TriangleColor(k); @@ -240,6 +317,22 @@ int main() { FanRestart[k] = FanIndices[k]; } FanRestart[FanRim] = 0xffffu; + for (std::uint32_t quad = 0; quad < Columns; ++quad) { + const auto color = TriangleColor(quad); + const std::array rgba{color[0] / 255.0f, color[1] / 255.0f, color[2] / 255.0f, 1.0f}; + const float left = -1.0f + 2.0f * static_cast(quad) / Columns; + const float right = left + 1.6f / Columns; + Quads[4 * quad] = {{left, -1.0f, 0.5f, 1.0f}, rgba}; + Quads[4 * quad + 1] = {{left, 0.0f, 0.5f, 1.0f}, rgba}; + Quads[4 * quad + 2] = {{right, -1.0f, 0.5f, 1.0f}, rgba}; + Quads[4 * quad + 3] = {{right, 0.0f, 0.5f, 1.0f}, rgba}; + for (std::uint32_t k = 0; k < 4; ++k) { + QuadStrips[5 * quad + k] = static_cast(4 * quad + k); + QuadStrips32[5 * quad + k] = 4 * quad + k; + } + QuadStrips[5 * quad + 4] = 0xffffu; + QuadStrips32[5 * quad + 4] = 0xffffffffu; + } const auto testDevice = OpenVulkanTestDevice(); if (!testDevice) return VulkanTestSkipped; @@ -264,11 +357,66 @@ int main() { CheckTriangles((std::string("non-indexed triangle list, ") + name).c_str()); } + auto wave32Subgroup = SmallSubgroup; + wave32Subgroup.threadsPerGroup = 32u; + for (const auto& subgroup : {wave32Subgroup, SmallSubgroup, WideSubgroup}) { + ClearPixels(); + DrawMesh(device, {subgroup, {reinterpret_cast(Indices.data()), static_cast(Indices.size()), 2, 1, 0, true}, VertexBufferDescriptor(Scrambled.data(), static_cast(Scrambled.size())), ShaderRecompiler::MeshDrawPushOffsetBytes, false, 32u}); + CheckTriangles("indexed wave32 triangle list"); + ClearPixels(); + DrawMesh(device, {subgroup, {0, static_cast(Ordered.size()), 0, 1, 0, false}, VertexBufferDescriptor(Ordered.data(), static_cast(Ordered.size())), ShaderRecompiler::MeshDrawPushOffsetBytes, false, 32u}); + CheckTriangles("non-indexed wave32 triangle list"); + } + + ClearPixels(); + DrawMesh(device, {FastLaunchSubgroup, {0, Triangles, 0, 1, 0, false}, VertexBufferDescriptor(Ordered.data(), static_cast(Ordered.size())), ShaderRecompiler::MeshDrawPushOffsetBytes, false, 32u, VK_CULL_MODE_NONE, FastLaunchCode, 0x482030u}); + CheckTriangles("fast-launch subgroups"); + ClearPixels(); + DrawMesh(device, {FastLaunchSubgroup, {0, Triangles / 2u, 0, 1, 0, false, Triangles / 4u}, VertexBufferDescriptor(Ordered.data(), static_cast(Ordered.size())), ShaderRecompiler::MeshDrawPushOffsetBytes, false, 32u, VK_CULL_MODE_NONE, FastLaunchCode, 0x482030u}); + for (std::uint32_t triangle = 0; triangle < Triangles; ++triangle) { + const bool drawn = triangle >= Triangles / 4u && triangle < Triangles / 4u + Triangles / 2u; + Require(PixelIs(CellPixel(triangle), drawn ? TriangleColor(triangle) : Background), "fast-launch subgroups from a start vertex: triangle " + std::to_string(triangle) + " pixel " + PixelText(CellPixel(triangle))); + } + bool fastLaunchIndexedRefused = false; + try { + DrawMesh(device, {FastLaunchSubgroup, {reinterpret_cast(Indices.data()), Triangles, 2, 1, 0, true}, VertexBufferDescriptor(Ordered.data(), static_cast(Ordered.size())), ShaderRecompiler::MeshDrawPushOffsetBytes, false, 32u, VK_CULL_MODE_NONE, FastLaunchCode, 0x482030u}); + } catch (const std::runtime_error& error) { + fastLaunchIndexedRefused = std::string(error.what()).find("indexed fast-launch draws are unsupported") != std::string::npos; + if (!fastLaunchIndexedRefused) throw; + } + Require(fastLaunchIndexedRefused, "an indexed fast-launch draw was drawn"); + + 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 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})); { GuestAllocations::Mutation mutation; - mutation.Add(record, recordBlockBytes, true, true); + mutation.Add(record, recordBlockBytes, true, true, true); } const auto drawIndirect = [&](std::uint32_t count, std::uint32_t instances, std::uint32_t first) { const std::array words{count, instances, first, 0u, 0u}; @@ -312,6 +460,21 @@ int main() { } Require(PixelIs((static_cast(Height / 4u) * Width + Width / 2u) * 4u, Background), "triangle strip: the upper half changed"); + const auto quadBuffer = VertexBufferDescriptor(Quads.data(), static_cast(Quads.size())); + ClearPixels(); + DrawMesh(device, {strip, {reinterpret_cast(QuadStrips.data()), static_cast(QuadStrips.size()), 2, 1, 0, true}, quadBuffer, ShaderRecompiler::MeshDrawPushOffsetBytes, true, 64u, VK_CULL_MODE_FRONT_BIT}); + CheckQuads("16-bit triangle strips with restarts", 0, Columns); + ClearPixels(); + DrawMesh(device, {strip, {reinterpret_cast(QuadStrips32.data()), static_cast(QuadStrips32.size()), 4, 1, 0, true}, quadBuffer, ShaderRecompiler::MeshDrawPushOffsetBytes, true, 64u, VK_CULL_MODE_FRONT_BIT}); + CheckQuads("32-bit triangle strips with restarts", 0, Columns); + const std::array quadWords{15u, 1u, 5u, 0u, 0u}; + std::copy(quadWords.begin(), quadWords.end(), record); + AgcDriver::Pm4::DrawParameters quadDraw{reinterpret_cast(QuadStrips.data()), static_cast(QuadStrips.size()), 2, 1, 0, true}; + quadDraw.indirect = AgcDriver::Pm4::DrawParameters::IndirectDraw{reinterpret_cast(record), 0x25u, 20u, 20u, 1u, false, 0u, 0x280u, 0x280u, 0x280u, false, 0u}; + ClearPixels(); + DrawMesh(device, {strip, quadDraw, quadBuffer, ShaderRecompiler::MeshDrawPushOffsetBytes, true, 64u, VK_CULL_MODE_FRONT_BIT}); + CheckQuads("indirect triangle strips with restarts", 1, 3); + const ShaderRecompiler::MeshConfiguration fan{5u, 3u, 5u, 9u, 3u, 64u, 1024u, 0u, 4u}; ClearPixels(); DrawMesh(device, {fan, {0, static_cast(Fan.size()), 0, 1, 0, false}, VertexBufferDescriptor(Fan.data(), static_cast(Fan.size()))}); 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/PassHazards.cpp b/core/libs/prx/libSceAgcDriver/tests/PassHazards.cpp new file mode 100644 index 0000000000..25332fe672 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/PassHazards.cpp @@ -0,0 +1,270 @@ +#include "prx/libSceAgcDriver/Graphics/include/PassHazards.hpp" +#include +#include +#include +#include +#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; +} + +using Ranges = std::vector>; +using Images = std::vector>; + +VkImage image(std::uint64_t id) { + static_assert(sizeof(VkImage) == sizeof(std::uint64_t)); + VkImage handle = VK_NULL_HANDLE; + std::memcpy(&handle, &id, sizeof(handle)); + return handle; +} + +struct Use { + Ranges reads; + Ranges writes; + Images images; + Images attachments; + bool anyReads = false; + bool anyWrites = false; + Ranges writeOnly; + + PassAccess View() const { return {reads, writes, images, attachments, anyReads, anyWrites, writeOnly}; } +}; + +void testRangeSet() { + GuestRangeSet set; + Expect(set.Empty() && !set.Overlaps(0, 100), "an empty set overlaps nothing"); + set.Insert(0x1000, 0x2000); + set.Insert(0x3000, 0x4000); + Expect(set.Size() == 2, "disjoint ranges stay apart"); + Expect(set.Overlaps(0x1fff, 0x2001) && set.Overlaps(0x0, 0x1001) && set.Overlaps(0x3800, 0x3900), "overlaps inside and at the edges"); + Expect(!set.Overlaps(0x2000, 0x3000) && !set.Overlaps(0x0, 0x1000) && !set.Overlaps(0x4000, 0x5000), "touching ranges do not overlap"); + set.Insert(0x2000, 0x3000); + Expect(set.Size() == 1 && set.Overlaps(0x2800, 0x2801), "an adjacent range merges both neighbours"); + set.Insert(0x500, 0x5000); + Expect(set.Size() == 1 && set.Overlaps(0x600, 0x601) && set.Overlaps(0x4fff, 0x5000), "a covering range swallows the set"); + set.Insert(0x9000, 0x9000); + Expect(set.Size() == 1 && !set.Overlaps(0x9000, 0x9001), "an empty range is ignored"); + set.Clear(); + Expect(set.Empty(), "cleared"); +} + +void testRanges() { + PassHazards hazards; + hazards.BeginPass(); + Use reader; + reader.reads = {{0x1000, 0x2000}}; + Expect(hazards.Check(reader.View(), true) == PassHazard::None, "nothing recorded: no hazard"); + hazards.Add(reader.View()); + Expect(hazards.Check(reader.View(), true) == PassHazard::None, "read after read"); + Use writer; + writer.writes = {{0x1800, 0x1900}}; + Expect(hazards.Check(writer.View(), true) == PassHazard::WriteAfterRead, "write after read of the same bytes"); + Use disjoint; + disjoint.writes = {{0x2000, 0x2100}}; + Expect(hazards.Check(disjoint.View(), true) == PassHazard::None, "write next to a read"); + hazards.Add(disjoint.View()); + Use rereader; + rereader.reads = {{0x20f0, 0x2200}}; + Expect(hazards.Check(rereader.View(), true) == PassHazard::ReadAfterWrite, "read after write"); + Use rewriter; + rewriter.writes = {{0x20fc, 0x2100}}; + Expect(hazards.Check(rewriter.View(), true) == PassHazard::WriteAfterWrite, "write after write"); + Use empty; + empty.reads = {{0x2000, 0x2000}}; + empty.writes = {{0x1000, 0x1000}}; + Expect(hazards.Check(empty.View(), true) == PassHazard::None, "empty ranges touch nothing"); + hazards.Clear(); + Expect(hazards.Empty() && hazards.Check(writer.View(), true) == PassHazard::None, "a barrier clears the accesses"); +} + +void testUnknown() { + PassHazards hazards; + hazards.BeginPass(); + Use addressBased; + addressBased.anyReads = true; + Use imageWriter; + imageWriter.images = {{image(7), true}}; + hazards.Add(imageWriter.View()); + Expect(hazards.Check(addressBased.View(), true) == PassHazard::None, "unknown reads after an image write only"); + hazards.Add(addressBased.View()); + Expect(hazards.Check(addressBased.View(), true) == PassHazard::None, "unknown reads after unknown reads"); + Use writer; + writer.writes = {{0x100, 0x104}}; + Expect(hazards.Check(writer.View(), true) == PassHazard::UnknownRead, "a write after unknown reads (WAR guard)"); + Use reader; + reader.reads = {{0x100, 0x104}}; + Expect(hazards.Check(reader.View(), true) == PassHazard::None, "a ranged read after unknown reads"); + hazards.Clear(); + hazards.Add(writer.View()); + Expect(hazards.Check(addressBased.View(), true) == PassHazard::UnknownRead, "unknown reads after a write"); + hazards.Clear(); + Use unknownWriter; + unknownWriter.anyWrites = true; + Expect(hazards.Check(unknownWriter.View(), true) == PassHazard::None, "unknown writes first"); + hazards.Add(imageWriter.View()); + Expect(hazards.Check(unknownWriter.View(), true) == PassHazard::None, "unknown writes after an image access only"); + hazards.Add(reader.View()); + Expect(hazards.Check(unknownWriter.View(), true) == PassHazard::UnknownWrite, "unknown writes after a read"); + hazards.Clear(); + hazards.Add(unknownWriter.View()); + Expect(hazards.Check(reader.View(), true) == PassHazard::UnknownWrite, "a read after unknown writes"); + Expect(hazards.Check(addressBased.View(), true) == PassHazard::UnknownWrite, "unknown reads after unknown writes"); + Expect(hazards.Check(imageWriter.View(), true) == PassHazard::None, "an image-only access after unknown writes"); +} + +void testImages() { + PassHazards hazards; + hazards.BeginPass(); + Use sampler; + sampler.images = {{image(1), false}}; + hazards.Add(sampler.View()); + Expect(hazards.Check(sampler.View(), true) == PassHazard::None, "two reads of an image"); + Use storer; + storer.images = {{image(1), true}}; + Expect(hazards.Check(storer.View(), true) == PassHazard::Image, "a store after a read of the image"); + Use other; + other.images = {{image(2), true}}; + Expect(hazards.Check(other.View(), true) == PassHazard::None, "a store to another image"); + hazards.Add(other.View()); + Use otherReader; + otherReader.images = {{image(2), false}}; + Expect(hazards.Check(otherReader.View(), true) == PassHazard::Image, "a read after a store of the image"); + Use null; + null.images = {{VK_NULL_HANDLE, true}}; + null.attachments = {{VK_NULL_HANDLE, true}}; + Expect(hazards.Check(null.View(), true) == PassHazard::None, "null images are ignored"); +} + +void testAttachments() { + PassHazards hazards; + hazards.BeginPass(); + Use draw; + draw.attachments = {{image(10), true}, {image(11), true}}; + hazards.Add(draw.View()); + Expect(hazards.Check(draw.View(), true) == PassHazard::None, "attachment writes of the same pass are in rasterization order"); + Expect(hazards.Check(draw.View(), false) == PassHazard::Image, "the same attachments in a new pass"); + Use readOnlyDepth; + readOnlyDepth.attachments = {{image(12), false}}; + hazards.Add(readOnlyDepth.View()); + hazards.BeginPass(); + Use otherPass; + otherPass.attachments = {{image(20), true}, {image(12), false}}; + Expect(hazards.Check(otherPass.View(), false) == PassHazard::None, "a new pass of other targets reading a depth image the last pass only read"); + hazards.Add(otherPass.View()); + Expect(hazards.Check(otherPass.View(), true) == PassHazard::None, "continuing the new pass"); + Use sampleEarlier; + sampleEarlier.images = {{image(10), false}}; + Expect(hazards.Check(sampleEarlier.View(), true) == PassHazard::Image, "sampling a target of an earlier pass"); + Use backToFirst; + backToFirst.attachments = {{image(10), true}}; + Expect(hazards.Check(backToFirst.View(), false) == PassHazard::Image, "rendering into a target of an earlier pass"); + hazards.Clear(); + hazards.BeginPass(); + Use sampled; + sampled.images = {{image(30), false}}; + hazards.Add(sampled.View()); + Use renderInto; + renderInto.attachments = {{image(30), true}}; + Expect(hazards.Check(renderInto.View(), true) == PassHazard::Image, "rendering into an image a draw of the pass sampled"); + Use storageWrite; + storageWrite.images = {{image(31), true}}; + hazards.Add(storageWrite.View()); + Use renderIntoStored; + renderIntoStored.attachments = {{image(31), true}}; + Expect(hazards.Check(renderIntoStored.View(), true) == PassHazard::Image, "rendering into an image a draw stored to"); +} + +} + +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; + } + std::printf("pass hazard tests passed\n"); + return 0; +} 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 new file mode 100644 index 0000000000..efad88d641 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp @@ -0,0 +1,604 @@ +#include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp" +#include "prx/libSceAgcDriver/Execution/include/TraceFile.hpp" +#include "zlib.h" +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::TraceFormat; +using AgcDriver::TraceThread; +using AgcDriver::TraceWindow; +using Clock = std::chrono::steady_clock; +using Edge = TraceWindow::Edge; +using std::chrono::microseconds; +using std::chrono::milliseconds; + +void Check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +void WindowByFrames() { + auto window = TraceWindow::Parse("3:2", nullptr).value(); + std::vector edges; + for (std::uint64_t present = 0; present < 8; ++present) edges.push_back(window.Present(present, std::chrono::seconds(present))); + Check(edges == std::vector{Edge::None, Edge::None, Edge::None, Edge::Open, Edge::None, Edge::Close, Edge::None, Edge::None}, "frame window opened or closed at the wrong present"); + Check(window.Describe() == "frames 3:2", "frame window description changed"); +} + +void WindowBySeconds() { + auto window = TraceWindow::Parse(nullptr, "2:1").value(); + std::vector edges; + for (const auto elapsed : {milliseconds(0), milliseconds(1999), milliseconds(2000), milliseconds(2999), milliseconds(3000), milliseconds(5000)}) + edges.push_back(window.Present(0, elapsed)); + Check(edges == std::vector{Edge::None, Edge::None, Edge::Open, Edge::None, Edge::Close, Edge::None}, "seconds window opened or closed at the wrong present"); + Check(window.Describe() == "seconds 2:1", "seconds window description changed"); +} + +void WindowRejects() { + Check(!TraceWindow::Parse(nullptr, nullptr).has_value(), "a window was made without a switch"); + for (const auto* text : {"5", "a:1", "1:0", "1:", ":1", "1:2:3", "-1:2"}) { + bool rejected = false; + try { TraceWindow::Parse(text, nullptr); } + catch (const std::invalid_argument&) { rejected = true; } + Check(rejected, "a malformed window was accepted"); + } + bool rejected = false; + try { TraceWindow::Parse("1:1", "1:1"); } + catch (const std::invalid_argument&) { rejected = true; } + Check(rejected, "frames and seconds were accepted together"); +} + +void BufferCap() { + std::atomic presents{1}; + TraceThread thread("queue worker 0x0", presents, 2); + const auto origin = Clock::now(); + for (int index = 0; index < 3; ++index) thread.Add("Driver.Packet", nullptr, origin + milliseconds(index), origin + milliseconds(index) + microseconds(30)); + const auto snapshot = thread.Collect(); + Check(snapshot.slices.size() == 2 && snapshot.dropped, "a full buffer did not stop and set the dropped flag"); + Check(snapshot.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); + const auto origin = Clock::now(); + thread.Add("Driver.Packet", nullptr, origin, origin + microseconds(5)); + thread.Add("Driver.Packet", "draw", origin + microseconds(5), origin + microseconds(10)); + thread.Add("Driver.Draw", nullptr, origin + microseconds(10), origin + microseconds(40)); + presents.store(2); + thread.Add("Driver.Completion", nullptr, origin + microseconds(50), origin + microseconds(90)); + const auto snapshot = thread.Collect(); + Check(snapshot.slices.size() == 3 && !snapshot.dropped, "short slices were written or long ones lost"); + Check(std::string_view(snapshot.slices[0].scope) == "Driver.Draw", "the first long slice is missing"); + Check(snapshot.slices[1].scope == nullptr && snapshot.slices[1].begin == origin && snapshot.slices[1].length == microseconds(10), "the folded sample of the previous present is wrong"); + Check(std::string_view(snapshot.slices[2].scope) == "Driver.Completion", "the slice after the present is missing"); + Check(snapshot.totals.size() == 4, "totals did not keep scope and stage apart"); + for (const auto& total : snapshot.totals) Check(total.count == 1, "a total counted a slice twice"); +} + +void EscapeStrings() { + std::string output; + TraceFormat::AppendString(output, "a\"b\\c\n\xe9"); + Check(output == "\"a\\\"b\\\\c\\u000a\\u00e9\"", "string escaping is not valid JSON"); +} + +void EventsShape() { + const auto origin = Clock::now(); + const std::vector slices{ + {"Driver.Packet", nullptr, origin + microseconds(10), microseconds(30)}, + {"Driver.Packet", "draw", origin + microseconds(12), microseconds(25)}, + {nullptr, nullptr, origin, milliseconds(2)}, + }; + const std::vector threads{{"queue worker 0x0", slices, {}, false}}; + const std::vector presents{origin, origin + milliseconds(16)}; + const auto json = TraceFormat::Events(7, origin, threads, 0, presents, 5); + const std::string expected = + "{\"traceEvents\":[\n" + "{\"name\":\"process_name\",\"ph\":\"M\",\"ts\":0.000,\"pid\":7,\"tid\":0,\"args\":{\"name\":\"AnyPS5\"}},\n" + "{\"name\":\"thread_name\",\"ph\":\"M\",\"ts\":0.000,\"pid\":7,\"tid\":1,\"args\":{\"name\":\"queue worker 0x0\"}},\n" + "{\"name\":\"queue worker 0x0 folded\",\"ph\":\"C\",\"ts\":0.000,\"pid\":7,\"tid\":1,\"args\":{\"ms\":2.000}},\n" + "{\"name\":\"Driver.Packet\",\"ph\":\"X\",\"ts\":10.000,\"pid\":7,\"tid\":1,\"dur\":30.000},\n" + "{\"name\":\"Driver.Packet.draw\",\"ph\":\"X\",\"ts\":12.000,\"pid\":7,\"tid\":1,\"dur\":25.000},\n" + "{\"name\":\"present\",\"ph\":\"i\",\"ts\":0.000,\"pid\":7,\"tid\":1,\"s\":\"g\",\"args\":{\"present\":5}}\n" + "]}\n"; + Check(json == expected, "trace events changed shape"); +} + +void 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; + summary.commit = "abc"; + summary.dirty = false; + summary.environment = {{"APS5_TRACE_FRAMES", "3:2"}}; + summary.gpu = "GPU"; + summary.driver = "Driver 1"; + summary.os = "OS 1"; + summary.width = 1920; + summary.height = 1080; + summary.window = "frames 3:2"; + summary.firstPresent = 3; + summary.gpuBusyMs = 20; + summary.trace = "aps5-trace-abc-7.json.gz"; + const std::vector threads{ + {"queue worker 0x0", {}, {{"Driver.Packet", nullptr, milliseconds(12), 4}, {"Driver.WorkerWait", nullptr, milliseconds(8), 4}, {"Driver.Completion", nullptr, milliseconds(2), 4}, {"Driver.Packet", "draw", milliseconds(10), 2}}, false}, + {"presenter", {}, {{"Driver.Present", nullptr, milliseconds(6), 2}, {"Driver.Present", "inflight_wait", milliseconds(4), 2}}, false}, + }; + const std::vector presents{origin, origin + milliseconds(16), origin + milliseconds(40)}; + const auto line = TraceFormat::Summary(summary, threads, presents); + const std::string expected = + "{\"commit\":\"abc\",\"dirty\":false,\"env\":{\"APS5_TRACE_FRAMES\":\"3:2\"},\"gpu\":\"GPU\",\"driver\":\"Driver 1\",\"os\":\"OS 1\"," + "\"resolution\":\"1920x1080\",\"window\":{\"spec\":\"frames 3:2\",\"first_present\":3,\"ms\":40.000},\"presents\":2," + "\"frame_ms\":{\"p50\":24.000,\"p90\":24.000,\"p99\":24.000,\"max\":24.000},\"gpu_busy_ms_per_present\":10.000," + "\"worker_busy_pct\":{\"queue worker 0x0\":35.000}," + "\"top_phases_ms_per_frame\":{\"Driver.Packet\":6.000,\"Driver.Packet.draw\":5.000,\"Driver.Present\":3.000,\"Driver.Present.inflight_wait\":2.000,\"Driver.Completion\":1.000}," + "\"trace_dropped\":false,\"trace\":\"aps5-trace-abc-7.json.gz\"}\n"; + Check(line == expected, "summary line changed"); + auto dropped = threads; + dropped[1].dropped = true; + Check(TraceFormat::Summary(summary, dropped, presents).find("\"trace_dropped\":true") != std::string::npos, "a dropped thread did not mark the summary"); + 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() { + const auto origin = Clock::now(); + const AgcDriver::TraceClockSample first{1000.0, origin + microseconds(10)}; + const AgcDriver::TraceClockSample last{41000.0, origin + microseconds(30)}; + const std::vector ranges{{"batch", 5000.0, 9000.0}, {"draw", 6000.0, 7000.0}, {"batch", -39000.0, -30000.0}, {"copy", 8000.0, 8000.0}}; + const auto slices = TraceFormat::GpuSlices(ranges, first, last, origin); + Check(slices.size() == 2, "a GPU range before the window or without length was kept"); + Check(std::string_view(slices[0].scope) == "batch" && slices[0].stage == nullptr && slices[0].begin == origin + microseconds(12) && slices[0].length == microseconds(2), "a GPU range was not mapped between the two calibrations"); + Check(std::string_view(slices[1].scope) == "draw" && slices[1].begin == origin + std::chrono::nanoseconds(12500) && slices[1].length == std::chrono::nanoseconds(500), "a nested GPU range was not mapped"); + const AgcDriver::TraceClockSample same{1000.0, origin + microseconds(10)}; + Check(TraceFormat::GpuSlices(ranges, same, same, origin)[0].begin == origin + microseconds(14), "one calibration does not map at the CPU rate"); +} + +void IntervalDue() { + Check(!AgcDriver::TraceInterval::Parse(nullptr).has_value(), "an interval was made without a switch"); + for (const auto* text : {"", "0", "-5", "1.5", "a", "5s", "1:2", "99999999999"}) { + bool rejected = false; + try { AgcDriver::TraceInterval::Parse(text); } + catch (const std::invalid_argument&) { rejected = true; } + Check(rejected, "a malformed interval was accepted"); + } + auto interval = AgcDriver::TraceInterval::Parse("30").value(); + Check(interval.Describe() == "summary_every 30", "interval description changed"); + Check(!interval.Due(milliseconds(29999)) && interval.Due(milliseconds(30000)), "the interval was due at the wrong time"); + interval.Restart(milliseconds(30500)); + Check(interval.Begun() == milliseconds(30500) && !interval.Due(milliseconds(60499)) && interval.Due(milliseconds(60500)), "a restarted interval did not count from its restart"); +} + +void SummaryOnlyThread() { + std::atomic presents{1}; + std::atomic buffering{false}; + TraceThread thread("queue worker 0x0", presents, 0, buffering); + const auto origin = Clock::now(); + thread.Add("Driver.Packet", nullptr, origin, origin + milliseconds(2)); + thread.Add("Driver.Packet", nullptr, origin + milliseconds(3), origin + milliseconds(4)); + presents.store(2); + thread.Add("Driver.Completion", nullptr, origin + milliseconds(5), origin + microseconds(5005)); + auto snapshot = thread.Collect(); + Check(snapshot.slices.empty() && !snapshot.dropped, "a thread that is not buffering kept a slice"); + Check(snapshot.totals.size() == 2 && snapshot.totals[0].count + snapshot.totals[1].count == 3, "a thread that is not buffering missed a total"); + buffering.store(true); + thread.Add("Driver.Packet", nullptr, origin + milliseconds(6), origin + milliseconds(7)); + Check(thread.Collect().slices.empty(), "a zero capacity buffer held a slice"); +} + +void TotalsSince() { + const std::vector before{{"Driver.Packet", nullptr, milliseconds(4), 2}, {"Driver.Present", nullptr, milliseconds(1), 1}}; + const std::vector now{{"Driver.Packet", nullptr, milliseconds(10), 5}, {"Driver.Present", nullptr, milliseconds(1), 1}, {"Driver.Completion", nullptr, milliseconds(3), 2}}; + const auto delta = TraceFormat::Since(now, before); + Check(delta.size() == 2, "a total without a new slice was kept or a new one lost"); + Check(delta[0].length == milliseconds(6) && delta[0].count == 3, "a total was not reduced by the previous interval"); + Check(std::string_view(delta[1].scope) == "Driver.Completion" && delta[1].length == milliseconds(3) && delta[1].count == 2, "a total without a previous value changed"); + Check(TraceFormat::Since(now, {}).size() == 3, "the first interval dropped a total"); +} + +void IntervalSummaryFields() { + const auto origin = Clock::now(); + AgcDriver::TraceSummary summary; + summary.commit = "abc"; + summary.dirty = true; + summary.environment = {{"APS5_TRACE_SUMMARY_EVERY", "30"}}; + summary.gpu = "GPU"; + summary.driver = "Driver 1"; + summary.os = "OS 1"; + summary.width = 1920; + summary.height = 1080; + summary.window = "summary_every 30"; + summary.firstPresent = 120; + summary.gpuBusyMs = 20; + summary.interval = std::make_pair(30.5, 60.25); + const std::vector threads{ + {"queue worker 0x0", {}, {{"Driver.Packet", nullptr, milliseconds(12), 4}}, false}, + }; + const std::vector presents{origin, origin + milliseconds(16), origin + milliseconds(40)}; + const auto line = TraceFormat::Summary(summary, threads, presents); + const std::string expected = + "{\"commit\":\"abc\",\"dirty\":true,\"env\":{\"APS5_TRACE_SUMMARY_EVERY\":\"30\"},\"gpu\":\"GPU\",\"driver\":\"Driver 1\",\"os\":\"OS 1\"," + "\"resolution\":\"1920x1080\",\"window\":{\"spec\":\"summary_every 30\",\"first_present\":120,\"ms\":40.000},\"interval_start_s\":30.500,\"interval_end_s\":60.250,\"presents\":2," + "\"frame_ms\":{\"p50\":24.000,\"p90\":24.000,\"p99\":24.000,\"max\":24.000},\"gpu_busy_ms_per_present\":10.000," + "\"worker_busy_pct\":{\"queue worker 0x0\":30.000}," + "\"top_phases_ms_per_frame\":{\"Driver.Packet\":6.000}," + "\"trace_dropped\":false,\"trace\":\"\"}\n"; + Check(line == expected, "interval summary line changed"); +} + +void 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"; + const auto stem = "aps5-trace-test-" + std::to_string(Clock::now().time_since_epoch().count()); + const auto path = std::filesystem::temp_directory_path() / (stem + ".json.gz"); + AgcDriver::TraceFile::WriteGzip(path, text); + std::ifstream input(path, std::ios::binary); + const std::vector bytes((std::istreambuf_iterator(input)), std::istreambuf_iterator()); + input.close(); + std::filesystem::remove(path); + Check(bytes.size() > 18 && bytes[0] == 0x1f && bytes[1] == 0x8b && bytes[2] == 8 && bytes[3] == 0, "the gzip header is wrong"); + z_stream stream{}; + Check(inflateInit2(&stream, -MAX_WBITS) == Z_OK, "inflateInit2 failed"); + std::string inflated(text.size() + 1, 0); + stream.next_in = const_cast(bytes.data() + 10); + stream.avail_in = static_cast(bytes.size() - 18); + stream.next_out = reinterpret_cast(inflated.data()); + stream.avail_out = static_cast(inflated.size()); + const int status = inflate(&stream, Z_FINISH); + inflated.resize(inflated.size() - stream.avail_out); + const bool consumed = stream.avail_in == 0; + inflateEnd(&stream); + Check(status == Z_STREAM_END && consumed, "the deflate stream does not end at the trailer"); + Check(inflated == text, "the gzip file does not hold the text"); + const auto word = [&](std::size_t at) { + std::uint32_t value = 0; + for (unsigned index = 0; index < 4; ++index) value |= static_cast(bytes[at + index]) << (8 * index); + return value; + }; + Check(word(bytes.size() - 8) == crc32(0, reinterpret_cast(text.data()), static_cast(text.size())), "the CRC-32 is wrong"); + Check(word(bytes.size() - 4) == text.size(), "the size is wrong"); +} + +void AppendLines() { + const auto path = std::filesystem::temp_directory_path() / ("aps5-summary-test-" + std::to_string(Clock::now().time_since_epoch().count()) + ".jsonl"); + AgcDriver::TraceFile::AppendLine(path, "{\"presents\":1}\n"); + AgcDriver::TraceFile::AppendLine(path, "{\"presents\":2}\n"); + std::ifstream input(path, std::ios::binary); + const std::string written((std::istreambuf_iterator(input)), std::istreambuf_iterator()); + input.close(); + std::filesystem::remove(path); + Check(written == "{\"presents\":1}\n{\"presents\":2}\n", "summary lines were not appended"); +} + +} + +int main() { + try { + WindowByFrames(); + WindowBySeconds(); + WindowRejects(); + BufferCap(); + 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; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/PipelineLinkOptimization.cpp b/core/libs/prx/libSceAgcDriver/tests/PipelineLinkOptimization.cpp new file mode 100644 index 0000000000..976b492c6b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/PipelineLinkOptimization.cpp @@ -0,0 +1,32 @@ +#include "prx/libSceAgcDriver/Execution/include/PipelineLinkOptimization.hpp" +#include + +namespace { + +struct Case { + VkDriverId driver; + bool usable; +}; + +constexpr Case Cases[]{ + {VK_DRIVER_ID_NVIDIA_PROPRIETARY, false}, + {VK_DRIVER_ID_MESA_RADV, true}, + {VK_DRIVER_ID_AMD_PROPRIETARY, true}, + {VK_DRIVER_ID_AMD_OPEN_SOURCE, true}, + {VK_DRIVER_ID_INTEL_PROPRIETARY_WINDOWS, true}, + {VK_DRIVER_ID_MESA_LLVMPIPE, true}, + {VK_DRIVER_ID_MOLTENVK, true}, +}; + +} + +int main() { + int failures = 0; + for (const auto& item : Cases) { + if (AgcDriver::PipelineLinkTimeOptimizationUsable(item.driver) != item.usable) { + std::fprintf(stderr, "driver %d: expected %s background link-time optimization\n", static_cast(item.driver), item.usable ? "usable" : "unusable"); + ++failures; + } + } + return failures == 0 ? 0 : 1; +} 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 new file mode 100644 index 0000000000..f345d9f076 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/PlaceholderImages.cpp @@ -0,0 +1,104 @@ +#include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" +#include "IntermediateRepresentation/IrProgram.hpp" +#include +#include +#include +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +namespace { + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +VkImageViewType ViewType(RdnaImageDimension dimension) { + switch (dimension) { + case RdnaImageDimension::Dim1D: return VK_IMAGE_VIEW_TYPE_1D; + case RdnaImageDimension::Dim1DArray: return VK_IMAGE_VIEW_TYPE_1D_ARRAY; + case RdnaImageDimension::Dim2D: + case RdnaImageDimension::Dim2DMsaa: return VK_IMAGE_VIEW_TYPE_2D; + case RdnaImageDimension::Dim2DArray: + case RdnaImageDimension::Dim2DMsaaArray: return VK_IMAGE_VIEW_TYPE_2D_ARRAY; + case RdnaImageDimension::Dim3D: return VK_IMAGE_VIEW_TYPE_3D; + default: throw std::runtime_error("unknown image dimension"); + } +} + +VkFormat Format(const ImageResource& image) { + if (image.depthCompare) return VK_FORMAT_D16_UNORM; + if (image.atomic64) return VK_FORMAT_R64_UINT; + switch (image.numericClass) { + case IrTextureNumericClass::Float: return VK_FORMAT_R8G8B8A8_UNORM; + case IrTextureNumericClass::Uint: return VK_FORMAT_R32_UINT; + case IrTextureNumericClass::Sint: return VK_FORMAT_R32_SINT; + default: throw std::runtime_error("unknown numeric class"); + } +} + +void CheckEveryHeap() { + constexpr std::array dimensions{RdnaImageDimension::Dim1D, RdnaImageDimension::Dim1DArray, RdnaImageDimension::Dim2D, RdnaImageDimension::Dim2DArray, + RdnaImageDimension::Dim2DMsaa, RdnaImageDimension::Dim2DMsaaArray, RdnaImageDimension::Dim3D}; + std::set heaps; + for (const auto resourceClass : {ImageResourceClass::Sampled, ImageResourceClass::Storage}) { + for (const auto numericClass : {IrTextureNumericClass::Float, IrTextureNumericClass::Uint, IrTextureNumericClass::Sint}) { + for (const bool depthCompare : {false, true}) { + for (const bool atomic : {false, true}) { + for (const bool atomic64 : {false, true}) { + if ((depthCompare && resourceClass == ImageResourceClass::Storage) || (atomic64 && !atomic)) continue; + for (const auto dimension : dimensions) { + ImageResource image; + image.resourceClass = resourceClass; + image.numericClass = numericClass; + image.dimension = dimension; + image.depthCompare = depthCompare; + image.atomic = atomic; + image.atomic64 = atomic64; + std::uint32_t heap = 0; + try { + heap = static_cast(DescriptorBindingForImage(image)); + } catch (const std::exception&) { + 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; + Require(((shape.usage & VK_IMAGE_USAGE_STORAGE_BIT) != 0) == storage && ((shape.usage & VK_IMAGE_USAGE_SAMPLED_BIT) != 0) == !storage, "placeholder usage disagrees with the resource class" + where); + Require((shape.aspect == VK_IMAGE_ASPECT_DEPTH_BIT) == depthCompare, "placeholder aspect disagrees with depth comparison" + where); + 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); + } + } + } + } + } + } + Require(heaps.size() == RuntimeAbi::ImageBindingCount, "not every typed image heap is reachable from an image resource: " + std::to_string(heaps.size())); +} + +} + +int main() { + try { + CheckEveryHeap(); + std::cout << "placeholder image tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index 9ef389177e..6900032797 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -2,16 +2,31 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp" +#include "prx/libSceAgcDriver/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 @@ -20,6 +35,9 @@ #define NOMINMAX #endif #include +#else +#include +#include #endif extern "C" int APS5_VABI sceKernelAvailableFlexibleMemorySize(size_t* size); @@ -50,8 +68,10 @@ std::uint32_t low(const void* pointer) { return static_cast(reint std::uint32_t high(const void* pointer) { return static_cast(reinterpret_cast(pointer) >> 32u); } void execute(AgcDriver::QueueState& state, const std::vector& packet) { + AgcDriver::DriverDetail::RegisterStateKey(state); AgcDriver::Pm4::Validate(packet, 0); AgcDriver::Pm4::Execute(packet, state); + check(AgcDriver::DriverDetail::RegisterStateKey(state) == AgcDriver::DriverDetail::RegisterStateKey(state, false, false), "PM4 changed registers without updating their fingerprint"); } void testCatalog() { @@ -59,18 +79,34 @@ 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() == 55, "reference opcode catalog is incomplete"); + check(values.size() == 56, "reference opcode catalog is incomplete"); expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0xff, {0}), 0); }, "not known"); + 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() { + check(AgcDriver::Pm4::PacketWords(0xffff1000u) == 1, "the one-dword NOP is not one dword"); + for (const auto queue : {0u, 0x20u}) AgcDriver::Pm4::Validate(std::vector{0xffff1000u}, queue); + expectFailure([] { AgcDriver::Pm4::Validate(std::vector{0xffff1000u, 0}, 0); }, "filler size"); + for (const auto header : {0xffff1001u, 0xffff1002u}) check(AgcDriver::Pm4::PacketWords(header) == 0x4001, "a NOP with count 0x3fff and header flags is not sized by its count"); + AgcDriver::Pm4::Validate(makePacket(0x10, {0}), 0); + std::vector largest(0x4000, 0); + largest[0] = 0xfffe1000u; + check(AgcDriver::Pm4::PacketWords(largest[0]) == largest.size(), "the largest counted NOP is not sized by its count"); + AgcDriver::Pm4::Validate(largest, 0); } void testRegisters() { @@ -90,10 +126,15 @@ void testRegisters() { pairs[4] = 0xffffffffu; expectFailure([&] { execute(state, packet); }, "sentinel"); check(!registers.contains(0x12), "invalid indirect packet partially changed state"); + std::array values{0x11223344, 0x55667788}; + execute(state, makePacket(opcode, {low(values.data()), high(values.data()), 0x0a, 2})); + check(registers.at(0x0a) == 0x11223344 && registers.at(0x0b) == 0x55667788, "consecutive indirect registers lost"); + expectFailure([&] { execute(state, makePacket(opcode, {low(values.data()), high(values.data()), 0xffff, 2})); }, "indirect-register"); + expectFailure([&] { execute(state, makePacket(opcode, {low(values.data()), high(values.data()), 0x4000000a, 2})); }, "indirect-register"); packet[1] = 0x1000; packet[2] = 0; expectFailure([&] { execute(state, packet); }, "guest"); - packet[3] = 0; + packet[3] = 0x10000; expectFailure([&] { AgcDriver::Pm4::Validate(packet, 0); }, "control"); } execute(state, makePacket(0x69, {0x11, 50, 51})); @@ -102,7 +143,17 @@ void testRegisters() { check(state.userConfig.at(0x10) == 60, "uconfig index zero failed"); execute(state, makePacket(0x7a, {0x20000243, 0x441})); check(state.indexType == 1 && state.userConfig.at(0x243) == 0x441, "indexed VGT_INDEX_TYPE write lost state"); + execute(state, makePacket(0x7a, {0x10000242, 0x11})); + check(state.userConfig.at(0x242) == 0x11, "indexed VGT_PRIMITIVE_TYPE write lost state"); expectFailure([&] { execute(state, makePacket(0x7a, {0x10000010, 1})); }, "bank selection"); + expectFailure([&] { execute(state, makePacket(0x7a, {0x10000243, 1})); }, "bank selection"); + expectFailure([&] { execute(state, makePacket(0x7a, {0x20000242, 1})); }, "bank selection"); + execute(state, makePacket(0x28, {0x80000000, 0x80000000})); + execute(state, makePacket(0x28, {0, 0})); + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x28, {0x80000001, 0x80000000}), 0); }, "shadowing"); + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x28, {0x80000000, 0x80000002}), 0); }, "shadowing"); + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x28, {0x80000000, 0x80000000}), 0x20); }, "compute queue"); + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x28, {0x80000000}), 0); }, "packet size"); expectFailure([&] { execute(state, makePacket(0x69, {0xffff, 1, 2})); }, "overflow"); expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x9f, {0, 0, 0x80000000, 0}), 0x20); }, "compute"); } @@ -117,10 +168,16 @@ void testRegisterFile() { std::vector> order; for (const auto& [offset, value] : registers) order.emplace_back(offset, value); check(order == std::vector>{{0x10, 7}, {0x41, 2}, {0x300, 3}}, "register file order"); + std::vector> visited; + registers.ForEach([&](std::uint32_t offset, std::uint32_t value) { visited.emplace_back(offset, value); }); + check(visited == order, "register file visit order"); auto copy = registers; copy[0x7000] = 5; check(copy.size() == 4 && registers.size() == 3 && !registers.contains(0x7000) && !(copy == registers), "register file copy"); check(copy.erase(0x7000) == 1 && copy.erase(0x7000) == 0 && copy == registers, "register file erase"); + visited.clear(); + copy.ForEach([&](std::uint32_t offset, std::uint32_t value) { visited.emplace_back(offset, value); }); + check(visited == order, "register file visited an erased register"); bool threw = false; try { static_cast(registers.at(0x42)); @@ -132,13 +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}; @@ -153,6 +219,27 @@ void testContextAndBases() { AgcDriver::Pm4::Validate(makePacket(0x15, {1, 1, 1, 0x2041}), 0); AgcDriver::Pm4::Validate(makePacket(0x16, {0, 0xa041}), 0); expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x15, {1, 1, 1, 0x4041}), 0); }, "dispatch modifiers"); + AgcDriver::Pm4::Validate(makePacket(0x15, {0x280, 1, 1, 0x2045}, 2), 0); + AgcDriver::Pm4::Validate(makePacket(0x16, {0, 0x2045}, 2), 0); + AgcDriver::Pm4::Validate(makePacket(0x15, {0xc0, 1, 1, 0x2047}, 2), 0); + AgcDriver::Pm4::Validate(makePacket(0x15, {0xc0, 1, 1, 0x2007}, 2), 0); + AgcDriver::Pm4::Validate(makePacket(0x16, {0, 0x2005}, 2), 0); + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x15, {1, 1, 1, 0x40}), 0); }, "dispatch modifiers"); + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x16, {0, 0x2047}, 2), 0); }, "indirect dispatch modifiers"); + using Threads = std::array; + check(AgcDriver::Pm4::PartialGroupThreads({0xc0, 1, 1}, {0x00400040, 0x00010001, 0x00010001}) == Threads{}, "complete last groups reported partial threads"); + check(AgcDriver::Pm4::PartialGroupThreads({3, 2, 1}, {0x00100040, 0x00080008, 0x00010001}) == Threads{144, 16, 1}, "partial last group threads lost"); + check(AgcDriver::Pm4::PartialGroupThreads({0, 1, 1}, {0x00100040, 0x00010001, 0x00010001}) == Threads{}, "an empty dispatch reported partial threads"); + expectFailure([] { static_cast(AgcDriver::Pm4::PartialGroupThreads({1, 1, 1}, {0x00000040, 0x00010001, 0x00010001})); }, "partial group size"); + expectFailure([] { static_cast(AgcDriver::Pm4::PartialGroupThreads({1, 1, 1}, {0x00410040, 0x00010001, 0x00010001})); }, "partial group size"); + expectFailure([] { static_cast(AgcDriver::Pm4::PartialGroupThreads({1, 0, 1}, {0x00100040, 0x00010001, 0x00000001})); }, "partial group size"); + 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"); + 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})); @@ -161,7 +248,8 @@ void testContextAndBases() { execute(state, makePacket(0x10, {0x00636261}, 0x2c)); check(state.markers.back() == "abc", "marker text lost"); execute(state, makePacket(0x10, {0}, 0x30)); - expectFailure([&] { execute(state, makePacket(0x10, {0}, 0x30)); }, "underflow"); + execute(state, makePacket(0x10, {0}, 0x30)); + check(state.markers.empty(), "an unbalanced marker pop changed the marker stack"); execute(state, makePacket(0x10, {0}, 0x24)); check(state.shader.empty() && state.context == AgcDriver::InitialContextRegisters() && state.dispatchIndirectBase == 0 && state.indexBase == 0 && !state.savedContext, "dispatch reset retained state"); } @@ -261,6 +349,14 @@ void testIndirectDraw() { check(draw.indirect->baseVertexLocation == 0x280 && draw.indirect->startInstanceLocation == 0x8e && draw.indirect->drawIndexLocation == 0x280 && !draw.indirect->drawIndexEnabled && draw.indirect->indxOffset == 5 && draw.firstVertex == 5 && !draw.indexed && draw.indexCount == 0 && draw.instanceCount == 0, "indirect draw locations mismatch"); check(draw.indirect->RangeBytes() == 16 && draw.indirect->VertexDwordOffset() == 8 && draw.indirect->InstanceDwordOffset() == 12, "indirect draw record geometry mismatch"); check(AgcDriver::Pm4::ResolveDraw(makePacket(0x24, {0x60, 0x280, 0x8e, 2}), state).indirect->arguments == 0x105d87fca0ull, "indirect draw offset not applied"); + AgcDriver::Pm4::Validate(makePacket(0x24, {0, 0x8d, 0, 2}), 0); + AgcDriver::Pm4::Validate(makePacket(0x11, {1, 0x10400014, 0x40}), 0); + expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x11, {1, 0x10400012, 0x40}), 0); }, "alignment"); + const auto unused = AgcDriver::Pm4::ResolveDraw(makePacket(0x24, {0, 0x8d, 0, 2}), state); + check(unused.indirect->baseVertexLocation == 0x8d && unused.indirect->startInstanceLocation == 0x280, "an indirect draw location 0 did not mean no register"); + const auto unusedMulti = AgcDriver::Pm4::ResolveDraw(makePacket(0x2c, {0x20, 0x8c, 0, 0, 3, 0, 0, 32, 0x22}), state); + check(unusedMulti.indirect->startInstanceLocation == 0x280 && unusedMulti.indirect->drawIndexLocation == 0x280, "a multi indirect draw location 0 did not mean no register"); + expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x24, {0, 0x8d, 0x200, 2}), 0); }, "invalid indirect draw register location"); const auto multi = AgcDriver::Pm4::ResolveDraw(makePacket(0x2c, {0x20, 0x8c, 0x280, 0x8d | (1u << 31u), 3, 0, 0, 32, 0x22}), state); check(multi.indirect->count == 3 && multi.indirect->stride == 32 && multi.indirect->drawIndexEnabled && multi.indirect->drawIndexLocation == 0x8d && !multi.indirect->countIndirect && multi.flags == 0x20 && multi.indirect->RangeBytes() == 80, "indirect multi-draw mismatch"); expectFailure([&] { AgcDriver::Pm4::ResolveDraw(makePacket(0x25, {0, 0x8c, 0x280, 0}), state); }, "index base"); @@ -289,6 +385,95 @@ void testIndirectDraw() { expectFailure([&] { AgcDriver::Pm4::ReadDrawCount(*local.indirect); }, "count address"); } +bool sameDraw(const AgcDriver::Pm4::DrawParameters& left, const AgcDriver::Pm4::DrawParameters& right) { + return left.indexAddress == right.indexAddress && left.indexCount == right.indexCount && left.indexSize == right.indexSize && left.instanceCount == right.instanceCount && + left.flags == right.flags && left.indexed == right.indexed && left.firstVertex == right.firstVertex && left.indirect.has_value() == right.indirect.has_value(); +} + +void testIndexAttributesIndirect() { + check(AgcDriver::Pm4::Name(0xc0029100u) == "INDEX_ATTRIBUTES_INDIRECT" && AgcDriver::Pm4::UnsupportedReason(0xc0029100u).empty(), "INDEX_ATTRIBUTES_INDIRECT is not executed"); + check(AgcDriver::Pm4::AccessesMemory(0xc0029100u), "INDEX_ATTRIBUTES_INDIRECT must synchronize guest memory"); + alignas(4) static std::array indices{}; + const auto indexAddress = reinterpret_cast(indices.data()) + 4; + alignas(16) static std::array records{}; + records.fill(0xdeadbeefu); + records[4] = static_cast(indexAddress); + records[5] = static_cast(indexAddress >> 32u); + records[6] = 12; + records[7] = 1; + const auto packetFor = [](const void* base, std::uint32_t index) { return makePacket(0x91, {low(base), high(base), index}); }; + const auto packet = packetFor(records.data(), 1); + AgcDriver::QueueState loaded; + loaded.indexBase = 0x1234; + loaded.indexBufferSize = 77; + loaded.indexType = 2; + execute(loaded, packet); + check(loaded.indexBase == indexAddress && loaded.indexBufferSize == 12 && loaded.indexType == 1, "INDEX_ATTRIBUTES_INDIRECT did not load INDEX_BASE, INDEX_BUFFER_SIZE and INDEX_TYPE from its record"); + AgcDriver::QueueState direct; + execute(direct, makePacket(0x26, {static_cast(indexAddress), static_cast(indexAddress >> 32u)})); + execute(direct, makePacket(0x13, {12})); + execute(direct, makePacket(0x2a, {1})); + const auto offsetDraw = makePacket(0x35, {12, 1, 5, 0}); + const auto fromRecord = AgcDriver::Pm4::ResolveDraw(offsetDraw, loaded); + check(sameDraw(fromRecord, AgcDriver::Pm4::ResolveDraw(offsetDraw, direct)) && fromRecord.indexAddress == indexAddress + 4 && fromRecord.indexSize == 4 && fromRecord.indexCount == 5, "an indexed draw after INDEX_ATTRIBUTES_INDIRECT differs from the same draw after the direct packets"); + execute(loaded, makePacket(0x11, {1, low(records.data()), high(records.data())})); + execute(direct, makePacket(0x11, {1, low(records.data()), high(records.data())})); + const auto indirectDraw = makePacket(0x25, {0, 0x8c, 0x280, 0}); + const auto indirectFromRecord = AgcDriver::Pm4::ResolveDraw(indirectDraw, loaded); + check(sameDraw(indirectFromRecord, AgcDriver::Pm4::ResolveDraw(indirectDraw, direct)) && indirectFromRecord.indexAddress == indexAddress && indirectFromRecord.indexCount == 12 && indirectFromRecord.indexSize == 4, "an indexed indirect draw after INDEX_ATTRIBUTES_INDIRECT differs from the same draw after the direct packets"); + records[6] = 0x7fffffffu; + execute(loaded, packet); + 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; + flushed = 0; + struct FlushHookReset { + ~FlushHookReset() { AgcDriver::GuestMemory::SetFlushHook(nullptr); } + } reset; + AgcDriver::GuestMemory::SetFlushHook([](std::uint64_t address, std::size_t bytes) { + check(address == reinterpret_cast(records.data() + 4) && bytes == 16, "INDEX_ATTRIBUTES_INDIRECT synchronized an unrelated range"); + records[6] = 9; + ++flushed; + }); + execute(loaded, packet); + check(flushed == 1 && loaded.indexBufferSize == 9, "INDEX_ATTRIBUTES_INDIRECT read its record before the GPU work that writes it"); + AgcDriver::GuestMemory::SetFlushHook(nullptr); + records[6] = 12; + + const auto unchanged = [&](const std::vector& rejected, const char* text) { + AgcDriver::QueueState state; + state.indexBase = 0x40; + state.indexBufferSize = 3; + state.indexType = 2; + expectFailure([&] { execute(state, rejected); }, text); + check(state.indexBase == 0x40 && state.indexBufferSize == 3 && state.indexType == 2, "a rejected INDEX_ATTRIBUTES_INDIRECT changed the index state"); + }; + unchanged(makePacket(0x91, {low(records.data()) | 1u, high(records.data()), 1}), "indirect mode"); + unchanged(makePacket(0x91, {low(records.data()) | 4u, high(records.data()), 1}), "16-byte aligned"); + unchanged(makePacket(0x91, {low(records.data()), high(records.data()), 0x10001u}), "reserved attribute index bits"); + unchanged(makePacket(0x91, {low(records.data()), high(records.data())}), "packet size"); + unchanged(packetFor(nullptr, 0), "null or misaligned"); + unchanged(packetFor(reinterpret_cast(0x1000), 0), "guest"); + unchanged(makePacket(0x91, {0xfffffff0u, 0xffffffffu, 1}), "record address overflow"); + expectFailure([&] { AgcDriver::Pm4::Validate(packet, 0x20); }, "compute queue"); + records[7] = 4; + unchanged(packet, "has index type 0x4"); + char named[64]; + std::snprintf(named, sizeof(named), "record at 0x%llx", static_cast(reinterpret_cast(records.data() + 4))); + unchanged(packet, named); + records[7] = 3; + execute(loaded, packet); + expectFailure([&] { AgcDriver::Pm4::ResolveDraw(offsetDraw, loaded); }, "unsupported index type"); + records[7] = 1; +} + void testMemory() { AgcDriver::QueueState state; std::array data{0, 0, 0, 0}; @@ -300,11 +485,24 @@ void testMemory() { check(data[0] == 41 && data[1] == 42, "WRITE_DATA from the PFP failed"); execute(state, makePacket(0x37, {0x04100200, low(data.data()), high(data.data()), 61, 62})); check(data[0] == 61 && data[1] == 62, "WRITE_DATA with a cache policy failed"); + execute(state, makePacket(0x37, {0x40100500, low(data.data()), high(data.data()), 81, 82})); + check(data[0] == 81 && data[1] == 82, "WRITE_DATA to memory from the graphics queue failed"); + execute(state, makePacket(0x37, {0x06100300, low(data.data()), high(data.data()), 91, 92})); + check(data[0] == 91 && data[1] == 92, "WRITE_DATA with destination 3 did not write memory"); + const auto fence = makePacket(0x37, {0x06100300, low(data.data()), high(data.data()), 0}); + const auto fenceLabel = AgcDriver::Pm4::DecodeLabelWrite(fence); + check(fenceLabel.has_value() && fenceLabel->address == reinterpret_cast(data.data()), "WRITE_DATA with destination 3 did not resolve as a label"); + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x37, {0x100000, low(data.data()), high(data.data()), 1}), 0); }, "register destination"); expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x37, {0x08000100, low(data.data()), high(data.data()), 71}), 0); }, "reserved"); expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x37, {0x80000100, low(data.data()), high(data.data()), 51}), 0); }, "engine"); + execute(state, makePacket(0x83, {0xbff8, 2, low(data.data()), high(data.data())})); + check(data[0] == 0 && data[1] == 0, "unwritten constant RAM is not zero"); execute(state, makePacket(0x81, {4, 31, 32})); execute(state, makePacket(0x83, {4, 2, low(data.data()), high(data.data())})); check(data[0] == 31 && data[1] == 32, "constant RAM round trip failed"); + auto copied = state; + execute(copied, makePacket(0x83, {0, 3, low(data.data()), high(data.data())})); + check(data[0] == 0 && data[1] == 31 && data[2] == 32, "a copied queue state lost its constant RAM"); expectFailure([&] { execute(state, makePacket(0x81, {0xbffc, 1, 2})); }, "overflow"); expectFailure([&] { execute(state, makePacket(0x40, {0x10105, 0, 0, low(data.data()), high(data.data())})); }, "64-bit immediate"); } @@ -317,6 +515,22 @@ void testCopies() { check(destination[0] == 11 && destination[1] == 12 && destination[2] == 0, "64-bit COPY_DATA failed"); execute(state, makePacket(0x40, {0x105, 0x12345678, 0, low(destination.data()), high(destination.data())})); check(destination[0] == 0x12345678, "immediate COPY_DATA failed"); + execute(state, makePacket(0x40, {0x505, 0x7654321, 0, low(destination.data()), high(destination.data())})); + check(destination[0] == 0x7654321, "immediate COPY_DATA to DST_SEL 5 memory failed"); + alignas(8) std::array stamp{}; + execute(state, makePacket(0x40, {0x110509, 0, 0, low(stamp.data()), high(stamp.data())})); + check(stamp[0] != 0, "GPU timestamp COPY_DATA to DST_SEL 5 memory failed"); + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x40, {0x305, 1, 0, 0, 0}), 0); }, "GDS or performance-counter destination"); + alignas(8) std::array clearValues{0x55, 0x3f800000}; + execute(state, makePacket(0x40, {0x10001, low(clearValues.data()), high(clearValues.data()), 0xa00a, 0})); + check(state.context.at(0x0a) == 0x55 && state.context.at(0x0b) == 0x3f800000, "COPY_DATA to two context registers failed"); + execute(state, makePacket(0x40, {0x1, low(&clearValues[1]), high(&clearValues[1]), 0xa323, 0})); + check(state.context.at(0x323) == 0x3f800000 && !state.context.contains(0x324), "COPY_DATA to one context register failed"); + check(!AgcDriver::Pm4::ResolveStore(makePacket(0x40, {0x1, low(clearValues.data()), high(clearValues.data()), 0xa00c, 0}), state, 64).has_value(), "COPY_DATA to a register resolved as a memory store"); + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x40, {0x5, 7, 0, 0xa00a, 0}), 0); }, "to a register from a register, GDS, immediate"); + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x40, {0x1, low(clearValues.data()), high(clearValues.data()), 0x2e40, 0}), 0); }, "outside the context registers"); + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x40, {0x10001, low(clearValues.data()), high(clearValues.data()), 0xa3ff, 0}), 0); }, "outside the context registers"); + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x40, {0x1, low(clearValues.data()) + 2, high(clearValues.data()), 0xa00a, 0}), 0); }, "misaligned COPY_DATA source"); alignas(8) std::array clock{}; const auto clockCopy = [&](std::uint64_t* target) { return makePacket(0x40, {0x06016209, 0, 0, low(target), high(target)}); }; execute(state, clockCopy(&clock[0])); @@ -328,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})); @@ -352,7 +589,15 @@ void testCopies() { check(source == destination, "DMA_DATA GDS round trip failed"); expectFailure([&] { execute(state, makePacket(0x50, {0x60100000, low(source.data()), high(source.data()), 0xfffc, 0, 8})); }, "exceeds the GDS"); expectFailure([&] { execute(state, makePacket(0x50, {0x20000000, 0, 1, low(destination.data()), high(destination.data()), 4})); }, "exceeds the GDS"); - expectFailure([&] { execute(state, makePacket(0x50, {0x60200000, low(source.data()), high(source.data()), 0, 0, 4})); }, "destination is not implemented"); + destination = {}; + const auto prefetch = makePacket(0x50, {0x60200000, low(source.data()), high(source.data()), low(source.data()), high(source.data()), 0x80000010}); + const auto prefetchStore = AgcDriver::Pm4::ResolveStore(prefetch, state, 64); + check(prefetchStore.has_value() && prefetchStore->Bytes().empty(), "a DMA_DATA prefetch resolved as a memory store"); + check(!AgcDriver::Pm4::DecodeMemoryCopy(prefetch).has_value(), "a DMA_DATA prefetch decoded as a copy"); + execute(state, prefetch); + check(source[0] == 11 && source[1] == 12 && destination[0] == 0, "a DMA_DATA prefetch wrote memory"); + expectFailure([&] { execute(state, makePacket(0x50, {0x40200000, 1, 0, 0, 0, 4})); }, "prefetch of a register"); + expectFailure([&] { execute(state, makePacket(0x50, {0x60200000, low(source.data()), high(source.data()), 0, 0, 4 | (1u << 27u)})); }, "register destination"); expectFailure([&] { execute(state, makePacket(0x50, {0x60000000 | (1u << 15u), low(source.data()), high(source.data()), low(destination.data()), high(destination.data()), 4})); }, "reserved fields"); expectFailure([&] { execute(state, makePacket(0x37, {0x100, 0x1000, 0, 1})); }, "guest"); #ifdef _WIN32 @@ -367,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; @@ -495,6 +755,28 @@ void testWriteChangedKeepsUntouchedBytes() { check(guest[3] == 7 && guest[100] == 0x55, "write-back rolled back a byte the GPU did not change"); } +void testWriteChangedStoresExactlyTheChangedRuns() { + alignas(256) static std::uint8_t guest[1024 + 13]; + std::vector original(sizeof(guest)), current(sizeof(guest)); + std::uint32_t seed = 12345; + const auto next = [&] { return seed = seed * 1103515245u + 12345u, seed >> 16u; }; + for (std::size_t i = 0; i < sizeof(guest);) { + const auto length = 1 + next() % 40; + const bool changed = next() % 2 == 0; + for (std::size_t j = i; j < std::min(sizeof(guest), i + length); ++j) { + original[j] = static_cast(next()); + current[j] = changed && next() % 9 != 0 ? static_cast(static_cast(original[j]) ^ (1u + next() % 255u)) : original[j]; + } + i += length; + } + for (std::size_t i = 0; i < sizeof(guest); ++i) guest[i] = static_cast(0xa0u ^ i); + AgcDriver::GuestMemory::WriteChanged(reinterpret_cast(guest), current, original); + for (std::size_t i = 0; i < sizeof(guest); ++i) { + const auto expected = current[i] != original[i] ? static_cast(current[i]) : static_cast(0xa0u ^ i); + check(guest[i] == expected, "write-back stored a byte the GPU did not change or missed one it did"); + } +} + void testEventWrite() { for (const auto eventType : {0x07u, 0x0fu, 0x10u}) { AgcDriver::Pm4::Validate(makePacket(0x46, {0x400u | eventType}), 0); @@ -531,6 +813,31 @@ void testEventWrite() { expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x410, 0, 0}), 0); }, "packet size"); expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x13a, 0, 0}), 0); }, "event type 58"); expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x0d}), 0); }, "event type 13"); + for (const auto eventType : {0x19u, 0x1au, 0x26u}) { + AgcDriver::Pm4::Validate(makePacket(0x46, {eventType}), 0); + AgcDriver::Pm4::Validate(makePacket(0x46, {eventType}), 0x20); + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x100u | eventType}), 0); }, "pipeline statistics or SQ event index"); + } + AgcDriver::Pm4::Validate(makePacket(0x46, {0x24}), 0); + 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() { @@ -563,12 +870,14 @@ void testAcquireMem() { invalidWord(0, captured[0] | 2u, "header flags"); invalidWord(1, 4, "control flags"); invalidWord(1, 0x00800000, "control flags"); - invalidWord(3, 1, "above 40 bits"); - invalidWord(5, 1, "above 40 bits"); + invalidWord(3, 0x100, "range high bits"); + invalidWord(5, 0x100, "range high bits"); invalidWord(6, 0x10000, "poll interval"); invalidWord(7, 0x40000, "GCR flags"); invalidWord(7, 0x2000, "cache discard"); - expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x58, {0, 2, 0, 0xffffffff, 0, 0, 0}), 0); }, "range exceeds"); + AgcDriver::Pm4::Validate(makePacket(0x58, {0, 0xffffffff, 0xff, 0, 0, 10, 0xc3b1}), 0); + AgcDriver::Pm4::Validate(makePacket(0x58, {0, 2, 0, 0xffffffff, 0, 0, 0}), 0); + expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x58, {0, 2, 0, 0xffffffff, 0xff, 0, 0}), 0); }, "range exceeds"); expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x58, {0, 0, 0, 0, 0}), 0); }, "packet size"); expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x58, {0, 0, 0, 0, 0, 0, 0, 0}), 0); }, "packet size"); } @@ -633,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())}) @@ -703,6 +1014,85 @@ 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; + alignas(16) static std::array record{0, 0, 0, 4}; + gate = 0; + done = 0; + auto words = joinPackets({ + makePacket(0x3c, {0x13, low(&gate), high(&gate), 1, 0xffffffffu, 0x19}), + makePacket(0x91, {low(record.data()), high(record.data()), 0}), + writeWord(done, 1)}); + submitWords(words); + record = {0x1000, 0, 6, 0}; + std::atomic_ref(gate).store(1); + AgcDriverWaitIdle_nid_postfix(); + check(std::atomic_ref(done).load() == 1, "INDEX_ATTRIBUTES_INDIRECT did not read its record when the packet ran"); +} + +void testSuspendPointWritesQueuedLabels() { + alignas(8) static std::uint32_t gate = 0, marker = 0; + gate = 0; + marker = 0; + auto words = joinPackets({makePacket(0x3c, {0x13, low(&gate), high(&gate), 1, 0xffffffffu, 0x190}), + makePacket(0x49, {0x0030c514, 0x20000000, low(&marker), high(&marker), 1, 0, 0})}); + submitWords(words); + AgcDriverSuspendPoint_nid_postfix(); + std::atomic_ref(gate).store(1); + for (int waited = 0; waited < 2000 && std::atomic_ref(marker).load() == 0; ++waited) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + check(std::atomic_ref(marker).load() == 1, "a label queued before a suspend point was never written"); + AgcDriverWaitIdle_nid_postfix(); +} + void testConditionalSubmission() { alignas(8) static std::uint32_t zero = 0, one = 1, condition = 0; static std::array results{}; @@ -771,6 +1161,20 @@ void testConditionalSubmission() { check(results[13] == 0, "a rejected conditional submission executed a guarded packet"); } +void testNopPadSubmission() { + alignas(8) static std::uint32_t zero = 0; + static std::array results{}; + results.fill(0); + auto words = joinPackets({{0xffff1000u, 0xc0027904u, 0x342u, 0xce200000u, 0u, 0xc0017904u, 0x342u, 0xcea00000u}, writeWord(results[0], 81), {0xffff1000u}}); + submitWords(words); + AgcDriverWaitIdle_nid_postfix(); + check(results[0] == 81, "a one-dword NOP stopped the packets after it"); + words = joinPackets({conditional(zero, 6), {0xffff1000u}, writeWord(results[1], 82), writeWord(results[2], 83)}); + submitWords(words); + AgcDriverWaitIdle_nid_postfix(); + check(results[1] == 0 && results[2] == 83, "COND_EXEC did not count a one-dword NOP as one dword"); +} + std::vector branch(std::uint32_t mode, std::uint32_t function, const std::vector* first, const std::vector* second) { const auto address = [](const std::vector* target) { return target ? reinterpret_cast(target->data()) : std::uintptr_t{0}; }; const auto size = [](const std::vector* target) { return target ? static_cast(target->size()) : 0u; }; @@ -934,6 +1338,426 @@ 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); + submitWords(words); + expectFailure([] { AgcDriverWaitIdle_nid_postfix(); }, "refused"); + check(result == 0, "a queue worker whose priority was refused executed its submission"); + expectFailure([] { LibcRunShutdown_nid_postfix(); }, "refused"); +} + } void testUnwrittenUserData() { @@ -944,6 +1768,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") { @@ -951,15 +1898,46 @@ int main(int argc, char** argv) { std::puts("PM4 asynchronous memory failure propagated to idle, suspend, submit and shutdown"); return 0; } + if (argc == 2 && std::string(argv[1]) == "priority-refused") { + testRefusedWorkerPriority(); + 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(); + testWriteChangedStoresExactlyTheChangedRuns(); testRegisters(); testRegisterFile(); testContextAndBases(); testIndexedDraw(); testAutoDraw(); testIndirectDraw(); + testIndexAttributesIndirect(); testMemory(); + testTraceRegisterWrites(); testCopies(); testMemoryCopyDecode(); testMemorySynchronization(); @@ -973,9 +1951,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 new file mode 100644 index 0000000000..22b9923a2f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/PositionInvariance.cpp @@ -0,0 +1,132 @@ +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void requireInvariantPositions(const ShaderRecompiler::RecompileResult& result, const char* what) { + constexpr std::uint32_t Variable = ~0u; + const auto& words = result.spirv.Words(); + std::set> positions; + std::set> invariant; + 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::OpMemberDecorate && count >= 4) { + if (words[at + 3] == spv::DecorationBuiltIn && count == 5 && words[at + 4] == spv::BuiltInPosition) positions.emplace(words[at + 1], words[at + 2]); + if (words[at + 3] == spv::DecorationInvariant) invariant.emplace(words[at + 1], words[at + 2]); + } + if (op == spv::OpDecorate && count >= 3) { + if (words[at + 2] == spv::DecorationBuiltIn && count == 4 && words[at + 3] == spv::BuiltInPosition) positions.emplace(words[at + 1], Variable); + if (words[at + 2] == spv::DecorationInvariant) invariant.emplace(words[at + 1], Variable); + } + } + if (positions.empty()) throw std::runtime_error(std::string(what) + ": the module declares no position output"); + for (const auto& position : positions) { + if (!invariant.contains(position)) throw std::runtime_error(std::string(what) + ": the position output is not Invariant"); + } +} + +constexpr std::array PositionExport{0xf80008cfu, 0u, 0xbf810000u}; + +constexpr std::array VertexCapabilities{spv::CapabilityClipDistance}; + +ShaderRecompiler::RecompileRequest baseRequest(ShaderRecompiler::ShaderStage stage) { + using namespace ShaderRecompiler; + RecompileRequest request{}; + request.shader = {stage, 0x10000u, PositionExport, 0, {}}; + request.context.waveSize = 64; + request.context.vertex = ShaderVertexStageInfo{}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 64; + request.target.supportedCapabilities = VertexCapabilities; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + return request; +} + +void verifyVertex() { + using namespace ShaderRecompiler; + 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}; + static constexpr std::array extensions{"SPV_EXT_mesh_shader"}; + static constexpr std::array hiddenUserWords{}; + auto request = baseRequest(ShaderStage::Mesh); + request.context.userData = hiddenUserWords; + request.target.spirvVersion = 0x00010400u; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.maxWorkgroupSize = {1024u, 1024u, 64u}; + request.target.maxWorkgroupInvocations = 1024u; + request.target.maxWorkgroupSharedMemoryBytes = 32768u; + request.target.mesh = MeshTargetLimits{{128u, 1u, 1u}, 128u, 32768u, 256u, 256u, 128u, 32768u, 1u, 1u}; + request.graphics = GraphicsCompileContext{0u, {}, MeshConfiguration{4u, 1u, 3u, 3u, 1u, 64u, 128u, 0u, 4u}, std::nullopt, {}}; + requireInvariantPositions(Recompile(request), "mesh shader"); +} + +} // namespace + +int main() { + try { + verifyVertex(); + verifyGuestClipDistances(); + verifyMesh(); + std::cout << "Vertex and mesh position outputs are Invariant\n"; + return 0; + } catch (const std::exception& error) { + const std::string message(error.what()); + std::cerr << message.substr(0, message.find("RecompileRequest:")) << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/PresentationPass.cpp b/core/libs/prx/libSceAgcDriver/tests/PresentationPass.cpp new file mode 100644 index 0000000000..f55704d524 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/PresentationPass.cpp @@ -0,0 +1,582 @@ +#include "prx/libSceAgcDriver/Execution/include/AspectFit.hpp" +#include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp" +#include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" +#include "prx/libSceAgcDriver/Execution/include/PresentationPass.hpp" +#include "prx/libSceAgcDriver/Execution/include/PqToSdr.hpp" +#include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace AgcDriver::Graphics; +using AgcDriver::DisplayBuffer; +using AgcDriver::PresentationPass; +using AgcDriver::PresentationScaler; + +constexpr int VulkanTestSkipped = 77; +constexpr std::uint64_t Bgra8 = 0x8000000000000000ull; +constexpr std::uint64_t Rgba8 = 0x8000000022000000ull; +constexpr std::uint64_t TenBit = 0x0100000000000000ull; +constexpr std::uint64_t Rgba16Float = 0xc001000622000000ull; +constexpr std::uint64_t Rgb10Pq = 0x8100070422000000ull; +constexpr std::uint64_t Bgr10Pq = 0x8100070400000000ull; + +class Device { +public: + Device() { +#ifdef _WIN32 + library = SDL_LoadObject("vulkan-1.dll"); +#else + library = SDL_LoadObject("libvulkan.so.1"); +#endif + Require(library != nullptr, "cannot load Vulkan"); + try { + instanceProc = reinterpret_cast(SDL_LoadFunction(library, "vkGetInstanceProcAddr")); + Require(instanceProc != nullptr, "missing Vulkan instance resolver"); + VkApplicationInfo application{VK_STRUCTURE_TYPE_APPLICATION_INFO}; + application.apiVersion = VK_API_VERSION_1_1; + VkInstanceCreateInfo info{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; + info.pApplicationInfo = &application; + Check(function("vkCreateInstance")(&info, nullptr, &instance), "vkCreateInstance"); + std::uint32_t count = 0; + const auto enumerate = function("vkEnumeratePhysicalDevices"); + Check(enumerate(instance, &count, nullptr), "vkEnumeratePhysicalDevices"); + Require(count != 0, "no Vulkan device"); + std::vector devices(count); + Check(enumerate(instance, &count, devices.data()), "vkEnumeratePhysicalDevices"); + context.physical = devices.front(); + const auto queues = function("vkGetPhysicalDeviceQueueFamilyProperties"); + queues(context.physical, &count, nullptr); + std::vector families(count); + queues(context.physical, &count, families.data()); + std::uint32_t family = 0; + while (family < count && (families[family].queueFlags & VK_QUEUE_GRAPHICS_BIT) == 0) ++family; + Require(family < count, "no Vulkan graphics queue"); + const float priority = 1; + VkDeviceQueueCreateInfo queue{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; + queue.queueFamilyIndex = family; + queue.queueCount = 1; + queue.pQueuePriorities = &priority; + VkDeviceCreateInfo device{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO}; + device.queueCreateInfoCount = 1; + device.pQueueCreateInfos = &queue; + Check(function("vkCreateDevice")(context.physical, &device, nullptr, &context.device), "vkCreateDevice"); + context.deviceProc = function("vkGetDeviceProcAddr"); + function("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory); + VkPhysicalDeviceProperties properties{}; + function("vkGetPhysicalDeviceProperties")(context.physical, &properties); + context.limits = properties.limits; + context.formatProperties = function("vkGetPhysicalDeviceFormatProperties"); + context.imageFormatProperties = function("vkGetPhysicalDeviceImageFormatProperties"); + context.Function("vkGetDeviceQueue")(context.device, family, 0, &context.queue); + VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + pool.queueFamilyIndex = family; + Check(context.Function("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool"); + } catch (...) { + release(); + throw; + } + } + + ~Device() { release(); } + const Context& GetContext() const { return context; } + +private: + template + TFunction function(const char* name) const { + const auto result = reinterpret_cast(instanceProc(instance, name)); + Require(result != nullptr, name); + return result; + } + + void release() noexcept { + if (context.pool != VK_NULL_HANDLE) context.Function("vkDestroyCommandPool")(context.device, context.pool, nullptr); + context.bufferPool.reset(); + if (context.device != VK_NULL_HANDLE) function("vkDestroyDevice")(context.device, nullptr); + if (instance != VK_NULL_HANDLE) function("vkDestroyInstance")(instance, nullptr); + if (library != nullptr) SDL_UnloadObject(library); + } + + void* library = nullptr; + PFN_vkGetInstanceProcAddr instanceProc = nullptr; + VkInstance instance = VK_NULL_HANDLE; + Context context{}; +}; + +class Image { +public: + Image(const Context& context, VkFormat format, VkExtent2D extent, VkImageUsageFlags usage, VkImageCreateFlags flags = 0, std::uint32_t layers = 1) : context(context), extent(extent) { + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.flags = flags; + info.imageType = VK_IMAGE_TYPE_2D; + info.format = format; + info.extent = {extent.width, extent.height, 1}; + info.mipLevels = 1; + info.arrayLayers = layers; + info.samples = VK_SAMPLE_COUNT_1_BIT; + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.usage = usage; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage resident present test"); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + const auto allocated = context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory); + if (allocated != VK_SUCCESS) context.Function("vkDestroyImage")(context.device, image, nullptr); + Check(allocated, "vkAllocateMemory resident present test"); + Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory resident present test"); + } + ~Image() { + context.Function("vkDestroyImage")(context.device, image, nullptr); + context.Function("vkFreeMemory")(context.device, memory, nullptr); + } + Image(const Image&) = delete; + Image& operator=(const Image&) = delete; + VkImage Handle() const { return image; } + VkExtent2D Extent() const { return extent; } + +private: + const Context& context; + VkExtent2D extent; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; +}; + +void transition(const Context& context, VkCommandBuffer commands, VkImage image, VkImageLayout from, VkImageLayout to, VkPipelineStageFlags sourceStage, VkPipelineStageFlags destinationStage, VkAccessFlags sourceAccess, VkAccessFlags destinationAccess, std::uint32_t layers = 1) { + VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + barrier.srcAccessMask = sourceAccess; + barrier.dstAccessMask = destinationAccess; + barrier.oldLayout = from; + barrier.newLayout = to; + barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = image; + barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, layers}; + context.Function("vkCmdPipelineBarrier")(commands, sourceStage, destinationStage, 0, 0, nullptr, 0, nullptr, 1, &barrier); +} + +void readImage(const Context& context, VkCommandBuffer commands, const Image& image, Buffer& destination) { + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {image.Extent().width, image.Extent().height, 1}; + context.Function("vkCmdCopyImageToBuffer")(commands, image.Handle(), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destination.Handle(), 1, ©); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); +} + +std::vector bytesOf(Buffer& buffer, std::size_t size) { + const auto bytes = buffer.Bytes(); + return {bytes.begin(), bytes.begin() + static_cast(size)}; +} + +void letterboxBandTests() { + const std::array, 7> cases{{{3840, 2160, 2261, 1272}, {3840, 2160, 3840, 2160}, {3840, 2160, 1000, 1000}, {3840, 2160, 2000, 500}, {200, 130, 260, 130}, {200, 130, 117, 76}, {7, 3, 5, 11}}}; + for (const auto& [width, height, destinationWidth, destinationHeight] : cases) { + const auto rect = AgcDriver::ComputeContainRect_nid_postfix(width, height, destinationWidth, destinationHeight); + const auto bands = PresentationScaler::LetterboxBands(width, height, destinationWidth, destinationHeight); + std::vector covered(static_cast(destinationWidth) * destinationHeight, 0); + auto cover = [&](const AgcDriver::AspectFitRect& area) { + Require(area.width != 0 && area.height != 0, "an empty letterbox band"); + for (std::uint32_t y = 0; y < area.height; ++y) { + for (std::uint32_t x = 0; x < area.width; ++x) ++covered[static_cast(area.y + y) * destinationWidth + area.x + x]; + } + }; + cover(rect); + for (const auto& band : bands) cover(band); + Require(std::all_of(covered.begin(), covered.end(), [](std::uint8_t count) { return count == 1; }), "the letterbox bands and the image do not tile the destination: " + std::to_string(width) + "x" + std::to_string(height) + " into " + std::to_string(destinationWidth) + "x" + std::to_string(destinationHeight)); + } + Require(PresentationScaler::LetterboxBands(3840, 2160, 1920, 1080).empty(), "an exact fit has letterbox bands"); +} + +struct Source { + std::uint64_t pixelFormat; + VkFormat storage; + std::uint32_t width; + std::uint32_t height; + std::vector words; + std::vector decoded; +}; + +Source makeSource(std::uint64_t pixelFormat, VkFormat storage, std::uint32_t width, std::uint32_t height, std::uint32_t salt = 0) { + Source source{pixelFormat, storage, width, height, std::vector(static_cast(width) * height * 4), {}}; + std::uint32_t seed = 0x9e3779b9u ^ static_cast(storage) ^ static_cast(pixelFormat >> 32u) ^ salt; + for (auto& byte : source.words) { + seed = seed * 1664525u + 1013904223u; + byte = static_cast(seed >> 24u); + } + const ColorTargetLayout layout(width, height, ColorTileMode::RenderTarget); + std::vector storageBytes(layout.Bytes() + 65536); + auto* aligned = reinterpret_cast((reinterpret_cast(storageBytes.data()) + 65535u) & ~std::uintptr_t{65535}); + std::span guest(aligned, layout.Bytes()); + layout.Tile(source.words, guest); + const DisplayBuffer display{reinterpret_cast(aligned), pixelFormat, width, height}; + Require(AgcDriver::DisplayBufferSize(display) == guest.size(), "test display buffer size differs from the color layout"); + source.decoded = AgcDriver::DecodeDisplayBuffer(display, guest); + return source; +} + +std::unique_ptr residentImage(const Context& context, const std::vector& slices) { + const auto& source = *slices.front(); + const auto layers = static_cast(slices.size()); + auto image = std::make_unique(context, source.storage, VkExtent2D{source.width, source.height}, VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT, layers); + Buffer upload(context, source.words.size() * layers, VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + std::vector regions(layers); + for (std::uint32_t layer = 0; layer < layers; ++layer) { + std::memcpy(upload.Bytes().data() + source.words.size() * layer, slices[layer]->words.data(), source.words.size()); + regions[layer].bufferOffset = source.words.size() * layer; + regions[layer].imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, layer, 1}; + regions[layer].imageExtent = {source.width, source.height, 1}; + } + CommandBatch batch(context); + const auto commands = batch.Handle(); + transition(context, commands, image->Handle(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT, layers); + context.Function("vkCmdCopyBufferToImage")(commands, upload.Handle(), image->Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, layers, regions.data()); + transition(context, commands, image->Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT, layers); + batch.SubmitAndWait(); + return image; +} + +std::unique_ptr residentImage(const Context& context, const Source& source) { return residentImage(context, std::vector{&source}); } + +std::vector convertPathPresent(const Context& context, const Source& source, VkExtent2D extent) { + Buffer upload(context, source.decoded.size(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + std::memcpy(upload.Bytes().data(), source.decoded.data(), source.decoded.size()); + PresentationScaler scaler(context, VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_B8G8R8A8_UNORM); + Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, extent, VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + const auto bytes = static_cast(extent.width) * extent.height * 4; + Buffer readback(context, bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + scaler.EnsureSourceImage(source.width, source.height); + CommandBatch batch(context); + const auto commands = batch.Handle(); + scaler.RecordUpload(commands, upload.Handle()); + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT); + VkClearColorValue black{}; + black.float32[3] = 1.0f; + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Function("vkCmdClearColorImage")(commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &range); + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + scaler.RecordBlit(commands, destination.Handle(), extent.width, extent.height); + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + readImage(context, commands, destination, readback); + batch.SubmitAndWait(); + return bytesOf(readback, bytes); +} + +std::vector passPresent(const Context& context, PresentationPass& pass, const Source& source, const Image& resident, VkExtent2D extent, VkFormat format, std::uint32_t layer = 0, float whiteNits = AgcDriver::SdrToneMapLuminanceNits, 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); + pass.Bind(0, resident.Handle(), source.width, source.height, source.pixelFormat, whiteNits, layer); + { + CommandBatch batch(context); + const auto commands = batch.Handle(); + pass.RecordPresent(commands, 0, destination.Handle(), format, colorSpace, extent, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + readImage(context, commands, destination, readback); + batch.SubmitAndWait(); + } + pass.Release(0); + return bytesOf(readback, bytes); +} + +std::vector passDump(const Context& context, PresentationPass& pass, const Source& source, const Image& resident, std::uint32_t stride, std::uint32_t layer = 0, float whiteNits = AgcDriver::SdrToneMapLuminanceNits) { + const auto extent = PresentationPass::DumpExtent(source.width, source.height, stride); + const auto bytes = static_cast(extent.width) * extent.height * 4; + Buffer readback(context, bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + pass.Bind(1, resident.Handle(), source.width, source.height, source.pixelFormat, whiteNits, layer); + { + CommandBatch batch(context); + pass.RecordDump(batch.Handle(), 1, stride, readback.Handle()); + batch.SubmitAndWait(); + } + pass.Release(1); + return bytesOf(readback, bytes); +} + +std::string describe(const Source& source, VkExtent2D extent) { + return std::string(AgcDriver::DisplayPq(source.pixelFormat) ? "PQ " : AgcDriver::DisplayTenBit(source.pixelFormat) ? "10-bit " : "8-bit ") + (AgcDriver::DisplayRedLow(source.pixelFormat) ? "red-low" : "blue-low") + " display in VkFormat " + std::to_string(static_cast(source.storage)) + " at " + std::to_string(extent.width) + "x" + std::to_string(extent.height); +} + +void passMatchesConvertPath(const Context& context, PresentationPass& pass, const Source& source, const Image& resident) { + const std::array exact{{{source.width, source.height}, {source.width + 60, source.height}, {source.width, source.height + 51}}}; + for (const auto extent : exact) { + const auto expected = convertPathPresent(context, source, extent); + const auto drawn = passPresent(context, pass, source, resident, extent, VK_FORMAT_B8G8R8A8_UNORM); + Require(expected == drawn, "the presentation pass differs from the convert path: " + describe(source, extent)); + } + const std::array scaled{{{117, 76}, {331, 215}, {150, 150}}}; + for (const auto extent : scaled) { + const auto expected = convertPathPresent(context, source, extent); + const auto drawn = passPresent(context, pass, source, resident, extent, VK_FORMAT_B8G8R8A8_UNORM); + int worst = 0; + std::size_t differing = 0; + for (std::size_t i = 0; i < expected.size(); ++i) { + const int difference = std::abs(static_cast(expected[i]) - static_cast(drawn[i])); + worst = std::max(worst, difference); + differing += difference != 0; + } + std::printf(" %s: scaled, %zu of %zu channels differ, by at most %d\n", describe(source, extent).c_str(), differing, expected.size(), worst); + Require(worst <= 2, "the scaled presentation pass differs from the convert path's filtered blit by more than two levels: " + describe(source, extent)); + } +} + +void passDumpMatchesSubsample(const Context& context, PresentationPass& pass, const Source& source, const Image& resident, std::uint32_t layer = 0) { + for (const std::uint32_t stride : {1u, 3u, 4u}) { + const auto extent = PresentationPass::DumpExtent(source.width, source.height, stride); + std::vector expected(static_cast(extent.width) * extent.height * 4); + for (std::uint32_t y = 0; y < extent.height; ++y) { + for (std::uint32_t x = 0; x < extent.width; ++x) std::memcpy(expected.data() + (static_cast(y) * extent.width + x) * 4, source.decoded.data() + (static_cast(y) * stride * source.width + x * stride) * 4, 4); + } + Require(passDump(context, pass, source, resident, stride, layer) == expected, "the presentation dump differs from the subsampled guest-path frame: " + describe(source, extent) + " stride " + std::to_string(stride)); + } +} + +bool renderable(const Context& context, VkFormat format) { + VkFormatProperties properties{}; + context.formatProperties(context.physical, format, &properties); + constexpr VkFormatFeatureFlags needed = VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT | VK_FORMAT_FEATURE_BLIT_DST_BIT; + return (properties.optimalTilingFeatures & needed) == needed; +} + +void 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}) { + if (!renderable(context, format)) { + std::printf(" 10-bit destination VkFormat %d is not renderable here; skipped\n", static_cast(format)); + continue; + } + const bool redLowDestination = format == VK_FORMAT_A2B10G10R10_UNORM_PACK32; + const VkExtent2D extent{source.width + 40, source.height}; + const auto rect = AgcDriver::ComputeContainRect_nid_postfix(source.width, source.height, extent.width, extent.height); + const auto converted = convertPathPresent(context, source, extent); + const auto drawn = passPresent(context, pass, source, resident, extent, format); + for (std::uint32_t y = 0; y < extent.height; ++y) { + for (std::uint32_t x = 0; x < extent.width; ++x) { + const auto index = static_cast(y) * extent.width + x; + std::uint32_t word = 0; + std::memcpy(&word, drawn.data() + index * 4, 4); + const std::uint32_t low = word & 0x3ffu, green = (word >> 10u) & 0x3ffu, high = (word >> 20u) & 0x3ffu; + const std::uint32_t red = redLowDestination ? low : high, blue = redLowDestination ? high : low; + const bool inside = static_cast(x) >= rect.x && static_cast(x) < rect.x + static_cast(rect.width); + if (!inside) { + Require(word == 0xc0000000u, "a 10-bit letterbox band is not opaque black: " + describe(source, extent)); + continue; + } + std::uint32_t sourceWord = 0; + std::memcpy(&sourceWord, source.words.data() + (static_cast(y) * source.width + (x - static_cast(rect.x))) * 4, 4); + const std::uint32_t sourceLow = sourceWord & 0x3ffu, sourceGreen = (sourceWord >> 10u) & 0x3ffu, sourceHigh = (sourceWord >> 20u) & 0x3ffu; + const std::uint32_t sourceRed = redLowSource ? sourceLow : sourceHigh, sourceBlue = redLowSource ? sourceHigh : sourceLow; + Require(red == sourceRed && green == sourceGreen && blue == sourceBlue && (word >> 30u) == 3u, "the 10-bit presentation lost bits: " + describe(source, extent) + " into VkFormat " + std::to_string(static_cast(format))); + const auto* eight = converted.data() + index * 4; + Require(static_cast(eight[0]) == (blue >> 2u) && static_cast(eight[1]) == (green >> 2u) && static_cast(eight[2]) == (red >> 2u), "the 10-bit presentation's top bits differ from the convert path: " + describe(source, extent)); + } + } + } +} + +void blitLetterboxMatchesClear(const Context& context) { + const auto source = makeSource(Bgra8, VK_FORMAT_B8G8R8A8_UNORM, 200, 130); + const auto resident = residentImage(context, source); + PresentationScaler scaler(context, VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_B8G8R8A8_UNORM); + for (const auto extent : {VkExtent2D{200, 130}, VkExtent2D{260, 130}, VkExtent2D{200, 181}, VkExtent2D{117, 76}, VkExtent2D{331, 300}}) { + const auto bytes = static_cast(extent.width) * extent.height * 4; + std::array, 2> results; + for (std::size_t banded = 0; banded < 2; ++banded) { + Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, extent, VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + Buffer readback(context, bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + CommandBatch batch(context); + const auto commands = batch.Handle(); + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT); + if (banded != 0) { + scaler.RecordLetterbox(commands, source.width, source.height, destination.Handle(), extent.width, extent.height); + } else { + VkClearColorValue black{}; + black.float32[3] = 1.0f; + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Function("vkCmdClearColorImage")(commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &range); + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + } + PresentationScaler::RecordBlitFrom(context, commands, resident->Handle(), VK_IMAGE_LAYOUT_GENERAL, source.width, source.height, VK_FILTER_LINEAR, destination.Handle(), extent.width, extent.height); + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + readImage(context, commands, destination, readback); + batch.SubmitAndWait(); + results[banded] = bytesOf(readback, bytes); + } + Require(results[0] == results[1], "the letterbox bands differ from a full clear: " + describe(source, extent)); + } +} + +void passPresentsSlice(const Context& context, PresentationPass& pass) { + const auto first = makeSource(Bgra8, VK_FORMAT_B8G8R8A8_UNORM, 200, 130); + const auto second = makeSource(Bgra8, VK_FORMAT_B8G8R8A8_UNORM, 200, 130, 0x5a5a5a5au); + Require(first.decoded != second.decoded, "the two slices of the array test image hold the same texels"); + const auto resident = residentImage(context, std::vector{&first, &second}); + const std::array slices{&first, &second}; + for (std::uint32_t layer = 0; layer < slices.size(); ++layer) { + const auto& source = *slices[layer]; + for (const auto extent : {VkExtent2D{source.width, source.height}, VkExtent2D{source.width + 60, source.height}}) { + Require(passPresent(context, pass, source, *resident, extent, VK_FORMAT_B8G8R8A8_UNORM, layer) == convertPathPresent(context, source, extent), "the presentation pass of slice " + std::to_string(layer) + " of an array image differs from the convert path: " + describe(source, extent)); + } + passDumpMatchesSubsample(context, pass, source, *resident, layer); + } +} + +void passRefusesHalfFloat(PresentationPass& pass, const Image& resident) { + bool refused = false; + try { + pass.Bind(1, resident.Handle(), 200, 130, Rgba16Float, AgcDriver::SdrToneMapLuminanceNits); + } catch (const std::exception&) { + refused = true; + } + Require(refused, "the presentation pass took a 16-bit float display buffer"); + refused = false; + try { + pass.Bind(1, resident.Handle(), 200, 130, Rgb10Pq, 0.0f); + } catch (const std::exception&) { + refused = true; + } + Require(refused, "the presentation pass took a white level below 1 nit"); +} + +int largestDifference(const std::vector& expected, const std::vector& actual) { + Require(expected.size() == actual.size(), "compared frames differ in size"); + int worst = 0; + for (std::size_t i = 0; i < expected.size(); ++i) worst = std::max(worst, std::abs(std::to_integer(expected[i]) - std::to_integer(actual[i]))); + return worst; +} + +Source convertedAt(const Source& source, float whiteNits) { + auto converted = source; + for (std::size_t i = 0; i < converted.decoded.size(); i += 4) { + std::uint32_t word = 0; + std::memcpy(&word, source.words.data() + i, 4); + const auto pixel = AgcDriver::PqToSdrPixel(word, AgcDriver::DisplayRedLow(source.pixelFormat), whiteNits); + for (std::size_t channel = 0; channel < pixel.size(); ++channel) converted.decoded[i + channel] = static_cast(pixel[channel]); + } + return converted; +} + +void passConvertsPq(const Context& context, PresentationPass& pass, const Source& guest, const Image& resident, float whiteNits) { + const auto source = convertedAt(guest, whiteNits); + for (const auto extent : {VkExtent2D{source.width, source.height}, VkExtent2D{source.width + 60, source.height}, VkExtent2D{source.width, source.height + 51}}) { + Require(largestDifference(convertPathPresent(context, source, extent), passPresent(context, pass, source, resident, extent, VK_FORMAT_B8G8R8A8_UNORM, 0, whiteNits)) <= 1, "the PQ presentation pass differs from the CPU conversion by more than one level: " + describe(source, extent)); + } + for (const auto extent : {VkExtent2D{117, 76}, VkExtent2D{331, 215}}) { + const int worst = largestDifference(convertPathPresent(context, source, extent), passPresent(context, pass, source, resident, extent, VK_FORMAT_B8G8R8A8_UNORM, 0, whiteNits)); + std::printf(" %s, white %.0f nits: scaled, by at most %d\n", describe(source, extent).c_str(), whiteNits, worst); + Require(worst <= 2, "the scaled PQ presentation pass differs from the convert path's filtered blit by more than two levels: " + describe(source, extent)); + } + for (const std::uint32_t stride : {1u, 3u}) { + const auto extent = PresentationPass::DumpExtent(source.width, source.height, stride); + std::vector expected(static_cast(extent.width) * extent.height * 4); + for (std::uint32_t y = 0; y < extent.height; ++y) { + for (std::uint32_t x = 0; x < extent.width; ++x) std::memcpy(expected.data() + (static_cast(y) * extent.width + x) * 4, source.decoded.data() + (static_cast(y) * stride * source.width + x * stride) * 4, 4); + } + Require(largestDifference(expected, passDump(context, pass, source, resident, stride, 0, whiteNits)) <= 1, "the PQ presentation dump differs from the subsampled CPU conversion: " + describe(source, extent) + " stride " + std::to_string(stride)); + } +} + +void residentPresentTests(const Context& context) { + PresentationPass pass(context, 2); + const std::array, 5> cases{{ + {Bgra8 | TenBit, VK_FORMAT_A2B10G10R10_UNORM_PACK32}, + {Rgba8 | TenBit, VK_FORMAT_A2B10G10R10_UNORM_PACK32}, + {Bgra8 | TenBit, VK_FORMAT_A2R10G10B10_UNORM_PACK32}, + {Bgra8, VK_FORMAT_R8G8B8A8_UNORM}, + {Rgba8, VK_FORMAT_B8G8R8A8_UNORM}, + }}; + for (const auto& [pixelFormat, storage] : cases) { + const auto source = makeSource(pixelFormat, storage, 200, 130); + const auto resident = residentImage(context, source); + passMatchesConvertPath(context, pass, source, *resident); + passDumpMatchesSubsample(context, pass, source, *resident); + if (AgcDriver::DisplayTenBit(pixelFormat)) passKeepsTenBits(context, pass, source, *resident); + if (pixelFormat == Bgra8) passRefusesHalfFloat(pass, *resident); + } + for (const auto& [pixelFormat, storage] : {std::pair{Rgb10Pq, VK_FORMAT_A2B10G10R10_UNORM_PACK32}, std::pair{Bgr10Pq, VK_FORMAT_A2R10G10B10_UNORM_PACK32}, std::pair{Bgr10Pq, VK_FORMAT_A2B10G10R10_UNORM_PACK32}}) { + const auto source = makeSource(pixelFormat, storage, 200, 130); + const auto resident = residentImage(context, source); + passConvertsPq(context, pass, source, *resident, AgcDriver::SdrToneMapLuminanceNits); + passConvertsPq(context, pass, source, *resident, 1000.0f); + passPassesPqThrough(context, pass, source, *resident); + } + passRefusesSdrToHdr10(context, pass); + passPresentsSlice(context, pass); + blitLetterboxMatchesClear(context); +} + +} + +int main() { + try { + letterboxBandTests(); + std::unique_ptr device; + try { + device = std::make_unique(); + } catch (const std::exception& error) { + if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw; + std::printf("skipped, no usable Vulkan device: %s\n", error.what()); + return VulkanTestSkipped; + } + residentPresentTests(device->GetContext()); + std::cout << "resident present format, presentation pass and letterbox tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/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 ddd72d4e47..497adb234f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -1,18 +1,26 @@ +#include "prx/libSceAgcDriver/Execution/include/PipelineLinkOptimization.hpp" #include "Triangle_frag_spv.h" #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" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Shaders.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureCache.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp" #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" +#include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp" #include "prx/libc/include/GuestAllocations.hpp" @@ -20,26 +28,47 @@ #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" +#include "Cover_vert_spv.h" +#include "Cover_frag_spv.h" +#include "VertexRange_vert_spv.h" +#include "VertexRangeProducer_frag_spv.h" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetTransfer.hpp" +#include "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 #include @@ -53,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; @@ -87,11 +117,116 @@ void ReleaseWatched(void* block, std::size_t bytes) { #endif } +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; +} + +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 @@ -112,26 +247,15 @@ 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"); + 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"); std::vector available(count); @@ -152,12 +276,44 @@ 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; + VkPhysicalDeviceFeatures supported{}; + function("vkGetPhysicalDeviceFeatures")(context.physical, &supported); + enabled.occlusionQueryPrecise = supported.occlusionQueryPrecise; + 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}; VkPhysicalDeviceDriverProperties driverProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES}; @@ -186,9 +342,70 @@ public: minLod.pNext = address.pNext; address.pNext = &minLod; } + VkPhysicalDeviceDynamicRenderingFeaturesKHR rendering{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES_KHR}; + VkPhysicalDeviceExtendedDynamicStateFeaturesEXT dynamicState{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_FEATURES_EXT, &rendering}; + VkPhysicalDeviceGraphicsPipelineLibraryFeaturesEXT library{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_GRAPHICS_PIPELINE_LIBRARY_FEATURES_EXT, &dynamicState}; + const 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)) { + VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &library}; + function("vkGetPhysicalDeviceFeatures2")(context.physical, &features); + VkPhysicalDeviceGraphicsPipelineLibraryPropertiesEXT libraryProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_GRAPHICS_PIPELINE_LIBRARY_PROPERTIES_EXT}; + VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &libraryProperties}; + function("vkGetPhysicalDeviceProperties2")(context.physical, &properties); + context.graphicsPipelineLibrary = library.graphicsPipelineLibrary == VK_TRUE && dynamicState.extendedDynamicState == VK_TRUE && rendering.dynamicRendering == VK_TRUE && libraryProperties.graphicsPipelineLibraryFastLinking == VK_TRUE && properties.properties.limits.maxPushConstantsSize >= PipelinePushConstantBytes; + if (context.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}; + function("vkGetPhysicalDeviceProperties2")(context.physical, &driverProperties); + context.pipelineLinkTimeOptimization = AgcDriver::PipelineLinkTimeOptimizationUsable(driver.driverID); + } + } + rendering = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES_KHR, address.pNext, VK_TRUE}; + library = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_GRAPHICS_PIPELINE_LIBRARY_FEATURES_EXT, &dynamicState, VK_TRUE}; + dynamicState = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_FEATURES_EXT, &rendering, VK_TRUE}; + if (context.graphicsPipelineLibrary) { + 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}; + function("vkGetPhysicalDeviceFeatures2")(context.physical, &features); + nullDescriptors = robustness.nullDescriptor == VK_TRUE; + } + robustness = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT}; + robustness.nullDescriptor = VK_TRUE; + if (nullDescriptors) { + extensionsEnabled.push_back(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME); + robustness.pNext = address.pNext; + 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; @@ -197,14 +414,27 @@ public: function("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory); VkPhysicalDeviceProperties properties{}; function("vkGetPhysicalDeviceProperties")(context.physical, &properties); + 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; @@ -212,7 +442,22 @@ public: } ~Device() { release(); } + 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: @@ -224,6 +469,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) { @@ -238,10 +485,280 @@ private: PFN_vkGetInstanceProcAddr instanceProc = nullptr; VkInstance instance = VK_NULL_HANDLE; Context context{}; + bool graphics = false; + std::mutex settleMutex; + bool nullDescriptors = false; }; using Kind = Recorder::ReadKind; +void copiedDirectOrderingTests(const Device& device, Recorder& recorder) { + auto context = device.GetContext(); + context.nullDescriptors = device.NullDescriptors(); + if (context.hostImportAlignment == 0 || !context.nullDescriptors || !AgcDriver::GuestMemory::WriteWatched()) { + std::cout << "Host imports, null descriptors or write watching unavailable: copied/direct ordering test skipped\n"; + return; + } + recorder.Sync(); + const auto bytes = static_cast(std::max(131072, context.hostImportAlignment)); + auto* block = static_cast(AllocateWatched(bytes, bytes)); + Require(block != nullptr, "allocate copied/direct ordering memory"); + const auto address = reinterpret_cast(block); + std::vector> writers; + context.copiedWriters = &writers; + struct Release { + const Context& context; + Recorder& recorder; + std::uint32_t* block; + std::size_t bytes; + std::vector>& writers; + VkPipeline pipeline = VK_NULL_HANDLE; + VkPipelineLayout layout = VK_NULL_HANDLE; + VkShaderModule module = VK_NULL_HANDLE; + std::shared_ptr layoutOwner; + ~Release() { + recorder.Sync(); + writers.clear(); + DrawCopiedWriters()->clear(); + if (pipeline) context.Function("vkDestroyPipeline")(context.device, pipeline, nullptr); + if (layout) context.Function("vkDestroyPipelineLayout")(context.device, layout, nullptr); + if (module) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + static_cast(HostImportFor(context, reinterpret_cast(block), bytes)); + recorder.Sync(); + ReleaseWatched(block, bytes); + } + } release{context, recorder, block, bytes, writers}; + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + std::memset(block, 0, bytes); + Require(HostImportFor(context, address, bytes) != nullptr, "import copied/direct ordering memory"); + ShaderRecompiler::RecompileResult program; + ShaderRecompiler::DescriptorBinding binding; + binding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + binding.role = ShaderRecompiler::DescriptorRole::GuestBuffers; + binding.descriptorSet = 0; + binding.binding = 0; + binding.count = 1; + binding.guestDescriptor = {static_cast(address), static_cast(address >> 32u) & 0xffffu, 1024, 0x31000000u}; + binding.bufferWritten = {true}; + program.bindings.push_back(binding); + const CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + auto direct = std::make_shared(context, compute); + release.layoutOwner = direct; + Require(!direct->HasCopiedWrites(), "ordering consumer did not bind directly"); + const auto setLayout = direct->Layout(); + const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(std::uint32_t)}; + VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layoutInfo.setLayoutCount = 1; + layoutInfo.pSetLayouts = &setLayout; + layoutInfo.pushConstantRangeCount = 1; + layoutInfo.pPushConstantRanges = &push; + Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &release.layout), "ordering pipeline layout"); + VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + moduleInfo.codeSize = sizeof(BUFFER_UPDATE_SPV); + moduleInfo.pCode = BUFFER_UPDATE_SPV; + Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &release.module), "ordering shader module"); + VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; + pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0, VK_SHADER_STAGE_COMPUTE_BIT, release.module, "main", nullptr}; + pipelineInfo.layout = release.layout; + Check(context.Function("vkCreateComputePipelines")(context.device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &release.pipeline), "ordering pipeline"); + const auto update = [&](const std::shared_ptr& resources, std::uint32_t delta, std::uint32_t count = 1) { + const auto commands = recorder.Commands(); + recorder.Keep(resources); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_MEMORY_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, release.pipeline); + resources->Bind(commands, VK_PIPELINE_BIND_POINT_COMPUTE, release.layout); + context.Function("vkCmdPushConstants")(commands, release.layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(delta), &delta); + context.Function("vkCmdDispatch")(commands, count, 1, 1); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT | VK_ACCESS_HOST_READ_BIT); + resources->MarkGpuWrites(recorder); + }; + for (int mode = 0; mode < 3; ++mode) { + *block = 10; + auto copiedContext = context; + copiedContext.hostImportAlignment = 0; + copiedContext.dmaBufImport = false; + auto copied = std::make_shared(copiedContext, compute); + Require(copied->HasCopiedWrites(), "ordering producer did not take the copied path"); + auto& pending = mode == 2 ? *DrawCopiedWriters() : writers; + pending.push_back(copied); + update(copied, 1); + recorder.OnComplete([copied, &pending] { + copied->WriteBackBuffers(); + std::erase(pending, copied); + }); + Require(!recorder.Idle() && *block == 10, "copied producer was completed before the consumer test"); + if (mode == 0) direct = std::make_shared(context, compute); + else Require(direct->Revalidate(std::span(&compute, 1)), "the imported template could not be revalidated"); + update(direct, 2); + recorder.Sync(); + Require(*block == 13, mode == 0 ? "fresh direct consumer ran before copied write-back" : "reused direct consumer ran before copied write-back"); + } + *block = 20; + block[1024] = 30; + auto copiedContext = context; + copiedContext.hostImportAlignment = 0; + copiedContext.dmaBufImport = false; + auto copied = std::make_shared(copiedContext, compute); + writers.push_back(copied); + update(copied, 1); + recorder.OnComplete([copied, &writers] { + copied->WriteBackBuffers(); + std::erase(writers, copied); + }); + auto separateProgram = program; + separateProgram.bindings[0].guestDescriptor[0] += 4096; + const CompiledShader separateCompute{ShaderRecompiler::ShaderStage::Compute, &separateProgram, 0}; + auto separate = std::make_shared(context, separateCompute); + Require(!writers.empty() && *block == 20, "a disjoint consumer waited for copied writes"); + update(separate, 2); + recorder.Sync(); + Require(*block == 21 && block[1024] == 32, "disjoint copied and direct writes changed each other"); + const std::uint32_t label = 40; + static_cast(recorder.Commands()); + recorder.AfterCompletions(address, std::as_bytes(std::span(&label, 1)), 1000, 0, false); + Require(direct->Revalidate(std::span(&compute, 1)), "template reuse after a CPU completion label failed"); + update(direct, 2); + recorder.Sync(); + Require(*block == 42, "direct consumer preceded an overlapping CPU completion label"); + update(direct, 1); + const auto submissions = recorder.Submissions(); + Require(direct->Revalidate(std::span(&compute, 1)) && recorder.Submissions() == submissions, "ordinary direct work incurred a CPU write-back wait"); + update(direct, 2); + recorder.Sync(); + Require(*block == 45, "direct consumers lost GPU-ordered updates"); + { + 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; + imageContext.detiler = &detiler; + TextureCache textureCache(imageContext); + imageContext.textureCache = &textureCache; + Buffer readback(context, 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + struct Clear { + const Context& context; + Recorder& recorder; + ~Clear() { recorder.Sync(); ClearCachedTextures(context.device); } + } clear{context, recorder}; + ShaderRecompiler::RecompileResult imageProgram; + ShaderRecompiler::DescriptorBinding imageBinding; + imageBinding.kind = ShaderRecompiler::DescriptorKind::SampledImage; + imageBinding.role = ShaderRecompiler::DescriptorRole::GuestImages; + imageBinding.descriptorSet = 0; + imageBinding.binding = ShaderRecompiler::RuntimeAbi::FirstImageBinding; + imageBinding.count = 1; + imageBinding.imageSamplers = {0}; + imageBinding.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D; + imageBinding.guestDescriptor = {static_cast(address >> 8), static_cast(address >> 40) | (56u << 20) | (3u << 30), 15u | (63u << 14), 0x91b00fac, 0, 0, 0, 0}; + imageProgram.bindings.push_back(imageBinding); + const CompiledShader imageCompute{ShaderRecompiler::ShaderStage::Compute, &imageProgram, 0}; + auto sampled = std::make_shared(imageContext, imageCompute); + recorder.Sync(); + for (int reuse = 0; reuse < 2; ++reuse) { + auto producer = std::make_shared(copiedContext, compute); + writers.push_back(producer); + update(producer, 1); + recorder.OnComplete([producer, &writers] { + producer->WriteBackBuffers(); + std::erase(writers, producer); + }); + if (reuse == 0) sampled = std::make_shared(imageContext, imageCompute); + else Require(sampled->Revalidate(std::span(&imageCompute, 1)), "sampled image template did not revalidate"); + const auto images = sampled->StorageImages(); + Require(images.size() == 1, "sampled image did not use imported texture storage"); + const auto commands = recorder.Commands(); + recorder.Keep(sampled); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {1, 1, 1}; + context.Function("vkCmdCopyImageToBuffer")(commands, images.front().first, VK_IMAGE_LAYOUT_GENERAL, readback.Handle(), 1, ©); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + recorder.Sync(); + std::uint32_t value = 0; + std::memcpy(&value, readback.Bytes().data(), sizeof(value)); + Require(value == static_cast(46 + reuse), "cached sampled image preceded a copied producer"); + } + sampled.reset(); + ClearCachedTextures(context.device); + const auto keys = address + 65536; + const auto keyBytes = DccKeyBytes(65536); + imageProgram.bindings[0].guestDescriptor[6] = (1u << 21) | (static_cast((keys >> 8) & 0xffu) << 24); + imageProgram.bindings[0].guestDescriptor[7] = static_cast(keys >> 16); + auto keyProgram = program; + keyProgram.bindings[0].guestDescriptor = {static_cast(keys), static_cast(keys >> 32u) & 0xffffu, static_cast(keyBytes), 0x31000000u}; + const CompiledShader keyCompute{ShaderRecompiler::ShaderStage::Compute, &keyProgram, 0}; + for (int reuse = 0; reuse < 2; ++reuse) { + std::memset(reinterpret_cast(keys), 0, keyBytes); + AgcDriver::GuestMemory::MarkWritten(keys, keyBytes); + sampled = std::make_shared(imageContext, imageCompute); + recorder.Sync(); + auto producer = std::make_shared(copiedContext, keyCompute); + writers.push_back(producer); + update(producer, 0x40404040, static_cast(keyBytes / 4)); + recorder.OnComplete([producer, &writers] { + producer->WriteBackBuffers(); + std::erase(writers, producer); + }); + if (reuse == 0) sampled = std::make_shared(imageContext, imageCompute); + else Require(sampled->Revalidate(std::span(&imageCompute, 1)), "metadata-only producer invalidated the sampled template"); + const auto images = sampled->StorageImages(); + Require(images.size() == 1, "metadata texture is not resident"); + const auto commands = recorder.Commands(); + recorder.Keep(sampled); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {1, 1, 1}; + context.Function("vkCmdCopyImageToBuffer")(commands, images.front().first, VK_IMAGE_LAYOUT_GENERAL, readback.Handle(), 1, ©); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + recorder.Sync(); + std::uint32_t value = 0; + std::memcpy(&value, readback.Bytes().data(), sizeof(value)); + Require(value == 0xff000000, "cached sampled image ignored copied compression-metadata writes"); + } + } + std::cout << "Copied producers precede fresh and reused direct consumers\n"; +} + void readTrackingTests(const Device& device, Recorder& recorder) { Require(Recorder::ReadTracking(), "read tracking is off (APS5_COPY_READ_TRACKING=0 set?)"); Require(recorder.Idle() && !recorder.PendingReadOverlaps(0x10000, 16), "a fresh recorder reports a read"); @@ -275,6 +792,342 @@ void readTrackingTests(const Device& device, Recorder& recorder) { Require(recorder.Idle() && !recorder.PendingReadOverlaps(0x20000, 0x1000, false), "noted ranges outlived their batch"); } +Recorder::WriteRanges expectedWriteUnion(Recorder::WriteRanges ranges) { + std::sort(ranges.begin(), ranges.end()); + Recorder::WriteRanges result; + for (const auto& [begin, end] : ranges) { + if (!result.empty() && begin <= result.back().second) result.back().second = std::max(result.back().second, end); + else result.emplace_back(begin, end); + } + return result; +} + +void pendingWriteSnapshotTests(const Device& device, Recorder& recorder) { + recorder.Sync(); + Recorder::WriteRanges expected; + const auto check = [&] { + const auto snapshot = Recorder::PendingWriteSnapshot(); + Require(snapshot != nullptr && *snapshot == expectedWriteUnion(expected), "pending write snapshot differs from the full union"); + }; + const auto note = [&](std::uint64_t address, std::size_t bytes) { + const auto previous = Recorder::PendingWriteSnapshot(); + const auto saved = previous == nullptr ? Recorder::WriteRanges{} : *previous; + recorder.NotePendingWrite(address, bytes); + if (bytes != 0) expected.emplace_back(address, address + bytes); + check(); + Require(previous == nullptr || *previous == saved, "publishing a new write modified an older reader's snapshot"); + }; + note(0x10000, 16); + note(0x10020, 16); + note(0x10010, 16); + note(0x10004, 4); + note(0x10000, 0); + note(0, 1); + note(UINT64_MAX - 16, 16); + const Recorder::WriteRanges additions{{0x20020, 0x20040}, {0x20000, 0x20020}, {0x20010, 0x20030}, {0x20040, 0x20040}, {0x20050, 0x20040}}; + recorder.NotePendingWrites(additions); + for (const auto& range : additions) if (range.second > range.first) expected.push_back(range); + check(); + recorder.NotePendingFill(0x30000, 16, 0xA5); + expected.emplace_back(0x30000, 0x30010); + check(); + alignas(64) static std::array labels{}; + const auto labelAddress = reinterpret_cast(labels.data()); + const std::array value{std::byte{1}, std::byte{2}, std::byte{3}, std::byte{4}}; + recorder.NoteLabel(labelAddress, value, 1, 0); + expected.emplace_back(labelAddress, labelAddress + value.size()); + check(); + recorder.Submit(); + device.WaitQueue(); + recorder.AfterCompletions(labelAddress + 8, value, 2, 0, true); + expected.emplace_back(labelAddress + 8, labelAddress + 8 + value.size()); + check(); + note(0x40000, 16); + bool completing = false; + recorder.OnComplete([&] { + const auto snapshot = Recorder::PendingWriteSnapshot(); + Require(Recorder::SnapshotOverlaps(snapshot.get(), 0x40000, 16), "a finishing batch left the snapshot before its completion"); + completing = true; + }); + recorder.Sync(); + Require(completing && Recorder::PendingWriteSnapshot()->empty(), "completed writes remained published"); + std::mt19937_64 random(0x41505335u); + for (int trial = 0; trial < 16; ++trial) { + expected.clear(); + for (int step = 0; step < 64; ++step) note(0x80000 + random() % 4096, 1 + random() % 128); + recorder.Sync(); + Require(Recorder::PendingWriteSnapshot()->empty(), "random writes survived their GPU batch"); + } + expected.clear(); + note(0x90000, 16); + { + std::atomic invalid{false}; + std::atomic ready{0}; + std::atomic reads{0}; + std::array readers; + for (auto& reader : readers) reader = std::jthread([&](std::stop_token stop) { + ready.fetch_add(1, std::memory_order_release); + while (!stop.stop_requested()) { + const auto snapshot = Recorder::PendingWriteSnapshot(); + if (snapshot == nullptr || !Recorder::SnapshotOverlaps(snapshot.get(), 0x90000, 16)) invalid.store(true, std::memory_order_relaxed); + if (snapshot != nullptr) { + for (std::size_t i = 0; i < snapshot->size(); ++i) { + const auto [begin, end] = (*snapshot)[i]; + if (end <= begin || (i != 0 && (*snapshot)[i - 1].second >= begin)) invalid.store(true, std::memory_order_relaxed); + } + } + reads.fetch_add(1, std::memory_order_relaxed); + } + }); + while (ready.load(std::memory_order_acquire) != readers.size()) std::this_thread::yield(); + for (std::uint64_t i = 0; i < 256; ++i) note(0x91000 + i * 128, 32); + for (auto& reader : readers) reader.request_stop(); + for (auto& reader : readers) reader.join(); + Require(reads.load() != 0 && !invalid.load(), "concurrent snapshot readers observed an invalid or incomplete union"); + } + recorder.Sync(); + expected.clear(); + note(UINT64_MAX - 3, 8); + note(0x50000, 16); + recorder.Sync(); + recorder.NotePendingWrite(0x58000, 16); + { + Recorder other(device.GetContext()); + other.Activate(); + recorder.NotePendingWrite(0x59000, 16); + recorder.Activate(); + recorder.NotePendingWrite(0x5A000, 16); + Require(*Recorder::PendingWriteSnapshot() == Recorder::WriteRanges{{0x58000, 0x58010}, {0x59000, 0x59010}, {0x5A000, 0x5A010}}, "reactivation missed writes recorded while inactive"); + other.Activate(); + recorder.Sync(); + recorder.Activate(); + recorder.NotePendingWrite(0x5B000, 16); + Require(*Recorder::PendingWriteSnapshot() == Recorder::WriteRanges{{0x5B000, 0x5B010}}, "reactivation retained writes completed while inactive"); + recorder.Sync(); + other.Activate(); + other.NotePendingWrite(0x60000, 16); + const auto otherSnapshot = Recorder::PendingWriteSnapshot(); + recorder.Activate(); + recorder.NotePendingWrite(0x70000, 16); + const auto ownSnapshot = Recorder::PendingWriteSnapshot(); + Require(ownSnapshot != nullptr && *ownSnapshot == Recorder::WriteRanges{{0x70000, 0x70010}}, "a recorder extended another recorder's snapshot"); + Require(*otherSnapshot == Recorder::WriteRanges{{0x60000, 0x60010}}, "a replaced owner's retained snapshot changed"); + other.Sync(); + Require(Recorder::PendingWriteSnapshot() == ownSnapshot, "an inactive recorder replaced the active snapshot"); + recorder.Sync(); + } + Require(Recorder::PendingWriteSnapshot()->empty(), "snapshot ownership tests left pending writes"); + std::cout << "Pending write snapshot differential, lifetime, completion and ownership tests passed\n"; +} + +void pendingWriteSnapshotBenchmark(Recorder& recorder) { + for (std::uint64_t distinct : {8u, 32u, 512u}) { + for (int duplicates : {0, 8, 64}) { + std::array samples{}; + for (auto& sample : samples) { + recorder.Sync(); + static_cast(recorder.Commands()); + const auto start = std::chrono::steady_clock::now(); + for (std::uint64_t i = 0; i < distinct; ++i) { + const auto address = 0x100000 + i * 4096; + recorder.NotePendingWrite(address, 256); + for (int repeat = 0; repeat < duplicates; ++repeat) recorder.NotePendingWrite(address, 256); + } + sample = std::chrono::duration(std::chrono::steady_clock::now() - start).count(); + const auto snapshot = Recorder::PendingWriteSnapshot(); + Require(snapshot != nullptr && snapshot->size() == distinct, "snapshot benchmark produced another union"); + } + std::sort(samples.begin(), samples.end()); + std::cout << "snapshot-note benchmark: " << distinct << " distinct, " << duplicates << " repeats, median " << samples[2] << " ms\n"; + } + } + recorder.Sync(); +} + +void 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); @@ -338,6 +1191,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; @@ -403,6 +1317,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; @@ -655,6 +1607,65 @@ void keyProofTests(const Device& device, Recorder& recorder) { Require(ProvedClearKeys(resource, surfaceBytes, proof) == DccKeys::Clear0001 && KeyProofCounts().proved == before.proved + 1, "(9) the proof did not hold after the batch finished"); } +void targetKeyProofTests(const Device& device, Recorder& recorder) { + using namespace AgcDriver::GuestMemory; + constexpr std::size_t bytes = 65536; + void* block = AllocateWatched(bytes, bytes); + if (block == nullptr) { + std::cout << "no write watching: target key proofs not tested\n"; + return; + } + struct Release { + void* block; + ~Release() { ReleaseWatched(block, bytes); } + } release{block}; + constexpr std::size_t keyCount = 1024; + constexpr std::uint64_t surfaceBytes = keyCount * 256; + auto* keys = static_cast(block); + std::memset(keys, 0x20, keyCount); + const auto address = reinterpret_cast(block); + const auto scans = [] { return KeyProofCounts().rangeScanned; }; + const auto proofs = [] { return KeyProofCounts().rangeProved; }; + DccRangeProof proof; + auto scanned = scans(); + auto proved = proofs(); + Require(ProvedCurrentDccKeys(address, surfaceBytes, proof) == DccKeys::ClearRegister, "register clear keys read as another code"); + if (!KeyFastPath()) { + Require(proof.generation == 0 && scans() == scanned && proofs() == proved, "target key proofs were kept while disabled"); + std::cout << "key fast path off: every target key read scans\n"; + return; + } + Require(scans() == scanned + 1 && proofs() == proved && proof.generation != 0 && proof.keys == DccKeys::ClearRegister, "the first read of a settled key range left no proof"); + Require(ProvedCurrentDccKeys(address, surfaceBytes, proof) == DccKeys::ClearRegister && scans() == scanned + 1 && proofs() == proved + 1, "an unchanged key range was scanned again"); + MarkWritten(address, keyCount); + scanned = scans(); + proved = proofs(); + Require(ProvedCurrentDccKeys(address, surfaceBytes, proof) == DccKeys::ClearRegister && scans() == scanned + 1 && proofs() == proved, "a key store over the range was answered from the proof"); + Require(ProvedCurrentDccKeys(address, surfaceBytes, proof) == DccKeys::ClearRegister && proofs() == proved + 1, "the rescan after a key store left no proof"); + std::memset(keys, 0xff, keyCount); + scanned = scans(); + Require(ProvedCurrentDccKeys(address, surfaceBytes, proof) == DccKeys::Uncompressed && scans() == scanned + 1, "a CPU write of the keys was not seen"); + scanned = scans(); + proved = proofs(); + Require(ProvedCurrentDccKeys(address, surfaceBytes / 2, proof) == DccKeys::Uncompressed && scans() == scanned + 1 && proofs() == proved, "the proof of another key range answered"); + Require(ProvedCurrentDccKeys(address + 256, surfaceBytes / 2, proof) == DccKeys::Uncompressed && scans() == scanned + 2 && proofs() == proved, "the proof of a key range at another address answered"); + Require(ProvedCurrentDccKeys(address, surfaceBytes, proof) == DccKeys::Uncompressed && scans() == scanned + 3, "a proof taken over a shorter range answered the whole range"); + recorder.NotePendingWrite(address, keyCount); + MarkWritten(address, keyCount); + NoteKeysFillOnGpu(address, keyCount, DccKeys::Clear0000); + scanned = scans(); + proved = proofs(); + Require(ProvedCurrentDccKeys(address, surfaceBytes, proof) == DccKeys::Clear0000 && proof.generation == 0, "a pending key fill was not the answer, or its answer was kept"); + Require(ProvedCurrentDccKeys(address, surfaceBytes, proof) == DccKeys::Clear0000 && scans() == scanned + 2 && proofs() == proved, "a pending key fill was answered from a proof"); + std::memset(keys, 0x00, keyCount); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + Require(ProvedCurrentDccKeys(address, surfaceBytes, proof) == DccKeys::Clear0000 && proof.generation != 0, "the landed fill was not proved"); + proved = proofs(); + Require(ProvedCurrentDccKeys(address, surfaceBytes, proof) == DccKeys::Clear0000 && proofs() == proved + 1, "the proof after the fill landed did not hold"); +} + // (8) The group close against a collect in progress: a CPU store made after the close must refuse // the entry although another thread's resetting walk of the label's page (over a large range) may // absorb the store and stamp the block with the generation it took before the close. And an entry @@ -731,7 +1742,7 @@ void storeRunTests(const Device& device, Recorder& recorder) { const auto address = reinterpret_cast(block); { GuestAllocations::Mutation mutation; - mutation.Add(block, bytes, true, true); + mutation.Add(block, bytes, true, true, true); } const auto* import = HostImportFor(context, address, bytes); if (import == nullptr) { @@ -789,6 +1800,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) { @@ -805,7 +2285,7 @@ void remappedImportTests(const Device& device) { const auto address = reinterpret_cast(block); { GuestAllocations::Mutation mutation; - mutation.Add(block, bytes, true, true); + mutation.Add(block, bytes, true, true, true); } if (HostImportFor(context, address, bytes) == nullptr) { std::cout << "host import of the remap test block refused: remapped imports not tested\n"; @@ -816,7 +2296,7 @@ void remappedImportTests(const Device& device) { { GuestAllocations::Mutation mutation; mutation.Remove(block); - mutation.Add(block, bytes, true, true); + mutation.Add(block, bytes, true, true, true); } Require(HostImportFor(context, address, bytes) != nullptr, "the remapped range was not imported again"); const auto second = HostImportSerial(context, address, bytes, true); @@ -828,6 +2308,474 @@ void remappedImportTests(const Device& device) { HostImportFor(context, address, bytes); } +void hostImportRetirementTests(const Device& device) { + const auto& context = device.GetContext(); + constexpr std::size_t UnitBytes = 65536; + constexpr std::size_t AllocationBytes = 4 * UnitBytes; + constexpr std::size_t CopyBytes = 256; +#ifdef _WIN32 + void* block = VirtualAlloc(nullptr, AllocationBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); +#else + void* block = std::aligned_alloc(UnitBytes, AllocationBytes); +#endif + Require(block != nullptr, "cannot allocate the host import retirement block"); + const auto release = [](void* pointer) { +#ifdef _WIN32 + VirtualFree(pointer, 0, MEM_RELEASE); +#else + std::free(pointer); +#endif + }; + std::unique_ptr allocation(block, release); + auto* memory = static_cast(block); + const auto address = reinterpret_cast(block); + std::unique_lock gpu(GpuMutex()); + PrepareImportWatch(context); + Recorder recorder(context); + recorder.Activate(); + Buffer readback(context, CopyBytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + std::array registered{}; + const auto cleanup = [&] { + if (!gpu.owns_lock()) gpu.lock(); + recorder.Sync(); + recorder.FlushDeferredReleases(); + ClearHostImports(context.device); + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < registered.size(); ++index) { + if (!registered[index]) continue; + mutation.Remove(memory + index * UnitBytes); + registered[index] = false; + } + SetHostImportWindow(0); + }; + const auto add = [&](std::size_t index) { + GuestAllocations::Mutation mutation; + mutation.Add(memory + index * UnitBytes, UnitBytes, true, true, true); + registered[index] = true; + }; + const auto mapped = [&](std::size_t index) { + GuestAllocations::Mapped result; + for (const auto& range : GuestAllocations::GuestAllocationsAcquire_nid_postfix()) { + if (range->address == address + index * UnitBytes) result.push_back(range); + } + Require(result.size() == 1, "host import retirement lost its registered range"); + return result; + }; + bool deferredEveryRetirement = true; + bool preservedEveryRecorder = true; + try { + const std::array shapes{"same-base growth", "earlier-base overlap", "predecessor split"}; + for (std::size_t shape = 0; shape < shapes.size(); ++shape) { + SetHostImportWindow(0); + Require(ImportMappedRanges(context, {}, GuestAllocations::GuestAllocationsGeneration_nid_postfix(), true), "host import retirement could not adopt the device"); + const std::size_t first = shape == 1 ? 1 : 0; + add(first); + if (shape == 2) add(1); + for (std::size_t index = 0; index < AllocationBytes; ++index) memory[index] = static_cast((index * 37u + shape * 19u + 11u) & 0xffu); + const auto initial = mapped(first); + const auto initialGeneration = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + gpu.unlock(); + const bool fresh = ImportMappedRanges(context, initial, initialGeneration, false); + const bool unchanged = ImportMappedRanges(context, initial, initialGeneration, false); + gpu.lock(); + Require(fresh && unchanged, "host import retirement blocked a fresh or unchanged fast import"); + const auto sourceAddress = address + (shape == 2 ? UnitBytes : first * UnitBytes); + const auto* imported = HostImportFor(context, sourceAddress, CopyBytes); + Require(imported != nullptr, "host import retirement could not import its source"); + const auto source = imported->buffer; + const auto sourceOffset = sourceAddress - imported->base; + std::array expected{}; + std::memcpy(expected.data(), reinterpret_cast(sourceAddress), CopyBytes); + recorder.Commands(); + recorder.Submit(); + device.WaitQueue(); + Require(!recorder.Recording() && recorder.InFlightBatches() == 1, "host import retirement requires a submitted batch without an open batch"); + std::size_t changed = 0; + if (shape == 0) { + changed = 1; + add(changed); + } else if (shape == 1) { + add(changed); + } else { + SetHostImportWindow(UnitBytes); + add(3); + changed = 1; + } + const auto ranges = mapped(changed); + const auto generation = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + gpu.unlock(); + const bool deferred = !ImportMappedRanges(context, ranges, generation, false); + gpu.lock(); + const bool preserved = !recorder.Recording() && recorder.InFlightBatches() == 1; + const auto commands = recorder.Commands(); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + CopyBuffer(context, commands, source, sourceOffset, readback.Handle(), 0, CopyBytes); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + Require(ImportMappedRanges(context, ranges, generation, true), "host import retirement refused its retry under the GPU lock"); + Require(HostImportFor(context, sourceAddress, CopyBytes) != nullptr, "host import retirement lost the replacement import"); + recorder.Sync(); + readback.Invalidate(); + Require(std::equal(expected.begin(), expected.end(), readback.Bytes().begin()), "host import retirement changed bytes copied from the retired import"); + std::cout << "host import retirement: " << shapes[shape] << " GPU readback verified; deferred=" << deferred << "; recorder preserved=" << preserved << '\n'; + deferredEveryRetirement = deferredEveryRetirement && deferred; + preservedEveryRecorder = preservedEveryRecorder && preserved; + cleanup(); + } + } catch (...) { + cleanup(); + throw; + } + Require(deferredEveryRetirement, "unlocked mapping retired an import instead of requesting the GPU lock"); + Require(preservedEveryRecorder, "unlocked mapping changed recorder batches"); + std::cout << "Host import retirement locking and GPU readback tests passed\n"; +} +#ifdef _WIN32 +void secondMappingImportTests(const Device& device) { + const auto& context = device.GetContext(); + if (context.hostImportAlignment == 0) { + std::cout << "host imports unavailable: imports after a second mapping not tested\n"; + return; + } + constexpr std::size_t bytes = 65536; + constexpr std::size_t page = 16384; + struct Release { + void* mapping = nullptr; + void* alias = nullptr; + std::int64_t physical = -1; + ~Release() { + if (alias != nullptr) sceKernelMunmap(alias, page); + if (mapping != nullptr) sceKernelMunmap(mapping, bytes); + if (physical >= 0) sceKernelReleaseDirectMemory(physical, bytes); + } + } release; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, bytes, 0, &release.physical) == 0, "allocate the second mapping test backing"); + Require(sceKernelMapDirectMemory(&release.mapping, bytes, 0x33, 0, release.physical, bytes) == 0, "map the second mapping test backing"); + const auto address = reinterpret_cast(release.mapping); + if (HostImportFor(context, address, bytes) == nullptr) { + std::cout << "host import of singly mapped direct memory refused: imports after a second mapping not tested\n"; + return; + } + const auto first = HostImportSerial(context, address, bytes, true); + Require(first != 0 && HostImportSerial(context, address, bytes, true) == first, "singly mapped direct memory lost its import"); + Require(sceKernelMapDirectMemory(&release.alias, page, 0x33, 0, release.physical, page) == 0, "map the first page a second time"); + Require(HostImportFor(context, address, bytes) != nullptr, "direct memory was not imported again after a second mapping of its first page"); + const auto second = HostImportSerial(context, address, bytes, true); + Require(second != 0 && second != first, "the import of direct memory whose pages were replaced was kept"); + HostImportFor(context, address, bytes); +} + +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; + 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.empty(), "the kept side was imported again instead of trimmed after the " + importedAgain + " cuts"); + std::printf("re-import bench: every cut at the head, at the tail and inside the range kept the larger side by trimming the first import\n"); + return 0; +} + +int trimmedImportTests(const Device& device) { + const auto& context = device.GetContext(); + if (context.hostImportAlignment == 0 || context.settleQueue == VK_NULL_HANDLE || !AgcDriver::GuestMemory::WriteWatched()) { + std::cout << "skipped, the trimmed import 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; + bool trimmed; + }; + 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, true}, + {"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, true}, + {"an unmap of a 2 MiB run inside the import", inside, run, true, inside + run, bytes, 0, inside, true}, + {"an unmap of the import's first 2 MiB run", 0, run, true, run, bytes, 0, 0, true}, + {"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, false}, + }}; + 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 trimmed import test backing"); + Require(sceKernelMapDirectMemory(&backing.mapping, bytes, 0x33, 0, backing.physical, run) == 0, "map the trimmed import 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"); + const auto original = whole->memory; + 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"); + const auto keptMemory = kept->memory; + if (test.trimmed) Require(kept->memory == original, "the larger side of " + name + " was imported again instead of kept"); + else Require(kept->memory != original, "the side of " + name + " whose 2 MiB run the unmap replaced is served from the import that pinned the run's old pages"); + 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->memory != original, "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->memory == original, "importing the smaller side of " + name + " dropped 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->memory != keptMemory, "after " + name + " the pages mapped again are served from the import made before they were mapped again"); + 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 << "trimmed import: " << name << ": the larger side was " << (test.trimmed ? "kept" : "imported again") << " and the GPU and the 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 kept side while the import waits trimmed"; + Backing backing; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, run, 0, &backing.physical) == 0, "allocate the trimmed import test backing"); + Require(sceKernelMapDirectMemory(&backing.mapping, bytes, 0x33, 0, backing.physical, run) == 0, "map the trimmed import 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"); + const auto original = whole->memory; + 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->memory != original, "after " + name + " the pages mapped again are served from the import that pinned the old ones"); + cleanup(); + gpu.unlock(); + std::cout << "trimmed import: " << 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 << "Trimmed host import tests passed\n"; + return 0; +} +#endif + void movedMetadataTests(const Device& device, Recorder& recorder) { const auto& base = device.GetContext(); if (base.hostImportAlignment == 0) { @@ -850,7 +2798,7 @@ void movedMetadataTests(const Device& device, Recorder& recorder) { const auto address = reinterpret_cast(block); { GuestAllocations::Mutation mutation; - mutation.Add(block, bytes, true, true); + mutation.Add(block, bytes, true, true, true); } struct Unregister { const Context& context; @@ -943,7 +2891,7 @@ void viewPastLastMipTests(const Device& device, Recorder& recorder) { const auto address = reinterpret_cast(block); { GuestAllocations::Mutation mutation; - mutation.Add(block, bytes, true, true); + mutation.Add(block, bytes, true, true, true); } struct Unregister { const Context& context; @@ -1027,7 +2975,7 @@ void resourceReadTests(const Device& device, Recorder& recorder) { const auto address = reinterpret_cast(block); { GuestAllocations::Mutation mutation; - mutation.Add(block, bytes, true, true); + mutation.Add(block, bytes, true, true, true); } if (HostImportFor(context, address, bytes) == nullptr) { std::cout << "host import of the test block refused: resource read notes not tested\n"; @@ -1078,16 +3026,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); @@ -1113,6 +3064,256 @@ void resourceReadTests(const Device& device, Recorder& recorder) { HostImportFor(context, address, bytes); } +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 false; + } + constexpr std::size_t bytes = 8u << 20u; +#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 test block"); + std::memset(block, 0x11, bytes); + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + if (HostImportFor(context, address, bytes) == nullptr) { + std::cout << "host import of the test block refused: read and written staging not tested\n"; + return false; + } + const auto boundInPlace = [&](std::uint64_t element, std::size_t elementBytes, bool read) { + GuestBufferMemory memory(context); + memory.AllowDeviceStaging(); + memory.AddWritable(element, elementBytes, false, read); + memory.Upload(false); + const auto reads = memory.InPlaceReads(); + const bool direct = reads.size() == 1 && reads[0].first == element && reads[0].second == element + elementBytes; + Require(direct || reads.empty(), "an element was bound neither in place nor staged"); + memory.RecordCopyBacks(recorder); + recorder.Sync(); + return direct; + }; + 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, std::uint32_t stride, std::uint32_t extent) { + const auto element = address + 4096; + 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; + binding.role = ShaderRecompiler::DescriptorRole::GuestBuffers; + binding.descriptorSet = 0; + binding.binding = 0; + binding.count = 1; + 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 + elementBytes; }); + resources.MarkGpuWrites(recorder); + recorder.Sync(); + return direct; + }; + 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) { const auto& context = device.GetContext(); const auto alignment = context.limits.minStorageBufferOffsetAlignment; @@ -1135,7 +3336,7 @@ void misalignedRegionTests(const Device& device, Recorder& recorder) { const auto address = reinterpret_cast(block); { GuestAllocations::Mutation mutation; - mutation.Add(block, bytes, true, true); + mutation.Add(block, bytes, true, true, true); } struct Unregister { const Context& context; @@ -1188,12 +3389,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(); } @@ -1223,7 +3425,7 @@ void misalignedSnapshotTests(const Device& device, Recorder& recorder) { const auto address = reinterpret_cast(block); { GuestAllocations::Mutation mutation; - mutation.Add(block, bytes, true, true); + mutation.Add(block, bytes, true, true, true); } struct Unregister { const Context& context; @@ -1272,9 +3474,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(); @@ -1282,6 +3484,241 @@ void misalignedSnapshotTests(const Device& device, Recorder& recorder) { recorder.Activate(); } +class DescriptorUpdates { +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")) { + Require(active == nullptr, "nested descriptor update capture"); + if (previous != nullptr) functions = *previous; + functions.updateDescriptorSets = captureUpdate; + context.functions = &functions; + active = this; + } + ~DescriptorUpdates() { context.functions = previous; active = nullptr; } + std::vector calls; + std::uint32_t copies = 0; + +private: + static VKAPI_ATTR void VKAPI_CALL captureUpdate(VkDevice device, std::uint32_t count, const VkWriteDescriptorSet* writes, std::uint32_t copyCount, const VkCopyDescriptorSet* copies) { + 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]; + else contents.images[key] = write.pImageInfo[element]; + } + } + active->calls.push_back(std::move(contents)); + active->copies += copyCount; + active->update(device, count, writes, copyCount, copies); + } + Context& context; + const DeviceFunctions* previous; + DeviceFunctions functions{}; + PFN_vkUpdateDescriptorSets update; + static inline DescriptorUpdates* active = nullptr; +}; + +void drawDescriptorWriteTests(const Device& device, Recorder& recorder) { + auto context = device.GetContext(); + DescriptorUpdates updates(context); + { + DescriptorCache cache(context); + context.descriptorCache = &cache; + Recorder snapshots(context); + snapshots.Activate(); + ShaderRecompiler::RecompileResult program; + ShaderRecompiler::DescriptorBinding data{}; + data.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + data.role = ShaderRecompiler::DescriptorRole::FlattenedSrt; + data.binding = 2; + data.count = 1; + data.guestDescriptor = {1, 2, 3, 4}; + program.bindings.push_back(data); + data.binding = 7; + program.bindings.push_back(data); + ShaderRecompiler::DescriptorBinding sampler{}; + sampler.kind = ShaderRecompiler::DescriptorKind::Sampler; + sampler.role = ShaderRecompiler::DescriptorRole::GuestSamplers; + sampler.binding = static_cast(ShaderRecompiler::RuntimeAbi::Binding::Samplers); + sampler.count = 2; + sampler.guestDescriptor.resize(8); + sampler.samplerDepthCompare = {false, false}; + program.bindings.push_back(sampler); + const CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + ShaderResources resources(context, shader); + Require(updates.calls.size() == 1, "building resources did not write one descriptor set"); + 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 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"); + const auto& actual = updates.calls.back(); + Require(actual.buffers.size() == original.buffers.size() && actual.images.size() == original.images.size(), "snapshot update omitted unchanged descriptors"); + for (const auto& [key, before] : original.buffers) { + const auto after = actual.buffers.at(key); + const auto expected = key.first == (allocation == 0 ? 2u : 7u) ? draw->snapshots.front().buffer->Handle() : before.buffer; + Require(after.buffer == expected && after.offset == before.offset && after.range == before.range, "snapshot update replaced the wrong buffer or changed its range"); + } + for (const auto& [key, before] : original.images) { + const auto after = actual.images.at(key); + Require(after.sampler == before.sampler && after.imageView == before.imageView && after.imageLayout == before.imageLayout, "snapshot update changed an image descriptor"); + } + } + snapshots.Sync(); + } + 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; @@ -1298,7 +3735,7 @@ void drawSnapshotReuseTests(const Device& device, Recorder& recorder) { const auto address = reinterpret_cast(block); { GuestAllocations::Mutation mutation; - mutation.Add(block, bytes, true, true); + mutation.Add(block, bytes, true, true, true); } struct Unregister { const Context& context; @@ -1320,7 +3757,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; @@ -1357,6 +3794,14 @@ void drawSnapshotReuseTests(const Device& device, Recorder& recorder) { AgcDriver::GuestMemory::MarkWritten(element, 4); const auto afterStore = snapshot(std::byte{0x33}); Require(afterStore != afterCpu, "a draw snapshot outlived a driver store to its range"); + std::memset(reinterpret_cast(element), 0x33, elementBytes); + Require(snapshot(std::byte{0x33}) == afterStore, "a CPU store of the same bytes made the draw input copy again"); + AgcDriver::GuestMemory::MarkWritten(element, 4); + Require(snapshot(std::byte{0x33}) == afterStore, "a driver store of the same bytes made the draw input copy again"); + reinterpret_cast(element)[elementBytes - 1] = 0x34; + const auto tail = resources.PrepareDrawBindings(snapshotRecorder); + Require(tail != nullptr && tail->snapshots.size() == 1 && tail->snapshots[0].buffer != afterStore && tail->snapshots[0].buffer->Bytes()[elementBytes - 1] == std::byte{0x34}, "a draw snapshot outlived a store that changed its last byte"); + std::memset(reinterpret_cast(element), 0x33, elementBytes); { GuestAllocations::Mutation mutation; } @@ -1366,6 +3811,348 @@ void drawSnapshotReuseTests(const Device& device, Recorder& recorder) { recorder.Activate(); } +void drawSnapshotPatchTests(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 or write watching unavailable: snapshot patching 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 * 13u + 5u); + 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: snapshot patching not tested\n"; + return; + } + if (!AgcDriver::GuestMemory::Watched(address, bytes)) { + std::cout << "host imports are compared, not watched: snapshot patching not tested\n"; + return; + } + constexpr std::size_t elementOffset = 4096; + constexpr std::size_t elementBytes = 3 * 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; + }; + const auto settle = [&](const std::weak_ptr& buffer) { + snapshotRecorder.Sync(); + gpu.unlock(); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (buffer.use_count() > 1 && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + gpu.lock(); + Require(buffer.use_count() == 1, "a finished draw still holds its snapshot"); + }; + std::weak_ptr first = snapshot(); + settle(first); + guest[elementOffset + 10] ^= 0xffu; + guest[elementOffset + 65536 + 300] ^= 0xffu; + guest[elementOffset + 2 * 65536 + 7] = guest[elementOffset + 2 * 65536 + 7]; + guest[elementOffset + elementBytes - 1] ^= 0x5au; + AgcDriver::GuestMemory::BumpCollectEpoch(); + { + const auto patched = snapshot(); + Require(patched == first.lock(), "a stale snapshot nothing else holds was copied whole instead of patched"); + } + settle(first); + std::memset(guest + elementOffset + 65536, 0x77, 65536); + AgcDriver::GuestMemory::BumpCollectEpoch(); + { + const auto patched = snapshot(); + Require(patched == first.lock(), "a snapshot whose whole middle block changed was not patched"); + } + settle(first); + const auto held = snapshot(); + Require(held == first.lock(), "an unchanged snapshot was not reused"); + const std::vector before(held->Bytes().begin(), held->Bytes().end()); + guest[elementOffset + 2 * 65536 + 100] ^= 0xffu; + AgcDriver::GuestMemory::BumpCollectEpoch(); + const auto fresh = snapshot(); + Require(fresh != held, "a snapshot a recorded draw still holds was patched"); + Require(std::memcmp(held->Bytes().data(), before.data(), before.size()) == 0, "a held snapshot's bytes changed"); + snapshotRecorder.Sync(); + } +} + +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(); + const char* previous = std::getenv("APS5_DRAW_SNAPSHOT_MAX_MIB"); + const std::string saved = previous != nullptr ? previous : ""; +#ifdef _WIN32 + Require(_putenv_s("APS5_DRAW_SNAPSHOT_MAX_MIB", "1") == 0, "cannot set snapshot cap"); +#else + Require(setenv("APS5_DRAW_SNAPSHOT_MAX_MIB", "1", 1) == 0, "cannot set snapshot cap"); +#endif + struct RestoreCap { + const char* previous; + std::string saved; + ~RestoreCap() { +#ifdef _WIN32 + _putenv_s("APS5_DRAW_SNAPSHOT_MAX_MIB", saved.c_str()); +#else + if (previous != nullptr) { + setenv("APS5_DRAW_SNAPSHOT_MAX_MIB", saved.c_str(), 1); + } else { + unsetenv("APS5_DRAW_SNAPSHOT_MAX_MIB"); + } +#endif + } + } restore{previous, saved}; + constexpr std::size_t bytes = 3 * 1024 * 1024; + void* block = context.hostImportAlignment != 0 ? AllocateWatched(bytes, 65536) : nullptr; + if (block == nullptr) { + std::cout << "host imports or write watching unavailable: draw snapshot cap not tested\n"; + return; + } + 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; + ~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 cap not tested\n"; + return; + } + const auto smallAddress = address + 4096; + constexpr std::size_t smallBytes = 1024 * 1024 - 4096; + const auto largeAddress = address + 1024 * 1024 + 8192; + constexpr std::size_t largeBytes = 1024 * 1024 + 4096; + ShaderRecompiler::RecompileResult program; + ShaderRecompiler::DescriptorBinding smallBinding; + smallBinding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + smallBinding.role = ShaderRecompiler::DescriptorRole::GuestBuffers; + smallBinding.descriptorSet = 0; + smallBinding.binding = 0; + smallBinding.count = 1; + smallBinding.guestDescriptor = {static_cast(smallAddress), static_cast(smallAddress >> 32u) & 0xffffu, static_cast(smallBytes), 0x31000000u}; + smallBinding.bufferWritten = {false}; + program.bindings.push_back(smallBinding); + ShaderRecompiler::DescriptorBinding largeBinding; + largeBinding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + largeBinding.role = ShaderRecompiler::DescriptorRole::GuestBuffers; + largeBinding.descriptorSet = 0; + largeBinding.binding = 1; + largeBinding.count = 1; + largeBinding.guestDescriptor = {static_cast(largeAddress), static_cast(largeAddress >> 32u) & 0xffffu, static_cast(largeBytes), 0x31000000u}; + largeBinding.bufferWritten = {false}; + program.bindings.push_back(largeBinding); + 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 bindings = resources.PrepareDrawBindings(snapshotRecorder); + Require(bindings != nullptr, "read-only draw bindings were not prepared"); + Require(bindings->snapshots.size() == 1, "an input exceeding the draw snapshot cap was snapshotted"); + Require(bindings->snapshots[0].address == smallAddress, "the snapshotted input does not match the smaller buffer below the cap"); + Require(snapshotRecorder.ReusableDrawSnapshot(largeAddress, largeBytes) == nullptr, "an input exceeding the draw snapshot cap was kept in the snapshot cache"); + if (Watched(address, bytes)) { + Require(snapshotRecorder.ReusableDrawSnapshot(smallAddress, smallBytes) != nullptr, "an input below the draw snapshot cap was not kept in the snapshot cache"); + } + const auto contents = bindings->snapshots[0].buffer->Bytes(); + Require(contents.size() == smallBytes && std::all_of(contents.begin(), contents.end(), [](std::byte value) { return value == std::byte{0x11}; }), "a draw snapshot does not hold the guest bytes of its draw"); + std::memset(reinterpret_cast(smallAddress), 0x22, smallBytes); + std::memset(reinterpret_cast(largeAddress), 0x33, largeBytes); + Require(std::all_of(contents.begin(), contents.end(), [](std::byte value) { return value == std::byte{0x11}; }), "snapshotted input observed CPU overwrite"); + const auto* inPlace = reinterpret_cast(largeAddress); + Require(inPlace[0] == std::byte{0x33} && std::all_of(inPlace, inPlace + largeBytes, [](std::byte value) { return value == std::byte{0x33}; }), "in-place bound input did not observe CPU write"); + std::memset(block, 0x11, bytes); + snapshotRecorder.Sync(); +#ifdef _WIN32 + Require(_putenv_s("APS5_DRAW_SNAPSHOT_MAX_MIB", "64") == 0, "cannot raise snapshot cap"); +#else + Require(setenv("APS5_DRAW_SNAPSHOT_MAX_MIB", "64", 1) == 0, "cannot raise snapshot cap"); +#endif + const auto again = resources.PrepareDrawBindings(snapshotRecorder); + Require(again != nullptr && again->snapshots.size() == 1 && again->snapshots[0].address == smallAddress, "built resources read the draw snapshot cap again instead of keeping the one they were built with"); + snapshotRecorder.Sync(); +#ifdef _WIN32 + Require(_putenv_s("APS5_DRAW_SNAPSHOT_MAX_MIB", "1") == 0, "cannot restore snapshot cap"); +#else + Require(setenv("APS5_DRAW_SNAPSHOT_MAX_MIB", "1", 1) == 0, "cannot restore snapshot cap"); +#endif + } + const auto firstAddress = address + 4096; + constexpr std::size_t firstBytes = 600 * 1024; + const auto secondAddress = address + 600 * 1024 + 8192; + constexpr std::size_t secondBytes = 600 * 1024; + ShaderRecompiler::RecompileResult cumulativeProgram; + ShaderRecompiler::DescriptorBinding firstBinding; + firstBinding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + firstBinding.role = ShaderRecompiler::DescriptorRole::GuestBuffers; + firstBinding.descriptorSet = 0; + firstBinding.binding = 0; + firstBinding.count = 1; + firstBinding.guestDescriptor = {static_cast(firstAddress), static_cast(firstAddress >> 32u) & 0xffffu, static_cast(firstBytes), 0x31000000u}; + firstBinding.bufferWritten = {false}; + cumulativeProgram.bindings.push_back(firstBinding); + ShaderRecompiler::DescriptorBinding secondBinding; + secondBinding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + secondBinding.role = ShaderRecompiler::DescriptorRole::GuestBuffers; + secondBinding.descriptorSet = 0; + secondBinding.binding = 1; + secondBinding.count = 1; + secondBinding.guestDescriptor = {static_cast(secondAddress), static_cast(secondAddress >> 32u) & 0xffffu, static_cast(secondBytes), 0x31000000u}; + secondBinding.bufferWritten = {false}; + cumulativeProgram.bindings.push_back(secondBinding); + const CompiledShader cumulativeCompute{ShaderRecompiler::ShaderStage::Compute, &cumulativeProgram, 0}; + { + auto snapshotContext = context; + DescriptorCache cache(snapshotContext); + snapshotContext.descriptorCache = &cache; + Recorder snapshotRecorder(snapshotContext); + snapshotRecorder.Activate(); + ShaderResources resources(snapshotContext, cumulativeCompute); + const auto bindings = resources.PrepareDrawBindings(snapshotRecorder); + Require(bindings != nullptr, "read-only draw bindings were not prepared for cumulative test"); + Require(bindings->snapshots.size() == 1, "cumulative cap permitted both inputs to snapshot"); + Require(bindings->snapshots[0].address == firstAddress, "the earlier binding in the layout was not prioritised for the snapshot budget"); + snapshotRecorder.Sync(); + } + recorder.Activate(); +} + void drawSnapshotEvictionTests(const Device& device) { using namespace AgcDriver::GuestMemory; constexpr std::size_t bytes = 65536; @@ -1404,6 +4191,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; @@ -1450,7 +4348,7 @@ void drawInputReuseTests(const Device& device, Recorder& recorder) { alignas(64) static std::byte other[64]; { GuestAllocations::Mutation mutation; - mutation.Add(other, sizeof(other), true, true); + mutation.Add(other, sizeof(other), true, true, true); } { GuestAllocations::Mutation mutation; @@ -1480,22 +4378,130 @@ void drawInputReuseTests(const Device& device, Recorder& recorder) { recorder.Sync(); } +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; + if (block == nullptr) { + std::cout << "host imports or write watching unavailable: in-place draw inputs not tested\n"; + return; + } + struct Release { + void* block; + ~Release() { ReleaseWatched(block, bytes); } + } release{block}; + 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: in-place draw inputs not tested\n"; + return; + } + 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); + Require(read.has_value() && read->kind == Recorder::ReadKind::DrawInput && read->open, "an in-place draw input's read was not noted on the open batch"); + Require(InPlaceDrawInput(context, vertices + 2, 256, 4) == nullptr, "a draw input misaligned for its format was bound in place"); + 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"); + auto unrecorded = context; + unrecorded.recorder = nullptr; + Require(InPlaceDrawInput(unrecorded, vertices, 256, 4) == nullptr, "a draw without a recorder bound its input in place"); + recorder.Sync(); + Require(!recorder.PendingReadOverlaps(vertices, 256), "an in-place draw input's read outlived its batch"); + if (!AgcDriver::GuestMemory::Watched(address, bytes)) { + std::cout << "host imports are compared, not watched: kept highest indices not tested\n"; + return; + } + const auto indices = address + 20480; + constexpr std::size_t kept = Recorder::KeptIndexRangeIndices * 2; + std::vector first(Recorder::KeptIndexRangeIndices * 2, 1); + first[0] = 3; + first[1] = 9; + first[2] = 0xffff; + std::memcpy(reinterpret_cast(indices), first.data(), kept * 2); + auto range = InPlaceIndexRange(recorder, indices, kept, 2, false); + Require(!range.reused && range.highest == 0xffffu, "an in-place index range was not scanned at its first use"); + range = InPlaceIndexRange(recorder, indices, kept, 2, false); + Require(range.reused && range.highest == 0xffffu, "an unchanged in-place index range was scanned again"); + range = InPlaceIndexRange(recorder, indices, kept, 2, true); + Require(!range.reused && range.highest == 9u, "an in-place index range served a scan made under the other restart rule"); + Require(!InPlaceIndexRange(recorder, indices, kept + 2, 2, true).reused, "an in-place index range served a scan of another size"); + const std::uint16_t raised = 12; + std::memcpy(reinterpret_cast(indices + 2), &raised, sizeof(raised)); + range = InPlaceIndexRange(recorder, indices, kept, 2, true); + Require(!range.reused && range.highest == 12u, "an in-place index range kept its highest index over a guest write"); + range = InPlaceIndexRange(recorder, indices, kept, 2, true); + Require(range.reused && range.highest == 12u, "a rescanned in-place index range was not kept"); + range = InPlaceIndexRange(recorder, indices, kept - 2, 2, true); + Require(!range.reused && range.highest == 12u, "an in-place index range below the kept size was not scanned"); + range = InPlaceIndexRange(recorder, indices, kept - 2, 2, true); + Require(!range.reused && range.highest == 12u, "an in-place index range below the kept size was kept"); + range = InPlaceIndexRange(recorder, indices, kept * 2 - 4, 4, false); + Require(!range.reused && range.highest == 0x000c0003u, "a 32-bit in-place index range below the kept size was not scanned"); + Require(!InPlaceIndexRange(recorder, indices, kept * 2 - 4, 4, false).reused, "a 32-bit in-place index range below the kept size was kept"); + Require(!InPlaceIndexRange(recorder, indices, kept * 2, 4, false).reused, "a 32-bit in-place index range was not scanned at its first use"); + range = InPlaceIndexRange(recorder, indices, kept * 2, 4, false); + Require(range.reused && range.highest == 0x000c0003u, "an unchanged 32-bit in-place index range was scanned again"); + const std::vector restarts(Recorder::KeptIndexRangeIndices, 0xffff); + std::memcpy(reinterpret_cast(indices + 20480), restarts.data(), kept); + Require(!InPlaceIndexRange(recorder, indices + 20480, kept, 2, true).highest.has_value(), "an index range of restart indices has a highest index"); + range = InPlaceIndexRange(recorder, indices + 20480, kept, 2, true); + Require(range.reused && !range.highest.has_value(), "an empty in-place index range was scanned again"); +} + // Unit shadows (UnitShadow.hpp) over a host import of write-watched arena memory: a retile piece's // slab destination and its seeds, freshness from the tracker (a publish never stamps, a CPU write // 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. @@ -1510,7 +4516,7 @@ void unitShadowTests(const Device& device, Recorder& recorder) { const auto address = reinterpret_cast(block); { GuestAllocations::Mutation mutation; - mutation.Add(block, bytes, true, true); + mutation.Add(block, bytes, true, true, true); } struct Unregister { const Context& context; @@ -1529,11 +4535,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); @@ -1558,7 +4564,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); @@ -1654,25 +4660,21 @@ void unitShadowTests(const Device& device, Recorder& recorder) { retile(*high, unit128, unit, 0x77, false); Require(AnyShadowedOverlaps(unit128, unit), "the second slab's unit is not shadowed"); high.reset(); - // Retire through the registry: the block is re-registered as its first five units, so the next - // lookup reconciles and retires the import; the fresh units still inside a registered range - // are published into its (kept) buffer, the rest (memory the title took back) are dropped. - // Under the one-slab budget unit 5's slab evicts the second one first (unit 128 published). retile(*ShadowDestinationFor(context, *import, unit5, unit5 + unit), unit5, unit, 0x88, false); retile(*ShadowDestinationFor(context, *import, address + 2 * unit, unit3), address + 2 * unit, unit, 0x99, false); Require(AnyShadowedOverlaps(unit5, unit) && AnyShadowedOverlaps(address + 2 * unit, unit), "units 2 and 5 are not shadowed before the retire"); { GuestAllocations::Mutation mutation; mutation.Remove(block); - mutation.Add(block, static_cast(5 * unit), true, true); + mutation.Add(block, static_cast(5 * unit), true, true, true); } Require(HostImportFor(context, address, bytes) == nullptr, "a shrunk registration still imports the old range"); Require(!AnyShadowedOverlaps(address, bytes), "the retired import's shadow survived"); recorder.Sync(); Require(words[2 * unit] == 0x99 && words[3 * unit - 1] == 0x99, "the retire did not publish the unit still registered"); - Require(words[5 * unit] == 0x11 && words[5 * unit + unit - 1] == 0x11, "the retire published a unit whose memory is no longer registered"); - if (budgetMiB >= 16) Require(words[127 * unit] == 0x11 && words[128 * unit] == 0x11, "the retire published the second slab's units outside the registration"); - else Require(words[128 * unit] == 0x77, "the eviction before the retire did not publish unit 128"); + 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 storageRefreshTests(const Device& device, Recorder& recorder, bool watched) { @@ -1707,7 +4709,7 @@ void storageRefreshTests(const Device& device, Recorder& recorder, bool watched) const auto address = reinterpret_cast(block); { GuestAllocations::Mutation mutation; - mutation.Add(block, bytes, true, true); + mutation.Add(block, bytes, true, true, true); } struct Unregister { const Context& context; @@ -1750,7 +4752,7 @@ void storageRefreshTests(const Device& device, Recorder& recorder, bool watched) DccKeyProof proof; Require(ProvedClearKeys(resource, surfaceBytes, proof) == DccKeys::Clear0000, "0x00 keys are not a 0000 clear"); { - auto image = std::make_shared(context, detiler, resource, 0); + auto image = StorageTexture::Create(context, detiler, resource, 0); const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; const auto draw = [&](VkClearColorValue value) { const auto commands = recorder.Commands(); @@ -1817,7 +4819,16 @@ void storageRefreshTests(const Device& device, Recorder& recorder, bool watched) draw({{1.0f, 0.0f, 0.0f, 1.0f}}); image->Refresh(); Require(holds({255, 0, 0, 255}), "a refresh under the image's own clear keys dropped the draw's results"); - Require(memoryHolds({255, 0, 0, 255}), "the refresh's store of the results did not reach guest memory"); + if (watched) { + Require(memoryHolds({255, 0, 0, 255}), "the refresh's store of the results did not reach watched guest memory"); + } else { + Require(StorageTexture::FindPending(address, surfaceBytes) == image, "an unchanged untracked refresh discarded the pending image"); + Require(StorageTexture::FlushPending(address, surfaceBytes, nullptr, "storage refresh visibility test"), "the pending untracked image was not flushed"); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + Require(memoryHolds({255, 0, 0, 255}), "an explicit flush did not make pending results visible in untracked guest memory"); + } if (watched) { draw({{0.0f, 0.0f, 1.0f, 1.0f}}); std::memset(texels, 0x33, 65536); @@ -1836,12 +4847,273 @@ void storageRefreshTests(const Device& device, Recorder& recorder, bool watched) recorder.Sync(); } +bool writtenBufferOrderTests(const Device& device, Recorder& recorder) { + const auto& base = device.GetContext(); + if (base.hostImportAlignment == 0) { + std::cout << "host imports unavailable: written buffer order not tested\n"; + return false; + } + constexpr std::uint32_t side = 256; + constexpr std::size_t bytes = side * side * 4; + void* block = AllocateWatched(bytes, 65536); + if (block == nullptr) { + std::cout << "no write watching: written buffer order not tested\n"; + return false; + } + std::memset(block, 0x55, bytes); + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + std::uint64_t address; + ~Unregister() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, address, bytes); + } + } unregister{base, block, address}; + if (HostImportFor(base, address, bytes) == nullptr) { + std::cout << "host import of the written buffer order block refused: written buffer order not tested\n"; + return false; + } + std::cout << "written buffer order: host imports are " << (AgcDriver::GuestMemory::Watched(address, bytes) ? "watched" : "compared") << "\n"; + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + GuestTextureResource resource{}; + resource.baseAddress = address; + resource.width = side; + resource.height = side; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kR64KBX; + resource.dimension = TextureDimension::k2D; + resource.format = 56; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + Require(DescribeSurface(resource).guestBytes == bytes, "the written buffer order surface has an unexpected size"); + const auto settle = [&] { + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + }; + { + auto image = StorageTexture::Create(context, detiler, resource, 0); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + const VkClearColorValue red{{1.0f, 0.0f, 0.0f, 1.0f}}; + { + const auto commands = recorder.Commands(); + recorder.Keep(image); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function("vkCmdClearColorImage")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, &red, 1, &range); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + image->MarkDirty(); + } + constexpr std::size_t written = 40800; + GuestBufferMemory memory(context); + memory.AcquireRegistered(); + memory.AddWritable(address, written, false, false); + memory.Upload(true); + std::uint32_t adjustment = 0; + const auto view = memory.Descriptor(address, written, adjustment); + Require(adjustment == 0, "the written buffer view is not aligned to its element"); + { + const auto commands = recorder.Commands(); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function("vkCmdFillBuffer")(commands, view.buffer, view.offset, written, 0xa5a5a5a5u); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + } + memory.RecordCopyBacks(recorder); + memory.MarkDirectWrites(); + settle(); + StorageTexture::FlushAllPending("written buffer order test"); + settle(); + std::vector read(bytes); + AgcDriver::GuestMemory::Read(address, read); + Require(std::all_of(read.begin(), read.begin() + written, [](std::byte value) { return value == std::byte{0xa5}; }), "storage results pending under a buffer a later use wrote were stored over the buffer's contents"); + for (std::size_t i = written; i < bytes; ++i) Require(std::to_integer(read[i]) == (i % 4 == 1 || i % 4 == 2 ? 0 : 255), "the storage results outside the written buffer did not reach guest memory"); + } + recorder.Sync(); + return true; +} + +void hostImportUnmapTests(const Device& device, Recorder& recorder) { + const auto& context = device.GetContext(); + if (context.hostImportAlignment == 0) { + std::cout << "host imports unavailable: import unmap lifetime not tested\n"; + return; + } + const auto alignment = static_cast(std::max(65536u, context.hostImportAlignment)); + constexpr std::size_t RequestedBytes = 17u << 20u; + const auto bytes = ((RequestedBytes + alignment - 1u) / alignment) * alignment; +#ifdef _WIN32 + void* block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); +#else + void* block = std::aligned_alloc(alignment, bytes); +#endif + Require(block != nullptr, "cannot allocate the host import unmap block"); + struct Block { + void* address; + std::size_t bytes; + bool registered = true; + bool released = false; + void Release() { +#ifdef _WIN32 + Require(VirtualFree(address, 0, MEM_RELEASE) != 0, "cannot release the host import unmap block"); +#else + std::free(address); +#endif + released = true; + } + ~Block() { + if (registered) { + try { + GuestAllocations::Mutation mutation; + mutation.Remove(address); + } catch (...) { + return; + } + } + if (!released) { +#ifdef _WIN32 + VirtualFree(address, 0, MEM_RELEASE); +#else + std::free(address); +#endif + } + } + } allocation{block, bytes}; + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + const HostImport* imported = nullptr; + { + std::lock_guard gpu(GpuMutex()); + imported = HostImportFor(context, address, bytes); + } + if (imported == nullptr) { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + allocation.registered = false; + allocation.Release(); + std::cout << "host import refused: import unmap lifetime not tested\n"; + return; + } + bool retiredBeforeRelease = false; + { + GuestAllocations::Mutation mutation; + mutation.Unmap(block, bytes, [&](const void*, std::size_t, const void*, bool) { + retiredBeforeRelease = !HostImportCovers(context, address, bytes); + if (retiredBeforeRelease) allocation.Release(); + }); + allocation.registered = false; + } + if (!retiredBeforeRelease) { + { + std::lock_guard gpu(GpuMutex()); + static_cast(HostImportFor(context, address, bytes)); + recorder.Sync(); + } + allocation.Release(); + } + Require(retiredBeforeRelease, "guest unmap released pages before retiring their host import"); + std::cout << "host import unmap lifetime: ok\n"; +} + +void parkedImportUnmapTests(const Device& device) { + const auto& context = device.GetContext(); + if (context.hostImportAlignment == 0) { + std::cout << "host imports unavailable: parked import unmap not tested\n"; + return; + } + const auto alignment = static_cast(std::max(65536u, context.hostImportAlignment)); + const auto bytes = alignment * 4u; +#ifdef _WIN32 + void* block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); +#else + void* block = std::aligned_alloc(alignment, bytes); +#endif + Require(block != nullptr, "cannot allocate the parked import unmap block"); + const auto release = [&] { +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + }; + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + std::unique_ptr replacement; + bool imported = false; + { + std::lock_guard gpu(GpuMutex()); + imported = HostImportFor(context, address, bytes) != nullptr; + } + if (imported) { + replacement = std::make_unique(); + std::lock_guard gpu(GpuMutex()); + imported = HostImportFor(replacement->GetContext(), address, bytes) != nullptr; + } + if (!imported) { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + if (replacement) ClearHostImports(replacement->GetContext().device); + release(); + std::cout << "host import refused: parked import unmap not tested\n"; + return; + } + bool unmapped = false; + try { + GuestAllocations::Mutation mutation; + mutation.Unmap(block, bytes, [&](const void*, std::size_t, const void*, bool) { unmapped = true; }); + } catch (const std::exception& error) { + ClearHostImports(replacement->GetContext().device); + ClearHostImports(context.device); + GuestAllocations::Mutation mutation; + mutation.Remove(block); + release(); + throw std::runtime_error(std::string("guest unmap after a device replacement: ") + error.what()); + } + ClearHostImports(replacement->GetContext().device); + ClearImageMirrors(replacement->GetContext().device); + replacement.reset(); + release(); + Require(unmapped, "guest unmap of a range a replaced device imported did not run"); + std::cout << "parked import unmap: ok\n"; +} + void importWatchTests(const Device& device) { using namespace AgcDriver::GuestMemory; 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"; @@ -1849,9 +5121,13 @@ void importWatchTests(const Device& device) { } const auto probe = ProbeImportWriteProtection(context); 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.writtenAtImport << " after the import\n"; + 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"); @@ -1874,7 +5150,7 @@ void importWatchTests(const Device& device) { }; const auto registerRange = [](void* block, std::size_t bytes) { GuestAllocations::Mutation mutation; - mutation.Add(block, bytes, true, true); + mutation.Add(block, bytes, true, true, true); }; const auto unregisterRange = [](void* block) { GuestAllocations::Mutation mutation; @@ -1978,7 +5254,7 @@ void staleGenerationTests(const Device& device, Recorder& recorder) { const auto address = reinterpret_cast(block); { GuestAllocations::Mutation mutation; - mutation.Add(block, bytes, true, true); + mutation.Add(block, bytes, true, true, true); } struct Unregister { const Context& context; @@ -2009,11 +5285,12 @@ void staleGenerationTests(const Device& device, Recorder& recorder) { resource.dstSelW = 7; resource.dccAddress = address + surfaceBytes; { - auto image = std::make_shared(context, detiler, resource, 0); - Require(AgcDriver::GuestMemory::Watched(address, bytes), "(s) the storage image imported its memory before a generation was taken"); + Require(AgcDriver::GuestMemory::Watched(address, bytes), "(s) the block is not watched before the storage image takes its generation"); + 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); + Require(image->Generation() != 0, "(s) the storage image took no generation while its memory was watched"); const VkClearColorValue red{{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, &red, 1, &range); @@ -2038,6 +5315,11 @@ void staleGenerationTests(const Device& device, Recorder& recorder) { recorder.Submit(); device.WaitQueue(); recorder.Sync(); + Require(StorageTexture::FindPending(address, surfaceBytes) == image, "(s) the image is not pending after the refresh"); + Require(StorageTexture::FlushPending(address, surfaceBytes, nullptr, "stale generation test"), "(s) the pending image was not flushed"); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); const auto pixels = readback.Bytes(); constexpr std::array expected{255, 0, 0, 255}; for (std::size_t i = 0; i < pixels.size(); ++i) Require(std::to_integer(pixels[i]) == expected[i % 4], "(s) results pending when their memory left the watch were dropped"); @@ -2068,17 +5350,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; @@ -2120,7 +5406,7 @@ void writeBackPaddingTests(const Device& device, Recorder& recorder, bool watche const auto address = blockAddress + test.offset; { GuestAllocations::Mutation mutation; - mutation.Add(block, bytes, true, true); + mutation.Add(block, bytes, true, true, true); } struct Unregister { const Context& context; @@ -2146,7 +5432,149 @@ void writeBackPaddingTests(const Device& device, Recorder& recorder, bool watche context.detiler = &detiler; resource.baseAddress = address; { - auto image = std::make_shared(context, detiler, resource, 0); + 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}; const auto commands = recorder.Commands(); recorder.Keep(image); @@ -2156,32 +5584,946 @@ 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) { + const auto& base = device.GetContext(); + if (base.hostImportAlignment == 0 || !base.singlePassStorage) { + std::cout << "host imports or storage access without a format unavailable: single-pass storage moves not tested\n"; + return; + } + struct Case { + const char* name; + std::uint32_t format; + TextureTileMode tileMode; + std::uint32_t width; + std::uint32_t height; + std::uint32_t mipCount; + std::uint32_t pipeBankXor = 0; + }; + constexpr Case cases[] = { + {"R8 64 KiB R_X 200x150", 1, TextureTileMode::kR64KBX, 200, 150, 1}, + {"R16 64 KiB R_X 200x150, 3 mips", 7, TextureTileMode::kR64KBX, 200, 150, 3}, + {"RGBA8 64 KiB R_X 520x260, 6 mips", 56, TextureTileMode::kR64KBX, 520, 260, 6}, + {"RGBA16F 64 KiB R_X 384x256", 71, TextureTileMode::kR64KBX, 384, 256, 1}, + {"RGBA16F 64 KiB R_X 256x200, 4 mips", 71, TextureTileMode::kR64KBX, 256, 200, 4}, + {"RGBA32F 64 KiB R_X 136x72, 2 mips", 77, TextureTileMode::kR64KBX, 136, 72, 2}, + {"RGBA8 64 KiB S 300x170", 56, TextureTileMode::kStandard64KB, 300, 170, 1}, + {"RGBA16F 4 KiB S 100x70, 2 mips", 71, TextureTileMode::kStandard4KB, 100, 70, 2}, + {"R8 256 B S 90x40", 1, TextureTileMode::kStandard256B, 90, 40, 1}, + {"RGBA8 64 KiB R_X 520x260, 2 mips, pipe/bank XOR 5", 56, TextureTileMode::kR64KBX, 520, 260, 2, 5}, + }; + std::string failures; + std::size_t tested = 0; + for (const auto& test : cases) { + if (!StorageFormatAvailable(base, test.format)) continue; + const std::string what = std::string(watched ? "(watched) " : "") + test.name; + GuestTextureResource resource{}; + resource.width = test.width; + resource.height = test.height; + resource.mipCount = test.mipCount; + resource.tileMode = test.tileMode; + resource.dimension = TextureDimension::k2D; + resource.format = test.format; + resource.pipeBankXor = test.pipeBankXor; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + const auto geometry = DescribeSurface(resource); + const auto elementBytes = BytesPerElement(test.format); + const auto bytes = (static_cast(geometry.guestBytes) + 65535) / 65536 * 65536 + 65536; + struct Result { + std::vector uploaded, memory, reuploaded; + std::uint64_t moves = 0; + }; + std::array results; + bool skipped = false; + for (int mode = 0; mode < 2 && !skipped; ++mode) { + auto context = base; + context.singlePassStorage = mode == 1; + void* block = nullptr; + if (watched) { + block = AllocateWatched(bytes, 65536); + } else { +#ifdef _WIN32 + block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); +#else + block = std::aligned_alloc(65536, bytes); +#endif + } + if (block == nullptr) { + std::cout << "no write watching: single-pass storage moves in watched memory not tested\n"; + return; + } + auto* memory = static_cast(block); + std::uint32_t seed = 0x13579bdfu; + const auto random = [&] { + seed ^= seed << 13; + seed ^= seed >> 17; + seed ^= seed << 5; + return static_cast(seed >> 9); + }; + for (std::size_t i = 0; i < bytes; ++i) memory[i] = random(); + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + std::uint64_t address; + std::size_t bytes; + bool watched; + ~Unregister() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, address, bytes); + if (watched) ReleaseWatched(block, bytes); +#ifdef _WIN32 + else VirtualFree(block, 0, MEM_RELEASE); +#else + else std::free(block); +#endif + } + } unregister{base, block, address, bytes, watched}; + if (HostImportFor(base, address, bytes) == nullptr) { + std::cout << "host import refused: single-pass storage moves not tested\n"; + return; + } + TextureDetiler detiler(base); + context.detiler = &detiler; + resource.baseAddress = address; + std::size_t linearBytes = 0; + for (const auto& mip : geometry.mips) linearBytes += static_cast(mip.width) * mip.height * elementBytes; + const auto readImage = [&](const StorageTexture& image) { + Buffer readback(context, linearBytes, 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); + std::vector copies; + std::size_t at = 0; + for (std::uint32_t level = 0; level < geometry.mips.size(); ++level) { + VkBufferImageCopy copy{}; + copy.bufferOffset = at; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level, 0, 1}; + copy.imageExtent = {geometry.mips[level].width, geometry.mips[level].height, 1}; + copies.push_back(copy); + at += static_cast(geometry.mips[level].width) * geometry.mips[level].height * elementBytes; + } + context.Function("vkCmdCopyImageToBuffer")(commands, image.Image(), VK_IMAGE_LAYOUT_GENERAL, readback.Handle(), static_cast(copies.size()), copies.data()); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + const auto read = readback.Bytes(); + return std::vector(reinterpret_cast(read.data()), reinterpret_cast(read.data()) + linearBytes); + }; + std::vector texels(linearBytes); + for (auto& value : texels) value = random(); + const auto movesBefore = StorageTexture::SinglePassMoves(); + { + auto image = StorageTexture::Create(context, detiler, resource, 0); + recorder.Keep(image); + results[mode].uploaded = readImage(*image); + Buffer staging(context, linearBytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + std::memcpy(staging.Bytes().data(), texels.data(), linearBytes); + 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_WRITE_BIT); + std::vector copies; + std::size_t at = 0; + for (std::uint32_t level = 0; level < geometry.mips.size(); ++level) { + VkBufferImageCopy copy{}; + copy.bufferOffset = at; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level, 0, 1}; + copy.imageExtent = {geometry.mips[level].width, geometry.mips[level].height, 1}; + copies.push_back(copy); + at += static_cast(geometry.mips[level].width) * geometry.mips[level].height * elementBytes; + } + context.Function("vkCmdCopyBufferToImage")(commands, staging.Handle(), image->Image(), VK_IMAGE_LAYOUT_GENERAL, static_cast(copies.size()), copies.data()); + 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(); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + auto again = StorageTexture::Create(context, detiler, resource, 0); + recorder.Keep(again); + results[mode].reuploaded = readImage(*again); + std::vector read(bytes); + if (watched) AgcDriver::GuestMemory::Read(address, read); + else { + StorageTexture::FlushPending(address, bytes, nullptr, "test", PublishScope::Whole); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + std::memcpy(read.data(), memory, bytes); + } + results[mode].memory.assign(reinterpret_cast(read.data()), reinterpret_cast(read.data()) + bytes); + } + recorder.Sync(); + results[mode].moves = StorageTexture::SinglePassMoves() - movesBefore; + if (results[mode].reuploaded != texels) failures += what + (mode == 1 ? " (single pass)" : " (copies)") + ": the second image does not hold the texels written back\n"; + } + if (results[0].moves != 0 || results[1].moves < 3) failures += what + ": single-pass moves " + std::to_string(results[0].moves) + " with the copies and " + std::to_string(results[1].moves) + " in single pass (want 0 and at least 3)\n"; + if (results[0].uploaded != results[1].uploaded) failures += what + ": the first upload differs between the copies and single pass\n"; + if (results[0].memory != results[1].memory) { + std::size_t differ = 0; + for (std::size_t i = 0; i < results[0].memory.size(); ++i) differ += results[0].memory[i] != results[1].memory[i]; + failures += what + ": guest memory after the write-back differs in " + std::to_string(differ) + " bytes between the copies and single pass\n"; + } + ++tested; + } + if (!failures.empty()) throw std::runtime_error("single-pass storage moves:\n" + failures); + std::cout << "single-pass storage moves" << (watched ? " (watched)" : "") << ": ok (" << tested << " surfaces)\n"; +} + +class StorageUploadProbe { +public: + StorageUploadProbe(Context& context, bool failView) : context(context), resolver(context.deviceProc), failView(failView) { + Require(active == nullptr, "nested storage upload probe"); + active = this; + context.deviceProc = resolve; + } + ~StorageUploadProbe() { + for (const auto view : destroyedViews) function("vkDestroyImageView")(context.device, view, nullptr); + if (imageDestroyed) function("vkDestroyImage")(context.device, image, nullptr); + if (memoryFreed) function("vkFreeMemory")(context.device, memory, nullptr); + context.deviceProc = resolver; + active = nullptr; + } + VkImage Image() const { return image; } + bool Destroyed() const { return imageDestroyed || memoryFreed; } + bool Released() const { return imageDestroyed && memoryFreed; } + bool FailedView() const { return failedView; } + std::uint32_t Clears() const { return clears; } + +private: + template + TFunction function(const char* name) const { return reinterpret_cast(resolver(context.device, name)); } + static VKAPI_ATTR VkResult VKAPI_CALL createImage(VkDevice device, const VkImageCreateInfo* info, const VkAllocationCallbacks* callbacks, VkImage* created) { + const auto result = active->function("vkCreateImage")(device, info, callbacks, created); + if (result == VK_SUCCESS) active->image = *created; + return result; + } + static VKAPI_ATTR VkResult VKAPI_CALL bindImageMemory(VkDevice device, VkImage image, VkDeviceMemory memory, VkDeviceSize offset) { + const auto result = active->function("vkBindImageMemory")(device, image, memory, offset); + if (result == VK_SUCCESS && image == active->image) active->memory = memory; + return result; + } + static VKAPI_ATTR VkResult VKAPI_CALL createView(VkDevice device, const VkImageViewCreateInfo* info, const VkAllocationCallbacks* callbacks, VkImageView* created) { + if (info->image == active->image && active->failView && active->clears != 0) { + active->failedView = true; + return VK_ERROR_OUT_OF_DEVICE_MEMORY; + } + const auto result = active->function("vkCreateImageView")(device, info, callbacks, created); + if (result == VK_SUCCESS && info->image == active->image) active->views.push_back(*created); + return result; + } + static VKAPI_ATTR void VKAPI_CALL destroyView(VkDevice device, VkImageView view, const VkAllocationCallbacks* callbacks) { + if (std::find(active->views.begin(), active->views.end(), view) != active->views.end()) active->destroyedViews.push_back(view); + else active->function("vkDestroyImageView")(device, view, callbacks); + } + static VKAPI_ATTR void VKAPI_CALL destroyImage(VkDevice device, VkImage image, const VkAllocationCallbacks* callbacks) { + if (image == active->image) active->imageDestroyed = true; + else active->function("vkDestroyImage")(device, image, callbacks); + } + static VKAPI_ATTR void VKAPI_CALL freeMemory(VkDevice device, VkDeviceMemory memory, const VkAllocationCallbacks* callbacks) { + if (memory == active->memory) active->memoryFreed = true; + else active->function("vkFreeMemory")(device, memory, callbacks); + } + static VKAPI_ATTR void VKAPI_CALL clearImage(VkCommandBuffer commands, VkImage image, VkImageLayout layout, const VkClearColorValue* value, std::uint32_t count, const VkImageSubresourceRange* ranges) { + if (image == active->image) ++active->clears; + active->function("vkCmdClearColorImage")(commands, image, layout, value, count, ranges); + } + static VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL resolve(VkDevice device, const char* name) { + if (std::strcmp(name, "vkCreateImage") == 0) return reinterpret_cast(createImage); + if (std::strcmp(name, "vkBindImageMemory") == 0) return reinterpret_cast(bindImageMemory); + if (std::strcmp(name, "vkCreateImageView") == 0) return reinterpret_cast(createView); + if (std::strcmp(name, "vkDestroyImageView") == 0) return reinterpret_cast(destroyView); + if (std::strcmp(name, "vkDestroyImage") == 0) return reinterpret_cast(destroyImage); + if (std::strcmp(name, "vkFreeMemory") == 0) return reinterpret_cast(freeMemory); + if (std::strcmp(name, "vkCmdClearColorImage") == 0) return reinterpret_cast(clearImage); + return active->resolver(device, name); + } + Context& context; + PFN_vkGetDeviceProcAddr resolver; + bool failView; + bool failedView = false; + bool imageDestroyed = false; + bool memoryFreed = false; + std::uint32_t clears = 0; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + std::vector views; + std::vector destroyedViews; + static inline StorageUploadProbe* active = nullptr; +}; + +void storageUploadLifetimeTests(const Device& device, Recorder& previous, bool failView) { + auto context = device.GetContext(); + context.hostImportAlignment = 0; + TextureDetiler detiler(context); + context.detiler = &detiler; + GuestTextureResource resource{}; + resource.width = 64; + resource.height = 64; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kLinear; + resource.dimension = TextureDimension::k2D; + resource.format = 56; + const auto guestBytes = static_cast(DescribeSurface(resource).guestBytes); + std::vector texels(guestBytes + 256, std::byte{0x55}); + std::vector keys(guestBytes / 256, std::byte{0}); + resource.baseAddress = (reinterpret_cast(texels.data()) + 255) & ~std::uintptr_t{255}; + resource.dccAddress = reinterpret_cast(keys.data()); + DccKeyProof proof; + Require(ProvedClearKeys(resource, guestBytes, proof) == DccKeys::Clear0000, "storage lifetime fixture has no DCC clear"); + StorageUploadProbe probe(context, failView); + Buffer readback(device.GetContext(), resource.width * resource.height * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + std::memset(readback.Bytes().data(), 0x55, readback.Bytes().size()); + std::weak_ptr lifetime; + bool premature = false; + bool retained = true; + bool threw = false; + { + Recorder recorder(context); + recorder.Activate(); + try { + auto image = StorageTexture::Create(context, detiler, resource, 0); + lifetime = image; + image.reset(); + retained = !lifetime.expired(); + } catch (const std::exception& error) { + if (!probe.FailedView() || std::string_view(error.what()).find("vkCreateImageView storage") == std::string_view::npos) throw; + threw = true; + } + premature = probe.Destroyed(); + Require(probe.Clears() == 1 && recorder.Recording(), "storage creation did not record its initial clear"); + const auto commands = recorder.Commands(); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {resource.width, resource.height, 1}; + context.Function("vkCmdCopyImageToBuffer")(commands, probe.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.Sync(); + } + previous.Activate(); + Require(threw == failView && probe.FailedView() == failView, "storage view failure injection did not take the requested path"); + Require(std::all_of(readback.Bytes().begin(), readback.Bytes().end(), [](std::byte value) { return value == std::byte{0}; }), "recorded storage clear did not reach every texel"); + Require(lifetime.expired() && probe.Released(), "storage upload resources outlived recorder teardown"); + Require(!premature && retained, failView ? "failed storage creation released an image still referenced by its recorded clear" : "storage creation did not retain its image through the recorded clear"); + std::cout << "Storage upload lifetime" << (failView ? " after view failure" : " after caller release") << ": passed\n"; +} + +void refreshProofTests(const Device& device, Recorder& recorder) { + const auto& base = device.GetContext(); + if (base.hostImportAlignment == 0) { + std::cout << "host imports unavailable: refresh proofs not tested\n"; + return; + } + constexpr std::uint32_t side = 256; + constexpr std::size_t surfaceBytes = side * side * 4; + constexpr std::size_t keyCount = surfaceBytes / 256; + constexpr std::size_t bytes = surfaceBytes + 65536; + void* block = AllocateWatched(bytes, 65536); + if (block == nullptr) { + std::cout << "no write watching: refresh proofs not tested\n"; + return; + } + auto* texels = static_cast(block); + auto* keys = texels + surfaceBytes; + std::memset(texels, 0x55, surfaceBytes); + std::memset(keys, 0xff, keyCount); + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + std::uint64_t address; + ~Unregister() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, address, bytes); + } + } unregister{base, block, address}; + if (HostImportFor(base, address, bytes) == nullptr || !AgcDriver::GuestMemory::Watched(address, bytes)) { + std::cout << "no watched host import: refresh proofs not tested\n"; + return; + } + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + GuestTextureResource resource{}; + resource.baseAddress = address; + resource.width = side; + resource.height = side; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kR64KBX; + resource.dimension = TextureDimension::k2D; + resource.format = 56; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + resource.dccAddress = address + surfaceBytes; + const auto proved = [] { return StorageTexture::RefreshesProved(); }; + { + auto image = StorageTexture::Create(context, detiler, resource, 0); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + const auto draw = [&](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 settle = [&] { + image->Refresh(); + image->Refresh(); + }; + const auto expectProved = [&](const char* what) { + const auto before = proved(); + Require(image->Refresh(), what); + Require(proved() == before + 1, what); + }; + const auto expectFull = [&](const char* what) { + const auto before = proved(); + image->Refresh(); + Require(proved() == before, what); + }; + settle(); + expectProved("an unchanged surface was not proved current"); + expectProved("a proved surface was not proved again"); + draw({{1.0f, 0.0f, 0.0f, 1.0f}}); + settle(); + expectProved("a target holding its own pending results was not proved current"); + draw({{0.0f, 1.0f, 0.0f, 1.0f}}); + expectProved("a draw into an already pending target broke its proof"); + texels[0] = 0x11; + expectFull("a CPU write of the surface was answered from the proof"); + settle(); + expectProved("the proof was not taken again after a CPU write"); + AgcDriver::GuestMemory::MarkWritten(address + 65536, 4096); + expectFull("a driver store into the surface was answered from the proof"); + settle(); + AgcDriver::GuestMemory::MarkWritten(resource.dccAddress, keyCount); + expectFull("a key store over the surface's keys was answered from the proof"); + settle(); + expectProved("the proof was not taken again after a key store"); + auto other = resource; + other.dccAddress = 0; + auto pending = StorageTexture::Create(context, detiler, other, 0); + recorder.Keep(pending); + pending->MarkDirty(); + expectFull("another image's pending results over the surface were answered from the proof"); + settle(); + expectProved("the proof was not taken again after another image's results were stored"); + pending->MarkDirty(); + expectFull("another image's pending results were answered from the proof"); + Require(!AgcDriver::Graphics::PendingStorageOverlaps(address, surfaceBytes, image.get()), "the full refresh left another image's results pending"); + auto fresh = StorageTexture::Create(context, detiler, resource, 0); + const auto before = proved(); + fresh->Refresh(); + Require(proved() == before, "a new image of the surface was answered from another image's proof"); + std::memset(keys, 0x00, keyCount); + expectFull("a fast clear of the keys was answered from the proof"); + recorder.Sync(); + } + recorder.Sync(); +} + + +void unchangedCpuStampTests(const Device& device, Recorder& recorder) { + const auto& base = device.GetContext(); + constexpr std::uint32_t side = 256; + constexpr std::size_t surfaceBytes = side * side * 4; + constexpr std::uint64_t offset = 0x2000; + constexpr std::size_t bytes = 6 * 65536; + constexpr std::array red{255, 0, 0, 255}; + const auto pattern = [](std::size_t i) { return static_cast(i * 131u + 7u); }; + for (const bool imported : {false, true}) { +#ifdef _WIN32 + if (imported) { + std::cout << "unchanged CPU stamps over a watched host import: Linux write watch only\n"; + continue; + } +#endif + if (imported && base.hostImportAlignment == 0) { + std::cout << "host imports unavailable: unchanged CPU stamps over a watched host import not tested\n"; + continue; + } + const auto decided = PrepareImportWatch(base); + if (imported) SetImportWatch(base, ImportWatch::Watch); + struct Restore { + const Context& context; + ImportWatch decided; + ~Restore() { SetImportWatch(context, decided); } + } restore{base, decided}; + for (const bool changed : {false, true}) { + const std::string what = std::string(changed ? "a CPU store that changed a byte" : "a CPU store that left the bytes as they were") + (imported ? " over a watched host import" : " over watched memory"); + void* block = AllocateWatched(bytes, 65536); + if (block == nullptr) { + std::cout << "no write watching: unchanged CPU stamps not tested\n"; + return; + } + 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 + offset; + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + std::uint64_t address; + std::size_t bytes; + ~Unregister() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, address, bytes); + ReleaseWatched(block, bytes); + } + } unregister{base, block, blockAddress, bytes}; + AgcDriver::GuestMemory::CollectWritesUncached(blockAddress, bytes); + TextureDetiler detiler(base); + auto context = base; + if (!imported) context.hostImportAlignment = 0; + context.detiler = &detiler; + GuestTextureResource resource{}; + resource.baseAddress = address; + resource.width = side; + resource.height = side; + 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; + Require(DescribeSurface(resource).guestBytes == surfaceBytes, "(c) unexpected surface size"); + { + 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(); + for (std::size_t i = 0; i < 4096; ++i) memory[offset + i] = pattern(offset + i); + if (changed) memory[offset + 16] = static_cast(~pattern(offset + 16)); + AgcDriver::GuestMemory::CollectWritesUncached(blockAddress, bytes); + Require(AgcDriver::GuestMemory::Watched(address, surfaceBytes) && (HostImportFor(context, address, surfaceBytes) != nullptr) == imported, "(c) " + what + ": the image is not on the path under test"); + image->WriteBack(); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + std::vector read(bytes); + AgcDriver::GuestMemory::Read(blockAddress, read); + const auto texel = [&](std::size_t at) { + for (std::size_t c = 0; c < 4; ++c) { + if (std::to_integer(read[at + c]) != red[c]) return false; + } + return true; + }; + const auto headEnd = static_cast(65536); + std::size_t stored = 0, total = 0; + for (std::size_t at = offset; at < offset + surfaceBytes; at += 4) { + ++total; + if (texel(at)) ++stored; + } + if (!changed) { + if (stored != total) throw std::runtime_error(std::string("unchanged CPU stamps: ") + what + " dropped " + std::to_string(total - stored) + " of " + std::to_string(total) + " texels the GPU wrote"); + } else { + Require(std::to_integer(read[offset + 16]) == static_cast(~pattern(offset + 16)), "(c) " + what + ": the changed byte was overwritten by the GPU results"); + for (std::size_t at = headEnd; at < offset + surfaceBytes; at += 4) Require(texel(at), "(c) " + what + ": GPU results outside the block the CPU changed were dropped"); + } + } + recorder.Sync(); + } + } + 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 + std::cout << "generation baseline over a pending store: Linux write watch only\n"; + return; +#endif + if (base.hostImportAlignment == 0) { + std::cout << "host imports unavailable: generation baseline over a pending store not tested\n"; + return; + } + constexpr std::uint32_t side = 256; + constexpr std::uint64_t offset = 0x2000; + constexpr std::size_t bytes = 6 * 65536; + const auto decided = PrepareImportWatch(base); + SetImportWatch(base, ImportWatch::Watch); + struct Restore { + const Context& context; + ImportWatch decided; + ~Restore() { SetImportWatch(context, decided); } + } restore{base, decided}; + for (const bool pending : {false, true}) { + void* block = AllocateWatched(bytes, 65536); + if (block == nullptr) { + std::cout << "no write watching: generation baseline over a pending store not tested\n"; + return; + } + std::memset(block, 0x5a, bytes); + const auto blockAddress = reinterpret_cast(block); + const auto address = blockAddress + offset; + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + std::uint64_t address; + std::size_t bytes; + ~Unregister() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, address, bytes); + ReleaseWatched(block, bytes); + } + } unregister{base, block, blockAddress, bytes}; + AgcDriver::GuestMemory::CollectWritesUncached(blockAddress, bytes); + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + GuestTextureResource resource{}; + resource.baseAddress = address; + resource.width = side; + resource.height = side; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::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(HostImportFor(context, address, surfaceBytes) != nullptr && AgcDriver::GuestMemory::Watched(address, surfaceBytes), "(b) the surface is not a watched host import"); + static_cast(recorder.Commands()); + if (pending) recorder.NotePendingWrite(address + 4096, 4096); + const auto before = recorder.Submissions(); + { + auto image = StorageTexture::Create(context, detiler, resource, 0); + recorder.Keep(image); + if (pending) Require(recorder.Submissions() == before, "(b) an upload over a pending recorded store submitted the open batch to read its generation baseline"); + } + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + } + std::cout << "generation baseline over a pending store: ok\n"; } void importWindowTests(const Device& device, Recorder& recorder) { @@ -2209,9 +6551,7 @@ void importWindowTests(const Device& device, Recorder& recorder) { void* block; ~Release() { ReleaseWatched(block, bytes); } } release{block}; - const auto probe = ProbeImportWriteProtection(base); - Require(probe.failure == nullptr, "(w) the import probe failed"); - const bool importWrites = probe.writtenAtImport != 0; + Require(ProbeImportWriteProtection(base).failure == nullptr, "(w) the import probe failed"); const auto decided = PrepareImportWatch(base); if (decided == ImportWatch::Unwatch) { std::cout << "host imports are compared, not watched: the import window not tested\n"; @@ -2229,7 +6569,7 @@ void importWindowTests(const Device& device, Recorder& recorder) { const auto address = reinterpret_cast(block); { GuestAllocations::Mutation mutation; - mutation.Add(block, bytes, true, true); + mutation.Add(block, bytes, true, true, true); } struct Unregister { const Context& context; @@ -2260,8 +6600,10 @@ void importWindowTests(const Device& device, Recorder& recorder) { resource.dstSelW = 7; resource.dccAddress = address + surfaceBytes; { - auto image = std::make_shared(context, detiler, resource, 0); - Require(!HostImportCovers(base, address, bytes), "(w) the storage image imported its memory before a generation was taken"); + Require(!HostImportCovers(base, address, bytes), "(w) the block is imported before the cache takes its generation"); + const auto cached = CollectWritesUncached(address, surfaceBytes); + Require(cached != 0 && UnchangedSince(address, surfaceBytes, cached), "(w) the cache's generation is not current before the import"); + 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); @@ -2270,8 +6612,6 @@ void importWindowTests(const Device& device, Recorder& recorder) { context.Function("vkCmdClearColorImage")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, &red, 1, &range); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); image->MarkDirty(); - const auto cached = CollectWritesUncached(address, surfaceBytes); - Require(cached != 0 && UnchangedSince(address, surfaceBytes, cached), "(w) the cache's generation is not current before the import"); const auto* import = HostImportFor(base, address, bytes); if (import == nullptr) { std::cout << "host import of the import window block refused: the import window not tested\n"; @@ -2280,15 +6620,11 @@ void importWindowTests(const Device& device, Recorder& recorder) { } Require(!import->unwatched && Watched(address, bytes), "(w) an import under the watch decision left the watch"); CollectWritesUncached(address, surfaceBytes); - if (importWrites) { - Require(!UnchangedSince(address, surfaceBytes, cached), "(w) a cache over the pages the import reported sees no change"); - std::array changed{}; - const std::array generations{cached, cached, cached, cached}; - Require(ChangedBlocks(address, surfaceBytes, generations, changed) && std::all_of(changed.begin(), changed.end(), [](std::uint8_t value) { return value == BlockMaybeWritten; }), "(w) the pages the import reported read as written by the CPU"); - Require(!WrittenSince(address, surfaceBytes, cached), "(w) the import window reads as a CPU store"); - } else { - std::cout << "the import reports no pages here: only the pending results of the import window checked\n"; - } + Require(UnchangedSince(address, surfaceBytes, cached), "(w) an import that stored nothing reads as a change to a cache taken before it"); + Require(image->Generation() != 0 && UnchangedSince(address, surfaceBytes, image->Generation()), "(w) an import that stored nothing reads as a change to the pending storage image"); + std::array changed{}; + const std::array generations{cached, cached, cached, cached}; + Require(ChangedBlocks(address, surfaceBytes, generations, changed) && std::all_of(changed.begin(), changed.end(), [](std::uint8_t value) { return value == BlockUnchanged; }), "(w) the pages the import resolved read as written"); const auto after = CollectWritesUncached(address, surfaceBytes); Require(after != 0 && UnchangedSince(address, surfaceBytes, after), "(w) the imported range is not watched again after the import window"); image->Refresh(); @@ -2318,11 +6654,371 @@ void importWindowTests(const Device& device, Recorder& recorder) { #endif } -void pendingKeyStoreTests(const Device& device, Recorder& recorder) { +void queuedWriteGroupTests() { + using Write = Recorder::QueuedWrite; + using Groups = std::vector; + const auto paint = [](const std::vector& writes, const Groups& groups, std::size_t bytes) { + std::vector memory(bytes, 0); + std::size_t first = 0; + for (const auto end : groups) { + for (const bool computed : {false, true}) { + for (auto i = first; i < end; ++i) { + if (writes[i].computed != computed) continue; + for (auto at = writes[i].begin; at < writes[i].end; ++at) memory[at] = static_cast(i + 1); + } + } + first = end; + } + return memory; + }; + const auto programOrder = [](const std::vector& writes, std::size_t bytes) { + std::vector memory(bytes, 0); + for (std::size_t i = 0; i < writes.size(); ++i) { + for (auto at = writes[i].begin; at < writes[i].end; ++at) memory[at] = static_cast(i + 1); + } + 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}}; + Require(Recorder::QueuedWriteGroups(reused) == Groups{1, 3}, "a store over a computed write of its group did not start a new group"); + const std::vector apart{{0x100, 0x1f8, true}, {0x1f8, 0x200, false}, {0x300, 0x310, false}}; + Require(Recorder::QueuedWriteGroups(apart) == Groups{1, 3}, "stores after the computed writes joined their group"); + const std::vector label{{0x100, 0x1f8, true}, {0x108, 0x200, true}, {0x400, 0x408, false}}; + Require(Recorder::QueuedWriteGroups(label) == Groups{2, 3}, "a label after the dumps landed before them"); + const std::vector stores{{0x100, 0x110, false}, {0x100, 0x110, false}}; + Require(Recorder::QueuedWriteGroups(stores) == Groups{2}, "stores alone were split into groups"); + std::uint64_t seed = 0x9e3779b97f4a7c15ull; + const auto next = [&](std::uint64_t bound) { + seed ^= seed << 13u; + seed ^= seed >> 7u; + seed ^= seed << 17u; + return seed % bound; + }; + constexpr std::size_t bytes = 64; + for (int round = 0; round < 20000; ++round) { + std::vector writes; + const auto count = 1 + next(12); + for (std::uint64_t i = 0; i < count; ++i) { + const auto begin = next(bytes - 1); + const auto end = begin + 1 + next(std::min(16, bytes - begin)); + writes.push_back({begin, std::min(end, bytes), next(2) == 0}); + } + 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"); + } + } + } +} + +constexpr std::uint64_t SampleReady = 1ull << 63u; + +void queuedSampleDumpTests(Recorder& recorder, const HostImport& import, std::uint64_t* words, std::uint64_t address, VkDeviceAddress target) { + constexpr std::uint64_t untouched = 0xaaaaaaaaaaaaaaaaull; + const auto storeZero = [&](std::uint64_t at, std::size_t bytes) { + const std::vector zero(bytes); + recorder.RecordStore(import.buffer, at - import.base, zero, at); + }; + recorder.Sync(); + const auto base = recorder.SamplesTotal(); + std::fill(words, words + 256, untouched); + Require(recorder.DumpSamples(target, address), "a dump was not queued"); + Require(recorder.HasQueuedStores() && recorder.QueuedStoreOverlaps(address + 240, 8) && !recorder.QueuedStoreOverlaps(address + 248, 8), "a queued dump does not cover its 16 counters"); + recorder.FlushStoresOverlapping(address + 256, 64); + Require(recorder.HasQueuedStores(), "a reader of other bytes recorded the queued dump"); + Require(words[0] == untouched, "a queued dump landed before anything recorded it"); + recorder.FlushStoresOverlapping(address + 16, 8); + Require(!recorder.HasQueuedStores(), "a reader of the dumped bytes left the dump queued"); + recorder.Sync(); + Require(words[0] == (base | SampleReady) && words[2] == SampleReady && words[1] == untouched, "the dump a reader recorded stored the wrong counters"); + std::fill(words, words + 256, untouched); + storeZero(address, 256); + Require(recorder.DumpSamples(target, address), "a dump after a clear was not queued"); + storeZero(address + 512, 16); + Require(recorder.DumpSamples(target + 8, address + 8), "the end dump was not queued"); + Require(recorder.DumpSamples(target + 256, address + 256), "a dump of another slot was not queued"); + storeZero(address + 256, 16); + Require(recorder.DumpSamples(target + 256 + 8, address + 256 + 8), "a dump after a store over an earlier dump was not queued"); + recorder.Sync(); + Require(words[0] == (base | SampleReady) && words[1] == (base | SampleReady) && words[2] == SampleReady && words[3] == SampleReady, "a clear and its dumps landed out of order"); + Require(words[32] == 0 && words[34] == SampleReady, "a store after a dump of its bytes did not land last"); + Require(words[33] == (base | SampleReady) && words[35] == SampleReady, "a dump after a store over an earlier dump landed before it"); + Require(words[64] == 0 && words[65] == 0 && words[66] == untouched, "a store between dumps was lost"); +} + +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 { + 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) { + VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + info.codeSize = bytes; + info.pCode = code; + VkShaderModule result = VK_NULL_HANDLE; + Check(context.Function("vkCreateShaderModule")(context.device, &info, nullptr, &result), "vkCreateShaderModule cover"); + return result; + }; + vertex = module(COVER_VERT_SPV, sizeof(COVER_VERT_SPV)); + fragment = module(COVER_FRAG_SPV, sizeof(COVER_FRAG_SPV)); + 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 vertexInput{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO}; + VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO}; + assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + const VkViewport viewport{0, 0, 8, 8, 0, 1}; + const VkRect2D scissor{{0, 0}, {8, 8}}; + VkPipelineViewportStateCreateInfo viewportState{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO}; + viewportState.viewportCount = 1; + viewportState.pViewports = &viewport; + viewportState.scissorCount = 1; + 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.lineWidth = 1; + VkPipelineMultisampleStateCreateInfo multisample{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; + multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + VkPipelineColorBlendStateCreateInfo blendState{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; + blendState.attachmentCount = 1; + blendState.pAttachments = &blend; + VkGraphicsPipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO}; + pipelineInfo.stageCount = static_cast(stages.size()); + pipelineInfo.pStages = stages.data(); + pipelineInfo.pVertexInputState = &vertexInput; + pipelineInfo.pInputAssemblyState = &assembly; + pipelineInfo.pViewportState = &viewportState; + pipelineInfo.pRasterizationState = &raster; + pipelineInfo.pMultisampleState = &multisample; + pipelineInfo.pColorBlendState = &blendState; + pipelineInfo.layout = layout; + 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(); + throw; + } + } + ~CoverPass() { release(); } + CoverPass(const CoverPass&) = delete; + CoverPass& operator=(const CoverPass&) = delete; + + bool Draw(Recorder& recorder) const { + const auto start = recorder.StartDrawPass(PassKey, false, false, PassAccess{}); + const bool continued = start.continued; + const auto commands = start.commands; + if (!continued) { + recorder.PrepareSampleSlot(); + 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); + context.Function("vkCmdDraw")(commands, 3, 1, 0, 0); + recorder.LeaveRenderPassOpen(PassKey, Recorder::NoTiming, false, PassAccess{}); + return continued; + } + + static constexpr std::uint64_t Samples = 64; + +private: + static constexpr std::uint64_t PassKey = 0xc0de; + void release() noexcept { + const auto device = context.device; + if (pipeline != VK_NULL_HANDLE) context.Function("vkDestroyPipeline")(device, pipeline, nullptr); + if (vertex != VK_NULL_HANDLE) context.Function("vkDestroyShaderModule")(device, vertex, nullptr); + if (fragment != VK_NULL_HANDLE) context.Function("vkDestroyShaderModule")(device, fragment, nullptr); + if (layout != VK_NULL_HANDLE) context.Function("vkDestroyPipelineLayout")(device, layout, nullptr); + if (framebuffer != VK_NULL_HANDLE) context.Function("vkDestroyFramebuffer")(device, framebuffer, nullptr); + if (renderPass != VK_NULL_HANDLE) context.Function("vkDestroyRenderPass")(device, renderPass, nullptr); + pipeline = VK_NULL_HANDLE; + vertex = fragment = VK_NULL_HANDLE; + layout = VK_NULL_HANDLE; + framebuffer = VK_NULL_HANDLE; + renderPass = VK_NULL_HANDLE; + } + const Context& context; + RenderTarget target; + mutable bool transitioned = false; + VkRenderPass renderPass = VK_NULL_HANDLE; + VkFramebuffer framebuffer = VK_NULL_HANDLE; + VkPipelineLayout layout = VK_NULL_HANDLE; + VkShaderModule vertex = VK_NULL_HANDLE; + VkShaderModule fragment = VK_NULL_HANDLE; + VkPipeline pipeline = VK_NULL_HANDLE; +}; + +void inPassSampleDumpTests(Recorder& recorder, const Context& context, const CoverPass& cover, std::uint64_t* words, std::uint64_t address, VkDeviceAddress target) { + const auto samples = [&](std::uint64_t word) { return word & ~SampleReady; }; + recorder.Sync(); + auto base = recorder.SamplesTotal(); + std::fill(words, words + 64, 0); + Require(!cover.Draw(recorder), "the first draw of a batch continued a pass"); + Require(recorder.DumpSamples(target, address), "a dump inside a render pass was not made on the GPU"); + Require(cover.Draw(recorder), "an occlusion dump ended the render pass"); + Require(recorder.DumpSamples(target + 8, address + 8), "the second dump inside the pass was not made on the GPU"); + Require(cover.Draw(recorder), "the second occlusion dump ended the render pass"); + Require(words[0] == 0 && words[1] == 0, "a dump inside the pass landed before its batch ran"); + recorder.Sync(); + const auto first = samples(words[0]) - base, second = samples(words[1]) - base, total = recorder.SamplesTotal() - base; + Require((words[0] & SampleReady) != 0 && (words[1] & SampleReady) != 0, "a dump inside a pass lost its ready bit"); + Require(first > 0 && second > first && total > second, "dumps inside one pass do not split its draws' samples in order"); + if (context.occlusionQueryPrecise) Require(first == CoverPass::Samples && second == 2 * CoverPass::Samples && total == 3 * CoverPass::Samples, "dumps inside one pass counted the wrong samples"); + base = recorder.SamplesTotal(); + Require(!cover.Draw(recorder), "the first draw of a new batch continued a pass"); + Require(recorder.DumpSamples(target, address), "a dump before a reader was not made on the GPU"); + recorder.FlushStoresOverlapping(address, 8); + Require(!cover.Draw(recorder), "a reader of a queued dump recorded it inside the render pass"); + recorder.Sync(); + Require(samples(words[0]) > base && samples(words[0]) < recorder.SamplesTotal(), "the dump a reader recorded in the pass counted the wrong draws"); + base = recorder.SamplesTotal(); + constexpr std::size_t slots = 160; + std::fill(words, words + slots * 32, 0); + std::size_t continued = 0; + for (std::size_t i = 0; i < slots; ++i) { + if (cover.Draw(recorder)) ++continued; + Require(recorder.DumpSamples(target + 256 * i, address + 256 * i), "a dump of many in one pass was not made on the GPU"); + } + recorder.Sync(); + Require(continued + 2 >= slots, "draws between dumps did not keep their render pass"); + std::uint64_t previous = base; + for (std::size_t i = 0; i < slots; ++i) { + const auto value = samples(words[32 * i]); + Require((words[32 * i] & SampleReady) != 0 && value > previous, "a dump past one query pool's slots counted the wrong draws"); + if (context.occlusionQueryPrecise) Require(value == base + (i + 1) * CoverPass::Samples, "a dump past one query pool's slots counted the wrong samples"); + previous = value; + } + base = recorder.SamplesTotal(); + constexpr std::size_t passes = 70; + for (std::size_t i = 0; i < passes; ++i) { + Require(!cover.Draw(recorder), "a draw after other work continued a pass"); + recorder.Commands(); + } + recorder.Submit(); + Require(recorder.DumpSamples(target, address), "a dump after many passes was not made on the GPU"); + recorder.Sync(); + const auto folded = samples(words[0]); + Require(folded > base && folded == recorder.SamplesTotal(), "segments folded at Submit without a dump were lost"); + if (context.occlusionQueryPrecise) Require(folded == base + passes * CoverPass::Samples, "segments folded at Submit without a dump counted the wrong samples"); +} + +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; @@ -2337,7 +7033,7 @@ void pendingKeyStoreTests(const Device& device, Recorder& recorder) { const auto address = reinterpret_cast(block); { GuestAllocations::Mutation mutation; - mutation.Add(block, bytes, true, true); + mutation.Add(block, bytes, true, true, true); } struct Unregister { const Context& context; @@ -2353,10 +7049,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"); @@ -2368,9 +7064,38 @@ 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"); + recorder.NotePendingWrite(address + keyCount / 2, 16); + Require(CurrentDccKeys(address, surfaceBytes) == DccKeys::Uncompressed && !recorder.PendingWriteOverlaps(address, keyCount), "a later writer of unknown bytes over pending fills was not waited for"); + recorder.NotePendingFill(address, keyCount / 2, 0x00); + Require(CurrentDccKeys(address, surfaceBytes) == DccKeys::Mixed && recorder.PendingWriteOverlaps(address, keyCount), "a pending 0000 fill over half of uncompressed keys did not read as mixed without a wait"); recorder.Sync(); +#ifdef _WIN32 + auto* edgeBlock = static_cast(VirtualAlloc(nullptr, 2 * bytes, MEM_RESERVE, PAGE_NOACCESS)); + Require(edgeBlock != nullptr && VirtualAlloc(edgeBlock, bytes, MEM_COMMIT, PAGE_READWRITE) != nullptr, "cannot reserve the key block past committed memory"); +#else + void* edgeMapping = mmap(nullptr, 2 * bytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(edgeMapping != MAP_FAILED && mprotect(edgeMapping, bytes, PROT_READ | PROT_WRITE) == 0, "cannot reserve the key block past committed memory"); + auto* edgeBlock = static_cast(edgeMapping); +#endif + const auto edge = reinterpret_cast(edgeBlock) + bytes - keyCount / 2; + Require(!AgcDriver::GuestMemory::Accessible(reinterpret_cast(edge), keyCount), "keys past the end of committed memory read as accessible"); + recorder.NotePendingFill(edge, keyCount / 2, 0x40); + Require(CurrentDccKeys(edge, surfaceBytes) == DccKeys::Unreadable && !recorder.PendingWriteOverlaps(edge, keyCount), "keys running past the end of committed memory, with a pending fill over their committed half, were not read as unreadable after a wait"); +#ifdef _WIN32 + VirtualFree(edgeBlock, 0, MEM_RELEASE); +#else + munmap(edgeBlock, 2 * bytes); +#endif constexpr std::uint64_t displayBytes = 15u * 9u * 65536u; constexpr std::size_t consoleKeys = 49152; const auto displayKeys = DccKeyCount(TextureTileMode::kR64KBX, 4, 1920, 1080, displayBytes); @@ -2383,6 +7108,108 @@ 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) { + using namespace AgcDriver::GuestMemory; + const auto& context = device.GetContext(); + if (!UnitShadowEnabled() || context.hostImportAlignment == 0) { + std::cout << "unit shadows or host imports unavailable: keys under a unit shadow not tested\n"; + return false; + } + constexpr std::uint64_t unit = 65536; + constexpr std::size_t bytes = 2 * unit; + constexpr std::size_t surfaceBytes = 65536; + constexpr std::size_t keyCount = surfaceBytes / 256; +#ifdef _WIN32 + void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, unit); + Require(block != nullptr, "cannot reserve the shadowed key block"); + const auto section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, 0, static_cast(bytes), nullptr); + Require(section != nullptr, "cannot create the shadowed key block"); + GuestArena::GuestArenaMap_nid_postfix(block, bytes, section, 0, PAGE_READWRITE); + CloseHandle(section); +#else + void* block = AllocateWatched(bytes, unit); + Require(block != nullptr, "cannot allocate the shadowed key block"); +#endif + struct Release { + void* block; + ~Release() { ReleaseWatched(block, bytes); } + } release{block}; + auto* keys = static_cast(block); + std::memset(block, 0xff, bytes); + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + std::uint64_t address; + ~Unregister() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, address, bytes); + } + } unregister{context, block, address}; + const auto* import = HostImportFor(context, address, bytes); + if (import == nullptr || !Watched(address, bytes)) { + std::cout << "the shadowed key block is not imported and watched: keys under a unit shadow not tested\n"; + return false; + } + recorder.Sync(); + CollectWritesUncached(address, bytes); + const auto copyBuffer = context.Function("vkCmdCopyBuffer"); + const auto shadow = [&](std::uint8_t value) { + auto destination = ShadowDestinationFor(context, *import, address, address + unit); + Require(destination.has_value() && destination->seedUnits.empty(), "the key unit has no unit shadow destination"); + auto pattern = std::make_shared(context, static_cast(unit), VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + std::memset(pattern->Bytes().data(), value, pattern->Bytes().size()); + const auto commands = recorder.Commands(); + recorder.Keep(pattern); + recorder.Keep(destination->slab); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_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{address, address + unit, destination->slab}; + MarkShadowed(*import, std::span(&range, 1), TrackerGeneration()); + Require(AnyShadowedOverlaps(address, keyCount), "the key unit is not shadowed"); + }; + recorder.NotePendingFill(address, unit, 0x00); + MarkWritten(address, unit); + shadow(0x40); + Require(CurrentDccKeys(address, surfaceBytes) == DccKeys::Clear0001 && !recorder.PendingWriteOverlaps(address, keyCount), "the keys under a fresh unit shadow were not read from its published bytes after a wait"); + Require(keys[0] == 0x40 && keys[unit - 1] == 0x40 && keys[unit] == 0xff && !AnyShadowedOverlaps(address, unit), "the fresh unit's publish did not land"); + recorder.Sync(); + shadow(0x22); + 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"); + Require(!AnyShadowedOverlaps(address, unit), "the stale unit still counts as shadowed after the key read"); + recorder.Sync(); + GuestTextureResource resource{}; + resource.baseAddress = address + unit; + resource.format = 56; + resource.dccAddress = address; + shadow(0x22); + recorder.NotePendingFill(address, unit, 0xc0); + MarkWritten(address, unit); + Require(!WaitForKeyWriters(resource, surfaceBytes).has_value() && recorder.PendingWriteOverlaps(address, keyCount), "the key writers over a stale unit shadow were waited for"); + recorder.Sync(); + Require(keys[0] == 0x40 && keys[unit - 1] == 0x40, "a stale unit's slab bytes were published"); + return true; } void sampleDumpTests(const Device& device, Recorder& recorder) { @@ -2404,7 +7231,7 @@ void sampleDumpTests(const Device& device, Recorder& recorder) { const auto address = reinterpret_cast(block); { GuestAllocations::Mutation mutation; - mutation.Add(block, bytes, true, true); + mutation.Add(block, bytes, true, true, true); } struct Unregister { const Context& context; @@ -2426,7 +7253,7 @@ void sampleDumpTests(const Device& device, Recorder& recorder) { const auto target = import->address + (address - import->base); recorder.Sync(); constexpr std::uint64_t ready = 1ull << 63u; - Require(recorder.DumpSamples(target), "the occlusion counters were not dumped on the GPU"); + Require(recorder.DumpSamples(target, address), "the occlusion counters were not dumped on the GPU"); Require(words[0] == untouched && !recorder.Idle(), "the occlusion counter dump waited for the GPU or landed before its batch ran"); recorder.Sync(); const auto begin = recorder.SamplesTotal(); @@ -2434,7 +7261,7 @@ void sampleDumpTests(const Device& device, Recorder& recorder) { Require(words[db * 2] == ((db == 0 ? begin : 0) | ready), "an occlusion counter dump stored the wrong value"); Require(words[db * 2 + 1] == untouched, "an occlusion counter dump stored over the next counter"); } - Require(recorder.DumpSamples(target + 8), "the second occlusion counter dump was not made on the GPU"); + Require(recorder.DumpSamples(target + 8, address + 8), "the second occlusion counter dump was not made on the GPU"); recorder.Submit(); recorder.Sync(); for (std::size_t db = 0; db < 16; ++db) { @@ -2442,9 +7269,93 @@ void sampleDumpTests(const Device& device, Recorder& recorder) { Require(words[db * 2] == ((db == 0 ? begin : 0) | ready), "the second dump stored over the first"); } Require(recorder.SamplesTotal() == begin, "the sample total moved with nothing drawn"); - for (int i = 0; i < 40; ++i) Require(recorder.DumpSamples(target), "a dump past one batch's query slots was not made on the GPU"); + for (int i = 0; i < 40; ++i) Require(recorder.DumpSamples(target, address), "a dump past one batch's query slots was not made on the GPU"); recorder.Sync(); Require(words[0] == (begin | ready) && recorder.SamplesTotal() == begin, "dumps past one batch's query slots moved the total"); + Require(recorder.QueuesSampleDumps(), "occlusion dumps are recorded at once, not queued with the batch's stores"); + queuedSampleDumpTests(recorder, *import, words, address, target); + if (!device.Graphics()) { + std::cout << "the test queue has no graphics: occlusion dumps inside a render pass not tested\n"; + return; + } + CoverPass cover(context); + inPassSampleDumpTests(recorder, context, cover, words, address, target); +} + +void cmaskPassTests(const Device& device, Recorder& recorder) { + namespace GuestMemory = AgcDriver::GuestMemory; + auto context = device.GetContext(); + context.hostImportAlignment = 0; + TextureDetiler detiler(context); + context.detiler = &detiler; + constexpr std::size_t surfaceBytes = 256 * 256 * 4; + constexpr std::size_t bytes = surfaceBytes + 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 CMASK pass 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() { + ClearCachedTextures(context.device); + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + } release{context, block}; + Require(HostImportFor(context, address, bytes) == nullptr, "CMASK copy-path test imported its memory"); + std::memset(block, 0x55, surfaceBytes); + std::memset(static_cast(block) + surfaceBytes, 0, 4096); + ColorTarget color{}; + color.address = address; + color.extent = {256, 256}; + color.format = VK_FORMAT_R8G8B8A8_UNORM; + color.bytes = surfaceBytes; + color.componentMapping = 0xe4u; + color.tileMode = ColorTileMode::RenderTarget; + color.cmaskAddress = address + surfaceBytes; + color.cmaskBytes = CmaskBytes(color.extent.width, color.extent.height); + color.clearWords = {0x80402010u, 0}; + const auto run = [&] { RunColorMetadataPass(context, {ColorMetadataPass::Mode::EliminateFastClear, {color}}); }; + auto broken = context; + broken.deviceProc = nullptr; + bool refused = false; + try { + RunColorMetadataPass(broken, {ColorMetadataPass::Mode::EliminateFastClear, {color}}); + } catch (const std::runtime_error& error) { + refused = std::string_view(error.what()).find("missing Vulkan device function resolver") != std::string_view::npos; + if (!refused) throw; + } + Require(refused, "CMASK storage creation failure was silently ignored"); + Require(*static_cast(block) == 0x55 && ReadDccKeys(color.cmaskAddress, color.cmaskBytes * 256u) == DccKeys::Clear0000, "failed CMASK storage creation changed guest memory"); + run(); + Require(StorageTexture::FindPending(address, surfaceBytes) != nullptr && *static_cast(block) == 0x55, "CMASK clear without host imports did not stay on the GPU"); + Require(ReadDccKeys(color.cmaskAddress, color.cmaskBytes * 256u) == DccKeys::Uncompressed, "resident CMASK clear left unresolved metadata"); + color.clearWords[0] = 0x12345678; + run(); + std::vector stored(surfaceBytes / 4); + GuestMemory::Read(address, std::as_writable_bytes(std::span(stored))); + Require(std::ranges::all_of(stored, [](auto word) { return word == 0x80402010u; }), "expanded CMASK changed a pending clear or copy write-back lost it"); + const std::vector clearKeys(4096); + GuestMemory::Write(color.cmaskAddress, clearKeys); + run(); + GuestMemory::Read(address, std::as_writable_bytes(std::span(stored))); + Require(std::ranges::all_of(stored, [](auto word) { return word == 0x12345678u; }), "a second resident CMASK clear retained the first frame"); + recorder.Sync(); } void metadataPassTests(const Device& device, Recorder& recorder) { @@ -2468,7 +7379,7 @@ void metadataPassTests(const Device& device, Recorder& recorder) { const auto address = reinterpret_cast(block); { GuestAllocations::Mutation mutation; - mutation.Add(block, bytes, true, true); + mutation.Add(block, bytes, true, true, true); } struct Unregister { const Context& context; @@ -2539,7 +7450,9 @@ void metadataPassTests(const Device& device, Recorder& recorder) { Require(keys[0] == 0xff, "the recorded key fill landed before its batch ran"); Require(CurrentDccKeys(color.dccAddress, surfaceBytes) == DccKeys::ClearRegister, "the keys read past a pending fast-clear fill"); } + std::memset(texels, 0x77, surfaceBytes); RunColorMetadataPass(context, pass); + Require(StorageTexture::FindPending(address, surfaceBytes) != nullptr && texels[0] == 0x77, "a fast clear recorded before the pass fell back to CPU write-back"); Require(keysUncompressed() && memoryHolds({0x10, 0x20, 0x40, 0x80}), "a fast clear recorded before the pass was not eliminated"); std::memset(texels, 0x66, surfaceBytes); RunColorMetadataPass(context, pass); @@ -2563,6 +7476,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 @@ -2594,6 +7526,105 @@ void dataWordPositionsTests() { } } +std::array linearTextureWords(std::uint64_t address, std::uint32_t width, std::uint32_t height, std::uint32_t format) { + const auto base40 = address >> 8u; + std::array words{}; + words[0] = static_cast(base40); + words[1] = static_cast((base40 >> 32u) & 0xffu) | (format << 20u) | (((width - 1u) & 3u) << 30u); + words[2] = (((width - 1u) >> 2u) & 0xfffu) | ((height - 1u) << 14u); + words[3] = 4u | (5u << 3u) | (6u << 6u) | (7u << 9u) | (9u << 28u); + return words; +} + +void imageMemoTests(const Device& device, Recorder& recorder) { + const auto& base = device.GetContext(); + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + TextureCache textureCache(context); + context.textureCache = &textureCache; + if (!device.NullDescriptors()) { + std::cout << "nullDescriptor unavailable: image memo not tested\n"; + return; + } + context.nullDescriptors = true; + constexpr std::uint32_t width = 64; + constexpr std::uint32_t height = 4; + constexpr std::size_t surfaceBytes = width * height * 4; + constexpr std::size_t bytes = 2 * 65536; + void* block = AllocateWatched(bytes, 65536); + const bool watched = block != nullptr; +#ifdef _WIN32 + if (!watched) block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); +#else + if (!watched) block = std::aligned_alloc(65536, bytes); +#endif + Require(block != nullptr, "cannot allocate the image memo block"); + auto* first = static_cast(block); + auto* second = first + 65536; + std::memset(first, 0x11, surfaceBytes); + std::memset(second, 0x22, surfaceBytes); + ShaderRecompiler::RecompileResult program; + ShaderRecompiler::DescriptorBinding binding; + binding.kind = ShaderRecompiler::DescriptorKind::SampledImage; + binding.role = ShaderRecompiler::DescriptorRole::GuestImages; + binding.descriptorSet = 0; + binding.binding = 1; + binding.count = 1; + binding.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D; + binding.imageSamplers = {0u}; + program.bindings.push_back(binding); + const CompiledShader compiled{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + std::vector> kept; + const auto sample = [&](const std::uint8_t* surface) { + const auto words = linearTextureWords(reinterpret_cast(surface), width, height, 56); + program.bindings[0].guestDescriptor.assign(words.begin(), words.end()); + ShaderResources resources(context, compiled); + Require(resources.SampledTextures().size() == 1, "the build has no sampled texture"); + kept.push_back(resources.SampledTextures().front()); + return kept.back(); + }; + const auto made = sample(first); + Require(sample(first) == made, "an unchanged sampled texture was made again"); + const auto other = sample(second); + Require(other != made && sample(first) == made, "a texture of another descriptor answered for the first"); + ClearCachedTextures(context.device); + const auto remade = sample(first); + Require(remade != made && remade != other, "a texture the cache dropped was bound again"); + Require(sample(first) == remade, "the remade texture is not the cache's"); + const auto replaced = sample(second); + Require(replaced != other && sample(second) == replaced && sample(first) == remade, "the cache does not answer with the replacements"); + const auto firstWords = linearTextureWords(reinterpret_cast(first), width, height, 56); + const auto secondWords = linearTextureWords(reinterpret_cast(second), width, height, 56); + const auto bindElements = [&](std::initializer_list> elements, std::vector depthCompare) { + program.bindings[0].count = static_cast(elements.size()); + program.bindings[0].guestDescriptor.clear(); + for (const auto& words : elements) program.bindings[0].guestDescriptor.insert(program.bindings[0].guestDescriptor.end(), words.begin(), words.end()); + program.bindings[0].imageDepthCompare = std::move(depthCompare); + program.bindings[0].imageSamplers.assign(elements.size(), 0u); + ShaderResources resources(context, compiled); + Require(resources.SampledTextures().size() == elements.size(), "the build has the wrong number of sampled textures"); + kept.insert(kept.end(), resources.SampledTextures().begin(), resources.SampledTextures().end()); + return resources.SampledTextures(); + }; + const auto repeated = bindElements({firstWords, firstWords, secondWords, firstWords}, {}); + Require(repeated[0] == remade && repeated[1] == remade && repeated[2] == replaced && repeated[3] == remade, "repeated elements of one build do not bind the cache's textures"); + program.bindings[0].count = 1; + program.bindings[0].imageDepthCompare.clear(); + program.bindings[0].imageSamplers.assign(1, 0u); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + kept.clear(); + ClearCachedTextures(context.device); + if (watched) ReleaseWatched(block, bytes); +#ifdef _WIN32 + else VirtualFree(block, 0, MEM_RELEASE); +#else + else std::free(block); +#endif +} + // A template's data buffers refreshed by words from a patched compiled result (a data-only hit) // and back: DataWordsHash() follows the buffers exactly, so a later recipe hit's hash compare // (RecordedDispatch::DataRefresh::Hash) decides correctly in both directions. @@ -2636,6 +7667,606 @@ void dataRefreshTests(const Device& device, Recorder& recorder) { recorder.Sync(); } +void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { + const auto& base = device.GetContext(); + if ((base.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_4_BIT) == 0 || !device.NullDescriptors()) { + std::cout << "4x color targets or nullDescriptor unavailable: multisampled target guest access not tested\n"; + return; + } + constexpr std::uint32_t side = 64; + constexpr std::size_t targetBytes = 65536u; + constexpr std::size_t bytes = 5u * 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 multisampled target block"); + std::memset(block, 0, bytes); + const auto address = reinterpret_cast(block); + { + 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; + context.nullDescriptors = true; + TextureCache cache(context); + context.textureCache = &cache; + const auto clearTargets = [&] { + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + ClearMultisampleTargets(context.device); + }; + const auto refused = [](const std::function& access, const std::string& expected, const std::string& what) { + std::string message; + try { + access(); + } catch (const std::exception& error) { + message = error.what(); + } + Require(message.find(expected) != std::string::npos && message.find("multisampled color target") != std::string::npos, what + " of a resolved-only multisampled target's memory did not throw: " + message); + }; + ColorTarget target{}; + target.address = address; + target.surfaceAddress = address; + target.extent = {side, side}; + target.surfaceExtent = target.extent; + 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(); + } + std::array texels{}; + refused([&] { AgcDriver::GuestMemory::Read(address + 256u, texels); }, "a CPU read", "a CPU read"); + refused([&] { AgcDriver::GuestMemory::ReadCommitted(address + 256u, texels); }, "a CPU read", "a committed CPU read"); + refused([&] { AgcDriver::GuestMemory::ReadCommittedUnsynced(address + 256u, texels); }, "a CPU read", "a committed CPU read without the flush hook"); + refused([&] { RequireNoMultisampleTarget(address, 64u, "a DMA copy"); }, "a DMA copy", "a DMA copy"); + 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; + binding.role = ShaderRecompiler::DescriptorRole::GuestImages; + binding.descriptorSet = 0; + binding.binding = ShaderRecompiler::RuntimeAbi::FirstImageBinding; + binding.count = 1; + binding.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D; + binding.imageSamplers = {0}; + binding.guestDescriptor = { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (56u << 20u) | (((side - 1u) & 3u) << 30u), + ((side - 1u) >> 2u) | ((side - 1u) << 14u), + 0xfacu | (9u << 28u), + 0u, + 0u, + 0u, + 0u, + }; + program.bindings.push_back(binding); + const CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + refused([&] { + 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(); + target.tileMode = ColorTileMode::RenderTarget; + { + std::lock_guard gpu(GpuMutex()); + Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "the tiled multisampled target has no view"); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + } + refused([&] { AgcDriver::GuestMemory::Read(address + targetBytes - texels.size(), texels); }, "a CPU read", "a CPU read inside the tiled multisampled target"); + AgcDriver::GuestMemory::Read(address + targetBytes, texels); + clearTargets(); + if ((base.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_2_BIT) != 0) { + target.extent = {128, 64}; + target.surfaceExtent = target.extent; + target.samples = 2; + { + std::lock_guard gpu(GpuMutex()); + Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "the 2x tiled multisampled target has no view"); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + } + refused([&] { AgcDriver::GuestMemory::Read(address + 2u * targetBytes - texels.size(), texels); }, "a CPU read", "a CPU read inside the 2x tile-height padding"); + AgcDriver::GuestMemory::Read(address + 2u * targetBytes, texels); + clearTargets(); + } + if ((base.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_8_BIT) != 0) { + target.extent = {64, 32}; + target.surfaceExtent = target.extent; + target.samples = 8; + { + std::lock_guard gpu(GpuMutex()); + Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "the 8x tiled multisampled target has no view"); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + } + refused([&] { AgcDriver::GuestMemory::Read(address + 2u * targetBytes - texels.size(), texels); }, "a CPU read", "a CPU read inside the 8x tile-height padding"); + AgcDriver::GuestMemory::Read(address + 2u * targetBytes, texels); + clearTargets(); + } + AgcDriver::GuestMemory::Read(address + 256u, texels); + target.extent = {side, side}; + target.surfaceExtent = target.extent; + target.samples = 4; + VkImageView previous = VK_NULL_HANDLE; + 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(); + } + { + GuestAllocations::Mutation mutation; + mutation.Protect(block, bytes, true, true, true, [] {}); + } + refused([&] { RequireNoMultisampleTarget(address, 64u, "a CPU read"); }, "a CPU read", "a read after protection changed"); + bool failed = false; + try { + GuestAllocations::Mutation mutation; + mutation.Unmap(block, bytes, [](const void*, std::size_t, const void*, bool) { throw std::runtime_error("unmap failed"); }); + } catch (const std::runtime_error&) { + failed = true; + } + Require(failed, "the failed host unmap did not throw"); + refused([&] { RequireNoMultisampleTarget(address, 64u, "a CPU read"); }, "a CPU read", "a read after a failed unmap"); + { + GuestAllocations::Mutation mutation; + if (removeAllocation) mutation.Remove(block); + else mutation.Unmap(block, bytes, [](const void*, std::size_t, const void*, bool) {}); + mutation.Add(block, bytes, true, true, true); + } + RequireNoMultisampleTarget(address, bytes, "a texture read"); + AgcDriver::GuestMemory::Read(address + 256u, texels); + 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()) { + std::cout << "nullDescriptor unavailable: depth surface fast proof not tested\n"; + return; + } + constexpr std::uint32_t side = 64; + constexpr std::size_t bytes = 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 depth surface block"); + std::memset(block, 0, bytes); + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + ~Unregister() { + ClearDepthSurfaces(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; + context.nullDescriptors = true; + TextureCache cache(context); + context.textureCache = &cache; + const DepthTarget target{address, 0, {side, side}, VK_FORMAT_D32_SFLOAT, 1.0f, 0}; + Require(DepthSurfaceView(context, target) != VK_NULL_HANDLE, "the depth surface has no view"); + ShaderRecompiler::RecompileResult program; + ShaderRecompiler::DescriptorBinding binding; + binding.kind = ShaderRecompiler::DescriptorKind::SampledImage; + binding.role = ShaderRecompiler::DescriptorRole::GuestImages; + binding.descriptorSet = 0; + binding.binding = ShaderRecompiler::RuntimeAbi::FirstImageBinding; + binding.count = 1; + binding.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D; + binding.imageSamplers = {0}; + binding.guestDescriptor = { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (22u << 20u) | (((side - 1u) & 3u) << 30u), + ((side - 1u) >> 2u) | ((side - 1u) << 14u), + 0xfacu | (9u << 28u), + 0u, + 0u, + 0u, + 0u, + }; + program.bindings.push_back(binding); + const CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + bool reusable = false; + std::array proved{}; + std::array reports{}; + { + ShaderResources resources(context, compute); + reusable = resources.Reusable(); + for (std::size_t use = 0; reusable && use < reports.size(); ++use) proved[use] = resources.Revalidate(compute, &reports[use]); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + } + Require(reusable, "a set sampling only a depth surface is not reusable"); + for (std::size_t use = 0; use < reports.size(); ++use) { + const auto name = std::to_string(use + 2); + Require(proved[use], "a set sampling an unchanged depth surface failed its proof on use " + name); + Require(reports[use].path == ShaderResources::ProofPath::Fast, "a set sampling an unchanged depth surface left the fast proof for the full walk on use " + name); + } + const auto resource = DecodeTextureResource(binding.guestDescriptor); + const VkComponentMapping identity{VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY}; + const auto texture = DepthSurfaceTexture(context, binding.guestDescriptor, resource, identity); + Require(texture != nullptr && DepthSurfaceHolds(context, resource, texture.get()), "a 2D depth lookup is not fast-provable"); + auto arrayed = resource; + arrayed.dimension = TextureDimension::k2DArray; + Require(!DepthSurfaceHolds(context, arrayed, texture.get()), "a 2D-array depth lookup took the fast proof instead of the full walk"); + auto layered = resource; + layered.baseArray = 1; + Require(!DepthSurfaceHolds(context, layered, texture.get()), "a layered depth lookup took the fast proof instead of the full walk"); +} + } class SampleProgram { @@ -2813,7 +8444,7 @@ void singleCubeTests(const Device& device, Recorder& recorder) { resource.baseAddress = reinterpret_cast(surface); const std::span snapshot(surface, static_cast(geometry.guestBytes)); Texture texture(context, detiler, resource, identity, snapshot); - auto storage = std::make_shared(context, detiler, resource, 0); + auto storage = StorageTexture::Create(context, detiler, resource, 0); recorder.Keep(storage); Texture storageView(context, storage, resource, identity); for (std::uint32_t face = 0; face < 6; ++face) { @@ -2824,6 +8455,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); @@ -2847,7 +8606,7 @@ void firstLayerViewTests(const Device& device, Recorder& recorder) { auto* surface = reinterpret_cast((reinterpret_cast(memory.data()) + 255) & ~std::uintptr_t{255}); for (std::uint32_t layer = 0; layer < 3; ++layer) std::memset(surface + geometry.GuestLayerOffset(layer), 0x20 * (layer + 1), static_cast(geometry.layerBytes)); resource.baseAddress = reinterpret_cast(surface); - auto image = std::make_shared(withDetiler, detiler, resource, 0); + auto image = StorageTexture::Create(withDetiler, detiler, resource, 0); recorder.Keep(image); SampleProgram program(context, recorder); expectRed(program.Red(image->FirstLayerView(0), VK_IMAGE_LAYOUT_GENERAL, 0.0f), 0x40 / 255.0f, "a first-layer view does not read the BASE_ARRAY layer"); @@ -2868,6 +8627,127 @@ void firstLayerViewTests(const Device& device, Recorder& recorder) { expectRed(program.Red(flatTexture.View(), flatTexture.Layout(), 0.0f), 0x20 / 255.0f, "a 2D texture over the surface does not read its first layer"); } +void sliceDirtyTests(const Device& device, Recorder& recorder) { + const auto& base = device.GetContext(); + constexpr std::uint32_t side = 128; + constexpr std::size_t sliceBytes = 65536; + constexpr std::size_t bytes = 4 * sliceBytes; + constexpr std::uint8_t untouched = 0x11; + for (const auto tileMode : {TextureTileMode::kLinear, TextureTileMode::kR64KBX}) { + const std::string what = tileMode == TextureTileMode::kLinear ? "a linear two-slice image" : "a tiled two-slice image"; + void* block = AllocateWatched(bytes, 65536); + if (block == nullptr) { + std::cout << "no write watching: slice dirty marking not tested\n"; + return; + } + auto* memory = static_cast(block); + std::memset(memory, untouched, bytes); + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + std::uint64_t address; + std::size_t bytes; + ~Unregister() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, address, bytes); + ReleaseWatched(block, bytes); + } + } unregister{base, block, address, bytes}; + AgcDriver::GuestMemory::CollectWritesUncached(address, bytes); + TextureDetiler detiler(base); + auto context = base; + context.hostImportAlignment = 0; + context.detiler = &detiler; + GuestTextureResource resource{}; + resource.baseAddress = address; + resource.width = side; + resource.height = side; + resource.depthOrLastArray = 1; + resource.mipCount = 1; + resource.tileMode = tileMode; + resource.dimension = TextureDimension::k2DArray; + resource.format = 56; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + const auto geometry = DescribeSurface(resource); + Require(geometry.imageLayers == 2 && geometry.layerBytes == sliceBytes && geometry.guestBytes == 2 * sliceBytes, what + " does not hold two 64 KiB slices"); + const auto stored = [&](std::uint32_t slice, std::array color) { + std::vector read(sliceBytes); + AgcDriver::GuestMemory::Read(address + geometry.GuestLayerOffset(slice), read); + for (std::size_t at = 0; at < read.size(); ++at) { + if (std::to_integer(read[at]) != color[at % 4]) return false; + } + return true; + }; + { + auto image = StorageTexture::Create(context, detiler, resource, 0); + const auto commands = recorder.Commands(); + recorder.Keep(image); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + const VkClearColorValue red{{1.0f, 0.0f, 0.0f, 1.0f}}; + const VkClearColorValue green{{0.0f, 1.0f, 0.0f, 1.0f}}; + const VkImageSubresourceRange first{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + const VkImageSubresourceRange second{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 1, 1}; + context.Function("vkCmdClearColorImage")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, &red, 1, &first); + context.Function("vkCmdClearColorImage")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, &green, 1, &second); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + image->MarkSliceDirty(0); + for (std::uint32_t slice = 0; slice < 2; ++slice) { + const auto display = address + geometry.GuestLayerOffset(slice); + const auto found = StorageTexture::FindPendingSlice(display, sliceBytes); + Require(found == image, what + ": a display buffer that is slice " + std::to_string(slice) + " does not find the pending image"); + Require(AgcDriver::ResidentPresentSlice(found->Descriptor().baseAddress, found->GuestBytes(), found->ImageLayers(), display, sliceBytes) == slice, what + ": a display buffer that is slice " + std::to_string(slice) + " is not presented from that layer"); + } + Require(StorageTexture::FindPending(address + geometry.GuestLayerOffset(1), sliceBytes) == nullptr, what + ": the lookup by base address finds a display buffer in slice 1"); + Require(StorageTexture::FindPendingSlice(address + 2 * sliceBytes, sliceBytes) == nullptr && StorageTexture::FindPendingSlice(address + geometry.GuestLayerOffset(1), sliceBytes + 1) == nullptr, what + ": a range past the image's last slice finds the pending image"); + image->WriteBack(); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + Require(stored(0, {255, 0, 0, 255}), what + ": the slice a draw wrote was not stored"); + Require(stored(1, {untouched, untouched, untouched, untouched}), what + ": a slice no draw wrote was stored over guest memory"); + static_cast(recorder.Commands()); + recorder.Keep(image); + image->MarkSliceDirty(1); + image->WriteBack(); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + Require(stored(0, {255, 0, 0, 255}) && stored(1, {0, 255, 0, 255}), what + ": the second slice was not stored once a draw wrote it"); + auto other = StorageTexture::Create(context, detiler, resource, 0); + static_cast(recorder.Commands()); + recorder.Keep(image); + recorder.Keep(other); + image->MarkSliceDirty(1); + other->MarkSliceDirty(1); + std::string ambiguous; + try { + static_cast(StorageTexture::FindPendingSlice(address + geometry.GuestLayerOffset(1), sliceBytes)); + } catch (const std::runtime_error& error) { + ambiguous = error.what(); + } + Require(ambiguous.find("is a slice of the pending array images") != std::string::npos, what + ": a slice of two pending array images did not throw: " + ambiguous); + image->WriteBack(); + other->WriteBack(); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + } + recorder.Sync(); + } + std::cout << "slice dirty marking: ok\n"; +} + void atomicViewTests(const Device& device, Recorder& recorder) { const auto& context = device.GetContext(); TextureDetiler detiler(context); @@ -2888,7 +8768,7 @@ void atomicViewTests(const Device& device, Recorder& recorder) { const auto geometry = DescribeSurface(resource); std::vector memory(static_cast(geometry.guestBytes) + 256); resource.baseAddress = (reinterpret_cast(memory.data()) + 255) & ~std::uintptr_t{255}; - auto image = std::make_shared(withDetiler, detiler, resource, 0); + auto image = StorageTexture::Create(withDetiler, detiler, resource, 0); recorder.Keep(image); if (format == 56u) { bool refused = false; @@ -2906,6 +8786,165 @@ void atomicViewTests(const Device& device, Recorder& recorder) { } } +bool vertexTargetRangeTests(const Device& device, Recorder& recorder, bool imported) { + if (!device.Graphics() || (imported && device.GetContext().hostImportAlignment == 0)) return false; + constexpr std::size_t unit = 65536; + constexpr std::size_t bytes = unit * 3; + void* watched = AllocateWatched(bytes, unit); + std::vector fallback(watched == nullptr ? bytes + unit : 0); + auto* block = watched != nullptr ? static_cast(watched) : reinterpret_cast((reinterpret_cast(fallback.data()) + unit - 1) & ~(unit - 1)); + struct Release { + void* watched; + ~Release() { if (watched != nullptr) ReleaseWatched(watched, bytes); } + } release{watched}; + std::fill_n(block, bytes, std::byte{0}); + const auto address = reinterpret_cast(block); + auto context = device.GetContext(); + context.recorder = &recorder; + if (!imported) { + context.hostImportAlignment = 0; + context.dmaBufImport = false; + } + TextureDetiler detiler(context); + context.detiler = &detiler; + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Cleanup { + const Context& context; + Recorder& recorder; + void* block; + ~Cleanup() { + recorder.Sync(); + WaitForOptimizedPipelines(context.device); + SharedResourceCache().Clear(); + ClearCachedTextures(context.device); + recorder.Sync(); + ClearCachedPipelines(context.device); + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, reinterpret_cast(block), bytes); + } + } cleanup{context, recorder, block}; + if (imported && HostImportFor(context, address, bytes) == nullptr) return false; + const auto shader = [](auto& words, std::uint64_t variant) { + ShaderRecompiler::RecompileResult program; + program.spirv = std::vector(std::begin(words), std::end(words)); + program.variantId = variant; + return program; + }; + auto producerVertex = shader(COVER_VERT_SPV, 101); + auto producerPixel = shader(VERTEXRANGEPRODUCER_FRAG_SPV, 102); + auto vertex = shader(VERTEXRANGE_VERT_SPV, 103); + auto pixel = shader(COVER_FRAG_SPV, 104); + const std::array producerShaders{CompiledShader{ShaderRecompiler::ShaderStage::Vertex, &producerVertex, 0}, CompiledShader{ShaderRecompiler::ShaderStage::Fragment, &producerPixel, 0}}; + const std::array shaders{CompiledShader{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, CompiledShader{ShaderRecompiler::ShaderStage::Fragment, &pixel, 0}}; + const auto stateFor = [](std::uint64_t base, VkFormat format, std::uint32_t elementBytes) { + State state{}; + state.stages.path = ShaderPath::Vertex; + state.color = {base, {64, 64}, format, unit, 0xe4u, ColorTileMode::RenderTarget, elementBytes}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {64, 64}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, 64, 64, -64, 0, 1}; + state.scissor = {{0, 0}, {64, 64}}; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + return state; + }; + const auto producer = stateFor(address + unit, VK_FORMAT_R32G32B32A32_SFLOAT, 16); + const auto consumer = stateFor(address, VK_FORMAT_R8G8B8A8_UNORM, 4); + const AgcDriver::Pm4::DrawParameters triangle{0, 3, 0, 1, 0, false}; + const auto produce = [&] { + Draw(context, producer, triangle, producerShaders); + WaitForOptimizedPipelines(context.device); + const std::array range{std::pair{address + unit, address + 2 * unit}}; + Require(StorageTexture::AnyPendingOverlaps(range), "the color producer did not leave GPU results pending"); + }; + const auto attributeFor = [](std::uint64_t base, std::uint32_t stride, std::uint32_t fetchIndex) { + return ShaderRecompiler::VertexAttribute{0, 4, {{static_cast(base), static_cast(base >> 32u) | (stride << 16u), static_cast(bytes / 16), 77u << 12u}}, fetchIndex}; + }; + const auto check = [&](std::uint32_t edge) { + std::vector texels(64 * 64 * 4); + ReadColorTarget(consumer.color, texels); + for (std::size_t y = 0; y < 64; ++y) { + for (std::size_t x = 0; x < 64; ++x) { + const auto expected = x < edge ? std::byte{0} : std::byte{255}; + for (std::size_t channel = 0; channel < 4; ++channel) Require(texels[(y * 64 + x) * 4 + channel] == expected, "the draw did not consume the GPU-produced vertex offset"); + } + } + }; + const auto reject = [&](auto action, const char* expected) { + try { action(); } + catch (const std::runtime_error& error) { + Require(std::string(error.what()).find(expected) != std::string::npos, error.what()); + return; + } + throw std::runtime_error("an actual render-target alias was accepted"); + }; + for (const std::uint32_t fetchIndex : {0u, 1u}) { + producerPixel = shader(VERTEXRANGEPRODUCER_FRAG_SPV, 102); + vertex.vertexAttributes = {attributeFor(address, 16, fetchIndex)}; + auto draw = triangle; + if (fetchIndex == 0) draw.firstVertex = unit / 16; + else draw.firstInstance = unit / 16; + produce(); + std::shared_ptr recipe; + Draw(context, consumer, draw, shaders, {}, &recipe); + WaitForOptimizedPipelines(context.device); + const auto read = recorder.DescribePendingRead(address + unit, 16); + Require(read.has_value() == imported && (!read || read->kind == Recorder::ReadKind::DrawInput || 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); + AgcDriver::GuestMemory::Write(address, cleared); + for (std::size_t offset = 5; offset < producerPixel.spirv.size();) { + const auto count = producerPixel.spirv[offset] >> 16u; + Require(count != 0, "invalid producer SPIR-V"); + if ((producerPixel.spirv[offset] & 0xffffu) == 43u && count == 4 && producerPixel.spirv[offset + 3] == 0x3f000000u) producerPixel.spirv[offset + 3] = 0x3f400000u; + offset += count; + } + producerPixel.variantId = 105; + produce(); + const auto hit = DrawWithRecipe(context, consumer, draw, shaders, {}, *recipe); + WaitForOptimizedPipelines(context.device); + Require(hit.recorded, "the vertex range regression did not reuse its draw recipe"); + check(24); + vertex.vertexAttributes = {attributeFor(address + unit, 16, fetchIndex)}; + Draw(context, consumer, triangle, shaders); + WaitForOptimizedPipelines(context.device); + check(24); + vertex.vertexAttributes = {attributeFor(address, 16, fetchIndex)}; + auto indirect = draw; + indirect.indirect.emplace(); + reject([&] { Draw(context, consumer, indirect, shaders); }, "vertex buffer aliases the render target"); + if (fetchIndex == 0) { + const std::array indices{0, 1, 2}; + AgcDriver::GuestMemory::Write(address + 2 * unit, std::as_bytes(std::span(indices))); + auto indexed = draw; + indexed.indexed = true; + indexed.indexSize = 2; + indexed.indexAddress = address + 2 * unit; + produce(); + Draw(context, consumer, indexed, shaders); + WaitForOptimizedPipelines(context.device); + check(24); + indexed.indexAddress = address; + reject([&] { Draw(context, consumer, indexed, shaders); }, "index buffer aliases the render target"); + } + reject([&] { Draw(context, consumer, triangle, shaders); }, "vertex buffer aliases the render target"); + vertex.vertexAttributes = {attributeFor(address, 0, fetchIndex)}; + reject([&] { Draw(context, consumer, draw, shaders); }, "vertex buffer aliases the render target"); + } + std::cout << "Vertex range and pending color producer tests passed (" << (imported ? "imported" : "copied") << ")\n"; + return true; +} + void pipelineCacheTests(const Device& device, bool benchmark) { std::thread establishThreadedProcess([] {}); establishThreadedProcess.join(); @@ -2928,7 +8967,7 @@ void pipelineCacheTests(const Device& device, bool benchmark) { state.colors.push_back(state.color); state.blend.colorWriteMask = 15; state.blends.push_back(state.blend); - ShaderResources resources(context, shaders, state.color, 0, 0); + ShaderResources resources(context, shaders, state.color, static_cast(state.colors.size()), 0, 0); const auto lookup = [&](const VertexInputLayout& input) { return CachedPipeline(context, state, input, resources, shaders, VK_IMAGE_LAYOUT_GENERAL); }; @@ -2938,8 +8977,8 @@ void pipelineCacheTests(const Device& device, bool benchmark) { state.cullMode = VK_CULL_MODE_BACK_BIT; Require(lookup(input) != first, "pipeline cache ignored changed raster state"); state.cullMode = 0; - for (const auto attributes : {0u, 8u, 32u}) { - Require(attributes <= context.limits.maxVertexInputBindings && attributes <= context.limits.maxVertexInputAttributes, "pipeline cache fixture exceeds vertex input limits"); + const auto largest = std::min({32u, context.limits.maxVertexInputBindings, context.limits.maxVertexInputAttributes}); + for (const auto attributes : {0u, 8u, largest}) { input.bindings.clear(); input.attributes.clear(); for (std::uint32_t i = 0; i < attributes; ++i) { @@ -2962,15 +9001,247 @@ 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}; + const std::array key{binding.binding, binding.descriptorType, binding.descriptorCount, binding.stageFlags}; + const auto layout = cache.Layout(key, std::span(&binding, 1)); + const VkDescriptorPoolSize size{binding.descriptorType, binding.descriptorCount}; + const auto held = cache.Allocate(layout, std::span(&size, 1)); + std::uint64_t initialPools = 0; + for (unsigned round = 0; round < 3; ++round) { + std::vector sets; + for (unsigned i = 0; i < 1100; ++i) { + const auto allocation = cache.Allocate(layout, std::span(&size, 1)); + Require(allocation.set != VK_NULL_HANDLE, "descriptor pool rollover refused a valid layout"); + sets.push_back(allocation); + } + const auto pools = cache.Counters().pools; + if (round == 0) { + initialPools = pools; + } else { + Require(pools <= initialPools + 1, "descriptor pool recycling grows behind one retained set"); + } + for (const auto& allocation : sets) cache.Free(allocation); + } + cache.Free(held); + std::cout << "Descriptor pool recycling passed across 3,301 sets (" << initialPools << " initial pools, " << cache.Counters().pools << " final pools)\n"; +} + void keysFillTests(const Device& device, Recorder& recorder) { const auto& base = device.GetContext(); if (base.hostImportAlignment == 0) { @@ -2995,7 +9266,7 @@ void keysFillTests(const Device& device, Recorder& recorder) { const auto keysAddress = address + surfaceBytes; { GuestAllocations::Mutation mutation; - mutation.Add(block, bytes, true, true); + mutation.Add(block, bytes, true, true, true); } struct Unregister { const Context& context; @@ -3031,7 +9302,7 @@ void keysFillTests(const Device& device, Recorder& recorder) { resource.dccAddress = keysAddress; Require(DescribeSurface(resource).guestBytes == surfaceBytes, "the key fill surface has an unexpected size"); { - auto image = std::make_shared(context, detiler, resource, 0); + auto image = StorageTexture::Create(context, detiler, resource, 0); const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; const auto draw = [&](VkClearColorValue value) { const auto commands = recorder.Commands(); @@ -3087,23 +9358,764 @@ void keysFillTests(const Device& device, Recorder& recorder) { recorder.Sync(); } +void integerClearTests(const Device& device) { + const auto& context = device.GetContext(); + std::size_t tested = 0; + for (const std::uint32_t format : {5u, 11u, 20u}) { + if (!StorageFormatAvailable(context, format)) continue; + ++tested; + Require(StorageClearAvailable(context, format, DccKeys::Clear0000), "an integer storage format refused the 0000 clear"); + for (const auto keys : {DccKeys::Clear0001, DccKeys::Clear1111, DccKeys::ClearRegister, DccKeys::Uncompressed}) { + Require(!StorageClearAvailable(context, format, keys), "an integer storage format took a clear code other than 0000"); + } + } + Require(tested == 3, "an integer storage format has no storage form"); + std::cout << "integer storage clears: " << tested << " formats ok\n"; +} + +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) { + std::cout << "host imports unavailable: denormal fill clears not tested\n"; + return; + } + constexpr std::uint32_t side = 64; + constexpr std::size_t bytes = 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 denormal clear block"); + std::memset(block, 0x55, bytes); + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + std::uint64_t address; + ~Unregister() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, address, bytes); + } + } unregister{base, block, address}; + if (HostImportFor(base, address, bytes) == nullptr) { + std::cout << "host import of the denormal clear block refused: denormal fill clears not tested\n"; + return; + } + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + GuestTextureResource resource{}; + resource.baseAddress = address; + resource.width = side; + resource.height = side; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kR64KBX; + resource.dimension = TextureDimension::k2D; + resource.format = 29; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + Require(ResolveTextureFormat(resource.format) == VK_FORMAT_R16G16_SFLOAT, "guest format 29 is not R16G16_SFLOAT"); + const bool keeps = ClearKeepsDenormals(context, VK_FORMAT_R16G16_SFLOAT); + std::cout << "denormal half clears are " << (keeps ? "kept" : "flushed") << " by this device\n"; + Require(ClearKeepsDenormals(context, VK_FORMAT_R16G16_SFLOAT) == keeps, "the denormal clear probe changed its answer"); + Require(!ClearKeepsDenormals(context, VK_FORMAT_R32G32B32A32_SFLOAT), "a format without small floats probed as keeping denormals"); + { + auto image = StorageTexture::Create(context, detiler, resource, 0); + const auto holds = [&](std::uint32_t texel) { + Buffer readback(context, side * side * 4, 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 = {side, side, 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(); + for (std::size_t at = 0; at + 4 <= pixels.size(); at += 4) { + std::uint32_t value = 0; + std::memcpy(&value, pixels.data() + at, 4); + if (value != texel) return false; + } + return true; + }; + const char* refusal = nullptr; + const std::array normal{0x3c00bc00u, 0x3c00bc00u, 0x3c00bc00u, 0x3c00bc00u}; + Require(image->FillClear(normal, StorageTexture::WholeImage, refusal), "a normal half pattern was refused"); + Require(holds(0x3c00bc00u), "a normal half fill clear stored other bits"); + const std::array denormal{0x83ff0001u, 0x83ff0001u, 0x83ff0001u, 0x83ff0001u}; + refusal = nullptr; + const bool cleared = image->FillClear(denormal, StorageTexture::WholeImage, refusal); + Require(cleared == keeps, keeps ? "a denormal half pattern was refused on a device whose clears keep denormals" : "a denormal half pattern was cleared on a device whose clears flush them"); + if (cleared) Require(holds(0x83ff0001u), "a denormal half fill clear stored other bits"); + const std::array nan{0x7e007e00u, 0x7e007e00u, 0x7e007e00u, 0x7e007e00u}; + refusal = nullptr; + Require(!image->FillClear(nan, StorageTexture::WholeImage, refusal), "a NaN half pattern was cleared"); + } + recorder.Sync(); +} + +void pipelineLibraryTests(const Device& device) { + std::lock_guard gpu(GpuMutex()); + const auto& context = device.GetContext(); + struct Cleanup { + VkDevice device; + ~Cleanup() { ClearCachedPipelines(device); } + } cleanup{context.device}; + ClearCachedPipelines(context.device); + ShaderRecompiler::RecompileResult vertex, fragment; + vertex.variantId = 11; + fragment.variantId = 12; + vertex.spirv = std::vector(std::begin(TRIANGLE_vert_SPV), std::end(TRIANGLE_vert_SPV)); + fragment.spirv = std::vector(std::begin(TRIANGLE_frag_SPV), std::end(TRIANGLE_frag_SPV)); + const std::array shaders{CompiledShader{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, CompiledShader{ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}; + State state{}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.hasColorTarget = true; + state.color.format = VK_FORMAT_R8G8B8A8_UNORM; + state.colors.push_back(state.color); + state.blend.colorWriteMask = 15; + state.blends.push_back(state.blend); + ShaderResources resources(context, shaders, state.color, static_cast(state.colors.size()), 0, 0); + VertexInputLayout input; + const auto lookup = [&] { return CachedPipeline(context, state, input, resources, shaders, VK_IMAGE_LAYOUT_GENERAL); }; + const auto counters = [&] { return PipelineLibraryCountersOf(context.device); }; + const auto first = lookup(); + if (!context.graphicsPipelineLibrary) { + Require(counters().linked == 0, "a pipeline was linked from libraries without VK_EXT_graphics_pipeline_library"); + std::cout << "graphics pipeline libraries unavailable or disabled: pipelines are created whole\n"; + return; + } + const auto built = counters(); + Require(built.linked == 1 && built.built == std::array{1, 1, 1, 1}, "the first pipeline was not linked from four new libraries"); + state.blends.front().blendEnable = VK_TRUE; + state.blends.front().srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; + state.blends.front().dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + const auto blended = lookup(); + Require(blended != first && counters().built == std::array{1, 1, 1, 2}, "a blend change rebuilt more than the fragment output library"); + state.cullMode = VK_CULL_MODE_BACK_BIT; + state.frontFace = VK_FRONT_FACE_CLOCKWISE; + const auto culled = lookup(); + Require(culled != blended && counters().built == std::array{1, 1, 1, 2}, "a cull mode change rebuilt a library instead of setting dynamic state"); + input.bindings.push_back({0, 16, VK_VERTEX_INPUT_RATE_VERTEX}); + input.attributes.push_back({0, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 0}); + static_cast(lookup()); + Require(counters().built == std::array{2, 1, 1, 2} && counters().linked == 4, "a vertex input change rebuilt more than the vertex input library"); + 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.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.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; + static_cast(lookup()); + Require(counters().built == std::array{2, 2, 3, 3}, "another target format rebuilt more than the fragment output library"); + state.samples = 4; + state.colors.front().samples = 4; + const auto multisampled = lookup(); + Require(counters().built == std::array{2, 2, 4, 4}, "a 4x pipeline shared the 1x fragment shader or fragment output library"); + state.samples = 1; + state.colors.front().samples = 1; + Require(lookup() != multisampled && counters().built == std::array{2, 2, 4, 4}, "the 1x pipeline after a 4x one built its libraries again"); + state.depth = DepthTarget{0, 0, {64, 64}, VK_FORMAT_D32_SFLOAT, 1.0f, 0}; + state.depthTest = true; + state.depthCompare = VK_COMPARE_OP_LESS; + static_cast(lookup()); + Require(counters().built == std::array{2, 3, 5, 5}, "a depth target shared the shader libraries built with no depth format"); + state.depth.reset(); + state.depthTest = false; + if ((context.sampleLocationSampleCounts & VK_SAMPLE_COUNT_4_BIT) != 0) { + state.samples = 4; + state.colors.front().samples = 4; + state.customSampleLocations = true; + state.sampleLocations = {0x10, 0x21, 0x32, 0x43}; + const auto pattern = lookup(); + const auto beforePatternChange = counters().built; + state.sampleLocations[0] = 0x20; + const auto otherPattern = lookup(); + 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; + for (const auto format : {VK_FORMAT_D32_SFLOAT, VK_FORMAT_D16_UNORM, VK_FORMAT_D24_UNORM_S8_UINT, VK_FORMAT_D32_SFLOAT_S8_UINT}) { + state.depth = DepthTarget{}; + state.depth->format = format; + state.depth->samples = state.samples; + (ValidateSampleLocations(context, state) ? compatibleDepth : incompatibleDepth) = format; + } + if (compatibleDepth && incompatibleDepth) { + state.depth->format = *compatibleDepth; + const auto compatible = lookup(); + const auto beforeDepthChange = counters().built; + state.depth->format = *incompatibleDepth; + const auto incompatible = lookup(); + Require(incompatible != compatible && counters().built[2] == beforeDepthChange[2] + 1u && counters().built[3] == beforeDepthChange[3] + 1u, "depth-format sample-location enablement did not rebuild both fragment libraries"); + } + state.customSampleLocations = true; + } + WaitForOptimizedPipelines(context.device); + const auto lto = std::getenv("APS5_NO_PIPELINE_LTO") == nullptr && context.pipelineLinkTimeOptimization; + Require(counters().optimized == (lto ? counters().linked : 0u), "a linked pipeline was not optimized in the background"); + Require(first->Optimized() == lto && culled->Optimized() == lto, "a pipeline did not switch to its optimized link"); + ClearCachedPipelines(context.device); + Require(counters().linked == 0, "clearing the pipelines kept the device's libraries"); + std::cout << "Pipeline library reuse tests passed\n"; +} + +#ifdef _WIN32 +namespace StorageCpuSnapshotTest { +constexpr std::uint32_t Side = 256; +constexpr std::uint32_t Format32UInt = 20; +constexpr std::uint32_t TileR64KBX = 0x1b; +constexpr std::uint32_t Type2D = 9; +constexpr std::size_t SurfaceBytes = std::size_t{Side} * Side * 4u; +constexpr std::size_t BlockBytes = 12 * SurfaceBytes; + +class Block { +public: + explicit Block(const Context& context, bool watched = false) : context(context), watched(watched) { + if (watched) { + memory = GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536); + const auto section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, 0, BlockBytes, nullptr); + Require(section != nullptr, "cannot allocate the shared texture section"); + try { GuestArena::GuestArenaMap_nid_postfix(memory, BlockBytes, section, 0, PAGE_READWRITE); } + catch (...) { CloseHandle(section); throw; } + CloseHandle(section); + } else memory = VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); + Require(memory != nullptr, "cannot allocate the texture block"); + std::memset(memory, 0x5a, BlockBytes); + GuestAllocations::Mutation().Add(memory, BlockBytes, true, true, true); + } + + ~Block() { + ClearCachedTextures(context.device); + GuestAllocations::Mutation().Remove(memory); + HostImportFor(context, Surface(0), BlockBytes); + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(memory, BlockBytes); + GuestArena::GuestArenaRelease_nid_postfix(memory, BlockBytes); + } else VirtualFree(memory, 0, MEM_RELEASE); + } + + std::uint64_t Surface(std::size_t surface) const { return reinterpret_cast(memory) + surface * SurfaceBytes; } + +private: + const Context& context; + bool watched; + void* memory = nullptr; +}; + +std::array Descriptor(std::uint64_t address, std::uint32_t side = Side) { + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((side - 1u) & 3u) << 30u), + ((side - 1u) >> 2u) | ((side - 1u) << 14u), + 0xfacu | (TileR64KBX << 20u) | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +GuestTextureResource Resource(std::uint64_t address, std::uint32_t side = Side) { + const auto words = Descriptor(address, side); + return DecodeTextureResource(words); +} + +void Run(Context context) { + TextureDetiler detiler(context); + context.detiler = &detiler; + for (const bool watched : {true, false}) { + Block block(context, watched); + const auto address = block.Surface(0); + Require(AgcDriver::GuestMemory::Watched(address, SurfaceBytes) == watched, "snapshot fixture has the wrong write tracking"); + Require(HostImportFor(context, address, SurfaceBytes) != nullptr, "snapshot fixture requires a host import"); + Require(AgcDriver::GuestMemory::Watched(address, SurfaceBytes) == watched, "host import changed the fixture's write tracking"); + auto image = StorageTexture::Create(context, *context.detiler, Resource(address), 0); + const auto expectedBytes = watched ? 0u : SurfaceBytes; + Require(image->OriginalSnapshotBytes() == expectedBytes, "watched GPU-direct upload retained an unnecessary CPU surface snapshot"); + const auto readPixel = [&] { + Buffer result(context, 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + CommandBatch batch(context); + RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {1, 1, 1}; + context.Function("vkCmdCopyImageToBuffer")(batch.Handle(), image->Image(), VK_IMAGE_LAYOUT_GENERAL, result.Handle(), 1, ©); + RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + batch.SubmitAndWait(); + result.Invalidate(); + std::uint32_t pixel = 0; + std::memcpy(&pixel, result.Bytes().data(), sizeof(pixel)); + return pixel; + }; + Require(readPixel() == 0x5a5a5a5au, "snapshot upload returned incorrect GPU texels"); + std::memset(reinterpret_cast(address), 0x6b, SurfaceBytes); + image->Refresh(); + Require(readPixel() == 0x6b6b6b6bu, "snapshot refresh missed a CPU write"); + Require(image->OriginalSnapshotBytes() == expectedBytes, "watched refresh allocated a CPU surface snapshot"); + { + CommandBatch batch(context); + RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + VkClearColorValue clear{}; + clear.uint32[0] = 0x7c7c7c7cu; + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Function("vkCmdClearColorImage")(batch.Handle(), image->Image(), VK_IMAGE_LAYOUT_GENERAL, &clear, 1, &range); + batch.SubmitAndWait(); + } + image->MarkDirty(); + image->WriteBack(); + std::uint32_t pixel = 0; + AgcDriver::GuestMemory::ReadCommitted(address, std::as_writable_bytes(std::span(&pixel, 1))); + Require(pixel == 0x7c7c7c7cu, "snapshot write-back lost GPU texels"); + Require(image->OriginalSnapshotBytes() == expectedBytes, "GPU-direct write-back allocated a CPU surface snapshot"); + if (watched) { + AgcDriver::GuestMemory::Unwatch(address, SurfaceBytes); + Require(!AgcDriver::GuestMemory::Watched(address, SurfaceBytes), "snapshot fixture is still write-watched"); + std::memset(reinterpret_cast(address), 0x8d, SurfaceBytes); + image->Refresh(); + Require(image->OriginalSnapshotBytes() == SurfaceBytes, "unwatched refresh did not allocate its comparison snapshot"); + Require(readPixel() == 0x8d8d8d8du, "unwatched refresh returned incorrect GPU texels"); + } + image.reset(); + Context cpu = context; + cpu.hostImportAlignment = 0; + image = StorageTexture::Create(cpu, *context.detiler, Resource(address), 0); + Require(image->OriginalSnapshotBytes() == SurfaceBytes, "CPU upload has no guest-byte snapshot"); + const auto expectedPixel = watched ? 0x8d8d8d8du : 0x7c7c7c7cu; + Require(readPixel() == expectedPixel, "CPU upload returned incorrect GPU texels"); + { + CommandBatch batch(context); + VkClearColorValue clear{}; + clear.uint32[0] = 0x9e9e9e9eu; + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function("vkCmdClearColorImage")(batch.Handle(), image->Image(), VK_IMAGE_LAYOUT_GENERAL, &clear, 1, &range); + batch.SubmitAndWait(); + } + image->MarkDirty(); + image->WriteBack(); + AgcDriver::GuestMemory::ReadCommitted(address, std::as_writable_bytes(std::span(&pixel, 1))); + Require(pixel == 0x9e9e9e9eu, "CPU write-back lost guest texels"); + std::printf("%s snapshot fixture: direct %zu bytes, CPU fallback %zu bytes\n", watched ? "watched" : "unwatched", expectedBytes, image->OriginalSnapshotBytes()); + } +} +} +#endif + int main(int argc, char** argv) { try { - Device device; + 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"; + return 0; + } + std::unique_ptr created; + try { + created = std::make_unique(argc == 2 && (std::string_view(argv[1]) == "--import-watch-other-family-only" || std::string_view(argv[1]) == "--deferred-settle-other-family-only")); + } catch (const std::exception& error) { + if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw; + std::cout << "skipped, no usable Vulkan device: " << error.what() << '\n'; + return 77; + } + Device& device = *created; + if (argc == 2 && std::string_view(argv[1]) == "--host-import-lifetime-only") return RunHostImportLifetimeTests(device.GetContext()); + if (argc == 2 && std::string_view(argv[1]) == "--host-import-mutation-race-only") return RunHostImportMutationRaceTests(device.GetContext()); + if (argc == 2 && (std::string_view(argv[1]) == "--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]) == "--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]) == "--trimmed-import-only") return trimmedImportTests(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) { + std::cout << "skipped, host import retirement requires host imports aligned within 64 KiB; alignment=" << alignment << '\n'; + return 77; + } + hostImportRetirementTests(device); + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--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; + } if (argc == 2 && (std::string_view(argv[1]) == "--benchmark-pipeline-cache" || std::string_view(argv[1]) == "--pipeline-cache-only")) { pipelineCacheTests(device, std::string_view(argv[1]) == "--benchmark-pipeline-cache"); return 0; } - std::lock_guard gpu(GpuMutex()); + 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; + } + std::unique_lock gpu(GpuMutex()); +#ifdef _WIN32 + if (argc == 2 && std::string_view(argv[1]) == "--storage-cpu-snapshot-only") { + if (device.GetContext().hostImportAlignment == 0) { + Require(std::getenv("ANYPS5_REQUIRE_VULKAN") == nullptr, "storage snapshot test requires host imports"); + std::cout << "skipped, host imports unavailable\n"; + return 77; + } + StorageCpuSnapshotTest::Run(device.GetContext()); + std::cout << "Storage CPU snapshot tests passed\n"; + return 0; + } +#endif std::cout << "host imports " << (PrepareImportWatch(device.GetContext()) == ImportWatch::Unwatch ? "are compared" : "stay watched") << '\n'; Recorder recorder(device.GetContext()); recorder.Activate(); + if (argc == 2 && std::string_view(argv[1]) == "--copied-direct-only") { + copiedDirectOrderingTests(device, recorder); + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--pending-write-snapshot-only") { + pendingWriteSnapshotTests(device, recorder); + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--benchmark-pending-write-snapshot") { + pendingWriteSnapshotBenchmark(recorder); + return 0; + } + if (argc == 2 && (std::string_view(argv[1]) == "--storage-upload-lifetime-only" || std::string_view(argv[1]) == "--storage-upload-failure-only")) { + storageUploadLifetimeTests(device, recorder, std::string_view(argv[1]) == "--storage-upload-failure-only"); + return 0; + } + 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; + } + if (argc == 2 && std::string_view(argv[1]) == "--settled-baseline-only") { + settledBaselineTests(device, recorder); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--completion-labels-only") { completionCountTests(device, recorder); afterRecordedWorkTests(device, recorder); @@ -3114,7 +10126,181 @@ 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); + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--slice-dirty-only") { + sliceDirtyTests(device, recorder); + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--resident-present-only") { + RunResidentPresentTests(device.GetContext()); + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--parked-import-unmap-only") { + gpu.unlock(); + parkedImportUnmapTests(device); + 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; + } + 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") { + cmaskPassTests(device, recorder); + 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"; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--pending-key-stores-only") { + if (device.GetContext().hostImportAlignment == 0) { + std::cout << "skipped, host imports are unavailable\n"; + return 77; + } + 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"; + return 77; + } + std::cout << "DCC keys under unit shadows tests passed\n"; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--draw-inputs-in-place-only") { + drawInputInPlaceTests(device, recorder); + std::cout << "In-place draw input tests passed\n"; + 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; + } + +#ifdef _WIN32 + if (argc == 2 && std::string_view(argv[1]) == "--host-import-second-mapping-only") { + secondMappingImportTests(device); + return 0; + } +#endif readTrackingTests(device, recorder); + readListTests(device, recorder); + overlappedRangeTests(recorder); + spaceWriteSyncTests(device, recorder); writeSettledTests(device, recorder); completionCountTests(device, recorder); afterRecordedWorkTests(device, recorder); @@ -3126,35 +10312,78 @@ int main(int argc, char** argv) { unchangedSinceTests(); closeRaceTests(device, recorder); keyProofTests(device, recorder); + targetKeyProofTests(device, recorder); + refreshProofTests(device, recorder); resourceReadTests(device, recorder); + 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()); + sliceDirtyTests(device, recorder); storeRunTests(device, recorder); + queuedWriteGroupTests(); remappedImportTests(device); +#ifdef _WIN32 + secondMappingImportTests(device); +#endif 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); storageRefreshTests(device, recorder, false); storageRefreshTests(device, recorder, true); + 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); importWindowTests(device, recorder); dataWordPositionsTests(); dataRefreshTests(device, recorder); + 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); pendingKeyStoreTests(device, recorder); + shadowedKeyStoreTests(device, recorder); sampleDumpTests(device, recorder); + copiedDirectOrderingTests(device, recorder); + gpu.unlock(); + hostImportUnmapTests(device, recorder); + drawSnapshotPatchTests(device); + drawSnapshotEpochTests(device); + gpu.lock(); + recorder.Activate(); std::cout << "Recorder read tracking and label tests passed\n"; return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/RecorderKeptBytes.cpp b/core/libs/prx/libSceAgcDriver/tests/RecorderKeptBytes.cpp index a8d3425634..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)}, @@ -194,6 +216,90 @@ void CountFollowsEveryPath() { Expect(recorder.Reap() && recorder.InFlightKeptBytes() == 0, "a reaped batch still counts " + std::to_string(recorder.InFlightKeptBytes()) + " kept bytes in flight"); } +void SharedOwnerCountsOncePerBatch() { + mock = MockDevice{}; + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Recorder recorder(mockContext()); + auto shared = std::make_shared(0); + for (int keep = 0; keep < 3; ++keep) { + recorder.Keep(shared); + recorder.KeepBytes(shared.get(), Budget / 2); + recorder.BoundKeptBytes(); + } + Expect(recorder.Recording() && recorder.OpenKeptBytes() == Budget / 2 && mock.submits == 0, "three keeps of one owner's half budget counted " + std::to_string(recorder.OpenKeptBytes()) + " kept bytes, not half the budget once"); + auto other = std::make_shared(1); + recorder.Keep(other); + recorder.KeepBytes(other.get(), Budget / 2); + recorder.BoundKeptBytes(); + Expect(!recorder.Recording() && recorder.Submissions() == 1 && recorder.InFlightKeptBytes() == Budget, "a second owner's half budget did not complete the batch's budget"); + recorder.Keep(shared); + recorder.KeepBytes(shared.get(), Budget / 2); + Expect(recorder.OpenKeptBytes() == Budget / 2, "the next batch did not count an owner the submitted batch counted"); + 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() { @@ -202,6 +308,13 @@ int main() { InFlightWithinTwiceTheBudget(); 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 new file mode 100644 index 0000000000..a14108fb90 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/RectListPositionInvariant.cpp @@ -0,0 +1,226 @@ +#include "Recompiler.hpp" +#include +#include +#include +#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); +} + +bool PositionIsInvariant(std::span words) { + Check(words.size() >= 5 && words[0] == spv::MagicNumber, "rect-list invariant: missing SPIR-V module"); + std::vector> positionMembers; + std::vector> invariantMembers; + 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 invariant: malformed SPIR-V instruction"); + const auto opcode = words[offset] & spv::OpCodeMask; + if (opcode == spv::OpMemberDecorate && count == 5 && words[offset + 3] == spv::DecorationBuiltIn && words[offset + 4] == spv::BuiltInPosition) { + positionMembers.emplace_back(words[offset + 1], words[offset + 2]); + } + if (opcode == spv::OpMemberDecorate && count == 4 && words[offset + 3] == spv::DecorationInvariant) { + invariantMembers.emplace_back(words[offset + 1], words[offset + 2]); + } + offset += count; + } + Check(!positionMembers.empty(), "rect-list invariant: shader has no gl_Position"); + for (const auto& member : positionMembers) { + if (std::find(invariantMembers.begin(), invariantMembers.end(), member) == invariantMembers.end()) return false; + } + 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() { + try { + static constexpr std::array capabilities{spv::CapabilityShader, spv::CapabilityTessellation}; + SpirvTarget target{}; + target.vulkanVersion = 0x00401000u; + target.spirvVersion = 0x00010400u; + target.supportedCapabilities = capabilities; + target.tessellation = TessellationTargetLimits{32, 128, 128, 120, 4096, 128, 128}; + RecompileResult vertex; + RecompileResult fragment; + 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) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp b/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp new file mode 100644 index 0000000000..a17b7cf87d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp @@ -0,0 +1,475 @@ +#include "IntermediateRepresentation/IrProgram.hpp" +#include "Optimization/RedundantValueEliminator.hpp" +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +namespace { + +struct Builder { + IrProgram program; + IrBlock* block = nullptr; + + Builder() { + block = &newBlock(); + program.SetEntryBlock(*block); + } + + IrBlock& newBlock() { + auto& created = program.CreateBlock(); + program.BlockOrder().push_back(&created); + return created; + } + + IrValue& constant(std::uint32_t value) { + auto& created = program.CreateValue(IrOpcode::Void, IrType::U32); + created.SetImmediateU32(value); + return created; + } + + IrValue& floatConstant(std::uint32_t bits) { + auto& created = program.CreateValue(IrOpcode::Void, IrType::F32); + created.SetImmediateU32(bits); + return created; + } + + IrValue& truth() { + auto& created = program.CreateValue(IrOpcode::Void, IrType::Bool); + created.SetImmediateBool(true); + return created; + } + + IrValue& emit(IrOpcode opcode, IrType type, std::initializer_list arguments, std::uint64_t flags = 0u) { + auto& created = program.CreateValue(opcode, type, flags); + for (auto* argument : arguments) created.AddArgument(argument); + block->AppendInstruction(&created); + return created; + } + + IrValue& unknown() { + return emit(IrOpcode::LaneId, IrType::U32, {}); + } + + IrValue& flush(IrValue& bits) { + auto& exponent = emit(IrOpcode::BitwiseAnd32, IrType::U32, {&bits, &constant(0x7f800000u)}); + auto& denormal = emit(IrOpcode::IEqual32, IrType::Bool, {&exponent, &constant(0u)}); + auto& sign = emit(IrOpcode::BitwiseAnd32, IrType::U32, {&bits, &constant(0x80000000u)}); + return emit(IrOpcode::SelectU32, IrType::U32, {&denormal, &sign, &bits}); + } + + void keep(IrValue& value) { + static_cast(emit(IrOpcode::ReferenceU32, IrType::Void, {&value})); + } + + std::uint32_t eliminate() { + return RedundantValueEliminator{}.Eliminate(program).replacedValues; + } +}; + +bool expect(const char* name, bool condition) { + if (!condition) std::fprintf(stderr, "%s\n", name); + return condition; +} + +bool run(const char* name, const std::function& test) { + try { + return expect(name, test()); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s: %s\n", name, error.what()); + return false; + } +} + +bool AllUsed(const std::vector& values) { + for (const auto* value : values) { + if (!value->HasUses()) return false; + } + return true; +} + +bool PureSpirv(std::uint32_t opcode) { + switch (opcode) { + case spv::OpBitcast: + case spv::OpUConvert: + case spv::OpSConvert: + case spv::OpFConvert: + case spv::OpConvertFToU: + case spv::OpConvertFToS: + case spv::OpConvertUToF: + case spv::OpConvertSToF: + case spv::OpCompositeConstruct: + case spv::OpCompositeExtract: + case spv::OpSNegate: + case spv::OpFNegate: + case spv::OpIAdd: + case spv::OpISub: + case spv::OpIMul: + case spv::OpFAdd: + case spv::OpFSub: + case spv::OpFMul: + case spv::OpShiftLeftLogical: + case spv::OpShiftRightLogical: + case spv::OpShiftRightArithmetic: + case spv::OpBitwiseAnd: + case spv::OpBitwiseOr: + case spv::OpBitwiseXor: + case spv::OpNot: + case spv::OpBitFieldInsert: + case spv::OpBitFieldUExtract: + case spv::OpBitFieldSExtract: + case spv::OpBitReverse: + case spv::OpBitCount: + case spv::OpSelect: + case spv::OpIEqual: + case spv::OpINotEqual: + case spv::OpULessThan: + case spv::OpULessThanEqual: + case spv::OpUGreaterThan: + case spv::OpUGreaterThanEqual: + case spv::OpSLessThan: + case spv::OpSLessThanEqual: + case spv::OpSGreaterThan: + case spv::OpSGreaterThanEqual: + case spv::OpFOrdEqual: + case spv::OpFOrdNotEqual: + case spv::OpFOrdLessThan: + case spv::OpFOrdGreaterThan: + case spv::OpFOrdLessThanEqual: + case spv::OpFOrdGreaterThanEqual: + case spv::OpFUnordEqual: + case spv::OpFUnordNotEqual: + case spv::OpFUnordLessThan: + case spv::OpFUnordGreaterThan: + case spv::OpFUnordLessThanEqual: + case spv::OpFUnordGreaterThanEqual: + case spv::OpIsNan: + case spv::OpLogicalAnd: + case spv::OpLogicalOr: + case spv::OpLogicalNot: + case spv::OpLogicalEqual: + case spv::OpLogicalNotEqual: + return true; + default: + return false; + } +} + +std::size_t RepeatedInBlock(std::span words) { + if (words.size() < 5 || words[0] != spv::MagicNumber) throw std::runtime_error("the recompiler returned no SPIR-V module"); + std::size_t repeated = 0; + std::set> seen; + for (std::size_t offset = 5; offset < words.size();) { + const auto count = words[offset] >> spv::WordCountShift; + if (count == 0 || count > words.size() - offset) throw std::runtime_error("the recompiler returned a malformed SPIR-V instruction"); + const auto opcode = words[offset] & spv::OpCodeMask; + if (opcode == spv::OpLabel) seen.clear(); + if (PureSpirv(opcode) && count >= 3) { + std::vector key{opcode, words[offset + 1]}; + key.insert(key.end(), words.begin() + static_cast(offset + 3), words.begin() + static_cast(offset + count)); + if (!seen.insert(std::move(key)).second) ++repeated; + } + offset += count; + } + return repeated; +} + +std::vector Recompiled(std::span code) { + const std::array userData{0x10000000u, 0x00100000u, 0x40u, 0x00027facu}; + const std::array capabilities{1u, 61u}; + const std::array extensions{"SPV_KHR_storage_buffer_storage_class"}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}}; + request.context.waveSize = 64; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00403000u; + request.target.spirvVersion = 0x00010600u; + request.target.subgroupSize = 32u; + request.target.bdaAbiVersion = 1; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.maxWorkgroupSize = {1024u, 1024u, 64u}; + request.target.maxWorkgroupInvocations = 1024; + request.target.maxWorkgroupSharedMemoryBytes = 49152; + request.layout = {0, 0, 0, 128}; + request.useCache = false; + const auto& words = Recompile(request).spirv.Words(); + return {words.begin(), words.end()}; +} + +} + +int main() { + bool passed = true; + + passed &= run("a value recomputed from the same operands in one block is replaced by the first", [] { + Builder b; + auto& source = b.unknown(); + auto& first = b.flush(source); + auto& second = b.flush(source); + b.keep(first); + b.keep(second); + return b.eliminate() == 4u && !second.HasUses() && first.UseCount() == 2u; + }); + + passed &= run("values that differ in an immediate, an operand or the opcode stay", [] { + Builder b; + auto& source = b.unknown(); + auto& other = b.unknown(); + auto& masked = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&source, &b.constant(0x7f800000u)}); + auto& otherMask = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&source, &b.constant(0x80000000u)}); + auto& otherSource = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&other, &b.constant(0x7f800000u)}); + auto& otherOpcode = b.emit(IrOpcode::BitwiseOr32, IrType::U32, {&source, &b.constant(0x7f800000u)}); + b.keep(masked); + b.keep(otherMask); + b.keep(otherSource); + b.keep(otherOpcode); + return b.eliminate() == 0u && AllUsed({&masked, &otherMask, &otherSource, &otherOpcode}); + }); + + passed &= run("values that differ only in flags, the result type or the type of an immediate stay", [] { + Builder b; + auto& source = b.unknown(); + auto& plain = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&source, &b.constant(0x3f800000u)}); + auto& flagged = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&source, &b.constant(0x3f800000u)}, 1u); + auto& otherType = b.emit(IrOpcode::BitwiseAnd32, IrType::F32, {&source, &b.constant(0x3f800000u)}); + auto& floatImmediate = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&source, &b.floatConstant(0x3f800000u)}); + b.keep(plain); + b.keep(flagged); + b.keep(otherType); + b.keep(floatImmediate); + return b.eliminate() == 0u && AllUsed({&plain, &flagged, &otherType, &floatImmediate}); + }); + + passed &= run("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()}); + auto& plusZero = b.emit(IrOpcode::FPAdd32, IrType::F32, {&x, &b.floatConstant(0x00000000u)}); + auto& minusZero = b.emit(IrOpcode::FPAdd32, IrType::F32, {&x, &b.floatConstant(0x80000000u)}); + auto& quiet = b.emit(IrOpcode::FPMax32, IrType::F32, {&x, &b.floatConstant(0x7fc00000u)}); + auto& sameQuiet = b.emit(IrOpcode::FPMax32, IrType::F32, {&x, &b.floatConstant(0x7fc00000u)}); + auto& otherPayload = b.emit(IrOpcode::FPMax32, IrType::F32, {&x, &b.floatConstant(0x7fc00001u)}); + for (auto* value : {&plusZero, &minusZero, &quiet, &sameQuiet, &otherPayload}) b.keep(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {value})); + return b.eliminate() == 2u && !sameQuiet.HasUses() && AllUsed({&quiet, &plusZero, &minusZero, &otherPayload}); + }); + + passed &= run("a value recomputed in a dominated block is replaced, one in a sibling block stays", [] { + Builder b; + IrBlock& head = *b.block; + IrBlock& then = b.newBlock(); + IrBlock& otherwise = b.newBlock(); + IrBlock& merge = b.newBlock(); + head.AddBranch(&then); + head.AddBranch(&otherwise); + then.AddBranch(&merge); + otherwise.AddBranch(&merge); + auto& source = b.unknown(); + auto& other = b.unknown(); + auto& first = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&source}); + b.keep(first); + b.block = &then; + auto& dominated = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&source}); + auto& thenOnly = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&other}); + b.keep(dominated); + b.keep(thenOnly); + b.block = &otherwise; + auto& sibling = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&other}); + b.keep(sibling); + b.block = &merge; + auto& merged = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&other}); + b.keep(merged); + return b.eliminate() == 1u && !dominated.HasUses() && first.UseCount() == 2u && AllUsed({&thenOnly, &sibling, &merged}); + }); + + passed &= run("a loop body reuses a value of its header, the loop exit does not reuse a value of the body", [] { + Builder b; + IrBlock& header = b.newBlock(); + IrBlock& body = b.newBlock(); + IrBlock& exit = b.newBlock(); + b.block->AddBranch(&header); + header.AddBranch(&body); + header.AddBranch(&exit); + body.AddBranch(&header); + auto& source = b.unknown(); + auto& other = b.unknown(); + b.block = &header; + auto& inHeader = b.emit(IrOpcode::IAdd32, IrType::U32, {&source, &b.constant(1u)}); + b.keep(inHeader); + b.block = &body; + auto& inBody = b.emit(IrOpcode::IAdd32, IrType::U32, {&source, &b.constant(1u)}); + auto& bodyOnly = b.emit(IrOpcode::IAdd32, IrType::U32, {&other, &b.constant(1u)}); + b.keep(inBody); + b.keep(bodyOnly); + b.block = &exit; + auto& afterLoop = b.emit(IrOpcode::IAdd32, IrType::U32, {&other, &b.constant(1u)}); + b.keep(afterLoop); + return b.eliminate() == 1u && !inBody.HasUses() && inHeader.UseCount() == 2u && AllUsed({&bodyOnly, &afterLoop}); + }); + + passed &= run("a branch condition that is replaced names the value that stays", [] { + Builder b; + auto& source = b.unknown(); + auto& first = b.emit(IrOpcode::IEqual32, IrType::Bool, {&source, &b.constant(0u)}); + auto& second = b.emit(IrOpcode::IEqual32, IrType::Bool, {&source, &b.constant(0u)}); + b.keep(b.emit(IrOpcode::SelectU32, IrType::U32, {&first, &source, &b.constant(1u)})); + b.keep(b.emit(IrOpcode::SelectU32, IrType::U32, {&second, &b.constant(1u), &source})); + BlockInfo info; + info.condition = &second; + b.program.Metadata().blockInfo.push_back(info); + return b.eliminate() == 1u && b.program.Metadata().blockInfo.front().condition == &first; + }); + + passed &= run("lane reads are not merged", [] { + Builder b; + auto& first = b.unknown(); + auto& second = b.unknown(); + b.keep(first); + b.keep(second); + return b.eliminate() == 0u && second.HasUses(); + }); + + passed &= run("cross-lane reads with the same operands are not merged", [] { + Builder b; + auto& source = b.unknown(); + std::vector values; + for (int copy = 0; copy < 2; ++copy) { + values.push_back(&b.emit(IrOpcode::ReadFirstLane, IrType::U32, {&source, &b.truth()})); + values.push_back(&b.emit(IrOpcode::ReadLane, IrType::U32, {&source, &b.constant(3u)})); + values.push_back(&b.emit(IrOpcode::DppMoveU32, IrType::U32, {&source, &b.truth()})); + values.push_back(&b.emit(IrOpcode::SwizzleU32, IrType::U32, {&source, &b.constant(0x1fu), &b.truth()})); + } + for (auto* value : values) b.keep(*value); + return b.eliminate() == 0u && AllUsed(values); + }); + + passed &= run("loads of one address are not merged, with no store, a store or a barrier between them", [] { + Builder b; + auto& address = b.unknown(); + std::vector loads; + loads.push_back(&b.emit(IrOpcode::LoadShared, IrType::U32, {&address})); + loads.push_back(&b.emit(IrOpcode::LoadShared, IrType::U32, {&address})); + static_cast(b.emit(IrOpcode::StoreShared, IrType::Void, {&address, &b.constant(7u)})); + loads.push_back(&b.emit(IrOpcode::LoadShared, IrType::U32, {&address})); + static_cast(b.emit(IrOpcode::Barrier, IrType::Void, {})); + loads.push_back(&b.emit(IrOpcode::LoadShared, IrType::U32, {&address})); + for (auto* load : loads) b.keep(*load); + return b.eliminate() == 0u && AllUsed(loads); + }); + + passed &= run("atomics with the same operands are not merged", [] { + Builder b; + auto& address = b.unknown(); + auto& first = b.emit(IrOpcode::SharedAtomicIAdd32, IrType::U32, {&address, &b.constant(1u), &b.truth()}); + auto& second = b.emit(IrOpcode::SharedAtomicIAdd32, IrType::U32, {&address, &b.constant(1u), &b.truth()}); + b.keep(first); + b.keep(second); + return b.eliminate() == 0u && AllUsed({&first, &second}); + }); + + passed &= run("phis with the same incoming values are not merged", [] { + Builder b; + IrBlock& left = b.newBlock(); + IrBlock& right = b.newBlock(); + IrBlock& merge = b.newBlock(); + b.block->AddBranch(&left); + b.block->AddBranch(&right); + left.AddBranch(&merge); + right.AddBranch(&merge); + auto& one = b.unknown(); + auto& two = b.unknown(); + b.block = &merge; + auto& first = b.emit(IrOpcode::Phi, IrType::U32, {}); + auto& second = b.emit(IrOpcode::Phi, IrType::U32, {}); + for (auto* phi : {&first, &second}) { + phi->AddPhiOperand(&left, &one); + phi->AddPhiOperand(&right, &two); + } + b.keep(first); + b.keep(second); + return b.eliminate() == 0u && AllUsed({&first, &second}); + }); + + passed &= run("float operations with the same operands in the same order are merged", [] { + Builder b; + auto& lhs = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&b.unknown()}); + auto& rhs = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&b.unknown()}); + auto& first = b.emit(IrOpcode::FPMul32, IrType::F32, {&lhs, &rhs}); + auto& second = b.emit(IrOpcode::FPMul32, IrType::F32, {&lhs, &rhs}); + auto& swapped = b.emit(IrOpcode::FPMul32, IrType::F32, {&rhs, &lhs}); + b.keep(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&first})); + b.keep(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&second})); + b.keep(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&swapped})); + return b.eliminate() == 2u && !second.HasUses() && swapped.HasUses(); + }); + + passed &= run("a block that the entry does not reach throws", [] { + Builder b; + static_cast(b.newBlock()); + try { + static_cast(b.eliminate()); + } catch (const std::runtime_error&) { + return true; + } + return false; + }); + + passed &= run("a wave64 shader that reads one register in several float operations computes nothing twice in a block", [] { + const std::array code{ + 0x10020100u, + 0x10040300u, + 0x10060500u, + 0x06020503u, + 0xe0700000u, 0x80000100u, + 0xbf810000u, + }; + const auto module = Recompiled(code); + const auto repeated = RepeatedInBlock(module); + if (repeated != 0u) std::fprintf(stderr, " %zu pure instructions repeat an earlier one in their block of the %zu-word module\n", repeated, module.size()); + return repeated == 0u; + }); + + return passed ? 0 : 1; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/RegisterKey.cpp b/core/libs/prx/libSceAgcDriver/tests/RegisterKey.cpp new file mode 100644 index 0000000000..f9f295a3d3 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/RegisterKey.cpp @@ -0,0 +1,168 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include +#include +#include +#include +#include +#include + +using namespace AgcDriver; +using namespace AgcDriver::DriverDetail; + +namespace { + +void check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +std::uint64_t baselineRegisterStateKey(const QueueState& queue, bool allUserWords = false) { + std::uint64_t key = 0xcbf29ce484222325ull; + const auto mix = [&](std::uint64_t value) { + key ^= value; + key *= 0x100000001b3ull; + }; + const auto userEnd = [&](std::uint32_t base) { + if (allUserWords) return base + 32u; + const auto resources = queue.shader.find(base - 1); + if (resources == queue.shader.end()) return base; + const auto count = ((resources->second >> 1u) & 0x1fu) | (((resources->second >> 27u) & 1u) << 5u); + return base + std::min(count, 32u); + }; + const std::array, 3> users{{{0x00cu, userEnd(0x00cu)}, {0x08cu, userEnd(0x08cu)}, {0x10cu, userEnd(0x10cu)}}}; + const auto unread = [&](std::uint32_t offset) { + return std::any_of(users.begin(), users.end(), [&](const auto& user) { return offset >= user.second && offset < user.first + 32u; }); + }; + for (const auto& range : Graphics::DrawKeyRegisters) { + const auto& bank = range.bank == Graphics::RegisterBank::Context ? queue.context : range.bank == Graphics::RegisterBank::Shader ? queue.shader : queue.userConfig; + mix((static_cast(range.bank) << 32u) | range.first); + const auto end = range.first + range.count; + const bool shader = range.bank == Graphics::RegisterBank::Shader; + for (auto it = bank.lower_bound(range.first); it != bank.end() && it->first < end; ++it) { + if (shader && unread(it->first)) continue; + mix(it->first); + mix(it->second); + } + } + return key; +} + +void testFingerprints() { + static_assert(!std::is_convertible_v); + std::mt19937 random(42); + QueueState queue; + 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 = [&] { + const auto expected = baselineRegisterStateKey(queue); + const auto actual = RegisterStateKey(queue); + check(actual == RegisterStateKey(queue, false, false), "incremental fingerprint missed a mutation"); + check(RegisterStateKey(queue, true) == RegisterStateKey(queue, true, false), "all-user fingerprint missed a mutation"); + check((expected == reference) == (actual == tracked), "exact key changed its dependency set"); + reference = expected; + tracked = actual; + }; + for (const auto& range : Graphics::DrawKeyRegisters) { + auto& bank = range.bank == Graphics::RegisterBank::Context ? queue.context : range.bank == Graphics::RegisterBank::Shader ? queue.shader : queue.userConfig; + for (auto offset = range.first; offset < range.first + range.count; ++offset) { + bank.erase(offset); compare(); + bank.emplace(offset, 0); compare(); + bank.insert_or_assign(offset, 0); compare(); + bank[offset] = random(); compare(); + bank.at(offset) ^= 1; compare(); + } + } + for (std::uint32_t i = 0; i < 30000; ++i) { + auto& bank = i % 3 == 0 ? queue.context : i % 3 == 1 ? queue.shader : queue.userConfig; + const auto offset = random() % 0x500; + switch (i % 11) { + case 0: bank.erase(offset); break; + case 1: bank.emplace(offset, random()); break; + case 2: bank[offset] |= random(); break; + case 3: bank[offset] &= random(); break; + case 6: bank[offset] += random(); break; + case 4: { + auto retained = bank[offset]; + bank.insert_or_assign(0x1000, random()); + compare(); + retained = random(); + break; + } + case 5: { + auto copy = bank; + copy.insert_or_assign(offset, random()); + compare(); + bank = copy; + break; + } + default: bank.insert_or_assign(offset, random()); break; + } + compare(); + } + queue.shader[0x8b] = 0; compare(); + queue.shader[0x8c] = random(); compare(); + queue.shader[0x8b] = 2; compare(); + queue.shader[0x8b] = 1u << 27u; compare(); + queue.shader.erase(0x8b); compare(); + queue.savedContext = queue.context; + queue.ClearContext(); compare(); + queue.context = *queue.savedContext; compare(); + auto moved = std::move(queue.context); + check(queue.context.empty(), "moved register bank retained its population"); + queue.context.emplace(0x200, 1); compare(); + queue.context = std::move(moved); compare(); + queue.context = queue.context; compare(); + queue.context.clear(); compare(); + queue.context.emplace(0x200, 0); compare(); + queue = QueueState{}; compare(); + Registers registers{{0, 7}, {64, 9}}; + auto low = std::make_shared(std::vector{1}); + auto high = std::make_shared(std::vector{0, 1}); + const auto lowHash = registers.Fingerprint(low); + check(registers.Fingerprint(high) != lowHash && registers.Fingerprint(low) == lowHash, "projection switch reused another mask's hash"); + registers[64] = 8; + check(registers.Fingerprint(low) == lowHash && registers.Fingerprint(high) == registers.RecomputeFingerprint(*high), "projection switch lost a masked write"); +} + +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) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/ResidentPresent.cpp b/core/libs/prx/libSceAgcDriver/tests/ResidentPresent.cpp index b535571d27..2d5dfcd701 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ResidentPresent.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ResidentPresent.cpp @@ -1,6 +1,7 @@ #include "ResidentPresent.hpp" #include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp" #include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" +#include "prx/libSceAgcDriver/Execution/include/PqToSdr.hpp" #include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" #include "prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" @@ -10,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -20,10 +22,14 @@ namespace { using namespace AgcDriver::Graphics; using AgcDriver::DisplayBuffer; using AgcDriver::ResidentPresent; +using AgcDriver::ResidentPresentSlice; constexpr std::uint64_t Bgra8 = 0x8000000000000000ull; constexpr std::uint64_t Rgba8 = 0x8000000022000000ull; constexpr std::uint64_t TenBit = 0x0100000000000000ull; +constexpr std::uint64_t Rgba16Float = 0xc001000622000000ull; +constexpr std::uint64_t Rgb10Pq = 0x8100070422000000ull; +constexpr std::uint64_t Bgr10Pq = 0x8100070400000000ull; void decisionTests() { using AgcDriver::DisplayTexelFormat; @@ -34,12 +40,21 @@ 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)"); } Require(ResidentPresentPath(VK_FORMAT_B8G8R8A8_UNORM, display, true) == ResidentPresent::None, "an 8-bit image presents a 10-bit display"); } + Require(DisplayTexelFormat(Bgr10Pq) == VK_FORMAT_A2R10G10B10_UNORM_PACK32 && DisplayTexelFormat(Rgb10Pq) == VK_FORMAT_A2B10G10R10_UNORM_PACK32, "PQ display texel formats are wrong"); + for (const auto display : {Bgr10Pq, Rgb10Pq}) { + for (const auto storage : {VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_FORMAT_A2R10G10B10_UNORM_PACK32}) { + Require(ResidentPresentPath(storage, display, true) == ResidentPresent::Convert, "a PQ image is blitted (it must be converted to SDR)"); + } + Require(ResidentPresentPath(VK_FORMAT_B8G8R8A8_UNORM, display, true) == ResidentPresent::None, "an 8-bit image presents a PQ display"); + } Require(ResidentPresentPath(VK_FORMAT_A2B10G10R10_UNORM_PACK32, Bgra8, true) == ResidentPresent::None && ResidentPresentPath(VK_FORMAT_R16G16_SFLOAT, Bgra8, true) == ResidentPresent::None, "an image of another type presents an 8-bit display"); const auto guest = FindGuestColorTargetFormat(VK_FORMAT_A2R10G10B10_UNORM_PACK32, 4); Require(guest.has_value() && guest == FindGuestTextureFormat(VK_FORMAT_A2B10G10R10_UNORM_PACK32, 4), "the blue-low 10-bit color target has no storage format"); @@ -52,30 +67,61 @@ std::uint32_t memoryType(const Context& context, std::uint32_t bits, VkMemoryPro throw std::runtime_error("no device-local memory type for the test image"); } -void conversionTest(const Context& context, std::uint64_t pixelFormat, VkFormat format) { +void sliceTests() { + constexpr std::uint64_t image = 0x208000000ull; + constexpr std::uint64_t slice = 0x1fe0000ull; + Require(ResidentPresentSlice(image, slice, 1, image, slice) == 0u, "a display buffer that is the whole image is not its slice 0"); + Require(ResidentPresentSlice(image, 2 * slice, 2, image, slice) == 0u, "a display buffer at the start of a two-slice image is not its slice 0"); + Require(ResidentPresentSlice(image, 2 * slice, 2, image + slice, slice) == 1u, "a display buffer one slice into a two-slice image is not its slice 1"); + Require(ResidentPresentSlice(image, 6 * slice, 6, image + 5 * slice, slice) == 5u, "a display buffer at the last of six slices is not slice 5"); + Require(!ResidentPresentSlice(image, 2 * slice, 2, image + 2 * slice, slice), "a display buffer past the image's last slice has a slice"); + Require(!ResidentPresentSlice(image, 2 * slice, 2, image + 0x10000, slice), "a display buffer between two slices has a slice"); + Require(!ResidentPresentSlice(image, 2 * slice, 2, image - slice, slice), "a display buffer before the image has a slice"); + Require(!ResidentPresentSlice(image, 2 * slice, 2, image, 2 * slice), "a display buffer the size of two slices has a slice"); + Require(!ResidentPresentSlice(image, 2 * slice, 2, image, slice / 2), "a display buffer of half a slice has a slice"); + Require(!ResidentPresentSlice(image, 2 * slice + 1, 2, image, slice), "an image whose bytes do not divide into its slices has a slice"); + Require(!ResidentPresentSlice(image, 0, 0, image, slice) && !ResidentPresentSlice(image, slice, 1, image, 0), "an empty image or display buffer has a slice"); +} + +void conversionTest(const Context& context, std::uint64_t pixelFormat, VkFormat format, std::uint32_t layer = 0, float whiteNits = AgcDriver::SdrToneMapLuminanceNits) { constexpr std::uint32_t width = 200; constexpr std::uint32_t height = 130; - std::vector words(static_cast(width) * height * 4); + const auto texelBytes = AgcDriver::DisplayTexelBytes(pixelFormat); + std::vector words(static_cast(width) * height * texelBytes); std::uint32_t seed = 0x9e3779b9u ^ static_cast(format); for (std::size_t i = 0; i < words.size(); ++i) { seed = seed * 1664525u + 1013904223u; words[i] = static_cast(seed >> 24u); } - const ColorTargetLayout layout(width, height, ColorTileMode::RenderTarget); + const ColorTargetLayout layout(width, height, ColorTileMode::RenderTarget, texelBytes); std::vector storage(layout.Bytes() + 65536); auto* aligned = reinterpret_cast((reinterpret_cast(storage.data()) + 65535u) & ~std::uintptr_t{65535}); std::span guest(aligned, layout.Bytes()); - layout.Tile(words, guest); const DisplayBuffer display{reinterpret_cast(aligned), pixelFormat, width, height}; Require(AgcDriver::DisplayBufferSize(display) == guest.size(), "test display buffer size differs from the color layout"); - const auto expected = AgcDriver::DecodeDisplayBuffer(display, guest); + if (texelBytes == 4u) { + layout.Tile(words, guest); + } else { + for (std::uint32_t y = 0; y < height; ++y) { + for (std::uint32_t x = 0; x < width; ++x) std::memcpy(guest.data() + AgcDriver::DisplayBufferOffset(display, x, y), words.data() + (static_cast(y) * width + x) * texelBytes, texelBytes); + } + } + auto expected = AgcDriver::DecodeDisplayBuffer(display, guest); + if (AgcDriver::DisplayPq(pixelFormat) && whiteNits != AgcDriver::SdrToneMapLuminanceNits) { + for (std::size_t i = 0; i < expected.size(); i += 4) { + std::uint32_t word = 0; + std::memcpy(&word, words.data() + i, 4); + const auto pixel = AgcDriver::PqToSdrPixel(word, AgcDriver::DisplayRedLow(pixelFormat), whiteNits); + for (std::size_t channel = 0; channel < pixel.size(); ++channel) expected[i + channel] = static_cast(pixel[channel]); + } + } VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; imageInfo.imageType = VK_IMAGE_TYPE_2D; imageInfo.format = format; imageInfo.extent = {width, height, 1}; imageInfo.mipLevels = 1; - imageInfo.arrayLayers = 1; + imageInfo.arrayLayers = layer + 1u; imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; @@ -106,14 +152,17 @@ void conversionTest(const Context& context, std::uint64_t pixelFormat, VkFormat Buffer fromGuest(context, expected.size(), VK_BUFFER_USAGE_TRANSFER_DST_BIT); GpuColorTransfer transfer(context); const bool redLow = AgcDriver::DisplayRedLow(pixelFormat); - const bool tenBit = AgcDriver::DisplayTenBit(pixelFormat); + const bool pq = AgcDriver::DisplayPq(pixelFormat); + const bool tenBit = AgcDriver::DisplayTenBit(pixelFormat) && !pq; + const bool halfFloat = AgcDriver::DisplayHalfFloat(pixelFormat); + const auto pqWhiteNits = pq ? std::optional(whiteNits) : std::nullopt; const auto barrier = context.Function("vkCmdPipelineBarrier"); const auto readback = [&](VkCommandBuffer commands, Buffer& destination) { const VkBufferCopy copy{0, 0, expected.size()}; context.Function("vkCmdCopyBuffer")(commands, transfer.LinearBuffer(), destination.Handle(), 1, ©); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_HOST_READ_BIT); }; - transfer.Upload(display.address, width, height, ColorTileMode::RenderTarget); + transfer.Upload(display.address, width, height, ColorTileMode::RenderTarget, texelBytes); { CommandBatch batch(context); const auto commands = batch.Handle(); @@ -123,37 +172,55 @@ void conversionTest(const Context& context, std::uint64_t pixelFormat, VkFormat toTransfer.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; toTransfer.srcQueueFamilyIndex = toTransfer.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; toTransfer.image = image; - toTransfer.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + toTransfer.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, layer + 1u}; barrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toTransfer); VkBufferImageCopy region{}; - region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, layer, 1}; region.imageExtent = {width, height, 1}; context.Function("vkCmdCopyBufferToImage")(commands, upload.Handle(), image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); + if (layer != 0) { + const VkClearColorValue other{}; + const VkImageSubresourceRange lower{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, layer}; + context.Function("vkCmdClearColorImage")(commands, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &other, 1, &lower); + } auto toGeneral = toTransfer; toGeneral.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; toGeneral.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; toGeneral.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL; barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); - transfer.Detile(commands, redLow, tenBit); + transfer.Detile(commands, redLow, tenBit, halfFloat, pqWhiteNits); readback(commands, fromGuest); - transfer.DetileImage(commands, image, VK_IMAGE_LAYOUT_GENERAL, width, height, ColorTileMode::RenderTarget, redLow, tenBit); - readback(commands, fromImage); + if (!pq) { + transfer.DetileImage(commands, image, VK_IMAGE_LAYOUT_GENERAL, width, height, ColorTileMode::RenderTarget, redLow, tenBit, halfFloat, layer); + readback(commands, fromImage); + } batch.SubmitAndWait(); } - const auto name = std::string(tenBit ? "10-bit " : "8-bit ") + (redLow ? "red-low" : "blue-low") + " display from VkFormat " + std::to_string(static_cast(format)); - Require(std::equal(expected.begin(), expected.end(), fromGuest.Bytes().begin()), "the GPU detile differs from the CPU decode: " + name); - Require(std::equal(expected.begin(), expected.end(), fromImage.Bytes().begin()), "the resident image presents other pixels than guest memory: " + name); + const auto name = std::string(halfFloat ? "16-bit float " : pq ? "PQ " : tenBit ? "10-bit " : "8-bit ") + (redLow ? "red-low" : "blue-low") + " display from layer " + std::to_string(layer) + " of VkFormat " + std::to_string(static_cast(format)); + const auto matches = [&](std::span actual) { + return std::equal(expected.begin(), expected.end(), actual.begin(), [&](std::byte want, std::byte got) { return std::abs(std::to_integer(want) - std::to_integer(got)) <= (pq ? 1 : 0); }); + }; + Require(matches(fromGuest.Bytes()), "the GPU detile differs from the CPU decode: " + name); + Require(pq || matches(fromImage.Bytes()), "the resident image presents other pixels than guest memory: " + name); } } void RunResidentPresentTests(const Context& context) { decisionTests(); + sliceTests(); conversionTest(context, Bgra8 | TenBit, VK_FORMAT_A2B10G10R10_UNORM_PACK32); conversionTest(context, Rgba8 | TenBit, VK_FORMAT_A2B10G10R10_UNORM_PACK32); conversionTest(context, Bgra8 | TenBit, VK_FORMAT_A2R10G10B10_UNORM_PACK32); conversionTest(context, Bgra8, VK_FORMAT_R8G8B8A8_UNORM); conversionTest(context, Rgba8, VK_FORMAT_B8G8R8A8_UNORM); + conversionTest(context, Rgba16Float, VK_FORMAT_R16G16B16A16_SFLOAT); + conversionTest(context, Rgba8, VK_FORMAT_B8G8R8A8_UNORM, 1); + conversionTest(context, Bgra8 | TenBit, VK_FORMAT_A2B10G10R10_UNORM_PACK32, 2); + conversionTest(context, Rgb10Pq, VK_FORMAT_A2B10G10R10_UNORM_PACK32); + conversionTest(context, Bgr10Pq, VK_FORMAT_A2R10G10B10_UNORM_PACK32); + conversionTest(context, Rgb10Pq, VK_FORMAT_A2B10G10R10_UNORM_PACK32, 0, 1000.0f); + conversionTest(context, Bgr10Pq, VK_FORMAT_A2R10G10B10_UNORM_PACK32, 0, 150.0f); std::cout << "resident present format and conversion tests passed\n"; } diff --git a/core/libs/prx/libSceAgcDriver/tests/RobustBufferAccess.cpp b/core/libs/prx/libSceAgcDriver/tests/RobustBufferAccess.cpp new file mode 100644 index 0000000000..6b0331109f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/RobustBufferAccess.cpp @@ -0,0 +1,33 @@ +#include "prx/libSceAgcDriver/Execution/include/RobustBufferAccess.hpp" +#include + +namespace { + +struct Case { + VkDriverId driver; + bool usable; +}; + +constexpr Case Cases[]{ + {VK_DRIVER_ID_AMD_PROPRIETARY, false}, + {VK_DRIVER_ID_AMD_OPEN_SOURCE, true}, + {VK_DRIVER_ID_MESA_RADV, true}, + {VK_DRIVER_ID_NVIDIA_PROPRIETARY, true}, + {VK_DRIVER_ID_INTEL_PROPRIETARY_WINDOWS, true}, + {VK_DRIVER_ID_INTEL_OPEN_SOURCE_MESA, true}, + {VK_DRIVER_ID_MESA_LLVMPIPE, true}, + {VK_DRIVER_ID_MOLTENVK, true}, +}; + +} + +int main() { + int failures = 0; + for (const auto& item : Cases) { + if (AgcDriver::RobustBufferAccess2Usable(item.driver) != item.usable) { + std::fprintf(stderr, "driver %d: expected robustBufferAccess2 to be %s\n", static_cast(item.driver), item.usable ? "usable" : "unusable"); + ++failures; + } + } + return failures == 0 ? 0 : 1; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/ScalarSelectMask.cpp b/core/libs/prx/libSceAgcDriver/tests/ScalarSelectMask.cpp new file mode 100644 index 0000000000..43258d0d96 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/ScalarSelectMask.cpp @@ -0,0 +1,352 @@ +#include "ControlFlow/GraphBuilder.hpp" +#include "ControlFlow/Structurizer.hpp" +#include "IntermediateRepresentation/IrProgram.hpp" +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" +#include "Translation/InstructionTranslator.hpp" +#include "Translation/ShaderInputInfoBuilder.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +namespace { + +constexpr std::uint32_t Lanes = 64; + +using LaneWords = std::array; + +struct Value { + std::array component{}; +}; + +class Evaluator { +public: + Evaluator(const IrProgram& program, std::uint32_t waveSize) : lanes(waveSize), exec(waveSize >= 64u ? ~0ull : (1ull << waveSize) - 1ull) { + for (std::uint32_t lane = 0; lane < Lanes; ++lane) vector[0][lane] = lane; + for (const auto& block : program.Blocks()) { + for (const auto* inst : block->Instructions()) run(*inst); + } + } + + LaneWords output{}; + bool exported = false; + std::map maskTag; + std::map threadBitValue; + +private: + std::unordered_map values; + std::array vector{}; + std::array scalar{}; + std::map threadBit; + std::uint32_t lanes; + std::uint64_t exec; + std::uint64_t vcc = 0; + bool scc = false; + + static std::uint32_t registerIndex(const IrValue& inst) { + return inst.Argument(0)->Register().index; + } + + Value operand(const IrValue* argument) { + argument = argument->Resolve(); + const auto found = values.find(argument); + if (found != values.end()) return found->second; + if (!argument->HasImmediate()) throw std::runtime_error("the evaluator met a value that has not been computed"); + Value constant{}; + const std::uint32_t word = argument->Type() == IrType::Bool ? (argument->ImmediateBool() ? 1u : 0u) : argument->ImmediateU32(); + constant.component[0].fill(word); + return constant; + } + + Value fromBits(std::uint64_t bits) const { + Value result{}; + for (std::uint32_t lane = 0; lane < lanes; ++lane) result.component[0][lane] = static_cast((bits >> lane) & 1u); + return result; + } + + std::uint64_t toBits(const Value& value) const { + std::uint64_t bits = 0; + for (std::uint32_t lane = 0; lane < lanes; ++lane) bits |= static_cast(value.component[0][lane] & 1u) << lane; + return bits; + } + + static Value uniform(std::uint32_t word) { + Value result{}; + result.component[0].fill(word); + return result; + } + + std::uint32_t uniformWord(const Value& value) const { + for (std::uint32_t lane = 1; lane < lanes; ++lane) { + if (value.component[0][lane] != value.component[0][0]) throw std::runtime_error("a scalar register was written with a value that differs between lanes"); + } + return value.component[0][0]; + } + + template + Value lanewise(const IrValue& inst, TOperation operation) { + const Value left = operand(inst.Argument(0)); + const Value right = operand(inst.Argument(1)); + Value result{}; + for (std::uint32_t lane = 0; lane < Lanes; ++lane) result.component[0][lane] = operation(left.component[0][lane], right.component[0][lane]); + return result; + } + + Value select(const IrValue& inst) { + const Value condition = operand(inst.Argument(0)); + const Value active = operand(inst.Argument(1)); + const Value inactive = operand(inst.Argument(2)); + Value result{}; + for (std::uint32_t lane = 0; lane < Lanes; ++lane) { + result.component[0][lane] = (condition.component[0][lane] & 1u) != 0u ? active.component[0][lane] : inactive.component[0][lane]; + } + return result; + } + + void run(const IrValue& inst) { + switch (inst.Opcode()) { + case IrOpcode::SetExec: exec = toBits(operand(inst.Argument(0))); return; + case IrOpcode::SetExecLo: exec = (exec & ~0xffffffffull) | uniformWord(operand(inst.Argument(0))); return; + case IrOpcode::SetExecHi: exec = (exec & 0xffffffffull) | (static_cast(uniformWord(operand(inst.Argument(0)))) << 32); return; + case IrOpcode::GetExec: values[&inst] = fromBits(exec); return; + case IrOpcode::GetExecLo: values[&inst] = uniform(static_cast(exec)); return; + case IrOpcode::GetExecHi: values[&inst] = uniform(static_cast(exec >> 32)); return; + case IrOpcode::SetVcc: vcc = toBits(operand(inst.Argument(0))); return; + case IrOpcode::SetVccLo: vcc = (vcc & ~0xffffffffull) | uniformWord(operand(inst.Argument(0))); return; + case IrOpcode::SetVccHi: vcc = (vcc & 0xffffffffull) | (static_cast(uniformWord(operand(inst.Argument(0)))) << 32); return; + case IrOpcode::GetVcc: values[&inst] = fromBits(vcc); return; + case IrOpcode::GetVccLo: values[&inst] = uniform(static_cast(vcc)); return; + case IrOpcode::GetVccHi: values[&inst] = uniform(static_cast(vcc >> 32)); return; + case IrOpcode::SetScc: scc = (uniformWord(operand(inst.Argument(0))) & 1u) != 0u; return; + case IrOpcode::GetScc: values[&inst] = uniform(scc ? 1u : 0u); return; + case IrOpcode::SetScalarRegister: scalar.at(registerIndex(inst)) = uniformWord(operand(inst.Argument(1))); return; + case IrOpcode::GetScalarRegister: values[&inst] = uniform(scalar.at(registerIndex(inst))); return; + case IrOpcode::SetScalarMaskTag: maskTag[registerIndex(inst)] = (uniformWord(operand(inst.Argument(1))) & 1u) != 0u; return; + case IrOpcode::GetScalarMaskTag: { + const auto found = maskTag.find(registerIndex(inst)); + values[&inst] = uniform(found != maskTag.end() && found->second ? 1u : 0u); + return; + } + case IrOpcode::SetThreadBitScalarRegister: + threadBit[registerIndex(inst)] = operand(inst.Argument(1)).component[0]; + threadBitValue[registerIndex(inst)] = inst.Argument(1)->Resolve(); + return; + case IrOpcode::GetThreadBitScalarRegister: { + Value result{}; + const auto found = threadBit.find(registerIndex(inst)); + if (found != threadBit.end()) result.component[0] = found->second; + values[&inst] = result; + return; + } + case IrOpcode::SetVectorRegister: vector.at(registerIndex(inst)) = operand(inst.Argument(1)).component[0]; return; + case IrOpcode::GetVectorRegister: { + Value result{}; + result.component[0] = vector.at(registerIndex(inst)); + values[&inst] = result; + return; + } + case IrOpcode::GetBuiltin: values[&inst] = uniform(0u); return; + case IrOpcode::LaneId: { + Value result{}; + for (std::uint32_t lane = 0; lane < Lanes; ++lane) result.component[0][lane] = lane; + values[&inst] = result; + return; + } + case IrOpcode::Ballot: { + const std::uint64_t bits = toBits(operand(inst.Argument(0))); + Value result{}; + result.component[0].fill(static_cast(bits)); + result.component[1].fill(static_cast(bits >> 32)); + values[&inst] = result; + return; + } + case IrOpcode::CompositeExtractU32x4: { + const Value source = operand(inst.Argument(0)); + Value result{}; + result.component[0] = source.component.at(operand(inst.Argument(1)).component[0][0]); + values[&inst] = result; + return; + } + case IrOpcode::CompositeConstructU32x4: { + Value result{}; + for (std::uint32_t index = 0; index < 4; ++index) result.component[index] = operand(inst.Argument(index)).component[0]; + values[&inst] = result; + return; + } + case IrOpcode::UGreaterThan32: values[&inst] = lanewise(inst, [](std::uint32_t a, std::uint32_t b) { return a > b ? 1u : 0u; }); return; + case IrOpcode::ULessThan32: values[&inst] = lanewise(inst, [](std::uint32_t a, std::uint32_t b) { return a < b ? 1u : 0u; }); return; + case IrOpcode::IEqual32: values[&inst] = lanewise(inst, [](std::uint32_t a, std::uint32_t b) { return a == b ? 1u : 0u; }); return; + case IrOpcode::INotEqual32: values[&inst] = lanewise(inst, [](std::uint32_t a, std::uint32_t b) { return a != b ? 1u : 0u; }); return; + case IrOpcode::LogicalAnd: values[&inst] = lanewise(inst, [](std::uint32_t a, std::uint32_t b) { return (a & b) & 1u; }); return; + case IrOpcode::BitwiseAnd32: values[&inst] = lanewise(inst, [](std::uint32_t a, std::uint32_t b) { return a & b; }); return; + case IrOpcode::ShiftRightLogical32: values[&inst] = lanewise(inst, [](std::uint32_t a, std::uint32_t b) { return a >> (b & 31u); }); return; + case IrOpcode::SelectU1: + case IrOpcode::SelectU32: values[&inst] = select(inst); return; + case IrOpcode::SetAttribute: + output = operand(inst.Argument(0)).component[0]; + exported = true; + return; + default: throw std::runtime_error("the evaluator does not support IR opcode " + std::to_string(static_cast(inst.Opcode()))); + } + } +}; + +constexpr std::uint32_t Vcc = 0x6au; +constexpr std::uint32_t Exec = 0x7eu; +constexpr std::uint32_t Zero = 0x80u; +constexpr std::uint32_t MinusOne = 0xc1u; +constexpr std::uint32_t S4 = 0x04u; +constexpr std::uint32_t Literal = 0xffu; + +std::uint32_t vcmpLanesBelow(std::uint32_t count) { + return 0x7d880080u + count; +} + +std::uint32_t sCmpScc(bool value) { + return value ? 0xbf068080u : 0xbf068180u; +} + +std::uint32_t sCselectB64(std::uint32_t destination, std::uint32_t trueSource, std::uint32_t falseSource) { + return 0x85800000u | (destination << 16) | (falseSource << 8) | trueSource; +} + +std::uint32_t sMovB64(std::uint32_t destination, std::uint32_t source) { + return 0xbe800400u | (destination << 16) | source; +} + +std::array vCndmaskV1(std::uint32_t mask) { + return {0xd5010001u, 0x80u | (129u << 9) | (mask << 18)}; +} + +struct Outcome { + LaneWords lanes{}; + std::uint32_t waveSize = 64; + bool maskTagS4 = false; + bool constantSources = false; +}; + +Outcome run(const std::vector& body, std::uint32_t mask, std::uint32_t waveSize = 64) { + std::vector code = body; + const auto select = vCndmaskV1(mask); + code.insert(code.end(), {select[0], select[1], 0xf80008cfu, 0x01010101u, 0xbf810000u}); + const auto decoded = RdnaInstructionDecoder{}.Decode(code); + auto cfg = GraphBuilder{}.Build(decoded); + Structurizer{}.Structurize(cfg); + GuestContext context{}; + context.waveSize = waveSize; + context.vertex = ShaderVertexStageInfo{}; + TranslateOptions options{}; + options.stage = ShaderStageKind::Vertex; + options.waveSize = waveSize; + options.userDataCount = 0; + options.inputInfo = BuildShaderStageInputInfo(ShaderStageKind::Vertex, context, waveSize); + const auto program = InstructionTranslator{}.Translate(decoded, cfg, options); + const Evaluator evaluator(program, waveSize); + if (!evaluator.exported) throw std::runtime_error("the program exported nothing"); + Outcome outcome; + outcome.lanes = evaluator.output; + outcome.waveSize = waveSize; + const auto tag = evaluator.maskTag.find(S4); + outcome.maskTagS4 = tag != evaluator.maskTag.end() && tag->second; + const auto bit = evaluator.threadBitValue.find(S4); + if (bit != evaluator.threadBitValue.end() && bit->second->Opcode() == IrOpcode::SelectU1) { + outcome.constantSources = bit->second->Argument(1)->Resolve()->HasImmediate() && bit->second->Argument(2)->Resolve()->HasImmediate(); + } + return outcome; +} + +int failures = 0; + +void expectLanes(const char* name, const Outcome& outcome, std::uint64_t expectedBits) { + for (std::uint32_t lane = 0; lane < outcome.waveSize; ++lane) { + const std::uint32_t expected = static_cast((expectedBits >> lane) & 1u); + if (outcome.lanes[lane] != expected) { + std::fprintf(stderr, "%s: lane %u holds %u, expected %u (expected mask 0x%016llx)\n", name, lane, outcome.lanes[lane], expected, static_cast(expectedBits)); + ++failures; + return; + } + } +} + +void expectMaskPath(const char* name, const Outcome& outcome) { + if (!outcome.maskTagS4) { + std::fprintf(stderr, "%s: the destination pair was not marked as a mask\n", name); + ++failures; + } +} + +void expectConstantSources(const char* name, const Outcome& outcome, bool expected) { + if (outcome.constantSources != expected) { + std::fprintf(stderr, "%s: the sources of the select are %sconstants\n", name, expected ? "not " : ""); + ++failures; + } +} + +constexpr std::uint64_t AllLanes = ~0ull; + +std::uint64_t below(std::uint32_t count) { + return count >= 64u ? AllLanes : (1ull << count) - 1ull; +} + +void selectCase(const char* name, std::vector setup, std::uint32_t trueSource, std::uint32_t falseSource, std::uint64_t whenSccSet, std::uint64_t whenSccClear, bool maskPath, int constantSources = -1) { + for (const bool sccValue : {true, false}) { + auto body = setup; + body.push_back(sCmpScc(sccValue)); + body.push_back(sCselectB64(4u, trueSource, falseSource)); + const auto outcome = run(body, S4); + const std::string label = std::string(name) + (sccValue ? ", scc = 1" : ", scc = 0"); + expectLanes(label.c_str(), outcome, sccValue ? whenSccSet : whenSccClear); + if (maskPath) expectMaskPath(label.c_str(), outcome); + if (constantSources >= 0) expectConstantSources(label.c_str(), outcome, constantSources != 0); + } +} + +} + +int main() { + try { + selectCase("vcc, exec", {vcmpLanesBelow(4)}, Vcc, Exec, below(4), AllLanes, true); + selectCase("exec, vcc", {vcmpLanesBelow(4)}, Exec, Vcc, AllLanes, below(4), true); + selectCase("wave64 halves, vcc, exec", {vcmpLanesBelow(40)}, Vcc, Exec, below(40), AllLanes, true); + selectCase("wave64 halves, exec, vcc", {vcmpLanesBelow(40)}, Exec, Vcc, AllLanes, below(40), true); + selectCase("exec, zero", {}, Exec, Zero, AllLanes, 0ull, true, 0); + selectCase("zero, exec", {}, Zero, Exec, 0ull, AllLanes, true, 0); + selectCase("zero, zero", {}, Zero, Zero, 0ull, 0ull, true, 1); + selectCase("minus one, zero", {}, MinusOne, Zero, AllLanes, 0ull, true, 1); + selectCase("minus one, exec", {}, MinusOne, Exec, AllLanes, AllLanes, true, 0); + selectCase("destination is the true source", {vcmpLanesBelow(40), sMovB64(4u, Vcc)}, S4, Exec, below(40), AllLanes, false); + selectCase("destination is the false source", {vcmpLanesBelow(40), sMovB64(4u, Vcc)}, Exec, S4, AllLanes, below(40), false); + for (const bool sccValue : {true, false}) { + const auto outcome = run({sCmpScc(sccValue), sCselectB64(4u, Literal, Exec), 0x0000ffffu}, S4); + const std::string label = std::string("literal 0xffff, exec") + (sccValue ? ", scc = 1" : ", scc = 0"); + expectLanes(label.c_str(), outcome, sccValue ? below(16) : AllLanes); + } + for (const bool sccValue : {true, false}) { + const auto outcome = run({sCmpScc(sccValue), sCselectB64(4u, Literal, Zero), 0xffffffffu}, S4); + const std::string label = std::string("wave64, literal all ones, zero") + (sccValue ? ", scc = 1" : ", scc = 0"); + expectLanes(label.c_str(), outcome, sccValue ? below(32) : 0ull); + expectConstantSources(label.c_str(), outcome, false); + } + for (const bool sccValue : {true, false}) { + const auto outcome = run({sCmpScc(sccValue), sCselectB64(4u, Literal, Zero), 0xffffffffu}, S4, 32); + const std::string label = std::string("wave32, literal all ones, zero") + (sccValue ? ", scc = 1" : ", scc = 0"); + expectLanes(label.c_str(), outcome, sccValue ? below(32) : 0ull); + expectConstantSources(label.c_str(), outcome, true); + } + for (const bool sccValue : {true, false}) { + const auto outcome = run({vcmpLanesBelow(40), sCmpScc(sccValue), sCselectB64(Vcc, Vcc, Exec)}, Vcc); + expectLanes(sccValue ? "destination vcc, scc = 1" : "destination vcc, scc = 0", outcome, sccValue ? below(40) : AllLanes); + } + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } + return failures == 0 ? 0 : 1; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/Screenshot.cpp b/core/libs/prx/libSceAgcDriver/tests/Screenshot.cpp new file mode 100644 index 0000000000..e6202ceb1a --- /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(_putenv_s("ANYPS5_SCREENSHOTS", directory.string().c_str()) == 0, "set ANYPS5_SCREENSHOTS"); +#else + require(::setenv("ANYPS5_SCREENSHOTS", directory.string().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 / "nested"; + 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 f8466db766..20112c62f3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp @@ -9,6 +9,7 @@ #include #include #include +#include namespace { @@ -16,8 +17,8 @@ using AgcDriver::Graphics::Require; struct ProfileInput { std::vector capabilities{spv::CapabilityShader, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; - std::array extensionStrings{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; - std::array extensions{extensionStrings[0], extensionStrings[1]}; + std::vector extensionStrings{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + std::vector extensions{extensionStrings[0], extensionStrings[1]}; VkPhysicalDeviceRobustness2FeaturesEXT robustness{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT, nullptr, VK_FALSE, VK_FALSE, VK_TRUE}; VkPhysicalDeviceDescriptorIndexingFeatures indexing{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES, &robustness}; VkPhysicalDevice8BitStorageFeatures bytes{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_8BIT_STORAGE_FEATURES, &indexing, VK_TRUE}; @@ -54,7 +55,7 @@ void Reject(TAction action, const char* expected) { Require(std::string(error.what()).find(expected) != std::string::npos, "unexpected profile validation error"); return; } - throw std::runtime_error("invalid shader device profile was accepted"); + throw std::runtime_error(std::string("invalid shader device profile was accepted: ") + expected); } void CheckProfile() { @@ -80,9 +81,14 @@ 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"); - invalid([](auto& value) { value.robustness.nullDescriptor = VK_FALSE; }, "nullDescriptor"); - invalid([](auto& value) { value.indexing.pNext = nullptr; }, "nullDescriptor"); + ProfileInput withoutNull; + withoutNull.robustness.nullDescriptor = VK_FALSE; + Require(!AgcDriver::ShaderDeviceProfile(withoutNull.Target(), withoutNull.device, withoutNull.limits).NullDescriptors(), "a device without nullDescriptor was profiled with it"); + ProfileInput withoutRobustness; + withoutRobustness.indexing.pNext = nullptr; + Require(!AgcDriver::ShaderDeviceProfile(withoutRobustness.Target(), withoutRobustness.device, withoutRobustness.limits).NullDescriptors(), "a device without VK_EXT_robustness2 was profiled with nullDescriptor"); invalid([](auto& value) { value.core.fragmentStoresAndAtomics = VK_FALSE; }, "graphics stores and atomics"); invalid([](auto& value) { value.limits.maxPushConstantsSize = 127u; }, "exceeds device limits"); invalid([](auto& value) { value.limits.maxBoundDescriptorSets = 0u; }, "exceeds device limits"); @@ -95,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); @@ -102,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() { @@ -114,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"); @@ -135,17 +166,31 @@ void CheckHeaps() { image.numericClass = IrTextureNumericClass::Float; image.dimension = RdnaImageDimension::Dim2D; 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"); Reject([&] { allocate(image, 1u, RuntimeAbi::SamplerHeapCapacity + 1u); }, "metadata capacity"); image.resourceClass = ImageResourceClass::Storage; image.mipMode = ImageMipMode::DynamicStorage; - image.mipCount = RuntimeAbi::StorageHeapCapacity; + image.mipCount = RuntimeAbi::StorageMipSlots; const auto storage = allocate(image, 1u); Require(BindingAllocator{}.FindBinding(storage.layout, DescriptorBindingForImage(image)).resources.size() == image.mipCount, "storage heap did not reserve each mip"); - Reject([&] { allocate(image, 2u); }, "heap capacity exceeded"); + const auto shared = allocate(image, 2u); + Require(BindingAllocator{}.FindBinding(shared.layout, DescriptorBindingForImage(image)).resources.size() == 2u * image.mipCount, "a storage heap did not hold two dynamic-mip images"); + const auto capacity = RuntimeAbi::StorageHeapCapacity / image.mipCount; + const auto filled = allocate(image, capacity); + Require(BindingAllocator{}.FindBinding(filled.layout, DescriptorBindingForImage(image)).resources.size() == RuntimeAbi::StorageHeapCapacity, "a full storage heap was not allocated"); + Reject([&] { allocate(image, capacity + 1u); }, "heap capacity exceeded"); Reject([&] { allocate(image, 0u, RuntimeAbi::SamplerHeapCapacity / 2u + 1u); }, "sampler pairs"); const std::array dimensions{RdnaImageDimension::Dim1D, RdnaImageDimension::Dim1DArray, RdnaImageDimension::Dim2D, RdnaImageDimension::Dim2DArray, RdnaImageDimension::Dim3D, RdnaImageDimension::Dim2DMsaa, RdnaImageDimension::Dim2DMsaaArray}; std::set classes; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 89295c7d3a..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" @@ -7,22 +8,43 @@ #include "Optimization/DescriptorBindingBuilder.hpp" #include "ShaderCacheDirectory.hpp" #include "ControlFlow/RequestSerializer.hpp" +#include "Optimization/RequestMemoryView.hpp" +#include "Optimization/SrtWalker.hpp" +#include "IntermediateRepresentation/IrBlock.hpp" #include #include #include #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 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include +#include +#include "prx/libc/include/General.hpp" +#endif namespace { @@ -40,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.samplerUnnormalized == right.samplerUnnormalized && left.imageUnnormalized == right.imageUnnormalized && 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) { @@ -54,17 +134,21 @@ bool sameBindings(const std::vector& left, const std::vector< void requireSameArtifact(const CompiledShaderArtifact& left, const CompiledShaderArtifact& right, const char* what) { const std::string prefix = std::string(what) + ": "; - require(left.spirv.Words() == right.spirv.Words(), prefix + "SPIR-V differs"); + require(left.spirv == right.spirv, prefix + "SPIR-V differs"); require(left.bdaAbiVersion == right.bdaAbiVersion, prefix + "BDA ABI version differs"); require(left.runtimeAbiVersion == right.runtimeAbiVersion, prefix + "runtime ABI version differs"); require(left.memoryOffsetDword == right.memoryOffsetDword, prefix + "memory offset differs"); 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]; @@ -78,14 +162,27 @@ 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]; const auto& b = right.vertexAttributes[i]; 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) { @@ -114,11 +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; } @@ -133,11 +236,15 @@ 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}}}; + result.registrationModule = true; result.specialization = {{512, 4}, {516, 0x3f800000u}, {517, 0}}; result.vertexAttributes = {{1, 4, {{0x1000u, 0x20000u, 0x30u, 0x4u}}, 2}, {5, 2, {{9u, 8u, 7u, 6u}}, 0}}; result.vertexInputs[0].outputMask = 5u; @@ -150,8 +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; } @@ -195,15 +314,25 @@ CompiledVariant sampleVariant() { image.r128 = true; image.fmaskCompatible = false; 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}}; + 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; @@ -223,9 +352,12 @@ CompiledVariant sampleVariant() { info.info.hasBitwiseXor = true; info.info.usesDma = true; info.info.usesFaultBuffer = true; + info.info.rasterPrimitive = true; + info.info.layerExported = true; info.bindings.pushDataStartDword = 2; info.bindings.memoryOffsetDword = 1; info.bindings.memoryOffsetCount = 5; + info.bindings.stencilReferenceDword = 23u; info.bindings.userDataRegisters = {4, 5, 9}; info.bindings.descriptors = {{DescriptorBindingKind::Buffers, {0, 1}}, {DescriptorBindingKind::FlattenedSrt, {}}}; variant.bindings.layout = info.bindings; @@ -327,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(); @@ -350,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(); @@ -390,6 +742,12 @@ void verifyKeySensitivity() { changes("non-constant texel offsets", [](SampleRequest& sample) { sample.request.target.nonConstantImageOffsets = true; }); changes("the sRGB formats decoded in the shader", [](SampleRequest& sample) { sample.request.target.srgbDecodeFormats = 2u; }); changes("the narrow subgroup clock", [](SampleRequest& sample) { sample.request.target.narrowSubgroupClock = true; }); + changes("f64 denormal preservation", [](SampleRequest& sample) { sample.request.target.float64DenormPreserve = true; }); + changes("f16 denormal preservation", [](SampleRequest& sample) { sample.request.target.float16DenormPreserve = true; }); + changes("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; }); @@ -414,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; @@ -474,6 +866,140 @@ 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 + HANDLE file; + explicit Reader(const std::filesystem::path& path) + : file(CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)) { + require(file != INVALID_HANDLE_VALUE, "cannot retain the cache file"); + } + ~Reader() { CloseHandle(file); } + std::vector Read() { + require(SetFilePointerEx(file, {}, nullptr, FILE_BEGIN), "cannot seek the retained cache file"); + std::array bytes{}; + DWORD count = 0; + require(ReadFile(file, bytes.data(), static_cast(bytes.size()), &count, nullptr), "cannot read the retained cache file"); + return {bytes.begin(), bytes.begin() + count}; + } +#else + std::ifstream file; + explicit Reader(const std::filesystem::path& path) : file(path, std::ios::binary) { + require(file.is_open(), "cannot retain the cache file"); + } + std::vector Read() { + file.clear(); + file.seekg(0); + require(!file.fail(), "cannot seek the retained cache file"); + std::array bytes{}; + file.read(reinterpret_cast(bytes.data()), bytes.size()); + require(file.eof() && !file.bad(), "cannot read the retained cache file"); + return {bytes.begin(), bytes.begin() + file.gcount()}; + } +#endif + Reader(const Reader&) = delete; + Reader& operator=(const Reader&) = delete; + }; + const auto root = directory / "atomic-replacement"; + const auto path = root / "entry.bin"; + const std::vector original{std::byte{0x12}, std::byte{0x00}, std::byte{0x34}}; + const std::vector replacement{std::byte{0xab}, std::byte{0xcd}, std::byte{0x00}, std::byte{0xef}}; + require(WriteFileAtomically(path, original), "cannot write the initial cache entry"); +#ifdef _WIN32 + { + const auto renaming = CreateFileW(path.c_str(), DELETE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + require(renaming != INVALID_HANDLE_VALUE, "cannot retain the cache entry's rename handle"); + struct Close { HANDLE file; ~Close() { CloseHandle(file); } } close{renaming}; + std::vector bytes; + require(ReadWholeFile(path, bytes) && bytes == original, "cache reader failed while the published entry retained its rename handle"); + } +#endif + { + Reader old(path); + require(old.Read() == original, "the retained reader did not read the original entry"); + require(WriteFileAtomically(path, replacement), "cannot replace cache entry with a retained reader"); + require(old.Read() == original, "replacement changed the retained original entry"); + std::vector fresh; + require(ReadWholeFile(path, fresh) && fresh == replacement, "a fresh reader did not read the replacement entry"); + Reader current(path); + require(current.Read() == replacement, "the replacement reader did not retain its entry"); + require(std::filesystem::remove(path), "cannot delete the replacement while readers retain it"); + require(!ReadWholeFile(path, fresh), "the deleted cache entry remains readable by path"); + require(old.Read() == original && current.Read() == replacement, "deletion invalidated retained cache readers"); + } + require(std::filesystem::is_empty(root), "atomic replacement left a temporary file behind"); + require(std::filesystem::remove(root), "cannot remove the empty replacement directory"); +} + void verifyStore() { require(ShaderDiskCache::Enabled(), "the disk cache is not enabled"); SampleRequest sample; @@ -505,6 +1031,92 @@ void verifyStore() { require(ShaderDiskCache::Load(key, loaded), "a rewritten entry does not load"); } +void verifyPersist() { + SampleRequest sample; + const auto key = sample.Key(); + const auto variant = sampleVariant(); + const auto persisted = ShaderDiskCache::Persist(key, variant); + require(persisted != nullptr, "a persisted entry was not read back"); + const auto path = ShaderDiskCache::EntryDirectory() / ShaderDiskCache::EntryName(key); +#ifdef _WIN32 + MEMORY_BASIC_INFORMATION region{}; + require(VirtualQuery(persisted->artifact.spirv.data(), ®ion, sizeof(region)) == sizeof(region) && region.Type == MEM_MAPPED, "persisted SPIR-V is not a mapped cache entry"); + const auto close = [](void* handle) { CloseHandle(handle); }; + std::unique_ptr writer(CreateFileW(path.c_str(), GENERIC_WRITE | DELETE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr), close); + require(writer.get() != INVALID_HANDLE_VALUE, "cannot retain the published cache writer"); + CompiledVariant loaded; + require(ShaderDiskCache::Load(key, loaded), "cannot load a cache entry while its writer retains rename access"); + const auto& loadedWords = loaded.artifact.spirv; + require(VirtualQuery(loadedWords.data(), ®ion, sizeof(region)) == sizeof(region) && region.Type == MEM_MAPPED, "cache loading with a retained writer did not map the entry"); + writer.reset(); +#endif + requireSameVariant(variant, *persisted, "persist round trip"); + auto detached = persisted->artifact.spirv; + detached[0] ^= 1u; + require(detached[0] != persisted->artifact.spirv[0], "changing a copied mapped module changed its owner"); + require(WriteFileAtomically(path, std::vector(64, std::byte{0x5a})), "cannot replace the persisted entry"); + std::error_code error; + std::filesystem::remove(path, error); + require(!error, "cannot remove the replaced cache entry"); + requireSameArtifact(variant.artifact, persisted->artifact, "a persisted entry whose file was replaced and removed"); +} + +#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}; @@ -749,7 +1361,7 @@ void verifyBindingPlanSelection() { BindingAllocationResult full; builder.Populate(full, compiled, plan, 0u, snapshot, {}); require(full.bindings.size() == 4u, "binding plan lost a descriptor before module selection"); - require(full.bindings[0].bufferAtomic == std::vector{true, false} && full.bindings[0].bufferWritten == std::vector{true, false}, "binding plan lost buffer access metadata"); + require(full.bindings[0].bufferAtomic == std::vector{true, false} && full.bindings[0].bufferWritten == std::vector{true, false} && full.bindings[0].bufferRead == std::vector{false, true}, "binding plan lost buffer access metadata"); const std::array liveBindings{full.bindings[0].binding, full.bindings[0].binding, full.bindings[3].binding}; const auto selected = builder.Select(plan, liveBindings); snapshot.userData.clear(); @@ -782,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; @@ -855,7 +1468,7 @@ void verifyVertexTypeSpecialization() { } } require(checked == 4u, "vertex specialization lost attribute loads"); - require(artifact.spirv.Words() == original, "vertex specialization mutated the shared template"); + require(std::ranges::equal(artifact.spirv.Words(), original), "vertex specialization mutated the shared template"); } } auto invalid = artifact; @@ -999,6 +1612,831 @@ void verifySpecializationLiveness() { require(SpecializeSpirv(specialized) == specialized, "dead computation removal is not stable"); } +constexpr std::uint64_t PlanSrtAddress = 0x7f0000010000ull; +constexpr std::uint64_t PlanTableAddress = 0x7f0000020000ull; +constexpr std::uint64_t PlanMaterialAddress = 0x7f0000030000ull; +constexpr std::uint64_t PlanPayloadAddress = 0x7f0000040000ull; +constexpr std::uint64_t PlanPointerAddress = 0x7f0000048000ull; +constexpr std::uint64_t PlanOutputAddress = 0x7f0000050000ull; +constexpr std::uint64_t PlanTextureAddress = 0x7f0000060000ull; +constexpr std::uint64_t PlanUnmappedAddress = 0x7f00000f0000ull; + +struct PlanFixture { + std::string name; + ShaderStage stage = ShaderStage::Compute; + std::uint64_t codeAddress = 0; + std::uint32_t waveSize = 64; + std::vector code; + std::vector userData; + std::vector>> regions; + std::optional pixel; + std::vector memory; + + RecompileRequest Request(bool useCache) { + static constexpr std::array capabilities{spv::CapabilityShader, spv::CapabilityImageGatherExtended, spv::CapabilitySampledImageArrayDynamicIndexing, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityClipDistance}; + static constexpr std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + memory.clear(); + for (const auto& [address, words] : regions) memory.push_back({address, std::as_bytes(std::span(words))}); + RecompileRequest request{}; + request.shader = {stage, codeAddress, code, 0, {}}; + request.context.waveSize = waveSize; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + if (stage == ShaderStage::Compute) request.context.compute = ShaderComputeStageInfo{{waveSize, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.context.pixel = pixel; + request.context.memory = memory; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.bdaAbiVersion = 1u; + request.layout.pushConstantSizeBytes = 128; + request.useCache = useCache; + return request; + } +}; + +std::vector imageWords(std::uint64_t base) { + return {static_cast(base >> 8u), static_cast((base >> 40u) & 0xffu) | (56u << 20u) | (3u << 30u), 3u << 14u, 0xfacu | (9u << 28u), 0u, 0u, 0u, 0u}; +} + +std::vector bufferWords(std::uint64_t base, std::uint32_t stride, std::uint32_t records, std::uint32_t format = 0xfacu) { + return {static_cast(base), static_cast((base >> 32u) & 0xffffu) | (stride << 16u), records, format}; +} + +void place(std::vector& words, std::size_t offset, const std::vector& values) { + std::copy(values.begin(), values.end(), words.begin() + static_cast(offset)); +} + +std::vector planFixtures() { + std::vector fixtures; + const std::vector srtPointer{static_cast(PlanSrtAddress), static_cast(PlanSrtAddress >> 32u)}; + const auto add = [&](std::string name, std::uint32_t waveSize, std::vector code, std::vector userData, std::vector>> regions) { + PlanFixture fixture; + fixture.name = std::move(name); + fixture.codeAddress = 0x100000u + static_cast(fixtures.size()) * 0x1000u; + fixture.waveSize = waveSize; + fixture.code = std::move(code); + fixture.userData = std::move(userData); + fixture.regions = std::move(regions); + fixtures.push_back(std::move(fixture)); + }; + add("user data buffer", 64, {0xe0700000u, 0x80000000u, 0xbf810000u}, bufferWords(PlanOutputAddress, 0u, 0x40u, 0x00027facu), {}); + + std::vector guarded(32, 0u); + place(guarded, 0, imageWords(PlanTextureAddress)); + place(guarded, 16, bufferWords(PlanOutputAddress, 0u, 16u)); + guarded[20] = 1u; + add("guarded image", 32, {0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xbf068010u, 0xbf850006u, 0xf40c0200u, 0xfa000000u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, guarded}}); + add("guarded image stores", 32, {0xf4000400u, 0xfa000050u, 0xbf8cc07fu, 0xbf068010u, 0xbf850006u, 0xf40c0100u, 0xfa000000u, 0xbf8cc07fu, 0xf0200f08u, 0x00010004u, 0xbf820005u, 0xf40c0100u, 0xfa000000u, 0xbf8cc07fu, 0xf0200f10u, 0x00010004u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, guarded}}); + + std::vector heap(32, 0u); + place(heap, 0, imageWords(PlanTextureAddress)); + place(heap, 8, imageWords(PlanTextureAddress + 0x100u)); + place(heap, 24, imageWords(PlanTextureAddress)); + const std::vector materials{0u, 1u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 3u, 0u, 0u}; + std::vector tables(16, 0u); + place(tables, 0, bufferWords(PlanTableAddress, 32u, 4u)); + place(tables, 8, bufferWords(PlanMaterialAddress, 16u, 3u)); + place(tables, 12, bufferWords(PlanOutputAddress, 0u, 16u)); + add("bindless material table", 64, {0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x93109010u, 0xf4200406u, 0x20000004u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, tables}, {PlanTableAddress, heap}, {PlanMaterialAddress, materials}}); + add("bindless whole table", 64, {0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, tables}, {PlanTableAddress, heap}, {PlanMaterialAddress, materials}}); + add("bindless sample in a loop latch", 64, {0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xbea80380u, 0xf09c0f08u, 0x00450000u, 0x80288128u, 0xbf0a8428u, 0xbf85fffbu, 0xe0700000u, 0x80070000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, tables}, {PlanTableAddress, heap}, {PlanMaterialAddress, materials}}); + 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)); + place(phis, 12, {0x49u, 0u, 0x00500000u, 0u}); + place(phis, 16, bufferWords(PlanOutputAddress, 0u, 16u)); + phis[20] = 1u; + place(phis, 24, imageWords(PlanTextureAddress + 0x100u)); + add("descriptor phi sampler", 32, {0xf40c0200u, 0xfa000000u, 0xf4000400u, 0xfa000050u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf4080500u, 0xfa000020u, 0xbf820002u, 0xf4080500u, 0xfa000030u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, phis}}); + add("descriptor phi image", 32, {0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf40c0200u, 0xfa000000u, 0xbf820002u, 0xf40c0200u, 0xfa000060u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, phis}}); + add("descriptor phi loop", 32, {0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xf40c0200u, 0xfa000000u, 0xbe910380u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf068011u, 0xbf850003u, 0xf40c0200u, 0xfa000000u, 0xbf820002u, 0xf40c0200u, 0xfa000060u, 0xbf8cc07fu, 0x80118111u, 0xbf0a8211u, 0xbf85fff0u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, phis}}); + add("descriptor phi in sequence", 32, {0xf4000400u, 0xfa000050u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf40c0200u, 0xfa000000u, 0xbf820002u, 0xf40c0200u, 0xfa000060u, 0xbf068010u, 0xbf850003u, 0xf4080500u, 0xfa000020u, 0xbf820002u, 0xf4080500u, 0xfa000030u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, phis}}); + + std::vector root(16, 0u); + place(root, 0, bufferWords(PlanOutputAddress, 0u, 256u, 0x30027facu)); + root[4] = 1u; + root[6] = static_cast(PlanPayloadAddress); + root[7] = static_cast(PlanPayloadAddress >> 32u); + const std::vector payload{0x3f800000u}; + add("guarded nested read", 64, {0xf4080100u, 0xfa000000u, 0xf4000200u, 0xfa000010u, 0xf4040280u, 0xfa000018u, 0xbe8c03ffu, 0x11111111u, 0xbf8cc07fu, 0xbf068008u, 0xbf850003u, 0xf4000305u, 0xfa000000u, 0xbf8cc07fu, 0x7e02020cu, 0x34040082u, 0xe0701000u, 0x80010102u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, root}, {PlanPayloadAddress, payload}}); + auto chained = root; + chained[6] = static_cast(PlanPointerAddress); + chained[7] = static_cast(PlanPointerAddress >> 32u); + add("guarded pointer chain", 64, {0xf4080100u, 0xfa000000u, 0xf4000200u, 0xfa000010u, 0xf4040280u, 0xfa000018u, 0xbe8c03ffu, 0x11111111u, 0xbf8cc07fu, 0xbf068008u, 0xbf850006u, 0xf4040385u, 0xfa000000u, 0xbf8cc07fu, 0xf4000307u, 0xfa000000u, 0xbf8cc07fu, 0x7e02020cu, 0x34040082u, 0xe0701000u, 0x80010102u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, chained}, {PlanPayloadAddress, payload}, {PlanPointerAddress, {static_cast(PlanPayloadAddress), static_cast(PlanPayloadAddress >> 32u)}}}); + add("descriptor through a pointer", 64, {0xf4040280u, 0xfa000018u, 0xbf8cc07fu, 0xf4080105u, 0xfa000000u, 0xbf8cc07fu, 0x34040082u, 0xe0701000u, 0x80010002u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, chained}, {PlanPointerAddress, bufferWords(PlanOutputAddress, 0u, 256u, 0x30027facu)}}); + add("descriptor invariant phi", 32, {0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf40c0200u, 0xfa000000u, 0xbf820002u, 0xf40c0200u, 0xfa000000u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, phis}}); + + 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; + fragment.codeAddress = 0x100000u + static_cast(fixtures.size()) * 0x1000u; + fragment.code = {0xc8100000u, 0xc8110001u, 0xc8140402u, 0xc8150403u, 0xf800180fu, 0x05040504u, 0xbf810000u}; + ShaderPixelStageInfo pixel{}; + pixel.interpolatorCount = 2u; + pixel.interpolatorSettings[1] = 1u; + pixel.wave32 = true; + pixel.inputAddr = PixelInputBit(PixelInput::PerspectiveCenter) | PixelInputBit(PixelInput::LinearCenter); + pixel.hasPerspectiveCenterVgpr = true; + pixel.noPerspective = true; + pixel.targetOutputMode[0] = 9u; + pixel.targetExportMapping[0] = 0xe4u; + fragment.pixel = pixel; + fixtures.push_back(std::move(fragment)); + return fixtures; +} + +IrResourcePlan handBuiltPlan() { + IrResourcePlan plan; + plan.valueStorage.Reserve(64); + std::uint32_t ids = 0; + const auto make = [&](IrOpcode opcode, IrType type) -> IrValue& { + return *plan.valueStorage.Create(opcode, type, ids++); + }; + const auto constant = [&](std::uint32_t value) -> IrValue& { + auto& immediate = make(IrOpcode::Void, IrType::U32); + immediate.SetImmediateU32(value); + return immediate; + }; + auto& resource = make(IrOpcode::GetSrtResource, IrType::SrtResource); + const auto readConst = [&](std::uint32_t slot) -> IrValue& { + auto& read = make(IrOpcode::ReadConst, IrType::U32); + read.AddArgument(&resource); + read.AddArgument(&constant(slot)); + return read; + }; + const auto userData = [&](std::uint32_t index) -> IrValue& { + auto& reg = make(IrOpcode::Void, IrType::ScalarReg); + reg.SetRegister({RegisterBank::Scalar, index}); + auto& read = make(IrOpcode::GetUserData, IrType::U32); + read.AddArgument(®); + return read; + }; + auto& handle = make(IrOpcode::CompositeConstructU64, IrType::U64); + handle.AddArgument(&userData(0)); + handle.AddArgument(&userData(1)); + auto& pointer = make(IrOpcode::LoadAddressU32, IrType::U32); + pointer.AddArgument(&handle); + pointer.AddArgument(&constant(8)); + plan.blockStorage.push_back(std::make_unique(3u)); + plan.blockStorage.push_back(std::make_unique(7u)); + auto& phi = make(IrOpcode::Phi, IrType::U32); + phi.AddPhiOperand(plan.blockStorage[0].get(), &readConst(0)); + phi.AddPhiOperand(plan.blockStorage[1].get(), &constant(0)); + auto& condition = make(IrOpcode::Void, IrType::Bool); + condition.SetImmediateU64(1u); + condition.SetFlags(std::uint64_t{0x1234u}); + plan.stage = IrShaderStage::Compute; + plan.shaderHash = 0x123456789abcull; + plan.userDataBase = 2u; + plan.userDataCount = 4u; + plan.srgbDecodeFormats = 5u; + MemoryInfo memory; + memory.kind = ResourceKind::Image; + memory.resource = 1u; + memory.dmask = 0xfu; + memory.imageDimension = RdnaImageDimension::Dim2D; + memory.gpuDescriptor = true; + memory.flushDenormals = true; + MemoryInfo every; + every.kind = ResourceKind::Buffer; + every.resource = 2u; + every.sampler = 3u; + every.offset = 0x14u; + every.secondaryOffset = 0x28u; + every.dmask = 0x5u; + every.dataDwords = 3u; + every.dataBits = 16u; + every.componentIndex = 1u; + every.componentCount = 2u; + every.dataFormat = 10u; + every.numberFormat = 7u; + every.imageSampleFlags = 0x21u; + every.imageDimension = RdnaImageDimension::Dim3D; + every.imageAddressComponents = 3u; + every.addressIsFull = true; + every.dataSigned = true; + every.typed = true; + every.formatted = true; + every.d16 = true; + every.imageHasMip = true; + every.imageR128 = true; + every.imagePacked = true; + every.imageByElements = 2u; + every.idxen = true; + every.offen = true; + every.planningOnly = true; + every.coherent = true; + every.gpuDescriptor = true; + every.flushDenormals = true; + plan.memoryInfo = {memory, every, MemoryInfo{}}; + DescriptorSource buffer; + buffer.dwordCount = 4u; + buffer.dwords = {&readConst(1), &readConst(2), &phi, &pointer}; + DescriptorSource table; + table.dwordCount = 8u; + table.dwords[0] = &readConst(3); + table.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); + DescriptorSource material; + material.dwordCount = 4u; + material.dwords[2] = &readConst(7); + auto& wide = make(IrOpcode::Void, IrType::U64); + wide.SetImmediateU64(0x8000000000000001ull); + material.dwords[3] = &wide; + plan.descriptorSources = {buffer, table, heap, material}; + plan.controlFlow = {{&condition, {1u, 2u}, {0u}, {}, {1u}}, {nullptr, {2u}, {3u}, {0u}, {0u, 1u}}, {nullptr, {}, {}, {}, {}}}; + plan.materializationSources = {0u, 2u}; + plan.srtReads = {{&readConst(4), 2u}, {&pointer, 1u}}; + plan.guardedSrtSlots = {1u}; + plan.srtGuardOffset = 6u; + plan.cleanFlatSlots = {0u, 1u}; + plan.pureFlatSlots = {1u, 0u}; + plan.requiresSpecializationMemory = true; + plan.srtPlanComplete = true; + plan.resourceTrackingComplete = true; + plan.info.buffers = {BufferResource{}, BufferResource{2u, 0x40u, 0x100u, 0u, true, true, true, true, true, 0x7u, true, 4u}}; + ImageResource image{}; + image.source = 1u; + image.resourceClass = ImageResourceClass::Sampled; + image.dimension = RdnaImageDimension::Dim2D; + plan.info.images = {image}; + ResourceMaterializer::PrepareImageModes(plan.info); + plan.uniformFill.fill = {UniformFillKind::Buffer, 1u, {4u, 8u, 12u}, 2u, 0x3f800000u}; + auto& fillValue = readConst(5); + plan.uniformFill.values = {&fillValue, &fillValue, nullptr, nullptr}; + return plan; +} + +std::vector encodedPlan(const IrResourcePlan& plan) { + std::vector bytes; + require(ShaderDiskCache::EncodePlan(plan, bytes), "a self-contained plan does not encode"); + return bytes; +} + +bool samePlan(const IrResourcePlan& left, const IrResourcePlan& right) { + if (left.valueStorage.Size() != right.valueStorage.Size() || left.blockStorage.size() != right.blockStorage.size()) return false; + std::map leftValues; + std::map rightValues; + std::map leftBlocks; + std::map rightBlocks; + for (std::size_t index = 0; index < left.valueStorage.Size(); ++index) { + leftValues.emplace(&left.valueStorage[index], 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); + rightBlocks.emplace(right.blockStorage[index].get(), index); + } + const auto sameValue = [&](const IrValue* a, const IrValue* b) { + if (a == nullptr || b == nullptr) return a == b; + const auto foundA = leftValues.find(a); + const auto foundB = rightValues.find(b); + return foundA != leftValues.end() && foundB != rightValues.end() && foundA->second == foundB->second; + }; + const auto sameBlock = [&](const IrBlock* a, const IrBlock* b) { + const auto foundA = leftBlocks.find(a); + const auto foundB = rightBlocks.find(b); + return foundA != leftBlocks.end() && foundB != rightBlocks.end() && foundA->second == foundB->second; + }; + for (std::size_t index = 0; index < left.blockStorage.size(); ++index) { + if (left.blockStorage[index]->Id() != right.blockStorage[index]->Id()) return false; + } + for (std::size_t index = 0; index < left.valueStorage.Size(); ++index) { + const auto& a = left.valueStorage[index]; + const auto& b = right.valueStorage[index]; + if (a.Opcode() != b.Opcode() || a.Type() != b.Type() || a.Id() != b.Id() || a.Flags() != b.Flags() || !(a.Register() == b.Register()) || a.HasImmediate() != b.HasImmediate() || (a.HasImmediate() && a.ImmediateU64() != b.ImmediateU64())) return false; + if (a.ArgumentCount() != b.ArgumentCount() || a.IsPhi() != b.IsPhi() || a.PhiBlockCount() != b.PhiBlockCount()) return false; + for (std::size_t operand = 0; operand < a.ArgumentCount(); ++operand) { + if (!sameValue(a.Argument(operand), b.Argument(operand))) return false; + } + for (std::size_t operand = 0; operand < a.PhiBlockCount(); ++operand) { + if (!sameBlock(a.PhiBlock(operand), b.PhiBlock(operand))) return false; + } + } + if (left.stage != right.stage || left.shaderHash != right.shaderHash || left.userDataBase != right.userDataBase || left.userDataCount != right.userDataCount || left.srgbDecodeFormats != right.srgbDecodeFormats) return false; + if (left.memoryInfo != right.memoryInfo || left.descriptorSources.size() != right.descriptorSources.size() || left.controlFlow.size() != right.controlFlow.size() || left.srtReads.size() != right.srtReads.size()) return false; + for (std::size_t index = 0; index < left.descriptorSources.size(); ++index) { + const auto& a = left.descriptorSources[index]; + const auto& b = right.descriptorSources[index]; + if (a.dwordCount != b.dwordCount || a.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; + } + } + 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 || 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; + } + for (std::size_t index = 0; index < left.uniformFill.values.size(); ++index) { + if (!sameValue(left.uniformFill.values[index], right.uniformFill.values[index])) return false; + } + return left.materializationSources == right.materializationSources && left.guardedSrtSlots == right.guardedSrtSlots && left.srtGuardOffset == right.srtGuardOffset && left.cleanFlatSlots == right.cleanFlatSlots && left.pureFlatSlots == right.pureFlatSlots && + left.requiresSpecializationMemory == right.requiresSpecializationMemory && left.srtPlanComplete == right.srtPlanComplete && left.resourceTrackingComplete == right.resourceTrackingComplete && left.info == right.info && left.uniformFill.fill == right.uniformFill.fill; +} + +struct MaterializedPlan { + bool ok = false; + std::string error; + ResourceSnapshot snapshot; + SrtReadTrace trace; + std::size_t leaf = 0; +}; + +MaterializedPlan materializePlan(const IrResourcePlan& plan, SrtRuntime runtime) { + MaterializedPlan outcome; + runtime.readTrace = &outcome.trace; + try { + ResourceMaterializer{}.Materialize(plan, runtime, outcome.snapshot); + outcome.ok = true; + } catch (const std::exception& error) { + outcome.error = error.what(); + } + outcome.leaf = static_cast(std::find_if(plan.valueStorage.begin(), plan.valueStorage.end(), [&](const auto& value) { return &value == 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 && 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; +} + +void mutateWords(std::vector& words, std::mt19937& random) { + static constexpr std::array addresses{PlanSrtAddress, PlanTableAddress, PlanMaterialAddress, PlanPayloadAddress, PlanPointerAddress, PlanOutputAddress, PlanTextureAddress, PlanUnmappedAddress}; + for (auto& word : words) { + switch (random() % 16u) { + case 0: word = static_cast(random()); break; + case 1: word = 0u; break; + case 2: { + const auto address = addresses[random() % addresses.size()] + (random() % 8u) * 4u; + word = random() % 2u == 0u ? static_cast(address) : static_cast(address >> 32u); + break; + } + case 3: word ^= 1u << (random() % 32u); break; + default: break; + } + } +} + +std::size_t verifyRoundTripRuntimes(const IrResourcePlan& plan, const IrResourcePlan& decoded, const PlanFixture& fixture, std::uint32_t seed, const std::string& what) { + std::mt19937 random(seed); + std::size_t succeeded = 0; + for (std::uint32_t run = 0; run < 1000u; ++run) { + auto userData = fixture.userData; + auto regions = fixture.regions; + if (userData.size() < plan.userDataCount) userData.resize(plan.userDataCount, 0u); + if (run != 0u) { + mutateWords(userData, random); + for (auto& region : regions) mutateWords(region.second, random); + } + std::vector memory; + for (const auto& [address, words] : regions) memory.push_back({address, std::as_bytes(std::span(words))}); + RequestMemoryView view(memory); + const auto runtime = view.MakeRuntime(userData, fixture.codeAddress); + const auto original = materializePlan(plan, runtime); + const auto loaded = materializePlan(decoded, runtime); + require(sameMaterialization(original, loaded), what + ": runtime " + std::to_string(run) + " materialized differently from the decoded plan (" + original.error + " / " + loaded.error + ")"); + if (original.ok) ++succeeded; + } + return succeeded; +} + +void verifyPlanEntryDamage(const std::vector& key, const IrResourcePlan& plan, const std::string& what) { + const auto file = ShaderDiskCache::EncodePlanEntry(key, plan); + require(!file.empty(), what + ": the plan entry was not encoded"); + IrResourcePlan decoded; + require(ShaderDiskCache::DecodePlanEntry(file, key, decoded) == ShaderDiskCache::LoadStatus::Loaded && encodedPlan(decoded) == encodedPlan(plan), what + ": the plan entry does not decode to the plan"); + for (std::size_t size = 0; size < file.size(); size += size < 512 ? 1 : 37) { + IrResourcePlan partial; + require(ShaderDiskCache::DecodePlanEntry(std::span(file).first(size), key, partial) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry truncated to " + std::to_string(size) + " bytes is not rejected"); + } + for (std::size_t offset = 0; offset < file.size(); offset += offset < 512 ? 1 : 29) { + for (const auto bit : {std::byte{0x01}, std::byte{0x80}}) { + auto damaged = file; + damaged[offset] ^= bit; + IrResourcePlan partial; + require(ShaderDiskCache::DecodePlanEntry(damaged, key, partial) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry with byte " + std::to_string(offset) + " flipped is not rejected"); + } + } + auto longer = file; + longer.push_back(std::byte{0}); + require(ShaderDiskCache::DecodePlanEntry(longer, key, decoded) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry with a trailing byte is not rejected"); + auto otherKey = key; + otherKey.back() ^= std::byte{1}; + require(ShaderDiskCache::DecodePlanEntry(file, otherKey, decoded) == ShaderDiskCache::LoadStatus::KeyMismatch, what + ": a plan entry for another key loads"); + auto previousFormat = file; + previousFormat[4] = static_cast(ShaderDiskCache::FormatVersion - 1u); + require(ShaderDiskCache::DecodePlanEntry(previousFormat, key, decoded) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry of the previous format loads"); + auto otherSource = file; + otherSource[8] ^= std::byte{1}; + require(ShaderDiskCache::DecodePlanEntry(otherSource, key, decoded) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry of another source version loads"); + CompiledVariant variant; + require(ShaderDiskCache::DecodeEntry(file, key, variant) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry decodes as a compiled variant"); +} + +void verifyPlanPayloadDamage(const IrResourcePlan& plan, const std::string& what) { + const auto bytes = encodedPlan(plan); + for (std::size_t size = 0; size < bytes.size(); ++size) { + IrResourcePlan partial; + require(!ShaderDiskCache::DecodePlan(std::span(bytes).first(size), partial), what + ": a plan truncated to " + std::to_string(size) + " bytes decodes"); + } + std::size_t rejected = 0; + for (std::size_t offset = 0; offset < bytes.size(); ++offset) { + for (const auto bit : {std::byte{0x01}, std::byte{0x40}}) { + auto damaged = bytes; + damaged[offset] ^= bit; + IrResourcePlan decoded; + if (!ShaderDiskCache::DecodePlan(damaged, decoded)) { + ++rejected; + continue; + } + std::vector again; + require(ShaderDiskCache::EncodePlan(decoded, again), what + ": a plan accepted with byte " + std::to_string(offset) + " flipped is not self-contained"); + } + } + require(rejected != 0u, what + ": no damaged plan was rejected"); +} + +void verifyMalformedPlans() { + const auto rejects = [](const std::string& what, const std::function& damage) { + auto plan = handBuiltPlan(); + damage(plan); + std::vector bytes; + require(ShaderDiskCache::EncodePlan(plan, bytes), "malformed plan: " + what + " did not encode"); + IrResourcePlan decoded; + require(!ShaderDiskCache::DecodePlan(bytes, decoded), "malformed plan: " + what + " decodes"); + }; + rejects("a branch with one successor", [](IrResourcePlan& plan) { plan.controlFlow[0].successors.pop_back(); }); + rejects("a successor past the blocks", [](IrResourcePlan& plan) { plan.controlFlow[1].successors[0] = 3u; }); + rejects("a block source past the sources", [](IrResourcePlan& plan) { plan.controlFlow[0].sources[0] = 4u; }); + rejects("a block image past the images", [](IrResourcePlan& plan) { plan.controlFlow[1].images[0] = 1u; }); + rejects("a materialized source past the sources", [](IrResourcePlan& plan) { plan.materializationSources.push_back(4u); }); + rejects("a buffer past the sources", [](IrResourcePlan& plan) { plan.info.buffers[0].source = 4u; }); + rejects("an image past the sources", [](IrResourcePlan& plan) { plan.info.images[0].source = 5u; }); + rejects("a descriptor of nine dwords", [](IrResourcePlan& plan) { plan.descriptorSources[1].dwordCount = 9u; }); + rejects("a uniform fill of five words", [](IrResourcePlan& plan) { plan.uniformFill.fill.words = 5u; }); + rejects("an image table heap past the sources", [](IrResourcePlan& plan) { plan.descriptorSources[1].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.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"); +} + +void verifyPlanRoundTrip() { + const auto handBuilt = handBuiltPlan(); + const auto handBytes = encodedPlan(handBuilt); + IrResourcePlan handDecoded; + require(ShaderDiskCache::DecodePlan(handBytes, handDecoded), "the hand-built plan does not decode"); + require(encodedPlan(handDecoded) == handBytes && handDecoded.valueStorage.Size() == handBuilt.valueStorage.Size() && handDecoded.blockStorage.size() == handBuilt.blockStorage.size(), "the hand-built plan changed in a round trip"); + const auto& phi = *handDecoded.descriptorSources[0].dwords[2]; + require(phi.IsPhi() && phi.PhiBlockCount() == 2u && phi.PhiBlock(0)->Id() == 3u && phi.PhiBlock(1)->Id() == 7u && phi.Argument(1)->ImmediateU32() == 0u, "the decoded phi lost its blocks or operands"); + require(handDecoded.uniformFill.values[0] == handDecoded.uniformFill.values[1] && handDecoded.uniformFill.values[0] != nullptr && handDecoded.uniformFill.values[2] == nullptr, "the decoded uniform fill lost its shared or null values"); + require(samePlan(handBuilt, handDecoded), "the decoded hand-built plan differs from the plan in a field or a value"); + PlanFixture handFixture; + handFixture.userData = {static_cast(PlanSrtAddress), static_cast(PlanSrtAddress >> 32u), 0u, 0u}; + std::vector handSrt(16, 0u); + place(handSrt, 0, bufferWords(PlanOutputAddress, 0u, 16u)); + handFixture.regions = {{PlanSrtAddress, handSrt}, {PlanPayloadAddress, {0x3f800000u}}}; + static_cast(verifyRoundTripRuntimes(handBuilt, handDecoded, handFixture, 7u, "hand-built plan")); + verifyPlanPayloadDamage(handBuilt, "hand-built plan"); + verifyMalformedPlans(); + + auto fixtures = planFixtures(); + std::vector> plans; + std::size_t index = 0; + for (auto& fixture : fixtures) { + auto request = fixture.Request(false); + const auto plan = GetResourcePlan(request); + plans.push_back(plan); + const auto bytes = encodedPlan(*plan); + IrResourcePlan decoded; + require(ShaderDiskCache::DecodePlan(bytes, decoded), fixture.name + ": the plan does not decode"); + require(encodedPlan(decoded) == bytes, fixture.name + ": the plan changed in a round trip"); + require(samePlan(*plan, decoded), fixture.name + ": the decoded plan differs from the plan in a field or a value"); + require(decoded.valueStorage.Size() == plan->valueStorage.Size() && decoded.blockStorage.size() == plan->blockStorage.size(), fixture.name + ": the decoded plan holds another graph"); + const auto succeeded = verifyRoundTripRuntimes(*plan, decoded, fixture, 0x5eedu + static_cast(index), fixture.name); + require(succeeded != 0u, fixture.name + ": no runtime materialized"); + std::vector key; + ShaderDiskCache::BuildPlanKey(request, 32u, key); + if (index % 4u == 0u) verifyPlanEntryDamage(key, *plan, fixture.name); + std::cout << "plan round trip: " << fixture.name << ", " << plan->valueStorage.Size() << " values, " << ShaderDiskCache::EncodePlanEntry(key, *plan).size() << " bytes per entry, " << succeeded << " of 1000 runtimes materialized\n"; + ++index; + } + 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.tableColumn.has_value() && source.tableColumn->keyDomains.empty(); }); }), "no fixture plans a whole image table"); + require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.descriptorSources.begin(), plan.descriptorSources.end(), [](const DescriptorSource& source) { return source.tableColumn.has_value() && !source.tableColumn->keyDomains.empty(); }); }), "no fixture plans an image table with a key domain"); + require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.descriptorSources.begin(), plan.descriptorSources.end(), [](const DescriptorSource& source) { return source.tableColumn.has_value() && source.tableColumn->address; }); }), "no fixture plans an image table through a pointer"); + require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.descriptorSources.begin(), plan.descriptorSources.end(), [](const DescriptorSource& source) { return source.tableColumn.has_value() && source.tableColumn->selected; }); }), "no fixture plans a selected image table record"); + require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.controlFlow.begin(), plan.controlFlow.end(), [](const ResourceBlock& block) { return block.condition != nullptr; }); }), "no fixture plans a control-flow condition"); + require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.valueStorage.begin(), plan.valueStorage.end(), [](const auto& value) { return value.IsPhi(); }); }), "no fixture plans a phi"); + require(covers([](const IrResourcePlan& plan) { return !plan.guardedSrtSlots.empty(); }), "no fixture plans a guarded SRT read"); + require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.pureFlatSlots.begin(), plan.pureFlatSlots.end(), [](std::uint8_t pure) { return pure != 0u; }); }), "no fixture plans a pure flat slot"); +} + +void verifyPlanKey() { + auto fixtures = planFixtures(); + auto& fixture = fixtures[1]; + const auto planKey = [&](const std::function& change, std::uint32_t hostSubgroupSize = 32u) { + auto request = fixture.Request(true); + change(request); + std::vector key; + ShaderDiskCache::BuildPlanKey(request, hostSubgroupSize, key); + return key; + }; + const auto key = planKey([](RecompileRequest&) {}); + require(planKey([](RecompileRequest&) {}) == key, "the plan key is not deterministic"); + std::vector variantKey; + ShaderDiskCache::BuildKey(fixture.Request(true), 32u, variantKey); + require(variantKey != key && ShaderDiskCache::EntryName(variantKey) != ShaderDiskCache::EntryName(key), "the plan and the compiled variant share an entry"); + require(planKey([](RecompileRequest& request) { request.layout = {1u, 4u, 16u, 64u}; }) == key, "the binding layout changed the plan key"); + require(planKey([](RecompileRequest& request) { request.shader.codeAddress += 0x1000u; request.context.memory = {}; }) == key, "the code address or the memory changed the plan key"); + auto userData = fixture.userData; + userData[0] ^= 0x100u; + require(planKey([&](RecompileRequest& request) { request.context.userData = userData; }) == key, "user data values changed the plan key"); + const auto original = fixture.code; + for (std::size_t word = 0; word < original.size(); ++word) { + for (std::uint32_t bit = 0; bit < 32u; ++bit) { + fixture.code = original; + fixture.code[word] ^= 1u << bit; + require(planKey([](RecompileRequest&) {}) != key, "the plan key ignores code word " + std::to_string(word) + " bit " + std::to_string(bit)); + } + } + fixture.code = original; + fixture.code.push_back(0xbf810000u); + require(planKey([](RecompileRequest&) {}) != key, "the plan key ignores an appended code word"); + fixture.code = original; + require(planKey([](RecompileRequest&) {}, 64u) != key, "the plan key ignores the host subgroup size"); + require(planKey([](RecompileRequest& request) { request.target.subgroupSize = 64u; }) != key, "the plan key ignores the target"); + require(planKey([](RecompileRequest& request) { request.target.srgbDecodeFormats = 2u; }) != key, "the plan key ignores the sRGB formats decoded in the shader"); + require(planKey([](RecompileRequest& request) { request.context.waveSize = 64u; request.context.compute->numThreads[0] = 64u; }) != key, "the plan key ignores the wave size"); + require(planKey([](RecompileRequest& request) { request.context.floatMode = ShaderFloatMode{0xc0u, true, false, false}; }) != key, "the plan key ignores the float mode"); +} + +void appendWords(std::vector& out, std::span words) { + const auto count = static_cast(words.size()); + const auto countBytes = std::as_bytes(std::span(&count, 1)); + out.insert(out.end(), countBytes.begin(), countBytes.end()); + const auto bytes = std::as_bytes(words); + out.insert(out.end(), bytes.begin(), bytes.end()); +} + +void appendSnapshot(std::vector& out, const ResourceCapture& capture) { + const auto& snapshot = capture.snapshot; + for (const auto* values : {&snapshot.buffers, &snapshot.images, &snapshot.samplers}) { + std::vector words; + for (const auto& value : *values) { + words.push_back(value.dwordCount); + words.insert(words.end(), value.dwords.begin(), value.dwords.end()); + } + appendWords(out, words); + } + appendWords(out, snapshot.flattenedSrt); + appendWords(out, snapshot.userData); + const auto& fill = snapshot.uniformFill; + appendWords(out, std::vector{static_cast(fill.kind), fill.resource, fill.groupStride[0], fill.groupStride[1], fill.groupStride[2], fill.words, fill.value, snapshot.nullRootReads}); + std::vector poison; + for (const auto& entry : snapshot.srtPoison) poison.insert(poison.end(), {entry.slot, entry.pc, static_cast(entry.address), static_cast(entry.address >> 32u)}); + appendWords(out, poison); + std::vector leaves{capture.readTrace.leafSlot}; + for (const auto& [slot, address] : capture.readTrace.leaves) leaves.insert(leaves.end(), {slot, static_cast(address), static_cast(address >> 32u)}); + appendWords(out, leaves); + std::vector reads; + for (const auto address : capture.readTrace.otherReads) reads.insert(reads.end(), {static_cast(address), static_cast(address >> 32u)}); + appendWords(out, reads); +} + +std::vector> preparedOutputs(std::vector& fixtures, std::vector>* plans = nullptr) { + std::vector> outputs; + for (auto& fixture : fixtures) { + const auto request = fixture.Request(true); + const auto handle = PrepareShader(request); + RequestMemoryView view(request.context.memory); + const auto capture = CaptureResources(request, view.MakeRuntime(request.context.userData, request.shader.codeAddress), *handle); + const auto result = MaterializeShader(request, *capture, *handle); + std::vector bytes; + appendWords(bytes, GetPreparedArtifact(*handle).spirv.Words()); + ShaderDiskCache::EncodeResult(*result, bytes); + appendSnapshot(bytes, *capture); + require(ShaderDiskCache::EncodePlan(*capture->plan, bytes), fixture.name + ": the captured plan does not encode"); + if (plans != nullptr) plans->push_back(capture->plan); + outputs.push_back(std::move(bytes)); + } + return outputs; +} + +std::filesystem::path planOutputPath(std::size_t index) { + return ShaderCacheDirectory() / "plan-outputs" / ("fixture-" + std::to_string(index) + ".bin"); +} + +std::filesystem::path planEntryPath(PlanFixture& fixture) { + std::vector key; + ShaderDiskCache::BuildPlanKey(fixture.Request(true), 32u, key); + return ShaderDiskCache::EntryDirectory() / ShaderDiskCache::EntryName(key); +} + +int runPlanLoadingProcess(std::string_view mode) { + if (mode == "--load-plan-switch") setEnvironment("APS5_TRACE_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; + const auto outputs = preparedOutputs(fixtures, &plans); + ShaderDiskCache::Flush(); + const auto totals = ShaderDiskCache::Totals(); + const auto builds = FrontEndBuilds(); + const auto counts = " (front end builds " + std::to_string(builds) + ", plan hits " + std::to_string(totals.planHits) + ", plan misses " + std::to_string(totals.planMisses) + ", plan load failures " + std::to_string(totals.planLoadFailures) + ", variant hits " + std::to_string(totals.hits) + ", variant misses " + std::to_string(totals.misses) + ")"; + if (mode == "--load-plan-switch") { + require(builds == count && totals.planHits == 0u && totals.planMisses == count && totals.misses == count, "another switch value reused stored plans or variants" + counts); + std::cout << "a process with another switch value rebuilt all " << count << " plans\n"; + return 0; + } + if (mode == "--load-plan") { + require(builds == 0u && totals.planHits == count && totals.planMisses == 0u && totals.planLoadFailures == 0u && totals.hits == count && totals.misses == 0u, "the second process ran the front end" + counts); + } else if (mode == "--load-plan-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 { + require(builds == 1u && totals.planHits == count - 1u && totals.planLoadFailures == 1u && totals.planWrites == 1u && totals.hits == count && totals.misses == 0u, "the damaged plan was not rejected and rebuilt alone" + counts); + } + for (std::size_t index = 0; index < count; ++index) { + std::vector expected; + require(ReadWholeFile(planOutputPath(index), expected), "cannot read the first process's output for " + fixtures[index].name); + require(outputs[index] == expected, fixtures[index].name + ": the SPIR-V, bindings, push constants, snapshot or plan differ from the first process"); + } + if (mode == "--load-plan") { + auto changed = planFixtures(); + auto& fixture = changed.front(); + fixture.code.insert(fixture.code.end() - 1, 0xbf800000u); + const auto request = fixture.Request(true); + static_cast(PrepareShader(request)); + const auto after = ShaderDiskCache::Totals(); + require(FrontEndBuilds() == 1u && after.planHits == count && after.planMisses == 1u, "a changed code word reused a stored plan"); + std::cout << "second process prepared " << count << " shaders from stored plans without running the front end\n"; + } else if (mode == "--load-plan-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 { + std::cout << "a damaged plan entry was rejected, rebuilt and stored again with the same output\n"; + } + for (std::size_t index = 0; index < count; ++index) { + const auto fresh = GetResourcePlan(fixtures[index].Request(false)); + require(samePlan(*plans[index], *fresh), fixtures[index].name + ": the plan this process used differs from the plan its front end builds"); + } + ShaderDiskCache::Flush(); + return 0; +} + +void verifyPlanAcrossProcesses(const char* self) { + auto fixtures = planFixtures(); + const auto builds = FrontEndBuilds(); + const auto before = ShaderDiskCache::Totals(); + const auto outputs = preparedOutputs(fixtures); + ShaderDiskCache::Flush(); + const auto after = ShaderDiskCache::Totals(); + require(FrontEndBuilds() - builds == fixtures.size() && after.planMisses - before.planMisses == fixtures.size() && after.planWrites - before.planWrites == fixtures.size(), "the first process did not build and store every plan"); + std::filesystem::create_directories(planOutputPath(0).parent_path()); + for (std::size_t index = 0; index < outputs.size(); ++index) require(WriteFileAtomically(planOutputPath(index), outputs[index]), "cannot write the first process's output"); + auto refused = fixtures[1]; + refused.codeAddress = 0x200000u; + refused.code = {0xf40c0200u, 0xfa000000u, 0xf4000400u, 0xfa000050u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf4080500u, 0xfa000020u, 0xbf820002u, 0xf4080500u, 0x20000000u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}; + bool threw = false; + try { + static_cast(PrepareShader(refused.Request(true))); + } catch (const std::runtime_error&) { + threw = true; + } + ShaderDiskCache::Flush(); + require(threw && ShaderDiskCache::Totals().planWrites == after.planWrites && !std::filesystem::exists(planEntryPath(refused)), "a plan the front end refused was stored"); + std::uintmax_t planBytes = 0; + for (auto& fixture : fixtures) planBytes += std::filesystem::file_size(planEntryPath(fixture)); + std::cout << fixtures.size() << " plan entries, " << planBytes / fixtures.size() << " bytes per entry on average\n"; + const auto run = [&](const char* mode) { + const std::string command = "\"" + std::string(self) + "\" " + mode; + require(std::system(command.c_str()) == 0, std::string("the process ") + mode + " failed"); + }; + run("--load-plan"); + run("--load-plan-off"); + run("--load-plan-reuse-off"); + std::vector entry; + const auto damaged = planEntryPath(fixtures.front()); + require(ReadWholeFile(damaged, entry), "cannot read a stored plan entry"); + entry[entry.size() / 2] ^= std::byte{0x20}; + require(WriteFileAtomically(damaged, entry), "cannot damage a stored plan entry"); + run("--load-plan-corrupt"); + std::vector rewritten; + std::vector key; + ShaderDiskCache::BuildPlanKey(fixtures.front().Request(true), 32u, key); + IrResourcePlan plan; + require(ReadWholeFile(damaged, rewritten) && ShaderDiskCache::DecodePlanEntry(rewritten, key, plan) == ShaderDiskCache::LoadStatus::Loaded, "the rebuilt plan was not stored again"); + run("--load-plan-switch"); +} + void verifyDefaultDirectory(const char* self) { setEnvironment("ANYPS5_SHADER_CACHE_DIR", ""); setEnvironment("ANYPS5_NO_SHADER_CACHE", "1"); @@ -1014,20 +2452,66 @@ 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{}())); std::filesystem::remove_all(directory); verifyDefaultDirectory(argv[0]); setEnvironment("ANYPS5_NO_SHADER_CACHE", "0"); setEnvironment("ANYPS5_SHADER_CACHE_DIR", directory.string()); + 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]); verifyInvocationIsolation(); @@ -1037,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 34f8135f69..a754f42271 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1,29 +1,51 @@ #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" #include "Optimization/ResourceMaterializer.hpp" #include "Optimization/ResourceProgram.hpp" #include "Optimization/ShaderStageInputInfo.hpp" +#include "Optimization/SrtWalker.hpp" +#include "Optimization/SrtWalker/SrtDescriptorEvaluation.hpp" #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" #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 +#include +#include #include #include #include #include #include #include +#include #include #include +#include +#include +#include +#include #include #include #include @@ -43,8 +65,36 @@ 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}; + 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"}; ShaderRecompiler::SpirvTarget target{}; target.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version; @@ -79,6 +129,392 @@ void verifyResult(const ShaderRecompiler::RecompileResult& first, const ShaderRe } } +ShaderRecompiler::RecompileRequest FrontendRequest(std::span code, std::span userData) { + using namespace ShaderRecompiler; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x100000u, 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 = BufferTarget(); + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32u; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + return request; +} + +void verifyFrontendIdentity() { + using namespace ShaderRecompiler; + const bool reuse = std::getenv("APS5_NO_PERF_FRONTEND_PAIR") == nullptr; + const std::vector code{0xbf800000u, 0xbf810000u}; + const std::array userData{}; + const auto request = FrontendRequest(code, userData); + ShaderPreparationContext preparation; + const auto first = preparation.AcquireFrontend(request); + require(first != nullptr, "frontend pair: no frontend was prepared"); + require((preparation.AcquireFrontend(request) == first) == reuse, "frontend pair: repeated acquisition ignored the reuse switch"); + + const auto relocatedCode = code; + auto relocated = request; + relocated.shader.code = relocatedCode; + relocated.shader.codeAddress += 0x1000u; + require((preparation.AcquireFrontend(relocated) == first) == reuse, "frontend pair: identical words at another allocation or address changed reuse"); + auto partial = request; + partial.context.compute->partialThreads = {33u, 1u, 1u}; + require((preparation.AcquireFrontend(partial) == first) == reuse, "frontend pair: partial-thread state changed the frontend identity"); + auto target = request; + target.target.subgroupSize = 64u; + target.context.waveSize = 32u; + target.context.compute->numThreads = {32u, 2u, 1u}; + require((preparation.AcquireFrontend(target) == first) == reuse, "frontend pair: translation-only state changed the frontend identity"); + const std::array changedUserData{1u, 2u}; + auto userValues = request; + userValues.context.userData = changedUserData; + require((preparation.AcquireFrontend(userValues) == first) == reuse, "frontend pair: user-data values changed the frontend identity"); + + const auto verifyMiss = [&](const RecompileRequest& changed, const char* message) { + ShaderPreparationContext isolated; + const auto original = isolated.AcquireFrontend(request); + const auto replacement = isolated.AcquireFrontend(changed); + require(replacement != original, message); + require((isolated.AcquireFrontend(changed) == replacement) == reuse, "frontend pair: a successful miss was not retained"); + require(isolated.AcquireFrontend(request) != original, "frontend pair: replacement retained more than the last frontend"); + }; + auto changed = request; + auto changedCode = code; + changedCode[0] = 0xbf800001u; + changed.shader.code = changedCode; + verifyMiss(changed, "frontend pair: changed code words reused the frontend"); + changedCode = code; + changedCode.push_back(0xbf800000u); + changed.shader.code = changedCode; + verifyMiss(changed, "frontend pair: changed code length reused the frontend"); + changed = request; + changed.context.userDataBaseRegister = 4u; + verifyMiss(changed, "frontend pair: changed user-data base reused the frontend"); + changed = request; + changed.context.userData = std::span(userData).first(1u); + verifyMiss(changed, "frontend pair: changed user-data count reused the frontend"); + changed = request; + changed.shader.stage = ShaderStage::Fragment; + changed.context.compute.reset(); + changed.context.pixel = ShaderPixelStageInfo{}; + verifyMiss(changed, "frontend pair: a different stage reused the frontend"); + changed = request; + changed.shader.stage = ShaderStage::Vertex; + changed.context.compute.reset(); + changed.context.vertex = ShaderVertexStageInfo{}; + verifyMiss(changed, "frontend pair: a fetch-capable stage reused the compute frontend"); + + auto invalid = request; + invalid.shader.code = {}; + bool failed = false; + try { + static_cast(preparation.AcquireFrontend(invalid)); + } catch (const std::out_of_range& error) { + failed = std::string_view(error.what()).find("before s_endpgm") != std::string_view::npos; + } + require(failed, "frontend pair: invalid code did not fail during frontend construction"); + require((preparation.AcquireFrontend(request) == first) == reuse, "frontend pair: a failed miss replaced the prior frontend"); +} + +void verifyFrontendProgram(const ShaderRecompiler::IrProgram& reused, const ShaderRecompiler::IrProgram& fresh) { + using namespace ShaderRecompiler; + require(ProgramToString(reused) == ProgramToString(fresh), "frontend pair: reused frontend changed the IR"); + require(reused.WaveSize() == fresh.WaveSize() && reused.Info() == fresh.Info(), "frontend pair: reused frontend changed shader information"); + const auto& left = reused.Resources(); + const auto& right = fresh.Resources(); + require(left.stage == right.stage && left.shaderHash == right.shaderHash && left.userDataBase == right.userDataBase && left.userDataCount == right.userDataCount && left.srgbDecodeFormats == right.srgbDecodeFormats, "frontend pair: resource identity changed"); + require(left.memoryInfo == right.memoryInfo && left.materializationSources == right.materializationSources && left.guardedSrtSlots == right.guardedSrtSlots && left.srtGuardOffset == right.srtGuardOffset && left.cleanFlatSlots == right.cleanFlatSlots && left.pureFlatSlots == right.pureFlatSlots, "frontend pair: resource planning changed"); + require(left.descriptorSources.size() == right.descriptorSources.size() && left.srtReads.size() == right.srtReads.size(), "frontend pair: resource counts changed"); + 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.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"); + } + } + require(reused.BlockOrder().size() == fresh.BlockOrder().size(), "frontend pair: block count changed"); + for (std::size_t block = 0; block < reused.BlockOrder().size(); ++block) { + const auto* a = reused.BlockOrder()[block]; + const auto* b = fresh.BlockOrder()[block]; + require(a != b && a->Instructions().size() == b->Instructions().size(), "frontend pair: mutable IR blocks were shared or changed"); + auto other = b->Instructions().begin(); + for (const auto* x : a->Instructions()) { + const auto* y = *other++; + require(x != y && x->Flags() == y->Flags(), "frontend pair: mutable IR values were shared or their flags changed"); + } + } +} + +void verifyFrontendPair() { + using namespace ShaderRecompiler; + verifyFrontendIdentity(); + const bool reuse = std::getenv("APS5_NO_PERF_FRONTEND_PAIR") == nullptr; + const std::vector code{0xe0700000u, 0x80000000u, 0xbf810000u}; + const std::array buffer{0x10000000u, 0u, 0x40u, 0x00027facu}; + const auto request = FrontendRequest(code, buffer); + auto partial = request; + partial.context.compute->partialThreads = {33u, 1u, 1u}; + const auto freshFull = PrepareResourceProgram(request); + const auto freshPartial = PrepareResourceProgram(partial); + require(ProgramToString(freshFull) != ProgramToString(freshPartial), "frontend pair: fixture does not distinguish full and partial threads"); + const auto storeThreadLimits = [](const IrProgram& program) { + std::vector pending; + for (const auto* block : program.BlockOrder()) { + for (const auto* inst : block->Instructions()) { + if (inst->Opcode() == IrOpcode::StoreBufferU32) pending.push_back(inst->Argument(inst->ArgumentCount() - 1u)); + } + } + require(pending.size() == 1u, "frontend pair: fixture does not retain exactly one buffer store"); + std::vector visited; + std::uint32_t axes = 0u; + while (!pending.empty()) { + const auto* value = pending.back()->Resolve(); + pending.pop_back(); + if (std::find(visited.begin(), visited.end(), value) != visited.end()) continue; + visited.push_back(value); + if (value->Opcode() == IrOpcode::GetBuiltin && value->Argument(0)->Resolve()->ImmediateU32() == static_cast(StageInputKind::DispatchThreadLimit)) { + const auto axis = value->Argument(1)->Resolve()->ImmediateU32(); + require(axis < 3u, "frontend pair: invalid dispatch-limit axis"); + axes |= 1u << axis; + } + for (const auto* argument : value->Arguments()) pending.push_back(argument); + } + return axes; + }; + require(storeThreadLimits(freshFull) == 0u && storeThreadLimits(freshPartial) == 7u, "frontend pair: buffer-store predicate does not retain all partial-thread bounds"); + require(!freshFull.Resources().memoryInfo.empty() && !freshFull.Resources().descriptorSources.empty() && !freshFull.Info().buffers.empty(), "frontend pair: fixture has no buffer resources"); + + for (const bool partialFirst : {false, true}) { + ShaderPreparationContext preparation; + std::shared_ptr retained; + { + auto temporary = code; + auto borrowed = request; + borrowed.shader.code = temporary; + retained = preparation.AcquireFrontend(borrowed); + std::fill(temporary.begin(), temporary.end(), 0xffffffffu); + require((preparation.AcquireFrontend(request) == retained) == reuse, "frontend pair: mutating the caller's code changed the retained frontend"); + } + require((preparation.AcquireFrontend(request) == retained) == reuse, "frontend pair: destroying the caller's code changed the retained frontend"); + const auto first = PrepareResourceProgram(partialFirst ? partial : request, &preparation); + const auto second = PrepareResourceProgram(partialFirst ? request : partial, &preparation); + verifyFrontendProgram(first, partialFirst ? freshPartial : freshFull); + verifyFrontendProgram(second, partialFirst ? freshFull : freshPartial); + require(first.BlockOrder().front() != second.BlockOrder().front(), "frontend pair: full and partial variants share mutable IR"); + const auto repeated = PrepareResourceProgram(partialFirst ? partial : request, &preparation); + verifyFrontendProgram(repeated, first); + auto translated = request; + translated.context.waveSize = 32u; + translated.context.compute->numThreads = {32u, 2u, 1u}; + const auto changed = PrepareResourceProgram(translated, &preparation); + const auto freshChanged = PrepareResourceProgram(translated); + verifyFrontendProgram(changed, freshChanged); + require(changed.WaveSize() != freshFull.WaveSize(), "frontend pair: changed translation inputs were ignored"); + require((preparation.AcquireFrontend(request) == retained) == reuse, "frontend pair: translating a variant replaced the frontend"); + } + + for (const bool partialFirst : {false, true}) { + auto pairCode = code; + pairCode.insert(pairCode.begin(), 0xbf800000u | static_cast(partialFirst)); + auto full = request; + full.shader.code = pairCode; + auto limited = partial; + limited.shader.code = pairCode; + const auto fullReference = Recompile(full); + const auto partialReference = Recompile(limited); + ShaderPreparationContext preparation; + const auto first = PrepareShader(partialFirst ? limited : full, &preparation); + const auto second = PrepareShader(partialFirst ? full : limited, &preparation); + const auto& fullHandle = partialFirst ? second : first; + const auto& partialHandle = partialFirst ? first : second; + const auto& fullArtifact = GetPreparedArtifact(*fullHandle); + const auto& partialArtifact = GetPreparedArtifact(*partialHandle); + require(first->source != second->source, "frontend pair: full and partial fixtures did not exercise separate sources"); + require(fullArtifact.spirv != partialArtifact.spirv, "frontend pair: full and partial threads share the same artifact"); + SrtRuntime runtime{}; + runtime.userData = buffer; + runtime.shaderBase = full.shader.codeAddress; + const auto fullCapture = CaptureResources(full, runtime, *fullHandle); + const auto partialCapture = CaptureResources(limited, runtime, *partialHandle); + const auto fullResult = MaterializeShader(full, *fullCapture, *fullHandle); + const auto partialResult = MaterializeShader(limited, *partialCapture, *partialHandle); + verifyResult(fullReference, *fullResult); + verifyResult(partialReference, *partialResult); + } + + const std::array color{0x3f800000u}; + for (const bool zeroFirst : {false, true}) { + const std::vector pixelCode{0xbf800000u | static_cast(zeroFirst), 0x7e000200u, 0xf800180fu, 0u, 0xbf810000u}; + auto pixel = FrontendRequest(pixelCode, color); + pixel.shader.stage = ShaderStage::Fragment; + pixel.context.compute.reset(); + pixel.context.pixel = ShaderPixelStageInfo{}; + pixel.context.pixel->wave32 = true; + pixel.context.pixel->targetOutputMode[0] = 9u; + pixel.context.waveSize = 32u; + auto zero = pixel; + zero.layout.pushConstantSizeBytes = 0u; + const auto pixelReference = Recompile(pixel); + const auto zeroReference = Recompile(zero); + ShaderPreparationContext preparation; + const auto first = PrepareShader(zeroFirst ? zero : pixel, &preparation); + const auto second = PrepareShader(zeroFirst ? pixel : zero, &preparation); + const auto& pixelHandle = zeroFirst ? second : first; + const auto& zeroHandle = zeroFirst ? first : second; + require(first->source == second->source, "frontend pair: pixel capacities did not exercise the same source"); + require(GetPreparedArtifact(*pixelHandle).spirv != GetPreparedArtifact(*zeroHandle).spirv, "frontend pair: pixel fixture does not distinguish push-constant capacities"); + SrtRuntime runtime{}; + runtime.userData = color; + runtime.shaderBase = pixel.shader.codeAddress; + const auto pixelCapture = CaptureResources(pixel, runtime, *pixelHandle); + const auto zeroCapture = CaptureResources(zero, runtime, *zeroHandle); + verifyResult(pixelReference, *MaterializeShader(pixel, *pixelCapture, *pixelHandle)); + verifyResult(zeroReference, *MaterializeShader(zero, *zeroCapture, *zeroHandle)); + } +} + +ShaderRecompiler::RecompileRequest LayoutPairRequest(std::span code, std::span userData, bool registration, std::uint32_t wordLimit) { + using namespace ShaderRecompiler; + auto request = FrontendRequest(code, userData); + request.shader.stage = ShaderStage::Fragment; + request.context.compute.reset(); + request.context.pixel = ShaderPixelStageInfo{}; + request.context.pixel->wave32 = true; + request.context.pixel->targetOutputMode[0] = 9u; + request.context.waveSize = 32u; + request.target.registrationModules = registration; + request.target.registrationWordLimit = wordLimit; + request.useCache = true; + return request; +} + +void verifyLayoutPairVariant(const ShaderRecompiler::SourceHandle& paired, const ShaderRecompiler::SourceHandle& isolated) { + const auto& left = *paired.artifact; + const auto& right = *isolated.artifact; + require(left.artifact.spirv == right.artifact.spirv, "layout pair: the SPIR-V differs from an isolated compile"); + require(left.bindings.layout == right.bindings.layout && left.bindings.layout.UsesPushData() == right.bindings.layout.UsesPushData() && left.bindings.pushConstantOffsetBytes == right.bindings.pushConstantOffsetBytes && left.bindings.pushConstantSizeBytes == right.bindings.pushConstantSizeBytes, "layout pair: the binding layout or push range differs from an isolated compile"); + const auto& a = left.artifact; + const auto& b = right.artifact; + const auto sameParameter = [](const ShaderRecompiler::FragmentParameter& p, const ShaderRecompiler::FragmentParameter& q) { + return p.location == q.location && p.sourceLocation == q.sourceLocation && p.flat == q.flat && p.perVertex == q.perVertex && p.custom == q.custom && p.flatP10 == q.flatP10; + }; + require(a.vertexInputPatches == b.vertexInputPatches && a.memoryOffsetDword == b.memoryOffsetDword && a.shaderDataDwords == b.shaderDataDwords && a.imageMetadataDword == b.imageMetadataDword && a.runtimeImageCount == b.runtimeImageCount && a.runtimeImageResources == b.runtimeImageResources && a.registrationModule == b.registrationModule && a.bdaAbiVersion == b.bdaAbiVersion && a.runtimeAbiVersion == b.runtimeAbiVersion, "layout pair: the artifact's bindings or ABI differ from an isolated compile"); + require(a.vertexInputs == b.vertexInputs && a.vertexOffsetSgpr == b.vertexOffsetSgpr && a.instanceOffsetSgpr == b.instanceOffsetSgpr && a.vertexOffsetShared == b.vertexOffsetShared && a.instanceOffsetShared == b.instanceOffsetShared && a.vertexOffsetConflict == b.vertexOffsetConflict && a.instanceOffsetConflict == b.instanceOffsetConflict && a.hostSubgroupSize == b.hostSubgroupSize, "layout pair: the artifact's vertex interface differs from an isolated compile"); + require(a.parameterExports == b.parameterExports && std::ranges::equal(a.fragmentParameters, b.fragmentParameters, sameParameter) && a.barycentricEmulation.active == b.barycentricEmulation.active && a.barycentricEmulation.smooth == b.barycentricEmulation.smooth && a.barycentricEmulation.linear == b.barycentricEmulation.linear, "layout pair: the artifact's fragment interface differs from an isolated compile"); + const auto& x = left.info; + const auto& y = right.info; + require(x.stage == y.stage && x.shaderHash == y.shaderHash && x.waveSize == y.waveSize && x.userDataBase == y.userDataBase && x.userDataCount == y.userDataCount && x.scratchDwords == y.scratchDwords && x.paramExportMask == y.paramExportMask && x.info == y.info && x.bindings == y.bindings, "layout pair: the compiled shader information differs from an isolated compile"); +} + +void verifyLayoutPairIsolation(const ShaderRecompiler::RecompileRequest& request, const ShaderRecompiler::SourceHandle& handle, std::uint32_t separateLimit) { + using namespace ShaderRecompiler; + const std::vector isolatedCode(request.shader.code.begin(), request.shader.code.end()); + auto isolated = request; + isolated.shader.code = isolatedCode; + isolated.target.registrationWordLimit = separateLimit; + const auto reference = PrepareShader(isolated); + require(reference->source != handle.source, "layout pair: the isolated compile shared a source"); + verifyLayoutPairVariant(handle, *reference); + SrtRuntime runtime{}; + runtime.userData = request.context.userData; + runtime.shaderBase = request.shader.codeAddress; + auto uncached = request; + uncached.useCache = false; + verifyResult(Recompile(uncached), *MaterializeShader(request, *CaptureResources(request, runtime, handle), handle)); +} + +void verifyLayoutPair() { + using namespace ShaderRecompiler; + const bool reuse = std::getenv("APS5_NO_PERF_LAYOUT_PAIR") == nullptr; + const std::array buffer{0x10000000u, 0u, 0x40u, 0x00027facu}; + const auto pixelCode = [](std::uint32_t variant) { + return std::vector{0xbf800000u | variant, 0xf4200100u, 0xfa000000u, 0xbf8cc07fu, 0x7e000204u, 0xf800180fu, 0u, 0xbf810000u}; + }; + const auto interpolatedCode = [](std::uint32_t variant) { + return std::vector{0xbf800000u | variant, 0xc8100000u, 0xc8110001u, 0xc8140500u, 0xc8150501u, 0xf4200100u, 0xfa000000u, 0xbf8cc07fu, 0x7e000204u, 0xf800180fu, 0x05040500u, 0xbf810000u}; + }; + const auto defaultLimit = SpirvTarget{}.registrationWordLimit; + struct Configuration { + bool registration; + std::uint32_t wordLimit; + std::uint32_t pushOffset; + bool interpolated; + }; + std::uint32_t variant = 0u; + for (const auto& [registration, wordLimit, pushOffset, interpolated] : {Configuration{false, defaultLimit, 0u, false}, Configuration{true, defaultLimit, 0u, false}, Configuration{true, defaultLimit, 128u, false}, Configuration{true, 1u, 128u, false}, Configuration{true, defaultLimit, 128u, true}}) { + for (const bool zeroFirst : {false, true}) { + const auto code = interpolated ? interpolatedCode(variant++) : pixelCode(variant++); + auto pixel = LayoutPairRequest(code, buffer, registration, wordLimit); + pixel.layout.pushConstantOffsetBytes = pushOffset; + if (interpolated) { + pixel.context.pixel->interpolatorCount = 2u; + pixel.context.pixel->interpolatorSettings[1] = 1u; + pixel.context.pixel->inputAddr = PixelInputBit(PixelInput::PerspectiveCenter); + pixel.context.pixel->hasPerspectiveCenterVgpr = true; + } + auto zero = pixel; + zero.layout.pushConstantSizeBytes = 0u; + ShaderPreparationContext preparation; + const auto first = PrepareShader(zeroFirst ? zero : pixel, &preparation); + const auto second = PrepareShader(zeroFirst ? pixel : zero, &preparation); + require(first->source == second->source, "layout pair: the two layouts did not share a source"); + require(preparation.ResourcePrograms() == (reuse ? 1u : 2u), "layout pair: the second layout ignored the reuse switch"); + const auto& pixelHandle = zeroFirst ? second : first; + const auto& zeroHandle = zeroFirst ? first : second; + require(pixelHandle->artifact->bindings.layout.UsesPushData() && !zeroHandle->artifact->bindings.layout.UsesPushData(), "layout pair: the fixture does not distinguish the push-constant capacities"); + const bool module = registration && wordLimit == defaultLimit; + require(GetPreparedArtifact(*pixelHandle).registrationModule == module && GetPreparedArtifact(*zeroHandle).registrationModule == module, "layout pair: the word limit did not choose the module mode"); + require(GetPreparedArtifact(*pixelHandle).fragmentParameters.size() == (interpolated ? 2u : 0u) && GetPreparedArtifact(*zeroHandle).fragmentParameters.size() == (interpolated ? 2u : 0u), "layout pair: the interpolated fixture did not declare its two parameters"); + verifyLayoutPairIsolation(pixel, *pixelHandle, wordLimit + 1u); + verifyLayoutPairIsolation(zero, *zeroHandle, wordLimit + 2u); + } + } + + const auto code = pixelCode(variant++); + const auto pixel = LayoutPairRequest(code, buffer, true, defaultLimit); + auto relocated = pixel; + relocated.shader.codeAddress += 0x1000u; + relocated.layout.pushConstantSizeBytes = 0u; + ShaderPreparationContext preparation; + const auto first = PrepareShader(pixel, &preparation); + const auto moved = PrepareShader(relocated, &preparation); + require(first->source == moved->source, "layout pair: relocated code did not share the source"); + require(preparation.ResourcePrograms() == 2u && moved->artifact->info.shaderHash == relocated.shader.codeAddress, "layout pair: a program translated at another code address was reused"); + verifyLayoutPairIsolation(relocated, *moved, defaultLimit + 1u); + + const std::vector colorCode{0xbf800000u | variant++, 0x7e000200u, 0xf800180fu, 0u, 0xbf810000u}; + auto color = LayoutPairRequest(colorCode, std::span(buffer).first(1u), true, defaultLimit); + color.shader.codeAddress = relocated.shader.codeAddress; + color.layout.pushConstantSizeBytes = 0u; + require(ResolveSource(color) != nullptr, "layout pair: the other source was not resolved"); + const auto colored = PrepareShader(color, &preparation); + require(colored->source != first->source && preparation.ResourcePrograms() == 2u, "layout pair: another source at the same code address reused the retained program"); + verifyLayoutPairIsolation(color, *colored, defaultLimit + 1u); + + const auto retained = colored->source.use_count(); + const auto otherCode = pixelCode(variant++); + static_cast(PrepareResourceProgram(LayoutPairRequest(otherCode, buffer, true, defaultLimit), &preparation)); + require(preparation.ResourcePrograms() == 3u && retained - colored->source.use_count() == (reuse ? 1 : 0), "layout pair: the context kept a program while it built another"); + + const std::vector computeCode{0xbf800000u, 0xbf810000u}; + auto compute = FrontendRequest(computeCode, buffer); + compute.context.userDataBaseRegister = 2u; + compute.context.compute->scratchDwords = 64u; + auto program = PrepareResourceProgram(compute); + const auto copied = program.CompiledInfo(); + const auto taken = std::move(program).TakeCompiledInfo(); + require(taken.userDataBase == 2u && taken.scratchDwords == 64u, "layout pair: the compute fixture did not set a user-data base and scratch"); + require(copied.stage == taken.stage && copied.shaderHash == taken.shaderHash && copied.waveSize == taken.waveSize && copied.userDataBase == taken.userDataBase && copied.userDataCount == taken.userDataCount && copied.scratchDwords == taken.scratchDwords && copied.paramExportMask == taken.paramExportMask && copied.info == taken.info && copied.bindings == taken.bindings, "layout pair: the copied compiled information differs from the moved one"); +} + void verifyRegisterSources() { using namespace ShaderRecompiler; IrResourcePlan plan; @@ -106,6 +542,137 @@ void verifyRegisterSources() { require(!EquivalentValue(plan, &samplerRegister, &firstVector), "different register types were merged"); } +void verifySignedMinimum() { + using namespace ShaderRecompiler; + IrResourcePlan plan; + IrValue reg(IrOpcode::Void, IrType::ScalarReg, 0); + reg.SetRegister({RegisterBank::Scalar, 0}); + IrValue input(IrOpcode::GetUserData, IrType::U32, 1); + input.AddArgument(®); + IrValue limit(IrOpcode::Void, IrType::U32, 2); + limit.SetImmediateU32(8); + IrValue minimum(IrOpcode::SMin32, IrType::U32, 3); + minimum.AddArgument(&input); + minimum.AddArgument(&limit); + require(Detail::IsRuntimeUniformOp(IrOpcode::SMin32), "signed minimum must be a runtime uniform operation"); + for (const auto& [value, expected] : std::array, 6>{{{0, 0}, {4, 4}, {20, 8}, {0x7fffffffu, 8}, {0xffffffffu, 0xffffffffu}, {0x80000000u, 0x80000000u}}}) { + std::array data{value}; + SrtRuntime runtime{data}; + Detail::Evaluator evaluator(plan, runtime); + std::uint32_t result = 0; + 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()); + } +} + void verifyEvaluatedValues() { using namespace ShaderRecompiler; std::vector> values; @@ -127,6 +694,174 @@ void verifyEvaluatedValues() { require(!table.Find(&absent, found), "evaluated values: a value that was never inserted was found"); } +void verifySignedSrtComparison() { + using namespace ShaderRecompiler; + IrResourcePlan plan; + plan.srtPlanComplete = true; + plan.userDataBase = 8; + std::vector> values; + std::uint32_t id = 0; + const auto make = [&](IrOpcode opcode, IrType type, std::initializer_list arguments = {}) -> IrValue& { + auto value = std::make_unique(opcode, type, id++); + for (auto* argument : arguments) value->AddArgument(argument); + values.push_back(std::move(value)); + return *values.back(); + }; + const auto constant = [&](std::uint32_t bits) -> IrValue& { + auto& value = make(IrOpcode::Void, IrType::U32); + value.SetImmediateU32(bits); + return value; + }; + auto& registerValue = make(IrOpcode::Void, IrType::ScalarReg); + registerValue.SetRegister({RegisterBank::Scalar, 8u}); + auto& userData = make(IrOpcode::GetUserData, IrType::U32, {®isterValue}); + auto& zero = constant(0u); + auto& intMin = constant(0x80000000u); + auto& intMax = constant(0x7fffffffu); + auto& negative = constant(0xau); + auto& nonnegative = constant(0xbu); + auto& greaterThanMin = constant(0xcu); + auto& equalMin = constant(0xdu); + auto& belowMax = constant(0xeu); + auto& equalMax = constant(0xfu); + auto& isNegative = make(IrOpcode::SLessThan32, IrType::Bool, {&userData, &zero}); + auto& aboveMin = make(IrOpcode::SLessThan32, IrType::Bool, {&intMin, &userData}); + auto& belowMaxValue = make(IrOpcode::SLessThan32, IrType::Bool, {&userData, &intMax}); + DescriptorSource source; + source.dwordCount = 3u; + source.dwords[0] = &make(IrOpcode::SelectU32, IrType::U32, {&isNegative, &negative, &nonnegative}); + source.dwords[1] = &make(IrOpcode::SelectU32, IrType::U32, {&aboveMin, &greaterThanMin, &equalMin}); + source.dwords[2] = &make(IrOpcode::SelectU32, IrType::U32, {&belowMaxValue, &belowMax, &equalMax}); + plan.descriptorSources.push_back(source); + for (std::uint32_t dword = 0; dword < source.dwordCount; ++dword) { + require(SrtWalker().ValidateRuntimeValue(plan, source.dwords[dword]), "signed SRT comparison: the runtime validator rejects a descriptor dword the evaluator computes"); + } + const std::array sourceIndex{0u}; + const std::array>, 4> cases{{ + {0x80000000u, {0xau, 0xdu, 0xeu}}, + {0xffffffffu, {0xau, 0xcu, 0xeu}}, + {0u, {0xbu, 0xcu, 0xeu}}, + {0x7fffffffu, {0xbu, 0xcu, 0xfu}}, + }}; + std::vector descriptors; + std::vector flat; + std::vector active; + for (const auto& [input, expected] : cases) { + const std::array userDataWords{input}; + const SrtRuntime runtime{userDataWords}; + require(Detail::EvaluateRuntimeSourcesImpl(plan, sourceIndex, runtime, descriptors, flat, false, {}, active), "signed SRT comparison: descriptor expression could not be evaluated"); + require(descriptors.size() == 1u && descriptors[0].dwordCount == expected.size(), "signed SRT comparison: descriptor shape changed"); + for (std::size_t index = 0; index < expected.size(); ++index) { + require(descriptors[0].dwords[index] == expected[index], "signed SRT comparison: signed boundary selected the wrong descriptor dword"); + } + } +} + +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++) { + code.push_back(0xbe800300u | ((8u + dword) << 16u) | (32u + dword)); + } + code.insert(code.end(), {0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}); + using namespace ShaderRecompiler; + constexpr std::uint32_t Format8888UNorm = 56; + constexpr std::uint32_t Type2D = 9; + struct alignas(256) Texture { std::array bytes{}; }; + static Texture textures[2]; + const auto imageDescriptor = [&](const Texture& texture) { + const auto base = static_cast(reinterpret_cast(texture.bytes.data())); + return std::array{static_cast(base >> 8u), static_cast((base >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (3u << 30u), 3u << 14u, 0xfacu | (Type2D << 28u), 0u, 0u, 0u, 0u}; + }; + std::array output{}; + const auto outputAddress = static_cast(reinterpret_cast(output.data())); + std::array srt{}; + const auto first = imageDescriptor(textures[0]); + const auto second = imageDescriptor(textures[1]); + std::copy(first.begin(), first.end(), srt.begin()); + const std::array outputV{static_cast(outputAddress), static_cast((outputAddress >> 32u) & 0xffffu), 16u, 0xfacu}; + std::copy(outputV.begin(), outputV.end(), srt.begin() + 16); + srt[20] = 1u; + std::copy(second.begin(), second.end(), srt.begin() + 24); + const auto srtAddress = static_cast(reinterpret_cast(srt.data())); + const std::array userData{static_cast(srtAddress), static_cast(srtAddress >> 32u)}; + const std::array capabilities{29u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x23000u, code, 0, {}}; + request.context.waveSize = 32u; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.bdaAbiVersion = BdaAbi::Version; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + const auto plan = GetResourcePlan(request); + 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); + 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) { + return std::equal(words.begin(), words.end(), value.dwords.begin()); + }); + }; + require(holds(first) && holds(std::array{}), "out-of-range s_buffer_load: a T# read through a null V# was not evaluated as zero"); +} + // The pure flat slots of a hand-built plan (Detail::ComputePureFlatSlots): a slot is pure unless // a descriptor dword, a condition, a uniform value or another slot's address cone reaches it. void verifyPureFlatSlots() { @@ -200,9 +935,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; @@ -210,69 +945,176 @@ void verifyPureFlatSlots() { require(Detail::ComputePureFlatSlots(plan) == Pure{0, 0, 0, 0}, "pure flat slots: an incomplete plan was classified"); } -// 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() { +void verifyGuardedDescriptorSources() { using namespace ShaderRecompiler; 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 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, 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())); + const std::array outputV{static_cast(outputAddress), static_cast((outputAddress >> 32u) & 0xffffu), 16u, 0xfacu}; + std::array srt{}; + const auto fill = [&](const std::array& image, std::uint32_t mode) { + std::copy(image.begin(), image.end(), srt.begin()); + std::copy(sampler.begin(), sampler.end(), srt.begin() + 8); + std::copy(outputV.begin(), outputV.end(), srt.begin() + 16); + srt[20] = mode; }; - 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}; const auto srtAddress = static_cast(reinterpret_cast(srt.data())); const std::array userData{static_cast(srtAddress), static_cast(srtAddress >> 32u)}; const std::array capabilities{29u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; - // A wave64 workgroup on a 32-wide host is held by one subgroup (two lanes per invocation). - const auto makeRequest = [&](const std::vector& code) { + RecompileRequest request{}; + const std::vector code{0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xbf068010u, 0xbf850006u, 0xf40c0200u, 0xfa000000u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}; + request.shader = {ShaderStage::Compute, 0x22000u, code, 0, {}}; + request.context.waveSize = 32; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.bdaAbiVersion = BdaAbi::Version; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + + fill(invalid, 0u); + const auto plan = GetResourcePlan(request); + require(plan->info.images.size() == 1u && plan->info.samplers.size() == 1u, "guarded sources: the image and its sampler were not planned"); + require(!plan->controlFlow.empty(), "guarded sources: the branch around the image was not planned"); + { + AgcDriver::ShaderMemory memory({}); + const auto capture = memory.Capture(request); + require(capture->snapshot.images.size() == 1u && std::ranges::all_of(capture->snapshot.images[0].dwords, [](std::uint32_t word) { return word == 0u; }), "guarded sources: the skipped T# was not left null"); + } + fill(invalid, 1u); + { + AgcDriver::ShaderMemory memory({}); + expectFailure([&] { static_cast(memory.Capture(request)); }, "runtime image descriptor is invalid", "guarded sources: an invalid T# behind a taken branch was accepted"); + } + 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({}); + const auto capture = memory.Capture(request); + require(capture->snapshot.images.size() == 1u && std::equal(valid.begin(), valid.end(), capture->snapshot.images[0].dwords.begin()), "guarded sources: the T# behind a taken branch was not read"); + } + fill(valid, 0u); + { + AgcDriver::ShaderMemory memory({}); + const auto capture = memory.Capture(request); + require(capture->snapshot.images.size() == 1u && std::ranges::all_of(capture->snapshot.images[0].dwords, [](std::uint32_t word) { return word == 0u; }), "guarded sources: a valid T# behind a skipped branch was read"); + } + + const std::vector storeCode{0xf4000400u, 0xfa000050u, 0xbf8cc07fu, 0xbf068010u, 0xbf850006u, 0xf40c0100u, 0xfa000000u, 0xbf8cc07fu, 0xf0200f08u, 0x00010004u, 0xbf820005u, 0xf40c0100u, 0xfa000000u, 0xbf8cc07fu, 0xf0200f10u, 0x00010004u, 0xbf810000u}; + auto storeRequest = request; + storeRequest.shader = {ShaderStage::Compute, 0x23000u, storeCode, 0, {}}; + const auto compileStore = [&](std::uint32_t mode) { + fill(valid, mode); + AgcDriver::ShaderMemory memory({}); + const auto capture = memory.Capture(storeRequest); + 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#"); + static_cast(compileStore(1u)); + expectFailure([&] { static_cast(compileStore(0u)); }, "incompatible with the static runtime image interface", "guarded images: a store of the wrong dimension behind a taken branch was accepted"); +} + +bool readDirect(void*, std::uint64_t address, std::uint32_t* value) { + std::memcpy(value, reinterpret_cast(address), sizeof(*value)); + return true; +} + +void verifySrtTape() { + using namespace ShaderRecompiler; + struct Walk { + bool ok = false; + std::vector results; + std::vector flat; + std::vector active; + std::size_t poisoned = 0; + std::uint32_t nullRoots = 0; + std::vector> leaves; + std::vector reads; + std::uint64_t asBase = 0; + std::uint64_t asValue = 0; + std::vector> userDataReads; + bool operator==(const Walk&) const = default; + }; + const auto walk = [](const IrResourcePlan& plan, SrtRuntime runtime, bool tape) { + Walk out; + SrtReadTrace trace; + SrtUserDataUse use; + runtime.readTrace = &trace; + runtime.userDataUse = &use; + const std::vector sources(plan.materializationSources.begin(), plan.materializationSources.end()); + const std::vector clean(plan.cleanFlatSlots.begin(), plan.cleanFlatSlots.end()); + const auto sourceSpan = tape ? std::span(plan.materializationSources) : std::span(sources); + const auto cleanSpan = tape ? std::span(plan.cleanFlatSlots) : std::span(clean); + std::vector poison; + out.ok = Detail::EvaluateRuntimeSourcesImpl(plan, sourceSpan, runtime, out.results, out.flat, true, cleanSpan, out.active, &poison, &out.nullRoots); + out.poisoned = poison.size(); + out.leaves = trace.leaves; + out.reads = trace.otherReads; + std::sort(out.leaves.begin(), out.leaves.end()); + std::sort(out.reads.begin(), out.reads.end()); + out.reads.erase(std::unique(out.reads.begin(), out.reads.end()), out.reads.end()); + out.asBase = use.asBase; + out.asValue = use.asValue; + out.userDataReads = use.reads; + std::sort(out.userDataReads.begin(), out.userDataReads.end()); + out.userDataReads.erase(std::unique(out.userDataReads.begin(), out.userDataReads.end()), out.userDataReads.end()); + return out; + }; + const auto answers = [](const IrResourcePlan& plan, const SrtRuntime& runtime) { + const auto* tape = Detail::SrtTape::For(plan); + require(tape != nullptr, "SRT tape: a complete plan has no tape"); + std::vector results; + std::vector flat; + std::vector active; + return tape->Run(plan, runtime, results, flat, active); + }; + constexpr std::uint32_t Format8888UNorm = 56; + constexpr std::uint32_t Type2D = 9; + struct alignas(256) Texture { std::array bytes{}; }; + static Texture texture; + const auto base = static_cast(reinterpret_cast(texture.bytes.data())); + const std::array valid{static_cast(base >> 8u), static_cast((base >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (3u << 30u), 3u << 14u, 0xfacu | (Type2D << 28u), 0u, 0u, 0u, 0u}; + const std::array invalid{0x12345678u, 0xffffffffu, 0xffffffffu, 0u, 0u, 0u, 0u, 0u}; + std::array output{}; + const auto outputAddress = static_cast(reinterpret_cast(output.data())); + const std::array outputV{static_cast(outputAddress), static_cast((outputAddress >> 32u) & 0xffffu), 16u, 0xfacu}; + std::array srt{}; + const auto srtAddress = static_cast(reinterpret_cast(srt.data())); + const std::array userData{static_cast(srtAddress), static_cast(srtAddress >> 32u)}; + const std::array nullUserData{0u, 0u}; + const std::array capabilities{29u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + const auto requestFor = [&](std::uint64_t address, const std::vector& code) { RecompileRequest request{}; - request.shader = {ShaderStage::Compute, 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; @@ -283,178 +1125,2431 @@ 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 = [&](const std::vector& 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"); - - // 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 verifyDescriptorPhis() { using namespace ShaderRecompiler; @@ -484,7 +3579,7 @@ void verifyDescriptorPhis() { const std::array userData{static_cast(srtAddress), static_cast(srtAddress >> 32u)}; const std::array capabilities{29u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; - const auto makeRequest = [&](const std::vector& code, std::uint32_t waveSize = 32u) { + const auto makeRequest = [&](std::span code, std::uint32_t waveSize = 32u) { RecompileRequest request{}; request.shader = {ShaderStage::Compute, 0x21000u, code, 0, {}}; request.context.waveSize = waveSize; @@ -501,7 +3596,7 @@ void verifyDescriptorPhis() { request.layout.pushConstantSizeBytes = 128; return request; }; - const auto countOps = [](const std::vector& words, std::uint32_t opcode) { + const auto countOps = [](std::span words, std::uint32_t opcode) { std::size_t count = 0; for (std::size_t cursor = 5; cursor < words.size();) { const auto length = words[cursor] >> 16u; @@ -517,13 +3612,14 @@ void verifyDescriptorPhis() { const std::vector imageCode{0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf40c0200u, 0xfa000000u, 0xbf820002u, 0xf40c0200u, 0xfa000060u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}; const std::vector dynamicCode{0xf40c0200u, 0xfa000000u, 0xf4000400u, 0xfa000050u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf4080500u, 0xfa000020u, 0xbf820002u, 0xf4080500u, 0x20000000u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}; - const auto compile = [&](const std::vector& code, std::size_t images, std::size_t samplers) { + const auto compile = [&](std::span code, std::size_t images, std::size_t samplers) { auto request = makeRequest(code); const auto plan = GetResourcePlan(request); require(plan->info.images.size() == images && plan->info.samplers.size() == samplers && plan->info.sampledPairs.size() == 2u, "descriptor Phi: the edges were not given one resource each"); 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 @@ -554,7 +3650,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); @@ -570,6 +3667,31 @@ void verifyDescriptorPhis() { }; require(loopImages.size() == 2u && holdsLoopImage(first) && holdsLoopImage(second), "descriptor Phi: the loop's chained T# Phis were not split into the two SRT T#s"); + const std::vector sequentialCode{0xf4000400u, 0xfa000050u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf40c0200u, 0xfa000000u, 0xbf820002u, 0xf40c0200u, 0xfa000060u, 0xbf068010u, 0xbf850003u, 0xf4080500u, 0xfa000020u, 0xbf820002u, 0xf4080500u, 0xfa000030u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}; + auto sequential = makeRequest(sequentialCode); + const auto sequentialPlan = GetResourcePlan(sequential); + 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); + 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); @@ -591,7 +3713,7 @@ void verifyDescriptorPhis() { const std::array prologue{0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xf40c0200u, 0xfa000000u, 0xbf8cc07fu}; const std::array epilogue{0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}; - const auto planImages = [&](const std::vector& body) { + const auto planImages = [&](std::span body) { std::vector code(prologue.begin(), prologue.end()); code.insert(code.end(), body.begin(), body.end()); code.insert(code.end(), epilogue.begin(), epilogue.end()); @@ -619,6 +3741,134 @@ void verifyDescriptorPhis() { expectFailure([&] { static_cast(planImages(splitReloads(260u))); }, "GetImageResource dword 0 is not a valid runtime value", "descriptor Phi: a web over more than 512 descriptor tuples was accepted"); } +void verifyUniformTableImage() { + using namespace ShaderRecompiler; + constexpr std::uint32_t Format8888UNorm = 56; + constexpr std::uint32_t Type2D = 9; + struct alignas(256) Texture { std::array bytes{}; }; + static Texture textures[2]; + struct alignas(4096) GuestTables { + std::array, 3> table{}; + std::array output{}; + std::array srt{}; + }; + static GuestTables guest; + const auto texture = [](const Texture& image) { + const auto base = static_cast(reinterpret_cast(image.bytes.data())); + return std::array{static_cast(base >> 8u), static_cast((base >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (3u << 30u), 3u << 14u, 0xfacu | (Type2D << 28u), 0u, 0u, 0u, 0u}; + }; + const auto first = texture(textures[0]); + const auto second = texture(textures[1]); + std::copy(first.begin(), first.end(), guest.table[1].begin() + 4); + std::copy(second.begin(), second.end(), guest.table[2].begin() + 4); + const auto bufferDescriptor = [](const void* base, std::uint32_t records) { + const auto address = static_cast(reinterpret_cast(base)); + return std::array{static_cast(address), static_cast((address >> 32u) & 0xffffu), records, 0xfacu}; + }; + const auto tableV = bufferDescriptor(guest.table.data(), static_cast(sizeof(guest.table))); + const auto outputV = bufferDescriptor(guest.output.data(), 16u); + std::copy(tableV.begin(), tableV.end(), guest.srt.begin()); + std::copy(outputV.begin(), outputV.end(), guest.srt.begin() + 12); + const auto srtAddress = static_cast(reinterpret_cast(guest.srt.data())); + std::array userData{static_cast(srtAddress), static_cast(srtAddress >> 32u), 2u}; + const std::array capabilities{29u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + const std::vector code{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000030u, 0x9310b002u, 0xf42c0502u, 0x20000010u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}}; + request.context.waveSize = 64; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.bdaAbiVersion = BdaAbi::Version; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + const auto plan = GetResourcePlan(request); + 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); + 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"); + } +} + +void verifyLaneSelectedDescriptors() { + std::vector code{0xf40c0200u, 0xfa000000u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xf4000400u, 0xfa000050u, 0xf40c0800u, 0xfa000060u, 0xbf8cc07fu, 0x7d880090u, 0xbf068010u, 0xbf850011u}; + for (std::uint32_t dword = 0; dword < 8u; dword++) { + const auto vgpr = 1u + dword; + code.push_back(0x7e000200u | (vgpr << 17u) | (32u + dword)); + code.push_back(0x02000000u | (vgpr << 17u) | (vgpr << 9u) | (8u + dword)); + } + code.push_back(0xbf820008u); + for (std::uint32_t dword = 0; dword < 8u; dword++) { + code.push_back(0x7e000200u | ((1u + dword) << 17u) | (8u + dword)); + } + for (std::uint32_t dword = 0; dword < 8u; dword++) { + code.push_back(0x7e000400u | ((8u + dword) << 17u) | (257u + dword)); + } + code.insert(code.end(), {0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}); + using namespace ShaderRecompiler; + constexpr std::uint32_t Format8888UNorm = 56; + constexpr std::uint32_t Type2D = 9; + struct alignas(256) Texture { std::array bytes{}; }; + static Texture textures[2]; + const auto imageDescriptor = [&](const Texture& texture) { + const auto base = static_cast(reinterpret_cast(texture.bytes.data())); + return std::array{static_cast(base >> 8u), static_cast((base >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (3u << 30u), 3u << 14u, 0xfacu | (Type2D << 28u), 0u, 0u, 0u, 0u}; + }; + std::array output{}; + const auto outputAddress = static_cast(reinterpret_cast(output.data())); + std::array srt{}; + const auto first = imageDescriptor(textures[0]); + const auto second = imageDescriptor(textures[1]); + std::copy(first.begin(), first.end(), srt.begin()); + const std::array outputV{static_cast(outputAddress), static_cast((outputAddress >> 32u) & 0xffffu), 16u, 0xfacu}; + std::copy(outputV.begin(), outputV.end(), srt.begin() + 16); + srt[20] = 1u; + std::copy(second.begin(), second.end(), srt.begin() + 24); + const auto srtAddress = static_cast(reinterpret_cast(srt.data())); + const std::array userData{static_cast(srtAddress), static_cast(srtAddress >> 32u)}; + const std::array capabilities{29u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x22000u, code, 0, {}}; + request.context.waveSize = 32u; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.bdaAbiVersion = BdaAbi::Version; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + const auto plan = GetResourcePlan(request); + 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); + 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) { + return std::equal(words.begin(), words.end(), value.dwords.begin()); + }); + }; + require(holds(first) && holds(second), "lane-selected T#: the snapshot T#s are not the two SRT T#s"); +} + void verifyProgramCounterRelativeData() { using namespace ShaderRecompiler; static const std::array code{ @@ -655,7 +3905,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; @@ -719,7 +3970,7 @@ void verifyLanesOutsideHostSubgroup() { void verifyHalfWaveReduction() { using namespace ShaderRecompiler; - static constexpr std::array capabilities{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic}; + static constexpr std::array capabilities{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic, spv::CapabilityClipDistance}; const auto pixel = [](std::span code, std::uint32_t subgroupSize, bool arithmetic) { ShaderPixelStageInfo info{}; info.inputAddr = PixelInputBit(PixelInput::PositionX); @@ -738,14 +3989,14 @@ void verifyHalfWaveReduction() { request.useCache = false; return Recompile(request).spirv; }; - const auto reductions = [](const std::vector& spirv, spv::Op opcode) { + const auto reductions = [](std::span spirv, spv::Op opcode) { std::uint32_t count = 0; for (std::size_t cursor = 5; cursor < spirv.size() && (spirv[cursor] >> 16u) != 0u; cursor += spirv[cursor] >> 16u) { if ((spirv[cursor] & 0xffffu) == static_cast(opcode) && (spirv[cursor] >> 16u) == 6u && spirv[cursor + 4u] == spv::GroupOperationReduce) ++count; } return count; }; - const auto halves = [](const std::vector& spirv, spv::Op opcode, std::uint32_t identity) { + const auto halves = [](std::span spirv, spv::Op opcode, std::uint32_t identity) { std::map definitions; std::vector reduces; for (std::size_t cursor = 5; cursor < spirv.size() && (spirv[cursor] >> 16u) != 0u; cursor += spirv[cursor] >> 16u) { @@ -803,10 +4054,82 @@ void verifyHalfWaveReduction() { {0u, 0x38000000u, 0x88000000u, spv::OpGroupNonUniformBitwiseOr, "half-wave reduction: the wave64 OR scan did not read lane 31 as a subgroup OR on 32 lanes"}, }}; for (const auto& reduction : scans) require(reductions(pixel(scan(reduction.identity, reduction.vector, reduction.scalar), 32u, true), reduction.reduce) == 1u, reduction.message); + auto comparedScan = scan(0u, 0x38000000u, 0x88000000u); + *std::find(comparedScan.begin(), comparedScan.end(), 0xbe98047eu) = 0x8a986a7eu; + comparedScan.insert(comparedScan.begin(), 0x7c080080u); + require(reductions(pixel(comparedScan, 32u, true), spv::OpGroupNonUniformBitwiseOr) == 1u, "half-wave reduction: an OR scan keyed by the lanes that failed a compare did not read lane 31 as a subgroup OR on 32 lanes"); + auto narrowedScan = comparedScan; + std::vector narrowing; + for (std::uint32_t step = 1; step <= 600u; ++step) { + const auto threshold = -static_cast(step); + std::uint32_t bits = 0; + std::memcpy(&bits, &threshold, sizeof(bits)); + narrowing.insert(narrowing.end(), {0x7c0800ffu, bits, 0x87fe6a7eu}); + } + narrowedScan.insert(narrowedScan.begin(), narrowing.begin(), narrowing.end()); + require(reductions(pixel(narrowedScan, 32u, true), spv::OpGroupNonUniformBitwiseOr) == 1u, "half-wave reduction: an OR scan keyed by lanes that 600 compares narrowed exec to did not read lane 31 as a subgroup OR on 32 lanes"); for (const auto& reduction : scans) { const auto wide = "half-wave reduction: a wave64 scan reduced by SPIR-V opcode " + std::to_string(reduction.reduce) + " did not read lanes 31 and 63 as reductions of host invocations 0-31 and 32-63 on 64 lanes"; require(halves(pixel(scan(reduction.identity, reduction.vector, reduction.scalar), 64u, true), reduction.reduce, reduction.identity), wide.c_str()); } + const auto vertex = [](std::vector code, std::uint32_t subgroupSize) { + *std::find(code.begin(), code.end(), 0xf800180fu) = 0xf80008cfu; + RecompileRequest request{}; + request.shader = {ShaderStage::Vertex, 0x10000u, code, 0, {}}; + request.context.waveSize = 64; + request.context.vertex = ShaderVertexStageInfo{}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = subgroupSize; + request.target.supportedCapabilities = capabilities; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + return Recompile(request).spirv; + }; + require(reductions(vertex(scan(0xffffffffu, 0x26000000u, 0x83800000u), 32u), spv::OpGroupNonUniformUMin) == 1u, "half-wave reduction: the wave64 vertex UMin scan, whose entry EXEC is every invocation, did not read lane 31 as a subgroup UMin on 32 lanes"); + require(halves(vertex(scan(0x80000000u, 0x24000000u, 0x84000000u), 64u), spv::OpGroupNonUniformSMax, 0x80000000u), "half-wave reduction: the wave64 vertex SMax scan did not read lanes 31 and 63 as reductions of host invocations 0-31 and 32-63 on 64 lanes"); + const auto domain = [](std::vector code, std::uint32_t subgroupSize) { + *std::find(code.begin(), code.end(), 0xf800180fu) = 0xf80008cfu; + *std::find(code.begin(), code.end(), 0x7e160f00u) = 0x7e160f01u; + RecompileRequest request{}; + request.shader = {ShaderStage::TessellationEvaluation, 0x10000u, code, 0, {}}; + request.context.waveSize = 64; + request.context.vertex = ShaderVertexStageInfo{}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = subgroupSize; + request.target.supportedCapabilities = capabilities; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + request.graphics = GraphicsCompileContext{0u, {}, std::nullopt, TessellationConfiguration{3u, 3u, 1u, 2u, 2u}, {}}; + request.useCache = false; + return Recompile(request).spirv; + }; + require(reductions(domain(scan(0xffffffffu, 0x26000000u, 0x83800000u), 32u), spv::OpGroupNonUniformUMin) == 1u, "half-wave reduction: the wave64 domain UMin scan under s_orn2_saveexec_b64 vcc, exec did not read lane 31 as a subgroup UMin on 32 lanes"); + require(halves(domain(scan(0u, 0x38000000u, 0x88000000u), 64u), spv::OpGroupNonUniformBitwiseOr, 0u), "half-wave reduction: the wave64 domain OR scan did not read lanes 31 and 63 as reductions of host invocations 0-31 and 32-63 on 64 lanes"); + const auto floatScan = [&scan](std::uint32_t identity, std::uint32_t vector, std::uint32_t join) { + auto code = scan(identity, vector, 0u); + code[23] = join; + code[24] = 0x00000e06u; + return code; + }; + const auto fmax = floatScan(0xff800000u, 0x20000000u, 0xd5100001u); + const auto fmin = floatScan(0x7f800000u, 0x1e000000u, 0xd50f0001u); + for (const auto& code : {fmax, fmin}) { + const auto narrow = pixel(code, 32u, false); + require(!narrow.empty() && reductions(narrow, spv::OpGroupNonUniformFMax) == 0u && reductions(narrow, spv::OpGroupNonUniformFMin) == 0u, "half-wave reduction: a wave64 v_max_f32/v_min_f32 scan did not read lane 63 as its -inf/+inf keys on 32 lanes"); + require(!pixel(code, 64u, false).empty(), "half-wave reduction: a wave64 v_max_f32/v_min_f32 scan did not build on 64 lanes"); + } + auto fmaxUnderExec = fmax; + fmaxUnderExec[3] = 0xbeea047eu; + expectFailure([&] { static_cast(pixel(fmaxUnderExec, 32u, true)); }, "v_readlane_b32 of lane 63 is outside the 32-lane host subgroup", "half-wave reduction: a v_max_f32 scan under the entry EXEC read lane 63"); + auto fmaxJoinedByMin = fmax; + fmaxJoinedByMin[17] = 0x1e000000u | 0x001a190du; + expectFailure([&] { static_cast(pixel(fmaxJoinedByMin, 32u, true)); }, "v_readlane_b32 of lane 63 is outside the 32-lane host subgroup", "half-wave reduction: a v_max_f32 row scan joined by v_min_f32 read lane 63"); + auto fmaxOfZero = fmax; + fmaxOfZero[6] = 0u; + expectFailure([&] { static_cast(pixel(fmaxOfZero, 32u, true)); }, "v_readlane_b32 of lane 63 is outside the 32-lane host subgroup", "half-wave reduction: a v_max_f32 scan of 0 outside the live lanes read lane 63"); const auto umin = scan(0xffffffffu, 0x26000000u, 0x83800000u); const auto patched = [&umin](std::initializer_list> words) { auto code = umin; @@ -815,6 +4138,18 @@ void verifyHalfWaveReduction() { }; require(reductions(pixel(patched({{3, 0xbeea25c1u}}), 32u, true), spv::OpGroupNonUniformUMin) == 1u, "half-wave reduction: the scan under s_or_saveexec_b64 -1 did not read lane 31 as a subgroup UMin"); const char* lane63 = "v_readlane_b32 of lane 63 is outside the 32-lane host subgroup"; + auto divergent = umin; + divergent.insert(divergent.begin() + 3, 0x88fe1a7eu); + divergent.insert(divergent.begin() + 2, {0xbe860480u, 0x7d821688u, 0xbe9a246au, 0xbf880001u}); + require(reductions(pixel(divergent, 32u, true), spv::OpGroupNonUniformUMin) == 1u, "half-wave reduction: keys masked inside a divergent branch did not read lane 31 as a subgroup UMin"); + auto windowed = umin; + windowed.insert(windowed.begin() + 23, 0x7e008514u); + require(reductions(pixel(windowed, 32u, true), spv::OpGroupNonUniformUMin) == 1u, "half-wave reduction: a UMin scan of registers in a v_movreld_b32 window did not read lane 31 as a subgroup UMin on 32 lanes"); + require(halves(pixel(windowed, 64u, true), spv::OpGroupNonUniformUMin, 0xffffffffu), "half-wave reduction: a UMin scan of registers in a v_movreld_b32 window did not read lanes 31 and 63 as reductions of host invocations 0-31 and 32-63 on 64 lanes"); + auto overwritten = umin; + overwritten.insert(overwritten.begin() + 17, {0x7ef80500u, 0x7e008514u}); + expectFailure([&] { static_cast(pixel(overwritten, 32u, true)); }, lane63, "half-wave reduction: a scan of window registers read lane 63 after a v_movreld_b32 at a runtime M0 that may overwrite them"); + require(reductions(domain(divergent, 32u), spv::OpGroupNonUniformUMin) == 1u, "half-wave reduction: domain keys masked inside a divergent branch did not read lane 31 as a subgroup UMin on 32 lanes"); const char* arithmetic = "v_readlane_b32 of lane 31 of a wave64 half-wave reduction scan needs subgroup arithmetic"; expectFailure([&] { static_cast(pixel(umin, 32u, false)); }, arithmetic, "half-wave reduction: a device without subgroup arithmetic read lane 31 on 32 lanes"); expectFailure([&] { static_cast(pixel(umin, 64u, false)); }, arithmetic, "half-wave reduction: a device without subgroup arithmetic read lane 31 on 64 lanes"); @@ -836,6 +4171,228 @@ 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}; + static constexpr std::array userData{0u, 0u, 0u, 0x2020u}; + const auto vertex = [](std::span code, std::uint32_t waveSize, std::uint32_t subgroupSize, std::uint32_t userDataBase) { + RecompileRequest request{}; + request.shader = {ShaderStage::Vertex, 0x50000u, code, 0, {}}; + request.context.waveSize = waveSize; + request.context.userDataBaseRegister = userDataBase; + if (userDataBase == 0u) request.context.userData = userData; + request.context.vertex = ShaderVertexStageInfo{}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = subgroupSize; + request.target.supportedCapabilities = capabilities; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + return request; + }; + const auto prepared = [&vertex](std::span code, std::uint32_t subgroupSize, std::uint32_t userDataBase = 8u) { + return GetPreparedArtifact(*PrepareShader(vertex(code, 64u, subgroupSize, userDataBase))).spirv; + }; + const auto prepares = [&prepared](std::span code, std::uint32_t subgroupSize, std::uint32_t userDataBase = 8u) { + return !prepared(code, subgroupSize, userDataBase).empty(); + }; + const auto reductions = [](std::span spirv) { + std::uint32_t count = 0; + for (std::size_t cursor = 5; cursor < spirv.size() && (spirv[cursor] >> 16u) != 0u; cursor += spirv[cursor] >> 16u) { + if ((spirv[cursor] & 0xffffu) == spv::OpGroupNonUniformBitwiseOr && (spirv[cursor] >> 16u) == 6u && spirv[cursor + 4u] == spv::GroupOperationReduce) ++count; + } + return count; + }; + const std::vector culling{ + 0xd7650015u, 0x000100c1u, 0xd7660015u, 0x00022ac1u, 0x7e0e0280u, 0x7da206f9u, 0x81060015u, 0x360e0a81u, + 0xbefe04c1u, 0xd4c50002u, 0x00020e80u, 0xd763000fu, 0x00022a81u, 0xd5010000u, 0x000a1e80u, + 0x380000fau, 0xff091100u, 0x380000fau, 0xff091200u, 0x380000fau, 0xff091400u, 0x380000fau, 0xff091800u, + 0xd778000fu, 0x03058300u, 0x38001f00u, 0xd7600002u, 0x00013f00u, 0xd760006bu, 0x00017f00u, 0x88026b02u, + 0x7e020202u, 0xf80008cfu, 0x01010101u, 0xbf810000u, + }; + const auto patched = [&culling](std::initializer_list> words) { + auto code = culling; + for (const auto& [index, word] : words) code[index] = word; + return code; + }; + const char* lane63 = "v_readlane_b32 of lane 63 is outside the 32-lane host subgroup"; + require(reductions(prepared(culling, 32u)) == 1u, "narrow vertex wave: the NGG culling scan, keyed on lanes below the primitive count in s3, did not read lane 31 as a subgroup OR on 32 lanes"); + const auto wide64 = prepared(culling, 64u); + require(!wide64.empty() && reductions(wide64) == 0u, "narrow vertex wave: the NGG culling scan, keyed on lanes below 64 on 64 lanes, did not keep its DPP scan"); + require(prepares(patched({{6, 0x80060015u}}), 32u), "narrow vertex wave: the scan keyed on lanes below the vertex count in s3 did not prepare on 32 lanes"); + static constexpr std::array noArithmetic{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityClipDistance}; + const auto otherKeys = patched({{7, 0x360e0a82u}}); + auto withoutArithmetic = vertex(otherKeys, 64u, 32u, 8u); + withoutArithmetic.target.supportedCapabilities = noArithmetic; + const auto shuffled = GetPreparedArtifact(*PrepareShader(withoutArithmetic)).spirv; + require(!shuffled.empty() && reductions(shuffled) == 0u, "narrow vertex wave: without subgroup arithmetic, the scan did not keep lane 31 as a shuffle with lane 63 read as 0"); + require(prepares(patched({{5, 0x7da82aa0u}, {6, 0xbf800000u}}), 32u), "narrow vertex wave: the scan keyed on lanes below 32 did not prepare on 32 lanes"); + expectFailure([&] { static_cast(prepares(patched({{5, 0x7da82aa1u}, {6, 0xbf800000u}}), 32u)); }, lane63, "narrow vertex wave: a scan keyed on lanes below 33 read lane 63"); + expectFailure([&] { static_cast(prepares(culling, 32u, 0u)); }, lane63, "narrow vertex wave: a scan keyed on a count from user data read lane 63"); + expectFailure([&] { static_cast(prepares(patched({{6, 0x81060005u}}), 32u)); }, lane63, "narrow vertex wave: a scan keyed on the vertex index instead of the lane read lane 63"); + auto otherRegister = patched({{21, 0x380200fau}, {24, 0x03058301u}, {25, 0x38001f01u}}); + otherRegister.insert(otherRegister.begin(), 0x7e020305u); + require(reductions(prepared(otherRegister, 32u)) == 1u, "narrow vertex wave: a scan whose bound_ctrl:1 step is written to another register did not read lane 31 as a subgroup OR on 32 lanes"); + otherRegister[23] = 0xff011800u; + expectFailure([&] { static_cast(prepares(otherRegister, 32u)); }, lane63, "narrow vertex wave: a scan step written to another register without bound_ctrl:1, which keeps that register's lanes outside the row, read lane 63"); + auto threeSteps = culling; + threeSteps.erase(threeSteps.begin() + 21, threeSteps.begin() + 23); + const auto partialScan = prepared(threeSteps, 32u); + require(!partialScan.empty() && reductions(partialScan) == 0u, "narrow vertex wave: lane 63 of row_shr 1/2/4 steps over keys of 0 in lanes 32-63 did not read 0, or lane 31 became a subgroup OR"); + threeSteps[5] = 0x7da82aa1u; + threeSteps[6] = 0xbf800000u; + expectFailure([&] { static_cast(prepares(threeSteps, 32u)); }, lane63, "narrow vertex wave: lane 63 of row_shr 1/2/4 steps over keys of lanes below 33 read 0"); + + std::array, 2> narrow; + std::array, 2> wide; + for (const bool narrowFirst : {false, true}) { + const std::vector code{0xbf800001u + static_cast(narrowFirst), 0x7e020203u, 0xf80008cfu, 0x01010101u, 0xbf810000u}; + const auto narrowRequest = vertex(code, 64u, 32u, 8u); + const auto wideRequest = vertex(code, 64u, 64u, 8u); + ShaderPreparationContext preparation; + const auto first = PrepareShader(narrowFirst ? narrowRequest : wideRequest, &preparation); + const auto second = PrepareShader(narrowFirst ? wideRequest : narrowRequest, &preparation); + const auto& narrowHandle = narrowFirst ? *first : *second; + const auto& wideHandle = narrowFirst ? *second : *first; + require(first->source != second->source, "narrow vertex wave: the program prepared for 32 and for 64 lanes shares one translated source"); + require(MatchesPreparedShader(narrowRequest, narrowHandle) && MatchesPreparedShader(wideRequest, wideHandle), "narrow vertex wave: a prepared program did not match its own request"); + require(!MatchesPreparedShader(narrowRequest, wideHandle) && !MatchesPreparedShader(wideRequest, narrowHandle), "narrow vertex wave: a program prepared for one subgroup width matched a request for the other"); + narrow[narrowFirst ? 1u : 0u] = GetPreparedArtifact(narrowHandle).spirv.Copy(); + wide[narrowFirst ? 1u : 0u] = GetPreparedArtifact(wideHandle).spirv.Copy(); + } + require(narrow[0] == narrow[1] && wide[0] == wide[1], "narrow vertex wave: preparing for one subgroup width after the other gave another program than preparing it first"); + require(narrow[0] != wide[0], "narrow vertex wave: s3 of a wave64 vertex program was the same on 32 and 64 lanes"); + const std::vector wave32{0x7e020203u, 0xf80008cfu, 0x01010101u, 0xbf810000u}; + require(GetPreparedArtifact(*PrepareShader(vertex(wave32, 32u, 32u, 8u))).spirv == GetPreparedArtifact(*PrepareShader(vertex(wave32, 32u, 64u, 8u))).spirv, "narrow vertex wave: s3 of a wave32 vertex program depended on the subgroup width"); +} + +void verifySingleLaneVertexWave() { + using namespace ShaderRecompiler; + static constexpr std::array capabilities{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic, spv::CapabilityClipDistance}; + const auto vertex = [](std::span code, std::uint32_t subgroupSize, bool vertexSubgroups) { + RecompileRequest request{}; + request.shader = {ShaderStage::Vertex, 0x58000u, code, 0, {}}; + request.context.waveSize = 64; + request.context.userDataBaseRegister = 8; + request.context.vertex = ShaderVertexStageInfo{}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = subgroupSize; + request.target.supportedCapabilities = capabilities; + request.target.vertexSubgroups = vertexSubgroups; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + return request; + }; + const auto compiled = [&vertex](std::span code, std::uint32_t subgroupSize, bool vertexSubgroups) { + return Recompile(vertex(code, subgroupSize, vertexSubgroups)).spirv; + }; + const auto usesSubgroups = [](std::span spirv) { + for (std::size_t cursor = 5; cursor < spirv.size() && (spirv[cursor] >> 16u) != 0u; cursor += spirv[cursor] >> 16u) { + const auto op = spirv[cursor] & 0xffffu; + if (op == spv::OpCapability && spirv[cursor + 1u] >= spv::CapabilityGroupNonUniform && spirv[cursor + 1u] <= spv::CapabilityGroupNonUniformQuad) return true; + if (op >= spv::OpGroupNonUniformElect && op <= spv::OpGroupNonUniformQuadSwap) return true; + if (op == spv::OpDecorate && spirv[cursor + 2u] == spv::DecorationBuiltIn && (spirv[cursor + 3u] == spv::BuiltInSubgroupLocalInvocationId || spirv[cursor + 3u] == spv::BuiltInSubgroupSize)) return true; + } + return false; + }; + const auto hasConstant = [](std::span spirv, std::uint32_t value) { + for (std::size_t cursor = 5; cursor < spirv.size() && (spirv[cursor] >> 16u) != 0u; cursor += spirv[cursor] >> 16u) { + if ((spirv[cursor] & 0xffffu) == spv::OpConstant && (spirv[cursor] >> 16u) == 4u && spirv[cursor + 3u] == value) return true; + } + return false; + }; + static constexpr std::array waveInfo{0x7e020203u, 0xf80008cfu, 0x01010101u, 0xbf810000u}; + static constexpr std::array laneOps{ + 0xd7650015u, 0x000100c1u, 0xd7660015u, 0x00022ac1u, 0x7e0c0500u, 0xd7600007u, 0x00010100u, 0x4a022a06u, + 0x4a020207u, 0xf80008cfu, 0x01010101u, 0xbf810000u, + }; + static constexpr std::array waterfall{0xbe80047eu, 0xbe821400u, 0xd7600003u, 0x00000500u, 0xbe801c02u, 0xbf138000u, 0xbf85fffau, 0x7e020203u, 0xf80008cfu, 0x01010101u, 0xbf810000u}; + static constexpr std::array lane31{0xd7600006u, 0x00013f00u, 0x7e020206u, 0xf80008cfu, 0x01010101u, 0xbf810000u}; + + const auto single = compiled(waveInfo, 32u, false); + const auto wide = compiled(waveInfo, 64u, true); + require(hasConstant(single, 0x0101u) && !hasConstant(single, 0x4040u) && !hasConstant(single, 0x2020u), "single-lane vertex wave: s3 of a vertex program without vertex subgroups was not one vertex and one primitive"); + require(hasConstant(wide, 0x4040u) && !hasConstant(wide, 0x0101u), "single-lane vertex wave: s3 of a vertex program with vertex subgroups on 64 lanes changed"); + require(single == compiled(waveInfo, 4u, false), "single-lane vertex wave: the program without vertex subgroups depended on the device subgroup size"); + for (const auto code : {std::span(laneOps), std::span(waterfall)}) { + const auto withSubgroups = compiled(code, 32u, true); + const auto withoutSubgroups = compiled(code, 32u, false); + require(usesSubgroups(withSubgroups), "single-lane vertex wave: the program did not use subgroup operations with vertex subgroups"); + require(!withoutSubgroups.empty() && !usesSubgroups(withoutSubgroups), "single-lane vertex wave: a vertex program without vertex subgroups declared a subgroup capability, built-in or instruction"); + } + expectFailure([&] { static_cast(compiled(lane31, 32u, false)); }, "v_readlane_b32 of lane 31 is outside the 1-lane host subgroup", "single-lane vertex wave: a vertex program without vertex subgroups read lane 31"); + require(!compiled(lane31, 32u, true).empty(), "single-lane vertex wave: lane 31 of a 32-lane vertex subgroup did not build"); + + ShaderPixelStageInfo info{}; + info.inputAddr = PixelInputBit(PixelInput::PerspectiveCenter); + info.hasPerspectiveCenterVgpr = true; + info.targetOutputMode[0] = 9u; + info.targetExportMapping.fill(0xe4u); + static constexpr std::array pixelLane31{0xd7600006u, 0x00013f00u, 0x7e020206u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + RecompileRequest pixel{}; + pixel.shader = {ShaderStage::Fragment, 0x30000u, pixelLane31, 0, {}}; + pixel.context.waveSize = 64; + pixel.context.pixel = info; + pixel.target.vulkanVersion = 0x00401000u; + pixel.target.spirvVersion = 0x00010300u; + pixel.target.subgroupSize = 32u; + pixel.target.vertexSubgroups = false; + pixel.layout.pushConstantSizeBytes = 128; + pixel.useCache = false; + require(usesSubgroups(Recompile(pixel).spirv), "single-lane vertex wave: a pixel program stopped using subgroups when only the vertex stage lacks them"); + + auto request = vertex(waveInfo, 32u, false); + const auto replay = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(request)); + require(!replay.request.target.vertexSubgroups && replay.request.target.subgroupSize == 32u, "single-lane vertex wave: missing vertex subgroups were lost in serialization"); + require(Recompile(replay.request).spirv == single, "single-lane vertex wave: the replayed request built another program"); + std::vector withoutKey; + std::vector withKey; + RecompileCacheKey::Build(request, withoutKey); + request.target.vertexSubgroups = true; + RecompileCacheKey::Build(request, withKey); + require(withoutKey != withKey, "single-lane vertex wave: vertex subgroup support did not change the cache key"); +} + void verifyMeshConfiguration() { using namespace ShaderRecompiler; ShaderMeshInputInfo list; @@ -861,6 +4418,44 @@ void verifyMeshConfiguration() { request.graphics = GraphicsCompileContext{0u, {}, mesh, std::nullopt, {}}; const auto replay = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(request)); require(replay.request.graphics.has_value() && replay.request.graphics->mesh.has_value() && replay.request.graphics->mesh->esgsItemSize == 12u && replay.request.graphics->mesh->primitivesPerGroup == 21u, "mesh configuration was lost in serialization"); + { + RecompileRequest vertex{}; + vertex.shader = {ShaderStage::Vertex, 0x10000u, code, 0, {}}; + vertex.context.waveSize = 64; + ShaderVertexStageInfo info{}; + info.paClVsOutCntl = 0x0040000fu; + vertex.context.vertex = info; + const auto back = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(vertex)); + require(back.request.context.vertex.has_value() && back.request.context.vertex->paClVsOutCntl == 0x0040000fu, "PA_CL_VS_OUT_CNTL was lost in serialization"); + std::vector clipped; + RecompileCacheKey::Build(vertex, clipped); + vertex.context.vertex->paClVsOutCntl = 0; + std::vector unclipped; + RecompileCacheKey::Build(vertex, unclipped); + require(clipped != unclipped, "PA_CL_VS_OUT_CNTL is not part of the recompile cache key"); + } + { + 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; @@ -870,6 +4465,80 @@ void verifyMeshConfiguration() { other.graphics = GraphicsCompileContext{0u, {}, otherMesh, std::nullopt, {}}; RecompileCacheKey::Build(other, key); require(key != first && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(other), "the cache keys ignore the mesh configuration"); + require(mesh.outputPrimitiveSize == 3u && replay.request.graphics->mesh->outputPrimitiveSize == 3u, "a mesh configuration does not output triangles by default"); + otherMesh = mesh; + otherMesh.outputPrimitiveSize = 2u; + other.graphics = GraphicsCompileContext{0u, {}, otherMesh, std::nullopt, {}}; + const auto lineReplay = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(other)); + require(lineReplay.request.graphics.has_value() && lineReplay.request.graphics->mesh.has_value() && lineReplay.request.graphics->mesh->outputPrimitiveSize == 2u, "the mesh output primitive size was lost in serialization"); + RecompileCacheKey::Build(other, key); + require(key != first && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(other), "the cache keys ignore the mesh output primitive size"); + RecompileRequest hull{}; + hull.shader = {ShaderStage::TessellationControl, 0x10000u, code, 0, {}}; + hull.context.waveSize = 64; + hull.graphics = GraphicsCompileContext{0u, {}, std::nullopt, TessellationConfiguration{4u, 4u, 2u, 2u, 3u, 63u, 63u, true}, {}}; + const auto hullReplay = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(hull)); + const auto* replayed = hullReplay.request.graphics && hullReplay.request.graphics->tessellation ? &*hullReplay.request.graphics->tessellation : nullptr; + require(replayed != nullptr && replayed->domain == 2u && replayed->outputTopology == 3u && replayed->patchesPerGroup == 63u && replayed->primitiveGroupSize == 63u && replayed->breakAtInstance, "the quad tessellation configuration, its patches per group, its primitive group size or BREAK_WAVE_AT_EOI were lost in serialization"); + auto grouped = hull; + grouped.graphics->tessellation->patchesPerGroup = 62u; + for (const auto stage : {ShaderStage::Local, ShaderStage::TessellationControl, ShaderStage::TessellationEvaluation}) { + hull.shader.stage = stage; + grouped.shader.stage = stage; + RecompileCacheKey::Build(hull, key); + const auto patchKey = key; + RecompileCacheKey::Build(grouped, key); + require((key != patchKey) == (stage != ShaderStage::Local), "only the hull and domain cache keys may depend on the patches per group, which bound their relative patch index"); + } + auto primitiveGrouped = hull; + primitiveGrouped.graphics->tessellation->primitiveGroupSize = 126u; + for (const auto stage : {ShaderStage::Local, ShaderStage::TessellationControl, ShaderStage::TessellationEvaluation}) { + hull.shader.stage = stage; + primitiveGrouped.shader.stage = stage; + RecompileCacheKey::Build(hull, key); + const auto patchKey = key; + RecompileCacheKey::Build(primitiveGrouped, key); + require((key != patchKey) == (stage != ShaderStage::Local), "only the hull and domain cache keys may depend on the primitive group size, which rebuilds their relative patch index"); + } + auto 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; + instanced.shader.stage = stage; + longer.shader.stage = stage; + breaking.shader.stage = stage; + RecompileCacheKey::Build(hull, key); + 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; + request.context.vertex = ShaderVertexStageInfo{}; + request.target.subgroupSize = 32u; + request.graphics->mesh = MeshConfiguration{1u, 1u, 1u, 1u, 1u, 32u, 256u, 0u, 4u}; + for (const std::uint32_t threads : {32u, 64u, 128u, 480u}) { + request.graphics->mesh->threadsPerGroup = threads; + require(PrepareResourceProgram(request).WaveSize() == 32u, "a wave32 mesh program did not preserve its guest wave size"); + } + for (const std::uint32_t threads : {31u, 512u}) { + request.graphics->mesh->threadsPerGroup = threads; + expectFailure([&] { static_cast(PrepareResourceProgram(request)); }, "mesh shader translation configuration is not supported", "mesh translation accepted a partial wave or overflowing wave count"); + } } ShaderRecompiler::ShaderPixelStageInfo twoParameterPixel() { @@ -899,15 +4568,186 @@ std::string requestPrefix(std::string_view text, std::size_t bytes) { return result; } +void verifyCanonicalVersion20And21Requests() { + using namespace ShaderRecompiler; + struct Fixture { ShaderStage stage; std::uint32_t selector; bool subgroups; std::string_view text; }; + static constexpr std::array fixtures{{ + {ShaderStage::Fragment, 0u, false, + "NVNQQRQAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEAAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAA" + "AAF4VjQSAQEBOTAAAAA="}, + {ShaderStage::Fragment, 0u, true, + "NVNQQRQAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEAAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAA" + "AAF4VjQSAQEBOTAAAAE="}, + {ShaderStage::Fragment, 1u, false, + "NVNQQRQAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEBAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAA" + "AAF4VjQSAQEBOTAAAAA="}, + {ShaderStage::Fragment, 1u, true, + "NVNQQRQAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEBAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAA" + "AAF4VjQSAQEBOTAAAAE="}, + {ShaderStage::Fragment, 2u, false, + "NVNQQRQAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQECAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAA" + "AAF4VjQSAQEBOTAAAAA="}, + {ShaderStage::Fragment, 2u, true, + "NVNQQRQAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQECAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAA" + "AAF4VjQSAQEBOTAAAAE="}, + {ShaderStage::Fragment, 3u, false, + "NVNQQRQAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEDAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAA" + "AAF4VjQSAQEBOTAAAAA="}, + {ShaderStage::Fragment, 3u, true, + "NVNQQRQAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEDAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAA" + "AAF4VjQSAQEBOTAAAAE="}, + {ShaderStage::Vertex, 330245u, false, + "NVNQQRQAAAABAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQAAAA0AAAABBQoFAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAAAAF4VjQS" + "AQEBOTAAAAA="}, + {ShaderStage::Vertex, 330245u, true, + "NVNQQRQAAAABAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQAAAA0AAAABBQoFAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAAAAF4VjQS" + "AQEBOTAAAAE="}, + {ShaderStage::Vertex, 656650u, false, + "NVNQQRQAAAABAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQAAAA0AAAABCgUKAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAAAAF4VjQS" + "AQEBOTAAAAA="}, + {ShaderStage::Vertex, 656650u, true, + "NVNQQRQAAAABAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQAAAA0AAAABCgUKAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAAAAF4VjQS" + "AQEBOTAAAAE="}, + }}; + constexpr std::array userData{0x13579bdfu, 0x2468ace0u, 0xdeadbeefu}; + constexpr std::array bytes{std::byte{0x12}, std::byte{0x34}, std::byte{0x56}, std::byte{0x78}}; + const RequestSerializer serializer; + // Original serializers at 4bb2c7f394 (20) and 075169f7b3 (21) emitted these + // twelve payloads independently; every pair differs only in its version header. + for (const auto version : {20u, 21u}) { + for (const auto& fixture : fixtures) { + auto text = std::string(fixture.text); + if (version == 21u) { + constexpr std::string_view prefix = "NVNQQRUAAAA"; + text.replace(0, prefix.size(), prefix); + } + const auto legacy = serializer.Deserialize(text); + const auto& request = legacy.request; + require(request.shader.stage == fixture.stage && request.shader.codeAddress == 0x30000u && request.shader.code.size() == 1u && request.shader.code[0] == 0xbf810000u, "canonical version20/21 shader identity was misread"); + require(request.context.waveSize == 64u && request.context.userDataBaseRegister == 8u && request.context.userData.size() == userData.size() && std::equal(request.context.userData.begin(), request.context.userData.end(), userData.begin()), "canonical version20/21 guest context was misread"); + require(request.context.memory.size() == 1u && request.context.memory[0].guestAddress == 0x50000u && request.context.memory[0].bytes.size() == bytes.size() && std::equal(request.context.memory[0].bytes.begin(), request.context.memory[0].bytes.end(), bytes.begin()), "canonical version20/21 trailing memory was misread"); + require(request.layout.descriptorSet == 2u && request.layout.firstBinding == 11u && request.layout.pushConstantOffsetBytes == 16u && request.layout.pushConstantSizeBytes == 64u && !request.useCache, "canonical version20/21 trailing layout or cache flag was misread"); + require(request.target.vertexSubgroups == fixture.subgroups && request.target.float64DenormPreserve && request.target.registrationModules && request.target.registrationWordLimit == 12345u && request.target.nonConstantImageOffsets && request.target.srgbDecodeFormats == 0x12345678u && request.target.narrowSubgroupClock, "canonical version20/21 target fields were misread"); + if (fixture.stage == ShaderStage::Fragment) { + require(request.context.pixel.has_value() && !request.context.vertex, "canonical version20/21 pixel presence was misread"); + const auto& pixel = *request.context.pixel; + require(pixel.rasterPrimitive == static_cast(fixture.selector) && !pixel.layerExported && !pixel.ditheredAlphaToCoverage && pixel.alphaToCoverage && pixel.stencilRefExportEnable && pixel.targetOutputMode[0] == 9u && pixel.targetExportMapping[0] == 0xe4u, "canonical version20/21 pixel fields were misread"); + } else { + require(request.context.vertex.has_value() && !request.context.pixel, "canonical version20/21 vertex presence was misread"); + const auto& vertex = *request.context.vertex; + require(vertex.paClVsOutCntl == fixture.selector && vertex.fetchAttribReg == 9u && vertex.fetchBufferReg == 13u && vertex.fetchEmbedded, "canonical version20/21 vertex fields were misread"); + } + const auto upgraded = serializer.Deserialize(serializer.Serialize(request)); + require(RecompileCacheKey::ContextHash(request) == RecompileCacheKey::ContextHash(upgraded.request), "upgrading canonical version20/21 changed shader identity"); + require(serializer.Serialize(upgraded.request) == serializer.Serialize(request), "upgrading canonical version20/21 changed request fields"); + } + } +} void verifyPixelRequestSerialization() { using namespace ShaderRecompiler; const auto fields = [](const ShaderPixelStageInfo& value) { 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.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}; @@ -936,6 +4776,7 @@ void verifyPixelRequestSerialization() { .posZ = true, .posW = true, .frontFace = true, + .frontFaceAllBits = true, .ancillary = true, .sampleShading = true, .noPerspective = true, @@ -950,6 +4791,10 @@ void verifyPixelRequestSerialization() { .targetOutputMode = {1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u} }; for (std::uint32_t i = 0; i < pixel.interpolatorSettings.size(); ++i) pixel.interpolatorSettings[i] = 0x10101010u + i; + pixel.alphaToCoverage = true; + pixel.stencilRefExportEnable = true; + pixel.ditheredAlphaToCoverage = true; + pixel.reversedBlend = 0x570001u; const std::array, 3> mappings{{ {0x00u, 0xe4u, 0xc6u, 0x1bu, 0xffu, 0x80u, 0x55u, 0xaau}, {0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u}, @@ -974,6 +4819,22 @@ void verifyPixelRequestSerialization() { RecompileCacheKey::Build(changed, replayKey); require(key == replayKey && RecompileCacheKey::ContextHash(request) == RecompileCacheKey::ContextHash(changed), "runtime pixel export mapping changed the static shader identity"); } + auto undithered = request; + undithered.context.pixel->ditheredAlphaToCoverage = false; + RecompileCacheKey::Build(undithered, replayKey); + require(key != replayKey && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(undithered), "dithered coverage did not change shader identity"); + auto 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); + require(key != replayKey && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(withoutCoverage), "alpha-to-coverage did not change the static shader identity"); + auto withoutStencilRef = request; + withoutStencilRef.context.pixel->stencilRefExportEnable = false; + RecompileCacheKey::Build(withoutStencilRef, replayKey); + require(key != replayKey && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(withoutStencilRef), "the stencil reference export did not change the static shader identity"); } request.context.pixel.reset(); request.shader.stage = ShaderStage::Vertex; @@ -984,20 +4845,499 @@ void verifyPixelRequestSerialization() { minimal.shader = {ShaderStage::Fragment, 0x30000u, code, 0, {}}; minimal.context.waveSize = 64; minimal.context.pixel = ShaderPixelStageInfo{}; + minimal.context.pixel->rasterPrimitive = RasterPrimitive::Rectangle; + minimal.context.pixel->layerExported = true; + minimal.context.pixel->ditheredAlphaToCoverage = true; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQQ0AAAA=", "new requests did not use serialization version 13"); - constexpr std::size_t mappingOffset = 8u + 37u + 18u + 163u; - for (std::size_t bytes = 0; bytes < 8u; ++bytes) { - expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated version-12 pixel mapping was accepted"); + require(requestPrefix(encoded, 8u) == "NVNQQSoAAAA=", "new requests did not use serialization version 42"); + 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( + "NVNQQRAAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAEEAAAAMBAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAAQAAAAgAAAAAAAAAAAAAAAAQ=="); + require(legacy16.request.context.pixel->alphaToCoverage && !legacy16.request.context.pixel->ditheredAlphaToCoverage, "version16 coverage acquired a dither mode"); + require(legacy16.request.context.pixel->targetOutputMode[0] == 9u && legacy16.request.context.pixel->targetExportMapping[0] == 0xe4u && legacy16.request.target.float64DenormPreserve, "version16 export or FP64 fields were lost"); + require(legacy16.request.layout.firstBinding == 11u && legacy16.request.layout.pushConstantOffsetBytes == 16u && !legacy16.request.useCache, "version16 trailing fields were misaligned"); + const auto upgraded16 = serializer.Deserialize(serializer.Serialize(legacy16.request)); + require(RecompileCacheKey::ContextHash(legacy16.request) == RecompileCacheKey::ContextHash(upgraded16.request), "upgrading version16 changed shader identity"); + const auto legacy17 = serializer.Deserialize( + "NVNQQREAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAABBAAAADAQBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAEAAAAIAAAAAAAAAAAAAAAAEBOTAAAA=="); + require(legacy17.request.context.pixel->alphaToCoverage && legacy17.request.context.pixel->stencilRefExportEnable && !legacy17.request.context.pixel->ditheredAlphaToCoverage, "version17 coverage or stencil flags were lost"); + require(legacy17.request.target.float64DenormPreserve && legacy17.request.target.registrationModules && legacy17.request.target.registrationWordLimit == 12345u, "version17 registration or FP64 fields were lost"); + require(legacy17.request.context.pixel->targetOutputMode[0] == 9u && legacy17.request.context.pixel->targetExportMapping[0] == 0xe4u && legacy17.request.layout.firstBinding == 11u && legacy17.request.layout.pushConstantOffsetBytes == 16u && !legacy17.request.useCache, "version17 export or trailing fields were misaligned"); + const auto upgraded17 = serializer.Deserialize(serializer.Serialize(legacy17.request)); + require(RecompileCacheKey::ContextHash(legacy17.request) == RecompileCacheKey::ContextHash(upgraded17.request), "upgrading version17 changed shader identity"); + const std::array, 2> legacy18Requests{{ + {false, + "NVNQQRIAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAEAAAAAAAAAAAAAAAAAAAAAEBOTAAAAA="}, + {true, + "NVNQQRIAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAEAAAAAAAAAAAAAAAAAAAAAEBOTAAAAE="} + }}; + for (const auto& [subgroups, text] : legacy18Requests) { + const auto legacy18 = serializer.Deserialize(text); + require(legacy18.request.target.vertexSubgroups == subgroups, "version18 vertex-subgroup flag was lost"); + require(legacy18.request.context.pixel->alphaToCoverage && legacy18.request.context.pixel->stencilRefExportEnable && !legacy18.request.context.pixel->ditheredAlphaToCoverage, "version18 coverage or stencil flags were lost"); + require(legacy18.request.target.float64DenormPreserve && legacy18.request.target.registrationModules && legacy18.request.target.registrationWordLimit == 12345u, "version18 registration or FP64 fields were lost"); + require(legacy18.request.context.pixel->targetOutputMode[0] == 9u && legacy18.request.context.pixel->targetExportMapping[0] == 0xe4u && legacy18.request.layout.firstBinding == 11u && legacy18.request.layout.pushConstantOffsetBytes == 16u && !legacy18.request.useCache, "version18 export or trailing fields were misaligned"); + const auto upgraded18 = serializer.Deserialize(serializer.Serialize(legacy18.request)); + require(RecompileCacheKey::ContextHash(legacy18.request) == RecompileCacheKey::ContextHash(upgraded18.request), "upgrading version18 changed shader identity"); + require(upgraded18.request.target.vertexSubgroups == subgroups, "upgrading version18 changed vertex-subgroup support"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQQ4AAAA="}) { + const std::array, std::string_view>, 8> legacy19Requests{{ + {{static_cast(0u), false}, + "NVNQQRMAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAABAAAAAAAAAAAAAAAAAAAAABATkwAAAA"}, + {{static_cast(0u), true}, + "NVNQQRMAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAABAAAAAAAAAAAAAAAAAAAAABATkwAAAB"}, + {{static_cast(1u), false}, + "NVNQQRMAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAABAAAAAAAAAAAAAAAAAAAAABATkwAAAA"}, + {{static_cast(1u), true}, + "NVNQQRMAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAABAAAAAAAAAAAAAAAAAAAAABATkwAAAB"}, + {{static_cast(2u), false}, + "NVNQQRMAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQECAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAABAAAAAAAAAAAAAAAAAAAAABATkwAAAA"}, + {{static_cast(2u), true}, + "NVNQQRMAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQECAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAABAAAAAAAAAAAAAAAAAAAAABATkwAAAB"}, + {{static_cast(3u), false}, + "NVNQQRMAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAABAAAAAAAAAAAAAAAAAAAAABATkwAAAA"}, + {{static_cast(3u), true}, + "NVNQQRMAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAABAAAAAAAAAAAAAAAAAAAAABATkwAAAB"}, + }}; + for (const auto& [flags, text] : legacy19Requests) { + const auto legacy19 = serializer.Deserialize(text); + require(legacy19.request.context.pixel->rasterPrimitive == flags.first && !legacy19.request.context.pixel->ditheredAlphaToCoverage, "version19 primitive or dither mode was misread"); + require(legacy19.request.context.pixel->alphaToCoverage && legacy19.request.context.pixel->stencilRefExportEnable, "version19 coverage or stencil flags were lost"); + require(legacy19.request.target.vertexSubgroups == flags.second && legacy19.request.target.float64DenormPreserve && legacy19.request.target.registrationModules && legacy19.request.target.registrationWordLimit == 12345u, "version19 target fields were misaligned"); + require(legacy19.request.context.pixel->targetOutputMode[0] == 9u && legacy19.request.context.pixel->targetExportMapping[0] == 0xe4u && legacy19.request.layout.firstBinding == 11u && legacy19.request.layout.pushConstantOffsetBytes == 16u && !legacy19.request.useCache, "version19 export or trailing fields were misaligned"); + const auto upgraded19 = serializer.Deserialize(serializer.Serialize(legacy19.request)); + require(RecompileCacheKey::ContextHash(legacy19.request) == RecompileCacheKey::ContextHash(upgraded19.request), "upgrading version19 changed shader identity"); + } + constexpr std::size_t mappingOffset = 8u + 37u + 18u + 163u; + for (std::size_t bytes = 0; bytes < 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=", "NVNQQSsAAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } + static constexpr std::string_view version18Request{ + "NVNQQRIAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAB0gQAAAE="}; + const auto version18 = serializer.Deserialize(version18Request); + require(version18.request.context.pixel.has_value() && version18.request.context.pixel->rasterPrimitive == RasterPrimitive::Triangle, "a version 18 request read a primitive class"); + require(version18.request.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"); +} + +void verifyPositionExportComponents() { + constexpr std::uint32_t none = 0xffffffffu; + struct Case { + std::uint32_t control; + std::array clip; + std::array cull; + }; + const std::array cases{{ + {0x00400000u, {none, none, none, none}, {none, none, none, none}}, + {0x00400001u, {0u, none, none, none}, {none, none, none, none}}, + {0x00400002u, {none, 0u, none, none}, {none, none, none, none}}, + {0x00400005u, {0u, none, 1u, none}, {none, none, none, none}}, + {0x00400100u, {none, none, none, none}, {0u, none, none, none}}, + {0x00400400u, {none, none, none, none}, {none, none, 0u, none}}, + }}; + for (const auto& test : cases) { + for (std::uint32_t component = 0; component < 4; ++component) { + const auto output = ShaderRecompiler::DecodePositionExportComponent(test.control, 1u, component); + require(output.clipDistance == test.clip[component] && output.cullDistance == test.cull[component], "PA_CL_VS_OUT_CNTL decoded a position component to the wrong clip or cull distance"); + } + } +} + +std::vector decodeBase64(std::string_view text) { + constexpr std::string_view alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + std::vector bytes; + std::uint32_t chunk = 0; + std::size_t bits = 0; + for (const char symbol : text) { + if (symbol == '=') break; + const auto value = alphabet.find(symbol); + require(value != std::string_view::npos, "invalid base64 symbol"); + chunk = ((chunk << 6u) | static_cast(value)) & 0xffffffu; + bits += 6u; + if (bits >= 8u) { + bits -= 8u; + bytes.push_back(static_cast((chunk >> bits) & 0xffu)); + } + } + return bytes; +} + +std::string encodeBase64(const std::vector& bytes) { + constexpr std::string_view alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + std::string text; + for (std::size_t index = 0; index < bytes.size(); index += 3u) { + const std::size_t count = std::min(3u, bytes.size() - index); + std::uint32_t chunk = static_cast(bytes[index]) << 16u; + if (count > 1u) chunk |= static_cast(bytes[index + 1u]) << 8u; + if (count > 2u) chunk |= bytes[index + 2u]; + for (std::size_t symbol = 0; symbol < 4u; ++symbol) { + text.push_back(symbol <= count ? alphabet[(chunk >> (18u - 6u * symbol)) & 0x3fu] : '='); + } + } + return text; +} + +void verifyVertexInfoVersion19() { + using namespace ShaderRecompiler; + const std::array code{0xbf810000u}; + const RequestSerializer serializer; + RecompileRequest vertex{}; + vertex.shader = {ShaderStage::Vertex, 0x10000u, code, 0, {}}; + vertex.context.waveSize = 64; + vertex.context.vertex = ShaderVertexStageInfo{}; + const auto unmarked = serializer.Serialize(vertex); + vertex.context.vertex->paClVsOutCntl = 0x89abcdefu; + const auto marked = serializer.Serialize(vertex); + const auto plain = decodeBase64(unmarked); + const auto flagged = decodeBase64(marked); + require(plain.size() == flagged.size(), "PA_CL_VS_OUT_CNTL changed the request size"); + std::size_t offset = 0; + while (offset < plain.size() && plain[offset] == flagged[offset]) ++offset; + require(offset + 4u <= plain.size() && flagged[offset] == 0xefu && flagged[offset + 1u] == 0xcdu && flagged[offset + 2u] == 0xabu && flagged[offset + 3u] == 0x89u, "PA_CL_VS_OUT_CNTL is not the four bytes the version 20 vertex info adds"); + require(std::equal(plain.begin() + offset + 4u, plain.end(), flagged.begin() + offset + 4u), "PA_CL_VS_OUT_CNTL moved the fields after it"); + auto 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"); + require(serializer.Serialize(replay20Vertex.request) == marked, "a main version20 vertex request misread trailing fields"); + vertex.context.vertex->paClVsOutCntl = (1u << 18u) | (1u << 21u) | 0xfu; + const auto layered = serializer.Deserialize(serializer.Serialize(vertex)); + require(layered.request.context.vertex.has_value() && layered.request.context.vertex->paClVsOutCntl == vertex.context.vertex->paClVsOutCntl, "a layered vertex info lost its clip distances or render target index"); + 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); + const auto replay = serializer.Deserialize(encodeBase64(legacy)); + require(replay.request.context.vertex.has_value() && replay.request.context.vertex->paClVsOutCntl == 0u, "a legacy vertex info read a PA_CL_VS_OUT_CNTL word"); + require(serializer.Serialize(replay.request) == unmarked, "a legacy vertex info misread the fields after it"); + } + RecompileRequest pixel{}; + pixel.shader = {ShaderStage::Fragment, 0x20000u, code, 0, {}}; + pixel.context.waveSize = 64; + pixel.context.pixel = ShaderPixelStageInfo{}; + pixel.context.pixel->rasterPrimitive = RasterPrimitive::Rectangle; + const auto encodedPixel = serializer.Serialize(pixel); + auto exportedPixel = pixel; + exportedPixel.context.pixel->layerExported = true; + const auto exportedBytes = decodeBase64(serializer.Serialize(exportedPixel)); + auto 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 < 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)); + 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, "a version 35 depth remap request changed on re-serialization"); + const auto plainBytes = decodeBase64(plain); + const auto remappedBytes = decodeBase64(remapped); + require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 5u && std::equal(plainBytes.begin(), plainBytes.end() - 7, remappedBytes.begin()), "the depth remap flag and mode are not the 6 bytes before the f32 flush and denormal preserve flags 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); + 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.pop_back(); + 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 = remappedBytes; + 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"); + auto legacy = decodeBase64(merged); + legacy.pop_back(); + require(legacy.size() > 52u && 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{ + static constexpr std::array legacyPixelRequests{ "NVNQQQEAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA" "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAkAAAAAAAAAAAAA" @@ -1048,6 +5388,26 @@ void verifyLegacyPixelRequests() { "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAABAAAAAAAAAAAAAAAAAAAAAgkAAAAAAAAA" "AAAAAAAAAAAAAAAAAAAAAAAAEEAAAAMBAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" "AAsAAAAAAAAAgAAAAAAAAQAAAAA=", + "NVNQQQsAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAABAAAAAAAAAAAAAAAAAAAAAgAJAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAABBAAAADAQBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAALAAAAAAAAAIAAAAAAAAEAAAAA", + "NVNQQQwAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAABAAAAAAAAAAAAAAAAAAAAAgAJAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAABBAAAADAQBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAALAAAAAAAAAIAAAAAAAAEAAAAAAA==", + "NVNQQQ0AAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAABAAAAAAAAAAAAAAAAAAAAAgAJAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAQQAAAAwEAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAACwAAAAAAAACAAAAAAAABAAAAAAA=", + "NVNQQQ4AAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAABAAAAAAAAAAAAAAAAAAAAAgAJAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBAAAADAQBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAALAAAAAAAAAIAAAAAAAAEAAAAAAA==", }; const RequestSerializer serializer; for (std::size_t index = 0; index < legacyPixelRequests.size(); ++index) { @@ -1057,12 +5417,18 @@ void verifyLegacyPixelRequests() { require(request.context.pixel.has_value(), "legacy pixel state was lost"); const auto& pixel = *request.context.pixel; require(pixel.targetExportMapping == std::array{}, "legacy pixel mapping no longer defaults to zero"); + require(pixel.targetExportPacking == std::array{}, "a legacy request packed a color export"); + require(!pixel.dualSourceBlend, "a legacy request enabled dual-source blending"); + require(!pixel.alphaToCoverage && !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"); require(request.shader.stage == ShaderStage::Fragment && request.shader.code.size() == 1u && request.shader.code[0] == 0xbf810000u && !request.context.vertex.has_value() && request.context.memory.empty(), "legacy guest context was misaligned"); require(request.target.vulkanVersion == 0x00401000u && request.target.spirvVersion == 0x00010300u && request.target.subgroupSize == 64u && request.layout.firstBinding == 11u && request.layout.pushConstantSizeBytes == 128u, "legacy target or binding layout was misaligned"); - require(request.useCache == (version == 1u) && request.target.nonConstantImageOffsets == (version >= 6u) && request.target.srgbDecodeFormats == 0u && !request.target.narrowSubgroupClock, "legacy request trailer was misread"); + require(request.useCache == (version == 1u) && request.target.nonConstantImageOffsets == (version >= 6u) && request.target.srgbDecodeFormats == 0u && !request.target.narrowSubgroupClock && !request.target.float64DenormPreserve && !request.target.registrationModules && !request.target.float16DenormPreserve, "legacy request trailer was misread"); const auto upgraded = serializer.Deserialize(serializer.Serialize(request)); require(upgraded.request.context.pixel->targetExportMapping == pixel.targetExportMapping && RecompileCacheKey::ContextHash(upgraded.request) == RecompileCacheKey::ContextHash(request), "upgrading a legacy capture changed its pixel mapping"); } @@ -1279,7 +5645,7 @@ void verifyComputedTexelOffsets() { }; const auto computed = program(0x100u, 0u); const auto constant = program(0xffu, 0x3fu | (1u << 8u)); - const auto recompile = [&](const std::vector& code, bool offsets) { + const auto recompile = [&](std::span code, bool offsets) { RecompileRequest request{}; request.shader = {ShaderStage::Compute, 0x30000u, code, 0, {}}; request.target = BufferTarget(); @@ -1296,10 +5662,11 @@ 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 = [](const std::vector& words) { + const auto sampleOperands = [](std::span words) { std::uint32_t mask = 0; bool gatherExtended = false; for (std::size_t cursor = 5; cursor < words.size();) { @@ -1346,7 +5713,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; @@ -1384,7 +5752,7 @@ void verifyInt64AtomicCapabilities() { 0u, 0u, 0u, 0u}; const std::vector bufferAtomic{0xe1705000u, 0x80012803u, 0xbf810000u}; const std::vector imageAtomic{0xf03c0308u, 0x00020a08u, 0xbf810000u}; - const auto compile = [&](const std::vector& code, spv::Capability added) { + const auto compile = [&](std::span code, spv::Capability added) { const std::array capabilities{spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, static_cast(added)}; RecompileRequest request{}; request.shader = {ShaderStage::Compute, 0x58000u, code, 0, {}}; @@ -1403,7 +5771,7 @@ void verifyInt64AtomicCapabilities() { request.useCache = false; return Recompile(request).spirv; }; - const auto declares = [](const std::vector& words, spv::Capability capability) { + const auto declares = [](std::span words, spv::Capability capability) { for (std::size_t cursor = 5; cursor < words.size();) { const auto count = words[cursor] >> 16u; require(count != 0 && count <= words.size() - cursor, "64-bit atomics: truncated SPIR-V instruction"); @@ -1418,11 +5786,207 @@ void verifyInt64AtomicCapabilities() { require(declares(compile(imageAtomic, spv::CapabilityInt64ImageEXT), spv::CapabilityInt64ImageEXT), "64-bit atomics: an image_atomic_swap on a 32_32 image does not declare Int64ImageEXT"); } +void verifySpecializedBufferAtomicBindings() { + using namespace ShaderRecompiler; + const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + const std::array capabilities{spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + alignas(256) static std::array buffer{}; + const auto bufferAddress = static_cast(reinterpret_cast(buffer.data())); + const std::array userData{ + 0u, 0u, 0u, 0u, + static_cast(bufferAddress), static_cast((bufferAddress >> 32u) & 0xffffu), 256u, 0x31016facu, + 0u, 0u, 0u, 0u, + 0u, 0u, 0u, 0u}; + const std::vector code{0xe0f05000u, 0x80012803u, 0xbf810000u}; + const auto compile = [&](std::span supported) { + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x58800u, code, 0, {}}; + request.context.waveSize = 32; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32; + request.target.bdaAbiVersion = BdaAbi::Version; + request.target.supportedCapabilities = supported; + request.target.supportedExtensions = extensions; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + return Recompile(request); + }; + const auto result = compile(capabilities); + std::size_t tables = 0; + std::size_t faults = 0; + std::size_t atomicElements = 0; + for (const auto& binding : result.bindings) { + if (binding.role == DescriptorRole::BdaPagetable) ++tables; + if (binding.role == DescriptorRole::FaultBuffer) ++faults; + atomicElements += static_cast(std::count(binding.bufferAtomic.begin(), binding.bufferAtomic.end(), true)); + } + require(tables == 0, "specialized buffer atomic: a buffer_atomic_inc through a V# the program specializes binds the address tables"); + require(faults == 1 && result.bdaAbiVersion == BdaAbi::Version, "specialized buffer atomic: the program has no fault buffer for its unaligned atomic"); + require(atomicElements == 1, "specialized buffer atomic: the V# element is not reported as atomic"); + const auto words = result.spirv.Words(); + std::uint32_t faultFunction = 0; + std::size_t faultCalls = 0; + bool physical = false; + bool int64 = false; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + const auto opcode = words[cursor] & 0xffffu; + require(count != 0 && count <= words.size() - cursor, "specialized buffer atomic: truncated SPIR-V instruction"); + if (opcode == spv::OpCapability && words[cursor + 1] == spv::CapabilityPhysicalStorageBufferAddresses) physical = true; + if (opcode == spv::OpCapability && words[cursor + 1] == spv::CapabilityInt64) int64 = true; + if (opcode == spv::OpMemoryModel && words[cursor + 1] != spv::AddressingModelLogical) physical = true; + if (opcode == spv::OpName && std::string_view(reinterpret_cast(&words[cursor + 2])) == "record_bda_fault") faultFunction = words[cursor + 1]; + if (opcode == spv::OpFunctionCall && faultFunction != 0 && words[cursor + 3] == faultFunction) ++faultCalls; + cursor += count; + } + require(!physical, "specialized buffer atomic: the module addresses physical storage without an address access"); + require(int64, "specialized buffer atomic: the fault address is 64-bit and the module does not declare Int64"); + require(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"); +} + +void verifyCoherentGds() { + using namespace ShaderRecompiler; + const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + const std::array capabilities{spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + const std::vector code{0x34000082u, 0xbefc03ffu, 0x0000ffffu, 0xd8da0000u, 0x01000000u, 0xbf8cc07fu, 0xd8360080u, 0x00000100u, 0xbf810000u}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x5c000u, code, 0, {}}; + request.context.waveSize = 32; + request.context.userDataBaseRegister = 0; + request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32; + request.target.bdaAbiVersion = BdaAbi::Version; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + const auto words = Recompile(request).spirv; + std::uint32_t gds = 0u; + std::map> decorations; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + const auto opcode = words[cursor] & 0xffffu; + require(count != 0 && count <= words.size() - cursor, "coherent GDS: truncated SPIR-V instruction"); + if (opcode == spv::OpName && std::string_view(reinterpret_cast(&words[cursor + 2])) == "gds") gds = words[cursor + 1]; + if (opcode == spv::OpDecorate) decorations[words[cursor + 1]].push_back(words[cursor + 2]); + cursor += count; + } + require(gds != 0u, "coherent GDS: a ds_read_b32 and ds_write_b32 on GDS declare no gds buffer"); + require(std::ranges::find(decorations[gds], static_cast(spv::DecorationCoherent)) != decorations[gds].end(), "coherent GDS: the gds buffer is not Coherent"); +} + +void verifyCoherentBufferAccesses() { + using namespace ShaderRecompiler; + alignas(256) static std::array input{}; + alignas(256) static std::array output{}; + const auto inputAddress = static_cast(reinterpret_cast(input.data())); + const auto outputAddress = static_cast(reinterpret_cast(output.data())); + const std::array userData{ + static_cast(inputAddress), static_cast((inputAddress >> 32u) & 0xffffu), 256u, 0x31016facu, + static_cast(outputAddress), static_cast((outputAddress >> 32u) & 0xffffu), 256u, 0x31016facu}; + const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + const std::array capabilities{spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + struct Module { + std::map names; + std::map> decorations; + std::vector loads; + std::vector stores; + }; + const auto compile = [&](bool invalidate, bool glcLoad, bool glcStore) { + std::vector code{0x34020082u}; + if (invalidate) code.insert(code.end(), {0xe1c40000u, 0u}); + code.insert(code.end(), {0xe0301000u | (glcLoad ? 0x4000u : 0u), 0x80000201u, 0xbf8c3f70u, 0xe0701000u | (glcStore ? 0x4000u : 0u), 0x80010201u, 0xbf810000u}); + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x5a000u, 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; + const auto words = Recompile(request).spirv; + Module module; + std::map bases; + const auto base = [&](std::uint32_t pointer) { + const auto found = bases.find(pointer); + const auto variable = found == bases.end() ? pointer : found->second; + const auto name = module.names.find(variable); + return name == module.names.end() ? std::string() : name->second; + }; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + const auto opcode = words[cursor] & 0xffffu; + require(count != 0 && count <= words.size() - cursor, "coherent buffers: truncated SPIR-V instruction"); + if (opcode == spv::OpName) module.names[words[cursor + 1]] = reinterpret_cast(&words[cursor + 2]); + if (opcode == spv::OpDecorate) module.decorations[words[cursor + 1]].push_back(words[cursor + 2]); + if (opcode == spv::OpAccessChain || opcode == spv::OpInBoundsAccessChain) { + const auto found = bases.find(words[cursor + 3]); + bases[words[cursor + 2]] = found == bases.end() ? words[cursor + 3] : found->second; + } + if (opcode == spv::OpLoad && base(words[cursor + 3]).starts_with("buffers")) module.loads.push_back(base(words[cursor + 3])); + if (opcode == spv::OpStore && base(words[cursor + 1]).starts_with("buffers")) module.stores.push_back(base(words[cursor + 1])); + cursor += count; + } + return module; + }; + const auto decorated = [](const Module& module, std::string_view name, spv::Decoration decoration) { + for (const auto& [id, variable] : module.names) { + if (variable != name) continue; + const auto found = module.decorations.find(id); + return found != module.decorations.end() && std::ranges::find(found->second, static_cast(decoration)) != found->second.end(); + } + return false; + }; + const auto declared = [](const Module& module, std::string_view name) { + return std::ranges::any_of(module.names, [&](const auto& entry) { return entry.second == name; }); + }; + const auto only = [](const std::vector& accesses, std::string_view name) { + return !accesses.empty() && std::ranges::all_of(accesses, [&](const std::string& access) { return access == name; }); + }; + + const auto plain = compile(false, false, false); + require(!declared(plain, "buffers_coherent") && !decorated(plain, "buffers", spv::DecorationCoherent), "coherent buffers: a program without GLC or DLC accesses declares coherent buffers"); + require(only(plain.loads, "buffers") && only(plain.stores, "buffers"), "coherent buffers: a program without GLC or DLC accesses does not use the buffer array"); + + const auto store = compile(false, false, true); + require(declared(store, "buffers_coherent") && decorated(store, "buffers_coherent", spv::DecorationCoherent), "coherent buffers: a GLC store has no coherent buffer array"); + require(!decorated(store, "buffers", spv::DecorationCoherent), "coherent buffers: a GLC store makes the loads without GLC coherent"); + require(decorated(store, "buffers", spv::DecorationAliased) && decorated(store, "buffers_coherent", spv::DecorationAliased), "coherent buffers: the two buffer arrays are not decorated Aliased"); + require(only(store.loads, "buffers"), "coherent buffers: a load without GLC goes through the coherent buffer array"); + require(only(store.stores, "buffers_coherent"), "coherent buffers: a GLC store goes through the buffer array without Coherent"); + + const auto load = compile(false, true, false); + require(only(load.loads, "buffers_coherent"), "coherent buffers: a GLC load goes through the buffer array without Coherent"); + require(only(load.stores, "buffers_coherent"), "coherent buffers: a store in a program with GLC accesses goes through the buffer array without Coherent"); + + const auto invalidated = compile(true, false, true); + require(!declared(invalidated, "buffers_coherent") && decorated(invalidated, "buffers", spv::DecorationCoherent), "coherent buffers: a program that invalidates the vector caches does not keep every buffer access coherent"); + require(only(invalidated.loads, "buffers") && only(invalidated.stores, "buffers"), "coherent buffers: a program that invalidates the vector caches does not use the coherent buffer array"); +} + void verifyUnnormalizedSamplers() { using namespace ShaderRecompiler; constexpr std::uint32_t Format8888UNorm = 56; constexpr std::uint32_t Format11_11_10UInt = 34; constexpr std::uint32_t Format32Float = 22; + constexpr std::uint32_t Type1D = 8; constexpr std::uint32_t Type2D = 9; constexpr std::uint32_t Type3D = 10; constexpr std::uint32_t TypeCube = 11; @@ -1448,7 +6012,7 @@ void verifyUnnormalizedSamplers() { }; const std::array capabilities{1u, static_cast(spv::CapabilityImageGatherExtended), static_cast(spv::CapabilityMinLod)}; std::uint64_t nextAddress = 0x40000u; - const auto recompile = [&](const std::vector& code, const std::array& data, std::uint64_t address = 0u) { + const auto recompile = [&](std::span code, const std::array& data, std::uint64_t address = 0u, bool offsets = false) { RecompileRequest request{}; request.shader = {ShaderStage::Compute, address != 0u ? address : (nextAddress += 0x1000u), code, 0, {}}; request.context.waveSize = 32; @@ -1460,10 +6024,12 @@ void verifyUnnormalizedSamplers() { request.target.subgroupSize = 32; request.target.supportedCapabilities = capabilities; request.target.fragmentShaderBarycentricEnabled = false; + request.target.nonConstantImageOffsets = offsets; 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) { @@ -1475,7 +6041,7 @@ void verifyUnnormalizedSamplers() { const auto proven = [&](const RecompileResult& result, bool expected) { return flags(result, DescriptorRole::GuestSamplers) == std::vector{expected} && flags(result, DescriptorRole::GuestImages) == std::vector{expected}; }; - const auto auditSpirv = [](const std::vector& words) { + const auto auditSpirv = [](std::span words) { std::size_t samples = 0; for (std::size_t cursor = 5; cursor < words.size();) { const auto count = words[cursor] >> 16u; @@ -1497,30 +6063,110 @@ void verifyUnnormalizedSamplers() { auditSpirv(accepted.spirv.Words()); const auto plain = recompile(lz, userData(normalized), 0x40000u); require(proven(plain, false), "unnormalized samplers: a normalized S# was flagged"); - require(plain.variantId == accepted.variantId && plain.spirv.Words() == accepted.spirv.Words(), "unnormalized samplers: the unnormalized S# compiled another variant"); + require(plain.variantId == accepted.variantId && plain.spirv == accepted.spirv, "unnormalized samplers: the unnormalized S# compiled another variant"); - for (const std::uint32_t mimg : {0xf0900f08u, 0xf0800f08u, 0xf0940f08u, 0xf0840f08u}) { + for (const std::uint32_t mimg : {0xf0900f08u, 0xf0800f08u, 0xf0940f08u, 0xf0840f08u, 0xf0880f08u}) { const auto result = recompile(program(mimg), userData(unnormalized)); - require(proven(result, true), "unnormalized samplers: a compute image_sample_l, image_sample, image_sample_b or image_sample_cl was not flagged"); + require(proven(result, true), "unnormalized samplers: a compute image_sample_l, image_sample, image_sample_b, image_sample_cl or image_sample_d was not flagged"); auditSpirv(result.spirv.Words()); } const auto reject = [&](std::uint32_t mimg, const std::array& data, const char* reason, const char* what) { expectFailure([&] { static_cast(recompile(program(mimg), data)); }, reason, what); }; - reject(0xf0c00f08u, userData(unnormalized), "unnormalized guest sampler is used with a texel offset, which is not implemented", "unnormalized samplers: image_sample_o was accepted"); reject(0xf0bc0f08u, imageData(unnormalized, Type2D, Format32Float, 0u), "unnormalized guest sampler is used with depth comparison, which is not implemented", "unnormalized samplers: image_sample_c_lz was accepted"); reject(0xf11c0108u, userData(unnormalized), "unnormalized guest sampler is used by a gather, which is not implemented", "unnormalized samplers: image_gather4_lz was accepted"); reject(0xf1800308u, userData(unnormalized), "unnormalized guest sampler is used by image_get_lod, which is not implemented", "unnormalized samplers: image_get_lod was accepted"); - reject(0xf0880f08u, userData(unnormalized), "unnormalized guest sampler is used by a sample with derivatives, which is not implemented", "unnormalized samplers: image_sample_d was accepted"); reject(0xf0800f09u, userData(unnormalized), "unnormalized guest sampler is used by an image_sample_*_a variant, which is not implemented", "unnormalized samplers: image_sample_a was accepted"); + 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"); reject(0xf09c0f08u, imageData(unnormalized, Type2D, Format11_11_10UInt, 0u), "unnormalized guest sampler samples an image that needs a format conversion or packed access", "unnormalized samplers: a T# with a format conversion was accepted"); + reject(0xf15c0108u, userData(unnormalized), "unnormalized guest sampler is used by a gather, which is not implemented", "unnormalized samplers: image_gather4_lz_o was accepted"); + reject(0xf0fc0f08u, imageData(unnormalized, Type2D, Format32Float, 0u), "unnormalized guest sampler is used with depth comparison, which is not implemented", "unnormalized samplers: image_sample_c_lz_o was accepted"); + reject(0xf0dc0f28u, imageData(unnormalized, Type2DArray, Format8888UNorm, 5u), "unnormalized guest sampler samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: a 2D-array image_sample_lz_o was accepted"); for (const std::uint32_t mimg : {0xf0c00f08u, 0xf11c0108u, 0xf09c0f18u}) { static_cast(recompile(program(mimg), imageData(normalized, mimg == 0xf09c0f18u ? TypeCube : Type2D, Format8888UNorm, mimg == 0xf09c0f18u ? 5u : 0u))); } + const std::vector latchSample{0xbe900380u, 0x7e020280u, 0x7e040280u, 0x7e060280u, 0x7e080280u, 0x7e0a0280u, 0x7e0c0280u, 0xf09c0f28u, 0x00400801u, 0x80108110u, 0xbf0a8410u, 0xbf85fffbu, 0xe0700000u, 0x80030800u, 0xbf810000u}; + static_cast(recompile(latchSample, imageData(normalized, Type2DArray, Format8888UNorm, 5u))); + + const auto offsetProgram = [](std::uint32_t mimg, std::uint32_t offsetSource, std::uint32_t literal) { + std::vector code{0x7e020200u | offsetSource}; + if (offsetSource == 0xffu) code.push_back(literal); + code.insert(code.end(), {0x7e040280u, 0x7e060280u, 0x7e080280u, 0x7e0a0280u, 0x7e0c0280u, mimg, 0x00400801u, 0xe0700000u, 0x80030800u, 0xbf810000u}); + return code; + }; + const auto foldedSamples = [](std::span words) { + std::vector exact; + std::vector sums; + std::vector coordinates; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + require(count != 0 && count <= words.size() - cursor, "unnormalized texel offsets: truncated SPIR-V instruction"); + const auto op = words[cursor] & 0xffffu; + if (op == spv::OpDecorate && count == 3u && words[cursor + 2] == spv::DecorationNoContraction) exact.push_back(words[cursor + 1]); + if (op == spv::OpFAdd) sums.push_back(words[cursor + 2]); + if (op == spv::OpImageSampleExplicitLod) coordinates.push_back(words[cursor + 4]); + cursor += count; + } + return !coordinates.empty() && std::ranges::all_of(coordinates, [&](std::uint32_t id) { return std::ranges::count(sums, id) != 0 && std::ranges::count(exact, id) != 0; }); + }; + const auto sampleMasks = [](std::span words) { + std::vector masks; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + require(count != 0 && count <= words.size() - cursor, "unnormalized texel offsets: truncated SPIR-V instruction"); + if ((words[cursor] & 0xffffu) == spv::OpImageSampleExplicitLod && count > 5u) masks.push_back(words[cursor + 5]); + cursor += count; + } + return masks; + }; + for (const std::uint32_t source : {0xffu, 0x100u}) { + const auto oneD = recompile(offsetProgram(0xf0dc0f00u, source, 0x3du), imageData(unnormalized, Type1D, Format8888UNorm, 0u)); + require(proven(oneD, true), "unnormalized texel offsets: a 1D image_sample_lz_o through an unnormalized S# was not flagged"); + auditSpirv(oneD.spirv.Words()); + require(foldedSamples(oneD.spirv.Words()), "unnormalized texel offsets: a 1D offset was not added to the coordinate"); + } + const auto constantLz = offsetProgram(0xf0dc0f08u, 0xffu, 0x3du | (2u << 8u)); + const auto foldedLz = recompile(constantLz, userData(unnormalized), 0x48000u); + require(proven(foldedLz, true), "unnormalized texel offsets: image_sample_lz_o through an unnormalized S# was not flagged"); + auditSpirv(foldedLz.spirv.Words()); + require(foldedSamples(foldedLz.spirv.Words()), "unnormalized texel offsets: a constant offset was not added to the coordinates"); + const auto operandLz = recompile(constantLz, userData(normalized), 0x48000u); + require(proven(operandLz, false) && operandLz.variantId == foldedLz.variantId && operandLz.PipelineVariantId() != foldedLz.PipelineVariantId(), "unnormalized texel offsets: a normalized S# did not get its own specialization of the shared artifact"); + require(sampleMasks(operandLz.spirv.Words()) == std::vector{spv::ImageOperandsLodMask | spv::ImageOperandsConstOffsetMask} && !foldedSamples(operandLz.spirv.Words()), "unnormalized texel offsets: a normalized image_sample_lz_o lost its ConstOffset operand"); + require(recompile(constantLz, userData(unnormalized), 0x48000u).PipelineVariantId() == foldedLz.PipelineVariantId(), "unnormalized texel offsets: the unnormalized S# did not reuse its specialization"); + for (const std::uint32_t mimg : {0xf0dc0f08u, 0xf0d00f08u, 0xf0c00f08u, 0xf0d40f08u, 0xf0c40f08u}) { + const auto computed = offsetProgram(mimg, 0x100u, 0u); + for (const bool offsets : {false, true}) { + const auto result = recompile(computed, userData(unnormalized), 0u, offsets); + require(proven(result, true), "unnormalized texel offsets: a compute image_sample_lz_o, _l_o, _o, _b_o or _cl_o with a computed offset was not flagged"); + auditSpirv(result.spirv.Words()); + require(foldedSamples(result.spirv.Words()), "unnormalized texel offsets: a computed offset was not added to the coordinates"); + } + expectFailure([&] { static_cast(recompile(computed, userData(normalized))); }, "normalized guest sampler is used with a texel offset that is not a constant", "unnormalized texel offsets: a normalized computed offset was accepted without maintenance8"); + expectFailure([&] { static_cast(recompile(computed, imageData(unnormalized, Type2D, Format11_11_10UInt, 0u))); }, "takes only images an unnormalized S# can sample", "unnormalized texel offsets: a computed offset sampled a converted image without maintenance8"); + const auto masks = sampleMasks(recompile(computed, userData(normalized), 0u, true).spirv.Words()); + require(!masks.empty() && std::ranges::all_of(masks, [](std::uint32_t mask) { return (mask & spv::ImageOperandsOffsetMask) != 0u; }), "unnormalized texel offsets: a normalized computed offset lost its Offset operand"); + } + + const auto gradient = program(0xf0880f08u); + const auto gradientAddress = nextAddress += 0x1000u; + const auto gradientUnnormalized = recompile(gradient, userData(unnormalized), gradientAddress); + const auto gradientNormalized = recompile(gradient, userData(normalized), gradientAddress); + require(proven(gradientNormalized, false) && gradientNormalized.variantId == gradientUnnormalized.variantId && gradientNormalized.PipelineVariantId() != gradientUnnormalized.PipelineVariantId(), "unnormalized samplers: a normalized image_sample_d did not get its own specialization of the shared artifact"); + require(sampleMasks(gradientNormalized.spirv.Words()) == std::vector{spv::ImageOperandsGradMask}, "unnormalized samplers: a normalized image_sample_d lost its derivatives"); + require(sampleMasks(gradientUnnormalized.spirv.Words()) == std::vector{spv::ImageOperandsLodMask}, "unnormalized samplers: an unnormalized image_sample_d kept its derivatives"); const std::array pixelCode{0xf0800f08u, 0x00400801u, 0xf800180fu, 0x0b0a0908u, 0xbf810000u}; const auto pixelData = userData(unnormalized); @@ -1535,7 +6181,84 @@ void verifyUnnormalizedSamplers() { pixel.target.subgroupSize = 64; pixel.layout.pushConstantSizeBytes = 128; pixel.useCache = false; - expectFailure([&] { static_cast(Recompile(pixel)); }, "unnormalized guest sampler is used by an implicit-LOD sample, which is not implemented", "unnormalized samplers: a pixel image_sample was accepted"); + const auto implicitSamples = [](std::span words) { + std::size_t samples = 0; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + require(count != 0 && count <= words.size() - cursor, "unnormalized samplers: truncated SPIR-V instruction"); + if ((words[cursor] & 0xffffu) == spv::OpImageSampleImplicitLod) ++samples; + cursor += count; + } + return samples; + }; + const auto pixelUnnormalized = Recompile(pixel); + require(proven(pixelUnnormalized, true), "unnormalized samplers: a pixel image_sample through an unnormalized S# was not flagged"); + auditSpirv(pixelUnnormalized.spirv.Words()); + const auto normalizedPixelData = userData(normalized); + pixel.context.userData = normalizedPixelData; + const auto pixelNormalized = Recompile(pixel); + require(proven(pixelNormalized, false) && implicitSamples(pixelNormalized.spirv.Words()) == 1u, "unnormalized samplers: a pixel image_sample through a normalized S# is not an implicit-LOD sample"); + 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() { @@ -1559,10 +6282,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); @@ -1572,9 +6306,70 @@ void verifyUnusedUnnormalizedSampler() { auto compared = info; compared.samplers[0].depthCompare = true; expectFailure([&] { static_cast(populate(compared)); }, "unnormalized guest sampler is used with depth comparison, which is not implemented", "unnormalized samplers: a depth-compare S# without live uses was accepted"); + auto offset = info; + offset.samplers[0].uses = SamplerUseExplicitLod | SamplerUseOffset; + const auto folded = populate(offset); + require(folded[0].imageUnnormalized == std::vector{true} && folded[1].samplerUnnormalized == std::vector{true}, "unnormalized samplers: an offset use was not flagged"); + auto gathered = offset; + gathered.samplers[0].uses |= SamplerUseGather; + expectFailure([&] { static_cast(populate(gathered)); }, "unnormalized guest sampler is used by a gather, which is not implemented", "unnormalized samplers: an offset gather was accepted"); + auto foldOnly = offset; + foldOnly.samplers[0].foldTexelOffsetsOnly = true; + static_cast(populate(foldOnly)); + auto foldVolume = foldOnly; + foldVolume.images[0].dimension = RdnaImageDimension::Dim3D; + foldVolume.images[0].foldTexelOffsetsOnly = true; + snapshot.images[0].dwords[3] = 0xa0000041u; + const auto foldedConstant = populate(foldVolume); + require(foldedConstant.size() == 1u && foldedConstant[0].samplerUnnormalized == std::vector{true}, "unnormalized texel offsets: a 3D view whose channels select constants was bound or refused"); + snapshot.images[0].dwords[3] = 0xa0000facu; + expectFailure([&] { static_cast(populate(foldVolume)); }, "takes only images an unnormalized S# can sample", "unnormalized texel offsets: an ordinary 3D view matched a constant-only mode"); + snapshot.images[0].dwords[3] = 0x90000facu; + const auto atBaseLevel = [&](const ShaderInfo& shader) { return populate(shader)[0].imageUnnormalizedBaseLevel; }; + auto levelZero = info; + levelZero.samplers[0].uses = SamplerUseExplicitLod; + require(atBaseLevel(levelZero) == std::vector{true}, "unnormalized samplers: an image read only by image_sample_lz was not flagged as exact at its base level"); + require(atBaseLevel(info) == std::vector{false}, "unnormalized samplers: an image without live uses was flagged as exact at its base level"); + require(atBaseLevel(offset) == std::vector{false}, "unnormalized samplers: an image read with a texel offset was flagged as exact at its base level"); + auto selectedLod = levelZero; + selectedLod.samplers[0].uses |= SamplerUseNonZeroLod; + require(atBaseLevel(selectedLod) == std::vector{false}, "unnormalized samplers: an image also read at a selected LOD was flagged as exact at its base level"); + snapshot.samplers[0].dwords[1] = 0x00fff100u; + require(atBaseLevel(levelZero) == std::vector{false}, "unnormalized samplers: an S# with a minimum LOD was flagged as exact at the base level"); + snapshot.samplers[0].dwords[1] = 0x00fff000u; + 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() { @@ -1657,7 +6452,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();) { @@ -1680,10 +6476,10 @@ void verifyBdaReadFallbackFunctions() { alignas(256) static std::array output{}; const auto outputAddress = static_cast(reinterpret_cast(output.data())); const std::array userData{0x10000000u, 0u, 0u, 0u, static_cast(outputAddress), static_cast((outputAddress >> 32u) & 0xffffu), 128u, 0x01016facu}; - const auto compile = [&](std::uint32_t loads, bool barrier, bool coherent) { + const auto compile = [&](std::uint32_t loads, bool barrier, bool coherent, std::uint32_t stride) { std::vector code{0x7e020200u, 0x7e040201u}; for (std::uint32_t load = 0; load < loads; ++load) { - code.push_back(0xdc308000u | (coherent ? 0x10000u : 0u) | (4u * load + 4u)); + code.push_back(0xdc308000u | (coherent ? 0x10000u : 0u) | (stride * load + 4u)); code.push_back(((3u + load) << 24u) | 0x007d0001u); } code.push_back(0xbf8c3f70u); @@ -1709,18 +6505,27 @@ void verifyBdaReadFallbackFunctions() { }; for (const bool barrier : {false, true}) { for (const bool coherent : {false, true}) { - const auto one = compile(1u, barrier, coherent); - const auto five = compile(5u, barrier, coherent); + const auto one = compile(1u, barrier, coherent, 32u); + const auto five = compile(5u, barrier, coherent, 32u); + const auto adjacent = compile(5u, barrier, coherent, 4u); std::map names; std::map definitions; const auto reader = std::string("read_bda_dword_bytes") + (barrier ? "" : "_stop") + (coherent ? "_coherent" : ""); const auto span = std::string("read_bda_span") + (barrier ? "" : "_stop") + (coherent ? "_coherent" : ""); + const auto loader = std::string("load_bda_span") + (coherent ? "_coherent" : ""); std::string function; std::size_t compareExchanges = 0; std::size_t mainLookups = 0; std::size_t mainProbes = 0; std::size_t spanCalls = 0; + std::size_t spanLoads = 0; + std::size_t spanLoaderCalls = 0; + std::size_t loaderLoads = 0; + std::size_t loaderCalls = 0; std::array readerLoads{}; + std::size_t lookupFaults = 0; + std::size_t lookupVariables = 0; + std::size_t readerLookups = 0; for (std::size_t cursor = 5; cursor < five.size();) { const auto length = five[cursor] >> 16u; require(length != 0 && length <= five.size() - cursor, "BDA read functions: truncated SPIR-V instruction"); @@ -1728,7 +6533,7 @@ void verifyBdaReadFallbackFunctions() { if (op == spv::OpName) names[five[cursor + 1]] = reinterpret_cast(&five[cursor + 2]); if (op == spv::OpFunction) { function = names[five[cursor + 2]]; - if (function == "record_bda_fault" || function.starts_with("read_bda_dword_bytes") || function.starts_with("read_bda_span")) { + if (function == "record_bda_fault" || function.starts_with("read_bda_dword_bytes") || function.starts_with("read_bda_span") || function.starts_with("load_bda_span")) { require((five[cursor + 3] & spv::FunctionControlDontInlineMask) != 0u, "BDA read functions: a fault, byte read or span read function may be inlined"); ++definitions[function]; } @@ -1740,21 +6545,142 @@ void verifyBdaReadFallbackFunctions() { if (callee == "probe_bda_pointer" || callee.starts_with("read_bda_dword_bytes")) ++mainProbes; if (callee == span) ++spanCalls; } + if ((op == spv::OpLoad || op == spv::OpAtomicLoad) && function == span) ++spanLoads; + if (op == spv::OpFunctionCall && function == span && names[five[cursor + 3]] == loader) ++spanLoaderCalls; + if ((op == spv::OpLoad || op == spv::OpAtomicLoad) && function == loader) ++loaderLoads; + if (op == spv::OpFunctionCall && function == loader) ++loaderCalls; if (op == spv::OpLoad && function == reader) ++readerLoads[length > 4u && (five[cursor + 4] & spv::MemoryAccessVolatileMask) != 0u]; + if (op == spv::OpFunctionCall && function == "get_bda_pointer" && names[five[cursor + 3]] == "record_bda_fault") ++lookupFaults; + if (op == spv::OpVariable && function == "get_bda_pointer") ++lookupVariables; + if (op == spv::OpFunctionCall && function == reader && names[five[cursor + 3]] == "get_bda_pointer") ++readerLookups; cursor += length; } require(definitions["record_bda_fault"] == 1u && definitions[reader] == 1u && definitions[span] == 1u, "BDA read functions: the fault, byte read and span read functions are not defined"); + require(definitions[loader] == 1u && loaderLoads == 4u && loaderCalls == 0u, "BDA read functions: the span load function does not hold the four in-place loads and nothing else"); + require(spanLoads == 0u && spanLoaderCalls == 1u, "BDA read functions: a span read function loads in place itself"); for (const auto& [name, count] : definitions) require(count == 1u, "BDA read functions: a function is defined twice"); require(compareExchanges == 1u, "BDA read functions: a fault is recorded outside record_bda_fault"); require(mainLookups == 0u, "BDA read functions: a read site looks up its bytes inline"); - require(readerLoads[coherent] == 4u && readerLoads[!coherent] == 0u, "BDA read functions: the byte loads do not keep the access's coherence"); + require(readerLoads[coherent] == 1u && readerLoads[!coherent] == 0u, "BDA read functions: the byte loads do not keep the access's coherence"); + require(lookupFaults <= 4u, "BDA read functions: the page-table lookup records its faults from more than 4 call sites"); + require(lookupVariables == 0u, "BDA read functions: the page-table lookup keeps its search bounds in function variables"); + require(readerLookups == 1u, "BDA read functions: the byte read does not look up its bytes once, in a loop"); require(mainProbes == 0u, "BDA read functions: a read site probes or reads bytes outside its span read function"); require(spanCalls == 5u, "BDA read functions: a read site does not call its span read function once"); require(five.size() - one.size() < 4u * 150u, "BDA read functions: a read site takes 150 SPIR-V words or more"); + std::size_t adjacentSpanCalls = 0; + names.clear(); + function.clear(); + for (std::size_t cursor = 5; cursor < adjacent.size();) { + const auto length = adjacent[cursor] >> 16u; + require(length != 0 && length <= adjacent.size() - cursor, "BDA read functions: truncated SPIR-V instruction"); + const auto op = adjacent[cursor] & 0xffffu; + if (op == spv::OpName) names[adjacent[cursor + 1]] = reinterpret_cast(&adjacent[cursor + 2]); + if (op == spv::OpFunction) function = names[adjacent[cursor + 2]]; + if (op == spv::OpFunctionCall && function == "main" && names[adjacent[cursor + 3]] == span) ++adjacentSpanCalls; + cursor += length; + } + require(adjacentSpanCalls == 2u, "BDA read functions: five adjacent dwords of one address do not share two span reads"); } } } +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) { @@ -1865,6 +6791,13 @@ void verifyFunctionLdsBound() { }); require(unbounded == FunctionLdsDwordLimit && unboundedRequirements.functionLdsDwords == FunctionLdsDwordLimit, "function LDS: an address without a bound must keep the full array"); + const auto [counter, counterRequirements] = build([](IrProgram&, IrBlock&, auto& emit, auto& constant, IrValue& active) { + auto& m0 = emit(IrOpcode::GetUserData, IrType::U32, {&constant(0u)}); + emit(IrOpcode::DataAppend, IrType::U32, {&m0, &active}, 2u); + emit(IrOpcode::DataConsume, IrType::U32, {&m0, &active}, 1u); + }); + require(counter == 192u && counterRequirements.functionLdsDwords == 192u, "function LDS: an LDS append or consume must size the array by its offset, not by M0"); + const auto wrapping = build([](IrProgram&, IrBlock&, auto& emit, auto& constant, IrValue& active) { emit(IrOpcode::LoadSharedU32, IrType::U32, {&constant(0x20u), &active}, 3u); }).first; @@ -1895,6 +6828,115 @@ void verifyFunctionLdsBound() { require(unsized == FunctionLdsDwordLimit, "function LDS: an access without a known width must keep the full array"); } +void verifyFunctionLdsUpdates() { + using namespace ShaderRecompiler; + static constexpr std::array capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityClipDistance}; + const auto compile = [](ShaderStage stage, std::uint32_t waveSize, std::span body) { + std::vector code{0x7e000280u, 0x7e020292u, 0xbefc03ffu, 0x0000ffffu}; + code.insert(code.end(), body.begin(), body.end()); + code.insert(code.end(), {0xbf8cc07fu, 0xd8d80000u, 0x02000000u, 0xbf8cc07fu, stage == ShaderStage::Fragment ? 0xf800180fu : 0xf80008cfu, 0x02020202u, 0xbf810000u}); + RecompileRequest request{}; + request.shader = {stage, 0x40000u, code, 0, {}}; + request.context.waveSize = waveSize; + if (stage == ShaderStage::Fragment) { + ShaderPixelStageInfo info{}; + info.wave32 = waveSize == 32u; + info.inputAddr = PixelInputBit(PixelInput::PerspectiveCenter); + info.hasPerspectiveCenterVgpr = true; + info.targetOutputMode[0] = 9u; + info.targetExportMapping.fill(0xe4u); + request.context.pixel = info; + } else { + request.context.vertex = ShaderVertexStageInfo{}; + } + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32u; + request.target.supportedCapabilities = capabilities; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + return Recompile(request); + }; + const auto violations = [](std::span words) { + std::map storage; + std::map constants; + std::map types; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + const auto op = static_cast(words[at] & 0xffffu); + if (op == spv::OpTypePointer) storage[words[at + 1]] = words[at + 2]; + if (op == spv::OpConstant && (words[at] >> 16u) == 4u) constants[words[at + 2]] = words[at + 3]; + if (op == spv::OpVariable || op == spv::OpAccessChain || op == spv::OpInBoundsAccessChain) types[words[at + 2]] = words[at + 1]; + } + const auto workgroupScope = [&](std::uint32_t id) { return constants.count(id) != 0 && constants.at(id) == spv::ScopeWorkgroup; }; + std::size_t found = 0; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + const auto op = static_cast(words[at] & 0xffffu); + if (op == spv::OpMemoryBarrier && workgroupScope(words[at + 1])) ++found; + if ((op < spv::OpAtomicLoad || op > spv::OpAtomicXor) && op != spv::OpAtomicFMinEXT && op != spv::OpAtomicFMaxEXT && op != spv::OpAtomicFAddEXT) continue; + const auto pointer = words[at + (op == spv::OpAtomicStore ? 1u : 3u)]; + const auto type = types.find(pointer); + if (type != types.end() && storage.count(type->second) != 0 && storage.at(type->second) == spv::StorageClassFunction) ++found; + if (workgroupScope(words[at + (op == spv::OpAtomicStore ? 2u : 4u)])) ++found; + } + return found; + }; + using Program = std::pair>; + static constexpr std::array programs{{ + {"ds_write_b8", {0xd8780000u, 0x00000100u}}, + {"ds_write_b16", {0xd87c0000u, 0x00000100u}}, + {"ds_write_b8_d16_hi", {0xda800000u, 0x00000100u}}, + {"ds_write_b16_d16_hi", {0xda840000u, 0x00000100u}}, + {"ds_add_u32", {0xd8000000u, 0x00000100u}}, + {"ds_inc_rtn_u32", {0xd88c0000u, 0x03000100u}}, + {"ds_mskor_b32", {0xd8300000u, 0x00010100u}}, + {"ds_append", {0xd8f80000u, 0x03000000u}}, + }}; + for (const auto& [name, body] : programs) { + for (const auto stage : {ShaderStage::Fragment, ShaderStage::Vertex}) { + for (const auto waveSize : {64u, 32u}) { + const auto what = std::string("function LDS: ") + name + (stage == ShaderStage::Fragment ? " in a pixel" : " in a vertex") + " wave" + std::to_string(waveSize) + " program"; + const auto result = compile(stage, waveSize, body); + require(!result.spirv.empty(), (what + " did not compile").c_str()); + require(violations(result.spirv.Words()) == 0, (what + " uses an atomic on a Function pointer or a Workgroup memory scope").c_str()); + } + } + } +} + +void verifySrtReadPatching() { + using namespace ShaderRecompiler; + IrProgram program; + program.Resources().stage = IrShaderStage::Compute; + MemoryInfo memory; + memory.kind = ResourceKind::ScalarAddress; + program.Resources().memoryInfo.push_back(memory); + + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + IrBuilder builder(program); + builder.SetInsertionPoint(block); + auto& address = builder.Emit(IrOpcode::GetAddressResource, IrType::AddressResource, {&builder.Constant(0u), &builder.Constant(0u)}); + auto& active = program.CreateValue(IrOpcode::Void, IrType::U1); + active.SetImmediateBool(true); + auto& read = builder.Emit(IrOpcode::LoadAddressU32, IrType::U32, {&address, &builder.Constant(0u), &builder.Constant(0u), &active}, MemoryFlags{0u, 0u}); + auto& repeatedUse = builder.Emit(IrOpcode::IAdd32, IrType::U32, {&read, &read}); + auto& secondUser = builder.Emit(IrOpcode::IMul32, IrType::U32, {&read, &builder.Constant(2u)}); + + SrtWalker{}.BuildPlan(program); + + require(program.Resources().srtReads.size() == 1u, "SRT read patching: the scalar read was not planned"); + 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"); +} + struct NestedRequest { std::vector code; std::array userData{}; @@ -1952,6 +6994,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; @@ -1989,6 +7186,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); @@ -1996,9 +7215,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}) { @@ -2008,16 +7230,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)); @@ -2038,22 +7260,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) { @@ -2074,7 +7301,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); @@ -2105,6 +7333,7 @@ void verifyGuardedNullPointers() { const auto tableAddress = static_cast(reinterpret_cast(table.data())); vertexRoot[2] = static_cast(tableAddress); vertexRoot[3] = static_cast(tableAddress >> 32u); + vertexRoot[0] = 1u; AgcDriver::ShaderMemory mappedVertexMemory({}); const auto mappedVertex = mappedVertexMemory.Capture(vertex.request); require(mappedVertex->snapshot.srtPoison.empty() && mappedVertex->snapshot.buffers.size() == 1u && mappedVertex->snapshot.buffers[0].dwords[0] == table[4], "guarded pointer: a vertex V# behind a mapped pointer was poisoned or not read"); @@ -2115,18 +7344,24 @@ 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 } NestedRequest rootless(branchCode, nullptr); AgcDriver::ShaderMemory rootlessMemory({}); - expectFailure([&] { static_cast(rootlessMemory.Capture(rootless.request)); }, "null or misaligned address", "guarded pointer: a null user-data pointer was accepted"); + 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"); + 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); #ifdef _WIN32 VirtualFree(reserved, 0, MEM_RELEASE); @@ -2135,26 +7370,337 @@ 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{}; + image.resourceClass = ImageResourceClass::Sampled; + image.dimension = RdnaImageDimension::Dim2DArray; + image.depthCompare = true; + const DescriptorValue descriptor{{0x02143930u, 0x08200000u, 0u, 0xb0500facu, 5u, 0x00700000u, 0u, 0u}, 8u}; + const auto modes = ResourceMaterializer::RuntimeImageModes(image); + const auto selected = ResourceMaterializer::RuntimeImageMode(image, descriptor, modes); + require(modes[selected].cube && (modes[selected].emulatedCompare & EmulatedCompare::Enabled) != 0u, "cube color comparison did not select its emulated runtime mode"); +} + +void verifyVolumeArrayLoadMode() { + using namespace ShaderRecompiler; + const DescriptorValue descriptor{{0x02466600u, 0x01400000u, 0u, 0xa0500204u, 1u, 0x00700000u, 0u, 0u}, 8u}; + ImageResource image{}; + image.resourceClass = ImageResourceClass::Sampled; + image.dimension = RdnaImageDimension::Dim2DArray; + const auto modes = ResourceMaterializer::RuntimeImageModes(image); + const auto selected = ResourceMaterializer::RuntimeImageMode(image, descriptor, modes); + require(modes[selected].dimension == RdnaImageDimension::Dim3D && !modes[selected].cube, "2D-array image load on a 3D T# did not select a volume runtime mode"); + image.fmaskCompatible = false; + expectFailure([&] { static_cast(ResourceMaterializer::RuntimeImageMode(image, descriptor, ResourceMaterializer::RuntimeImageModes(image))); }, "incompatible with the static runtime image interface", "2D-array image sample on a 3D T# was accepted"); + 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; + store.written = true; + store.fmaskCompatible = false; + const auto storeModes = ResourceMaterializer::RuntimeImageModes(store); + require(storeModes[ResourceMaterializer::RuntimeImageMode(store, descriptor, storeModes)].dimension == RdnaImageDimension::Dim3D, "2D-array image store on a 3D T# did not select a volume runtime mode"); + store.atomic = true; + expectFailure([&] { static_cast(ResourceMaterializer::RuntimeImageMode(store, descriptor, ResourceMaterializer::RuntimeImageModes(store))); }, "incompatible with the static runtime image interface", "2D-array image atomic on a 3D T# was accepted"); +} + +void 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 verifySrgbDepthCompareModes() { + using namespace ShaderRecompiler; + for (const auto decodeFormats : {0u, 1u, 2u, 3u}) { + ImageResource image{}; + image.resourceClass = ImageResourceClass::Sampled; + image.dimension = RdnaImageDimension::Dim2D; + image.depthCompare = true; + image.srgbDecodeCompatible = false; + image.srgbDecodeFormats = decodeFormats; + const auto modes = ResourceMaterializer::RuntimeImageModes(image); + for (const auto format : {128u, 129u, 130u}) { + const DescriptorValue descriptor{{0x100u, format << 20u, 0u, 0x90000facu, 0u, 0u, 0u, 0u}, 8u}; + const auto selected = ResourceMaterializer::RuntimeImageMode(image, descriptor, modes); + require(!modes[selected].srgbDecode && (modes[selected].emulatedCompare & EmulatedCompare::Enabled) != 0u, "sRGB color comparison did not select its emulated runtime mode independently of decode support"); + } + } +} + 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"; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--layout-pair") { + verifyLayoutPair(); + std::cout << "Both push-constant layouts of a pixel shader compile from one program unchanged\n"; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--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(); + 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") { + verifySrtReadPatching(); + 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(); - verifyBindlessTable(); + verifyGuardedDescriptorSources(); + verifySrtTape(); + verifyImageTableKeys(); + verifyImageTables(); + verifyImageTableRegistration(); + verifyScalarAddressRule(); + verifyPointerImageTable(); + verifyPointerTableOffsetCarry(); + verifyChainedTableOffset(); + verifyPointerChainImageTable(); + verifyWaterfallImageTable(); verifyDescriptorPhis(); + verifyUniformTableImage(); + verifyLaneSelectedDescriptors(); verifyProgramCounterRelativeData(); verifyLanesOutsideHostSubgroup(); verifyHalfWaveReduction(); + verifyNarrowVertexWave(); + verifySingleLaneVertexWave(); verifyMeshConfiguration(); verifyPixelInputs(); + verifyCanonicalVersion20And21Requests(); verifyPixelRequestSerialization(); + verifyVertexInfoVersion19(); + verifyDepthRemapRequests(); + verifyMergedHullConfiguration(); + verifyPositionExportComponents(); verifyLegacyPixelRequests(); verifyPixelExportReplay(); verifyPixelParameterSlots(); @@ -2162,13 +7708,28 @@ int main(int argc, char** argv) { verifyComputedTexelOffsets(); verifyShaderClockScopes(); verifyInt64AtomicCapabilities(); + verifySpecializedBufferAtomicBindings(); + verifyCoherentBufferAccesses(); verifyUnnormalizedSamplers(); verifyUnusedUnnormalizedSampler(); + verifyNullVolumeUnnormalizedSampler(); verifyWaveUniformValues(); verifyTwoLaneUniformValues(); verifyBdaReadFallbackFunctions(); + verifyBdaAnchors(); verifyFunctionLdsBound(); + verifyFunctionLdsUpdates(); + verifySrtReadPatching(); + verifyGuardedDirectRoot(); verifyGuardedNullPointers(); + verifyFmaskImageMode(); + verifyCubeDepthCompareMode(); + verifyVolumeArrayLoadMode(); + verifyVolumeBiasTableRejection(); + verifyDynamicStorageMipChain(); + verifySrgbDepthCompareModes(); + verifyQuadMaskNeighbourReads(); + verifyCoherentGds(); #if ANYPS5_ENABLE_SPIRV_TOOLS const std::vector minimalSpirv{ 0x07230203u, 0x00010000u, 0u, 5u, 0u, @@ -2269,6 +7830,35 @@ int main(int argc, char** argv) { auto narrowClock = uncached; narrowClock.target.narrowSubgroupClock = true; require(RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(narrowClock)).request.target.narrowSubgroupClock, "the narrow subgroup clock was lost in serialization"); + auto float64Denormals = uncached; + float64Denormals.target.float64DenormPreserve = true; + require(RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(float64Denormals)).request.target.float64DenormPreserve, "f64 denormal preservation was lost in serialization"); + auto registrationModules = uncached; + registrationModules.target.registrationModules = true; + registrationModules.target.registrationWordLimit = 1234u; + const auto roundTrip = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(registrationModules)).request.target; + require(roundTrip.registrationModules && roundTrip.registrationWordLimit == 1234u, "registration modules were lost in serialization"); + std::vector plainKey; + std::vector registrationKey; + RecompileCacheKey::Build(uncached, plainKey); + RecompileCacheKey::Build(registrationModules, registrationKey); + require(plainKey != registrationKey, "registration modules did not change the cache key"); + auto float16Denormals = uncached; + float16Denormals.target.float16DenormPreserve = true; + require(RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(float16Denormals)).request.target.float16DenormPreserve, "f16 denormal preservation was lost in serialization"); + auto 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"); @@ -2284,6 +7874,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); @@ -2305,7 +7909,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; @@ -2365,8 +7970,12 @@ int main(int argc, char** argv) { request.context.memory = {}; const std::array nullUserData{}; request.context.userData = nullUserData; - AgcDriver::ShaderMemory invalid({}); - expectFailure([&] { invalid.Capture(request); }, "null or misaligned address", "null user-data pointer was accepted"); + AgcDriver::ShaderMemory nullRoot({}); + const auto nullRootCapture = nullRoot.Capture(request); + require(nullRootCapture->snapshot.nullRootReads != 0u, "reads through a null user-data pointer were not counted"); + 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; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderModeRegisters.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderModeRegisters.cpp new file mode 100644 index 0000000000..534787fa76 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderModeRegisters.cpp @@ -0,0 +1,332 @@ +#include "ControlFlow/GraphBuilder.hpp" +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" +#include "Recompiler.hpp" +#include "Translation/InstructionTranslator.hpp" +#include "BdaAbi.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); +} + +IrProgram translate(std::span code, std::optional mode, std::uint32_t waveSize = 32) { + const auto decoded = RdnaInstructionDecoder{}.Decode(code); + const auto graph = GraphBuilder{}.Build(decoded); + const ShaderComputeInputInfo compute{}; + TranslateOptions options; + options.stage = ShaderStageKind::Compute; + options.waveSize = waveSize; + options.floatMode = mode; + options.inputInfo.compute = &compute; + return InstructionTranslator{}.Translate(decoded, graph, options); +} + +void expectRegister(const IrProgram& program, std::uint32_t index, std::uint32_t expected) { + const IrValue* value = nullptr; + for (const auto& block : program.Blocks()) { + for (const auto* instruction : block->Instructions()) { + if (instruction->Opcode() != IrOpcode::SetScalarRegister) continue; + const auto reg = instruction->Argument(0)->Register(); + if (reg.bank == RegisterBank::Scalar && reg.index == index) value = instruction->Argument(1); + } + } + require(value && value->HasImmediate() && value->ImmediateU32() == expected, + "MODE read into s" + std::to_string(index) + " did not produce " + std::to_string(expected)); +} + +void expectRejected(std::span code, std::optional mode, std::string_view diagnostic, std::uint32_t pc = 0) { + try { + static_cast(translate(code, mode)); + } catch (const std::runtime_error& error) { + const std::string message = error.what(); + require(message.find(diagnostic) != std::string::npos && message.find("at pc " + std::to_string(pc) + " ") != std::string::npos, + "unexpected MODE diagnostic: " + message); + return; + } + throw std::runtime_error("unsupported MODE operation was accepted: " + std::string(diagnostic)); +} + +void verifyReads() { + constexpr std::array code{ + 0xb9140001, 0xb9150801, 0xb9161001, 0xb9171801, 0xb9180041, + 0xb9190841, 0xb91a1041, 0xb91b0081, 0xb91c0881, 0xb91d00c1, 0xbf810000 + }; + constexpr std::array offsets{0, 0, 0, 0, 1, 1, 1, 2, 2, 3}; + constexpr std::array masks{1, 3, 7, 15, 1, 3, 7, 1, 3, 1}; + for (const auto waveSize : {32u, 64u}) { + for (std::uint32_t mode = 0; mode < 256; ++mode) { + const auto program = translate(code, ShaderFloatMode{mode, true, true, true}, waveSize); + for (std::uint32_t index = 0; index < offsets.size(); ++index) { + expectRegister(program, 20 + index, (mode >> offsets[index]) & masks[index]); + } + } + const auto legacy = translate(code, std::nullopt, waveSize); + for (std::uint32_t index = 0; index < offsets.size(); ++index) expectRegister(legacy, 20 + index, 0); + } +} + +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}) { + 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}; + constexpr std::array offsets{0, 0, 2, 1}; + 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, getregMode, endProgram}; + const ShaderFloatMode mode{0xc0u | 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, endProgram}; + static_cast(translate(full, std::nullopt)); + 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{roundMode(upper | value), getregMode, endProgram}; + expectRegister(translate(code, ShaderFloatMode{0xc0u | rounding}), 20, value); + } + } + } + constexpr std::array code{0xbfa40000, endProgram}; + static_cast(translate(code, std::nullopt)); + 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, endProgram}; + expectRejected(code, ShaderFloatMode{0xc0}, "s_getreg_b32"); + } +} + +void verifyBranch() { + for (std::uint32_t rounding = 0; rounding < 16; ++rounding) { + const ShaderFloatMode mode{0xc0u | rounding}; + std::array code{0xbf840002, 0xba801801, rounding, getregMode, endProgram}; + expectRegister(translate(code, mode), 20, rounding); + code[2] ^= 1u; + 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}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x20000, code, 0, {}}; + request.context.waveSize = 64; + request.context.compute = ShaderComputeStageInfo{{1, 1, 1}, 0, {false, false, false}, false, 0}; + request.context.floatMode = ShaderFloatMode{0xc5}; + request.target.vulkanVersion = 0x00403000; + request.target.spirvVersion = 0x00010600; + request.target.subgroupSize = 64; + request.target.bdaAbiVersion = BdaAbi::Version; + request.target.supportedCapabilities = capabilities; + request.target.maxWorkgroupSize = {1024, 1024, 64}; + request.target.maxWorkgroupInvocations = 1024; + request.layout = {0, 0, 0, 128}; + request.useCache = false; + require(!Recompile(request).spirv.empty(), "declared MODE did not reach full recompilation"); + request.context.floatMode.reset(); + try { + static_cast(Recompile(request)); + } catch (const std::runtime_error& error) { + require(std::string(error.what()).find("s_round_mode") != std::string::npos, "unexpected full recompilation failure"); + return; + } + throw std::runtime_error("full recompilation ignored a runtime MODE change"); +} + +} + +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 : checks) { + try { + check.run(); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s: %s\n", check.name, error.what()); + ++failures; + } + } + if (failures != 0) return 1; + std::puts("Shader MODE register tests passed"); + return 0; +} 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/SharedWrites.cpp b/core/libs/prx/libSceAgcDriver/tests/SharedWrites.cpp index 895cbc8de8..f1fe06c422 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SharedWrites.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SharedWrites.cpp @@ -118,6 +118,73 @@ void smallStores() { require(StoreOwnBytes(0, std::span{}) == 0, "empty store produced a version"); } +void partialCollect() { + constexpr std::size_t page = 4096; + Mapping mapping(page * 4); + auto* data = static_cast(mapping.data); +#ifdef _WIN32 + void* alias = nullptr; + require(sceKernelMapDirectMemory(&alias, mapping.size, 3, 0, mapping.physical, 65536) == 0, "partial collect alias failed"); +#endif + require(WriteWatched(), "write tracking unavailable for partial collect"); + const auto before = CollectWritesUncached(mapping.Address(), mapping.size); + require(before != 0, "partial collect range is not watched"); + data[page * 3] = std::byte{91}; + require(CollectWritesUncached(mapping.Address(), page) != 0, "prefix collect failed"); + require(!UnchangedSinceCollected(mapping.Address() + page * 3, page, before), "partial collect lost a neighboring guest write"); + const auto settled = CollectWritesUncached(mapping.Address(), mapping.size); + require(UnchangedSinceCollected(mapping.Address(), mapping.size, settled), "fully collected shared range remained dirty"); +#ifdef _WIN32 + const auto collect = [&](void* view, std::size_t offset, std::size_t bytes, std::size_t capacity, bool clear) { + std::array pages{}; + std::size_t count = capacity; + const auto address = reinterpret_cast(view); + require(GuestArena::GuestArenaCollectWrites_nid_postfix(address + offset, bytes, pages.data(), &count, clear), "shared subpage collection failed"); + std::vector offsets; + for (std::size_t i = 0; i < count; ++i) offsets.push_back(reinterpret_cast(pages[i]) - address); + return offsets; + }; + const std::vector none; + collect(alias, 0, mapping.size, 4, true); + data[page * 3] = std::byte{92}; + require(collect(mapping.data, 0, page * 3, 4, false) == none, "a shared write was reported for the pages next to it"); + require(collect(mapping.data, 0, mapping.size, 4, false) == std::vector{page * 3}, "probe missed a shared write"); + require(collect(mapping.data, 0, mapping.size, 4, false) == std::vector{page * 3}, "probe consumed a shared write"); + require(collect(mapping.data, 0, page, 4, true) == none, "prefix reset reported an unwritten page"); + require(collect(alias, 0, mapping.size, 4, false) == std::vector{page * 3}, "prefix reset consumed an alias write"); + require(collect(mapping.data, page * 3, page, 4, true) == std::vector{page * 3}, "prefix reset consumed a neighboring write"); + require(collect(mapping.data, page * 3, page, 4, false).empty(), "reset subpage remained dirty"); + require(collect(mapping.data, 0, mapping.size, 4, false).empty(), "collected subpages remained dirty"); + require(collect(alias, 0, mapping.size, 4, true) == std::vector{page * 3}, "collecting one view consumed an alias write"); + static_cast(alias)[page] = std::byte{93}; + require(collect(mapping.data, page * 2, page, 4, true) == none, "an alias write was reported for another page"); + require(collect(mapping.data, 0, mapping.size, 4, true) == std::vector{page}, "alias write did not invalidate the other view"); + require(collect(mapping.data, 0, mapping.size, 4, false).empty(), "full collect left pending subpages"); + require(collect(alias, 0, mapping.size, 4, true) == std::vector{page}, "alias write was consumed for its own view"); + for (std::size_t at = 0; at < mapping.size; at += page) static_cast(alias)[at + 8] = std::byte{99}; + require(collect(mapping.data, page, page * 2, 4, true) == std::vector{page, page * 2}, "partial resets consumed intervening writes"); + require(collect(mapping.data, page, page * 2, 4, false).empty(), "interior reset left its subpages dirty"); + require(collect(mapping.data, 0, mapping.size, 4, true) == std::vector{0, page * 3}, "full collect after an interior reset did not preserve pending subpages"); + require(collect(alias, 0, mapping.size, 4, true) == std::vector{0, page, page * 2, page * 3}, "an interior reset consumed the alias's writes"); + static_cast(alias)[0] = std::byte{94}; + static_cast(alias)[page * 2] = std::byte{95}; + static_cast(alias)[page * 3] = std::byte{96}; + require(collect(mapping.data, 0, mapping.size, 1, true) == std::vector{0}, "limited collect returned the wrong prefix"); + require(collect(mapping.data, 0, mapping.size, 4, false) == std::vector{page * 2, page * 3}, "limited collect consumed unreported writes"); + require(collect(mapping.data, page, page * 3, 1, true) == std::vector{page * 2}, "limited collect continuation lost writes"); + require(collect(mapping.data, page * 3, page, 4, true) == std::vector{page * 3}, "limited collect consumed a write after its last report"); + require(collect(mapping.data, 0, mapping.size, 4, false).empty(), "limited collection did not settle"); + require(collect(alias, 0, mapping.size, 4, true) == std::vector{0, page * 2, page * 3}, "limited collection consumed the alias's writes"); + data[page] = std::byte{97}; + data[page * 2] = std::byte{98}; + require(collect(alias, 0, mapping.size, 4, true) == std::vector{page, page * 2}, "writes to two pages were not reported apart"); + require(collect(mapping.data, 0, mapping.size, 4, true) == std::vector{page, page * 2}, "writes to two pages were not reported for their own view"); + require(collect(mapping.data, 0, mapping.size, 4, false).empty() && collect(alias, 0, mapping.size, 4, false).empty(), "page tracking did not settle"); + require(sceKernelMunmap(alias, mapping.size) == 0, "partial collect alias unmap failed"); +#endif + std::cout << "Partial shared write collection tests passed\n"; +} + template void fails(TAction&& fn) { bool failed = false; try { fn(); } catch (const std::runtime_error&) { failed = true; } @@ -208,7 +275,14 @@ void run() { int main(int argc, char** argv) { try { - if (argc == 2 && std::string_view(argv[1]) == "--benchmark") benchmark(); + if (argc == 2 && std::string_view(argv[1]) == "--partial-collect") { + if (!WriteWatched()) { + std::cout << "Partial shared write collection skipped: write tracking unavailable\n"; + return 77; + } + partialCollect(); + } + else if (argc == 2 && std::string_view(argv[1]) == "--benchmark") benchmark(); else if (argc == 2 && std::string_view(argv[1]) == "--benchmark-labels") benchmark(true); else if (argc == 2 && std::string_view(argv[1]) == "--benchmark-fallback") benchmark(false, false); else if (argc == 2 && std::string_view(argv[1]) == "--benchmark-labels-fallback") benchmark(true, false); 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/Spirv13Image.cpp b/core/libs/prx/libSceAgcDriver/tests/Spirv13Image.cpp new file mode 100644 index 0000000000..ebaccaefc3 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/Spirv13Image.cpp @@ -0,0 +1,75 @@ +#include "BdaAbi.hpp" +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ShaderRecompiler; + +alignas(256) std::array Output{}; +alignas(256) std::array Texture{}; +alignas(256) constexpr std::array Code{ + 0x7e080218u, 0x343c0885u, 0x4a3c3d00u, 0x34063c84u, 0x2c3e3c87u, 0x363c3cffu, 0x0000007fu, + 0x7e140280u, 0x7e160280u, 0x7e180280u, 0x7e1a0280u, 0xf0001f08u, 0x00010a1eu, + 0xbf8c3f70u, 0xe0781000u, 0x80000a03u, 0xbf810000u, +}; + +void CheckImage(std::uint32_t version) { + const auto outputAddress = reinterpret_cast(Output.data()); + const auto textureAddress = reinterpret_cast(Texture.data()); + const std::array buffer{ + static_cast(outputAddress), static_cast((outputAddress >> 32u) & 0xffffu), + static_cast(Output.size() * 4u), 0x31016facu, + }; + const std::array image{ + static_cast(textureAddress >> 8u), + static_cast((textureAddress >> 40u) & 0xffu) | (6u << 20u) | (3u << 30u), + 31u | (127u << 14u), 0x24cu | (9u << 28u), 0u, 0u, 0u, 0u, + }; + const std::array sampler{0x92u, 0x00fff000u, 0u, 0u}; + std::array userData{}; + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(image.begin(), image.end(), userData.begin() + 4); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 12); + const std::array memory{{{reinterpret_cast(Code.data()), std::as_bytes(std::span(Code))}}}; + const std::array capabilities{ + spv::CapabilityShader, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, + spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, + spv::CapabilityImageQuery, spv::CapabilityStorageImageExtendedFormats, spv::CapabilityStorageImageReadWithoutFormat, + spv::CapabilityStorageImageWriteWithoutFormat, spv::CapabilitySampled1D, spv::CapabilityImage1D, + spv::CapabilitySampledBuffer, spv::CapabilityImageBuffer, spv::CapabilityFloat16, + }; + const std::array extensions{ + "SPV_EXT_descriptor_indexing", "SPV_KHR_float_controls", "SPV_KHR_physical_storage_buffer", "SPV_KHR_16bit_storage", + }; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, reinterpret_cast(Code.data()), Code, 0, {}}; + request.context = {32, 0, userData, ShaderComputeStageInfo{{32, 1, 1}, 0, {true, false, false}, false, 1}, std::nullopt, std::nullopt, memory}; + request.target = {0x00401000u, version, 32, BdaAbi::Version, capabilities, extensions, false, {1024, 1024, 64}, 1024, 32768, {}, {}}; + request.layout = {0, 0, 0, 128}; + request.useCache = false; + const auto result = Recompile(request); + const auto& words = result.spirv.Words(); + if (words.size() < 5 || words[0] != spv::MagicNumber || words[1] > version) { + throw std::runtime_error("image compilation did not preserve the requested SPIR-V target"); + } +} + +} + +int main() { + try { + CheckImage(0x00010300u); + CheckImage(0x00010400u); + std::puts("SPIR-V 1.3 and 1.4 image compilation 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/SpirvBranchTargets.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvBranchTargets.cpp new file mode 100644 index 0000000000..3add236bdb --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvBranchTargets.cpp @@ -0,0 +1,73 @@ +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ShaderRecompiler; + +void require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +std::vector recompile(std::span code) { + const std::array userData{0x10000000u, 0x00100000u, 0x40u, 0x00027facu, 0u, 0u, 0u, 0u, 7u, 9u, 0x10002000u, 0u}; + const std::array extensions{"SPV_KHR_storage_buffer_storage_class"}; + const std::array capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}}; + request.context.waveSize = 64; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00403000u; + request.target.spirvVersion = 0x00010600u; + request.target.subgroupSize = 64; + request.target.bdaAbiVersion = 1; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.maxWorkgroupSize = {1024u, 1024u, 64u}; + request.target.maxWorkgroupInvocations = 1024; + request.target.maxWorkgroupSharedMemoryBytes = 49152; + request.layout = {0, 0, 0, 128}; + request.useCache = false; + auto result = Recompile(request); + require(!result.spirv.empty(), "the shader did not recompile"); + return result.spirv.Copy(); +} + +void requireDistinctTargets(const char* name, std::span code) { + const auto words = recompile(code); + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + require(count != 0 && cursor + count <= words.size(), std::string(name) + ": truncated SPIR-V instruction"); + if ((words[cursor] & 0xffffu) == spv::OpBranchConditional) { + require(words[cursor + 2] != words[cursor + 3], std::string(name) + ": OpBranchConditional with the same true and false label, invalid in SPIR-V 1.6"); + } + cursor += count; + } +} + +} + +int main() { + try { + constexpr std::array zeroOffsetBranch{0xbf840000u, 0xbf810000u}; + requireDistinctTargets("s_cbranch_scc0 to the next instruction", zeroOffsetBranch); + constexpr std::array unalignedStore{0x7e020281u, 0xe0700000u, 0x80020100u, 0xbf810000u}; + requireDistinctTargets("store through a descriptor with an unaligned base", unalignedStore); + } catch (const std::exception& error) { + std::fprintf(stderr, "SpirvBranchTargets: %s\n", error.what()); + return 1; + } + std::puts("SpirvBranchTargets: no conditional branch names one label twice"); + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/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 new file mode 100644 index 0000000000..8155564e0b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvLdsAtomicLock.cpp @@ -0,0 +1,213 @@ +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ShaderRecompiler; + +constexpr std::uint32_t DsAddU64 = 0xd9000000u; +constexpr std::uint32_t DsAddU32Hi = 0xd8000004u; +constexpr std::uint32_t DsIncU32Hi = 0xd80c0004u; +constexpr std::uint32_t DsOperands = 0x00000201u; +constexpr std::uint32_t DsOperand32 = 0x00000401u; +constexpr std::uint32_t FlatAtomicAddU32 = 0xdcc90000u; +constexpr std::uint32_t FlatAtomicOperands = 0x0a7d0402u; + +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) { + if (!condition) throw std::runtime_error(message); +} + +std::vector RecompileProgram(std::span code) { + const std::array capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + const std::array userData{}; + const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + 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}, 4u, {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.layout = {0, 0, 0, 128}; + request.useCache = false; + auto result = Recompile(request); + Require(!result.spirv.empty(), "the LDS atomic program did not recompile"); + return std::vector(result.spirv.Words().begin(), result.spirv.Words().end()); +} + +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::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; +} + +Usage Compile(std::initializer_list body) { + std::vector code{0x7e020280u, 0x7e0402c1u, 0x7e060280u, 0x7e080281u}; + code.insert(code.end(), body); + code.insert(code.end(), {0xbf8cc07fu, 0xbf810000u}); + return Scan(RecompileProgram(code)); +} + +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() { + const auto usage = Compile({FlatAtomicAddU32, FlatAtomicOperands}); + Require(usage.elects == 0, "a program with only a 32-bit flat atomic got a workgroup lock: elect " + std::to_string(usage.elects)); +} + +void Check32BitOnlyIsUnchanged() { + const auto add = Compile({DsAddU32Hi, DsOperand32}); + Require(add.elects == 0 && add.exchanges == 0 && add.releases == 0 && add.adds == 1, "a program with only ds_add_u32 got a workgroup lock: elect " + std::to_string(add.elects) + ", lock acquire " + std::to_string(add.exchanges) + ", lock release " + std::to_string(add.releases) + ", native add " + std::to_string(add.adds)); + const auto inc = Compile({DsIncU32Hi, DsOperand32}); + Require(inc.elects == 0 && inc.releases == 0, "a program with only ds_inc_u32 got a workgroup lock: elect " + std::to_string(inc.elects) + ", lock release " + std::to_string(inc.releases)); +} + +} + +int main() { + try { + Check64BitOnlyLocks(); + Check32BitTakesLockWith64Bit(); + CheckOtherUpdatePathTakesLock(); + CheckFlatAtomicTakesLockWith64Bit(); + CheckFlatAtomicOnlyIsUnchanged(); + Check32BitOnlyIsUnchanged(); + } catch (const std::exception& error) { + std::fprintf(stderr, "SpirvLdsAtomicLock: %s\n", error.what()); + return 1; + } + std::puts("SpirvLdsAtomicLock: 32-bit LDS atomics take the 64-bit workgroup lock only in programs with a 64-bit LDS atomic"); + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp index bfd9fd061d..1be75623e3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp @@ -1,7 +1,11 @@ #include "SpirvBackend/SpirvModule.hpp" +#include "SpirvBackend/SpirvSpecialization.hpp" +#include #include +#include #include #include +#include #include #include @@ -184,7 +188,178 @@ void testEmittedWordsAreStable() { } +void testStructExtractsAreNotShuffled() { + const auto op = [](std::uint32_t count, spv::Op code) { return (count << 16u) | static_cast(code); }; + for (const bool vectorSource : {false, true}) { + const std::vector words{ + spv::MagicNumber, 0x00010300u, 0u, 20u, 0u, + op(2, spv::OpCapability), spv::CapabilityShader, + op(3, spv::OpMemoryModel), spv::AddressingModelLogical, spv::MemoryModelGLSL450, + op(6, spv::OpEntryPoint), spv::ExecutionModelGLCompute, 10u, 0x6e69616du, 0u, 9u, + op(6, spv::OpExecutionMode), 10u, spv::ExecutionModeLocalSize, 1u, 1u, 1u, + op(2, spv::OpTypeVoid), 1u, + op(4, spv::OpTypeInt), 2u, 32u, 0u, + vectorSource ? op(4, spv::OpTypeVector) : op(4, spv::OpTypeStruct), 3u, 2u, 2u, + op(4, spv::OpTypeVector), 4u, 2u, 4u, + op(4, spv::OpTypePointer), 5u, spv::StorageClassPrivate, 4u, + op(3, spv::OpTypeFunction), 6u, 1u, + op(4, spv::OpConstant), 2u, 7u, 1u, + op(4, spv::OpConstant), 2u, 8u, 2u, + op(4, spv::OpVariable), 5u, 9u, spv::StorageClassPrivate, + op(5, spv::OpFunction), 1u, 10u, 0u, 6u, + op(2, spv::OpLabel), 11u, + op(5, spv::OpCompositeConstruct), 3u, 12u, 7u, 8u, + op(5, spv::OpCompositeExtract), 2u, 13u, 12u, 0u, + op(5, spv::OpCompositeExtract), 2u, 14u, 12u, 1u, + op(7, spv::OpCompositeConstruct), 4u, 15u, 13u, 14u, 13u, 13u, + op(3, spv::OpStore), 9u, 15u, + op(1, spv::OpReturn), + op(1, spv::OpFunctionEnd), + }; + const auto specialized = ShaderRecompiler::SpecializeSpirv(words); + bool shuffled = false; + for (std::size_t cursor = 5; cursor < specialized.size(); cursor += specialized[cursor] >> 16u) { + if ((specialized[cursor] >> 16u) == 0u) break; + if ((specialized[cursor] & 0xffffu) == spv::OpVectorShuffle) shuffled = true; + } + check(shuffled == vectorSource, vectorSource ? "extracts of one vector were not folded into a shuffle" : "extracts of a struct were folded into a vector shuffle"); + } +} + +std::vector foldedSwitchModule(std::uint32_t deadSum, std::array localSize) { + const auto op = [](std::uint32_t count, spv::Op code) { return (count << 16u) | static_cast(code); }; + return { + spv::MagicNumber, 0x00010300u, 0u, 40u, 0u, + op(2, spv::OpCapability), spv::CapabilityShader, + op(3, spv::OpMemoryModel), spv::AddressingModelLogical, spv::MemoryModelGLSL450, + op(6, spv::OpEntryPoint), spv::ExecutionModelGLCompute, 11u, 0x6e69616du, 0u, 10u, + op(6, spv::OpExecutionMode), 11u, spv::ExecutionModeLocalSize, localSize[0], localSize[1], localSize[2], + op(2, spv::OpTypeVoid), 1u, + op(4, spv::OpTypeInt), 2u, 32u, 0u, + op(4, spv::OpTypePointer), 4u, spv::StorageClassPrivate, 2u, + op(3, spv::OpTypeFunction), 5u, 1u, + op(4, spv::OpConstant), 2u, 6u, 2u, + op(4, spv::OpConstant), 2u, 7u, 10u, + op(4, spv::OpConstant), 2u, 8u, 20u, + op(4, spv::OpVariable), 4u, 10u, spv::StorageClassPrivate, + op(5, spv::OpFunction), 1u, 11u, 0u, 5u, + op(2, spv::OpLabel), 12u, + op(5, spv::OpIAdd), 2u, deadSum, 6u, 7u, + op(5, spv::OpIMul), 2u, 14u, deadSum, 7u, + op(3, spv::OpSelectionMerge), 20u, spv::SelectionControlMaskNone, + op(7, spv::OpSwitch), 6u, 15u, 1u, 16u, 2u, 17u, + op(2, spv::OpLabel), 15u, + op(3, spv::OpStore), 10u, 7u, + op(2, spv::OpBranch), 20u, + op(2, spv::OpLabel), 16u, + op(3, spv::OpStore), 10u, 8u, + op(2, spv::OpBranch), 20u, + op(2, spv::OpLabel), 17u, + op(3, spv::OpStore), 10u, 14u, + op(2, spv::OpBranch), 20u, + op(2, spv::OpLabel), 20u, + op(1, spv::OpReturn), + op(1, spv::OpFunctionEnd), + }; +} + +void checkFoldedSwitch(const std::vector& words, std::uint32_t deadSum, const char* what) { + const auto specialized = ShaderRecompiler::SpecializeSpirv(words); + const auto counts = opcodeCounts(specialized); + std::set constants; + bool storesFolded = false; + for (std::size_t cursor = 5; cursor < specialized.size(); cursor += specialized[cursor] >> 16u) { + if ((specialized[cursor] >> 16u) == 0u) break; + const auto opcode = specialized[cursor] & 0xffffu; + if (opcode == spv::OpConstant) constants.insert(specialized[cursor + 2u]); + if (opcode == spv::OpStore && specialized[cursor + 2u] == 14u) storesFolded = true; + } + const std::string context = std::string(" (") + what + ")"; + check(!counts.contains(spv::OpUnreachable), ("blocks the folded switch left unreachable were kept" + context).c_str()); + check(counts.at(spv::OpLabel) == 3u, ("the specialized function does not have exactly its three live blocks" + context).c_str()); + check(storesFolded && constants.contains(14u), ("the folded product the live case stores is missing" + context).c_str()); + check(!constants.contains(deadSum), ("a folded constant without uses was kept" + context).c_str()); +} + +void testFoldingLeavesNoDeadBlocksOrConstants() { + checkFoldedSwitch(foldedSwitchModule(13u, {1u, 1u, 1u}), 13u, "no literal names a dead id"); + checkFoldedSwitch(foldedSwitchModule(32u, {15u, 16u, 1u}), 32u, "the OpTypeInt width and the LocalSize literals equal the dead ids"); +} + +void testIdsBeyondTheBoundAreRejected() { + auto words = foldedSwitchModule(13u, {1u, 1u, 1u}); + words[3] = 12u; + bool rejected = false; + try { + static_cast(ShaderRecompiler::SpecializeSpirv(words)); + } catch (const std::exception&) { + rejected = true; + } + check(rejected, "a module whose ids reach its bound was specialized"); +} + +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(); + testStructExtractsAreNotShuffled(); testRepeatedTypesShareOneId(); testOperandShapesDoNotCollide(); testOperandKindsAgree(); @@ -195,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/SpirvNarrowConstantStore.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvNarrowConstantStore.cpp index daa5ac8497..b63b6b04ae 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvNarrowConstantStore.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvNarrowConstantStore.cpp @@ -44,14 +44,14 @@ void CheckStorageOnlyModule(const RecompileResult& result, std::string_view name validator.SetMessageConsumer([&](spv_message_level_t, const char*, const spv_position_t&, const char* message) { if (message != nullptr) diagnostics += std::string(message) + '\n'; }); - const bool valid = validator.Validate(words); + const bool valid = validator.Validate(words.data(), words.size()); Check(valid, std::string(name) + ": emitted invalid SPIR-V:\n" + diagnostics); #endif } SpirvTarget StorageOnlyTarget() { - static constexpr std::array capabilities{ - spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, + static constexpr std::array capabilities{ + spv::CapabilityInt64, 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", diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp index c511be056e..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 @@ -15,8 +17,10 @@ namespace { int failures = 0; constexpr std::uint32_t Vulkan11 = 0x00401000u; +constexpr std::uint32_t Vulkan13 = 0x00403000u; constexpr std::uint32_t Spirv11 = 0x00010100u; constexpr std::uint32_t Spirv13 = 0x00010300u; +constexpr std::uint32_t Spirv16 = 0x00010600u; void check(bool condition, const std::string& message) { if (!condition) { @@ -101,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; } @@ -164,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) { @@ -241,6 +247,444 @@ OpFunctionEnd } } +void testSpecializedDeadContinue() { + for (const bool folded : {false, true}) { + const std::string source = std::string(R"(OpCapability Shader +OpMemoryModel Logical GLSL450 +OpEntryPoint GLCompute %main "main" +OpExecutionMode %main LocalSize 8 1 1 +%void = OpTypeVoid +%bool = OpTypeBool +%uint = OpTypeInt 32 0 +%private = OpTypePointer Private %uint +%function = OpTypeFunction %void +%zero = OpConstant %uint 0 +%one = OpConstant %uint 1 +%false = OpConstantFalse %bool +%output = OpVariable %private Private +%main = OpFunction %void None %function +%entry = OpLabel +OpBranch %header +%header = OpLabel +%count = OpPhi %uint %zero %entry %next %continue +OpLoopMerge %exit %continue None +OpBranch %body +%body = OpLabel +OpStore %output %count +)") + (folded ? "OpBranchConditional %false %continue %exit\n" : "OpBranch %exit\n") + R"(%continue = OpLabel +%next = OpIAdd %uint %count %one +OpBranch %header +%exit = OpLabel +OpReturn +OpFunctionEnd +)"; + const std::string label = folded ? "loop exit on a constant condition" : "loop with an unreachable continue target"; + try { + const auto words = assemble(source); + static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(words, Vulkan11, Spirv13, false, false)); + const auto specialized = ShaderRecompiler::SpecializeSpirv(words); + static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(specialized, Vulkan11, Spirv13, false, false)); + check(opcodeCount(specialized, spv::OpLoopMerge) == 1u, label + ": specialization lost the loop"); + check(opcodeCount(specialized, spv::OpIAdd) == 0u, label + ": specialization retained the dead continue block"); + check(ShaderRecompiler::SpecializeSpirv(specialized) == specialized, label + ": specialization is not stable"); + } catch (const std::exception& error) { + check(false, label + ": " + error.what()); + } + } +} + +void 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"); @@ -277,14 +721,239 @@ void testInvalidInputIsRejected() { } } +void testSpecializedSelectionExitSpirv16() { + const std::string source = R"(OpCapability Shader +OpMemoryModel Logical GLSL450 +OpEntryPoint GLCompute %main "main" %index %output +OpExecutionMode %main LocalSize 8 1 1 +OpDecorate %index BuiltIn LocalInvocationIndex +%void = OpTypeVoid +%bool = OpTypeBool +%uint = OpTypeInt 32 0 +%input = OpTypePointer Input %uint +%private = OpTypePointer Private %uint +%function = OpTypeFunction %void +%zero = OpConstant %uint 0 +%true = OpConstantTrue %bool +%index = OpVariable %input Input +%output = OpVariable %private Private +%main = OpFunction %void None %function +%entry = OpLabel +%lane = OpLoad %uint %index +%condition = OpIEqual %bool %lane %zero +OpSelectionMerge %exit None +OpBranchConditional %true %body %dead +%body = OpLabel +OpBranchConditional %condition %exit %nested +%nested = OpLabel +OpSelectionMerge %join None +OpBranchConditional %condition %write %join +%write = OpLabel +OpStore %output %lane +OpBranch %join +%join = OpLabel +%sum = OpIAdd %uint %lane %lane +OpBranch %exit +%dead = OpLabel +OpBranch %exit +%exit = OpLabel +%value = OpPhi %uint %zero %body %sum %join %zero %dead +OpStore %output %value +OpReturn +OpFunctionEnd +)"; + try { + auto words = assemble(source); + words[1] = Spirv16; + static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(words, Vulkan13, Spirv16, false, false)); + const auto specialized = ShaderRecompiler::SpecializeSpirv(words); + static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(specialized, Vulkan13, Spirv16, false, false)); + check(ShaderRecompiler::SpecializeSpirv(specialized) == specialized, "SPIR-V 1.6 selection exit specialization is not stable"); + } catch (const std::exception& error) { + check(false, std::string("specialized SPIR-V 1.6 selection exit: ") + error.what()); + } +} + +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/StackWalk.cpp b/core/libs/prx/libSceAgcDriver/tests/StackWalk.cpp new file mode 100644 index 0000000000..aa8370e367 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/StackWalk.cpp @@ -0,0 +1,180 @@ +#include "prx/libSceAgcDriver/Execution/include/StackWalk.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::CollapsedStacks; +using AgcDriver::DwarfFrames; +using Byte = DwarfFrames::Byte; +using Word = DwarfFrames::Word; +using Step = DwarfFrames::Step; + +constexpr std::size_t FunctionOffset = 0x100; +constexpr std::size_t FunctionSize = 0x40; + +void Check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +class Section { +public: + explicit Section(Byte returnColumn) : bytes(0x200, 0) { + const std::size_t cie = append32(0); + append32(0); + append({1, 'z', 'R', 0, 1, 0x78, returnColumn, 1, 0x1b, 0x0c, 7, 8, static_cast(0x80 | returnColumn), 1}); + patch32(cie, static_cast(cursor - cie - 4)); + const std::size_t fde = append32(0); + append32(static_cast(cursor - cie)); + append32(static_cast(Function() - Address(cursor))); + append32(FunctionSize); + append({0, 0x41, 0x0e, 0x10, 0x86, 0x02, 0x43, 0x0d, 0x06, 0, 0}); + patch32(fde, static_cast(cursor - fde - 4)); + end = cursor; + append32(0); + } + + Word Function() const { + return Address(FunctionOffset); + } + + DwarfFrames Frames() const { + return DwarfFrames(Address(0), Address(bytes.size()), bytes.data(), bytes.data() + end + 4); + } + +private: + Word Address(std::size_t offset) const { + return reinterpret_cast(bytes.data() + offset); + } + + std::size_t append32(std::uint32_t value) { + const auto at = cursor; + std::memcpy(bytes.data() + cursor, &value, 4); + cursor += 4; + return at; + } + + void append(std::initializer_list values) { + for (const auto value : values) bytes[cursor++] = value; + } + + void patch32(std::size_t at, std::uint32_t value) { + std::memcpy(bytes.data() + at, &value, 4); + } + + std::vector bytes; + std::size_t cursor = 0; + std::size_t end = 0; +}; + +struct Stack { + std::array words{}; + + Word At(std::size_t index) const { + return reinterpret_cast(&words[index]); + } + + auto Reader() const { + return [this](Word address, Word& value) { + if (address < At(0) || address > At(words.size() - 1)) return false; + std::memcpy(&value, reinterpret_cast(address), sizeof(value)); + return true; + }; + } +}; + +DwarfFrames::Registers Registers(Word pc, Word sp, Word fp) { + DwarfFrames::Registers registers{}; + registers[16] = pc; + registers[7] = sp; + registers[6] = fp; + return registers; +} + +void Prologue() { + const Section section(16); + const auto frames = section.Frames(); + const auto function = section.Function(); + Stack stack; + stack.words[32] = 0x5555; + stack.words[33] = 0x1234; + + auto registers = Registers(function, stack.At(33), 0x9999); + Check(frames.Unwind(registers, true, stack.Reader()) == Step::Stepped, "the entry of a function did not step"); + Check(registers[16] == 0x1234 && registers[7] == stack.At(34) && registers[6] == 0x9999, "the entry of a function stepped to the wrong frame"); + + registers = Registers(function + 2, stack.At(32), 0x9999); + Check(frames.Unwind(registers, true, stack.Reader()) == Step::Stepped, "the frame after the push did not step"); + Check(registers[16] == 0x1234 && registers[7] == stack.At(34) && registers[6] == 0x5555, "the frame after the push did not restore the frame pointer"); + + registers = Registers(function + 0x20, stack.At(8), stack.At(32)); + Check(frames.Unwind(registers, false, stack.Reader()) == Step::Stepped, "the body of a function did not step"); + Check(registers[16] == 0x1234 && registers[7] == stack.At(34) && registers[6] == 0x5555, "the body of a function did not step through the frame pointer"); +} + +void Outside() { + const Section section(16); + const auto frames = section.Frames(); + const auto function = section.Function(); + Stack stack; + auto registers = Registers(function + FunctionSize, stack.At(8), 0); + Check(frames.Unwind(registers, true, stack.Reader()) == Step::NoRecord, "a pc after the function had a record"); + registers = Registers(function - 1, stack.At(8), 0); + Check(frames.Unwind(registers, true, stack.Reader()) == Step::NoRecord, "a pc before the function had a record"); + Check(frames.Covers(function + 0x10) && !frames.Covers(function + FunctionSize), "the record covers the wrong range"); + registers = Registers(function, stack.At(63) + 4, 0); + const auto before = registers; + Check(frames.Unwind(registers, true, stack.Reader()) == Step::Failed && registers == before, "a read outside the stack was not refused"); + Check(DwarfFrames(0, 0, nullptr, nullptr).Unwind(registers, true, stack.Reader()) == Step::NoRecord, "a module without records stepped"); +} + +void ReturnColumn32() { + const Section section(32); + const auto frames = section.Frames(); + Stack stack; + stack.words[10] = 0x4321; + auto registers = Registers(section.Function(), stack.At(10), 0); +#ifdef _WIN32 + Check(frames.Unwind(registers, true, stack.Reader()) == Step::Stepped && registers[16] == 0x4321 && registers[7] == stack.At(11), "the MinGW return column was not read"); +#else + Check(frames.Unwind(registers, true, stack.Reader()) == Step::NoRecord, "a return column above 16 was accepted"); +#endif +} + +void Collapsed() { + CollapsedStacks stacks; + const std::uint64_t full[] = {0x10, 0x20, 0x30}; + const std::uint64_t cut[] = {0x10, 0x40}; + stacks.Add(full, false); + stacks.Add(full, false); + stacks.Add(cut, true); + const auto text = stacks.Format([](std::uint64_t address) { + char name[32]; + std::snprintf(name, sizeof(name), "m+0x%llx", static_cast(address)); + return std::string(name); + }); + Check(text == "m+0x30;m+0x20;m+0x10 2\n[truncated];m+0x40;m+0x10 1\n", "collapsed stacks changed format"); + Check(stacks.Truncated() == 1, "the truncated walk was not counted"); +} + +} + +int main() { + try { + Prologue(); + Outside(); + ReturnColumn32(); + Collapsed(); + std::cout << "stack walk tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp b/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp index bd62650a5a..c3f0359632 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp @@ -1,9 +1,14 @@ #include "ControlFlow/Structurizer.hpp" #include #include +#include #include +#include +#include +#include #include #include +#include #include #include @@ -74,41 +79,439 @@ void requireStructuredBranches(const ControlFlowGraph& graph, const char* name) } } +void requireExactPostDominators(const ControlFlowGraph& graph, const char* name) { + const auto count = static_cast(graph.blocks.size()); + const auto reachesExitAvoiding = [&](std::uint32_t from, std::uint32_t avoided) { + std::vector seen(count, false); + std::vector stack{from}; + seen[from] = true; + while (!stack.empty()) { + const auto id = stack.back(); + stack.pop_back(); + if (graph.blocks[id].successors.empty()) return true; + for (const auto successor : graph.blocks[id].successors) { + if (successor == avoided || seen[successor]) continue; + seen[successor] = true; + stack.push_back(successor); + } + } + return false; + }; + for (const auto& block : graph.blocks) { + std::vector expected; + for (std::uint32_t candidate = 0; candidate < count; ++candidate) { + if (candidate == block.id || !reachesExitAvoiding(block.id, candidate)) expected.push_back(candidate); + } + if (block.postDominators != expected) { + throw std::runtime_error(std::string(name) + ": block " + std::to_string(block.id) + " has " + std::to_string(block.postDominators.size()) + " post-dominators, expected " + std::to_string(expected.size())); + } + } +} + +void requireDominatedMerges(const ControlFlowGraph& graph, const char* name) { + for (const auto& block : graph.blocks) { + const auto merge = block.terminator.mergeBlock; + if (merge != InvalidControlFlowId && !graph.Dominates(block.id, merge)) { + throw std::runtime_error(std::string(name) + ": header " + std::to_string(block.id) + " does not dominate its merge " + std::to_string(merge)); + } + } +} + +using Routes = std::map; + +std::uint32_t originalBlock(const ControlFlowGraph& graph, std::uint32_t blockId, Routes& routes) { + for (std::size_t step = 0; step <= graph.blocks.size(); ++step) { + const auto& block = graph.FindBlock(blockId); + const auto& terminator = block.terminator; + if (block.instructionBegin != block.instructionEnd) return blockId; + if (terminator.gotoValue >= 0) routes[terminator.gotoVariable] = terminator.gotoValue != 0; + if (terminator.kind == TerminatorKind::Branch) { + blockId = terminator.trueBlock; + } else if (terminator.kind == TerminatorKind::ConditionalBranch && terminator.condition == BranchCondition::GotoVariable && routes.contains(terminator.gotoVariable)) { + blockId = routes.at(terminator.gotoVariable) ? terminator.trueBlock : terminator.falseBlock; + } else { + break; + } + } + throw std::runtime_error("block " + std::to_string(blockId) + " does not lead to an original block"); +} + +void requireOriginalBranches(const ControlFlowGraph& graph, const std::vector>& successors, const char* name) { + std::vector seen(successors.size(), false); + Routes entryRoutes; + std::vector> pending{{originalBlock(graph, graph.entryBlock, entryRoutes), entryRoutes}}; + std::vector> visited; + while (!pending.empty()) { + auto state = std::move(pending.back()); + pending.pop_back(); + if (std::find(visited.begin(), visited.end(), state) != visited.end()) continue; + visited.push_back(state); + const auto& block = graph.FindBlock(state.first); + const auto original = block.instructionBegin / 2; + std::vector branchTargets; + if (block.terminator.kind == TerminatorKind::Branch) branchTargets = {block.terminator.trueBlock}; + if (block.terminator.kind == TerminatorKind::ConditionalBranch) branchTargets = {block.terminator.trueBlock, block.terminator.falseBlock}; + std::vector targets; + for (const auto target : branchTargets) { + auto routes = state.second; + const auto reached = originalBlock(graph, target, routes); + targets.push_back(graph.FindBlock(reached).instructionBegin / 2); + pending.emplace_back(reached, std::move(routes)); + } + if (targets != successors[original]) { + throw std::runtime_error(std::string(name) + ": block " + std::to_string(original) + " no longer branches to its original targets"); + } + seen[original] = true; + } + if (std::find(seen.begin(), seen.end(), false) != seen.end()) { + throw std::runtime_error(std::string(name) + ": an original block is missing"); + } +} + +void requireStructuredLoop(const std::vector>& successors, const char* name) { + auto graph = makeGraph(successors); + Structurizer{}.Structurize(graph); + requireExactPostDominators(graph, name); + requireStructuredBranches(graph, name); + requireDominatedMerges(graph, name); + requireOriginalBranches(graph, successors, name); +} + +const BasicBlock& originalCopy(const ControlFlowGraph& graph, std::uint32_t original) { + const auto copy = std::find_if(graph.blocks.begin(), graph.blocks.end(), [&](const BasicBlock& block) { return block.instructionBegin == original * 2 && block.instructionEnd == original * 2 + 2; }); + if (copy == graph.blocks.end()) throw std::runtime_error("original block " + std::to_string(original) + " is missing"); + return *copy; +} + +std::uint32_t guestBlock(const ControlFlowGraph& graph, std::uint32_t blockId, Routes& routes, const char* name) { + for (std::size_t step = 0; step <= graph.blocks.size(); ++step) { + const auto& block = graph.FindBlock(blockId); + const auto& terminator = block.terminator; + if (block.instructionBegin != block.instructionEnd) { + if (terminator.gotoValue >= 0) throw std::runtime_error(std::string(name) + ": block " + std::to_string(blockId) + " sets a route variable after guest instructions"); + return blockId; + } + if (terminator.gotoValue >= 0) routes[terminator.gotoVariable] = terminator.gotoValue != 0; + if (terminator.kind == TerminatorKind::Branch) { + blockId = terminator.trueBlock; + } else if (terminator.kind == TerminatorKind::ConditionalBranch && terminator.condition == BranchCondition::GotoVariable) { + const auto route = routes.find(terminator.gotoVariable); + if (route == routes.end()) throw std::runtime_error(std::string(name) + ": block " + std::to_string(blockId) + " reads route variable " + std::to_string(terminator.gotoVariable) + " before any path sets it"); + blockId = route->second ? terminator.trueBlock : terminator.falseBlock; + } else { + return blockId; + } + } + throw std::runtime_error(std::string(name) + ": a cycle of empty blocks"); +} + +void requireSameExecutions(const ControlFlowGraph& original, const ControlFlowGraph& structured, const char* name) { + if (structured.blocks.empty() || structured.entryBlock != 0 || structured.blocks.front().id != 0) { + throw std::runtime_error(std::string(name) + ": the structured graph does not start at block 0"); + } + using State = std::tuple; + std::vector pending{{0u, original.entryBlock, {}}}; + std::set visited; + while (!pending.empty()) { + auto [structuredId, originalId, routes] = std::move(pending.back()); + pending.pop_back(); + const auto copyId = guestBlock(structured, structuredId, routes, name); + if (!visited.emplace(copyId, originalId, routes).second) continue; + const auto& copy = structured.FindBlock(copyId); + const auto& source = original.FindBlock(originalId); + if (copy.instructionBegin != source.instructionBegin || copy.instructionEnd != source.instructionEnd || copy.terminator.kind != source.terminator.kind || copy.terminator.condition != source.terminator.condition) { + throw std::runtime_error(std::string(name) + ": block " + std::to_string(copyId) + " does not run like original block " + std::to_string(originalId)); + } + if (source.terminator.kind == TerminatorKind::Branch || source.terminator.kind == TerminatorKind::ConditionalBranch) pending.emplace_back(copy.terminator.trueBlock, source.terminator.trueBlock, routes); + if (source.terminator.kind == TerminatorKind::ConditionalBranch) pending.emplace_back(copy.terminator.falseBlock, source.terminator.falseBlock, routes); + } +} + +void requireUnconditionalEntry(const ControlFlowGraph& original, const ControlFlowGraph& structured, const char* name) { + const auto& entry = original.FindBlock(original.entryBlock); + auto blockId = structured.blocks.front().id; + for (std::size_t step = 0; step <= structured.blocks.size(); ++step) { + const auto& block = structured.FindBlock(blockId); + if (block.instructionBegin == entry.instructionBegin && block.instructionEnd == entry.instructionEnd) return; + if (block.instructionBegin != block.instructionEnd || block.terminator.kind != TerminatorKind::Branch) break; + blockId = block.terminator.trueBlock; + } + throw std::runtime_error(std::string(name) + ": the entry block does not run unconditionally first"); +} + +void requireSameStructuredExecutions(const std::vector>& successors, const char* name, const std::map& words = {}) { + auto original = makeGraph(successors); + for (const auto& [blockId, count] : words) original.blocks[blockId].estimatedSpirvWords = count; + auto graph = original; + Structurizer{}.Structurize(graph); + requireExactPostDominators(graph, name); + requireStructuredBranches(graph, name); + requireDominatedMerges(graph, name); + requireOriginalBranches(graph, successors, name); + requireSameExecutions(original, graph, name); + requireUnconditionalEntry(original, graph, name); +} + +void requireNoRouteVariables(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 { + Structurizer{}.Structurize(graph); + } catch (const std::runtime_error& error) { + if (std::string(error.what()).starts_with(growthBudget)) return; + throw std::runtime_error(std::string(name) + ": refused with \"" + error.what() + "\" instead of the shared merge splitting growth budget"); + } + throw std::runtime_error(std::string(name) + ": was structurized instead of refused at the shared merge splitting growth budget"); +} + +std::vector> originalPostDominators(const ControlFlowGraph& graph) { + std::vector all; + for (std::uint32_t id = 0; id < graph.blocks.size(); ++id) all.push_back(id); + std::vector> result; + for (const auto& block : graph.blocks) result.push_back(block.successors.empty() ? std::vector{block.id} : all); + bool changed = true; + while (changed) { + changed = false; + for (const auto& block : graph.blocks) { + std::vector next; + if (block.successors.empty()) { + next = {block.id}; + } else { + next = result[block.successors.front()]; + for (std::size_t index = 1; index < block.successors.size(); ++index) { + std::vector intersection; + const auto& successor = result[block.successors[index]]; + std::set_intersection(next.begin(), next.end(), successor.begin(), successor.end(), std::back_inserter(intersection)); + next = std::move(intersection); + } + if (std::find(next.begin(), next.end(), block.id) == next.end()) next.push_back(block.id); + std::sort(next.begin(), next.end()); + next.erase(std::unique(next.begin(), next.end()), next.end()); + } + if (next != result[block.id]) { + result[block.id] = std::move(next); + changed = true; + } + } + } + return result; +} + +std::vector> originalDominators(const ControlFlowGraph& graph) { + std::vector all; + for (std::uint32_t id = 0; id < graph.blocks.size(); ++id) all.push_back(id); + std::vector> result; + for (const auto& block : graph.blocks) result.push_back(block.id == graph.entryBlock ? std::vector{block.id} : all); + bool changed = true; + while (changed) { + changed = false; + for (const auto& block : graph.blocks) { + if (block.id == graph.entryBlock) continue; + std::vector next; + if (block.predecessors.empty()) { + next = {block.id}; + } else { + next = result[block.predecessors.front()]; + for (std::size_t index = 1; index < block.predecessors.size(); ++index) { + std::vector intersection; + const auto& predecessor = result[block.predecessors[index]]; + std::set_intersection(next.begin(), next.end(), predecessor.begin(), predecessor.end(), std::back_inserter(intersection)); + next = std::move(intersection); + } + if (std::find(next.begin(), next.end(), block.id) == next.end()) next.push_back(block.id); + std::sort(next.begin(), next.end()); + next.erase(std::unique(next.begin(), next.end()), next.end()); + } + if (next != result[block.id]) { + result[block.id] = std::move(next); + changed = true; + } + } + } + return result; +} + +void requireOriginalDominanceSets(ControlFlowGraph graph, const char* name) { + for (auto& block : graph.blocks) { + block.dominators.clear(); + block.postDominators.clear(); + } + const auto expected = originalPostDominators(graph); + const auto expectedDominators = originalDominators(graph); + const auto original = graph; + const std::string stopAfterAnalyses = "dominance differential fixture: stop before CFG rewrites"; + graph.unsupported = true; + graph.unsupportedReason = stopAfterAnalyses; + bool stopped = false; + try { + Structurizer{}.Structurize(graph); + } catch (const std::runtime_error& error) { + if (error.what() != stopAfterAnalyses) throw; + stopped = true; + } + if (!stopped || graph.blocks.size() != original.blocks.size() || graph.entryBlock != original.entryBlock) { + throw std::runtime_error(std::string(name) + ": differential fixture did not stop before CFG rewrites"); + } + for (std::size_t id = 0; id < graph.blocks.size(); ++id) { + const auto& block = graph.blocks[id]; + if (block.id != original.blocks[id].id || block.successors != original.blocks[id].successors || block.predecessors != original.blocks[id].predecessors) { + throw std::runtime_error(std::string(name) + ": differential fixture changed the CFG"); + } + if (block.postDominators != expected[id]) { + throw std::runtime_error(std::string(name) + ": block " + std::to_string(id) + " differs from the original post-dominator fixed point"); + } + if (block.dominators != expectedDominators[id]) { + throw std::runtime_error(std::string(name) + ": block " + std::to_string(id) + " differs from the original dominator fixed point"); + } + } +} + } int main() { try { + auto largeBlocks = makeGraph({{1}, {2, 3}, {1, 4}, {5, 6}, {1, 7}, {8, 7}, {9, 4}, {10}, {6, 10}, {10}, {}}); + for (auto& block : largeBlocks.blocks) block.estimatedSpirvWords = 1000u; + Structurizer{}.Structurize(largeBlocks); + requireStructuredBranches(largeBlocks, "large loop exit blocks retain the baseline result"); + auto exitJunction = makeGraph({{1}, {2, 3}, {9}, {4, 6}, {5, 9}, {1}, {7, 8}, {9}, {1}, {}}); + Structurizer{}.Structurize(exitJunction); + requireStructuredBranches(exitJunction, "shared loop exit junctions retain structured branches"); auto nested = makeGraph({{1}, {2}, {5, 3}, {5, 4}, {7}, {6, 7}, {}, {1}}); Structurizer{}.Structurize(nested); - if (nested.FindBlock(2).terminator.mergeBlock == nested.FindBlock(3).terminator.mergeBlock) { - std::fprintf(stderr, "the nested selections share merge block %u\n", nested.FindBlock(2).terminator.mergeBlock); + requireExactPostDominators(nested, "nested selections"); + if (originalCopy(nested, 2).terminator.mergeBlock == originalCopy(nested, 3).terminator.mergeBlock) { + std::fprintf(stderr, "the nested selections share merge block %u\n", originalCopy(nested, 2).terminator.mergeBlock); return 1; } requireStructuredBranches(nested, "nested selections"); auto exitTail = makeGraph({{1}, {2}, {3, 4}, {6}, {6, 5}, {1}, {}}); Structurizer{}.Structurize(exitTail); + requireExactPostDominators(exitTail, "a loop exit through a tail block"); requireStructuredBranches(exitTail, "a loop exit through a tail block"); auto exitTails = makeGraph({{1}, {2}, {3, 4}, {7}, {5, 6}, {7, 1}, {7}, {}}); Structurizer{}.Structurize(exitTails); + requireExactPostDominators(exitTails, "a loop with two exit tails"); requireStructuredBranches(exitTails, "a loop with two exit tails"); auto endingExits = makeGraph({{6, 1}, {2}, {3}, {6, 4}, {2, 5}, {}, {}}); Structurizer{}.Structurize(endingExits); + requireExactPostDominators(endingExits, "a loop whose two exits end the program"); requireStructuredBranches(endingExits, "a loop whose two exits end the program"); auto threeEndingExits = makeGraph({{1}, {2}, {5, 3}, {6, 4}, {1, 7}, {}, {}, {}}); Structurizer{}.Structurize(threeEndingExits); + requireExactPostDominators(threeEndingExits, "a loop whose three exits end the program"); requireStructuredBranches(threeEndingExits, "a loop whose three exits end the program"); auto innerEndingExit = makeGraph({{1}, {2}, {3}, {7, 4}, {2, 5}, {1, 6}, {}, {}}); Structurizer{}.Structurize(innerEndingExit); + requireExactPostDominators(innerEndingExit, "an inner loop exit that ends the program"); requireStructuredBranches(innerEndingExit, "an inner loop exit that ends the program"); + requireStructuredLoop({{1}, {2, 4}, {3, 4}, {4, 5}, {1, 6}, {1}, {}}, "nested selections in a loop with two latches"); + requireStructuredLoop({{1}, {5, 2}, {5, 3}, {6, 4}, {7, 5}, {8, 1}, {1}, {1}, {}}, "nested selections on the false side in a loop with three latches"); + requireStructuredLoop({{1}, {2, 5}, {3, 4}, {5, 6}, {5}, {1, 7}, {1}, {}}, "an if-else in a loop whose one arm continues"); + requireStructuredLoop({{1}, {2}, {3, 4}, {2, 5}, {}, {1}}, "an inner loop that returns or continues its outer loop"); + requireStructuredLoop({{1}, {2, 3}, {4, 5}, {}, {3, 6}, {1}, {2}}, "an inner loop that leaves both loops or continues its outer loop"); + requireStructuredLoop({{1, 3}, {2, 4}, {3, 1}, {4}, {}}, "a loop whose latch exits into a block the branch before the loop also reaches"); + requireStructuredLoop({{1, 2}, {3, 2}, {4}, {4, 1}, {}}, "a loop whose header exits into a block the branch before the loop also reaches"); + requireStructuredLoop({{1, 2}, {4}, {3, 1}, {4, 2}, {}}, "a loop whose header exits into a block the branch before the loop reaches first"); auto earlyReturn = makeGraph({{2, 1}, {4, 2}, {3, 5}, {5}, {}, {}}); Structurizer{}.Structurize(earlyReturn); + requireExactPostDominators(earlyReturn, "an early return inside a selection that joins its parent's merge"); requireStructuredBranches(earlyReturn, "an early return inside a selection that joins its parent's merge"); - for (const auto& block : earlyReturn.blocks) { - const auto merge = block.terminator.mergeBlock; - if (merge != InvalidControlFlowId && !earlyReturn.Dominates(block.id, merge)) { - std::fprintf(stderr, "an early return inside a selection: header %u does not dominate its merge %u\n", block.id, merge); - return 1; + requireDominatedMerges(earlyReturn, "an early return inside a selection that joins its parent's merge"); + const std::vector> elseReturnSuccessors{{1, 2}, {4}, {3, 4}, {}, {5, 6}, {7}, {7}, {}}; + auto elseReturn = makeGraph(elseReturnSuccessors); + Structurizer{}.Structurize(elseReturn); + requireExactPostDominators(elseReturn, "an early return in one arm of a selection whose other arm reaches the same merge"); + 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; + diamonds.push_back({head + 1u, head + 2u}); + diamonds.push_back({head + 3u}); + diamonds.push_back({head + 3u}); + } + diamonds.push_back({}); + auto chain = makeGraph(diamonds); + Structurizer{}.Structurize(chain); + requireExactPostDominators(chain, "a chain of fifty selections"); + requireStructuredLoop({{1}, {2, 8}, {3, 5}, {4, 8}, {7}, {6, 7}, {7, 8}, {1}, {}}, "nested selections sharing a loop continuation"); + auto sharedDiscard = makeGraph({{5, 1}, {4, 2}, {7, 3}, {7, 4}, {5}, {7, 6}, {}, {}}); + const std::vector sharedDiscardWords{444, 1944, 68, 1128, 8, 2084, 148, 28}; + for (auto& block : sharedDiscard.blocks) { + std::sort(block.successors.begin(), block.successors.end()); + block.estimatedSpirvWords = sharedDiscardWords[block.id]; + } + Structurizer{}.Structurize(sharedDiscard); + requireStructuredBranches(sharedDiscard, "early discards that share one ending block"); + requireSameStructuredExecutions({{1, 2}, {3, 0}, {}, {2}}, "an entry loop with two exits whose merges collide"); + requireSameStructuredExecutions({{2}, {}, {4, 1}, {1}, {2, 3}}, "a selection region that wraps around to its dominator"); + requireSameStructuredExecutions({{1}, {2, 3}, {0, 4}, {}, {3}}, "an entry loop whose merge splitting runs out of budget"); + requireSameStructuredExecutions({{2, 5}, {4, 1}, {1, 3}, {}, {}, {1}}, "a self-loop entered from both arms of the entry selection"); + requireSameStructuredExecutions({{1, 2}, {2, 3}, {1, 3}, {}}, "an irreducible cycle entered at both blocks"); + requireSameStructuredExecutions({{1}, {2, 4}, {3, 4}, {4, 5}, {1, 6}, {1}, {}}, "nested selections in a loop with two latches compared run by run"); + requireSameStructuredExecutions({{1, 2}, {2, 0}, {1}}, "a cycle entered at both blocks inside a loop that never ends"); + requireSameStructuredExecutions({{1, 3}, {2}, {1}, {}}, "a branch that returns or enters a loop that never ends"); + requireSameStructuredExecutions({{1}, {1}}, "a block that branches to itself forever"); + requireSameStructuredExecutions({{1, 13}, {2, 8}, {3, 6}, {4, 0}, {5, 4}, {6}, {}, {}, {5, 9}, {10, 0}, {11}, {11, 12}, {}, {14}, {15}, {16, 17}, {4}, {18, 19}, {7, 0}, {}}, "merge splitting that adds one forwarder per round until its budget runs out"); + const std::vector> junctionLoop{{1, 2}, {}, {3, 10}, {4, 8}, {6, 0}, {}, {7, 8}, {0}, {5, 9}, {7}, {9, 11}, {11}}; + requireSameStructuredExecutions(junctionLoop, "a loop whose latch is too large to clone, so merge splitting keeps adding junctions", {{7u, 2000u}}); + auto unsupportedExit = makeGraph(junctionLoop); + unsupportedExit.blocks[7].estimatedSpirvWords = 2000u; + unsupportedExit.blocks[5].terminator.kind = TerminatorKind::Unsupported; + requireGrowthBudgetRefusal(std::move(unsupportedExit), "a junction loop whose unsupported exit keeps the reach-flag fallback out"); + requireOriginalDominanceSets(makeGraph(diamonds), "raw diamond chain"); + requireOriginalDominanceSets(makeGraph({{1}, {2, 3}, {1}, {}}), "a loop with an exit"); + requireOriginalDominanceSets(makeGraph({{1}, {2}, {1}}), "a non-exiting SCC"); + requireOriginalDominanceSets(makeGraph({{1, 3}, {2}, {1}, {}}), "an exit-or-spin branch"); + requireOriginalDominanceSets(makeGraph({{1}, {}, {3}, {}, {5}, {4}}), "unreachable exit and non-exiting islands"); + auto reordered = makeGraph({{1}, {}, {2}, {0, 4}, {1}}); + reordered.entryBlock = 3; + requireOriginalDominanceSets(std::move(reordered), "a nonzero entry with permuted block IDs"); + requireOriginalDominanceSets(makeGraph({{1, 2}, {}, {}}), "disjoint exits with an empty post-dominator intersection"); + requireOriginalDominanceSets(makeGraph({{2}, {2}, {}}), "separate roots with an empty dominator intersection"); + requireOriginalDominanceSets(makeGraph({{1}, {1, 2}, {}}), "a self-loop with an exit"); + std::uint32_t randomState = 0x7b19a5c3u; + const auto randomWord = [&] { randomState = randomState * 1664525u + 1013904223u; return randomState; }; + for (std::uint32_t trial = 0; trial < 24u; ++trial) { + const auto count = 8u + trial % 9u; + std::vector ids(count); + for (std::uint32_t id = 0; id < count; ++id) ids[id] = id; + for (auto size = count; size > 1u; --size) std::swap(ids[size - 1u], ids[randomWord() % size]); + if (ids.front() == 0u) std::swap(ids.front(), ids.back()); + std::vector> successors(count); + for (std::uint32_t position = 0; position + 1u < count; ++position) { + const auto remaining = count - position - 1u; + const auto degree = position == 0u ? 2u : position == 1u ? 1u : std::min(randomWord() % 3u, remaining); + if (degree == 0u) continue; + auto& edges = successors[ids[position]]; + const auto first = position + 1u + randomWord() % remaining; + edges.push_back(ids[first]); + if (degree == 2u) { + auto second = position + 1u + randomWord() % (remaining - 1u); + if (second >= first) ++second; + edges.push_back(ids[second]); + } + std::sort(edges.begin(), edges.end()); } + auto dag = makeGraph(successors); + dag.entryBlock = ids.front(); + const auto name = "deterministic DAG " + std::to_string(trial); + requireOriginalDominanceSets(std::move(dag), name.c_str()); } } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); 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/SubgroupClock.cpp b/core/libs/prx/libSceAgcDriver/tests/SubgroupClock.cpp index 8adf998a90..0763da9a2d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SubgroupClock.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SubgroupClock.cpp @@ -6,24 +6,31 @@ namespace { struct Case { VkDriverId driver; + std::uint32_t deviceId; std::string_view name; bool narrow; }; constexpr Case Cases[]{ - {VK_DRIVER_ID_MESA_RADV, "AMD Radeon RX 6750 XT (RADV NAVI22)", true}, - {VK_DRIVER_ID_MESA_RADV, "AMD Ryzen 5 7600X 6-Core Processor (RADV RAPHAEL_MENDOCINO)", true}, - {VK_DRIVER_ID_MESA_RADV, "AMD Radeon RX 7900 XTX (RADV NAVI31)", true}, - {VK_DRIVER_ID_MESA_RADV, "AMD Radeon 890M (RADV STRIX1)", true}, - {VK_DRIVER_ID_MESA_RADV, "AMD Radeon RX 9070 XT (RADV GFX1201)", false}, - {VK_DRIVER_ID_MESA_RADV, "AMD Radeon RX 5700 XT (RADV NAVI10)", false}, - {VK_DRIVER_ID_MESA_RADV, "AMD BC-250 (RADV GFX1013)", false}, - {VK_DRIVER_ID_MESA_RADV, "AMD Radeon RX 6750 XT (RADV NAVI22", false}, - {VK_DRIVER_ID_MESA_RADV, "AMD Radeon RX 6750 XT (RADV NAVI2)", false}, - {VK_DRIVER_ID_MESA_RADV, "", false}, - {VK_DRIVER_ID_AMD_PROPRIETARY, "AMD Radeon RX 6750 XT (RADV NAVI22)", false}, - {VK_DRIVER_ID_AMD_OPEN_SOURCE, "AMD Radeon RX 6750 XT", false}, - {VK_DRIVER_ID_NVIDIA_PROPRIETARY, "NVIDIA GeForce RTX 5070 Ti", false}, + {VK_DRIVER_ID_MESA_RADV, 0, "AMD Radeon RX 6750 XT (RADV NAVI22)", true}, + {VK_DRIVER_ID_MESA_RADV, 0, "AMD Ryzen 5 7600X 6-Core Processor (RADV RAPHAEL_MENDOCINO)", true}, + {VK_DRIVER_ID_MESA_RADV, 0, "AMD Radeon RX 7900 XTX (RADV NAVI31)", true}, + {VK_DRIVER_ID_MESA_RADV, 0, "AMD Radeon 890M (RADV STRIX1)", true}, + {VK_DRIVER_ID_MESA_RADV, 0, "AMD Radeon RX 9070 XT (RADV GFX1201)", false}, + {VK_DRIVER_ID_MESA_RADV, 0, "AMD Radeon RX 5700 XT (RADV NAVI10)", false}, + {VK_DRIVER_ID_MESA_RADV, 0, "AMD BC-250 (RADV GFX1013)", false}, + {VK_DRIVER_ID_MESA_RADV, 0, "AMD Radeon RX 6750 XT (RADV NAVI22", false}, + {VK_DRIVER_ID_MESA_RADV, 0, "AMD Radeon RX 6750 XT (RADV NAVI2)", false}, + {VK_DRIVER_ID_MESA_RADV, 0, "", false}, + {VK_DRIVER_ID_AMD_PROPRIETARY, 0x1114, "AMD Radeon 860M", true}, + {VK_DRIVER_ID_AMD_PROPRIETARY, 0x1115, "AMD Radeon 860M", false}, + {VK_DRIVER_ID_AMD_PROPRIETARY, 0x744c, "AMD Radeon RX 7900 XT", true}, + {VK_DRIVER_ID_AMD_PROPRIETARY, 0x744d, "AMD Radeon RX 7900 XT", false}, + {VK_DRIVER_ID_AMD_PROPRIETARY, 0x747e, "AMD Radeon RX 7800 XT", true}, + {VK_DRIVER_ID_AMD_PROPRIETARY, 0x747f, "AMD Radeon RX 7800 XT", false}, + {VK_DRIVER_ID_AMD_PROPRIETARY, 0, "AMD Radeon RX 6750 XT (RADV NAVI22)", false}, + {VK_DRIVER_ID_AMD_OPEN_SOURCE, 0x1114, "AMD Radeon 860M", false}, + {VK_DRIVER_ID_NVIDIA_PROPRIETARY, 0, "NVIDIA GeForce RTX 5070 Ti", false}, }; } @@ -31,8 +38,8 @@ constexpr Case Cases[]{ int main() { int failures = 0; for (const auto& item : Cases) { - if (AgcDriver::NarrowSubgroupClock(item.driver, item.name) != item.narrow) { - std::fprintf(stderr, "driver %d \"%.*s\": expected %s subgroup clock\n", static_cast(item.driver), static_cast(item.name.size()), item.name.data(), item.narrow ? "a narrow" : "a full"); + if (AgcDriver::NarrowSubgroupClock(item.driver, item.deviceId, item.name) != item.narrow) { + std::fprintf(stderr, "driver %d device 0x%x \"%.*s\": expected %s subgroup clock\n", static_cast(item.driver), item.deviceId, static_cast(item.name.size()), item.name.data(), item.narrow ? "a narrow" : "a full"); ++failures; } } diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index e774ae46e2..d97862941b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -1,12 +1,17 @@ #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "execution/VulkanTestDevice.hpp" #include "prx/libSceAgcDriver/Execution/include/QueueState.hpp" +#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" #include "prx/libSceAgcDriver/Submit/include/Acb.hpp" #include "prx/libSceAgcDriver/Eq/include/Query.hpp" #include "prx/libSceAgcDriver/Eq/include/Event.hpp" +#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" #include #include @@ -19,6 +24,7 @@ #include #include #include +#include #include #include #ifdef _WIN32 @@ -108,6 +114,17 @@ void testValidation() { AgcDriverWaitIdle_nid_postfix(); } +void testReadOnlyResummarize() { + std::vector commands; + for (const auto [offset, value] : {std::pair{0x000u, 0x10u}, {0x200u, 0x007007f3u}, {0x010u, 0xa0000183u}, {0x011u, 0x20000180u}, {0x8eu, 0xffffffffu}, {0x8fu, 0u}, {0x1c4u, 0u}, {0x203u, 0u}}) { + commands.insert(commands.end(), {0xc0016900u, offset, value}); + } + commands.insert(commands.end(), {0xc0012d00u, 3u, 2u}); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + check(sceAgcDriverSubmitDcb(&packet) == 0, "read-only resummarize submission failed"); + AgcDriverWaitIdle_nid_postfix(); +} + void testClearState() { AgcDriver::QueueState graphics{{{0x20c, 1}}, {{0x10, 17}, {0x11, 23}}, {{0x242, 5}}}; const auto shader = graphics.shader; @@ -179,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"); @@ -196,6 +228,40 @@ 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"); + check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "flip after a release interrupt submit failed"); + AgcDriverWaitIdle_nid_postfix(); + 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); words = {0xc0064900, 0x528, (3u << 29u) | (3u << 24u) | (1u << 16u), static_cast(labelAddress), static_cast(static_cast(labelAddress) >> 32u), 0x89abcdefu, 0x01234567u, 0}; @@ -276,7 +342,7 @@ void testEndOfPipeLabelsWithoutWork() { } void testLabelStoredSinceSubmission() { - alignas(64) static volatile std::uint32_t gate = 0, label = 0, done = 0, late = 0; + alignas(64) static volatile std::uint32_t gate = 0, label = 0, done = 0, late = 0, waiting = 0; submit(0x20, commands(waitEqual(&gate, 1), waitEqual(&label, 1), writeData(&done, 1))); submit(0, commands(writeData(&label, 1))); waitFor(&label, 1, "producer label never landed"); @@ -284,13 +350,17 @@ void testLabelStoredSinceSubmission() { gate = 1; check(waitFor(&done, 1, "consumer never passed its waits") < std::chrono::milliseconds(500), "a label stored after the wait's submission did not satisfy it"); AgcDriverWaitIdle_nid_postfix(); - submit(0x20, commands(waitEqual(&label, 1), writeData(&late, 1))); - check(waitFor(&late, 1, "consumer never passed its wait") >= std::chrono::milliseconds(900), "a label stored before the wait's submission satisfied it"); + submit(0x20, commands(writeData(&waiting, 1), waitEqual(&label, 1), writeData(&late, 1))); + waitFor(&waiting, 1, "consumer never reached its label wait"); + std::this_thread::sleep_for(std::chrono::milliseconds(1200)); + check(late == 0, "a label stored before the wait's submission satisfied it or the wait expired"); + submit(0, commands(writeData(&label, 1))); + waitFor(&late, 1, "consumer did not resume after a new producer label"); AgcDriverWaitIdle_nid_postfix(); } void testWideLabelStoredSinceSubmission() { - alignas(64) static volatile std::uint32_t gate = 0, done = 0, late = 0; + alignas(64) static volatile std::uint32_t gate = 0, done = 0, late = 0, waiting = 0; alignas(64) static volatile std::uint32_t label[2] = {0, 0x5eed}; submit(0x20, commands(waitEqual(&gate, 1), waitEqual64(label, 1, 0xffffffffu), writeData(&done, 1))); submit(0, commands(writeData(label, 1))); @@ -300,17 +370,21 @@ void testWideLabelStoredSinceSubmission() { check(waitFor(&done, 1, "consumer never passed its waits") < std::chrono::milliseconds(500), "a 32-bit label stored after a low-dword 64-bit wait's submission did not satisfy it"); AgcDriverWaitIdle_nid_postfix(); gate = 0; - submit(0x20, commands(waitEqual(&gate, 1), waitEqual64(label, 1, ~0ull), writeData(&late, 1))); + submit(0x20, commands(waitEqual(&gate, 1), writeData(&waiting, 1), waitEqual64(label, 1, ~0ull), writeData(&late, 1))); submit(0, commands(writeData(label, 1))); waitFor(label, 1, "producer label never landed"); label[0] = 0; gate = 1; - check(waitFor(&late, 1, "consumer never passed its wait") >= std::chrono::milliseconds(900), "a 32-bit store satisfied a 64-bit wait whose high dword never matched"); + waitFor(&waiting, 1, "consumer never reached its wide label wait"); + std::this_thread::sleep_for(std::chrono::milliseconds(1200)); + check(late == 0, "a mismatched high dword satisfied the 64-bit wait or the wait expired"); + submit(0, commands(writeData(label + 1, 0), writeData(label, 1))); + waitFor(&late, 1, "consumer did not resume after a matching 64-bit label"); AgcDriverWaitIdle_nid_postfix(); } void testLabelHeldAtSubmission() { - alignas(64) static volatile std::uint32_t gate = 0, label = 1, done = 0, reset = 0; + alignas(64) static volatile std::uint32_t gate = 0, label = 1, done = 0, reset = 0, waiting = 0; submit(0x20, commands(waitEqual(&gate, 1), waitEqual(&label, 1), writeData(&done, 1))); label = 0; gate = 1; @@ -318,9 +392,13 @@ void testLabelHeldAtSubmission() { AgcDriverWaitIdle_nid_postfix(); gate = 0; label = 1; - submit(0x20, commands(waitEqual(&gate, 1), writeData(&label, 0), waitEqual(&label, 1), writeData(&reset, 1))); + submit(0x20, commands(waitEqual(&gate, 1), writeData(&label, 0), writeData(&waiting, 1), waitEqual(&label, 1), writeData(&reset, 1))); gate = 1; - check(waitFor(&reset, 1, "consumer never passed its wait") >= std::chrono::milliseconds(900), "a label its own queue stored first counted as held at the submission"); + waitFor(&waiting, 1, "consumer never reached its reset label wait"); + std::this_thread::sleep_for(std::chrono::milliseconds(1200)); + check(reset == 0, "a label reset by its own queue satisfied the wait or the wait expired"); + submit(0x28, commands(writeData(&label, 1))); + waitFor(&reset, 1, "consumer did not resume after another queue restored the label"); AgcDriverWaitIdle_nid_postfix(); } @@ -440,6 +518,105 @@ void testMultiSubmissions() { } } +std::array memoryCopy(const void* destination, const void* source, std::uint32_t bytes) { + const auto from = reinterpret_cast(source), to = reinterpret_cast(destination); + 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); + auto* produced = static_cast(GuestHeap::GuestHeapAlignForGpu_nid_postfix(65536, bytes)); + auto* source = static_cast(GuestHeap::GuestHeapAlignForGpu_nid_postfix(65536, bytes)); + std::vector destination(words); + for (const bool label : {false, true}) { + for (std::size_t i = 0; i < words; ++i) { + produced[i] = 0xa5000000u | static_cast(i); + source[i] = 0x5b000000u | static_cast(i); + } + std::fill(destination.begin(), destination.end(), 0u); + const auto copiesBefore = AgcDriver::DriverDetail::copiesToHostMemory.load(); + if (label) submit(0, commands(memoryCopy(source, produced, bytes), writeData(destination.data(), 0x1abe1u), memoryCopy(destination.data(), source, bytes))); + else submit(0, commands(memoryCopy(source, produced, bytes), memoryCopy(destination.data(), source, bytes))); + AgcDriverWaitIdle_nid_postfix(); + check(std::equal(destination.begin(), destination.end(), produced), label ? "a copy into host memory behind a label into its destination lost bytes" : "a copy into host memory did not see the GPU copy into its source"); + check(AgcDriver::DriverDetail::copiesToHostMemory.load() == copiesBefore + 1, "a copy into host memory drained instead of syncing its source"); + } + GuestHeap::GuestHeapFree_nid_postfix(source); + 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 { @@ -481,6 +658,25 @@ void testShaderHeaderAlignment() { refused(expectFailure([&] { AgcDriverRegisterShader_nid_postfix(at(4, truncated)); }), "smaller than its fixed fields"); } +void testHeaderWithoutProgramAddress() { + alignas(256) static const std::array code{0xbf810000}; + struct Header { + Shader shader{}; + std::array registers{}; + }; + alignas(8) static Header header{}; + header.shader.file_header = 0x34333231; + header.shader.version = 0x18; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = sizeof(code); + header.shader.code = code.data(); + header.shader.type = 4; + header.registers = {{{0x08a, 0x60000002}, {0x08b, 0x00030008}, {0x0ca, 0x03000002}}}; + header.shader.sh_registers = header.registers.data(); + header.shader.num_sh_registers = header.registers.size(); + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + void testRegisteredFloatMode() { using AgcDriver::DriverDetail::RegisteredFloatMode; using AgcDriver::DriverDetail::ShaderSnapshot; @@ -516,6 +712,185 @@ 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); + registered[0] = 0xbf810000; + alignas(256) static std::array raw{}; + raw.fill(0xbf800000); + raw[1] = 0xbf810000; + const auto registeredAddress = reinterpret_cast(registered.data()); + const auto rawAddress = reinterpret_cast(raw.data()); + AgcDriver::DriverDetail::ShaderRegistry shaders; + AgcDriver::DriverDetail::ShaderSnapshot snapshot{registeredAddress, 0x1000, 2, {}, {}}; + snapshot.code.assign(registered.begin(), registered.end()); + snapshot.header.resize(sizeof(Shader)); + const auto entry = std::make_shared(std::move(snapshot)); + 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"); + 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{}; + decode.programs.resize(2); + decode.programs[0].snapshot = unregistered; + decode.programs[1].snapshot = entry; + decode.programs[1].codeOffset = 8; + check(AgcDriver::DriverDetail::DecodeProgramsCurrent(decode, shaders), "an unchanged decode was not current"); + registered[8] = 0xbf810000; + check(AgcDriver::DriverDetail::DecodeProgramsCurrent(decode, shaders), "a rewritten registered program made a decode stale"); + raw[0] = 0xbf810000; + check(!AgcDriver::DriverDetail::DecodeProgramsCurrent(decode, shaders), "a rewritten unregistered program left its decode current"); + decode.programs[0].snapshot = AgcDriver::DriverDetail::ProgramSnapshot({}, rawAddress, {2}); + 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), {}}; + return result; + }; + const auto plain = snapshot({0xbf800000, 0xbf810000}); + check(!AgcDriver::DriverDetail::ProgramUsesWorkgroup(plain, 0) && !AgcDriver::DriverDetail::ProgramUsesWorkgroup(plain, 0), "a program without LDS or barriers needs a workgroup"); + const auto barrier = snapshot({0xbf8a0000, 0xbf810000}); + check(AgcDriver::DriverDetail::ProgramUsesWorkgroup(barrier, 0), "an s_barrier did not need a workgroup"); + const auto lds = snapshot({0xd8340000, 0x00000100, 0xbf810000}); + check(AgcDriver::DriverDetail::ProgramUsesWorkgroup(lds, 0) && AgcDriver::DriverDetail::ProgramUsesWorkgroup(lds, 0), "an LDS write did not need a workgroup"); + const auto entry = snapshot({0xbf8a0000, 0xbf810000, 0xbf800000, 0xbf810000}); + check(!AgcDriver::DriverDetail::ProgramUsesWorkgroup(entry, 2) && AgcDriver::DriverDetail::ProgramUsesWorkgroup(entry, 0), "the workgroup check did not start at the program's entry"); +} + +void testVertexTailSnapshots() { + using namespace AgcDriver::DriverDetail; + alignas(256) static std::array prolog{0xbe802008u, 0xbf9f0000u}; + alignas(256) static std::array continuation{0xbf800000u, 0xbf810000u}; + const auto address = reinterpret_cast(prolog.data()); + const auto next = reinterpret_cast(continuation.data()); + const std::array data{static_cast(next), static_cast(next >> 32u)}; + const auto source = ReadRawShader(address); + check(source->userDataTailRegister == 8u, "raw tail metadata is missing"); + const auto target = ReadUserDataTailContinuation(source->code, 8u, data); + check(target && target->codeAddress == next, "tail did not read its user-data continuation"); + DrawDecode decode{}; + decode.programs.resize(1); + decode.programs[0].snapshot = source; + decode.programs[0].tailContinuation = target; + check(DecodeProgramsCurrent(decode, {}), "unchanged tail snapshot became stale"); + continuation[0] = 0xbf8a0000u; + check(!DecodeProgramsCurrent(decode, {}), "rewriting the continuation did not invalidate the draw cache"); + AgcDriver::QueueState queue{}; + queue.context[0x2d5] = 0x2000u; + queue.shader = {{0xc8, static_cast(address >> 8u)}, {0xc9, static_cast(address >> 40u)}, + {0x8b, 4u}, {0x8c, data[0]}, {0x8d, data[1]}}; + check(VertexWorkgroupRequired(queue, {}), "a barrier in the continuation was hidden from workgroup routing"); + queue.context[0x2d5] |= 0x02000000u; + check(!VertexWorkgroupRequired(queue, {}), "a passthrough program was rerouted to a workgroup"); + const std::array nullAddress{}; + check(expectFailure([&] { static_cast(ReadUserDataTailContinuation(source->code, 8u, nullAddress)); }).find("address is null") != std::string::npos, "null tail address was accepted"); + const std::array unaligned{data[0] + 1u, data[1]}; + check(expectFailure([&] { static_cast(ReadUserDataTailContinuation(source->code, 8u, unaligned)); }).find("dword aligned") != std::string::npos, "unaligned tail address was accepted"); + const std::array unmapped{0x1000u, 0u}; + check(!expectFailure([&] { static_cast(ReadUserDataTailContinuation(source->code, 8u, unmapped)); }).empty(), "unmapped tail address was accepted"); +} + +void testRegisteredVertexWorkgroupRouting() { + using namespace AgcDriver::DriverDetail; + ShaderSnapshot snapshot{0x20000, 0x30000, 2, {0xd8340000u, 0x00000100u, 0xbf810000u, 0xbf810000u}, {}}; + snapshot.header.resize(sizeof(Shader)); + const ShaderRegistry registry{{snapshot.codeAddress, {std::make_shared(std::move(snapshot))}}}; + AgcDriver::QueueState queue{}; + queue.shader = {{0xc8, 0x200}, {0xc9, 0}}; + queue.context[0x2d5] = 0x2000; + check(VertexWorkgroupRequired(queue, registry), "a registered NGG vertex program using LDS did not need a workgroup"); + for (const auto routing : {0u, 0x2020u, 0x2004u, 0x02002000u}) { + queue.context[0x2d5] = routing; + check(!VertexWorkgroupRequired(queue, registry), "legacy, geometry, tessellation or passthrough routing changed to an NGG vertex workgroup"); + } + queue.context[0x2d5] = 0x2000; + auto unaligned = registry; + const auto aligned = unaligned.begin()->second.front(); + ShaderSnapshot shifted{0x1ffff, 0x30000, 2, aligned->code, aligned->header}; + unaligned.clear(); + unaligned[shifted.codeAddress].push_back(std::make_shared(std::move(shifted))); + check(expectFailure([&] { static_cast(VertexWorkgroupRequired(queue, unaligned)); }).find("not dword aligned") != std::string::npos, "an unaligned registered entry was accepted"); + queue.shader[0xc9] = 0x100; + 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, {}}; @@ -534,30 +909,49 @@ void testWorkerFailure() { } -int main() { +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(); + std::puts("Read-only resummarize submission passed"); + return 0; + } 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; rawCode[1] = 0xbf810000; rawCode[2] = 0xbf810000; const auto rawAddress = reinterpret_cast(rawCode.data()); - const auto literal = AgcDriver::DriverDetail::ReadRawComputeShader(rawAddress); + const auto literal = AgcDriver::DriverDetail::ReadRawShader(rawAddress); check(literal->code.size() == 3 && literal->header.empty(), "raw compute stopped at an instruction literal"); - check(AgcDriver::DriverDetail::ReadRawComputeShader(rawAddress) == literal, "unchanged raw compute code lost its snapshot identity"); + check(AgcDriver::DriverDetail::ReadRawShader(rawAddress) == literal, "unchanged raw compute code lost its snapshot identity"); rawCode[0] = 0xbf820002; rawCode[1] = 0xbf810000; rawCode[2] = 0xbf800000; rawCode[3] = 0xbf810000; - const auto branched = AgcDriver::DriverDetail::ReadRawComputeShader(rawAddress); + const auto branched = AgcDriver::DriverDetail::ReadRawShader(rawAddress); check(branched->code.size() == 4 && literal->code[0] == 0xbe8003ff, "raw compute lost branch targets or modified an earlier snapshot"); check(branched != literal, "changed raw compute code reused a stale snapshot"); std::array, 8> concurrent; std::vector readers; for (auto& snapshot : concurrent) readers.emplace_back([&snapshot, rawAddress] { - snapshot = AgcDriver::DriverDetail::ReadRawComputeShader(rawAddress); + snapshot = AgcDriver::DriverDetail::ReadRawShader(rawAddress); }); for (auto& reader : readers) reader.join(); for (const auto& snapshot : concurrent) check(snapshot == branched, "concurrent raw compute readers lost snapshot reuse"); @@ -566,17 +960,24 @@ int main() { const auto firstAddress = reinterpret_cast(programs.front().data()); readers.clear(); for (auto& snapshot : concurrent) readers.emplace_back([&snapshot, firstAddress] { - snapshot = AgcDriver::DriverDetail::ReadRawComputeShader(firstAddress); + snapshot = AgcDriver::DriverDetail::ReadRawShader(firstAddress); }); for (auto& reader : readers) reader.join(); for (const auto& snapshot : concurrent) check(snapshot == concurrent.front(), "concurrent raw compute misses duplicated snapshots"); - const auto evicted = AgcDriver::DriverDetail::ReadRawComputeShader(firstAddress); + const auto evicted = AgcDriver::DriverDetail::ReadRawShader(firstAddress); for (std::size_t i = 1; i < programs.size(); ++i) - AgcDriver::DriverDetail::ReadRawComputeShader(reinterpret_cast(programs[i].data())); - check(AgcDriver::DriverDetail::ReadRawComputeShader(firstAddress) != evicted && evicted->code[0] == 0xbf810000, + AgcDriver::DriverDetail::ReadRawShader(reinterpret_cast(programs[i].data())); + check(AgcDriver::DriverDetail::ReadRawShader(firstAddress) != evicted && evicted->code[0] == 0xbf810000, "raw compute cache eviction lost snapshot lifetime or exceeded its entry limit"); - check(!expectFailure([&] { AgcDriver::DriverDetail::ReadRawComputeShader(rawAddress + 4); }).empty(), "raw compute accepted a misaligned entry"); - check(!expectFailure([] { AgcDriver::DriverDetail::ReadRawComputeShader(0); }).empty(), "raw compute accepted an unmapped entry"); + check(!expectFailure([&] { AgcDriver::DriverDetail::ReadRawShader(rawAddress + 4); }).empty(), "raw compute accepted a misaligned entry"); + check(!expectFailure([] { AgcDriver::DriverDetail::ReadRawShader(0); }).empty(), "raw compute accepted an unmapped entry"); + alignas(256) std::array straddling{}; + straddling.fill(0xbf800000); + straddling[63] = 0xf4000000; + straddling[64] = 0xfa000000; + straddling[65] = 0xbf810000; + const auto straddled = AgcDriver::DriverDetail::ReadRawShader(reinterpret_cast(straddling.data())); + check(straddled->code.size() == 66 && straddled->code[64] == 0xfa000000, "raw compute stopped at a memory instruction across its first read window"); #ifdef _WIN32 auto* mapping = static_cast(VirtualAlloc(nullptr, 8192, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); check(mapping != nullptr, "cannot allocate raw compute boundary test"); @@ -585,21 +986,27 @@ int main() { auto* boundedCode = mapping + 1024 - 64; std::fill_n(boundedCode, 64, 0xbf800000u); const auto boundedAddress = reinterpret_cast(boundedCode); - const auto unterminated = expectFailure([&] { AgcDriver::DriverDetail::ReadRawComputeShader(boundedAddress); }); + const auto unterminated = expectFailure([&] { AgcDriver::DriverDetail::ReadRawShader(boundedAddress); }); boundedCode[63] = 0xbf810000; - const auto bounded = AgcDriver::DriverDetail::ReadRawComputeShader(boundedAddress); + const auto bounded = AgcDriver::DriverDetail::ReadRawShader(boundedAddress); check(VirtualProtect(mapping + 1024, 4096, PAGE_READWRITE, &protection) != 0, "cannot extend raw compute mapping"); boundedCode[63] = 0xbf800000; boundedCode[64] = 0xbf810000; - const auto extended = AgcDriver::DriverDetail::ReadRawComputeShader(boundedAddress); + const auto extended = AgcDriver::DriverDetail::ReadRawShader(boundedAddress); check(extended != bounded && extended->code.size() == 65, "raw compute reused code before its end changed"); check(VirtualProtect(mapping + 1024, 4096, PAGE_NOACCESS, &protection) != 0, "cannot revoke cached raw compute tail"); - check(!expectFailure([&] { AgcDriver::DriverDetail::ReadRawComputeShader(boundedAddress); }).empty(), "raw compute reused an inaccessible cached tail"); + check(!expectFailure([&] { AgcDriver::DriverDetail::ReadRawShader(boundedAddress); }).empty(), "raw compute reused an inaccessible cached tail"); check(VirtualProtect(mapping, 4096, PAGE_NOACCESS, &protection) != 0, "cannot revoke cached raw compute code"); - check(!expectFailure([&] { AgcDriver::DriverDetail::ReadRawComputeShader(boundedAddress); }).empty(), "raw compute reused inaccessible cached code"); + check(!expectFailure([&] { AgcDriver::DriverDetail::ReadRawShader(boundedAddress); }).empty(), "raw compute reused inaccessible cached code"); check(VirtualFree(mapping, 0, MEM_RELEASE) != 0, "cannot release raw compute boundary test"); 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(); @@ -616,7 +1023,13 @@ int main() { testWaitFreeSubmissionTheCpuWaitsFor(); testMultiSubmissions(); 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"); std::puts("AGC driver submit tests passed"); 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..1b1ec2d0da --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/TessellationLowering.cpp @@ -0,0 +1,210 @@ +#include "IntermediateRepresentation/IrBuilder.hpp" +#include "IntermediateRepresentation/IrProgram.hpp" +#include "Optimization/TessellationLowering.hpp" +#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; +} + +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; + }); + + return passed ? 0 : 1; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureDetiler.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureDetiler.cpp index 02183e9826..5874fbb5c2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureDetiler.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureDetiler.cpp @@ -1,4 +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 @@ -26,6 +29,7 @@ struct Push { std::uint32_t linearBase; std::uint32_t columnBegin; std::uint32_t rowBegin; + std::uint32_t pipeBankXor; }; Push decodePush(const std::vector& bytes) { @@ -165,8 +169,45 @@ void RunTextureDetilerTests(const Context& context, const TextureDetilerTestAcce detiler.Dispatch(commands, TextureTileMode::kD4KBX, 4, source, 0, destination, 0, layout, false, 0); const auto equationSpecialization = access.lastSpecialization(); + Require(decodePush(access.lastDispatch().pushConstants).pipeBankXor == 0u, "a dispatch without a window passed a pipe/bank XOR"); + detiler.Dispatch(commands, TextureTileMode::kD4KBX, 4, source, 0, destination, 0, layout, false, 0, false, {.pipeBankXor = 0xa00u}); + Require(decodePush(access.lastDispatch().pushConstants).pipeBankXor == 0xa00u, "the window's pipe/bank XOR did not reach the detiling shader"); Require(equationSpecialization[0] == 4 && equationSpecialization[1] == 4096 && equationSpecialization[2] == 2, "SW_4KB_D_X detiling must select the equation family over 4 KiB blocks"); + { + const auto mip = ComputeColorTargetMipLayout(ColorTileMode::RenderTarget, 4, 1024, 1024, 11, 4); + Require(mip.width == 64 && mip.height == 64 && mip.tail, "the first packed render-target mip has the wrong extent"); + Require(mip.tiledOffset == 0 && mip.tiledSize == 65536 && mip.tailX == 64 && mip.tailY == 0, "the first packed render-target mip lost its shared-block origin"); + Require(mip.pitchBytes == 256 && mip.linearSize == 16384, "packed render-target mips must use tightly packed transfer rows"); + const auto next = ComputeColorTargetMipLayout(ColorTileMode::RenderTarget, 4, 1024, 1024, 11, 5); + Require(next.width == 32 && next.height == 32 && next.tail && next.tiledOffset == 0 && next.tiledSize == 65536 && next.tailX == 0 && next.tailY == 64, "the next packed render-target mip lost its shared-block origin"); + const auto regular = ComputeColorTargetMipLayout(ColorTileMode::RenderTarget, 4, 1024, 1024, 11, 3); + Require(!regular.tail && regular.width == 128 && regular.height == 128 && regular.tiledOffset == 0 && regular.tiledSize == 65536, "a regular render-target mip must be relative to its own target buffer"); + 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 0e632a0de4..31ff43033e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp @@ -1,12 +1,19 @@ #include "GraphicsTests.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include +#include +#include +#include #include +#include #include #include +#include +#include namespace { @@ -44,6 +51,20 @@ void srgbDecodeTests() { Require(SampledTextureFormat(context, 130) == VK_FORMAT_R8G8B8A8_SRGB && SampledTextureFormat(context, 14) == VK_FORMAT_R8G8_UNORM && SampledTextureFormat(context, 170) == VK_FORMAT_BC1_RGBA_SRGB_BLOCK, "formats without a shader decode must keep their views"); } +void srgbTwinTests() { + constexpr std::uint32_t srgb8 = 1u; + constexpr std::uint32_t srgb8_8 = 2u; + Context context{}; + Require(SrgbTwinFormat(context, VK_FORMAT_R8_UNORM) == VK_FORMAT_R8_SRGB && SrgbTwinFormat(context, VK_FORMAT_R8G8_UNORM) == VK_FORMAT_R8G8_SRGB, "a device that samples R8_SRGB and R8G8_SRGB must give R8 and R8G8 their sRGB twins"); + context.srgbDecodeFormats = srgb8_8; + Require(SrgbTwinFormat(context, VK_FORMAT_R8G8_UNORM) == VK_FORMAT_UNDEFINED, "a device that decodes 8_8_SRGB in the shader must not give R8G8 an sRGB twin"); + Require(SrgbTwinFormat(context, VK_FORMAT_R8_UNORM) == VK_FORMAT_R8_SRGB, "R8 must keep its sRGB twin when only 8_8_SRGB is decoded in the shader"); + context.srgbDecodeFormats = srgb8 | srgb8_8; + Require(SrgbTwinFormat(context, VK_FORMAT_R8_UNORM) == VK_FORMAT_UNDEFINED, "a device that decodes 8_SRGB in the shader must not give R8 an sRGB twin"); + Require(SrgbTwinFormat(context, VK_FORMAT_R8G8B8A8_UNORM) == VK_FORMAT_R8G8B8A8_SRGB && SrgbTwinFormat(context, VK_FORMAT_BC7_UNORM_BLOCK) == VK_FORMAT_BC7_SRGB_BLOCK, "formats without a shader decode must keep their sRGB twins"); + Require(SrgbTwinFormat(context, VK_FORMAT_R32_SFLOAT) == VK_FORMAT_UNDEFINED, "formats without an sRGB counterpart have no twin"); +} + template void reject(TAction action, std::string_view reason) { try { @@ -55,6 +76,73 @@ void reject(TAction action, std::string_view reason) { throw std::runtime_error(std::string("expected texture format rejection: ") + std::string(reason)); } +int channelLowBit(VkFormat format, VkComponentSwizzle channel) { + const auto index = static_cast(channel - VK_COMPONENT_SWIZZLE_R); + switch (format) { + case VK_FORMAT_R5G6B5_UNORM_PACK16: return std::array{11, 5, 0, -1}[index]; + case VK_FORMAT_A1R5G5B5_UNORM_PACK16: return std::array{10, 5, 0, 15}[index]; + case VK_FORMAT_B4G4R4A4_UNORM_PACK16: return std::array{4, 8, 12, 0}[index]; + default: throw std::runtime_error("no channel layout for packed format " + std::to_string(format)); + } +} + +void packedChannelTests() { + constexpr std::array>, 3> packed{{{133u, {0, 5, 11, 0}}, {134u, {0, 5, 10, 15}}, {136u, {0, 4, 8, 12}}}}; + constexpr std::array components{VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_A}; + for (const auto& [format, lowBits] : packed) { + for (std::size_t component = 0; component < components.size(); ++component) { + const auto channel = TextureComponentChannel(format, components[component]); + Require(channelLowBit(ResolveTextureFormat(format), channel) == lowBits[component], "format " + std::to_string(format) + " component " + std::to_string(component) + " is not read from bit " + std::to_string(lowBits[component])); + } + } + 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 convertedDccClearTests() { alignas(64) std::array keys{}; GuestTextureResource resource{}; @@ -72,15 +160,72 @@ void convertedDccClearTests() { Require(read(20, 0x40) == DccKeys::Clear0001 && read(20, 0x80) == DccKeys::Clear1110, "format 20 must keep its 0001 and 1110 DCC clear codes"); } +void singleDccKeysTests() { + alignas(256) std::array texels{}; + for (std::size_t index = 0; index < texels.size(); ++index) texels[index] = 0x01000000u + static_cast(index); + alignas(64) std::array keys{}; + keys.fill(0x10); + GuestTextureResource resource{}; + resource.baseAddress = reinterpret_cast(texels.data()); + resource.dccAddress = reinterpret_cast(keys.data()); + const auto read = [&](std::uint32_t format) { + resource.format = format; + return TextureClearKeys(resource, sizeof(texels)); + }; + const auto* original = reinterpret_cast(texels.data()); + for (const std::uint32_t format : {1u, 56u, 77u}) { + Require(read(format) == DccKeys::ClearSingle, "comp-to-single keys of format " + std::to_string(format) + " must read as single"); + const auto elementBytes = BytesPerElement(format); + std::vector surface(sizeof(texels)); + ReadTextureSurface(resource, DccKeys::ClearSingle, surface); + bool expanded = true; + for (std::size_t offset = 0; offset < surface.size(); ++offset) expanded = expanded && surface[offset] == original[offset / 256 * 256 + offset % elementBytes]; + Require(expanded, "a read of format " + std::to_string(format) + " under comp-to-single keys must give every 256-byte block its first element"); + } + Require(read(74) == DccKeys::Uncompressed, "comp-to-single keys of a format whose element does not divide 256 bytes must read as stored"); + keys[1] = 0xff; + Require(read(56) == DccKeys::Uncompressed, "comp-to-single keys mixed with uncompressed ones must read as stored"); +} + +void packedFloatDccClearTests() { + for (const bool alphaOnMsb : {false, true}) { + for (const auto keys : {DccKeys::Clear0000, DccKeys::Clear0001, DccKeys::Clear1110, DccKeys::Clear1111}) { + std::array texels{}; + texels.fill(0xdeadbeefu); + Require(FillDccClear(VK_FORMAT_B10G11R11_UFLOAT_PACK32, keys, alphaOnMsb, std::as_writable_bytes(std::span(texels))), "packed float fast clear was not encoded"); + const std::uint32_t expected = keys == DccKeys::Clear1110 || keys == DccKeys::Clear1111 ? 0x781e03c0u : 0u; + for (const auto texel : texels) Require(texel == expected, "packed float DCC clear did not encode RGB zero or one"); + } + } + alignas(64) std::array keys{0xc0, 0xc0, 0xc0, 0xc0}; + GuestTextureResource resource{}; + resource.format = 36; + resource.dccAddress = reinterpret_cast(keys.data()); + Require(TextureClearKeys(resource, 1024) == DccKeys::Clear1111, "packed float texture discarded its white fast-clear keys"); +} + } void RunTextureFormatTests() { 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"); Require(!IsBlockCompressed(1), "format 1 must not be block compressed"); Require(BlockWidth(1) == 1u && BlockHeight(1) == 1u, "format 1 must have a one-texel block"); + Require(ResolveTextureFormat(50) == VK_FORMAT_A2B10G10R10_UNORM_PACK32, "format 50 must resolve to A2B10G10R10_UNORM"); + Require(ResolveTextureFormat(51) == VK_FORMAT_A2B10G10R10_SNORM_PACK32, "format 51 must resolve to A2B10G10R10_SNORM"); + Require(ResolveTextureFormat(54) == VK_FORMAT_A2B10G10R10_UINT_PACK32, "format 54 must resolve to A2B10G10R10_UINT"); + Require(ResolveTextureFormat(55) == VK_FORMAT_A2B10G10R10_SINT_PACK32, "format 55 must resolve to A2B10G10R10_SINT"); + for (const std::uint32_t format : {50u, 51u, 54u, 55u}) Require(BytesPerElement(format) == 4u && !IsBlockCompressed(format), "packed 10-bit format " + std::to_string(format) + " must be four bytes and not block compressed"); + Require(ShaderRecompiler::SampledTextureNumericClass(ShaderRecompiler::IrBufferFormat::Format10_10_10_2SNorm) == ShaderRecompiler::IrTextureNumericClass::Float, "format 51 must sample as float"); + Require(ShaderRecompiler::SampledTextureNumericClass(ShaderRecompiler::IrBufferFormat::Format10_10_10_2SInt) == ShaderRecompiler::IrTextureNumericClass::Sint, "format 55 must sample as signed integer"); + Require(ShaderRecompiler::SampledTextureNumericClass(ShaderRecompiler::IrBufferFormat::Format10_10_10_2UInt) == ShaderRecompiler::IrTextureNumericClass::Uint, "format 54 must sample as unsigned integer"); + Require(ResolveTextureFormat(56) == VK_FORMAT_R8G8B8A8_UNORM, "format 56 must resolve to R8G8B8A8_UNORM"); Require(BytesPerElement(56) == 4u, "format 56 must be four bytes wide"); @@ -114,9 +259,13 @@ void RunTextureFormatTests() { reject([] { ResolveTextureFormat(0); }, "unsupported guest texture format"); reject([] { ResolveTextureFormat(183); }, "unsupported guest texture format"); reject([] { ResolveTextureFormat(9999); }, "unsupported guest texture format"); - reject([] { BytesPerElement(2); }, "unsupported guest texture format"); + reject([] { BytesPerElement(3); }, "unsupported guest texture format"); 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(); } diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp index d9b5a107a3..cb439c6ffe 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"); @@ -286,6 +304,12 @@ void RunTextureTilingTests() { } constexpr ElementAddress xorZx64[] = {{0, 0, 0, 0, 0x8400}, {0, 32, 19, 11, 0xbc350}, {0, 16, 6, 6, 0x69040}, {0, 11, 19, 0, 0xe778}, {0, 32, 0, 11, 0xb8100}, {1, 0, 0, 0, 0x400}, {1, 15, 9, 5, 0x52eb8}, {1, 8, 3, 3, 0x32950}, {1, 5, 9, 0, 0x598}, {1, 15, 0, 5, 0x52fa8}, {2, 0, 0, 0, 0x800}, {2, 7, 4, 2, 0x21ca8}, {2, 4, 1, 1, 0x10090}, {2, 2, 4, 0, 0x1820}, {2, 7, 0, 2, 0x20ca8}, {3, 0, 0, 0, 0x4200}, {3, 3, 1, 0, 0x4238}, {3, 2, 0, 0, 0x4220}, {3, 1, 1, 0, 0x4218}, {3, 3, 0, 0, 0x4228}, {4, 0, 0, 0, 0x200}, {4, 1, 0, 0, 0x208}, {4, 1, 0, 0, 0x208}, {4, 0, 0, 0, 0x200}, {4, 1, 0, 0, 0x208}, {5, 0, 0, 0, 0x2000}, {5, 0, 0, 0, 0x2000}, {5, 0, 0, 0, 0x2000}, {5, 0, 0, 0, 0x2000}, {5, 0, 0, 0, 0x2000}}; requireSliceAddresses(thickVolume(TextureTileMode::kZ64KBX, 71, 33, 20, 12, 6), 8, 786432, xorZx64, "thin SW_64KB_Z_X 64 bpp 33x20x12, 6 levels"); + constexpr ElementAddress xorDx32[] = {{0, 0, 0, 0, 0x0}, {0, 63, 63, 15, 0x3f0fc}, {0, 47, 4, 9, 0x13134}, {0, 54, 3, 3, 0x19af8}, {0, 23, 39, 12, 0x2ba5c}, {0, 57, 14, 7, 0x1c6e4}}; + requireThickAddresses(thickVolume(TextureTileMode::kD64KBX, 56, 64, 64, 16, 1), 4, 1, 262144, xorDx32, "SW_64KB_D_X 32 bpp 64x64x16, 1 level"); + constexpr ElementAddress xorDx8[] = {{0, 0, 0, 0, 0x20000}, {0, 99, 59, 69, 0x112b9f}, {0, 15, 52, 41, 0xa1347}, {0, 86, 43, 67, 0x11637e}, {0, 87, 59, 38, 0xb61fb}, {1, 0, 0, 0, 0x10000}, {1, 49, 29, 34, 0x76d29}, {1, 11, 28, 6, 0x126a3}, {1, 8, 27, 2, 0x12238}, {1, 30, 6, 17, 0x1c756}, {2, 0, 0, 0, 0x800}, {2, 24, 14, 16, 0xee10}, {2, 8, 3, 12, 0x1998}, {2, 15, 5, 9, 0x1d4f}, {2, 8, 8, 11, 0x3824}, {3, 0, 0, 0, 0x200}, {3, 11, 6, 7, 0x7b7}, {3, 7, 3, 6, 0x2fb}, {3, 3, 5, 4, 0x68b}, {3, 7, 2, 0, 0x253}, {4, 0, 0, 0, 0x4200}, {4, 5, 2, 3, 0x4275}, {4, 0, 2, 1, 0x4214}, {4, 2, 1, 1, 0x420e}, {4, 0, 2, 0, 0x4210}, {5, 0, 0, 0, 0x2000}, {5, 2, 0, 1, 0x2006}, {5, 1, 0, 0, 0x2001}, {6, 0, 0, 0, 0x2500}}; + requireThickAddresses(thickVolume(TextureTileMode::kD64KBX, 1, 100, 60, 70, 7), 1, 2, 1179648, xorDx8, "SW_64KB_D_X 8 bpp 100x60x70, 7 levels"); + constexpr ElementAddress xorDx128[] = {{0, 0, 0, 0, 0x10000}, {0, 8, 4, 2, 0x1c180}, {0, 0, 2, 1, 0x10820}, {0, 2, 3, 0, 0x10a40}, {0, 8, 4, 1, 0x1c120}, {1, 0, 0, 0, 0x8100}, {1, 3, 1, 0, 0x8350}, {1, 1, 0, 0, 0x8110}, {1, 0, 1, 0, 0x8140}, {1, 2, 0, 0, 0x8300}, {2, 0, 0, 0, 0x100}, {2, 1, 0, 0, 0x110}, {3, 0, 0, 0, 0x1000}}; + requireThickAddresses(thickVolume(TextureTileMode::kD64KBX, 77, 9, 5, 3, 4), 16, 1, 131072, xorDx128, "SW_64KB_D_X 128 bpp 9x5x3, 4 levels"); } { @@ -303,6 +327,10 @@ void RunTextureTilingTests() { requireThinAddresses(TextureTileMode::kS4KBX, 1, 100, 70, 6, 3, 73728, 24576, atS4KBX, "swizzle mode 21, 1 bytes, 100x70, 6 levels, 3 slices"); constexpr ElementAddress atD4KBX[] = {{0, 0, 0, 0, 0x2000}, {0, 32, 19, 0, 0x3030}, {0, 16, 6, 0, 0x2860}, {0, 32, 0, 0, 0x3400}, {0, 0, 19, 0, 0x2430}, {0, 0, 0, 1, 0x6800}, {0, 32, 19, 1, 0x7830}, {0, 16, 6, 1, 0x6060}, {0, 32, 0, 1, 0x7c00}, {0, 0, 19, 1, 0x6c30}, {1, 0, 0, 0, 0x1000}, {1, 15, 9, 0, 0x139c}, {1, 8, 3, 0, 0x1230}, {1, 15, 0, 0, 0x128c}, {1, 0, 9, 0, 0x1110}, {1, 0, 0, 1, 0x5800}, {1, 15, 9, 1, 0x5b9c}, {1, 8, 3, 1, 0x5a30}, {1, 15, 0, 1, 0x5a8c}, {1, 0, 9, 1, 0x5910}, {2, 0, 0, 0, 0x800}, {2, 7, 4, 0, 0x8cc}, {2, 4, 1, 0, 0x890}, {2, 7, 0, 0, 0x88c}, {2, 0, 4, 0, 0x840}, {2, 0, 0, 1, 0x4000}, {2, 7, 4, 1, 0x40cc}, {2, 4, 1, 1, 0x4090}, {2, 7, 0, 1, 0x408c}, {2, 0, 4, 1, 0x4040}, {3, 0, 0, 0, 0x600}, {3, 3, 1, 0, 0x61c}, {3, 2, 0, 0, 0x608}, {3, 3, 0, 0, 0x60c}, {3, 0, 1, 0, 0x610}, {3, 0, 0, 1, 0x4e00}, {3, 3, 1, 1, 0x4e1c}, {3, 2, 0, 1, 0x4e08}, {3, 3, 0, 1, 0x4e0c}, {3, 0, 1, 1, 0x4e10}}; requireThinAddresses(TextureTileMode::kD4KBX, 56, 33, 20, 4, 2, 32768, 16384, atD4KBX, "swizzle mode 22, 4 bytes, 33x20, 4 levels, 2 slices"); + constexpr ElementAddress atZ64KBX8[] = {{0, 0, 0, 0, 0x20000}, {1, 0, 0, 0, 0x10000}}; + requireThinAddresses(TextureTileMode::kZ64KBX, 1, 256, 256, 9, 1, 196608, 196608, atZ64KBX8, "swizzle mode 24, 1 bytes, 256x256, 9 levels, 1 slices"); + constexpr ElementAddress atZ64KBX16[] = {{0, 0, 0, 0, 0x10000}}; + requireThinAddresses(TextureTileMode::kZ64KBX, 7, 128, 128, 8, 1, 131072, 131072, atZ64KBX16, "swizzle mode 24, 2 bytes, 128x128, 8 levels, 1 slices"); } { @@ -360,4 +388,99 @@ void RunTextureTilingTests() { 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/ThreadPriority.cpp b/core/libs/prx/libSceAgcDriver/tests/ThreadPriority.cpp new file mode 100644 index 0000000000..9c43f7e6cb --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/ThreadPriority.cpp @@ -0,0 +1,85 @@ +#include "prx/libSceAgcDriver/Execution/include/ThreadPriority.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace AgcDriver; + +constexpr std::uint64_t Unlimited = std::numeric_limits::max(); +constexpr RtkitLimits Rtkit{20, 200000}; + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +void ParseTests() { + for (const char* text : {static_cast(nullptr), "", "off"}) Require(ParseThreadPriorityMode(text) == ThreadPriorityMode::Off, std::string("APS5_THREAD_PRIORITY=") + (text == nullptr ? "(unset)" : text) + " must leave priorities alone"); + Require(ParseThreadPriorityMode("rt") == ThreadPriorityMode::Realtime, "APS5_THREAD_PRIORITY=rt must ask for real-time priority"); + Require(ParseThreadPriorityMode("high") == ThreadPriorityMode::High, "APS5_THREAD_PRIORITY=high must ask for a nice level"); + for (const char* text : {"hgih", "HIGH", "realtime", "on", "1", "0", " high"}) { + bool threw = false; + try { + static_cast(ParseThreadPriorityMode(text)); + } catch (const std::invalid_argument&) { + threw = true; + } + Require(threw, std::string("APS5_THREAD_PRIORITY=") + text + " must be rejected"); + } +} + +void ExpectRefused(const RealtimePlan& plan, const std::string& what) { + Require(!plan.refusal.empty() && !plan.limit.has_value(), what + " must refuse real-time priority without touching RLIMIT_RTTIME"); +} + +void ExpectLimit(const RealtimePlan& plan, RttimeLimit expected, const std::string& what) { + Require(plan.refusal.empty() && plan.limit.has_value(), what + " must set RLIMIT_RTTIME"); + Require(*plan.limit == expected, what + " set RLIMIT_RTTIME to soft " + std::to_string(plan.limit->soft) + " hard " + std::to_string(plan.limit->hard) + ", expected soft " + std::to_string(expected.soft) + " hard " + std::to_string(expected.hard)); +} + +void PlanTests() { + ExpectRefused(PlanRealtime(10, std::nullopt, {Unlimited, Unlimited}, 200000), "an unreachable rtkit"); + ExpectRefused(PlanRealtime(10, std::nullopt, {30000, 150000}, 200000), "an unreachable rtkit"); + ExpectRefused(PlanRealtime(21, Rtkit, {Unlimited, Unlimited}, 200000), "a priority above rtkit's MaxRealtimePriority"); + ExpectRefused(PlanRealtime(10, RtkitLimits{20, 0}, {Unlimited, Unlimited}, 200000), "rtkit without an RTTimeUSecMax"); + ExpectLimit(PlanRealtime(20, Rtkit, {Unlimited, Unlimited}, 200000), {100000, 200000}, "an unlimited hard limit"); + ExpectLimit(PlanRealtime(10, Rtkit, {100000, Unlimited}, 200000), {100000, 200000}, "a lowered soft limit under an unlimited hard limit"); + ExpectLimit(PlanRealtime(10, Rtkit, {Unlimited, Unlimited}, 500000), {100000, 200000}, "APS5_THREAD_RTTIME_US above RTTimeUSecMax"); + ExpectLimit(PlanRealtime(10, Rtkit, {Unlimited, Unlimited}, 100000), {50000, 100000}, "APS5_THREAD_RTTIME_US below RTTimeUSecMax"); + ExpectLimit(PlanRealtime(10, Rtkit, {150000, 150000}, 200000), {75000, 150000}, "a hard limit rtkit accepts with a soft limit above half of it"); + const auto kept = PlanRealtime(10, Rtkit, {30000, 150000}, 200000); + Require(kept.refusal.empty() && !kept.limit.has_value(), "limits rtkit already accepts must be left alone"); +} + +void RefusalTest() { + bool threw = false; + try { + RaiseWorkerThreadPriority("test"); + } catch (const std::runtime_error& error) { + threw = true; + std::cout << "refused: " << error.what() << '\n'; + } + Require(threw, "a refused priority request must throw"); +} + +} + +int main(int argc, char** argv) { + try { + if (argc > 1 && std::string(argv[1]) == "refused") { + RefusalTest(); + return 0; + } + ParseTests(); + PlanTests(); + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp b/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp new file mode 100644 index 0000000000..a69e325898 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp @@ -0,0 +1,327 @@ +#include "Recompiler.hpp" +#include "ControlFlow/GraphBuilder.hpp" +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" +#include "Translation/InstructionTranslator.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +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; +constexpr std::uint32_t MovreldV4V0 = 0x7e088500u; +constexpr std::uint32_t MoveV1V5 = 0x7e020305u; +constexpr std::uint32_t EndProgram = 0xbf810000u; +constexpr std::uint32_t MoveV2Zero = 0x7e040280u; +constexpr std::uint32_t MoveV3Zero = 0x7e060280u; +constexpr std::uint32_t WritelaneV4 = 0xd7610004u; +constexpr std::uint32_t FromS8Lane0 = 0x00010008u; +constexpr std::uint32_t ReadlaneS8 = 0xd7600008u; +constexpr std::uint32_t FromV4Lane0 = 0x00010104u; +constexpr std::uint32_t ImageSampleLz2d = 0xf09c0f08u; +constexpr std::uint32_t SampleV8V2S0S8 = 0x00400802u; +constexpr std::uint32_t AddV8V1V8 = 0x4a101101u; +constexpr std::uint32_t BufferStoreDword = 0xe0700000u; +constexpr std::uint32_t StoreV8WithS12 = 0x80030800u; + +struct Shape { + std::size_t functionArrays = 0; + std::uint32_t arrayLength = 0; + std::size_t accessChains = 0; + std::size_t selects = 0; +}; + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +std::vector RecompileProgram(std::span code, std::span userData) { + const std::array capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformVote, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses}; + const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + 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}, 4u, {true, 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.layout = {0, 0, 0, 128}; + request.useCache = false; + auto result = Recompile(request); + Require(!result.spirv.empty(), "the relative-addressing program did not recompile"); + return std::vector(result.spirv.Words().begin(), result.spirv.Words().end()); +} + +std::vector RecompileProgram(std::span code) { + const std::array userData{0x10000000u, 0u, 0x40u, 0x31016facu}; + return RecompileProgram(code, userData); +} + +Shape Scan(std::span words) { + Shape shape; + std::vector> constants; + std::vector> arrays; + std::vector> functionPointers; + std::vector windowVariables; + 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::OpConstant && count == 4) constants.emplace_back(words[cursor + 2], words[cursor + 3]); + if (op == spv::OpTypeArray) arrays.emplace_back(words[cursor + 1], words[cursor + 3]); + if (op == spv::OpTypePointer && words[cursor + 2] == spv::StorageClassFunction) functionPointers.emplace_back(words[cursor + 1], words[cursor + 3]); + if (op == spv::OpVariable && words[cursor + 3] == spv::StorageClassFunction) { + for (const auto& [pointer, pointee] : functionPointers) { + if (pointer != words[cursor + 1]) continue; + for (const auto& [array, length] : arrays) { + if (array != pointee) continue; + ++shape.functionArrays; + windowVariables.push_back(words[cursor + 2]); + for (const auto& [constant, value] : constants) { + if (constant == length) shape.arrayLength = value; + } + } + } + } + if (op == spv::OpAccessChain) { + for (const auto variable : windowVariables) { + if (words[cursor + 3] == variable) ++shape.accessChains; + } + } + if (op == spv::OpSelect) ++shape.selects; + cursor += count; + } + 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; + compute.threadsNum[0] = waveSize; + ShaderVertexInputInfo vertex{}; + vertex.mesh.threadsNum[0] = waveSize; + vertex.mesh.inputPrimitive = 1u; + vertex.mesh.primitivesPerGroup = 1u; + vertex.mesh.verticesPerGroup = 1u; + vertex.mesh.esgsItemSize = 1u; + ShaderPixelInputInfo pixel{}; + std::uint32_t next = 0u; + for (const auto input : {PixelInput::PerspectiveCenter, PixelInput::PerspectiveCentroid, PixelInput::LinearCenter, PixelInput::LinearCentroid, + PixelInput::PositionX, PixelInput::PositionY, PixelInput::PositionZ, PixelInput::PositionW, + PixelInput::FrontFace, PixelInput::Ancillary, PixelInput::LineStipple, PixelInput::PositionFixedPoint}) { + pixel.psInputVgpr[static_cast(input)] = next; + next += PixelInputVgprCount(input); + } + std::vector code{MoveM0Two}; + if (fetchCount != 0u) { + code.push_back(0xbe802102u); + code.push_back(0x7e008700u | base); + code.push_back(0x7e1002f2u); + vertex.resourcesNum = 1; + vertex.resourcesDst[0] = {static_cast(fetchStart), static_cast(fetchCount), 0, 0u}; + } + code.push_back(0x7e008700u | base); + code.push_back(EndProgram); + const auto decoded = RdnaInstructionDecoder{}.Decode(code); + if (fetchCount != 0u) { + const auto& call = decoded.instructions.at(1); + Require(call.op == RdnaOpcode::SSwappcB64 && call.family == RdnaInstructionFamily::SOP1 && call.opcodeId == 0x21u && + call.destination.kind == RdnaOperandKind::ScalarRegister && call.destination.reg == 0u && + call.source0.kind == RdnaOperandKind::ScalarRegister && call.source0.reg == 2u, + "external fetch fixture did not decode s_swappc_b64 s[0:1], s[2:3]"); + } + const SwappcInfo swappc{fetchCount != 0u, 0u, 8u}; + const auto graph = GraphBuilder{}.Build(decoded, &swappc); + Require(graph.hasFetchCall == (fetchCount != 0u), "stage fixture did not identify its external fetch call"); + TranslateOptions options{}; + options.stage = stage; + options.waveSize = waveSize; + options.userDataCount = 8u; + options.inputInfo = {&vertex, &pixel, &compute}; + return InstructionTranslator{}.Translate(decoded, graph, options); +} + +void RequireWindow(const IrProgram& program, std::uint32_t base, std::uint32_t limit, std::span initialized) { + Require(program.Metadata().vectorWindowBase == base && program.Metadata().vectorWindowSize == limit - base, "relative window does not include exactly the initialized stage registers"); + std::array stored{}; + for (const auto* instruction : program.BlockOrder().front()->Instructions()) { + if (instruction->Opcode() == IrOpcode::StoreVectorWindow) { + Require(instruction->Argument(0)->HasImmediate(), "stage input uses a dynamic window index"); + const auto index = base + instruction->Argument(0)->ImmediateU32(); + Require(index < limit, "stage input lies past the window"); + stored[index] = true; + } else if (instruction->Opcode() == IrOpcode::SetVectorRegister) { + const auto index = instruction->Argument(0)->Register().index; + Require(index < base || !program.Metadata().vectorWindowIndexedWrites, "stage input escaped the relative window into SSA"); + if (index < base) stored[index] = true; + } + } + for (const auto index : initialized) Require(stored[index], "stage input v" + std::to_string(index) + " was not initialized"); + bool bounded = false; + for (const auto& block : program.Blocks()) { + for (const auto* instruction : block->Instructions()) { + if (instruction->Opcode() == IrOpcode::ULessThan32 && instruction->Argument(1)->HasImmediate() && instruction->Argument(1)->ImmediateU32() == limit - base) bounded = true; + } + } + Require(bounded, "relative access did not use the final stage window bound"); +} + +template +bool RunStage(const char* name, TAction action) { + try { + action(); + std::printf("vector window stage inputs: %s passed\n", name); + return true; + } catch (const std::exception& error) { + std::fprintf(stderr, "vector window stage inputs: %s: %s\n", name, error.what()); + return false; + } +} + +bool StageInputs() { + bool passed = true; + const auto stage = [&](const char* name, ShaderStageKind kind, std::uint32_t limit, std::initializer_list initialized) { + passed &= RunStage(name, [&] { + for (const auto wave : {32u, 64u}) RequireWindow(TranslateStage(kind, wave), 0u, limit, {initialized.begin(), initialized.size()}); + }); + }; + stage("compute", ShaderStageKind::Compute, 3u, {0u, 1u, 2u}); + stage("vertex", ShaderStageKind::Vertex, 9u, {5u, 8u}); + stage("local", ShaderStageKind::Local, 6u, {2u, 3u, 5u}); + stage("tessellation control", ShaderStageKind::TessellationControl, 2u, {0u, 1u}); + stage("tessellation evaluation", ShaderStageKind::TessellationEvaluation, 9u, {0u, 5u, 6u, 7u, 8u}); + stage("mesh", ShaderStageKind::Mesh, 9u, {0u, 1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u}); + stage("pixel", ShaderStageKind::Pixel, 16u, {0u, 1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u, 9u, 10u, 11u, 12u, 13u, 14u, 15u}); + passed &= RunStage("SSA prefix", [] { + const std::array local{2u, 3u, 5u}; + const std::array vertex{5u, 8u}; + for (const auto wave : {32u, 64u}) { + RequireWindow(TranslateStage(ShaderStageKind::Local, wave, 4u), 4u, 6u, local); + RequireWindow(TranslateStage(ShaderStageKind::Vertex, wave, 16u), 16u, 17u, vertex); + } + }); + passed &= RunStage("external fetch", [] { + const std::array vertex{5u, 8u}; + for (const auto wave : {32u, 64u}) { + for (const auto start : {20u, 252u}) { + for (const auto count : {1u, 2u, 3u, 4u}) { + const auto program = TranslateStage(ShaderStageKind::Vertex, wave, 0u, start, count); + RequireWindow(program, 0u, start + count, vertex); + std::uint32_t fetched = 0u; + std::uint32_t boundedReads = 0u; + for (const auto& block : program.Blocks()) { + for (const auto* instruction : block->Instructions()) { + if (instruction->Opcode() == IrOpcode::StoreVectorWindow) { + const auto* value = instruction->Argument(1); + if (value->Opcode() == IrOpcode::SelectU32) value = value->Argument(1); + if (value->Opcode() == IrOpcode::GetAttribute) { + Require(instruction->Argument(0)->ImmediateU32() == start + fetched && value->Argument(1)->ImmediateU32() == fetched, + "external fetch did not initialize its entire window span"); + ++fetched; + } + } + if (instruction->Opcode() == IrOpcode::ULessThan32 && instruction->Argument(1)->HasImmediate() && instruction->Argument(1)->ImmediateU32() == start + count) ++boundedReads; + } + } + Require(fetched == count && boundedReads == 2u, "relative accesses to the external fetch disagree about the window"); + } + } + } + }); + if (passed) std::puts("Vector window stage input and fetch tests passed"); + return passed; +} + +bool run(const char* name, bool (*test)()) { + try { + if (test()) return true; + std::fprintf(stderr, "%s\n", name); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s: %s\n", name, error.what()); + } + return false; +} + +} + +int main(int argc, char** argv) { + if (argc == 2 && std::string_view(argv[1]) == "--stage-inputs-only") return StageInputs() ? 0 : 1; + if (argc != 1) return 2; + bool passed = true; + + passed &= run("observable relative reads and writes index a function-scope window", [] { + const std::array code{MoveM0GroupX, MoveV4One, MoveV5Two, MoveV6Four, MovrelsV1V4, + 0xe0700000u, 0x80000100u, MovreldV4V0, MoveV1V5, 0xe0700004u, 0x80000100u, EndProgram}; + const auto shape = Scan(RecompileProgram(code)); + if (shape.functionArrays != 1 || shape.arrayLength != 3 || shape.accessChains < 2) std::fprintf(stderr, " window shape: arrays %zu length %u access chains %zu selects %zu\n", shape.functionArrays, shape.arrayLength, shape.accessChains, shape.selects); + return shape.functionArrays == 1 && shape.arrayLength == 3 && shape.accessChains >= 2; + }); + + passed &= run("a program without relative addressing keeps its registers in SSA values", [] { + const std::array code{MoveV4One, MoveV5Two, MoveV6Four, MoveV1V5, 0xe0700000u, 0x80000100u, EndProgram}; + const auto shape = Scan(RecompileProgram(code)); + return shape.functionArrays == 0 && shape.accessChains == 0; + }); + + passed &= run("a sampler dword spilled into a relatively read register keeps its definition through v_readlane", [] { + const std::array code{MoveM0GroupX, MoveV2Zero, MoveV3Zero, WritelaneV4, FromS8Lane0, MovrelsV1V4, ReadlaneS8, FromV4Lane0, ImageSampleLz2d, SampleV8V2S0S8, AddV8V1V8, BufferStoreDword, StoreV8WithS12, EndProgram}; + const std::array userData{}; + const auto shape = Scan(RecompileProgram(code, userData)); + return shape.functionArrays == 1; + }); + + 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 new file mode 100644 index 0000000000..2b0313c64d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/VertexBarrier.cpp @@ -0,0 +1,78 @@ +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include + +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}; + +ShaderRecompiler::RecompileRequest vertexRequest(std::span code) { + using namespace ShaderRecompiler; + RecompileRequest request{}; + request.shader = {ShaderStage::Vertex, 0x10000u, code, 0, {}}; + request.context.waveSize = 32; + request.context.vertex = ShaderVertexStageInfo{}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32; + request.target.supportedCapabilities = VertexCapabilities; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + return request; +} + +bool hasControlBarrier(const ShaderRecompiler::RecompileResult& result) { + 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::OpControlBarrier) return true; + } + return false; +} + +void verifyBarrierWithoutMemory() { + const auto result = ShaderRecompiler::Recompile(vertexRequest(BarrierExport)); + 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)); + } catch (const std::exception& error) { + if (std::string(error.what()).find("s_barrier in a vertex program that accesses LDS or GDS or writes memory is not implemented") != std::string::npos) return; + throw; + } + throw std::runtime_error("a vertex program with s_barrier and an LDS write compiled"); +} + +} // namespace + +int main() { + try { + verifyBarrierWithoutMemory(); + verifyBarrierBeforeLds(); + verifyBarrierWithLds(); + std::cout << "vertex barrier tests passed\n"; + return 0; + } catch (const std::exception& error) { + const std::string message(error.what()); + std::cerr << message.substr(0, message.find("RecompileRequest:")) << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/VertexInput.cpp b/core/libs/prx/libSceAgcDriver/tests/VertexInput.cpp new file mode 100644 index 0000000000..f0f999a04c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/VertexInput.cpp @@ -0,0 +1,294 @@ +#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]) == 64, "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].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..bd6a291b22 --- /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, 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/VertexLayerExport.cpp b/core/libs/prx/libSceAgcDriver/tests/VertexLayerExport.cpp new file mode 100644 index 0000000000..1b8f715065 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/VertexLayerExport.cpp @@ -0,0 +1,95 @@ +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct Module { + std::vector capabilities; + std::vector extensions; + bool layerOutput = false; + std::uint32_t version = 0; +}; + +Module inspect(std::span words) { + Module module; + if (words.size() > 1) module.version = words[1]; + std::vector layerIds; + for (std::size_t at = 5; at < words.size();) { + const auto length = words[at] >> 16u; + const auto opcode = words[at] & 0xffffu; + if (length == 0 || at + length > words.size()) break; + if (opcode == 17u && length == 2) module.capabilities.push_back(words[at + 1]); + if (opcode == 10u && length >= 2) module.extensions.emplace_back(reinterpret_cast(&words[at + 1])); + if (opcode == 71u && length == 4 && words[at + 2] == 11u && words[at + 3] == 9u) module.layerOutput = true; + at += length; + } + return module; +} + +bool has(std::span values, std::uint32_t value) { + for (const auto entry : values) { + if (entry == value) return true; + } + return false; +} + +bool named(const std::vector& values, std::string_view value) { + for (const auto entry : values) { + if (entry == value) return true; + } + return false; +} + +Module recompile(std::uint32_t control, bool layerExtension) { + using namespace ShaderRecompiler; + static const std::array code{0xf80000cfu, 0x00000000u, 0xf80008dfu, 0x01010101u, 0xbf810000u}; + static const std::array withExtension{1u, 32u, 5254u}; + static const std::array without{1u, 32u}; + ShaderVertexStageInfo vertex{}; + vertex.paClVsOutCntl = control; + RecompileRequest request{}; + request.shader = {ShaderStage::Vertex, 0x30000u, code, 0, {}}; + request.context.waveSize = 64; + request.context.vertex = vertex; + request.target.vulkanVersion = layerExtension ? 0x00401000u : 0x00402000u; + request.target.spirvVersion = layerExtension ? 0x00010300u : 0x00010500u; + request.target.subgroupSize = 64; + if (layerExtension) request.target.supportedCapabilities = withExtension; + else request.target.supportedCapabilities = without; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + return inspect(Recompile(request).spirv); +} + +} + +int main() { + try { + constexpr std::uint32_t layer = (1u << 18u) | (1u << 21u); + const auto extension = recompile(layer, true); + if (!extension.layerOutput || !has(extension.capabilities, 5254u) || has(extension.capabilities, 69u) || !named(extension.extensions, "SPV_EXT_shader_viewport_index_layer") || extension.version != 0x00010300u) { + std::fprintf(stderr, "a vertex layer export with VK_EXT_shader_viewport_index_layer: layer %d, version 0x%x\n", extension.layerOutput ? 1 : 0, extension.version); + return 1; + } + const auto core = recompile(layer, false); + if (!core.layerOutput || !has(core.capabilities, 69u) || has(core.capabilities, 5254u) || core.version != 0x00010500u) { + std::fprintf(stderr, "a vertex layer export without the extension: layer %d, version 0x%x\n", core.layerOutput ? 1 : 0, core.version); + return 1; + } + const auto ignored = recompile(0u, true); + if (ignored.layerOutput || has(ignored.capabilities, 5254u)) { + std::fprintf(stderr, "a misc export without USE_VTX_RENDER_TARGET_INDX wrote gl_Layer\n"); + return 1; + } + } catch (const std::exception& error) { + std::fprintf(stderr, "unexpected error: %s\n", error.what()); + return 1; + } + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/Vop2EncodingDecode.cpp b/core/libs/prx/libSceAgcDriver/tests/Vop2EncodingDecode.cpp new file mode 100644 index 0000000000..83f4a3c52c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/Vop2EncodingDecode.cpp @@ -0,0 +1,82 @@ +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +namespace { + +struct Accepted { + std::vector words; + RdnaOpcode op; + const char* name; +}; + +struct Refused { + std::vector words; + const char* reason; + const char* name; +}; + +} + +int main() { + const std::vector accepted{ + {{0x2c040300u}, RdnaOpcode::VLshrrevB32, "v_lshrrev_b32"}, + {{0x6e040300u, 0x00003c00u}, RdnaOpcode::VFmamkF16, "v_fmamk_f16"}, + {{0x78040300u}, RdnaOpcode::VPkFmacF16, "v_pk_fmac_f16"}, + {{0xd5160002u, 0x00020300u}, RdnaOpcode::VLshrrevB32, "VOP3 v_lshrrev_b32"}, + {{0xd7640002u, 0x00020300u}, RdnaOpcode::VBcntU32B32, "VOP3 v_bcnt_u32_b32"}, + {{0xd7650002u, 0x00020300u}, RdnaOpcode::VMbcntLoU32B32, "VOP3 v_mbcnt_lo_u32_b32"}, + {{0xd7660002u, 0x00020300u}, RdnaOpcode::VMbcntHiU32B32, "VOP3 v_mbcnt_hi_u32_b32"}, + }; + const std::vector refused{ + {{0x2a040300u}, "VOP2 opcode is not implemented", "VOP2 opcode 0x15"}, + {{0x2e040300u}, "VOP2 opcode is not implemented", "VOP2 opcode 0x17"}, + {{0x32040300u}, "VOP2 opcode is not implemented", "VOP2 opcode 0x19"}, + {{0x44040300u}, "VOP2 opcode is not implemented", "VOP2 opcode 0x22"}, + {{0x46040300u}, "VOP2 opcode is not implemented", "VOP2 opcode 0x23"}, + {{0x48040300u}, "VOP2 opcode is not implemented", "VOP2 opcode 0x24"}, + {{0xd5150002u, 0x00020300u}, "VOP3 opcode is not implemented", "VOP3 opcode 0x115"}, + {{0xd5170002u, 0x00020300u}, "VOP3 opcode is not implemented", "VOP3 opcode 0x117"}, + {{0xd5190002u, 0x00020300u}, "VOP3 opcode is not implemented", "VOP3 opcode 0x119"}, + {{0xd5220002u, 0x00020300u}, "VOP3 opcode is not implemented", "VOP3 opcode 0x122"}, + {{0xd5230002u, 0x00020300u}, "VOP3 opcode is not implemented", "VOP3 opcode 0x123"}, + {{0xd5240002u, 0x00020300u}, "VOP3 opcode is not implemented", "VOP3 opcode 0x124"}, + {{0xd5370002u, 0x00020300u}, "VOP3 opcode is not implemented", "VOP3 opcode 0x137"}, + {{0xd5380002u, 0x00020300u}, "VOP3 opcode is not implemented", "VOP3 opcode 0x138"}, + {{0xd53c0002u, 0x00020300u}, "VOP3 opcode is not implemented", "VOP3 opcode 0x13c"}, + }; + int failures = 0; + for (const auto& test : accepted) { + auto code = test.words; + code.push_back(0xbf810000u); + try { + const auto decoded = RdnaInstructionDecoder{}.Decode(code); + if (decoded.instructions.empty() || decoded.instructions[0].op != test.op || decoded.instructions[0].wordCount != test.words.size()) { + std::fprintf(stderr, "%s decodes to the wrong opcode or length\n", test.name); + ++failures; + } + } catch (const std::exception& error) { + std::fprintf(stderr, "%s was refused: %s\n", test.name, error.what()); + ++failures; + } + } + for (const auto& test : refused) { + auto code = test.words; + code.push_back(0xbf810000u); + std::string refusal; + try { + static_cast(RdnaInstructionDecoder{}.Decode(code)); + } catch (const std::exception& error) { + refusal = error.what(); + } + if (refusal.find(test.reason) == std::string::npos) { + std::fprintf(stderr, "%s was not refused\n", test.name); + ++failures; + } + } + return failures == 0 ? 0 : 1; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/Vop3InterpDecode.cpp b/core/libs/prx/libSceAgcDriver/tests/Vop3InterpDecode.cpp new file mode 100644 index 0000000000..52027341ca --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/Vop3InterpDecode.cpp @@ -0,0 +1,125 @@ +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" +#include +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +namespace { + +struct Accepted { + std::vector words; + RdnaOpcode op; + const char* name; + RdnaOperandKind source0Kind; + std::uint32_t source0Value; + std::uint32_t attribute; + std::uint32_t channel; + bool absolute; + bool negate; + bool clamp; + std::uint32_t omod; +}; + +struct Refused { + std::vector words; + const char* reason; + const char* name; +}; + +} + +int main() { + constexpr std::uint32_t Destination = 5; + const std::vector accepted{ + {{0xd6000005u, 0x02020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr0.x", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, false, false, false, 0u}, + {{0xd6010005u, 0x02020440u}, RdnaOpcode::VInterpP2F32, "v_interp_p2_f32_e64 v5, v2, attr0.y", RdnaOperandKind::VectorRegister, 2u, 0u, 1u, false, false, false, 0u}, + {{0xd6020005u, 0x02000200u}, RdnaOpcode::VInterpMovF32, "v_interp_mov_f32_e64 v5, p20, attr0.x", RdnaOperandKind::IntegerInlineConstant, 1u, 0u, 0u, false, false, false, 0u}, + {{0xd6000005u, 0x0202041fu}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr31.x", RdnaOperandKind::VectorRegister, 2u, 31u, 0u, false, false, false, 0u}, + {{0xd6000005u, 0x02020420u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr32.x", RdnaOperandKind::VectorRegister, 2u, 32u, 0u, false, false, false, 0u}, + {{0xd6000005u, 0x0203fe00u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v255, attr0.x", RdnaOperandKind::VectorRegister, 255u, 0u, 0u, false, false, false, 0u}, + {{0xd6000205u, 0x02020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, |v2|, attr0.x", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, true, false, false, 0u}, + {{0xd6000005u, 0x42020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, -v2, attr0.x", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, false, true, false, 0u}, + {{0xd6008005u, 0x02020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr0.x clamp", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, false, false, true, 0u}, + {{0xd6000005u, 0x0a020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr0.x mul:2", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, false, false, false, 1u}, + {{0xd6000005u, 0x12020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr0.x mul:4", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, false, false, false, 2u}, + {{0xd6000005u, 0x1a020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr0.x div:2", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, false, false, false, 3u}, + {{0xd6000205u, 0x42020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, -|v2|, attr0.x", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, true, true, false, 0u}, + {{0xd6000205u, 0x5a020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, -|v2|, attr0.x div:2", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, true, true, false, 3u}, + {{0xd6010005u, 0x42020481u}, RdnaOpcode::VInterpP2F32, "v_interp_p2_f32_e64 v5, -v2, attr1.z", RdnaOperandKind::VectorRegister, 2u, 1u, 2u, false, true, false, 0u}, + {{0xd6018205u, 0x02020400u}, RdnaOpcode::VInterpP2F32, "v_interp_p2_f32_e64 v5, |v2|, attr0.x clamp", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, true, false, true, 0u}, + {{0xd6028005u, 0x02000000u}, RdnaOpcode::VInterpMovF32, "v_interp_mov_f32_e64 v5, p10, attr0.x clamp", RdnaOperandKind::IntegerInlineConstant, 0u, 0u, 0u, false, false, true, 0u}, + {{0xd6020005u, 0x0a000000u}, RdnaOpcode::VInterpMovF32, "v_interp_mov_f32_e64 v5, p10, attr0.x mul:2", RdnaOperandKind::IntegerInlineConstant, 0u, 0u, 0u, false, false, false, 1u}, + }; + const std::vector refused{ + {{0xd6000005u, 0x02000400u}, "not a vector register", "v_interp_p1_f32_e64 with a non-VGPR coordinate"}, + {{0xd6020005u, 0x02000600u}, "not an interpolation parameter", "v_interp_mov_f32_e64 with a reserved parameter"}, + {{0xd6000005u, 0x02060400u}, "third source is not the inline constant 0", "v_interp_p1_f32_e64 with a third source other than the inline constant 0"}, + {{0xd6000805u, 0x02020400u}, "modifiers are not implemented", "v_interp_p1_f32_e64 with op_sel"}, + {{0xd6000005u, 0x02020500u}, "modifiers are not implemented", "v_interp_p1_f32_e64 with the packed-half selector"}, + {{0xd6000105u, 0x02020400u}, "modifiers are not implemented", "v_interp_p1_f32_e64 with an absolute source 0"}, + {{0xd6020005u, 0x22000200u}, "modifiers are not implemented", "v_interp_mov_f32_e64 with a negated parameter"}, + {{0xd7424005u, 0x00020201u}, "modifiers are not implemented", "v_interp_p1ll_f16 with the high-half destination selector"}, + }; + int failures = 0; + for (const auto& test : accepted) { + auto code = test.words; + code.push_back(0xbf810000u); + try { + const auto decoded = RdnaInstructionDecoder{}.Decode(code); + if (decoded.instructions.empty()) { + std::fprintf(stderr, "%s decoded no instruction\n", test.name); + ++failures; + continue; + } + const auto& instruction = decoded.instructions[0]; + const auto source0Value = test.source0Kind == RdnaOperandKind::VectorRegister ? instruction.source0.reg : instruction.source0.value; + if (instruction.op != test.op || instruction.wordCount != test.words.size() || instruction.family != RdnaInstructionFamily::VOP3 || + instruction.destination.kind != RdnaOperandKind::VectorRegister || instruction.destination.reg != Destination || + instruction.destination.clamp != test.clamp || instruction.destination.omod != test.omod || instruction.sourceCount != 3u || + instruction.source0.kind != test.source0Kind || source0Value != test.source0Value || + instruction.source0.absolute != test.absolute || instruction.source0.negate != test.negate || + instruction.source1.kind != RdnaOperandKind::IntegerInlineConstant || instruction.source1.value != test.attribute || + instruction.source2.kind != RdnaOperandKind::IntegerInlineConstant || instruction.source2.value != test.channel) { + std::fprintf(stderr, "%s decoded to the wrong opcode or operands\n", test.name); + ++failures; + } + } catch (const std::exception& error) { + std::fprintf(stderr, "%s was refused: %s\n", test.name, error.what()); + ++failures; + } + } + for (const auto& test : refused) { + auto code = test.words; + code.push_back(0xbf810000u); + std::string refusal; + try { + static_cast(RdnaInstructionDecoder{}.Decode(code)); + } catch (const std::exception& error) { + refusal = error.what(); + } + if (refusal.find(test.reason) == std::string::npos) { + std::fprintf(stderr, "%s was not refused\n", test.name); + ++failures; + } + } + for (const std::uint32_t selector : {0x0u, 0x4000u}) { + const std::vector code{0xd75a0005u | selector, 0x041e0201u, 0xbf810000u}; + try { + const auto decoded = RdnaInstructionDecoder{}.Decode(code); + const auto& instruction = decoded.instructions.at(0); + const std::uint32_t half = selector != 0u ? 5u : 6u; + if (instruction.op != RdnaOpcode::VInterpP2F16 || instruction.destination.reg != Destination || instruction.destination.sdwaSel != half || + instruction.sourceCount != 4u || instruction.source0.reg != 1u || instruction.source1.value != 1u || instruction.source3.reg != 7u) { + std::fprintf(stderr, "v_interp_p2_f16 v5, v1, attr1.x, v7%s decoded to the wrong opcode or operands\n", selector != 0u ? " op_sel:[0,0,0,1]" : ""); + ++failures; + } + } catch (const std::exception& error) { + std::fprintf(stderr, "v_interp_p2_f16 v5, v1, attr1.x, v7%s was refused: %s\n", selector != 0u ? " op_sel:[0,0,0,1]" : "", error.what()); + ++failures; + } + } + return failures == 0 ? 0 : 1; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/WaitShutdown.cpp b/core/libs/prx/libSceAgcDriver/tests/WaitShutdown.cpp new file mode 100644 index 0000000000..ee2a3fb221 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/WaitShutdown.cpp @@ -0,0 +1,149 @@ +#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" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct alignas(64) LabelState { + volatile std::uint32_t label = 0; + volatile std::uint32_t entered = 0; + volatile std::uint32_t after = 0; + volatile std::uint32_t queued = 0; +}; + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +void DelayExit() { + std::this_thread::sleep_for(std::chrono::seconds(1)); +} + +std::array WriteData(volatile std::uint32_t* destination) { + const auto address = static_cast(reinterpret_cast(destination)); + return {0xc0033700, 0x00100200, static_cast(address), static_cast(address >> 32u), 1}; +} + +std::vector WaitingCommands(LabelState& state) { + const auto address = static_cast(reinterpret_cast(&state.label)); + const auto entered = WriteData(&state.entered); + const std::array wait{0xc0053c00, 0x13, static_cast(address), static_cast(address >> 32u), 1, 0xffffffffu, 0x19}; + const auto after = WriteData(&state.after); + std::vector words(entered.begin(), entered.end()); + words.insert(words.end(), wait.begin(), wait.end()); + words.insert(words.end(), after.begin(), after.end()); + return words; +} + +void Submit(std::uint32_t queue, const std::vector& words) { + Packet packet{const_cast(words.data()), static_cast(words.size()), 0, {}}; + const auto result = queue == 0 ? sceAgcDriverSubmitDcb(&packet) : sceAgcDriverSubmitAcb(queue, &packet); + 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}; + for (std::size_t index = 0; index < queues.size(); ++index) { + Submit(queues[index], WaitingCommands(states[index])); + const auto queued = WriteData(&states[index].queued); + Submit(queues[index], {queued.begin(), queued.end()}); + } + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + for (const auto& state : states) { + while (state.entered != 1) { + Require(std::chrono::steady_clock::now() < deadline, "queue did not reach its unsatisfied wait"); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + } + std::this_thread::sleep_for(std::chrono::seconds(1)); + for (const auto& state : states) { + Require(state.label == 0, "unsatisfied label was unexpectedly signalled"); + Require(state.after == 0 && state.queued == 0, "unsatisfied wait completed before shutdown"); + } + const auto started = std::chrono::steady_clock::now(); + LibcRunShutdown_nid_postfix(); + Require(std::chrono::steady_clock::now() - started < std::chrono::seconds(2), "shutdown did not promptly cancel waiting queues"); + for (const auto& state : states) { + Require(state.label == 0, "shutdown signalled an unsatisfied label"); + Require(state.after == 0, "shutdown executed commands after an unsatisfied wait"); + Require(state.queued == 0, "shutdown executed a pending submission behind an unsatisfied wait"); + } + LibcRunShutdown_nid_postfix(); +} + +} + +int main(int argc, char** argv) { + if (argc == 2 && std::strcmp(argv[1], "--resolution-shutdown") == 0) { + AgcDriverWaitIdle_nid_postfix(); + LibcRegisterShutdown_nid_postfix(DelayExit); + LibcRequestExit_nid_postfix(0); + AgcDriverResolveShaderAbi_nid_postfix(nullptr, {}, {}); + return 1; + } + try { + CheckCommandBufferSubmission(); + CheckShutdownCancellation(); + std::puts("unsatisfied label waits cancelled cleanly at shutdown"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + try { + LibcRunShutdown_nid_postfix(); + } catch (const std::exception& shutdown) { + std::fprintf(stderr, "shutdown: %s\n", shutdown.what()); + } + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/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 b924f0d5c6..700455ddd0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libc/include/GuestAllocations.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/GuestWriteWatch.hpp" #ifdef _WIN32 @@ -6,15 +7,20 @@ #define NOMINMAX #endif #include +#include #else #include +#include #endif #include #include #include #include +#include #include #include +#include +#include #include #include @@ -23,11 +29,81 @@ namespace { 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); @@ -95,6 +171,180 @@ void CheckSharedBlock() { if (!Watched(outside, unwatched.size())) Require(StoredOver(outside, unwatched.size(), TrackerGeneration()), "an unwatched range reads as not 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(); + 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 CheckOwnStore() { void* memory = AllocateWatched(2 * Block); const auto base = reinterpret_cast(memory); @@ -152,7 +402,178 @@ void CheckUnwatch() { Require(!UnchangedSince(base, Block, adjacent), "unwatch hid a CPU edit in unrelated memory"); } +void CheckNewestStamp() { + void* memory = AllocateWatched(4 * Block); + const auto base = reinterpret_cast(memory); + const auto first = base + Block; + const auto second = base + 2 * Block; + std::memset(memory, 0x11, 4 * Block); + BumpCollectEpoch(); + CollectWrites(base, 4 * Block); + Unwatch(first, 2 * Block); + const auto firstBefore = NewestStamp(first, Block); + const auto secondBefore = NewestStamp(second, Block); + MarkWritten(second + 16, 4); + Require(NewestStamp(second, Block) > secondBefore, "a driver store into unwatched memory did not advance its stamp"); + Require(NewestStamp(first, Block) == firstBefore, "a driver store advanced the stamp of other unwatched memory"); + const auto generation = TrackerGeneration(); + static_cast(memory)[0] = 0x22; + CollectWritesUncached(base, Block); + Require(TrackerGeneration() > generation && NewestStamp(first, Block) == firstBefore, "a CPU store into watched memory advanced the stamp of unwatched memory"); + StoreOwnBytes(first, 1, [&] { *reinterpret_cast(first) = 0x42; }); + Require(NewestStamp(first, Block) > firstBefore, "an own store into unwatched memory did not advance its stamp"); + const auto partial = NewestStamp(base + 3 * Block - 8, Block + 8); + static_cast(memory)[3 * Block] = 0x33; + CollectWritesUncached(base + 3 * Block, Block); + 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; + HANDLE section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, bytes, nullptr); + Require(section != nullptr, "cannot create shared import backing"); + auto* memory = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block)); + Require(memory != nullptr, "cannot allocate shared import address"); + GuestArena::GuestArenaMap_nid_postfix(memory, bytes, section, 0, PAGE_READWRITE); + const auto base = reinterpret_cast(memory); + const auto protection = [&](std::size_t offset) { + MEMORY_BASIC_INFORMATION info{}; + Require(VirtualQuery(memory + offset, &info, sizeof(info)) == sizeof(info), "cannot query shared import protection"); + return info.Protect; + }; + Require(ImportWatched(base, bytes, [] { return true; }), "fresh shared import failed"); + Require(protection(0) == PAGE_READWRITE && protection(bytes - 1) == PAGE_READWRITE, "fresh import armed shared pages"); + const auto gpu = MarkWritten(base + 4096, 4); + Require(gpu != 0 && protection(0) == PAGE_READONLY && protection(Block) == PAGE_READWRITE, "GPU write did not activate only its block"); + CollectWritesUncached(base, Block); + Require(!WrittenSince(base + 4096, 4, gpu) && !StoredOver(base + 4096, 4, gpu), "first collect overwrote a GPU generation"); + static_cast(memory)[4096] = 0x44; + CollectWritesUncached(base, Block); + Require(WrittenSince(base + 4096, 4, gpu) && StoredOver(base + 4096, 4, gpu), "CPU store after GPU write was missed"); + + const auto second = base + Block; + const auto before = CollectWritesUncached(second, 4096); + Require(MarkWritten(second, 4) != 0, "GPU write next to fresh pages failed"); + Require(UnchangedSinceCollected(second, 4, before), "fresh neighbors were stamped as new CPU writes over a collected page"); + Require(StoredOver(second + 8 * 4096, 4096, before), "fresh neighbor history was lost"); + const std::array generations{before}; + std::array changed{}; + std::array cpu{}; + Require(ChangedBlocks(second, Block, generations, changed, cpu) && changed[0] == BlockWritten && cpu[0] == 0, "fresh collection set the block's CPU stamp"); + const auto generation = TrackerGeneration(); + Require(MarkWritten(second, 4) == generation + 1, "an already collected GPU block advanced twice"); + + const auto third = base + 2 * Block; + const auto cached = CollectWritesUncached(third, Block); + Require(ImportWatched(third, Block, [&] { + static_cast(memory)[2 * Block] = 0x55; + return true; + }), "tracked import window failed"); + Require(!UnchangedSince(third, Block, cached) && WrittenSince(third, Block, cached), "a store of new bytes within a tracked import window was missed"); + const auto afterImport = CollectWritesUncached(third, Block); + static_cast(memory)[2 * Block] = 0x66; + Require(ImportWatched(third, Block, [] { return true; }) && WrittenSince(third, Block, afterImport), "pre-import CPU store was not collected"); + + GuestArena::GuestArenaReset_nid_postfix(memory + 3 * Block, 16384); + const auto edge = base + 3 * Block + 16384; + Require(ImportWatched(edge, Block - 16384, [] { return true; }), "unaligned shared import failed"); + const auto edgeGpu = MarkWritten(edge, 4); + Require(edgeGpu != 0, "fresh collection rejected an unmapped block neighbor"); + CollectWritesUncached(edge, Block - 16384); + Require(!WrittenSince(edge, 4, edgeGpu), "edge block collection overwrote GPU results"); + GuestArena::GuestArenaReset_nid_postfix(memory, bytes); + GuestArena::GuestArenaRelease_nid_postfix(memory, bytes); + CloseHandle(section); +} + + void CheckPrivateMappingReuse() { void* memory = AllocateWatched(3 * Block); const auto base = reinterpret_cast(memory); @@ -184,6 +605,310 @@ void CheckPrivateMappingReuse() { GuestArena::GuestArenaReset_nid_postfix(replacement, 3 * Block); 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)); + GuestArena::GuestArenaCommit_nid_postfix(const_cast(memory), bytes, PAGE_READWRITE, 16384); + struct Cleanup { + void* memory; + ~Cleanup() { + GuestArena::GuestArenaReset_nid_postfix(memory, bytes); + GuestArena::GuestArenaRelease_nid_postfix(memory, bytes); + } + } cleanup{const_cast(memory)}; + const auto base = reinterpret_cast(memory); + const auto hinted = base + Block; + constexpr std::size_t hintedBytes = 2 * Block; + for (std::size_t offset = 0; offset < bytes; offset += 4096) memory[offset] = 0x11; + BumpCollectEpoch(); + Require(CollectWrites(base, bytes) != 0 && UntrackedHint(base, bytes) == 0, "watched memory has an untracked hint"); + Unwatch(hinted, hintedBytes); + const auto stamp = NewestStamp(hinted, hintedBytes); + const auto first = UntrackedHint(hinted, hintedBytes); + Require(first != 0, "memory that left the watch has no untracked hint"); + Require(UntrackedHint(base, bytes) == 0 && UntrackedHint(hinted - 4096, 8192) == 0, "a range with watched blocks has an untracked hint"); + std::array host{}; + Require(UntrackedHint(reinterpret_cast(host.data()), host.size()) == 0, "memory outside the arena has an untracked hint"); + Require(!Watched(hinted, hintedBytes) && CollectWrites(hinted, hintedBytes) == 0 && !UnchangedSince(hinted, hintedBytes, TrackerGeneration()), "a hinted range reads as tracked"); + std::array generations{first, first}; + std::array changed{}; + Require(!ChangedBlocks(hinted, hintedBytes, generations, changed), "a hinted range uses block stamps"); + for (unsigned epoch = 0; epoch < 8; ++epoch) { + BumpCollectEpoch(); + Require(UntrackedHint(hinted, hintedBytes) == first, "an untracked hint moved without a store"); + } + memory[Block + 3 * 16384 + 8] ^= 1; + Require(UntrackedHint(hinted, hintedBytes) == first, "an untracked hint was walked twice in one epoch"); + BumpCollectEpoch(); + const auto stored = UntrackedHint(hinted, hintedBytes); + Require(stored > first, "a CPU store into a hinted range did not move its hint"); + Require(UntrackedHint(hinted + Block, Block) == first, "a CPU store moved the hint of another block"); + BumpCollectEpoch(); + Require(UntrackedHint(hinted, hintedBytes) == stored, "an untracked hint moved again without a store"); + memory[Block + 8] = memory[Block + 8]; + BumpCollectEpoch(); + const auto rewritten = UntrackedHint(hinted, hintedBytes); + Require(rewritten > stored, "a store of the bytes already there did not move the hint"); + std::thread([&] { memory[2 * Block + 5 * 4096] ^= 1; }).join(); + BumpCollectEpoch(); + const auto threaded = UntrackedHint(hinted, hintedBytes); + Require(threaded > rewritten, "a store from another thread did not move the hint"); + Require(UntrackedHint(hinted + Block, Block) == threaded && UntrackedHint(hinted, Block) == rewritten, "a walk stamped a store into the wrong block"); + Require(NewestStamp(hinted, hintedBytes) == stamp, "untracked hint walks advanced the block stamps"); + Unwatch(hinted, hintedBytes); + BumpCollectEpoch(); + Require(UntrackedHint(hinted, hintedBytes) == threaded, "unwatching a hinted range again moved its hint"); + Unwatch(base, Block); + const auto own = UntrackedHint(base, Block); + Require(own > threaded && UntrackedHint(hinted, hintedBytes) == threaded, "a range leaving the watch did not start a hint of its own"); + GuestArena::GuestArenaReset_nid_postfix(const_cast(memory) + Block, hintedBytes); + GuestArena::GuestArenaCommit_nid_postfix(const_cast(memory) + Block, hintedBytes, PAGE_READWRITE, 16384); + BumpCollectEpoch(); + const auto fresh = CollectWrites(hinted, hintedBytes); + Require(Watched(hinted, hintedBytes) && fresh != 0 && UnchangedSince(hinted, hintedBytes, fresh), "fresh backing of a hinted range is not watched"); + memory[Block + 8] = 0x5a; + BumpCollectEpoch(); + Require(UntrackedHint(hinted, hintedBytes) == 0, "watched fresh backing has an untracked hint"); + CollectWritesUncached(hinted, hintedBytes); + Require(!UnchangedSince(hinted, Block, fresh), "an untracked hint walk took a CPU store from watched memory"); + Unwatch(hinted, hintedBytes); + Require(UntrackedHint(hinted, hintedBytes) > own, "fresh backing leaving the watch did not start a new hint"); +} + +std::uint64_t ChildHostHole() { + std::uintptr_t arena = 0; + std::size_t arenaBytes = 0; + GuestArena::GuestArenaRange_nid_postfix(&arena, &arenaBytes); + return (arena + arenaBytes / 2) & ~(std::uint64_t{Block} - 1); +} + +void CheckHostHoleInChild() { + std::array executable{}; + const DWORD length = GetModuleFileNameW(nullptr, executable.data(), static_cast(executable.size())); + Require(length != 0 && length < executable.size(), "cannot name the test executable"); + const std::wstring command = L"\"" + std::wstring(executable.data(), length) + L"\" " + HostHoleMode; + std::vector commandLine(command.begin(), command.end()); + commandLine.push_back(L'\0'); + STARTUPINFOW startup{}; + startup.cb = sizeof(startup); + startup.dwFlags = STARTF_USESTDHANDLES; + startup.hStdInput = GetStdHandle(STD_INPUT_HANDLE); + startup.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); + startup.hStdError = GetStdHandle(STD_ERROR_HANDLE); + PROCESS_INFORMATION process{}; + Require(CreateProcessW(executable.data(), commandLine.data(), nullptr, nullptr, TRUE, CREATE_NO_WINDOW | CREATE_SUSPENDED, nullptr, nullptr, &startup, &process) != 0, "cannot create the host-hole child"); + const bool reserved = VirtualAllocEx(process.hProcess, reinterpret_cast(ChildHostHole()), Block, MEM_RESERVE, PAGE_NOACCESS) != nullptr; + const bool resumed = reserved && ResumeThread(process.hThread) != static_cast(-1); + const bool exited = resumed && WaitForSingleObject(process.hProcess, 60000) == WAIT_OBJECT_0; + if (!exited) { + TerminateProcess(process.hProcess, 1); + WaitForSingleObject(process.hProcess, INFINITE); + } + DWORD exitCode = 1; + const bool reported = GetExitCodeProcess(process.hProcess, &exitCode) != 0; + CloseHandle(process.hThread); + CloseHandle(process.hProcess); + Require(reserved, "cannot reserve a host range in the child's arena interval"); + Require(resumed, "cannot resume the host-hole child"); + Require(exited, "the host-hole child did not exit"); + Require(reported && exitCode == 0, "the host-hole child failed"); +} + +void CheckHostHole() { + constexpr std::size_t bytes = 64; + const auto hole = ChildHostHole(); + Require(GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(hole, bytes), "the range reserved before the arena is not an arena host region"); + const auto before = TrackerGeneration(); + Require(!Watched(hole, bytes), "an arena host hole reads as watched"); + Require(CollectWrites(hole, bytes) == 0 && CollectWritesUncached(hole, bytes) == 0, "an arena host hole was collected"); + auto* watched = static_cast(AllocateWatched(Block, true)); + struct Cleanup { + void* memory; + ~Cleanup() { GuestArena::GuestArenaRelease_nid_postfix(memory, Block); } + } cleanup{watched}; + const auto base = reinterpret_cast(watched); + for (unsigned epoch = 0; epoch < 2; ++epoch) { + BumpCollectEpoch(); + watched[7] ^= 1; + Require(CollectWrites(base + 7, 1) != 0, "a warm host-hole check could not collect its watched range"); + for (unsigned repeat = 0; repeat < 3; ++repeat) { + Require(CollectWrites(base + 7, 1) != 0, "a warm host-hole check lost its watched range"); + Require(CollectWrites(hole, bytes) == 0, "a repeated host-hole query hit a collect memo"); + Require(CollectWrites(hole + 7, 1) == 0, "a page-rounded host-hole query hit a collect memo"); + } + } + Require(StoredOver(hole, bytes, before), "an arena host hole reads as not stored over"); + Require(!UnchangedSince(hole, bytes, before), "an arena host hole reads as unchanged"); + const std::array queries{UnchangedQuery{hole, bytes, before}}; + Require(!UnchangedSinceAll(queries), "an arena host hole reads as unchanged in a batch"); + Require(!UnchangedSinceCollected(hole, bytes, before), "an arena host hole reads as unchanged since a collect"); + Require(MarkWritten(hole, bytes) == 0, "a driver store into an arena host hole is stamped"); + Require(StoreOwnBytes(hole, bytes, [] {}) == 0, "an own store into an arena host hole is stamped"); + Require(!WrittenSince(hole, bytes, before), "an arena host hole reads its stamps as written"); + std::array generations{before}; + std::array changed{}; + std::array cpu{}; + Require(!ChangedBlocks(hole, bytes, generations, changed, cpu) && changed[0] == BlockWritten && cpu[0] == 1, "an arena host hole uses block stamps"); + bool imported = false; + Require(ImportWatched(hole, bytes, [&] { + imported = true; + return true; + }) && 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() { @@ -206,28 +931,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; @@ -235,11 +975,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"); @@ -301,20 +1042,409 @@ void CheckCollectBoundaries() { } } +void CheckImportWindow() { + void* memory = AllocateWatched(3 * Block); + const auto base = reinterpret_cast(memory); + auto* bytes = static_cast(memory); + std::memset(memory, 0x11, 3 * Block); + const auto synced = CollectWritesUncached(base, 3 * Block); + Require(synced != 0, "the import window block is not collected"); + Require(ImportWatched(base, 3 * Block, [&] { + bytes[0x100] = 0x11; + bytes[Block + 0x2000] = 0x22; + return true; + }), "the import window import failed"); + Require(Watched(base, 3 * Block), "an import window left its range unwatched"); + const std::array generations{synced, synced, synced}; + std::array changed{}; + std::array cpu{}; + Require(ChangedBlocks(base, 3 * Block, generations, changed, cpu), "the import window block is not tracked"); + Require(changed[0] == BlockUnchanged && UnchangedSince(base, Block, synced), "a page an import window resolved without changing its bytes reads as written"); + Require(changed[1] == BlockWritten && WrittenSince(base + Block, Block, synced) && cpu[1] == 1, "a store of new bytes within an import window was missed"); + Require(changed[2] == BlockUnchanged && cpu[2] == 0, "a block the import window did not touch reads as written"); + Require(cpu[0] == 1, "a page an import window resolved no longer counts as possibly touched by the CPU"); + const auto after = CollectWritesUncached(base, 3 * Block); + bytes[0x100] = 0x33; + static_cast(CollectWritesUncached(base, 3 * Block)); + Require(!UnchangedSince(base, Block, after), "a store after the import window was missed"); +} + +void 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 ProtectPage(std::uint64_t address, bool readable) { +#ifdef _WIN32 + DWORD previous = 0; + Require(VirtualProtect(reinterpret_cast(address), 4096, readable ? PAGE_READWRITE : PAGE_NOACCESS, &previous) != 0, "cannot change the protection of a test page"); +#else + Require(mprotect(reinterpret_cast(address), 4096, readable ? PROT_READ | PROT_WRITE : PROT_NONE) == 0, "cannot change the protection of a test page"); +#endif + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(static_cast(address), 4096); +} + +void 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); + auto* bytes = static_cast(memory); + std::memset(memory, 0x11, 3 * Block); + const auto synced = CollectWritesUncached(base, 3 * Block); + Require(synced != 0, "the unwatched import block is not collected"); + const auto imported = base + Block / 2; + const std::size_t importedBytes = Block + Block / 2; + const auto unreadable = base + Block + 0x3000; + ProtectPage(unreadable, false); + bool threw = false; + bool ran = false; + try { + static_cast(ImportWatched(imported, importedBytes, [&] { + ran = true; + return true; + })); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw && !ran, "an import that stays watched did not read its range before the import"); + threw = false; + try { + Require(ImportWatched(imported, importedBytes, [&] { + bytes[Block / 2 + 0x100] = 0x22; + ran = true; + return true; + }, false), "an import that leaves the watch failed"); + } catch (const std::runtime_error&) { + threw = true; + } + Require(!threw && ran, "an import that leaves the watch read its range"); + ProtectPage(unreadable, true); + Require(!Watched(imported, importedBytes) && !Watched(imported, 4096) && !Watched(base + Block, Block), "an import that leaves the watch is still watched"); + Require(Watched(base + 2 * Block, Block), "an import that leaves the watch unwatched a block outside its range"); + Require(UnchangedSince(base + 2 * Block, Block, synced), "an import that leaves the watch stamped a block outside its range"); +#ifndef _WIN32 + Require(Watched(base, Block / 2), "an import that leaves the watch unwatched the pages before its range"); + const std::array generations{synced}; + std::array changed{}; + std::array cpu{}; + Require(ChangedBlocks(base, Block / 2, generations, changed, cpu), "the pages before an import that leaves the watch are not tracked"); + Require(changed[0] == BlockUnchanged && UnchangedSince(base, Block / 2, synced) && !WrittenSince(base, Block / 2, synced), "a store to a page that left the watch reads as a store to the watched pages of its block"); + Require(cpu[0] == 1, "a page resolved while an import left the watch no longer counts as possibly touched by the CPU"); + const auto after = CollectWritesUncached(base, Block / 2); + bytes[0x100] = 0x33; + static_cast(CollectWritesUncached(base, Block / 2)); + Require(after != 0 && !UnchangedSince(base, Block / 2, after), "a store beside an import that left the watch was missed"); +#endif +} + +class PartlyCommitted { +public: + static constexpr std::size_t Bytes = 4 * Block; + + PartlyCommitted() { +#ifdef _WIN32 + memory = static_cast(GuestArena::GuestArenaAllocateAtOrAbove_nid_postfix(GuestArena::GuestArenaHighestFree_nid_no_patch(Bytes, Block), Bytes, Block)); +#else + void* raw = mmap(nullptr, Bytes + Block, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0); + Require(raw != MAP_FAILED, "cannot reserve the partly committed range"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + Block - 1) & ~static_cast(Block - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + Bytes != begin + Bytes + Block) munmap(reinterpret_cast(aligned + Bytes), begin + Block - aligned); + memory = reinterpret_cast(aligned); +#endif + Require(memory != nullptr, "cannot reserve the partly committed range"); + } + + ~PartlyCommitted() { +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(memory, Bytes); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address(), Bytes); + GuestArena::GuestArenaRelease_nid_postfix(memory, Bytes); +#else + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory, Bytes); + munmap(memory, Bytes); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address(), Bytes); +#endif + } + + PartlyCommitted(const PartlyCommitted&) = delete; + PartlyCommitted& operator=(const PartlyCommitted&) = delete; + + void Commit(std::size_t block) { +#ifdef _WIN32 + GuestArena::GuestArenaCommit_nid_postfix(memory + block * Block, Block, PAGE_READWRITE, Block); +#else + Require(mmap(memory + block * Block, Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) != MAP_FAILED, "cannot commit a block of the partly committed range"); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(memory + block * Block, Block); +#endif + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address(block), Block); + } + + void Decommit(std::size_t block) { +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(memory + block * Block, Block); +#else + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory + block * Block, Block); + Require(mmap(memory + block * Block, Block, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED, -1, 0) != MAP_FAILED, "cannot decommit a block of the partly committed range"); +#endif + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address(block), Block); + } + + std::uint8_t* Data() const { return memory; } + std::uint64_t Address(std::size_t block = 0) const { return reinterpret_cast(memory) + block * Block; } + +private: + std::uint8_t* memory = nullptr; +}; + +using Runs = std::vector>; + +std::uint64_t CollectCommitted(const PartlyCommitted& range, Runs& runs) { + BumpCollectEpoch(); + return CollectCommittedWrites(range.Address(), PartlyCommitted::Bytes, runs); +} + +void CheckCommittedRanges() { + PartlyCommitted range; + const auto base = range.Address(); + range.Commit(0); + std::memset(range.Data(), 0x11, Block); + Require(CollectWrites(base, PartlyCommitted::Bytes) == 0, "a partly committed range was collected as a whole"); + Runs runs; + const Runs first{{base, base + Block}}; + const auto synced = CollectCommitted(range, runs); + Require(synced != 0 && runs == first, "the committed part of a partly committed range was not collected"); + Require(UnchangedCommittedSince(runs, synced), "a collected committed part reads as changed"); + + range.Data()[8] = 0x22; + const auto written = CollectCommitted(range, runs); + Require(written > synced && runs == first && !UnchangedCommittedSince(runs, synced) && UnchangedCommittedSince(runs, written), "a CPU write to the committed part was missed"); + Require(MarkWritten(base + 16, 4) != 0 && !UnchangedCommittedSince(first, written), "a driver store into the committed part was missed"); + Require(UnchangedCommittedSince(first, CollectCommitted(range, runs)), "a collect after a driver store reads the committed part as changed"); + + const auto beforeCommit = CollectCommitted(range, runs); + range.Commit(2); + const Runs both{{base, base + Block}, {range.Address(2), range.Address(3)}}; + const auto grown = CollectCommitted(range, runs); + Require(grown != 0 && runs == both, "a block committed inside the range is not described"); + Require(!UnchangedCommittedSince(runs, beforeCommit), "a block committed inside the range reads as unchanged since before the commit"); + Require(UnchangedCommittedSince(runs, grown), "the committed parts read as changed after their collect"); + + range.Decommit(2); + range.Commit(2); + const auto recommitted = CollectCommitted(range, runs); + Require(recommitted != 0 && runs == both && !UnchangedCommittedSince(runs, grown), "a block decommitted and committed again reads as unchanged"); + + range.Decommit(2); + Require(CollectCommitted(range, runs) != 0 && runs == first, "a decommitted block is still described as committed"); + +#ifdef _WIN32 + HANDLE firstSection = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, Block, nullptr); + HANDLE secondSection = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, Block, nullptr); + struct Sections { + HANDLE first; + HANDLE second; + ~Sections() { + if (first != nullptr) CloseHandle(first); + if (second != nullptr) CloseHandle(second); + } + } sections{firstSection, secondSection}; + Require(firstSection != nullptr && secondSection != nullptr, "cannot create the shared backings of the partly committed range"); + const auto beforeMap = CollectCommitted(range, runs); + GuestArena::GuestArenaMap_nid_postfix(range.Data() + 3 * Block, Block, firstSection, 0, PAGE_READWRITE); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(range.Address(3), Block); + const Runs viewed{{base, base + Block}, {range.Address(3), range.Address(4)}}; + const auto mapped = CollectCommitted(range, runs); + Require(mapped != 0 && runs == viewed, "a view mapped inside the range is not described"); + Require(!UnchangedCommittedSince(runs, beforeMap), "a view mapped inside the range reads as unchanged since before the mapping"); + Require(UnchangedCommittedSince(runs, mapped), "a collected view reads as changed"); + GuestArena::GuestArenaReset_nid_postfix(range.Data() + 3 * Block, Block); + GuestArena::GuestArenaMap_nid_postfix(range.Data() + 3 * Block, Block, secondSection, 0, PAGE_READWRITE); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(range.Address(3), Block); + Require(CollectCommitted(range, runs) != 0 && runs == viewed && !UnchangedCommittedSince(runs, mapped), "a view replaced inside the range reads as unchanged"); + GuestArena::GuestArenaReset_nid_postfix(range.Data() + 3 * Block, Block); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(range.Address(3), Block); + Require(CollectCommitted(range, runs) != 0 && runs == first, "an unmapped view is still described as committed"); +#endif + + range.Decommit(0); + Require(CollectCommitted(range, runs) != 0 && runs.empty() && UnchangedCommittedSince(runs, TrackerGeneration()), "a range with no committed page did not collect as empty"); + Require(!UnchangedCommittedSince(first, 0), "an unknown generation was accepted for the committed parts"); +} + +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; } +#ifdef _WIN32 + if (std::wstring_view(GetCommandLineW()).ends_with(HostHoleMode)) { + CheckHostHole(); + return 0; + } + if (std::wstring_view(GetCommandLineW()).ends_with(ResidentRemapMode)) { + CheckResidentDirectMappingReuse(); + return 0; + } +#endif CheckSharedBlock(); + CheckStoredAllSince(); + CheckSharedBlockProofs(); 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(); + 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 new file mode 100644 index 0000000000..5c662f4b2d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp @@ -0,0 +1,1004 @@ +#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/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#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 "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 +#include +#include +#include + +#ifndef _WIN32 +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} +#endif + +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 Float16x4Info = 0x0730u; +constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; +constexpr std::uint32_t Float32Export = 9; +constexpr std::uint32_t Float16Export = 4; +constexpr std::uint32_t Unorm16Export = 5; +constexpr std::uint32_t Snorm16Export = 6; + +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 Destinations{{ + {0.5f, 0.25f, 0.75f, 0.5f}, {1.0f, 0.0f, 0.125f, 0.25f}, {0.0625f, 0.875f, 0.5f, 1.0f}, {0.25f, 0.5f, 0.375f, 0.75f} +}}; + +alignas(256) std::array, 16> Programs{}; +std::size_t programCount = 0; + +std::uint16_t Half(float value) { + Require(value == 0.0f || (value >= 0.0078125f && value <= 1.0f), "the test's half values are normal and at most 1"); + if (value == 0.0f) return 0; + const auto bits = std::bit_cast(value); + Require((bits & 0x1fffu) == 0, "the test's half values are exact"); + 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((half & 0x8000u) == 0 && exponent != 31u, "a blended texel is negative, infinite or NaN"); + return exponent == 0 ? std::ldexp(static_cast(mantissa), -24) : std::ldexp(1.0f + static_cast(mantissa) / 1024.0f, static_cast(exponent) - 15); +} + +std::uint32_t Packed16(float value, std::uint32_t exportFormat) { + if (exportFormat == Float16Export) return Half(value); + if (exportFormat == Unorm16Export) { + Require(value >= 0.0f && value <= 1.0f, "the test's UNORM16 values are in [0, 1]"); + return static_cast(std::lround(value * 65535.0f)); + } + Require(exportFormat == Snorm16Export && value >= -1.0f && value <= 1.0f, "the test's SNORM16 values are in [-1, 1]"); + return static_cast(static_cast(std::lround(value * 32767.0f))); +} + +std::span Program(const Color& color, std::uint32_t exportFormat, std::optional sampleMask = std::nullopt, std::optional packedAlpha = std::nullopt) { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + const auto move = [&](std::uint32_t vgpr, std::uint32_t value) { + code[at++] = 0x7e0002ffu | (vgpr << 17u); + code[at++] = value; + }; + if (sampleMask) { + move(8, *sampleMask); + code[at++] = 0xf8001084u; + code[at++] = 0x00080000u; + } + if (exportFormat != Float32Export) { + move(4, Packed16(color[0], exportFormat) | (Packed16(color[1], exportFormat) << 16u)); + move(5, Packed16(color[2], exportFormat) | ((packedAlpha ? *packedAlpha : Packed16(color[3], exportFormat)) << 16u)); + code[at++] = 0xf8001c0fu; + code[at++] = 0x00000504u; + } else { + for (std::uint32_t channel = 0; channel < 4; ++channel) move(4 + channel, 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 samples = 1) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Float16x4Info; + const auto log2 = static_cast(std::countr_zero(samples)); + cx[0x31d] = (log2 << 12u) | (log2 << 15u); + cx[0x321] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = TiledAttrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == VK_FORMAT_R16G16B16A16_SFLOAT && color.elementBytes == 8 && color.samples == samples && color.bytes * samples == block.bytes, "the 16_16_16_16 float target did not decode as a 256x128 RGBA16F surface"); + return color; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, bool alphaToCoverage, std::uint32_t exportFormat, std::span pixelCode, bool dithered = false, bool sampleMaskExport = false) { + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = 0xf; + 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; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + state.alphaToCoverage = alphaToCoverage; + state.ditheredAlphaToCoverage = dithered; + state.samples = color.samples; + + const auto codeAddress = reinterpret_cast(pixelCode.data()); + const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, sampleMaskExport ? 0x100u : 0u}, {0x1c5, exportFormat}}; + const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; + auto prepared = AgcDriver::Graphics::DecodePixelStageInfo(context, {}); + prepared.alphaToCoverage = alphaToCoverage; + prepared.ditheredAlphaToCoverage = dithered; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, + {waveSize, 0, {}, std::nullopt, prepared, std::nullopt, pixelMemory}, + 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->alphaToCoverage = state.alphaToCoverage; + fragment.context.pixel->ditheredAlphaToCoverage = state.ditheredAlphaToCoverage; + const ShaderRecompiler::SrtRuntime runtime{}; + const auto materialize = [&](const ShaderRecompiler::RecompileRequest& request) { + const auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, request); + 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); +} + +alignas(256) std::array, 3> DepthOnlyPrograms{}; + +void DepthOnlyCoverage(AgcDriver::VulkanDevice& device, const DepthFastClearHarness::Shaders& harness, const Block& depth, float alpha, std::size_t slot, bool written, std::uint32_t enable = 0x3u, float fourth = 0.0f) { + namespace Harness = DepthFastClearHarness; + const Harness::Surface surface{depth.Address(), 0, 0, false, VK_FORMAT_D32_SFLOAT}; + auto& code = DepthOnlyPrograms[slot]; + code = {0x7e0802ffu, 0x3e800000u, 0x7e0a02ffu, std::bit_cast(alpha), 0x7e0e02ffu, std::bit_cast(fourth), 0xf8001800u | enable, 0x07060504u, 0xbf810000u}; + const std::span pixelCode(code.data(), 9); + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.wave32 = true; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode[0] = 3; + pixel.targetExportMapping.fill(0xe4u); + pixel.alphaToCoverage = true; + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {32, 0, {}, std::nullopt, pixel, std::nullopt, pixelMemory}, + device.Target(), + PixelPushLayout(harness.vertexPush, device.Target()) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &harness.vertex, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(harness.vertexPush, device.Target())} + }}; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; + state.depth = AgcDriver::Graphics::DepthTarget{surface.depth, 0, {Harness::Width, Harness::Height}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0, false}; + state.depthTest = true; + state.depthWrite = true; + state.depthCompare = VK_COMPARE_OP_ALWAYS; + state.alphaToCoverage = true; + state.renderExtent = {Harness::Width, Harness::Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Harness::Height), static_cast(Harness::Width), -static_cast(Harness::Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Harness::Width, Harness::Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Harness::Triangles().size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); + const auto passed = Harness::Draw(device, harness, surface, {.depthCompare = VK_COMPARE_OP_LESS}); + char detail[160]; + std::snprintf(detail, sizeof(detail), "a depth-only draw with alpha-to-coverage and a 32_AR alpha of %.2f (export mask 0x%x, fourth channel %.2f) left %u of %u pixels at the cleared depth", alpha, enable, fourth, passed, Harness::Covered); + Require(passed == (written ? 0u : Harness::Covered), detail); +} + +std::vector ReadBack(AgcDriver::VulkanDevice& device, const Block& block) { + AgcDriver::Graphics::StorageTexture::FlushPending(block.Address(), block.bytes, nullptr, "test read-back"); + device.WaitIdle(); + 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, const ColorTargetLayout& layout) { + std::vector halves(block.bytes / 2u); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto& color = Destinations[(x + y) % Destinations.size()]; + const auto index = layout.Offset(x, y) / 2u; + for (std::uint32_t channel = 0; channel < 4; ++channel) halves[index + channel] = Half(color[channel]); + } + } + std::memcpy(block.data, halves.data(), block.bytes); + return halves; +} + +void Check(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const std::vector& before, const std::function& expectedColor, 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 auto expected = expectedColor(Destinations[(x + y) % Destinations.size()]); + for (std::uint32_t channel = 0; channel < 3; ++channel) { + const bool inside = x < ScissorWidth; + const bool match = inside ? std::fabs(FromHalf(stored[index + channel]) - expected[channel]) <= 1.0f / 2048.0f : stored[index + channel] == before[index + channel]; + if (match) continue; + char detail[160]; + std::snprintf(detail, sizeof(detail), ": channel %u of pixel (%u, %u) %s the scissor holds half 0x%04x, expected %.6g", channel, x, y, inside ? "inside" : "outside", stored[index + channel], inside ? expected[channel] : FromHalf(before[index + channel])); + throw std::runtime_error(what + detail); + } + } + } +} + +void DitheredCoverage(AgcDriver::VulkanDevice& device) { + struct Case { + std::uint32_t bits; + std::uint32_t format; + std::array masks; + }; + constexpr std::array cases{{ + {0x00000000u, 9u, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x80000000u, 9u, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x3c7fffffu, 9u, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x3c800000u, 9u, {0x10u, 0x00u, 0x00u, 0x00u}}, + {0x3c800001u, 9u, {0x10u, 0x00u, 0x00u, 0x00u}}, + {0x3cffffffu, 9u, {0x10u, 0x00u, 0x00u, 0x00u}}, + {0x3d000000u, 9u, {0x04u, 0x00u, 0x00u, 0x00u}}, + {0x3d000001u, 9u, {0x04u, 0x00u, 0x00u, 0x00u}}, + {0x3effffffu, 9u, {0x35u, 0x36u, 0x36u, 0x35u}}, + {0x3f000000u, 9u, {0x35u, 0x36u, 0x36u, 0x35u}}, + {0x3f000001u, 9u, {0x35u, 0x36u, 0x36u, 0x35u}}, + {0x3f77ffffu, 9u, {0xffu, 0xf7u, 0xffu, 0xffu}}, + {0x3f780000u, 9u, {0xffu, 0x7fu, 0xffu, 0xffu}}, + {0x3f780001u, 9u, {0xffu, 0x7fu, 0xffu, 0xffu}}, + {0x3f800000u, 9u, {0xffu, 0xffu, 0xffu, 0xffu}}, + {0x7f800000u, 9u, {0xffu, 0xffu, 0xffu, 0xffu}}, + {0xff800000u, 9u, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x7fc00000u, 9u, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x00000000u, 4u, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x00008000u, 4u, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x000023ffu, 4u, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x00002400u, 4u, {0x10u, 0x00u, 0x00u, 0x00u}}, + {0x00002401u, 4u, {0x10u, 0x00u, 0x00u, 0x00u}}, + {0x000027ffu, 4u, {0x10u, 0x00u, 0x00u, 0x00u}}, + {0x00002800u, 4u, {0x04u, 0x00u, 0x00u, 0x00u}}, + {0x00002801u, 4u, {0x04u, 0x00u, 0x00u, 0x00u}}, + {0x000037ffu, 4u, {0x35u, 0x36u, 0x36u, 0x35u}}, + {0x00003800u, 4u, {0x35u, 0x36u, 0x36u, 0x35u}}, + {0x00003801u, 4u, {0x35u, 0x36u, 0x36u, 0x35u}}, + {0x00003bbfu, 4u, {0xffu, 0xf7u, 0xffu, 0xffu}}, + {0x00003bc0u, 4u, {0xffu, 0x7fu, 0xffu, 0xffu}}, + {0x00003bc1u, 4u, {0xffu, 0x7fu, 0xffu, 0xffu}}, + {0x00003c00u, 4u, {0xffu, 0xffu, 0xffu, 0xffu}}, + {0x00007c00u, 4u, {0xffu, 0xffu, 0xffu, 0xffu}}, + {0x0000fc00u, 4u, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x00007e00u, 4u, {0x00u, 0x00u, 0x00u, 0x00u}}, + }}; + const ColorTargetLayout layout(Width, Height, ColorTileMode::RenderTarget, 8); + std::vector> blocks; + for (const auto& test : cases) { + blocks.push_back(std::make_unique(layout.Bytes() * 8u)); + auto& source = *blocks.back(); + std::memset(source.data, 0, source.bytes); + const auto multisampled = DecodeTarget(source, 8); + for (std::uint32_t sample = 0; sample < 8; ++sample) { + const Color color{std::ldexp(1.0f, static_cast(sample) - 7), 0, 0, 1}; + Draw(device, multisampled, false, Float32Export, Program(color, Float32Export, 1u << sample), false, true); + } + const auto alpha = test.format == Float32Export ? std::bit_cast(test.bits) : 0.0f; + const Color color{0, 0, 0, alpha}; + const auto half = test.format == Float16Export ? std::optional(static_cast(test.bits)) : std::nullopt; + Draw(device, multisampled, true, test.format, Program(color, test.format, std::nullopt, half), true); + blocks.push_back(std::make_unique(layout.Bytes())); + auto& resolved = *blocks.back(); + std::memset(resolved.data, 0, resolved.bytes); + AgcDriver::Graphics::ColorMetadataPass pass{}; + pass.mode = AgcDriver::Graphics::ColorMetadataPass::Mode::Resolve; + pass.source = multisampled; + pass.targets = {DecodeTarget(resolved)}; + device.ColorMetadataPass(pass); + const auto stored = ReadBack(device, resolved); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto mask = test.masks[2u * (x & 1u) + (y & 1u)]; + const auto expected = x < ScissorWidth ? static_cast(255u - mask) / 1024.0f : 0.0f; + const auto actual = FromHalf(stored[layout.Offset(x, y) / 2u]); + if (actual == expected) continue; + char detail[192]; + std::snprintf(detail, sizeof(detail), "dithered coverage export %u alpha 0x%08x pixel (%u,%u): resolved %.9g, expected %.9g for mask0x%02x", test.format, test.bits, x, y, actual, expected, mask); + throw std::runtime_error(detail); + } + } + programCount = 0; + } + AgcDriver::Graphics::ClearMultisampleTargets(device.Device()); +} + +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); + } catch (const std::runtime_error& error) { + if (std::string_view(error.what()) != "AGC graphics: the device cannot render color with the target's sample count") throw; + std::printf("skipped, the device lacks eight-sample color attachments: %s\n", error.what()); + return VulkanTestSkipped; + } + std::puts("dithered eight-sample alpha-to-coverage tests passed"); + return 0; + } + Require(argc == 1, "unknown alpha-to-coverage test argument"); + const ColorTargetLayout layout(Width, Height, ColorTileMode::RenderTarget, 8); + Block block(layout.Bytes()); + struct Case { + std::uint32_t alpha; + bool alphaToCoverage; + bool written; + }; + constexpr std::array cases{{ + {0x00000000u, true, false}, {0x80000000u, true, false}, {0x3e800000u, true, false}, {0x3efffffeu, true, false}, + {0x3effffffu, true, false}, {0x3f000000u, true, true}, {0x3f000001u, true, true}, {0x3f000002u, true, true}, + {0x3f400000u, true, true}, {0x3f800000u, true, true}, {0x40000000u, true, true}, {0xbf800000u, true, false}, + {0x7f800000u, true, true}, {0xff800000u, true, false}, {0x7fc00000u, true, false}, {0xffc00000u, true, false}, + {0x3e800000u, false, true}, + }}; + for (const auto& test : cases) { + const Color source{0.25f, 0.5f, 0.125f, std::bit_cast(test.alpha)}; + const auto before = Fill(block, layout); + Draw(*device, DecodeTarget(block), test.alphaToCoverage, Float32Export, Program(source, Float32Export)); + char name[96]; + std::snprintf(name, sizeof(name), "alpha 0x%08x with alpha-to-coverage %s", test.alpha, test.alphaToCoverage ? "on" : "off"); + Check(*device, block, layout, before, [&](const Color& destination) { return test.written ? source : destination; }, name); + programCount = 0; + } + struct PackedCase { + std::uint32_t exportFormat; + const char* name; + float alpha; + bool written; + }; + constexpr std::array packedCases{{ + {Float16Export, "FP16_ABGR", 0.75f, true}, {Float16Export, "FP16_ABGR", 0.25f, false}, + {Unorm16Export, "UNORM16_ABGR", 0.75f, true}, {Unorm16Export, "UNORM16_ABGR", 0.25f, false}, + {Snorm16Export, "SNORM16_ABGR", 0.75f, true}, {Snorm16Export, "SNORM16_ABGR", 0.25f, false}, {Snorm16Export, "SNORM16_ABGR", -0.5f, false}, + }}; + for (const auto& test : packedCases) { + const Color source{0.25f, 0.5f, 0.125f, test.alpha}; + const auto before = Fill(block, layout); + Draw(*device, DecodeTarget(block), true, test.exportFormat, Program(source, test.exportFormat)); + char name[96]; + std::snprintf(name, sizeof(name), "alpha %.2f with %s exports", test.alpha, test.name); + Check(*device, block, layout, before, [&](const Color& destination) { return test.written ? source : destination; }, name); + programCount = 0; + } + const auto harness = DepthFastClearHarness::Compile(*device); + const Block coveredDepth(BlockBytes); + const Block discardedDepth(BlockBytes); + DepthOnlyCoverage(*device, harness, coveredDepth, 0.75f, 0, true); + DepthOnlyCoverage(*device, harness, discardedDepth, 0.25f, 1, false); + const Block fullMaskDepth(BlockBytes); + DepthOnlyCoverage(*device, harness, fullMaskDepth, 0.25f, 2, false, 0xfu, 0.75f); + std::puts("alpha-to-coverage tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp new file mode 100644 index 0000000000..f9e388295e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp @@ -0,0 +1,376 @@ +#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/ShaderResources.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 + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::StorageTexture; +using AgcDriver::Graphics::TextureDimension; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Side = 128; +constexpr std::uint32_t Layers = 4; +constexpr std::size_t LayerBytes = 65536; +constexpr std::size_t SurfaceBytes = LayerBytes * Layers; +constexpr std::uint32_t Format32Float = 22; +constexpr std::uint32_t TileR64KBX = 0x1b; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t Type2DArray = 13; +constexpr std::uint32_t Color32FloatInfo = (4u << 2u) | (7u << 8u); +constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; +constexpr std::uint32_t Float32Export = 9; +constexpr std::uint32_t Initial = 0x5a5a5a5au; +constexpr std::uint32_t ScissorWidth = 16; +constexpr float Drawn = 0.25f; + +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, Layers> LoadCodes{}; +alignas(256) constexpr std::array Sample2DCode{ + 0x7e020d00, 0x060202f0, 0x100202ff, 0x3c000000, 0x7e0402ff, 0x3b800000, 0xf09c0108, 0x00610301, + 0x34080082, 0xbf8c3f70, 0xe0701000, 0x80000304, 0xbf810000, +}; +alignas(256) std::array PixelCode{}; +alignas(256) std::array Output{}; + +void SampleSliceZero(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode); + +std::uint64_t AddressOf(const void* data) { + return static_cast(reinterpret_cast(data)); +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +class GuestBlock { +public: + explicit GuestBlock(std::size_t registeredBytes = SurfaceBytes, std::size_t registeredOffset = 0) : 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); +#else + void* mapped = mmap(nullptr, SurfaceBytes + LayerBytes, PROT_READ | PROT_WRITE, 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); + } +#endif + Require(block != nullptr, "array layer target: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block + registeredOffset, registeredBytes, true, true, true); + } + + ~GuestBlock() { + if (registered) GuestAllocations::Mutation().Remove(block + registeredOffset); +#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; } + 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) { + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format32Float << 20u) | (((Side - 1u) & 3u) << 30u), + ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), + 0xfacu | (swizzleMode << 20u) | (type << 28u), + lastLayer, + 0u, + 0u, + 0u, + }; +} + +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(); +} + +void Load(AgcDriver::VulkanDevice& device, std::span code, const std::array& texture) { + Output.fill(0xdeadbeefu); + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + const std::array pointSampler{0u, 0xfffu << 12u, 1u << 26u, 0u}; + std::copy(pointSampler.begin(), pointSampler.end(), userData.begin() + 12); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + Dispatch(device, code, userData); +} + +void LoadLayer(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t layer, std::uint32_t swizzleMode) { + auto& code = LoadCodes[layer]; + code = {0x7e020280, 0x7e040280u | layer, 0xf0001128, 0x00010300, 0x34080082, 0xbf8c3f70, 0xe0701000, 0x80000304, 0xbf810000}; + Load(device, code, SurfaceDescriptor(surface, swizzleMode, Type2DArray, Layers - 1u)); +} + +void Sample2D(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t swizzleMode) { + Load(device, Sample2DCode, SurfaceDescriptor(surface, swizzleMode, Type2D, 0u)); +} + +void RequireOutput(std::uint32_t inside, std::uint32_t outside, const std::string& what) { + for (std::uint32_t x = 0; x < Threads; ++x) { + const auto expected = x < ScissorWidth ? inside : outside; + Require(Output[x] == expected, what + ": texel (" + std::to_string(x) + ", 0) reads " + Hex(Output[x]) + ", expected " + Hex(expected)); + } +} + +AgcDriver::Graphics::ColorTarget DecodeTarget(std::uint64_t address, std::uint32_t slice, std::uint32_t swizzleMode, bool collapsed) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(address >> 8u); + cx[0x31b] = slice | (slice << 13u); + cx[0x31c] = Color32FloatInfo; + cx[0x31d] = 0; + cx[0x3b0] = ((Side - 1u) << 14u) | (Side - 1u); + cx[0x3b8] = (TiledAttrib3 & ~(0x1fu << 14u)) | (swizzleMode << 14u) | (collapsed ? 0u : Layers - 1u); + cx[0x390] = static_cast(address >> 40u); + const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == VK_FORMAT_R32_SFLOAT && color.bytes == LayerBytes && color.address == address + slice * LayerBytes, "the COLOR_32 float view did not decode as a 128x128 R32 slice of the array"); + if (collapsed) Require(color.arrayAddress == 0 && color.arrayLastSlice == 0, "a collapsed slice view unexpectedly describes a whole array"); + else Require(color.arrayAddress == address && color.arrayLastSlice == Layers - 1u && color.arraySlice == slice, "the color view did not keep its array and slice"); + return color; +} + +void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t slice, std::uint32_t swizzleMode, bool collapsed) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::size_t at = 0; + for (std::uint32_t channel = 0; channel < 4; ++channel) { + PixelCode[at++] = 0x7e0002ffu | ((4u + channel) << 17u); + PixelCode[at++] = std::bit_cast(Drawn); + } + PixelCode[at++] = 0xf800180fu; + PixelCode[at++] = 0x07060504u; + PixelCode[at++] = 0xbf810000u; + const std::span pixelCode(PixelCode.data(), at); + 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 color = DecodeTarget(surface, slice, swizzleMode, collapsed); + 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.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.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + 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, AgcDriver::Graphics::ExportMappings(state)), std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +void Run(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode, bool collapsed = false, std::uint32_t slice = 1) { + const auto surface = AddressOf(texels); + std::fill_n(reinterpret_cast(texels), SurfaceBytes / 4u, Initial); + AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + const auto drawn = std::bit_cast(Drawn); + DrawSlice(device, surface, slice, swizzleMode, collapsed); + const auto pending = StorageTexture::FindPending(collapsed ? surface + slice * LayerBytes : surface, collapsed ? LayerBytes : SurfaceBytes); + if (collapsed) Require(pending != nullptr && pending->Descriptor().dimension == TextureDimension::k2D && pending->Descriptor().baseAddress == surface + slice * LayerBytes, "a collapsed slice did not render into its shifted surface"); + else Require(pending != nullptr && pending->Descriptor().dimension == TextureDimension::k2DArray && pending->Descriptor().baseAddress == surface && pending->Descriptor().depthOrLastArray == Layers - 1u, "the draw into slice 1 did not render into the image of its array"); + LoadLayer(device, surface, slice, swizzleMode); + RequireOutput(drawn, Initial, "slice " + std::to_string(slice) + " read through the array after the draw"); + for (std::uint32_t other = 0; other < Layers; ++other) { + if (other == slice) continue; + LoadLayer(device, surface, other, swizzleMode); + RequireOutput(Initial, Initial, "slice " + std::to_string(other) + " after the draw into slice " + std::to_string(slice)); + } + StorageTexture::FlushPending(surface, SurfaceBytes, nullptr, "test"); + device.WaitIdle(); + AgcDriver::Graphics::ClearCachedTextures(device.Device()); + AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + LoadLayer(device, surface, slice, swizzleMode); + RequireOutput(drawn, Initial, "slice " + std::to_string(slice) + " uploaded again from guest memory"); + AgcDriver::Graphics::ClearCachedTextures(device.Device()); + SampleSliceZero(device, texels, swizzleMode); +} + +void SampleSliceZero(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode) { + const auto surface = AddressOf(texels); + std::fill_n(reinterpret_cast(texels), SurfaceBytes / 4u, Initial); + AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + DrawSlice(device, surface, 0, swizzleMode, false); + const auto pending = StorageTexture::FindPending(surface, LayerBytes); + Require(pending != nullptr && pending->Descriptor().dimension == TextureDimension::k2DArray, "the draw into slice 0 did not render into the image of its array"); + const auto plain = SurfaceDescriptor(surface, swizzleMode, Type2D, 0u); + Require(AgcDriver::Graphics::Texture::CanCopyFrom(*pending, AgcDriver::Graphics::DecodeTextureResource(plain)), "a 2D texture over slice 0 cannot view the array image's first layer"); + Sample2D(device, surface, swizzleMode); + RequireOutput(std::bit_cast(Drawn), Initial, "slice 0 sampled as a 2D texture after the draw"); + StorageTexture::FlushPending(surface, SurfaceBytes, nullptr, "test"); + device.WaitIdle(); + AgcDriver::Graphics::ClearCachedTextures(device.Device()); +} + +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"); +} + +} + +int main() { + try { + GuestBlock block; + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + 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); + std::puts("array layer 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/BcBlockCopy.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BcBlockCopy.cpp new file mode 100644 index 0000000000..23eeb6dcf7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BcBlockCopy.cpp @@ -0,0 +1,244 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Format32x2Uint = 62; +constexpr std::uint32_t Format32x4Uint = 75; +constexpr std::uint32_t FormatBc1Unorm = 169; +constexpr std::uint32_t FormatBc7Unorm = 181; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t ClampEdge = 2; +constexpr std::size_t BlockBytes = 1u << 20u; +constexpr std::uint32_t Texels = 256; + +struct Shape { + std::uint32_t format; + std::uint32_t width; + std::uint32_t height; +}; + +struct Case { + std::string name; + Shape blocks; + Shape compressed; + std::span store; +}; + +alignas(256) constexpr std::array StoreQuadCode{ + 0x7e28020c, 0x7e2a0300, 0x7e2c020c, 0x7e2e0300, 0x7e3c0300, 0x7e3e0280, 0xf0201f08, 0x0001141e, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array StorePairCode{ + 0x7e28020c, 0x7e2a0300, 0x7e3c0300, 0x7e3e0280, 0xf0201308, 0x0001141e, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array SampleCode{ + 0x1614008c, 0xe03c1000, 0x8000010a, 0xbf8c3f70, 0xf09c0f08, 0x00820402, 0xbf8c3f70, + 0x34160084, 0xe0781000, 0x8001040b, 0xbf810000, 0xbf810000, +}; + +std::span Input; +std::span Output; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, PAGE_READWRITE, BlockBytes); +#else + constexpr std::uintptr_t alignment = 65536; + void* mapped = mmap(nullptr, BlockBytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + alignment - 1) & ~(alignment - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + BlockBytes != begin + BlockBytes + alignment) munmap(reinterpret_cast(aligned + BlockBytes), begin + alignment - aligned); + block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, BlockBytes); + } +#endif + Require(block != nullptr, "bc block copy: cannot allocate the guest block"); + Require(AgcDriver::GuestMemory::Watched(reinterpret_cast(block), BlockBytes), "bc block copy: the guest block is not write-watched"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + AgcDriver::GuestMemory::CollectWritesUncached(reinterpret_cast(block), BlockBytes); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, BlockBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, BlockBytes); +#else + munmap(block, BlockBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, BlockBytes); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array TextureDescriptor(const void* data, const Shape& shape) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (shape.format << 20u) | (((shape.width - 1u) & 3u) << 30u), + ((shape.width - 1u) >> 2u) | ((shape.height - 1u) << 14u), + 0xfacu | (Type2D << 28u), + 0u, 0u, 0u, 0u, + }; +} + +std::array SamplerDescriptor() { + return {ClampEdge | (ClampEdge << 3u) | (ClampEdge << 6u), 0u, 0u, 0u}; +} + +ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::span code, std::span userData) { + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +void Store(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const Case& test, std::uint32_t value) { + std::vector userData(16, 0u); + const auto texture = TextureDescriptor(texels, test.blocks); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + userData[12] = value; + const auto result = Compile(device, test.store, userData); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(test.store.data())); + device.WaitIdle(); +} + +std::uint64_t SurfaceBytes(std::uint8_t* texels, const Shape& shape) { + return AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(texels, shape))).guestBytes; +} + +void Settle(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint64_t bytes) { + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(texels), bytes, nullptr, "test"); + device.WaitIdle(); + AgcDriver::GuestMemory::CollectWritesUncached(reinterpret_cast(texels), bytes); +} + +std::vector Sample(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const Shape& shape) { + std::vector userData(24, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size_bytes())); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size_bytes())); + const auto texture = TextureDescriptor(texels, shape); + const auto sampler = SamplerDescriptor(); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 16); + const auto program = Compile(device, SampleCode, userData); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Input[tid * 3u + 0u] = 0.0f; + Input[tid * 3u + 1u] = (static_cast(tid) * 4.0f + 1.5f) / static_cast(shape.width); + Input[tid * 3u + 2u] = 1.5f / static_cast(shape.height); + } + std::fill(Output.begin(), Output.end(), 0xdeadbeefu); + device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(SampleCode.data())); + device.WaitIdle(); + return {Output.begin(), Output.end()}; +} + +std::vector Reference(AgcDriver::VulkanDevice& device, const Case& test, std::uint32_t value) { + GuestBlock block; + auto* texels = block.Data(); + Store(device, texels, test, value); + Settle(device, texels, SurfaceBytes(texels, test.blocks)); + return Sample(device, texels, test.compressed); +} + +void RequireSame(const std::vector& sampled, const std::vector& expected, const std::string& what) { + for (std::size_t word = 0; word < expected.size(); ++word) { + Require(sampled[word] == expected[word], what + ": thread " + std::to_string(word / 4u) + " component " + std::to_string(word % 4u) + " read " + std::to_string(sampled[word]) + ", expected " + std::to_string(expected[word])); + } +} + +void PendingBlocksCase(AgcDriver::VulkanDevice& device, const Case& test) { + constexpr std::uint32_t First = 0x13572440u; + constexpr std::uint32_t Second = 0x9bdf6840u; + const auto first = Reference(device, test, First); + const auto second = Reference(device, test, Second); + Require(first != second, test.name + ": the two block patterns sample alike"); + GuestBlock block; + auto* texels = block.Data(); + const auto bytes = SurfaceBytes(texels, test.compressed); + Require(bytes == SurfaceBytes(texels, test.blocks), test.name + ": the block view and the compressed view disagree on the surface size"); + Store(device, texels, test, First); + Settle(device, texels, bytes); + RequireSame(Sample(device, texels, test.compressed), first, test.name + ": sample of stored blocks"); + Store(device, texels, test, Second); + const auto pending = Sample(device, texels, test.compressed); + Require(AgcDriver::Graphics::StorageTexture::FindPending(reinterpret_cast(texels), bytes) != nullptr, test.name + ": sampling the compressed view stored the pending blocks to guest memory"); + RequireSame(pending, second, test.name + ": sample of pending blocks"); + Store(device, texels, test, First); + RequireSame(Sample(device, texels, test.compressed), first, test.name + ": sample of blocks stored again"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock buffers; + Input = std::span(reinterpret_cast(buffers.Data()), Threads * 3u); + Output = std::span(reinterpret_cast(buffers.Data() + 4096u), Threads * 4u); + PendingBlocksCase(*device, {"BC7 through 32x4 uint blocks", {Format32x4Uint, Texels / 4u, Texels / 4u}, {FormatBc7Unorm, Texels, Texels}, StoreQuadCode}); + PendingBlocksCase(*device, {"BC1 through 32x2 uint blocks", {Format32x2Uint, Texels / 4u, Texels / 4u}, {FormatBc1Unorm, Texels, Texels}, StorePairCode}); + std::puts("bc block copy tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/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/BdaSpanReads.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BdaSpanReads.cpp index ccc63037b8..bb0fcc4fda 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/BdaSpanReads.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BdaSpanReads.cpp @@ -52,7 +52,7 @@ public: block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); #endif Require(block != nullptr, "BDA span reads: cannot allocate the guest block"); - GuestAllocations::Mutation().Add(block, BlockBytes, true, true); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); } ~GuestBlock() { 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/BitfieldMask.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BitfieldMask.cpp new file mode 100644 index 0000000000..77d144b5e0 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BitfieldMask.cpp @@ -0,0 +1,96 @@ +#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 = 32u; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; +alignas(256) constexpr std::array Code{ + 0x34020083u, 0x34040084u, 0xe0341000u, 0x80000401u, 0xbf8c3f70u, 0xd7630008u, 0x00020b04u, + 0xbe8c0380u, 0xbe8d0380u, 0xf4240200u, 0x18000000u, 0xbf8cc07fu, 0xbf060d0du, 0x920a0908u, + 0x850b8081u, 0xd7610009u, 0x00001a0au, 0xd761000au, 0x00001a0bu, 0xbf070d0du, 0x920a0908u, + 0x850b8081u, 0xd761000bu, 0x00001a0bu, 0x800c880cu, 0x800d810du, 0xbf0aa00du, 0xbf85ffedu, + 0xe0781000u, 0x80010802u, 0xbf810000u +}; + +std::array bufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::uint32_t expectedMask(std::uint32_t width, std::uint32_t offset) { + std::uint32_t mask = 0u; + for (std::uint32_t bit = offset; bit < 32u && bit - offset < width; ++bit) mask |= 1u << bit; + return mask; +} + +void run(AgcDriver::VulkanDevice& device) { + std::array userData{}; + 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, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0u, {}}, + {32u, 0u, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0u, 0u, 0u, 128u} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + for (const std::uint32_t upperBits : {0u, 32u, 0x80000000u, 0xffffffe0u}) { + for (std::uint32_t width = 0u; width < 32u; ++width) { + for (std::uint32_t offset = 0u; offset < Threads; ++offset) { + Input[offset * 2u] = width | upperBits; + Input[offset * 2u + 1u] = offset | upperBits; + } + Output.fill(0xdeadbeefu); + device.Dispatch(result, 1u, 1u, 1u, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + for (std::uint32_t offset = 0u; offset < Threads; ++offset) { + const auto expected = expectedMask(width, offset); + const auto description = "bitfield mask width=" + std::to_string(width | upperBits) + + " offset=" + std::to_string(offset | upperBits); + Require(Output[offset * 4u] == expected, description + " vector result " + + std::to_string(Output[offset * 4u]) + ", expected " + std::to_string(expected)); + Require(Output[offset * 4u + 1u] == expected, description + " scalar result " + + std::to_string(Output[offset * 4u + 1u]) + ", expected " + std::to_string(expected)); + Require(Output[offset * 4u + 2u] == 1u, description + " changed SCC=1"); + Require(Output[offset * 4u + 3u] == 0u, description + " changed SCC=0"); + } + } + } +} + +} + +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); + std::puts("4096 scalar/vector bitfield mask execution cases passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicCsub.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicCsub.cpp index 5a2a267f44..0b150b2da8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicCsub.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicCsub.cpp @@ -28,12 +28,12 @@ constexpr std::uint32_t SharedExhausted = 1000u; alignas(256) std::array Input{}; alignas(256) std::array Output{}; -alignas(256) constexpr std::array Code{ +alignas(256) constexpr std::array Code{ 0x34020084, 0x34040085, 0xe0301000, 0x80000401, 0xe0301004, 0x80000501, 0xe0301008, 0x80000601, - 0xbf8c3f70, 0x7e140280, 0x461414c1, 0x481414c1, 0x7e0e0304, 0xe0d05000, 0x80010702, 0x7e100304, - 0xbefe03ff, 0x55555555, 0xe0d05004, 0x80010802, 0xbefe03c1, 0x7e120305, 0xe0d04800, 0x80010900, - 0x7e160306, 0xe0d04804, 0x80010b00, 0xbf8c3f70, 0xe0701008, 0x80010702, 0xe070100c, 0x80010802, - 0xe0701010, 0x80010a02, 0xbf810000, + 0xbf8c3f70, 0x7e140280, 0xd765000a, 0x000214c1, 0xd766000a, 0x000214c1, 0x7e0e0304, 0xe0d05000, + 0x80010702, 0x7e100304, 0xbefe03ff, 0x55555555, 0xe0d05004, 0x80010802, 0xbefe03c1, 0x7e120305, + 0xe0d04800, 0x80010900, 0x7e160306, 0xe0d04804, 0x80010b00, 0xbf8c3f70, 0xe0701008, 0x80010702, + 0xe070100c, 0x80010802, 0xe0701010, 0x80010a02, 0xbf810000, }; alignas(256) constexpr std::array NoReturnCode{ 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 new file mode 100644 index 0000000000..51fa4f8630 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp @@ -0,0 +1,171 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "Optimization/ResourceProgram.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Lanes = 32; +constexpr std::uint32_t InputBytes = 256; +constexpr std::uint32_t Fill = 0xdeadbeefu; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020082, 0xe0301000, 0x80000201, 0xbf8c3f70, 0xe0701000, 0x80010201, 0xbf810000, +}; + +struct Case { + const char* name; + std::uint32_t baseOffset; + std::uint32_t records; + bool present; +}; + +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; +} + +ShaderRecompiler::RecompileRequest Request(std::span userData, const ShaderRecompiler::SpirvTarget& target, std::span memory) { + const ShaderRecompiler::ShaderComputeStageInfo compute{{Lanes, 1, 1}, 0u, {false, false, false}, false, 1}; + return { + {ShaderStage::Compute, reinterpret_cast(Code.data()), Code, 0, {}}, + {Lanes, 0, userData, compute, std::nullopt, std::nullopt, memory}, + target, + {0, 0, 0, 128} + }; +} + +void Run(AgcDriver::VulkanDevice& device, bool registrationModules) { + const std::string mode = registrationModules ? "registration modules" : "specialized modules"; + const std::array memory{{{reinterpret_cast(Code.data()), std::as_bytes(std::span(Code))}}}; + std::array userData{}; + auto target = device.ComputeTarget(Lanes); + target.registrationModules = registrationModules; + const auto request = Request(userData, target, memory); + const auto handle = ShaderRecompiler::PrepareShader(request); + const std::array cases{{ + {"aligned", 0, 128, true}, + {"base +1", 1, 128, true}, + {"base +2", 2, 128, true}, + {"base +3", 3, 128, true}, + {"records 37", 0, 37, true}, + {"records 64 base +2", 2, 64, true}, + {"absent", 0, 128, false}, + }}; + for (const auto& test : cases) { + Output.fill(Fill); + const auto input = test.present ? BufferDescriptor(Input.data() + test.baseOffset, test.records) : std::array{0u, 0u, 0u, 0x31016facu}; + const auto output = BufferDescriptor(Output.data(), sizeof(Output)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + ShaderRecompiler::SrtRuntime runtime{}; + runtime.userData = userData; + const auto capture = ShaderRecompiler::CaptureResources(request, runtime, *handle); + const auto result = ShaderRecompiler::MaterializeShader(request, *capture, *handle); + Require(!result->registrationModule, "buffer descriptor runtime, " + mode + ", " + test.name + ": a compute shader was emitted in registration form"); + device.Dispatch(*result, 1, 1, 1, {}, reinterpret_cast(Code.data())); + device.WaitIdle(); + for (std::uint32_t lane = 0; lane < Lanes; ++lane) { + std::uint32_t expected = 0; + if (test.present && lane * 4u + 4u <= test.records) std::memcpy(&expected, Input.data() + test.baseOffset + lane * 4u, sizeof(expected)); + Require(Output[lane] == expected, "buffer descriptor runtime, " + mode + ", " + test.name + ": lane " + std::to_string(lane) + " is " + Hex(Output[lane]) + ", expected " + Hex(expected)); + } + } +} + +void ScalarReads(AgcDriver::VulkanDevice& device) { + constexpr std::array scalarCode{0x34020082u, 0xf4200200u, 0xfa000004u, 0xbf8cc07fu, 0x7e040208u, 0xe0701000u, 0x80010201u, 0xbf810000u}; + std::array userData{}; + const auto output = BufferDescriptor(Output.data(), sizeof(Output)); + std::copy(output.begin(), output.end(), userData.begin() + 4); + auto request = Request(userData, device.ComputeTarget(Lanes), {}); + request.shader.code = scalarCode; + request.shader.codeAddress = reinterpret_cast(scalarCode.data()); + request.useCache = false; + std::uint32_t expected = 0; + std::memcpy(&expected, Input.data() + 4u, sizeof(expected)); + for (const auto ignoredWord1 : {0u, 0x40000000u}) { + for (const auto word : {0u, 0x31016facu, 0x91b00f2eu, 0xffffffffu}) { + auto input = BufferDescriptor(Input.data(), InputBytes); + input[1] |= ignoredWord1; + input[3] = word; + std::copy(input.begin(), input.end(), userData.begin()); + Output.fill(Fill); + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1); + device.WaitIdle(); + Require(std::all_of(Output.begin(), Output.end(), [expected](std::uint32_t value) { return value == expected; }), "scalar buffer read changed with ignored descriptor word " + Hex(word)); + } + } + constexpr std::array mixedCode{0xf4200200u, 0xfa000004u, 0xbf8cc07fu, 0x34020082u, 0xe0301000u, 0x80000201u, 0xbf8c3f70u, 0xe0701000u, 0x80010201u, 0x7e060208u, 0xe0701000u, 0x80010301u, 0xbf810000u}; + for (const auto code : {std::span(Code), std::span(mixedCode)}) { + request.shader.code = code; + request.shader.codeAddress = reinterpret_cast(code.data()); + try { + static_cast(ShaderRecompiler::Recompile(request)); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find("buffer descriptor uses an unsupported type") != std::string::npos, "vector buffer descriptor failed for another reason"); + 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"); + } + for (const auto code : {std::span(Code), std::span(mixedCode)}) { + request.shader.code = code; + request.shader.codeAddress = reinterpret_cast(code.data()); + auto input = BufferDescriptor(Input.data(), InputBytes); + 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); + device.Dispatch(result, 1, 1, 1); + device.WaitIdle(); + Require(Output == baseline, "vector or mixed buffer read changed with reserved bit 30 set"); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (std::uint32_t index = 0; index < InputBytes; ++index) Input[index] = static_cast(index * 41u + 3u + (index >> 3u)); + Run(*device, false); + Run(*device, true); + ScalarReads(*device); + std::puts("buffer descriptor runtime tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} 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/Bvh64IntersectRay.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Bvh64IntersectRay.cpp index 788ff56ff6..77ced64f4c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Bvh64IntersectRay.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Bvh64IntersectRay.cpp @@ -152,7 +152,7 @@ public: #endif Require(block != nullptr, "bvh64 intersect ray: cannot allocate the guest block"); std::memset(block, 0, BlockBytes); - GuestAllocations::Mutation().Add(block, BlockBytes, true, false); + GuestAllocations::Mutation().Add(block, BlockBytes, true, false, true); } ~GuestBlock() { 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/Color1555Target.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Color1555Target.cpp new file mode 100644 index 0000000000..e4cf4c69bc --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Color1555Target.cpp @@ -0,0 +1,299 @@ +#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 Color1555Info = 0x8044u; +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) { + 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)); }; + return static_cast(unorm(color[0], 31) | (unorm(color[1], 31) << 5u) | (unorm(color[2], 31) << 10u) | (unorm(color[3], 1) << 15u)); +} + +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) { + 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] = attrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == VK_FORMAT_A1R5G5B5_UNORM_PACK16 && color.elementBytes == 2 && color.bytes == block.bytes, "the 1_5_5_5 unorm target did not decode as an A1R5G5B5 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, 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); + auto before = Fill(block); + struct Case { + Color color; + std::uint16_t texel; + }; + const std::array cases{{ + {{1.0f, 0.0f, 0.0f, 1.0f}, 0x801f}, + {{0.0f, 1.0f, 0.0f, 0.0f}, 0x03e0}, + {{0.0f, 0.0f, 1.0f, 0.0f}, 0x7c00}, + {{0.0f, 0.0f, 0.0f, 1.0f}, 0x8000}, + {{1.0f, 0.0f, 1.0f, 0.0f}, 0x7c1f}, + {{1.0f, 1.0f, 1.0f, 1.0f}, 0xffff}, + {{0.0f, 0.0f, 0.0f, 0.0f}, 0x0000}, + {{3.0f / 31.0f, 10.0f / 31.0f, 21.0f / 31.0f, 1.0f}, (3u) | (10u << 5u) | (21u << 10u) | 0x8000u}, + {{21.0f / 31.0f, 3.0f / 31.0f, 10.0f / 31.0f, 0.0f}, (21u) | (3u << 5u) | (10u << 10u)}, + {{1.0f / 31.0f, 30.0f / 31.0f, 2.0f / 31.0f, 0.75f}, (1u) | (30u << 5u) | (2u << 10u) | 0x8000u}, + {{2.0f, -1.0f, 10.0f / 31.0f, 0.25f}, (31u) | (0u << 5u) | (10u << 10u)}, + }}; + for (const auto& test : cases) { + Require(Texel(test.color) == test.texel, "the test's expected texel disagrees with the 1_5_5_5 field layout"); + Draw(device, color, Float32Export, Program(test.color)); + char what[160]; + std::snprintf(what, sizeof(what), "a 32_ABGR export of (%.9g, %.9g, %.9g, %.9g) into a %s 1_5_5_5 unorm target", test.color[0], test.color[1], test.color[2], test.color[3], kind); + ExpectSurface(device, block, layout, before, test.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()); + TargetTests(*device, tiled, TiledAttrib3, ColorTileMode::RenderTarget, "tiled"); + TargetTests(*device, linear, LinearAttrib3, ColorTileMode::Linear, "linear"); + std::puts("1_5_5_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/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/ComputeUserDataCalls.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ComputeUserDataCalls.cpp new file mode 100644 index 0000000000..f69cfaf201 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ComputeUserDataCalls.cpp @@ -0,0 +1,135 @@ +#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 + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Untouched = 0xcafebabeu; + +alignas(256) constexpr std::array Caller{ + 0x7e020280u, 0xbe940383u, 0xbe8e0300u, 0xbe8f0301u, 0xbe8e210eu, 0xbe8e0302u, 0xbe8f0303u, 0xbe8e210eu, + 0x80948114u, 0xbf078014u, 0xbf85fff7u, 0x34040082u, 0xe0701000u, 0x80010102u, 0xbf810000u, 0xbf9f0000u, + 0x30306c73u, 0x00000040u, 0x00000008u}; +alignas(256) constexpr std::array AddFive{0x4a020285u, 0xbe80200eu}; +alignas(256) constexpr std::array AddSixtyFour{0x4a0202c0u, 0xbe80200eu}; +alignas(256) constexpr std::array AddOne{0x4a020281u, 0xbe80200eu}; +alignas(256) std::array Output{}; + +std::array UserData(const void* first, const void* second) { + const auto low = reinterpret_cast(first); + const auto high = reinterpret_cast(second); + const auto output = reinterpret_cast(Output.data()); + return {static_cast(low), static_cast(low >> 32u), static_cast(high), static_cast(high >> 32u), + static_cast(output), static_cast((output >> 32u) & 0xffffu), static_cast(sizeof(Output)), 0x31016facu}; +} + +std::vector Commands(const std::array& data, bool wave32) { + 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{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; +} + +void Dispatch(const void* first, const void* second, bool wave32) { + Output.fill(Untouched); + const auto words = Commands(UserData(first, second), wave32); + Packet packet{const_cast(words.data()), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output.data()), sizeof(Output)); +} + +void Check(const void* first, const void* second, bool wave32, std::uint32_t expected, const std::string& what) { + Dispatch(first, second, wave32); + 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)); + } +} + +void Register(bool wave32) { + struct Header { + Shader shader{}; + std::array registers{}; + ShaderSpecialRegs specials{}; + }; + alignas(8) static Header header{}; + const auto address = reinterpret_cast(Caller.data()); + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = sizeof(Caller); + header.shader.code = Caller.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 = wave32 ? 0x8000u : 0u; + header.registers = {{{0x20c, static_cast(address >> 8u)}, {0x20d, static_cast(address >> 40u)}, {0x207, Threads}, {0x208, 1}, {0x209, 1}, {0x212, 0}, {0x213, 8u << 1u}}}; + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +template +void ExpectFailure(TAction action, const char* expected) { + try { + action(); + } catch (const std::exception& error) { + Require(std::string(error.what()).find(expected) != std::string::npos, std::string("unexpected failure: ") + error.what()); + return; + } + throw std::runtime_error(std::string("expected a failure containing ") + expected); +} + +} + +int main(int argc, char** argv) { + try { + Require(argc == 1 || (argc == 2 && std::string(argv[1]) == "--registered"), "usage: [--registered]"); + const bool registered = argc == 2; + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + if (registered) Register(true); + for (const bool wave32 : registered ? std::vector{true} : std::vector{false, true}) { + Check(AddFive.data(), AddSixtyFour.data(), wave32, 3u * (5u + 64u), "two user-data callees called from a loop"); + Check(AddOne.data(), AddSixtyFour.data(), wave32, 3u * (1u + 64u), "the first callee replaced"); + Check(AddSixtyFour.data(), AddSixtyFour.data(), wave32, 3u * (64u + 64u), "one callee behind both pointers"); + } + const auto* unmapped = reinterpret_cast(std::uintptr_t{0x1000u}); + ExpectFailure([&] { Dispatch(AddFive.data(), unmapped, registered); }, "is not readable"); + ExpectFailure([] { AgcDriverShutdown_nid_postfix(); }, "is not readable"); + std::puts("compute user-data 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/ConservativeRasterization.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ConservativeRasterization.cpp index 4bc982a2b4..3a641d9053 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ConservativeRasterization.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ConservativeRasterization.cpp @@ -86,13 +86,13 @@ void Draw(AgcDriver::VulkanDevice& device, VkConservativeRasterizationModeEXT mo {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, {WaveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, target, - {0, 0, vertexPush, 128 - vertexPush} + PixelPushLayout(vertexPush, target) }; fragment.useCache = false; const auto pixelResult = ShaderRecompiler::Recompile(fragment); const std::array shaders{{ {ShaderStage::Vertex, &vertexResult, 0}, - {ShaderStage::Fragment, &pixelResult, vertexPush} + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} }}; AgcDriver::Graphics::State state{}; 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/ConstantStore.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ConstantStore.cpp index c9b16efb5f..8d542c7ff3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ConstantStore.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ConstantStore.cpp @@ -35,7 +35,7 @@ public: #endif Require(block != nullptr, "constant store: cannot allocate the guest block"); Clear(); - GuestAllocations::Mutation().Add(block, BlockBytes, true, true); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); } ~GuestBlock() { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/CopiedWriterCopy.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/CopiedWriterCopy.cpp new file mode 100644 index 0000000000..f587262360 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/CopiedWriterCopy.cpp @@ -0,0 +1,141 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#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 = 32; +constexpr std::uint32_t OutputBytes = Threads * 16u * 4u; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint8_t Fill = 0xcd; + +alignas(256) constexpr std::array LoadsCode{ + 0x34020084, 0x34040086, 0xdc388000, 0x04080001, 0xbf8c3f70, 0x7e1402ff, 0x5a5a0000, 0x7e1602ff, + 0x5a5a0001, 0x7e1802ff, 0x5a5a0002, 0x7e1a02ff, 0x5a5a0003, 0x7e1c02ff, 0x5a5a0004, 0x7e1e02ff, + 0x5a5a0005, 0x7e2002ff, 0x5a5a0006, 0x7e2202ff, 0x5a5a0007, 0x7e2402ff, 0x5a5a0008, 0x7e2602ff, + 0x5a5a0009, 0x7e2802ff, 0x5a5a000a, 0x7e2a02ff, 0x5a5a000b, 0x7e2c02ff, 0x5a5a000c, 0x7e2e02ff, + 0x5a5a000d, 0x7e3002ff, 0x5a5a000e, 0x7e3202ff, 0x5a5a000f, 0x4a4008ff, 0x00000a00, 0x4a420eff, + 0x00000200, 0xd70f6a22, 0x02024008, 0x7e460209, 0x50464680, 0x4a4c0aff, 0x00000200, 0xd70f6a24, + 0x02024c08, 0x7e4a0209, 0x504a4a80, 0x36500c81, 0x7daa5080, 0xdc208800, 0x0a080020, 0xdc2487ff, + 0x0b080020, 0xdc288ffd, 0x0c080020, 0xdc2c8001, 0x0d7d0022, 0xdc308fff, 0x0e080020, 0xdc308000, + 0x18080021, 0xdc208800, 0x197d0022, 0xbf8c0070, 0xbefe04c1, 0xe0781000, 0x80010a02, 0xe0781010, + 0x80010e02, 0xe0781020, 0x80011202, 0xe0781030, 0x80011602, 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, "copied writer: cannot allocate a guest block"); + std::memset(block, Fill, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() const { return block; } + std::uint64_t Address() const { return static_cast(reinterpret_cast(block)); } + +private: + std::uint8_t* block = nullptr; +}; + +void RecordDispatch(AgcDriver::VulkanDevice& device, const GuestBlock& input, const GuestBlock& output) { + std::vector userData(10, 0u); + const auto address = output.Address(); + const std::array descriptor{static_cast(address), static_cast((address >> 32u) & 0xffffu), OutputBytes, 0x31016facu}; + std::copy(descriptor.begin(), descriptor.end(), userData.begin() + 4); + userData[8] = static_cast(input.Address()); + userData[9] = static_cast(input.Address() >> 32u); + const std::span code(LoadsCode); + 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())); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const GuestBlock input; + std::memset(input.Data(), 0, BlockBytes); + const GuestBlock output; + const GuestBlock destination; + RecordDispatch(*device, input, output); + auto* recorder = AgcDriver::Graphics::Recorder::Active(); + Require(recorder != nullptr, "copied writer: no active recorder"); + constexpr std::array labelValue{std::byte{1}, std::byte{0}, std::byte{0}, std::byte{0}}; + recorder->NoteLabel(output.Address(), labelValue, 1, 0); + Require(recorder->AfterRecordedWork([] {}), "copied writer: completion action was not recorded"); + AgcDriver::VulkanDevice::CopyOutcome outcome{}; + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + outcome = device->CopyBuffer(destination.Address(), output.Address(), OutputBytes, 0, std::numeric_limits::max(), 0, 0, 0, [](std::span, std::uint64_t) {}); + } + if (outcome.path == 2) { + device->WaitIdle(); + std::puts("skipped, the device imports no host memory"); + return VulkanTestSkipped; + } + Require(outcome.path == 1, "copied writer: the copy did not run as a transfer between the host imports (path " + std::to_string(outcome.path) + ")"); + Require(!outcome.synced, "copied writer: a copy of what an address-based dispatch stored through a bound buffer waited for its batch, although no CPU write-back follows it"); + device->WaitIdle(); + Require(std::any_of(output.Data(), output.Data() + OutputBytes, [](std::uint8_t byte) { return byte != Fill; }), "copied writer: the dispatch stored nothing"); + for (std::uint32_t offset = 0; offset < OutputBytes; ++offset) { + Require(destination.Data()[offset] == output.Data()[offset], "copied writer: copied byte " + std::to_string(offset) + " is " + std::to_string(destination.Data()[offset]) + ", the dispatch stored " + std::to_string(output.Data()[offset])); + } + std::puts("copied writer 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/CrossWorkgroupFlag.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/CrossWorkgroupFlag.cpp new file mode 100644 index 0000000000..1238181a8c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/CrossWorkgroupFlag.cpp @@ -0,0 +1,96 @@ +#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; + +alignas(256) constexpr std::array CrossWorkgroupFlagCode{ + 0xbe840380, 0x7e000280, 0xbf068002, 0xbf840004, 0x7e020281, 0xdc718000, 0x00000100, 0xbf810000, + 0xdc319000, 0x01000000, 0xbf8c3f70, 0x7d840280, 0x80048104, 0xbf0aff04, 0x000f4240, 0xbf840005, + 0xbf87fff7, 0x7e020282, 0xdc718004, 0x00000100, 0xbf810000, 0x7e020283, 0xdc718004, 0x00000100, + 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, "cross-workgroup flag: 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; +}; + +std::uint32_t Run(AgcDriver::VulkanDevice& device, GuestBlock& guest, std::uint32_t waveSize, std::uint32_t preset) { + std::memset(guest.Data(), 0, BlockBytes); + std::memcpy(guest.Data(), &preset, sizeof(preset)); + const auto address = static_cast(reinterpret_cast(guest.Data())); + const std::vector userData{static_cast(address), static_cast(address >> 32u)}; + const std::span code(CrossWorkgroupFlagCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{64, 1, 1}, 0, {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, 2, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + std::uint32_t words[2]{}; + std::memcpy(words, guest.Data(), sizeof(words)); + std::printf("wave%u: flag %u, result %u\n", waveSize, words[0], words[1]); + return words[1]; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock guest; + Require(Run(*device, guest, 64, 1) == 2u && Run(*device, guest, 32, 1) == 2u, "a workgroup did not see a flag the CPU stored before the dispatch"); + const auto wave64 = Run(*device, guest, 64, 0); + const auto wave32 = Run(*device, guest, 32, 0); + Require(wave64 == 2u && wave32 == 2u, "a workgroup did not see another workgroup's flag store (2 = seen, 3 = gave up)"); + std::puts("cross-workgroup flag 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/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 c3c4bd5eb4..9f844dd5cc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp @@ -1,7 +1,10 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" @@ -31,6 +34,16 @@ #include #include +#ifndef _WIN32 +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} +#endif + namespace { using AgcDriver::Graphics::DccKeys; @@ -65,6 +78,13 @@ alignas(256) constexpr std::array ClearKeysCode{ 0x34020083, 0x7e040280, 0x7e060280, 0xe0741000, 0x80000201, 0xbf810000, }; +alignas(256) constexpr std::array SampleSecondTexelCode{ + 0x34060084, 0x7e2802ff, 0x3c400000, 0x7e2a02ff, 0x3b800000, 0xf09c0f08, 0x00610a14, 0xbf8c3f70, + 0xe0781000, 0x80000a03, 0xbf810000, +}; + +constexpr std::array PointSampler{0x00000092u, 0x00fff000u, 0u, 0u}; + std::uint64_t AddressOf(const void* data) { return static_cast(reinterpret_cast(data)); } @@ -80,6 +100,9 @@ public: const auto section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, 0, BlockBytes, nullptr); Require(section != nullptr, "cannot create shared test memory"); GuestArena::GuestArenaMap_nid_postfix(block, BlockBytes, section, 0, PAGE_READWRITE); + twin = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536)); + Require(twin != nullptr, "cannot reserve the second mapping of shared test memory"); + GuestArena::GuestArenaMap_nid_postfix(twin, BlockBytes, section, 0, PAGE_READWRITE); CloseHandle(section); } else if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, PAGE_READWRITE, BlockBytes); } else { @@ -104,12 +127,16 @@ public: #endif Require(block != nullptr, "DCC keys first write: cannot allocate the guest block"); Require(!watched || AgcDriver::GuestMemory::Watched(AddressOf(block), BlockBytes), "DCC keys first write: the watched guest block is not write-watched"); - GuestAllocations::Mutation().Add(block, BlockBytes, true, true); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); } ~GuestBlock() { GuestAllocations::Mutation().Remove(block); #ifdef _WIN32 + if (twin != nullptr) { + GuestArena::GuestArenaReset_nid_postfix(twin, BlockBytes); + GuestArena::GuestArenaRelease_nid_postfix(twin, BlockBytes); + } if (watched) { GuestArena::GuestArenaReset_nid_postfix(block, BlockBytes); GuestArena::GuestArenaRelease_nid_postfix(block, BlockBytes); @@ -134,6 +161,7 @@ public: private: bool watched; std::uint8_t* block = nullptr; + std::uint8_t* twin = nullptr; }; std::array TextureDescriptor(const std::uint8_t* texels, const std::uint8_t* keys) { @@ -209,6 +237,8 @@ void Run(AgcDriver::VulkanDevice& device, std::uint8_t* block) { Require(image != nullptr && image->Descriptor().dccAddress == AddressOf(keys), "the written surface has no pending image under its keys"); Require(AgcDriver::Graphics::StorageImageServesKeys(*image, AddressOf(copied)), "the written image does not serve the copied keys"); Require(image->UploadedKeys() == DccKeys::Uncompressed && image->FilledKeys() == DccKeys::Uncompressed, "the written image still holds the clear it was uploaded under"); + const bool stillWatched = AgcDriver::GuestMemory::Watched(surface, SurfaceBytes); + Require(image->GuestSnapshotValid() == !stillWatched, stillWatched ? "a fast-cleared upload of write-watched memory read its guest bytes" : "a fast-cleared upload of untracked memory kept no guest bytes to compare"); const auto pendingVersion = image->Version(); for (int repeat = 0; repeat < 3; ++repeat) { Require(image->Refresh(), "an unchanged pending image was uploaded again"); @@ -305,7 +335,7 @@ void Run(AgcDriver::VulkanDevice& device, std::uint8_t* block) { const std::array pattern{word, word, word, word}; return device.FillBuffer(AddressOf(keys + offset), bytes, pattern); }; - if (!fill(0, KeyBytes, 0x10)) { + if (!fill(0, KeyBytes, 0x5a)) { std::puts("the key range has no host import: pending key fills are not tested"); return; } @@ -313,18 +343,39 @@ void Run(AgcDriver::VulkanDevice& device, std::uint8_t* block) { std::puts("host imports are compared, not watched: the write-watched block runs as unwatched"); watched = false; } - readPending(DccKeys::Uncompressed, false, "a pending fill of the keys with 0x10, which is no clear code"); + readPending(DccKeys::Uncompressed, false, "a pending fill of the keys with 0x5a, which is no clear code"); + Require(fill(0, KeyBytes, 0x10), "the comp-to-single fill of the keys was not recorded"); + readPending(DccKeys::ClearSingle, false, "a pending comp-to-single fill of the keys"); + RequireKeys(keys, 0x10, "the keys after a pending comp-to-single fill landed"); Require(fill(0, KeyBytes / 2, 0x00) && fill(KeyBytes / 2, KeyBytes / 2, 0x00), "the 0000 fills of the key halves were not recorded"); - readPending(DccKeys::Clear0000, true, "pending 0000 fills over both halves of the keys"); + readPending(DccKeys::Clear0000, false, "pending 0000 fills over both halves of the keys"); + RequireKeys(keys, 0x00, "the keys after pending 0000 fills over both halves landed"); + Require(fill(0, KeyBytes / 2, 0x00) && fill(KeyBytes / 2, KeyBytes / 2, 0x40), "the 0000 and 0001 fills of the key halves were not recorded"); + readPending(DccKeys::Uncompressed, false, "a pending 0000 fill and a pending 0001 fill over the halves of the keys"); Require(fill(0, KeyBytes / 2, 0x40), "the 0001 fill of the first key half was not recorded"); Dispatch(device, ClearKeysCode, clearTailData, false); readPending(DccKeys::Uncompressed, true, "a pending 0001 fill over half the keys and a kernel's 0000 store over the other half"); + Require(fill(0, KeyBytes, 0x40), "the 0001 fill of the keys was not recorded"); + Dispatch(device, ClearKeysCode, clearTailData, false); + Require(fill(0, KeyBytes / 2, 0x40), "the 0001 fill of the first key half over the kernel's store was not recorded"); + readPending(DccKeys::Uncompressed, true, "a pending 0001 fill over half the keys, whose other half a kernel stored over an earlier 0001 fill"); Require(fill(0, KeyBytes, 0x10) && fill(0, KeyBytes, 0x00), "the 0x10 and 0000 fills of the keys were not recorded"); - readPending(DccKeys::Clear0000, true, "a pending 0x10 fill of the keys overwritten by a pending 0000 fill"); + readPending(DccKeys::Clear0000, false, "a pending 0x10 fill of the keys overwritten by a pending 0000 fill"); + RequireKeys(keys, 0x00, "the keys after a pending 0x10 fill and a pending 0000 fill landed"); + Require(fill(0, KeyBytes, 0x10), "the 0x10 fill of the keys under a kernel's store was not recorded"); + Dispatch(device, ClearKeysCode, clearData, false); + Require(fill(0, KeyBytes, 0x40), "the 0001 fill over the kernel's store was not recorded"); + readPending(DccKeys::Clear0001, false, "a pending 0001 fill of the keys over a kernel's 0000 store over a pending 0x10 fill"); + RequireKeys(keys, 0x40, "the keys after a 0x10 fill, a kernel's 0000 store and a 0001 fill landed"); Require(fill(KeyBytes / 2, KeyBytes / 2, 0x10), "the 0x10 fill of the second key half was not recorded"); device.WaitIdle(); Require(fill(0, KeyBytes / 2, 0x00), "the 0000 fill of the first key half was not recorded"); readPending(DccKeys::Uncompressed, false, "a pending 0000 fill over half the keys whose other half holds 0x10"); + Require(fill(KeyBytes / 2, KeyBytes / 2, 0xc0), "the 1111 fill of the second key half was not recorded"); + device.WaitIdle(); + Require(fill(0, KeyBytes / 2, 0xc0), "the 1111 fill of the first key half was not recorded"); + readPending(DccKeys::Clear1111, false, "a pending 1111 fill over half the keys whose other half holds 1111"); + RequireKeys(keys, 0xc0, "the keys after a pending 1111 fill over a half that holds 1111 landed"); Require(fill(0, KeyBytes, 0x10), "the 0x10 fill of the keys was not recorded"); device.WaitIdle(); Require(fill(0, KeyBytes / 2, 0x00) && fill(KeyBytes / 2, KeyBytes / 2, 0x00), "the 0000 fills of the key halves were not recorded"); @@ -343,6 +394,197 @@ void Run(AgcDriver::VulkanDevice& device, std::uint8_t* block) { RequireKeys(keys, 0x40, "a write marked after a 0001 key fill over a cleared image, before a refresh saw the fill"); } +void RunReusedKeys(AgcDriver::VulkanDevice& device, std::uint8_t* block) { + auto* texels = block; + auto* keys = block + KeysOffset; + const auto surface = AddressOf(texels); + std::memset(texels, 0x55, SurfaceBytes); + std::memset(keys, 0x00, KeyBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + const auto texture = TextureDescriptor(texels, keys); + std::vector writeData(16, 0u); + std::copy(texture.begin(), texture.end(), writeData.begin() + 4); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Dispatch(device, WriteCode, writeData); + const auto image = StorageTexture::FindPending(surface, SurfaceBytes); + Require(image != nullptr && image->UploadedKeys() == DccKeys::Uncompressed, "reused keys: the first write did not store the keys uncompressed"); + RequireKeys(keys, 0xff, "reused keys: the keys after the first write"); + std::memset(keys, 0x5a, KeyBytes); + StorageTexture::FlushPending(surface, SurfaceBytes, nullptr, "test reused keys"); + device.WaitIdle(); + RequireKeys(keys, 0x5a, "the key bytes the title reused after the first write, after the image's write-back"); + Require(Count(texels, SurfaceBytes, 0xff) == Threads * 4u, "reused keys: the write-back lost the image's results"); +} + +void RunSingleKeys(AgcDriver::VulkanDevice& device, std::uint8_t* block) { + auto* texels = block; + auto* keys = block + KeysOffset; + const auto surface = AddressOf(texels); + std::vector original(SurfaceBytes / 4); + for (std::size_t index = 0; index < original.size(); ++index) original[index] = 0x01000000u + static_cast(index); + std::memcpy(texels, original.data(), SurfaceBytes); + std::memset(keys, 0x10, KeyBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + const auto texture = TextureDescriptor(texels, keys); + std::vector writeData(16, 0u); + std::copy(texture.begin(), texture.end(), writeData.begin() + 4); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Dispatch(device, WriteCode, writeData); + const auto image = StorageTexture::FindPending(surface, SurfaceBytes); + Require(image != nullptr && image->UploadedKeys() == DccKeys::Uncompressed, "comp-to-single keys: the first write did not store the keys uncompressed"); + RequireKeys(keys, 0xff, "comp-to-single keys: the keys after the first write"); + StorageTexture::FlushPending(surface, SurfaceBytes, nullptr, "test comp-to-single keys"); + device.WaitIdle(); + const AgcDriver::Graphics::ColorTargetLayout layout(Side, Side, AgcDriver::Graphics::ColorTileMode::RenderTarget, 4); + std::size_t wrong = 0; + for (std::uint32_t y = 0; y < Side; ++y) { + for (std::uint32_t x = 0; x < Side; ++x) { + const auto offset = layout.Offset(x, y); + std::uint32_t texel = 0; + std::memcpy(&texel, texels + offset, 4); + const auto expected = y == 0 && x < Threads ? 0xffffffffu : original[offset / 256 * 64]; + if (texel != expected) ++wrong; + } + } + char message[256]; + std::snprintf(message, sizeof(message), "comp-to-single keys: %zu of %u written-back texels are neither the kernel's write nor the first texel of their 256-byte block", wrong, Side * Side); + Require(wrong == 0, message); +} + +void RunSingleKeysSampled(AgcDriver::VulkanDevice& device, std::uint8_t* block, std::uint8_t* outputBlock, const char* memory) { + auto* texels = block; + auto* keys = block + KeysOffset; + auto* output = reinterpret_cast(outputBlock); + const bool watched = AgcDriver::GuestMemory::Watched(AddressOf(texels), SurfaceBytes); + const AgcDriver::Graphics::ColorTargetLayout layout(Side, Side, AgcDriver::Graphics::ColorTileMode::RenderTarget, 4); + const auto first = layout.Offset(1, 0) / 256 * 256; + std::memset(texels, 0x00, SurfaceBytes); + std::memset(texels + first, 0xff, 4); + std::memset(keys, 0x10, KeyBytes); + const auto texture = TextureDescriptor(texels, keys); + const auto results = BufferDescriptor(output, Threads * 16u); + std::vector userData(24, 0u); + std::copy(results.begin(), results.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + std::copy(PointSampler.begin(), PointSampler.end(), userData.begin() + 12); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + const auto sample = [&](std::uint32_t expected, const char* when) { + std::fill(output, output + Threads * 4u, 0xdeadbeefu); + AgcDriver::GuestMemory::BumpCollectEpoch(); + Dispatch(device, SampleSecondTexelCode, userData); + for (std::uint32_t index = 0; index < Threads * 4u; ++index) { + char message[256]; + std::snprintf(message, sizeof(message), "comp-to-single keys sampled %s from %s memory: component %u of thread %u is 0x%08x, expected 0x%08x", when, memory, index % 4u, index / 4u, output[index], expected); + Require(output[index] == expected, message); + } + }; + sample(0x3f800000u, "before the first texel of the block changed"); + Require(!watched || AgcDriver::GuestMemory::Watched(AddressOf(texels), SurfaceBytes), "comp-to-single keys sampled from write-watched memory: the first sampling left the surface unwatched"); + std::memset(texels + first, 0x00, 4); + sample(0u, "after the first texel of the block changed"); +} + +void RunSingleKeysOverPendingImage(AgcDriver::VulkanDevice& device, std::uint8_t* block, std::uint8_t* outputBlock) { +#ifndef _WIN32 + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + const auto decided = AgcDriver::Graphics::PrepareImportWatch(context); + AgcDriver::Graphics::SetImportWatch(context, AgcDriver::Graphics::ImportWatch::Watch); + struct Restore { + const AgcDriver::Graphics::Context& context; + AgcDriver::Graphics::ImportWatch decided; + ~Restore() { AgcDriver::Graphics::SetImportWatch(context, decided); } + } restore{context, decided}; +#endif + auto* texels = block; + auto* keys = block + KeysOffset; + auto* output = reinterpret_cast(outputBlock); + const auto surface = AddressOf(texels); + const AgcDriver::Graphics::ColorTargetLayout layout(Side, Side, AgcDriver::Graphics::ColorTileMode::RenderTarget, 4); + std::memset(texels, 0x00, SurfaceBytes); + std::memset(texels + layout.Offset(1, 0) / 256 * 256, 0xff, 4); + std::memset(keys, 0xff, KeyBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + const auto texture = TextureDescriptor(texels, keys); + std::vector writeData(16, 0u); + std::copy(texture.begin(), texture.end(), writeData.begin() + 4); + const auto results = BufferDescriptor(output, Threads * 16u); + std::vector sampleData(24, 0u); + std::copy(results.begin(), results.end(), sampleData.begin()); + std::copy(texture.begin(), texture.end(), sampleData.begin() + 4); + std::copy(PointSampler.begin(), PointSampler.end(), sampleData.begin() + 12); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Dispatch(device, WriteRowCode, writeData); + const auto image = StorageTexture::FindPending(surface, SurfaceBytes); + Require(image != nullptr && image->UploadedKeys() == DccKeys::Uncompressed, "comp-to-single keys over a pending storage image: the row write left no image pending under uncompressed keys"); + Require(AgcDriver::GuestMemory::Watched(surface, SurfaceBytes), "comp-to-single keys over a pending storage image: the image's host import left the surface unwatched"); + std::memset(keys, 0x10, KeyBytes); + std::fill(output, output + Threads * 4u, 0xdeadbeefu); + AgcDriver::GuestMemory::BumpCollectEpoch(); + Dispatch(device, SampleSecondTexelCode, sampleData); + for (std::uint32_t index = 0; index < Threads * 4u; ++index) { + char message[256]; + std::snprintf(message, sizeof(message), "comp-to-single keys stored over a pending storage image: component %u of thread %u is 0x%08x, expected 0x3f800000", index % 4u, index / 4u, output[index]); + Require(output[index] == 0x3f800000u, message); + } +} + +void RunSampledKeysBehindKernel(AgcDriver::VulkanDevice& device, std::uint8_t* block) { + auto* texels = block; + auto* keys = block + KeysOffset; + std::memset(texels, 0x55, SurfaceBytes); + for (std::size_t i = 0; i < KeyBytes; i += 4) { + keys[i] = 0x00; + keys[i + 1] = 0x00; + keys[i + 2] = 0x80; + keys[i + 3] = 0x3f; + } + AgcDriver::GuestMemory::BumpCollectEpoch(); + const auto texture = TextureDescriptor(texels, keys); + const auto resource = AgcDriver::Graphics::DecodeTextureResource(texture); + std::vector clearData(16, 0u); + const auto head = BufferDescriptor(keys, KeyBytes / 2); + std::copy(head.begin(), head.end(), clearData.begin()); + std::vector clearTailData(16, 0u); + const auto tail = BufferDescriptor(keys + KeyBytes / 2, KeyBytes / 2); + std::copy(tail.begin(), tail.end(), clearTailData.begin()); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Require(AgcDriver::Graphics::TextureClearKeys(resource, SurfaceBytes) == DccKeys::Uncompressed, "sampled keys behind a kernel: key bytes that are no keys were not read as stored"); + Dispatch(device, ClearKeysCode, clearData, false); + Dispatch(device, ClearKeysCode, clearTailData, false); + const auto read = AgcDriver::Graphics::TextureClearKeys(resource, SurfaceBytes); + char message[256]; + std::snprintf(message, sizeof(message), "a sampled texture whose keys a recorded kernel stores 0000 over read %s before the kernel ran", AgcDriver::Graphics::DccKeysName(read)); + Require(read == DccKeys::Clear0000, message); + device.WaitIdle(); + RequireKeys(keys, 0x00, "sampled keys behind a kernel: the keys after the kernel"); +} + +void RunUniformKeysBehindKernel(AgcDriver::VulkanDevice& device, std::uint8_t* block) { + auto* texels = block; + auto* keys = block + KeysOffset; + std::memset(texels, 0x55, SurfaceBytes); + std::memset(keys, 0xff, KeyBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + const auto texture = TextureDescriptor(texels, keys); + const auto resource = AgcDriver::Graphics::DecodeTextureResource(texture); + std::vector clearData(16, 0u); + const auto head = BufferDescriptor(keys, KeyBytes / 2); + std::copy(head.begin(), head.end(), clearData.begin()); + std::vector clearTailData(16, 0u); + const auto tail = BufferDescriptor(keys + KeyBytes / 2, KeyBytes / 2); + std::copy(tail.begin(), tail.end(), clearTailData.begin()); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Require(AgcDriver::Graphics::TextureClearKeys(resource, SurfaceBytes) == DccKeys::Uncompressed, "uniform sampled keys behind a kernel: the keys before the kernel were not read uncompressed"); + Dispatch(device, ClearKeysCode, clearData, false); + Dispatch(device, ClearKeysCode, clearTailData, false); + const auto read = AgcDriver::Graphics::TextureClearKeys(resource, SurfaceBytes); + char message[256]; + std::snprintf(message, sizeof(message), "a sampled texture whose uniform keys a recorded kernel stores 0000 over read %s before the kernel ran", AgcDriver::Graphics::DccKeysName(read)); + Require(read == DccKeys::Clear0000, message); + device.WaitIdle(); + RequireKeys(keys, 0x00, "uniform sampled keys behind a kernel: the keys after the kernel"); +} + void RunUntrackedReuse(AgcDriver::VulkanDevice& device, std::uint8_t* block) { const auto address = AddressOf(block); std::memset(block, 0x55, BlockBytes); @@ -393,7 +635,124 @@ void RunUntrackedReuse(AgcDriver::VulkanDevice& device, std::uint8_t* block) { Require(image->Refresh(), "the GPU-edited image was uploaded twice"); } +#ifndef _WIN32 +// An import window stamps the pages whose write protection it saw resolved as a possible CPU +// store. A driver that pins imported pages for writing (NVIDIA on Linux) resolves it without +// storing anything, which a store of the byte already there reproduces on any device. +void RunImportWindow(AgcDriver::VulkanDevice& device, std::uint8_t* block) { + auto* texels = block; + auto* keys = block + KeysOffset; + const auto surface = AddressOf(texels); + std::memset(texels, 0x55, SurfaceBytes); + std::memset(keys, 0x00, KeyBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + const auto texture = TextureDescriptor(texels, keys); + std::vector writeData(16, 0u); + std::copy(texture.begin(), texture.end(), writeData.begin() + 4); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Dispatch(device, WriteCode, writeData); + const auto image = StorageTexture::FindPending(surface, SurfaceBytes); + Require(image != nullptr, "import window: the written surface has no pending image"); + if (!AgcDriver::GuestMemory::Watched(surface, SurfaceBytes)) { + std::puts("host imports are compared, not watched: the import window is not tested"); + StorageTexture::FlushPending(surface, SurfaceBytes, nullptr, "test"); + device.WaitIdle(); + return; + } + const auto storeInWindow = [&](std::uint8_t value) { + auto* page = reinterpret_cast(texels); + Require(AgcDriver::GuestMemory::ImportWatched(surface, SurfaceBytes, [&] { + page[0] = value; + return true; + }), "import window: the import failed"); + }; + const auto pendingVersion = image->Version(); + storeInWindow(0x55); + Require(image->Refresh(), "import window: a pending image was uploaded again after an import that stored nothing new"); + Require(image->Version() == pendingVersion && StorageTexture::FindPending(surface, SurfaceBytes) == image, "import window: an import that stored nothing new wrote the pending image back"); + storeInWindow(0x66); + Require(!image->Refresh(), "import window: a CPU store within an import window was missed"); + StorageTexture::FlushPending(surface, SurfaceBytes, nullptr, "test"); + device.WaitIdle(); +} +#endif + #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); + descriptor[2] = ((Side - 1u) >> 2u) | ((2 * Side - 1u) << 14u); + descriptor[6] = 0; + descriptor[7] = 0; + std::vector userData(16, 0u); + std::copy(descriptor.begin(), descriptor.end(), userData.begin() + 4); + std::lock_guard gpu(GpuMutex()); + BumpCollectEpoch(); + Dispatch(device, WriteCode, userData); + const auto image = StorageTexture::FindPending(address, BlockBytes); + Require(image != nullptr && image->GuestBytes() == BlockBytes, "hinted import: the two-block image has no pending results"); + if (Watched(address, BlockBytes)) { + std::puts("the write-watched image stayed watched: its untracked hint is not tested"); + StorageTexture::FlushPending(address, BlockBytes, nullptr, "test hinted import"); + device.WaitIdle(); + return; + } + StorageTexture::FlushPending(address, BlockBytes, nullptr, "test hinted import"); + device.WaitIdle(); + for (int settle = 0; settle < 2; ++settle) { + BumpCollectEpoch(); + image->Refresh(); + } + const auto version = image->Version(); + const auto compares = StorageTexture::UntrackedCompares(); + for (int epoch = 0; epoch < 50; ++epoch) { + BumpCollectEpoch(); + Require(image->Refresh() && image->Version() == version, "hinted import: an unchanged image was uploaded again"); + } + char message[192]; + std::snprintf(message, sizeof(message), "hinted import: 50 collect epochs without a store compared the image %llu times", static_cast(StorageTexture::UntrackedCompares() - compares)); + Require(StorageTexture::UntrackedCompares() == compares, message); + block[SurfaceBytes + 0x100] = 0x33; + BumpCollectEpoch(); + Require(!image->Refresh() && image->Version() != version, "hinted import: a CPU store into the imported image was missed"); + const auto stored = StorageTexture::UntrackedCompares(); + Require(stored > compares, "hinted import: the CPU store was found without a compare"); + BumpCollectEpoch(); + Require(image->Refresh() && StorageTexture::UntrackedCompares() - stored <= 1, "hinted import: the stored image did not settle"); + const auto settled = image->Version(); + block[SurfaceBytes + 0x200] = 0x44; + std::array marked{}; + std::size_t count = marked.size(); + Require(GuestArena::GuestArenaCollectWrites_nid_postfix(static_cast(address + SurfaceBytes), 4096, marked.data(), &count, true) && count == 1, "hinted import: the store to hide was not marked"); + MarkWritten(address + SurfaceBytes + 0x200, 1); + BumpCollectEpoch(); + Require(!image->Refresh() && image->Version() != settled, "hinted import: a recorded store without a write-watch mark was missed"); + Dispatch(device, WriteCode, userData); + Require(StorageTexture::FindPending(address, BlockBytes) == image, "hinted import: the second write left no pending results"); + BumpCollectEpoch(); + image->Refresh(); + const auto pendingVersion = image->Version(); + const auto beforeRewrite = StorageTexture::UntrackedCompares(); + auto* rewritten = reinterpret_cast(block + SurfaceBytes + 0x2000); + *rewritten = *rewritten; + BumpCollectEpoch(); + Require(image->Refresh() && image->Version() == pendingVersion, "hinted import: a store of the bytes already there uploaded the image"); + Require(StorageTexture::UntrackedCompares() - beforeRewrite == 1, "hinted import: a store of the bytes already there did not cost exactly one compare"); + Require(StorageTexture::FindPending(address, BlockBytes) == image, "hinted import: a store of the bytes already there dropped the pending results"); + BumpCollectEpoch(); + Require(image->Refresh() && StorageTexture::UntrackedCompares() - beforeRewrite == 1, "hinted import: the image was compared again without a store"); + StorageTexture::FlushPending(address, BlockBytes, nullptr, "test hinted import"); + device.WaitIdle(); + Require(Count(block, SurfaceBytes, 0xff) == Threads * 4u, "hinted import: the pending results were lost"); + Require(block[SurfaceBytes + 0x100] == 0x33 && block[SurfaceBytes + 0x200] == 0x44, "hinted import: the write-back lost the CPU store"); +} + void RunSharedImport(AgcDriver::VulkanDevice& device, std::uint8_t* block) { using namespace AgcDriver::GuestMemory; const auto address = AddressOf(block); @@ -432,12 +791,32 @@ int main() { std::optional watched; std::optional untracked; std::optional shared; + std::optional window; + std::optional pendingSingle; + std::optional hinted; const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; block.emplace(false); Run(*device, block->Data()); untracked.emplace(false); RunUntrackedReuse(*device, untracked->Data()); + std::optional reused; + reused.emplace(false); + RunReusedKeys(*device, reused->Data()); + std::optional single; + single.emplace(false); + RunSingleKeys(*device, single->Data()); + std::optional sampled; + sampled.emplace(false); + std::optional sampledOutput; + sampledOutput.emplace(false); + RunSingleKeysSampled(*device, sampled->Data(), sampledOutput->Data(), "unwatched"); + std::optional behind; + behind.emplace(false); + RunSampledKeysBehindKernel(*device, behind->Data()); + std::optional uniform; + uniform.emplace(false); + RunUniformKeysBehindKernel(*device, uniform->Data()); #ifdef _WIN32 shared.emplace(true, true); RunSharedImport(*device, shared->Data()); @@ -446,6 +825,22 @@ int main() { if (AgcDriver::GuestMemory::WriteWatched()) { watched.emplace(true); Run(*device, watched->Data()); + std::optional sampledWatched; + sampledWatched.emplace(true); + RunSingleKeysSampled(*device, sampledWatched->Data(), sampledOutput->Data(), "write-watched"); +#ifdef _WIN32 + pendingSingle.emplace(true, true); +#else + pendingSingle.emplace(true); +#endif + RunSingleKeysOverPendingImage(*device, pendingSingle->Data(), sampledOutput->Data()); +#ifndef _WIN32 + window.emplace(true); + RunImportWindow(*device, window->Data()); +#else + hinted.emplace(true); + RunHintedImport(*device, hinted->Data()); +#endif } else { std::puts("guest memory has no write watch: a write-watched surface is not tested"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthBiasPerFace.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthBiasPerFace.cpp new file mode 100644 index 0000000000..f672de40ff --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthBiasPerFace.cpp @@ -0,0 +1,294 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#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 float SurfaceDepth = 0.5f; +constexpr float Bias = 1048576.0f; +alignas(256) std::array Pixels{}; +alignas(256) std::array DepthMemory{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e0e02f2, 0xf800180f, 0x07070707, 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, float frontBias, float backBias) { + 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, SurfaceDepth, 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.depth = AgcDriver::Graphics::DepthTarget{reinterpret_cast(DepthMemory.data()), 0, {Width, Height}, VK_FORMAT_D32_SFLOAT, SurfaceDepth, 0}; + state.depthTest = true; + state.depthWrite = false; + state.depthCompare = VK_COMPARE_OP_LESS; + state.depthBias = true; + state.depthBiasConstant = frontBias; + state.depthBiasPerFace = frontBias != backBias; + state.backDepthBiasConstant = backBias; + 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(bool frontDrawn, bool backDrawn, std::string_view what) { + for (const auto& [x, y, front] : std::array, 2>{{{4, 4, true}, {20, 4, false}}}) { + const bool drawn = front ? frontDrawn : backDrawn; + const auto expected = drawn ? std::byte{255} : Kept; + const auto actual = Pixels[(y * Width + x) * 4u]; + Require(actual == expected, std::string(what) + ": the " + (front ? "front" : "back") + "-facing triangle at (" + std::to_string(x) + ", " + std::to_string(y) + ") is " + std::to_string(static_cast(actual)) + ", expected " + (drawn ? "drawn" : "depth-rejected")); + } +} + +class GuestBlock { +public: + static constexpr std::size_t BlockBytes = 65536; + + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "cannot allocate a guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() const { return block; } + std::uint64_t Address() const { return static_cast(reinterpret_cast(block)); } + +private: + std::uint8_t* block = nullptr; +}; + +void DepthDrawRecipe(AgcDriver::VulkanDevice& device) { + constexpr std::uint32_t TargetWidth = 128; + constexpr std::uint32_t TargetHeight = 128; + const AgcDriver::Graphics::ColorTargetLayout colorLayout(TargetWidth, TargetHeight, AgcDriver::Graphics::ColorTileMode::RenderTarget, 4); + + const GuestBlock guestVertices; + const GuestBlock guestTarget; + alignas(256) static std::array RecipeDepthMemory{}; + alignas(256) static std::array RecipeAlternateDepthMemory{}; + + std::memset(guestTarget.Data(), static_cast(Kept), GuestBlock::BlockBytes); + RecipeDepthMemory.fill(1.0f); + RecipeAlternateDepthMemory.fill(1.0f); + + auto* targetVertices = reinterpret_cast*>(guestVertices.Data()); + for (std::size_t index = 0; index < Corners.size(); ++index) { + targetVertices[index] = {Corners[index][0] * 2.0f / TargetWidth - 1.0f, 1.0f - Corners[index][1] * 2.0f / TargetHeight, SurfaceDepth, 1.0f}; + } + const auto target = device.Target(); + + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(targetVertices, 16u, static_cast(Corners.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {WaveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + const std::vector pixelUserData(8, 0u); + const std::array pixelMemory{{{reinterpret_cast(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, + {WaveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, WaveSize, WaveSize, std::nullopt, std::nullopt}; + state.color = {guestTarget.Address(), {TargetWidth, TargetHeight}, VK_FORMAT_R8G8B8A8_UNORM, colorLayout.Bytes(), 0xe4u, AgcDriver::Graphics::ColorTileMode::RenderTarget}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.depth = AgcDriver::Graphics::DepthTarget{reinterpret_cast(RecipeDepthMemory.data()), 0, {TargetWidth, TargetHeight}, VK_FORMAT_D32_SFLOAT, SurfaceDepth, 0}; + state.depthTest = true; + state.depthWrite = false; + state.depthCompare = VK_COMPARE_OP_LESS; + state.depthBias = true; + state.depthBiasConstant = -Bias; + state.depthBiasPerFace = false; + state.backDepthBiasConstant = -Bias; + state.renderExtent = {TargetWidth, TargetHeight}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(TargetHeight), static_cast(TargetWidth), -static_cast(TargetHeight), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {TargetWidth, TargetHeight}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Corners.size()), 0, 1, 0, false}; + + std::shared_ptr recipe; + device.Draw(state, draw, shaders, {}, &recipe); + device.WaitIdle(); + Require(recipe != nullptr, "a direct draw with depth target must produce a recipe"); + Require(recipe->hasDepth, "the recipe must record depth target presence"); + Require(recipe->targetViews.size() == 2, "the recipe must record color and depth views"); + + const auto outcome = device.DrawFromRecipe(state, draw, shaders, {}, recipe); + device.WaitIdle(); + Require(outcome == AgcDriver::RecipeOutcome::Recorded, "drawing with depth from recipe must record successfully"); + + auto mutatedAddressState = state; + mutatedAddressState.depth = AgcDriver::Graphics::DepthTarget{reinterpret_cast(RecipeAlternateDepthMemory.data()), 0, {TargetWidth, TargetHeight}, VK_FORMAT_D32_SFLOAT, SurfaceDepth, 0}; + const auto rejectedAddressOutcome = device.DrawFromRecipe(mutatedAddressState, draw, shaders, {}, recipe); + Require(rejectedAddressOutcome == AgcDriver::RecipeOutcome::Rebuild, "a recipe with depth must be rejected when depth target address changes"); + + auto removedDepthState = state; + removedDepthState.depth.reset(); + const auto rejectedDepthOutcome = device.DrawFromRecipe(removedDepthState, draw, shaders, {}, recipe); + Require(rejectedDepthOutcome == AgcDriver::RecipeOutcome::Rebuild, "a recipe with depth must be rejected when depth target is removed"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Draw(*device, -Bias, Bias); + Check(true, false, "front faces pulled closer, back faces pushed away"); + Draw(*device, Bias, -Bias); + Check(false, true, "front faces pushed away, back faces pulled closer"); + Draw(*device, -Bias, -Bias); + Check(true, true, "both faces pulled closer"); + Draw(*device, -Bias, 0.0f); + Check(true, false, "only front faces biased"); + DepthDrawRecipe(*device); + std::puts("per-face depth bias tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} 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/DepthCompareCopy.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthCompareCopy.cpp new file mode 100644 index 0000000000..1db56eaa87 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthCompareCopy.cpp @@ -0,0 +1,315 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +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 Type2D = 9; +constexpr std::uint32_t Type2DArray = 13; +constexpr std::uint32_t LessEqual = 3; +constexpr std::uint32_t ClampEdge = 2; +constexpr std::size_t BlockBytes = 2u << 20u; + +struct Shape { + std::uint32_t format; + std::uint32_t width; + std::uint32_t height; + std::uint32_t layers; + std::uint32_t mips; + bool array; +}; + +alignas(256) constexpr std::array StoreHalfCode{ + 0x7e280d00, 0x102828ff, 0x3d000000, 0x7e3c0300, 0x7e3e0280, 0xf0201108, 0x0001141e, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array StoreQuarterCode{ + 0x7e280d00, 0x102828ff, 0x3c800000, 0x7e3c0300, 0x7e3e0280, 0xf0201108, 0x0001141e, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array StoreLayerMipCode{ + 0x7e280d00, 0x1028280e, 0x7e3c0300, 0x7e3e0280, 0x7e40020c, 0x7e42020d, 0xf0241128, 0x0001141e, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array SampleCode{ + 0x1614008c, 0xe03c1000, 0x8000010a, 0xbf8c3f70, 0xf0bc0108, 0x00820401, 0xbf8c3f70, + 0x34160082, 0xe0701000, 0x8001040b, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array SampleLayerMipCode{ + 0x1614008c, 0xe03c1000, 0x8000010a, 0x7e080214, 0x7e0a0215, 0xbf8c3f70, 0xf0b00128, 0x00820601, 0xbf8c3f70, + 0x34160082, 0xe0701000, 0x8001060b, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +std::span Input; +std::span Output; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, PAGE_READWRITE, BlockBytes); +#else + constexpr std::uintptr_t alignment = 65536; + void* mapped = mmap(nullptr, BlockBytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + alignment - 1) & ~(alignment - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + BlockBytes != begin + BlockBytes + alignment) munmap(reinterpret_cast(aligned + BlockBytes), begin + alignment - aligned); + block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, BlockBytes); + } +#endif + Require(block != nullptr, "depth compare copy: cannot allocate the guest block"); + Require(AgcDriver::GuestMemory::Watched(reinterpret_cast(block), BlockBytes), "depth compare copy: the guest block is not write-watched"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + AgcDriver::GuestMemory::CollectWritesUncached(reinterpret_cast(block), BlockBytes); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, BlockBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, BlockBytes); +#else + munmap(block, BlockBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, BlockBytes); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array TextureDescriptor(const void* data, const Shape& shape) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (shape.format << 20u) | (((shape.width - 1u) & 3u) << 30u), + ((shape.width - 1u) >> 2u) | ((shape.height - 1u) << 14u), + 0xfacu | ((shape.mips - 1u) << 16u) | ((shape.array ? Type2DArray : Type2D) << 28u), + shape.array ? shape.layers - 1u : 0u, + (shape.mips - 1u) << 4u, + 0u, 0u, + }; +} + +std::array SamplerDescriptor(bool mips) { + return {ClampEdge | (ClampEdge << 3u) | (ClampEdge << 6u) | (LessEqual << 12u), mips ? 0xf00u << 12u : 0u, mips ? 1u << 26u : 0u, 0u}; +} + +std::uint32_t Bits(float value) { + std::uint32_t bits = 0; + std::memcpy(&bits, &value, sizeof(bits)); + return bits; +} + +ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::span code, std::span userData) { + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +void Store(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const Shape& shape, std::span code, std::uint32_t layer = 0, std::uint32_t mip = 0, float scale = 0.0f) { + std::vector userData(16, 0u); + const auto texture = TextureDescriptor(texels, shape); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + userData[12] = layer; + userData[13] = mip; + userData[14] = Bits(scale); + const auto result = Compile(device, code, userData); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +ShaderRecompiler::RecompileResult SampleProgram(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const Shape& shape, std::uint32_t layer = 0, std::uint32_t mip = 0) { + std::vector userData(24, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size_bytes())); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size_bytes())); + const auto texture = TextureDescriptor(texels, shape); + const auto sampler = SamplerDescriptor(shape.mips > 1u); + 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); + userData[20] = Bits(static_cast(layer)); + userData[21] = Bits(static_cast(mip)); + return Compile(device, shape.array ? std::span(SampleLayerMipCode) : std::span(SampleCode), userData); +} + +void Sample(AgcDriver::VulkanDevice& device, const ShaderRecompiler::RecompileResult& program, const Shape& shape, float scale, const std::string& what, std::uint32_t layer = 0, std::uint32_t mip = 0) { + const auto width = std::max(shape.width >> mip, 1u); + const auto height = std::max(shape.height >> mip, 1u); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const float value = static_cast(tid) * scale; + Input[tid * 3u + 0u] = (tid & 1u) != 0u ? value - scale / 4.0f : value + scale / 4.0f; + Input[tid * 3u + 1u] = (static_cast(tid) + 0.5f) / static_cast(width); + Input[tid * 3u + 2u] = 0.5f / static_cast(height); + } + std::fill(Output.begin(), Output.end(), -1.0f); + const auto* code = shape.array ? SampleLayerMipCode.data() : SampleCode.data(); + device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(code)); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const float expected = (tid & 1u) != 0u ? 1.0f : 0.0f; + Require(Output[tid] == expected, what + ": thread " + std::to_string(tid) + " compared to " + std::to_string(Output[tid]) + ", expected " + std::to_string(expected)); + } +} + +std::uint64_t SurfaceBytes(std::uint8_t* texels, const Shape& shape) { + const auto surface = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(texels, shape))); + Require(surface.guestBytes >= (1u << 20u) && surface.guestBytes <= BlockBytes, "depth compare copy: the surface does not fit the guest block or is below the depth copy floor"); + return surface.guestBytes; +} + +std::uint64_t PendingVersion(std::uint8_t* texels, std::uint64_t bytes, const std::string& what) { + const auto source = AgcDriver::Graphics::StorageTexture::FindPending(reinterpret_cast(texels), bytes); + Require(source != nullptr, what + ": no storage image holds the surface's results"); + return source->Version(); +} + +void Settle(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint64_t bytes) { + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(texels), bytes, nullptr, "test"); + device.WaitIdle(); + AgcDriver::GuestMemory::CollectWritesUncached(reinterpret_cast(texels), bytes); +} + +void RequireCounts(std::uint64_t textures, std::uint64_t refreshes, const std::string& what) { + const auto counts = AgcDriver::Graphics::Texture::TakeDepthCopyCounts(); + Require(counts.textures == textures && counts.refreshes == refreshes, what + ": " + std::to_string(counts.textures) + " depth copy textures and " + std::to_string(counts.refreshes) + " refreshes, expected " + std::to_string(textures) + " and " + std::to_string(refreshes)); +} + +void RequireNewerVersion(std::uint64_t before, std::uint64_t after, const std::string& what) { + Require(after > before, what + ": the storage image version stayed at " + std::to_string(before)); +} + +float Texel(const std::uint8_t* texels, std::uint32_t x) { + float value = 0.0f; + std::memcpy(&value, texels + static_cast(x) * sizeof(float), sizeof(float)); + return value; +} + +void RequireGuestTexels(const std::uint8_t* texels, float scale, const std::string& what) { + for (std::uint32_t x = 0; x < Threads; ++x) { + const float expected = static_cast(x) * scale; + Require(Texel(texels, x) == expected, what + ": texel " + std::to_string(x) + " is " + std::to_string(Texel(texels, x)) + ", expected " + std::to_string(expected)); + } +} + +void SingleLevelCase(AgcDriver::VulkanDevice& device, const Shape& shape, const std::string& name) { + GuestBlock block; + auto* texels = block.Data(); + const auto bytes = SurfaceBytes(texels, shape); + Store(device, texels, shape, StoreQuarterCode); + Settle(device, texels, bytes); + AgcDriver::Graphics::Texture::TakeDepthCopyCounts(); + Store(device, texels, shape, StoreHalfCode); + const auto program = SampleProgram(device, texels, shape); + Sample(device, program, shape, 1.0f / 32.0f, name + ": comparison sample of pending results"); + RequireCounts(1, 1, name + ": first comparison sample"); + const auto before = PendingVersion(texels, bytes, name); + Store(device, texels, shape, StoreQuarterCode); + RequireNewerVersion(before, PendingVersion(texels, bytes, name), name + ": second store"); + Sample(device, program, shape, 1.0f / 64.0f, name + ": comparison sample of the same descriptor set after a second store"); + RequireCounts(0, 1, name + ": comparison sample after a second store"); + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(texels), bytes, nullptr, "test"); + device.WaitIdle(); + if (shape.format == Format32Float) RequireGuestTexels(texels, 1.0f / 64.0f, name + ": flushed results"); +} + +void LayersAndMipsCase(AgcDriver::VulkanDevice& device) { + const Shape shape{Format32Float, 256, 256, 4, 3, true}; + const std::string name = "R32F 256x256, 4 layers, 3 mips"; + GuestBlock block; + auto* texels = block.Data(); + const auto bytes = SurfaceBytes(texels, shape); + Store(device, texels, shape, StoreLayerMipCode, 0, 0, 1.0f / 512.0f); + Settle(device, texels, bytes); + AgcDriver::Graphics::Texture::TakeDepthCopyCounts(); + Store(device, texels, shape, StoreLayerMipCode, 0, 0, 1.0f / 32.0f); + Store(device, texels, shape, StoreLayerMipCode, 2, 1, 1.0f / 64.0f); + Store(device, texels, shape, StoreLayerMipCode, 3, 2, 1.0f / 128.0f); + const auto first = SampleProgram(device, texels, shape, 0, 0); + const auto second = SampleProgram(device, texels, shape, 2, 1); + const auto third = SampleProgram(device, texels, shape, 3, 2); + Sample(device, first, shape, 1.0f / 32.0f, name + ": layer 0 mip 0", 0, 0); + Sample(device, second, shape, 1.0f / 64.0f, name + ": layer 2 mip 1", 2, 1); + Sample(device, third, shape, 1.0f / 128.0f, name + ": layer 3 mip 2", 3, 2); + RequireCounts(1, 1, name + ": first comparison samples"); + const auto before = PendingVersion(texels, bytes, name); + Store(device, texels, shape, StoreLayerMipCode, 2, 1, 1.0f / 256.0f); + RequireNewerVersion(before, PendingVersion(texels, bytes, name), name + ": second store"); + Sample(device, second, shape, 1.0f / 256.0f, name + ": layer 2 mip 1 after a second store", 2, 1); + Sample(device, first, shape, 1.0f / 32.0f, name + ": layer 0 mip 0 after a second store", 0, 0); + RequireCounts(0, 1, name + ": comparison samples after a second store"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock buffers; + Input = std::span(reinterpret_cast(buffers.Data()), Threads * 3u); + Output = std::span(reinterpret_cast(buffers.Data() + 4096u), Threads); + SingleLevelCase(*device, {Format32Float, 512, 512, 1, 1, false}, "R32F 512x512"); + SingleLevelCase(*device, {Format16Unorm, 1024, 512, 1, 1, false}, "R16 unorm 1024x512"); + LayersAndMipsCase(*device); + std::puts("depth compare 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/DepthFastClear.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClear.cpp new file mode 100644 index 0000000000..181a10cebc --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClear.cpp @@ -0,0 +1,68 @@ +#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::Height; +using DepthFastClearHarness::HtileBytes; +using DepthFastClearHarness::Require; +using DepthFastClearHarness::Surface; +using DepthFastClearHarness::Width; + +alignas(4096) std::array Depth{}; +alignas(256) std::array Htile{}; +alignas(16) std::array FillValue{}; +constexpr std::array MemoryValueFillKernel{0xd7460000u, 0x04010c08u, 0xf4201a82u, 0xfa000000u, 0xbf8cc07fu, 0x7e02026au, 0xe0102000u, 0x80000100u, 0xbf810000u}; + +void FillFromMemory(std::uint64_t htile, std::uint32_t groups, std::uint32_t value) { + FillValue[0] = value; + const auto valueAddress = reinterpret_cast(FillValue.data()); + const std::array userData{static_cast(htile), static_cast((htile >> 32u) & 0xffffu) | (4u << 16u), static_cast(DepthFastClearHarness::HtileBytes / 4u), 0x00014204u, static_cast(valueAddress), static_cast(valueAddress >> 32u) | (16u << 16u), 1u, 0x0004dfacu}; + const auto fill = AgcDriver::Graphics::MatchMemoryValueFill(MemoryValueFillKernel, userData, {64u, 1u, 1u}, {groups, 1u, 1u}); + DepthFastClearHarness::Require(fill.has_value(), "the HTILE fill from a value in memory was not recognized"); + AgcDriver::Graphics::NoteMemoryValueFill(*fill); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Htile.fill(0xffffffffu); + const auto shaders = Compile(*device); + const Surface surface{reinterpret_cast(Depth.data()), 0, reinterpret_cast(Htile.data()), false, VK_FORMAT_D32_SFLOAT}; + const auto htile = surface.htile; + Require(Draw(*device, shaders, surface, {.depthWrite = true}) == 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"); + AgcDriver::Graphics::NoteDepthMetadataFill(htile, HtileBytes - 4u, 0u); + Require(Draw(*device, shaders, surface, {}) == 0u, "a fill shorter than the HTILE cleared the depth"); + AgcDriver::Graphics::NoteDepthMetadataFill(htile, HtileBytes, 0xffffffffu); + Require(Draw(*device, shaders, surface, {}) == 0u, "an expanded HTILE fill (ZMask 0xf) cleared the depth"); + AgcDriver::Graphics::NoteDepthMetadataFill(htile, HtileBytes, 0u); + Require(Draw(*device, shaders, surface, {.depthWrite = true}) == Covered, "a fill of ZMask 0 over the whole HTILE did not clear the depth to DB_DEPTH_CLEAR (1.0)"); + Require(Draw(*device, shaders, surface, {}) == 0u, "the depth written after a fast clear was lost"); + AgcDriver::Graphics::NoteDepthMetadataFill(htile, HtileBytes, 0u); + Require(Draw(*device, shaders, surface, {}) == Covered, "a second whole-HTILE fill of ZMask 0 did not clear the depth again"); + constexpr auto wholeHtileGroups = static_cast(HtileBytes / 4u / 64u); + Require(Draw(*device, shaders, surface, {.depthWrite = true}) == Covered, "the depth was not written before the memory value fills"); + FillFromMemory(htile, wholeHtileGroups, 0xffffffffu); + Require(Draw(*device, shaders, surface, {}) == 0u, "a memory value fill of ZMask 0xf cleared the depth"); + FillFromMemory(htile, wholeHtileGroups - 1u, 0u); + Require(Draw(*device, shaders, surface, {}) == 0u, "a memory value fill shorter than the HTILE cleared the depth"); + FillFromMemory(htile, wholeHtileGroups, 0u); + Require(Draw(*device, shaders, surface, {}) == Covered, "a memory value fill of ZMask 0 over the whole HTILE did not clear the depth to DB_DEPTH_CLEAR (1.0)"); + std::puts("depth fast clear tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp new file mode 100644 index 0000000000..e1d74d89e9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp @@ -0,0 +1,154 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_EXECUTION_DEPTHFASTCLEARHARNESS_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_EXECUTION_DEPTHFASTCLEARHARNESS_HPP + +#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 DepthFastClearHarness { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +inline constexpr std::uint32_t Width = 64; +inline constexpr std::uint32_t Height = 32; +inline constexpr std::uint32_t Covered = Width * Height; +inline constexpr std::size_t HtileBytes = ((Width + 1023u) / 1024u) * ((Height + 511u) / 512u) * 32768u; + +alignas(256) inline std::array Pixels{}; + +alignas(256) inline constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) inline constexpr std::array PixelCode{ + 0x7e0002f2, 0xf800180f, 0x00000000, 0xbf810000, +}; + +struct Surface { + std::uint64_t depth; + std::uint64_t stencil; + 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 { + bool depthTest = true; + bool depthWrite = false; + 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) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +inline const std::vector>& Triangles() { + static const std::vector> 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}, + }; + return vertices; +} + +struct Shaders { + ShaderRecompiler::RecompileResult vertex; + ShaderRecompiler::RecompileResult pixel; + std::uint32_t vertexPush = 0; +}; + +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(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; + shaders.vertex = ShaderRecompiler::Recompile(vertex); + shaders.vertexPush = static_cast(shaders.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(4, 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, {}}, + {32, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(shaders.vertexPush, target) + }; + fragment.useCache = false; + shaders.pixel = ShaderRecompiler::Recompile(fragment); + return shaders; +} + +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()), extent, VK_FORMAT_R8G8B8A8_UNORM, pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + 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 = extent; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + 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}, extent}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangles().size()), 0, 1, 0, false}; + 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; + } + return covered; +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearSubmit.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearSubmit.cpp new file mode 100644 index 0000000000..f535dcfc10 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearSubmit.cpp @@ -0,0 +1,315 @@ +#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" +#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 + +namespace { + +using DepthFastClearHarness::Compile; +using DepthFastClearHarness::Covered; +using DepthFastClearHarness::Draw; +using DepthFastClearHarness::HtileBytes; +using DepthFastClearHarness::Require; +using DepthFastClearHarness::Shaders; +using DepthFastClearHarness::Surface; + +constexpr auto HtileFillBytes = static_cast(HtileBytes); + +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 fast clear submit: 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() { return block; } + + std::uint64_t Address() const { return static_cast(reinterpret_cast(block)); } + +private: + std::uint8_t* block = nullptr; +}; + +GuestBlock& NewBlock() { + static auto* blocks = new std::deque(); + return blocks->emplace_back(); +} + +std::uint32_t Low(std::uint64_t value) { return static_cast(value); } + +std::uint32_t High(std::uint64_t value) { return static_cast(value >> 32u); } + +std::vector Pm4Packet(std::uint32_t opcode, std::initializer_list body) { + std::vector words{0xc0000000u | (static_cast(body.size() - 1) << 16u) | (opcode << 8u)}; + words.insert(words.end(), body); + return words; +} + +void Submit(std::initializer_list> packets) { + std::vector words; + for (const auto& packet : packets) words.insert(words.end(), packet.begin(), packet.end()); + Packet dcb{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&dcb) == 0, "depth fast clear submit: DCB submission failed"); + AgcDriverWaitIdle_nid_postfix(); +} + +void DmaFill(const GuestBlock& htile, std::uint32_t bytes, std::uint32_t pattern) { + Submit({Pm4Packet(0x50, {0x40000000u, pattern, 0, Low(htile.Address()), High(htile.Address()), bytes})}); +} + +constexpr std::array FillKernel{0xd7460004u, 0x04010c08u, 0x7e000204u, 0x7e020205u, 0x7e040206u, 0x7e060207u, 0xe01c2000u, 0x80000004u, 0xbf810000u}; + +constexpr std::array BufferPatternFillKernel{0xd7460004u, 0x04010c08u, 0xf4280302u, 0xfa000000u, 0xbf8cc07fu, 0x7e00020cu, 0x7e02020du, 0x7e04020eu, 0x7e06020fu, 0xe01c2000u, 0x80000004u, 0xbf810000u}; + +void DispatchFill(const GuestBlock& code, const GuestBlock& htile, std::uint32_t bytes, const GuestBlock* patternBuffer = nullptr) { + const auto kernel = code.Address(); + const auto base = htile.Address(); + const auto source = patternBuffer != nullptr ? patternBuffer->Address() : 0; + Submit({ + Pm4Packet(0x76, {0x20c, Low(kernel >> 8u)}), + Pm4Packet(0x76, {0x20d, High(kernel >> 8u) & 0xffu}), + Pm4Packet(0x76, {0x207, 64}), + Pm4Packet(0x76, {0x208, 1}), + Pm4Packet(0x76, {0x209, 1}), + Pm4Packet(0x76, {0x213, (8u << 1u) | 0x80u}), + Pm4Packet(0x76, {0x240, Low(base)}), + Pm4Packet(0x76, {0x241, (High(base) & 0xffffu) | (16u << 16u)}), + Pm4Packet(0x76, {0x242, bytes / 16u}), + Pm4Packet(0x76, {0x243, 0x4bfacu}), + Pm4Packet(0x76, {0x244, Low(source)}), + Pm4Packet(0x76, {0x245, source != 0 ? (High(source) & 0xffffu) | (16u << 16u) : 0}), + Pm4Packet(0x76, {0x246, source != 0 ? 1u : 0}), + Pm4Packet(0x76, {0x247, source != 0 ? 0x4dfacu : 0}), + Pm4Packet(0x15, {bytes / 1024u, 1, 1, 0x41}), + }); +} + +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{}; + const bool stored = device.FillBuffer(probe.Address(), 4096, pattern); + device.WaitIdle(); + return stored; +} + +void DmaFillClearsDepth(AgcDriver::VulkanDevice& device, const Shaders& shaders) { + const auto& htile = NewBlock(); + const Surface surface{NewBlock().Address(), 0, htile.Address(), false, VK_FORMAT_D32_SFLOAT}; + Require(Draw(device, shaders, surface, {.depthWrite = true}) == Covered, "a new depth surface was not cleared to DB_DEPTH_CLEAR (1.0)"); + Require(Draw(device, shaders, surface, {}) == 0u, "a draw at depth 0.5 passed LESS against the 0.5 written before it"); + DmaFill(htile, HtileFillBytes - 4u, 0u); + Require(Draw(device, shaders, surface, {}) == 0u, "a DMA_DATA fill shorter than the HTILE cleared the depth"); + DmaFill(htile, HtileFillBytes, 0xffffffffu); + Require(Draw(device, shaders, surface, {}) == 0u, "a DMA_DATA fill expanded to ZMask 0xf cleared the depth"); + DmaFill(htile, HtileFillBytes, 0u); + Require(Draw(device, shaders, surface, {.depthWrite = true}) == Covered, "a DMA_DATA fill of 0 over the HTILE did not clear the depth to DB_DEPTH_CLEAR (1.0)"); + Require(Draw(device, shaders, surface, {}) == 0u, "the depth written after the DMA_DATA clear was lost"); +} + +void DispatchFillClearsDepth(AgcDriver::VulkanDevice& device, const Shaders& shaders) { + const auto& htile = NewBlock(); + auto& code = NewBlock(); + std::memcpy(code.Data(), FillKernel.data(), sizeof(FillKernel)); + 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)"); + Require(Draw(device, shaders, surface, {}) == 0u, "a draw at depth 0.5 passed LESS against the 0.5 written before it"); + DispatchFill(code, htile, HtileFillBytes); + Require(Draw(device, shaders, surface, {.depthWrite = true}) == Covered, "a compute fill of 0 over the HTILE did not clear the depth to DB_DEPTH_CLEAR (1.0)"); + Require(Draw(device, shaders, surface, {}) == 0u, "the depth written after the compute fill clear was lost"); +} + +void DispatchBufferPatternFillClearsDepth(AgcDriver::VulkanDevice& device, const Shaders& shaders) { + const auto& htile = NewBlock(); + auto& code = NewBlock(); + std::memcpy(code.Data(), BufferPatternFillKernel.data(), sizeof(BufferPatternFillKernel)); + auto& pattern = NewBlock(); + const Surface surface{NewBlock().Address(), 0, htile.Address(), false, VK_FORMAT_D32_SFLOAT}; + Require(Draw(device, shaders, surface, {.depthWrite = true}) == Covered, "a new depth surface was not cleared to DB_DEPTH_CLEAR (1.0)"); + std::memset(pattern.Data(), 0xff, 16); + DispatchFill(code, htile, HtileFillBytes, &pattern); + Require(Draw(device, shaders, surface, {}) == 0u, "a compute fill with the buffer pattern 0xffffffff (ZMask 0xf) cleared the depth"); + std::memset(pattern.Data(), 0, 16); + DispatchFill(code, htile, HtileFillBytes, &pattern); + Require(Draw(device, shaders, surface, {.depthWrite = true}) == Covered, "a compute fill with the buffer pattern 0 over the HTILE did not clear the depth to DB_DEPTH_CLEAR (1.0)"); +} + +void 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}; + 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}; + const VkStencilOpState equalZero{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_EQUAL, 0xffu, 0u, 0u}; + Require(Draw(device, shaders, surface, {.depthWrite = true, .stencilTest = true, .stencil = writeFive}) == Covered, "the stencil write over a new depth-stencil surface did not cover the target"); + DmaFill(htile, HtileFillBytes, 0x3f0u); + Require(Draw(device, shaders, surface, {.depthTest = false, .stencilTest = true, .stencil = equalFive}) == Covered, "a depth-only fill (SMem 3) cleared the stencil"); + Require(Draw(device, shaders, surface, {}) == Covered, "a depth-only fill did not clear the depth to DB_DEPTH_CLEAR (1.0)"); + DmaFill(htile, HtileFillBytes, 0xf0u); + Require(Draw(device, shaders, surface, {.depthTest = false, .stencilTest = true, .stencil = equalZero}) == Covered, "a depth and stencil fast clear (SMem 0) did not clear the stencil to 0"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + 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(); + std::puts(computeRan ? "depth fast clear submit tests passed" : "depth fast clear submit tests passed without the compute fill"); + 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/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 new file mode 100644 index 0000000000..cdb94c5363 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp @@ -0,0 +1,420 @@ +#include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace AgcDriver::Graphics; + +constexpr std::uint32_t Side = 256; +constexpr std::uint32_t Format32Float = 22; +constexpr std::size_t LayerBytes = std::size_t{Side} * Side * 4u; +constexpr std::size_t BlockBytes = 2u * LayerBytes; +constexpr int Skipped = 77; + +PFN_vkGetDeviceProcAddr realDeviceProc = nullptr; +PFN_vkCmdCopyImageToBuffer realCopyImageToBuffer = nullptr; +PFN_vkCmdCopyBufferToImage realCopyBufferToImage = nullptr; +std::atomic imageToBufferCopies{0}; +std::atomic bufferToImageCopies{0}; + +VKAPI_ATTR void VKAPI_CALL CountImageToBuffer(VkCommandBuffer commands, VkImage image, VkImageLayout layout, VkBuffer buffer, std::uint32_t count, const VkBufferImageCopy* regions) { + imageToBufferCopies.fetch_add(1); + realCopyImageToBuffer(commands, image, layout, buffer, count, regions); +} + +VKAPI_ATTR void VKAPI_CALL CountBufferToImage(VkCommandBuffer commands, VkBuffer buffer, VkImage image, VkImageLayout layout, std::uint32_t count, const VkBufferImageCopy* regions) { + bufferToImageCopies.fetch_add(1); + realCopyBufferToImage(commands, buffer, image, layout, count, regions); +} + +VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL CountingDeviceProc(VkDevice device, const char* name) { + const auto function = realDeviceProc(device, name); + if (function == nullptr) return nullptr; + if (std::strcmp(name, "vkCmdCopyImageToBuffer") == 0) { + realCopyImageToBuffer = reinterpret_cast(function); + return reinterpret_cast(&CountImageToBuffer); + } + if (std::strcmp(name, "vkCmdCopyBufferToImage") == 0) { + realCopyBufferToImage = reinterpret_cast(function); + return reinterpret_cast(&CountBufferToImage); + } + return function; +} + +class Device { +public: + Device() { +#ifdef _WIN32 + library = SDL_LoadObject("vulkan-1.dll"); +#else + library = SDL_LoadObject("libvulkan.so.1"); +#endif + Require(library != nullptr, "cannot load Vulkan"); + try { + instanceProc = reinterpret_cast(SDL_LoadFunction(library, "vkGetInstanceProcAddr")); + Require(instanceProc != nullptr, "missing Vulkan instance resolver"); + VkApplicationInfo application{VK_STRUCTURE_TYPE_APPLICATION_INFO}; + application.apiVersion = VK_API_VERSION_1_1; + VkInstanceCreateInfo info{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; + info.pApplicationInfo = &application; + Check(function("vkCreateInstance")(&info, nullptr, &instance), "vkCreateInstance"); + std::uint32_t count = 0; + const auto enumerate = function("vkEnumeratePhysicalDevices"); + Check(enumerate(instance, &count, nullptr), "vkEnumeratePhysicalDevices"); + Require(count != 0, "no Vulkan device"); + std::vector devices(count); + Check(enumerate(instance, &count, devices.data()), "vkEnumeratePhysicalDevices"); + context.physical = devices.front(); + const auto extensions = function("vkEnumerateDeviceExtensionProperties"); + Check(extensions(context.physical, nullptr, &count, nullptr), "vkEnumerateDeviceExtensionProperties"); + std::vector available(count); + Check(extensions(context.physical, nullptr, &count, available.data()), "vkEnumerateDeviceExtensionProperties"); + auto bytes = AgcDriver::QueryBdaByteFeatures(context.physical, function("vkGetPhysicalDeviceFeatures2"), available); + auto address = AgcDriver::QueryBdaFeatures(context.physical, function("vkGetPhysicalDeviceFeatures2"), available); + const auto queues = function("vkGetPhysicalDeviceQueueFamilyProperties"); + queues(context.physical, &count, nullptr); + std::vector families(count); + queues(context.physical, &count, families.data()); + constexpr VkQueueFlags needed = VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_COMPUTE_BIT; + std::uint32_t family = 0; + while (family < count && (families[family].queueFlags & needed) != needed) ++family; + Require(family < count, "no Vulkan graphics and compute queue"); + const float priority = 1; + VkDeviceQueueCreateInfo queue{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; + queue.queueFamilyIndex = family; + queue.queueCount = 1; + queue.pQueuePriorities = &priority; + VkPhysicalDeviceFeatures enabled{}; + enabled.shaderInt64 = VK_TRUE; + address.pNext = &bytes; + const std::array extensionsEnabled{VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME, VK_KHR_8BIT_STORAGE_EXTENSION_NAME}; + VkDeviceCreateInfo device{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, &address}; + device.queueCreateInfoCount = 1; + device.pQueueCreateInfos = &queue; + device.enabledExtensionCount = static_cast(extensionsEnabled.size()); + device.ppEnabledExtensionNames = extensionsEnabled.data(); + device.pEnabledFeatures = &enabled; + Check(function("vkCreateDevice")(context.physical, &device, nullptr, &context.device), "vkCreateDevice"); + realDeviceProc = function("vkGetDeviceProcAddr"); + context.deviceProc = &CountingDeviceProc; + function("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory); + VkPhysicalDeviceProperties properties{}; + function("vkGetPhysicalDeviceProperties")(context.physical, &properties); + context.limits = properties.limits; + context.bufferDeviceAddress = true; + context.formatProperties = function("vkGetPhysicalDeviceFormatProperties"); + context.Function("vkGetDeviceQueue")(context.device, family, 0, &context.queue); + VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + pool.queueFamilyIndex = family; + Check(context.Function("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool"); + } catch (...) { + release(); + throw; + } + } + + ~Device() { release(); } + Device(const Device&) = delete; + Device& operator=(const Device&) = delete; + const Context& GetContext() const { return context; } + +private: + template + TFunction function(const char* name) const { + const auto result = reinterpret_cast(instanceProc(instance, name)); + Require(result != nullptr, name); + return result; + } + + void release() noexcept { + if (context.device != VK_NULL_HANDLE) ClearDepthSurfaces(context.device); + if (context.pool != VK_NULL_HANDLE) context.Function("vkDestroyCommandPool")(context.device, context.pool, nullptr); + context.bufferPool.reset(); + if (context.device != VK_NULL_HANDLE) function("vkDestroyDevice")(context.device, nullptr); + if (instance != VK_NULL_HANDLE) function("vkDestroyInstance")(instance, nullptr); + if (library != nullptr) SDL_UnloadObject(library); + } + + void* library = nullptr; + PFN_vkGetInstanceProcAddr instanceProc = nullptr; + VkInstance instance = VK_NULL_HANDLE; + Context context{}; +}; + +class WatchedBlock { +public: + WatchedBlock() { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, PAGE_READWRITE, BlockBytes); +#else + constexpr std::uintptr_t alignment = 65536; + void* mapped = mmap(nullptr, BlockBytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + alignment - 1) & ~(alignment - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + BlockBytes != begin + BlockBytes + alignment) munmap(reinterpret_cast(aligned + BlockBytes), begin + alignment - aligned); + block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, BlockBytes); + } +#endif + Require(block != nullptr, "depth plane guest layers: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~WatchedBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, BlockBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, BlockBytes); +#else + munmap(block, BlockBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, BlockBytes); +#endif + } + + WatchedBlock(const WatchedBlock&) = delete; + WatchedBlock& operator=(const WatchedBlock&) = delete; + + std::uint8_t* Data() const { return block; } + std::uint64_t Address() const { return static_cast(reinterpret_cast(block)); } + +private: + std::uint8_t* block = nullptr; +}; + +std::atomic watchedBegin{0}; +std::atomic watchedEnd{0}; +std::atomic watchedReads{0}; + +void CountReads(std::uint64_t address, std::size_t bytes) { + if (address < watchedEnd.load() && address + bytes > watchedBegin.load()) watchedReads.fetch_add(1); +} + +void FillLayer(std::uint8_t* layer, float depth) { + for (std::size_t offset = 0; offset < LayerBytes; offset += sizeof(depth)) std::memcpy(layer + offset, &depth, sizeof(depth)); +} + +GuestTextureResource ArrayOver(std::uint64_t address) { + GuestTextureResource resource{}; + resource.baseAddress = address; + resource.width = Side; + resource.height = Side; + resource.depthOrLastArray = 1; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kZ64KBX; + resource.dimension = TextureDimension::k2DArray; + resource.format = Format32Float; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + return resource; +} + +constexpr std::array words{}; +constexpr VkComponentMapping components{VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY}; + +void Run(const Context& context, const WatchedBlock& block) { + const auto address = block.Address(); + FillLayer(block.Data(), 1.0f); + FillLayer(block.Data() + LayerBytes, 0.25f); + Require(DepthSliceBytes({Side, Side}, 4u) == LayerBytes, "the depth slice of the test surface is not one layer of the guest block"); + DepthSurfaceView(context, DepthTarget{address, 0, {Side, Side}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0}); + const auto resource = ArrayOver(address); + watchedBegin = address + LayerBytes; + watchedEnd = address + BlockBytes; + AgcDriver::GuestMemory::SetFlushHook(&CountReads); + const auto sample = [&](const char* when) { + watchedReads = 0; + const auto texture = DepthSurfaceTexture(context, words, resource, components); + Require(texture != nullptr, std::string("the 2D array over the depth surface was not sampled through its depth planes ") + when); + return watchedReads.load(); + }; + try { + Require(sample("at its first use") != 0, "the first sampling of the depth planes did not read the layer no depth surface holds"); + Require(sample("again") == 0, "sampling the depth planes again read the guest layer no depth surface holds although nothing wrote it"); + FillLayer(block.Data() + LayerBytes, 0.75f); + Require(sample("after a CPU write") != 0, "sampling the depth planes after a CPU write to the guest layer did not read it again"); + Require(sample("after the re-read") == 0, "sampling the depth planes after the re-read of the written layer read it once more"); + AgcDriver::GuestMemory::MarkWritten(address + LayerBytes, 4096); + Require(sample("after a driver store") != 0, "sampling the depth planes after a driver store over the guest layer did not read it again"); + const DepthTarget slice{address, 0, {Side, Side}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0}; + const DepthTarget other{address + 2 * BlockBytes, 0, {Side, Side}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0}; + const auto copies = [&](const char* when, std::uint32_t fromSlices, std::uint32_t intoCopy) { + imageToBufferCopies = 0; + bufferToImageCopies = 0; + sample(when); + if (imageToBufferCopies.load() != fromSlices || bufferToImageCopies.load() != intoCopy) { + throw std::runtime_error(std::string("sampling the depth planes ") + when + " recorded " + std::to_string(imageToBufferCopies.load()) + " depth surface copies and " + std::to_string(bufferToImageCopies.load()) + " plane copy fills, expected " + std::to_string(fromSlices) + " and " + std::to_string(intoCopy)); + } + }; + DepthSurfaceView(context, other); + copies("after a draw to another depth surface", 1, 1); + copies("with nothing written since", 0, 0); + DepthSurfaceView(context, slice); + copies("while a draw to its depth surface is being recorded", 1, 1); + copies("again while that draw is the last depth draw", 1, 1); + DepthSurfaceView(context, other); + copies("after that draw", 1, 1); + copies("with nothing written since that draw", 0, 0); + FillLayer(block.Data() + LayerBytes, 0.5f); + copies("after a CPU write to the guest layer only", 0, 1); + copies("with nothing written since the CPU write", 0, 0); + } catch (...) { + AgcDriver::GuestMemory::SetFlushHook(nullptr); + throw; + } + AgcDriver::GuestMemory::SetFlushHook(nullptr); +} + +void RecordedUploads(const Context& context, const WatchedBlock& block) { + const auto resource = ArrayOver(block.Address()); + Recorder recorder(context); + recorder.Activate(); + for (int round = 1; round <= 3; ++round) { + FillLayer(block.Data() + LayerBytes, 0.125f * static_cast(round)); + const auto before = recorder.OpenKeptBytes(); + Require(DepthSurfaceTexture(context, words, resource, components) != nullptr, "the 2D array over the depth surface was not sampled through its depth planes while recording"); + const auto kept = recorder.OpenKeptBytes() - before; + Require(kept >= LayerBytes, "the recorded upload of a written guest layer added " + std::to_string(kept) + " bytes to the open batch's kept bytes, less than the " + std::to_string(LayerBytes) + " bytes of the layer it holds until the batch completes"); + } + recorder.Sync(); +} + +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() { + try { + if (!AgcDriver::GuestMemory::WriteWatched()) { + std::puts("skipped, guest memory has no write watch"); + return Skipped; + } + std::optional device; + try { + device.emplace(); + } catch (const std::exception& error) { + if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw; + std::printf("skipped, no usable Vulkan device: %s\n", error.what()); + return Skipped; + } + { + TextureDetiler detiler(device->GetContext()); + auto context = device->GetContext(); + context.detiler = &detiler; + WatchedBlock block; + Require(AgcDriver::GuestMemory::Watched(block.Address(), BlockBytes), "depth plane guest layers: the guest block is not write-watched"); + Run(context, block); + RecordedUploads(context, block); + RunCubeGather(context, block); + RunCubeTiledSlice(context, block); + } + device.reset(); + std::puts("depth plane guest layer tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp new file mode 100644 index 0000000000..33c6cfdb6b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp @@ -0,0 +1,461 @@ +#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 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#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 DepthFastClearHarness::BufferDescriptor; +using DepthFastClearHarness::PixelCode; +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; +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 = 8u << 20u; +constexpr std::size_t OutputOffset = 0; +constexpr std::size_t ArrayOffset = 0x10000; +constexpr std::size_t CubeOffset = 0x40000; +constexpr std::size_t IntegerOffset = 0xa0000; +constexpr std::size_t DepthBitsOffset = 0xb0000; +constexpr std::size_t OneSliceOffset = 0xd0000; +constexpr std::size_t FirstSliceOffset = 0xe0000; +constexpr 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; + +constexpr std::array LoadDims{1u, 3u, 5u}; + +std::array LoadCode(std::uint32_t dim) { + return { + 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, + 0xbf8c3f70, // s_waitcnt vmcnt(0) + 0x34080082, // v_lshlrev_b32 v4, 2, v0 + 0xe0701000, // buffer_store_dword v3, v4, s[0:3], 0 offen + 0x80000304, + 0xbf810000, // s_endpgm + 0xbf810000, + 0xbf810000, + }; +} + +alignas(256) std::array Load2D = LoadCode(LoadDims[0]); +alignas(256) std::array LoadCube = LoadCode(LoadDims[1]); +alignas(256) std::array LoadArray = LoadCode(LoadDims[2]); + +alignas(256) std::array Pixels{}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, PAGE_READWRITE, BlockBytes); +#else + constexpr std::uintptr_t alignment = 65536; + void* mapped = mmap(nullptr, BlockBytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + alignment - 1) & ~(alignment - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + BlockBytes != begin + BlockBytes + alignment) munmap(reinterpret_cast(aligned + BlockBytes), begin + alignment - aligned); + block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, BlockBytes); + } +#endif + Require(block != nullptr, "depth plane sampling: cannot allocate the guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + AgcDriver::GuestMemory::CollectWritesUncached(reinterpret_cast(block), BlockBytes); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, BlockBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, BlockBytes); +#else + munmap(block, BlockBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, BlockBytes); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* At(std::size_t offset) { return block + offset; } + +private: + std::uint8_t* block = nullptr; +}; + +struct Shape { + std::uint32_t format; + std::uint32_t width; + std::uint32_t height; + std::uint32_t type; + std::uint32_t lastArray; + std::uint32_t swizzle; + std::uint32_t baseArray = 0; +}; + +std::array TextureDescriptor(const void* data, const Shape& shape) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (shape.format << 20u) | (((shape.width - 1u) & 3u) << 30u), + ((shape.width - 1u) >> 2u) | ((shape.height - 1u) << 14u), + 0xfacu | (shape.swizzle << 20u) | (shape.type << 28u), + shape.lastArray | (shape.baseArray << 16u), + 0u, 0u, 0u, + }; +} + +std::uint32_t Bits(float value) { + std::uint32_t bits = 0; + std::memcpy(&bits, &value, sizeof(bits)); + return bits; +} + +// Draws a full-target triangle at depth `z` into the depth surface at `address`, which makes the driver create the +// surface (the base slice a later plane copy reads from the surface instead of from guest memory). +void DrawDepth(AgcDriver::VulkanDevice& device, std::uint64_t address, VkExtent2D extent, VkFormat format, float z) { + static std::deque>> vertexData; + vertexData.push_back({{-1.0f, -1.0f, z, 1.0f}, {3.0f, -1.0f, z, 1.0f}, {-1.0f, 3.0f, z, 1.0f}}); + const auto& vertices = vertexData.back(); + const auto target = device.Target(); + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(vertices.data(), 16u, static_cast(vertices.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertexRequest{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {32, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertexRequest.useCache = false; + const auto vertex = ShaderRecompiler::Recompile(vertexRequest); + const auto vertexPush = static_cast(vertex.pushConstants.size()); + ShaderRecompiler::ShaderPixelStageInfo pixelInfo{}; + pixelInfo.wave32 = true; + pixelInfo.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixelInfo.posX = true; + pixelInfo.posY = true; + pixelInfo.targetOutputMode[0] = 9; + pixelInfo.targetExportMapping.fill(0xe4u); + std::vector pixelUserData(4, 0u); + const std::array pixelMemory{{{reinterpret_cast(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}}; + ShaderRecompiler::RecompileRequest fragmentRequest{ + {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, + {32, 0, pixelUserData, std::nullopt, pixelInfo, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragmentRequest.useCache = false; + const auto pixel = ShaderRecompiler::Recompile(fragmentRequest); + const std::array shaders{{ + {ShaderStage::Vertex, &vertex, 0}, + {ShaderStage::Fragment, &pixel, PixelPushOffset(vertexPush, target)} + }}; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), extent, VK_FORMAT_R8G8B8A8_UNORM, 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}; + state.depthTest = true; + state.depthWrite = true; + state.depthCompare = VK_COMPARE_OP_ALWAYS; + state.renderExtent = extent; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(extent.height), static_cast(extent.width), -static_cast(extent.height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, extent}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(vertices.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +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}; + const auto texture = TextureDescriptor(texels, shape); + std::copy(out.begin(), out.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + userData[12] = slice; + 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{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + std::fill(output.begin(), output.end(), 0xdeadbeefu); + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Expect(std::span output, std::uint32_t expected, const std::string& what) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + char text[160]; + std::snprintf(text, sizeof(text), "%s: texel %u read 0x%08x, expected 0x%08x", what.c_str(), tid, output[tid], expected); + Require(output[tid] == expected, text); + } +} + +void FillSlice(std::uint8_t* slice, std::uint64_t bytes, std::uint32_t value, std::uint32_t elementBytes) { + for (std::uint64_t offset = 0; offset < bytes; offset += elementBytes) std::memcpy(slice + offset, &value, elementBytes); +} + +// A 3-slice D32 array: slice 0 from the live depth surface, slices 1 and 2 from guest memory (detiled). +void ArrayCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span output) { + auto* base = block.At(ArrayOffset); + DrawDepth(device, reinterpret_cast(base), {128, 128}, VK_FORMAT_D32_SFLOAT, DrawnDepth); + FillSlice(base + SliceBytes, SliceBytes, Bits(0.5f), 4u); + FillSlice(base + 2u * SliceBytes, SliceBytes, Bits(0.75f), 4u); + const Shape shape{Format32Float, 128, 128, Type2DArray, 2, Swizzle64KbZX}; + const std::array expected{DrawnDepth, 0.5f, 0.75f}; + for (std::uint32_t slice = 0; slice < 3u; ++slice) { + Load(device, base, shape, slice, output); + Expect(output, Bits(expected[slice]), "D32 2D array slice " + std::to_string(slice)); + } +} + +// A cube over six square D32 slices: face 0 from the live surface, faces 1-5 from guest memory. +void CubeCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span output) { + auto* base = block.At(CubeOffset); + DrawDepth(device, reinterpret_cast(base), {128, 128}, VK_FORMAT_D32_SFLOAT, DrawnDepth); + for (std::uint32_t face = 1; face < 6u; ++face) FillSlice(base + face * SliceBytes, SliceBytes, Bits(static_cast(face) / 8.0f), 4u); + const Shape shape{Format32Float, 128, 128, TypeCube, 0, Swizzle64KbZX}; + for (std::uint32_t face = 0; face < 6u; ++face) { + Load(device, base, shape, face, output); + Expect(output, Bits(face == 0 ? DrawnDepth : static_cast(face) / 8.0f), "D32 cube face " + std::to_string(face)); + } +} + +// A D16 plane read through an R16_UINT view (a non-depth swizzle, so not flagged as depth bits): the raw depth value. +void IntegerCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span output) { + auto* base = block.At(IntegerOffset); + DrawDepth(device, reinterpret_cast(base), {256, 128}, VK_FORMAT_D16_UNORM, DrawnD16Depth); + Load(device, base, {Format16Uint, 256, 128, Type2D, 0, Swizzle64KbS}, 0, output); + Expect(output, DrawnD16Bits, "D16 plane as R16_UINT"); +} + +// A D16 2D array whose R16_UINT T# has a depth swizzle (flagged as depth bits): both slices' raw values. +void DepthBitsArrayCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span output) { + auto* base = block.At(DepthBitsOffset); + DrawDepth(device, reinterpret_cast(base), {256, 128}, VK_FORMAT_D16_UNORM, DrawnD16Depth); + FillSlice(base + SliceBytes, SliceBytes, 0x1234u, 2u); + const Shape shape{Format16Uint, 256, 128, Type2DArray, 1, Swizzle64KbZX}; + Load(device, base, shape, 0, output); + Expect(output, DrawnD16Bits, "D16 depth-bits array slice 0"); + Load(device, base, shape, 1, output); + Expect(output, 0x1234u, "D16 depth-bits array slice 1"); +} + +// A 2D shader image over a 2D-array T# reads the array's first slice: a one-slice array, which samples the depth +// image itself, and a 3-slice array, which samples the plane copy. +void 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); + Load(device, one, {Format32Float, 128, 128, Type2DArray, 0, Swizzle64KbZX}, 0, output, true); + Expect(output, Bits(DrawnDepth), "one-slice D32 2D array through a 2D shader image"); + auto* base = block.At(FirstSliceOffset); + DrawDepth(device, reinterpret_cast(base), {128, 128}, VK_FORMAT_D32_SFLOAT, DrawnDepth); + FillSlice(base + SliceBytes, SliceBytes, Bits(0.5f), 4u); + FillSlice(base + 2u * SliceBytes, SliceBytes, Bits(0.75f), 4u); + Load(device, base, {Format32Float, 128, 128, Type2DArray, 2, Swizzle64KbZX}, 0, output, true); + Expect(output, Bits(DrawnDepth), "3-slice D32 2D array through a 2D shader image"); +} + +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)); + } +} + +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() { + try { + GuestBlock block; + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const std::span output(reinterpret_cast(block.At(OutputOffset)), Threads); + ArrayCase(*device, block, output); + CubeCase(*device, block, output); + IntegerCase(*device, block, output); + DepthBitsArrayCase(*device, block, output); + DepthBitsBaseSliceCase(*device, block, output); + GuestDepthBitsCase(*device, block, output); + FirstSliceCase(*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) { + std::cerr << error.what() << '\n'; + return 1; + } +} 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 new file mode 100644 index 0000000000..3ab804d307 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp @@ -0,0 +1,335 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace AgcDriver::Graphics; + +constexpr std::uint32_t Side = 64; +constexpr std::size_t BlockBytes = 65536; +alignas(4096) std::array Storage{}; +constexpr std::uint32_t Format32Float = 22; +constexpr std::uint32_t Format8Uint = 5; + +class Device { +public: + Device() { +#ifdef _WIN32 + library = SDL_LoadObject("vulkan-1.dll"); +#else + library = SDL_LoadObject("libvulkan.so.1"); +#endif + Require(library != nullptr, "cannot load Vulkan"); + try { + instanceProc = reinterpret_cast(SDL_LoadFunction(library, "vkGetInstanceProcAddr")); + Require(instanceProc != nullptr, "missing Vulkan instance resolver"); + VkApplicationInfo application{VK_STRUCTURE_TYPE_APPLICATION_INFO}; + application.apiVersion = VK_API_VERSION_1_1; + VkInstanceCreateInfo info{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; + info.pApplicationInfo = &application; + Check(function("vkCreateInstance")(&info, nullptr, &instance), "vkCreateInstance"); + std::uint32_t count = 0; + const auto enumerate = function("vkEnumeratePhysicalDevices"); + Check(enumerate(instance, &count, nullptr), "vkEnumeratePhysicalDevices"); + Require(count != 0, "no Vulkan device"); + std::vector devices(count); + Check(enumerate(instance, &count, devices.data()), "vkEnumeratePhysicalDevices"); + context.physical = devices.front(); + const auto extensions = function("vkEnumerateDeviceExtensionProperties"); + Check(extensions(context.physical, nullptr, &count, nullptr), "vkEnumerateDeviceExtensionProperties"); + std::vector available(count); + Check(extensions(context.physical, nullptr, &count, available.data()), "vkEnumerateDeviceExtensionProperties"); + const auto hasExtension = [&](const char* name) { + for (const auto& extension : available) { + if (std::strcmp(extension.extensionName, name) == 0) return true; + } + return false; + }; + auto bytes = AgcDriver::QueryBdaByteFeatures(context.physical, function("vkGetPhysicalDeviceFeatures2"), available); + auto address = AgcDriver::QueryBdaFeatures(context.physical, function("vkGetPhysicalDeviceFeatures2"), available); + const auto queues = function("vkGetPhysicalDeviceQueueFamilyProperties"); + queues(context.physical, &count, nullptr); + std::vector families(count); + queues(context.physical, &count, families.data()); + std::uint32_t family = 0; + while (family < count && (families[family].queueFlags & VK_QUEUE_COMPUTE_BIT) == 0) ++family; + Require(family < count, "no Vulkan compute queue"); + const float priority = 1; + VkDeviceQueueCreateInfo queue{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; + queue.queueFamilyIndex = family; + queue.queueCount = 1; + queue.pQueuePriorities = &priority; + VkPhysicalDeviceFeatures enabled{}; + enabled.shaderInt64 = VK_TRUE; + address.pNext = &bytes; + std::vector extensionsEnabled{VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME, VK_KHR_8BIT_STORAGE_EXTENSION_NAME}; + if (hasExtension(VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME)) { + extensionsEnabled.push_back(VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME); + VkPhysicalDeviceExternalMemoryHostPropertiesEXT hostProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_MEMORY_HOST_PROPERTIES_EXT}; + VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &hostProperties}; + function("vkGetPhysicalDeviceProperties2")(context.physical, &properties); + context.hostImportAlignment = hostProperties.minImportedHostPointerAlignment; + } + VkDeviceCreateInfo device{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, &address}; + device.queueCreateInfoCount = 1; + device.pQueueCreateInfos = &queue; + device.enabledExtensionCount = static_cast(extensionsEnabled.size()); + device.ppEnabledExtensionNames = extensionsEnabled.data(); + device.pEnabledFeatures = &enabled; + Check(function("vkCreateDevice")(context.physical, &device, nullptr, &context.device), "vkCreateDevice"); + context.deviceProc = function("vkGetDeviceProcAddr"); + function("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory); + VkPhysicalDeviceProperties properties{}; + function("vkGetPhysicalDeviceProperties")(context.physical, &properties); + context.limits = properties.limits; + context.bufferDeviceAddress = true; + context.formatProperties = function("vkGetPhysicalDeviceFormatProperties"); + context.imageFormatProperties = function("vkGetPhysicalDeviceImageFormatProperties"); + if (hasExtension(VK_EXT_MEMORY_BUDGET_EXTENSION_NAME)) context.memoryProperties2 = function("vkGetPhysicalDeviceMemoryProperties2"); + context.Function("vkGetDeviceQueue")(context.device, family, 0, &context.queue); + VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + pool.queueFamilyIndex = family; + Check(context.Function("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool"); + } catch (...) { + release(); + throw; + } + } + + ~Device() { release(); } + const Context& GetContext() const { return context; } + +private: + template + TFunction function(const char* name) const { + const auto result = reinterpret_cast(instanceProc(instance, name)); + Require(result != nullptr, name); + return result; + } + + void release() noexcept { + if (context.pool != VK_NULL_HANDLE) context.Function("vkDestroyCommandPool")(context.device, context.pool, nullptr); + context.bufferPool.reset(); + if (context.device != VK_NULL_HANDLE) function("vkDestroyDevice")(context.device, nullptr); + if (instance != VK_NULL_HANDLE) function("vkDestroyInstance")(instance, nullptr); + if (library != nullptr) SDL_UnloadObject(library); + } + + void* library = nullptr; + PFN_vkGetInstanceProcAddr instanceProc = nullptr; + VkInstance instance = VK_NULL_HANDLE; + Context context{}; +}; + + +template +void ExpectFailure(TAction action, const char* expected, const char* message) { + try { + action(); + } catch (const std::exception& error) { + Require(std::string(error.what()).find(expected) != std::string::npos, error.what()); + return; + } + throw std::runtime_error(message); +} + +GuestTextureResource PlaneResource(std::uint64_t address, std::uint32_t format, std::uint32_t side = Side) { + GuestTextureResource resource{}; + resource.baseAddress = address; + resource.width = side; + resource.height = side; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kLinear; + resource.dimension = TextureDimension::k2D; + resource.format = format; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + return resource; +} + +std::size_t CountFloats(const std::uint8_t* bytes, float value) { + std::size_t count = 0; + for (std::size_t i = 0; i < Side * Side; ++i) { + float texel = 0; + std::memcpy(&texel, bytes + i * sizeof(float), sizeof(texel)); + if (texel == value) ++count; + } + return count; +} + +void Run(Context context, std::uint8_t* block) { + const auto address = reinterpret_cast(block); + const auto stencilAddress = address + BlockBytes / 2; + const auto submit = [&](Recorder& recorder) { + recorder.Submit(); + Check(context.Function("vkQueueWaitIdle")(context.queue), "vkQueueWaitIdle"); + }; + Recorder recorder(context); + recorder.Activate(); + const auto readBack = [&](StorageTexture& texture) { + texture.MarkDirty(); + texture.WriteBack(); + submit(recorder); + }; + const DepthTarget target{address, stencilAddress, {Side, Side}, VK_FORMAT_D32_SFLOAT_S8_UINT, 0.25f, 7}; + Require(DepthSurfaceView(context, target) != VK_NULL_HANDLE, "depth surface was not created"); + Require(DepthSurfaceStorage(context, {}, PlaneResource(address, Format32Float, Side / 2), 0) == nullptr, "a storage view of another extent over the depth memory was taken for the depth surface"); + Require(DepthSurfaceTexture(context, {}, PlaneResource(address, Format32Float, Side / 2), {}) == nullptr, "a sampled view of another extent over the depth memory was taken for the depth surface"); + Require(DepthSurfaceStorage(context, {}, PlaneResource(address, Format8Uint), 0) == nullptr, "an 8-bit storage view of the depth plane was taken for the depth surface"); + 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); + readBack(*depth); + Require(CountFloats(block, 0.25f) == Side * Side, "the storage image did not receive the cleared depth"); + const std::array pattern{std::bit_cast(0.75f), std::bit_cast(0.75f), std::bit_cast(0.75f), std::bit_cast(0.75f)}; + const char* refusal = nullptr; + Require(depth->FillClear(pattern, StorageTexture::WholeImage, refusal), refusal != nullptr ? refusal : "the storage image could not be filled"); + Require(DepthSurfaceStoreBack(context, *depth), "the depth plane's storage image was not stored back"); + submit(recorder); + Require(DepthSurfaceStorage(context, {}, PlaneResource(address, Format32Float), 0) == depth, "the depth plane's storage image was not reused"); + submit(recorder); + readBack(*depth); + Require(CountFloats(block, 0.75f) == Side * Side, "the stored depth did not come back from the depth surface"); + const auto stencil = DepthSurfaceStorage(context, {}, PlaneResource(stencilAddress, Format8Uint), 0); + Require(stencil != nullptr && stencil != depth, "the stencil plane has no storage image of its own"); + submit(recorder); + readBack(*stencil); + const auto stencilBytes = DescribeSurface(PlaneResource(stencilAddress, Format8Uint)).guestBytes; + Require(stencilBytes <= BlockBytes / 2 && std::count(block + BlockBytes / 2, block + BlockBytes / 2 + stencilBytes, 7) == static_cast(Side * Side), "the storage image did not receive the cleared stencil"); + const std::array half{std::bit_cast(0.5f), std::bit_cast(0.5f), std::bit_cast(0.5f), std::bit_cast(0.5f)}; + Require(depth->FillClear(half, StorageTexture::WholeImage, refusal), refusal != nullptr ? refusal : "the storage image could not be filled"); + Require(DepthSurfaceStoreBack(context, *depth), "the depth plane's storage image was not stored back"); + submit(recorder); + auto layeredDepth = PlaneResource(address, Format32Float); + layeredDepth.dimension = TextureDimension::k2DArray; + const auto arrayDepth = DepthSurfaceStorage(context, {}, layeredDepth, 0); + Require(arrayDepth != nullptr && arrayDepth != depth, "a one-layer 2D array view of the depth plane was not taken for the depth surface as a storage image of its own"); + ExpectFailure([&] { static_cast(DepthSurfaceStoreBack(context, *depth)); }, "through a view that another view of the plane", "a store back through the 2D view that the one-layer 2D array view replaced was accepted"); + submit(recorder); + readBack(*arrayDepth); + Require(CountFloats(block, 0.5f) == Side * Side, "the one-layer 2D array view did not receive the depth plane"); + std::fill(block + BlockBytes / 2, block + BlockBytes / 2 + stencilBytes, std::uint8_t{0}); + auto layeredStencil = PlaneResource(stencilAddress, Format8Uint); + layeredStencil.dimension = TextureDimension::k2DArray; + const auto arrayStencil = DepthSurfaceStorage(context, {}, layeredStencil, 0); + Require(arrayStencil != nullptr, "a one-layer 2D array view of the stencil plane was not taken for the depth surface"); + submit(recorder); + readBack(*arrayStencil); + Require(std::count(block + BlockBytes / 2, block + BlockBytes / 2 + stencilBytes, 7) == static_cast(Side * Side), "the one-layer 2D array view did not receive the stencil plane"); + layeredStencil.depthOrLastArray = 1; + ExpectFailure([&] { static_cast(DepthSurfaceStorage(context, {}, layeredStencil, 0)); }, "storage image access to depth/stencil surface", "a two-layer 2D array view of the stencil plane was accepted"); + layeredDepth.depthOrLastArray = 1; + ExpectFailure([&] { static_cast(DepthSurfaceStorage(context, {}, layeredDepth, 0)); }, "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.baseArray = 1; + 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"); +} + +} + +int main() { + try { + std::unique_ptr device; + try { + device = std::make_unique(); + } catch (const std::exception& error) { + if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw; + std::printf("skipped, no usable Vulkan device: %s\n", error.what()); + return VulkanTestSkipped; + } + auto context = device->GetContext(); + TextureDetiler detiler(context); + context.detiler = &detiler; + auto* block = Storage.data() + (BlockBytes - reinterpret_cast(Storage.data()) % BlockBytes) % BlockBytes; + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Run(context, block); + ClearDepthSurfaces(context.device); + RunPairings(context, block); + ClearDepthSurfaces(context.device); + } + GuestAllocations::Mutation().Remove(block); + std::cout << "depth surface storage access tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageDispatch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageDispatch.cpp new file mode 100644 index 0000000000..4c2e019b69 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageDispatch.cpp @@ -0,0 +1,414 @@ +#include "DepthFastClearHarness.hpp" +#include "VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using DepthFastClearHarness::Covered; +using DepthFastClearHarness::Draw; +using DepthFastClearHarness::Height; +using DepthFastClearHarness::Pixels; +using DepthFastClearHarness::Shaders; +using DepthFastClearHarness::Surface; +using DepthFastClearHarness::Width; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Format8Uint = 5; +constexpr std::uint32_t Format32Float = 22; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t Type2DArray = 13; +constexpr std::uint32_t TileLinear = 0; +constexpr std::uint32_t TileStandard256B = 1; +constexpr std::uint32_t TileZ64KBX = 0x18; +constexpr std::size_t BlockBytes = 2u << 20u; +constexpr std::size_t StencilOffset = 1u << 16u; +constexpr std::size_t OutputOffset = 2u << 16u; +constexpr std::size_t MultisampledOffset = 4u << 16u; +constexpr std::size_t MultisampledColorOffset = 5u << 16u; +constexpr std::size_t SampledOffset = 8u << 16u; +constexpr std::size_t LaterDepthOffset = 10u << 16u; +constexpr std::size_t ArrayDepthOffset = 12u << 16u; +constexpr std::size_t ArrayStencilOffset = 13u << 16u; +constexpr std::size_t ArrayOffset = 16u << 16u; +constexpr std::uint32_t ArrayLayers = 3; + +alignas(256) constexpr std::array ScaleDepthCode{ + 0x7e3c0300u, 0x7e3e0280u, 0x7e140280u, 0xf0001108u, 0x00010a1eu, 0xbf8c3f70u, 0x34060082u, + 0xe0701000u, 0x80000a03u, 0x1014140cu, 0xf0201108u, 0x00010a1eu, 0xbf810000u, 0xbf810000u, +}; + +alignas(256) constexpr std::array AddStencilCode{ + 0x7e3c0300u, 0x7e3e0280u, 0x7e140280u, 0xf0001108u, 0x00010a1eu, 0xbf8c3f70u, 0x34060082u, + 0xe0701000u, 0x80000a03u, 0x4a141484u, 0xf0201108u, 0x00010a1eu, 0xbf810000u, 0xbf810000u, +}; + +alignas(256) constexpr std::array ScaleDepthLayerCode{ + 0x7e3c0300u, 0x7e3e0280u, 0x7e400280u, 0x7e140280u, 0xf0001128u, 0x00010a1eu, 0xbf8c3f70u, 0x34060082u, + 0xe0701000u, 0x80000a03u, 0x1014140cu, 0xf0201128u, 0x00010a1eu, 0xbf810000u, 0xbf810000u, 0xbf810000u, +}; + +alignas(256) constexpr std::array AddStencilLayerCode{ + 0x7e3c0300u, 0x7e3e0280u, 0x7e400280u, 0x7e140280u, 0xf0001128u, 0x00010a1eu, 0xbf8c3f70u, 0x34060082u, + 0xe0701000u, 0x80000a03u, 0x4a141484u, 0xf0201128u, 0x00010a1eu, 0xbf810000u, 0xbf810000u, 0xbf810000u, +}; + +alignas(256) constexpr std::array StoreCode{ + 0x7e280d00u, 0x102828ffu, 0x3c800000u, 0x7e3c0300u, 0x7e3e0280u, 0xf0201108u, 0x0001141eu, 0xbf810000u, 0xbf810000u, +}; + +alignas(256) constexpr std::array StoreLayerCode{ + 0x7e280d00u, 0x1028280eu, 0x7e3c0300u, 0x7e3e0280u, 0x7e40020cu, 0x7e42020du, 0xf0241128u, 0x0001141eu, 0xbf810000u, 0xbf810000u, +}; + +alignas(256) constexpr std::array StoreTwoViewsCode{ + 0x7e280d00u, 0x102828ffu, 0x3c800000u, 0x7e3c0300u, 0x7e3e0280u, 0xf0201108u, 0x0001141eu, 0xf0201108u, 0x0003141eu, 0xbf810000u, +}; + +alignas(256) constexpr std::array LoadLayerCode{ + 0x7e020280u, 0x7e04020cu, 0xf0001128u, 0x00010300u, 0xbf8c3f70u, 0x34080082u, 0xe0701000u, 0x80000304u, 0xbf810000u, 0xbf810000u, 0xbf810000u, +}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(65536, BlockBytes)); +#endif + Require(block != nullptr, "depth storage dispatch: cannot allocate the guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array TextureDescriptor(const void* data, std::uint32_t format, std::uint32_t type = Type2D, std::uint32_t tile = TileLinear, std::uint32_t lastArray = 0, 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 | (baseArray << 16u), 0u, 0u, 0u, + }; +} + +ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::span code, std::span userData) { + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderRecompiler::ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +ShaderRecompiler::RecompileResult PlaneProgram(AgcDriver::VulkanDevice& device, std::span code, std::span output, const std::uint8_t* plane, std::uint32_t format, float factor) { + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(output.data(), static_cast(output.size_bytes())); + const auto texture = TextureDescriptor(plane, format); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + userData[12] = std::bit_cast(factor); + return Compile(device, code, userData); +} + +void RunPlane(AgcDriver::VulkanDevice& device, const ShaderRecompiler::RecompileResult& program, std::span code, std::span output, std::uint32_t expected, const std::string& what) { + std::fill(output.begin(), output.end(), 0xdeadbeefu); + device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + Require(output[lane] == expected, what + ": the dispatch read " + std::to_string(output[lane]) + " at texel " + std::to_string(lane) + ", expected " + std::to_string(expected)); + } +} + +void RequireRow(std::uint32_t covered, const std::string& what) { + Require(covered == Threads, what + ": " + std::to_string(covered) + " pixels passed, expected the " + std::to_string(Threads) + " the dispatch stored"); + for (std::uint32_t x = 0; x < Threads; ++x) { + Require(std::to_integer(Pixels[x * 4u]) == 255u, what + ": pixel " + std::to_string(x) + " of the first row did not pass"); + } +} + +template +void ExpectRefusal(TAction action, const char* expected, const std::string& what) { + try { + action(); + } catch (const std::exception& error) { + Require(std::string(error.what()).find(expected) != std::string::npos, what + " threw another error: " + error.what()); + return; + } + throw std::runtime_error(what + " was accepted"); +} + +VkStencilOpState StencilEqual(std::uint32_t reference) { + return {VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_EQUAL, 0xffu, 0u, reference}; +} + +void StoreBackCase(AgcDriver::VulkanDevice& device, const Shaders& shaders, std::uint8_t* block) { + auto* depth = block; + auto* stencil = block + StencilOffset; + const std::span output(reinterpret_cast(block + OutputOffset), Threads); + const Surface surface{reinterpret_cast(depth), reinterpret_cast(stencil), 0, false, VK_FORMAT_D32_SFLOAT_S8_UINT}; + const VkStencilOpState writeFive{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_REPLACE, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_ALWAYS, 0xffu, 0xffu, 5u}; + const auto bits = [](float value) { return std::bit_cast(value); }; + Require(Draw(device, shaders, surface, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS, .stencilTest = true, .stencil = writeFive}) == Covered, "the depth and stencil producer did not cover the surface"); + + const auto raise = PlaneProgram(device, ScaleDepthCode, output, depth, Format32Float, 1.5f); + const auto lower = PlaneProgram(device, ScaleDepthCode, output, depth, Format32Float, 0.5f); + RunPlane(device, raise, ScaleDepthCode, output, bits(0.5f), "first depth dispatch"); + RequireRow(Draw(device, shaders, surface, {.depthCompare = VK_COMPARE_OP_LESS}), "depth test against the first dispatch's depth"); + RunPlane(device, lower, ScaleDepthCode, output, bits(0.75f), "second depth dispatch"); + RequireRow(Draw(device, shaders, surface, {.depthCompare = VK_COMPARE_OP_GREATER}), "depth test against the second dispatch's depth"); + Require(Draw(device, shaders, surface, {.depthCompare = VK_COMPARE_OP_LESS}) == 0, "the first dispatch's depth outlived the second dispatch"); + RunPlane(device, raise, ScaleDepthCode, output, bits(0.375f), "third depth dispatch, its program's second"); + RequireRow(Draw(device, shaders, surface, {.depthCompare = VK_COMPARE_OP_LESS}), "depth test against the third dispatch's depth"); + + const auto add = PlaneProgram(device, AddStencilCode, output, stencil, Format8Uint, 0.0f); + RunPlane(device, add, AddStencilCode, output, 5u, "first stencil dispatch"); + RequireRow(Draw(device, shaders, surface, {.depthTest = false, .stencilTest = true, .stencil = StencilEqual(9u)}), "stencil test against the first dispatch's stencil"); + RunPlane(device, add, AddStencilCode, output, 9u, "second stencil dispatch"); + RequireRow(Draw(device, shaders, surface, {.depthTest = false, .stencilTest = true, .stencil = StencilEqual(13u)}), "stencil test against the second dispatch's stencil"); + Require(Draw(device, shaders, surface, {.depthTest = false, .stencilTest = true, .stencil = StencilEqual(5u)}) == Covered - Threads, "a stencil dispatch changed stencil values it did not store"); + RequireRow(Draw(device, shaders, surface, {.depthCompare = VK_COMPARE_OP_LESS}), "depth test after the stencil dispatches"); + + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(depth), Width * Height * sizeof(float), nullptr, "test"); + device.WaitIdle(); + for (std::uint32_t x = 0; x < Width; ++x) { + float texel = 0.0f; + std::memcpy(&texel, depth + x * sizeof(float), sizeof(texel)); + Require(texel == (x < Threads ? 0.5625f : 0.5f), "guest depth texel " + std::to_string(x) + " is " + std::to_string(texel) + " after the flush"); + } + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(stencil), Width * Height, nullptr, "test"); + device.WaitIdle(); + for (std::uint32_t x = 0; x < Width; ++x) { + Require(stencil[x] == (x < Threads ? 13u : 5u), "guest stencil texel " + std::to_string(x) + " is " + std::to_string(stencil[x]) + " after the flush"); + } +} + +void ArrayViewCase(AgcDriver::VulkanDevice& device, const Shaders& shaders, std::uint8_t* block) { + auto* depth = block + ArrayDepthOffset; + auto* stencil = block + ArrayStencilOffset; + const std::span output(reinterpret_cast(block + OutputOffset), Threads); + const Surface surface{reinterpret_cast(depth), reinterpret_cast(stencil), 0, false, VK_FORMAT_D32_SFLOAT_S8_UINT}; + const VkStencilOpState writeFive{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_REPLACE, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_ALWAYS, 0xffu, 0xffu, 5u}; + Require(Draw(device, shaders, surface, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS, .stencilTest = true, .stencil = writeFive}) == Covered, "the depth and stencil producer of the array views did not cover the surface"); + const auto layer = [&](std::span code, const std::uint8_t* plane, std::uint32_t format, float factor) { + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(output.data(), static_cast(output.size_bytes())); + const auto texture = TextureDescriptor(plane, format, Type2DArray); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + userData[12] = std::bit_cast(factor); + return Compile(device, code, userData); + }; + RunPlane(device, layer(ScaleDepthLayerCode, depth, Format32Float, 1.5f), ScaleDepthLayerCode, output, std::bit_cast(0.5f), "depth dispatch through a one-layer 2D array view"); + RequireRow(Draw(device, shaders, surface, {.depthCompare = VK_COMPARE_OP_LESS}), "depth test against the one-layer 2D array view's depth"); + RunPlane(device, layer(AddStencilLayerCode, stencil, Format8Uint, 0.0f), AddStencilLayerCode, output, 5u, "stencil dispatch through a one-layer 2D array view"); + RequireRow(Draw(device, shaders, surface, {.depthTest = false, .stencilTest = true, .stencil = StencilEqual(9u)}), "stencil test against the one-layer 2D array view's stencil"); +} + +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, baseArray); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + userData[12] = layer; + userData[14] = std::bit_cast(1.0f / 128.0f); + return Compile(device, code, userData); +} + +void StoreLayer(AgcDriver::VulkanDevice& device, std::span layers, std::span output, std::uint32_t layer, 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(); +} + +void RequireGuestLayer(std::span layer, bool stored, float rest, float initial, const std::string& what) { + std::vector seen(Threads, 0u); + std::uint32_t kept = 0; + for (std::size_t offset = 0; offset < layer.size(); offset += sizeof(float)) { + float texel = 0.0f; + std::memcpy(&texel, layer.data() + offset, sizeof(texel)); + const auto lane = static_cast(texel * 128.0f); + if (stored && texel < 0.25f && texel != initial && static_cast(lane) / 128.0f == texel) { + ++seen[lane]; + continue; + } + if (texel == rest) { + ++kept; + continue; + } + Require(texel == initial, what + ": guest texel at offset " + std::to_string(offset) + " is " + std::to_string(texel) + ", expected " + std::to_string(rest) + " or the untouched " + std::to_string(initial)); + } + const auto texels = static_cast(layer.size() / sizeof(float)); + const auto expected = (rest == initial ? texels : Width * Height) - (stored ? Threads : 0u); + Require(kept == expected, what + ": " + std::to_string(kept) + " guest texels hold " + std::to_string(rest) + ", expected " + std::to_string(expected)); + if (!stored) return; + for (std::uint32_t lane = 0; lane < Threads; ++lane) Require(seen[lane] == 1u, what + ": the value stored by lane " + std::to_string(lane) + " is in guest memory " + std::to_string(seen[lane]) + " times"); +} + +void LayeredArrayViewCase(AgcDriver::VulkanDevice& device, const Shaders& shaders, std::uint8_t* block) { + const auto sliceBytes = static_cast(AgcDriver::Graphics::DepthSliceBytes({Width, Height}, 4u)); + Require(sliceBytes == 1u << 16u, "a depth slice of the test extent is not one 64 KiB block"); + const std::span layers(block + ArrayOffset, sliceBytes * ArrayLayers); + const std::span output(reinterpret_cast(block + OutputOffset), Threads); + const auto guestLayer = [&](std::uint32_t layer) { return layers.subspan(sliceBytes * layer, sliceBytes); }; + const float untouched = 0.875f; + const float guestDepth = 0.75f; + for (std::uint32_t layer = 0; layer < ArrayLayers; ++layer) { + const float value = layer == 2 ? guestDepth : untouched; + for (std::size_t offset = 0; offset < sliceBytes; offset += sizeof(float)) std::memcpy(guestLayer(layer).data() + offset, &value, sizeof(float)); + } + const Surface first{reinterpret_cast(guestLayer(0).data()), 0, 0, false, VK_FORMAT_D32_SFLOAT}; + const Surface second{reinterpret_cast(guestLayer(1).data()), 0, 0, false, VK_FORMAT_D32_SFLOAT}; + Require(Draw(device, shaders, first, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the first layer's depth producer did not cover its surface"); + Require(Draw(device, shaders, second, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the second layer's depth producer did not cover its surface"); + + StoreLayer(device, layers, output, 1); + RequireRow(Draw(device, shaders, second, {.depthCompare = VK_COMPARE_OP_GREATER}), "depth test against the values a 2D array storage view stored to the second layer's surface"); + Require(Draw(device, shaders, first, {.depthCompare = VK_COMPARE_OP_GREATER}) == 0, "a store to the second layer changed the first layer's surface"); + StoreLayer(device, layers, output, 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(); + RequireGuestLayer(guestLayer(0), false, 0.5f, untouched, "the first layer in guest memory after the flush"); + RequireGuestLayer(guestLayer(1), true, 0.5f, untouched, "the second layer in guest memory after the flush"); + RequireGuestLayer(guestLayer(2), true, guestDepth, guestDepth, "the third layer in guest memory after the flush"); +} + +void MultisampledCase(AgcDriver::VulkanDevice& device, const Shaders& compiled, std::uint8_t* block) { + auto* depth = block + MultisampledOffset; + const std::array shaders{{ + {ShaderRecompiler::ShaderStage::Vertex, &compiled.vertex, 0}, + {ShaderRecompiler::ShaderStage::Fragment, &compiled.pixel, PixelPushOffset(compiled.vertexPush, device.Target())} + }}; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(block + MultisampledColorOffset), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Width * Height * 16u, 0xe4u}; + state.color.samples = 4; + state.colors = {state.color}; + state.hasColorTarget = true; + state.depth = AgcDriver::Graphics::DepthTarget{reinterpret_cast(depth), 0, {Width, Height}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0, false, 4}; + state.depthTest = true; + state.depthWrite = true; + state.depthCompare = VK_COMPARE_OP_ALWAYS; + state.samples = 4; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(DepthFastClearHarness::Triangles().size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); + std::vector userData(16, 0u); + const auto texture = TextureDescriptor(depth, Format32Float); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + const auto program = Compile(device, StoreCode, userData); + ExpectRefusal([&] { device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(StoreCode.data())); }, "storage image access to a multisampled depth surface", "a storage view of a multisampled depth plane"); +} + +void SampledCopyCase(AgcDriver::VulkanDevice& device, const Shaders& shaders, std::uint8_t* block) { + auto* depth = block + SampledOffset; + const std::span output(reinterpret_cast(block + OutputOffset), Threads); + const Surface surface{reinterpret_cast(depth), 0, 0, false, VK_FORMAT_D32_SFLOAT}; + const Surface later{reinterpret_cast(block + LaterDepthOffset), 0, 0, false, VK_FORMAT_D32_SFLOAT}; + Require(Draw(device, shaders, surface, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the sampled depth producer did not cover the surface"); + Require(Draw(device, shaders, later, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the later depth producer did not cover its surface"); + + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(output.data(), static_cast(output.size_bytes())); + const auto layers = TextureDescriptor(depth, Format32Float, Type2DArray, TileLinear, 1u); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(layers.begin(), layers.end(), userData.begin() + 4); + const auto load = Compile(device, LoadLayerCode, userData); + const auto raise = PlaneProgram(device, ScaleDepthCode, output, depth, Format32Float, 1.5f); + RunPlane(device, load, LoadLayerCode, output, std::bit_cast(0.5f), "first layer of the depth plane copy"); + RunPlane(device, load, LoadLayerCode, output, std::bit_cast(0.5f), "first layer of the unchanged depth plane copy"); + RunPlane(device, raise, ScaleDepthCode, output, std::bit_cast(0.5f), "depth dispatch between two plane copies"); + RunPlane(device, load, LoadLayerCode, output, std::bit_cast(0.75f), "first layer of the depth plane copy after the dispatch stored to the plane"); +} + +void TwoViewsCase(AgcDriver::VulkanDevice& device, std::uint8_t* block) { + std::vector userData(20, 0u); + const auto first = TextureDescriptor(block, Format32Float, Type2D, TileLinear); + const auto second = TextureDescriptor(block, Format32Float, Type2D, TileStandard256B); + std::copy(first.begin(), first.end(), userData.begin() + 4); + std::copy(second.begin(), second.end(), userData.begin() + 12); + const auto program = Compile(device, StoreTwoViewsCode, userData); + ExpectRefusal([&] { device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(StoreTwoViewsCode.data())); }, "through a view that another view of the plane", "a dispatch storing to two views of one depth plane"); +} + +} + +int main() { + try { + GuestBlock block; + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto shaders = DepthFastClearHarness::Compile(*device); + StoreBackCase(*device, shaders, block.Data()); + ArrayViewCase(*device, shaders, block.Data()); + LayeredArrayViewCase(*device, shaders, block.Data()); + MultisampledCase(*device, shaders, block.Data()); + TwoViewsCase(*device, block.Data()); + SampledCopyCase(*device, shaders, block.Data()); + std::puts("depth storage dispatch tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageReuse.cpp new file mode 100644 index 0000000000..dbd774f671 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageReuse.cpp @@ -0,0 +1,90 @@ +#include "DepthFastClearHarness.hpp" +#include "VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using DepthFastClearHarness::Height; +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, +}; +alignas(256) constexpr std::array SampleCode{ + 0x7e3c02ffu, 0x3f000000u, 0x7e3e02ffu, 0x3f000000u, 0xf09c0108u, + 0x00610a1eu, 0x34060082u, 0xe0701000u, 0x80000a03u, 0xbf810000u, +}; + +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 = 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 | (swizzle << 20u), 0u, 0u, 0u, 0u, + 0x92u, 0u, 0u, 0u, + }; + const auto code = sampled ? std::span(SampleCode) : std::span(LoadCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{32, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderRecompiler::ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, data, compute, std::nullopt, std::nullopt, memory}, device.Target(), {0, 0, 0, 128}, + }; + request.useCache = false; + const auto shader = ShaderRecompiler::Recompile(request); + device.Dispatch(shader, 1, 1, 1); + device.WaitIdle(); + for (std::size_t lane = 0; lane < 32; ++lane) { + Require((sampled && format == 7u ? std::abs(std::bit_cast(Output[lane]) - expected) <= 1.0f / 65535.0f : Output[lane] == std::bit_cast(expected)), "depth reuse returned the wrong GPU value at lane " + std::to_string(lane) + ": got " + std::to_string(Output[lane]) + ", expected " + std::to_string(std::bit_cast(expected))); + } + Require(std::all_of(Depth.begin(), Depth.end(), [](float value) { return value == 0.25f; }), "depth reuse changed the guest bytes"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto shaders = DepthFastClearHarness::Compile(*device); + 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); + const DepthFastClearHarness::Surface surface{address, 0, 0, false, format}; + 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; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} 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/Division.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Division.cpp index 39bf75b417..9cbfb1997d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Division.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Division.cpp @@ -7,7 +7,9 @@ #include #include #include +#include #include +#include #include #include @@ -40,6 +42,51 @@ alignas(256) constexpr std::array FmaCode{ 0xd54b000a, 0x041a0b04, 0xe0702000, 0x80010a01, 0xbf810000, }; +alignas(256) constexpr std::array ScaleCode{ + 0x34020082u, 0xe0302000u, 0x80000401u, 0xe0302004u, 0x80000501u, 0xe0302008u, 0x80000601u, 0xe030200cu, + 0x80000701u, 0xbf8c3f70u, 0xd56d140au, 0x041a0b04u, 0xd501000bu, 0x00510280u, 0xe0702000u, 0x80010a01u, + 0xe0702004u, 0x80010b01u, 0xbf810000u, +}; + +struct ScaleVector { + std::uint32_t s0, s1, s2, flushed, kept, vcc; +}; + +constexpr ScaleVector ScaleVectors[] = { + {0x00000001u, 0x3f800000u, 0x3f800000u, 0x00000000u, 0x00000001u, 0x00000000u}, + {0x3f800000u, 0x00000001u, 0x3f800000u, 0xffc00000u, 0x3f800000u, 0x00000001u}, + {0x3f800000u, 0x3f800000u, 0x00000001u, 0xffc00000u, 0x3f800000u, 0x00000001u}, + {0x80000001u, 0x3f800000u, 0x3f800000u, 0x80000000u, 0x80000001u, 0x00000000u}, + {0x3f800000u, 0x80000001u, 0x3f800000u, 0xffc00000u, 0x3f800000u, 0x00000001u}, + {0x3f800000u, 0x3f800000u, 0x80000001u, 0xffc00000u, 0x3f800000u, 0x00000001u}, + {0x007fffffu, 0x3f800000u, 0x3f800000u, 0x00000000u, 0x007fffffu, 0x00000000u}, + {0x3f800000u, 0x007fffffu, 0x3f800000u, 0xffc00000u, 0x3f800000u, 0x00000001u}, + {0x3f800000u, 0x3f800000u, 0x007fffffu, 0xffc00000u, 0x3f800000u, 0x00000001u}, + {0x807fffffu, 0x3f800000u, 0x3f800000u, 0x80000000u, 0x807fffffu, 0x00000000u}, + {0x3f800000u, 0x807fffffu, 0x3f800000u, 0xffc00000u, 0x3f800000u, 0x00000001u}, + {0x3f800000u, 0x3f800000u, 0x807fffffu, 0xffc00000u, 0x3f800000u, 0x00000001u}, + {0x3f800000u, 0x00000001u, 0x00000001u, 0xffc00000u, 0x5f800000u, 0x00000000u}, + {0xbf800000u, 0x80000001u, 0x80000001u, 0xffc00000u, 0xdf800000u, 0x00000000u}, + {0x3f800000u, 0x00000001u, 0x80000001u, 0xffc00000u, 0x5f800000u, 0x00000000u}, + {0x00000000u, 0x3f800000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x80000000u, 0x3f800000u, 0x3f800000u, 0x80000000u, 0x80000000u, 0x00000000u}, + {0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x00000000u}, + {0xbfc00000u, 0x3f800000u, 0x3f800000u, 0xbfc00000u, 0xbfc00000u, 0x00000000u}, + {0x00800000u, 0x3f800000u, 0x3f800000u, 0x00800000u, 0x00800000u, 0x00000000u}, + {0x80800000u, 0x3f800000u, 0x3f800000u, 0x80800000u, 0x80800000u, 0x00000000u}, + {0x7f800000u, 0x3f800000u, 0x3f800000u, 0x7f800000u, 0x7f800000u, 0x00000000u}, + {0xff800000u, 0x3f800000u, 0x3f800000u, 0xff800000u, 0xff800000u, 0x00000000u}, + {0x7fc12345u, 0x3f800000u, 0x3f800000u, 0x7fc12345u, 0x7fc12345u, 0x00000000u}, + {0xff812345u, 0x3f800000u, 0x3f800000u, 0xff812345u, 0xff812345u, 0x00000000u}, + {0x3f800000u, 0x00000000u, 0x3f800000u, 0xffc00000u, 0xffc00000u, 0x00000001u}, + {0x3f800000u, 0x3f800000u, 0x00000000u, 0xffc00000u, 0xffc00000u, 0x00000001u}, + {0x3f800000u, 0x80000000u, 0x3f800000u, 0xffc00000u, 0xffc00000u, 0x00000001u}, + {0x3f800000u, 0x3f800000u, 0x80000000u, 0xffc00000u, 0xffc00000u, 0x00000001u}, + {0x3f800000u, 0x00800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x00000001u}, + {0x3f800000u, 0x3f800000u, 0x00800000u, 0x3f800000u, 0x3f800000u, 0x00000001u}, + {0x00800000u, 0x00800000u, 0x3f800000u, 0x20800000u, 0x20800000u, 0x00000001u}, +}; + struct HelperVector { std::uint32_t s0, s1, s2, vcc; std::uint32_t scale, scaleVcc, fmas, fixup; @@ -302,6 +349,13 @@ constexpr HelperVector HelperVectors[] = { {0x8c367fd8u, 0xbf976e24u, 0x8c367fd8u, 1u, 0xac367fd8u, 1u, 0x00000000u, 0x0c367fd8u}, {0x00f745cfu, 0x7f7fffffu, 0x00f745cfu, 0u, 0x00f745cfu, 1u, 0x40f745ceu, 0x00000000u}, {0xcb7cf32du, 0xbfe52415u, 0xbf6a1e62u, 0u, 0xcb7cf32du, 0u, 0x4be2692eu, 0x4b7cf32du}, + {0x3f800000u, 0x7f7fffffu, 0xff7fffffu, 1u, 0x1f800000u, 0u, 0x00000000u, 0xbf800000u}, + {0x3f800000u, 0xf1800000u, 0x71800000u, 1u, 0x3f800000u, 0u, 0x00000000u, 0xbf800000u}, + {0x7f7fffffu, 0xbf800000u, 0x7f7fffffu, 1u, 0x7f7fffffu, 1u, 0x00000000u, 0xff7fffffu}, + {0x3f800000u, 0xdf800000u, 0x5f800000u, 1u, 0x3f800000u, 0u, 0x00000000u, 0xbf800000u}, + {0x5f400000u, 0xdf200000u, 0x7ef00000u, 1u, 0x5f400000u, 0u, 0x00000000u, 0xdf400000u}, + {0xe40a117du, 0xbf800000u, 0xe40a117du, 1u, 0xe40a117du, 0u, 0x00000000u, 0x640a117du}, + {0x3f800000u, 0xdf7fffffu, 0x5f7fffffu, 1u, 0x3f800000u, 0u, 0x00000000u, 0xbf800000u}, }; struct DivisionVector { @@ -567,6 +621,29 @@ constexpr DivisionVector DivisionVectors[] = { {0x2210592fu, 0x0ac8fdd3u, 0x56b7dabeu}, }; +struct FmasNanVector { + std::uint32_t s0, s1, s2, noIeee, ieee; +}; + +constexpr FmasNanVector FmasNanVectors[] = { + {0xff812345u, 0x3f800000u, 0x3f800000u, 0xff812345u, 0xffc12345u}, + {0x7f812345u, 0x3f800000u, 0x3f800000u, 0x7f812345u, 0x7fc12345u}, + {0x3f800000u, 0xff812345u, 0x3f800000u, 0xff812345u, 0xffc12345u}, + {0x3f800000u, 0x3f800000u, 0xff812345u, 0xff812345u, 0xffc12345u}, + {0x7fc12345u, 0xff812345u, 0xff812345u, 0x7fc12345u, 0x7fc12345u}, + {0xff812345u, 0x7fc12345u, 0x7f812345u, 0xff812345u, 0xffc12345u}, + {0x3f800000u, 0x7fc12345u, 0xff812345u, 0x7fc12345u, 0x7fc12345u}, + {0x3f800000u, 0x3f800000u, 0x7fc12345u, 0x7fc12345u, 0x7fc12345u}, + {0x7f800000u, 0x00000000u, 0x7fc12345u, 0xffc00000u, 0xffc00000u}, + {0xff800000u, 0x80000000u, 0xff812345u, 0xffc00000u, 0xffc00000u}, + {0x00000000u, 0xff800000u, 0x7f812345u, 0xffc00000u, 0xffc00000u}, + {0x80000000u, 0x7f800000u, 0xffc12345u, 0xffc00000u, 0xffc00000u}, + {0x7f800000u, 0x3f800000u, 0xff800000u, 0xffc00000u, 0xffc00000u}, + {0x3f800000u, 0x7f7fffffu, 0xff7fffffu, 0x00000000u, 0x00000000u}, + {0x3f800000u, 0xf1800000u, 0x71800000u, 0x00000000u, 0x00000000u}, + {0x7f800000u, 0x3f800000u, 0x3f800000u, 0x7f800000u, 0x7f800000u}, +}; + 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}; @@ -574,7 +651,8 @@ std::array BufferDescriptor(const void* data, std::uint32_t co class Shader { public: - Shader(AgcDriver::VulkanDevice& device, std::span code) : device(device), code(code) { + Shader(AgcDriver::VulkanDevice& device, std::span code, + const std::optional& floatMode = ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}) : device(device), code(code) { 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())); @@ -589,7 +667,7 @@ public: {0, 0, 0, 128} }; request.useCache = false; - request.context.floatMode = ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}; + request.context.floatMode = floatMode; result = ShaderRecompiler::Recompile(request); } @@ -621,6 +699,41 @@ bool HostFusesFma(AgcDriver::VulkanDevice& device) { return Output[0] == 0x2f8e8ae0u; } +void CheckScaleDenormals(AgcDriver::VulkanDevice& device) { + const std::array, 5> modes{{ + std::nullopt, + ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}, + ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}, + ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}, + ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}, + }}; + for (const auto& mode : modes) { + Shader shader(device, ScaleCode, mode); + Input.fill(0u); + for (std::uint32_t lane = 0; lane < std::size(ScaleVectors); ++lane) { + const auto& vector = ScaleVectors[lane]; + std::copy_n(std::array{vector.s0, vector.s1, vector.s2, 0u}.begin(), 4, Input.begin() + lane * Stride); + } + shader.Run(); + for (std::uint32_t lane = 0; lane < std::size(ScaleVectors); ++lane) { + const auto& vector = ScaleVectors[lane]; + const auto* out = &Output[lane * Stride]; + const auto expected = mode.has_value() && ((mode->floatMode >> 4u) & 3u) == 3u ? vector.kept : vector.flushed; + const std::string where = "division scale denormals: lane " + std::to_string(lane) + " mode " + (mode.has_value() ? Hex(mode->floatMode) : "unknown"); + Require(out[0] == expected && out[1] == vector.vcc, where + ": v_div_scale_f32 is " + Hex(out[0]) + " vcc " + std::to_string(out[1]) + ", expected " + Hex(expected) + " vcc " + std::to_string(vector.vcc)); + } + } + for (const std::uint32_t mode : {0xd0u, 0xe0u}) { + bool refused = false; + try { + Shader shader(device, ScaleCode, 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, "division scale denormals: FLOAT_MODE " + Hex(mode) + " was not refused"); + } +} + void CheckHelpers(AgcDriver::VulkanDevice& device, bool fused) { Shader shader(device, HelperCode); constexpr std::uint32_t count = sizeof(HelperVectors) / sizeof(HelperVectors[0]); @@ -644,6 +757,24 @@ void CheckHelpers(AgcDriver::VulkanDevice& device, bool fused) { } } +void CheckFmasNan(AgcDriver::VulkanDevice& device, const std::optional& floatMode) { + Shader shader(device, HelperCode, floatMode); + Input.fill(0u); + for (std::uint32_t lane = 0; lane < std::size(FmasNanVectors) * 2u; ++lane) { + const auto& vector = FmasNanVectors[lane / 2u]; + std::copy_n(std::array{vector.s0, vector.s1, vector.s2, lane & 1u}.begin(), 4, Input.begin() + lane * Stride); + } + shader.Run(); + const bool ieee = floatMode.has_value() && floatMode->ieeeMode; + for (std::uint32_t lane = 0; lane < std::size(FmasNanVectors) * 2u; ++lane) { + const auto& vector = FmasNanVectors[lane / 2u]; + const auto actual = Output[lane * Stride + 2u]; + const auto expected = ieee ? vector.ieee : vector.noIeee; + const std::string mode = floatMode.has_value() ? Hex(floatMode->floatMode) + " IEEE=" + std::to_string(ieee) : "no float mode"; + Require(actual == expected, "division fmas nan: lane " + std::to_string(lane) + " " + mode + " vcc " + std::to_string(lane & 1u) + ": v_div_fmas_f32 is " + Hex(actual) + ", expected " + Hex(expected)); + } +} + void CheckDivision(AgcDriver::VulkanDevice& device) { Shader shader(device, DivisionCode); constexpr std::uint32_t count = sizeof(DivisionVectors) / sizeof(DivisionVectors[0]); @@ -668,12 +799,18 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; + CheckScaleDenormals(*device); const bool fused = HostFusesFma(*device); CheckHelpers(*device, fused); + CheckFmasNan(*device, std::nullopt); + CheckFmasNan(*device, ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}); + CheckFmasNan(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}); + CheckFmasNan(*device, ShaderRecompiler::ShaderFloatMode{0xf0u, false, false, false}); + CheckFmasNan(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, false, true, false}); if (fused) { CheckDivision(*device); } else { - std::puts("the device splits fma into a multiply and an add, so v_div_fmas_f32 and the division macro are not checked"); + std::puts("the device splits fma into a multiply and an add, so only the directed v_div_fmas_f32 rows are checked and the division macro is not checked"); } std::puts("division tests passed"); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Dpp8PkFmac.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Dpp8PkFmac.cpp new file mode 100644 index 0000000000..af31f08747 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Dpp8PkFmac.cpp @@ -0,0 +1,113 @@ +#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 Stride = 4; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +std::array Code(std::uint32_t selectors) { + const auto dpp8 = [&](std::uint32_t source) { return (selectors << 8u) | source; }; + return { + 0x34080084u, + 0xe03c1000u, 0x80000104u, + 0xbf8c3f70u, + 0x7e140301u, + 0x781406e9u, dpp8(2u), + 0x7e1a02e9u, dpp8(2u), + 0x7e160301u, + 0x7816070du, + 0x7e180301u, + 0x78180702u, + 0xe07c1000u, 0x80010a04u, + 0xbf810000u, + }; +} + +std::uint32_t Selectors(const std::array& lanes) { + std::uint32_t value = 0; + for (std::uint32_t lane = 0; lane < 8u; ++lane) value |= lanes[lane] << (lane * 3u); + return value; +} + +std::uint16_t Half(std::uint32_t seed) { + const auto sign = (seed >> 15u) & 1u; + const auto exponent = 10u + seed % 10u; + const auto mantissa = (seed >> 4u) & 0x3ffu; + return static_cast((sign << 15u) | (exponent << 10u) | mantissa); +} + +std::array BufferDescriptor(const void* data, std::size_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), static_cast(bytes), 0x31016facu}; +} + +void Run(AgcDriver::VulkanDevice& device, std::uint32_t wave, const std::array& lanes, const std::string& what) { + for (std::uint32_t lane = 0; lane < MaxThreads; ++lane) { + for (std::uint32_t word = 0; word < 3u; ++word) { + const auto seed = lane * 2654435761u + word * 40503u + 12345u; + Input[lane * Stride + word] = static_cast(Half(seed)) | (static_cast(Half(seed * 7u + 3u)) << 16u); + } + } + Output.fill(0xdeadbeefu); + const auto code = Code(Selectors(lanes)); + std::vector userData; + for (const auto& descriptor : {BufferDescriptor(Input.data(), sizeof(Input)), BufferDescriptor(Output.data(), sizeof(Output))}) { + userData.insert(userData.end(), descriptor.begin(), descriptor.end()); + } + const std::span program(code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(program)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{wave, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), program, 0, {}}, + {wave, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.ComputeTarget(wave), + {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(); + bool permuted = false; + for (std::uint32_t lane = 0; lane < wave; ++lane) { + const auto fused = Output[lane * Stride]; + const auto separate = Output[lane * Stride + 1u]; + const auto plain = Output[lane * Stride + 2u]; + Require(fused == separate, what + " (wave" + std::to_string(wave) + "): lane " + std::to_string(lane) + " v_pk_fmac_f16 dpp8 gave " + std::to_string(fused) + + ", v_mov_b32 dpp8 then v_pk_fmac_f16 gave " + std::to_string(separate)); + permuted = permuted || fused != plain; + } + Require(permuted, what + " (wave" + std::to_string(wave) + "): the dpp8 result equals the unpermuted one in every lane"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const std::uint32_t wave : {32u, 64u}) { + Run(*device, wave, {7, 6, 5, 4, 3, 2, 1, 0}, "reversed groups of eight"); + Run(*device, wave, {1, 0, 3, 2, 5, 4, 7, 6}, "swapped neighbours"); + } + std::puts("dpp8 v_pk_fmac_f16 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 new file mode 100644 index 0000000000..ab9818c6ba --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp @@ -0,0 +1,255 @@ +#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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::DriverDetail::DrawMiss; +using AgcDriver::DriverDetail::DrawKeySightings; + +alignas(256) constexpr std::array VertexCode{0x7e000208u, 0x7e020280u, 0x7e040280u, 0x7e0602f2u, 0xf80008cfu, 0x03020100u, 0xbf810000u, 0xbf810000u}; +alignas(256) constexpr std::array 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{}; + ShaderUserData userData{}; +}; + +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; + 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 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); + 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(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, 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}); + 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); + 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"); + Require(counters.inserts == 1, mode + ": the three draws inserted " + std::to_string(counters.inserts) + " draw entries, expected 1: a key is cached the second time it is seen"); + Require(layout == 0, mode + ": the repeated draw missed the draw cache on its push offset (" + std::to_string(layout) + " layout misses)"); + Require(counters.hits == 1, mode + ": the repeated draw did not reuse the cached draw (" + std::to_string(counters.hits) + " hits)"); +} + +void 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"); + Require(!sightings.SeenBefore(11) && !sightings.SeenBefore(12), "an unseen draw key was admitted"); + Require(sightings.SeenBefore(11) && sightings.SeenBefore(12) && sightings.SeenBefore(11), "two keys in one bucket, drawn in turn, were not admitted at their second sight"); + Require(!sightings.SeenBefore(13) && !sightings.SeenBefore(14), "an unseen draw key was admitted"); + Require(!sightings.SeenBefore(0), "the oldest key of a full bucket was kept over the newest"); + Require(sightings.SeenBefore(12) && sightings.SeenBefore(13) && sightings.SeenBefore(14), "a full bucket lost a key newer than the one it replaced"); +} + +} + +int main() { + try { + SightingsKeepCollidingKeys(); + bool libraries = false; + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + libraries = device->GraphicsPipelineLibraries(); + } + 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) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} 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 new file mode 100644 index 0000000000..9413952247 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawOffsetIndices.cpp @@ -0,0 +1,606 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#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 +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; +using Path = AgcDriver::Graphics::IndirectDrawPath; +using Rule = AgcDriver::Pm4::DrawParameters::IndirectDraw::Rule; + +constexpr std::uint32_t BaseVertexSgpr = 16; +constexpr std::uint32_t StartInstanceSgpr = 17; +constexpr std::uint32_t Vertices = 3; +constexpr std::uint32_t Instances = 2; +constexpr std::uint32_t Slots = Vertices * Instances; +constexpr std::uint32_t Unwritten = 0xdeadbeefu; +constexpr std::uint32_t NoLocation = 0x280u; + +alignas(256) constexpr std::array VertexCode{ + 0x7e020305, 0x7e040308, + 0xd70f6a05, 0x00020a10, + 0xd70f6a08, 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 PostAddCode{ + 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, + 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, + 0xd70f6a08, 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 ClampCode{ + 0x7e020280, 0x7e040280, + 0xd55d8005, 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 TwiceCode{ + 0x7e020280, 0x7e040280, + 0xd70f7d05, 0x00020a10, + 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 ExecCode{ + 0xbefe03c1, 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 CarryWriteCode{ + 0xd70f1102, 0x00020100, + 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 OtherVgprCode{ + 0x7e020280, 0x7e040280, + 0xd70f7d00, 0x00020010, + 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 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; +constexpr std::uint32_t Height = 32; +alignas(256) std::array Pixels{}; +alignas(256) std::array Output{}; +alignas(256) std::array VertexSlots{}; +alignas(256) std::array InstanceSlots{}; +alignas(256) std::array BufferTable{}; +alignas(256) constexpr std::array Indices{2, 0, 1, 0}; + +std::array SlotDescriptor(const void* data, std::uint32_t records) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (4u << 16u), records, 0x01014facu}; +} + +std::array OutputDescriptor() { + const auto address = reinterpret_cast(Output.data()); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), static_cast(sizeof(Output)), 0x31016facu}; +} + +struct Expected { + std::uint32_t vertex; + std::uint32_t instance; + std::uint32_t vertexSum; + std::uint32_t instanceSum; +}; + +class Fixture { +public: + explicit Fixture(std::shared_ptr device) : device(std::move(device)) { + const auto vertex = SlotDescriptor(VertexSlots.data(), static_cast(VertexSlots.size())); + const auto instance = SlotDescriptor(InstanceSlots.data(), static_cast(InstanceSlots.size())); + std::copy(vertex.begin(), vertex.end(), BufferTable.begin()); + std::copy(instance.begin(), instance.end(), BufferTable.begin() + 4); + records = static_cast(::operator new(RecordBytes, std::align_val_t{65536})); + GuestAllocations::Mutation mutation; + mutation.Add(records, RecordBytes, true, true, true); + } + + std::vector UserData(std::uint32_t baseVertex, std::uint32_t startInstance) const { + std::vector userData(10, 0u); + const auto output = OutputDescriptor(); + std::copy(output.begin(), output.end(), userData.begin()); + const auto table = reinterpret_cast(BufferTable.data()); + userData[4] = static_cast(table); + userData[5] = static_cast(table >> 32u); + userData[BaseVertexSgpr - 8u] = baseVertex; + userData[StartInstanceSgpr - 8u] = startInstance; + return userData; + } + + ShaderRecompiler::RecompileResult Vertex(std::span code, const std::vector& userData) const { + ShaderRecompiler::ShaderVertexStageInfo info{}; + info.resourcesNum = 2; + info.resources[0].fields = {BufferTable[0], BufferTable[1], BufferTable[2], BufferTable[3]}; + info.resources[1].fields = {BufferTable[4], BufferTable[5], BufferTable[6], BufferTable[7]}; + info.resourcesDst[0] = {10, 1, 0, 0}; + info.resourcesDst[1] = {11, 1, 1, 1}; + info.fetchAttribReg = 6; + info.fetchBufferReg = 4; + info.fetchEmbedded = true; + const std::array memory{{ + {reinterpret_cast(code.data()), std::as_bytes(code)}, + {reinterpret_cast(BufferTable.data()), std::as_bytes(std::span(BufferTable))} + }}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Vertex, reinterpret_cast(code.data()), code, 0, {}}, + {32, 8, userData, std::nullopt, std::nullopt, info, memory}, + device->Target(), + {0, 0, 0, 64} + }; + request.useCache = false; + auto result = ShaderRecompiler::Recompile(request); + Require(result.vertexOffsetSgpr == static_cast(BaseVertexSgpr) && result.instanceOffsetSgpr == static_cast(StartInstanceSgpr), "the fetch adds of s16 and s17 were not folded into the draw (vertex " + std::to_string(result.vertexOffsetSgpr) + ", instance " + std::to_string(result.instanceOffsetSgpr) + ")"); + return result; + } + + 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 = 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; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15u; + state.blends = {state.blend}; + return state; + } + + void Draw(const ShaderRecompiler::RecompileResult& vertex, const AgcDriver::Pm4::DrawParameters& draw) { + const auto vertexPush = static_cast(vertex.pushConstants.size()); + ShaderRecompiler::ShaderPixelStageInfo pixelInfo{}; + pixelInfo.wave32 = true; + pixelInfo.targetOutputMode[0] = 9u; + pixelInfo.targetExportMapping.fill(0xe4u); + ShaderRecompiler::RecompileRequest pixelRequest{{ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, {32u, 0u, {}, std::nullopt, pixelInfo, std::nullopt, {}}, 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())}}}; + Output.fill(Unwritten); + device->Draw(State(), draw, shaders); + device->WaitIdle(); + } + + void Place(std::uint32_t firstVertexFetch, std::span fetchedVertices, std::uint32_t firstInstanceFetch) { + VertexSlots.fill(Unwritten); + InstanceSlots.fill(Unwritten); + for (std::uint32_t vertex = 0; vertex < fetchedVertices.size(); ++vertex) VertexSlots[firstVertexFetch + fetchedVertices[vertex]] = vertex; + for (std::uint32_t instance = 0; instance < Instances; ++instance) InstanceSlots[firstInstanceFetch + instance] = instance * Vertices; + } + + std::string Check(const std::string& what, std::span rawVertices, std::uint32_t baseVertex, std::uint32_t startInstance, bool checkRaw) const { + std::string failures; + for (std::uint32_t instance = 0; instance < Instances; ++instance) { + for (std::uint32_t vertex = 0; vertex < Vertices; ++vertex) { + const auto slot = instance * Vertices + vertex; + const Expected expected{rawVertices[vertex], instance, rawVertices[vertex] + baseVertex, instance + startInstance}; + const auto* actual = Output.data() + slot * 4u; + const bool same = actual[2] == expected.vertexSum && actual[3] == expected.instanceSum && (!checkRaw || (actual[0] == expected.vertex && actual[1] == expected.instance)); + char line[320]; + if (checkRaw) std::snprintf(line, sizeof(line), "%s, vertex %u instance %u: v5 %u v8 %u, after the fetch adds %u %u; expected %u %u, %u %u\n", what.c_str(), vertex, instance, actual[0], actual[1], actual[2], actual[3], expected.vertex, expected.instance, expected.vertexSum, expected.instanceSum); + else std::snprintf(line, sizeof(line), "%s, vertex %u instance %u: after the fetch adds %u %u; expected %u %u\n", what.c_str(), vertex, instance, actual[2], actual[3], expected.vertexSum, expected.instanceSum); + std::cout << line; + if (!same) failures += line; + } + } + return failures; + } + + std::uint32_t* Records() const { + return records; + } + + std::shared_ptr device; + +private: + static constexpr std::size_t RecordBytes = 65536; + std::uint32_t* records = nullptr; +}; + +AgcDriver::DriverDetail::DrawProgram Program(const std::vector& userData) { + AgcDriver::DriverDetail::DrawProgram program{}; + program.firstUserSgpr = 8; + program.userData = userData; + return program; +} + +AgcDriver::Pm4::DrawParameters FoldedDraw(const ShaderRecompiler::RecompileResult& vertex, const std::vector& userData, AgcDriver::Pm4::DrawParameters draw) { + AgcDriver::DriverDetail::Driver::FoldDrawOffsets(vertex, Program(userData), draw); + return draw; +} + +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, 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'; + if (path == expectedPath && rules) return {}; + return what + ": classified to " + PathName(path) + ", expected " + PathName(expectedPath) + (expectedPath ? "" : expectedRule == Rule::InPlace ? " with both dimensions in place" : " with both dimensions constant") + "\n"; +} + +std::string Analysis(Fixture& fixture) { + struct Case { + const char* what; + std::span code; + bool vertex; + bool instance; + }; + 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}, + {"a program whose vertex add is a clamped v_sad_u32", ClampCode, true, false}, + {"a program that adds s16 twice", TwiceCode, true, false}, + {"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; + for (const auto& entry : cases) { + const auto result = fixture.Vertex(entry.code, userData); + char line[256]; + std::snprintf(line, sizeof(line), "%s: folded value observable outside the fetch add: vertex %d instance %d; expected %d %d\n", entry.what, result.vertexOffsetShared ? 1 : 0, result.instanceOffsetShared ? 1 : 0, entry.vertex ? 1 : 0, entry.instance ? 1 : 0); + std::cout << line; + if (result.vertexOffsetShared != entry.vertex || result.instanceOffsetShared != entry.instance) failures += line; + } + return failures; +} + +std::string Run(Fixture& fixture) { + std::string failures = Analysis(fixture); + const std::array autoIndices{0, 1, 2}; + const std::array bufferIndices{Indices[0], Indices[1], Indices[2]}; + const auto indexAddress = reinterpret_cast(Indices.data()); + + { + constexpr std::uint32_t indxOffset = 5; + constexpr std::uint32_t baseVertex = 1000; + constexpr std::uint32_t startInstance = 40; + const auto userData = fixture.UserData(baseVertex, startInstance); + const auto vertex = fixture.Vertex(VertexCode, userData); + const auto draw = FoldedDraw(vertex, userData, {0, Vertices, 0, Instances, 0, false, indxOffset, 0}); + fixture.Place(indxOffset + baseVertex, autoIndices, startInstance); + fixture.Draw(vertex, draw); + const std::array raw{indxOffset, indxOffset + 1, indxOffset + 2}; + failures += fixture.Check("direct auto draw, GE_INDX_OFFSET 5, s16 1000, s17 40", raw, baseVertex, startInstance, true); + } + { + constexpr std::uint32_t baseVertex = 1000; + constexpr std::uint32_t startInstance = 40; + const auto userData = fixture.UserData(baseVertex, startInstance); + const auto vertex = fixture.Vertex(VertexCode, userData); + const auto draw = FoldedDraw(vertex, userData, {indexAddress, Vertices, 2, Instances, 0, true, 0, 0}); + fixture.Place(baseVertex, bufferIndices, startInstance); + fixture.Draw(vertex, draw); + failures += fixture.Check("direct indexed draw, s16 1000, s17 40", bufferIndices, baseVertex, startInstance, true); + } + + const auto staleUserData = fixture.UserData(0x51u, 0x62u); + const std::array indexedRecord{Vertices, Instances, 0, 700, 59455}; + std::copy(indexedRecord.begin(), indexedRecord.end(), fixture.Records()); + AgcDriver::Pm4::DrawParameters::IndirectDraw indexedIndirect{reinterpret_cast(fixture.Records()), 0x25u, 20u, 20u, 1u, false, 0u, 0x10u, 0x11u, NoLocation, false, 0u}; + indexedIndirect.baseVertexSgpr = static_cast(BaseVertexSgpr); + indexedIndirect.startInstanceSgpr = static_cast(StartInstanceSgpr); + { + const std::string what = "indexed indirect record whose program copies v5/v8 before its fetch adds, vertex offset 700, start instance 59455"; + AgcDriver::Pm4::DrawParameters packet{indexAddress, static_cast(Indices.size()), 2, 0, 0, true, 0, 0}; + packet.indirect = indexedIndirect; + failures += Classified(fixture, what, fixture.Vertex(VertexCode, staleUserData), staleUserData, packet, Path::NotFolded, Rule::Constant); + const auto arguments = AgcDriver::Pm4::ReadDrawArguments(indexedIndirect, 0); + const auto userData = fixture.UserData(arguments.vertexOffset, arguments.firstInstance); + const auto vertex = fixture.Vertex(VertexCode, userData); + const auto draw = FoldedDraw(vertex, userData, {indexAddress + arguments.firstVertexOrIndex * 2u, arguments.count, 2, arguments.instances, 0, true, 0, 0}); + fixture.Place(arguments.vertexOffset, bufferIndices, arguments.firstInstance); + fixture.Draw(vertex, draw); + failures += fixture.Check(what + ", on the CPU path", bufferIndices, arguments.vertexOffset, arguments.firstInstance, true); + } + { + const std::string what = "auto indirect record whose program copies v5/v8 before its fetch adds, start vertex 300, start instance 7"; + const std::array record{Vertices, Instances, 300, 7}; + std::copy(record.begin(), record.end(), fixture.Records() + 16); + AgcDriver::Pm4::DrawParameters::IndirectDraw indirect{reinterpret_cast(fixture.Records() + 16), 0x24u, 16u, 16u, 1u, false, 0u, 0x10u, 0x11u, NoLocation, false, 0u}; + indirect.baseVertexSgpr = static_cast(BaseVertexSgpr); + indirect.startInstanceSgpr = static_cast(StartInstanceSgpr); + AgcDriver::Pm4::DrawParameters packet{0, 0, 0, 0, 0, false, 0, 0}; + packet.indirect = indirect; + failures += Classified(fixture, what, fixture.Vertex(VertexCode, staleUserData), staleUserData, packet, Path::NotFolded, Rule::Constant); + const auto arguments = AgcDriver::Pm4::ReadDrawArguments(indirect, 0); + const auto userData = fixture.UserData(arguments.firstVertexOrIndex, arguments.firstInstance); + const auto vertex = fixture.Vertex(VertexCode, userData); + const auto draw = FoldedDraw(vertex, userData, {0, arguments.count, 0, arguments.instances, 0, false, indirect.indxOffset, 0}); + fixture.Place(arguments.firstVertexOrIndex, autoIndices, arguments.firstInstance); + fixture.Draw(vertex, draw); + failures += fixture.Check(what + ", on the CPU path", autoIndices, arguments.firstVertexOrIndex, arguments.firstInstance, true); + } + if (fixture.device->DrawIndirectSupport().firstInstance) { + 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"; + const auto vertex = fixture.Vertex(VertexCode, staleUserData); + AgcDriver::Pm4::DrawParameters draw{indexAddress, static_cast(Indices.size()), 2, 0, 0, true, 0, 0}; + draw.indirect = indexedIndirect; + draw.indirect->baseVertexLocation = NoLocation; + draw.indirect->startInstanceLocation = NoLocation; + draw.indirect->baseVertexSgpr = -1; + draw.indirect->startInstanceSgpr = -1; + failures += Classified(fixture, what, vertex, staleUserData, draw, std::nullopt, Rule::Constant); + fixture.Place(0x51u, bufferIndices, 0x62u); + 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; +} + +} + +int main() { + try { + std::shared_ptr device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Fixture fixture(device); + const auto failures = Run(fixture); + if (!failures.empty()) { + std::cerr << "vertex and instance indices differ from the guest's:\n" << failures; + return 1; + } + std::cout << "draw offset index 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/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/DualSourceBlend.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DualSourceBlend.cpp new file mode 100644 index 0000000000..043d195484 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DualSourceBlend.cpp @@ -0,0 +1,353 @@ +#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 Float16x4Info = 0x0730u; +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 Destinations{{ + {0.5f, 0.25f, 0.75f, 0.5f}, {1.0f, 0.0f, 0.125f, 0.25f}, {0.0625f, 0.875f, 0.5f, 1.0f}, {0.25f, 0.5f, 0.375f, 0.75f} +}}; + +alignas(256) std::array, 8> Programs{}; +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((half & 0x8000u) == 0 && exponent != 31u, "a blended texel is negative, infinite or NaN"); + return exponent == 0 ? std::ldexp(static_cast(mantissa), -24) : std::ldexp(1.0f + static_cast(mantissa) / 1024.0f, static_cast(exponent) - 15); +} + +std::span Program(const Color& first, const Color& second, 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(first[0]) | (Half(first[1]) << 16u)); + move(5, Half(first[2]) | (Half(first[3]) << 16u)); + move(6, Half(second[0]) | (Half(second[1]) << 16u)); + move(7, Half(second[2]) | (Half(second[3]) << 16u)); + code[at++] = 0xf800040fu; + code[at++] = 0x00000504u; + code[at++] = 0xf8001c1fu; + code[at++] = 0x00000706u; + } else { + for (std::uint32_t channel = 0; channel < 4; ++channel) move(4 + channel, std::bit_cast(first[channel])); + for (std::uint32_t channel = 0; channel < 4; ++channel) move(8 + channel, std::bit_cast(second[channel])); + code[at++] = 0xf800000fu; + code[at++] = 0x07060504u; + code[at++] = 0xf800181fu; + code[at++] = 0x0b0a0908u; + } + 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) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Float16x4Info; + cx[0x31d] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = TiledAttrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == VK_FORMAT_R16G16B16A16_SFLOAT && color.elementBytes == 8 && color.bytes == block.bytes, "the 16_16_16_16 float target did not decode as a 256x128 RGBA16F surface"); + return color; +} + +VkPipelineColorBlendAttachmentState Blend(VkBlendFactor source, VkBlendFactor destination, VkBlendFactor sourceAlpha, VkBlendFactor destinationAlpha) { + VkPipelineColorBlendAttachmentState blend{}; + blend.blendEnable = VK_TRUE; + blend.srcColorBlendFactor = source; + blend.dstColorBlendFactor = destination; + blend.colorBlendOp = VK_BLEND_OP_ADD; + blend.srcAlphaBlendFactor = sourceAlpha; + blend.dstAlphaBlendFactor = destinationAlpha; + blend.alphaBlendOp = VK_BLEND_OP_ADD; + blend.colorWriteMask = 0xf; + return blend; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, const VkPipelineColorBlendAttachmentState& blend, 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; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + state.dualSourceBlend = true; + + const auto codeAddress = reinterpret_cast(pixelCode.data()); + const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, exportFormat | (exportFormat << 4u)}}; + 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->dualSourceBlend = state.dualSourceBlend; + 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 single = fragment; + single.context.pixel->dualSourceBlend = false; + const auto plain = materialize(single); + 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(), "dual-source blending 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 / 2u); + AgcDriver::GuestMemory::Read(block.Address(), std::as_writable_bytes(std::span(stored)), 1); + return stored; +} + +std::vector Fill(Block& block, const ColorTargetLayout& layout) { + std::vector halves(block.bytes / 2u); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto& color = Destinations[(x + y) % Destinations.size()]; + const auto index = layout.Offset(x, y) / 2u; + for (std::uint32_t channel = 0; channel < 4; ++channel) halves[index + channel] = Half(color[channel]); + } + } + std::memcpy(block.data, halves.data(), block.bytes); + return halves; +} + +void Check(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const std::vector& before, const std::function& blended, 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 auto expected = blended(Destinations[(x + y) % Destinations.size()]); + for (std::uint32_t channel = 0; channel < 4; ++channel) { + const bool inside = x < ScissorWidth; + const bool match = inside ? std::fabs(FromHalf(stored[index + channel]) - expected[channel]) <= 1.0f / 2048.0f : stored[index + channel] == before[index + channel]; + if (match) continue; + char detail[160]; + std::snprintf(detail, sizeof(detail), ": channel %u of pixel (%u, %u) %s the scissor holds half 0x%04x, expected %.6g", channel, x, y, inside ? "inside" : "outside", stored[index + channel], inside ? expected[channel] : FromHalf(before[index + channel])); + throw std::runtime_error(what + detail); + } + } + } +} + +struct BlendCase { + const char* name; + VkPipelineColorBlendAttachmentState blend; + std::function color; + std::function alpha; +}; + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!device->DualSrcBlend()) { + std::puts("skipped, the device has no dual-source blending"); + return VulkanTestSkipped; + } + const ColorTargetLayout layout(Width, Height, ColorTileMode::RenderTarget, 8); + Block block(layout.Bytes()); + const Color first{0.25f, 0.5f, 0.125f, 0.5f}; + const Color second{0.5f, 0.25f, 1.0f, 0.75f}; + const std::array cases{{ + {"ONE + SRC1_COLOR", Blend(VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_SRC1_COLOR, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_SRC1_COLOR), + [](float s, float t, float d, float) { return s + t * d; }, [](float s, float t, float d) { return s + t * d; }}, + {"SRC1_ALPHA + ONE_MINUS_SRC1_ALPHA", Blend(VK_BLEND_FACTOR_SRC1_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA, VK_BLEND_FACTOR_ZERO, VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA), + [](float s, float, float d, float a) { return s * a + d * (1.0f - a); }, [](float, float t, float d) { return d * (1.0f - t); }}, + {"ONE_MINUS_SRC1_COLOR + ZERO", Blend(VK_BLEND_FACTOR_ONE_MINUS_SRC1_COLOR, VK_BLEND_FACTOR_ZERO, VK_BLEND_FACTOR_SRC1_ALPHA, VK_BLEND_FACTOR_ZERO), + [](float s, float t, float, float) { return s * (1.0f - t); }, [](float s, float t, float) { return s * t; }}, + }}; + for (const bool compressed : {false, true}) { + for (const auto& test : cases) { + const auto before = Fill(block, layout); + const auto color = DecodeTarget(block); + Draw(*device, color, test.blend, compressed ? Float16Export : Float32Export, Program(first, second, compressed)); + Check(*device, block, layout, before, [&](const Color& destination) { + Color result{}; + for (std::uint32_t channel = 0; channel < 3; ++channel) result[channel] = test.color(first[channel], second[channel], destination[channel], second[3]); + result[3] = test.alpha(first[3], second[3], destination[3]); + return result; + }, std::string(test.name) + (compressed ? " with FP16_ABGR exports" : " with 32_ABGR exports")); + } + } + std::puts("dual-source 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/DwordPatternFill.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DwordPatternFill.cpp new file mode 100644 index 0000000000..f7a18fada0 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DwordPatternFill.cpp @@ -0,0 +1,189 @@ +#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::MatchDwordPatternFill; + +alignas(256) constexpr std::array Code{ + 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}; +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{}; + +std::vector UserData(void* output, std::uint32_t records, std::uint32_t count, std::uint32_t period, std::uint32_t format = 0x00014004u) { + const auto address = reinterpret_cast(output); + 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}; + const auto data = UserData(Output.data(), 1024, 256, 4); + const auto fill = MatchDwordPatternFill(packet, Code, data, compute); + Require(fill && fill->base == reinterpret_cast(Output.data()) && fill->bytes == 1024 && fill->pattern == Pattern, "the dword pattern-fill kernel was not recognized as a 16-byte pattern fill"); + for (const auto period : {1u, 2u}) { + const auto narrow = MatchDwordPatternFill(packet, Code, UserData(Output.data(), 1024, 256, period), compute); + Require(narrow && narrow->pattern[1] == Pattern[1 % period] && narrow->pattern[3] == Pattern[3 % period], "a shorter pattern period was not repeated over 16 bytes"); + } + for (std::size_t word = 0; word < Code.size(); ++word) { + auto altered = Code; + altered[word] ^= 1u; + Require(!MatchDwordPatternFill(packet, altered, data, compute), "a different shader matched the dword pattern-fill kernel"); + } + Require(!MatchDwordPatternFill(packet, std::span(Code).first(68), data, compute), "a truncated shader matched"); + Require(!MatchDwordPatternFill(packet, Code, UserData(Output.data(), 1024, 256, 3), compute), "a three-dword period matched a 16-byte pattern"); + Require(!MatchDwordPatternFill(packet, Code, UserData(Output.data(), 1024, 250, 4), compute), "a count of whole dwords not filling 16-byte units matched"); + Require(!MatchDwordPatternFill(packet, Code, UserData(Output.data() + 1, 1024, 256, 4), compute), "an unaligned destination matched"); + Require(!MatchDwordPatternFill(packet, Code, UserData(Output.data(), 1024, 256, 4, 0x00814004u), compute), "an add-tid descriptor matched"); + for (const std::uint32_t format : {0x00015004u, 0x00016004u}) { + Require(MatchDwordPatternFill(packet, Code, UserData(Output.data(), 1024, 256, 4, format), compute).has_value(), "a 32-bit output format was refused"); + } + for (const std::uint32_t format : {0x00005004u, 0x0000b004u, 0x0000d004u, 0x0001b004u, 0x00027facu, 0x0004b004u}) { + Require(!MatchDwordPatternFill(packet, Code, UserData(Output.data(), 1024, 256, 4, format), compute), "an output format whose elements are not one 32-bit value matched"); + } + auto extra = data; + extra.push_back(0); + Require(!MatchDwordPatternFill(packet, Code, extra, compute), "a different user SGPR count matched (the group id moves)"); + auto partial = packet; + partial[4] |= 0x20u; + Require(!MatchDwordPatternFill(partial, Code, data, compute), "a partial dispatch matched"); + auto noGroup = compute; + noGroup.groupIdEnable[0] = false; + Require(!MatchDwordPatternFill(packet, Code, data, noGroup), "a dispatch without the group id matched"); + const auto clamped = MatchDwordPatternFill(packet, Code, UserData(Output.data(), 64, 256, 4), compute); + Require(clamped && clamped->bytes == 256, "the fill exceeded the descriptor's records"); +} + +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, count, period; + bool partial; + std::uint32_t threads; + }; + constexpr std::array cases{{ + {4, 1024, 256, 4, false, 0}, + {4, 1024, 256, 2, false, 0}, + {4, 1024, 256, 1, false, 0}, + {16, 1024, 1024, 4, false, 0}, + {4, 1024, 100, 4, false, 0}, + {4, 1024, 250, 4, false, 0}, + {4, 64, 256, 4, false, 0}, + {4, 1024, 256, 3, false, 0}, + {4, 1024, 256, 5, false, 0}, + {0, 1024, 256, 4, true, 70}, + }}; + 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)); + } + } + } +} + +} + +int main() { + try { + if (std::getenv("APS5_NO_FILL_HLE") == nullptr) { + MatchTests(); + SwappedComparesTests(); + } + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + ExecuteTests(); + AgcDriverShutdown_nid_postfix(); + std::puts("dword pattern 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/EmulatedColorCompare.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp index 54514f2102..1d96a16670 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -24,10 +25,19 @@ using ShaderRecompiler::ShaderStage; constexpr std::uint32_t Threads = 32; constexpr std::uint32_t Side = 2; constexpr std::uint32_t Format8888UNorm = 56; +constexpr std::uint32_t Format8UNorm = 1; +constexpr std::uint32_t Format8_8UNorm = 14; +constexpr std::uint32_t Format1616UNorm = 23; +constexpr std::uint32_t Format9995Float = 132; +constexpr std::uint32_t Format8Srgb = 128; +constexpr std::uint32_t Format8_8Srgb = 129; +constexpr std::uint32_t Format8888Srgb = 130; constexpr std::uint32_t Format8888UInt = 60; constexpr std::uint32_t Format32Float = 22; constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t TypeCube = 11; constexpr std::uint32_t LessEqual = 3; +constexpr std::uint32_t GreaterEqual = 6; constexpr std::uint32_t ClampWrap = 0; constexpr std::uint32_t ClampMirror = 1; constexpr std::uint32_t ClampEdge = 2; @@ -38,12 +48,15 @@ constexpr std::uint32_t BorderWhite = 2; constexpr std::uint32_t BorderTable = 3; constexpr std::uint32_t FilterPoint = 0; constexpr std::uint32_t FilterBilinear = 1; +constexpr std::uint32_t FilterAnisoPoint = 2; constexpr std::uint32_t FilterAnisoBilinear = 3; constexpr std::uint32_t ReductionMin = 1; constexpr std::array Red{0, 64, 128, 255}; alignas(256) std::array Input{}; -alignas(256) std::array Output{}; +alignas(256) std::array CubeInput{}; +alignas(256) std::array Output{}; +std::uint32_t Swizzle = 0xfacu; alignas(256) std::array Extra{}; alignas(4096) std::array Texels{}; @@ -52,17 +65,37 @@ alignas(256) constexpr std::array Code{ 0x34160082, 0xe0701000, 0x8001040b, 0xbf810000, 0xbf810000, 0xbf810000, }; +alignas(256) constexpr std::array ArrayCode{ + 0x1614008c, 0xe03c1000, 0x8000010a, 0xbf8c3f70, 0xf0bc0128, 0x00820401, 0xbf8c3f70, + 0x34160082, 0xe0701000, 0x8001040b, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array CubeCode{ + 0x16140090, 0xe0381000, 0x8000010a, 0xbf8c3f70, 0xf0bc0118, 0x00820401, 0xbf8c3f70, + 0x34160082, 0xe0701000, 0x8001040b, 0xbf810000, 0xbf810000, 0xbf810000, +}; + alignas(256) constexpr std::array OffsetCode{ 0x1614008c, 0xe03c1000, 0x8000030a, 0xe03c1000, 0x8005060a, 0xbf8c3f70, 0x7e020306, 0x7e040307, 0x7e0c0308, 0xf0f80108, 0x00820901, 0xbf8c3f70, 0x34160082, 0xe0701000, 0x8001090b, 0xbf810000, }; +alignas(256) constexpr std::array QuadCode{ + 0x1614008c, 0xe03c1000, 0x8000010a, 0x7e0802f3, 0x7e0a02f3, 0x7e0c02f3, 0x7e0e02f3, 0xbf8c3f70, 0xf0bc0f08, 0x00820401, 0xbf8c3f70, 0x34160084, 0xe0781000, 0x8001040b, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array PairCode{ + 0x1614008c, 0xe03c1000, 0x8000010a, 0x7e0802f3, 0x7e0a02f3, 0x7e0c02f3, 0x7e0e02f3, 0xbf8c3f70, 0xf0bc0608, 0x00820401, 0xbf8c3f70, 0x34160084, 0xe0781000, 0x8001040b, 0xbf810000, 0xbf810000, 0xbf810000, +}; + struct Sampler { std::uint32_t clamp; std::uint32_t filter; std::uint32_t border = BorderBlack; std::uint32_t reduction = 0; std::uint32_t lodBias = 0; + bool disableCubeWrap = false; + std::uint32_t function = LessEqual; }; std::array BufferDescriptor(const void* data, std::uint32_t bytes) { @@ -70,20 +103,20 @@ std::array BufferDescriptor(const void* data, std::uint32_t by return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; } -std::array TextureDescriptor(std::uint32_t format) { +std::array TextureDescriptor(std::uint32_t format, bool cube = false) { const auto address = static_cast(reinterpret_cast(Texels.data())); return { static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((Side - 1u) & 3u) << 30u), ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), - 0xfacu | (Type2D << 28u), - 0u, 0u, 0u, 0u, + Swizzle | ((cube ? TypeCube : Type2D) << 28u), + cube ? 5u : 0u, 0u, 0u, 0u, }; } std::array SamplerDescriptor(const Sampler& sampler) { return { - sampler.clamp | (sampler.clamp << 3u) | (ClampEdge << 6u) | (LessEqual << 12u) | (sampler.reduction << 29u), + sampler.clamp | (sampler.clamp << 3u) | (ClampEdge << 6u) | (sampler.function << 12u) | (sampler.reduction << 29u) | (sampler.disableCubeWrap ? (1u << 28u) : 0u), 0u, sampler.lodBias | (sampler.filter << 20u) | (sampler.filter << 22u), sampler.border << 30u, @@ -92,20 +125,38 @@ std::array SamplerDescriptor(const Sampler& sampler) { void FillTexels() { Texels.fill(0); - const auto descriptor = TextureDescriptor(Format8888UNorm); + const auto descriptor = TextureDescriptor(Format1616UNorm); const auto surface = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(descriptor)); const auto& mip = surface.mips.at(0); for (std::uint32_t y = 0; y < Side; ++y) { for (std::uint32_t x = 0; x < Side; ++x) { auto* texel = Texels.data() + mip.tiledOffset + y * mip.pitchBytes + x * 4u; texel[0] = Red[y * Side + x]; - texel[1] = 0x11; + texel[1] = Red[y * Side + x]; texel[2] = 0x22; texel[3] = 0xff; } } } +void FillCubeTexels() { + Texels.fill(0); + const auto descriptor = TextureDescriptor(Format1616UNorm, true); + const auto surface = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(descriptor)); + Require(surface.layers == 6u && surface.guestBytes <= Texels.size(), "cube descriptor did not expose six faces within texture storage"); + const auto& mip = surface.mips.at(0); + for (std::uint32_t face = 0; face < 6u; ++face) { + for (std::uint32_t y = 0; y < Side; ++y) { + for (std::uint32_t x = 0; x < Side; ++x) { + auto* texel = Texels.data() + surface.GuestLayerOffset(face) + mip.tiledOffset + y * mip.pitchBytes + x * 4u; + texel[0] = static_cast(face == 0u ? 255u : 0u); + texel[1] = texel[0]; + texel[3] = 0xffu; + } + } + } +} + void FillInput() { for (std::uint32_t tid = 0; tid < Threads; ++tid) { Input[tid * 3u + 0u] = -0.6f + 0.07f * static_cast(tid); @@ -166,11 +217,11 @@ float Expected(std::uint32_t tid, const Sampler& sampler, bool offsets) { 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) { +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 = BufferDescriptor(Input.data(), static_cast(sizeof(Input))); + const auto input = cube ? BufferDescriptor(CubeInput.data(), static_cast(sizeof(CubeInput))) : BufferDescriptor(Input.data(), static_cast(sizeof(Input))); const auto output = BufferDescriptor(Output.data(), static_cast(sizeof(Output))); - const auto texture = TextureDescriptor(format); + const auto texture = TextureDescriptor(format, cube); const auto samplerWords = SamplerDescriptor(sampler); std::copy(input.begin(), input.end(), userData.begin()); std::copy(output.begin(), output.end(), userData.begin() + 4); @@ -191,10 +242,11 @@ ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std:: return ShaderRecompiler::Recompile(request); } -void Run(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char* name, bool offsets = false, bool useCache = false) { +std::uint64_t Run(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char* name, bool offsets = false, bool useCache = false, bool array = false, std::uint64_t expectedVariant = 0) { Output.fill(-1.0f); - const std::span code = offsets ? std::span(OffsetCode) : std::span(Code); - const auto result = Compile(device, Format8888UNorm, sampler, code, useCache); + const std::span code = offsets ? std::span(OffsetCode) : array ? std::span(ArrayCode) : std::span(Code); + const auto result = Compile(device, Format1616UNorm, sampler, code, useCache); + if (expectedVariant != 0) Require(result.cacheHit && result.variantId == expectedVariant, std::string(name) + " did not reuse the compiled comparison artifact"); for (const auto& binding : result.bindings) { Require(binding.role != ShaderRecompiler::DescriptorRole::GuestSamplers, "emulated comparison retained a sampler binding"); Require(std::all_of(binding.imageSamplers.begin(), binding.imageSamplers.end(), [](auto mask) { return mask == 0u; }), "emulated comparison retained an unused sampler association"); @@ -206,6 +258,54 @@ void Run(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char* na 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)); } + return result.variantId; +} + +constexpr std::uint32_t Select(std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint32_t w) { + return x | (y << 3u) | (z << 6u) | (w << 9u); +} + +void RunChannels(AgcDriver::VulkanDevice& device, std::span code, std::array channels, std::uint32_t registers, std::uint32_t firstChannel, const char* name) { + const Sampler sampler{ClampEdge, FilterBilinear}; + Swizzle = Select(channels[0], channels[1], channels[2], channels[3]); + Output.fill(-2.0f); + const auto result = Compile(device, Format1616UNorm, sampler, code); + Swizzle = 0xfacu; + 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; + } + 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)); + } + } +} + +void RunCube(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char* name) { + FillCubeTexels(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + CubeInput[tid * 4u + 0u] = 0.5f; + CubeInput[tid * 4u + 1u] = 1.0f; + CubeInput[tid * 4u + 2u] = 0.0f; + CubeInput[tid * 4u + 3u] = 0.0f; + } + Output.fill(-1.0f); + const auto result = Compile(device, Format1616UNorm, sampler, CubeCode, false, true, true); + for (const auto& binding : result.bindings) { + Require(binding.role != ShaderRecompiler::DescriptorRole::GuestSamplers, "cube emulated comparison retained a sampler binding"); + Require(std::all_of(binding.imageSamplers.begin(), binding.imageSamplers.end(), [](auto mask) { return mask == 0u; }), "cube emulated comparison retained an unused sampler association"); + } + 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"); + } } bool BindsDepthCompare(const ShaderRecompiler::RecompileResult& result) { @@ -214,9 +314,39 @@ bool BindsDepthCompare(const ShaderRecompiler::RecompileResult& result) { }); } -void Reject(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, std::string_view reason, std::span code = Code, bool nativeSampleOffsets = true) { +void RunSrgbCubeZero(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; + CubeInput[tid * 4u + 1u] = 1.0f; + CubeInput[tid * 4u + 2u] = 0.0f; + CubeInput[tid * 4u + 3u] = 0.0f; + } + Output.fill(-1.0f); + const auto result = Compile(device, format, sampler, CubeCode, false, true, true); + Require(!BindsDepthCompare(result), std::string(name) + " kept a depth-compare binding"); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(CubeCode.data())); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Require(Output[tid] == 0.0f, std::string(name) + ": thread " + std::to_string(tid) + " compared to " + std::to_string(Output[tid]) + ", expected 0"); + } +} + +void RunSrgbZero(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"); + Require(!BindsDepthCompare(result), std::string(name) + " kept a depth-compare binding"); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(Code.data())); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Require(Output[tid] == 0.0f, std::string(name) + ": thread " + std::to_string(tid) + " compared to " + std::to_string(Output[tid]) + ", expected 0"); + } +} + +void Reject(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, std::string_view reason, std::span code = Code, bool nativeSampleOffsets = true, bool cube = false) { try { - static_cast(Compile(device, format, sampler, code, false, nativeSampleOffsets)); + static_cast(Compile(device, format, sampler, code, false, nativeSampleOffsets, cube)); } catch (const std::exception& error) { Require(std::string_view(error.what()).find(reason) != std::string_view::npos, std::string("unexpected rejection: ") + error.what()); return; @@ -230,11 +360,57 @@ 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); + 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()); + } + 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"); + 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"); + for (std::uint32_t function = 0; function < 8u; ++function) { + const auto name = "bilinear 8_8_8_8 unorm cube function " + std::to_string(function); + RunSrgbCubeZero(*device, Format8888UNorm, {ClampEdge, FilterBilinear, BorderBlack, 0u, 0u, false, function}, name.c_str()); + } + RunSrgbCubeZero(*device, Format8888UNorm, {ClampEdge, FilterAnisoBilinear, BorderBlack, 0u, 0u, false, GreaterEqual}, "anisotropic bilinear 8_8_8_8 unorm cube without DISABLE_CUBE_WRAP"); + Reject(*device, Format1616UNorm, {ClampEdge, FilterAnisoBilinear}, "point or bilinear"); + Reject(*device, Format1616UNorm, {ClampEdge, FilterAnisoPoint}, "point or bilinear"); + FillTexels(); Require(BindsDepthCompare(Compile(*device, Format32Float, {ClampEdge, FilterBilinear})), "an R32 float texture left the native comparison path"); Require(!BindsDepthCompare(Compile(*device, Format8888UNorm, {ClampEdge, FilterBilinear})), "a color texture kept a depth-compare binding"); + 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"); + for (const auto format : {Format8Srgb, Format8_8Srgb, Format8888Srgb}) { + 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); + } + } + } + if (useCache) Run(*device, {ClampEdge, FilterPoint}, "cached UNORM after sRGB", false, true, false, variant); + } + for (const auto format : {Format8UNorm, Format8_8UNorm, Format8888UNorm}) { + for (std::uint32_t function = 0; function < 8u; ++function) { + for (const auto filter : {FilterPoint, FilterBilinear, FilterAnisoBilinear}) { + if (format != Format8888UNorm && function != LessEqual) continue; + const auto name = "8-bit unorm format " + std::to_string(format) + " function " + std::to_string(function) + " filter " + std::to_string(filter); + RunSrgbZero(*device, format, {ClampEdge, filter, BorderBlack, 0u, 0u, false, function}, name.c_str(), false, 0u); + } + } + } Run(*device, {ClampEdge, FilterPoint}, "point, clamp to edge"); + Run(*device, {ClampWrap, FilterPoint}, "point, wrap"); + 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"); Run(*device, {ClampBorder, FilterPoint, BorderWhite}, "point, white border"); @@ -242,21 +418,35 @@ 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"); + RunChannels(*device, QuadCode, {0, 0, 0, 0}, 4, 0, "view 0,0,0,0"); + RunChannels(*device, QuadCode, {1, 1, 1, 1}, 4, 0, "view 1,1,1,1"); + RunChannels(*device, QuadCode, {0, 4, 1, 5}, 4, 0, "view 0,R,1,G"); + RunChannels(*device, QuadCode, {4, 0, 0, 1}, 4, 0, "view R,0,0,1"); + RunChannels(*device, QuadCode, {5, 4, 5, 6}, 4, 0, "view G,R,B,A"); + RunChannels(*device, QuadCode, {7, 4, 4, 4}, 4, 0, "view A,R,R,R"); + RunChannels(*device, PairCode, {0, 4, 4, 4}, 2, 1, "view 0,R,R,R, dmask 6"); + Swizzle = Select(4, 2, 4, 4); + Reject(*device, Format8888UNorm, {ClampEdge, FilterPoint}, "every view channel is a constant or a color channel"); + Swizzle = Select(5, 5, 5, 5); + Reject(*device, Format8888UNorm, {ClampEdge, FilterPoint}, "a view channel selects red"); + Swizzle = 0xfacu; Reject(*device, Format8888UInt, {ClampEdge, FilterPoint}, "unsupported format"); Reject(*device, Format8888UNorm, {ClampMirror, FilterPoint}, "wrap, clamp-to-edge or clamp-to-border"); 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, FilterAnisoBilinear}, "point or bilinear"); 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/ExecHostLanes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ExecHostLanes.cpp index 1978891b9f..2eb9752be0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ExecHostLanes.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ExecHostLanes.cpp @@ -99,13 +99,13 @@ void Draw(AgcDriver::VulkanDevice& device) { {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, {64, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, device.Target(), - {0, 0, vertexPush, 128 - vertexPush} + PixelPushLayout(vertexPush, device.Target()) }; fragment.useCache = false; const auto pixelResult = ShaderRecompiler::Recompile(fragment); const std::array shaders{{ {ShaderStage::Vertex, &vertexResult, 0}, - {ShaderStage::Fragment, &pixelResult, vertexPush} + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, device.Target())} }}; AgcDriver::Graphics::State state{}; 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/F16DenormalFlush.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F16DenormalFlush.cpp new file mode 100644 index 0000000000..60e0f3fac2 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F16DenormalFlush.cpp @@ -0,0 +1,353 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 1; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +struct alignas(256) Shader { + std::array words; +}; + +struct Vector { + std::uint32_t half; + std::uint32_t expected[4]; +}; + +struct Case { + const char* name; + std::uint32_t word; + std::span rows; +}; + +constexpr Vector CeilRows[] = { + {0x0001u, {0x5a5a0000u, 0x5a5a3c00u, 0x5a5a0000u, 0x5a5a3c00u}}, + {0x0200u, {0x5a5a0000u, 0x5a5a3c00u, 0x5a5a0000u, 0x5a5a3c00u}}, + {0x03ffu, {0x5a5a0000u, 0x5a5a3c00u, 0x5a5a0000u, 0x5a5a3c00u}}, + {0x8001u, {0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u}}, + {0x8200u, {0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u}}, + {0x83ffu, {0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u}}, + {0x0400u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0x8400u, {0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u}}, + {0x0000u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x3c00u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0xbc00u, {0x5a5abc00u, 0x5a5abc00u, 0x5a5abc00u, 0x5a5abc00u}}, + {0x3800u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, +}; + +constexpr Vector CvtF32Rows[] = { + {0x0001u, {0x00000000u, 0x33800000u, 0x00000000u, 0x33800000u}}, + {0x0200u, {0x00000000u, 0x38000000u, 0x00000000u, 0x38000000u}}, + {0x03ffu, {0x00000000u, 0x387fc000u, 0x00000000u, 0x387fc000u}}, + {0x8001u, {0x80000000u, 0xb3800000u, 0x80000000u, 0xb3800000u}}, + {0x8200u, {0x80000000u, 0xb8000000u, 0x80000000u, 0xb8000000u}}, + {0x83ffu, {0x80000000u, 0xb87fc000u, 0x80000000u, 0xb87fc000u}}, + {0x0400u, {0x38800000u, 0x38800000u, 0x38800000u, 0x38800000u}}, + {0x8400u, {0xb8800000u, 0xb8800000u, 0xb8800000u, 0xb8800000u}}, + {0x0000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x3c00u, {0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u}}, + {0xbc00u, {0xbf800000u, 0xbf800000u, 0xbf800000u, 0xbf800000u}}, + {0x3800u, {0x3f000000u, 0x3f000000u, 0x3f000000u, 0x3f000000u}}, +}; + +constexpr Vector CvtNormI16Rows[] = { + {0x0001u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x0200u, {0x5a5a0000u, 0x5a5a0001u, 0x5a5a0000u, 0x5a5a0001u}}, + {0x03ffu, {0x5a5a0000u, 0x5a5a0002u, 0x5a5a0000u, 0x5a5a0002u}}, + {0x8001u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x8200u, {0x5a5a0000u, 0x5a5affffu, 0x5a5a0000u, 0x5a5affffu}}, + {0x83ffu, {0x5a5a0000u, 0x5a5afffeu, 0x5a5a0000u, 0x5a5afffeu}}, + {0x0400u, {0x5a5a0002u, 0x5a5a0002u, 0x5a5a0002u, 0x5a5a0002u}}, + {0x8400u, {0x5a5afffeu, 0x5a5afffeu, 0x5a5afffeu, 0x5a5afffeu}}, + {0x0000u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x3c00u, {0x5a5a7fffu, 0x5a5a7fffu, 0x5a5a7fffu, 0x5a5a7fffu}}, + {0xbc00u, {0x5a5a8001u, 0x5a5a8001u, 0x5a5a8001u, 0x5a5a8001u}}, + {0x3800u, {0x5a5a4000u, 0x5a5a4000u, 0x5a5a4000u, 0x5a5a4000u}}, + {0xb800u, {0x5a5ac000u, 0x5a5ac000u, 0x5a5ac000u, 0x5a5ac000u}}, +}; + +constexpr Vector CvtNormU16Rows[] = { + {0x0001u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x0200u, {0x5a5a0000u, 0x5a5a0002u, 0x5a5a0000u, 0x5a5a0002u}}, + {0x03ffu, {0x5a5a0000u, 0x5a5a0004u, 0x5a5a0000u, 0x5a5a0004u}}, + {0x8001u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x8200u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x83ffu, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x0400u, {0x5a5a0004u, 0x5a5a0004u, 0x5a5a0004u, 0x5a5a0004u}}, + {0x8400u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x0000u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x3c00u, {0x5a5affffu, 0x5a5affffu, 0x5a5affffu, 0x5a5affffu}}, + {0xbc00u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x3800u, {0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u}}, + {0xb800u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, +}; + +constexpr Vector ExpRows[] = { + {0x0001u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0x0200u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0x03ffu, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0x8001u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0x8200u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0x83ffu, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0x0400u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0x8400u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0x0000u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0x3c00u, {0x5a5a4000u, 0x5a5a4000u, 0x5a5a4000u, 0x5a5a4000u}}, + {0xbc00u, {0x5a5a3800u, 0x5a5a3800u, 0x5a5a3800u, 0x5a5a3800u}}, + {0xcb01u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a03fau, 0x5a5a03fau}}, + {0xcca0u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a002du, 0x5a5a002du}}, + {0xce3cu, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0001u, 0x5a5a0001u}}, + {0xc800u, {0x5a5a1c00u, 0x5a5a1c00u, 0x5a5a1c00u, 0x5a5a1c00u}}, + {0xc400u, {0x5a5a2c00u, 0x5a5a2c00u, 0x5a5a2c00u, 0x5a5a2c00u}}, +}; + +constexpr Vector FloorRows[] = { + {0x0001u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x0200u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x03ffu, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x8001u, {0x5a5a8000u, 0x5a5abc00u, 0x5a5a8000u, 0x5a5abc00u}}, + {0x8200u, {0x5a5a8000u, 0x5a5abc00u, 0x5a5a8000u, 0x5a5abc00u}}, + {0x83ffu, {0x5a5a8000u, 0x5a5abc00u, 0x5a5a8000u, 0x5a5abc00u}}, + {0x0400u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x8400u, {0x5a5abc00u, 0x5a5abc00u, 0x5a5abc00u, 0x5a5abc00u}}, + {0x0000u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x3c00u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0xbc00u, {0x5a5abc00u, 0x5a5abc00u, 0x5a5abc00u, 0x5a5abc00u}}, + {0xb800u, {0x5a5abc00u, 0x5a5abc00u, 0x5a5abc00u, 0x5a5abc00u}}, +}; + +constexpr Vector FractRows[] = { + {0x0001u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0001u}}, + {0x0200u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0200u}}, + {0x03ffu, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a03ffu}}, + {0x8001u, {0x5a5a0000u, 0x5a5a3bffu, 0x5a5a0000u, 0x5a5a3bffu}}, + {0x8200u, {0x5a5a0000u, 0x5a5a3bffu, 0x5a5a0000u, 0x5a5a3bffu}}, + {0x83ffu, {0x5a5a0000u, 0x5a5a3bffu, 0x5a5a0000u, 0x5a5a3bffu}}, + {0x0400u, {0x5a5a0400u, 0x5a5a0400u, 0x5a5a0400u, 0x5a5a0400u}}, + {0x8400u, {0x5a5a3bffu, 0x5a5a3bffu, 0x5a5a3bffu, 0x5a5a3bffu}}, + {0x0000u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x3c00u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0xbc00u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0xb800u, {0x5a5a3800u, 0x5a5a3800u, 0x5a5a3800u, 0x5a5a3800u}}, + {0x4900u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, +}; + +constexpr Vector FrexpExpRows[] = { + {0x0001u, {0x5a5a0000u, 0x5a5affe9u, 0x5a5a0000u, 0x5a5affe9u}}, + {0x0200u, {0x5a5a0000u, 0x5a5afff2u, 0x5a5a0000u, 0x5a5afff2u}}, + {0x03ffu, {0x5a5a0000u, 0x5a5afff2u, 0x5a5a0000u, 0x5a5afff2u}}, + {0x8001u, {0x5a5a0000u, 0x5a5affe9u, 0x5a5a0000u, 0x5a5affe9u}}, + {0x8200u, {0x5a5a0000u, 0x5a5afff2u, 0x5a5a0000u, 0x5a5afff2u}}, + {0x83ffu, {0x5a5a0000u, 0x5a5afff2u, 0x5a5a0000u, 0x5a5afff2u}}, + {0x0400u, {0x5a5afff3u, 0x5a5afff3u, 0x5a5afff3u, 0x5a5afff3u}}, + {0x8400u, {0x5a5afff3u, 0x5a5afff3u, 0x5a5afff3u, 0x5a5afff3u}}, + {0x0000u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x3c00u, {0x5a5a0001u, 0x5a5a0001u, 0x5a5a0001u, 0x5a5a0001u}}, + {0xbc00u, {0x5a5a0001u, 0x5a5a0001u, 0x5a5a0001u, 0x5a5a0001u}}, + {0x4900u, {0x5a5a0004u, 0x5a5a0004u, 0x5a5a0004u, 0x5a5a0004u}}, +}; + +constexpr Vector FrexpMantRows[] = { + {0x0001u, {0x5a5a0000u, 0x5a5a3800u, 0x5a5a0000u, 0x5a5a3800u}}, + {0x0200u, {0x5a5a0000u, 0x5a5a3800u, 0x5a5a0000u, 0x5a5a3800u}}, + {0x03ffu, {0x5a5a0000u, 0x5a5a3bfeu, 0x5a5a0000u, 0x5a5a3bfeu}}, + {0x8001u, {0x5a5a8000u, 0x5a5ab800u, 0x5a5a8000u, 0x5a5ab800u}}, + {0x8200u, {0x5a5a8000u, 0x5a5ab800u, 0x5a5a8000u, 0x5a5ab800u}}, + {0x83ffu, {0x5a5a8000u, 0x5a5abbfeu, 0x5a5a8000u, 0x5a5abbfeu}}, + {0x0400u, {0x5a5a3800u, 0x5a5a3800u, 0x5a5a3800u, 0x5a5a3800u}}, + {0x8400u, {0x5a5ab800u, 0x5a5ab800u, 0x5a5ab800u, 0x5a5ab800u}}, + {0x0000u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x3c00u, {0x5a5a3800u, 0x5a5a3800u, 0x5a5a3800u, 0x5a5a3800u}}, + {0xbc00u, {0x5a5ab800u, 0x5a5ab800u, 0x5a5ab800u, 0x5a5ab800u}}, + {0x4900u, {0x5a5a3900u, 0x5a5a3900u, 0x5a5a3900u, 0x5a5a3900u}}, +}; + +constexpr Vector LogRows[] = { + {0x0001u, {0x5a5afc00u, 0x5a5ace00u, 0x5a5afc00u, 0x5a5ace00u}}, + {0x0200u, {0x5a5afc00u, 0x5a5acb80u, 0x5a5afc00u, 0x5a5acb80u}}, + {0x03ffu, {0x5a5afc00u, 0x5a5acb00u, 0x5a5afc00u, 0x5a5acb00u}}, + {0x8001u, {0x5a5afc00u, 0x5a5afe00u, 0x5a5afc00u, 0x5a5afe00u}}, + {0x8200u, {0x5a5afc00u, 0x5a5afe00u, 0x5a5afc00u, 0x5a5afe00u}}, + {0x83ffu, {0x5a5afc00u, 0x5a5afe00u, 0x5a5afc00u, 0x5a5afe00u}}, + {0x0400u, {0x5a5acb00u, 0x5a5acb00u, 0x5a5acb00u, 0x5a5acb00u}}, + {0x8400u, {0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u}}, + {0x0000u, {0x5a5afc00u, 0x5a5afc00u, 0x5a5afc00u, 0x5a5afc00u}}, + {0x3c00u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0xbc00u, {0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u}}, + {0x4900u, {0x5a5a42a5u, 0x5a5a42a5u, 0x5a5a42a5u, 0x5a5a42a5u}}, + {0x0801u, {0x5a5aca80u, 0x5a5aca80u, 0x5a5aca80u, 0x5a5aca80u}}, +}; + +constexpr Vector RcpRows[] = { + {0x0001u, {0x5a5a7c00u, 0x5a5a7c00u, 0x5a5a7c00u, 0x5a5a7c00u}}, + {0x0200u, {0x5a5a7c00u, 0x5a5a7800u, 0x5a5a7c00u, 0x5a5a7800u}}, + {0x03ffu, {0x5a5a7c00u, 0x5a5a7401u, 0x5a5a7c00u, 0x5a5a7401u}}, + {0x8001u, {0x5a5afc00u, 0x5a5afc00u, 0x5a5afc00u, 0x5a5afc00u}}, + {0x8200u, {0x5a5afc00u, 0x5a5af800u, 0x5a5afc00u, 0x5a5af800u}}, + {0x83ffu, {0x5a5afc00u, 0x5a5af401u, 0x5a5afc00u, 0x5a5af401u}}, + {0x0400u, {0x5a5a7400u, 0x5a5a7400u, 0x5a5a7400u, 0x5a5a7400u}}, + {0x8400u, {0x5a5af400u, 0x5a5af400u, 0x5a5af400u, 0x5a5af400u}}, + {0x0000u, {0x5a5a7c00u, 0x5a5a7c00u, 0x5a5a7c00u, 0x5a5a7c00u}}, + {0x3c00u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0xbc00u, {0x5a5abc00u, 0x5a5abc00u, 0x5a5abc00u, 0x5a5abc00u}}, + {0x7bffu, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0100u, 0x5a5a0100u}}, + {0x7401u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a03ffu, 0x5a5a03ffu}}, + {0xfbffu, {0x5a5a8000u, 0x5a5a8000u, 0x5a5a8100u, 0x5a5a8100u}}, + {0xf404u, {0x5a5a8000u, 0x5a5a8000u, 0x5a5a83fcu, 0x5a5a83fcu}}, + {0x7800u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0200u, 0x5a5a0200u}}, +}; + +constexpr Vector RsqRows[] = { + {0x0001u, {0x5a5a7c00u, 0x5a5a6c00u, 0x5a5a7c00u, 0x5a5a6c00u}}, + {0x0200u, {0x5a5a7c00u, 0x5a5a59a8u, 0x5a5a7c00u, 0x5a5a59a8u}}, + {0x03ffu, {0x5a5a7c00u, 0x5a5a5801u, 0x5a5a7c00u, 0x5a5a5801u}}, + {0x8001u, {0x5a5afc00u, 0x5a5afe00u, 0x5a5afc00u, 0x5a5afe00u}}, + {0x8200u, {0x5a5afc00u, 0x5a5afe00u, 0x5a5afc00u, 0x5a5afe00u}}, + {0x83ffu, {0x5a5afc00u, 0x5a5afe00u, 0x5a5afc00u, 0x5a5afe00u}}, + {0x0400u, {0x5a5a5800u, 0x5a5a5800u, 0x5a5a5800u, 0x5a5a5800u}}, + {0x8400u, {0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u}}, + {0x0000u, {0x5a5a7c00u, 0x5a5a7c00u, 0x5a5a7c00u, 0x5a5a7c00u}}, + {0x3c00u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0xbc00u, {0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u}}, + {0x4400u, {0x5a5a3800u, 0x5a5a3800u, 0x5a5a3800u, 0x5a5a3800u}}, +}; + +constexpr Vector SinRows[] = { + {0x0001u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0006u}}, + {0x0200u, {0x5a5a0000u, 0x5a5a0a48u, 0x5a5a0000u, 0x5a5a0a48u}}, + {0x03ffu, {0x5a5a0000u, 0x5a5a0e47u, 0x5a5a0000u, 0x5a5a0e47u}}, + {0x8001u, {0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u, 0x5a5a8006u}}, + {0x8200u, {0x5a5a8000u, 0x5a5a8a48u, 0x5a5a8000u, 0x5a5a8a48u}}, + {0x83ffu, {0x5a5a8000u, 0x5a5a8e47u, 0x5a5a8000u, 0x5a5a8e47u}}, + {0x0400u, {0x5a5a0e48u, 0x5a5a0e48u, 0x5a5a0e48u, 0x5a5a0e48u}}, + {0x8400u, {0x5a5a8e48u, 0x5a5a8e48u, 0x5a5a8e48u, 0x5a5a8e48u}}, + {0x0000u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x3c00u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0xbc00u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x00a0u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a03edu}}, + {0x80a0u, {0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u, 0x5a5a83edu}}, + {0x3800u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, +}; + +constexpr Vector SqrtRows[] = { + {0x0001u, {0x5a5a0000u, 0x5a5a0c00u, 0x5a5a0000u, 0x5a5a0c00u}}, + {0x0200u, {0x5a5a0000u, 0x5a5a1da8u, 0x5a5a0000u, 0x5a5a1da8u}}, + {0x03ffu, {0x5a5a0000u, 0x5a5a1fffu, 0x5a5a0000u, 0x5a5a1fffu}}, + {0x8001u, {0x5a5a8000u, 0x5a5afe00u, 0x5a5a8000u, 0x5a5afe00u}}, + {0x8200u, {0x5a5a8000u, 0x5a5afe00u, 0x5a5a8000u, 0x5a5afe00u}}, + {0x83ffu, {0x5a5a8000u, 0x5a5afe00u, 0x5a5a8000u, 0x5a5afe00u}}, + {0x0400u, {0x5a5a2000u, 0x5a5a2000u, 0x5a5a2000u, 0x5a5a2000u}}, + {0x8400u, {0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u}}, + {0x0000u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x3c00u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0xbc00u, {0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u}}, + {0x4400u, {0x5a5a4000u, 0x5a5a4000u, 0x5a5a4000u, 0x5a5a4000u}}, +}; + +constexpr Case Cases[] = { + {"v_ceil_f16", 0x7e14b904u, CeilRows}, + {"v_cvt_f32_f16", 0x7e141704u, CvtF32Rows}, + {"v_cvt_norm_i16_f16", 0x7e14c704u, CvtNormI16Rows}, + {"v_cvt_norm_u16_f16", 0x7e14c904u, CvtNormU16Rows}, + {"v_exp_f16", 0x7e14b104u, ExpRows}, + {"v_floor_f16", 0x7e14b704u, FloorRows}, + {"v_fract_f16", 0x7e14bf04u, FractRows}, + {"v_frexp_exp_i16_f16", 0x7e14b504u, FrexpExpRows}, + {"v_frexp_mant_f16", 0x7e14b304u, FrexpMantRows}, + {"v_log_f16", 0x7e14af04u, LogRows}, + {"v_rcp_f16", 0x7e14a904u, RcpRows}, + {"v_rsq_f16", 0x7e14ad04u, RsqRows}, + {"v_sin_f16", 0x7e14c104u, SinRows}, + {"v_sqrt_f16", 0x7e14ab04u, SqrtRows}, +}; + +constexpr std::uint32_t CaseCount = std::size(Cases); +std::array Shaders{}; + +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, const Shader& shader, 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].half; + } + 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(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{(3u << 4u) | (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].half) + " is " + Hex(actual) + ", expected " + Hex(expected)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (std::uint32_t index = 0; index < CaseCount; ++index) { + Shaders[index].words = {0x34020084u, 0x34040082u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e1402ffu, 0x5a5a5a5au, Cases[index].word, + 0xe0701000u, 0x80010a02u, 0xbf810000u}; + for (std::uint32_t denorm16 = 0u; denorm16 < 4u; ++denorm16) { + for (const bool ieee : {false, true}) { + Run(*device, Cases[index], Shaders[index], denorm16, ieee); + Check(Cases[index], denorm16, ieee); + } + } + } + std::puts("f16 denormal flush 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/F16MinMaxNan.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F16MinMaxNan.cpp new file mode 100644 index 0000000000..9afb2e1a37 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F16MinMaxNan.cpp @@ -0,0 +1,853 @@ +#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 = 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, 0x34060086u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e140280u, 0x7e160280u, 0x7e180280u, + 0x7e1a0280u, 0x7e1c0280u, 0x7e1e0280u, 0x7e200280u, 0x7e220280u, 0x74140b04u, 0x72160b04u, 0xd751000cu, + 0x041a0b04u, 0xd754000du, 0x041a0b04u, 0xd757000eu, 0x041a0b04u, 0xd53a020fu, 0x22020b04u, 0xd5390110u, + 0x42020b04u, 0xe0781000u, 0x80010a03u, 0xe0781010u, 0x80010e03u, 0xbf810000u, +}; + +alignas(256) constexpr std::array SdwaLowCode{ + 0x34020084u, 0x34060086u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e140280u, 0x7e160280u, 0x7e180280u, + 0x7e1a0280u, 0x7e1c0280u, 0x7e1e0280u, 0x7e200280u, 0x7e220280u, 0x74140af9u, 0x04041404u, 0x72160af9u, + 0x04041404u, 0x741e0af9u, 0x24141404u, 0x72200af9u, 0x14241404u, 0xe0781000u, 0x80010a03u, 0xe0781010u, + 0x80010e03u, 0xbf810000u, +}; + +alignas(256) constexpr std::array SdwaHighCode{ + 0x34020084u, 0x34060086u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e140280u, 0x7e160280u, 0x7e180280u, + 0x7e1a0280u, 0x7e1c0280u, 0x7e1e0280u, 0x7e200280u, 0x7e220280u, 0x74140af9u, 0x05051404u, 0x72160af9u, + 0x05051404u, 0x741e0af9u, 0x25151404u, 0x72200af9u, 0x15251404u, 0xe0781000u, 0x80010a03u, 0xe0781010u, + 0x80010e03u, 0xbf810000u, +}; + +constexpr std::uint32_t Rows[128][4] = { + {0x12340000u, 0x56780000u, 0x9abcc000u, 0x00000000u}, + {0x12340000u, 0x56788000u, 0x9abc0001u, 0x00000000u}, + {0x12340000u, 0x56783c00u, 0x9abc7e00u, 0x00000000u}, + {0x12340000u, 0x5678c000u, 0x9abc3c00u, 0x00000000u}, + {0x12340000u, 0x56787c00u, 0x9abcfd23u, 0x00000000u}, + {0x12340000u, 0x5678fc00u, 0x9abcfc00u, 0x00000000u}, + {0x12340000u, 0x56787e00u, 0x9abc8000u, 0x00000000u}, + {0x12340000u, 0x5678fe55u, 0x9abc7c01u, 0x00000000u}, + {0x12340000u, 0x56787c01u, 0x9abc7c00u, 0x00000000u}, + {0x12340000u, 0x5678fd23u, 0x9abc0000u, 0x00000000u}, + {0x12340000u, 0x56780001u, 0x9abcfe55u, 0x00000000u}, + {0x12348000u, 0x56780000u, 0x9abcc000u, 0x00000000u}, + {0x12348000u, 0x56788000u, 0x9abc0001u, 0x00000000u}, + {0x12348000u, 0x56783c00u, 0x9abc7e00u, 0x00000000u}, + {0x12348000u, 0x5678c000u, 0x9abc3c00u, 0x00000000u}, + {0x12348000u, 0x56787c00u, 0x9abcfd23u, 0x00000000u}, + {0x12348000u, 0x5678fc00u, 0x9abcfc00u, 0x00000000u}, + {0x12348000u, 0x56787e00u, 0x9abc8000u, 0x00000000u}, + {0x12348000u, 0x5678fe55u, 0x9abc7c01u, 0x00000000u}, + {0x12348000u, 0x56787c01u, 0x9abc7c00u, 0x00000000u}, + {0x12348000u, 0x5678fd23u, 0x9abc0000u, 0x00000000u}, + {0x12348000u, 0x56780001u, 0x9abcfe55u, 0x00000000u}, + {0x12343c00u, 0x56780000u, 0x9abcc000u, 0x00000000u}, + {0x12343c00u, 0x56788000u, 0x9abc0001u, 0x00000000u}, + {0x12343c00u, 0x56783c00u, 0x9abc7e00u, 0x00000000u}, + {0x12343c00u, 0x5678c000u, 0x9abc3c00u, 0x00000000u}, + {0x12343c00u, 0x56787c00u, 0x9abcfd23u, 0x00000000u}, + {0x12343c00u, 0x5678fc00u, 0x9abcfc00u, 0x00000000u}, + {0x12343c00u, 0x56787e00u, 0x9abc8000u, 0x00000000u}, + {0x12343c00u, 0x5678fe55u, 0x9abc7c01u, 0x00000000u}, + {0x12343c00u, 0x56787c01u, 0x9abc7c00u, 0x00000000u}, + {0x12343c00u, 0x5678fd23u, 0x9abc0000u, 0x00000000u}, + {0x12343c00u, 0x56780001u, 0x9abcfe55u, 0x00000000u}, + {0x1234c000u, 0x56780000u, 0x9abcc000u, 0x00000000u}, + {0x1234c000u, 0x56788000u, 0x9abc0001u, 0x00000000u}, + {0x1234c000u, 0x56783c00u, 0x9abc7e00u, 0x00000000u}, + {0x1234c000u, 0x5678c000u, 0x9abc3c00u, 0x00000000u}, + {0x1234c000u, 0x56787c00u, 0x9abcfd23u, 0x00000000u}, + {0x1234c000u, 0x5678fc00u, 0x9abcfc00u, 0x00000000u}, + {0x1234c000u, 0x56787e00u, 0x9abc8000u, 0x00000000u}, + {0x1234c000u, 0x5678fe55u, 0x9abc7c01u, 0x00000000u}, + {0x1234c000u, 0x56787c01u, 0x9abc7c00u, 0x00000000u}, + {0x1234c000u, 0x5678fd23u, 0x9abc0000u, 0x00000000u}, + {0x1234c000u, 0x56780001u, 0x9abcfe55u, 0x00000000u}, + {0x12347c00u, 0x56780000u, 0x9abcc000u, 0x00000000u}, + {0x12347c00u, 0x56788000u, 0x9abc0001u, 0x00000000u}, + {0x12347c00u, 0x56783c00u, 0x9abc7e00u, 0x00000000u}, + {0x12347c00u, 0x5678c000u, 0x9abc3c00u, 0x00000000u}, + {0x12347c00u, 0x56787c00u, 0x9abcfd23u, 0x00000000u}, + {0x12347c00u, 0x5678fc00u, 0x9abcfc00u, 0x00000000u}, + {0x12347c00u, 0x56787e00u, 0x9abc8000u, 0x00000000u}, + {0x12347c00u, 0x5678fe55u, 0x9abc7c01u, 0x00000000u}, + {0x12347c00u, 0x56787c01u, 0x9abc7c00u, 0x00000000u}, + {0x12347c00u, 0x5678fd23u, 0x9abc0000u, 0x00000000u}, + {0x12347c00u, 0x56780001u, 0x9abcfe55u, 0x00000000u}, + {0x1234fc00u, 0x56780000u, 0x9abcc000u, 0x00000000u}, + {0x1234fc00u, 0x56788000u, 0x9abc0001u, 0x00000000u}, + {0x1234fc00u, 0x56783c00u, 0x9abc7e00u, 0x00000000u}, + {0x1234fc00u, 0x5678c000u, 0x9abc3c00u, 0x00000000u}, + {0x1234fc00u, 0x56787c00u, 0x9abcfd23u, 0x00000000u}, + {0x1234fc00u, 0x5678fc00u, 0x9abcfc00u, 0x00000000u}, + {0x1234fc00u, 0x56787e00u, 0x9abc8000u, 0x00000000u}, + {0x1234fc00u, 0x5678fe55u, 0x9abc7c01u, 0x00000000u}, + {0x1234fc00u, 0x56787c01u, 0x9abc7c00u, 0x00000000u}, + {0x1234fc00u, 0x5678fd23u, 0x9abc0000u, 0x00000000u}, + {0x1234fc00u, 0x56780001u, 0x9abcfe55u, 0x00000000u}, + {0x12347e00u, 0x56780000u, 0x9abcc000u, 0x00000000u}, + {0x12347e00u, 0x56788000u, 0x9abc0001u, 0x00000000u}, + {0x12347e00u, 0x56783c00u, 0x9abc7e00u, 0x00000000u}, + {0x12347e00u, 0x5678c000u, 0x9abc3c00u, 0x00000000u}, + {0x12347e00u, 0x56787c00u, 0x9abcfd23u, 0x00000000u}, + {0x12347e00u, 0x5678fc00u, 0x9abcfc00u, 0x00000000u}, + {0x12347e00u, 0x56787e00u, 0x9abc8000u, 0x00000000u}, + {0x12347e00u, 0x5678fe55u, 0x9abc7c01u, 0x00000000u}, + {0x12347e00u, 0x56787c01u, 0x9abc7c00u, 0x00000000u}, + {0x12347e00u, 0x5678fd23u, 0x9abc0000u, 0x00000000u}, + {0x12347e00u, 0x56780001u, 0x9abcfe55u, 0x00000000u}, + {0x1234fe55u, 0x56780000u, 0x9abcc000u, 0x00000000u}, + {0x1234fe55u, 0x56788000u, 0x9abc0001u, 0x00000000u}, + {0x1234fe55u, 0x56783c00u, 0x9abc7e00u, 0x00000000u}, + {0x1234fe55u, 0x5678c000u, 0x9abc3c00u, 0x00000000u}, + {0x1234fe55u, 0x56787c00u, 0x9abcfd23u, 0x00000000u}, + {0x1234fe55u, 0x5678fc00u, 0x9abcfc00u, 0x00000000u}, + {0x1234fe55u, 0x56787e00u, 0x9abc8000u, 0x00000000u}, + {0x1234fe55u, 0x5678fe55u, 0x9abc7c01u, 0x00000000u}, + {0x1234fe55u, 0x56787c01u, 0x9abc7c00u, 0x00000000u}, + {0x1234fe55u, 0x5678fd23u, 0x9abc0000u, 0x00000000u}, + {0x1234fe55u, 0x56780001u, 0x9abcfe55u, 0x00000000u}, + {0x12347c01u, 0x56780000u, 0x9abcc000u, 0x00000000u}, + {0x12347c01u, 0x56788000u, 0x9abc0001u, 0x00000000u}, + {0x12347c01u, 0x56783c00u, 0x9abc7e00u, 0x00000000u}, + {0x12347c01u, 0x5678c000u, 0x9abc3c00u, 0x00000000u}, + {0x12347c01u, 0x56787c00u, 0x9abcfd23u, 0x00000000u}, + {0x12347c01u, 0x5678fc00u, 0x9abcfc00u, 0x00000000u}, + {0x12347c01u, 0x56787e00u, 0x9abc8000u, 0x00000000u}, + {0x12347c01u, 0x5678fe55u, 0x9abc7c01u, 0x00000000u}, + {0x12347c01u, 0x56787c01u, 0x9abc7c00u, 0x00000000u}, + {0x12347c01u, 0x5678fd23u, 0x9abc0000u, 0x00000000u}, + {0x12347c01u, 0x56780001u, 0x9abcfe55u, 0x00000000u}, + {0x1234fd23u, 0x56780000u, 0x9abcc000u, 0x00000000u}, + {0x1234fd23u, 0x56788000u, 0x9abc0001u, 0x00000000u}, + {0x1234fd23u, 0x56783c00u, 0x9abc7e00u, 0x00000000u}, + {0x1234fd23u, 0x5678c000u, 0x9abc3c00u, 0x00000000u}, + {0x1234fd23u, 0x56787c00u, 0x9abcfd23u, 0x00000000u}, + {0x1234fd23u, 0x5678fc00u, 0x9abcfc00u, 0x00000000u}, + {0x1234fd23u, 0x56787e00u, 0x9abc8000u, 0x00000000u}, + {0x1234fd23u, 0x5678fe55u, 0x9abc7c01u, 0x00000000u}, + {0x1234fd23u, 0x56787c01u, 0x9abc7c00u, 0x00000000u}, + {0x1234fd23u, 0x5678fd23u, 0x9abc0000u, 0x00000000u}, + {0x1234fd23u, 0x56780001u, 0x9abcfe55u, 0x00000000u}, + {0x12340001u, 0x56780000u, 0x9abcc000u, 0x00000000u}, + {0x12340001u, 0x56788000u, 0x9abc0001u, 0x00000000u}, + {0x12340001u, 0x56783c00u, 0x9abc7e00u, 0x00000000u}, + {0x12340001u, 0x5678c000u, 0x9abc3c00u, 0x00000000u}, + {0x12340001u, 0x56787c00u, 0x9abcfd23u, 0x00000000u}, + {0x12340001u, 0x5678fc00u, 0x9abcfc00u, 0x00000000u}, + {0x12340001u, 0x56787e00u, 0x9abc8000u, 0x00000000u}, + {0x12340001u, 0x5678fe55u, 0x9abc7c01u, 0x00000000u}, + {0x12340001u, 0x56787c01u, 0x9abc7c00u, 0x00000000u}, + {0x12340001u, 0x5678fd23u, 0x9abc0000u, 0x00000000u}, + {0x12340001u, 0x56780001u, 0x9abcfe55u, 0x00000000u}, + {0x12347c01u, 0x5678fd23u, 0x9abc7e00u, 0x00000000u}, + {0x1234fd23u, 0x56787e00u, 0x9abc7c01u, 0x00000000u}, + {0x12347e00u, 0x56787c01u, 0x9abcfd23u, 0x00000000u}, + {0x1234fe55u, 0x56787e00u, 0x9abc7c01u, 0x00000000u}, + {0x12343c00u, 0x56787c01u, 0x9abcfd23u, 0x00000000u}, + {0x12347e00u, 0x5678fe55u, 0x9abc3c00u, 0x00000000u}, + {0x12340001u, 0x56788001u, 0x9abc7c01u, 0x00000000u} +}; +constexpr std::uint32_t Expected[128][7] = { + {0x00000000u, 0x00000000u, 0x0000c000u, 0x00000000u, 0x00000000u, 0x00008000u, 0x00000000u}, + {0x00008000u, 0x00000000u, 0x00008000u, 0x00000001u, 0x00000000u, 0x00008000u, 0x00000000u}, + {0x00000000u, 0x00003c00u, 0x00000000u, 0x00003c00u, 0x00000000u, 0x00008000u, 0x00000000u}, + {0x0000c000u, 0x00000000u, 0x0000c000u, 0x00003c00u, 0x00000000u, 0x00008000u, 0x00004000u}, + {0x00000000u, 0x00007c00u, 0x00000000u, 0x00007c00u, 0x00000000u, 0x00008000u, 0x00000000u}, + {0x0000fc00u, 0x00000000u, 0x0000fc00u, 0x00000000u, 0x0000fc00u, 0x00008000u, 0x00007c00u}, + {0x00000000u, 0x00000000u, 0x00008000u, 0x00000000u, 0x00008000u, 0x00008000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00008000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00007c00u, 0x00000000u, 0x00008000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00008000u, 0x00000000u}, + {0x00000000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00008000u, 0x00000000u}, + {0x00008000u, 0x00000000u, 0x0000c000u, 0x00000000u, 0x00008000u, 0x00000000u, 0x00000000u}, + {0x00008000u, 0x00008000u, 0x00008000u, 0x00000001u, 0x00008000u, 0x00000000u, 0x00000000u}, + {0x00008000u, 0x00003c00u, 0x00008000u, 0x00003c00u, 0x00008000u, 0x00000000u, 0x00000000u}, + {0x0000c000u, 0x00008000u, 0x0000c000u, 0x00003c00u, 0x00008000u, 0x00000000u, 0x00004000u}, + {0x00008000u, 0x00007c00u, 0x00008000u, 0x00007c00u, 0x00008000u, 0x00000000u, 0x00000000u}, + {0x0000fc00u, 0x00008000u, 0x0000fc00u, 0x00008000u, 0x0000fc00u, 0x00000000u, 0x00007c00u}, + {0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00000000u, 0x00000000u}, + {0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00000000u, 0x00000000u}, + {0x00008000u, 0x00008000u, 0x00008000u, 0x00007c00u, 0x00008000u, 0x00000000u, 0x00000000u}, + {0x00008000u, 0x00008000u, 0x00008000u, 0x00000000u, 0x00008000u, 0x00000000u, 0x00000000u}, + {0x00008000u, 0x00000001u, 0x00008000u, 0x00000001u, 0x00008000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00003c00u, 0x0000c000u, 0x00003c00u, 0x00000000u, 0x0000bc00u, 0x00003c00u}, + {0x00008000u, 0x00003c00u, 0x00008000u, 0x00003c00u, 0x00000001u, 0x0000bc00u, 0x00003c00u}, + {0x00003c00u, 0x00003c00u, 0x00003c00u, 0x00003c00u, 0x00003c00u, 0x0000bc00u, 0x00003c00u}, + {0x0000c000u, 0x00003c00u, 0x0000c000u, 0x00003c00u, 0x00003c00u, 0x0000bc00u, 0x00004000u}, + {0x00003c00u, 0x00007c00u, 0x00003c00u, 0x00007c00u, 0x00003c00u, 0x0000bc00u, 0x00003c00u}, + {0x0000fc00u, 0x00003c00u, 0x0000fc00u, 0x00003c00u, 0x0000fc00u, 0x0000bc00u, 0x00007c00u}, + {0x00003c00u, 0x00003c00u, 0x00008000u, 0x00003c00u, 0x00008000u, 0x0000bc00u, 0x00003c00u}, + {0x00003c00u, 0x00003c00u, 0x00003c00u, 0x00003c00u, 0x00003c00u, 0x0000bc00u, 0x00003c00u}, + {0x00003c00u, 0x00003c00u, 0x00003c00u, 0x00007c00u, 0x00003c00u, 0x0000bc00u, 0x00003c00u}, + {0x00003c00u, 0x00003c00u, 0x00000000u, 0x00003c00u, 0x00000000u, 0x0000bc00u, 0x00003c00u}, + {0x00000001u, 0x00003c00u, 0x00000001u, 0x00003c00u, 0x00000001u, 0x0000bc00u, 0x00003c00u}, + {0x0000c000u, 0x00000000u, 0x0000c000u, 0x00000000u, 0x0000c000u, 0x00000000u, 0x00004000u}, + {0x0000c000u, 0x00008000u, 0x0000c000u, 0x00000001u, 0x00008000u, 0x00000000u, 0x00004000u}, + {0x0000c000u, 0x00003c00u, 0x0000c000u, 0x00003c00u, 0x0000c000u, 0x00003c00u, 0x00004000u}, + {0x0000c000u, 0x0000c000u, 0x0000c000u, 0x00003c00u, 0x0000c000u, 0x00004000u, 0x00004000u}, + {0x0000c000u, 0x00007c00u, 0x0000c000u, 0x00007c00u, 0x0000c000u, 0x00004000u, 0x00004000u}, + {0x0000fc00u, 0x0000c000u, 0x0000fc00u, 0x0000c000u, 0x0000fc00u, 0x00004000u, 0x00007c00u}, + {0x0000c000u, 0x0000c000u, 0x0000c000u, 0x00008000u, 0x0000c000u, 0x00004000u, 0x00004000u}, + {0x0000c000u, 0x0000c000u, 0x0000c000u, 0x0000c000u, 0x0000c000u, 0x00004000u, 0x00004000u}, + {0x0000c000u, 0x0000c000u, 0x0000c000u, 0x00007c00u, 0x0000c000u, 0x00004000u, 0x00004000u}, + {0x0000c000u, 0x0000c000u, 0x0000c000u, 0x00000000u, 0x0000c000u, 0x00004000u, 0x00004000u}, + {0x0000c000u, 0x00000001u, 0x0000c000u, 0x00000001u, 0x0000c000u, 0x00000001u, 0x00004000u}, + {0x00000000u, 0x00007c00u, 0x0000c000u, 0x00007c00u, 0x00000000u, 0x0000fc00u, 0x00007c00u}, + {0x00008000u, 0x00007c00u, 0x00008000u, 0x00007c00u, 0x00000001u, 0x0000fc00u, 0x00007c00u}, + {0x00003c00u, 0x00007c00u, 0x00003c00u, 0x00007c00u, 0x00003c00u, 0x0000fc00u, 0x00007c00u}, + {0x0000c000u, 0x00007c00u, 0x0000c000u, 0x00007c00u, 0x00003c00u, 0x0000fc00u, 0x00007c00u}, + {0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x0000fc00u, 0x00007c00u}, + {0x0000fc00u, 0x00007c00u, 0x0000fc00u, 0x00007c00u, 0x0000fc00u, 0x0000fc00u, 0x00007c00u}, + {0x00007c00u, 0x00007c00u, 0x00008000u, 0x00007c00u, 0x00008000u, 0x0000fc00u, 0x00007c00u}, + {0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x0000fc00u, 0x00007c00u}, + {0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x0000fc00u, 0x00007c00u}, + {0x00007c00u, 0x00007c00u, 0x00000000u, 0x00007c00u, 0x00000000u, 0x0000fc00u, 0x00007c00u}, + {0x00000001u, 0x00007c00u, 0x00000001u, 0x00007c00u, 0x00000001u, 0x0000fc00u, 0x00007c00u}, + {0x0000fc00u, 0x00000000u, 0x0000fc00u, 0x00000000u, 0x0000c000u, 0x00000000u, 0x00007c00u}, + {0x0000fc00u, 0x00008000u, 0x0000fc00u, 0x00000001u, 0x00008000u, 0x00000000u, 0x00007c00u}, + {0x0000fc00u, 0x00003c00u, 0x0000fc00u, 0x00003c00u, 0x0000fc00u, 0x00003c00u, 0x00007c00u}, + {0x0000fc00u, 0x0000c000u, 0x0000fc00u, 0x00003c00u, 0x0000c000u, 0x00004000u, 0x00007c00u}, + {0x0000fc00u, 0x00007c00u, 0x0000fc00u, 0x00007c00u, 0x0000fc00u, 0x00007c00u, 0x00007c00u}, + {0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x00007c00u, 0x00007c00u}, + {0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x00008000u, 0x0000fc00u, 0x00007c00u, 0x00007c00u}, + {0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x00007c00u, 0x00007c00u}, + {0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x00007c00u, 0x0000fc00u, 0x00007c00u, 0x00007c00u}, + {0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x00000000u, 0x0000fc00u, 0x00007c00u, 0x00007c00u}, + {0x0000fc00u, 0x00000001u, 0x0000fc00u, 0x00000001u, 0x0000fc00u, 0x00000001u, 0x00007c00u}, + {0x00000000u, 0x00000000u, 0x0000c000u, 0x00000000u, 0x0000c000u, 0x00000000u, 0x00008000u}, + {0x00008000u, 0x00008000u, 0x00008000u, 0x00000001u, 0x00008000u, 0x00000000u, 0x00000000u}, + {0x00003c00u, 0x00003c00u, 0x00003c00u, 0x00003c00u, 0x00003c00u, 0x00003c00u, 0x0000bc00u}, + {0x0000c000u, 0x0000c000u, 0x0000c000u, 0x00003c00u, 0x0000c000u, 0x00004000u, 0x00004000u}, + {0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x0000fc00u}, + {0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x00007c00u, 0x00007c00u}, + {0x00007e00u, 0x00007e00u, 0x00008000u, 0x00008000u, 0x00008000u, 0x0000fe00u, 0x00007e00u}, + {0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x0000fe00u, 0x00007e00u}, + {0x00007e00u, 0x00007e00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x0000fe00u, 0x00007e00u}, + {0x00007e00u, 0x00007e00u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000fe00u, 0x00007e00u}, + {0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00008001u}, + {0x00000000u, 0x00000000u, 0x0000c000u, 0x00000000u, 0x0000c000u, 0x00000000u, 0x00008000u}, + {0x00008000u, 0x00008000u, 0x00008000u, 0x00000001u, 0x00008000u, 0x00000000u, 0x00000000u}, + {0x00003c00u, 0x00003c00u, 0x00003c00u, 0x00003c00u, 0x00003c00u, 0x00003c00u, 0x0000bc00u}, + {0x0000c000u, 0x0000c000u, 0x0000c000u, 0x00003c00u, 0x0000c000u, 0x00004000u, 0x00004000u}, + {0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x0000fc00u}, + {0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x00007c00u, 0x00007c00u}, + {0x0000fe55u, 0x0000fe55u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00007e55u, 0x00007e55u}, + {0x0000fe55u, 0x0000fe55u, 0x0000fe55u, 0x0000fe55u, 0x0000fe55u, 0x00007e55u, 0x00007e55u}, + {0x0000fe55u, 0x0000fe55u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007e55u, 0x00007e55u}, + {0x0000fe55u, 0x0000fe55u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00007e55u, 0x00007e55u}, + {0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00008001u}, + {0x00000000u, 0x00000000u, 0x0000c000u, 0x00000000u, 0x0000c000u, 0x00000000u, 0x00008000u}, + {0x00008000u, 0x00008000u, 0x00008000u, 0x00000001u, 0x00008000u, 0x00000000u, 0x00000000u}, + {0x00003c00u, 0x00003c00u, 0x00003c00u, 0x00003c00u, 0x00003c00u, 0x00003c00u, 0x0000bc00u}, + {0x0000c000u, 0x0000c000u, 0x0000c000u, 0x00003c00u, 0x0000c000u, 0x00004000u, 0x00004000u}, + {0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x0000fc00u}, + {0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x00007c00u, 0x00007c00u}, + {0x00007c01u, 0x00007c01u, 0x00008000u, 0x00008000u, 0x00008000u, 0x0000fc01u, 0x00007c01u}, + {0x00007c01u, 0x00007c01u, 0x00007c01u, 0x00007c01u, 0x00007c01u, 0x0000fc01u, 0x00007c01u}, + {0x00007c01u, 0x00007c01u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x0000fc01u, 0x00007c01u}, + {0x00007c01u, 0x00007c01u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000fc01u, 0x00007c01u}, + {0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00008001u}, + {0x00000000u, 0x00000000u, 0x0000c000u, 0x00000000u, 0x0000c000u, 0x00000000u, 0x00008000u}, + {0x00008000u, 0x00008000u, 0x00008000u, 0x00000001u, 0x00008000u, 0x00000000u, 0x00000000u}, + {0x00003c00u, 0x00003c00u, 0x00003c00u, 0x00003c00u, 0x00003c00u, 0x00003c00u, 0x0000bc00u}, + {0x0000c000u, 0x0000c000u, 0x0000c000u, 0x00003c00u, 0x0000c000u, 0x00004000u, 0x00004000u}, + {0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x0000fc00u}, + {0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x00007c00u, 0x00007c00u}, + {0x0000fd23u, 0x0000fd23u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00007d23u, 0x00007d23u}, + {0x0000fd23u, 0x0000fd23u, 0x0000fd23u, 0x0000fd23u, 0x0000fd23u, 0x00007d23u, 0x00007d23u}, + {0x0000fd23u, 0x0000fd23u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007d23u, 0x00007d23u}, + {0x0000fd23u, 0x0000fd23u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00007d23u, 0x00007d23u}, + {0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00008001u}, + {0x00000000u, 0x00000001u, 0x0000c000u, 0x00000001u, 0x00000000u, 0x00008001u, 0x00000001u}, + {0x00008000u, 0x00000001u, 0x00008000u, 0x00000001u, 0x00000001u, 0x00008001u, 0x00000001u}, + {0x00000001u, 0x00003c00u, 0x00000001u, 0x00003c00u, 0x00000001u, 0x00008001u, 0x00000001u}, + {0x0000c000u, 0x00000001u, 0x0000c000u, 0x00003c00u, 0x00000001u, 0x00008001u, 0x00004000u}, + {0x00000001u, 0x00007c00u, 0x00000001u, 0x00007c00u, 0x00000001u, 0x00008001u, 0x00000001u}, + {0x0000fc00u, 0x00000001u, 0x0000fc00u, 0x00000001u, 0x0000fc00u, 0x00008001u, 0x00007c00u}, + {0x00000001u, 0x00000001u, 0x00008000u, 0x00000001u, 0x00008000u, 0x00008001u, 0x00000001u}, + {0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00008001u, 0x00000001u}, + {0x00000001u, 0x00000001u, 0x00000001u, 0x00007c00u, 0x00000001u, 0x00008001u, 0x00000001u}, + {0x00000001u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00008001u, 0x00000001u}, + {0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00008001u, 0x00000001u}, + {0x00007c01u, 0x00007c01u, 0x00007c01u, 0x00007c01u, 0x00007c01u, 0x0000fc01u, 0x00007c01u}, + {0x0000fd23u, 0x0000fd23u, 0x0000fd23u, 0x0000fd23u, 0x0000fd23u, 0x00007d23u, 0x00007d23u}, + {0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x0000fe00u, 0x00007e00u}, + {0x0000fe55u, 0x0000fe55u, 0x0000fe55u, 0x0000fe55u, 0x0000fe55u, 0x00007e55u, 0x00007e55u}, + {0x00003c00u, 0x00003c00u, 0x00003c00u, 0x00003c00u, 0x00003c00u, 0x0000bc00u, 0x00003c00u}, + {0x00007e00u, 0x00007e00u, 0x00003c00u, 0x00003c00u, 0x00003c00u, 0x0000fe00u, 0x00007e00u}, + {0x00008001u, 0x00000001u, 0x00008001u, 0x00000001u, 0x00008001u, 0x00008001u, 0x00000001u} +}; +constexpr std::uint32_t ExpectedIeee[128][7] = { + {0x00000000u, 0x00000000u, 0x0000c000u, 0x00000000u, 0x00000000u, 0x00008000u, 0x00000000u}, + {0x00008000u, 0x00000000u, 0x00008000u, 0x00000001u, 0x00000000u, 0x00008000u, 0x00000000u}, + {0x00000000u, 0x00003c00u, 0x00000000u, 0x00003c00u, 0x00000000u, 0x00008000u, 0x00000000u}, + {0x0000c000u, 0x00000000u, 0x0000c000u, 0x00003c00u, 0x00000000u, 0x00008000u, 0x00004000u}, + {0x00000000u, 0x00007c00u, 0x0000ff23u, 0x0000ff23u, 0x0000ff23u, 0x00008000u, 0x00000000u}, + {0x0000fc00u, 0x00000000u, 0x0000fc00u, 0x00000000u, 0x0000fc00u, 0x00008000u, 0x00007c00u}, + {0x00000000u, 0x00000000u, 0x00008000u, 0x00000000u, 0x00008000u, 0x00008000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00007e01u, 0x00007e01u, 0x00007e01u, 0x00008000u, 0x00000000u}, + {0x00007e01u, 0x00007e01u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007e01u, 0x0000fe01u}, + {0x0000ff23u, 0x0000ff23u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00007f23u, 0x00007f23u}, + {0x00000000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00008000u, 0x00000000u}, + {0x00008000u, 0x00000000u, 0x0000c000u, 0x00000000u, 0x00008000u, 0x00000000u, 0x00000000u}, + {0x00008000u, 0x00008000u, 0x00008000u, 0x00000001u, 0x00008000u, 0x00000000u, 0x00000000u}, + {0x00008000u, 0x00003c00u, 0x00008000u, 0x00003c00u, 0x00008000u, 0x00000000u, 0x00000000u}, + {0x0000c000u, 0x00008000u, 0x0000c000u, 0x00003c00u, 0x00008000u, 0x00000000u, 0x00004000u}, + {0x00008000u, 0x00007c00u, 0x0000ff23u, 0x0000ff23u, 0x0000ff23u, 0x00000000u, 0x00000000u}, + {0x0000fc00u, 0x00008000u, 0x0000fc00u, 0x00008000u, 0x0000fc00u, 0x00000000u, 0x00007c00u}, + {0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00000000u, 0x00000000u}, + {0x00008000u, 0x00008000u, 0x00007e01u, 0x00007e01u, 0x00007e01u, 0x00000000u, 0x00000000u}, + {0x00007e01u, 0x00007e01u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007e01u, 0x0000fe01u}, + {0x0000ff23u, 0x0000ff23u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00007f23u, 0x00007f23u}, + {0x00008000u, 0x00000001u, 0x00008000u, 0x00000001u, 0x00008000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00003c00u, 0x0000c000u, 0x00003c00u, 0x00000000u, 0x0000bc00u, 0x00003c00u}, + {0x00008000u, 0x00003c00u, 0x00008000u, 0x00003c00u, 0x00000001u, 0x0000bc00u, 0x00003c00u}, + {0x00003c00u, 0x00003c00u, 0x00003c00u, 0x00003c00u, 0x00003c00u, 0x0000bc00u, 0x00003c00u}, + {0x0000c000u, 0x00003c00u, 0x0000c000u, 0x00003c00u, 0x00003c00u, 0x0000bc00u, 0x00004000u}, + {0x00003c00u, 0x00007c00u, 0x0000ff23u, 0x0000ff23u, 0x0000ff23u, 0x0000bc00u, 0x00003c00u}, + {0x0000fc00u, 0x00003c00u, 0x0000fc00u, 0x00003c00u, 0x0000fc00u, 0x0000bc00u, 0x00007c00u}, + {0x00003c00u, 0x00003c00u, 0x00008000u, 0x00003c00u, 0x00008000u, 0x0000bc00u, 0x00003c00u}, + {0x00003c00u, 0x00003c00u, 0x00007e01u, 0x00007e01u, 0x00007e01u, 0x0000bc00u, 0x00003c00u}, + {0x00007e01u, 0x00007e01u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007e01u, 0x0000fe01u}, + {0x0000ff23u, 0x0000ff23u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00007f23u, 0x00007f23u}, + {0x00000001u, 0x00003c00u, 0x00000001u, 0x00003c00u, 0x00000001u, 0x0000bc00u, 0x00003c00u}, + {0x0000c000u, 0x00000000u, 0x0000c000u, 0x00000000u, 0x0000c000u, 0x00000000u, 0x00004000u}, + {0x0000c000u, 0x00008000u, 0x0000c000u, 0x00000001u, 0x00008000u, 0x00000000u, 0x00004000u}, + {0x0000c000u, 0x00003c00u, 0x0000c000u, 0x00003c00u, 0x0000c000u, 0x00003c00u, 0x00004000u}, + {0x0000c000u, 0x0000c000u, 0x0000c000u, 0x00003c00u, 0x0000c000u, 0x00004000u, 0x00004000u}, + {0x0000c000u, 0x00007c00u, 0x0000ff23u, 0x0000ff23u, 0x0000ff23u, 0x00004000u, 0x00004000u}, + {0x0000fc00u, 0x0000c000u, 0x0000fc00u, 0x0000c000u, 0x0000fc00u, 0x00004000u, 0x00007c00u}, + {0x0000c000u, 0x0000c000u, 0x0000c000u, 0x00008000u, 0x0000c000u, 0x00004000u, 0x00004000u}, + {0x0000c000u, 0x0000c000u, 0x00007e01u, 0x00007e01u, 0x00007e01u, 0x00004000u, 0x00004000u}, + {0x00007e01u, 0x00007e01u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007e01u, 0x0000fe01u}, + {0x0000ff23u, 0x0000ff23u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00007f23u, 0x00007f23u}, + {0x0000c000u, 0x00000001u, 0x0000c000u, 0x00000001u, 0x0000c000u, 0x00000001u, 0x00004000u}, + {0x00000000u, 0x00007c00u, 0x0000c000u, 0x00007c00u, 0x00000000u, 0x0000fc00u, 0x00007c00u}, + {0x00008000u, 0x00007c00u, 0x00008000u, 0x00007c00u, 0x00000001u, 0x0000fc00u, 0x00007c00u}, + {0x00003c00u, 0x00007c00u, 0x00003c00u, 0x00007c00u, 0x00003c00u, 0x0000fc00u, 0x00007c00u}, + {0x0000c000u, 0x00007c00u, 0x0000c000u, 0x00007c00u, 0x00003c00u, 0x0000fc00u, 0x00007c00u}, + {0x00007c00u, 0x00007c00u, 0x0000ff23u, 0x0000ff23u, 0x0000ff23u, 0x0000fc00u, 0x00007c00u}, + {0x0000fc00u, 0x00007c00u, 0x0000fc00u, 0x00007c00u, 0x0000fc00u, 0x0000fc00u, 0x00007c00u}, + {0x00007c00u, 0x00007c00u, 0x00008000u, 0x00007c00u, 0x00008000u, 0x0000fc00u, 0x00007c00u}, + {0x00007c00u, 0x00007c00u, 0x00007e01u, 0x00007e01u, 0x00007e01u, 0x0000fc00u, 0x00007c00u}, + {0x00007e01u, 0x00007e01u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007e01u, 0x0000fe01u}, + {0x0000ff23u, 0x0000ff23u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00007f23u, 0x00007f23u}, + {0x00000001u, 0x00007c00u, 0x00000001u, 0x00007c00u, 0x00000001u, 0x0000fc00u, 0x00007c00u}, + {0x0000fc00u, 0x00000000u, 0x0000fc00u, 0x00000000u, 0x0000c000u, 0x00000000u, 0x00007c00u}, + {0x0000fc00u, 0x00008000u, 0x0000fc00u, 0x00000001u, 0x00008000u, 0x00000000u, 0x00007c00u}, + {0x0000fc00u, 0x00003c00u, 0x0000fc00u, 0x00003c00u, 0x0000fc00u, 0x00003c00u, 0x00007c00u}, + {0x0000fc00u, 0x0000c000u, 0x0000fc00u, 0x00003c00u, 0x0000c000u, 0x00004000u, 0x00007c00u}, + {0x0000fc00u, 0x00007c00u, 0x0000ff23u, 0x0000ff23u, 0x0000ff23u, 0x00007c00u, 0x00007c00u}, + {0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x00007c00u, 0x00007c00u}, + {0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x00008000u, 0x0000fc00u, 0x00007c00u, 0x00007c00u}, + {0x0000fc00u, 0x0000fc00u, 0x00007e01u, 0x00007e01u, 0x00007e01u, 0x00007c00u, 0x00007c00u}, + {0x00007e01u, 0x00007e01u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007e01u, 0x0000fe01u}, + {0x0000ff23u, 0x0000ff23u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00007f23u, 0x00007f23u}, + {0x0000fc00u, 0x00000001u, 0x0000fc00u, 0x00000001u, 0x0000fc00u, 0x00000001u, 0x00007c00u}, + {0x00000000u, 0x00000000u, 0x0000c000u, 0x00000000u, 0x0000c000u, 0x00000000u, 0x00008000u}, + {0x00008000u, 0x00008000u, 0x00008000u, 0x00000001u, 0x00008000u, 0x00000000u, 0x00000000u}, + {0x00003c00u, 0x00003c00u, 0x00003c00u, 0x00003c00u, 0x00003c00u, 0x00003c00u, 0x0000bc00u}, + {0x0000c000u, 0x0000c000u, 0x0000c000u, 0x00003c00u, 0x0000c000u, 0x00004000u, 0x00004000u}, + {0x00007c00u, 0x00007c00u, 0x0000ff23u, 0x0000ff23u, 0x0000ff23u, 0x00007c00u, 0x0000fc00u}, + {0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x00007c00u, 0x00007c00u}, + {0x00007e00u, 0x00007e00u, 0x00008000u, 0x00008000u, 0x00008000u, 0x0000fe00u, 0x00007e00u}, + {0x00007e00u, 0x00007e00u, 0x00007e01u, 0x00007e01u, 0x00007e01u, 0x0000fe00u, 0x00007e00u}, + {0x00007e01u, 0x00007e01u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007e01u, 0x0000fe01u}, + {0x0000ff23u, 0x0000ff23u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00007f23u, 0x00007f23u}, + {0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00008001u}, + {0x00000000u, 0x00000000u, 0x0000c000u, 0x00000000u, 0x0000c000u, 0x00000000u, 0x00008000u}, + {0x00008000u, 0x00008000u, 0x00008000u, 0x00000001u, 0x00008000u, 0x00000000u, 0x00000000u}, + {0x00003c00u, 0x00003c00u, 0x00003c00u, 0x00003c00u, 0x00003c00u, 0x00003c00u, 0x0000bc00u}, + {0x0000c000u, 0x0000c000u, 0x0000c000u, 0x00003c00u, 0x0000c000u, 0x00004000u, 0x00004000u}, + {0x00007c00u, 0x00007c00u, 0x0000ff23u, 0x0000ff23u, 0x0000ff23u, 0x00007c00u, 0x0000fc00u}, + {0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x00007c00u, 0x00007c00u}, + {0x0000fe55u, 0x0000fe55u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00007e55u, 0x00007e55u}, + {0x0000fe55u, 0x0000fe55u, 0x00007e01u, 0x00007e01u, 0x00007e01u, 0x00007e55u, 0x00007e55u}, + {0x00007e01u, 0x00007e01u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007e01u, 0x0000fe01u}, + {0x0000ff23u, 0x0000ff23u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00007f23u, 0x00007f23u}, + {0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00008001u}, + {0x00007e01u, 0x00007e01u, 0x0000c000u, 0x0000c000u, 0x0000c000u, 0x0000fe01u, 0x00007e01u}, + {0x00007e01u, 0x00007e01u, 0x00000001u, 0x00000001u, 0x00000001u, 0x0000fe01u, 0x00007e01u}, + {0x00007e01u, 0x00007e01u, 0x00007e01u, 0x00007e01u, 0x00007e01u, 0x0000fe01u, 0x00007e01u}, + {0x00007e01u, 0x00007e01u, 0x00003c00u, 0x00003c00u, 0x00003c00u, 0x0000fe01u, 0x00007e01u}, + {0x00007e01u, 0x00007e01u, 0x0000ff23u, 0x0000ff23u, 0x0000ff23u, 0x0000fe01u, 0x00007e01u}, + {0x00007e01u, 0x00007e01u, 0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x0000fe01u, 0x00007e01u}, + {0x00007e01u, 0x00007e01u, 0x00008000u, 0x00008000u, 0x00008000u, 0x0000fe01u, 0x00007e01u}, + {0x00007e01u, 0x00007e01u, 0x00007e01u, 0x00007e01u, 0x00007e01u, 0x0000fe01u, 0x00007e01u}, + {0x00007e01u, 0x00007e01u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x0000fe01u, 0x00007e01u}, + {0x00007e01u, 0x00007e01u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000fe01u, 0x00007e01u}, + {0x00007e01u, 0x00007e01u, 0x00007e01u, 0x00007e01u, 0x00007e01u, 0x0000fe01u, 0x00007e01u}, + {0x0000ff23u, 0x0000ff23u, 0x0000c000u, 0x0000c000u, 0x0000c000u, 0x00007f23u, 0x00007f23u}, + {0x0000ff23u, 0x0000ff23u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00007f23u, 0x00007f23u}, + {0x0000ff23u, 0x0000ff23u, 0x0000ff23u, 0x0000ff23u, 0x0000ff23u, 0x00007f23u, 0x00007f23u}, + {0x0000ff23u, 0x0000ff23u, 0x00003c00u, 0x00003c00u, 0x00003c00u, 0x00007f23u, 0x00007f23u}, + {0x0000ff23u, 0x0000ff23u, 0x0000ff23u, 0x0000ff23u, 0x0000ff23u, 0x00007f23u, 0x00007f23u}, + {0x0000ff23u, 0x0000ff23u, 0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x00007f23u, 0x00007f23u}, + {0x0000ff23u, 0x0000ff23u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00007f23u, 0x00007f23u}, + {0x0000ff23u, 0x0000ff23u, 0x00007e01u, 0x00007e01u, 0x00007e01u, 0x00007f23u, 0x00007f23u}, + {0x0000ff23u, 0x0000ff23u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007f23u, 0x00007f23u}, + {0x0000ff23u, 0x0000ff23u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00007f23u, 0x00007f23u}, + {0x0000ff23u, 0x0000ff23u, 0x0000ff23u, 0x0000ff23u, 0x0000ff23u, 0x00007f23u, 0x00007f23u}, + {0x00000000u, 0x00000001u, 0x0000c000u, 0x00000001u, 0x00000000u, 0x00008001u, 0x00000001u}, + {0x00008000u, 0x00000001u, 0x00008000u, 0x00000001u, 0x00000001u, 0x00008001u, 0x00000001u}, + {0x00000001u, 0x00003c00u, 0x00000001u, 0x00003c00u, 0x00000001u, 0x00008001u, 0x00000001u}, + {0x0000c000u, 0x00000001u, 0x0000c000u, 0x00003c00u, 0x00000001u, 0x00008001u, 0x00004000u}, + {0x00000001u, 0x00007c00u, 0x0000ff23u, 0x0000ff23u, 0x0000ff23u, 0x00008001u, 0x00000001u}, + {0x0000fc00u, 0x00000001u, 0x0000fc00u, 0x00000001u, 0x0000fc00u, 0x00008001u, 0x00007c00u}, + {0x00000001u, 0x00000001u, 0x00008000u, 0x00000001u, 0x00008000u, 0x00008001u, 0x00000001u}, + {0x00000001u, 0x00000001u, 0x00007e01u, 0x00007e01u, 0x00007e01u, 0x00008001u, 0x00000001u}, + {0x00007e01u, 0x00007e01u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007e01u, 0x0000fe01u}, + {0x0000ff23u, 0x0000ff23u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00007f23u, 0x00007f23u}, + {0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00008001u, 0x00000001u}, + {0x00007e01u, 0x00007e01u, 0x00007e01u, 0x00007e01u, 0x00007e01u, 0x0000fe01u, 0x00007e01u}, + {0x0000ff23u, 0x0000ff23u, 0x00007e01u, 0x00007e01u, 0x00007e01u, 0x00007f23u, 0x00007f23u}, + {0x00007e01u, 0x00007e01u, 0x0000ff23u, 0x0000ff23u, 0x0000ff23u, 0x00007e01u, 0x0000fe01u}, + {0x0000fe55u, 0x0000fe55u, 0x00007e01u, 0x00007e01u, 0x00007e01u, 0x00007e55u, 0x00007e55u}, + {0x00007e01u, 0x00007e01u, 0x0000ff23u, 0x0000ff23u, 0x0000ff23u, 0x00007e01u, 0x0000fe01u}, + {0x00007e00u, 0x00007e00u, 0x00003c00u, 0x00003c00u, 0x00003c00u, 0x0000fe00u, 0x00007e00u}, + {0x00008001u, 0x00000001u, 0x00007e01u, 0x00007e01u, 0x00007e01u, 0x00008001u, 0x00000001u} +}; +constexpr std::uint32_t DenormalRows[32][4] = { + {0x12343c01u, 0x567803ffu, 0x9abc0400u, 0x00000000u}, + {0x1234bc01u, 0x567803ffu, 0x9abcbc00u, 0x00000000u}, + {0x12340001u, 0x56788001u, 0x9abc03ffu, 0x00000000u}, + {0x123403ffu, 0x567883ffu, 0x9abc83ffu, 0x00000000u}, + {0x12340001u, 0x56780000u, 0x9abc8001u, 0x00000000u}, + {0x12348001u, 0x56788000u, 0x9abc0001u, 0x00000000u}, + {0x123403ffu, 0x56780400u, 0x9abc7e55u, 0x00000000u}, + {0x123483ffu, 0x56788400u, 0x9abcfd23u, 0x00000000u}, + {0x12340001u, 0x56787e00u, 0x9abc0400u, 0x00000000u}, + {0x12348001u, 0x5678fe55u, 0x9abc8400u, 0x00000000u}, + {0x123403ffu, 0x56787c01u, 0x9abc8001u, 0x00000000u}, + {0x123483ffu, 0x5678fd23u, 0x9abc0001u, 0x00000000u}, + {0x12340001u, 0x56787c00u, 0x9abc7c01u, 0x00000000u}, + {0x12348001u, 0x5678fc00u, 0x9abcfe55u, 0x00000000u}, + {0x123403ffu, 0x56780001u, 0x9abc0200u, 0x00000000u}, + {0x123483ffu, 0x56788001u, 0x9abc8200u, 0x00000000u}, + {0x123403ffu, 0x56783c01u, 0x9abc03ffu, 0x00000000u}, + {0x123403ffu, 0x5678bc01u, 0x9abc83ffu, 0x00000000u}, + {0x12348001u, 0x56780001u, 0x9abc7c01u, 0x00000000u}, + {0x123483ffu, 0x567803ffu, 0x9abc7e55u, 0x00000000u}, + {0x12340000u, 0x56780001u, 0x9abc0400u, 0x00000000u}, + {0x12348000u, 0x56788001u, 0x9abc8400u, 0x00000000u}, + {0x12340400u, 0x567803ffu, 0x9abc0001u, 0x00000000u}, + {0x12348400u, 0x567883ffu, 0x9abc8001u, 0x00000000u}, + {0x12347e00u, 0x56780001u, 0x9abc7e00u, 0x00000000u}, + {0x1234fe55u, 0x56788001u, 0x9abc7c01u, 0x00000000u}, + {0x12347c01u, 0x567803ffu, 0x9abc0001u, 0x00000000u}, + {0x1234fd23u, 0x567883ffu, 0x9abc8001u, 0x00000000u}, + {0x12347c00u, 0x56780001u, 0x9abc03ffu, 0x00000000u}, + {0x1234fc00u, 0x56788001u, 0x9abc83ffu, 0x00000000u}, + {0x12340001u, 0x567803ffu, 0x9abc7c00u, 0x00000000u}, + {0x12348001u, 0x567883ffu, 0x9abcfc00u, 0x00000000u}, +}; +constexpr std::uint32_t DenormalFlushed[32][4] = { + {0x00000000u, 0x00003c01u, 0x0000bc01u, 0x00003c01u}, + {0x0000bc01u, 0x00000000u, 0x00000000u, 0x00003c01u}, + {0x00008000u, 0x00000000u, 0x00008000u, 0x00000000u}, + {0x00008000u, 0x00000000u, 0x00008000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00008000u, 0x00000000u}, + {0x00008000u, 0x00008000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000400u, 0x00008000u, 0x00000000u}, + {0x00008400u, 0x00008000u, 0x00000000u, 0x00000400u}, + {0x00000000u, 0x00000000u, 0x00008000u, 0x00000000u}, + {0x00008000u, 0x00008000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00008000u, 0x00000000u}, + {0x00008000u, 0x00008000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00007c00u, 0x00008000u, 0x00000000u}, + {0x0000fc00u, 0x00008000u, 0x00000000u, 0x00007c00u}, + {0x00000000u, 0x00000000u, 0x00008000u, 0x00000000u}, + {0x00008000u, 0x00008000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00003c01u, 0x00008000u, 0x00000000u}, + {0x0000bc01u, 0x00000000u, 0x00008000u, 0x00003c01u}, + {0x00008000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00008000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00008000u, 0x00000000u}, + {0x00008000u, 0x00008000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000400u, 0x00008400u, 0x00000400u}, + {0x00008400u, 0x00008000u, 0x00000000u, 0x00000400u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00008000u}, + {0x00008000u, 0x00008000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00008000u}, + {0x00008000u, 0x00008000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00007c00u, 0x0000fc00u, 0x00007c00u}, + {0x0000fc00u, 0x00008000u, 0x00000000u, 0x00007c00u}, + {0x00000000u, 0x00000000u, 0x00008000u, 0x00000000u}, + {0x00008000u, 0x00008000u, 0x00000000u, 0x00000000u}, +}; +constexpr std::uint32_t DenormalKept[32][4] = { + {0x000003ffu, 0x00003c01u, 0x0000bc01u, 0x00003c01u}, + {0x0000bc01u, 0x000003ffu, 0x000003ffu, 0x00003c01u}, + {0x00008001u, 0x00000001u, 0x00008001u, 0x00000001u}, + {0x000083ffu, 0x000003ffu, 0x000083ffu, 0x000003ffu}, + {0x00000000u, 0x00000001u, 0x00008001u, 0x00000001u}, + {0x00008001u, 0x00008000u, 0x00000000u, 0x00000001u}, + {0x000003ffu, 0x00000400u, 0x000083ffu, 0x000003ffu}, + {0x00008400u, 0x000083ffu, 0x000003ffu, 0x00000400u}, + {0x00000001u, 0x00000001u, 0x00008001u, 0x00000001u}, + {0x00008001u, 0x00008001u, 0x00000001u, 0x00000001u}, + {0x000003ffu, 0x000003ffu, 0x000083ffu, 0x000003ffu}, + {0x000083ffu, 0x000083ffu, 0x000003ffu, 0x000003ffu}, + {0x00000001u, 0x00007c00u, 0x00008001u, 0x00000001u}, + {0x0000fc00u, 0x00008001u, 0x00000001u, 0x00007c00u}, + {0x00000001u, 0x000003ffu, 0x000083ffu, 0x000003ffu}, + {0x000083ffu, 0x00008001u, 0x00000001u, 0x000003ffu}, + {0x000003ffu, 0x00003c01u, 0x000083ffu, 0x000003ffu}, + {0x0000bc01u, 0x000003ffu, 0x000083ffu, 0x00003c01u}, + {0x00008001u, 0x00000001u, 0x00000001u, 0x00000001u}, + {0x000083ffu, 0x000003ffu, 0x000003ffu, 0x000003ffu}, + {0x00000000u, 0x00000001u, 0x00008000u, 0x00000000u}, + {0x00008001u, 0x00008000u, 0x00000000u, 0x00000001u}, + {0x000003ffu, 0x00000400u, 0x00008400u, 0x00000400u}, + {0x00008400u, 0x000083ffu, 0x000003ffu, 0x00000400u}, + {0x00000001u, 0x00000001u, 0x00000001u, 0x00008001u}, + {0x00008001u, 0x00008001u, 0x00000001u, 0x00000001u}, + {0x000003ffu, 0x000003ffu, 0x000003ffu, 0x000083ffu}, + {0x000083ffu, 0x000083ffu, 0x000003ffu, 0x000003ffu}, + {0x00000001u, 0x00007c00u, 0x0000fc00u, 0x00007c00u}, + {0x0000fc00u, 0x00008001u, 0x00000001u, 0x00007c00u}, + {0x00000001u, 0x000003ffu, 0x00008001u, 0x00000001u}, + {0x000083ffu, 0x00008001u, 0x00000001u, 0x000003ffu}, +}; +constexpr std::uint32_t DenormalFlushedIeee[32][4] = { + {0x00000000u, 0x00003c01u, 0x0000bc01u, 0x00003c01u}, + {0x0000bc01u, 0x00000000u, 0x00000000u, 0x00003c01u}, + {0x00008000u, 0x00000000u, 0x00008000u, 0x00000000u}, + {0x00008000u, 0x00000000u, 0x00008000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00008000u, 0x00000000u}, + {0x00008000u, 0x00008000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000400u, 0x00008000u, 0x00000000u}, + {0x00008400u, 0x00008000u, 0x00000000u, 0x00000400u}, + {0x00000000u, 0x00000000u, 0x00008000u, 0x00000000u}, + {0x00008000u, 0x00008000u, 0x00000000u, 0x00000000u}, + {0x00007e01u, 0x00007e01u, 0x00007e01u, 0x0000fe01u}, + {0x0000ff23u, 0x0000ff23u, 0x00007f23u, 0x00007f23u}, + {0x00000000u, 0x00007c00u, 0x00008000u, 0x00000000u}, + {0x0000fc00u, 0x00008000u, 0x00000000u, 0x00007c00u}, + {0x00000000u, 0x00000000u, 0x00008000u, 0x00000000u}, + {0x00008000u, 0x00008000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00003c01u, 0x00008000u, 0x00000000u}, + {0x0000bc01u, 0x00000000u, 0x00008000u, 0x00003c01u}, + {0x00008000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00008000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00008000u, 0x00000000u}, + {0x00008000u, 0x00008000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000400u, 0x00008400u, 0x00000400u}, + {0x00008400u, 0x00008000u, 0x00000000u, 0x00000400u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00008000u}, + {0x00008000u, 0x00008000u, 0x00000000u, 0x00000000u}, + {0x00007e01u, 0x00007e01u, 0x0000fe01u, 0x00007e01u}, + {0x0000ff23u, 0x0000ff23u, 0x00007f23u, 0x00007f23u}, + {0x00000000u, 0x00007c00u, 0x0000fc00u, 0x00007c00u}, + {0x0000fc00u, 0x00008000u, 0x00000000u, 0x00007c00u}, + {0x00000000u, 0x00000000u, 0x00008000u, 0x00000000u}, + {0x00008000u, 0x00008000u, 0x00000000u, 0x00000000u}, +}; +constexpr std::uint32_t DenormalKeptIeee[32][4] = { + {0x000003ffu, 0x00003c01u, 0x0000bc01u, 0x00003c01u}, + {0x0000bc01u, 0x000003ffu, 0x000003ffu, 0x00003c01u}, + {0x00008001u, 0x00000001u, 0x00008001u, 0x00000001u}, + {0x000083ffu, 0x000003ffu, 0x000083ffu, 0x000003ffu}, + {0x00000000u, 0x00000001u, 0x00008001u, 0x00000001u}, + {0x00008001u, 0x00008000u, 0x00000000u, 0x00000001u}, + {0x000003ffu, 0x00000400u, 0x000083ffu, 0x000003ffu}, + {0x00008400u, 0x000083ffu, 0x000003ffu, 0x00000400u}, + {0x00000001u, 0x00000001u, 0x00008001u, 0x00000001u}, + {0x00008001u, 0x00008001u, 0x00000001u, 0x00000001u}, + {0x00007e01u, 0x00007e01u, 0x00007e01u, 0x0000fe01u}, + {0x0000ff23u, 0x0000ff23u, 0x00007f23u, 0x00007f23u}, + {0x00000001u, 0x00007c00u, 0x00008001u, 0x00000001u}, + {0x0000fc00u, 0x00008001u, 0x00000001u, 0x00007c00u}, + {0x00000001u, 0x000003ffu, 0x000083ffu, 0x000003ffu}, + {0x000083ffu, 0x00008001u, 0x00000001u, 0x000003ffu}, + {0x000003ffu, 0x00003c01u, 0x000083ffu, 0x000003ffu}, + {0x0000bc01u, 0x000003ffu, 0x000083ffu, 0x00003c01u}, + {0x00008001u, 0x00000001u, 0x00000001u, 0x00000001u}, + {0x000083ffu, 0x000003ffu, 0x000003ffu, 0x000003ffu}, + {0x00000000u, 0x00000001u, 0x00008000u, 0x00000000u}, + {0x00008001u, 0x00008000u, 0x00000000u, 0x00000001u}, + {0x000003ffu, 0x00000400u, 0x00008400u, 0x00000400u}, + {0x00008400u, 0x000083ffu, 0x000003ffu, 0x00000400u}, + {0x00000001u, 0x00000001u, 0x00000001u, 0x00008001u}, + {0x00008001u, 0x00008001u, 0x00000001u, 0x00000001u}, + {0x00007e01u, 0x00007e01u, 0x0000fe01u, 0x00007e01u}, + {0x0000ff23u, 0x0000ff23u, 0x00007f23u, 0x00007f23u}, + {0x00000001u, 0x00007c00u, 0x0000fc00u, 0x00007c00u}, + {0x0000fc00u, 0x00008001u, 0x00000001u, 0x00007c00u}, + {0x00000001u, 0x000003ffu, 0x00008001u, 0x00000001u}, + {0x000083ffu, 0x00008001u, 0x00000001u, 0x000003ffu}, +}; +constexpr std::uint32_t TernaryDenormalFlushed[32][3] = { + {0x00000000u, 0x00003c01u, 0x00000400u}, + {0x0000bc01u, 0x00000000u, 0x0000bc00u}, + {0x00008000u, 0x00000000u, 0x00000000u}, + {0x00008000u, 0x00000000u, 0x00008000u}, + {0x00008000u, 0x00000000u, 0x00000000u}, + {0x00008000u, 0x00000000u, 0x00008000u}, + {0x00000000u, 0x00000400u, 0x00000000u}, + {0x00008400u, 0x00008000u, 0x00008400u}, + {0x00000000u, 0x00000400u, 0x00000000u}, + {0x00008400u, 0x00008000u, 0x00008400u}, + {0x00008000u, 0x00000000u, 0x00008000u}, + {0x00008000u, 0x00000000u, 0x00008000u}, + {0x00000000u, 0x00007c00u, 0x00000000u}, + {0x0000fc00u, 0x00008000u, 0x0000fc00u}, + {0x00000000u, 0x00000000u, 0x00000000u}, + {0x00008000u, 0x00008000u, 0x00008000u}, + {0x00000000u, 0x00003c01u, 0x00000000u}, + {0x0000bc01u, 0x00000000u, 0x00008000u}, + {0x00008000u, 0x00000000u, 0x00008000u}, + {0x00008000u, 0x00000000u, 0x00008000u}, + {0x00000000u, 0x00000400u, 0x00000000u}, + {0x00008400u, 0x00008000u, 0x00008000u}, + {0x00000000u, 0x00000400u, 0x00000000u}, + {0x00008400u, 0x00008000u, 0x00008000u}, + {0x00000000u, 0x00000000u, 0x00000000u}, + {0x00008000u, 0x00008000u, 0x00008000u}, + {0x00000000u, 0x00000000u, 0x00000000u}, + {0x00008000u, 0x00008000u, 0x00008000u}, + {0x00000000u, 0x00007c00u, 0x00000000u}, + {0x0000fc00u, 0x00008000u, 0x00008000u}, + {0x00000000u, 0x00007c00u, 0x00000000u}, + {0x0000fc00u, 0x00008000u, 0x00008000u}, +}; +constexpr std::uint32_t TernaryDenormalKept[32][3] = { + {0x000003ffu, 0x00003c01u, 0x00000400u}, + {0x0000bc01u, 0x000003ffu, 0x0000bc00u}, + {0x00008001u, 0x000003ffu, 0x00000001u}, + {0x000083ffu, 0x000003ffu, 0x000083ffu}, + {0x00008001u, 0x00000001u, 0x00000000u}, + {0x00008001u, 0x00000001u, 0x00008000u}, + {0x000003ffu, 0x00000400u, 0x000003ffu}, + {0x00008400u, 0x000083ffu, 0x00008400u}, + {0x00000001u, 0x00000400u, 0x00000001u}, + {0x00008400u, 0x00008001u, 0x00008400u}, + {0x00008001u, 0x000003ffu, 0x00008001u}, + {0x000083ffu, 0x00000001u, 0x000083ffu}, + {0x00000001u, 0x00007c00u, 0x00000001u}, + {0x0000fc00u, 0x00008001u, 0x0000fc00u}, + {0x00000001u, 0x000003ffu, 0x00000200u}, + {0x000083ffu, 0x00008001u, 0x00008200u}, + {0x000003ffu, 0x00003c01u, 0x000003ffu}, + {0x0000bc01u, 0x000003ffu, 0x000083ffu}, + {0x00008001u, 0x00000001u, 0x00008001u}, + {0x000083ffu, 0x000003ffu, 0x000083ffu}, + {0x00000000u, 0x00000400u, 0x00000001u}, + {0x00008400u, 0x00008000u, 0x00008001u}, + {0x00000001u, 0x00000400u, 0x000003ffu}, + {0x00008400u, 0x00008001u, 0x000083ffu}, + {0x00000001u, 0x00000001u, 0x00000001u}, + {0x00008001u, 0x00008001u, 0x00008001u}, + {0x00000001u, 0x000003ffu, 0x00000001u}, + {0x000083ffu, 0x00008001u, 0x000083ffu}, + {0x00000001u, 0x00007c00u, 0x000003ffu}, + {0x0000fc00u, 0x00008001u, 0x000083ffu}, + {0x00000001u, 0x00007c00u, 0x000003ffu}, + {0x0000fc00u, 0x00008001u, 0x000083ffu}, +}; +constexpr std::uint32_t TernaryDenormalFlushedIeee[32][3] = { + {0x00000000u, 0x00003c01u, 0x00000400u}, + {0x0000bc01u, 0x00000000u, 0x0000bc00u}, + {0x00008000u, 0x00000000u, 0x00000000u}, + {0x00008000u, 0x00000000u, 0x00008000u}, + {0x00008000u, 0x00000000u, 0x00000000u}, + {0x00008000u, 0x00000000u, 0x00008000u}, + {0x00000000u, 0x00000400u, 0x00000000u}, + {0x0000ff23u, 0x0000ff23u, 0x0000ff23u}, + {0x00000000u, 0x00000400u, 0x00000000u}, + {0x00008400u, 0x00008000u, 0x00008400u}, + {0x00008000u, 0x00008000u, 0x00008000u}, + {0x00000000u, 0x00000000u, 0x00000000u}, + {0x00007e01u, 0x00007e01u, 0x00007e01u}, + {0x0000fc00u, 0x00008000u, 0x0000fc00u}, + {0x00000000u, 0x00000000u, 0x00000000u}, + {0x00008000u, 0x00008000u, 0x00008000u}, + {0x00000000u, 0x00003c01u, 0x00000000u}, + {0x0000bc01u, 0x00000000u, 0x00008000u}, + {0x00007e01u, 0x00007e01u, 0x00007e01u}, + {0x00008000u, 0x00000000u, 0x00008000u}, + {0x00000000u, 0x00000400u, 0x00000000u}, + {0x00008400u, 0x00008000u, 0x00008000u}, + {0x00000000u, 0x00000400u, 0x00000000u}, + {0x00008400u, 0x00008000u, 0x00008000u}, + {0x00000000u, 0x00000000u, 0x00000000u}, + {0x00007e01u, 0x00007e01u, 0x00007e01u}, + {0x00000000u, 0x00000000u, 0x00000000u}, + {0x00008000u, 0x00008000u, 0x00008000u}, + {0x00000000u, 0x00007c00u, 0x00000000u}, + {0x0000fc00u, 0x00008000u, 0x00008000u}, + {0x00000000u, 0x00007c00u, 0x00000000u}, + {0x0000fc00u, 0x00008000u, 0x00008000u}, +}; +constexpr std::uint32_t TernaryDenormalKeptIeee[32][3] = { + {0x000003ffu, 0x00003c01u, 0x00000400u}, + {0x0000bc01u, 0x000003ffu, 0x0000bc00u}, + {0x00008001u, 0x000003ffu, 0x00000001u}, + {0x000083ffu, 0x000003ffu, 0x000083ffu}, + {0x00008001u, 0x00000001u, 0x00000000u}, + {0x00008001u, 0x00000001u, 0x00008000u}, + {0x000003ffu, 0x00000400u, 0x000003ffu}, + {0x0000ff23u, 0x0000ff23u, 0x0000ff23u}, + {0x00000001u, 0x00000400u, 0x00000001u}, + {0x00008400u, 0x00008001u, 0x00008400u}, + {0x00008001u, 0x00008001u, 0x00008001u}, + {0x00000001u, 0x00000001u, 0x00000001u}, + {0x00007e01u, 0x00007e01u, 0x00007e01u}, + {0x0000fc00u, 0x00008001u, 0x0000fc00u}, + {0x00000001u, 0x000003ffu, 0x00000200u}, + {0x000083ffu, 0x00008001u, 0x00008200u}, + {0x000003ffu, 0x00003c01u, 0x000003ffu}, + {0x0000bc01u, 0x000003ffu, 0x000083ffu}, + {0x00007e01u, 0x00007e01u, 0x00007e01u}, + {0x000083ffu, 0x000003ffu, 0x000083ffu}, + {0x00000000u, 0x00000400u, 0x00000001u}, + {0x00008400u, 0x00008000u, 0x00008001u}, + {0x00000001u, 0x00000400u, 0x000003ffu}, + {0x00008400u, 0x00008001u, 0x000083ffu}, + {0x00000001u, 0x00000001u, 0x00000001u}, + {0x00007e01u, 0x00007e01u, 0x00007e01u}, + {0x00000001u, 0x00000001u, 0x00000001u}, + {0x00008001u, 0x00008001u, 0x00008001u}, + {0x00000001u, 0x00007c00u, 0x000003ffu}, + {0x0000fc00u, 0x00008001u, 0x000083ffu}, + {0x00000001u, 0x00007c00u, 0x000003ffu}, + {0x0000fc00u, 0x00008001u, 0x000083ffu}, +}; +constexpr std::uint32_t Chunks = 4; +std::uint32_t Chunk = 0; +bool Ieee = false; +std::uint32_t Denorm16 = 4u; +std::uint32_t SourceForm = 0u; +constexpr const char* Names[7] = { + "v_min_f16 v10, v4, v5", + "v_max_f16 v11, v4, v5", + "v_min3_f16 v12, v4, v5, v6", + "v_max3_f16 v13, v4, v5, v6", + "v_med3_f16 v14, v4, v5, v6", + "v_min_f16_e64 v15, -v4, |v5|", + "v_max_f16_e64 v16, |v4|, -v5", +}; + +void Fill(std::uint32_t tid, std::uint32_t* words) { + const auto& row = Denorm16 < 4u ? DenormalRows[tid] : Rows[Chunk * Threads + tid]; + std::copy(std::begin(row), std::end(row), words); + if (SourceForm == 2u) { + words[0] = (words[0] << 16u) | 0x1234u; + words[1] = (words[1] << 16u) | 0x5678u; + } +} + +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 tid, std::uint32_t actual, std::uint32_t expected, const char* name) { + Require(actual == expected, std::string(Ieee ? "f16 min max nan, IEEE mode: row " : "f16 min max nan: row ") + std::to_string(tid) + ", denorm16 " + std::to_string(Denorm16) + ", source form " + std::to_string(SourceForm) + " " + 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]); + 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 = SourceForm == 1u ? std::span(SdwaLowCode) : (SourceForm == 2u ? std::span(SdwaHighCode) : std::span(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; + if (Denorm16 < 4u) request.context.floatMode = ShaderRecompiler::ShaderFloatMode{Denorm16 << 6u, true, Ieee, false}; + else if (Ieee) request.context.floatMode = ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t* in = &Input[tid * Inputs]; + const std::uint32_t* out = &Output[tid * Results]; + if (Denorm16 < 4u) { + const auto& expected = Ieee ? ((Denorm16 & 1u) != 0u ? DenormalKeptIeee : DenormalFlushedIeee) : ((Denorm16 & 1u) != 0u ? DenormalKept : DenormalFlushed); + constexpr std::array indices{0u, 1u, 5u, 6u}; + for (std::uint32_t i = 0u; i < indices.size(); ++i) Expect(tid, out[indices[i]], expected[tid][i], Names[indices[i]]); + if (SourceForm == 0u) { + const auto& ternaryExpected = Ieee ? ((Denorm16 & 1u) != 0u ? TernaryDenormalKeptIeee : TernaryDenormalFlushedIeee) : ((Denorm16 & 1u) != 0u ? TernaryDenormalKept : TernaryDenormalFlushed); + for (std::uint32_t i = 0u; i < 3u; ++i) Expect(tid, out[i + 2u], ternaryExpected[tid][i], Names[i + 2u]); + } + continue; + } + for (std::uint32_t i = 0; i < 7; ++i) { + Expect(Chunk * Threads + tid, out[i], (Ieee ? ExpectedIeee : Expected)[Chunk * Threads + tid][i], Names[i]); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const bool ieee : {false, true}) { + Ieee = ieee; + for (Chunk = 0; Chunk < Chunks; ++Chunk) { + Run(*device); + Check(); + } + for (SourceForm = 0u; SourceForm < 3u; ++SourceForm) { + for (Denorm16 = 0u; Denorm16 < 4u; ++Denorm16) { + Run(*device); + Check(); + } + } + SourceForm = 0u; + } + std::puts("f16 min max 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/F16PackTowardZero.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F16PackTowardZero.cpp new file mode 100644 index 0000000000..b07d5ca682 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F16PackTowardZero.cpp @@ -0,0 +1,117 @@ +#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 Capacity = 1u << 18; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +std::uint32_t HalfToFloatBits(std::uint32_t half) { + const std::uint32_t exponent = (half >> 10u) & 0x1fu; + const std::uint32_t mantissa = half & 0x3ffu; + if (exponent != 0u) return ((exponent + 112u) << 23u) | (mantissa << 13u); + if (mantissa == 0u) return 0u; + std::uint32_t shift = 0; + while ((mantissa << shift & 0x400u) == 0u) ++shift; + return ((113u - shift) << 23u) | (((mantissa << shift) & 0x3ffu) << 13u); +} + +std::vector Inputs() { + std::vector inputs{ + 0x00000000u, 0x00000001u, 0x007fffffu, 0x00800000u, 0x33000000u, 0x33000001u, 0x337fffffu, 0x33800000u, + 0x387fffffu, 0x38800000u, 0x477fe000u, 0x477fefffu, 0x477ff000u, 0x477fffffu, 0x47800000u, 0x7f7fffffu, + 0x7f800000u, 0x7fc00000u, 0x7f800001u, 0x7fffe000u, 0x7fbfffffu, 0x7f802000u, + }; + for (std::uint32_t half = 0; half < 0x7c00u; ++half) { + const std::uint32_t exact = HalfToFloatBits(half); + const std::uint32_t next = HalfToFloatBits(half + 1u); + const std::uint32_t middle = exact + (next - exact) / 2u; + for (const std::uint32_t value : {exact, exact + 1u, middle - 1u, middle, middle + 1u}) inputs.push_back(value); + } + while (inputs.size() % Threads != 0u) inputs.push_back(0u); + if (inputs.size() > Capacity) throw std::runtime_error("f16 pack toward zero: too many inputs"); + return inputs; +} + +std::uint32_t TowardZero(std::uint32_t bits) { + if ((bits & 0x7f800000u) == 0u) bits &= 0x80000000u; + const std::uint32_t sign = (bits >> 16u) & 0x8000u; + const std::uint32_t exponent = (bits >> 23u) & 0xffu; + const std::uint32_t mantissa = bits & 0x7fffffu; + if (exponent == 0xffu) return mantissa == 0u ? sign | 0x7c00u : sign | 0x7e00u | (mantissa >> 13u); + if (exponent >= 143u) return sign | 0x7bffu; + if (exponent >= 113u) return sign | ((exponent - 112u) << 10u) | (mantissa >> 13u); + if (exponent >= 103u) return sign | ((mantissa | 0x800000u) >> (126u - exponent)); + return sign; +} + +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; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto inputs = Inputs(); + std::copy(inputs.begin(), inputs.end(), Input.begin()); + Output.fill(0xdeadbeefu); + const std::vector code{ + 0x34020082u, 0x8f098708u, 0x4a020209u, 0xe0301000u, 0x80000401u, 0xbf8c3f70u, + 0xd52f0005u, 0x40020904u, 0xe0701000u, 0x80010501u, 0xbf810000u, + }; + 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 words(code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(words)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {true, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), words, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device->Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}; + const auto result = ShaderRecompiler::Recompile(request); + device->Dispatch(result, static_cast(inputs.size() / Threads), 1, 1, {}, reinterpret_cast(code.data())); + device->WaitIdle(); + for (std::size_t index = 0; index < inputs.size(); ++index) { + const std::uint32_t source = inputs[index]; + const std::uint32_t expected = TowardZero(source) | (TowardZero(source ^ 0x80000000u) << 16u); + Require(Output[index] == expected, "f16 pack toward zero: v_cvt_pkrtz_f16_f32 of " + Hex(source) + ", -" + Hex(source) + " is " + Hex(Output[index]) + ", expected " + Hex(expected)); + } + std::printf("f16 pack toward zero tests passed (%zu inputs)\n", inputs.size()); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F16ToF32Unpack.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F16ToF32Unpack.cpp new file mode 100644 index 0000000000..b826f18fb6 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F16ToF32Unpack.cpp @@ -0,0 +1,209 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Halves = 1u << 16u; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array ConvertCode{ + 0x34020082, 0x8f098708, 0x4a020209, 0xe0301000, 0x80000401, 0xbf8c3f70, 0x7e0a1704, 0xe0701000, + 0x80010501, 0xbf810000, +}; + +alignas(256) constexpr std::array ByteCode{ + 0x34020082, 0x8f098708, 0x4a020209, 0xe0301000, 0x80000401, 0x7e0e02ff, 0x00008000, 0xbf8c3f70, + 0x640c0ef9, 0x06010604, 0xe0701000, 0x80010601, 0xbf810000, +}; + +alignas(256) constexpr std::array CompareCode{ + 0x34020082, 0x34040083, 0x8f098708, 0x8f0a8808, 0x4a020209, 0x4a04040a, 0xe0301000, 0x80000401, + 0x7e0c02ff, 0x00003c00, 0xbf8c3f70, 0x7dd00904, 0xd5010005, 0x01a90280, 0x7d920d04, 0xd5010007, + 0x01a90280, 0xe0701000, 0x80010502, 0xe0701004, 0x80010702, 0xbf810000, +}; + +std::uint32_t HalfToFloatBits(std::uint32_t half) { + const std::uint32_t sign = (half & 0x8000u) << 16u; + const std::uint32_t exponent = (half >> 10u) & 0x1fu; + const std::uint32_t mantissa = half & 0x3ffu; + if (exponent == 0x1fu) return sign | 0x7f800000u | (mantissa << 13u); + if (exponent != 0u) return sign | ((exponent + 112u) << 23u) | (mantissa << 13u); + return sign | std::bit_cast(static_cast(mantissa) * 0x1p-24f); +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::vector UserData() { + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + return userData; +} + +ShaderRecompiler::RecompileRequest Request(std::span code, const ShaderRecompiler::SpirvTarget& target, std::span userData, std::span memory) { + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {true, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + target, + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}; + return request; +} + +struct F16Decode { + std::uint32_t unpacks = 0; + std::uint32_t nanRebuilds = 0; + bool denormPreserve16 = false; +}; + +F16Decode ScanF16Decode(std::span words) { + F16Decode decode; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + const auto opcode = words[cursor] & 0xffffu; + Require(count != 0 && cursor + count <= words.size(), "f16 to f32 unpack: malformed SPIR-V"); + if (opcode == spv::OpExecutionMode && count == 4 && words[cursor + 2] == spv::ExecutionModeDenormPreserve && words[cursor + 3] == 16u) decode.denormPreserve16 = true; + if (opcode == spv::OpExtInst && count >= 5 && words[cursor + 4] == GLSLstd450UnpackHalf2x16) ++decode.unpacks; + if (opcode == spv::OpShiftRightArithmetic) ++decode.nanRebuilds; + cursor += count; + } + return decode; +} + +void VerifySpirv() { + constexpr std::array capabilities{ + spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, + spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, + spv::CapabilityDenormPreserve, spv::CapabilitySignedZeroInfNanPreserve, + }; + constexpr std::array extensions{ + "SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage", "SPV_KHR_float_controls", + "SPV_KHR_storage_buffer_storage_class", + }; + const auto userData = UserData(); + ShaderRecompiler::SpirvTarget target{}; + target.vulkanVersion = 0x00403000u; + target.spirvVersion = 0x00010600u; + target.subgroupSize = 32; + target.bdaAbiVersion = 1; + target.supportedCapabilities = capabilities; + target.supportedExtensions = extensions; + target.maxWorkgroupSize = {1024, 1024, 64}; + target.maxWorkgroupInvocations = 1024; + target.maxWorkgroupSharedMemoryBytes = 49152; + struct Program { + std::span code; + const char* name; + std::optional nanBits; + }; + const std::array programs{{ + {ConvertCode, "v_cvt_f32_f16", true}, + {ByteCode, "v_add_f16 src0_sel:BYTE_1", std::nullopt}, + {CompareCode, "v_cmp_u_f16 and v_cmp_lt_f16", false}, + }}; + for (const auto& program : programs) { + const std::array memory{{{reinterpret_cast(program.code.data()), std::as_bytes(program.code)}}}; + for (const bool preserve : {false, true}) { + target.float16DenormPreserve = preserve; + const std::string flag = std::string(" with float16DenormPreserve=") + (preserve ? "true" : "false"); + const auto decode = ScanF16Decode(ShaderRecompiler::Recompile(Request(program.code, target, userData, memory)).spirv.Words()); + const std::string name = std::string("f16 to f32 unpack: ") + program.name; + Require(decode.denormPreserve16 == preserve, name + (preserve ? " lacks" : " has") + " DenormPreserve 16" + flag); + Require(preserve ? decode.unpacks != 0u : decode.unpacks == 0u, name + " has " + std::to_string(decode.unpacks) + " UnpackHalf2x16" + flag); + if (program.nanBits.has_value()) { + Require(decode.nanRebuilds == (preserve && *program.nanBits ? decode.unpacks : 0u), name + " rebuilds " + std::to_string(decode.nanRebuilds) + " of " + std::to_string(decode.unpacks) + " NaN results" + flag); + } + } + } +} + +void Dispatch(AgcDriver::VulkanDevice& device, std::span code, const ShaderRecompiler::SpirvTarget& target) { + const auto userData = UserData(); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const auto result = ShaderRecompiler::Recompile(Request(code, target, userData, memory)); + Output.fill(0xdeadbeefu); + device.Dispatch(result, Halves / Threads, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Run(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target, const char* name) { + Dispatch(device, ConvertCode, target); + for (std::uint32_t lane = 0; lane < Halves; ++lane) { + Require(Output[lane] == HalfToFloatBits(lane), std::string("f16 to f32 unpack (") + name + "): v_cvt_f32_f16 of " + Hex(Input[lane]) + " is " + Hex(Output[lane]) + ", expected " + Hex(HalfToFloatBits(lane))); + } + Dispatch(device, ByteCode, target); + for (std::uint32_t lane = 0; lane < Halves; ++lane) { + const std::uint32_t selected = (Input[lane] >> 8u) & 0xffu; + Require((Output[lane] & 0xffffu) == selected, std::string("f16 to f32 unpack (") + name + "): v_add_f16 src0_sel:BYTE_1 of " + Hex(Input[lane]) + " and -0 is " + Hex(Output[lane]) + ", expected " + Hex(selected)); + } + Dispatch(device, CompareCode, target); + for (std::uint32_t lane = 0; lane < Halves; ++lane) { + const float value = std::bit_cast(HalfToFloatBits(lane)); + const std::uint32_t unordered = value != value ? 1u : 0u; + const std::uint32_t less = value < 1.0f ? 1u : 0u; + Require(Output[lane * 2u] == unordered, std::string("f16 to f32 unpack (") + name + "): v_cmp_u_f16 of " + Hex(lane) + " with itself is " + Hex(Output[lane * 2u]) + ", expected " + Hex(unordered)); + Require(Output[lane * 2u + 1u] == less, std::string("f16 to f32 unpack (") + name + "): v_cmp_lt_f16 of " + Hex(lane) + " and 1.0 is " + Hex(Output[lane * 2u + 1u]) + ", expected " + Hex(less)); + } +} + +} + +int main(int argc, char** argv) { + try { + if (argc == 2 && std::string_view(argv[1]) == "--verify-spirv") { + VerifySpirv(); + std::puts("f16 to f32 conversions use UnpackHalf2x16 only under DenormPreserve 16"); + return 0; + } + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (std::uint32_t lane = 0; lane < Halves; ++lane) Input[lane] = lane | ((lane ^ 0xa5a5u) << 16u); + auto target = device->Target(); + const bool preserve = target.float16DenormPreserve; + target.float16DenormPreserve = false; + Run(*device, target, "integer decode"); + if (preserve) { + target.float16DenormPreserve = true; + Run(*device, target, "UnpackHalf2x16"); + } + std::printf("f16 to f32 unpack tests passed (%u halves, %s)\n", Halves, preserve ? "both decodes" : "integer decode only"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F32CompareConstant.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F32CompareConstant.cpp new file mode 100644 index 0000000000..4b16ff31a6 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F32CompareConstant.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 +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Words = 8; +alignas(256) std::array Input{ + 0x00000000u, 0x80000000u, 0x00000001u, 0x80000001u, 0x007fffffu, 0x807fffffu, 0x00800000u, 0x80800000u, + 0x00800001u, 0x80800001u, 0x3f000000u, 0xbf000000u, 0x3effffffu, 0x3f000001u, 0xc0800000u, 0xc07fffffu, + 0xc0800001u, 0x3f800000u, 0x7f800000u, 0xff800000u, 0x7fc00000u, 0xffc00001u, 0x7f800001u, 0x7f7fffffu, + 0xff7fffffu, 0x00400000u, 0x80400000u, 0x0000ffffu, 0x3e800000u, 0xbe800000u, 0x80000000u, 0x01000000u, +}; +alignas(256) std::array Output{}; + +struct Constant { + std::uint32_t bits; + std::uint32_t inlineSource; +}; + +constexpr std::array Constants{{ + {0x00000000u, 0x80u}, {0x80000000u, 0xffu}, {0x00000001u, 0xffu}, {0x807fffffu, 0xffu}, {0x3f000000u, 0xf0u}, + {0xc0800000u, 0xf7u}, {0x00800000u, 0xffu}, {0x7f800000u, 0xffu}, {0x7fc00000u, 0xffu}, +}}; + +constexpr std::array Predicates{0x01u, 0x02u, 0x03u, 0x04u, 0x05u, 0x06u, 0x09u, 0x0au, 0x0bu, 0x0cu, 0x0du, 0x0eu}; +constexpr std::array PredicateNames{"lt", "eq", "le", "gt", "lg", "ge", "nge", "nlg", "ngt", "nle", "neq", "nlt"}; + +constexpr std::uint32_t Compares = static_cast(Constants.size() * Predicates.size() * 2u); + +std::vector Build(bool registerOperand) { + std::vector code{0x34020082u, 0x34060085u, 0xe0301000u, 0x80000401u, 0xbf8c3f70u, 0x7e2c0281u}; + for (std::uint32_t word = 0; word < Words; ++word) code.push_back(0x7e000280u | ((30u + word) << 17)); + std::uint32_t bit = 0; + for (const auto& constant : Constants) { + if (registerOperand) code.insert(code.end(), {0x7e2a02ffu, constant.bits}); + const std::uint32_t source = registerOperand ? 256u + 21u : constant.inlineSource; + const bool literal = !registerOperand && source == 0xffu; + for (const std::uint32_t predicate : Predicates) { + for (const bool constantFirst : {true, false}) { + if (constantFirst) { + code.push_back(0x7c000000u | (predicate << 17) | (4u << 9) | source); + } else { + code.push_back(0xd4000000u | (predicate << 16) | 106u); + code.push_back((source << 9) | (256u + 4u)); + } + if (literal) code.push_back(constant.bits); + const std::uint32_t accumulator = 30u + bit / 32u; + code.push_back((0x01u << 25) | (20u << 17) | (22u << 9) | 0x80u); + code.push_back(0xd4000000u | (0x36fu << 16) | accumulator); + code.push_back(((256u + accumulator) << 18) | ((0x80u + bit % 32u) << 9) | (256u + 20u)); + ++bit; + } + } + } + for (std::uint32_t word = 0; word < Words; ++word) code.insert(code.end(), {0xe0701000u | (word * 4u), 0x80010003u | ((30u + word) << 8)}); + code.push_back(0xbf810000u); + return code; +} + +std::uint32_t Flush(std::uint32_t bits, bool flush) { + return flush && (bits & 0x7f800000u) == 0u ? bits & 0x80000000u : bits; +} + +bool Compare(std::uint32_t predicate, std::uint32_t lhsBits, std::uint32_t rhsBits) { + const float lhs = std::bit_cast(lhsBits); + const float rhs = std::bit_cast(rhsBits); + switch (predicate) { + case 0x01u: return lhs < rhs; + case 0x02u: return lhs == rhs; + case 0x03u: return lhs <= rhs; + case 0x04u: return lhs > rhs; + case 0x05u: return lhs < rhs || lhs > rhs; + case 0x06u: return lhs >= rhs; + case 0x09u: return !(lhs >= rhs); + case 0x0au: return !(lhs < rhs || lhs > rhs); + case 0x0bu: return !(lhs > rhs); + case 0x0cu: return !(lhs <= rhs); + case 0x0du: return !(lhs == rhs); + case 0x0eu: return !(lhs < rhs); + default: throw std::runtime_error("f32 compare constant: unknown predicate"); + } +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +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::optional& floatMode, bool registerOperand) { + Output.fill(0xdeadbeefu); + const auto code = Build(registerOperand); + 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 words(code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(words)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), words, 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(const std::string& mode, bool flush) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t value = Flush(Input[tid], flush); + std::uint32_t bit = 0; + for (const auto& constant : Constants) { + const std::uint32_t flushedConstant = Flush(constant.bits, flush); + for (std::size_t predicate = 0; predicate < Predicates.size(); ++predicate) { + for (const bool constantFirst : {true, false}) { + const bool expected = constantFirst ? Compare(Predicates[predicate], flushedConstant, value) : Compare(Predicates[predicate], value, flushedConstant); + const bool actual = ((Output[tid * Words + bit / 32u] >> (bit % 32u)) & 1u) != 0u; + Require(actual == expected, "f32 compare constant: " + mode + " lane " + std::to_string(tid) + " v_cmp_" + PredicateNames[predicate] + "_f32 " + + (constantFirst ? Hex(constant.bits) + ", " + Hex(Input[tid]) : Hex(Input[tid]) + ", " + Hex(constant.bits)) + " is " + (actual ? "1" : "0") + ", expected " + (expected ? "1" : "0")); + ++bit; + } + } + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + static_assert(Compares <= Words * 32u); + struct Mode { + const char* name; + std::optional floatMode; + bool flush; + }; + const std::array modes{{ + {"no float mode", std::nullopt, true}, + {"IEEE=0 f32 denormals flushed", ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}, true}, + {"IEEE=1 all denormals flushed", ShaderRecompiler::ShaderFloatMode{0x00u, false, true, false}, true}, + }}; + for (const auto& mode : modes) { + for (const bool registerOperand : {true, false}) { + Run(*device, mode.floatMode, registerOperand); + Check(std::string(mode.name) + (registerOperand ? ", register operand" : ", constant operand"), mode.flush); + } + } + std::puts("f32 compare constant 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/F32ConstantFactorTininess.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F32ConstantFactorTininess.cpp new file mode 100644 index 0000000000..4cc7734df1 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F32ConstantFactorTininess.cpp @@ -0,0 +1,176 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#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, 0x34060086u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, + 0x7e3402f4u, 0x7e3602f0u, 0x7e3802ffu, 0x3f7ffffeu, 0x7e3a02f2u, 0x7e3c02ffu, 0x00400000u, 0x7e3e02f6u, + 0x7e4002ffu, 0x3e800000u, 0x7e4202f4u, + 0x101408f4u, 0x1016091au, + 0x101808f0u, 0x101a091bu, + 0x101c08ffu, 0x3f7ffffeu, 0x101e091cu, + 0x102008ffu, 0x3f800000u, 0x1022091du, + 0x102408ffu, 0x00400000u, 0x1026091eu, + 0x0e2808f6u, 0x0e2a091fu, + 0x582c0b04u, 0x3e800000u, 0xd54b0017u, 0x04164104u, + 0xd54b0218u, 0x4415e904u, 0xd54b0219u, 0x44164304u, + 0xe0781000u, 0x80010a03u, 0xe0781010u, 0x80010e03u, 0xe0781020u, 0x80011203u, 0xe0781030u, 0x80011603u, + 0xbf810000u, +}; + +constexpr std::uint32_t Rows[32][2] = { + {0x00800000u, 0x00000000u}, {0x80800000u, 0x00000000u}, {0x00800001u, 0x00000000u}, {0x00ffffffu, 0x00000000u}, + {0x01000000u, 0x00000000u}, {0x017fffffu, 0x00000000u}, {0x01800000u, 0x00000000u}, {0x00000001u, 0x00000000u}, + {0x807fffffu, 0x00000000u}, {0x00000000u, 0x00000000u}, {0x80000000u, 0x80000000u}, {0x7f800000u, 0x00000000u}, + {0xff800000u, 0x00000000u}, {0x7fc00000u, 0x00000000u}, {0x7f7fffffu, 0x00000000u}, {0x3f800000u, 0x00000000u}, + {0xbf800000u, 0x3f800000u}, {0x00c00000u, 0x00000000u}, {0x80ffffffu, 0x80000000u}, {0x01000001u, 0x00000000u}, + {0x3f7fffffu, 0x00000000u}, {0x40490fdbu, 0x00000000u}, {0x00fffffeu, 0x00000000u}, {0x81800000u, 0x00000000u}, + {0x017ffffeu, 0x00000000u}, {0x01800001u, 0x00000000u}, {0x0080ffffu, 0x00000000u}, {0x7f000000u, 0x00000000u}, + {0x00800000u, 0x3f800000u}, {0x817fffffu, 0x80000000u}, {0x017fffffu, 0x00800000u}, {0x017fffffu, 0x80000001u} +}; + +constexpr const char* Names[8] = { + "v_mul_f32 2.0", + "v_mul_f32 0.5", + "v_mul_f32 0x3f7ffffe", + "v_mul_f32 0x3f800000", + "v_mul_f32 0x00400000", + "v_mul_legacy_f32 4.0", + "v_fmamk_f32 0x3e800000", + "v_fma_f32 -|2.0|" +}; + +struct Pinned { + std::uint32_t tid; + std::uint32_t pair; + std::uint32_t value; +}; + +constexpr std::array TinyProducts{{ + {0u, 2u, 0x00000000u}, + {3u, 1u, 0x00000000u}, + {18u, 1u, 0x80000000u}, + {5u, 6u, 0x00000000u}, + {29u, 6u, 0x80000000u}, + {31u, 6u, 0x00000000u}, +}}; + +constexpr Pinned TinyOnlyBeforeRounding{2u, 2u, 0x00800000u}; + +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; +} + +bool IsNan(std::uint32_t value) { + return (value & 0x7fffffffu) > 0x7f800000u; +} + +void Run(AgcDriver::VulkanDevice& device, const std::optional& floatMode) { + Input.fill(0u); + 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::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 = floatMode; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const char* mode) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t* out = &Output[tid * Results]; + for (std::uint32_t pair = 0; pair < 8u; ++pair) { + const std::uint32_t constant = out[pair * 2u]; + const std::uint32_t reg = out[pair * 2u + 1u]; + const std::string where = std::string("f32 constant factor: lane ") + std::to_string(tid) + " " + mode + " " + Names[pair]; + if (IsNan(reg)) { + Require(IsNan(constant), where + " is " + Hex(constant) + " with a constant and a NaN with a register"); + continue; + } + Require(constant == reg, where + " is " + Hex(constant) + " with a constant and " + Hex(reg) + " with a register"); + } + } +} + +void CheckTinyProducts(const char* mode) { + for (const Pinned& pinned : TinyProducts) { + const std::uint32_t actual = Output[pinned.tid * Results + pinned.pair * 2u]; + Require(actual == pinned.value, std::string("f32 constant factor: lane ") + std::to_string(pinned.tid) + " " + mode + " " + Names[pinned.pair] + " is " + Hex(actual) + ", expected the flushed tiny product " + Hex(pinned.value)); + } +} + +void CheckTininessAfterRounding(const char* mode) { + const Pinned& pinned = TinyOnlyBeforeRounding; + const std::uint32_t actual = Output[pinned.tid * Results + pinned.pair * 2u]; + Require(actual == pinned.value, std::string("f32 tininess after rounding: lane ") + std::to_string(pinned.tid) + " " + mode + " " + Names[pinned.pair] + " is " + Hex(actual) + ", expected " + Hex(pinned.value) + ": the exact product 0x3f7ffffe * 0x00800001 is below 2^-126 and rounds to it, and RDNA2 (gfx1036) keeps it (#2682), where a check before rounding flushes it"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device, std::nullopt); + Check("no float mode"); + CheckTinyProducts("no float mode"); + CheckTininessAfterRounding("no float mode"); + Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}); + Check("IEEE=0 f32 denormals flushed"); + CheckTinyProducts("IEEE=0 f32 denormals flushed"); + CheckTininessAfterRounding("IEEE=0 f32 denormals flushed"); + Run(*device, ShaderRecompiler::ShaderFloatMode{0x00u, false, true, false}); + Check("IEEE=1 all denormals flushed"); + CheckTinyProducts("IEEE=1 all denormals flushed"); + CheckTininessAfterRounding("IEEE=1 all denormals flushed"); + Run(*device, ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}); + Check("f32 denormals kept"); + std::puts("f32 constant factor 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 37427d2692..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 { @@ -21,16 +22,20 @@ using ShaderRecompiler::ShaderStage; constexpr std::uint32_t Threads = 32; constexpr std::uint32_t Inputs = 4; -constexpr std::uint32_t Results = 16; +constexpr std::uint32_t Results = 19; +constexpr std::uint32_t TinyResults = 16; +constexpr std::uint32_t TinyProductLane = 20; +constexpr std::uint32_t SplitTinyProductSum = 0x00800000u; alignas(256) std::array Input{}; alignas(256) std::array Output{}; -alignas(256) constexpr std::array Code{ - 0x34020084u, 0x34060086u, 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, 0xe0781000u, 0x80010a03u, 0xe0781010u, 0x80010e03u, - 0xe0781020u, 0x80011203u, 0xe0781030u, 0x80011603u, 0xbf810000u, +alignas(256) constexpr std::array Code{ + 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 Rows[32][4] = { @@ -67,41 +72,41 @@ constexpr std::uint32_t Rows[32][4] = { {0x80000001u, 0x80000001u, 0x80000001u, 0x00000000u}, {0x00000001u, 0x00000001u, 0x00000001u, 0x00000000u} }; -constexpr std::uint32_t Expected[32][16] = { - {0x3f800000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x80000000u}, - {0x3f800000u, 0x3f800000u, 0x80000000u, 0x00000000u, 0x00800000u, 0x00800000u, 0x80000000u, 0x3f800000u, 0x00800000u, 0x80000000u, 0x3f800000u, 0x80000000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x80000000u}, - {0x3f800000u, 0xbf800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x3f000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x80000000u}, - {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u}, - {0x3f7fffffu, 0xbf7fffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00800000u, 0x3f7fffffu, 0x00800000u, 0x00000000u, 0x3f800000u, 0x3f7fffffu, 0x00000000u, 0x00000000u, 0xffffff83u, 0x80000000u}, - {0xbf7fffffu, 0x3f7fffffu, 0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0xbf7fffffu, 0x00800000u, 0x80000000u, 0xbf800000u, 0x3f800000u, 0xbf7fffffu, 0x00000001u, 0xffffffffu, 0xffffff83u, 0x80000000u}, - {0x3f7fffffu, 0xbf7fffffu, 0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u, 0x00800001u, 0x3f7fffffu, 0x00800001u, 0x00000000u, 0x3f800000u, 0x3f7fffffu, 0x00000000u, 0x00000000u, 0xffffff83u, 0x80800000u}, - {0x3f000000u, 0xbf000000u, 0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u, 0x01000000u, 0x3f000000u, 0x01000000u, 0x00000000u, 0x3f800000u, 0x3f000000u, 0x00000000u, 0x00000000u, 0xffffff84u, 0x80800000u}, - {0x80000000u, 0x81000000u, 0x80000000u, 0x80000000u, 0x00800000u, 0x00800000u, 0x80800001u, 0x00800000u, 0x00800000u, 0x00000000u, 0x80000000u, 0x3f000000u, 0x00000000u, 0x00000000u, 0xffffff83u, 0x00000000u}, - {0x01000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x3f800000u, 0x00800000u, 0x3f800000u, 0x00800001u, 0x00000000u, 0x3f800000u, 0x3f000001u, 0x00000000u, 0x00000000u, 0xffffff83u, 0x80000000u}, - {0x00000000u, 0x80000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, - {0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u}, - {0x7f800000u, 0xff800000u, 0xffc00000u, 0x00000000u, 0xffc00000u, 0xffc00000u, 0x00000000u, 0x7f800000u, 0x3f800000u, 0x7f800000u, 0x00000000u, 0x7f800000u, 0x00000000u, 0x7fffffffu, 0x00000000u, 0xffc00000u}, - {0xff800000u, 0xff800000u, 0xffc00000u, 0x00000000u, 0xffc00000u, 0xffc00000u, 0xff800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xff800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffc00000u}, - {0x7fc00000u, 0xffc00000u, 0x7fc00000u, 0x00000000u, 0x7fc00000u, 0x7fc00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x7fc00000u, 0x00000000u, 0x7fc00000u, 0x00000000u, 0x7fffffffu, 0x00000000u, 0x7fc00000u}, - {0x40000000u, 0xc0000000u, 0x00000000u, 0x00000000u, 0x80800000u, 0x80800000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x3f000000u, 0x00000000u, 0x00000002u, 0x00000000u, 0x80000000u}, - {0x40000000u, 0x00000000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0xbf800000u}, - {0xbf800000u, 0x3f800000u, 0x80000000u, 0x00000000u, 0x3f000000u, 0x3f000000u, 0xbf800000u, 0x3f000000u, 0x00000000u, 0xbf800000u, 0x00000000u, 0xbf000000u, 0x00000001u, 0xffffffffu, 0x00000000u, 0x80000000u}, - {0x34800000u, 0x00000000u, 0x28800000u, 0x28800000u, 0x28800000u, 0x28800000u, 0x34000000u, 0x34000000u, 0x34000000u, 0x00000000u, 0x3f800000u, 0x3f000000u, 0x00000000u, 0x00000000u, 0xffffffeau, 0xa8800000u}, - {0x20000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x1f800000u, 0x1f800000u, 0x1f800000u, 0x00000000u, 0x3f800000u, 0x3f000000u, 0x00000000u, 0x00000000u, 0xffffffc1u, 0x80000000u}, - {0x207fffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00ffffffu, 0x00ffffffu, 0x1fffffffu, 0x1fffffffu, 0x1fffffffu, 0x00000000u, 0x3f800000u, 0x3f7fffffu, 0x00000000u, 0x00000000u, 0xffffffc1u, 0x80000000u}, - {0x20200000u, 0x9fc00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x1f000000u, 0x20000000u, 0x1f000000u, 0x00000000u, 0x3f800000u, 0x3f000000u, 0x00000000u, 0x00000000u, 0xffffffc0u, 0x80000000u}, - {0x20400000u, 0x9f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x1f7fffffu, 0x20000000u, 0x1f7fffffu, 0x00000000u, 0x3f800000u, 0x3f000000u, 0x00000000u, 0x00000000u, 0xffffffc0u, 0x80000000u}, - {0x20400001u, 0x9f800002u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x1f7fffffu, 0x20000001u, 0x1f7fffffu, 0x00000000u, 0x3f800000u, 0x3f000001u, 0x00000000u, 0x00000000u, 0xffffffc0u, 0x80000000u}, - {0x9f800000u, 0x20400000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0xa0000000u, 0x1f7fffffu, 0x80000000u, 0xbf800000u, 0x3f800000u, 0xbf000000u, 0x00000001u, 0xffffffffu, 0xffffffc0u, 0x80000000u}, - {0x00000000u, 0xc0000000u, 0xbf800000u, 0xbf800000u, 0xbf800000u, 0xbf800000u, 0xbf800000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0xbf800000u, 0x3f000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x3f800000u}, - {0x3f000000u, 0x3f000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00800000u, 0x3f000000u, 0x00800000u, 0x00000000u, 0x3f800000u, 0x3f000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x80000000u}, - {0x3f7fffffu, 0x3f7fffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00800000u, 0x3f7fffffu, 0x00800000u, 0x00000000u, 0x3f800000u, 0x3f000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x80000000u}, - {0x41200000u, 0xc1200000u, 0x00000000u, 0x00000000u, 0x3dcccccdu, 0x3dcccccdu, 0x00000000u, 0x41200000u, 0x3dcccccdu, 0x41200000u, 0x00000000u, 0x3f200000u, 0x00000000u, 0x0000000au, 0x00000000u, 0x80000000u}, - {0xc0490fdbu, 0x40490fdbu, 0x00000000u, 0x00000000u, 0x40490fdbu, 0x40490fdbu, 0xc0490fdbu, 0x40490fdbu, 0x80000000u, 0xc0800000u, 0x80000000u, 0xbf490fdbu, 0x00000001u, 0xfffffffcu, 0x00000000u, 0x00000000u}, - {0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, - {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u} +constexpr std::uint32_t Expected[32][19] = { + {0x3f800000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x80000000u, 0x00000000u, 0xff7fffffu, 0x00000000u}, + {0x3f800000u, 0x3f800000u, 0x80000000u, 0x00000000u, 0x00800000u, 0x00800000u, 0x80000000u, 0x3f800000u, 0x00800000u, 0x80000000u, 0x3f800000u, 0x80000000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x80000000u, 0x00400000u, 0x00000000u, 0x00000000u}, + {0x3f800000u, 0xbf800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x3f000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x80000000u, 0xffc00000u, 0xff7fffffu, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x7f800000u, 0x00000000u, 0x00000000u}, + {0x3f7fffffu, 0xbf7fffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00800000u, 0x3f7fffffu, 0x00800000u, 0x00000000u, 0x3f800000u, 0x3f7fffffu, 0x00000000u, 0x00000000u, 0xffffff83u, 0x80000000u, 0x00000000u, 0xff7fffffu, 0x00000000u}, + {0xbf7fffffu, 0x3f7fffffu, 0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0xbf7fffffu, 0x00800000u, 0x80000000u, 0xbf800000u, 0x3f800000u, 0xbf7fffffu, 0x00000001u, 0xffffffffu, 0xffffff83u, 0x80000000u, 0x80000000u, 0xff7fffffu, 0x80000000u}, + {0x3f7fffffu, 0xbf7fffffu, 0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u, 0x00800001u, 0x3f7fffffu, 0x00800001u, 0x00000000u, 0x3f800000u, 0x3f7fffffu, 0x00000000u, 0x00000000u, 0xffffff83u, 0x80800000u, 0x00000000u, 0xff7fffffu, 0x00800000u}, + {0x3f000000u, 0xbf000000u, 0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u, 0x01000000u, 0x3f000000u, 0x01000000u, 0x00000000u, 0x3f800000u, 0x3f000000u, 0x00000000u, 0x00000000u, 0xffffff84u, 0x80800000u, 0x80000000u, 0xff7fffffu, 0x00800000u}, + {0x80000000u, 0x81000000u, 0x80000000u, 0x80000000u, 0x00800000u, 0x00800000u, 0x80800001u, 0x00800000u, 0x00800000u, 0x00000000u, 0x80000000u, 0x3f000000u, 0x00000000u, 0x00000000u, 0xffffff83u, 0x00000000u, 0x80800000u, 0x80000000u, 0x80000000u}, + {0x01000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x3f800000u, 0x00800000u, 0x3f800000u, 0x00800001u, 0x00000000u, 0x3f800000u, 0x3f000001u, 0x00000000u, 0x00000000u, 0xffffff83u, 0x80000000u, 0x00800001u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x80000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffc00000u, 0xff7fffffu, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0xffc00000u, 0xff7fffffu, 0x00000000u}, + {0x7f800000u, 0xff800000u, 0xffc00000u, 0x00000000u, 0xffc00000u, 0xffc00000u, 0x00000000u, 0x7f800000u, 0x3f800000u, 0x7f800000u, 0x00000000u, 0x7f800000u, 0x00000000u, 0x7fffffffu, 0x00000000u, 0xffc00000u, 0x7f800000u, 0x00000000u, 0x00000000u}, + {0xff800000u, 0xff800000u, 0xffc00000u, 0x00000000u, 0xffc00000u, 0xffc00000u, 0xff800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xff800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffc00000u, 0x80000000u, 0xff7fffffu, 0xff7fffffu}, + {0x7fc00000u, 0xffc00000u, 0x7fc00000u, 0x00000000u, 0x7fc00000u, 0x7fc00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x7fc00000u, 0x00000000u, 0x7fc00000u, 0x00000000u, 0x7fffffffu, 0x00000000u, 0x7fc00000u, 0xffc00000u, 0xff7fffffu, 0x00000000u}, + {0x40000000u, 0xc0000000u, 0x00000000u, 0x00000000u, 0x80800000u, 0x80800000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x3f000000u, 0x00000000u, 0x00000002u, 0x00000000u, 0x80000000u, 0xff800000u, 0xff7fffffu, 0x00000000u}, + {0x40000000u, 0x00000000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0xbf800000u, 0x80000000u, 0xff7fffffu, 0x3f800000u}, + {0xbf800000u, 0x3f800000u, 0x80000000u, 0x00000000u, 0x3f000000u, 0x3f000000u, 0xbf800000u, 0x3f000000u, 0x00000000u, 0xbf800000u, 0x00000000u, 0xbf000000u, 0x00000001u, 0xffffffffu, 0x00000000u, 0x80000000u, 0x7f800000u, 0x00000000u, 0x00000000u}, + {0x34800000u, 0x00000000u, 0x28800000u, 0x28800000u, 0x28800000u, 0x28800000u, 0x34000000u, 0x34000000u, 0x34000000u, 0x00000000u, 0x3f800000u, 0x3f000000u, 0x00000000u, 0x00000000u, 0xffffffeau, 0xa8800000u, 0x00000000u, 0xff7fffffu, 0x28800000u}, + {0x20000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x1f800000u, 0x1f800000u, 0x1f800000u, 0x00000000u, 0x3f800000u, 0x3f000000u, 0x00000000u, 0x00000000u, 0xffffffc1u, 0x80000000u, 0x00000000u, 0xff7fffffu, 0x00000000u}, + {0x207fffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00ffffffu, 0x00ffffffu, 0x1fffffffu, 0x1fffffffu, 0x1fffffffu, 0x00000000u, 0x3f800000u, 0x3f7fffffu, 0x00000000u, 0x00000000u, 0xffffffc1u, 0x80000000u, 0x1fffffffu, 0x00000000u, 0x00000000u}, + {0x20200000u, 0x9fc00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x1f000000u, 0x20000000u, 0x1f000000u, 0x00000000u, 0x3f800000u, 0x3f000000u, 0x00000000u, 0x00000000u, 0xffffffc0u, 0x80000000u, 0x00000000u, 0xff7fffffu, 0x00000000u}, + {0x20400000u, 0x9f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x1f7fffffu, 0x20000000u, 0x1f7fffffu, 0x00000000u, 0x3f800000u, 0x3f000000u, 0x00000000u, 0x00000000u, 0xffffffc0u, 0x80000000u, 0x00000000u, 0xff7fffffu, 0x00000000u}, + {0x20400001u, 0x9f800002u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x1f7fffffu, 0x20000001u, 0x1f7fffffu, 0x00000000u, 0x3f800000u, 0x3f000001u, 0x00000000u, 0x00000000u, 0xffffffc0u, 0x80000000u, 0x00000000u, 0xff7fffffu, 0x00000000u}, + {0x9f800000u, 0x20400000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0xa0000000u, 0x1f7fffffu, 0x80000000u, 0xbf800000u, 0x3f800000u, 0xbf000000u, 0x00000001u, 0xffffffffu, 0xffffffc0u, 0x80000000u, 0x80000000u, 0xff7fffffu, 0x80000000u}, + {0x00000000u, 0xc0000000u, 0xbf800000u, 0xbf800000u, 0xbf800000u, 0xbf800000u, 0xbf800000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0xbf800000u, 0x3f000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x3f800000u, 0x80000000u, 0xff7fffffu, 0xbf800000u}, + {0x3f000000u, 0x3f000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00800000u, 0x3f000000u, 0x00800000u, 0x00000000u, 0x3f800000u, 0x3f000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0xff7fffffu, 0x00000000u}, + {0x3f7fffffu, 0x3f7fffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00800000u, 0x3f7fffffu, 0x00800000u, 0x00000000u, 0x3f800000u, 0x3f000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0xff7fffffu, 0x00000000u}, + {0x41200000u, 0xc1200000u, 0x00000000u, 0x00000000u, 0x3dcccccdu, 0x3dcccccdu, 0x00000000u, 0x41200000u, 0x3dcccccdu, 0x41200000u, 0x00000000u, 0x3f200000u, 0x00000000u, 0x0000000au, 0x00000000u, 0x80000000u, 0x7f800000u, 0x00000000u, 0x00000000u}, + {0xc0490fdbu, 0x40490fdbu, 0x00000000u, 0x00000000u, 0x40490fdbu, 0x40490fdbu, 0xc0490fdbu, 0x40490fdbu, 0x80000000u, 0xc0800000u, 0x80000000u, 0xbf490fdbu, 0x00000001u, 0xfffffffcu, 0x00000000u, 0x00000000u, 0xff800000u, 0x00000000u, 0x00000000u}, + {0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffc00000u, 0xff7fffffu, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0xffc00000u, 0xff7fffffu, 0x00000000u} }; -constexpr const char* Names[16] = { +constexpr const char* Names[19] = { "v_add_f32 v10, v4, v5", "v_sub_f32 v11, v5, v4", "v_mul_f32 v12, v4, v5", @@ -117,12 +122,254 @@ constexpr const char* Names[16] = { "v_cmp_lt_f32 v4, v5", "v_cvt_flr_i32_f32 v23, v4", "v_frexp_exp_i32_f32 v24, v5", - "v_mul_f32 v25, |v4|, -v5" + "v_mul_f32 v25, |v4|, -v5", + "v_div_fixup_f32 v26, v4, v5, v6", + "v_mullit_f32 v27, v4, v5, v6", + "v_mullit_f32 v28, v4, v5, 1.0" }; -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 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[32][4] = { + {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} +}; +constexpr std::uint32_t TinyExpected[32][8] = { + {0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u, 0x80800000u}, + {0x80800000u, 0x80800000u, 0x80800000u, 0x80800000u, 0x80800000u, 0x80800000u, 0x80800000u, 0x00800000u}, + {0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u, 0x3f7ffffcu, 0x00800000u, 0x00800000u, 0x80800000u}, + {0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u, 0x008003e5u, 0x00800000u, 0x00800000u, 0x80800000u}, + {0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u, 0x00f7fffeu, 0x00800000u, 0x00800000u, 0x80800000u}, + {0x80800000u, 0x80800000u, 0x80800000u, 0x80800000u, 0x80f7fffeu, 0x80800000u, 0x80800000u, 0x00800000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00800f94u, 0x00000000u, 0x00000000u, 0x80000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00800000u, 0x00000000u, 0x00000000u, 0x80000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00fffffdu, 0x00000000u, 0x00000000u, 0x80000000u}, + {0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u, 0x008005a7u, 0x00800000u, 0x00800000u, 0x80800000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00f7fffdu, 0x00000000u, 0x00000000u, 0x80000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00fffffdu, 0x00000000u, 0x00000000u, 0x80000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x008005a8u, 0x00000000u, 0x00000000u, 0x80000000u}, + {0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u, 0x80800000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u}, + {0x80800000u, 0x80800000u, 0x80800000u, 0x80800000u, 0x80800000u, 0x80800000u, 0x80800000u, 0x00800000u}, + {0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u, 0x80800000u}, + {0x80800000u, 0x80800000u, 0x80800000u, 0x80800000u, 0xbf7ffffcu, 0x80800000u, 0x80800000u, 0x00800000u}, + {0x80800000u, 0x80800000u, 0x80800000u, 0x80800000u, 0x808003e5u, 0x80800000u, 0x80800000u, 0x00800000u}, + {0x80800000u, 0x80800000u, 0x80800000u, 0x80800000u, 0x80f7fffeu, 0x80800000u, 0x80800000u, 0x00800000u}, + {0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u, 0x00f7fffeu, 0x00800000u, 0x00800000u, 0x80800000u}, + {0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x80800f94u, 0x80000000u, 0x80000000u, 0x00000000u}, + {0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x80800000u, 0x80000000u, 0x80000000u, 0x00000000u}, + {0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x80fffffdu, 0x80000000u, 0x80000000u, 0x00000000u}, + {0x80800000u, 0x80800000u, 0x80800000u, 0x80800000u, 0x808005a7u, 0x80800000u, 0x80800000u, 0x00800000u}, + {0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x80f7fffdu, 0x80000000u, 0x80000000u, 0x00000000u}, + {0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x80fffffdu, 0x80000000u, 0x80000000u, 0x00000000u}, + {0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x808005a8u, 0x80000000u, 0x80000000u, 0x00000000u}, + {0x80800000u, 0x80800000u, 0x80800000u, 0x80800000u, 0x80800000u, 0x80800000u, 0x80800000u, 0x00800000u}, + {0x80000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x00000000u}, + {0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x00000000u} +}; +constexpr std::uint32_t TinyDirected[3][32][2] = { + { + {0x00800000u, 0x80000000u}, {0x80000000u, 0x00800000u}, {0x00800000u, 0x80000000u}, {0x00800000u, 0x80000000u}, + {0x00800000u, 0x80000000u}, {0x80000000u, 0x00800000u}, {0x00800000u, 0x80000000u}, {0x00000000u, 0x80000000u}, + {0x00000000u, 0x80000000u}, {0x00800000u, 0x80000000u}, {0x00000000u, 0x80000000u}, {0x00000000u, 0x80000000u}, + {0x00800000u, 0x80000000u}, {0x00800000u, 0x80000000u}, {0x00000000u, 0x80000000u}, {0x00000000u, 0x80000000u}, + {0x80000000u, 0x00800000u}, {0x00800000u, 0x80000000u}, {0x80000000u, 0x00800000u}, {0x80000000u, 0x00800000u}, + {0x80000000u, 0x00800000u}, {0x00800000u, 0x80000000u}, {0x80000000u, 0x00800000u}, {0x80000000u, 0x00000000u}, + {0x80000000u, 0x00000000u}, {0x80000000u, 0x00800000u}, {0x80000000u, 0x00000000u}, {0x80000000u, 0x00000000u}, + {0x80000000u, 0x00800000u}, {0x80000000u, 0x00800000u}, {0x80000000u, 0x00000000u}, {0x80000000u, 0x00000000u} + }, + { + {0x00000000u, 0x80800000u}, {0x80800000u, 0x00000000u}, {0x00000000u, 0x80800000u}, {0x00000000u, 0x80800000u}, + {0x00000000u, 0x80800000u}, {0x80800000u, 0x00000000u}, {0x00000000u, 0x80800000u}, {0x00000000u, 0x80000000u}, + {0x00000000u, 0x80000000u}, {0x00000000u, 0x80800000u}, {0x00000000u, 0x80000000u}, {0x00000000u, 0x80000000u}, + {0x00000000u, 0x80800000u}, {0x00000000u, 0x80800000u}, {0x00000000u, 0x80000000u}, {0x00000000u, 0x80000000u}, + {0x80800000u, 0x00000000u}, {0x00000000u, 0x80800000u}, {0x80800000u, 0x00000000u}, {0x80800000u, 0x00000000u}, + {0x80800000u, 0x00000000u}, {0x00000000u, 0x80800000u}, {0x80800000u, 0x00000000u}, {0x80000000u, 0x00000000u}, + {0x80000000u, 0x00000000u}, {0x80800000u, 0x00000000u}, {0x80000000u, 0x00000000u}, {0x80000000u, 0x00000000u}, + {0x80800000u, 0x00000000u}, {0x80800000u, 0x00000000u}, {0x80000000u, 0x00000000u}, {0x80000000u, 0x00000000u} + }, + { + {0x00000000u, 0x80000000u}, {0x80000000u, 0x00000000u}, {0x00000000u, 0x80000000u}, {0x00000000u, 0x80000000u}, + {0x00000000u, 0x80000000u}, {0x80000000u, 0x00000000u}, {0x00000000u, 0x80000000u}, {0x00000000u, 0x80000000u}, + {0x00000000u, 0x80000000u}, {0x00000000u, 0x80000000u}, {0x00000000u, 0x80000000u}, {0x00000000u, 0x80000000u}, + {0x00000000u, 0x80000000u}, {0x00000000u, 0x80000000u}, {0x00000000u, 0x80000000u}, {0x00000000u, 0x80000000u}, + {0x80000000u, 0x00000000u}, {0x00000000u, 0x80000000u}, {0x80000000u, 0x00000000u}, {0x80000000u, 0x00000000u}, + {0x80000000u, 0x00000000u}, {0x00000000u, 0x80000000u}, {0x80000000u, 0x00000000u}, {0x80000000u, 0x00000000u}, + {0x80000000u, 0x00000000u}, {0x80000000u, 0x00000000u}, {0x80000000u, 0x00000000u}, {0x80000000u, 0x00000000u}, + {0x80000000u, 0x00000000u}, {0x80000000u, 0x00000000u}, {0x80000000u, 0x00000000u}, {0x80000000u, 0x00000000u} + } +}; +constexpr std::uint32_t TinyLegacyExpected[32] = { + 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x80000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x80000000u, + 0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x00000000u, 0x80000000u +}; +constexpr const char* TinyLegacyNames[2] = { + "v_mad_legacy_f32 v18, v4, v5, v6", + "v_mac_legacy_f32 v19, v4, v5" +}; +constexpr const char* TinyNames[8] = { + "v_mul_f32 v10, v4, v5", + "v_mul_legacy_f32 v11, v4, v5", + "v_fma_f32 v12, v4, v5, v6", + "v_fmac_f32 v13, v4, v5", + "v_fmamk_f32 v14, v4, 0x3f7ffffe, v6", + "v_fmaak_f32 v15, v4, v5, 0x80000000", + "v_fma_mix_f32 v16, v4, v5, v6", + "v_mul_f32 v17, -v4, v5" +}; + +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}, +}; +constexpr std::uint32_t MixedTinyExpected[16][3] = { + {0x00800000u, 0x00000000u, 0x00801001u}, + {0x80800000u, 0x80000000u, 0x80801001u}, + {0x00000000u, 0x00000000u, 0x00800fffu}, + {0x00000000u, 0x00000000u, 0x00801000u}, + {0x00800000u, 0x00800000u, 0x00801001u}, + {0x00800001u, 0x00800001u, 0x00801002u}, + {0x80000000u, 0x80000000u, 0x80800fffu}, + {0x80000000u, 0x80000000u, 0x80801000u}, + {0x80800000u, 0x80800000u, 0x80801001u}, + {0x80800001u, 0x80800001u, 0x80801002u}, + {0x80000000u, 0x80000000u, 0x00800fffu}, + {0x80000000u, 0x80000000u, 0x00801000u}, + {0x80800000u, 0x80800000u, 0x00801001u}, + {0x80800001u, 0x80800001u, 0x00801002u}, + {0x00000000u, 0x00000000u, 0x00800000u}, + {0x80000000u, 0x80000000u, 0x80800000u}, +}; +constexpr std::uint32_t MixedTinyDirected[3][16][2] = { + { + {0x00800000u, 0x00000000u}, + {0x80000000u, 0x80000000u}, + {0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u}, + {0x00800000u, 0x00800000u}, + {0x00800001u, 0x00800001u}, + {0x80000000u, 0x80000000u}, + {0x80000000u, 0x80000000u}, + {0x80000000u, 0x80000000u}, + {0x80800000u, 0x80800000u}, + {0x80000000u, 0x80000000u}, + {0x80000000u, 0x80000000u}, + {0x80000000u, 0x80000000u}, + {0x80800000u, 0x80800000u}, + {0x00000000u, 0x00000000u}, + {0x80000000u, 0x80000000u}, + }, + { + {0x00000000u, 0x00000000u}, + {0x80800000u, 0x80000000u}, + {0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u}, + {0x00800000u, 0x00800000u}, + {0x80000000u, 0x80000000u}, + {0x80000000u, 0x80000000u}, + {0x80800000u, 0x80800000u}, + {0x80800001u, 0x80800001u}, + {0x80000000u, 0x80000000u}, + {0x80000000u, 0x80000000u}, + {0x80800000u, 0x80800000u}, + {0x80800001u, 0x80800001u}, + {0x00000000u, 0x00000000u}, + {0x80000000u, 0x80000000u}, + }, + { + {0x00000000u, 0x00000000u}, + {0x80000000u, 0x80000000u}, + {0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u}, + {0x00800000u, 0x00800000u}, + {0x80000000u, 0x80000000u}, + {0x80000000u, 0x80000000u}, + {0x80000000u, 0x80000000u}, + {0x80800000u, 0x80800000u}, + {0x80000000u, 0x80000000u}, + {0x80000000u, 0x80000000u}, + {0x80000000u, 0x80000000u}, + {0x80800000u, 0x80800000u}, + {0x00000000u, 0x00000000u}, + {0x80000000u, 0x80000000u}, + }, +}; +constexpr std::uint32_t MixedAddendDirected[3][2] = { + {0x00800000u, 0x80000000u}, + {0x00000000u, 0x80800000u}, + {0x00000000u, 0x80000000u}, +}; +constexpr const char* MixedTinyNames[8] = { + "v_fma_mix_f32 v10, v4, v5, v6 op_sel_hi:[0,1,0]", + "v_fma_mix_f32 v11, v5, v4, v6 op_sel_hi:[1,0,0]", + "v_fma_mix_f32 v12, v4, v5, v6 op_sel:[0,1,0] op_sel_hi:[0,1,0]", + "v_fma_mix_f32 v13, v5, v4, v6 op_sel:[1,0,0] op_sel_hi:[1,0,0]", + "v_fma_mix_f32 v14, v4, v5, v7 op_sel_hi:[0,1,1]", + "v_fma_mix_f32 v15, v5, v4, v7 op_sel_hi:[1,0,1]", + "v_fma_mix_f32 v16, v4, v8, v7 op_sel_hi:[0,0,1]", + "v_fma_mix_f32 v17, v4, v8, v7 op_sel:[0,0,1] op_sel_hi:[0,0,1]", +}; std::array BufferDescriptor(const void* data, std::uint32_t bytes) { const auto address = reinterpret_cast(data); @@ -143,15 +390,39 @@ bool IsNan(std::uint32_t value) { return (value & 0x7fffffffu) > 0x7f800000u; } -void Run(AgcDriver::VulkanDevice& device, const std::optional& floatMode) { - for (std::uint32_t tid = 0; tid < Threads; ++tid) Fill(tid, &Input[tid * Inputs]); +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); 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{ @@ -162,16 +433,23 @@ void Run(AgcDriver::VulkanDevice& device, const std::optional(code.data())); device.WaitIdle(); } void Check(const char* mode) { + const std::uint32_t* tinyProduct = &Output[TinyProductLane * Results]; + const bool hostSplits = tinyProduct[4] == SplitTinyProductSum && tinyProduct[5] == SplitTinyProductSum; + if (hostSplits) { + std::printf("the device splits fma into a multiply and an add, so the fused sum of lane %u is not checked (%s)\n", TinyProductLane, 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) { - const bool floatResult = i < 12u || i == 15u; + for (std::uint32_t i = 0; i < Results; ++i) { + if (hostSplits && tid == TinyProductLane && (i == 4u || i == 5u)) continue; + const bool floatResult = i < 12u || i >= 15u; if (floatResult && IsNan(Expected[tid][i])) { Require(IsNan(out[i]), std::string("f32 denormal flush: lane ") + std::to_string(tid) + " " + mode + " " + Names[i] + " is " + Hex(out[i]) + ", expected a NaN"); continue; @@ -181,26 +459,105 @@ void Check(const char* mode) { } } +void CheckTiny(const char* mode) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t i = 0; i < 8; ++i) Expect(tid, Output[tid * TinyResults + i], TinyExpected[tid][i], (std::string(mode) + " " + TinyNames[i]).c_str()); + } +} + +void CheckMixedTiny(const std::string& mode, std::uint32_t round = 0u) { + for (std::uint32_t tid = 0; tid < std::size(MixedTinyExpected); ++tid) { + for (std::uint32_t i = 0; i < std::size(MixedTinyNames); ++i) { + const bool highFactor = i == 2u || i == 3u; + std::uint32_t expected = MixedTinyExpected[tid][i >= 6u ? 2u : highFactor ? 1u : 0u]; + if (round != 0u) { + if (i >= 6u) { + if (tid < 14u) continue; + expected = MixedAddendDirected[round - 1u][tid - 14u]; + } else { + if ((MixedTinyRows[tid][0] & 0x7fffffffu) == 0x00801003u) continue; + expected = MixedTinyDirected[round - 1u][tid][highFactor ? 1u : 0u]; + } + } + Expect(tid, Output[tid * TinyResults + i], expected, (mode + " " + MixedTinyNames[i]).c_str()); + } + } +} + +void CheckTinyLegacy(const std::string& mode) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t i = 0; i < 2; ++i) Expect(tid, Output[tid * TinyResults + 8u + i], TinyLegacyExpected[tid], (mode + " " + TinyLegacyNames[i]).c_str()); + } +} + +void CheckTinyDirected(std::uint32_t round) { + const std::string mode = "round mode " + std::to_string(round) + " "; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Expect(tid, Output[tid * TinyResults], TinyDirected[round - 1u][tid][0], (mode + TinyNames[0]).c_str()); + Expect(tid, Output[tid * TinyResults + 7u], TinyDirected[round - 1u][tid][1], (mode + TinyNames[7]).c_str()); + } +} + } 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"); - 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; + 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)); + } + 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/F32Dot2Rounding.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F32Dot2Rounding.cpp new file mode 100644 index 0000000000..2c6e1cb9ad --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F32Dot2Rounding.cpp @@ -0,0 +1,278 @@ +#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 Capacity = 1u << 18; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +struct Case { + std::uint32_t a; + std::uint32_t b; + std::uint32_t c; + std::uint32_t expected; +}; + +class Exact { +public: + void Add(std::uint64_t magnitude, std::uint32_t shift, bool negative) { + std::array term{}; + for (std::uint32_t bit = 0; bit < 64u; ++bit) { + if (((magnitude >> bit) & 1u) == 0u) continue; + const std::uint32_t position = bit + shift; + term[position / 32u] |= 1u << (position % 32u); + } + if (negative) { + for (auto& limb : term) limb = ~limb; + Accumulate(term, 1u); + } else { + Accumulate(term, 0u); + } + } + + bool Zero() const { return std::all_of(limbs.begin(), limbs.end(), [](std::uint32_t limb) { return limb == 0u; }); } + + std::optional Rounded() const { + auto magnitude = limbs; + const bool negative = (magnitude[Limbs - 1u] >> 31u) != 0u; + if (negative) { + for (auto& limb : magnitude) limb = ~limb; + std::uint64_t carry = 1u; + for (auto& limb : magnitude) { + const std::uint64_t sum = std::uint64_t{limb} + carry; + limb = static_cast(sum); + carry = sum >> 32u; + } + } + int top = -1; + for (int bit = static_cast(Limbs * 32u) - 1; bit >= 0; --bit) { + if (Bit(magnitude, static_cast(bit))) { + top = bit; + break; + } + } + if (top < 23) return std::nullopt; + std::uint64_t mantissa = 0; + for (int bit = top; bit > top - 24; --bit) mantissa = (mantissa << 1u) | (Bit(magnitude, static_cast(bit)) ? 1u : 0u); + const int guardBit = top - 24; + const bool guard = guardBit >= 0 && Bit(magnitude, static_cast(guardBit)); + bool sticky = false; + for (int bit = guardBit - 1; bit >= 0 && !sticky; --bit) sticky = Bit(magnitude, static_cast(bit)); + int exponent = top - 22; + if (guard && (sticky || (mantissa & 1u) != 0u)) { + if (++mantissa == (1u << 24u)) { + mantissa >>= 1u; + ++exponent; + } + } + if (exponent < 1 || exponent > 254) return std::nullopt; + return (negative ? 0x80000000u : 0u) | (static_cast(exponent) << 23u) | static_cast(mantissa & 0x7fffffu); + } + +private: + static constexpr std::uint32_t Limbs = 10; + + static bool Bit(const std::array& value, std::uint32_t bit) { return ((value[bit / 32u] >> (bit % 32u)) & 1u) != 0u; } + + void Accumulate(const std::array& term, std::uint64_t carry) { + for (std::uint32_t index = 0; index < Limbs; ++index) { + const std::uint64_t sum = std::uint64_t{limbs[index]} + term[index] + carry; + limbs[index] = static_cast(sum); + carry = sum >> 32u; + } + } + + std::array limbs{}; +}; + +struct Half { + std::uint32_t bits; + bool nan; + bool infinite; + bool zero; + bool negative; + std::uint64_t significand; + std::uint32_t scale; +}; + +Half Decode(std::uint32_t packed, std::uint32_t shift) { + const std::uint32_t bits = (packed >> shift) & 0xffffu; + const std::uint32_t magnitude = bits & 0x7fffu; + const std::uint32_t exponent = magnitude >> 10u; + const std::uint32_t fraction = magnitude & 0x3ffu; + Half half{bits, magnitude > 0x7c00u, magnitude == 0x7c00u, magnitude == 0u, (bits & 0x8000u) != 0u, 0u, 0u}; + half.significand = exponent == 0u ? fraction : (fraction | 0x400u); + half.scale = exponent == 0u ? 0u : exponent - 1u; + return half; +} + +std::optional Dot2(std::uint32_t a, std::uint32_t b, std::uint32_t c) { + const Half aLow = Decode(a, 0u); + const Half bLow = Decode(b, 0u); + const Half aHigh = Decode(a, 16u); + const Half bHigh = Decode(b, 16u); + const auto quiet = [](const Half& half) { return (half.negative ? 0x80000000u : 0u) | 0x7fc00000u | ((half.bits & 0x3ffu) << 13u); }; + for (const Half* half : {&aLow, &bLow, &aHigh, &bHigh}) { + if (half->nan) return quiet(*half); + } + const std::uint32_t magnitudeC = c & 0x7fffffffu; + if (magnitudeC > 0x7f800000u) return c | 0x00400000u; + const bool negativeC = (c & 0x80000000u) != 0u; + const bool infiniteC = magnitudeC == 0x7f800000u; + const bool signLow = aLow.negative != bLow.negative; + const bool signHigh = aHigh.negative != bHigh.negative; + const auto invalidProduct = [](const Half& lhs, const Half& rhs) { return (lhs.infinite && rhs.zero) || (rhs.infinite && lhs.zero); }; + const auto infiniteProduct = [](const Half& lhs, const Half& rhs) { return (lhs.infinite && !rhs.zero) || (rhs.infinite && !lhs.zero); }; + const bool infiniteLow = infiniteProduct(aLow, bLow); + const bool infiniteHigh = infiniteProduct(aHigh, bHigh); + const bool invalid = invalidProduct(aLow, bLow) || invalidProduct(aHigh, bHigh) || (infiniteLow && infiniteHigh && signLow != signHigh) || + (infiniteLow && infiniteC && signLow != negativeC) || (infiniteHigh && infiniteC && signHigh != negativeC); + if (invalid) return 0xffc00000u; + if (infiniteLow || infiniteHigh || infiniteC) return ((infiniteLow ? signLow : infiniteHigh ? signHigh : negativeC) ? 0x80000000u : 0u) | 0x7f800000u; + Exact sum; + sum.Add(aLow.significand * bLow.significand, aLow.scale + bLow.scale + 101u, signLow); + sum.Add(aHigh.significand * bHigh.significand, aHigh.scale + bHigh.scale + 101u, signHigh); + if (magnitudeC >= 0x00800000u) sum.Add((c & 0x7fffffu) | 0x800000u, (magnitudeC >> 23u) - 1u, negativeC); + if (sum.Zero()) return (signLow && signHigh && negativeC) ? 0x80000000u : 0u; + const auto rounded = sum.Rounded(); + if (!rounded.has_value() || ((*rounded >> 23u) & 0xffu) < 27u || ((*rounded >> 23u) & 0xffu) >= 254u) return std::nullopt; + return rounded; +} + +std::uint32_t RandomHalf(std::mt19937& random) { + const std::uint32_t exponent = random() % 31u; + return ((random() & 1u) << 15u) | (exponent << 10u) | (random() & 0x3ffu); +} + +std::uint32_t RandomF32(std::mt19937& random, std::uint32_t lowExponent, std::uint32_t highExponent) { + const std::uint32_t exponent = lowExponent + random() % (highExponent - lowExponent + 1u); + return ((random() & 1u) << 31u) | (exponent << 23u) | (random() & 0x7fffffu); +} + +std::uint32_t HalfToF32(std::uint32_t half) { + const Half decoded = Decode(half, 0u); + if (decoded.zero) return decoded.negative ? 0x80000000u : 0u; + std::uint64_t significand = decoded.significand; + std::uint32_t exponent = decoded.scale + 103u; + while ((significand & 0x400u) == 0u) { + significand <<= 1u; + --exponent; + } + return (decoded.negative ? 0x80000000u : 0u) | (exponent << 23u) | static_cast((significand & 0x3ffu) << 13u); +} + +std::vector Cases() { + std::vector cases; + const auto add = [&](std::uint32_t a, std::uint32_t b, std::uint32_t c) { + const auto expected = Dot2(a, b, c); + if (expected.has_value()) cases.push_back({a, b, c, *expected}); + }; + std::mt19937 random(2026u); + for (std::uint32_t index = 0; index < 60000u; ++index) add(RandomHalf(random) | (RandomHalf(random) << 16u), RandomHalf(random) | (RandomHalf(random) << 16u), RandomF32(random, 60u, 170u)); + for (std::uint32_t index = 0; index < 30000u; ++index) add(RandomHalf(random) | (RandomHalf(random) << 16u), RandomHalf(random) | (RandomHalf(random) << 16u), 0u); + for (std::uint32_t index = 0; index < 60000u; ++index) { + const std::uint32_t a = RandomHalf(random) | (RandomHalf(random) << 16u); + const std::uint32_t b = RandomHalf(random) | (RandomHalf(random) << 16u); + const float low = std::bit_cast(HalfToF32(a & 0xffffu)) * std::bit_cast(HalfToF32(b & 0xffffu)); + const float high = std::bit_cast(HalfToF32(a >> 16u)) * std::bit_cast(HalfToF32(b >> 16u)); + const std::uint32_t pick = random() % 4u; + const float base = pick == 0u ? -low : pick == 1u ? -high : -(low + high); + const std::uint32_t nudge = static_cast(static_cast(random() % 9u) - 4); + add(a, b, std::bit_cast(base) + nudge); + } + const std::array powers{0x3c00u, 0x4000u, 0x3800u, 0x0400u, 0x0001u, 0x7bffu, 0x3c01u, 0x3bffu}; + for (const std::uint32_t lowA : powers) for (const std::uint32_t highA : powers) for (const std::uint32_t lowB : powers) for (const std::uint32_t highB : powers) { + for (const std::uint32_t c : {0x00000000u, 0x3f800000u, 0xbf800000u, 0x33800000u, 0x4b800000u, 0x3f800001u, 0xcb7fffffu, 0x0d800000u, 0x00800000u, 0x8c7fffffu}) { + add(lowA | (highA << 16u), lowB | (highB << 16u), c); + add(lowA | (highA << 16u), (lowB ^ 0x8000u) | (highB << 16u), c); + } + } + const std::array specials{0x7c00u, 0xfc00u, 0x7e00u, 0x7c01u, 0xfe55u, 0x0000u, 0x8000u, 0x3c00u, 0xbc00u, 0x0001u}; + for (const std::uint32_t lowA : specials) for (const std::uint32_t highA : specials) for (const std::uint32_t lowB : specials) { + for (const std::uint32_t c : {0x00000000u, 0x80000000u, 0x7f800000u, 0xff800000u, 0x7fc00000u, 0x7f800123u, 0x3f800000u, 0x00000001u}) add(lowA | (highA << 16u), lowB | (0x3c00u << 16u), c); + } + while (cases.size() % Threads != 0u) cases.push_back({0u, 0u, 0x3f800000u, 0x3f800000u}); + if (cases.size() > Capacity) throw std::runtime_error("f32 dot2 rounding: too many cases"); + return cases; +} + +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; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!TargetHasCapability(device->Target(), spv::CapabilityFloat64)) { + std::puts("skipped, v_dot2_f32_f16 takes an f64 branch and the device has no shaderFloat64"); + return VulkanTestSkipped; + } + const auto cases = Cases(); + for (std::size_t index = 0; index < cases.size(); ++index) { + Input[index * 4u] = cases[index].a; + Input[index * 4u + 1u] = cases[index].b; + Input[index * 4u + 2u] = cases[index].c; + } + Output.fill(0xdeadbeefu); + const std::vector code{ + 0x34020084u, 0x8f098908u, 0x4a020209u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, + 0xcc134008u, 0x1c1a0b04u, 0x2c060282u, 0xe0701000u, 0x80010803u, 0xbf810000u, + }; + 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 words(code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(words)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {true, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), words, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device->Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}; + const auto result = ShaderRecompiler::Recompile(request); + device->Dispatch(result, static_cast(cases.size() / Threads), 1, 1, {}, reinterpret_cast(code.data())); + device->WaitIdle(); + for (std::size_t index = 0; index < cases.size(); ++index) { + const auto& entry = cases[index]; + Require(Output[index] == entry.expected, "f32 dot2 rounding: v_dot2_f32_f16 " + Hex(entry.a) + ", " + Hex(entry.b) + ", " + Hex(entry.c) + " is " + Hex(Output[index]) + ", expected " + Hex(entry.expected)); + } + std::printf("f32 dot2 rounding tests passed (%zu cases)\n", cases.size()); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F32PackRounding.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F32PackRounding.cpp new file mode 100644 index 0000000000..8b99ee8b3d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F32PackRounding.cpp @@ -0,0 +1,130 @@ +#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 + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Capacity = 1u << 20; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +std::uint32_t Bits(float value) { return std::bit_cast(value); } + +std::vector Inputs() { + std::vector inputs{ + 0x00000000u, 0x80000000u, 0x00000001u, 0x807fffffu, 0x00800000u, 0x3f000000u, 0xbf000000u, 0x3f7fffffu, + 0xbf7fffffu, 0x3f800000u, 0xbf800000u, 0x3f800001u, 0x7f800000u, 0xff800000u, 0x7fc00000u, 0xffc00001u, + 0x7f800001u, 0x437f0000u, 0x437f8000u, 0x43800000u, 0x7f7fffffu, 0x37800000u, 0x377fffffu, 0x38000000u, + }; + const auto around = [&](double exact) { + const std::uint32_t nearest = Bits(static_cast(exact)); + for (const int delta : {-2, -1, 0, 1, 2}) inputs.push_back(nearest + static_cast(delta)); + }; + for (const double scale : {65535.0, 32767.0}) { + for (std::uint32_t k = 0; k + 1u < static_cast(scale); ++k) around((k + 0.5) / scale); + } + for (std::uint32_t k = 0; k < 256u; ++k) around(k + 0.5); + const std::size_t positive = inputs.size(); + for (std::size_t index = 0; index < positive; index += 7u) inputs.push_back(inputs[index] ^ 0x80000000u); + while (inputs.size() % Threads != 0u) inputs.push_back(0u); + if (inputs.size() > Capacity) throw std::runtime_error("f32 pack rounding: too many inputs"); + return inputs; +} + +bool Nan(std::uint32_t bits) { return (bits & 0x7fffffffu) > 0x7f800000u; } + +std::uint32_t Flushed(std::uint32_t bits) { return (bits & 0x7f800000u) == 0u ? bits & 0x80000000u : bits; } + +std::uint32_t Norm(std::uint32_t source, bool signedValue) { + const std::uint32_t bits = Flushed(source); + if (Nan(bits)) return 0u; + const double scale = signedValue ? 32767.0 : 65535.0; + const bool negative = (bits & 0x80000000u) != 0u; + const double magnitude = std::fabs(static_cast(std::bit_cast(bits))); + const std::uint32_t value = magnitude >= 1.0 ? static_cast(scale) : static_cast(std::nearbyint(magnitude * scale)); + if (!negative) return value; + return signedValue ? (0u - value) & 0xffffu : 0u; +} + +std::uint32_t U8(std::uint32_t source) { + const float value = std::bit_cast(Flushed(source)); + if (Nan(source) || !(value > 0.0f)) return 0u; + if (value >= 255.0f) return 255u; + return static_cast(std::nearbyint(static_cast(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; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto inputs = Inputs(); + std::copy(inputs.begin(), inputs.end(), Input.begin()); + Output.fill(0xdeadbeefu); + const std::vector code{ + 0x34020082u, 0x8f098708u, 0x4a020209u, 0xe0301000u, 0x80000401u, 0x34060282u, 0xbf8c3f70u, + 0xd7690005u, 0x00020904u, 0xd7680006u, 0x00020904u, 0xd55e0007u, 0x02010104u, + 0xe0781000u, 0x80010503u, 0xbf810000u, + }; + 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 words(code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(words)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {true, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), words, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device->Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}; + const auto result = ShaderRecompiler::Recompile(request); + device->Dispatch(result, static_cast(inputs.size() / Threads), 1, 1, {}, reinterpret_cast(code.data())); + device->WaitIdle(); + for (std::size_t index = 0; index < inputs.size(); ++index) { + const std::uint32_t* out = &Output[index * 4u]; + const std::uint32_t source = inputs[index]; + const std::uint32_t unsignedNorm = Norm(source, false); + const std::uint32_t signedNorm = Norm(source, true); + Require(out[0] == (unsignedNorm | (unsignedNorm << 16u)), "f32 pack rounding: v_cvt_pknorm_u16_f32 of " + Hex(source) + " is " + Hex(out[0]) + ", expected " + Hex(unsignedNorm | (unsignedNorm << 16u))); + Require(out[1] == (signedNorm | (signedNorm << 16u)), "f32 pack rounding: v_cvt_pknorm_i16_f32 of " + Hex(source) + " is " + Hex(out[1]) + ", expected " + Hex(signedNorm | (signedNorm << 16u))); + Require(out[2] == U8(source), "f32 pack rounding: v_cvt_pk_u8_f32 of " + Hex(source) + " is " + Hex(out[2]) + ", expected " + Hex(U8(source))); + } + std::printf("f32 pack rounding tests passed (%zu inputs)\n", inputs.size()); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} 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 c8e592d6f2..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 @@ -22,6 +23,7 @@ constexpr std::uint32_t Inputs = 4; constexpr std::uint32_t Results = 16; alignas(256) std::array Input{}; alignas(256) std::array Output{}; +bool Ieee = false; alignas(256) constexpr std::array Code{ 0x34020084, 0x34060086, 0xe0381000, 0x80000401, 0xbf8c3f70, 0xd564000a, 0x02020d04, 0xd565000c, @@ -98,6 +100,40 @@ constexpr std::uint32_t Expected[32][16] = { {0x00000000u, 0x7ffc0000u, 0x00000000u, 0x7ffc0000u, 0x00000000u, 0x7ffc0000u, 0x00000000u, 0x7ffc0000u, 0x00000000u, 0x7ffc0000u, 0x00000000u, 0x7ffc0000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x7ffc0000u}, {0x00000000u, 0x41e00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x41e00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x41e00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x41e00000u} }; +constexpr std::uint32_t MinMaxNoIeee[32][4] = { + {0x00000000u, 0x3ff00000u, 0x80000000u, 0x40000000u}, + {0x00000000u, 0x3ff80000u, 0x7fffffffu, 0x40000000u}, + {0xfffff7ccu, 0x3ff00000u, 0xffffffffu, 0x7fefffffu}, + {0x00000000u, 0xbff00000u, 0xfffffbcdu, 0x3ff00000u}, + {0x00000000u, 0x3fe00000u, 0x00000401u, 0x3ff00000u}, + {0x00000000u, 0xfff00000u, 0xffc00000u, 0x41dfffffu}, + {0x12eaf8e8u, 0xc020f641u, 0x8a59a474u, 0xbefa45c5u}, + {0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0xbff00000u, 0x00000000u, 0x7ff00000u}, + {0x00000000u, 0x80000000u, 0x00000000u, 0x80000000u}, + {0x00000000u, 0x7ff00000u, 0x00000000u, 0x7ff00000u}, + {0x00000000u, 0x41f00000u, 0x00000000u, 0x41f00000u}, + {0xffffffffu, 0xffefffffu, 0x00000000u, 0x3ff80000u}, + {0x00000000u, 0xfff00000u, 0x00000000u, 0xfff00000u}, + {0xc66ab30du, 0x8e9a2140u, 0x047b57e5u, 0x02f22376u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x3ff00000u}, + {0x00000000u, 0xbff00000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x3fe00000u}, + {0x00000000u, 0xbfe00000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x3ff80000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x40040000u}, + {0x00000000u, 0xc0040000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0xffffffffu, 0x7fefffffu}, + {0xffffffffu, 0xffefffffu, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x41e00000u} +}; constexpr const char* Names[16] = {"add lo", "add hi", "mul lo", "mul hi", "fma lo", "fma hi", "fma error lo", "fma error hi", "min lo", "min hi", "max lo", "max hi", "ldexp lo", "ldexp hi", "add abs neg lo", "add abs neg hi"}; void Fill(std::uint32_t tid, std::uint32_t* words) { @@ -116,18 +152,16 @@ std::string Hex(std::uint32_t value) { } void Expect(std::uint32_t tid, std::uint32_t actual, std::uint32_t expected, const char* name) { - Require(actual == expected, std::string("f64 arithmetic: lane ") + std::to_string(tid) + " " + name + " is " + Hex(actual) + ", expected " + Hex(expected)); + 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{ @@ -137,6 +171,7 @@ void Run(AgcDriver::VulkanDevice& device) { {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(); @@ -146,10 +181,96 @@ void Check() { for (std::uint32_t tid = 0; tid < Threads; ++tid) { const std::uint32_t* in = &Input[tid * Inputs]; const std::uint32_t* out = &Output[tid * Results]; - for (std::uint32_t i = 0; i < 16; ++i) Expect(tid, out[i], Expected[tid][i], Names[i]); + for (std::uint32_t i = 0; i < 16; ++i) { + std::uint32_t expected = !Ieee && i >= 8u && i < 12u ? MinMaxNoIeee[tid][i - 8u] : Expected[tid][i]; + if (!Ieee && i == 13u && tid == 13u) expected = in[1]; + Expect(tid, out[i], expected, Names[i]); + } } } +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() { @@ -160,8 +281,13 @@ int main() { std::puts("skipped, the device has no shaderFloat64"); return VulkanTestSkipped; } - Run(*device); - Check(); + for (const bool ieee : {false, true}) { + Ieee = ieee; + 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/F64Conversions.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F64Conversions.cpp index ab7877a623..67e8aab62d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/F64Conversions.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F64Conversions.cpp @@ -137,6 +137,7 @@ void Run(AgcDriver::VulkanDevice& device) { {0, 0, 0, 128} }; request.useCache = false; + request.context.floatMode = ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}; const auto result = ShaderRecompiler::Recompile(request); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F64Division.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F64Division.cpp index 84d1c6f684..24f6440846 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/F64Division.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F64Division.cpp @@ -138,6 +138,7 @@ void Run(AgcDriver::VulkanDevice& device) { {0, 0, 0, 128} }; request.useCache = false; + request.context.floatMode = ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}; const auto result = ShaderRecompiler::Recompile(request); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F64DivisionModifiers.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F64DivisionModifiers.cpp index 16dcd87477..61f5616a9e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/F64DivisionModifiers.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F64DivisionModifiers.cpp @@ -138,6 +138,7 @@ void Run(AgcDriver::VulkanDevice& device) { {0, 0, 0, 128} }; request.useCache = false; + request.context.floatMode = ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}; const auto result = ShaderRecompiler::Recompile(request); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F64FmaSubnormals.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F64FmaSubnormals.cpp index b77fe2b37b..b00770b08e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/F64FmaSubnormals.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F64FmaSubnormals.cpp @@ -2,12 +2,15 @@ #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "Recompiler.hpp" #include "VulkanTestDevice.hpp" +#include #include #include #include #include #include +#include #include +#include #include namespace { @@ -94,6 +97,64 @@ std::array BufferDescriptor(const void* data, std::uint32_t by return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; } +std::vector Recompile(bool float64DenormPreserve) { + constexpr std::array capabilities{ + spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, + spv::CapabilityInt64, spv::CapabilityFloat64, spv::CapabilityPhysicalStorageBufferAddresses, + spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityDenormPreserve, spv::CapabilitySignedZeroInfNanPreserve, + }; + constexpr std::array extensions{ + "SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage", "SPV_KHR_float_controls", + "SPV_KHR_storage_buffer_storage_class", + }; + std::array input{}; + std::array output{}; + std::array userData{}; + const auto inputDescriptor = BufferDescriptor(input.data(), static_cast(input.size() * sizeof(input[0]))); + const auto outputDescriptor = BufferDescriptor(output.data(), static_cast(output.size() * sizeof(output[0]))); + std::copy(inputDescriptor.begin(), inputDescriptor.end(), userData.begin()); + std::copy(outputDescriptor.begin(), outputDescriptor.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + ShaderRecompiler::RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}}; + request.context.waveSize = 32; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderRecompiler::ShaderComputeStageInfo{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + request.context.memory = memory; + 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 = {1024, 1024, 64}; + request.target.maxWorkgroupInvocations = 1024; + request.target.maxWorkgroupSharedMemoryBytes = 49152; + request.target.float64DenormPreserve = float64DenormPreserve; + request.layout = {0, 0, 0, 128}; + request.useCache = false; + return ShaderRecompiler::Recompile(request).spirv.Copy(); +} + +bool HasDenormPreserve64(std::span words) { + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + Require(count != 0 && cursor + count <= words.size(), "malformed SPIR-V while scanning denorm modes"); + if ((words[cursor] & 0xffffu) == spv::OpExecutionMode && count == 4 && words[cursor + 2] == spv::ExecutionModeDenormPreserve && words[cursor + 3] == 64u) return true; + cursor += count; + } + return false; +} + +void VerifySpirvDenormMode() { + const auto enabled = Recompile(true); + const auto disabled = Recompile(false); + Require(HasDenormPreserve64(enabled), "float64DenormPreserve=true did not emit DenormPreserve 64"); + Require(!HasDenormPreserve64(disabled), "float64DenormPreserve=false emitted DenormPreserve 64"); +} + std::string Hex(std::uint64_t value) { char text[24]; std::snprintf(text, sizeof(text), "0x%016llx", static_cast(value)); @@ -131,23 +192,49 @@ void Run(AgcDriver::VulkanDevice& device, const std::array } template -void Check(const char* name, const std::uint64_t (&rows)[Count][4]) { +bool Check(const char* name, const std::uint64_t (&rows)[Count][4]) { for (std::uint32_t tid = 0; tid < Count; ++tid) { const std::uint64_t actual = Output[tid * Results] | (static_cast(Output[tid * Results + 1]) << 32u); - Require(actual == rows[tid][3], std::string(name) + ": lane " + std::to_string(tid) + " (" + Hex(rows[tid][0]) + ", " + Hex(rows[tid][1]) + ", " + Hex(rows[tid][2]) + ") is " + Hex(actual) + ", expected " + Hex(rows[tid][3])); + if (actual != rows[tid][3]) { + std::cerr << name << ": lane " << tid << " (" << Hex(rows[tid][0]) << ", " << Hex(rows[tid][1]) << ", " << Hex(rows[tid][2]) << ") is " << Hex(actual) << ", expected " << Hex(rows[tid][3]) << '\n'; + return false; + } } + return true; } } -int main() { +int main(int argc, char** argv) { try { + if (argc == 2 && std::string_view(argv[1]) == "--verify-spv-denorm-mode") { + VerifySpirvDenormMode(); + std::puts("f64 DenormPreserve execution mode follows the target capability"); + return 0; + } 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 bool canPreserveDenormals = device->Target().float64DenormPreserve; Run(*device, Code, Rows); - Check("v_fma_f64", Rows); + if (!Check("v_fma_f64", Rows)) { + if (!canPreserveDenormals) { + std::puts("skipped, the device flushes f64 subnormals without DenormPreserve support"); + return VulkanTestSkipped; + } + return 1; + } Run(*device, FmasCode, FmasRows); - Check("v_div_fmas_f64 with VCC set", FmasRows); + if (!Check("v_div_fmas_f64 with VCC set", FmasRows)) { + if (!canPreserveDenormals) { + std::puts("skipped, the device flushes f64 subnormals without DenormPreserve support"); + return VulkanTestSkipped; + } + return 1; + } std::puts("v_fma_f64 subnormal tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F64NanConversions.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F64NanConversions.cpp index 39d5cc5794..074d914482 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/F64NanConversions.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F64NanConversions.cpp @@ -132,6 +132,7 @@ void Run(AgcDriver::VulkanDevice& device) { {0, 0, 0, 128} }; request.useCache = false; + request.context.floatMode = ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}; const auto result = ShaderRecompiler::Recompile(request); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); 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/F64Transcendental.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F64Transcendental.cpp index c3d8c21b3e..f991f67fe3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/F64Transcendental.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F64Transcendental.cpp @@ -152,6 +152,7 @@ void Run(AgcDriver::VulkanDevice& device) { {0, 0, 0, 128} }; request.useCache = false; + request.context.floatMode = ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}; const auto result = ShaderRecompiler::Recompile(request); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); 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/FillCoverKeys.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FillCoverKeys.cpp index 7d4084a271..7be773667f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/FillCoverKeys.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FillCoverKeys.cpp @@ -67,7 +67,7 @@ public: #endif Require(block != nullptr, "fill cover test: cannot allocate the guest block"); std::memset(block, 0, BlockBytes); - GuestAllocations::Mutation().Add(block, BlockBytes, true, true); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); } ~GuestBlock() { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FlatApertureSources.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FlatApertureSources.cpp new file mode 100644 index 0000000000..21078c04c9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FlatApertureSources.cpp @@ -0,0 +1,240 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; +using Words = std::vector; + +constexpr std::uint32_t SharedBase = 235u; +constexpr std::uint32_t SharedLimit = 236u; +constexpr std::uint32_t PrivateBase = 237u; +constexpr std::uint32_t PrivateLimit = 238u; +constexpr std::array Sources{SharedBase, SharedLimit, PrivateBase, PrivateLimit}; +constexpr std::array SourceNames{"src_shared_base", "src_shared_limit", "src_private_base", "src_private_limit"}; +constexpr std::array SourceValues{0x8000000000000000ull, 0x80000000ffffffffull, 0x7000000000000000ull, 0x70000000ffffffffull}; +constexpr std::uint32_t LaneDwords = 32u; +constexpr std::uint32_t LdsDwords = 256u; +constexpr std::uint32_t ScratchDwords = 4u; +constexpr std::uint32_t ScratchOffset = 4u; +constexpr std::uint32_t SharedData = 0x5eed0000u; +constexpr std::uint32_t PrivateData = 0x51de0000u; +constexpr std::uint32_t WaitCnt0 = 0xbf8c0070u; +constexpr std::uint32_t EndProgram = 0xbf810000u; +constexpr std::uint32_t InlineZero = 128u; +constexpr std::uint32_t InlineOne = 129u; +constexpr std::uint32_t Literal = 255u; +constexpr std::uint32_t MovB32 = 0x03u; +constexpr std::uint32_t MovB64 = 0x04u; +constexpr std::uint32_t CndmaskOp = 0x01u; +constexpr std::uint32_t LshlrevOp = 0x1au; +constexpr std::uint32_t AddNcOp = 0x25u; +constexpr std::uint32_t FlatStoreDword = 0x1cu; +constexpr std::uint32_t FlatLoadDword = 0x0cu; +constexpr std::uint32_t FlatSegment = 0u; +constexpr std::uint32_t ScratchSegment = 1u; +constexpr std::uint32_t CmpLeU64 = 0xe3u; +constexpr std::uint32_t CmpGeU64 = 0xe6u; +constexpr std::uint32_t LaneOffset = 1u; +constexpr std::uint32_t SharedPointer = 3u; +constexpr std::uint32_t SharedValue = 5u; +constexpr std::uint32_t SharedFlatRead = 6u; +constexpr std::uint32_t PrivatePointer = 7u; +constexpr std::uint32_t PrivateValue = 9u; +constexpr std::uint32_t LdsRead = 10u; +constexpr std::uint32_t ScratchRead = 11u; +constexpr std::uint32_t EdgePointer = 12u; +constexpr std::uint32_t PastPointer = 14u; +constexpr std::uint32_t Result = 16u; +constexpr std::uint32_t PrivateFlatRead = 17u; +constexpr std::uint32_t One = 20u; +constexpr std::uint32_t NarrowReads = 8u; +constexpr std::uint32_t WideReads = 12u; +constexpr std::uint32_t PastHigh = 20u; + +struct Compare { + std::uint32_t op; + std::uint32_t source; + std::uint32_t pointer; + std::uint32_t expected; + const char* name; +}; + +constexpr std::array Compares{{ + {CmpLeU64, SharedBase, SharedPointer, 1u, "src_shared_base <= shared pointer"}, + {CmpGeU64, SharedLimit, SharedPointer, 1u, "src_shared_limit >= shared pointer"}, + {CmpLeU64, SharedBase, PrivatePointer, 0u, "src_shared_base <= private pointer"}, + {CmpLeU64, PrivateBase, PrivatePointer, 1u, "src_private_base <= private pointer"}, + {CmpGeU64, PrivateLimit, PrivatePointer, 1u, "src_private_limit >= private pointer"}, + {CmpGeU64, PrivateLimit, SharedPointer, 0u, "src_private_limit >= shared pointer"}, + {CmpGeU64, SharedLimit, EdgePointer, 1u, "src_shared_limit >= src_shared_limit"}, + {CmpGeU64, SharedLimit, PastPointer, 0u, "src_shared_limit >= the first address past it"}, +}}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::uint32_t ScalarMove(std::uint32_t opcode, std::uint32_t sdst, std::uint32_t source) { + return 0xbe800000u | (sdst << 16u) | (opcode << 8u) | source; +} + +std::uint32_t VectorMove(std::uint32_t vdst, std::uint32_t source) { + return 0x7e000200u | (vdst << 17u) | source; +} + +std::uint32_t Vop2(std::uint32_t opcode, std::uint32_t vdst, std::uint32_t src0, std::uint32_t vsrc1) { + return (opcode << 25u) | (vdst << 17u) | (vsrc1 << 9u) | src0; +} + +void EmitFlat(Words& code, std::uint32_t segment, std::uint32_t op, std::uint32_t vdst, std::uint32_t data, std::uint32_t address) { + code.push_back(0xdc000000u | (op << 18u) | (segment << 14u)); + code.push_back((vdst << 24u) | (0x7du << 16u) | (data << 8u) | address); +} + +void EmitOutputStore(Words& code, std::uint32_t vdata, std::uint32_t slot) { + code.push_back(0xe0701000u | (slot * 4u)); + code.push_back(0x80000000u | LaneOffset | (vdata << 8u)); +} + +Words Kernel() { + Words code{Vop2(LshlrevOp, LaneOffset, 135u, 0u)}; + for (std::uint32_t index = 0; index < Sources.size(); ++index) { + code.push_back(ScalarMove(MovB32, NarrowReads + index, Sources[index])); + code.push_back(ScalarMove(MovB64, WideReads + index * 2u, Sources[index])); + } + for (std::uint32_t index = 0; index < 12u; ++index) { + code.push_back(VectorMove(Result, NarrowReads + index)); + EmitOutputStore(code, Result, index); + } + code.push_back(Vop2(LshlrevOp, SharedPointer, 130u, 0u)); + code.push_back(VectorMove(SharedPointer + 1u, WideReads + 1u)); + code.push_back(Vop2(AddNcOp, SharedValue, Literal, 0u)); + code.push_back(SharedData); + code.push_back(VectorMove(PrivatePointer, InlineZero + ScratchOffset)); + code.push_back(VectorMove(PrivatePointer + 1u, WideReads + 5u)); + code.push_back(Vop2(AddNcOp, PrivateValue, Literal, 0u)); + code.push_back(PrivateData); + EmitFlat(code, FlatSegment, FlatStoreDword, 0u, SharedValue, SharedPointer); + EmitFlat(code, FlatSegment, FlatStoreDword, 0u, PrivateValue, PrivatePointer); + code.push_back(WaitCnt0); + code.push_back(0xd8d80000u); + code.push_back((LdsRead << 24u) | SharedPointer); + EmitFlat(code, ScratchSegment, FlatLoadDword, ScratchRead, 0u, PrivatePointer); + EmitFlat(code, FlatSegment, FlatLoadDword, SharedFlatRead, 0u, SharedPointer); + EmitFlat(code, FlatSegment, FlatLoadDword, PrivateFlatRead, 0u, PrivatePointer); + code.push_back(WaitCnt0); + EmitOutputStore(code, LdsRead, 12u); + EmitOutputStore(code, ScratchRead, 13u); + EmitOutputStore(code, SharedFlatRead, 14u); + EmitOutputStore(code, PrivateFlatRead, 15u); + code.push_back(VectorMove(EdgePointer, WideReads + 2u)); + code.push_back(VectorMove(EdgePointer + 1u, WideReads + 3u)); + code.push_back(0x80000000u | (PastHigh << 16u) | (InlineOne << 8u) | (WideReads + 3u)); + code.push_back(VectorMove(PastPointer, InlineZero)); + code.push_back(VectorMove(PastPointer + 1u, PastHigh)); + code.push_back(VectorMove(One, InlineOne)); + for (std::uint32_t index = 0; index < Compares.size(); ++index) { + const auto& compare = Compares[index]; + code.push_back(0x7c000000u | (compare.op << 17u) | (compare.pointer << 9u) | compare.source); + code.push_back(Vop2(CndmaskOp, Result, InlineZero, One)); + EmitOutputStore(code, Result, 16u + index); + } + for (std::uint32_t index = 0; index < Sources.size(); ++index) { + code.push_back(0xd5010000u | Result); + code.push_back(InlineZero | (InlineOne << 9u) | (Sources[index] << 18u)); + EmitOutputStore(code, Result, 24u + index); + } + code.push_back(EndProgram); + return code; +} + +void Expect(const std::vector& output, std::uint32_t waveSize, std::uint32_t lane, std::uint32_t slot, std::uint32_t expected, const std::string& what) { + const auto read = output[lane * LaneDwords + slot]; + Require(read == expected, "flat aperture sources wave" + std::to_string(waveSize) + ": lane " + std::to_string(lane) + " " + what + " read " + Hex(read) + ", expected " + Hex(expected)); +} + +void Check(const std::vector& output, std::uint32_t waveSize) { + for (std::uint32_t lane = 0; lane < waveSize; ++lane) { + for (std::uint32_t index = 0; index < Sources.size(); ++index) { + const auto value = SourceValues[index]; + const std::string name = SourceNames[index]; + Expect(output, waveSize, lane, index, static_cast(value), "s_mov_b32 of " + name); + Expect(output, waveSize, lane, 4u + index * 2u, static_cast(value), "s_mov_b64 of " + name + " low"); + Expect(output, waveSize, lane, 5u + index * 2u, static_cast(value >> 32u), "s_mov_b64 of " + name + " high"); + const auto maskBit = waveSize == 32u ? (static_cast(value) >> lane) & 1u : static_cast(value >> lane) & 1u; + Expect(output, waveSize, lane, 24u + index, maskBit, "v_cndmask_b32 with " + name + " as the lane mask"); + } + Expect(output, waveSize, lane, 12u, SharedData + lane, "ds_read_b32 after a flat store through src_shared_base"); + Expect(output, waveSize, lane, 13u, PrivateData + lane, "scratch_load_dword after a flat store through src_private_base"); + Expect(output, waveSize, lane, 14u, SharedData + lane, "flat_load_dword through src_shared_base"); + Expect(output, waveSize, lane, 15u, PrivateData + lane, "flat_load_dword through src_private_base"); + for (std::uint32_t index = 0; index < Compares.size(); ++index) { + Expect(output, waveSize, lane, 16u + index, Compares[index].expected, Compares[index].name); + } + } +} + +void Run(AgcDriver::VulkanDevice& device, std::uint32_t waveSize) { + std::vector output(waveSize * LaneDwords, 0xdeadbeefu); + std::vector userData(8, 0u); + const auto descriptor = BufferDescriptor(output.data(), static_cast(output.size() * 4u)); + std::copy(descriptor.begin(), descriptor.end(), userData.begin()); + const auto code = Kernel(); + const std::span codeSpan(code); + const std::array memory{{{reinterpret_cast(codeSpan.data()), std::as_bytes(codeSpan)}}}; + ShaderRecompiler::ShaderComputeStageInfo compute{{waveSize, 1, 1}, LdsDwords, {false, false, false}, false, 1}; + compute.scratchDwords = ScratchDwords; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(codeSpan.data()), codeSpan, 0, {}}, + {waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(codeSpan.data())); + device.WaitIdle(); + Check(output, waveSize); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device, 32u); + if (device->Target().subgroupSize >= 32u) { + Run(*device, 64u); + } else { + std::printf("skipped wave64, subgroup size %u cannot hold a wave64 in two lanes\n", device->Target().subgroupSize); + } + std::puts("flat aperture sources tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FlatApertures.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FlatApertures.cpp new file mode 100644 index 0000000000..72f895af11 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FlatApertures.cpp @@ -0,0 +1,519 @@ +#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 +#include +#include +#include +#include +#include + + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; +using Words = std::vector; + +constexpr std::uint32_t SharedHigh = 0x80000000u; +constexpr std::uint32_t PrivateHigh = 0x70000000u; +constexpr std::uint32_t LdsDwords = 256u; +constexpr std::uint32_t ScratchDwords = 4u; +constexpr std::size_t GuestBytes = 65536u; +constexpr std::uint32_t GuestStoreSlot = 0x200u; +constexpr std::uint32_t GuestAtomicSlot = 0x800u; +constexpr std::uint32_t WaitCnt0 = 0xbf8c0070u; +constexpr std::uint32_t EndProgram = 0xbf810000u; +constexpr std::uint32_t ShiftLaneBy5 = 0x34020085u; +constexpr std::uint32_t StoreByteOp = 0x18u; +constexpr std::uint32_t StoreShortOp = 0x1au; +constexpr std::uint32_t StoreDwordOp = 0x1cu; +constexpr std::uint32_t StoreDwordx2Op = 0x1du; +constexpr std::uint32_t LoadUbyteOp = 0x08u; +constexpr std::uint32_t LoadUshortOp = 0x0au; +constexpr std::uint32_t LoadDwordOp = 0x0cu; +constexpr std::uint32_t LoadDwordx2Op = 0x0du; + +enum class Target : std::uint8_t { Shared, Private, Guest, Mixed }; +enum class AtomicOp : std::uint8_t { Swap, Cmpswap, Add, Sub, Smin, Umin, Smax, Umax, And, Or, Xor, Inc, Dec, Fcmpswap, Fmin, Fmax }; + +constexpr std::size_t Atomic32Ops = 13; +constexpr std::array AtomicOpcodes32{0x30u, 0x31u, 0x32u, 0x33u, 0x35u, 0x36u, 0x37u, 0x38u, 0x39u, 0x3au, 0x3bu, 0x3cu, 0x3du}; +constexpr std::array AtomicOpcodes64{0x50u, 0x51u, 0x52u, 0x53u, 0x55u, 0x56u, 0x57u, 0x58u, 0x59u, 0x5au, 0x5bu, 0x5cu, 0x5du, 0x5eu, 0x5fu, 0x60u}; +constexpr std::array TargetNames{"shared", "private", "guest", "mixed"}; +constexpr std::array OpNames{"swap", "cmpswap", "add", "sub", "smin", "umin", "smax", "umax", "and", "or", "xor", "inc", "dec", "fcmpswap", "fmin", "fmax"}; + +struct Row { + Target target; + bool atomic; + AtomicOp op; + std::uint32_t width; + std::uint32_t offset; + std::uint32_t waveSize; +}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, GuestBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(GuestBytes, GuestBytes)); +#endif + Require(block != nullptr, "flat apertures: cannot allocate the guest block"); + std::memset(block, 0, GuestBytes); + GuestAllocations::Mutation().Add(block, GuestBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uintptr_t Address() const { return reinterpret_cast(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::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::string Hex64(std::uint64_t value) { + char text[24]; + std::snprintf(text, sizeof(text), "0x%016llx", static_cast(value)); + return text; +} + +std::uint32_t FlatWord0(std::uint32_t op, bool glc, std::uint32_t offset = 0u) { + return 0xdc000000u | (op << 18u) | (glc ? 0x10000u : 0u) | offset; +} + +std::uint32_t FlatWord1(std::uint32_t vdst, std::uint32_t data, std::uint32_t address) { + return (vdst << 24u) | (0x7du << 16u) | (data << 8u) | address; +} + +void EmitFlat(Words& code, std::uint32_t op, bool glc, std::uint32_t vdst, std::uint32_t data, std::uint32_t address) { + code.push_back(FlatWord0(op, glc)); + code.push_back(FlatWord1(vdst, data, address)); +} + +void EmitTableLoad(Words& code, std::uint32_t vdst, std::uint32_t offset) { + code.push_back(0xe0381000u | offset); + code.push_back(0x80010001u | (vdst << 8u)); +} + +void EmitOutputStore(Words& code, std::uint32_t vdata, std::uint32_t offset) { + code.push_back(0xe0701000u | offset); + code.push_back(0x80000001u | (vdata << 8u)); +} + +std::uint32_t StoreOp(std::uint32_t width) { + return width == 8u ? StoreByteOp : (width == 16u ? StoreShortOp : StoreDwordOp); +} + +std::uint32_t LoadOp(std::uint32_t width) { + return width == 8u ? LoadUbyteOp : (width == 16u ? LoadUshortOp : LoadDwordOp); +} + +Words Kernel32(const Row& row) { + Words code{ShiftLaneBy5}; + EmitTableLoad(code, 2u, 0u); + EmitTableLoad(code, 6u, 16u); + code.push_back(WaitCnt0); + if (row.atomic) { + EmitFlat(code, StoreDwordOp, false, 0u, 6u, 2u); + code.push_back(WaitCnt0); + EmitFlat(code, AtomicOpcodes32[static_cast(row.op)], true, 10u, 4u, 2u); + code.push_back(WaitCnt0); + EmitFlat(code, LoadDwordOp, false, 11u, 0u, 2u); + code.push_back(WaitCnt0); + EmitOutputStore(code, 10u, 0u); + EmitOutputStore(code, 11u, 4u); + } else { + EmitFlat(code, StoreOp(row.width), false, 0u, 4u, 2u); + code.push_back(WaitCnt0); + EmitFlat(code, LoadOp(row.width), false, 10u, 0u, 2u); + EmitFlat(code, LoadUbyteOp, false, 11u, 0u, 2u); + code.push_back(FlatWord0(LoadUbyteOp, false, row.width / 8u - 1u)); + code.push_back(FlatWord1(12u, 0u, 2u)); + code.push_back(WaitCnt0); + EmitOutputStore(code, 10u, 0u); + EmitOutputStore(code, 11u, 4u); + EmitOutputStore(code, 12u, 8u); + } + code.push_back(EndProgram); + return code; +} + +Words Kernel64(const Row& row) { + Words code{ShiftLaneBy5}; + EmitTableLoad(code, 2u, 0u); + EmitTableLoad(code, 6u, 16u); + code.push_back(WaitCnt0); + EmitFlat(code, StoreDwordx2Op, false, 0u, 8u, 2u); + code.push_back(WaitCnt0); + EmitFlat(code, AtomicOpcodes64[static_cast(row.op)], true, 10u, 4u, 2u); + code.push_back(WaitCnt0); + EmitFlat(code, LoadDwordx2Op, false, 12u, 0u, 2u); + code.push_back(WaitCnt0); + EmitOutputStore(code, 10u, 0u); + EmitOutputStore(code, 11u, 4u); + EmitOutputStore(code, 12u, 8u); + EmitOutputStore(code, 13u, 12u); + code.push_back(EndProgram); + return code; +} + +Words Kernel(const Row& row) { + return row.atomic && row.width == 64u ? Kernel64(row) : Kernel32(row); +} + +std::uint32_t OperandA(std::uint32_t lane) { + return 0x9e3779b1u * (lane + 1u) ^ 0x5a5a0f0fu; +} + +std::uint32_t InitValue(std::uint32_t lane) { + return (lane % 2u) != 0u ? (0x80000000u | (lane * 0x101u)) : (lane * 0x35u + 7u); +} + +std::uint32_t CmpValue(std::uint32_t lane) { + return (lane % 2u) != 0u ? InitValue(lane) + 1u : InitValue(lane); +} + +std::uint32_t Apply(AtomicOp op, std::uint32_t init, std::uint32_t a, std::uint32_t b) { + const auto signedInit = static_cast(init); + const auto signedA = static_cast(a); + switch (op) { + case AtomicOp::Swap: return a; + case AtomicOp::Cmpswap: return init == b ? a : init; + case AtomicOp::Add: return init + a; + case AtomicOp::Sub: return init - a; + case AtomicOp::Smin: return signedInit < signedA ? init : a; + case AtomicOp::Umin: return init < a ? init : a; + case AtomicOp::Smax: return signedInit > signedA ? init : a; + case AtomicOp::Umax: return init > a ? init : a; + case AtomicOp::And: return init & a; + case AtomicOp::Or: return init | a; + case AtomicOp::Xor: return init ^ a; + case AtomicOp::Inc: return init >= a ? 0u : init + 1u; + case AtomicOp::Dec: return (init == 0u || init > a) ? a : init - 1u; + default: break; + } + return init; +} + +std::uint64_t Pack(std::uint32_t high, std::uint32_t low) { + return (static_cast(high) << 32u) | low; +} + +std::uint64_t Bits(double value) { + std::uint64_t bits = 0; + std::memcpy(&bits, &value, sizeof(bits)); + return bits; +} + +bool IsFloatOp(AtomicOp op) { + return op == AtomicOp::Fcmpswap || op == AtomicOp::Fmin || op == AtomicOp::Fmax; +} + +std::uint64_t IntInit64(std::uint32_t lane) { + return Pack((lane % 2u) != 0u ? (0x80000000u | (lane * 0x1357u)) : (lane * 0x0a0bu + 0x11u), InitValue(lane)); +} + +std::uint64_t IntA64(std::uint32_t lane) { + return Pack(0x85ebca6bu * (lane + 1u) ^ 0xc2b2ae35u, OperandA(lane)); +} + +std::uint64_t IntCmp64(std::uint32_t lane) { + return (lane % 2u) != 0u ? IntInit64(lane) + 1u : IntInit64(lane); +} + +double FloatInit(std::uint32_t lane) { + return (lane % 2u) == 0u ? static_cast(lane + 1u) * 3.25 : -static_cast(lane + 1u) * 7.5; +} + +double FloatA(std::uint32_t lane) { + return (lane % 2u) == 0u ? -static_cast(lane + 2u) * 1.5 : static_cast(lane + 3u) * 2.75 - 2.0; +} + +double FloatCmp(std::uint32_t lane) { + return (lane % 2u) == 0u ? FloatInit(lane) : FloatInit(lane) + 1.0; +} + +std::uint64_t InitBits64(AtomicOp op, std::uint32_t lane) { + return IsFloatOp(op) ? Bits(FloatInit(lane)) : IntInit64(lane); +} + +std::uint64_t OperandBits64(AtomicOp op, std::uint32_t lane) { + return IsFloatOp(op) ? Bits(FloatA(lane)) : IntA64(lane); +} + +std::uint64_t CmpBits64(AtomicOp op, std::uint32_t lane) { + return IsFloatOp(op) ? Bits(FloatCmp(lane)) : IntCmp64(lane); +} + +std::uint64_t Apply64(AtomicOp op, std::uint64_t init, std::uint64_t a, std::uint64_t cmp) { + const auto signedInit = static_cast(init); + const auto signedA = static_cast(a); + switch (op) { + case AtomicOp::Swap: return a; + case AtomicOp::Cmpswap: return init == cmp ? a : init; + case AtomicOp::Add: return init + a; + case AtomicOp::Sub: return init - a; + case AtomicOp::Smin: return signedInit < signedA ? init : a; + case AtomicOp::Umin: return init < a ? init : a; + case AtomicOp::Smax: return signedInit > signedA ? init : a; + case AtomicOp::Umax: return init > a ? init : a; + case AtomicOp::And: return init & a; + case AtomicOp::Or: return init | a; + case AtomicOp::Xor: return init ^ a; + case AtomicOp::Inc: return init >= a ? 0u : init + 1u; + case AtomicOp::Dec: return (init == 0u || init > a) ? a : init - 1u; + default: break; + } + return init; +} + +std::uint64_t Expected64(AtomicOp op, std::uint32_t lane) { + if (IsFloatOp(op)) { + const auto init = FloatInit(lane); + const auto a = FloatA(lane); + switch (op) { + case AtomicOp::Fcmpswap: return Bits(init == FloatCmp(lane) ? a : init); + case AtomicOp::Fmin: return Bits(std::min(init, a)); + case AtomicOp::Fmax: return Bits(std::max(init, a)); + default: break; + } + } + return Apply64(op, IntInit64(lane), IntA64(lane), IntCmp64(lane)); +} + +std::uint64_t LaneAddress(const Row& row, std::uint32_t lane, std::uint64_t guestBase) { + Target target = row.target; + if (target == Target::Mixed) { + target = lane % 3u == 0u ? Target::Shared : (lane % 3u == 1u ? Target::Private : Target::Guest); + } + const std::uint32_t privateBase = row.width == 64u ? 8u : 4u; + switch (target) { + case Target::Shared: return (static_cast(SharedHigh) << 32u) | (8u * lane + row.offset); + case Target::Private: return (static_cast(PrivateHigh) << 32u) | (privateBase + row.offset); + case Target::Guest: return guestBase + 8u * lane + row.offset; + default: break; + } + throw std::runtime_error("flat apertures: no address for the row target"); +} + +std::string Describe(const Row& row) { + std::string access; + if (row.atomic) { + access = std::string(row.width == 64u ? "flat_atomic64_" : "flat_atomic_") + OpNames[static_cast(row.op)]; + } else { + access = "flat_store" + std::to_string(row.width); + } + return access + " " + TargetNames[static_cast(row.target)] + " offset " + std::to_string(row.offset) + " wave" + std::to_string(row.waveSize); +} + +void Check(const Row& row, const std::vector& output) { + const std::string name = Describe(row); + for (std::uint32_t lane = 0; lane < row.waveSize; ++lane) { + const auto a = OperandA(lane); + if (row.atomic) { + const auto init = InitValue(lane); + const auto old = output[lane * 8u]; + Require(old == init, name + ": lane " + std::to_string(lane) + " returned " + Hex(old) + ", expected " + Hex(init)); + const auto expected = Apply(row.op, init, a, CmpValue(lane)); + const auto left = output[lane * 8u + 1u]; + Require(left == expected, name + ": lane " + std::to_string(lane) + " left " + Hex(left) + ", expected " + Hex(expected)); + } else { + const auto read = output[lane * 8u]; + if (row.target == Target::Shared && 8u * lane + row.offset + row.width / 8u > LdsDwords * 4u) { + Require(read == 0u, name + ": lane " + std::to_string(lane) + " read " + Hex(read) + " back from an access past the end of LDS, expected 0 (the store is dropped whole)"); + continue; + } + const auto expected = row.width == 8u ? (a & 0xffu) : (row.width == 16u ? (a & 0xffffu) : a); + Require(read == expected, name + ": lane " + std::to_string(lane) + " read " + Hex(read) + " back, expected " + Hex(expected)); + const auto first = output[lane * 8u + 1u]; + Require(first == (a & 0xffu), name + ": lane " + std::to_string(lane) + " has first byte " + Hex(first) + " in memory, expected " + Hex(a & 0xffu)); + const auto last = output[lane * 8u + 2u]; + const auto lastExpected = (a >> (8u * (row.width / 8u - 1u))) & 0xffu; + Require(last == lastExpected, name + ": lane " + std::to_string(lane) + " has last byte " + Hex(last) + " in memory, expected " + Hex(lastExpected)); + } + } +} + +void Check64(const Row& row, const std::vector& output) { + const std::string name = Describe(row); + for (std::uint32_t lane = 0; lane < row.waveSize; ++lane) { + const auto old = Pack(output[lane * 8u + 1u], output[lane * 8u]); + const auto init = InitBits64(row.op, lane); + Require(old == init, name + ": lane " + std::to_string(lane) + " returned " + Hex64(old) + ", expected " + Hex64(init)); + const auto left = Pack(output[lane * 8u + 3u], output[lane * 8u + 2u]); + const auto expected = Expected64(row.op, lane); + Require(left == expected, name + ": lane " + std::to_string(lane) + " left " + Hex64(left) + ", expected " + Hex64(expected)); + } +} + +ShaderRecompiler::RecompileRequest MakeRequest(AgcDriver::VulkanDevice& device, std::span code, const std::array& memory, std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, std::uint32_t waveSize) { + return ShaderRecompiler::RecompileRequest{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; +} + +void Run(AgcDriver::VulkanDevice& device, const Row& row, const GuestBlock& guest) { + const auto lanes = row.waveSize; + const auto guestSlot = row.atomic ? GuestAtomicSlot : GuestStoreSlot; + const auto guestBase = static_cast(guest.Address()) + guestSlot; + const bool wideAtomic = row.atomic && row.width == 64u; + std::vector table(lanes * 8u, 0u); + for (std::uint32_t lane = 0; lane < lanes; ++lane) { + const auto address = LaneAddress(row, lane, guestBase); + table[lane * 8u] = static_cast(address); + table[lane * 8u + 1u] = static_cast(address >> 32u); + if (wideAtomic) { + const auto operand = OperandBits64(row.op, lane); + const auto cmp = CmpBits64(row.op, lane); + const auto init = InitBits64(row.op, lane); + table[lane * 8u + 2u] = static_cast(operand); + table[lane * 8u + 3u] = static_cast(operand >> 32u); + table[lane * 8u + 4u] = static_cast(cmp); + table[lane * 8u + 5u] = static_cast(cmp >> 32u); + table[lane * 8u + 6u] = static_cast(init); + table[lane * 8u + 7u] = static_cast(init >> 32u); + } else { + table[lane * 8u + 2u] = OperandA(lane); + table[lane * 8u + 3u] = row.atomic ? CmpValue(lane) : 0u; + table[lane * 8u + 4u] = row.atomic ? InitValue(lane) : 0u; + } + } + std::vector output(lanes * 8u, 0xdeadbeefu); + std::vector userData(8, 0u); + const auto outputDescriptor = BufferDescriptor(output.data(), static_cast(output.size() * 4u)); + const auto tableDescriptor = BufferDescriptor(table.data(), static_cast(table.size() * 4u)); + std::copy(outputDescriptor.begin(), outputDescriptor.end(), userData.begin()); + std::copy(tableDescriptor.begin(), tableDescriptor.end(), userData.begin() + 4); + const auto code = Kernel(row); + const std::span codeSpan(code); + const std::array memory{{{reinterpret_cast(codeSpan.data()), std::as_bytes(codeSpan)}}}; + ShaderRecompiler::ShaderComputeStageInfo compute{{lanes, 1, 1}, LdsDwords, {false, false, false}, false, 1}; + compute.scratchDwords = ScratchDwords; + auto request = MakeRequest(device, codeSpan, memory, userData, compute, row.waveSize); + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(codeSpan.data())); + device.WaitIdle(); + if (wideAtomic) { + Check64(row, output); + } else { + Check(row, output); + } +} + +std::vector Rows() { + std::vector rows; + const std::array single{Target::Shared, Target::Private, Target::Guest}; + const std::array, 7> stores{{{8u, 0u}, {8u, 3u}, {16u, 0u}, {16u, 1u}, {16u, 3u}, {32u, 0u}, {32u, 2u}}}; + for (const Target target : single) { + for (const auto [width, offset] : stores) { + rows.push_back(Row{target, false, AtomicOp::Swap, width, offset, 32u}); + } + } + rows.push_back(Row{Target::Mixed, false, AtomicOp::Swap, 16u, 3u, 32u}); + rows.push_back(Row{Target::Mixed, false, AtomicOp::Swap, 32u, 2u, 32u}); + for (const auto [width, offset] : std::array, 5>{{{32u, 773u}, {32u, 774u}, {32u, 776u}, {16u, 775u}, {8u, 775u}}}) { + rows.push_back(Row{Target::Shared, false, AtomicOp::Swap, width, offset, 32u}); + } + for (const Target target : single) { + for (std::uint32_t op = 0; op < Atomic32Ops; ++op) { + rows.push_back(Row{target, true, static_cast(op), 32u, 0u, 32u}); + } + } + for (const Target target : single) { + for (std::uint32_t op = 0; op < OpNames.size(); ++op) { + rows.push_back(Row{target, true, static_cast(op), 64u, 0u, 32u}); + } + } + rows.push_back(Row{Target::Mixed, true, AtomicOp::Add, 32u, 0u, 32u}); + rows.push_back(Row{Target::Mixed, true, AtomicOp::Cmpswap, 32u, 0u, 32u}); + rows.push_back(Row{Target::Mixed, true, AtomicOp::Add, 64u, 0u, 32u}); + rows.push_back(Row{Target::Mixed, true, AtomicOp::Cmpswap, 64u, 0u, 32u}); + rows.push_back(Row{Target::Mixed, true, AtomicOp::Fmax, 64u, 0u, 32u}); + rows.push_back(Row{Target::Mixed, false, AtomicOp::Swap, 16u, 3u, 64u}); + rows.push_back(Row{Target::Mixed, false, AtomicOp::Swap, 32u, 2u, 64u}); + rows.push_back(Row{Target::Mixed, true, AtomicOp::Add, 32u, 0u, 64u}); + rows.push_back(Row{Target::Shared, true, AtomicOp::Cmpswap, 32u, 0u, 64u}); + rows.push_back(Row{Target::Mixed, true, AtomicOp::Add, 64u, 0u, 64u}); + rows.push_back(Row{Target::Shared, true, AtomicOp::Fmin, 64u, 0u, 64u}); + return rows; +} + +} + +int main(int argc, char** argv) { + try { + std::size_t part = 0, parts = 1; + if (argc == 3 && std::string_view(argv[1]) == "--part") { + const std::string_view value(argv[2]); + const auto slash = value.find('/'); + part = std::stoul(std::string(value.substr(0, slash))); + parts = slash == std::string_view::npos ? 0 : std::stoul(std::string(value.substr(slash + 1))); + } else if (argc != 1) { + parts = 0; + } + if (parts == 0 || part >= parts) throw std::invalid_argument("usage: [--part K/N] with K < N"); + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const bool wave64 = device->Target().subgroupSize >= 32u; + const bool int64Atomics = TargetHasCapability(device->Target(), spv::CapabilityInt64Atomics); + GuestBlock guest; + std::size_t checked = 0; + std::map, std::size_t> shaders; + for (const Row& row : Rows()) { + const auto shader = shaders.emplace(std::pair{Kernel(row), row.waveSize}, shaders.size()).first->second; + if (shader % parts != part) continue; + if (row.waveSize == 64u && !wave64) continue; + if (row.atomic && row.width == 64u && !int64Atomics) continue; + Run(*device, row, guest); + ++checked; + } + if (!wave64) std::printf("skipped the wave64 rows, subgroup size %u cannot hold a wave64 in two lanes\n", device->Target().subgroupSize); + if (!int64Atomics) std::puts("skipped the 64-bit atomic rows, the device has no shaderBufferInt64Atomics"); + std::printf("flat aperture rows checked: %zu\n", checked); + std::puts("flat aperture 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/FlatCacheBits.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FlatCacheBits.cpp index 742faa65c2..dede33116a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/FlatCacheBits.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FlatCacheBits.cpp @@ -86,7 +86,7 @@ public: block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); #endif Require(block != nullptr, "flat cache bits: cannot allocate the guest block"); - GuestAllocations::Mutation().Add(block, BlockBytes, true, true); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); } ~GuestBlock() { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FlatD16Loads.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FlatD16Loads.cpp index abc38e9651..7f57139ab8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/FlatD16Loads.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FlatD16Loads.cpp @@ -482,7 +482,7 @@ public: block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); #endif Require(block != nullptr, "flat d16 loads: cannot allocate the guest block"); - GuestAllocations::Mutation().Add(block, BlockBytes, true, false); + GuestAllocations::Mutation().Add(block, BlockBytes, true, false, true); } ~GuestBlock() { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FlatLoads.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FlatLoads.cpp index e96afcd629..a6ed287475 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/FlatLoads.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FlatLoads.cpp @@ -94,7 +94,7 @@ public: block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); #endif Require(block != nullptr, "flat loads: cannot allocate the guest block"); - GuestAllocations::Mutation().Add(block, BlockBytes, true, false); + GuestAllocations::Mutation().Add(block, BlockBytes, true, false, true); } ~GuestBlock() { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FlatLoadsX2X3.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FlatLoadsX2X3.cpp index c85f891fd3..ab29a898a3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/FlatLoadsX2X3.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FlatLoadsX2X3.cpp @@ -93,7 +93,7 @@ public: block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); #endif Require(block != nullptr, "flat loads x2 x3: cannot allocate the guest block"); - GuestAllocations::Mutation().Add(block, BlockBytes, true, false); + GuestAllocations::Mutation().Add(block, BlockBytes, true, false, true); } ~GuestBlock() { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FlatLoadsX4.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FlatLoadsX4.cpp index 330114afb9..5c65c59733 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/FlatLoadsX4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FlatLoadsX4.cpp @@ -93,7 +93,7 @@ public: block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); #endif Require(block != nullptr, "flat loads x4: cannot allocate the guest block"); - GuestAllocations::Mutation().Add(block, BlockBytes, true, false); + GuestAllocations::Mutation().Add(block, BlockBytes, true, false, true); } ~GuestBlock() { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FlatStore.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FlatStore.cpp index 25bcf93966..2d6da41fd0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/FlatStore.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FlatStore.cpp @@ -1,6 +1,8 @@ +#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/GuestWriteWatch.hpp" #include "Recompiler.hpp" #include "VulkanTestDevice.hpp" #ifdef _WIN32 @@ -8,6 +10,8 @@ #define NOMINMAX #endif #include +#else +#include #endif #include #include @@ -27,6 +31,11 @@ constexpr std::uint32_t MaskedThreads = 16; constexpr std::size_t BlockBytes = 65536; constexpr std::uint32_t CheckedBytes = 0x2000; constexpr std::uint8_t Fill = 0xcd; +#if defined(__linux__) && defined(ANYPS5_ENABLE_ASAN) +constexpr std::array GuestWindowAddresses{0x60000000, 0x200000000000}; +#elif defined(__linux__) +constexpr std::array GuestWindowAddresses{0x80000000000, 0x200000000000}; +#endif alignas(256) constexpr std::array FlatStoreCode{ 0x34020082, 0x34040084, 0x340c0081, 0x340e0083, 0x4a060200, 0x7e080201, 0x4a1806ff, 0x00000204, @@ -39,28 +48,54 @@ alignas(256) constexpr std::array FlatStoreCode{ class GuestBlock { public: - explicit GuestBlock(bool writable) { + explicit GuestBlock(bool writable, bool watched = false, std::uintptr_t at = 0) : bytes(watched ? 2 * BlockBytes : BlockBytes), watched(watched) { #ifdef _WIN32 - block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + Require(!watched, "flat store: watched guest blocks are mapped only on Linux"); + block = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); #else - block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); + if (watched) { +#ifdef __linux__ + void* raw = mmap(reinterpret_cast(at), bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0); + Require(raw == reinterpret_cast(at), "flat store: cannot map the watched guest block at its address"); + block = static_cast(raw); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes); +#else + Require(false, "flat store: watched guest blocks are mapped only on Linux"); +#endif + } else { + block = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } #endif Require(block != nullptr, "flat store: cannot allocate the guest block"); + Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "flat store: the guest block is not write-watched"); Clear(); - GuestAllocations::Mutation().Add(block, BlockBytes, true, writable); + GuestAllocations::Mutation().Add(block, bytes, true, writable, true); } ~GuestBlock() { GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + if (watched) { + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, bytes); + munmap(block, bytes); + } else { + std::free(block); + } +#endif } GuestBlock(const GuestBlock&) = delete; GuestBlock& operator=(const GuestBlock&) = delete; - void Clear() { std::memset(block, Fill, BlockBytes); } + void Clear() { std::memset(block, Fill, bytes); } const std::uint8_t* Data() const { return block; } + std::uint64_t Address() const { return reinterpret_cast(block); } private: + std::size_t bytes; + bool watched; std::uint8_t* block = nullptr; }; @@ -116,6 +151,27 @@ void RunStores(AgcDriver::VulkanDevice& device, GuestBlock& guest, std::uint32_t } } +#ifdef __linux__ +void RunStampedBlocks(AgcDriver::VulkanDevice& device, GuestBlock& guest) { + namespace GuestMemory = AgcDriver::GuestMemory; + const auto base = guest.Address(); + Dispatch(device, 32, guest.Data()); + if (!GuestMemory::Watched(base, 2 * BlockBytes)) { + std::printf("host imports are compared, not watched: stamped blocks at 0x%llx not tested\n", static_cast(base)); + return; + } + for (int use = 0; use < 2; ++use) { + guest.Clear(); + const auto collected = GuestMemory::CollectWrites(base, 2 * BlockBytes); + Require(collected != 0, "flat store: the watched guest blocks are not collected"); + Require(GuestMemory::UnchangedSince(base, 2 * BlockBytes, collected), "flat store: the guest blocks changed before the store"); + Dispatch(device, 32, guest.Data()); + Require(!GuestMemory::UnchangedSince(base, BlockBytes, collected), "flat store: the stored block is not stamped as written"); + Require(GuestMemory::UnchangedSince(base + BlockBytes, BlockBytes, collected), "flat store: a block no store reached is stamped as written"); + } +} +#endif + void RunReadOnly(AgcDriver::VulkanDevice& device, const GuestBlock& guest) { Dispatch(device, 32, guest.Data()); for (std::uint32_t offset = 0; offset < CheckedBytes; ++offset) { @@ -134,6 +190,14 @@ int main() { RunStores(*device, writable, 32); RunStores(*device, writable, 64); RunReadOnly(*device, readOnly); +#ifdef __linux__ + if (AgcDriver::GuestMemory::WriteWatched()) { + for (const auto at : GuestWindowAddresses) { + GuestBlock watched(true, true, at); + RunStampedBlocks(*device, watched); + } + } +#endif std::puts("flat store tests passed"); return 0; } catch (const std::exception& error) { 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/Float16FmaRounding.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Float16FmaRounding.cpp new file mode 100644 index 0000000000..424059a6c1 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Float16FmaRounding.cpp @@ -0,0 +1,232 @@ +#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 = 64; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 8; +constexpr std::uint32_t Checks = 7; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084, 0x34060085, 0xe0381000, 0x80000401, 0xbf8c3f70, 0xd74b000a, 0x041a0b04, 0xd74b380b, + 0x041a0b04, 0x7e180306, 0x6c180b04, 0xcc0e400d, 0x1c1a0b04, 0x7e1c0306, 0x781c0b04, 0x701e0f04, + 0x00000001, 0x6e200f04, 0x00003d55, 0x7e220280, 0xe0781000, 0x80010a03, 0xe0781010, 0x80010e03, + 0xbf810000, +}; + +constexpr std::uint32_t Rows[Threads][Inputs] = { + {0x800062e9u, 0x3c00d159u, 0x800023ecu, 0x0000f9a3u}, + {0x0000d4e0u, 0xbc005ae0u, 0x0000881fu, 0x00001e01u}, + {0x4200c1b4u, 0x4400f580u, 0xca008544u, 0x00003700u}, + {0x7c005af7u, 0x4000bde3u, 0x3c00772cu, 0x000027e9u}, + {0x3c0053e4u, 0x3c005900u, 0xfc000955u, 0x00002500u}, + {0x5bff3464u, 0x5bff5542u, 0x3c0012ffu, 0x00006d19u}, + {0x3c00c6d2u, 0x3c01064eu, 0x800129d8u, 0x00003200u}, + {0x1400e568u, 0x14004b80u, 0x00000891u, 0x000069d9u}, + {0xba18f442u, 0x70c0b900u, 0x88810546u, 0x00000700u}, + {0xbba84092u, 0x6a804788u, 0x814512ffu, 0x00008ba1u}, + {0xbc2cd70cu, 0x6d3ad700u, 0xa7f90ee0u, 0x00002100u}, + {0xbf00562cu, 0x72cc3774u, 0x8497ee8cu, 0x0000c716u}, + {0x56b4e294u, 0xdf004f00u, 0x15fb0683u, 0x00001700u}, + {0x1d0dd0e0u, 0x0a56cea0u, 0x1828837fu, 0x00001a01u}, + {0x5a40f006u, 0xcad01f55u, 0x80e6ea0eu, 0x00000900u}, + {0xcb3cc300u, 0x9615e4bcu, 0xc4a80843u, 0x0000d0fbu}, + {0x9e945d6eu, 0x95793cf9u, 0x998179b2u, 0x00001a00u}, + {0x603451cdu, 0xcf80d341u, 0x022c2276u, 0x00009041u}, + {0x06fbc6d0u, 0xf697ed40u, 0x5ea10c65u, 0x000030c0u}, + {0xd416668bu, 0x0a5d0272u, 0xc2d759b3u, 0x000032c9u}, + {0x6836db60u, 0x4900c9e0u, 0x806a02f8u, 0x00001ca0u}, + {0x02637ad4u, 0xe2b43100u, 0xd7ad0608u, 0x00002007u}, + {0xb4544d00u, 0x70806a2eu, 0x84828f49u, 0x00002a74u}, + {0x560003e6u, 0xd8453e28u, 0x0245af54u, 0x00005100u}, + {0x9b9bd580u, 0x08356004u, 0x14eb1222u, 0x000021e8u}, + {0x9aec64f8u, 0x67e64440u, 0x01018892u, 0x0000ad81u}, + {0x4366c00eu, 0x6e00eddcu, 0x0c16abffu, 0x00003900u}, + {0x002142a6u, 0xc7c266dbu, 0x28409b9du, 0x00000f11u}, + {0x45e74af4u, 0x015b6900u, 0xb5681505u, 0x00002d00u}, + {0x18eb7a00u, 0x6bc32ce7u, 0x804202f8u, 0x00008001u}, + {0xe990f8f2u, 0xc1201b57u, 0x06f71bc1u, 0x00008280u}, + {0xd9001c4du, 0xddeaf293u, 0x165316e1u, 0x00000e2bu}, + {0x532aee50u, 0x4e7c41c0u, 0xa97e11aau, 0x000009c0u}, + {0x066e7100u, 0xf4fa45aeu, 0xeee58a84u, 0x00003274u}, + {0x4dabed80u, 0x143c493cu, 0xcc6d8b1au, 0x000009e8u}, + {0xbd10d940u, 0x6d205d18u, 0x87670b12u, 0x00002001u}, + {0x68704f00u, 0x4e60e80au, 0x11f601a7u, 0x0000289cu}, + {0x5c805cd0u, 0xd9d45820u, 0x977a112bu, 0x00008357u}, + {0xf3003d96u, 0x34946e00u, 0x07660459u, 0x00003a00u}, + {0x363c73c0u, 0xa5223f70u, 0xb7e30d44u, 0x0000083au}, + {0x73de9e78u, 0xbe00262fu, 0x8872ac8eu, 0x00005a80u}, + {0xe6d7ccfeu, 0xb8f8e900u, 0xa88286b3u, 0x00001561u}, + {0x2824d550u, 0xf1805ac0u, 0x00860ec2u, 0x000022c0u}, + {0x36442fcau, 0x770019c7u, 0x8c9f254au, 0x00005fd8u}, + {0x6e192033u, 0xa3ae06abu, 0x84228eadu, 0x00005a00u}, + {0xad87d9b0u, 0x51cad8e0u, 0xf3f304cbu, 0x0000f6e3u}, + {0x39da80abu, 0xbad699fdu, 0xdc3c807du, 0x000012a2u}, + {0x199a8735u, 0x724992a9u, 0x9b4f82d6u, 0x0000a863u}, + {0xe56f0101u, 0x22719dfau, 0xe53e820au, 0x00000e86u}, + {0x66400f97u, 0x4c280a53u, 0x14008394u, 0x00004c5cu}, + {0x54409989u, 0xce7880b9u, 0x0151825eu, 0x0000066eu}, + {0x54b00f7cu, 0x506091e1u, 0x00a08146u, 0x00008201u}, + {0x50d689e7u, 0x4237096cu, 0x0dfa831eu, 0x00006495u}, + {0x04288b6fu, 0x0fb3084eu, 0x01e9037fu, 0x0000a90cu}, + {0xa1eb1a63u, 0x853d01e1u, 0x015a03b9u, 0x00003ea2u}, + {0x8af80d78u, 0x98e105dau, 0x02ef8286u, 0x0000294bu}, + {0x216c08b8u, 0x86d30ac8u, 0x8336022fu, 0x00006f80u}, + {0x87f41c46u, 0x0c0688eau, 0x82e782efu, 0x0000c6b4u}, + {0x0a215be0u, 0x183e5c10u, 0x83618001u, 0x00001c60u}, + {0x03dcdbe0u, 0x96385c10u, 0x02650001u, 0x000092e8u}, + {0x13496ff2u, 0x92dd5328u, 0x0270cb02u, 0x00008a00u}, + {0x8b276d1au, 0x9f695348u, 0x8397f1e6u, 0x000048c4u}, + {0x9560ee89u, 0x817d02f3u, 0x0331d633u, 0x0000de07u}, + {0x0fb3d541u, 0x0a3c089du, 0x822a503fu, 0x00003c3du} +}; +constexpr std::uint32_t Expected[Threads][Checks] = { + {0xf89fu, 0x8000u, 0xf89fu, 0x8000f89fu, 0x8000f89fu, 0xfc00u, 0xf97eu}, + {0xf431u, 0x0000u, 0xf431u, 0x0000f431u, 0x0000f431u, 0xb751u, 0xd67fu}, + {0x7bd7u, 0x0000u, 0x7bd7u, 0x00007bd7u, 0x00007bd7u, 0xbcfdu, 0xc2bau}, + {0x7717u, 0x7c00u, 0x7717u, 0x7c007717u, 0x7c007717u, 0x46e3u, 0x5ca5u}, + {0x70efu, 0xfc00u, 0x70efu, 0xfc0070efu, 0xfc0070efu, 0x3cefu, 0x5543u}, + {0x4dc5u, 0x7bfeu, 0x4dc5u, 0x7bfe4dc5u, 0x7bfe4dc5u, 0x6598u, 0x6d19u}, + {0x29c3u, 0x3c01u, 0x29c3u, 0x3c0129c3u, 0x3c0129c3u, 0xbd1du, 0xc874u}, + {0xf511u, 0x0010u, 0xf511u, 0x0010f511u, 0x0010f511u, 0xfc00u, 0x647du}, + {0x7153u, 0xef3du, 0x7153u, 0xef3d7153u, 0xef3d7153u, 0xbf73u, 0xf5adu}, + {0x4c4du, 0xea39u, 0x4c4du, 0xea394c4du, 0xea394c4du, 0x905cu, 0x4217u}, + {0x722bu, 0xed73u, 0x722bu, 0xed73722bu, 0xed73722bu, 0xbc67u, 0xd8b2u}, + {0xee81u, 0xf5f3u, 0xee81u, 0xf5f3ee81u, 0xf5f3ee81u, 0xe177u, 0x57c9u}, + {0xf5c1u, 0xf9ddu, 0xf5c1u, 0xf9ddf5c1u, 0xf9ddf5c1u, 0xbdc1u, 0xe462u}, + {0x6409u, 0x1829u, 0x6409u, 0x18296409u, 0x18296409u, 0xaf51u, 0xd27fu}, + {0xea2bu, 0xe953u, 0xea2bu, 0xe953ea2bu, 0xe953ea2bu, 0xbd07u, 0xf15du}, + {0x6c25u, 0xc4a3u, 0x6c25u, 0xc4a36c25u, 0xc4a36c25u, 0x585cu, 0xd190u}, + {0x79bfu, 0x997du, 0x79bfu, 0x997d79bfu, 0x997d79bfu, 0x3c13u, 0x5f3du}, + {0xe943u, 0xf3e1u, 0xe943u, 0xf3e1e943u, 0xf3e1e943u, 0xa62bu, 0x53bbu}, + {0x7879u, 0x5e95u, 0x7879u, 0x5e957879u, 0x5e957879u, 0xbc0bu, 0xc877u}, + {0x59b3u, 0xc2ddu, 0x59b3u, 0xc2dd59b3u, 0xc2dd59b3u, 0x5d8du, 0x685du}, + {0x696bu, 0x7543u, 0x696bu, 0x7543696bu, 0x7543696bu, 0xbc43u, 0xdceau}, + {0x7045u, 0xd7adu, 0x7045u, 0xd7ad7045u, 0xd7ad7045u, 0x5ee0u, 0x7c00u}, + {0x7bb9u, 0xe8dfu, 0x7bb9u, 0xe8df7bb9u, 0xe8df7bb9u, 0x3c09u, 0x4eadu}, + {0xaf53u, 0xf267u, 0xaf53u, 0xf267af53u, 0xf267af53u, 0x18e0u, 0x5100u}, + {0xf985u, 0x14ebu, 0xf985u, 0x14ebf985u, 0x14ebf985u, 0xbc0fu, 0xd755u}, + {0x6d47u, 0xc6d5u, 0x6d47u, 0xc6d56d47u, 0xc6d56d47u, 0xd6d6u, 0x669fu}, + {0x71f1u, 0x758du, 0x71f1u, 0x758d71f1u, 0x758d71f1u, 0xbd11u, 0xc028u}, + {0x6db3u, 0x283fu, 0x6db3u, 0x283f6db3u, 0x283f6db3u, 0x15dfu, 0x446fu}, + {0x7859u, 0xb567u, 0x7859u, 0xb5677859u, 0xb5677859u, 0x3c59u, 0x4ca7u}, + {0x6b5bu, 0x48c5u, 0x6b5bu, 0x48c56b5bu, 0x48c56b5bu, 0x9a00u, 0x7bffu}, + {0xd889u, 0x6f21u, 0xd889u, 0x6f21d889u, 0x6f21d889u, 0x3e2fu, 0xfa98u}, + {0xd311u, 0x7b65u, 0xd311u, 0x7b65d311u, 0x7b65d311u, 0x001cu, 0x1e1eu}, + {0xf489u, 0x65cfu, 0xf489u, 0x65cff489u, 0x65cff489u, 0xbc89u, 0xf035u}, + {0x7b19u, 0xeee5u, 0x7b19u, 0xeee57b19u, 0xeee57b19u, 0x6809u, 0x72aau}, + {0xfb33u, 0xcc6bu, 0xfb33u, 0xcc6bfb33u, 0xcc6bfb33u, 0xbc0fu, 0xef55u}, + {0xfaafu, 0xee7du, 0xfaafu, 0xee7dfaafu, 0xee7dfaafu, 0xbd41u, 0xdaffu}, + {0xfb11u, 0x7b13u, 0xfb11u, 0x7b13fb11u, 0x7b13fb11u, 0x3c09u, 0x50acu}, + {0x78f7u, 0xfa8fu, 0x78f7u, 0xfa8f78f7u, 0xfa8f78f7u, 0xa405u, 0x5e6au}, + {0x7031u, 0xec01u, 0x7031u, 0xec017031u, 0xec017031u, 0x3c31u, 0x4139u}, + {0x7735u, 0xb801u, 0x7735u, 0xb8017735u, 0xb8017735u, 0x4018u, 0x752au}, + {0xac91u, 0xf5e7u, 0xac91u, 0xf5e7ac91u, 0xf5e7ac91u, 0xbd41u, 0x5a80u}, + {0x7a3du, 0x643fu, 0x7a3du, 0x643f7a3du, 0x643f7a3du, 0xa6b7u, 0xcea7u}, + {0xf47bu, 0xddb1u, 0xf47bu, 0xddb1f47bu, 0xddb1f47bu, 0xbc7bu, 0xd715u}, + {0x2561u, 0x717bu, 0x2561u, 0x717b2561u, 0x717b2561u, 0x53a3u, 0x5fd9u}, + {0x8ea9u, 0xd5dbu, 0x8ea9u, 0xd5db8ea9u, 0xd5db8ea9u, 0x3e4du, 0x5a00u}, + {0x76efu, 0xf3f3u, 0x76efu, 0xf3f376efu, 0xf3f376efu, 0x7c00u, 0xf6f2u}, + {0x807du, 0xdc3fu, 0x807du, 0xdc3f807du, 0xdc3f807du, 0x0001u, 0x1286u}, + {0x82d5u, 0x5067u, 0x82d5u, 0x506782d5u, 0x506782d5u, 0x0040u, 0xa868u}, + {0x820bu, 0xe54fu, 0x820bu, 0xe54f820bu, 0xe54f820bu, 0x0001u, 0x0edcu}, + {0x8393u, 0x767fu, 0x8393u, 0x767f8393u, 0x767f8393u, 0x2023u, 0x4c5cu}, + {0x825du, 0xe6dfu, 0x825du, 0xe6df825du, 0xe6df825du, 0x8003u, 0x9b2du}, + {0x814bu, 0x6921u, 0x814bu, 0x6921814bu, 0x6921814bu, 0x0001u, 0x10bdu}, + {0x831fu, 0x5783u, 0x831fu, 0x5783831fu, 0x5783831fu, 0xb2c3u, 0x6495u}, + {0x037fu, 0x01e9u, 0x037fu, 0x01e9037fu, 0x01e9037fu, 0x0097u, 0xa916u}, + {0x03bbu, 0x0169u, 0x03bbu, 0x016903bbu, 0x016903bbu, 0x1d4cu, 0x3ea6u}, + {0x8285u, 0x02f7u, 0x8285u, 0x02f78285u, 0x02f78285u, 0x00e9u, 0x295au}, + {0x022fu, 0x8349u, 0x022fu, 0x8349022fu, 0x8349022fu, 0x3c6du, 0x6f80u}, + {0x82f9u, 0x82e7u, 0x82f9u, 0x82e782f9u, 0x82e782f9u, 0xa729u, 0xc6b3u}, + {0x7bffu, 0x835bu, 0x7bffu, 0x835b7bffu, 0x835b7bffu, 0x3c4fu, 0x5d40u}, + {0xfbffu, 0x0263u, 0xfbffu, 0x0263fbffu, 0x0263fbffu, 0x32ccu, 0xdd40u}, + {0x7c00u, 0x0263u, 0x7c00u, 0x02637c00u, 0x02637c00u, 0xbdf5u, 0x714cu}, + {0x7c00u, 0x837du, 0x7c00u, 0x837d7c00u, 0x837d7c00u, 0x7a14u, 0x6ecfu}, + {0xd638u, 0x0331u, 0xd638u, 0x0331d638u, 0x0331d638u, 0x7c00u, 0xf08bu}, + {0x503fu, 0x8229u, 0x503fu, 0x8229503fu, 0x8229503fu, 0xd591u, 0xd6f0u} +}; +constexpr std::uint32_t Masks[Checks] = {0xffffu, 0xffffu, 0xffffu, 0xffffffffu, 0xffffffffu, 0xffffu, 0xffffu}; +constexpr const char* Names[Checks] = { + "v_fma_f16 v10, v4, v5, v6", + "v_fma_f16 v11, v4, v5, v6 op_sel:[1,1,1,0]", + "v_fmac_f16 v12, v4, v5", + "v_pk_fma_f16 v13, v4, v5, v6", + "v_pk_fmac_f16 v14, v4, v5", + "v_fmaak_f16 v15, v4, v7, 0x0001", + "v_fmamk_f16 v16, v4, 0x3d55, v7", +}; + +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) { + 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::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t index = 0; index < Checks; ++index) { + const std::uint32_t actual = Output[tid * Results + index] & Masks[index]; + Require(actual == Expected[tid][index], std::string("f16 fma rounding: lane ") + std::to_string(tid) + " " + Names[index] + " is " + Hex(actual) + ", expected " + Hex(Expected[tid][index])); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device); + Check(); + std::puts("f16 fma 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/Float16MinMaxBits.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Float16MinMaxBits.cpp new file mode 100644 index 0000000000..a70e3086fd --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Float16MinMaxBits.cpp @@ -0,0 +1,296 @@ +#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 Stride = 8; +constexpr std::uint32_t Batches = 32; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array ScalarCode{ + 0x34020084, 0x34060085, 0xe0381000, 0x80000401, 0xbf8c3f70, + 0x74140b04, + 0x72160b04, + 0xd539810c, 0x20020d04, + 0xd53a800d, 0x40020d05, + 0xd751000e, 0x041a0b04, + 0xd754280f, 0x441a0b04, + 0xd7570010, 0x041a0b04, + 0xd7579111, 0x841e0b04, + 0xe0781000, 0x80010a03, 0xe0781010, 0x80010e03, 0xbf810000, +}; + +alignas(256) constexpr std::array PackedCode{ + 0x34020084, 0x34060085, 0xe0381000, 0x80000401, 0xbf8c3f70, + 0xcc11400a, 0x18020b04, + 0xcc12400b, 0x18020b04, + 0xcc12ca0c, 0x30020d04, + 0xcc11c00d, 0x58020f05, + 0xe0781000, 0x80010a03, 0xbf810000, +}; + +enum class Kind { Min, Max, Min3, Max3, Med3 }; + +struct Source { + std::uint32_t reg; + bool opSel = false; + bool opSelHi = true; + bool absolute = false; + bool negate = false; + bool negateHi = false; +}; + +struct Check { + const char* name; + Kind kind; + std::array sources; + bool clamp = false; +}; + +const std::array ScalarChecks{{ + {"v_min_f16 v10, v4, v5", Kind::Min, {{{0u}, {1u}, {}}}}, + {"v_max_f16 v11, v4, v5", Kind::Max, {{{0u}, {1u}, {}}}}, + {"v_max_f16 v12, -|v4|, v6 clamp", Kind::Max, {{{0u, false, true, true, true}, {2u}, {}}}, true}, + {"v_min_f16 v13, v5, -v6 clamp", Kind::Min, {{{1u}, {2u, false, true, false, true}, {}}}, true}, + {"v_min3_f16 v14, v4, v5, v6", Kind::Min3, {{{0u}, {1u}, {2u}}}}, + {"v_max3_f16 v15, v4, -v5, v6 op_sel:[1,0,1,0]", Kind::Max3, {{{0u, true}, {1u, false, true, false, true}, {2u, true}}}}, + {"v_med3_f16 v16, v4, v5, v6", Kind::Med3, {{{0u}, {1u}, {2u}}}}, + {"v_med3_f16 v17, |v4|, v5, -v7 op_sel:[0,1,0,0] clamp", Kind::Med3, {{{0u, false, true, true}, {1u, true}, {3u, false, true, false, true}}}, true}, +}}; + +const std::array PackedChecks{{ + {"v_pk_min_f16 v10, v4, v5", Kind::Min, {{{0u}, {1u}, {}}}}, + {"v_pk_max_f16 v11, v4, v5", Kind::Max, {{{0u}, {1u}, {}}}}, + {"v_pk_max_f16 v12, v4, v6 op_sel:[1,0] op_sel_hi:[0,1] neg_lo:[1,0] neg_hi:[0,1] clamp", Kind::Max, {{{0u, true, false, false, true, false}, {2u, false, true, false, false, true}, {}}}, true}, + {"v_pk_min_f16 v13, v5, v7 neg_lo:[0,1] clamp", Kind::Min, {{{1u}, {3u, false, true, false, true}, {}}}, true}, +}}; + +struct Mode { + const char* name; + std::optional floatMode; +}; + +const std::array Modes{{ + {"no FLOAT_MODE", std::nullopt}, + {"dx10 ieee", ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}}, + {"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}}, + {"f16 input denormals flushed", ShaderRecompiler::ShaderFloatMode{0xb0u, false, false, false}}, +}}; + +std::uint64_t Seed = 0x2545f4914f6cdd1dull; + +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) % 9u) { + 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 | 0x3c00u | (bits & 0x3u); + default: return bits & 0xffffu; + } +} + +bool IsNan(std::uint32_t bits) { return (bits & 0x7fffu) > 0x7c00u; } + +bool IsSnan(std::uint32_t bits) { return IsNan(bits) && (bits & 0x0200u) == 0u; } + +double Value(std::uint32_t bits) { + const double sign = (bits & 0x8000u) != 0u ? -1.0 : 1.0; + const std::uint32_t exponent = (bits >> 10u) & 0x1fu; + const std::uint32_t mantissa = bits & 0x3ffu; + if (exponent == 0u) return sign * std::ldexp(static_cast(mantissa), -24); + if (exponent == 31u) return mantissa != 0u ? std::nan("") : sign * INFINITY; + return sign * std::ldexp(static_cast(mantissa | 0x400u), static_cast(exponent) - 25); +} + +struct Model { + bool ieee; + bool dx10; + bool flushInputs; + + struct Half { + double value; + std::uint32_t bits; + }; + + std::uint32_t Quiet(std::uint32_t bits) const { return ieee ? bits | 0x0200u : bits; } + + Half Numeric(bool maxValue, const Half& lhs, const Half& rhs) const { + const bool ordered = maxValue ? lhs.value >= rhs.value : lhs.value < rhs.value; + const bool bothZero = ((lhs.bits | rhs.bits) & 0x7fffu) == 0u; + const std::uint32_t zero = maxValue ? lhs.bits & rhs.bits : lhs.bits | rhs.bits; + return {ordered ? lhs.value : rhs.value, bothZero ? zero : (ordered ? lhs.bits : rhs.bits)}; + } + + Half Pair(bool maxValue, const Half& lhs, const Half& rhs) const { + const Half numeric = Numeric(maxValue, lhs, rhs); + std::uint32_t bits = IsNan(lhs.bits) ? rhs.bits : numeric.bits; + bits = IsNan(rhs.bits) ? lhs.bits : bits; + if (ieee) { + bits = IsSnan(rhs.bits) ? Quiet(rhs.bits) : bits; + bits = IsSnan(lhs.bits) ? Quiet(lhs.bits) : bits; + } + return {IsNan(rhs.bits) ? lhs.value : numeric.value, bits}; + } + + std::uint32_t Clamp(bool clamp, std::uint32_t bits) const { + if (!clamp) return bits; + const std::uint32_t magnitude = bits & 0x7fffu; + const bool zero = bits > 0x7fffu || magnitude > 0x7c00u; + const std::uint32_t clamped = zero ? 0u : (magnitude > 0x3c00u ? 0x3c00u : bits); + return dx10 || magnitude <= 0x7c00u ? clamped : bits; + } + + Half Read(std::uint32_t bits) const { + if (flushInputs && (bits & 0x7fffu) < 0x0400u) bits &= 0x8000u; + return {Value(bits), bits}; + } + + std::uint32_t Scalar(const Check& check, const std::uint32_t* registers) const { + std::array args{}; + for (std::uint32_t index = 0; index < 3u; ++index) { + const Source& source = check.sources[index]; + std::uint32_t bits = (registers[source.reg] >> (source.opSel ? 16u : 0u)) & 0xffffu; + if (source.absolute) bits &= 0x7fffu; + if (source.negate) bits ^= 0x8000u; + args[index] = Read(bits); + } + std::uint32_t result = 0u; + switch (check.kind) { + case Kind::Min: result = Pair(false, args[0], args[1]).bits; break; + case Kind::Max: result = Pair(true, args[0], args[1]).bits; break; + case Kind::Min3: result = Pair(false, Pair(false, args[0], args[1]), args[2]).bits; break; + case Kind::Max3: result = Pair(true, Pair(true, args[0], args[1]), args[2]).bits; break; + case Kind::Med3: { + const bool anyNan = IsNan(args[0].bits) || IsNan(args[1].bits) || IsNan(args[2].bits); + const Half minimum = Pair(false, Pair(false, args[0], args[1]), args[2]); + const Half median = Numeric(true, Numeric(false, args[0], args[1]), Numeric(false, Numeric(true, args[0], args[1]), args[2])); + result = anyNan ? minimum.bits : median.bits; + break; + } + } + return Clamp(check.clamp, result); + } + + std::uint32_t Packed(const Check& check, const std::uint32_t* registers) const { + std::uint32_t result = 0u; + for (std::uint32_t high = 0; high < 2u; ++high) { + std::array bits{}; + for (std::uint32_t index = 0; index < 2u; ++index) { + const Source& source = check.sources[index]; + const bool selectHigh = high != 0u ? source.opSelHi : source.opSel; + bits[index] = (registers[source.reg] >> (selectHigh ? 16u : 0u)) & 0xffffu; + if (high != 0u ? source.negateHi : source.negate) bits[index] ^= 0x8000u; + } + result |= Clamp(check.clamp, Pair(check.kind == Kind::Max, Read(bits[0]), Read(bits[1])).bits) << (16u * high); + } + return result; + } +}; + +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; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + 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 ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + const auto compile = [&](std::span code, const Mode& mode) { + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + 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.floatMode; + return ShaderRecompiler::Recompile(request); + }; + std::uint64_t compared = 0u; + for (const auto& mode : Modes) { + 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); + 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]; + for (std::uint32_t index = 0; index < Inputs; ++index) row[index] = Half() | (Half() << 16u); + if (tid % 8u == 0u) row[1] = row[0]; + if (tid % 8u == 1u) row[1] = row[0] ^ 0x80008000u; + if (tid % 8u == 2u) row[2] = row[1]; + if (tid % 8u == 3u) row[2] = row[0] ^ 0x80008000u; + } + const auto run = [&](const ShaderRecompiler::RecompileResult& result, std::span code, std::span checks, bool isPacked) { + Output.fill(0xdeadbeefu); + 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* row = &Input[tid * Inputs]; + for (std::uint32_t index = 0; index < checks.size(); ++index) { + const std::uint32_t actual = Output[tid * Stride + index]; + const std::uint32_t expected = isPacked ? model.Packed(checks[index], row) : model.Scalar(checks[index], row); + Require(actual == expected, std::string("f16 min/max: ") + mode.name + ", " + checks[index].name + " with v4=" + Hex(row[0]) + " v5=" + Hex(row[1]) + " v6=" + Hex(row[2]) + " v7=" + Hex(row[3]) + " is " + Hex(actual) + ", expected " + Hex(expected)); + ++compared; + } + } + }; + run(scalar, ScalarCode, ScalarChecks, false); + run(packed, PackedCode, PackedChecks, true); + } + } + std::printf("f16 min/max tests passed (%llu results)\n", static_cast(compared)); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Float16Modifiers.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Float16Modifiers.cpp index ad9657aedb..26f1be254b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Float16Modifiers.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Float16Modifiers.cpp @@ -10,7 +10,9 @@ #include #include #include +#include #include +#include #include namespace { @@ -35,6 +37,11 @@ alignas(256) constexpr std::array Code{ 0x80010e03, 0xe0781020, 0x80011203, 0xe0781030, 0x80011603, 0xbf810000, }; +alignas(256) constexpr std::array Min3Div2{0xd7510001, 0x1c020100, 0xbf810000}; +alignas(256) constexpr std::array Max3Mul2{0xd7540001, 0x0c020100, 0xbf810000}; +alignas(256) constexpr std::array Med3Mul4{0xd7570001, 0x14020100, 0xbf810000}; +alignas(256) constexpr std::array DivFixupMul2{0xd75f0001, 0x0c020100, 0xbf810000}; + constexpr std::uint32_t Rows[32][4] = { {0x00000000u, 0x0000ec99u, 0x000073abu, 0x0000965eu}, {0x0000c000u, 0x000079cbu, 0x0000cb00u, 0x00004400u}, @@ -151,7 +158,7 @@ void ExpectHalf(std::uint32_t tid, std::uint32_t actual, std::uint32_t expected, Expect(tid, actual, expected, name); } -void Run(AgcDriver::VulkanDevice& device, const std::optional& floatMode) { +void Run(AgcDriver::VulkanDevice& device, std::span code, const std::optional& floatMode) { for (std::uint32_t tid = 0; tid < Threads; ++tid) Fill(tid, &Input[tid * Inputs]); Output.fill(0xdeadbeefu); std::vector userData(8, 0u); @@ -159,7 +166,6 @@ void Run(AgcDriver::VulkanDevice& device, const std::optional(Output.size() * 4u)); std::copy(input.begin(), input.end(), userData.begin()); std::copy(output.begin(), output.end(), userData.begin() + 4); - const std::span code(Code); const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; ShaderRecompiler::RecompileRequest request{ @@ -183,23 +189,37 @@ void Check() { } } +bool RefusesOutputModifier(AgcDriver::VulkanDevice& device, std::span code) { + try { + Run(device, code, ShaderRecompiler::ShaderFloatMode{0x00u, true, false, false}); + } catch (const std::runtime_error& error) { + return std::string(error.what()).find("output modifier on an f16") != std::string::npos; + } + return false; +} + } int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - Run(*device, std::nullopt); + Run(*device, Code, std::nullopt); Check(); - Run(*device, ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}); + Run(*device, Code, ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}); Check(); - bool refused = false; - try { - Run(*device, ShaderRecompiler::ShaderFloatMode{0x00u, true, false, false}); - } catch (const std::runtime_error& error) { - refused = std::string(error.what()).find("output modifier on an f16") != std::string::npos; + Require(RefusesOutputModifier(*device, Code), "float16 modifiers: an output modifier with f16 output denormals flushed was not refused"); + const std::pair, const char*> ternaries[] = { + {Min3Div2, "v_min3_f16 div:2"}, + {Max3Mul2, "v_max3_f16 mul:2"}, + {Med3Mul4, "v_med3_f16 mul:4"}, + {DivFixupMul2, "v_div_fixup_f16 mul:2"}, + }; + for (const auto& [code, name] : ternaries) { + Run(*device, code, std::nullopt); + Run(*device, code, ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}); + Require(RefusesOutputModifier(*device, code), std::string("float16 modifiers: ") + name + " with f16 output denormals flushed was not refused"); } - Require(refused, "float16 modifiers: an output modifier with f16 output denormals flushed was not refused"); std::puts("float16 modifiers tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/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/Float16Rounding.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Float16Rounding.cpp index ec1c19913f..838b177130 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Float16Rounding.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Float16Rounding.cpp @@ -292,6 +292,342 @@ constexpr Vector Vectors[] = { {0x498f4a8bu, 0x6749af47u, 0xc69e8021u, {0xabcd7c00u, 0xabcdaf47u, 0xabcd0004u}}, }; +alignas(256) constexpr std::array DirectedCode{ + 0x34020084u, 0x34040086u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e140280u, 0x7e160280u, 0x7e180280u, + 0x7e1a0280u, 0x7e1c0280u, 0x7e1e0280u, 0x7e200280u, 0x7e220280u, 0x7e240280u, 0x7e260280u, 0x7e280280u, + 0x7e2a0280u, 0x7e2c0280u, 0x7e2e0280u, 0x7e300280u, 0x7e320280u, 0x7e1402ffu, 0x5a5a5a5au, 0x7e1602ffu, + 0x5a5a5a5au, 0x7e1802ffu, 0x5a5a5a5au, 0x7e1a02ffu, 0x5a5a5a5au, 0x7e141504u, 0x7e16a305u, 0x7e18a105u, + 0x761a0f06u, 0xe0781000u, 0x80010a02u, 0xe0781010u, 0x80010e02u, 0xe0781020u, 0x80011202u, 0xe0781030u, + 0x80011602u, 0xbf810000u, +}; + +struct DirectedVector { + std::array inputs; + std::array, 2>, 3> expected; +}; + +constexpr DirectedVector DirectedVectors[] = { + {{0x00000000u, 0x5a5a0000u, 0xa5a50000u, 0x00000000u}, {{ + {{{0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}}, + {{{0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}}, + {{{0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}}, + }}}, + {{0x80000000u, 0x5a5a0001u, 0xa5a58000u, 0x00000001u}, {{ + {{{0x5a5a8000u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a8000u}, {0x5a5a8000u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a8000u}}}, + {{{0x5a5a8000u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a8000u}, {0x5a5a8000u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a8000u}}}, + {{{0x5a5a8000u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a8000u}, {0x5a5a8000u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a8000u}}}, + }}}, + {{0x3f800000u, 0x5a5a8001u, 0xa5a53c00u, 0xffffffe8u}, {{ + {{{0x5a5a3c00u, 0x5a5af7ffu, 0x5a5a7801u, 0x5a5a0001u}, {0x5a5a3c00u, 0x5a5af7ffu, 0x5a5a7801u, 0x5a5a0001u}}}, + {{{0x5a5a3c00u, 0x5a5af800u, 0x5a5a7800u, 0x5a5a0001u}, {0x5a5a3c00u, 0x5a5af800u, 0x5a5a7800u, 0x5a5a0001u}}}, + {{{0x5a5a3c00u, 0x5a5af7ffu, 0x5a5a7800u, 0x5a5a0001u}, {0x5a5a3c00u, 0x5a5af7ffu, 0x5a5a7800u, 0x5a5a0001u}}}, + }}}, + {{0xbf800000u, 0x5a5affffu, 0xa5a5bc00u, 0x00000040u}, {{ + {{{0x5a5abc00u, 0x5a5abc00u, 0x5a5a7c00u, 0x5a5afbffu}, {0x5a5abc00u, 0x5a5abc00u, 0x5a5a7bffu, 0x5a5afbffu}}}, + {{{0x5a5abc00u, 0x5a5abc00u, 0x5a5a7bffu, 0x5a5afc00u}, {0x5a5abc00u, 0x5a5abc00u, 0x5a5a7bffu, 0x5a5afbffu}}}, + {{{0x5a5abc00u, 0x5a5abc00u, 0x5a5a7bffu, 0x5a5afbffu}, {0x5a5abc00u, 0x5a5abc00u, 0x5a5a7bffu, 0x5a5afbffu}}}, + }}}, + {{0x3f800001u, 0x5a5a0000u, 0xa5a53c01u, 0xffffffc0u}, {{ + {{{0x5a5a3c01u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0001u}, {0x5a5a3c01u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0001u}}}, + {{{0x5a5a3c00u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}, {0x5a5a3c00u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}}, + {{{0x5a5a3c00u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}, {0x5a5a3c00u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}}, + }}}, + {{0xbf800001u, 0x5a5a0001u, 0xa5a5bc01u, 0x00000041u}, {{ + {{{0x5a5abc00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5afbffu}, {0x5a5abc00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5afbffu}}}, + {{{0x5a5abc01u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5afc00u}, {0x5a5abc01u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5afbffu}}}, + {{{0x5a5abc00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5afbffu}, {0x5a5abc00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5afbffu}}}, + }}}, + {{0x3f801000u, 0x5a5a07ffu, 0xa5a57bffu, 0xffffffbfu}, {{ + {{{0x5a5a3c01u, 0x5a5a67ffu, 0x5a5a67ffu, 0x5a5a0001u}, {0x5a5a3c01u, 0x5a5a67ffu, 0x5a5a67ffu, 0x5a5a0001u}}}, + {{{0x5a5a3c00u, 0x5a5a67ffu, 0x5a5a67ffu, 0x5a5a0000u}, {0x5a5a3c00u, 0x5a5a67ffu, 0x5a5a67ffu, 0x5a5a0000u}}}, + {{{0x5a5a3c00u, 0x5a5a67ffu, 0x5a5a67ffu, 0x5a5a0000u}, {0x5a5a3c00u, 0x5a5a67ffu, 0x5a5a67ffu, 0x5a5a0000u}}}, + }}}, + {{0xbf801000u, 0x5a5a0800u, 0xa5a5fbffu, 0x00007fffu}, {{ + {{{0x5a5abc00u, 0x5a5a6800u, 0x5a5a6800u, 0x5a5afbffu}, {0x5a5abc00u, 0x5a5a6800u, 0x5a5a6800u, 0x5a5afbffu}}}, + {{{0x5a5abc01u, 0x5a5a6800u, 0x5a5a6800u, 0x5a5afc00u}, {0x5a5abc01u, 0x5a5a6800u, 0x5a5a6800u, 0x5a5afbffu}}}, + {{{0x5a5abc00u, 0x5a5a6800u, 0x5a5a6800u, 0x5a5afbffu}, {0x5a5abc00u, 0x5a5a6800u, 0x5a5a6800u, 0x5a5afbffu}}}, + }}}, + {{0x3f801fffu, 0x5a5a0801u, 0xa5a57c00u, 0xffff8000u}, {{ + {{{0x5a5a3c01u, 0x5a5a6801u, 0x5a5a6801u, 0x5a5a7c00u}, {0x5a5a3c01u, 0x5a5a6801u, 0x5a5a6801u, 0x5a5a7c00u}}}, + {{{0x5a5a3c00u, 0x5a5a6800u, 0x5a5a6800u, 0x5a5a7c00u}, {0x5a5a3c00u, 0x5a5a6800u, 0x5a5a6800u, 0x5a5a7c00u}}}, + {{{0x5a5a3c00u, 0x5a5a6800u, 0x5a5a6800u, 0x5a5a7c00u}, {0x5a5a3c00u, 0x5a5a6800u, 0x5a5a6800u, 0x5a5a7c00u}}}, + }}}, + {{0xb3000000u, 0x5a5a7fffu, 0xa5a57e55u, 0x00000001u}, {{ + {{{0x5a5a8000u, 0x5a5a7800u, 0x5a5a7800u, 0x5a5a7e55u}, {0x5a5a8000u, 0x5a5a7800u, 0x5a5a7800u, 0x5a5a7e55u}}}, + {{{0x5a5a8001u, 0x5a5a77ffu, 0x5a5a77ffu, 0x5a5a7e55u}, {0x5a5a8001u, 0x5a5a77ffu, 0x5a5a77ffu, 0x5a5a7e55u}}}, + {{{0x5a5a8000u, 0x5a5a77ffu, 0x5a5a77ffu, 0x5a5a7e55u}, {0x5a5a8000u, 0x5a5a77ffu, 0x5a5a77ffu, 0x5a5a7e55u}}}, + }}}, + {{0x33000001u, 0x5a5a8000u, 0xa5a5fe55u, 0xffffffffu}, {{ + {{{0x5a5a0001u, 0x5a5af800u, 0x5a5a7800u, 0x5a5afe55u}, {0x5a5a0001u, 0x5a5af800u, 0x5a5a7800u, 0x5a5afe55u}}}, + {{{0x5a5a0000u, 0x5a5af800u, 0x5a5a7800u, 0x5a5afe55u}, {0x5a5a0000u, 0x5a5af800u, 0x5a5a7800u, 0x5a5afe55u}}}, + {{{0x5a5a0000u, 0x5a5af800u, 0x5a5a7800u, 0x5a5afe55u}, {0x5a5a0000u, 0x5a5af800u, 0x5a5a7800u, 0x5a5afe55u}}}, + }}}, + {{0x33800000u, 0x5a5a8001u, 0xa5a57c01u, 0x0000000au}, {{ + {{{0x5a5a0001u, 0x5a5af7ffu, 0x5a5a7801u, 0x5a5a7e01u}, {0x5a5a0001u, 0x5a5af7ffu, 0x5a5a7801u, 0x5a5a7e01u}}}, + {{{0x5a5a0001u, 0x5a5af800u, 0x5a5a7800u, 0x5a5a7e01u}, {0x5a5a0001u, 0x5a5af800u, 0x5a5a7800u, 0x5a5a7e01u}}}, + {{{0x5a5a0001u, 0x5a5af7ffu, 0x5a5a7800u, 0x5a5a7e01u}, {0x5a5a0001u, 0x5a5af7ffu, 0x5a5a7800u, 0x5a5a7e01u}}}, + }}}, + {{0xb37fffffu, 0x5a5a0000u, 0xa5a50000u, 0x00000010u}, {{ + {{{0x5a5a8000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}, {0x5a5a8000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}}, + {{{0x5a5a8001u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}, {0x5a5a8001u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}}, + {{{0x5a5a8000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}, {0x5a5a8000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}}, + }}}, + {{0x387fffffu, 0x5a5a0001u, 0xa5a58000u, 0xfffffff0u}, {{ + {{{0x5a5a0400u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a8000u}, {0x5a5a0400u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a8000u}}}, + {{{0x5a5a03ffu, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a8000u}, {0x5a5a03ffu, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a8000u}}}, + {{{0x5a5a03ffu, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a8000u}, {0x5a5a03ffu, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a8000u}}}, + }}}, + {{0x477fe000u, 0x5a5a0801u, 0xa5a503ffu, 0x00000040u}, {{ + {{{0x5a5a7bffu, 0x5a5a6801u, 0x5a5a6801u, 0x5a5a7c00u}, {0x5a5a7bffu, 0x5a5a6801u, 0x5a5a6801u, 0x5a5a7bffu}}}, + {{{0x5a5a7bffu, 0x5a5a6800u, 0x5a5a6800u, 0x5a5a7bffu}, {0x5a5a7bffu, 0x5a5a6800u, 0x5a5a6800u, 0x5a5a7bffu}}}, + {{{0x5a5a7bffu, 0x5a5a6800u, 0x5a5a6800u, 0x5a5a7bffu}, {0x5a5a7bffu, 0x5a5a6800u, 0x5a5a6800u, 0x5a5a7bffu}}}, + }}}, + {{0x477fe001u, 0x5a5a0803u, 0xa5a583ffu, 0xffffffc0u}, {{ + {{{0x5a5a7c00u, 0x5a5a6802u, 0x5a5a6802u, 0x5a5a8000u}, {0x5a5a7bffu, 0x5a5a6802u, 0x5a5a6802u, 0x5a5a8000u}}}, + {{{0x5a5a7bffu, 0x5a5a6801u, 0x5a5a6801u, 0x5a5a8001u}, {0x5a5a7bffu, 0x5a5a6801u, 0x5a5a6801u, 0x5a5a8001u}}}, + {{{0x5a5a7bffu, 0x5a5a6801u, 0x5a5a6801u, 0x5a5a8000u}, {0x5a5a7bffu, 0x5a5a6801u, 0x5a5a6801u, 0x5a5a8000u}}}, + }}}, + {{0xc77fe001u, 0x5a5a7fffu, 0xa5a50400u, 0x00000041u}, {{ + {{{0x5a5afbffu, 0x5a5a7800u, 0x5a5a7800u, 0x5a5a7c00u}, {0x5a5afbffu, 0x5a5a7800u, 0x5a5a7800u, 0x5a5a7bffu}}}, + {{{0x5a5afc00u, 0x5a5a77ffu, 0x5a5a77ffu, 0x5a5a7bffu}, {0x5a5afbffu, 0x5a5a77ffu, 0x5a5a77ffu, 0x5a5a7bffu}}}, + {{{0x5a5afbffu, 0x5a5a77ffu, 0x5a5a77ffu, 0x5a5a7bffu}, {0x5a5afbffu, 0x5a5a77ffu, 0x5a5a77ffu, 0x5a5a7bffu}}}, + }}}, + {{0x477ff000u, 0x5a5a8000u, 0xa5a58400u, 0xffffffbfu}, {{ + {{{0x5a5a7c00u, 0x5a5af800u, 0x5a5a7800u, 0x5a5a8000u}, {0x5a5a7bffu, 0x5a5af800u, 0x5a5a7800u, 0x5a5a8000u}}}, + {{{0x5a5a7bffu, 0x5a5af800u, 0x5a5a7800u, 0x5a5a8001u}, {0x5a5a7bffu, 0x5a5af800u, 0x5a5a7800u, 0x5a5a8001u}}}, + {{{0x5a5a7bffu, 0x5a5af800u, 0x5a5a7800u, 0x5a5a8000u}, {0x5a5a7bffu, 0x5a5af800u, 0x5a5a7800u, 0x5a5a8000u}}}, + }}}, + {{0x47800000u, 0x5a5a8001u, 0xa5a53c00u, 0x00007fffu}, {{ + {{{0x5a5a7c00u, 0x5a5af7ffu, 0x5a5a7801u, 0x5a5a7c00u}, {0x5a5a7bffu, 0x5a5af7ffu, 0x5a5a7801u, 0x5a5a7bffu}}}, + {{{0x5a5a7bffu, 0x5a5af800u, 0x5a5a7800u, 0x5a5a7bffu}, {0x5a5a7bffu, 0x5a5af800u, 0x5a5a7800u, 0x5a5a7bffu}}}, + {{{0x5a5a7bffu, 0x5a5af7ffu, 0x5a5a7800u, 0x5a5a7bffu}, {0x5a5a7bffu, 0x5a5af7ffu, 0x5a5a7800u, 0x5a5a7bffu}}}, + }}}, + {{0xc7800000u, 0x5a5affffu, 0xa5a5bc00u, 0xffff8000u}, {{ + {{{0x5a5afbffu, 0x5a5abc00u, 0x5a5a7c00u, 0x5a5a8000u}, {0x5a5afbffu, 0x5a5abc00u, 0x5a5a7bffu, 0x5a5a8000u}}}, + {{{0x5a5afc00u, 0x5a5abc00u, 0x5a5a7bffu, 0x5a5a8001u}, {0x5a5afbffu, 0x5a5abc00u, 0x5a5a7bffu, 0x5a5a8001u}}}, + {{{0x5a5afbffu, 0x5a5abc00u, 0x5a5a7bffu, 0x5a5a8000u}, {0x5a5afbffu, 0x5a5abc00u, 0x5a5a7bffu, 0x5a5a8000u}}}, + }}}, + {{0x7f7fffffu, 0x5a5a0000u, 0xa5a53c01u, 0x00000000u}, {{ + {{{0x5a5a7c00u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a3c01u}, {0x5a5a7bffu, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a3c01u}}}, + {{{0x5a5a7bffu, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a3c01u}, {0x5a5a7bffu, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a3c01u}}}, + {{{0x5a5a7bffu, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a3c01u}, {0x5a5a7bffu, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a3c01u}}}, + }}}, + {{0xff7fffffu, 0x5a5a0001u, 0xa5a5bc01u, 0x00000001u}, {{ + {{{0x5a5afbffu, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5ac001u}, {0x5a5afbffu, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5ac001u}}}, + {{{0x5a5afc00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5ac001u}, {0x5a5afbffu, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5ac001u}}}, + {{{0x5a5afbffu, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5ac001u}, {0x5a5afbffu, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5ac001u}}}, + }}}, + {{0x7f800000u, 0x5a5a07ffu, 0xa5a57bffu, 0xffffffffu}, {{ + {{{0x5a5a7c00u, 0x5a5a67ffu, 0x5a5a67ffu, 0x5a5a77ffu}, {0x5a5a7c00u, 0x5a5a67ffu, 0x5a5a67ffu, 0x5a5a77ffu}}}, + {{{0x5a5a7c00u, 0x5a5a67ffu, 0x5a5a67ffu, 0x5a5a77ffu}, {0x5a5a7c00u, 0x5a5a67ffu, 0x5a5a67ffu, 0x5a5a77ffu}}}, + {{{0x5a5a7c00u, 0x5a5a67ffu, 0x5a5a67ffu, 0x5a5a77ffu}, {0x5a5a7c00u, 0x5a5a67ffu, 0x5a5a67ffu, 0x5a5a77ffu}}}, + }}}, + {{0xff800000u, 0x5a5a0800u, 0xa5a5fbffu, 0x0000000au}, {{ + {{{0x5a5afc00u, 0x5a5a6800u, 0x5a5a6800u, 0x5a5afbffu}, {0x5a5afc00u, 0x5a5a6800u, 0x5a5a6800u, 0x5a5afbffu}}}, + {{{0x5a5afc00u, 0x5a5a6800u, 0x5a5a6800u, 0x5a5afc00u}, {0x5a5afc00u, 0x5a5a6800u, 0x5a5a6800u, 0x5a5afbffu}}}, + {{{0x5a5afc00u, 0x5a5a6800u, 0x5a5a6800u, 0x5a5afbffu}, {0x5a5afc00u, 0x5a5a6800u, 0x5a5a6800u, 0x5a5afbffu}}}, + }}}, + {{0x7fc12345u, 0x5a5a0801u, 0xa5a57c00u, 0xfffffff6u}, {{ + {{{0x5a5a7e09u, 0x5a5a6801u, 0x5a5a6801u, 0x5a5a7c00u}, {0x5a5a7e09u, 0x5a5a6801u, 0x5a5a6801u, 0x5a5a7c00u}}}, + {{{0x5a5a7e09u, 0x5a5a6800u, 0x5a5a6800u, 0x5a5a7c00u}, {0x5a5a7e09u, 0x5a5a6800u, 0x5a5a6800u, 0x5a5a7c00u}}}, + {{{0x5a5a7e09u, 0x5a5a6800u, 0x5a5a6800u, 0x5a5a7c00u}, {0x5a5a7e09u, 0x5a5a6800u, 0x5a5a6800u, 0x5a5a7c00u}}}, + }}}, + {{0xffc12345u, 0x5a5a0803u, 0xa5a5fc00u, 0x00000010u}, {{ + {{{0x5a5afe09u, 0x5a5a6802u, 0x5a5a6802u, 0x5a5afc00u}, {0x5a5afe09u, 0x5a5a6802u, 0x5a5a6802u, 0x5a5afc00u}}}, + {{{0x5a5afe09u, 0x5a5a6801u, 0x5a5a6801u, 0x5a5afc00u}, {0x5a5afe09u, 0x5a5a6801u, 0x5a5a6801u, 0x5a5afc00u}}}, + {{{0x5a5afe09u, 0x5a5a6801u, 0x5a5a6801u, 0x5a5afc00u}, {0x5a5afe09u, 0x5a5a6801u, 0x5a5a6801u, 0x5a5afc00u}}}, + }}}, + {{0x7f812345u, 0x5a5a7fffu, 0xa5a57e55u, 0xfffffff0u}, {{ + {{{0x5a5a7e09u, 0x5a5a7800u, 0x5a5a7800u, 0x5a5a7e55u}, {0x5a5a7e09u, 0x5a5a7800u, 0x5a5a7800u, 0x5a5a7e55u}}}, + {{{0x5a5a7e09u, 0x5a5a77ffu, 0x5a5a77ffu, 0x5a5a7e55u}, {0x5a5a7e09u, 0x5a5a77ffu, 0x5a5a77ffu, 0x5a5a7e55u}}}, + {{{0x5a5a7e09u, 0x5a5a77ffu, 0x5a5a77ffu, 0x5a5a7e55u}, {0x5a5a7e09u, 0x5a5a77ffu, 0x5a5a77ffu, 0x5a5a7e55u}}}, + }}}, + {{0xff812345u, 0x5a5a8000u, 0xa5a5fe55u, 0x00000018u}, {{ + {{{0x5a5afe09u, 0x5a5af800u, 0x5a5a7800u, 0x5a5afe55u}, {0x5a5afe09u, 0x5a5af800u, 0x5a5a7800u, 0x5a5afe55u}}}, + {{{0x5a5afe09u, 0x5a5af800u, 0x5a5a7800u, 0x5a5afe55u}, {0x5a5afe09u, 0x5a5af800u, 0x5a5a7800u, 0x5a5afe55u}}}, + {{{0x5a5afe09u, 0x5a5af800u, 0x5a5a7800u, 0x5a5afe55u}, {0x5a5afe09u, 0x5a5af800u, 0x5a5a7800u, 0x5a5afe55u}}}, + }}}, + {{0x00000000u, 0x00000000u, 0xa5a53c01u, 0x00000010u}, {{ + {{{0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a7c00u}, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a7bffu}}}, + {{{0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a7bffu}, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a7bffu}}}, + {{{0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a7bffu}, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a7bffu}}}, + }}}, + {{0x00000000u, 0x00000000u, 0xa5a5bc01u, 0x00000010u}, {{ + {{{0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5afbffu}, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5afbffu}}}, + {{{0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5afc00u}, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5afbffu}}}, + {{{0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5afbffu}, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5afbffu}}}, + }}}, + {{0x00000001u, 0x5a5a07ffu, 0xa5a50001u, 0xffffffffu}, {{ + {{{0x5a5a0001u, 0x5a5a67ffu, 0x5a5a67ffu, 0x5a5a0001u}, {0x5a5a0001u, 0x5a5a67ffu, 0x5a5a67ffu, 0x5a5a0001u}}}, + {{{0x5a5a0000u, 0x5a5a67ffu, 0x5a5a67ffu, 0x5a5a0000u}, {0x5a5a0000u, 0x5a5a67ffu, 0x5a5a67ffu, 0x5a5a0000u}}}, + {{{0x5a5a0000u, 0x5a5a67ffu, 0x5a5a67ffu, 0x5a5a0000u}, {0x5a5a0000u, 0x5a5a67ffu, 0x5a5a67ffu, 0x5a5a0000u}}}, + }}}, + {{0x00000000u, 0x00000000u, 0xa5a58001u, 0xffffffffu}, {{ + {{{0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a8000u}, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a8000u}}}, + {{{0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a8001u}, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a8001u}}}, + {{{0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a8000u}, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a8000u}}}, + }}}, + {{0x00000000u, 0x00000000u, 0xa5a503ffu, 0x00000000u}, {{ + {{{0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a03ffu}, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a03ffu}}}, + {{{0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a03ffu}, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a03ffu}}}, + {{{0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a03ffu}, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a03ffu}}}, + }}}, + {{0x00000000u, 0x00000000u, 0xa5a583ffu, 0x00000000u}, {{ + {{{0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a83ffu}, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a83ffu}}}, + {{{0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a83ffu}, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a83ffu}}}, + {{{0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a83ffu}, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a83ffu}}}, + }}}, + {{0x00000000u, 0x00000000u, 0xa5a57bffu, 0x00007fffu}, {{ + {{{0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a7c00u}, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a7bffu}}}, + {{{0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a7bffu}, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a7bffu}}}, + {{{0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a7bffu}, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a7bffu}}}, + }}}, + {{0x00000000u, 0x00000000u, 0xa5a5fbffu, 0xffff8000u}, {{ + {{{0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a8000u}, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a8000u}}}, + {{{0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a8001u}, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a8001u}}}, + {{{0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a8000u}, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a8000u}}}, + }}}, +}; + +constexpr Vector DenormalVectors[4][32] = { + { + {0u, 0x00000401u, 0x000003ffu, {0u, 0xabcd0401u, 0xabcd0000u}}, + {0u, 0x00000400u, 0x00008001u, {0u, 0xabcd0400u, 0xabcd8000u}}, + {0u, 0x00000001u, 0x00000001u, {0u, 0xabcd0000u, 0xabcd0000u}}, + {0u, 0x000003ffu, 0x00000001u, {0u, 0xabcd0000u, 0xabcd0000u}}, + {0u, 0x00008001u, 0x00008001u, {0u, 0xabcd8000u, 0xabcd0000u}}, + {0u, 0x000003ffu, 0x00008001u, {0u, 0xabcd0000u, 0xabcd8000u}}, + {0u, 0x00000400u, 0x00008400u, {0u, 0xabcd0000u, 0xabcd8000u}}, + {0u, 0x00008400u, 0x00000001u, {0u, 0xabcd8400u, 0xabcd8000u}}, + {0u, 0x00003c00u, 0x00000001u, {0u, 0xabcd3c00u, 0xabcd0000u}}, + {0u, 0x0000bc00u, 0x00000001u, {0u, 0xabcdbc00u, 0xabcd8000u}}, + {0u, 0x00003c00u, 0x00008001u, {0u, 0xabcd3c00u, 0xabcd8000u}}, + {0u, 0x00003bffu, 0x00000400u, {0u, 0xabcd3bffu, 0xabcd0000u}}, + {0u, 0x00003c01u, 0x000003ffu, {0u, 0xabcd3c01u, 0xabcd0000u}}, + {0u, 0x0000bc01u, 0x000003ffu, {0u, 0xabcdbc01u, 0xabcd8000u}}, + {0u, 0x00003bffu, 0x00008400u, {0u, 0xabcd3bffu, 0xabcd8000u}}, + {0u, 0x00003800u, 0x00000400u, {0u, 0xabcd3800u, 0xabcd0000u}}, + {0u, 0x0000b800u, 0x00000400u, {0u, 0xabcdb800u, 0xabcd8000u}}, + {0u, 0x00003800u, 0x00000401u, {0u, 0xabcd3800u, 0xabcd0000u}}, + {0u, 0x00003800u, 0x000003ffu, {0u, 0xabcd3800u, 0xabcd0000u}}, + {0u, 0x00000400u, 0x00000400u, {0u, 0xabcd0800u, 0xabcd0000u}}, + {0u, 0x00003c00u, 0x00000400u, {0u, 0xabcd3c00u, 0xabcd0400u}}, + {0u, 0x00007c00u, 0x00000001u, {0u, 0xabcd7c00u, 0xabcdfe00u}}, + {0u, 0x0000fc00u, 0x00008001u, {0u, 0xabcdfc00u, 0xabcdfe00u}}, + {0u, 0x00000001u, 0x00007c00u, {0u, 0xabcd7c00u, 0xabcdfe00u}}, + {0u, 0x00000000u, 0x00008000u, {0u, 0xabcd0000u, 0xabcd8000u}}, + {0u, 0x00008000u, 0x00008000u, {0u, 0xabcd8000u, 0xabcd0000u}}, + {0u, 0x00003c01u, 0x0000bc00u, {0u, 0xabcd1400u, 0xabcdbc01u}}, + {0u, 0x00007bffu, 0x00003c01u, {0u, 0xabcd7bffu, 0xabcd7c00u}}, + {0u, 0x00007e55u, 0x00000001u, {0u, 0xabcd7e55u, 0xabcd7e55u}}, + {0u, 0x00000001u, 0x0000fe01u, {0u, 0xabcdfe01u, 0xabcdfe01u}}, + {0u, 0x00007c01u, 0x000003ffu, {0u, 0xabcd7e01u, 0xabcd7e01u}}, + {0u, 0x000003ffu, 0x0000fd23u, {0u, 0xabcdff23u, 0xabcdff23u}}, + }, + { + {0u, 0x00000401u, 0x000003ffu, {0u, 0xabcd0800u, 0xabcd0000u}}, + {0u, 0x00000400u, 0x00008001u, {0u, 0xabcd0000u, 0xabcd8000u}}, + {0u, 0x00000001u, 0x00000001u, {0u, 0xabcd0000u, 0xabcd0000u}}, + {0u, 0x000003ffu, 0x00000001u, {0u, 0xabcd0400u, 0xabcd0000u}}, + {0u, 0x00008001u, 0x00008001u, {0u, 0xabcd8000u, 0xabcd0000u}}, + {0u, 0x000003ffu, 0x00008001u, {0u, 0xabcd0000u, 0xabcd8000u}}, + {0u, 0x00000400u, 0x00008400u, {0u, 0xabcd0000u, 0xabcd8000u}}, + {0u, 0x00008400u, 0x00000001u, {0u, 0xabcd8000u, 0xabcd8000u}}, + {0u, 0x00003c00u, 0x00000001u, {0u, 0xabcd3c00u, 0xabcd0000u}}, + {0u, 0x0000bc00u, 0x00000001u, {0u, 0xabcdbc00u, 0xabcd8000u}}, + {0u, 0x00003c00u, 0x00008001u, {0u, 0xabcd3c00u, 0xabcd8000u}}, + {0u, 0x00003bffu, 0x00000400u, {0u, 0xabcd3bffu, 0xabcd0000u}}, + {0u, 0x00003c01u, 0x000003ffu, {0u, 0xabcd3c01u, 0xabcd0400u}}, + {0u, 0x0000bc01u, 0x000003ffu, {0u, 0xabcdbc01u, 0xabcd8400u}}, + {0u, 0x00003bffu, 0x00008400u, {0u, 0xabcd3bffu, 0xabcd8000u}}, + {0u, 0x00003800u, 0x00000400u, {0u, 0xabcd3800u, 0xabcd0000u}}, + {0u, 0x0000b800u, 0x00000400u, {0u, 0xabcdb800u, 0xabcd8000u}}, + {0u, 0x00003800u, 0x00000401u, {0u, 0xabcd3800u, 0xabcd0000u}}, + {0u, 0x00003800u, 0x000003ffu, {0u, 0xabcd3800u, 0xabcd0000u}}, + {0u, 0x00000400u, 0x00000400u, {0u, 0xabcd0800u, 0xabcd0000u}}, + {0u, 0x00003c00u, 0x00000400u, {0u, 0xabcd3c00u, 0xabcd0400u}}, + {0u, 0x00007c00u, 0x00000001u, {0u, 0xabcd7c00u, 0xabcd7c00u}}, + {0u, 0x0000fc00u, 0x00008001u, {0u, 0xabcdfc00u, 0xabcd7c00u}}, + {0u, 0x00000001u, 0x00007c00u, {0u, 0xabcd7c00u, 0xabcd7c00u}}, + {0u, 0x00000000u, 0x00008000u, {0u, 0xabcd0000u, 0xabcd8000u}}, + {0u, 0x00008000u, 0x00008000u, {0u, 0xabcd8000u, 0xabcd0000u}}, + {0u, 0x00003c01u, 0x0000bc00u, {0u, 0xabcd1400u, 0xabcdbc01u}}, + {0u, 0x00007bffu, 0x00003c01u, {0u, 0xabcd7bffu, 0xabcd7c00u}}, + {0u, 0x00007e55u, 0x00000001u, {0u, 0xabcd7e55u, 0xabcd7e55u}}, + {0u, 0x00000001u, 0x0000fe01u, {0u, 0xabcdfe01u, 0xabcdfe01u}}, + {0u, 0x00007c01u, 0x000003ffu, {0u, 0xabcd7e01u, 0xabcd7e01u}}, + {0u, 0x000003ffu, 0x0000fd23u, {0u, 0xabcdff23u, 0xabcdff23u}}, + }, + { + {0u, 0x00000401u, 0x000003ffu, {0u, 0xabcd0401u, 0xabcd0000u}}, + {0u, 0x00000400u, 0x00008001u, {0u, 0xabcd0400u, 0xabcd8000u}}, + {0u, 0x00000001u, 0x00000001u, {0u, 0xabcd0000u, 0xabcd0000u}}, + {0u, 0x000003ffu, 0x00000001u, {0u, 0xabcd0000u, 0xabcd0000u}}, + {0u, 0x00008001u, 0x00008001u, {0u, 0xabcd8000u, 0xabcd0000u}}, + {0u, 0x000003ffu, 0x00008001u, {0u, 0xabcd0000u, 0xabcd8000u}}, + {0u, 0x00000400u, 0x00008400u, {0u, 0xabcd0000u, 0xabcd8000u}}, + {0u, 0x00008400u, 0x00000001u, {0u, 0xabcd8400u, 0xabcd8000u}}, + {0u, 0x00003c00u, 0x00000001u, {0u, 0xabcd3c00u, 0xabcd0000u}}, + {0u, 0x0000bc00u, 0x00000001u, {0u, 0xabcdbc00u, 0xabcd8000u}}, + {0u, 0x00003c00u, 0x00008001u, {0u, 0xabcd3c00u, 0xabcd8000u}}, + {0u, 0x00003bffu, 0x00000400u, {0u, 0xabcd3bffu, 0xabcd0400u}}, + {0u, 0x00003c01u, 0x000003ffu, {0u, 0xabcd3c01u, 0xabcd0000u}}, + {0u, 0x0000bc01u, 0x000003ffu, {0u, 0xabcdbc01u, 0xabcd8000u}}, + {0u, 0x00003bffu, 0x00008400u, {0u, 0xabcd3bffu, 0xabcd8400u}}, + {0u, 0x00003800u, 0x00000400u, {0u, 0xabcd3800u, 0xabcd0200u}}, + {0u, 0x0000b800u, 0x00000400u, {0u, 0xabcdb800u, 0xabcd8200u}}, + {0u, 0x00003800u, 0x00000401u, {0u, 0xabcd3800u, 0xabcd0200u}}, + {0u, 0x00003800u, 0x000003ffu, {0u, 0xabcd3800u, 0xabcd0000u}}, + {0u, 0x00000400u, 0x00000400u, {0u, 0xabcd0800u, 0xabcd0000u}}, + {0u, 0x00003c00u, 0x00000400u, {0u, 0xabcd3c00u, 0xabcd0400u}}, + {0u, 0x00007c00u, 0x00000001u, {0u, 0xabcd7c00u, 0xabcdfe00u}}, + {0u, 0x0000fc00u, 0x00008001u, {0u, 0xabcdfc00u, 0xabcdfe00u}}, + {0u, 0x00000001u, 0x00007c00u, {0u, 0xabcd7c00u, 0xabcdfe00u}}, + {0u, 0x00000000u, 0x00008000u, {0u, 0xabcd0000u, 0xabcd8000u}}, + {0u, 0x00008000u, 0x00008000u, {0u, 0xabcd8000u, 0xabcd0000u}}, + {0u, 0x00003c01u, 0x0000bc00u, {0u, 0xabcd1400u, 0xabcdbc01u}}, + {0u, 0x00007bffu, 0x00003c01u, {0u, 0xabcd7bffu, 0xabcd7c00u}}, + {0u, 0x00007e55u, 0x00000001u, {0u, 0xabcd7e55u, 0xabcd7e55u}}, + {0u, 0x00000001u, 0x0000fe01u, {0u, 0xabcdfe01u, 0xabcdfe01u}}, + {0u, 0x00007c01u, 0x000003ffu, {0u, 0xabcd7e01u, 0xabcd7e01u}}, + {0u, 0x000003ffu, 0x0000fd23u, {0u, 0xabcdff23u, 0xabcdff23u}}, + }, + { + {0u, 0x00000401u, 0x000003ffu, {0u, 0xabcd0800u, 0xabcd0000u}}, + {0u, 0x00000400u, 0x00008001u, {0u, 0xabcd03ffu, 0xabcd8000u}}, + {0u, 0x00000001u, 0x00000001u, {0u, 0xabcd0002u, 0xabcd0000u}}, + {0u, 0x000003ffu, 0x00000001u, {0u, 0xabcd0400u, 0xabcd0000u}}, + {0u, 0x00008001u, 0x00008001u, {0u, 0xabcd8002u, 0xabcd0000u}}, + {0u, 0x000003ffu, 0x00008001u, {0u, 0xabcd03feu, 0xabcd8000u}}, + {0u, 0x00000400u, 0x00008400u, {0u, 0xabcd0000u, 0xabcd8000u}}, + {0u, 0x00008400u, 0x00000001u, {0u, 0xabcd83ffu, 0xabcd8000u}}, + {0u, 0x00003c00u, 0x00000001u, {0u, 0xabcd3c00u, 0xabcd0001u}}, + {0u, 0x0000bc00u, 0x00000001u, {0u, 0xabcdbc00u, 0xabcd8001u}}, + {0u, 0x00003c00u, 0x00008001u, {0u, 0xabcd3c00u, 0xabcd8001u}}, + {0u, 0x00003bffu, 0x00000400u, {0u, 0xabcd3bffu, 0xabcd0400u}}, + {0u, 0x00003c01u, 0x000003ffu, {0u, 0xabcd3c01u, 0xabcd0400u}}, + {0u, 0x0000bc01u, 0x000003ffu, {0u, 0xabcdbc01u, 0xabcd8400u}}, + {0u, 0x00003bffu, 0x00008400u, {0u, 0xabcd3bffu, 0xabcd8400u}}, + {0u, 0x00003800u, 0x00000400u, {0u, 0xabcd3800u, 0xabcd0200u}}, + {0u, 0x0000b800u, 0x00000400u, {0u, 0xabcdb800u, 0xabcd8200u}}, + {0u, 0x00003800u, 0x00000401u, {0u, 0xabcd3800u, 0xabcd0200u}}, + {0u, 0x00003800u, 0x000003ffu, {0u, 0xabcd3800u, 0xabcd0200u}}, + {0u, 0x00000400u, 0x00000400u, {0u, 0xabcd0800u, 0xabcd0000u}}, + {0u, 0x00003c00u, 0x00000400u, {0u, 0xabcd3c00u, 0xabcd0400u}}, + {0u, 0x00007c00u, 0x00000001u, {0u, 0xabcd7c00u, 0xabcd7c00u}}, + {0u, 0x0000fc00u, 0x00008001u, {0u, 0xabcdfc00u, 0xabcd7c00u}}, + {0u, 0x00000001u, 0x00007c00u, {0u, 0xabcd7c00u, 0xabcd7c00u}}, + {0u, 0x00000000u, 0x00008000u, {0u, 0xabcd0000u, 0xabcd8000u}}, + {0u, 0x00008000u, 0x00008000u, {0u, 0xabcd8000u, 0xabcd0000u}}, + {0u, 0x00003c01u, 0x0000bc00u, {0u, 0xabcd1400u, 0xabcdbc01u}}, + {0u, 0x00007bffu, 0x00003c01u, {0u, 0xabcd7bffu, 0xabcd7c00u}}, + {0u, 0x00007e55u, 0x00000001u, {0u, 0xabcd7e55u, 0xabcd7e55u}}, + {0u, 0x00000001u, 0x0000fe01u, {0u, 0xabcdfe01u, 0xabcdfe01u}}, + {0u, 0x00007c01u, 0x000003ffu, {0u, 0xabcd7e01u, 0xabcd7e01u}}, + {0u, 0x000003ffu, 0x0000fd23u, {0u, 0xabcdff23u, 0xabcdff23u}}, + } +}; + constexpr std::array Names{"v_cvt_f16_f32", "v_add_f16", "v_mul_f16"}; std::array BufferDescriptor(const void* data, std::uint32_t count) { @@ -313,10 +649,10 @@ bool Matches(std::uint32_t actual, std::uint32_t expected) { return actual == expected || ((actual >> 16u) == (expected >> 16u) && IsNan16(actual) && IsNan16(expected)); } -void Run(AgcDriver::VulkanDevice& device, std::uint32_t first, std::uint32_t count) { +void Run(AgcDriver::VulkanDevice& device, std::uint32_t first, std::uint32_t count, std::span vectors = Vectors, std::uint32_t denorm16 = 4u, bool ieee = true, bool overflow = false) { Input.fill(0u); for (std::uint32_t lane = 0; lane < count; ++lane) { - const auto& vector = Vectors[first + lane]; + const auto& vector = vectors[first + lane]; Input[lane * Inputs] = vector.a; Input[lane * Inputs + 1] = vector.b; Input[lane * Inputs + 2] = vector.c; @@ -336,16 +672,52 @@ void Run(AgcDriver::VulkanDevice& device, std::uint32_t first, std::uint32_t cou device.Target(), {0, 0, 0, 128} }; + if (denorm16 < 4u) request.context.floatMode = ShaderRecompiler::ShaderFloatMode{denorm16 << 6u, true, ieee, overflow}; 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 first, std::uint32_t count) { +void RunDirected(AgcDriver::VulkanDevice& device, std::uint32_t rounding, std::uint32_t overflow, std::uint32_t round32) { + Input.fill(0u); + for (std::uint32_t lane = 0; lane < std::size(DirectedVectors); ++lane) { + std::copy(DirectedVectors[lane].inputs.begin(), DirectedVectors[lane].inputs.end(), Input.begin() + lane * Inputs); + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size())); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size())); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(DirectedCode); + 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.context.floatMode = ShaderRecompiler::ShaderFloatMode{0xf0u | (rounding << 2u) | round32, true, true, overflow != 0u}; + 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 lane = 0; lane < std::size(DirectedVectors); ++lane) { + for (std::uint32_t column = 0; column < 4u; ++column) { + const auto expected = DirectedVectors[lane].expected[rounding - 1u][overflow][column]; + const auto actual = Output[lane * Results + column]; + const bool matches = column == 0u ? Matches(actual, expected) : actual == expected; + Require(matches, "directed f16 rounding: mode " + std::to_string(rounding) + " overflow " + std::to_string(overflow) + " lane " + std::to_string(lane) + " column " + std::to_string(column) + " is " + Hex(actual) + ", expected " + Hex(expected)); + } + } +} + +void Check(std::uint32_t first, std::uint32_t count, std::span vectors = Vectors, std::uint32_t firstColumn = 0u) { for (std::uint32_t lane = 0; lane < count; ++lane) { - const auto& vector = Vectors[first + lane]; - for (std::uint32_t column = 0; column < Names.size(); ++column) { + const auto& vector = vectors[first + lane]; + for (std::uint32_t column = firstColumn; column < Names.size(); ++column) { const auto expected = vector.expected[column]; const auto actual = Output[lane * Results + column]; Require(Matches(actual, expected), std::string("f16 rounding: vector ") + std::to_string(first + lane) + " (" + Hex(vector.a) + ", " + Hex(vector.b) + ", " + Hex(vector.c) + ") " + Names[column] + " is " + Hex(actual) + ", expected " + Hex(expected)); @@ -353,6 +725,75 @@ void Check(std::uint32_t first, std::uint32_t count) { } } +alignas(256) constexpr std::array LdexpDenormalCode{ + 0x34020082u, 0x34060084u, 0xe0302000u, 0x80000401u, 0xe0302004u, 0x80000501u, 0x7e1402ffu, 0xabcdabcdu, + 0xbf8c3f70u, 0x76140b04u, 0xe0702000u, 0x80010a03u, 0xbf810000u, +}; + +struct LdexpDenormalVector { + std::uint32_t half; + std::uint32_t exponent; + std::array expected; +}; + +constexpr LdexpDenormalVector LdexpDenormalVectors[] = { + {0x00000401u, 0xffffffffu, {0xabcd0000u, 0xabcd0000u, 0xabcd0200u, 0xabcd0200u}}, + {0x000003ffu, 0x00000000u, {0xabcd0000u, 0xabcd0000u, 0xabcd0000u, 0xabcd03ffu}}, + {0x000003ffu, 0x00000001u, {0xabcd0000u, 0xabcd07feu, 0xabcd0000u, 0xabcd07feu}}, + {0x00000400u, 0xffffffffu, {0xabcd0000u, 0xabcd0000u, 0xabcd0200u, 0xabcd0200u}}, + {0x00008400u, 0xffffffffu, {0xabcd8000u, 0xabcd8000u, 0xabcd8200u, 0xabcd8200u}}, + {0x000007ffu, 0xffffffffu, {0xabcd0000u, 0xabcd0000u, 0xabcd0400u, 0xabcd0400u}}, + {0x000087ffu, 0xffffffffu, {0xabcd8000u, 0xabcd8000u, 0xabcd8400u, 0xabcd8400u}}, + {0x00000001u, 0x00000000u, {0xabcd0000u, 0xabcd0000u, 0xabcd0000u, 0xabcd0001u}}, + {0x00000001u, 0x00000001u, {0xabcd0000u, 0xabcd0000u, 0xabcd0000u, 0xabcd0002u}}, + {0x00008001u, 0x00000000u, {0xabcd8000u, 0xabcd8000u, 0xabcd8000u, 0xabcd8001u}}, + {0x00000200u, 0x00000000u, {0xabcd0000u, 0xabcd0000u, 0xabcd0000u, 0xabcd0200u}}, + {0x00000200u, 0x00000001u, {0xabcd0000u, 0xabcd0400u, 0xabcd0000u, 0xabcd0400u}}, + {0x00003c01u, 0x00000000u, {0xabcd3c01u, 0xabcd3c01u, 0xabcd3c01u, 0xabcd3c01u}}, + {0x00003c01u, 0xffffffffu, {0xabcd3801u, 0xabcd3801u, 0xabcd3801u, 0xabcd3801u}}, + {0x00007c00u, 0x00000001u, {0xabcd7c00u, 0xabcd7c00u, 0xabcd7c00u, 0xabcd7c00u}}, + {0x00007e55u, 0x00000000u, {0xabcd7e55u, 0xabcd7e55u, 0xabcd7e55u, 0xabcd7e55u}}, + {0x00000000u, 0x00000040u, {0xabcd0000u, 0xabcd0000u, 0xabcd0000u, 0xabcd0000u}}, + {0x00008000u, 0xffffffffu, {0xabcd8000u, 0xabcd8000u, 0xabcd8000u, 0xabcd8000u}}, +}; + +void RunLdexpDenormal(AgcDriver::VulkanDevice& device, std::uint32_t denorm16) { + Input.fill(0u); + for (std::uint32_t lane = 0; lane < std::size(LdexpDenormalVectors); ++lane) { + Input[lane * Inputs] = LdexpDenormalVectors[lane].half; + Input[lane * Inputs + 1] = LdexpDenormalVectors[lane].exponent; + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size())); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size())); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(LdexpDenormalCode); + 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.context.floatMode = ShaderRecompiler::ShaderFloatMode{denorm16 << 6u, true, true, false}; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void CheckLdexpDenormal(std::uint32_t denorm16) { + for (std::uint32_t lane = 0; lane < std::size(LdexpDenormalVectors); ++lane) { + const auto& vector = LdexpDenormalVectors[lane]; + const auto expected = vector.expected[denorm16]; + const auto actual = Output[lane * Results]; + Require(Matches(actual, expected), std::string("f16 ldexp denormal: denorm16 ") + std::to_string(denorm16) + " vector " + std::to_string(lane) + " (" + Hex(vector.half) + ", " + Hex(vector.exponent) + ") is " + Hex(actual) + ", expected " + Hex(expected)); + } +} + } int main() { @@ -365,6 +806,26 @@ int main() { Run(*device, first, count); Check(first, count); } + for (std::uint32_t rounding = 1u; rounding < 4u; ++rounding) { + for (std::uint32_t overflow = 0u; overflow < 2u; ++overflow) { + RunDirected(*device, rounding, overflow, 0u); + RunDirected(*device, rounding, overflow, 3u); + } + } + for (bool ieee : {false, true}) { + for (bool overflow : {false, true}) { + for (std::uint32_t denorm16 = 0u; denorm16 < 4u; ++denorm16) { + auto vectors = std::to_array(DenormalVectors[denorm16]); + if (overflow) vectors[27].expected[2] = 0xabcd7bffu; + Run(*device, 0u, 32u, vectors, denorm16, ieee, overflow); + Check(0u, 32u, vectors, 1u); + } + } + } + for (std::uint32_t denorm16 = 0u; denorm16 < 4u; ++denorm16) { + RunLdexpDenormal(*device, denorm16); + CheckLdexpDenormal(denorm16); + } std::puts("f16 rounding tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FmaLegacy.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FmaLegacy.cpp index 53af35045c..f4ae0b8dc1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/FmaLegacy.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FmaLegacy.cpp @@ -4,8 +4,6 @@ #include "VulkanTestDevice.hpp" #include #include -#include -#include #include #include #include @@ -32,29 +30,42 @@ alignas(256) constexpr std::array FmaLegacyCode{ struct Row { std::uint32_t a, b, c; + std::array expected; }; -constexpr std::array Rows{{ - {0x00000000u, 0x7f800000u, 0x3f800000u}, - {0x80000000u, 0x7f800000u, 0xc0200000u}, - {0x7f800000u, 0x00000000u, 0x3f400000u}, - {0xff800000u, 0x80000000u, 0x40400000u}, - {0x00000000u, 0x7fc00000u, 0x3fc00000u}, - {0x7fc00000u, 0x80000000u, 0xc0800000u}, - {0x00000000u, 0x40000000u, 0x7f800000u}, - {0x80000000u, 0xc0400000u, 0xff800000u}, - {0x00000000u, 0x3f800000u, 0x7fc00000u}, - {0x3fc00000u, 0x40100000u, 0xbf400000u}, - {0xc0c00000u, 0x3f000000u, 0x41200000u}, - {0x7f800000u, 0x40000000u, 0x3f800000u}, - {0x7f800000u, 0xbf800000u, 0x7f800000u}, - {0x7fc00000u, 0x3f800000u, 0x3f800000u}, - {0x40400000u, 0x40800000u, 0xc1400000u}, - {0x7f7fffffu, 0x40000000u, 0x3f800000u}, - {0x3ec00000u, 0xc1480000u, 0x42c88000u}, - {0xc4800000u, 0xbd800000u, 0xc2800000u}, - {0x40e00000u, 0x41100000u, 0x3f000000u}, - {0x80000000u, 0x80000000u, 0x40a00000u}, +constexpr std::array Rows{{ + {0x00000000u, 0x7f800000u, 0x3f800000u, {0x3f800000u, 0x3f800000u, 0xbf800000u, 0x3f800000u, 0x3f800000u}}, + {0x80000000u, 0x7f800000u, 0xc0200000u, {0xc0200000u, 0xc0200000u, 0x40200000u, 0xc0200000u, 0xc0200000u}}, + {0x7f800000u, 0x00000000u, 0x3f400000u, {0x3f400000u, 0x3f400000u, 0xbf400000u, 0x3f400000u, 0x3f400000u}}, + {0xff800000u, 0x80000000u, 0x40400000u, {0x40400000u, 0x40400000u, 0xc0400000u, 0x40400000u, 0x40400000u}}, + {0x00000000u, 0x7fc00000u, 0x3fc00000u, {0x3fc00000u, 0x3fc00000u, 0xbfc00000u, 0x3fc00000u, 0x3fc00000u}}, + {0x7fc00000u, 0x80000000u, 0xc0800000u, {0xc0800000u, 0xc0800000u, 0x40800000u, 0xc0800000u, 0xc0800000u}}, + {0x00000000u, 0x40000000u, 0x7f800000u, {0x7f800000u, 0x7f800000u, 0xff800000u, 0x7f800000u, 0x7f800000u}}, + {0x80000000u, 0xc0400000u, 0xff800000u, {0xff800000u, 0xff800000u, 0x7f800000u, 0xff800000u, 0xff800000u}}, + {0x00000000u, 0x3f800000u, 0x7fc00000u, {0x7fc00000u, 0x7fc00000u, 0xffc00000u, 0x7fc00000u, 0x7fc00000u}}, + {0x3fc00000u, 0x40100000u, 0xbf400000u, {0x40280000u, 0xc0840000u, 0x40840000u, 0xbf400000u, 0x40280000u}}, + {0xc0c00000u, 0x3f000000u, 0x41200000u, {0x40e00000u, 0x41500000u, 0xc0e00000u, 0x41200000u, 0x40e00000u}}, + {0x7f800000u, 0x40000000u, 0x3f800000u, {0x7f800000u, 0xff800000u, 0x7f800000u, 0x3f800000u, 0x7f800000u}}, + {0x7f800000u, 0xbf800000u, 0x7f800000u, {0xffc00000u, 0x7f800000u, 0xff800000u, 0x7f800000u, 0x7f800000u}}, + {0x7fc00000u, 0x3f800000u, 0x3f800000u, {0x7fc00000u, 0xffc00000u, 0x7fc00000u, 0x3f800000u, 0xffc00000u}}, + {0x40400000u, 0x40800000u, 0xc1400000u, {0x00000000u, 0xc1c00000u, 0x41c00000u, 0xc1400000u, 0x00000000u}}, + {0x7f7fffffu, 0x40000000u, 0x3f800000u, {0x7f800000u, 0xff800000u, 0x7f800000u, 0x3f800000u, 0x7f800000u}}, + {0x3ec00000u, 0xc1480000u, 0x42c88000u, {0x42bf2000u, 0x42d1e000u, 0xc2d1e000u, 0x42c88000u, 0x42d1e000u}}, + {0xc4800000u, 0xbd800000u, 0xc2800000u, {0x00000000u, 0xc3000000u, 0x00000000u, 0xc2800000u, 0xc3000000u}}, + {0x40e00000u, 0x41100000u, 0x3f000000u, {0x427e0000u, 0xc27a0000u, 0x427a0000u, 0x3f000000u, 0x427e0000u}}, + {0x80000000u, 0x80000000u, 0x40a00000u, {0x40a00000u, 0x40a00000u, 0xc0a00000u, 0x40a00000u, 0x40a00000u}}, + {0x3f800800u, 0x3f800800u, 0xbf800000u, {0x3a000000u, 0xc0000800u, 0x40000800u, 0xbf800000u, 0x3a000000u}}, + {0x3f800001u, 0x3f7fffffu, 0xbf800000u, {0x00000000u, 0xc0000000u, 0x40000000u, 0xbf800000u, 0x00000000u}}, + {0x3faaaaabu, 0x40400000u, 0xc0800000u, {0x00000000u, 0xc1000000u, 0x41000000u, 0xc0800000u, 0x00000000u}}, + {0x7f7fffffu, 0x40000000u, 0xff7fffffu, {0x7f800000u, 0xff800000u, 0x7f800000u, 0xff7fffffu, 0x7f800000u}}, + {0x00800000u, 0x3f000000u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x00000001u, 0x4b000000u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x00000001u, 0x3f800000u, 0x80000000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x80000001u, 0x3f800000u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x00800000u, 0x3f800000u, 0x80400000u, {0x00800000u, 0x80800000u, 0x00800000u, 0x00000000u, 0x00800000u}}, + {0x3f800000u, 0x00400000u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x7f812345u, 0x3f800000u, 0x3f800000u, {0x7f812345u, 0xff812345u, 0x7f812345u, 0x3f800000u, 0xff812345u}}, + {0x3f800000u, 0x3f800000u, 0x7f812345u, {0x7f812345u, 0x7f812345u, 0xff812345u, 0x7f812345u, 0x7f812345u}}, }}; std::array BufferDescriptor(const void* data, std::uint32_t count) { @@ -96,33 +107,15 @@ std::string Hex(std::uint32_t value) { return text; } -bool IsNan(std::uint32_t bits) { - return (bits & 0x7fffffffu) > 0x7f800000u; -} - -std::uint32_t FmaLegacy(float a, float b, float c) { - if (a == 0.0f || b == 0.0f) { - return std::bit_cast(0.0f + c); - } - return std::bit_cast(std::fma(a, b, c)); -} - void Check() { constexpr std::array names{ - "v_fma_legacy_f32", "v_fma_legacy_f32 -src0", "v_fma_legacy_f32 |src0| -src2", "v_fma_legacy_f32 src0=0", "v_fma_legacy_f32 -src0 -|src1|", + "v_mad_legacy_f32", "v_mad_legacy_f32 -src0", "v_mad_legacy_f32 |src0| -src2", "v_mad_legacy_f32 src0=0", "v_mad_legacy_f32 -src0 -|src1|", }; for (std::uint32_t tid = 0; tid < Rows.size(); ++tid) { const auto& row = Rows[tid]; - const float a = std::bit_cast(row.a); - const float b = std::bit_cast(row.b); - const float c = std::bit_cast(row.c); - const std::array expected{ - FmaLegacy(a, b, c), FmaLegacy(-a, b, c), FmaLegacy(std::fabs(a), b, -c), FmaLegacy(0.0f, b, c), FmaLegacy(-a, -std::fabs(b), c), - }; - for (std::uint32_t j = 0; j < expected.size(); ++j) { + for (std::uint32_t j = 0; j < row.expected.size(); ++j) { const auto actual = Output[tid * Results + j]; - const bool matches = actual == expected[j] || (IsNan(actual) && IsNan(expected[j])); - Require(matches, std::string("fma legacy: lane ") + std::to_string(tid) + " (" + Hex(row.a) + ", " + Hex(row.b) + ", " + Hex(row.c) + ") " + names[j] + " is " + Hex(actual) + ", expected " + Hex(expected[j])); + Require(actual == row.expected[j], std::string("mad legacy: lane ") + std::to_string(tid) + " (" + Hex(row.a) + ", " + Hex(row.b) + ", " + Hex(row.c) + ") " + names[j] + " is " + Hex(actual) + ", expected " + Hex(row.expected[j])); } } } @@ -135,7 +128,7 @@ int main() { if (!device) return VulkanTestSkipped; Run(*device); Check(); - std::puts("fma legacy tests passed"); + std::puts("mad legacy tests passed"); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; 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/FormattedStoreUnaligned.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FormattedStoreUnaligned.cpp new file mode 100644 index 0000000000..a39cd01e81 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FormattedStoreUnaligned.cpp @@ -0,0 +1,135 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t LaneBytes = 64; +constexpr std::uint32_t Fill = 0xaaaaaaaau; + +struct Store { + std::uint32_t format; + std::uint32_t width; + std::uint32_t channels; + std::uint32_t components; + std::uint32_t offset; +}; + +constexpr std::array Stores{{ + {5, 1, 1, 1, 1}, + {11, 2, 1, 1, 3}, + {60, 1, 4, 4, 6}, + {60, 1, 4, 3, 14}, + {11, 2, 1, 1, 19}, + {27, 2, 2, 2, 26}, + {69, 2, 4, 4, 33}, + {18, 1, 2, 2, 45}, +}}; + +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +std::vector Program() { + std::vector code{0x34020084, 0x34060086}; + for (std::uint32_t component = 0; component < Inputs; ++component) code.insert(code.end(), {0xe0301000u | (component * 4u), 0x80000401u | (component << 8u)}); + code.push_back(0xbf8c3f70); + code.insert(code.end(), {0x7e3c02ff, Fill}); + for (std::uint32_t offset = 0; offset < LaneBytes; offset += 4) code.insert(code.end(), {0xe0701000u | offset, 0x80011e03}); + code.push_back(0xbf8c3f70); + for (const auto& store : Stores) { + const std::uint32_t op = 3u + store.components; + code.insert(code.end(), {0xe8001000u | (store.format << 19u) | (op << 16u) | store.offset, 0x80010403}); + } + code.push_back(0xbf810000); + return code; +} + +std::uint32_t Value(std::uint32_t tid, std::uint32_t component) { + const auto mixed = (tid + 1u) * 2654435761u + component * 40503u; + return tid % 5u == 3u ? mixed | 0x10000u : mixed >> 3u & 0xffffu; +} + +std::array Expected(std::uint32_t tid) { + std::array bytes{}; + bytes.fill(static_cast(Fill & 0xffu)); + for (const auto& store : Stores) { + const std::uint32_t width = store.width; + const std::uint32_t limit = width == 1u ? 0xffu : 0xffffu; + for (std::uint32_t component = 0; component < store.channels; ++component) { + const auto value = component < store.components ? std::min(Value(tid, component), limit) : 0u; + for (std::uint32_t byte = 0; byte < width; ++byte) bytes[store.offset + component * width + byte] = static_cast(value >> (8u * byte)); + } + } + return bytes; +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +void Run(AgcDriver::VulkanDevice& device) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t component = 0; component < Inputs; ++component) Input[tid * Inputs + component] = Value(tid, component); + } + Output.fill(0xdeu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size())); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + static const auto code = Program(); + const std::span words(code); + const std::array memory{{{reinterpret_cast(words.data()), std::as_bytes(words)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(words.data()), words, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(words.data())); + device.WaitIdle(); +} + +void Check() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto expected = Expected(tid); + for (std::uint32_t byte = 0; byte < LaneBytes; ++byte) { + const auto actual = Output[tid * LaneBytes + byte]; + Require(actual == expected[byte], "formatted store unaligned: lane " + std::to_string(tid) + " byte " + std::to_string(byte) + " is " + std::to_string(actual) + ", expected " + std::to_string(expected[byte])); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device); + Check(); + std::puts("formatted store unaligned tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Fp16Overflow.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Fp16Overflow.cpp new file mode 100644 index 0000000000..f83aa30c3c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Fp16Overflow.cpp @@ -0,0 +1,245 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 16; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084u, 0x34060086u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e140280u, 0x7e160280u, 0x7e180280u, + 0x7e1a0280u, 0x7e1c0280u, 0x7e1e0280u, 0x7e200280u, 0x7e220280u, 0x7e240280u, 0x7e260280u, 0x7e280280u, + 0x7e2a0280u, 0x7e2c0280u, 0x7e2e0280u, 0x7e300280u, 0x7e320280u, 0x64140b04u, 0x66160b04u, 0x6a180b04u, + 0xd74b000du, 0x041a0b04u, 0xd75f000eu, 0x041a0b04u, 0xcc10400fu, 0x00020b04u, 0xcc0f4010u, 0x00020b04u, + 0xcc214011u, 0x1c1a0b04u, 0x64240af9u, 0x04041504u, 0x7e26b104u, 0x7e28af04u, 0x7e2aa904u, 0x7e2cad04u, + 0x762e0f04u, 0x7e301507u, 0x7e32a107u, 0xe0781000u, 0x80010a03u, 0xe0781010u, 0x80010e03u, 0xe0781020u, + 0x80011203u, 0xe0781030u, 0x80011603u, 0xbf810000u, +}; + +constexpr std::uint32_t Rows[32][4] = { + {0x00007bffu, 0x00004c00u, 0x00000000u, 0x477ff000u}, + {0x00007bffu, 0x00004bffu, 0x00000000u, 0x477fefffu}, + {0x00007bffu, 0x00004c01u, 0x00000000u, 0x0000ffffu}, + {0x00007bffu, 0x00007bffu, 0x00000000u, 0x0000fff0u}, + {0x0000fbffu, 0x0000fbffu, 0x00000000u, 0x0000ffefu}, + {0x00007800u, 0x00004000u, 0x00000000u, 0x00000001u}, + {0x00007800u, 0x0000c000u, 0x00000000u, 0x00000010u}, + {0x00007c00u, 0x00003c00u, 0x00000000u, 0x7f800000u}, + {0x00007c00u, 0x00004000u, 0x00003c00u, 0xff800000u}, + {0x00007c00u, 0x00000000u, 0x00000000u, 0x7f7fffffu}, + {0x00007c00u, 0x0000fc00u, 0x00000000u, 0x47800000u}, + {0x00007e00u, 0x00003c00u, 0x00003c00u, 0x47800001u}, + {0x00007d00u, 0x00003c00u, 0x00003c00u, 0xc7800000u}, + {0x00007bffu, 0x00003c00u, 0x00004c00u, 0x00000040u}, + {0x00007bffu, 0x00004000u, 0x0000fbffu, 0x0000ffc0u}, + {0x00007bffu, 0x00004000u, 0x0000fc00u, 0x477fe000u}, + {0x00003c00u, 0x00000000u, 0x00003c00u, 0x00000010u}, + {0x00003c00u, 0x00003c00u, 0x00007c00u, 0x0000000fu}, + {0x00007bffu, 0x00000001u, 0x00003c00u, 0x477ff001u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x4b800000u}, + {0x00008000u, 0x00003c00u, 0x00000000u, 0x5f800000u}, + {0x00000001u, 0x00000001u, 0x00000000u, 0x00000028u}, + {0x00000100u, 0x00003c00u, 0x00000000u, 0x00000020u}, + {0x00000200u, 0x00003c00u, 0x00000000u, 0x0000001fu}, + {0x00004c00u, 0x00003c00u, 0x00003c00u, 0x0000000cu}, + {0x00004bffu, 0x00003c00u, 0x00003c00u, 0x0000000cu}, + {0x00005640u, 0x00005640u, 0x00005640u, 0x3f800000u}, + {0x0000fc00u, 0x00003c00u, 0x00007c00u, 0x00008000u}, + {0x00007800u, 0x00004400u, 0x00000000u, 0x7fa00000u}, + {0x0000fbffu, 0x0000cc00u, 0x00000000u, 0x00007fffu}, + {0x00007bffu, 0x0000cc00u, 0x00000000u, 0x00000002u}, + {0x00007c00u, 0x00007c00u, 0x00007c00u, 0xfc000000u} +}; +constexpr std::uint32_t ExpectedOverflow[32][16] = { + {0x00007bffu, 0x00007bfeu, 0x00007bffu, 0x00007bffu, 0x00000000u, 0x7bff7bffu, 0x7bff7bffu, 0x00007bffu, 0x7bff0000u, 0x00007bffu, 0x00004c00u, 0x00000100u, 0x00001c00u, 0x00000000u, 0x00007bffu, 0x00007b80u}, + {0x00007bffu, 0x00007bffu, 0x00007bffu, 0x00007bffu, 0x00000000u, 0x7bff7bffu, 0x7bff7bffu, 0x00007bffu, 0x7bff0000u, 0x00007bffu, 0x00004c00u, 0x00000100u, 0x00001c00u, 0x00000000u, 0x00007bffu, 0x00007b80u}, + {0x00007bffu, 0x00007bfeu, 0x00007bffu, 0x00007bffu, 0x00000000u, 0x7bff7bffu, 0x7bff7bffu, 0x00007bffu, 0x7bff0000u, 0x00007bffu, 0x00004c00u, 0x00000100u, 0x00001c00u, 0x000077ffu, 0x00000000u, 0x00007bffu}, + {0x00007bffu, 0x00000000u, 0x00007bffu, 0x00007bffu, 0x00000000u, 0x7bff7bffu, 0x7bff7bffu, 0x00007bffu, 0x7bff0000u, 0x00007bffu, 0x00004c00u, 0x00000100u, 0x00001c00u, 0x00003bffu, 0x00000000u, 0x00007bffu}, + {0x0000fbffu, 0x00000000u, 0x00007bffu, 0x00007bffu, 0x00008000u, 0x7bff7bffu, 0xfbfffbffu, 0x00007bffu, 0xfbff0000u, 0x00000000u, 0x0000fe00u, 0x00008100u, 0x0000fe00u, 0x0000b7ffu, 0x00000000u, 0x00007bffu}, + {0x00007800u, 0x00007800u, 0x00007bffu, 0x00007bffu, 0x00000000u, 0x7bff7bffu, 0x78007800u, 0x00007bffu, 0x78000000u, 0x00007bffu, 0x00004b80u, 0x00000200u, 0x00001da8u, 0x00007bffu, 0x00000000u, 0x00003c00u}, + {0x00007800u, 0x00007800u, 0x0000fbffu, 0x0000fbffu, 0x00008000u, 0xfbfffbffu, 0x78007800u, 0x0000fbffu, 0x78000000u, 0x00007bffu, 0x00004b80u, 0x00000200u, 0x00001da8u, 0x00007bffu, 0x00000000u, 0x00004c00u}, + {0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00000000u, 0x7c007c00u, 0x7c007c00u, 0x00007c00u, 0x7c000000u, 0x00007c00u, 0x00007c00u, 0x00000000u, 0x00000000u, 0x00007c00u, 0x00007c00u, 0x00000000u}, + {0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007bffu, 0x7c007c00u, 0x7c007c00u, 0x00007c00u, 0x7c000000u, 0x00007c00u, 0x00007c00u, 0x00000000u, 0x00000000u, 0x00007c00u, 0x0000fc00u, 0x00000000u}, + {0x00007c00u, 0x00007c00u, 0x0000fe00u, 0x0000fe00u, 0x0000fe00u, 0xfe00fe00u, 0x7c007c00u, 0x0000fe00u, 0x7c000000u, 0x00007c00u, 0x00007c00u, 0x00000000u, 0x00000000u, 0x00007c00u, 0x00007bffu, 0x00007bffu}, + {0x0000fe00u, 0x00007c00u, 0x0000fc00u, 0x0000fc00u, 0x00008000u, 0xfc00fc00u, 0xfe00fe00u, 0x0000fc00u, 0xfe000000u, 0x00007c00u, 0x00007c00u, 0x00000000u, 0x00000000u, 0x00007c00u, 0x00007bffu, 0x00000000u}, + {0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007bffu, 0x7e007e00u, 0x7e007e00u, 0x00007e00u, 0x7e000000u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007bffu, 0x00003c00u}, + {0x00007d00u, 0x00007d00u, 0x00007d00u, 0x00007d00u, 0x00007bffu, 0x7d007d00u, 0x7d007d00u, 0x00007f00u, 0x7d000000u, 0x00007d00u, 0x00007d00u, 0x00007d00u, 0x00007d00u, 0x00007d00u, 0x0000fbffu, 0x00000000u}, + {0x00007bffu, 0x00007bffu, 0x00007bffu, 0x00007bffu, 0x00007bffu, 0x7bff7bffu, 0x7bff7bffu, 0x00007bffu, 0x7bff0000u, 0x00007bffu, 0x00004c00u, 0x00000100u, 0x00001c00u, 0x00007bffu, 0x00000000u, 0x00005400u}, + {0x00007bffu, 0x00007bffu, 0x00007bffu, 0x00007bffu, 0x0000fbffu, 0x7bff7bffu, 0x7bff7bffu, 0x00007bffu, 0x7bff0000u, 0x00007bffu, 0x00004c00u, 0x00000100u, 0x00001c00u, 0x00000000u, 0x00000000u, 0x00007bfeu}, + {0x00007bffu, 0x00007bffu, 0x00007bffu, 0x0000fc00u, 0x0000fc00u, 0x7bff7bffu, 0x7bff7bffu, 0x0000fc00u, 0x7bff0000u, 0x00007bffu, 0x00004c00u, 0x00000100u, 0x00001c00u, 0x00000000u, 0x00007bffu, 0x00007b00u}, + {0x00003c00u, 0x00003c00u, 0x00000000u, 0x00003c00u, 0x00007c00u, 0x00000000u, 0x3c003c00u, 0x00003c00u, 0x3c000000u, 0x00004000u, 0x00000000u, 0x00003c00u, 0x00003c00u, 0x00007bffu, 0x00000000u, 0x00004c00u}, + {0x00004000u, 0x00000000u, 0x00003c00u, 0x00007c00u, 0x00007c00u, 0x3c003c00u, 0x40004000u, 0x00007c00u, 0x40000000u, 0x00004000u, 0x00000000u, 0x00003c00u, 0x00003c00u, 0x00007800u, 0x00000000u, 0x00004b80u}, + {0x00007bffu, 0x00007bffu, 0x00001bffu, 0x00003c04u, 0x00007bffu, 0x1bff1bffu, 0x7bff7bffu, 0x00003c04u, 0x7bff0000u, 0x00007bffu, 0x00004c00u, 0x00000100u, 0x00001c00u, 0x00000000u, 0x00007bffu, 0x00007b80u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000fe00u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00003c00u, 0x0000fbffu, 0x00007bffu, 0x00007bffu, 0x00000000u, 0x00007bffu, 0x00000000u}, + {0x00003c00u, 0x0000bc00u, 0x00008000u, 0x00000000u, 0x00000000u, 0x80008000u, 0x3c003c00u, 0x00000000u, 0x3c000000u, 0x00003c00u, 0x0000fbffu, 0x0000fbffu, 0x0000fbffu, 0x00008000u, 0x00007bffu, 0x00000000u}, + {0x00000002u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00020002u, 0x00000000u, 0x00020000u, 0x00003c00u, 0x0000ce00u, 0x00007bffu, 0x00006c00u, 0x00007bffu, 0x00000000u, 0x00005100u}, + {0x00003c00u, 0x0000bc00u, 0x00000100u, 0x00000100u, 0x00000000u, 0x01000100u, 0x3c003c00u, 0x00000100u, 0x3c000000u, 0x00003c00u, 0x0000cc00u, 0x00007bffu, 0x00005c00u, 0x00007bffu, 0x00000000u, 0x00005000u}, + {0x00003c00u, 0x0000bc00u, 0x00000200u, 0x00000200u, 0x00000000u, 0x02000200u, 0x3c003c00u, 0x00000200u, 0x3c000000u, 0x00003c00u, 0x0000cb80u, 0x00007800u, 0x000059a8u, 0x00007bffu, 0x00000000u, 0x00004fc0u}, + {0x00004c40u, 0x00004b80u, 0x00004c00u, 0x00004c40u, 0x00004c00u, 0x4c004c00u, 0x4c404c40u, 0x00004c40u, 0x4c400000u, 0x00007bffu, 0x00004400u, 0x00002c00u, 0x00003400u, 0x00007bffu, 0x00000000u, 0x00004a00u}, + {0x00004c40u, 0x00004b7fu, 0x00004bffu, 0x00004c40u, 0x00004bffu, 0x4bff4bffu, 0x4c404c40u, 0x00004c40u, 0x4c400000u, 0x00007bf5u, 0x00004400u, 0x00002c01u, 0x00003400u, 0x00007bffu, 0x00000000u, 0x00004a00u}, + {0x00005a40u, 0x00000000u, 0x000070e2u, 0x000070eeu, 0x00005640u, 0x70e270e2u, 0x5a405a40u, 0x000070eeu, 0x5a400000u, 0x00007bffu, 0x000046a5u, 0x0000211fu, 0x00002e66u, 0x00005640u, 0x00003c00u, 0x00000000u}, + {0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x0000fe00u, 0x00007c00u, 0xfc00fc00u, 0xfc00fc00u, 0x0000fe00u, 0xfc000000u, 0x00000000u, 0x0000fe00u, 0x00008000u, 0x0000fe00u, 0x0000fc00u, 0x00000000u, 0x00007800u}, + {0x00007800u, 0x00007800u, 0x00007bffu, 0x00007bffu, 0x00000000u, 0x7bff7bffu, 0x78007800u, 0x00007bffu, 0x78000000u, 0x00007bffu, 0x00004b80u, 0x00000200u, 0x00001da8u, 0x00007800u, 0x00007f00u, 0x00000000u}, + {0x0000fbffu, 0x0000fbfeu, 0x00007bffu, 0x00007bffu, 0x00008000u, 0x7bff7bffu, 0xfbfffbffu, 0x00007bffu, 0xfbff0000u, 0x00000000u, 0x0000fe00u, 0x00008100u, 0x0000fe00u, 0x0000fbffu, 0x00000000u, 0x00007800u}, + {0x00007bfeu, 0x00007bffu, 0x0000fbffu, 0x0000fbffu, 0x00008000u, 0xfbfffbffu, 0x7bfe7bfeu, 0x0000fbffu, 0x7bfe0000u, 0x00007bffu, 0x00004c00u, 0x00000100u, 0x00001c00u, 0x00007bffu, 0x00000000u, 0x00004000u}, + {0x00007c00u, 0x0000fe00u, 0x00007c00u, 0x00007c00u, 0x0000fe00u, 0x7c007c00u, 0x7c007c00u, 0x00007c00u, 0x7c000000u, 0x00007c00u, 0x00007c00u, 0x00000000u, 0x00000000u, 0x00007c00u, 0x0000fbffu, 0x00000000u} +}; +constexpr std::uint32_t ExpectedInfinity[32][16] = { + {0x00007c00u, 0x00007bfeu, 0x00007c00u, 0x00007c00u, 0x00000000u, 0x7c007c00u, 0x7c007c00u, 0x00007c00u, 0x7c000000u, 0x00007c00u, 0x00004c00u, 0x00000100u, 0x00001c00u, 0x00000000u, 0x00007c00u, 0x00007b80u}, + {0x00007bffu, 0x00007bffu, 0x00007c00u, 0x00007c00u, 0x00000000u, 0x7c007c00u, 0x7bff7bffu, 0x00007c00u, 0x7bff0000u, 0x00007c00u, 0x00004c00u, 0x00000100u, 0x00001c00u, 0x00000000u, 0x00007bffu, 0x00007b80u}, + {0x00007c00u, 0x00007bfeu, 0x00007c00u, 0x00007c00u, 0x00000000u, 0x7c007c00u, 0x7c007c00u, 0x00007c00u, 0x7c000000u, 0x00007c00u, 0x00004c00u, 0x00000100u, 0x00001c00u, 0x000077ffu, 0x00000000u, 0x00007c00u}, + {0x00007c00u, 0x00000000u, 0x00007c00u, 0x00007c00u, 0x00000000u, 0x7c007c00u, 0x7c007c00u, 0x00007c00u, 0x7c000000u, 0x00007c00u, 0x00004c00u, 0x00000100u, 0x00001c00u, 0x00003bffu, 0x00000000u, 0x00007c00u}, + {0x0000fc00u, 0x00000000u, 0x00007c00u, 0x00007c00u, 0x00008000u, 0x7c007c00u, 0xfc00fc00u, 0x00007c00u, 0xfc000000u, 0x00000000u, 0x0000fe00u, 0x00008100u, 0x0000fe00u, 0x0000b7ffu, 0x00000000u, 0x00007bffu}, + {0x00007800u, 0x00007800u, 0x00007c00u, 0x00007c00u, 0x00000000u, 0x7c007c00u, 0x78007800u, 0x00007c00u, 0x78000000u, 0x00007c00u, 0x00004b80u, 0x00000200u, 0x00001da8u, 0x00007c00u, 0x00000000u, 0x00003c00u}, + {0x00007800u, 0x00007800u, 0x0000fc00u, 0x0000fc00u, 0x00008000u, 0xfc00fc00u, 0x78007800u, 0x0000fc00u, 0x78000000u, 0x00007c00u, 0x00004b80u, 0x00000200u, 0x00001da8u, 0x00007c00u, 0x00000000u, 0x00004c00u}, + {0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00000000u, 0x7c007c00u, 0x7c007c00u, 0x00007c00u, 0x7c000000u, 0x00007c00u, 0x00007c00u, 0x00000000u, 0x00000000u, 0x00007c00u, 0x00007c00u, 0x00000000u}, + {0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x7c007c00u, 0x7c007c00u, 0x00007c00u, 0x7c000000u, 0x00007c00u, 0x00007c00u, 0x00000000u, 0x00000000u, 0x00007c00u, 0x0000fc00u, 0x00000000u}, + {0x00007c00u, 0x00007c00u, 0x0000fe00u, 0x0000fe00u, 0x0000fe00u, 0xfe00fe00u, 0x7c007c00u, 0x0000fe00u, 0x7c000000u, 0x00007c00u, 0x00007c00u, 0x00000000u, 0x00000000u, 0x00007c00u, 0x00007c00u, 0x00007c00u}, + {0x0000fe00u, 0x00007c00u, 0x0000fc00u, 0x0000fc00u, 0x00008000u, 0xfc00fc00u, 0xfe00fe00u, 0x0000fc00u, 0xfe000000u, 0x00007c00u, 0x00007c00u, 0x00000000u, 0x00000000u, 0x00007c00u, 0x00007c00u, 0x00000000u}, + {0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007c00u, 0x7e007e00u, 0x7e007e00u, 0x00007e00u, 0x7e000000u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007c00u, 0x00003c00u}, + {0x00007d00u, 0x00007d00u, 0x00007d00u, 0x00007d00u, 0x00007c00u, 0x7d007d00u, 0x7d007d00u, 0x00007f00u, 0x7d000000u, 0x00007d00u, 0x00007d00u, 0x00007d00u, 0x00007d00u, 0x00007d00u, 0x0000fc00u, 0x00000000u}, + {0x00007bffu, 0x00007bffu, 0x00007bffu, 0x00007c00u, 0x00007bffu, 0x7bff7bffu, 0x7bff7bffu, 0x00007c00u, 0x7bff0000u, 0x00007c00u, 0x00004c00u, 0x00000100u, 0x00001c00u, 0x00007c00u, 0x00000000u, 0x00005400u}, + {0x00007bffu, 0x00007bffu, 0x00007c00u, 0x00007bffu, 0x0000fbffu, 0x7c007c00u, 0x7bff7bffu, 0x00007bffu, 0x7bff0000u, 0x00007c00u, 0x00004c00u, 0x00000100u, 0x00001c00u, 0x00000000u, 0x00000000u, 0x00007bfeu}, + {0x00007bffu, 0x00007bffu, 0x00007c00u, 0x0000fc00u, 0x0000fc00u, 0x7c007c00u, 0x7bff7bffu, 0x0000fc00u, 0x7bff0000u, 0x00007c00u, 0x00004c00u, 0x00000100u, 0x00001c00u, 0x00000000u, 0x00007bffu, 0x00007b00u}, + {0x00003c00u, 0x00003c00u, 0x00000000u, 0x00003c00u, 0x00007c00u, 0x00000000u, 0x3c003c00u, 0x00003c00u, 0x3c000000u, 0x00004000u, 0x00000000u, 0x00003c00u, 0x00003c00u, 0x00007c00u, 0x00000000u, 0x00004c00u}, + {0x00004000u, 0x00000000u, 0x00003c00u, 0x00007c00u, 0x00007c00u, 0x3c003c00u, 0x40004000u, 0x00007c00u, 0x40000000u, 0x00004000u, 0x00000000u, 0x00003c00u, 0x00003c00u, 0x00007800u, 0x00000000u, 0x00004b80u}, + {0x00007bffu, 0x00007bffu, 0x00001bffu, 0x00003c04u, 0x00007bffu, 0x1bff1bffu, 0x7bff7bffu, 0x00003c04u, 0x7bff0000u, 0x00007c00u, 0x00004c00u, 0x00000100u, 0x00001c00u, 0x00000000u, 0x00007c00u, 0x00007b80u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000fe00u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00003c00u, 0x0000fc00u, 0x00007c00u, 0x00007c00u, 0x00000000u, 0x00007c00u, 0x00000000u}, + {0x00003c00u, 0x0000bc00u, 0x00008000u, 0x00000000u, 0x00000000u, 0x80008000u, 0x3c003c00u, 0x00000000u, 0x3c000000u, 0x00003c00u, 0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x00008000u, 0x00007c00u, 0x00000000u}, + {0x00000002u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00020002u, 0x00000000u, 0x00020000u, 0x00003c00u, 0x0000ce00u, 0x00007c00u, 0x00006c00u, 0x00007c00u, 0x00000000u, 0x00005100u}, + {0x00003c00u, 0x0000bc00u, 0x00000100u, 0x00000100u, 0x00000000u, 0x01000100u, 0x3c003c00u, 0x00000100u, 0x3c000000u, 0x00003c00u, 0x0000cc00u, 0x00007c00u, 0x00005c00u, 0x00007c00u, 0x00000000u, 0x00005000u}, + {0x00003c00u, 0x0000bc00u, 0x00000200u, 0x00000200u, 0x00000000u, 0x02000200u, 0x3c003c00u, 0x00000200u, 0x3c000000u, 0x00003c00u, 0x0000cb80u, 0x00007800u, 0x000059a8u, 0x00007c00u, 0x00000000u, 0x00004fc0u}, + {0x00004c40u, 0x00004b80u, 0x00004c00u, 0x00004c40u, 0x00004c00u, 0x4c004c00u, 0x4c404c40u, 0x00004c40u, 0x4c400000u, 0x00007c00u, 0x00004400u, 0x00002c00u, 0x00003400u, 0x00007c00u, 0x00000000u, 0x00004a00u}, + {0x00004c40u, 0x00004b7fu, 0x00004bffu, 0x00004c40u, 0x00004bffu, 0x4bff4bffu, 0x4c404c40u, 0x00004c40u, 0x4c400000u, 0x00007bf5u, 0x00004400u, 0x00002c01u, 0x00003400u, 0x00007bffu, 0x00000000u, 0x00004a00u}, + {0x00005a40u, 0x00000000u, 0x000070e2u, 0x000070eeu, 0x00005640u, 0x70e270e2u, 0x5a405a40u, 0x000070eeu, 0x5a400000u, 0x00007c00u, 0x000046a5u, 0x0000211fu, 0x00002e66u, 0x00005640u, 0x00003c00u, 0x00000000u}, + {0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x0000fe00u, 0x00007c00u, 0xfc00fc00u, 0xfc00fc00u, 0x0000fe00u, 0xfc000000u, 0x00000000u, 0x0000fe00u, 0x00008000u, 0x0000fe00u, 0x0000fc00u, 0x00000000u, 0x00007800u}, + {0x00007800u, 0x00007800u, 0x00007c00u, 0x00007c00u, 0x00000000u, 0x7c007c00u, 0x78007800u, 0x00007c00u, 0x78000000u, 0x00007c00u, 0x00004b80u, 0x00000200u, 0x00001da8u, 0x00007800u, 0x00007f00u, 0x00000000u}, + {0x0000fc00u, 0x0000fbfeu, 0x00007c00u, 0x00007c00u, 0x00008000u, 0x7c007c00u, 0xfc00fc00u, 0x00007c00u, 0xfc000000u, 0x00000000u, 0x0000fe00u, 0x00008100u, 0x0000fe00u, 0x0000fc00u, 0x00000000u, 0x00007800u}, + {0x00007bfeu, 0x00007c00u, 0x0000fc00u, 0x0000fc00u, 0x00008000u, 0xfc00fc00u, 0x7bfe7bfeu, 0x0000fc00u, 0x7bfe0000u, 0x00007c00u, 0x00004c00u, 0x00000100u, 0x00001c00u, 0x00007c00u, 0x00000000u, 0x00004000u}, + {0x00007c00u, 0x0000fe00u, 0x00007c00u, 0x00007c00u, 0x0000fe00u, 0x7c007c00u, 0x7c007c00u, 0x00007c00u, 0x7c000000u, 0x00007c00u, 0x00007c00u, 0x00000000u, 0x00000000u, 0x00007c00u, 0x0000fc00u, 0x00000000u} +}; +constexpr const char* Names[16] = { + "v_add_f16", + "v_sub_f16", + "v_mul_f16", + "v_fma_f16", + "v_div_fixup_f16", + "v_pk_mul_f16 op_sel_hi:[0,0]", + "v_pk_add_f16 op_sel_hi:[0,0]", + "v_fma_mixlo_f16", + "v_add_f16_sdwa dst_sel:WORD_1", + "v_exp_f16", + "v_log_f16", + "v_rcp_f16", + "v_rsq_f16", + "v_ldexp_f16", + "v_cvt_f16_f32", + "v_cvt_f16_u16" +}; + +bool WholeDword(std::uint32_t column) { + return column == 5u || column == 6u || column == 8u; +} + +bool DriverNan(std::uint32_t column) { + return column != 4u && column != 5u && column != 6u && column != 13u && column != 15u; +} + +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; +} + +bool IsNan16(std::uint32_t half) { + return (half & 0x7fffu) > 0x7c00u; +} + +void Run(AgcDriver::VulkanDevice& device, 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); + 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 = floatMode; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const std::uint32_t (&expected)[32][16], const char* mode) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t column = 0; column < Results; ++column) { + const std::uint32_t actual = Output[tid * Results + column]; + const std::uint32_t wanted = expected[tid][column]; + const std::string name = std::string("fp16 overflow: lane ") + std::to_string(tid) + " " + mode + " " + Names[column] + " is " + Hex(actual) + ", expected " + Hex(wanted); + for (std::uint32_t shift = 0; shift < 32u; shift += 16u) { + if (shift == 16u && !WholeDword(column)) break; + const std::uint32_t half = (actual >> shift) & 0xffffu; + const std::uint32_t expectedHalf = (wanted >> shift) & 0xffffu; + if (DriverNan(column) && IsNan16(expectedHalf)) { + Require(IsNan16(half), name + " (any NaN)"); + } else { + Require(half == expectedHalf, name); + } + } + } + } +} + +} + +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; + } + Run(*device, std::nullopt); + Check(ExpectedInfinity, "no float mode"); + Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}); + Check(ExpectedInfinity, "FP16_OVFL=0"); + Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, true}); + Check(ExpectedOverflow, "FP16_OVFL=1"); + std::puts("fp16 overflow 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/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/GatedDispatchCapture.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GatedDispatchCapture.cpp new file mode 100644 index 0000000000..8fe09da45f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GatedDispatchCapture.cpp @@ -0,0 +1,145 @@ +#include "VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.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 SpinCode{0xf4080200u, 0xfa000000u, 0xbe840381u, 0x9304ff04u, 0x0019660du, 0x8004ff04u, 0x3c6ef35fu, 0x80828102u, 0xbf078002u, 0xbf85fff9u, 0xbf8cc07fu, 0x7e000204u, 0xe0700000u, 0x80020000u, 0xbf810000u}; +alignas(256) std::array ReaderCode{0xf4040100u, 0xfa000000u, 0xf4080201u, 0xfa000000u, 0xbf8cc07fu, 0xf4000182u, 0xfa000000u, 0xbf8cc07fu, 0x7e000206u, 0xe0700000u, 0x80020000u, 0xbf810000u}; + +constexpr std::uint32_t StaleValue = 0x57a1e000u; +constexpr std::uint32_t FreshValue = 0xf7e54000u; +constexpr std::uint32_t SpinIterationsPerDispatch = 1u << 20u; + +struct Slot { + alignas(256) std::array stale{}; + alignas(256) std::array fresh{}; + alignas(256) std::array table{}; + alignas(256) std::array output{}; + alignas(256) std::array outputDescriptor{}; + alignas(256) std::array spinOutput{}; + alignas(256) std::array spinDescriptor{}; + alignas(256) volatile std::uint32_t label = 0; + alignas(256) volatile std::uint32_t done = 0; +}; + +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); +} + +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 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 WaitEqual(std::vector& words, volatile std::uint32_t* target, std::uint32_t value) { + words.insert(words.end(), {0xc0053c00u, 0x13u, Low(target), High(target), value, 0xffffffffu, 0x19u}); +} + +void Submit(std::uint32_t queue, std::vector& words) { + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitAcb(queue, &packet) == 0, "compute submit failed"); +} + +enum class Outcome { Fresh, Stale, Inconclusive }; + +Outcome Run(Slot& slot, std::uint32_t spinDispatches) { + slot.stale[0] = StaleValue; + slot.fresh[0] = FreshValue; + slot.table = {Low(slot.stale.data()), High(slot.stale.data())}; + slot.outputDescriptor = Descriptor(slot.output.data(), sizeof(slot.output)); + slot.spinDescriptor = Descriptor(slot.spinOutput.data(), sizeof(slot.spinOutput)); + std::vector producer; + for (std::uint32_t index = 0; index < spinDispatches; ++index) Dispatch(producer, SpinCode.data(), {Low(slot.spinDescriptor.data()), High(slot.spinDescriptor.data()), SpinIterationsPerDispatch}); + WriteData(producer, &slot.label, 1); + std::vector consumer; + WaitEqual(consumer, &slot.label, 1); + Dispatch(consumer, ReaderCode.data(), {Low(slot.table.data()), High(slot.table.data()), Low(slot.outputDescriptor.data()), High(slot.outputDescriptor.data())}); + WriteData(consumer, &slot.done, 1); + Submit(0x28, producer); + Submit(0x20, consumer); + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + slot.table = {Low(slot.fresh.data()), High(slot.fresh.data())}; + std::atomic_thread_fence(std::memory_order_seq_cst); + const bool landedBeforeFill = slot.label != 0; + const auto start = std::chrono::steady_clock::now(); + while (slot.done == 0) { + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(10), "the gated submissions did not finish"); + std::this_thread::sleep_for(std::chrono::microseconds(100)); + } + AgcDriverWaitIdle_nid_postfix(); + if (landedBeforeFill) return Outcome::Inconclusive; + Require(slot.output[0] == FreshValue || slot.output[0] == StaleValue, "the gated dispatch stored neither table's value: " + std::to_string(slot.output[0])); + return slot.output[0] == FreshValue ? Outcome::Fresh : Outcome::Stale; +} + +} + +int main() { + try { + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + device.reset(); + std::uint32_t spinDispatches = (1u << 26u) / SpinIterationsPerDispatch; + for (auto& slot : Slots) { + const auto outcome = Run(slot, spinDispatches); + Require(outcome != Outcome::Stale, "a dispatch behind a cross-queue WAIT_REG_MEM read its table before the producer's label reached memory, so it missed the table the CPU filled after submitting"); + if (outcome == Outcome::Fresh) { + std::cout << "gated dispatch capture test passed (" << spinDispatches << " producer dispatches of " << SpinIterationsPerDispatch << " iterations)\n"; + AgcDriverShutdown_nid_postfix(); + return 0; + } + spinDispatches *= 2u; + } + std::cout << "the producer's label always reached memory before the table was filled: gated dispatch capture not tested\n"; + AgcDriverShutdown_nid_postfix(); + return VulkanTestSkipped; + } 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/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/GlobalAtomicCsub.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GlobalAtomicCsub.cpp index 02ac733f2c..b54523dd61 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GlobalAtomicCsub.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GlobalAtomicCsub.cpp @@ -89,7 +89,7 @@ public: block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); #endif Require(block != nullptr, "global atomic csub: cannot allocate the guest block"); - GuestAllocations::Mutation().Add(block, BlockBytes, true, true); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); } ~GuestBlock() { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GlobalAtomics.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GlobalAtomics.cpp index e36953f05e..2daef9cd54 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GlobalAtomics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GlobalAtomics.cpp @@ -292,7 +292,7 @@ public: block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); #endif Require(block != nullptr, "global atomics: cannot allocate the guest block"); - GuestAllocations::Mutation().Add(block, BlockBytes, true, writable); + GuestAllocations::Mutation().Add(block, BlockBytes, true, writable, true); } ~GuestBlock() { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GlobalAtomics64.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GlobalAtomics64.cpp index 859f39c58c..528beb55c9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GlobalAtomics64.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GlobalAtomics64.cpp @@ -256,7 +256,7 @@ public: block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); #endif Require(block != nullptr, "global atomics: cannot allocate the guest block"); - GuestAllocations::Mutation().Add(block, BlockBytes, true, writable); + GuestAllocations::Mutation().Add(block, BlockBytes, true, writable, true); } ~GuestBlock() { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GlobalAtomicsFloat.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GlobalAtomicsFloat.cpp index 800fb39473..503ccf662f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GlobalAtomicsFloat.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GlobalAtomicsFloat.cpp @@ -585,7 +585,7 @@ public: block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); #endif Require(block != nullptr, "global float atomics: cannot allocate the guest block"); - GuestAllocations::Mutation().Add(block, BlockBytes, true, writable); + GuestAllocations::Mutation().Add(block, BlockBytes, true, writable, true); } ~GuestBlock() { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GlobalAtomicsLanes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GlobalAtomicsLanes.cpp index d0bee4f750..1723943011 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GlobalAtomicsLanes.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GlobalAtomicsLanes.cpp @@ -67,7 +67,7 @@ public: block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); #endif Require(block != nullptr, "global atomics: cannot allocate the guest block"); - GuestAllocations::Mutation().Add(block, BlockBytes, true, writable); + GuestAllocations::Mutation().Add(block, BlockBytes, true, writable, true); } ~GuestBlock() { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GlobalLoadAddtid.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GlobalLoadAddtid.cpp index e7875353c9..34ccd295eb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GlobalLoadAddtid.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GlobalLoadAddtid.cpp @@ -28,12 +28,13 @@ constexpr std::uint32_t MaskedExecLo = 0x55555555u; alignas(256) std::array Table{}; alignas(256) std::array Output{}; -alignas(256) constexpr std::array AddtidCode{ +alignas(256) constexpr std::array AddtidCode{ 0x34020085, 0x34040082, 0xbe880300, 0x8709ff01, 0x0000ffff, 0x800aff08, 0x00000200, 0x820b8009, - 0x7e080280, 0x460808c1, 0x480808c1, 0x7e0a02ff, 0xabcdabcd, 0x7e0c02ff, 0xabcdabcd, 0x7e0e02ff, - 0xabcdabcd, 0xe0301000, 0x80000802, 0xdc588040, 0x05080000, 0xdc588fe4, 0x060a0000, 0xbefe03ff, - 0x55555555, 0xdc588000, 0x07080000, 0xbefe03c1, 0xe0701000, 0x80010401, 0xe0701004, 0x80010501, - 0xe0701008, 0x80010601, 0xe070100c, 0x80010701, 0xe0701010, 0x80010801, 0xbf810000, + 0x7e080280, 0xd7650004, 0x000208c1, 0xd7660004, 0x000208c1, 0x7e0a02ff, 0xabcdabcd, 0x7e0c02ff, + 0xabcdabcd, 0x7e0e02ff, 0xabcdabcd, 0xe0301000, 0x80000802, 0xdc588040, 0x05080000, 0xdc588fe4, + 0x060a0000, 0xbefe03ff, 0x55555555, 0xdc588000, 0x07080000, 0xbefe03c1, 0xe0701000, 0x80010401, + 0xe0701004, 0x80010501, 0xe0701008, 0x80010601, 0xe070100c, 0x80010701, 0xe0701010, 0x80010801, + 0xbf810000, }; struct Workgroup { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GlobalLoads.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GlobalLoads.cpp index 26938a5d33..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 @@ -94,7 +96,7 @@ public: block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); #endif Require(block != nullptr, "global loads: cannot allocate the guest block"); - GuestAllocations::Mutation().Add(block, BlockBytes, true, false); + GuestAllocations::Mutation().Add(block, BlockBytes, true, false, true); } ~GuestBlock() { @@ -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/GlobalLoadsX2X3.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GlobalLoadsX2X3.cpp index 878bf64269..73eceacdbe 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GlobalLoadsX2X3.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GlobalLoadsX2X3.cpp @@ -93,7 +93,7 @@ public: block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); #endif Require(block != nullptr, "global loads x2 x3: cannot allocate the guest block"); - GuestAllocations::Mutation().Add(block, BlockBytes, true, false); + GuestAllocations::Mutation().Add(block, BlockBytes, true, false, true); } ~GuestBlock() { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GlobalLoadsX4.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GlobalLoadsX4.cpp index 6dc40863b8..44a9af2f5e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GlobalLoadsX4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GlobalLoadsX4.cpp @@ -93,7 +93,7 @@ public: block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); #endif Require(block != nullptr, "global loads x4: cannot allocate the guest block"); - GuestAllocations::Mutation().Add(block, BlockBytes, true, false); + GuestAllocations::Mutation().Add(block, BlockBytes, true, false, true); } ~GuestBlock() { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GlobalStoreAddtid.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GlobalStoreAddtid.cpp index fcf2f94d33..7a9f4a7440 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GlobalStoreAddtid.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GlobalStoreAddtid.cpp @@ -61,7 +61,7 @@ public: block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); #endif Require(block != nullptr, "global store addtid: cannot allocate the guest block"); - GuestAllocations::Mutation().Add(block, BlockBytes, true, true); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); } ~GuestBlock() { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GlobalVccBase.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GlobalVccBase.cpp index e0e4419897..76e40037f0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GlobalVccBase.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GlobalVccBase.cpp @@ -56,7 +56,7 @@ public: block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); #endif Require(block != nullptr, "global vcc base: cannot allocate the guest block"); - GuestAllocations::Mutation().Add(block, BlockBytes, true, true); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); } ~GuestBlock() { 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 new file mode 100644 index 0000000000..5a4da94b89 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GpuMapOnFiber.cpp @@ -0,0 +1,154 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include + +using GuestFiberEntry = void (APS5_VABI*)(std::uint64_t, std::uint64_t); + +extern "C" { +std::int32_t APS5_VABI _sceFiberInitializeImpl_nid_postfix(FiberObject*, const char*, GuestFiberEntry, std::uint64_t, void*, std::uint64_t, const void*, std::uint32_t); +std::int32_t APS5_VABI sceFiberFinalize(FiberObject*); +std::int32_t APS5_VABI sceFiberRun_nid_postfix(FiberObject*, std::uint64_t, std::uint64_t*); +std::int32_t APS5_VABI sceFiberReturnToThread(std::uint64_t, std::uint64_t*); +} + +namespace { + +using AgcDriver::DriverDetail::Driver; +using AgcDriver::Graphics::Require; + +constexpr std::size_t 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; +struct alignas(8) FiberStorage { + unsigned char bytes[0x100]; +}; +FiberStorage FiberObjectStorage; + +alignas(256) constexpr std::array VertexCode{0x7e000208u, 0x7e020280u, 0x7e040280u, 0x7e0602f2u, 0xf80008cfu, 0x03020100u, 0xbf810000u, 0xbf810000u}; + +struct Header { + Shader shader{}; + ShaderUserData userData{}; +}; +alignas(8) Header VertexHeader{}; + +std::uint8_t* Mapping = nullptr; + +void RegisterVertexShader() { + VertexHeader.shader.file_header = 0x34333231u; + VertexHeader.shader.version = 0x18u; + VertexHeader.shader.header_size = sizeof(Header); + VertexHeader.shader.shader_size = static_cast(sizeof(VertexCode)); + VertexHeader.shader.code = VertexCode.data(); + VertexHeader.shader.type = 2; + VertexHeader.shader.user_data = &VertexHeader.userData; + AgcDriverRegisterShader_nid_postfix(&VertexHeader.shader); +} + +void MapForGpu() { + GuestAllocations::Mutation mutation; + mutation.Add(Mapping, MappingBytes, true, true, true); + mutation.NoteGpuMapping(Mapping, MappingBytes); +} + +void APS5_VABI MapOnFiber(std::uint64_t, std::uint64_t) { + MapForGpu(); + sceFiberReturnToThread(0, nullptr); +} + +void ReleaseMapping() { + if (Mapping == nullptr) return; +#ifdef _WIN32 + VirtualFree(Mapping, 0, MEM_RELEASE); +#else + std::free(Mapping); +#endif + Mapping = nullptr; +} + +std::size_t FiberStackUsed(std::size_t contextBytes) { + const std::size_t words = contextBytes / sizeof(std::uint64_t); + std::size_t untouched = 0; + 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)}; +} + +} + +int main() { + try { + if (!OpenVulkanTestDevice()) return VulkanTestSkipped; + RegisterVertexShader(); +#ifdef _WIN32 + Mapping = static_cast(VirtualAlloc(nullptr, MappingBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + Mapping = static_cast(std::aligned_alloc(65536, MappingBytes)); +#endif + Require(Mapping != nullptr, "GPU mapping on a fiber: cannot allocate the mapped memory"); + std::memset(Mapping, 0, MappingBytes); + 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(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(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(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 " << 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'; + AgcDriverShutdown_nid_postfix(); + ReleaseMapping(); + return 1; + } +} 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..6c92e2171d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GuestMemoryLifetime.cpp @@ -0,0 +1,261 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceAgcDriver/tests/execution/VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/tests/GuestMemoryLifetimeSupport.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +void* APS5_VABI mmap_nid_postfix(void*, std::size_t, int, int, int, std::int64_t) noexcept; +int APS5_VABI munmap_nid_postfix(void*, std::size_t) noexcept; +int APS5_VABI mprotect_nid_postfix(void*, std::size_t, int) noexcept; +void* APS5_VABI memalign_nid_postfix(std::size_t, std::size_t); +void* APS5_VABI realloc_nid_postfix(void*, std::size_t); +void APS5_VABI free_nid_postfix(void*); +int APS5_VABI sceKernelMapFlexibleMemory(void**, std::size_t, int, int); +int APS5_VABI sceKernelAllocateDirectMemory(std::int64_t, std::int64_t, std::size_t, std::size_t, int, std::int64_t*); +int APS5_VABI sceKernelMapDirectMemory(void**, std::size_t, int, int, std::int64_t, std::size_t); +int APS5_VABI sceKernelReleaseDirectMemory(std::int64_t, std::size_t); +} + +namespace { + +constexpr std::size_t BlockBytes = 65536; +constexpr std::size_t FillBytes = 4096; +constexpr std::array Pattern{0x12345678u, 0x89abcdefu, 0x24681357u, 0xfedcba98u}; + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +std::uint64_t Address(const void* pointer) { + return reinterpret_cast(pointer); +} + +bool Imported(const AgcDriver::VulkanDevice& device, const void* pointer, std::size_t bytes = BlockBytes) { + AgcDriver::Graphics::Context probe{}; + probe.device = device.Device(); + probe.hostImportAlignment = 1; + return AgcDriver::Graphics::HostImportCovers(probe, Address(pointer), bytes); +} + +void* Map(std::size_t bytes = BlockBytes) { + auto* result = mmap_nid_postfix(nullptr, bytes, 3, 0x1002, -1, 0); + Require(result != reinterpret_cast(~std::uintptr_t{0}), "guest mmap failed"); + { + GuestAllocations::Mutation mutation; + const auto range = mutation.Find(result); + Require(range.readable && range.writable && !range.gpu, "guest mmap did not register CPU-only read/write access"); + } + std::memset(result, 0, bytes); + return result; +} + +void CheckPattern(const void* pointer) { + const auto* data = static_cast(pointer); + for (std::size_t offset = 0; offset < FillBytes; offset += sizeof(Pattern)) + Require(std::memcmp(data + offset, Pattern.data(), sizeof(Pattern)) == 0, "guest mutation lost the pending GPU fill"); +} + +void Fill(AgcDriver::VulkanDevice& device, void* pointer) { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Require(device.FillBuffer(Address(pointer), FillBytes, Pattern), "Vulkan device did not import the guest allocation"); + Require(Imported(device, pointer), "GPU fill did not create a host import"); +} + +void CheckMappings(AgcDriver::VulkanDevice& device) { + auto* mapping = Map(); + Fill(device, mapping); + Require(mprotect_nid_postfix(mapping, BlockBytes, 3) == 0, "guest writable protection refresh failed"); + Require(!Imported(device, mapping), "host import survived guest writable protection refresh"); + CheckPattern(mapping); + Fill(device, mapping); + Require(mprotect_nid_postfix(mapping, BlockBytes, 1) == 0, "guest read-only protection failed"); + Require(!Imported(device, mapping), "host import survived guest protection change"); + CheckPattern(mapping); + Require(mprotect_nid_postfix(mapping, BlockBytes, 0) == 0, "guest inaccessible protection failed"); + Require(mprotect_nid_postfix(mapping, BlockBytes, 3) == 0, "guest writable protection restore failed"); + CheckPattern(mapping); + Fill(device, mapping); + void* replaced = mapping; + Require(sceKernelMapFlexibleMemory(&replaced, BlockBytes, 0x33, 0x10) == 0 && replaced == mapping, "guest fixed mapping replacement failed"); + Require(!Imported(device, mapping), "host import survived guest fixed mapping replacement"); + { + GuestAllocations::Mutation mutation; + const auto range = mutation.Find(mapping); + Require(range.readable && range.writable && range.gpu, "guest fixed mapping did not register its requested CPU and GPU access"); + } + const auto* data = static_cast(mapping); + for (std::size_t offset = 0; offset < BlockBytes; ++offset) + Require(data[offset] == 0, "GPU work reached replacement guest pages"); + Fill(device, mapping); + Require(munmap_nid_postfix(mapping, BlockBytes) == 0, "guest munmap failed"); + Require(!Imported(device, mapping), "host import survived guest munmap"); + + mapping = Map(3 * BlockBytes); + Fill(device, mapping); + auto* middle = static_cast(mapping) + BlockBytes; + Require(mprotect_nid_postfix(middle, BlockBytes, 1) == 0, "guest partial protection failed"); + Require(!Imported(device, mapping), "whole host import survived partial protection change"); + CheckPattern(mapping); + Fill(device, mapping); + Require(munmap_nid_postfix(middle, BlockBytes) == 0, "guest middle unmap failed"); + Require(Imported(device, mapping), "unrelated prefix import was retired by middle unmap"); + Require(munmap_nid_postfix(mapping, BlockBytes) == 0, "guest prefix unmap failed"); + Require(!Imported(device, mapping), "prefix host import survived guest munmap"); + Require(munmap_nid_postfix(middle + BlockBytes, BlockBytes) == 0, "guest suffix unmap failed"); +} + +void CheckHeap(AgcDriver::VulkanDevice& device) { + const std::array api{}; + ApplicationHeapRegister_nid_no_patch(api.data()); + auto* allocation = memalign_nid_postfix(BlockBytes, BlockBytes); + Require(allocation != nullptr, "guest aligned allocation failed"); + std::memset(allocation, 0, BlockBytes); + Fill(device, allocation); + auto* replacement = realloc_nid_postfix(allocation, 2 * BlockBytes); + Require(replacement != nullptr, "guest realloc failed"); + Require(!Imported(device, allocation), "host import survived guest realloc"); + CheckPattern(replacement); + free_nid_postfix(replacement); + for (int iteration = 0; iteration < 3; ++iteration) { + allocation = memalign_nid_postfix(BlockBytes, BlockBytes); + Require(allocation != nullptr, "guest heap reuse allocation failed"); + std::memset(allocation, 0, BlockBytes); + Fill(device, allocation); + free_nid_postfix(allocation); + Require(!Imported(device, allocation), "host import survived guest free or heap cache eviction"); + } +} + +void CheckLockedMutation(AgcDriver::VulkanDevice& device) { + auto* mapping = Map(); + { + std::lock_guard outer(AgcDriver::GuestMemory::GpuMutex()); + Fill(device, mapping); + Require(mprotect_nid_postfix(mapping, BlockBytes, 3) == 0, "writable protection refresh under the GPU lock did not finish"); + Require(!Imported(device, mapping), "locked protection refresh retained the import"); + CheckPattern(mapping); + Fill(device, mapping); + Require(mprotect_nid_postfix(mapping, BlockBytes, 1) == 0, "protection change under the GPU lock did not finish"); + Require(!Imported(device, mapping), "locked protection change retained the import"); + CheckPattern(mapping); + Require(munmap_nid_postfix(mapping, BlockBytes) == 0, "unmap under the GPU lock did not finish"); + } + + mapping = Map(); + { + std::lock_guard outer(AgcDriver::GuestMemory::GpuMutex()); + auto lease = std::make_shared(GuestAllocations::GuestAllocationsAcquire_nid_postfix()); + auto* recorder = AgcDriver::Graphics::Recorder::Active(); + Require(recorder != nullptr, "Vulkan device has no active recorder"); + recorder->Keep(std::move(lease)); + Require(munmap_nid_postfix(mapping, BlockBytes) == 0, "completed batch kept its lease until the outer GPU lock was released"); + } +} + +void CheckCopy(AgcDriver::VulkanDevice& device) { + auto* 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/HalfExpansion.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/HalfExpansion.cpp new file mode 100644 index 0000000000..94809ab80b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/HalfExpansion.cpp @@ -0,0 +1,116 @@ +#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 = 64; +constexpr std::uint32_t Results = 4; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0xe0302000, 0x80000400, 0xbf8c3f70, 0x7e141704, 0x2c0a0890, 0x7e161705, 0x7e0c0280, + 0x76180cf9, 0x06001604, 0x761a0cf9, 0x06011604, 0xe0782000, 0x80010a00, 0xbf810000, +}; + +struct Vector { + std::uint32_t input; + std::array expected; +}; + +constexpr Vector Vectors[] = { + {0x3c003c00u, {0x3f800000u, 0x3f800000u, 0x00000000u, 0x0000003cu}}, + {0xc000bc00u, {0xbf800000u, 0xc0000000u, 0x00000000u, 0x000000bcu}}, + {0x80000000u, {0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u}}, + {0x00008000u, {0x80000000u, 0x00000000u, 0x00000000u, 0x00000080u}}, + {0x03ff0001u, {0x33800000u, 0x387fc000u, 0x00000001u, 0x00000000u}}, + {0x040003ffu, {0x387fc000u, 0x38800000u, 0x000000ffu, 0x00000003u}}, + {0x7bff0400u, {0x38800000u, 0x477fe000u, 0x00000000u, 0x00000004u}}, + {0xfbff7bffu, {0x477fe000u, 0xc77fe000u, 0x000000ffu, 0x0000007bu}}, + {0xfc007c00u, {0x7f800000u, 0xff800000u, 0x00000000u, 0x0000007cu}}, + {0x7e01fe01u, {0xffc02000u, 0x7fc02000u, 0x00000001u, 0x000000feu}}, + {0xfe017e01u, {0x7fc02000u, 0xffc02000u, 0x00000001u, 0x0000007eu}}, + {0x7fff7e55u, {0x7fcaa000u, 0x7fffe000u, 0x00000055u, 0x0000007eu}}, + {0xfffffe55u, {0xffcaa000u, 0xffffe000u, 0x00000055u, 0x000000feu}}, + {0xfff0fff0u, {0xfffe0000u, 0xfffe0000u, 0x000000f0u, 0x000000ffu}}, + {0xa5a500f0u, {0x37700000u, 0xbcb4a000u, 0x000000f0u, 0x00000000u}}, + {0xdead00ffu, {0x377f0000u, 0xc3d5a000u, 0x000000ffu, 0x00000000u}}, + {0x7c003555u, {0x3eaaa000u, 0x7f800000u, 0x00000055u, 0x00000035u}}, + {0x3555fc00u, {0xff800000u, 0x3eaaa000u, 0x00000000u, 0x000000fcu}}, + {0x3c000200u, {0x38000000u, 0x3f800000u, 0x00000000u, 0x00000002u}}, + {0x80018001u, {0xb3800000u, 0xb3800000u, 0x00000001u, 0x00000080u}}, +}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), Threads, 0x01016facu}; +} + +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, const ShaderRecompiler::SpirvTarget& target) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) Input[tid] = Vectors[tid % std::size(Vectors)].input; + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), 4u); + const auto output = BufferDescriptor(Output.data(), Results * 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{{waveSize, 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}, + 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(); + constexpr std::array names{"low half", "high half", "BYTE_0 ldexp", "BYTE_1 ldexp"}; + for (std::uint32_t tid = 0; tid < waveSize; ++tid) { + const auto& vector = Vectors[tid % std::size(Vectors)]; + for (std::uint32_t i = 0; i < Results; ++i) { + const auto actual = Output[tid * Results + i]; + Require(actual == vector.expected[i], + "half expansion: wave " + std::to_string(waveSize) + ", lane " + std::to_string(tid) + + ", input " + Hex(vector.input) + ", " + names[i] + " is " + Hex(actual) + + ", expected " + Hex(vector.expected[i])); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device, 32u, device->ComputeTarget(32u)); + Run(*device, 64u, device->Target()); + Run(*device, 64u, device->ComputeTarget(32u)); + std::puts("half expansion 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/HostImportAtMap.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/HostImportAtMap.cpp new file mode 100644 index 0000000000..00a4f23f96 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/HostImportAtMap.cpp @@ -0,0 +1,185 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::size_t BlockBytes = 65536; +constexpr std::size_t AllocationBytes = BlockBytes * 4; +constexpr std::size_t ReservationBytes = AllocationBytes + BlockBytes * 2; + +class GuestAllocation { +public: + GuestAllocation() { +#ifdef _WIN32 + allocation = static_cast(VirtualAlloc(nullptr, ReservationBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + allocation = static_cast(std::aligned_alloc(BlockBytes, ReservationBytes)); +#endif + Require(allocation != nullptr, "host import at map: cannot allocate the guest memory"); + memory = allocation + BlockBytes; + std::memset(memory, 0, AllocationBytes); + Register(); + } + + ~GuestAllocation() { + if (registered) Unregister(); +#ifdef _WIN32 + VirtualFree(allocation, 0, MEM_RELEASE); +#else + std::free(allocation); +#endif + } + + GuestAllocation(const GuestAllocation&) = delete; + GuestAllocation& operator=(const GuestAllocation&) = delete; + + std::uint64_t Address() const { return static_cast(reinterpret_cast(memory)); } + + void Register() { + GuestAllocations::Mutation().Add(memory, AllocationBytes, true, true, true); + registered = true; + } + + void Unregister() { + GuestAllocations::Mutation().Remove(memory); + registered = false; + } + + GuestAllocations::Mapped Ranges() const { + GuestAllocations::Mapped ranges; + for (const auto& range : GuestAllocations::GuestAllocationsAcquire_nid_postfix()) { + if (range->address == Address()) ranges.push_back(range); + } + Require(ranges.size() == 1, "host import at map: the allocation is not registered"); + return ranges; + } + +private: + std::uint8_t* allocation = nullptr; + std::uint8_t* memory = nullptr; + bool registered = false; +}; + +void Reconcile(AgcDriver::VulkanDevice& device, const GuestAllocation& probe) { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + const std::array pattern{5u, 6u, 7u, 8u}; + Require(device.FillBuffer(probe.Address(), BlockBytes, pattern), "host import at map: the probe fill was not imported"); + device.WaitIdle(); +} + +void Import(AgcDriver::VulkanDevice& device, const GuestAllocations::Mapped& ranges, std::uint64_t generation = GuestAllocations::GuestAllocationsGeneration_nid_postfix()) { + if (device.ImportGuestMemory(ranges, generation, false)) return; + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Require(device.ImportGuestMemory(ranges, generation, true), "host import at map: the import under the GPU lock refused"); +} + +bool Imported(VkDevice device, std::uint64_t address, std::size_t bytes) { + AgcDriver::Graphics::Context probe{}; + probe.device = device; + probe.hostImportAlignment = 1; + return AgcDriver::Graphics::HostImportCovers(probe, address, bytes); +} + +} + +int main() { + try { +#ifdef _WIN32 + _putenv_s("APS5_PIN_WAIT_MS", "2000"); +#else + setenv("APS5_PIN_WAIT_MS", "2000", 1); +#endif + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestAllocation first; + Import(*device, first.Ranges()); + const bool importedFirst = Imported(device->Device(), first.Address(), AllocationBytes); + first.Unregister(); + Require(!Imported(device->Device(), first.Address(), AllocationBytes), "host import at map: unmapping an allocation imported before any lookup kept its import"); + GuestAllocation probe; + bool filled = false; + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + const std::array pattern{1u, 2u, 3u, 4u}; + filled = device->FillBuffer(probe.Address(), BlockBytes, pattern); + device->WaitIdle(); + } + if (!filled) { + std::printf("skipped, the device does not import guest memory\n"); + return VulkanTestSkipped; + } + Require(importedFirst, "host import at map: a mapping before any lookup was not imported"); + GuestAllocation guest; + const auto handle = device->Device(); + Require(!Imported(handle, guest.Address(), AllocationBytes), "host import at map: the allocation was imported before anything asked for it"); + Import(*device, guest.Ranges()); + Require(Imported(handle, guest.Address(), AllocationBytes), "host import at map: the mapped allocation was not imported"); + + GuestAllocation stale; + Import(*device, stale.Ranges()); + stale.Unregister(); + GuestAllocation mapped; + Import(*device, mapped.Ranges()); + Require(Imported(handle, mapped.Address(), AllocationBytes), "host import at map: a mapping made after another changed was not imported"); + + GuestAllocation remapped; + Import(*device, remapped.Ranges()); + remapped.Unregister(); + remapped.Register(); + Import(*device, remapped.Ranges()); + Require(Imported(handle, remapped.Address(), AllocationBytes), "host import at map: a range mapped again was not imported"); + GuestAllocation gone; + const auto goneRanges = gone.Ranges(); + gone.Unregister(); + Import(*device, goneRanges); + Require(!Imported(handle, gone.Address(), AllocationBytes), "host import at map: a mapping unregistered before its import was imported"); + gone.Register(); + + GuestAllocation late; + const auto lateRanges = late.Ranges(); + const auto lateAt = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + GuestAllocation newer; + Reconcile(*device, probe); + Import(*device, lateRanges, lateAt); + Require(!Imported(handle, late.Address(), AllocationBytes), "host import at map: an import older than the last reconcile was made"); + + GuestAllocation contended; + std::atomic held{false}; + std::atomic release{false}; + std::thread holder([&] { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + held = true; + while (!release) std::this_thread::yield(); + }); + while (!held) std::this_thread::yield(); + const bool unlocked = device->ImportGuestMemory(contended.Ranges(), GuestAllocations::GuestAllocationsGeneration_nid_postfix(), false); + release = true; + holder.join(); + Require(unlocked && Imported(handle, contended.Address(), AllocationBytes), "host import at map: a mapping on a device that already imports needed the GPU lock"); + std::puts("host import at map 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/HostImportTeardown.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/HostImportTeardown.cpp index 72095307ca..fb95ceae9e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/HostImportTeardown.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/HostImportTeardown.cpp @@ -35,7 +35,7 @@ public: #endif Require(block != nullptr, "host import teardown: cannot allocate the guest block"); std::memset(block, 0, BlockBytes); - GuestAllocations::Mutation().Add(block, BlockBytes, true, true); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); } ~GuestBlock() { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/HostImportWindows.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/HostImportWindows.cpp new file mode 100644 index 0000000000..e6d9bd6b30 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/HostImportWindows.cpp @@ -0,0 +1,185 @@ +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +namespace Graphics = AgcDriver::Graphics; +using Graphics::Require; + +constexpr std::uint64_t MiB = 1ull << 20u; +constexpr std::uint64_t WindowBytes = 64 * MiB; +constexpr std::uint64_t BlockBytes = 2 * WindowBytes; +constexpr std::uint64_t OtherBytes = 64 * 1024; +constexpr std::uint64_t GuestAlignment = 64 * 1024; +constexpr std::uint64_t ProbeBytes = 4096; + +std::string Hex(std::uint64_t value) { + char text[32]; + std::snprintf(text, sizeof(text), "0x%llx", static_cast(value)); + return text; +} + +class GuestBlock { +public: + explicit GuestBlock(std::uint64_t size) : bytes(size) { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(GuestAlignment, bytes)); +#endif + Require(block != nullptr, "host import windows: cannot allocate a guest block of " + Hex(bytes)); + std::memset(block, 0, bytes); + GuestAllocations::Mutation().Add(block, bytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() const { return block; } + std::uint64_t Address() const { return static_cast(reinterpret_cast(block)); } + +private: + std::uint8_t* block = nullptr; + std::uint64_t bytes = 0; +}; + +enum class Outcome : std::uint8_t { NoDevice, NoImport, Passed }; + +Graphics::Context Probe(const AgcDriver::VulkanDevice& device) { + Graphics::Context probe{}; + probe.device = device.Device(); + probe.hostImportAlignment = 1; + return probe; +} + +bool Fill(AgcDriver::VulkanDevice& device, std::uint64_t address) { + const std::array pattern{0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u}; + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + const bool filled = device.FillBuffer(address, ProbeBytes, pattern); + device.WaitIdle(); + return filled; +} + +Outcome ClearingDropsImports() { + GuestBlock guest(BlockBytes); + auto device = OpenVulkanTestDevice(); + if (!device) return Outcome::NoDevice; + if (!Fill(*device, guest.Address() + WindowBytes)) return Outcome::NoImport; + const auto probe = Probe(*device); + Require(Graphics::HostImportCovers(probe, guest.Address() + WindowBytes, ProbeBytes), "host import windows: the second window of " + Hex(guest.Address()) + " is not imported"); + Graphics::ClearHostImports(device->Device()); + Require(!Graphics::HostImportCovers(probe, guest.Address() + WindowBytes, ProbeBytes), "host import windows: ClearHostImports kept the window of " + Hex(guest.Address())); + return Outcome::Passed; +} + +Outcome WindowsFollowTheirRange() { + GuestBlock guest(BlockBytes); + auto device = OpenVulkanTestDevice(); + if (!device) return Outcome::NoDevice; + const auto probe = Probe(*device); + const auto address = guest.Address() + WindowBytes + WindowBytes / 2; + if (!Fill(*device, address)) return Outcome::NoImport; + Require(Graphics::HostImportCovers(probe, guest.Address() + WindowBytes, ProbeBytes), "host import windows: the second window of " + Hex(guest.Address()) + " is not imported"); + Require(!Graphics::HostImportCovers(probe, guest.Address(), ProbeBytes), "host import windows: the first window of " + Hex(guest.Address()) + " is imported along with the second"); + const auto first = Graphics::HostImportSerial(probe, address, ProbeBytes, false); + Require(first != 0, "host import windows: the window at " + Hex(address) + " has no serial"); + + GuestBlock other(OtherBytes); + Require(Fill(*device, address), "host import windows: the window at " + Hex(address) + " cannot be filled after an unrelated registration"); + Require(Graphics::HostImportSerial(probe, address, ProbeBytes, false) == first, "host import windows: the window at " + Hex(address) + " was dropped by an unrelated registration, although its range still owns it"); + + GuestAllocations::Mutation().Remove(guest.Data()); + GuestAllocations::Mutation().Add(guest.Data(), BlockBytes, true, true, true); + Require(Fill(*device, address), "host import windows: the window at " + Hex(address) + " cannot be filled after its range was mapped again"); + const auto second = Graphics::HostImportSerial(probe, address, ProbeBytes, false); + Require(second != 0 && second != first, "host import windows: the window of the range mapped again kept the serial " + Hex(first)); + return Outcome::Passed; +} + +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() { + try { + const auto counting = ClearingDropsImports(); + if (counting == Outcome::NoDevice) return VulkanTestSkipped; + if (counting == Outcome::NoImport) { + std::printf("skipped, the device does not import guest memory\n"); + return VulkanTestSkipped; + } + const auto windows = WindowsFollowTheirRange(); + Require(windows == Outcome::Passed, "host import windows: the device stopped importing guest memory between two scenarios"); + Require(ResidentMappingsImportAfterCollection() == Outcome::Passed, "resident import test failed"); + std::puts("host import windows tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/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/ImageAddressDimension.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageAddressDimension.cpp index fe5bc89fc0..26b6bec54b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageAddressDimension.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageAddressDimension.cpp @@ -53,9 +53,15 @@ alignas(256) constexpr std::array VolumeCode{ 0xbf810000, }; -alignas(256) constexpr std::array NarrowCode{ - 0x34020087, 0xe0301040, 0x80001001, 0xe0301044, 0x80001101, 0xbf8c3f70, 0xf0900f00, 0x00610c10, - 0xe0701070, 0x80000c01, 0xbf810000, +alignas(256) constexpr std::array VolumeArrayCode{ + 0x34020087, 0xe030104c, 0x80001301, 0xe0301050, 0x80001401, 0xe0301054, 0x80001501, 0xbf8c3f70, + 0xf0001f28, 0x00010c13, 0xe0701070, 0x80000c01, 0xe0701074, 0x80000d01, 0xe0701078, 0x80000e01, + 0xe070107c, 0x80000f01, 0xbf810000, +}; + +alignas(256) constexpr std::array NarrowCode{ + 0x34020087, 0xe0301040, 0x80001001, 0xe0301048, 0x80001101, 0xbf8c3f70, 0xf0900f00, 0x00610c10, + 0xe0701070, 0x80000c01, 0xe0701074, 0x80000d01, 0xe0701078, 0x80000e01, 0xbf810000, }; struct Sample { @@ -166,10 +172,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 BilinearClampSamplerDescriptor() { + return {2u << 3u, 0xfffu << 12u, (1u << 20u) | (1u << 22u) | (1u << 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); @@ -213,14 +222,49 @@ void CheckVolume() { } } -void RequireRefused(AgcDriver::VulkanDevice& device) { - std::string refusal; - try { - Run(device, NarrowCode, TextureDescriptor(Texels.data())); - } catch (const std::exception& error) { - refusal = error.what(); +void CheckVolumeArray(AgcDriver::VulkanDevice& device) { + FillInput(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) Buffer[tid * Words + PlainInput + 5u] = tid % Depth; + Run(device, VolumeArrayCode, VolumeDescriptor()); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto sample = SampleOf(tid); + const std::array expected{VolumeSliceMarker(tid % Depth), sample.x >> sample.level, sample.y >> sample.level, 255u}; + for (std::uint32_t component = 0; component < 4u; ++component) { + const float value = std::bit_cast(Buffer[tid * Words + PlainResult + 4u + component]) * 255.0f; + Require(std::lround(value) == static_cast(expected[component]), "image_load 2d_array on a 3D texture: thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + std::to_string(value) + ", expected " + std::to_string(expected[component])); + } + } +} + +void CheckNarrow(AgcDriver::VulkanDevice& device) { + FillInput(); + Run(device, NarrowCode, TextureDescriptor(Texels.data())); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto sample = SampleOf(tid); + const std::array expected{sample.level, sample.x >> sample.level, 0u}; + for (std::uint32_t component = 0; component < 3u; ++component) { + const float value = std::bit_cast(Buffer[tid * Words + PlainResult + 4u + component]) * 255.0f; + Require(std::lround(value) == static_cast(expected[component]), "image_sample_l 1d on a 2D texture: thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + std::to_string(value) + ", expected " + std::to_string(expected[component])); + } + } +} + +void CheckNarrowBilinearClamp(AgcDriver::VulkanDevice& device) { + FillInput(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto sample = SampleOf(tid); + Buffer[tid * Words + PlainInput] = Bits((static_cast(sample.x) + 0.5f) / static_cast(Size)); + Buffer[tid * Words + PlainInput + 2u] = Bits(0.0f); + } + Run(device, NarrowCode, TextureDescriptor(Texels.data()), BilinearClampSamplerDescriptor()); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto sample = SampleOf(tid); + const std::array expected{0.0f, static_cast(sample.x), 0.0f}; + for (std::uint32_t component = 0; component < 3u; ++component) { + const float value = std::bit_cast(Buffer[tid * Words + PlainResult + 4u + component]) * 255.0f; + Require(std::fabs(value - expected[component]) < 0.25f, "image_sample_l 1d on a 2D texture, bilinear clamp: thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + std::to_string(value) + ", expected " + std::to_string(expected[component])); + } } - Require(refusal.find("too few coordinate components") != std::string::npos, "image_sample_l 1d on a 2D texture was not refused: " + refusal); } } @@ -236,7 +280,9 @@ int main() { FillVolume(); Run(*device, VolumeCode, VolumeDescriptor()); CheckVolume(); - RequireRefused(*device); + CheckVolumeArray(*device); + CheckNarrow(*device); + CheckNarrowBilinearClamp(*device); std::puts("image address dimension tests passed"); return 0; } catch (const std::exception& error) { 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..0d29ae08e4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageArrayWindowPastLayerLimit.cpp @@ -0,0 +1,147 @@ +#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{}; +alignas(65536) std::array Texels{}; + +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() { + Texels.fill(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(Layers - 1u)); + 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 { + 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/ImageBy.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageBy.cpp index fccae5c4f4..47e0ea0344 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageBy.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageBy.cpp @@ -166,7 +166,7 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - GuestAllocations::Mutation().Add(Texels.data(), Bytes, true, true); + GuestAllocations::Mutation().Add(Texels.data(), Bytes, true, true, true); constexpr Format r8{R8UInt, 1u, 1u}; constexpr Format r16{R16UInt, 2u, 1u}; constexpr Format rg8{Rg8UInt, 1u, 2u}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp index a74578e4d8..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, @@ -103,7 +125,7 @@ public: block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); #endif Require(block != nullptr, "image converted unorm: cannot allocate the guest block"); - GuestAllocations::Mutation().Add(block, BlockBytes, true, true); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); } ~GuestBlock() { @@ -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/ImagePacked16Load.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePacked16Load.cpp new file mode 100644 index 0000000000..7d7053efa5 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePacked16Load.cpp @@ -0,0 +1,152 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.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 Width = 256; +constexpr std::uint32_t Groups = Width / Threads; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t SwizzleXYZW = 0xfacu; +constexpr std::uint32_t SwizzleZYXW = 0xf2eu; +constexpr std::uint32_t One = 0x3f800000u; +constexpr std::array PointSampler{0x00000092u, 0x00fff000u, 0u, 0u}; + +struct Packed { + std::uint32_t format; + std::array masks; + const char* name; +}; + +constexpr std::array Formats{{ + {133u, {0x001fu, 0x07e0u, 0xf800u, 0x0000u}, "5_6_5_UNORM"}, + {134u, {0x001fu, 0x03e0u, 0x7c00u, 0x8000u}, "1_5_5_5_UNORM"}, + {136u, {0x000fu, 0x00f0u, 0x0f00u, 0xf000u}, "4_4_4_4_UNORM"}, +}}; + +alignas(256) std::array Output{}; +alignas(256) std::array Texels{}; + +alignas(256) constexpr std::array LoadXyzw{ + 0x7e080218u, 0x343c0885u, 0x4a3c3d00u, 0x34063c84u, 0x7e3e0280u, 0x7e140280u, 0x7e160280u, 0x7e180280u, + 0x7e1a0280u, 0xf0001f08u, 0x00010a1eu, 0xbf8c3f70u, 0xe0781000u, 0x80000a03u, 0xbf810000u, +}; + +alignas(256) constexpr std::array SampleLz{ + 0x34060084u, 0x7e280280u, 0x7e2a0280u, 0xf09c0f08u, 0x00610a14u, 0xbf8c3f70u, 0xe0781000u, 0x80000a03u, + 0xbf810000u, +}; + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::uint32_t Pattern(std::uint32_t x) { + return (x + 1u) & 15u; +} + +std::array TextureDescriptor(std::uint32_t format, std::uint32_t swizzle) { + const auto address = static_cast(reinterpret_cast(Texels.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((Width - 1u) & 3u) << 30u), + (Width - 1u) >> 2u, + swizzle | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +void Fill(const Packed& format) { + for (std::uint32_t x = 0; x < Width; ++x) { + std::uint16_t texel = 0; + for (std::uint32_t component = 0; component < 4u; ++component) { + if (((Pattern(x) >> component) & 1u) != 0u) texel = static_cast(texel | format.masks[component]); + } + Texels[x] = texel; + } +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, std::uint32_t format, std::uint32_t swizzle, std::uint32_t groups) { + Output.fill(0xdeadbeefu); + std::vector userData(24, 0u); + const auto outputAddress = reinterpret_cast(Output.data()); + const std::array output{static_cast(outputAddress), static_cast((outputAddress >> 32u) & 0xffffu), static_cast(Output.size() * 4u), 0x31016facu}; + const auto texture = TextureDescriptor(format, swizzle); + std::copy(output.begin(), output.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + std::copy(PointSampler.begin(), PointSampler.end(), userData.begin() + 12); + 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} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, groups, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Verify(const Packed& format, std::uint32_t swizzle, std::uint32_t texels, bool sampled, const std::string& what) { + for (std::uint32_t index = 0; index < texels; ++index) { + const auto x = sampled ? 0u : index; + std::array channels{}; + for (std::uint32_t component = 0; component < 4u; ++component) { + channels[component] = format.masks[component] != 0u && ((Pattern(x) >> component) & 1u) != 0u ? One : 0u; + } + if (format.masks[3] == 0u) channels[3] = channels[0]; + for (std::uint32_t component = 0; component < 4u; ++component) { + const auto selector = (swizzle >> (component * 3u)) & 7u; + const auto expected = selector == 0u ? 0u : selector == 1u ? One : channels[selector - 4u]; + const auto actual = Output[index * 4u + component]; + Require(actual == expected, what + ": " + (sampled ? "thread " : "texel ") + std::to_string(index) + " component " + std::to_string(component) + " is " + Hex(actual) + ", expected " + Hex(expected)); + } + } +} + +void Check(AgcDriver::VulkanDevice& device, const Packed& format, std::uint32_t swizzle, const std::string& swizzleName) { + Fill(format); + Run(device, LoadXyzw, format.format, swizzle, Groups); + Verify(format, swizzle, Width, false, std::string(format.name) + " image_load " + swizzleName); + Run(device, SampleLz, format.format, swizzle, 1u); + Verify(format, swizzle, Threads, true, std::string(format.name) + " image_sample_lz " + swizzleName); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const auto& format : Formats) { + Check(*device, format, SwizzleXYZW, "X Y Z W"); + Check(*device, format, SwizzleZYXW, "Z Y X W"); + } + std::puts("image packed 16-bit load 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/ImagePckN.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePckN.cpp index b16b208471..c080c817d4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImagePckN.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePckN.cpp @@ -170,7 +170,7 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - GuestAllocations::Mutation().Add(Texels.data(), Bytes, true, true); + GuestAllocations::Mutation().Add(Texels.data(), Bytes, true, true, true); constexpr Format r8{R8UInt, 1u, 1u}; constexpr Format r16{R16UInt, 2u, 1u}; constexpr Format rg8{Rg8UInt, 1u, 2u}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImagePipeBankXor.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePipeBankXor.cpp new file mode 100644 index 0000000000..caf7f8f01f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePipeBankXor.cpp @@ -0,0 +1,153 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#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 +#include +#include + +namespace { + +using namespace AgcDriver::Graphics; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Width = 160; +constexpr std::uint32_t Height = 70; +constexpr std::uint32_t Format8888UNorm = 56; +constexpr std::uint32_t Type2D = 9; + +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; +constexpr std::size_t TexelBytes = 1u << 20u; +alignas(4096) std::array TexelStorage{}; +const std::span Texels(TexelStorage.data() + ((0u - reinterpret_cast(TexelStorage.data())) & 0xffffu), TexelBytes); + +alignas(256) constexpr std::array Code{ + 0x34020084, 0xe0381000, 0x80000201, 0xbf8c3f70, 0xf0001f08, 0x00020602, 0xbf8c3f70, 0xe0781000, + 0x80010601, 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 tileMode, std::uint32_t pipeBankXor) { + const auto address = static_cast(reinterpret_cast(Texels.data())) + pipeBankXor; + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (((Width - 1u) & 3u) << 30u), + ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), + 0xfacu | (tileMode << 20u) | (Type2D << 28u), + 0u, 0u, 0u, 0u, + }; +} + +std::array Texel(std::uint32_t x, std::uint32_t y) { + return {static_cast(x + 1u), static_cast(y * 3u + 2u), static_cast((x ^ y) & 0xffu), 0xff}; +} + +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; +} + +void FillTexels(std::uint32_t tileMode, std::uint32_t pipeBankXor) { + std::ranges::fill(Texels, std::uint8_t{0}); + const auto descriptor = DecodeTextureResource(TextureDescriptor(tileMode, pipeBankXor)); + Require(descriptor.baseAddress == reinterpret_cast(Texels.data()) && descriptor.pipeBankXor == pipeBankXor, "the descriptor base did not split into an aligned base and its pipe/bank XOR"); + const auto geometry = DescribeSurface(descriptor); + Require(geometry.guestBytes <= Texels.size(), "texture does not fit the test allocation"); + const auto* equation = FindTextureSwizzleEquation(XorSwizzleMode(descriptor.tileMode), 4u); + Require(equation != nullptr, "swizzle equation is missing"); + const auto& mip = geometry.mips.at(0); + const auto layout = ThinBlockLayout(descriptor.tileMode, 4u); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + auto offset = mip.tiledOffset; + if (mip.tail) offset += EquationOffset(*equation, x + mip.tailX, y + mip.tailY) ^ pipeBankXor; + else offset += (static_cast(y / layout[2]) * mip.blocksPerRow + x / layout[1]) * layout[0] + (EquationOffset(*equation, x, y) ^ pipeBankXor); + const auto texel = Texel(x, y); + std::copy(texel.begin(), texel.end(), Texels.begin() + static_cast(offset)); + } + } +} + +void FillInput() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Input[tid * 4u + 0u] = (tid * 37u + 11u) % Width; + Input[tid * 4u + 1u] = (tid * 23u + 5u) % Height; + Input[tid * 4u + 2u] = 0u; + Input[tid * 4u + 3u] = 0u; + } + Input[0] = 0u; Input[1] = 0u; + Input[4] = Width - 1u; Input[5] = Height - 1u; + Input[8] = 33u; Input[9] = 17u; +} + +void Run(AgcDriver::VulkanDevice& device, std::uint32_t tileMode, std::uint32_t pipeBankXor, const char* name) { + FillTexels(tileMode, pipeBankXor); + Output.fill(-1.0f); + std::vector userData(16, 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(tileMode, pipeBankXor); + 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); + 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 auto expected = Texel(Input[tid * 4u], Input[tid * 4u + 1u]); + for (std::uint32_t component = 0; component < 4u; ++component) { + const float scaled = Output[tid * 4u + component] * 255.0f; + Require(std::fabs(scaled - std::round(scaled)) < 1e-3f && std::lround(scaled) == expected[component], std::string(name) + ": texel (" + std::to_string(Input[tid * 4u]) + ", " + std::to_string(Input[tid * 4u + 1u]) + ") component " + std::to_string(component) + " is " + std::to_string(scaled) + "/255, expected " + std::to_string(expected[component]) + "/255"); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillInput(); + Run(*device, 0x16u, 0x000u, "SW_4KB_D_X without a pipe/bank XOR"); + Run(*device, 0x16u, 0xa00u, "SW_4KB_D_X with pipe/bank XOR 0xa00"); + Run(*device, 0x19u, 0x5600u, "SW_64KB_S_X with pipe/bank XOR 0x5600"); + Run(*device, 0x1bu, 0xf100u, "SW_64KB_R_X with pipe/bank XOR 0xf100"); + std::puts("image pipe/bank XOR 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/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 new file mode 100644 index 0000000000..6e3186841c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnLine.cpp @@ -0,0 +1,374 @@ +#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 Mips = 5; +constexpr std::uint32_t Format8888UNorm = 56; +constexpr std::uint32_t Type1D = 8; +constexpr std::uint32_t Type2D = 9; +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 Plane{}; +alignas(256) std::array MippedLine{}; + +alignas(256) constexpr std::array Code{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xe0301010, 0x80000601, 0xe0301014, 0x80000701, 0xbf8c3f70, 0xf09c0f08, 0x00610c02, + 0xf0001f08, 0x00011005, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xe0701024, 0x80000d01, 0xe0701028, + 0x80000e01, 0xe070102c, 0x80000f01, 0xe0701030, 0x80001001, 0xe0701034, 0x80001101, 0xe0701038, + 0x80001201, 0xe070103c, 0x80001301, 0xbf810000, +}; + +alignas(256) constexpr std::array ExplicitLodCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 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}; + +std::uint32_t TexelOf(std::uint32_t tid) { + return (tid * 7u + 3u) % Size; +} + +std::uint32_t Bits(float value) { + return std::bit_cast(value); +} + +void FillInput(bool line) { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto x = TexelOf(tid); + const auto u = Bits((static_cast(x) + 0.5f) / static_cast(Size)); + const auto v = line ? Bits(LineV[tid % LineV.size()]) : Bits((static_cast((tid * 5u + 1u) % Size) + 0.5f) / static_cast(Size)); + const auto y = line ? LineY[tid % LineY.size()] : (tid * 5u + 1u) % Size; + const std::array words{u, v, 0u, x, y, 0u}; + std::copy(words.begin(), words.end(), &Buffer[tid * Words]); + } +} + +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 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 { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (((Size - 1u) & 3u) << 30u), + ((Size - 1u) >> 2u) | ((Size - 1u) << 14u), + 0xfacu | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +void FillTexture(std::span storage, const std::array& descriptor, std::uint32_t rows) { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(descriptor)); + Require(geometry.guestBytes <= storage.size(), "image plane 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 y = 0; y < rows; ++y) { + for (std::uint32_t x = 0; x < Size; ++x) { + auto* texel = &storage[geometry.GuestLayerOffset(0) + mip.tiledOffset + static_cast(y) * mip.pitchBytes + x * 4u]; + texel[0] = 77u; + texel[1] = static_cast(x); + texel[2] = static_cast(y); + texel[3] = 255u; + } + } +} + +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}; +} + +std::array SamplerDescriptor() { + return {0u, 0xfffu << 12u, 1u << 26u, 0u}; +} + +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)); + 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() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::array texel{77u, TexelOf(tid), 0u, 255u}; + Expect(tid, SampleResult, texel, "image_sample_lz 2d on a 1D texture with v " + std::to_string(LineV[tid % LineV.size()])); + Expect(tid, LoadResult, texel, "image_load 2d on a 1D texture with y " + std::to_string(static_cast(LineY[tid % LineY.size()]))); + } +} + +void CheckPlane() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::array texel{77u, TexelOf(tid), (tid * 5u + 1u) % Size, 255u}; + Expect(tid, SampleResult, texel, "image_sample_lz 2d on a 2D texture"); + Expect(tid, LoadResult, texel, "image_load 2d on a 2D texture"); + } +} + +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() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillTexture(Line, LineDescriptor(), 1u); + FillInput(true); + Run(*device, Code, LineDescriptor()); + CheckLine(); + FillTexture(Plane, PlaneDescriptor(), Size); + FillInput(false); + Run(*device, Code, PlaneDescriptor()); + CheckPlane(); + CheckExplicitLod(*device); + CheckArray(*device); + std::puts("image plane address on line tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnLineImplicitLod.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnLineImplicitLod.cpp new file mode 100644 index 0000000000..28a89a1f78 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnLineImplicitLod.cpp @@ -0,0 +1,196 @@ +#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 Width = 64; +constexpr std::uint32_t Height = 16; +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 float SecondCoordinateScale = 3.0f; +alignas(256) std::array Pixels{}; +alignas(256) std::array Line{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0xbefe0a7e, 0x10040010, 0x06060300, 0x10060611, 0xf0800f08, 0x00610402, 0xbf8c3f70, 0xf800180f, 0x07060504, 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 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 | ((Mips - 1u) << 16u) | (Type1D << 28u), + 0u, + (Mips - 1u) << 4u, + 0u, + 0u, + }; +} + +std::array SamplerDescriptor() { + return {0u, 0xfffu << 12u, 1u << 26u, 0u}; +} + +std::uint8_t LevelMark(std::uint32_t level) { + return static_cast(10u + 40u * level); +} + +void FillTexture() { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(LineDescriptor())); + Require(geometry.guestBytes <= Line.size(), "implicit LOD through a 1D texture: the texture does not fit the texel storage"); + Require(geometry.mips.size() == Mips, "implicit LOD through a 1D texture: the texture has " + std::to_string(geometry.mips.size()) + " mips"); + std::fill(Line.begin(), Line.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) { + auto* texel = &Line[geometry.GuestLayerOffset(0) + mip.tiledOffset + x * 4u]; + texel[0] = LevelMark(level); + texel[1] = static_cast(x * 8u); + texel[2] = 99u; + texel[3] = 255u; + } + } +} + +void Draw(AgcDriver::VulkanDevice& device, std::span pixelCode, float firstCoordinateScale) { + const auto& target = device.Target(); + 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(18, 0u); + const auto texture = LineDescriptor(); + const auto sampler = SamplerDescriptor(); + std::copy(texture.begin(), texture.end(), pixelUserData.begin() + 4); + std::copy(sampler.begin(), sampler.end(), pixelUserData.begin() + 12); + pixelUserData[16] = std::bit_cast(firstCoordinateScale); + pixelUserData[17] = std::bit_cast(SecondCoordinateScale); + 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(std::uint32_t level) { + const auto what = "image_sample 2d on a 1D texture with the first coordinate advancing " + std::to_string(1u << level) + " texels per pixel"; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const std::array expected{LevelMark(level), (x % (Size >> level)) * 8u, 99u, 255u}; + for (std::uint32_t component = 0; component < 4u; ++component) { + const auto value = std::to_integer(Pixels[(y * Width + x) * 4u + component]); + Require(value == expected[component], what + ": 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::string refusal; + try { + Draw(device, BiasPixelCode, 1.0f / static_cast(Size)); + } 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 on a 1D texture was not refused: " + refusal); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillTexture(); + for (std::uint32_t level = 0; level < 3u; ++level) { + Draw(*device, PixelCode, static_cast(1u << level) / static_cast(Size)); + Check(level); + } + RequireRefused(*device); + std::puts("image plane address on line implicit LOD 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/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/ImageSampleAnisoOverride.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleAnisoOverride.cpp new file mode 100644 index 0000000000..401c860e30 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleAnisoOverride.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 +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Size = 8; +constexpr std::uint32_t Levels = 4; +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 AnisoSampler{0x00000092u | (4u << 9u), 0x00fff000u, (3u << 20u) | (3u << 22u), 0u}; +constexpr std::uint32_t AnisoOverride = 1u << 29u; + +alignas(256) std::array Output{}; +using Storage = std::array; +alignas(256) Storage SingleIntegers{}; +alignas(256) Storage SingleColors{}; +alignas(256) Storage MipIntegers{}; +alignas(256) Storage MipColors{}; + +alignas(256) constexpr std::array Code{ + 0x34060085u, 0x7e2802ffu, 0x3ef00000u, 0x7e2a02ffu, 0x3f300000u, 0xf09c0f08u, 0x00610a14u, 0xf09c0f08u, + 0x00640e14u, 0xbf8c3f70u, 0xe0781000u, 0x80000a03u, 0xe0781010u, 0x80000e03u, 0xbf810000u, 0xbf810000u, +}; + +struct Levelled { + std::uint32_t lastLevel; + std::uint32_t maxMip; +}; + +constexpr Levelled SingleLevel{0, 0}; +constexpr Levelled Mipmapped{Levels - 1u, Levels - 1u}; +constexpr Levelled ClampedToOneLevel{Levels - 1u, 0}; + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +AgcDriver::Graphics::TileMipLayout BaseLevel(std::uint32_t format, std::uint32_t levels) { + const auto mips = AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, format, Size, Size, levels); + Require(AgcDriver::Graphics::ComputeSurfaceSize(mips, 1) <= Storage{}.size(), "image sample aniso override: a texture does not fit its storage"); + return mips[0]; +} + +std::int32_t Integer(std::uint32_t x, std::uint32_t y) { + return -static_cast(100u * x + 7u * y + 1u); +} + +void Fill(Storage& integers, Storage& colors, std::uint32_t levels) { + const auto integerLevel = BaseLevel(Format32SInt, levels); + const auto colorLevel = BaseLevel(Format8888UNorm, levels); + 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[integerLevel.linearOffset + static_cast(y) * integerLevel.pitchBytes + x * 4u]; + for (std::uint32_t byte = 0; byte < 4u; ++byte) integer[byte] = static_cast(bits >> (byte * 8u)); + auto* color = &colors[colorLevel.linearOffset + static_cast(y) * colorLevel.pitchBytes + 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, Levelled levels) { + 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 | (levels.lastLevel << 16u) | (Type2D << 28u), + 0u, + levels.maxMip << 4u, + 0u, + 0u, + }; +} + +std::optional Run(AgcDriver::VulkanDevice& device, Levelled first, Levelled second, std::uint32_t samplerFlags) { + Output.fill(0xdeadbeefu); + std::vector userData(24, 0u); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + const auto integers = TextureDescriptor(first.maxMip == 0 ? SingleIntegers.data() : MipIntegers.data(), Format32SInt, SwizzleX111, first); + const auto colors = TextureDescriptor(second.maxMip == 0 ? SingleColors.data() : MipColors.data(), Format8888UNorm, IdentitySwizzle, second); + auto sampler = AnisoSampler; + sampler[2] |= samplerFlags; + std::copy(output.begin(), output.end(), userData.begin()); + std::copy(integers.begin(), integers.end(), userData.begin() + 4); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 12); + std::copy(colors.begin(), colors.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, {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} + }; + request.useCache = false; + try { + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + } catch (const std::runtime_error& error) { + return std::string(error.what()); + } + return std::nullopt; +} + +void CheckSamples(const std::string& what) { + const std::array integer{static_cast(Integer(SampleX, SampleY)), 1u, 1u, 1u}; + const std::array color{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], what + ": 32_SINT lane " + std::to_string(lane) + " component " + std::to_string(component) + " is " + Hex(actual) + ", expected " + Hex(integer[component])); + const auto bits = Output[lane * 8u + 4u + component]; + Require(std::fabs(std::bit_cast(bits) - color[component]) <= Tolerance, what + ": 8_8_8_8_UNORM lane " + std::to_string(lane) + " component " + std::to_string(component) + " is " + Hex(bits) + ", expected " + std::to_string(color[component])); + } + } +} + +void ExpectRuns(AgcDriver::VulkanDevice& device, const std::string& what, Levelled first, Levelled second, std::uint32_t samplerFlags) { + const auto error = Run(device, first, second, samplerFlags); + Require(!error.has_value(), what + " was refused: " + error.value_or("")); + CheckSamples(what); +} + +void ExpectMixedRefusal(AgcDriver::VulkanDevice& device, const std::string& what, Levelled first, Levelled second) { + const auto error = Run(device, first, second, AnisoOverride); + Require(error.has_value() && error->find("both single-level and mipmapped") != std::string::npos, what + " was not refused as a mixed ANISO_OVERRIDE pairing" + (error.has_value() ? ": " + *error : std::string())); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Fill(SingleIntegers, SingleColors, 1u); + Fill(MipIntegers, MipColors, Levels); + ExpectRuns(*device, "an ANISO_OVERRIDE sampler shared by two single-level images", SingleLevel, SingleLevel, AnisoOverride); + ExpectRuns(*device, "an ANISO_OVERRIDE sampler shared by two mipmapped images", Mipmapped, Mipmapped, AnisoOverride); + ExpectRuns(*device, "an ANISO_OVERRIDE sampler shared by a mipmapped image and one whose LAST_LEVEL is clamped to MAX_MIP 0", ClampedToOneLevel, Mipmapped, AnisoOverride); + ExpectRuns(*device, "an anisotropic sampler without ANISO_OVERRIDE shared by a single-level and a mipmapped image", SingleLevel, Mipmapped, 0u); + ExpectMixedRefusal(*device, "an ANISO_OVERRIDE sampler shared by a single-level and a mipmapped image", SingleLevel, Mipmapped); + ExpectMixedRefusal(*device, "an ANISO_OVERRIDE sampler shared by a single-level image and one whose LAST_LEVEL is clamped to MAX_MIP 0", SingleLevel, ClampedToOneLevel); + std::puts("image sample aniso override 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/ImageSampleBorderColor.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleBorderColor.cpp new file mode 100644 index 0000000000..b4a5db512a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleBorderColor.cpp @@ -0,0 +1,114 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.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 Width = 4; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t Unorm8_8_8_8 = 56; +constexpr std::uint32_t Uint8_8_8_8 = 60; +constexpr std::uint32_t SwizzleXYZW = 0xfacu; +constexpr std::uint32_t One = 0x3f800000u; +constexpr std::uint32_t ClampBorder = 0x1b6u; +constexpr std::uint32_t AnisoOverride = (1u << 29u) | (2u << 22u) | (2u << 20u); + +alignas(256) std::array Output{}; +alignas(256) std::array Texels{}; + +alignas(256) constexpr std::array SampleOutside{ + 0x34060084u, 0x7e2802f4u, 0x7e2a02f4u, 0xf09c0f08u, 0x00610a14u, 0xbf8c3f70u, 0xe0781000u, 0x80000a03u, + 0xbf810000u, +}; + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::array TextureDescriptor(std::uint32_t format) { + const auto address = static_cast(reinterpret_cast(Texels.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((Width - 1u) & 3u) << 30u), + (Width - 1u) >> 2u, + SwizzleXYZW | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +void Run(AgcDriver::VulkanDevice& device, std::uint32_t format, std::uint32_t borderType, std::uint32_t filters) { + Output.fill(0xdeadbeefu); + Texels.fill(0u); + std::vector userData(24, 0u); + const auto outputAddress = reinterpret_cast(Output.data()); + const std::array output{static_cast(outputAddress), static_cast((outputAddress >> 32u) & 0xffffu), static_cast(Output.size() * 4u), 0x31016facu}; + const auto texture = TextureDescriptor(format); + const std::array sampler{ClampBorder, 0x00fff000u, filters, borderType << 30u}; + std::copy(output.begin(), output.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(SampleOutside); + 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} + }; + 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 format, std::uint32_t borderType, const std::array& expected, const std::string& what, std::uint32_t filters = 0u) { + Run(device, format, borderType, filters); + for (std::uint32_t thread = 0; thread < Threads; ++thread) { + for (std::uint32_t component = 0; component < 4u; ++component) { + const auto actual = Output[thread * 4u + component]; + Require(actual == expected[component], what + ": thread " + std::to_string(thread) + " component " + std::to_string(component) + " is " + Hex(actual) + ", expected " + Hex(expected[component])); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Check(*device, Unorm8_8_8_8, 0u, {0u, 0u, 0u, 0u}, "8_8_8_8_UNORM transparent black border"); + Check(*device, Unorm8_8_8_8, 1u, {0u, 0u, 0u, One}, "8_8_8_8_UNORM opaque black border"); + Check(*device, Unorm8_8_8_8, 2u, {One, One, One, One}, "8_8_8_8_UNORM opaque white border"); + Check(*device, Uint8_8_8_8, 0u, {0u, 0u, 0u, 0u}, "8_8_8_8_UINT transparent black border"); + Check(*device, Uint8_8_8_8, 1u, {0u, 0u, 0u, 1u}, "8_8_8_8_UINT opaque black border"); + Check(*device, Uint8_8_8_8, 2u, {1u, 1u, 1u, 1u}, "8_8_8_8_UINT opaque white border"); + Check(*device, Unorm8_8_8_8, 2u, {One, One, One, One}, "8_8_8_8_UNORM opaque white border under ANISO_OVERRIDE", AnisoOverride); + Check(*device, Uint8_8_8_8, 2u, {1u, 1u, 1u, 1u}, "8_8_8_8_UINT opaque white border under ANISO_OVERRIDE", AnisoOverride); + std::puts("image sample border color 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/ImageSampleCubeDerivatives.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleCubeDerivatives.cpp new file mode 100644 index 0000000000..b44e5d2429 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleCubeDerivatives.cpp @@ -0,0 +1,163 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Words = 32; +constexpr std::uint32_t ResultWord = 8; +constexpr std::uint32_t Size = 16; +constexpr std::uint32_t Levels = 5; +constexpr std::uint32_t Faces = 6; +constexpr std::uint32_t Gradients = 4; +constexpr std::uint32_t Format8888UNorm = 56; +constexpr std::uint32_t TypeCube = 11; +alignas(256) std::array Buffer{}; +alignas(256) std::array Texels{}; + +alignas(256) constexpr std::array Code{ + 0x34020087, 0xe0301000, 0x80000501, 0xe0301004, 0x80000601, 0xe0301008, 0x80000701, 0xe030100c, + 0x80000801, 0xe0301010, 0x80000901, 0xe0301014, 0x80000a01, 0xe0301018, 0x80000b01, 0xbf8c3f70, + 0xf0880f18, 0x00610c05, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xe0701024, 0x80000d01, 0xe0701028, + 0x80000e01, 0xe070102c, 0x80000f01, 0xbf810000, +}; + +struct Sample { + std::uint32_t level; + std::uint32_t face; + std::uint32_t gradient; + std::uint32_t x; + std::uint32_t y; +}; + +Sample SampleOf(std::uint32_t tid) { + return {tid % Levels, (tid / Levels) % Faces, (tid / 3u) % Gradients, (tid * 7u + 3u) % Size, (tid * 5u + 1u) % Size}; +} + +std::uint32_t Bits(float value) { + return std::bit_cast(value); +} + +std::array TextureDescriptor(const void* data) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (((Size - 1u) & 3u) << 30u), + ((Size - 1u) >> 2u) | ((Size - 1u) << 14u), + 0xfacu | ((Levels - 1u) << 16u) | (TypeCube << 28u), + Faces - 1u, + (Levels - 1u) << 4u, + 0u, + 0u, + }; +} + +void FillInput() { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto sample = SampleOf(tid); + auto* words = &Buffer[tid * Words]; + for (std::uint32_t gradient = 0; gradient < Gradients; ++gradient) { + words[gradient] = Bits(gradient == sample.gradient ? std::exp2(static_cast(sample.level) - 0.25f) / static_cast(Size) : 0.0f); + } + words[4] = Bits(1.0f + (static_cast(sample.x) + 0.5f) / static_cast(Size)); + words[5] = Bits(1.0f + (static_cast(sample.y) + 0.5f) / static_cast(Size)); + words[6] = Bits(static_cast(sample.face)); + } +} + +void FillTexture() { + Texels.fill(0xeeu); + const auto surface = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(Texels.data()))); + Require(surface.layers == Faces && surface.mips.size() == Levels && surface.guestBytes <= Texels.size(), "image sample cube derivatives: the cube mip chain does not fit the texel storage"); + for (std::uint32_t face = 0; face < Faces; ++face) { + for (std::uint32_t level = 0; level < Levels; ++level) { + const auto& mip = surface.mips[level]; + for (std::uint32_t y = 0; y < mip.height; ++y) { + for (std::uint32_t x = 0; x < mip.width; ++x) { + auto* texel = &Texels[surface.GuestLayerOffset(face) + mip.tiledOffset + static_cast(y) * mip.pitchBytes + x * 4u]; + texel[0] = static_cast(level); + texel[1] = static_cast(face); + texel[2] = 0u; + texel[3] = 255u; + } + } + } + } +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array SamplerDescriptor() { + return {0u, 0xfffu << 12u, 1u << 26u, 0u}; +} + +void Run(AgcDriver::VulkanDevice& device) { + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Buffer.data(), static_cast(Buffer.size() * 4u)); + const auto texture = TextureDescriptor(Texels.data()); + const auto sampler = SamplerDescriptor(); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 12); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto sample = SampleOf(tid); + const std::array expected{sample.level, sample.face, 0u, 255u}; + for (std::uint32_t component = 0; component < 4u; ++component) { + const float value = std::bit_cast(Buffer[tid * Words + ResultWord + component]) * 255.0f; + Require(std::lround(value) == static_cast(expected[component]), "image_sample_d cube: thread " + std::to_string(tid) + " (face " + std::to_string(sample.face) + ", level " + std::to_string(sample.level) + ") component " + std::to_string(component) + " is " + std::to_string(value) + ", expected " + std::to_string(expected[component])); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillInput(); + FillTexture(); + Run(*device); + Check(); + std::puts("image sample cube derivatives tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp index 965994506d..331882d14a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp @@ -24,9 +24,14 @@ constexpr std::uint32_t Threads = 32; constexpr std::uint32_t Size = 4; constexpr std::uint32_t Format8888UNorm = 56; constexpr std::uint32_t Format8888Srgb = 130; +constexpr std::uint32_t Format32Float = 22; +constexpr std::uint32_t 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; +constexpr std::uint32_t FilterAnisoBilinear = 3; +constexpr std::uint32_t AnisoOverride = 1u << 29u; constexpr std::uint32_t ForceDegamma = 1u << 20u; alignas(256) std::array Input{}; @@ -57,8 +62,8 @@ std::array TextureDescriptor(std::uint32_t format) { }; } -std::array SamplerDescriptor(std::uint32_t filter, std::uint32_t forced) { - return {0x92u | forced, 0x00fff000u, (filter << 20u) | (filter << 22u) | (1u << 24u), 0u}; +std::array SamplerDescriptor(std::uint32_t filter, std::uint32_t forced, std::uint32_t filters) { + return {0x92u | forced, 0x00fff000u, (filter << 20u) | (filter << 22u) | (1u << 24u) | filters, 0u}; } std::uint8_t Code8(std::uint32_t x, std::uint32_t y, std::uint32_t channel) { @@ -92,12 +97,12 @@ float SrgbToLinear(std::uint8_t code) { return c <= 0.04045f ? c / 12.92f : std::pow((c + 0.055f) / 1.055f, 2.4f); } -std::array Sample(AgcDriver::VulkanDevice& device, std::uint32_t format, std::uint32_t filter, std::uint32_t forced) { +std::array Sample(AgcDriver::VulkanDevice& device, std::uint32_t format, std::uint32_t filter, std::uint32_t forced, std::uint32_t filters = 0u) { 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(format); - const auto sampler = SamplerDescriptor(filter, forced); + const auto sampler = SamplerDescriptor(filter, forced, filters); 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); @@ -118,13 +123,13 @@ std::array Sample(AgcDriver::VulkanDevice& device, std::uint return Output; } -void RequireSame(const std::array& forced, const std::array& plain, const char* name) { +void RequireSame(const std::array& forced, const std::array& plain, const char* name, const char* first = "through FORCE_DEGAMMA", const char* second = "without it") { for (std::uint32_t index = 0; index < forced.size(); ++index) { - Require(std::bit_cast(forced[index]) == std::bit_cast(plain[index]), std::string(name) + ": thread " + std::to_string(index / 4u) + " component " + std::to_string(index % 4u) + " sampled " + std::to_string(forced[index]) + " through FORCE_DEGAMMA, " + std::to_string(plain[index]) + " without it"); + Require(std::bit_cast(forced[index]) == std::bit_cast(plain[index]), std::string(name) + ": thread " + std::to_string(index / 4u) + " component " + std::to_string(index % 4u) + " sampled " + std::to_string(forced[index]) + " " + first + ", " + std::to_string(plain[index]) + " " + second); } } -void RequireDecoded(const std::array& sampled) { +void RequireDecoded(const std::array& sampled, const char* name) { for (std::uint32_t tid = 0; tid < Threads; ++tid) { const auto x = tid % Size; const auto y = (tid / Size) % Size; @@ -132,7 +137,7 @@ void RequireDecoded(const std::array& sampled) { const auto code = Code8(x, y, channel); const float expected = channel == 3u ? static_cast(code) / 255.0f : SrgbToLinear(code); const float value = sampled[tid * 4u + channel]; - Require(std::fabs(value - expected) <= 0.005f, "8_8_8_8_SRGB through FORCE_DEGAMMA: thread " + std::to_string(tid) + " component " + std::to_string(channel) + " sampled " + std::to_string(value) + ", expected " + std::to_string(expected)); + Require(std::fabs(value - expected) <= 0.005f, std::string(name) + " through FORCE_DEGAMMA: thread " + std::to_string(tid) + " component " + std::to_string(channel) + " sampled " + std::to_string(value) + ", expected " + std::to_string(expected)); } } } @@ -156,12 +161,23 @@ int main() { FillTexels(); FillInput(true); const auto forcedPoint = Sample(*device, Format8888Srgb, FilterPoint, ForceDegamma); - RequireDecoded(forcedPoint); + RequireDecoded(forcedPoint, "8_8_8_8_SRGB"); RequireSame(forcedPoint, Sample(*device, Format8888Srgb, FilterPoint, 0u), "8_8_8_8_SRGB, point"); FillInput(false); const auto forcedBilinear = Sample(*device, Format8888Srgb, FilterBilinear, ForceDegamma); RequireSame(forcedBilinear, Sample(*device, Format8888Srgb, FilterBilinear, 0u), "8_8_8_8_SRGB, bilinear"); - Reject(*device, Format8888UNorm, "guest format 56, which is not sRGB, is sampled through a sampler that forces sRGB decoding"); + RequireSame(Sample(*device, Format8888Srgb, FilterAnisoBilinear, ForceDegamma, AnisoOverride), forcedBilinear, "8_8_8_8_SRGB, single level", "anisotropically under ANISO_OVERRIDE", "bilinearly"); + FillInput(true); + const auto unormPoint = Sample(*device, Format8888UNorm, FilterPoint, ForceDegamma); + RequireDecoded(unormPoint, "8_8_8_8_UNORM"); + RequireSame(unormPoint, forcedPoint, "8_8_8_8_UNORM, point", "through FORCE_DEGAMMA", "as 8_8_8_8_SRGB"); + const auto unormPlain = Sample(*device, Format8888UNorm, FilterPoint, 0u); + Require(unormPlain[0] != unormPoint[0] || unormPlain[4] != unormPoint[4], "8_8_8_8_UNORM without FORCE_DEGAMMA sampled the decoded values"); + FillInput(false); + RequireSame(Sample(*device, Format8888UNorm, FilterBilinear, ForceDegamma), forcedBilinear, "8_8_8_8_UNORM, bilinear", "through FORCE_DEGAMMA", "as 8_8_8_8_SRGB"); + 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 4d12749125..4bf2fffbce 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleUnnormalized.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleUnnormalized.cpp @@ -47,7 +47,42 @@ alignas(256) constexpr std::array Code{ 0x80001001, 0xe0701034, 0x80001101, 0xe0701038, 0x80001201, 0xe070103c, 0x80001301, 0xbf810000, }; -constexpr std::array Instructions{"image_sample_lz", "image_sample_l 2.7", "image_sample"}; +alignas(256) constexpr std::array LevelZeroCode{ + 0x34020086, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xbf8c3f70, 0xf09c0f08, 0x00610802, + 0xbf8c3f70, 0xe0701010, 0x80000801, 0xe0701014, 0x80000901, 0xe0701018, 0x80000a01, 0xe070101c, + 0x80000b01, 0xbf810000, +}; + +alignas(256) constexpr std::array GradientCode{ + 0x34020086, 0xe0301000, 0x80000601, 0xe0301004, 0x80000701, 0xbf8c3f70, 0x7e0402f6, 0x7e060280, + 0x7e080280, 0x7e0a02f6, 0xf0880f08, 0x00610802, 0xbf8c3f70, 0xe0701010, 0x80000801, 0xe0701014, + 0x80000901, 0xe0701018, 0x80000a01, 0xe070101c, 0x80000b01, 0xbf810000, +}; + +alignas(256) constexpr std::array OffsetCode{ + 0x34020086, 0xe0301008, 0x80000201, 0xe0301000, 0x80000301, 0xe0301004, 0x80000401, 0xbf8c3f70, + 0x7e0a02ff, 0x402ccccd, 0x7e2802ff, 0x0000023d, 0xf0dc0f08, 0x00610802, 0xf0d00f08, 0x00610c02, + 0xf0dc0f0a, 0x00611014, 0x00000403, 0xbf8c3f70, 0xe0701010, 0x80000801, 0xe0701014, 0x80000901, + 0xe0701018, 0x80000a01, 0xe070101c, 0x80000b01, 0xe0701020, 0x80000c01, 0xe0701024, 0x80000d01, + 0xe0701028, 0x80000e01, 0xe070102c, 0x80000f01, 0xe0701030, 0x80001001, 0xe0701034, 0x80001101, + 0xe0701038, 0x80001201, 0xe070103c, 0x80001301, 0xbf810000, +}; + +enum class Offset { None, Lane, Constant }; +constexpr std::int32_t ConstantOffsetU = -3; +constexpr std::int32_t ConstantOffsetV = 2; + +struct Program { + std::span code; + std::array instructions; + std::array offsets; + std::size_t count = 3; +}; + +const Program Plain{Code, {"image_sample_lz", "image_sample_l 2.7", "image_sample"}, {Offset::None, Offset::None, Offset::None}}; +const Program LevelZero{LevelZeroCode, {"image_sample_lz", "", ""}, {Offset::None, Offset::None, Offset::None}, 1}; +const Program Gradient{GradientCode, {"image_sample_d (4, 0, 0, 4)", "", ""}, {Offset::None, Offset::None, Offset::None}, 1}; +const Program Offsets{OffsetCode, {"image_sample_lz_o", "image_sample_l_o 2.7", "image_sample_lz_o (-3, 2) NSA"}, {Offset::Lane, Offset::Lane, Offset::Constant}}; alignas(256) constexpr std::array ConstantCode{ 0x34020086, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xbf8c3f70, 0xf09c0608, 0x00610802, @@ -62,43 +97,68 @@ struct SamplerCase { std::array words; bool linear; bool border; + bool atBaseLevel; }; constexpr std::array Samplers{{ - {"bilinear clamp-to-last-texel, point mips, MAX_LOD 0xfff", {0x00008092u, 0x00fff000u, 0x05500000u, 0u}, true, false}, - {"point clamp-to-last-texel, point mips", {0x00008092u, 0x00fff000u, 0x04000000u, 0u}, false, false}, - {"bilinear clamp-to-last-texel, linear mips, MIN_LOD 1, LOD bias 1.5", {0x00008092u, 0x00fff100u, 0x09500180u, 0u}, true, false}, - {"bilinear clamp-to-border, transparent black", {0x000080b6u, 0x00fff000u, 0x05500000u, 0u}, true, true}, - {"point clamp-to-border, no mips, transparent black", {0x000080b6u, 0u, 0u, 0u}, false, true}, + {"bilinear clamp-to-last-texel, point mips, MAX_LOD 0xfff", {0x00008092u, 0x00fff000u, 0x05500000u, 0u}, true, false, true}, + {"point clamp-to-last-texel, point mips", {0x00008092u, 0x00fff000u, 0x04000000u, 0u}, false, false, true}, + {"bilinear clamp-to-last-texel, linear mips, MIN_LOD 1, LOD bias 1.5", {0x00008092u, 0x00fff100u, 0x09500180u, 0u}, true, false, false}, + {"bilinear clamp-to-border, transparent black", {0x000080b6u, 0x00fff000u, 0x05500000u, 0u}, true, true, true}, + {"point clamp-to-border, no mips, transparent black", {0x000080b6u, 0u, 0u, 0u}, false, true, true}, }}; struct Coordinate { float u; float v; + std::int32_t du = 0; + std::int32_t dv = 0; }; -std::vector Coordinates() { +std::int32_t LevelWidth(std::uint32_t level) { + return std::max(Width >> level, 1); +} + +std::int32_t LevelHeight(std::uint32_t level) { + return std::max(Height >> level, 1); +} + +std::vector Coordinates(std::uint32_t level) { + const auto width = LevelWidth(level); + const auto height = LevelHeight(level); std::vector us; - for (std::int32_t k = 0; k < Width; ++k) { + for (std::int32_t k = 0; k < width; ++k) { for (const float fraction : {0.5f, 0.0f, 0.25f, 0.75f}) us.push_back(static_cast(k) + fraction); } - for (const float edge : {-1.0f, -0.5f, 13.0f, 14.0f, 12.75f}) us.push_back(edge); + for (const float edge : {-1.0f, -0.5f, static_cast(width), static_cast(width + 1), static_cast(width) - 0.25f}) us.push_back(edge); constexpr std::array fractions{0.5f, 0.0f, 0.75f}; std::vector result; for (std::size_t index = 0; index < us.size(); ++index) { for (std::size_t fraction = 0; fraction < fractions.size(); ++fraction) { - result.push_back({us[index], static_cast((index * fractions.size() + fraction) % Height) + fractions[fraction]}); + result.push_back({us[index], static_cast((index * fractions.size() + fraction) % height) + fractions[fraction]}); } } - for (std::int32_t x = 0; x < Width; ++x) { - const float v = static_cast(x % Height) + 0.5f; + for (std::int32_t x = 0; x < width; ++x) { + const float v = static_cast(x % height) + 0.5f; result.push_back({static_cast(x) - 1.0f, v}); result.push_back({static_cast(x) + 2.0f, v}); } return result; } -std::array TexelOf(std::int32_t x, std::int32_t y) { +std::vector OffsetCoordinates() { + constexpr std::array us{-32, -5, -1, 0, 1, 3, 31}; + constexpr std::array vs{-2, -1, 0, 2, -32, 31}; + auto result = Coordinates(0u); + for (std::size_t index = 0; index < result.size(); ++index) { + result[index].du = us[index % us.size()]; + result[index].dv = vs[index % vs.size()]; + } + return result; +} + +std::array TexelOf(std::uint32_t level, std::int32_t x, std::int32_t y) { + if (level == 1u) return {240.0 * ((x + y + 1) & 1), 32.0 * y + 48.0, 16.0 * x + 40.0, 200.0}; return {240.0 * ((x + y) & 1), 16.0 * x + 16.0, 32.0 * y + 32.0, 240.0}; } @@ -111,9 +171,9 @@ void FillTexture(std::span texels, std::uint32_t levels) { for (std::uint32_t y = 0; y < mip.height; ++y) { for (std::uint32_t x = 0; x < mip.width; ++x) { auto* texel = &texels[mip.tiledOffset + static_cast(y) * mip.pitchBytes + x * 4u]; - const auto value = TexelOf(static_cast(x), static_cast(y)); + const auto value = TexelOf(level, static_cast(x), static_cast(y)); for (std::uint32_t component = 0; component < 4u; ++component) { - texel[component] = level == 0u ? static_cast(value[component]) : OtherLevelTexel; + texel[component] = level <= 1u ? static_cast(value[component]) : OtherLevelTexel; } } } @@ -147,7 +207,7 @@ std::array TextureDescriptor(const void* data, std::uint32_t l using Samples = std::vector>; -Samples Run(AgcDriver::VulkanDevice& device, const std::array& texture, const std::array& sampler, const std::vector& coordinates, std::span code = Code) { +Samples Run(AgcDriver::VulkanDevice& device, const Program& program, const std::array& texture, const std::array& sampler, const std::vector& coordinates) { Samples samples; for (std::size_t first = 0; first < coordinates.size(); first += Threads) { Buffer.fill(0xdeadbeefu); @@ -155,12 +215,14 @@ Samples Run(AgcDriver::VulkanDevice& device, const std::array& const auto coordinate = first + lane < coordinates.size() ? coordinates[first + lane] : Coordinate{0.5f, 0.5f}; Buffer[lane * Words] = std::bit_cast(coordinate.u); Buffer[lane * Words + 1u] = std::bit_cast(coordinate.v); + Buffer[lane * Words + 2u] = 0xffffc0c0u | (static_cast(coordinate.du) & 0x3fu) | ((static_cast(coordinate.dv) & 0x3fu) << 8u); } 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 auto code = program.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{ @@ -182,13 +244,15 @@ Samples Run(AgcDriver::VulkanDevice& device, const std::array& return samples; } -std::array Fetch(const SamplerCase& sampler, std::int32_t x, std::int32_t y) { - if (sampler.border && (x < 0 || x >= Width || y < 0 || y >= Height)) return {0.0, 0.0, 0.0, 0.0}; - return TexelOf(std::clamp(x, 0, Width - 1), std::clamp(y, 0, Height - 1)); +std::array Fetch(const SamplerCase& sampler, std::uint32_t level, std::int32_t x, std::int32_t y) { + const auto width = LevelWidth(level); + const auto height = LevelHeight(level); + if (sampler.border && (x < 0 || x >= width || y < 0 || y >= height)) return {0.0, 0.0, 0.0, 0.0}; + return TexelOf(level, std::clamp(x, 0, width - 1), std::clamp(y, 0, height - 1)); } -std::array Reference(const SamplerCase& sampler, const Coordinate& coordinate) { - if (!sampler.linear) return Fetch(sampler, static_cast(std::floor(coordinate.u)), static_cast(std::floor(coordinate.v))); +std::array Reference(const SamplerCase& sampler, std::uint32_t level, const Coordinate& coordinate) { + if (!sampler.linear) return Fetch(sampler, level, static_cast(std::floor(coordinate.u)), static_cast(std::floor(coordinate.v))); const double u = static_cast(coordinate.u) - 0.5; const double v = static_cast(coordinate.v) - 0.5; const auto i = static_cast(std::floor(u)); @@ -197,30 +261,34 @@ std::array Reference(const SamplerCase& sampler, const Coordinate& co const double beta = v - j; std::array result{}; for (std::uint32_t component = 0; component < 4u; ++component) { - result[component] = (1.0 - alpha) * (1.0 - beta) * Fetch(sampler, i, j)[component] + alpha * (1.0 - beta) * Fetch(sampler, i + 1, j)[component] - + (1.0 - alpha) * beta * Fetch(sampler, i, j + 1)[component] + alpha * beta * Fetch(sampler, i + 1, j + 1)[component]; + result[component] = (1.0 - alpha) * (1.0 - beta) * Fetch(sampler, level, i, j)[component] + alpha * (1.0 - beta) * Fetch(sampler, level, i + 1, j)[component] + + (1.0 - alpha) * beta * Fetch(sampler, level, i, j + 1)[component] + alpha * beta * Fetch(sampler, level, i + 1, j + 1)[component]; } return result; } -void Check(const SamplerCase& sampler, const char* image, const std::vector& coordinates, const Samples& samples) { +void Check(const Program& program, const SamplerCase& sampler, std::uint32_t level, const char* image, const std::vector& coordinates, const Samples& samples) { Require(samples.size() == coordinates.size(), "image sample unnormalized: a dispatch lost samples"); const double tolerance = sampler.linear ? 0.5 : 0.25; for (std::size_t index = 0; index < coordinates.size(); ++index) { - const auto expected = Reference(sampler, coordinates[index]); - for (std::uint32_t instruction = 0; instruction < Instructions.size(); ++instruction) { + const auto& coordinate = coordinates[index]; + for (std::uint32_t instruction = 0; instruction < program.count; ++instruction) { + const auto offset = program.offsets[instruction]; + const std::int32_t du = offset == Offset::Lane ? coordinate.du : offset == Offset::Constant ? ConstantOffsetU : 0; + const std::int32_t dv = offset == Offset::Lane ? coordinate.dv : offset == Offset::Constant ? ConstantOffsetV : 0; + const auto expected = Reference(sampler, level, {coordinate.u + static_cast(du), coordinate.v + static_cast(dv)}); for (std::uint32_t component = 0; component < 4u; ++component) { const double value = static_cast(std::bit_cast(samples[index][instruction * 4u + component])) * 255.0; - Require(std::fabs(value - expected[component]) <= tolerance, std::string(Instructions[instruction]) + ", " + sampler.name + ", " + image + ": (" + std::to_string(coordinates[index].u) + ", " + std::to_string(coordinates[index].v) + ") component " + std::to_string(component) + " is " + std::to_string(value) + ", expected " + std::to_string(expected[component])); + Require(std::fabs(value - expected[component]) <= tolerance, std::string(program.instructions[instruction]) + ", " + sampler.name + ", " + image + ": (" + std::to_string(coordinate.u) + ", " + std::to_string(coordinate.v) + ") offset (" + std::to_string(du) + ", " + std::to_string(dv) + ") component " + std::to_string(component) + " is " + std::to_string(value) + ", expected " + std::to_string(expected[component])); } } } } -void ExpectFailure(AgcDriver::VulkanDevice& device, const std::array& texture, const std::array& sampler, std::string_view reason, const char* what) { +void ExpectFailure(AgcDriver::VulkanDevice& device, const Program& program, const std::array& texture, const std::array& sampler, std::string_view reason, const char* what) { const std::vector one{{0.5f, 0.5f}}; try { - static_cast(Run(device, texture, sampler, one)); + static_cast(Run(device, program, texture, sampler, one)); } catch (const std::exception& error) { Require(std::string_view(error.what()).find(reason) != std::string_view::npos, std::string(what) + ": unexpected error: " + error.what()); return; @@ -236,19 +304,42 @@ int main() { if (!device) return VulkanTestSkipped; FillTexture(SingleLevel, 1); FillTexture(MultiLevel, StorageLevels); - const auto coordinates = Coordinates(); + const auto coordinates = Coordinates(0u); + const auto levelOneCoordinates = Coordinates(1u); const auto single = TextureDescriptor(SingleLevel.data(), 0u, 0u); const auto multi = TextureDescriptor(MultiLevel.data(), 0u, StorageLevels - 1u); + const auto chain = TextureDescriptor(MultiLevel.data(), StorageLevels - 1u, StorageLevels - 1u); + auto fromLevelOne = chain; + fromLevelOne[3] |= 1u << 12u; + const bool driverSamplesUnnormalizedAboveMipZero = device->DeviceName().find("llvmpipe") == std::string::npos; + const auto offsetCoordinates = OffsetCoordinates(); for (const auto& sampler : Samplers) { - const auto singleSamples = Run(*device, single, sampler.words, coordinates); - Check(sampler, "1-level image", coordinates, singleSamples); - const auto multiSamples = Run(*device, multi, sampler.words, coordinates); - Check(sampler, "first level of a 4-level image", coordinates, multiSamples); + const auto singleSamples = Run(*device, Plain, single, sampler.words, coordinates); + Check(Plain, sampler, 0u, "1-level image", coordinates, singleSamples); + const auto multiSamples = Run(*device, Plain, multi, sampler.words, coordinates); + Check(Plain, sampler, 0u, "first level of a 4-level image", coordinates, multiSamples); Require(multiSamples == singleSamples, std::string(sampler.name) + ": the first level of a 4-level image does not sample like a 1-level image"); + Check(Offsets, sampler, 0u, "1-level image", offsetCoordinates, Run(*device, Offsets, single, sampler.words, offsetCoordinates)); + Check(Gradient, sampler, 0u, "1-level image", coordinates, Run(*device, Gradient, single, sampler.words, coordinates)); + Check(Gradient, sampler, 0u, "first level of a 4-level image", coordinates, Run(*device, Gradient, multi, sampler.words, coordinates)); + if (!sampler.atBaseLevel) continue; + const auto levelZeroSingle = Run(*device, LevelZero, single, sampler.words, coordinates); + const auto levelZeroChain = Run(*device, LevelZero, chain, sampler.words, coordinates); + Check(LevelZero, sampler, 0u, "4-level view", coordinates, levelZeroChain); + Require(levelZeroChain == levelZeroSingle, std::string(sampler.name) + ": a 4-level view read by image_sample_lz does not sample its base level like a 1-level image"); + if (driverSamplesUnnormalizedAboveMipZero) Check(LevelZero, sampler, 1u, "3-level view starting at mip 1", levelOneCoordinates, Run(*device, LevelZero, fromLevelOne, sampler.words, levelOneCoordinates)); } - ExpectFailure(*device, TextureDescriptor(MultiLevel.data(), 2u, 2u), Samplers[0].words, "single-level", "a 3-level view"); - ExpectFailure(*device, single, {0x00008092u, 0x00fff000u, 0x05100000u, 0u}, "different minification", "unequal minification and magnification filters"); - ExpectFailure(*device, single, {0x00008090u, 0x00fff000u, 0x05500000u, 0u}, "clamp mode 0", "wrap on X"); + ExpectFailure(*device, Plain, TextureDescriptor(MultiLevel.data(), 2u, 2u), Samplers[0].words, "single-level", "a 3-level view read by a selected LOD"); + ExpectFailure(*device, Plain, chain, Samplers[0].words, "single-level", "a 4-level view read by image_sample_l and image_sample"); + ExpectFailure(*device, Plain, fromLevelOne, Samplers[0].words, "single-level", "a view starting at mip 1 read by image_sample_l and image_sample"); + ExpectFailure(*device, Offsets, chain, Samplers[0].words, "single-level", "a 4-level view read with texel offsets"); + ExpectFailure(*device, Gradient, chain, Samplers[0].words, "single-level", "a 4-level view read by image_sample_d"); + ExpectFailure(*device, LevelZero, chain, Samplers[2].words, "single-level", "a 4-level view through an S# with a minimum LOD and a LOD bias"); + 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); @@ -256,7 +347,7 @@ int main() { return descriptor; }; for (const auto& sampler : Samplers) { - for (const auto& sample : Run(*device, constantView(Type3D, Format8888UNorm, OneZeroOneZero), sampler.words, coordinates)) { + for (const auto& sample : Run(*device, Plain, constantView(Type3D, Format8888UNorm, OneZeroOneZero), sampler.words, coordinates)) { for (std::uint32_t index = 0; index < Results; ++index) { const std::uint32_t expected = index % 2u == 0u ? 0x3f800000u : 0u; Require(sample[index] == expected, std::string(sampler.name) + ": a 3D view whose channels select (1, 0, 1, 0) returned " + Hex(sample[index]) + " for result " + std::to_string(index)); @@ -275,10 +366,11 @@ int main() { {"a 1D view selecting (0, 0, 0, 0)", constantView(Type1D, Format8888UNorm, 0u), 0u}, }}; const std::array constantInstructions{"image_sample_lz dmask 0x6", "image_gather4_lz dmask 0x2", "image_gather4_lz dmask 0x4"}; + const Program constantProgram{ConstantCode, constantInstructions, Plain.offsets}; for (const auto& constant : constants) { auto normalized = Samplers[0].words; normalized[0] &= ~0x8000u; - for (const auto& sample : Run(*device, constant.texture, normalized, coordinates, ConstantCode)) { + for (const auto& sample : Run(*device, constantProgram, constant.texture, normalized, coordinates)) { const std::array expected{0u, constant.one, 0u, 0u, 0u, 0u, constant.one, constant.one, constant.one, constant.one}; for (std::uint32_t index = 0; index < expected.size(); ++index) { const auto instruction = constantInstructions[index < 2u ? 0u : index < 6u ? 1u : 2u]; 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 560166355b..eafabe0d7c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbLoad.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbLoad.cpp @@ -66,12 +66,21 @@ constexpr std::array SamplerCode(std::uint32_t word0) { }; } +constexpr std::array MixedSamplerCode(std::uint32_t word0) { + return { + 0x34060084u, 0x7e280280u, 0x7e2a0280u, 0x7e2c0280u, 0xf0bc0108u, 0x00610a14u, 0xbf8c3f70u, + 0xe0701000u, 0x80000a03u, word0, 0x00610a14u, 0xbf8c3f70u, 0xe0701004u, 0x80000a03u, 0xbf810000u, + }; +} + alignas(256) constexpr auto LoadXyzw = LoadCode(0xfu); alignas(256) constexpr auto LoadXy = LoadCode(0x3u); alignas(256) constexpr auto StoreX = StoreCode(0x1u); alignas(256) constexpr auto StoreXy = StoreCode(0x3u); alignas(256) constexpr auto SampleLz = SamplerCode(0xf09c0f08u); alignas(256) constexpr auto Gather4Lz = SamplerCode(0xf11c0108u); +alignas(256) constexpr auto MixedSampleLz = MixedSamplerCode(0xf09c0f08u); +alignas(256) constexpr auto MixedGather4Lz = MixedSamplerCode(0xf11c0108u); class GuestBlock { public: @@ -82,7 +91,7 @@ public: block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); #endif Require(block != nullptr, "image sRGB load: cannot allocate the guest block"); - GuestAllocations::Mutation().Add(block, BlockBytes, true, true); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); } ~GuestBlock() { @@ -147,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)); @@ -167,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) { @@ -185,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); } @@ -235,6 +264,8 @@ int main() { CheckStoredLoad(*device, block, LoadXyzw, 0xfu, Srgb8, SwizzleXXX1, "8_SRGB image_load dmask:0xf X X X 1 of 8_UNORM image_stores"); RequireRefused(*device, SampleLz, "image_sample_lz"); RequireRefused(*device, Gather4Lz, "image_gather4_lz"); + RequireRefused(*device, MixedSampleLz, "image_sample_c_lz followed by image_sample_lz"); + RequireRefused(*device, MixedGather4Lz, "image_sample_c_lz followed by image_gather4_lz"); std::puts("image sRGB load tests passed"); return 0; } catch (const std::exception& error) { 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/ImageStoreArrayThroughVolume.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreArrayThroughVolume.cpp new file mode 100644 index 0000000000..b410321d5c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreArrayThroughVolume.cpp @@ -0,0 +1,173 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Size = 4; +constexpr std::uint32_t Depth = 2; +constexpr std::uint32_t Format32UInt = 20; +constexpr std::uint32_t Type3D = 10; +constexpr std::uint32_t IdentitySwizzle = 0xfacu; +constexpr std::size_t TexelBytes = 16384; +constexpr std::size_t BlockBytes = 65536; + +alignas(256) std::array Input{}; + +alignas(256) constexpr std::array StoreCode{ + 0x34020084, 0xe0381000, 0x80001401, 0xbf8c3f70, 0xf0201128, 0x00011714, 0x7e2c0282, 0xf0201128, + 0x00011714, 0xbf810000, +}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "image store array through volume: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::uint32_t Value(std::uint32_t tid) { + return (tid + 1u) * 0x9e3779b1u; +} + +std::vector Initial() { + std::vector bytes(TexelBytes); + for (std::size_t index = 0; index < bytes.size(); ++index) bytes[index] = static_cast(0x80u + 37u * index + 11u * (index >> 4u)); + return bytes; +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array VolumeDescriptor(const void* data) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((Size - 1u) & 3u) << 30u), + ((Size - 1u) >> 2u) | ((Size - 1u) << 14u), + IdentitySwizzle | (Type3D << 28u), + Depth - 1u, + 0u, + 0u, + 0u, + }; +} + +void Dispatch(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::size_t bytes) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::array words{tid % Size, (tid / Size) % Size, tid / (Size * Size), Value(tid)}; + std::copy(words.begin(), words.end(), Input.begin() + tid * 4u); + } + const auto initial = Initial(); + std::copy(initial.begin(), initial.end(), texels); + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto texture = VolumeDescriptor(texels); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + const std::span code(StoreCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(texels), bytes, nullptr, "test"); + device.WaitIdle(); +} + +void Check(const std::uint8_t* texels) { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(VolumeDescriptor(texels))); + const auto& mip = geometry.mips.at(0); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto x = tid % Size; + const auto y = (tid / Size) % Size; + const auto z = tid / (Size * Size); + std::uint32_t actual = 0; + std::memcpy(&actual, texels + geometry.GuestLayerOffset(z) + mip.tiledOffset + static_cast(y) * mip.pitchBytes + x * 4u, 4u); + Require(actual == Value(tid), "image_store 2d_array through a 3D T#: texel (" + std::to_string(x) + ", " + std::to_string(y) + ", " + std::to_string(z) + ") is " + Hex(actual) + ", expected " + Hex(Value(tid))); + } + const auto initial = Initial(); + Require(std::equal(texels + geometry.guestBytes, texels + TexelBytes, initial.begin() + static_cast(geometry.guestBytes)), "image_store 2d_array through a 3D T#: a slice past the volume changed bytes after it"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock block; + auto* texels = block.Data(); + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(VolumeDescriptor(texels))); + Require(geometry.guestBytes <= TexelBytes, "image store array through volume: the volume does not fit the texel storage"); + Dispatch(*device, texels, geometry.guestBytes); + Check(texels); + std::puts("image store array through volume tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/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/ImageStoreLineThroughPlane.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreLineThroughPlane.cpp new file mode 100644 index 0000000000..6f6e1cfb2d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreLineThroughPlane.cpp @@ -0,0 +1,193 @@ +#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 Width = 32; +constexpr std::uint32_t Type1D = 8; +constexpr std::uint32_t IdentitySwizzle = 0xfacu; +constexpr std::size_t TexelBytes = 4096; +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 line through plane: 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; +}; + +constexpr std::array Formats{{ + {"32_SINT", 21u, 1u}, + {"32_32_32_32_SINT", 76u, 4u}, +}}; + +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, std::uint32_t index) { + return (tid * 4u + index + 1u) * 0x9e3779b1u; +} + +std::uint32_t SecondStore(std::uint32_t x, std::uint32_t component) { + constexpr std::uint32_t Dmask = 0x5u; + if (((Dmask >> component) & 1u) == 0u) return 0u; + std::uint32_t data = 0; + for (std::uint32_t lower = 0; lower < component; ++lower) data += (Dmask >> lower) & 1u; + return Value(x, data); +} + +std::uint64_t TexelOffset(const Format& format, std::uint32_t x) { + const auto mip = AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, format.format, Width, 1u, 1u).at(0); + return mip.tiledOffset + static_cast(x) * format.components * 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}; +} + +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, + IdentitySwizzle | (Type1D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +void Dispatch(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::span code, std::uint32_t format) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t index = 0; index < 4u; ++index) Input[tid * 4u + index] = Value(tid, index); + } + 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 = TextureDescriptor(texels, format); + 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 CheckTexels(const std::uint8_t* texels, const Format& format) { + const auto initial = Initial(); + for (std::uint32_t x = 0; x < Width; ++x) { + const auto offset = TexelOffset(format, x); + for (std::uint32_t component = 0; component < format.components; ++component) { + std::uint32_t actual = 0; + std::memcpy(&actual, texels + offset + component * 4u, 4u); + const auto wanted = SecondStore(x, component); + Require(actual == wanted, std::string("image_store 2d through a 1D ") + format.name + " T#: texel " + std::to_string(x) + " component " + std::to_string(component) + " is " + Hex(actual) + ", expected the y = 1 store with dmask 0x5 " + Hex(wanted)); + } + } + const auto end = TexelOffset(format, Width); + Require(std::equal(texels + end, texels + TexelBytes, initial.begin() + static_cast(end)), std::string("image_store 2d through a 1D ") + format.name + " T#: bytes past the texture changed"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock block; + auto* texels = block.Data(); + for (const auto& format : Formats) { + Dispatch(*device, texels, StoreCode, format.format); + CheckTexels(texels, format); + } + std::puts("image store line through plane 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/ImageStorePacked.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStorePacked.cpp index 0a3a058a5b..427ebc94b0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageStorePacked.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStorePacked.cpp @@ -58,7 +58,7 @@ public: block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); #endif Require(block != nullptr, "image store packed: cannot allocate the guest block"); - GuestAllocations::Mutation().Add(block, BlockBytes, true, true); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); } ~GuestBlock() { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreSint.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreSint.cpp index 8f8e1752bc..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}; @@ -76,7 +82,7 @@ public: block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); #endif Require(block != nullptr, "image store sint: cannot allocate the guest block"); - GuestAllocations::Mutation().Add(block, BlockBytes, true, true); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); } ~GuestBlock() { @@ -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/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/ImageVolumeAddressOnPlane.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageVolumeAddressOnPlane.cpp new file mode 100644 index 0000000000..a30fdcb5bf --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageVolumeAddressOnPlane.cpp @@ -0,0 +1,214 @@ +#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 Type2D = 9; +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 Plane{}; +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 ExplicitLodCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xbf8c3f70, 0xf0900f10, 0x00610c02, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xbf810000, +}; + +constexpr std::array PlaneR{0.0f, 0.5f, 1.0f, 1.5f, -0.25f, 3.0f, 0.25f, 0.75f}; +constexpr std::array PlaneZ{0u, 1u, 0u, 2u, 0xffffffffu, 4u, 0u, 3u}; + +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 = Bits((static_cast(lane.y) + 0.5f) / static_cast(Size)); + const auto r = volume ? Bits((static_cast(tid % Depth) + 0.5f) / static_cast(Depth)) : Bits(PlaneR[tid % PlaneR.size()]); + const auto z = volume ? tid % Depth : PlaneZ[tid % PlaneZ.size()]; + const std::array words{u, v, r, lane.x, lane.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 PlaneDescriptor() { + const auto address = static_cast(reinterpret_cast(Plane.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (((Size - 1u) & 3u) << 30u), + ((Size - 1u) >> 2u) | ((Size - 1u) << 14u), + 0xfacu | (Type2D << 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) { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(descriptor)); + Require(geometry.guestBytes <= storage.size(), "image volume address on a plane: 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 < Size; ++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 CheckPlane() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto lane = LaneOf(tid); + const std::array texel{SliceMarker(0), lane.x, lane.y, 255u}; + const auto z = PlaneZ[tid % PlaneZ.size()]; + Expect(tid, SampleResult, texel, "image_sample_lz 3d on a 2D texture with r " + std::to_string(PlaneR[tid % PlaneR.size()])); + Expect(tid, LoadResult, z == 0u ? texel : std::array{}, "image_load 3d on a 2D texture with 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::string refusal; + try { + Run(device, ExplicitLodCode, PlaneDescriptor()); + } catch (const std::exception& error) { + refusal = error.what(); + } + Require(refusal.find("incompatible with the static runtime image interface") != std::string::npos, "image_sample_l 3d on a 2D texture was not refused: " + refusal); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillTexture(Plane, PlaneDescriptor(), 1u); + FillInput(false); + Run(*device, Code, PlaneDescriptor()); + CheckPlane(); + FillTexture(Volume, VolumeDescriptor(), Depth); + FillInput(true); + Run(*device, Code, VolumeDescriptor()); + CheckVolume(); + RequireRefused(*device); + std::puts("image volume address on plane 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/ImageVolumeAddressPlanarTexture.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageVolumeAddressPlanarTexture.cpp new file mode 100644 index 0000000000..3e311f2047 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageVolumeAddressPlanarTexture.cpp @@ -0,0 +1,209 @@ +#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 Layers = 2; +constexpr std::uint32_t Format8888UNorm = 56; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t Type2DArray = 13; +constexpr std::uint32_t SampleResult = 8; +constexpr std::uint32_t LoadResult = 12; +constexpr std::uint32_t LoadMipResult = 16; +constexpr std::uint32_t ResinfoResult = 20; +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, 0xe0301014, 0x80000701, 0xe0301018, 0x80000801, 0xe030101c, + 0x80000901, 0xbf8c3f70, 0xf09c0f10, 0x00610c02, 0xf0001f10, 0x00011005, 0xf0041f10, 0x00011405, + 0xf0381f10, 0x00011809, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xe0701024, 0x80000d01, 0xe0701028, + 0x80000e01, 0xe070102c, 0x80000f01, 0xe0701030, 0x80001001, 0xe0701034, 0x80001101, 0xe0701038, + 0x80001201, 0xe070103c, 0x80001301, 0xe0701040, 0x80001401, 0xe0701044, 0x80001501, 0xe0701048, + 0x80001601, 0xe070104c, 0x80001701, 0xe0701050, 0x80001801, 0xe0701054, 0x80001901, 0xe0701058, + 0x80001a01, 0xe070105c, 0x80001b01, 0xbf810000, +}; + +alignas(256) constexpr std::array LodCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xbf8c3f70, + 0xf09c0f10, 0x00610c02, 0xf1800310, 0x00611002, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xe0701040, + 0x80001001, 0xbf810000, +}; + +struct Lane { + std::uint32_t x; + std::uint32_t y; + float r; + std::uint32_t z; + std::uint32_t sampledLayer; + bool loadInside; +}; + +constexpr std::array PlanarR{0.0f, 0.5f, 1.0f, 1.5f, -0.25f, 3.0f}; +constexpr std::array PlanarZ{0u, 1u, 0u, 4u, 0u, 0xffffffffu, 0u, 2u}; +constexpr std::array ArrayR{0.0f, 1.0f, 3.0f, -0.25f, 0.75f, 1.2f}; +constexpr std::array ArrayLayer{0u, 1u, 1u, 0u, 1u, 1u}; + +Lane LaneOf(std::uint32_t tid, bool array) { + const auto x = (tid * 7u + 3u) % Size; + const auto y = (tid * 5u + 1u) % Size; + if (array) return {x, y, ArrayR[tid % ArrayR.size()], tid % Layers, ArrayLayer[tid % ArrayLayer.size()], true}; + const auto z = PlanarZ[tid % PlanarZ.size()]; + return {x, y, PlanarR[tid % PlanarR.size()], z, 0u, z == 0u}; +} + +std::uint32_t Bits(float value) { + return std::bit_cast(value); +} + +std::uint8_t LayerMarker(std::uint32_t layer) { + return static_cast(layer * 100u + 7u); +} + +void FillInput(bool array) { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto lane = LaneOf(tid, array); + const std::array words{Bits((static_cast(lane.x) + 0.5f) / static_cast(Size)), Bits((static_cast(lane.y) + 0.5f) / static_cast(Size)), Bits(lane.r), lane.x, lane.y, lane.z, 0u, 0u}; + 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}; +} + +std::array TextureDescriptor(bool array) { + const auto address = static_cast(reinterpret_cast(Texels.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (((Size - 1u) & 3u) << 30u), + ((Size - 1u) >> 2u) | ((Size - 1u) << 14u), + 0xfacu | ((array ? Type2DArray : Type2D) << 28u), + array ? Layers - 1u : 0u, + 0u, + 0u, + 0u, + }; +} + +void FillTexture(bool array) { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(array))); + Require(geometry.guestBytes <= Texels.size(), "volume address on a planar texture: the texture does not fit the texel storage"); + const auto& mip = geometry.mips.at(0); + Texels.fill(0xeeu); + for (std::uint32_t layer = 0; layer < (array ? Layers : 1u); ++layer) { + for (std::uint32_t y = 0; y < Size; ++y) { + for (std::uint32_t x = 0; x < Size; ++x) { + auto* texel = &Texels[geometry.GuestLayerOffset(layer) + mip.tiledOffset + static_cast(y) * mip.pitchBytes + x * 4u]; + texel[0] = LayerMarker(layer); + texel[1] = static_cast(x); + texel[2] = static_cast(y); + texel[3] = 255u; + } + } + } +} + +std::array SamplerDescriptor() { + return {0u, 0xfffu << 12u, 1u << 26u, 0u}; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, bool array) { + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Buffer.data(), static_cast(Buffer.size() * 4u)); + const auto texture = TextureDescriptor(array); + 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 ExpectTexel(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 Check(bool array) { + const std::string texture = array ? "a 2D-array texture" : "a 2D texture"; + const std::array resinfo{Size, Size, array ? Layers : 1u, 1u}; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto lane = LaneOf(tid, array); + const std::array sampled{LayerMarker(lane.sampledLayer), lane.x, lane.y, 255u}; + const std::array loaded = lane.loadInside ? std::array{LayerMarker(lane.z), lane.x, lane.y, 255u} : std::array{0u, 0u, 0u, 0u}; + const auto z = std::to_string(static_cast(lane.z)); + ExpectTexel(tid, SampleResult, sampled, "image_sample_lz 3d on " + texture + " with r " + std::to_string(lane.r)); + ExpectTexel(tid, LoadResult, loaded, "image_load 3d on " + texture + " with z " + z); + ExpectTexel(tid, LoadMipResult, loaded, "image_load_mip 3d on " + texture + " with z " + z); + for (std::uint32_t component = 0; component < 4u; ++component) { + const auto value = Buffer[tid * Words + ResinfoResult + component]; + Require(value == resinfo[component], "image_get_resinfo 3d on " + texture + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + std::to_string(value) + ", expected " + std::to_string(resinfo[component])); + } + } +} + +void RequireLodRefused(AgcDriver::VulkanDevice& device, bool array) { + std::string refusal; + try { + Run(device, LodCode, array); + } catch (const std::exception& error) { + refusal = error.what(); + } + Require(refusal.find("incompatible with the static runtime image interface") != std::string::npos, std::string("image_get_lod 3d on a ") + (array ? "2D-array" : "2D") + " texture was not refused: " + refusal); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const bool array : {false, true}) { + FillInput(array); + FillTexture(array); + Run(*device, Code, array); + Check(array); + RequireLodRefused(*device, array); + } + std::puts("volume address on planar texture 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 3aaf8d19f0..4c3e66ac8f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageXorSwizzleVolume.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageXorSwizzleVolume.cpp @@ -27,8 +27,10 @@ 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; alignas(256) std::array Input{}; alignas(256) std::array Output{}; @@ -78,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) { @@ -85,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)); } @@ -144,7 +147,9 @@ 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"); std::puts("image XOR swizzle volume tests passed"); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/InactiveVertexPointSize.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/InactiveVertexPointSize.cpp new file mode 100644 index 0000000000..9cc96a457c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/InactiveVertexPointSize.cpp @@ -0,0 +1,132 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::byte Kept{0x40}; + +alignas(256) constexpr std::array VertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060280u, 0x7e0802f2u, 0x7e0a02ffu, 0x44800000u, 0xf80000d1u, 0x00000005u, 0xf80008cfu, 0x04030201u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u, +}; +alignas(256) constexpr std::array PixelCode{0x7e000200u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u}; +alignas(256) std::array ClearMemory{}; +alignas(256) std::array EnabledMemory{}; + +struct Header { + Shader shader{}; + ShaderUserData userData{}; +}; + +alignas(8) Header VertexHeader{}; +alignas(8) Header PixelHeader{}; + +void Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = static_cast(bytes); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.userData; + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +void Append(std::vector& commands, std::uint32_t opcode, std::initializer_list payload) { + commands.push_back(0xc0000000u | (static_cast(payload.size() - 1) << 16u) | (opcode << 8u)); + commands.insert(commands.end(), payload); +} + +std::vector DrawBoth() { + const auto vertex = reinterpret_cast(VertexCode.data()); + const auto pixel = reinterpret_cast(PixelCode.data()); + const std::vector> context{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u}, + {0x3b8, 0x9000000}, {0x1e0, 0}, + {0xc, 0}, {0xd, 0x40040}, + {0x81, 0x80000000}, {0x82, 0x40040}, + {0x90, 0x80000000}, {0x91, 0x40040}, + {0x94, 0x80000000}, {0x95, 0x40040}, + {0x10f, std::bit_cast(32.0f)}, {0x110, std::bit_cast(32.0f)}, + {0x111, std::bit_cast(-2.0f)}, {0x112, std::bit_cast(2.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, + {0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0}, + }; + const std::vector> shader{ + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 1u << 1u}, {0x8c, 0}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 1u << 1u}, {0x00c, std::bit_cast(1.0f)}, + }; + std::vector commands; + Append(commands, 0x79, {0x242, 4}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); + const auto draw = [&](std::array& memory, std::uint32_t exports) { + const auto color = reinterpret_cast(memory.data()); + Append(commands, 0x69, {0x318, static_cast(color >> 8u)}); + Append(commands, 0x69, {0x390, static_cast(color >> 40u)}); + Append(commands, 0x69, {0x207, exports}); + Append(commands, 0x2d, {3, 2}); + }; + draw(ClearMemory, 0x01200000); + draw(EnabledMemory, 0x01210000); + return commands; +} + +void InactivePointSizeDrawsLikeDisabled() { + ClearMemory.fill(Kept); + EnabledMemory.fill(Kept); + Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); + Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + auto commands = DrawBoth(); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + const auto kept = static_cast(std::count(ClearMemory.begin(), ClearMemory.end(), Kept)); + Require(kept != 0 && kept != ClearMemory.size(), "the triangle without USE_VTX_POINT_SIZE covered " + std::to_string(ClearMemory.size() - kept) + " of " + std::to_string(ClearMemory.size()) + " bytes, expected an edge through the target"); + for (std::size_t index = 0; index < ClearMemory.size(); ++index) { + Require(EnabledMemory[index] == ClearMemory[index], "byte " + std::to_string(index) + " is " + std::to_string(static_cast(EnabledMemory[index])) + " with USE_VTX_POINT_SIZE on a triangle draw and " + std::to_string(static_cast(ClearMemory[index])) + " without it"); + } +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + InactivePointSizeDrawsLikeDisabled(); + AgcDriverShutdown_nid_postfix(); + std::puts("USE_VTX_POINT_SIZE on a triangle draw drew the same pixels as the state without it"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/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/IndirectArgumentsWrittenAhead.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/IndirectArgumentsWrittenAhead.cpp new file mode 100644 index 0000000000..ad4c1f5a42 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/IndirectArgumentsWrittenAhead.cpp @@ -0,0 +1,99 @@ +#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; + 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(), {0xc0021102u, 1u, Low(Arguments.data()), High(Arguments.data())}); + words.insert(words.end(), {0xc0011600u, 0u, 0x8061u}); + 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"); + std::cout << "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/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/IndirectPatchedStage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/IndirectPatchedStage.cpp new file mode 100644 index 0000000000..7cf22540a2 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/IndirectPatchedStage.cpp @@ -0,0 +1,149 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::byte Kept{0x40}; +constexpr std::uint32_t BaseVertexLocation = 0x8c; +constexpr std::uint32_t Clipped = 0x40000000u; + +alignas(256) constexpr std::array VertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060208u, 0x7e0802f2u, 0x7e0a02ffu, 0x44800000u, 0xf80000d1u, 0x00000005u, 0xf80008cfu, 0x04030201u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u, +}; +alignas(256) constexpr std::array PixelCode{0x7e000200u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u}; +alignas(256) std::array DirectMemory{}; +alignas(256) std::array PatchedInMemory{}; +alignas(256) std::array PatchedOutMemory{}; +alignas(256) std::array Records{3, 1, 0, 0, 3, 1, Clipped, 0}; + +struct Header { + Shader shader{}; + ShaderUserData userData{}; +}; + +alignas(8) Header VertexHeader{}; +alignas(8) Header PixelHeader{}; + +void Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = static_cast(bytes); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.userData; + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +void Append(std::vector& commands, std::uint32_t opcode, std::initializer_list payload) { + commands.push_back(0xc0000000u | (static_cast(payload.size() - 1) << 16u) | (opcode << 8u)); + commands.insert(commands.end(), payload); +} + +std::vector DrawThree() { + const auto vertex = reinterpret_cast(VertexCode.data()); + const auto pixel = reinterpret_cast(PixelCode.data()); + const auto records = reinterpret_cast(Records.data()); + const std::vector> context{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x207, 0x01200000}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u}, + {0x3b8, 0x9000000}, {0x1e0, 0}, + {0xc, 0}, {0xd, 0x40040}, + {0x81, 0x80000000}, {0x82, 0x40040}, + {0x90, 0x80000000}, {0x91, 0x40040}, + {0x94, 0x80000000}, {0x95, 0x40040}, + {0x10f, std::bit_cast(32.0f)}, {0x110, std::bit_cast(32.0f)}, + {0x111, std::bit_cast(-2.0f)}, {0x112, std::bit_cast(2.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, + {0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0}, + }; + const std::vector> shader{ + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 1u << 1u}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 1u << 1u}, {0x00c, std::bit_cast(1.0f)}, + }; + std::vector commands; + Append(commands, 0x79, {0x242, 4}); + Append(commands, 0x79, {0x24a, 0}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); + Append(commands, 0x11, {1, static_cast(records), static_cast(records >> 32u)}); + const auto target = [&](std::array& memory, std::uint32_t userWord) { + const auto color = reinterpret_cast(memory.data()); + Append(commands, 0x69, {0x318, static_cast(color >> 8u)}); + Append(commands, 0x69, {0x390, static_cast(color >> 40u)}); + Append(commands, 0x76, {BaseVertexLocation, userWord}); + }; + target(DirectMemory, 0); + Append(commands, 0x2d, {3, 2}); + target(PatchedInMemory, Clipped); + Append(commands, 0x24, {0, BaseVertexLocation, 0, 2}); + target(PatchedOutMemory, 0); + Append(commands, 0x24, {16, BaseVertexLocation, 0, 2}); + return commands; +} + +std::size_t Covered(const std::array& memory) { + return static_cast(std::count_if(memory.begin(), memory.end(), [](std::byte value) { return value != Kept; })); +} + +void PatchedStagesDrawWithTheirRecordsWords() { + DirectMemory.fill(Kept); + PatchedInMemory.fill(Kept); + PatchedOutMemory.fill(Kept); + Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); + Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + auto commands = DrawThree(); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + const auto direct = Covered(DirectMemory); + Require(direct != 0 && direct != DirectMemory.size(), "the direct draw with depth 0 covered " + std::to_string(direct) + " of " + std::to_string(DirectMemory.size()) + " bytes, expected an edge through the target"); + for (std::size_t index = 0; index < DirectMemory.size(); ++index) { + Require(PatchedInMemory[index] == DirectMemory[index], "byte " + std::to_string(index) + " of the indirect draw whose record sets depth 0 over a clipped register value is " + std::to_string(static_cast(PatchedInMemory[index])) + ", the direct draw wrote " + std::to_string(static_cast(DirectMemory[index]))); + } + const auto out = Covered(PatchedOutMemory); + Require(out == 0, "the indirect draw whose record sets a clipped depth over a register value of 0 covered " + std::to_string(out) + " bytes, expected none"); +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + PatchedStagesDrawWithTheirRecordsWords(); + AgcDriverShutdown_nid_postfix(); + std::puts("CPU-path indirect draws drew with the user data word their records patched"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/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/IntegerToF32Rounding.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/IntegerToF32Rounding.cpp new file mode 100644 index 0000000000..3f6fd39854 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/IntegerToF32Rounding.cpp @@ -0,0 +1,130 @@ +#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 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, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e1402ffu, 0x5a5a5a5au, 0x7e1602ffu, + 0x5a5a5a5au, 0x7e140b04u, 0x7e160d05u, 0xe0781000u, 0x80010a02u, 0xbf810000u, +}; + +constexpr std::uint32_t Values[32] = { + 0x00000000u, 0x00000001u, 0x00000002u, 0x00000003u, 0x00000005u, 0x00000008u, 0x0000000fu, 0x00000010u, + 0x00000018u, 0x0000001fu, 0x00000020u, 0x00000021u, 0x0000003fu, 0x00007fffu, 0x00008000u, 0x0000ffffu, + 0x00010000u, 0x007fffffu, 0x00800000u, 0x00ffffffu, 0x01000000u, 0x12345678u, 0x7fffffffu, 0x80000000u, + 0x80000001u, 0xfffffffeu, 0xffffffffu, 0xdeadbeefu, 0x3f800000u, 0x00ff00ffu, 0xffff8000u, 0x0000807fu, +}; + +constexpr std::array, 32> Expected[3] = { + {{ + {{0x00000000u, 0x00000000u}}, {{0x3f800000u, 0x3f800000u}}, {{0x40000000u, 0x40000000u}}, {{0x40400000u, 0x40400000u}}, + {{0x40a00000u, 0x40a00000u}}, {{0x41000000u, 0x41000000u}}, {{0x41700000u, 0x41700000u}}, {{0x41800000u, 0x41800000u}}, + {{0x41c00000u, 0x41c00000u}}, {{0x41f80000u, 0x41f80000u}}, {{0x42000000u, 0x42000000u}}, {{0x42040000u, 0x42040000u}}, + {{0x427c0000u, 0x427c0000u}}, {{0x46fffe00u, 0x46fffe00u}}, {{0x47000000u, 0x47000000u}}, {{0x477fff00u, 0x477fff00u}}, + {{0x47800000u, 0x47800000u}}, {{0x4afffffeu, 0x4afffffeu}}, {{0x4b000000u, 0x4b000000u}}, {{0x4b7fffffu, 0x4b7fffffu}}, + {{0x4b800000u, 0x4b800000u}}, {{0x4d91a2b4u, 0x4d91a2b4u}}, {{0x4f000000u, 0x4f000000u}}, {{0xcf000000u, 0x4f000000u}}, + {{0xceffffffu, 0x4f000001u}}, {{0xc0000000u, 0x4f800000u}}, {{0xbf800000u, 0x4f800000u}}, {{0xce054904u, 0x4f5eadbfu}}, + {{0x4e7e0000u, 0x4e7e0000u}}, {{0x4b7f00ffu, 0x4b7f00ffu}}, {{0xc7000000u, 0x4f7fff80u}}, {{0x47007f00u, 0x47007f00u}}, + }}, + {{ + {{0x00000000u, 0x00000000u}}, {{0x3f800000u, 0x3f800000u}}, {{0x40000000u, 0x40000000u}}, {{0x40400000u, 0x40400000u}}, + {{0x40a00000u, 0x40a00000u}}, {{0x41000000u, 0x41000000u}}, {{0x41700000u, 0x41700000u}}, {{0x41800000u, 0x41800000u}}, + {{0x41c00000u, 0x41c00000u}}, {{0x41f80000u, 0x41f80000u}}, {{0x42000000u, 0x42000000u}}, {{0x42040000u, 0x42040000u}}, + {{0x427c0000u, 0x427c0000u}}, {{0x46fffe00u, 0x46fffe00u}}, {{0x47000000u, 0x47000000u}}, {{0x477fff00u, 0x477fff00u}}, + {{0x47800000u, 0x47800000u}}, {{0x4afffffeu, 0x4afffffeu}}, {{0x4b000000u, 0x4b000000u}}, {{0x4b7fffffu, 0x4b7fffffu}}, + {{0x4b800000u, 0x4b800000u}}, {{0x4d91a2b3u, 0x4d91a2b3u}}, {{0x4effffffu, 0x4effffffu}}, {{0xcf000000u, 0x4f000000u}}, + {{0xcf000000u, 0x4f000000u}}, {{0xc0000000u, 0x4f7fffffu}}, {{0xbf800000u, 0x4f7fffffu}}, {{0xce054905u, 0x4f5eadbeu}}, + {{0x4e7e0000u, 0x4e7e0000u}}, {{0x4b7f00ffu, 0x4b7f00ffu}}, {{0xc7000000u, 0x4f7fff80u}}, {{0x47007f00u, 0x47007f00u}}, + }}, + {{ + {{0x00000000u, 0x00000000u}}, {{0x3f800000u, 0x3f800000u}}, {{0x40000000u, 0x40000000u}}, {{0x40400000u, 0x40400000u}}, + {{0x40a00000u, 0x40a00000u}}, {{0x41000000u, 0x41000000u}}, {{0x41700000u, 0x41700000u}}, {{0x41800000u, 0x41800000u}}, + {{0x41c00000u, 0x41c00000u}}, {{0x41f80000u, 0x41f80000u}}, {{0x42000000u, 0x42000000u}}, {{0x42040000u, 0x42040000u}}, + {{0x427c0000u, 0x427c0000u}}, {{0x46fffe00u, 0x46fffe00u}}, {{0x47000000u, 0x47000000u}}, {{0x477fff00u, 0x477fff00u}}, + {{0x47800000u, 0x47800000u}}, {{0x4afffffeu, 0x4afffffeu}}, {{0x4b000000u, 0x4b000000u}}, {{0x4b7fffffu, 0x4b7fffffu}}, + {{0x4b800000u, 0x4b800000u}}, {{0x4d91a2b3u, 0x4d91a2b3u}}, {{0x4effffffu, 0x4effffffu}}, {{0xcf000000u, 0x4f000000u}}, + {{0xceffffffu, 0x4f000000u}}, {{0xc0000000u, 0x4f7fffffu}}, {{0xbf800000u, 0x4f7fffffu}}, {{0xce054904u, 0x4f5eadbeu}}, + {{0x4e7e0000u, 0x4e7e0000u}}, {{0x4b7f00ffu, 0x4b7f00ffu}}, {{0xc7000000u, 0x4f7fff80u}}, {{0x47007f00u, 0x47007f00u}}, + }}, +}; + +std::array BufferDescriptor(const void* data, std::uint32_t words) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), words * 4u, 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 rounding) { + Input.fill(0u); + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + Input[lane * Inputs] = Values[lane]; + Input[lane * Inputs + 1] = Values[lane]; + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size())); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size())); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(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.context.floatMode = ShaderRecompiler::ShaderFloatMode{0xf0u | (rounding << 2u) | rounding, true, true, false}; + 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 lane = 0; lane < Threads; ++lane) { + for (std::uint32_t column = 0; column < 2u; ++column) { + const auto expected = Expected[rounding - 1u][lane][column]; + const auto actual = Output[lane * Results + column]; + Require(actual == expected, "integer to f32 rounding: mode " + std::to_string(rounding) + " lane " + std::to_string(lane) + " column " + std::to_string(column) + " is " + Hex(actual) + ", expected " + Hex(expected)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (std::uint32_t rounding = 1u; rounding < 4u; ++rounding) { + Run(*device, rounding); + } + std::puts("integer to f32 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/InterpolationF16.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationF16.cpp index 6cfadd21b5..4d8dee9d60 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationF16.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationF16.cpp @@ -35,7 +35,7 @@ alignas(256) constexpr std::array VertexCode{ alignas(256) constexpr std::array PixelCode{ 0x7e080f02u, 0x7e0a0f03u, 0xd5430006u, 0x0411ff05u, 0x000000c0u, 0x7e300300u, 0x7e320301u, 0xd742001au, 0x00020000u, 0x7e3602ffu, 0xabcd0000u, 0xd75a001bu, 0x046a0200u, 0xd7420008u, 0x00020100u, 0x7e3802ffu, - 0x12340000u, 0xd75a001cu, 0x04220300u, 0x7e1e02ffu, 0x4500bc00u, 0xd743001du, 0x043e0140u, 0x7e3c0280u, + 0x00001234u, 0xd75a401cu, 0x04220300u, 0x7e1e02ffu, 0x4500bc00u, 0xd743001du, 0x043e0140u, 0x7e3c0280u, 0xd75a001eu, 0x04760340u, 0x7e3e0280u, 0xd75a801fu, 0x046a0200u, 0xd7420020u, 0x00020181u, 0xd7420021u, 0x000201c1u, 0xd7420022u, 0x40020000u, 0xd7430023u, 0x043e0000u, 0xe0782000u, 0x80011806u, 0xe0782010u, 0x80011c06u, 0xe0782020u, 0x80012006u, 0x7e0e02f2u, 0xf800180fu, 0x07070707u, 0xbf810000u, @@ -133,7 +133,7 @@ ShaderRecompiler::RecompileResult RecompilePixel(std::span {ShaderStage::Fragment, reinterpret_cast(code.data()), code, 0, {}}, {waveSize, 0, pixelUserData, std::nullopt, PixelStage(waveSize, input0), std::nullopt, pixelMemory}, target, - {0, 0, pushOffset, 128 - pushOffset} + PixelPushLayout(pushOffset, target) }; fragment.useCache = false; fragment.context.floatMode = ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}; @@ -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, vertexPush} - }}; + 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}; @@ -235,7 +239,7 @@ void Check(std::uint32_t waveSize) { result[1], std::bit_cast(lowX), 0xabcd0000u | HalfOf(SecondStep(Delta(LowX, 2), j, lowX)), - 0x12340000u | HalfOf(SecondStep(Delta(HighX, 2), j, highX)), + 0x1234u | (HalfOf(SecondStep(Delta(HighX, 2), j, highX)) << 16u), std::bit_cast(highY), HalfOf(SecondStep(Delta(HighY, 2), j, highY)), HalfOf(Saturate(SecondStep(Delta(LowX, 2), j, lowX))), @@ -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 new file mode 100644 index 0000000000..5581d46dcc --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeDraw.cpp @@ -0,0 +1,303 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::ColorTileMode; +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 48; +constexpr std::uint32_t Height = 16; +constexpr std::uint32_t Slices = 4; +constexpr std::uint32_t FirstSlice = 1; +constexpr std::uint32_t Layers = Slices - FirstSlice; +constexpr std::uint32_t BandWidth = Width / Layers; +constexpr std::uint8_t Kept = 0x40; +constexpr std::uint32_t LayerExport = (1u << 18u) | (1u << 21u); +constexpr std::uint32_t ShaderViewportIndexLayerCapability = 5254; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xe030201c, 0x80000405, 0xbf8c3f70, 0xf80000cf, 0x03020100, 0xf80008d4, 0x00040000, 0xbf810000, +}; + +alignas(256) constexpr std::array GeometryCode{ + 0x8f6a9003, 0x94fe6ac1, 0xbf88000b, 0xd7650006, 0x000100c1, 0xd7660006, 0x00020cc1, 0x93ebff03, + 0x00040018, 0xd7460006, 0x04190c6b, 0x340c0c82, 0xd8340000, 0x00000506, 0xbf8cc07f, 0xbefe04c1, + 0xbf8a0000, 0x938dff02, 0x00090016, 0x9382ff03, 0x00040018, 0x938cff03, 0x00080008, 0xd7650009, + 0x000100c1, 0xd7660009, 0x000212c1, 0xd746000a, 0x04250c02, 0x7da8120c, 0xbf88002e, 0x361600ff, + 0x0000ffff, 0x2c180090, 0x361a02ff, 0x0000ffff, 0xd8d80000, 0x0b00000b, 0xd8d80000, 0x0c00000c, + 0xd8d80000, 0x0d00000d, 0xbf8cc07f, 0xe0382000, 0x8002100b, 0xe0382010, 0x8002140b, 0xe0382000, + 0x8002180c, 0xe0382010, 0x80021c0c, 0xe0382000, 0x8002200d, 0xe0382010, 0x8002240d, 0x161c1483, + 0x161e14ff, 0x00000060, 0xbf8c3f70, 0xdb7c0400, 0x0000100f, 0xdb7c0410, 0x0000140f, 0xdb7c0420, + 0x0000180f, 0xdb7c0430, 0x00001c0f, 0xdb7c0440, 0x0000200f, 0xdb7c0450, 0x0000240f, 0x4a501c81, + 0x4a521c82, 0x3450508a, 0x34525294, 0xd772002a, 0x04a6510e, 0xbf8cc07f, 0xbefe04c1, 0xbf8a0000, + 0x930e830d, 0xbf078002, 0xbf850003, 0x8f0f8c0d, 0x887c0f0e, 0xbf900009, 0x7da8140d, 0xbf880002, + 0xf8000941, 0x0000002a, 0xbefe04c1, 0x7da8140e, 0xbf88000c, 0x34561485, 0xdbfc0400, 0x2c00002b, + 0xdbfc0410, 0x3000002b, 0xbf8cc07f, 0xf80000cf, 0x2f2e2d2c, 0xf80008d4, 0x00330000, 0xf800020f, + 0x33323130, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; + +alignas(256) constexpr std::array LayerFieldPixelCode{ + 0x2c020090, 0x7e020d01, 0x100202f0, 0xf800180f, 0x01010101, 0xbf810000, +}; + +constexpr std::array WhiteBands{255, 255, 255}; +constexpr std::array LayerFieldBands{0, 128, 255}; + +struct Vertex { + std::array position; + std::array color; + std::uint32_t layer; +}; + +alignas(256) std::array Vertices{}; + +constexpr ShaderRecompiler::MeshConfiguration Subgroup{4u, 4u, 12u, 12u, 4u, 64u, 1024u, 0u, 4u}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +void BuildBands() { + for (std::uint32_t layer = 0; layer < Layers; ++layer) { + const float left = -1.0f + 2.0f * static_cast(layer) / Layers; + const float right = -1.0f + 2.0f * static_cast(layer + 1u) / Layers; + const std::array, 6> corners{{{left, -1.0f}, {right, -1.0f}, {left, 1.0f}, {right, -1.0f}, {right, 1.0f}, {left, 1.0f}}}; + for (std::uint32_t k = 0; k < corners.size(); ++k) Vertices[6u * layer + k] = {{corners[k][0], corners[k][1], 0.5f, 1.0f}, {1.0f, 1.0f, 1.0f}, layer}; + } +} + +AgcDriver::Graphics::ColorTarget VolumeTarget(std::byte* volume, ColorTileMode tileMode = ColorTileMode::RenderTarget) { + AgcDriver::Graphics::ColorTarget color{reinterpret_cast(volume), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, 0, 0xe4u}; + color.tileMode = tileMode; + color.depth = Slices; + color.depthSlice = FirstSlice; + color.layers = Layers; + color.bytes = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::SurfaceForTarget(color)).guestBytes; + return color; +} + +class GuestVolume { +public: + explicit GuestVolume(std::size_t bytes) { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(65536, bytes)); +#endif + Require(block != nullptr, "cannot allocate the volume"); + GuestAllocations::Mutation().Add(block, bytes, true, true, true); + std::memset(block, Kept, bytes); + } + + ~GuestVolume() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestVolume(const GuestVolume&) = delete; + GuestVolume& operator=(const GuestVolume&) = delete; + std::byte* Data() const { return block; } + +private: + std::byte* block = nullptr; +}; + +bool DeclaresLayer(std::span words) { + for (std::size_t at = 5; at < words.size();) { + const auto length = words[at] >> 16u; + const auto opcode = words[at] & 0xffffu; + if (length == 0 || at + length > words.size()) break; + if (opcode == 71u && length == 4 && words[at + 2] == 11u && words[at + 3] == 9u) return true; + if (opcode == 72u && length == 5 && words[at + 3] == 11u && words[at + 4] == 9u) return true; + at += length; + } + return false; +} + +void Run(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, bool mesh, std::span pixelCode, bool layerField) { + const auto target = device.Target(); + const auto vertexBuffer = BufferDescriptor(Vertices.data(), sizeof(Vertex), static_cast(Vertices.size())); + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Vertices.size()), 0, 1, 0, false}; + ShaderRecompiler::ShaderVertexStageInfo vertexInfo{}; + vertexInfo.paClVsOutCntl = LayerExport; + std::vector userData(mesh ? 12u : 4u, 0u); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), userData.begin() + (mesh ? 8 : 0)); + const std::span code = mesh ? std::span(GeometryCode) : std::span(VertexCode); + const auto stage = mesh ? ShaderStage::Mesh : ShaderStage::Vertex; + const std::array geometryMemory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + ShaderRecompiler::RecompileRequest geometry{ + {stage, reinterpret_cast(code.data()), code, 0, {}}, + {64, 0, userData, std::nullopt, std::nullopt, vertexInfo, geometryMemory}, + target, + {0, 0, 0, mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : 64u} + }; + if (mesh) geometry.graphics = ShaderRecompiler::GraphicsCompileContext{0, {}, Subgroup, std::nullopt, {draw.indexAddress, draw.indexCount, draw.indexSize, draw.instanceCount}}; + geometry.useCache = false; + const auto geometryResult = ShaderRecompiler::Recompile(geometry); + const auto geometryPush = static_cast(geometryResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + if (layerField) { + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary); + pixel.ancillary = true; + pixel.layerExported = true; + } + const std::vector pixelUserData(8, 0u); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {64, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(geometryPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {stage, &geometryResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(geometryPush, target)} + }}; + + AgcDriver::Graphics::State state{}; + if (mesh) state.stages = {AgcDriver::Graphics::ShaderPath::Geometry, 0x20u, 64, 64, Subgroup, std::nullopt}; + else state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 64, 64, std::nullopt, std::nullopt}; + state.color = color; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.renderLayers = Layers; + state.layerExports = LayerExport; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + if (mesh) { + Require(DeclaresLayer(geometryResult.spirv), "the mesh program declares no Layer output"); + VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; + subgroup.subgroupSize = target.subgroupSize; + subgroup.supportedStages = VK_SHADER_STAGE_ALL; + subgroup.supportedOperations = 0xffu; + subgroup.quadOperationsInAllStages = VK_TRUE; + AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, false, false, true); + return; + } + device.Draw(state, draw, shaders); + device.WaitIdle(); + AgcDriver::Graphics::StorageTexture::FlushAllPending("layered volume draw test"); + device.WaitIdle(); +} + +std::uint64_t BlockOffset(const AgcDriver::Graphics::TextureSwizzleEquation& equation, std::uint32_t x, std::uint32_t y, std::uint32_t z) { + std::uint64_t offset = 0; + for (std::uint32_t bit = 0; bit < equation.bits.size(); ++bit) { + const auto mask = equation.bits[bit]; + const auto selected = (x & (mask & 0xfffu)) ^ ((y << 12u) & (mask & 0xfff000u)) ^ ((z << 24u) & (mask & 0xff000000u)); + offset |= static_cast(std::popcount(selected) & 1u) << bit; + } + return offset; +} + +void Check(const std::byte* volume, const AgcDriver::Graphics::ColorTarget& color, const std::string& what, const std::array& bands) { + using namespace AgcDriver::Graphics; + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::SurfaceForTarget(color)); + 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 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)")); + } + } + } + } +} + +} + +int main(int argc, char** argv) { + try { + const bool vertexOnly = argc == 2 && std::strcmp(argv[1], "--vertex-only") == 0; + Require(argc == 1 || vertexOnly, "usage: layered volume draw [--vertex-only]"); + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto target = device->Target(); + if (std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), ShaderViewportIndexLayerCapability) == target.supportedCapabilities.end()) { + std::puts("skipped, the device has no VK_EXT_shader_viewport_index_layer"); + return VulkanTestSkipped; + } + BuildBands(); + const GuestVolume volume((VolumeTarget(nullptr).bytes + 65535u) & ~std::size_t{65535u}); + const auto color = VolumeTarget(volume.Data()); + Run(*device, color, false, PixelCode, false); + Check(volume.Data(), color, "vertex draw", WhiteBands); + const GuestVolume fieldVolume((VolumeTarget(nullptr).bytes + 65535u) & ~std::size_t{65535u}); + const auto fieldColor = VolumeTarget(fieldVolume.Data()); + Run(*device, fieldColor, false, LayerFieldPixelCode, true); + Check(fieldVolume.Data(), fieldColor, "a pixel shader reading the render target index of its packed ancillary word", LayerFieldBands); + 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"); + std::puts("layered volume draw tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeSubmit.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeSubmit.cpp new file mode 100644 index 0000000000..7c66f54f0e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeSubmit.cpp @@ -0,0 +1,287 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::ColorTileMode; +using AgcDriver::Graphics::Require; + +constexpr std::uint32_t Width = 48; +constexpr std::uint32_t Height = 16; +constexpr std::uint32_t Slices = 4; +constexpr std::uint32_t FirstSlice = 1; +constexpr std::uint32_t Layers = Slices - FirstSlice; +constexpr std::uint32_t BandWidth = Width / Layers; +constexpr std::uint8_t Kept = 0x40; +constexpr std::uint32_t LayerExport = (1u << 18u) | (1u << 21u); +constexpr std::uint32_t ShaderViewportIndexLayerCapability = 5254; +constexpr std::uint32_t SetContextReg = 0x69u; +constexpr std::uint32_t SetShReg = 0x76u; +constexpr std::uint32_t SetUconfigReg = 0x79u; +constexpr std::uint32_t DrawIndexAuto = 0x2du; +constexpr std::uint32_t RenderTargetVolumeAttrib3 = (Slices - 1u) | (0x1bu << 14u) | (2u << 24u) | (1u << 27u); +constexpr std::size_t Block = 65536; + +alignas(64) volatile std::uint32_t EndOfPipe = 0; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80020005, 0xe030201c, 0x80020405, 0xbf8c3f70, 0xf80000cf, 0x03020100, 0xf80008d4, 0x00040000, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; + +struct Vertex { + std::array position; + std::array color; + std::uint32_t layer; +}; + +alignas(256) std::array Vertices{}; + +struct RegisteredShader { + Shader shader; + ShaderUserData userData; +}; +RegisteredShader VertexShader{}; +RegisteredShader PixelShader{}; + +const std::vector> ContextBaseline{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31d, 0}, {0x1e0, 0}, + {0xc, 0}, + {0x81, 0x80000000}, {0x90, 0x80000000}, {0x94, 0x80000000}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, {0x114, 0}, {0x113, std::bit_cast(1.0f)}, +}; + +std::byte* AllocateWatched(std::size_t bytes) { +#ifdef _WIN32 + void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block); + GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); +#else + void* raw = mmap(nullptr, bytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (raw == MAP_FAILED) throw std::runtime_error("cannot map the watched volume"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + Block - 1) & ~static_cast(Block - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + Block) munmap(reinterpret_cast(aligned + bytes), begin + Block - aligned); + void* block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes); +#endif + GuestAllocations::Mutation().Add(block, bytes, true, true, true); + return static_cast(block); +} + +void Register(RegisteredShader& registered, const void* code, std::uint32_t bytes, std::uint8_t type) { + registered.shader.file_header = 0x34333231u; + registered.shader.version = 0x18u; + registered.shader.user_data = ®istered.userData; + registered.shader.code = code; + registered.shader.header_size = sizeof(RegisteredShader); + registered.shader.shader_size = bytes; + registered.shader.type = type; + AgcDriverRegisterShader_nid_postfix(®istered.shader); +} + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +void SetRegister(std::vector& words, std::uint32_t opcode, std::uint32_t offset, std::uint32_t value) { + words.push_back(0xc0010000u | (opcode << 8u)); + words.push_back(offset); + words.push_back(value); +} + +void BuildBands() { + for (std::uint32_t layer = 0; layer < Layers; ++layer) { + const float left = -1.0f + 2.0f * static_cast(layer) / Layers; + const float right = -1.0f + 2.0f * static_cast(layer + 1u) / Layers; + const std::array, 6> corners{{{left, -1.0f}, {right, -1.0f}, {left, 1.0f}, {right, -1.0f}, {right, 1.0f}, {left, 1.0f}}}; + for (std::uint32_t k = 0; k < corners.size(); ++k) Vertices[6u * layer + k] = {{corners[k][0], corners[k][1], 0.5f, 1.0f}, {1.0f, 1.0f, 1.0f}, layer}; + } +} + +AgcDriver::Graphics::ColorTarget VolumeTarget(std::byte* volume) { + AgcDriver::Graphics::ColorTarget color{reinterpret_cast(volume), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, 0, 0xe4u}; + color.tileMode = ColorTileMode::RenderTarget; + color.elementBytes = 4; + color.depth = Slices; + color.depthSlice = FirstSlice; + color.layers = Layers; + color.bytes = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::SurfaceForTarget(color)).guestBytes; + return color; +} + +std::vector Frame(const std::byte* volume, const std::byte* keys) { + const auto colorAddress = reinterpret_cast(volume); + const auto vertexAddress = reinterpret_cast(VertexCode.data()); + const auto pixelAddress = reinterpret_cast(PixelCode.data()); + std::vector words; + for (const auto& [offset, value] : ContextBaseline) SetRegister(words, SetContextReg, offset, value); + SetRegister(words, SetContextReg, 0x207, LayerExport); + SetRegister(words, SetContextReg, 0x318, static_cast(colorAddress >> 8u)); + SetRegister(words, SetContextReg, 0x390, static_cast(colorAddress >> 40u)); + SetRegister(words, SetContextReg, 0x31b, FirstSlice | ((Slices - 1u) << 13u)); + SetRegister(words, SetContextReg, 0x3b0, ((Width - 1u) << 14u) | (Height - 1u)); + SetRegister(words, SetContextReg, 0x3b8, RenderTargetVolumeAttrib3); + if (keys != nullptr) { + const auto keyAddress = reinterpret_cast(keys); + SetRegister(words, SetContextReg, 0x31c, 0x28028u | 0x10000000u); + SetRegister(words, SetContextReg, 0x325, static_cast(keyAddress >> 8u)); + SetRegister(words, SetContextReg, 0x3a8, static_cast(keyAddress >> 40u)); + } + SetRegister(words, SetContextReg, 0x10f, std::bit_cast(Width / 2.0f)); + SetRegister(words, SetContextReg, 0x110, std::bit_cast(Width / 2.0f)); + SetRegister(words, SetContextReg, 0x111, std::bit_cast(-(Height / 2.0f))); + SetRegister(words, SetContextReg, 0x112, std::bit_cast(Height / 2.0f)); + const auto extent = (Height << 16u) | Width; + SetRegister(words, SetContextReg, 0xd, extent); + SetRegister(words, SetContextReg, 0x82, extent); + SetRegister(words, SetContextReg, 0x91, extent); + SetRegister(words, SetContextReg, 0x95, extent); + SetRegister(words, SetUconfigReg, 0x242, 4u); + SetRegister(words, SetShReg, 0x008, static_cast(pixelAddress >> 8u)); + SetRegister(words, SetShReg, 0x009, static_cast(pixelAddress >> 40u)); + SetRegister(words, SetShReg, 0x00b, 0u); + SetRegister(words, SetShReg, 0x0c8, static_cast(vertexAddress >> 8u)); + SetRegister(words, SetShReg, 0x0c9, static_cast(vertexAddress >> 40u)); + SetRegister(words, SetShReg, 0x08b, 4u << 1u); + const auto descriptor = BufferDescriptor(Vertices.data(), sizeof(Vertex), static_cast(Vertices.size())); + for (std::uint32_t i = 0; i < descriptor.size(); ++i) SetRegister(words, SetShReg, 0x08c + i, descriptor[i]); + words.insert(words.end(), {0xc0000000u | (1u << 16u) | (DrawIndexAuto << 8u), static_cast(Vertices.size()), 2u}); + const auto labelAddress = reinterpret_cast(&EndOfPipe); + words.insert(words.end(), {0xc0064900u, 0x514u, (1u << 29u) | (2u << 24u), static_cast(labelAddress), static_cast(static_cast(labelAddress) >> 32u), 1u, 0u, 0u}); + return words; +} + +std::uint64_t BlockOffset(const AgcDriver::Graphics::TextureSwizzleEquation& equation, std::uint32_t x, std::uint32_t y, std::uint32_t z) { + std::uint64_t offset = 0; + for (std::uint32_t bit = 0; bit < equation.bits.size(); ++bit) { + const auto mask = equation.bits[bit]; + const auto selected = (x & (mask & 0xfffu)) ^ ((y << 12u) & (mask & 0xfff000u)) ^ ((z << 24u) & (mask & 0xff000000u)); + offset |= static_cast(std::popcount(selected) & 1u) << bit; + } + return offset; +} + +void Check(const std::vector& volume, const AgcDriver::Graphics::ColorTarget& color, std::uint8_t undrawn, const std::string& what) { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::SurfaceForTarget(color)); + Require(!geometry.thick, "a 64 KiB render-target volume is not stored as thin slices"); + const auto* equation = AgcDriver::Graphics::FindTextureSwizzleEquation(static_cast(ColorTileMode::RenderTarget), 4); + Require(equation != nullptr, "missing the 64 KiB R_X swizzle equation"); + for (std::uint32_t slice = 0; slice < Slices; ++slice) { + const auto* base = volume.data() + geometry.GuestLayerOffset(slice); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const bool drawn = slice >= FirstSlice && x / BandWidth == slice - FirstSlice; + const auto expected = drawn ? std::uint8_t{255} : undrawn; + const auto* texel = base + BlockOffset(*equation, x, y, slice); + for (std::uint32_t channel = 0; channel < 4; ++channel) { + const auto actual = std::to_integer(texel[channel]); + Require(actual == expected, what + ": slice " + std::to_string(slice) + " texel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(actual) + ", expected " + std::to_string(expected) + (drawn ? " (the band of layer " + std::to_string(slice - FirstSlice) + ")" : slice < FirstSlice ? " (outside the color view)" : " (another layer's band)")); + } + } + } + } +} + +bool DeviceRoutesLayers() { + const auto device = OpenVulkanTestDevice(); + if (!device) return false; + const auto target = device->Target(); + return std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), ShaderViewportIndexLayerCapability) != target.supportedCapabilities.end(); +} + +void Run(const char* what, std::optional key) { + const auto bytes = (VolumeTarget(nullptr).bytes + Block - 1u) & ~(Block - 1u); + auto* volume = AllocateWatched(bytes); + std::memset(volume, static_cast(Kept), bytes); + const auto color = VolumeTarget(volume); + const auto keyBytes = static_cast(color.bytes / 256u); + std::byte* keys = nullptr; + if (key) { + keys = AllocateWatched(Block); + std::memset(keys, *key, keyBytes); + } + EndOfPipe = 0; + auto words = Frame(volume, keys); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, std::string(what) + ": the DCB submission failed"); + AgcDriverWaitIdle_nid_postfix(); + Require(EndOfPipe == 1u, std::string(what) + ": the end-of-pipe label was not written"); + std::vector stored(bytes); + AgcDriver::GuestMemory::Read(static_cast(reinterpret_cast(volume)), stored); + Check(stored, color, key == std::uint8_t{0x00} ? std::uint8_t{0} : Kept, what); + if (keys != nullptr) { + std::vector storedKeys(keyBytes); + AgcDriver::GuestMemory::Read(static_cast(reinterpret_cast(keys)), storedKeys); + const auto uncompressed = static_cast(std::count(storedKeys.begin(), storedKeys.end(), std::byte{0xff})); + Require(uncompressed == keyBytes, std::string(what) + ": " + std::to_string(uncompressed) + " of " + std::to_string(keyBytes) + " DCC key bytes read uncompressed after the draw"); + } +} + +} + +int main() { + try { + if (!DeviceRoutesLayers()) { + std::puts("skipped, no Vulkan device with VK_EXT_shader_viewport_index_layer"); + return VulkanTestSkipped; + } + BuildBands(); + Register(VertexShader, VertexCode.data(), sizeof(VertexCode), 2); + Register(PixelShader, PixelCode.data(), sizeof(PixelCode), 1); + Run("layered volume draw through the driver", std::nullopt); + Run("layered volume draw through the driver under uncompressed DCC keys", std::uint8_t{0xff}); + Run("layered volume draw through the driver under 0000 fast-clear DCC keys", std::uint8_t{0x00}); + AgcDriverShutdown_nid_postfix(); + std::puts("layered volume draws through the driver passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LdexpF16InlineConstants.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LdexpF16InlineConstants.cpp new file mode 100644 index 0000000000..fd14f4ee2c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LdexpF16InlineConstants.cpp @@ -0,0 +1,139 @@ +#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 Results = 16; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array LdexpCode{ + 0x34060084, 0xe0302000, 0x80000400, 0xbe8803f0, 0x7e0a02ff, 0x00003800, 0xbf8c3f70, 0xd53b000a, + 0x0201e104, 0xd53b000b, 0x0201e304, 0xd53b000c, 0x0201e504, 0xd53b000d, 0x0201e704, 0xd53b000e, + 0x0201e904, 0xd53b000f, 0x0201eb04, 0xd53b0010, 0x0201ed04, 0xd53b0011, 0x0201ef04, 0xd53b0012, + 0x0201f104, 0xd53b0013, 0x02010504, 0xd53b0014, 0x02018304, 0xd53b0015, 0x02018104, 0xd53b0016, + 0x0201a104, 0xd53b0017, 0x0201ff04, 0x3f000000, 0xd53b0018, 0x02001104, 0xd53b0019, 0x02020b04, + 0xe0702000, 0x80010a03, 0xe0702004, 0x80010b03, 0xe0702008, 0x80010c03, 0xe070200c, 0x80010d03, + 0xe0702010, 0x80010e03, 0xe0702014, 0x80010f03, 0xe0702018, 0x80011003, 0xe070201c, 0x80011103, + 0xe0702020, 0x80011203, 0xe0702024, 0x80011303, 0xe0702028, 0x80011403, 0xe070202c, 0x80011503, + 0xe0702030, 0x80011603, 0xe0702034, 0x80011703, 0xe0702038, 0x80011803, 0xe070203c, 0x80011903, + 0xbf810000, +}; + +struct Row { + std::uint32_t half; + std::array expected; +}; + +constexpr std::array Rows{{ + {0x3c00u, {0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x4400u, 0x3800u, 0x7c00u, 0x0100u, 0x3c00u, 0x3c00u, 0x7c00u}}, + {0x3555u, {0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x3d55u, 0x3155u, 0x7c00u, 0x0055u, 0x3555u, 0x3555u, 0x7c00u}}, + {0x4248u, {0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x4a48u, 0x3e48u, 0x7c00u, 0x0324u, 0x4248u, 0x4248u, 0x7c00u}}, + {0x7bffu, {0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x7c00u, 0x77ffu, 0x7c00u, 0x3bffu, 0x7bffu, 0x7bffu, 0x7c00u}}, + {0x0400u, {0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0c00u, 0x0200u, 0x7c00u, 0x0000u, 0x0400u, 0x0400u, 0x7c00u}}, + {0x2e66u, {0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x3666u, 0x2a66u, 0x7c00u, 0x001au, 0x2e66u, 0x2e66u, 0x7c00u}}, + {0x5640u, {0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x5e40u, 0x5240u, 0x7c00u, 0x1640u, 0x5640u, 0x5640u, 0x7c00u}}, + {0x3800u, {0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x4000u, 0x3400u, 0x7c00u, 0x0080u, 0x3800u, 0x3800u, 0x7c00u}}, + {0x07ffu, {0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0fffu, 0x0400u, 0x7c00u, 0x0000u, 0x07ffu, 0x07ffu, 0x7c00u}}, + {0x0800u, {0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x1000u, 0x0400u, 0x7c00u, 0x0000u, 0x0800u, 0x0800u, 0x7c00u}}, + {0xbc00u, {0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0xc400u, 0xb800u, 0xfc00u, 0x8100u, 0xbc00u, 0xbc00u, 0xfc00u}}, + {0xb555u, {0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0xbd55u, 0xb155u, 0xfc00u, 0x8055u, 0xb555u, 0xb555u, 0xfc00u}}, + {0xc248u, {0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0xca48u, 0xbe48u, 0xfc00u, 0x8324u, 0xc248u, 0xc248u, 0xfc00u}}, + {0xfbffu, {0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0xfc00u, 0xf7ffu, 0xfc00u, 0xbbffu, 0xfbffu, 0xfbffu, 0xfc00u}}, + {0x8400u, {0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0x8c00u, 0x8200u, 0xfc00u, 0x8000u, 0x8400u, 0x8400u, 0xfc00u}}, + {0xae66u, {0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0xb666u, 0xaa66u, 0xfc00u, 0x801au, 0xae66u, 0xae66u, 0xfc00u}}, + {0xd640u, {0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0xde40u, 0xd240u, 0xfc00u, 0x9640u, 0xd640u, 0xd640u, 0xfc00u}}, + {0x8800u, {0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0x9000u, 0x8400u, 0xfc00u, 0x8000u, 0x8800u, 0x8800u, 0xfc00u}}, + {0x0001u, {0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0004u, 0x0000u, 0x7c00u, 0x0000u, 0x0001u, 0x0001u, 0x7c00u}}, + {0x0002u, {0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0008u, 0x0001u, 0x7c00u, 0x0000u, 0x0002u, 0x0002u, 0x7c00u}}, + {0x0003u, {0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x000cu, 0x0002u, 0x7c00u, 0x0000u, 0x0003u, 0x0003u, 0x7c00u}}, + {0x0100u, {0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0400u, 0x0080u, 0x7c00u, 0x0000u, 0x0100u, 0x0100u, 0x7c00u}}, + {0x0155u, {0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0554u, 0x00aau, 0x7c00u, 0x0000u, 0x0155u, 0x0155u, 0x7c00u}}, + {0x0200u, {0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0800u, 0x0100u, 0x7c00u, 0x0000u, 0x0200u, 0x0200u, 0x7c00u}}, + {0x03ffu, {0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0000u, 0x7c00u, 0x0bfeu, 0x0200u, 0x7c00u, 0x0000u, 0x03ffu, 0x03ffu, 0x7c00u}}, + {0x8001u, {0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0x8004u, 0x8000u, 0xfc00u, 0x8000u, 0x8001u, 0x8001u, 0xfc00u}}, + {0x8003u, {0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0x800cu, 0x8002u, 0xfc00u, 0x8000u, 0x8003u, 0x8003u, 0xfc00u}}, + {0x8155u, {0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0x8554u, 0x80aau, 0xfc00u, 0x8000u, 0x8155u, 0x8155u, 0xfc00u}}, + {0x8200u, {0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0x8800u, 0x8100u, 0xfc00u, 0x8000u, 0x8200u, 0x8200u, 0xfc00u}}, + {0x83ffu, {0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0x8000u, 0xfc00u, 0x8bfeu, 0x8200u, 0xfc00u, 0x8000u, 0x83ffu, 0x83ffu, 0xfc00u}}, + {0x0000u, {0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0000u}}, + {0x8000u, {0x8000u, 0x8000u, 0x8000u, 0x8000u, 0x8000u, 0x8000u, 0x8000u, 0x8000u, 0x8000u, 0x8000u, 0x8000u, 0x8000u, 0x8000u, 0x8000u, 0x8000u, 0x8000u}}, +}}; + +constexpr std::array Names{ + "0.5", "-0.5", "1.0", "-1.0", "2.0", "-2.0", "4.0", "-4.0", "0.15915494", "2", "-1", "64", "-16", "literal 0x3f000000", "s8 = 0x3f000000", "v5 = 0x3800", +}; + +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) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) Input[tid] = Rows[tid].half; + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size())); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size())); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(LdexpCode); + 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 = ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, 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%04x", value); + return text; +} + +void Check() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto& row = Rows[tid]; + for (std::uint32_t column = 0; column < Results; ++column) { + const auto actual = Output[tid * Results + column] & 0xffffu; + Require(actual == row.expected[column], std::string("ldexp f16 inline constants: v_ldexp_f16 ") + Hex(row.half) + ", " + Names[column] + " is " + Hex(actual) + ", expected " + Hex(row.expected[column])); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device); + Check(); + std::puts("ldexp f16 inline constants 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/LdexpF32Denormal.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LdexpF32Denormal.cpp new file mode 100644 index 0000000000..fabe123469 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LdexpF32Denormal.cpp @@ -0,0 +1,163 @@ +#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; +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 kept; + std::uint32_t keptIeee; + std::uint32_t flushed; + std::uint32_t keptTowardPositive; + std::uint32_t keptTowardNegative; + std::uint32_t keptTowardZero; + std::uint32_t flushedTowardPositive; + std::uint32_t flushedTowardNegative; + std::uint32_t flushedTowardZero; +}; + +constexpr std::array Vectors{{ + {0x00000001u, 0x00000003u, 0x00000008u, 0x00000008u, 0x00000000u, 0x00000008u, 0x00000008u, 0x00000008u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000001u, 0x00000016u, 0x00400000u, 0x00400000u, 0x00000000u, 0x00400000u, 0x00400000u, 0x00400000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x007fffffu, 0xffffffe9u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x007fffffu, 0xfffffffdu, 0x00100000u, 0x00100000u, 0x00000000u, 0x00100000u, 0x000fffffu, 0x000fffffu, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000001u, 0x00000019u, 0x01800000u, 0x01800000u, 0x00000000u, 0x01800000u, 0x01800000u, 0x01800000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000001u, 0x0000007du, 0x33800000u, 0x33800000u, 0x00000000u, 0x33800000u, 0x33800000u, 0x33800000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000001u, 0x000003e8u, 0x7f800000u, 0x7f800000u, 0x00000000u, 0x7f800000u, 0x7f7fffffu, 0x7f7fffffu, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x80000001u, 0x000003e8u, 0xff800000u, 0xff800000u, 0x80000000u, 0xff7fffffu, 0xff800000u, 0xff7fffffu, 0x80000000u, 0x80000000u, 0x80000000u}, + {0x80000001u, 0x00000003u, 0x80000008u, 0x80000008u, 0x80000000u, 0x80000008u, 0x80000008u, 0x80000008u, 0x80000000u, 0x80000000u, 0x80000000u}, + {0x3f800000u, 0xffffff6cu, 0x00000002u, 0x00000002u, 0x00000000u, 0x00000002u, 0x00000002u, 0x00000002u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x3f000000u, 0xffffff6cu, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x3f000000u, 0xffffff82u, 0x00400000u, 0x00400000u, 0x00000000u, 0x00400000u, 0x00400000u, 0x00400000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0xbf000000u, 0xffffff6cu, 0x80000001u, 0x80000001u, 0x80000000u, 0x80000001u, 0x80000001u, 0x80000001u, 0x80000000u, 0x80000000u, 0x80000000u}, + {0x00800000u, 0xffffffe8u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x3eaaaaabu, 0xffffff83u, 0x00555556u, 0x00555556u, 0x00000000u, 0x00555556u, 0x00555555u, 0x00555555u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x80800000u, 0xffffffe8u, 0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x80000001u, 0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u}, + {0x3f7fffffu, 0xffffff82u, 0x00800000u, 0x00800000u, 0x00000000u, 0x00800000u, 0x007fffffu, 0x007fffffu, 0x00000000u, 0x00000000u, 0x00000000u}, + {0xbfc00000u, 0xffffff6au, 0x80000001u, 0x80000001u, 0x80000000u, 0x80000000u, 0x80000001u, 0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u}, + {0x3f7fffffu, 0xffffff6cu, 0x00000002u, 0x00000002u, 0x00000000u, 0x00000002u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x3fc00000u, 0xffffff6cu, 0x00000003u, 0x00000003u, 0x00000000u, 0x00000003u, 0x00000003u, 0x00000003u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000003u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x80000000u, 0x00000003u, 0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u}, + {0x7f800000u, 0x00000003u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u}, + {0xff800000u, 0xfffffffdu, 0xff800000u, 0xff800000u, 0xff800000u, 0xff800000u, 0xff800000u, 0xff800000u, 0xff800000u, 0xff800000u, 0xff800000u}, + {0x7fc00000u, 0x00000003u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u}, + {0x7fa00000u, 0x00000003u, 0x7fa00000u, 0x7fe00000u, 0x7fa00000u, 0x7fa00000u, 0x7fa00000u, 0x7fa00000u, 0x7fa00000u, 0x7fa00000u, 0x7fa00000u}, + {0xffbfffffu, 0xfffffffdu, 0xffbfffffu, 0xffffffffu, 0xffbfffffu, 0xffbfffffu, 0xffbfffffu, 0xffbfffffu, 0xffbfffffu, 0xffbfffffu, 0xffbfffffu}, + {0x7f800001u, 0x00000019u, 0x7f800001u, 0x7fc00001u, 0x7f800001u, 0x7f800001u, 0x7f800001u, 0x7f800001u, 0x7f800001u, 0x7f800001u, 0x7f800001u}, + {0x3f800000u, 0x000003e8u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f7fffffu, 0x7f7fffffu, 0x7f800000u, 0x7f7fffffu, 0x7f7fffffu}, + {0x3f800000u, 0xfffffc17u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x3f800000u, 0x00000081u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f7fffffu, 0x7f7fffffu, 0x7f800000u, 0x7f7fffffu, 0x7f7fffffu}, + {0xbf800000u, 0x00000081u, 0xff800000u, 0xff800000u, 0xff800000u, 0xff7fffffu, 0xff800000u, 0xff7fffffu, 0xff7fffffu, 0xff800000u, 0xff7fffffu}, + {0x40200000u, 0xffffff6au, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000002u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0xc0e80000u, 0xffffff6cu, 0x8000000eu, 0x8000000eu, 0x80000000u, 0x8000000eu, 0x8000000fu, 0x8000000eu, 0x80000000u, 0x80000000u, 0x80000000u}, + {0x3f800000u, 0xfffe7960u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0xbf800000u, 0xfffe7960u, 0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x80000001u, 0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u}, +}}; + +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 std::optional& 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 LdexpVector::*expected, const char* mode) { + for (std::uint32_t lane = 0; lane < Vectors.size(); ++lane) { + const auto& vector = Vectors[lane]; + const auto actual = Output[lane * Results]; + Require(actual == vector.*expected, std::string("ldexp f32 denormals: ") + mode + " v_ldexp_f32 (" + Hex(vector.value) + ", " + Hex(vector.exponent) + ") is " + Hex(actual) + ", expected " + Hex(vector.*expected)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device, ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}); + Check(&LdexpVector::kept, "denormals kept, IEEE=0"); + Run(*device, ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}); + Check(&LdexpVector::keptIeee, "denormals kept, IEEE=1"); + Run(*device, ShaderRecompiler::ShaderFloatMode{0xf1u, true, false, false}); + Check(&LdexpVector::keptTowardPositive, "denormals kept, round toward +inf"); + Run(*device, ShaderRecompiler::ShaderFloatMode{0xf2u, true, false, false}); + Check(&LdexpVector::keptTowardNegative, "denormals kept, round toward -inf"); + Run(*device, ShaderRecompiler::ShaderFloatMode{0xf3u, true, false, false}); + Check(&LdexpVector::keptTowardZero, "denormals kept, round toward zero"); + Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}); + Check(&LdexpVector::flushed, "denormals flushed"); + Run(*device, ShaderRecompiler::ShaderFloatMode{0xc1u, true, false, false}); + Check(&LdexpVector::flushedTowardPositive, "denormals flushed, round toward +inf"); + Run(*device, ShaderRecompiler::ShaderFloatMode{0xc2u, true, false, false}); + Check(&LdexpVector::flushedTowardNegative, "denormals flushed, round toward -inf"); + Run(*device, ShaderRecompiler::ShaderFloatMode{0xc3u, true, false, false}); + Check(&LdexpVector::flushedTowardZero, "denormals flushed, round toward zero"); + Run(*device, std::nullopt); + Check(&LdexpVector::flushed, "no float mode"); + std::puts("ldexp f32 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/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/LdsAtomics64.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LdsAtomics64.cpp index 3ca9e67f2c..e6bf5cd895 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LdsAtomics64.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LdsAtomics64.cpp @@ -90,7 +90,12 @@ constexpr std::uint64_t Sign = 0x8000000000000000ull; bool Nan(std::uint64_t bits) { return (bits & ~Sign) > 0x7ff0000000000000ull; } std::uint64_t OrderKey(std::uint64_t bits) { return (bits & Sign) != 0u ? ~bits : bits | Sign; } +constexpr std::uint64_t Quiet = 0x0008000000000000ull; + +bool Signaling(std::uint64_t bits) { return Nan(bits) && (bits & Quiet) == 0u; } std::uint64_t MinMax(std::uint64_t old, std::uint64_t data, bool max) { + if (Signaling(data)) return data | Quiet; + if (Signaling(old)) return old | Quiet; if (Nan(old)) return data; if (Nan(data)) return old; return (max ? OrderKey(data) > OrderKey(old) : OrderKey(data) < OrderKey(old)) ? data : old; @@ -114,7 +119,7 @@ void Fill(std::uint32_t tid, std::uint32_t* words) { std::array values{}; if (kind == 0u) values = {seed, seed * 0xbf58476d1ce4e5b9ull, seed ^ 0x94d049bb133111ebull}; if (kind == 1u) values = {tid % 7u, (tid * 3u) % 7u, tid % 5u}; - if (kind == 2u) values = {floats[tid % 12u], floats[(tid / 2u + 3u) % 12u], floats[(tid + 5u) % 12u]}; + if (kind == 2u) values = {floats[tid % 12u], floats[(tid / 4u + 9u) % 12u], floats[(tid + 5u) % 12u]}; if (kind == 3u) values = {seed, (tid & 4u) != 0u ? seed : seed + 0x100000000ull, seed * 3u}; for (std::uint32_t i = 0; i < values.size(); ++i) { words[i * 2u] = static_cast(values[i]); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LdsBeyondDeviceLimit.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LdsBeyondDeviceLimit.cpp new file mode 100644 index 0000000000..fb6fd2d583 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LdsBeyondDeviceLimit.cpp @@ -0,0 +1,227 @@ +#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 = 64; +constexpr std::uint32_t Results = 16; +constexpr std::uint32_t Checked = 12; +constexpr std::uint32_t MaxGroups = 16; +constexpr std::uint32_t AboveLimitBytes = 4096; +constexpr std::uint32_t BelowLimitBytes = 512; +constexpr std::uint32_t Untouched = 0xdeadbeefu; +constexpr std::uint32_t OpTypeInt = 21; +constexpr std::uint32_t OpTypeFloat = 22; +constexpr std::uint32_t OpTypeVector = 23; +constexpr std::uint32_t OpTypeArray = 28; +constexpr std::uint32_t OpTypeStruct = 30; +constexpr std::uint32_t OpTypePointer = 32; +constexpr std::uint32_t OpConstant = 43; +constexpr std::uint32_t OpVariable = 59; +constexpr std::uint32_t StorageClassWorkgroup = 4; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; +alignas(256) std::array IndirectGroups{}; + +alignas(256) constexpr std::array Code{ + 0x34020084, 0xe0381000, 0x80000401, 0x7e100208, 0x7e3e0209, 0x343e3e82, 0x4a103f08, 0x7e40020a, + 0x34404083, 0x4a104108, 0x34121090, 0x4a121300, 0x4a1412ff, 0x10000000, 0x7e160280, 0x7e180280, + 0x7e1a0281, 0x7e1e0281, 0x7e200280, 0xbf8c0000, 0xd9340000, 0x00000b06, 0xd834000c, 0x00000b06, + 0xd8340000, 0x00000904, 0xd8340000, 0x00000a05, 0xd8340000, 0x00000907, 0xbf8c0000, 0xbf8a0000, + 0xd880000c, 0x28000d06, 0xd9800000, 0x29000f06, 0x3a260884, 0x3a2a0aff, 0x00000080, 0x363608ff, + 0xfffffff8, 0xbf8c0000, 0xbf8a0000, 0xd8d80000, 0x2b000013, 0xd8d80000, 0x2c000015, 0xd8d80000, + 0x2d000007, 0xd8d8000c, 0x2e000006, 0xd9d80000, 0x2f000006, 0xd9d80000, 0x3100001b, 0xd8d80000, + 0x33000005, 0x343c1086, 0x4a3c3d00, 0x343c3c86, 0xbf8c0000, 0xe0781000, 0x8001281e, 0xe0781010, + 0x80012c1e, 0xe0781020, 0x8001301e, 0xbf8c0000, 0xbf810000, +}; + +constexpr const char* Names[Checked] = { + "ds_add_rtn_u32 result", "ds_add_rtn_u64 result low", "ds_add_rtn_u64 result high", "neighbour's word above the limit", + "other wave's word below the limit", "out-of-range read", "final 32-bit counter", "final 64-bit counter low", + "final 64-bit counter high", "ds_read_b64 above the limit, low", "ds_read_b64 above the limit, high", "own word below the limit", +}; + +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::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 && at + count <= words.size(), "lds beyond the device limit: malformed SPIR-V at word " + std::to_string(at)); + const auto instruction = words.subspan(at, count); + const auto opcode = instruction[0] & 0xffffu; + if (opcode == OpTypeInt || opcode == OpTypeFloat || opcode == OpTypeVector || opcode == OpTypeArray || opcode == OpTypeStruct || opcode == OpTypePointer) types[instruction[1]] = instruction; + if (opcode == OpConstant) constants[instruction[2]] = instruction[3]; + if (opcode == OpVariable && instruction[3] == StorageClassWorkgroup) pointers.push_back(instruction[1]); + at += count; + } + const std::function size = [&](std::uint32_t id) -> std::uint64_t { + const auto type = types.find(id); + Require(type != types.end(), "lds beyond the device limit: a Workgroup variable has a type the test does not size"); + const auto& definition = type->second; + switch (definition[0] & 0xffffu) { + case OpTypeInt: + case OpTypeFloat: + return definition[2] / 8u; + case OpTypeVector: + return size(definition[2]) * definition[3]; + case OpTypeArray: { + const auto length = constants.find(definition[3]); + Require(length != constants.end(), "lds beyond the device limit: a Workgroup array has a length that is not a constant"); + return size(definition[2]) * length->second; + } + case OpTypeStruct: { + std::uint64_t bytes = 0; + for (const auto member : definition.subspan(2)) bytes += size(member); + return bytes; + } + default: + throw std::runtime_error("lds beyond the device limit: a Workgroup variable has a type the test does not size"); + } + }; + std::uint64_t total = 0; + for (const auto pointer : pointers) { + const auto type = types.find(pointer); + Require(type != types.end() && (type->second[0] & 0xffffu) == OpTypePointer, "lds beyond the device limit: a Workgroup variable is not a pointer"); + total += size(type->second[3]); + } + return total; +} + +ShaderRecompiler::RecompileResult Compile(std::uint32_t waveSize, std::uint32_t ldsBytes, const ShaderRecompiler::SpirvTarget& target) { + 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(std::span(Code))}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, ldsBytes / 4u, {true, true, true}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(Code.data()), Code, 0, {}}, + {waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory}, + target, + {0, 0, 0, 128} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +std::uint32_t Group(std::uint32_t x, std::uint32_t y, std::uint32_t z) { + return (z << 3u) | (y << 2u) | x; +} + +void Run(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, const ShaderRecompiler::SpirvTarget& target, std::uint32_t groupsX, std::uint32_t groupsY, std::uint32_t groupsZ, bool indirect, const std::string& run) { + const auto limit = target.maxWorkgroupSharedMemoryBytes; + Require(limit % 8u == 0u && limit >= 2048u, "lds beyond the device limit: unexpected maxComputeSharedMemorySize " + std::to_string(limit)); + const auto fits = [&](const ShaderRecompiler::RecompileResult& compiled, const std::string& what) { + const auto bytes = WorkgroupBytes(compiled.spirv.Words()); + Require(bytes <= limit, "lds beyond the device limit " + run + ": " + what + " declares " + std::to_string(bytes) + " bytes of Workgroup memory, over the device's " + std::to_string(limit)); + }; + const auto within = Compile(waveSize, limit - BelowLimitBytes, target); + 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 + 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) { + Input[tid * 4u + 0u] = limit + tid * 4u; + Input[tid * 4u + 1u] = 1024u + tid * 4u; + Input[tid * 4u + 2u] = bytes - 16u; + Input[tid * 4u + 3u] = bytes + tid * 4u; + } + Output.fill(Untouched); + const auto result = Compile(waveSize, bytes, target); + 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}; + const auto outcome = device.DispatchIndirect(result, reinterpret_cast(IndirectGroups.data()), {}, reinterpret_cast(Code.data())); + Require(outcome.cpuReason == 8, "lds beyond the device limit " + run + ": the indirect group counts were not read on the CPU (reason " + std::to_string(outcome.cpuReason) + ")"); + } else { + device.Dispatch(result, groupsX, groupsY, groupsZ, {}, reinterpret_cast(Code.data())); + } + device.WaitIdle(); + std::vector dispatched(MaxGroups, false); + for (std::uint32_t z = 0; z < groupsZ; ++z) { + for (std::uint32_t y = 0; y < groupsY; ++y) { + for (std::uint32_t x = 0; x < groupsX; ++x) dispatched[Group(x, y, z)] = true; + } + } + for (std::uint32_t group = 0; group < MaxGroups; ++group) { + const std::string where = "lds beyond the device limit " + run + ", group " + std::to_string(group); + std::vector counter32; + std::vector counter64; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto* lane = &Output[(group * Threads + tid) * Results]; + if (!dispatched[group]) { + Require(std::all_of(lane, lane + Results, [](std::uint32_t word) { return word == Untouched; }), where + ": lane " + std::to_string(tid) + " of a group that was not dispatched was written"); + continue; + } + const auto value = [&](std::uint32_t laneId) { return (group << 16u) | laneId; }; + const std::array expected{ + lane[0], lane[1], 0u, value(tid ^ 1u), + value(tid ^ 32u) + 0x10000000u, 0u, Threads, Threads, + 0u, value(tid & ~1u), value(tid | 1u), value(tid) + 0x10000000u, + }; + for (std::uint32_t index = 0; index < Checked; ++index) { + Require(lane[index] == expected[index], where + ": lane " + std::to_string(tid) + " " + Names[index] + " is " + Hex(lane[index]) + ", expected " + Hex(expected[index])); + } + counter32.push_back(lane[0]); + counter64.push_back(lane[1]); + } + if (!dispatched[group]) continue; + std::sort(counter32.begin(), counter32.end()); + std::sort(counter64.begin(), counter64.end()); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Require(counter32[tid] == tid && counter64[tid] == tid, where + ": the LDS atomics returned " + Hex(counter32[tid]) + " / " + Hex(counter64[tid]) + " as their " + std::to_string(tid) + "th smallest old value"); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device, 32, device->Target(), 3, 2, 1, false, "wave32 3x2"); + Run(*device, 32, device->Target(), 4, 2, 1, false, "wave32 4x2"); + Run(*device, 64, device->Target(), 3, 2, 1, false, "wave64 3x2"); + Run(*device, 64, device->ComputeTarget(32), 4, 2, 1, false, "wave64 split 4x2"); + Run(*device, 64, device->Target(), 2, 2, 2, false, "wave64 2x2x2"); + Run(*device, 32, device->Target(), 2, 2, 2, true, "wave32 indirect 2x2x2"); + std::puts("lds beyond the device 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/LdsFloatAtomicModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LdsFloatAtomicModes.cpp new file mode 100644 index 0000000000..0233d2c12a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LdsFloatAtomicModes.cpp @@ -0,0 +1,480 @@ +#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 = 32; +constexpr std::uint32_t Inputs = 8; +constexpr std::uint32_t Results = 32; +constexpr std::uint32_t Columns = 27; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020085u, 0x34040087u, 0xe0381000u, 0x80000401u, 0xe0381010u, 0x80000801u, 0xbf8c3f70u, 0x7e180280u, + 0x7e1a0280u, 0x7e1c0280u, 0x7e1e0280u, 0x7e200280u, 0x7e220280u, 0x7e240280u, 0x7e260280u, 0x7e280280u, + 0x7e2a0280u, 0x7e2c0280u, 0x7e2e0280u, 0x7e300280u, 0x7e320280u, 0x7e340280u, 0x7e360280u, 0x7e380280u, + 0x7e3a0280u, 0x7e3c0280u, 0x7e3e0280u, 0x7e400280u, 0x7e420280u, 0x7e440280u, 0x7e460280u, 0x7e480280u, + 0x7e4a0280u, 0x7e4c0280u, 0x7e4e0280u, 0x7e5002ffu, 0x45464748u, 0x7e5202ffu, 0x41424344u, 0x34060083u, + 0xd8340000u, 0x00000403u, 0xbf8cc07fu, 0xd8c80000u, 0x0d000503u, 0xbf8cc07fu, 0xd8d80000u, 0x0c000003u, + 0xbf8cc07fu, 0xd8340000u, 0x00000403u, 0xbf8cc07fu, 0xd8cc0000u, 0x0f000503u, 0xbf8cc07fu, 0xd8d80000u, + 0x0e000003u, 0xbf8cc07fu, 0xd8340000u, 0x00000403u, 0xbf8cc07fu, 0xd8c40000u, 0x110a0503u, 0xbf8cc07fu, + 0xd8d80000u, 0x10000003u, 0xbf8cc07fu, 0xd8340000u, 0x00000403u, 0xbf8cc07fu, 0xd8480000u, 0x00000503u, + 0xbf8cc07fu, 0xd8d80000u, 0x12000003u, 0xbf8cc07fu, 0xd8340000u, 0x00000403u, 0xbf8cc07fu, 0xd84c0000u, + 0x00000503u, 0xbf8cc07fu, 0xd8d80000u, 0x13000003u, 0xbf8cc07fu, 0xd8340000u, 0x00000403u, 0xbf8cc07fu, + 0xd8440000u, 0x000a0503u, 0xbf8cc07fu, 0xd8d80000u, 0x14000003u, 0xbf8cc07fu, 0xd9340000u, 0x00000603u, + 0xbf8cc07fu, 0xd9c80000u, 0x18000803u, 0xbf8cc07fu, 0xd9d80000u, 0x16000003u, 0xbf8cc07fu, 0xd9340000u, + 0x00000603u, 0xbf8cc07fu, 0xd9cc0000u, 0x1c000803u, 0xbf8cc07fu, 0xd9d80000u, 0x1a000003u, 0xbf8cc07fu, + 0xd9340000u, 0x00000603u, 0xbf8cc07fu, 0xd9480000u, 0x00000803u, 0xbf8cc07fu, 0xd9d80000u, 0x1e000003u, + 0xbf8cc07fu, 0xd9340000u, 0x00000603u, 0xbf8cc07fu, 0xd94c0000u, 0x00000803u, 0xbf8cc07fu, 0xd9d80000u, + 0x20000003u, 0xbf8cc07fu, 0xd9340000u, 0x00000603u, 0xbf8cc07fu, 0xd9c40000u, 0x24280803u, 0xbf8cc07fu, + 0xd9d80000u, 0x22000003u, 0xbf8cc07fu, 0xd9340000u, 0x00000603u, 0xbf8cc07fu, 0xd9440000u, 0x00280803u, + 0xbf8cc07fu, 0xd9d80000u, 0x26000003u, 0xbf8cc07fu, 0xe0781000u, 0x80010c02u, 0xe0781010u, 0x80011002u, + 0xe0781020u, 0x80011402u, 0xe0781030u, 0x80011802u, 0xe0781040u, 0x80011c02u, 0xe0781050u, 0x80012002u, + 0xe0781060u, 0x80012402u, 0xbf810000u, +}; + +constexpr std::uint32_t Rows[32][8] = { + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x45464748u, 0x00000000u}, + {0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x45464748u, 0x00000000u}, + {0x80000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x45464748u, 0x00000000u}, + {0x3f800000u, 0xbf800000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0xbff00000u, 0x45464748u, 0x00000000u}, + {0xbf800000u, 0x3f800000u, 0x00000000u, 0xbff00000u, 0x00000000u, 0x3ff00000u, 0x45464748u, 0x00000000u}, + {0x7f800000u, 0xff800000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0xfff00000u, 0x45464748u, 0x00000000u}, + {0xff800000u, 0x7f800000u, 0x00000000u, 0xfff00000u, 0x00000000u, 0x7ff00000u, 0x45464748u, 0x00000000u}, + {0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x45464748u, 0x00000000u}, + {0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x45464748u, 0x00000000u}, + {0x80000001u, 0x80000000u, 0x00000001u, 0x80000000u, 0x00000000u, 0x80000000u, 0x45464748u, 0x00000000u}, + {0x00000001u, 0x80000001u, 0x00000001u, 0x00000000u, 0x00000001u, 0x80000000u, 0x45464748u, 0x00000000u}, + {0x3f800000u, 0x00000001u, 0x00000000u, 0x3ff00000u, 0x00000001u, 0x00000000u, 0x45464748u, 0x00000000u}, + {0x7fc00000u, 0x3f800000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x3ff00000u, 0x45464748u, 0x00000000u}, + {0x3f800000u, 0x7fc00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x7ff80000u, 0x45464748u, 0x00000000u}, + {0x7fc00000u, 0x7fc00123u, 0x00000000u, 0x7ff80000u, 0x00000123u, 0x7ff80000u, 0x45464748u, 0x00000000u}, + {0x7fc00123u, 0x7fc00000u, 0x00000123u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x45464748u, 0x00000000u}, + {0xffc00000u, 0x7fc00000u, 0x00000000u, 0xfff80000u, 0x00000000u, 0x7ff80000u, 0x45464748u, 0x00000000u}, + {0x7fc00000u, 0x7fc00000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x45464748u, 0x00000000u}, + {0x7f800001u, 0x3f800000u, 0x00000001u, 0x7ff00000u, 0x00000000u, 0x3ff00000u, 0x45464748u, 0x00000000u}, + {0x3f800000u, 0x7f800001u, 0x00000000u, 0x3ff00000u, 0x00000001u, 0x7ff00000u, 0x45464748u, 0x00000000u}, + {0x7f800001u, 0x7fc00000u, 0x00000001u, 0x7ff00000u, 0x00000000u, 0x7ff80000u, 0x45464748u, 0x00000000u}, + {0x7fc00000u, 0x7f800001u, 0x00000000u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x45464748u, 0x00000000u}, + {0x7f800001u, 0x7f800123u, 0x00000001u, 0x7ff00000u, 0x00000123u, 0x7ff00000u, 0x45464748u, 0x00000000u}, + {0x7f800123u, 0x7f800001u, 0x00000123u, 0x7ff00000u, 0x00000001u, 0x7ff00000u, 0x45464748u, 0x00000000u}, + {0xff800001u, 0x7f800001u, 0x00000001u, 0xfff00000u, 0x00000001u, 0x7ff00000u, 0x45464748u, 0x00000000u}, + {0x7f800001u, 0xff800001u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0xfff00000u, 0x45464748u, 0x00000000u}, + {0x00000001u, 0x7f800001u, 0x00000001u, 0x00000000u, 0x00000001u, 0x7ff00000u, 0x45464748u, 0x00000000u}, + {0x7f800001u, 0x00000001u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x00000000u, 0x45464748u, 0x00000000u}, + {0x7f800000u, 0x7f800001u, 0x00000000u, 0x7ff00000u, 0x00000001u, 0x7ff00000u, 0x45464748u, 0x00000000u}, + {0x7f800001u, 0x7f800000u, 0x00000001u, 0x7ff00000u, 0x00000000u, 0x7ff00000u, 0x45464748u, 0x00000000u}, + {0x00000000u, 0x7f800001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x7ff00000u, 0x45464748u, 0x00000000u}, + {0x7f800001u, 0x00000000u, 0x00000001u, 0x7ff00000u, 0x00000000u, 0x00000000u, 0x45464748u, 0x00000000u} +}; + +constexpr std::uint32_t ExpectedTitles[32][Columns] = { + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x45464748u, 0x00000000u, 0x00000000u, 0x00000000u, 0x45464748u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x45464748u, 0x41424344u, 0x00000000u, 0x00000000u, 0x45464748u, 0x41424344u}, + {0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x45464748u, 0x00000000u, 0x80000000u, 0x00000000u, 0x45464748u, + 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x80000000u, 0x00000000u, 0x00000000u, 0x45464748u, 0x41424344u, 0x00000000u, 0x00000000u, 0x45464748u, 0x41424344u}, + {0x80000000u, 0x80000000u, 0x00000000u, 0x80000000u, 0x45464748u, 0x80000000u, 0x80000000u, 0x00000000u, 0x45464748u, + 0x00000000u, 0x80000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, + 0x80000000u, 0x00000000u, 0x00000000u, 0x45464748u, 0x41424344u, 0x00000000u, 0x80000000u, 0x45464748u, 0x41424344u}, + {0xbf800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0xbf800000u, 0x3f800000u, 0x3f800000u, + 0x00000000u, 0xbff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, + 0xbff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u}, + {0xbf800000u, 0xbf800000u, 0x3f800000u, 0xbf800000u, 0xbf800000u, 0xbf800000u, 0xbf800000u, 0x3f800000u, 0xbf800000u, + 0x00000000u, 0xbff00000u, 0x00000000u, 0xbff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0xbff00000u, 0x00000000u, + 0xbff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0xbff00000u, 0x00000000u, 0xbff00000u, 0x00000000u, 0xbff00000u}, + {0xff800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0xff800000u, 0x7f800000u, 0x7f800000u, + 0x00000000u, 0xfff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, + 0xfff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0x7ff00000u}, + {0xff800000u, 0xff800000u, 0x7f800000u, 0xff800000u, 0xff800000u, 0xff800000u, 0xff800000u, 0x7f800000u, 0xff800000u, + 0x00000000u, 0xfff00000u, 0x00000000u, 0xfff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0xfff00000u, 0x00000000u, + 0xfff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0xfff00000u, 0x00000000u, 0xfff00000u, 0x00000000u, 0xfff00000u}, + {0x00000000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x45464748u, 0x00000001u, 0x00000000u, 0x00000000u, 0x45464748u, + 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x45464748u, 0x00000000u, 0x00000000u, 0x00000000u, 0x45464748u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x80000000u, 0x80000001u, 0x80000000u, 0x80000001u, 0x45464748u, 0x80000001u, 0x80000000u, 0x80000000u, 0x45464748u, + 0x00000001u, 0x80000000u, 0x00000001u, 0x80000000u, 0x00000000u, 0x80000000u, 0x00000001u, 0x80000000u, 0x00000001u, + 0x80000000u, 0x00000000u, 0x80000000u, 0x00000001u, 0x80000000u, 0x00000001u, 0x80000000u, 0x00000001u, 0x80000000u}, + {0x80000000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x45464748u, 0x00000001u, 0x80000000u, 0x00000000u, 0x45464748u, + 0x00000001u, 0x80000000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000001u, + 0x80000000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u}, + {0x00000000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x3f800000u, + 0x00000001u, 0x00000000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000001u, + 0x00000000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u}, + {0x3f800000u, 0x7fc00000u, 0x3f800000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x3f800000u, 0x3f800000u, 0x7fc00000u, + 0x00000000u, 0x3ff00000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x7ff80000u, 0x00000000u, + 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u}, + {0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, + 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, + 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u}, + {0x7fc00123u, 0x7fc00000u, 0x7fc00123u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00123u, 0x7fc00123u, 0x7fc00000u, + 0x00000123u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000123u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000123u, + 0x7ff80000u, 0x00000123u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u}, + {0x7fc00000u, 0x7fc00123u, 0x7fc00000u, 0x7fc00123u, 0x7fc00123u, 0x7fc00123u, 0x7fc00000u, 0x7fc00000u, 0x7fc00123u, + 0x00000000u, 0x7ff80000u, 0x00000123u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000123u, 0x7ff80000u, 0x00000000u, + 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000123u, 0x7ff80000u, 0x00000123u, 0x7ff80000u, 0x00000123u, 0x7ff80000u}, + {0x7fc00000u, 0xffc00000u, 0x7fc00000u, 0xffc00000u, 0xffc00000u, 0xffc00000u, 0x7fc00000u, 0x7fc00000u, 0xffc00000u, + 0x00000000u, 0x7ff80000u, 0x00000000u, 0xfff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0xfff80000u, 0x00000000u, + 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0xfff80000u, 0x00000000u, 0xfff80000u, 0x00000000u, 0xfff80000u}, + {0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, + 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, + 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u}, + {0x7fc00001u, 0x7f800001u, 0x7fc00001u, 0x7f800001u, 0x7f800001u, 0x7f800001u, 0x7fc00001u, 0x7fc00001u, 0x7f800001u, + 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u}, + {0x7fc00001u, 0x3f800000u, 0x7fc00001u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x7fc00001u, 0x7fc00001u, 0x3f800000u, + 0x00000001u, 0x7ff80000u, 0x00000000u, 0x3ff00000u, 0x00000001u, 0x7ff80000u, 0x00000000u, 0x3ff00000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u}, + {0x7fc00001u, 0x7f800001u, 0x7fc00001u, 0x7f800001u, 0x7f800001u, 0x7f800001u, 0x7fc00001u, 0x7fc00001u, 0x7f800001u, + 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u}, + {0x7fc00001u, 0x7fc00000u, 0x7fc00001u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00001u, 0x7fc00001u, 0x7fc00000u, + 0x00000001u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u}, + {0x7fc00123u, 0x7f800001u, 0x7fc00123u, 0x7f800001u, 0x7f800001u, 0x7f800001u, 0x7fc00123u, 0x7fc00123u, 0x7f800001u, + 0x00000123u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000123u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000123u, + 0x7ff80000u, 0x00000123u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u}, + {0x7fc00001u, 0x7f800123u, 0x7fc00001u, 0x7f800123u, 0x7f800123u, 0x7f800123u, 0x7fc00001u, 0x7fc00001u, 0x7f800123u, + 0x00000001u, 0x7ff80000u, 0x00000123u, 0x7ff00000u, 0x00000001u, 0x7ff80000u, 0x00000123u, 0x7ff00000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000123u, 0x7ff00000u, 0x00000123u, 0x7ff00000u, 0x00000123u, 0x7ff00000u}, + {0x7fc00001u, 0xff800001u, 0x7fc00001u, 0xff800001u, 0xff800001u, 0xff800001u, 0x7fc00001u, 0x7fc00001u, 0xff800001u, + 0x00000001u, 0x7ff80000u, 0x00000001u, 0xfff00000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0xfff00000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0xfff00000u, 0x00000001u, 0xfff00000u, 0x00000001u, 0xfff00000u}, + {0xffc00001u, 0x7f800001u, 0xffc00001u, 0x7f800001u, 0x7f800001u, 0x7f800001u, 0xffc00001u, 0xffc00001u, 0x7f800001u, + 0x00000001u, 0xfff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0xfff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, + 0xfff80000u, 0x00000001u, 0xfff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u}, + {0x7fc00001u, 0x00000001u, 0x7fc00001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x7fc00001u, 0x7fc00001u, 0x00000001u, + 0x00000001u, 0x7ff80000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x00000000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u}, + {0x7fc00001u, 0x7f800001u, 0x7fc00001u, 0x7f800001u, 0x7f800001u, 0x7f800001u, 0x7fc00001u, 0x7fc00001u, 0x7f800001u, + 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u}, + {0x7fc00001u, 0x7f800000u, 0x7fc00001u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7fc00001u, 0x7fc00001u, 0x7f800000u, + 0x00000001u, 0x7ff80000u, 0x00000000u, 0x7ff00000u, 0x00000001u, 0x7ff80000u, 0x00000000u, 0x7ff00000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0x7ff00000u}, + {0x7fc00001u, 0x7f800001u, 0x7fc00001u, 0x7f800001u, 0x7f800001u, 0x7f800001u, 0x7fc00001u, 0x7fc00001u, 0x7f800001u, + 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u}, + {0x7fc00001u, 0x00000000u, 0x7fc00001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x7fc00001u, 0x7fc00001u, 0x00000000u, + 0x00000001u, 0x7ff80000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x7ff80000u, 0x00000000u, 0x00000000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x7fc00001u, 0x7f800001u, 0x7fc00001u, 0x7f800001u, 0x7f800001u, 0x7f800001u, 0x7fc00001u, 0x7fc00001u, 0x7f800001u, + 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u} +}; + +constexpr std::uint32_t ExpectedKept[32][Columns] = { + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x45464748u, 0x00000000u, 0x00000000u, 0x00000000u, 0x45464748u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x45464748u, 0x41424344u, 0x00000000u, 0x00000000u, 0x45464748u, 0x41424344u}, + {0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x45464748u, 0x00000000u, 0x80000000u, 0x00000000u, 0x45464748u, + 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x80000000u, 0x00000000u, 0x00000000u, 0x45464748u, 0x41424344u, 0x00000000u, 0x00000000u, 0x45464748u, 0x41424344u}, + {0x80000000u, 0x80000000u, 0x00000000u, 0x80000000u, 0x45464748u, 0x80000000u, 0x80000000u, 0x00000000u, 0x45464748u, + 0x00000000u, 0x80000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, + 0x80000000u, 0x00000000u, 0x00000000u, 0x45464748u, 0x41424344u, 0x00000000u, 0x80000000u, 0x45464748u, 0x41424344u}, + {0xbf800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0xbf800000u, 0x3f800000u, 0x3f800000u, + 0x00000000u, 0xbff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, + 0xbff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u}, + {0xbf800000u, 0xbf800000u, 0x3f800000u, 0xbf800000u, 0xbf800000u, 0xbf800000u, 0xbf800000u, 0x3f800000u, 0xbf800000u, + 0x00000000u, 0xbff00000u, 0x00000000u, 0xbff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0xbff00000u, 0x00000000u, + 0xbff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0xbff00000u, 0x00000000u, 0xbff00000u, 0x00000000u, 0xbff00000u}, + {0xff800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0xff800000u, 0x7f800000u, 0x7f800000u, + 0x00000000u, 0xfff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, + 0xfff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0x7ff00000u}, + {0xff800000u, 0xff800000u, 0x7f800000u, 0xff800000u, 0xff800000u, 0xff800000u, 0xff800000u, 0x7f800000u, 0xff800000u, + 0x00000000u, 0xfff00000u, 0x00000000u, 0xfff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0xfff00000u, 0x00000000u, + 0xfff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0xfff00000u, 0x00000000u, 0xfff00000u, 0x00000000u, 0xfff00000u}, + {0x00000000u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000001u, + 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x80000001u, 0x80000001u, 0x80000000u, 0x80000001u, 0x80000001u, 0x80000001u, 0x80000001u, 0x80000000u, 0x80000001u, + 0x00000001u, 0x80000000u, 0x00000001u, 0x80000000u, 0x00000000u, 0x80000000u, 0x00000001u, 0x80000000u, 0x00000001u, + 0x80000000u, 0x00000000u, 0x80000000u, 0x00000001u, 0x80000000u, 0x00000001u, 0x80000000u, 0x00000001u, 0x80000000u}, + {0x80000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x80000001u, 0x00000001u, 0x00000001u, + 0x00000001u, 0x80000000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000001u, + 0x80000000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u}, + {0x00000001u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x00000001u, 0x3f800000u, 0x3f800000u, + 0x00000001u, 0x00000000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000001u, + 0x00000000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u}, + {0x3f800000u, 0x7fc00000u, 0x3f800000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x3f800000u, 0x3f800000u, 0x7fc00000u, + 0x00000000u, 0x3ff00000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x7ff80000u, 0x00000000u, + 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u}, + {0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, + 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, + 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u}, + {0x7fc00123u, 0x7fc00000u, 0x7fc00123u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00123u, 0x7fc00123u, 0x7fc00000u, + 0x00000123u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000123u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000123u, + 0x7ff80000u, 0x00000123u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u}, + {0x7fc00000u, 0x7fc00123u, 0x7fc00000u, 0x7fc00123u, 0x7fc00123u, 0x7fc00123u, 0x7fc00000u, 0x7fc00000u, 0x7fc00123u, + 0x00000000u, 0x7ff80000u, 0x00000123u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000123u, 0x7ff80000u, 0x00000000u, + 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000123u, 0x7ff80000u, 0x00000123u, 0x7ff80000u, 0x00000123u, 0x7ff80000u}, + {0x7fc00000u, 0xffc00000u, 0x7fc00000u, 0xffc00000u, 0xffc00000u, 0xffc00000u, 0x7fc00000u, 0x7fc00000u, 0xffc00000u, + 0x00000000u, 0x7ff80000u, 0x00000000u, 0xfff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0xfff80000u, 0x00000000u, + 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0xfff80000u, 0x00000000u, 0xfff80000u, 0x00000000u, 0xfff80000u}, + {0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, + 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, + 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u}, + {0x7fc00001u, 0x7f800001u, 0x7fc00001u, 0x7f800001u, 0x7f800001u, 0x7f800001u, 0x7fc00001u, 0x7fc00001u, 0x7f800001u, + 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u}, + {0x7fc00001u, 0x3f800000u, 0x7fc00001u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x7fc00001u, 0x7fc00001u, 0x3f800000u, + 0x00000001u, 0x7ff80000u, 0x00000000u, 0x3ff00000u, 0x00000001u, 0x7ff80000u, 0x00000000u, 0x3ff00000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u}, + {0x7fc00001u, 0x7f800001u, 0x7fc00001u, 0x7f800001u, 0x7f800001u, 0x7f800001u, 0x7fc00001u, 0x7fc00001u, 0x7f800001u, + 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u}, + {0x7fc00001u, 0x7fc00000u, 0x7fc00001u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00001u, 0x7fc00001u, 0x7fc00000u, + 0x00000001u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u}, + {0x7fc00123u, 0x7f800001u, 0x7fc00123u, 0x7f800001u, 0x7f800001u, 0x7f800001u, 0x7fc00123u, 0x7fc00123u, 0x7f800001u, + 0x00000123u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000123u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000123u, + 0x7ff80000u, 0x00000123u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u}, + {0x7fc00001u, 0x7f800123u, 0x7fc00001u, 0x7f800123u, 0x7f800123u, 0x7f800123u, 0x7fc00001u, 0x7fc00001u, 0x7f800123u, + 0x00000001u, 0x7ff80000u, 0x00000123u, 0x7ff00000u, 0x00000001u, 0x7ff80000u, 0x00000123u, 0x7ff00000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000123u, 0x7ff00000u, 0x00000123u, 0x7ff00000u, 0x00000123u, 0x7ff00000u}, + {0x7fc00001u, 0xff800001u, 0x7fc00001u, 0xff800001u, 0xff800001u, 0xff800001u, 0x7fc00001u, 0x7fc00001u, 0xff800001u, + 0x00000001u, 0x7ff80000u, 0x00000001u, 0xfff00000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0xfff00000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0xfff00000u, 0x00000001u, 0xfff00000u, 0x00000001u, 0xfff00000u}, + {0xffc00001u, 0x7f800001u, 0xffc00001u, 0x7f800001u, 0x7f800001u, 0x7f800001u, 0xffc00001u, 0xffc00001u, 0x7f800001u, + 0x00000001u, 0xfff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0xfff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, + 0xfff80000u, 0x00000001u, 0xfff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u}, + {0x7fc00001u, 0x00000001u, 0x7fc00001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x7fc00001u, 0x7fc00001u, 0x00000001u, + 0x00000001u, 0x7ff80000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x00000000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u}, + {0x7fc00001u, 0x7f800001u, 0x7fc00001u, 0x7f800001u, 0x7f800001u, 0x7f800001u, 0x7fc00001u, 0x7fc00001u, 0x7f800001u, + 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u}, + {0x7fc00001u, 0x7f800000u, 0x7fc00001u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7fc00001u, 0x7fc00001u, 0x7f800000u, + 0x00000001u, 0x7ff80000u, 0x00000000u, 0x7ff00000u, 0x00000001u, 0x7ff80000u, 0x00000000u, 0x7ff00000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0x7ff00000u}, + {0x7fc00001u, 0x7f800001u, 0x7fc00001u, 0x7f800001u, 0x7f800001u, 0x7f800001u, 0x7fc00001u, 0x7fc00001u, 0x7f800001u, + 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u}, + {0x7fc00001u, 0x00000000u, 0x7fc00001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x7fc00001u, 0x7fc00001u, 0x00000000u, + 0x00000001u, 0x7ff80000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x7ff80000u, 0x00000000u, 0x00000000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x7fc00001u, 0x7f800001u, 0x7fc00001u, 0x7f800001u, 0x7f800001u, 0x7f800001u, 0x7fc00001u, 0x7fc00001u, 0x7f800001u, + 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u} +}; + +constexpr std::uint32_t ExpectedFlushed[32][Columns] = { + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x45464748u, 0x00000000u, 0x00000000u, 0x00000000u, 0x45464748u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x45464748u, 0x41424344u, 0x00000000u, 0x00000000u, 0x45464748u, 0x41424344u}, + {0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x45464748u, 0x00000000u, 0x80000000u, 0x00000000u, 0x45464748u, + 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x80000000u, 0x00000000u, 0x00000000u, 0x45464748u, 0x41424344u, 0x00000000u, 0x00000000u, 0x45464748u, 0x41424344u}, + {0x80000000u, 0x80000000u, 0x00000000u, 0x80000000u, 0x45464748u, 0x80000000u, 0x80000000u, 0x00000000u, 0x45464748u, + 0x00000000u, 0x80000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, + 0x80000000u, 0x00000000u, 0x00000000u, 0x45464748u, 0x41424344u, 0x00000000u, 0x80000000u, 0x45464748u, 0x41424344u}, + {0xbf800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0xbf800000u, 0x3f800000u, 0x3f800000u, + 0x00000000u, 0xbff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, + 0xbff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u}, + {0xbf800000u, 0xbf800000u, 0x3f800000u, 0xbf800000u, 0xbf800000u, 0xbf800000u, 0xbf800000u, 0x3f800000u, 0xbf800000u, + 0x00000000u, 0xbff00000u, 0x00000000u, 0xbff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0xbff00000u, 0x00000000u, + 0xbff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0xbff00000u, 0x00000000u, 0xbff00000u, 0x00000000u, 0xbff00000u}, + {0xff800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0xff800000u, 0x7f800000u, 0x7f800000u, + 0x00000000u, 0xfff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, + 0xfff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0x7ff00000u}, + {0xff800000u, 0xff800000u, 0x7f800000u, 0xff800000u, 0xff800000u, 0xff800000u, 0xff800000u, 0x7f800000u, 0xff800000u, + 0x00000000u, 0xfff00000u, 0x00000000u, 0xfff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0xfff00000u, 0x00000000u, + 0xfff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0xfff00000u, 0x00000000u, 0xfff00000u, 0x00000000u, 0xfff00000u}, + {0x00000000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x45464748u, 0x00000001u, 0x00000000u, 0x00000000u, 0x45464748u, + 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x45464748u, 0x41424344u, 0x00000001u, 0x00000000u, 0x45464748u, 0x41424344u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x45464748u, 0x00000000u, 0x00000000u, 0x00000000u, 0x45464748u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x45464748u, 0x41424344u, 0x00000000u, 0x00000000u, 0x45464748u, 0x41424344u}, + {0x80000000u, 0x80000001u, 0x80000000u, 0x80000001u, 0x45464748u, 0x80000001u, 0x80000000u, 0x80000000u, 0x45464748u, + 0x00000000u, 0x80000000u, 0x00000001u, 0x80000000u, 0x00000000u, 0x80000000u, 0x00000001u, 0x80000000u, 0x00000000u, + 0x80000000u, 0x00000000u, 0x80000000u, 0x45464748u, 0x41424344u, 0x00000001u, 0x80000000u, 0x45464748u, 0x41424344u}, + {0x80000000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x45464748u, 0x00000001u, 0x80000000u, 0x00000000u, 0x45464748u, + 0x00000000u, 0x80000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, + 0x80000000u, 0x00000000u, 0x00000000u, 0x45464748u, 0x41424344u, 0x00000001u, 0x00000000u, 0x45464748u, 0x41424344u}, + {0x00000000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x3f800000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u}, + {0x3f800000u, 0x7fc00000u, 0x3f800000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x3f800000u, 0x3f800000u, 0x7fc00000u, + 0x00000000u, 0x3ff00000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x7ff80000u, 0x00000000u, + 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u}, + {0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, + 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, + 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u}, + {0x7fc00123u, 0x7fc00000u, 0x7fc00123u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00123u, 0x7fc00123u, 0x7fc00000u, + 0x00000123u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000123u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000123u, + 0x7ff80000u, 0x00000123u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u}, + {0x7fc00000u, 0x7fc00123u, 0x7fc00000u, 0x7fc00123u, 0x7fc00123u, 0x7fc00123u, 0x7fc00000u, 0x7fc00000u, 0x7fc00123u, + 0x00000000u, 0x7ff80000u, 0x00000123u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000123u, 0x7ff80000u, 0x00000000u, + 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000123u, 0x7ff80000u, 0x00000123u, 0x7ff80000u, 0x00000123u, 0x7ff80000u}, + {0x7fc00000u, 0xffc00000u, 0x7fc00000u, 0xffc00000u, 0xffc00000u, 0xffc00000u, 0x7fc00000u, 0x7fc00000u, 0xffc00000u, + 0x00000000u, 0x7ff80000u, 0x00000000u, 0xfff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0xfff80000u, 0x00000000u, + 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0xfff80000u, 0x00000000u, 0xfff80000u, 0x00000000u, 0xfff80000u}, + {0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, + 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, + 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u}, + {0x7fc00001u, 0x7f800001u, 0x7fc00001u, 0x7f800001u, 0x7f800001u, 0x7f800001u, 0x7fc00001u, 0x7fc00001u, 0x7f800001u, + 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u}, + {0x7fc00001u, 0x3f800000u, 0x7fc00001u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x7fc00001u, 0x7fc00001u, 0x3f800000u, + 0x00000001u, 0x7ff80000u, 0x00000000u, 0x3ff00000u, 0x00000001u, 0x7ff80000u, 0x00000000u, 0x3ff00000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u}, + {0x7fc00001u, 0x7f800001u, 0x7fc00001u, 0x7f800001u, 0x7f800001u, 0x7f800001u, 0x7fc00001u, 0x7fc00001u, 0x7f800001u, + 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u}, + {0x7fc00001u, 0x7fc00000u, 0x7fc00001u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00001u, 0x7fc00001u, 0x7fc00000u, + 0x00000001u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u}, + {0x7fc00123u, 0x7f800001u, 0x7fc00123u, 0x7f800001u, 0x7f800001u, 0x7f800001u, 0x7fc00123u, 0x7fc00123u, 0x7f800001u, + 0x00000123u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000123u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000123u, + 0x7ff80000u, 0x00000123u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u}, + {0x7fc00001u, 0x7f800123u, 0x7fc00001u, 0x7f800123u, 0x7f800123u, 0x7f800123u, 0x7fc00001u, 0x7fc00001u, 0x7f800123u, + 0x00000001u, 0x7ff80000u, 0x00000123u, 0x7ff00000u, 0x00000001u, 0x7ff80000u, 0x00000123u, 0x7ff00000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000123u, 0x7ff00000u, 0x00000123u, 0x7ff00000u, 0x00000123u, 0x7ff00000u}, + {0x7fc00001u, 0xff800001u, 0x7fc00001u, 0xff800001u, 0xff800001u, 0xff800001u, 0x7fc00001u, 0x7fc00001u, 0xff800001u, + 0x00000001u, 0x7ff80000u, 0x00000001u, 0xfff00000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0xfff00000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0xfff00000u, 0x00000001u, 0xfff00000u, 0x00000001u, 0xfff00000u}, + {0xffc00001u, 0x7f800001u, 0xffc00001u, 0x7f800001u, 0x7f800001u, 0x7f800001u, 0xffc00001u, 0xffc00001u, 0x7f800001u, + 0x00000001u, 0xfff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0xfff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, + 0xfff80000u, 0x00000001u, 0xfff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u}, + {0x7fc00001u, 0x00000001u, 0x7fc00001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x7fc00001u, 0x7fc00001u, 0x00000001u, + 0x00000001u, 0x7ff80000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x00000000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u}, + {0x7fc00001u, 0x7f800001u, 0x7fc00001u, 0x7f800001u, 0x7f800001u, 0x7f800001u, 0x7fc00001u, 0x7fc00001u, 0x7f800001u, + 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u}, + {0x7fc00001u, 0x7f800000u, 0x7fc00001u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7fc00001u, 0x7fc00001u, 0x7f800000u, + 0x00000001u, 0x7ff80000u, 0x00000000u, 0x7ff00000u, 0x00000001u, 0x7ff80000u, 0x00000000u, 0x7ff00000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0x7ff00000u}, + {0x7fc00001u, 0x7f800001u, 0x7fc00001u, 0x7f800001u, 0x7f800001u, 0x7f800001u, 0x7fc00001u, 0x7fc00001u, 0x7f800001u, + 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u}, + {0x7fc00001u, 0x00000000u, 0x7fc00001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x7fc00001u, 0x7fc00001u, 0x00000000u, + 0x00000001u, 0x7ff80000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x7ff80000u, 0x00000000u, 0x00000000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x7fc00001u, 0x7f800001u, 0x7fc00001u, 0x7f800001u, 0x7f800001u, 0x7f800001u, 0x7fc00001u, 0x7fc00001u, 0x7f800001u, + 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, + 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u} +}; + +constexpr const char* Names[18] = { + "ds_min_rtn_f32 memory", + "ds_min_rtn_f32 returned", + "ds_max_rtn_f32 memory", + "ds_max_rtn_f32 returned", + "ds_cmpst_rtn_f32 memory", + "ds_cmpst_rtn_f32 returned", + "ds_min_f32 memory", + "ds_max_f32 memory", + "ds_cmpst_f32 memory", + "ds_min_rtn_f64 memory", + "ds_min_rtn_f64 returned", + "ds_max_rtn_f64 memory", + "ds_max_rtn_f64 returned", + "ds_min_f64 memory", + "ds_max_f64 memory", + "ds_cmpst_rtn_f64 memory", + "ds_cmpst_rtn_f64 returned", + "ds_cmpst_f64 memory" +}; + +constexpr std::uint32_t Columns64 = 9; + +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; +} + +const char* ColumnName(std::uint32_t column) { + return column < Columns64 ? Names[column] : Names[Columns64 + (column - Columns64) / 2u]; +} + +void Run(AgcDriver::VulkanDevice& device, 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); + 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}, 64u, {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(const std::uint32_t (&expected)[32][Columns], const char* mode) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t* out = &Output[tid * Results]; + for (std::uint32_t column = 0; column < Columns; ++column) { + const std::uint32_t actual = out[column < Columns64 ? column : column + 1u]; + Require(actual == expected[tid][column], std::string("lds float atomic modes: ") + mode + " lane " + std::to_string(tid) + " " + ColumnName(column) + + (column >= Columns64 && (column - Columns64) % 2u ? " high" : column >= Columns64 ? " low" : "") + " is " + Hex(actual) + ", expected " + Hex(expected[tid][column])); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device, std::nullopt); + Check(ExpectedTitles, "no float mode"); + Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}); + Check(ExpectedTitles, "FLOAT_MODE 0xc0"); + Run(*device, ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}); + Check(ExpectedKept, "FLOAT_MODE 0xf0"); + Run(*device, ShaderRecompiler::ShaderFloatMode{0x00u, true, false, false}); + Check(ExpectedFlushed, "FLOAT_MODE 0x00"); + Run(*device, ShaderRecompiler::ShaderFloatMode{0x50u, true, false, false}); + Check(ExpectedKept, "FLOAT_MODE 0x50"); + Run(*device, ShaderRecompiler::ShaderFloatMode{0xa0u, true, false, false}); + Check(ExpectedFlushed, "FLOAT_MODE 0xa0"); + std::puts("lds float atomic modes tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LdsMaskWaterfall.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LdsMaskWaterfall.cpp new file mode 100644 index 0000000000..eb4f2499b6 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LdsMaskWaterfall.cpp @@ -0,0 +1,140 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Lanes = 64; +constexpr std::uint32_t Slots = 16; +constexpr std::uint32_t Stride = 4; +constexpr std::uint32_t Guard = 4096; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array WaterfallCode{ + 0x34240083, 0x7e040280, 0x7e060280, 0xd9340000, 0x00000212, 0xbf8cc07f, 0x16080085, 0x3608088f, + 0x34080883, 0xd6ff0002, 0x00010300, 0xd9280000, 0x00000204, 0xbf8cc07f, 0xd9d80000, 0x06000012, + 0xbf8cc07f, 0xd4e50002, 0x00020c80, 0x7e140280, 0x7e160280, 0xbe8a0380, 0xbefe0400, 0xbf138002, + 0xbf840016, 0xbe8b1402, 0xbe821c0b, 0x8f0c830b, 0x7e18020c, 0xd9d80000, 0x0e00000c, 0xbf8cc07f, + 0x7dca1c80, 0xbf86fff5, 0x7e1c050e, 0x7e1e050f, 0xbe90140e, 0x8f921081, 0x3a1c1c12, 0x3a1e1e13, + 0x4a141481, 0x4a161610, 0x800a810a, 0xbf08ff0a, 0x00001000, 0xbf850001, 0xbf82fff1, 0xbefe04c1, + 0x7e1a020a, 0xe0702000, 0x80010a00, 0xe0702004, 0x80010b00, 0xe0702008, 0x80010d00, 0xe070200c, + 0x80010600, 0xbf810000, +}; + +alignas(256) constexpr std::array MaskedOrCode{ + 0x34240083, 0x7e040280, 0x7e060280, 0xd9340000, 0x00000212, 0xbf8cc07f, 0x3608008f, 0x34080883, + 0xd6ff0002, 0x00010300, 0xbefe0400, 0xd9280000, 0x00000204, 0xbf8cc07f, 0xbefe04c1, 0xd9d80000, + 0x06000012, 0xbf8cc07f, 0xe0702000, 0x80010600, 0xe0702004, 0x80010700, 0xbf810000, +}; + +std::array BufferDescriptor(const void* data, std::uint32_t records) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | ((Stride * 4u) << 16u), records, 0x11016facu}; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, std::uint64_t exec, bool spanned) { + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + userData[0] = static_cast(exec); + userData[1] = static_cast(exec >> 32u); + const auto output = BufferDescriptor(Output.data(), Lanes); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const auto address = reinterpret_cast(code.data()); + const std::array memory{{{address, std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Lanes, 1, 1}, Lanes * 2u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, address, code, 0, {}}, + {Lanes, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.target.spannedWaves = spanned; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + Require(result.fullSubgroups == spanned, spanned ? "the program did not span two host subgroups" : "the program spans host subgroups without APS5_SPANNED_WAVE"); + device.Dispatch(result, 1, 1, 1, {}, address); + device.WaitIdle(); +} + +void Check(std::uint64_t exec, bool spanned) { + std::array slots{}; + for (std::uint32_t lane = 0; lane < Lanes; ++lane) slots[(lane * 5u) & (Slots - 1u)] |= std::uint64_t{1} << lane; + std::uint32_t iterations = 0; + std::uint32_t indexSum = 0; + for (std::uint32_t slot = 0; slot < Slots; ++slot) { + for (auto mask = slots[slot]; mask != 0; mask &= mask - 1) { + ++iterations; + indexSum += static_cast(std::countr_zero(mask)); + } + } + char name[64]; + std::snprintf(name, sizeof(name), "%s, exec 0x%016llx", spanned ? "spanned" : "two-lane", static_cast(exec)); + for (std::uint32_t lane = 0; lane < Lanes; ++lane) { + const bool active = ((exec >> lane) & 1u) != 0; + const std::array expected{active ? iterations : 0u, active ? indexSum : 0u, iterations, static_cast(slots[lane])}; + const std::array fields{"inner iterations", "bit index sum", "wave iterations", "own slot low word"}; + for (std::uint32_t field = 0; field < Stride; ++field) { + const auto actual = Output[lane * Stride + field]; + Require(actual != Guard + 1u || field != 2u, std::string(name) + ": the waterfall ran past " + std::to_string(Guard) + " iterations"); + Require(actual == expected[field], std::string(name) + ": lane " + std::to_string(lane) + " " + fields[field] + " is " + std::to_string(actual) + ", expected " + std::to_string(expected[field])); + } + } +} + +void CheckMaskedOr(std::uint64_t exec, bool spanned) { + std::array slots{}; + for (std::uint32_t lane = 0; lane < Lanes; ++lane) { + if (((exec >> lane) & 1u) != 0) slots[lane & (Slots - 1u)] |= std::uint64_t{1} << lane; + } + char name[64]; + std::snprintf(name, sizeof(name), "%s masked ds_or_b64, exec 0x%016llx", spanned ? "spanned" : "two-lane", static_cast(exec)); + for (std::uint32_t lane = 0; lane < Lanes; ++lane) { + const std::array expected{static_cast(slots[lane]), static_cast(slots[lane] >> 32u)}; + for (std::uint32_t word = 0; word < expected.size(); ++word) { + const auto actual = Output[lane * Stride + word]; + Require(actual == expected[word], std::string(name) + ": lane " + std::to_string(lane) + " slot word " + std::to_string(word) + " is " + std::to_string(actual) + ", expected " + std::to_string(expected[word])); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (device->Target().subgroupSize < 32) { + std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize); + return VulkanTestSkipped; + } + const bool spannable = device->Target().subgroupSize == 32u && device->ComputeFullSubgroups(); + if (!spannable) std::puts("spanned mode skipped: the device has no full 32-lane compute subgroups"); + for (const bool spanned : {false, true}) { + if (spanned && !spannable) continue; + for (const std::uint64_t exec : {~std::uint64_t{0}, std::uint64_t{0xaaaaaaaaaaaaaaaa}, std::uint64_t{0xffff000000000000}, std::uint64_t{0x000000000000ffff}, std::uint64_t{1} << 63u, std::uint64_t{1}, std::uint64_t{0x0000000100000000}}) { + Run(*device, WaterfallCode, exec, spanned); + Check(exec, spanned); + } + for (const std::uint64_t exec : {~std::uint64_t{0}, std::uint64_t{0x00000000ffffffff}, std::uint64_t{0xffffffff00000000}, std::uint64_t{1} << 40u, std::uint64_t{0x8000000000000001}}) { + Run(*device, MaskedOrCode, exec, spanned); + CheckMaskedOr(exec, spanned); + } + } + std::printf("LDS mask waterfall tests passed (two-lane%s)\n", spannable ? " and spanned" : ""); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp new file mode 100644 index 0000000000..1f8032e93c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp @@ -0,0 +1,378 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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; +constexpr std::uint32_t LdsDwords = 4; +alignas(256) std::array Output{}; +alignas(256) std::array Input = [] { + std::array rows{}; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + rows[tid * 4u] = 0xffffffffu; + rows[tid * 4u + 2u] = 1u; + } + return rows; +}(); + +template +constexpr auto Unrolled(const std::array& head, const std::array& body, const std::array& tail) { + std::array code{}; + std::size_t next = 0; + for (const auto word : head) code[next++] = word; + for (std::uint32_t iteration = 0; iteration < Count; ++iteration) { + for (const auto word : body) code[next++] = word; + } + for (const auto word : tail) code[next++] = word; + return code; +} + +alignas(256) constexpr auto UniformCode = Unrolled( + std::to_array({ + 0x34100083, + 0x7e020280, + 0x7e0402c1, + 0x7e060280, + 0x7e080281, + 0x7e0c0280, + 0x7e0e0280, + 0xd9340000, 0x00000601, + 0xbf8cc07f, + 0xbf8a0000, + }), + std::to_array({ + 0xd9000000, 0x00000201, + 0xd8000004, 0x00000401, + 0xbf8cc07f, + }), + std::to_array({ + 0xbf8cc07f, + 0xbf8a0000, + 0xd9d80000, 0x0a000001, + 0xbf8cc07f, + 0xe0741000, 0x80010a08, + 0xbf810000, + })); + +alignas(256) constexpr auto DivergentCode = Unrolled( + std::to_array({ + 0x34100083, + 0x7e020280, + 0x7e0402c1, + 0x7e060280, + 0x7e080281, + 0x7e0c0280, + 0x7e0e0280, + 0xd9340000, 0x00000601, + 0xbf8cc07f, + 0xbf8a0000, + 0xbe89037e, + }), + std::to_array({ + 0xbefe03ff, 0x55555555, + 0xd9000000, 0x00000201, + 0xbefe03ff, 0xaaaaaaaa, + 0xd8000004, 0x00000401, + 0xbf8cc07f, + }), + std::to_array({ + 0xbefe0309, + 0xbf8cc07f, + 0xbf8a0000, + 0xd9d80000, 0x0a000001, + 0xbf8cc07f, + 0xe0741000, 0x80010a08, + 0xbf810000, + })); + +alignas(256) constexpr auto ParityCode = Unrolled<1>( + std::to_array({ + 0x34100083, + 0x7e020280, + 0x7e0402c1, + 0x7e060280, + 0x7e080281, + 0x7e0c0280, + 0x7e0e0280, + 0xd9340000, 0x00000601, + 0xbf8cc07f, + 0xbf8a0000, + 0xbe89037e, + }), + std::to_array({ + 0xbefe03ff, 0x55555555, + 0xd9000000, 0x00000201, + 0xbf8cc07f, + 0xbefe03ff, 0xaaaaaaaa, + 0xd8000004, 0x00000401, + 0xbf8cc07f, + }), + std::to_array({ + 0xbefe0309, + 0xbf8cc07f, + 0xbf8a0000, + 0xd9d80000, 0x0a000001, + 0xbf8cc07f, + 0xe0741000, 0x80010a08, + 0xbf810000, + })); + +alignas(256) constexpr auto LoadedParityCode = Unrolled<1>( + std::to_array({ + 0x34100083, + 0x340a0084, + 0x7e020280, + 0xe0381000, 0x80000c05, + 0x7e0c0280, + 0x7e0e0280, + 0xd9340000, 0x00000601, + 0xbf8c0070, + 0xbf8a0000, + 0xbe89037e, + }), + std::to_array({ + 0xbefe03ff, 0x55555555, + 0xd9000000, 0x00000c01, + 0xbf8cc07f, + 0xbefe03ff, 0xaaaaaaaa, + 0xd8000004, 0x00000e01, + 0xbf8cc07f, + }), + std::to_array({ + 0xbefe0309, + 0xbf8cc07f, + 0xbf8a0000, + 0xd9d80000, 0x0a000001, + 0xbf8cc07f, + 0xe0741000, 0x80010a08, + 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}; +} + +std::string Hex(std::uint64_t value) { + char text[24]; + std::snprintf(text, sizeof(text), "0x%016llx", static_cast(value)); + return text; +} + +void Check(const char* name, std::uint64_t operations, std::uint32_t run) { + const std::uint64_t expected = operations * 0xffffffffull + (operations << 32u); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint64_t actual = Output[tid * 2u] | (static_cast(Output[tid * 2u + 1u]) << 32u); + Require(actual == expected, std::string("lds mixed width atomics ") + name + " run " + std::to_string(run) + ": lane " + std::to_string(tid) + " read " + Hex(actual) + ", expected " + Hex(expected) + " after " + std::to_string(operations) + " ds_add_u64 and " + std::to_string(operations) + " ds_add_u32 on one slot"); + } +} + +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}; + 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, 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 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); + Run(*device, LoadedParityCode, "split lanes with per-lane operands", Threads / 2u, Repeats); + std::puts("lds mixed width atomics 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/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/LogNearOne.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LogNearOne.cpp new file mode 100644 index 0000000000..8821426321 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LogNearOne.cpp @@ -0,0 +1,252 @@ +#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 = 4; +constexpr std::uint32_t Columns = 2; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e140280u, 0x7e160280u, 0x7e180280u, 0x7e1a0280u, + 0x7e144f04u, 0x7e16af05u, 0xe0781000u, 0x80010a01u, 0xbf810000u, +}; + +constexpr std::uint32_t Rows[Threads][4] = { + {0x3fc00000u, 0x00003e00u, 0x00000000u, 0x00000000u}, + {0x3f000000u, 0x00003800u, 0x00000000u, 0x00000000u}, + {0x3fa00000u, 0x00003d00u, 0x00000000u, 0x00000000u}, + {0x3f400000u, 0x00003a00u, 0x00000000u, 0x00000000u}, + {0x3f900000u, 0x00003c80u, 0x00000000u, 0x00000000u}, + {0x3f600000u, 0x00003b00u, 0x00000000u, 0x00000000u}, + {0x3f880000u, 0x00003c40u, 0x00000000u, 0x00000000u}, + {0x3f700000u, 0x00003b80u, 0x00000000u, 0x00000000u}, + {0x3f840000u, 0x00003c20u, 0x00000000u, 0x00000000u}, + {0x3f780000u, 0x00003bc0u, 0x00000000u, 0x00000000u}, + {0x3f820000u, 0x00003c10u, 0x00000000u, 0x00000000u}, + {0x3f7c0000u, 0x00003be0u, 0x00000000u, 0x00000000u}, + {0x3f810000u, 0x00003c08u, 0x00000000u, 0x00000000u}, + {0x3f7e0000u, 0x00003bf0u, 0x00000000u, 0x00000000u}, + {0x3f808000u, 0x00003c04u, 0x00000000u, 0x00000000u}, + {0x3f7f0000u, 0x00003bf8u, 0x00000000u, 0x00000000u}, + {0x3f804000u, 0x00003c02u, 0x00000000u, 0x00000000u}, + {0x3f7f8000u, 0x00003bfcu, 0x00000000u, 0x00000000u}, + {0x3f802000u, 0x00003c01u, 0x00000000u, 0x00000000u}, + {0x3f7fc000u, 0x00003bfeu, 0x00000000u, 0x00000000u}, + {0x3f801000u, 0x00003c00u, 0x00000000u, 0x00000000u}, + {0x3f7fe000u, 0x00003bffu, 0x00000000u, 0x00000000u}, + {0x3f800800u, 0x00003c00u, 0x00000000u, 0x00000000u}, + {0x3f7ff000u, 0x00003bffu, 0x00000000u, 0x00000000u}, + {0x3f800400u, 0x00003c00u, 0x00000000u, 0x00000000u}, + {0x3f7ff800u, 0x00003bffu, 0x00000000u, 0x00000000u}, + {0x3f800200u, 0x00003c00u, 0x00000000u, 0x00000000u}, + {0x3f7ffc00u, 0x00003bffu, 0x00000000u, 0x00000000u}, + {0x3f800100u, 0x00003c00u, 0x00000000u, 0x00000000u}, + {0x3f7ffe00u, 0x00003bffu, 0x00000000u, 0x00000000u}, + {0x3f800080u, 0x00003c00u, 0x00000000u, 0x00000000u}, + {0x3f7fff00u, 0x00003bffu, 0x00000000u, 0x00000000u}, + {0x3f800040u, 0x00003c00u, 0x00000000u, 0x00000000u}, + {0x3f7fff80u, 0x00003bffu, 0x00000000u, 0x00000000u}, + {0x3f800020u, 0x00003c00u, 0x00000000u, 0x00000000u}, + {0x3f7fffc0u, 0x00003bffu, 0x00000000u, 0x00000000u}, + {0x3f800010u, 0x00003c00u, 0x00000000u, 0x00000000u}, + {0x3f7fffe0u, 0x00003bffu, 0x00000000u, 0x00000000u}, + {0x3f800008u, 0x00003c00u, 0x00000000u, 0x00000000u}, + {0x3f7ffff0u, 0x00003bffu, 0x00000000u, 0x00000000u}, + {0x3f800004u, 0x00003c00u, 0x00000000u, 0x00000000u}, + {0x3f7ffff8u, 0x00003bffu, 0x00000000u, 0x00000000u}, + {0x3f800002u, 0x00003c00u, 0x00000000u, 0x00000000u}, + {0x3f7ffffcu, 0x00003bffu, 0x00000000u, 0x00000000u}, + {0x3f800001u, 0x00003c00u, 0x00000000u, 0x00000000u}, + {0x3f7ffffeu, 0x00003bffu, 0x00000000u, 0x00000000u}, + {0x3f800000u, 0x00003c00u, 0x00000000u, 0x00000000u}, + {0x3f7fffffu, 0x00003bffu, 0x00000000u, 0x00000000u}, + {0x3f800000u, 0x00003c00u, 0x00000000u, 0x00000000u}, + {0x3f600000u, 0x00003b00u, 0x00000000u, 0x00000000u}, + {0x3f900000u, 0x00003c80u, 0x00000000u, 0x00000000u}, + {0x3f666666u, 0x00003b33u, 0x00000000u, 0x00000000u}, + {0x3f8ccccdu, 0x00003c66u, 0x00000000u, 0x00000000u}, + {0x3f000000u, 0x00003800u, 0x00000000u, 0x00000000u}, + {0x40000000u, 0x00004000u, 0x00000000u, 0x00000000u}, + {0x40400000u, 0x00004200u, 0x00000000u, 0x00000000u}, + {0x41200000u, 0x00004900u, 0x00000000u, 0x00000000u}, + {0x3dcccccdu, 0x00002e66u, 0x00000000u, 0x00000000u}, + {0x3a83126fu, 0x00001419u, 0x00000000u, 0x00000000u}, + {0x447a0000u, 0x000063d0u, 0x00000000u, 0x00000000u}, + {0x00800000u, 0x00000400u, 0x00000000u, 0x00000000u}, + {0x7f7fffffu, 0x00007bffu, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0xbf800000u, 0x0000bc00u, 0x00000000u, 0x00000000u} +}; + +constexpr std::uint32_t Expected[Threads][Columns] = { + {0x3f15c01au, 0x000038aeu}, + {0xbf800000u, 0x0000bc00u}, + {0x3ea4d3c2u, 0x00003527u}, + {0xbed47fccu, 0x0000b6a4u}, + {0x3e2e00d2u, 0x00003170u}, + {0xbe4544c0u, 0x0000b22au}, + {0x3db31fb8u, 0x00002d99u}, + {0xbdbeb025u, 0x0000adf6u}, + {0x3d35d69cu, 0x000029afu}, + {0xbd3b9ca6u, 0x0000a9ddu}, + {0x3cb73cb4u, 0x000025bau}, + {0xbcba1f74u, 0x0000a5d1u}, + {0x3c37f286u, 0x000021c0u}, + {0xbc3963ddu, 0x0000a1cbu}, + {0x3bb84e24u, 0x00001dc2u}, + {0xbbb906ceu, 0x00009dc8u}, + {0x3b387c20u, 0x000019c4u}, + {0xbb38d875u, 0x000099c7u}, + {0x3ab8932au, 0x000015c5u}, + {0xbab8c155u, 0x000095c6u}, + {0x3a389eb2u, 0x00000000u}, + {0xba38b5c7u, 0x000091c6u}, + {0x39b8a476u, 0x00000000u}, + {0xb9b8b001u, 0x000091c6u}, + {0x3938a759u, 0x00000000u}, + {0xb938ad1eu, 0x000091c6u}, + {0x38b8a8cau, 0x00000000u}, + {0xb8b8abadu, 0x000091c6u}, + {0x3838a983u, 0x00000000u}, + {0xb838aaf4u, 0x000091c6u}, + {0x37b8a9dfu, 0x00000000u}, + {0xb7b8aa98u, 0x000091c6u}, + {0x3738aa0du, 0x00000000u}, + {0xb738aa6au, 0x000091c6u}, + {0x36b8aa24u, 0x00000000u}, + {0xb6b8aa53u, 0x000091c6u}, + {0x3638aa30u, 0x00000000u}, + {0xb638aa47u, 0x000091c6u}, + {0x35b8aa36u, 0x00000000u}, + {0xb5b8aa41u, 0x000091c6u}, + {0x3538aa39u, 0x00000000u}, + {0xb538aa3eu, 0x000091c6u}, + {0x34b8aa3au, 0x00000000u}, + {0xb4b8aa3du, 0x000091c6u}, + {0x3438aa3bu, 0x00000000u}, + {0xb438aa3cu, 0x000091c6u}, + {0x00000000u, 0x00000000u}, + {0xb3b8aa3cu, 0x000091c6u}, + {0x00000000u, 0x00000000u}, + {0xbe4544c0u, 0x0000b22au}, + {0x3e2e00d2u, 0x00003170u}, + {0xbe1ba6b5u, 0x0000b0deu}, + {0x3e0ccdbbu, 0x00003062u}, + {0xbf800000u, 0x0000bc00u}, + {0x3f800000u, 0x00003c00u}, + {0x3fcae00du, 0x00003e57u}, + {0x40549a78u, 0x000042a5u}, + {0xc0549a79u, 0x0000c2a5u}, + {0xc11f73dbu, 0x0000c8fcu}, + {0x411f73dau, 0x000048fcu}, + {0xc2fc0000u, 0x0000cb00u}, + {0x42ffffffu, 0x00004c00u}, + {0xff800000u, 0x0000fc00u}, + {0xffc00000u, 0x0000fe00u} +}; + +constexpr const char* Names[Columns] = {"v_log_f32", "v_log_f16"}; +constexpr std::uint32_t Tolerance[Threads] = {12u, 12u, 12u, 12u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 6u, 12u, 12u, 12u, 12u, 12u, 12u, 12u, 2u, 2u, 2u, 2u}; + +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 Ordered(std::uint32_t bits, std::uint32_t width) { + const std::uint32_t sign = 1u << (width - 1u); + return (bits & sign) != 0u ? sign - (bits & ~sign) : bits | sign; +} + +std::uint32_t Distance(std::uint32_t actual, std::uint32_t expected, std::uint32_t width) { + const std::uint32_t a = Ordered(actual, width); + const std::uint32_t e = Ordered(expected, width); + return a > e ? a - e : e - a; +} + +bool IsPinned(std::uint32_t bits, std::uint32_t width) { + const std::uint32_t magnitude = width == 16u ? bits & 0x7fffu : bits & 0x7fffffffu; + const std::uint32_t infinity = width == 16u ? 0x7c00u : 0x7f800000u; + return magnitude == 0u || magnitude >= infinity; +} + +void Run(AgcDriver::VulkanDevice& device) { + 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::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 = ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t* out = &Output[tid * Results]; + for (std::uint32_t column = 0; column < Columns; ++column) { + const std::uint32_t width = column == 0u ? 32u : 16u; + const std::uint32_t mask = width == 16u ? 0xffffu : 0xffffffffu; + const std::uint32_t actual = out[column] & mask; + const std::uint32_t expected = Expected[tid][column] & mask; + const std::string name = "log near one: lane " + std::to_string(tid) + " " + Names[column] + " of " + Hex(Rows[tid][column]) + " is " + Hex(actual) + ", expected " + Hex(expected); + if (IsPinned(expected, width)) { + Require(actual == expected, name); + continue; + } + Require(Distance(actual, expected, width) <= Tolerance[tid], name + " (tolerance " + std::to_string(Tolerance[tid]) + " ulp)"); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device); + Check(); + std::puts("log near one 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/MacLegacy.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MacLegacy.cpp new file mode 100644 index 0000000000..2c31787312 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MacLegacy.cpp @@ -0,0 +1,137 @@ +#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 Inputs = 8; +constexpr std::uint32_t Results = 8; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array MacLegacyCode{ + 0x34020083, 0x34060083, 0xe0302000, 0x80000401, 0xe0302004, 0x80000501, 0xe0302008, 0x80000601, + 0xbf8c3f70, 0x7e140306, 0x0c140b04, 0x7e160306, 0xd506000b, 0x20020b04, 0x7e180306, 0xd506030c, + 0x40020b04, 0x7e1a0306, 0x0c1a0a80, 0xe0702000, 0x80010a03, 0xe0702004, 0x80010b03, 0xe0702008, + 0x80010c03, 0xe070200c, 0x80010d03, 0xbf810000, +}; + +struct Row { + std::uint32_t a, b, c; + std::array expected; +}; + +constexpr std::array Rows{{ + {0x00000000u, 0x7f800000u, 0x3f800000u, {0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u}}, + {0x80000000u, 0x7f800000u, 0xc0200000u, {0xc0200000u, 0xc0200000u, 0xc0200000u, 0xc0200000u}}, + {0x7f800000u, 0x00000000u, 0x3f400000u, {0x3f400000u, 0x3f400000u, 0x3f400000u, 0x3f400000u}}, + {0xff800000u, 0x80000000u, 0x40400000u, {0x40400000u, 0x40400000u, 0x40400000u, 0x40400000u}}, + {0x00000000u, 0x7fc00000u, 0x3fc00000u, {0x3fc00000u, 0x3fc00000u, 0x3fc00000u, 0x3fc00000u}}, + {0x7fc00000u, 0x80000000u, 0xc0800000u, {0xc0800000u, 0xc0800000u, 0xc0800000u, 0xc0800000u}}, + {0x00000000u, 0x40000000u, 0x7f800000u, {0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u}}, + {0x80000000u, 0xc0400000u, 0xff800000u, {0xff800000u, 0xff800000u, 0xff800000u, 0xff800000u}}, + {0x00000000u, 0x3f800000u, 0x7fc00000u, {0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u}}, + {0x3fc00000u, 0x40100000u, 0xbf400000u, {0x40280000u, 0xc0840000u, 0xc0840000u, 0xbf400000u}}, + {0xc0c00000u, 0x3f000000u, 0x41200000u, {0x40e00000u, 0x41500000u, 0x40e00000u, 0x41200000u}}, + {0x7f800000u, 0x40000000u, 0x3f800000u, {0x7f800000u, 0xff800000u, 0xff800000u, 0x3f800000u}}, + {0x7f800000u, 0xbf800000u, 0x7f800000u, {0xffc00000u, 0x7f800000u, 0xffc00000u, 0x7f800000u}}, + {0x7fc00000u, 0x3f800000u, 0x3f800000u, {0x7fc00000u, 0xffc00000u, 0x7fc00000u, 0x3f800000u}}, + {0x40400000u, 0x40800000u, 0xc1400000u, {0x00000000u, 0xc1c00000u, 0xc1c00000u, 0xc1400000u}}, + {0x7f7fffffu, 0x40000000u, 0x3f800000u, {0x7f800000u, 0xff800000u, 0xff800000u, 0x3f800000u}}, + {0x3ec00000u, 0xc1480000u, 0x42c88000u, {0x42bf2000u, 0x42d1e000u, 0x42bf2000u, 0x42c88000u}}, + {0xc4800000u, 0xbd800000u, 0xc2800000u, {0x00000000u, 0xc3000000u, 0xc3000000u, 0xc2800000u}}, + {0x40e00000u, 0x41100000u, 0x3f000000u, {0x427e0000u, 0xc27a0000u, 0xc27a0000u, 0x3f000000u}}, + {0x80000000u, 0x80000000u, 0x40a00000u, {0x40a00000u, 0x40a00000u, 0x40a00000u, 0x40a00000u}}, + {0x3f800800u, 0x3f800800u, 0xbf800000u, {0x3a000000u, 0xc0000800u, 0xc0000800u, 0xbf800000u}}, + {0x3f800001u, 0x3f7fffffu, 0xbf800000u, {0x00000000u, 0xc0000000u, 0xc0000000u, 0xbf800000u}}, + {0x3faaaaabu, 0x40400000u, 0xc0800000u, {0x00000000u, 0xc1000000u, 0xc1000000u, 0xc0800000u}}, + {0x7f7fffffu, 0x40000000u, 0xff7fffffu, {0x7f800000u, 0xff800000u, 0xff800000u, 0xff7fffffu}}, + {0x00800000u, 0x3f000000u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x00000001u, 0x4b000000u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x00000001u, 0x3f800000u, 0x80000000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x80000001u, 0x3f800000u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x00800000u, 0x3f800000u, 0x80400000u, {0x00800000u, 0x80800000u, 0x80800000u, 0x00000000u}}, + {0x3f800000u, 0x00400000u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x7f812345u, 0x3f800000u, 0x3f800000u, {0x7f812345u, 0xff812345u, 0x7f812345u, 0x3f800000u}}, + {0x3f800000u, 0x3f800000u, 0x7f812345u, {0x7f812345u, 0x7f812345u, 0x7f812345u, 0x7f812345u}}, +}}; + +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) { + Input.fill(0u); + for (std::uint32_t tid = 0; tid < Rows.size(); ++tid) { + const auto& row = Rows[tid]; + const std::array words{row.a, row.b, row.c}; + std::copy(words.begin(), words.end(), Input.begin() + tid * Inputs); + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size())); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size())); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(MacLegacyCode); + 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; +} + +void Check() { + constexpr std::array names{ + "v_mac_legacy_f32", "v_mac_legacy_f32_e64 -src0", "v_mac_legacy_f32_e64 |src0| -|src1|", "v_mac_legacy_f32 src0=0", + }; + for (std::uint32_t tid = 0; tid < Rows.size(); ++tid) { + const auto& row = Rows[tid]; + for (std::uint32_t j = 0; j < row.expected.size(); ++j) { + const auto actual = Output[tid * Results + j]; + Require(actual == row.expected[j], std::string("mac legacy: lane ") + std::to_string(tid) + " (" + Hex(row.a) + ", " + Hex(row.b) + ", " + Hex(row.c) + ") " + names[j] + " is " + Hex(actual) + ", expected " + Hex(row.expected[j])); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device); + Check(); + std::puts("mac legacy 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/MaskedGlobalStore.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MaskedGlobalStore.cpp new file mode 100644 index 0000000000..83dc78c5bf --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MaskedGlobalStore.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 = 64 * 1024; + +alignas(256) constexpr std::array MaskedStoreCode{0xd7460000u, 0x04010c06u, 0x34000084u, 0xdc388000u, 0x04020000u, 0xbf8c3f70u, 0xd7710008u, 0x00120805u, 0xd7710009u, 0x00120a05u, 0xd771000au, 0x00120c05u, 0xd771000bu, 0x00120e05u, 0xdc788000u, 0x00020800u, 0xbf810000u}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "masked global store: 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::uint32_t value, std::uint32_t keep) { + constexpr std::uint32_t groups = 64; + constexpr std::size_t words = groups * 64u * 4u; + auto* data = reinterpret_cast(guest.Data()); + for (std::size_t index = 0; index < BlockBytes / 4u; ++index) data[index] = static_cast(index * 0x9e3779b1u); + const auto address = static_cast(reinterpret_cast(guest.Data())); + const std::vector userData{0, 0, static_cast(address), static_cast(address >> 32u), value, keep}; + const std::span code(MaskedStoreCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{64, 1, 1}, 0, {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, groups, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + for (std::size_t index = 0; index < BlockBytes / 4u; ++index) { + const auto original = static_cast(index * 0x9e3779b1u); + const auto expected = index < words ? (original & keep) | value : original; + Require(data[index] == expected, "a masked global_store_dwordx4 left dword " + std::to_string(index) + " incorrect"); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock guest; + for (const auto waveSize : {64u, 32u}) { + Run(*device, guest, waveSize, 0u, 0x00000ff0u); + Run(*device, guest, waveSize, 0xfffff00fu, 0x00000ff0u); + } + std::puts("masked global 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/MemoryValueFill.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MemoryValueFill.cpp new file mode 100644 index 0000000000..eece8a90ba --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MemoryValueFill.cpp @@ -0,0 +1,180 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/DwordPatternFill.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::DriverDetail::KeysFillMemo; +using AgcDriver::DriverDetail::MatchMemoryValueBufferFill; + +alignas(256) constexpr std::array Code{0xd7460000u, 0x04010c08u, 0xf4201a82u, 0xfa000000u, 0xbf8cc07fu, 0x7e02026au, 0xe0102000u, 0x80000100u, 0xbf810000u}; +constexpr std::uint32_t Untouched = 0xcafebabeu; +constexpr std::uint32_t Format32Uint = 0x00014004u; +alignas(256) std::array Value{}; +alignas(256) std::array Output{}; + +void KeysFillMemoTests() { + using AgcDriver::Graphics::DccKeys; + const auto bytes = [](std::uint32_t byte) { const auto word = byte * 0x01010101u; return std::array{word, word, word, word}; }; + const std::array, 6> codes{{{0x00u, DccKeys::Clear0000}, {0x40u, DccKeys::Clear0001}, {0x80u, DccKeys::Clear1110}, {0xc0u, DccKeys::Clear1111}, {0x20u, DccKeys::ClearRegister}, {0xffu, DccKeys::Uncompressed}}}; + for (const auto& [byte, keys] : codes) { + Require(KeysFillMemo(bytes(byte), false, true) == keys, "a memory-value fill of a key code over keys that are no storage image's was not recorded"); + Require(KeysFillMemo(bytes(byte), true, false) == keys, "a fill of a key code over a storage image's keys was not recorded"); + Require(KeysFillMemo(bytes(byte), true, true) == keys, "a memory-value fill over a storage image's keys was not recorded"); + Require(!KeysFillMemo(bytes(byte), false, false), "a fill over keys that are no storage image's, other than a memory-value fill, was recorded"); + } + Require(!KeysFillMemo(bytes(0x12u), true, true), "a byte that is no key code was recorded"); + auto mixed = bytes(0x40u); + mixed[3] = 0x40404041u; + Require(!KeysFillMemo(mixed, true, true), "a pattern of two different words was recorded"); + mixed = bytes(0x40u); + mixed[0] = 0x40404000u; + Require(!KeysFillMemo(mixed, true, true), "a word of different bytes was recorded"); +} + +std::vector UserData(const std::uint32_t* output, std::uint32_t records, std::uint32_t valueRecords = 16, std::uint32_t valueStride = 0, std::uint32_t format = Format32Uint) { + const auto address = reinterpret_cast(output); + const auto value = reinterpret_cast(Value.data()); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (4u << 16u), records, format, static_cast(value), static_cast((value >> 32u) & 0xffffu) | (valueStride << 16u), valueRecords, Format32Uint}; +} + +void MatchTests() { + const ShaderRecompiler::ShaderComputeStageInfo compute{{64, 1, 1}, 0, {true, false, false}, false, 1}; + const std::array packet{0xc0031500u, 4, 1, 1, 0x41u}; + const auto data = UserData(Output.data(), 1024); + const auto fill = MatchMemoryValueBufferFill(packet, Code, data, compute); + Require(fill && fill->address == reinterpret_cast(Output.data()) && fill->bytes == 1024 && fill->valueAddress == reinterpret_cast(Value.data()), "the memory-value fill kernel was not taken as a fill of 256 dwords"); + for (const std::uint32_t format : {0x00015004u, 0x00016004u}) { + Require(MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 1024, 16, 0, format), compute).has_value(), "a 32-bit output format was refused"); + } + for (const std::uint32_t format : {0x0000d004u, 0x00027facu, 0x00038004u}) { + Require(!MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 1024, 16, 0, format), compute), "an output format whose elements are not one 32-bit value matched"); + } + Require(MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 1024, 1, 4), compute).has_value(), "a value descriptor of one 4-byte element was refused"); + Require(MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 1024, 4), compute).has_value(), "a value descriptor of 4 bytes was refused"); + Require(!MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 1024, 0), compute), "a value descriptor without records matched (the load returns 0)"); + Require(!MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 1024, 3), compute), "a value descriptor shorter than the loaded dword matched"); + Require(!MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 1024, 1, 2), compute), "a value descriptor of one 2-byte element matched"); + Require(!MatchMemoryValueBufferFill(packet, Code, UserData(Output.data() + 1, 1024), compute), "an unaligned destination matched"); + Require(!MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 102), compute), "a record count not filling 16-byte units matched"); + auto extra = data; + extra.push_back(0); + Require(!MatchMemoryValueBufferFill(packet, Code, extra, compute), "a different user SGPR count matched (the group id moves)"); + auto partial = packet; + partial[4] |= 0x20u; + Require(!MatchMemoryValueBufferFill(partial, Code, data, compute), "a partial dispatch matched"); + auto noGroup = compute; + noGroup.groupIdEnable[0] = false; + Require(!MatchMemoryValueBufferFill(packet, Code, data, noGroup), "a dispatch without the group id matched"); + auto altered = Code; + altered[5] ^= 1u; + Require(!MatchMemoryValueBufferFill(packet, altered, data, compute), "a different shader matched"); + const auto clamped = MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 64), compute); + Require(clamped && clamped->bytes == 256, "the fill exceeded the output descriptor's records"); +} + +std::vector Commands(const std::vector& data, std::uint32_t groups, std::uint32_t wave) { + std::vector words; + const auto registers = [&](std::uint32_t first, std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array shape{64, 1, 1}; + registers(0x207, shape); + const auto address = reinterpret_cast(Code.data()); + const std::array program{static_cast(address >> 8u), static_cast(address >> 40u)}; + registers(0x20c, program); + const std::array resource{(static_cast(data.size()) << 1u) | (1u << 7u)}; + registers(0x213, resource); + registers(0x240, data); + words.insert(words.end(), {0xc0031500u, groups, 1, 1, 0x41u | (wave == 32 ? 0x8000u : 0u)}); + return words; +} + +void Label(std::vector& words, std::uint32_t* target, std::uint32_t value) { + const auto address = reinterpret_cast(target); + words.insert(words.end(), {0xc0064900u, 0x514u, (1u << 29u) | (2u << 24u), static_cast(address), static_cast(address >> 32u), value, 0, 0}); +} + +void Submit(const std::vector& words) { + Packet packet{const_cast(words.data()), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output.data()), sizeof(Output)); +} + +void Expect(std::size_t first, std::size_t count, std::uint32_t value, const std::string& what) { + for (std::size_t i = 0; i < Output.size(); ++i) { + const auto expected = i >= first && i < first + count ? value : Untouched; + Require(Output[i] == expected, what + ": word " + std::to_string(i) + " is " + std::to_string(Output[i]) + ", expected " + std::to_string(expected)); + } +} + +void ExecuteTests() { + struct Case { + std::uint32_t value, first, records, groups, valueRecords, valueStride, stored; + const char* what; + }; + constexpr std::array cases{{ + {0xffffffffu, 0, 256, 4, 16, 0, 0xffffffffu, "a 16-byte aligned fill of every launched thread"}, + {0u, 0, 256, 2, 16, 0, 0u, "a fill shorter than its records"}, + {0x40404040u, 0, 1000, 4, 16, 0, 0x40404040u, "a fill longer than its launched threads"}, + {0x0c0c0c0cu, 0, 256, 4, 1, 4, 0x0c0c0c0cu, "a fill whose value descriptor holds one 4-byte element"}, + {0x12345678u, 0, 255, 4, 16, 0, 0x12345678u, "a fill whose size is not a multiple of 16 bytes"}, + {0x80808080u, 1, 256, 4, 16, 0, 0x80808080u, "a fill whose start is not 16-byte aligned"}, + {0x20202020u, 0, 0, 4, 16, 0, 0x20202020u, "an empty fill"}, + {0xc0c0c0c0u, 0, 256, 4, 0, 0, 0u, "a fill whose value descriptor has no records (the load returns 0)"}, + }}; + for (const auto wave : {32u, 64u}) { + for (const auto& item : cases) { + Output.fill(Untouched); + Value[0] = item.value; + Submit(Commands(UserData(Output.data() + item.first, item.records, item.valueRecords, item.valueStride), item.groups, wave)); + Expect(item.first, std::min(item.records, item.groups * 64u), item.stored, std::string(item.what) + " (wave " + std::to_string(wave) + ")"); + } + } + Output.fill(Untouched); + Value[0] = 0x11111111u; + auto words = Commands(UserData(Output.data(), 256), 4, 64); + Label(words, &Value[0], 0xc0c0c0c0u); + const auto again = Commands(UserData(Output.data(), 128), 2, 64); + words.insert(words.end(), again.begin(), again.end()); + Submit(words); + for (std::size_t i = 0; i < 256; ++i) { + Require(Output[i] == (i < 128 ? 0xc0c0c0c0u : 0x11111111u), "a fill did not read the value a preceding GPU label stored, at word " + std::to_string(i)); + } +} + +} + +int main() { + try { + KeysFillMemoTests(); + if (std::getenv("APS5_NO_FILL_HLE") == nullptr) MatchTests(); + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + ExecuteTests(); + AgcDriverShutdown_nid_postfix(); + std::puts("memory-value fill tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/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..3c7b2a9333 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MeshProvokingVertex.cpp @@ -0,0 +1,181 @@ +#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 + +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, 0}; +alignas(256) constexpr std::array, 3> 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} +}}; +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) { + 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, 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}); + 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(); + 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"); + } +} + +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(); + 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" << (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 1d7b3d44ab..972108e80a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/MixPrecision.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MixPrecision.cpp @@ -2,11 +2,14 @@ #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "Recompiler.hpp" #include "VulkanTestDevice.hpp" +#include #include #include #include #include #include +#include +#include #include #include @@ -31,6 +34,111 @@ alignas(256) constexpr std::array MixCode{ 0x80011103, 0xe0702020, 0x80011203, 0xbf810000, }; +alignas(256) constexpr std::array RoundingCode{ + 0x34020082, 0x34060084, 0xe0302000, 0x80000401, 0xe0302004, 0x80000501, 0xe0302008, 0x80000601, + 0x7e1402ff, 0xabcd0000, 0x7e1602ff, 0x0000abcd, 0x7e1802ff, 0xabcd0000, 0x7e1a02ff, 0xabcd0000, + 0x7e1c02ff, 0xabcd0000, 0x7e1e02ff, 0xabcd0000, 0x7e2002ff, 0xabcd0000, 0xbf8c3f70, 0xcc21000a, + 0x041a0b04, 0xcc22000b, 0x041a0b04, 0xcc21400c, 0x1c1a0b04, 0xcc21000d, 0x1c1a0b04, 0xcc21400e, + 0x041a0b04, 0xcc21400f, 0x0c1a0b04, 0xcc210010, 0x141a0b04, 0xe0702000, 0x80010a03, 0xe0702004, + 0x80010b03, 0xe0702008, 0x80010c03, 0xe070200c, 0x80010d03, 0xe0702010, 0x80010e03, 0xe0702014, + 0x80010f03, 0xe0702018, 0x80011003, 0xbf810000, +}; + +struct RoundingRow { + std::uint32_t a, b, c, result, expected; +}; + +constexpr std::array RoundingRows{{ + {0x000062e9u, 0x0000d159u, 0x000023ecu, 2u, 0xf89fu}, + {0xbfc00000u, 0x33800000u, 0x00800000u, 0u, 0x8001u}, + {0x3bec0000u, 0xc73c0000u, 0xb767d928u, 0u, 0xdd6bu}, + {0xb4e70000u, 0xc5500000u, 0x8d000000u, 0u, 0x15ddu}, + {0xcc750000u, 0xb0500000u, 0x2f800000u, 0u, 0x2a39u}, + {0xc7b30000u, 0x35500000u, 0x0d000000u, 0u, 0xac8bu}, + {0x36400000u, 0x4c414000u, 0xb3800000u, 0u, 0x5887u}, + {0x4ae00000u, 0x3604c000u, 0x00800000u, 0u, 0x4b43u}, + {0xcf840000u, 0x36fc0000u, 0x31054000u, 0u, 0xf80fu}, + {0x43320000u, 0x41580000u, 0xaf800000u, 0u, 0x68b1u}, + {0x38400000u, 0x34406b26u, 0x43ab9000u, 0u, 0x5d5du}, + {0x00007780u, 0x00001478u, 0x00000002u, 2u, 0x5031u}, + {0x0000e520u, 0x0000cc10u, 0x00000002u, 2u, 0x7535u}, + {0x00004138u, 0x0000e440u, 0x00000001u, 2u, 0xe98bu}, + {0x000020ccu, 0x0000d880u, 0x00000001u, 2u, 0xbd65u}, + {0x00004880u, 0x0000d81cu, 0x00000002u, 2u, 0xe49fu}, + {0x0000cad4u, 0x00005900u, 0x00008002u, 2u, 0xe845u}, + {0x00001836u, 0x0000b5b3u, 0x00003a9au, 2u, 0x3a99u}, + {0x000063f0u, 0x00005340u, 0x00008002u, 2u, 0x7b31u}, + {0x0000e700u, 0x0000cca4u, 0x00008100u, 2u, 0x780fu}, + {0x00000003u, 0x0000f89du, 0x0d000000u, 3u, 0x9eebu}, + {0x00009d20u, 0x000036a0u, 0x8d000000u, 3u, 0x983fu}, + {0x000008a5u, 0x0000b400u, 0x80800000u, 3u, 0x8253u}, + {0x00009400u, 0x00002935u, 0x8d000000u, 3u, 0x829bu}, + {0x0000c5c0u, 0x00004630u, 0xaf800000u, 3u, 0xd073u}, + {0x0000140du, 0x00000c40u, 0x42e11000u, 3u, 0x5709u}, + {0x00008800u, 0x0000b439u, 0x00800000u, 3u, 0x021du}, + {0x00006c80u, 0x00003c04u, 0x33800000u, 3u, 0x6c85u}, + {0x00002840u, 0x0000f018u, 0x33800000u, 3u, 0xdc59u}, + {0x4f200000u, 0x34d98000u, 0x00008100u, 4u, 0x643fu}, + {0xced08000u, 0x37900000u, 0x00008100u, 4u, 0xf755u}, + {0xb4b63000u, 0xce800000u, 0x00008100u, 4u, 0x5db1u}, + {0xca094000u, 0x38a00000u, 0x00008002u, 4u, 0xd95du}, + {0x37fb0000u, 0xcdb00000u, 0x00008100u, 4u, 0xf165u}, + {0x46c00000u, 0xbe374000u, 0x00000002u, 4u, 0xec4bu}, + {0x48580000u, 0xbdf60000u, 0x00008822u, 4u, 0xf67du}, + {0x40186000u, 0x46c00000u, 0x00000001u, 4u, 0x7b25u}, + {0xcc500000u, 0x33bd0000u, 0x00008001u, 4u, 0xc4cdu}, + {0x00001889u, 0x45c00000u, 0x00008002u, 5u, 0x4acdu}, + {0x0000a007u, 0xc4c00000u, 0x00000001u, 5u, 0x4a0bu}, + {0x0000a87du, 0x42c00000u, 0x00000002u, 5u, 0xc2bbu}, + {0x00003018u, 0xc6680000u, 0x00000100u, 5u, 0xe76bu}, + {0x0000d61eu, 0x3dc00000u, 0x00008001u, 5u, 0xc897u}, + {0x00002888u, 0xc7480000u, 0x00008100u, 5u, 0xe715u}, + {0x0000c064u, 0xc0900000u, 0x00000001u, 5u, 0x48f1u}, + {0x000074deu, 0xbe200000u, 0x00000001u, 5u, 0xea15u}, + {0x0000d86du, 0xc1400000u, 0x00008100u, 5u, 0x66a3u}, + {0x48c00000u, 0x000018dfu, 0x00800000u, 6u, 0x634fu}, + {0x41e00000u, 0x0000a002u, 0x2f800000u, 6u, 0xb303u}, + {0xb2f61000u, 0x00004094u, 0xc23ed000u, 6u, 0xd1f7u}, + {0xb5200000u, 0x0000941eu, 0xc575b000u, 6u, 0xebadu}, + {0xc1600000u, 0x000003dau, 0x0d000000u, 6u, 0x92bdu}, + {0x4a500000u, 0x00001894u, 0x33800000u, 6u, 0x6f71u}, + {0x3ce00000u, 0x0000681au, 0xaf800000u, 6u, 0x532du}, + {0x43a00000u, 0x00002a02u, 0x34079c43u, 6u, 0x4b83u}, + {0x43c00000u, 0x0000c81bu, 0xb3800000u, 6u, 0xea29u}, + {0x1f000000u, 0x1f000000u, 0x3f801000u, 0u, 0x3c01u}, + {0x1f000000u, 0x9f000000u, 0x3f801000u, 0u, 0x3c00u}, + {0x00800000u, 0x3e800000u, 0x3f801000u, 0u, 0x3c01u}, + {0x33000000u, 0x3f800000u, 0x00800000u, 0u, 0x0001u}, + {0x33000000u, 0x3f800000u, 0x80800000u, 0u, 0x0000u}, + {0x477ff000u, 0x3f800000u, 0xa6800000u, 0u, 0x7bffu}, + {0x7f7fffffu, 0x40000000u, 0xff7fffffu, 0u, 0x7c00u}, + {0x00003c01u, 0x00004200u, 0x00800000u, 3u, 0x4202u}, +}}; + +struct DirectedRow { + std::uint32_t a, b, c; + std::array expected; +}; + +constexpr std::array DirectedRows{{ + {0xbfc00000u, 0x33800000u, 0x00800000u, {0x8001u, 0x8001u, 0x8002u, 0x8001u}}, + {0x3fc00000u, 0x33800000u, 0x00800000u, {0x0002u, 0x0002u, 0x0001u, 0x0001u}}, + {0x3f800000u, 0x3f801400u, 0x00000000u, {0x3c01u, 0x3c01u, 0x3c00u, 0x3c00u}}, + {0xbf800000u, 0x3f801400u, 0x00000000u, {0xbc01u, 0xbc00u, 0xbc01u, 0xbc00u}}, + {0x477ff000u, 0x3f800000u, 0x00000000u, {0x7c00u, 0x7c00u, 0x7bffu, 0x7bffu}}, + {0xc77ff000u, 0x3f800000u, 0x00000000u, {0xfc00u, 0xfbffu, 0xfc00u, 0xfbffu}}, + {0x49742400u, 0x3f800000u, 0x00000000u, {0x7c00u, 0x7c00u, 0x7bffu, 0x7bffu}}, + {0x3fc00000u, 0x40000000u, 0x3e800000u, {0x4280u, 0x4280u, 0x4280u, 0x4280u}}, + {0x3f800000u, 0x3f800000u, 0xbf7fffffu, {0x0001u, 0x0001u, 0x0001u, 0x0001u}}, + {0xbf800000u, 0x3f800000u, 0x3f7fffffu, {0x8001u, 0x8001u, 0x8001u, 0x8001u}}, + {0x3f800000u, 0x3f800000u, 0xbf800000u, {0x0000u, 0x0000u, 0x8000u, 0x0000u}}, + {0x40000000u, 0x3f800000u, 0xc0000000u, {0x0000u, 0x0000u, 0x8000u, 0x0000u}}, + {0x00000000u, 0x3f800000u, 0x80000000u, {0x0000u, 0x0000u, 0x8000u, 0x0000u}}, + {0x80000000u, 0x3f800000u, 0x80000000u, {0x8000u, 0x8000u, 0x8000u, 0x8000u}}, + {0x33000000u, 0x3f000000u, 0x00000000u, {0x0000u, 0x0001u, 0x0000u, 0x0000u}}, + {0xb3000000u, 0x3f000000u, 0x00000000u, {0x8000u, 0x8000u, 0x8001u, 0x8000u}}, +}}; + float Quarter(std::uint32_t index) { return static_cast(static_cast(index % 17u) - 8) / 4.0f; } @@ -84,14 +192,21 @@ 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) { +void FillRoundingInput() { + Input.fill(0u); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto& row = RoundingRows[tid]; + std::copy_n(std::array{row.a, row.b, row.c}.begin(), 3, Input.begin() + tid * Inputs); + } +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, const std::optional& floatMode = std::nullopt) { Output.fill(0xdeadbeefu); std::vector userData(8, 0u); const auto input = BufferDescriptor(Input.data(), static_cast(Input.size())); const auto output = BufferDescriptor(Output.data(), static_cast(Output.size())); std::copy(input.begin(), input.end(), userData.begin()); std::copy(output.begin(), output.end(), userData.begin() + 4); - const std::span code(MixCode); 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{ @@ -101,6 +216,7 @@ void Run(AgcDriver::VulkanDevice& device) { {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(); @@ -131,6 +247,38 @@ void Check() { } } +void CheckRounding() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto& row = RoundingRows[tid]; + const auto* out = &Output[tid * Results]; + const std::uint32_t expected = 0xabcd0000u | row.expected; + const auto where = "mix rounding: thread " + std::to_string(tid) + " (" + Hex(row.a) + ", " + Hex(row.b) + ", " + Hex(row.c) + ") result " + std::to_string(row.result); + Require(out[row.result] == expected, where + " is " + Hex(out[row.result]) + ", expected " + Hex(expected)); + if (row.result == 0u) { + Require(out[1] == ((row.expected << 16u) | 0xabcdu), where + ": v_fma_mixhi_f16 is " + Hex(out[1])); + } + } +} + +void FillDirectedInput() { + Input.fill(0u); + for (std::uint32_t tid = 0; tid < DirectedRows.size(); ++tid) { + const auto& row = DirectedRows[tid]; + std::copy_n(std::array{row.a, row.b, row.c}.begin(), 3, Input.begin() + tid * Inputs); + } +} + +void CheckDirected(std::uint32_t rounding) { + for (std::uint32_t tid = 0; tid < DirectedRows.size(); ++tid) { + const auto& row = DirectedRows[tid]; + const auto* out = &Output[tid * Results]; + const std::uint32_t expected = row.expected[rounding]; + const auto where = "mix f16 rounding " + std::to_string(rounding) + ": thread " + std::to_string(tid) + " (" + Hex(row.a) + ", " + Hex(row.b) + ", " + Hex(row.c) + ")"; + Require(out[0] == (0xabcd0000u | expected), where + " v_fma_mixlo_f16 is " + Hex(out[0]) + ", expected " + Hex(0xabcd0000u | expected)); + Require(out[1] == ((expected << 16u) | 0xabcdu), where + " v_fma_mixhi_f16 is " + Hex(out[1]) + ", expected " + Hex((expected << 16u) | 0xabcdu)); + } +} + } int main() { @@ -138,8 +286,16 @@ int main() { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; FillInput(); - Run(*device); + Run(*device, MixCode); Check(); + FillRoundingInput(); + Run(*device, RoundingCode); + CheckRounding(); + for (std::uint32_t rounding = 0; rounding < 4u; ++rounding) { + FillDirectedInput(); + Run(*device, RoundingCode, ShaderRecompiler::ShaderFloatMode{0xc0u | (rounding << 2u), true, false, false}); + CheckDirected(rounding); + } std::puts("mix precision tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MsaaEnableSingleSample.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MsaaEnableSingleSample.cpp new file mode 100644 index 0000000000..d7500ebc29 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MsaaEnableSingleSample.cpp @@ -0,0 +1,132 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::byte Kept{0x40}; + +alignas(256) constexpr std::array VertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060280u, 0x7e0802f2u, 0xf80008cfu, 0x04030201u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u, +}; +alignas(256) constexpr std::array PixelCode{0x7e000200u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u}; +alignas(256) std::array ClearMemory{}; +alignas(256) std::array EnabledMemory{}; + +struct Header { + Shader shader{}; + ShaderUserData userData{}; +}; + +alignas(8) Header VertexHeader{}; +alignas(8) Header PixelHeader{}; + +void Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = static_cast(bytes); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.userData; + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +void Append(std::vector& commands, std::uint32_t opcode, std::initializer_list payload) { + commands.push_back(0xc0000000u | (static_cast(payload.size() - 1) << 16u) | (opcode << 8u)); + commands.insert(commands.end(), payload); +} + +std::vector DrawBoth() { + const auto vertex = reinterpret_cast(VertexCode.data()); + const auto pixel = reinterpret_cast(PixelCode.data()); + const std::vector> context{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u}, + {0x3b8, 0x9000000}, {0x1e0, 0}, + {0xc, 0}, {0xd, 0x40040}, + {0x81, 0x80000000}, {0x82, 0x40040}, + {0x90, 0x80000000}, {0x91, 0x40040}, + {0x94, 0x80000000}, {0x95, 0x40040}, + {0x10f, std::bit_cast(32.0f)}, {0x110, std::bit_cast(32.0f)}, + {0x111, std::bit_cast(-2.0f)}, {0x112, std::bit_cast(2.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, + {0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0}, + }; + const std::vector> shader{ + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 1u << 1u}, {0x8c, 0}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 1u << 1u}, {0x00c, std::bit_cast(1.0f)}, + }; + std::vector commands; + Append(commands, 0x79, {0x242, 4}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); + const auto draw = [&](std::array& memory, std::uint32_t scanMode) { + const auto color = reinterpret_cast(memory.data()); + Append(commands, 0x69, {0x318, static_cast(color >> 8u)}); + Append(commands, 0x69, {0x390, static_cast(color >> 40u)}); + Append(commands, 0x69, {0x292, scanMode}); + Append(commands, 0x2d, {3, 2}); + }; + draw(ClearMemory, 2); + draw(EnabledMemory, 3); + return commands; +} + +void MsaaEnableDrawsLikeDisabled() { + ClearMemory.fill(Kept); + EnabledMemory.fill(Kept); + Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); + Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + auto commands = DrawBoth(); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + const auto kept = static_cast(std::count(ClearMemory.begin(), ClearMemory.end(), Kept)); + Require(kept != 0 && kept != ClearMemory.size(), "the triangle without MSAA_ENABLE covered " + std::to_string(ClearMemory.size() - kept) + " of " + std::to_string(ClearMemory.size()) + " bytes, expected an edge through the target"); + for (std::size_t index = 0; index < ClearMemory.size(); ++index) { + Require(EnabledMemory[index] == ClearMemory[index], "byte " + std::to_string(index) + " is " + std::to_string(static_cast(EnabledMemory[index])) + " with MSAA_ENABLE on a single-sample target and " + std::to_string(static_cast(ClearMemory[index])) + " without it"); + } +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + MsaaEnableDrawsLikeDisabled(); + AgcDriverShutdown_nid_postfix(); + std::puts("MSAA_ENABLE on a single-sample target drew the same pixels as the state without it"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/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/MultisampleFillHostOnly.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleFillHostOnly.cpp new file mode 100644 index 0000000000..354b880d73 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleFillHostOnly.cpp @@ -0,0 +1,277 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::ColorTarget; +using AgcDriver::Graphics::Require; + +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Side = 64; +constexpr std::byte Marker{0xab}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "multisampled fill: cannot allocate a guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::byte* Data() { return block; } + + std::uint64_t Address() const { return static_cast(reinterpret_cast(block)); } + + bool Holds(std::byte value, std::size_t offset, std::size_t bytes) const { + return std::all_of(block + offset, block + offset + bytes, [&](std::byte held) { return held == value; }); + } + +private: + std::byte* block = nullptr; +}; + +GuestBlock& NewBlock() { + static auto* blocks = new std::deque(); + return blocks->emplace_back(); +} + +std::uint32_t Low(std::uint64_t value) { return static_cast(value); } + +std::uint32_t High(std::uint64_t value) { return static_cast(value >> 32u); } + +std::vector Pm4Packet(std::uint32_t opcode, std::initializer_list body) { + std::vector words{0xc0000000u | (static_cast(body.size() - 1) << 16u) | (opcode << 8u)}; + words.insert(words.end(), body); + return words; +} + +void Submit(std::initializer_list> packets) { + std::vector words; + for (const auto& packet : packets) words.insert(words.end(), packet.begin(), packet.end()); + Packet dcb{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&dcb) == 0, "multisampled fill: DCB submission failed"); + AgcDriverWaitIdle_nid_postfix(); +} + +constexpr std::array FillKernel{0xd7460004u, 0x04010c08u, 0x7e000204u, 0x7e020205u, 0x7e040206u, 0x7e060207u, 0xe01c2000u, 0x80000004u, 0xbf810000u}; + +void DispatchFill(const GuestBlock& code, std::uint64_t base, std::uint32_t bytes, std::uint32_t value) { + const auto kernel = code.Address(); + Submit({ + Pm4Packet(0x76, {0x20c, Low(kernel >> 8u)}), + Pm4Packet(0x76, {0x20d, High(kernel >> 8u) & 0xffu}), + Pm4Packet(0x76, {0x207, 64}), + Pm4Packet(0x76, {0x208, 1}), + Pm4Packet(0x76, {0x209, 1}), + Pm4Packet(0x76, {0x213, (8u << 1u) | 0x80u}), + Pm4Packet(0x76, {0x240, Low(base)}), + Pm4Packet(0x76, {0x241, (High(base) & 0xffffu) | (16u << 16u)}), + Pm4Packet(0x76, {0x242, bytes / 16u}), + Pm4Packet(0x76, {0x243, 0x4bfacu}), + Pm4Packet(0x76, {0x244, value}), + Pm4Packet(0x76, {0x245, value}), + Pm4Packet(0x76, {0x246, value}), + Pm4Packet(0x76, {0x247, value}), + Pm4Packet(0x15, {bytes / 1024u, 1, 1, 0x41}), + }); +} + +void Settle(const GuestBlock& code, const GuestBlock& sentinel) { + static std::uint32_t round = 0; + ++round; + DispatchFill(code, sentinel.Address(), 1024, round); + std::array stored{}; + AgcDriver::GuestMemory::Read(sentinel.Address(), std::as_writable_bytes(std::span(stored)), 4); + Require(stored == std::array{round, round, round, round}, "the sentinel fill did not land"); +} + +ColorTarget MultisampledTarget(const GuestBlock& color) { + ColorTarget target{}; + target.address = color.Address(); + target.surfaceAddress = target.address; + target.extent = {Side, Side}; + target.surfaceExtent = target.extent; + target.format = VK_FORMAT_R8G8B8A8_UNORM; + target.tileMode = AgcDriver::Graphics::ColorTileMode::RenderTarget; + target.elementBytes = 4; + target.bytes = BlockBytes / 4; + target.samples = 4; + return target; +} + +bool FillsTakenAsMultisampledClearsAreNotStored(AgcDriver::VulkanDevice& device) { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + if ((context.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_4_BIT) == 0) { + std::cout << "skipped, the device has no 4x color targets\n"; + return false; + } + struct ClearTargets { + AgcDriver::VulkanDevice& device; + VkDevice handle; + ~ClearTargets() { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + 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); + AgcDriver::Graphics::ClearCachedTextures(handle); + } + } clearTargets{device, context.device}; + auto& code = NewBlock(); + std::memcpy(code.Data(), FillKernel.data(), sizeof(FillKernel)); + + auto& sentinel = NewBlock(); + auto& control = NewBlock(); + std::memset(control.Data(), static_cast(Marker), BlockBytes); + DispatchFill(code, control.Address(), BlockBytes, 0); + Settle(code, sentinel); + if (control.Holds(Marker, 0, BlockBytes)) { + std::cout << "skipped, the device does not store compute fills into guest memory\n"; + return false; + } + Require(control.Holds(std::byte{}, 0, BlockBytes), "a compute fill of 0 over plain guest memory did not store 0"); + + auto& color = NewBlock(); + const auto target = MultisampledTarget(color); + Require(AgcDriver::Graphics::MultisampledColorBytes(target) == BlockBytes, "the 4x 64x64 RGBA8 target does not take one 64 KiB block"); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + 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(); + } + Require(AgcDriver::Graphics::MultisampledColorFillAbsorbs(color.Address(), BlockBytes, 0), "a fill of 0 over the whole color memory is not taken as the target's clear"); + Require(!AgcDriver::Graphics::MultisampledColorFillAbsorbs(color.Address(), BlockBytes / 2, 0), "a fill over half of the color memory is taken as the target's clear"); + Require(!AgcDriver::Graphics::MultisampledColorFillAbsorbs(color.Address() + 4096, BlockBytes - 4096, 0), "a fill starting inside the color memory is taken as the target's clear"); + Require(!AgcDriver::Graphics::MultisampledColorFillAbsorbs(color.Address(), 2 * BlockBytes, 0), "a fill reaching past the color memory is taken as the target's clear"); + + std::memset(color.Data(), static_cast(Marker), BlockBytes); + DispatchFill(code, color.Address(), BlockBytes, 0); + Settle(code, sentinel); + Require(color.Holds(Marker, 0, BlockBytes), "a compute fill taken as the multisampled target's clear was stored into its color memory"); + DispatchFill(code, color.Address(), BlockBytes / 2, 0); + Settle(code, sentinel); + Require(color.Holds(std::byte{}, 0, BlockBytes / 2) && color.Holds(Marker, BlockBytes / 2, BlockBytes / 2), "a compute fill over half of the color memory was not stored"); + + std::memset(control.Data(), static_cast(Marker), BlockBytes); + DispatchFill(code, control.Address(), BlockBytes, 0); + Settle(code, sentinel); + Require(control.Holds(std::byte{}, 0, BlockBytes), "a compute fill of plain guest memory was not stored once a multisampled target exists"); + auto& srgbColor = NewBlock(); + auto srgb = MultisampledTarget(srgbColor); + srgb.format = VK_FORMAT_R8G8B8A8_SRGB; + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + 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(); + } + Require(AgcDriver::Graphics::MultisampledColorFillAbsorbs(srgbColor.Address(), BlockBytes, 0), "a fill of 0 over an sRGB target's color memory is not taken as its clear"); + Require(!AgcDriver::Graphics::MultisampledColorFillAbsorbs(srgbColor.Address(), BlockBytes, 0x80808080u), "a fill with no exact sRGB clear value is taken as the target's clear"); + std::memset(srgbColor.Data(), static_cast(Marker), BlockBytes); + DispatchFill(code, srgbColor.Address(), BlockBytes, 0x80808080u); + Settle(code, sentinel); + Require(srgbColor.Holds(std::byte{0x80}, 0, BlockBytes), "a compute fill with no exact clear value was not stored into the color memory"); + + { + GuestAllocations::Mutation mutation; + mutation.Remove(color.Data()); + mutation.Add(color.Data(), BlockBytes, true, true, true); + } + Require(!AgcDriver::Graphics::MultisampledColorFillAbsorbs(color.Address(), BlockBytes, 0), "a fill over a retired target's color memory is taken as its clear"); + std::memset(color.Data(), static_cast(Marker), BlockBytes); + DispatchFill(code, color.Address(), BlockBytes, 0); + Settle(code, sentinel); + Require(color.Holds(std::byte{}, 0, BlockBytes), "a compute fill over a retired target's color memory was not stored"); + { + 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; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const bool tested = FillsTakenAsMultisampledClearsAreNotStored(*device); + LibcRunShutdown_nid_postfix(); + if (!tested) return VulkanTestSkipped; + std::cout << "multisampled fill tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { + LibcRunShutdown_nid_postfix(); + } catch (...) { + } + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleReadback.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleReadback.cpp new file mode 100644 index 0000000000..8b6cc1b256 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleReadback.cpp @@ -0,0 +1,400 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.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) { + 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); + 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) : 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); + 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) / 65536.0f); + std::memcpy(result.data() + static_cast(index) * elementBytes, words.data(), elementBytes); + } + return result; + } +}; + +void CheckReadback(TextureDetiler& reader, const Surface& surface, std::uint32_t first, std::uint32_t elements, std::uint32_t repetitions, std::size_t extraBytes = 0u) { + const auto expected = surface.Expected(first, elements); + 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_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"); +} + +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"); +} + +} + +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", "unknown multisample readback test mode"); + if (mode == "depth") 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; + } + auto limited = context; + limited.limits.maxComputeWorkGroupCount[0] = 1u; + TextureDetiler reader(limited); + 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..b603cc7326 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleRetirement.cpp @@ -0,0 +1,542 @@ +#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 + +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 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(); + } + 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"); + } + +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(); + 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/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/NanQuieting.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NanQuieting.cpp index 07de164aa0..b14810e75c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NanQuieting.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NanQuieting.cpp @@ -21,6 +21,7 @@ using ShaderRecompiler::ShaderStage; constexpr std::uint32_t Threads = 32; constexpr std::uint32_t Inputs = 4; constexpr std::uint32_t Results = 16; +constexpr std::uint32_t Float64Capability = 10; alignas(256) std::array Input{}; alignas(256) std::array Output{}; @@ -34,6 +35,73 @@ alignas(256) constexpr std::array Code{ 0x80011203u, 0xe0781030u, 0x80011603u, 0xbf810000u, }; +alignas(256) constexpr std::array CodeRounding{ + 0x34020084u, 0x34060086u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e140280u, 0x7e160280u, 0x7e180280u, + 0x7e1a0280u, 0x7e1c0280u, 0x7e1e0280u, 0x7e200280u, 0x7e220280u, 0x7e240280u, 0x7e260280u, 0x7e280280u, + 0x7e2a0280u, 0x7e2c0280u, 0x7e2e0280u, 0x7e300280u, 0x7e320280u, 0x7e147d04u, 0x7e183504u, 0x7e1c3104u, + 0x7e202f04u, 0x7e243304u, 0x7e287b04u, 0xd5680016u, 0x02020d04u, 0x7e302107u, 0xe0781000u, 0x80010a03u, + 0xe0781010u, 0x80010e03u, 0xe0781020u, 0x80011203u, 0xe0781030u, 0x80011603u, 0xbf810000u, +}; + +alignas(256) constexpr std::array CodeApproximate{ + 0x34020084u, 0x34060086u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e140280u, 0x7e160280u, 0x7e180280u, + 0x7e1a0280u, 0x7e1c0280u, 0x7e1e0280u, 0x7e200280u, 0x7e220280u, 0x7e240280u, 0x7e260280u, 0x7e280280u, + 0x7e2a0280u, 0x7e2c0280u, 0x7e2e0280u, 0x7e300280u, 0x7e320280u, 0x7e145f04u, 0x7e186304u, 0x7e1c6904u, + 0xd5600010u, 0x03ca08f2u, 0xd5600012u, 0x0411e4f2u, 0xd5600014u, 0x841208f2u, 0x7e2c1f04u, 0xe0781000u, + 0x80010a03u, 0xe0781010u, 0x80010e03u, 0xe0781020u, 0x80011203u, 0xe0781030u, 0x80011603u, 0xbf810000u, +}; + +constexpr std::uint32_t RowsF64[8][4] = { + {0x00000001u, 0x7ff00000u, 0x00000001u, 0x7f800001u}, + {0x12345678u, 0xfff00000u, 0x00000001u, 0xff812345u}, + {0x00000000u, 0x7ff40000u, 0x00000000u, 0x7fa00000u}, + {0x00000000u, 0x7ff80000u, 0x00000001u, 0x7fc00000u}, + {0x12345678u, 0xfff80000u, 0x00000001u, 0xffc12345u}, + {0x00000000u, 0x7ff00000u, 0x00000001u, 0x7f800000u}, + {0x00000000u, 0x3ff80000u, 0x00000001u, 0x3f800000u}, + {0x00000000u, 0xc0040000u, 0x00000002u, 0xc0200000u} +}; +constexpr std::uint32_t ExpectedRoundingIeee[8][16] = { + {0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x20000000u, 0x7ff80000u}, + {0x12345678u, 0xfff80000u, 0x12345678u, 0xfff80000u, 0x12345678u, 0xfff80000u, 0x12345678u, 0xfff80000u, 0x12345678u, 0xfff80000u, 0x12345678u, 0xfff80000u, 0x12345678u, 0xfff80000u, 0xa0000000u, 0xfff82468u}, + {0x00000000u, 0x7ffc0000u, 0x00000000u, 0x7ffc0000u, 0x00000000u, 0x7ffc0000u, 0x00000000u, 0x7ffc0000u, 0x00000000u, 0x7ffc0000u, 0x00000000u, 0x7ffc0000u, 0x00000000u, 0x7ffc0000u, 0x00000000u, 0x7ffc0000u}, + {0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u}, + {0x12345678u, 0xfff80000u, 0x12345678u, 0xfff80000u, 0x12345678u, 0xfff80000u, 0x12345678u, 0xfff80000u, 0x12345678u, 0xfff80000u, 0x12345678u, 0xfff80000u, 0x12345678u, 0xfff80000u, 0xa0000000u, 0xfff82468u}, + {0x00000000u, 0xfff80000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0x7ff00000u}, + {0x00000000u, 0x3fe00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x3fe80000u, 0x00000000u, 0x40080000u, 0x00000000u, 0x3ff00000u}, + {0x00000000u, 0x3fe00000u, 0x00000000u, 0xc0080000u, 0x00000000u, 0xc0000000u, 0x00000000u, 0xc0000000u, 0x00000000u, 0xc0000000u, 0x00000000u, 0xbfe40000u, 0x00000000u, 0xc0240000u, 0x00000000u, 0xc0040000u} +}; +constexpr std::uint32_t ExpectedRoundingNoIeee[8][16] = { + {0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u, 0x20000000u, 0x7ff00000u}, + {0x12345678u, 0xfff00000u, 0x12345678u, 0xfff00000u, 0x12345678u, 0xfff00000u, 0x12345678u, 0xfff00000u, 0x12345678u, 0xfff00000u, 0x12345678u, 0xfff00000u, 0x12345678u, 0xfff00000u, 0xa0000000u, 0xfff02468u}, + {0x00000000u, 0x7ff40000u, 0x00000000u, 0x7ff40000u, 0x00000000u, 0x7ff40000u, 0x00000000u, 0x7ff40000u, 0x00000000u, 0x7ff40000u, 0x00000000u, 0x7ff40000u, 0x00000000u, 0x7ff40000u, 0x00000000u, 0x7ff40000u}, + {0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u}, + {0x12345678u, 0xfff80000u, 0x12345678u, 0xfff80000u, 0x12345678u, 0xfff80000u, 0x12345678u, 0xfff80000u, 0x12345678u, 0xfff80000u, 0x12345678u, 0xfff80000u, 0x12345678u, 0xfff80000u, 0xa0000000u, 0xfff82468u}, + {0x00000000u, 0xfff80000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0x7ff00000u}, + {0x00000000u, 0x3fe00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x3fe80000u, 0x00000000u, 0x40080000u, 0x00000000u, 0x3ff00000u}, + {0x00000000u, 0x3fe00000u, 0x00000000u, 0xc0080000u, 0x00000000u, 0xc0000000u, 0x00000000u, 0xc0000000u, 0x00000000u, 0xc0000000u, 0x00000000u, 0xbfe40000u, 0x00000000u, 0xc0240000u, 0x00000000u, 0xc0040000u} +}; +constexpr std::uint32_t ExpectedApproximateIeee[8][16] = { + {0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0x7ff80000u, 0x00000001u, 0xfff80000u, 0x7fc00000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x12345678u, 0xfff80000u, 0x12345678u, 0xfff80000u, 0x12345678u, 0xfff80000u, 0x12345678u, 0xfff80000u, 0x12345678u, 0xfff80000u, 0x12345678u, 0x7ff80000u, 0xffc00000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x7ffc0000u, 0x00000000u, 0x7ffc0000u, 0x00000000u, 0x7ffc0000u, 0x00000000u, 0x7ffc0000u, 0x00000000u, 0x7ffc0000u, 0x00000000u, 0xfffc0000u, 0x7fe00000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0xfff80000u, 0x7fc00000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x12345678u, 0xfff80000u, 0x12345678u, 0xfff80000u, 0x12345678u, 0xfff80000u, 0x12345678u, 0xfff80000u, 0x12345678u, 0xfff80000u, 0x12345678u, 0x7ff80000u, 0xffc00000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0xfff80000u, 0x7f800000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x54000000u, 0x3fe55555u, 0x74000000u, 0x3fea20bdu, 0x14000000u, 0x3ff3988eu, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0xbff00000u, 0x3fc00000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x9c000000u, 0xbfd99999u, 0x00000000u, 0xfff80000u, 0x00000000u, 0xfff80000u, 0x00000000u, 0xbff00000u, 0x00000000u, 0xbff00000u, 0x00000000u, 0xbff00000u, 0xc0200000u, 0x00000000u, 0x00000000u, 0x00000000u} +}; +constexpr std::uint32_t ExpectedApproximateNoIeee[8][16] = { + {0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0x7ff00000u, 0x00000001u, 0xfff00000u, 0x7fc00000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x12345678u, 0xfff00000u, 0x12345678u, 0xfff00000u, 0x12345678u, 0xfff00000u, 0x12345678u, 0xfff00000u, 0x12345678u, 0xfff00000u, 0x12345678u, 0x7ff00000u, 0xffc00000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x7ff40000u, 0x00000000u, 0x7ff40000u, 0x00000000u, 0x7ff40000u, 0x00000000u, 0x7ff40000u, 0x00000000u, 0x7ff40000u, 0x00000000u, 0xfff40000u, 0x7fe00000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0xfff80000u, 0x7fc00000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x12345678u, 0xfff80000u, 0x12345678u, 0xfff80000u, 0x12345678u, 0xfff80000u, 0x12345678u, 0xfff80000u, 0x12345678u, 0xfff80000u, 0x12345678u, 0x7ff80000u, 0xffc00000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x7ff00000u, 0x00000000u, 0xfff80000u, 0x7f800000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x54000000u, 0x3fe55555u, 0x74000000u, 0x3fea20bdu, 0x14000000u, 0x3ff3988eu, 0x00000000u, 0x3ff00000u, 0x00000000u, 0x3ff00000u, 0x00000000u, 0xbff00000u, 0x3fc00000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x9c000000u, 0xbfd99999u, 0x00000000u, 0xfff80000u, 0x00000000u, 0xfff80000u, 0x00000000u, 0xbff00000u, 0x00000000u, 0xbff00000u, 0x00000000u, 0xbff00000u, 0xc0200000u, 0x00000000u, 0x00000000u, 0x00000000u} +}; + constexpr std::uint32_t Rows[32][4] = { {0x7f812345u, 0x40000000u, 0x40400000u, 0x40007d23u}, {0xff800001u, 0x40000000u, 0x40400000u, 0x4000fc01u}, @@ -154,10 +222,27 @@ constexpr const char* Names[16] = { "v_trunc_f32", "v_rndne_f16" }; +constexpr const char* NamesRounding[8] = { + "v_fract_f64", + "v_floor_f64", + "v_ceil_f64", + "v_trunc_f64", + "v_rndne_f64", + "v_frexp_mant_f64", + "v_ldexp_f64", + "v_cvt_f64_f32" +}; +constexpr const char* NamesApproximate[8] = { + "v_rcp_f64", + "v_rsq_f64", + "v_sqrt_f64", + "v_div_fixup_f64 denominator", + "v_div_fixup_f64 numerator", + "v_div_fixup_f64 both", + "v_cvt_f32_f64", + "unused" +}; -void Fill(std::uint32_t tid, std::uint32_t* words) { - std::copy(std::begin(Rows[tid]), std::end(Rows[tid]), words); -} std::array BufferDescriptor(const void* data, std::uint32_t bytes) { const auto address = reinterpret_cast(data); @@ -182,15 +267,20 @@ bool IsNan16(std::uint32_t value) { return (value & 0x7fffu) > 0x7c00u; } -void Run(AgcDriver::VulkanDevice& device, const std::optional& floatMode) { - for (std::uint32_t tid = 0; tid < Threads; ++tid) Fill(tid, &Input[tid * Inputs]); +bool IsNan64(std::uint32_t low, std::uint32_t high) { + return (high & 0x7fffffffu) > 0x7ff00000u || ((high & 0x7fffffffu) == 0x7ff00000u && low != 0u); +} + +template +void Run(AgcDriver::VulkanDevice& device, const std::array& program, const std::uint32_t (&rows)[Count][4], const std::optional& floatMode) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) std::copy(std::begin(rows[tid % Count]), std::end(rows[tid % Count]), &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::span code(program); 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{ @@ -221,18 +311,49 @@ void Check(const std::uint32_t (&expected)[32][16], const char* mode) { } } +void CheckF64(const std::uint32_t (&expected)[8][16], const char* const (&names)[8], std::uint32_t approximate, const char* mode) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t* out = &Output[tid * Results]; + const std::uint32_t* want = expected[tid % 8u]; + for (std::uint32_t i = 0; i < 16; ++i) { + const std::uint32_t pair = i / 2u; + if (pair < approximate && !IsNan64(want[pair * 2u], want[pair * 2u + 1u])) continue; + const std::string name = std::string(mode) + " " + names[pair] + (i % 2u ? " high" : " low"); + Expect(tid, out[i], want[i], name.c_str()); + } + } +} + +void CheckF64Modes(AgcDriver::VulkanDevice& device) { + const std::optional modes[3] = {std::nullopt, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}, ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}}; + const char* labels[3] = {"no float mode", "IEEE=0", "IEEE=1"}; + for (std::uint32_t mode = 0; mode < 3; ++mode) { + const bool ieee = mode == 2u; + Run(device, CodeRounding, RowsF64, modes[mode]); + CheckF64(ieee ? ExpectedRoundingIeee : ExpectedRoundingNoIeee, NamesRounding, 0u, labels[mode]); + Run(device, CodeApproximate, RowsF64, modes[mode]); + CheckF64(ieee ? ExpectedApproximateIeee : ExpectedApproximateNoIeee, NamesApproximate, 3u, labels[mode]); + } +} + } int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - Run(*device, std::nullopt); + Run(*device, Code, Rows, std::nullopt); Check(ExpectedNoIeee, "no float mode"); - Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}); + Run(*device, Code, Rows, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}); Check(ExpectedNoIeee, "IEEE=0"); - Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}); + Run(*device, Code, Rows, ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}); Check(ExpectedIeee, "IEEE=1"); + const auto capabilities = device->Target().supportedCapabilities; + if (std::find(capabilities.begin(), capabilities.end(), Float64Capability) == capabilities.end()) { + std::puts("f64 nan quieting skipped, the device has no shaderFloat64"); + } else { + CheckF64Modes(*device); + } std::puts("nan quieting tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeDepthBdaClear.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeDepthBdaClear.cpp new file mode 100644 index 0000000000..d1564e3562 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeDepthBdaClear.cpp @@ -0,0 +1,186 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context Context(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::Require; +namespace Memory = AgcDriver::GuestMemory; + +constexpr std::size_t BlockBytes = 1048576; + +class GuestBlock { +public: + GuestBlock() { + Require(Memory::WriteWatched(), "native depth BDA clear requires guest write watching"); +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, BlockBytes)); + Require(block != nullptr, "cannot allocate the native depth BDA clear block"); + section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, BlockBytes, nullptr); + Require(section != nullptr, "cannot create the native depth BDA clear backing"); + GuestArena::GuestArenaMapDirect_nid_postfix(block, BlockBytes, section, 0, PAGE_READWRITE); +#else + void* raw = mmap(nullptr, 2 * BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the native depth BDA clear block"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned != begin + BlockBytes) munmap(reinterpret_cast(aligned + BlockBytes), begin + BlockBytes - aligned); + block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, BlockBytes); +#endif + Require(Memory::Watched(Address(), BlockBytes), "the native depth BDA clear block is not watched"); + std::memset(block, 0xcd, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, BlockBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, BlockBytes); + GuestArena::GuestArenaForgetSection_nid_postfix(section); + CloseHandle(section); +#else + munmap(block, BlockBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, BlockBytes); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + std::uint64_t Address() const { return reinterpret_cast(block); } + const std::uint8_t* Data() const { return block; } + void FillWith(std::uint8_t value) const { std::memset(block, value, BlockBytes); } + +private: + std::uint8_t* block = nullptr; +#ifdef _WIN32 + HANDLE section = nullptr; +#endif +}; + +int RunNativeClear(AgcDriver::VulkanDevice& device) { + using namespace ShaderRecompiler; + using namespace AgcDriver::Graphics; + const std::lock_guard lock(Memory::GpuMutex()); + const GuestBlock depth, metadata; + metadata.FillWith(0); + constexpr VkExtent2D extent{5u, 3u}; + const auto metadataBytes = static_cast(HtileMetadataBytes(extent)); + alignas(256) static constexpr std::array code{ + 0xd7460000, 0x04010c06, 0x34000084, 0xdc388000, 0x04020000, + 0xbf8c3f70, 0xd7710008, 0x00120805, 0xd7710009, 0x00120a05, + 0xd771000a, 0x00120c05, 0xd771000b, 0x00120e05, + 0xdc788000, 0x00020800, 0xbf810000, + }; + std::array userData{0u, 0u, static_cast(metadata.Address()), static_cast(metadata.Address() >> 32u), 0u, 0u}; + const ShaderComputeStageInfo compute{{64u, 1u, 1u}, 0u, {true, false, false}, false, 1u}; + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(std::span(code))}}}; + const auto groups = static_cast(metadataBytes / 1024u); + const std::array packet{0u, groups, 1u, 1u, 1u}; + Require(HtileDepthClearBytes_nid_no_patch(code, userData, compute, packet, 0u) == metadataBytes, "the native GPU clear kernel is not recognized"); + const auto compile = [&] { + RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {64u, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), {0, 0, 0, 128}, std::nullopt, false, + }; + auto result = Recompile(request); + Require(std::ranges::any_of(result.bindings, [](const auto& binding) { return binding.role == DescriptorRole::BdaPagetable; }), "the native clear shader did not use BDA"); + return result; + }; + device.Dispatch(compile(), groups, 1u, 1u, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + Require(std::all_of(metadata.Data(), metadata.Data() + BlockBytes, [](auto value) { return value == 0u; }), "the warm-up native clear did not retain zero metadata"); + if (!Memory::Watched(depth.Address(), BlockBytes) || !Memory::Watched(metadata.Address(), BlockBytes)) { + std::puts("skipped, the device does not retain guest write watching for native clear imports"); + return VulkanTestSkipped; + } + const auto context = AgcDriver::VulkanDeviceTestAccess::Context(device); + Require(context.recorder != nullptr, "the native clear context has no recorder"); + context.recorder->Activate(); + struct Cleanup { + AgcDriver::VulkanDevice& device; + ~Cleanup() { device.WaitIdle(); ClearCachedTextures(device.Device()); ClearDepthSurfaces(device.Device()); } + } cleanup{device}; + DepthTarget target{depth.Address(), 0u, extent, VK_FORMAT_D32_SFLOAT, 0.0f, 0u, metadata.Address(), false, 4u}; + target.depthSwizzle = 24u; + target.htileDepthOnly = target.iterate256 = target.htilePipeAligned = true; + const auto view = DepthSurfaceView(context, target); + const auto base = target.address >> 8u; + const auto htile = target.htileAddress >> 8u; + const std::array words{ + static_cast(base), static_cast((base >> 32u) & 255u) | (22u << 20u), + 1u | (2u << 14u), (14u << 28u) | (24u << 20u) | (2u << 16u) | 0x204u, 0u, 2u << 4u, + (1u << 10u) | (1u << 19u) | (1u << 21u) | (static_cast(htile & 255u) << 24u), static_cast(htile >> 8u), + }; + const auto sampled = CachedSampledTexture(context, words); + Require(sampled != nullptr, "the native GPU clear has no initial sampled view"); + device.WaitIdle(); + target.clearDepth = 1.0f; + Require(DepthSurfaceView(context, target) == view, "changing the native clear value replaced its attachment"); + NoteHtileDepthClear(target.htileAddress, metadataBytes, metadataBytes); + const auto generation = Memory::TrackerGeneration(); + userData[4] = 0xfffffff0u; + device.Dispatch(compile(), groups, 1u, 1u, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + for (std::size_t offset = 0; offset < BlockBytes; offset += 4u) { + std::uint32_t value = 0; + std::memcpy(&value, metadata.Data() + offset, 4u); + Require(value == (offset < metadataBytes ? 0xfffffff0u : 0u), "the native GPU clear did not write its exact metadata range"); + } + std::printf("Native full BDA clear: exact %zu-byte metadata readback passed; complete=%d\n", metadataBytes, Memory::StoredAllSince_nid_no_patch(target.htileAddress, metadataBytes, generation)); + Require(CachedSampledTexture(context, words) == sampled, "a completed full BDA clear lost its native sampled view"); + Require(DepthSurfaceView(context, target) == view, "a completed full BDA clear replaced its native attachment"); + device.WaitIdle(); + Require(std::all_of(depth.Data(), depth.Data() + BlockBytes, [](auto value) { return value == 0xcdu; }), "the native GPU clear changed guest depth bytes"); + std::puts("Native full BDA clear accepted"); + return 0; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + return RunNativeClear(*device); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp new file mode 100644 index 0000000000..cda5b9280a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -0,0 +1,1188 @@ +#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/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 +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif + +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) : bytes(bytes) { +#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); + } + ~TargetMemory() { +#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; +}; + +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; + TargetMemory memory(static_cast(MultisampledDepthBytes_nid_no_patch(extent, 4u, samples))), 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(); + 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(); + std::puts("Native depth feedback refused through the draw resource path"); +} + +enum class ClearFault { None, PartialStore, CpuWrite, DepthWrite, BadMetadata, UnprovenDispatch, PendingWrite, Overlap }; + +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, 0xf0u, 0xf0u}; + 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 = false, std::optional depthOnlyOverride = std::nullopt, ClearFault fault = ClearFault::None, 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); + 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"); + 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; + const auto nativeBytes = fault == ClearFault::UnprovenDispatch ? 0u : static_cast(metadataBytes); + NoteHtileDepthClear(target.htileAddress, static_cast(metadataBytes), nativeBytes); + if (fault == ClearFault::Overlap) NoteHtileDepthClear(target.htileAddress, static_cast(metadataBytes), nativeBytes); + const auto storedBytes = fault == ClearFault::PartialStore ? 4u : static_cast(metadataBytes); + 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}); + 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 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); + 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(); + const std::lock_guard 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::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); }, 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); ++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, iterate256); ++checked; } + } + CheckHtileRouting(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/NggVertexWaveInfo.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NggVertexWaveInfo.cpp index ba1c0124fc..1fc52d68ef 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NggVertexWaveInfo.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NggVertexWaveInfo.cpp @@ -24,6 +24,11 @@ alignas(256) constexpr std::array VertexCode{ 0xe0382000, 0x80020005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, }; +alignas(256) constexpr std::array GroupInfoVertexCode{ + 0x9380ff02, 0x0009000c, 0x8f009000, 0x94fe00c1, + 0xe0382000, 0x80020005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + alignas(256) constexpr std::array PixelCode{ 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, }; @@ -37,16 +42,16 @@ constexpr std::array, 3> Triangle{{ {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} }}; -void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize) { +void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, std::span vertexCode) { Pixels.fill(Kept); const auto target = device.Target(); std::vector vertexUserData(4, 0u); const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast(Triangle.size())); std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); - const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + const std::array vertexMemory{{{reinterpret_cast(vertexCode.data()), std::as_bytes(vertexCode)}}}; ShaderRecompiler::RecompileRequest vertex{ - {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {ShaderStage::Vertex, reinterpret_cast(vertexCode.data()), vertexCode, 0, {}}, {waveSize, 8, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, target, {0, 0, 0, 64} @@ -68,13 +73,13 @@ void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize) { {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, target, - {0, 0, vertexPush, 128 - vertexPush} + PixelPushLayout(vertexPush, target) }; fragment.useCache = false; const auto pixelResult = ShaderRecompiler::Recompile(fragment); const std::array shaders{{ {ShaderStage::Vertex, &vertexResult, 0}, - {ShaderStage::Fragment, &pixelResult, vertexPush} + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} }}; AgcDriver::Graphics::State state{}; @@ -97,12 +102,12 @@ void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize) { device.WaitIdle(); } -void Check(std::uint32_t waveSize) { - const auto what = "NGG vertex prologue wave" + std::to_string(waveSize); +void Check(std::uint32_t waveSize, const std::string& source) { + const auto what = "NGG vertex prologue wave" + std::to_string(waveSize) + " (" + source + ")"; for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { for (std::uint32_t channel = 0; channel < 4; ++channel) { const auto actual = Pixels[pixel * 4u + channel]; - Require(actual == std::byte{255}, what + ": pixel " + std::to_string(pixel) + " channel " + std::to_string(channel) + " is " + std::to_string(static_cast(actual)) + ", expected 255 (EXEC built from the merged wave info must cover every vertex)"); + Require(actual == std::byte{255}, what + ": pixel " + std::to_string(pixel) + " channel " + std::to_string(channel) + " is " + std::to_string(static_cast(actual)) + ", expected 255 (EXEC built from the " + source + " must cover every vertex)"); } } } @@ -118,8 +123,10 @@ int main() { return VulkanTestSkipped; } for (const auto waveSize : {64u, 32u}) { - Draw(*device, waveSize); - Check(waveSize); + Draw(*device, waveSize, VertexCode); + Check(waveSize, "merged wave info"); + Draw(*device, waveSize, GroupInfoVertexCode); + Check(waveSize, "group info"); } std::puts("NGG vertex wave info tests passed"); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NggVertexWaveReduction.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NggVertexWaveReduction.cpp new file mode 100644 index 0000000000..e5822da0d9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NggVertexWaveReduction.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 + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 32; +constexpr std::uint32_t Columns = 7; +constexpr std::uint32_t Rows = 5; +alignas(256) std::array Pixels{}; + +alignas(256) constexpr std::array VertexCode{ + 0xd7650015, 0x000100c1, 0xd7660015, 0x00022ac1, 0x7e0e0280, 0x7da206f9, 0x81060015, 0x360e0a81, + 0xbefe04c1, 0xd4c50002, 0x00020e80, 0xd763000f, 0x00022a81, 0xd5010000, 0x000a1e80, 0x380000fa, + 0xff091100, 0x380000fa, 0xff091200, 0x380000fa, 0xff091400, 0x380000fa, 0xff091800, 0xd778000f, + 0x03058300, 0x38001f00, 0x88040302, 0xd7600002, 0x00013f00, 0xd760006b, 0x00017f00, 0x88026b02, + 0xbf060402, 0x850580f2, 0xe0382000, 0x80020005, 0xbf8c3f70, 0x10000005, 0x10020205, 0x10040405, + 0x10060605, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 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; + for (std::uint32_t cell = 0; cell < Columns * Rows; ++cell) { + const float left = -1.0f + 2.0f * static_cast(cell % Columns) / Columns; + const float right = -1.0f + 2.0f * static_cast(cell % Columns + 1u) / Columns; + const float bottom = -1.0f + 2.0f * static_cast(cell / Columns) / Rows; + const float top = -1.0f + 2.0f * static_cast(cell / Columns + 1u) / Rows; + for (const auto& corner : {std::array{left, bottom}, std::array{right, bottom}, std::array{right, top}, std::array{left, bottom}, std::array{right, top}, std::array{left, top}}) { + vertices.push_back({corner[0], corner[1], 0.5f, 1.0f}); + } + } + return vertices; +} + +void Draw(AgcDriver::VulkanDevice& device) { + static const auto triangles = Triangles(); + Pixels.fill(std::byte{0}); + const auto target = device.Target(); + + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(triangles.data(), 16u, static_cast(triangles.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {64u, 8, 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, {}}, + {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(); +} + +void Check() { + for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { + const auto actual = std::to_integer(Pixels[pixel * 4u]); + Require(actual == 255u, "NGG vertex wave reduction: pixel " + std::to_string(pixel) + " is " + std::to_string(actual) + ", expected 255 (a vertex whose wave OR of its lanes' bits, read from lanes 31 and 63, differs from its wave's ballot collapses its triangle)"); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto subgroupSize = device->Target().subgroupSize; + if (subgroupSize != 32u) { + std::printf("skipped, s3 describes a 32-lane wave only on 32-lane subgroups, and the device has %u lanes\n", subgroupSize); + return VulkanTestSkipped; + } + if ((device->SubgroupStages() & VK_SHADER_STAGE_VERTEX_BIT) == 0u || !TargetHasCapability(device->Target(), spv::CapabilityGroupNonUniformArithmetic)) { + std::puts("skipped, the device has no subgroup arithmetic in vertex shaders"); + return VulkanTestSkipped; + } + for (std::uint32_t pass = 0; pass < 3; ++pass) { + Draw(*device); + Check(); + } + std::puts("NGG vertex wave 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/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 new file mode 100644 index 0000000000..03d1ada000 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/OversizedDispatch.cpp @@ -0,0 +1,213 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/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 { + +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; +constexpr std::uint32_t Period = 4; +constexpr std::uint32_t Groups = 129199u; +constexpr std::uint32_t Stores = Groups * Threads; + +alignas(256) constexpr std::array Code{ + 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}; +constexpr std::array Pattern{0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u}; + +class GuestBlock { +public: + explicit GuestBlock(std::size_t dwords) : Bytes(dwords * sizeof(std::uint32_t)) { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, Bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(256, Bytes)); +#endif + Require(block != nullptr, "oversized dispatch: cannot allocate the guest block"); + Clear(); + GuestAllocations::Mutation().Add(block, Bytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + void Clear() const { std::memset(block, 0, Bytes); } + auto* Data() const { return reinterpret_cast(block); } + const std::size_t Bytes; + +private: + std::uint8_t* block = nullptr; +}; + +void CheckSplitCoverage(AgcDriver::VulkanDevice& device) { + const GuestBlock guest(Stores); + const auto address = static_cast(reinterpret_cast(guest.Data())); + const std::vector userData{ + static_cast(address), static_cast((address >> 32u) & 0xffffu) | (4u << 16u), + Stores, 0x00014004u, Pattern[0], Pattern[1], Pattern[2], Pattern[3], Stores, Period}; + 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, {true, false, 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); + device.Dispatch(result, Groups, 1, 1, {}, reinterpret_cast(code.data())); + 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 " + Hex(guest.Data()[i]) + ", expected " + Hex(Pattern[i % Period])); + } +} + + +constexpr std::array LdsCode{ + 0x34020084, 0xe0381000, 0x80000401, 0x7e100208, 0x7e3e0209, 0x343e3e82, 0x4a103f08, 0x7e40020a, + 0x34404083, 0x4a104108, 0x34121090, 0x4a121300, 0x4a1412ff, 0x10000000, 0x7e160280, 0x7e180280, + 0x7e1a0281, 0x7e1e0281, 0x7e200280, 0xbf8c0000, 0xd9340000, 0x00000b06, 0xd834000c, 0x00000b06, + 0xd8340000, 0x00000904, 0xd8340000, 0x00000a05, 0xd8340000, 0x00000907, 0xbf8c0000, 0xbf8a0000, + 0xd880000c, 0x28000d06, 0xd9800000, 0x29000f06, 0x3a260884, 0x3a2a0aff, 0x00000080, 0x363608ff, + 0xfffffff8, 0xbf8c0000, 0xbf8a0000, 0xd8d80000, 0x2b000013, 0xd8d80000, 0x2c000015, 0xd8d80000, + 0x2d000007, 0xd8d8000c, 0x2e000006, 0xd9d80000, 0x2f000006, 0xd9d80000, 0x3100001b, 0xd8d80000, + 0x33000005, 0x343c1086, 0x4a3c3d00, 0x343c3c86, 0xbf8c0000, 0xe0781000, 0x8001281e, 0xe0781010, + 0x80012c1e, 0xe0781020, 0x8001301e, 0xbf8c0000, 0xbf810000, +}; +alignas(256) std::array LdsInput{}; +alignas(256) std::array LdsOutput{}; + +void CheckLdsSplitThrows(AgcDriver::VulkanDevice& device) { + const auto target = device.Target(); + const auto ldsBytes = target.maxWorkgroupSharedMemoryBytes + 4096u; + std::vector userData(8, 0u); + const auto descriptor = [&](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 = descriptor(LdsInput.data(), static_cast(LdsInput.size() * 4u)); + const auto output = descriptor(LdsOutput.data(), static_cast(LdsOutput.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(LdsCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, ldsBytes / 4u, {true, true, true}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderRecompiler::ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {64, 0, userData, compute, std::nullopt, std::nullopt, memory}, + target, {0, 0, 0, 128}, std::nullopt, false}; + 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; + } + device.WaitIdle(); + Require(threw, "oversized dispatch: a Y count over the limit with device-memory LDS did not throw"); +} + +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), 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, {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); + device.Dispatch(result, 1, groups, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + 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; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + CheckLdsSplitThrows(*device); + bool split = CheckGroupYSplitCoverage(*device); + if (device->WorkGroupCountLimit()[0] < Groups) { + CheckSplitCoverage(*device); + split = true; + } else { + 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(); + std::puts("oversized dispatch 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/PackedAlu.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedAlu.cpp index a54e9e63f1..b643f84060 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PackedAlu.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedAlu.cpp @@ -23,7 +23,7 @@ constexpr std::uint32_t Results = 64; alignas(256) std::array Input{}; alignas(256) std::array Output{}; -alignas(256) constexpr std::array Code{ +alignas(256) constexpr std::array Code{ 0x34020084, 0x34060088, 0xe0381000, 0x80000401, 0xbf8c3f70, 0xcc00400a, 0x1c1a0b04, 0xcc00280b, 0x141a0b04, 0xcc09400c, 0x1c1a0b04, 0xcc09300d, 0x0c1a0b04, 0xcc01400e, 0x1a020b04, 0xcc01580f, 0x02020b04, 0xcc024010, 0x1a020b04, 0xcc024811, 0x1a020b04, 0xcc0a4012, 0x1a020b04, 0xcc0a4013, @@ -35,10 +35,11 @@ alignas(256) constexpr std::array Code{ 0x1c1a0b04, 0xcc0e5828, 0x041a0b04, 0xcc0e4229, 0x3c1a0b04, 0xcc0f402a, 0x1a020b04, 0xcc0f482b, 0x1a020b04, 0xcc0f412c, 0x5a020b04, 0xcc10402d, 0x1a020b04, 0xcc10402e, 0x02020b04, 0xcc10402f, 0x7a020b04, 0xcc114030, 0x1a020b04, 0xcc115831, 0x1a020b04, 0xcc114232, 0x3a020b04, 0xcc124033, - 0x1a020b04, 0xcc124834, 0x12020b04, 0xcc124135, 0x5a020b04, 0xe0781000, 0x80010a03, 0xe0781010, + 0x1a020b04, 0xcc124834, 0x12020b04, 0xcc124135, 0x5a020b04, 0xcc11c036, 0x1a020b04, 0xcc11c837, + 0x12020b04, 0xcc12c038, 0x1a020b04, 0xcc12c139, 0x5a020b04, 0xe0781000, 0x80010a03, 0xe0781010, 0x80010e03, 0xe0781020, 0x80011203, 0xe0781030, 0x80011603, 0xe0781040, 0x80011a03, 0xe0781050, 0x80011e03, 0xe0781060, 0x80012203, 0xe0781070, 0x80012603, 0xe0781080, 0x80012a03, 0xe0781090, - 0x80012e03, 0xe07810a0, 0x80013203, 0xbf810000, + 0x80012e03, 0xe07810a0, 0x80013203, 0xe07810b0, 0x80013603, 0xbf810000, }; constexpr std::uint32_t Rows[32][4] = { @@ -75,75 +76,75 @@ constexpr std::uint32_t Rows[32][4] = { {0x00007c00u, 0xfc000000u, 0x7d557e55u, 0x00000000u}, {0x7c000000u, 0x00007c00u, 0xfe557d55u, 0x00000000u}, }; -constexpr std::uint32_t Expected[32][44] = { - {0x3c003c00u, 0x3c003c00u, 0x3c003c00u, 0x3c003c00u, 0x00000000u, 0x00000000u, 0x7c007c00u, 0x7c00fc00u, 0x7c007c00u, 0x7c007c00u, 0x7c007c00u, 0x7c007c00u, 0x7c007c00u, 0xfc00fc00u, 0x80000000u, 0x00008000u, 0x80000000u, 0x00008000u, 0x80000000u, 0x80000000u, 0xfc007c00u, 0x7c007c00u, 0x80000000u, 0x80008000u, 0xfc007c00u, 0x8000fc00u, 0x80000000u, 0x00007c00u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0xfc007c00u, 0xfc00fc00u, 0x7c007c00u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0xfc000000u, 0xfc00fc00u, 0xfc00fc00u, 0x80007c00u, 0x7c000000u, 0x7c007c00u}, - {0xbc007c00u, 0x7c00bc00u, 0xbc007c00u, 0x7c00bc00u, 0x00000000u, 0x00000000u, 0x78007800u, 0x7800f800u, 0x78007800u, 0x78007800u, 0x80008000u, 0x80008000u, 0x80008000u, 0x00000000u, 0x7c00fc00u, 0xfc007c00u, 0x7c00fc00u, 0xfc007c00u, 0x7c00fc00u, 0x7c00fc00u, 0x7c007c00u, 0x7c007c00u, 0xfc00fc00u, 0xfc00fc00u, 0xfc00fc00u, 0x7c00fc00u, 0x7c007c00u, 0xfc007c00u, 0xfc00fe00u, 0xfc00fe00u, 0xfc00fe00u, 0x7c007c00u, 0xfe00fe00u, 0xfe00fc00u, 0x7c007c00u, 0xfc00fc00u, 0xfc00fc00u, 0xfc00fc00u, 0xfc00fc00u, 0xfc00fc00u, 0xfc00fc00u, 0x7c007c00u, 0x7c00fc00u, 0x7c007c00u}, - {0x7bffc000u, 0xc000fbffu, 0x7bffc000u, 0x7fff3c00u, 0x43ff0400u, 0x040043ffu, 0xbc00fbffu, 0xbc003c00u, 0xbc00fbffu, 0xfbfffbffu, 0xbc02fc01u, 0xbc02bc02u, 0xbc02fc01u, 0x7c013c02u, 0xfffeffffu, 0xfffe7fffu, 0x3fffffffu, 0xffff3fffu, 0x3fffffffu, 0x3fffffffu, 0x7fffffffu, 0xffffffffu, 0x3c01fc00u, 0x3c013c01u, 0x7fffffffu, 0xfc00ffffu, 0x3c01fc00u, 0x3c017fffu, 0x7fffffffu, 0x7fffffffu, 0xffff7fffu, 0xffffffffu, 0x7fffffffu, 0x7fffffffu, 0x7fff7fffu, 0x7fffffffu, 0xffffffffu, 0x7fff7fffu, 0x3c01fc00u, 0x3c013c01u, 0x3c017c00u, 0x3c01fc00u, 0xfc003c01u, 0xbc01fc00u}, - {0xc0009400u, 0x1400c000u, 0xc0009400u, 0x1400c000u, 0x00008000u, 0x80000000u, 0x1400ffffu, 0x14009400u, 0x1400ffffu, 0xffffffffu, 0x1400ffffu, 0x14001400u, 0x1400ffffu, 0x7fff9400u, 0x00000000u, 0x80000000u, 0x00000001u, 0x00010000u, 0x0000ffffu, 0x00008000u, 0x14007fffu, 0x7fff7fffu, 0x00008000u, 0x00000000u, 0x14008000u, 0x7fff8000u, 0x00007fffu, 0x14000000u, 0xc0007fffu, 0xc0007fffu, 0x7fff1400u, 0xc000ffffu, 0x14007fffu, 0x14001400u, 0x94007fffu, 0x00007fffu, 0x7fff7fffu, 0x0000ffffu, 0x00008000u, 0x00000000u, 0x80008000u, 0x14008000u, 0x00001400u, 0x00000000u}, - {0x7e00d5feu, 0x7ffe8000u, 0x7e00d5feu, 0x81fe7e00u, 0x8000d600u, 0xd6008000u, 0x7fff7c55u, 0x7fff7dffu, 0x7fff7c55u, 0x7c557c55u, 0x7fff8055u, 0x7fff7fffu, 0x7fff8055u, 0x7e5581ffu, 0x0000c000u, 0x00000000u, 0x000103f0u, 0x03f00001u, 0xffff03f0u, 0xffff0000u, 0xffff7e00u, 0x7e007e00u, 0x8000fe55u, 0x80008000u, 0xfffffe55u, 0xfe55ffffu, 0x80007e00u, 0x7e008000u, 0xfffffe55u, 0xfffffe55u, 0xfe55ffffu, 0xffff7e55u, 0xfffffe55u, 0xffffffffu, 0x7ffffe55u, 0xfffffe55u, 0xfe55fe55u, 0xffff7e55u, 0x8000fe55u, 0x80008000u, 0x00007e55u, 0x8000fe55u, 0x8000ffffu, 0x8000fe55u}, - {0x80007656u, 0x4254c3feu, 0x80007656u, 0x4254c3feu, 0x00017801u, 0x78010001u, 0xfffe7802u, 0xfffebc00u, 0xfffe7802u, 0x78027802u, 0x00000000u, 0x00000000u, 0x00000000u, 0xbc0243feu, 0x80007802u, 0x8000fffeu, 0x00001e00u, 0x1e000000u, 0x00001e00u, 0x00000000u, 0x7fff3c01u, 0x3c013c01u, 0x7fff3c01u, 0x7fff7fffu, 0x7fff3c01u, 0x7fff7fffu, 0x7fff3c01u, 0x3c013c01u, 0x7ffffe55u, 0x7ffffe55u, 0x7fff7fffu, 0x7ffffe55u, 0x7fff4001u, 0x7fff7fffu, 0xffff0000u, 0x7fff3c02u, 0x3c023c02u, 0x7fff3c02u, 0x7fff3c01u, 0x7fff7fffu, 0x7fffbc01u, 0x7fff3c01u, 0x3c013c01u, 0xffff3c01u}, - {0x7bff6000u, 0x60007bffu, 0x7bff6000u, 0x60007bffu, 0x00000000u, 0x00000000u, 0xedcc6dccu, 0xedcc6dccu, 0xedcc6dccu, 0x6dcc6dccu, 0xedcc6dccu, 0xedccedccu, 0xedcc6dccu, 0xedcc6dccu, 0x00000000u, 0x00000000u, 0x00000008u, 0x00080000u, 0x0000fff8u, 0x0000fff8u, 0x0000edccu, 0xedccedccu, 0xedcc8000u, 0xedccedccu, 0xedccedccu, 0xedccedccu, 0x00008000u, 0x80000000u, 0x7bff6000u, 0x7bff6000u, 0x7bff6000u, 0x7bff6000u, 0xedccedccu, 0xedccedccu, 0x6dccedccu, 0x80000000u, 0x00000000u, 0x80000000u, 0xedccedccu, 0xedccedccu, 0xedcc8000u, 0x00008000u, 0x00008000u, 0x6dcc0000u}, - {0x00001dccu, 0xedccd000u, 0x00001dccu, 0xbdcc0000u, 0x00003000u, 0x30000000u, 0x8000ce34u, 0x80009634u, 0x8000ce34u, 0xce34ce34u, 0x8800a9ccu, 0x88008800u, 0x8800a9ccu, 0xc00071ccu, 0xfc001234u, 0x1234fc00u, 0xfc001234u, 0x1234fc00u, 0xfc001234u, 0xfc001234u, 0xfc001234u, 0x12341234u, 0x8400bc00u, 0x84008400u, 0xfc00bc00u, 0xfc008400u, 0x84001234u, 0x1234bc00u, 0x7c00edccu, 0x7c00edccu, 0x92347c00u, 0xfc00edccu, 0xfc00bbfeu, 0xfc0011b4u, 0xfc00bc01u, 0x7c009234u, 0x92349234u, 0x7c009234u, 0xfc00bc00u, 0xfc00fc00u, 0x84001234u, 0x84001234u, 0xbc001234u, 0x04009234u}, - {0xc99f5e41u, 0x4ceb74ddu, 0xc99f5e41u, 0xcd1df4abu, 0xcb9f0801u, 0x0801cb9fu, 0xb278f7feu, 0xb278f922u, 0xb278f7feu, 0xf7fef7feu, 0x47ce0000u, 0x47ce47ceu, 0x47ce0000u, 0x46aa0124u, 0xaaa88000u, 0xdff88000u, 0x06aa0000u, 0x000006aau, 0x06aa0000u, 0x06aa0f7fu, 0x7d237bffu, 0x7bff7bffu, 0x35557bffu, 0x35553555u, 0x7d237bffu, 0x7bff7d23u, 0x35557bffu, 0x7bff3555u, 0x7f237c00u, 0x7f237c00u, 0xfe007f23u, 0x7f23fc00u, 0x7f237c00u, 0x7f237f23u, 0xff230000u, 0x7f237c00u, 0x7c007c00u, 0x7f237c00u, 0x7f237bffu, 0x7f237f23u, 0x7f23fbffu, 0x7f237bffu, 0x7bff7f23u, 0xff237bffu}, - {0x00008000u, 0x80000000u, 0x00008000u, 0x80000000u, 0x80008000u, 0x80008000u, 0xb555fc01u, 0xb555b555u, 0xb555fc01u, 0xfc01fc01u, 0xb555fc01u, 0xb555b555u, 0xb555fc01u, 0xfc01b555u, 0x00000000u, 0x00000000u, 0x04004000u, 0x40000400u, 0xfc00c000u, 0xfc00fc00u, 0x35557c01u, 0x7c017c01u, 0x80008000u, 0x80008000u, 0x80008000u, 0x80008000u, 0x35557c01u, 0x35557c01u, 0x80007e01u, 0x80007e01u, 0x7e010000u, 0x0000fe01u, 0x35557e01u, 0x35553555u, 0xb5557e01u, 0x80007e01u, 0x7e017e01u, 0x8000fe01u, 0x80007e01u, 0x80008000u, 0x0000fe01u, 0x35557e01u, 0x7e013555u, 0x80007e01u}, - {0x03ff0000u, 0xa2480000u, 0x03ff0000u, 0x0000a248u, 0x03ff0000u, 0x000003ffu, 0xfc00c248u, 0xfc007fffu, 0xfc00c248u, 0xc248c248u, 0x03fec248u, 0x03fe03feu, 0x03fec248u, 0x46477fffu, 0x80000000u, 0x00000100u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x7fff0000u, 0x00000000u, 0x7c01c248u, 0x7c017c01u, 0x7fffc248u, 0xc2487fffu, 0x7c010000u, 0x00007c01u, 0x7fff0000u, 0x7fff0000u, 0x00007fffu, 0x7fff0000u, 0x7fffc248u, 0x7fff7fffu, 0xffffc248u, 0x7fff8000u, 0x80008000u, 0x7fff8000u, 0x7e01c248u, 0x7e017e01u, 0xfe010000u, 0x7e010000u, 0x7e010000u, 0x7e018000u}, - {0xc2337fffu, 0x87fd7fffu, 0xc2337fffu, 0xffff07fdu, 0x42348000u, 0x80004234u, 0x4e35bffeu, 0x4e35d234u, 0x4e35bffeu, 0xbffebffeu, 0xd6333ffeu, 0xd633d633u, 0xd6333ffeu, 0xc3fd5234u, 0xc0100000u, 0x00004000u, 0x03c00003u, 0x000303c0u, 0x03c0ffffu, 0x03c0fc00u, 0x3c01fffeu, 0xfffefffeu, 0x1234c000u, 0x12341234u, 0x3c01fffeu, 0xfffec000u, 0x1234c000u, 0x12343c01u, 0x7ffffffeu, 0x7ffffffeu, 0xfffeffffu, 0x7fff7ffeu, 0x3c02fffeu, 0x3c02bfffu, 0x3c00fffeu, 0x1236fffeu, 0xfffefffeu, 0x12367ffeu, 0x1234c000u, 0x12341234u, 0xbc01c000u, 0x3c01c000u, 0x3c011234u, 0x3c014000u}, - {0x9c00a56eu, 0xcffef560u, 0x9c00a56eu, 0x795e4c00u, 0x2000a570u, 0xa5702000u, 0xc6480000u, 0xc6483014u, 0xc6480000u, 0x00000000u, 0xbe482468u, 0xbe48be48u, 0xbe482468u, 0x8e34547cu, 0x0000dcc0u, 0xcc004000u, 0x00840edcu, 0x0edc0084u, 0xff84fedcu, 0xff84ffedu, 0x42481234u, 0x12341234u, 0x8400edccu, 0x84008400u, 0x8400edccu, 0x8400edccu, 0x42481234u, 0x42481234u, 0x7c00fffeu, 0x7c00fffeu, 0x7c00fffeu, 0x7c00fffeu, 0x4248edccu, 0x4248edcbu, 0xc2486dccu, 0x8a48c47fu, 0xc47fc47fu, 0x8a48c47fu, 0x8400edccu, 0x84008400u, 0x0400edccu, 0x42481234u, 0x12344248u, 0x84006dccu}, - {0x04023a00u, 0x3a000402u, 0x04023a00u, 0x38020600u, 0x00020200u, 0x02000002u, 0xfffd7dffu, 0xfffd7ffdu, 0xfffd7dffu, 0x7dff7dffu, 0xffff7e01u, 0xffffffffu, 0xffff7e01u, 0xfe017fffu, 0xc0007fffu, 0xc000ffffu, 0x00037fffu, 0x7fff0003u, 0xffff7fffu, 0xffff0001u, 0xffff7fffu, 0x7fff7fffu, 0xfffefe00u, 0xfffefffeu, 0xfffffe00u, 0xfffffffeu, 0xfffe7fffu, 0x7ffffe00u, 0xfffefe00u, 0xfffefe00u, 0xfe00fffeu, 0xfffe7e00u, 0xfffefe00u, 0xfffefffeu, 0x7ffefe00u, 0xfffefe00u, 0xfe00fe00u, 0xfffe7e00u, 0xfffefe00u, 0xfffefffeu, 0xfffe7e00u, 0xfffefe00u, 0xfe00fffeu, 0x7ffefe00u}, - {0x96003bb8u, 0xee00ebccu, 0x96003bb8u, 0x6bcc6e00u, 0x9800bdb8u, 0xbdb89800u, 0x9034c247u, 0x90349233u, 0x9034c247u, 0xc247c247u, 0x9434c249u, 0x94349434u, 0x9434c249u, 0xc4489235u, 0xe000ff00u, 0xfff00000u, 0x07e0007fu, 0x007f07e0u, 0x07e0007fu, 0x07e007ffu, 0x7e007fffu, 0x7fff7fffu, 0x12344248u, 0x12341234u, 0x7e007fffu, 0x7e007fffu, 0x12344248u, 0x12344248u, 0x7e007fffu, 0x7e007fffu, 0x7fff7e00u, 0xfe007fffu, 0x7e007fffu, 0x7e007fffu, 0x7e00ffffu, 0x7e007fffu, 0x7fff7fffu, 0x7e00ffffu, 0x12344248u, 0x12341234u, 0x1234c248u, 0x12344248u, 0x42481234u, 0x92344248u}, - {0x80000800u, 0x08006340u, 0x80000800u, 0xeb408000u, 0x00000000u, 0x00000000u, 0x35555240u, 0x35558b95u, 0x35555240u, 0x52405240u, 0x3555a5c0u, 0x35553555u, 0x3555a5c0u, 0xfc00df15u, 0x00005640u, 0xc8000000u, 0x00005640u, 0x56400000u, 0x00005640u, 0x000002b2u, 0x35555640u, 0x56405640u, 0x0000fc00u, 0x00000000u, 0x3555fc00u, 0xfc005640u, 0x00005640u, 0x35550000u, 0x0000fc00u, 0x0000fc00u, 0xfc000800u, 0x80007c00u, 0x3555fc00u, 0x35555645u, 0xb555fc00u, 0x0000fc00u, 0xfc00fc00u, 0x0000fc00u, 0x0000fc00u, 0x00000000u, 0x80005640u, 0x35555640u, 0x00005640u, 0x0000d640u}, - {0xdd70b2c8u, 0x611b56e4u, 0xdd70b2c8u, 0x9ee3191cu, 0xa57032c9u, 0x32c9a570u, 0x0000fa46u, 0x00006aefu, 0x0000fa46u, 0xfa46fa46u, 0xdb980000u, 0xdb98db98u, 0xdb980000u, 0x6aef70a9u, 0x4000e918u, 0x300091a0u, 0x00010fa4u, 0x0fa40001u, 0x00010fa4u, 0x00010007u, 0x12347d23u, 0x7d237d23u, 0xedcc7d23u, 0xedccedccu, 0xedcc7d23u, 0x7d23edccu, 0x12347d23u, 0x7d231234u, 0xc3fd7f23u, 0xc3fd7f23u, 0x7f237fffu, 0x44ffff23u, 0xedcc7f23u, 0xedcc7f23u, 0x6dcc7f23u, 0xc47f7f23u, 0x7f237f23u, 0xc47fff23u, 0xedcc7f23u, 0xedccedccu, 0xedccff23u, 0x12347f23u, 0x7f237f23u, 0x6dcc7f23u}, - {0xc000d640u, 0xd6413c00u, 0xc000d640u, 0x5640bc01u, 0x84008000u, 0x80008400u, 0xfbff7fffu, 0xfbfffc00u, 0xfbff7fffu, 0x7fff7fffu, 0xfc017fffu, 0xfc01fc01u, 0xfc017fffu, 0x8000fc00u, 0x7fff0000u, 0x80008000u, 0x7fff0001u, 0x00017fffu, 0x7fffffffu, 0x7fff8000u, 0x7fffffffu, 0xffffffffu, 0x7c008000u, 0x7c007c00u, 0x7fffffffu, 0xffff8000u, 0x7c008000u, 0x7c007fffu, 0x7fffffffu, 0x7fffffffu, 0xffff7fffu, 0xffff7fffu, 0x7fffffffu, 0x7fff7c00u, 0x7fffffffu, 0x7fffffffu, 0xffffffffu, 0x7fff7fffu, 0x7c008000u, 0x7c007c00u, 0x7c008000u, 0x7c008000u, 0xffff7c00u, 0xfc000000u}, - {0xfffecd56u, 0x38002153u, 0xfffecd56u, 0x5955fffeu, 0x00009556u, 0x95560000u, 0x3c013553u, 0x3c017156u, 0x3c013553u, 0x35533553u, 0x3c01caa9u, 0x3c013c01u, 0x3c01caa9u, 0xfffe06acu, 0x00004000u, 0x6aaa0000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00001aaau, 0x3c013555u, 0x35553555u, 0x0000fffeu, 0x00000000u, 0x3c01fffeu, 0xfffe3c01u, 0x00003555u, 0x35550000u, 0xfffefffeu, 0xfffefffeu, 0xfffe3800u, 0xfffe7ffeu, 0x3c01fffeu, 0x3c013d56u, 0xbc01fffeu, 0x0000fffeu, 0xfffefffeu, 0x00007ffeu, 0x00003555u, 0x00000000u, 0x80003555u, 0x3c013555u, 0x00003c01u, 0x0000b555u}, - {0x31b86c40u, 0xd5b84840u, 0x31b86c40u, 0x6c40b1b8u, 0x1db83440u, 0x34401db8u, 0x3e478490u, 0x3e470490u, 0x3e478490u, 0x84908490u, 0x46490000u, 0x46494649u, 0x46490000u, 0xc6498000u, 0xff004800u, 0x4800ff00u, 0x007b0042u, 0x0042007bu, 0x007b0042u, 0x007b0042u, 0x7bff4248u, 0x42484248u, 0xc2484248u, 0xc248c248u, 0xc2484248u, 0x7bffc248u, 0x7bff4248u, 0x42484248u, 0xfc00492fu, 0xfc00492fu, 0x48effc00u, 0x7c00c8afu, 0x7bff4648u, 0x7bff0000u, 0x7bff0000u, 0xfc0048efu, 0x48ef48efu, 0xfc0048efu, 0xc2484248u, 0xc248c248u, 0xfbffc248u, 0x7bff4248u, 0x7bff4248u, 0x7bff4248u}, - {0x9a908000u, 0xdcbc4000u, 0x9a908000u, 0x80009cbcu, 0x5a900000u, 0x00005a90u, 0xdb98fe55u, 0xdb98edccu, 0xdb98fe55u, 0xfe55fe55u, 0x0000fe55u, 0x00000000u, 0x0000fe55u, 0x1089edccu, 0xc0000000u, 0x0000b980u, 0x000e0000u, 0x0000000eu, 0xfffe0000u, 0xfffe0000u, 0xedcc0000u, 0x00000000u, 0xedccfe55u, 0xedccedccu, 0xedccfe55u, 0xfe55edccu, 0xedcc0000u, 0x0000edccu, 0x7c00fe55u, 0x7c00fe55u, 0xfe557c00u, 0xfc007e55u, 0xf1ccfe55u, 0xf1ccedccu, 0x0000fe55u, 0x7c00fe55u, 0xfe55fe55u, 0x7c007e55u, 0xedcc0000u, 0xedccedccu, 0xedcc0000u, 0xedcc0000u, 0xedcc0000u, 0x6dcc8000u}, - {0x12344400u, 0x14000a34u, 0x12344400u, 0x0c001234u, 0x00003000u, 0x30000000u, 0xfffef1ccu, 0xfffe03feu, 0xfffef1ccu, 0xf1ccf1ccu, 0xfffee9ccu, 0xfffefffeu, 0xfffee9ccu, 0xedccfbfeu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000400u, 0x04000400u, 0xfffeedccu, 0xfffefffeu, 0xfffeedccu, 0xedccfffeu, 0x00000400u, 0x04000000u, 0xfffeb5c8u, 0xfffeb5c8u, 0xb5c9fffeu, 0xfffe35d0u, 0xfffeedccu, 0xfffefffeu, 0x7ffeedccu, 0xfffeb5ccu, 0xb5ccb5ccu, 0xfffeb5ccu, 0x0000edccu, 0x00000000u, 0x80000400u, 0x00000400u, 0x00000400u, 0x00008400u}, - {0x7fff9c00u, 0xbc00c69eu, 0x7fff9c00u, 0x029f7fffu, 0x0000e000u, 0xe0000000u, 0x7d235e55u, 0x7d237b78u, 0x7d235e55u, 0x5e555e55u, 0x7d2361abu, 0x7d237d23u, 0x7d2361abu, 0x60007eceu, 0x0000fe55u, 0xf2a80000u, 0x0000fe55u, 0xfe550000u, 0x0000fe55u, 0x0000ffcau, 0x7d236000u, 0x60006000u, 0x0000fe55u, 0x00000000u, 0x7d23fe55u, 0x6000fe55u, 0x00006000u, 0x7d230000u, 0x7f23fe55u, 0x7f23fe55u, 0xfe557f23u, 0x7f23fe55u, 0x7f23fe55u, 0x7f237f23u, 0xff237e55u, 0x7f23fe55u, 0xfe55fe55u, 0x7f237e55u, 0x7f236000u, 0x7f237f23u, 0x7f23e000u, 0x7f236000u, 0x60007f23u, 0xff236000u}, - {0x3c008400u, 0x84007c00u, 0x3c008400u, 0x84007c00u, 0xc0000000u, 0x0000c000u, 0x3c018000u, 0x3c017c01u, 0x3c018000u, 0x80008000u, 0xbc018000u, 0xbc01bc01u, 0xbc018000u, 0xc0007c01u, 0x80000000u, 0x0000c000u, 0x60000000u, 0x00006000u, 0xe0000000u, 0xe0000000u, 0x7c010000u, 0x00000000u, 0xc0008000u, 0xc000c000u, 0xc0008000u, 0xc0007c01u, 0x7c010000u, 0x00008000u, 0x7e018400u, 0x7e018400u, 0x7c007e01u, 0x7e018400u, 0x7e010000u, 0x7e017e01u, 0xfe018000u, 0x7e018000u, 0x80008000u, 0x7e018000u, 0x7e018000u, 0x7e017e01u, 0x7e010000u, 0x7e010000u, 0x80007e01u, 0xfe018000u}, - {0x40006fe8u, 0xaa007de8u, 0x40006fe8u, 0xe7e84000u, 0x2c00f1e8u, 0xf1e82c00u, 0xb155c09du, 0xb155f79du, 0xb155c09du, 0xc09dc09du, 0xb9553c0du, 0xb955b955u, 0xb9553c0du, 0x8255730du, 0x80004900u, 0x49008000u, 0x03e00612u, 0x061203e0u, 0x03e0fe12u, 0x03e0fe12u, 0x7c00fe55u, 0xfe55fe55u, 0x3555c248u, 0x35553555u, 0x7c00fe55u, 0xfe55c248u, 0x3555c248u, 0x35557c00u, 0x7c00fe55u, 0x7c00fe55u, 0xfe557e00u, 0xfc007e55u, 0x7c00fe55u, 0x7c00c19du, 0x7c00fe55u, 0x7c00fe55u, 0xfe55fe55u, 0x7c007e55u, 0x3555c248u, 0x35553555u, 0xfc00c248u, 0x7c00c248u, 0x7c003555u, 0x7c004248u}, - {0x755fffffu, 0x5e480e33u, 0x755fffffu, 0x4e341e47u, 0x7960c3ffu, 0xc3ff7960u, 0x30143c00u, 0x3014edcbu, 0x30143c00u, 0x3c003c00u, 0xab843c02u, 0xab84ab84u, 0xab843c02u, 0xf9b9edcdu, 0x8000fffeu, 0xf0008490u, 0x00047fffu, 0x7fff0004u, 0x0004ffffu, 0x0004ffffu, 0x42483c01u, 0x3c013c01u, 0xedccffffu, 0xedccedccu, 0xedccffffu, 0x4248ffffu, 0x42483c01u, 0xedcc3c01u, 0xfc00ffffu, 0xfc00ffffu, 0xfffff48du, 0xf9b8ffffu, 0xedcbffffu, 0xedcbffffu, 0x6dcd7fffu, 0xf48dffffu, 0xffffffffu, 0xf48d7fffu, 0xedcc3c01u, 0xedccedccu, 0xedccbc01u, 0x42483c01u, 0x4248edccu, 0x6dcc3c01u}, - {0x8132edccu, 0x70a9fe55u, 0x8132edccu, 0xedcc8132u, 0x82dd0000u, 0x000082ddu, 0x7d227d23u, 0x7d227d23u, 0x7d227d23u, 0x7d237d23u, 0x7d247d23u, 0x7d247d24u, 0x7d247d23u, 0x7d247d23u, 0xfff80000u, 0x0000fff8u, 0x1fff0000u, 0x00001fffu, 0xffff0000u, 0xffff0000u, 0x7d237d23u, 0x7d237d23u, 0xffff0000u, 0xffffffffu, 0xffff7d23u, 0xffff7d23u, 0x7d230000u, 0x00007d23u, 0x7f237f23u, 0x7f237f23u, 0x7f237f23u, 0x7f23ff23u, 0x7f237f23u, 0x7f237f23u, 0xff237f23u, 0x7f237f23u, 0x7f237f23u, 0x7f23ff23u, 0x7f237f23u, 0x7f237f23u, 0x7f23ff23u, 0x7f237f23u, 0x7f237f23u, 0xff237f23u}, - {0x00008000u, 0x80002000u, 0x00008000u, 0xa0000000u, 0x00000000u, 0x00000000u, 0xc2480400u, 0xc248c648u, 0xc2480400u, 0x04000400u, 0xc248fc00u, 0xc248c248u, 0xc248fc00u, 0x0000be48u, 0x00000400u, 0x00000000u, 0x00000400u, 0x04000000u, 0x00000400u, 0x00000004u, 0x00000400u, 0x04000400u, 0xc2480000u, 0xc248c248u, 0xc2480400u, 0x0000c248u, 0x00000000u, 0x04000000u, 0x00000000u, 0x00000000u, 0x00008000u, 0x00008000u, 0xc2480400u, 0xc248c248u, 0x42488400u, 0x80000000u, 0x00000000u, 0x80000000u, 0xc2480000u, 0xc248c248u, 0xc2488000u, 0x00000400u, 0x00000400u, 0x42480000u}, - {0xcaab5400u, 0x9634ac00u, 0xcaab5400u, 0x30001234u, 0xcaabd000u, 0xd000caabu, 0x3554e9ccu, 0x35543155u, 0x3554e9ccu, 0xe9cce9ccu, 0x3556f1ccu, 0x35563556u, 0x3556f1ccu, 0xedcd3955u, 0xffe00000u, 0x8000f000u, 0x07ff000fu, 0x000f07ffu, 0xffffffffu, 0xffffffe0u, 0x3555fc00u, 0xfc00fc00u, 0xffffedccu, 0xffffffffu, 0xfffffc00u, 0xfffffc00u, 0x3555edccu, 0x3555edccu, 0xffff7c00u, 0xffff7c00u, 0x7c00ffffu, 0x7ffffc00u, 0xfffffc00u, 0xfffffc00u, 0xffff7c00u, 0xffff7c00u, 0x7c007c00u, 0xffff7c00u, 0x3555fc00u, 0x35553555u, 0x3555fc00u, 0x3555edccu, 0xedcc3555u, 0xb5557c00u}, - {0x8400c000u, 0xa11c8400u, 0x8400c000u, 0xc000651cu, 0x00000000u, 0x00000000u, 0x7d231234u, 0x7d230000u, 0x7d231234u, 0x12341234u, 0x82dd1234u, 0x82dd82ddu, 0x82dd1234u, 0x95110000u, 0x7d230000u, 0x0000d230u, 0x7d230000u, 0x00007d23u, 0x7d230000u, 0x7d230000u, 0x7d231234u, 0x12341234u, 0x00000000u, 0x00000000u, 0x7d231234u, 0x7d230000u, 0x00000000u, 0x00001234u, 0x7f23c000u, 0x7f23c000u, 0x84007f23u, 0xff23c000u, 0x7f231234u, 0x7f230000u, 0x7f231234u, 0x7f230000u, 0x00000000u, 0x7f230000u, 0x7f230000u, 0x7f237f23u, 0xff239234u, 0x7f231234u, 0x7f230000u, 0x7f231234u}, - {0x7d557e55u, 0x7e557d55u, 0x7d557e55u, 0x7e557d55u, 0x00000000u, 0x00000000u, 0xfc007c00u, 0xfc000000u, 0xfc007c00u, 0x7c007c00u, 0x04007c00u, 0x04000400u, 0x04007c00u, 0x80000000u, 0xfc000000u, 0x0000fc00u, 0xfc000000u, 0x0000fc00u, 0xfc000000u, 0xfc000000u, 0x00007c00u, 0x7c007c00u, 0xfc000000u, 0xfc00fc00u, 0xfc007c00u, 0xfc000000u, 0x00000000u, 0x00007c00u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0xfc007c00u, 0xfc000000u, 0xfc007c00u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0xfc000000u, 0xfc00fc00u, 0x0000fc00u, 0x00007c00u, 0x7c000000u, 0x80007c00u}, - {0xfe557d55u, 0x7d55fe55u, 0xfe557d55u, 0x7d55fe55u, 0x00000000u, 0x00000000u, 0x7c007c00u, 0x7c00f800u, 0x7c007c00u, 0x7c007c00u, 0x7c008400u, 0x7c007c00u, 0x7c008400u, 0x00000000u, 0x00007c00u, 0x7c000000u, 0x00007c00u, 0x7c000000u, 0x00007c00u, 0x00007c00u, 0x7c007c00u, 0x7c007c00u, 0x00000000u, 0x00000000u, 0x7c007c00u, 0x00007c00u, 0x00000000u, 0x7c000000u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0x7c007c00u, 0x7c007c00u, 0xfc00fc00u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0x00000000u, 0x00000000u, 0x80008000u, 0x7c007c00u, 0x00007c00u, 0x00000000u}, +constexpr std::uint32_t Expected[32][48] = { + {0x3c003c00u, 0x3c003c00u, 0x3c003c00u, 0x3c003c00u, 0x00000000u, 0x00000000u, 0x7c007c00u, 0x7c00fc00u, 0x7c007c00u, 0x7c007c00u, 0x7c007c00u, 0x7c007c00u, 0x7c007c00u, 0xfc00fc00u, 0x80000000u, 0x00008000u, 0x80000000u, 0x00008000u, 0x80000000u, 0x80000000u, 0xfc007c00u, 0x7c007c00u, 0x80000000u, 0x80008000u, 0xfc007c00u, 0x8000fc00u, 0x80000000u, 0x00007c00u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0xfc007c00u, 0xfc00fc00u, 0x7c007c00u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0xfc000000u, 0xfc00fc00u, 0xfc00fc00u, 0x80007c00u, 0x7c000000u, 0x7c007c00u, 0x00000000u, 0x00000000u, 0x00003c00u, 0x3c003c00u}, + {0xbc007c00u, 0x7c00bc00u, 0xbc007c00u, 0x7c00bc00u, 0x00000000u, 0x00000000u, 0x78007800u, 0x7800f800u, 0x78007800u, 0x78007800u, 0x80008000u, 0x80008000u, 0x80008000u, 0x00000000u, 0x7c00fc00u, 0xfc007c00u, 0x7c00fc00u, 0xfc007c00u, 0x7c00fc00u, 0x7c00fc00u, 0x7c007c00u, 0x7c007c00u, 0xfc00fc00u, 0xfc00fc00u, 0xfc00fc00u, 0x7c00fc00u, 0x7c007c00u, 0xfc007c00u, 0xfc00fe00u, 0xfc00fe00u, 0xfc00fe00u, 0x7c007c00u, 0xfe00fe00u, 0xfe00fc00u, 0x7c007c00u, 0xfc00fc00u, 0xfc00fc00u, 0xfc00fc00u, 0xfc00fc00u, 0xfc00fc00u, 0xfc00fc00u, 0x7c007c00u, 0x7c00fc00u, 0x7c007c00u, 0x00000000u, 0x3c000000u, 0x3c003c00u, 0x3c003c00u}, + {0x7bffc000u, 0xc000fbffu, 0x7bffc000u, 0x7fff3c00u, 0x43ff0400u, 0x040043ffu, 0xbc00fbffu, 0xbc003c00u, 0xbc00fbffu, 0xfbfffbffu, 0xbc02fc01u, 0xbc02bc02u, 0xbc02fc01u, 0x7c013c02u, 0xfffeffffu, 0xfffe7fffu, 0x3fffffffu, 0xffff3fffu, 0x3fffffffu, 0x3fffffffu, 0x7fffffffu, 0xffffffffu, 0x3c01fc00u, 0x3c013c01u, 0x7fffffffu, 0xfc00ffffu, 0x3c01fc00u, 0x3c017fffu, 0x7fffffffu, 0x7fffffffu, 0xffff7fffu, 0xffffffffu, 0x7fffffffu, 0x7fffffffu, 0x7fff7fffu, 0x7fffffffu, 0xffffffffu, 0x7fff7fffu, 0x3c01fc00u, 0x3c013c01u, 0x3c017c00u, 0x3c01fc00u, 0xfc003c01u, 0xbc01fc00u, 0x3c000000u, 0x00003c00u, 0x3c000000u, 0x00000000u}, + {0xc0009400u, 0x1400c000u, 0xc0009400u, 0x1400c000u, 0x00008000u, 0x80000000u, 0x1400ffffu, 0x14009400u, 0x1400ffffu, 0xffffffffu, 0x1400ffffu, 0x14001400u, 0x1400ffffu, 0x7fff9400u, 0x00000000u, 0x80000000u, 0x00000001u, 0x00010000u, 0x0000ffffu, 0x00008000u, 0x14007fffu, 0x7fff7fffu, 0x00008000u, 0x00000000u, 0x14008000u, 0x7fff8000u, 0x00007fffu, 0x14000000u, 0xc0007fffu, 0xc0007fffu, 0x7fff1400u, 0xc000ffffu, 0x14007fffu, 0x14001400u, 0x94007fffu, 0x00007fffu, 0x7fff7fffu, 0x0000ffffu, 0x00008000u, 0x00000000u, 0x80008000u, 0x14008000u, 0x00001400u, 0x00000000u, 0x00000000u, 0x00000000u, 0x14000000u, 0x00000000u}, + {0x7e00d5feu, 0x7ffe8000u, 0x7e00d5feu, 0x81fe7e00u, 0x8000d600u, 0xd6008000u, 0x7fff7c55u, 0x7fff7dffu, 0x7fff7c55u, 0x7c557c55u, 0x7fff8055u, 0x7fff7fffu, 0x7fff8055u, 0x7e5581ffu, 0x0000c000u, 0x00000000u, 0x000103f0u, 0x03f00001u, 0xffff03f0u, 0xffff0000u, 0xffff7e00u, 0x7e007e00u, 0x8000fe55u, 0x80008000u, 0xfffffe55u, 0xfe55ffffu, 0x80007e00u, 0x7e008000u, 0xfffffe55u, 0xfffffe55u, 0xfe55ffffu, 0xffff7e55u, 0xfffffe55u, 0xffffffffu, 0x7ffffe55u, 0xfffffe55u, 0xfe55fe55u, 0xffff7e55u, 0x8000fe55u, 0x80008000u, 0x00007e55u, 0x8000fe55u, 0x8000ffffu, 0x8000fe55u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x80007656u, 0x4254c3feu, 0x80007656u, 0x4254c3feu, 0x00017801u, 0x78010001u, 0xfffe7802u, 0xfffebc00u, 0xfffe7802u, 0x78027802u, 0x00000000u, 0x00000000u, 0x00000000u, 0xbc0243feu, 0x80007802u, 0x8000fffeu, 0x00001e00u, 0x1e000000u, 0x00001e00u, 0x00000000u, 0x7fff3c01u, 0x3c013c01u, 0x7fff3c01u, 0x7fff7fffu, 0x7fff3c01u, 0x7fff7fffu, 0x7fff3c01u, 0x3c013c01u, 0x7ffffe55u, 0x7ffffe55u, 0x7fff7fffu, 0x7ffffe55u, 0x7fff4001u, 0x7fff7fffu, 0xffff0000u, 0x7fff3c02u, 0x3c023c02u, 0x7fff3c02u, 0x7fff3c01u, 0x7fff7fffu, 0x7fffbc01u, 0x7fff3c01u, 0x3c013c01u, 0xffff3c01u, 0x00003c00u, 0x3c003c00u, 0x00003c00u, 0x00003c00u}, + {0x7bff6000u, 0x60007bffu, 0x7bff6000u, 0x60007bffu, 0x00000000u, 0x00000000u, 0xedcc6dccu, 0xedcc6dccu, 0xedcc6dccu, 0x6dcc6dccu, 0xedcc6dccu, 0xedccedccu, 0xedcc6dccu, 0xedcc6dccu, 0x00000000u, 0x00000000u, 0x00000008u, 0x00080000u, 0x0000fff8u, 0x0000fff8u, 0x0000edccu, 0xedccedccu, 0xedcc8000u, 0xedccedccu, 0xedccedccu, 0xedccedccu, 0x00008000u, 0x80000000u, 0x7bff6000u, 0x7bff6000u, 0x7bff6000u, 0x7bff6000u, 0xedccedccu, 0xedccedccu, 0x6dccedccu, 0x80000000u, 0x00000000u, 0x80000000u, 0xedccedccu, 0xedccedccu, 0xedcc8000u, 0x00008000u, 0x00008000u, 0x6dcc0000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3c000000u}, + {0x00001dccu, 0xedccd000u, 0x00001dccu, 0xbdcc0000u, 0x00003000u, 0x30000000u, 0x8000ce34u, 0x80009634u, 0x8000ce34u, 0xce34ce34u, 0x8800a9ccu, 0x88008800u, 0x8800a9ccu, 0xc00071ccu, 0xfc001234u, 0x1234fc00u, 0xfc001234u, 0x1234fc00u, 0xfc001234u, 0xfc001234u, 0xfc001234u, 0x12341234u, 0x8400bc00u, 0x84008400u, 0xfc00bc00u, 0xfc008400u, 0x84001234u, 0x1234bc00u, 0x7c00edccu, 0x7c00edccu, 0x92347c00u, 0xfc00edccu, 0xfc00bbfeu, 0xfc0011b4u, 0xfc00bc01u, 0x7c009234u, 0x92349234u, 0x7c009234u, 0xfc00bc00u, 0xfc00fc00u, 0x84001234u, 0x84001234u, 0xbc001234u, 0x04009234u, 0x00000000u, 0x00000000u, 0x00001234u, 0x04000000u}, + {0xc99f5e41u, 0x4ceb74ddu, 0xc99f5e41u, 0xcd1df4abu, 0xcb9f0801u, 0x0801cb9fu, 0xb278f7feu, 0xb278f922u, 0xb278f7feu, 0xf7fef7feu, 0x47ce0000u, 0x47ce47ceu, 0x47ce0000u, 0x46aa0124u, 0xaaa88000u, 0xdff88000u, 0x06aa0000u, 0x000006aau, 0x06aa0000u, 0x06aa0f7fu, 0x7d237bffu, 0x7bff7bffu, 0x35557bffu, 0x35553555u, 0x7d237bffu, 0x7bff7d23u, 0x35557bffu, 0x7bff3555u, 0x7f237c00u, 0x7f237c00u, 0xfe007f23u, 0x7f23fc00u, 0x7f237c00u, 0x7f237f23u, 0xff230000u, 0x7f237c00u, 0x7c007c00u, 0x7f237c00u, 0x7f237bffu, 0x7f237f23u, 0x7f23fbffu, 0x7f237bffu, 0x7bff7f23u, 0xff237bffu, 0x00003c00u, 0x35550000u, 0x00003c00u, 0x00003c00u}, + {0x00008000u, 0x80000000u, 0x00008000u, 0x80000000u, 0x80008000u, 0x80008000u, 0xb555fc01u, 0xb555b555u, 0xb555fc01u, 0xfc01fc01u, 0xb555fc01u, 0xb555b555u, 0xb555fc01u, 0xfc01b555u, 0x00000000u, 0x00000000u, 0x04004000u, 0x40000400u, 0xfc00c000u, 0xfc00fc00u, 0x35557c01u, 0x7c017c01u, 0x80008000u, 0x80008000u, 0x80008000u, 0x80008000u, 0x35557c01u, 0x35557c01u, 0x80007e01u, 0x80007e01u, 0x7e010000u, 0x0000fe01u, 0x35557e01u, 0x35553555u, 0xb5557e01u, 0x80007e01u, 0x7e017e01u, 0x8000fe01u, 0x80007e01u, 0x80008000u, 0x0000fe01u, 0x35557e01u, 0x7e013555u, 0x80007e01u, 0x00000000u, 0x00000000u, 0x35550000u, 0x00000000u}, + {0x03ff0000u, 0xa2480000u, 0x03ff0000u, 0x0000a248u, 0x03ff0000u, 0x000003ffu, 0xfc00c248u, 0xfc007fffu, 0xfc00c248u, 0xc248c248u, 0x03fec248u, 0x03fe03feu, 0x03fec248u, 0x46477fffu, 0x80000000u, 0x00000100u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x7fff0000u, 0x00000000u, 0x7c01c248u, 0x7c017c01u, 0x7fffc248u, 0xc2487fffu, 0x7c010000u, 0x00007c01u, 0x7fff0000u, 0x7fff0000u, 0x00007fffu, 0x7fff0000u, 0x7fffc248u, 0x7fff7fffu, 0xffffc248u, 0x7fff8000u, 0x80008000u, 0x7fff8000u, 0x7e01c248u, 0x7e017e01u, 0xfe010000u, 0x7e010000u, 0x7e010000u, 0x7e018000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0xc2337fffu, 0x87fd7fffu, 0xc2337fffu, 0xffff07fdu, 0x42348000u, 0x80004234u, 0x4e35bffeu, 0x4e35d234u, 0x4e35bffeu, 0xbffebffeu, 0xd6333ffeu, 0xd633d633u, 0xd6333ffeu, 0xc3fd5234u, 0xc0100000u, 0x00004000u, 0x03c00003u, 0x000303c0u, 0x03c0ffffu, 0x03c0fc00u, 0x3c01fffeu, 0xfffefffeu, 0x1234c000u, 0x12341234u, 0x3c01fffeu, 0xfffec000u, 0x1234c000u, 0x12343c01u, 0x7ffffffeu, 0x7ffffffeu, 0xfffeffffu, 0x7fff7ffeu, 0x3c02fffeu, 0x3c02bfffu, 0x3c00fffeu, 0x1236fffeu, 0xfffefffeu, 0x12367ffeu, 0x1234c000u, 0x12341234u, 0xbc01c000u, 0x3c01c000u, 0x3c011234u, 0x3c014000u, 0x12340000u, 0x3c000000u, 0x3c000000u, 0x3c003c00u}, + {0x9c00a56eu, 0xcffef560u, 0x9c00a56eu, 0x795e4c00u, 0x2000a570u, 0xa5702000u, 0xc6480000u, 0xc6483014u, 0xc6480000u, 0x00000000u, 0xbe482468u, 0xbe48be48u, 0xbe482468u, 0x8e34547cu, 0x0000dcc0u, 0xcc004000u, 0x00840edcu, 0x0edc0084u, 0xff84fedcu, 0xff84ffedu, 0x42481234u, 0x12341234u, 0x8400edccu, 0x84008400u, 0x8400edccu, 0x8400edccu, 0x42481234u, 0x42481234u, 0x7c00fffeu, 0x7c00fffeu, 0x7c00fffeu, 0x7c00fffeu, 0x4248edccu, 0x4248edcbu, 0xc2486dccu, 0x8a48c47fu, 0xc47fc47fu, 0x8a48c47fu, 0x8400edccu, 0x84008400u, 0x0400edccu, 0x42481234u, 0x12344248u, 0x84006dccu, 0x00000000u, 0x00000000u, 0x3c001234u, 0x00003c00u}, + {0x04023a00u, 0x3a000402u, 0x04023a00u, 0x38020600u, 0x00020200u, 0x02000002u, 0xfffd7dffu, 0xfffd7ffdu, 0xfffd7dffu, 0x7dff7dffu, 0xffff7e01u, 0xffffffffu, 0xffff7e01u, 0xfe017fffu, 0xc0007fffu, 0xc000ffffu, 0x00037fffu, 0x7fff0003u, 0xffff7fffu, 0xffff0001u, 0xffff7fffu, 0x7fff7fffu, 0xfffefe00u, 0xfffefffeu, 0xfffffe00u, 0xfffffffeu, 0xfffe7fffu, 0x7ffffe00u, 0xfffefe00u, 0xfffefe00u, 0xfe00fffeu, 0xfffe7e00u, 0xfffefe00u, 0xfffefffeu, 0x7ffefe00u, 0xfffefe00u, 0xfe00fe00u, 0xfffe7e00u, 0xfffefe00u, 0xfffefffeu, 0xfffe7e00u, 0xfffefe00u, 0xfe00fffeu, 0x7ffefe00u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x96003bb8u, 0xee00ebccu, 0x96003bb8u, 0x6bcc6e00u, 0x9800bdb8u, 0xbdb89800u, 0x9034c247u, 0x90349233u, 0x9034c247u, 0xc247c247u, 0x9434c249u, 0x94349434u, 0x9434c249u, 0xc4489235u, 0xe000ff00u, 0xfff00000u, 0x07e0007fu, 0x007f07e0u, 0x07e0007fu, 0x07e007ffu, 0x7e007fffu, 0x7fff7fffu, 0x12344248u, 0x12341234u, 0x7e007fffu, 0x7e007fffu, 0x12344248u, 0x12344248u, 0x7e007fffu, 0x7e007fffu, 0x7fff7e00u, 0xfe007fffu, 0x7e007fffu, 0x7e007fffu, 0x7e00ffffu, 0x7e007fffu, 0x7fff7fffu, 0x7e00ffffu, 0x12344248u, 0x12341234u, 0x1234c248u, 0x12344248u, 0x42481234u, 0x92344248u, 0x12343c00u, 0x3c001234u, 0x12343c00u, 0x00003c00u}, + {0x80000800u, 0x08006340u, 0x80000800u, 0xeb408000u, 0x00000000u, 0x00000000u, 0x35555240u, 0x35558b95u, 0x35555240u, 0x52405240u, 0x3555a5c0u, 0x35553555u, 0x3555a5c0u, 0xfc00df15u, 0x00005640u, 0xc8000000u, 0x00005640u, 0x56400000u, 0x00005640u, 0x000002b2u, 0x35555640u, 0x56405640u, 0x0000fc00u, 0x00000000u, 0x3555fc00u, 0xfc005640u, 0x00005640u, 0x35550000u, 0x0000fc00u, 0x0000fc00u, 0xfc000800u, 0x80007c00u, 0x3555fc00u, 0x35555645u, 0xb555fc00u, 0x0000fc00u, 0xfc00fc00u, 0x0000fc00u, 0x0000fc00u, 0x00000000u, 0x80005640u, 0x35555640u, 0x00005640u, 0x0000d640u, 0x00000000u, 0x00003555u, 0x35553c00u, 0x00000000u}, + {0xdd70b2c8u, 0x611b56e4u, 0xdd70b2c8u, 0x9ee3191cu, 0xa57032c9u, 0x32c9a570u, 0x0000fa46u, 0x00006aefu, 0x0000fa46u, 0xfa46fa46u, 0xdb980000u, 0xdb98db98u, 0xdb980000u, 0x6aef70a9u, 0x4000e918u, 0x300091a0u, 0x00010fa4u, 0x0fa40001u, 0x00010fa4u, 0x00010007u, 0x12347d23u, 0x7d237d23u, 0xedcc7d23u, 0xedccedccu, 0xedcc7d23u, 0x7d23edccu, 0x12347d23u, 0x7d231234u, 0xc3fd7f23u, 0xc3fd7f23u, 0x7f237fffu, 0x44ffff23u, 0xedcc7f23u, 0xedcc7f23u, 0x6dcc7f23u, 0xc47f7f23u, 0x7f237f23u, 0xc47fff23u, 0xedcc7f23u, 0xedccedccu, 0xedccff23u, 0x12347f23u, 0x7f237f23u, 0x6dcc7f23u, 0x00000000u, 0x00000000u, 0x12340000u, 0x3c000000u}, + {0xc000d640u, 0xd6413c00u, 0xc000d640u, 0x5640bc01u, 0x84008000u, 0x80008400u, 0xfbff7fffu, 0xfbfffc00u, 0xfbff7fffu, 0x7fff7fffu, 0xfc017fffu, 0xfc01fc01u, 0xfc017fffu, 0x8000fc00u, 0x7fff0000u, 0x80008000u, 0x7fff0001u, 0x00017fffu, 0x7fffffffu, 0x7fff8000u, 0x7fffffffu, 0xffffffffu, 0x7c008000u, 0x7c007c00u, 0x7fffffffu, 0xffff8000u, 0x7c008000u, 0x7c007fffu, 0x7fffffffu, 0x7fffffffu, 0xffff7fffu, 0xffff7fffu, 0x7fffffffu, 0x7fff7c00u, 0x7fffffffu, 0x7fffffffu, 0xffffffffu, 0x7fff7fffu, 0x7c008000u, 0x7c007c00u, 0x7c008000u, 0x7c008000u, 0xffff7c00u, 0xfc000000u, 0x3c000000u, 0x00000000u, 0x3c000000u, 0x00000000u}, + {0xfffecd56u, 0x38002153u, 0xfffecd56u, 0x5955fffeu, 0x00009556u, 0x95560000u, 0x3c013553u, 0x3c017156u, 0x3c013553u, 0x35533553u, 0x3c01caa9u, 0x3c013c01u, 0x3c01caa9u, 0xfffe06acu, 0x00004000u, 0x6aaa0000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00001aaau, 0x3c013555u, 0x35553555u, 0x0000fffeu, 0x00000000u, 0x3c01fffeu, 0xfffe3c01u, 0x00003555u, 0x35550000u, 0xfffefffeu, 0xfffefffeu, 0xfffe3800u, 0xfffe7ffeu, 0x3c01fffeu, 0x3c013d56u, 0xbc01fffeu, 0x0000fffeu, 0xfffefffeu, 0x00007ffeu, 0x00003555u, 0x00000000u, 0x80003555u, 0x3c013555u, 0x00003c01u, 0x0000b555u, 0x00003555u, 0x00003555u, 0x3c003555u, 0x00000000u}, + {0x31b86c40u, 0xd5b84840u, 0x31b86c40u, 0x6c40b1b8u, 0x1db83440u, 0x34401db8u, 0x3e478490u, 0x3e470490u, 0x3e478490u, 0x84908490u, 0x46490000u, 0x46494649u, 0x46490000u, 0xc6498000u, 0xff004800u, 0x4800ff00u, 0x007b0042u, 0x0042007bu, 0x007b0042u, 0x007b0042u, 0x7bff4248u, 0x42484248u, 0xc2484248u, 0xc248c248u, 0xc2484248u, 0x7bffc248u, 0x7bff4248u, 0x42484248u, 0xfc00492fu, 0xfc00492fu, 0x48effc00u, 0x7c00c8afu, 0x7bff4648u, 0x7bff0000u, 0x7bff0000u, 0xfc0048efu, 0x48ef48efu, 0xfc0048efu, 0xc2484248u, 0xc248c248u, 0xfbffc248u, 0x7bff4248u, 0x7bff4248u, 0x7bff4248u, 0x00003c00u, 0x3c000000u, 0x3c003c00u, 0x3c003c00u}, + {0x9a908000u, 0xdcbc4000u, 0x9a908000u, 0x80009cbcu, 0x5a900000u, 0x00005a90u, 0xdb98fe55u, 0xdb98edccu, 0xdb98fe55u, 0xfe55fe55u, 0x0000fe55u, 0x00000000u, 0x0000fe55u, 0x1089edccu, 0xc0000000u, 0x0000b980u, 0x000e0000u, 0x0000000eu, 0xfffe0000u, 0xfffe0000u, 0xedcc0000u, 0x00000000u, 0xedccfe55u, 0xedccedccu, 0xedccfe55u, 0xfe55edccu, 0xedcc0000u, 0x0000edccu, 0x7c00fe55u, 0x7c00fe55u, 0xfe557c00u, 0xfc007e55u, 0xf1ccfe55u, 0xf1ccedccu, 0x0000fe55u, 0x7c00fe55u, 0xfe55fe55u, 0x7c007e55u, 0xedcc0000u, 0xedccedccu, 0xedcc0000u, 0xedcc0000u, 0xedcc0000u, 0x6dcc8000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3c000000u}, + {0x12344400u, 0x14000a34u, 0x12344400u, 0x0c001234u, 0x00003000u, 0x30000000u, 0xfffef1ccu, 0xfffe03feu, 0xfffef1ccu, 0xf1ccf1ccu, 0xfffee9ccu, 0xfffefffeu, 0xfffee9ccu, 0xedccfbfeu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000400u, 0x04000400u, 0xfffeedccu, 0xfffefffeu, 0xfffeedccu, 0xedccfffeu, 0x00000400u, 0x04000000u, 0xfffeb5c8u, 0xfffeb5c8u, 0xb5c9fffeu, 0xfffe35d0u, 0xfffeedccu, 0xfffefffeu, 0x7ffeedccu, 0xfffeb5ccu, 0xb5ccb5ccu, 0xfffeb5ccu, 0x0000edccu, 0x00000000u, 0x80000400u, 0x00000400u, 0x00000400u, 0x00008400u, 0x00000000u, 0x00000400u, 0x00000400u, 0x00000000u}, + {0x7fff9c00u, 0xbc00c69eu, 0x7fff9c00u, 0x029f7fffu, 0x0000e000u, 0xe0000000u, 0x7d235e55u, 0x7d237b78u, 0x7d235e55u, 0x5e555e55u, 0x7d2361abu, 0x7d237d23u, 0x7d2361abu, 0x60007eceu, 0x0000fe55u, 0xf2a80000u, 0x0000fe55u, 0xfe550000u, 0x0000fe55u, 0x0000ffcau, 0x7d236000u, 0x60006000u, 0x0000fe55u, 0x00000000u, 0x7d23fe55u, 0x6000fe55u, 0x00006000u, 0x7d230000u, 0x7f23fe55u, 0x7f23fe55u, 0xfe557f23u, 0x7f23fe55u, 0x7f23fe55u, 0x7f237f23u, 0xff237e55u, 0x7f23fe55u, 0xfe55fe55u, 0x7f237e55u, 0x7f236000u, 0x7f237f23u, 0x7f23e000u, 0x7f236000u, 0x60007f23u, 0xff236000u, 0x00003c00u, 0x00000000u, 0x00003c00u, 0x00003c00u}, + {0x3c008400u, 0x84007c00u, 0x3c008400u, 0x84007c00u, 0xc0000000u, 0x0000c000u, 0x3c018000u, 0x3c017c01u, 0x3c018000u, 0x80008000u, 0xbc018000u, 0xbc01bc01u, 0xbc018000u, 0xc0007c01u, 0x80000000u, 0x0000c000u, 0x60000000u, 0x00006000u, 0xe0000000u, 0xe0000000u, 0x7c010000u, 0x00000000u, 0xc0008000u, 0xc000c000u, 0xc0008000u, 0xc0007c01u, 0x7c010000u, 0x00008000u, 0x7e018400u, 0x7e018400u, 0x7c007e01u, 0x7e018400u, 0x7e010000u, 0x7e017e01u, 0xfe018000u, 0x7e018000u, 0x80008000u, 0x7e018000u, 0x7e018000u, 0x7e017e01u, 0x7e010000u, 0x7e010000u, 0x80007e01u, 0xfe018000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x40006fe8u, 0xaa007de8u, 0x40006fe8u, 0xe7e84000u, 0x2c00f1e8u, 0xf1e82c00u, 0xb155c09du, 0xb155f79du, 0xb155c09du, 0xc09dc09du, 0xb9553c0du, 0xb955b955u, 0xb9553c0du, 0x8255730du, 0x80004900u, 0x49008000u, 0x03e00612u, 0x061203e0u, 0x03e0fe12u, 0x03e0fe12u, 0x7c00fe55u, 0xfe55fe55u, 0x3555c248u, 0x35553555u, 0x7c00fe55u, 0xfe55c248u, 0x3555c248u, 0x35557c00u, 0x7c00fe55u, 0x7c00fe55u, 0xfe557e00u, 0xfc007e55u, 0x7c00fe55u, 0x7c00c19du, 0x7c00fe55u, 0x7c00fe55u, 0xfe55fe55u, 0x7c007e55u, 0x3555c248u, 0x35553555u, 0xfc00c248u, 0x7c00c248u, 0x7c003555u, 0x7c004248u, 0x35550000u, 0x3c000000u, 0x3c000000u, 0x3c003c00u}, + {0x755fffffu, 0x5e480e33u, 0x755fffffu, 0x4e341e47u, 0x7960c3ffu, 0xc3ff7960u, 0x30143c00u, 0x3014edcbu, 0x30143c00u, 0x3c003c00u, 0xab843c02u, 0xab84ab84u, 0xab843c02u, 0xf9b9edcdu, 0x8000fffeu, 0xf0008490u, 0x00047fffu, 0x7fff0004u, 0x0004ffffu, 0x0004ffffu, 0x42483c01u, 0x3c013c01u, 0xedccffffu, 0xedccedccu, 0xedccffffu, 0x4248ffffu, 0x42483c01u, 0xedcc3c01u, 0xfc00ffffu, 0xfc00ffffu, 0xfffff48du, 0xf9b8ffffu, 0xedcbffffu, 0xedcbffffu, 0x6dcd7fffu, 0xf48dffffu, 0xffffffffu, 0xf48d7fffu, 0xedcc3c01u, 0xedccedccu, 0xedccbc01u, 0x42483c01u, 0x4248edccu, 0x6dcc3c01u, 0x00003c00u, 0x3c000000u, 0x3c003c00u, 0x3c003c00u}, + {0x8132edccu, 0x70a9fe55u, 0x8132edccu, 0xedcc8132u, 0x82dd0000u, 0x000082ddu, 0x7d227d23u, 0x7d227d23u, 0x7d227d23u, 0x7d237d23u, 0x7d247d23u, 0x7d247d24u, 0x7d247d23u, 0x7d247d23u, 0xfff80000u, 0x0000fff8u, 0x1fff0000u, 0x00001fffu, 0xffff0000u, 0xffff0000u, 0x7d237d23u, 0x7d237d23u, 0xffff0000u, 0xffffffffu, 0xffff7d23u, 0xffff7d23u, 0x7d230000u, 0x00007d23u, 0x7f237f23u, 0x7f237f23u, 0x7f237f23u, 0x7f23ff23u, 0x7f237f23u, 0x7f237f23u, 0xff237f23u, 0x7f237f23u, 0x7f237f23u, 0x7f23ff23u, 0x7f237f23u, 0x7f237f23u, 0x7f23ff23u, 0x7f237f23u, 0x7f237f23u, 0xff237f23u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00008000u, 0x80002000u, 0x00008000u, 0xa0000000u, 0x00000000u, 0x00000000u, 0xc2480400u, 0xc248c648u, 0xc2480400u, 0x04000400u, 0xc248fc00u, 0xc248c248u, 0xc248fc00u, 0x0000be48u, 0x00000400u, 0x00000000u, 0x00000400u, 0x04000000u, 0x00000400u, 0x00000004u, 0x00000400u, 0x04000400u, 0xc2480000u, 0xc248c248u, 0xc2480400u, 0x0000c248u, 0x00000000u, 0x04000000u, 0x00000000u, 0x00000000u, 0x00008000u, 0x00008000u, 0xc2480400u, 0xc248c248u, 0x42488400u, 0x80000000u, 0x00000000u, 0x80000000u, 0xc2480000u, 0xc248c248u, 0xc2488000u, 0x00000400u, 0x00000400u, 0x42480000u, 0x00000000u, 0x00000000u, 0x00000400u, 0x3c000000u}, + {0xcaab5400u, 0x9634ac00u, 0xcaab5400u, 0x30001234u, 0xcaabd000u, 0xd000caabu, 0x3554e9ccu, 0x35543155u, 0x3554e9ccu, 0xe9cce9ccu, 0x3556f1ccu, 0x35563556u, 0x3556f1ccu, 0xedcd3955u, 0xffe00000u, 0x8000f000u, 0x07ff000fu, 0x000f07ffu, 0xffffffffu, 0xffffffe0u, 0x3555fc00u, 0xfc00fc00u, 0xffffedccu, 0xffffffffu, 0xfffffc00u, 0xfffffc00u, 0x3555edccu, 0x3555edccu, 0xffff7c00u, 0xffff7c00u, 0x7c00ffffu, 0x7ffffc00u, 0xfffffc00u, 0xfffffc00u, 0xffff7c00u, 0xffff7c00u, 0x7c007c00u, 0xffff7c00u, 0x3555fc00u, 0x35553555u, 0x3555fc00u, 0x3555edccu, 0xedcc3555u, 0xb5557c00u, 0x35550000u, 0x00000000u, 0x35550000u, 0x00003c00u}, + {0x8400c000u, 0xa11c8400u, 0x8400c000u, 0xc000651cu, 0x00000000u, 0x00000000u, 0x7d231234u, 0x7d230000u, 0x7d231234u, 0x12341234u, 0x82dd1234u, 0x82dd82ddu, 0x82dd1234u, 0x95110000u, 0x7d230000u, 0x0000d230u, 0x7d230000u, 0x00007d23u, 0x7d230000u, 0x7d230000u, 0x7d231234u, 0x12341234u, 0x00000000u, 0x00000000u, 0x7d231234u, 0x7d230000u, 0x00000000u, 0x00001234u, 0x7f23c000u, 0x7f23c000u, 0x84007f23u, 0xff23c000u, 0x7f231234u, 0x7f230000u, 0x7f231234u, 0x7f230000u, 0x00000000u, 0x7f230000u, 0x7f230000u, 0x7f237f23u, 0xff239234u, 0x7f231234u, 0x7f230000u, 0x7f231234u, 0x00000000u, 0x00000000u, 0x00001234u, 0x00001234u}, + {0x7d557e55u, 0x7e557d55u, 0x7d557e55u, 0x7e557d55u, 0x00000000u, 0x00000000u, 0xfc007c00u, 0xfc000000u, 0xfc007c00u, 0x7c007c00u, 0x04007c00u, 0x04000400u, 0x04007c00u, 0x80000000u, 0xfc000000u, 0x0000fc00u, 0xfc000000u, 0x0000fc00u, 0xfc000000u, 0xfc000000u, 0x00007c00u, 0x7c007c00u, 0xfc000000u, 0xfc00fc00u, 0xfc007c00u, 0xfc000000u, 0x00000000u, 0x00007c00u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0xfc007c00u, 0xfc000000u, 0xfc007c00u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0xfc000000u, 0xfc00fc00u, 0x0000fc00u, 0x00007c00u, 0x7c000000u, 0x80007c00u, 0x00000000u, 0x00000000u, 0x00003c00u, 0x00003c00u}, + {0xfe557d55u, 0x7d55fe55u, 0xfe557d55u, 0x7d55fe55u, 0x00000000u, 0x00000000u, 0x7c007c00u, 0x7c00f800u, 0x7c007c00u, 0x7c007c00u, 0x7c008400u, 0x7c007c00u, 0x7c008400u, 0x00000000u, 0x00007c00u, 0x7c000000u, 0x00007c00u, 0x7c000000u, 0x00007c00u, 0x00007c00u, 0x7c007c00u, 0x7c007c00u, 0x00000000u, 0x00000000u, 0x7c007c00u, 0x00007c00u, 0x00000000u, 0x7c000000u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0x7c007c00u, 0x7c007c00u, 0xfc00fc00u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0x00000000u, 0x00000000u, 0x80008000u, 0x7c007c00u, 0x00007c00u, 0x00000000u, 0x00000000u, 0x00003c00u, 0x3c003c00u, 0x00000000u}, }; -constexpr std::uint32_t ExpectedIeeeOff[32][44] = { - {0x3c003c00u, 0x3c003c00u, 0x3c003c00u, 0x3c003c00u, 0x00000000u, 0x00000000u, 0x7c007c00u, 0x7c00fc00u, 0x7c007c00u, 0x7c007c00u, 0x7c007c00u, 0x7c007c00u, 0x7c007c00u, 0xfc00fc00u, 0x80000000u, 0x00008000u, 0x80000000u, 0x00008000u, 0x80000000u, 0x80000000u, 0xfc007c00u, 0x7c007c00u, 0x80000000u, 0x80008000u, 0xfc007c00u, 0x8000fc00u, 0x80000000u, 0x00007c00u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0xfc007c00u, 0xfc00fc00u, 0x7c007c00u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0xfc000000u, 0xfc00fc00u, 0xfc00fc00u, 0x80007c00u, 0x7c000000u, 0x7c007c00u}, - {0xbc007c00u, 0x7c00bc00u, 0xbc007c00u, 0x7c00bc00u, 0x00000000u, 0x00000000u, 0x78007800u, 0x7800f800u, 0x78007800u, 0x78007800u, 0x80008000u, 0x80008000u, 0x80008000u, 0x00000000u, 0x7c00fc00u, 0xfc007c00u, 0x7c00fc00u, 0xfc007c00u, 0x7c00fc00u, 0x7c00fc00u, 0x7c007c00u, 0x7c007c00u, 0xfc00fc00u, 0xfc00fc00u, 0xfc00fc00u, 0x7c00fc00u, 0x7c007c00u, 0xfc007c00u, 0xfc00fe00u, 0xfc00fe00u, 0xfc00fe00u, 0x7c007c00u, 0xfe00fe00u, 0xfe00fc00u, 0x7c007c00u, 0xfc00fc00u, 0xfc00fc00u, 0xfc00fc00u, 0xfc00fc00u, 0xfc00fc00u, 0xfc00fc00u, 0x7c007c00u, 0x7c00fc00u, 0x7c007c00u}, - {0x7bffc000u, 0xc000fbffu, 0x7bffc000u, 0x7fff3c00u, 0x43ff0400u, 0x040043ffu, 0xbc00fbffu, 0xbc003c00u, 0xbc00fbffu, 0xfbfffbffu, 0xbc02fc01u, 0xbc02bc02u, 0xbc02fc01u, 0x7c013c02u, 0xfffeffffu, 0xfffe7fffu, 0x3fffffffu, 0xffff3fffu, 0x3fffffffu, 0x3fffffffu, 0x7fffffffu, 0xffffffffu, 0x3c01fc00u, 0x3c013c01u, 0x7fffffffu, 0xfc00ffffu, 0x3c01fc00u, 0x3c017fffu, 0x7fffffffu, 0x7fffffffu, 0xffff7fffu, 0xffffffffu, 0x7fffffffu, 0x7fffffffu, 0x7fff7fffu, 0x7fffffffu, 0xffffffffu, 0x7fff7fffu, 0x3c01fc00u, 0x3c013c01u, 0x3c017c00u, 0x3c01fc00u, 0xfc003c01u, 0xbc01fc00u}, - {0xc0009400u, 0x1400c000u, 0xc0009400u, 0x1400c000u, 0x00008000u, 0x80000000u, 0x1400ffffu, 0x14009400u, 0x1400ffffu, 0xffffffffu, 0x1400ffffu, 0x14001400u, 0x1400ffffu, 0x7fff9400u, 0x00000000u, 0x80000000u, 0x00000001u, 0x00010000u, 0x0000ffffu, 0x00008000u, 0x14007fffu, 0x7fff7fffu, 0x00008000u, 0x00000000u, 0x14008000u, 0x7fff8000u, 0x00007fffu, 0x14000000u, 0xc0007fffu, 0xc0007fffu, 0x7fff1400u, 0xc000ffffu, 0x14007fffu, 0x14001400u, 0x94007fffu, 0x00007fffu, 0x7fff7fffu, 0x0000ffffu, 0x00008000u, 0x00000000u, 0x80008000u, 0x14008000u, 0x00001400u, 0x00000000u}, - {0x7e00d5feu, 0x7ffe8000u, 0x7e00d5feu, 0x81fe7e00u, 0x8000d600u, 0xd6008000u, 0x7fff7c55u, 0x7fff7dffu, 0x7fff7c55u, 0x7c557c55u, 0x7fff8055u, 0x7fff7fffu, 0x7fff8055u, 0x7e5581ffu, 0x0000c000u, 0x00000000u, 0x000103f0u, 0x03f00001u, 0xffff03f0u, 0xffff0000u, 0xffff7e00u, 0x7e007e00u, 0x8000fe55u, 0x80008000u, 0xfffffe55u, 0xfe55ffffu, 0x80007e00u, 0x7e008000u, 0xfffffe55u, 0xfffffe55u, 0xfe55ffffu, 0xffff7e55u, 0xfffffe55u, 0xffffffffu, 0x7ffffe55u, 0xfffffe55u, 0xfe55fe55u, 0xffff7e55u, 0x8000fe55u, 0x80008000u, 0x00007e55u, 0x8000fe55u, 0x8000ffffu, 0x8000fe55u}, - {0x80007656u, 0x4254c3feu, 0x80007656u, 0x4254c3feu, 0x00017801u, 0x78010001u, 0xfffe7802u, 0xfffebc00u, 0xfffe7802u, 0x78027802u, 0x00000000u, 0x00000000u, 0x00000000u, 0xbc0243feu, 0x80007802u, 0x8000fffeu, 0x00001e00u, 0x1e000000u, 0x00001e00u, 0x00000000u, 0x7fff3c01u, 0x3c013c01u, 0x7fff3c01u, 0x7fff7fffu, 0x7fff3c01u, 0x7fff7fffu, 0x7fff3c01u, 0x3c013c01u, 0x7ffffe55u, 0x7ffffe55u, 0x7fff7fffu, 0x7ffffe55u, 0x7fff4001u, 0x7fff7fffu, 0xffff0000u, 0x7fff3c02u, 0x3c023c02u, 0x7fff3c02u, 0x7fff3c01u, 0x7fff7fffu, 0x7fffbc01u, 0x7fff3c01u, 0x3c013c01u, 0xffff3c01u}, - {0x7bff6000u, 0x60007bffu, 0x7bff6000u, 0x60007bffu, 0x00000000u, 0x00000000u, 0xedcc6dccu, 0xedcc6dccu, 0xedcc6dccu, 0x6dcc6dccu, 0xedcc6dccu, 0xedccedccu, 0xedcc6dccu, 0xedcc6dccu, 0x00000000u, 0x00000000u, 0x00000008u, 0x00080000u, 0x0000fff8u, 0x0000fff8u, 0x0000edccu, 0xedccedccu, 0xedcc8000u, 0xedccedccu, 0xedccedccu, 0xedccedccu, 0x00008000u, 0x80000000u, 0x7bff6000u, 0x7bff6000u, 0x7bff6000u, 0x7bff6000u, 0xedccedccu, 0xedccedccu, 0x6dccedccu, 0x80000000u, 0x00000000u, 0x80000000u, 0xedccedccu, 0xedccedccu, 0xedcc8000u, 0x00008000u, 0x00008000u, 0x6dcc0000u}, - {0x00001dccu, 0xedccd000u, 0x00001dccu, 0xbdcc0000u, 0x00003000u, 0x30000000u, 0x8000ce34u, 0x80009634u, 0x8000ce34u, 0xce34ce34u, 0x8800a9ccu, 0x88008800u, 0x8800a9ccu, 0xc00071ccu, 0xfc001234u, 0x1234fc00u, 0xfc001234u, 0x1234fc00u, 0xfc001234u, 0xfc001234u, 0xfc001234u, 0x12341234u, 0x8400bc00u, 0x84008400u, 0xfc00bc00u, 0xfc008400u, 0x84001234u, 0x1234bc00u, 0x7c00edccu, 0x7c00edccu, 0x92347c00u, 0xfc00edccu, 0xfc00bbfeu, 0xfc0011b4u, 0xfc00bc01u, 0x7c009234u, 0x92349234u, 0x7c009234u, 0xfc00bc00u, 0xfc00fc00u, 0x84001234u, 0x84001234u, 0xbc001234u, 0x04009234u}, - {0xc99f5e41u, 0x4ceb74ddu, 0xc99f5e41u, 0xcd1df4abu, 0xcb9f0801u, 0x0801cb9fu, 0xb278f7feu, 0xb278f922u, 0xb278f7feu, 0xf7fef7feu, 0x47ce0000u, 0x47ce47ceu, 0x47ce0000u, 0x46aa0124u, 0xaaa88000u, 0xdff88000u, 0x06aa0000u, 0x000006aau, 0x06aa0000u, 0x06aa0f7fu, 0x7d237bffu, 0x7bff7bffu, 0x35557bffu, 0x35553555u, 0x7d237bffu, 0x7bff7d23u, 0x35557bffu, 0x7bff3555u, 0x7d237c00u, 0x7d237c00u, 0xfe007d23u, 0x7d23fc00u, 0x7d237c00u, 0x7d237d23u, 0xfd230000u, 0x7d237c00u, 0x7c007c00u, 0x7d237c00u, 0x35557bffu, 0x35553555u, 0xb555fbffu, 0x35557bffu, 0x7bff7bffu, 0x35557bffu}, - {0x00008000u, 0x80000000u, 0x00008000u, 0x80000000u, 0x80008000u, 0x80008000u, 0xb555fc01u, 0xb555b555u, 0xb555fc01u, 0xfc01fc01u, 0xb555fc01u, 0xb555b555u, 0xb555fc01u, 0xfc01b555u, 0x00000000u, 0x00000000u, 0x04004000u, 0x40000400u, 0xfc00c000u, 0xfc00fc00u, 0x35557c01u, 0x7c017c01u, 0x80008000u, 0x80008000u, 0x80008000u, 0x80008000u, 0x35557c01u, 0x35557c01u, 0x80007c01u, 0x80007c01u, 0x7c010000u, 0x0000fc01u, 0x35557c01u, 0x35553555u, 0xb5557c01u, 0x80007c01u, 0x7c017c01u, 0x8000fc01u, 0x80008000u, 0x80008000u, 0x00008000u, 0x35558000u, 0x80003555u, 0x80000000u}, - {0x03ff0000u, 0xa2480000u, 0x03ff0000u, 0x0000a248u, 0x03ff0000u, 0x000003ffu, 0xfc00c248u, 0xfc007fffu, 0xfc00c248u, 0xc248c248u, 0x03fec248u, 0x03fe03feu, 0x03fec248u, 0x46477fffu, 0x80000000u, 0x00000100u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x7fff0000u, 0x00000000u, 0x7c01c248u, 0x7c017c01u, 0x7fffc248u, 0xc2487fffu, 0x7c010000u, 0x00007c01u, 0x7fff0000u, 0x7fff0000u, 0x00007fffu, 0x7fff0000u, 0x7fffc248u, 0x7fff7fffu, 0xffffc248u, 0x7fff8000u, 0x80008000u, 0x7fff8000u, 0x7fffc248u, 0x7fff7fffu, 0x7fff0000u, 0x7fff0000u, 0xc2480000u, 0xffff8000u}, - {0xc2337fffu, 0x87fd7fffu, 0xc2337fffu, 0xffff07fdu, 0x42348000u, 0x80004234u, 0x4e35bffeu, 0x4e35d234u, 0x4e35bffeu, 0xbffebffeu, 0xd6333ffeu, 0xd633d633u, 0xd6333ffeu, 0xc3fd5234u, 0xc0100000u, 0x00004000u, 0x03c00003u, 0x000303c0u, 0x03c0ffffu, 0x03c0fc00u, 0x3c01fffeu, 0xfffefffeu, 0x1234c000u, 0x12341234u, 0x3c01fffeu, 0xfffec000u, 0x1234c000u, 0x12343c01u, 0x7ffffffeu, 0x7ffffffeu, 0xfffeffffu, 0x7fff7ffeu, 0x3c02fffeu, 0x3c02bfffu, 0x3c00fffeu, 0x1236fffeu, 0xfffefffeu, 0x12367ffeu, 0x1234c000u, 0x12341234u, 0xbc01c000u, 0x3c01c000u, 0x3c011234u, 0x3c014000u}, - {0x9c00a56eu, 0xcffef560u, 0x9c00a56eu, 0x795e4c00u, 0x2000a570u, 0xa5702000u, 0xc6480000u, 0xc6483014u, 0xc6480000u, 0x00000000u, 0xbe482468u, 0xbe48be48u, 0xbe482468u, 0x8e34547cu, 0x0000dcc0u, 0xcc004000u, 0x00840edcu, 0x0edc0084u, 0xff84fedcu, 0xff84ffedu, 0x42481234u, 0x12341234u, 0x8400edccu, 0x84008400u, 0x8400edccu, 0x8400edccu, 0x42481234u, 0x42481234u, 0x7c00fffeu, 0x7c00fffeu, 0x7c00fffeu, 0x7c00fffeu, 0x4248edccu, 0x4248edcbu, 0xc2486dccu, 0x8a48c47fu, 0xc47fc47fu, 0x8a48c47fu, 0x8400edccu, 0x84008400u, 0x0400edccu, 0x42481234u, 0x12344248u, 0x84006dccu}, - {0x04023a00u, 0x3a000402u, 0x04023a00u, 0x38020600u, 0x00020200u, 0x02000002u, 0xfffd7dffu, 0xfffd7ffdu, 0xfffd7dffu, 0x7dff7dffu, 0xffff7e01u, 0xffffffffu, 0xffff7e01u, 0xfe017fffu, 0xc0007fffu, 0xc000ffffu, 0x00037fffu, 0x7fff0003u, 0xffff7fffu, 0xffff0001u, 0xffff7fffu, 0x7fff7fffu, 0xfffefe00u, 0xfffefffeu, 0xfffffe00u, 0xfffffffeu, 0xfffe7fffu, 0x7ffffe00u, 0xfffefe00u, 0xfffefe00u, 0xfe00fffeu, 0xfffe7e00u, 0xfffefe00u, 0xfffefffeu, 0x7ffefe00u, 0xfffefe00u, 0xfe00fe00u, 0xfffe7e00u, 0xfffefe00u, 0xfffefffeu, 0xfffe7e00u, 0xfffefe00u, 0xfe00fffeu, 0x7ffefe00u}, - {0x96003bb8u, 0xee00ebccu, 0x96003bb8u, 0x6bcc6e00u, 0x9800bdb8u, 0xbdb89800u, 0x9034c247u, 0x90349233u, 0x9034c247u, 0xc247c247u, 0x9434c249u, 0x94349434u, 0x9434c249u, 0xc4489235u, 0xe000ff00u, 0xfff00000u, 0x07e0007fu, 0x007f07e0u, 0x07e0007fu, 0x07e007ffu, 0x7e007fffu, 0x7fff7fffu, 0x12344248u, 0x12341234u, 0x7e007fffu, 0x7e007fffu, 0x12344248u, 0x12344248u, 0x7e007fffu, 0x7e007fffu, 0x7fff7e00u, 0xfe007fffu, 0x7e007fffu, 0x7e007fffu, 0x7e00ffffu, 0x7e007fffu, 0x7fff7fffu, 0x7e00ffffu, 0x12344248u, 0x12341234u, 0x1234c248u, 0x12344248u, 0x42481234u, 0x92344248u}, - {0x80000800u, 0x08006340u, 0x80000800u, 0xeb408000u, 0x00000000u, 0x00000000u, 0x35555240u, 0x35558b95u, 0x35555240u, 0x52405240u, 0x3555a5c0u, 0x35553555u, 0x3555a5c0u, 0xfc00df15u, 0x00005640u, 0xc8000000u, 0x00005640u, 0x56400000u, 0x00005640u, 0x000002b2u, 0x35555640u, 0x56405640u, 0x0000fc00u, 0x00000000u, 0x3555fc00u, 0xfc005640u, 0x00005640u, 0x35550000u, 0x0000fc00u, 0x0000fc00u, 0xfc000800u, 0x80007c00u, 0x3555fc00u, 0x35555645u, 0xb555fc00u, 0x0000fc00u, 0xfc00fc00u, 0x0000fc00u, 0x0000fc00u, 0x00000000u, 0x80005640u, 0x35555640u, 0x00005640u, 0x0000d640u}, - {0xdd70b2c8u, 0x611b56e4u, 0xdd70b2c8u, 0x9ee3191cu, 0xa57032c9u, 0x32c9a570u, 0x0000fa46u, 0x00006aefu, 0x0000fa46u, 0xfa46fa46u, 0xdb980000u, 0xdb98db98u, 0xdb980000u, 0x6aef70a9u, 0x4000e918u, 0x300091a0u, 0x00010fa4u, 0x0fa40001u, 0x00010fa4u, 0x00010007u, 0x12347d23u, 0x7d237d23u, 0xedcc7d23u, 0xedccedccu, 0xedcc7d23u, 0x7d23edccu, 0x12347d23u, 0x7d231234u, 0xc3fd7d23u, 0xc3fd7d23u, 0x7d237fffu, 0x44fffd23u, 0xedcc7d23u, 0xedcc7d23u, 0x6dcc7d23u, 0xc47f7d23u, 0x7d237d23u, 0xc47ffd23u, 0xedcc7d23u, 0xedccedccu, 0xedccfd23u, 0x12347d23u, 0x1234edccu, 0x6dcc7d23u}, - {0xc000d640u, 0xd6413c00u, 0xc000d640u, 0x5640bc01u, 0x84008000u, 0x80008400u, 0xfbff7fffu, 0xfbfffc00u, 0xfbff7fffu, 0x7fff7fffu, 0xfc017fffu, 0xfc01fc01u, 0xfc017fffu, 0x8000fc00u, 0x7fff0000u, 0x80008000u, 0x7fff0001u, 0x00017fffu, 0x7fffffffu, 0x7fff8000u, 0x7fffffffu, 0xffffffffu, 0x7c008000u, 0x7c007c00u, 0x7fffffffu, 0xffff8000u, 0x7c008000u, 0x7c007fffu, 0x7fffffffu, 0x7fffffffu, 0xffff7fffu, 0xffff7fffu, 0x7fffffffu, 0x7fff7c00u, 0x7fffffffu, 0x7fffffffu, 0xffffffffu, 0x7fff7fffu, 0x7c008000u, 0x7c007c00u, 0x7c008000u, 0x7c008000u, 0xffff7c00u, 0xfc000000u}, - {0xfffecd56u, 0x38002153u, 0xfffecd56u, 0x5955fffeu, 0x00009556u, 0x95560000u, 0x3c013553u, 0x3c017156u, 0x3c013553u, 0x35533553u, 0x3c01caa9u, 0x3c013c01u, 0x3c01caa9u, 0xfffe06acu, 0x00004000u, 0x6aaa0000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00001aaau, 0x3c013555u, 0x35553555u, 0x0000fffeu, 0x00000000u, 0x3c01fffeu, 0xfffe3c01u, 0x00003555u, 0x35550000u, 0xfffefffeu, 0xfffefffeu, 0xfffe3800u, 0xfffe7ffeu, 0x3c01fffeu, 0x3c013d56u, 0xbc01fffeu, 0x0000fffeu, 0xfffefffeu, 0x00007ffeu, 0x00003555u, 0x00000000u, 0x80003555u, 0x3c013555u, 0x00003c01u, 0x0000b555u}, - {0x31b86c40u, 0xd5b84840u, 0x31b86c40u, 0x6c40b1b8u, 0x1db83440u, 0x34401db8u, 0x3e478490u, 0x3e470490u, 0x3e478490u, 0x84908490u, 0x46490000u, 0x46494649u, 0x46490000u, 0xc6498000u, 0xff004800u, 0x4800ff00u, 0x007b0042u, 0x0042007bu, 0x007b0042u, 0x007b0042u, 0x7bff4248u, 0x42484248u, 0xc2484248u, 0xc248c248u, 0xc2484248u, 0x7bffc248u, 0x7bff4248u, 0x42484248u, 0xfc00492fu, 0xfc00492fu, 0x48effc00u, 0x7c00c8afu, 0x7bff4648u, 0x7bff0000u, 0x7bff0000u, 0xfc0048efu, 0x48ef48efu, 0xfc0048efu, 0xc2484248u, 0xc248c248u, 0xfbffc248u, 0x7bff4248u, 0x7bff4248u, 0x7bff4248u}, - {0x9a908000u, 0xdcbc4000u, 0x9a908000u, 0x80009cbcu, 0x5a900000u, 0x00005a90u, 0xdb98fe55u, 0xdb98edccu, 0xdb98fe55u, 0xfe55fe55u, 0x0000fe55u, 0x00000000u, 0x0000fe55u, 0x1089edccu, 0xc0000000u, 0x0000b980u, 0x000e0000u, 0x0000000eu, 0xfffe0000u, 0xfffe0000u, 0xedcc0000u, 0x00000000u, 0xedccfe55u, 0xedccedccu, 0xedccfe55u, 0xfe55edccu, 0xedcc0000u, 0x0000edccu, 0x7c00fe55u, 0x7c00fe55u, 0xfe557c00u, 0xfc007e55u, 0xf1ccfe55u, 0xf1ccedccu, 0x0000fe55u, 0x7c00fe55u, 0xfe55fe55u, 0x7c007e55u, 0xedcc0000u, 0xedccedccu, 0xedcc0000u, 0xedcc0000u, 0xedcc0000u, 0x6dcc8000u}, - {0x12344400u, 0x14000a34u, 0x12344400u, 0x0c001234u, 0x00003000u, 0x30000000u, 0xfffef1ccu, 0xfffe03feu, 0xfffef1ccu, 0xf1ccf1ccu, 0xfffee9ccu, 0xfffefffeu, 0xfffee9ccu, 0xedccfbfeu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000400u, 0x04000400u, 0xfffeedccu, 0xfffefffeu, 0xfffeedccu, 0xedccfffeu, 0x00000400u, 0x04000000u, 0xfffeb5c8u, 0xfffeb5c8u, 0xb5c9fffeu, 0xfffe35d0u, 0xfffeedccu, 0xfffefffeu, 0x7ffeedccu, 0xfffeb5ccu, 0xb5ccb5ccu, 0xfffeb5ccu, 0x0000edccu, 0x00000000u, 0x80000400u, 0x00000400u, 0x00000400u, 0x00008400u}, - {0x7fff9c00u, 0xbc00c69eu, 0x7fff9c00u, 0x029f7fffu, 0x0000e000u, 0xe0000000u, 0x7d235e55u, 0x7d237b78u, 0x7d235e55u, 0x5e555e55u, 0x7d2361abu, 0x7d237d23u, 0x7d2361abu, 0x60007eceu, 0x0000fe55u, 0xf2a80000u, 0x0000fe55u, 0xfe550000u, 0x0000fe55u, 0x0000ffcau, 0x7d236000u, 0x60006000u, 0x0000fe55u, 0x00000000u, 0x7d23fe55u, 0x6000fe55u, 0x00006000u, 0x7d230000u, 0x7d23fe55u, 0x7d23fe55u, 0xfe557d23u, 0x7d23fe55u, 0x7d23fe55u, 0x7d237d23u, 0xfd237e55u, 0x7d23fe55u, 0xfe55fe55u, 0x7d237e55u, 0x00006000u, 0x00000000u, 0x8000e000u, 0x00006000u, 0x60007d23u, 0x00006000u}, - {0x3c008400u, 0x84007c00u, 0x3c008400u, 0x84007c00u, 0xc0000000u, 0x0000c000u, 0x3c018000u, 0x3c017c01u, 0x3c018000u, 0x80008000u, 0xbc018000u, 0xbc01bc01u, 0xbc018000u, 0xc0007c01u, 0x80000000u, 0x0000c000u, 0x60000000u, 0x00006000u, 0xe0000000u, 0xe0000000u, 0x7c010000u, 0x00000000u, 0xc0008000u, 0xc000c000u, 0xc0008000u, 0xc0007c01u, 0x7c010000u, 0x00008000u, 0x7c018400u, 0x7c018400u, 0x7c007c01u, 0x7c018400u, 0x7c010000u, 0x7c017c01u, 0xfc018000u, 0x7c018000u, 0x80008000u, 0x7c018000u, 0xc0008000u, 0xc000c000u, 0x40000000u, 0xc0000000u, 0x80000000u, 0xc0008000u}, - {0x40006fe8u, 0xaa007de8u, 0x40006fe8u, 0xe7e84000u, 0x2c00f1e8u, 0xf1e82c00u, 0xb155c09du, 0xb155f79du, 0xb155c09du, 0xc09dc09du, 0xb9553c0du, 0xb955b955u, 0xb9553c0du, 0x8255730du, 0x80004900u, 0x49008000u, 0x03e00612u, 0x061203e0u, 0x03e0fe12u, 0x03e0fe12u, 0x7c00fe55u, 0xfe55fe55u, 0x3555c248u, 0x35553555u, 0x7c00fe55u, 0xfe55c248u, 0x3555c248u, 0x35557c00u, 0x7c00fe55u, 0x7c00fe55u, 0xfe557e00u, 0xfc007e55u, 0x7c00fe55u, 0x7c00c19du, 0x7c00fe55u, 0x7c00fe55u, 0xfe55fe55u, 0x7c007e55u, 0x3555c248u, 0x35553555u, 0xfc00c248u, 0x7c00c248u, 0x7c003555u, 0x7c004248u}, - {0x755fffffu, 0x5e480e33u, 0x755fffffu, 0x4e341e47u, 0x7960c3ffu, 0xc3ff7960u, 0x30143c00u, 0x3014edcbu, 0x30143c00u, 0x3c003c00u, 0xab843c02u, 0xab84ab84u, 0xab843c02u, 0xf9b9edcdu, 0x8000fffeu, 0xf0008490u, 0x00047fffu, 0x7fff0004u, 0x0004ffffu, 0x0004ffffu, 0x42483c01u, 0x3c013c01u, 0xedccffffu, 0xedccedccu, 0xedccffffu, 0x4248ffffu, 0x42483c01u, 0xedcc3c01u, 0xfc00ffffu, 0xfc00ffffu, 0xfffff48du, 0xf9b8ffffu, 0xedcbffffu, 0xedcbffffu, 0x6dcd7fffu, 0xf48dffffu, 0xffffffffu, 0xf48d7fffu, 0xedcc3c01u, 0xedccedccu, 0xedccbc01u, 0x42483c01u, 0x4248edccu, 0x6dcc3c01u}, - {0x8132edccu, 0x70a9fe55u, 0x8132edccu, 0xedcc8132u, 0x82dd0000u, 0x000082ddu, 0x7d227d23u, 0x7d227d23u, 0x7d227d23u, 0x7d237d23u, 0x7d247d23u, 0x7d247d24u, 0x7d247d23u, 0x7d247d23u, 0xfff80000u, 0x0000fff8u, 0x1fff0000u, 0x00001fffu, 0xffff0000u, 0xffff0000u, 0x7d237d23u, 0x7d237d23u, 0xffff0000u, 0xffffffffu, 0xffff7d23u, 0xffff7d23u, 0x7d230000u, 0x00007d23u, 0x7d237d23u, 0x7d237d23u, 0x7d237d23u, 0x7d23fd23u, 0x7d237d23u, 0x7d237d23u, 0xfd237d23u, 0x7d237d23u, 0x7d237d23u, 0x7d23fd23u, 0x7d230000u, 0x7d237d23u, 0x7d230000u, 0x7d230000u, 0x7d230000u, 0xfd238000u}, - {0x00008000u, 0x80002000u, 0x00008000u, 0xa0000000u, 0x00000000u, 0x00000000u, 0xc2480400u, 0xc248c648u, 0xc2480400u, 0x04000400u, 0xc248fc00u, 0xc248c248u, 0xc248fc00u, 0x0000be48u, 0x00000400u, 0x00000000u, 0x00000400u, 0x04000000u, 0x00000400u, 0x00000004u, 0x00000400u, 0x04000400u, 0xc2480000u, 0xc248c248u, 0xc2480400u, 0x0000c248u, 0x00000000u, 0x04000000u, 0x00000000u, 0x00000000u, 0x00008000u, 0x00008000u, 0xc2480400u, 0xc248c248u, 0x42488400u, 0x80000000u, 0x00000000u, 0x80000000u, 0xc2480000u, 0xc248c248u, 0xc2488000u, 0x00000400u, 0x00000400u, 0x42480000u}, - {0xcaab5400u, 0x9634ac00u, 0xcaab5400u, 0x30001234u, 0xcaabd000u, 0xd000caabu, 0x3554e9ccu, 0x35543155u, 0x3554e9ccu, 0xe9cce9ccu, 0x3556f1ccu, 0x35563556u, 0x3556f1ccu, 0xedcd3955u, 0xffe00000u, 0x8000f000u, 0x07ff000fu, 0x000f07ffu, 0xffffffffu, 0xffffffe0u, 0x3555fc00u, 0xfc00fc00u, 0xffffedccu, 0xffffffffu, 0xfffffc00u, 0xfffffc00u, 0x3555edccu, 0x3555edccu, 0xffff7c00u, 0xffff7c00u, 0x7c00ffffu, 0x7ffffc00u, 0xfffffc00u, 0xfffffc00u, 0xffff7c00u, 0xffff7c00u, 0x7c007c00u, 0xffff7c00u, 0x3555fc00u, 0x35553555u, 0x3555fc00u, 0x3555edccu, 0xedcc3555u, 0xb5557c00u}, - {0x8400c000u, 0xa11c8400u, 0x8400c000u, 0xc000651cu, 0x00000000u, 0x00000000u, 0x7d231234u, 0x7d230000u, 0x7d231234u, 0x12341234u, 0x82dd1234u, 0x82dd82ddu, 0x82dd1234u, 0x95110000u, 0x7d230000u, 0x0000d230u, 0x7d230000u, 0x00007d23u, 0x7d230000u, 0x7d230000u, 0x7d231234u, 0x12341234u, 0x00000000u, 0x00000000u, 0x7d231234u, 0x7d230000u, 0x00000000u, 0x00001234u, 0x7d23c000u, 0x7d23c000u, 0x84007d23u, 0xfd23c000u, 0x7d231234u, 0x7d230000u, 0x7d231234u, 0x7d230000u, 0x00000000u, 0x7d230000u, 0x00000000u, 0x00000000u, 0x00009234u, 0x00001234u, 0x12340000u, 0x80001234u}, - {0x7d557e55u, 0x7e557d55u, 0x7d557e55u, 0x7e557d55u, 0x00000000u, 0x00000000u, 0xfc007c00u, 0xfc000000u, 0xfc007c00u, 0x7c007c00u, 0x04007c00u, 0x04000400u, 0x04007c00u, 0x80000000u, 0xfc000000u, 0x0000fc00u, 0xfc000000u, 0x0000fc00u, 0xfc000000u, 0xfc000000u, 0x00007c00u, 0x7c007c00u, 0xfc000000u, 0xfc00fc00u, 0xfc007c00u, 0xfc000000u, 0x00000000u, 0x00007c00u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0xfc007c00u, 0xfc000000u, 0xfc007c00u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0xfc000000u, 0xfc00fc00u, 0x0000fc00u, 0x00007c00u, 0x7c000000u, 0x80007c00u}, - {0xfe557d55u, 0x7d55fe55u, 0xfe557d55u, 0x7d55fe55u, 0x00000000u, 0x00000000u, 0x7c007c00u, 0x7c00f800u, 0x7c007c00u, 0x7c007c00u, 0x7c008400u, 0x7c007c00u, 0x7c008400u, 0x00000000u, 0x00007c00u, 0x7c000000u, 0x00007c00u, 0x7c000000u, 0x00007c00u, 0x00007c00u, 0x7c007c00u, 0x7c007c00u, 0x00000000u, 0x00000000u, 0x7c007c00u, 0x00007c00u, 0x00000000u, 0x7c000000u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0x7c007c00u, 0x7c007c00u, 0xfc00fc00u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0x00000000u, 0x00000000u, 0x80008000u, 0x7c007c00u, 0x00007c00u, 0x00000000u}, +constexpr std::uint32_t ExpectedIeeeOff[32][48] = { + {0x3c003c00u, 0x3c003c00u, 0x3c003c00u, 0x3c003c00u, 0x00000000u, 0x00000000u, 0x7c007c00u, 0x7c00fc00u, 0x7c007c00u, 0x7c007c00u, 0x7c007c00u, 0x7c007c00u, 0x7c007c00u, 0xfc00fc00u, 0x80000000u, 0x00008000u, 0x80000000u, 0x00008000u, 0x80000000u, 0x80000000u, 0xfc007c00u, 0x7c007c00u, 0x80000000u, 0x80008000u, 0xfc007c00u, 0x8000fc00u, 0x80000000u, 0x00007c00u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0xfc007c00u, 0xfc00fc00u, 0x7c007c00u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0xfc000000u, 0xfc00fc00u, 0xfc00fc00u, 0x80007c00u, 0x7c000000u, 0x7c007c00u, 0x00000000u, 0x00000000u, 0x00003c00u, 0x3c003c00u}, + {0xbc007c00u, 0x7c00bc00u, 0xbc007c00u, 0x7c00bc00u, 0x00000000u, 0x00000000u, 0x78007800u, 0x7800f800u, 0x78007800u, 0x78007800u, 0x80008000u, 0x80008000u, 0x80008000u, 0x00000000u, 0x7c00fc00u, 0xfc007c00u, 0x7c00fc00u, 0xfc007c00u, 0x7c00fc00u, 0x7c00fc00u, 0x7c007c00u, 0x7c007c00u, 0xfc00fc00u, 0xfc00fc00u, 0xfc00fc00u, 0x7c00fc00u, 0x7c007c00u, 0xfc007c00u, 0xfc00fe00u, 0xfc00fe00u, 0xfc00fe00u, 0x7c007c00u, 0xfe00fe00u, 0xfe00fc00u, 0x7c007c00u, 0xfc00fc00u, 0xfc00fc00u, 0xfc00fc00u, 0xfc00fc00u, 0xfc00fc00u, 0xfc00fc00u, 0x7c007c00u, 0x7c00fc00u, 0x7c007c00u, 0x00000000u, 0x3c000000u, 0x3c003c00u, 0x3c003c00u}, + {0x7bffc000u, 0xc000fbffu, 0x7bffc000u, 0x7fff3c00u, 0x43ff0400u, 0x040043ffu, 0xbc00fbffu, 0xbc003c00u, 0xbc00fbffu, 0xfbfffbffu, 0xbc02fc01u, 0xbc02bc02u, 0xbc02fc01u, 0x7c013c02u, 0xfffeffffu, 0xfffe7fffu, 0x3fffffffu, 0xffff3fffu, 0x3fffffffu, 0x3fffffffu, 0x7fffffffu, 0xffffffffu, 0x3c01fc00u, 0x3c013c01u, 0x7fffffffu, 0xfc00ffffu, 0x3c01fc00u, 0x3c017fffu, 0x7fffffffu, 0x7fffffffu, 0xffff7fffu, 0xffffffffu, 0x7fffffffu, 0x7fffffffu, 0x7fff7fffu, 0x7fffffffu, 0xffffffffu, 0x7fff7fffu, 0x3c01fc00u, 0x3c013c01u, 0x3c017c00u, 0x3c01fc00u, 0xfc003c01u, 0xbc01fc00u, 0x3c000000u, 0x00003c00u, 0x3c000000u, 0x00000000u}, + {0xc0009400u, 0x1400c000u, 0xc0009400u, 0x1400c000u, 0x00008000u, 0x80000000u, 0x1400ffffu, 0x14009400u, 0x1400ffffu, 0xffffffffu, 0x1400ffffu, 0x14001400u, 0x1400ffffu, 0x7fff9400u, 0x00000000u, 0x80000000u, 0x00000001u, 0x00010000u, 0x0000ffffu, 0x00008000u, 0x14007fffu, 0x7fff7fffu, 0x00008000u, 0x00000000u, 0x14008000u, 0x7fff8000u, 0x00007fffu, 0x14000000u, 0xc0007fffu, 0xc0007fffu, 0x7fff1400u, 0xc000ffffu, 0x14007fffu, 0x14001400u, 0x94007fffu, 0x00007fffu, 0x7fff7fffu, 0x0000ffffu, 0x00008000u, 0x00000000u, 0x80008000u, 0x14008000u, 0x00001400u, 0x00000000u, 0x00000000u, 0x00000000u, 0x14000000u, 0x00000000u}, + {0x7e00d5feu, 0x7ffe8000u, 0x7e00d5feu, 0x81fe7e00u, 0x8000d600u, 0xd6008000u, 0x7fff7c55u, 0x7fff7dffu, 0x7fff7c55u, 0x7c557c55u, 0x7fff8055u, 0x7fff7fffu, 0x7fff8055u, 0x7e5581ffu, 0x0000c000u, 0x00000000u, 0x000103f0u, 0x03f00001u, 0xffff03f0u, 0xffff0000u, 0xffff7e00u, 0x7e007e00u, 0x8000fe55u, 0x80008000u, 0xfffffe55u, 0xfe55ffffu, 0x80007e00u, 0x7e008000u, 0xfffffe55u, 0xfffffe55u, 0xfe55ffffu, 0xffff7e55u, 0xfffffe55u, 0xffffffffu, 0x7ffffe55u, 0xfffffe55u, 0xfe55fe55u, 0xffff7e55u, 0x8000fe55u, 0x80008000u, 0x00007e55u, 0x8000fe55u, 0x8000ffffu, 0x8000fe55u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x80007656u, 0x4254c3feu, 0x80007656u, 0x4254c3feu, 0x00017801u, 0x78010001u, 0xfffe7802u, 0xfffebc00u, 0xfffe7802u, 0x78027802u, 0x00000000u, 0x00000000u, 0x00000000u, 0xbc0243feu, 0x80007802u, 0x8000fffeu, 0x00001e00u, 0x1e000000u, 0x00001e00u, 0x00000000u, 0x7fff3c01u, 0x3c013c01u, 0x7fff3c01u, 0x7fff7fffu, 0x7fff3c01u, 0x7fff7fffu, 0x7fff3c01u, 0x3c013c01u, 0x7ffffe55u, 0x7ffffe55u, 0x7fff7fffu, 0x7ffffe55u, 0x7fff4001u, 0x7fff7fffu, 0xffff0000u, 0x7fff3c02u, 0x3c023c02u, 0x7fff3c02u, 0x7fff3c01u, 0x7fff7fffu, 0x7fffbc01u, 0x7fff3c01u, 0x3c013c01u, 0xffff3c01u, 0x00003c00u, 0x3c003c00u, 0x00003c00u, 0x00003c00u}, + {0x7bff6000u, 0x60007bffu, 0x7bff6000u, 0x60007bffu, 0x00000000u, 0x00000000u, 0xedcc6dccu, 0xedcc6dccu, 0xedcc6dccu, 0x6dcc6dccu, 0xedcc6dccu, 0xedccedccu, 0xedcc6dccu, 0xedcc6dccu, 0x00000000u, 0x00000000u, 0x00000008u, 0x00080000u, 0x0000fff8u, 0x0000fff8u, 0x0000edccu, 0xedccedccu, 0xedcc8000u, 0xedccedccu, 0xedccedccu, 0xedccedccu, 0x00008000u, 0x80000000u, 0x7bff6000u, 0x7bff6000u, 0x7bff6000u, 0x7bff6000u, 0xedccedccu, 0xedccedccu, 0x6dccedccu, 0x80000000u, 0x00000000u, 0x80000000u, 0xedccedccu, 0xedccedccu, 0xedcc8000u, 0x00008000u, 0x00008000u, 0x6dcc0000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3c000000u}, + {0x00001dccu, 0xedccd000u, 0x00001dccu, 0xbdcc0000u, 0x00003000u, 0x30000000u, 0x8000ce34u, 0x80009634u, 0x8000ce34u, 0xce34ce34u, 0x8800a9ccu, 0x88008800u, 0x8800a9ccu, 0xc00071ccu, 0xfc001234u, 0x1234fc00u, 0xfc001234u, 0x1234fc00u, 0xfc001234u, 0xfc001234u, 0xfc001234u, 0x12341234u, 0x8400bc00u, 0x84008400u, 0xfc00bc00u, 0xfc008400u, 0x84001234u, 0x1234bc00u, 0x7c00edccu, 0x7c00edccu, 0x92347c00u, 0xfc00edccu, 0xfc00bbfeu, 0xfc0011b4u, 0xfc00bc01u, 0x7c009234u, 0x92349234u, 0x7c009234u, 0xfc00bc00u, 0xfc00fc00u, 0x84001234u, 0x84001234u, 0xbc001234u, 0x04009234u, 0x00000000u, 0x00000000u, 0x00001234u, 0x04000000u}, + {0xc99f5e41u, 0x4ceb74ddu, 0xc99f5e41u, 0xcd1df4abu, 0xcb9f0801u, 0x0801cb9fu, 0xb278f7feu, 0xb278f922u, 0xb278f7feu, 0xf7fef7feu, 0x47ce0000u, 0x47ce47ceu, 0x47ce0000u, 0x46aa0124u, 0xaaa88000u, 0xdff88000u, 0x06aa0000u, 0x000006aau, 0x06aa0000u, 0x06aa0f7fu, 0x7d237bffu, 0x7bff7bffu, 0x35557bffu, 0x35553555u, 0x7d237bffu, 0x7bff7d23u, 0x35557bffu, 0x7bff3555u, 0x7d237c00u, 0x7d237c00u, 0xfe007d23u, 0x7d23fc00u, 0x7d237c00u, 0x7d237d23u, 0xfd230000u, 0x7d237c00u, 0x7c007c00u, 0x7d237c00u, 0x35557bffu, 0x35553555u, 0xb555fbffu, 0x35557bffu, 0x7bff7bffu, 0x35557bffu, 0x35553c00u, 0x35553c00u, 0x35553c00u, 0x35553c00u}, + {0x00008000u, 0x80000000u, 0x00008000u, 0x80000000u, 0x80008000u, 0x80008000u, 0xb555fc01u, 0xb555b555u, 0xb555fc01u, 0xfc01fc01u, 0xb555fc01u, 0xb555b555u, 0xb555fc01u, 0xfc01b555u, 0x00000000u, 0x00000000u, 0x04004000u, 0x40000400u, 0xfc00c000u, 0xfc00fc00u, 0x35557c01u, 0x7c017c01u, 0x80008000u, 0x80008000u, 0x80008000u, 0x80008000u, 0x35557c01u, 0x35557c01u, 0x80007c01u, 0x80007c01u, 0x7c010000u, 0x0000fc01u, 0x35557c01u, 0x35553555u, 0xb5557c01u, 0x80007c01u, 0x7c017c01u, 0x8000fc01u, 0x80008000u, 0x80008000u, 0x00008000u, 0x35558000u, 0x80003555u, 0x80000000u, 0x00000000u, 0x00000000u, 0x35550000u, 0x00000000u}, + {0x03ff0000u, 0xa2480000u, 0x03ff0000u, 0x0000a248u, 0x03ff0000u, 0x000003ffu, 0xfc00c248u, 0xfc007fffu, 0xfc00c248u, 0xc248c248u, 0x03fec248u, 0x03fe03feu, 0x03fec248u, 0x46477fffu, 0x80000000u, 0x00000100u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x7fff0000u, 0x00000000u, 0x7c01c248u, 0x7c017c01u, 0x7fffc248u, 0xc2487fffu, 0x7c010000u, 0x00007c01u, 0x7fff0000u, 0x7fff0000u, 0x00007fffu, 0x7fff0000u, 0x7fffc248u, 0x7fff7fffu, 0xffffc248u, 0x7fff8000u, 0x80008000u, 0x7fff8000u, 0x7fffc248u, 0x7fff7fffu, 0x7fff0000u, 0x7fff0000u, 0xc2480000u, 0xffff8000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0xc2337fffu, 0x87fd7fffu, 0xc2337fffu, 0xffff07fdu, 0x42348000u, 0x80004234u, 0x4e35bffeu, 0x4e35d234u, 0x4e35bffeu, 0xbffebffeu, 0xd6333ffeu, 0xd633d633u, 0xd6333ffeu, 0xc3fd5234u, 0xc0100000u, 0x00004000u, 0x03c00003u, 0x000303c0u, 0x03c0ffffu, 0x03c0fc00u, 0x3c01fffeu, 0xfffefffeu, 0x1234c000u, 0x12341234u, 0x3c01fffeu, 0xfffec000u, 0x1234c000u, 0x12343c01u, 0x7ffffffeu, 0x7ffffffeu, 0xfffeffffu, 0x7fff7ffeu, 0x3c02fffeu, 0x3c02bfffu, 0x3c00fffeu, 0x1236fffeu, 0xfffefffeu, 0x12367ffeu, 0x1234c000u, 0x12341234u, 0xbc01c000u, 0x3c01c000u, 0x3c011234u, 0x3c014000u, 0x12340000u, 0x3c000000u, 0x3c000000u, 0x3c003c00u}, + {0x9c00a56eu, 0xcffef560u, 0x9c00a56eu, 0x795e4c00u, 0x2000a570u, 0xa5702000u, 0xc6480000u, 0xc6483014u, 0xc6480000u, 0x00000000u, 0xbe482468u, 0xbe48be48u, 0xbe482468u, 0x8e34547cu, 0x0000dcc0u, 0xcc004000u, 0x00840edcu, 0x0edc0084u, 0xff84fedcu, 0xff84ffedu, 0x42481234u, 0x12341234u, 0x8400edccu, 0x84008400u, 0x8400edccu, 0x8400edccu, 0x42481234u, 0x42481234u, 0x7c00fffeu, 0x7c00fffeu, 0x7c00fffeu, 0x7c00fffeu, 0x4248edccu, 0x4248edcbu, 0xc2486dccu, 0x8a48c47fu, 0xc47fc47fu, 0x8a48c47fu, 0x8400edccu, 0x84008400u, 0x0400edccu, 0x42481234u, 0x12344248u, 0x84006dccu, 0x00000000u, 0x00000000u, 0x3c001234u, 0x00003c00u}, + {0x04023a00u, 0x3a000402u, 0x04023a00u, 0x38020600u, 0x00020200u, 0x02000002u, 0xfffd7dffu, 0xfffd7ffdu, 0xfffd7dffu, 0x7dff7dffu, 0xffff7e01u, 0xffffffffu, 0xffff7e01u, 0xfe017fffu, 0xc0007fffu, 0xc000ffffu, 0x00037fffu, 0x7fff0003u, 0xffff7fffu, 0xffff0001u, 0xffff7fffu, 0x7fff7fffu, 0xfffefe00u, 0xfffefffeu, 0xfffffe00u, 0xfffffffeu, 0xfffe7fffu, 0x7ffffe00u, 0xfffefe00u, 0xfffefe00u, 0xfe00fffeu, 0xfffe7e00u, 0xfffefe00u, 0xfffefffeu, 0x7ffefe00u, 0xfffefe00u, 0xfe00fe00u, 0xfffe7e00u, 0xfffefe00u, 0xfffefffeu, 0xfffe7e00u, 0xfffefe00u, 0xfe00fffeu, 0x7ffefe00u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x96003bb8u, 0xee00ebccu, 0x96003bb8u, 0x6bcc6e00u, 0x9800bdb8u, 0xbdb89800u, 0x9034c247u, 0x90349233u, 0x9034c247u, 0xc247c247u, 0x9434c249u, 0x94349434u, 0x9434c249u, 0xc4489235u, 0xe000ff00u, 0xfff00000u, 0x07e0007fu, 0x007f07e0u, 0x07e0007fu, 0x07e007ffu, 0x7e007fffu, 0x7fff7fffu, 0x12344248u, 0x12341234u, 0x7e007fffu, 0x7e007fffu, 0x12344248u, 0x12344248u, 0x7e007fffu, 0x7e007fffu, 0x7fff7e00u, 0xfe007fffu, 0x7e007fffu, 0x7e007fffu, 0x7e00ffffu, 0x7e007fffu, 0x7fff7fffu, 0x7e00ffffu, 0x12344248u, 0x12341234u, 0x1234c248u, 0x12344248u, 0x42481234u, 0x92344248u, 0x12343c00u, 0x3c001234u, 0x12343c00u, 0x00003c00u}, + {0x80000800u, 0x08006340u, 0x80000800u, 0xeb408000u, 0x00000000u, 0x00000000u, 0x35555240u, 0x35558b95u, 0x35555240u, 0x52405240u, 0x3555a5c0u, 0x35553555u, 0x3555a5c0u, 0xfc00df15u, 0x00005640u, 0xc8000000u, 0x00005640u, 0x56400000u, 0x00005640u, 0x000002b2u, 0x35555640u, 0x56405640u, 0x0000fc00u, 0x00000000u, 0x3555fc00u, 0xfc005640u, 0x00005640u, 0x35550000u, 0x0000fc00u, 0x0000fc00u, 0xfc000800u, 0x80007c00u, 0x3555fc00u, 0x35555645u, 0xb555fc00u, 0x0000fc00u, 0xfc00fc00u, 0x0000fc00u, 0x0000fc00u, 0x00000000u, 0x80005640u, 0x35555640u, 0x00005640u, 0x0000d640u, 0x00000000u, 0x00003555u, 0x35553c00u, 0x00000000u}, + {0xdd70b2c8u, 0x611b56e4u, 0xdd70b2c8u, 0x9ee3191cu, 0xa57032c9u, 0x32c9a570u, 0x0000fa46u, 0x00006aefu, 0x0000fa46u, 0xfa46fa46u, 0xdb980000u, 0xdb98db98u, 0xdb980000u, 0x6aef70a9u, 0x4000e918u, 0x300091a0u, 0x00010fa4u, 0x0fa40001u, 0x00010fa4u, 0x00010007u, 0x12347d23u, 0x7d237d23u, 0xedcc7d23u, 0xedccedccu, 0xedcc7d23u, 0x7d23edccu, 0x12347d23u, 0x7d231234u, 0xc3fd7d23u, 0xc3fd7d23u, 0x7d237fffu, 0x44fffd23u, 0xedcc7d23u, 0xedcc7d23u, 0x6dcc7d23u, 0xc47f7d23u, 0x7d237d23u, 0xc47ffd23u, 0xedcc7d23u, 0xedccedccu, 0xedccfd23u, 0x12347d23u, 0x1234edccu, 0x6dcc7d23u, 0x00000000u, 0x12340000u, 0x12340000u, 0x3c000000u}, + {0xc000d640u, 0xd6413c00u, 0xc000d640u, 0x5640bc01u, 0x84008000u, 0x80008400u, 0xfbff7fffu, 0xfbfffc00u, 0xfbff7fffu, 0x7fff7fffu, 0xfc017fffu, 0xfc01fc01u, 0xfc017fffu, 0x8000fc00u, 0x7fff0000u, 0x80008000u, 0x7fff0001u, 0x00017fffu, 0x7fffffffu, 0x7fff8000u, 0x7fffffffu, 0xffffffffu, 0x7c008000u, 0x7c007c00u, 0x7fffffffu, 0xffff8000u, 0x7c008000u, 0x7c007fffu, 0x7fffffffu, 0x7fffffffu, 0xffff7fffu, 0xffff7fffu, 0x7fffffffu, 0x7fff7c00u, 0x7fffffffu, 0x7fffffffu, 0xffffffffu, 0x7fff7fffu, 0x7c008000u, 0x7c007c00u, 0x7c008000u, 0x7c008000u, 0xffff7c00u, 0xfc000000u, 0x3c000000u, 0x00000000u, 0x3c000000u, 0x00000000u}, + {0xfffecd56u, 0x38002153u, 0xfffecd56u, 0x5955fffeu, 0x00009556u, 0x95560000u, 0x3c013553u, 0x3c017156u, 0x3c013553u, 0x35533553u, 0x3c01caa9u, 0x3c013c01u, 0x3c01caa9u, 0xfffe06acu, 0x00004000u, 0x6aaa0000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00001aaau, 0x3c013555u, 0x35553555u, 0x0000fffeu, 0x00000000u, 0x3c01fffeu, 0xfffe3c01u, 0x00003555u, 0x35550000u, 0xfffefffeu, 0xfffefffeu, 0xfffe3800u, 0xfffe7ffeu, 0x3c01fffeu, 0x3c013d56u, 0xbc01fffeu, 0x0000fffeu, 0xfffefffeu, 0x00007ffeu, 0x00003555u, 0x00000000u, 0x80003555u, 0x3c013555u, 0x00003c01u, 0x0000b555u, 0x00003555u, 0x00003555u, 0x3c003555u, 0x00000000u}, + {0x31b86c40u, 0xd5b84840u, 0x31b86c40u, 0x6c40b1b8u, 0x1db83440u, 0x34401db8u, 0x3e478490u, 0x3e470490u, 0x3e478490u, 0x84908490u, 0x46490000u, 0x46494649u, 0x46490000u, 0xc6498000u, 0xff004800u, 0x4800ff00u, 0x007b0042u, 0x0042007bu, 0x007b0042u, 0x007b0042u, 0x7bff4248u, 0x42484248u, 0xc2484248u, 0xc248c248u, 0xc2484248u, 0x7bffc248u, 0x7bff4248u, 0x42484248u, 0xfc00492fu, 0xfc00492fu, 0x48effc00u, 0x7c00c8afu, 0x7bff4648u, 0x7bff0000u, 0x7bff0000u, 0xfc0048efu, 0x48ef48efu, 0xfc0048efu, 0xc2484248u, 0xc248c248u, 0xfbffc248u, 0x7bff4248u, 0x7bff4248u, 0x7bff4248u, 0x00003c00u, 0x3c000000u, 0x3c003c00u, 0x3c003c00u}, + {0x9a908000u, 0xdcbc4000u, 0x9a908000u, 0x80009cbcu, 0x5a900000u, 0x00005a90u, 0xdb98fe55u, 0xdb98edccu, 0xdb98fe55u, 0xfe55fe55u, 0x0000fe55u, 0x00000000u, 0x0000fe55u, 0x1089edccu, 0xc0000000u, 0x0000b980u, 0x000e0000u, 0x0000000eu, 0xfffe0000u, 0xfffe0000u, 0xedcc0000u, 0x00000000u, 0xedccfe55u, 0xedccedccu, 0xedccfe55u, 0xfe55edccu, 0xedcc0000u, 0x0000edccu, 0x7c00fe55u, 0x7c00fe55u, 0xfe557c00u, 0xfc007e55u, 0xf1ccfe55u, 0xf1ccedccu, 0x0000fe55u, 0x7c00fe55u, 0xfe55fe55u, 0x7c007e55u, 0xedcc0000u, 0xedccedccu, 0xedcc0000u, 0xedcc0000u, 0xedcc0000u, 0x6dcc8000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3c000000u}, + {0x12344400u, 0x14000a34u, 0x12344400u, 0x0c001234u, 0x00003000u, 0x30000000u, 0xfffef1ccu, 0xfffe03feu, 0xfffef1ccu, 0xf1ccf1ccu, 0xfffee9ccu, 0xfffefffeu, 0xfffee9ccu, 0xedccfbfeu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000400u, 0x04000400u, 0xfffeedccu, 0xfffefffeu, 0xfffeedccu, 0xedccfffeu, 0x00000400u, 0x04000000u, 0xfffeb5c8u, 0xfffeb5c8u, 0xb5c9fffeu, 0xfffe35d0u, 0xfffeedccu, 0xfffefffeu, 0x7ffeedccu, 0xfffeb5ccu, 0xb5ccb5ccu, 0xfffeb5ccu, 0x0000edccu, 0x00000000u, 0x80000400u, 0x00000400u, 0x00000400u, 0x00008400u, 0x00000000u, 0x00000400u, 0x00000400u, 0x00000000u}, + {0x7fff9c00u, 0xbc00c69eu, 0x7fff9c00u, 0x029f7fffu, 0x0000e000u, 0xe0000000u, 0x7d235e55u, 0x7d237b78u, 0x7d235e55u, 0x5e555e55u, 0x7d2361abu, 0x7d237d23u, 0x7d2361abu, 0x60007eceu, 0x0000fe55u, 0xf2a80000u, 0x0000fe55u, 0xfe550000u, 0x0000fe55u, 0x0000ffcau, 0x7d236000u, 0x60006000u, 0x0000fe55u, 0x00000000u, 0x7d23fe55u, 0x6000fe55u, 0x00006000u, 0x7d230000u, 0x7d23fe55u, 0x7d23fe55u, 0xfe557d23u, 0x7d23fe55u, 0x7d23fe55u, 0x7d237d23u, 0xfd237e55u, 0x7d23fe55u, 0xfe55fe55u, 0x7d237e55u, 0x00006000u, 0x00000000u, 0x8000e000u, 0x00006000u, 0x60007d23u, 0x00006000u, 0x00003c00u, 0x00000000u, 0x00003c00u, 0x00003c00u}, + {0x3c008400u, 0x84007c00u, 0x3c008400u, 0x84007c00u, 0xc0000000u, 0x0000c000u, 0x3c018000u, 0x3c017c01u, 0x3c018000u, 0x80008000u, 0xbc018000u, 0xbc01bc01u, 0xbc018000u, 0xc0007c01u, 0x80000000u, 0x0000c000u, 0x60000000u, 0x00006000u, 0xe0000000u, 0xe0000000u, 0x7c010000u, 0x00000000u, 0xc0008000u, 0xc000c000u, 0xc0008000u, 0xc0007c01u, 0x7c010000u, 0x00008000u, 0x7c018400u, 0x7c018400u, 0x7c007c01u, 0x7c018400u, 0x7c010000u, 0x7c017c01u, 0xfc018000u, 0x7c018000u, 0x80008000u, 0x7c018000u, 0xc0008000u, 0xc000c000u, 0x40000000u, 0xc0000000u, 0x80000000u, 0xc0008000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x40006fe8u, 0xaa007de8u, 0x40006fe8u, 0xe7e84000u, 0x2c00f1e8u, 0xf1e82c00u, 0xb155c09du, 0xb155f79du, 0xb155c09du, 0xc09dc09du, 0xb9553c0du, 0xb955b955u, 0xb9553c0du, 0x8255730du, 0x80004900u, 0x49008000u, 0x03e00612u, 0x061203e0u, 0x03e0fe12u, 0x03e0fe12u, 0x7c00fe55u, 0xfe55fe55u, 0x3555c248u, 0x35553555u, 0x7c00fe55u, 0xfe55c248u, 0x3555c248u, 0x35557c00u, 0x7c00fe55u, 0x7c00fe55u, 0xfe557e00u, 0xfc007e55u, 0x7c00fe55u, 0x7c00c19du, 0x7c00fe55u, 0x7c00fe55u, 0xfe55fe55u, 0x7c007e55u, 0x3555c248u, 0x35553555u, 0xfc00c248u, 0x7c00c248u, 0x7c003555u, 0x7c004248u, 0x35550000u, 0x3c000000u, 0x3c000000u, 0x3c003c00u}, + {0x755fffffu, 0x5e480e33u, 0x755fffffu, 0x4e341e47u, 0x7960c3ffu, 0xc3ff7960u, 0x30143c00u, 0x3014edcbu, 0x30143c00u, 0x3c003c00u, 0xab843c02u, 0xab84ab84u, 0xab843c02u, 0xf9b9edcdu, 0x8000fffeu, 0xf0008490u, 0x00047fffu, 0x7fff0004u, 0x0004ffffu, 0x0004ffffu, 0x42483c01u, 0x3c013c01u, 0xedccffffu, 0xedccedccu, 0xedccffffu, 0x4248ffffu, 0x42483c01u, 0xedcc3c01u, 0xfc00ffffu, 0xfc00ffffu, 0xfffff48du, 0xf9b8ffffu, 0xedcbffffu, 0xedcbffffu, 0x6dcd7fffu, 0xf48dffffu, 0xffffffffu, 0xf48d7fffu, 0xedcc3c01u, 0xedccedccu, 0xedccbc01u, 0x42483c01u, 0x4248edccu, 0x6dcc3c01u, 0x00003c00u, 0x3c000000u, 0x3c003c00u, 0x3c003c00u}, + {0x8132edccu, 0x70a9fe55u, 0x8132edccu, 0xedcc8132u, 0x82dd0000u, 0x000082ddu, 0x7d227d23u, 0x7d227d23u, 0x7d227d23u, 0x7d237d23u, 0x7d247d23u, 0x7d247d24u, 0x7d247d23u, 0x7d247d23u, 0xfff80000u, 0x0000fff8u, 0x1fff0000u, 0x00001fffu, 0xffff0000u, 0xffff0000u, 0x7d237d23u, 0x7d237d23u, 0xffff0000u, 0xffffffffu, 0xffff7d23u, 0xffff7d23u, 0x7d230000u, 0x00007d23u, 0x7d237d23u, 0x7d237d23u, 0x7d237d23u, 0x7d23fd23u, 0x7d237d23u, 0x7d237d23u, 0xfd237d23u, 0x7d237d23u, 0x7d237d23u, 0x7d23fd23u, 0x7d230000u, 0x7d237d23u, 0x7d230000u, 0x7d230000u, 0x7d230000u, 0xfd238000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00008000u, 0x80002000u, 0x00008000u, 0xa0000000u, 0x00000000u, 0x00000000u, 0xc2480400u, 0xc248c648u, 0xc2480400u, 0x04000400u, 0xc248fc00u, 0xc248c248u, 0xc248fc00u, 0x0000be48u, 0x00000400u, 0x00000000u, 0x00000400u, 0x04000000u, 0x00000400u, 0x00000004u, 0x00000400u, 0x04000400u, 0xc2480000u, 0xc248c248u, 0xc2480400u, 0x0000c248u, 0x00000000u, 0x04000000u, 0x00000000u, 0x00000000u, 0x00008000u, 0x00008000u, 0xc2480400u, 0xc248c248u, 0x42488400u, 0x80000000u, 0x00000000u, 0x80000000u, 0xc2480000u, 0xc248c248u, 0xc2488000u, 0x00000400u, 0x00000400u, 0x42480000u, 0x00000000u, 0x00000000u, 0x00000400u, 0x3c000000u}, + {0xcaab5400u, 0x9634ac00u, 0xcaab5400u, 0x30001234u, 0xcaabd000u, 0xd000caabu, 0x3554e9ccu, 0x35543155u, 0x3554e9ccu, 0xe9cce9ccu, 0x3556f1ccu, 0x35563556u, 0x3556f1ccu, 0xedcd3955u, 0xffe00000u, 0x8000f000u, 0x07ff000fu, 0x000f07ffu, 0xffffffffu, 0xffffffe0u, 0x3555fc00u, 0xfc00fc00u, 0xffffedccu, 0xffffffffu, 0xfffffc00u, 0xfffffc00u, 0x3555edccu, 0x3555edccu, 0xffff7c00u, 0xffff7c00u, 0x7c00ffffu, 0x7ffffc00u, 0xfffffc00u, 0xfffffc00u, 0xffff7c00u, 0xffff7c00u, 0x7c007c00u, 0xffff7c00u, 0x3555fc00u, 0x35553555u, 0x3555fc00u, 0x3555edccu, 0xedcc3555u, 0xb5557c00u, 0x35550000u, 0x00000000u, 0x35550000u, 0x00003c00u}, + {0x8400c000u, 0xa11c8400u, 0x8400c000u, 0xc000651cu, 0x00000000u, 0x00000000u, 0x7d231234u, 0x7d230000u, 0x7d231234u, 0x12341234u, 0x82dd1234u, 0x82dd82ddu, 0x82dd1234u, 0x95110000u, 0x7d230000u, 0x0000d230u, 0x7d230000u, 0x00007d23u, 0x7d230000u, 0x7d230000u, 0x7d231234u, 0x12341234u, 0x00000000u, 0x00000000u, 0x7d231234u, 0x7d230000u, 0x00000000u, 0x00001234u, 0x7d23c000u, 0x7d23c000u, 0x84007d23u, 0xfd23c000u, 0x7d231234u, 0x7d230000u, 0x7d231234u, 0x7d230000u, 0x00000000u, 0x7d230000u, 0x00000000u, 0x00000000u, 0x00009234u, 0x00001234u, 0x12340000u, 0x80001234u, 0x00000000u, 0x12340000u, 0x00001234u, 0x00001234u}, + {0x7d557e55u, 0x7e557d55u, 0x7d557e55u, 0x7e557d55u, 0x00000000u, 0x00000000u, 0xfc007c00u, 0xfc000000u, 0xfc007c00u, 0x7c007c00u, 0x04007c00u, 0x04000400u, 0x04007c00u, 0x80000000u, 0xfc000000u, 0x0000fc00u, 0xfc000000u, 0x0000fc00u, 0xfc000000u, 0xfc000000u, 0x00007c00u, 0x7c007c00u, 0xfc000000u, 0xfc00fc00u, 0xfc007c00u, 0xfc000000u, 0x00000000u, 0x00007c00u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0xfc007c00u, 0xfc000000u, 0xfc007c00u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0xfc000000u, 0xfc00fc00u, 0x0000fc00u, 0x00007c00u, 0x7c000000u, 0x80007c00u, 0x00000000u, 0x00000000u, 0x00003c00u, 0x00003c00u}, + {0xfe557d55u, 0x7d55fe55u, 0xfe557d55u, 0x7d55fe55u, 0x00000000u, 0x00000000u, 0x7c007c00u, 0x7c00f800u, 0x7c007c00u, 0x7c007c00u, 0x7c008400u, 0x7c007c00u, 0x7c008400u, 0x00000000u, 0x00007c00u, 0x7c000000u, 0x00007c00u, 0x7c000000u, 0x00007c00u, 0x00007c00u, 0x7c007c00u, 0x7c007c00u, 0x00000000u, 0x00000000u, 0x7c007c00u, 0x00007c00u, 0x00000000u, 0x7c000000u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0x7c007c00u, 0x7c007c00u, 0xfc00fc00u, 0xfe00fe00u, 0xfe00fe00u, 0xfe00fe00u, 0x00000000u, 0x00000000u, 0x80008000u, 0x7c007c00u, 0x00007c00u, 0x00000000u, 0x00000000u, 0x00003c00u, 0x3c003c00u, 0x00000000u}, }; -constexpr const char* Names[44] = { +constexpr const char* Names[48] = { "v_pk_mad_i16 v4, v5, v6", "v_pk_mad_i16 v4, v5, v6 op_sel:[1,0,1] op_sel_hi:[0,1,0]", "v_pk_mad_u16 v4, v5, v6", @@ -188,10 +189,14 @@ constexpr const char* Names[44] = { "v_pk_max_f16 v4, v5", "v_pk_max_f16 v4, v5 op_sel:[1,0] op_sel_hi:[0,1]", "v_pk_max_f16 v4, v5 neg_lo:[0,1] neg_hi:[1,0]", + "v_pk_min_f16 v4, v5 clamp", + "v_pk_min_f16 v4, v5 op_sel:[1,0] op_sel_hi:[0,1] clamp", + "v_pk_max_f16 v4, v5 clamp", + "v_pk_max_f16 v4, v5 neg_lo:[0,1] neg_hi:[1,0] clamp", }; 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); @@ -231,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(); @@ -241,7 +246,7 @@ void Check() { for (std::uint32_t tid = 0; tid < Threads; ++tid) { const std::uint32_t* in = &Input[tid * Inputs]; const std::uint32_t* out = &Output[tid * Results]; - for (std::uint32_t i = 0; i < 44; ++i) { + for (std::uint32_t i = 0; i < 48; ++i) { Expect(tid, out[i], (Ieee ? Expected : ExpectedIeeeOff)[tid][i], Names[i]); } } @@ -259,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/PackedFloat16Native.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloat16Native.cpp new file mode 100644 index 0000000000..e1455fa12d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloat16Native.cpp @@ -0,0 +1,215 @@ +#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 = 8; +constexpr std::uint32_t Batches = 32; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084, 0x34060085, 0xe0381000, 0x80000401, 0xbf8c3f70, + 0xcc0f400a, 0x18020b04, + 0xcc10400b, 0x18020b04, + 0xcc0e400c, 0x1c1a0b04, + 0xcc0f4a0d, 0x30020b04, + 0xcc10c00e, 0x18020d04, + 0xcc0e330f, 0x8c1a0b04, + 0x7e200306, 0x78200b04, + 0xcc0ec011, 0x3c1a0f04, + 0xe0781000, 0x80010a03, 0xe0781010, 0x80010e03, 0xbf810000, +}; + +constexpr const char* Names[Results] = { + "v_pk_add_f16 v10, v4, v5", + "v_pk_mul_f16 v11, v4, v5", + "v_pk_fma_f16 v12, v4, v5, v6", + "v_pk_add_f16 v13, v4, v5 op_sel:[1,0] op_sel_hi:[0,1] neg_lo:[1,0] neg_hi:[0,1]", + "v_pk_mul_f16 v14, v4, v6 clamp", + "v_pk_fma_f16 v15, v4, v5, v6 op_sel:[0,1,1] op_sel_hi:[1,0,0] neg_lo:[0,0,1] neg_hi:[1,1,0]", + "v_pk_fmac_f16 v16, v4, v5", + "v_pk_fma_f16 v17, v4, v7, v6 neg_lo:[1,0,0] clamp", +}; + +struct Mode { + const char* name; + std::optional floatMode; +}; + +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}}, +}}; + +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(), "packed f16 native: malformed SPIR-V"); + if (opcode == spv::OpTypeFloat) { + Require(words >= 3u, "packed f16 native: malformed float type"); + if (spirv[offset + 2u] == 16u) half = spirv[offset + 1u]; + } + if (opcode == spv::OpTypeVector) { + Require(words >= 4u, "packed 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, "packed 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("packed 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; + 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 && nativeMath.mul, std::string("packed f16 native: ") + mode.name + " does not use f16vec2 add and mul"); + Require(!nativeMath.fma, std::string("packed 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("packed 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 * Results + index]; + const std::uint32_t expected = emulatedOutput[tid * Results + index]; + const std::uint32_t* row = &Input[tid * Inputs]; + Require(actual == expected, std::string("packed 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 * 2u; + } + } + std::printf("packed f16 native tests passed (%llu halves)\n", static_cast(compared)); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloatColorTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloatColorTarget.cpp new file mode 100644 index 0000000000..9b322cb6d0 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloatColorTarget.cpp @@ -0,0 +1,338 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "Optimization/ResourceProgram.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::ColorTargetLayout; +using AgcDriver::Graphics::ColorTileMode; +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 256; +constexpr std::uint32_t Height = 128; +constexpr std::uint32_t ScissorWidth = 128; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Float11_11_10Info = 0x4171cu; +constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t Float32Export = 9; +constexpr std::uint32_t Float16Export = 4; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, 32> Programs{}; +std::size_t programCount = 0; + +std::uint32_t SmallFloat(float value, std::uint32_t mantissaBits) { + const std::uint32_t infinity = 0x1fu << mantissaBits; + if (std::isnan(value)) { + const auto top = (std::bit_cast(value) & 0x7fffffu) >> (23u - mantissaBits); + return infinity | (top == 0u ? 1u : top); + } + if (!(value > 0.0f)) return 0; + if (std::isinf(value)) return infinity; + const double magnitude = value; + const double scale = std::ldexp(1.0, static_cast(mantissaBits)); + if (magnitude < std::ldexp(1.0, -14)) return static_cast(std::floor(magnitude * std::ldexp(1.0, 14) * scale)); + int exponent = 0; + const double fraction = std::frexp(magnitude, &exponent); + if (exponent - 1 > 15) return infinity - 1u; + return (static_cast(exponent - 1 + 15) << mantissaBits) | static_cast(std::floor((fraction * 2.0 - 1.0) * scale)); +} + +std::uint32_t Packed(std::array rgb) { + return SmallFloat(rgb[2], 5) | (SmallFloat(rgb[1], 6) << 10u) | (SmallFloat(rgb[0], 6) << 21u); +} + +std::uint32_t Half(float value) { + Require(value == 0.0f || (value >= 0.0625f && value <= 1.0f), "the test's half values are normal and at most 1"); + if (value == 0.0f) return 0; + const auto bits = std::bit_cast(value); + Require((bits & 0x1fffu) == 0, "the test's half values are exact"); + return ((((bits >> 23u) & 0xffu) - 112u) << 10u) | ((bits >> 13u) & 0x3ffu); +} + +std::span Program(const std::array& vgprs, bool compressed) { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + for (std::uint32_t vgpr = 0; vgpr < (compressed ? 2u : 4u); ++vgpr) { + code[at++] = 0x7e0002ffu | ((4u + vgpr) << 17u); + code[at++] = vgprs[vgpr]; + } + code[at++] = compressed ? 0xf8001c0fu : 0xf800180fu; + code[at++] = compressed ? 0x00000504u : 0x07060504u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +std::span Float32Program(std::array rgb) { + return Program({std::bit_cast(rgb[0]), std::bit_cast(rgb[1]), std::bit_cast(rgb[2]), std::bit_cast(1.0f)}, false); +} + +std::span Float16Program(std::array rgb) { + return Program({Half(rgb[0]) | (Half(rgb[1]) << 16u), Half(rgb[2]) | (Half(1.0f) << 16u), 0, 0}, true); +} + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (watched) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the color target"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate the color target"); + Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched"); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (watched) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + bool watched; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t attrib3) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Float11_11_10Info; + cx[0x31d] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = attrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == VK_FORMAT_R32_UINT && color.elementBytes == 4 && color.componentMapping == 0xc6u && color.packing == ShaderRecompiler::ColorExportPacking::Float11_11_10 && color.bytes == block.bytes, "the 11_11_10 float target did not decode as a packed R32_UINT surface"); + return color; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::uint32_t exportFormat, std::span pixelCode) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto address = reinterpret_cast(Triangle.data()); + const std::array vertexBuffer{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), static_cast(Triangle.size()), 0x01016facu}; + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {ScissorWidth, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + + const auto codeAddress = reinterpret_cast(pixelCode.data()); + const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, exportFormat}}; + const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, + {waveSize, 0, {}, std::nullopt, AgcDriver::Graphics::DecodePixelStageInfo(context, {}), std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + const auto handle = ShaderRecompiler::PrepareShader(fragment); + AgcDriver::DriverDetail::ShaderSnapshot snapshot{codeAddress, 0, 1, {pixelCode.begin(), pixelCode.end()}, {}}; + snapshot.prepared->entries.push_back({0, handle}); + fragment.shader.code = snapshot.code; + fragment.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); + fragment.context.pixel->targetExportPacking = AgcDriver::Graphics::ExportPackings(state); + const ShaderRecompiler::SrtRuntime runtime{}; + const auto materialize = [&](const ShaderRecompiler::RecompileRequest& request) { + const auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, request); + return invocation.Materialize(*invocation.Capture(runtime)); + }; + const auto pixelResult = materialize(fragment); + auto unpacked = fragment; + unpacked.context.pixel->targetExportPacking.fill(ShaderRecompiler::ColorExportPacking::None); + const auto plain = materialize(unpacked); + Require(pixelResult->variantId == ShaderRecompiler::GetPreparedArtifact(*handle).variantId && plain->variantId == pixelResult->variantId, "the draw did not use the registered pixel shader"); + Require(plain->PipelineVariantId() != pixelResult->PipelineVariantId() && materialize(fragment)->PipelineVariantId() == pixelResult->PipelineVariantId(), "the 11_11_10 float packing did not key its own specialization"); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, pixelResult.get(), PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +std::vector ReadBack(AgcDriver::VulkanDevice& device, const Block& block) { + AgcDriver::Graphics::StorageTexture::FlushPending(block.Address(), block.bytes, nullptr, "test read-back"); + device.WaitIdle(); + std::vector stored(block.bytes / 4u); + AgcDriver::GuestMemory::Read(block.Address(), std::as_writable_bytes(std::span(stored)), 1); + return stored; +} + +void ExpectSurface(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const std::vector& before, std::uint32_t word, const std::string& what) { + const auto stored = ReadBack(device, block); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto index = layout.Offset(x, y) / 4u; + const bool inside = x < ScissorWidth; + const auto expected = inside ? word : before[index]; + if (stored[index] == expected) continue; + char detail[160]; + std::snprintf(detail, sizeof(detail), ": pixel (%u, %u) %s the scissor holds 0x%08x, expected 0x%08x", x, y, inside ? "inside" : "outside", stored[index], expected); + throw std::runtime_error(what + detail); + } + } +} + +std::vector Fill(Block& block) { + std::vector words(block.bytes / 4u); + for (std::size_t index = 0; index < words.size(); ++index) words[index] = static_cast(index * 0x9e3779b9u); + std::memcpy(block.data, words.data(), block.bytes); + return words; +} + +std::string Describe(std::array rgb) { + char text[96]; + std::snprintf(text, sizeof(text), "(%.9g, %.9g, %.9g)", rgb[0], rgb[1], rgb[2]); + return text; +} + +void TargetTests(AgcDriver::VulkanDevice& device, Block& block, std::uint32_t attrib3, ColorTileMode mode, const char* kind) { + const ColorTargetLayout layout(Width, Height, mode, 4); + Require(layout.Bytes() == block.bytes, "the test surface size disagrees with its layout"); + const auto color = DecodeTarget(block, attrib3); + auto before = Fill(block); + const float nan = std::numeric_limits::quiet_NaN(); + const float infinity = std::numeric_limits::infinity(); + const std::array, 10> cases{{ + {1.0f, 0.5f, 0.25f}, + {2.0f, 0.75f, 65024.0f}, + {1.0f + 1.9f / 64.0f, 1.0f + 0.9f / 64.0f, 1.0f + 1.9f / 32.0f}, + {65000.0f, 70000.0f, 1.0e30f}, + {nan, -1.0f, infinity}, + {-infinity, -0.0f, std::numeric_limits::denorm_min()}, + {std::ldexp(1.0f, -15), std::ldexp(3.0f, -20), std::ldexp(1.0f, -19)}, + {std::ldexp(1.0f, -21), std::ldexp(1.9f, -20), std::ldexp(1.9f, -19)}, + {std::numeric_limits::min(), 0.1f, 1.0f / 3.0f}, + {3.0f, 5.0f, 7.0f}, + }}; + for (const auto& rgb : cases) { + Draw(device, color, Float32Export, Float32Program(rgb)); + ExpectSurface(device, block, layout, before, Packed(rgb), std::string("a 32_ABGR export of ") + Describe(rgb) + " into a " + kind + " 11_11_10 float target"); + before = ReadBack(device, block); + } + const std::array halves{0.5f, 0.75f, 0.125f}; + Draw(device, color, Float16Export, Float16Program(halves)); + ExpectSurface(device, block, layout, before, Packed(halves), std::string("an FP16_ABGR export of ") + Describe(halves) + " into a " + kind + " 11_11_10 float target"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Block tiled(ColorTargetLayout(Width, Height, ColorTileMode::RenderTarget, 4).Bytes()); + Block linear(ColorTargetLayout(Width, Height, ColorTileMode::Linear, 4).Bytes()); + TargetTests(*device, tiled, TiledAttrib3, ColorTileMode::RenderTarget, "tiled"); + TargetTests(*device, linear, LinearAttrib3, ColorTileMode::Linear, "linear"); + std::puts("11_11_10 float color target tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedIntegerNegate.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedIntegerNegate.cpp new file mode 100644 index 0000000000..b170bc08d3 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedIntegerNegate.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 + +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{ + 0x34020084, 0x34060086, 0xe0381000, 0x80000401, 0xbf8c3f70, 0xcc07410a, 0x300208ff, 0x00007fff, + 0xcc0a410b, 0x58020b04, 0xcc03420c, 0x38020b04, 0xcc08430d, 0x78020b04, 0xcc0c420e, 0x58020b04, + 0xcc0d410f, 0x38020b04, 0xcc014210, 0x38020b04, 0xcc004511, 0x9c1a0b04, 0xcc094612, 0x7c1a0b04, + 0xcc044213, 0x58020b04, 0xcc064314, 0x58020b04, 0xcc054215, 0x78020b04, 0xcc024a16, 0x30020b04, + 0xcc0b5317, 0x68020b04, 0xcc02c218, 0x58020b04, 0xcc074919, 0x30020906, 0xe0781000, 0x80010a03, + 0xe0781010, 0x80010e03, 0xe0781020, 0x80011203, 0xe0781030, 0x80011603, 0xbf810000, +}; + +constexpr std::uint32_t Rows[32][4] = { + {0x1234021bu, 0xffff7fffu, 0x8001edccu, 0x00000000u}, + {0xc000021bu, 0x0002021bu, 0x000fedccu, 0x00000000u}, + {0xedcc0001u, 0x00ff0218u, 0x0002fffeu, 0x00000000u}, + {0x000f00ffu, 0x000f0218u, 0x80100218u, 0x00000000u}, + {0x000f8000u, 0x000f021bu, 0x00ff021bu, 0x00000000u}, + {0x7fff0218u, 0x4000000fu, 0x80018000u, 0x00000000u}, + {0xffff4000u, 0x00ff8000u, 0x00021234u, 0x00000000u}, + {0x7ffeffffu, 0x021bffffu, 0x8010c000u, 0x00000000u}, + {0x80107fffu, 0x80010010u, 0xedcc8001u, 0x00000000u}, + {0xc0000000u, 0x80008000u, 0x0218021bu, 0x00000000u}, + {0x00020001u, 0x00027fffu, 0x40000218u, 0x00000000u}, + {0x00ffc000u, 0xffff0002u, 0xfffefffeu, 0x00000000u}, + {0x00100000u, 0x00ff000fu, 0xfffe4000u, 0x00000000u}, + {0x021bfffeu, 0x00ff8001u, 0x00fffffeu, 0x00000000u}, + {0xedccedccu, 0x021b021bu, 0x7ffe8001u, 0x00000000u}, + {0x00007ffeu, 0x12340218u, 0xfffe0001u, 0x00000000u}, + {0xc0000218u, 0x0010ffffu, 0xffff8001u, 0x00000000u}, + {0x400000ffu, 0x7ffec000u, 0x80008000u, 0x00000000u}, + {0x000f4000u, 0xedcc0001u, 0x80008000u, 0x00000000u}, + {0x7fff0218u, 0x8010c000u, 0x80108010u, 0x00000000u}, + {0x80008000u, 0x80000218u, 0x00007ffeu, 0x00000000u}, + {0x021bffffu, 0x000fc000u, 0x7fffedccu, 0x00000000u}, + {0x02180002u, 0x000f8000u, 0x0001edccu, 0x00000000u}, + {0x021b4000u, 0x021b0001u, 0x02188001u, 0x00000000u}, + {0x7fff8000u, 0x40008010u, 0xffff1234u, 0x00000000u}, + {0x80100000u, 0x00010001u, 0x80000218u, 0x00000000u}, + {0x4000c000u, 0xffff021bu, 0x40008010u, 0x00000000u}, + {0x7fff4000u, 0x7ffe0002u, 0x000f00ffu, 0x00000000u}, + {0x000fedccu, 0x8000fffeu, 0x021b4000u, 0x00000000u}, + {0xedcc0001u, 0x8000021bu, 0xedcc8000u, 0x00000000u}, + {0x02180000u, 0x00ffedccu, 0x12344000u, 0x00000000u}, + {0xc000ffffu, 0x80010218u, 0x7fffc000u, 0x00000000u}, +}; + +enum class Kind { MaxI, MinI, MaxU, MinU, AddU, AddI, SubU, SubI, MulLo, MadI, MadU, Lshl, Ashr, Lshr }; + +constexpr std::int32_t Literal = -1; + +struct Case { + const char* name; + Kind kind; + std::array sources; + std::array opSel; + std::array opSelHi; + std::array neg; + std::array negHi; + bool clamp; +}; + +constexpr std::uint32_t LiteralValue = 0x00007fffu; + +constexpr std::array Cases{{ + {"v_pk_max_i16 v10, lit 0x7fff, v4 op_sel_hi:[0,1] neg_lo:[1,0] neg_hi:[1,0]", Kind::MaxI, {Literal, 0, 0}, {false, false, false}, {false, true, true}, {true, false, false}, {true, false, false}, false}, + {"v_pk_add_u16 v11, v4, v5 neg_lo:[0,1] neg_hi:[1,0]", Kind::AddU, {0, 1, 0}, {}, {true, true, true}, {false, true, false}, {true, false, false}, false}, + {"v_pk_sub_i16 v12, v4, v5 neg_lo:[1,0] neg_hi:[0,1]", Kind::SubI, {0, 1, 0}, {}, {true, true, true}, {true, false, false}, {false, true, false}, false}, + {"v_pk_min_i16 v13, v4, v5 neg_lo:[1,1] neg_hi:[1,1]", Kind::MinI, {0, 1, 0}, {}, {true, true, true}, {true, true, false}, {true, true, false}, false}, + {"v_pk_max_u16 v14, v4, v5 neg_lo:[0,1] neg_hi:[0,1]", Kind::MaxU, {0, 1, 0}, {}, {true, true, true}, {false, true, false}, {false, true, false}, false}, + {"v_pk_min_u16 v15, v4, v5 neg_lo:[1,0] neg_hi:[1,0]", Kind::MinU, {0, 1, 0}, {}, {true, true, true}, {true, false, false}, {true, false, false}, false}, + {"v_pk_mul_lo_u16 v16, v4, v5 neg_lo:[1,0] neg_hi:[0,1]", Kind::MulLo, {0, 1, 0}, {}, {true, true, true}, {true, false, false}, {false, true, false}, false}, + {"v_pk_mad_i16 v17, v4, v5, v6 neg_lo:[0,0,1] neg_hi:[1,0,1]", Kind::MadI, {0, 1, 2}, {}, {true, true, true}, {false, false, true}, {true, false, true}, false}, + {"v_pk_mad_u16 v18, v4, v5, v6 neg_lo:[1,1,0] neg_hi:[0,1,1]", Kind::MadU, {0, 1, 2}, {}, {true, true, true}, {true, true, false}, {false, true, true}, false}, + {"v_pk_lshlrev_b16 v19, v4, v5 neg_lo:[0,1] neg_hi:[0,1]", Kind::Lshl, {0, 1, 0}, {}, {true, true, true}, {false, true, false}, {false, true, false}, false}, + {"v_pk_ashrrev_i16 v20, v4, v5 neg_lo:[0,1] neg_hi:[1,1]", Kind::Ashr, {0, 1, 0}, {}, {true, true, true}, {false, true, false}, {true, true, false}, false}, + {"v_pk_lshrrev_b16 v21, v4, v5 neg_lo:[1,1] neg_hi:[0,1]", Kind::Lshr, {0, 1, 0}, {}, {true, true, true}, {true, true, false}, {false, true, false}, false}, + {"v_pk_add_i16 v22, v4, v5 op_sel:[1,0] op_sel_hi:[0,1] neg_lo:[1,0] neg_hi:[0,1]", Kind::AddI, {0, 1, 0}, {true, false, false}, {false, true, true}, {true, false, false}, {false, true, false}, false}, + {"v_pk_sub_u16 v23, v4, v5 op_sel:[0,1] op_sel_hi:[1,0] neg_lo:[1,1] neg_hi:[1,1]", Kind::SubU, {0, 1, 0}, {false, true, false}, {true, false, true}, {true, true, false}, {true, true, false}, false}, + {"v_pk_add_i16 v24, v4, v5 neg_lo:[0,1] neg_hi:[0,1] clamp", Kind::AddI, {0, 1, 0}, {}, {true, true, true}, {false, true, false}, {false, true, false}, true}, + {"v_pk_max_i16 v25, v6, v4 op_sel:[1,0] op_sel_hi:[0,1] neg_lo:[1,0] neg_hi:[1,0]", Kind::MaxI, {2, 0, 0}, {true, false, false}, {false, true, true}, {true, false, false}, {true, false, false}, false}, +}}; + +std::int32_t Signed(std::uint32_t half) { + return static_cast(static_cast(half)); +} + +std::uint32_t Source(const Case& c, const std::uint32_t* in, std::size_t i, bool highLane) { + const std::uint32_t word = c.sources[i] == Literal ? LiteralValue : in[c.sources[i]]; + const bool high = highLane ? c.opSelHi[i] : c.opSel[i]; + const bool negate = highLane ? c.negHi[i] : c.neg[i]; + return ((high ? word >> 16u : word) & 0xffffu) ^ (negate ? 0x8000u : 0u); +} + +std::uint32_t Lane(const Case& c, const std::uint32_t* in, bool highLane) { + const std::uint32_t a = Source(c, in, 0, highLane); + const std::uint32_t b = Source(c, in, 1, highLane); + const std::uint32_t s = Source(c, in, 2, highLane); + std::int64_t value = 0; + switch (c.kind) { + case Kind::MaxI: value = std::max(Signed(a), Signed(b)); break; + case Kind::MinI: value = std::min(Signed(a), Signed(b)); break; + case Kind::MaxU: value = std::max(a, b); break; + case Kind::MinU: value = std::min(a, b); break; + case Kind::AddU: value = a + b; break; + case Kind::AddI: value = static_cast(Signed(a)) + Signed(b); break; + case Kind::SubU: value = static_cast(a) - b; break; + case Kind::SubI: value = static_cast(Signed(a)) - Signed(b); break; + case Kind::MulLo: value = static_cast(a) * b; break; + case Kind::MadI: value = static_cast(Signed(a)) * Signed(b) + Signed(s); break; + case Kind::MadU: value = static_cast(a) * b + s; break; + case Kind::Lshl: value = static_cast(b) << (a & 15u); break; + case Kind::Ashr: value = Signed(b) >> (a & 15u); break; + case Kind::Lshr: value = b >> (a & 15u); break; + } + if (c.clamp) value = std::clamp(value, -32768, 32767); + return static_cast(value) & 0xffffu; +} + +std::uint32_t Reference(const Case& c, const std::uint32_t* in) { + return Lane(c, in, false) | (Lane(c, in, true) << 16u); +} + +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) { + 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::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 failures = 0; + std::string report; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t* in = &Input[tid * Inputs]; + const std::uint32_t* out = &Output[tid * Results]; + const auto clampLane = [&](std::uint32_t x) { return static_cast(std::max(-1, Signed(x))) & 0xffffu; }; + const std::uint32_t tsr = clampLane(in[0] & 0xffffu) | (clampLane(in[0] >> 16u) << 16u); + Require(Reference(Cases[0], in) == tsr, "packed integer negate: reference disagrees with max(-1, x)"); + for (std::uint32_t i = 0; i < Results; ++i) { + const std::uint32_t expected = Reference(Cases[i], in); + if (out[i] != expected && failures++ < 24u) { + report += std::string("packed integer negate: lane ") + std::to_string(tid) + " " + Cases[i].name + " is " + Hex(out[i]) + ", expected " + Hex(expected) + "\n"; + } + } + } + Require(failures == 0u, report + std::to_string(failures) + " mismatches"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device); + Check(); + std::puts("packed integer negate 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/PackedMipTailTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedMipTailTarget.cpp new file mode 100644 index 0000000000..a8a85b6211 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedMipTailTarget.cpp @@ -0,0 +1,257 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.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 + +namespace { + +using AgcDriver::Graphics::ColorTargetLayout; +using AgcDriver::Graphics::ColorTileMode; +using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::StorageTexture; +using AgcDriver::Graphics::TextureTileMode; +using AgcDriver::Graphics::TileMipLayout; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Side = 256; +constexpr std::uint32_t Levels = 9; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t BlockSide = 128; +constexpr std::size_t SurfaceBytes = BlockBytes + Side * Side * 4u + BlockBytes; +constexpr std::uint32_t Color32FloatInfo = (4u << 2u) | (7u << 8u); +constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; +constexpr std::uint32_t Float32Export = 9; +constexpr std::uint32_t Initial = 0x5a5a5a5au; + +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, Levels> PixelCodes{}; + +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: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, BlockBytes)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, SurfaceBytes, PAGE_READWRITE, SurfaceBytes); +#else + void* mapped = mmap(nullptr, SurfaceBytes + 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 + SurfaceBytes != begin + SurfaceBytes + BlockBytes) munmap(reinterpret_cast(aligned + SurfaceBytes), begin + BlockBytes - aligned); + block = reinterpret_cast(aligned); + if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, SurfaceBytes); + } +#endif + Require(block != nullptr, "packed mip tail target: cannot allocate the guest block"); + 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; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(std::uint64_t address, std::uint32_t level, const TileMipLayout& mip) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(address >> 8u); + cx[0x31b] = level << 26u; + cx[0x31c] = Color32FloatInfo; + cx[0x31d] = 0; + cx[0x3b0] = ((Levels - 1u) << 28u) | ((Side - 1u) << 14u) | (Side - 1u); + cx[0x3b8] = TiledAttrib3; + cx[0x390] = static_cast(address >> 40u); + const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == VK_FORMAT_R32_SFLOAT && color.tileMode == ColorTileMode::RenderTarget, "the COLOR_32 float view did not decode as an SW_64KB_R_X R32 target"); + Require(color.mip == level && color.mipCount == Levels && color.mipTail == mip.tail, "the color view did not keep its mip of the surface"); + Require(color.extent.width == mip.width && color.extent.height == mip.height && color.address == address + mip.tiledOffset, "the color view did not decode the extent and address of mip " + std::to_string(level)); + return color; +} + +void DrawLevel(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t level, const TileMipLayout& mip, std::uint32_t value) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + auto& pixelWords = PixelCodes[level]; + std::size_t at = 0; + for (std::uint32_t channel = 0; channel < 4; ++channel) { + pixelWords[at++] = 0x7e0002ffu | ((4u + channel) << 17u); + pixelWords[at++] = value; + } + pixelWords[at++] = 0xf800180fu; + pixelWords[at++] = 0x07060504u; + pixelWords[at++] = 0xbf810000u; + const std::span pixelCode(pixelWords.data(), at); + 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 color = DecodeTarget(surface, level, mip); + 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 = color.extent; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(mip.height), static_cast(mip.width), -static_cast(mip.height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, color.extent}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + 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, AgcDriver::Graphics::ExportMappings(state)), std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +void Expect(std::vector& expected, const TileMipLayout& mip, std::uint32_t value) { + const ColorTargetLayout block(BlockSide, BlockSide, ColorTileMode::RenderTarget, 4); + const ColorTargetLayout own(mip.width, mip.height, ColorTileMode::RenderTarget, 4); + for (std::uint32_t y = 0; y < mip.height; ++y) { + for (std::uint32_t x = 0; x < mip.width; ++x) { + const auto offset = mip.tail ? block.Offset(x + mip.tailX, y + mip.tailY) : mip.tiledOffset + own.Offset(x, y); + Require(expected.at(offset / 4u) == Initial, "the expected texels of two mips overlap at byte " + Hex(offset)); + expected[offset / 4u] = value; + } + } +} + +void Check(const std::uint8_t* texels, const std::vector& expected, const std::string& what) { + const auto* words = reinterpret_cast(texels); + for (std::size_t index = 0; index < expected.size(); ++index) { + Require(words[index] == expected[index], what + ": surface byte " + Hex(index * 4u) + " holds " + Hex(words[index]) + ", expected " + Hex(expected[index])); + } +} + +void Run(AgcDriver::VulkanDevice& device, std::uint8_t* texels) { + const auto mips = AgcDriver::Graphics::ComputeElementMipLayout(TextureTileMode::kR64KBX, 4, Side, Side, Levels); + Require(AgcDriver::Graphics::ComputeSurfaceSize(mips, 1) == SurfaceBytes, "the 256x256 R32 surface with 9 mips is not one tail block, mip 0 and mip 1"); + Require(!mips[1].tail && mips[2].tail && mips[3].tail && mips[2].width == 64 && mips[3].width == 32, "mips 2 and 3 of the 256x256 R32 surface are not its first packed tail mips"); + const auto surface = AddressOf(texels); + std::fill_n(reinterpret_cast(texels), SurfaceBytes / 4u, Initial); + AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + std::vector expected(SurfaceBytes / 4u, Initial); + const std::array, 3> draws{{{2u, 0.25f}, {3u, 0.75f}, {1u, 0.5f}}}; + for (const auto& [level, drawn] : draws) { + const auto value = std::bit_cast(drawn); + DrawLevel(device, surface, level, mips[level], value); + Require(StorageTexture::FindPending(surface, SurfaceBytes) == nullptr, "the draw into mip " + std::to_string(level) + " rendered into a resident image instead of the guest-memory fallback"); + Expect(expected, mips[level], value); + Check(texels, expected, "after the draw into mip " + std::to_string(level)); + } + AgcDriver::Graphics::ClearCachedTextures(device.Device()); +} + +} + +int main() { + try { +#ifdef _WIN32 + _putenv_s("APS5_NO_RESIDENT_TARGETS", "1"); +#else + setenv("APS5_NO_RESIDENT_TARGETS", "1", 1); +#endif + GuestBlock block; + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device, block.Data()); + std::puts("packed mip tail 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/PackedUnormColorTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnormColorTarget.cpp new file mode 100644 index 0000000000..31bf88c82d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnormColorTarget.cpp @@ -0,0 +1,326 @@ +#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 Unorm10_11_11Info = 0x8018u; +constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t Float32Export = 9; +constexpr std::uint32_t Float16Export = 4; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, 32> Programs{}; +std::size_t programCount = 0; + +std::uint32_t Unorm(float value, std::uint32_t bits) { + const auto maximum = (1u << bits) - 1u; + if (!(value > 0.0f)) return 0; + if (value >= 1.0f) return maximum; + return static_cast(std::nearbyint(static_cast(value) * maximum)); +} + +std::uint32_t Packed(std::array rgb) { + return Unorm(rgb[0], 11) | (Unorm(rgb[1], 11) << 11u) | (Unorm(rgb[2], 10) << 22u); +} + +std::uint32_t Half(float value) { + Require(value == 0.0f || (value >= 0.0625f && value <= 1.0f), "the test's half values are normal and at most 1"); + if (value == 0.0f) return 0; + const auto bits = std::bit_cast(value); + Require((bits & 0x1fffu) == 0, "the test's half values are exact"); + return ((((bits >> 23u) & 0xffu) - 112u) << 10u) | ((bits >> 13u) & 0x3ffu); +} + +std::span Program(const std::array& vgprs, bool compressed) { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + for (std::uint32_t vgpr = 0; vgpr < (compressed ? 2u : 4u); ++vgpr) { + code[at++] = 0x7e0002ffu | ((4u + vgpr) << 17u); + code[at++] = vgprs[vgpr]; + } + code[at++] = compressed ? 0xf8001c0fu : 0xf800180fu; + code[at++] = compressed ? 0x00000504u : 0x07060504u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +std::span Float32Program(std::array rgb) { + return Program({std::bit_cast(rgb[0]), std::bit_cast(rgb[1]), std::bit_cast(rgb[2]), std::bit_cast(1.0f)}, false); +} + +std::span Float16Program(std::array rgb) { + return Program({Half(rgb[0]) | (Half(rgb[1]) << 16u), Half(rgb[2]) | (Half(1.0f) << 16u), 0, 0}, true); +} + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (watched) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the color target"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate the color target"); + Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched"); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (watched) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + bool watched; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t attrib3) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Unorm10_11_11Info; + 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.packing == ShaderRecompiler::ColorExportPacking::Unorm10_11_11 && color.bytes == block.bytes, "the 10_11_11 unorm 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 10_11_11 unorm packing did not key its own specialization"); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, pixelResult.get(), PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +std::vector ReadBack(AgcDriver::VulkanDevice& device, const Block& block) { + AgcDriver::Graphics::StorageTexture::FlushPending(block.Address(), block.bytes, nullptr, "test read-back"); + device.WaitIdle(); + std::vector stored(block.bytes / 4u); + AgcDriver::GuestMemory::Read(block.Address(), std::as_writable_bytes(std::span(stored)), 1); + return stored; +} + +void ExpectSurface(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const std::vector& before, std::uint32_t word, const std::string& what) { + const auto stored = ReadBack(device, block); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto index = layout.Offset(x, y) / 4u; + const bool inside = x < ScissorWidth; + const auto expected = inside ? word : before[index]; + if (stored[index] == expected) continue; + char detail[160]; + std::snprintf(detail, sizeof(detail), ": pixel (%u, %u) %s the scissor holds 0x%08x, expected 0x%08x", x, y, inside ? "inside" : "outside", stored[index], expected); + throw std::runtime_error(what + detail); + } + } +} + +std::vector Fill(Block& block) { + std::vector words(block.bytes / 4u); + for (std::size_t index = 0; index < words.size(); ++index) words[index] = static_cast(index * 0x9e3779b9u); + std::memcpy(block.data, words.data(), block.bytes); + return words; +} + +std::string Describe(std::array rgb) { + char text[96]; + std::snprintf(text, sizeof(text), "(%.9g, %.9g, %.9g)", rgb[0], rgb[1], rgb[2]); + return text; +} + +void TargetTests(AgcDriver::VulkanDevice& device, Block& block, std::uint32_t attrib3, ColorTileMode mode, const char* kind) { + const ColorTargetLayout layout(Width, Height, mode, 4); + Require(layout.Bytes() == block.bytes, "the test surface size disagrees with its layout"); + const auto color = DecodeTarget(block, attrib3); + auto before = Fill(block); + const float nan = std::numeric_limits::quiet_NaN(); + const float infinity = std::numeric_limits::infinity(); + const std::array, 8> cases{{ + {0.0f, 1.0f, 0.5f}, + {0.5f, 0.25f, 1.0f / 3.0f}, + {nan, -1.0f, 2.0f}, + {infinity, -0.0f, std::numeric_limits::denorm_min()}, + {0.5f / 2047.0f, 1.5f / 2047.0f, 0.5f / 1023.0f}, + {2046.5f / 2047.0f, 1023.5f / 2047.0f, 1022.5f / 1023.0f}, + {-infinity, std::numeric_limits::min(), 0.999999940f}, + {0.1f, 0.7f, 0.9f}, + }}; + for (const auto& rgb : cases) { + Draw(device, color, Float32Export, Float32Program(rgb)); + ExpectSurface(device, block, layout, before, Packed(rgb), std::string("a 32_ABGR export of ") + Describe(rgb) + " into a " + kind + " 10_11_11 unorm target"); + before = ReadBack(device, block); + } + const std::array halves{0.5f, 0.75f, 0.125f}; + Draw(device, color, Float16Export, Float16Program(halves)); + ExpectSurface(device, block, layout, before, Packed(halves), std::string("an FP16_ABGR export of ") + Describe(halves) + " into a " + kind + " 10_11_11 unorm target"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Block tiled(ColorTargetLayout(Width, Height, ColorTileMode::RenderTarget, 4).Bytes()); + Block linear(ColorTargetLayout(Width, Height, ColorTileMode::Linear, 4).Bytes()); + TargetTests(*device, tiled, TiledAttrib3, ColorTileMode::RenderTarget, "tiled"); + TargetTests(*device, linear, LinearAttrib3, ColorTileMode::Linear, "linear"); + std::puts("10_11_11 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/PartiallyResidentSnapshot.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp new file mode 100644 index 0000000000..0afb33266f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp @@ -0,0 +1,441 @@ +#include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace AgcDriver::Graphics; + +constexpr std::uint32_t Side = 256; +constexpr std::uint32_t Format32UInt = 20; +constexpr std::uint32_t TileR64KBX = 0x1b; +constexpr std::uint32_t Type2D = 9; +constexpr std::size_t BlockBytes = 65536; +constexpr std::size_t SurfaceBytes = std::size_t{Side} * Side * 4u; +constexpr std::uint32_t LargeSide = 512; +constexpr std::size_t LargeSurfaceBytes = std::size_t{LargeSide} * LargeSide * 4u; +constexpr int Skipped = 77; + +class Device { +public: + Device() { +#ifdef _WIN32 + library = SDL_LoadObject("vulkan-1.dll"); +#else + library = SDL_LoadObject("libvulkan.so.1"); +#endif + Require(library != nullptr, "cannot load Vulkan"); + try { + instanceProc = reinterpret_cast(SDL_LoadFunction(library, "vkGetInstanceProcAddr")); + Require(instanceProc != nullptr, "missing Vulkan instance resolver"); + VkApplicationInfo application{VK_STRUCTURE_TYPE_APPLICATION_INFO}; + application.apiVersion = VK_API_VERSION_1_1; + VkInstanceCreateInfo info{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; + info.pApplicationInfo = &application; + Check(function("vkCreateInstance")(&info, nullptr, &instance), "vkCreateInstance"); + std::uint32_t count = 0; + const auto enumerate = function("vkEnumeratePhysicalDevices"); + Check(enumerate(instance, &count, nullptr), "vkEnumeratePhysicalDevices"); + Require(count != 0, "no Vulkan device"); + std::vector devices(count); + Check(enumerate(instance, &count, devices.data()), "vkEnumeratePhysicalDevices"); + context.physical = devices.front(); + const auto extensions = function("vkEnumerateDeviceExtensionProperties"); + Check(extensions(context.physical, nullptr, &count, nullptr), "vkEnumerateDeviceExtensionProperties"); + std::vector available(count); + Check(extensions(context.physical, nullptr, &count, available.data()), "vkEnumerateDeviceExtensionProperties"); + auto bytes = AgcDriver::QueryBdaByteFeatures(context.physical, function("vkGetPhysicalDeviceFeatures2"), available); + auto address = AgcDriver::QueryBdaFeatures(context.physical, function("vkGetPhysicalDeviceFeatures2"), available); + const auto queues = function("vkGetPhysicalDeviceQueueFamilyProperties"); + queues(context.physical, &count, nullptr); + std::vector families(count); + queues(context.physical, &count, families.data()); + std::uint32_t family = 0; + while (family < count && (families[family].queueFlags & VK_QUEUE_COMPUTE_BIT) == 0) ++family; + Require(family < count, "no Vulkan compute queue"); + const float priority = 1; + VkDeviceQueueCreateInfo queue{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; + queue.queueFamilyIndex = family; + queue.queueCount = 1; + queue.pQueuePriorities = &priority; + VkPhysicalDeviceFeatures enabled{}; + enabled.shaderInt64 = VK_TRUE; + address.pNext = &bytes; + const std::array extensionsEnabled{VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME, VK_KHR_8BIT_STORAGE_EXTENSION_NAME}; + VkDeviceCreateInfo device{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, &address}; + device.queueCreateInfoCount = 1; + device.pQueueCreateInfos = &queue; + device.enabledExtensionCount = static_cast(extensionsEnabled.size()); + device.ppEnabledExtensionNames = extensionsEnabled.data(); + device.pEnabledFeatures = &enabled; + Check(function("vkCreateDevice")(context.physical, &device, nullptr, &context.device), "vkCreateDevice"); + context.deviceProc = function("vkGetDeviceProcAddr"); + function("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory); + VkPhysicalDeviceProperties properties{}; + function("vkGetPhysicalDeviceProperties")(context.physical, &properties); + context.limits = properties.limits; + context.bufferDeviceAddress = true; + context.formatProperties = function("vkGetPhysicalDeviceFormatProperties"); + context.imageFormatProperties = function("vkGetPhysicalDeviceImageFormatProperties"); + context.Function("vkGetDeviceQueue")(context.device, family, 0, &context.queue); + VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + pool.queueFamilyIndex = family; + Check(context.Function("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool"); + } catch (...) { + release(); + throw; + } + } + + ~Device() { release(); } + Device(const Device&) = delete; + Device& operator=(const Device&) = delete; + const Context& GetContext() const { return context; } + +private: + template + TFunction function(const char* name) const { + const auto result = reinterpret_cast(instanceProc(instance, name)); + Require(result != nullptr, name); + return result; + } + + void release() noexcept { + if (context.device != VK_NULL_HANDLE) ClearCachedTextures(context.device); + if (context.pool != VK_NULL_HANDLE) context.Function("vkDestroyCommandPool")(context.device, context.pool, nullptr); + context.bufferPool.reset(); + if (context.device != VK_NULL_HANDLE) function("vkDestroyDevice")(context.device, nullptr); + if (instance != VK_NULL_HANDLE) function("vkDestroyInstance")(instance, nullptr); + if (library != nullptr) SDL_UnloadObject(library); + } + + void* library = nullptr; + PFN_vkGetInstanceProcAddr instanceProc = nullptr; + VkInstance instance = VK_NULL_HANDLE; + Context context{}; +}; + +class ReservedSurface { +public: + 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, size, true, true, true); + } + + ~ReservedSurface() { + GuestAllocations::Mutation().Remove(block); + Decommit(0, size); + GuestArena::GuestArenaRelease_nid_postfix(block, size); + } + + ReservedSurface(const ReservedSurface&) = delete; + ReservedSurface& operator=(const ReservedSurface&) = delete; + + void Commit(std::size_t offset, std::size_t bytes) { +#ifdef _WIN32 + GuestArena::GuestArenaCommit_nid_postfix(block + offset, bytes, PAGE_READWRITE, bytes); +#else + Require(mmap(block + offset, bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) != MAP_FAILED, "partially resident snapshot: cannot commit a page"); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block + offset, bytes); +#endif + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(reinterpret_cast(block + offset), bytes); + for (std::size_t page = 0; page < bytes; page += 4096) static_cast(static_cast(block)[offset + page]); + } + + void Decommit(std::size_t offset, std::size_t bytes) { +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block + offset, bytes); +#else + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block + offset, bytes); + Require(mmap(block + offset, bytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED, -1, 0) != MAP_FAILED, "partially resident snapshot: cannot decommit a page"); +#endif + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(reinterpret_cast(block + offset), bytes); + } + + std::uint8_t* Data() const { return block; } + std::uint64_t Address() const { return static_cast(reinterpret_cast(block)); } + +private: + std::size_t size = 0; + std::uint8_t* block = nullptr; +}; + +std::atomic surfaceBegin{0}; +std::atomic surfaceEnd{0}; +std::atomic surfaceReads{0}; + +void CountReads(std::uint64_t address, std::size_t bytes) { + if (address < surfaceEnd.load() && address + bytes > surfaceBegin.load()) surfaceReads.fetch_add(1); +} + +std::array Descriptor(std::uint64_t address, std::uint32_t side = Side) { + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((side - 1u) & 3u) << 30u), + ((side - 1u) >> 2u) | ((side - 1u) << 14u), + 0xfacu | (TileR64KBX << 20u) | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +void Run(const Context& context, ReservedSurface& surface) { + const auto address = surface.Address(); + const auto words = Descriptor(address); + Require(DescribeSurface(DecodeTextureResource(words)).guestBytes == SurfaceBytes, "the test descriptor does not describe the reserved surface"); + surface.Commit(0, BlockBytes); + std::memset(surface.Data(), 0x5a, BlockBytes); + surfaceBegin = address; + surfaceEnd = address + SurfaceBytes; + std::shared_ptr previous; + const auto sample = [&](const std::string& when, bool compared, bool same) { + AgcDriver::GuestMemory::BumpCollectEpoch(); + surfaceReads = 0; + auto texture = CachedSampledTexture(context, words); + Require(texture != nullptr && !texture->ViewsStorageImage(), "the partially resident surface was not sampled through a snapshot " + when); + const auto reads = surfaceReads.load(); + if (compared && reads == 0) throw std::runtime_error("the cached snapshot of the partially resident surface was not compared with guest memory " + when); + if (!compared && reads != 0) throw std::runtime_error("the cached snapshot of the partially resident surface was compared with guest memory " + when + " although nothing changed (" + std::to_string(reads) + " reads)"); + if (previous != nullptr && same != (texture == previous)) throw std::runtime_error(std::string("sampling the partially resident surface ") + when + (same ? " replaced the snapshot" : " kept the stale snapshot")); + previous = texture; + }; + AgcDriver::GuestMemory::SetFlushHook(&CountReads); + try { + sample("at its first use", true, false); + sample("again", false, true); + surface.Data()[0x100] = 0x11; + sample("after a CPU write to its committed part", true, false); + sample("after the re-read of the written part", false, true); + AgcDriver::GuestMemory::MarkWritten(address + 0x200, 4); + sample("after a driver store into its committed part", true, true); + sample("after the compare that followed the driver store", false, true); + surface.Commit(2 * BlockBytes, BlockBytes); + sample("after a page inside it was committed", true, true); + sample("after the compare that followed the commit", false, true); + std::memset(surface.Data() + 2 * BlockBytes, 0x33, BlockBytes); + sample("after a CPU write to the newly committed page", true, false); + sample("after the re-read of the newly committed page", false, true); + surface.Decommit(2 * BlockBytes, BlockBytes); + surface.Commit(2 * BlockBytes, BlockBytes); + sample("after a page inside it was decommitted and committed again", true, false); + sample("after the re-read of the recommitted page", false, true); + surface.Decommit(2 * BlockBytes, BlockBytes); + sample("after a page inside it was decommitted", true, true); + sample("after the compare that followed the decommit", false, true); + } catch (...) { + AgcDriver::GuestMemory::SetFlushHook(nullptr); + throw; + } + AgcDriver::GuestMemory::SetFlushHook(nullptr); +} + +void 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); + surface.Commit(0, BlockBytes); + std::memset(surface.Data(), 0x5a, BlockBytes); + Recorder recorder(context); + recorder.Activate(); + const auto lookup = [&] { + AgcDriver::GuestMemory::BumpCollectEpoch(); + return CachedSampledTexture(context, words); + }; + auto previous = lookup(); + Require(previous != nullptr && !previous->ViewsStorageImage(), "the pending-store surface did not produce a snapshot"); + Require(lookup() == previous, "the unchanged pending-store surface did not reuse its snapshot"); + for (const bool submitted : {false, true}) { + const auto landed = std::make_shared(false); + const auto value = submitted ? std::byte{0x99} : std::byte{0x77}; + std::vector original(reinterpret_cast(surface.Data()), reinterpret_cast(surface.Data()) + 0x1000); + recorder.NotePendingWrite(address, 0x1000); + recorder.OnComplete([address, landed, value, original = std::move(original)] { + const std::vector current(0x1000, value); + AgcDriver::GuestMemory::WriteChanged(address, current, original); + *landed = true; + }); + if (submitted) recorder.Submit(); + const auto next = lookup(); + Require(*landed, "the partially resident lookup returned before its pending copied store completed"); + Require(next != previous, "the partially resident lookup reused the snapshot from before its pending copied store"); + Require(surface.Data()[0] == std::to_integer(value), "the pending copied store did not update guest memory"); + Require(lookup() == next, "the partially resident lookup did not reuse the snapshot after its pending copied store"); + previous = next; + } + const auto unrelated = std::make_shared(false); + recorder.NotePendingWrite(address + SurfaceBytes, BlockBytes); + recorder.OnComplete([unrelated] { *unrelated = true; }); + Require(lookup() == previous, "an unrelated pending write replaced the partially resident snapshot"); + Require(!*unrelated, "a partially resident lookup waited for an unrelated pending write"); + recorder.Sync(); + Require(*unrelated, "the unrelated pending completion did not run when synchronized"); +} + +} + +int main(int argc, char** argv) { + try { + if (!AgcDriver::GuestMemory::WriteWatched()) { + std::puts("skipped, guest memory has no write watch"); + return Skipped; + } + std::optional device; + try { + device.emplace(); + } catch (const std::exception& error) { + if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw; + std::printf("skipped, no usable Vulkan device: %s\n", error.what()); + return Skipped; + } + { + auto context = device->GetContext(); + TextureDetiler detiler(context); + context.detiler = &detiler; + 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 (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(); + std::puts("partially resident snapshot tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/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/PatternBufferFill.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PatternBufferFill.cpp index 9e0872423b..f56fbe9c0a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PatternBufferFill.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PatternBufferFill.cpp @@ -167,6 +167,24 @@ void ExecuteTests() { } } +void PartialGroupTests() { + for (const auto wave : {32u, 64u}) { + Output.fill(0xcafebabeu); + Pattern[0] = 0x5a5a5a5au; + Control = {256, 0, 0, 0}; + auto words = Commands(UserData(Output.data(), 256), 128, wave, false, false); + words[2] = 0x00100040u; + words[3] = 0x00010001u; + words[4] = 0x00010001u; + words.back() |= 0x2u; + Submit(words); + for (std::size_t i = 0; i < Output.size(); ++i) { + const auto expected = i < 80 ? 0x5a5a5a5au : 0xcafebabeu; + Require(Output[i] == expected, "partial last thread group fill mismatch at " + std::to_string(i) + " wave " + std::to_string(wave)); + } + } +} + void Benchmark() { constexpr std::size_t bytes = 64u << 20u; std::vector storage(bytes / 4 + 4); @@ -196,7 +214,10 @@ int main(int argc, char** argv) { if (!device) return VulkanTestSkipped; } if (argc == 2 && std::string_view(argv[1]) == "--benchmark") Benchmark(); - else ExecuteTests(); + else { + ExecuteTests(); + PartialGroupTests(); + } AgcDriverShutdown_nid_postfix(); std::puts("pattern buffer fill tests passed"); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PendingExactSurface.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PendingExactSurface.cpp new file mode 100644 index 0000000000..9386584c9f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PendingExactSurface.cpp @@ -0,0 +1,130 @@ +#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 + +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 BlockBytes = 0x60000; +constexpr std::uint32_t Untouched = 0xdeadbeefu; + +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::uint32_t* texels, std::uint32_t maxMip) { + 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, + maxMip << 4u, + 0u, + 0u, + }; +} + +std::uint64_t GuestBytes(const std::uint32_t* texels, std::uint32_t maxMip) { + const auto words = TextureDescriptor(texels, maxMip); + return AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(words)).guestBytes; +} + +void Store(AgcDriver::VulkanDevice& device, const std::uint32_t* texels, std::uint32_t maxMip) { + std::vector userData(16, 0u); + const auto texture = TextureDescriptor(texels, maxMip); + 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(); +} + +std::string Describe(const std::shared_ptr& image) { + if (image == nullptr) return "no pending image"; + return "the pending image of " + std::to_string(image->GuestBytes()) + " bytes and " + std::to_string(image->Descriptor().mipCount) + " mips"; +} + +} + +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 surface = AddressOf(texels); + const auto chainBytes = GuestBytes(texels, 1u); + const auto singleBytes = GuestBytes(texels, 0u); + Require(singleBytes < chainBytes && chainBytes <= BlockBytes, "the 2-mip chain must cover more guest bytes than the 1-mip surface"); + + Store(*device, texels, 1u); + const auto chain = StorageTexture::FindPending(surface, chainBytes); + Require(chain != nullptr && chain->GuestBytes() == chainBytes, "the write through the 2-mip chain left " + Describe(chain) + " at the surface"); + const auto prefix = StorageTexture::FindPending(surface, singleBytes); + Require(prefix == chain, "a lookup of the chain's first mip found " + Describe(prefix) + " instead of the pending chain"); + + Store(*device, texels, 0u); + Require(StorageTexture::FindPending(surface, chainBytes) == chain, "the write through the 1-mip surface flushed the pending chain"); + const auto single = StorageTexture::FindPending(surface, singleBytes); + Require(single != nullptr && single != chain && single->GuestBytes() == singleBytes, "a lookup of the 1-mip surface found " + Describe(single) + " instead of the pending 1-mip image"); + + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + AgcDriver::Graphics::FlushCachedTextures(device->Device()); + } + device->WaitIdle(); + { + GuestAllocations::Mutation mutation; + mutation.Remove(texels); + } + std::puts("pending exact surface 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 new file mode 100644 index 0000000000..f2b56411c0 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp @@ -0,0 +1,279 @@ +#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/PipelineLibrary.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#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; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::byte Kept{0x40}; +alignas(256) std::array Pixels{}; +alignas(256) std::array DepthMemory{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; +alignas(256) constexpr std::array MaskedPixelCode{ + 0x7e040f00, 0x36060481, 0x7da40680, 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; +alignas(256) constexpr std::array FullPixelCode{ + 0x7e0e02f2, 0xf800180f, 0x07070707, 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, 12> DepthTriangles{{ + {-1.0f, -1.0f, 0.25f, 1.0f}, {0.0f, -1.0f, 0.25f, 1.0f}, {-1.0f, 1.0f, 0.25f, 1.0f}, + {-1.0f, 1.0f, 0.25f, 1.0f}, {0.0f, -1.0f, 0.25f, 1.0f}, {0.0f, 1.0f, 0.25f, 1.0f}, + {0.0f, -1.0f, 0.75f, 1.0f}, {1.0f, -1.0f, 0.75f, 1.0f}, {0.0f, 1.0f, 0.75f, 1.0f}, + {0.0f, 1.0f, 0.75f, 1.0f}, {1.0f, -1.0f, 0.75f, 1.0f}, {1.0f, 1.0f, 0.75f, 1.0f} +}}; + +struct PipelineTrace { + PFN_vkGetDeviceProcAddr resolve = nullptr; + PFN_vkCreateGraphicsPipelines create = nullptr; + PFN_vkCmdBindPipeline bind = nullptr; + PFN_vkCmdDraw draw = nullptr; + DeviceFunctions functions{}; + std::mutex mutex; + std::condition_variable released; + bool allowOptimization = false; + std::vector fast; + std::vector optimized; + VkCommandBuffer boundCommands = VK_NULL_HANDLE; + VkPipeline bound = VK_NULL_HANDLE; + std::vector drawn; + + void ReleaseOptimization() { + { + std::lock_guard lock(mutex); + allowOptimization = true; + } + released.notify_all(); + } +}; + +PipelineTrace* Trace = nullptr; + +VKAPI_ATTR VkResult VKAPI_CALL CreatePipelines(VkDevice device, VkPipelineCache cache, std::uint32_t count, const VkGraphicsPipelineCreateInfo* infos, const VkAllocationCallbacks* allocator, VkPipeline* pipelines) { + const bool optimizing = std::any_of(infos, infos + count, [](const auto& info) { return (info.flags & VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT) != 0; }); + if (optimizing) { + std::unique_lock lock(Trace->mutex); + Trace->released.wait(lock, [] { return Trace->allowOptimization; }); + } + const auto result = Trace->create(device, cache, count, infos, allocator, pipelines); + std::lock_guard lock(Trace->mutex); + for (std::uint32_t index = 0; index < count; ++index) { + if (pipelines[index] == VK_NULL_HANDLE || (infos[index].flags & VK_PIPELINE_CREATE_LIBRARY_BIT_KHR) != 0) continue; + auto& handles = (infos[index].flags & VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT) != 0 ? Trace->optimized : Trace->fast; + handles.push_back(pipelines[index]); + } + return result; +} + +VKAPI_ATTR void VKAPI_CALL BindPipeline(VkCommandBuffer commands, VkPipelineBindPoint point, VkPipeline pipeline) { + if (point == VK_PIPELINE_BIND_POINT_GRAPHICS) { + Trace->boundCommands = commands; + Trace->bound = pipeline; + } + Trace->bind(commands, point, pipeline); +} + +VKAPI_ATTR void VKAPI_CALL DrawCommands(VkCommandBuffer commands, std::uint32_t vertices, std::uint32_t instances, std::uint32_t firstVertex, std::uint32_t firstInstance) { + Trace->drawn.push_back(Trace->boundCommands == commands ? Trace->bound : VK_NULL_HANDLE); + Trace->draw(commands, vertices, instances, firstVertex, firstInstance); +} + +VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL Resolve(VkDevice device, const char* name) { + if (std::strcmp(name, "vkCreateGraphicsPipelines") == 0) return reinterpret_cast(CreatePipelines); + if (std::strcmp(name, "vkCmdBindPipeline") == 0) return reinterpret_cast(BindPipeline); + if (std::strcmp(name, "vkCmdDraw") == 0) return reinterpret_cast(DrawCommands); + return Trace->resolve(device, name); +} + +struct OptimizationDrain { + VkDevice device; + ~OptimizationDrain() { + Trace->ReleaseOptimization(); + WaitForOptimizedPipelines(device); + } +}; + +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 CheckPixels(bool masked, std::string_view phase) { + for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { + const bool drawn = masked ? (pixel % Width) % 2u == 0u : pixel % Width < Width / 2u; + const auto expected = drawn ? std::byte{255} : Kept; + for (std::uint32_t channel = 0; channel < 4; ++channel) { + const auto actual = Pixels[pixel * 4u + channel]; + Require(actual == expected, std::string(phase) + ": pixel " + std::to_string(pixel) + " channel " + std::to_string(channel) + " is " + std::to_string(static_cast(actual)) + ", expected " + std::to_string(static_cast(expected))); + } + } +} + +void CheckDrawn(PipelineTrace& trace, bool optimized) { + std::lock_guard lock(trace.mutex); + const auto& expected = optimized ? trace.optimized : trace.fast; + Require(trace.drawn.size() == 1, "pipeline readback did not observe exactly one real draw"); + Require(std::find(expected.begin(), expected.end(), trace.drawn.front()) != expected.end(), optimized ? "readback draw did not bind an optimized pipeline" : "readback draw did not bind a fast-linked pipeline"); +} + +void Run(AgcDriver::VulkanDevice& device, Context context, PipelineTrace& trace, std::uint32_t waveSize, VkFormat depthFormat, bool optimize) { + const bool masked = depthFormat == VK_FORMAT_UNDEFINED; + const auto target = device.Target(); + const auto vertices = masked ? std::span>(Triangle) : std::span>(DepthTriangles); + const auto vertexBuffer = BufferDescriptor(vertices.data(), 16u, static_cast(vertices.size())); + const std::vector vertexUserData(vertexBuffer.begin(), vertexBuffer.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, {}}, + {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 pixelCode = masked ? std::span(MaskedPixelCode) : std::span(FullPixelCode); + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.wave32 = waveSize == 32u; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + 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)} + }}; + State state{}; + state.stages = {ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + if (!masked) { + state.depth = DepthTarget{reinterpret_cast(DepthMemory.data()), 0, {Width, Height}, depthFormat, 0.5f, 0}; + state.depthTest = true; + state.depthCompare = VK_COMPARE_OP_LESS; + } + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(vertices.size()), 0, 1, 0, false}; + const auto render = [&](bool optimized, std::string_view phase) { + Pixels.fill(Kept); + trace.drawn.clear(); + trace.bound = VK_NULL_HANDLE; + trace.boundCommands = VK_NULL_HANDLE; + AgcDriver::Graphics::Draw(context, state, draw, shaders); + device.WaitIdle(); + CheckDrawn(trace, optimized); + CheckPixels(masked, phase); + }; + render(false, "fast link"); + trace.ReleaseOptimization(); + WaitForOptimizedPipelines(context.device); + Require(PipelineLibraryCountersOf(context.device).linked == 1, "the readback pipeline was not linked from graphics libraries"); + Require(trace.optimized.size() == (optimize ? 1u : 0u), "the optimized pipeline creation count differs from the requested mode"); + render(optimize, "completed link"); + std::printf("pipeline readback passed: fast draws %u, optimized draws %u, optimized creations %zu\n", optimize ? 1u : 2u, optimize ? 1u : 0u, trace.optimized.size()); +} + +} + +int main(int argc, char** argv) { + try { + std::string_view mode = "d32"; + bool force = false; + for (int index = 1; index < argc; ++index) { + const std::string_view argument = argv[index]; + if (argument == "--force-lto") force = true; + else { + Require(argument == "color32" || argument == "color64" || argument == "d32" || argument == "d16", "unknown pipeline readback case"); + mode = argument; + } + } + PipelineTrace trace; + Trace = &trace; + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); + if (!context.graphicsPipelineLibrary) { + std::puts("skipped, graphics pipeline libraries are unavailable or disabled"); + return VulkanTestSkipped; + } + const bool masked = mode == "color32" || mode == "color64"; + if (masked && device->Target().subgroupSize < 32u) { + std::puts("skipped, the masked pixel shader requires at least 32 subgroup lanes"); + return VulkanTestSkipped; + } + if (force) context.pipelineLinkTimeOptimization = true; + const bool optimize = context.pipelineLinkTimeOptimization && std::getenv("APS5_NO_PIPELINE_LTO") == nullptr; + std::printf("pipeline readback: %s, %.*s, LTO %s%s\n", device->DeviceName().c_str(), static_cast(mode.size()), mode.data(), optimize ? "enabled" : "disabled", force ? " (explicit test override)" : " (production policy)"); + trace.resolve = context.deviceProc; + trace.create = context.Function("vkCreateGraphicsPipelines"); + trace.bind = context.Function("vkCmdBindPipeline"); + trace.draw = context.Function("vkCmdDraw"); + trace.functions = context.functions != nullptr ? *context.functions : DeviceFunctions{}; + trace.functions.cmdBindPipeline = BindPipeline; + trace.functions.cmdDraw = DrawCommands; + context.functions = &trace.functions; + context.deviceProc = Resolve; + 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'; + return 1; + } +} 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..1c13bc169d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedFill.cpp @@ -0,0 +1,248 @@ +#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 + +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 { + std::atomic release{false}; + std::uint32_t* target = nullptr; + ~Blocker() { release = true; } + void Enqueue(std::vector writes = {}) { + DrawPipeline::Queue0().Enqueue([this] { + while (!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()); + }, std::move(writes)); + } +}; + +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 = true; +} + +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(); + 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(); + 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(); + 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 = true; + Finish(); + 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 = true; + 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(); + 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..437a8ecaa2 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedIndirectDraw.cpp @@ -0,0 +1,259 @@ +#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 + +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 { + std::atomic release{false}; + void Enqueue() { + DrawPipeline::Queue0().Enqueue([this] { + while (!release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + std::lock_guard gpuLock(AgcDriver::GuestMemory::GpuMutex()); + }, {}); + } +}; + +void SubmitBehind(Blocker& blocker, std::vector& commands) { + std::unique_lock gpuLock(AgcDriver::GuestMemory::GpuMutex()); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + 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); + 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 = true; + 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 = true; + Finish(); + 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; + SubmitBehind(blocker, commands); + const auto start = std::chrono::steady_clock::now(); + while (DrawPipeline::Queue0().Outstanding() < 2 && std::chrono::steady_clock::now() - start < std::chrono::seconds(10)) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + blocker.release = true; + Finish(); + 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/Pixel32ArExport.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Pixel32ArExport.cpp new file mode 100644 index 0000000000..daaad752c8 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Pixel32ArExport.cpp @@ -0,0 +1,299 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t ElementBytes = 4; +constexpr std::size_t RowBytes = std::size_t{Width} * ElementBytes; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Float32Info = 0x0710u; +constexpr std::uint32_t Float32AlternateReversedInfo = 0x1f10u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t Export32Ar = 3; +constexpr float Destination = 0.75f; + +struct Row { + std::uint32_t enables; + std::array sources; +}; + +constexpr std::array Rows{{ + {0x3u, {0.5f, 0.25f, 2.0f, 0.0625f}}, + {0xbu, {0.5f, 0.25f, 2.0f, 0.0625f}}, + {0xfu, {0.125f, 0.5f, 2.0f, 0.0625f}}, + {0x3u, {0.25f, 0.75f, 2.0f, 0.0625f}}, +}}; +constexpr std::uint32_t Height = Rows.size() + 1; +constexpr std::size_t TargetBytes = RowBytes * Height; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, Rows.size()> Programs{}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +std::span Program(std::size_t index) { + auto& code = Programs[index]; + const auto& row = Rows[index]; + std::size_t at = 0; + for (std::uint32_t channel = 0; channel < 4u; ++channel) { + code[at++] = 0x7e0002ffu | ((4u + channel) << 17u); + code[at++] = std::bit_cast(row.sources[channel]); + } + code[at++] = 0xf8001800u | row.enables; + code[at++] = 0x07060504u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (watched) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the color target"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate the color target"); + Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched"); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (watched) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + bool watched; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t info) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = info; + cx[0x31d] = 0; + cx[0x323] = 0; + cx[0x324] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = LinearAttrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + return AgcDriver::Graphics::DecodeColorBuffer(cx, 0); +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, const VkPipelineColorBlendAttachmentState& blend, std::uint32_t waveSize, std::size_t row) { + const auto target = device.Target(); + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast(Triangle.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, static_cast(row)}, {Width, 1}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + + const auto pixelCode = Program(row); + const AgcDriver::Registers context{{0x1b3, 0x300u}, {0x1b4, 0x300u}, {0x1b6, waveSize == 32u ? 0x8000u : 0u}, {0x1c5, Export32Ar}, {0x203, 0u}}; + const auto pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); + Require(pixel.targetOutputMode[0] == Export32Ar && pixel.wave32 == (waveSize == 32u), "the pixel registers did not decode as a 32_AR MRT0 export"); + const std::vector pixelUserData(8, 0u); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +std::vector ReadBack(AgcDriver::VulkanDevice& device, std::uint64_t address, std::size_t bytes) { + AgcDriver::Graphics::StorageTexture::FlushPending(address, bytes, nullptr, "test read-back"); + device.WaitIdle(); + std::vector stored(bytes / sizeof(std::uint32_t)); + AgcDriver::GuestMemory::Read(address, std::as_writable_bytes(std::span(stored)), 1); + return stored; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::string Describe(const Row& row) { + char text[96]; + std::snprintf(text, sizeof(text), "EN 0x%x of (%g, %g, %g, %g)", row.enables, row.sources[0], row.sources[1], row.sources[2], row.sources[3]); + return text; +} + +struct TargetCase { + const char* name; + std::uint32_t info; + std::uint8_t mapping; + bool blended; +}; + +void ExportTests(AgcDriver::VulkanDevice& device, const TargetCase& test, std::uint32_t waveSize) { + Block block(BlockBytes); + std::vector filled(BlockBytes / sizeof(std::uint32_t), std::bit_cast(Destination)); + std::memcpy(block.data, filled.data(), BlockBytes); + const auto color = DecodeTarget(block, test.info); + Require(color.format == VK_FORMAT_R32_SFLOAT && color.componentMapping == test.mapping && color.elementBytes == ElementBytes, std::string("the ") + test.name + " target did not decode as R32_SFLOAT with component mapping " + Hex(test.mapping)); + Require(AgcDriver::Graphics::ColorTargetLayout(Width, Height, AgcDriver::Graphics::ColorTileMode::Linear, ElementBytes).Bytes() == TargetBytes, "the linear COLOR_32 target is padded"); + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = 0xf; + if (test.blended) { + blend.blendEnable = VK_TRUE; + blend.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; + blend.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + blend.colorBlendOp = VK_BLEND_OP_ADD; + blend.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; + blend.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; + blend.alphaBlendOp = VK_BLEND_OP_ADD; + } + for (std::size_t row = 0; row < Rows.size(); ++row) Draw(device, color, blend, waveSize, row); + const auto stored = ReadBack(device, block.Address(), TargetBytes); + for (std::size_t row = 0; row < Height; ++row) { + float expected = Destination; + if (row < Rows.size()) { + const auto red = Rows[row].sources[0]; + const auto alpha = Rows[row].sources[1]; + expected = test.blended ? red * alpha + Destination * (1.0f - alpha) : alpha; + } + for (std::uint32_t x = 0; x < Width; ++x) { + const auto actual = stored[row * Width + x]; + if (actual == std::bit_cast(expected)) continue; + const auto source = row < Rows.size() ? "the 32_AR export with " + Describe(Rows[row]) : std::string("a row outside the drawn rows"); + throw std::runtime_error("wave" + std::to_string(waveSize) + " " + source + " into the " + test.name + " target stored " + Hex(actual) + " at pixel (" + std::to_string(x) + ", " + std::to_string(row) + "), expected " + Hex(std::bit_cast(expected))); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + constexpr std::array targets{{ + {"COLOR_32 FLOAT ALT_REV (alpha only)", Float32AlternateReversedInfo, 0xe7u, false}, + {"COLOR_32 FLOAT STD blended with SRC_ALPHA and ONE_MINUS_SRC_ALPHA", Float32Info, 0xe4u, true}, + }}; + for (const std::uint32_t waveSize : {32u, 64u}) { + for (const auto& test : targets) ExportTests(*device, test, waveSize); + } + std::puts("32_AR export tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelAncillaryWord.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelAncillaryWord.cpp new file mode 100644 index 0000000000..559a514ed7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelAncillaryWord.cpp @@ -0,0 +1,306 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Rgba32FloatInfo = 0x8738u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t Float32AbgrExport = 9; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, 5> Programs{}; +std::size_t programCount = 0; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +std::span Program(std::uint32_t ancillaryVgpr, bool wholeWord) { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + if (wholeWord) { + code[at++] = 0x36000000u | (20u << 17u) | (ancillaryVgpr << 9u) | 0xffu; + code[at++] = 0x0000ffffu; + code[at++] = 0x2c000000u | (21u << 17u) | (ancillaryVgpr << 9u) | 0x90u; + } else { + code[at++] = 0xd5480000u | 20u; + code[at++] = (0x84u << 18u) | (0x88u << 9u) | (256u + ancillaryVgpr); + code[at++] = 0x7e000280u | (21u << 17u); + } + code[at++] = 0x7e000c00u | (20u << 17u) | (256u + 20u); + code[at++] = 0x7e000c00u | (21u << 17u) | (256u + 21u); + code[at++] = 0x7e000280u | (22u << 17u); + code[at++] = 0x7e0002f2u | (23u << 17u); + code[at++] = 0xf800180fu; + code[at++] = 0x17161514u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (watched) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the color target"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate the color target"); + Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched"); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (watched) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + bool watched; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t info) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = info; + cx[0x31d] = 0; + cx[0x323] = 0; + cx[0x324] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = LinearAttrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + color.exportIndex = 0; + return color; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::uint32_t inputAddr, ShaderRecompiler::RasterPrimitive primitive, std::span pixelCode) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast(Triangle.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = inputAddr; + pixel.ancillary = true; + pixel.rasterPrimitive = primitive; + pixel.targetOutputMode.at(0) = Float32AbgrExport; + pixel.targetExportMapping.fill(0xe4u); + const std::vector pixelUserData(8, 0u); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + VkPipelineColorBlendAttachmentState blend{}; + blend.blendEnable = VK_FALSE; + blend.colorWriteMask = 0xf; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +std::vector ReadBack(AgcDriver::VulkanDevice& device, std::uint64_t address, std::size_t bytes) { + AgcDriver::Graphics::StorageTexture::FlushPending(address, bytes, nullptr, "test read-back"); + device.WaitIdle(); + std::vector stored(bytes); + AgcDriver::GuestMemory::Read(address, std::as_writable_bytes(std::span(stored)), 1); + return stored; +} + +void Expect(AgcDriver::VulkanDevice& device, std::uint32_t inputAddr, std::uint32_t ancillaryVgpr, ShaderRecompiler::RasterPrimitive primitive, const std::string& what) { + constexpr std::size_t bytes = std::size_t{Width} * Height * 16u; + Require(AgcDriver::Graphics::ColorTargetLayout(Width, Height, AgcDriver::Graphics::ColorTileMode::Linear, 16).Bytes() == bytes, what + ": the linear target is padded"); + Block block(bytes); + std::fill(block.data, block.data + bytes, std::uint8_t{0xff}); + const auto color = DecodeTarget(block, Rgba32FloatInfo); + Require(color.format == VK_FORMAT_R32G32B32A32_SFLOAT && color.elementBytes == 16 && color.bytes == bytes, what + ": the target decoded to the wrong format"); + Draw(device, color, inputAddr, primitive, Program(ancillaryVgpr, true)); + const auto stored = ReadBack(device, block.Address(), bytes); + const auto word = static_cast(primitive); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + std::array texel{}; + std::memcpy(texel.data(), stored.data() + (static_cast(y) * Width + x) * 16u, sizeof(texel)); + const std::array expected{static_cast(word & 0xffffu), static_cast(word >> 16u), 0.0f, 1.0f}; + Require(texel == expected, what + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") holds (" + std::to_string(texel[0]) + ", " + std::to_string(texel[1]) + ", " + std::to_string(texel[2]) + ", " + std::to_string(texel[3]) + ")"); + } + } +} + +std::uint64_t PixelModule(const ShaderRecompiler::SpirvTarget& target, std::span code, ShaderRecompiler::RasterPrimitive primitive) { + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary); + pixel.ancillary = true; + pixel.rasterPrimitive = primitive; + pixel.targetOutputMode.at(0) = Float32AbgrExport; + pixel.targetExportMapping.fill(0xe4u); + const std::vector userData(8, 0u); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Fragment, reinterpret_cast(code.data()), code, 0, {}}, + {64, 0, userData, std::nullopt, pixel, std::nullopt, memory}, + target, + PixelPushLayout(0, target) + }; + return ShaderRecompiler::Recompile(request).PipelineVariantId(); +} + +void ExpectModules(const ShaderRecompiler::SpirvTarget& target, bool wholeWord, const std::string& what) { + using ShaderRecompiler::RasterPrimitive; + const auto code = Program(0u, wholeWord); + const auto triangle = PixelModule(target, code, RasterPrimitive::Triangle); + Require(PixelModule(target, code, RasterPrimitive::Triangle) == triangle, what + ": the same primitive class built a second module"); + for (const auto primitive : {RasterPrimitive::Line, RasterPrimitive::Rectangle}) { + Require((PixelModule(target, code, primitive) != triangle) == wholeWord, what + (wholeWord ? ": a line or rectangle draw reused the triangle module" : ": a line or rectangle draw built its own module")); + } +} + +void ExpectPrimitive(VkPrimitiveTopology topology, bool rectList, bool tessellated, ShaderRecompiler::RasterPrimitive expected, const std::string& what) { + AgcDriver::Graphics::State state{}; + state.topology = topology; + state.rectList = rectList; + if (tessellated) state.stages.tessellation = ShaderRecompiler::TessellationConfiguration{}; + Require(AgcDriver::Graphics::RasterPrimitiveOf(state) == expected, what + ": wrong primitive class"); +} + +} + +int main() { + try { + using ShaderRecompiler::PixelInput; + using ShaderRecompiler::PixelInputBit; + using ShaderRecompiler::RasterPrimitive; + ExpectPrimitive(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, false, false, RasterPrimitive::Triangle, "triangle strip"); + ExpectPrimitive(VK_PRIMITIVE_TOPOLOGY_LINE_LIST, false, false, RasterPrimitive::Line, "line list"); + ExpectPrimitive(VK_PRIMITIVE_TOPOLOGY_POINT_LIST, false, false, RasterPrimitive::Point, "point list"); + ExpectPrimitive(VK_PRIMITIVE_TOPOLOGY_PATCH_LIST, true, false, RasterPrimitive::Rectangle, "rect list"); + ExpectPrimitive(VK_PRIMITIVE_TOPOLOGY_PATCH_LIST, false, true, RasterPrimitive::Triangle, "tessellated patches"); + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Expect(*device, PixelInputBit(PixelInput::Ancillary), 0u, RasterPrimitive::Triangle, "ANCILLARY alone, triangle"); + Expect(*device, PixelInputBit(PixelInput::PerspectiveCenter) | PixelInputBit(PixelInput::Ancillary), 2u, RasterPrimitive::Rectangle, "ANCILLARY after the perspective center, rectangle"); + Expect(*device, PixelInputBit(PixelInput::Ancillary), 0u, RasterPrimitive::Line, "ANCILLARY alone, line"); + ExpectModules(device->Target(), true, "whole word"); + ExpectModules(device->Target(), false, "sample field only"); + std::puts("pixel ancillary word tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp new file mode 100644 index 0000000000..9978674554 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp @@ -0,0 +1,300 @@ +#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::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 VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf800020f, 0x03020100, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array BarycentricPixelCode{ + 0xf800180f, 0x01000100, 0xbf810000, +}; + +alignas(256) constexpr std::array VertexValuePixelCode{ + 0xc80a0000, 0xc80e0001, 0xc8120002, 0xf800180f, 0x04040302, 0xbf810000, +}; + +alignas(256) constexpr std::array InterpolatedPixelCode{ + 0xc8080000, 0xc8090001, 0xf800180f, 0x02020202, 0xbf810000, +}; + +struct Vertex { + float x; + float y; + float w; +}; + +constexpr std::array Corners{{{-0.8f, -0.8f, 1.0f}, {0.9f, -0.6f, 2.5f}, {-0.5f, 0.85f, 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, 0x01016facu}; +} + +enum class Emulation { + Geometry, + Tessellation, +}; + +ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, Emulation emulation, std::span pixelCode, std::uint32_t inputs, VkCullModeFlags cullMode = VK_CULL_MODE_NONE) { + Pixels.fill(Unwritten); + auto target = device.Target(); + target.fragmentShaderBarycentricEnabled = false; + 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, {}}, + {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.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(vertexPush, target) + }; + fragment.useCache = false; + auto pixelResult = ShaderRecompiler::Recompile(fragment); + std::vector shaders{{ShaderStage::Vertex, &vertexResult, 0}}; + ShaderRecompiler::RecompileResult geometry; + ShaderRecompiler::BarycentricTessellationShaders patches; + if (pixelResult.barycentricEmulation.active && emulation == Emulation::Geometry) { + geometry = ShaderRecompiler::BuildBarycentricGeometryShader(vertexResult, pixelResult, target, device.GeometryLimits()); + shaders.push_back({ShaderStage::Geometry, &geometry, 0}); + } else if (pixelResult.barycentricEmulation.active) { + patches = ShaderRecompiler::BuildBarycentricTessellationShaders(vertexResult, pixelResult, target, device.MaxFragmentInputComponents()); + shaders.push_back({ShaderStage::TessellationControl, &patches.control, 0}); + shaders.push_back({ShaderStage::TessellationEvaluation, &patches.evaluation, 0}); + } + shaders.push_back({ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)}); + + 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_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 = cullMode; + 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(positions.size()), 0, 1, 0, false}; + 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; +} + +Expected BarycentricsUnsnapped(std::uint32_t x, std::uint32_t y) { + return Barycentrics(x, y, false); +} + +Expected BarycentricsSnapped(std::uint32_t x, std::uint32_t y) { + return Barycentrics(x, y, true); +} + +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 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 = BarycentricsUnsnapped(x, y); + if (!expected.inside) continue; + const auto snapped = BarycentricsSnapped(x, y); + 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; +} + +void CheckVertexValues() { + const auto positions = ClipPositions(); + const auto p0 = positions[0][0]; + const std::array expected{positions[1][0] - p0, positions[2][0] - p0, p0}; + std::uint32_t checked = 0; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + if (!BarycentricsUnsnapped(x, y).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"); +} + +void CheckInterpolated() { + 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); + float reference = 0.0f; + float snappedReference = 0.0f; + for (std::size_t i = 0; i < 3; ++i) { + reference += expected.perspective[i] * positions[i][0]; + snappedReference += snapped.perspective[i] * positions[i][0]; + } + for (std::uint32_t channel = 0; channel < 4; ++channel) { + Require(MatchesEither(Pixel(x, y, channel), reference, snappedReference), "v_interp_p1/p2: 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) + " or " + std::to_string(snappedReference)); + } + ++checked; + } + } + Require(checked > 200u, "v_interp_p1/p2: too few interior pixels were checked"); +} + +bool Drawn() { + return std::any_of(Pixels.begin(), Pixels.end(), [](float value) { return value != Unwritten; }); +} + +void CheckWinding(AgcDriver::VulkanDevice& device, Emulation emulation, const std::string& name) { + const auto perspective = PixelInputBit(PixelInput::PerspectiveCenter); + for (const auto cullMode : {VK_CULL_MODE_BACK_BIT, VK_CULL_MODE_FRONT_BIT}) { + Draw(device, emulation, InterpolatedPixelCode, perspective, cullMode); + const bool host = Drawn(); + Draw(device, emulation, BarycentricPixelCode, perspective, cullMode); + Require(Drawn() == host, name + ": culling the " + (cullMode == VK_CULL_MODE_BACK_BIT ? "back" : "front") + " face does not match the draw without emulation"); + } +} + +void RunEmulation(AgcDriver::VulkanDevice& device, Emulation emulation) { + const std::string name = emulation == Emulation::Geometry ? "geometry stage" : "tessellation stages"; + const auto perspective = PixelInputBit(PixelInput::PerspectiveCenter); + const auto linear = PixelInputBit(PixelInput::LinearCenter); + auto result = Draw(device, emulation, BarycentricPixelCode, perspective); + Require(result.barycentricEmulation.active && result.barycentricEmulation.smooth && !result.barycentricEmulation.linear, "reading PERSP_CENTER I/J did not emulate perspective barycentrics"); + const auto smooth = CheckBarycentrics(name + " PERSP_CENTER I/J", true); + result = Draw(device, emulation, BarycentricPixelCode, linear); + Require(result.barycentricEmulation.active && !result.barycentricEmulation.smooth && result.barycentricEmulation.linear, "reading LINEAR_CENTER I/J did not emulate linear barycentrics"); + const auto noPerspective = CheckBarycentrics(name + " LINEAR_CENTER I/J", false); + result = Draw(device, emulation, VertexValuePixelCode, perspective); + Require(result.barycentricEmulation.active && result.fragmentParameters.size() == 1 && result.fragmentParameters[0].perVertex, "v_interp_mov of P10 and P20 did not emulate per-vertex parameters"); + CheckVertexValues(); + result = Draw(device, emulation, InterpolatedPixelCode, perspective); + Require(!result.barycentricEmulation.active, "plain v_interp_p1/p2 needed a generated stage"); + CheckInterpolated(); + CheckWinding(device, emulation, name); + std::printf("barycentric emulation through the %s passed: %u perspective and %u linear I/J pixels, raw vertex values, host interpolation and face culling\n", name.c_str(), smooth, noPerspective); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + 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; + } + 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'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelFixedPointPosition.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelFixedPointPosition.cpp new file mode 100644 index 0000000000..60aa9b05e5 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelFixedPointPosition.cpp @@ -0,0 +1,254 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Rgba32FloatInfo = 0x8738u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t Float32AbgrExport = 9; +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); +} + +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 fixedVgpr, std::uint32_t stippleVgpr, 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(fixedVgpr, stippleVgpr)); + 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{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]) + ")"); + } + } +} + +} + +int main() { + try { + using ShaderRecompiler::PixelInput; + using ShaderRecompiler::PixelInputBit; + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Expect(*device, PixelInputBit(PixelInput::PositionFixedPoint), 0u, NoInput, "POS_FIXED_PT alone"); + Expect(*device, PixelInputBit(PixelInput::LineStipple) | PixelInputBit(PixelInput::PositionFixedPoint), 1u, 0u, "LINE_STIPPLE_TEX and POS_FIXED_PT"); + Expect(*device, PixelInputBit(PixelInput::PerspectiveCenter) | PixelInputBit(PixelInput::LineStipple) | PixelInputBit(PixelInput::PositionFixedPoint), 3u, 2u, "POS_FIXED_PT after the perspective center and LINE_STIPPLE_TEX"); + std::puts("pixel fixed-point position 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/PixelHelperLanes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelHelperLanes.cpp index 0660b80360..606fcdb410 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PixelHelperLanes.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelHelperLanes.cpp @@ -87,13 +87,13 @@ void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, const ShaderR {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, target, - {0, 0, vertexPush, 128 - vertexPush} + PixelPushLayout(vertexPush, target) }; fragment.useCache = false; const auto pixelResult = ShaderRecompiler::Recompile(fragment); const std::array shaders{{ {ShaderStage::Vertex, &vertexResult, 0}, - {ShaderStage::Fragment, &pixelResult, vertexPush} + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} }}; AgcDriver::Graphics::State state{}; 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/PixelInterlock.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelInterlock.cpp index ea1bd10b24..116235fa11 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PixelInterlock.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelInterlock.cpp @@ -58,7 +58,7 @@ public: block = static_cast(std::aligned_alloc(GuestBytes, 2 * GuestBytes)); #endif Require(block != nullptr, "pixel interlock: cannot allocate the guest block"); - GuestAllocations::Mutation().Add(block, GuestBytes, true, true); + GuestAllocations::Mutation().Add(block, GuestBytes, true, true, true); } ~GuestCounters() { @@ -137,13 +137,13 @@ void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, std::span(pixelCode.data()), pixelCode, 0, {}}, {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, target, - {0, 0, vertexPush, 128 - vertexPush} + PixelPushLayout(vertexPush, target) }; fragment.useCache = false; const auto pixelResult = ShaderRecompiler::Recompile(fragment); const std::array shaders{{ {ShaderStage::Vertex, &vertexResult, 0}, - {ShaderStage::Fragment, &pixelResult, vertexPush} + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} }}; AgcDriver::Graphics::State state{}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelLds.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelLds.cpp index ba7b210d86..174ef5e521 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PixelLds.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelLds.cpp @@ -102,13 +102,13 @@ void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, bool bounded) {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, target, - {0, 0, vertexPush, 128 - vertexPush} + PixelPushLayout(vertexPush, target) }; fragment.useCache = false; const auto pixelResult = ShaderRecompiler::Recompile(fragment); const std::array shaders{{ {ShaderStage::Vertex, &vertexResult, 0}, - {ShaderStage::Fragment, &pixelResult, vertexPush} + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} }}; AgcDriver::Graphics::State state{}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsLaneSlots.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsLaneSlots.cpp index a1aa594a10..8311db5429 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsLaneSlots.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsLaneSlots.cpp @@ -95,13 +95,13 @@ void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize) { {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, target, - {0, 0, vertexPush, 128 - vertexPush} + PixelPushLayout(vertexPush, target) }; fragment.useCache = false; const auto pixelResult = ShaderRecompiler::Recompile(fragment); const std::array shaders{{ {ShaderStage::Vertex, &vertexResult, 0}, - {ShaderStage::Fragment, &pixelResult, vertexPush} + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} }}; AgcDriver::Graphics::State state{}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsReadModifyWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsReadModifyWrite.cpp new file mode 100644 index 0000000000..cf2496bda8 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsReadModifyWrite.cpp @@ -0,0 +1,198 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Sentinel = 0xdeadbeefu; +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 8; +constexpr std::uint32_t Results = 8; +constexpr std::uint32_t Factor = 0x00a5c3e1u; +constexpr std::uint32_t Counter = 0x1234u; +constexpr std::uint32_t Slots = 64; +constexpr std::uint32_t CounterOffset = Slots * 16u; +alignas(256) std::array Pixels{}; +alignas(256) std::array Outputs{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +std::vector PixelCode() { + std::vector code{0x7e040f00, 0x7e060f01, 0xd5430006, 0x0409ff03, Width}; + const auto vop2 = [&](std::uint32_t opcode, std::uint32_t destination, std::uint32_t source, std::uint32_t vector) { + code.push_back(opcode | (destination << 17u) | (vector << 9u) | source); + }; + const auto ds = [&](std::uint32_t opcode, std::uint32_t offset, std::uint32_t address, std::uint32_t data0, std::uint32_t data1, std::uint32_t destination) { + code.push_back(0xd8000000u | (opcode << 18u) | offset); + code.push_back(address | (data0 << 8u) | (data1 << 16u) | (destination << 24u)); + }; + vop2(0x36000000, 8, 0xff, 6); + code.push_back(Slots - 1u); + vop2(0x34000000, 8, 0x84, 8); + code.push_back(0x7e126f06); + vop2(0x16000000, 10, 0xff, 6); + code.push_back(Factor); + code.insert(code.end(), {0x7e1c02c1, 0x7e2002ff, 0xffff0000, 0x7e240280, 0x7e2602ff, Counter, 0xbefc03ff, 0x0000ffff}); + for (std::uint32_t offset = 0; offset < 16; offset += 4) ds(0x0d, offset, 8, 9, 0, 0); + ds(0x1e, 0, 8, 10, 0, 0); + ds(0x1e, 2, 8, 6, 0, 0); + ds(0xa1, 6, 8, 10, 0, 0); + ds(0x1f, 8, 8, 6, 0, 0); + ds(0xa0, 11, 8, 10, 0, 0); + ds(0x20, 12, 8, 10, 0, 12); + ds(0x23, 12, 8, 14, 0, 13); + ds(0x2c, 12, 8, 16, 6, 15); + ds(0x0d, CounterOffset, 18, 19, 0, 0); + ds(0x3e, CounterOffset, 0, 0, 0, 17); + code.push_back(0xbf8cc07f); + for (std::uint32_t dword = 0; dword < 4; ++dword) ds(0x36, dword * 4u, 8, 0, 0, 20 + dword); + code.push_back(0xbf8cc07f); + const std::array stored{20, 21, 22, 23, 12, 13, 15, 17}; + for (std::uint32_t slot = 0; slot < Results; ++slot) { + code.push_back(0xe0702000 | (slot * 4u)); + code.push_back(0x80010006 | (stored[slot] << 8u)); + } + code.insert(code.end(), {0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000}); + return code; +} + +std::array Expected(std::uint32_t pixel) { + const auto initial = ~pixel; + const auto product = static_cast(std::uint64_t{Factor} * pixel); + const auto added = initial + product; + const auto incremented = added == 0xffffffffu ? 0u : added + 1u; + const auto masked = (incremented & 0x0000ffffu) | pixel; + return { + (initial & 0xff00ff00u) | (product & 0xffu) | ((pixel & 0xffu) << 16u), + (initial & 0x0000ffffu) | (product & 0xffff0000u), + (initial & 0x00ff0000u) | (pixel & 0xffffu) | (((product >> 16u) & 0xffu) << 24u), + masked, + initial, + added, + incremented, + Counter, + }; +} + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +constexpr std::array, 3> Triangles{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize) { + const auto& triangles = Triangles; + const auto pixelCode = PixelCode(); + Pixels.fill(std::byte{0}); + Outputs.fill(Sentinel); + const auto target = device.Target(); + + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(triangles.data(), 16u, static_cast(triangles.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.wave32 = waveSize == 32u; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + std::vector pixelUserData(8, 0u); + const auto results = BufferDescriptor(Outputs.data(), Results * 4u, Width * Height); + std::copy(results.begin(), results.end(), pixelUserData.begin() + 4); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(std::span(pixelCode))}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(triangles.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +void Check(std::uint32_t waveSize) { + static constexpr std::array names{"ds_write_b8 dword", "ds_write_b16_d16_hi dword", "ds_write_b16 and ds_write_b8_d16_hi dword", "atomic dword", "ds_add_rtn_u32", "ds_inc_rtn_u32", "ds_mskor_rtn_b32", "ds_append"}; + const auto what = "pixel LDS read-modify-write wave" + std::to_string(waveSize); + for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { + for (std::uint32_t channel = 0; channel < 4; ++channel) { + Require(Pixels[pixel * 4u + channel] == std::byte{255}, what + ": missing color at pixel " + std::to_string(pixel)); + } + const auto expected = Expected(pixel); + for (std::uint32_t slot = 0; slot < Results; ++slot) { + const auto actual = Outputs[pixel * Results + slot]; + Require(actual == expected[slot], what + ": pixel " + std::to_string(pixel) + " " + names[slot] + " expected " + std::to_string(expected[slot]) + " got " + std::to_string(actual)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const auto waveSize : {64u, 32u}) { + std::fprintf(stderr, "pixel LDS read-modify-write wave%u\n", waveSize); + for (std::uint32_t pass = 0; pass < 2; ++pass) { + Draw(*device, waveSize); + Check(waveSize); + } + } + std::puts("pixel LDS read-modify-write tests passed: 4 draws, 2048 pixels, 16384 results"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/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..7c6d5398c2 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelSampleCoverage.cpp @@ -0,0 +1,294 @@ +#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 + +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} +}}; + +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 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); +} + +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 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.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; + 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; +} + +} + +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"); + 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/PixelUint16Export.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelUint16Export.cpp index 04e41da4c7..f354ffc864 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PixelUint16Export.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelUint16Export.cpp @@ -107,7 +107,7 @@ struct Block { 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); + mutation.Add(data, bytes, true, true, true); } ~Block() { AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); @@ -187,13 +187,13 @@ void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarge {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, target, - {0, 0, vertexPush, 128 - vertexPush} + PixelPushLayout(vertexPush, target) }; fragment.useCache = false; const auto pixelResult = ShaderRecompiler::Recompile(fragment); const std::array shaders{{ {ShaderStage::Vertex, &vertexResult, 0}, - {ShaderStage::Fragment, &pixelResult, vertexPush} + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} }}; AgcDriver::Graphics::State state{}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelValidMask.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelValidMask.cpp index 914ba8e972..2b8f7b61fe 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PixelValidMask.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelValidMask.cpp @@ -71,13 +71,13 @@ void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, std::span(pixelCode.data()), pixelCode, 0, {}}, {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, target, - {0, 0, vertexPush, 128 - vertexPush} + PixelPushLayout(vertexPush, target) }; fragment.useCache = false; const auto pixelResult = ShaderRecompiler::Recompile(fragment); const std::array shaders{{ {ShaderStage::Vertex, &vertexResult, 0}, - {ShaderStage::Fragment, &pixelResult, vertexPush} + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} }}; AgcDriver::Graphics::State state{}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelWaveReduction.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelWaveReduction.cpp index efdfff7d9f..7c007900a0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PixelWaveReduction.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelWaveReduction.cpp @@ -105,13 +105,13 @@ void Draw(AgcDriver::VulkanDevice& device, std::span pixelC {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, {64u, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, target, - {0, 0, vertexPush, 128 - vertexPush} + PixelPushLayout(vertexPush, target) }; fragment.useCache = false; const auto pixelResult = ShaderRecompiler::Recompile(fragment); const std::array shaders{{ {ShaderStage::Vertex, &vertexResult, 0}, - {ShaderStage::Fragment, &pixelResult, vertexPush} + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} }}; AgcDriver::Graphics::State state{}; @@ -194,13 +194,16 @@ int main() { }}; for (const auto& layout : layouts) { for (const auto& reduction : reductions) { - const auto code = PixelCode(reduction); - std::uint32_t partial = 0; - for (std::uint32_t pass = 0; pass < 4; ++pass) { - Draw(*device, code, layout); - partial += Check(reduction, target.subgroupSize); + 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::printf("%s, laid out for %u lanes on %u-lane subgroups: %u partial waves over 4 draws\n", reduction.name, layout.subgroupSize, target.subgroupSize, partial); } } std::puts("pixel wave reduction tests passed"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PknormF16Denormals.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PknormF16Denormals.cpp new file mode 100644 index 0000000000..7e87976c54 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PknormF16Denormals.cpp @@ -0,0 +1,151 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Inputs = 4; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +struct alignas(256) Shader { + std::array words; +}; + +struct Vector { + std::uint32_t low; + std::uint32_t high; + std::uint32_t expected[4]; +}; + +struct Case { + const char* name; + Shader shader; + std::span rows; +}; + +constexpr Vector PknormI16Rows[] = { + {0x0001u, 0x8001u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x0155u, 0x8155u, {0x00000000u, 0xffff0001u, 0x00000000u, 0xffff0001u}}, + {0x0200u, 0x8200u, {0x00000000u, 0xffff0001u, 0x00000000u, 0xffff0001u}}, + {0x03ffu, 0x83ffu, {0x00000000u, 0xfffe0002u, 0x00000000u, 0xfffe0002u}}, + {0x8001u, 0x0001u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x8200u, 0x0200u, {0x00000000u, 0x0001ffffu, 0x00000000u, 0x0001ffffu}}, + {0x83ffu, 0x03ffu, {0x00000000u, 0x0002fffeu, 0x00000000u, 0x0002fffeu}}, + {0x0400u, 0x8400u, {0xfffe0002u, 0xfffe0002u, 0xfffe0002u, 0xfffe0002u}}, + {0x8400u, 0x0400u, {0x0002fffeu, 0x0002fffeu, 0x0002fffeu, 0x0002fffeu}}, + {0x0000u, 0x8000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x8000u, 0x0000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x3c00u, 0xbc00u, {0x80017fffu, 0x80017fffu, 0x80017fffu, 0x80017fffu}}, + {0xbc00u, 0x3c00u, {0x7fff8001u, 0x7fff8001u, 0x7fff8001u, 0x7fff8001u}}, + {0x3800u, 0xb800u, {0xc0004000u, 0xc0004000u, 0xc0004000u, 0xc0004000u}}, + {0x7c00u, 0xfc00u, {0x80017fffu, 0x80017fffu, 0x80017fffu, 0x80017fffu}}, + {0x7e00u, 0xfe00u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, +}; + +constexpr Vector PknormU16Rows[] = { + {0x0001u, 0x8001u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x0155u, 0x8155u, {0x00000000u, 0x00000001u, 0x00000000u, 0x00000001u}}, + {0x0200u, 0x8200u, {0x00000000u, 0x00000002u, 0x00000000u, 0x00000002u}}, + {0x03ffu, 0x83ffu, {0x00000000u, 0x00000004u, 0x00000000u, 0x00000004u}}, + {0x8001u, 0x0001u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x8200u, 0x0200u, {0x00000000u, 0x00020000u, 0x00000000u, 0x00020000u}}, + {0x83ffu, 0x03ffu, {0x00000000u, 0x00040000u, 0x00000000u, 0x00040000u}}, + {0x0400u, 0x8400u, {0x00000004u, 0x00000004u, 0x00000004u, 0x00000004u}}, + {0x8400u, 0x0400u, {0x00040000u, 0x00040000u, 0x00040000u, 0x00040000u}}, + {0x0000u, 0x8000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x8000u, 0x0000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x3c00u, 0xbc00u, {0x0000ffffu, 0x0000ffffu, 0x0000ffffu, 0x0000ffffu}}, + {0xbc00u, 0x3c00u, {0xffff0000u, 0xffff0000u, 0xffff0000u, 0xffff0000u}}, + {0x3800u, 0xb800u, {0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u}}, + {0x7c00u, 0xfc00u, {0x0000ffffu, 0x0000ffffu, 0x0000ffffu, 0x0000ffffu}}, + {0x7e00u, 0xfe00u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, +}; + +const Case Cases[] = { + {"v_cvt_pknorm_i16_f16", {{0x34020084u, 0x34040082u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e140280u, 0xd712000au, 0x00020b04u, 0xe0701000u, 0x80010a02u, 0xbf810000u}}, PknormI16Rows}, + {"v_cvt_pknorm_u16_f16", {{0x34020084u, 0x34040082u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e140280u, 0xd713000au, 0x00020b04u, 0xe0701000u, 0x80010a02u, 0xbf810000u}}, PknormU16Rows}, +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, const Case& item, std::uint32_t denorm16, bool ieee) { + Input.fill(0u); + for (std::uint32_t row = 0; row < item.rows.size(); ++row) { + Input[row * Inputs] = item.rows[row].low; + Input[row * Inputs + 1] = item.rows[row].high; + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(item.shader.words); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = ShaderRecompiler::ShaderFloatMode{denorm16 << 6u, true, ieee, false}; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const Case& item, std::uint32_t denorm16, bool ieee) { + for (std::uint32_t row = 0; row < item.rows.size(); ++row) { + const auto actual = Output[row]; + const auto expected = item.rows[row].expected[denorm16]; + Require(actual == expected, std::string(item.name) + " denorm16 " + std::to_string(denorm16) + (ieee ? " ieee" : " no ieee") + " input " + + Hex(item.rows[row].low) + " " + Hex(item.rows[row].high) + " is " + Hex(actual) + ", expected " + Hex(expected)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const auto& item : Cases) { + for (std::uint32_t denorm16 = 0u; denorm16 < 4u; ++denorm16) { + for (const bool ieee : {false, true}) { + Run(*device, item, denorm16, ieee); + Check(item, denorm16, ieee); + } + } + } + std::puts("pknorm f16 denormal tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp new file mode 100644 index 0000000000..785886aa84 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp @@ -0,0 +1,590 @@ +#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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +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; +constexpr VkMemoryPropertyFlags HostMemory = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; +constexpr std::size_t RequestedBytes = 100; +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; + return sizes; +} + +VKAPI_ATTR VkResult VKAPI_CALL CountedCreateBuffer(VkDevice device, const VkBufferCreateInfo* info, const VkAllocationCallbacks* allocator, VkBuffer* buffer) { + CreatedSizes().push_back(info->size); + 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); +} + +class Device { +public: + Device() { +#ifdef _WIN32 + library = SDL_LoadObject("vulkan-1.dll"); +#else + library = SDL_LoadObject("libvulkan.so.1"); +#endif + Require(library != nullptr, "cannot load Vulkan"); + try { + instanceProc = reinterpret_cast(SDL_LoadFunction(library, "vkGetInstanceProcAddr")); + Require(instanceProc != nullptr, "missing Vulkan instance resolver"); + VkApplicationInfo application{VK_STRUCTURE_TYPE_APPLICATION_INFO}; + application.apiVersion = VK_API_VERSION_1_1; + VkInstanceCreateInfo info{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; + info.pApplicationInfo = &application; + Check(function("vkCreateInstance")(&info, nullptr, &instance), "vkCreateInstance"); + std::uint32_t count = 0; + const auto enumerate = function("vkEnumeratePhysicalDevices"); + Check(enumerate(instance, &count, nullptr), "vkEnumeratePhysicalDevices"); + Require(count != 0, "no Vulkan device"); + std::vector devices(count); + Check(enumerate(instance, &count, devices.data()), "vkEnumeratePhysicalDevices"); + const auto properties = function("vkGetPhysicalDeviceProperties"); + 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 (named ? std::strstr(described.deviceName, requested) != nullptr : described.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) { + context.physical = candidate; + break; + } + } + VkPhysicalDeviceProperties selected{}; + properties(context.physical, &selected); + context.limits = selected.limits; + 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"); + const auto queues = function("vkGetPhysicalDeviceQueueFamilyProperties"); + queues(context.physical, &count, nullptr); + std::vector families(count); + queues(context.physical, &count, families.data()); + std::uint32_t family = 0; + while (family < count && (families[family].queueFlags & VK_QUEUE_COMPUTE_BIT) == 0) ++family; + Require(family < count, "no Vulkan compute queue"); + const float priority = 1; + VkDeviceQueueCreateInfo queue{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; + queue.queueFamilyIndex = family; + queue.queueCount = 1; + queue.pQueuePriorities = &priority; + VkPhysicalDeviceFeatures features{}; + features.robustBufferAccess = VK_TRUE; + VkDeviceCreateInfo device{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO}; + device.queueCreateInfoCount = 1; + device.pQueueCreateInfos = &queue; + device.pEnabledFeatures = &features; + Check(function("vkCreateDevice")(context.physical, &device, nullptr, &context.device), "vkCreateDevice"); + 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 (...) { + release(); + throw; + } + } + + ~Device() { release(); } + Device(const Device&) = delete; + Device& operator=(const Device&) = delete; + const Context& GetContext() const { return context; } + +private: + template + TFunction function(const char* name) const { + const auto result = reinterpret_cast(instanceProc(instance, name)); + Require(result != nullptr, name); + return result; + } + + void release() noexcept { + if (context.pool != VK_NULL_HANDLE) context.Function("vkDestroyCommandPool")(context.device, context.pool, nullptr); + context.bufferPool.reset(); + 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); + } + + void* library = nullptr; + PFN_vkGetInstanceProcAddr instanceProc = nullptr; + VkInstance instance = VK_NULL_HANDLE; + 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); +} + +std::vector CopyPastEndModule() { + enum : std::uint32_t { Void = 1, FunctionType, Uint, Words, Block, BlockPointer, Source, Destination, UintPointer, Zero, Index, Main, Label, SourcePointer, Value, DestinationPointer, Bound }; + constexpr std::uint32_t storageBuffer = 12; + std::vector words{0x07230203u, 0x00010300u, 0u, Bound, 0u}; + Emit(words, 17, {1}); + Emit(words, 14, {0, 1}); + Emit(words, 15, {5, Main, 0x6e69616du, 0u}); + Emit(words, 16, {Main, 17, 1, 1, 1}); + Emit(words, 71, {Words, 6, 4}); + Emit(words, 71, {Block, 2}); + Emit(words, 72, {Block, 0, 35, 0}); + Emit(words, 71, {Source, 34, 0}); + Emit(words, 71, {Source, 33, 0}); + Emit(words, 71, {Destination, 34, 0}); + Emit(words, 71, {Destination, 33, 1}); + Emit(words, 19, {Void}); + Emit(words, 33, {FunctionType, Void}); + Emit(words, 21, {Uint, 32, 0}); + Emit(words, 29, {Words, Uint}); + Emit(words, 30, {Block, Words}); + Emit(words, 32, {BlockPointer, storageBuffer, Block}); + Emit(words, 59, {BlockPointer, Source, storageBuffer}); + Emit(words, 59, {BlockPointer, Destination, storageBuffer}); + Emit(words, 32, {UintPointer, storageBuffer, Uint}); + Emit(words, 43, {Uint, Zero, 0}); + Emit(words, 43, {Uint, Index, PastEnd}); + Emit(words, 54, {Void, Main, 0, FunctionType}); + Emit(words, 248, {Label}); + Emit(words, 65, {UintPointer, SourcePointer, Source, Zero, Index}); + Emit(words, 61, {Uint, Value, SourcePointer}); + Emit(words, 65, {UintPointer, DestinationPointer, Destination, Zero, Zero}); + Emit(words, 62, {DestinationPointer, Value}); + Emit(words, 253, {}); + Emit(words, 56, {}); + return words; +} + +std::uint32_t ReadPastEnd(const Context& context, AgcDriver::Graphics::Buffer& source, AgcDriver::Graphics::Buffer& destination) { + const std::array bindings{{{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}, {1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}}}; + VkDescriptorSetLayoutCreateInfo setLayoutInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + setLayoutInfo.bindingCount = static_cast(bindings.size()); + setLayoutInfo.pBindings = bindings.data(); + VkDescriptorSetLayout setLayout = VK_NULL_HANDLE; + Check(context.Function("vkCreateDescriptorSetLayout")(context.device, &setLayoutInfo, nullptr, &setLayout), "vkCreateDescriptorSetLayout"); + const VkDescriptorPoolSize poolSize{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2}; + VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + poolInfo.maxSets = 1; + poolInfo.poolSizeCount = 1; + poolInfo.pPoolSizes = &poolSize; + VkDescriptorPool descriptorPool = VK_NULL_HANDLE; + Check(context.Function("vkCreateDescriptorPool")(context.device, &poolInfo, nullptr, &descriptorPool), "vkCreateDescriptorPool"); + VkDescriptorSetAllocateInfo allocateInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + allocateInfo.descriptorPool = descriptorPool; + allocateInfo.descriptorSetCount = 1; + allocateInfo.pSetLayouts = &setLayout; + VkDescriptorSet set = VK_NULL_HANDLE; + Check(context.Function("vkAllocateDescriptorSets")(context.device, &allocateInfo, &set), "vkAllocateDescriptorSets"); + const std::array infos{{{source.Handle(), 0, VK_WHOLE_SIZE}, {destination.Handle(), 0, VK_WHOLE_SIZE}}}; + std::array writes{}; + for (std::uint32_t binding = 0; binding < writes.size(); ++binding) { + writes[binding] = {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + writes[binding].dstSet = set; + writes[binding].dstBinding = binding; + writes[binding].descriptorCount = 1; + writes[binding].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + writes[binding].pBufferInfo = &infos[binding]; + } + context.Function("vkUpdateDescriptorSets")(context.device, static_cast(writes.size()), writes.data(), 0, nullptr); + + const auto module = CopyPastEndModule(); + VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + moduleInfo.codeSize = module.size() * sizeof(std::uint32_t); + moduleInfo.pCode = module.data(); + VkShaderModule shader = VK_NULL_HANDLE; + Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &shader), "vkCreateShaderModule"); + VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layoutInfo.setLayoutCount = 1; + layoutInfo.pSetLayouts = &setLayout; + VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; + Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &pipelineLayout), "vkCreatePipelineLayout"); + VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; + pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; + pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; + pipelineInfo.stage.module = shader; + pipelineInfo.stage.pName = "main"; + pipelineInfo.layout = pipelineLayout; + VkPipeline pipeline = VK_NULL_HANDLE; + Check(context.Function("vkCreateComputePipelines")(context.device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateComputePipelines"); + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + AgcDriver::Graphics::CommandBatch batch(context); + context.Function("vkCmdBindPipeline")(batch.Handle(), VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); + context.Function("vkCmdBindDescriptorSets")(batch.Handle(), VK_PIPELINE_BIND_POINT_COMPUTE, pipelineLayout, 0, 1, &set, 0, nullptr); + context.Function("vkCmdDispatch")(batch.Handle(), 1, 1, 1); + AgcDriver::Graphics::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(); + } + context.Function("vkDestroyPipeline")(context.device, pipeline, nullptr); + context.Function("vkDestroyPipelineLayout")(context.device, pipelineLayout, nullptr); + context.Function("vkDestroyShaderModule")(context.device, shader, nullptr); + context.Function("vkDestroyDescriptorPool")(context.device, descriptorPool, nullptr); + context.Function("vkDestroyDescriptorSetLayout")(context.device, setLayout, nullptr); + destination.Invalidate(); + std::uint32_t value = 0; + std::memcpy(&value, destination.Bytes().data(), sizeof(value)); + return value; +} + +void RobustReadPastEnd(const Context& context) { + constexpr VkBufferUsageFlags usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT; + const auto classBytes = BufferPool::Capacity(RequestedBytes, HostMemory); + Require(classBytes > (PastEnd + 1) * sizeof(std::uint32_t), "the requested buffer's size class does not reach the word read past its end"); + { + AgcDriver::Graphics::Buffer earlier(context, classBytes, usage); + const auto bytes = earlier.Bytes(); + for (std::size_t offset = 0; offset + sizeof(Stale) <= bytes.size(); offset += sizeof(Stale)) std::memcpy(bytes.data() + offset, &Stale, sizeof(Stale)); + } + AgcDriver::Graphics::Buffer source(context, RequestedBytes, usage); + const auto bytes = source.Bytes(); + Require(bytes.size() == RequestedBytes, "the pooled buffer does not span the requested bytes"); + for (std::size_t offset = 0; offset + sizeof(Fresh) <= bytes.size(); offset += sizeof(Fresh)) std::memcpy(bytes.data() + offset, &Fresh, sizeof(Fresh)); + AgcDriver::Graphics::Buffer destination(context, 4, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + std::memset(destination.Bytes().data(), 0, destination.Bytes().size()); + const auto value = ReadPastEnd(context, source, destination); + char text[160]; + std::snprintf(text, sizeof(text), "a robust read of word %u of a %zu-byte pooled buffer returned 0x%08x, the bytes a %zu-byte earlier user left in its memory", PastEnd, RequestedBytes, value, classBytes); + Require(value != Stale, text); + Require(value == 0 || value == Fresh, "a robust read past the buffer returned neither zero nor a value inside it"); +} + +void ExpectCreated(std::initializer_list sizes, const std::string& what) { + const std::vector expected(sizes); + std::string got; + for (const auto size : CreatedSizes()) got += " " + std::to_string(size); + Require(CreatedSizes() == expected, what + ": VkBuffer sizes created:" + (got.empty() ? std::string(" none") : got)); + CreatedSizes().clear(); +} + +void ExactSizes(const Context& context) { + constexpr VkBufferUsageFlags usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; + const auto hostClass = BufferPool::Capacity(100, HostMemory); + const auto deviceClass = BufferPool::Capacity(3000, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + Require(BufferPool::Capacity(80, HostMemory) == hostClass && hostClass != 80, "80 and 100 bytes are not one host size class"); + Require(BufferPool::Capacity(2500, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) == deviceClass && deviceClass != 2500, "2500 and 3000 bytes are not one device size class"); + CreatedSizes().clear(); + { + AgcDriver::Graphics::Buffer fresh(context, 100, usage); + ExpectCreated({100}, "a fresh 100-byte buffer"); + } + { + AgcDriver::Graphics::Buffer pooled(context, 80, usage); + ExpectCreated({80}, "an 80-byte buffer taken from the " + std::to_string(hostClass) + "-byte class"); + } + { + AgcDriver::Graphics::Buffer again(context, 80, usage); + ExpectCreated({}, "an 80-byte buffer reusing an 80-byte one"); + } + { + AgcDriver::Graphics::DeviceBuffer fresh(context, 3000, usage); + ExpectCreated({3000}, "a fresh 3000-byte device buffer"); + } + { + AgcDriver::Graphics::DeviceBuffer pooled(context, 2500, usage); + ExpectCreated({2500}, "a 2500-byte device buffer taken from the " + std::to_string(deviceClass) + "-byte class"); + } +} + +} + +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; + } + 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) { + std::cerr << error.what() << '\n'; + return 1; + } +} 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/PreparedExports.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedExports.cpp index b0b85a7b73..bdbfd13628 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedExports.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedExports.cpp @@ -29,7 +29,7 @@ void Run(AgcDriver::VulkanDevice& device, bool integer) { const std::array values = integer ? std::array{11, 22, 33, 44} : std::array{0, 0x3e800000u, 0x3f000000u, 0x3f800000u}; const std::array code{0x7e0002ffu, values[0], 0x7e0202ffu, values[1], 0x7e0402ffu, values[2], 0x7e0602ffu, values[3], 0xf800100fu, 0x03020100u, 0xf800183fu, 0x03020100u, 0xbf810000u}; AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, integer ? 0x7007u : 0x9009u}}; - RecompileRequest request{{ShaderStage::Fragment, 0x20000, code, 0, {}}, {32, 0, {}, {}, AgcDriver::Graphics::DecodePixelStageInfo(context, {}), {}, {}}, device.Target(), {0, 0, 0, 128}}; + RecompileRequest request{{ShaderStage::Fragment, 0x20000, code, 0, {}}, {32, 0, {}, {}, AgcDriver::Graphics::DecodePixelStageInfo(context, {}), {}, {}}, device.Target(), PixelPushLayout(0u, device.Target())}; const auto handle = PrepareShader(request); const auto& artifact = GetPreparedArtifact(*handle); std::uint32_t scalarType = 0; @@ -85,6 +85,10 @@ void Run(AgcDriver::VulkanDevice& device, bool integer) { request.useCache = cached; const auto materialized = MaterializeShader(request, *capture, *handle); Require(materialized->spirv.data() == pixel->spirv.data() && materialized->PipelineVariantId() == pixel->PipelineVariantId(), "export mapping did not reuse its specialized module"); + if (device.Target().registrationModules) { + Require(materialized->PipelineVariantId() == first->PipelineVariantId(), "registration modules: the export mapping specialized the fragment module"); + continue; + } for (std::size_t cursor = 5; cursor < materialized->spirv.size();) { const auto count = materialized->spirv[cursor] >> 16u; Require(count != 0u && count <= materialized->spirv.size() - cursor, "invalid specialized fragment instruction"); @@ -94,7 +98,7 @@ void Run(AgcDriver::VulkanDevice& device, bool integer) { } request.useCache = true; if (integer) continue; - const std::array shaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::Fragment, pixel.get(), 0}}}; + const std::array shaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::Fragment, pixel.get(), PixelPushOffset(0u, device.Target())}}}; pixels.fill(0xdeadbeef); otherPixels.fill(0xdeadbeef); device.Draw(state, draw, shaders); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp index 7708197535..56ea345d1f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp @@ -1,18 +1,25 @@ +#include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" #include "VulkanTestDevice.hpp" #include "prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp" #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 { @@ -42,7 +49,7 @@ struct Fixture { header.shader.type = type; header.shader.user_data = &header.users; code.fill(0xffffffffu); - code.back() = 0xbf810000u; + code.back() = type == 5u ? 0xbefd2106u : 0xbf810000u; const auto address = reinterpret_cast(code.data()); const auto headerAddress = reinterpret_cast(&header); snapshot = std::make_shared(); @@ -79,7 +86,7 @@ void Check(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ShaderPath path fragment.snapshot->code.resize(64); fragment.snapshot->code.insert(fragment.snapshot->code.end(), pixelCode.begin(), pixelCode.end()); ShaderRegistry registry; - for (const auto* fixture : {&front, &back, &domain, &fragment}) registry.emplace(fixture->snapshot->codeAddress, fixture->snapshot); + for (const auto* fixture : {&front, &back, &domain, &fragment}) registry.emplace(fixture->snapshot->codeAddress, std::vector>{fixture->snapshot}); AgcDriver::QueueState queue{}; queue.context[0x8e] = 0xfu; queue.context[0x8f] = 0xfu; @@ -113,7 +120,7 @@ void Check(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ShaderPath path for (std::size_t index = 0; index < prepared.programs.size(); ++index) { const auto& expected = prepared.programs[index]; const auto& actual = draw.programs[index]; - Require(expected.codeOffset == 64u && expected.binary.code.size() == (prepared.roles[index] == ProgramRole::Fragment ? pixelCode.size() : 1u) && expected.binary.code[0] == (prepared.roles[index] == ProgramRole::Fragment ? pixelCode[0] : 0xbf810000u), "graphics entry point did not trim the code prefix"); + Require(expected.codeOffset == 64u && expected.binary.code.size() == (prepared.roles[index] == ProgramRole::Fragment ? pixelCode.size() : 1u) && expected.binary.code[0] == (prepared.roles[index] == ProgramRole::Fragment ? pixelCode[0] : prepared.roles[index] == ProgramRole::Local ? 0xbefd2106u : 0xbf810000u), "graphics entry point did not trim the code prefix"); Require(expected.binary.stage == actual.binary.stage && expected.firstUserSgpr == actual.firstUserSgpr && expected.userData.size() == actual.userData.size(), "graphics preparation changed the user SGPR ABI"); } auto target = device.Target(); @@ -125,9 +132,34 @@ void Check(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ShaderPath path target.supportedCapabilities = capabilities; target.supportedExtensions = extensions; target.mesh = MeshTargetLimits{{128, 1, 1}, 128, 32768, 256, 256, 128, 32768, 1, 1}; + target.spirvVersion = 0x00010400u; } const auto stages = PrepareGraphicsStages(prepared, target); Require(stages.size() == (tessellation ? 4u : 2u), "graphics preparation compiled the wrong stages"); + if (!mesh && !tessellation && target.registrationModules) { + const auto prebuild = [&](const std::vector& group, std::initializer_list picked) { + std::vector shaders; + for (const auto index : picked) { + Require(group.at(index).registration != nullptr, "registration modules: a prepared stage has no registration result"); + shaders.push_back({group[index].stage, group[index].registration.get(), group[index].pushOffset}); + } + const auto before = AgcDriver::Graphics::PipelineLibraryCountersOf(device.Device()); + Require(device.PrebuildGraphicsLibraries(prepared.state.stages, shaders), "registration modules: the shader libraries were not prebuilt"); + const auto after = AgcDriver::Graphics::PipelineLibraryCountersOf(device.Device()); + Require(after.linked == before.linked, "registration modules: a prebuild linked a pipeline"); + return std::pair{after.built[1] - before.built[1], after.built[2] - before.built[2]}; + }; + const auto vertexOnly = prebuild(stages, {0u}); + Require(vertexOnly.first == 1u, "registration modules: the vertex stage alone did not build its pre-rasterization library"); + const auto pixelOnly = prebuild(stages, {1u}); + Require(pixelOnly.second == 1u, "registration modules: the pixel stage alone did not build its fragment library"); + const auto pair = prebuild(stages, {0u, 1u}); + Require(pair.first == 0u && pair.second == 0u, "registration modules: the pair of stages did not find the libraries prebuilt one stage at a time"); + const auto again = PrepareGraphicsStages(prepared, target); + Require(again.front().registration != stages.front().registration, "registration modules: the second preparation reused the first result object"); + const auto repeated = prebuild(again, {0u, 1u}); + Require(repeated.first == 0u && repeated.second == 0u, "registration modules: another materialization of the same stages did not find the prebuilt libraries"); + } for (const auto& stage : stages) stage.snapshot->prepared->entries.push_back(stage.entry); const auto& pixelArtifact = GetPreparedArtifact(*stages.back().entry.handle); Require(!pixelArtifact.fragmentParameters.empty() && pixelArtifact.fragmentParameters.front().sourceLocation == 3u, "prepared fragment lost interpolant mapping"); @@ -182,6 +214,18 @@ void Check(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ShaderPath path ResolvePreparedGraphics(*front.snapshot, {}, 7, target); Require(front.snapshot->prepared->fragments.size() == 1, "expired helper link was not removed"); Require(front.snapshot->prepared->rectangleProgress.size() == 1 && !front.snapshot->prepared->rectangleProgress.front().fragment.expired(), "expired rectangle progress was not removed"); + auto drawOnly = prepared; + drawOnly.pixel.interpolatorSettings[0] = 0x405u; + const auto drawStages = PrepareGraphicsStages(drawOnly, target); + auto unlinked = std::make_shared(); + unlinked->type = 1; + unlinked->prepared->entries.push_back(drawStages.back().entry); + const auto drawPixelId = GetPreparedArtifact(*drawStages.back().entry.handle).variantId; + const auto drawVertexId = GetPreparedArtifact(*drawStages.front().entry.handle).variantId; + Require(drawVertexId == GetPreparedArtifact(*stages.front().entry.handle).variantId && drawPixelId != pixelArtifact.variantId && drawPixelId != newPixelId, "draw-only fragment test did not isolate a new fragment variant"); + const auto drawRectangle = DrawRectangle(*front.snapshot, unlinked, drawVertexId, drawPixelId, target); + Require(!drawRectangle.control.spirv.empty() && !drawRectangle.evaluation.spirv.empty(), "a rect-list draw did not prepare the rectangle of a fragment variant prepared at draw"); + static_cast(PreparedRectangle(*front.snapshot, drawVertexId, drawPixelId)); } if (!dump.empty()) { for (std::size_t index = 0; index < stages.size(); ++index) { @@ -217,15 +261,57 @@ void Check(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ShaderPath path request.layout.pushConstantSizeBytes -= 4; static_cast(InvocationFor(*program.snapshot, program.codeOffset, request)); request.layout.pushConstantOffsetBytes = 2; - Reject([&] { static_cast(InvocationFor(*program.snapshot, program.codeOffset, request)); }, "artifact is missing"); + Reject([&] { static_cast(InvocationFor(*program.snapshot, program.codeOffset, request)); }, "not dword-aligned"); request.layout = {0, 0, 0, mesh ? MeshDrawPushOffsetBytes : 128u}; if (pixel) request.context.pixel->interpolatorSettings[0] ^= 1u; else if (tessellation) ++request.graphics->tessellation->outputControlPoints; else if (mesh) ++request.graphics->mesh->maxVertices; else ++request.context.userDataBaseRegister; - Reject([&] { static_cast(InvocationFor(*program.snapshot, program.codeOffset, request)); }, "artifact is missing"); + const auto predicted = program.snapshot->prepared->entries.size(); + static_cast(InvocationFor(*program.snapshot, program.codeOffset, request)); + Require(program.snapshot->prepared->entries.size() == predicted + 1, "a draw state that registration did not predict was not prepared"); + static_cast(InvocationFor(*program.snapshot, program.codeOffset, request)); + Require(program.snapshot->prepared->entries.size() == predicted + 1, "an artifact prepared at a draw was not reused"); + } + if (tessellation) { + Fixture late; + late.Initialize(5u); + registry.emplace(late.snapshot->codeAddress, std::vector>{late.snapshot}); + auto unbound = queue; + unbound.shader[0x148u] = 0u; + unbound.shader[0x149u] = 0u; + DrawDecode resolved{}; + resolved.state = prepared.state; + resolved.pixel = prepared.pixel; + DecodeGraphicsPrograms(resolved, unbound, registry, true, true); + Require(resolved.programs.size() == 3u && resolved.roles.front() == ProgramRole::Hull, "an unbound local program was not left to the draw"); + Require(PrepareGraphicsStages(resolved, target).size() == 3u, "graphics preparation without the local program compiled the wrong stages"); + auto bound = queue; + late.Bind(bound, 0x148u, 0x10bu); + DrawDecode lateDraw{}; + lateDraw.state = prepared.state; + lateDraw.pixel = prepared.pixel; + DecodeGraphicsPrograms(lateDraw, bound, registry, false, true); + const auto& program = lateDraw.programs.front(); + Require(program.snapshot == late.snapshot && late.snapshot->prepared->entries.empty(), "the late local program was decoded from another shader"); + const auto vertex = AgcDriver::Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, nullptr, true); + RecompileRequest request{program.binary, {prepared.state.stages.vertexWaveSize, program.firstUserSgpr, program.userData, {}, std::nullopt, vertex, memory}, target, {0, 0, 0, 128u}, GraphicsCompileContext{program.firstUserSgpr, linked, std::nullopt, prepared.state.stages.tessellation, {0, 3, 4, 1}}}; + static_cast(InvocationFor(*late.snapshot, program.codeOffset, request)); + static_cast(InvocationFor(*late.snapshot, program.codeOffset, request)); + Require(late.snapshot->prepared->entries.size() == 1u, "the local program was not prepared once at its first draw"); + ++request.graphics->tessellation->outputControlPoints; + static_cast(InvocationFor(*late.snapshot, program.codeOffset, request)); + Require(late.snapshot->prepared->entries.size() == 2u, "a second state of the local program was not prepared at its draw"); + registry.erase(late.snapshot->codeAddress); } ShaderRegistry invalidRegistry; + 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"); @@ -234,6 +320,397 @@ 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; + front.Initialize(2u); + const auto null = std::make_shared(PrepareNullPixelProgram(device)); + const auto registered = null->prepared->entries; + Require(!registered.empty(), "the null pixel program was not prepared at registration"); + ShaderRegistry registry; + registry.emplace(front.snapshot->codeAddress, std::vector>{front.snapshot}); + registry.emplace(null->codeAddress, std::vector>{null}); + AgcDriver::QueueState queue{}; + queue.context[0x8e] = 0xfu; + queue.context[0x8f] = 0xfu; + front.Bind(queue, 0xc8u, 0x8bu); + alignas(8) const std::array merged{}; + const auto mergedAddress = reinterpret_cast(merged.data()); + queue.shader[0x82u] = static_cast(mergedAddress); + queue.shader[0x83u] = static_cast(mergedAddress >> 32u); + queue.shader[0x008u] = 0u; + queue.shader[0x009u] = 0u; + queue.shader[0x00bu] = 4u << 1u; + queue.context[0x1b3u] = 0x30u; + queue.context[0x1b4u] = 0x30u; + DrawDecode draw{}; + draw.state.stages.path = AgcDriver::Graphics::ShaderPath::Vertex; + draw.state.stages.vertexWaveSize = 32u; + draw.state.stages.fragmentWaveSize = 64u; + std::array mappings{}; + mappings.fill(0xe4u); + draw.pixel = AgcDriver::Graphics::DecodePixelStageInfo(queue.context, mappings, true); + DecodeGraphicsPrograms(draw, queue, registry, false, true); + const auto& fragment = draw.programs.back(); + Require(fragment.snapshot == null && fragment.userData.empty(), "a null pixel draw took the last pixel shader's user SGPRs"); + const auto stages = PrepareGraphicsStages(draw, device.Target()); + const auto& handle = stages.back().entry.handle; + Require(std::ranges::any_of(registered, [&](const auto& entry) { return entry.handle == handle; }), "a null pixel draw did not match the variant prepared at registration"); +} + } int main(int argc, char** argv) { @@ -241,10 +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 99f9c45a3c..beaed11d60 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedPublication.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedPublication.cpp @@ -1,12 +1,20 @@ #include "VulkanTestDevice.hpp" +#include "SceShaders.hpp" #include "prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "Optimization/ResourceProgram.hpp" +#include +#include #include #include #include #include +#include #include +#include +#include +#include +#include #include namespace { @@ -106,6 +114,160 @@ void Transactions() { Require(!partial && front.prepared->entries.size() == 200 && pixel->prepared->entries.size() == 200, "concurrent group publication was partial or lost an update"); } +void HeaderSelection() { + const auto makeSnapshot = [](std::uint64_t headerAddress, std::uint8_t type) { + auto snapshot = std::make_shared(); + snapshot->codeAddress = 0x10000; + snapshot->headerAddress = headerAddress; + snapshot->type = type; + snapshot->code = {0xbf810000u}; + Shader header{}; + header.code = reinterpret_cast(snapshot->codeAddress); + header.type = type; + snapshot->header.resize(sizeof(Shader)); + std::memcpy(snapshot->header.data(), &header, sizeof(Shader)); + return snapshot; + }; + const auto vertex = makeSnapshot(0x20000, 2); + const auto compute = makeSnapshot(0x30000, 0); + std::shared_ptr registry; + PublishRegisteredShader(registry, vertex); + PublishRegisteredShader(registry, compute); + Require(RegisteredProgram(*registry, 0x10000, {2}) == vertex && RegisteredProgram(*registry, 0x10000, {0}) == compute, "shared code selected another registered type"); + Reject([&] { static_cast(RegisteredProgram(*registry, 0x10000, {1})); }); + Require(RegisteredProgram(*registry, 0x10100, {2}) == nullptr, "an out-of-range entry selected registered code"); + const auto copy = makeSnapshot(0x40000, 2); + PublishRegisteredShader(registry, copy); + Require(RegisteredProgram(*registry, 0x10000, {2}) == copy, "equivalent registered headers became ambiguous"); + PublishRegisteredShader(registry, makeSnapshot(vertex->headerAddress, 2)); + Require(RegisteredProgram(*registry, 0x10000, {2}) == vertex && registry->at(0x10000).size() == 3, "identical earlier registration did not restore its prepared snapshot as current"); + const auto replacement = makeSnapshot(copy->headerAddress, 2); + Shader changed; + std::memcpy(&changed, replacement->header.data(), sizeof(Shader)); + changed.target = 1; + std::memcpy(replacement->header.data(), &changed, sizeof(Shader)); + PublishRegisteredShader(registry, replacement); + Require(RegisteredProgram(*registry, 0x10000, {2}) == replacement, "same-type registration did not keep the latest program metadata"); + const auto geometry = makeSnapshot(0x60000, 4); + PublishRegisteredShader(registry, geometry); + 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{}; + std::array registers{{{0x100, 0x11}, {0x108, 0x100}, {0x109, 0}, {0x10a, 0x2}, {0x10b, 0x10}, {0x148, 0}, {0x149, 0}}}; + ShaderSpecialRegs specials{}; + } back; + back.shader.file_header = 0x34333231u; + back.shader.version = 0x18; + back.shader.header_size = sizeof(Hull); + back.shader.shader_size = 4; + back.shader.code = reinterpret_cast(std::uintptr_t{0x50000}); + back.shader.type = 7; + back.shader.sh_registers = back.registers.data(); + back.shader.num_sh_registers = back.registers.size(); + back.shader.specials = &back.specials; + auto snapshot = std::make_shared(); + snapshot->codeAddress = 0x50000; + snapshot->headerAddress = reinterpret_cast(&back); + snapshot->type = 7; + snapshot->code = {0xbf810000u}; + snapshot->header.resize(sizeof(Hull)); + std::memcpy(snapshot->header.data(), &back, sizeof(Hull)); + std::shared_ptr registry; + PublishRegisteredShader(registry, snapshot); + auto registers = back.registers; + registers[0].value = 0x22; + registers[3].value = 0x3; + registers[4].value = 0x1e; + registers[5].value = 0x600; + registers[6].value = 0x1; + Shader fused = back.shader; + fused.type = 3; + fused.sh_registers = registers.data(); + fused.user_data = nullptr; + Require(RegisteredHeader(*registry, &fused) == snapshot, "a fused hull header did not select its registered back half"); + registers[1].value = 0x200; + Require(RegisteredHeader(*registry, &fused) == nullptr, "a fused hull header with another hull program selected the back half"); + registers[1].value = 0x100; + registers[2].offset = 0x10c; + Require(RegisteredHeader(*registry, &fused) == nullptr, "a fused hull header with other registers selected the back half"); + registers[2].offset = 0x109; + fused.target ^= 1u; + Require(RegisteredHeader(*registry, &fused) == nullptr, "a fused hull header with another target selected the back half"); + fused.target ^= 1u; + fused.type = 2; + Require(RegisteredHeader(*registry, &fused) == nullptr, "a vertex header selected a registered hull back half"); +} + void Registration(AgcDriver::VulkanDevice& device) { const std::array code{0xbf810000u}; RecompileRequest request{{ShaderStage::Compute, 0x10000, code, 0, {}}, {32, 0, {}, ShaderComputeStageInfo{{1, 1, 1}, 0, {false, false, false}, false, 0, {}}, {}, {}, {}}, device.ComputeTarget(32), {0, 0, 0, 128}}; @@ -130,7 +292,7 @@ void Registration(AgcDriver::VulkanDevice& device) { for (unsigned iteration = 0; iteration < 40; ++iteration) { ShaderPreparationTransaction transaction; PublishRegisteredShader(registry, makeSnapshot()); - const auto current = registry->at(0x10000); + const auto current = registry->at(0x10000).back(); Require(current == original && current->prepared->rectangleRequested, "identical registration replaced a prepared snapshot"); auto& entries = transaction.Edit(*current).entries; entries.push_back({iteration + 1, handle}); @@ -160,11 +322,11 @@ void Registration(AgcDriver::VulkanDevice& device) { transaction.Commit(); } replacementWriter.get(); - Require(registry->at(0x10000) == replacement && submission->at(0x10000) == original, "replacement modified an in-flight registry"); + Require(registry->at(0x10000).back() == replacement && submission->at(0x10000).back() == original, "replacement modified an in-flight registry"); Require(original->prepared->entries.size() == 162 && replacement->prepared->entries.empty(), "stale preparation was published into a replacement snapshot"); original.reset(); - request.shader.code = submission->at(0x10000)->code; - const auto invocation = InvocationFor(*submission->at(0x10000), 0, request); + request.shader.code = submission->at(0x10000).back()->code; + const auto invocation = InvocationFor(*submission->at(0x10000).back(), 0, request); const auto capture = invocation.Capture({}); const auto result = invocation.Materialize(*capture); Require(result->variantId == GetPreparedArtifact(*handle).variantId, "old submission lost its prepared artifact"); @@ -172,19 +334,76 @@ void Registration(AgcDriver::VulkanDevice& device) { device.WaitIdle(); Require(!old.expired(), "in-flight submission released its snapshot early"); submission.reset(); - Require(old.expired(), "completed submission retained an obsolete snapshot"); + Require(!old.expired() && registry->at(0x10000).size() == 2, "registration discarded an earlier header for shared code"); Require(invocation.Materialize(*capture)->spirv.data() == result->spirv.data(), "prepared handle did not retain its artifact"); auto changed = makeSnapshot(); changed->headerAddress = replacement->headerAddress; changed->code[0] ^= 1; PublishRegisteredShader(registry, changed); - Require(registry->at(0x10000) == changed, "changed shader code was treated as identical"); + Require(registry->at(0x10000).back() == changed && registry->at(0x10000).size() == 1 && old.expired(), "changed shader code retained obsolete headers"); auto metadata = makeSnapshot(); metadata->headerAddress = changed->headerAddress; metadata->code = changed->code; metadata->header.push_back(std::byte{1}); PublishRegisteredShader(registry, metadata); - Require(registry->at(0x10000) == metadata, "changed shader metadata was treated as identical"); + Require(registry->at(0x10000).back() == metadata, "changed shader metadata was treated as identical"); +} + +void TransactionLock() { + ShaderSnapshot shader{}; + const auto publish = [&] { + ShaderPreparationTransaction other; + other.Edit(shader).rectangleRequested = true; + other.Commit(); + }; + std::optional open(std::in_place); + auto unread = std::async(std::launch::async, publish); + const bool unblocked = unread.wait_for(std::chrono::seconds(10)) == std::future_status::ready; + if (!unblocked) open.reset(); + unread.get(); + Require(unblocked, "an open transaction that read nothing blocked another one"); + static_cast(open->Read(shader)); + auto waiting = std::async(std::launch::async, publish); + const bool excluded = waiting.wait_for(std::chrono::milliseconds(200)) == std::future_status::timeout; + open->Commit(); + open.reset(); + waiting.get(); + Require(excluded, "a transaction ran beside one that had read prepared state"); +} + +void PreparationPriority(bool beforeSubmit) { + const auto workers = std::max(2u, std::thread::hardware_concurrency() / 2u); + std::barrier started(static_cast(workers + 1u)); + std::counting_semaphore<> release(0); + std::latch finished(static_cast(workers)); + for (unsigned index = 0; index < workers; ++index) { + SubmitRegistrationPreparation([&] { + started.arrive_and_wait(); + release.acquire(); + finished.count_down(); + }); + } + started.arrive_and_wait(); + PreparedShaders prepared; + std::atomic order{0}; + std::promise background; + std::promise demanded; + auto backgroundDone = background.get_future(); + auto demandedDone = demanded.get_future(); + SubmitRegistrationPreparation([&] { background.set_value(order.fetch_add(1u)); }); + if (beforeSubmit) PrioritizeRegistrationPreparation(prepared); + SubmitRegistrationPreparation([&] { demanded.set_value(order.fetch_add(1u)); }, &prepared); + if (!beforeSubmit) { + PrioritizeRegistrationPreparation(prepared); + PrioritizeRegistrationPreparation(prepared); + } + release.release(); + const bool settled = demandedDone.wait_for(std::chrono::seconds(5)) == std::future_status::ready; + const bool backgroundSettled = backgroundDone.wait_for(std::chrono::seconds(5)) == std::future_status::ready; + release.release(static_cast(workers - 1u)); + finished.wait(); + Require(settled && backgroundSettled, "preparation queue did not complete its work"); + Require(demandedDone.get() == 0u && backgroundDone.get() == 1u && order.load() == 2u, "demanded preparation waited behind background work or ran twice"); } } @@ -192,6 +411,12 @@ void Registration(AgcDriver::VulkanDevice& device) { int main() { try { Transactions(); + HeaderSelection(); + RegistryVersions(); + FusedHullHeaderSelection(); + TransactionLock(); + PreparationPriority(false); + PreparationPriority(true); auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; Registration(*device); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index 977f26c1a9..734a7c6825 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -1,11 +1,25 @@ #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 #include #include +#include +#include +#include #include #include #include @@ -13,6 +27,22 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#else +#include +#endif namespace { @@ -58,18 +88,41 @@ void RunUnregistered(AgcDriver::VulkanDevice& device, ShaderRecompiler::Recompil } } +void NullPixelAtDraw(AgcDriver::VulkanDevice& device) { + std::vector code(64, 0); + code.front() = 0xbf810000u; + AgcDriver::DriverDetail::ShaderSnapshot snapshot{AgcDriver::DriverDetail::NullPixelProgramAddress(), 0, 1, code, {}}; + snapshot.header.resize(sizeof(Shader)); + std::array users{}; + const auto pixel = AgcDriver::Graphics::DecodePixelStageInfo({}, {}, true); + const ShaderRecompiler::RecompileRequest registered{{ShaderRecompiler::ShaderStage::Fragment, snapshot.codeAddress, snapshot.code, 0, {}}, {64, 0, {}, {}, pixel, {}, {}}, device.Target(), {0, 0, 0, 128}}; + snapshot.prepared->entries.push_back({0, ShaderRecompiler::PrepareShader(registered)}); + ShaderRecompiler::RecompileRequest request{{ShaderRecompiler::ShaderStage::Fragment, snapshot.codeAddress, snapshot.code, 0, {}}, {32, 0, users, {}, pixel, {}, {}}, device.Target(), {0, 0, 20, 108}}; + static_cast(AgcDriver::DriverDetail::InvocationFor(snapshot, 0, request)); + Require(snapshot.prepared->entries.size() == 2, "the null pixel program was not prepared at draw"); + static_cast(AgcDriver::DriverDetail::InvocationFor(snapshot, 0, request)); + Require(AgcDriver::DriverDetail::SourceHandleFor(snapshot, 0, request) == snapshot.prepared->entries.back().handle && snapshot.prepared->entries.size() == 2, "the null pixel program was prepared again for the same draw"); +} + void Run(AgcDriver::VulkanDevice& device) { + NullPixelAtDraw(device); alignas(256) std::array code{0xbf810000u}; std::array users{}; 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, users, compute, {}, {}, {}}, device.ComputeTarget(32), {0, 0, 0, 128}}; RunUnregistered(device, request); + { + AgcDriver::DriverDetail::ShaderSnapshot unpredicted{request.shader.codeAddress, 0, 0, {code.begin(), code.end()}, {}}; + unpredicted.header.resize(sizeof(Shader)); + const auto prepared = AgcDriver::DriverDetail::SourceHandleFor(unpredicted, 0, request); + Require(unpredicted.prepared->entries.size() == 1 && unpredicted.prepared->entries.front().handle == prepared, "a registered shader with no matching artifact was not prepared"); + Require(AgcDriver::DriverDetail::SourceHandleFor(unpredicted, 0, request) == prepared && unpredicted.prepared->entries.size() == 1, "an artifact prepared at a dispatch was not reused"); + } AgcDriver::DriverDetail::ShaderSnapshot snapshot{request.shader.codeAddress, 0, 0, {code.begin(), code.end()}, {}}; snapshot.header.resize(sizeof(Shader)); - ExpectFailure([&] { static_cast(AgcDriver::DriverDetail::SourceHandleFor(snapshot, 0, request)); }, "artifact is missing"); const auto handle = ShaderRecompiler::PrepareShader(request); snapshot.prepared->entries.push_back({0, handle}); - ExpectFailure([&] { static_cast(AgcDriver::DriverDetail::SourceHandleFor(snapshot, 1, request)); }, "artifact is missing"); + ExpectFailure([&] { static_cast(AgcDriver::DriverDetail::SourceHandleFor(snapshot, 1, request)); }, "outside the snapshot"); const auto& artifact = ShaderRecompiler::GetPreparedArtifact(*handle); for (std::uint32_t value = 0; value < 8; ++value) { users.fill(value); @@ -90,7 +143,10 @@ void Run(AgcDriver::VulkanDevice& device) { ExpectFailure([&] { static_cast(AgcDriver::DriverDetail::InvocationFor(snapshot, 0, request)); }, "does not refer to registered code"); ExpectFailure([&] { static_cast(AgcDriver::DriverDetail::InvocationFor(snapshot, 1, registeredRequest)); }, "outside the snapshot"); registeredRequest.context.compute->numThreads[0] = 2; - ExpectFailure([&] { static_cast(AgcDriver::DriverDetail::InvocationFor(snapshot, 0, registeredRequest)); }, "artifact is missing"); + static_cast(AgcDriver::DriverDetail::InvocationFor(snapshot, 0, registeredRequest)); + Require(snapshot.prepared->entries.size() == 2, "a dispatch state that registration did not predict was not prepared"); + static_cast(AgcDriver::DriverDetail::InvocationFor(snapshot, 0, registeredRequest)); + Require(snapshot.prepared->entries.size() == 2, "an artifact prepared at a dispatch was not reused"); ShaderRecompiler::SrtRuntime preparedRuntime{}; preparedRuntime.userData = users; const auto preparedCapture = invocation.Capture(preparedRuntime); @@ -116,12 +172,13 @@ void Run(AgcDriver::VulkanDevice& device) { replacement->Dispatch(*replacementResult, 1, 1, 1); replacement->WaitIdle(); replacementRequest.target.nonConstantImageOffsets = !replacementRequest.target.nonConstantImageOffsets; - ExpectFailure([&] { static_cast(AgcDriver::DriverDetail::InvocationFor(snapshot, 0, replacementRequest)); }, "artifact is missing"); + static_cast(AgcDriver::DriverDetail::InvocationFor(snapshot, 0, replacementRequest)); + Require(snapshot.prepared->entries.size() == 3, "a target that registration did not prepare for was not prepared"); ShaderRecompiler::SrtRuntime runtime{}; runtime.userData = users; const auto capture = ShaderRecompiler::CaptureResources(request, runtime, *handle); request.context.compute->numThreads[0] = 2; - ExpectFailure([&] { static_cast(AgcDriver::DriverDetail::SourceHandleFor(snapshot, 0, request)); }, "artifact is missing"); + Require(AgcDriver::DriverDetail::SourceHandleFor(snapshot, 0, request) == snapshot.prepared->entries[1].handle, "a dispatch did not find the artifact prepared for its state"); ExpectFailure([&] { static_cast(ShaderRecompiler::CaptureResources(request, runtime, *handle)); }, "does not match the static ABI"); ExpectFailure([&] { static_cast(ShaderRecompiler::MaterializeShader(request, *capture, *handle)); }, "does not match the static ABI"); const auto otherHandle = ShaderRecompiler::PrepareShader(request); @@ -132,10 +189,72 @@ void Run(AgcDriver::VulkanDevice& device) { request.layout.pushConstantSizeBytes = 124; Require(AgcDriver::DriverDetail::SourceHandleFor(snapshot, 0, request) == handle, "compatible push constant capacity discarded the prepared artifact"); request.layout.pushConstantSizeBytes = 126; - ExpectFailure([&] { static_cast(AgcDriver::DriverDetail::SourceHandleFor(snapshot, 0, request)); }, "artifact is missing"); + ExpectFailure([&] { static_cast(AgcDriver::DriverDetail::SourceHandleFor(snapshot, 0, request)); }, "not dword-aligned"); request.layout.pushConstantSizeBytes = 128; code[0] = 0xffffffffu; - ExpectFailure([&] { static_cast(AgcDriver::DriverDetail::SourceHandleFor(snapshot, 0, request)); }, "artifact is missing"); + ExpectFailure([&] { static_cast(AgcDriver::DriverDetail::SourceHandleFor(snapshot, 0, request)); }, "does not refer to registered code"); +} + +void FailureCapture(AgcDriver::VulkanDevice& device) { + using namespace ShaderRecompiler; + const std::array code{0xffffffffu}; + const std::vector header(2048, std::byte{0x5a}); + RecompileRequest request{{ShaderStage::Compute, 0x12345cafeull, code, 0x20000, header}, + {32, 0, {}, ShaderComputeStageInfo{{1, 1, 1}, 0, {}, false, 1, {}}, {}, {}, {}}, device.ComputeTarget(32), {0, 0, 0, 128}}; + const auto failure = [&](auto action) { + try { action(); } + catch (const std::exception& error) { return std::string(error.what()); } + throw std::runtime_error("invalid shader preparation succeeded"); + }; + 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.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 + Require(unsetenv("APS5_DUMP_SHADERS") == 0, "cannot disable shader capture"); +#endif + std::ostringstream prefix; + prefix << "shader 0x" << std::hex << request.shader.codeAddress << " stage " << static_cast(request.shader.stage) << ": "; + const auto diagnosed = failure([&] { static_cast(AgcDriver::DriverDetail::PrepareShaderWithDiagnostics(request)); }); + Require(diagnosed.rfind(prefix.str(), 0) == 0, "diagnosed failure names no shader address and stage"); + Require(diagnosed.substr(prefix.str().size()) == expected, "diagnosed failure changed the reason"); + Require(!std::ifstream(path, std::ios::binary).is_open(), "disabled shader capture created a request"); +#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 + const auto actual = failure([&] { static_cast(AgcDriver::DriverDetail::PrepareShaderWithDiagnostics(request)); }); + 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 + Require((previous != nullptr ? setenv("APS5_DUMP_SHADERS", saved.c_str(), 1) : unsetenv("APS5_DUMP_SHADERS")) == 0, "cannot restore shader capture"); +#endif + std::ifstream file(path, std::ios::binary); + std::ostringstream text; + text << file.rdbuf(); + file.close(); + std::remove(path.c_str()); + const RequestSerializer serializer; + Require(text.str().size() > 1024 && text.str() == serializer.Serialize(request), "shader failure capture was incomplete"); + const auto replay = serializer.Deserialize(text.str()); + Require(serializer.Serialize(replay.request) == text.str(), "shader failure capture changed on replay"); } void PrepareMultisampledStorage(AgcDriver::VulkanDevice& device) { @@ -148,6 +267,9 @@ void PrepareMultisampledStorage(AgcDriver::VulkanDevice& device) { request.context.waveSize = 32; request.context.compute = ShaderComputeStageInfo{{8, 8, 1}, 0, {true, true, false}, false, 2, {}}; request.target = device.ComputeTarget(32); + std::vector multisampleCapabilities(request.target.supportedCapabilities.begin(), request.target.supportedCapabilities.end()); + if (std::ranges::find(multisampleCapabilities, spv::CapabilityStorageImageMultisample) == multisampleCapabilities.end()) multisampleCapabilities.push_back(spv::CapabilityStorageImageMultisample); + request.target.supportedCapabilities = multisampleCapabilities; request.layout.pushConstantSizeBytes = 128; request.useCache = false; for (std::uint32_t variant = 0; variant < 5; ++variant) { @@ -189,7 +311,192 @@ void PrepareMultisampledStorage(AgcDriver::VulkanDevice& device) { ExpectFailure([&] { static_cast(PrepareShader(request)); }, "storage image multisampling is unavailable on the target device"); } +void SettleLater(AgcDriver::DriverDetail::PreparedShaders& prepared, std::vector entries, std::exception_ptr failure) { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + { + std::lock_guard lock(prepared.mutex); + prepared.entries.insert(prepared.entries.end(), entries.begin(), entries.end()); + prepared.failure = failure; + prepared.pending = false; + } + prepared.settled.notify_all(); +} + +std::exception_ptr InjectedFailure() { + try { + throw std::runtime_error("injected registration preparation failure"); + } catch (...) { + return std::current_exception(); + } +} + +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{}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{1, 1, 1}, 0, {false, false, false}, false, 1, {}}; + const auto address = reinterpret_cast(code.data()); + for (const bool source : {true, false}) { + AgcDriver::DriverDetail::ShaderSnapshot snapshot{address, 0, 0, {code.begin(), code.end()}, {}}; + snapshot.header.resize(sizeof(Shader)); + ShaderRecompiler::RecompileRequest request{{ShaderRecompiler::ShaderStage::Compute, address, snapshot.code, 0, {}}, {32, 0, users, compute, {}, {}, {}}, device.ComputeTarget(32), {0, 0, 0, 128}}; + const auto handle = ShaderRecompiler::PrepareShader(request); + snapshot.prepared->pending = true; + auto settle = std::async(std::launch::async, SettleLater, std::ref(*snapshot.prepared), std::vector{{0, handle}}, nullptr); + if (source) Require(AgcDriver::DriverDetail::SourceHandleFor(snapshot, 0, request) == handle, "first use did not wait for registration preparation"); + else static_cast(AgcDriver::DriverDetail::InvocationFor(snapshot, 0, request)); + settle.get(); + Require(snapshot.prepared->entries.size() == 1, "first use prepared a pending registration again"); + } + AgcDriver::DriverDetail::ShaderSnapshot snapshot{address, 0, 0, {code.begin(), code.end()}, {}}; + snapshot.header.resize(sizeof(Shader)); + snapshot.prepared->pending = true; + auto settle = std::async(std::launch::async, SettleLater, std::ref(*snapshot.prepared), std::vector{}, InjectedFailure()); + { + AgcDriver::DriverDetail::ShaderPreparationTransaction transaction; + ExpectFailure([&] { static_cast(transaction.Read(snapshot)); }, "injected registration preparation failure"); + } + settle.get(); + ShaderRecompiler::RecompileRequest request{{ShaderRecompiler::ShaderStage::Compute, address, snapshot.code, 0, {}}, {32, 0, users, compute, {}, {}, {}}, device.ComputeTarget(32), {0, 0, 0, 128}}; + ExpectFailure([&] { static_cast(AgcDriver::DriverDetail::SourceHandleFor(snapshot, 0, request)); }, "injected registration preparation failure"); + ExpectFailure([&] { static_cast(AgcDriver::DriverDetail::InvocationFor(snapshot, 0, request)); }, "injected registration preparation failure"); +} + +void UnsupportedTypeRegistration() { + alignas(256) static const std::array code{0xbf810000u}; + struct Header { + Shader shader{}; + std::array registers{{{0, 0x1218}, {1, 0x40104004}, {2, 0xa0}, {5, 0}, {3, 6}, {4, 0xc}, {6, 0}, {7, 0}}}; + } 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.type = 8; + AgcDriverRegisterShader_nid_postfix(&header.shader); + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +void SharedCodeHeaders() { + 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}}}; + } vertex, mesh; + const auto address = reinterpret_cast(code.data()); + for (auto* header : {&vertex, &mesh}) { + 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->shader.user_data = &header->users; + header->specials.dispatch_modifier = 0x8000; + } + vertex.shader.cx_registers = vertex.context.data(); + vertex.shader.num_cx_registers = vertex.context.size(); + vertex.shader.type = 2; + vertex.registers = {{{0xc8, static_cast(address >> 8u)}, {0xc9, static_cast(address >> 40u)}, {0x8b, 0}, {0x8a, 0}, {0xca, 0}, {0xcb, 0}, {0xcc, 0}}}; + mesh.shader.type = 4; + mesh.registers = vertex.registers; + const std::array primitive{{{0x242, 4}}}; + AgcDriverRegisterShader_nid_postfix(&vertex.shader); + AgcDriverRegisterShader_nid_postfix(&mesh.shader); + AgcDriverResolveShaderAbi_nid_postfix(&vertex.shader, {}, primitive); + AgcDriverRegisterShader_nid_postfix(&mesh.shader); + Shader copy = vertex.shader; + copy.user_data = nullptr; + AgcDriverResolveShaderAbi_nid_postfix(©, {}, primitive); + auto moved = vertex.registers; + copy.sh_registers = moved.data(); + AgcDriverResolveShaderAbi_nid_postfix(©, {}, primitive); + moved[2].value ^= 2u; + ExpectFailure([&] { AgcDriverResolveShaderAbi_nid_postfix(©, {}, primitive); }, "replaced shader header"); + copy.sh_registers = vertex.shader.sh_registers; + copy.target ^= 1u; + ExpectFailure([&] { AgcDriverResolveShaderAbi_nid_postfix(©, {}, primitive); }, "replaced shader header"); + std::vector commands; + for (const auto reg : vertex.registers) commands.insert(commands.end(), {0xc0017600u, reg.offset, reg.value}); + for (const auto reg : vertex.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, {}}; + for (unsigned iteration = 0; iteration < 2; ++iteration) { + Require(sceAgcDriverSubmitDcb(&packet) == 0, "shared-code draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + } + mesh.shader.type = 2; + mesh.shader.cx_registers = mesh.context.data(); + mesh.shader.num_cx_registers = mesh.context.size(); + AgcDriverRegisterShader_nid_postfix(&mesh.shader); + AgcDriverResolveShaderAbi_nid_postfix(&mesh.shader, {}, primitive); + AgcDriverResolveShaderAbi_nid_postfix(&vertex.shader, {}, primitive); + Require(sceAgcDriverSubmitDcb(&packet) == 0, "same-type registered header draw failed"); + AgcDriverWaitIdle_nid_postfix(); + AgcDriverRegisterShader_nid_postfix(&vertex.shader); + AgcDriverResolveShaderAbi_nid_postfix(&vertex.shader, {}, primitive); + Require(sceAgcDriverSubmitDcb(&packet) == 0, "re-registered earlier header draw failed"); + AgcDriverWaitIdle_nid_postfix(); +} + +void RegistrationWithoutSpecials() { + alignas(256) std::array code{0xbf810000u}; + struct Header { + Shader shader{}; + std::array registers{}; + } 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.registers = {{{0x20c, static_cast(address >> 8u)}, {0x20d, static_cast(address >> 40u)}, {0x207, 1}, {0x208, 1}, {0x209, 1}, {0x212, 0}, {0x213, 0}}}; + AgcDriverRegisterShader_nid_postfix(&header.shader); + std::vector commands; + for (const auto reg : header.registers) commands.insert(commands.end(), {0xc0017600u, reg.offset, reg.value}); + for (const std::uint32_t initiator : {0x8041u, 0x41u}) commands.insert(commands.end(), {0xc0031500u, 1, 1, 1, initiator}); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + sceAgcDriverSubmitAcb(0x20, &packet); + AgcDriverWaitIdle_nid_postfix(); +} + void Registration(bool indirect) { + UnsupportedTypeRegistration(); alignas(256) std::array code{0xbf810000u}; struct Header { Shader shader{}; @@ -224,6 +531,17 @@ void Registration(bool indirect) { })); } for (auto& registration : registrations) registration.get(); + Shader copy = header.shader; + copy.user_data = reinterpret_cast(©); + AgcDriverResolveShaderAbi_nid_postfix(©, {}, {}); + alignas(Shader) std::array padded{}; + auto* unaligned = padded.data() + 4; + Require(reinterpret_cast(unaligned) % alignof(Shader) != 0, "shader ABI test header is unexpectedly aligned"); + std::memcpy(unaligned, ©, sizeof(Shader)); + padded.back() = std::byte{0x7d}; + AgcDriverResolveShaderAbi_nid_postfix(reinterpret_cast(unaligned), {}, {}); + copy.target ^= 1u; + ExpectFailure([&] { AgcDriverResolveShaderAbi_nid_postfix(©, {}, {}); }, "replaced shader header"); header.registers[1].value |= 0x100u; ExpectFailure([&] { AgcDriverRegisterShader_nid_postfix(&header.shader); }, "invalid registered program address"); header.registers[1].value &= 0xffu; @@ -255,23 +573,752 @@ void Registration(bool indirect) { commands.insert(commands.end(), {0xc0033700u, 0x00100200u, static_cast(destinationAddress), static_cast(destinationAddress >> 32u), 7}); packet = Packet{commands.data(), static_cast(commands.size()), 0, {}}; sceAgcDriverSubmitAcb(0x20, &packet); - ExpectFailure([] { AgcDriverWaitIdle_nid_postfix(); }, "artifact is missing"); - ExpectFailure([] { AgcDriverWaitIdle_nid_postfix(); }, "artifact is missing"); - ExpectFailure([] { AgcDriverShutdown_nid_postfix(); }, "artifact is missing"); + AgcDriverWaitIdle_nid_postfix(); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (std::atomic_ref(destination).load() != 7 && std::chrono::steady_clock::now() < deadline) std::this_thread::yield(); + Require(destination == 7, "a dispatch whose state registration did not predict did not run"); + destination = 0; + alignas(256) static const std::array invalid{0xffffffffu}; + const auto invalidAddress = reinterpret_cast(invalid.data()); + commands[2] = static_cast(invalidAddress >> 8u); + commands[5] = static_cast(invalidAddress >> 40u); + packet = Packet{commands.data(), static_cast(commands.size()), 0, {}}; + sceAgcDriverSubmitAcb(0x20, &packet); + ExpectFailure([] { AgcDriverWaitIdle_nid_postfix(); }, ""); + ExpectFailure([] { AgcDriverWaitIdle_nid_postfix(); }, ""); + ExpectFailure([] { AgcDriverShutdown_nid_postfix(); }, ""); Require(destination == 0, "failed dispatch executed a subsequent memory write"); } +void RegisteredVertexWorkgroup(const ShaderRecompiler::SpirvTarget& target, bool registrationOnly = false) { + if (!registrationOnly && ((target.subgroupSize != 32u && target.subgroupSize != 64u) || std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), spv::CapabilityMeshShadingEXT) == target.supportedCapabilities.end())) { + std::cout << "registered NGG wave64 preparation not run: requires mesh shading and a 32- or 64-lane host subgroup; subgroup=" << target.subgroupSize << '\n'; + return; + } + alignas(256) static constexpr std::array code{ + 0xd8340000u, 0u, 0xd7600000u, 0x00017f05u, 0x7e020200u, 0xf80008cfu, 0x04030201u, 0xbf810000u + }; + struct Header { + Shader shader{}; + std::array registers{}; + std::array context{{{0x1ff, 64}, {0x2ab, 4}, {0x2ce, 0}}}; + ShaderSpecialRegs specials{{0x25b, 0x8040}, {0x2d5, 0x2000}, 0, {}, {}, {0x29b, 2}, {0x260, 0}}; + ShaderUserData userData{}; + } 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.type = 2; + header.shader.user_data = &header.userData; + 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.registers = {{{0xc8, static_cast(address >> 8u)}, {0xc9, static_cast(address >> 40u)}, {0x8a, 0x622c0187}, {0x8b, 5u << 19u}}}; + AgcDriverRegisterShader_nid_postfix(&header.shader); + if (registrationOnly) { + header.shader.user_data = nullptr; + AgcDriverRegisterShader_nid_postfix(&header.shader); + 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 && 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'; + return; + } + const std::array primitive{{{0x242, 4}}}; + AgcDriverResolveShaderAbi_nid_postfix(&header.shader, {}, primitive); + AgcDriverResolveShaderAbi_nid_postfix(&header.shader, {}, primitive); + 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 { + 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, 1}, {0x208, 1}, {0x209, 1}, {0x212, 0}, {0x213, 12}, {0x207, 1}}}; + AgcDriverRegisterShader_nid_postfix(&header.shader); + std::vector commands; + for (const auto reg : header.registers) commands.insert(commands.end(), {0xc0017600u, reg.offset, reg.value}); + commands.insert(commands.end(), {0xc0031500u, 1, 1, 1, 0x8041}); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + sceAgcDriverSubmitAcb(0x20, &packet); + ExpectFailure([] { AgcDriverWaitIdle_nid_postfix(); }, "not statically resolvable"); + ExpectFailure([] { AgcDriverShutdown_nid_postfix(); }, "not statically resolvable"); +} + +void DeferredUndecodableRegistration() { + alignas(256) std::array code{0xbe842104u, 0xf1949f05u, 0x00060018u}; + 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, 1}, {0x208, 1}, {0x209, 1}, {0x212, 0}, {0x213, 12}, {0x207, 1}}}; + AgcDriverRegisterShader_nid_postfix(&header.shader); + std::vector commands; + for (const auto reg : header.registers) commands.insert(commands.end(), {0xc0017600u, reg.offset, reg.value}); + commands.insert(commands.end(), {0xc0031500u, 1, 1, 1, 0x8041}); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + sceAgcDriverSubmitAcb(0x20, &packet); + ExpectFailure([] { AgcDriverWaitIdle_nid_postfix(); }, "unsupported MIMG opcode"); + 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")), "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; + RegisteredVertexWorkgroup(device->Target(), true); + AgcDriverShutdown_nid_postfix(); + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--deferred") { + if (!OpenVulkanTestDevice()) return VulkanTestSkipped; + DeferredRegistration(); + std::cout << "deferred shader preparation tests passed\n"; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--deferred-undecodable") { + if (!OpenVulkanTestDevice()) return VulkanTestSkipped; + DeferredUndecodableRegistration(); + std::cout << "deferred undecodable shader tests passed\n"; + return 0; + } auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; Run(*device); + 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"; return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedUnregisteredGraphics.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedUnregisteredGraphics.cpp new file mode 100644 index 0000000000..df68d87509 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedUnregisteredGraphics.cpp @@ -0,0 +1,104 @@ +#include "VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp" +#include "SceShaders.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "Optimization/ResourceProgram.hpp" +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +alignas(256) constexpr std::array VertexCode{0xbf810000u}; +alignas(256) constexpr std::array PixelCode{0xc8020002u, 0xc8060102u, 0xc80a0202u, 0xc80e0302u, 0xf800180fu, 0x03020100u, 0xbf810000u}; + +void Bind(AgcDriver::QueueState& queue, const void* code, std::uint32_t program, std::uint32_t resources) { + const auto address = reinterpret_cast(code); + queue.shader[program] = static_cast(address >> 8u); + queue.shader[program + 1u] = static_cast(address >> 40u); + queue.shader[resources] = 16u << 1u; +} + +void Check(AgcDriver::VulkanDevice& device) { + using namespace AgcDriver::DriverDetail; + using namespace ShaderRecompiler; + AgcDriver::QueueState queue{}; + queue.context[0x8e] = 0xfu; + queue.context[0x8f] = 0xfu; + Bind(queue, VertexCode.data(), 0xc8u, 0x8bu); + Bind(queue, PixelCode.data(), 0x8u, 0xbu); + DrawDecode draw{}; + draw.state.stages.path = AgcDriver::Graphics::ShaderPath::Vertex; + draw.state.stages.vertexWaveSize = 32u; + draw.state.stages.fragmentWaveSize = 32u; + draw.pixel.wave32 = true; + draw.pixel.interpolatorCount = 2; + draw.pixel.interpolatorSettings[0] = 0x403u; + draw.pixel.interpolatorSettings[1] = 0x220u; + draw.pixel.targetOutputMode[0] = 9; + draw.pixel.targetExportMapping.fill(0xe4u); + const ShaderRegistry registry; + DecodeGraphicsPrograms(draw, queue, registry, false, true); + 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"); + 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"); + } + 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; + 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}); + memory.insert(memory.end(), program.memory.begin(), program.memory.end()); + } + std::array variants{}; + for (std::size_t index = 0; index < draw.programs.size(); ++index) { + const auto& program = draw.programs[index]; + const bool pixel = program.binary.stage == ShaderStage::Fragment; + std::optional vertex; + if (!pixel) vertex = AgcDriver::Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, nullptr, true); + RecompileRequest request{program.binary, {32u, program.firstUserSgpr, program.userData, {}, pixel ? std::optional(draw.pixel) : std::nullopt, vertex, memory}, target, {0, 0, 0, 128u}, GraphicsCompileContext{program.firstUserSgpr, linked, std::nullopt, std::nullopt, {0, 3, 4, 1}}}; + static_cast(InvocationFor(*program.snapshot, program.codeOffset, request)); + Require(program.snapshot->prepared->entries.size() == 1, "an unregistered graphics program was not prepared at first use"); + variants[index] = GetPreparedArtifact(*program.snapshot->prepared->entries.front().handle).variantId; + static_cast(InvocationFor(*program.snapshot, program.codeOffset, request)); + Require(program.snapshot->prepared->entries.size() == 1, "a prepared unregistered graphics program was prepared again"); + request.layout.pushConstantOffsetBytes = 4; + request.layout.pushConstantSizeBytes -= 4; + static_cast(InvocationFor(*program.snapshot, program.codeOffset, request)); + Require(program.snapshot->prepared->entries.size() == 1, "a relocated push-constant block prepared an unregistered graphics program again"); + } + ResolvePreparedGraphics(*draw.programs[0].snapshot, draw.programs[1].snapshot, 17, target); + const auto rectangle = PreparedRectangle(*draw.programs[0].snapshot, variants[0], variants[1]); + Require(!rectangle.control.spirv.empty() && !rectangle.evaluation.spirv.empty(), "unregistered graphics programs did not prepare rectangle shaders"); +} + +} + +int main() { + try { + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Check(*device); + std::cout << "prepared unregistered graphics 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/ProvokingVertex.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ProvokingVertex.cpp new file mode 100644 index 0000000000..79cdc00701 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ProvokingVertex.cpp @@ -0,0 +1,185 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "VulkanTestDevice.hpp" +#include "ProvokingVertex_vert_spv.h" +#include "ProvokingVertex_frag_spv.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +constexpr std::uint32_t Panel = 64; +constexpr std::uint32_t Width = Panel * 3; +constexpr std::uint32_t Height = Panel; +constexpr std::byte Sentinel{0x40}; +alignas(256) std::array Pixels{}; +using Color = std::array; +constexpr Color Red{255, 0, 0, 255}; +constexpr Color Green{0, 255, 0, 255}; +constexpr Color Blue{0, 0, 255, 255}; +constexpr Color Yellow{255, 255, 0, 255}; +constexpr Color Magenta{255, 0, 255, 255}; + +ShaderRecompiler::RecompileResult Shader(std::span words, std::uint64_t variant) { + ShaderRecompiler::RecompileResult result{}; + result.spirv = std::vector(words.begin(), words.end()); + result.variantId = variant; + return result; +} + +ShaderRecompiler::RecompileResult GuestPixel(const ShaderRecompiler::SpirvTarget& target) { + alignas(256) static constexpr std::array code{0xc8120002u, 0xc8160102u, 0xc81a0202u, 0xc81e0302u, 0xf800180fu, 0x07060504u, 0xbf810000u}; + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.interpolatorCount = 1; + pixel.interpolatorSettings[0] = 0x400u; + pixel.inputAddr = 2; + pixel.hasPerspectiveCenterVgpr = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + ShaderRecompiler::RecompileRequest request{}; + request.shader = {ShaderRecompiler::ShaderStage::Fragment, reinterpret_cast(code.data()), code, 0, {}}; + request.context.waveSize = 64; + request.context.pixel = pixel; + request.target = target; + request.layout = PixelPushLayout(0u, target); + request.useCache = false; + auto result = ShaderRecompiler::Recompile(request); + result.variantId = 3; + Require(result.fragmentParameters.size() == 1 && result.fragmentParameters[0].flat && !result.fragmentParameters[0].perVertex, "guest P0 shader did not produce a flat input"); + return result; +} + +void CheckPixel(std::uint32_t x, std::uint32_t y, const Color& expected, const std::string& name) { + for (std::uint32_t channel = 0; channel < expected.size(); ++channel) { + const auto actual = std::to_integer(Pixels[(y * Width + x) * 4 + channel]); + Require(actual == expected[channel], name + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(actual) + ", expected " + std::to_string(expected[channel])); + } +} + +void Run(AgcDriver::VulkanDevice& device, VkPrimitiveTopology topology, std::span shaders, bool useLast = true, std::string_view shaderName = "GLSL") { + 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 = topology; + state.scissor = {{0, 0}, {Width, Height}}; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + const bool strip = topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; + const AgcDriver::Pm4::DrawParameters draw{0, strip ? 4u : 6u, 0, 1, 0, false}; + for (std::uint32_t panel = 0; panel < 3; ++panel) { + state.viewport = {static_cast(panel * Panel), static_cast(Height), static_cast(Panel), -static_cast(Height), 0, 1}; + state.provokingVertexMode = panel == 1 && useLast ? VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT : VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT; + device.Draw(state, draw, shaders); + } + device.WaitIdle(); + const auto name = std::string(shaderName) + (strip ? " triangle strip" : " triangle list"); + for (std::uint32_t panel = 0; panel < 3; ++panel) { + const bool last = panel == 1 && useLast; + const auto firstTriangle = last ? Blue : Red; + const auto secondTriangle = strip ? (last ? Yellow : Green) : (last ? Magenta : Yellow); + for (std::uint32_t offset = 0; offset < 4; ++offset) { + CheckPixel(panel * Panel + 16 + offset, 16, firstTriangle, name); + CheckPixel(panel * Panel + 48 - offset, 48, secondTriangle, name); + } + CheckPixel(panel * Panel, 0, {64, 64, 64, 64}, name + " background"); + } +} + +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() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto vertex = Shader(PROVOKING_VERTEX_VERT_SPV, 1); + const auto fragment = Shader(PROVOKING_VERTEX_FRAG_SPV, 2); + const std::array shaders{{ + {ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, + {ShaderRecompiler::ShaderStage::Fragment, &fragment, 0} + }}; + if (!device->ProvokingVertexLast()) { + for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP}) Run(*device, topology, shaders, false); + bool rejected = false; + try { + Run(*device, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, shaders); + } catch (const std::exception& error) { + Require(std::string(error.what()).find("last provoking vertex requires VK_EXT_provoking_vertex with provokingVertexLast enabled") != std::string::npos, std::string("unexpected last-vertex rejection: ") + error.what()); + rejected = true; + } + Require(rejected, "last provoking vertex was drawn without device support"); + device->WaitIdle(); + Require(std::getenv("ANYPS5_REQUIRE_VULKAN") == nullptr, "required provoking vertex rendering coverage is unavailable: provokingVertexLast is not supported"); + std::puts("skipped last-vertex rendering, provokingVertexLast is unavailable; first-vertex rendering and feature rejection passed"); + return VulkanTestSkipped; + } + for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP}) Run(*device, topology, shaders); + const auto guestPixel = GuestPixel(device->Target()); + const std::array guestShaders{{ + {ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, + {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"); + 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'; + return 1; + } +} 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/RecordedKeptBytes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/RecordedKeptBytes.cpp new file mode 100644 index 0000000000..f6e0f04fd7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/RecordedKeptBytes.cpp @@ -0,0 +1,317 @@ +#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/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 +#include + +namespace { + +using AgcDriver::Graphics::ColorTargetLayout; +using AgcDriver::Graphics::ColorTileMode; +using AgcDriver::Graphics::Recorder; +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::size_t BufferBytes = 65536; +constexpr std::uint32_t Width = 256; +constexpr std::uint32_t Height = 128; + +alignas(256) constexpr std::array StoreCode{ + 0x34020083, 0x7e040280, 0x7e060280, 0xe0741000, 0x80000201, 0xbf810000, +}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; + +std::uint64_t AddressOf(const void* data) { + return static_cast(reinterpret_cast(data)); +} + +std::array RawBuffer(const void* data, std::uint32_t bytes) { + const auto address = AddressOf(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array VertexBuffer(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = AddressOf(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +class GuestBuffer { +public: + explicit GuestBuffer(bool registered, std::size_t bytes = BufferBytes) : registered(registered), bytes(bytes), watched(registered && AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (watched) { + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, 65536)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); + } else { + block = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (watched) { + constexpr std::uintptr_t alignment = 65536; + void* mapped = mmap(nullptr, bytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(mapped != MAP_FAILED, "recorded kept bytes: cannot map a guest buffer"); + 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); + } else { + block = static_cast(std::aligned_alloc(65536, bytes)); + } +#endif + Require(block != nullptr, "recorded kept bytes: cannot allocate a guest buffer"); + std::memset(block, 0, bytes); + if (registered) GuestAllocations::Mutation().Add(block, bytes, true, true, true); + } + + ~GuestBuffer() { + if (registered) { + AgcDriver::Graphics::StorageTexture::FlushPending(AddressOf(block), bytes, nullptr, "test release"); + GuestAllocations::Mutation().Remove(block); + } +#ifdef _WIN32 + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(block, bytes); + GuestArena::GuestArenaRelease_nid_postfix(block, bytes); + } else { + VirtualFree(block, 0, MEM_RELEASE); + } +#else + if (watched) { + munmap(block, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, bytes); + } else { + std::free(block); + } +#endif + } + + GuestBuffer(const GuestBuffer&) = delete; + GuestBuffer& operator=(const GuestBuffer&) = delete; + + std::uint8_t* Data() { return block; } + +private: + bool registered; + std::size_t bytes; + bool watched; + std::uint8_t* block = nullptr; +}; + +Recorder& ActiveRecorder() { + auto* recorder = Recorder::Active(); + Require(recorder != nullptr, "recorded kept bytes: no active recorder"); + return *recorder; +} + +std::string Bytes(std::size_t bytes) { + return std::to_string(bytes) + " bytes"; +} + +void Dispatch(AgcDriver::VulkanDevice& device, std::uint8_t* target) { + std::vector userData(16, 0u); + const auto descriptor = RawBuffer(target, static_cast(BufferBytes)); + std::copy(descriptor.begin(), descriptor.end(), userData.begin()); + 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); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); +} + +const ColorTargetLayout TargetLayout(Width, Height, ColorTileMode::RenderTarget, 4); + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +void FillVertices(GuestBuffer& vertices) { + std::memcpy(vertices.Data(), Triangle.data(), sizeof(Triangle)); +} + +void Draw(AgcDriver::VulkanDevice& device, GuestBuffer& pixels, GuestBuffer& vertices) { + const auto target = device.Target(); + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = VertexBuffer(vertices.Data(), 16u, static_cast(BufferBytes / 16u)); + 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); + std::vector pixelUserData(4, 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, {}}, + {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 = {AddressOf(pixels.Data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, TargetLayout.Bytes(), 0xe4u, ColorTileMode::RenderTarget}; + 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); +} + +void CopiedDispatch(AgcDriver::VulkanDevice& device) { + GuestBuffer target(false); + device.WaitIdle(); + Dispatch(device, target.Data()); + const auto& recorder = ActiveRecorder(); + Require(recorder.Recording(), "recorded kept bytes: the dispatch was not left in the open batch"); + const auto kept = recorder.OpenKeptBytes(); + Require(kept >= BufferBytes, "a dispatch writing a copied " + Bytes(BufferBytes) + " buffer left " + Bytes(kept) + " in its batch's kept bytes"); + device.WaitIdle(); +} + +void BufferOnlyDispatchBounds(AgcDriver::VulkanDevice& device) { + GuestBuffer first(false); + GuestBuffer second(false); + device.WaitIdle(); + Dispatch(device, first.Data()); + auto& recorder = ActiveRecorder(); + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + recorder.Keep(std::make_shared(0), Recorder::KeptBytesBudget); + } + const auto submitted = recorder.Submissions(); + Dispatch(device, second.Data()); + Require(recorder.Submissions() > submitted, "a buffer-only dispatch did not submit the preceding batch at its kept-byte budget"); + device.WaitIdle(); +} + +void ResidentTarget(AgcDriver::VulkanDevice& device, GuestBuffer& pixels) { + GuestBuffer vertices(false); + FillVertices(vertices); + Draw(device, pixels, vertices); + device.WaitIdle(); +} + +void CopiedDraw(AgcDriver::VulkanDevice& device, GuestBuffer& pixels) { + GuestBuffer vertices(false); + FillVertices(vertices); + device.WaitIdle(); + Draw(device, pixels, vertices); + const auto& recorder = ActiveRecorder(); + Require(recorder.Recording(), "recorded kept bytes: the draw was not left in the open batch"); + const auto kept = recorder.OpenKeptBytes(); + Require(kept >= BufferBytes, "a draw reading a copied " + Bytes(BufferBytes) + " buffer left " + Bytes(kept) + " in its batch's kept bytes"); + device.WaitIdle(); +} + +bool SnapshotDraws(AgcDriver::VulkanDevice& device, GuestBuffer& pixels) { + GuestBuffer vertices(true); + FillVertices(vertices); + device.WaitIdle(); + Draw(device, pixels, vertices); + auto& recorder = ActiveRecorder(); + Require(recorder.Recording(), "recorded kept bytes: the snapshot draw was not left in the open batch"); + if (recorder.ReusableDrawSnapshot(AddressOf(vertices.Data()), BufferBytes) == nullptr) { + std::puts("the draw took no reusable snapshot (no host import, or the buffer is not write-watched): draw snapshots are not tested"); + device.WaitIdle(); + return false; + } + const auto first = recorder.OpenKeptBytes(); + Require(first >= BufferBytes, "a draw snapshotting a " + Bytes(BufferBytes) + " buffer left " + Bytes(first) + " in its batch's kept bytes"); + Draw(device, pixels, vertices); + Require(recorder.Recording(), "recorded kept bytes: the second snapshot draw was not left in the open batch"); + const auto second = recorder.OpenKeptBytes(); + Require(second == first, "a second draw reusing the batch's snapshot raised its kept bytes from " + Bytes(first) + " to " + Bytes(second)); + device.WaitIdle(); + return true; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + CopiedDispatch(*device); + BufferOnlyDispatchBounds(*device); + GuestBuffer pixels(true, TargetLayout.Bytes()); + ResidentTarget(*device, pixels); + CopiedDraw(*device, pixels); + const bool snapshots = SnapshotDraws(*device, pixels); + std::printf("recorded kept bytes tests passed%s\n", snapshots ? "" : " (draw snapshots not tested)"); + 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/RegistrationPrepareShutdown.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/RegistrationPrepareShutdown.cpp new file mode 100644 index 0000000000..94f3c46064 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/RegistrationPrepareShutdown.cpp @@ -0,0 +1,76 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libc/include/Shutdown.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("registration prepare shutdown: " + message); +} + +void CheckShutdownWaits() { + std::mutex mutex; + std::condition_variable changed; + bool started = false; + bool released = false; + std::atomic finished{false}; + std::atomic returned{false}; + std::atomic finishedAtReturn{false}; + AgcDriver::DriverDetail::Driver::Get(); + AgcDriver::DriverDetail::SubmitRegistrationPreparation([&] { + std::unique_lock lock(mutex); + started = true; + changed.notify_all(); + changed.wait(lock, [&] { return released; }); + finished.store(true); + }); + { + std::unique_lock lock(mutex); + changed.wait(lock, [&] { return started; }); + } + std::thread shutdown([&] { + LibcRunShutdown_nid_postfix(); + finishedAtReturn.store(finished.load()); + returned.store(true); + }); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + const bool returnedEarly = returned.load(); + { + std::lock_guard lock(mutex); + released = true; + } + changed.notify_all(); + shutdown.join(); + Require(!returnedEarly, "shutdown returned while a registration preparation was still running"); + Require(finishedAtReturn.load(), "shutdown returned before the running registration preparation finished"); +} + +void CheckLateSubmissionRunsInline() { + const auto caller = std::this_thread::get_id(); + std::thread::id ran; + AgcDriver::DriverDetail::SubmitRegistrationPreparation([&] { ran = std::this_thread::get_id(); }); + Require(ran == caller, "a registration preparation submitted after shutdown did not run on the caller's thread"); +} + +} + +int main() { + try { + CheckShutdownWaits(); + CheckLateSubmissionRunsInline(); + std::puts("registration prepare shutdown 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/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/RelativeAncillary.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/RelativeAncillary.cpp new file mode 100644 index 0000000000..86fc0c75af --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/RelativeAncillary.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 + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; +constexpr std::uint32_t Extent = 64u; +alignas(256) std::array pixels{}; +alignas(256) constexpr std::array, 6> positions{{ + {0.0f, 0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f, 1.0f}, + {-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 vertexCode{ + 0xe0382000u, 0x80000005u, 0xbf8c3f70u, 0xf80008cfu, 0x03020100u, 0xbf810000u +}; + +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, std::span fragmentCode, std::uint32_t offset, std::uint32_t waveSize, std::byte expected, bool reject = false, bool implicitInputs = false) { + pixels.fill(std::byte{0x40}); + const auto target = device.Target(); + const auto userData = BufferDescriptor(positions.data(), 16u, positions.size()); + const auto code = std::span(vertexCode); + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(code.data()), code, 0, {}}, + {32u, 0u, userData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, {}}, + target, {0u, 0u, 0u, 64u} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto pushBytes = static_cast(vertexResult.pushConstants.size()); + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.wave32 = waveSize == 32u; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary); + pixel.ancillary = true; + if (implicitInputs) { + pixel.inputAddr |= ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionZ) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionW); + pixel.posX = pixel.posY = pixel.posZ = pixel.posW = true; + } + pixel.targetOutputMode[0] = 9u; + pixel.targetExportMapping.fill(0xe4u); + const std::array pixelData{offset}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(fragmentCode.data()), fragmentCode, 0, {}}, + {waveSize, 0u, pixelData, std::nullopt, pixel, std::nullopt, {}}, target, PixelPushLayout(pushBytes, target) + }; + fragment.useCache = false; + const auto pixelResult = [&] { + try { + auto result = ShaderRecompiler::Recompile(fragment); + Require(!reject, "an unsupported ancillary word was accepted"); + return result; + } catch (const std::exception& error) { + if (!reject || std::string(error.what()).find("packed pixel ancillary input has an unsupported live use") == std::string::npos) throw; + return ShaderRecompiler::RecompileResult{}; + } + }(); + if (reject) return; + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0u}, {ShaderStage::Fragment, &pixelResult, PixelPushOffset(pushBytes, 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_TRIANGLE_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}; + AgcDriver::Pm4::DrawParameters draw{0u, 3u, 0u, 1u, 0u, false}; + draw.firstVertex = 3u; + draw.firstInstance = 2u; + device.Draw(state, draw, shaders); + device.WaitIdle(); + for (const auto value : pixels) Require(value == expected, "relative ancillary draw produced an incorrect pixel: " + std::to_string(std::to_integer(value))); +} + +} // namespace + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const auto waveSize : {32u, 64u}) { + const std::array read{ + 0xbefc0300u, 0x7e028700u, 0xd5480001u, 0x02111101u, + 0x7e020d01u, 0x060202f2u, 0xf800180fu, 0x01010101u + }; + auto code = std::vector(read.begin(), read.end()); + code.push_back(0xbf810000u); + std::cerr << "wave " << waveSize << " packed read\n"; + Draw(*device, code, 0u, waveSize, std::byte{255}); + Draw(*device, code, 4u, waveSize, std::byte{255}, false, true); + const std::array prefix{ + 0xd5480000u, 0x02111100u, 0x7e000d00u, 0x7e020280u, + 0x7e0402f2u, 0xbefc0300u, 0x7e008502u, 0x7e008702u + }; + code.assign(prefix.begin(), prefix.end()); + code.insert(code.end(), {0xf800180fu, 0x01010101u, 0xbf810000u}); + std::cerr << "in-range\n"; + Draw(*device, code, 1u, waveSize, std::byte{255}); + std::cerr << "out-of-range\n"; + Draw(*device, code, 8u, waveSize, std::byte{0}); + code[6] = 0x7e02d100u; + code.erase(code.begin() + 7); + std::cerr << "in-range\n"; + Draw(*device, code, 0x00010001u, waveSize, std::byte{255}); + std::cerr << "out-of-range\n"; + Draw(*device, code, 0x00010008u, waveSize, std::byte{0}); + code.assign(prefix.begin(), prefix.begin() + 6); + code.insert(code.end(), {0xbefe0480u, 0x7e008502u, 0xbefe04c1u, 0xf800180fu, 0x01010101u, 0xbf810000u}); + std::cerr << "inactive write\n"; + Draw(*device, code, 1u, waveSize, std::byte{0}); + for (const auto modifier : {0x0000e402u, 0xff00e402u}) { + code.assign(prefix.begin(), prefix.begin() + 6); + code.insert(code.end(), {0x7e0084fau, modifier, 0xf800180fu, 0x01010101u, 0xbf810000u}); + std::cerr << "DPP write " << modifier << '\n'; + Draw(*device, code, 1u, waveSize, modifier == 0xff00e402u ? std::byte{255} : std::byte{0}); + Draw(*device, code, 8u, waveSize, std::byte{0}); + } + const std::array whole{0xbefc0300u, 0x7e028700u, 0x7e020d01u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + std::cerr << "whole word\n"; + Draw(*device, whole, 0u, waveSize, std::byte{255}); + Draw(*device, whole, 4u, waveSize, std::byte{255}, false, true); + } + std::cout << "relative ancillary GPU readback tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/RelativeIndexing.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/RelativeIndexing.cpp index 8b2ab8f12e..2255013294 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/RelativeIndexing.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/RelativeIndexing.cpp @@ -17,14 +17,15 @@ namespace { using AgcDriver::Graphics::Require; using ShaderRecompiler::ShaderStage; -constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t MaxThreads = 64; 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 Results = 32; +constexpr std::uint32_t Checked = 19; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; -alignas(256) constexpr std::array Code{ - 0x34020084, 0x34060086, 0xe0301000, 0x80000401, 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280, +alignas(256) constexpr std::array Code{ + 0x34020084, 0x34060087, 0xe0301000, 0x80000401, 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280, 0x7e1c0280, 0x7e1e0280, 0x7e200280, 0x7e220280, 0x7e240280, 0x7e260280, 0x7e280280, 0xbf8c3f70, 0x7e3c0304, 0xd525001f, 0x00010304, 0xd5250020, 0x00010504, 0xd5250021, 0x00010704, 0xd5250022, 0x00010904, 0xd5250023, 0x00010b04, 0xd5250024, 0x00010d04, 0xd5250025, 0x00010f04, 0xbefc0382, @@ -34,44 +35,21 @@ alignas(256) constexpr std::array Code{ 0x0000011f, 0x7e280323, 0xe0701000, 0x80010a03, 0xe0701004, 0x80010b03, 0xe0701008, 0x80010c03, 0xe070100c, 0x80010d03, 0xe0701010, 0x80010e03, 0xe0701014, 0x80010f03, 0xe0701018, 0x80011003, 0xe070101c, 0x80011103, 0xe0701020, 0x80011203, 0xe0701024, 0x80011303, 0xe0701028, 0x80011403, - 0xbf810000, + 0xbefc03c1, 0x7e2a8720, 0xe070102c, 0x80011503, 0x7e428504, 0xe0701030, 0x80012003, 0x7e468922, + 0xe0701034, 0x80012203, 0xe0701038, 0x80012103, 0xbefc03ff, 0x000103ff, 0x7e3ed120, 0xe070103c, + 0x80012003, 0x7e48911e, 0xe0701040, 0x80012503, 0x7e2c0327, 0xe0701044, 0x80011603, 0xbefc0388, + 0x7e2e871e, 0xe0701048, 0x80011703, 0xbf810000, }; -constexpr std::uint32_t Expected[32][11] = { - {0x00001002u, 0x00001002u, 0x00001001u, 0x00001030u, 0x00001020u, 0x00001000u, 0x00001007u, 0x00001006u, 0x00001007u, 0x00001005u, 0x00001005u}, - {0x00002002u, 0x00002002u, 0x00002001u, 0x00002030u, 0x00002020u, 0x00002000u, 0x00002007u, 0x00002006u, 0x00002007u, 0x00002005u, 0x00002005u}, - {0x00003002u, 0x00003002u, 0x00003001u, 0x00003030u, 0x00003020u, 0x00003000u, 0x00003007u, 0x00003006u, 0x00003007u, 0x00003005u, 0x00003005u}, - {0x00004002u, 0x00004002u, 0x00004001u, 0x00004030u, 0x00004020u, 0x00004000u, 0x00004007u, 0x00004006u, 0x00004007u, 0x00004005u, 0x00004005u}, - {0x00005002u, 0x00005002u, 0x00005001u, 0x00005030u, 0x00005020u, 0x00005000u, 0x00005007u, 0x00005006u, 0x00005007u, 0x00005005u, 0x00005005u}, - {0x00006002u, 0x00006002u, 0x00006001u, 0x00006030u, 0x00006020u, 0x00006000u, 0x00006007u, 0x00006006u, 0x00006007u, 0x00006005u, 0x00006005u}, - {0x00007002u, 0x00007002u, 0x00007001u, 0x00007030u, 0x00007020u, 0x00007000u, 0x00007007u, 0x00007006u, 0x00007007u, 0x00007005u, 0x00007005u}, - {0x00008002u, 0x00008002u, 0x00008001u, 0x00008030u, 0x00008020u, 0x00008000u, 0x00008007u, 0x00008006u, 0x00008007u, 0x00008005u, 0x00008005u}, - {0x00009002u, 0x00009002u, 0x00009001u, 0x00009030u, 0x00009020u, 0x00009000u, 0x00009007u, 0x00009006u, 0x00009007u, 0x00009005u, 0x00009005u}, - {0x0000a002u, 0x0000a002u, 0x0000a001u, 0x0000a030u, 0x0000a020u, 0x0000a000u, 0x0000a007u, 0x0000a006u, 0x0000a007u, 0x0000a005u, 0x0000a005u}, - {0x0000b002u, 0x0000b002u, 0x0000b001u, 0x0000b030u, 0x0000b020u, 0x0000b000u, 0x0000b007u, 0x0000b006u, 0x0000b007u, 0x0000b005u, 0x0000b005u}, - {0x0000c002u, 0x0000c002u, 0x0000c001u, 0x0000c030u, 0x0000c020u, 0x0000c000u, 0x0000c007u, 0x0000c006u, 0x0000c007u, 0x0000c005u, 0x0000c005u}, - {0x0000d002u, 0x0000d002u, 0x0000d001u, 0x0000d030u, 0x0000d020u, 0x0000d000u, 0x0000d007u, 0x0000d006u, 0x0000d007u, 0x0000d005u, 0x0000d005u}, - {0x0000e002u, 0x0000e002u, 0x0000e001u, 0x0000e030u, 0x0000e020u, 0x0000e000u, 0x0000e007u, 0x0000e006u, 0x0000e007u, 0x0000e005u, 0x0000e005u}, - {0x0000f002u, 0x0000f002u, 0x0000f001u, 0x0000f030u, 0x0000f020u, 0x0000f000u, 0x0000f007u, 0x0000f006u, 0x0000f007u, 0x0000f005u, 0x0000f005u}, - {0x00010002u, 0x00010002u, 0x00010001u, 0x00010030u, 0x00010020u, 0x00010000u, 0x00010007u, 0x00010006u, 0x00010007u, 0x00010005u, 0x00010005u}, - {0x00011002u, 0x00011002u, 0x00011001u, 0x00011030u, 0x00011020u, 0x00011000u, 0x00011007u, 0x00011006u, 0x00011007u, 0x00011005u, 0x00011005u}, - {0x00012002u, 0x00012002u, 0x00012001u, 0x00012030u, 0x00012020u, 0x00012000u, 0x00012007u, 0x00012006u, 0x00012007u, 0x00012005u, 0x00012005u}, - {0x00013002u, 0x00013002u, 0x00013001u, 0x00013030u, 0x00013020u, 0x00013000u, 0x00013007u, 0x00013006u, 0x00013007u, 0x00013005u, 0x00013005u}, - {0x00014002u, 0x00014002u, 0x00014001u, 0x00014030u, 0x00014020u, 0x00014000u, 0x00014007u, 0x00014006u, 0x00014007u, 0x00014005u, 0x00014005u}, - {0x00015002u, 0x00015002u, 0x00015001u, 0x00015030u, 0x00015020u, 0x00015000u, 0x00015007u, 0x00015006u, 0x00015007u, 0x00015005u, 0x00015005u}, - {0x00016002u, 0x00016002u, 0x00016001u, 0x00016030u, 0x00016020u, 0x00016000u, 0x00016007u, 0x00016006u, 0x00016007u, 0x00016005u, 0x00016005u}, - {0x00017002u, 0x00017002u, 0x00017001u, 0x00017030u, 0x00017020u, 0x00017000u, 0x00017007u, 0x00017006u, 0x00017007u, 0x00017005u, 0x00017005u}, - {0x00018002u, 0x00018002u, 0x00018001u, 0x00018030u, 0x00018020u, 0x00018000u, 0x00018007u, 0x00018006u, 0x00018007u, 0x00018005u, 0x00018005u}, - {0x00019002u, 0x00019002u, 0x00019001u, 0x00019030u, 0x00019020u, 0x00019000u, 0x00019007u, 0x00019006u, 0x00019007u, 0x00019005u, 0x00019005u}, - {0x0001a002u, 0x0001a002u, 0x0001a001u, 0x0001a030u, 0x0001a020u, 0x0001a000u, 0x0001a007u, 0x0001a006u, 0x0001a007u, 0x0001a005u, 0x0001a005u}, - {0x0001b002u, 0x0001b002u, 0x0001b001u, 0x0001b030u, 0x0001b020u, 0x0001b000u, 0x0001b007u, 0x0001b006u, 0x0001b007u, 0x0001b005u, 0x0001b005u}, - {0x0001c002u, 0x0001c002u, 0x0001c001u, 0x0001c030u, 0x0001c020u, 0x0001c000u, 0x0001c007u, 0x0001c006u, 0x0001c007u, 0x0001c005u, 0x0001c005u}, - {0x0001d002u, 0x0001d002u, 0x0001d001u, 0x0001d030u, 0x0001d020u, 0x0001d000u, 0x0001d007u, 0x0001d006u, 0x0001d007u, 0x0001d005u, 0x0001d005u}, - {0x0001e002u, 0x0001e002u, 0x0001e001u, 0x0001e030u, 0x0001e020u, 0x0001e000u, 0x0001e007u, 0x0001e006u, 0x0001e007u, 0x0001e005u, 0x0001e005u}, - {0x0001f002u, 0x0001f002u, 0x0001f001u, 0x0001f030u, 0x0001f020u, 0x0001f000u, 0x0001f007u, 0x0001f006u, 0x0001f007u, 0x0001f005u, 0x0001f005u}, - {0x00020002u, 0x00020002u, 0x00020001u, 0x00020030u, 0x00020020u, 0x00020000u, 0x00020007u, 0x00020006u, 0x00020007u, 0x00020005u, 0x00020005u}, -}; -constexpr const char* Names[11] = {"movrelsd", "swapped_2", "sd2_untouched", "swaprel_dst", "swaprel_src", "swaprel_other", "swap_dst", "swap_src", "swaprel_same", "swaprel_same_other", "movrelsd_e64_out_of_range"}; +constexpr const char* Names[Checked] = {"movrelsd", "swapped_2", "sd2_untouched", "swaprel_dst", "swaprel_src", "swaprel_other", "swap_dst", "swap_src", "swaprel_same", "swaprel_same_other", "movrelsd_e64_dst", + "movrels_out_of_range", "movreld_out_of_range_neighbour", "movrelsd_out_of_range_source", "movrelsd_out_of_range_other", "swaprel_one_side_out_of_range", "movrelsd_2_source_out_of_range", + "unwritten_direct", "unwritten_relative"}; + +std::array ExpectedFor(std::uint32_t tid) { + const std::uint32_t x = 0x1000u * (tid + 1u); + return {x + 2u, x + 2u, x + 1u, x + 0x30u, x + 0x20u, x, x + 7u, x + 6u, x + 7u, x + 5u, x + 5u, + tid, x + 0x30u, x + 2u, x + 0x20u, x + 0x30u, tid, 0u, 0u}; +} void Fill(std::uint32_t tid, std::uint32_t* words) { words[0] = 0x1000u * (tid + 1u); @@ -88,12 +66,12 @@ std::string Hex(std::uint32_t value) { return text; } -void Expect(std::uint32_t tid, std::uint32_t actual, std::uint32_t expected, const char* name) { - Require(actual == expected, std::string("relative indexing: lane ") + std::to_string(tid) + " " + name + " is " + Hex(actual) + ", expected " + Hex(expected)); +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: wave") + std::to_string(waveSize) + " 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::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)); @@ -102,10 +80,10 @@ void Run(AgcDriver::VulkanDevice& device) { 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}; + 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}, + {waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory}, device.Target(), {0, 0, 0, 128} }; @@ -115,11 +93,11 @@ void Run(AgcDriver::VulkanDevice& device) { device.WaitIdle(); } -void Check() { - for (std::uint32_t tid = 0; tid < Threads; ++tid) { - const std::uint32_t* in = &Input[tid * Inputs]; +void Check(std::uint32_t waveSize, std::uint32_t threads) { + for (std::uint32_t tid = 0; tid < threads; ++tid) { const std::uint32_t* out = &Output[tid * Results]; - for (std::uint32_t index = 0; index < 11; ++index) Expect(tid, out[index], Expected[tid][index], Names[index]); + const auto expected = ExpectedFor(tid); + for (std::uint32_t index = 0; index < Checked; ++index) Expect(waveSize, tid, out[index], expected[index], Names[index]); } } @@ -129,8 +107,10 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - Run(*device); - Check(); + Run(*device, 32, 32); + Check(32, 32); + Run(*device, 64, 64); + Check(64, 64); std::puts("relative indexing tests passed"); return 0; } catch (const std::exception& error) { 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/RelativeStageInputs.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/RelativeStageInputs.cpp new file mode 100644 index 0000000000..ae310f425b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/RelativeStageInputs.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 + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Extent = 64u; +alignas(256) std::array pixels{}; +alignas(256) std::array output{}; +alignas(256) constexpr std::array, 6> positions{{ + {0.0f, 0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f, 1.0f}, + {-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 relativeVertexCode{ + 0xbefc0385u, 0x7e028700u, 0xbefc0388u, 0x7e008700u, 0x4a000300u, + 0xe0382000u, 0x80000000u, 0xbf8c3f70u, 0xf80008cfu, 0x03020100u, 0xbf810000u +}; +constexpr std::array vertexCode{ + 0xe0382000u, 0x80000005u, 0xbf8c3f70u, 0xf80008cfu, 0x03020100u, 0xbf810000u +}; +constexpr std::array relativePixelCode{ + 0xbefc0383u, 0x7e008700u, 0xf800180fu, 0x00000000u, 0xbf810000u +}; +constexpr std::array pixelCode{ + 0x7e0002f2u, 0xf800180fu, 0x00000000u, 0xbf810000u +}; +constexpr std::array computeCode{ + 0xbefc0382u, 0x7e028700u, 0x34020283u, 0x4a000300u, 0x34000082u, + 0x7e028700u, 0xe0701000u, 0x80000100u, 0xbf810000u +}; + +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}; +} + +void Draw(AgcDriver::VulkanDevice& device, bool relativeVertex) { + pixels.fill(std::byte{0x40}); + const auto target = device.Target(); + const auto userData = BufferDescriptor(positions.data(), 16u, positions.size()); + const auto code = relativeVertex ? std::span(relativeVertexCode) : std::span(vertexCode); + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(code.data()), code, 0, {}}, + {32u, 0u, userData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, {}}, + target, {0u, 0u, 0u, 64u} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto pushBytes = static_cast(vertexResult.pushConstants.size()); + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.wave32 = true; + if (!relativeVertex) { + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) + | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY) + | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionZ) + | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionW); + pixel.posX = pixel.posY = pixel.posZ = pixel.posW = true; + } + pixel.targetOutputMode[0] = 9u; + pixel.targetExportMapping.fill(0xe4u); + const auto fragmentCode = relativeVertex ? std::span(pixelCode) : std::span(relativePixelCode); + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(fragmentCode.data()), fragmentCode, 0, {}}, + {32u, 0u, {}, std::nullopt, pixel, std::nullopt, {}}, target, PixelPushLayout(pushBytes, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0u}, {ShaderStage::Fragment, &pixelResult, PixelPushOffset(pushBytes, 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_TRIANGLE_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}; + AgcDriver::Pm4::DrawParameters draw{0u, 3u, 0u, 1u, 0u, false}; + draw.firstVertex = relativeVertex ? 1u : 3u; + draw.firstInstance = 2u; + device.Draw(state, draw, shaders); + device.WaitIdle(); + for (const auto value : pixels) Require(value == std::byte{255}, "relative stage input draw did not cover every pixel in white"); +} + +void Dispatch(AgcDriver::VulkanDevice& device) { + output.fill(0xdeadbeefu); + const auto userData = BufferDescriptor(output.data(), 0u, sizeof(output)); + const ShaderRecompiler::ShaderComputeStageInfo compute{{8u, 1u, 2u}, 0u, {false, false, false}, false, 3u}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(computeCode.data()), computeCode, 0, {}}, + {32u, 0u, userData, compute, std::nullopt, std::nullopt, {}}, device.Target(), {0u, 0u, 0u, 128u} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1u, 1u, 1u); + device.WaitIdle(); + for (std::size_t index = 0; index < output.size(); ++index) { + Require(output[index] == index / 8u, "relative stage input dispatch did not preserve the implicit Z input at output " + std::to_string(index) + + ": got " + std::to_string(output[index]) + ", expected " + std::to_string(index / 8u)); + } +} + +template +bool Run(const char* name, TAction action) { + try { + action(); + std::cout << "relative stage inputs: " << name << " GPU readback passed\n"; + return true; + } catch (const std::exception& error) { + std::cerr << "relative stage inputs: " << name << ": " << error.what() << '\n'; + return false; + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + bool passed = Run("vertex", [&] { Draw(*device, true); }); + passed &= Run("pixel", [&] { Draw(*device, false); }); + passed &= Run("compute", [&] { Dispatch(*device); }); + if (!passed) return 1; + std::cout << "relative stage input 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/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/SameQueueLabelWait.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SameQueueLabelWait.cpp new file mode 100644 index 0000000000..607faa3dd3 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SameQueueLabelWait.cpp @@ -0,0 +1,164 @@ +#include "VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Submit/include/Acb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +alignas(256) std::array SpinCode{0xf4080200u, 0xfa000000u, 0xbe840381u, 0x9304ff04u, 0x0019660du, 0x8004ff04u, 0x3c6ef35fu, 0x80828102u, 0xbf078002u, 0xbf85fff9u, 0xbf8cc07fu, 0x7e000204u, 0xe0700000u, 0x80020000u, 0xbf810000u}; + +constexpr std::uint32_t SpinIterations = 1u << 20u; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Waits = 16; + +struct Slot { + alignas(256) std::array spinOutput{}; + alignas(256) std::array spinDescriptor{}; + alignas(256) volatile std::uint32_t label = 0; + alignas(256) volatile std::uint32_t done = 0; +}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "same-queue label wait: cannot allocate the guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + Slot& Data() { return *reinterpret_cast(block); } + +private: + std::uint8_t* block = nullptr; +}; + +std::uint32_t Low(const volatile void* pointer) { + return static_cast(reinterpret_cast(pointer)); +} + +std::uint32_t High(const volatile void* pointer) { + return static_cast(static_cast(reinterpret_cast(pointer)) >> 32u); +} + +void SetShaderRegister(std::vector& words, std::uint32_t offset, std::uint32_t value) { + words.insert(words.end(), {0xc0017600u, offset, value}); +} + +void Dispatch(std::vector& words, const void* code, std::initializer_list userData) { + const auto address = static_cast(reinterpret_cast(code)); + SetShaderRegister(words, 0x20c, static_cast(address >> 8u)); + SetShaderRegister(words, 0x20d, static_cast(address >> 40u) & 0xffu); + for (const std::uint32_t offset : {0x207u, 0x208u, 0x209u}) SetShaderRegister(words, offset, 1); + SetShaderRegister(words, 0x212, 0); + SetShaderRegister(words, 0x213, static_cast(userData.size()) << 1u); + std::uint32_t offset = 0x240; + for (const auto value : userData) SetShaderRegister(words, offset++, value); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x8041u}); +} + +void ReleaseLabel(std::vector& words, volatile std::uint32_t* target, std::uint32_t value) { + words.insert(words.end(), {0xc0064900u, 0x514u, (1u << 29u) | (2u << 24u), Low(target), High(target), value, 0u, 0u}); +} + +void WriteData(std::vector& words, volatile std::uint32_t* target, std::uint32_t value) { + words.insert(words.end(), {0xc0033700u, 0x00100200u, Low(target), High(target), value}); +} + +void WaitEqual(std::vector& words, volatile std::uint32_t* target, std::uint32_t value) { + words.insert(words.end(), {0xc0053c00u, 0x13u, Low(target), High(target), value, 0xffffffffu, 0x19u}); +} + +std::uint64_t Run(Slot& slot, std::uint32_t value, bool wait) { + slot.spinDescriptor = {Low(slot.spinOutput.data()), High(slot.spinOutput.data()) & 0xffffu, sizeof(slot.spinOutput), 0x31016facu}; + slot.done = 0; + std::vector words; + Dispatch(words, SpinCode.data(), {Low(slot.spinDescriptor.data()), High(slot.spinDescriptor.data()), SpinIterations}); + for (std::uint32_t index = 0; index < Waits; ++index) { + ReleaseLabel(words, &slot.label, value + index); + if (wait) WaitEqual(words, &slot.label, value + index); + } + WriteData(words, &slot.done, 1); + const auto before = AgcDriver::GuestMemory::CollectEpochBumps(); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitAcb(0x20, &packet) == 0, "compute submit failed"); + const auto start = std::chrono::steady_clock::now(); + while (slot.done == 0) { + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(10), "the submission did not finish"); + std::this_thread::sleep_for(std::chrono::microseconds(100)); + } + AgcDriverWaitIdle_nid_postfix(); + Require(slot.label == value + Waits - 1, "the last label did not reach memory"); + return AgcDriver::GuestMemory::CollectEpochBumps() - before; +} + +} + +int main() { + try { + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + device.reset(); + GuestBlock block; + auto& slot = block.Data(); + Run(slot, 1, false); + std::uint64_t extra = ~std::uint64_t{0}; + for (std::uint32_t attempt = 0; attempt < 3; ++attempt) { + const auto without = Run(slot, 100 + attempt * 2 * Waits, false); + const auto with = Run(slot, 100 + (attempt * 2 + 1) * Waits, true); + std::cout << "attempt " << attempt << ": " << with << " bumps with " << Waits << " waits, " << without << " without\n"; + extra = std::min(extra, with > without ? with - without : 0); + } + Require(extra < Waits / 2, "WAIT_REG_MEMs satisfied by labels their own queue released earlier in the submission started new collect epochs: " + std::to_string(extra) + " extra bumps for " + std::to_string(Waits) + " waits"); + std::cout << "same-queue label wait test passed\n"; + AgcDriverShutdown_nid_postfix(); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { + AgcDriverShutdown_nid_postfix(); + } catch (const std::exception& shutdownError) { + std::cerr << shutdownError.what() << '\n'; + } + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/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..4d4f77cca1 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp @@ -0,0 +1,740 @@ +#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 "Recompiler.hpp" +#include "Optimization/ResourceProgram.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::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) : 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::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 = 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::vector userData; + const auto program = CompileLoad(device, texture, userData, arrayed); + std::vector> loaded(Side * Side * Samples); + 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 / Samples; + const std::array words = arrayed + ? std::array{0u, 0u, pixel % Side, pixel / Side, 0u, index % Samples} + : std::array{0u, 0u, 0u, pixel % Side, pixel / Side, index % Samples}; + 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) { + auto loaded = RunLoad(device, texture, false); + const auto loadedArray = RunLoad(device, texture, true); + 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; +} + +} + +int main(int argc, char** argv) { + try { + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (argc == 2 && std::string_view(argv[1]) == "--native-vertex-input") return NativeVertexInput(device); + Block block(4u * BlockBytes); + Block other(BlockBytes); + const auto color = DecodeTarget(block); + const auto state = TargetState(color); + if (!AgcDriver::VulkanDeviceTestAccess::ContextOf(*device).storageImageMultisample) { + std::puts("multisampled target sampling not tested: storage image multisampling unavailable"); + return VulkanTestSkipped; + } + Draw(*device, state); + CheckSamples(state, LoadSamples(*device, block), {}); + const auto colorBytes = AgcDriver::Graphics::MultisampledColorBytes(color); + 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 cf910ab2cc..46e3184ea6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -1,10 +1,16 @@ #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.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/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 @@ -12,8 +18,12 @@ #define NOMINMAX #endif #include +#else +#include #endif +#include #include +#include #include #include #include @@ -22,9 +32,13 @@ #include #include #include +#include +#include #include #include +#include #include +#include #include namespace { @@ -38,11 +52,75 @@ constexpr std::uint32_t Type2D = 9; constexpr std::size_t SurfaceBytes = std::size_t{Side} * Side * 4u; constexpr std::size_t Surfaces = 12; constexpr std::size_t BlockBytes = SurfaceBytes * Surfaces; +constexpr std::size_t RegistrationGapBytes = 65536; +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: - Device() { + explicit Device(bool enableStorageImageMultisample = true) { #ifdef _WIN32 library = SDL_LoadObject("vulkan-1.dll"); #else @@ -63,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); @@ -88,8 +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; + 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)) { @@ -110,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"); @@ -117,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 (...) { @@ -128,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 { @@ -154,13 +252,13 @@ class Block { public: explicit Block(const Context& context) : context(context) { #ifdef _WIN32 - memory = VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); + memory = VirtualAlloc(nullptr, AllocationBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); #else - memory = std::aligned_alloc(65536, BlockBytes); + memory = std::aligned_alloc(65536, AllocationBytes); #endif Require(memory != nullptr, "cannot allocate the texture block"); std::memset(memory, 0x5a, BlockBytes); - GuestAllocations::Mutation().Add(memory, BlockBytes, true, true); + GuestAllocations::Mutation().Add(memory, BlockBytes, true, true, true); } ~Block() { @@ -181,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), @@ -204,6 +349,152 @@ GuestTextureResource Resource(std::uint64_t address, std::uint32_t side = Side) return DecodeTextureResource(words); } +std::shared_ptr ImageResources(const Context& context, const Block& block, std::size_t first, std::uint32_t count, bool sampled) { + ShaderRecompiler::DescriptorBinding binding{}; + binding.kind = sampled ? ShaderRecompiler::DescriptorKind::SampledImage : ShaderRecompiler::DescriptorKind::StorageImage; + binding.role = ShaderRecompiler::DescriptorRole::GuestImages; + binding.descriptorSet = 0; + binding.binding = (sampled ? ShaderRecompiler::RuntimeAbi::FirstImageBinding : ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding) + PlaceholderDimensionsPerClass + 2u; + binding.count = count; + binding.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D; + binding.imageWritten.assign(count, false); + binding.imageSamplers.assign(count, 0u); + for (std::uint32_t element = 0; element < count; ++element) { + const auto words = Descriptor(block.Surface(first + element)); + binding.guestDescriptor.insert(binding.guestDescriptor.end(), words.begin(), words.end()); + } + ShaderRecompiler::RecompileResult program; + program.bindings.push_back(std::move(binding)); + const CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + 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; + TextureCache textures(context); + context.textureCache = &textures; + Block block(context); + if (context.hostImportAlignment == 0 || HostImportFor(context, block.Surface(0), BlockBytes) == nullptr) { + Require(std::getenv("ANYPS5_REQUIRE_VULKAN") == nullptr, "resource cache tests require host imports"); + std::puts("skipped, resource cache tests require host imports"); + return false; + } + auto& cache = SharedResourceCache(); + cache.Clear(); + try { + if (mode == "resource-clear") { + auto storage = ImageResources(context, block, 0, 1, false); + auto sampled = ImageResources(context, block, 0, 1, true); + Require(sampled->SampledTextures().front()->ViewsStorageImage(), "the sampled resource is not a storage-image view"); + const std::weak_ptr image = StorageTexture::FindLive(block.Surface(0), SurfaceBytes); + const std::weak_ptr storageSet = storage; + const std::weak_ptr sampledSet = sampled; + cache.Insert({1}, storage); + cache.Insert({2}, sampled); + Require(cache.Size() == 2, "the resource cache did not accept the live descriptor sets"); + sampled.reset(); + ClearCachedTextures(context.device); + Require(cache.Find({1}) == nullptr && cache.Find({2}) == nullptr && cache.Size() == 0, "clearing storage images left their resources cached"); + Require(sampledSet.expired(), "cache clear kept the sampled descriptor set alive"); + Require(!image.expired() && !storageSet.expired(), "cache clear destroyed an active caller's resources"); + storage.reset(); + Require(image.expired() && storageSet.expired(), "cleared storage image outlived its last active resource"); + } else if (mode == "resource-oversized") { + for (const bool sampledImages : {false, true}) { + const std::weak_ptr early = CachedStorageSurface(context, Resource(block.Surface(0))); + auto oversized = ImageResources(context, block, 0, static_cast(Surfaces), sampledImages); + const std::weak_ptr rejected = oversized; + auto image = early.lock(); + Require(oversized->Reusable(), "the oversized descriptor set is not eligible for resource caching"); + Require(image != nullptr && !image->Cached(), "the oversized descriptor set did not evict an early image while building"); + if (sampledImages) Require(oversized->SampledTextures().size() == Surfaces && oversized->SampledTextures().front()->StorageSource() == image.get(), "the oversized sampled descriptor set does not retain the early storage image"); + std::vector> displaced; + cache.Insert({1}, oversized, &displaced); + 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(); + 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); + } + } else { + auto storage = ImageResources(context, block, 0, 1, false); + auto sampled = ImageResources(context, block, 0, 1, true); + Require(storage->Reusable() && sampled->Reusable(), "the stale descriptor sets are not eligible for resource caching"); + Require(sampled->SampledTextures().front()->ViewsStorageImage(), "the sampled resource is not a storage-image view"); + auto image = StorageTexture::FindLive(block.Surface(0), SurfaceBytes); + const std::weak_ptr staleImage = image; + const std::weak_ptr storageSet = storage; + const std::weak_ptr sampledSet = sampled; + Require(image != nullptr && image->Cached(), "the resource has no cached storage image"); + for (std::size_t surface = 1; surface < Surfaces && image->Cached(); ++surface) CachedStorageSurface(context, Resource(block.Surface(surface))); + Require(!image->Cached(), "the test never evicted the oldest storage image"); + auto live = ImageResources(context, block, Surfaces - 1, 1, false); + cache.Insert({1}, live); + Require(cache.Find({1}) == live, "the resource cache rejected a live descriptor set"); + std::vector> displaced; + for (const auto& stale : {storage, sampled}) { + cache.Insert({2}, stale, &displaced); + Require(cache.Find({2}) == nullptr && cache.Size() == 1 && displaced.empty(), "an uncached image was admitted to the resource cache"); + cache.Insert({1}, stale, &displaced); + Require(cache.Find({1}) == live && cache.Size() == 1 && displaced.empty(), "an uncached image replaced a live resource-cache entry"); + } + image.reset(); + storage.reset(); + 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(); + 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; + } + cache.Clear(); + return true; +} + void viewTests(const Context& context) { if (context.hostImportAlignment == 0) { std::puts("host imports unavailable: views of storage images not tested"); @@ -218,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"); @@ -226,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) { @@ -278,6 +672,271 @@ void snapshotTests(const Context& context) { Require(TextureCacheUsage().sampledBytes == tiny->AllocationBytes(), "a snapshot whose " + std::to_string(tiny->AllocationBytes()) + "-byte image is smaller than its " + std::to_string(guestBytes) + " guest bytes counted " + std::to_string(TextureCacheUsage().sampledBytes) + " bytes, not its allocation"); } +void heldTests(const Context& context) { + Block block(context); + auto held = Sampled(context, block.Surface(0)); + const auto bytes = TextureCacheUsage().sampledBytes; + const auto capacity = static_cast(Budget / bytes); + Require((capacity + 1u) * bytes <= Ceiling && capacity + 2u < Surfaces, "a snapshot of the 256 KiB surface counts " + std::to_string(bytes) + " bytes, which the test's ceiling does not fit as planned"); + std::vector> released; + for (std::size_t surface = 1; surface < capacity; ++surface) released.push_back(Sampled(context, block.Surface(surface))); + Sampled(context, block.Surface(capacity)); + Require(Sampled(context, block.Surface(0)) == held, "the sampled texture cache evicted a texture its caller still holds"); + Require(released.front().expired(), "the least recently used texture nobody holds was not evicted in place of the held one"); + held.reset(); + ClearCachedTextures(context.device); + std::vector> all; + for (std::size_t surface = 0; surface <= capacity; ++surface) all.push_back(Sampled(context, block.Surface(surface))); + Require(TextureCacheUsage().sampledEntries == capacity + 1u && TextureCacheUsage().sampledBytes > Budget, "textures their callers hold left a cache over its budget"); + for (std::size_t surface = 0; surface < all.size(); ++surface) Require(Sampled(context, block.Surface(surface)) == all[surface], "surface " + std::to_string(surface) + " was made again while its texture was held"); + all.push_back(Sampled(context, block.Surface(capacity + 1u))); + Require(TextureCacheUsage().sampledEntries == capacity + 1u && TextureCacheUsage().sampledBytes <= Ceiling, "held textures took the sampled texture cache past a quarter over its budget"); + for (std::size_t surface = 1; surface < all.size(); ++surface) Require(Sampled(context, block.Surface(surface)) == all[surface], "past the ceiling, surface " + std::to_string(surface) + " left in place of the least recently used held texture"); + all.clear(); + Sampled(context, block.Surface(capacity + 2u)); + 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; + Block block(context); + const auto small = [&](std::size_t surface) { return Sampled(context, block.Surface(0) + surface * SmallBytes, SmallSide); }; + std::vector> all{small(0)}; + const auto bytes = TextureCacheUsage().sampledBytes; + const auto capacity = static_cast(Budget / bytes); + Require((capacity + 3u) * bytes <= Ceiling && (capacity + 3u) * SmallBytes <= BlockBytes, "a snapshot of the 64 KiB surface counts " + std::to_string(bytes) + " bytes, which the test's ceiling does not fit as planned"); + for (std::size_t surface = 1; surface <= capacity; ++surface) all.push_back(small(surface)); + const std::weak_ptr newest = all.back(); + all.pop_back(); + all.push_back(small(capacity + 1u)); + std::this_thread::sleep_for(std::chrono::milliseconds(1100)); + all.push_back(small(capacity + 2u)); + Require(newest.expired(), "a released texture behind held ones was still cached a second later"); + Require(TextureCacheUsage().sampledEntries == capacity + 2u, "the rescan left " + std::to_string(TextureCacheUsage().sampledEntries) + " entries, not the " + std::to_string(capacity + 2u) + " textures still held"); +} + +void viewEvictionTests(const Context& context) { + if (context.hostImportAlignment == 0) return; + Block snapshots(context); + Block imported(context); + if (HostImportFor(context, imported.Surface(0), BlockBytes) == nullptr) return; + const std::weak_ptr view = Sampled(context, imported.Surface(0)); + Require(!view.expired() && TextureCacheUsage().sampledBytes == 0, "the view of a cached storage image did not enter the sampled texture cache at 0 bytes"); + Sampled(context, snapshots.Surface(0)); + const auto snapshotBytes = TextureCacheUsage().sampledBytes; + Require(snapshotBytes != 0, "the snapshot fixture unexpectedly became a view of a cached storage image"); + const auto capacity = static_cast(Budget / snapshotBytes); + for (std::size_t surface = 1; surface <= capacity; ++surface) Sampled(context, snapshots.Surface(surface)); + Require(!view.expired(), "the sampled texture cache evicted a view of a cached storage image, which frees no memory"); +} + +void frameTests(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 frame = capacity + 2u; + Require(capacity >= 2u && 2u * frame <= 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"); + std::vector> made(frame); + for (std::size_t surface = 0; surface < frame; ++surface) made[surface] = Sampled(context, block.Surface(surface)); + Require(TextureCacheUsage().sampledEntries == capacity, "a frame of " + std::to_string(frame) + " textures left " + std::to_string(TextureCacheUsage().sampledEntries) + " cached, not the " + std::to_string(capacity) + " the budget holds"); + for (std::size_t surface = capacity; surface < frame; ++surface) Require(made[surface].expired(), "surface " + std::to_string(surface) + ", used once in a frame whose textures fill the cache, was cached in place of an earlier one"); + Recorder::CountPresent(); + std::size_t hits = 0; + for (std::size_t surface = 0; surface < frame; ++surface) { + const auto again = Sampled(context, block.Surface(surface)); + 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 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)); + for (std::size_t surface = 0; surface < frame; ++surface) Require(moved[surface].expired(), "surface " + std::to_string(frame + surface) + ", first used in the frame after the cached textures were, was cached in place of one of them"); + Require(TextureCacheUsage().sampledEntries == capacity, "a frame over other textures left " + std::to_string(TextureCacheUsage().sampledEntries) + " of the previous frame's textures cached"); + Recorder::CountPresent(); + for (std::size_t surface = 0; surface < frame; ++surface) moved[surface] = Sampled(context, block.Surface(frame + surface)); + for (std::size_t surface = 0; surface < capacity; ++surface) Require(!moved[surface].expired(), "surface " + std::to_string(frame + surface) + " of a second frame over other textures did not replace the textures last used two frames before"); + 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); + 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"); + std::vector> cached(capacity); + for (std::size_t surface = 0; surface < capacity; ++surface) cached[surface] = Sampled(context, block.Surface(surface)); + Recorder::CountPresent(); + for (std::size_t surface = 0; surface < capacity; ++surface) { + const auto again = Sampled(context, block.Surface(surface)); + Require(again == cached[surface].lock(), "surface " + std::to_string(surface) + " missed in a frame over the textures the cache holds"); + } + Recorder::CountPresent(); + Sampled(context, block.Surface(capacity)); + std::size_t hits = 0; + for (std::size_t surface = 0; surface < capacity; ++surface) { + const auto again = Sampled(context, block.Surface(surface)); + hits += again == cached[surface].lock(); + } + Require(hits == capacity, "a frame that first uses a new texture before its " + std::to_string(capacity) + " cached ones found " + std::to_string(hits) + " of them cached"); +} + +constexpr std::uint64_t KeyHashBasis = 14695981039346656037ull; +constexpr std::uint64_t KeyHashPrime = 1099511628211ull; + +std::array, 2> CollidingDescriptors(VkDevice device, std::uint64_t address) { + const auto words = Descriptor(address); + auto prefix = KeyHashBasis ^ reinterpret_cast(device); + for (std::size_t word = 0; word < 6; ++word) prefix = (prefix ^ words[word]) * KeyHashPrime; + std::unordered_map word6ByHighHalf; + for (std::uint32_t candidate = 0; candidate < 0x200u; ++candidate) { + const auto word6 = ((candidate >> 1u) << 24u) | (candidate & 1u); + const auto hash = (prefix ^ word6) * KeyHashPrime; + const auto [match, added] = word6ByHighHalf.emplace(static_cast(hash >> 32u), word6); + if (added) continue; + auto first = words; + auto second = words; + first[6] = match->second; + second[6] = word6; + second[7] = static_cast(hash ^ ((prefix ^ match->second) * KeyHashPrime)); + return {first, second}; + } + throw std::runtime_error("no two descriptors of the surface that differ only in word 6's statistics counter and the unused meta address collide in TextureKeyHash"); +} + +void collidingKeyTests(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"); + std::vector> cached(capacity); + for (std::size_t surface = 0; surface < capacity; ++surface) cached[surface] = Sampled(context, block.Surface(surface)); + Recorder::CountPresent(); + const auto [first, second] = CollidingDescriptors(context.device, block.Surface(capacity)); + CachedSampledTexture(context, first); + CachedSampledTexture(context, second); + std::size_t kept = 0; + for (const auto& texture : cached) kept += !texture.expired(); + Require(kept == capacity && TextureCacheUsage().sampledEntries == capacity, "the first use of a descriptor whose cache key hash equals that of another descriptor first used in the frame left " + std::to_string(kept) + " of the " + std::to_string(capacity) + " textures the last frame used cached"); +} + constexpr std::uint64_t ReportedBudget = 64ull << 30u; constexpr std::uint64_t ReportedUsage = 12ull << 30u; @@ -301,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(); @@ -318,30 +992,92 @@ 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(); + 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) { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + if (!resourceCacheTests(context, mode)) return VulkanTestSkipped; + 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; @@ -350,7 +1086,17 @@ int main(int argc, char** argv) { std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); 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); } std::puts("sampled texture cache budget tests passed"); 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/ScalarSwappcFetch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ScalarSwappcFetch.cpp index 158fb8d508..a5ad23d658 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ScalarSwappcFetch.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ScalarSwappcFetch.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "Recompiler.hpp" #include "VulkanTestDevice.hpp" +#include #include #include #include @@ -35,9 +36,11 @@ constexpr std::array, 3> Triangle{{ {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.25f, 1.0f}, {-1.0f, 3.0f, 0.75f, 1.0f} }}; -std::array VertexBufferDescriptor(std::uint32_t select) { +alignas(256) constexpr std::array RestartIndices{0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff}; + +std::array VertexBufferDescriptor(std::uint32_t select, std::uint32_t records) { const auto address = reinterpret_cast(Triangle.data()); - return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), static_cast(Triangle.size()), 0x0104d000u | select}; + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), records, 0x0104d000u | select}; } std::array OutputDescriptor() { @@ -56,10 +59,10 @@ ShaderRecompiler::RecompileRequest VertexRequest(const AgcDriver::VulkanDevice& return request; } -ShaderRecompiler::ShaderVertexStageInfo VertexInfo(std::uint32_t select, std::int32_t registers) { +ShaderRecompiler::ShaderVertexStageInfo VertexInfo(std::uint32_t select, std::int32_t registers, std::uint32_t records = static_cast(Triangle.size())) { ShaderRecompiler::ShaderVertexStageInfo info{}; info.resourcesNum = 1; - info.resources[0].fields = VertexBufferDescriptor(select); + info.resources[0].fields = VertexBufferDescriptor(select, records); info.resourcesDst[0] = {8, registers, 0, 0}; info.fetchAttribReg = 8; info.fetchBufferReg = 10; @@ -76,12 +79,12 @@ std::vector UserData() { return userData; } -void Draw(AgcDriver::VulkanDevice& device, std::uint32_t select) { +void Draw(AgcDriver::VulkanDevice& device, std::uint32_t select, bool onlyRestart = false) { Pixels.fill(std::byte{0x40}); Output.fill(0xdeadbeefu); const auto userData = UserData(); const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; - const auto vertexResult = ShaderRecompiler::Recompile(VertexRequest(device, userData, VertexInfo(select, 4), vertexMemory)); + const auto vertexResult = ShaderRecompiler::Recompile(VertexRequest(device, userData, VertexInfo(select, 4, onlyRestart ? 0u : static_cast(Triangle.size())), vertexMemory)); const auto vertexPush = static_cast(vertexResult.pushConstants.size()); ShaderRecompiler::ShaderPixelStageInfo pixel{}; @@ -96,13 +99,13 @@ void Draw(AgcDriver::VulkanDevice& device, std::uint32_t select) { {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, {64u, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, device.Target(), - {0, 0, vertexPush, 128 - vertexPush} + PixelPushLayout(vertexPush, device.Target()) }; fragment.useCache = false; const auto pixelResult = ShaderRecompiler::Recompile(fragment); const std::array shaders{{ {ShaderStage::Vertex, &vertexResult, 0}, - {ShaderStage::Fragment, &pixelResult, vertexPush} + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, device.Target())} }}; AgcDriver::Graphics::State state{}; @@ -111,7 +114,8 @@ void Draw(AgcDriver::VulkanDevice& device, std::uint32_t select) { state.colors = {state.color}; state.hasColorTarget = true; state.renderExtent = {Width, Height}; - state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.topology = onlyRestart ? VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP : VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.primitiveRestart = onlyRestart; state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; state.negativeOneToOne = false; state.scissor = {{0, 0}, {Width, Height}}; @@ -120,7 +124,7 @@ void Draw(AgcDriver::VulkanDevice& device, std::uint32_t select) { state.blend.colorWriteMask = 15; state.blends = {state.blend}; state.blendConstants = {}; - const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + const auto draw = onlyRestart ? AgcDriver::Pm4::DrawParameters{reinterpret_cast(RestartIndices.data()), static_cast(RestartIndices.size()), 2, 1, 0, true} : AgcDriver::Pm4::DrawParameters{0, static_cast(Triangle.size()), 0, 1, 0, false}; device.Draw(state, draw, shaders); device.WaitIdle(); } @@ -159,6 +163,8 @@ int main() { Draw(*device, ReverseSelect); Check(ReverseSelect, "fetch-shader call, reversed dst_sel"); CheckRegisterLimit(*device); + Draw(*device, IdentitySelect, true); + Require(std::all_of(Output.begin(), Output.end(), [](std::uint32_t word) { return word == 0xdeadbeefu; }) && std::all_of(Pixels.begin(), Pixels.end(), [](std::byte value) { return value == std::byte{0x40}; }), "an indexed draw of only restart indices ran its vertices or wrote pixels"); std::puts("scalar swappc fetch tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ScalarWideLoads.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ScalarWideLoads.cpp index 97f04ea301..ba7fa6821b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ScalarWideLoads.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ScalarWideLoads.cpp @@ -159,7 +159,7 @@ public: block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); #endif Require(block != nullptr, "scalar wide loads: cannot allocate the guest block"); - GuestAllocations::Mutation().Add(block, BlockBytes, true, false); + GuestAllocations::Mutation().Add(block, BlockBytes, true, false, true); } ~GuestBlock() { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SdwaCvtF16U16.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SdwaCvtF16U16.cpp new file mode 100644 index 0000000000..6bd1837170 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SdwaCvtF16U16.cpp @@ -0,0 +1,109 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 4; +constexpr std::uint32_t Garbage = 0xabcd1234u; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array SdwaCode{ + 0x34020082, 0x34060082, 0xe0302000, 0x80000401, + 0x7e1402ff, 0xabcd1234, 0x7e1602ff, 0xabcd1234, 0x7e1802ff, 0xabcd1234, 0x7e1a02ff, 0xabcd1234, + 0xbf8c3f70, 0x7e14a0f9, 0x00031404, 0x7e16a0f9, 0x00000604, 0x7e18a0f9, 0x00011504, 0x7e1aa0f9, 0x00020604, + 0xe0702000, 0x80010a03, 0xe0702004, 0x80010b03, 0xe0702008, 0x80010c03, 0xe070200c, 0x80010d03, + 0xbf810000, +}; + +std::uint32_t HalfBits(std::uint32_t value) { + if (value == 0u) return 0u; + int exponent = 0; + const float mantissa = std::frexp(static_cast(value), &exponent); + const auto fraction = static_cast(std::ldexp(mantissa, 11)); + return (static_cast(exponent + 14) << 10u) | (fraction & 0x3ffu); +} + +void FillInput() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Input[tid * Inputs] = tid | ((tid + 64u) << 8u) | ((255u - tid) << 16u) | (((tid * 3u + 128u) & 0xffu) << 24u); + } +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::array BufferDescriptor(const void* data, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (4u << 16u), count, 0x11016facu}; +} + +void Run(AgcDriver::VulkanDevice& device) { + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size())); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size())); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(SdwaCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto source = Input[tid * Inputs]; + const auto* out = &Output[tid * Results]; + const auto half = [&](std::uint32_t byte) { return HalfBits((source >> (byte * 8u)) & 0xffu); }; + const auto where = [&](std::uint32_t result) { return "sdwa cvt f16 u16: thread " + std::to_string(tid) + " result " + std::to_string(result) + " is " + Hex(out[result]); }; + const auto preserved = (Garbage & 0xffff0000u) | half(3u); + Require(out[0] == preserved, where(0) + ", expected " + Hex(preserved) + ": src0_sel:BYTE_3 dst_sel:WORD_0 dst_unused:UNUSED_PRESERVE must convert the top byte and keep the high half"); + Require(out[1] == half(0u), where(1) + ", expected " + Hex(half(0u)) + ": src0_sel:BYTE_0 dst_sel:DWORD must convert the low byte"); + const auto high = (half(1u) << 16u) | (Garbage & 0xffffu); + Require(out[2] == high, where(2) + ", expected " + Hex(high) + ": src0_sel:BYTE_1 dst_sel:WORD_1 dst_unused:UNUSED_PRESERVE must keep the low half"); + Require(out[3] == half(2u), where(3) + ", expected " + Hex(half(2u)) + ": src0_sel:BYTE_2 dst_sel:DWORD must convert the third byte"); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillInput(); + Run(*device); + Check(); + std::puts("sdwa cvt f16 u16 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/SdwaCvtF32Ubyte.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SdwaCvtF32Ubyte.cpp new file mode 100644 index 0000000000..4468d5d220 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SdwaCvtF32Ubyte.cpp @@ -0,0 +1,104 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 4; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array SdwaCode{ + 0x34020082, 0x34060082, 0xe0302000, 0x80000401, + 0x7e1402ff, 0xabcd1234, 0x7e1602ff, 0xabcd1234, 0x7e1802ff, 0xabcd1234, 0x7e1a02ff, 0xabcd1234, + 0xbf8c3f70, 0x7e1424f9, 0x00040604, 0x7e1624f9, 0x00050604, 0x7e1826f9, 0x000d0604, 0x7e1a28f9, 0x000b0604, + 0xe0702000, 0x80010a03, 0xe0702004, 0x80010b03, 0xe0702008, 0x80010c03, 0xe070200c, 0x80010d03, + 0xbf810000, +}; + +void FillInput() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Input[tid * Inputs] = tid | ((tid + 64u) << 8u) | ((255u - tid) << 16u) | (((tid * 3u + 128u) & 0xffu) << 24u); + } +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::uint32_t FloatBits(std::uint32_t value) { + return std::bit_cast(static_cast(value)); +} + +std::array BufferDescriptor(const void* data, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (4u << 16u), count, 0x11016facu}; +} + +void Run(AgcDriver::VulkanDevice& device) { + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size())); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size())); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(SdwaCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto source = Input[tid * Inputs]; + const auto* out = &Output[tid * Results]; + const auto where = [&](std::uint32_t result) { return "sdwa cvt f32 ubyte: thread " + std::to_string(tid) + " result " + std::to_string(result) + " is " + Hex(out[result]); }; + const auto byte1 = FloatBits((source >> 8u) & 0xffu); + const auto byte3 = FloatBits(source >> 24u); + const auto signFill = FloatBits((source & 0x80000000u) != 0u ? 0xffu : 0u); + Require(out[0] == byte1, where(0) + ", expected " + Hex(byte1) + ": v_cvt_f32_ubyte1 src0_sel:WORD_0 must convert bits 15:8"); + Require(out[1] == byte3, where(1) + ", expected " + Hex(byte3) + ": v_cvt_f32_ubyte1 src0_sel:WORD_1 must convert bits 31:24"); + Require(out[2] == signFill, where(2) + ", expected " + Hex(signFill) + ": v_cvt_f32_ubyte2 src0_sel:WORD_1 src0_sext must convert the sign extension of the high word"); + Require(out[3] == signFill, where(3) + ", expected " + Hex(signFill) + ": v_cvt_f32_ubyte3 src0_sel:BYTE_3 src0_sext must convert the sign extension of the top byte"); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillInput(); + Run(*device); + Check(); + std::puts("sdwa cvt f32 ubyte tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SdwaDestination.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SdwaDestination.cpp index 59a1b54909..c1cf037a15 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SdwaDestination.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SdwaDestination.cpp @@ -17,21 +17,23 @@ using ShaderRecompiler::ShaderStage; constexpr std::uint32_t Threads = 64; constexpr std::uint32_t Inputs = 4; -constexpr std::uint32_t Results = 8; +constexpr std::uint32_t Results = 16; constexpr std::uint32_t Garbage = 0xabcd1234u; alignas(256) std::array Input{}; alignas(256) std::array Output{}; -alignas(256) constexpr std::array SdwaCode{ - 0x34020082, 0x34060083, 0xe0302000, 0x80000401, 0xe0302004, 0x80000501, 0xe0302008, 0x80000601, +alignas(256) constexpr std::array SdwaCode{ + 0x34020082, 0x34060084, 0xe0302000, 0x80000401, 0xe0302004, 0x80000501, 0xe0302008, 0x80000601, 0x7e1402ff, 0xabcd1234, 0x7e1602ff, 0xabcd1234, 0x7e1802ff, 0xabcd1234, 0x7e1a02ff, 0xabcd1234, 0x7e1c02ff, 0xabcd1234, 0x7e1e02ff, 0xabcd1234, 0x7e2002ff, 0xabcd1234, 0x7e2202ff, 0xabcd1234, 0x7e2402ff, 0xabcd1234, 0xbf8c3f70, 0x7e1414f9, 0x00060604, 0x7e161504, 0x7e1814f9, 0x00061505, 0x7e1a14f9, 0x00060604, 0x7e1c14f9, 0x00060605, 0xd746000d, 0x0435210e, 0x641e0cf9, 0x05040606, 0x7e2002f9, 0x00061004, 0x7e2202f9, 0x00061204, 0x7e2402f9, 0x00060104, + 0x7e2602ff, 0xabcd1234, 0x7e266ef9, 0x00061404, + 0x7e2802ff, 0xabcd1234, 0x7e28a0f9, 0x00031404, 0x7e2a02ff, 0xabcd1234, 0x7e2aa2f9, 0x000b1404, 0xe0702000, 0x80010a03, 0xe0702004, 0x80010b03, 0xe0702008, 0x80010c03, 0xe070200c, 0x80010d03, 0xe0702010, 0x80010f03, 0xe0702014, 0x80011003, 0xe0702018, 0x80011103, - 0xe070201c, 0x80011203, 0xbf810000, + 0xe070201c, 0x80011203, 0xe0702020, 0x80011303, 0xe0702024, 0x80011403, 0xe0702028, 0x80011503, 0xbf810000, }; float Quarter(std::uint32_t index) { @@ -106,6 +108,13 @@ void Check() { Require(out[5] == ((Garbage & 0xffffff00u) | low), where(5) + ", expected " + Hex((Garbage & 0xffffff00u) | low) + ": v_mov_b32_sdwa dst_sel:BYTE_0 dst_unused:UNUSED_PRESERVE must keep the other bytes"); Require(out[6] == ((Garbage & 0xff00ffffu) | (low << 16u)), where(6) + ", expected " + Hex((Garbage & 0xff00ffffu) | (low << 16u)) + ": v_mov_b32_sdwa dst_sel:BYTE_2 dst_unused:UNUSED_PRESERVE must keep the other bytes"); Require(out[7] == (low << 8u), where(7) + ", expected " + Hex(low << 8u) + ": v_mov_b32_sdwa dst_sel:BYTE_1 dst_unused:UNUSED_PAD must zero the other bytes"); + const auto inverted = ~in[0] & 0xffffu; + Require(out[8] == ((Garbage & 0xffff0000u) | inverted), where(8) + ", expected " + Hex((Garbage & 0xffff0000u) | inverted) + ": v_not_b32_sdwa dst_sel:WORD_0 dst_unused:UNUSED_PRESERVE must keep the high half"); + const auto top = in[0] >> 24u; + const auto unsignedHalf = HalfBits(static_cast(top)); + Require(out[9] == ((Garbage & 0xffff0000u) | unsignedHalf), where(9) + ", expected " + Hex((Garbage & 0xffff0000u) | unsignedHalf) + ": v_cvt_f16_u16_sdwa dst_sel:WORD_0 dst_unused:UNUSED_PRESERVE src0_sel:BYTE_3 must convert the zero-extended top byte"); + const auto signedHalf = HalfBits(static_cast(static_cast(top))); + Require(out[10] == ((Garbage & 0xffff0000u) | signedHalf), where(10) + ", expected " + Hex((Garbage & 0xffff0000u) | signedHalf) + ": v_cvt_f16_i16_sdwa dst_sel:WORD_0 dst_unused:UNUSED_PRESERVE src0_sel:BYTE_3 src0_sext must convert the sign-extended top byte"); } } 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..8f835d52de --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp @@ -0,0 +1,184 @@ +#include "prx/libSceAgcDriver/Execution/include/AsyncLog.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp" +#include "CompiledVariant.hpp" +#include "ShaderDiskCache.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +void SetEnvironment(const char* name, const std::string& value) { +#ifdef _WIN32 + _putenv_s(name, value.c_str()); +#else + setenv(name, value.c_str(), 1); +#endif +} + +void ClosingStartsNoPreparation() { + using namespace AgcDriver::DriverDetail; + Require(RegistrationPreparationThreads() == 0, "a shader preparation thread ran before anything was prepared"); + CloseRegistrationPreparation(); + Require(RegistrationPreparationThreads() == 0, "closing the shader preparation started preparation threads"); + const auto caller = std::this_thread::get_id(); + bool ranHere = false; + SubmitRegistrationPreparation([&] { ranHere = std::this_thread::get_id() == caller; }); + SubmitLibraryPrebuild([&] { ranHere = ranHere && std::this_thread::get_id() == caller; }); + Require(ranHere, "a preparation submitted after closing ran on another thread"); + Require(RegistrationPreparationThreads() == 0, "a preparation submitted after closing started preparation threads"); +} + +void ClosingJoinsPreparation() { + using namespace AgcDriver::DriverDetail; + std::promise prepared; + std::promise prebuilt; + auto preparedOn = prepared.get_future(); + auto prebuiltOn = prebuilt.get_future(); + std::promise gate; + auto opened = gate.get_future().share(); + SubmitRegistrationPreparation([&] { prepared.set_value(std::this_thread::get_id()); opened.wait(); }); + SubmitLibraryPrebuild([&] { prebuilt.set_value(std::this_thread::get_id()); opened.wait(); }); + const auto caller = std::this_thread::get_id(); + const bool offCaller = preparedOn.get() != caller && prebuiltOn.get() != caller; + auto closing = std::async(std::launch::async, [] { CloseRegistrationPreparation(); }); + const bool waited = closing.wait_for(std::chrono::milliseconds(20)) == std::future_status::timeout; + gate.set_value(); + closing.get(); + Require(offCaller, "a queued preparation ran on the caller instead of a preparation thread"); + Require(waited, "closing returned while preparation tasks were still running"); + Require(RegistrationPreparationThreads() == 0, "closing the shader preparation left its threads running"); + CloseRegistrationPreparation(); + bool ranHere = false; + SubmitRegistrationPreparation([&] { ranHere = std::this_thread::get_id() == caller; }); + SubmitLibraryPrebuild([&] { ranHere = ranHere && std::this_thread::get_id() == caller; }); + Require(ranHere && RegistrationPreparationThreads() == 0, "a closed preparation pool restarted after being joined"); +} + +void StoppingJoinsCompareHelpers() { + using namespace AgcDriver::Graphics; + const std::vector first(std::size_t{2} << 20u, std::byte{1}); + auto second = first; + second.back() = std::byte{2}; + const auto half = first.size() / 2u; + const std::array spans{{{first.data(), second.data(), half}, {first.data() + half, second.data() + half, half}}}; + std::array equal{}; + CompareSpans(spans, equal); + Require(equal[0] == 1u && equal[1] == 0u, "the compare helpers compared the spans wrongly"); + Require(CompareHelpersRunning() == CompareHelpers(), "a parallel compare started no compare helpers"); + StopCompareHelpers(); + Require(CompareHelpersRunning() == 0, "stopping the compare helpers left them running"); + equal = {}; + CompareSpans(spans, equal); + Require(equal[0] == 1u && equal[1] == 0u, "a compare after the helpers stopped compared the spans wrongly"); + Require(CompareHelpersRunning() == 0, "a compare after the helpers stopped started them again"); +} + +void ShutdownJoinsDrawCommitter() { + using namespace AgcDriver::DriverDetail; + auto& pipeline = DrawPipeline::Queue0(); + const auto caller = std::this_thread::get_id(); + std::promise started; + auto startedOn = started.get_future(); + std::promise gate; + auto opened = gate.get_future().share(); + bool queuedRan = false; + pipeline.Enqueue([&started, opened] { + started.set_value(std::this_thread::get_id()); + opened.wait(); + }, {}); + pipeline.Enqueue([&] { queuedRan = true; }, {}); + Require(startedOn.get() != caller, "a pipelined commit ran on the caller instead of the draw committer"); + Require(DrawPipeline::Committers() == 1, "a pipelined commit started no draw committer"); + gate.set_value(); + AgcDriverShutdown_nid_postfix(); + Require(DrawPipeline::Committers() == 0, "the driver's shutdown left the draw committer running"); + Require(queuedRan, "the driver's shutdown dropped a pipelined commit"); + bool ranHere = false; + pipeline.Enqueue([&] { ranHere = std::this_thread::get_id() == caller; }, {}); + Require(ranHere, "a commit enqueued after the shutdown ran on another thread"); + Require(DrawPipeline::Committers() == 0, "a commit enqueued after the shutdown started a draw committer"); +} + +void ShutdownJoinsShaderCacheWriter(bool lateInitialization = false) { + namespace Cache = ShaderRecompiler::ShaderDiskCache; + const auto directory = std::filesystem::temp_directory_path() / ("aps5-shutdown-shader-cache-" + std::to_string(std::random_device{}())); + SetEnvironment("ANYPS5_NO_SHADER_CACHE", "0"); + SetEnvironment("ANYPS5_SHADER_CACHE_DIR", directory.string()); + if (lateInitialization) AgcDriverShutdown_nid_postfix(); + Require(Cache::Enabled(), "the shader cache is not enabled"); + Require(Cache::Writers() == (lateInitialization ? 0u : 1u), "the shader cache writer did not respect the shutdown state"); + const auto variant = std::make_shared(); + const std::vector queued{std::byte{1}}; + const std::vector late{std::byte{2}}; + Cache::Store(queued, variant); + AgcDriverShutdown_nid_postfix(); + Require(Cache::Writers() == 0, "the driver's shutdown left the shader cache writer running"); + Require(std::filesystem::exists(Cache::EntryDirectory() / Cache::EntryName(queued)), "the driver's shutdown dropped a queued shader cache write"); + Cache::Store(late, variant); + Require(std::filesystem::exists(Cache::EntryDirectory() / Cache::EntryName(late)), "a shader cache store after the shutdown was not written at once"); + Require(Cache::Writers() == 0, "a shader cache store after the shutdown started a writer"); + std::error_code error; + std::filesystem::remove_all(directory, error); +} + +void ShutdownPassesHeldAsyncLog() { + static_cast(AgcDriver::DriverDetail::Driver::Get()); + std::promise held; + std::promise release; + auto released = release.get_future(); + std::thread holder([&held, &released] { + const auto lock = AgcDriver::HoldAsyncLog(); + held.set_value(); + released.wait(); + }); + held.get_future().wait(); + auto shutdown = std::async(std::launch::async, [] { AgcDriverShutdown_nid_postfix(); }); + const bool returned = shutdown.wait_for(std::chrono::seconds(10)) == std::future_status::ready; + release.set_value(); + holder.join(); + shutdown.get(); + Require(returned, "the driver's shutdown waited for the async log's lock"); +} + +} + +int main(int argc, char** argv) { + try { + const std::string_view mode = argc > 1 ? argv[1] : ""; + if (mode == "--joined") ClosingJoinsPreparation(); + else if (mode == "--compare-helpers") StoppingJoinsCompareHelpers(); + else if (mode == "--draw-committer") ShutdownJoinsDrawCommitter(); + else if (mode == "--shader-cache-writer") ShutdownJoinsShaderCacheWriter(); + else if (mode == "--late-shader-cache") ShutdownJoinsShaderCacheWriter(true); + else if (mode == "--async-log-held") ShutdownPassesHeldAsyncLog(); + else if (mode == "--exit-after-start") static_cast(AgcDriver::DriverDetail::Driver::Get()); + else ClosingStartsNoPreparation(); + std::cout << "shutdown threads tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SinCosNearZero.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SinCosNearZero.cpp new file mode 100644 index 0000000000..5b5f481300 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SinCosNearZero.cpp @@ -0,0 +1,190 @@ +#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 + +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 = 4; +constexpr std::uint32_t Columns = 4; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e140280u, 0x7e160280u, 0x7e180280u, 0x7e1a0280u, + 0x7e146b04u, 0x7e166d04u, 0x7e18c105u, 0x7e1ac305u, 0xe0781000u, 0x80010a01u, 0xbf810000u, +}; + +constexpr std::uint32_t Rows[Threads][4] = { + {0x80800000u, 0x00008001u, 0x00000000u, 0x00000000u}, + {0x8d800000u, 0x00008400u, 0x00000000u, 0x00000000u}, + {0xb0800000u, 0x00009000u, 0x00000000u, 0x00000000u}, + {0xb58637bdu, 0x0000868eu, 0x00000000u, 0x00000000u}, + {0x00800000u, 0x00000001u, 0x00000000u, 0x00000000u}, + {0x358637bdu, 0x00000400u, 0x00000000u, 0x00000000u}, + {0xb87fda40u, 0x000083ffu, 0x00000000u, 0x00000000u}, + {0x387fda40u, 0x000003ffu, 0x00000000u, 0x00000000u}, + {0xbeffffffu, 0x0000b7ffu, 0x00000000u, 0x00000000u}, + {0x3effffffu, 0x000037ffu, 0x00000000u, 0x00000000u}, + {0x3f000000u, 0x00003800u, 0x00000000u, 0x00000000u}, + {0xbf000000u, 0x0000b800u, 0x00000000u, 0x00000000u}, + {0x3e7fffffu, 0x000033ffu, 0x00000000u, 0x00000000u}, + {0xbe7fffffu, 0x0000b3ffu, 0x00000000u, 0x00000000u}, + {0x3e800000u, 0x00003400u, 0x00000000u, 0x00000000u}, + {0x3f400000u, 0x00003a00u, 0x00000000u, 0x00000000u}, + {0x437f8000u, 0x00005bfcu, 0x00000000u, 0x00000000u}, + {0x43802000u, 0x00005c01u, 0x00000000u, 0x00000000u}, + {0xc47a2000u, 0x0000e3d1u, 0x00000000u, 0x00000000u}, + {0x7f7fc99eu, 0x00007bffu, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x80000000u, 0x00008000u, 0x00000000u, 0x00000000u}, + {0x7f800000u, 0x00007c00u, 0x00000000u, 0x00000000u}, + {0x7fc00000u, 0x00007e00u, 0x00000000u, 0x00000000u}, + {0x3dcccccdu, 0x00002e66u, 0x00000000u, 0x00000000u}, + {0xbdcccccdu, 0x0000ae66u, 0x00000000u, 0x00000000u}, + {0x3e99999au, 0x000034cdu, 0x00000000u, 0x00000000u}, + {0xbf333333u, 0x0000b99au, 0x00000000u, 0x00000000u}, + {0x3fa66666u, 0x00003d33u, 0x00000000u, 0x00000000u}, + {0xc0266666u, 0x0000c133u, 0x00000000u, 0x00000000u}, + {0x41240000u, 0x00004920u, 0x00000000u, 0x00000000u}, + {0xc2c84000u, 0x0000d642u, 0x00000000u, 0x00000000u} +}; + +constexpr std::uint32_t Expected[Threads][Columns] = { + {0x81c90fd5u, 0x3f800000u, 0x00008006u, 0x00003c00u}, + {0x8ec90fd5u, 0x3f800000u, 0x00008e48u, 0x00003c00u}, + {0xb1c90fd5u, 0x3f800000u, 0x00009a48u, 0x00003c00u}, + {0xb6d2d421u, 0x3f800000u, 0x00009126u, 0x00003c00u}, + {0x01c90fd5u, 0x3f800000u, 0x00000006u, 0x00003c00u}, + {0x36d2d421u, 0x3f800000u, 0x00000e48u, 0x00003c00u}, + {0xb9c8f22fu, 0x3f7ffffeu, 0x00008e47u, 0x00003c00u}, + {0x39c8f22fu, 0x3f7ffffeu, 0x00000e47u, 0x00003c00u}, + {0xb4490fdcu, 0xbf800000u, 0x00009648u, 0x0000bc00u}, + {0x34490fdcu, 0xbf800000u, 0x00001648u, 0x0000bc00u}, + {0x00000000u, 0xbf800000u, 0x00000000u, 0x0000bc00u}, + {0x00000000u, 0xbf800000u, 0x00000000u, 0x0000bc00u}, + {0x3f800000u, 0x33c90fd9u, 0x00003c00u, 0x00001248u}, + {0xbf800000u, 0x33c90fd9u, 0x0000bc00u, 0x00001248u}, + {0x3f800000u, 0x00000000u, 0x00003c00u, 0x00000000u}, + {0xbf800000u, 0x00000000u, 0x0000bc00u, 0x00000000u}, + {0x00000000u, 0xbf800000u, 0x00000000u, 0x0000bc00u}, + {0x3f800000u, 0x00000000u, 0x00003c00u, 0x00000000u}, + {0x00000000u, 0xbf800000u, 0x00000000u, 0x0000bc00u}, + {0x00000000u, 0x3f800000u, 0x00000000u, 0x00003c00u}, + {0x00000000u, 0x3f800000u, 0x00000000u, 0x00003c00u}, + {0x80000000u, 0x3f800000u, 0x00008000u, 0x00003c00u}, + {0xffc00000u, 0xffc00000u, 0x0000fe00u, 0x0000fe00u}, + {0x7fc00000u, 0x7fc00000u, 0x00007e00u, 0x00007e00u}, + {0x3f167919u, 0x3f4f1bbdu, 0x000038b4u, 0x00003a79u}, + {0xbf167919u, 0x3f4f1bbdu, 0x0000b8b4u, 0x00003a79u}, + {0x3f737870u, 0xbe9e377cu, 0x00003b9cu, 0x0000b4f3u}, + {0x3f737870u, 0xbe9e377cu, 0x00003b9du, 0x0000b4edu}, + {0x3f737872u, 0xbe9e3770u, 0x00003b9du, 0x0000b4edu}, + {0x3f167910u, 0xbf4f1bc3u, 0x000038b0u, 0x0000ba7cu}, + {0x3f800000u, 0x00000000u, 0x00003c00u, 0x00000000u}, + {0xbf3504f4u, 0x3f3504f3u, 0x0000b9a8u, 0x000039a8u} +}; + +constexpr const char* Names[Columns] = {"v_sin_f32", "v_cos_f32", "v_sin_f16", "v_cos_f16"}; +constexpr std::uint32_t Tolerance[Threads] = {0u, 0u, 0u, 0u, 0u, 0u, 2u, 2u, 1u, 1u, 0u, 0u, 2u, 2u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 12u, 12u, 12u, 12u, 12u, 12u, 12u, 12u}; + +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 Ordered(std::uint32_t bits, std::uint32_t width) { + const std::uint32_t sign = 1u << (width - 1u); + return (bits & sign) != 0u ? sign - (bits & ~sign) : bits | sign; +} + +std::uint32_t Distance(std::uint32_t actual, std::uint32_t expected, std::uint32_t width) { + const std::uint32_t a = Ordered(actual, width); + const std::uint32_t e = Ordered(expected, width); + return a > e ? a - e : e - a; +} + +bool IsPinned(std::uint32_t bits, std::uint32_t width) { + const std::uint32_t magnitude = width == 16u ? bits & 0x7fffu : bits & 0x7fffffffu; + const std::uint32_t infinity = width == 16u ? 0x7c00u : 0x7f800000u; + return magnitude == 0u || magnitude >= infinity; +} + +void Run(AgcDriver::VulkanDevice& device, 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); + 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 = floatMode; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t* out = &Output[tid * Results]; + for (std::uint32_t column = 0; column < Columns; ++column) { + const std::uint32_t width = column < 2u ? 32u : 16u; + const std::uint32_t mask = width == 16u ? 0xffffu : 0xffffffffu; + const std::uint32_t actual = out[column] & mask; + const std::uint32_t expected = Expected[tid][column] & mask; + const std::string name = "sin cos near zero: lane " + std::to_string(tid) + " " + Names[column] + " is " + Hex(actual) + ", expected " + Hex(expected); + if (IsPinned(expected, width)) { + Require(actual == expected, name); + continue; + } + Require(Distance(actual, expected, width) <= Tolerance[tid], name + " (tolerance " + std::to_string(Tolerance[tid]) + " ulp)"); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}); + Check(); + std::puts("sin cos near zero 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/SinCosReduction.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SinCosReduction.cpp new file mode 100644 index 0000000000..f4e1ed0b17 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SinCosReduction.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 = 32; +constexpr std::uint32_t Results = 2; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020081, 0xe0302000, 0x80000400, 0xbf8c3f70, 0x7e146b04, 0x7e166d04, 0xe0702000, 0x80010a01, + 0xe0702004, 0x80010b01, 0xbf810000, +}; + +struct Row { + std::uint32_t x, sin, cos; +}; + +constexpr std::array Rows{{ + {0x80800000u, 0x81c90fd5u, 0x3f800000u}, + {0x8f800000u, 0x90c90fd5u, 0x3f800000u}, + {0xa3d42337u, 0xa5269cc5u, 0x3f800000u}, + {0xb0800000u, 0xb1c90fd5u, 0x3f800000u}, + {0xb2800000u, 0xb3c90fd5u, 0x3f800000u}, + {0xb3000000u, 0xb4490fd5u, 0x3f800000u}, + {0xb3800000u, 0xb4c90fd5u, 0x3f800000u}, + {0xb3c00000u, 0xb516cbe0u, 0x3f800000u}, + {0xb8732b55u, 0xb9befc10u, 0x3f7ffffeu}, + {0xbc23d70au, 0xbd80984du, 0x3f7f7eadu}, + {0xbe82a4d7u, 0xbf7fdd81u, 0xbd04dfbau}, + {0xbe000000u, 0xbf3504f4u, 0x3f3504f3u}, + {0xbeb33333u, 0xbf4f1bbdu, 0xbf167918u}, + {0xbef5c28fu, 0xbe005760u, 0xbf7dfb3au}, + {0x00800000u, 0x01c90fd5u, 0x3f800000u}, + {0x30800000u, 0x31c90fd5u, 0x3f800000u}, + {0x33800000u, 0x34c90fd5u, 0x3f800000u}, + {0x3c23d70au, 0x3d80984du, 0x3f7f7eadu}, + {0x3e82a4d7u, 0x3f7fdd81u, 0xbd04dfbau}, + {0x3f3d70a4u, 0xbf7f7eadu, 0xbd809845u}, + {0xbf3d70a4u, 0x3f7f7eadu, 0xbd809845u}, + {0x42c8999au, 0x3f73780du, 0xbe9e39ddu}, + {0xc2c8999au, 0xbf73780du, 0xbe9e39ddu}, + {0xc348b333u, 0x3f73780du, 0xbe9e39ddu}, + {0x80000000u, 0x80000000u, 0x3f800000u}, + {0x00000000u, 0x00000000u, 0x3f800000u}, + {0xbf000000u, 0x00000000u, 0xbf800000u}, + {0xbe800000u, 0xbf800000u, 0x00000000u}, + {0xbf800000u, 0x00000000u, 0x3f800000u}, + {0xcb000001u, 0x00000000u, 0x3f800000u}, + {0x7f7fffffu, 0x00000000u, 0x3f800000u}, + {0xff800000u, 0xffc00000u, 0xffc00000u}, +}}; + +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) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) Input[tid] = Rows[tid].x; + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size())); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size())); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(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; + 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; +} + +bool Near(std::uint32_t actual, std::uint32_t expected) { + const float value = std::bit_cast(expected); + return std::abs(static_cast(std::bit_cast(actual)) - value) <= std::abs(value) * 0x1p-10; +} + +void Expect(std::uint32_t tid, const char* name, std::uint32_t actual, std::uint32_t expected, bool accurate) { + const float value = std::bit_cast(expected); + const bool exact = !std::isfinite(value) || value == 0.0f; + Require(exact ? actual == expected : !accurate || Near(actual, expected), std::string("sin cos reduction: lane ") + std::to_string(tid) + " " + name + " of " + Hex(Rows[tid].x) + " is " + Hex(actual) + (exact ? ", expected " : ", expected near ") + Hex(expected)); +} + +void Check() { + bool accurate = true; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const float x = std::bit_cast(Rows[tid].x); + if (x > 0.0f && x < 0.5f) { + accurate = accurate && Near(Output[tid * Results], Rows[tid].sin) && Near(Output[tid * Results + 1], Rows[tid].cos); + } + } + if (!accurate) { + std::puts("the device's sin and cos are not within 2^-10 of the hardware for small positive inputs, so only zero, infinite and NaN results are checked"); + } + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Expect(tid, "v_sin_f32", Output[tid * Results], Rows[tid].sin, accurate); + Expect(tid, "v_cos_f32", Output[tid * Results + 1], Rows[tid].cos, accurate); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device); + Check(); + std::puts("sin cos 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/SkipShader.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SkipShader.cpp new file mode 100644 index 0000000000..377e819b46 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SkipShader.cpp @@ -0,0 +1,71 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +alignas(256) constexpr std::array Skipped{0xd7460000u, 0x04010c04u, 0x7e0202ffu, 0x12345678u, 0xe0702000u, 0x80000100u, 0xbf810000u}; +alignas(256) constexpr std::array Kept{0xd7460000u, 0x04010c04u, 0x7e0202ffu, 0x12345678u, 0xe0702000u, 0x80000100u, 0xbf810000u}; +constexpr std::uint32_t Untouched = 0xcafebabeu; +constexpr std::uint32_t Stored = 0x12345678u; +alignas(256) std::array SkippedOutput{}; +alignas(256) std::array KeptOutput{}; + +void Dispatch(const std::array& code, std::array& output) { + const auto address = reinterpret_cast(output.data()); + const std::array data{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (4u << 16u), static_cast(output.size()), 0x00014204u}; + const auto program = reinterpret_cast(code.data()); + std::vector words{ + 0xc0037600u, 0x207, 64, 1, 1, + 0xc0027600u, 0x20c, static_cast(program >> 8u), static_cast(program >> 40u), + 0xc0017600u, 0x213, (4u << 1u) | (1u << 7u), + 0xc0047600u, 0x240, data[0], data[1], data[2], data[3], + 0xc0031500u, 1, 1, 1, 0x41u, + }; + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(address, sizeof(output)); +} + +} + +int main() { + try { + char value[32]; + std::snprintf(value, sizeof(value), "%llx", static_cast(reinterpret_cast(Skipped.data()))); +#ifdef _WIN32 + Require(_putenv_s("APS5_SKIP_SHADER", value) == 0, "cannot set APS5_SKIP_SHADER"); +#else + Require(setenv("APS5_SKIP_SHADER", value, 1) == 0, "cannot set APS5_SKIP_SHADER"); +#endif + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + SkippedOutput.fill(Untouched); + KeptOutput.fill(Untouched); + Dispatch(Skipped, SkippedOutput); + Dispatch(Kept, KeptOutput); + for (std::size_t i = 0; i < SkippedOutput.size(); ++i) { + Require(SkippedOutput[i] == Untouched, "the dispatch of the skipped shader stored dword " + std::to_string(i)); + Require(KeptOutput[i] == Stored, "the dispatch of another shader did not store dword " + std::to_string(i)); + } + AgcDriverShutdown_nid_postfix(); + std::puts("skip shader tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SparseBufferHoles.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SparseBufferHoles.cpp new file mode 100644 index 0000000000..a7a25f9f88 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SparseBufferHoles.cpp @@ -0,0 +1,397 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "VulkanTestDevice.hpp" +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace AgcDriver::Graphics; + +constexpr std::size_t PageBytes = 65536; +constexpr std::size_t BufferBytes = 3 * PageBytes; +constexpr std::size_t ReservedBytes = 4 * PageBytes; +constexpr std::uint32_t WorkgroupSize = 64; +constexpr std::array Patterns{0x11223344u, 0x8899aabbu, 0x55667788u}; + +class Device { +public: + Device() { +#ifdef _WIN32 + library = SDL_LoadObject("vulkan-1.dll"); +#else + library = SDL_LoadObject("libvulkan.so.1"); +#endif + Require(library != nullptr, "cannot load Vulkan"); + try { + instanceProc = reinterpret_cast(SDL_LoadFunction(library, "vkGetInstanceProcAddr")); + Require(instanceProc != nullptr, "missing Vulkan instance resolver"); + VkApplicationInfo application{VK_STRUCTURE_TYPE_APPLICATION_INFO}; + application.apiVersion = VK_API_VERSION_1_1; + VkInstanceCreateInfo info{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; + info.pApplicationInfo = &application; + Check(function("vkCreateInstance")(&info, nullptr, &instance), "vkCreateInstance"); + std::uint32_t count = 0; + const auto enumerate = function("vkEnumeratePhysicalDevices"); + Check(enumerate(instance, &count, nullptr), "vkEnumeratePhysicalDevices"); + Require(count != 0, "no Vulkan device"); + std::vector devices(count); + Check(enumerate(instance, &count, devices.data()), "vkEnumeratePhysicalDevices"); + const auto properties = function("vkGetPhysicalDeviceProperties"); + int rank = -1; + for (const auto candidate : devices) { + VkPhysicalDeviceProperties described{}; + properties(candidate, &described); + if (described.apiVersion < VK_API_VERSION_1_1) continue; + const int candidateRank = described.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU ? 2 : described.deviceType == VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU ? 1 : 0; + if (candidateRank <= rank) continue; + context.physical = candidate; + rank = candidateRank; + } + Require(context.physical != VK_NULL_HANDLE, "no Vulkan 1.1 device"); + VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; + VkPhysicalDeviceProperties2 selected{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &subgroup}; + function("vkGetPhysicalDeviceProperties2")(context.physical, &selected); + context.limits = selected.properties.limits; + const auto queues = function("vkGetPhysicalDeviceQueueFamilyProperties"); + queues(context.physical, &count, nullptr); + std::vector families(count); + queues(context.physical, &count, families.data()); + std::uint32_t family = 0; + while (family < count && (families[family].queueFlags & VK_QUEUE_COMPUTE_BIT) == 0) ++family; + Require(family < count, "no Vulkan compute queue"); + std::printf("Selected GPU name=%s vendor=0x%x device=0x%x subgroup=%u\n", selected.properties.deviceName, selected.properties.vendorID, selected.properties.deviceID, subgroup.subgroupSize); + std::printf("sparse buffer holes GPU: api=%u.%u.%u; queueFamily=%u\n", VK_API_VERSION_MAJOR(selected.properties.apiVersion), VK_API_VERSION_MINOR(selected.properties.apiVersion), VK_API_VERSION_PATCH(selected.properties.apiVersion), family); + const float priority = 1; + VkDeviceQueueCreateInfo queue{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; + queue.queueFamilyIndex = family; + queue.queueCount = 1; + queue.pQueuePriorities = &priority; + VkDeviceCreateInfo device{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO}; + device.queueCreateInfoCount = 1; + device.pQueueCreateInfos = &queue; + Check(function("vkCreateDevice")(context.physical, &device, nullptr, &context.device), "vkCreateDevice"); + context.deviceProc = function("vkGetDeviceProcAddr"); + function("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory); + context.Function("vkGetDeviceQueue")(context.device, family, 0, &context.queue); + VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + pool.queueFamilyIndex = family; + Check(context.Function("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool"); + } catch (...) { + release(); + throw; + } + } + + ~Device() { release(); } + Device(const Device&) = delete; + Device& operator=(const Device&) = delete; + const Context& GetContext() const { return context; } + +private: + template + TFunction function(const char* name) const { + const auto result = reinterpret_cast(instanceProc(instance, name)); + Require(result != nullptr, name); + return result; + } + + void release() noexcept { + if (context.pool != VK_NULL_HANDLE) context.Function("vkDestroyCommandPool")(context.device, context.pool, nullptr); + context.bufferPool.reset(); + if (context.device != VK_NULL_HANDLE) function("vkDestroyDevice")(context.device, nullptr); + if (instance != VK_NULL_HANDLE) function("vkDestroyInstance")(instance, nullptr); + if (library != nullptr) SDL_UnloadObject(library); + } + + void* library = nullptr; + PFN_vkGetInstanceProcAddr instanceProc = nullptr; + VkInstance instance = VK_NULL_HANDLE; + Context context{}; +}; + +class GuestPages { +public: + explicit GuestPages(bool committed) { +#ifdef _WIN32 + memory = static_cast(VirtualAlloc(nullptr, ReservedBytes, MEM_RESERVE, PAGE_NOACCESS)); +#else + const auto block = mmap(nullptr, ReservedBytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (block != MAP_FAILED) memory = static_cast(block); +#endif + Require(memory != nullptr, "cannot reserve sparse guest pages"); + try { + for (std::size_t page = 0; page < Patterns.size(); ++page) { + if (page == 1 && !committed) continue; + auto* begin = memory + page * PageBytes; +#ifdef _WIN32 + Require(VirtualAlloc(begin, PageBytes, MEM_COMMIT, PAGE_READWRITE) == begin, "cannot commit sparse guest page"); +#else + Require(mprotect(begin, PageBytes, PROT_READ | PROT_WRITE) == 0, "cannot commit sparse guest page"); +#endif + for (std::size_t offset = 0; offset < PageBytes; offset += sizeof(std::uint32_t)) std::memcpy(begin + offset, &Patterns[page], sizeof(std::uint32_t)); + } + } catch (...) { + release(); + throw; + } + } + + ~GuestPages() { release(); } + GuestPages(const GuestPages&) = delete; + GuestPages& operator=(const GuestPages&) = delete; + std::uint64_t Address() const { return reinterpret_cast(memory); } + +private: + void release() noexcept { +#ifdef _WIN32 + if (memory != nullptr) VirtualFree(memory, 0, MEM_RELEASE); +#else + if (memory != nullptr) munmap(memory, ReservedBytes); +#endif + memory = nullptr; + } + + std::byte* memory = nullptr; +}; + +void Emit(std::vector& words, spv::Op opcode, std::initializer_list operands) { + words.push_back(static_cast((operands.size() + 1u) << 16u) | opcode); + words.insert(words.end(), operands); +} + +std::vector CopyModule() { + enum : std::uint32_t { Void = 1, FunctionType, Uint, Uint3, Words, Block, BlockPointer, Source, Destination, UintPointer, InputPointer, Invocation, Zero, Main, Label, InvocationValue, Index, SourcePointer, Value, DestinationPointer, Bound }; + std::vector words{spv::MagicNumber, 0x00010300u, 0u, Bound, 0u}; + Emit(words, spv::OpCapability, {spv::CapabilityShader}); + Emit(words, spv::OpMemoryModel, {spv::AddressingModelLogical, spv::MemoryModelGLSL450}); + Emit(words, spv::OpEntryPoint, {spv::ExecutionModelGLCompute, Main, 0x6e69616du, 0u, Invocation}); + Emit(words, spv::OpExecutionMode, {Main, spv::ExecutionModeLocalSize, WorkgroupSize, 1, 1}); + Emit(words, spv::OpDecorate, {Invocation, spv::DecorationBuiltIn, spv::BuiltInGlobalInvocationId}); + Emit(words, spv::OpDecorate, {Words, spv::DecorationArrayStride, 4}); + Emit(words, spv::OpDecorate, {Block, spv::DecorationBlock}); + Emit(words, spv::OpMemberDecorate, {Block, 0, spv::DecorationOffset, 0}); + Emit(words, spv::OpDecorate, {Source, spv::DecorationDescriptorSet, 0}); + Emit(words, spv::OpDecorate, {Source, spv::DecorationBinding, 0}); + Emit(words, spv::OpDecorate, {Destination, spv::DecorationDescriptorSet, 0}); + Emit(words, spv::OpDecorate, {Destination, spv::DecorationBinding, 1}); + Emit(words, spv::OpTypeVoid, {Void}); + Emit(words, spv::OpTypeFunction, {FunctionType, Void}); + Emit(words, spv::OpTypeInt, {Uint, 32, 0}); + Emit(words, spv::OpTypeVector, {Uint3, Uint, 3}); + Emit(words, spv::OpTypeRuntimeArray, {Words, Uint}); + Emit(words, spv::OpTypeStruct, {Block, Words}); + Emit(words, spv::OpTypePointer, {BlockPointer, spv::StorageClassStorageBuffer, Block}); + Emit(words, spv::OpVariable, {BlockPointer, Source, spv::StorageClassStorageBuffer}); + Emit(words, spv::OpVariable, {BlockPointer, Destination, spv::StorageClassStorageBuffer}); + Emit(words, spv::OpTypePointer, {UintPointer, spv::StorageClassStorageBuffer, Uint}); + Emit(words, spv::OpTypePointer, {InputPointer, spv::StorageClassInput, Uint3}); + Emit(words, spv::OpVariable, {InputPointer, Invocation, spv::StorageClassInput}); + Emit(words, spv::OpConstant, {Uint, Zero, 0}); + Emit(words, spv::OpFunction, {Void, Main, 0, FunctionType}); + Emit(words, spv::OpLabel, {Label}); + Emit(words, spv::OpLoad, {Uint3, InvocationValue, Invocation}); + Emit(words, spv::OpCompositeExtract, {Uint, Index, InvocationValue, 0}); + Emit(words, spv::OpAccessChain, {UintPointer, SourcePointer, Source, Zero, Index}); + Emit(words, spv::OpLoad, {Uint, Value, SourcePointer}); + Emit(words, spv::OpAccessChain, {UintPointer, DestinationPointer, Destination, Zero, Index}); + Emit(words, spv::OpStore, {DestinationPointer, Value}); + Emit(words, spv::OpReturn, {}); + Emit(words, spv::OpFunctionEnd, {}); + return words; +} + +class CopyPipeline { +public: + explicit CopyPipeline(const Context& context) : context(context) { + try { + const std::array bindings{{{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}, {1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}}}; + VkDescriptorSetLayoutCreateInfo setLayoutInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + setLayoutInfo.bindingCount = static_cast(bindings.size()); + setLayoutInfo.pBindings = bindings.data(); + Check(context.Function("vkCreateDescriptorSetLayout")(context.device, &setLayoutInfo, nullptr, &setLayout), "vkCreateDescriptorSetLayout"); + const VkDescriptorPoolSize poolSize{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2}; + VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + poolInfo.maxSets = 1; + poolInfo.poolSizeCount = 1; + poolInfo.pPoolSizes = &poolSize; + Check(context.Function("vkCreateDescriptorPool")(context.device, &poolInfo, nullptr, &descriptorPool), "vkCreateDescriptorPool"); + VkDescriptorSetAllocateInfo allocateInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + allocateInfo.descriptorPool = descriptorPool; + allocateInfo.descriptorSetCount = 1; + allocateInfo.pSetLayouts = &setLayout; + Check(context.Function("vkAllocateDescriptorSets")(context.device, &allocateInfo, &set), "vkAllocateDescriptorSets"); + const auto module = CopyModule(); + VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + moduleInfo.codeSize = module.size() * sizeof(std::uint32_t); + moduleInfo.pCode = module.data(); + Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &shader), "vkCreateShaderModule"); + VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layoutInfo.setLayoutCount = 1; + layoutInfo.pSetLayouts = &setLayout; + Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &pipelineLayout), "vkCreatePipelineLayout"); + VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; + pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; + pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; + pipelineInfo.stage.module = shader; + pipelineInfo.stage.pName = "main"; + pipelineInfo.layout = pipelineLayout; + Check(context.Function("vkCreateComputePipelines")(context.device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateComputePipelines"); + } catch (...) { + release(); + throw; + } + } + + ~CopyPipeline() { release(); } + CopyPipeline(const CopyPipeline&) = delete; + CopyPipeline& operator=(const CopyPipeline&) = delete; + + void Copy(const VkDescriptorBufferInfo& source, Buffer& destination) const { + Require(source.range == BufferBytes && destination.Bytes().size() == BufferBytes, "sparse buffer holes GPU copy has an unexpected extent"); + const std::array infos{{source, {destination.Handle(), 0, BufferBytes}}}; + std::array writes{}; + for (std::uint32_t binding = 0; binding < writes.size(); ++binding) { + writes[binding] = {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + writes[binding].dstSet = set; + writes[binding].dstBinding = binding; + writes[binding].descriptorCount = 1; + writes[binding].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + writes[binding].pBufferInfo = &infos[binding]; + } + context.Function("vkUpdateDescriptorSets")(context.device, static_cast(writes.size()), writes.data(), 0, nullptr); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + CommandBatch batch(context); + RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + context.Function("vkCmdBindPipeline")(batch.Handle(), VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); + context.Function("vkCmdBindDescriptorSets")(batch.Handle(), VK_PIPELINE_BIND_POINT_COMPUTE, pipelineLayout, 0, 1, &set, 0, nullptr); + context.Function("vkCmdDispatch")(batch.Handle(), static_cast(BufferBytes / sizeof(std::uint32_t) / WorkgroupSize), 1, 1); + RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + batch.SubmitAndWait(); + destination.Invalidate(); + } + +private: + void release() noexcept { + if (pipeline != VK_NULL_HANDLE) context.Function("vkDestroyPipeline")(context.device, pipeline, nullptr); + if (pipelineLayout != VK_NULL_HANDLE) context.Function("vkDestroyPipelineLayout")(context.device, pipelineLayout, nullptr); + if (shader != VK_NULL_HANDLE) context.Function("vkDestroyShaderModule")(context.device, shader, nullptr); + if (descriptorPool != VK_NULL_HANDLE) context.Function("vkDestroyDescriptorPool")(context.device, descriptorPool, nullptr); + if (setLayout != VK_NULL_HANDLE) context.Function("vkDestroyDescriptorSetLayout")(context.device, setLayout, nullptr); + } + + const Context& context; + VkDescriptorSetLayout setLayout = VK_NULL_HANDLE; + VkDescriptorPool descriptorPool = VK_NULL_HANDLE; + VkDescriptorSet set = VK_NULL_HANDLE; + VkShaderModule shader = VK_NULL_HANDLE; + VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; + VkPipeline pipeline = VK_NULL_HANDLE; +}; + +void Run(const Context& context, const CopyPipeline& pipeline, bool writable, const std::string& shape) { + const auto label = std::string("sparse buffer holes GPU: ") + (writable ? "writable " : "readable ") + shape; + const bool committed = shape == "committed"; + const auto requested = shape == "shortened" ? ReservedBytes : BufferBytes; + GuestPages pages(committed); + const auto ranges = AgcDriver::GuestMemory::DescribeCommitted(pages.Address(), requested); + Require(ranges.whole == committed, label + ": unexpected guest commitment"); + if (!committed) { + Require(ranges.ranges == std::vector>{{pages.Address(), pages.Address() + PageBytes}, {pages.Address() + 2 * PageBytes, pages.Address() + BufferBytes}}, label + ": guest hole is not inaccessible"); + } + Buffer readback(context, BufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); + std::memset(readback.Bytes().data(), 0xcc, BufferBytes); + VkBuffer primed = VK_NULL_HANDLE; + { + Buffer earlier(context, BufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + primed = earlier.Handle(); + std::memset(earlier.Bytes().data(), 0xa5, BufferBytes); + } + GuestBufferMemory memory(context); + if (writable) memory.AddWritable(pages.Address(), requested); + else memory.AddReadable(pages.Address(), requested); + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + memory.Upload(false); + } + std::uint32_t adjustment = 0; + const auto descriptor = memory.Descriptor(pages.Address(), requested, adjustment); + Require(descriptor.buffer == primed && descriptor.offset == 0 && descriptor.range == BufferBytes && adjustment == 0, label + ": the upload did not reuse the primed buffer at the expected extent"); + std::cout << label << " reused primed buffer; requested=" << requested << "; allocated=" << descriptor.range << '\n'; + pipeline.Copy(descriptor, readback); + const auto bytes = readback.Bytes(); + std::size_t mismatches = 0; + std::size_t staleWords = 0; + for (std::size_t offset = 0; offset < BufferBytes; offset += sizeof(std::uint32_t)) { + std::uint32_t actual = 0; + std::memcpy(&actual, bytes.data() + offset, sizeof(actual)); + const auto page = offset / PageBytes; + if (page == 1 && !committed) { + mismatches += actual != 0; + staleWords += actual == 0xa5a5a5a5u; + } else { + Require(actual == Patterns[page], label + ": GPU readback changed backed guest bytes"); + } + } + std::cout << label << " readback checked; words=" << BufferBytes / sizeof(std::uint32_t) << "; nonzeroHoleWords=" << mismatches << "; staleHoleWords=" << staleWords << '\n'; + Require(mismatches == 0, label + ": GPU readback exposed stale pooled bytes in guest holes"); + std::cout << label << " passed\n"; +} + +} + +int main() { + try { + std::unique_ptr device; + try { + device = std::make_unique(); + } catch (const std::exception& error) { + if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw; + std::printf("skipped, no usable Vulkan device: %s\n", error.what()); + return VulkanTestSkipped; + } + const auto& context = device->GetContext(); + Require(context.limits.maxStorageBufferRange >= BufferBytes && context.limits.maxComputeWorkGroupSize[0] >= WorkgroupSize && context.limits.maxComputeWorkGroupInvocations >= WorkgroupSize && context.limits.maxComputeWorkGroupCount[0] >= BufferBytes / sizeof(std::uint32_t) / WorkgroupSize, "Vulkan limits cannot support the bounded sparse buffer readback"); + CopyPipeline pipeline(context); + bool passed = true; + for (const bool writable : {false, true}) { + for (const auto* shape : {"interior", "shortened", "committed"}) { + try { + Run(context, pipeline, writable, shape); + } catch (const std::exception& error) { + if (std::string(error.what()).find(": GPU readback exposed stale pooled bytes in guest holes") == std::string::npos) throw; + std::cerr << error.what() << '\n'; + passed = false; + } + } + } + if (!passed) return 1; + std::puts("Sparse buffer hole GPU readback tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp new file mode 100644 index 0000000000..949c7ef5f4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp @@ -0,0 +1,250 @@ +#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 +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +int main() { + std::printf("skipped, udmabuf imports are Linux only\n"); + return VulkanTestSkipped; +} +#else +namespace 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; +int backing = -1; + +bool Resolve(std::uintptr_t address, std::size_t bytes, int* file, std::uint64_t* offset) { + const auto begin = reinterpret_cast(mapping); + if (address < begin || address + bytes > begin + mappingBytes) return false; + *file = dup(backing); + *offset = address - begin; + return *file >= 0; +} + +std::uint64_t UdmabufLimitBytes() { + std::ifstream limit("/sys/module/udmabuf/parameters/size_limit_mb"); + std::uint64_t megabytes = 64; + limit >> megabytes; + return megabytes << 20u; +} + +bool Imported(VkDevice device, std::uint64_t address, std::size_t bytes) { + AgcDriver::Graphics::Context probe{}; + probe.device = device; + probe.hostImportAlignment = 1; + return AgcDriver::Graphics::HostImportCovers(probe, address, bytes); +} + +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() { + try { + const int udmabuf = open("/dev/udmabuf", O_RDWR | O_CLOEXEC); + if (udmabuf < 0) { + std::printf("skipped, host imports are unavailable: /dev/udmabuf is not accessible\n"); + return VulkanTestSkipped; + } + close(udmabuf); + 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; + + constexpr std::size_t ProbeBytes = 65536; + auto* probe = static_cast(std::aligned_alloc(ProbeBytes, ProbeBytes)); + Require(probe != nullptr, "sparse import: cannot allocate the probe"); + GuestAllocations::Mutation().Add(probe, ProbeBytes, true, true, true); + bool adopted = false; + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + const std::array pattern{1u, 2u, 3u, 4u}; + adopted = device->FillBuffer(reinterpret_cast(probe), ProbeBytes, pattern); + device->WaitIdle(); + } + if (!adopted) { + std::printf("skipped, the device does not import guest memory\n"); + return VulkanTestSkipped; + } + + const auto bytes = (UdmabufLimitBytes() / ChunkBytes + 2) * ChunkBytes; + mappingBytes = bytes + ChunkBytes; + backing = memfd_create("anyps5-sparse-import-test", MFD_CLOEXEC | MFD_ALLOW_SEALING); + Require(backing >= 0 && ftruncate(backing, static_cast(mappingBytes)) == 0, "sparse import: cannot make the backing file"); + Require(fcntl(backing, F_ADD_SEALS, F_SEAL_SHRINK) == 0, "sparse import: cannot seal the backing file"); + void* mapped = mmap(nullptr, mappingBytes, PROT_READ | PROT_WRITE, MAP_SHARED, backing, 0); + Require(mapped != MAP_FAILED, "sparse import: cannot map the backing file"); + mapping = static_cast(mapped); + GuestArena::GuestArenaSetSharedBacking_nid_postfix(Resolve); + + const auto Register = [](std::uint8_t* memory, std::uint64_t size) { + GuestAllocations::Mutation().Add(memory, static_cast(size), true, true, true); + GuestAllocations::Mapped ranges; + for (const auto& range : GuestAllocations::GuestAllocationsAcquire_nid_postfix()) { + if (range->address == reinterpret_cast(memory)) ranges.push_back(range); + } + Require(ranges.size() == 1, "sparse import: the allocation is not registered"); + return ranges; + }; + + auto* small = mapping + bytes; + constexpr std::uint64_t SmallBytes = std::uint64_t{1} << 20u; + 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; + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + const std::array pattern{0x5EEDF00Du, 0x5EEDF00Du, 0x5EEDF00Du, 0x5EEDF00Du}; + filled = device->FillBuffer(address + boundary - 32768, 65536, pattern); + device->WaitIdle(); + } + Require(filled, "sparse import: the fill across the chunk boundary was not served by the import"); + for (std::uint64_t offset = boundary - 32768; offset < boundary + 32768; offset += 4) { + std::uint32_t value = 0; + std::memcpy(&value, mapping + offset, sizeof(value)); + Require(value == 0x5EEDF00Du, "sparse import: the GPU fill did not reach the backing memory across the chunk boundary"); + } + Require(mapping[boundary - 32769] == 0x11 && mapping[boundary + 32768] == 0x11, "sparse import: the GPU fill reached past its range"); + GuestAllocations::Mutation().Remove(mapping); + + 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"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} +#endif 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/Srgb8ColorTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Srgb8ColorTarget.cpp index c1315cbbda..58de25068d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Srgb8ColorTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Srgb8ColorTarget.cpp @@ -127,7 +127,7 @@ struct Block { 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); + mutation.Add(data, bytes, true, true, true); } ~Block() { AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); @@ -227,13 +227,13 @@ void Draw(AgcDriver::VulkanDevice& device, std::vector(pixelCode.data()), pixelCode, 0, {}}, {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, target, - {0, 0, vertexPush, 128 - vertexPush} + PixelPushLayout(vertexPush, target) }; fragment.useCache = false; const auto pixelResult = ShaderRecompiler::Recompile(fragment); const std::array shaders{{ {ShaderStage::Vertex, &vertexResult, 0}, - {ShaderStage::Fragment, &pixelResult, vertexPush} + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} }}; AgcDriver::Graphics::State state{}; 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/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/StencilRefExport.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StencilRefExport.cpp new file mode 100644 index 0000000000..ac0318165a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StencilRefExport.cpp @@ -0,0 +1,159 @@ +#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 + +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}; +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, 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}, +}}; + +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, 0x300u}, {0x1b4, 0x300u}, {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) { + Pixels.fill(Kept); + 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()); + + 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, PixelInfo(shaderControl), 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.depth = AgcDriver::Graphics::DepthTarget{reinterpret_cast(DepthMemory.data()), reinterpret_cast(StencilMemory.data()), {Width, Height}, VK_FORMAT_D32_SFLOAT_S8_UINT, 1.0f, 0}; + 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 = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Vertices.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +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; + if (!TargetHasCapability(device->Target(), spv::CapabilityStencilExportEXT)) { + std::puts("skipped, the device lacks VK_EXT_shader_stencil_export"); + return VulkanTestSkipped; + } + constexpr std::uint32_t exported = 0x52u; + constexpr std::uint32_t plain = 0x50u; + const VkStencilOpState replace{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_REPLACE, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_ALWAYS, 0xffu, 0xffu, 0u}; + const auto equal = [](std::uint32_t reference) { return VkStencilOpState{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_EQUAL, 0xffu, 0u, reference}; }; + Draw(*device, ColumnPixelCode, exported, replace); + Check([](std::uint32_t, std::uint32_t) { return true; }, "the draw that replaces the stencil with the exported column"); + for (const auto column : {0u, 5u, 37u, 63u}) { + Draw(*device, PlainPixelCode, plain, equal(column)); + Check([&](std::uint32_t x, std::uint32_t) { return x == column; }, "stencil equal to " + std::to_string(column) + " after the exported column was stored"); + } + Draw(*device, ColumnPixelCode, exported, equal(200u)); + Check([](std::uint32_t, std::uint32_t) { return true; }, "the exported column tested against the stored column"); + Draw(*device, RowPixelCode, exported, equal(200u)); + Check([](std::uint32_t x, std::uint32_t y) { return x == y; }, "the exported row tested against the stored column"); + Draw(*device, ColumnPixelCode, plain, equal(9u)); + Check([](std::uint32_t x, std::uint32_t) { return x == 9u; }, "a column export without STENCIL_TEST_VAL_EXPORT_ENABLE"); + std::puts("stencil reference export tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/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 9a1cc61c80..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; @@ -30,12 +34,10 @@ constexpr std::uint64_t FixedBudget = 16ull << 20u; constexpr std::uint64_t TestBudget = 32ull << 20u; constexpr std::uint32_t MaxSurfaces = 80; -alignas(256) constexpr std::array LoadStoreCode{ - 0x7e040280, 0x7e060280, 0xf0001108, 0x00010402, 0xbf8c3f70, 0xf0200108, 0x00010402, 0xe0700000, 0x80000400, 0xbf810000, +alignas(256) constexpr std::array StoreCode{ + 0x7e020280, 0x7e0402f2, 0x7e0602f2, 0x7e0802f2, 0x7e0a02f2, 0xf0201f08, 0x00010200, 0xbf810000, }; -alignas(256) std::array Output{}; - std::uint32_t Marker(std::uint32_t surface) { return 0x5a000000u | surface; } @@ -53,16 +55,11 @@ std::array TextureDescriptor(std::uint64_t address) { }; } -std::shared_ptr Load(AgcDriver::VulkanDevice& device, std::uint64_t base, std::uint32_t surface) { - const auto address = base + surface * Stride; - Output.fill(0xdeadbeefu); +void Record(AgcDriver::VulkanDevice& device, std::uint64_t address) { std::vector userData(16, 0u); - const auto output = reinterpret_cast(Output.data()); - const std::array buffer{static_cast(output), static_cast((output >> 32u) & 0xffffu), static_cast(Output.size() * 4u), 0x31016facu}; const auto texture = TextureDescriptor(address); - std::copy(buffer.begin(), buffer.end(), userData.begin()); std::copy(texture.begin(), texture.end(), userData.begin() + 4); - const std::span code(LoadStoreCode); + const std::span code(StoreCode); const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; const ShaderRecompiler::ShaderComputeStageInfo compute{{32, 1, 1}, 0u, {false, false, false}, false, 1}; ShaderRecompiler::RecompileRequest request{ @@ -71,20 +68,73 @@ std::shared_ptr Load(AgcDriver::VulkanDevice& device, std::uint6 device.Target(), {0, 0, 0, 128} }; - request.useCache = false; + 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(); - Require(Output[0] == Marker(surface), "surface " + std::to_string(surface) + ": the shader read " + std::to_string(Output[0]) + " instead of its first texel"); 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 @@ -100,7 +150,7 @@ int main() { const auto blockBytes = static_cast(SurfaceBytes + (MaxSurfaces - 1u) * Stride); { GuestAllocations::Mutation mutation; - mutation.Add(texels, blockBytes, true, true); + mutation.Add(texels, blockBytes, true, true, true); } const auto release = [&] { AgcDriver::Graphics::ClearCachedTextures(device->Device()); @@ -108,28 +158,61 @@ int main() { mutation.Remove(texels); }; try { - const auto first = Load(*device, base, 0); + auto first = Store(*device, base, 0); Require(first != nullptr, "surface 0 has no cached storage image"); + Require(AgcDriver::Graphics::SharedResourceCache().Size() != 0, "the storage cache workload did not populate the resource cache"); const auto held = std::max(first->AllocationBytes(), first->GuestBytes()); const auto reused = static_cast(FixedBudget / held) + 1u; std::printf("texture cache budget %llu MiB; each 512x512 32_UINT surface holds %llu MiB; cyclic set of %u surfaces (%llu MiB)\n", static_cast(budget >> 20u), static_cast(held >> 20u), reused, static_cast((reused * held) >> 20u)); Require(budget >= reused * held, "the cyclic set exceeds the test cache budget"); - std::vector> images(reused); + std::vector> images(MaxSurfaces); images[0] = first; - for (std::uint32_t surface = 1; surface < reused; ++surface) images[surface] = Load(*device, base, surface); + first.reset(); + for (std::uint32_t surface = 1; surface < reused; ++surface) images[surface] = Store(*device, base, surface); std::uint32_t remade = 0; for (std::uint32_t surface = 0; surface < reused; ++surface) { - const auto again = Load(*device, base, surface); + const auto again = Store(*device, base, surface); if (again == nullptr || again != images[surface].lock()) ++remade; } Require(remade == 0, std::to_string(remade) + " of " + std::to_string(reused) + " storage images were made again on the second pass over a " + std::to_string((reused * held) >> 20u) + " MiB cyclic set under a " + std::to_string(budget >> 20u) + " MiB budget"); const auto capacity = static_cast(budget / held); Require(capacity + 2u <= MaxSurfaces, "the eviction set exceeds the test storage"); - for (std::uint32_t surface = reused; surface < capacity + 2u; ++surface) Load(*device, base, surface); + for (std::uint32_t surface = reused; surface < capacity + 2u; ++surface) images[surface] = Store(*device, base, surface); for (std::uint32_t surface = 0; surface < capacity + 2u; ++surface) { const bool cached = StorageTexture::FindLive(base + surface * Stride, SurfaceBytes) != nullptr; 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/StorageOneSliceAlias.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StorageOneSliceAlias.cpp new file mode 100644 index 0000000000..78437ac99d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StorageOneSliceAlias.cpp @@ -0,0 +1,207 @@ +#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 + +namespace { + +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 Format32UInt = 20; +constexpr std::uint32_t TileR64KBX = 0x1b; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t Type2DArray = 13; +constexpr std::size_t SurfaceBytes = 65536; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Initial = 0x5a5a5a5au; + +alignas(256) constexpr std::array StoreCode{ + 0x7e020280, 0x7e04020c, 0xf0201108, 0x00010200, 0xbf810000, +}; + +alignas(256) constexpr std::array LoadArrayCode{ + 0x7e020280, 0x7e040280, 0xf0001128, 0x00010300, 0x34080082, 0xbf8c3f70, 0xe0701000, 0x80000304, 0xbf810000, +}; + +alignas(256) std::array Output{}; + +std::uint64_t AddressOf(const void* data) { + return static_cast(reinterpret_cast(data)); +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +class GuestBlock { +public: + explicit GuestBlock(bool watched) { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, PAGE_READWRITE, BlockBytes); + static_cast(watched); +#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, "one-slice alias: cannot allocate the guest block"); + Require(!watched || AgcDriver::GuestMemory::Watched(AddressOf(block), BlockBytes), "one-slice alias: the guest block is not write-watched"); + 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::array TextureDescriptor(const std::uint8_t* texels, std::uint32_t type) { + 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) | (type << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +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(); +} + +void Store(AgcDriver::VulkanDevice& device, const std::uint8_t* texels, std::uint32_t value) { + std::vector userData(16, 0u); + const auto texture = TextureDescriptor(texels, Type2D); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + userData[12] = value; + Dispatch(device, StoreCode, userData); +} + +void LoadArray(AgcDriver::VulkanDevice& device, const std::uint8_t* texels) { + Output.fill(0xdeadbeefu); + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + const auto texture = TextureDescriptor(texels, Type2DArray); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + Dispatch(device, LoadArrayCode, userData); +} + +void RequireOutput(std::uint32_t expected, const std::string& what) { + for (std::uint32_t x = 0; x < Threads; ++x) { + Require(Output[x] == expected, what + ": texel (" + std::to_string(x) + ", 0) reads " + Hex(Output[x]) + ", expected " + Hex(expected)); + } +} + +void Run(AgcDriver::VulkanDevice& device, std::uint8_t* texels, bool watched) { + const auto surface = AddressOf(texels); + std::fill_n(reinterpret_cast(texels), SurfaceBytes / 4u, Initial); + AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Store(device, texels, 0x11111111u); + Require(StorageTexture::FindPending(surface, SurfaceBytes) != nullptr, "the 2D store left no pending image"); + LoadArray(device, texels); + RequireOutput(0x11111111u, "the first load through the one-slice array"); + Store(device, texels, 0x22222222u); + const auto written = StorageTexture::FindPending(surface, SurfaceBytes); + Require(written != nullptr && written->Descriptor().dimension == AgcDriver::Graphics::TextureDimension::k2D, "the second 2D store left no pending 2D image"); + LoadArray(device, texels); + RequireOutput(0x22222222u, "the second load through the one-slice array"); + if (watched && AgcDriver::GuestMemory::Watched(surface, SurfaceBytes)) { + Require(StorageTexture::FindPending(surface, SurfaceBytes) == written, "the array image's refresh wrote the 2D image's results back instead of taking them on the device"); + } else { + std::puts("the imported block is compared, not watched: taking the results on the device is not checked"); + } + StorageTexture::FlushPending(surface, SurfaceBytes, nullptr, "test"); + device.WaitIdle(); + Require(std::count(reinterpret_cast(texels), reinterpret_cast(texels) + SurfaceBytes / 4u, 0x22222222u) == static_cast(Threads), "guest memory does not hold the 32 texels of the second store after the flush"); + AgcDriver::Graphics::ClearCachedTextures(device.Device()); +} + +} + +int main() { + try { + const bool watched = AgcDriver::GuestMemory::WriteWatched(); + GuestBlock block(watched); + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!watched) std::puts("guest memory has no write watch: only the texels read through the array are checked"); + Run(*device, block.Data(), watched); + std::puts("storage one-slice alias 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..acde6de596 --- /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 + bytes + 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/StoreIdleResults.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StoreIdleResults.cpp new file mode 100644 index 0000000000..b2935dd9c9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StoreIdleResults.cpp @@ -0,0 +1,146 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "VulkanTestDevice.hpp" +#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 = 256; +constexpr std::uint32_t Format32UInt = 20; +constexpr std::uint32_t TileR64KBX = 0x1b; +constexpr std::uint32_t Type2D = 9; +constexpr std::size_t BlockBytes = 0x60000; +constexpr std::uint32_t Untouched = 0xdeadbeefu; + +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::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 Commands(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 auto address = reinterpret_cast(StoreCode.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}); + return words; +} + +void Store(const std::uint32_t* texels) { + auto words = Commands(texels); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); +} + +} + +int main(int argc, char** argv) { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + std::vector storage((BlockBytes + 0x10000u) / 4u); + const bool wake = argc == 2 && std::string_view(argv[1]) == "--idle-epoch-wake"; + std::vector nextStorage(wake ? (BlockBytes + 0x10000u) / 4u : 0u); + auto* nextTexels = wake ? reinterpret_cast((reinterpret_cast(nextStorage.data()) + 0xffffu) & ~std::uintptr_t{0xffffu}) : nullptr; + struct ShutdownGuard { + ~ShutdownGuard() { try { AgcDriverShutdown_nid_postfix(); } catch (...) {} } + } shutdown; + auto* texels = reinterpret_cast((reinterpret_cast(storage.data()) + 0xffffu) & ~std::uintptr_t{0xffffu}); + std::fill(texels, texels + BlockBytes / 4u, Untouched); + { + GuestAllocations::Mutation mutation; + mutation.Add(texels, BlockBytes, true, true, true); + if (nextTexels != nullptr) { + std::fill(nextTexels, nextTexels + BlockBytes / 4u, Untouched); + mutation.Add(nextTexels, BlockBytes, true, true, true); + } + } + const auto bytes = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(texels))).guestBytes; + const volatile std::uint32_t* cpu = texels; + const auto stored = [&] { + for (std::size_t index = 0; index < bytes / 4u; ++index) { + if (cpu[index] == 0x12345678u) return true; + } + return false; + }; + + Store(texels); + if (wake) { + const auto pendingDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + while (!AgcDriver::Graphics::StorageTexture::AnyPending() && std::chrono::steady_clock::now() < pendingDeadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + Require(AgcDriver::Graphics::StorageTexture::AnyPending(), "idle wake fixture did not observe pending storage results"); + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + { + std::lock_guard gpuLock(AgcDriver::GuestMemory::GpuMutex()); + AgcDriver::Graphics::StorageTexture::FlushAllPending("idle wake fixture"); + } + std::array nop{0xc0001000u, 0u}; + Packet packet{nop.data(), static_cast(nop.size()), 0, {}}; + AgcDriver::Submit(&packet, 0x30u); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + cpu = nextTexels; + Store(nextTexels); + } + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + while (!stored() && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + Require(stored(), "the queue-idle store did not bring the result to guest memory without a direct flush or further submission"); + + AgcDriverShutdown_nid_postfix(); + { + GuestAllocations::Mutation mutation; + mutation.Remove(texels); + if (nextTexels != nullptr) mutation.Remove(nextTexels); + } + std::puts("store idle 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/StoreOverUntracked.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp new file mode 100644 index 0000000000..919662cc42 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp @@ -0,0 +1,529 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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 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; +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)); +} + +std::array TextureDescriptor(const std::uint32_t* texels, std::uint32_t side = Side) { + const auto address = AddressOf(texels); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((side - 1u) & 3u) << 30u), + ((side - 1u) >> 2u) | ((side - 1u) << 14u), + 0xfacu | (surfaceTile << 20u) | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +void Dispatch(std::span code, std::uint32_t threads, std::uint32_t resource, std::span userData, std::uint32_t groups) { + std::vector words; + const auto registers = [&](const std::uint32_t first, const std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array shape{threads, 1, 1}; + registers(0x207, shape); + const auto address = AddressOf(code.data()); + const std::array program{static_cast(address >> 8u), static_cast(address >> 40u)}; + registers(0x20c, program); + const std::array layout{resource}; + registers(0x213, layout); + registers(0x240, userData); + words.insert(words.end(), {0xc0031500u, groups, 1, 1, 0x41u}); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); +} + +void Store(const std::uint32_t* texels, std::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(code, 8, static_cast(userData.size()) << 1u, userData, 1); +} + +void Fill(const std::uint32_t* texels) { + const auto address = AddressOf(texels); + const std::array userData{ + static_cast(address), + static_cast((address >> 32u) & 0xffffu) | (16u << 16u), + static_cast(SurfaceBytes / 16u), + 0x4bfacu, + Filled, + Filled, + Filled, + Filled, + }; + Dispatch(FillCode, 64, (static_cast(userData.size()) << 1u) | 0x80u, userData, static_cast(SurfaceBytes / 1024u)); +} + +std::array BufferDescriptor(const void* data, std::size_t bytes) { + const auto address = AddressOf(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), static_cast(bytes), 0x31016facu}; +} + +void ReadImage(const std::uint32_t* texels, std::uint32_t* output) { + std::array userData{}; + const auto buffer = BufferDescriptor(output, SurfaceBytes); + const auto texture = TextureDescriptor(texels); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + Dispatch(ReadCode, 32, static_cast(userData.size()) << 1u, userData, 1); +} + +class Surface { +public: + explicit Surface(bool watched = false, std::size_t lead = 0) : watched(watched), regionBytes(lead == 0 ? SurfaceBytes : SurfaceBytes + Block) { + if (watched) { +#ifdef _WIN32 + 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, 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 + 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); + allocation = region != nullptr ? static_cast(region) : static_cast(texels); + GuestAllocations::Mutation().Add(allocation, region != nullptr ? regionBytes : SurfaceBytes, true, true, true); + } + + ~Surface() { + GuestAllocations::Mutation().Remove(allocation); + if (!watched) return; +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(region, regionBytes); + GuestArena::GuestArenaRelease_nid_postfix(region, regionBytes); +#else + munmap(region, regionBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(region, regionBytes); +#endif + } + + Surface(const Surface&) = delete; + Surface& operator=(const Surface&) = delete; + + std::uint32_t* Texels() const { return texels; } + std::uint8_t* Bytes() const { return reinterpret_cast(texels); } + std::uint64_t Address() const { return AddressOf(texels); } + +private: + bool watched; + std::size_t regionBytes; + std::vector storage; + std::uint8_t* region = nullptr; + void* allocation = nullptr; + std::uint32_t* texels = nullptr; +}; + +std::uint8_t Pattern(std::size_t offset) { + return static_cast(0x40u + offset % 0x3bu); +} + +std::size_t LostBytes(const std::uint8_t* bytes) { + std::size_t lost = 0; + for (std::size_t offset = 0; offset < Page; ++offset) { + if (static_cast(bytes)[offset] != Pattern(offset)) ++lost; + } + return lost; +} + +bool Holds(const std::uint32_t* texels, std::uint32_t value) { + const auto* words = static_cast(texels); + for (std::size_t index = 0; index < SurfaceBytes / 4u; ++index) { + if (words[index] == value) return true; + } + return false; +} + +template +bool HoldPending(THold& hold, const Surface& surface, std::uint32_t result) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (true) { + hold.lock(); + 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"); + std::this_thread::yield(); + } +} + +template +void Settle(THold& hold, const Surface& surface, std::uint32_t result) { + if (!HoldPending(hold, surface, result)) return; + StorageTexture::FlushAllPending("store over untracked fixture"); + hold.unlock(); +} + +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; +constexpr std::size_t LastBlock = SurfaceBytes - Block; + +struct Case { + const char* name; + Kind kind; + std::size_t access; +}; + +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 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); + if (test.kind == Kind::Copied) { + Store(source->Texels()); + Settle(hold, *source, Stored); + Fill(source->Texels()); + Settle(hold, *source, Filled); + } + Store(surface.Texels()); + if (test.kind == Kind::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) { + std::vector settled(SurfaceBytes); + AgcDriver::GuestMemory::Read(address, settled); + } + Fill(surface.Texels()); + if (!HoldPending(hold, surface, Filled)) return Outcome::Missed; + } else if (test.kind == Kind::Copied) { + Settle(hold, surface, Stored); + hold.lock(); + const auto from = StorageTexture::FindLive(source->Address(), SurfaceBytes); + const auto to = StorageTexture::FindLive(address, SurfaceBytes); + Require(from != nullptr && to != nullptr, "store over untracked: the surfaces have no storage images to copy between"); + const char* refusal = nullptr; + if (!to->CopyFrom(*from, refusal)) { + Require(refusal != nullptr && std::string_view(refusal) == "not imported", "store over untracked: the image copy was refused"); + std::fprintf(stderr, "[store over untracked] %s: not run, the surface has no host import\n", test.name); + return Outcome::Kept; + } + } else if (test.kind == Kind::NeighbourCopied) { + // The surface's image has results pending in its first block when a copy replaces the + // image of its last block: the copy supersedes the saved bytes there only. + const auto settledLive = [&](std::uint64_t at, std::size_t size) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (true) { + hold.lock(); + StorageTexture::FlushAllPending("store over untracked fixture"); + const bool live = StorageTexture::FindLive(at, size) != nullptr; + hold.unlock(); + if (live) return; + Require(std::chrono::steady_clock::now() < deadline, "store over untracked: the block's storage image was never created"); + std::this_thread::yield(); + } + }; + Store(surface.Texels() + LastBlock / 4u, BlockSide); + settledLive(address + LastBlock, Block); + Store(source->Texels(), BlockSide); + settledLive(source->Address(), Block); + std::fill(surface.Texels(), surface.Texels() + SurfaceBytes / 4u, Untouched); + Store(surface.Texels()); + // Pending results are the surface image's alone once it exists. + const 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 && StorageTexture::AnyPendingOverlaps(range)) break; + hold.unlock(); + 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); + const auto to = StorageTexture::FindLive(address + LastBlock, Block); + Require(from != nullptr && to != nullptr, "store over untracked: the blocks have no storage images to copy between"); + const char* refusal = nullptr; + if (!to->CopyFrom(*from, refusal)) { + Require(refusal != nullptr && std::string_view(refusal) == "not imported", "store over untracked: the image copy was refused"); + StorageTexture::FlushAllPending("store over untracked fixture"); + std::fprintf(stderr, "[store over untracked] %s: not run, the block has no host import\n", test.name); + return Outcome::Kept; + } + } else if (!HoldPending(hold, surface, Stored)) { + return Outcome::Missed; + } + const auto writePattern = [&] { + for (std::size_t offset = 0; offset < Page; ++offset) static_cast(bytes)[offset] = Pattern(offset); + }; + if (test.kind == Kind::WrittenThenLeftWatch) writePattern(); + if (leaves) { + if (!AgcDriver::GuestMemory::Watched(address, SurfaceBytes)) { + StorageTexture::FlushAllPending("store over untracked fixture"); + std::fprintf(stderr, "[store over untracked] %s: not run, the surface is not write-watched\n", test.name); + return Outcome::Kept; + } + const auto image = StorageTexture::FindLive(address, SurfaceBytes); + 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); + 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); + if (test.kind != Kind::WrittenThenLeftWatch) writePattern(); + AgcDriver::GuestMemory::Read(address + test.access, read); + const auto lostAtAccess = LostBytes(bytes); + StorageTexture::FlushAllPending("store over untracked fixture"); + std::vector whole(SurfaceBytes); + AgcDriver::GuestMemory::Read(address, whole); + const auto lostAtEnd = LostBytes(bytes); + if (test.kind == Kind::Filled || test.kind == Kind::Copied || leaves) Require(static_cast(surface.Texels())[2 * Block / 4u] == Filled, "store over untracked: the result did not reach a block the CPU left alone"); + if (lostAtAccess == 0 && lostAtEnd == 0) return Outcome::Kept; + std::fprintf(stderr, "[store over untracked] %s: of 0x%zx bytes the CPU wrote while the result was pending, %zu were lost after the access at +0x%zx and %zu after every result was stored\n", test.name, Page, lostAtAccess, test.access, lostAtEnd); + return Outcome::Lost; +} + +} + +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; + } + 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{{ + {"another block read after the CPU write", Kind::ReadAfter, Block + Page}, + {"another block read before the CPU write", Kind::ReadBefore, 0xe000}, + {"a fill of the surface", Kind::Filled, 0xe000}, + {"an image copy into the surface", Kind::Copied, 0xe000}, + {"a surface that left the write watch", Kind::LeftWatch, 0xe000}, + {"a surface written before it left the write watch", Kind::WrittenThenLeftWatch, 0xe000}, + {"a surface whose import left the write watch", Kind::ImportLeftWatch, 0xe000}, + {"an image copy into another block of the surface", Kind::NeighbourCopied, 0xe000}, + {"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) { + auto outcome = Outcome::Missed; + for (std::uint32_t attempt = 0; attempt < 256u && outcome == Outcome::Missed; ++attempt) outcome = Run(test); + Require(outcome != Outcome::Missed, "store over untracked: the result was never held pending"); + kept = outcome == Outcome::Kept && kept; + } + { + 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"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} 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 new file mode 100644 index 0000000000..bb378bac87 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp @@ -0,0 +1,201 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "../SubvectorLoopFixture.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; +using SubvectorLoopFixture::ExecKeptCode; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Columns = 9; +constexpr std::uint32_t Ones = 0xffffffffu; +constexpr std::uint32_t Marker = 0x5a5a5a5au; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array ExecResetCode{ + 0x34020082, 0xe0301000, 0x80000401, 0xbf8c3f70, 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280, + 0x7e1c0280, 0xbe940380, 0xbea603ff, 0x5a5a5a5a, 0x7daa0880, 0xbda6000b, 0x80148114, 0x4a141481, + 0x7e160214, 0x7e180226, 0xbe96047e, 0x7e1a0216, 0x7e1c0217, 0xbf098414, 0xbf850002, 0xbefe04c1, + 0xbe26fff5, 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, +}; + +constexpr std::array Names{"passes", "last pass", "s38 in the pass", "exec lo in the pass", "exec hi in the pass", "s38 after the loop", "exec lo after the loop", "exec hi after the loop", "total passes"}; + +struct Group { + std::uint32_t first; + std::uint32_t last; + std::array values; +}; + +struct Case { + const char* name; + std::uint64_t mask; + std::vector groups; +}; + +const std::vector ExecKeptCases{ + {"all lanes", ~0ull, { + {0, 31, {1, 1, Ones, Ones, 0, Ones, Ones, Ones, 2}}, + {32, 63, {1, 2, Ones, 0, Ones, Ones, Ones, Ones, 2}}, + }}, + {"low half", 0xffffffffull, { + {0, 31, {1, 1, 0, Ones, 0, 0, Ones, 0, 1}}, + {32, 63, {0, 0, 0, 0, 0, 0, Ones, 0, 1}}, + }}, + {"high half", 0xffffffffull << 32, { + {0, 31, {0, 0, 0, 0, 0, 0, 0, Ones, 1}}, + {32, 63, {1, 1, 0, 0, Ones, 0, 0, Ones, 1}}, + }}, + {"no lane", 0ull, { + {0, 63, {0, 0, 0, 0, 0, Marker, 0, 0, 0}}, + }}, + {"lanes 3 and 40", (1ull << 3) | (1ull << 40), { + {0, 63, {0, 0, 0, 0, 0, 8, 8, 0x100u, 2}}, + {3, 3, {1, 1, 0x100u, 8, 0, 8, 8, 0x100u, 2}}, + {40, 40, {1, 2, 8, 0, 0x100u, 8, 8, 0x100u, 2}}, + }}, + {"lane 3", 1ull << 3, { + {0, 63, {0, 0, 0, 0, 0, 0, 8, 0, 1}}, + {3, 3, {1, 1, 0, 8, 0, 0, 8, 0, 1}}, + }}, + {"lane 40", 1ull << 40, { + {0, 63, {0, 0, 0, 0, 0, 0, 0, 0x100u, 1}}, + {40, 40, {1, 1, 0, 0, 0x100u, 0, 0, 0x100u, 1}}, + }}, +}; + +const std::vector ExecResetCases{ + {"all lanes", ~0ull, { + {0, 63, {0, 0, 0, 0, 0, Ones, Ones, Ones, 1}}, + {0, 31, {1, 1, Ones, Ones, 0, Ones, Ones, Ones, 1}}, + }}, + {"low half", 0xffffffffull, { + {0, 63, {0, 0, 0, 0, 0, 0, 0, Ones, 1}}, + {0, 31, {1, 1, 0, Ones, 0, 0, 0, Ones, 1}}, + }}, + {"high half", 0xffffffffull << 32, { + {0, 63, {0, 0, 0, 0, 0, 0, 0, Ones, 1}}, + {32, 63, {1, 1, 0, 0, Ones, 0, 0, Ones, 1}}, + }}, + {"no lane", 0ull, { + {0, 63, {0, 0, 0, 0, 0, Marker, 0, 0, 0}}, + }}, + {"lanes 3 and 40", (1ull << 3) | (1ull << 40), { + {0, 63, {0, 0, 0, 0, 0, 0x100u, 0x100u, Ones, 1}}, + {3, 3, {1, 1, 0x100u, 8, 0, 0x100u, 0x100u, Ones, 1}}, + }}, + {"lane 3", 1ull << 3, { + {0, 63, {0, 0, 0, 0, 0, 0, 0, Ones, 1}}, + {3, 3, {1, 1, 0, 8, 0, 0, 0, Ones, 1}}, + }}, + {"lane 40", 1ull << 40, { + {0, 63, {0, 0, 0, 0, 0, 0, 0, Ones, 1}}, + {40, 40, {1, 1, 0, 0, 0x100u, 0, 0, Ones, 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}; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, std::uint64_t mask) { + for (std::uint32_t lane = 0; lane < Threads; ++lane) Input[lane] = ((mask >> lane) & 1u) != 0u ? 1u : 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}, 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); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const char* variant, const Case& testCase) { + std::array, Threads> expected{}; + for (const auto& group : testCase.groups) { + for (std::uint32_t lane = group.first; lane <= group.last; ++lane) expected[lane] = group.values; + } + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + for (std::uint32_t column = 0; column < Columns; ++column) { + const auto actual = Output[column * Threads + lane]; + Require(actual == expected[lane][column], std::string("subvector loop ") + variant + ", initial exec " + testCase.name + ": lane " + std::to_string(lane) + " " + Names[column] + " is " + std::to_string(actual) + ", expected " + std::to_string(expected[lane][column])); + } + } +} + +void CheckWave32Throws(AgcDriver::VulkanDevice& device, std::span code) { + std::vector userData(8, 0u); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{32, 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.ComputeTarget(32), + {0, 0, 0, 128} + }; + request.useCache = false; + try { + static_cast(ShaderRecompiler::Recompile(request)); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find("s_subvector_loop_begin in a wave32 shader is not measured") != std::string::npos, std::string("wave32 subvector loop threw an unexpected error: ") + error.what()); + return; + } + Require(false, "a wave32 subvector loop was recompiled"); +} + +void RunCases(AgcDriver::VulkanDevice& device, const char* variant, std::span code, const std::vector& cases) { + for (const auto& testCase : cases) { + Run(device, code, testCase.mask); + Check(variant, testCase); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + CheckWave32Throws(*device, ExecKeptCode); + if (device->Target().subgroupSize < 32u) { + std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize); + return VulkanTestSkipped; + } + RunCases(*device, "exec kept by the body", ExecKeptCode, ExecKeptCases); + RunCases(*device, "body resets exec", ExecResetCode, ExecResetCases); + std::puts("subvector loop 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/SwapAltBlend.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SwapAltBlend.cpp new file mode 100644 index 0000000000..393d98a1f1 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SwapAltBlend.cpp @@ -0,0 +1,322 @@ +#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 +#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 SwapAlt8x8Info = (3u << 2u) | (1u << 11u) | 0x8000u; +constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; +constexpr std::uint32_t Float32Export = 9; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array StoringVertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xe0700000, 0x80010000, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) std::array Stored{}; + +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> Destinations{{ + {64, 128}, {255, 0}, {32, 224}, {128, 64} +}}; + +alignas(256) std::array, 8> Programs{}; +std::size_t programCount = 0; + +std::span Program(const Color& exported) { + 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(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::Graphics::ColorTarget DecodeTarget(const Block& block) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = SwapAlt8x8Info; + cx[0x31d] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = TiledAttrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == VK_FORMAT_R8G8_UNORM && color.componentMapping == 0xecu && color.elementBytes == 2 && color.bytes == block.bytes, "the 8_8 SWAP_ALT target did not decode as a 256x128 R8G8 surface holding the x and w exports"); + return color; +} + +VkPipelineColorBlendAttachmentState Blend(VkBlendFactor source, VkBlendFactor destination, VkBlendOp operation, VkBlendFactor sourceAlpha, VkBlendFactor destinationAlpha, VkBlendOp alphaOperation) { + VkPipelineColorBlendAttachmentState blend{}; + blend.blendEnable = VK_TRUE; + blend.srcColorBlendFactor = source; + blend.dstColorBlendFactor = destination; + blend.colorBlendOp = operation; + blend.srcAlphaBlendFactor = sourceAlpha; + blend.dstAlphaBlendFactor = destinationAlpha; + blend.alphaBlendOp = alphaOperation; + blend.colorWriteMask = 0xf; + return blend; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, const VkPipelineColorBlendAttachmentState& blend, std::span pixelCode, std::span vertexCode = VertexCode) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(8, 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 auto stored = reinterpret_cast(Stored.data()); + const std::array storeBuffer{static_cast(stored), static_cast((stored >> 32u) & 0xffffu), static_cast(sizeof(Stored)), 0x01016facu}; + std::copy(storeBuffer.begin(), storeBuffer.end(), vertexUserData.begin() + 4); + 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()); + + 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; + 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, AgcDriver::Graphics::ExportMappings(state)), std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +std::vector Fill(Block& block, const ColorTargetLayout& layout) { + 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& color = Destinations[(x + y) % Destinations.size()]; + const auto index = layout.Offset(x, y); + bytes[index] = color[0]; + bytes[index + 1] = color[1]; + } + } + std::memcpy(block.data, bytes.data(), block.bytes); + return bytes; +} + +void Check(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const std::vector& before, const std::function(float first, float second)>& blended, 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 index = layout.Offset(x, y); + const auto& destination = Destinations[(x + y) % Destinations.size()]; + const auto expected = blended(static_cast(destination[0]) / 255.0f, static_cast(destination[1]) / 255.0f); + for (std::uint32_t channel = 0; channel < 2; ++channel) { + const bool inside = x < ScissorWidth; + const auto want = std::clamp(expected[channel], 0.0f, 1.0f) * 255.0f; + const bool match = inside ? std::fabs(static_cast(stored[index + channel]) - want) <= 1.0f : stored[index + channel] == before[index + channel]; + if (match) continue; + char detail[160]; + std::snprintf(detail, sizeof(detail), ": channel %u of pixel (%u, %u) %s the scissor holds %u, expected %.2f", channel, x, y, inside ? "inside" : "outside", stored[index + channel], inside ? want : static_cast(before[index + channel])); + throw std::runtime_error(what + detail); + } + } + } +} + +struct BlendCase { + const char* name; + VkPipelineColorBlendAttachmentState blend; + bool split; + std::function(float first, float second)> blended; +}; + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const ColorTargetLayout layout(Width, Height, ColorTileMode::RenderTarget, 2); + Block block(layout.Bytes()); + const Color exported{0.25f, 0.75f, 0.125f, 0.5f}; + const float x = exported[0]; + const float w = exported[3]; + const std::array cases{{ + {"SRC_ALPHA + ONE_MINUS_SRC_ALPHA, alpha ONE + ZERO", Blend(VK_BLEND_FACTOR_SRC_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, VK_BLEND_OP_ADD, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ZERO, VK_BLEND_OP_ADD), true, + [&](float r, float) { return std::array{x * w + r * (1.0f - w), w}; }}, + {"ONE + ONE, alpha ZERO + ONE_MINUS_SRC_ALPHA", Blend(VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, VK_BLEND_FACTOR_ZERO, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, VK_BLEND_OP_ADD), true, + [&](float r, float g) { return std::array{x + r, g * (1.0f - w)}; }}, + {"ONE + ZERO, alpha ONE reverse-subtract ONE", Blend(VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ZERO, VK_BLEND_OP_ADD, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_REVERSE_SUBTRACT), true, + [&](float, float g) { return std::array{x, g - w}; }}, + {"ONE + ONE_MINUS_SRC_ALPHA for color and alpha", Blend(VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, VK_BLEND_OP_ADD, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, VK_BLEND_OP_ADD), false, + [&](float r, float g) { return std::array{x + r * (1.0f - w), w + g * (1.0f - w)}; }}, + }}; + { + Fill(block, layout); + const auto color = DecodeTarget(block); + bool rejected = false; + try { + Draw(*device, color, cases[0].blend, Program(exported), StoringVertexCode); + } catch (const std::exception& error) { + rejected = std::string_view(error.what()).find("a draw whose shaders write memory") != std::string_view::npos; + if (!rejected) throw; + } + Require(rejected, "a split 8_8 SWAP_ALT draw whose vertex shader stores to memory was not rejected"); + } + for (const auto& test : cases) { + const auto before = Fill(block, layout); + const auto color = DecodeTarget(block); + AgcDriver::Graphics::State probe{}; + probe.colors = {color}; + probe.blends = {test.blend}; + Require(AgcDriver::Graphics::SplitsAlternateAlpha(probe) == test.split, (std::string(test.name) + ": the split decision is wrong").c_str()); + Draw(*device, color, test.blend, Program(exported)); + Check(*device, block, layout, before, test.blended, test.name); + } + std::puts("8_8 SWAP_ALT 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/Tessellation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp new file mode 100644 index 0000000000..87c935f0d2 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp @@ -0,0 +1,1027 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "SceShaders.hpp" +#include "VulkanTestDevice.hpp" +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 256; +constexpr std::uint32_t Height = 80; +constexpr std::array Background{16, 24, 40, 255}; +constexpr std::size_t GuestBytes = 65536; +alignas(256) std::array Pixels{}; +std::array HullUserData{0x1000u}; + +alignas(256) constexpr std::array LocalCode{ + 0xd5480004, 0x02090502, 0xd5480005, 0x02090102, 0xd5480006, 0x02110902, 0xd5480007, 0x02111102, + 0x7e080d04, 0x7e0a0d05, 0x7e0c0d06, 0x7e0e0d07, 0x10100cff, 0x3f19999a, 0x061010ff, 0xbf666666, + 0x3e1008ff, 0x3e800000, 0xd54b0009, 0x03c5e105, 0x161406a0, 0xd9340000, 0x0000080a, 0xd8340008, + 0x0000070a, 0xd8340010, 0x0000070a, 0xbf8cc07f, 0xbefd2106, +}; + +alignas(256) constexpr std::array HullCode{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, + 0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, + 0x02000015, 0x162c2890, 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, + 0x7da82881, 0xbe94047e, 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, 0xd8dc1008, + 0x07000013, 0xbf8cc07f, 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, + 0xe0781000, 0x0403180a, 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, + 0xbf810000, +}; + +alignas(256) constexpr std::array HullCompareBranch{ + 0xd4040016, 0x00020080, 0xbeea0416, 0xbf870001, 0xbf800000, 0xd7650000, 0x000100c1, 0xd7660000, + 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, 0x02151101, 0x362402ff, 0x000000ff, + 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, 0x02000015, 0x162c2890, 0xd5430016, + 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, 0x7da82881, 0xbe94047e, 0xb06a0060, + 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, 0xd8dc1008, 0x07000013, 0xbf8cc07f, 0x7e300306, + 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, 0xe0781000, 0x0403180a, 0x4a162481, + 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, 0xbf810000, +}; + +alignas(256) constexpr std::array HullSingleInput{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, + 0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x022224ff, 0x00000020, 0xdbfc0000, + 0x02000015, 0x162c2890, 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, + 0x7da82881, 0xbe94047e, 0xb06a0020, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, 0xd8dc0201, + 0x07000013, 0xbf8cc07f, 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, + 0xe0781000, 0x0403180a, 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, + 0xbf810000, +}; + +alignas(256) constexpr std::array HullSingleOutput{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, + 0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x022224ff, 0x00000020, 0xdbfc0000, + 0x02000015, 0x7e2c0280, 0xd5430016, 0x045a24ff, 0x00000010, 0xbf8cc07f, 0xe0781000, 0x02020216, + 0xbf800000, 0xbe94047e, 0xb06a0020, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, 0xd8dc0201, + 0x07000013, 0xbf8cc07f, 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, + 0xe0781000, 0x0403180a, 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, + 0xbf810000, +}; + +alignas(256) constexpr std::array HullRestoredExecSaved{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, + 0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, + 0x02000015, 0x162c2890, 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, + 0x7da82881, 0xbe94047e, 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, 0xd8dc1008, + 0x07000013, 0xbf8cc07f, 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, + 0xbe94047e, 0xbefe0414, 0xe0781000, 0x0403180a, 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, + 0xe0741000, 0x0202070b, 0xbf810000, +}; + +alignas(256) constexpr std::array LocalCodeStride24{ + 0xd5480004, 0x02090502, 0xd5480005, 0x02090102, 0xd5480006, 0x02110902, 0xd5480007, 0x02111102, + 0x7e080d04, 0x7e0a0d05, 0x7e0c0d06, 0x7e0e0d07, 0x10100cff, 0x3f19999a, 0x061010ff, 0xbf666666, + 0x3e1008ff, 0x3e800000, 0xd54b0009, 0x03c5e105, 0x16140698, 0xd9340000, 0x0000080a, 0xd8340008, + 0x0000070a, 0xd8340010, 0x0000070a, 0xbf8cc07f, 0xbefd2106, +}; + +alignas(256) constexpr std::array HullNextInput{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, + 0x02151101, 0x362402ff, 0x000000ff, 0x16002898, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0018, + 0x02000015, 0x162c2890, 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, + 0x7da82881, 0xbe94047e, 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, 0xd8dc1008, + 0x07000013, 0xbf8cc07f, 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, + 0xbe94047e, 0xbefe0414, 0xe0781000, 0x0403180a, 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, + 0xe0741000, 0x0202070b, 0xbf810000, +}; + +alignas(256) constexpr std::array DomainCode{ + 0xf8000941, 0x00000000, 0x34020e84, 0x16020283, 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01, + 0xe0341020, 0x04030e01, 0xbf8c3f70, 0x08040af2, 0x08040d02, 0x10201505, 0x3e201906, 0x3e201d02, + 0x10221705, 0x3e221b06, 0x3e221f02, 0xd5510012, 0x040a0d05, 0x102424ff, 0x40400000, 0x7e260280, + 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, + 0x08040d02, 0x10201505, 0x3e201906, 0x3e201d02, 0x10221705, 0x3e221b06, 0x3e221f02, 0xd5510012, + 0x040a0d05, 0x102424ff, 0x40400000, 0x7e260280, 0x7e2802f2, 0xf80008cf, 0x14131110, 0xf800020f, + 0x14151214, 0xbf810000, +}; + +alignas(256) constexpr std::array DomainCompareBranch{ + 0xf8000941, 0x00000000, 0xd4040014, 0x00020a80, 0xbeea0414, 0xbf870001, 0xbf800000, 0x34020e84, + 0x16020283, 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01, 0xe0341020, 0x04030e01, 0xbf8c3f70, + 0x08040af2, 0x08040d02, 0x10201505, 0x3e201906, 0x3e201d02, 0x10221705, 0x3e221b06, 0x3e221f02, + 0xd5510012, 0x040a0d05, 0x102424ff, 0x40400000, 0x7e260280, 0x7e2802f2, 0xf80008cf, 0x14131110, + 0xf800020f, 0x14131214, 0xbf810000, +}; + +alignas(256) constexpr std::array LocalIndexAsData{ + 0xd5480004, 0x02090502, 0xd5480005, 0x02090102, 0xd5480006, 0x02110902, 0xd5480007, 0x02111102, + 0x7e080d04, 0x7e0a0d05, 0x7e0c0d06, 0x7e0e0d07, 0x10100cff, 0x3f19999a, 0x061010ff, 0xbf666666, + 0x3e1008ff, 0x3e800000, 0xd54b0009, 0x03c5e105, 0x161406a0, 0xd9340000, 0x0000080a, 0xd8340008, + 0x0000030a, 0xd8340010, 0x0000070a, 0xbf8cc07f, 0xbefd2106, +}; + +alignas(256) constexpr std::array HullDescendingRecords{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, + 0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, + 0x02000015, 0x162c2890, 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, + 0x7da82881, 0xbe94047e, 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, 0xd8dc1008, + 0x07000013, 0xbf8cc07f, 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, + 0xe0781000, 0x0403180a, 0x4a162481, 0x161616ff, 0x000000d0, 0x4c1616ff, 0x00008000, 0xe0741000, + 0x0202070b, 0xbf810000, +}; + +alignas(256) constexpr std::array HullPatchConstant{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, + 0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, + 0x02000015, 0x162c2890, 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, + 0x7da82881, 0xbe94047e, 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, 0xd8dc1008, + 0x07000013, 0xbf8cc07f, 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, + 0xe0781000, 0x0403180a, 0x4a162481, 0x161616ff, 0x000000d0, 0x4c1616ff, 0x00008000, 0xe0741000, + 0x0202070b, 0xe0701064, 0x0202060b, 0xbf810000, +}; + +alignas(256) constexpr std::array HullRelativeData{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, + 0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, + 0x02000015, 0x162c2890, 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, + 0x7da82881, 0xbe94047e, 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, 0xd8dc1008, + 0x07000013, 0xbf8cc07f, 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, + 0xe0781000, 0x0403180a, 0x4a162481, 0x161616ff, 0x000000d0, 0x4c1616ff, 0x00008000, 0xe0741000, + 0x0202070b, 0x4a182481, 0x7e180d0c, 0xe0701064, 0x02020c0b, 0xbf810000, +}; + +alignas(256) constexpr std::array HullUnguardedFactors{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, + 0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, + 0x02000015, 0x162c2890, 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, + 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, 0xd8dc1008, 0x07000013, 0xbf8cc07f, + 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xe0781000, 0x0403180a, 0x4a162481, + 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, 0xbf810000, +}; + +alignas(256) constexpr std::array HullDynamicIndex{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, + 0x02151101, 0x362402ff, 0x000000ff, 0x26002882, 0x34000085, 0xd5430015, 0x040224ff, 0x00000060, + 0xdbfc0000, 0x02000015, 0x162c2890, 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, + 0x02020216, 0x7da82881, 0xbe94047e, 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, + 0xd8dc1008, 0x07000013, 0xbf8cc07f, 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, + 0xbefe0414, 0xe0781000, 0x0403180a, 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, + 0x0202070b, 0xbf810000, +}; + +alignas(256) constexpr std::array HullHalfSlotIndex{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, + 0x02151101, 0x362402ff, 0x000000ff, 0x26002882, 0x34000084, 0xd5430015, 0x040224ff, 0x00000060, + 0xdbfc0000, 0x02000015, 0x162c2890, 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, + 0x02020216, 0x7da82881, 0xbe94047e, 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, + 0xd8dc1008, 0x07000013, 0xbf8cc07f, 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, + 0xbefe0414, 0xe0781000, 0x0403180a, 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, + 0x0202070b, 0xbf810000, +}; + +alignas(256) constexpr std::array HullSquaredAddress{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, + 0x02151101, 0x362402ff, 0x000000ff, 0x16002914, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, + 0x02000015, 0x162c2890, 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, + 0x7da82881, 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, 0xd8dc1008, 0x07000013, + 0xbf8cc07f, 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xe0781000, 0x0403180a, + 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, 0xbf810000, +}; + +alignas(256) constexpr std::array HullAndSaveexec{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, + 0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, + 0x02000015, 0x162c2890, 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0x7d8a2887, 0xbe94246a, + 0xe0781000, 0x02020216, 0x7da82881, 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, + 0xd8dc1008, 0x07000013, 0xbf8cc07f, 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, + 0xe0781000, 0x0403180a, 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, + 0xbf810000, +}; + +alignas(256) constexpr std::array HullSaveexecLoop{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, + 0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, + 0x02000015, 0x162c2890, 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xbe980382, 0xbe9a0480, + 0x7d882881, 0xbe96246a, 0x889a7e1a, 0xbefe0416, 0x80988118, 0xbf078018, 0xbf85fff9, 0xe0781000, + 0x02020216, 0x7d882881, 0xbe94246a, 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, + 0xd8dc1008, 0x07000013, 0xbf8cc07f, 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, + 0xe0781000, 0x0403180a, 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, + 0xbf810000, +}; + +alignas(256) constexpr std::array HullLaneCount{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, + 0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, + 0x02000015, 0x162c2890, 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, + 0x7da82881, 0xbe98107e, 0xbe94047e, 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, + 0xd8dc1008, 0x07000013, 0xbf8cc07f, 0x7e300218, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, + 0xbefe0414, 0xe0781000, 0x0403180a, 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, + 0x0202070b, 0xbf810000, +}; + +alignas(256) constexpr std::array HullBdaScale{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, + 0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, + 0x02000015, 0x162c2890, 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, + 0x7da82881, 0xbe94047e, 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, 0xd8dc1008, + 0x07000013, 0xbf8cc07f, 0x7e180280, 0xdc308000, 0x0d06000c, 0xbf8c3f70, 0x100c1b06, 0x7e300306, + 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, 0xe0781000, 0x0403180a, 0x4a162481, + 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, 0xbf810000, +}; + +alignas(256) constexpr std::array HullBdaScaleWithoutBarrier{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0x7da206f9, 0x81060000, 0xd5480014, 0x02151101, + 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, 0x02000015, + 0x162c2890, 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, 0x7da82881, + 0xbe94047e, 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, 0xd8dc1008, 0x07000013, + 0xbf8cc07f, 0x7e180280, 0xdc308000, 0x0d06000c, 0xbf8c3f70, 0x100c1b06, 0x7e300306, 0x7e320306, + 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, 0xe0781000, 0x0403180a, 0x4a162481, 0x16161690, + 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, 0xbf810000, +}; + +alignas(256) constexpr std::array DomainPatchRead{ + 0xf8000941, 0x00000000, 0x34020e84, 0x16020283, 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01, + 0xe0341020, 0x04030e01, 0x4a060e81, 0x160606ff, 0x000000d0, 0x4c0606ff, 0x00008000, 0xe0301064, + 0x04031503, 0xbf8c3f70, 0x08040af2, 0x08040d02, 0x10201505, 0x3e201906, 0x3e201d02, 0x10221705, + 0x3e221b06, 0x3e221f02, 0xd5510012, 0x040a0d05, 0x102424ff, 0x40400000, 0x102a2aff, 0x3e000000, + 0x7e260280, 0x7e2802f2, 0xf80008cf, 0x14131110, 0xf800020f, 0x14151214, 0xbf810000, +}; + +alignas(256) constexpr std::array DomainNeighbourRecord{ + 0xf8000941, 0x00000000, 0x34020e84, 0x16020283, 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01, + 0xe0341020, 0x04030e01, 0x4a060e81, 0x160606ff, 0x000000d0, 0x4c0606ff, 0x00008000, 0xe0301064, + 0x04031503, 0xe0301134, 0x04031603, 0xbf8c3f70, 0x062a2b16, 0x08040af2, 0x08040d02, 0x10201505, + 0x3e201906, 0x3e201d02, 0x10221705, 0x3e221b06, 0x3e221f02, 0xd5510012, 0x040a0d05, 0x102424ff, + 0x40400000, 0x102a2aff, 0x3e000000, 0x7e260280, 0x7e2802f2, 0xf80008cf, 0x14131110, 0xf800020f, + 0x14151214, 0xbf810000, +}; + +alignas(256) constexpr std::array DomainFixedPatchRead{ + 0xf8000941, 0x00000000, 0x34020e84, 0x16020283, 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01, + 0xe0341020, 0x04030e01, 0x4a060e81, 0x160606ff, 0x000000d0, 0x4c0606ff, 0x00008000, 0xe0301064, + 0x04031503, 0xe0300064, 0x04031600, 0xbf8c3f70, 0x062a2b16, 0x08040af2, 0x08040d02, 0x10201505, + 0x3e201906, 0x3e201d02, 0x10221705, 0x3e221b06, 0x3e221f02, 0xd5510012, 0x040a0d05, 0x102424ff, + 0x40400000, 0x102a2aff, 0x3e000000, 0x7e260280, 0x7e2802f2, 0xf80008cf, 0x14131110, 0xf800020f, + 0x14151214, 0xbf810000, +}; + +alignas(256) constexpr std::array DomainNeighbourPoint{ + 0xf8000941, 0x00000000, 0x34020e84, 0x16020283, 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01, + 0xe0341020, 0x04030e01, 0x4a060e81, 0x160606ff, 0x000000d0, 0x4c0606ff, 0x00008000, 0xe0301064, + 0x04031503, 0xe0301030, 0x04031601, 0xbf8c3f70, 0x062a2b16, 0x08040af2, 0x08040d02, 0x10201505, + 0x3e201906, 0x3e201d02, 0x10221705, 0x3e221b06, 0x3e221f02, 0xd5510012, 0x040a0d05, 0x102424ff, + 0x40400000, 0x102a2aff, 0x3e000000, 0x7e260280, 0x7e2802f2, 0xf80008cf, 0x14131110, 0xf800020f, + 0x14151214, 0xbf810000, +}; + +alignas(256) constexpr std::array DomainCulledPrimitive{ + 0x380000ff, 0x80000000, 0xf8000941, 0x00000000, 0x34020e84, 0x16020283, 0xe0341000, 0x04030a01, + 0xe0341010, 0x04030c01, 0xe0341020, 0x04030e01, 0xbf8c3f70, 0x08040af2, 0x08040d02, 0x10201505, + 0x3e201906, 0x3e201d02, 0x10221705, 0x3e221b06, 0x3e221f02, 0xd5510012, 0x040a0d05, 0x102424ff, + 0x40400000, 0x7e260280, 0x7e2802f2, 0xf80008cf, 0x14131110, 0xf800020f, 0x14131214, 0xbf810000, +}; + +alignas(256) constexpr std::array QuadHullCode{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, + 0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000080, 0xdbfc0000, + 0x02000015, 0x162c2890, 0xd5430016, 0x045a24ff, 0x00000040, 0xbf8cc07f, 0xe0781000, 0x02020216, + 0x7da82881, 0xb06a0080, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, 0xbf8cc07f, 0x7e300306, + 0x7e320306, 0x7e340306, 0x7e360306, 0x16142498, 0xe0781000, 0x0403180a, 0xe0741010, 0x0403180a, + 0xbf810000, +}; + +alignas(256) constexpr std::array QuadDomainCode{ + 0xf8000941, 0x00000000, 0x34020e86, 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01, 0xe0341020, + 0x04030e01, 0xe0341030, 0x04031001, 0xbf8c3f70, 0x08040af2, 0x08060cf2, 0x10241502, 0x3e241905, + 0x10261702, 0x3e261b05, 0x10281d05, 0x3e282102, 0x102a1f05, 0x3e2a2302, 0x102c2503, 0x3e2c2906, + 0x102e2703, 0x3e2e2b06, 0xd5510018, 0x040a0d05, 0xd5510018, 0x04620718, 0x103030f4, 0x7e320280, + 0x7e3402f2, 0xf80008cf, 0x1a191716, 0xf800020f, 0x1a19181a, 0xbf810000, +}; + +alignas(256) constexpr std::array 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, +}; + +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) { + return static_cast((PatchLevels[patch] << 8u) | (patch << 4u) | (x << 2u) | y); +} + +alignas(256) constexpr std::array Indices{ + Corner(0, 0, 0), Corner(0, 2, 0), Corner(0, 1, 2), + Corner(1, 0, 0), Corner(1, 2, 0), Corner(1, 1, 2), + Corner(2, 0, 0), Corner(2, 2, 0), Corner(2, 1, 2), +}; + +alignas(256) constexpr std::array LatePatchIndices{ + Corner(1, 0, 0), Corner(1, 2, 0), Corner(1, 1, 2), + Corner(1, 0, 0), Corner(1, 2, 0), Corner(1, 1, 2), + Corner(1, 0, 0), Corner(1, 2, 0), Corner(1, 1, 2), + Corner(1, 0, 0), Corner(1, 2, 0), Corner(1, 1, 2), + Corner(1, 0, 0), Corner(1, 2, 0), Corner(1, 1, 2), + Corner(1, 0, 0), Corner(1, 2, 0), Corner(1, 1, 2), + Corner(2, 0, 0), Corner(2, 2, 0), Corner(2, 1, 2), + Corner(0, 0, 0), Corner(0, 2, 0), Corner(0, 1, 2), +}; + +alignas(256) constexpr std::array QuadIndices{ + Corner(0, 0, 0), Corner(0, 2, 0), Corner(0, 2, 2), Corner(0, 0, 2), + Corner(1, 0, 0), Corner(1, 2, 0), Corner(1, 2, 2), Corner(1, 0, 2), + Corner(2, 0, 0), Corner(2, 2, 0), Corner(2, 2, 2), Corner(2, 0, 2), +}; + +struct PatchDraw { + TessellationConfiguration configuration; + std::span indices; + float minLevel = 1.0f; + float maxLevel = 64.0f; + std::uint32_t instances = 1; + std::uint32_t firstInstance = 0; +}; + +alignas(256) constexpr std::array NextInputIndices{ + Corner(0, 0, 0), Corner(0, 0, 0), Corner(0, 2, 0), Corner(0, 1, 2), + Corner(1, 0, 0), Corner(1, 0, 0), Corner(1, 2, 0), Corner(1, 1, 2), + Corner(2, 0, 0), Corner(2, 0, 0), Corner(2, 2, 0), Corner(2, 1, 2), +}; + +const PatchDraw Triangles{Patches, Indices}; +const PatchDraw NextInputTriangles{{4u, 3u, 1u, 2u, 2u}, NextInputIndices}; +const PatchDraw MergedTriangles{MergedPatches, Indices}; + +struct Stages { + ShaderRecompiler::RecompileResult local; + ShaderRecompiler::RecompileResult hull; + ShaderRecompiler::RecompileResult domain; + ShaderRecompiler::RecompileResult fragment; +}; + +ShaderRecompiler::RecompileResult Compile(const ShaderRecompiler::SpirvTarget& target, ShaderStage stage, std::span code, std::uint32_t& pushCursor, const PatchDraw& patches = Triangles) { + static const std::array domainUserData{}; + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const bool hull = stage == ShaderStage::TessellationControl; + const auto userData = hull ? std::span(HullUserData) : stage == ShaderStage::TessellationEvaluation ? std::span(domainUserData) : std::span(); + std::optional pixel; + std::optional vertex; + std::optional graphics; + if (stage == ShaderStage::Fragment) { + ShaderRecompiler::ShaderPixelStageInfo info{}; + info.interpolatorCount = 1; + info.interpolatorSettings[0] = 0x400u; + info.targetOutputMode[0] = 9; + info.targetExportMapping.fill(0xe4u); + 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, 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; + const ShaderRecompiler::RecompileRequest request{ + {stage, reinterpret_cast(code.data()), code, 0, {}}, + {64, hull ? 0u : 8u, userData, std::nullopt, pixel, vertex, memory}, + target, + layout, + graphics + }; + auto result = ShaderRecompiler::Recompile(request); + pushCursor += static_cast(result.pushConstants.size()); + return result; +} + +void ExpectRefusal(const ShaderRecompiler::SpirvTarget& target, ShaderStage stage, std::span code, const std::string& reason, const PatchDraw& patches = Triangles) { + std::uint32_t cursor = 0; + try { + static_cast(Compile(target, stage, code, cursor, patches)); + } catch (const std::exception& error) { + Require(std::string(error.what()).find(reason) != std::string::npos, "the refusal named another reason: " + std::string(error.what())); + return; + } + Require(false, "a tessellation program was accepted although " + reason); +} + +void RequireCompareBranches(const ShaderRecompiler::SpirvTarget& target) { + std::uint32_t cursor = 0; + Require(!Compile(target, ShaderStage::TessellationEvaluation, DomainCompareBranch, cursor).spirv.empty(), "a domain program branching on a compare copied to VCC was not compiled"); + Require(!Compile(target, ShaderStage::TessellationControl, HullCompareBranch, cursor).spirv.empty(), "a hull program branching on a compare copied to VCC was not compiled"); +} + +std::array PixelAt(std::uint32_t x, std::uint32_t y) { + const auto offset = (static_cast(y) * Width + x) * 4u; + return {std::to_integer(Pixels[offset]), std::to_integer(Pixels[offset + 1]), std::to_integer(Pixels[offset + 2]), std::to_integer(Pixels[offset + 3])}; +} + +std::string PixelText(const std::array& pixel) { + char text[64]; + std::snprintf(text, sizeof(text), "(%u, %u, %u, %u)", pixel[0], pixel[1], pixel[2], pixel[3]); + return text; +} + +std::uint32_t Column(std::uint32_t patch, float offset) { + return static_cast((-0.9f + 0.6f * static_cast(patch) + offset + 1.0f) * 0.5f * Width); +} + +std::uint32_t Row(float y) { + return static_cast((1.0f - y) * 0.5f * Height); +} + +void Draw(AgcDriver::VulkanDevice& device, const Stages& stages, VkCullModeFlags cull, VkFrontFace front, const PatchDraw& patches = Triangles) { + for (std::size_t i = 0; i < Pixels.size(); i += 4) { + for (std::size_t c = 0; c < 4; ++c) Pixels[i + c] = std::byte{Background[c]}; + } + std::uint32_t cursor = 0; + const auto offset = [&](const ShaderRecompiler::RecompileResult& result) { + const auto at = result.pushConstants.empty() ? 0u : cursor; + cursor += static_cast(result.pushConstants.size()); + return at; + }; + const std::array shaders{{ + {ShaderStage::Local, &stages.local, offset(stages.local)}, + {ShaderStage::TessellationControl, &stages.hull, offset(stages.hull)}, + {ShaderStage::TessellationEvaluation, &stages.domain, offset(stages.domain)}, + {ShaderStage::Fragment, &stages.fragment, PixelPushOffset(cursor, device.Target())}, + }}; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Tessellation, 0x0200210du, 64, 64, std::nullopt, patches.configuration, patches.minLevel, patches.maxLevel}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_PATCH_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 = cull; + state.frontFace = front; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + device.Draw(state, {reinterpret_cast(patches.indices.data()), static_cast(patches.indices.size()), 2, patches.instances, 0, true, 0, patches.firstInstance}, shaders); + device.WaitIdle(); +} + +std::uint32_t CoveredPixels(std::uint32_t patch, bool interior) { + std::uint32_t count = 0; + for (std::uint32_t y = Row(0.5f); y <= Row(-0.5f) && y < Height; ++y) { + for (std::uint32_t x = Column(patch, 0.0f); x <= Column(patch, 0.5f) && x < Width; ++x) { + const auto pixel = PixelAt(x, y); + if (pixel == Background) continue; + Require(pixel[0] == 255 && pixel[2] == 0 && pixel[3] == 255, "patch " + std::to_string(patch) + " pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") is " + PixelText(pixel)); + if (!interior || pixel[1] > 32) ++count; + } + } + 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, 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, domain, cursor, patches), + Compile(target, ShaderStage::Fragment, PixelCode, cursor, patches), + }; + const auto center = Row(-0.5f + 1.0f / 3.0f); + for (const auto cull : {VK_CULL_MODE_NONE, VK_CULL_MODE_BACK_BIT}) { + Draw(device, stages, cull, VK_FRONT_FACE_COUNTER_CLOCKWISE, patches); + const std::string name = program + (cull == VK_CULL_MODE_NONE ? ", unculled" : ", back-face culled"); + Require(CoveredPixels(0, false) > 100 && CoveredPixels(0, true) == 0, name + ": the level-1 patch was not drawn as one flat triangle, centre " + PixelText(PixelAt(Column(0, 0.25f), center))); + Require(CoveredPixels(1, false) == 0, name + ": the patch with level 0 was not culled"); + Require(CoveredPixels(2, false) > 100 && PixelAt(Column(2, 0.25f), center)[1] > 32, name + ": the level-5 patch has no interior domain points at its centre, " + PixelText(PixelAt(Column(2, 0.25f), center))); + Require(PixelAt(0, 0) == Background && PixelAt(Width - 1, Height - 1) == Background, name + ": the corners changed"); + } + Draw(device, stages, VK_CULL_MODE_BACK_BIT, VK_FRONT_FACE_CLOCKWISE, patches); + Require(CoveredPixels(0, false) == 0 && CoveredPixels(2, false) == 0, program + ": tessellated triangles did not keep the control points' winding"); +} + +Stages CompileTriangles(const ShaderRecompiler::SpirvTarget& target, const PatchDraw& patches) { + std::uint32_t cursor = 0; + return { + Compile(target, ShaderStage::Local, LocalCode, cursor, patches), + Compile(target, ShaderStage::TessellationControl, HullCode, cursor, patches), + Compile(target, ShaderStage::TessellationEvaluation, DomainCode, cursor, patches), + Compile(target, ShaderStage::Fragment, PixelCode, cursor, patches), + }; +} + +void RequireCounterClockwise(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target) { + const PatchDraw patches{{3u, 3u, 1u, 2u, 3u}, Indices}; + const auto stages = CompileTriangles(target, patches); + Draw(device, stages, VK_CULL_MODE_BACK_BIT, VK_FRONT_FACE_COUNTER_CLOCKWISE, patches); + Require(CoveredPixels(0, false) == 0 && CoveredPixels(2, false) == 0, "counter-clockwise output kept the winding of clockwise output"); + Draw(device, stages, VK_CULL_MODE_BACK_BIT, VK_FRONT_FACE_CLOCKWISE, patches); + Require(CoveredPixels(0, false) > 100 && CoveredPixels(2, false) > 100, "counter-clockwise output did not reverse the control points' winding"); +} + +bool HasInstruction(std::span words, spv::Op opcode, std::uint32_t length, std::uint32_t operand, std::uint32_t value) { + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + Require(count != 0 && cursor + count <= words.size(), "malformed SPIR-V while scanning a tessellation module"); + if ((words[cursor] & 0xffffu) == opcode && count == length && words[cursor + operand] == value) return true; + cursor += count; + } + return false; +} + +bool HasSpacing(const ShaderRecompiler::RecompileResult& domain, spv::ExecutionMode spacing) { + return HasInstruction(domain.spirv.Words(), spv::OpExecutionMode, 3u, 2u, static_cast(spacing)); +} + +bool HasLevelBound(const ShaderRecompiler::RecompileResult& hull, float bound) { + return HasInstruction(hull.spirv.Words(), spv::OpConstant, 4u, 3u, std::bit_cast(bound)); +} + +void RequireLevelClamps(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target) { + const auto center = Row(-0.5f + 1.0f / 3.0f); + const auto stages = CompileTriangles(target, Triangles); + Require(HasSpacing(stages.domain, spv::ExecutionModeSpacingFractionalOdd) && !HasSpacing(stages.domain, spv::ExecutionModeSpacingEqual), "fractional-odd partitioning did not compile to fractional-odd spacing"); + Require(HasLevelBound(stages.hull, 63.0f) && !HasLevelBound(stages.hull, 64.0f), "fractional-odd partitioning did not bound its tessellation factors at 63"); + const PatchDraw flattened{Patches, Indices, 1.0f, 1.0f}; + Draw(device, stages, VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE, flattened); + Require(CoveredPixels(2, false) > 100 && CoveredPixels(2, true) == 0, "VGT_HOS_MAX_TESS_LEVEL 1 did not flatten the level-5 patch, centre " + PixelText(PixelAt(Column(2, 0.25f), center))); + Require(CoveredPixels(1, false) == 0, "VGT_HOS_MAX_TESS_LEVEL 1 kept the patch with level 0"); + Draw(device, stages, VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE, Triangles); + Require(CoveredPixels(2, false) > 100 && PixelAt(Column(2, 0.25f), center)[1] > 32, "the same hull kept the previous draw's VGT_HOS_MAX_TESS_LEVEL 1 for a draw with level 64, centre " + PixelText(PixelAt(Column(2, 0.25f), center))); + const PatchDraw raised{Patches, Indices, 5.0f, 64.0f}; + Draw(device, stages, VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE, raised); + Require(PixelAt(Column(0, 0.25f), center)[1] > 32, "VGT_HOS_MIN_TESS_LEVEL 5 did not tessellate the level-1 patch, centre " + PixelText(PixelAt(Column(0, 0.25f), center))); + Require(CoveredPixels(1, false) == 0, "VGT_HOS_MIN_TESS_LEVEL 5 raised the patch with level 0 instead of culling it"); +} + +const PatchDraw RecordPatches{{3u, 3u, 1u, 2u, 2u, 63u}, Indices}; + +void RequirePatchConstants(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target) { + std::uint32_t cursor = 0; + const Stages stages{ + Compile(target, ShaderStage::Local, LocalCode, cursor, RecordPatches), + Compile(target, ShaderStage::TessellationControl, HullPatchConstant, cursor, RecordPatches), + Compile(target, ShaderStage::TessellationEvaluation, DomainPatchRead, cursor, RecordPatches), + Compile(target, ShaderStage::Fragment, PixelCode, cursor, RecordPatches), + }; + Draw(device, stages, VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE, RecordPatches); + const auto center = Row(-0.5f + 1.0f / 3.0f); + for (const auto& [patch, blue] : {std::pair{0u, 32}, std::pair{2u, 159}}) { + const auto pixel = PixelAt(Column(patch, 0.25f), center); + Require(pixel[0] == 255 && std::abs(pixel[2] - blue) <= 1, "patch " + std::to_string(patch) + " did not take its blue channel from the patch constant its hull wrote, " + PixelText(pixel)); + } + Require(PixelAt(Column(1, 0.25f), center) == Background, "the patch with level 0 was not culled"); +} + +void RequireIntegerPartitioning(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target) { + const auto center = Row(-0.5f + 1.0f / 3.0f); + const PatchDraw patches{{3u, 3u, 1u, 0u, 2u}, Indices}; + const auto stages = CompileTriangles(target, patches); + Require(HasSpacing(stages.domain, spv::ExecutionModeSpacingEqual) && !HasSpacing(stages.domain, spv::ExecutionModeSpacingFractionalOdd), "integer partitioning did not compile to equal spacing"); + Require(HasLevelBound(stages.hull, 64.0f) && !HasLevelBound(stages.hull, 63.0f), "integer partitioning did not bound its tessellation factors at 64"); + Draw(device, stages, VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE, patches); + Require(CoveredPixels(0, false) > 100 && CoveredPixels(0, true) == 0, "integer partitioning: the level-1 patch was not drawn as one flat triangle, centre " + PixelText(PixelAt(Column(0, 0.25f), center))); + Require(CoveredPixels(1, false) == 0, "integer partitioning: the patch with level 0 was not culled"); + Require(CoveredPixels(2, false) > 100 && PixelAt(Column(2, 0.25f), center)[1] > 32, "integer partitioning: the level-5 patch has no interior domain points at its centre, " + PixelText(PixelAt(Column(2, 0.25f), center))); +} + +void RequireRelativePatches(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target) { + const auto center = Row(-0.5f + 1.0f / 3.0f); + for (const auto& [hull, domain, name] : {std::tuple{std::span(HullRelativeData), std::span(DomainPatchRead), "hull"}, std::tuple{std::span(HullCode), std::span(DomainRelativeData), "domain"}}) { + for (const auto& [patches, blue] : {std::pair{PatchDraw{{3u, 3u, 1u, 2u, 2u, 3u, 3u, true}, Indices}, 96}, std::pair{PatchDraw{{3u, 3u, 1u, 2u, 2u, 2u, 4u, true}, Indices}, 32}, std::pair{PatchDraw{{3u, 3u, 1u, 2u, 2u, 3u, 4u, true}, LatePatchIndices}, 96}, std::pair{PatchDraw{{3u, 3u, 1u, 2u, 2u, 3u, 2u, true}, Indices}, 32}}) { + std::uint32_t cursor = 0; + const Stages stages{ + Compile(target, ShaderStage::Local, LocalCode, cursor, patches), + Compile(target, ShaderStage::TessellationControl, hull, cursor, patches), + Compile(target, ShaderStage::TessellationEvaluation, domain, cursor, patches), + Compile(target, ShaderStage::Fragment, PixelCode, cursor, patches), + }; + Draw(device, stages, VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE, patches); + for (const auto& [patch, expected] : {std::pair{0u, 32}, std::pair{2u, blue}}) { + const auto pixel = PixelAt(Column(patch, 0.25f), center); + Require(pixel[0] == 255 && std::abs(pixel[2] - expected) <= 1, "the patch in column " + std::to_string(patch) + " with " + std::to_string(patches.configuration.patchesPerGroup) + " patches per group and " + std::to_string(patches.configuration.primitiveGroupSize) + " primitives per group did not take its blue channel from the " + name + " program's relative patch index, " + PixelText(pixel)); + } + Require(PixelAt(Column(1, 0.25f), center) == Background, "the patch with level 0 was not culled"); + } + } +} + +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() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, 2 * GuestBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(GuestBytes, 2 * GuestBytes)); +#endif + Require(block != nullptr, "tessellation: cannot allocate the guest block"); + std::memset(block, 0, 2 * GuestBytes); + GuestAllocations::Mutation().Add(block, GuestBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + std::uint64_t Address() const { return reinterpret_cast(block); } + +private: + std::uint8_t* block = nullptr; +}; + +void RequireHullBdaReads(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target) { + GuestBlock guest; + const float scale = 1.0f; + std::memcpy(guest.Data(), &scale, sizeof(scale)); + const auto address = [](std::uint64_t value) { + HullUserData[6] = static_cast(value); + HullUserData[7] = static_cast(value >> 32u); + }; + const bool llvmpipe = device.DeviceName().starts_with("llvmpipe"); + for (const auto& [hull, program] : {std::pair{std::span(HullBdaScale), std::string("hull program scaling its levels by a BDA read after s_barrier")}, std::pair{std::span(HullBdaScaleWithoutBarrier), std::string("hull program scaling its levels by a BDA read without s_barrier")}}) { + address(guest.Address()); + RequirePatches(device, target, hull, program); + if (llvmpipe) { + std::puts(("skipped the unmapped read of the " + program + ": llvmpipe runs tessellation-control loads that the check of the unmapped address skips").c_str()); + continue; + } + address(guest.Address() + GuestBytes); + std::uint32_t cursor = 0; + const Stages stages{ + Compile(target, ShaderStage::Local, LocalCode, cursor), + Compile(target, ShaderStage::TessellationControl, hull, cursor), + Compile(target, ShaderStage::TessellationEvaluation, DomainCode, cursor), + Compile(target, ShaderStage::Fragment, PixelCode, cursor), + }; + std::string failure; + try { + Draw(device, stages, VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE); + } catch (const std::exception& error) { + failure = error.what(); + } + device.WaitIdle(); + Require(failure.find("BDA access failed") != std::string::npos && failure.find("stage=7 reason=1") != std::string::npos, program + ": the unmapped read was reported as \"" + failure + "\", not as a tessellation-control BDA fault"); + } + address(0u); +} + +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{}; + for (const std::uint32_t topology : {2u, 3u}) { + const PatchDraw patches{{4u, 4u, 2u, 2u, topology}, QuadIndices, 1.0f, 9.0f}; + std::uint32_t cursor = 0; + const Stages stages{ + Compile(target, ShaderStage::Local, LocalCode, cursor, patches), + Compile(target, ShaderStage::TessellationControl, QuadHullCode, cursor, patches), + Compile(target, ShaderStage::TessellationEvaluation, QuadDomainCode, cursor, patches), + Compile(target, ShaderStage::Fragment, PixelCode, cursor, patches), + }; + const std::string name = topology == 2u ? "clockwise quad patches" : "counter-clockwise quad patches"; + Draw(device, stages, VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE, patches); + Require(CoveredPixels(0, false) > 2000 && CoveredPixels(0, true) == 0, name + ": the level-1 patch was not drawn as one flat quad, " + std::to_string(CoveredPixels(0, false)) + " pixels"); + Require(PixelAt(Column(0, 0.02f), Row(0.45f)) != Background && PixelAt(Column(0, 0.48f), Row(0.45f)) != Background, name + ": the level-1 patch misses its upper corners"); + Require(CoveredPixels(1, false) == 0, name + ": the patch with level 0 was not culled"); + Require(CoveredPixels(2, false) > 2000 && PixelAt(Column(2, 0.25f), center)[1] > 32, name + ": the level-5 patch has no interior domain points at its centre, " + PixelText(PixelAt(Column(2, 0.25f), center))); + Draw(device, stages, VK_CULL_MODE_BACK_BIT, VK_FRONT_FACE_COUNTER_CLOCKWISE, patches); + frontFacing[topology - 2u] = CoveredPixels(0, false) > 2000; + Require(frontFacing[topology - 2u] == (CoveredPixels(2, false) > 2000) && (frontFacing[topology - 2u] || CoveredPixels(0, false) == 0), name + ": back-face culling kept part of a patch"); + } + Require(frontFacing[0] != frontFacing[1], "clockwise and counter-clockwise quad patches have the same winding"); +} + +struct RegisteredStage { + Shader shader{}; + std::array registers{}; + std::array context{}; + ShaderSpecialRegs specials{}; + ShaderUserData users{}; +}; + +template +void Describe(RegisteredStage& stage, const std::array& code, std::uint8_t type, std::initializer_list registers, std::initializer_list context, std::uint32_t routing) { + stage.shader.file_header = 0x34333231u; + stage.shader.version = 0x18u; + stage.shader.header_size = sizeof(RegisteredStage); + stage.shader.shader_size = sizeof(code); + stage.shader.code = code.data(); + stage.shader.type = type; + stage.shader.user_data = &stage.users; + std::copy(registers.begin(), registers.end(), stage.registers.begin()); + stage.shader.sh_registers = stage.registers.data(); + stage.shader.num_sh_registers = static_cast(registers.size()); + std::copy(context.begin(), context.end(), stage.context.begin()); + stage.shader.cx_registers = stage.context.data(); + stage.shader.num_cx_registers = static_cast(context.size()); + stage.specials.ge_cntl = {0x25bu, 0u}; + stage.specials.vgt_shader_stages_en = {0x2d5u, routing}; + stage.specials.vgt_gs_out_prim_type = {0x29bu, 0u}; + stage.specials.ge_user_vgpr_en = {0x262u, 0u}; + stage.shader.specials = &stage.specials; +} + +void RequireRegisteredStages() { + 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 local; + RegisteredStage hull; + RegisteredStage domain; + Describe(local, LocalCode, 5u, {{0x10au, 0u}, {0x10bu, 0u}}, {}, 0u); + Describe(hull, HullCode, 7u, {{0x148u, 0u}, {0x149u, 0u}, {0x10au, 0u}, {0x10bu, 16u}, {0x108u, low(HullCode)}, {0x109u, high(HullCode)}}, {{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 : {&local, &hull, &domain}) AgcDriverRegisterShader_nid_postfix(&stage->shader); + const std::array stages{&hull.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); + AgcDriverShutdown_nid_postfix(); +} + +ShaderRecompiler::RecompileResult CompileHull(const ShaderRecompiler::SpirvTarget& target, std::span code, const ShaderRecompiler::TessellationConfiguration& configuration) { + static const std::array userData{0x1000u}; + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::RecompileRequest request{ + {ShaderStage::TessellationControl, reinterpret_cast(code.data()), code, 0, {}}, + {64, 0u, userData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, memory}, + target, + ShaderRecompiler::BindingLayout{0, 0, 0, AgcDriver::Graphics::PipelinePushSlotBytes}, + ShaderRecompiler::GraphicsCompileContext{0u, {}, std::nullopt, configuration, {reinterpret_cast(Indices.data()), static_cast(Indices.size()), 2, 1}}, + }; + return ShaderRecompiler::Recompile(request); +} + +void RequireSingleControlPointHulls(const ShaderRecompiler::SpirvTarget& target) { + Require(!CompileHull(target, HullSingleInput, {1u, 3u, 1u, 2u, 2u}).spirv.empty(), "a hull that reads only the one input control point of its patch was not compiled"); + Require(!CompileHull(target, HullSingleOutput, {1u, 1u, 1u, 2u, 2u}).spirv.empty(), "a hull with one output control point and unguarded patch outputs was not compiled"); +} + +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 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() { + try { + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto target = device->Target(); + if (!target.tessellation.has_value()) { + std::puts("tessellation: skipped, the device has no tessellation shaders"); + return VulkanTestSkipped; + } + RequireSingleControlPointHulls(target); + ExpectRefusal(target, ShaderStage::TessellationControl, HullUnguardedFactors, "not written by control point 0 alone"); + ExpectRefusal(target, ShaderStage::TessellationControl, HullSquaredAddress, "not a whole number of control points"); + ExpectRefusal(target, ShaderStage::TessellationControl, HullHalfSlotIndex, "not a whole number of control points"); + ExpectRefusal(target, ShaderStage::TessellationControl, HullLaneCount, "lane mask"); + ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainNeighbourRecord, "more than one patch-constant record", RecordPatches); + ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainFixedPatchRead, "not indexed by its relative patch", RecordPatches); + ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainNeighbourPoint, "neither its patch's control points nor a patch-constant record", RecordPatches); + ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainCulledPrimitive, "passthrough primitive"); + ExpectRefusal(target, ShaderStage::Local, LocalIndexAsData, "outside a tessellation ring or LDS address"); + ExpectRefusal(target, ShaderStage::TessellationControl, HullRelativeData, "GE_CNTL.PRIM_GRP_SIZE is 0", RecordPatches); + ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainRelativeData, "GE_CNTL.PRIM_GRP_SIZE is 0", RecordPatches); + ExpectRefusal(target, ShaderStage::TessellationControl, HullCode, "outside its patch's quad factors", {{3u, 3u, 2u, 2u, 2u}, Indices}); + ExpectRefusal(target, ShaderStage::TessellationControl, HullDescendingRecords, "not affine"); + ExpectRefusal(target, ShaderStage::TessellationControl, HullDescendingRecords, "not affine", {{3u, 3u, 1u, 2u, 2u, 158u}, Indices}); + ExpectRefusal(target, ShaderStage::TessellationControl, HullDescendingRecords, "outside its control points and patch constants", {{3u, 3u, 1u, 2u, 2u, 129u}, Indices}); + + RequirePatches(*device, target, HullCode, "hull program"); + RequirePatches(*device, target, HullRestoredExecSaved, "hull program saving its restored exec"); + RequirePatches(*device, target, 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); + 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; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} 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/Uint8x4ColorTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Uint8x4ColorTarget.cpp new file mode 100644 index 0000000000..dddb4482e2 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Uint8x4ColorTarget.cpp @@ -0,0 +1,257 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 256; +constexpr std::uint32_t Height = 64; +constexpr std::uint32_t ScissorWidth = 128; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Rgba8UintInfo = 0x8428u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t Uint16AbgrExport = 7; +constexpr std::uint8_t Kept = 0x5a; + +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, 6> 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::array abgr) { + 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; + }; + move(4, static_cast(abgr[0]) | (static_cast(abgr[1]) << 16u)); + move(5, static_cast(abgr[2]) | (static_cast(abgr[3]) << 16u)); + code[at++] = 0xf8001c0fu; + code[at++] = 0x00000504u; + 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::Graphics::ColorTileMode mode, std::uint32_t pipeBankXor) { + AgcDriver::Registers cx; + const auto address = block.Address() + pipeBankXor; + cx[0x318] = static_cast(address >> 8u); + cx[0x31b] = 0; + cx[0x31c] = info; + cx[0x31d] = 0; + cx[0x323] = 0; + cx[0x324] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = LinearAttrib3 | (static_cast(mode) << 14u); + cx[0x390] = static_cast(address >> 40u); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + color.exportIndex = 0; + color.uintExport = true; + 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 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 = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode.at(0) = Uint16AbgrExport; + 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}, {ScissorWidth, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +std::vector ReadBack(AgcDriver::VulkanDevice& device, std::uint64_t address, std::size_t bytes) { + AgcDriver::Graphics::StorageTexture::FlushPending(address, bytes, nullptr, "test read-back"); + device.WaitIdle(); + std::vector stored(bytes); + AgcDriver::GuestMemory::Read(address, std::as_writable_bytes(std::span(stored)), 1); + return stored; +} + +void Expect(AgcDriver::VulkanDevice& device, std::uint32_t info, VkFormat format, std::uint32_t elementBytes, std::array exported, const std::string& what, AgcDriver::Graphics::ColorTileMode mode = AgcDriver::Graphics::ColorTileMode::Linear, std::uint32_t pipeBankXor = 0) { + const auto bytes = static_cast(Width) * Height * elementBytes; + const AgcDriver::Graphics::ColorTargetLayout layout(Width, Height, mode, elementBytes, pipeBankXor); + Block block(layout.Bytes()); + std::memset(block.data, Kept, block.bytes); + const auto color = DecodeTarget(block, info, mode, pipeBankXor); + Require(color.format == format && color.elementBytes == elementBytes && color.bytes == layout.Bytes() && color.tileMode == mode && color.pipeBankXor == pipeBankXor, what + ": the target decoded to the wrong format, mode or XOR"); + Draw(device, color, Program(exported)); + const auto tiled = ReadBack(device, block.Address(), block.bytes); + std::vector stored(bytes); + layout.Detile(std::as_bytes(std::span(tiled)), std::as_writable_bytes(std::span(stored))); + for (std::size_t offset = 0; offset < bytes; ++offset) { + const bool inside = (offset / elementBytes) % Width < ScissorWidth; + const auto expected = inside ? static_cast(exported[offset % elementBytes]) : Kept; + Require(stored[offset] == expected, what + ": byte " + std::to_string(offset) + " is " + std::to_string(stored[offset]) + ", expected " + std::to_string(expected)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Expect(*device, Rgba8UintInfo, VK_FORMAT_R8G8B8A8_UINT, 4, {0x12u, 0xbeu, 0x42u, 0x7fu}, "a UINT16_ABGR export into an 8_8_8_8_UINT target"); + Expect(*device, Rgba8UintInfo, VK_FORMAT_R8G8B8A8_UINT, 4, {0x23u, 0x9au, 0x65u, 0x81u}, "a UINT16_ABGR export into a SW_64KB_D_X target", AgcDriver::Graphics::ColorTileMode::D64KBX); + Expect(*device, Rgba8UintInfo, VK_FORMAT_R8G8B8A8_UINT, 4, {0x31u, 0x7cu, 0x42u, 0x96u}, "a UINT16_ABGR export into a SW_64KB_D_X target with XOR", AgcDriver::Graphics::ColorTileMode::D64KBX, 0x4600); + Expect(*device, Rgba8UintInfo, VK_FORMAT_R8G8B8A8_UINT, 4, {0x47u, 0x2bu, 0xd1u, 0x6eu}, "a UINT16_ABGR export into a SW_64KB_Z_X target", AgcDriver::Graphics::ColorTileMode::Z64KBX); + Expect(*device, Rgba8UintInfo, VK_FORMAT_R8G8B8A8_UINT, 4, {0x5cu, 0x83u, 0x19u, 0xa4u}, "a UINT16_ABGR export into a SW_64KB_Z_X target with XOR", AgcDriver::Graphics::ColorTileMode::Z64KBX, 0x4600); + std::puts("8_8_8_8_UINT color target tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/UnimportedLabelOrder.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/UnimportedLabelOrder.cpp new file mode 100644 index 0000000000..2cfd3dd659 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/UnimportedLabelOrder.cpp @@ -0,0 +1,113 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Context.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 + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t ReleaseMemHeader = 0xc0064900u; + +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, "unimported label order: 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; + + volatile std::uint32_t* Words() { return reinterpret_cast(block); } + + std::uint64_t Address() const { return static_cast(reinterpret_cast(block)); } + +private: + std::uint8_t* block = nullptr; +}; + +alignas(64) volatile std::uint32_t commandMemory[16] = {}; + +std::uint64_t AddressOf(volatile const std::uint32_t* word) { + return static_cast(reinterpret_cast(word)); +} + +int StoreLabel(AgcDriver::VulkanDevice& device, std::uint64_t address, std::uint32_t value, std::uint64_t stamp) { + const auto bytes = std::as_bytes(std::span(&value, 1)); + const int reason = device.WriteLabelOnGpu(address, bytes, stamp, 0); + if (reason >= 1 && reason <= 4) { + if (reason != 1) device.WaitIdle(); + AgcDriver::GuestMemory::Write(address, bytes, 4); + } + return reason; +} + +} + +int main() { + try { + GuestBlock labels; + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + const std::array pattern{0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u}; + if (!device->FillBuffer(labels.Address() + 4096, 4096, pattern)) { + std::printf("skipped, the device does not import guest memory\n"); + return VulkanTestSkipped; + } + StoreLabel(*device, AddressOf(&commandMemory[8]), 1, 1); + StoreLabel(*device, labels.Address() + 4, 2, 2); + device->SubmitRecorded(true); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (labels.Words()[1] != 2) { + Require(std::chrono::steady_clock::now() < deadline, "unimported label order: the later label never landed"); + std::this_thread::sleep_for(std::chrono::microseconds(100)); + } + const std::uint32_t seen = commandMemory[8]; + commandMemory[8] = ReleaseMemHeader; + device->WaitIdle(); + const std::uint32_t reused = commandMemory[8]; + std::array message{}; + std::snprintf(message.data(), message.size(), "unimported label order: the label held 0x%08x when a later label had landed, and the guest's reused word holds 0x%08x instead of 0x%08x", seen, reused, ReleaseMemHeader); + Require(seen == 1 && reused == ReleaseMemHeader, message.data()); + std::puts("unimported label order 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/VCmpEqF16Clamp.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VCmpEqF16Clamp.cpp new file mode 100644 index 0000000000..5f6fcd90d6 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VCmpEqF16Clamp.cpp @@ -0,0 +1,143 @@ +#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 Inputs = 4; +constexpr std::uint32_t Results = 4; +constexpr std::uint32_t VccLow = 106u; +constexpr std::uint32_t VectorSourceBase = 256u; +constexpr std::uint32_t ShaderCmpEqF16 = 0xcau; +constexpr std::uint32_t ShaderCmpLeF64 = 0x23u; +constexpr std::uint32_t ShaderCmpNgtF64 = 0x2bu; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +struct Row { + std::array sources; + std::uint32_t expected; +}; + +struct Vop3CompareWords { + std::uint32_t word0; + std::uint32_t word1; +}; + +Vop3CompareWords Vop3Compare(std::uint32_t opcode, std::uint32_t source0, std::uint32_t source1) { + const std::uint32_t encoded0 = VectorSourceBase + source0; + const std::uint32_t encoded1 = VectorSourceBase + source1; + return {0xd4000000u | (opcode << 16u) | VccLow | 0x8000u, encoded0 | (encoded1 << 9u)}; +} + +std::vector ClampCode(const Vop3CompareWords& compare) { + return { + 0x34020082u, 0x34060082u, + 0xe0302000u, 0x80000401u, + 0xe0302004u, 0x80000501u, + 0xe0302008u, 0x80000601u, + 0xe030200cu, 0x80000701u, + 0xbf8c3f70u, + compare.word0, compare.word1, + 0xd5010014u, 0x01a90280u, + 0xe0702000u, 0x80011403u, + 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) | (4u << 16u), count, 0x11016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void RunClampRows(AgcDriver::VulkanDevice& device, const char* name, const Vop3CompareWords& compare, + const ShaderRecompiler::ShaderFloatMode& floatMode, const std::vector& rows) { + Input.fill(0u); + for (std::size_t index = 0; index < rows.size(); ++index) { + std::copy(rows[index].sources.begin(), rows[index].sources.end(), + Input.begin() + static_cast(index * Inputs)); + } + 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 auto code = ClampCode(compare); + const std::span words(code); + const std::array memory{{{reinterpret_cast(words.data()), std::as_bytes(words)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(words.data()), words, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.context.floatMode = floatMode; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(words.data())); + device.WaitIdle(); + for (std::size_t index = 0; index < rows.size(); ++index) { + const auto actual = Output[index * Results]; + Require(actual == rows[index].expected, std::string(name) + " clamp row " + std::to_string(index) + " is " + Hex(actual) + ", expected " + Hex(rows[index].expected)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const ShaderRecompiler::ShaderFloatMode denormalsKept{0xf0u, true, true, false}; + const ShaderRecompiler::ShaderFloatMode denorm32Flushed{0xc0u, true, false, false}; + RunClampRows(*device, "v_cmp_eq_f16_e64 vcc_lo, v4, v5", Vop3Compare(ShaderCmpEqF16, 4u, 5u), denormalsKept, { + {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, 0x00000001u}, + {{0x0000bc00u, 0x00000000u, 0x00000000u, 0x00000000u}, 0x00000000u}, + {{0x00003555u, 0x00000000u, 0x00000000u, 0x00000000u}, 0x00000000u}, + {{0x00000001u, 0x00008000u, 0x00000000u, 0x00000000u}, 0x00000000u}, + {{0x0000fc00u, 0x00008000u, 0x00000000u, 0x00000000u}, 0x00000000u}, + {{0x00007c01u, 0x00003c00u, 0x00000000u, 0x00000000u}, 0x00000000u}, + {{0x00006400u, 0x00003c00u, 0x00000000u, 0x00000000u}, 0x00000000u}, + {{0xbeef8000u, 0xbeefbc00u, 0xbeef0000u, 0x00000000u}, 0x00000000u}, + }); + RunClampRows(*device, "v_cmp_le_f64_e64 vcc_lo, v[4:5], v[6:7]", Vop3Compare(ShaderCmpLeF64, 4u, 6u), denorm32Flushed, { + {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, 0x00000001u}, + {{0x00000000u, 0xbff00000u, 0x00000000u, 0xfff00000u}, 0x00000000u}, + {{0x00000000u, 0xc0040000u, 0x00000000u, 0xbff00000u}, 0x00000001u}, + {{0x00000000u, 0x00100000u, 0x00000001u, 0x7ff00000u}, 0x00000000u}, + {{0x00000000u, 0xfff00000u, 0x00000000u, 0x40040000u}, 0x00000001u}, + {{0x00000001u, 0x7ff00000u, 0x00000000u, 0x41e00000u}, 0x00000000u}, + }); + RunClampRows(*device, "v_cmp_ngt_f64_e64 vcc_lo, v[4:5], v[6:7]", Vop3Compare(ShaderCmpNgtF64, 4u, 6u), denormalsKept, { + {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, 0x00000001u}, + {{0x00000000u, 0xbff00000u, 0x00000000u, 0xfff00000u}, 0x00000000u}, + {{0x00000000u, 0xc0040000u, 0x00000000u, 0xbff00000u}, 0x00000001u}, + {{0x00000000u, 0x00100000u, 0x00000001u, 0x7ff00000u}, 0x00000001u}, + }); + std::puts("v_cmp clamp 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 new file mode 100644 index 0000000000..22568a2f9a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VCmpF32Denormals.cpp @@ -0,0 +1,177 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t ValueCount = 16; +constexpr std::uint32_t Groups = ValueCount * ValueCount / Threads; +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, +}; + +constexpr std::uint32_t CaptureExec = 0xbe95037eu; +constexpr std::uint32_t CaptureVcc = 0xbe95036au; + +alignas(256) std::array Code{ + 0x34020084u, 0x34060082u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0xbe94037eu, 0x7c240b04u, + 0xbe95037eu, 0xbefe0314u, 0xd501000au, 0x00550280u, 0xe0701000u, 0x80010a03u, 0xbf810000u, +}; + +struct Row { + const char* condition; + std::uint32_t cmpx; + std::uint32_t cmp; + std::uint32_t kept[Groups]; + std::uint32_t flushed[Groups]; +}; + +constexpr std::uint32_t RowCount = 12; +constexpr Row Rows[RowCount] = { + {"eq", 0x7c240b04u, 0x7c040b04u, + {0x00030003u, 0x00080004u, 0x00200010u, 0x00800040u, 0x02000100u, 0x08000400u, 0x00000000u, 0x80000000u}, + {0x003f003fu, 0x003f003fu, 0x003f003fu, 0x00800040u, 0x02000100u, 0x08000400u, 0x00000000u, 0x80000000u}}, + {"ge", 0x7c2c0b04u, 0x7c0c0b04u, + {0x0aab0aabu, 0x0aa80aafu, 0x0aa00abfu, 0x0a800affu, 0x0a000bffu, 0x08008fffu, 0x00000000u, 0x8bff0000u}, + {0x0abf0abfu, 0x0abf0abfu, 0x0abf0abfu, 0x0a800affu, 0x0a000bffu, 0x08008fffu, 0x00000000u, 0x8bff0000u}}, + {"gt", 0x7c280b04u, 0x7c080b04u, + {0x0aa80aa8u, 0x0aa00aabu, 0x0a800aafu, 0x0a000abfu, 0x08000affu, 0x00008bffu, 0x00000000u, 0x0bff0000u}, + {0x0a800a80u, 0x0a800a80u, 0x0a800a80u, 0x0a000abfu, 0x08000affu, 0x00008bffu, 0x00000000u, 0x0bff0000u}}, + {"le", 0x7c260b04u, 0x7c060b04u, + {0x85578557u, 0x855f8554u, 0x857f8550u, 0x85ff8540u, 0x87ff8500u, 0x8fff0400u, 0x00000000u, 0x84000000u}, + {0x857f857fu, 0x857f857fu, 0x857f857fu, 0x85ff8540u, 0x87ff8500u, 0x8fff0400u, 0x00000000u, 0x84000000u}}, + {"lg", 0x7c2a0b04u, 0x7c0a0b04u, + {0x8ffc8ffcu, 0x8ff78ffbu, 0x8fdf8fefu, 0x8f7f8fbfu, 0x8dff8effu, 0x87ff8bffu, 0x00000000u, 0x0fff0000u}, + {0x8fc08fc0u, 0x8fc08fc0u, 0x8fc08fc0u, 0x8f7f8fbfu, 0x8dff8effu, 0x87ff8bffu, 0x00000000u, 0x0fff0000u}}, + {"lt", 0x7c220b04u, 0x7c020b04u, + {0x85548554u, 0x85578550u, 0x855f8540u, 0x857f8500u, 0x85ff8400u, 0x87ff0000u, 0x00000000u, 0x04000000u}, + {0x85408540u, 0x85408540u, 0x85408540u, 0x857f8500u, 0x85ff8400u, 0x87ff0000u, 0x00000000u, 0x04000000u}}, + {"neq", 0x7c3a0b04u, 0x7c1a0b04u, + {0xfffcfffcu, 0xfff7fffbu, 0xffdfffefu, 0xff7fffbfu, 0xfdfffeffu, 0xf7fffbffu, 0xffffffffu, 0x7fffffffu}, + {0xffc0ffc0u, 0xffc0ffc0u, 0xffc0ffc0u, 0xff7fffbfu, 0xfdfffeffu, 0xf7fffbffu, 0xffffffffu, 0x7fffffffu}}, + {"nge", 0x7c320b04u, 0x7c120b04u, + {0xf554f554u, 0xf557f550u, 0xf55ff540u, 0xf57ff500u, 0xf5fff400u, 0xf7ff7000u, 0xffffffffu, 0x7400ffffu}, + {0xf540f540u, 0xf540f540u, 0xf540f540u, 0xf57ff500u, 0xf5fff400u, 0xf7ff7000u, 0xffffffffu, 0x7400ffffu}}, + {"ngt", 0x7c360b04u, 0x7c160b04u, + {0xf557f557u, 0xf55ff554u, 0xf57ff550u, 0xf5fff540u, 0xf7fff500u, 0xffff7400u, 0xffffffffu, 0xf400ffffu}, + {0xf57ff57fu, 0xf57ff57fu, 0xf57ff57fu, 0xf5fff540u, 0xf7fff500u, 0xffff7400u, 0xffffffffu, 0xf400ffffu}}, + {"nle", 0x7c380b04u, 0x7c180b04u, + {0x7aa87aa8u, 0x7aa07aabu, 0x7a807aafu, 0x7a007abfu, 0x78007affu, 0x7000fbffu, 0xffffffffu, 0x7bffffffu}, + {0x7a807a80u, 0x7a807a80u, 0x7a807a80u, 0x7a007abfu, 0x78007affu, 0x7000fbffu, 0xffffffffu, 0x7bffffffu}}, + {"nlg", 0x7c340b04u, 0x7c140b04u, + {0x70037003u, 0x70087004u, 0x70207010u, 0x70807040u, 0x72007100u, 0x78007400u, 0xffffffffu, 0xf000ffffu}, + {0x703f703fu, 0x703f703fu, 0x703f703fu, 0x70807040u, 0x72007100u, 0x78007400u, 0xffffffffu, 0xf000ffffu}}, + {"nlt", 0x7c3c0b04u, 0x7c1c0b04u, + {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; +} + +ShaderRecompiler::RecompileResult Compile(const AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloatMode mode, std::uint32_t compare, std::uint32_t capture) { + Code[6] = compare; + Code[7] = capture; + 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; + 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) { + Require(Output[lane] <= 1u, "v_cmp f32 denormals: lane " + std::to_string(lane) + " stored " + Hex(Output[lane])); + mask |= Output[lane] << lane; + } + return mask; +} + +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, program, group); + if (actual == expected[group]) continue; + const std::uint32_t diff = actual ^ expected[group]; + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + if ((diff >> lane & 1u) == 0u) continue; + const std::uint32_t pair = group * Threads + lane; + Require(false, + name + " " + modeName + " of " + Hex(Values[pair / ValueCount]) + " and " + + Hex(Values[pair % ValueCount]) + " is " + std::to_string(actual >> lane & 1u) + ", expected " + + std::to_string(expected[group] >> lane & 1u)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const Row& row : Rows) { + for (const bool exec : {true, false}) { + Check(*device, "denormals kept, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}, row, exec, row.kept); + Check(*device, "denormals kept, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xf0u, false, false, false}, row, exec, row.kept); + Check(*device, "denormals flushed, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}, row, exec, row.flushed); + Check(*device, "denormals flushed, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xc0u, false, false, false}, row, exec, row.flushed); + } + } + std::puts("v_cmp and v_cmpx f32 denormals 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/VCvtI16F16OutputModifiers.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VCvtI16F16OutputModifiers.cpp new file mode 100644 index 0000000000..d7a66c2b49 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VCvtI16F16OutputModifiers.cpp @@ -0,0 +1,136 @@ +#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; +constexpr std::uint32_t RowCount = 16; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) std::array Code{ + 0x34020084u, 0x34060084u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, + 0x7e1402ffu, 0x5a5a5a5au, 0xd5d3000au, 0x00000104u, + 0xe0781000u, 0x80010a03u, 0xbf810000u, +}; + +struct Variant { + const char* modifiers; + std::uint32_t clamp; + std::uint32_t omod; +}; + +constexpr Variant Variants[] = { + {"mul:2", 0, 1}, + {"mul:4", 0, 2}, + {"div:2", 0, 3}, + {"clamp", 1, 0}, + {"clamp mul:2", 1, 1}, +}; + +constexpr std::uint32_t Rows[RowCount] = { + 0x00003c00u, + 0x0000bc00u, + 0x0000c100u, + 0x00004200u, + 0x00006400u, + 0x00005640u, + 0x0000d640u, + 0x00007bffu, + 0x0000fbffu, + 0x00007e00u, + 0xbeef3c00u, + 0xbeefc100u, + 0x00007c00u, + 0x0000fc00u, + 0x00007c01u, + 0x00003e00u, +}; + +constexpr std::uint32_t Expected[RowCount] = { + 0x5a5a0001u, 0x5a5affffu, 0x5a5afffeu, 0x5a5a0003u, + 0x5a5a0400u, 0x5a5a0064u, 0x5a5aff9cu, 0x5a5a7fffu, + 0x5a5a8000u, 0x5a5a0000u, 0x5a5a0001u, 0x5a5afffeu, + 0x5a5a7fffu, 0x5a5a8000u, 0x5a5a0000u, 0x5a5a0001u, +}; + +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 ShaderRecompiler::ShaderFloatMode& floatMode, const Variant& variant) { + Code[7] = 0xd5d3000au | variant.clamp << 15u; + Code[8] = 0x00000104u | variant.omod << 27u; + Input.fill(0u); + for (std::uint32_t tid = 0; tid < RowCount; ++tid) Input[tid * Inputs] = Rows[tid]; + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {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(const char* mode, const Variant& variant) { + for (std::uint32_t tid = 0; tid < RowCount; ++tid) { + const std::uint32_t actual = Output[tid * Results]; + Require(actual == Expected[tid], std::string("v_cvt_i16_f16 ") + variant.modifiers + " " + mode + ": lane " + std::to_string(tid) + + " of " + Hex(Rows[tid]) + " is " + Hex(actual) + ", expected " + Hex(Expected[tid])); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const Variant& variant : Variants) { + Run(*device, ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}, variant); + Check("M1", variant); + Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}, variant); + Check("M2", variant); + } + std::puts("v_cvt_i16_f16 output modifiers 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/VCvtPknormF16SourceModifiers.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VCvtPknormF16SourceModifiers.cpp new file mode 100644 index 0000000000..4e90961e54 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VCvtPknormF16SourceModifiers.cpp @@ -0,0 +1,174 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 2; +constexpr std::uint32_t RowCount = 8; +constexpr std::uint32_t VariantCount = 8; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) std::array Code{ + 0x34020084u, 0x34060083u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0xd712000au, 0x00020b04u, + 0xd713000bu, 0x00020b04u, 0xe0701000u, 0x80010a03u, 0xe0701004u, 0x80010b03u, 0xbf810000u, +}; + +struct Variant { + const char* operands; + std::uint32_t abs; + std::uint32_t neg; + std::uint32_t opSel; +}; + +constexpr Variant Variants[VariantCount] = { + {"|v4|, v5", 1, 0, 0}, + {"v4, |v5|", 2, 0, 0}, + {"-v4, v5", 0, 1, 0}, + {"v4, -v5", 0, 2, 0}, + {"-|v4|, v5", 1, 1, 0}, + {"v4, -|v5|", 2, 2, 0}, + {"v4, v5 op_sel:[1,0,0]", 0, 0, 1}, + {"v4, v5 op_sel:[0,1,0]", 0, 0, 2}, +}; + +constexpr std::uint32_t Rows[RowCount][2] = { + {0x00000400u, 0x00000400u}, + {0x000083ffu, 0x000083ffu}, + {0x00003555u, 0x0000d640u}, + {0x0000bc00u, 0x00003c00u}, + {0x00007bffu, 0x00000400u}, + {0xbeef0000u, 0xbeef3c00u}, + {0xbeef8000u, 0xbeef0000u}, + {0xbeef0000u, 0xbeeffc00u}, +}; + +constexpr std::uint32_t Expected[VariantCount][Results][RowCount] = { + { + {0x00020002u, 0xfffe0002u, 0x80012aa8u, 0x7fff7fffu, 0x00027fffu, 0x7fff0000u, 0x00000000u, 0x80010000u}, + {0x00040004u, 0x00000004u, 0x00005550u, 0xffffffffu, 0x0004ffffu, 0xffff0000u, 0x00000000u, 0x00000000u}, + }, + { + {0x00020002u, 0x0002fffeu, 0x7fff2aa8u, 0x7fff8001u, 0x00027fffu, 0x7fff0000u, 0x00000000u, 0x7fff0000u}, + {0x00040004u, 0x00040000u, 0xffff5550u, 0xffff0000u, 0x0004ffffu, 0xffff0000u, 0x00000000u, 0xffff0000u}, + }, + { + {0x0002fffeu, 0xfffe0002u, 0x8001d558u, 0x7fff7fffu, 0x00028001u, 0x7fff0000u, 0x00000000u, 0x80010000u}, + {0x00040000u, 0x00000004u, 0x00000000u, 0xffffffffu, 0x00040000u, 0xffff0000u, 0x00000000u, 0x00000000u}, + }, + { + {0xfffe0002u, 0x0002fffeu, 0x7fff2aa8u, 0x80018001u, 0xfffe7fffu, 0x80010000u, 0x00000000u, 0x7fff0000u}, + {0x00000004u, 0x00040000u, 0xffff5550u, 0x00000000u, 0x0000ffffu, 0x00000000u, 0x00000000u, 0xffff0000u}, + }, + { + {0x0002fffeu, 0xfffefffeu, 0x8001d558u, 0x7fff8001u, 0x00028001u, 0x7fff0000u, 0x00000000u, 0x80010000u}, + {0x00040000u, 0x00000000u, 0x00000000u, 0xffff0000u, 0x00040000u, 0xffff0000u, 0x00000000u, 0x00000000u}, + }, + { + {0xfffe0002u, 0xfffefffeu, 0x80012aa8u, 0x80018001u, 0xfffe7fffu, 0x80010000u, 0x00000000u, 0x80010000u}, + {0x00000004u, 0x00000000u, 0x00005550u, 0x00000000u, 0x0000ffffu, 0x00000000u, 0x00000000u, 0x00000000u}, + }, + { + {0x00020000u, 0xfffe0000u, 0x80010000u, 0x7fff0000u, 0x00020000u, 0x7fff8001u, 0x00008001u, 0x80018001u}, + {0x00040000u, 0x00000000u, 0x00000000u, 0xffff0000u, 0x00040000u, 0xffff0000u, 0x00000000u, 0x00000000u}, + }, + { + {0x00000002u, 0x0000fffeu, 0x00002aa8u, 0x00008001u, 0x00007fffu, 0x80010000u, 0x80010000u, 0x80010000u}, + {0x00000004u, 0x00000000u, 0x00005550u, 0x00000000u, 0x0000ffffu, 0x00000000u, 0x00000000u, 0x00000000u}, + }, +}; + +constexpr const char* Opcodes[Results] = {"v_cvt_pknorm_i16_f16_e64 v10, ", "v_cvt_pknorm_u16_f16_e64 v11, "}; + +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 mode, const Variant& variant) { + const std::uint32_t first = 0xd7120000u | variant.opSel << 11u | variant.abs << 8u | 10u; + const std::uint32_t second = 0xd7130000u | variant.opSel << 11u | variant.abs << 8u | 11u; + const std::uint32_t sources = 0x00020b04u | variant.neg << 29u; + Code[5] = first; + Code[6] = sources; + Code[7] = second; + Code[8] = sources; + for (std::uint32_t index = 0; index < RowCount; ++index) { + std::copy(std::begin(Rows[index]), std::end(Rows[index]), &Input[index * Inputs]); + } + for (std::uint32_t index = RowCount * Inputs; index < Input.size(); ++index) Input[index] = 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; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const char* modeName, std::uint32_t variantIndex) { + for (std::uint32_t slot = 0; slot < Results; ++slot) { + for (std::uint32_t index = 0; index < RowCount; ++index) { + const std::uint32_t actual = Output[index * Results + slot]; + const std::uint32_t expected = Expected[variantIndex][slot][index]; + Require(actual == expected, + std::string("v_cvt_pknorm f16 source modifiers: ") + modeName + " row " + std::to_string(index) + " " + + Opcodes[slot] + Variants[variantIndex].operands + " of " + Hex(Rows[index][0]) + " and " + + Hex(Rows[index][1]) + " is " + Hex(actual) + ", expected " + Hex(expected)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (std::uint32_t index = 0; index < VariantCount; ++index) { + Run(*device, ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}, Variants[index]); + Check("FLOAT_MODE 0xf0", index); + Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}, Variants[index]); + Check("FLOAT_MODE 0xc0", index); + } + std::puts("v_cvt_pknorm f16 source modifiers 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/VectorWindowDirectAccess.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VectorWindowDirectAccess.cpp new file mode 100644 index 0000000000..0f02fa9bd9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VectorWindowDirectAccess.cpp @@ -0,0 +1,125 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t MaxThreads = 64; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 16; +constexpr std::uint32_t Checked = 14; +constexpr std::uint32_t WrittenLane = 3; +constexpr std::uint32_t ReadLane = 5; +constexpr std::uint32_t LaneValue = 0x5a5a0000u; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084, 0x34060086, 0xe0301000, 0x80000401, 0xe0301004, 0x80000b01, 0xbe8803ff, 0x5a5a0000, + 0xbf8c3f70, 0x4a100881, 0x4a120882, 0x7e140304, 0xd761000a, 0x00010608, 0xd7600009, 0x0001070a, + 0xd760000a, 0x00010b0a, 0x4a180809, 0xbefc0380, 0x7e208708, 0xbefc0381, 0x7e228708, 0xbefc0382, + 0x7e248708, 0xbefc0383, 0x7e268708, 0xbefc0384, 0x7e288708, 0x34120884, 0xbefc0381, 0x7e2a8708, + 0xbefc0388, 0x7e2c8708, 0x7e2e0209, 0x7e30020a, 0xe0701000, 0x80010803, 0xe0701004, 0x80010903, + 0xe0701008, 0x80010a03, 0xe070100c, 0x80010b03, 0xe0701010, 0x80010c03, 0xe0701014, 0x80011003, + 0xe0701018, 0x80011103, 0xe070101c, 0x80011203, 0xe0701020, 0x80011303, 0xe0701024, 0x80011403, + 0xe0701028, 0x80011503, 0xe070102c, 0x80011603, 0xe0701030, 0x80011703, 0xe0701034, 0x80011803, 0xbf810000, +}; + +constexpr const char* Names[Checked] = {"valu_direct", "rewritten_direct", "writelane_direct", "loaded_direct", "readlane_sum_direct", "movrels_valu", "movrels_valu_neighbour", + "movrels_writelane", "movrels_loaded", "movrels_readlane_sum", "movrels_rewritten", "movrels_of_movrels_result", "readlane_written_lane", "readlane_other_lane"}; + +std::uint32_t First(std::uint32_t tid) { + return 0x1000u * (tid + 1u); +} + +std::uint32_t Second(std::uint32_t tid) { + return 0x00770000u + tid * 0x101u; +} + +std::array ExpectedFor(std::uint32_t tid) { + const std::uint32_t x = First(tid); + const std::uint32_t y = Second(tid); + const std::uint32_t lane = tid == WrittenLane ? LaneValue : x; + return {x + 1u, x << 4u, lane, y, x + LaneValue, x + 1u, x + 2u, lane, y, x + LaneValue, x << 4u, x + 1u, LaneValue, First(ReadLane)}; +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, std::uint32_t threads) { + Input.fill(0u); + for (std::uint32_t tid = 0; tid < threads; ++tid) { + Input[tid * Inputs] = First(tid); + Input[tid * Inputs + 1u] = Second(tid); + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(std::uint32_t waveSize, std::uint32_t threads) { + for (std::uint32_t tid = 0; tid < threads; ++tid) { + const std::uint32_t* out = &Output[tid * Results]; + const auto expected = ExpectedFor(tid); + for (std::uint32_t index = 0; index < Checked; ++index) { + Require(out[index] == expected[index], std::string("vector window direct access: wave") + std::to_string(waveSize) + " lane " + std::to_string(tid) + " " + Names[index] + " is " + Hex(out[index]) + ", expected " + Hex(expected[index])); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (device->Target().subgroupSize < 32u) { + std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize); + return VulkanTestSkipped; + } + Run(*device, 32, 32); + Check(32, 32); + Run(*device, 64, 64); + Check(64, 64); + std::puts("vector window direct access tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/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 new file mode 100644 index 0000000000..08adb9fde4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDistances.cpp @@ -0,0 +1,208 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 192; +constexpr std::uint32_t Height = 128; +constexpr std::uint32_t DistanceVector = 0x00400000u; +alignas(256) std::array Pixels{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xe0382010, 0x80000405, 0xbf8c3f70, + 0xf80000cf, 0x03020100, 0xf80008df, 0x07060504, 0xbf810000, +}; + +alignas(256) constexpr std::array MeshCode{ + 0x8f6a9003, 0x94fe6ac1, 0xbf88000b, 0xd7650006, 0x000100c1, 0xd7660006, 0x00020cc1, 0x93ebff03, + 0x00040018, 0xd7460006, 0x04190c6b, 0x340c0c82, 0xd8340000, 0x00000506, 0xbf8cc07f, 0xbefe04c1, + 0xbf8a0000, 0x938dff02, 0x00090016, 0x9382ff03, 0x00040018, 0x938cff03, 0x00080008, 0xd7650009, + 0x000100c1, 0xd7660009, 0x000212c1, 0xd746000a, 0x04250c02, 0x7da8120c, 0xbf88002e, 0x361600ff, + 0x0000ffff, 0x2c180090, 0x361a02ff, 0x0000ffff, 0xd8d80000, 0x0b00000b, 0xd8d80000, 0x0c00000c, + 0xd8d80000, 0x0d00000d, 0xbf8cc07f, 0xe0382000, 0x8002100b, 0xe0382010, 0x8002140b, 0xe0382000, + 0x8002180c, 0xe0382010, 0x80021c0c, 0xe0382000, 0x8002200d, 0xe0382010, 0x8002240d, 0x161c1483, + 0x161e14ff, 0x00000060, 0xbf8c3f70, 0xdb7c0400, 0x0000100f, 0xdb7c0410, 0x0000140f, 0xdb7c0420, + 0x0000180f, 0xdb7c0430, 0x00001c0f, 0xdb7c0440, 0x0000200f, 0xdb7c0450, 0x0000240f, 0x4a501c81, + 0x4a521c82, 0x3450508a, 0x34525294, 0xd772002a, 0x04a6510e, 0xbf8cc07f, 0xbefe04c1, 0xbf8a0000, + 0x930e830d, 0xbf078002, 0xbf850003, 0x8f0f8c0d, 0x887c0f0e, 0xbf900009, 0x7da8140d, 0xbf880002, + 0xf8000941, 0x0000002a, 0xbefe04c1, 0x7da8140e, 0xbf88000c, 0x34561485, 0xdbfc0400, 0x2c00002b, + 0xdbfc0410, 0x3000002b, 0xbf8cc07f, 0xf80000cf, 0x2f2e2d2c, 0xf80008df, 0x33323130, 0xf800020f, + 0x33323130, 0xbf810000, +}; + +constexpr ShaderRecompiler::MeshConfiguration Subgroup{4u, 4u, 12u, 12u, 4u, 64u, 1024u, 0u, 4u}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e0002f2, 0x7e0202f2, 0x7e0402f2, 0x7e0602f2, 0xf800080f, 0x03020100, 0xbf810000, +}; + +struct Vertex { + std::array position; + std::array distances; +}; + +constexpr std::array PositionX{-3.0f, 1.0f, 1.0f}; +constexpr std::array PositionY{-1.0f, -1.0f, 3.0f}; +constexpr std::array Negative{-1.0f, -1.0f, -1.0f}; +constexpr std::array Mixed{-1.0f, 1.0f, -1.0f}; + +enum class Visible { All, None, X, Y, XAndY }; + +struct Row { + const char* name; + std::uint32_t control; + Visible visible; + std::array component0; + std::array component1; + std::array component2; +}; + +const std::array Rows{{ + {"no enable bits", DistanceVector, Visible::All, Negative, Negative, Negative}, + {"clip plane 0 from component 0", DistanceVector | 0x1u, Visible::X, PositionX, Negative, Negative}, + {"clip plane 1 alone, from component 1", DistanceVector | 0x2u, Visible::Y, Negative, PositionY, Negative}, + {"clip planes 0x5, components 0 and 2", DistanceVector | 0x5u, Visible::XAndY, PositionX, Negative, PositionY}, + {"cull plane 0, every vertex negative", DistanceVector | 0x100u, Visible::None, Negative, Negative, Negative}, + {"cull plane 0, one vertex not negative", DistanceVector | 0x100u, Visible::All, Mixed, Negative, Negative}, + {"cull plane 0 from component 2, sparse 0x4", DistanceVector | 0x400u, Visible::None, Mixed, Negative, Negative}, +}}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +bool Expected(Visible visible, std::uint32_t pixel) { + const double x = -1.0 + 2.0 * ((pixel % Width) + 0.5) / Width; + const double y = 1.0 - 2.0 * ((pixel / Width) + 0.5) / Height; + switch (visible) { + case Visible::All: return true; + case Visible::None: return false; + case Visible::X: return x >= 0.0; + case Visible::Y: return y >= 0.0; + case Visible::XAndY: return x >= 0.0 && y >= 0.0; + } + return false; +} + +void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, const Row& row, bool mesh) { + Pixels.fill(std::byte{0}); + std::array vertices{}; + for (std::size_t i = 0; i < vertices.size(); ++i) { + vertices[i] = {{PositionX[i], PositionY[i], 0.5f, 1.0f}, {row.component0[i], row.component1[i], row.component2[i], 0.0f}}; + } + + std::vector vertexUserData(mesh ? 12u : 4u, 0u); + const auto vertexBuffer = BufferDescriptor(vertices.data(), sizeof(Vertex), static_cast(vertices.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin() + (mesh ? 8 : 0)); + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(vertices.size()), 0, 1, 0, false}; + const std::span code = mesh ? std::span(MeshCode) : std::span(VertexCode); + const auto stage = mesh ? ShaderStage::Mesh : ShaderStage::Vertex; + const std::array vertexMemory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + ShaderRecompiler::ShaderVertexStageInfo vertexInfo{}; + vertexInfo.paClVsOutCntl = row.control; + ShaderRecompiler::RecompileRequest vertex{ + {stage, reinterpret_cast(code.data()), code, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, vertexInfo, vertexMemory}, + device.Target(), + {0, 0, 0, mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : 64u} + }; + if (mesh) vertex.graphics = ShaderRecompiler::GraphicsCompileContext{0, {}, Subgroup, std::nullopt, {draw.indexAddress, draw.indexCount, draw.indexSize, draw.instanceCount}}; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.wave32 = waveSize == 32u; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + const std::array pixelMemory{{{reinterpret_cast(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}}; + const std::vector pixelUserData(4, 0u); + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, + {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + device.Target(), + PixelPushLayout(vertexPush, device.Target()) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {stage, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, device.Target())} + }}; + + AgcDriver::Graphics::State state{}; + if (mesh) state.stages = {AgcDriver::Graphics::ShaderPath::Geometry, 0x20u, waveSize, waveSize, Subgroup, std::nullopt}; + else state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.paClVsOutCntl = row.control; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + 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 = {}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +void Check(const Row& row, std::uint32_t waveSize, bool mesh) { + const auto what = std::string(mesh ? "mesh" : "vertex") + " clip and cull distances, " + row.name + ", wave" + std::to_string(waveSize); + for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { + const auto value = std::to_integer(Pixels[pixel * 4u]); + Require(value == 0u || value == 255u, what + ": pixel " + std::to_string(pixel) + " stored " + std::to_string(value)); + const bool written = value == 255u; + const bool expected = Expected(row.visible, pixel); + Require(written == expected, what + ": pixel (" + std::to_string(pixel % Width) + ", " + std::to_string(pixel / Width) + ") is " + (written ? "written" : "clear") + ", expected " + (expected ? "written" : "clear")); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (device->Target().subgroupSize < 32u) { + std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize); + return VulkanTestSkipped; + } + for (const auto waveSize : {64u, 32u}) { + for (const auto& row : Rows) { + Draw(*device, waveSize, row, false); + Check(row, waveSize, false); + } + } + if (device->Target().mesh.has_value()) { + for (const auto& row : Rows) { + Draw(*device, 64u, row, true); + Check(row, 64u, true); + } + } else { + std::puts("mesh case skipped, the device has no VK_EXT_mesh_shader"); + } + std::puts("vertex clip and cull distance tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDraw.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDraw.cpp new file mode 100644 index 0000000000..076e006c49 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDraw.cpp @@ -0,0 +1,246 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::uint32_t Width = 192; +constexpr std::uint32_t Height = 128; +constexpr std::uint32_t DistanceVector = 0x00400000u; +constexpr std::uint32_t SetContextReg = 0x69u; +constexpr std::uint32_t SetShReg = 0x76u; +constexpr std::uint32_t SetUconfigReg = 0x79u; +constexpr std::uint32_t DrawIndexAuto = 0x2du; + +constexpr std::size_t Block = 65536; +constexpr std::size_t ColorBytes = Width * Height * 4; +std::byte* ColorMemory = nullptr; +alignas(64) volatile std::uint32_t EndOfPipe = 0; + +std::byte* AllocateWatched(std::size_t bytes) { +#ifdef _WIN32 + void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block); + GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); +#else + void* raw = mmap(nullptr, bytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (raw == MAP_FAILED) throw std::runtime_error("cannot map the watched block"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + Block - 1) & ~static_cast(Block - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + Block) munmap(reinterpret_cast(aligned + bytes), begin + Block - aligned); + void* block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes); +#endif + return static_cast(block); +} + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80020005, 0xe0382010, 0x80020405, 0xbf8c3f70, + 0xf80000cf, 0x03020100, 0xf80008df, 0x07060504, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e0002f2, 0x7e0202f2, 0x7e0402f2, 0x7e0602f2, 0xf800080f, 0x03020100, 0xbf810000, +}; + +struct Vertex { + std::array position; + std::array distances; +}; + +constexpr std::array PositionX{-3.0f, 1.0f, 1.0f}; +constexpr std::array PositionY{-1.0f, -1.0f, 3.0f}; +constexpr std::array Negative{-1.0f, -1.0f, -1.0f}; +constexpr std::array Mixed{-1.0f, 1.0f, -1.0f}; + +enum class Visible { All, None, X, Y }; + +struct Step { + const char* name; + std::uint32_t control; + Visible visible; + std::array component0; + std::array component1; + std::array component2; +}; + +const std::array Steps{{ + {"no enable bits", DistanceVector, Visible::All, Negative, Negative, Negative}, + {"clip plane 0 from component 0", DistanceVector | 0x1u, Visible::X, PositionX, Negative, Negative}, + {"cull plane 0 from component 2, not every vertex negative", DistanceVector | 0x400u, Visible::All, Negative, Negative, Mixed}, + {"clip plane 1 alone, from component 1", DistanceVector | 0x2u, Visible::Y, Negative, PositionY, Negative}, +}}; + +struct RegisteredShader { + Shader shader; + ShaderUserData userData; +}; +RegisteredShader VertexShader{}; +RegisteredShader PixelShader{}; + +const std::vector> ContextBaseline{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b8, 0x9000000}, {0x1e0, 0}, + {0xc, 0}, + {0x81, 0x80000000}, {0x90, 0x80000000}, {0x94, 0x80000000}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, {0x114, 0}, {0x113, std::bit_cast(1.0f)}, +}; + +void Register(RegisteredShader& registered, const void* code, std::uint32_t bytes, std::uint8_t type) { + registered.shader.file_header = 0x34333231u; + registered.shader.version = 0x18u; + registered.shader.user_data = ®istered.userData; + registered.shader.code = code; + registered.shader.header_size = sizeof(RegisteredShader); + registered.shader.shader_size = bytes; + registered.shader.type = type; + AgcDriverRegisterShader_nid_postfix(®istered.shader); +} + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +void SetRegister(std::vector& words, std::uint32_t opcode, std::uint32_t offset, std::uint32_t value) { + words.push_back(0xc0010000u | (opcode << 8u)); + words.push_back(offset); + words.push_back(value); +} + +std::vector Frame(const Step& step, const std::array& vertices) { + const auto colorAddress = reinterpret_cast(ColorMemory); + const auto vertexAddress = reinterpret_cast(VertexCode.data()); + const auto pixelAddress = reinterpret_cast(PixelCode.data()); + std::vector words; + for (const auto& [offset, value] : ContextBaseline) SetRegister(words, SetContextReg, offset, value); + SetRegister(words, SetContextReg, 0x207, step.control); + SetRegister(words, SetContextReg, 0x318, static_cast(colorAddress >> 8u)); + SetRegister(words, SetContextReg, 0x390, static_cast(colorAddress >> 40u)); + SetRegister(words, SetContextReg, 0x3b0, ((Width - 1u) << 14u) | (Height - 1u)); + SetRegister(words, SetContextReg, 0x10f, std::bit_cast(Width / 2.0f)); + SetRegister(words, SetContextReg, 0x110, std::bit_cast(Width / 2.0f)); + SetRegister(words, SetContextReg, 0x111, std::bit_cast(-(Height / 2.0f))); + SetRegister(words, SetContextReg, 0x112, std::bit_cast(Height / 2.0f)); + const auto extent = (Height << 16u) | Width; + SetRegister(words, SetContextReg, 0xd, extent); + SetRegister(words, SetContextReg, 0x82, extent); + SetRegister(words, SetContextReg, 0x91, extent); + SetRegister(words, SetContextReg, 0x95, extent); + SetRegister(words, SetUconfigReg, 0x242, 4u); + SetRegister(words, SetShReg, 0x008, static_cast(pixelAddress >> 8u)); + SetRegister(words, SetShReg, 0x009, static_cast(pixelAddress >> 40u)); + SetRegister(words, SetShReg, 0x00b, 0u); + SetRegister(words, SetShReg, 0x0c8, static_cast(vertexAddress >> 8u)); + SetRegister(words, SetShReg, 0x0c9, static_cast(vertexAddress >> 40u)); + SetRegister(words, SetShReg, 0x08b, 4u << 1u); + const auto descriptor = BufferDescriptor(vertices.data(), sizeof(Vertex), static_cast(vertices.size())); + for (std::uint32_t i = 0; i < descriptor.size(); ++i) SetRegister(words, SetShReg, 0x08c + i, descriptor[i]); + words.insert(words.end(), {0xc0000000u | (1u << 16u) | (DrawIndexAuto << 8u), static_cast(vertices.size()), 2u}); + const auto labelAddress = reinterpret_cast(&EndOfPipe); + words.insert(words.end(), {0xc0064900u, 0x514u, (1u << 29u) | (2u << 24u), static_cast(labelAddress), static_cast(static_cast(labelAddress) >> 32u), 1u, 0u, 0u}); + return words; +} + +void Check(const Step& step, const std::vector& colors) { + const auto what = std::string("vertex clip and cull distances through the driver, ") + step.name; + for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { + const auto value = std::to_integer(colors[pixel * 4u]); + Require(value == 0u || value == 255u, what + ": pixel " + std::to_string(pixel) + " stored " + std::to_string(value)); + const double x = -1.0 + 2.0 * ((pixel % Width) + 0.5) / Width; + const double y = 1.0 - 2.0 * ((pixel / Width) + 0.5) / Height; + const bool expected = step.visible == Visible::All || (step.visible == Visible::X && x >= 0.0) || (step.visible == Visible::Y && y >= 0.0); + const bool written = value == 255u; + Require(written == expected, what + ": pixel (" + std::to_string(pixel % Width) + ", " + std::to_string(pixel / Width) + ") is " + (written ? "written" : "clear") + ", expected " + (expected ? "written" : "clear")); + } +} + + +// A program that exports only pos0 is compiled without the PA_CL_VS_OUT_CNTL position export bits, at +// registration and at draw alike; one that exports pos1 keeps them, without the bits outside the layout. +void PositionExportLayoutTests() { + using AgcDriver::DriverDetail::PositionExportLayout; + constexpr std::array PositionOnly{0xf80008cfu, 0x03020100u, 0xbf810000u}; + Require(PositionExportLayout(nullptr, 0, VertexCode, 0x0040000fu) == 0x0040000fu, "a program exporting pos1 lost its position export bits"); + Require(PositionExportLayout(nullptr, 0, VertexCode, 0x0144000fu) == 0x0040000fu, "bits outside the position export layout reached the vertex info"); + Require(PositionExportLayout(nullptr, 0, PositionOnly, 0x0040000fu) == 0u, "a program exporting only pos0 kept the position export bits"); + AgcDriver::DriverDetail::ShaderSnapshot snapshot{0x10000u, 0u, 1u, {PositionOnly.begin(), PositionOnly.end()}, {}}; + for (int use = 0; use < 2; ++use) Require(PositionExportLayout(&snapshot, 0, snapshot.code, 0x0040000fu) == 0u, "a cached pos0-only program kept the position export bits"); + snapshot.code.assign(VertexCode.begin(), VertexCode.end()); + Require(PositionExportLayout(&snapshot, 2, std::span(snapshot.code).subspan(2), 0x0040000fu) == 0x0040000fu, "a program at an offset was answered from the cache of its snapshot"); + constexpr std::array PaddedTail{0u}; + Require(PositionExportLayout(nullptr, 0, PaddedTail, 0x0040000fu) == 0x0040000fu, "an undecodable registered tail lost the guest's position export bits"); +} + +} + +int main(int argc, char** argv) { + try { + const bool noKey = argc > 1 && std::string(argv[1]) == "no-key"; + if (noKey) Require(std::getenv("APS5_NO_DRAW_KEY") != nullptr, "the no-key run must start with APS5_NO_DRAW_KEY set"); + PositionExportLayoutTests(); + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + ColorMemory = AllocateWatched(2 * Block); + Register(VertexShader, VertexCode.data(), sizeof(VertexCode), 2); + Register(PixelShader, PixelCode.data(), sizeof(PixelCode), 1); + for (const auto& step : Steps) { + std::array vertices{}; + for (std::size_t i = 0; i < vertices.size(); ++i) { + vertices[i] = {{PositionX[i], PositionY[i], 0.5f, 1.0f}, {step.component0[i], step.component1[i], step.component2[i], 0.0f}}; + } + std::fill_n(ColorMemory, ColorBytes, std::byte{0}); + EndOfPipe = 0; + auto words = Frame(step, vertices); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, std::string("DCB submission failed for ") + step.name); + AgcDriverWaitIdle_nid_postfix(); + Require(EndOfPipe == 1u, std::string("the end-of-pipe label was not written for ") + step.name); + std::vector colors(ColorBytes); + AgcDriver::GuestMemory::Read(static_cast(reinterpret_cast(ColorMemory)), colors); + Check(step, colors); + } + AgcDriverShutdown_nid_postfix(); + std::puts(noKey ? "vertex clip and cull distance draws passed without the draw key" : "vertex clip and cull distance draws passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VertexFetch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VertexFetch.cpp index 4baa2d05b3..14df999eb0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/VertexFetch.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VertexFetch.cpp @@ -53,8 +53,9 @@ void Run(AgcDriver::VulkanDevice& device) { std::uint32_t format; bool moved; std::uint32_t offset; + bool specializedPixel = false; }; - constexpr std::array cases{{{16u, 77u, false, 1u}, {32u, 77u, true, 0u}, {8u, 71u, false, 1u}, {16u, 71u, true, 0u}}}; + constexpr std::array cases{{{16u, 77u, false, 1u}, {32u, 77u, true, 0u}, {8u, 71u, false, 1u}, {16u, 71u, true, 0u}, {16u, 77u, false, 1u, true}}}; for (const auto& value : cases) { auto& storage = value.moved ? movedVertices : vertices; storage.fill(std::byte{0}); @@ -85,9 +86,11 @@ void Run(AgcDriver::VulkanDevice& device) { pixelInfo.wave32 = true; pixelInfo.targetOutputMode[0] = 9u; pixelInfo.targetExportMapping.fill(0xe4u); - ShaderRecompiler::RecompileRequest pixelRequest{{ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, {32u, 0u, {}, std::nullopt, pixelInfo, std::nullopt, {}}, device.Target(), {0u, 0u, pushBytes, 128u - pushBytes}}; + ShaderRecompiler::RecompileRequest pixelRequest{{ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, {32u, 0u, {}, std::nullopt, pixelInfo, std::nullopt, {}}, device.Target(), PixelPushLayout(pushBytes, device.Target())}; + if (value.specializedPixel) pixelRequest.target.registrationWordLimit = 1u; const auto pixel = ShaderRecompiler::Recompile(pixelRequest); - const std::array shaders{{{ShaderStage::Vertex, &vertex, 0u}, {ShaderStage::Fragment, &pixel, pushBytes}}}; + if (device.Target().registrationModules) Require(vertex.registrationModule && pixel.registrationModule != value.specializedPixel, "registration modules: the word limit did not choose the module mode"); + 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}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VertexUserDataTail.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VertexUserDataTail.cpp new file mode 100644 index 0000000000..df9e6f2c6d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VertexUserDataTail.cpp @@ -0,0 +1,167 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +constexpr std::byte Kept{0x40}; +using Image = std::array; +alignas(256) Image Pixels{}; +alignas(256) std::array Prolog{}; +alignas(256) std::array Reference{}; +alignas(256) std::array Continuation{0xbf840003u, 0x7e08020cu, 0xf80008cfu, 0x04030201u, 0xbf810000u}; +alignas(256) const std::array OtherContinuation{0xbf840003u, 0x7e0802f4u, 0xf80008cfu, 0x04030201u, 0xbf810000u}; +alignas(256) constexpr std::array PixelCode{0x7e000200u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u}; +alignas(256) constexpr std::array VertexColorPixelCode{0xc8080000u, 0xc8090001u, 0xf800180fu, 0x02020202u, 0xbf810000u}; + +void Append(std::vector& commands, std::uint32_t opcode, std::initializer_list payload) { + commands.push_back(0xc0000000u | (static_cast(payload.size() - 1) << 16u) | (opcode << 8u)); + commands.insert(commands.end(), payload); +} + +Image Draw(const std::uint32_t* code, const std::uint32_t* continuation, std::uint32_t distanceLayout = 0, bool vertexColor = false) { + Pixels.fill(Kept); + const auto vertex = reinterpret_cast(code); + const auto pixel = reinterpret_cast(vertexColor ? VertexColorPixelCode.data() : PixelCode.data()); + const auto next = reinterpret_cast(continuation); + const std::vector> context{ + {0x2d5, 0x2000}, + {0x1b6, vertexColor ? 1u : 0u}, {0x1b3, 2}, {0x1b4, 2}, {0x191, 0}, + {0x207, distanceLayout}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u}, + {0x3b8, 0x9000000}, {0x1e0, 0}, + {0xc, 0}, {0xd, 0x40040}, + {0x81, 0x80000000}, {0x82, 0x40040}, + {0x90, 0x80000000}, {0x91, 0x40040}, + {0x94, 0x80000000}, {0x95, 0x40040}, + {0x10f, std::bit_cast(32.0f)}, {0x110, std::bit_cast(32.0f)}, + {0x111, std::bit_cast(-2.0f)}, {0x112, std::bit_cast(2.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, + {0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0}, + }; + const std::vector> shader{ + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 2u << 1u}, + {0x8c, static_cast(next)}, {0x8d, static_cast(next >> 32u)}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 1u << 1u}, {0x00c, std::bit_cast(1.0f)}, + }; + std::vector commands; + Append(commands, 0x79, {0x242, 4}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); + const auto color = reinterpret_cast(Pixels.data()); + Append(commands, 0x69, {0x318, static_cast(color >> 8u)}); + Append(commands, 0x69, {0x390, static_cast(color >> 40u)}); + Append(commands, 0x2d, {3, 2}); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "vertex tail draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(color, Pixels.size()); + return Pixels; +} + +Image Check(std::span prefix, std::span continuation, std::uint32_t distanceLayout = 0, std::size_t continuationWords = 5, bool vertexColor = false) { + Prolog.fill(0xbf9f0000u); + Reference.fill(0xbf9f0000u); + std::copy(prefix.begin(), prefix.end(), Prolog.begin()); + Prolog[prefix.size()] = 0xbe802008u; + std::copy(prefix.begin(), prefix.end(), Reference.begin()); + std::copy_n(continuation.begin(), continuationWords, Reference.begin() + prefix.size()); + const auto expected = Draw(Reference.data(), continuation.data(), distanceLayout, vertexColor); + for (unsigned repeat = 0; repeat < 2u; ++repeat) { + const auto actual = Draw(Prolog.data(), continuation.data(), distanceLayout, vertexColor); + Require(actual == expected, "linked vertex tail pixels differ from the monolithic shader"); + } + return expected; +} + +void CheckBranchColors(const Image& written, const Image& skipped, const char* state) { + std::size_t covered = 0; + for (std::size_t pixel = 0; pixel < written.size(); pixel += 4) { + const bool untouched = written[pixel] == Kept; + for (std::size_t component = 0; component < 4; ++component) { + Require(written[pixel + component] == (untouched ? Kept : std::byte{0xff}) && + skipped[pixel + component] == (untouched ? Kept : std::byte{0}), std::string(state) + " did not select the expected vertex color"); + } + if (!untouched) ++covered; + } + Require(covered != 0 && covered != written.size() / 4, std::string(state) + " reference triangle did not cross the render target"); +} + +void ControlStateDraws(std::vector prefix) { + alignas(256) const std::array sccContinuation{0xbf840001u, 0x7e0c02f2u, 0xf800020fu, 0x06060606u, 0xf80008cfu, 0x04030201u, 0xbf810000u}; + prefix.insert(prefix.end() - 1, 0x7e0c0280u); + const auto trueScc = Check(prefix, sccContinuation, 0, sccContinuation.size(), true); + prefix.back() = 0xbf070c0cu; + const auto falseScc = Check(prefix, sccContinuation, 0, sccContinuation.size(), true); + CheckBranchColors(trueScc, falseScc, "SCC"); + alignas(256) const std::array execContinuation{0x7e0c02f2u, 0xbefe040eu, 0xf800020fu, 0x06060606u, 0xf80008cfu, 0x04030201u, 0xbf810000u}; + prefix.back() = 0xbf060c0cu; + prefix.push_back(0xbe8e047eu); + const auto activeExec = Check(prefix, execContinuation, 0, execContinuation.size(), true); + prefix.push_back(0xbefe0480u); + const auto emptyExec = Check(prefix, execContinuation, 0, execContinuation.size(), true); + CheckBranchColors(activeExec, emptyExec, "EXEC"); +} + +void VertexTailDraws() { + std::vector prefix{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060280u, 0x7e0802f2u, 0xbe8c03ffu, 0x3f800000u, 0xbf060c0cu, + }; + const auto original = Check(prefix, Continuation); + const auto kept = std::count(original.begin(), original.end(), Kept); + Require(kept != 0 && kept != static_cast(original.size()), "reference triangle did not cross the render target"); + const auto changedAddress = Check(prefix, OtherContinuation); + Require(changedAddress != original, "changing the continuation did not change the reference pixels"); + Continuation[1] = OtherContinuation[1]; + Require(Check(prefix, Continuation) == changedAddress, "rewritten continuation kept the old pixels"); + Continuation[1] = 0x7e08020cu; + Require(Check(prefix, Continuation) == original, "restored continuation did not restore the pixels"); + alignas(256) const std::array clipContinuation{0xbf840005u, 0x7e08020cu, 0xf80000cfu, 0x04030201u, 0xf80008dfu, 0x01010101u, 0xbf810000u}; + const auto clipped = Check(prefix, clipContinuation, 0x00400001u, clipContinuation.size()); + Require(clipped != original && std::count(clipped.begin(), clipped.end(), Kept) > kept, "a clip distance exported by the continuation did not clip the triangle"); + const auto culled = Check(prefix, clipContinuation, 0x00400100u, clipContinuation.size()); + Require(culled == original, "a mixed-sign cull distance discarded the triangle"); + ControlStateDraws(prefix); +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + VertexTailDraws(); + AgcDriverShutdown_nid_postfix(); + std::puts("vertex user-data tail tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { AgcDriverShutdown_nid_postfix(); } catch (const std::exception&) {} + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ViewPastLastMip.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ViewPastLastMip.cpp index c2b535c5d3..ec229885ef 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ViewPastLastMip.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ViewPastLastMip.cpp @@ -95,7 +95,7 @@ int main() { std::fill(texels, texels + SurfaceBytes / 4u, Untouched); { GuestAllocations::Mutation mutation; - mutation.Add(texels, SurfaceBytes, true, true); + mutation.Add(texels, SurfaceBytes, true, true, true); } Store(*device, texels, 3u, StorePastCode); Check(texels, false, "store through level 3"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Vop3AddSubClamp.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Vop3AddSubClamp.cpp new file mode 100644 index 0000000000..b297825ecd --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Vop3AddSubClamp.cpp @@ -0,0 +1,167 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 16; +constexpr std::uint32_t Checked = 8; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084u, 0x34040086u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e140280u, 0x7e160280u, 0x7e180280u, + 0x7e1a0280u, 0x7e1c0280u, 0x7e1e0280u, 0x7e200280u, 0x7e220280u, 0x7e240280u, 0x7e260280u, 0x7e280280u, + 0x7e2a0280u, 0x7e2c0280u, 0x7e2e0280u, 0x7e300280u, 0x7e320280u, 0xd525800au, 0x00020b04u, 0xd526800bu, + 0x00020b04u, 0xd527800cu, 0x00020b04u, 0xd703800du, 0x00020b04u, 0xd704800eu, 0x00020b04u, 0xd70d800fu, + 0x00020b04u, 0xd70e8010u, 0x00020b04u, 0x4a220b04u, 0xe0781000u, 0x80010a02u, 0xe0781010u, 0x80010e02u, + 0xe0781020u, 0x80011202u, 0xe0781030u, 0x80011602u, 0xbf8c3f70u, 0xbf810000u, +}; + +constexpr std::uint32_t Rows[32][4] = { + {0xffffffffu, 0x00000001u, 0x00000000u, 0x00000000u}, + {0xfffffffeu, 0x00000001u, 0x00000000u, 0x00000000u}, + {0x80000000u, 0x80000000u, 0x00000000u, 0x00000000u}, + {0x7fffffffu, 0x80000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u}, + {0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000005u, 0x00000005u, 0x00000000u, 0x00000000u}, + {0x12345678u, 0xfedcba98u, 0x00000000u, 0x00000000u}, + {0xfedcba98u, 0x12345678u, 0x00000000u, 0x00000000u}, + {0x00007fffu, 0x00000001u, 0x00000000u, 0x00000000u}, + {0x00008000u, 0x0000ffffu, 0x00000000u, 0x00000000u}, + {0x00008000u, 0x00000001u, 0x00000000u, 0x00000000u}, + {0x0000ffffu, 0x00000001u, 0x00000000u, 0x00000000u}, + {0x0000ffffu, 0x0000ffffu, 0x00000000u, 0x00000000u}, + {0x00007fffu, 0x00007fffu, 0x00000000u, 0x00000000u}, + {0x00008000u, 0x00008000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00008000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00007fffu, 0x00000000u, 0x00000000u}, + {0x1234ffffu, 0xabcd0001u, 0x00000000u, 0x00000000u}, + {0xabcd7fffu, 0x12340001u, 0x00000000u, 0x00000000u}, + {0xffff8000u, 0x00017fffu, 0x00000000u, 0x00000000u}, + {0x00010000u, 0x00000001u, 0x00000000u, 0x00000000u}, + {0x00000001u, 0x00010000u, 0x00000000u, 0x00000000u}, + {0x0000fffeu, 0x0000ffffu, 0x00000000u, 0x00000000u}, + {0x00007ffeu, 0x0000fffeu, 0x00000000u, 0x00000000u}, + {0x00008001u, 0x00000002u, 0x00000000u, 0x00000000u}, + {0x00000002u, 0x00008001u, 0x00000000u, 0x00000000u}, + {0xffffffffu, 0xffffffffu, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0xffffffffu, 0x00000000u, 0x00000000u}, + {0x7fff8000u, 0x8000ffffu, 0x00000000u, 0x00000000u}, + {0x40000000u, 0xc0000000u, 0x00000000u, 0x00000000u} +}; +constexpr std::uint32_t Expected[32][Checked] = { + {0xffffffffu, 0xfffffffeu, 0x00000000u, 0x0000ffffu, 0x0000fffeu, 0x00000000u, 0x0000fffeu, 0x00000000u}, + {0xffffffffu, 0xfffffffdu, 0x00000000u, 0x0000ffffu, 0x0000fffdu, 0x0000ffffu, 0x0000fffdu, 0xffffffffu}, + {0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0xffffffffu, 0x00000000u, 0x00000001u, 0x0000ffffu, 0x0000ffffu, 0x0000ffffu, 0x0000ffffu, 0xffffffffu}, + {0x00000001u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000001u, 0x0000ffffu, 0x00000001u}, + {0x00000001u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u}, + {0x0000000au, 0x00000000u, 0x00000000u, 0x0000000au, 0x00000000u, 0x0000000au, 0x00000000u, 0x0000000au}, + {0xffffffffu, 0x00000000u, 0xeca86420u, 0x0000ffffu, 0x00000000u, 0x00001110u, 0x00007fffu, 0x11111110u}, + {0xffffffffu, 0xeca86420u, 0x00000000u, 0x0000ffffu, 0x00006420u, 0x00001110u, 0x00008000u, 0x11111110u}, + {0x00008000u, 0x00007ffeu, 0x00000000u, 0x00008000u, 0x00007ffeu, 0x00007fffu, 0x00007ffeu, 0x00008000u}, + {0x00017fffu, 0x00000000u, 0x00007fffu, 0x0000ffffu, 0x00000000u, 0x00008000u, 0x00008001u, 0x00017fffu}, + {0x00008001u, 0x00007fffu, 0x00000000u, 0x00008001u, 0x00007fffu, 0x00008001u, 0x00008000u, 0x00008001u}, + {0x00010000u, 0x0000fffeu, 0x00000000u, 0x0000ffffu, 0x0000fffeu, 0x00000000u, 0x0000fffeu, 0x00010000u}, + {0x0001fffeu, 0x00000000u, 0x00000000u, 0x0000ffffu, 0x00000000u, 0x0000fffeu, 0x00000000u, 0x0001fffeu}, + {0x0000fffeu, 0x00000000u, 0x00000000u, 0x0000fffeu, 0x00000000u, 0x00007fffu, 0x00000000u, 0x0000fffeu}, + {0x00010000u, 0x00000000u, 0x00000000u, 0x0000ffffu, 0x00000000u, 0x00008000u, 0x00000000u, 0x00010000u}, + {0x00008000u, 0x00000000u, 0x00008000u, 0x00008000u, 0x00000000u, 0x00008000u, 0x00007fffu, 0x00008000u}, + {0x00007fffu, 0x00000000u, 0x00007fffu, 0x00007fffu, 0x00000000u, 0x00007fffu, 0x00008001u, 0x00007fffu}, + {0xbe020000u, 0x00000000u, 0x99980002u, 0x0000ffffu, 0x0000fffeu, 0x00000000u, 0x0000fffeu, 0xbe020000u}, + {0xbe018000u, 0x99997ffeu, 0x00000000u, 0x00008000u, 0x00007ffeu, 0x00007fffu, 0x00007ffeu, 0xbe018000u}, + {0xffffffffu, 0xfffe0001u, 0x00000000u, 0x0000ffffu, 0x00000001u, 0x0000ffffu, 0x00008000u, 0x0000ffffu}, + {0x00010001u, 0x0000ffffu, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x0000ffffu, 0x00010001u}, + {0x00010001u, 0x00000000u, 0x0000ffffu, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00010001u}, + {0x0001fffdu, 0x00000000u, 0x00000001u, 0x0000ffffu, 0x00000000u, 0x0000fffdu, 0x0000ffffu, 0x0001fffdu}, + {0x00017ffcu, 0x00000000u, 0x00008000u, 0x0000ffffu, 0x00000000u, 0x00007ffcu, 0x00007fffu, 0x00017ffcu}, + {0x00008003u, 0x00007fffu, 0x00000000u, 0x00008003u, 0x00007fffu, 0x00008003u, 0x00008000u, 0x00008003u}, + {0x00008003u, 0x00000000u, 0x00007fffu, 0x00008003u, 0x00000000u, 0x00008003u, 0x00007fffu, 0x00008003u}, + {0xffffffffu, 0x00000000u, 0x00000000u, 0x0000ffffu, 0x00000000u, 0x0000fffeu, 0x00000000u, 0xfffffffeu}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0xffffffffu, 0x00000000u, 0xffffffffu, 0x0000ffffu, 0x00000000u, 0x0000ffffu, 0x00000001u, 0xffffffffu}, + {0xffffffffu, 0x00000000u, 0x00017fffu, 0x0000ffffu, 0x00000000u, 0x00008000u, 0x00008001u, 0x00007fffu}, + {0xffffffffu, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u} +}; +constexpr const char* Names[Checked] = { + "v_add_nc_u32 v10, v4, v5 clamp", + "v_sub_nc_u32 v11, v4, v5 clamp", + "v_subrev_nc_u32 v12, v4, v5 clamp", + "v_add_nc_u16 v13, v4, v5 clamp", + "v_sub_nc_u16 v14, v4, v5 clamp", + "v_add_nc_i16 v15, v4, v5 clamp", + "v_sub_nc_i16 v16, v4, v5 clamp", + "v_add_nc_u32 v17, v4, v5" +}; + +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) { + 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::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(); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t i = 0; i < Checked; ++i) { + const std::uint32_t actual = Output[tid * Results + i]; + Require(actual == Expected[tid][i], std::string("VOP3 add/sub clamp: lane ") + std::to_string(tid) + " " + Names[i] + " is " + Hex(actual) + ", expected " + Hex(Expected[tid][i])); + } + } + std::puts("VOP3 add/sub clamp 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/Vop3CubeClamp.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Vop3CubeClamp.cpp index d655f5f83d..a26753bd63 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Vop3CubeClamp.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Vop3CubeClamp.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -101,6 +102,40 @@ constexpr std::uint32_t Expected[32][16] = { {0x40000000u, 0x00000000u, 0x7f800001u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x7f800001u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x40000000u, 0x00000000u, 0xff812345u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x7f812345u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u} }; +constexpr std::uint32_t ExpectedIeee[32][16] = { + {0x40800000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x40800000u, 0x3f800000u, 0x3f800000u, 0x40000000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x40800000u, 0xbf800000u, 0xbf800000u, 0xc0000000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u}, + {0x00000000u, 0xbf000000u, 0xbf000000u, 0x7fc00001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3f000000u, 0x3f000000u, 0x7fc00001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x7fc12345u, 0xbf000000u, 0x40800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x3f800000u, 0xffc12345u, 0x3f000000u, 0x40800000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0xffc00000u, 0xffc00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x7fc00000u, 0x7fc00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0xffc00001u, 0x7fc00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x7fc00001u, 0xffc00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x3f800000u, 0x00000007u, 0x00000007u, 0xff800000u, 0x3f800000u, 0x00000007u, 0x00000007u, 0x00000000u, 0x00000000u, 0x00000007u, 0x80000007u, 0x7f800000u, 0x00000000u, 0x00000007u, 0x00000007u, 0x3f800000u}, + {0x00000000u, 0x7f800000u, 0x80000007u, 0x7fc00000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xff800000u, 0x00000007u, 0x7fc00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x3f800000u, 0x80000007u, 0x80000000u, 0xbf800000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000007u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000007u, 0x00000000u, 0x3f800000u}, + {0x40000000u, 0x00000007u, 0x7fc00001u, 0x00000000u, 0x3f800000u, 0x00000007u, 0x00000000u, 0x00000000u, 0x40000000u, 0x00000007u, 0x7fc00001u, 0x00000000u, 0x3f800000u, 0x00000007u, 0x00000000u, 0x00000000u}, + {0x40000000u, 0x00000007u, 0x80000007u, 0x7f800000u, 0x3f800000u, 0x00000007u, 0x00000000u, 0x3f800000u, 0x40000000u, 0x00000007u, 0x80000007u, 0x7f800000u, 0x3f800000u, 0x00000007u, 0x00000007u, 0x3f800000u}, + {0x40800000u, 0x00000007u, 0x80000007u, 0x00000000u, 0x3f800000u, 0x00000007u, 0x00000000u, 0x00000000u, 0x40800000u, 0x00000007u, 0x00000007u, 0x00000000u, 0x3f800000u, 0x00000007u, 0x00000000u, 0x00000000u}, + {0x40400000u, 0xff800000u, 0xc0000000u, 0xff800000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40000000u, 0x7f800000u, 0x40000000u, 0xff800000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x3f800000u}, + {0x40a00000u, 0xc0000000u, 0x40200000u, 0xc0a00000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x40a00000u, 0x40000000u, 0xc0200000u, 0x40a00000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x7f800000u, 0x7fc00000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x80000000u, 0xff800000u, 0x7fc00000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x00000000u}, + {0x40800000u, 0xc0200000u, 0xff800000u, 0x7f800000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x40800000u, 0xc0200000u, 0x7f800000u, 0xff800000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x3f800000u}, + {0x40800000u, 0xbf000000u, 0x3f000000u, 0x3f800000u, 0x3f800000u, 0x00000000u, 0x3f000000u, 0x3f800000u, 0x40800000u, 0xbf000000u, 0xbf000000u, 0xbf800000u, 0x3f800000u, 0x00000000u, 0x3f000000u, 0x3f800000u}, + {0x00000000u, 0x40200000u, 0x7f800000u, 0x7fc00000u, 0x00000000u, 0x3f800000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0xc0200000u, 0xff800000u, 0x7fc00000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x00000000u}, + {0x40400000u, 0x00000000u, 0x80000000u, 0xff800000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0xff800000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x3f800000u}, + {0x40400000u, 0x00000000u, 0x3f000000u, 0xff800000u, 0x3f800000u, 0x00000000u, 0x3f000000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x3f000000u, 0xff800000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x3f800000u}, + {0x40a00000u, 0x7f800000u, 0x80000000u, 0xff800000u, 0x3f800000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x40a00000u, 0x7f800000u, 0x00000000u, 0x7f800000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x40000000u, 0xff800000u, 0x00000000u, 0x7f800000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x40000000u, 0x7f800000u, 0x80000000u, 0x7f800000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x3f800000u}, + {0x40000000u, 0xff800000u, 0xbf000000u, 0x7f800000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x40000000u, 0x7f800000u, 0xbf000000u, 0x7f800000u, 0x3f800000u, 0x00000000u, 0x3f000000u, 0x3f800000u}, + {0x40a00000u, 0x7f800000u, 0x7f800000u, 0xff800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x00000000u, 0x40a00000u, 0x7f800000u, 0xff800000u, 0x7f800000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x00000000u}, + {0x40400000u, 0xff800000u, 0x3f000000u, 0xff800000u, 0x3f800000u, 0x00000000u, 0x3f000000u, 0x00000000u, 0x40000000u, 0x7f800000u, 0x3f000000u, 0xff800000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x3f800000u}, + {0x3f800000u, 0xbf000000u, 0x3f000000u, 0xff800000u, 0x3f800000u, 0x00000000u, 0x3f000000u, 0x00000000u, 0x00000000u, 0xbf000000u, 0xbf000000u, 0x7f800000u, 0x00000000u, 0x3f000000u, 0x3f000000u, 0x3f800000u}, + {0x40800000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x40800000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x40000000u, 0x00000000u, 0x7fc00000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x7fc00000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x40000000u, 0x00000000u, 0xffc00000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x7fc00000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x40000000u, 0x00000000u, 0x7fc00001u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x7fc00001u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x40000000u, 0x00000000u, 0xffc12345u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x7fc12345u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u} +}; constexpr const char* Names[16] = { "v_cubeid_f32 v10, v4, v5, v6", "v_cubesc_f32 v11, v4, v5, v6", @@ -120,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); @@ -139,7 +300,7 @@ void Expect(std::uint32_t tid, std::uint32_t actual, std::uint32_t expected, con Require(actual == expected, std::string("vop3 cube clamp: lane ") + std::to_string(tid) + " " + name + " is " + Hex(actual) + ", expected " + Hex(expected)); } -ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::span code) { +ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::span code, const std::optional& floatMode = std::nullopt) { std::vector userData(8, 0u); const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); @@ -154,37 +315,23 @@ ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std:: {0, 0, 0, 128} }; request.useCache = false; + request.context.floatMode = floatMode; return ShaderRecompiler::Recompile(request); } -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, 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); + 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() { +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* in = &Input[tid * Inputs]; const std::uint32_t* out = &Output[tid * Results]; for (std::uint32_t i = 0; i < 16; ++i) { - Expect(tid, out[i], Expected[tid][i], Names[i]); + Expect(tid, out[i], expected[tid][i], (std::string(mode) + " " + names[i]).c_str()); } } } @@ -195,9 +342,22 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - Run(*device); - Check(); - 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/Vop3PackLdexp.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Vop3PackLdexp.cpp index 181109a980..e20d2a8608 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Vop3PackLdexp.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Vop3PackLdexp.cpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include #include #include @@ -102,8 +104,123 @@ constexpr std::uint32_t Expected[32][14] = { }; constexpr const char* Names[14] = {"cvt_pknorm_i16_f320", "cvt_pknorm_i16_f321", "cvt_pknorm_u16_f322", "cvt_pknorm_u16_f323", "ldexp_f324", "ldexp_f325", "ldexp_f326", "lerp_u87", "pack_b32_f168", "pack_b32_f169", "pack_b32_f1610", "pack_b32_f1611", "mul_lo_i32_0", "mul_lo_i32_1"}; -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 PackCode{ + 0x34020084u, 0x34060086u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0xd711000au, 0x02020b04u, 0xd711180bu, + 0x02020b04u, 0xd711020cu, 0x22020b04u, 0xd711090du, 0x42020905u, 0xe0781000u, 0x80010a03u, 0xe0781010u, + 0x80010e03u, 0xe0781020u, 0x80011203u, 0xe0781030u, 0x80011603u, 0xbf810000u, +}; + +constexpr std::uint32_t PackRows[32][4] = { + {0xfd237c01u, 0xfc017dffu, 0x00000000u, 0x00000000u}, + {0x7e00fd23u, 0x00007c00u, 0x00000000u, 0x00000000u}, + {0x7c007dffu, 0xfd230001u, 0x00000000u, 0x00000000u}, + {0x0000fc01u, 0xfc00fd23u, 0x00000000u, 0x00000000u}, + {0x83ff7e00u, 0xc1007fffu, 0x00000000u, 0x00000000u}, + {0x7c01fe55u, 0x7fff8000u, 0x00000000u, 0x00000000u}, + {0xfc017fffu, 0x83ff7c01u, 0x00000000u, 0x00000000u}, + {0x7fff7c00u, 0x7e00fe55u, 0x00000000u, 0x00000000u}, + {0x3c00fc00u, 0x80000000u, 0x00000000u, 0x00000000u}, + {0x00013c00u, 0x7dffc100u, 0x00000000u, 0x00000000u}, + {0xc1000000u, 0x3c007e00u, 0x00000000u, 0x00000000u}, + {0x7dff8000u, 0x7c013c00u, 0x00000000u, 0x00000000u}, + {0xfe550001u, 0x7c007bffu, 0x00000000u, 0x00000000u}, + {0xfc0083ffu, 0x7bfffc01u, 0x00000000u, 0x00000000u}, + {0x80007bffu, 0xfe55fc00u, 0x00000000u, 0x00000000u}, + {0x7bffc100u, 0x000183ffu, 0x00000000u, 0x00000000u}, + {0xfd237dffu, 0xfc017c01u, 0x00000000u, 0x00000000u}, + {0x7e007c00u, 0x0000fd23u, 0x00000000u, 0x00000000u}, + {0x7c000001u, 0xfd237dffu, 0x00000000u, 0x00000000u}, + {0x0000fd23u, 0xfc00fc01u, 0x00000000u, 0x00000000u}, + {0x83ff7fffu, 0xc1007e00u, 0x00000000u, 0x00000000u}, + {0x7c018000u, 0x7ffffe55u, 0x00000000u, 0x00000000u}, + {0xfc017c01u, 0x83ff7fffu, 0x00000000u, 0x00000000u}, + {0x7ffffe55u, 0x7e007c00u, 0x00000000u, 0x00000000u}, + {0x3c000000u, 0x8000fc00u, 0x00000000u, 0x00000000u}, + {0x0001c100u, 0x7dff3c00u, 0x00000000u, 0x00000000u}, + {0xc1007e00u, 0x3c000000u, 0x00000000u, 0x00000000u}, + {0x7dff3c00u, 0x7c018000u, 0x00000000u, 0x00000000u}, + {0xfe557bffu, 0x7c000001u, 0x00000000u, 0x00000000u}, + {0xfc00fc01u, 0x7bff83ffu, 0x00000000u, 0x00000000u}, + {0x8000fc00u, 0xfe557bffu, 0x00000000u, 0x00000000u}, + {0x7bff83ffu, 0x0001c100u, 0x00000000u, 0x00000000u} +}; +constexpr std::uint32_t PackExpected[32][4] = { + {0x7dff7c01u, 0xfc01fd23u, 0x7dfffc01u, 0xfc017c01u}, + {0x7c00fd23u, 0x00007e00u, 0x7c007d23u, 0x7d230000u}, + {0x00017dffu, 0xfd237c00u, 0x0001fdffu, 0xfdff7d23u}, + {0xfd23fc01u, 0xfc000000u, 0x7d237c01u, 0x7c017c00u}, + {0x7fff7e00u, 0xc10083ffu, 0x7ffffe00u, 0xfe004100u}, + {0x8000fe55u, 0x7fff7c01u, 0x00007e55u, 0x7e557fffu}, + {0x7c017fffu, 0x83fffc01u, 0x7c01ffffu, 0xffff03ffu}, + {0xfe557c00u, 0x7e007fffu, 0x7e55fc00u, 0xfc007e00u}, + {0x0000fc00u, 0x80003c00u, 0x00007c00u, 0x7c000000u}, + {0xc1003c00u, 0x7dff0001u, 0x4100bc00u, 0xbc007dffu}, + {0x7e000000u, 0x3c00c100u, 0x7e008000u, 0x80003c00u}, + {0x3c008000u, 0x7c017dffu, 0x3c000000u, 0x00007c01u}, + {0x7bff0001u, 0x7c00fe55u, 0x7bff8001u, 0x80017c00u}, + {0xfc0183ffu, 0x7bfffc00u, 0x7c0103ffu, 0x03ff7bffu}, + {0xfc007bffu, 0xfe558000u, 0x7c00fbffu, 0xfbff7e55u}, + {0x83ffc100u, 0x00017bffu, 0x03ff4100u, 0x41000001u}, + {0x7c017dffu, 0xfc01fd23u, 0x7c01fdffu, 0xfdff7c01u}, + {0xfd237c00u, 0x00007e00u, 0x7d23fc00u, 0xfc000000u}, + {0x7dff0001u, 0xfd237c00u, 0x7dff8001u, 0x80017d23u}, + {0xfc01fd23u, 0xfc000000u, 0x7c017d23u, 0x7d237c00u}, + {0x7e007fffu, 0xc10083ffu, 0x7e00ffffu, 0xffff4100u}, + {0xfe558000u, 0x7fff7c01u, 0x7e550000u, 0x00007fffu}, + {0x7fff7c01u, 0x83fffc01u, 0x7ffffc01u, 0xfc0103ffu}, + {0x7c00fe55u, 0x7e007fffu, 0x7c007e55u, 0x7e557e00u}, + {0xfc000000u, 0x80003c00u, 0x7c008000u, 0x80000000u}, + {0x3c00c100u, 0x7dff0001u, 0x3c004100u, 0x41007dffu}, + {0x00007e00u, 0x3c00c100u, 0x0000fe00u, 0xfe003c00u}, + {0x80003c00u, 0x7c017dffu, 0x0000bc00u, 0xbc007c01u}, + {0x00017bffu, 0x7c00fe55u, 0x0001fbffu, 0xfbff7c00u}, + {0x83fffc01u, 0x7bfffc00u, 0x03ff7c01u, 0x7c017bffu}, + {0x7bfffc00u, 0xfe558000u, 0x7bff7c00u, 0x7c007e55u}, + {0xc10083ffu, 0x00017bffu, 0x410003ffu, 0x03ff0001u} +}; +constexpr std::uint32_t PackExpectedIeee[32][4] = { + {0x7fff7e01u, 0xfe01ff23u, 0x7ffffe01u, 0xfe017e01u}, + {0x7c00ff23u, 0x00007e00u, 0x7c007f23u, 0x7f230000u}, + {0x00017fffu, 0xff237c00u, 0x0001ffffu, 0xffff7f23u}, + {0xff23fe01u, 0xfc000000u, 0x7f237e01u, 0x7e017c00u}, + {0x7fff7e00u, 0xc10083ffu, 0x7ffffe00u, 0xfe004100u}, + {0x8000fe55u, 0x7fff7e01u, 0x00007e55u, 0x7e557fffu}, + {0x7e017fffu, 0x83fffe01u, 0x7e01ffffu, 0xffff03ffu}, + {0xfe557c00u, 0x7e007fffu, 0x7e55fc00u, 0xfc007e00u}, + {0x0000fc00u, 0x80003c00u, 0x00007c00u, 0x7c000000u}, + {0xc1003c00u, 0x7fff0001u, 0x4100bc00u, 0xbc007fffu}, + {0x7e000000u, 0x3c00c100u, 0x7e008000u, 0x80003c00u}, + {0x3c008000u, 0x7e017fffu, 0x3c000000u, 0x00007e01u}, + {0x7bff0001u, 0x7c00fe55u, 0x7bff8001u, 0x80017c00u}, + {0xfe0183ffu, 0x7bfffc00u, 0x7e0103ffu, 0x03ff7bffu}, + {0xfc007bffu, 0xfe558000u, 0x7c00fbffu, 0xfbff7e55u}, + {0x83ffc100u, 0x00017bffu, 0x03ff4100u, 0x41000001u}, + {0x7e017fffu, 0xfe01ff23u, 0x7e01ffffu, 0xffff7e01u}, + {0xff237c00u, 0x00007e00u, 0x7f23fc00u, 0xfc000000u}, + {0x7fff0001u, 0xff237c00u, 0x7fff8001u, 0x80017f23u}, + {0xfe01ff23u, 0xfc000000u, 0x7e017f23u, 0x7f237c00u}, + {0x7e007fffu, 0xc10083ffu, 0x7e00ffffu, 0xffff4100u}, + {0xfe558000u, 0x7fff7e01u, 0x7e550000u, 0x00007fffu}, + {0x7fff7e01u, 0x83fffe01u, 0x7ffffe01u, 0xfe0103ffu}, + {0x7c00fe55u, 0x7e007fffu, 0x7c007e55u, 0x7e557e00u}, + {0xfc000000u, 0x80003c00u, 0x7c008000u, 0x80000000u}, + {0x3c00c100u, 0x7fff0001u, 0x3c004100u, 0x41007fffu}, + {0x00007e00u, 0x3c00c100u, 0x0000fe00u, 0xfe003c00u}, + {0x80003c00u, 0x7e017fffu, 0x0000bc00u, 0xbc007e01u}, + {0x00017bffu, 0x7c00fe55u, 0x0001fbffu, 0xfbff7c00u}, + {0x83fffe01u, 0x7bfffc00u, 0x03ff7e01u, 0x7e017bffu}, + {0x7bfffc00u, 0xfe558000u, 0x7bff7c00u, 0x7c007e55u}, + {0xc10083ffu, 0x00017bffu, 0x410003ffu, 0x03ff0001u} +}; +constexpr const char* PackNames[4] = { + "v_pack_b32_f16 v10, v4, v5", + "v_pack_b32_f16 v11, v4, v5 op_sel:[1,1,0]", + "v_pack_b32_f16 v12, -v4, |v5|", + "v_pack_b32_f16 v13, |v5|, -v4 op_sel:[1,0,0]" +}; + +void Fill(const std::uint32_t (&rows)[32][4]) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) std::copy(std::begin(rows[tid]), std::end(rows[tid]), &Input[tid * Inputs]); } std::array BufferDescriptor(const void* data, std::uint32_t bytes) { @@ -121,15 +238,14 @@ void Expect(std::uint32_t tid, std::uint32_t actual, std::uint32_t expected, con Require(actual == expected, std::string("vop3 pack ldexp: 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, const std::uint32_t (&rows)[32][4], const std::optional& floatMode) { + Fill(rows); 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{ @@ -139,6 +255,7 @@ void Run(AgcDriver::VulkanDevice& device) { {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(); @@ -152,14 +269,27 @@ void Check() { } } +void CheckPack(const std::uint32_t (&expected)[32][4], 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 < 4; ++i) Expect(tid, out[i], expected[tid][i], (std::string(mode) + " " + PackNames[i]).c_str()); + } +} + } int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - Run(*device); + Run(*device, Code, Rows, std::nullopt); Check(); + Run(*device, PackCode, PackRows, std::nullopt); + CheckPack(PackExpected, "no float mode"); + Run(*device, PackCode, PackRows, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}); + CheckPack(PackExpected, "IEEE=0"); + Run(*device, PackCode, PackRows, ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}); + CheckPack(PackExpectedIeee, "IEEE=1"); std::puts("vop3 pack ldexp 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/VulkanTestDevice.hpp b/core/libs/prx/libSceAgcDriver/tests/execution/VulkanTestDevice.hpp index bab6cab86c..0b7c23d836 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/VulkanTestDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VulkanTestDevice.hpp @@ -27,4 +27,13 @@ inline std::unique_ptr OpenVulkanTestDevice() { } } +inline std::uint32_t PixelPushOffset(std::uint32_t vertexPush, const ShaderRecompiler::SpirvTarget& target) { + return AgcDriver::Graphics::StagePushOffset(vertexPush, ShaderRecompiler::ShaderStage::Fragment, target.fixedPushSlots); +} + +inline ShaderRecompiler::BindingLayout PixelPushLayout(std::uint32_t vertexPush, const ShaderRecompiler::SpirvTarget& target) { + const auto offset = PixelPushOffset(vertexPush, target); + return {0, 0, offset, AgcDriver::Graphics::PipelinePushSlotBytes - offset % AgcDriver::Graphics::PipelinePushSlotBytes}; +} + #endif diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Wave32ExecLaneBits.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Wave32ExecLaneBits.cpp new file mode 100644 index 0000000000..a785e4ef2c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Wave32ExecLaneBits.cpp @@ -0,0 +1,95 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x340c0082u, + 0x7d88008du, + 0x02020080u, + 0xbe8a3c6au, + 0x4a0202c0u, + 0x887e0a7eu, + 0x7d820094u, + 0xbe8b3c6au, + 0x4a0202a0u, + 0xbefe030bu, + 0x7d82008au, + 0x7e0e0281u, + 0x02040e80u, + 0x4a020302u, + 0xe0701000u, 0x80000106u, + 0xbf810000u, +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::uint32_t Expected(std::uint32_t lane) { + std::uint32_t value = lane < 13u ? lane + 64u : 0u; + if (lane > 20u) value += 32u; + if (lane > 10u) value += 1u; + return value; +} + +std::size_t Ballots(std::span words) { + std::size_t count = 0; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + if (static_cast(words[at] & 0xffffu) == spv::OpGroupNonUniformBallot) ++count; + } + return count; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Output.fill(0xdeadbeefu); + std::vector userData(4, 0u); + const auto buffer = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device->Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + const auto ballots = Ballots(result.spirv.Words()); + Require(ballots == 0u, "wave32 compares, saveexec and exec restores emitted " + std::to_string(ballots) + " subgroup ballots, expected none"); + device->Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device->WaitIdle(); + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + Require(Output[lane] == Expected(lane), "wave32 lane " + std::to_string(lane) + " is " + std::to_string(Output[lane]) + ", expected " + std::to_string(Expected(lane))); + } + std::puts("wave32 exec lane bits tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/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/WriteDataWrittenAhead.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/WriteDataWrittenAhead.cpp new file mode 100644 index 0000000000..529aad4163 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/WriteDataWrittenAhead.cpp @@ -0,0 +1,105 @@ +#include "VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Submit/include/Acb.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +alignas(256) std::array CounterCode{0xf4080200u, 0xfa000000u, 0xbf8cc07fu, 0x7e000281u, 0xe0c80000u, 0x80020000u, 0xbf810000u}; + +alignas(256) std::array First{}; +alignas(256) std::array Stale{}; +alignas(256) std::array Fresh{}; +alignas(256) std::array FirstDescriptor{}; +alignas(256) std::array SecondDescriptor{}; +alignas(256) volatile std::uint32_t Gate = 1; +alignas(256) volatile std::uint32_t Done = 0; + +std::uint32_t Low(const volatile void* pointer) { + return static_cast(reinterpret_cast(pointer)); +} + +std::uint32_t High(const volatile void* pointer) { + return static_cast(static_cast(reinterpret_cast(pointer)) >> 32u); +} + +std::array Descriptor(const void* pointer, std::uint32_t bytes) { + return {Low(pointer), High(pointer) & 0xffffu, bytes, 0x31016facu}; +} + +void SetShaderRegister(std::vector& words, std::uint32_t offset, std::uint32_t value) { + words.insert(words.end(), {0xc0017600u, offset, value}); +} + +void Program(std::vector& words, const void* code, std::initializer_list userData) { + const auto address = static_cast(reinterpret_cast(code)); + SetShaderRegister(words, 0x20c, static_cast(address >> 8u)); + SetShaderRegister(words, 0x20d, static_cast(address >> 40u) & 0xffu); + for (const std::uint32_t offset : {0x207u, 0x208u, 0x209u}) SetShaderRegister(words, offset, 1); + SetShaderRegister(words, 0x212, 0); + SetShaderRegister(words, 0x213, static_cast(userData.size()) << 1u); + std::uint32_t offset = 0x240; + for (const auto value : userData) SetShaderRegister(words, offset++, value); +} + +void Count(std::vector& words, const void* descriptor) { + Program(words, CounterCode.data(), {Low(descriptor), High(descriptor)}); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x8041u}); +} + +} + +int main() { + try { + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + device.reset(); + FirstDescriptor = Descriptor(First.data(), sizeof(First)); + SecondDescriptor = Descriptor(Stale.data(), sizeof(Stale)); + std::vector warmUp; + Count(warmUp, FirstDescriptor.data()); + Count(warmUp, SecondDescriptor.data()); + warmUp.insert(warmUp.end(), {0xc0033700u, 0x00100200u, Low(&Done), High(&Done), 2u}); + Packet first{warmUp.data(), static_cast(warmUp.size()), 0, {}}; + Require(sceAgcDriverSubmitAcb(0x20, &first) == 0, "warm-up submit failed"); + AgcDriverWaitIdle_nid_postfix(); + Require(Done == 2 && First[0] == 1 && Stale[0] == 1, "the warm-up dispatches did not run"); + First[0] = 0; + Stale[0] = 0; + const auto fresh = Descriptor(Fresh.data(), sizeof(Fresh)); + std::vector words; + words.insert(words.end(), {0xc0053c00u, 0x13u, Low(&Gate), High(&Gate), 1u, 0xffffffffu, 0x19u}); + Count(words, FirstDescriptor.data()); + words.insert(words.end(), {0xc0063700u, 0x00100200u, Low(SecondDescriptor.data()), High(SecondDescriptor.data()), fresh[0], fresh[1], fresh[2], fresh[3]}); + Count(words, SecondDescriptor.data()); + words.insert(words.end(), {0xc0033700u, 0x00100200u, Low(&Done), High(&Done), 1u}); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitAcb(0x20, &packet) == 0, "compute submit failed"); + AgcDriverWaitIdle_nid_postfix(); + Require(Done == 1, "the submission did not finish"); + Require(SecondDescriptor[0] == fresh[0] && SecondDescriptor[1] == fresh[1], "WRITE_DATA did not replace the descriptor"); + Require(First[0] == 1, "the first dispatch did not run"); + Require(Fresh[0] == 1 && Stale[0] == 0, "a dispatch whose descriptor a WRITE_DATA before it replaces used the old descriptor (stale " + std::to_string(Stale[0]) + ", fresh " + std::to_string(Fresh[0]) + ")"); + std::cout << "write data written ahead test passed\n"; + AgcDriverShutdown_nid_postfix(); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { + AgcDriverShutdown_nid_postfix(); + } catch (const std::exception& shutdownError) { + std::cerr << shutdownError.what() << '\n'; + } + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ZeroPositionVertex.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ZeroPositionVertex.cpp new file mode 100644 index 0000000000..db8010acb0 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ZeroPositionVertex.cpp @@ -0,0 +1,137 @@ +#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 Extent = 64u; +alignas(256) std::array pixels{}; +alignas(256) std::array vertices{}; +constexpr std::array vertexCode{0xf4080100u, 0xfa000000u, 0x4a0a0a02u, 0x4a0a0b08u, 0x4a0a0a03u, 0xe00c2000u, 0x80010005u, 0xbf8c3f70u, 0xf80008cfu, 0x03020100u, 0xbf810000u}; +constexpr std::array pixelCode{0x7e0002f2u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u}; + +using Position = std::array; + +struct Counts { + unsigned total = 0; + unsigned upper = 0; + std::array, 16> map{}; +}; + +Counts RunCase(AgcDriver::VulkanDevice& device, const char* name, std::uint32_t records, const std::vector& positions, std::size_t stored, VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP) { + vertices.fill(std::byte{0}); + constexpr std::uint32_t stride = 16u; + for (std::size_t index = 0; index < stored; ++index) std::memcpy(vertices.data() + index * stride, positions[index].data(), 16u); + 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}; + 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), records, 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); + 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 = topology; + 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(positions.size()), 0u, 1u, 0u, false}; + draw.firstInstance = 0u; + pixels.fill(std::byte{0}); + device.Draw(state, draw, shaders); + device.WaitIdle(); + Counts counts; + 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; + ++counts.total; + const float ndcX = (static_cast(x) + 0.5f) / Extent * 2.0f - 1.0f; + const float ndcY = 1.0f - (static_cast(y) + 0.5f) / Extent * 2.0f; + if (ndcX + ndcY > -1.0f) ++counts.upper; + ++counts.map[y / 4u][x / 4u]; + } + } + std::cout << "case " << name << ": records=" << records << " total=" << counts.total << " outsideFirstTriangle(x+y>-1)=" << counts.upper << "\n"; + for (const auto& row : counts.map) { + std::string line; + for (const auto count : row) line += count == 0u ? '.' : count == 16u ? '#' : '+'; + std::cout << " " << line << "\n"; + } + return counts; +} + +void Run(AgcDriver::VulkanDevice& device) { + std::cout << "device: " << device.DeviceName() << "\n"; + const Position v0{-1.0f, -1.0f, 0.0f, 1.0f}; + const Position v1{0.0f, -1.0f, 0.0f, 1.0f}; + const Position v2{-1.0f, 0.0f, 0.0f, 1.0f}; + const auto make = [&](const Position& last) { return std::vector{v0, v1, v2, last}; }; + const auto a = RunCase(device, "A out-of-range vertex 3", 3u, make({0.0f, 0.0f, 0.0f, 0.0f}), 3u); + const auto b = RunCase(device, "B in-range (0,0,0,0)", 4u, make({0.0f, 0.0f, 0.0f, 0.0f}), 4u); + const auto c = RunCase(device, "C repeat of v1", 4u, make(v1), 4u); + const auto d = RunCase(device, "D (0,0,0,1) at the centre", 4u, make({0.0f, 0.0f, 0.0f, 1.0f}), 4u); + const auto e = RunCase(device, "E (1,1,0,1) real triangle", 4u, make({1.0f, 1.0f, 0.0f, 1.0f}), 4u); + const Position zero{0.0f, 0.0f, 0.0f, 0.0f}; + const auto f = RunCase(device, "F negative zeros", 4u, make({-0.0f, 0.0f, -0.0f, -0.0f}), 4u); + const auto g = RunCase(device, "G triangle list, second triangle has (0,0,0,0)", 6u, {v0, v1, v2, v1, v2, zero}, 6u, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST); + const auto h = RunCase(device, "H strip with two (0,0,0,0) vertices", 5u, {v0, v1, v2, zero, zero}, 5u); + const auto p = RunCase(device, "P point list, (-0.5,-0.5,0,1) and (0,0,0,0)", 2u, {{-0.5f, -0.5f, 0.0f, 1.0f}, zero}, 2u, VK_PRIMITIVE_TOPOLOGY_POINT_LIST); + Require(e.upper > 0u, "probe cannot see the second triangle"); + Require(c.total > 0u && c.upper == 0u, "the first triangle is not drawn alone"); + Require(a.upper == 0u && a.total == c.total, "a vertex fetched past num_records must clip the primitive away"); + Require(b.upper == 0u && b.total == c.total, "a stored (0,0,0,0) vertex must clip the primitive away"); + Require(f.upper == 0u && f.total == c.total, "a stored (-0,0,-0,-0) vertex must clip the primitive away"); + Require(g.upper == 0u && g.total == c.total, "a triangle list with a (0,0,0,0) vertex must keep only the first triangle"); + Require(h.upper == 0u && h.total == c.total, "a strip with two (0,0,0,0) vertices must keep only the first triangle"); + Require(d.upper == 496u, "a (0,0,0,1) vertex must not be clipped away"); + Require(p.total == 1u, "a point at (0,0,0,0) must not be drawn"); +} + +} + +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/ZeroStrideVertexFetch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ZeroStrideVertexFetch.cpp new file mode 100644 index 0000000000..5715c39d27 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ZeroStrideVertexFetch.cpp @@ -0,0 +1,164 @@ +#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 = 32; +constexpr std::uint32_t Vertices = 3; +alignas(256) std::array Pixels{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array VertexCode{ + 0xbe822100, 0x34020a84, 0xe0781000, 0x80010801, 0xf80008cf, 0x0b0a0908, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; + +alignas(256) constexpr std::array Element{0.25f, -2.0f, 7.5f, 1.0f}; + +struct Case { + std::uint32_t select; + std::uint32_t records; + bool inRange; + const char* what; + std::uint32_t selectors = 0xfacu; +}; + +std::vector registrationModule; + +std::array VertexBufferDescriptor(const Case& value) { + const auto address = reinterpret_cast(Element.data()); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), value.records, (0x0104dfacu & ~0xfffu) | value.selectors | (value.select << 28u)}; +} + +std::array OutputDescriptor() { + const auto address = reinterpret_cast(Output.data()); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), static_cast(sizeof(Output)), 0x31016facu}; +} + +void Draw(AgcDriver::VulkanDevice& device, const Case& value) { + Pixels.fill(std::byte{0x40}); + Output.fill(0xdeadbeefu); + std::vector userData(12, 0u); + userData[0] = 0x10000000u; + userData[1] = 0x00000001u; + const auto output = OutputDescriptor(); + std::copy(output.begin(), output.end(), userData.begin() + 4); + ShaderRecompiler::ShaderVertexStageInfo info{}; + info.resourcesNum = 1; + info.resources[0].fields = VertexBufferDescriptor(value); + info.resourcesDst[0] = {8, 4, 0, 0}; + info.fetchAttribReg = 8; + info.fetchBufferReg = 10; + info.fetchEmbedded = true; + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertexRequest{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {64u, 0, userData, std::nullopt, std::nullopt, info, vertexMemory}, + device.Target(), + {0, 0, 0, 64} + }; + vertexRequest.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertexRequest); + Require(vertexResult.vertexAttributes.size() == 1u, std::string(value.what) + ": the fetch did not become a vertex attribute"); + if (device.Target().registrationModules) { + if (registrationModule.empty()) registrationModule = vertexResult.spirv.Copy(); + Require(std::ranges::equal(vertexResult.spirv.Words(), registrationModule), std::string(value.what) + ": registration modules: the V# specialized the vertex module"); + } + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + 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, {}}, + {64u, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + device.Target(), + PixelPushLayout(vertexPush, device.Target()) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, device.Target())} + }}; + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 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, Vertices, 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +void Check(const Case& value) { + for (std::uint32_t vertex = 0; vertex < Vertices; ++vertex) { + for (std::uint32_t component = 0; component < 4u; ++component) { + const auto selector = (value.selectors >> (component * 3u)) & 7u; + const auto selected = selector >= 4u ? std::bit_cast(Element[selector - 4u]) : selector == 1u ? std::bit_cast(1.0f) : 0u; + const auto expected = value.inRange || selector < 4u ? selected : 0u; + const auto actual = Output[vertex * 4u + component]; + Require(actual == expected, std::string(value.what) + ": vertex " + std::to_string(vertex) + " v" + std::to_string(8u + component) + " is " + std::to_string(actual) + ", expected " + std::to_string(expected)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + constexpr std::array cases{{ + {2u, 1u, true, "OOB_SELECT 2, one record"}, + {2u, 0u, false, "OOB_SELECT 2, no records"}, + {3u, 16u, true, "OOB_SELECT 3, 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)}, + }}; + for (const auto& value : cases) { + Draw(*device, value); + Check(value); + } + std::puts("zero-stride vertex fetch tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/shaders/BufferUpdate.comp b/core/libs/prx/libSceAgcDriver/tests/shaders/BufferUpdate.comp new file mode 100644 index 0000000000..b9332d0804 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/shaders/BufferUpdate.comp @@ -0,0 +1,5 @@ +#version 450 +layout(local_size_x = 1) in; +layout(set = 0, binding = 0, std430) buffer Values { uint values[]; }; +layout(push_constant) uniform Update { uint delta; } update; +void main() { values[gl_GlobalInvocationID.x] += update.delta; } diff --git a/core/libs/prx/libSceAgcDriver/tests/shaders/Cover.frag b/core/libs/prx/libSceAgcDriver/tests/shaders/Cover.frag new file mode 100644 index 0000000000..4bb961d02f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/shaders/Cover.frag @@ -0,0 +1,5 @@ +#version 450 +layout(location = 0) out vec4 color; +void main() { + color = vec4(1.0); +} diff --git a/core/libs/prx/libSceAgcDriver/tests/shaders/Cover.vert b/core/libs/prx/libSceAgcDriver/tests/shaders/Cover.vert new file mode 100644 index 0000000000..4a5591d03e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/shaders/Cover.vert @@ -0,0 +1,6 @@ +#version 450 +out gl_PerVertex { vec4 gl_Position; }; +void main() { + vec2 position = vec2(float((gl_VertexIndex << 1) & 2), float(gl_VertexIndex & 2)); + gl_Position = vec4(position * 2.0 - 1.0, 0.5, 1.0); +} 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..3a72da6058 --- /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) / 65536.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/ProvokingVertex.frag b/core/libs/prx/libSceAgcDriver/tests/shaders/ProvokingVertex.frag new file mode 100644 index 0000000000..966977ddc8 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/shaders/ProvokingVertex.frag @@ -0,0 +1,6 @@ +#version 450 +layout(location = 0) flat in vec4 color; +layout(location = 0) out vec4 outputColor; +void main() { + outputColor = color; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/shaders/ProvokingVertex.vert b/core/libs/prx/libSceAgcDriver/tests/shaders/ProvokingVertex.vert new file mode 100644 index 0000000000..c561c90e01 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/shaders/ProvokingVertex.vert @@ -0,0 +1,13 @@ +#version 450 +out gl_PerVertex { vec4 gl_Position; }; +layout(location = 0) flat out vec4 color; +void main() { + vec2 positions[6] = vec2[]( + vec2(-0.75, 0.75), vec2(-0.75, -0.75), vec2(0.75, 0.75), + vec2(0.75, -0.75), vec2(0.75, 0.75), vec2(-0.75, -0.75)); + vec4 colors[6] = vec4[]( + vec4(1, 0, 0, 1), vec4(0, 1, 0, 1), vec4(0, 0, 1, 1), + vec4(1, 1, 0, 1), vec4(0, 1, 1, 1), vec4(1, 0, 1, 1)); + gl_Position = vec4(positions[gl_VertexIndex], 0.5, 1.0); + color = colors[gl_VertexIndex]; +} 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/shaders/VertexRange.vert b/core/libs/prx/libSceAgcDriver/tests/shaders/VertexRange.vert new file mode 100644 index 0000000000..f19a9cc564 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/shaders/VertexRange.vert @@ -0,0 +1,7 @@ +#version 450 +layout(location = 0) in vec4 offset; +out gl_PerVertex { vec4 gl_Position; }; +void main() { + vec2 positions[3] = vec2[](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0)); + gl_Position = vec4(positions[gl_VertexIndex % 3] + vec2(offset.x, 0.0), 0.5, 1.0); +} diff --git a/core/libs/prx/libSceAgcDriver/tests/shaders/VertexRangeProducer.frag b/core/libs/prx/libSceAgcDriver/tests/shaders/VertexRangeProducer.frag new file mode 100644 index 0000000000..c7225d0c00 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/shaders/VertexRangeProducer.frag @@ -0,0 +1,5 @@ +#version 450 +layout(location = 0) out vec4 color; +void main() { + color = vec4(0.5, 0.0, 0.0, 1.0); +} diff --git a/core/libs/prx/libSceAgcDriver/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/ComputePipelineFixture.hpp b/core/libs/prx/libSceAgcDriver/tools/ComputePipelineFixture.hpp new file mode 100644 index 0000000000..ea59418ec3 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tools/ComputePipelineFixture.hpp @@ -0,0 +1,35 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TOOLS_COMPUTEPIPELINEFIXTURE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TOOLS_COMPUTEPIPELINEFIXTURE_HPP + +#include "prx/libSceAgcDriver/Execution/include/ComputePipelineCapture.hpp" + +namespace AgcDriver { + +inline ComputePipelineCapture ComputePipelineFixture() { + ComputePipelineCapture capture; + capture.spirv = { + 0x07230203u, 0x00010000u, 0u, 7u, 0u, + 0x00020011u, 1u, + 0x0003000eu, 0u, 1u, + 0x0005000fu, 5u, 3u, 0x6e69616du, 0u, + 0x00060010u, 3u, 17u, 1u, 1u, 1u, + 0x00040047u, 6u, 1u, 7u, + 0x00020013u, 1u, + 0x00030021u, 2u, 1u, + 0x00040015u, 5u, 32u, 0u, + 0x00040032u, 5u, 6u, 1u, + 0x00050036u, 1u, 3u, 0u, 2u, + 0x000200f8u, 4u, + 0x000100fdu, + 0x00010038u + }; + capture.specialization.push_back({7u, 3u}); + capture.sets = {{0, {{3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2, VK_SHADER_STAGE_COMPUTE_BIT, 0}}}}; + capture.pushConstants = {{VK_SHADER_STAGE_COMPUTE_BIT, 16, 16}}; + capture.flags = VK_PIPELINE_CREATE_DISPATCH_BASE_BIT; + return capture; +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/tools/ComputePipelineReplay.cpp b/core/libs/prx/libSceAgcDriver/tools/ComputePipelineReplay.cpp new file mode 100644 index 0000000000..5baabe910d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tools/ComputePipelineReplay.cpp @@ -0,0 +1,154 @@ +#include "ComputePipelineFixture.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct Options { + std::filesystem::path source; + std::filesystem::path cacheIn; + std::filesystem::path cacheOut; + std::filesystem::path output; + std::filesystem::path fixtureDirectory; + bool fixture = false; +}; + +Options Parse(int argc, char** argv) { + Options result; + for (int index = 1; index < argc; ++index) { + const std::string_view argument(argv[index]); + const auto value = [&](std::filesystem::path& destination) { + if (!destination.empty() || ++index == argc || *argv[index] == '\0') throw std::runtime_error("missing or repeated option: " + std::string(argument)); + destination = argv[index]; + }; + if (argument == "--cache-in") value(result.cacheIn); + else if (argument == "--cache-out") value(result.cacheOut); + else if (argument == "--output") value(result.output); + else if (argument == "--write-fixture") value(result.fixtureDirectory); + else if (argument == "--fixture" && !result.fixture) result.fixture = true; + else if (argument.starts_with("--") || !result.source.empty()) throw std::runtime_error("unknown or repeated argument: " + std::string(argument)); + else result.source = argument; + } + const auto sources = static_cast(!result.source.empty()) + static_cast(result.fixture) + static_cast(!result.fixtureDirectory.empty()); + if (sources != 1) throw std::runtime_error("select a capture file, --fixture, or --write-fixture DIRECTORY"); + if (!result.fixtureDirectory.empty() && (!result.cacheIn.empty() || !result.cacheOut.empty() || !result.output.empty())) throw std::runtime_error("--write-fixture cannot be combined with replay options"); + for (const auto& path : {result.cacheOut, result.output}) { + if (!path.empty() && std::filesystem::exists(path)) throw std::runtime_error("output already exists: " + path.string()); + } + if (!result.cacheOut.empty() && !result.output.empty() && std::filesystem::absolute(result.cacheOut).lexically_normal() == std::filesystem::absolute(result.output).lexically_normal()) throw std::runtime_error("cache and report must use different paths"); + return result; +} + +std::vector ReadCache(const std::filesystem::path& path) { + if (path.empty()) return {}; + const auto bytes = std::filesystem::file_size(path); + if (bytes == 0 || bytes > 64u * 1024u * 1024u) throw std::runtime_error("imported pipeline cache must contain 1 byte to 64 MiB"); + std::vector result(static_cast(bytes)); + std::ifstream input(path, std::ios::binary); + input.read(reinterpret_cast(result.data()), static_cast(result.size())); + if (!input || input.peek() != std::char_traits::eof()) throw std::runtime_error("could not read complete pipeline cache: " + path.string()); + return result; +} + +void WriteBytes(const std::filesystem::path& path, std::span bytes) { + if (std::filesystem::exists(path)) throw std::runtime_error("output already exists: " + path.string()); + std::ofstream output(path, std::ios::binary); + output.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + output.close(); + if (!output) throw std::runtime_error("could not write complete output: " + path.string()); +} + +std::string Quote(std::string_view text) { + std::ostringstream result; + result << '"'; + for (const unsigned char character : text) { + if (character == '\\' || character == '"') result << '\\' << character; + else if (character < 0x20u) result << "\\u" << std::hex << std::setw(4) << std::setfill('0') << static_cast(character); + else result << character; + } + result << '"'; + return result.str(); +} + +std::string Uuid(std::span bytes) { + std::ostringstream result; + for (const auto byte : bytes) result << std::hex << std::setw(2) << std::setfill('0') << static_cast(byte); + return result.str(); +} + +template +void WriteDevice(std::ostream& output, const TDevice& device) { + output << "{\"name\":" << Quote(device.deviceName) + << ",\"vendor_id\":" << device.vendorId << ",\"device_id\":" << device.deviceId + << ",\"driver_version_raw\":" << device.driverVersion << ",\"api_version_raw\":" << device.apiVersion + << ",\"pipeline_cache_uuid\":" << Quote(Uuid(device.pipelineCacheUuid)) << '}'; +} + +std::string Report(const Options& options, const AgcDriver::ComputePipelineCapture& capture, const AgcDriver::ComputePipelineReplayResult& replay) { + std::ostringstream output; + output << std::setprecision(12) + << "{\n \"schema_version\":1,\n \"source\":" << Quote(options.fixture ? "synthetic-no-op-compute" : options.source.string()) + << ",\n \"capture_identity\":" << Quote(capture.Identity()) + << ",\n \"capture_identity_algorithm\":\"fnv1a64\"" + << ",\n \"spirv_words\":" << capture.spirv.size() + << ",\n \"descriptor_sets\":" << capture.sets.size() + << ",\n \"push_constant_ranges\":" << capture.pushConstants.size() + << ",\n \"specialization_constants\":" << capture.specialization.size() + << ",\n \"required_subgroup_size\":" << capture.requiredSubgroupSize + << ",\n \"pipeline_flags\":" << capture.flags + << ",\n \"stage_flags\":" << capture.stageFlags + << ",\n \"captured_device\":"; + WriteDevice(output, capture); + output << ",\n \"replay_device\":"; + WriteDevice(output, replay); + output << ",\n \"application_cache_policy\":" << Quote(options.cacheIn.empty() ? "fresh" : "imported") + << ",\n \"cache_import_path\":" << Quote(options.cacheIn.string()) + << ",\n \"cache_import_bytes\":" << replay.initialCacheBytes + << ",\n \"cache_export_path\":" << Quote(options.cacheOut.string()) + << ",\n \"cache_export_bytes\":" << (options.cacheOut.empty() ? 0u : replay.cache.size()) + << ",\n \"driver_cache_state\":\"uncontrolled\"" + << ",\n \"dispatch_count\":0,\n \"pipeline_statistics\":false" + << ",\n \"milliseconds\":{\"shader_module\":" << replay.moduleMilliseconds + << ",\"layout\":" << replay.layoutMilliseconds << ",\"pipeline_create\":" << replay.pipelineMilliseconds + << ",\"cache\":" << replay.cacheMilliseconds << ",\"replay_total\":" << replay.totalMilliseconds << "}\n}\n"; + return output.str(); +} + +} + +int main(int argc, char** argv) { + if (argc == 2 && std::string_view(argv[1]) == "--help") { + std::cout << "agc_compute_pipeline_replay CAPTURE|--fixture [--cache-in FILE] [--cache-out FILE] [--output FILE]\n" + "agc_compute_pipeline_replay --write-fixture DIRECTORY\n" + "Compiles one compute pipeline without dispatch. Fresh application cache does not clear driver caches.\n"; + return 0; + } + try { + const auto options = Parse(argc, argv); + if (!options.fixtureDirectory.empty()) { + std::cout << AgcDriver::ComputePipelineFixture().Write(options.fixtureDirectory).string() << '\n'; + return 0; + } + const auto* statistics = std::getenv("APS5_PIPELINE_STATS"); + if (statistics != nullptr && std::string_view(statistics) != "0" && *statistics != '\0') throw std::runtime_error("unset APS5_PIPELINE_STATS before benchmarking"); + const auto capture = options.fixture ? AgcDriver::ComputePipelineFixture() : AgcDriver::ComputePipelineCapture::Read(options.source); + if ((capture.flags & VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR) != 0) throw std::runtime_error("capture requests pipeline statistics; capture again with APS5_PIPELINE_STATS disabled"); + const auto cache = ReadCache(options.cacheIn); + AgcDriver::VulkanDevice device(AgcDriver::VulkanDevice::CompileOnly{}); + const auto replay = device.ReplayComputePipeline(capture, cache); + if (!options.cacheOut.empty()) WriteBytes(options.cacheOut, replay.cache); + const auto report = Report(options, capture, replay); + if (!options.output.empty()) WriteBytes(options.output, std::as_bytes(std::span(report.data(), report.size()))); + std::cout << report; + return 0; + } catch (const std::exception& error) { + std::cerr << "compute pipeline replay: " << error.what() << '\n'; + return 1; + } +} 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 b62ee64a71..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 { @@ -41,8 +43,12 @@ bool g_disassemble = false; 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) { @@ -105,16 +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 || 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.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(" %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) { @@ -136,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"); @@ -152,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] [--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; @@ -173,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; @@ -181,6 +223,14 @@ int main(int argc, char** argv) { g_code = true; continue; } + if (std::string(argv[i]) == "--prepare") { + 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/libSceAgcDriver/trace_revision.cmake b/core/libs/prx/libSceAgcDriver/trace_revision.cmake new file mode 100644 index 0000000000..a63974e68e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/trace_revision.cmake @@ -0,0 +1,19 @@ +set(commit unknown) +set(dirty -1) +if(GIT) + execute_process(COMMAND "${GIT}" -C "${SOURCE}" rev-parse --short=12 HEAD + OUTPUT_VARIABLE head RESULT_VARIABLE failed OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) + if(failed EQUAL 0 AND NOT head STREQUAL "") + set(commit ${head}) + execute_process(COMMAND "${GIT}" -C "${SOURCE}" status --porcelain --untracked-files=no + OUTPUT_VARIABLE changes RESULT_VARIABLE failed ERROR_QUIET) + if(failed EQUAL 0 AND changes STREQUAL "") + set(dirty 0) + elseif(failed EQUAL 0) + set(dirty 1) + endif() + endif() +endif() +file(WRITE "${OUTPUT}.new" "#pragma once\n#define APS5_TRACE_COMMIT \"${commit}\"\n#define APS5_TRACE_DIRTY ${dirty}\n") +execute_process(COMMAND ${CMAKE_COMMAND} -E copy_if_different "${OUTPUT}.new" "${OUTPUT}") +file(REMOVE "${OUTPUT}.new") diff --git a/core/libs/prx/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/CMakeLists.txt b/core/libs/prx/libSceAjm.native/CMakeLists.txt index aa4e24f833..51b7c2507f 100644 --- a/core/libs/prx/libSceAjm.native/CMakeLists.txt +++ b/core/libs/prx/libSceAjm.native/CMakeLists.txt @@ -1,11 +1,14 @@ 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 ${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 @@ -13,11 +16,19 @@ add_library(atrac9 STATIC EXCLUDE_FROM_ALL ${LIBATRAC9_DIR}/quantization.c ${LIBATRAC9_DIR}/scale_factors.c ${LIBATRAC9_DIR}/tables.c - ${LIBATRAC9_DIR}/unpack.c + ${LIBATRAC9_PATCHED_DIR}/unpack.c ${LIBATRAC9_DIR}/utility.c ) +target_include_directories(atrac9 PRIVATE ${LIBATRAC9_DIR}) set_target_properties(atrac9 PROPERTIES POSITION_INDEPENDENT_CODE ON) +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) + add_library(libSceAjm.native SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Ajm.cpp @@ -26,18 +37,49 @@ add_library(libSceAjm.native SHARED EXCLUDE_FROM_ALL target_include_directories(libSceAjm.native PRIVATE ${LIBS_INCLUDE_DIR} ${LIBATRAC9_DIR} + ${VGMSTREAM_ATRAC9_DIR} ) -target_link_libraries(libSceAjm.native PRIVATE atrac9 ffmpeg libc libkernel) +target_link_libraries(libSceAjm.native PRIVATE atrac9 atrac9_vibration ffmpeg libc libkernel) configure_windows_unwind(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_compile_options(atrac9_gradient_tests PRIVATE -ffunction-sections -fdata-sections) + if(APPLE) + target_link_options(atrac9_gradient_tests PRIVATE LINKER:-dead_strip) + else() + target_link_options(atrac9_gradient_tests PRIVATE LINKER:--gc-sections) + 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) + 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}) + target_link_libraries(guest_ajm_teardown_tests PRIVATE libc ${CMAKE_DL_LIBS}) + add_dependencies(guest_ajm_teardown_tests libSceAjm.native) + 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 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 8f5edd6e9f..2df65a236d 100644 --- a/core/libs/prx/libSceAjm.native/Export.cpp +++ b/core/libs/prx/libSceAjm.native/Export.cpp @@ -14,18 +14,6 @@ static void AjmStub(const char* name) { extern "C" { -int APS5_VABI sceAjmBatchJobDecodeSplit(AjmBatchInfo* info, uint32_t instance, const AjmBuffer* input_buffers, size_t input_buffers_num, const AjmBuffer* output_buffers, size_t output_buffers_num, void* result) { - (void)info; - (void)instance; - (void)input_buffers; - (void)input_buffers_num; - (void)output_buffers; - (void)output_buffers_num; - (void)result; - AjmStub(__func__); - return 0; -} - 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) { (void)info; (void)instance; @@ -38,17 +26,6 @@ int APS5_VABI sceAjmBatchJobEncode(AjmBatchInfo* info, uint32_t instance, const return 0; } -int APS5_VABI sceAjmBatchJobSetResampleParametersEx(AjmBatchInfo* info, uint32_t instance, float ratio_start, float ratio_change_per_sample, uint32_t flags, void* result) { - (void)info; - (void)instance; - (void)ratio_start; - (void)ratio_change_per_sample; - (void)flags; - (void)result; - AjmStub(__func__); - return 0; -} - const char* APS5_VABI sceAjmStrError(int error) { (void)error; AjmStub(__func__); @@ -65,4 +42,28 @@ int APS5_VABI sceAjmDecWVorbisCreateSetupPacket(void) { return 0; } +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 new file mode 100644 index 0000000000..6c1b91c006 --- /dev/null +++ b/core/libs/prx/libSceAjm.native/PatchLibAtrac9.cmake @@ -0,0 +1,30 @@ +file(READ ${LIBATRAC9_DIR}/unpack.c libatrac9Unpack) +set(libatrac9OldGuard "block->GradientEndUnit < 0 || block->GradientEndUnit >= 48") +set(libatrac9NewGuard "block->GradientEndUnit < 0 || block->GradientEndUnit > 31") +string(FIND "${libatrac9Unpack}" "${libatrac9OldGuard}" libatrac9GuardPosition) +if(libatrac9GuardPosition EQUAL -1) + message(FATAL_ERROR "LibAtrac9 gradient guard has changed; review the local bounds fix") +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 b7649e1517..69fc72c729 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -4,10 +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 @@ -16,10 +20,10 @@ extern "C" { #include #include #include -#include #include #include #include +#include // The batch buffer and its job records are private to this library: the guest only reserves the // memory and reads the "used bytes" field of AjmBatchInfo. Sideband results written back to guest @@ -38,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; @@ -100,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; @@ -108,6 +127,7 @@ struct SidebandResampleInfo { struct Resampler { float ratio = 1.0f; + float change = 0.0f; std::vector frames; double position = 1.0; }; @@ -116,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; @@ -124,6 +145,7 @@ struct Instance { std::uint32_t superframeRemaining = 0; std::uint32_t frameInSuperframe = 0; std::uint64_t totalDecodedSamples = 0; + std::uint64_t segmentSamples = 0; SidebandGaplessDecode gapless{}; bool flagsReported = false; AVCodecContext* mp3 = nullptr; @@ -132,25 +154,43 @@ struct Instance { std::uint32_t mp3Bitrate = 0; 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); } }; -std::mutex g_lock; -std::map> g_instances; +std::mutex& g_lock = *new std::mutex(); +std::map>& g_instances = *new std::map>(); std::atomic g_nextContext{1}; std::atomic g_nextInstance{1}; std::atomic g_nextBatch{1}; -std::mutex g_batchLock; -std::set g_batches; +std::mutex& g_batchLock = *new std::mutex(); +std::map& g_batches = *new std::map(); enum class JobKind : std::uint32_t { Initialize = 1, @@ -193,6 +233,7 @@ static_assert(sizeof(PackedJob) == 32); int Append(AjmBatchInfo* info, const JobHeader& header, const AjmBuffer* inputs, const AjmBuffer* outputs) { if (!info || !info->p_buffer) return SCE_AJM_ERROR_INVALID_PARAMETER; if (header.inputCount > 0xffu || header.outputCount > 0xffu || header.parameterSize > sizeof(header.parameters) || header.sidebandSize > 0xffffffffu) return SCE_AJM_ERROR_INVALID_PARAMETER; + if ((header.inputCount && !inputs) || (header.outputCount && !outputs)) NotImplemented_nid_no_patch("AJM batch job with a null buffer list and a nonzero buffer count"); const std::size_t parameterBytes = (header.parameterSize + 7u) & ~std::size_t{7}; const std::size_t bytes = sizeof(PackedJob) + (header.inputCount + header.outputCount) * sizeof(AjmBuffer) + parameterBytes; if (info->offset > info->size || bytes > info->size - info->offset) return SCE_AJM_ERROR_OUT_OF_RESOURCES; @@ -200,9 +241,9 @@ int Append(AjmBatchInfo* info, const JobHeader& header, const AjmBuffer* inputs, const PackedJob record{static_cast(header.kind), static_cast(header.inputCount), static_cast(header.outputCount), static_cast(header.parameterSize), header.instance, header.flags, header.sideband, static_cast(header.sidebandSize), static_cast(bytes)}; std::memcpy(cursor, &record, sizeof(record)); cursor += sizeof(record); - if (header.inputCount) std::memcpy(cursor, inputs, header.inputCount * sizeof(AjmBuffer)); + if (inputs) std::memcpy(cursor, inputs, header.inputCount * sizeof(AjmBuffer)); cursor += header.inputCount * sizeof(AjmBuffer); - if (header.outputCount) std::memcpy(cursor, outputs, header.outputCount * sizeof(AjmBuffer)); + if (outputs) std::memcpy(cursor, outputs, header.outputCount * sizeof(AjmBuffer)); cursor += header.outputCount * sizeof(AjmBuffer); if (header.parameterSize) std::memcpy(cursor, header.parameters, header.parameterSize); info->offset += bytes; @@ -264,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]; @@ -287,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(); @@ -307,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) { @@ -321,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); @@ -351,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; @@ -366,18 +466,24 @@ std::int32_t InitializeInstance(Instance& instance, const std::uint8_t* paramete std::memcpy(&channels, parameters, sizeof(channels)); std::memcpy(&sampleRate, parameters + 4, sizeof(sampleRate)); if (channels == 0 || channels > 8 || sampleRate == 0) return AJM_RESULT_INVALID_PARAMETER; - std::uint32_t third = 0; - if (size >= 12) std::memcpy(&third, parameters + 8, sizeof(third)); - if (sampleRate != 48000 || third != 0) NotImplemented_nid_no_patch("sceAjmBatchJobInitialize (Opus sample rate other than 48000 or nonzero third parameter word)"); + 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) 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; + instance.segmentSamples = 0; instance.gapless = {}; return 0; } + if (instance.codec == CODEC_M4AAC) return InitializeAac(instance, parameters, size); if (instance.codec != CODEC_AT9) { instance.initialized = true; return 0; @@ -392,6 +498,7 @@ std::int32_t InitializeInstance(Instance& instance, const std::uint8_t* paramete } instance.initialized = true; instance.totalDecodedSamples = 0; + instance.segmentSamples = 0; instance.gapless = {}; return 0; } @@ -418,14 +525,30 @@ bool consumedFirstRun(Instance& instance) { return true; } -std::vector JoinInputs(const JobHeader& job, const AjmBuffer* inputs) { - std::vector input; - for (std::uint32_t index = 0; index < job.inputCount; ++index) { - const auto* data = static_cast(inputs[index].ptr); - input.insert(input.end(), data, data + inputs[index].size); +class JobInput { +public: + JobInput(const JobHeader& job, const AjmBuffer* inputs) { + if (job.inputCount == 1) { + bytes = {static_cast(inputs[0].ptr), static_cast(inputs[0].size)}; + return; + } + for (std::uint32_t index = 0; index < job.inputCount; ++index) { + const auto* data = static_cast(inputs[index].ptr); + joined.insert(joined.end(), data, data + inputs[index].size); + } + bytes = joined; } - return input; -} + JobInput(const JobInput&) = delete; + JobInput& operator=(const JobInput&) = delete; + std::size_t size() const { return bytes.size(); } + bool empty() const { return bytes.empty(); } + const std::uint8_t* data() const { return bytes.data(); } + std::uint8_t operator[](std::size_t index) const { return bytes[index]; } + +private: + std::vector joined; + std::span bytes; +}; struct PcmOutputs { const AjmBuffer* buffers; @@ -489,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) { @@ -507,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; @@ -522,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) { @@ -545,9 +674,10 @@ void EmitFrame(Instance& instance, PcmOutputs& outputs, const std::uint8_t* pcm, 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)) { outputs.Emit(pcm + first * channels * sampleBytes, count * channels * sampleBytes); @@ -565,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"); } } @@ -590,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)); } @@ -601,7 +736,7 @@ void WriteRunSideband(const JobHeader& job, const Instance& instance, std::int32 } void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) { - const auto input = JoinInputs(job, inputs); + const JobInput input(job, inputs); PcmOutputs pcmOutputs(outputs, job.outputCount); const auto channels = static_cast(instance.info.channels); @@ -627,7 +762,9 @@ void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c const auto superframeSize = static_cast(instance.info.superframeSize); const auto framesInSuperframe = static_cast(std::max(1, instance.info.framesInSuperframe)); if (Resampling(instance)) ResamplerProduce(instance, pcmOutputs, channels, encoding, sampleBytes); + const auto segmentEnded = [&] { return instance.gapless.totalSamples != 0 && instance.segmentSamples >= instance.gapless.totalSamples; }; for (;;) { + if (segmentEnded()) break; if (instance.superframeRemaining == 0) consumed += RiffDataOffset(input.data() + consumed, input.size() - consumed); const std::size_t needed = instance.superframeRemaining == 0 ? superframeSize : instance.superframeRemaining; if (input.size() - consumed < needed) { @@ -659,14 +796,14 @@ void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c ++frames; EmitFrame(instance, pcmOutputs, pcm.data(), frameSamples, channels, sampleBytes); - if ((job.flags & RUN_MULTIPLE_FRAMES) == 0) break; } + if (segmentEnded()) instance.segmentSamples = 0; if (TraceEnabled()) { std::fprintf(stderr, "[ajm] instance %u run flags 0x%llx: %zu input bytes in %u buffers, %zu output bytes in %u buffers, sideband %llu bytes; superframe %d (%d frames), %d channels, %d samples/frame, %d Hz, config %02x %02x %02x %02x -> result 0x%x, decoder status 0x%x, %u frames, consumed %zu, produced %zu, total samples %llu, gapless total %u skip %u skipped %u, input", job.instance, static_cast(job.flags), input.size(), job.inputCount, pcmOutputs.capacity, job.outputCount, static_cast(job.sidebandSize), instance.info.superframeSize, instance.info.framesInSuperframe, instance.info.channels, instance.info.frameSamples, instance.info.samplingRate, instance.info.configData[0], instance.info.configData[1], instance.info.configData[2], instance.info.configData[3], static_cast(result), static_cast(decodeStatus), frames, consumed, pcmOutputs.produced, static_cast(instance.totalDecodedSamples), instance.gapless.totalSamples, instance.gapless.skipSamples, instance.gapless.skippedSamples); - for (std::size_t i = 0; i < input.size() && i < 16; ++i) std::fprintf(stderr, " %02x", input[i]); + for (std::size_t i = 0; i < consumed && i < 16; ++i) std::fprintf(stderr, " %02x", input[i]); std::fprintf(stderr, "\n"); } WriteRunSideband(job, instance, result, consumed, pcmOutputs.produced, frames, CurrentFormat(instance)); @@ -838,7 +975,7 @@ void OpenMp3(Instance& instance) { } void RunMp3(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) { - const auto input = JoinInputs(job, inputs); + const JobInput input(job, inputs); PcmOutputs pcmOutputs(outputs, job.outputCount); const auto encoding = PcmEncoding(instance); if (encoding > 2) throw std::runtime_error("AJM MP3: PCM encoding " + std::to_string(encoding) + " is not implemented"); @@ -924,6 +1061,44 @@ 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}; + static constexpr std::uint8_t mapping[8][8] = { + {0}, {0, 1}, {0, 2, 1}, {0, 1, 2, 3}, + {0, 4, 1, 2, 3}, {0, 4, 1, 2, 3, 5}, + {0, 4, 1, 2, 3, 5, 6}, {0, 6, 1, 2, 3, 4, 5, 7} + }; + const std::size_t index = 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] = 1; + header[19] = streams[index]; + header[20] = coupled[index]; + std::memcpy(header + 21, mapping[index], instance.opusChannels); + } if (avcodec_open2(instance.opus, codec, nullptr) < 0) throw std::runtime_error("AJM: cannot open the FFmpeg Opus decoder"); } @@ -947,8 +1122,120 @@ 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 auto input = JoinInputs(job, inputs); + const JobInput input(job, inputs); PcmOutputs pcmOutputs(outputs, job.outputCount); const auto encoding = PcmEncoding(instance); if (encoding > 2) throw std::runtime_error("AJM Opus: PCM encoding " + std::to_string(encoding) + " is not implemented"); @@ -976,6 +1263,7 @@ void RunOpus(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, instance.opusPending.erase(instance.opusPending.begin(), instance.opusPending.begin() + static_cast(bytes)); } while (consumed < input.size() && instance.opusPending.empty()) { + if (frames != 0 && pcmOutputs.Room() < frameBytes) break; if (input.size() - consumed < 2) { result |= AJM_RESULT_PARTIAL_INPUT; break; @@ -995,6 +1283,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) { @@ -1023,9 +1312,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); @@ -1057,22 +1347,220 @@ 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; instance.gapless.skippedSamples = 0; 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; } } @@ -1096,7 +1584,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); @@ -1133,18 +1621,25 @@ void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* out AJM_TRACE("[ajm] instance %u set gapless decode: total %u, skip %u, reset %llu (sideband %llu bytes)\n", job.instance, gapless.totalSamples, gapless.skipSamples, static_cast(job.flags), static_cast(job.sidebandSize)); instance->gapless.totalSamples = gapless.totalSamples; instance->gapless.skipSamples = gapless.skipSamples; - if (job.flags) instance->gapless.skippedSamples = 0; + if (job.flags) { + instance->gapless.skippedSamples = 0; + instance->segmentSamples = 0; + } WriteResult(job.sideband, job.sidebandSize, 0); break; } 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); @@ -1155,12 +1650,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)); @@ -1168,6 +1663,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); } @@ -1231,12 +1730,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) { @@ -1329,6 +1833,16 @@ int APS5_VABI sceAjmBatchJobSetResampleParameters(AjmBatchInfo* info, uint32_t i return Append(info, header, nullptr, nullptr); } +int APS5_VABI sceAjmBatchJobSetResampleParametersEx(AjmBatchInfo* info, uint32_t instance, float ratio_start, float ratio_change_per_sample, uint32_t flags, void* 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) { return Append(info, MakeHeader(JobKind::GetResampleInfo, instance, result, sizeof(SidebandResult) + sizeof(SidebandResampleInfo)), nullptr, nullptr); } @@ -1337,6 +1851,10 @@ int APS5_VABI sceAjmBatchJobDecode(AjmBatchInfo* info, uint32_t instance, const return sceAjmBatchJobRun(info, instance, SIDEBAND_STREAM, bitstream_input, bitstream_input_size, pcm_output, pcm_output_size, result, sizeof(SidebandResult) + sizeof(SidebandStream)); } +int APS5_VABI sceAjmBatchJobDecodeSplit(AjmBatchInfo* info, uint32_t instance, const AjmBuffer* input_buffers, size_t input_buffers_num, const AjmBuffer* output_buffers, size_t output_buffers_num, void* result) { + return sceAjmBatchJobRunSplit(info, instance, SIDEBAND_STREAM, input_buffers, input_buffers_num, output_buffers, output_buffers_num, result, sizeof(SidebandResult) + sizeof(SidebandStream)); +} + int APS5_VABI sceAjmBatchJobDecodeSingle(AjmBatchInfo* info, uint32_t instance, const void* bitstream_input, size_t bitstream_input_size, void* pcm_output, size_t pcm_output_size, void* result) { return sceAjmBatchJobRun(info, instance, SIDEBAND_STREAM, bitstream_input, bitstream_input_size, pcm_output, pcm_output_size, result, sizeof(SidebandResult) + sizeof(SidebandStream)); } @@ -1359,7 +1877,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)); @@ -1389,18 +1907,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; @@ -1409,7 +1929,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.native/tests/Atrac9Gradient.c b/core/libs/prx/libSceAjm.native/tests/Atrac9Gradient.c new file mode 100644 index 0000000000..5ad7595aa1 --- /dev/null +++ b/core/libs/prx/libSceAjm.native/tests/Atrac9Gradient.c @@ -0,0 +1,36 @@ +#include "unpack.c" +#include + +static void WriteBits(unsigned char* data, int* position, int value, int width) +{ + for (int bit = width - 1; bit >= 0; --bit) + { + int offset = (*position)++; + data[offset / 8] |= (unsigned char)(((value >> bit) & 1) << (7 - offset % 8)); + } +} + +static At9Status GradientStatus(int endUnit) +{ + unsigned char data[4] = {0}; + int position = 0; + WriteBits(data, &position, 0, 2); + WriteBits(data, &position, 0, 6); + WriteBits(data, &position, endUnit - 1, 6); + WriteBits(data, &position, 0, 5); + WriteBits(data, &position, 0, 5); + WriteBits(data, &position, 0, 4); + + Block block = {0}; + BitReaderCxt reader; + InitBitReaderCxt(&reader, data, sizeof(data)); + return ReadGradientParams(&block, &reader); +} + +int main(void) +{ + if (GradientStatus(31) != ERR_SUCCESS) return EXIT_FAILURE; + if (GradientStatus(32) != ERR_UNPACK_GRAD_END_UNIT_OOB) return EXIT_FAILURE; + if (GradientStatus(47) != ERR_UNPACK_GRAD_END_UNIT_OOB) return EXIT_FAILURE; + return EXIT_SUCCESS; +} 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/CMakeLists.txt b/core/libs/prx/libSceAmpr/CMakeLists.txt index bf963496e6..f9898075a1 100644 --- a/core/libs/prx/libSceAmpr/CMakeLists.txt +++ b/core/libs/prx/libSceAmpr/CMakeLists.txt @@ -1,6 +1,6 @@ add_library(libSceAmpr SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceAmpr PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(libSceAmpr PRIVATE libkernel) +target_link_libraries(libSceAmpr PRIVATE libkernel libc) configure_windows_unwind(libSceAmpr) if(BUILD_TESTING) @@ -13,4 +13,40 @@ if(BUILD_TESTING) set_tests_properties(guest_ampr_markers PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + add_test_executable(guest_amm_placement_tests ${LIBS_INCLUDE_DIR}/tests/GuestAmmPlacement.cpp) + target_include_directories(guest_amm_placement_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_amm_placement_tests PRIVATE libSceAmpr libkernel libc) + configure_windows_unwind(guest_amm_placement_tests) + add_test(NAME guest_amm_placement COMMAND guest_amm_placement_tests) + if(WIN32) + set_tests_properties(guest_amm_placement PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(guest_ampr_threads_tests ${LIBS_INCLUDE_DIR}/tests/GuestAmprThreads.cpp) + target_include_directories(guest_ampr_threads_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ampr_threads_tests PRIVATE libSceAmpr libkernel libc) + configure_windows_unwind(guest_ampr_threads_tests) + add_test(NAME guest_ampr_threads COMMAND guest_ampr_threads_tests) + if(WIN32) + set_tests_properties(guest_ampr_threads PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") + enable_language(ASM) + add_test_executable(guest_ampr_cpp_error_tests + ${LIBS_INCLUDE_DIR}/tests/GuestAmprCppError.cpp + ${LIBS_INCLUDE_DIR}/tests/GuestAmprCppError.S) + target_include_directories(guest_ampr_cpp_error_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ampr_cpp_error_tests PRIVATE libSceAmpr libkernel libc) + configure_windows_unwind(guest_ampr_cpp_error_tests) + add_test(NAME guest_ampr_cpp_error COMMAND guest_ampr_cpp_error_tests) + set_tests_properties(guest_ampr_cpp_error PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(guest_ampr_cpp_error PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endif() endif() diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index 6105b3e461..6e55ecbd7b 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -1,19 +1,32 @@ #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/SonyCrt.hpp" #include "prx/libkernel/Apr/include/AprCommandBuffer.hpp" +#include "prx/libkernel/Time/include/Time.hpp" +#include +#include #include +#include #include +#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; static constexpr int SCE_KERNEL_ERROR_EBUSY = 0x80020010; +static std::uint32_t RemainingCommandBytes(const Apr::CommandBufferObject& buffer) { + if (buffer.offset > buffer.size) throw std::invalid_argument("AMPR: command buffer offset exceeds bound memory"); + return buffer.size - buffer.offset; +} + static int Append(Apr::CommandBufferObject* buffer, const void* command, uint32_t bytes) { - if (!buffer->base || bytes > buffer->size - buffer->offset) return SCE_AMPR_ERROR_BUFFER_FULL; + if (!buffer->base) 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; @@ -79,7 +92,8 @@ static std::uint64_t NopBytes(std::uint32_t dwords) { static int AppendNop(Apr::CommandBufferObject* buffer, std::uint32_t dwords, const std::uint32_t* data) { if (!buffer) return SCE_KERNEL_ERROR_EINVAL; const std::uint64_t bytes = NopBytes(dwords); - if (!buffer->base || bytes > buffer->size - buffer->offset) 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); @@ -102,7 +116,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 > buffer->size - buffer->offset) 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); @@ -129,7 +144,7 @@ static bool ValidMap(std::uint64_t address, std::uint64_t directOffset, std::uin static constexpr std::int64_t AmmMeasureInvalid = SCE_KERNEL_ERROR_EINVAL; static int AppendAmm(Apr::CommandBufferObject* buffer, const void* command, std::uint32_t bytes) { - if (bytes > buffer->size - buffer->offset) return SCE_KERNEL_ERROR_EBUSY; + if (bytes > RemainingCommandBytes(*buffer)) return SCE_KERNEL_ERROR_EBUSY; return Append(buffer, command, bytes); } @@ -173,7 +188,7 @@ static constexpr std::int32_t PrtAllocationMask = 1019; static int RecordPrt(Apr::CommandBufferObject* buffer, const void* command, std::uint32_t bytes) { if (!buffer) return SCE_KERNEL_ERROR_EINVAL; - if (bytes > buffer->size - buffer->offset) return SCE_KERNEL_ERROR_EBUSY; + if (bytes > RemainingCommandBytes(*buffer)) return SCE_KERNEL_ERROR_EBUSY; if (!buffer->base) return SCE_KERNEL_ERROR_EPERM; return Append(buffer, command, bytes); } @@ -210,7 +225,24 @@ static int RecordWriteAddressFromCounterPair(Apr::CommandBufferObject* buffer, v return AppendCommand(buffer, Apr::Opcode::WriteAddressFromCounterPair, Apr::WriteAddressFromCounterCommand{{}, reinterpret_cast(address), counter, counter + 1u}); } +static std::atomic_flag sharedPtrSpinLock; + +struct AmmUsageStatsData { + std::uint64_t sizeInBytes; + std::uint16_t numPageTablePoolEntries; + std::uint16_t allocatedPageTablePoolEntries; + std::uint16_t highWatermarkAllocatedEntries; + std::uint16_t reserved; + std::uint32_t ringIdleFlags; +}; +static_assert(sizeof(AmmUsageStatsData) == 0x18); +static constexpr std::uint16_t AmmPageTablePoolEntries = 512; +static constexpr std::uint32_t AmmRingIdleFlags = 0x7; + extern "C" { +int APS5_VABI _Cnd_init_nid_postfix(void** handle); +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; @@ -257,6 +289,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; @@ -297,7 +330,11 @@ int APS5_VABI sceAmprAprCommandBufferReadFileGather(Apr::CommandBufferObject* bu (void)mapState; (void)scatterGatherState; if (!ValidReadLength(size) || !ValidReadOffset(offset)) return SCE_KERNEL_ERROR_EINVAL; - return AppendRead(buffer, Apr::Opcode::ReadFileGather, 0, nullptr, size, offset); + if (!buffer || (buffer->recording & Apr::ScatterGatherValid) == 0u) return SCE_KERNEL_ERROR_EINVAL; + const Apr::CompactReadFileGatherCommand command{ + static_cast(Apr::Opcode::CompactReadFileGather) | static_cast(offset >> 32u) << 8u, + static_cast(size - 1u), static_cast(offset)}; + return Append(buffer, &command, sizeof(command)); } int APS5_VABI sceAmprAprCommandBufferReadFileGatherScatter(Apr::CommandBufferObject* buffer, std::uint64_t* mapState, std::uint64_t* scatterGatherState, void* destination, std::uint64_t size, std::uint64_t offset) { @@ -358,7 +395,8 @@ int APS5_VABI sceAmprCommandBufferConstructNop(Apr::CommandBufferObject* buffer, } int APS5_VABI sceAmprCommandBufferConstructor(Apr::CommandBufferObject* buffer) { - *buffer = {nullptr, 0, 0, 0, Apr::BufferType::Generic}; + if (!buffer) return SCE_KERNEL_ERROR_EINVAL; + *buffer = {Apr::BufferType::Generic, 0, 0, 0, 0, nullptr}; return 0; } @@ -412,12 +450,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; @@ -521,7 +561,7 @@ uint32_t APS5_VABI sceAmprMeasureCommandSizeReadFile(void) { std::uint64_t APS5_VABI sceAmprMeasureCommandSizeReadFileGather(std::uint64_t size, std::uint64_t offset) { if (!ValidReadLength(size) || !ValidReadOffset(offset)) return MeasureInvalid; - return sizeof(Apr::ReadFileCommand); + return sizeof(Apr::CompactReadFileGatherCommand); } std::uint64_t APS5_VABI sceAmprMeasureCommandSizeReadFileGatherScatter(void* destination, std::uint64_t size, std::uint64_t offset) { @@ -774,6 +814,11 @@ int APS5_VABI sceAmprAmmGetVirtualAddressRanges(std::uint64_t* start, std::uint6 return 0; } +int APS5_VABI sceAmprAmmSetPageTablePoolOccupancyNotificationThreshold(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceAmprAmmSubmitCommandBuffer(void* base, std::uint32_t offset, std::uint32_t priority) { if (priority > 2u) return SCE_KERNEL_ERROR_EINVAL; if (!base) return SCE_KERNEL_ERROR_EPERM; @@ -802,4 +847,120 @@ int APS5_VABI sceAmprAmmWaitCommandBufferCompletion(std::uint32_t id) { return AmmSubmitted_nid_no_patch(id) ? 0 : SCE_KERNEL_ERROR_ESRCH; } +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); +} + +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; +} + +void APS5_VABI _Lock_shared_ptr_spin_lock_nid_postfix(void) { + while (sharedPtrSpinLock.test_and_set(std::memory_order_acquire)) std::this_thread::yield(); +} + + +[[noreturn]] void APS5_VABI __cxa_call_unexpected_nid_postfix(void* unwindException) { + __cxa_call_terminate_nid_postfix(unwindException); +} + +APS5_EXPORT("0hlfW1O4Aa4", sceAmprLocaleconv); +SonyLconv* APS5_VABI sceAmprLocaleconv(void) { + return SonyLocaleconv(); +} + +APS5_EXPORT("802pFCwC9w0", sceAmprUdivti3); +unsigned __int128 APS5_VABI sceAmprUdivti3(unsigned __int128 dividend, unsigned __int128 divisor) { + return SonyUdivti3(dividend, divisor); +} + +APS5_EXPORT("MU25eqxSDTw", sceAmprSinh); +double APS5_VABI sceAmprSinh(double x, double y) { + return SonySinh(x, y); +} + +APS5_EXPORT("-vXEQdRADLI", sceAmprStrtokS); +char* APS5_VABI sceAmprStrtokS(char* str, const char* delim, char** context) { + return SonyStrtokS(str, delim, context); +} + +APS5_EXPORT("2UFh+YKfuzk", sceAmprCtimeS); +int APS5_VABI sceAmprCtimeS(char* buffer, std::size_t size, const std::int64_t* time) { + return SonyCtimeS(buffer, size, time); +} + +APS5_EXPORT("6f5f-qx4ucA", sceAmprWcscpyS); +int APS5_VABI sceAmprWcscpyS(std::uint16_t* dest, std::size_t size, const std::uint16_t* src) { + return SonyWcscpyS(dest, size, src); +} + +APS5_EXPORT("ENLfKJEZTjE", sceAmprSetConstraintHandlerS); +void* APS5_VABI sceAmprSetConstraintHandlerS(void* handler) { + return SonySetConstraintHandlerS(handler); +} + +APS5_EXPORT("Jo9ON-AX9eU", sceAmprFltrounds); +int APS5_VABI sceAmprFltrounds(void) { + return SonyFltrounds(); +} + +APS5_EXPORT("ODGONXcSmz4", sceAmprIgnoreHandlerS); +void APS5_VABI sceAmprIgnoreHandlerS(const char* message, void* pointer, int error) { + 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); +} + +APS5_EXPORT("qPe7-h5Jnuc", sceAmprAsctimeS); +int APS5_VABI sceAmprAsctimeS(char* buffer, std::size_t size, const GuestTm* time) { + return SonyAsctimeS(buffer, size, time); +} + +APS5_EXPORT("qlWiRfOJx1A", sceAmprFpclassifyd); +int APS5_VABI sceAmprFpclassifyd(double value) { + return SonyFpclassifyd(value); +} + +int APS5_VABI sceAmprAmmGetUsageStatsData(AmmUsageStatsData* stats) { + if (!stats || stats->sizeInBytes > sizeof(AmmUsageStatsData)) return SCE_KERNEL_ERROR_EINVAL; + AmmUsageStatsData out{}; + out.sizeInBytes = stats->sizeInBytes; + out.numPageTablePoolEntries = AmmPageTablePoolEntries; + out.ringIdleFlags = AmmRingIdleFlags; + std::memcpy(stats, &out, static_cast(stats->sizeInBytes)); + return 0; +} } diff --git a/core/libs/prx/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 695437ff94..bb0eba21ea 100644 --- a/core/libs/prx/libSceAppContent/Export.cpp +++ b/core/libs/prx/libSceAppContent/Export.cpp @@ -2,6 +2,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 #include @@ -49,7 +50,7 @@ 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); } @@ -93,9 +94,18 @@ int APS5_VABI sceAppContentAppParamGetInt(uint32_t param_id, int32_t* value) { } } +int APS5_VABI sceAppContentDownloadDataFormat(const AppContentMountPoint* mount_point) { + if (!mount_point) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (std::strncmp(mount_point->data, DOWNLOAD_MOUNT_POINT, sizeof(mount_point->data)) != 0) 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; + if (!mount_point || !available_space_kb) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (std::strncmp(mount_point->data, DOWNLOAD_MOUNT_POINT, sizeof(mount_point->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; @@ -163,8 +173,106 @@ int APS5_VABI sceAppContentTemporaryDataUnmount(const AppContentMountPoint* moun } -int APS5_VABI sceAppContentAddcontEnqueueDownload(void) { +int APS5_VABI sceAppContentAddcontEnqueueDownload(uint32_t service_label, const NpUnifiedEntitlementLabel* entitlement_label) { + (void)service_label; + if (!entitlement_label) return SCE_APP_CONTENT_ERROR_PARAMETER; + return SCE_APP_CONTENT_ERROR_NOT_FOUND; +} + +int APS5_VABI sceAppContentAddcontDelete(uint32_t service_label, const NpUnifiedEntitlementLabel* entitlement_label) { + (void)service_label; + if (!entitlement_label) return SCE_APP_CONTENT_ERROR_PARAMETER; + return SCE_APP_CONTENT_ERROR_NOT_FOUND; +} + +int APS5_VABI sceAppContentGetAddcontDownloadProgress(uint32_t service_label, const NpUnifiedEntitlementLabel* entitlement_label, void* progress) { + (void)service_label; + if (!entitlement_label || !progress) return SCE_APP_CONTENT_ERROR_PARAMETER; + return SCE_APP_CONTENT_ERROR_NOT_FOUND; +} +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(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } 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..845fd759b0 --- /dev/null +++ b/core/libs/prx/libSceAppInstUtil/Export.cpp @@ -0,0 +1,85 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +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() { + 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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceAppInstUtilAppInstallPkg() { + 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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceAppInstUtilAppUnInstallPat() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceAppInstUtilAppUnInstallTheme() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceAppInstUtilAppUnInstallTypes() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceAppInstUtilGetInstallProgressInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceAppInstUtilGetInstallProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceAppInstUtilGetPrimaryAppSlot() { + 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..791d2fac7e 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,14 @@ if(BUILD_TESTING) set_tests_properties(guest_audio3d_port 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..28854734cc 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,31 @@ 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 port_id, uint32_t* attributes, uint32_t* num_attributes) { + std::lock_guard lock(g_mutex); + if (num_attributes == nullptr) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + constexpr std::uint32_t supported[]{ + AUDIO3D_ATTRIBUTE_LATE_REVERB_LEVEL, + AUDIO3D_ATTRIBUTE_DOWNMIX_SPREAD_RADIUS, + AUDIO3D_ATTRIBUTE_DOWNMIX_SPREAD_HEIGHT_AWARE, + }; + if (attributes == nullptr) { + *num_attributes = std::size(supported); + return 0; + } + const std::uint32_t count = std::min(*num_attributes, std::size(supported)); + std::copy_n(std::begin(supported), count, attributes); + *num_attributes = count; + return 0; +} + int APS5_VABI sceAudio3dPortOpen(int user_id, const Audio3dOpenParameters* parameters, uint32_t* id) { std::lock_guard lock(g_mutex); if (!g_initialized) return AUDIO3D_ERROR_NOT_READY; @@ -143,33 +248,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,10 +346,159 @@ int APS5_VABI sceAudio3dObjectSetAttributes(uint32_t port_id, uint32_t object_id } int APS5_VABI sceAudio3dPortFlush(uint32_t port_id) { + 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; - g_port.advanced = 0; - g_port.playing.clear(); + 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; +} + +int APS5_VABI sceAudio3dGetSpeakerArrayMemorySize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dCreateSpeakerArray(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dDeleteSpeakerArray(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dGetSpeakerArrayMixCoefficients(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dGetSpeakerArrayMixCoefficients2(void) { + NotImplemented_nid_no_patch(__func__); return 0; } 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/CMakeLists.txt b/core/libs/prx/libSceAudioIn/CMakeLists.txt index 405b20d4a9..f25187b9c8 100644 --- a/core/libs/prx/libSceAudioIn/CMakeLists.txt +++ b/core/libs/prx/libSceAudioIn/CMakeLists.txt @@ -1,5 +1,5 @@ add_library(libSceAudioIn SHARED EXCLUDE_FROM_ALL Export.cpp) -target_include_directories(libSceAudioIn PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/SDL2/include) +target_include_directories(libSceAudioIn PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) target_link_libraries(libSceAudioIn PRIVATE SDL2-static) target_link_options(libSceAudioIn PRIVATE LINKER:--exclude-libs,ALL) configure_windows_unwind(libSceAudioIn) diff --git a/core/libs/prx/libSceAudioIn/Export.cpp b/core/libs/prx/libSceAudioIn/Export.cpp index 4ba03302fa..68fa257250 100644 --- a/core/libs/prx/libSceAudioIn/Export.cpp +++ b/core/libs/prx/libSceAudioIn/Export.cpp @@ -9,6 +9,7 @@ #include #include #include +#include namespace { @@ -21,6 +22,7 @@ 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; @@ -47,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; @@ -75,6 +78,28 @@ Port* find(int handle) { return port.used ? &port : nullptr; } +int checkArguments(std::uint32_t type, std::uint32_t index, std::uint32_t len, std::uint32_t freq) { + if (type != 0 && type != 1) return AUDIO_IN_ERROR_INVALID_TYPE; + if (index != 0) return AUDIO_IN_ERROR_INVALID_PARAM; + if (len != 128 && len != 256) return AUDIO_IN_ERROR_INVALID_SIZE; + if (freq != 48000 && freq != 16000) return AUDIO_IN_ERROR_INVALID_FREQ; + 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); + for (std::size_t i = 0; i < g_ports.size(); ++i) { + if (g_ports[i].used) continue; + const auto frameBytes = static_cast(SDL_AUDIO_BITSIZE(format.format) / 8 * format.channels); + const auto grain = std::chrono::duration_cast(std::chrono::microseconds(1000000ull * len / freq)); + g_ports[i] = {true, false, len, frameBytes, openDevice(freq, len, format), grain, {}}; + return static_cast(i + 1); + } + return AUDIO_IN_ERROR_PORT_FULL; +} + std::size_t capture(SDL_AudioDeviceID device, std::uint8_t* dest, std::size_t bytes, Clock::time_point deadline) { while (SDL_GetQueuedAudioSize(device) < bytes && Clock::now() < deadline) PreciseSleepUs(1000); const std::size_t queued = SDL_GetQueuedAudioSize(device); @@ -90,6 +115,10 @@ std::size_t capture(SDL_AudioDeviceID device, std::uint8_t* dest, std::size_t by extern "C" { +int APS5_VABI sceAudioInInit() { + return 0; +} + int APS5_VABI sceAudioInGetSilentState(int handle) { std::lock_guard lock(g_mutex); const Port* port = find(handle); @@ -132,25 +161,8 @@ 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 (type != 0 && type != 1) return AUDIO_IN_ERROR_INVALID_TYPE; - if (index != 0) return AUDIO_IN_ERROR_INVALID_PARAM; - if (len != 128 && len != 256) return AUDIO_IN_ERROR_INVALID_SIZE; - if (freq != 48000 && freq != 16000) return AUDIO_IN_ERROR_INVALID_FREQ; - if (param == 0x10) { - NotImplemented_nid_no_patch(__func__); - return 0; - } - Format format{}; - if (!formatOf(param, format)) return AUDIO_IN_ERROR_INVALID_PARAM; - std::lock_guard lock(g_mutex); - for (std::size_t i = 0; i < g_ports.size(); ++i) { - if (g_ports[i].used) continue; - const auto frameBytes = static_cast(SDL_AUDIO_BITSIZE(format.format) / 8 * format.channels); - const auto grain = std::chrono::duration_cast(std::chrono::microseconds(1000000ull * len / freq)); - g_ports[i] = {true, false, len, frameBytes, openDevice(freq, len, format), grain, {}}; - return static_cast(i + 1); - } - return AUDIO_IN_ERROR_PORT_FULL; + if (const int error = checkArguments(type, index, len, freq); error != 0) return error; + return openPort(len, freq, param); } int32_t APS5_VABI sceAudioInClose(int32_t handle) { @@ -164,31 +176,196 @@ int32_t APS5_VABI sceAudioInClose(int32_t handle) { } 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; - (void)type; - (void)index; - (void)len; - (void)freq; - (void)param; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)user_id; + if (const int error = checkArguments(type, index, len, freq); error != 0) return error; + if (len != 128) return AUDIO_IN_ERROR_INVALID_SIZE; + if (freq != 48000) return AUDIO_IN_ERROR_INVALID_FREQ; + 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; - (void)type; - (void)index; - (void)len; - (void)freq; - (void)param; + (void)user_id; + if (const int error = checkArguments(type, index, len, freq); error != 0 && error != AUDIO_IN_ERROR_INVALID_SIZE) return error; + if (len != 128 && len != 256 && len != MAX_ASYNC_LEN) return AUDIO_IN_ERROR_INVALID_SIZE; + return openPort(len, freq, param); +} + +int32_t APS5_VABI sceAudioInIsPadEmbeddedMic(int32_t handle) { + (void)handle; NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("X+4jdIS75P0", sceAudioInUnknown_X4jdIS75P0); -int32_t APS5_VABI sceAudioInUnknown_X4jdIS75P0(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int32_t APS5_VABI sceAudioInGetGain(int32_t handle, float* gain) { + if (!gain) throw std::runtime_error("sceAudioInGetGain: null gain"); + *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(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInHqOpenEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +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) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetNoiseGateThreshold(void) { + NotImplemented_nid_no_patch(__func__); + 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 c3d0bd85a8..af79a20cf1 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -1,22 +1,96 @@ 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} - ${CMAKE_SOURCE_DIR}/3rdparty/SDL2/include + ${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_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) + configure_windows_unwind(audio_out_initialization_tests) + add_test(NAME audio_out_initialization COMMAND audio_out_initialization_tests) + set_tests_properties(audio_out_initialization PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(audio_out_initialization PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(audio_out2_pad_mix_tests tests/AudioOut2PadMix.cpp src/AudioOut2PadMix.cpp) target_include_directories(audio_out2_pad_mix_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(audio_out2_pad_mix_tests PRIVATE libc) @@ -47,6 +121,46 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(audio_out_ex_tests tests/AudioOutEx.cpp) + target_include_directories(audio_out_ex_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(audio_out_ex_tests PRIVATE libSceAudioOut libc) + configure_windows_unwind(audio_out_ex_tests) + add_test(NAME audio_out_ex COMMAND audio_out_ex_tests) + if(WIN32) + set_tests_properties(audio_out_ex PROPERTIES ENVIRONMENT_MODIFICATION + "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) + configure_windows_unwind(audio_out_sys_hdmi_monitor_info_tests) + add_test(NAME audio_out_sys_hdmi_monitor_info COMMAND audio_out_sys_hdmi_monitor_info_tests) + if(WIN32) + set_tests_properties(audio_out_sys_hdmi_monitor_info PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(audio_out_ex_monitor_info_tests tests/AudioOutExMonitorInfo.cpp) + target_include_directories(audio_out_ex_monitor_info_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(audio_out_ex_monitor_info_tests PRIVATE libSceAudioOut libc) + configure_windows_unwind(audio_out_ex_monitor_info_tests) + add_test(NAME audio_out_ex_monitor_info COMMAND audio_out_ex_monitor_info_tests) + if(WIN32) + set_tests_properties(audio_out_ex_monitor_info PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(audio_out2_latency_tests tests/AudioOut2Latency.cpp) target_include_directories(audio_out2_latency_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(audio_out2_latency_tests PRIVATE libSceAudioOut libc) @@ -66,13 +180,44 @@ if(BUILD_TESTING) set_tests_properties(audio_out2_port_layouts PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(audio_out2_port_buffers_tests tests/AudioOut2PortBuffers.cpp) + target_include_directories(audio_out2_port_buffers_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(audio_out2_port_buffers_tests PRIVATE libSceAudioOut libc) + configure_windows_unwind(audio_out2_port_buffers_tests) + add_test(NAME audio_out2_port_buffers COMMAND audio_out2_port_buffers_tests) + if(WIN32) + 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) configure_windows_unwind(audio_out2_timing_tests) add_test(NAME audio_out2_timing COMMAND audio_out2_timing_tests) if(WIN32) - set_tests_properties(audio_out2_timing PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(audio_out2_timing PROPERTIES RUN_SERIAL TRUE ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + 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/include/AudioOutSdl.hpp b/core/libs/prx/libSceAudioOut/include/AudioOutSdl.hpp new file mode 100644 index 0000000000..f0e8c87cc4 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/include/AudioOutSdl.hpp @@ -0,0 +1,6 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAUDIOOUT_INCLUDE_AUDIOOUTSDL_HPP +#define CORE_LIBS_PRX_LIBSCEAUDIOOUT_INCLUDE_AUDIOOUTSDL_HPP + +int InitializeSdlAudio_nid_no_patch(); + +#endif diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index 5dcdd28210..0eda307e26 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 @@ -8,19 +6,27 @@ #include #include #include +#include #include #include #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; @@ -32,6 +38,20 @@ static constexpr std::uint32_t FORMAT_MASK = 0xFFu; static constexpr std::uint64_t TARGET_LATENCY_US = 40000; 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_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, @@ -80,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; @@ -91,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; @@ -104,7 +133,7 @@ static bool ensureSdlAudio() { if (g_sdlInitialized) { return true; } - if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { + if (InitializeSdlAudio_nid_no_patch() < 0) { return false; } g_sdlInitialized = true; @@ -122,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; } @@ -131,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); @@ -146,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++) { @@ -192,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); @@ -212,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 @@ -277,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 || @@ -292,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() { @@ -328,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) { @@ -376,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++) { @@ -402,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); } @@ -414,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) { @@ -496,6 +599,63 @@ int APS5_VABI sceAudioOutGetPortState(int handle, AudioOutPortState* state) { return 0; } +int APS5_VABI sceAudioOutDeviceIdOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSysClose(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(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; + } + std::memset(info, 0, HDMI_MONITOR_INFO_SIZE); + 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) { std::lock_guard lock(g_mutex); Port* port = getPort(handle); @@ -515,4 +675,57 @@ int APS5_VABI sceAudioOutSetMixLevelPadSpk(int handle, int mixLevel) { return 0; } +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(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); + 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 383ad018f7..e11ee633e5 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp @@ -13,14 +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" - -// 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 "AudioOutShutdown.hpp" +#include "prx/libSceAudioOut/include/AudioOutSdl.hpp" +#include "prx/libScePad/include/PadState.hpp" using Clock = std::chrono::steady_clock; @@ -106,7 +103,7 @@ static std::uint32_t QueueLevel(AudioOut2Context& context, Clock::time_point now static void DeviceCallback(void* userdata, Uint8* stream, int length); static void OpenDevice(AudioOut2Context& context) { - if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { + if (InitializeSdlAudio_nid_no_patch() < 0) { AUDIOOUT2_TRACE("SDL audio init failed: %s\n", SDL_GetError()); return; } @@ -134,7 +131,7 @@ static void CloseDevice(SDL_AudioDeviceID device) { } static void OpenPadDevice(AudioOut2Context& context) { - if (SDL_WasInit(SDL_INIT_AUDIO) == 0 && SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) return; + if (SDL_WasInit(SDL_INIT_AUDIO) == 0 && InitializeSdlAudio_nid_no_patch() < 0) return; const int count = SDL_GetNumAudioDevices(0); for (int index = 0; index < count; index++) { const char* listed = SDL_GetAudioDeviceName(index, 0); @@ -174,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; } @@ -200,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(); } @@ -212,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]); + } } } @@ -255,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) { @@ -282,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)); @@ -296,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++; @@ -311,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); @@ -329,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()); @@ -366,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 96b9ed3315..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; @@ -63,6 +68,9 @@ struct AudioOut2Port { bool int16 = false; const AudioOut2StereoFold* fold = nullptr; const void* data = nullptr; + std::deque queuedData; + const void* lastBuffer = nullptr; + const void* repeatedData = nullptr; float volume[AUDIO_OUT2_PORT_CHANNELS_MAX] = {1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; std::uint64_t dataSets = 0; std::uint64_t attributeTraces = 0; @@ -83,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; @@ -109,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..eaba6e102d --- /dev/null +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp @@ -0,0 +1,69 @@ +#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 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..e48537db95 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) { @@ -14,17 +27,39 @@ int APS5_VABI sceAudioOut2MasteringTerm(void) { } 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 03248d3e0d..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; @@ -23,6 +26,7 @@ static constexpr std::uint64_t ATTRIBUTE_TRACE_EVERY = 2000; // 8 and 9 (a u32, a u32 and a byte on object and voice ports) are not understood and ignored. static constexpr std::uint32_t ATTRIBUTE_DATA = 0; static constexpr std::uint32_t ATTRIBUTE_VOLUME = 1; +static constexpr std::size_t QUEUED_DATA_MAX = 16; static constexpr std::uint32_t FORMAT_CHANNELS_SHIFT = 8; static constexpr std::uint32_t FORMAT_CHANNELS_MASK = 0xFu; @@ -73,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++) { @@ -89,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]; } @@ -98,12 +102,26 @@ 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++) { - const auto& port = g_ports[index]; - if (port.used && port.context == &context && port.data != nullptr && port.channels != 0) grain.push_back({index, port.generation, port.data}); + auto& port = g_ports[index]; + if (!port.used || port.context != &context || port.channels == 0) continue; + const void* data = port.repeatedData; + if (!port.queuedData.empty()) { + data = port.queuedData.front(); + port.queuedData.pop_front(); + } + 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; } @@ -111,13 +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; - const auto route = padOut != nullptr ? AudioOut2RouteForPort(port.type, port.channels) : 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; @@ -158,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; @@ -199,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; @@ -234,6 +266,10 @@ int APS5_VABI sceAudioOut2PortSetAttributes(AudioOut2PortHandle port, const Audi verdict = "null value"; } else if (attribute.attribute_id == ATTRIBUTE_DATA && attribute.value_size == sizeof(entry->data)) { std::memcpy(&entry->data, attribute.value, sizeof(entry->data)); + entry->queuedData.push_back(entry->data); + if (entry->queuedData.size() > QUEUED_DATA_MAX) entry->queuedData.pop_front(); + entry->repeatedData = entry->data != nullptr && (entry->lastBuffer == nullptr || entry->lastBuffer == entry->data) ? entry->data : nullptr; + if (entry->data != nullptr) entry->lastBuffer = entry->data; entry->dataSets++; verdict = "pcm data pointer"; } else if (attribute.attribute_id == ATTRIBUTE_VOLUME && entry->channels != 0 && attribute.value_size == entry->channels * sizeof(float)) { @@ -244,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..26170f4622 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp @@ -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 67d66b44b4..c647f607ea 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp @@ -1,10 +1,13 @@ +#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}; extern "C" { @@ -12,8 +15,9 @@ 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 sceAudioOut2Set3DLatency(int userId, std::uint32_t output) { + (void)userId; + if (output > MAX_SUPPORTED_3D_OUTPUT) return SCE_AUDIO_OUT2_ERROR_INVALID_PARAM; return 0; } @@ -24,13 +28,18 @@ int APS5_VABI sceAudioOut2GetSystemState(AudioOut2SystemState* state) { } int APS5_VABI sceAudioOut2SetSystemDebugState(const AudioOut2SystemDebugStateParam* param) { - (void)param; - NotImplemented_nid_no_patch(__func__); + if (param == nullptr) return static_cast(0x80260502); + 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; } -int APS5_VABI sceAudioOut2UserGetSupportedAttributes(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAudioOut2EnableChat(void) { + if (!g_chatEnabled.exchange(true)) AUDIOOUT2_TRACE("t=%.3f EnableChat: chat audio is allowed alongside the title's; the host mixes no chat audio\n", AudioOut2TraceSeconds()); + return 0; +} + +int APS5_VABI sceAudioOut2DisableChat(void) { + if (g_chatEnabled.exchange(false)) AUDIOOUT2_TRACE("t=%.3f DisableChat: chat audio is no longer allowed alongside the title's\n", AudioOut2TraceSeconds()); return 0; } diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp index a139d22626..a8c42717bf 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp @@ -1,23 +1,44 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -#include +#include +#include +#include +#include static constexpr int SCE_AUDIO_OUT2_ERROR_INVALID_ARGUMENT = static_cast(0x80260502); -static std::atomic g_nextUser{1}; +static std::mutex g_usersLock; +static AudioOut2UserHandle g_nextUser = 1; +static std::unordered_set g_users; extern "C" { int APS5_VABI sceAudioOut2UserCreate(uint32_t user_id, AudioOut2UserHandle* handle) { (void)user_id; if (!handle) return SCE_AUDIO_OUT2_ERROR_INVALID_ARGUMENT; - *handle = g_nextUser.fetch_add(1, std::memory_order_relaxed); + std::lock_guard lock(g_usersLock); + if (g_nextUser == std::numeric_limits::max()) + throw std::runtime_error("AudioOut2: user handles exhausted"); + g_users.insert(g_nextUser); + *handle = g_nextUser++; return 0; } int APS5_VABI sceAudioOut2UserDestroy(AudioOut2UserHandle handle) { - (void)handle; + std::lock_guard lock(g_usersLock); + g_users.erase(handle); + return 0; +} + +int APS5_VABI sceAudioOut2UserGetSupportedAttributes(AudioOut2UserHandle handle, uint32_t* contextAttributes, uint32_t* portAttributes) { + if (!contextAttributes || !portAttributes) + throw std::runtime_error("sceAudioOut2UserGetSupportedAttributes: null output"); + std::lock_guard lock(g_usersLock); + if (!g_users.contains(handle)) + throw std::runtime_error("sceAudioOut2UserGetSupportedAttributes: unknown user handle"); + *contextAttributes = 0; + *portAttributes = (1u << 0) | (1u << 1); return 0; } diff --git a/core/libs/prx/libSceAudioOut/src/AudioOutSdl.cpp b/core/libs/prx/libSceAudioOut/src/AudioOutSdl.cpp new file mode 100644 index 0000000000..45f41f5a43 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/src/AudioOutSdl.cpp @@ -0,0 +1,10 @@ +#include "prx/libSceAudioOut/include/AudioOutSdl.hpp" + +#include "SDL.h" +#include + +int InitializeSdlAudio_nid_no_patch() { + static std::mutex initializationMutex; + std::lock_guard lock(initializationMutex); + return SDL_InitSubSystem(SDL_INIT_AUDIO); +} diff --git a/core/libs/prx/libSceAudioOut/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..02abcf4fd9 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp @@ -0,0 +1,428 @@ +#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 sceAudioOut2MasteringInitEx(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 sceAudioOut2LoTerminate(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 sceAudioOut2InitializeEx(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 5f6eea7b10..9a09ec4fba 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 sceAudioOut2LoInit(); +int APS5_VABI sceAudioOut2LoTerminate(); int APS5_VABI sceAudioOut2Set3DLatency(int, std::uint32_t); int APS5_VABI sceAudioOut2MasteringInit(std::uint32_t); int APS5_VABI sceAudioOut2MasteringTerm(); int APS5_VABI sceAudioOut2MasteringSetParam(const void*, std::uint32_t, std::uint32_t); +int APS5_VABI sceAudioOut2MasteringGetState(void*, std::uint32_t, std::uint64_t); +int APS5_VABI sceAudioOut2SetSystemDebugState(const void*); +int APS5_VABI sceAudioOut2EnableChat(); +int APS5_VABI sceAudioOut2DisableChat(); } static void Require(bool value, const char* message) { @@ -31,16 +39,27 @@ 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 errorInvalidArgument = static_cast(0x80260502); +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) == 0, "output 0 for the system user must be accepted"); + 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) == 0, "output 1 for another user must be accepted"); + Require(sceAudioOut2Set3DLatency(systemUser, 3) == errorInvalidParam, "output 3 must be refused"); + Require(sceAudioOut2Set3DLatency(systemUser, 0xFFFFFFFFu) == errorInvalidParam, "a large output must be refused"); } void TestMasteringInit() { @@ -57,7 +76,53 @@ 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() { + const std::uint64_t param[4] = {1, 0, 0, 0}; + Require(sceAudioOut2SetSystemDebugState(reinterpret_cast(param)) == 0, "a debug state must be accepted"); + Require(sceAudioOut2SetSystemDebugState(reinterpret_cast(param)) == 0, "a debug state must be accepted again"); + Require(sceAudioOut2SetSystemDebugState(nullptr) == errorInvalidArgument, "a null debug state is refused"); +} + +void TestEnableChat() { + Require(sceAudioOut2EnableChat() == 0, "enabling chat must be accepted"); + Require(sceAudioOut2EnableChat() == 0, "enabling chat again must be accepted"); + Require(sceAudioOut2DisableChat() == 0, "disabling chat must be accepted"); + Require(sceAudioOut2DisableChat() == 0, "disabling chat again must be accepted"); + Require(sceAudioOut2EnableChat() == 0, "enabling chat after disabling it must be accepted"); +} + +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(ThrowsRuntimeError([] { sceAudioOut2LoTerminate(); }), "stopping the low-latency service must throw"); } } @@ -68,5 +133,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 new file mode 100644 index 0000000000..b1d5725eca --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp @@ -0,0 +1,303 @@ +#include "SceTypes.hpp" + +#include +#include +#include +#include +#include +#include +#include +#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); +int APS5_VABI sceAudioOut2PortSetAttributes(AudioOut2PortHandle, const AudioOut2Attribute*, std::uint32_t); +} + +static void Require(bool value) { if (!value) std::abort(); } + +static 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 +} + +namespace { + +constexpr std::uint32_t grain = 256; +constexpr std::uint32_t channels = 2; +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 std::uint32_t silentGrains = 32; +constexpr float masterGain = 0.5f; +constexpr float tolerance = 1e-6f; + +void SetData(AudioOut2PortHandle port, const void* data) { + const AudioOut2Attribute attribute{attributeData, 0, &data, sizeof(data)}; + Require(sceAudioOut2PortSetAttributes(port, &attribute, 1) == 0); +} + +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()); + + AudioOut2ContextParam params{}; + Require(sceAudioOut2ContextResetParam(¶ms) == 0); + params.num_grains = grain; + params.queue_depth = queueDepth; + AudioOut2ContextHandle context = 0; + Require(sceAudioOut2ContextCreate(¶ms, nullptr, 0, &context) == 0); + AudioOut2PortParam portParams{}; + portParams.port_type = portTypeMain; + portParams.data_format = formatStereoFloat; + portParams.sampling_freq = frequency; + AudioOut2PortHandle port = 0; + Require(sceAudioOut2PortCreate(context, &portParams, &port) == 0); + feed(context, port); + for (std::uint32_t push = 0; push < silentGrains; push++) { + if (advanced) { + SetData(port, nullptr); + Require(sceAudioOut2ContextAdvance(context) == 0); + } + Require(sceAudioOut2ContextPush(context, 1) == 0); + } + Require(sceAudioOut2PortDestroy(port) == 0); + Require(sceAudioOut2ContextDestroy(context) == 0); + + std::ifstream file(path, std::ios::binary); + const std::vector bytes{std::istreambuf_iterator(file), std::istreambuf_iterator()}; + file.close(); + std::filesystem::remove(path); + Require(bytes.size() % sizeof(float) == 0); + const auto* samples = reinterpret_cast(bytes.data()); + std::size_t first = 0; + std::size_t last = bytes.size() / sizeof(float); + while (first < last && samples[first] == 0.0f) first++; + while (last > first && samples[last - 1] == 0.0f) last--; + return {samples + first, samples + last}; +} + +std::vector Grain(float level) { + std::vector data(static_cast(grain) * channels); + for (std::uint32_t frame = 0; frame < grain; frame++) { + const float sign = frame % 2 == 0 ? 1.0f : -1.0f; + data[frame * channels] = sign * level * static_cast(1 + frame % 5); + data[frame * channels + 1] = -sign * level * static_cast(1 + frame % 3); + } + return data; +} + +void RequirePlayed(const std::vector& played, const std::vector*>& grains) { + Require(played.size() == grains.size() * grain * channels); + for (std::size_t index = 0; index < grains.size(); index++) { + const auto& data = *grains[index]; + for (std::size_t sample = 0; sample < data.size(); sample++) { + Require(std::fabs(played[index * data.size() + sample] - data[sample] * masterGain) <= tolerance); + } + } +} + +void TestBuffersSetBeforeOnePushPlayInOrder() { + const auto first = Grain(0.01f); + const auto second = Grain(0.02f); + const auto third = Grain(0.03f); + const auto played = Play([&](AudioOut2ContextHandle context, AudioOut2PortHandle port) { + SetData(port, first.data()); + SetData(port, second.data()); + SetData(port, third.data()); + Require(sceAudioOut2ContextPush(context, 1) == 0); + Require(sceAudioOut2ContextPush(context, 1) == 0); + Require(sceAudioOut2ContextPush(context, 1) == 0); + }); + RequirePlayed(played, {&first, &second, &third}); +} + +void TestBurstThenGapKeepsEveryBuffer() { + const auto first = Grain(0.01f); + const auto second = Grain(0.02f); + const auto third = Grain(0.03f); + const auto played = Play([&](AudioOut2ContextHandle context, AudioOut2PortHandle port) { + SetData(port, first.data()); + Require(sceAudioOut2ContextPush(context, 1) == 0); + SetData(port, second.data()); + SetData(port, third.data()); + Require(sceAudioOut2ContextPush(context, 1) == 0); + Require(sceAudioOut2ContextPush(context, 1) == 0); + }); + RequirePlayed(played, {&first, &second, &third}); +} + +void TestAdvancingPortWithoutNewBufferPlaysSilence() { + 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(sceAudioOut2ContextPush(context, 1) == 0); + SetData(port, second.data()); + Require(sceAudioOut2ContextPush(context, 1) == 0); + }); + RequirePlayed(played, {&first, &second}); +} + +void TestRewrittenBufferRepeats() { + auto buffer = Grain(0.01f); + const auto first = buffer; + const auto second = Grain(0.02f); + const auto played = Play([&](AudioOut2ContextHandle context, AudioOut2PortHandle port) { + SetData(port, buffer.data()); + Require(sceAudioOut2ContextPush(context, 1) == 0); + Require(sceAudioOut2ContextPush(context, 1) == 0); + buffer = second; + SetData(port, nullptr); + }); + RequirePlayed(played, {&first, &second}); +} + +void TestAdvanceRetainsRewrittenBuffer() { + auto buffer = Grain(0.01f); + const auto first = buffer; + const auto second = Grain(0.02f); + const auto third = Grain(0.03f); + const auto played = Play([&](AudioOut2ContextHandle context, AudioOut2PortHandle port) { + SetData(port, buffer.data()); + Require(sceAudioOut2ContextAdvance(context) == 0); + buffer = second; + SetData(port, buffer.data()); + Require(sceAudioOut2ContextAdvance(context) == 0); + buffer = third; + Require(sceAudioOut2ContextPush(context, 1) == 0); + Require(sceAudioOut2ContextPush(context, 1) == 0); + }, true); + RequirePlayed(played, {&first, &second}); +} + +void TestAdvanceRetainsVolume() { + const auto buffer = Grain(0.02f); + auto reduced = buffer; + for (auto& sample : reduced) sample *= 0.5f; + const auto played = Play([&](AudioOut2ContextHandle context, AudioOut2PortHandle port) { + SetData(port, buffer.data()); + Require(sceAudioOut2ContextAdvance(context) == 0); + const std::array 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 56648e1b03..1cad3a7422 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2PortLayouts.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2PortLayouts.cpp @@ -9,10 +9,15 @@ #include #include #include +#include #include +#include #include extern "C" { +int APS5_VABI sceAudioOut2UserCreate(std::uint32_t, AudioOut2UserHandle*); +int APS5_VABI sceAudioOut2UserDestroy(AudioOut2UserHandle); +int APS5_VABI sceAudioOut2UserGetSupportedAttributes(AudioOut2UserHandle, std::uint32_t*, std::uint32_t*); int APS5_VABI sceAudioOut2ContextResetParam(AudioOut2ContextParam*); int APS5_VABI sceAudioOut2ContextCreate(const AudioOut2ContextParam*, void*, std::size_t, AudioOut2ContextHandle*); int APS5_VABI sceAudioOut2ContextDestroy(AudioOut2ContextHandle); @@ -37,6 +42,8 @@ namespace { constexpr std::uint32_t grain = 256; constexpr std::uint32_t frequency = 48000; constexpr std::uint16_t portTypeMain = 0; +constexpr std::uint16_t portTypePadSpeaker = 0x3; +constexpr std::uint16_t portTypePadVibration = 0x6; constexpr std::uint32_t attributeData = 0; constexpr std::uint32_t attributeVolume = 1; constexpr std::uint32_t formatFloat = 0; @@ -46,6 +53,51 @@ constexpr float masterGain = 0.5f; constexpr float fold = 0.70710678f; constexpr float tolerance = 1e-5f; +void TestUserAttributes() { + struct Output { + std::uint32_t before = 0x12345678u; + std::uint32_t value = 0xFFFFFFFFu; + std::uint32_t after = 0x87654321u; + } context, port; + AudioOut2UserHandle first = 0; + AudioOut2UserHandle second = 0; + Require(sceAudioOut2UserCreate(1, &first) == 0); + Require(sceAudioOut2UserCreate(2, &second) == 0 && second != first); + Require(sceAudioOut2UserGetSupportedAttributes(first, &context.value, &port.value) == 0); + Require(context.value == 0 && port.value == ((1u << attributeData) | (1u << attributeVolume))); + Require(context.before == 0x12345678u && context.after == 0x87654321u); + Require(port.before == 0x12345678u && port.after == 0x87654321u); + const auto rejects = [&](AudioOut2UserHandle handle, bool nullContext, bool nullPort) { + context.value = port.value = 0xFFFFFFFFu; + bool threw = false; + try { + sceAudioOut2UserGetSupportedAttributes(handle, nullContext ? nullptr : &context.value, nullPort ? nullptr : &port.value); + } catch (const std::runtime_error& error) { + threw = std::string(error.what()).find("sceAudioOut2UserGetSupportedAttributes") != std::string::npos; + } + Require(threw && context.value == 0xFFFFFFFFu && port.value == 0xFFFFFFFFu); + }; + rejects(first, true, false); + rejects(first, false, true); + rejects(first, true, true); + rejects(0, false, false); + rejects(std::numeric_limits::max(), false, false); + Require(sceAudioOut2UserDestroy(first) == 0); + rejects(first, false, false); + std::thread([&] { + Require(sceAudioOut2UserGetSupportedAttributes(second, &context.value, &port.value) == 0); + }).join(); + Require(context.value == 0 && port.value == 3); + AudioOut2UserHandle next = 0; + Require(sceAudioOut2UserCreate(1, &next) == 0 && next != first && next != second); + rejects(first, false, false); + Require(sceAudioOut2UserGetSupportedAttributes(next, &context.value, &port.value) == 0); + Require(context.value == 0 && port.value == 3); + Require(sceAudioOut2UserDestroy(next) == 0 && sceAudioOut2UserDestroy(second) == 0); + Require(context.before == 0x12345678u && context.after == 0x87654321u); + Require(port.before == 0x12345678u && port.after == 0x87654321u); +} + struct Layout { std::uint32_t channels; std::vector left; @@ -73,9 +125,9 @@ AudioOut2ContextHandle CreateContext() { return context; } -int CreatePort(AudioOut2ContextHandle context, std::uint32_t format, AudioOut2PortHandle* port) { +int CreatePort(AudioOut2ContextHandle context, std::uint32_t format, AudioOut2PortHandle* port, std::uint16_t type = portTypeMain) { AudioOut2PortParam params{}; - params.port_type = portTypeMain; + params.port_type = type; params.data_format = format; params.sampling_freq = frequency; return sceAudioOut2PortCreate(context, ¶ms, port); @@ -91,20 +143,31 @@ void SetVolume(AudioOut2PortHandle port, const std::vector& volume) { Require(sceAudioOut2PortSetAttributes(port, &attribute, 1) == 0); } -std::vector Play(std::uint32_t format, const void* data, const std::vector& volume) { +struct PortData { + std::uint16_t type; + std::uint32_t format; + const void* data; + std::vector volume; +}; + +std::vector Play(const std::vector& inputs) { const auto path = std::filesystem::temp_directory_path() / ("anyps5_audio_out2_port_layouts-" + std::to_string(std::random_device{}()) + ".raw"); std::filesystem::remove(path); SetEnvironment("SDL_DISKAUDIOFILE", path.string()); const auto context = CreateContext(); - AudioOut2PortHandle port = 0; - Require(CreatePort(context, format, &port) == 0); - SetVolume(port, volume); - SetData(port, data); + std::vector ports; + for (const auto& input : inputs) { + AudioOut2PortHandle port = 0; + Require(CreatePort(context, input.format, &port, input.type) == 0); + SetVolume(port, input.volume); + SetData(port, input.data); + ports.push_back(port); + } Require(sceAudioOut2ContextPush(context, 1) == 0); - SetData(port, nullptr); + for (const auto port : ports) SetData(port, nullptr); for (std::uint32_t push = 0; push < silentGrains; push++) Require(sceAudioOut2ContextPush(context, 1) == 0); - Require(sceAudioOut2PortDestroy(port) == 0); + for (const auto port : ports) Require(sceAudioOut2PortDestroy(port) == 0); Require(sceAudioOut2ContextDestroy(context) == 0); std::ifstream file(path, std::ios::binary); @@ -121,6 +184,10 @@ std::vector Play(std::uint32_t format, const void* data, const std::vecto return {samples + first, samples + last}; } +std::vector Play(std::uint32_t format, const void* data, const std::vector& volume) { + return Play({{portTypeMain, format, data, volume}}); +} + float Sample(std::uint32_t channel, std::uint32_t frame) { const float sign = frame % 2 == 0 ? 1.0f : -1.0f; return sign * 0.01f * static_cast(channel + 1) * static_cast(1 + frame % 5); @@ -204,6 +271,18 @@ void TestDroppedLfe() { RequireFold(Play(Format(surround714.channels, formatFloat), data.data(), volume), surround714, expected, volume, 1.0f); } +void TestPadPortsWithoutPadDevice() { + const auto speaker = FloatGrain(mono.channels); + std::vector vibration(static_cast(grain) * stereo.channels, 0.75f); + const std::vector unity{1.0f, 1.0f}; + const auto played = Play({ + {portTypePadVibration, Format(stereo.channels, formatFloat), vibration.data(), unity}, + {portTypePadSpeaker, Format(mono.channels, formatFloat), speaker.data(), {1.0f}}, + }); + RequireFold(played, mono, speaker, {1.0f}, 1.0f); + Require(Play({{portTypePadVibration, Format(stereo.channels, formatFloat), vibration.data(), unity}}).empty()); +} + void TestMeasuredMainPortFormats() { struct Case { std::uint32_t format; @@ -253,6 +332,7 @@ void TestMeasuredMainPortFormats() { } int main() { + TestUserAttributes(); SetEnvironment("SDL_AUDIODRIVER", "disk"); for (const Layout* layout : {&mono, &stereo, &surround71, &surround714}) { TestFloatLayout(*layout); @@ -270,6 +350,7 @@ int main() { } TestHeightChannels(); TestDroppedLfe(); + TestPadPortsWithoutPadDevice(); TestMeasuredMainPortFormats(); return 0; } 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/AudioOutEx.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutEx.cpp new file mode 100644 index 0000000000..b96f9dd570 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutEx.cpp @@ -0,0 +1,66 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include + +extern "C" { +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); +} + +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 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(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 new file mode 100644 index 0000000000..fecdf065e1 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutExMonitorInfo.cpp @@ -0,0 +1,71 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceAudioOutExGetMonitorInfo(int, int, void*, std::uint32_t); +} + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +namespace { + +constexpr std::uint32_t titleSize = 0x400; +constexpr std::uint32_t minimumSize = 0xA0; +constexpr std::size_t descriptorCountOffset = 0x98; +constexpr std::size_t guardSize = 16; +constexpr std::uint8_t untouched = 0xA5; +constexpr int invalidPointer = static_cast(0x80260004); + +bool Throws(int type, int index, std::uint32_t size, const std::string& expected) { + std::array info{}; + try { + sceAudioOutExGetMonitorInfo(type, index, info.data(), size); + } catch (const std::runtime_error& error) { + if (std::string(error.what()) == expected) return true; + std::fprintf(stderr, "threw: %s\n", error.what()); + return false; + } + return false; +} + +void CheckEmpty(std::uint32_t size) { + std::array buffer{}; + buffer.fill(untouched); + Require(sceAudioOutExGetMonitorInfo(0, 0, buffer.data(), size) == 0, "the monitor query must succeed"); + for (std::uint32_t i = 0; i < size; i++) { + Require(buffer[i] == 0, "the monitor info must describe a sink without audio descriptors"); + } + std::uint32_t descriptors = 0xFFFFFFFFu; + std::memcpy(&descriptors, buffer.data() + descriptorCountOffset, sizeof(descriptors)); + Require(descriptors == 0, "the audio descriptor count must be 0"); + for (std::size_t i = size; i < buffer.size(); i++) { + Require(buffer[i] == untouched, "nothing past the declared size may be written"); + } +} + +} + +int main() { + CheckEmpty(titleSize); + CheckEmpty(minimumSize); + Require(sceAudioOutExGetMonitorInfo(0, 0, nullptr, titleSize) == invalidPointer, + "a null buffer must report an invalid pointer"); + Require(Throws(0, 0, minimumSize - 1, "sceAudioOutExGetMonitorInfo: size 159 not supported"), + "a buffer without room for the descriptor count must throw"); + Require(Throws(1, 0, titleSize, "sceAudioOutExGetMonitorInfo: monitor 1, 0 not supported"), + "a nonzero first argument must throw"); + Require(Throws(0, 1, titleSize, "sceAudioOutExGetMonitorInfo: monitor 0, 1 not supported"), + "a nonzero second argument must throw"); + return 0; +} diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutInitialization.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutInitialization.cpp new file mode 100644 index 0000000000..8ae9cd327c --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutInitialization.cpp @@ -0,0 +1,65 @@ +#define SDL_MAIN_HANDLED +#include "prx/libSceAudioOut/include/AudioOutSdl.hpp" +#include "SDL.h" + +#include +#include +#include +#include +#include +#include + +namespace { +std::mutex stateMutex; +std::condition_variable stateChanged; +int calls = 0; +int active = 0; +int peak = 0; +bool secondEntered = false; +bool secondReady = false; +bool validFlags = true; +} + +extern "C" int SDLCALL SDL_InitSubSystem(Uint32 flags) { + std::unique_lock lock(stateMutex); + validFlags = validFlags && flags == SDL_INIT_AUDIO; + const int call = ++calls; + ++active; + if (active > peak) peak = active; + stateChanged.notify_all(); + if (call == 1) { + stateChanged.wait(lock, [] { return secondReady; }); + stateChanged.wait_for(lock, std::chrono::milliseconds(500), [] { return secondEntered; }); + } else { + secondEntered = true; + stateChanged.notify_all(); + } + --active; + return call == 1 ? -1 : 0; +} + +int main() { + int firstResult = 99; + int secondResult = 99; + std::thread first([&] { firstResult = InitializeSdlAudio_nid_no_patch(); }); + { + std::unique_lock lock(stateMutex); + stateChanged.wait(lock, [] { return calls == 1; }); + } + std::thread second([&] { + { + std::lock_guard lock(stateMutex); + secondReady = true; + stateChanged.notify_all(); + } + secondResult = InitializeSdlAudio_nid_no_patch(); + }); + first.join(); + second.join(); + if (peak != 1 || calls != 2 || active != 0 || !validFlags || firstResult != -1 || secondResult != 0) { + std::fprintf(stderr, "audio initialization overlap or return-value mismatch: peak=%d calls=%d results=%d,%d\n", peak, calls, firstResult, secondResult); + return 1; + } + std::puts("Serialized audio initialization: PASS"); + return 0; +} diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutLastOutputTime.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutLastOutputTime.cpp index c06cc14d1d..e6cdeed0d7 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOutLastOutputTime.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutLastOutputTime.cpp @@ -156,6 +156,48 @@ void TestErrors() { Require(destination == untouched, "a rejected call must not write the destination"); } +void TestIndependentPortOutputDuringWait(bool batch) { + const std::vector longBlock(frequency, 256); + const std::vector shortBlock(frames, 256); + const int waiting = sceAudioOutOpen(user, portTypeVibration, 0, frequency, frequency, formatS16Mono); + const int independent = Open(portTypeVibration); + Require(waiting > 0, "waiting port must open"); + Require(sceAudioOutOutput(waiting, longBlock.data()) == static_cast(frequency), "initial output failed"); + std::atomic started = false; + std::thread output([&] { + started = true; + AudioOutOutputParam param{waiting, longBlock.data()}; + const int result = batch ? sceAudioOutOutputs(¶m, 1) : sceAudioOutOutput(waiting, longBlock.data()); + Require(result == static_cast(frequency), "paced output failed"); + }); + while (!started) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + const auto before = std::chrono::steady_clock::now(); + Require(sceAudioOutOutput(independent, shortBlock.data()) == static_cast(frames), "independent output failed"); + const auto elapsed = std::chrono::steady_clock::now() - before; + output.join(); + Require(sceAudioOutClose(waiting) == 0 && sceAudioOutClose(independent) == 0, "ports must close"); + std::fprintf(stderr, "Independent port output waited %lld ms\n", static_cast(std::chrono::duration_cast(elapsed).count())); + Require(elapsed < std::chrono::milliseconds(100), "pacing one audio port must not block output on another port"); +} + +void TestOverlappingOutputBatches() { + const std::vector block(frames, 256); + const int first = Open(portTypeVibration); + const int second = Open(portTypeVibration); + AudioOutOutputParam duplicate[2] = {{first, block.data()}, {first, block.data()}}; + Require(sceAudioOutOutputs(duplicate, 2) == static_cast(frames), "duplicate handles in a batch must not deadlock"); + const auto output = [&](bool reverse) { + AudioOutOutputParam params[2] = {{reverse ? second : first, block.data()}, {reverse ? first : second, block.data()}}; + for (int i = 0; i < 12; ++i) Require(sceAudioOutOutputs(params, 2) == static_cast(frames), "overlapping output batch failed"); + }; + std::thread forward(output, false); + std::thread reverse(output, true); + forward.join(); + reverse.join(); + Require(sceAudioOutClose(first) == 0 && sceAudioOutClose(second) == 0, "batch ports must close"); +} + void TestQueriesDuringOutput(bool batch, bool drain) { const std::vector block(frequency, 256); const int handle = sceAudioOutOpen(user, portTypeVibration, 0, frequency, frequency, formatS16Mono); @@ -196,6 +238,9 @@ int main() { TestReopenedPort(); TestErrors(); TestPortWithoutDeviceKeepsRealTime(); + TestIndependentPortOutputDuringWait(false); + TestIndependentPortOutputDuringWait(true); + TestOverlappingOutputBatches(); TestQueriesDuringOutput(false, false); TestQueriesDuringOutput(false, true); TestQueriesDuringOutput(true, false); diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutPortLocks.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutPortLocks.cpp new file mode 100644 index 0000000000..107a2ef2f5 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutPortLocks.cpp @@ -0,0 +1,135 @@ +#include "SDL.h" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceAudioOutOpen(int, int, int, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceAudioOutOutput(int, const void*); +int APS5_VABI sceAudioOutClose(int); +} + +namespace { + +constexpr std::uint32_t Grain = 256; +std::atomic nextDevice{2}; +std::atomic blocked{false}; +std::atomic released{false}; +std::atomic clockQueries{0}; +std::atomic clears{0}; +std::array, 4> queued{}; +std::array, 4> closed{}; +std::mutex stateMutex; +std::condition_variable changed; + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "AudioOut port locks: %s\n", message); + std::abort(); +} + +bool WaitFor(const std::atomic& condition, std::chrono::milliseconds timeout) { + std::unique_lock lock(stateMutex); + return changed.wait_for(lock, timeout, [&] { return condition.load(std::memory_order_acquire); }); +} + +} + +extern "C" std::uint64_t APS5_VABI sceKernelGetProcessTime(void) { + clockQueries.fetch_add(1, std::memory_order_relaxed); + return 1; +} + +extern "C" int SDLCALL SDL_InitSubSystem(Uint32) { return 0; } +extern "C" Uint32 SDLCALL SDL_WasInit(Uint32) { return SDL_INIT_AUDIO; } +extern "C" void SDLCALL SDL_PauseAudioDevice(SDL_AudioDeviceID, int) {} + +extern "C" SDL_AudioDeviceID SDLCALL SDL_OpenAudioDevice(const char*, int, const SDL_AudioSpec* desired, SDL_AudioSpec* obtained, int) { + *obtained = *desired; + return nextDevice.fetch_add(1, std::memory_order_relaxed); +} + +extern "C" Uint32 SDLCALL SDL_GetQueuedAudioSize(SDL_AudioDeviceID device) { + Require(device == 2 || device == 3, "unknown device polled"); + if (device == 2 && !released.load(std::memory_order_acquire)) { + blocked.store(true, std::memory_order_release); + changed.notify_all(); + return 1u << 20; + } + return 0; +} + +extern "C" void SDLCALL SDL_ClearQueuedAudio(SDL_AudioDeviceID device) { + if (device == 2 && !released.load(std::memory_order_acquire)) { + clears.fetch_add(1, std::memory_order_relaxed); + released.store(true, std::memory_order_release); + } +} + +extern "C" int SDLCALL SDL_QueueAudio(SDL_AudioDeviceID device, const void* data, Uint32 bytes) { + Require(device == 2 || device == 3, "unknown device queued"); + Require(!closed[device].load(std::memory_order_acquire), "output used a closed device"); + Require(bytes == Grain * sizeof(std::int16_t), "output size changed"); + const auto* samples = static_cast(data); + const auto expected = device == 2 ? 1000 : 2000; + for (std::uint32_t i = 0; i < Grain; ++i) Require(samples[i] == expected, "output samples changed"); + queued[device].fetch_add(1, std::memory_order_relaxed); + return 0; +} + +extern "C" void SDLCALL SDL_CloseAudioDevice(SDL_AudioDeviceID device) { + Require(device == 2 || device == 3, "unknown device closed"); + Require(!closed[device].exchange(true, std::memory_order_acq_rel), "device closed twice"); +} + +int main(int argc, char** argv) { + Require(argc == 2, "expected a test mode"); + const std::string_view mode(argv[1]); + const bool unrelatedOutput = mode == "output"; + const bool unrelatedClose = mode == "close"; + const bool samePortClose = mode == "same-close"; + Require(unrelatedOutput || unrelatedClose || samePortClose, "unknown test mode"); + const int first = sceAudioOutOpen(0, 0, 0, Grain, 48000, 0); + const int second = sceAudioOutOpen(0, 0, 0, Grain, 48000, 0); + Require(first > 0 && second > 0 && first != second, "port creation failed"); + std::array firstSamples; + std::array secondSamples; + firstSamples.fill(1000); + secondSamples.fill(2000); + int firstResult = -1; + int secondResult = -1; + std::atomic secondStarted{false}; + std::atomic secondDone{false}; + std::thread producer([&] { firstResult = sceAudioOutOutput(first, firstSamples.data()); }); + Require(WaitFor(blocked, std::chrono::seconds(2)), "first port did not enter backpressure"); + std::thread other([&] { + secondStarted.store(true, std::memory_order_release); + changed.notify_all(); + secondResult = unrelatedOutput ? sceAudioOutOutput(second, secondSamples.data()) : sceAudioOutClose(samePortClose ? first : second); + secondDone.store(true, std::memory_order_release); + changed.notify_all(); + }); + Require(WaitFor(secondStarted, std::chrono::seconds(2)), "second operation did not start"); + const bool completedWhileBlocked = WaitFor(secondDone, std::chrono::milliseconds(samePortClose ? 150 : 2000)); + released.store(true, std::memory_order_release); + producer.join(); + other.join(); + Require(clockQueries.load() != 0 && clears.load() == 0, "drain timeout invalidated the controlled wait"); + Require(firstResult == static_cast(Grain), "blocked output failed"); + Require(secondResult == (unrelatedOutput ? static_cast(Grain) : 0), "second operation failed"); + Require(completedWhileBlocked != samePortClose, "port output/close lock scope is wrong"); + Require(queued[2].load() == 1 && queued[3].load() == (unrelatedOutput ? 1u : 0u), "unexpected queued output count"); + if (!samePortClose) Require(sceAudioOutClose(first) == 0, "first port close failed"); + if (!unrelatedClose) Require(sceAudioOutClose(second) == 0, "second port close failed"); + Require(closed[2].load() && closed[3].load(), "a device remained open"); + return 0; +} diff --git a/core/libs/prx/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 new file mode 100644 index 0000000000..ce23a664d7 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutSysHdmiMonitorInfo.cpp @@ -0,0 +1,71 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceAudioOutSysGetHdmiMonitorInfo(int, void*, std::uint32_t); +} + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +namespace { + +constexpr int monitorTypeHdmi = 1; +constexpr std::uint32_t infoSize = 0x180; +constexpr std::size_t descriptorCountOffset = 0x94; +constexpr std::size_t guardSize = 16; +constexpr std::uint8_t untouched = 0xA5; +constexpr int invalidPointer = static_cast(0x80260004); + +bool Throws(int type, std::uint32_t size, const std::string& expected) { + std::array info{}; + try { + sceAudioOutSysGetHdmiMonitorInfo(type, 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; +} + +} + +int main() { + std::array buffer{}; + buffer.fill(untouched); + Require(sceAudioOutSysGetHdmiMonitorInfo(monitorTypeHdmi, buffer.data(), infoSize) == 0, + "an HDMI query with a 0x180-byte buffer must succeed"); + for (std::uint32_t i = 0; i < infoSize; i++) { + Require(buffer[i] == 0, "the monitor info must describe a sink without a name or audio descriptors"); + } + std::uint32_t descriptors = 0xFFFFFFFFu; + std::memcpy(&descriptors, buffer.data() + descriptorCountOffset, sizeof(descriptors)); + Require(descriptors == 0, "the audio descriptor count must be 0"); + for (std::size_t i = infoSize; i < buffer.size(); i++) { + Require(buffer[i] == untouched, "nothing past 0x180 bytes may be written"); + } + + 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"); + 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"); + 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 31904b0c56..602d531cfa 100644 --- a/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp +++ b/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp @@ -81,6 +81,7 @@ std::int32_t APS5_VABI sceAudioPropagationSourceGetAudioPath(AudioPropagationHan 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 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); } diff --git a/core/libs/prx/libSceAudioPropagation/Export.cpp b/core/libs/prx/libSceAudioPropagation/Export.cpp index 59e0a7b18f..474117fe6f 100644 --- a/core/libs/prx/libSceAudioPropagation/Export.cpp +++ b/core/libs/prx/libSceAudioPropagation/Export.cpp @@ -1,4 +1,5 @@ #include "AudioPropagation.hpp" +#include "prx/libc/include/General.hpp" #include #include @@ -291,6 +292,14 @@ std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPaths(AudioPropagationHa return 0; } +std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPath(AudioPropagationHandle path, const void* data, float gain) { + static_cast(path); + static_cast(data); + static_cast(gain); + NotImplemented_nid_no_patch(__func__); + return 0; +} + std::int32_t APS5_VABI sceAudioPropagationSourceRender(AudioPropagationHandle system, const RenderInfo* infos, std::uint32_t count) { require(infos != nullptr && count != 0, __func__); auto& state = registry(); @@ -303,7 +312,106 @@ std::int32_t APS5_VABI sceAudioPropagationSourceRender(AudioPropagationHandle sy require(info.output != nullptr && info.outputSize != 0, __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); + return 0; +} + +int APS5_VABI sceAudioPropagationPathGetNumPoints() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioPropagationReportApi() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioPropagationResetAttributes() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioPropagationSourceQueryInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioPropagationSystemLock() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioPropagationSystemQueryInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioPropagationAudioPathInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioPropagationAudioPathPointInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioPropagationMaterialInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioPropagationPortalSettingsInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioPropagationRayInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioPropagationSourceRenderInfoInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioPropagationSourceSetAudioPathsParamInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioPropagationSystem() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioPropagationSystemMemoryInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioPropagationSystemOptionInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioPropagationReportRegisterHandler() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioPropagationReportUnregisterHandler() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioPropagationSystemEnumHandles() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioPropagationSystemUnlock() { + 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..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceAudioSystem/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/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..fc1a113e4f 100644 --- a/core/libs/prx/libSceAudiodec/Export.cpp +++ b/core/libs/prx/libSceAudiodec/Export.cpp @@ -139,7 +139,7 @@ std::int32_t createAac(const AudiodecParamM4aac* param, AudiodecM4aacInfo* info, if (param->ui_max_channels > 8) return ERROR_M4AAC_INVALID_MAX_CHANNELS; if (param->ui_enable_heaac > 1) return ERROR_M4AAC_INVALID_ENABLE_HEAAC; const std::uint32_t channels = param->ui_max_channels == 0 ? 2 : param->ui_max_channels; - instance.decoder = Audiodec::CreateAac(param->i_bw_pcm, param->ui_config_number == M4AAC_CONFIG_ADTS, param->ui_sampling_freq_index, channels); + instance.decoder = Audiodec::CreateAac(param->i_bw_pcm, param->ui_config_number == M4AAC_CONFIG_ADTS, param->ui_sampling_freq_index, channels, param->ui_enable_heaac == 1); instance.channels = channels; instance.sampleRate = sampleRate; info->ui_sampling_freq = sampleRate; diff --git a/core/libs/prx/libSceAudiodec/src/Codecs.cpp b/core/libs/prx/libSceAudiodec/src/Codecs.cpp index aca46d88d1..c3324c3b1a 100644 --- a/core/libs/prx/libSceAudiodec/src/Codecs.cpp +++ b/core/libs/prx/libSceAudiodec/src/Codecs.cpp @@ -122,35 +122,103 @@ public: } }; +struct AacLayout { + std::uint32_t objectType; + std::uint32_t samplingIndex; + std::uint32_t channelConfiguration; + bool operator==(const AacLayout&) const = default; +}; + +enum class Sbr { + Implicit, + Absent, + WithoutParametricStereo, +}; + +std::vector audioSpecificConfig(const AacLayout& layout, Sbr sbr, std::uint32_t sbrSampleRate) { + std::vector bytes; + std::size_t bits = 0; + const auto put = [&](std::uint32_t value, int width) { + for (int bit = width - 1; bit >= 0; --bit, ++bits) { + if (bits % 8 == 0) bytes.push_back(0); + bytes.back() |= static_cast((value >> bit & 1) << (7 - bits % 8)); + } + }; + put(layout.objectType, 5); + put(layout.samplingIndex, 4); + put(layout.channelConfiguration, 4); + put(0, 3); + if (sbr == Sbr::Implicit) return bytes; + put(0x2B7, 11); + put(5, 5); + put(sbr == Sbr::WithoutParametricStereo, 1); + if (sbr == Sbr::WithoutParametricStereo) { + const auto* rate = std::find(std::begin(AAC_SAMPLE_RATES), std::end(AAC_SAMPLE_RATES), sbrSampleRate); + if (rate == std::end(AAC_SAMPLE_RATES)) { + put(15, 4); + put(sbrSampleRate, 24); + } else { + put(static_cast(rate - std::begin(AAC_SAMPLE_RATES)), 4); + } + put(0x548, 11); + put(0, 1); + } + return bytes; +} + class AacDecoder final : public FfmpegDecoder { public: - AacDecoder(std::int32_t wordSize, bool adts, std::vector> configs) - : FfmpegDecoder(AV_CODEC_ID_AAC, wordSize, configs.empty() ? std::vector{} : configs.front()), adts(adts), configs(std::move(configs)) {} + AacDecoder(std::int32_t wordSize, bool adts, bool highEfficiency, std::vector layouts) + : FfmpegDecoder(AV_CODEC_ID_AAC, wordSize, layouts.empty() ? std::vector{} : audioSpecificConfig(layouts.front(), highEfficiency ? Sbr::Implicit : Sbr::Absent, 0)), + adts(adts), highEfficiency(highEfficiency), sbr(highEfficiency ? Sbr::Implicit : Sbr::Absent), layouts(std::move(layouts)) {} DecodeResult Decode(const std::uint8_t* data, std::size_t size, std::uint8_t* pcm, std::size_t pcmSize) override { - std::size_t frameBytes = size; - if (adts) { - if (size < 7 || data[0] != 0xFF || (data[1] & 0xF6) != 0xF0) return {DecodeStatus::InvalidData}; - frameBytes = static_cast((data[3] & 3) << 11 | data[4] << 3 | data[5] >> 5); - if (frameBytes < 7) return {DecodeStatus::InvalidData}; - if (frameBytes > size) return {DecodeStatus::PartialInput}; - } - if (configs.size() <= 1) return decodePacket(data, frameBytes, pcm, pcmSize); - for (std::size_t index = 0; index < configs.size(); ++index) { - if (index != 0) reopen(configs[index]); - const DecodeResult result = decodePacket(data, frameBytes, pcm, pcmSize); - if (result.status != DecodeStatus::InvalidData) { - configs = {configs[index]}; - return result; + if (!adts) return decodeAccessUnit(data, size, pcm, pcmSize); + if (size < 7 || data[0] != 0xFF || (data[1] & 0xF6) != 0xF0) return {DecodeStatus::InvalidData}; + const auto frameBytes = static_cast((data[3] & 3) << 11 | data[4] << 3 | data[5] >> 5); + const std::size_t headerBytes = (data[1] & 1) != 0 ? 7 : 9; + if (frameBytes <= headerBytes) return {DecodeStatus::InvalidData}; + if (frameBytes > size) return {DecodeStatus::PartialInput}; + const AacLayout layout{(data[2] >> 6) + 1u, data[2] >> 2 & 15u, (data[2] & 1u) << 2 | data[3] >> 6}; + if (layout.channelConfiguration == 0) { + if (!layouts.empty()) { + layouts.clear(); + reopen({}); } + return decodePacket(data, frameBytes, pcm, pcmSize); } - reopen(configs.front()); - return {DecodeStatus::InvalidData}; + if (layouts.empty() || layouts.front() != layout) { + layouts = {layout}; + sbr = highEfficiency ? Sbr::Implicit : Sbr::Absent; + reopen(audioSpecificConfig(layout, sbr, 0)); + } + DecodeResult result = decodeAccessUnit(data + headerBytes, frameBytes - headerBytes, pcm, pcmSize); + result.consumed = frameBytes; + return result; } private: + DecodeResult decodeAccessUnit(const std::uint8_t* data, std::size_t size, std::uint8_t* pcm, std::size_t pcmSize) { + for (std::size_t index = 0; index < layouts.size(); ++index) { + if (index != 0) reopen(audioSpecificConfig(layouts[index], sbr, 0)); + DecodeResult result = decodePacket(data, size, pcm, pcmSize); + if (result.status == DecodeStatus::InvalidData) continue; + layouts = {layouts[index]}; + if (sbr == Sbr::Implicit && layouts.front().channelConfiguration == 1 && result.channels > 1) { + sbr = Sbr::WithoutParametricStereo; + reopen(audioSpecificConfig(layouts.front(), sbr, result.sampleRate)); + result = decodePacket(data, size, pcm, pcmSize); + } + return result; + } + if (layouts.size() > 1) reopen(audioSpecificConfig(layouts.front(), sbr, 0)); + return {DecodeStatus::InvalidData}; + } + bool adts; - std::vector> configs; + bool highEfficiency; + Sbr sbr; + std::vector layouts; }; class At9Decoder final : public Decoder { @@ -239,20 +307,16 @@ std::unique_ptr CreateMp3(std::int32_t wordSize) { return std::make_unique(wordSize); } -std::unique_ptr CreateAac(std::int32_t wordSize, bool adts, std::uint32_t samplingFreqIndex, std::uint32_t channels) { - std::vector> configs; +std::unique_ptr CreateAac(std::int32_t wordSize, bool adts, std::uint32_t samplingFreqIndex, std::uint32_t channels, bool highEfficiency) { + std::vector layouts; if (!adts) { const std::uint32_t lowComplexity = 2; - const auto config = [&](std::uint32_t channelConfiguration) { - return std::vector{static_cast(lowComplexity << 3 | samplingFreqIndex >> 1), - static_cast((samplingFreqIndex & 1) << 7 | channelConfiguration << 3)}; - }; - configs.push_back(config(channels < 2 ? 1 : 2)); + layouts.push_back({lowComplexity, samplingFreqIndex, channels < 2 ? 1u : 2u}); for (std::uint32_t channelConfiguration = 3; channelConfiguration <= 7; ++channelConfiguration) { - if ((channelConfiguration == 7 ? 8 : channelConfiguration) <= channels) configs.push_back(config(channelConfiguration)); + if ((channelConfiguration == 7 ? 8 : channelConfiguration) <= channels) layouts.push_back({lowComplexity, samplingFreqIndex, channelConfiguration}); } } - return std::make_unique(wordSize, adts, std::move(configs)); + return std::make_unique(wordSize, adts, highEfficiency, std::move(layouts)); } std::unique_ptr CreateAt9(std::int32_t wordSize, const std::uint8_t (&config)[4], At9Format& format) { diff --git a/core/libs/prx/libSceAudiodec/src/Codecs.hpp b/core/libs/prx/libSceAudiodec/src/Codecs.hpp index ebf8434eb2..6d43c67e6b 100644 --- a/core/libs/prx/libSceAudiodec/src/Codecs.hpp +++ b/core/libs/prx/libSceAudiodec/src/Codecs.hpp @@ -57,7 +57,7 @@ bool ParseMp3Header(const std::uint8_t* data, std::size_t size, Mp3Header& heade std::uint32_t AacSampleRate(std::uint32_t index); bool ValidAt9Config(const std::uint8_t (&config)[4]); std::unique_ptr CreateMp3(std::int32_t wordSize); -std::unique_ptr CreateAac(std::int32_t wordSize, bool adts, std::uint32_t samplingFreqIndex, std::uint32_t channels); +std::unique_ptr CreateAac(std::int32_t wordSize, bool adts, std::uint32_t samplingFreqIndex, std::uint32_t channels, bool highEfficiency); std::unique_ptr CreateAt9(std::int32_t wordSize, const std::uint8_t (&config)[4], At9Format& format); } diff --git a/core/libs/prx/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..3b3e49adb8 --- /dev/null +++ b/core/libs/prx/libSceAutoMounterClient/Export.cpp @@ -0,0 +1,36 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceAutoMounterClientGetUsbDeviceInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceAutoMounterClientGetUsbDeviceList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceAutoMounterClientInit() { + 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/AvPlayerLibrary.cmake b/core/libs/prx/libSceAvPlayer/AvPlayerLibrary.cmake index 86a5d13d3a..32fc8bb786 100644 --- a/core/libs/prx/libSceAvPlayer/AvPlayerLibrary.cmake +++ b/core/libs/prx/libSceAvPlayer/AvPlayerLibrary.cmake @@ -4,6 +4,7 @@ function(add_sce_avplayer_library target) ${avPlayerDir}/Export.cpp ${avPlayerDir}/src/Player.cpp ${avPlayerDir}/src/Source.cpp + ${avPlayerDir}/../libkernel/Time/TimedWait.cpp ) target_include_directories(${target} PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(${target} PRIVATE ffmpeg libc) diff --git a/core/libs/prx/libSceAvPlayer/CMakeLists.txt b/core/libs/prx/libSceAvPlayer/CMakeLists.txt index e2e5994278..d8a652d7ff 100644 --- a/core/libs/prx/libSceAvPlayer/CMakeLists.txt +++ b/core/libs/prx/libSceAvPlayer/CMakeLists.txt @@ -2,17 +2,33 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/AvPlayerLibrary.cmake) add_sce_avplayer_library(libSceAvPlayer) if(BUILD_TESTING) + foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) + string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") + endforeach() add_test_executable(guest_avplayer_tests ${LIBS_INCLUDE_DIR}/tests/GuestAvPlayer.cpp) target_include_directories(guest_avplayer_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_avplayer_tests PRIVATE libSceAvPlayer libc) 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..59ce8e72a8 100644 --- a/core/libs/prx/libSceAvPlayer/Export.cpp +++ b/core/libs/prx/libSceAvPlayer/Export.cpp @@ -112,7 +112,7 @@ int APS5_VABI sceAvPlayerInitEx(const AvPlayerInitDataEx* init_ex, AvPlayerInter init.auto_start = init_ex->auto_start; } init.default_language = init_ex->default_language; - *handle = new Player(init, VideoBufferCount(init.num_output_video_framebuffers)); + *handle = new Player(init, VideoBufferCount(init.num_output_video_framebuffers), init_ex->this_size >= Ps5InitDataExSize); return SCE_OK; } diff --git a/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp b/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp index 0eeb944fbb..97d3be2383 100644 --- a/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp +++ b/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp @@ -4,7 +4,6 @@ #ifndef CORE_LIBS_PRX_LIBSCEAVPLAYER_INCLUDE_AVPLAYER_HPP #define CORE_LIBS_PRX_LIBSCEAVPLAYER_INCLUDE_AVPLAYER_HPP -#include #include #include #include @@ -13,6 +12,7 @@ #include #include #include "SceTypes.hpp" +#include "prx/libkernel/Time/include/TimedWait.hpp" namespace AvPlayer { @@ -32,6 +32,7 @@ constexpr std::int32_t EventWarningId = 0x20; constexpr std::uint32_t StreamTypeVideo = 1; constexpr std::uint32_t StreamTypeAudio = 0; +constexpr std::uint32_t Ps5StreamTypeAudio = 2; constexpr std::uint32_t StreamTypeTimedText = 2; constexpr std::uint32_t SourceTypeUnknown = 0; @@ -86,7 +87,7 @@ std::uint32_t DetectSourceType(std::string_view path); class Player final : public AvPlayerInternal, private ISourceEvents { public: - Player(const AvPlayerInitData& init, std::uint32_t bufferCount); + Player(const AvPlayerInitData& init, std::uint32_t bufferCount, bool ps5Layout = false); ~Player() override; Player(const Player&) = delete; Player& operator=(const Player&) = delete; @@ -126,16 +127,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 stopLocked(); AvPlayerFileReplacement file{}; AvPlayerMemAllocator memory{}; AvPlayerEventReplacement callback{}; bool autoStartEnabled = false; + bool ps5Layout = false; char language[4]{}; std::uint32_t videoBuffers = 2; @@ -149,9 +153,10 @@ private: std::uint32_t demuxVideoBytes = 0; std::mutex eventMutex; - std::condition_variable eventCondition; + 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 1e265d3950..cf28da31f7 100644 --- a/core/libs/prx/libSceAvPlayer/src/Player.cpp +++ b/core/libs/prx/libSceAvPlayer/src/Player.cpp @@ -29,13 +29,17 @@ void APS5_VABI GuestDeallocate_nid_no_patch(void*, void* memory) { GuestHeap::GuestHeapFree_nid_postfix(memory); } +void* APS5_VABI GuestAllocateTexture_nid_no_patch(void*, std::uint32_t alignment, std::uint32_t size) { + return GuestHeap::GuestHeapAlignForGpu_nid_postfix(std::max(alignment, 16), size); +} + AvPlayerMemAllocator WithGuestHeapFallback(AvPlayerMemAllocator memory) { if (!memory.allocate || !memory.deallocate) { memory.allocate = GuestAllocate_nid_no_patch; memory.deallocate = GuestDeallocate_nid_no_patch; } if (!memory.allocate_texture || !memory.deallocate_texture) { - memory.allocate_texture = GuestAllocate_nid_no_patch; + memory.allocate_texture = GuestAllocateTexture_nid_no_patch; memory.deallocate_texture = GuestDeallocate_nid_no_patch; } return memory; @@ -57,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; } @@ -66,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() { @@ -74,8 +78,13 @@ Player::~Player() { std::lock_guard lock(eventMutex); quit = true; } - eventCondition.notify_all(); - controller.join(); + eventCondition.NotifyAll(); + if (controller.get_id() == std::this_thread::get_id()) { + *controllerClosed = true; + controller.detach(); + } else { + controller.join(); + } std::lock_guard lock(mutex); source.reset(); } @@ -85,7 +94,7 @@ void Player::queue(const Event& event) { std::lock_guard lock(eventMutex); events.push_back(event); } - eventCondition.notify_all(); + eventCondition.NotifyAll(); } void Player::OnWarning(std::int32_t code) { @@ -96,13 +105,13 @@ void Player::OnError() { queue({0, 0, false, true}); } -void Player::controllerLoop() { +void Player::controllerLoop(const bool& closed) { for (;;) { Event event{}; bool pending = false; { std::unique_lock lock(eventMutex); - eventCondition.wait_for(lock, std::chrono::milliseconds(5), [this] { return quit || !events.empty(); }); + eventCondition.WaitUntil(lock, TimedWait::DeadlineNanos(5000), [this] { return quit || !events.empty(); }); if (quit) return; if (!events.empty()) { event = events.front(); @@ -115,6 +124,7 @@ void Player::controllerLoop() { } else { checkEndOfFile(); } + if (closed) return; } } @@ -136,7 +146,15 @@ void Player::deliver(const Event& event) { callback.event_callback(callback.object_ptr, event.id, 0, event.id == EventWarningId ? &warning : nullptr); } +bool Player::readyLocked() const { + return source && state == State::Ready; +} + void Player::autoStart() { + { + std::lock_guard lock(mutex); + if (!readyLocked()) return; + } const int count = StreamCount(); if (count <= 0) { Stop(); @@ -151,11 +169,12 @@ 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)); - Start(); + std::lock_guard lock(mutex); + if (readyLocked()) startLocked(); } void Player::checkEndOfFile() { @@ -214,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; } @@ -221,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; @@ -246,6 +267,10 @@ int Player::ChangeStream(std::uint32_t from, std::uint32_t to) { int Player::Start() { std::lock_guard lock(mutex); + return startLocked(); +} + +int Player::startLocked() { 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(); diff --git a/core/libs/prx/libSceAvPlayer/src/Source.cpp b/core/libs/prx/libSceAvPlayer/src/Source.cpp index 8990d5d57f..466b6ceae8 100644 --- a/core/libs/prx/libSceAvPlayer/src/Source.cpp +++ b/core/libs/prx/libSceAvPlayer/src/Source.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAvPlayer/include/AvPlayer.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/FilesystemError.hpp" #include #include @@ -62,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) { @@ -150,6 +151,7 @@ public: void Reset(double milliseconds) { base = milliseconds; running = false; + paused = false; } void Pause() { @@ -243,7 +245,9 @@ public: format->pb = replacement->Context(); format->flags |= AVFMT_FLAG_CUSTOM_IO; } else { - const auto resolved = ResolvePath_nid_no_patch(path.c_str()).u8string(); + std::filesystem::path hostPath; + if (ResolvePathError_nid_no_patch(path.c_str(), hostPath) != 0) return false; + const auto resolved = hostPath.u8string(); url = "file:" + std::string(resolved.begin(), resolved.end()); } if (const int error = avformat_open_input(&format, replacement ? nullptr : url.c_str(), nullptr, nullptr); error < 0) { @@ -339,6 +343,7 @@ public: demuxEpoch = 1; minEpoch = 1; clockEpoch = 0; + loopEpochs.clear(); audioDriving = false; presented = false; lastPresented = 0; @@ -502,16 +507,16 @@ 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; presentationClock.Rebase(static_cast(front.info.timestamp)); clockEpoch = front.epoch; } - if (synced) { + if (synced || audioDriving) { const auto now = presentationClock.Now(); - if (static_cast(front.info.timestamp) > now) return false; + if (synced && static_cast(front.info.timestamp) > now) return false; while (frames.size() > 1 && frames[1].epoch == frames.front().epoch && static_cast(frames[1].info.timestamp) <= now) recycleFront(video); } present(video, info); @@ -575,6 +580,8 @@ private: picture.luma_bit_depth = 8; picture.chroma_bit_depth = 8; picture.video_full_range_flag = parameters->color_range == AVCOL_RANGE_JPEG; + const auto rate = av_guess_frame_rate(format, const_cast(&stream), nullptr); + picture.frame_rate = rate.num > 0 && rate.den > 0 ? av_q2d(rate) : 0.0; } else { auto& sound = info.details.audio; sound.channel_count = basic.details.audio.channel_count; @@ -712,6 +719,12 @@ private: decoder.current = std::move(decoder.frames.front()); decoder.frames.pop_front(); info = decoder.current->info; + bool looped = false; + while (!loopEpochs.empty() && loopEpochs.front() <= decoder.current->epoch) { + looped = looped || loopEpochs.front() == decoder.current->epoch; + loopEpochs.pop_front(); + } + if (looped) events.OnWarning(SCE_AVPLAYER_ERROR_WAR_LOOPING_BACK); } void dropLateAudio() { @@ -752,6 +765,7 @@ private: presentationClock.Rebase(static_cast(requested->target)); ++demuxEpoch; minEpoch = demuxEpoch; + loopEpochs.clear(); for (auto* decoder : {&video, &audio}) { if (decoder->stream < 0) continue; for (auto& item : decoder->packets) av_packet_free(&item.packet); @@ -784,10 +798,10 @@ private: { std::lock_guard lock(mutex); ++demuxEpoch; + loopEpochs.push_back(demuxEpoch); pushAll(PacketKind::Drain, demuxEpoch, 0); } condition.notify_all(); - events.OnWarning(SCE_AVPLAYER_ERROR_WAR_LOOPING_BACK); continue; } { @@ -976,6 +990,8 @@ private: info.details.video.luma_bit_depth = 8; info.details.video.chroma_bit_depth = 8; info.details.video.video_full_range_flag = frame.color_range == AVCOL_RANGE_JPEG; + const auto rate = av_guess_frame_rate(format, format->streams[index], nullptr); + info.details.video.frame_rate = rate.num > 0 && rate.den > 0 ? av_q2d(rate) : 0.0; publish(decoder, output); return true; } @@ -1067,6 +1083,7 @@ private: std::uint64_t demuxEpoch = 0; std::uint64_t minEpoch = 0; std::uint64_t clockEpoch = 0; + std::deque loopEpochs; bool audioDriving = false; bool presented = false; std::uint64_t lastPresented = 0; diff --git a/core/libs/prx/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..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceAvSetting/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/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..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceBackupRestoreUtil/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/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..bb89f27861 --- /dev/null +++ b/core/libs/prx/libSceBgft/Export.cpp @@ -0,0 +1,340 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceBgftServiceInit(void* 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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceDownloadFindActivePupTask() { + 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, void* 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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceIntDownloadFindActiveTask() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceIntDownloadGetGameAndGameAcProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceIntDownloadGetPatchProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceIntDownloadGetPlayGoProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceIntDownloadGetProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceIntDownloadGetTaskInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceIntDownloadPauseTask() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceIntDownloadRegisterTaskByStorageEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceIntDownloadResumeTask() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceIntDownloadStopTask() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceIntDownloadStartTask() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceIntDownloadUnregisterTask() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceIntInit() { + 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..5b7a222827 100644 --- a/core/libs/prx/libSceBluetoothHid/Export.cpp +++ b/core/libs/prx/libSceBluetoothHid/Export.cpp @@ -49,4 +49,48 @@ int APS5_VABI sceBluetoothHidSetOutputReport() { return 0; } +int APS5_VABI sceBluetoothHidDebugGetVersion() { + return 0x1e; +} + +int APS5_VABI sceBluetoothHidDisconnectDevice() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidGetDeviceInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidGetFeatureReport() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidGetReportDescriptor() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidInterruptOutput() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidParamInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidSetFeatureReport() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidThreadParamInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceCdlgG2P/CMakeLists.txt b/core/libs/prx/libSceCdlgG2P/CMakeLists.txt new file mode 100644 index 0000000000..9809109f69 --- /dev/null +++ b/core/libs/prx/libSceCdlgG2P/CMakeLists.txt @@ -0,0 +1,14 @@ +add_library(libSceCdlgG2P SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceCdlgG2P PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceCdlgG2P) + +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) + add_test(NAME guest_g2p_dialog COMMAND guest_g2p_dialog_tests) + if(WIN32) + set_tests_properties(guest_g2p_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceCdlgG2P/Export.cpp b/core/libs/prx/libSceCdlgG2P/Export.cpp index 5b46c12229..082b4bb331 100644 --- a/core/libs/prx/libSceCdlgG2P/Export.cpp +++ b/core/libs/prx/libSceCdlgG2P/Export.cpp @@ -1,35 +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 sceG2PDialogInitialize(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + int expected = COMMON_DIALOG_STATUS_NONE; + if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_INITIALIZED)) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; + return 0; } -int APS5_VABI sceG2PDialogTerminate(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceG2PDialogOpen(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceG2PDialogGetStatus(void) { - NotImplemented_nid_no_patch(__func__); - 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 sceG2PDialogUpdateStatus(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return g_status.load(); } -int APS5_VABI sceG2PDialogGetResult(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceG2PDialogGetStatus(void) { + 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 sceG2PDialogClose(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 sceG2PDialogTerminate(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/libSceCdlgPlayerReview/Export.cpp b/core/libs/prx/libSceCdlgPlayerReview/Export.cpp index b19768c25a..2e454c8741 100644 --- a/core/libs/prx/libSceCdlgPlayerReview/Export.cpp +++ b/core/libs/prx/libSceCdlgPlayerReview/Export.cpp @@ -1,6 +1,5 @@ #include #include -#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -11,9 +10,11 @@ constexpr int COMMON_DIALOG_STATUS_NONE = 0; constexpr int COMMON_DIALOG_STATUS_RUNNING = 2; constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; +constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004u); constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast(0x80B80007u); +constexpr int COMMON_DIALOG_ERROR_NOT_RUNNING = static_cast(0x80B8000Bu); constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); } @@ -21,7 +22,7 @@ extern "C" { int APS5_VABI scePlayerReviewDialogInitialize(void) { int expected = 0; - if (!g_status.compare_exchange_strong(expected, 1)) throw std::logic_error("scePlayerReviewDialogInitialize: already initialized"); + if (!g_status.compare_exchange_strong(expected, 1)) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; return 0; } @@ -29,8 +30,9 @@ int APS5_VABI scePlayerReviewDialogTerminate(void) { int expected = 1; if (g_status.compare_exchange_strong(expected, 0)) return 0; expected = COMMON_DIALOG_STATUS_FINISHED; - if (!g_status.compare_exchange_strong(expected, 0)) throw std::logic_error("scePlayerReviewDialogTerminate: not initialized or still running"); - return 0; + if (g_status.compare_exchange_strong(expected, 0)) return 0; + if (g_status.load() == 0) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + return COMMON_DIALOG_ERROR_NOT_RUNNING; } int APS5_VABI scePlayerReviewDialogOpen(const void* param) { @@ -44,6 +46,7 @@ int APS5_VABI scePlayerReviewDialogOpen(const void* param) { int APS5_VABI scePlayerReviewDialogClose(void) { if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } 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-module/CMakeLists.txt b/core/libs/prx/libSceCesCs-module/CMakeLists.txt new file mode 100644 index 0000000000..64ae3a7a2e --- /dev/null +++ b/core/libs/prx/libSceCesCs-module/CMakeLists.txt @@ -0,0 +1,2 @@ +include(${CMAKE_CURRENT_SOURCE_DIR}/../libSceCesCs/CesLibrary.cmake) +add_sce_ces_library(libSceCesCs-module) diff --git a/core/libs/prx/libSceCesCs/CMakeLists.txt b/core/libs/prx/libSceCesCs/CMakeLists.txt index 82fbba577e..2601651d05 100644 --- a/core/libs/prx/libSceCesCs/CMakeLists.txt +++ b/core/libs/prx/libSceCesCs/CMakeLists.txt @@ -1,14 +1,34 @@ -add_library(libSceCesCs SHARED EXCLUDE_FROM_ALL Export.cpp) -target_include_directories(libSceCesCs PRIVATE ${LIBS_INCLUDE_DIR}) -configure_windows_unwind(libSceCesCs) +include(${CMAKE_CURRENT_SOURCE_DIR}/CesLibrary.cmake) +add_sce_ces_library(libSceCesCs) if(BUILD_TESTING) add_test_executable(guest_ces_tests ${LIBS_INCLUDE_DIR}/tests/GuestCes.cpp) target_include_directories(guest_ces_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_ces_tests PRIVATE libSceCesCs) add_test(NAME guest_ces COMMAND guest_ces_tests) + add_test_executable(guest_ces_module_tests ${LIBS_INCLUDE_DIR}/tests/GuestCes.cpp) + target_include_directories(guest_ces_module_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ces_module_tests PRIVATE libSceCesCs-module) + add_test(NAME guest_ces_module COMMAND guest_ces_module_tests) if(WIN32) - set_tests_properties(guest_ces PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_ces guest_ces_module PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + +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 new file mode 100644 index 0000000000..d18971f49b --- /dev/null +++ b/core/libs/prx/libSceCesCs/CesLibrary.cmake @@ -0,0 +1,8 @@ +function(add_sce_ces_library target) + 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/Export.cpp b/core/libs/prx/libSceCesCs/Export.cpp index a2649662df..50005b98f8 100644 --- a/core/libs/prx/libSceCesCs/Export.cpp +++ b/core/libs/prx/libSceCesCs/Export.cpp @@ -1,7 +1,13 @@ #include #include +#include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "JisX0208.hpp" +#include "JisX0212.hpp" namespace { @@ -13,18 +19,67 @@ 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 CP1252_PROFILE = 0; +constexpr uint8_t PROFILES[3] = {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, 0x0090, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0x009D, 0x017E, 0x0178, }; -uint32_t cp1252ToUnicode(uint8_t sbc) { +constexpr uint16_t CP1251_HIGH[128] = { + 0x0402, 0x0403, 0x201A, 0x0453, 0x201E, 0x2026, 0x2020, 0x2021, 0x20AC, 0x2030, 0x0409, 0x2039, 0x040A, 0x040C, 0x040B, 0x040F, + 0x0452, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x0098, 0x2122, 0x0459, 0x203A, 0x045A, 0x045C, 0x045B, 0x045F, + 0x00A0, 0x040E, 0x045E, 0x0408, 0x00A4, 0x0490, 0x00A6, 0x00A7, 0x0401, 0x00A9, 0x0404, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x0407, + 0x00B0, 0x00B1, 0x0406, 0x0456, 0x0491, 0x00B5, 0x00B6, 0x00B7, 0x0451, 0x2116, 0x0454, 0x00BB, 0x0458, 0x0405, 0x0455, 0x0457, + 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, +}; + +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, +}; + +uint32_t sbcToUnicode(const uint8_t* profile, uint8_t sbc) { + if (profile == CP1251_PROFILE || profile == CP1250_PROFILE) { + const uint16_t* high = profile == CP1251_PROFILE ? CP1251_HIGH : CP1250_HIGH; + return sbc >= 0x80 ? high[sbc - 0x80] : sbc; + } return sbc >= 0x80 && sbc < 0xA0 ? CP1252_HIGH[sbc - 0x80] : sbc; } -bool unicodeToCp1252(uint32_t code, uint8_t& sbc) { +bool unicodeToSbc(const uint8_t* profile, uint32_t code, uint8_t& sbc) { + if (profile == CP1251_PROFILE || profile == CP1250_PROFILE) { + if (code < 0x80) { + sbc = static_cast(code); + return true; + } + const uint16_t* high = profile == CP1251_PROFILE ? CP1251_HIGH : CP1250_HIGH; + for (uint32_t i = 0; i < 128; ++i) { + if (high[i] == code) { + sbc = static_cast(0x80 + i); + return true; + } + } + return false; + } if (code < 0x80 || (code >= 0xA0 && code <= 0xFF)) { sbc = static_cast(code); return true; @@ -76,20 +131,349 @@ 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 || profile == CP1251_PROFILE || profile == CP1250_PROFILE; +} + +const void* contextProfile(const void* context) { + const void* profile = nullptr; + std::memcpy(&profile, context, sizeof(profile)); + return profile; +} + +int decodeMbcs(const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t& length, uint32_t& 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 (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 (!isEucJp(profile) && !isSjis(profile) && !isSbc(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 (!isEucJp(profile) && !isSjis(profile) && !isSbc(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; +} + } extern "C" { const uint8_t* APS5_VABI sceCesRefersUcsProfileCp1252(void) { - return &CP1252_PROFILE; + return CP1252_PROFILE; +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileCp1251(void) { + return CP1251_PROFILE; +} + +const void* APS5_VABI sceCesRefersUcsProfileCp1250(void) { + return CP1250_PROFILE; +} + +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) { + 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; +} + +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) 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]; - const uint32_t length = encodeUtf8(cp1252ToUnicode(sbc), encoded); + const uint32_t length = encodeUtf8(sbcToUnicode(profile, sbc), encoded); if (utf8max < length) return CES_ERROR_DST_BUFFER_END; for (uint32_t i = 0; i < length; ++i) utf8[i] = encoded[i]; if (utf8_len) *utf8_len = length; @@ -97,7 +481,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) 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; @@ -105,9 +489,59 @@ int APS5_VABI sceCesUtf8ToSbc(const uint8_t* utf8, uint32_t utf8max, uint32_t* u uint32_t length = 0; uint32_t code = 0; if (const int result = decodeUtf8(utf8, utf8max, length, code); result != 0) return result; - if (!unicodeToCp1252(code, *sbc)) return CES_ERROR_UNASSIGNED_CODE; + if (!unicodeToSbc(profile, code, *sbc)) return CES_ERROR_UNASSIGNED_CODE; if (utf8_len) *utf8_len = length; 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) { + (void)profile; + (void)eucBuffer; + (void)eucMax; + (void)eucLength; + (void)utf8Buffer; + (void)utf8Max; + (void)utf8Length; + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCesBig5ToUtf8(void* profile, uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Length, uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length) { + (void)profile; + (void)big5Buffer; + (void)big5Max; + (void)big5Length; + (void)utf8Buffer; + (void)utf8Max; + (void)utf8Length; + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCesUhcToUtf8(void* profile, uint8_t* uhcBuffer, uint32_t uhcMax, uint32_t* uhcLength, uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length) { + (void)profile; + (void)uhcBuffer; + (void)uhcMax; + (void)uhcLength; + (void)utf8Buffer; + (void)utf8Max; + (void)utf8Length; + NotImplemented_nid_no_patch(__func__); +} + } 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..165fe278e8 --- /dev/null +++ b/core/libs/prx/libSceCesCs/PendingExports.cpp @@ -0,0 +1,1250 @@ +#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 sceCesRefersUcsProfileCp1254(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp1255(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp1256(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp1257(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp1258(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp437(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp737(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp775(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp850(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp852(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp855(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 sceCesRefersUcsProfileCp860(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp861(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp862(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp863(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp864(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp865(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp866(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_1(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileIso8859_10(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_13(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileIso8859_14(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileIso8859_15(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileIso8859_16(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileIso8859_2(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_4(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileIso8859_5(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 sceCesRefersUcsProfileIso8859_9(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 sceCesRefersUcsProfileKoi8R(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileKoi8U(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/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..c9eb03df55 100644 --- a/core/libs/prx/libSceCommonDialog/CommonDialog.hpp +++ b/core/libs/prx/libSceCommonDialog/CommonDialog.hpp @@ -2,6 +2,6 @@ #define CORE_LIBS_PRX_LIBSCECOMMONDIALOG_COMMONDIALOG_HPP constexpr int COMMON_DIALOG_OK = 0; -constexpr int COMMON_DIALOG_ERROR_ALREADY_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..bb18755ae2 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_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..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceComposite/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/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 16dd322fbc..3a9fe2a4c8 100644 --- a/core/libs/prx/libSceContentDelete/Export.cpp +++ b/core/libs/prx/libSceContentDelete/Export.cpp @@ -8,6 +8,10 @@ namespace { std::atomic g_initialized{false}; + +void RequireInitialized(const char* function) { + if (!g_initialized.load()) throw std::logic_error(std::string(function) + ": not initialized"); +} } extern "C" { @@ -19,9 +23,35 @@ int APS5_VABI sceContentDeleteInitialize(const ContentDeleteInitParam* init_para return 0; } -int APS5_VABI sceContentDeleteByPath(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceContentDeleteByPath(const char* contentPath) { + if (contentPath == nullptr) APS5_INVALID_ARG_EX; + RequireInitialized(__func__); + throw std::logic_error(std::string(__func__) + ": " + contentPath + " is not in the host content library, which is empty, and the console's error for it is unknown"); +} + +int APS5_VABI sceContentDeleteById(std::int64_t contentId) { + RequireInitialized(__func__); + throw std::logic_error(std::string(__func__) + ": content id " + std::to_string(contentId) + " is not in the host content library, which is empty, and the console's error for it is unknown"); +} + +int APS5_VABI sceContentDeleteByIdListForSf(void) { + 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) { diff --git a/core/libs/prx/libSceContentExport/CMakeLists.txt b/core/libs/prx/libSceContentExport/CMakeLists.txt index 7c401ea5fa..4dd9a470cb 100644 --- a/core/libs/prx/libSceContentExport/CMakeLists.txt +++ b/core/libs/prx/libSceContentExport/CMakeLists.txt @@ -12,4 +12,14 @@ if(BUILD_TESTING) set_tests_properties(guest_content_export PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + 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_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 1a7fde3068..b8a366cc65 100644 --- a/core/libs/prx/libSceContentExport/Export.cpp +++ b/core/libs/prx/libSceContentExport/Export.cpp @@ -3,11 +3,23 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include +#include +#include +#include #include #include namespace { +constexpr std::size_t MaxExecution = 10; 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"); + if (!g_sessions.contains(expId)) throw std::invalid_argument(std::string(function) + ": unknown export session"); +} } extern "C" { @@ -21,9 +33,11 @@ int APS5_VABI sceContentExportInit2(const ContentExportInitParam2* init_param) { return 0; } -int APS5_VABI sceContentExportFinish(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceContentExportFinish(std::int32_t expId) { + std::lock_guard lock(g_sessionMutex); + RequireSession(__func__, expId); + g_sessions.erase(expId); + return 0; } int APS5_VABI sceContentExportFromData(void) { @@ -41,12 +55,97 @@ int APS5_VABI sceContentExportFromFileWithThumbnail(void) { return 0; } -int APS5_VABI sceContentExportStart(void) { +int APS5_VABI sceContentExportAddToFavorites(void) { 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(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportFromFileWithContentIdList(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportFromFileWithTitleIdList(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportMalloc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportRemoveFromFavorites(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportRenameContent(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportValidateContents(void) { + 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; +} + +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"); +} + +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"); +} + +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"); +} + 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"); bool expected = true; if (!g_initialized.compare_exchange_strong(expected, false)) throw std::logic_error(std::string(__func__) + ": not initialized"); return 0; diff --git a/core/libs/prx/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/CMakeLists.txt b/core/libs/prx/libSceContentSearch/CMakeLists.txt new file mode 100644 index 0000000000..0c46ea1108 --- /dev/null +++ b/core/libs/prx/libSceContentSearch/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceContentSearch SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceContentSearch PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceContentSearch) + +if(BUILD_TESTING) + add_test_executable(guest_content_search_tests ${LIBS_INCLUDE_DIR}/tests/GuestContentSearch.cpp) + target_include_directories(guest_content_search_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_content_search_tests PRIVATE libSceContentSearch libc) + configure_windows_unwind(guest_content_search_tests) + add_test(NAME guest_content_search COMMAND guest_content_search_tests) + if(WIN32) + set_tests_properties(guest_content_search PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceContentSearch/Export.cpp b/core/libs/prx/libSceContentSearch/Export.cpp index cfb70402ad..2c9b8a5777 100644 --- a/core/libs/prx/libSceContentSearch/Export.cpp +++ b/core/libs/prx/libSceContentSearch/Export.cpp @@ -8,6 +8,56 @@ namespace { std::atomic g_initialized{false}; + +constexpr int ErrorNotInitialized = static_cast(0x809D1001u); +constexpr int ErrorLimitTooBig = static_cast(0x809D100Au); +constexpr std::uint32_t MaxSearchLimit = 92; +constexpr std::int64_t LibraryUpdateId = 0; + +bool IsContentColumn(std::int32_t column) { + switch (column) { + case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 9: case 10: case 11: case 12: case 14: return true; + default: return false; + } +} + +bool IsSortColumn(std::int32_t column) { + switch (column) { + case 1: case 4: case 5: case 6: case 9: case 10: return true; + default: return false; + } +} + +bool IsConnector(std::int32_t connector) { return connector >= 0 && connector <= 2; } + +void ValidateColumnSets(const char* funcName, const ContentSearchContentColumnSet* columnSet, std::uint32_t columnSetLength) { + if (columnSet == nullptr && columnSetLength != 0) throw std::invalid_argument(std::string(funcName) + ": null column set"); + for (std::uint32_t i = 0; i < columnSetLength; ++i) { + const auto& set = columnSet[i]; + if (set.columns == nullptr || set.columns_length <= 0) throw std::invalid_argument(std::string(funcName) + ": empty column set"); + if (!IsConnector(set.logical_connector)) throw std::invalid_argument(std::string(funcName) + ": unknown logical connector " + std::to_string(set.logical_connector)); + for (std::int32_t j = 0; j < set.columns_length; ++j) { + const auto& column = set.columns[j]; + if (!IsContentColumn(column.column_type)) throw std::invalid_argument(std::string(funcName) + ": unknown column " + std::to_string(column.column_type)); + if (column.column_operator < 0 || column.column_operator > 5) throw std::invalid_argument(std::string(funcName) + ": unknown column operator " + std::to_string(column.column_operator)); + if (column.value == nullptr) throw std::invalid_argument(std::string(funcName) + ": null column value"); + if (!IsConnector(column.logical_connector)) throw std::invalid_argument(std::string(funcName) + ": unknown logical connector " + std::to_string(column.logical_connector)); + } + } +} + +void ValidateOrderBy(const char* funcName, const ContentSearchContentOrderByCondition* orderByConditions, std::uint32_t orderByConditionsLength) { + if (orderByConditions == nullptr && orderByConditionsLength != 0) throw std::invalid_argument(std::string(funcName) + ": null order-by conditions"); + for (std::uint32_t i = 0; i < orderByConditionsLength; ++i) { + const auto& condition = orderByConditions[i]; + if (!IsSortColumn(condition.column_type)) throw std::invalid_argument(std::string(funcName) + ": unknown sort column " + std::to_string(condition.column_type)); + if (condition.sort_type != 0 && condition.sort_type != 1) throw std::invalid_argument(std::string(funcName) + ": unknown sort type " + std::to_string(condition.sort_type)); + } +} + +[[noreturn]] void ThrowUnknownMetadataId(const char* funcName, std::int32_t metadataId) { + throw std::logic_error(std::string(funcName) + ": metadata id " + std::to_string(metadataId) + " was never opened, as the host content library is empty, and the console's error for it is unknown"); +} } extern "C" { @@ -19,29 +69,70 @@ int APS5_VABI sceContentSearchInit(const ContentSearchInitParam* init_param) { return 0; } -int APS5_VABI sceContentSearchCloseMetadata(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceContentSearchCloseMetadata(std::int32_t metadataId) { + if (!g_initialized.load()) return ErrorNotInitialized; + ThrowUnknownMetadataId(__func__, metadataId); } -int APS5_VABI sceContentSearchGetMetadataFieldInfo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceContentSearchGetMetadataFieldInfo(std::int32_t metadataId, const char* field, std::int32_t* metadataType, std::int32_t* size) { + if (!g_initialized.load()) return ErrorNotInitialized; + if (field == nullptr || metadataType == nullptr || size == nullptr) APS5_INVALID_ARG_EX; + ThrowUnknownMetadataId(__func__, metadataId); } -int APS5_VABI sceContentSearchGetMetadataValue(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceContentSearchGetMetadataValue(std::int32_t metadataId, const char* field, void* value) { + if (!g_initialized.load()) return ErrorNotInitialized; + if (field == nullptr || value == nullptr) APS5_INVALID_ARG_EX; + ThrowUnknownMetadataId(__func__, metadataId); } -int APS5_VABI sceContentSearchOpenMetadata(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceContentSearchOpenMetadata(const char* filePath, std::int32_t* metadataId) { + if (!g_initialized.load()) return ErrorNotInitialized; + if (filePath == nullptr || metadataId == nullptr) APS5_INVALID_ARG_EX; + throw std::logic_error(std::string(__func__) + ": " + filePath + " is not in the host content library, which is empty, and the console's error for it is unknown"); } -int APS5_VABI sceContentSearchSearchContent(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceContentSearchSearchContent(const ContentSearchContentColumnSet* columnSet, std::uint32_t columnSetLength, const ContentSearchContentOrderByCondition* orderByConditions, std::uint32_t orderByConditionsLength, std::uint32_t offset, std::uint32_t limit, std::int64_t* numOfContent, void* infos, std::int64_t* lastUpdateId) { + (void)offset; + if (!g_initialized.load()) return ErrorNotInitialized; + if (numOfContent == nullptr || lastUpdateId == nullptr || (infos == nullptr && limit != 0)) APS5_INVALID_ARG_EX; + ValidateColumnSets(__func__, columnSet, columnSetLength); + ValidateOrderBy(__func__, orderByConditions, orderByConditionsLength); + if (limit > MaxSearchLimit) return ErrorLimitTooBig; + *numOfContent = 0; + *lastUpdateId = LibraryUpdateId; + return 0; +} + +int APS5_VABI sceContentSearchGetContentLastUpdateId(std::int64_t* lastUpdateId) { + if (!g_initialized.load()) return ErrorNotInitialized; + if (lastUpdateId == nullptr) APS5_INVALID_ARG_EX; + *lastUpdateId = LibraryUpdateId; + return 0; +} + +int APS5_VABI sceContentSearchOpenMetadataByContentId(std::int64_t contentId, std::int32_t* metadataId) { + if (!g_initialized.load()) return ErrorNotInitialized; + if (metadataId == nullptr) APS5_INVALID_ARG_EX; + throw std::logic_error(std::string(__func__) + ": content id " + std::to_string(contentId) + " is not in the host content library, which is empty, and the console's error for it is unknown"); +} + +int APS5_VABI sceContentSearchGetTotalContentSize(const ContentSearchContentColumnSet* columnSet, std::uint32_t columnSetLength, std::int64_t* size, std::int64_t* lastUpdateId) { + if (!g_initialized.load()) return ErrorNotInitialized; + if (size == nullptr || lastUpdateId == nullptr) APS5_INVALID_ARG_EX; + ValidateColumnSets(__func__, columnSet, columnSetLength); + *size = 0; + *lastUpdateId = LibraryUpdateId; + return 0; +} + +int APS5_VABI sceContentSearchGetNumOfContent(const ContentSearchContentColumnSet* columnSet, std::uint32_t columnSetLength, std::int64_t* numOfContent, std::int64_t* lastUpdateId) { + if (!g_initialized.load()) return ErrorNotInitialized; + if (numOfContent == nullptr || lastUpdateId == nullptr) APS5_INVALID_ARG_EX; + ValidateColumnSets(__func__, columnSet, columnSetLength); + *numOfContent = 0; + *lastUpdateId = LibraryUpdateId; + return 0; } int APS5_VABI sceContentSearchTerm(void) { @@ -50,4 +141,45 @@ int APS5_VABI sceContentSearchTerm(void) { 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/CMakeLists.txt b/core/libs/prx/libSceConvertKeycode/CMakeLists.txt new file mode 100644 index 0000000000..d4f82eb5f7 --- /dev/null +++ b/core/libs/prx/libSceConvertKeycode/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceConvertKeycode SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceConvertKeycode PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceConvertKeycode) + +if(BUILD_TESTING) + add_test_executable(guest_convert_keycode_tests ${LIBS_INCLUDE_DIR}/tests/GuestConvertKeycode.cpp) + target_include_directories(guest_convert_keycode_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_convert_keycode_tests PRIVATE libSceConvertKeycode libc) + configure_windows_unwind(guest_convert_keycode_tests) + add_test(NAME guest_convert_keycode COMMAND guest_convert_keycode_tests) + if(WIN32) + set_tests_properties(guest_convert_keycode PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceConvertKeycode/Export.cpp b/core/libs/prx/libSceConvertKeycode/Export.cpp index 2ac60a0cdb..9b9e92908f 100644 --- a/core/libs/prx/libSceConvertKeycode/Export.cpp +++ b/core/libs/prx/libSceConvertKeycode/Export.cpp @@ -1,4 +1,6 @@ #include +#include +#include #include "SceTypes.hpp" #include "HitLog.hpp" #include "prx/libc/include/General.hpp" @@ -6,6 +8,20 @@ namespace { constexpr int kErrInvalidAddress = static_cast(0x80BC0031u); constexpr int kErrInvalidUserId = static_cast(0x80BC0010u); +constexpr std::uint32_t kIgnoredStateBits = 0x4000000u; + +char16_t UsCharacter(std::uint16_t keycode) { + static constexpr char16_t kPunctuation[] = u"-=[]\\\0;'`,./"; + 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 == 0x64) return u'\\'; + if (keycode >= 0x54 && keycode <= 0x63) return kKeypad[keycode - 0x54]; + return 0; +} } // namespace extern "C" { @@ -27,4 +43,12 @@ int APS5_VABI sceConvertKeycodeGetCharacterFromKeyboardData(void) { 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 & ~kIgnoredStateBits) != 0) throw std::runtime_error("sceConvertKeycodeGetCharacter: unsupported state " + std::to_string(state)); + if (type != 0) throw std::runtime_error("sceConvertKeycodeGetCharacter: unsupported keyboard type " + std::to_string(type)); + *character = UsCharacter(keycode); + return 0; +} + } diff --git a/core/libs/prx/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..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceCustomMusicAudioOut/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/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..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceCustomMusicSysCallWrapper/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/libSceDataTransfer/Export.cpp b/core/libs/prx/libSceDataTransfer/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceDataTransfer/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/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/libSceDbgThreadSanitizer/Export.cpp b/core/libs/prx/libSceDbgThreadSanitizer/Export.cpp new file mode 100644 index 0000000000..f2bbed9a32 --- /dev/null +++ b/core/libs/prx/libSceDbgThreadSanitizer/Export.cpp @@ -0,0 +1,14 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +void APS5_VABI __tsan_acquire(void*) { + NotImplemented_nid_no_patch(__func__); +} + +void APS5_VABI __tsan_release(void*) { + NotImplemented_nid_no_patch(__func__); +} + +} diff --git a/core/libs/prx/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..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceDipsw/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/libSceDiscId/Export.cpp b/core/libs/prx/libSceDiscId/Export.cpp new file mode 100644 index 0000000000..055e970b29 --- /dev/null +++ b/core/libs/prx/libSceDiscId/Export.cpp @@ -0,0 +1,18 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceDiscIdGet() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDiscIdGet2() { + 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..80f2a899dc --- /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, void* flags, void* out1, void* 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..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceDseehx/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/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..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceDtsEnc/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/libSceEditMp4/Export.cpp b/core/libs/prx/libSceEditMp4/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceEditMp4/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/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..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceEsvmEngine/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/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 83572e7d11..81aec33202 100644 --- a/core/libs/prx/libSceFiber/Export.cpp +++ b/core/libs/prx/libSceFiber/Export.cpp @@ -2,8 +2,10 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/general/AsmMacros.hpp" #include #include +#include #include #include #include @@ -15,6 +17,9 @@ #ifdef _WIN32 #include #include "prx/libc/include/GuestArena.hpp" +#else +#include +#include #endif static constexpr int32_t SCE_OK = 0; @@ -29,6 +34,8 @@ static constexpr std::size_t FIBER_OBJECT_SIZE = 0x100; static constexpr std::size_t FIBER_OPT_PARAM_SIZE = 0x80; static constexpr std::size_t FIBER_MIN_CONTEXT_SIZE = 512; static constexpr std::uint64_t FIBER_CONTEXT_FILL = 0xdeadbeefdeadbeefull; +static constexpr std::size_t FIBER_HOST_STACK_MARGIN = 1u << 20; +static constexpr std::size_t FIBER_GUEST_STACK_MIN = 64u << 10; using GuestFiberEntry = void (APS5_VABI*)(std::uint64_t argOnInitialize, std::uint64_t argOnRun); @@ -53,6 +60,8 @@ struct Fiber { void* savedStack; char name[FIBER_MAX_NAME_LENGTH + 1]; bool contextSizeCheck; + std::uint8_t* hostStack; + std::uint64_t hostStackBytes; }; static_assert(sizeof(Fiber) <= FIBER_OBJECT_SIZE, "guest reserves 0x100 bytes for SceFiber"); @@ -158,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 @@ -200,11 +212,8 @@ static void UnpinStack(const void* context, std::uint64_t bytes) { extern "C" void Aps5FiberSwitchStack_nid_no_patch(void** save, void* load); extern "C" void Aps5FiberTrampoline_nid_no_patch(); -asm(R"( - .text - .globl Aps5FiberSwitchStack_nid_no_patch - .type Aps5FiberSwitchStack_nid_no_patch, @function -Aps5FiberSwitchStack_nid_no_patch: +asm(".text\n" + APS5_ASM_FUNCTION("Aps5FiberSwitchStack_nid_no_patch") R"( push %rbp push %rbx push %r12 @@ -226,17 +235,16 @@ Aps5FiberSwitchStack_nid_no_patch: pop %rbx pop %rbp ret - .size Aps5FiberSwitchStack_nid_no_patch, .-Aps5FiberSwitchStack_nid_no_patch - - .globl Aps5FiberTrampoline_nid_no_patch - .type Aps5FiberTrampoline_nid_no_patch, @function -Aps5FiberTrampoline_nid_no_patch: +)" APS5_ASM_FUNCTION_END("Aps5FiberSwitchStack_nid_no_patch") + APS5_ASM_FUNCTION("Aps5FiberTrampoline_nid_no_patch") R"( mov %r12, %rdi and $-16, %rsp - call Aps5FiberMain_nid_no_patch + push $0 + push $0 + mov %rsp, %rbp + call )" APS5_ASM_SYMBOL("Aps5FiberMain_nid_no_patch") R"( ud2 - .size Aps5FiberTrampoline_nid_no_patch, .-Aps5FiberTrampoline_nid_no_patch -)"); +)" APS5_ASM_FUNCTION_END("Aps5FiberTrampoline_nid_no_patch")); struct InitialFrame { std::uint32_t mxcsr; @@ -259,10 +267,89 @@ static void UnpinStack(const void*, std::uint64_t) {} #endif +#ifdef _WIN32 + +static std::uint64_t HostStackGuardBytes() { + static const std::uint64_t bytes = [] { + SYSTEM_INFO system{}; + GetSystemInfo(&system); + return static_cast(system.dwPageSize); + }(); + return bytes; +} + +static void AllocateHostStack(Fiber* fiber) { + const auto page = HostStackGuardBytes(); + const auto bytes = (fiber->contextSize + FIBER_HOST_STACK_MARGIN + page - 1) / page * page + page; + auto* mapping = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + if (mapping == nullptr) throw std::runtime_error(std::string("sceFiberInitialize: no host stack of ") + std::to_string(bytes) + " bytes for fiber '" + fiber->name + "' (error " + std::to_string(GetLastError()) + ")"); + DWORD previous = 0; + if (!VirtualProtect(mapping, page, PAGE_NOACCESS, &previous)) { + const auto error = GetLastError(); + VirtualFree(mapping, 0, MEM_RELEASE); + throw std::runtime_error(std::string("sceFiberInitialize: cannot protect the guard page of the host stack of fiber '") + fiber->name + "' (error " + std::to_string(error) + ")"); + } + fiber->hostStack = mapping; + fiber->hostStackBytes = bytes; +} + +static void ReleaseHostStack(Fiber* fiber) { + if (fiber->hostStack && !VirtualFree(fiber->hostStack, 0, MEM_RELEASE)) throw std::runtime_error(std::string("sceFiberFinalize: cannot release the host stack of fiber '") + fiber->name + "' (error " + std::to_string(GetLastError()) + ")"); + fiber->hostStack = nullptr; + fiber->hostStackBytes = 0; +} + +#else + +static void AllocateHostStack(Fiber* fiber) { + const auto page = static_cast(sysconf(_SC_PAGESIZE)); + const auto bytes = (fiber->contextSize + FIBER_HOST_STACK_MARGIN + page - 1) / page * page + page; + 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; + munmap(mapping, bytes); + throw std::runtime_error(std::string("sceFiberInitialize: cannot protect the guard page of the host stack of fiber '") + fiber->name + "' (errno " + std::to_string(error) + ")"); + } + fiber->hostStack = static_cast(mapping); + fiber->hostStackBytes = bytes; +} + +static void ReleaseHostStack(Fiber* fiber) { + if (fiber->hostStack) munmap(fiber->hostStack, fiber->hostStackBytes); + fiber->hostStack = nullptr; + fiber->hostStackBytes = 0; +} + +#endif + +static std::uint8_t* StackTop(const Fiber* fiber) { + return fiber->hostStack ? fiber->hostStack + fiber->hostStackBytes : fiber->context + fiber->contextSize; +} + +static std::uint64_t* SizedWindow(const Fiber* fiber) { + return reinterpret_cast(StackTop(fiber) - fiber->contextSize); +} + +#ifdef _WIN32 + +static StackBounds FiberBounds(const Fiber* fiber) { + if (!fiber->hostStack) return {fiber->context + fiber->contextSize, fiber->context, fiber->context}; + return {fiber->hostStack + fiber->hostStackBytes, fiber->hostStack + HostStackGuardBytes(), fiber->hostStack}; +} + +#else + static StackBounds FiberBounds(const Fiber* fiber) { return {fiber->context + fiber->contextSize, fiber->context, fiber->context}; } +#endif + static Fiber* AsFiber(FiberObject* object) { auto* fiber = reinterpret_cast(object); return fiber && fiber->magic == FIBER_MAGIC ? fiber : nullptr; @@ -275,7 +362,7 @@ extern "C" [[noreturn]] void Aps5FiberMain_nid_no_patch(Fiber* fiber) { } static void PrepareInitialStack(Fiber* fiber) { - const auto top = reinterpret_cast(fiber->context + fiber->contextSize) & ~static_cast(15); + const auto top = reinterpret_cast(StackTop(fiber)) & ~static_cast(15); auto* frame = reinterpret_cast(top - 256); std::memset(frame, 0, sizeof(*frame)); frame->mxcsr = _mm_getcsr(); @@ -330,7 +417,10 @@ int32_t APS5_VABI _sceFiberInitializeImpl_nid_postfix(FiberObject* object, const if (addr_context == nullptr) return SCE_FIBER_ERROR_INVALID; if (size_context < FIBER_MIN_CONTEXT_SIZE) return SCE_FIBER_ERROR_RANGE; auto* fiber = reinterpret_cast(object); - if (fiber->magic == FIBER_MAGIC) UnpinStack(fiber->context, fiber->contextSize); + if (fiber->magic == FIBER_MAGIC) { + UnpinStack(fiber->context, fiber->contextSize); + ReleaseHostStack(fiber); + } std::memset(object, 0, FIBER_OBJECT_SIZE); fiber->magic = FIBER_MAGIC; fiber->state.store(FiberState::Idle, std::memory_order_relaxed); @@ -339,9 +429,10 @@ int32_t APS5_VABI _sceFiberInitializeImpl_nid_postfix(FiberObject* object, const fiber->context = static_cast(addr_context); fiber->contextSize = size_context; std::strncpy(fiber->name, name, FIBER_MAX_NAME_LENGTH); + if (size_context < FIBER_GUEST_STACK_MIN) AllocateHostStack(fiber); fiber->contextSizeCheck = g_contextSizeCheck.load(std::memory_order_relaxed); if (fiber->contextSizeCheck) { - auto* words = static_cast(addr_context); + auto* words = SizedWindow(fiber); std::fill(words, words + size_context / sizeof(std::uint64_t), FIBER_CONTEXT_FILL); } PinStack(addr_context, size_context); @@ -356,6 +447,7 @@ int32_t APS5_VABI sceFiberFinalize(FiberObject* object) { if (state == FiberState::Running || state == FiberState::Suspending) return SCE_FIBER_ERROR_STATE; fiber->magic = 0; UnpinStack(fiber->context, fiber->contextSize); + ReleaseHostStack(fiber); return SCE_OK; } @@ -425,12 +517,12 @@ int32_t APS5_VABI sceFiberGetInfo(FiberObject* object, FiberInfo* fiber_info) { if (fiber_info->size != sizeof(FiberInfo)) return SCE_FIBER_ERROR_INVALID; fiber_info->entry = reinterpret_cast(fiber->entry); fiber_info->arg_on_initialize = fiber->argOnInitialize; - fiber_info->addr_context = fiber->context; + fiber_info->addr_context = SizedWindow(fiber); fiber_info->size_context = fiber->contextSize; std::memcpy(fiber_info->name, fiber->name, sizeof(fiber_info->name)); fiber_info->size_context_margin = static_cast(-1); if (fiber->contextSizeCheck) { - const auto* words = reinterpret_cast(fiber->context); + const auto* words = SizedWindow(fiber); const auto* end = words + fiber->contextSize / sizeof(std::uint64_t); fiber_info->size_context_margin = static_cast(std::find_if(words, end, [](std::uint64_t word) { return word != FIBER_CONTEXT_FILL; }) - words) * sizeof(std::uint64_t); } @@ -458,6 +550,10 @@ int32_t APS5_VABI sceFiberGetThreadFramePointerAddress(uint64_t* addr_frame_poin return SCE_OK; } +int32_t APS5_VABI _sceFiberGetThreadFramePointerAddress(uint64_t* addr_frame_pointer) { + return sceFiberGetThreadFramePointerAddress(addr_frame_pointer); +} + int32_t APS5_VABI sceFiberStartContextSizeCheck(uint32_t flags) { if (flags != 0) return SCE_FIBER_ERROR_INVALID; bool expected = false; @@ -469,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..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceFios2/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/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/FontInternal.hpp b/core/libs/prx/libSceFont/include/FontInternal.hpp index b65648a0d3..8c68010f10 100644 --- a/core/libs/prx/libSceFont/include/FontInternal.hpp +++ b/core/libs/prx/libSceFont/include/FontInternal.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -23,11 +24,21 @@ 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; @@ -49,6 +60,7 @@ struct GeneratedGlyph { std::vector outlineContours; bool metricsInitialized = false; bool outlineInitialized = false; + int attribute = 0; }; struct SystemFontFile { @@ -85,11 +97,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 68d242d53e..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; @@ -428,9 +428,12 @@ int Font::GetCharGlyphMetrics(FontHandle handle, std::uint32_t code, FontGlyphMe metrics->Horizontal.bearingX = static_cast(slot->metrics.horiBearingX) / 64.0f * ratioX; metrics->Horizontal.bearingY = static_cast(slot->metrics.horiBearingY) / 64.0f * ratioY; metrics->Horizontal.advance = static_cast(slot->metrics.horiAdvance) / 64.0f * ratioX; - metrics->Vertical.bearingX = 0.0f; - metrics->Vertical.bearingY = 0.0f; - metrics->Vertical.advance = 0.0f; + const bool vertical = FT_HAS_VERTICAL(face); + const FT_Pos vertBearingX = vertical ? slot->metrics.vertBearingX : slot->metrics.horiBearingX - slot->metrics.horiAdvance / 2; + const FT_Pos vertBearingY = vertical ? slot->metrics.vertBearingY : (slot->metrics.vertAdvance - slot->metrics.height) / 2; + metrics->Vertical.bearingX = static_cast(vertBearingX) / 64.0f * ratioX; + metrics->Vertical.bearingY = static_cast(vertBearingY) / 64.0f * ratioY; + metrics->Vertical.advance = static_cast(slot->metrics.vertAdvance) / 64.0f * ratioY; return finish(SCE_FONT_OK); } @@ -472,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) { @@ -492,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/Open.cpp b/core/libs/prx/libSceFont/src/Open.cpp index ae87bf31bf..21e0272990 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 @@ -15,6 +16,7 @@ #include #include "prx/libSceFont/include/FontInternal.hpp" +#include "prx/libc/include/FilesystemError.hpp" namespace { @@ -226,6 +228,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)); } @@ -280,7 +289,11 @@ int APS5_VABI sceFontOpenFontFile(FontLibrary library, const char* path, std::ui return SCE_FONT_ERROR_INVALID_LIBRARY; } if (!pFontHandle) return SCE_FONT_ERROR_INVALID_PARAMETER; - const std::filesystem::path hostPath = path ? ResolvePath_nid_no_patch(path) : std::filesystem::path{}; + std::filesystem::path hostPath; + if (path && ResolvePathError_nid_no_patch(path, hostPath) != 0) { + *pFontHandle = nullptr; + return SCE_FONT_ERROR_FS_OPEN_FAILED; + } const std::string hostPathString = hostPath.string(); std::uint32_t libraryLock = 0; const int beginRc = BeginOpen(library, pFontHandle, libraryLock); @@ -387,6 +400,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; @@ -402,6 +416,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; @@ -427,8 +442,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); diff --git a/core/libs/prx/libSceFont/src/Render.cpp b/core/libs/prx/libSceFont/src/Render.cpp index 55d3de834d..3f78fa635a 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,20 @@ 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, int attribute, int* oldAttribute) { + auto* generated = TryGetGeneratedGlyph(glyph); + if (!generated) return SCE_FONT_ERROR_INVALID_GLYPH; + if (oldAttribute) *oldAttribute = generated->attribute; + generated->attribute = attribute; + 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 = generated->attribute; return SCE_FONT_OK; } diff --git a/core/libs/prx/libSceFont/src/State.cpp b/core/libs/prx/libSceFont/src/State.cpp index 6911fa24f1..4ebd92348b 100644 --- a/core/libs/prx/libSceFont/src/State.cpp +++ b/core/libs/prx/libSceFont/src/State.cpp @@ -12,11 +12,14 @@ #include #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; @@ -52,11 +55,13 @@ 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() { - std::this_thread::sleep_for(std::chrono::microseconds(30)); + PreciseSleepUs(30); } std::uint32_t Font::AcquireWordLock(std::uint32_t& word) { @@ -248,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; @@ -261,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 e5503b8867..e4adb89793 100644 --- a/core/libs/prx/libSceFont/src/Style.cpp +++ b/core/libs/prx/libSceFont/src/Style.cpp @@ -51,6 +51,30 @@ int ReadFontStyle(FontHandle fontHandle, bool checkMagic, Getter&& getter) { return rc; } +template +int AccessFontSettings(FontHandle fontHandle, Access&& access) { + return ReadFontStyle(fontHandle, true, [&](FontHandleNative*) { + FontState* state = TryGetState(fontHandle); + return state ? access(*state) : SCE_FONT_ERROR_INVALID_FONT_HANDLE; + }); +} + +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; + }); +} + +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; + return SCE_FONT_OK; + }); +} + void ResetScaleOutputs(float* w, float* h) { if (w) *w = 0.0f; if (h) *h = 0.0f; @@ -77,6 +101,39 @@ 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); +} + +int APS5_VABI sceFontGetScriptLanguage(FontHandle fontHandle, int fontScript, int* fontLanguage) { + return GetFontSetting(fontHandle, &FontState::scriptLanguages, fontScript, fontLanguage); +} + +int APS5_VABI sceFontSetTypographicDesign(FontHandle fontHandle, int typographic, int feature) { + return SetFontSetting(fontHandle, &FontState::typographicFeatures, typographic, feature); +} + +int APS5_VABI sceFontGetTypographicDesign(FontHandle fontHandle, int typographic, int* feature) { + return GetFontSetting(fontHandle, &FontState::typographicFeatures, typographic, feature); +} + int APS5_VABI sceFontSetScalePixel(FontHandle fontHandle, float w, float h) { return UpdateFontStyle(fontHandle, [&](FontHandleNative* font) { const int changed = StyleStateSetScalePixel(&font->style, w, h); diff --git a/core/libs/prx/libSceFont/src/SystemFont.cpp b/core/libs/prx/libSceFont/src/SystemFont.cpp index bf12813803..5bcfb0789e 100644 --- a/core/libs/prx/libSceFont/src/SystemFont.cpp +++ b/core/libs/prx/libSceFont/src/SystemFont.cpp @@ -6,6 +6,8 @@ #include #ifdef _WIN32 #include +#elif defined(__APPLE__) +#include #endif #include "prx/libSceFont/include/FontInternal.hpp" @@ -35,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; @@ -43,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"}, @@ -58,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) { @@ -85,12 +91,52 @@ 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]; const auto length = GetModuleFileNameW(nullptr, module, MAX_PATH); if (length == 0 || length == MAX_PATH) throw std::runtime_error("libSceFont: cannot locate the executable"); return std::filesystem::path(module).parent_path(); +#elif defined(__APPLE__) + std::uint32_t size = 0; + _NSGetExecutablePath(nullptr, &size); + std::vector executable(size); + std::error_code error; + std::filesystem::path path; + if (_NSGetExecutablePath(executable.data(), &size) == 0) path = std::filesystem::canonical(executable.data(), error); + if (path.empty()) throw std::runtime_error("libSceFont: cannot locate the executable"); + return path.parent_path(); #else return std::filesystem::read_symlink("/proc/self/exe").parent_path(); #endif @@ -104,9 +150,7 @@ 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() { @@ -121,11 +165,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 5a43ff63fa..afc8a452f7 100644 --- a/core/libs/prx/libSceFont/src/Text.cpp +++ b/core/libs/prx/libSceFont/src/Text.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include "prx/libSceFont/include/FontInternal.hpp" @@ -63,10 +64,11 @@ struct FontWritingData { bool hasCurrentStep = false; bool maskFormatCharacters = true; bool ownsStorage = false; + bool hasRawGlyphMetrics = false; FontWritingStep step{}; FontWritingMetrics metrics{}; FontGlyphMetrics rawGlyphMetrics{}; - bool hasRawGlyphMetrics = false; + FontWritingLetterStep letterStep{}; }; static_assert(sizeof(FontWritingData) <= sizeof(FontWriting)); @@ -206,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) @@ -491,6 +507,16 @@ int APS5_VABI sceFontCharacterGetBidiLevel(const FontTextCharacter* textCharacte return SCE_FONT_OK; } +int APS5_VABI sceFontCharacterGetSyllableStringState(const FontTextCharacter* textCharacter, int* syllableStringState) { + if (!textCharacter || !syllableStringState) { + if (syllableStringState) *syllableStringState = 0; + return SCE_FONT_ERROR_INVALID_PARAMETER; + } + if (textCharacter->characterCode >= 0x0E00 && textCharacter->characterCode <= 0x0E7F) throw std::runtime_error("libSceFont: sceFontCharacterGetSyllableStringState for Thai syllable clusters is not implemented"); + *syllableStringState = 0; + return SCE_FONT_OK; +} + int APS5_VABI sceFontCharacterGetTextFontCode(const FontTextCharacter* textCharacter, FontHandle* pFontHandle, std::uint32_t* textCode) { if (!textCharacter) { if (pFontHandle) *pFontHandle = nullptr; @@ -539,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); @@ -596,21 +655,22 @@ const FontWritingStep* APS5_VABI sceFontWritingRefersRenderStep(FontWriting* fon return &writing->step; } -FontTextCharacter* APS5_VABI sceFontWritingRefersRenderStepCharacter(FontWriting* fontWriting, FontWritingLetterStep* letterStep) { - if (letterStep) std::memset(letterStep, 0, sizeof(*letterStep)); +FontTextCharacter* APS5_VABI sceFontWritingRefersRenderStepCharacter(FontWriting* fontWriting, const FontWritingLetterStep** pLetterStep) { + if (pLetterStep) *pLetterStep = nullptr; auto* writing = GetWritingData(fontWriting); if (!writing || !writing->hasCurrentStep || !writing->storage || writing->currentIndex >= writing->storage->characters.size()) return nullptr; - if (letterStep) { - letterStep->x = writing->step.x; - letterStep->y = writing->step.y; - letterStep->advanceX = writing->step.advanceX; - letterStep->advanceY = writing->step.advanceY; - letterStep->Components.textsCount = 1; - letterStep->Components.textsIndex = static_cast(writing->currentIndex); - letterStep->Components.glyphsCount = 1; - letterStep->Components.glyphsIndex = static_cast(writing->currentIndex); - letterStep->Components.characterTextCount = static_cast(std::max(writing->step.Profile.characterCount, 1)); - } + auto& letterStep = writing->letterStep; + letterStep = {}; + letterStep.x = writing->step.x; + letterStep.y = writing->step.y; + letterStep.advanceX = writing->step.advanceX; + letterStep.advanceY = writing->step.advanceY; + letterStep.Components.textsCount = 1; + letterStep.Components.textsIndex = static_cast(writing->currentIndex); + letterStep.Components.glyphsCount = 1; + letterStep.Components.glyphsIndex = static_cast(writing->currentIndex); + letterStep.Components.characterTextCount = static_cast(std::max(writing->step.Profile.characterCount, 1)); + if (pLetterStep) *pLetterStep = &letterStep; return &writing->storage->characters[writing->currentIndex]; } diff --git a/core/libs/prx/libSceFont/src/Unimplemented.cpp b/core/libs/prx/libSceFont/src/Unimplemented.cpp index 87c3bbcd5e..bc79aed66e 100644 --- a/core/libs/prx/libSceFont/src/Unimplemented.cpp +++ b/core/libs/prx/libSceFont/src/Unimplemented.cpp @@ -7,16 +7,6 @@ extern "C" { -int APS5_VABI sceFontCharacterGetSyllableStringState() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontCharactersRefersTextCodes() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFontControl() { NotImplemented_nid_no_patch(__func__); return 0; @@ -42,11 +32,6 @@ int APS5_VABI sceFontCreateWords() { return 0; } -int APS5_VABI sceFontDefineAttribute() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFontDestroyGraphicsDevice() { NotImplemented_nid_no_patch(__func__); return 0; @@ -63,11 +48,6 @@ int APS5_VABI sceFontDestroyWords() { } -int APS5_VABI sceFontGetAttribute() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFontGetFontGlyphsOutlineProfile() { NotImplemented_nid_no_patch(__func__); return 0; @@ -88,21 +68,6 @@ int APS5_VABI sceFontGetGlyphExpandBufferState() { return 0; } -int APS5_VABI sceFontGetScriptLanguage() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontGetTypographicDesign() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontGlyphGetAttribute() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFontGlyphRenderImage() { NotImplemented_nid_no_patch(__func__); return 0; @@ -368,31 +333,11 @@ int APS5_VABI sceFontGraphicsUpdateShapeFillPlot() { return 0; } -int APS5_VABI sceFontSetScriptLanguage() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontSetTypographicDesign() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFontSupportGlyphs() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontTextCodesStepBack() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontTextCodesStepNext() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFontWordsFindWordCharacters() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceFont/src/WritingLine.cpp b/core/libs/prx/libSceFont/src/WritingLine.cpp index 216a03deb7..35e014c3c8 100644 --- a/core/libs/prx/libSceFont/src/WritingLine.cpp +++ b/core/libs/prx/libSceFont/src/WritingLine.cpp @@ -1,30 +1,60 @@ -#include +#include #include #include +#include #include #include #include #include -#include #include "prx/libSceFont/include/FontTypes.hpp" 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 WritingLine { - std::vector steps; - std::size_t cursor = 0; +struct WritingOrder { + void* orderer = nullptr; + WritingOrder* prev = nullptr; + WritingOrder* next = nullptr; + std::uint64_t attribute = 0; FontWritingMetrics metrics{}; }; +struct WritingLevel { + WritingOrder* first = nullptr; + WritingOrder* last = nullptr; +}; + +struct WritingLine { + std::int32_t form = 0; + 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; + WritingOrder* last = nullptr; + WritingOrder* head = nullptr; + WritingOrder* tail = nullptr; + WritingOrder* cursor = nullptr; + std::array levels{}; + std::deque orders; +}; + std::mutex linesMutex; std::unordered_set lines; @@ -39,6 +69,147 @@ WritingLine* GetLine(const char* function, void* writingLine) { return line; } +bool IsRightToLeft(const WritingLine& line) { + return line.form == WRITING_FORM_HORIZONTAL_RTL; +} + +bool SpaceAfter(const WritingOrder& order) { + return (order.attribute & WRITING_ATTRIBUTE_SPACE_AFTER) != 0; +} + +bool SpaceBefore(const WritingOrder& order) { + return (order.attribute & WRITING_ATTRIBUTE_SPACE_BEFORE) != 0; +} + +bool IsSpaced(const WritingOrder& before, const WritingOrder& after) { + return SpaceAfter(before) || SpaceBefore(after); +} + +float HeadSpace(const WritingLine& line, const WritingOrder& order) { + return IsRightToLeft(line) ? order.metrics.advanceX - order.metrics.Extent.left : order.metrics.Extent.left; +} + +float TailSpace(const WritingLine& line, const WritingOrder& order) { + return IsRightToLeft(line) ? order.metrics.Extent.right : order.metrics.advanceX - order.metrics.Extent.right; +} + +void ResetOrders(WritingLine& line) { + line.orders.clear(); + line.levels.fill({}); + line.advanceX = 0.0f; + line.advanceY = 0.0f; + line.orderingSpace = 0.0f; + line.last = nullptr; + line.head = nullptr; + line.tail = nullptr; + line.cursor = nullptr; +} + +void InsertAfter(WritingOrder* anchor, WritingOrder* order) { + order->prev = anchor; + order->next = anchor->next; + if (anchor->next) anchor->next->prev = order; + anchor->next = order; +} + +void InsertBefore(WritingOrder* anchor, WritingOrder* order) { + order->next = anchor; + order->prev = anchor->prev; + if (anchor->prev) anchor->prev->next = order; + anchor->prev = order; +} + +void Link(WritingLine& line, WritingOrder* order, std::size_t level) { + WritingLevel& slot = line.levels[level]; + if (!line.last) { + slot = {order, order}; + return; + } + if (level >= line.lastLevel) { + if (line.lastLevel % 2 == 0) { + InsertAfter(line.last, order); + } else { + InsertBefore(line.last, order); + } + if (!slot.last || order->prev == slot.last) slot.last = order; + if (!slot.first || order->next == slot.first) slot.first = order; + return; + } + WritingOrder* first = line.levels[line.lastLevel].first; + WritingOrder* last = line.levels[line.lastLevel].last; + line.levels[line.lastLevel] = {}; + for (std::size_t closing = line.lastLevel - 1; closing > level; --closing) { + WritingLevel& closed = line.levels[closing]; + if (closing % 2 == 0) { + if (closed.first) first = closed.first; + } else { + if (closed.last) last = closed.last; + } + closed = {}; + } + if (level % 2 == 0) { + if (!slot.first) slot.first = first; + InsertAfter(last, order); + slot.last = order; + } else { + if (!slot.last) slot.last = last; + InsertBefore(first, order); + slot.first = order; + } +} + +float OrderingSpaceDelta(const WritingLine& line, const WritingOrder& order) { + const WritingOrder* before = order.prev; + const WritingOrder* after = order.next; + const bool split = before && after && IsSpaced(*before, *after); + const float sign = split ? -1.0f : 1.0f; + float delta = 0.0f; + if (before) { + const bool spaced = IsSpaced(*before, order); + if (spaced) delta = HeadSpace(line, order); + if (spaced != split) delta += sign * TailSpace(line, *before); + } + if (after) { + const bool spaced = IsSpaced(order, *after); + if (spaced) delta += TailSpace(line, order); + if (spaced != split) delta += sign * HeadSpace(line, *after); + } + return delta; +} + +void ExtendRenderMetrics(WritingLine& line) { + const FontWritingMetrics& run = line.step.Metrics; + const float x = line.step.x + line.step.Adjusting.x; + const float y = line.step.y + line.step.Adjusting.y; + line.step.x += run.advanceX; + line.step.y += run.advanceY; + FontWritingMetrics& total = line.renderMetrics; + total.advanceX = x + run.advanceX; + total.advanceY = y + run.advanceY; + if (!(run.Extent.left == run.Extent.right)) { + const float left = x + run.Extent.left; + const float right = x + run.Extent.right; + if (total.Extent.left == 0.0f && total.Extent.right == 0.0f) { + total.Extent.left = left; + total.Extent.right = right; + } else { + if (total.Extent.left > left) total.Extent.left = left; + if (right > total.Extent.right) total.Extent.right = right; + } + } + if (!(run.Extent.top == run.Extent.bottom)) { + const float top = y + run.Extent.top; + const float bottom = y + run.Extent.bottom; + if (total.Extent.top == 0.0f && total.Extent.bottom == 0.0f) { + total.Extent.top = top; + total.Extent.bottom = bottom; + } else { + if (total.Extent.top > top) total.Extent.top = top; + if (bottom > total.Extent.bottom) total.Extent.bottom = bottom; + } + } +} + } #pragma GCC visibility push(default) @@ -48,11 +219,12 @@ 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_LTR) + 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{}; + line->form = writingForm; { std::lock_guard lock(linesMutex); lines.insert(line); @@ -75,62 +247,100 @@ int APS5_VABI sceFontDestroyWritingLine(void** pWritingLine) { int APS5_VABI sceFontWritingLineClear(void* writingLine) { auto* line = GetLine(__func__, writingLine); - line->steps.clear(); - line->cursor = 0; - line->metrics = {}; + 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) Unsupported(__func__, "writing attribute " + std::to_string(writingAttribute) + " is not modelled"); - if (!writingMetrics) Unsupported(__func__, "null writing metrics are not modelled"); - const FontWritingMetrics& order = *writingMetrics; - const float pen = line->metrics.advanceX; - FontWritingLineStep step{}; - step.x = pen; - step.advanceX = order.advanceX; - step.advanceY = order.advanceY; - step.writingOrderer = writingOrderer; - step.Metrics = order; - const FontWritingExtent extent{order.Extent.top, order.Extent.bottom, pen + order.Extent.left, pen + order.Extent.right}; - if (line->steps.empty()) { - line->metrics.Extent = extent; - } else { - line->metrics.Extent.top = std::min(line->metrics.Extent.top, extent.top); - line->metrics.Extent.bottom = std::max(line->metrics.Extent.bottom, extent.bottom); - line->metrics.Extent.left = std::min(line->metrics.Extent.left, extent.left); - line->metrics.Extent.right = std::max(line->metrics.Extent.right, extent.right); - } - line->metrics.advanceX += order.advanceX; - line->metrics.advanceY += order.advanceY; - line->steps.push_back(step); - line->cursor = 0; + 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 (line->cursor) ResetOrders(*line); + WritingOrder* order = &line->orders.emplace_back(); + order->orderer = writingOrderer; + order->attribute = writingAttribute; + if (writingMetrics) order->metrics = *writingMetrics; + Link(*line, order, level); + if (!order->prev) line->head = order; + if (!order->next) line->tail = order; + line->orderingSpace += OrderingSpaceDelta(*line, *order); + line->advanceX += order->metrics.advanceX; + line->advanceY += order->metrics.advanceY; + line->last = order; + line->lastLevel = level; return SCE_FONT_OK; } const FontWritingLineStep* APS5_VABI sceFontWritingLineRefersRenderStep(void* writingLine) { if (!writingLine) return nullptr; auto* line = GetLine(__func__, writingLine); - if (line->cursor >= line->steps.size()) return nullptr; - return &line->steps[line->cursor++]; + if (line->cursor) { + ExtendRenderMetrics(*line); + } else { + line->steppedSpace = 0.0f; + line->step.x = 0.0f; + line->step.y = 0.0f; + line->step.spacingProgress = 0.0f; + line->step.Metrics = {}; + line->renderMetrics = {}; + } + const bool rightToLeft = IsRightToLeft(*line); + WritingOrder* next = nullptr; + if (rightToLeft) { + next = line->cursor ? line->cursor->prev : line->tail; + } else { + next = line->cursor ? line->cursor->next : line->head; + } + if (!next) { + line->cursor = nullptr; + line->step.Metrics = {}; + return nullptr; + } + if (rightToLeft) { + const WritingOrder* stepped = next->next; + if (stepped && IsSpaced(*next, *stepped)) + line->steppedSpace += stepped->metrics.advanceX - stepped->metrics.Extent.left + next->metrics.Extent.right; + } else { + const WritingOrder* stepped = next->prev; + if (stepped && IsSpaced(*stepped, *next)) + line->steppedSpace += stepped->metrics.advanceX - stepped->metrics.Extent.right + next->metrics.Extent.left; + } + line->cursor = next; + line->step.Metrics = next->metrics; + line->step.writingOrderer = next->orderer; + line->step.Adjusting = {}; + line->step.spacingProgress = 0.0f; + if (!(line->steppedSpace == 0.0f) && !(line->orderingSpace == 0.0f)) line->step.spacingProgress = line->steppedSpace / line->orderingSpace; + return &line->step; } 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 (line->steps.empty()) Unsupported(__func__, "metrics of an empty line are not modelled"); - *writingMetrics = line->metrics; + *writingMetrics = line->renderMetrics; return SCE_FONT_OK; } -int APS5_VABI sceFontWritingLineGetOrderingSpace(void* writingLine, float* headSpace, float* inlineSpace, float* tailSpace, float* advanceSpace) { - GetLine(__func__, writingLine); - if (!headSpace || !inlineSpace || !tailSpace || !advanceSpace) Unsupported(__func__, "null space pointer (error code not verified)"); - *headSpace = 0.0f; - *inlineSpace = 0.0f; - *tailSpace = 0.0f; - *advanceSpace = 0.0f; +int APS5_VABI sceFontWritingLineGetOrderingSpace(void* writingLine, float* advanceSpace, float* headSpace, float* inlineSpace, float* tailSpace) { + auto* line = GetLine(__func__, writingLine); + if (headSpace && !tailSpace) Unsupported(__func__, "a head space pointer without a tail space pointer is not modelled"); + if (advanceSpace) *advanceSpace = line->advanceX; + if (headSpace) { + const WritingOrder* leading = IsRightToLeft(*line) ? line->tail : line->head; + const WritingOrder* trailing = IsRightToLeft(*line) ? line->head : line->tail; + if (IsRightToLeft(*line)) { + *headSpace = leading ? leading->metrics.Extent.right : 0.0f; + *tailSpace = trailing ? trailing->metrics.advanceX - trailing->metrics.Extent.left : 0.0f; + } else { + *headSpace = leading ? leading->metrics.Extent.left : 0.0f; + *tailSpace = trailing ? trailing->metrics.advanceX - trailing->metrics.Extent.right : 0.0f; + } + } + if (inlineSpace) *inlineSpace = line->orderingSpace; return SCE_FONT_OK; } diff --git a/core/libs/prx/libSceFontFt/Export.cpp b/core/libs/prx/libSceFontFt/Export.cpp index d34a3ca559..0dcd8bcf90 100644 --- a/core/libs/prx/libSceFontFt/Export.cpp +++ b/core/libs/prx/libSceFontFt/Export.cpp @@ -1,11 +1,41 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include +#include +#include + +#include +#include FT_FREETYPE_H +#include FT_MODULE_H + #include "prx/libSceFontFt/include/FontFtDriver.hpp" #include "prx/libc/include/General.hpp" #pragma GCC visibility push(default) +namespace { + +FT_Library ProbeLibrary() { + static const FT_Library library = [] { + FT_Library created = nullptr; + if (FT_Init_FreeType(&created) != 0) return static_cast(nullptr); + return created; + }(); + return library; +} + +int SupportFormat(const char* function, std::initializer_list modules) { + const FT_Library library = ProbeLibrary(); + if (library == nullptr) throw std::runtime_error(std::string(function) + ": FreeType failed to initialize"); + for (const char* module : modules) { + if (FT_Get_Module(library, module) == nullptr) throw std::runtime_error(std::string(function) + ": FreeType module " + module + " is not built in"); + } + return 0; +} + +} + extern "C" { const Font::SysDriver* APS5_VABI sceFontSelectLibraryFt(int value) { @@ -32,13 +62,11 @@ int APS5_VABI sceFontFtSetAliasPath() { } int APS5_VABI sceFontFtSupportBdf() { - NotImplemented_nid_no_patch(__func__); - return 0; + return SupportFormat(__func__, {"bdf"}); } int APS5_VABI sceFontFtSupportCid() { - NotImplemented_nid_no_patch(__func__); - return 0; + return SupportFormat(__func__, {"t1cid"}); } int APS5_VABI sceFontFtSupportFontFormats() { @@ -47,28 +75,23 @@ int APS5_VABI sceFontFtSupportFontFormats() { } int APS5_VABI sceFontFtSupportOpenType() { - NotImplemented_nid_no_patch(__func__); - return 0; + return SupportFormat(__func__, {"cff", "truetype"}); } int APS5_VABI sceFontFtSupportOpenTypeOtf() { - NotImplemented_nid_no_patch(__func__); - return 0; + return SupportFormat(__func__, {"cff"}); } int APS5_VABI sceFontFtSupportOpenTypeTtf() { - NotImplemented_nid_no_patch(__func__); - return 0; + return SupportFormat(__func__, {"truetype"}); } int APS5_VABI sceFontFtSupportPcf() { - NotImplemented_nid_no_patch(__func__); - return 0; + return SupportFormat(__func__, {"pcf"}); } int APS5_VABI sceFontFtSupportPfr() { - NotImplemented_nid_no_patch(__func__); - return 0; + return SupportFormat(__func__, {"pfr"}); } int APS5_VABI sceFontFtSupportSystemFonts() { @@ -77,8 +100,7 @@ int APS5_VABI sceFontFtSupportSystemFonts() { } int APS5_VABI sceFontFtSupportTrueType() { - NotImplemented_nid_no_patch(__func__); - return 0; + return SupportFormat(__func__, {"truetype"}); } int APS5_VABI sceFontFtSupportTrueTypeGx() { @@ -87,18 +109,15 @@ int APS5_VABI sceFontFtSupportTrueTypeGx() { } int APS5_VABI sceFontFtSupportType1() { - NotImplemented_nid_no_patch(__func__); - return 0; + return SupportFormat(__func__, {"type1"}); } int APS5_VABI sceFontFtSupportType42() { - NotImplemented_nid_no_patch(__func__); - return 0; + return SupportFormat(__func__, {"type42"}); } int APS5_VABI sceFontFtSupportWinFonts() { - NotImplemented_nid_no_patch(__func__); - return 0; + return SupportFormat(__func__, {"winfonts"}); } int APS5_VABI sceFontFtTermAliases() { 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..e0a3b3e116 --- /dev/null +++ b/core/libs/prx/libSceFreeType/CMakeLists.txt @@ -0,0 +1,29 @@ +add_library(libSceFreeType SHARED EXCLUDE_FROM_ALL + Export.cpp + src/GuestObjects.cpp +) +target_include_directories(libSceFreeType PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceFreeType PRIVATE freetype) +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) + if(WIN32) + 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..0a098e6024 --- /dev/null +++ b/core/libs/prx/libSceFreeType/Export.cpp @@ -0,0 +1,143 @@ +#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 { + +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; + FT_Library host = nullptr; + const FT_Error error = FT_Init_FreeType(&host); + if (error != 0) return error; + *alibrary = new GuestLibrary{host}; + return 0; +} + +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; + face->library = library; + face->host = host; + SyncFace(*face); + *aface = &face.release()->rec; + return 0; +} + +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; +} + +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_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); +} + +} diff --git a/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp b/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp new file mode 100644 index 0000000000..2cf157b746 --- /dev/null +++ b/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp @@ -0,0 +1,168 @@ +#ifndef CORE_LIBS_PRX_LIBSCEFREETYPE_INCLUDE_GUESTFREETYPE_HPP +#define CORE_LIBS_PRX_LIBSCEFREETYPE_INCLUDE_GUESTFREETYPE_HPP + +#include +#include + +namespace FreeType { + +struct GuestGeneric { + void* data; + void* finalizer; +}; + +struct GuestVector { + std::int64_t x; + std::int64_t y; +}; + +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 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 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(GuestBitmapSize) == 32 && offsetof(GuestBitmapSize, size) == 8); +static_assert(sizeof(GuestCharMapRec) == 16 && offsetof(GuestCharMapRec, platform_id) == 12); +static_assert(sizeof(GuestSizeMetrics) == 56 && offsetof(GuestSizeMetrics, x_scale) == 8); +static_assert(sizeof(GuestSizeRec) == 88 && offsetof(GuestSizeRec, metrics) == 24); +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..9abdae1f81 --- /dev/null +++ b/core/libs/prx/libSceFreeType/include/GuestObjects.hpp @@ -0,0 +1,38 @@ +#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 + +namespace FreeType { + +struct GuestLibrary { + FT_Library host = nullptr; +}; + +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..5319615c39 --- /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 = &face.slot; + rec.size = &face.size; + 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/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..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceFsInternalForVsh/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/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/libSceGameLiveStreaming/Export.cpp b/core/libs/prx/libSceGameLiveStreaming/Export.cpp index b28b535c8e..aa280dcad4 100644 --- a/core/libs/prx/libSceGameLiveStreaming/Export.cpp +++ b/core/libs/prx/libSceGameLiveStreaming/Export.cpp @@ -40,4 +40,9 @@ int APS5_VABI sceGameLiveStreamingGetProgramInfo() { return 0; } +int APS5_VABI sceGameLiveStreamingEnableLiveStreaming(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceGameUpdate/Export.cpp b/core/libs/prx/libSceGameUpdate/Export.cpp index 4c0d0bb575..37c0901309 100644 --- a/core/libs/prx/libSceGameUpdate/Export.cpp +++ b/core/libs/prx/libSceGameUpdate/Export.cpp @@ -7,44 +7,78 @@ // 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_assert(sizeof(GameUpdateCheckParam) == 0x30); +static_assert(sizeof(GameUpdateCheckResult) == 0x30); static std::atomic g_initialized{false}; +static int CheckRequestId(int request_id) { + if (request_id <= 0) { + return SCE_GAME_UPDATE_ERROR_REQUEST_NOT_FOUND; + } + if (!g_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; + return CheckRequestId(request_id); } 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; + if (request_id <= 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(request_id); } int APS5_VABI sceGameUpdateCreateRequest(void) { + if (!g_initialized.load()) { + return SCE_GAME_UPDATE_ERROR_NOT_INITIALIZED; + } return SCE_GAME_UPDATE_ERROR_OFFLINE; } int APS5_VABI sceGameUpdateDeleteRequest(int request_id) { - (void)request_id; - return 0; + return CheckRequestId(request_id); } int APS5_VABI sceGameUpdateGetAddcontLatestVersion(uint32_t service_label, const void* entitlement_label, GameUpdateAddcontVersionInfo* info) { (void)service_label; - if (!g_initialized.load()) { - return SCE_GAME_UPDATE_ERROR_NOT_INITIALIZED; - } if (info == nullptr) { return SCE_GAME_UPDATE_ERROR_INVALID_ARGUMENT; } if (entitlement_label == nullptr) { - NotImplemented_nid_no_patch(__func__); + return SCE_GAME_UPDATE_ERROR_INVALID_ARGUMENT; + } + if (!g_initialized.load()) { + return SCE_GAME_UPDATE_ERROR_NOT_INITIALIZED; } if (info->size < sizeof(GameUpdateAddcontVersionInfo)) { return SCE_GAME_UPDATE_ERROR_INVALID_SIZE; @@ -57,12 +91,16 @@ int APS5_VABI sceGameUpdateGetAddcontLatestVersion(uint32_t service_label, const } int APS5_VABI sceGameUpdateInitialize(void) { - g_initialized.store(true); + if (g_initialized.exchange(true)) { + return SCE_GAME_UPDATE_ERROR_ALREADY_INITIALIZED; + } return 0; } int APS5_VABI sceGameUpdateTerminate(void) { - g_initialized.store(false); + if (!g_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..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceGic/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/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..f8a8c67c6a --- /dev/null +++ b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp @@ -0,0 +1,598 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +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) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDebugReset(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +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) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDriverInternalRetrieveGnmInterfaceForGpuDebugger(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDriverInternalRetrieveGnmInterfaceForReplay(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDriverInternalRetrieveGnmInterfaceForResourceRegistration(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDriverInternalRetrieveGnmInterfaceForValidation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDriverInternalVirtualQuery(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDriverTraceInProgress(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmEndWorkload(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmFindResources(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmFindResourcesPublic(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +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) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +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) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetPhysicalCounterFromVirtualized(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetProtectionFaultTimeStamp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetResourceBaseAddressAndSizeInBytes(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetResourceName(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetResourceRegistrationBuffers(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetResourceShaderGuid(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetResourceType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetResourceUserData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +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) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmRaiseUserExceptionEvent(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmRegisterGdsResource(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmRegisterGnmLiveCallbackConfig(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmRegisterOwner(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmRegisterOwnerForSystem(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmRegisterResource(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +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) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSdmaConstFill(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSdmaCopyLinear(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSdmaCopyTiled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSdmaCopyWindow(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSdmaFlush(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSdmaGetMinCmdSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSdmaOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +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) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSetResourceUserData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSetSpiEnableSqCounters(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSetSpiEnableSqCountersForUnitInstance(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +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) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmUnregisterOwnerAndResources(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmUnregisterResource(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +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) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmValidateDisableDiagnostics2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmValidateDispatchCommandBuffers(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmValidateDrawCommandBuffers(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmValidateGetDiagnosticInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmValidateGetDiagnostics(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmValidateGetVersion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmValidateOnSubmitEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmValidateResetState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmValidationRegisterMemoryCheckCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} 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..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceHidControl/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/libSceHmd/Export.cpp b/core/libs/prx/libSceHmd/Export.cpp index 36c2ca3c06..7c1e7c68c7 100644 --- a/core/libs/prx/libSceHmd/Export.cpp +++ b/core/libs/prx/libSceHmd/Export.cpp @@ -1,4 +1,5 @@ #include "Hmd.hpp" +#include "prx/libc/include/General.hpp" #include @@ -68,4 +69,796 @@ std::int32_t APS5_VABI sceHmdInternalMmapIsConnect() { return 0; } +int APS5_VABI sceHmdInternalGetPUVersion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalEventInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetVirtualDisplaySize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSocialScreenSetOutput(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetVirtualDisplayDepth(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetEyeStatus(void) { + 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(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetDebugTextMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdDistortionGetWorkMemoryAlign(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalRegisterEventCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdDistortionGetWorkMemorySize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStartWideNear(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetPUSerialNumber(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetDebugSocialScreenMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetM2LedBrightness(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetIPD(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalMirroringModeSetAspect(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuGetStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalAnotherScreenSendVideo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetRequiredPUPVersion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionSetCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuCheckAfterPvt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalResetInertialSensor(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionSetUserEventStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdDistortionSetOutputMinColor(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetVirtualDisplayDepth(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGetDistortionWorkMemoryAlign(void) { + return 0x400; +} + +int APS5_VABI sceHmdReprojectionStartMultilayer(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalPushVr2dData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionClearUserEventEnd(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetIPD(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetVRMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalCrashReportReadDataSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetVr2dData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalAnotherScreenSendAudio(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetPuPositionParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionInitializeCapture(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGet2DEyeOffset(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetDebugSocialScreenMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetHmuPowerStatusForDebug(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGetDistortionWorkMemorySize(void) { + return 0x20000; +} + +int APS5_VABI sceHmdReprojectionSetDisplayBuffers(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalAnotherScreenSetFadeAndSwitch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalCreateSharedMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalMirroringModeSetAspectDebug(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetPuBuildNumber(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdDistortionGetCompoundEyeCorrectionCommand(void) { + return static_cast(0x81110009); +} + +int APS5_VABI sceHmdInternalSetS3dPassMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStopCapture(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalIsCommonDlgMiniAppVr2d(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalMmapGetSensorCalibrationData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdDistortionGetCorrectionCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionQueryOnionBuffAlign(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetDebugMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalIsMiniAppVr2d(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalIsCommonDlgVr2d(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionSetOutputMinColor(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetPortConnection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionDebugGetLastInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetM2LedOn(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGetFieldOfView(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionAddDisplayBuffer(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetIpdSettingEnableForSystemService(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetLedOn(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternal3dAudioSendData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetDeviceInformationByHandle(void) { + 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) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetDeviceInformation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStartCapture(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetDefaultLedData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionQueryGarlicBuffAlign(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetHmuPowerControl(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalBindDeviceWithUserId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetHmuSerialNumber(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdDistortionGetWideNearCorrectionCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalCheckS3dPassModeAvailable(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalAnotherScreenGetAudioStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSeparateModeGetAudioStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStartLiveCapture(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalIsGameVr2d(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGetAssyError(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionFinalize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSocialScreenSetFadeAndSwitch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetVirtualDisplayHeight(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdDistortionInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuSetMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetForcedCrash(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuCheckPartialUpdateAvailable(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSeparateModeClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetDebugTextMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalCrashReportClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetHmuPowerControlForDebug(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetVirtualDisplayHeight(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStart(void) { + 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(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalCrashReportCancel(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSeparateModeGetVideoStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalCheckDeviceModelMk3(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdSetVr2dEnable(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetSidetone(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSeparateModeOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalMmapGetModeId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdDistortionGet2dVrCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuSendSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGetWideNearDistortionCorrectionCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStartMultilayer2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGetDistortionCorrectionCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetDeviceConnection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetVirtualDisplaySize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionUnsetDisplayBuffers(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetHmuOpticalParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSeparateModeSendVideo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalResetLedForVrTracker(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionQueryOnionBuffSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStartWithOverlay(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionSetUserEventEnd(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalCrashReportOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetDemoMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStartWideNearWithOverlay(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGetDistortionParams(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuSend(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionClearUserEventStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetUserType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetDemoMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalMapSharedMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetCrashDumpInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetTv4kCapability(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuReset(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStart2dVr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalMmapGetCount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetDefaultLedData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGetInertialSensorData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternal3dAudioClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetBrightness(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSocialScreenGetFadeState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSeparateModeSendAudio(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetStatusReport(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetBrightness(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionFinalizeCapture(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetPortStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStop(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalAnotherScreenGetVideoStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternal3dAudioOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalAnotherScreenGetFadeState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetDebugGpo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetIpdSettingEnableForSystemService(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionDebugGetLastInfoMultilayer(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetPuRevision(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionQueryGarlicBuffSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGet2dVrCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceHmd2/CMakeLists.txt b/core/libs/prx/libSceHmd2/CMakeLists.txt index 84efff8feb..fc0c32cbf4 100644 --- a/core/libs/prx/libSceHmd2/CMakeLists.txt +++ b/core/libs/prx/libSceHmd2/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(libSceHmd2 SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceHmd2 PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceHmd2 PRIVATE libc) configure_windows_unwind(libSceHmd2) if(BUILD_TESTING) diff --git a/core/libs/prx/libSceHmd2/Export.cpp b/core/libs/prx/libSceHmd2/Export.cpp index 2e1740373c..f6f53d804c 100644 --- a/core/libs/prx/libSceHmd2/Export.cpp +++ b/core/libs/prx/libSceHmd2/Export.cpp @@ -1,22 +1,100 @@ -#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 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" { -std::int32_t APS5_VABI sceHmd2Initialize(const void* param) { - (void)param; - return SCE_HMD2_ERROR_UNSUPPORTED_FEATURE; +int APS5_VABI sceHmd2ReprojectionGetStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; } -int APS5_VABI sceHmd2Close() { +Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign() { + return {0x1106000, 0x100}; +} + +std::int32_t APS5_VABI sceHmd2Initialize(const void* param) { + (void)param; + return Hmd::UnsupportedFeature; +} + +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; +} + +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; +} + +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 sceHmd2GazeGetResult() { NotImplemented_nid_no_patch(__func__); return 0; } @@ -26,19 +104,30 @@ int APS5_VABI sceHmd2GazeGetResultForFoveatedRendering() { return 0; } -int APS5_VABI sceHmd2GetDeviceInformation() { - NotImplemented_nid_no_patch(__func__); +std::int32_t APS5_VABI sceHmd2GetDeviceInformation(Hmd2::DeviceInformation* info) { + std::lock_guard lock(stateMutex); + return GetDeviceInformation(info); +} + +std::int32_t APS5_VABI sceHmd2GetFieldOfViewWithoutHandle(Hmd2::FieldOfView* fieldOfView) { + std::lock_guard lock(stateMutex); + if (!fieldOfView) return Hmd2::ParameterNull; + if (!initialized) return Hmd2::NotInitialized; + *fieldOfView = DefaultFieldOfView; return 0; } -int APS5_VABI sceHmd2GetFieldOfViewWithoutHandle() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2Open() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceHmd2Open(std::int32_t userId, std::int32_t type, std::int32_t index, const void* param) { + std::lock_guard lock(stateMutex); + if (!initialized) return Hmd2::NotInitialized; + if (type != 0 || index != 0 || param) return Hmd2::ParameterInvalid; + if (userId != USER_SERVICE_INITIAL_USER_ID) { + if (CompiledSdkVersion() >= Hmd2::UserIdSdkVersion) return Hmd2::ParameterInvalid; + throw std::runtime_error("sceHmd2Open: user is not the initial user"); + } + if (openHandle != 0) return Hmd2::AlreadyOpened; + openHandle = DeviceHandle; + return DeviceHandle; } int APS5_VABI sceHmd2ReprojectionBeginFrame() { @@ -46,16 +135,34 @@ int APS5_VABI sceHmd2ReprojectionBeginFrame() { return 0; } +int APS5_VABI sceHmd2ReprojectionDisableMirroring() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceHmd2ReprojectionDisableVrMode() { NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI sceHmd2ReprojectionEnableMirroring() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceHmd2ReprojectionEnableVrMode() { NotImplemented_nid_no_patch(__func__); return 0; } +Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionGetMirroringDisplayBufferSizeAlign() { + return {0x5FA0000, 0x10000}; +} + +Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionGetMirroringWorkMemorySizeAlign() { + return {0x4000, 0x100}; +} + int APS5_VABI sceHmd2ReprojectionGetPredictedDisplayTime() { NotImplemented_nid_no_patch(__func__); return 0; @@ -71,12 +178,21 @@ int APS5_VABI sceHmd2ReprojectionQueryBufferSizeAlign() { return 0; } -int APS5_VABI sceHmd2ReprojectionQueryDisplayBufferSizeAlign() { +Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionQueryDisplayBufferSizeAlign() { + return {0x6000000, 0x10000}; +} + +int APS5_VABI sceHmd2ReprojectionSetAllowPositionalReprojection() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmd2ReprojectionSetAllowPositionalReprojection() { +int APS5_VABI sceHmd2ReprojectionSetMirroringOption() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2ReprojectionSetParam() { NotImplemented_nid_no_patch(__func__); return 0; } @@ -91,9 +207,613 @@ int APS5_VABI sceHmd2ReprojectionSetRenderConfig() { return 0; } -int APS5_VABI sceHmd2SetVibration() { +int APS5_VABI sceHmd2ReprojectionSetTiming() { NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI sceHmd2ReprojectionTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2ReprojectionSetEventGpuStart() { + 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); +APS5_EXPORT("jp0apqNGgi8", Hmd2BeforeInitializeZero); +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); +APS5_EXPORT("CVcL2Cu2AEI", Hmd2BeforeInitializeZero); +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); +APS5_EXPORT("-tnG5UJPvAc", Hmd2BeforeInitializeError0004); +APS5_EXPORT("FPhFmG+jXuQ", Hmd2BeforeInitializeError0004); +APS5_EXPORT("CKlkvNngofQ", Hmd2BeforeInitializeError0004); +APS5_EXPORT("yL1CZaciFWE", Hmd2BeforeInitializeError0004); +APS5_EXPORT("qlzgyG3s0RY", Hmd2BeforeInitializeError0004); +APS5_EXPORT("kTIDrDLzMv0", Hmd2BeforeInitializeError0004); +APS5_EXPORT("9Gee0ef8a+U", Hmd2BeforeInitializeError0004); +APS5_EXPORT("ecZVjQdwmBI", Hmd2BeforeInitializeError0004); + +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); +APS5_EXPORT("DIU1sFRRaiE", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("ZQvU4vDzZGQ", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("TJpyGGP9YEY", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("Dih+Odk+RC0", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("oaqdSJw+HVY", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("m35gtHuanXU", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("CTM7QPLJF9Q", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("+rExHeC9lP0", Hmd2BeforeInitializeParameterNull); + +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); +APS5_EXPORT("DIwqTkcZiJo", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("64ERZWVFBTE", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("z0+0gXUJWs8", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("rjAd4nCY4DM", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("Acyg4vTdGm0", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("aGOWaEeg04I", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("9up+EPhw1No", Hmd2BeforeInitializeReprojectionNotInitialized); + +std::int32_t APS5_VABI Hmd2BeforeInitializeError002C() { + return BeforeInitialize(Hmd2::Error002C, __func__); +} +APS5_EXPORT("V1kLQmP-17U", Hmd2BeforeInitializeError002C); + +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); +APS5_EXPORT("sDP7hZ-P6OI", Hmd2CheckFirstThenError0004); +APS5_EXPORT("J+eVNbN9ndY", Hmd2CheckFirstThenError0004); +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); +APS5_EXPORT("JngJzgNbu8U", Hmd2CheckFirstThenZero); +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); +APS5_EXPORT("LxPIfjHaY4c", Hmd2CheckFirstTwoThenParameterInvalid); +APS5_EXPORT("w8gBTTdg+po", Hmd2CheckFirstTwoThenParameterInvalid); + +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); +APS5_EXPORT("LCQzi5ILGrY", Hmd2CheckSecondThenParameterInvalid); + +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__); +} +APS5_EXPORT("wEHILc7TP04", Hmd2CheckSecondThenInvalidHandle); + +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..c5ae98b060 --- /dev/null +++ b/core/libs/prx/libSceHmd2/Hmd2.hpp @@ -0,0 +1,85 @@ +#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; +}; + +static_assert(sizeof(SizeAlign) == 16); + +} + +extern "C" { +std::int32_t APS5_VABI sceHmd2Initialize(const void* param); +std::int32_t APS5_VABI sceHmd2Terminate(); +std::int32_t APS5_VABI sceHmd2Open(std::int32_t userId, std::int32_t type, std::int32_t index, const void* param); +std::int32_t APS5_VABI sceHmd2Close(std::int32_t handle); +std::int32_t APS5_VABI sceHmd2GetDeviceInformation(Hmd2::DeviceInformation* info); +std::int32_t APS5_VABI sceHmd2GetDeviceInformationByHandle(std::int32_t handle, Hmd2::DeviceInformation* info); +std::int32_t APS5_VABI sceHmd2GetFieldOfView(std::int32_t handle, Hmd2::FieldOfView* fieldOfView); +std::int32_t APS5_VABI sceHmd2GetFieldOfViewWithoutHandle(Hmd2::FieldOfView* fieldOfView); +std::int32_t APS5_VABI sceHmd2SetVibration(std::int32_t handle, std::uint32_t level); +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/Export.cpp b/core/libs/prx/libSceHmdSetupDialog/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceHmdSetupDialog/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/libSceHttp/CMakeLists.txt b/core/libs/prx/libSceHttp/CMakeLists.txt index 5c930a4e29..48a098bec6 100644 --- a/core/libs/prx/libSceHttp/CMakeLists.txt +++ b/core/libs/prx/libSceHttp/CMakeLists.txt @@ -5,7 +5,8 @@ configure_windows_unwind(libSceHttp) if(BUILD_TESTING) add_test_executable(guest_http_tests ${LIBS_INCLUDE_DIR}/tests/GuestHttp.cpp) target_include_directories(guest_http_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_http_tests PRIVATE libSceHttp) + target_link_libraries(guest_http_tests PRIVATE libSceHttp libc) + configure_windows_unwind(guest_http_tests) add_test(NAME guest_http COMMAND guest_http_tests) add_test_executable(guest_http_response_header_tests ${LIBS_INCLUDE_DIR}/tests/GuestHttpResponseHeader.cpp) target_include_directories(guest_http_response_header_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libSceHttp/Export.cpp b/core/libs/prx/libSceHttp/Export.cpp index 76a5774516..19bec94c78 100644 --- a/core/libs/prx/libSceHttp/Export.cpp +++ b/core/libs/prx/libSceHttp/Export.cpp @@ -1,18 +1,120 @@ #include #include +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#endif #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libSceHttp/src/HttpErrors.hpp" #include #include +#include #include +#include +#include +#include #include #include -#include +#include +#include +#include // No network is emulated: contexts, templates and requests can be created, but any request // that would touch the network fails with the library's network error. static std::atomic g_nextHandle{1}; +using HttpRequestStatusCallback = void (APS5_VABI *)(int, int, void*); +struct RequestStatusRegistration { + HttpRequestStatusCallback callback; + void* argument; +}; +static std::mutex g_statusMutex; +static std::map g_statusCallbacks; +struct HttpsCaList { + void* certs; + int certsNum; +}; + +#ifdef _WIN32 +struct HostMutex { + SRWLOCK native = SRWLOCK_INIT; + + HostMutex() = default; + HostMutex(const HostMutex&) = delete; + HostMutex& operator=(const HostMutex&) = delete; + + void lock() { AcquireSRWLockExclusive(&native); } + void unlock() { ReleaseSRWLockExclusive(&native); } +}; +#else +using HostMutex = std::mutex; +#endif + +class HostCondition { +public: + HostCondition() = default; + HostCondition(const HostCondition&) = delete; + HostCondition& operator=(const HostCondition&) = delete; + + void NotifyAll() { +#ifdef _WIN32 + WakeAllConditionVariable(&native); +#else + native.notify_all(); +#endif + } + + void Wait(std::unique_lock& lock) { +#ifdef _WIN32 + if (!SleepConditionVariableSRW(&native, &lock.mutex()->native, INFINITE, 0)) + throw std::runtime_error("libSceHttp: native condition wait failed"); +#else + native.wait(lock); +#endif + } + + void WaitUntil(std::unique_lock& lock, std::chrono::steady_clock::time_point deadline) { +#ifdef _WIN32 + const auto now = std::chrono::steady_clock::now(); + if (deadline <= now) return; + const auto milliseconds = std::chrono::ceil(deadline - now).count(); + if (!SleepConditionVariableSRW(&native, &lock.mutex()->native, static_cast(milliseconds), 0) && + GetLastError() != ERROR_TIMEOUT) + throw std::runtime_error("libSceHttp: native condition wait failed"); +#else + native.wait_until(lock, deadline); +#endif + } + +private: +#ifdef _WIN32 + CONDITION_VARIABLE native = CONDITION_VARIABLE_INIT; +#else + std::condition_variable native; +#endif +}; + +struct HttpEpoll { + HostMutex mutex; + HostCondition wake; + bool aborted = false; + bool destroyed = false; +}; + +static std::mutex g_stateMutex; +static std::unordered_set g_contexts; +static std::unordered_map> g_epolls; + +static std::shared_ptr FindEpoll(HttpEpollHandle eh) { + const auto it = g_epolls.find(eh); + return it == g_epolls.end() ? nullptr : it->second; +} + +static void MarkDestroyed(HttpEpoll& epoll) { + std::lock_guard lock(epoll.mutex); + epoll.destroyed = true; + epoll.wake.NotifyAll(); +} extern "C" { @@ -29,6 +131,20 @@ 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; @@ -46,9 +162,13 @@ int APS5_VABI sceHttpCreateConnectionWithURL(int tmpl_id, const char* url, int e } int APS5_VABI sceHttpCreateEpoll(int http_ctx_id, HttpEpollHandle* eh) { - (void)http_ctx_id; + std::lock_guard lock(g_stateMutex); + if (g_contexts.empty()) return ERROR_BEFORE_INIT; + if (!g_contexts.contains(http_ctx_id)) return ERROR_INVALID_ID; if (!eh) return ERROR_INVALID_VALUE; - *eh = new HttpEpoll{}; + auto epoll = std::make_shared(); + *eh = epoll.get(); + g_epolls.emplace(epoll.get(), std::move(epoll)); return 0; } @@ -82,7 +202,8 @@ int APS5_VABI sceHttpDeleteConnection(int conn_id) { } int APS5_VABI sceHttpDeleteRequest(int req_id) { - (void)req_id; + std::lock_guard lock(g_statusMutex); + g_statusCallbacks.erase(req_id); return 0; } @@ -92,8 +213,17 @@ int APS5_VABI sceHttpDeleteTemplate(int tmpl_id) { } int APS5_VABI sceHttpDestroyEpoll(int http_ctx_id, HttpEpollHandle eh) { - (void)http_ctx_id; - delete eh; + std::shared_ptr epoll; + { + std::lock_guard lock(g_stateMutex); + if (g_contexts.empty()) return ERROR_BEFORE_INIT; + if (!g_contexts.contains(http_ctx_id)) return ERROR_INVALID_ID; + if (!eh) return ERROR_INVALID_VALUE; + epoll = FindEpoll(eh); + if (!epoll) return ERROR_INVALID_ID; + g_epolls.erase(eh); + } + MarkDestroyed(*epoll); return 0; } @@ -121,7 +251,10 @@ int APS5_VABI sceHttpInit_nid_postfix(int memid, int ssl_ctx_id, uint64_t pool_s (void)memid; (void)ssl_ctx_id; (void)pool_size; - return g_nextHandle.fetch_add(1, std::memory_order_relaxed); + const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); + std::lock_guard lock(g_stateMutex); + g_contexts.insert(id); + return id; } int APS5_VABI sceHttpsDisableOption(int id, uint32_t ssl_flags) { @@ -188,6 +321,12 @@ int APS5_VABI sceHttpSetRecvTimeOut(int id, uint32_t usec) { return 0; } +int APS5_VABI sceHttpSetRecvBlockSize(int id, uint32_t block_size) { + (void)id; + (void)block_size; + return 0; +} + int APS5_VABI sceHttpSetResponseHeaderMaxSize(int id, uint64_t header_size) { (void)id; (void)header_size; @@ -232,7 +371,16 @@ int APS5_VABI sceHttpsSetSslCallback(int id, HttpsCallback cbfunc, void* user_ar } int APS5_VABI sceHttpTerm_nid_postfix(int http_ctx_id) { - (void)http_ctx_id; + std::vector> epolls; + { + 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; + if (!g_contexts.empty()) return 0; + for (auto& [handle, epoll] : g_epolls) epolls.push_back(std::move(epoll)); + g_epolls.clear(); + } + for (const auto& epoll : epolls) MarkDestroyed(*epoll); return 0; } @@ -242,10 +390,23 @@ int APS5_VABI sceHttpUnsetEpoll(int id) { } int APS5_VABI sceHttpWaitRequest(HttpEpollHandle eh, HttpNBEvent* nbev, int maxevents, int timeout) { - if (!eh || !nbev || maxevents <= 0) return ERROR_INVALID_VALUE; - if (timeout < 0) NotImplemented_nid_no_patch(__func__); - std::this_thread::sleep_for(std::chrono::microseconds(timeout)); - return 0; + std::shared_ptr epoll; + { + std::lock_guard lock(g_stateMutex); + if (g_contexts.empty()) return ERROR_BEFORE_INIT; + if (!eh || !nbev || maxevents <= 0) return ERROR_INVALID_VALUE; + epoll = FindEpoll(eh); + if (!epoll) return ERROR_INVALID_ID; + } + std::unique_lock lock(epoll->mutex); + const auto woken = [&] { return epoll->aborted || epoll->destroyed; }; + if (timeout < 0) { + while (!woken()) epoll->wake.Wait(lock); + } else { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::microseconds(timeout); + while (!woken() && std::chrono::steady_clock::now() < deadline) epoll->wake.WaitUntil(lock, deadline); + } + return epoll->aborted ? ERROR_ABORTED : 0; } int APS5_VABI sceHttpCreateRequestWithURL(int conn_id, int method, const char* url, uint64_t content_length) { @@ -281,8 +442,11 @@ int APS5_VABI sceHttpSetInflateGZIPEnabled(int id, int enable) { return 0; } -int APS5_VABI sceHttpSetRequestStatusCallback(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceHttpSetRequestStatusCallback(int id, HttpRequestStatusCallback callback, void* argument) { + if (id <= 0 || id >= g_nextHandle.load(std::memory_order_relaxed)) return static_cast(0x80431100u); + std::lock_guard lock(g_statusMutex); + if (callback) g_statusCallbacks[id] = {callback, argument}; + else g_statusCallbacks.erase(id); return 0; } @@ -317,35 +481,42 @@ 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) { @@ -415,4 +586,278 @@ int APS5_VABI sceHttpSetRedirectCallback(int id, HttpRedirectCallback cbfunc, vo return 0; } +int APS5_VABI sceHttpAbortWaitRequest(HttpEpollHandle eh) { + std::shared_ptr epoll; + { + std::lock_guard lock(g_stateMutex); + if (g_contexts.empty()) return ERROR_BEFORE_INIT; + if (!eh) return ERROR_INVALID_VALUE; + epoll = FindEpoll(eh); + if (!epoll) return ERROR_INVALID_ID; + } + std::lock_guard lock(epoll->mutex); + epoll->aborted = true; + epoll->wake.NotifyAll(); + return 0; +} + +int APS5_VABI sceHttpsGetCaList(int http_ctx_id, HttpsCaList* ca_list) { + (void)http_ctx_id; + if (!ca_list) return ERROR_INVALID_VALUE; + ca_list->certs = nullptr; + ca_list->certsNum = 0; + return 0; +} + +int APS5_VABI sceHttpsFreeCaList(int http_ctx_id, HttpsCaList* ca_list) { + (void)http_ctx_id; + if (!ca_list) return ERROR_INVALID_VALUE; + ca_list->certs = nullptr; + ca_list->certsNum = 0; + return 0; +} + +int APS5_VABI sceHttpAuthCacheImport(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpAuthCacheExport(void) { + 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(void) { + 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(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpDbgGetRequestStat(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpAbortRequestForce(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpAddRequestHeaderRaw(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpRemoveRequestHeader(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetAuthEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetAutoRedirect(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpTrySetNonblock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetNonblock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpTryGetNonblock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetEpollId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetEpollId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetEpoll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetCookie(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetCookieEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpAddCookie(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetCookieSendCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCookieExport(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCookieImport(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetCookieTotalMaxSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetCookieMaxSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetCookieMaxNum(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetCookieMaxNumPerDomain(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetCookieStats(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpsEnableOptionPrivate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpsDisableOptionPrivate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetPriorityOption(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetPolicyOption(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpRequestGetAllHeaders(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetAcceptEncodingGZIPEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetAcceptEncodingGZIPEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetDefaultAcceptEncodingGZIPEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetProxy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetSocketCreationCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetHttp09Enabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetRegisteredCtxIds(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetConnectionStat(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpAddQuery(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetDelayBuildRequestEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheRedirectedConnectionEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceHttp/src/HttpErrors.hpp b/core/libs/prx/libSceHttp/src/HttpErrors.hpp index b95f8d25b1..1b18ec3e1e 100644 --- a/core/libs/prx/libSceHttp/src/HttpErrors.hpp +++ b/core/libs/prx/libSceHttp/src/HttpErrors.hpp @@ -1,8 +1,11 @@ #ifndef CORE_LIBS_PRX_LIBSCEHTTP_SRC_HTTPERRORS_HPP #define CORE_LIBS_PRX_LIBSCEHTTP_SRC_HTTPERRORS_HPP +inline constexpr int ERROR_BEFORE_INIT = static_cast(0x80431001); inline constexpr int ERROR_OUT_OF_MEMORY = static_cast(0x80431022); inline constexpr int ERROR_NETWORK = static_cast(0x80431063); +inline constexpr int ERROR_ABORTED = static_cast(0x80431080); +inline constexpr int ERROR_INVALID_ID = static_cast(0x80431100); inline constexpr int ERROR_INVALID_VALUE = static_cast(0x804311FE); inline constexpr int ERROR_INVALID_URL = static_cast(0x80433060); inline constexpr int ERROR_PARSE_HTTP_INVALID_RESPONSE = static_cast(0x80432060); diff --git a/core/libs/prx/libSceHttp/src/Uri.cpp b/core/libs/prx/libSceHttp/src/Uri.cpp index 8bee28c3eb..670a29e7eb 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 @@ -34,10 +33,22 @@ struct UriParts { 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; } @@ -49,7 +60,7 @@ uint16_t defaultPort(std::string_view scheme) { } 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) { @@ -76,9 +87,9 @@ std::string removeDotSegments(std::string_view input) { 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])) { 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; @@ -133,7 +144,7 @@ int parseUri(std::string_view uri, UriParts& parts) { 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; + if (!isAsciiDigit(c)) return ERROR_INVALID_URL; portValue = portValue * 10 + static_cast(c - '0'); } if (portValue > 0xFFFF) return ERROR_INVALID_URL; @@ -182,7 +193,7 @@ int APS5_VABI sceHttpUriBuild(char* out, size_t* require, size_t prepare, const std::string uri; if (option & URI_BUILD_WITH_SCHEME && !scheme.empty()) uri.append(scheme).push_back(':'); - if (!src_element->opaque && option & URI_BUILD_WITH_HOSTNAME) uri.append("//"); + if (!src_element->opaque) uri.append("//"); uri.append(username); if (!password.empty()) uri.append(":").append(password); if (!username.empty() || !password.empty()) uri.push_back('@'); @@ -205,7 +216,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('%'); @@ -217,21 +229,31 @@ int APS5_VABI sceHttpUriEscape(char* out, size_t* require, size_t prepare, const } int APS5_VABI sceHttpUriUnescape(char* out, size_t* require, size_t prepare, const char* in) { - if (!in) return ERROR_INVALID_VALUE; + if (!in || (!out && !require)) return ERROR_INVALID_VALUE; + const auto hexDigit = [](char c) -> int { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; + }; 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; - } + if (input[i] != '%') { + decoded.push_back(input[i]); + continue; } - decoded.push_back(input[i]); + if (input.size() - i < 3) return ERROR_INVALID_URL; + const int high = hexDigit(input[i + 1]); + const int low = hexDigit(input[i + 2]); + if (high < 0 || low < 0) return ERROR_INVALID_URL; + decoded.push_back(static_cast(high * 16 + low)); + i += 2; + } + if (out && prepare < decoded.size() + 1) { + std::memcpy(out, decoded.data(), std::min(prepare, decoded.size())); + return ERROR_OUT_OF_MEMORY; } return copyOut(decoded, out, require, prepare); } @@ -298,7 +320,7 @@ int APS5_VABI sceHttpUriParse(SceHttpUriElement* out, const char* src_url, void* char* strings[7]; for (size_t i = 0; i < 7; ++i) { strings[i] = next; - std::memcpy(next, fields[i].data(), fields[i].size()); + if (!fields[i].empty()) std::memcpy(next, fields[i].data(), fields[i].size()); next[fields[i].size()] = '\0'; next += fields[i].size() + 1; } @@ -315,12 +337,46 @@ 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; + + const char* const fields[] = {src->scheme, src->hostname, src->path, src->username, src->password}; + SceHttpUriElement copy{}; + char** const slots[] = {©.scheme, ©.hostname, ©.path, ©.username, ©.password}; + if (write) std::memset(pool, 0, prepare); + size_t used = 0; + for (size_t i = 0; i < 5; ++i) { + const size_t length = fields[i] ? std::strlen(fields[i]) : 0; + const size_t needed = length + 1; + if (write) { + if (needed > prepare - used) { + *out = {}; + std::memset(pool, 0, prepare); + return ERROR_OUT_OF_MEMORY; + } + char* next = static_cast(pool) + used; + if (length != 0) std::memcpy(next, fields[i], length); + next[length] = '\0'; + *slots[i] = next; + } + used += needed; + } + if (write) { + copy.port = src->port; + *out = copy; + } + 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 +391,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 215d674e5b..3d681f4ede 100644 --- a/core/libs/prx/libSceHttp2/Export.cpp +++ b/core/libs/prx/libSceHttp2/Export.cpp @@ -1,21 +1,63 @@ #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" #include #include -#include #include #include #include +#include // No network is emulated: contexts, templates and requests can be created, but any request // that would touch the network fails with the library's network error. static constexpr int ERROR_NETWORK = static_cast(0x80436063); +static constexpr int ERROR_INVALID_ID = static_cast(0x817B1100); +static constexpr int ERROR_NULL_POINTER = static_cast(0x817B1225); + +enum class Http2Kind : std::uint8_t { Context, Template, Request, CookieBox }; + +struct Http2Handle { + Http2Kind kind; + int owner; + std::size_t poolSize; +}; + static std::atomic g_nextHandle{1}; -static std::mutex g_poolsMutex; -static std::map g_pools; +static std::mutex g_handlesMutex; +static std::unordered_map g_handles; + +static Http2Handle* FindHandle(int id, Http2Kind kind) { + const auto entry = g_handles.find(id); + return entry != g_handles.end() && entry->second.kind == kind ? &entry->second : nullptr; +} + +static Http2Handle* FindAnyHandle(int id) { + const auto entry = g_handles.find(id); + return entry != g_handles.end() ? &entry->second : nullptr; +} + +static std::vector ReleaseOwnedHandles(int owner) { + std::vector owned; + for (const auto& entry : g_handles) { + if (entry.second.owner == owner) owned.push_back(entry.first); + } + std::vector removed; + for (const int id : owned) { + const std::vector descendants = ReleaseOwnedHandles(id); + removed.insert(removed.end(), descendants.begin(), descendants.end()); + g_handles.erase(id); + removed.push_back(id); + } + return removed; +} + +static bool HandleIsLive(int id) { + std::lock_guard lock(g_handlesMutex); + return FindAnyHandle(id) != nullptr; +} struct Http2MemoryPoolStats { size_t pool_size; @@ -27,21 +69,36 @@ struct Http2MemoryPoolStats { static std::mutex g_completionsMutex; static std::unordered_map> g_completions; -static void CompleteAsync(const char* function, int req_id, const Http2AsyncOption* kqueue_option, const void* option) { - if (kqueue_option == nullptr) NotImplemented_nid_no_patch(function); +static void EraseCompletions(const std::vector& ids) { + std::lock_guard lock(g_completionsMutex); + for (const int id : ids) g_completions.erase(id); +} + +static int CompleteAsync(const char* function, int req_id, const Http2AsyncOption* kqueue_option, const void* option) { if (option != nullptr) NotImplemented_nid_no_patch(function); + bool queued = false; { - std::lock_guard lock(g_completionsMutex); - Http2AsyncResult completion{}; - completion.req_id = req_id; - completion.result = ERROR_NETWORK; - g_completions[req_id].push_back(completion); + std::lock_guard handlesLock(g_handlesMutex); + if (FindHandle(req_id, Http2Kind::Request) != nullptr) { + std::lock_guard completionsLock(g_completionsMutex); + Http2AsyncResult completion{}; + completion.req_id = req_id; + completion.result = ERROR_NETWORK; + g_completions[req_id].push_back(completion); + queued = true; + } } + if (!queued) return ERROR_INVALID_ID; + if (kqueue_option == nullptr) return 0; if (EqueueTriggerEvent_nid_postfix(kqueue_option->equeue, static_cast(kqueue_option->user_event_id), EVFILT_USER, kqueue_option->user_data) != 0) { - std::lock_guard lock(g_completionsMutex); - g_completions[req_id].pop_back(); + { + std::lock_guard lock(g_completionsMutex); + const auto pending = g_completions.find(req_id); + if (pending != g_completions.end() && !pending->second.empty()) pending->second.pop_back(); + } NotImplemented_nid_no_patch(function); } + return 0; } extern "C" { @@ -55,29 +112,46 @@ int APS5_VABI sceHttp2AddRequestHeader(int id, const char* name, const char* val } int APS5_VABI sceHttp2CreateRequestWithURL(int tmpl_id, const char* method, const char* url, uint64_t content_length) { - (void)tmpl_id; (void)method; (void)url; (void)content_length; - return g_nextHandle.fetch_add(1, std::memory_order_relaxed); + std::lock_guard lock(g_handlesMutex); + if (FindHandle(tmpl_id, Http2Kind::Template) == nullptr) return ERROR_INVALID_ID; + const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); + g_handles[id] = {Http2Kind::Request, tmpl_id, 0}; + return id; } int APS5_VABI sceHttp2CreateTemplate(int lib_http2_ctx_id, const char* user_agent, int http_ver, int is_auto_proxy_conf) { - (void)lib_http2_ctx_id; (void)user_agent; (void)http_ver; (void)is_auto_proxy_conf; - return g_nextHandle.fetch_add(1, std::memory_order_relaxed); + 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::Template, lib_http2_ctx_id, 0}; + return id; } int APS5_VABI sceHttp2DeleteRequest(int req_id) { - std::lock_guard lock(g_completionsMutex); - g_completions.erase(req_id); + { + std::lock_guard lock(g_handlesMutex); + if (FindHandle(req_id, Http2Kind::Request) == nullptr) return ERROR_INVALID_ID; + g_handles.erase(req_id); + } + EraseCompletions({req_id}); return 0; } int APS5_VABI sceHttp2DeleteTemplate(int tmpl_id) { - (void)tmpl_id; + std::vector removed; + { + std::lock_guard lock(g_handlesMutex); + if (FindHandle(tmpl_id, Http2Kind::Template) == nullptr) return ERROR_INVALID_ID; + removed = ReleaseOwnedHandles(tmpl_id); + g_handles.erase(tmpl_id); + } + EraseCompletions(removed); return 0; } @@ -106,8 +180,8 @@ int APS5_VABI sceHttp2Init(int libnet_mem_id, int libssl_ctx_id, size_t pool_siz (void)libssl_ctx_id; (void)max_concurrent_request; const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); - std::lock_guard lock(g_poolsMutex); - g_pools[id] = pool_size; + std::lock_guard lock(g_handlesMutex); + g_handles[id] = {Http2Kind::Context, 0, pool_size}; return id; } @@ -137,8 +211,7 @@ int APS5_VABI sceHttp2SendRequest(int req_id, const void* post_data, size_t size int APS5_VABI sceHttp2SendRequestAsync(int req_id, const void* post_data, size_t size, Http2AsyncOption* kqueue_option, void* option) { (void)post_data; (void)size; - CompleteAsync(__func__, req_id, kqueue_option, option); - return 0; + return CompleteAsync(__func__, req_id, kqueue_option, option); } int APS5_VABI sceHttp2SetAuthEnabled(int id, int is_enable) { @@ -216,26 +289,29 @@ int APS5_VABI sceHttp2SetTimeOut(int id, uint32_t usec) { } int APS5_VABI sceHttp2SslDisableOption(int id, uint32_t ssl_flags) { - (void)id; (void)ssl_flags; - return 0; + return HandleIsLive(id) ? 0 : ERROR_INVALID_ID; } int APS5_VABI sceHttp2SslEnableOption(int id, uint32_t ssl_flags) { - (void)id; (void)ssl_flags; - return 0; + return HandleIsLive(id) ? 0 : ERROR_INVALID_ID; } int APS5_VABI sceHttp2SetMinSslVersion(int id, uint32_t ssl_version) { - (void)id; (void)ssl_version; - return 0; + return HandleIsLive(id) ? 0 : ERROR_INVALID_ID; } int APS5_VABI sceHttp2Term(int lib_http2_ctx_id) { - std::lock_guard lock(g_poolsMutex); - g_pools.erase(lib_http2_ctx_id); + std::vector removed; + { + std::lock_guard lock(g_handlesMutex); + if (FindHandle(lib_http2_ctx_id, Http2Kind::Context) == nullptr) return ERROR_INVALID_ID; + removed = ReleaseOwnedHandles(lib_http2_ctx_id); + g_handles.erase(lib_http2_ctx_id); + } + EraseCompletions(removed); return 0; } @@ -256,11 +332,14 @@ int APS5_VABI sceHttp2AbortRequest(int req_id) { } int APS5_VABI sceHttp2GetMemoryPoolStats(int lib_http2_ctx_id, Http2MemoryPoolStats* stats) { - if (stats == nullptr) APS5_INVALID_ARG_EX; - std::lock_guard lock(g_poolsMutex); - const auto pool = g_pools.find(lib_http2_ctx_id); - if (pool == g_pools.end()) throw std::invalid_argument("sceHttp2GetMemoryPoolStats: unknown context"); - *stats = {pool->second, 0, 0, 0}; + if (stats == nullptr) return ERROR_NULL_POINTER; + std::lock_guard lock(g_handlesMutex); + const Http2Handle* handle = FindHandle(lib_http2_ctx_id, Http2Kind::Context); + if (handle == nullptr) { + std::memset(stats, 0, sizeof(*stats)); + return ERROR_INVALID_ID; + } + *stats = {handle->poolSize, 0, 0, 0}; return 0; } @@ -270,8 +349,18 @@ int APS5_VABI sceHttp2CookieFlush(int id) { } int APS5_VABI sceHttp2CreateCookieBox(int lib_http2_ctx_id) { - (void)lib_http2_ctx_id; - return g_nextHandle.fetch_add(1, std::memory_order_relaxed); + 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}; + return id; +} + +int APS5_VABI sceHttp2DeleteCookieBox(int cookie_box_id) { + std::lock_guard lock(g_handlesMutex); + if (FindHandle(cookie_box_id, Http2Kind::CookieBox) == nullptr) return ERROR_INVALID_ID; + g_handles.erase(cookie_box_id); + return 0; } int APS5_VABI sceHttp2SetCookieBox(int id, int cookie_box_id) { @@ -312,4 +401,103 @@ int APS5_VABI sceHttp2WebSocketSendDataMessageAsync() { return 0; } +int APS5_VABI sceHttp2WebSocketSetPingInterval() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2WebSocketSetPingTimeout() { + 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..6f5d37ccb1 --- /dev/null +++ b/core/libs/prx/libSceHttpCache/Export.cpp @@ -0,0 +1,142 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceHttpCacheClear() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheClearAll() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheCompleteRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheCreateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheCreateRequestWithTag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheDeleteRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheReadData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheRetrieve() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheRetrieveWithMemoryPool() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheRevalidate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheRevalidateWithMemoryPool() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheSetCacheSharing() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheSetData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheSetQuota() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheSetResponseHeader() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheSystemClearAll() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheSystemInit() { + 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() { + 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..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceHubAppUtil/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/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..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceIdu/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/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 489c43cc1a..460b1fed31 100644 --- a/core/libs/prx/libSceIme/CMakeLists.txt +++ b/core/libs/prx/libSceIme/CMakeLists.txt @@ -22,4 +22,25 @@ if(BUILD_TESTING) set_tests_properties(guest_ime_keyboard PROPERTIES ENVIRONMENT_MODIFICATION "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}) + target_link_libraries(guest_ime_open_tests PRIVATE libSceIme libc) + configure_windows_unwind(guest_ime_open_tests) + add_test(NAME guest_ime_open COMMAND guest_ime_open_tests) + if(WIN32) + set_tests_properties(guest_ime_open PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceIme/Export.cpp b/core/libs/prx/libSceIme/Export.cpp index 4675c980f5..6eaf4cbbe8 100644 --- a/core/libs/prx/libSceIme/Export.cpp +++ b/core/libs/prx/libSceIme/Export.cpp @@ -1,58 +1,441 @@ +#include +#include #include #include #include +#include #include +#include #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" namespace { +constexpr int ErrorBusy = static_cast(0x80bc0001u); constexpr int ErrorNotOpened = static_cast(0x80bc0002u); constexpr int ErrorConnectionFailed = static_cast(0x80bc0004u); +constexpr int ErrorInvalidText = static_cast(0x80bc0006u); constexpr int ErrorInvalidUserId = static_cast(0x80bc0010u); constexpr int ErrorInvalidType = static_cast(0x80bc0011u); +constexpr int ErrorInvalidEnterLabel = static_cast(0x80bc0013u); +constexpr int ErrorInvalidInputMethod = static_cast(0x80bc0014u); constexpr int ErrorInvalidOption = static_cast(0x80bc0015u); -constexpr int ErrorInvalidAddress = static_cast(0x80bc0031u); +constexpr int ErrorInvalidMaxTextLength = static_cast(0x80bc0016u); +constexpr int ErrorInvalidInputTextBuffer = static_cast(0x80bc0017u); +constexpr int ErrorInvalidPosX = static_cast(0x80bc0018u); +constexpr int ErrorInvalidPosY = static_cast(0x80bc0019u); +constexpr int ErrorInvalidHorizontalAlignment = static_cast(0x80bc001au); +constexpr int ErrorInvalidVerticalAlignment = static_cast(0x80bc001bu); +constexpr int ErrorInvalidExtended = static_cast(0x80bc001cu); +constexpr int ErrorInvalidWork = static_cast(0x80bc0020u); +constexpr int ErrorInvalidHandler = static_cast(0x80bc0022u); constexpr int ErrorNoResourceId = static_cast(0x80bc0023u); constexpr int ErrorInvalidMode = static_cast(0x80bc0024u); +constexpr int ErrorInvalidParam = static_cast(0x80bc0030u); +constexpr int ErrorInvalidAddress = static_cast(0x80bc0031u); +constexpr int ErrorInvalidReserved = static_cast(0x80bc0032u); +constexpr int ErrorInternal = static_cast(0x80bc00ffu); constexpr uint32_t ValidKeyboardModes = 0x0000007f; constexpr uint32_t TypeNumber = 4; constexpr uint32_t ValidOptions = 0x00007bff; +constexpr uint32_t OptionMultiline = 0x00000001; constexpr uint32_t OptionUseOver2K = 0x00004000; +constexpr uint32_t EnterLabelLast = 3; +constexpr uint32_t AlignmentLast = 2; +constexpr uint32_t PanelPriorityLast = 3; +constexpr uint32_t MaxTextLength = 2048; constexpr int32_t UserIdInvalid = -1; +constexpr uint32_t EventOpen = 0; +constexpr uint32_t EventUpdateText = 1; +constexpr uint32_t EventUpdateCaret = 2; +constexpr uint32_t EventPressClose = 4; +constexpr uint32_t EventPressEnter = 5; +constexpr uint32_t CaretMoveLeft = 1; +constexpr uint32_t CaretMoveRight = 2; + +struct Session { + Param param{}; + 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; + *width = (param.type == TypeNumber ? 370u : 793u) * scale; + *height = (param.type == TypeNumber ? 402u : 408u) * scale; +} + +bool IsHighSurrogate(char16_t unit) { return unit >= 0xd800 && unit <= 0xdbff; } +bool IsLowSurrogate(char16_t unit) { return unit >= 0xdc00 && unit <= 0xdfff; } + +bool ValidText(const char16_t* text, std::size_t length, bool multiline) { + for (std::size_t i = 0; i < length; ++i) { + const char16_t unit = text[i]; + if (!multiline && (unit == u'\n' || unit == u'\r')) return false; + if (IsLowSurrogate(unit)) return false; + if (IsHighSurrogate(unit)) { + if (i + 1 >= length || !IsLowSurrogate(text[i + 1])) return false; + ++i; + } + } + return true; +} + +std::size_t TextLength(const char16_t* text, std::size_t limit) { + std::size_t length = 0; + while (length < limit && text[length] != u'\0') ++length; + return length; +} + +std::u16string DecodeUtf8(const char* text, std::size_t size) { + std::u16string units; + for (std::size_t i = 0; i < size;) { + const auto lead = static_cast(text[i]); + std::uint32_t codePoint = 0; + std::size_t extra = 0; + if (lead < 0x80) codePoint = lead; + else if ((lead & 0xe0) == 0xc0) { codePoint = lead & 0x1f; extra = 1; } + else if ((lead & 0xf0) == 0xe0) { codePoint = lead & 0x0f; extra = 2; } + else if ((lead & 0xf8) == 0xf0) { codePoint = lead & 0x07; extra = 3; } + else throw std::invalid_argument("IME host input: invalid UTF-8 lead byte"); + if (extra != 0 && i + extra >= size) throw std::invalid_argument("IME host input: truncated UTF-8 sequence"); + for (std::size_t k = 1; k <= extra; ++k) { + const auto next = static_cast(text[i + k]); + if ((next & 0xc0) != 0x80) throw std::invalid_argument("IME host input: invalid UTF-8 continuation byte"); + codePoint = (codePoint << 6) | (next & 0x3f); + } + i += extra + 1; + if (codePoint >= 0x10000) { + codePoint -= 0x10000; + units.push_back(static_cast(0xd800 + (codePoint >> 10))); + units.push_back(static_cast(0xdc00 + (codePoint & 0x3ff))); + } else { + units.push_back(static_cast(codePoint)); + } + } + return units; +} + +void SyncBuffers(Session& session) { + const std::size_t bytes = session.text.size() * sizeof(char16_t); + std::memcpy(session.param.input_text_buffer, session.text.data(), bytes); + session.param.input_text_buffer[session.text.size()] = u'\0'; + auto* work = static_cast(session.param.work); + std::memcpy(work, session.text.data(), bytes); + work[session.text.size()] = u'\0'; +} + +void QueueText(Session& session, uint32_t id, uint32_t index, int32_t delta) { + ImeEvent event{}; + event.id = id; + event.param.text.str = static_cast(session.param.work); + event.param.text.caret_index = session.caret; + event.param.text.area_num = 1; + event.param.text.text_area[0].mode = TextAreaMode::Edit; + event.param.text.text_area[0].index = index; + event.param.text.text_area[0].length = delta; + session.events.push_back(event); +} + +void QueueCaretSteps(Session& session, uint32_t direction, std::size_t steps) { + for (std::size_t i = 0; i < steps; ++i) { + ImeEvent event{}; + event.id = EventUpdateCaret; + event.param.caret_move = direction; + session.events.push_back(event); + } +} + +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) { + if (!multiline && (unit == u'\n' || unit == u'\r')) continue; + accepted.push_back(unit); + } + const std::size_t room = session.param.max_text_length - session.text.size(); + std::size_t count = std::min(accepted.size(), room); + if (count > 0 && count < accepted.size() && IsHighSurrogate(accepted[count - 1])) --count; + if (count == 0) return 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); + return CommitEdit(session, oldText, index, static_cast(count)); +} + +std::size_t UnitsBefore(const Session& session) { + const uint32_t caret = session.caret; + if (caret == 0) return 0; + return caret >= 2 && IsLowSurrogate(session.text[caret - 1]) && IsHighSurrogate(session.text[caret - 2]) ? 2 : 1; +} + +std::size_t UnitsAfter(const Session& session) { + const uint32_t caret = session.caret; + if (caret >= session.text.size()) return 0; + return caret + 1 < session.text.size() && IsHighSurrogate(session.text[caret]) && IsLowSurrogate(session.text[caret + 1]) ? 2 : 1; +} + +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; + return Insert(session, DecodeUtf8(input.text, size)); + } + case ImeHostInputKind::Backspace: { + const std::size_t units = UnitsBefore(session); + if (units == 0) return std::nullopt; + const std::u16string oldText = session.text; + session.caret -= static_cast(units); + session.text.erase(session.caret, units); + return CommitEdit(session, oldText, session.caret, -static_cast(units)); + } + case ImeHostInputKind::Delete: { + const std::size_t units = UnitsAfter(session); + if (units == 0) return std::nullopt; + const std::u16string oldText = session.text; + session.text.erase(session.caret, units); + 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 std::nullopt; + } + case ImeHostInputKind::Right: { + const std::size_t units = UnitsAfter(session); + session.caret += static_cast(units); + QueueCaretSteps(session, CaretMoveRight, units); + return std::nullopt; + } + case ImeHostInputKind::Home: { + const std::size_t units = session.caret; + session.caret = 0; + QueueCaretSteps(session, CaretMoveLeft, units); + 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 std::nullopt; + } + case ImeHostInputKind::Enter: + if ((session.param.option & OptionMultiline) != 0) { + return Insert(session, u"\n"); + } + QueueText(session, EventPressEnter, session.caret, 0); + return std::nullopt; + case ImeHostInputKind::Escape: + QueueText(session, EventPressClose, session.caret, 0); + return std::nullopt; + } + throw std::invalid_argument("IME host input: unknown input kind"); +} + +int ValidateOpen(const Param* param, const ExtendedParam* extended) { + if (!param) return ErrorInvalidAddress; + if (param->user_id == UserIdInvalid) return ErrorInvalidUserId; + if (param->type > TypeNumber) return ErrorInvalidType; + if (param->enter_label > EnterLabelLast) return ErrorInvalidEnterLabel; + if (param->input_method != 0) return ErrorInvalidInputMethod; + if ((param->option & ~ValidOptions) != 0) return ErrorInvalidOption; + if (param->max_text_length == 0 || param->max_text_length > MaxTextLength) return ErrorInvalidMaxTextLength; + if (!param->input_text_buffer) return ErrorInvalidInputTextBuffer; + const float maxX = (param->option & OptionUseOver2K) != 0 ? 3840.0f : 1920.0f; + const float maxY = (param->option & OptionUseOver2K) != 0 ? 2160.0f : 1080.0f; + if (!(param->posx >= 0.0f && param->posx < maxX)) return ErrorInvalidPosX; + if (!(param->posy >= 0.0f && param->posy < maxY)) return ErrorInvalidPosY; + if (param->horizontal_alignment > AlignmentLast) return ErrorInvalidHorizontalAlignment; + if (param->vertical_alignment > AlignmentLast) return ErrorInvalidVerticalAlignment; + if (extended) { + if (extended->priority > PanelPriorityLast) return ErrorInvalidExtended; + for (const std::int8_t byte : extended->reserved) { + if (byte != 0) return ErrorInvalidExtended; + } + } + if (!param->work || (reinterpret_cast(param->work) & 3u) != 0) return ErrorInvalidWork; + if (!param->handler) return ErrorInvalidHandler; + for (const std::int8_t byte : param->reserved) { + if (byte != 0) return ErrorInvalidReserved; + } + const std::size_t length = TextLength(param->input_text_buffer, param->max_text_length); + if (!ValidText(param->input_text_buffer, length, (param->option & OptionMultiline) != 0)) return ErrorInvalidText; + return 0; +} } extern "C" { +void ImePublishInput_nid_postfix(const ImeHostInput& input) { + { + std::lock_guard lock(g_imeMutex); + if (g_session) { + 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() || 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; + 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); + ImeEvent open{}; + open.id = EventOpen; + open.param.rect.x = param->posx; + open.param.rect.y = param->posy; + PanelSize(*param, &open.param.rect.width, &open.param.rect.height); + session.events.push_back(open); + g_session = std::move(session); + return 0; +} + int APS5_VABI sceImeClose_nid_postfix(void) { - return ErrorNotOpened; + std::lock_guard lock(g_imeMutex); + if (!g_session) return ErrorNotOpened; + g_session.reset(); + return 0; } int APS5_VABI sceImeGetPanelPositionAndForm(PositionAndForm* form) { - (void)form; - return ErrorNotOpened; + std::lock_guard lock(g_imeMutex); + if (!g_session) return ErrorNotOpened; + if (!form) return ErrorInvalidAddress; + const Param& param = g_session->param; + *form = {}; + form->type = param.type; + form->posx = param.posx; + form->posy = param.posy; + form->horizontal_alignment = param.horizontal_alignment; + form->vertical_alignment = param.vertical_alignment; + PanelSize(param, &form->width, &form->height); + return 0; } int APS5_VABI sceImeGetPanelSize(const Param* param, uint32_t* width, uint32_t* height) { if (!param || !width || !height) return ErrorInvalidAddress; if (param->type > TypeNumber) return ErrorInvalidType; if ((param->option & ~ValidOptions) != 0) return ErrorInvalidOption; - const uint32_t scale = (param->option & OptionUseOver2K) != 0 ? 2 : 1; - *width = (param->type == TypeNumber ? 370u : 793u) * scale; - *height = (param->type == TypeNumber ? 402u : 408u) * scale; + PanelSize(*param, width, height); return 0; } 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; } @@ -75,7 +458,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; } @@ -86,11 +469,14 @@ int APS5_VABI sceImeKeyboardSetMode(int32_t user_id, uint32_t mode) { return (mode & ~ValidKeyboardModes) == 0 ? 0 : ErrorInvalidMode; } -int APS5_VABI sceImeOpen_nid_postfix(const Param* param, const ExtendedParam* extended) { - (void)param; - (void)extended; - NotImplemented_nid_no_patch(__func__); - return 0; +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) { @@ -100,25 +486,322 @@ void APS5_VABI sceImeParamInit(Param* param) { } int APS5_VABI sceImeSetCaret(const Caret* caret) { - (void)caret; - return ErrorNotOpened; + std::lock_guard lock(g_imeMutex); + if (!g_session) return ErrorNotOpened; + if (!caret) return ErrorInvalidAddress; + if (caret->index > g_session->text.size()) return ErrorInvalidParam; + g_session->caret = caret->index; + ++g_session->revision; + return 0; +} + +int APS5_VABI sceImeSetConsecutiveClauseConversionEnabled(bool enabled) { + (void)enabled; + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceImeSetText(const char16_t* text, uint32_t length) { - (void)text; - (void)length; - return ErrorNotOpened; + std::lock_guard lock(g_imeMutex); + if (!g_session) return ErrorNotOpened; + if (!text) return ErrorInvalidAddress; + Session& session = *g_session; + const std::size_t count = TextLength(text, std::min(length, session.param.max_text_length)); + if (!ValidText(text, count, (session.param.option & OptionMultiline) != 0)) return ErrorInvalidText; + session.text.assign(text, count); + ++session.revision; + if (session.caret > session.text.size()) session.caret = static_cast(session.text.size()); + SyncBuffers(session); + return 0; } int APS5_VABI sceImeSetTextGeometry(TextAreaMode mode, const TextGeometry* geometry) { - (void)mode; - (void)geometry; - return ErrorNotOpened; + (void)mode; + std::lock_guard lock(g_imeMutex); + if (!g_session) return ErrorNotOpened; + if (!geometry) return ErrorInvalidAddress; + return 0; } int APS5_VABI sceImeUpdate(EventHandler handler) { - if (!handler) APS5_INVALID_ARG_EX; - return 0; + if (!handler) APS5_INVALID_ARG_EX; + for (;;) { + ImeEvent event{}; + void* arg = nullptr; + 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) 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(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeKeyboardUpdate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshUpdate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshUpdateContext(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshClearPreedit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshNotifySetText(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshSetCaretGeometry(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshConfirmPreedit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshSetCaretIndexInPreedit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshSetPreeditGeometry(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshSetSelectGeometry(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshGetPanelPositionAndForm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeSetCandidateIndex(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeConfirmCandidate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDisableController(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeSetVoiceDictationEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeSetEditEventWithRemotePlayEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeForTestFunction(void) { + return ErrorInternal; +} + +int APS5_VABI sceImeVshInformConfirmdString2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshSetSelectionText(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeCheckUpdateTextInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeCheckRemoteEventParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeKeyboardOpenInternal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshSetParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshUpdateContext2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshSendTextInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshInformConfirmdString(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshNotifyCurrentTextInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshNotifyCurrentTextInfoForReconvertText(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshNotifyDeleteText(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshNotifyUpdateCaretText(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshCheckAutoReConvertingPreeditText(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshSetPanelPosition(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshNotifyPreeditMetaInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeConfigSet(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeConfigGet(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDicGetWords(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDicAddWord(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDicDeleteWord(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDicReplaceWord(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDicDeleteLearnDics(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDicDeleteUserDics(void) { + 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 new file mode 100644 index 0000000000..3d27316f20 --- /dev/null +++ b/core/libs/prx/libSceIme/include/ImeInput.hpp @@ -0,0 +1,31 @@ +#ifndef CORE_LIBS_PRX_LIBSCEIME_INCLUDE_IMEINPUT_HPP +#define CORE_LIBS_PRX_LIBSCEIME_INCLUDE_IMEINPUT_HPP + +#include +#include + +enum class ImeHostInputKind : std::uint32_t { + Text, + Backspace, + Delete, + Left, + Right, + Home, + End, + Enter, + Escape, +}; + +struct ImeHostInput { + ImeHostInputKind kind = ImeHostInputKind::Text; + char text[32] = {}; +}; + +extern "C" void ImePublishInput_nid_postfix(const ImeHostInput& input); +extern "C" bool ImeCapturesKeyboard_nid_postfix(); + +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..c9b181b025 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,107 @@ 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 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 +138,75 @@ 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) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDialogInitInternal(Param* param, ExtendedParam* extended) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDialogSetPanelPosition(int32_t posx, int32_t posy) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + + +int APS5_VABI sceImeDialogInitInternalWithSubInfo() { + 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..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceJemspace/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/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 new file mode 100644 index 0000000000..8539bbd0c2 --- /dev/null +++ b/core/libs/prx/libSceJobManager/CMakeLists.txt @@ -0,0 +1,19 @@ +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 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}) + set_tests_properties(guest_job_manager_${testCase} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_job_manager_${testCase} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() +endif() diff --git a/core/libs/prx/libSceJobManager/Export.cpp b/core/libs/prx/libSceJobManager/Export.cpp new file mode 100644 index 0000000000..155a95220d --- /dev/null +++ b/core/libs/prx/libSceJobManager/Export.cpp @@ -0,0 +1,790 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { +int APS5_VABI scePthreadAttrInit(PthreadAttr* attr); +int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr); +int APS5_VABI scePthreadAttrSetinheritsched(PthreadAttr* attr, int inheritSched); +int APS5_VABI scePthreadAttrSetschedparam(PthreadAttr* attr, const KernelSchedParam* param); +int APS5_VABI scePthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stackSize); +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 { + +constexpr int ManagerInvalidArgument = static_cast(0x81581000u); +constexpr int ManagerNullMemory = static_cast(0x81581001u); +constexpr int ManagerMemoryTooSmall = static_cast(0x81581002u); +constexpr int ManagerNotInitialized = static_cast(0x81581010u); +constexpr int ManagerAlreadyInitialized = static_cast(0x81581020u); +constexpr int ManagerMisalignedMemory = static_cast(0x81581030u); +constexpr int SequenceOutOfRange = static_cast(0x81580000u); +constexpr int SequenceNullArgument = static_cast(0x81580001u); +constexpr int SequenceNoNodes = static_cast(0x81580002u); +constexpr int SequenceLimitExceeded = static_cast(0x81580003u); +constexpr int SequenceNoneFree = static_cast(0x81580006u); +constexpr int SequenceInvalid = static_cast(0x81580007u); + +constexpr std::uint32_t MaxWorkers = 13; +constexpr std::uint32_t MaxSequences = 32; +constexpr std::uint32_t MaxInvocations = 0x7fff; +constexpr std::uint32_t MaxDispatches = 0x3ff; +constexpr std::uint32_t MaxSyncPoints = 0xff; +constexpr std::uint32_t MaxWaits = 0x3f; +constexpr std::uint64_t StateOffset = 0x10; +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; + 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; +}; + +constexpr ConfigParams DefaultConfig{0, 0x00021fff, 0x00010000, 0x2bc, nullptr, 0, 0x1fff, 0x100, 0}; + +struct BlockHeader { + std::uint16_t magic; + std::uint16_t stateOffset; + std::uint32_t version; + std::uint32_t size; + std::uint32_t reserved; +}; + +using JobEntry = void (APS5_VABI*)(void* argument, std::uint32_t index); + +std::uint64_t AlignUp(std::uint64_t value, std::uint64_t alignment) { + return (value + alignment - 1) & ~(alignment - 1); +} + +struct Manager; +class Sequence; + +enum class ItemType { Dispatch, SyncPoint, Wait, Compute, ComputeIndirect }; + +struct Item { + ItemType type; + Sequence* sequence; + JobEntry entry; + void* argument; + std::uint32_t count; + 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 { + std::uint32_t invocations = 0; + std::uint32_t dispatches = 0; + std::uint32_t syncPoints = 0; + std::uint32_t waits = 0; +}; + +class ISequence { +public: + APS5_VABI virtual int Dispatch(JobEntry entry, void* argument, std::uint32_t count, const char* name) = 0; + APS5_VABI virtual int SyncPoint() = 0; + APS5_VABI virtual int Wait() = 0; + APS5_VABI virtual void CompleteDestructor() {} + APS5_VABI virtual void DeletingDestructor() { throw std::runtime_error("libSceJobManager: a sequence was deleted by its caller"); } + APS5_VABI virtual int DispatchCompute(const void* shader, void* userData, std::int32_t x, std::int32_t y, std::int32_t z, const char* name) = 0; + APS5_VABI virtual int DispatchComputeIndirect(const void* shader, void* userData, const void* indirectArguments, const char* name) = 0; +}; + +class ISequenceFactory { +public: + APS5_VABI virtual int Create(ISequence** out, std::uint32_t invocations, std::uint32_t dispatches, std::uint32_t syncPoints, std::uint32_t waits, const char* name) = 0; + APS5_VABI virtual int Destroy(ISequence* sequence) = 0; + APS5_VABI virtual void CompleteDestructor() {} + APS5_VABI virtual void DeletingDestructor() { throw std::runtime_error("libSceJobManager: a sequence factory was deleted by its caller"); } + APS5_VABI virtual int CreateCompute(ISequence** out, std::uint32_t invocations, std::uint32_t dispatches, std::uint32_t syncPoints, std::uint32_t waits, const char* name) = 0; + APS5_VABI virtual int DestroyCompute(ISequence* sequence) = 0; +}; + +class Sequence final : public ISequence { +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* 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; + Counters completed; + std::uint32_t priority = 0; + int error = 0; + std::uint32_t outstanding = 0; + std::size_t cursor = 0; + std::deque items; + 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 { +public: + APS5_VABI int Create(ISequence** out, std::uint32_t invocations, std::uint32_t dispatches, std::uint32_t syncPoints, std::uint32_t waits, const char* name) override; + APS5_VABI int Destroy(ISequence* sequence) override; + APS5_VABI int CreateCompute(ISequence** out, std::uint32_t invocations, std::uint32_t dispatches, std::uint32_t syncPoints, std::uint32_t waits, const char* name) override; + APS5_VABI int DestroyCompute(ISequence* sequence) override; + + Manager* manager = nullptr; + std::uint32_t priority = 0; +}; + +struct Manager { + std::mutex lock; + std::condition_variable work; + bool running = false; + std::uint32_t blockedWaiters = 0; + std::uint32_t freeNodes = 0; + std::uint32_t freeSequences = 0; + 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; +}; + +Manager*& Implementation(void* self) { + return *static_cast(self); +} + +bool TakeNode(Manager& manager) { + if (manager.freeNodes == 0) return false; + --manager.freeNodes; + return true; +} + +void ReturnNodes(Manager& manager, std::uint32_t count) { + manager.freeNodes += count; +} + +int Sequence::Latch(int code) { + if (error == 0) error = code; + return code; +} + +bool Sequence::Open(Manager* owner, const Counters& limit, std::uint32_t runQueue) { + manager = owner; + limits = limit; + submitted = {}; + completed = {}; + priority = runQueue; + error = 0; + outstanding = 0; + cursor = 0; + items.clear(); + commands.clear(); + batchStarted = false; + if (!TakeNode(*owner)) return false; + open = true; + return true; +} + +void Sequence::RequireOpen() const { + if (!open) throw std::runtime_error("libSceJobManager: a destroyed sequence was used"); +} + +void Sequence::Advance() { + while (cursor < items.size()) { + Item& item = items[cursor]; + if (item.type == ItemType::Dispatch) { + ++outstanding; + ++cursor; + manager->runQueues[priority].push_back(&item); + 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; + ++cursor; + continue; + } + bool* const waiter = item.passed; + 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; + submitted.syncPoints -= completed.syncPoints; + --submitted.waits; + completed = {}; + } + ReturnNodes(*manager, retired); + *waiter = true; + passed.notify_all(); + } +} + +void Sequence::DispatchCompleted(std::uint32_t invocations) { + completed.invocations += invocations; + ++completed.dispatches; + --outstanding; + if (outstanding == 0) Advance(); +} + +int Sequence::Dispatch(JobEntry entry, void* argument, std::uint32_t count, const char*) { + if (entry == nullptr) return SequenceNullArgument; + if (count == 0 || count > MaxInvocations) return SequenceOutOfRange; + const std::lock_guard guard(manager->lock); + RequireOpen(); + if (error != 0) return SequenceInvalid; + const Counters before = submitted; + submitted.invocations += count; + ++submitted.dispatches; + if (before.invocations + count > limits.invocations || before.dispatches >= limits.dispatches) return Latch(SequenceLimitExceeded); + if (!TakeNode(*manager)) return Latch(SequenceNoNodes); + items.push_back({ItemType::Dispatch, this, entry, argument, count, 0, 0, nullptr}); + Advance(); + 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(); + if (error != 0) return SequenceInvalid; + const std::uint32_t before = submitted.syncPoints++; + if (before >= limits.syncPoints) return Latch(SequenceLimitExceeded); + if (!TakeNode(*manager)) return Latch(SequenceNoNodes); + items.push_back({ItemType::SyncPoint, this, nullptr, nullptr, 0, 0, 0, nullptr}); + Advance(); + return 0; +} + +int Sequence::Wait() { + std::unique_lock guard(manager->lock); + return WaitLocked(guard); +} + +int Sequence::WaitAndClose(std::uint32_t index) { + std::unique_lock guard(manager->lock); + const int result = WaitLocked(guard); + 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; + (compute ? manager->freeComputeSequences : manager->freeSequences) |= std::uint32_t{1} << index; + return result; +} + +int Sequence::WaitLocked(std::unique_lock& guard) { + RequireOpen(); + int result = 0; + if (error != 0) { + result = SequenceInvalid; + } else if (submitted.waits++ >= limits.waits) { + result = Latch(SequenceLimitExceeded); + } + ++manager->blockedWaiters; + bool done = false; + items.push_back({ItemType::Wait, this, nullptr, nullptr, 0, 0, 0, &done}); + Advance(); + passed.wait(guard, [&done] { return done; }); + --manager->blockedWaiters; + return result; +} + +bool ValidLimits(std::uint32_t invocations, std::uint32_t dispatches, std::uint32_t syncPoints, std::uint32_t waits) { + if (invocations > MaxInvocations || dispatches > MaxDispatches || syncPoints > MaxSyncPoints || waits > MaxWaits) return false; + return invocations != 0 || dispatches != 0 || syncPoints != 0 || waits != 0; +} + +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(limits.invocations, limits.dispatches, limits.syncPoints, limits.waits)) return SequenceOutOfRange; + const std::lock_guard guard(manager->lock); + 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; +} + +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 < sequences.size(); ++index) { + if (&sequences[index] == sequence) break; + } + 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::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::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) { + Item* item = nullptr; + for (const std::size_t queue : {std::size_t{1}, std::size_t{0}}) { + auto& pending = manager.runQueues[queue]; + if (pending.empty()) continue; + item = pending.front(); + if (item->handedOut + 1 == item->count) pending.pop_front(); + break; + } + if (item == nullptr) return false; + const std::uint32_t index = item->handedOut++; + const JobEntry entry = item->entry; + void* const argument = item->argument; + guard.unlock(); + entry(argument, index); + guard.lock(); + if (++item->completed == item->count) item->sequence->DispatchCompleted(item->count); + return true; +} + +void* APS5_VABI WorkerEntry(void* argument) { + Manager& manager = *static_cast(argument); + std::unique_lock guard(manager.lock); + while (manager.running) { + if (RunOne(manager, guard)) continue; + manager.work.wait(guard, [&manager] { return !manager.running || !manager.runQueues[0].empty() || !manager.runQueues[1].empty(); }); + } + return nullptr; +} + +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(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; +} + +int ValidateWorkers(const ConfigParams& config, std::uint32_t workers) { + const std::uint32_t mode = config.threadFlags & 0x30000; + if (config.reserved != 0 || config.priority < 0x100 || config.priority > 0x2ff || config.stackSize < 0x4000) return ManagerInvalidArgument; + if ((config.threadFlags & 0xfffce000) != 0 || static_cast(std::popcount(config.threadFlags & 0x1fff)) < workers) return ManagerInvalidArgument; + if ((mode != 0x10000 && mode != 0x20000) || config.spinMicroseconds > 0x32) return ManagerInvalidArgument; + return 0; +} + +void StartWorkers(Manager& manager, const ConfigParams& config, std::uint32_t workers) { + std::uint32_t remaining = config.threadFlags & 0x1fff; + for (std::uint32_t index = 0; index < workers; ++index) { + KernelCpumask affinity = config.threadFlags & 0x1fff; + if ((config.threadFlags & 0x30000) == 0x10000) { + affinity = remaining & (~remaining + 1); + remaining &= remaining - 1; + } + const std::string name = config.threadName != nullptr ? std::string(config.threadName).substr(0, 31) : "SceJobManagerWorker" + std::to_string(index); + const KernelSchedParam schedule{static_cast(config.priority)}; + PthreadAttr attributes = nullptr; + if (scePthreadAttrInit(&attributes) != 0) throw std::runtime_error("libSceJobManager: scePthreadAttrInit failed"); + if (scePthreadAttrSetinheritsched(&attributes, 0) != 0 || scePthreadAttrSetschedparam(&attributes, &schedule) != 0 || + scePthreadAttrSetstacksize(&attributes, config.stackSize) != 0 || scePthreadAttrSetaffinity(&attributes, affinity) != 0) { + throw std::runtime_error("libSceJobManager: a worker thread attribute was rejected"); + } + Pthread thread = nullptr; + const int created = scePthreadCreate(&thread, &attributes, WorkerEntry, &manager, name.c_str()); + scePthreadAttrDestroy(&attributes); + if (created != 0) throw std::runtime_error("libSceJobManager: scePthreadCreate failed for a worker"); + manager.workers.push_back(thread); + } +} + +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; + const auto base = reinterpret_cast(memory); + if ((base & 0xff) != 0) return ManagerMisalignedMemory; + if (size == 0 || workers == 0 || workers > MaxWorkers) return ManagerInvalidArgument; + if (unified) { + 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; + } + if (size < StateOffset) return ManagerMemoryTooSmall; + if (size < StateEnd) throw std::runtime_error("sce::Job::JobManager::initialize: a memory block smaller than 0x2d8 bytes is not supported"); + std::memset(static_cast(memory) + StateOffset, 0, StateEnd - StateOffset); + 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; + 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, 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; + manager->freeSequences = sequences == MaxSequences ? ~std::uint32_t{0} : (std::uint32_t{1} << sequences) - 1; + manager->sequences = std::vector(sequences); + for (std::uint32_t priority = 0; priority < 2; ++priority) { + manager->factories[priority].manager = manager; + manager->factories[priority].priority = priority; + 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; +} + +ISequenceFactory* FactoryFor(void* self, std::uint32_t priority, bool unified) { + Manager* manager = Implementation(self); + if (manager == nullptr) return nullptr; + priority &= 0xffff; + if (priority > 1) throw std::runtime_error("sce::Job::JobManager: sequence factory priority " + std::to_string(priority) + " does not exist"); + return unified ? &manager->unifiedFactories[priority] : &manager->factories[priority]; +} + +} + +extern "C" { + +void APS5_VABI _ZN3sce3Job10JobManagerC1Ev(void* self) { + Implementation(self) = nullptr; +} + +void APS5_VABI _ZN3sce3Job10JobManagerC2Ev(void* self) { + Implementation(self) = nullptr; +} + +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* self) { + _ZN3sce3Job10JobManagerD1Ev(self); +} + +std::uint64_t APS5_VABI _ZN3sce3Job10JobManager27calculateRequiredMemorySizeEPKNS1_21MemorySizeQueryParamsE(const MemorySizeQueryParams* params) { + if (params == nullptr || params->version > 1 || params->workers == 0 || params->workers > MaxWorkers || params->sequences > MaxSequences) return 0; + const bool unified = params->version == 1; + const std::uint32_t computeSequences = unified ? params->computeSequences : 0; + if (unified) { + if (computeSequences > MaxSequences || params->sequences + computeSequences == 0 || params->sequences + computeSequences > MaxSequences) return 0; + if ((params->computeQueueMask >> 56) != 0 || static_cast(std::popcount(params->computeQueueMask)) < computeSequences) return 0; + } else if (params->sequences == 0) { + return 0; + } + 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; + 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}; + 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; +} + +int APS5_VABI _ZN3sce3Job10JobManager10initializeEPvmjjjmPKNS1_12ConfigParamsE(void* self, void* memory, std::uint64_t memorySize, std::uint32_t workers, std::uint32_t sequences, std::uint32_t computeSequences, std::uint64_t computeQueueMask, const ConfigParams* config) { + return Initialize(self, memory, memorySize, workers, sequences, computeSequences, computeQueueMask, config, true); +} + +int APS5_VABI _ZN3sce3Job10JobManager10initializeEPvmjjPKNS1_12ConfigParamsE(void* self, void* memory, std::uint64_t memorySize, std::uint32_t workers, std::uint32_t sequences, const ConfigParams* config) { + return Initialize(self, memory, memorySize, workers, sequences, 0, 0, config, false); +} + +int APS5_VABI _ZN3sce3Job10JobManager8shutdownEv(void* self) { + Manager* manager = Implementation(self); + if (manager == nullptr) return ManagerNotInitialized; + { + 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; +} + +ISequenceFactory* APS5_VABI _ZN3sce3Job10JobManager27getSequenceFactoryInterfaceENS1_8PriorityE(void* self, std::uint32_t priority) { + return FactoryFor(self, priority, false); +} + +ISequenceFactory* APS5_VABI _ZN3sce3Job10JobManager34getUnifiedSequenceFactoryInterfaceENS1_8PriorityE(void* self, std::uint32_t priority) { + return FactoryFor(self, priority, true); +} + +int APS5_VABI _ZN3sce3Job10JobManager24setPersistentThreadCountEj(void* self, std::uint32_t count) { + Manager* manager = Implementation(self); + if (manager == nullptr) return ManagerNotInitialized; + if (count > manager->workers.size()) return ManagerInvalidArgument; + const std::lock_guard guard(manager->lock); + manager->persistentThreads = count; + return 0; +} + +} diff --git a/core/libs/prx/libSceJpegDec/Export.cpp b/core/libs/prx/libSceJpegDec/Export.cpp index d38764bf71..19ec61c18b 100644 --- a/core/libs/prx/libSceJpegDec/Export.cpp +++ b/core/libs/prx/libSceJpegDec/Export.cpp @@ -5,14 +5,16 @@ #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 { @@ -24,10 +26,17 @@ constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_WORK_MEMORY = static_cast(0x80690010); constexpr std::int32_t SCE_JPEG_DEC_ERROR_DECODE_ERROR = static_cast(0x80690012); -constexpr std::uint32_t ATTRIBUTE_NONE = 0; +constexpr std::uint32_t ATTRIBUTE_CSC_ORDINARY = 1; +constexpr std::uint32_t ATTRIBUTE_CSC_FULL = 2; constexpr std::uint32_t MEMORY_SIZE = 0x20; constexpr std::uintptr_t HANDLE_ALIGNMENT = 8; +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; @@ -45,11 +54,18 @@ static_assert(sizeof(Context) + HANDLE_ALIGNMENT - 1 <= MEMORY_SIZE); 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->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; return 0; } +std::uint32_t requireDownScale(const char* function, std::uint16_t decodeMode, std::uint16_t downScale) { + if (decodeMode != DECODE_MODE_NORMAL) throw std::runtime_error(std::string(function) + ": decode mode " + std::to_string(decodeMode) + " is not implemented"); + if (downScale != DOWN_SCALE_NONE && downScale != DOWN_SCALE_HALF && downScale != DOWN_SCALE_QUARTER && downScale != DOWN_SCALE_EIGHTH) + throw std::runtime_error(std::string(function) + ": down scale " + std::to_string(downScale) + " is not implemented"); + return downScale; +} + Context* toContext(void* handle) { const auto address = reinterpret_cast(handle); if (address == 0 || address % HANDLE_ALIGNMENT != 0) return nullptr; @@ -57,12 +73,15 @@ 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->component_number = static_cast(header.channels); + info->suitable_csc_attribute = ATTRIBUTE_CSC_ORDINARY; + info->output_image_width = Decoder::Jpeg::ScaledDimension(header.width, downScale); + info->output_image_height = Decoder::Jpeg::ScaledDimension(header.height, downScale); } std::span toBytes(const void* data, std::uint32_t size) { @@ -92,23 +111,28 @@ int32_t APS5_VABI sceJpegDecDecode(void* handle, const JpegDecDecodeParam* param if (!context) return SCE_JPEG_DEC_ERROR_INVALID_HANDLE; (void)context; if (!param) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; - if (!param->jpeg_mem_addr || !param->image_mem_addr) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; + if (!param->jpeg_mem_addr || !param->image_mem_addr || (reinterpret_cast(param->image_mem_addr) & 3u) != 0) 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 (param->alpha_value > 0xFF) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; + const std::uint32_t downScale = requireDownScale(__func__, param->decode_mode, 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; + 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 ((outputHeight - 1) * pitch + rowSize > param->image_mem_size) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; - 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 +149,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 (image_info) fillImageInfo(*header, downScale, image_info); + if (outputWidth > MAX_PACKED_DIMENSION || outputHeight > MAX_PACKED_DIMENSION) return 0; + return static_cast(outputWidth << 16 | outputHeight); } int32_t APS5_VABI sceJpegDecDelete(void* handle) { @@ -145,9 +169,10 @@ int32_t APS5_VABI sceJpegDecParseHeader(const JpegDecParseParam* param, JpegDecI if (!param) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; if (!param->jpeg_mem_addr || !image_info) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; if (param->jpeg_mem_size == 0) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; + const std::uint32_t downScale = requireDownScale(__func__, param->decode_mode, 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, image_info); return 0; } @@ -157,4 +182,9 @@ int32_t APS5_VABI sceJpegDecQueryMemorySize(const JpegDecCreateParam* param) { return static_cast(MEMORY_SIZE); } +int APS5_VABI sceJpegDecDecodeWithInputControl() { + NotImplemented_nid_no_patch(__func__); + 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..f3e7cb682f 100644 --- a/core/libs/prx/libSceJson2/CMakeLists.txt +++ b/core/libs/prx/libSceJson2/CMakeLists.txt @@ -13,3 +13,18 @@ if(BUILD_TESTING) "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 70675ee055..6231181390 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -43,6 +44,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; @@ -52,6 +66,7 @@ struct Node { String string{}; Array array{}; Object object{}; + Handlers handlers{}; }; using NullAccessCallback = const Value& (APS5_VABI*)(ValueType, const Value*, void*); @@ -94,7 +109,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) { @@ -111,23 +128,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) { @@ -137,25 +178,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) { @@ -186,12 +235,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) { @@ -227,7 +299,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 += '['; @@ -259,7 +335,7 @@ void Serialize(std::string& out, const Node& n) { 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) {} bool Parse(Node& out) { skip(); @@ -273,6 +349,7 @@ private: const char* _text; const char* _end; + Handlers _handlers; std::size_t _depth = 0; void skip() { while (_text < _end && (*_text == ' ' || *_text == '\t' || *_text == '\n' || *_text == '\r')) ++_text; } @@ -400,6 +477,7 @@ private: for (;;) { Value item{}; Construct(item); + item.node->handlers = _handlers; out.array.items->push_back(item); skip(); if (!value(*item.node)) return false; @@ -427,6 +505,7 @@ private: Pair pair{}; Construct(pair.key, key); Construct(pair.value); + pair.value.node->handlers = _handlers; out.object.items->push_back(pair); if (!value(*pair.value.node)) return false; skip(); @@ -512,6 +591,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"); @@ -526,19 +607,76 @@ 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(); } +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 && *self->text == text; } +bool APS5_VABI _ZNK3sce4Json6StringeqERKS1_(const String* self, const String* other) { return other != nullptr && other->text != nullptr && *self->text == *other->text; } +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); +} +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 self->text->compare(*other->text); +} void APS5_VABI _ZN3sce4Json5ArrayC1Ev(Array* self) { Construct(*self); } +void APS5_VABI _ZN3sce4Json5ArrayC1ERKS1_(Array* self, const Array* other) { CopyArray(*self, *other); } 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; } 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) { + for (auto& item : *self->items) Destroy(item); + self->items->clear(); +} +const Value* APS5_VABI _ZNK3sce4Json5Array5frontEv(const Array* self) { + if (self->items->empty()) throw std::out_of_range("sce::Json::Array::front: empty array"); + return &self->items->front(); +} 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(); @@ -546,7 +684,13 @@ const Value* APS5_VABI _ZNK3sce4Json5Array4backEv(const Array* self) { 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 _ZN3sce4Json5Array8iteratorC1ERKS2_(ArrayIterator* self, const ArrayIterator* other) { self->it = other->it; } +ArrayIterator* APS5_VABI _ZN3sce4Json5Array8iterator7advanceEm(ArrayIterator* self, std::size_t count) { + std::advance(self->it, count); + return self; +} void APS5_VABI _ZN3sce4Json6ObjectC1Ev(Object* self) { Construct(*self); } void APS5_VABI _ZN3sce4Json6ObjectC1ERKS1_(Object* self, const Object* other) { CopyObject(*self, *other); } @@ -572,9 +716,29 @@ ObjectIterator* APS5_VABI _ZNK3sce4Json6Object3endEv(ObjectIterator* result, con 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 _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; }); + 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) { return self->it == other->it; } +void APS5_VABI _ZN3sce4Json6Object8iteratorC1ERKS2_(ObjectIterator* self, const ObjectIterator* other) { self->it = other->it; } +ObjectIterator* APS5_VABI _ZN3sce4Json6Object8iterator7advanceEm(ObjectIterator* self, std::size_t count) { + std::advance(self->it, 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(); } +std::size_t 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 *pair.key.text == *key->text; }); + if (it == self->items->end()) return 0; + Destroy(it->key); + Destroy(it->value); + self->items->erase(it); + return 1; +} 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; } @@ -583,24 +747,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); } @@ -608,6 +776,9 @@ 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) { + std::swap(self->node, other->node); +} 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; } @@ -688,7 +859,7 @@ const Value* APS5_VABI _ZNK3sce4Json5ValueixEPKc(const Value* self, const char* 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); + return &MissingElement(self); } const Value* APS5_VABI _ZNK3sce4Json5ValueixEm(const Value* self, std::size_t index) { const Node& n = NodeOf(*self); @@ -697,11 +868,10 @@ 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) { @@ -709,7 +879,7 @@ const Value* APS5_VABI _ZNK3sce4Json5Value8getValueERKNS0_6StringE(const Value* 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); + return &MissingElement(self); } Value* APS5_VABI _ZN3sce4Json5Value10referValueEm(Value* self, std::size_t index) { Node& n = NodeOf(*self); @@ -755,10 +925,23 @@ 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()); +} + int APS5_VABI _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(Value* out, const char* text, std::size_t size) { - if (text == nullptr) throw std::invalid_argument("sce::Json::Parser::parse: null text"); + if (text == nullptr || size == 0) return JsonErrorParse; Node parsed{}; - Parser parser(text, size); + parsed.handlers = NodeOf(*out).handlers; + Parser parser(text, size, parsed.handlers); bool parsedOk = false; try { parsedOk = parser.Parse(parsed); @@ -776,31 +959,53 @@ int APS5_VABI _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(Value* out, const char* } -int APS5_VABI _ZN3sce4Json17InitParameterRttiC1EPNS0_14AllocParamRttiEPvm(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +struct InitParameterRtti { + void* allocParam; + void* userData; + std::size_t fileBufferSize; +}; + +void APS5_VABI _ZN3sce4Json17InitParameterRttiC1EPNS0_14AllocParamRttiEPvm(InitParameterRtti* self, void* allocParam, void* userData, std::size_t fileBufferSize) { + self->allocParam = allocParam; + self->userData = userData; + self->fileBufferSize = fileBufferSize; } struct InitParameter2 { void* allocator; void* userData; std::size_t fileBufferSize; + std::int32_t specialFloatFormatType; + std::uint32_t reserved[3]; }; -static_assert(sizeof(InitParameter2) <= 40); +static_assert(sizeof(InitParameter2) == 0x28 && offsetof(InitParameter2, specialFloatFormatType) == 0x18); void APS5_VABI _ZN3sce4Json14InitParameter2C1Ev(InitParameter2* self) { *self = {}; } +void APS5_VABI _ZN3sce4Json14InitParameter2C2Ev(InitParameter2* self) { + _ZN3sce4Json14InitParameter2C1Ev(self); +} + void APS5_VABI _ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv(InitParameter2* self, void* allocator, void* userData) { self->allocator = allocator; self->userData = userData; } +void APS5_VABI _ZN3sce4Json18InitParameterRtti216setAllocatorRttiEPNS0_14AllocParamRttiEPv(InitParameter2* self, void* allocParam, void* userData) { + self->allocator = allocParam; + self->userData = userData; +} + void APS5_VABI _ZN3sce4Json14InitParameter217setFileBufferSizeEm(InitParameter2* self, std::size_t size) { self->fileBufferSize = size; } +void APS5_VABI _ZN3sce4Json14InitParameter225setSpecialFloatFormatTypeENS0_22SpecialFloatFormatTypeE(InitParameter2* self, std::int32_t type) { + self->specialFloatFormatType = type; +} + int APS5_VABI _ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E(void* self, const InitParameter2* parameter) { (void)self; if (parameter == nullptr) throw std::invalid_argument("sce::Json::Initializer::initialize: null parameter"); @@ -812,9 +1017,130 @@ 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 _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 _ZN3sce4Json5Value22setTypeMismatchHandlerEPFRKS1_NS0_9ValueTypeEPS2_S5_PvES6_(Value* self, TypeMismatchHandler handler, void* context) { + SetHandlers(NodeOf(*self), [&](Handlers& handlers) { + handlers.typeMismatch = handler; + handlers.typeMismatchContext = context; + }); + return 0; +} + +bool APS5_VABI _ZNK3sce4Json5ValuecvbEv(const Value* self) { + const Node& n = NodeOf(*self); + if (n.type == TypeNull) return false; + if (n.type == TypeBoolean) return n.boolean; + return true; +} + +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; +} + +void APS5_VABI _ZN3sce4Json6Object4PairD1Ev(Pair* self) { + Destroy(self->key); + Destroy(self->value); +} + +ObjectIterator* APS5_VABI _ZN3sce4Json6Object6insertERKNS1_4PairE(ObjectIterator* result, Object* self, const Pair* pair) { + auto it = std::find_if(self->items->begin(), self->items->end(), [&](const Pair& p) { return *p.key.text == *pair->key.text; }); + if (it != self->items->end()) { + AssignNode(NodeOf(it->value), NodeOf(pair->value)); + result->it = it; + return result; + } + Pair copy{}; + copy.reserved = 0; + Construct(copy.key, *pair->key.text); + try { + CopyInto(copy.value, pair->value); + result->it = self->items->insert(self->items->end(), copy); + return result; + } catch (...) { + Destroy(copy.value); + Destroy(copy.key); + throw; + } +} + +ArrayIterator* APS5_VABI _ZN3sce4Json5Array8iteratorppEi(ArrayIterator* result, ArrayIterator* self, int) { + result->it = self->it; + ++self->it; + return result; +} + +ArrayIterator* APS5_VABI _ZN3sce4Json5Array8iteratoraSERKS2_(ArrayIterator* self, const ArrayIterator* other) { + self->it = other->it; + return self; +} + +void APS5_VABI _ZN3sce4Json5Array8iteratorC1Ev(ArrayIterator* self) { + self->it = {}; +} + +ObjectIterator* APS5_VABI _ZN3sce4Json6Object8iteratorppEi(ObjectIterator* result, ObjectIterator* self, int) { + result->it = self->it; + ++self->it; + return result; +} + +ObjectIterator* APS5_VABI _ZN3sce4Json6Object8iteratoraSERKS2_(ObjectIterator* self, const ObjectIterator* other) { + self->it = other->it; + return self; +} + +void APS5_VABI _ZN3sce4Json6Object8iteratorC1Ev(ObjectIterator* self) { + self->it = {}; +} + +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/Export.cpp b/core/libs/prx/libSceJxr/Export.cpp new file mode 100644 index 0000000000..608aedc418 --- /dev/null +++ b/core/libs/prx/libSceJxr/Export.cpp @@ -0,0 +1,29 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN +int APS5_VABI PKFree_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +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/CMakeLists.txt b/core/libs/prx/libSceKeyboard/CMakeLists.txt index 0e3fe276ff..6f23ed265f 100644 --- a/core/libs/prx/libSceKeyboard/CMakeLists.txt +++ b/core/libs/prx/libSceKeyboard/CMakeLists.txt @@ -11,7 +11,7 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tests/Keyboard.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../libSceVideoOut/src/KeyboardInput.cpp ) - target_include_directories(keyboard_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/SDL2/include) + target_include_directories(keyboard_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) target_link_libraries(keyboard_tests PRIVATE libSceKeyboard libkernel libc) configure_windows_unwind(keyboard_tests) add_test(NAME keyboard_input COMMAND keyboard_tests) diff --git a/core/libs/prx/libSceKeyboard/Export.cpp b/core/libs/prx/libSceKeyboard/Export.cpp index 9333cf673d..d8d408237a 100644 --- a/core/libs/prx/libSceKeyboard/Export.cpp +++ b/core/libs/prx/libSceKeyboard/Export.cpp @@ -1,3 +1,4 @@ +#include "prx/libc/include/General.hpp" #include "prx/libSceKeyboard/include/KeyboardState.hpp" extern "C" { @@ -27,6 +28,106 @@ int APS5_VABI sceKeyboardGetKey2Char(int32_t handle, int32_t arrange, uint32_t l return Keyboard::GetKey2Char(handle, arrange, led, modifier_key, key_code, char_data); } +int APS5_VABI sceKeyboardConnectPort(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKeyboardDebugGetDeviceId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKeyboardDeviceOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKeyboardDisconnectAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKeyboardDisconnectDevice(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKeyboardDisconnectPort(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKeyboardGetConnection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKeyboardGetDeviceInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKeyboardGetHandle(void) { + 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(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKeyboardSetProcessPrivilege(void) { + 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) { Keyboard::Publish(event); } diff --git a/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp b/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp index 2bb6273e75..0a16e2d75e 100644 --- a/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp +++ b/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp @@ -29,7 +29,7 @@ KeyboardData snapshot() { KeyboardData data{}; data.timestamp = sceKernelGetProcessTime(); data.connected = connected; - data.length = keyCount; + data.length = std::max(keyCount, 1); data.led = led; data.modifier_key = modifiers; std::copy_n(keys.begin(), keyCount, data.key_code); diff --git a/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp b/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp index 802e1da750..97e1dd5129 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); @@ -13,6 +14,8 @@ 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 sceKeyboardConnectPort(void); +int APS5_VABI sceKeyboardDisconnectAll(void); } static void Require(bool value) { if (!value) std::abort(); } @@ -54,7 +57,7 @@ int main() { Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 17) == KEYBOARD_ERROR_INVALID_ARG); Require(sceKeyboardReadState(KEYBOARD_HANDLE, nullptr) == KEYBOARD_ERROR_INVALID_ARG); Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1); - Require(data[0].connected && data[0].length == 0 && data[0].modifier_key == 0); + Require(data[0].connected && data[0].length == 1 && data[0].key_code[0] == 0 && data[0].modifier_key == 0); Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 0); KeyboardInput input; @@ -66,7 +69,7 @@ int main() { input.HandleEvent(KeyEvent(SDL_KEYDOWN, SDL_SCANCODE_RETURN, KMOD_NUM | KMOD_LSHIFT), 7); input.HandleEvent(KeyEvent(SDL_KEYUP, SDL_SCANCODE_A, KMOD_NUM | KMOD_LSHIFT), 7); Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 16) == 4); - Require(data[0].length == 0 && data[0].modifier_key == 2 && data[0].led == KEYBOARD_LED_NUM_LOCK); + Require(data[0].length == 1 && data[0].key_code[0] == 0 && data[0].modifier_key == 2 && data[0].led == KEYBOARD_LED_NUM_LOCK); Require(data[1].length == 1 && data[1].key_code[0] == 0x04); Require(data[2].length == 2 && data[2].key_code[0] == 0x04 && data[2].key_code[1] == 0x28); Require(data[3].length == 1 && data[3].key_code[0] == 0x28 && data[3].modifier_key == 2); @@ -84,7 +87,7 @@ int main() { window.window.event = SDL_WINDOWEVENT_FOCUS_LOST; input.HandleEvent(window, 7); Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1); - Require(data[0].connected && data[0].length == 0 && data[0].modifier_key == 0); + Require(data[0].connected && data[0].length == 1 && data[0].key_code[0] == 0 && data[0].modifier_key == 0); input.HandleEvent(KeyEvent(SDL_KEYDOWN, SDL_SCANCODE_B), 7); Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 0); window.window.event = SDL_WINDOWEVENT_FOCUS_GAINED; @@ -92,7 +95,7 @@ int main() { Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 0); window.window.event = SDL_WINDOWEVENT_CLOSE; input.HandleEvent(window, 7); - Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1 && !data[0].connected); + Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1 && !data[0].connected && data[0].length == 1 && data[0].key_code[0] == 0); window.window.event = SDL_WINDOWEVENT_FOCUS_GAINED; input.HandleEvent(window, 7); Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1 && data[0].connected); @@ -142,6 +145,22 @@ int main() { Require(sceKeyboardReadState(KEYBOARD_HANDLE, data) == KEYBOARD_ERROR_INVALID_HANDLE); Require(sceKeyboardClose(KEYBOARD_HANDLE) == KEYBOARD_ERROR_INVALID_HANDLE); Require(sceKeyboardOpen(1, 0, 0, nullptr) == KEYBOARD_HANDLE); - Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1 && data[0].length == 0); + Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1 && data[0].length == 1 && data[0].key_code[0] == 0); Require(sceKeyboardClose(KEYBOARD_HANDLE) == KEYBOARD_OK); + + bool threwConnect = false; + try { + sceKeyboardConnectPort(); + } catch (const std::runtime_error&) { + threwConnect = true; + } + Require(threwConnect); + + bool threwDisconnect = false; + try { + sceKeyboardDisconnectAll(); + } catch (const std::runtime_error&) { + threwDisconnect = true; + } + Require(threwDisconnect); } diff --git a/core/libs/prx/libSceLibcInternal/CMakeLists.txt b/core/libs/prx/libSceLibcInternal/CMakeLists.txt index 2c0060756b..1297f6abcf 100644 --- a/core/libs/prx/libSceLibcInternal/CMakeLists.txt +++ b/core/libs/prx/libSceLibcInternal/CMakeLists.txt @@ -1,9 +1,83 @@ -add_library(libSceLibcInternal SHARED EXCLUDE_FROM_ALL Export.cpp) +add_library(libSceLibcInternal SHARED EXCLUDE_FROM_ALL Export.cpp FileTime.cpp Getopt.cpp SignalList.cpp Spawn.cpp UserContext.cpp) target_include_directories(libSceLibcInternal PRIVATE ${LIBS_INCLUDE_DIR}) +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) + target_include_directories(guest_statvfs_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_statvfs_tests PRIVATE libSceLibcInternal libc) + configure_windows_unwind(guest_statvfs_tests) + add_test(NAME guest_statvfs COMMAND guest_statvfs_tests) + set_tests_properties(guest_statvfs PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_statvfs PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_fstatvfs_conversion_tests ${LIBS_INCLUDE_DIR}/tests/GuestFstatvfsConversion.cpp FilesystemStatistics.cpp) + target_include_directories(guest_fstatvfs_conversion_tests PRIVATE ${LIBS_INCLUDE_DIR}) + configure_windows_unwind(guest_fstatvfs_conversion_tests) + add_test(NAME guest_fstatvfs_conversion COMMAND guest_fstatvfs_conversion_tests) + set_tests_properties(guest_fstatvfs_conversion PROPERTIES TIMEOUT 20) + add_test_executable(guest_fstatvfs_tests ${LIBS_INCLUDE_DIR}/tests/GuestFstatvfs.cpp) + target_include_directories(guest_fstatvfs_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_fstatvfs_tests PRIVATE libSceLibcInternal libkernel libc) + configure_windows_unwind(guest_fstatvfs_tests) + add_test(NAME guest_fstatvfs COMMAND guest_fstatvfs_tests) + set_tests_properties(guest_fstatvfs PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_fstatvfs PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_fdopendir_tests ${LIBS_INCLUDE_DIR}/tests/GuestFdopendir.cpp) + target_include_directories(guest_fdopendir_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_fdopendir_tests PRIVATE libSceLibcInternal libkernel libc) + configure_windows_unwind(guest_fdopendir_tests) + add_test(NAME guest_fdopendir COMMAND guest_fdopendir_tests) + if(WIN32) + set_tests_properties(guest_fdopendir PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_utime_forwarding_tests ${LIBS_INCLUDE_DIR}/tests/GuestUtimeForwarding.cpp FileTime.cpp) + target_include_directories(guest_utime_forwarding_tests PRIVATE ${LIBS_INCLUDE_DIR}) + configure_windows_unwind(guest_utime_forwarding_tests) + add_test(NAME guest_utime_forwarding COMMAND guest_utime_forwarding_tests) + set_tests_properties(guest_utime_forwarding PROPERTIES TIMEOUT 20) + add_test_executable(guest_utime_tests ${LIBS_INCLUDE_DIR}/tests/GuestUtime.cpp) + target_include_directories(guest_utime_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_utime_tests PRIVATE libSceLibcInternal libc) + configure_windows_unwind(guest_utime_tests) + add_test(NAME guest_utime COMMAND guest_utime_tests) + set_tests_properties(guest_utime PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_utime PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_posix_spawn_tests ${LIBS_INCLUDE_DIR}/tests/GuestPosixSpawn.cpp) + target_include_directories(guest_posix_spawn_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_posix_spawn_tests PRIVATE libSceLibcInternal libc) + configure_windows_unwind(guest_posix_spawn_tests) + add_test(NAME guest_posix_spawn COMMAND guest_posix_spawn_tests) + set_tests_properties(guest_posix_spawn PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_posix_spawn PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_program_name_tests ${LIBS_INCLUDE_DIR}/tests/GuestProgramName.cpp) + target_include_directories(guest_program_name_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_program_name_tests PRIVATE libSceLibcInternal libkernel libc) + configure_windows_unwind(guest_program_name_tests) + add_test(NAME guest_program_name COMMAND guest_program_name_tests) + set_tests_properties(guest_program_name PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_program_name PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_thread_atexit_tests ${LIBS_INCLUDE_DIR}/tests/GuestThreadAtexit.cpp) target_include_directories(guest_thread_atexit_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_thread_atexit_tests PRIVATE libSceLibcInternal libkernel libc) @@ -14,11 +88,64 @@ if(BUILD_TESTING) set_tests_properties(guest_thread_atexit PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - if(NOT WIN32) - 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}) - add_test(NAME guest_force_tls_destructor COMMAND guest_force_tls_destructor_tests) - set_tests_properties(guest_force_tls_destructor PROPERTIES TIMEOUT 20) + 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) + configure_windows_unwind(guest_getopt_tests) + add_test(NAME guest_getopt COMMAND guest_getopt_tests) + set_tests_properties(guest_getopt PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_getopt PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_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}) + configure_windows_unwind(guest_force_tls_destructor_tests) + add_test(NAME guest_force_tls_destructor COMMAND guest_force_tls_destructor_tests) + set_tests_properties(guest_force_tls_destructor PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_force_tls_destructor PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_fminf_tests ${LIBS_INCLUDE_DIR}/tests/GuestFminf.cpp) + target_include_directories(guest_fminf_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_fminf_tests PRIVATE libSceLibcInternal libc) + configure_windows_unwind(guest_fminf_tests) + add_test(NAME guest_fminf COMMAND guest_fminf_tests) + set_tests_properties(guest_fminf PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_fminf PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_signal_list_tests ${LIBS_INCLUDE_DIR}/tests/GuestSignalList.cpp) + target_include_directories(guest_signal_list_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_signal_list_tests PRIVATE libSceLibcInternal) + configure_windows_unwind(guest_signal_list_tests) + add_test(NAME guest_signal_list COMMAND guest_signal_list_tests) + set_tests_properties(guest_signal_list PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_signal_list PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceLibcInternal/Directory.cpp b/core/libs/prx/libSceLibcInternal/Directory.cpp new file mode 100644 index 0000000000..48a1614e85 --- /dev/null +++ b/core/libs/prx/libSceLibcInternal/Directory.cpp @@ -0,0 +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, 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 f9425b744f..7ca113639e 100644 --- a/core/libs/prx/libSceLibcInternal/Export.cpp +++ b/core/libs/prx/libSceLibcInternal/Export.cpp @@ -15,6 +15,7 @@ extern "C" void APS5_VABI sceKernelSetThreadDtors(thread_dtors_func_t dtors); extern "C" int APS5_VABI sceKernelGetModuleInfoFromAddr(std::uint64_t address, int flags, ModuleInfoEx* info); +extern "C" const char* __progname_nid_postfix; namespace { @@ -104,6 +105,10 @@ extern "C" { int Need_sceLibcInternal_nid_postfix = 1; +const char* APS5_VABI getprogname_nid_postfix() { + return __progname_nid_postfix; +} + void APS5_VABI __cxa_finalize_nid_postfix(void* dsoHandle) { CxaFinalize_nid_no_patch(dsoHandle); } @@ -130,4 +135,9 @@ int APS5_VABI _sceLibcInternalForceTlsDestructor_nid_postfix(KernelModule handle return 0; } +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); +} } diff --git a/core/libs/prx/libSceLibcInternal/FileTime.cpp b/core/libs/prx/libSceLibcInternal/FileTime.cpp new file mode 100644 index 0000000000..a3bbfe8edc --- /dev/null +++ b/core/libs/prx/libSceLibcInternal/FileTime.cpp @@ -0,0 +1,21 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include + +struct LibcUtimbuf { + std::int64_t actime; + std::int64_t modtime; +}; +static_assert(sizeof(LibcUtimbuf) == 16); + +extern "C" int APS5_VABI utimes_nid_postfix(const char*, const KernelTimeval*); +extern "C" int* APS5_VABI __error_nid_postfix(); + +extern "C" int APS5_VABI utime_nid_postfix(const char* path, const LibcUtimbuf* times) { + int* const error = __error_nid_postfix(); + const int saved = *error; + const KernelTimeval values[2] = {{times ? times->actime : 0, 0}, {times ? times->modtime : 0, 0}}; + const int result = utimes_nid_postfix(path, times ? values : nullptr); + if (result == 0) *error = saved; + return result; +} diff --git a/core/libs/prx/libSceLibcInternal/FilesystemStatistics.cpp b/core/libs/prx/libSceLibcInternal/FilesystemStatistics.cpp new file mode 100644 index 0000000000..8cb50f1eca --- /dev/null +++ b/core/libs/prx/libSceLibcInternal/FilesystemStatistics.cpp @@ -0,0 +1,22 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceLibcInternal/include/Statvfs.hpp" +#include + +extern "C" int APS5_VABI _fstatfs_nid_postfix(int, KernelStatfs*); +extern "C" int* APS5_VABI __error_nid_postfix(); + +extern "C" int APS5_VABI fstatvfs_nid_postfix(int descriptor, LibcStatvfs* output) { + int* const error = __error_nid_postfix(); + const int saved = *error; + KernelStatfs volume{}; + const int status = _fstatfs_nid_postfix(descriptor, &volume); + if (status != 0) return status; + if (output == nullptr) { + *error = 14; + return -1; + } + 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 new file mode 100644 index 0000000000..80645072b4 --- /dev/null +++ b/core/libs/prx/libSceLibcInternal/Getopt.cpp @@ -0,0 +1,378 @@ +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +struct GuestOption { + const char* name; + int hasArgument; + int* flag; + int value; +}; +static_assert(sizeof(GuestOption) == 32); +static_assert(offsetof(GuestOption, hasArgument) == 8); +static_assert(offsetof(GuestOption, flag) == 16); +static_assert(offsetof(GuestOption, value) == 24); + +extern "C" { +extern const char* __progname_nid_postfix; +extern FileStream* __stderrp_nid_postfix; +char* APS5_VABI getenv_nid_postfix(const char* name); + +int opterr_nid_postfix = 1; +int optind_nid_postfix = 1; +int optopt_nid_postfix = 0; +int optreset_nid_postfix = 0; +char* optarg_nid_postfix = nullptr; +} + +namespace { + +constexpr int NoArgument = 0; +constexpr int RequiredArgument = 1; +constexpr int OptionalArgument = 2; + +constexpr int FlagPermute = 1; +constexpr int FlagAllArguments = 2; +constexpr int FlagLongOnly = 4; + +constexpr int BadCharacter = '?'; +constexpr int InOrder = 1; + +char g_emptyMessage[1] {}; +char* g_shortPlace = g_emptyMessage; +char* g_longPlace = g_emptyMessage; +int g_nonOptionStart = -1; +int g_nonOptionEnd = -1; +int g_posixlyCorrect = -1; + +void Warn(std::string_view message) { + const char* const name = __progname_nid_postfix ? __progname_nid_postfix : "(null)"; + std::string line(name); + line.append(": ").append(message).append("\n"); + FileStream* const stream = __stderrp_nid_postfix; + std::fwrite(line.data(), 1, line.size(), GetNativeStream(stream)); + stream->SyncStatus(); +} + +std::string Character(int character) { + return std::string(1, static_cast(character)); +} + +char* Argument(char* const* arguments, int index, const char* function) { + if (!arguments) throw std::invalid_argument(std::string(function) + ": null argument vector"); + if (index < 0) throw std::invalid_argument(std::string(function) + ": negative optind " + std::to_string(index)); + char* const argument = arguments[index]; + if (!argument) throw std::invalid_argument(std::string(function) + ": null argument " + std::to_string(index)); + return argument; +} + +int OptionCharacter(const char* place) { + return static_cast(*place); +} + +bool PrintError(const char* options) { + return opterr_nid_postfix && *options != ':'; +} + +int BadArgument(const char* options) { + return *options == ':' ? ':' : '?'; +} + +int Gcd(int a, int b) { + int c = a % b; + while (c != 0) { + a = b; + b = c; + c = a % b; + } + return b; +} + +void PermuteArguments(int nonOptionStart, int nonOptionEnd, int optionEnd, char* const* arguments) { + char** const vector = const_cast(arguments); + const int nonOptions = nonOptionEnd - nonOptionStart; + const int options = optionEnd - nonOptionEnd; + if (nonOptions <= 0 || options <= 0) throw std::logic_error("getopt_long: optind moved inside a permutation"); + const int cycles = Gcd(nonOptions, options); + const int cycleLength = (optionEnd - nonOptionStart) / cycles; + for (int i = 0; i < cycles; ++i) { + const int cycleStart = nonOptionEnd + i; + int position = cycleStart; + for (int j = 0; j < cycleLength; ++j) { + if (position >= nonOptionEnd) position -= nonOptions; + else position += options; + char* const swap = vector[position]; + vector[position] = vector[cycleStart]; + vector[cycleStart] = swap; + } + } +} + +int ParseLongOptions(char* const* arguments, const char* options, const GuestOption* longOptions, int* index, const char* dash, bool shortToo, int flags) { + char* const current = g_longPlace; + int match = -1; + bool exactMatch = false; + bool secondPartialMatch = false; + + ++optind_nid_postfix; + + char* hasEqual = std::strchr(current, '='); + std::size_t length; + if (hasEqual) { + length = static_cast(hasEqual - current); + ++hasEqual; + } else { + length = std::strlen(current); + } + + for (int i = 0; longOptions[i].name; ++i) { + if (std::strncmp(current, longOptions[i].name, length) != 0) continue; + if (std::strlen(longOptions[i].name) == length) { + match = i; + exactMatch = true; + break; + } + if (shortToo && length == 1) continue; + if (match == -1) match = i; + else if ((flags & FlagLongOnly) || longOptions[i].hasArgument != longOptions[match].hasArgument || + longOptions[i].flag != longOptions[match].flag || + longOptions[i].value != longOptions[match].value) + secondPartialMatch = true; + } + const std::string_view name(current, length); + if (!exactMatch && secondPartialMatch) { + if (PrintError(options)) Warn(std::string("option `") + dash + std::string(name) + "' is ambiguous"); + optopt_nid_postfix = 0; + return BadCharacter; + } + if (match == -1) { + if (shortToo) { + --optind_nid_postfix; + return -1; + } + if (PrintError(options)) Warn(std::string("unrecognized option `") + dash + current + "'"); + optopt_nid_postfix = 0; + return BadCharacter; + } + const GuestOption& option = longOptions[match]; + if (option.hasArgument == NoArgument && hasEqual) { + if (PrintError(options)) Warn(std::string("option `") + dash + std::string(name) + "' doesn't allow an argument"); + optopt_nid_postfix = option.flag ? 0 : option.value; + return BadCharacter; + } + if (option.hasArgument == RequiredArgument || option.hasArgument == OptionalArgument) { + if (hasEqual) optarg_nid_postfix = hasEqual; + else if (option.hasArgument == RequiredArgument) optarg_nid_postfix = arguments[optind_nid_postfix++]; + } + if (option.hasArgument == RequiredArgument && !optarg_nid_postfix) { + if (PrintError(options)) Warn(std::string("option `") + dash + current + "' requires an argument"); + optopt_nid_postfix = option.flag ? 0 : option.value; + --optind_nid_postfix; + return BadArgument(options); + } + if (index) *index = match; + if (option.flag) { + *option.flag = option.value; + return 0; + } + return option.value; +} + +int GetoptInternal(int count, char* const* arguments, const char* options, const GuestOption* longOptions, int* index, int flags) { + if (!options) return -1; + + if (optind_nid_postfix == 0) optind_nid_postfix = optreset_nid_postfix = 1; + + if (g_posixlyCorrect == -1 || optreset_nid_postfix) + g_posixlyCorrect = getenv_nid_postfix("POSIXLY_CORRECT") != nullptr; + if (*options == '-') flags |= FlagAllArguments; + else if (g_posixlyCorrect || *options == '+') flags &= ~FlagPermute; + if (*options == '+' || *options == '-') ++options; + + optarg_nid_postfix = nullptr; + if (optreset_nid_postfix) g_nonOptionStart = g_nonOptionEnd = -1; + + while (optreset_nid_postfix || !*g_longPlace) { + optreset_nid_postfix = 0; + if (optind_nid_postfix >= count) { + g_longPlace = g_emptyMessage; + if (g_nonOptionEnd != -1) { + PermuteArguments(g_nonOptionStart, g_nonOptionEnd, optind_nid_postfix, arguments); + optind_nid_postfix -= g_nonOptionEnd - g_nonOptionStart; + } else if (g_nonOptionStart != -1) { + optind_nid_postfix = g_nonOptionStart; + } + g_nonOptionStart = g_nonOptionEnd = -1; + return -1; + } + g_longPlace = Argument(arguments, optind_nid_postfix, "getopt_long"); + if (*g_longPlace != '-' || g_longPlace[1] == '\0') { + g_longPlace = g_emptyMessage; + if (flags & FlagAllArguments) { + optarg_nid_postfix = arguments[optind_nid_postfix++]; + return InOrder; + } + if (!(flags & FlagPermute)) return -1; + if (g_nonOptionStart == -1) { + g_nonOptionStart = optind_nid_postfix; + } else if (g_nonOptionEnd != -1) { + PermuteArguments(g_nonOptionStart, g_nonOptionEnd, optind_nid_postfix, arguments); + g_nonOptionStart = optind_nid_postfix - (g_nonOptionEnd - g_nonOptionStart); + g_nonOptionEnd = -1; + } + ++optind_nid_postfix; + continue; + } + if (g_nonOptionStart != -1 && g_nonOptionEnd == -1) g_nonOptionEnd = optind_nid_postfix; + + if (g_longPlace[1] != '\0' && *++g_longPlace == '-' && g_longPlace[1] == '\0') { + ++optind_nid_postfix; + g_longPlace = g_emptyMessage; + if (g_nonOptionEnd != -1) { + PermuteArguments(g_nonOptionStart, g_nonOptionEnd, optind_nid_postfix, arguments); + optind_nid_postfix -= g_nonOptionEnd - g_nonOptionStart; + } + g_nonOptionStart = g_nonOptionEnd = -1; + return -1; + } + break; + } + + if (longOptions && g_longPlace != 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++); + const char* found = nullptr; + if (character == ':' || (character == '-' && *g_longPlace != '\0') || + (found = std::strchr(options, character)) == nullptr) { + if (character == '-' && *g_longPlace == '\0') return -1; + if (!*g_longPlace) ++optind_nid_postfix; + if (PrintError(options)) + Warn((g_posixlyCorrect ? "illegal option -- " : "invalid option -- ") + Character(character)); + optopt_nid_postfix = character; + return BadCharacter; + } + if (longOptions && character == 'W' && found[1] == ';') { + if (!*g_longPlace) { + if (++optind_nid_postfix >= count) { + g_longPlace = g_emptyMessage; + if (PrintError(options)) Warn("option requires an argument -- " + Character(character)); + optopt_nid_postfix = character; + return BadArgument(options); + } + g_longPlace = Argument(arguments, optind_nid_postfix, "getopt_long"); + } + const int result = ParseLongOptions(arguments, options, longOptions, index, "-W ", false, flags); + g_longPlace = g_emptyMessage; + return result; + } + if (*++found != ':') { + if (!*g_longPlace) ++optind_nid_postfix; + } else { + optarg_nid_postfix = nullptr; + if (*g_longPlace) { + optarg_nid_postfix = g_longPlace; + } else if (found[1] != ':') { + if (++optind_nid_postfix >= count) { + g_longPlace = g_emptyMessage; + if (PrintError(options)) Warn("option requires an argument -- " + Character(character)); + optopt_nid_postfix = character; + return BadArgument(options); + } + optarg_nid_postfix = arguments[optind_nid_postfix]; + } + g_longPlace = g_emptyMessage; + ++optind_nid_postfix; + } + return character; +} + +} + +extern "C" { + +int APS5_VABI getopt_nid_postfix(int count, char* const* arguments, const char* options) { + if (!options) throw std::invalid_argument("getopt: null option string"); + if (optreset_nid_postfix || *g_shortPlace == '\0') { + optreset_nid_postfix = 0; + if (optind_nid_postfix >= count) { + g_shortPlace = g_emptyMessage; + return -1; + } + g_shortPlace = Argument(arguments, optind_nid_postfix, "getopt"); + if (*g_shortPlace++ != '-') { + g_shortPlace = g_emptyMessage; + return -1; + } + optopt_nid_postfix = OptionCharacter(g_shortPlace++); + if (optopt_nid_postfix == '-' && *g_shortPlace == '\0') { + ++optind_nid_postfix; + g_shortPlace = g_emptyMessage; + return -1; + } + if (optopt_nid_postfix == 0) { + g_shortPlace = g_emptyMessage; + if (!std::strchr(options, '-')) return -1; + optopt_nid_postfix = '-'; + } + } else { + optopt_nid_postfix = OptionCharacter(g_shortPlace++); + } + + const char* found = nullptr; + if (optopt_nid_postfix == ':' || (found = std::strchr(options, optopt_nid_postfix)) == nullptr) { + if (*g_shortPlace == '\0') ++optind_nid_postfix; + if (opterr_nid_postfix && *options != ':') Warn("illegal option -- " + Character(optopt_nid_postfix)); + return BadCharacter; + } + + if (found[1] != ':') { + optarg_nid_postfix = nullptr; + if (*g_shortPlace == '\0') ++optind_nid_postfix; + } else { + if (*g_shortPlace) { + optarg_nid_postfix = g_shortPlace; + } else if (found[2] == ':') { + optarg_nid_postfix = nullptr; + } else if (count > ++optind_nid_postfix) { + optarg_nid_postfix = arguments[optind_nid_postfix]; + } else { + g_shortPlace = g_emptyMessage; + if (*options == ':') return ':'; + if (opterr_nid_postfix) Warn("option requires an argument -- " + Character(optopt_nid_postfix)); + return BadCharacter; + } + g_shortPlace = g_emptyMessage; + ++optind_nid_postfix; + } + return optopt_nid_postfix; +} + +int APS5_VABI getopt_long_nid_postfix(int count, char* const* arguments, const char* options, const GuestOption* longOptions, int* index) { + return GetoptInternal(count, arguments, options, longOptions, index, FlagPermute); +} + +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 new file mode 100644 index 0000000000..c66f3dcc3b --- /dev/null +++ b/core/libs/prx/libSceLibcInternal/PathStatistics.cpp @@ -0,0 +1,161 @@ +#include "prx/libc/include/FilesystemError.hpp" +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#include +#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*); + +namespace { +int Fail(int error) { + errno = error; + return -1; +} + +int HostError(int error) { + switch (error) { + case ENOENT: return 2; + case EINTR: return 4; + case EIO: return 5; + case ENOMEM: return 12; + case EACCES: return 13; + case ENOTDIR: return 20; + case EINVAL: return 22; + case ENFILE: return 23; + case EMFILE: return 24; + case EAGAIN: return 35; + case ELOOP: return 62; + case ENAMETOOLONG: return 63; + default: throw std::system_error(error, std::generic_category(), "statvfs: unsupported host error"); + } +} + +#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() { + const int saved = errno; +#ifdef _WIN32 + ::_close(value); +#else + ::close(value); +#endif + errno = saved; + } +}; + +int OpenMetadata(const std::filesystem::path& path, bool directory) { +#ifdef _WIN32 + const HANDLE handle = CreateFileW(path.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) { + const DWORD error = GetLastError(); + if (error == ERROR_PATH_NOT_FOUND || error == ERROR_INVALID_NAME || error == ERROR_DIRECTORY) { + for (auto prefix = path.parent_path(); !prefix.empty();) { + const DWORD attributes = GetFileAttributesW(prefix.c_str()); + if (attributes != INVALID_FILE_ATTRIBUTES && !(attributes & FILE_ATTRIBUTE_DIRECTORY)) return Fail(20); + const auto parent = prefix.parent_path(); + if (parent == prefix) break; + prefix = parent; + } + } + switch (error) { + case ERROR_FILE_NOT_FOUND: + case ERROR_PATH_NOT_FOUND: + case ERROR_INVALID_DRIVE: return Fail(2); + case ERROR_ACCESS_DENIED: + case ERROR_SHARING_VIOLATION: return Fail(13); + case ERROR_DIRECTORY: return Fail(20); + case ERROR_INVALID_NAME: + case ERROR_INVALID_PARAMETER: return Fail(22); + case ERROR_CANT_RESOLVE_FILENAME: return Fail(62); + case ERROR_FILENAME_EXCED_RANGE: return Fail(63); + default: throw std::system_error(error, std::system_category(), "statvfs: CreateFileW"); + } + } + if (directory) { + BY_HANDLE_FILE_INFORMATION info{}; + if (!GetFileInformationByHandle(handle, &info)) { + const DWORD error = GetLastError(); + CloseHandle(handle); + throw std::system_error(error, std::system_category(), "statvfs: GetFileInformationByHandle"); + } + if (!(info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) { + CloseHandle(handle); + return Fail(20); + } + } + const int descriptor = ::_open_osfhandle(reinterpret_cast(handle), _O_RDONLY | _O_BINARY | _O_NOINHERIT); + if (descriptor < 0) { + const int error = errno; + CloseHandle(handle); + return Fail(HostError(error)); + } + return descriptor; +#else + const int descriptor = ::open(path.c_str(), O_PATH | O_CLOEXEC | (directory ? O_DIRECTORY : 0)); + return descriptor < 0 ? Fail(HostError(errno)) : descriptor; +#endif +} +#endif +} + +extern "C" int APS5_VABI statvfs_nid_postfix(const char* path, LibcStatvfs* output) { + 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 new file mode 100644 index 0000000000..d27a5531c4 --- /dev/null +++ b/core/libs/prx/libSceLibcInternal/SignalList.cpp @@ -0,0 +1,92 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { + +extern const char* const sys_siglist_nid_postfix[32] = { + "Signal 0", + "Hangup", + "Interrupt", + "Quit", + "Illegal instruction", + "Trace/BPT trap", + "Abort trap", + "EMT trap", + "Floating point exception", + "Killed", + "Bus error", + "Segmentation fault", + "Bad system call", + "Broken pipe", + "Alarm clock", + "Terminated", + "Urgent I/O condition", + "Suspended (signal)", + "Suspended", + "Continued", + "Child exited", + "Stopped (tty input)", + "Stopped (tty output)", + "I/O possible", + "Cputime limit exceeded", + "Filesize limit exceeded", + "Virtual timer expired", + "Profiling timer expired", + "Window size changes", + "Information request", + "User defined signal 1", + "User defined signal 2", +}; + +extern const int sys_nsig_nid_postfix = sizeof(sys_siglist_nid_postfix) / sizeof(sys_siglist_nid_postfix[0]); + +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 new file mode 100644 index 0000000000..b40cbc9d10 --- /dev/null +++ b/core/libs/prx/libSceLibcInternal/Spawn.cpp @@ -0,0 +1,215 @@ +#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"); + return **attributes; +} + +PosixSpawnFileActions& FileActions(PosixSpawnFileActions* const* actions, const char* function) { + if (actions == nullptr || *actions == nullptr) throw std::invalid_argument(std::string(function) + ": uninitialized file actions"); + return **actions; +} + +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)); + } catch (const std::bad_alloc&) { + return OutOfMemory; + } + return 0; +} + +} + +extern "C" { + +int APS5_VABI posix_spawnattr_init_nid_postfix(PosixSpawnAttributes** attributes) { + if (attributes == nullptr) throw std::invalid_argument("posix_spawnattr_init: null attributes"); + auto* created = new (std::nothrow) PosixSpawnAttributes{}; + if (created == nullptr) return OutOfMemory; + *attributes = created; + return 0; +} + +int APS5_VABI posix_spawnattr_destroy_nid_postfix(PosixSpawnAttributes** attributes) { + if (attributes == nullptr) throw std::invalid_argument("posix_spawnattr_destroy: null attributes"); + delete *attributes; + return 0; +} + +int APS5_VABI posix_spawnattr_setflags_nid_postfix(PosixSpawnAttributes** attributes, std::int16_t flags) { + Attributes(attributes, "posix_spawnattr_setflags").flags = flags; + return 0; +} + +int APS5_VABI posix_spawnattr_setsigdefault_nid_postfix(PosixSpawnAttributes** attributes, const GuestSignalSet* signalDefault) { + auto& target = Attributes(attributes, "posix_spawnattr_setsigdefault"); + if (signalDefault == nullptr) throw std::invalid_argument("posix_spawnattr_setsigdefault: null signal set"); + target.signalDefault = *signalDefault; + return 0; +} + +int APS5_VABI posix_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{}; + if (created == nullptr) return -1; + *actions = created; + return 0; +} + +int APS5_VABI posix_spawn_file_actions_destroy_nid_postfix(PosixSpawnFileActions** actions) { + delete &FileActions(actions, "posix_spawn_file_actions_destroy"); + return 0; +} + +int APS5_VABI posix_spawn_file_actions_addopen_nid_postfix(PosixSpawnFileActions** actions, int descriptor, const char* path, int openFlags, std::uint16_t mode) { + if (descriptor < 0) return BadDescriptor; + if (path == nullptr) throw std::invalid_argument("posix_spawn_file_actions_addopen: null path"); + auto& target = FileActions(actions, "posix_spawn_file_actions_addopen"); + try { + return Append(target, {PosixSpawnFileActionKind::Open, descriptor, path, openFlags, mode, 0}); + } catch (const std::bad_alloc&) { + return OutOfMemory; + } +} + +int APS5_VABI posix_spawn_file_actions_adddup2_nid_postfix(PosixSpawnFileActions** actions, int descriptor, int newDescriptor) { + if (descriptor < 0 || newDescriptor < 0) return BadDescriptor; + return Append(FileActions(actions, "posix_spawn_file_actions_adddup2"), {PosixSpawnFileActionKind::Duplicate, descriptor, {}, 0, 0, newDescriptor}); +} + +int APS5_VABI posix_spawn_file_actions_addclose_nid_postfix(PosixSpawnFileActions** actions, int descriptor) { + if (descriptor < 0) return BadDescriptor; + return Append(FileActions(actions, "posix_spawn_file_actions_addclose"), {PosixSpawnFileActionKind::Close, descriptor, {}, 0, 0, 0}); +} + +int APS5_VABI posix_spawn_nid_postfix(std::int32_t* processId, const char* path, PosixSpawnFileActions* const* actions, PosixSpawnAttributes* const* attributes, char* const* arguments, char* const* environment) { + (void)processId; + (void)arguments; + (void)environment; + 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)arguments; + (void)environment; + return Spawn("posix_spawnp", file, true, actions, attributes); +} + +} diff --git a/core/libs/prx/libSceLibcInternal/UserContext.cpp b/core/libs/prx/libSceLibcInternal/UserContext.cpp new file mode 100644 index 0000000000..f5f6c7eae8 --- /dev/null +++ b/core/libs/prx/libSceLibcInternal/UserContext.cpp @@ -0,0 +1,63 @@ +#include "prx/libc/include/GuestStacks.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/System/include/UserContext.hpp" +#include +#include +#include +#include + +extern "C" int APS5_VABI setcontext_nid_postfix(const UserUcontext* context); + +namespace { + +using ContextFunction = void (APS5_VABI*)(std::uint64_t, std::uint64_t, std::uint64_t, std::uint64_t, std::uint64_t, std::uint64_t); + +constexpr int MaximumArguments = 6; +constexpr std::uint64_t MinimumStackSize = 2048; + +} + +extern "C" { + +[[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(); +} + +void APS5_VABI makecontext_nid_postfix(UserUcontext* context, void (APS5_VABI* start)(), int count, ...) { + if (context == nullptr || context->mcontext.len != sizeof(UserMcontext)) return; + if (count < 0 || count > MaximumArguments || context->stackPointer == nullptr || context->stackSize < MinimumStackSize) { + context->mcontext.len = 0; + return; + } + const auto top = (reinterpret_cast(context->stackPointer) + context->stackSize) & ~static_cast(15); + auto* arguments = reinterpret_cast(top) - MaximumArguments; + auto* stack = arguments - 1; +#ifdef _WIN32 + __builtin_sysv_va_list list; + __builtin_sysv_va_start(list, count); + for (int index = 0; index < count; ++index) arguments[index] = __builtin_va_arg(list, std::uint64_t); + __builtin_sysv_va_end(list); +#else + std::va_list list; + va_start(list, count); + for (int index = 0; index < count; ++index) arguments[index] = va_arg(list, std::uint64_t); + va_end(list); +#endif + for (int index = count; index < MaximumArguments; ++index) arguments[index] = 0; + *stack = 0; + auto& machine = context->mcontext; + machine.rdi = reinterpret_cast(context); + machine.rsi = reinterpret_cast(start); + machine.rdx = reinterpret_cast(arguments); + machine.rbp = 0; + machine.rbx = reinterpret_cast(stack); + machine.rsp = reinterpret_cast(stack); + machine.rip = reinterpret_cast(&Aps5MakecontextStart_nid_no_patch); + GuestStacks::GuestStackRegister_nid_no_patch(context->stackPointer, context->stackSize); +} + +} diff --git a/core/libs/prx/libSceLibcInternal/include/Spawn.hpp b/core/libs/prx/libSceLibcInternal/include/Spawn.hpp new file mode 100644 index 0000000000..5e93fa8159 --- /dev/null +++ b/core/libs/prx/libSceLibcInternal/include/Spawn.hpp @@ -0,0 +1,37 @@ +#ifndef CORE_LIBS_PRX_LIBSCELIBCINTERNAL_INCLUDE_SPAWN_HPP +#define CORE_LIBS_PRX_LIBSCELIBCINTERNAL_INCLUDE_SPAWN_HPP + +#include +#include +#include + +struct GuestSignalSet { + std::uint32_t bits[4]; +}; +static_assert(sizeof(GuestSignalSet) == 16); + +struct PosixSpawnAttributes { + std::int16_t flags; + std::int32_t processGroup; + std::int32_t schedulingPriority; + std::int32_t schedulingPolicy; + GuestSignalSet signalDefault; + GuestSignalSet signalMask; +}; + +enum class PosixSpawnFileActionKind { Open, Duplicate, Close }; + +struct PosixSpawnFileAction { + PosixSpawnFileActionKind kind; + int descriptor; + std::string path; + int openFlags; + std::uint16_t mode; + int newDescriptor; +}; + +struct PosixSpawnFileActions { + std::vector actions; +}; + +#endif diff --git a/core/libs/prx/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/Export.cpp b/core/libs/prx/libSceLibreSsl/Export.cpp new file mode 100644 index 0000000000..93b05e0fec --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/Export.cpp @@ -0,0 +1,2004 @@ +#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 MD4_Final_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI MD4_Init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI MD4_Update_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI MD5_Final_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI MD5_Init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI MD5_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 SHA1_Final_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SHA1_Init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SHA1_Update_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SHA224_Final_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SHA224_Init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SHA224_Update_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SHA256_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SHA256_Final_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SHA256_Init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SHA256_Update_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SHA384_Final_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SHA384_Init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SHA384_Update_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SHA512_Final_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SHA512_Init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SHA512_Update_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/libSceLibreSsl3/Export.cpp b/core/libs/prx/libSceLibreSsl3/Export.cpp new file mode 100644 index 0000000000..d3289b659b --- /dev/null +++ b/core/libs/prx/libSceLibreSsl3/Export.cpp @@ -0,0 +1,444 @@ +#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(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +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 MD5_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 SHA1_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/libSceLoginDialog/Export.cpp b/core/libs/prx/libSceLoginDialog/Export.cpp index a986655ab2..7ca5a3626c 100644 --- a/core/libs/prx/libSceLoginDialog/Export.cpp +++ b/core/libs/prx/libSceLoginDialog/Export.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -11,12 +12,29 @@ 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_ERROR_NOT_INITIALIZED = static_cast(0x81340001u); +constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x81340002u); +constexpr int COMMON_DIALOG_ERROR_PARAM_INVALID = static_cast(0x81340003u); +constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast(0x81340005u); +constexpr int COMMON_DIALOG_ERROR_NO_SERVICE = static_cast(0x81340007u); constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; +constexpr std::uint32_t LOGIN_DIALOG_PARAM_SIZE = 0x40; +constexpr std::uint32_t LOGIN_DIALOG_SELECTOR_LIMIT = 2; +constexpr std::size_t LOGIN_DIALOG_FIRST_RESERVED_WORD = 0x2C / 4; + +bool ValidOpenParam(const void* param) { + if (param == nullptr) return false; + std::uint32_t size = 0; + std::memcpy(&size, param, sizeof(size)); + if (size != LOGIN_DIALOG_PARAM_SIZE) return false; + std::uint32_t words[LOGIN_DIALOG_PARAM_SIZE / sizeof(std::uint32_t)]; + std::memcpy(words, param, sizeof(words)); + if (words[1] >= LOGIN_DIALOG_SELECTOR_LIMIT) return false; + for (std::size_t i = LOGIN_DIALOG_FIRST_RESERVED_WORD; i < std::size(words); ++i) { + if (words[i] != 0) return false; + } + return true; +} std::atomic g_status{COMMON_DIALOG_STATUS_NONE}; @@ -31,10 +49,10 @@ int APS5_VABI sceLoginDialogInitialize(void) { } int APS5_VABI sceLoginDialogOpen(const void* param) { + if (!ValidOpenParam(param)) return COMMON_DIALOG_ERROR_PARAM_INVALID; 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; } @@ -48,17 +66,17 @@ int APS5_VABI sceLoginDialogGetStatus(void) { } int APS5_VABI sceLoginDialogGetResult(void* result) { + if (result == nullptr) return COMMON_DIALOG_ERROR_PARAM_INVALID; 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; + const std::int32_t words[] = {COMMON_DIALOG_RESULT_USER_CANCELED, COMMON_DIALOG_ERROR_NO_SERVICE}; + std::memcpy(result, words, sizeof(words)); + return status == COMMON_DIALOG_STATUS_FINISHED ? 0 : COMMON_DIALOG_ERROR_BUSY; } int APS5_VABI sceLoginDialogClose(void) { if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } diff --git a/core/libs/prx/libSceLoginMgrServer/Export.cpp b/core/libs/prx/libSceLoginMgrServer/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceLoginMgrServer/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/libSceLoginService/CMakeLists.txt b/core/libs/prx/libSceLoginService/CMakeLists.txt new file mode 100644 index 0000000000..5993f09b49 --- /dev/null +++ b/core/libs/prx/libSceLoginService/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceLoginService SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceLoginService PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceLoginService) + +if(BUILD_TESTING) + add_test_executable(guest_login_service_tests ${LIBS_INCLUDE_DIR}/tests/GuestLoginService.cpp) + target_include_directories(guest_login_service_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_login_service_tests PRIVATE libSceLoginService libc) + configure_windows_unwind(guest_login_service_tests) + add_test(NAME guest_login_service COMMAND guest_login_service_tests) + if(WIN32) + set_tests_properties(guest_login_service PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceLoginService/Export.cpp b/core/libs/prx/libSceLoginService/Export.cpp new file mode 100644 index 0000000000..43a1b1dcdf --- /dev/null +++ b/core/libs/prx/libSceLoginService/Export.cpp @@ -0,0 +1,21 @@ +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceLoginServiceInitialize(void) { + return 0; +} + +int APS5_VABI sceLoginServiceTerminate(void) { + return 0; +} + +int APS5_VABI sceLoginServiceRequestDevices(void* param) { + (void)param; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/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..2107420e01 100644 --- a/core/libs/prx/libSceMbus/Export.cpp +++ b/core/libs/prx/libSceMbus/Export.cpp @@ -34,4 +34,8 @@ std::int32_t APS5_VABI sceDeviceServiceQueryDeviceInfo_(std::int32_t deviceClass return 0; } +std::int32_t APS5_VABI sceDeviceServiceGetGeneration(void) { + NotImplemented_nid_no_patch(__func__); +} + } diff --git a/core/libs/prx/libSceMbus/PendingExports.cpp b/core/libs/prx/libSceMbus/PendingExports.cpp new file mode 100644 index 0000000000..907eeb12c0 --- /dev/null +++ b/core/libs/prx/libSceMbus/PendingExports.cpp @@ -0,0 +1,578 @@ +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceMbusAcquireControl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAcquireControlList() { + return static_cast(0x809B00FF); +} + +int APS5_VABI sceMbusAcquireControlWithState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAcquireControlWithState2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAcquireControlWithStateFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAddHandle() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAddHandleByDeviceId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAddHandleByUserId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAddHandleByVidPid() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAddHandleWithModuleId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAttachToApplicationByPid() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusBindDeviceWithUserId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusChangeAppModuleState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusCheckDriverCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusConvertToLocalDeviceId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusConvertToLocalDeviceId2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusConvertToMbusDeviceId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugAcquireControl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugAcquireControlWithState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugAcquireControlWithState2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugAcquireControlWithStateFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugAddProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugCheckProcessResume() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugDisableBgmForShellUi() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugGetApplicationStartupInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugGetControlStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugGetDeviceInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugGetInternalInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugGetPriorityInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugReenableBgmForShellUi() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugReleaseControl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugRemoveCameraAppModuleFocus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugResumeApplication() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugSetApplicationFocusByAppId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugSetCameraAppModuleFocus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugSetControllerFocusByAppId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugSetOtherProcessExcludedAction() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugSetPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugStartApplication() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugStartApplication2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugStartApplicationNull() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugSuspendApplication() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugTerminateApplication() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugTerminateProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDetachFromApplicationByPid() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDisableBgmForShellUi() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDisableChat() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDisconnectDevice() { + 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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDumpHandleStatusInfoAll() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDumpModuleStateInfo() { + 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() { + 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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusEnterAudioOutAutoAttenuation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusEventBusStatusChangeSubscribe() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusEventBusStatusChangeUnsubscribe() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusEventCallbackFuncsInit_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusEventCreate_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusEventDestroy() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusEventReceive() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetControlStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetDeviceDescription() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetDeviceInfoByBusId_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetDeviceInfoByConditionForDeviceService() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetDeviceInfoByCondition_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetDeviceInfo_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetSimulatedBusUsableStatusByBusType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetSimulatedHandleStatusInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetSimulatedHandleStatusInfo2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetSparkState() { + 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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusLeaveAudioOutAutoAttenuation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusNotifyBtDisconnection() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusNotifyDriverEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusReenableBgmForShellUi() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusRegisterDriver() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusReleaseControl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusRemoveCameraAppModuleFocus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusRemoveHandle() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusRemoveProxy() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusResolveByDeviceId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusResolveByHandle() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusResolveByPlayerId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusResolveByUserId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetAutoLoginMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetCameraAppModuleFocus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetDeviceFunctionState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetDuckingSetting() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetProxy() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetRoutingMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetScratchData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetScratchDataUpdatedEventMask() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetSocialScreenAudioMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetTestFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetThreadParam() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetUsbPowerBlackList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetUserMuteStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetVolumeDown() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusStartAudioOutAuxBroadcast() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusStartAudioOutSharePlay() { + 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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusUserLogin() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusUserLogout() { + 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/CMakeLists.txt b/core/libs/prx/libSceMouse/CMakeLists.txt index a8284f73af..b7f58761da 100644 --- a/core/libs/prx/libSceMouse/CMakeLists.txt +++ b/core/libs/prx/libSceMouse/CMakeLists.txt @@ -11,8 +11,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tests/Mouse.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../libSceVideoOut/src/MouseInput.cpp ) - target_include_directories(mouse_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/SDL2/include) - target_link_libraries(mouse_tests PRIVATE libSceMouse libkernel libc) + target_include_directories(mouse_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) + target_link_libraries(mouse_tests PRIVATE libSceMouse libkernel libc SDL2-static) configure_windows_unwind(mouse_tests) add_test(NAME mouse_input COMMAND mouse_tests) if(WIN32) diff --git a/core/libs/prx/libSceMouse/Export.cpp b/core/libs/prx/libSceMouse/Export.cpp index 9d820d245d..121b9d2e6a 100644 --- a/core/libs/prx/libSceMouse/Export.cpp +++ b/core/libs/prx/libSceMouse/Export.cpp @@ -1,4 +1,5 @@ #include "prx/libSceMouse/include/MouseState.hpp" +#include "prx/libc/include/General.hpp" extern "C" { @@ -18,8 +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(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; +} + +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 bf7d57a09f..390cf33968 100644 --- a/core/libs/prx/libSceMouse/include/MouseState.hpp +++ b/core/libs/prx/libSceMouse/include/MouseState.hpp @@ -16,13 +16,16 @@ struct MouseInputEvent { }; namespace Mouse { +bool IsOpen(); int Initialize(); int Open(int userId, std::int32_t type, std::int32_t index, const MouseOpenParam* param); int Close(std::int32_t handle); 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" 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 5bcf78e720..d73ccb929d 100644 --- a/core/libs/prx/libSceMouse/src/mouse_impl.cpp +++ b/core/libs/prx/libSceMouse/src/mouse_impl.cpp @@ -27,6 +27,11 @@ void enqueue(const MouseData& data) { namespace Mouse { +bool IsOpen() { + std::lock_guard lock(mouseMutex); + return opened; +} + int Initialize() { std::lock_guard lock(mouseMutex); initialized = true; @@ -77,6 +82,13 @@ int Read(std::int32_t handle, MouseData* data, std::int32_t num) { return available; } +int ResetAbsolutePosition(std::int32_t handle, std::uint16_t, std::uint16_t) { + std::lock_guard lock(mouseMutex); + if (!initialized) return MOUSE_ERROR_NOT_INITIALIZED; + if (handle != MOUSE_HANDLE || !opened) return MOUSE_ERROR_INVALID_HANDLE; + return MOUSE_OK; +} + void Publish(const MouseInputEvent& event) { std::lock_guard lock(mouseMutex); if (event.connectionChange) { diff --git a/core/libs/prx/libSceMouse/tests/Mouse.cpp b/core/libs/prx/libSceMouse/tests/Mouse.cpp index e01f3f9b4f..3fd5a90ca2 100644 --- a/core/libs/prx/libSceMouse/tests/Mouse.cpp +++ b/core/libs/prx/libSceMouse/tests/Mouse.cpp @@ -7,25 +7,33 @@ #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(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 +100,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..2ffc51523e --- /dev/null +++ b/core/libs/prx/libSceMove/Export.cpp @@ -0,0 +1,166 @@ +#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) { + std::lock_guard lock(stateMutex); + if (!initialized) return MOVE_ERROR_NOT_INIT; + return openHandles.contains(handle) ? MOVE_OK : MOVE_ERROR_INVALID_HANDLE; +} + +} + +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); result != MOVE_OK) return result; + if (!info) return MOVE_ERROR_INVALID_ARG; + return MOVE_NO_CONTROLLER_CONNECTED; +} + +int APS5_VABI sceMoveGetExtensionPortInfo(int32_t handle, void* data) { + if (const int result = checkHandle(handle); result != MOVE_OK) return result; + if (!data) return MOVE_ERROR_INVALID_ARG; + return MOVE_NO_CONTROLLER_CONNECTED; +} + +int APS5_VABI sceMoveReadStateLatest(int32_t handle, void* data) { + if (const int result = checkHandle(handle); result != MOVE_OK) return result; + if (!data) return MOVE_ERROR_INVALID_ARG; + 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); result != MOVE_OK) return result; + if (timestamp < 0 || !data || !outCount) return MOVE_ERROR_INVALID_ARG; + 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..8b2c497e09 --- /dev/null +++ b/core/libs/prx/libSceMoveTracker/Export.cpp @@ -0,0 +1,96 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceMoveTrackerCalibrateReset() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerCalibrateYaw() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerCameraUpdate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerCaptureInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerCaptureTerm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerControllersUpdate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerGetState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerGetVersion() { + return 2; +} + +int APS5_VABI sceMoveTrackerGetWorkingMemorySize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerPlayGetImages() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerPlayGetStates() { + 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..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceMusicFrameworkInterface/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/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..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceNKWebKit/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/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 84e0b910fa..95faf4584b 100644 --- a/core/libs/prx/libSceNet/CMakeLists.txt +++ b/core/libs/prx/libSceNet/CMakeLists.txt @@ -1,8 +1,71 @@ 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) + 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) + configure_windows_unwind(guest_datagram_receive_tests) + foreach(api IN ITEMS recv recvfrom sceNetRecv sceNetRecvfrom) + add_test(NAME guest_datagram_${api} COMMAND guest_datagram_receive_tests ${api}) + set_tests_properties(guest_datagram_${api} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_datagram_${api} PROPERTIES ENVIRONMENT_MODIFICATION + "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 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 + PROPERTIES TIMEOUT 20 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endforeach() + endif() add_test_executable(guest_sce_net_tests ${LIBS_INCLUDE_DIR}/tests/GuestSceNet.cpp) target_include_directories(guest_sce_net_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_sce_net_tests PRIVATE libSceNet libSceNetCtl libkernel libc) @@ -10,6 +73,7 @@ if(BUILD_TESTING) add_test(NAME guest_sce_net COMMAND guest_sce_net_tests) set_tests_properties(guest_sce_net PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77) if(WIN32) + target_link_libraries(guest_sce_net_tests PRIVATE ws2_32) set_tests_properties(guest_sce_net PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index b4a89eb265..82fb9cbd4c 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -9,11 +9,13 @@ #include #include #include +#include #include #include #include #endif #include +#include #include #include #include @@ -22,32 +24,43 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include #include +#include #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" // Guest socket descriptors retain PS5 semantics while their transport is backed by host sockets. namespace { // FreeBSD errno numbers as reported through sceNetErrnoLoc (SCE_NET_ERROR_* is 0x80410100 + errno). constexpr int NET_ENOENT = 2; +constexpr int NET_EINTR = 4; constexpr int NET_EBADF = 9; constexpr int NET_EACCES = 13; constexpr int NET_EFAULT = 14; constexpr int NET_EINVAL = 22; constexpr int NET_ENOSPC = 28; +constexpr int NET_EDOM = 33; 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; @@ -64,18 +77,35 @@ constexpr int NET_ECONNREFUSED = 61; constexpr int NET_EHOSTUNREACH = 65; constexpr int NET_ERROR_BASE = static_cast(0x80410100u); constexpr int NET_ERROR_RESOLVER_ENODNS = static_cast(0x804101E1u); +constexpr int NET_RESOLVER_ABORT_FLAG_NTOA_PRESERVATION = 0x1; +constexpr int NET_RESOLVER_ABORT_FLAG_ATON_PRESERVATION = 0x2; constexpr int NET_AF_INET = 2; constexpr int NET_AF_INET6 = 28; constexpr int NET_SOCK_STREAM = 1; constexpr int NET_SOCK_DGRAM = 2; constexpr int NET_SOCK_RAW = 3; +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; +constexpr int NET_SO_SCE_RCVTIMEO = 0x1106; constexpr int NET_MSG_PEEK = 0x2; constexpr int NET_MSG_TRUNC = 0x10; +constexpr int NET_MSG_DONTWAIT = 0x80; constexpr int NET_UIO_MAXIOV = 1024; #ifdef _WIN32 @@ -91,31 +121,8 @@ bool initialize_sockets() { return result == 0; } void close_socket(NativeSocket socket) { closesocket(socket); } -int native_error() { - switch (WSAGetLastError()) { - 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; - default: return 5; - } -} +int net_error(int code) { return GuestSockets::GuestErrorFromHost(code); } +int native_error() { return net_error(WSAGetLastError()); } #else using NativeSocket = int; using NativeLength = socklen_t; @@ -123,40 +130,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 native_error() { - switch (errno) { - 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); } @@ -166,7 +158,7 @@ bool guest_to_native_address(const void* address, std::uint32_t length, sockaddr_storage& native, NativeLength& native_length) { if (!address || length < 2) return false; const auto* bytes = static_cast(address); - if (bytes[1] == NET_AF_INET && bytes[0] == 16 && length >= 16) { + if (bytes[1] == NET_AF_INET && length >= 16) { auto& v4 = reinterpret_cast(native); v4.sin_family = AF_INET; std::memcpy(&v4.sin_port, bytes + 2, 2); @@ -174,7 +166,7 @@ bool guest_to_native_address(const void* address, std::uint32_t length, native_length = sizeof(v4); return true; } - if (bytes[1] == NET_AF_INET6 && bytes[0] == 28 && length >= 28) { + if (bytes[1] == NET_AF_INET6 && length >= 28) { auto& v6 = reinterpret_cast(native); v6.sin6_family = AF_INET6; std::memcpy(&v6.sin6_port, bytes + 2, 2); @@ -189,6 +181,10 @@ bool guest_to_native_address(const void* address, std::uint32_t length, bool native_to_guest_address(const sockaddr_storage& native, void* address, std::uint32_t* length) { if (!address || !length) return false; + if (native.ss_family == AF_UNSPEC) { + *length = 0; + return true; + } std::uint8_t bytes[28]{}; if (native.ss_family == AF_INET) { const auto& v4 = reinterpret_cast(native); @@ -207,9 +203,9 @@ bool native_to_guest_address(const sockaddr_storage& native, void* address, std: } else { return false; } - const auto required = static_cast(bytes[0]); - std::memcpy(address, bytes, std::min(*length, required)); - *length = required; + const auto copied = std::min(*length, bytes[0]); + std::memcpy(address, bytes, copied); + *length = copied; return true; } @@ -217,12 +213,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 @@ -288,7 +286,14 @@ struct NetMemoryPoolStats { std::size_t max_inuse_size; std::size_t current_inuse_size; }; -std::map g_resolvers; +struct Resolver { + int error = 0; + int running = 0; + std::uint64_t aborts = 0; + bool abort_ntoa = false; + bool abort_aton = false; +}; +std::map g_resolvers; int g_next_sock = 32; int g_next_epoll = 0x4000; int g_next_pool = 1; @@ -300,15 +305,79 @@ int* errno_slot() { return &e; } -int fail(int err) { +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; + *__error_nid_postfix() = err; +} + +int fail(int err) { + set_errno(err); return NET_ERROR_BASE | err; } -void set_resolver_error(int rid, int error) { +int set_native_timeout(NativeSocket native, int optname, std::int64_t microseconds) { + const int native_option = optname == NET_SO_RCVTIMEO ? SO_RCVTIMEO : SO_SNDTIMEO; +#ifdef _WIN32 + const std::int64_t milliseconds = (microseconds + 999) / 1000; + const DWORD timeout_ms = milliseconds > static_cast(MAXDWORD) ? MAXDWORD : static_cast(milliseconds); + return ::setsockopt(native, SOL_SOCKET, native_option, reinterpret_cast(&timeout_ms), sizeof(timeout_ms)); +#else + timeval timeout{}; + timeout.tv_sec = static_cast(microseconds / 1000000); + timeout.tv_usec = static_cast(microseconds % 1000000); + return ::setsockopt(native, SOL_SOCKET, native_option, &timeout, sizeof(timeout)); +#endif +} + +int set_kernel_timeout(int descriptor, int optname, const void* value, std::uint32_t length) { + KernelTimeval timeout{}; + if (value == nullptr && length != 0) return NET_EFAULT; + if (length < sizeof(timeout)) return NET_EINVAL; + std::memcpy(&timeout, value, sizeof(timeout)); + if (timeout.tv_sec < 0 || timeout.tv_usec < 0 || timeout.tv_usec >= 1000000) return NET_EDOM; + const std::int64_t microseconds = std::min(timeout.tv_sec, INT32_MAX) * 1000000 + timeout.tv_usec; + Sock socket; + { + std::lock_guard lk(g_mutex); + const auto it = g_socks.find(descriptor); + if (it == g_socks.end()) return NET_EBADF; + socket = it->second; + } + if (set_native_timeout(socket.native->value, optname, microseconds) != 0) return native_error(); + std::lock_guard lk(g_mutex); + 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.native->receiveTimeout = stored; + else it->second.native->sendTimeout = stored; + } + return 0; +} + +int begin_lookup(int rid, bool Resolver::*preservedAbort, std::uint64_t& generation) { std::lock_guard lk(g_mutex); const auto resolver = g_resolvers.find(rid); - if (resolver != g_resolvers.end()) resolver->second = error; + if (resolver == g_resolvers.end()) return fail(NET_EBADF); + if (std::exchange(resolver->second.*preservedAbort, false)) { + resolver->second.error = NET_ERROR_BASE | NET_EINTR; + return fail(NET_EINTR); + } + ++resolver->second.running; + generation = resolver->second.aborts; + return 0; +} + +int end_lookup(int rid, std::uint64_t generation, int error) { + std::lock_guard lk(g_mutex); + const auto resolver = g_resolvers.find(rid); + if (resolver == g_resolvers.end()) return error; + --resolver->second.running; + if (resolver->second.aborts != generation) error = fail(NET_EINTR); + resolver->second.error = error; + return error; } void log_soft(const char* func, const char* what) { @@ -349,6 +418,12 @@ struct NetMsghdr { static_assert(sizeof(NetMsghdr) == 48 && offsetof(NetMsghdr, iov) == 16 && offsetof(NetMsghdr, control) == 32 && offsetof(NetMsghdr, flags) == 44); +int timeout_option(int optname) { + if (optname == NET_SO_SCE_SNDTIMEO) return NET_SO_SNDTIMEO; + if (optname == NET_SO_SCE_RCVTIMEO) return NET_SO_RCVTIMEO; + return optname; +} + std::int64_t message_length(const NetMsghdr* message) { if (!message) return fail(NET_EFAULT); if (message->iov_length < 0 || message->iov_length > NET_UIO_MAXIOV) return fail(NET_EMSGSIZE); @@ -363,16 +438,190 @@ 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.native->receiveTimeout > 0) return NET_EAGAIN; +#else + (void)socket; +#endif + return native_error(); +} + +bool message_flags_supported(int flags, int accepted) { + 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(completeLength > 0 ? WSAEWOULDBLOCK : WSAETIMEDOUT); + 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 + if (flags & NET_MSG_DONTWAIT) native |= MSG_DONTWAIT; +#endif + 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 extern "C" { extern const std::uint32_t sce_net_in6addr_any[4] = {}; +alignas(std::uint32_t) extern const std::uint8_t sce_net_in6addr_linklocal_allnodes[16] = {0xff, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}; int* APS5_VABI sceNetErrnoLoc(void) { return errno_slot(); } +int APS5_VABI sceNetShowNetstat(void) { + return 0; +} + int PollSockets(KernelSocketPoll::Entry* entries, int count, int timeoutMilliseconds) { std::vector> natives(static_cast(count)); { @@ -432,6 +681,83 @@ int PollSockets(KernelSocketPoll::Entry* entries, int count, int timeoutMillisec const bool g_socketPollerRegistered = (KernelSetSocketPoller_nid_no_patch(&PollSockets), true); +extern "C" int APS5_VABI sceNetSocketClose(int s); + +bool CloseSocketForKernel(int descriptor, int* error) { + { + std::lock_guard lk(g_mutex); + if (g_socks.count(descriptor) == 0) return false; + } + *error = sceNetSocketClose(descriptor) == 0 ? 0 : *errno_slot(); + return true; +} + +const bool g_socketCloserRegistered = (KernelSetSocketCloser_nid_no_patch(&CloseSocketForKernel), true); + +extern "C" int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval, uint32_t optlen); + +bool SetSocketOptionForKernel(int descriptor, int level, int option, const void* value, std::uint32_t length, int* error) { + { + std::lock_guard lk(g_mutex); + if (g_socks.count(descriptor) == 0) return false; + } + if (level == NET_SOL_SOCKET && (option == NET_SO_RCVTIMEO || option == NET_SO_SNDTIMEO)) { + *error = set_kernel_timeout(descriptor, option, value, length); + if (*error != 0) set_errno(*error); + return true; + } + *error = sceNetSetsockopt(descriptor, level, option, value, length) == 0 ? 0 : *errno_slot(); + return true; +} + +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; @@ -472,15 +798,31 @@ 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) { + const int error = native_error(); + close_socket(native); + return fail(error); + } 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); } + const int fd = KernelReserveSocketNumber_nid_no_patch(static_cast(handle->value)); + if (fd < 0) return fail(-fd); std::lock_guard lk(g_mutex); - const int fd = g_next_sock++; Sock s; s.family = family; s.type = type; @@ -489,15 +831,25 @@ 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); + 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) { #ifdef _WIN32 ::shutdown(native->value, SD_BOTH); @@ -546,6 +898,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; } @@ -580,12 +933,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); @@ -594,12 +950,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); - int descriptor; +#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); - descriptor = g_next_sock++; 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); @@ -607,27 +975,90 @@ int APS5_VABI sceNetAccept(int s, void* addr, uint32_t* addrlen) { return descriptor; } +static std::uint16_t& native_port(sockaddr_storage& address) { + return address.ss_family == AF_INET ? reinterpret_cast(address).sin_port : reinterpret_cast(address).sin6_port; +} + +static int disconnect_datagram(const Sock& socket) { + const auto value = socket.native->value; + sockaddr_storage peer{}; + NativeLength peer_length = sizeof(peer); + if (::getpeername(value, reinterpret_cast(&peer), &peer_length) != 0) return 0; + sockaddr_storage local{}; + NativeLength local_length = sizeof(local); + if (::getsockname(value, reinterpret_cast(&local), &local_length) != 0) return fail(native_error()); +#ifdef _WIN32 + sockaddr_storage unspecified{}; + unspecified.ss_family = local.ss_family; + if (::connect(value, reinterpret_cast(&unspecified), local_length) != 0) return fail(native_error()); +#else + sockaddr unspecified{}; + unspecified.sa_family = AF_UNSPEC; + if (::connect(value, &unspecified, sizeof(unspecified)) != 0) return fail(native_error()); +#endif + sockaddr_storage current{}; + NativeLength current_length = sizeof(current); + if (::getsockname(value, reinterpret_cast(¤t), ¤t_length) != 0) return fail(native_error()); + if (native_port(current) != 0) return 0; + sockaddr_storage wildcard{}; + wildcard.ss_family = local.ss_family; + native_port(wildcard) = native_port(local); + if (::bind(value, reinterpret_cast(&wildcard), local_length) != 0) return fail(native_error()); + return 0; +} + +#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; { std::lock_guard lk(g_mutex); auto it = g_socks.find(s); if (it == g_socks.end()) return fail(NET_EBADF); + it->second.reported_error = 0; socket = it->second; } + if (socket.type == NET_SOCK_DGRAM && addr && addrlen >= 2 && static_cast(addr)[1] == 0) { + if (const int error = disconnect_datagram(socket)) return error; + return fail(NET_EAFNOSUPPORT); + } sockaddr_storage native{}; NativeLength native_length = 0; if (!guest_to_native_address(addr, addrlen, native, native_length)) return fail(NET_EINVAL); - 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()); } int64_t APS5_VABI sceNetRecv(int s, void* buf, size_t len, int flags) { - if (flags != 0 && flags != 2) return fail(NET_EOPNOTSUPP); + if (!message_flags_supported(flags, NET_MSG_PEEK | NET_MSG_DONTWAIT)) return fail(NET_EOPNOTSUPP); if (!buf && len) return fail(NET_EINVAL); if (len > INT_MAX) return fail(NET_EMSGSIZE); Sock socket; @@ -637,13 +1068,23 @@ 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; } - const int native_flags = flags == 2 ? MSG_PEEK : 0; - const int result = ::recv(socket.native->value, static_cast(buf), static_cast(len), native_flags); - return result >= 0 ? result : fail(native_error()); +#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 = 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)); } int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* from, uint32_t* fromlen) { - if (flags != 0 && flags != 2) return fail(NET_EOPNOTSUPP); + if (!message_flags_supported(flags, NET_MSG_PEEK | NET_MSG_DONTWAIT)) return fail(NET_EOPNOTSUPP); if (!buf && len) return fail(NET_EINVAL); if (len > INT_MAX) return fail(NET_EMSGSIZE); if (from && !fromlen) return fail(NET_EINVAL); @@ -654,18 +1095,39 @@ 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 = flags == 2 ? MSG_PEEK : 0; - const int result = ::recvfrom(socket.native->value, static_cast(buf), static_cast(len), - native_flags, from ? reinterpret_cast(&peer) : nullptr, from ? &peer_length : nullptr); - if (result < 0) return fail(native_error()); + const int native_flags = native_message_flags(flags); + 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 (from && !native_to_guest_address(peer, from, fromlen)) return fail(NET_EAFNOSUPPORT); return result; } int64_t APS5_VABI sceNetSend(int s, const void* buf, size_t len, int flags) { - if (flags != 0) return fail(NET_EOPNOTSUPP); + if (!message_flags_supported(flags, NET_MSG_DONTWAIT)) return fail(NET_EOPNOTSUPP); if (!buf && len) return fail(NET_EINVAL); if (len > INT_MAX) return fail(NET_EMSGSIZE); Sock socket; @@ -675,12 +1137,14 @@ int64_t APS5_VABI sceNetSend(int s, const void* buf, size_t len, int flags) { if (it == g_socks.end()) return fail(NET_EBADF); socket = it->second; } - const int result = ::send(socket.native->value, static_cast(buf), static_cast(len), NATIVE_SEND_FLAGS); + const int result = 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()); } int64_t APS5_VABI sceNetSendto(int s, const void* buf, size_t len, int flags, const void* to, uint32_t tolen) { - if (flags != 0) return fail(NET_EOPNOTSUPP); + if (!message_flags_supported(flags, NET_MSG_DONTWAIT)) return fail(NET_EOPNOTSUPP); if (!buf && len) return fail(NET_EINVAL); if (len > INT_MAX) return fail(NET_EMSGSIZE); Sock socket; @@ -693,16 +1157,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); + 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, - 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) { @@ -726,7 +1198,7 @@ int64_t APS5_VABI sceNetSendmsg(int s, const NetMsghdr* msg, int flags) { int64_t APS5_VABI sceNetRecvmsg(int s, NetMsghdr* msg, int flags) { const auto total = message_length(msg); if (total < 0) return total; - if (flags != 0 && flags != NET_MSG_PEEK) return fail(NET_EOPNOTSUPP); + if (!message_flags_supported(flags, NET_MSG_PEEK | NET_MSG_DONTWAIT)) return fail(NET_EOPNOTSUPP); Sock socket; { std::lock_guard lk(g_mutex); @@ -734,7 +1206,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)); @@ -743,11 +1232,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()), - flags == NET_MSG_PEEK ? MSG_PEEK : 0, 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(native_error()); + if (!datagram || WSAGetLastError() != WSAEMSGSIZE) return fail(receive_error(socket)); received = static_cast(buffer.size()); #else return fail(native_error()); @@ -778,55 +1270,79 @@ 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()); #endif } else if (optname == NET_SO_RCVTIMEO || optname == NET_SO_SNDTIMEO) { if (value < 0) return fail(NET_EINVAL); -#ifdef _WIN32 - const DWORD timeout_ms = value == 0 ? 0 : static_cast((value + 999) / 1000); - const int native_option = optname == NET_SO_RCVTIMEO ? SO_RCVTIMEO : SO_SNDTIMEO; - if (::setsockopt(socket.native->value, SOL_SOCKET, native_option, - reinterpret_cast(&timeout_ms), sizeof(timeout_ms)) != 0) + 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) { + 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 - timeval timeout{}; - timeout.tv_sec = value / 1000000; - timeout.tv_usec = value % 1000000; - const int native_option = optname == NET_SO_RCVTIMEO ? SO_RCVTIMEO : SO_SNDTIMEO; - if (::setsockopt(socket.native->value, SOL_SOCKET, native_option, &timeout, sizeof(timeout)) != 0) - return fail(native_error()); -#endif + } 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; } @@ -839,15 +1355,59 @@ 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 (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; + switch (timeout_option(optname)) { + 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); + if (::getsockopt(socket.native->value, SOL_SOCKET, SO_ERROR, reinterpret_cast(&pending), &size) != 0) + return fail(native_error()); +#ifdef _WIN32 + std::lock_guard lk(g_mutex); + auto it = g_socks.find(s); + if (it == g_socks.end()) return fail(NET_EBADF); + if (pending != 0 && pending == it->second.reported_error) pending = 0; + else if (pending != 0) it->second.reported_error = pending; +#endif + value = pending == 0 ? 0 : net_error(pending); + break; + } + 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)); @@ -866,8 +1426,15 @@ int APS5_VABI sceNetGetsockname(int s, void* addr, uint32_t* addrlen) { if (!addr || !addrlen) return fail(NET_EINVAL); sockaddr_storage native{}; NativeLength native_length = sizeof(native); - if (::getsockname(socket.native->value, reinterpret_cast(&native), &native_length) != 0) + if (::getsockname(socket.native->value, reinterpret_cast(&native), &native_length) != 0) { +#ifdef _WIN32 + if (WSAGetLastError() != WSAEINVAL) return fail(native_error()); + native = {}; + native.ss_family = socket.family == NET_AF_INET ? AF_INET : AF_INET6; +#else return fail(native_error()); +#endif + } return native_to_guest_address(native, addr, addrlen) ? 0 : fail(NET_EAFNOSUPPORT); } @@ -887,12 +1454,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) { @@ -905,15 +1576,34 @@ int APS5_VABI sceNetEpollCreate(const char* name, int flags) { return id; } -int APS5_VABI sceNetGetMemoryPoolStats(int memid, NetMemoryPoolStats* stats) { - if (stats == nullptr) { - return fail(NET_EINVAL); +int APS5_VABI sceNetShowNetstatWithMemory(int memid) { + struct SocketStatus { int id; int family; int type; bool nonblock; bool bound; bool listening; }; + std::vector sockets; + std::size_t poolSize = 0; + { + std::lock_guard lk(g_mutex); + const auto pool = g_pools.find(memid); + if (pool == g_pools.end()) return fail(NET_EBADF); + poolSize = pool->second; + sockets.reserve(g_socks.size()); + for (const auto& [id, socket] : g_socks) + sockets.push_back({id, socket.family, socket.type, socket.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) + std::fprintf(stdout, "socket=%d family=%d type=%d nonblock=%d bound=%d listening=%d\n", socket.id, socket.family, socket.type, int(socket.nonblock), int(socket.bound), int(socket.listening)); + return 0; +} + +int APS5_VABI sceNetGetMemoryPoolStats(int memid, NetMemoryPoolStats* stats) { std::lock_guard lk(g_mutex); const auto pool = g_pools.find(memid); if (pool == g_pools.end()) { return fail(NET_EBADF); } + if (stats == nullptr) { + return fail(NET_EINVAL); + } *stats = {pool->second, 0, 0}; return 0; } @@ -945,13 +1635,13 @@ int APS5_VABI sceNetEpollControl(int eid, int op, int id, const NetEpollEvent* e if (g_epolls.count(eid) == 0) { return fail(NET_EBADF); } - if (op < 1 || op > 3) return fail(NET_EINVAL); - if (op != 2 && !event) return fail(NET_EINVAL); - if (op != 2 && g_socks.count(id) == 0) return fail(NET_EBADF); + 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); auto& registrations = g_epoll_socks[eid]; - if (op == 1) { + if (op == NET_EPOLL_CTL_ADD) { if (!registrations.emplace(id, *event).second) return fail(NET_EADDRINUSE); - } else if (op == 2) { + } else if (op == NET_EPOLL_CTL_DEL) { if (registrations.erase(id) == 0) return fail(NET_ENOENT); } else { auto registration = registrations.find(id); @@ -979,7 +1669,7 @@ int APS5_VABI sceNetEpollWait(int eid, NetEpollEvent* events, int maxevents, int for (const auto& entry : registration->second) { const auto socket = g_socks.find(entry.first); if (socket == g_socks.end()) continue; - registered_events.push_back(entry.second); + registered_events.push_back({entry.second.events, 0, static_cast(entry.first), entry.second.data}); native_sockets.push_back(socket->second.native); } } @@ -1000,14 +1690,14 @@ int APS5_VABI sceNetEpollWait(int eid, NetEpollEvent* events, int maxevents, int for (std::size_t index = 0; index < descriptors.size(); ++index) { descriptors[index].fd = native_sockets[index]->value; if (registered_events[index].events & 1) descriptors[index].events |= POLLRDNORM; - if (registered_events[index].events & 4) descriptors[index].events |= POLLWRNORM; + if (registered_events[index].events & 2) descriptors[index].events |= POLLWRNORM; } #else std::vector descriptors(registered_events.size()); for (std::size_t index = 0; index < descriptors.size(); ++index) { descriptors[index].fd = native_sockets[index]->value; if (registered_events[index].events & 1) descriptors[index].events |= POLLIN; - if (registered_events[index].events & 4) descriptors[index].events |= POLLOUT; + if (registered_events[index].events & 2) descriptors[index].events |= POLLOUT; } #endif const auto now = std::chrono::steady_clock::now(); @@ -1037,10 +1727,10 @@ int APS5_VABI sceNetEpollWait(int eid, NetEpollEvent* events, int maxevents, int std::uint32_t guest_events = 0; #ifdef _WIN32 if (native_events & (POLLRDNORM | POLLPRI)) guest_events |= 1; - if (native_events & POLLWRNORM) guest_events |= 4; + if (native_events & POLLWRNORM) guest_events |= 2; #else if (native_events & (POLLIN | POLLPRI)) guest_events |= 1; - if (native_events & POLLOUT) guest_events |= 4; + if (native_events & POLLOUT) guest_events |= 2; #endif if (native_events & POLLERR) guest_events |= 8; if (native_events & POLLHUP) guest_events |= 16; @@ -1069,20 +1759,26 @@ 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) { if (src == nullptr || dst == nullptr) { - *errno_slot() = NET_EINVAL; + set_errno(NET_EINVAL); return nullptr; } if (af != NET_AF_INET && af != NET_AF_INET6) { - *errno_slot() = NET_EAFNOSUPPORT; + set_errno(NET_EAFNOSUPPORT); return nullptr; } char text[INET6_ADDRSTRLEN]; @@ -1092,12 +1788,12 @@ const char* APS5_VABI sceNetInetNtop(int af, const void* src, char* dst, uint32_ const auto* result = ::inet_ntop(af == NET_AF_INET ? AF_INET : AF_INET6, src, text, sizeof(text)); #endif if (!result) { - *errno_slot() = native_error(); + set_errno(native_error()); return nullptr; } const auto length = std::strlen(text) + 1; if (length > size) { - *errno_slot() = NET_ENOSPC; + set_errno(NET_ENOSPC); return nullptr; } std::memcpy(dst, text, length); @@ -1130,7 +1826,7 @@ int APS5_VABI sceNetResolverCreate(const char* name, int memid, int flags) { (void)flags; std::lock_guard lk(g_mutex); const int id = g_next_resolver++; - g_resolvers[id] = 0; + g_resolvers[id] = {}; return id; } @@ -1139,28 +1835,42 @@ int APS5_VABI sceNetResolverDestroy(int rid) { return g_resolvers.erase(rid) != 0 ? 0 : fail(NET_EBADF); } -int lookup_ipv4(int rid, const char* hostname, const char* func, std::vector& addresses) { - { - std::lock_guard lk(g_mutex); - if (g_resolvers.count(rid) == 0) return fail(NET_EBADF); - } +int lookup_host(int rid, const char* hostname, const char* func, int family, std::vector>& addresses) { if (!initialize_sockets()) return fail(5); + std::uint64_t generation = 0; + if (const int error = begin_lookup(rid, &Resolver::abort_ntoa, generation)) return error; addrinfo hints{}; - hints.ai_family = AF_INET; + hints.ai_family = family; addrinfo* results = nullptr; const int result = ::getaddrinfo(hostname, nullptr, &hints, &results); if (result != 0) { - *errno_slot() = result == EAI_AGAIN ? NET_ETIMEDOUT : NET_ENOENT; + set_errno(result == EAI_AGAIN ? NET_ETIMEDOUT : NET_ENOENT); log_soft(func, "host DNS lookup failed"); - set_resolver_error(rid, NET_ERROR_RESOLVER_ENODNS); - return NET_ERROR_RESOLVER_ENODNS; + return end_lookup(rid, generation, NET_ERROR_RESOLVER_ENODNS); } for (const addrinfo* entry = results; entry != nullptr; entry = entry->ai_next) { - const auto address = reinterpret_cast(entry->ai_addr)->sin_addr.s_addr; + if (entry->ai_family != family) continue; + std::array address{}; + if (family == AF_INET6) std::memcpy(address.data(), &reinterpret_cast(entry->ai_addr)->sin6_addr, 16); + else std::memcpy(address.data(), &reinterpret_cast(entry->ai_addr)->sin_addr, 4); if (std::find(addresses.begin(), addresses.end(), address) == addresses.end()) addresses.push_back(address); } ::freeaddrinfo(results); - set_resolver_error(rid, 0); + if (addresses.empty()) { + set_errno(NET_ENOENT); + return end_lookup(rid, generation, NET_ERROR_RESOLVER_ENODNS); + } + return end_lookup(rid, generation, 0); +} + +int lookup_ipv4(int rid, const char* hostname, const char* func, std::vector& addresses) { + std::vector> found; + if (const int error = lookup_host(rid, hostname, func, AF_INET, found)) return error; + for (const auto& address : found) { + std::uint32_t value = 0; + std::memcpy(&value, address.data(), sizeof(value)); + addresses.push_back(value); + } return 0; } @@ -1175,6 +1885,17 @@ int APS5_VABI sceNetResolverStartNtoa(int rid, const char* hostname, void* addr, return 0; } +int APS5_VABI sceNetResolverStartNtoa6(int rid, const char* hostname, void* addr, int timeout, int retry, int flags) { + (void)timeout; + (void)retry; + if (flags != 0) throw std::runtime_error("sceNetResolverStartNtoa6: flags " + std::to_string(flags) + " are not supported"); + if (!hostname || !addr) return fail(NET_EINVAL); + std::vector> addresses; + if (const int error = lookup_host(rid, hostname, __func__, AF_INET6, addresses)) return error; + std::memcpy(addr, addresses.front().data(), addresses.front().size()); + return 0; +} + struct NetResolverRecord { std::uint32_t address; std::uint8_t address6[12]; @@ -1191,14 +1912,11 @@ struct NetResolverInfo { static_assert(sizeof(NetResolverRecord) == 32 && offsetof(NetResolverRecord, family) == 16); static_assert(sizeof(NetResolverInfo) == 384 && offsetof(NetResolverInfo, count) == 320 && offsetof(NetResolverInfo, count4) == 324); -int APS5_VABI sceNetResolverStartNtoaMultipleRecordsEx(int rid, const char* hostname, NetResolverInfo* info, int timeout, - int retry, int flags) { - (void)timeout; - (void)retry; - if (flags != 0) throw std::runtime_error("sceNetResolverStartNtoaMultipleRecordsEx: flags " + std::to_string(flags) + " are not supported"); +int lookup_records(int rid, const char* hostname, NetResolverInfo* info, int flags, const char* func) { + if (flags != 0) throw std::runtime_error(std::string(func) + ": flags " + std::to_string(flags) + " are not supported"); if (!hostname || !info) return fail(NET_EINVAL); std::vector addresses; - if (const int error = lookup_ipv4(rid, hostname, __func__, addresses)) return error; + if (const int error = lookup_ipv4(rid, hostname, func, addresses)) return error; *info = {}; info->count = static_cast(std::min(addresses.size(), std::size(info->records))); info->count4 = info->count; @@ -1209,29 +1927,56 @@ int APS5_VABI sceNetResolverStartNtoaMultipleRecordsEx(int rid, const char* host return 0; } +int APS5_VABI sceNetResolverStartNtoaMultipleRecords(int rid, const char* hostname, NetResolverInfo* info, int timeout, + int retry, int flags) { + (void)timeout; + (void)retry; + return lookup_records(rid, hostname, info, flags, __func__); +} + +int APS5_VABI sceNetResolverStartNtoaMultipleRecordsEx(int rid, const char* hostname, NetResolverInfo* info, int timeout, + int retry, int flags) { + (void)timeout; + (void)retry; + return lookup_records(rid, hostname, info, flags, __func__); +} + +int 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; + std::vector name(static_cast(len)); + 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"); + return end_lookup(rid, generation, NET_ERROR_RESOLVER_ENODNS); + } + if (const int error = end_lookup(rid, generation, 0)) return error; + std::memcpy(hostname, name.data(), std::strlen(name.data()) + 1); + 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); - { - std::lock_guard lk(g_mutex); - if (g_resolvers.count(rid) == 0) return fail(NET_EBADF); - } - if (!initialize_sockets()) return fail(5); sockaddr_in address{}; address.sin_family = AF_INET; std::memcpy(&address.sin_addr, addr, sizeof(address.sin_addr)); - const int result = ::getnameinfo(reinterpret_cast(&address), sizeof(address), hostname, - static_cast(len), nullptr, 0, NI_NAMEREQD); - if (result != 0) { - *errno_slot() = result == EAI_AGAIN ? NET_ETIMEDOUT : NET_ENOENT; - log_soft(__func__, "host reverse DNS lookup failed"); - set_resolver_error(rid, NET_ERROR_RESOLVER_ENODNS); - return NET_ERROR_RESOLVER_ENODNS; - } - set_resolver_error(rid, 0); - return 0; + 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) { @@ -1239,21 +1984,915 @@ int APS5_VABI sceNetResolverGetError(int rid, int* status) { std::lock_guard lk(g_mutex); const auto resolver = g_resolvers.find(rid); if (resolver == g_resolvers.end()) return fail(NET_EBADF); - *status = resolver->second; + *status = resolver->second.error; return 0; } -int APS5_VABI sceNetResolverAbort(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNetResolverAbort(int rid, int flags) { + constexpr int preservation = NET_RESOLVER_ABORT_FLAG_NTOA_PRESERVATION | NET_RESOLVER_ABORT_FLAG_ATON_PRESERVATION; + std::lock_guard lk(g_mutex); + const auto resolver = g_resolvers.find(rid); + if (resolver == g_resolvers.end()) return fail(NET_EBADF); + if ((flags & ~preservation) != 0) throw std::runtime_error("sceNetResolverAbort: flags " + std::to_string(flags) + " are not supported"); + if (resolver->second.running != 0) { + ++resolver->second.aborts; + resolver->second.error = NET_ERROR_BASE | NET_EINTR; + return 0; + } + if ((flags & NET_RESOLVER_ABORT_FLAG_NTOA_PRESERVATION) != 0) resolver->second.abort_ntoa = true; + if ((flags & NET_RESOLVER_ABORT_FLAG_ATON_PRESERVATION) != 0) resolver->second.abort_aton = true; return 0; } -int APS5_VABI sceNetResolverStartNtoaMultipleRecords() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNetShowIfconfig() { return 0; } 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) { + if (flags != 0) throw std::runtime_error("sceNetClearDnsCache: flags " + std::to_string(flags) + " are not supported"); + return 0; +} + +int APS5_VABI sceNetGetStatisticsInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetGetInterfaceStats(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetSetDnsInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetShowIfconfigWithMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetShowPolicy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetShowPolicyWithMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetShowRoute(void) { + return 0; +} + +int APS5_VABI sceNetShowRouteWithMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetResolverConnectAbort(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetResolverConnectCreate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetResolverConnect(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetResolverConnectDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetGetSockInfo6(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetSetDns6Info(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetShowRoute6(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetShowRoute6WithMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetAllocateAllRouteInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetAllocateAllRouteInfo6(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetControl(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetDbgInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetEventCallbackCreate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetEventCallbackDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetEventCallbackGetError(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetEventCallbackWaitCB(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetFreeAllRouteInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetGetArpInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetGetAppMemInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetGetDnsInfo(std::uint32_t* info, int 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(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetGetIfListEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetGetIfListOnce(void) { + 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(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetGetNameToIndex(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetGetRandom(std::uint32_t* output) { + 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(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetGetsockoptEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void APS5_VABI sceNetGetSystemTime(std::uint64_t* output) { + if (output != nullptr) *output = sceKernelGetProcessTime(); +} + +int APS5_VABI sceNetGetSystemMemInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetInitParam(void) { + 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(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void* APS5_VABI sceNetMemoryAllocate(std::int64_t size, int flags) { + constexpr int zeroFill = 2; + if ((flags & ~zeroFill) != 0) + throw std::runtime_error("sceNetMemoryAllocate: flags " + std::to_string(flags) + " are not supported"); + if (size <= 0) return nullptr; + void* memory = nullptr; + try { + memory = GuestHeap::GuestHeapAllocate_nid_postfix(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(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetSetsockoptEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetSocketInternal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetSyncCreate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetSyncDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetSyncGet(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetSyncSignal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetSyncWait(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetSysctl(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetThreadCreate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetThreadExit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetThreadJoin(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetUsleep(int microseconds) { + if (microseconds < 0) return fail(NET_EINVAL); + return sceKernelUsleep_nid_postfix(static_cast(microseconds)); +} + +int APS5_VABI sceNetShowIfconfigForBuffer(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetShowRouteForBuffer(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetShowNetstatForBuffer(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetShowNetstatEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetShowNetstatExForBuffer(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetInetNtopWithScopeId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetInetPtonWithScopeId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetGetDns6Info(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetSetDns6InfoToKernel(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetShowRoute6ForBuffer(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigGetIfFlags(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigSetIfFlags(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigUnsetIfFlags(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigDelIfaddr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigDownInterface(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigSetDefaultRoute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigSetDefaultRouteDev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigSetIfaddr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigSetIfLinkLocalAddr6(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigUpInterface(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigUpInterfaceWithFlags(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigCleanUpAllInterfaces(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigFlushRoute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigFlushRoute6(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigAddIfaddr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigGetIfaddr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigSetIfmtu(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigAddArp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigAddArpWithInterface(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigAddMRoute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigAddRoute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigAddRouteWithInterface(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigDelArp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigDelArpWithInterface(void) { + 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(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigDelRoute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigGetDefaultRoute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetDhcpGetAutoipInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetDhcpGetInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetDhcpGetInfoEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetDhcpStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetDhcpStop(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigAddIfaddr6(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigDelIfaddr6(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigDelDefaultRoute6(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigSetIfaddr6(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigSetIfaddr6WithFlags(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigSetDefaultRoute6(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigGetIfaddr6(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigGetDefaultRoute6(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigDelRoute6(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigAddRoute6(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetAddrConfig6Start(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetAddrConfig6Stop(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetAddrConfig6GetInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetInfoDumpStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetInfoDumpStop(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetDhcpdStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetDhcpdStop(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetDuplicateIpStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetDuplicateIpStop(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanAdhocClearWakeOnWlan(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanAdhocCreate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanAdhocGetWakeOnWlanInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanAdhocJoin(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanAdhocLeave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanAdhocPspEmuClearWakeOnWlan(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanAdhocPspEmuGetWakeOnWlanInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanAdhocPspEmuSetWakeOnWlan(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanAdhocScanJoin(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanAdhocSetExtInfoElement(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanAdhocSetWakeOnWlan(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanApStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanApStop(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanBackgroundScanQuery(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanBackgroundScanStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanBackgroundScanStop(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanDiagGetDeviceInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanDiagSetAntenna(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanDiagSetTxFixedRate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanGetDeviceConfig(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanInfraGetRssiInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanInfraLeave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanInfraScanJoin(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanScan(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanSetDeviceConfig(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigEtherGetLinkMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigEtherPostPlugInOutEvent(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigEtherSetLinkMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetPppoeStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetPppoeStop(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBandwidthControlSetDefaultParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBandwidthControlGetDefaultParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBandwidthControlGetDataTraffic(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigSetDefaultScope(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigRoutingStart(void) { + 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(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetGetStatisticsInfoInternal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetDumpAbort(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetDumpCreate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetDumpSetFilterParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetDumpDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetDumpRead(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetEmulationDebugSettingsSet(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetEmulationDebugSettingsGet(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetEmulationSet(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetEmulationGet(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetEtherStrton(const char* str, void* address) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } 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 4bae507609..c8e3080177 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -4,30 +4,113 @@ #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 "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include +#include #include -#ifdef _WIN32 +#include +#include +#include #include -#endif // Network-control state reflects host interface addressing, not PSN sign-in status. 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; +struct CallbackSlot { + NetCtlCallback func = nullptr; + void* arg = nullptr; + int lastEvent = 0; +}; + static std::mutex g_callbackLock; -static NetCtlCallback g_callbacks[MAX_CALLBACKS] = {}; +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 (!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].func) { + callbacks[index] = {func, arg, 0}; + *cid = index; + return 0; + } + } + return SCE_NET_CTL_ERROR_CALLBACK_MAX; +} + +static int UnregisterCallback(CallbackSlot (&callbacks)[MAX_CALLBACKS], int cid) { + if (cid < 0 || cid >= MAX_CALLBACKS) return SCE_NET_CTL_ERROR_INVALID_ID; + std::lock_guard lock(g_callbackLock); + callbacks[cid] = {}; + return 0; +} + +static bool HasIpv4Callback() { + std::lock_guard lock(g_callbackLock); + for (const auto& slot : g_callbacks) { + if (slot.func) return true; + } + return false; +} static bool HostHasAddress(int family) { #ifdef _WIN32 @@ -78,20 +161,312 @@ 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; +}; + +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 FindHostIpv4Interface(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 +} + +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; + if (!(file >> value)) throw std::runtime_error(std::string("sceNetCtlGetIfStat: no byte counter in ") + path); + return value; +} + +static void HostTraffic(std::uint64_t& sent, std::uint64_t& received) { + sent = 0; + received = 0; +#ifdef _WIN32 + MIB_IF_TABLE2* table = nullptr; + if (GetIfTable2(&table) != NO_ERROR) throw std::runtime_error("sceNetCtlGetIfStat: the host interface table is unavailable"); + for (ULONG index = 0; index < table->NumEntries; ++index) { + const auto& row = table->Table[index]; + if (row.OperStatus != IfOperStatusUp || row.Type == IF_TYPE_SOFTWARE_LOOPBACK || !row.InterfaceAndOperStatusFlags.HardwareInterface) continue; + sent += row.OutOctets; + received += row.InOctets; + } + FreeMibTable(table); +#elif defined(__linux__) + ifaddrs* interfaces = nullptr; + if (getifaddrs(&interfaces) != 0) throw std::runtime_error("sceNetCtlGetIfStat: the host interface list is unavailable"); + for (auto* item = interfaces; item; item = item->ifa_next) { + if (!item->ifa_addr || item->ifa_addr->sa_family != AF_PACKET) continue; + if (!(item->ifa_flags & IFF_UP) || (item->ifa_flags & IFF_LOOPBACK)) continue; + const std::string statistics = std::string("/sys/class/net/") + item->ifa_name + "/statistics/"; + sent += NetCtlReadByteCounter_nid_no_patch((statistics + "tx_bytes").c_str()); + received += NetCtlReadByteCounter_nid_no_patch((statistics + "rx_bytes").c_str()); + } + freeifaddrs(interfaces); +#else + throw std::runtime_error("sceNetCtlGetIfStat: host interface byte counters are not implemented on this platform"); +#endif +} + extern "C" { int APS5_VABI sceNetCtlCheckCallback(void) { + 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 (!stat) return SCE_NET_CTL_ERROR_INVALID_ADDR; + if (!HostHasAddress(AF_UNSPEC)) return SCE_NET_CTL_ERROR_NOT_CONNECTED; + *stat = {}; + HostTraffic(stat->tx_bytes, stat->rx_bytes); return 0; } int APS5_VABI sceNetCtlGetInfo(int code, NetCtlInfo* info) { - (void)code; - (void)info; - return SCE_NET_CTL_ERROR_NOT_CONNECTED; + 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* nat_info) { - (void)nat_info; + if (!nat_info) return SCE_NET_CTL_ERROR_INVALID_ADDR; return SCE_NET_CTL_ERROR_NOT_CONNECTED; } @@ -119,25 +494,379 @@ int APS5_VABI sceNetCtlInit(void) { } int APS5_VABI sceNetCtlRegisterCallback(NetCtlCallback func, void* arg, int* cid) { - if (!func || !cid) return SCE_NET_CTL_ERROR_INVALID_ADDR; - std::lock_guard lock(g_callbackLock); - for (int index = 0; index < MAX_CALLBACKS; ++index) { - if (!g_callbacks[index]) { - g_callbacks[index] = func; - *cid = index; - return 0; - } - } - return SCE_NET_CTL_ERROR_CALLBACK_MAX; + return RegisterCallback(g_callbacks, func, arg, cid); +} + +int APS5_VABI sceNetCtlRegisterCallbackV6(NetCtlCallback func, void* arg, int* cid) { + return RegisterCallback(g_callbacksV6, func, arg, cid); +} + +int APS5_VABI sceNetCtlGetInfoV6(int code, void* 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) { + std::lock_guard lock(g_callbackLock); + for (auto& slot : g_callbacks) slot = {}; + for (auto& slot : g_callbacksV6) slot = {}; } int APS5_VABI sceNetCtlUnregisterCallback(int cid) { - if (cid < 0 || cid >= MAX_CALLBACKS) return SCE_NET_CTL_ERROR_INVALID_ID; - std::lock_guard lock(g_callbackLock); - g_callbacks[cid] = nullptr; + return UnregisterCallback(g_callbacks, cid); +} + +int APS5_VABI sceNetCtlUnregisterCallbackV6(int cid) { + return UnregisterCallback(g_callbacksV6, cid); +} + +int APS5_VABI sceNetCtlRegisterCallbackForNpToolkit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlRegisterCallbackIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlRegisterCallbackForLibIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlUnregisterCallbackForNpToolkit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlUnregisterCallbackIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlUnregisterCallbackForLibIpcInt(void) { + 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(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlClearEventForNpToolkit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlClearEventForLibIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetResultIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetStateIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetState2IpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetInfoIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetEtherLinkMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetWifiType(void) { + 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(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlDisconnectIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlScanIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetScanInfoBssidIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetScanInfoByBssidIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetScanInfoForSsidScanIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetScanInfoBssidForSsidListScanIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetScanInfoForSsidListScanIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlSetErrorNotificationEnabledIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlRegisterCallbackV6IpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlUnregisterCallbackV6IpcInt(void) { + 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(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetStateV6IpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetInfoV6IpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetNatInfoIpcInt(void) { + 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(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetNetEvConfigInfoIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetConnectionElapsedTimeIpcInt(void) { + 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(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApAppStartWithRetry(void) { + 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(void) { + 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(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBweRegisterCallbackIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBweUnregisterCallbackIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBweGetInfoIpcInt(void) { + 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(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetWifiInfoIpcInt(void) { + 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..d0bcb19845 --- /dev/null +++ b/core/libs/prx/libSceNetCtlAp/Export.cpp @@ -0,0 +1,130 @@ +#include +#include +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +namespace { + +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 (!ValidEventType(eventType)) return NET_CTL_AP_ERROR_INVALID_TYPE; + if (!errorCode) return NET_CTL_AP_ERROR_INVALID_ADDR; + *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(std::uint8_t* optStr, void* connectInfo) { + (void)optStr; + std::lock_guard lock(g_lock); + if (!g_initialized) return NET_CTL_AP_ERROR_NOT_INITIALIZED; + 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 new file mode 100644 index 0000000000..e1d2bc4a27 --- /dev/null +++ b/core/libs/prx/libSceNetCtlApDialog/Export.cpp @@ -0,0 +1,78 @@ +#include +#include +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +namespace { + +constexpr int COMMON_DIALOG_STATUS_NONE = 0; +constexpr int COMMON_DIALOG_STATUS_INITIALIZED = 1; +constexpr int COMMON_DIALOG_STATUS_RUNNING = 2; +constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; +constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); +constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004u); +constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); +constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast(0x80B80007u); +constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); +constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; + +struct SceNetCtlApDialogResult { + std::int32_t result = COMMON_DIALOG_RESULT_USER_CANCELED; + std::uint8_t reserved[36] = {}; +}; + +static_assert(sizeof(SceNetCtlApDialogResult) == 40); + +std::atomic g_status{COMMON_DIALOG_STATUS_NONE}; + +} + +extern "C" { + +int APS5_VABI sceNetCtlApDialogInitialize(void) { + int expected = COMMON_DIALOG_STATUS_NONE; + if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_INITIALIZED)) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; + return 0; +} + +int APS5_VABI sceNetCtlApDialogTerminate(void) { + if (g_status.exchange(COMMON_DIALOG_STATUS_NONE) == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + return 0; +} + +int APS5_VABI sceNetCtlApDialogOpen(const void* param) { + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (status == COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_BUSY; + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + g_status = COMMON_DIALOG_STATUS_FINISHED; + return 0; +} + +int APS5_VABI sceNetCtlApDialogClose(void) { + if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + g_status = COMMON_DIALOG_STATUS_FINISHED; + return 0; +} + +int APS5_VABI sceNetCtlApDialogUpdateStatus(void) { + return g_status.load(); +} + +int APS5_VABI sceNetCtlApDialogGetStatus(void) { + return g_status.load(); +} + +int APS5_VABI sceNetCtlApDialogGetResult(void* result) { + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + const SceNetCtlApDialogResult dialogResult{}; + std::memcpy(result, &dialogResult, sizeof(dialogResult)); + return 0; +} + +} diff --git a/core/libs/prx/libSceNgs2.native/CMakeLists.txt b/core/libs/prx/libSceNgs2.native/CMakeLists.txt index 8d100ce358..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) + 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 bc9254bb0f..b89226f992 100644 --- a/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake +++ b/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake @@ -3,11 +3,12 @@ function(add_sce_ngs2_library target) add_library(${target} SHARED EXCLUDE_FROM_ALL ${ngs2Dir}/src/Atrac9.cpp ${ngs2Dir}/src/Custom.cpp + ${ngs2Dir}/src/Geom.cpp + ${ngs2Dir}/src/Pan.cpp ${ngs2Dir}/src/Rack.cpp ${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 3c0d9ea6ba..a604adba3d 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -9,16 +9,20 @@ 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_VOICE_HANDLE = static_cast(0x804A8203); +static constexpr int SCE_NGS2_ERROR_NULL_PARAM_LIST = static_cast(0x804A8360); static constexpr std::uint32_t SCE_NGS2_RACK_ID_SAMPLER = 0x1000; static constexpr std::uint32_t SCE_NGS2_RACK_ID_SUBMIXER = 0x2000; static constexpr std::uint32_t SCE_NGS2_RACK_ID_REVERB = 0x2001; static constexpr std::uint32_t SCE_NGS2_RACK_ID_MASTERING = 0x3000; +static constexpr std::uint32_t SCE_NGS2_RACK_ID_CUSTOM_SAMPLER = 0x4001; static constexpr std::uint32_t SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER = 0x4002; static constexpr std::uint32_t SCE_NGS2_WAVEFORM_TYPE_PCM_I16L = 0x12; @@ -36,6 +40,7 @@ static constexpr std::uint32_t SCE_NGS2_VOICE_STATE_FLAG_INUSE = 1; static constexpr std::uint32_t SCE_NGS2_VOICE_STATE_FLAG_PLAYING = 2; static constexpr std::uint32_t SCE_NGS2_VOICE_STATE_FLAG_PAUSED = 4; static constexpr std::uint32_t SCE_NGS2_VOICE_STATE_FLAG_STOPPED = 8; +static constexpr std::uint32_t SCE_NGS2_VOICE_STATE_FLAG_EMPTY = 0x20; static constexpr std::uint32_t SCE_NGS2_VOICE_PARAM_MATRIX_LEVELS = 0x0001; static constexpr std::uint32_t SCE_NGS2_VOICE_PARAM_PORT_VOLUME = 0x0002; @@ -56,16 +61,25 @@ static constexpr std::uint32_t SCE_NGS2_REVERB_VOICE_PARAM_I3DL2 = 0x20010001; static constexpr std::uint32_t SCE_NGS2_MASTERING_VOICE_PARAM_SETUP = 0x30000000; static constexpr std::uint32_t SCE_NGS2_MASTERING_VOICE_PARAM_GAIN = 0x30000004; static constexpr std::uint32_t SCE_NGS2_MASTERING_VOICE_PARAM_OUTPUT = 0x30000005; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_SETUP = 0x40010000; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS = 0x40010001; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_EXIT_LOOP = 0x40010004; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_PITCH = 0x40010005; static constexpr std::uint32_t SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP = 0x40020000; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_FILTER = 0x40001300; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_USER_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_MODULE_ID_FILTER = 0x13; +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_WAVEFORM_BLOCKS_FLAG_CONTINUE = 1; static constexpr std::uint32_t SCE_NGS2_WAVEFORM_BLOCKS_FLAG_APPEND = 2; static constexpr std::uint32_t SCE_NGS2_WAVEFORM_BLOCKS_FLAG_RESET = 4; +static constexpr std::uint32_t SCE_NGS2_WAVEFORM_BLOCKS_FLAG_SILENCE = 0x10; static constexpr std::uint32_t SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END = 1; static constexpr std::uint32_t SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_REPEAT = 2; @@ -273,6 +287,17 @@ struct Ngs2CustomRackOption { }; static_assert(sizeof(Ngs2CustomRackOption) == 1280); +struct Ngs2CustomSamplerRackOption { + Ngs2CustomRackOption custom_rack_option; + std::uint32_t max_channel_works; + std::uint32_t max_waveform_blocks; + std::uint32_t max_atrac9_decoders; + std::uint32_t max_atrac9_channel_works; + std::uint32_t max_ajm_atrac9_decoders; + std::uint32_t max_codec_caches; +}; +static_assert(sizeof(Ngs2CustomSamplerRackOption) == 1304); + struct Ngs2CustomSubmixerRackOption { Ngs2CustomRackOption custom_rack_option; std::uint32_t max_channels; @@ -482,6 +507,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; @@ -489,6 +523,33 @@ struct Ngs2CustomVoiceUserFx2Param { }; static_assert(sizeof(Ngs2CustomVoiceUserFx2Param) == 24); +struct Ngs2CustomVoiceFilterParam { + Ngs2VoiceParamHeader header; + std::uint32_t type; + std::uint32_t channel_mask; + union { + struct { + float i0; + float i1; + float i2; + float o1; + float o2; + } direct; + struct { + float frequency; + float q; + float level; + std::uint32_t reserved; + std::uint32_t reserved2; + } fcq; + } param; + std::uint32_t reserved3; +}; +static_assert(sizeof(Ngs2CustomVoiceFilterParam) == 40); +static_assert(offsetof(Ngs2CustomVoiceFilterParam, type) == 8); +static_assert(offsetof(Ngs2CustomVoiceFilterParam, channel_mask) == 12); +static_assert(offsetof(Ngs2CustomVoiceFilterParam, param) == 16); + struct Ngs2MasteringVoiceSetupParam { Ngs2VoiceParamHeader header; std::uint32_t num_io_channels; @@ -572,6 +633,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; @@ -591,27 +661,103 @@ struct Ngs2WaveformInfo { static_assert(sizeof(Ngs2WaveformInfo) == 232); struct Ngs2PanParam { - std::uint32_t reserved[16]; + float angle; + float distance; + float fbw_level; + float lfe_level; }; +static_assert(sizeof(Ngs2PanParam) == 16); struct Ngs2PanWork { - std::uint32_t reserved[64]; + float speaker_angles[8]; + float unit_angle; + std::uint32_t num_speakers; }; +static_assert(sizeof(Ngs2PanWork) == 40); + +struct Ngs2GeomVector { + float x; + float y; + float z; +}; +static_assert(sizeof(Ngs2GeomVector) == 12); + +struct Ngs2GeomCone { + float inner_level; + float inner_angle; + float outer_level; + float outer_angle; +}; +static_assert(sizeof(Ngs2GeomCone) == 16); + +struct Ngs2GeomRolloff { + std::uint32_t model; + float max_distance; + float rolloff_factor; + float reference_distance; +}; +static_assert(sizeof(Ngs2GeomRolloff) == 16); struct Ngs2GeomListenerParam { - std::uint32_t reserved[32]; + Ngs2GeomVector position; + Ngs2GeomVector orient_front; + Ngs2GeomVector orient_up; + Ngs2GeomVector velocity; + float sound_speed; + std::uint32_t reserved[2]; }; +static_assert(sizeof(Ngs2GeomListenerParam) == 60); struct Ngs2GeomListenerWork { - std::uint32_t reserved[64]; + float matrix[4][4]; + Ngs2GeomVector velocity; + float sound_speed; + std::uint32_t coordinate; + std::uint32_t reserved[3]; }; +static_assert(sizeof(Ngs2GeomListenerWork) == 96); struct Ngs2GeomSourceParam { - std::uint32_t reserved[32]; + Ngs2GeomVector position; + Ngs2GeomVector velocity; + Ngs2GeomVector direction; + Ngs2GeomCone cone; + Ngs2GeomRolloff rolloff; + float doppler_factor; + float fbw_level; + float lfe_level; + float max_level; + float min_level; + float radius; + std::uint32_t num_speakers; + std::uint32_t matrix_format; + std::uint32_t reserved[2]; }; +static_assert(sizeof(Ngs2GeomSourceParam) == 108); + +struct Ngs2GeomA3dAttribute { + Ngs2GeomVector position; + float volume; + std::uint32_t reserved[4]; +}; +static_assert(sizeof(Ngs2GeomA3dAttribute) == 32); struct Ngs2GeomAttribute { - std::uint32_t reserved[32]; + float pitch_ratio; + float level[64]; + Ngs2GeomA3dAttribute a3d_attrib; + std::uint32_t reserved[4]; }; +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 240c0a7a95..b508ac56a3 100644 --- a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp @@ -217,14 +217,16 @@ struct RiffChunks { std::size_t dataSize = 0; }; -static int ReadChunks(const std::uint8_t* bytes, std::size_t size, RiffChunks& chunks) { +template +static int ReadChunks(std::size_t size, TRead&& read, RiffChunks& chunks) { if (size < 12) return SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA; - if (std::memcmp(bytes, "RIFF", 4) != 0 || std::memcmp(bytes + 8, "WAVE", 4) != 0) return SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT; - const std::size_t riffEnd = 8 + static_cast(ReadLe32(bytes + 4)); + const auto* header = read(0, 12); + if (std::memcmp(header, "RIFF", 4) != 0 || std::memcmp(header + 8, "WAVE", 4) != 0) return SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT; + const std::size_t riffEnd = 8 + static_cast(ReadLe32(header + 4)); if (riffEnd < 12) return SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA; const std::size_t end = std::min(size, riffEnd); for (std::size_t offset = 12; offset + 8 <= end;) { - const auto* chunk = bytes + offset; + const auto* chunk = read(offset, 8); const std::size_t payload = offset + 8; const std::size_t chunkSize = ReadLe32(chunk + 4); if (chunkSize > riffEnd - payload) return SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA; @@ -236,13 +238,13 @@ static int ReadChunks(const std::uint8_t* bytes, std::size_t size, RiffChunks& c } if (chunkSize > end - payload) return SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA; if (std::memcmp(chunk, "fmt ", 4) == 0) { - chunks.format = bytes + payload; + chunks.format = read(payload, chunkSize); chunks.formatSize = chunkSize; } else if (std::memcmp(chunk, "fact", 4) == 0) { - chunks.fact = bytes + payload; + chunks.fact = read(payload, chunkSize); chunks.factSize = chunkSize; } else if (std::memcmp(chunk, "smpl", 4) == 0) { - chunks.sampler = bytes + payload; + chunks.sampler = read(payload, chunkSize); chunks.samplerSize = chunkSize; } offset = payload + chunkSize + (chunkSize & 1); @@ -349,6 +351,17 @@ static int ParsePcm16(const RiffChunks& chunks, std::size_t size, Ngs2WaveformIn return SCE_NGS2_OK; } +static int ParseWaveform(const RiffChunks& chunks, std::size_t size, Ngs2WaveformInfo& info) { + if (chunks.formatSize < 2) return SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT; + const std::uint16_t tag = ReadLe16(chunks.format); + if (tag == WAVE_FORMAT_EXTENSIBLE && chunks.formatSize >= FMT_GUID_OFFSET + sizeof(ATRAC9_GUID) && + std::memcmp(chunks.format + FMT_GUID_OFFSET, ATRAC9_GUID, sizeof(ATRAC9_GUID)) == 0) { + return ParseAtrac9(chunks, info); + } + if (tag == WAVE_FORMAT_PCM) return ParsePcm16(chunks, size, info); + throw std::runtime_error("NGS2: parsing waveform format tag " + Ngs2Hex(tag) + " is not implemented"); +} + #pragma GCC visibility push(default) extern "C" { @@ -357,17 +370,11 @@ int APS5_VABI sceNgs2ParseWaveformData(const void* data, size_t data_size, Ngs2W if (info == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; *info = {}; if (data == nullptr) return SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA; + const auto* bytes = static_cast(data); RiffChunks chunks; - const int result = ReadChunks(static_cast(data), data_size, chunks); + const int result = ReadChunks(data_size, [bytes](std::size_t at, std::size_t) { return bytes + at; }, chunks); if (result != SCE_NGS2_OK) return result; - if (chunks.formatSize < 2) return SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT; - const std::uint16_t tag = ReadLe16(chunks.format); - if (tag == WAVE_FORMAT_EXTENSIBLE && chunks.formatSize >= FMT_GUID_OFFSET + sizeof(ATRAC9_GUID) && - std::memcmp(chunks.format + FMT_GUID_OFFSET, ATRAC9_GUID, sizeof(ATRAC9_GUID)) == 0) { - return ParseAtrac9(chunks, *info); - } - if (tag == WAVE_FORMAT_PCM) return ParsePcm16(chunks, data_size, *info); - throw std::runtime_error("NGS2: parsing waveform format tag " + Ngs2Hex(tag) + " is not implemented"); + return ParseWaveform(chunks, data_size, *info); } int APS5_VABI sceNgs2ParseWaveformFile(const char* path, uint32_t offset, Ngs2WaveformInfo* info) { @@ -379,11 +386,18 @@ int APS5_VABI sceNgs2ParseWaveformFile(const char* path, uint32_t offset, Ngs2Wa if (!file) throw std::runtime_error(std::string(__func__) + ": cannot open " + host.string()); const auto fileSize = static_cast(file.tellg()); if (offset > fileSize) return SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA; - const std::uint64_t length = fileSize - offset; - std::vector bytes(static_cast(length)); - file.seekg(offset); - if (!file.read(reinterpret_cast(bytes.data()), static_cast(bytes.size()))) throw std::runtime_error(std::string(__func__) + ": read failed for " + host.string()); - const int result = sceNgs2ParseWaveformData(bytes.data(), bytes.size(), info); + const auto length = static_cast(fileSize - offset); + const std::string function = __func__; + std::vector> pieces; + const auto readBytes = [&](std::size_t at, std::size_t count) { + auto& piece = pieces.emplace_back(std::max(count, 1)); + file.seekg(static_cast(offset + at)); + if (!file.read(reinterpret_cast(piece.data()), static_cast(count))) throw std::runtime_error(function + ": read failed for " + host.string()); + return static_cast(piece.data()); + }; + RiffChunks chunks; + int result = ReadChunks(length, readBytes, chunks); + if (result == SCE_NGS2_OK) result = ParseWaveform(chunks, length, *info); if (result != SCE_NGS2_OK) return result; info->data_offset += offset; for (std::uint32_t i = 0; i < info->num_blocks; i++) info->block[i].data_offset += offset; diff --git a/core/libs/prx/libSceNgs2.native/src/Custom.cpp b/core/libs/prx/libSceNgs2.native/src/Custom.cpp index c77dc52054..d3e586a4d9 100644 --- a/core/libs/prx/libSceNgs2.native/src/Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Custom.cpp @@ -17,10 +17,19 @@ void Ngs2CheckCustomRack(const Ngs2CustomRackOption& option) { if (option.num_modules > SCE_NGS2_CUSTOM_MAX_MODULES || option.rack_option.max_ports > SCE_NGS2_CUSTOM_MAX_PORTS) APS5_INVALID_ARG_EX; for (std::uint32_t m = 0; m < option.num_modules; m++) { const auto& info = option.module[m]; - if (info.module_id != SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2) throw std::runtime_error("NGS2: custom module id " + Ngs2Hex(info.module_id) + " is not implemented"); - if (info.option == nullptr || info.option->size != sizeof(Ngs2CustomUserFx2ModuleOption)) APS5_INVALID_ARG_EX; - if (info.source_buffer_id != 0 || info.dest_buffer_id != 0) APS5_INVALID_ARG_EX; - if (reinterpret_cast(info.option)->process_handler == nullptr) APS5_INVALID_ARG_EX; + if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2) { + if (info.option == nullptr || info.option->size != sizeof(Ngs2CustomUserFx2ModuleOption)) APS5_INVALID_ARG_EX; + if (info.source_buffer_id != 0 || info.dest_buffer_id != 0) APS5_INVALID_ARG_EX; + if (reinterpret_cast(info.option)->process_handler == nullptr) APS5_INVALID_ARG_EX; + } else if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_FILTER) { + if (info.option != nullptr || info.state_size != 0 || info.state_offset != 0) APS5_INVALID_ARG_EX; + if (info.source_buffer_id != 0 || info.extra_buffer_id != 0 || info.dest_buffer_id != 0) APS5_INVALID_ARG_EX; + } else if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX) { + if (info.source_buffer_id != 0 || info.dest_buffer_id != 0) APS5_INVALID_ARG_EX; + if (info.option != nullptr && info.option->size < sizeof(Ngs2CustomModuleOption)) APS5_INVALID_ARG_EX; + } else { + throw std::runtime_error("NGS2: custom module id " + Ngs2Hex(info.module_id) + " is not implemented"); + } } for (std::uint32_t p = 0; p < option.rack_option.max_ports; p++) { if (option.port[p].source_buffer_id != 0) APS5_INVALID_ARG_EX; @@ -34,23 +43,35 @@ static Ngs2UserFx2SetupContext UserFxContext(Ngs2Rack& rack, std::size_t module, } void Ngs2SetupUserFx(Ngs2Rack& rack, const Ngs2CustomRackOption& option) { + rack.customModuleIds.resize(option.num_modules); rack.userFx.resize(option.num_modules); rack.userFxCommon.resize(option.num_modules); - for (auto& voice : rack.voices) voice.userFx.resize(option.num_modules); + for (auto& voice : rack.voices) { + voice.userFx.resize(option.num_modules); + voice.customUserFx.resize(option.num_modules); + } for (std::uint32_t m = 0; m < option.num_modules; m++) { - 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)); + rack.customModuleIds[m] = option.module[m].module_id; + if (option.module[m].module_id == SCE_NGS2_CUSTOM_MODULE_ID_FILTER) continue; + 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; + } } } } @@ -58,6 +79,7 @@ void Ngs2SetupUserFx(Ngs2Rack& rack, const Ngs2CustomRackOption& option) { void Ngs2CleanupUserFx(Ngs2Rack& rack) { for (std::uint32_t i = 0; i < rack.voices.size(); i++) { for (std::size_t m = 0; m < rack.userFx.size(); m++) { + if (rack.customModuleIds[m] != SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2) continue; if (rack.userFx[m].cleanup_handler == nullptr) continue; auto context = UserFxContext(rack, m, i); const int result = rack.userFx[m].cleanup_handler(&context); @@ -83,31 +105,91 @@ 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; +} + void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { - if (voice.rack->rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); + const auto rackId = voice.rack->rackId; if (param.id == SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP) { + if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); SetupCustomSubmixer(voice, ParamAs(param)); return; } + if ((param.id & ~0x1fu) == SCE_NGS2_CUSTOM_VOICE_PARAM_FILTER) { + if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SAMPLER && rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) { + throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); + } + const auto index = param.id & 0x1f; + if (index >= voice.rack->customModuleIds.size() || voice.rack->customModuleIds[index] != SCE_NGS2_CUSTOM_MODULE_ID_FILTER) APS5_INVALID_ARG_EX; + const auto& custom = ParamAs(param); + Ngs2SamplerVoiceFilterParam filter{}; + filter.header = custom.header; + filter.index = index; + filter.location = 1; + filter.type = custom.type; + filter.channel_mask = custom.channel_mask; + if (custom.type == 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; + } else { + throw std::runtime_error("NGS2: custom filter type " + std::to_string(custom.type) + " is not implemented"); + } + Ngs2SetFilter(voice, filter); + return; + } if ((param.id & ~0x1fu) == SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2) { + if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SAMPLER && rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) { + throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); + } SetUserFxParam(voice, param.id & 0x1f, ParamAs(param)); return; } + if ((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()) 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++) { - 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_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 new file mode 100644 index 0000000000..28f5d9f10c --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/src/Geom.cpp @@ -0,0 +1,119 @@ +#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 (outWork == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; + if (flags > 1) throw std::runtime_error("NGS2: unknown listener coordinate flags"); + if (!std::isfinite(param->sound_speed) || param->sound_speed <= 0.0f) APS5_INVALID_ARG_EX; + const Vector position = ReadVector(param->position); + ReadVector(param->velocity); + const Vector front = Unit(ReadVector(param->orient_front)); + Vector up = Unit(ReadVector(param->orient_up)); + Vector right = 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 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; +} + +int Ngs2GeomResetListenerParam(Ngs2GeomListenerParam* out_listener_param) { + if (out_listener_param == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; + *out_listener_param = {}; + out_listener_param->orient_front.z = 1.0f; + out_listener_param->orient_up.y = 1.0f; + out_listener_param->sound_speed = DEFAULT_SOUND_SPEED; + return SCE_NGS2_OK; +} + +int Ngs2GeomResetSourceParam(Ngs2GeomSourceParam* out_source_param) { + if (out_source_param == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; + *out_source_param = {}; + out_source_param->direction.z = 1.0f; + out_source_param->cone.inner_level = 1.0f; + out_source_param->cone.inner_angle = 360.0f; + out_source_param->cone.outer_level = 1.0f; + out_source_param->cone.outer_angle = 360.0f; + out_source_param->rolloff.max_distance = 1000000.0f; + out_source_param->rolloff.rolloff_factor = 1.0f; + out_source_param->rolloff.reference_distance = 1.0f; + out_source_param->doppler_factor = 1.0f; + out_source_param->fbw_level = 1.0f; + out_source_param->lfe_level = 1.0f; + out_source_param->max_level = 1.0f; + out_source_param->num_speakers = 2; + 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 9b1e21e5f6..adc9f3d665 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,12 @@ struct Ngs2UserFx2 { std::uint32_t flags = 0; }; +struct Ngs2VoiceCustomUserFx { + Ngs2UserFxProcessHandler handler = nullptr; + std::array data{}; + std::uint32_t flags = 0; +}; + struct Ngs2Voice { Ngs2Rack* rack = nullptr; Ngs2PlayState state = Ngs2PlayState::Empty; @@ -128,6 +135,7 @@ struct Ngs2Voice { float fbwLevel = 1.0f; float lfeLevel = 1.0f; std::vector userFx; + std::vector customUserFx; Ngs2UserFxProcessHandler userFxHandler = nullptr; std::array userFxData{}; std::uint32_t userFxFlags = 0; @@ -151,11 +159,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 { @@ -164,6 +176,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; }; @@ -171,6 +184,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 @@ -185,6 +224,7 @@ void* Ngs2Place(const Ngs2ContextBufferInfo* bufferInfo, std::size_t size, std:: int Ngs2DestroyRack(Ngs2Rack& rack, Ngs2ContextBufferInfo* outBufferInfo); int Ngs2ReleaseBuffer(const Ngs2BufferAllocator& allocator, Ngs2ContextBufferInfo bufferInfo, Ngs2ContextBufferInfo* outBufferInfo); void Ngs2SetupAtrac9(Ngs2Voice& voice, const Ngs2WaveformFormat& format); +void Ngs2SetFilter(Ngs2Voice& voice, const Ngs2SamplerVoiceFilterParam& param); std::size_t Ngs2Atrac9BlockBytes(const Ngs2Voice& voice, const Ngs2WaveformBlock& block); void Ngs2RestartAtrac9(Ngs2Voice& voice); void Ngs2ConsumeAtrac9(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t systemRate); @@ -200,5 +240,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 new file mode 100644 index 0000000000..af88b23204 --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/src/Pan.cpp @@ -0,0 +1,135 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "prx/libc/include/General.hpp" +#include "Ngs2Internal.hpp" + +static constexpr float DEGREES = 360.0f; +static constexpr float RADIANS = 2.0f * std::numbers::pi_v; +static constexpr std::uint32_t MAX_PAN_PARAMS = 8; +static constexpr std::uint32_t LFE_CHANNEL = 3; + +struct PanLayout { + std::uint32_t inputOrder[7]; + std::uint32_t channels[7]; + float defaultDegrees[7]; + std::uint32_t mixChannels; +}; + +static constexpr PanLayout STEREO_LAYOUT{{1, 0}, {1, 0}, {90, 270}, 2}; +static constexpr PanLayout QUAD_LAYOUT{{1, 3, 2, 0}, {1, 5, 4, 0}, {40, 110, 250, 320}, 6}; +static constexpr PanLayout FIVE_LAYOUT{{2, 1, 4, 3, 0}, {2, 1, 5, 4, 0}, {0, 40, 110, 250, 320}, 6}; +static constexpr PanLayout SEVEN_LAYOUT{{2, 1, 4, 6, 5, 3, 0}, {2, 1, 5, 7, 6, 4, 0}, {0, 40, 110, 150, 210, 250, 320}, 8}; + +static const PanLayout* LayoutFor(std::uint32_t numSpeakers) { + switch (numSpeakers) { + case 2: return &STEREO_LAYOUT; + case 4: return &QUAD_LAYOUT; + case 5: return &FIVE_LAYOUT; + case 7: return &SEVEN_LAYOUT; + default: return nullptr; + } +} + +static bool IsUnitAngle(float unitAngle) { + return unitAngle == DEGREES || unitAngle == RADIANS; +} + +static float EqualPower(float position) { + return std::sin(position * (std::numbers::pi_v / 2.0f)); +} + +static void AddBetweenSpeakers(const Ngs2PanWork& work, std::uint32_t speaker, float angle, float weight, float* gains) { + const float from = work.speaker_angles[speaker - 1]; + const float to = work.speaker_angles[speaker]; + if (!(angle >= from) || !(to > angle)) return; + const float position = (angle - from) / (to - from); + gains[speaker - 1] += EqualPower(1.0f - position) * weight; + gains[speaker] += EqualPower(position) * weight; +} + +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; + const auto* layout = LayoutFor(num_speakers); + if (layout == nullptr) APS5_INVALID_ARG_EX; + *work = {}; + work->num_speakers = num_speakers; + if (speaker_angles == nullptr) { + for (std::uint32_t i = 0; i < num_speakers; i++) work->speaker_angles[i] = layout->defaultDegrees[i]; + work->speaker_angles[num_speakers] = layout->defaultDegrees[0] + DEGREES; + work->unit_angle = DEGREES; + if (unit_angle == RADIANS) { + for (std::uint32_t i = 0; i < num_speakers; i++) work->speaker_angles[i] = work->speaker_angles[i] / 180.0f * std::numbers::pi_v; + work->unit_angle = RADIANS; + } + return SCE_NGS2_OK; + } + for (std::uint32_t i = 0; i < num_speakers; i++) { + float angle = speaker_angles[layout->inputOrder[i]]; + if (!std::isfinite(angle)) APS5_INVALID_ARG_EX; + while (angle < 0.0f) { + if (angle + unit_angle == angle) throw std::runtime_error("NGS2: pan angles too large to wrap are not implemented"); + angle += unit_angle; + } + while (angle >= unit_angle) { + if (!(unit_angle < angle)) APS5_INVALID_ARG_EX; + if (angle - unit_angle == angle) throw std::runtime_error("NGS2: pan angles too large to wrap are not implemented"); + angle -= unit_angle; + } + work->speaker_angles[i] = angle; + } + work->speaker_angles[num_speakers] = work->speaker_angles[0] + unit_angle; + work->unit_angle = unit_angle; + return SCE_NGS2_OK; +} + +int Ngs2PanGetVolumeMatrix(Ngs2PanWork* work, const Ngs2PanParam* params, uint32_t num_params, uint32_t matrix_format, float* out_volume_matrix) { + if (work == nullptr || params == nullptr) APS5_INVALID_ARG_EX; + if (out_volume_matrix == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; + if (matrix_format != 1 && matrix_format != 2 && matrix_format != 6 && matrix_format != 8) APS5_INVALID_ARG_EX; + const auto* layout = LayoutFor(work->num_speakers); + if (layout == nullptr || !IsUnitAngle(work->unit_angle) || !std::isfinite(work->speaker_angles[0]) || num_params > MAX_PAN_PARAMS) APS5_INVALID_ARG_EX; + for (std::uint32_t p = 0; p < num_params; p++) { + if (!std::isfinite(params[p].angle) || std::isnan(params[p].distance)) APS5_INVALID_ARG_EX; + } + std::memset(out_volume_matrix, 0, static_cast(num_params) * matrix_format * sizeof(float)); + const std::uint32_t numSpeakers = work->num_speakers; + const float unit = work->unit_angle; + const float first = work->speaker_angles[0]; + for (std::uint32_t p = 0; p < num_params; p++) { + const auto& param = params[p]; + const float distance = std::max(-1.0f, std::min(1.0f, param.distance)); + const float towardWeight = std::sin((distance + 1.0f) * 0.5f * std::numbers::pi_v * 0.5f); + const float oppositeWeight = std::sin((1.0f - distance) * 0.5f * std::numbers::pi_v * 0.5f); + float angle = param.angle; + while (angle < first || angle >= first + unit) { + const float before = angle; + if (angle >= first + unit) angle -= unit; + if (angle < first) angle += unit; + if (angle == before) throw std::runtime_error("NGS2: pan angles too large to wrap are not implemented"); + } + float opposite = angle + unit * 0.5f; + if (!(opposite < first + unit)) opposite -= unit; + float gains[8] = {}; + for (std::uint32_t speaker = 1; speaker <= numSpeakers; speaker++) { + AddBetweenSpeakers(*work, speaker, angle, towardWeight, gains); + AddBetweenSpeakers(*work, speaker, opposite, oppositeWeight, gains); + } + float mix[8] = {}; + mix[layout->channels[0]] = gains[0] + gains[numSpeakers]; + for (std::uint32_t speaker = 1; speaker < numSpeakers; speaker++) mix[layout->channels[speaker]] = gains[speaker]; + for (std::uint32_t channel = 0; channel < 8; channel++) mix[channel] = channel == LFE_CHANNEL ? param.lfe_level : mix[channel] * param.fbw_level; + for (std::uint32_t dest = 0; dest < matrix_format; dest++) { + float& level = out_volume_matrix[static_cast(dest) * num_params + p]; + for (std::uint32_t channel = 0; channel < layout->mixChannels; channel++) level += Ngs2DefaultLevel(layout->mixChannels, channel, matrix_format, dest) * mix[channel]; + } + } + return SCE_NGS2_OK; +} diff --git a/core/libs/prx/libSceNgs2.native/src/Rack.cpp b/core/libs/prx/libSceNgs2.native/src/Rack.cpp index 1acc40d6b6..47897d3215 100644 --- a/core/libs/prx/libSceNgs2.native/src/Rack.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Rack.cpp @@ -17,6 +17,7 @@ union RackOptions { Ngs2SubmixerRackOption submixer; Ngs2MasteringRackOption mastering; Ngs2ReverbRackOption reverb; + Ngs2CustomSamplerRackOption customSampler; Ngs2CustomSubmixerRackOption customSubmixer; }; @@ -26,13 +27,16 @@ static std::size_t RackOptionSize(std::uint32_t rackId) { case SCE_NGS2_RACK_ID_SUBMIXER: return sizeof(Ngs2SubmixerRackOption); case SCE_NGS2_RACK_ID_MASTERING: return sizeof(Ngs2MasteringRackOption); case SCE_NGS2_RACK_ID_REVERB: return sizeof(Ngs2ReverbRackOption); + case SCE_NGS2_RACK_ID_CUSTOM_SAMPLER: return sizeof(Ngs2CustomSamplerRackOption); case SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER: return sizeof(Ngs2CustomSubmixerRackOption); default: throw std::runtime_error("NGS2: rack id " + Ngs2Hex(rackId) + " is not implemented"); } } static RackOptions DefaultRackOption(std::uint32_t rackId) { - if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) throw std::runtime_error("NGS2: default custom rack options are not implemented"); + if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER || rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) { + throw std::runtime_error("NGS2: default custom rack options are not implemented"); + } RackOptions options{}; auto& common = options.common; common.size = RackOptionSize(rackId); @@ -93,6 +97,7 @@ static RackOptions CheckedRackOption(std::uint32_t rackId, const Ngs2RackOption* if (options.common.max_voices == 0 || maxChannels == 0 || maxChannels > NGS2_MAX_CHANNELS) { throw std::invalid_argument("NGS2: invalid rack voice or channel count"); } + if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER) Ngs2CheckCustomRack(options.customSampler.custom_rack_option); if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) Ngs2CheckCustomRack(options.customSubmixer.custom_rack_option); if (rackId == SCE_NGS2_RACK_ID_REVERB && options.reverb.reverb_size != 1) { throw std::runtime_error("NGS2: reverb rack size " + std::to_string(options.reverb.reverb_size) + " is not implemented"); @@ -109,12 +114,15 @@ static int CreateRack(Ngs2Handle systemHandle, std::uint32_t rackId, const RackO rack->system = system; rack->rackId = rackId; rack->maxChannels = RackMaxChannels(rackId, options); - rack->maxFilters = rackId == SCE_NGS2_RACK_ID_SAMPLER ? options.sampler.max_filters : 0; + rack->maxFilters = rackId == SCE_NGS2_RACK_ID_SAMPLER ? options.sampler.max_filters + : rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER ? options.customSampler.custom_rack_option.num_modules + : rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER ? options.customSubmixer.custom_rack_option.num_modules : 0; rack->uid = nextUid++; std::memcpy(rack->name, options.common.name, sizeof(rack->name)); rack->name[sizeof(rack->name) - 1] = '\0'; rack->maxGrainSamples = options.common.max_grain_samples; if (rackId == SCE_NGS2_RACK_ID_SAMPLER) rack->maxChannelWorks = options.sampler.max_channel_works; + if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER) rack->maxChannelWorks = options.customSampler.max_channel_works; if (rackId == SCE_NGS2_RACK_ID_SUBMIXER) rack->maxInputs = options.submixer.max_inputs; if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) rack->maxInputs = options.customSubmixer.max_inputs; rack->bufferInfo = bufferInfo; @@ -125,6 +133,7 @@ static int CreateRack(Ngs2Handle systemHandle, std::uint32_t rackId, const RackO voice.ports.resize(options.common.max_ports); voice.matrices.resize(options.common.max_matrices); } + if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER) Ngs2SetupUserFx(*rack, options.customSampler.custom_rack_option); if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) Ngs2SetupUserFx(*rack, options.customSubmixer.custom_rack_option); system->racks.push_back(rack); *handle = reinterpret_cast(rack); @@ -132,9 +141,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) { @@ -164,7 +175,7 @@ int APS5_VABI sceNgs2RackCreate(uintptr_t system_handle, uint32_t rack_id, const 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()); + const Ngs2Lock lock; return CreateRack(system_handle, rack_id, options, *buffer_info, {}, handle); } @@ -172,7 +183,7 @@ int APS5_VABI sceNgs2RackCreateWithAllocator(uintptr_t system_handle, uint32_t r 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()); + const Ngs2Lock lock; if (Ngs2FindSystem(system_handle) == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; Ngs2ContextBufferInfo bufferInfo{}; bufferInfo.host_buffer_size = sizeof(Ngs2Rack); @@ -183,19 +194,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; } @@ -203,7 +220,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 = {}; @@ -228,17 +245,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 ea36ba29f8..fa764d375a 100644 --- a/core/libs/prx/libSceNgs2.native/src/Render.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Render.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -15,6 +16,7 @@ static constexpr std::uint64_t PHASE_FRACTION = 0xffffffff; static const std::uint8_t* FrameAt(const Ngs2Voice& voice, const Ngs2Block& block, std::uint32_t frame) { const std::size_t frameBytes = voice.channels * sizeof(std::int16_t); + if (block.data == nullptr) return nullptr; if (block.streaming) { for (const auto& piece : block.pieces) { if (frame >= piece.firstFrame && frame - piece.firstFrame < piece.frames) return piece.data + (frame - piece.firstFrame) * frameBytes; @@ -76,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; } @@ -93,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++) { @@ -144,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); @@ -159,12 +173,58 @@ static void RenderSampler(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t s static void RenderVoice(Ngs2Voice& voice, const std::vector& voices, std::uint32_t grain, std::uint32_t systemRate); +struct Ngs2DefaultRow { + float mono; + float stereo[2]; + float surround51[6]; + float surround71[8]; +}; + +static constexpr float HALF_POWER = std::numbers::sqrt2_v / 2.0f; +static constexpr float QUARTER_POWER = std::numbers::sqrt2_v / 4.0f; + +static constexpr Ngs2DefaultRow MONO_CENTER{1.0f, {HALF_POWER, HALF_POWER}, {0, 0, 1, 0, 0, 0}, {0, 0, 1, 0, 0, 0, 0, 0}}; +static constexpr Ngs2DefaultRow FRONT_LEFT{HALF_POWER, {1, 0}, {1, 0, 0, 0, 0, 0}, {1, 0, 0, 0, 0, 0, 0, 0}}; +static constexpr Ngs2DefaultRow FRONT_RIGHT{HALF_POWER, {0, 1}, {0, 1, 0, 0, 0, 0}, {0, 1, 0, 0, 0, 0, 0, 0}}; +static constexpr Ngs2DefaultRow LFE{0.0f, {0, 0}, {0, 0, 0, 1, 0, 0}, {0, 0, 0, 1, 0, 0, 0, 0}}; +static constexpr Ngs2DefaultRow QUAD_LEFT{0.5f, {HALF_POWER, 0}, {0, 0, 0, 0, 1, 0}, {0, 0, 0, 0, 1, 0, 0, 0}}; +static constexpr Ngs2DefaultRow QUAD_RIGHT{0.5f, {0, HALF_POWER}, {0, 0, 0, 0, 0, 1}, {0, 0, 0, 0, 0, 1, 0, 0}}; +static constexpr Ngs2DefaultRow FIVE_SURROUND_LEFT{0.5f, {0, 0}, {0, 0, 0, 0, 1, 0}, {0, 0, 0, 0, 1, 0, 0, 0}}; +static constexpr Ngs2DefaultRow FIVE_SURROUND_RIGHT{0.5f, {0, 0}, {0, 0, 0, 0, 0, 1}, {0, 0, 0, 0, 0, 1, 0, 0}}; +static constexpr Ngs2DefaultRow SURROUND_LEFT{0.5f, {0.5f, 0}, {0, 0, 0, 0, 1, 0}, {0, 0, 0, 0, 1, 0, 0, 0}}; +static constexpr Ngs2DefaultRow SURROUND_RIGHT{0.5f, {0, 0.5f}, {0, 0, 0, 0, 0, 1}, {0, 0, 0, 0, 0, 1, 0, 0}}; +static constexpr Ngs2DefaultRow BACK_CENTER{QUARTER_POWER, {QUARTER_POWER, QUARTER_POWER}, {0, 0, 0, 0, HALF_POWER, HALF_POWER}, {0, 0, 0, 0, 0, 0, HALF_POWER, HALF_POWER}}; +static constexpr Ngs2DefaultRow BACK_LEFT{0.5f, {0.5f, 0}, {0, 0, 0, 0, 1, 0}, {0, 0, 0, 0, 0, 0, 1, 0}}; +static constexpr Ngs2DefaultRow BACK_RIGHT{0.5f, {0, 0.5f}, {0, 0, 0, 0, 0, 1}, {0, 0, 0, 0, 0, 0, 0, 1}}; + +static constexpr Ngs2DefaultRow DEFAULT_MAP[NGS2_MAX_CHANNELS][NGS2_MAX_CHANNELS] = { + {MONO_CENTER}, + {FRONT_LEFT, FRONT_RIGHT}, + {FRONT_LEFT, FRONT_RIGHT, LFE}, + {FRONT_LEFT, FRONT_RIGHT, QUAD_LEFT, QUAD_RIGHT}, + {FRONT_LEFT, FRONT_RIGHT, MONO_CENTER, FIVE_SURROUND_LEFT, FIVE_SURROUND_RIGHT}, + {FRONT_LEFT, FRONT_RIGHT, MONO_CENTER, LFE, SURROUND_LEFT, SURROUND_RIGHT}, + {FRONT_LEFT, FRONT_RIGHT, MONO_CENTER, LFE, SURROUND_LEFT, SURROUND_RIGHT, BACK_CENTER}, + {FRONT_LEFT, FRONT_RIGHT, MONO_CENTER, LFE, SURROUND_LEFT, SURROUND_RIGHT, BACK_LEFT, BACK_RIGHT}, +}; + +float Ngs2DefaultLevel(std::uint32_t sourceChannels, std::uint32_t source, std::uint32_t destChannels, std::uint32_t dest) { + const auto& row = DEFAULT_MAP[sourceChannels - 1][source]; + switch (destChannels) { + case 1: return row.mono; + case 2: return row.stereo[dest]; + case 6: return row.surround51[dest]; + case 8: return row.surround71[dest]; + default: throw std::runtime_error("NGS2: the default channel map into " + std::to_string(destChannels) + " channels is not implemented"); + } +} + static void MixPort(Ngs2Voice& voice, const Ngs2Voice& source, const Ngs2Port& port, std::uint32_t grain) { - const auto* matrix = port.matrix < 0 ? nullptr : &source.matrices[port.matrix]; + const auto* matrix = port.matrix < 0 || source.matrices[port.matrix].empty() ? nullptr : &source.matrices[port.matrix]; const std::size_t outputs = matrix == nullptr ? voice.channels : std::min(voice.channels, matrix->size() / source.channels); for (std::size_t dst = 0; dst < outputs; dst++) { for (std::uint32_t src = 0; src < source.channels; src++) { - const float level = port.volume * (matrix == nullptr ? (src == dst ? 1.0f : 0.0f) : (*matrix)[dst * source.channels + src]); + const float level = port.volume * (matrix == nullptr ? Ngs2DefaultLevel(source.channels, src, voice.channels, static_cast(dst)) : (*matrix)[dst * source.channels + src]); if (level == 0.0f) continue; for (std::uint32_t i = 0; i < grain; i++) voice.samples[dst * grain + i] += source.samples[src * grain + i] * level; } @@ -194,11 +254,14 @@ static void RenderVoice(Ngs2Voice& voice, const std::vector& voices, voice.samples.assign(static_cast(grain) * voice.channels, 0.0f); voice.hasSamples = false; if (voice.state == Ngs2PlayState::Playing && voice.channels != 0) { - if (voice.rack->rackId == SCE_NGS2_RACK_ID_SAMPLER) RenderSampler(voice, grain, systemRate); - else MixInputs(voice, voices, grain, systemRate); + if (voice.rack->rackId == SCE_NGS2_RACK_ID_SAMPLER || voice.rack->rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER) { + RenderSampler(voice, grain, systemRate); + } else { + MixInputs(voice, voices, grain, systemRate); + } if (voice.rack->rackId == SCE_NGS2_RACK_ID_REVERB && voice.reverb) voice.hasSamples = Ngs2ProcessReverb(voice, grain, systemRate); Ngs2ProcessLegacyUserFx(voice, grain, systemRate); - if (voice.hasSamples) Ngs2ProcessUserFx(voice, grain, systemRate); + Ngs2ProcessUserFx(voice, grain, systemRate); } voice.rendered = true; } @@ -238,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) { @@ -260,12 +346,13 @@ 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; voice->stateFlags = static_cast(voice->state); + if (voice->state == Ngs2PlayState::Playing && voice->rack->rackId == SCE_NGS2_RACK_ID_SAMPLER && voice->blocks.empty() && voice->acceptsBlocks) voice->stateFlags |= SCE_NGS2_VOICE_STATE_FLAG_EMPTY; } system.renderCount++; } diff --git a/core/libs/prx/libSceNgs2.native/src/System.cpp b/core/libs/prx/libSceNgs2.native/src/System.cpp index cf861a6ae8..99eede7a7c 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)); @@ -110,6 +235,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 +256,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 +264,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 +297,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 +316,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 +325,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 +343,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 +351,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,24 +360,63 @@ 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; } @@ -251,6 +427,27 @@ int APS5_VABI sceNgs2SystemResetOption(Ngs2SystemOption* option) { 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 15fb195131..0000000000 --- a/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp +++ /dev/null @@ -1,61 +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 sceNgs2GeomCalcListener(const Ngs2GeomListenerParam* param, Ngs2GeomListenerWork* out_work, uint32_t flags) { - (void)param; - (void)out_work; - (void)flags; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNgs2GeomResetListenerParam(Ngs2GeomListenerParam* out_listener_param) { - (void)out_listener_param; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNgs2GeomResetSourceParam(Ngs2GeomSourceParam* out_source_param) { - (void)out_source_param; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNgs2PanGetVolumeMatrix(Ngs2PanWork* work, const Ngs2PanParam* params, uint32_t num_params, uint32_t matrix_format, float* out_volume_matrix) { - (void)work; - (void)params; - (void)num_params; - (void)matrix_format; - (void)out_volume_matrix; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNgs2PanInit(Ngs2PanWork* work, const float* speaker_angles, float unit_angle, uint32_t num_speakers) { - (void)work; - (void)speaker_angles; - (void)unit_angle; - (void)num_speakers; - 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 451bf98bc0..9cde79710f 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; } @@ -116,7 +123,14 @@ static void Patch(Ngs2Voice& voice, const Ngs2VoicePatchParam& patch) { return; } auto* dest = Ngs2FindVoice(patch.dest_handle); - if (dest == nullptr || dest->rack->system != voice.rack->system || dest->rack->rackId == SCE_NGS2_RACK_ID_SAMPLER) APS5_INVALID_ARG_EX; + if (dest == nullptr) { + auto* rack = Ngs2FindRack(patch.dest_handle); + if (rack != nullptr && !rack->voices.empty()) dest = &rack->voices.front(); + } + if (dest == nullptr || dest->rack->system != voice.rack->system || dest->rack->rackId == SCE_NGS2_RACK_ID_SAMPLER || + dest->rack->rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER) { + APS5_INVALID_ARG_EX; + } port.dest = dest; } @@ -199,9 +213,9 @@ 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; + 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) { @@ -218,9 +232,15 @@ static void AddWaveformBlocks(Ngs2Voice& voice, const Ngs2SamplerVoiceWaveformBl return; } voice.acceptsBlocks = (param.flags & SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE) != 0; + const bool allowsSilence = (param.flags & SCE_NGS2_WAVEFORM_BLOCKS_FLAG_SILENCE) != 0; for (std::uint32_t i = 0; i < param.num_blocks; i++) { const auto& block = param.blocks[i]; if (block.num_samples == 0 && block.data_size == 0) continue; + if (allowsSilence && block.data_size == 0) { + if (voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_ATRAC9) throw std::runtime_error("NGS2: a dataless waveform block under the silence flag is not implemented for ATRAC9 voices"); + voice.blocks.push_back({nullptr, block}); + continue; + } const std::uint64_t bytes = voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_ATRAC9 ? Ngs2Atrac9BlockBytes(voice, block) : (static_cast(block.num_skip_samples) + block.num_samples) * frameBytes; const bool streaming = block.num_samples != 0 && bytes > block.data_size && voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_PCM_I16L && @@ -239,7 +259,7 @@ static void AddWaveformBlocks(Ngs2Voice& voice, const Ngs2SamplerVoiceWaveformBl } } -static void SetFilter(Ngs2Voice& voice, const Ngs2SamplerVoiceFilterParam& param) { +void Ngs2SetFilter(Ngs2Voice& voice, const Ngs2SamplerVoiceFilterParam& param) { if (param.index >= voice.rack->maxFilters) APS5_INVALID_ARG_EX; if (voice.filters.size() <= param.index) voice.filters.resize(param.index + 1); auto& filter = voice.filters[param.index]; @@ -275,20 +295,28 @@ static void SetFilter(Ngs2Voice& voice, const Ngs2SamplerVoiceFilterParam& param static void ApplySamplerParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { switch (param.id) { - case SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP: SetupSampler(voice, ParamAs(param).format); break; - case SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS: AddWaveformBlocks(voice, ParamAs(param)); break; - case SCE_NGS2_SAMPLER_VOICE_PARAM_EXIT_LOOP: { + case SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP: + case SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_SETUP: + SetupSampler(voice, ParamAs(param).format); + break; + case SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS: + case SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS: + AddWaveformBlocks(voice, ParamAs(param)); + break; + case SCE_NGS2_SAMPLER_VOICE_PARAM_EXIT_LOOP: + case SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_EXIT_LOOP: { const auto looping = std::find_if(voice.blocks.begin(), voice.blocks.end(), [](const Ngs2Block& block) { return block.info.num_repeats != 0; }); if (looping != voice.blocks.end()) looping->info.num_repeats = 0; break; } - case SCE_NGS2_SAMPLER_VOICE_PARAM_PITCH: { + case SCE_NGS2_SAMPLER_VOICE_PARAM_PITCH: + case SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_PITCH: { const float ratio = ParamAs(param).ratio; if (!std::isfinite(ratio) || ratio < 0.0f) APS5_INVALID_ARG_EX; voice.pitch = ratio; break; } - case SCE_NGS2_SAMPLER_VOICE_PARAM_FILTER: SetFilter(voice, ParamAs(param)); break; + case SCE_NGS2_SAMPLER_VOICE_PARAM_FILTER: Ngs2SetFilter(voice, ParamAs(param)); break; default: throw std::runtime_error("NGS2: sampler voice param " + Ngs2Hex(param.id) + " is not implemented"); } } @@ -311,7 +339,8 @@ static void ApplyParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { } if (rackId != voice.rack->rackId) throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); switch (rackId) { - case SCE_NGS2_RACK_ID_SAMPLER: ApplySamplerParam(voice, param); return; + case SCE_NGS2_RACK_ID_SAMPLER: + case SCE_NGS2_RACK_ID_CUSTOM_SAMPLER: ApplySamplerParam(voice, param); return; case SCE_NGS2_RACK_ID_SUBMIXER: if (param.id == SCE_NGS2_SUBMIXER_VOICE_PARAM_USER_FX) { const auto& fx = ParamAs(param); @@ -361,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; @@ -383,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]; @@ -405,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; @@ -434,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"); } } @@ -441,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; @@ -453,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; @@ -467,11 +509,37 @@ 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; - std::lock_guard lock(Ngs2Mutex()); - *state_flags = CheckedVoice(voice_handle).stateFlags; + 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 sceNgs2PanInit(Ngs2PanWork* work, const float* speaker_angles, float unit_angle, uint32_t num_speakers) { + return Ngs2PanInit(work, speaker_angles, unit_angle, num_speakers); +} + +int APS5_VABI sceNgs2PanGetVolumeMatrix(Ngs2PanWork* work, const Ngs2PanParam* params, uint32_t num_params, uint32_t matrix_format, float* out_volume_matrix) { + 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); +} + +int APS5_VABI sceNgs2GeomCalcListener(const Ngs2GeomListenerParam* param, Ngs2GeomListenerWork* outWork, std::uint32_t flags) { + return Ngs2GeomCalcListener(param, outWork, flags); +} + +int APS5_VABI sceNgs2GeomResetSourceParam(Ngs2GeomSourceParam* out_source_param) { + return Ngs2GeomResetSourceParam(out_source_param); +} + } #pragma GCC visibility pop diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp index a6e94c2a9f..b9278520ac 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp @@ -481,6 +481,21 @@ static void TestAppendWithoutWaveform() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } +static void TestSilenceFlagRejected() { + const auto system = CreateSystem(); + const auto voice = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER)); + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 1, 48000, Config, 0, 0}}); + const Ngs2WaveformBlock block{0, 0, 0, 0, SuperframeSamples, 0, 0}; + bool rejected = false; + try { + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, Superframe, 0x10, 1, &block}); + } catch (const std::runtime_error& error) { + rejected = std::strstr(error.what(), "silence flag") != nullptr; + } + Require(rejected); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + int main() { const auto reference = Reference(); TestSkipAndBlockEnd(reference); @@ -495,5 +510,6 @@ int main() { TestStarvationAndTruncation(reference); TestAppendBoundaries(reference); TestAppendWithoutWaveform(); + TestSilenceFlagRejected(); return 0; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp index b5872f457e..379c277110 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp @@ -90,6 +90,17 @@ static bool CreateThrows(const Ngs2CustomSubmixerRackOption& option) { return false; } +template +static bool CreateSamplerThrows(const Ngs2CustomSamplerRackOption& option) { + Ngs2ContextBufferInfo query{}; + try { + sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &option.custom_rack_option.rack_option, &query); + } catch (const TException&) { + return true; + } + return false; +} + template static bool ControlThrows(uintptr_t voice, std::uint32_t id, TParam param) { param.header = {static_cast(sizeof(TParam)), 0, id}; @@ -224,8 +235,243 @@ static void TestRackOptions() { Require(calls.cleanupVoices == (std::vector{0, 1})); } +static void TestCustomSampler() { + calls = {}; + const auto system = CreateSystem(); + const auto master = Mastering(system, 2); + const Ngs2CustomUserFx2ModuleOption modules[2] = {Module(Gain), Module(Offset)}; + + Ngs2CustomSamplerRackOption option{}; + auto& common = option.custom_rack_option.rack_option; + common.size = sizeof(option); + common.max_grain_samples = 512; + common.max_voices = 2; + common.max_matrices = 1; + common.max_ports = 1; + option.custom_rack_option.num_buffers = 1; + option.custom_rack_option.num_modules = 2; + for (std::uint32_t m = 0; m < 2; m++) { + option.custom_rack_option.module[m].option = &modules[m].custom_module_option; + option.custom_rack_option.module[m].module_id = SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2; + option.custom_rack_option.module[m].state_size = 16; + } + option.max_channel_works = 4; + + Ngs2ContextBufferInfo query{}; + Require(sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &option.custom_rack_option.rack_option, &query) == SCE_NGS2_OK); + const auto info = Buffer(query); + uintptr_t rack = 0; + Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &option.custom_rack_option.rack_option, &info, &rack) == SCE_NGS2_OK); + + Ngs2RackInfo rackInfo{}; + Require(sceNgs2RackGetInfo(rack, &rackInfo, sizeof(rackInfo)) == SCE_NGS2_OK); + Require(rackInfo.rack_id == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER && rackInfo.type == 4 && rackInfo.max_channel_works == 4); + + const auto voice = Voice(rack); + Control(voice, SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 2, 48000, 0, 0, 0}}); + std::vector pcm(Grain * 2 * 4, 16384); + const Ngs2WaveformBlock block{0, pcm.size() * sizeof(std::int16_t), 1, 0, static_cast(pcm.size() / 2), 0, 0}; + Control(voice, SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), 0, 1, &block}); + Control(voice, SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_PITCH, Ngs2SamplerVoicePitchParam{{}, 1.0f}); + const Ngs2VoiceParamHeader exitLoop{sizeof(Ngs2VoiceParamHeader), 0, SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_EXIT_LOOP}; + Require(sceNgs2VoiceControl(voice, &exitLoop) == SCE_NGS2_OK); + Patch(voice, master); + Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); + + const auto out = Render(system); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.5f && out[i * 2 + 1] == 0.75f); + + 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); +} + +static void TestCustomFilter() { + const auto system = CreateSystem(); + const auto master = Mastering(system, 1); + + Ngs2CustomSamplerRackOption option{}; + option.custom_rack_option.rack_option.size = sizeof(option); + option.custom_rack_option.rack_option.max_grain_samples = 512; + option.custom_rack_option.rack_option.max_voices = 1; + option.custom_rack_option.rack_option.max_matrices = 1; + option.custom_rack_option.rack_option.max_ports = 1; + option.custom_rack_option.num_buffers = 1; + option.custom_rack_option.num_modules = 1; + option.custom_rack_option.module[0].module_id = SCE_NGS2_CUSTOM_MODULE_ID_FILTER; + option.max_channel_works = 1; + + auto invalid = option; + invalid.custom_rack_option.module[0].state_size = 4; + Require(CreateSamplerThrows(invalid)); + invalid = option; + invalid.custom_rack_option.module[0].option = reinterpret_cast(1); + Require(CreateSamplerThrows(invalid)); + + Ngs2ContextBufferInfo query{}; + Require(sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &option.custom_rack_option.rack_option, &query) == SCE_NGS2_OK); + const auto info = Buffer(query); + uintptr_t rack = 0; + Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &option.custom_rack_option.rack_option, &info, &rack) == SCE_NGS2_OK); + + const auto voice = Voice(rack); + Control(voice, SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_SETUP, + Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1, 48000, 0, 0, 0}}); + const std::vector pcm(Grain, 16384); + const Ngs2WaveformBlock block{0, pcm.size() * sizeof(std::int16_t), 0, 0, static_cast(pcm.size()), 0, 0}; + Control(voice, SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, + Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), 0, 1, &block}); + + Ngs2CustomVoiceFilterParam filter{}; + filter.type = 1; + filter.param.fcq.frequency = 10000.0f; + filter.param.fcq.q = 0.70710678f; + filter.param.fcq.level = 1.0f; + Control(voice, SCE_NGS2_CUSTOM_VOICE_PARAM_FILTER, filter); + Require(ControlThrows(voice, SCE_NGS2_CUSTOM_VOICE_PARAM_FILTER | 1, filter)); + Patch(voice, master); + Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); + + std::vector out(Grain, -1.0f); + const Ngs2RenderBufferInfo renderInfo{out.data(), out.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 1}; + Require(sceNgs2SystemRender(system, &renderInfo, 1) == SCE_NGS2_OK); + Require(out[0] > 0.0f && out[0] < 0.2f && out.back() > out[0]); + + 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 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); +} + int main() { TestUserFxChain(); TestRackOptions(); + TestCustomSampler(); + TestCustomFilter(); + TestCustomLegacyUserFx(); return 0; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp new file mode 100644 index 0000000000..2417e360e0 --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp @@ -0,0 +1,191 @@ +#include "Ngs2Test.hpp" + +#include +#include +#include +#include +#include + +static constexpr float SoundSpeed = 343.0f; + +static void TestLayouts() { + Require(sizeof(Ngs2GeomVector) == 12); + Require(sizeof(Ngs2GeomCone) == 16); + Require(sizeof(Ngs2GeomRolloff) == 16); + Require(sizeof(Ngs2GeomListenerParam) == 60); + Require(sizeof(Ngs2GeomListenerWork) == 96); + Require(sizeof(Ngs2GeomSourceParam) == 108); + Require(sizeof(Ngs2GeomA3dAttribute) == 32); + Require(sizeof(Ngs2GeomAttribute) == 308); +} + +static void TestResetListenerParam() { + Ngs2GeomListenerParam param; + std::memset(¶m, 0x5a, sizeof(param)); + Require(sceNgs2GeomResetListenerParam(¶m) == SCE_NGS2_OK); + Require(param.position.x == 0.0f && param.position.y == 0.0f && param.position.z == 0.0f); + Require(param.velocity.x == 0.0f && param.velocity.y == 0.0f && param.velocity.z == 0.0f); + Require(param.orient_front.x == 0.0f && param.orient_front.y == 0.0f && param.orient_front.z == 1.0f); + Require(param.orient_up.x == 0.0f && param.orient_up.y == 1.0f && param.orient_up.z == 0.0f); + Require(param.sound_speed == SoundSpeed); + Require(param.reserved[0] == 0u && param.reserved[1] == 0u); + Require(sceNgs2GeomResetListenerParam(nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); +} + +static void TestResetSourceParam() { + Ngs2GeomSourceParam param; + std::memset(¶m, 0x5a, sizeof(param)); + Require(sceNgs2GeomResetSourceParam(¶m) == SCE_NGS2_OK); + Require(param.position.x == 0.0f && param.position.y == 0.0f && param.position.z == 0.0f); + Require(param.velocity.x == 0.0f && param.velocity.y == 0.0f && param.velocity.z == 0.0f); + Require(param.direction.x == 0.0f && param.direction.y == 0.0f && param.direction.z == 1.0f); + Require(param.cone.inner_level == 1.0f && param.cone.inner_angle == 360.0f); + Require(param.cone.outer_level == 1.0f && param.cone.outer_angle == 360.0f); + Require(param.rolloff.model == 0u); + Require(param.rolloff.max_distance == 1000000.0f); + Require(param.rolloff.rolloff_factor == 1.0f); + Require(param.rolloff.reference_distance == 1.0f); + Require(param.doppler_factor == 1.0f && param.fbw_level == 1.0f && param.lfe_level == 1.0f); + Require(param.max_level == 1.0f && param.min_level == 0.0f && param.radius == 0.0f); + Require(param.num_speakers == 2u && param.matrix_format == 2u); + Require(param.reserved[0] == 0u && param.reserved[1] == 0u); + Require(sceNgs2GeomResetSourceParam(nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); +} + +static Ngs2GeomListenerParam Listener() { + Ngs2GeomListenerParam param{}; + Require(sceNgs2GeomResetListenerParam(¶m) == SCE_NGS2_OK); + return param; +} + +static void RequireMatrix(const Ngs2GeomListenerWork& work, const float (&expected)[4][4]) { + for (std::size_t row = 0; row < 4; row++) { + for (std::size_t column = 0; column < 4; column++) Require(std::abs(work.matrix[row][column] - expected[row][column]) < 0.00001f); + } +} + +static void TestListenerTransform() { + auto param = Listener(); + 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}}); + Require(work.velocity.x == 2.0f && work.velocity.y == -3.0f && work.velocity.z == 4.0f); + Require(work.sound_speed == 1500.0f && work.coordinate == 0); + Require(work.reserved[0] == 0 && work.reserved[1] == 0 && work.reserved[2] == 0); + + param.position = {3.0f, 4.0f, 5.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}}); + + 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) { + Ngs2GeomListenerWork work; + std::memset(&work, 0x5a, sizeof(work)); + unsigned char before[sizeof(work)]; + std::memcpy(before, &work, sizeof(work)); + bool rejected = false; + try { + sceNgs2GeomCalcListener(param, &work, flags); + } catch (const std::exception&) { + rejected = true; + } + Require(rejected); + Require(std::memcmp(before, &work, sizeof(work)) == 0); +} + +static void TestInvalidListener() { + auto param = Listener(); + Require(sceNgs2GeomCalcListener(¶m, nullptr, 0) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + RejectListener(nullptr, 0); + RejectListener(¶m, 2); + RejectListener(¶m, 0xffffffffu); + for (const float value : {0.0f, -1.0f, std::numeric_limits::infinity(), std::numeric_limits::quiet_NaN()}) { + param.sound_speed = value; + RejectListener(¶m, 0); + } + param = Listener(); + param.orient_front = {}; + RejectListener(¶m, 0); + param = Listener(); + param.orient_up = {}; + 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(); + TestListenerTransform(); + TestInvalidListener(); + TestGeomApply(); + return 0; +} diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp new file mode 100644 index 0000000000..56246ca178 --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp @@ -0,0 +1,48 @@ +#include "Ngs2Test.hpp" + +#include +#include + +static void Status(int actual, int expected, const char* message) { + if (actual != expected) throw std::runtime_error(message); +} + +int main() { + try { + constexpr int systemError = static_cast(0x804A8201u); + constexpr int rackError = static_cast(0x804A8202u); + constexpr int outputError = static_cast(0x804A8010u); + constexpr uintptr_t sentinel = 0x5a5a; + uintptr_t value = sentinel; + Status(sceNgs2SystemGetUserData(0x1234, &value), systemError, "invalid system handle returned a non-PS5 error"); + Require(value == sentinel); + Status(sceNgs2SystemDestroy(0x1234, nullptr), systemError, "system destroy returned a non-PS5 handle error"); + Status(sceNgs2RackGetVoiceHandle(0x1234, 0, &value), rackError, "invalid rack handle returned a non-PS5 error"); + Require(value == sentinel); + Status(sceNgs2RackGetVoiceHandle(0x1234, 0, nullptr), rackError, "voice lookup checked output before the rack handle"); + Status(sceNgs2RackDestroy(0x1234, nullptr), rackError, "rack destroy returned a non-PS5 handle error"); + const auto system = CreateSystem(); + const auto rack = CreateRack(system, SCE_NGS2_RACK_ID_REVERB); + Status(sceNgs2SystemGetUserData(rack, &value), systemError, "rack handle was accepted as a system"); + Require(value == sentinel); + Status(sceNgs2RackGetVoiceHandle(system, 0, &value), rackError, "system handle was accepted as a rack"); + Require(value == sentinel); + Status(sceNgs2RackGetVoiceHandle(system, 0, nullptr), rackError, "wrong-kind rack did not take precedence over null output"); + Status(sceNgs2RackGetVoiceHandle(rack, 0, nullptr), outputError, "valid rack lost null-output validation"); + Status(sceNgs2RackGetVoiceHandle(rack, 0, &value), SCE_NGS2_OK, "valid voice lookup failed"); + Require(value != 0 && value != sentinel); + Status(sceNgs2RackDestroy(rack, nullptr), SCE_NGS2_OK, "valid rack destroy failed"); + value = sentinel; + Status(sceNgs2RackGetVoiceHandle(rack, 0, &value), rackError, "retired rack returned the wrong error"); + Require(value == sentinel); + Status(sceNgs2RackGetVoiceHandle(rack, 0, nullptr), rackError, "retired rack did not take precedence over null output"); + Status(sceNgs2SystemDestroy(system, nullptr), SCE_NGS2_OK, "valid system destroy failed"); + Status(sceNgs2SystemGetUserData(system, &value), systemError, "retired system returned the wrong error"); + Require(value == sentinel); + std::cout << "NGS2 PS5 handle errors and validation precedence checked\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp index 396881d465..a2545f52e6 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp @@ -87,8 +87,35 @@ static void TestParseFile() { std::filesystem::remove(path); } +static void TestParseFileBank() { + const auto first = PcmFile(2, 48000, 16, 300); + auto second = PcmFile(1, 32000, 16, 50); + PutTag(second, "LIST"); + Put32(second, 4); + PutTag(second, "INFO"); + const std::uint32_t riffSize = static_cast(second.size() - 8); + std::memcpy(second.data() + 4, &riffSize, sizeof(riffSize)); + const char* const guestPath = "/aps5_ngs2_parse_bank.bin"; + const auto path = ResolvePath_nid_no_patch(guestPath); + { + std::ofstream out(path, std::ios::binary); + out.write(reinterpret_cast(first.data()), static_cast(first.size())); + out.write(reinterpret_cast(second.data()), static_cast(second.size())); + } + Ngs2WaveformInfo info{}; + Require(sceNgs2ParseWaveformFile(guestPath, 0, &info) == SCE_NGS2_OK); + Require(info.format.num_channels == 2 && info.format.sample_rate == 48000 && info.num_samples == 300); + Require(info.data_offset == 44 && info.data_size == 1200 && info.block[0].data_offset == 44); + const auto secondOffset = static_cast(first.size()); + Require(sceNgs2ParseWaveformFile(guestPath, secondOffset, &info) == SCE_NGS2_OK); + Require(info.format.num_channels == 1 && info.format.sample_rate == 32000 && info.num_samples == 50); + Require(info.data_offset == secondOffset + 44 && info.data_size == 100 && info.block[0].data_offset == secondOffset + 44); + std::filesystem::remove(path); +} + int main() { TestParsePcm(); TestParseFile(); + TestParseFileBank(); return 0; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp index eabf182c0f..4186d67946 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp @@ -6,12 +6,17 @@ #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}); @@ -39,8 +44,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); @@ -145,6 +150,131 @@ static void TestPcmBlockEnd() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } +static void TestPatchToRackHandle() { + const auto system = CreateSystem(); + const auto masteringRack = CreateRack(system, SCE_NGS2_RACK_ID_MASTERING); + const auto master = Voice(masteringRack); + Control(master, SCE_NGS2_MASTERING_VOICE_PARAM_SETUP, Ngs2MasteringVoiceSetupParam{{}, 1, 0}); + Control(master, SCE_NGS2_MASTERING_VOICE_PARAM_OUTPUT, Ngs2MasteringVoiceOutputParam{{}, 0, 0}); + Event(master, SCE_NGS2_VOICE_EVENT_PLAY); + + const std::vector pcm(Grain, 16384); + const auto sampler = Sampler(system, pcm, 0); + Control(sampler, SCE_NGS2_VOICE_PARAM_PATCH, Ngs2VoicePatchParam{{}, 0, 0, masteringRack}); + + Ngs2VoicePortInfo port{}; + Require(sceNgs2VoiceGetPortInfo(sampler, 0, &port, sizeof(port)) == SCE_NGS2_OK); + Require(port.dest_handle == master && port.dest_input_id == 0); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + const auto out = RenderI16(system); + Require(out == pcm); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +static bool Near(float a, float b) { + return std::fabs(a - b) < 1e-4f; +} + +static bool PanThrows(Ngs2PanWork* work, const Ngs2PanParam* params, std::uint32_t numParams, std::uint32_t format) { + float out[64] = {}; + try { sceNgs2PanGetVolumeMatrix(work, params, numParams, format, out); } catch (const std::exception&) { return true; } + return false; +} + +static bool PanInitThrows(Ngs2PanWork* work, const float* angles, float unitAngle, std::uint32_t numSpeakers) { + try { sceNgs2PanInit(work, angles, unitAngle, numSpeakers); } catch (const std::exception&) { return true; } + return false; +} + +struct PanCase { + std::vector angles; + float unitAngle; + std::uint32_t numSpeakers; + std::vector params; + std::uint32_t format; + std::vector expected; +}; + +static void TestPan() { + const float pi = std::numbers::pi_v; + Ngs2PanWork work{}; + Require(sceNgs2PanInit(&work, nullptr, 360.0f, 2) == SCE_NGS2_OK); + Require(work.num_speakers == 2 && work.unit_angle == 360.0f); + Require(work.speaker_angles[0] == 90.0f && work.speaker_angles[1] == 270.0f && work.speaker_angles[2] == 450.0f); + Require(sceNgs2PanInit(&work, nullptr, 2.0f * pi, 2) == SCE_NGS2_OK && work.unit_angle == 2.0f * pi); + Require(work.speaker_angles[0] == 90.0f / 180.0f * pi && work.speaker_angles[1] == 270.0f / 180.0f * pi && work.speaker_angles[2] == 450.0f); + Require(sceNgs2PanInit(&work, nullptr, 100.0f, 4) == SCE_NGS2_OK && work.unit_angle == 360.0f && work.speaker_angles[4] == 400.0f); + const float frontBack[] = {-30.0f, 30.0f, 0.0f, -110.0f, 470.0f}; + Require(sceNgs2PanInit(&work, frontBack, 360.0f, 5) == SCE_NGS2_OK); + const float circle[] = {0.0f, 30.0f, 110.0f, 250.0f, 330.0f, 360.0f}; + for (std::uint32_t i = 0; i < 6; i++) Require(work.speaker_angles[i] == circle[i]); + + const std::vector cases = { + {{}, 360.0f, 2, {{0, 1, 1, 0}, {45, 1, 1, 0}, {90, 1, 1, 0}, {135, 1, 1, 0}, {180, 1, 1, 0}, {225, 1, 1, 0}, {270, 1, 1, 0}, {315, 1, 1, 0}}, 2, + {0.707106769f, 0.382683456f, 0.0f, 0.382683456f, 0.707106769f, 0.923879504f, 1.0f, 0.923879504f, 0.707106769f, 0.923879504f, 1.0f, 0.923879504f, 0.707106769f, 0.382683456f, 0.0f, 0.382683456f}}, + {{}, 360.0f, 2, {{30, 0, 1, 0}, {30, -1, 1, 0}, {30, 0.5f, 0.5f, 0}, {30, 3, 1, 0}, {-30, -0.25f, 1, 0}}, 2, + {0.965925813f, 0.866025448f, 0.39667666f, 0.49999997f, 0.896872759f, 0.965925813f, 0.49999997f, 0.495722443f, 0.866025448f, 0.997858942f}}, + {{}, 360.0f, 2, {{60, 1, 1, 0.5f}}, 1, {0.866025388f}}, + {{}, 360.0f, 2, {{60, 1, 1, 0.5f}}, 6, {0.258819073f, 0.965925813f, 0.0f, 0.0f, 0.0f, 0.0f}}, + {{}, 2.0f * pi, 2, {{0.25f * pi, 1, 1, 0}, {1.5f * pi, 1, 1, 0}}, 2, {0.999965429f, 1.0f, 0.00831161533f, 0.0f}}, + {{}, 360.0f, 4, {{100, 1, 1, 0.25f}}, 6, {0.0f, 0.222520918f, 0.0f, 0.25f, 0.0f, 0.974927902f}}, + {{}, 360.0f, 4, {{100, 1, 1, 0.25f}}, 2, {0.0f, 0.709984899f}}, + {{}, 360.0f, 5, {{70, 1, 0.8f, 0.5f}, {200, 0.5f, 1, 0}}, 6, + {0.0f, 0.0f, 0.625465155f, 0.270598054f, 0.0f, 0.270598054f, 0.5f, 0.0f, 0.0f, 0.782271147f, 0.498791903f, 0.491533577f}}, + {{}, 360.0f, 5, {{70, 1, 0.8f, 0.5f}}, 1, {0.691666603f}}, + {{}, 360.0f, 7, {{180, 1, 1, 0.25f}}, 8, {0.0f, 0.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.707106769f, 0.707106769f}}, + {{}, 360.0f, 7, {{180, 1, 1, 0.25f}}, 6, {0.0f, 0.0f, 0.0f, 0.25f, 0.707106769f, 0.707106769f}}, + {{-pi / 6, pi / 6, 0, -110 * pi / 180, 110 * pi / 180}, 2.0f * pi, 5, {{1.0f, 1, 1, 0}, {-2.5f, 0.2f, 1, 0.1f}}, 6, + {0.0f, 0.0f, 0.859783232f, 0.582614243f, 0.0f, 0.0f, 0.0f, 0.100000001f, 0.0f, 0.753403723f, 0.510659218f, 0.372568965f}}, + {{-30, 30, 0, -90, 90, -150, 150}, 360.0f, 7, {{-120, 1, 1, 0}}, 1, {0.707106769f}}, + {{-45, 45}, 360.0f, 2, {{10, 1, 1, 0}, {720 + 170, 1, 1, 0}}, 2, {0.57357651f, 0.664795876f, 0.819152057f, 0.747025073f}}, + {{-40, 40, -120, 120}, 360.0f, 4, {{-80, 0.75f, 1, 0}}, 8, {0.69351995f, 0.0746578425f, 0.0f, 0.0f, 0.69351995f, 0.180239946f, 0.0f, 0.0f}}, + }; + for (const auto& panCase : cases) { + Require(sceNgs2PanInit(&work, panCase.angles.empty() ? nullptr : panCase.angles.data(), panCase.unitAngle, panCase.numSpeakers) == SCE_NGS2_OK); + std::vector out(panCase.expected.size(), -1.0f); + Require(sceNgs2PanGetVolumeMatrix(&work, panCase.params.data(), static_cast(panCase.params.size()), panCase.format, out.data()) == SCE_NGS2_OK); + for (std::size_t i = 0; i < out.size(); i++) Require(std::fabs(out[i] - panCase.expected[i]) < 1e-6f); + } + + const Ngs2PanParam front{0.0f, 1.0f, 1.0f, 0.0f}; + float out[2] = {}; + Require(sceNgs2PanInit(&work, nullptr, 360.0f, 2) == SCE_NGS2_OK); + Require(sceNgs2PanGetVolumeMatrix(&work, &front, 1, 2, nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2PanGetVolumeMatrix(&work, &front, 0, 2, out) == SCE_NGS2_OK); + Require(PanThrows(nullptr, &front, 1, 2) && PanThrows(&work, nullptr, 0, 2)); + Require(PanThrows(&work, &front, 1, 4) && PanThrows(&work, &front, 1, 3) && !PanThrows(&work, &front, 1, 8)); + const Ngs2PanParam many[9] = {}; + Require(PanThrows(&work, many, 9, 2) && !PanThrows(&work, many, 8, 2)); + const Ngs2PanParam nanAngle{NAN, 1.0f, 1.0f, 0.0f}; + Require(PanThrows(&work, &nanAngle, 1, 2)); + const Ngs2PanParam laterNan[2] = {front, nanAngle}; + float untouched[4] = {-1.0f, -1.0f, -1.0f, -1.0f}; + bool rejected = false; + try { sceNgs2PanGetVolumeMatrix(&work, laterNan, 2, 2, untouched); } catch (const std::invalid_argument&) { rejected = true; } + Require(rejected && std::all_of(std::begin(untouched), std::end(untouched), [](float level) { return level == -1.0f; })); + const Ngs2PanParam huge{1e30f, 1.0f, 1.0f, 0.0f}; + Require(PanThrows(&work, &huge, 1, 2)); + const float hugeSpeaker[] = {1e30f, 30.0f}; + const float hugeNegative[] = {-1e30f, 30.0f}; + Require(PanInitThrows(&work, hugeSpeaker, 360.0f, 2) && PanInitThrows(&work, hugeNegative, 360.0f, 2)); + Require(sceNgs2PanInit(&work, nullptr, 360.0f, 2) == SCE_NGS2_OK); + 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)); + const float pair[] = {-30.0f, 30.0f}; + Require(PanInitThrows(&work, pair, 180.0f, 2) && !PanInitThrows(&work, pair, 2.0f * pi, 2)); + const float invalid[] = {0.0f, NAN}; + Require(PanInitThrows(&work, invalid, 360.0f, 2)); + const float onUnit[] = {720.0f, 30.0f}; + const float belowZero[] = {-360.0f, 30.0f}; + Require(PanInitThrows(&work, onUnit, 360.0f, 2) && !PanInitThrows(&work, belowZero, 360.0f, 2)); + work.unit_angle = 100.0f; + Require(PanThrows(&work, &front, 1, 2)); +} + static void TestPitchAndRepeat() { const auto system = CreateSystem(); const auto master = Mastering(system, 1); @@ -170,6 +300,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); @@ -478,6 +765,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); } @@ -624,25 +938,499 @@ 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); +} + +static constexpr float HalfPower = std::numbers::sqrt2_v / 2.0f; + +static std::vector MixFrame(std::uint32_t sourceChannels, std::uint32_t sourceChannel, std::uint32_t destChannels) { + const auto firstBuffer = usedBuffers; + const auto system = CreateSystem(); + const auto master = Mastering(system, destChannels); + const auto voice = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER)); + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, sourceChannels, 48000, 0, 0, 0}}); + std::vector frames(Grain * sourceChannels, 0); + for (std::uint32_t i = 0; i < Grain; i++) frames[i * sourceChannels + sourceChannel] = 16384; + const Ngs2WaveformBlock block{0, frames.size() * sizeof(std::int16_t), 0, 0, Grain, 0, 0}; + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, frames.data(), 0, 1, &block}); + Patch(voice, master); + Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); + std::vector out(Grain * destChannels, -1.0f); + const Ngs2RenderBufferInfo info{out.data(), out.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, destChannels}; + Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); + usedBuffers = firstBuffer; + return std::vector(out.begin(), out.begin() + destChannels); +} + +static void TestDefaultChannelMap() { + const float half = 0.5f * HalfPower; + const float quarter = 0.5f * (std::numbers::sqrt2_v / 4.0f); + Require(MixFrame(1, 0, 1) == std::vector{0.5f}); + Require(MixFrame(1, 0, 2) == (std::vector{half, half})); + Require(MixFrame(1, 0, 6) == (std::vector{0, 0, 0.5f, 0, 0, 0})); + Require(MixFrame(1, 0, 8) == (std::vector{0, 0, 0.5f, 0, 0, 0, 0, 0})); + Require(MixFrame(2, 0, 1) == std::vector{half} && MixFrame(2, 1, 1) == std::vector{half}); + Require(MixFrame(2, 1, 2) == (std::vector{0, 0.5f})); + Require(MixFrame(2, 1, 6) == (std::vector{0, 0.5f, 0, 0, 0, 0})); + Require(MixFrame(3, 2, 2) == (std::vector{0, 0})); + Require(MixFrame(3, 2, 6) == (std::vector{0, 0, 0, 0.5f, 0, 0})); + Require(MixFrame(4, 2, 1) == std::vector{0.25f}); + Require(MixFrame(4, 2, 2) == (std::vector{half, 0})); + Require(MixFrame(4, 3, 6) == (std::vector{0, 0, 0, 0, 0, 0.5f})); + Require(MixFrame(5, 2, 2) == (std::vector{half, half})); + Require(MixFrame(5, 3, 1) == std::vector{0.25f}); + Require(MixFrame(5, 3, 2) == (std::vector{0, 0})); + Require(MixFrame(6, 3, 1) == std::vector{0}); + Require(MixFrame(6, 4, 2) == (std::vector{0.25f, 0})); + Require(MixFrame(6, 5, 8) == (std::vector{0, 0, 0, 0, 0, 0.5f, 0, 0})); + Require(MixFrame(7, 6, 1) == std::vector{quarter}); + Require(MixFrame(7, 6, 2) == (std::vector{quarter, quarter})); + Require(MixFrame(7, 6, 6) == (std::vector{0, 0, 0, 0, half, half})); + Require(MixFrame(7, 6, 8) == (std::vector{0, 0, 0, 0, 0, 0, half, half})); + Require(MixFrame(8, 6, 2) == (std::vector{0.25f, 0})); + Require(MixFrame(8, 7, 6) == (std::vector{0, 0, 0, 0, 0, 0.5f})); + Require(MixFrame(8, 7, 8) == (std::vector{0, 0, 0, 0, 0, 0, 0, 0.5f})); + + const auto system = CreateSystem(); + const auto master = Mastering(system, 2); + const auto submixer = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SUBMIXER)); + Control(submixer, SCE_NGS2_SUBMIXER_VOICE_PARAM_SETUP, Ngs2SubmixerVoiceSetupParam{{}, 4, 0}); + Patch(submixer, master); + Event(submixer, SCE_NGS2_VOICE_EVENT_PLAY); + const std::vector pcm(Grain, 16384); + const auto sampler = Sampler(system, pcm, 0); + Patch(sampler, submixer); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + std::vector out(Grain * 2, -1.0f); + const Ngs2RenderBufferInfo info{out.data(), out.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 2}; + bool unsupported = false; + try { sceNgs2SystemRender(system, &info, 1); } catch (const std::runtime_error&) { unsupported = true; } + Require(unsupported); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +static void TestUnsetMatrix() { + const auto system = CreateSystem(); + const auto master = Mastering(system, 2); + const auto submixer = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SUBMIXER)); + Control(submixer, SCE_NGS2_SUBMIXER_VOICE_PARAM_SETUP, Ngs2SubmixerVoiceSetupParam{{}, 2, 0}); + Patch(submixer, master); + Event(submixer, SCE_NGS2_VOICE_EVENT_PLAY); + + const std::vector pcm(Grain, 16384); + const auto sampler = Sampler(system, pcm, 0); + Patch(sampler, submixer); + Control(sampler, SCE_NGS2_VOICE_PARAM_PORT_MATRIX, Ngs2VoicePortMatrixParam{{}, 0, 0}); + Control(sampler, SCE_NGS2_VOICE_PARAM_PORT_VOLUME, Ngs2VoicePortVolumeParam{{}, 0, 0.5f}); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + + std::vector out(Grain * 2, -1.0f); + const Ngs2RenderBufferInfo info{out.data(), out.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 2}; + Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK); + const float spread = 0.25f * HalfPower; + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == spread && out[i * 2 + 1] == spread); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); + + const auto stereoSystem = CreateSystem(); + const auto stereoMaster = Mastering(stereoSystem, 2); + const auto stereo = Voice(CreateRack(stereoSystem, SCE_NGS2_RACK_ID_SAMPLER)); + Control(stereo, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 2, 48000, 0, 0, 0}}); + std::vector frames; + for (std::uint32_t i = 0; i < Grain; i++) { + frames.push_back(16384); + frames.push_back(8192); + } + const Ngs2WaveformBlock block{0, frames.size() * sizeof(std::int16_t), 0, 0, Grain, 0, 0}; + Control(stereo, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, frames.data(), 0, 1, &block}); + Patch(stereo, stereoMaster); + Control(stereo, SCE_NGS2_VOICE_PARAM_PORT_MATRIX, Ngs2VoicePortMatrixParam{{}, 0, 0}); + Event(stereo, SCE_NGS2_VOICE_EVENT_PLAY); + Require(sceNgs2SystemRender(stereoSystem, &info, 1) == SCE_NGS2_OK); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.5f && out[i * 2 + 1] == 0.25f); + 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); } @@ -671,16 +1459,36 @@ int main() { Require(std::atexit(RenderAfterStaticTeardown) == 0); TestErrorsAndInfo(); TestPcmBlockEnd(); + TestPatchToRackHandle(); + TestPan(); TestPitchAndRepeat(); TestSubmixerMatrix(); TestReverb(); 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 new file mode 100644 index 0000000000..146b691a34 --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Report.cpp @@ -0,0 +1,43 @@ +#include "Ngs2Test.hpp" + +#include +#include + +namespace { + +template +bool Throws(Call call) { + try { + call(); + } catch (const std::exception&) { + return true; + } + return false; +} + +int callbacks = 0; +void APS5_VABI Counted(const Ngs2ReportDataHeader*, std::uintptr_t) { + callbacks++; +} + +} + +int main() { + 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 4b983d3e7b..fa222ce472 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Stream.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Stream.cpp @@ -128,9 +128,86 @@ static void TestStreamRejections() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } +static const std::int16_t Unused = 0; + +static void TestSilenceBlock() { + const auto system = CreateSystem(); + const auto master = Mastering(system, 1); + const auto rack = CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER); + const auto voice = StreamVoice(rack, 0, master); + + AddBlock(voice, &Unused, 0, Grain, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_SILENCE); + Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); + auto out = RenderI16(system); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == 0); + Require(Flags(voice) == 0); + Ngs2SamplerVoiceState state{}; + Require(sceNgs2VoiceGetState(voice, &state.voice_state, sizeof(state)) == SCE_NGS2_OK); + Require(state.num_decoded_samples == Grain && state.decoded_data_size == Grain * 2); + + const auto withData = StreamVoice(rack, 1, master); + const std::vector pcm(Grain, 1234); + AddBlock(withData, pcm.data(), pcm.size() * 2, Grain, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_SILENCE); + Event(withData, SCE_NGS2_VOICE_EVENT_PLAY); + out = RenderI16(system); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == 1234); + Require(Flags(withData) == 0); + + const auto dataless = StreamVoice(rack, 2, master); + Require(Throws([&] { AddBlock(dataless, &Unused, 0, Grain, 0); })); + 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); + const auto rack = CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER); + const auto voice = StreamVoice(rack, 0, master); + const std::vector pcm(4, 16384); + constexpr std::uint32_t playing = SCE_NGS2_VOICE_STATE_FLAG_INUSE | SCE_NGS2_VOICE_STATE_FLAG_PLAYING; + + AddBlock(voice, pcm.data(), pcm.size() * 2, 4, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_RESET | SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE); + Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); + RenderI16(system); + Require(Flags(voice) == (playing | SCE_NGS2_VOICE_STATE_FLAG_EMPTY)); + + const std::vector longer(Grain * 2, 8192); + AddBlock(voice, longer.data(), longer.size() * 2, Grain * 2, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE); + const auto out = RenderI16(system); + Require(out[0] == 8192); + Require(Flags(voice) == playing); + + AddBlock(voice, pcm.data(), pcm.size() * 2, 4, 0); + RenderI16(system); + RenderI16(system); + Require((Flags(voice) & SCE_NGS2_VOICE_STATE_FLAG_EMPTY) == 0); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + int main() { TestStreamEndsAtDeclaredSamples(); TestStreamClosedBeforeDeclaredSamples(); 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 97843654b8..e861171262 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp @@ -25,18 +25,30 @@ int APS5_VABI sceNgs2SystemGetUserData(uintptr_t, uintptr_t*); int APS5_VABI sceNgs2SystemLock(uintptr_t); int APS5_VABI sceNgs2SystemUnlock(uintptr_t); int APS5_VABI sceNgs2SystemRender(uintptr_t, const Ngs2RenderBufferInfo*, uint32_t); +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 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) { 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..5937b3f763 --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2VoiceHandles.cpp @@ -0,0 +1,68 @@ +#include "Ngs2Test.hpp" + +#include +#include +#include +#include +#include +#include +#include + +static constexpr std::array Names{"control", "commands", "state", "port", "query", "flags"}; +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 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"); + 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"); + 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..954ec4c20f --- /dev/null +++ b/core/libs/prx/libSceNgs2/PendingExports.cpp @@ -0,0 +1,49 @@ +#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 sceNgs2GetWaveformFrameInfo(void* format, std::uint32_t* outFrameSize, std::uint32_t* outNumFrameSamples, std::uint32_t* outUnitsPerFrame, std::uint32_t* outNumDelaySamples) { + 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; +} +int APS5_VABI sceNgs2VoiceGetOwner(std::uintptr_t voiceHandle, std::uintptr_t* outOwnerRack, std::uint32_t* outVoiceIndex) { + 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 new file mode 100644 index 0000000000..ce6ce6a2f8 --- /dev/null +++ b/core/libs/prx/libSceNpAuth/CMakeLists.txt @@ -0,0 +1,14 @@ +add_library(libSceNpAuth SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceNpAuth PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceNpAuth) + +if(BUILD_TESTING) + add_test_executable(guest_np_auth_authorized_app_code_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpAuthAuthorizedAppCode.cpp) + target_include_directories(guest_np_auth_authorized_app_code_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_auth_authorized_app_code_tests PRIVATE libSceNpAuth) + add_test(NAME guest_np_auth_authorized_app_code COMMAND guest_np_auth_authorized_app_code_tests) + if(WIN32) + set_tests_properties(guest_np_auth_authorized_app_code PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceNpAuth/Export.cpp b/core/libs/prx/libSceNpAuth/Export.cpp index 1ef3f03a69..2e9ece6558 100644 --- a/core/libs/prx/libSceNpAuth/Export.cpp +++ b/core/libs/prx/libSceNpAuth/Export.cpp @@ -1,63 +1,157 @@ -#include -#include #include +#include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -static constexpr int SCE_NP_ERROR_INVALID_ARGUMENT = static_cast(0x80550003); +static constexpr int ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT = static_cast(0x80550301); static constexpr int SCE_NP_ERROR_SIGNED_OUT = static_cast(0x80550006); +static constexpr int ORBIS_NP_AUTH_ERROR_ABORTED = static_cast(0x80550304); +static constexpr int ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND = static_cast(0x80550306); +static constexpr int ORBIS_NP_AUTH_ERROR_INVALID_ID = static_cast(0x80550307); static constexpr int NP_POLL_ASYNC_FINISHED = 0; +static constexpr int NP_POLL_ASYNC_RUNNING = 1; -static std::atomic g_nextRequest{1}; +namespace { + +struct RequestInfo { + bool isAsync = false; + bool processed = false; + bool aborted = false; + bool polled = false; + int result = SCE_NP_ERROR_SIGNED_OUT; +}; + +std::mutex g_requestMutex; +std::unordered_map g_requests; +std::atomic g_nextRequest{1}; + +int CreateRequest(bool isAsync) { + const int reqId = g_nextRequest.fetch_add(1, std::memory_order_relaxed); + std::lock_guard lock(g_requestMutex); + g_requests[reqId] = {isAsync, false, false, false, SCE_NP_ERROR_SIGNED_OUT}; + return reqId; +} + +} extern "C" { int APS5_VABI sceNpAuthAbortRequest(int req_id) { - (void)req_id; + std::lock_guard lock(g_requestMutex); + auto it = g_requests.find(req_id); + if (it == g_requests.end()) return ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND; + if (!it->second.processed) it->second.aborted = true; return 0; } int APS5_VABI sceNpAuthCreateAsyncRequest(const void* param) { (void)param; - return g_nextRequest.fetch_add(1, std::memory_order_relaxed); + return CreateRequest(true); } int APS5_VABI sceNpAuthCreateRequest(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return CreateRequest(false); } int APS5_VABI sceNpAuthDeleteRequest(int req_id) { - (void)req_id; + std::lock_guard lock(g_requestMutex); + g_requests.erase(req_id); return 0; } +int APS5_VABI sceNpAuthGetAuthorizedAppCode() { + return SCE_NP_ERROR_SIGNED_OUT; +} + int APS5_VABI sceNpAuthGetAuthorizationCodeV3(int req_id, const void* param, void* auth_code, int* issuer_id) { - (void)req_id; (void)issuer_id; - if (!param || !auth_code) return SCE_NP_ERROR_INVALID_ARGUMENT; + if (!param || !auth_code) return ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_requestMutex); + auto it = g_requests.find(req_id); + if (it == g_requests.end()) return ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND; + if (it->second.processed) return ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT; + if (it->second.aborted) { + it->second.result = ORBIS_NP_AUTH_ERROR_ABORTED; + return ORBIS_NP_AUTH_ERROR_ABORTED; + } + it->second.processed = true; + if (it->second.isAsync) { + it->second.result = SCE_NP_ERROR_SIGNED_OUT; + return 0; + } return SCE_NP_ERROR_SIGNED_OUT; } int APS5_VABI sceNpAuthGetIdTokenV3(int req_id, const void* param, void* id_token) { - (void)req_id; - (void)param; - (void)id_token; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!param || !id_token) return ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_requestMutex); + auto it = g_requests.find(req_id); + if (it == g_requests.end()) return ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND; + if (it->second.processed) return ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT; + if (it->second.aborted) { + it->second.result = ORBIS_NP_AUTH_ERROR_ABORTED; + return ORBIS_NP_AUTH_ERROR_ABORTED; + } + it->second.processed = true; + if (it->second.isAsync) { + it->second.result = SCE_NP_ERROR_SIGNED_OUT; + return 0; + } + return SCE_NP_ERROR_SIGNED_OUT; +} + +int APS5_VABI 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) { - (void)req_id; - if (result) *result = SCE_NP_ERROR_SIGNED_OUT; + std::lock_guard lock(g_requestMutex); + auto it = g_requests.find(req_id); + if (it == g_requests.end()) return ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND; + if (!it->second.isAsync || (!it->second.processed && !it->second.aborted)) return ORBIS_NP_AUTH_ERROR_INVALID_ID; + if (!it->second.polled) { + it->second.polled = true; + return NP_POLL_ASYNC_RUNNING; + } + if (result) *result = it->second.result; return NP_POLL_ASYNC_FINISHED; } +int APS5_VABI sceNpAuthSetTimeout(int req_id, int resolve_retry, uint32_t resolve_timeout, uint32_t connect_timeout, uint32_t send_timeout, uint32_t receive_timeout) { + (void)resolve_retry; + (void)resolve_timeout; + (void)connect_timeout; + (void)send_timeout; + (void)receive_timeout; + std::lock_guard lock(g_requestMutex); + return g_requests.contains(req_id) ? 0 : ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND; +} + int APS5_VABI sceNpAuthWaitAsync(int req_id, int* result) { - (void)req_id; - (void)result; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!result) return ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_requestMutex); + auto it = g_requests.find(req_id); + if (it == g_requests.end()) return ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND; + if (!it->second.isAsync || (!it->second.processed && !it->second.aborted)) return ORBIS_NP_AUTH_ERROR_INVALID_ID; + *result = it->second.result; + return NP_POLL_ASYNC_FINISHED; +} + +int APS5_VABI sceNpAuthGetAuthorizationCodeA(int req_id, const void* param, void* auth_code, int* issuer_id) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpAuthGetIdTokenA(int req_id, const void* param, void* token) { + NotImplemented_nid_no_patch(__func__); + return 0; } } diff --git a/core/libs/prx/libSceNpAuthAuthorizedAppDialog/CMakeLists.txt b/core/libs/prx/libSceNpAuthAuthorizedAppDialog/CMakeLists.txt new file mode 100644 index 0000000000..3d65cfe796 --- /dev/null +++ b/core/libs/prx/libSceNpAuthAuthorizedAppDialog/CMakeLists.txt @@ -0,0 +1,14 @@ +add_library(libSceNpAuthAuthorizedAppDialog SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceNpAuthAuthorizedAppDialog PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceNpAuthAuthorizedAppDialog) + +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) + 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 + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceNpAuthAuthorizedAppDialog/Export.cpp b/core/libs/prx/libSceNpAuthAuthorizedAppDialog/Export.cpp new file mode 100644 index 0000000000..f1cbb534c2 --- /dev/null +++ b/core/libs/prx/libSceNpAuthAuthorizedAppDialog/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 sceNpAuthAuthorizedAppDialogInitialize(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 sceNpAuthAuthorizedAppDialogOpen(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 sceNpAuthAuthorizedAppDialogUpdateStatus(void) { + return g_status.load(); +} + +int APS5_VABI sceNpAuthAuthorizedAppDialogGetStatus(void) { + return g_status.load(); +} + +int APS5_VABI sceNpAuthAuthorizedAppDialogGetResult(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 sceNpAuthAuthorizedAppDialogClose(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 sceNpAuthAuthorizedAppDialogTerminate(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/libSceNpCommerce/CMakeLists.txt b/core/libs/prx/libSceNpCommerce/CMakeLists.txt new file mode 100644 index 0000000000..ec34bf1230 --- /dev/null +++ b/core/libs/prx/libSceNpCommerce/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceNpCommerce SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceNpCommerce PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceNpCommerce) + +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) + 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..2fd73aeb90 100644 --- a/core/libs/prx/libSceNpCommerce/Export.cpp +++ b/core/libs/prx/libSceNpCommerce/Export.cpp @@ -80,6 +80,11 @@ int APS5_VABI sceNpCommerceSetPsStoreIconLayout(int layout) { } int APS5_VABI sceNpCommerceDialogOpen2(void) { + 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..c9d6e53ffd --- /dev/null +++ b/core/libs/prx/libSceNpCommon/PendingExports.cpp @@ -0,0 +1,758 @@ +#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 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..6238d389f8 --- /dev/null +++ b/core/libs/prx/libSceNpCppWebApi/Export.cpp @@ -0,0 +1,1893 @@ +#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; +} +} diff --git a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp index 0cb5f3dd0a..ba654ff02f 100644 --- a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp +++ b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp @@ -12,6 +12,8 @@ #include #ifdef _WIN32 #include +#elif defined(__APPLE__) +#include #endif static constexpr int SCE_NP_ENTITLEMENT_ACCESS_ERROR_PARAMETER = static_cast(0x80558003); @@ -28,6 +30,15 @@ std::filesystem::path EntitlementsPath() { const auto length = GetModuleFileNameW(nullptr, module, MAX_PATH); if (length == 0 || length == MAX_PATH) throw std::runtime_error("NpEntitlementAccess: cannot locate the executable"); return std::filesystem::path(module).parent_path() / "anyps5-entitlements.ini"; +#elif defined(__APPLE__) + std::uint32_t size = 0; + _NSGetExecutablePath(nullptr, &size); + std::vector executable(size); + std::error_code error; + std::filesystem::path path; + if (_NSGetExecutablePath(executable.data(), &size) == 0) path = std::filesystem::canonical(executable.data(), error); + if (path.empty()) throw std::runtime_error("NpEntitlementAccess: cannot locate the executable"); + return path.parent_path() / "anyps5-entitlements.ini"; #else return std::filesystem::read_symlink("/proc/self/exe").parent_path() / "anyps5-entitlements.ini"; #endif @@ -135,27 +146,32 @@ int APS5_VABI sceNpEntitlementAccessRequestConsumeServiceEntitlement(void) { int APS5_VABI sceNpEntitlementAccessPollServiceEntitlementInfo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return SCE_NP_ERROR_SIGNED_OUT; } int APS5_VABI sceNpEntitlementAccessPollUnifiedEntitlementInfo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return SCE_NP_ERROR_SIGNED_OUT; } int APS5_VABI sceNpEntitlementAccessRequestServiceEntitlementInfo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return SCE_NP_ERROR_SIGNED_OUT; } int APS5_VABI sceNpEntitlementAccessRequestUnifiedEntitlementInfo(void) { - NotImplemented_nid_no_patch(__func__); + return SCE_NP_ERROR_SIGNED_OUT; +} + +int APS5_VABI sceNpEntitlementAccessGetPftFlag(uint32_t* flag) { + constexpr int errorParameter = static_cast(0x817D0002); + if (!flag) return errorParameter; + *flag = 0; return 0; } -int APS5_VABI sceNpEntitlementAccessGetPftFlag(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpEntitlementAccessGetGameTrialsFlag(uint32_t* flag) { + constexpr int errorParameter = static_cast(0x817D0002); + if (!flag) return errorParameter; + *flag = 0; return 0; } @@ -175,4 +191,24 @@ int APS5_VABI sceNpEntitlementAccessRequestUnifiedEntitlementInfoList() { 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..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceNpEulaDialog/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/libSceNpFriendListDialog/Export.cpp b/core/libs/prx/libSceNpFriendListDialog/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceNpFriendListDialog/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/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 e086b9ebfe..bdda18c9b9 100644 --- a/core/libs/prx/libSceNpManager/Export.cpp +++ b/core/libs/prx/libSceNpManager/Export.cpp @@ -2,8 +2,11 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libSceUserService/UserService.hpp" +#include #include #include +#include // PSN is not emulated: the user is reported as signed out and online queries fail. static constexpr int SCE_NP_ERROR_INVALID_ARGUMENT = static_cast(0x80550003); @@ -11,6 +14,8 @@ 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_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; @@ -21,6 +26,36 @@ namespace { std::mutex reachabilityMutex; void* reachabilityCallback = nullptr; void* reachabilityUserdata = nullptr; +std::mutex requestMutex; +std::unordered_set liveRequests; + +int AddRequest() { + const int reqId = g_nextRequest.fetch_add(1, std::memory_order_relaxed); + std::lock_guard lock(requestMutex); + liveRequests.insert(reqId); + return reqId; +} + +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" { @@ -31,6 +66,19 @@ int APS5_VABI sceNpAbortRequest(int req_id) { } 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; } @@ -55,15 +103,16 @@ int APS5_VABI sceNpCheckPremium(int req_id, const NpCheckPremiumParameter* param int APS5_VABI sceNpCreateAsyncRequest(const NpCreateAsyncRequestParameter* param) { (void)param; - return g_nextRequest.fetch_add(1, std::memory_order_relaxed); + return AddRequest(); } int APS5_VABI sceNpCreateRequest(void) { - return g_nextRequest.fetch_add(1, std::memory_order_relaxed); + return AddRequest(); } int APS5_VABI sceNpDeleteRequest(int req_id) { - (void)req_id; + std::lock_guard lock(requestMutex); + liveRequests.erase(req_id); return 0; } @@ -76,7 +125,7 @@ int APS5_VABI sceNpGetAccountAge(int req_id, int user_id, uint8_t* age) { 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; } @@ -101,7 +150,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; } @@ -126,9 +175,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) { @@ -160,9 +208,11 @@ 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; } @@ -178,7 +228,27 @@ int APS5_VABI sceNpSetNpTitleId(const NpTitleId* title_id, const NpTitleSecret* return 0; } +int APS5_VABI sceNpSetTimeout(int reqId, int32_t resolveRetry, uint32_t resolveTimeout, uint32_t connTimeout, + uint32_t sendTimeout, uint32_t recvTimeout) { + constexpr uint32_t minResolveTimeout = 1000000; + constexpr uint32_t minTransferTimeout = 10000000; + auto belowMinimum = [](uint32_t timeout, uint32_t minimum) { return timeout != 0 && timeout < minimum; }; + if (reqId <= 0 || resolveRetry < 0) return SCE_NP_ERROR_INVALID_ARGUMENT; + if (resolveRetry == 0 && resolveTimeout == 0 && connTimeout == 0 && sendTimeout == 0 && recvTimeout == 0) { + return SCE_NP_ERROR_INVALID_ARGUMENT; + } + if (belowMinimum(resolveTimeout, minResolveTimeout) || belowMinimum(connTimeout, minTransferTimeout) || + belowMinimum(sendTimeout, minTransferTimeout) || belowMinimum(recvTimeout, minTransferTimeout)) { + return SCE_NP_ERROR_INVALID_ARGUMENT; + } + std::lock_guard lock(requestMutex); + return liveRequests.count(reqId) ? 0 : SCE_NP_ERROR_REQUEST_NOT_FOUND; +} + int APS5_VABI sceNpUnregisterStateCallback(void) { + std::lock_guard lock(stateMutex); + if (!stateCallback.callback) return SCE_NP_ERROR_CALLBACK_NOT_REGISTERED; + stateCallback = {}; return 0; } @@ -194,14 +264,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; } @@ -209,7 +291,33 @@ int APS5_VABI sceNpUnregisterPremiumEventCallback(void) { return 0; } -int APS5_VABI sceNpGetUserIdByAccountId() { +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, void* buffer) { + (void)userId; + (void)buffer; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +APS5_EXPORT("WxLcN76q19o", sceNpUnknown_WxLcN76q19o); +int APS5_VABI sceNpUnknown_WxLcN76q19o(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + + +int APS5_VABI sceNpCheckCallbackForLib() { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceNpPartner001/CMakeLists.txt b/core/libs/prx/libSceNpPartner001/CMakeLists.txt new file mode 100644 index 0000000000..c339e97d94 --- /dev/null +++ b/core/libs/prx/libSceNpPartner001/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceNpPartner001 SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceNpPartner001 PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceNpPartner001) + +if(BUILD_TESTING) + add_test_executable(guest_np_partner_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpPartner.cpp) + target_include_directories(guest_np_partner_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_partner_tests PRIVATE libSceNpPartner001 libc) + configure_windows_unwind(guest_np_partner_tests) + add_test(NAME guest_np_partner COMMAND guest_np_partner_tests) + if(WIN32) + set_tests_properties(guest_np_partner PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceNpPartner001/Export.cpp b/core/libs/prx/libSceNpPartner001/Export.cpp index 74e81b2df4..d4dbeeefd9 100644 --- a/core/libs/prx/libSceNpPartner001/Export.cpp +++ b/core/libs/prx/libSceNpPartner001/Export.cpp @@ -1,14 +1,31 @@ #include "prx/libc/include/General.hpp" +#include +#include +#include + +namespace { +std::atomic g_initialized{false}; +} extern "C" { int APS5_VABI sceNpEAAccessInitialize(void) { - NotImplemented_nid_no_patch(__func__); + bool expected = false; + if (!g_initialized.compare_exchange_strong(expected, true)) throw std::logic_error(std::string(__func__) + ": already initialized"); return 0; } int APS5_VABI sceNpEAAccessTerminate(void) { - NotImplemented_nid_no_patch(__func__); + bool expected = true; + if (!g_initialized.compare_exchange_strong(expected, false)) throw std::logic_error(std::string(__func__) + ": not initialized"); + return 0; +} + +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..67fdadc679 --- /dev/null +++ b/core/libs/prx/libSceNpParty/Export.cpp @@ -0,0 +1,145 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +namespace { + +constexpr int NP_PARTY_ERROR_INVALID_ARGUMENT = static_cast(0x80552504u); +constexpr std::uint16_t NP_PARTY_STATE_NOT_IN_PARTY = 2; + +} + +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) { + NotImplemented_nid_no_patch(__func__); + 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) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyGetMemberInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyGetMemberInfoA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyGetMemberSessionInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyGetMemberVoiceInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyGetMembers(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyGetMembersA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyGetStateAsUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyGetStateAsUserA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyGetVoiceChatPriority(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyInitialize(void) { + NotImplemented_nid_no_patch(__func__); + 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(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyRegisterPrivateHandler(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartySendBinaryMessage(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +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) { + NotImplemented_nid_no_patch(__func__); + 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..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceNpProfileDialog/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/libSceNpRemotePlaySessionSignaling/Export.cpp b/core/libs/prx/libSceNpRemotePlaySessionSignaling/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceNpRemotePlaySessionSignaling/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/libSceNpSessionSignaling/Export.cpp b/core/libs/prx/libSceNpSessionSignaling/Export.cpp index b28941f1b6..7dfa95ff73 100644 --- a/core/libs/prx/libSceNpSessionSignaling/Export.cpp +++ b/core/libs/prx/libSceNpSessionSignaling/Export.cpp @@ -6,6 +6,7 @@ // Peer-to-peer signaling needs the network: contexts exist, but sessions never activate. static constexpr int SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT = static_cast(0x80553303); +static constexpr int SCE_NP_SESSION_SIGNALING_ERROR_CONN_NOT_FOUND = static_cast(0x8055330C); static constexpr int SCE_NP_SESSION_SIGNALING_ERROR_UNAVAILABLE = static_cast(0x80552D06); static std::atomic g_nextContext{1}; @@ -60,11 +61,10 @@ int APS5_VABI sceNpSessionSignalingTerminate(void) { int32_t APS5_VABI sceNpSessionSignalingGetConnectionStatus(int32_t context_id, int32_t connection_id, int32_t* status, void* peer_address, uint16_t* peer_port) { (void)context_id; (void)connection_id; - (void)status; (void)peer_address; (void)peer_port; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!status) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; + return SCE_NP_SESSION_SIGNALING_ERROR_CONN_NOT_FOUND; } int32_t APS5_VABI sceNpSessionSignalingGetLocalNetInfo(int32_t context_id, void* info) { @@ -93,4 +93,55 @@ int APS5_VABI sceNpSessionSignalingGetConnectionFromPeerAddress(void) { NotImplemented_nid_no_patch(__func__); return 0; } + +int APS5_VABI sceNpSessionSignalingGetGroupFromSessionId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSessionSignalingGetGroupInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSessionSignalingGetConnectionFromNetAddress2() { + 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/libSceNpSns/Export.cpp b/core/libs/prx/libSceNpSns/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceNpSns/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/libSceNpSnsFacebookDialog/Export.cpp b/core/libs/prx/libSceNpSnsFacebookDialog/Export.cpp new file mode 100644 index 0000000000..58bb27df19 --- /dev/null +++ b/core/libs/prx/libSceNpSnsFacebookDialog/Export.cpp @@ -0,0 +1,18 @@ +#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; +} + +} diff --git a/core/libs/prx/libSceNpSnsYouTubeDialog/Export.cpp b/core/libs/prx/libSceNpSnsYouTubeDialog/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceNpSnsYouTubeDialog/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/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..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceNpTcs/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/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/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..6a853b4c28 100644 --- a/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Constants.hpp +++ b/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Constants.hpp @@ -29,7 +29,8 @@ static constexpr std::uint32_t NP_TROPHY2_PROGRESS_PERCENTAGE = 0; static constexpr std::int32_t NP_TROPHY2_TROPHY_GRADE_BRONZE = 4; static constexpr std::int32_t NP_TROPHY2_PROGRESS_TYPE_NONE = 0; -static constexpr std::uint32_t NP_TROPHY2_PROGRESS_VALUE_NONE = 0; +static constexpr std::int32_t NP_TROPHY2_PROGRESS_TYPE_VALUE = 1; +static constexpr std::uint64_t NP_TROPHY2_PROGRESS_VALUE_NONE = 0; static constexpr std::size_t NP_TROPHY2_ICON_SIZE_NONE = 0; diff --git a/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Types.hpp b/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Types.hpp index 30fadecda7..ecfde98357 100644 --- a/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Types.hpp +++ b/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Types.hpp @@ -4,7 +4,11 @@ #include #include -struct NpTrophy2Progress { std::uint32_t value; }; +struct NpTrophy2Progress { + std::int32_t type; + std::uint8_t reserved[4]; + std::uint64_t value; +}; struct NpTrophy2GameDetails { std::uint32_t num_groups; @@ -67,4 +71,12 @@ struct NpTrophy2Data { std::uint64_t timestamp_tick; }; +static_assert(sizeof(NpTrophy2Progress) == 16); +static_assert(sizeof(NpTrophy2GameDetails) == 152); +static_assert(sizeof(NpTrophy2GameData) == 24); +static_assert(sizeof(NpTrophy2GroupDetails) == 152); +static_assert(sizeof(NpTrophy2GroupData) == 32); +static_assert(sizeof(NpTrophy2Details) == 1312); +static_assert(sizeof(NpTrophy2Data) == 32); + #endif diff --git a/core/libs/prx/libSceNpTrophy2/include/TitleTrophies.hpp b/core/libs/prx/libSceNpTrophy2/include/TitleTrophies.hpp new file mode 100644 index 0000000000..9d4653450c --- /dev/null +++ b/core/libs/prx/libSceNpTrophy2/include/TitleTrophies.hpp @@ -0,0 +1,73 @@ +#ifndef CORE_LIBS_PRX_LIBSCENPTROPHY2_INCLUDE_TITLETROPHIES_HPP +#define CORE_LIBS_PRX_LIBSCENPTROPHY2_INCLUDE_TITLETROPHIES_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "prx/libSceNpTrophy2/include/TrophyPackage.hpp" +#include "prx/libSceNpTrophy2/include/TrophyStore.hpp" + +namespace NpTrophy2 { + +inline constexpr std::uint64_t MaxUcpSize = std::uint64_t{256} << 20u; + +struct TitleTrophies { + std::optional package; + std::optional rules; +}; + +std::string PackagePath(std::uint32_t serviceLabel); +std::string StatRulesPath(std::uint32_t serviceLabel); +TitleTrophies LoadTitleTrophies(std::uint32_t serviceLabel, std::string_view language); +const TitleTrophies& Title(std::uint32_t serviceLabel); +const TrophyPackage* Package(std::uint32_t serviceLabel); +void CopyText(char* destination, std::size_t size, std::string_view text); + +using UnlockedQuery = std::function; + +UnlockedQuery UnlockedBy(std::int32_t userId, std::uint32_t serviceLabel); + +struct StatUpdate { + std::uint64_t statId = 0; + std::uint64_t value = 0; + std::string input; +}; + +struct StatProblem { + std::string rule; + std::string reason; +}; + +struct StatChanges { + std::vector updates; + std::vector problems; +}; + +StatChanges ApplyStatRules(const StatRules& rules, const NpTrophy2Event& event, StatStore& stats, std::int32_t userId, std::uint32_t serviceLabel); +std::vector ReachedTrophies(const TrophyPackage& package, const std::map& stats, const UnlockedQuery& unlocked); +std::optional PlatinumToUnlock(const TrophyPackage& package, std::int32_t trophyId, const UnlockedQuery& unlocked); +std::optional PostedUnlockRefusal(const TrophyPackage& package, std::int32_t trophyId); +std::optional ProgressTarget(const TrophyDefinition& trophy); +std::uint64_t TrophyProgress(const TrophyDefinition& trophy, std::optional stat, bool unlocked); + +struct TrophyCounts { + std::uint32_t trophies = 0; + std::uint32_t unlocked = 0; + std::array grades{}; + std::array unlockedGrades{}; + + std::uint32_t Percentage() const; +}; + +TrophyCounts CountTrophies(const TrophyPackage& package, std::optional group, const UnlockedQuery& unlocked); + +} + +#endif diff --git a/core/libs/prx/libSceNpTrophy2/include/TrophyPackage.hpp b/core/libs/prx/libSceNpTrophy2/include/TrophyPackage.hpp new file mode 100644 index 0000000000..4be4238929 --- /dev/null +++ b/core/libs/prx/libSceNpTrophy2/include/TrophyPackage.hpp @@ -0,0 +1,117 @@ +#ifndef CORE_LIBS_PRX_LIBSCENPTROPHY2_INCLUDE_TROPHYPACKAGE_HPP +#define CORE_LIBS_PRX_LIBSCENPTROPHY2_INCLUDE_TROPHYPACKAGE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace NpTrophy2 { + +inline constexpr std::uint32_t UcpMagic = 0xB228C60Au; +inline constexpr std::uint32_t UcpVersion = 1; +inline constexpr std::uint64_t UcpHeaderSize = 0x40; +inline constexpr std::uint64_t UcpTocHeaderSize = 0x20; +inline constexpr std::uint64_t UcpEntrySize = 0x40; +inline constexpr std::size_t UcpNameSize = 0x20; +inline constexpr std::uint64_t UcpMaxEntries = 4096; + +using UcpFiles = std::map>; + +UcpFiles ReadUcp(std::string_view data); + +class Json { +public: + enum class Kind { Null, Bool, Number, String, Array, Object }; + + Kind kind = Kind::Null; + bool boolean = false; + std::string text; + std::vector items; + std::vector> members; + + const Json* Find(std::string_view key) const; + bool IsTrue() const; + std::optional Unsigned() const; + std::optional Id() const; +}; + +Json ParseJson(std::string_view text); + +inline constexpr std::int32_t GradePlatinum = 1; +inline constexpr std::int32_t GradeGold = 2; +inline constexpr std::int32_t GradeSilver = 3; +inline constexpr std::int32_t GradeBronze = 4; +inline constexpr std::int32_t BaseGroup = -1; +inline constexpr std::size_t MaxTrophies = 1000; +inline constexpr std::size_t MaxGroups = 50; + +enum class Comparator { None, GreaterEqual, Greater, LessEqual, Less }; + +struct UnlockCondition { + std::uint64_t statId = 0; + std::uint64_t target = 0; + Comparator comparator = Comparator::None; + + bool Reached(std::uint64_t value) const; +}; + +struct TrophyDefinition { + std::int32_t id = 0; + std::int32_t grade = GradeBronze; + std::int32_t groupId = BaseGroup; + bool hidden = false; + bool hasReward = false; + bool progressive = false; + std::optional platinumId; + std::optional statId; + std::optional target; + Comparator comparator = Comparator::None; + std::string name; + std::string detail; + std::string reward; + + std::optional Condition() const; +}; + +struct TrophyPackage { + std::string language; + std::string title; + std::map trophies; + std::map groups; + std::vector skipped; +}; + +TrophyPackage ParseTrophyPackage(std::string_view ucp, std::string_view language); + +enum class StatType { UInt32, UInt64 }; + +struct StatDefinition { + StatType type = StatType::UInt64; + std::uint64_t minimum = 0; + std::uint64_t maximum = UINT64_MAX; +}; + +struct StatRule { + std::string input; + std::uint64_t statId = 0; +}; + +struct StatRules { + std::map stats; + std::map, std::less<>> events; + std::vector skipped; +}; + +StatRules ParseStatRules(std::string_view ucp); + +std::string_view LanguageTag(int consoleLanguage); + +} + +#endif diff --git a/core/libs/prx/libSceNpTrophy2/include/TrophyStore.hpp b/core/libs/prx/libSceNpTrophy2/include/TrophyStore.hpp new file mode 100644 index 0000000000..d9ab341ae0 --- /dev/null +++ b/core/libs/prx/libSceNpTrophy2/include/TrophyStore.hpp @@ -0,0 +1,110 @@ +#ifndef CORE_LIBS_PRX_LIBSCENPTROPHY2_INCLUDE_TROPHYSTORE_HPP +#define CORE_LIBS_PRX_LIBSCENPTROPHY2_INCLUDE_TROPHYSTORE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct NpTrophy2Unlock { + std::string titleId; + std::int32_t userId = 0; + std::uint32_t serviceLabel = 0; + std::int32_t trophyId = 0; + std::uint64_t tick = 0; +}; + +using NpTrophy2UnlockListener = void (*)(void* context, const NpTrophy2Unlock& unlock); + +struct NpTrophy2EventProperty { + std::string path; + std::optional value; + std::string text; +}; + +struct NpTrophy2Event { + std::string name; + std::vector properties; +}; + +namespace NpTrophy2 { + +inline constexpr char TrophyRootGuest[] = "/_trophy"; +inline constexpr std::uint64_t UnixEpochTick = 62135596800000000ull; + +std::uint64_t CurrentTick(); + +class TrophyStore { +public: + explicit TrophyStore(std::filesystem::path directory); + + bool Unlock(std::int32_t userId, std::uint32_t serviceLabel, std::int32_t trophyId, std::uint64_t tick); + std::optional UnlockTick(std::int32_t userId, std::uint32_t serviceLabel, std::int32_t trophyId); + std::size_t UnlockCount(std::int32_t userId, std::uint32_t serviceLabel); + std::filesystem::path RecordPath(std::int32_t userId, std::uint32_t serviceLabel) const; + +private: + using Key = std::pair; + using Unlocks = std::map; + + Unlocks& record(const Key& key); + void save(const Key& key, const Unlocks& unlocks) const; + + std::filesystem::path directory; + std::mutex mutex; + std::map records; +}; + +TrophyStore& TitleStore(); + +class StatStore { +public: + explicit StatStore(std::filesystem::path directory); + + void Set(std::int32_t userId, std::uint32_t serviceLabel, std::uint64_t statId, std::uint64_t value); + std::optional Value(std::int32_t userId, std::uint32_t serviceLabel, std::uint64_t statId); + std::map Values(std::int32_t userId, std::uint32_t serviceLabel); + std::filesystem::path RecordPath(std::int32_t userId, std::uint32_t serviceLabel) const; + +private: + using Key = std::pair; + using Stats = std::map; + + Stats& record(const Key& key); + + std::filesystem::path directory; + std::mutex mutex; + std::map records; +}; + +StatStore& TitleStats(); + +struct ContextOwner { + std::int32_t userId = 0; + std::uint32_t serviceLabel = 0; +}; + +int CreateContext(std::int32_t userId, std::uint32_t serviceLabel); +void DestroyContext(int context); +std::optional FindContext(int context); +std::optional ContextUnlockTick(int context, std::int32_t trophyId); +std::size_t ContextUnlockCount(int context); +bool RecordUnlock(std::int32_t userId, std::uint32_t serviceLabel, std::int32_t trophyId); + +} + +extern "C" { + +bool NpTrophy2RecordUnlock_nid_no_patch(std::int32_t userId, std::uint32_t serviceLabel, std::int32_t trophyId); +std::uint64_t NpTrophy2AddUnlockListener_nid_no_patch(NpTrophy2UnlockListener listener, void* context); +void NpTrophy2RemoveUnlockListener_nid_no_patch(std::uint64_t id); +void NpTrophy2ApplyEvent_nid_no_patch(std::int32_t userId, std::uint32_t serviceLabel, const NpTrophy2Event& event); + +} + +#endif diff --git a/core/libs/prx/libSceNpTrophy2/src/Context.cpp b/core/libs/prx/libSceNpTrophy2/src/Context.cpp index 0a7bd1dafb..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,14 +78,19 @@ 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; } -int APS5_VABI sceNpTrophy2ShowTrophyList(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpTrophy2ShowTrophyList(int context) { + (void)context; + return SCE_NP_TROPHY2_OK; } } 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 09fde57f51..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) { + 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 0f19c97822..ce29da1513 100644 --- a/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp @@ -1,39 +1,92 @@ +#include #include #include #include +#include #include #include #include "prx/libSceNpTrophy2/include/NpTrophy2.hpp" +#include "prx/libSceNpTrophy2/include/TitleTrophies.hpp" +#include "prx/libSceNpTrophy2/include/TrophyStore.hpp" #include "prx/libc/include/General.hpp" +namespace { + +void fillPlaceholderDetails(int trophyId, NpTrophy2Details* details) { + std::memset(details, 0, sizeof(*details)); + details->trophy_id = trophyId; + details->trophy_grade = NP_TROPHY2_TROPHY_GRADE_BRONZE; + details->group_id = NP_TROPHY2_GROUP_ID_BASE; + details->hidden = false; + details->has_reward = false; + details->target.type = NP_TROPHY2_PROGRESS_TYPE_NONE; + details->target.value = NP_TROPHY2_PROGRESS_VALUE_NONE; + std::strncpy(details->name, NP_TROPHY2_TROPHY_NAME, sizeof(details->name) - 1); + std::strncpy(details->description, NP_TROPHY2_TROPHY_DESCRIPTION, sizeof(details->description) - 1); + std::strncpy(details->reward, NP_TROPHY2_TROPHY_REWARD, sizeof(details->reward) - 1); +} + +void fillPlaceholderData(int context, int trophyId, NpTrophy2Data* data) { + std::memset(data, 0, sizeof(*data)); + const auto tick = NpTrophy2::ContextUnlockTick(context, trophyId); + data->trophy_id = trophyId; + data->unlocked = tick.has_value(); + data->progress.type = NP_TROPHY2_PROGRESS_TYPE_NONE; + data->progress.value = NP_TROPHY2_PROGRESS_VALUE_NONE; + data->timestamp_tick = tick.value_or(0); +} + +void fillDetails(const NpTrophy2::TrophyDefinition& trophy, NpTrophy2Details* details) { + std::memset(details, 0, sizeof(*details)); + details->trophy_id = trophy.id; + details->trophy_grade = trophy.grade; + details->group_id = trophy.groupId; + details->hidden = trophy.hidden; + details->has_reward = trophy.hasReward; + if (const auto target = NpTrophy2::ProgressTarget(trophy)) { + details->target.type = NP_TROPHY2_PROGRESS_TYPE_VALUE; + details->target.value = *target; + } + NpTrophy2::CopyText(details->name, sizeof(details->name), trophy.name); + NpTrophy2::CopyText(details->description, sizeof(details->description), trophy.detail); + NpTrophy2::CopyText(details->reward, sizeof(details->reward), trophy.reward); +} + +void fillData(const NpTrophy2::ContextOwner& owner, const NpTrophy2::TrophyDefinition& trophy, NpTrophy2Data* data) { + std::memset(data, 0, sizeof(*data)); + const auto tick = NpTrophy2::TitleStore().UnlockTick(owner.userId, owner.serviceLabel, trophy.id); + data->trophy_id = trophy.id; + data->unlocked = tick.has_value(); + data->timestamp_tick = tick.value_or(0); + if (!NpTrophy2::ProgressTarget(trophy)) return; + const auto stat = trophy.statId ? NpTrophy2::TitleStats().Value(owner.userId, owner.serviceLabel, *trophy.statId) : std::nullopt; + data->progress.type = NP_TROPHY2_PROGRESS_TYPE_VALUE; + data->progress.value = NpTrophy2::TrophyProgress(trophy, stat, data->unlocked); +} + +} + extern "C" { int APS5_VABI sceNpTrophy2GetTrophyInfo(int context, int handle, int trophy_id, NpTrophy2Details* details, NpTrophy2Data* data) { - if (details == nullptr || data == nullptr) { + if (details == nullptr && data == nullptr) { APS5_INVALID_ARG_EX; } - (void)context; (void)handle; - if (details != nullptr) { - std::memset(details, 0, sizeof(*details)); - details->trophy_id = trophy_id; - details->trophy_grade = NP_TROPHY2_TROPHY_GRADE_BRONZE; - details->group_id = NP_TROPHY2_GROUP_ID_BASE; - details->hidden = false; - details->has_reward = false; - details->target.value = NP_TROPHY2_PROGRESS_VALUE_NONE; - std::strncpy(details->name, NP_TROPHY2_TROPHY_NAME, sizeof(details->name) - 1); - std::strncpy(details->description, NP_TROPHY2_TROPHY_DESCRIPTION, sizeof(details->description) - 1); - std::strncpy(details->reward, NP_TROPHY2_TROPHY_REWARD, sizeof(details->reward) - 1); + const auto owner = NpTrophy2::FindContext(context); + const auto* package = owner ? NpTrophy2::Package(owner->serviceLabel) : nullptr; + if (package == nullptr) { + if (details != nullptr) fillPlaceholderDetails(trophy_id, details); + if (data != nullptr) fillPlaceholderData(context, trophy_id, data); + return SCE_NP_TROPHY2_OK; } - if (data != nullptr) { - std::memset(data, 0, sizeof(*data)); - data->trophy_id = trophy_id; - data->unlocked = false; - data->progress.value = NP_TROPHY2_PROGRESS_VALUE_NONE; - data->timestamp_tick = 0; + const auto trophy = package->trophies.find(trophy_id); + if (trophy == package->trophies.end()) { + throw std::invalid_argument(std::string(__func__) + ": trophy " + std::to_string(trophy_id) + " is not in " + NpTrophy2::PackagePath(owner->serviceLabel)); } + if (details != nullptr) fillDetails(trophy->second, details); + if (data != nullptr) fillData(*owner, trophy->second, data); return SCE_NP_TROPHY2_OK; } @@ -41,28 +94,22 @@ int APS5_VABI sceNpTrophy2GetTrophyInfoArray(int context, int handle, uint32_t o if (count == nullptr) { APS5_INVALID_ARG_EX; } - (void)context; (void)handle; - const uint32_t out_count = (offset == 0 && limit != 0 ? 1u : 0u); - *count = out_count; - if (out_count != 0 && details_array != nullptr) { - std::memset(details_array, 0, sizeof(*details_array)); - details_array->trophy_id = NP_TROPHY2_TROPHY_ID_DEFAULT; - details_array->trophy_grade = NP_TROPHY2_TROPHY_GRADE_BRONZE; - details_array->group_id = NP_TROPHY2_GROUP_ID_BASE; - details_array->hidden = false; - details_array->has_reward = false; - details_array->target.value = NP_TROPHY2_PROGRESS_VALUE_NONE; - std::strncpy(details_array->name, NP_TROPHY2_TROPHY_NAME, sizeof(details_array->name) - 1); - std::strncpy(details_array->description, NP_TROPHY2_TROPHY_DESCRIPTION, sizeof(details_array->description) - 1); - std::strncpy(details_array->reward, NP_TROPHY2_TROPHY_REWARD, sizeof(details_array->reward) - 1); + const auto owner = NpTrophy2::FindContext(context); + const auto* package = owner ? NpTrophy2::Package(owner->serviceLabel) : nullptr; + if (package == nullptr) { + const uint32_t out_count = (offset == 0 && limit != 0 ? 1u : 0u); + *count = out_count; + if (out_count != 0 && details_array != nullptr) fillPlaceholderDetails(NP_TROPHY2_TROPHY_ID_DEFAULT, details_array); + if (out_count != 0 && data_array != nullptr) fillPlaceholderData(context, NP_TROPHY2_TROPHY_ID_DEFAULT, data_array); + return SCE_NP_TROPHY2_OK; } - if (out_count != 0 && data_array != nullptr) { - std::memset(data_array, 0, sizeof(*data_array)); - data_array->trophy_id = NP_TROPHY2_TROPHY_ID_DEFAULT; - data_array->unlocked = false; - data_array->progress.value = NP_TROPHY2_PROGRESS_VALUE_NONE; - data_array->timestamp_tick = 0; + *count = 0; + auto trophy = package->trophies.begin(); + std::advance(trophy, std::min(offset, package->trophies.size())); + for (; trophy != package->trophies.end() && *count < limit; ++trophy, ++*count) { + if (details_array != nullptr) fillDetails(trophy->second, &details_array[*count]); + if (data_array != nullptr) fillData(*owner, trophy->second, &data_array[*count]); } return SCE_NP_TROPHY2_OK; } diff --git a/core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp b/core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp new file mode 100644 index 0000000000..5da39d1cb4 --- /dev/null +++ b/core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp @@ -0,0 +1,592 @@ +#include "prx/libSceNpTrophy2/include/TrophyPackage.hpp" + +#include +#include +#include +#include +#include +#include + +namespace NpTrophy2 { + +namespace { + +constexpr int MaxJsonDepth = 64; +constexpr std::size_t MaxJsonValues = std::size_t{1} << 20u; + +std::uint64_t bigEndian(std::string_view data, std::uint64_t offset, std::size_t size) { + std::uint64_t value = 0; + for (std::size_t i = 0; i < size; ++i) value = (value << 8u) | static_cast(data[offset + i]); + return value; +} + +std::string hex(std::uint64_t value) { + char text[24]; + std::snprintf(text, sizeof(text), "0x%llx", static_cast(value)); + return text; +} + +void appendUtf8(std::string& out, std::uint32_t code) { + if (code < 0x80) { + out += static_cast(code); + } else if (code < 0x800) { + out += static_cast(0xC0 | (code >> 6)); + out += static_cast(0x80 | (code & 0x3F)); + } else if (code < 0x10000) { + out += static_cast(0xE0 | (code >> 12)); + out += static_cast(0x80 | ((code >> 6) & 0x3F)); + out += static_cast(0x80 | (code & 0x3F)); + } else { + out += static_cast(0xF0 | (code >> 18)); + out += static_cast(0x80 | ((code >> 12) & 0x3F)); + out += static_cast(0x80 | ((code >> 6) & 0x3F)); + out += static_cast(0x80 | (code & 0x3F)); + } +} + +class JsonReader { +public: + explicit JsonReader(std::string_view text) : text(text) {} + + Json Document() { + if (text.starts_with("\xEF\xBB\xBF")) position = 3; + auto document = readValue(0); + skipSpace(); + if (position != text.size()) fail("text follows the document"); + return document; + } + +private: + [[noreturn]] void fail(const std::string& problem) const { + throw std::runtime_error(problem + " at byte " + std::to_string(position)); + } + + void skipSpace() { + while (position < text.size() && (text[position] == ' ' || text[position] == '\t' || text[position] == '\n' || text[position] == '\r')) ++position; + } + + bool take(char expected) { + if (position >= text.size() || text[position] != expected) return false; + ++position; + return true; + } + + void expect(std::string_view word) { + if (text.substr(position, word.size()) != word) fail("expected " + std::string(word)); + position += word.size(); + } + + Json readValue(int depth) { + if (depth >= MaxJsonDepth) fail("values nest deeper than " + std::to_string(MaxJsonDepth) + " levels"); + if (++values > MaxJsonValues) fail("the document holds more than " + std::to_string(MaxJsonValues) + " values"); + skipSpace(); + if (position >= text.size()) fail("the document ends before a value"); + Json result; + switch (text[position]) { + case '{': + result.kind = Json::Kind::Object; + readObject(result, depth); + break; + case '[': + result.kind = Json::Kind::Array; + readArray(result, depth); + break; + case '"': + result.kind = Json::Kind::String; + result.text = readString(); + break; + case 't': + expect("true"); + result.kind = Json::Kind::Bool; + result.boolean = true; + break; + case 'f': + expect("false"); + result.kind = Json::Kind::Bool; + break; + case 'n': + expect("null"); + break; + default: + result.kind = Json::Kind::Number; + result.text = readNumber(); + break; + } + return result; + } + + void readObject(Json& result, int depth) { + ++position; + skipSpace(); + if (take('}')) return; + while (true) { + skipSpace(); + if (position >= text.size() || text[position] != '"') fail("expected a member name"); + auto key = readString(); + skipSpace(); + if (!take(':')) fail("expected ':'"); + result.members.emplace_back(std::move(key), readValue(depth + 1)); + skipSpace(); + if (take('}')) return; + if (!take(',')) fail("expected ',' or '}'"); + } + } + + void readArray(Json& result, int depth) { + ++position; + skipSpace(); + if (take(']')) return; + while (true) { + result.items.push_back(readValue(depth + 1)); + skipSpace(); + if (take(']')) return; + if (!take(',')) fail("expected ',' or ']'"); + } + } + + std::uint32_t readHexDigits() { + if (text.size() - position < 4) fail("a \\u escape is cut short"); + std::uint32_t code = 0; + const auto* end = text.data() + position + 4; + const auto [stop, error] = std::from_chars(text.data() + position, end, code, 16); + if (error != std::errc{} || stop != end) fail("a \\u escape does not have 4 hex digits"); + position += 4; + return code; + } + + std::string readString() { + ++position; + std::string out; + while (true) { + if (position >= text.size()) fail("a string is not closed"); + const char character = text[position++]; + if (character == '"') return out; + if (static_cast(character) < 0x20) fail("a string holds a control character"); + if (character != '\\') { + out += character; + continue; + } + if (position >= text.size()) fail("a string is not closed"); + switch (text[position++]) { + case '"': out += '"'; break; + case '\\': out += '\\'; break; + case '/': out += '/'; break; + case 'b': out += '\b'; break; + case 'f': out += '\f'; break; + case 'n': out += '\n'; break; + case 'r': out += '\r'; break; + case 't': out += '\t'; break; + case 'u': { + auto code = readHexDigits(); + if (code >= 0xDC00 && code < 0xE000) fail("a low surrogate has no high surrogate"); + if (code >= 0xD800 && code < 0xDC00) { + if (text.substr(position, 2) != "\\u") fail("a high surrogate has no low surrogate"); + position += 2; + const auto low = readHexDigits(); + if (low < 0xDC00 || low >= 0xE000) fail("a high surrogate has no low surrogate"); + code = 0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00); + } + appendUtf8(out, code); + break; + } + default: + fail("a string has an unknown escape"); + } + } + } + + bool skipDigits() { + const auto start = position; + while (position < text.size() && text[position] >= '0' && text[position] <= '9') ++position; + return position != start; + } + + std::string readNumber() { + const auto start = position; + take('-'); + if (!take('0') && !skipDigits()) fail("expected a value"); + if (take('.') && !skipDigits()) fail("a number has no digits after '.'"); + if (take('e') || take('E')) { + if (!take('+')) take('-'); + if (!skipDigits()) fail("a number has no exponent digits"); + } + return std::string(text.substr(start, position - start)); + } + + std::string_view text; + std::size_t position = 0; + std::size_t values = 0; +}; + +std::string describe(const Json& value) { + switch (value.kind) { + case Json::Kind::Null: return "null"; + case Json::Kind::Bool: return value.boolean ? "true" : "false"; + case Json::Kind::Number: return value.text; + case Json::Kind::String: return "\"" + value.text + "\""; + case Json::Kind::Array: return "an array"; + case Json::Kind::Object: return "an object"; + } + throw std::logic_error("a JSON value has an unknown kind"); +} + +std::string kindName(Json::Kind kind) { + switch (kind) { + case Json::Kind::Null: return "null"; + case Json::Kind::Bool: return "boolean"; + case Json::Kind::Number: return "number"; + case Json::Kind::String: return "string"; + case Json::Kind::Array: return "array"; + case Json::Kind::Object: return "object"; + } + throw std::logic_error("a JSON value has an unknown kind"); +} + +const Json& member(const Json& object, std::string_view key, Json::Kind kind, const std::string& where) { + const auto* found = object.Find(key); + if (found == nullptr || found->kind != kind) throw std::runtime_error(where + " has no " + std::string(key) + " " + kindName(kind)); + return *found; +} + +Json document(const UcpFiles& files, const std::string& name) { + const auto found = files.find(name); + if (found == files.end()) throw std::runtime_error("it has no " + name); + Json parsed; + try { + parsed = ParseJson(found->second); + } catch (const std::runtime_error& error) { + throw std::runtime_error(name + " is not JSON: " + error.what()); + } + if (parsed.kind != Json::Kind::Object) throw std::runtime_error(name + " is not a JSON object"); + return parsed; +} + +Comparator comparatorOf(const Json& value) { + if (value.kind != Json::Kind::String) return Comparator::None; + if (value.text == "ge") return Comparator::GreaterEqual; + if (value.text == "gt") return Comparator::Greater; + if (value.text == "le") return Comparator::LessEqual; + if (value.text == "lt") return Comparator::Less; + return Comparator::None; +} + +std::string join(const std::vector& parts, std::string_view separator) { + std::string out; + for (const auto& part : parts) { + if (!out.empty()) out += separator; + out += part; + } + return out; +} + +std::string alternatives(const std::vector& parts) { + std::string out; + for (std::size_t index = 0; index < parts.size(); ++index) { + if (index != 0) out += index + 1 == parts.size() ? " or " : ", "; + out += parts[index]; + } + return out; +} + +TrophyDefinition readTrophy(const Json& definition, std::size_t index, std::vector& skipped) { + const auto where = "tropconf.json trophies[" + std::to_string(index) + "]"; + if (definition.kind != Json::Kind::Object) throw std::runtime_error(where + " is not an object"); + const auto* id = definition.Find("id"); + const auto parsedId = id != nullptr ? id->Id() : std::nullopt; + if (!parsedId) throw std::runtime_error(where + " has no id from 0 to 2147483647"); + TrophyDefinition trophy; + trophy.id = *parsedId; + const auto name = "tropconf.json trophy " + std::to_string(trophy.id); + const auto* grade = definition.Find("grade"); + const auto letter = grade != nullptr && grade->kind == Json::Kind::String && grade->text.size() == 1 ? std::string_view("PGSB").find(grade->text[0]) : std::string_view::npos; + if (letter == std::string_view::npos) throw std::runtime_error(name + " has no grade P, G, S or B"); + trophy.grade = GradePlatinum + static_cast(letter); + if (const auto* group = definition.Find("groupId")) { + const auto groupId = group->Id(); + if (!groupId) throw std::runtime_error(name + " has groupId " + describe(*group) + ", not an id from 0 to 2147483647"); + trophy.groupId = *groupId; + } + if (const auto* platinum = definition.Find("platinumTrophyId")) trophy.platinumId = platinum->Id(); + const auto* hidden = definition.Find("hidden"); + trophy.hidden = hidden != nullptr && hidden->IsTrue(); + const auto* reward = definition.Find("hasReward"); + trophy.hasReward = reward != nullptr && reward->IsTrue(); + const auto* condition = definition.Find("unlockCondition"); + if (condition == nullptr || condition->kind != Json::Kind::Object) return trophy; + const auto* progressive = condition->Find("progressive"); + trophy.progressive = progressive != nullptr && progressive->IsTrue(); + if (const auto* stat = condition->Find("udsStatId")) { + trophy.statId = stat->Unsigned(); + if (!trophy.statId) throw std::runtime_error(name + " has udsStatId " + describe(*stat) + ", not an unsigned integer"); + } + std::vector problems; + if (const auto* target = condition->Find("targetValue")) { + trophy.target = target->Unsigned(); + if (!trophy.target) problems.push_back("its targetValue " + describe(*target) + " is not an unsigned integer"); + } else if (trophy.statId) { + problems.emplace_back("it has no targetValue"); + } + if (const auto* comparator = condition->Find("comparator")) { + trophy.comparator = comparatorOf(*comparator); + if (trophy.comparator == Comparator::None) problems.push_back("its comparator " + describe(*comparator) + " is not ge, gt, le or lt"); + } else if (trophy.statId) { + problems.emplace_back("it has no comparator"); + } + if (!problems.empty()) skipped.push_back(name + " never unlocks through stats: " + join(problems, ", ")); + return trophy; +} + +std::optional propertyPath(std::string_view input) { + if (!input.starts_with("$.")) return std::nullopt; + const auto path = input.substr(2); + bool segmentStart = true; + for (const char character : path) { + if (character == '.') { + if (segmentStart) return std::nullopt; + segmentStart = true; + continue; + } + const bool word = (character >= 'a' && character <= 'z') || (character >= 'A' && character <= 'Z') || (character >= '0' && character <= '9') || character == '_'; + if (!word) return std::nullopt; + segmentStart = false; + } + if (segmentStart) return std::nullopt; + return std::string(path); +} + +void readStat(const Json& definition, std::size_t index, StatRules& rules) { + const auto* id = definition.Find("statId"); + const auto statId = id != nullptr ? id->Unsigned() : std::nullopt; + if (!statId) { + rules.skipped.push_back("stats_definition.json statDefinitionArray[" + std::to_string(index) + "] has no unsigned statId"); + return; + } + const auto name = "stat " + std::to_string(*statId); + const auto* aggregation = definition.Find("aggregation"); + if (aggregation == nullptr || aggregation->kind != Json::Kind::String || aggregation->text != "latest") { + rules.skipped.push_back(name + " has aggregation " + (aggregation != nullptr ? describe(*aggregation) : std::string("none")) + "; only \"latest\" is supported"); + return; + } + const auto* type = definition.Find("dataType"); + StatDefinition stat; + if (type != nullptr && type->kind == Json::Kind::String && type->text == "uint32") { + stat.type = StatType::UInt32; + stat.maximum = UINT32_MAX; + } else if (type != nullptr && type->kind == Json::Kind::String && type->text == "uint64") { + stat.type = StatType::UInt64; + stat.maximum = UINT64_MAX; + } else { + rules.skipped.push_back(name + " has dataType " + (type != nullptr ? describe(*type) : std::string("none")) + "; only \"uint32\" and \"uint64\" are supported"); + return; + } + if (const auto* minimum = definition.Find("minValue")) { + const auto value = minimum->Unsigned(); + if (!value || *value > stat.maximum) { + rules.skipped.push_back(name + " has minValue " + describe(*minimum) + ", which its " + type->text + " type cannot hold"); + return; + } + stat.minimum = *value; + } + if (const auto* maximum = definition.Find("maxValue")) { + const auto value = maximum->Unsigned(); + if (!value || *value > stat.maximum || *value < stat.minimum) { + rules.skipped.push_back(name + " has maxValue " + describe(*maximum) + ", which is not from its minValue to its " + type->text + " limit"); + return; + } + stat.maximum = *value; + } + if (!rules.stats.emplace(*statId, stat).second) rules.skipped.push_back(name + " is defined twice; the first definition is used"); +} + +void readRule(const Json& rule, std::size_t index, StatRules& rules) { + const auto where = "stats_extraction.json statsExtractionRuleArray[" + std::to_string(index) + "]"; + const auto* condition = rule.Find("condition"); + const auto* action = rule.Find("action"); + if (condition == nullptr || condition->kind != Json::Kind::Object || action == nullptr || action->kind != Json::Kind::Object) { + rules.skipped.push_back(where + " has no condition and action objects"); + return; + } + const auto* event = condition->Find("eventName"); + if (event == nullptr || event->kind != Json::Kind::String) { + rules.skipped.push_back(where + " has no eventName"); + return; + } + if (condition->members.size() != 1) { + rules.skipped.push_back(where + " filters " + event->text + " by more than its eventName; only eventName conditions are supported"); + return; + } + const auto* output = action->Find("output"); + const auto* id = output != nullptr ? output->Find("statId") : nullptr; + const auto statId = id != nullptr ? id->Unsigned() : std::nullopt; + if (!statId) { + rules.skipped.push_back(where + " for " + event->text + " has no unsigned output statId"); + return; + } + if (!rules.stats.contains(*statId)) { + rules.skipped.push_back(where + " for " + event->text + " writes stat " + std::to_string(*statId) + ", which has no supported definition"); + return; + } + const auto* input = action->Find("input"); + const auto path = input != nullptr && input->kind == Json::Kind::String ? propertyPath(input->text) : std::nullopt; + if (!path) { + rules.skipped.push_back(where + " for " + event->text + " reads " + (input != nullptr ? describe(*input) : std::string("no input")) + "; only \"$.name\" property paths are supported"); + return; + } + rules.events[event->text].push_back({*path, *statId}); +} + +} + +const Json* Json::Find(std::string_view key) const { + for (auto member = members.rbegin(); member != members.rend(); ++member) { + if (member->first == key) return &member->second; + } + return nullptr; +} + +bool Json::IsTrue() const { + return kind == Kind::Bool && boolean; +} + +std::optional Json::Unsigned() const { + if (kind != Kind::Number && kind != Kind::String) return std::nullopt; + std::uint64_t value = 0; + const auto* end = text.data() + text.size(); + const auto [stop, error] = std::from_chars(text.data(), end, value); + if (error != std::errc{} || stop != end) return std::nullopt; + return value; +} + +std::optional Json::Id() const { + const auto value = Unsigned(); + if (!value || *value > static_cast(INT32_MAX)) return std::nullopt; + return static_cast(*value); +} + +Json ParseJson(std::string_view text) { + return JsonReader(text).Document(); +} + +UcpFiles ReadUcp(std::string_view data) { + if (data.size() < UcpHeaderSize) throw std::runtime_error("it has " + std::to_string(data.size()) + " bytes, fewer than the " + std::to_string(UcpHeaderSize) + "-byte header"); + const auto magic = bigEndian(data, 0, 4); + if (magic != UcpMagic) throw std::runtime_error("its magic is " + hex(magic) + ", not " + hex(UcpMagic)); + const auto version = bigEndian(data, 4, 4); + if (version != UcpVersion) throw std::runtime_error("its version is " + std::to_string(version) + ", not " + std::to_string(UcpVersion)); + const auto size = bigEndian(data, 8, 8); + if (size < UcpHeaderSize || size > data.size()) throw std::runtime_error("its size " + hex(size) + " is not from the header size " + hex(UcpHeaderSize) + " to the file size " + hex(data.size())); + const auto count = bigEndian(data, 0x10, 4); + if (count > UcpMaxEntries) throw std::runtime_error("it lists " + std::to_string(count) + " files, more than " + std::to_string(UcpMaxEntries)); + const auto toc = bigEndian(data, 0x14, 4); + if (toc > size || UcpTocHeaderSize + count * UcpEntrySize > size - toc) { + throw std::runtime_error("its table of " + std::to_string(count) + " files at " + hex(toc) + " ends past its size " + hex(size)); + } + UcpFiles files; + for (std::uint64_t index = 0; index < count; ++index) { + const auto entry = toc + UcpTocHeaderSize + index * UcpEntrySize; + const auto field = data.substr(entry, UcpNameSize); + const std::string name(field.substr(0, field.find('\0'))); + const auto offset = bigEndian(data, entry + UcpNameSize, 8); + const auto length = bigEndian(data, entry + UcpNameSize + 8, 8); + if (offset > size || length > size - offset) { + throw std::runtime_error("its file \"" + name + "\" at " + hex(offset) + " with " + hex(length) + " bytes ends past its size " + hex(size)); + } + files.try_emplace(name, data.substr(offset, length)); + } + return files; +} + +bool UnlockCondition::Reached(std::uint64_t value) const { + switch (comparator) { + case Comparator::GreaterEqual: return value >= target; + case Comparator::Greater: return value > target; + case Comparator::LessEqual: return value <= target; + case Comparator::Less: return value < target; + case Comparator::None: break; + } + throw std::logic_error("an unlock condition on stat " + std::to_string(statId) + " has no comparator"); +} + +std::optional TrophyDefinition::Condition() const { + if (grade == GradePlatinum || !statId || !target || comparator == Comparator::None) return std::nullopt; + return UnlockCondition{*statId, *target, comparator}; +} + +TrophyPackage ParseTrophyPackage(std::string_view ucp, std::string_view language) { + const auto files = ReadUcp(ucp); + const auto conf = document(files, "tropconf.json"); + const auto& trophies = member(conf, "trophies", Json::Kind::Array, "tropconf.json"); + if (trophies.items.size() > MaxTrophies) throw std::runtime_error("tropconf.json lists " + std::to_string(trophies.items.size()) + " trophies, more than " + std::to_string(MaxTrophies)); + const auto& defaultLanguage = member(conf, "defaultLanguage", Json::Kind::String, "tropconf.json"); + TrophyPackage package; + const std::array candidates{language, "en-US", defaultLanguage.text}; + std::vector tried; + for (std::size_t index = 0; index < candidates.size() && package.language.empty(); ++index) { + const auto candidate = candidates[index]; + const auto earlier = candidates.begin() + static_cast(index); + if (candidate.empty() || std::find(candidates.begin(), earlier, candidate) != earlier) continue; + if (files.contains("tropmeta_" + std::string(candidate) + ".json")) { + package.language = candidate; + } else { + tried.push_back(index + 1 == candidates.size() ? "its defaultLanguage \"" + std::string(candidate) + "\"" : std::string(candidate)); + } + } + if (package.language.empty()) throw std::runtime_error("it has no tropmeta_.json for " + alternatives(tried)); + const auto metaName = "tropmeta_" + package.language + ".json"; + const auto meta = document(files, metaName); + const auto& metadata = member(meta, "metadata", Json::Kind::Object, metaName); + const auto& titleMetadata = member(metadata, "titleMetadata", Json::Kind::Object, metaName + " metadata"); + package.title = member(titleMetadata, "name", Json::Kind::String, metaName + " titleMetadata").text; + const auto& trophyMetadata = member(metadata, "trophyMetadata", Json::Kind::Array, metaName + " metadata"); + for (std::size_t index = 0; index < trophies.items.size(); ++index) { + auto trophy = readTrophy(trophies.items[index], index, package.skipped); + package.groups.try_emplace(trophy.groupId, trophy.groupId == BaseGroup ? package.title : std::string()); + if (package.groups.size() > MaxGroups) throw std::runtime_error("tropconf.json uses more than " + std::to_string(MaxGroups) + " groups"); + const auto id = trophy.id; + if (!package.trophies.emplace(id, std::move(trophy)).second) throw std::runtime_error("tropconf.json lists trophy " + std::to_string(id) + " twice"); + } + if (const auto* groups = metadata.Find("groupMetadata"); groups != nullptr && groups->kind == Json::Kind::Array) { + for (const auto& group : groups->items) { + const auto* id = group.Find("id"); + const auto* name = group.Find("name"); + const auto groupId = id != nullptr ? id->Id() : std::nullopt; + if (!groupId || name == nullptr || name->kind != Json::Kind::String) continue; + if (const auto found = package.groups.find(*groupId); found != package.groups.end()) found->second = name->text; + } + } + for (const auto& text : trophyMetadata.items) { + const auto* id = text.Find("id"); + const auto trophyId = id != nullptr ? id->Id() : std::nullopt; + const auto found = trophyId ? package.trophies.find(*trophyId) : package.trophies.end(); + if (found == package.trophies.end()) continue; + for (const auto& [key, field] : {std::pair{"name", &found->second.name}, std::pair{"detail", &found->second.detail}, std::pair{"reward", &found->second.reward}}) { + if (const auto* value = text.Find(key); value != nullptr && value->kind == Json::Kind::String) *field = value->text; + } + } + return package; +} + +StatRules ParseStatRules(std::string_view ucp) { + const auto files = ReadUcp(ucp); + const auto definitions = document(files, "stats_definition.json"); + const auto extraction = document(files, "stats_extraction.json"); + const auto& stats = member(definitions, "statDefinitionArray", Json::Kind::Array, "stats_definition.json"); + const auto& extractionRules = member(extraction, "statsExtractionRuleArray", Json::Kind::Array, "stats_extraction.json"); + StatRules rules; + for (std::size_t index = 0; index < stats.items.size(); ++index) readStat(stats.items[index], index, rules); + for (std::size_t index = 0; index < extractionRules.items.size(); ++index) readRule(extractionRules.items[index], index, rules); + return rules; +} + +std::string_view LanguageTag(int consoleLanguage) { + static constexpr std::array tags = { + "ja-JP", "en-US", "fr-FR", "es-ES", "de-DE", "it-IT", "nl-NL", "pt-PT", "ru-RU", "ko-KR", "zh-Hant", + "zh-Hans", "fi-FI", "sv-SE", "da-DK", "no-NO", "pl-PL", "pt-BR", "en-GB", "tr-TR", "es-419", "ar-AE", + "fr-CA", "cs-CZ", "hu-HU", "el-GR", "ro-RO", "th-TH", "vi-VN", "id-ID", "uk-UA"}; + if (consoleLanguage < 0 || consoleLanguage >= static_cast(tags.size())) { + throw std::out_of_range("console language " + std::to_string(consoleLanguage) + " has no locale tag"); + } + return tags[static_cast(consoleLanguage)]; +} + +} diff --git a/core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp b/core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp new file mode 100644 index 0000000000..b19804fe10 --- /dev/null +++ b/core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp @@ -0,0 +1,260 @@ +#include "prx/libSceNpTrophy2/include/TrophyStore.hpp" +#include "prx/libSceNpTrophy2/include/TitleTrophies.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace NpTrophy2 { + +namespace { + +template +bool parseNumber(std::string_view text, TValue& value) { + const auto* end = text.data() + text.size(); + const auto [stop, error] = std::from_chars(text.data(), end, value); + return error == std::errc{} && stop == end; +} + +template +bool negative(TKey key) { + if constexpr (std::is_signed_v) return key < 0; + return false; +} + +template +std::map readRecord(const std::filesystem::path& path, const std::string& what, const char* pair) { + std::map values; + std::error_code error; + const bool exists = std::filesystem::exists(path, error); + if (error) throw std::filesystem::filesystem_error("cannot check the " + what, path, error); + if (!exists) return values; + std::ifstream file(path, std::ios::binary); + if (!file) throw std::runtime_error("cannot read the " + what + " " + path.string()); + std::string line; + std::size_t number = 0; + while (std::getline(file, line)) { + ++number; + const auto space = line.find(' '); + TKey key{}; + std::uint64_t value = 0; + const std::string_view text(line); + if (space == std::string::npos || !parseNumber(text.substr(0, space), key) || negative(key) || !parseNumber(text.substr(space + 1), value) || !values.emplace(key, value).second) { + throw std::runtime_error("the " + what + " " + path.string() + " has a line " + std::to_string(number) + " that is not a new '" + pair + "' pair"); + } + } + if (file.bad()) throw std::runtime_error("cannot read the " + what + " " + path.string()); + return values; +} + +template +void writeRecord(const std::filesystem::path& directory, const std::filesystem::path& path, const std::map& values, const std::string& what) { + std::error_code error; + if (std::filesystem::create_directories(directory, error)) RecordWrittenPath_nid_no_patch(directory); + if (error) throw std::filesystem::filesystem_error("cannot create the trophy directory", directory, error); + auto temporary = path; + temporary += ".tmp"; + { + std::ofstream file(temporary, std::ios::binary | std::ios::trunc); + for (const auto& [key, value] : values) file << key << ' ' << value << '\n'; + file.flush(); + if (!file) throw std::runtime_error("cannot write the " + what + " " + temporary.string()); + } + std::filesystem::rename(temporary, path); + RecordWrittenPath_nid_no_patch(path); +} + +std::string recordName(std::int32_t userId, std::uint32_t serviceLabel) { + return std::to_string(userId) + "_" + std::to_string(serviceLabel); +} + +struct Listener { + std::uint64_t id = 0; + NpTrophy2UnlockListener callback = nullptr; + void* context = nullptr; +}; + +struct Listeners { + std::mutex mutex; + std::recursive_mutex dispatch; + std::vector entries; + std::uint64_t next = 1; +}; + +bool registered(Listeners& registry, std::uint64_t id) { + std::lock_guard lock(registry.mutex); + return std::any_of(registry.entries.begin(), registry.entries.end(), [id](const Listener& entry) { return entry.id == id; }); +} + +Listeners& listeners() { + static Listeners registry; + return registry; +} + +} + +std::uint64_t CurrentTick() { + const auto now = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + return UnixEpochTick + static_cast(now); +} + +TrophyStore::TrophyStore(std::filesystem::path directory) : directory(std::move(directory)) {} + +std::filesystem::path TrophyStore::RecordPath(std::int32_t userId, std::uint32_t serviceLabel) const { + return directory / (recordName(userId, serviceLabel) + ".txt"); +} + +TrophyStore::Unlocks& TrophyStore::record(const Key& key) { + if (const auto found = records.find(key); found != records.end()) return found->second; + auto unlocks = readRecord(RecordPath(key.first, key.second), "trophy record", " "); + return records.emplace(key, std::move(unlocks)).first->second; +} + +void TrophyStore::save(const Key& key, const Unlocks& unlocks) const { + writeRecord(directory, RecordPath(key.first, key.second), unlocks, "trophy record"); +} + +bool TrophyStore::Unlock(std::int32_t userId, std::uint32_t serviceLabel, std::int32_t trophyId, std::uint64_t tick) { + if (trophyId < 0) throw std::invalid_argument("trophy " + std::to_string(trophyId) + " cannot be unlocked: trophy ids are not negative"); + std::lock_guard lock(mutex); + const Key key{userId, serviceLabel}; + auto& unlocks = record(key); + if (unlocks.contains(trophyId)) return false; + auto updated = unlocks; + updated.emplace(trophyId, tick); + save(key, updated); + unlocks = std::move(updated); + return true; +} + +std::optional TrophyStore::UnlockTick(std::int32_t userId, std::uint32_t serviceLabel, std::int32_t trophyId) { + std::lock_guard lock(mutex); + const auto& unlocks = record({userId, serviceLabel}); + const auto found = unlocks.find(trophyId); + if (found == unlocks.end()) return std::nullopt; + return found->second; +} + +std::size_t TrophyStore::UnlockCount(std::int32_t userId, std::uint32_t serviceLabel) { + std::lock_guard lock(mutex); + return record({userId, serviceLabel}).size(); +} + +TrophyStore& TitleStore() { + static TrophyStore store(ResolvePath_nid_no_patch(TrophyRootGuest)); + return store; +} + +StatStore::StatStore(std::filesystem::path directory) : directory(std::move(directory)) {} + +std::filesystem::path StatStore::RecordPath(std::int32_t userId, std::uint32_t serviceLabel) const { + return directory / (recordName(userId, serviceLabel) + ".stats.txt"); +} + +StatStore::Stats& StatStore::record(const Key& key) { + if (const auto found = records.find(key); found != records.end()) return found->second; + auto stats = readRecord(RecordPath(key.first, key.second), "stat record", " "); + return records.emplace(key, std::move(stats)).first->second; +} + +void StatStore::Set(std::int32_t userId, std::uint32_t serviceLabel, std::uint64_t statId, std::uint64_t value) { + std::lock_guard lock(mutex); + const Key key{userId, serviceLabel}; + auto& stats = record(key); + if (const auto found = stats.find(statId); found != stats.end() && found->second == value) return; + auto updated = stats; + updated[statId] = value; + writeRecord(directory, RecordPath(userId, serviceLabel), updated, "stat record"); + stats = std::move(updated); +} + +std::optional StatStore::Value(std::int32_t userId, std::uint32_t serviceLabel, std::uint64_t statId) { + std::lock_guard lock(mutex); + const auto& stats = record({userId, serviceLabel}); + const auto found = stats.find(statId); + if (found == stats.end()) return std::nullopt; + return found->second; +} + +std::map StatStore::Values(std::int32_t userId, std::uint32_t serviceLabel) { + std::lock_guard lock(mutex); + return record({userId, serviceLabel}); +} + +StatStore& TitleStats() { + static StatStore store(ResolvePath_nid_no_patch(TrophyRootGuest)); + return store; +} + +std::optional ContextUnlockTick(int context, std::int32_t trophyId) { + const auto owner = FindContext(context); + if (!owner || trophyId < 0) return std::nullopt; + return TitleStore().UnlockTick(owner->userId, owner->serviceLabel, trophyId); +} + +std::size_t ContextUnlockCount(int context) { + const auto owner = FindContext(context); + return owner ? TitleStore().UnlockCount(owner->userId, owner->serviceLabel) : 0; +} + +bool RecordUnlock(std::int32_t userId, std::uint32_t serviceLabel, std::int32_t trophyId) { + const auto title = GetAppTitleId_nid_postfix(); + const std::string_view titleId(title.value, static_cast(std::find(std::begin(title.value), std::end(title.value), '\0') - std::begin(title.value))); + const NpTrophy2Unlock unlock{std::string(titleId), userId, serviceLabel, trophyId, CurrentTick()}; + if (!TitleStore().Unlock(userId, serviceLabel, trophyId, unlock.tick)) return false; + std::fprintf(stderr, "[trophy] %s unlocked trophy %d for user %d\n", unlock.titleId.c_str(), trophyId, userId); + auto& registry = listeners(); + std::lock_guard dispatching(registry.dispatch); + std::vector current; + { + std::lock_guard lock(registry.mutex); + current = registry.entries; + } + for (const auto& entry : current) { + if (registered(registry, entry.id)) entry.callback(entry.context, unlock); + } + const auto* package = Package(serviceLabel); + if (package == nullptr) return true; + if (const auto platinum = PlatinumToUnlock(*package, trophyId, UnlockedBy(userId, serviceLabel))) RecordUnlock(userId, serviceLabel, *platinum); + return true; +} + +} + +extern "C" bool NpTrophy2RecordUnlock_nid_no_patch(std::int32_t userId, std::uint32_t serviceLabel, std::int32_t trophyId) { + if (const auto* package = NpTrophy2::Package(serviceLabel)) { + if (const auto refusal = NpTrophy2::PostedUnlockRefusal(*package, trophyId)) { + std::fprintf(stderr, "[trophy] ignored the posted unlock of trophy %d for user %d: %s\n", trophyId, userId, refusal->c_str()); + return false; + } + } + return NpTrophy2::RecordUnlock(userId, serviceLabel, trophyId); +} + +extern "C" std::uint64_t NpTrophy2AddUnlockListener_nid_no_patch(NpTrophy2UnlockListener listener, void* context) { + if (listener == nullptr) { + APS5_INVALID_ARG_EX; + } + auto& registry = NpTrophy2::listeners(); + std::lock_guard lock(registry.mutex); + const auto id = registry.next++; + registry.entries.push_back({id, listener, context}); + return id; +} + +extern "C" void NpTrophy2RemoveUnlockListener_nid_no_patch(std::uint64_t id) { + auto& registry = NpTrophy2::listeners(); + std::lock_guard dispatching(registry.dispatch); + std::lock_guard lock(registry.mutex); + const auto removed = std::erase_if(registry.entries, [id](const NpTrophy2::Listener& entry) { return entry.id == id; }); + if (removed == 0) throw std::invalid_argument("no trophy unlock listener has id " + std::to_string(id)); +} diff --git a/core/libs/prx/libSceNpTrophy2/tests/TrophyPackage.cpp b/core/libs/prx/libSceNpTrophy2/tests/TrophyPackage.cpp new file mode 100644 index 0000000000..d1dcdba734 --- /dev/null +++ b/core/libs/prx/libSceNpTrophy2/tests/TrophyPackage.cpp @@ -0,0 +1,434 @@ +#include "prx/libSceNpTrophy2/include/TitleTrophies.hpp" +#include "prx/libSceNpTrophy2/include/TrophyPackage.hpp" +#include "prx/libSceNpTrophy2/include/TrophyStore.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using Files = std::vector>; + +constexpr std::int32_t InitialUser = 0x10000000; + +void check(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +template +std::string failure(TAction action) { + try { + action(); + } catch (const std::runtime_error& error) { + return error.what(); + } + return {}; +} + +bool contains(const std::string& text, const char* part) { + return text.find(part) != std::string::npos; +} + +void putBigEndian(std::string& data, std::size_t offset, std::uint64_t value, std::size_t size) { + for (std::size_t i = 0; i < size; ++i) data[offset + i] = static_cast((value >> (8 * (size - 1 - i))) & 0xFF); +} + +std::string ucp(const Files& files) { + const std::size_t toc = 0x40; + std::size_t offset = toc + 0x20 + files.size() * 0x40; + std::size_t size = offset; + for (const auto& [name, contents] : files) size += contents.size(); + std::string data(size, '\0'); + putBigEndian(data, 0, 0xB228C60A, 4); + putBigEndian(data, 4, 1, 4); + putBigEndian(data, 8, size, 8); + putBigEndian(data, 0x10, files.size(), 4); + putBigEndian(data, 0x14, toc, 4); + std::size_t entry = toc + 0x20; + for (const auto& [name, contents] : files) { + data.replace(entry, name.size(), name); + putBigEndian(data, entry + 0x20, offset, 8); + putBigEndian(data, entry + 0x28, contents.size(), 8); + data.replace(offset, contents.size(), contents); + offset += contents.size(); + entry += 0x40; + } + return data; +} + +const char TropConf[] = R"({"defaultLanguage":"en-US","trophies":[ + {"id":"0","grade":"P","hidden":false}, + {"id":"1","grade":"G","platinumTrophyId":"0","unlockCondition":{"udsStatId":"10","comparator":"ge","targetValue":"10","progressive":false}}, + {"id":"2","grade":"S","platinumTrophyId":0,"unlockCondition":{"udsStatId":20,"comparator":"gt","targetValue":100}}, + {"id":3,"grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"30","comparator":"le","targetValue":"5"}}, + {"id":"4","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"30","comparator":"lt","targetValue":"3"}}, + {"id":"5","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"40","comparator":"ge","targetValue":"5","progressive":true}}, + {"id":"6","grade":"B","groupId":"1","hidden":true,"hasReward":true,"platinumTrophyId":"-1"}, + {"id":"7","grade":"B","groupId":"1","unlockCondition":{"udsStatId":"10","comparator":"eq","targetValue":"-1"}} +]})"; + +const char TropMetaEnglish[] = R"({"metadata":{"titleMetadata":{"name":"Synthetic Game"}, + "trophyMetadata":[{"id":"0","name":"All of them","detail":"Unlock every trophy"},{"id":"1","name":"Ten","detail":"Reach ten"}, + {"id":"6","name":"Secret","detail":"A hidden one","reward":"A hat"},{"id":"99","name":"Not a trophy"}], + "groupMetadata":[{"id":"1","name":"Expansion"},{"id":"9","name":"Unused"}]}})"; + +const char TropMetaFrench[] = R"({"metadata":{"titleMetadata":{"name":"Jeu synth\u00e9tique"},"trophyMetadata":[{"id":"1","name":"Dix"}]}})"; + +const char StatsDefinition[] = R"({"statDefinitionArray":[ + {"statId":10,"dataType":"uint64","aggregation":"latest","minValue":"0"}, + {"statId":20,"dataType":"uint64","aggregation":"latest"}, + {"statId":30,"dataType":"uint32","aggregation":"latest"}, + {"statId":"40","dataType":"uint32","aggregation":"latest","maxValue":"50"}, + {"statId":50,"dataType":"uint64","aggregation":"sum"}, + {"statId":60,"dataType":"int32","aggregation":"latest"}, + {"statId":70,"dataType":"uint32","aggregation":"latest","maxValue":"5000000000"}, + {"dataType":"uint32","aggregation":"latest"} +]})"; + +const char StatsExtraction[] = R"({"statsExtractionRuleArray":[ + {"condition":{"eventName":"COLLECT"},"action":{"input":"$.counter","output":{"statId":10}}}, + {"condition":{"eventName":"COLLECT"},"action":{"input":"$.total","output":{"statId":20}}}, + {"condition":{"eventName":"TIME"},"action":{"input":"$.seconds","output":{"statId":30}}}, + {"condition":{"eventName":"STEPS"},"action":{"input":"$.player.steps","output":{"statId":"40"}}}, + {"condition":{"eventName":"SUM"},"action":{"input":"$.counter","output":{"statId":50}}}, + {"condition":{"eventName":"FILTERED","property":{"path":"$.level"}},"action":{"input":"$.counter","output":{"statId":10}}}, + {"condition":{"eventName":"COUNT"},"action":{"output":{"statId":10}}}, + {"condition":{"eventName":"BRACKETS"},"action":{"input":"$['counter']","output":{"statId":10}}}, + {"bad":1} +]})"; + +std::string trophyPackage() { + return ucp({{"tropconf.json", TropConf}, {"tropmeta_en-US.json", TropMetaEnglish}, {"tropmeta_fr-FR.json", TropMetaFrench}, {"trop0001.png", "not an image"}}); +} + +std::string statPackage() { + return ucp({{"stats_definition.json", StatsDefinition}, {"stats_extraction.json", StatsExtraction}}); +} + +void testContainer() { + const auto data = ucp({{"a.json", "{}"}, {"b.bin", std::string("\0\1\2", 3)}, {"a.json", "second"}}); + const auto files = NpTrophy2::ReadUcp(data); + check(files.size() == 2 && files.at("a.json") == "{}" && files.at("b.bin") == std::string_view("\0\1\2", 3), "the container files were not read, or a repeated name replaced the first"); + check(NpTrophy2::ReadUcp(ucp({})).empty(), "an empty container was not read"); + const std::string longName(0x20, 'n'); + const auto named = NpTrophy2::ReadUcp(ucp({{longName, "x"}})); + check(named.size() == 1 && named.begin()->first == longName, "a name that fills its 32 bytes was not read whole"); + auto padded = data + "trailing"; + check(NpTrophy2::ReadUcp(padded).size() == 2, "bytes after the container size were not ignored"); + + for (std::size_t length = 0; length < data.size(); ++length) { + check(!failure([&] { NpTrophy2::ReadUcp(std::string_view(data).substr(0, length)); }).empty(), "a truncated container was read"); + } + check(contains(failure([&] { NpTrophy2::ReadUcp(std::string_view(data).substr(0, 0x3F)); }), "fewer than the 64-byte header"), "a short header was not named"); + auto corrupt = data; + corrupt[0] = 0; + check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "magic is 0x28c60a"), "a wrong magic was not named"); + corrupt = data; + putBigEndian(corrupt, 4, 2, 4); + check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "version is 2"), "a wrong version was not named"); + corrupt = data; + putBigEndian(corrupt, 8, 0x3F, 8); + check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "its size 0x3f"), "a size inside the header was accepted"); + corrupt = data; + putBigEndian(corrupt, 8, data.size() + 1, 8); + check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "is not from the header size"), "a size past the file was accepted"); + corrupt = data; + putBigEndian(corrupt, 0x10, 4097, 4); + check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "more than 4096"), "too many files were accepted"); + corrupt = data; + putBigEndian(corrupt, 0x10, 40, 4); + check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "table of 40 files"), "a table past the size was accepted"); + corrupt = data; + putBigEndian(corrupt, 0x14, data.size() + 1, 4); + check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "ends past its size"), "a table offset past the size was accepted"); + corrupt = data; + putBigEndian(corrupt, 0x60 + 0x20, data.size() + 1, 8); + check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "its file \"a.json\""), "a file offset past the size was accepted"); + corrupt = data; + putBigEndian(corrupt, 0x60 + 0x28, ~std::uint64_t{0}, 8); + check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "ends past its size"), "a file length past the size was accepted"); +} + +void testJson() { + const auto value = NpTrophy2::ParseJson("\xEF\xBB\xBF {\"a\": [1, -2.5e+3, \"x\\u00e9\\ud83d\\ude00\\n\", true, false, null, {}], \"a\": 7, \"b\": \"12\"} "); + check(value.kind == NpTrophy2::Json::Kind::Object && value.members.size() == 3, "an object was not parsed"); + check(value.Find("a") != nullptr && value.Find("a")->text == "7", "a repeated key did not keep its last value"); + const auto& list = value.members[0].second.items; + check(list.size() == 7 && list[0].text == "1" && list[1].text == "-2.5e+3" && list[2].text == "x\xC3\xA9\xF0\x9F\x98\x80\n", "array values or escapes were not parsed"); + check(list[3].IsTrue() && !list[4].IsTrue() && list[5].kind == NpTrophy2::Json::Kind::Null && list[6].kind == NpTrophy2::Json::Kind::Object, "literals were not parsed"); + check(value.Find("b")->Unsigned() == std::optional(12) && list[0].Unsigned() == std::optional(1), "numbers and numeric strings are not unsigned"); + check(!list[1].Unsigned() && !list[3].Unsigned() && !NpTrophy2::ParseJson("\"\"").Unsigned() && !NpTrophy2::ParseJson("\" 1\"").Unsigned(), "a value that is not an unsigned integer was read as one"); + check(NpTrophy2::ParseJson("18446744073709551615").Unsigned() == std::optional(UINT64_MAX) && !NpTrophy2::ParseJson("18446744073709551616").Unsigned(), "the unsigned range is wrong"); + check(NpTrophy2::ParseJson("2147483647").Id() == std::optional(INT32_MAX) && !NpTrophy2::ParseJson("2147483648").Id() && !NpTrophy2::ParseJson("-1").Id(), "the id range is wrong"); + std::string deep(64, '['); + deep += std::string(64, ']'); + check(NpTrophy2::ParseJson(deep).kind == NpTrophy2::Json::Kind::Array, "64 levels of nesting were refused"); + for (const char* text : {"", "{", "[1,]", "{\"a\":1,}", "{\"a\" 1}", "{a:1}", "01", "1.", "-", "1e", "+1", "\"\\x\"", "\"\\u12g4\"", "\"\\ud800\"", "\"\\udc00\"", + "\"\\ud800\\u0041\"", "\"a\nb\"", "\"open", "tru", "nul", "{} {}", "[1] x"}) { + check(!failure([text] { NpTrophy2::ParseJson(text); }).empty(), "malformed JSON was parsed"); + } + const auto deeper = "[" + deep + "]"; + check(contains(failure([&deeper] { NpTrophy2::ParseJson(deeper); }), "deeper than 64"), "deep nesting was not refused"); + std::string most = "["; + for (std::size_t value = 2; value < 1048576; ++value) most += "0,"; + most += "0]"; + check(NpTrophy2::ParseJson(most).items.size() == 1048575, "a document of 1048576 values was refused"); + const auto more = "[0," + most.substr(1); + check(contains(failure([&more] { NpTrophy2::ParseJson(more); }), "holds more than 1048576 values"), "a document of more than 1048576 values was parsed"); +} + +void testPackage() { + const auto package = NpTrophy2::ParseTrophyPackage(trophyPackage(), "en-US"); + check(package.language == "en-US" && package.title == "Synthetic Game" && package.trophies.size() == 8, "the package was not read"); + const auto& platinum = package.trophies.at(0); + check(platinum.grade == NpTrophy2::GradePlatinum && platinum.name == "All of them" && platinum.detail == "Unlock every trophy" && !platinum.Condition(), "the platinum was not read"); + const auto& gold = package.trophies.at(1); + check(gold.grade == NpTrophy2::GradeGold && gold.name == "Ten" && gold.platinumId == std::optional(0) && gold.groupId == NpTrophy2::BaseGroup, "trophy 1 was not read"); + const auto condition = gold.Condition(); + check(condition && condition->statId == 10 && condition->target == 10 && condition->comparator == NpTrophy2::Comparator::GreaterEqual && !gold.progressive, "trophy 1's condition was not read"); + check(package.trophies.at(2).grade == NpTrophy2::GradeSilver && package.trophies.at(3).grade == NpTrophy2::GradeBronze, "grades were not read"); + check(package.trophies.at(2).Condition()->comparator == NpTrophy2::Comparator::Greater && package.trophies.at(3).Condition()->comparator == NpTrophy2::Comparator::LessEqual && + package.trophies.at(4).Condition()->comparator == NpTrophy2::Comparator::Less, + "comparators were not read"); + const auto& progressive = package.trophies.at(5); + check(progressive.progressive && NpTrophy2::ProgressTarget(progressive) == std::optional(5) && !NpTrophy2::ProgressTarget(gold), "the progressive target was not read"); + const auto& hidden = package.trophies.at(6); + check(hidden.hidden && hidden.hasReward && hidden.groupId == 1 && hidden.name == "Secret" && hidden.reward == "A hat" && !hidden.platinumId, "trophy 6 was not read"); + check(!package.trophies.at(7).Condition() && package.trophies.at(7).name.empty(), "an unusable condition was kept"); + check(package.skipped.size() == 1 && contains(package.skipped[0], "trophy 7 never unlocks through stats") && contains(package.skipped[0], "\"-1\"") && contains(package.skipped[0], "\"eq\""), + "the unusable condition was not reported"); + check(package.groups.size() == 2 && package.groups.at(NpTrophy2::BaseGroup) == "Synthetic Game" && package.groups.at(1) == "Expansion", "groups were not read"); + + const auto french = NpTrophy2::ParseTrophyPackage(trophyPackage(), "fr-FR"); + check(french.language == "fr-FR" && french.title == "Jeu synth\xC3\xA9tique" && french.trophies.at(1).name == "Dix" && french.trophies.at(6).name.empty(), "the system language was not used"); + check(NpTrophy2::ParseTrophyPackage(trophyPackage(), "de-DE").language == "en-US", "English was not the fallback"); + std::string conf = TropConf; + conf.replace(conf.find("en-US"), 5, "ja-JP"); + check(NpTrophy2::ParseTrophyPackage(ucp({{"tropconf.json", conf}, {"tropmeta_ja-JP.json", TropMetaEnglish}}), "de-DE").language == "ja-JP", "the default language was not the last fallback"); + const auto withoutTexts = ucp({{"tropconf.json", conf}}); + check(contains(failure([&withoutTexts] { NpTrophy2::ParseTrophyPackage(withoutTexts, "de-DE"); }), "no tropmeta_.json for de-DE, en-US or its defaultLanguage \"ja-JP\""), + "a package without texts was read"); + const auto missingTexts = [](const std::string& defaultLanguage, const char* language) { + std::string text = TropConf; + text.replace(text.find("en-US"), 5, defaultLanguage); + const auto data = ucp({{"tropconf.json", text}}); + return failure([&data, language] { NpTrophy2::ParseTrophyPackage(data, language); }); + }; + check(missingTexts("en-US", "en-US") == "it has no tropmeta_.json for en-US", "a missing text named en-US more than once"); + check(missingTexts("ja-JP", "en-US") == "it has no tropmeta_.json for en-US or its defaultLanguage \"ja-JP\"", "a missing text in English named English twice"); + check(missingTexts("en-US", "de-DE") == "it has no tropmeta_.json for de-DE or en-US", "a missing text with an English default named English twice"); + + const auto broken = [](const std::string& replaced, const std::string& replacement) { + std::string text = TropConf; + text.replace(text.find(replaced), replaced.size(), replacement); + const auto data = ucp({{"tropconf.json", text}, {"tropmeta_en-US.json", TropMetaEnglish}}); + return failure([&data] { NpTrophy2::ParseTrophyPackage(data, "en-US"); }); + }; + check(contains(broken(R"("grade":"G")", R"("grade":"X")"), "trophy 1 has no grade"), "a wrong grade was accepted"); + check(contains(broken(R"("grade":"G")", R"("grade":"GS")"), "trophy 1 has no grade"), "a two-letter grade was accepted"); + check(contains(broken(R"({"id":"4",)", R"({"id":"3",)"), "lists trophy 3 twice"), "a repeated trophy was accepted"); + check(contains(broken(R"({"id":"4",)", R"({"id":"-4",)"), "trophies[4] has no id"), "a negative id was accepted"); + check(contains(broken(R"("groupId":"1","hidden")", R"("groupId":"x","hidden")"), "trophy 6 has groupId \"x\""), "a wrong group was accepted"); + check(contains(broken(R"("udsStatId":"10")", R"("udsStatId":"-10")"), "udsStatId \"-10\""), "a negative stat id was accepted"); + check(contains(broken(R"("trophies":[)", R"("trophies":0,"other":[)"), "has no trophies array"), "trophies that are not an array were accepted"); + const auto withoutConf = ucp({{"tropmeta_en-US.json", TropMetaEnglish}}); + check(contains(failure([&withoutConf] { NpTrophy2::ParseTrophyPackage(withoutConf, "en-US"); }), "no tropconf.json"), "a package without tropconf.json was read"); + const auto brokenConf = ucp({{"tropconf.json", "{\"trophies\":["}, {"tropmeta_en-US.json", TropMetaEnglish}}); + check(contains(failure([&brokenConf] { NpTrophy2::ParseTrophyPackage(brokenConf, "en-US"); }), "tropconf.json is not JSON"), "a tropconf.json that is not JSON was read"); + const auto withoutTitle = ucp({{"tropconf.json", TropConf}, {"tropmeta_en-US.json", "{\"metadata\":{}}"}}); + check(contains(failure([&withoutTitle] { NpTrophy2::ParseTrophyPackage(withoutTitle, "en-US"); }), "has no titleMetadata"), "texts without a title were read"); + std::string groups = R"({"defaultLanguage":"en-US","trophies":[)"; + for (int id = 0; id <= 50; ++id) groups += (id == 0 ? "" : ",") + std::string(R"({"id":)") + std::to_string(id) + R"(,"grade":"B","groupId":)" + std::to_string(id) + "}"; + groups += "]}"; + const auto manyGroups = ucp({{"tropconf.json", groups}, {"tropmeta_en-US.json", TropMetaEnglish}}); + check(contains(failure([&manyGroups] { NpTrophy2::ParseTrophyPackage(manyGroups, "en-US"); }), "more than 50 groups"), "more than 50 groups were accepted"); + std::string padded = TropConf; + std::string padding = "\"padding\":["; + for (std::size_t value = 0; value < 1048576; ++value) padding += "0,"; + padded.insert(1, padding + "0],"); + const auto oversized = ucp({{"tropconf.json", padded}, {"tropmeta_en-US.json", TropMetaEnglish}}); + check(contains(failure([&oversized] { NpTrophy2::ParseTrophyPackage(oversized, "en-US"); }), "tropconf.json is not JSON: the document holds more than 1048576 values"), + "a tropconf.json of more than 1048576 values was read"); + check(contains(failure([] { NpTrophy2::ParseTrophyPackage("not a package", "en-US"); }), "fewer than the 64-byte header"), "a package that is not a container was read"); + check(NpTrophy2::LanguageTag(1) == "en-US" && NpTrophy2::LanguageTag(0) == "ja-JP" && NpTrophy2::LanguageTag(2) == "fr-FR" && NpTrophy2::LanguageTag(29) == "id-ID", "language tags are wrong"); + bool thrown = false; + try { + NpTrophy2::LanguageTag(31); + } catch (const std::out_of_range&) { + thrown = true; + } + check(thrown, "language 31 has a tag"); +} + +void testProgressAndPlatinum() { + const auto package = NpTrophy2::ParseTrophyPackage(trophyPackage(), "en-US"); + std::set unlocked; + const NpTrophy2::UnlockedQuery isUnlocked = [&unlocked](std::int32_t id) { return unlocked.contains(id); }; + const auto& progressive = package.trophies.at(5); + check(NpTrophy2::TrophyProgress(progressive, std::nullopt, false) == 0 && NpTrophy2::TrophyProgress(progressive, 3, false) == 3 && NpTrophy2::TrophyProgress(progressive, 9, false) == 5 && + NpTrophy2::TrophyProgress(progressive, 1, true) == 5, + "progress is not the stat up to the target"); + bool thrown = false; + try { + NpTrophy2::TrophyProgress(package.trophies.at(1), 3, false); + } catch (const std::invalid_argument&) { + thrown = true; + } + check(thrown, "a trophy that is not progressive reported progress"); + + unlocked = {1, 2, 3, 4}; + check(!NpTrophy2::PlatinumToUnlock(package, 4, isUnlocked), "the platinum unlocked before trophy 5"); + unlocked.insert(5); + check(NpTrophy2::PlatinumToUnlock(package, 5, isUnlocked) == std::optional(0), "the platinum did not unlock after the last trophy that names it"); + check(!NpTrophy2::PlatinumToUnlock(package, 6, isUnlocked) && !NpTrophy2::PlatinumToUnlock(package, 0, isUnlocked) && !NpTrophy2::PlatinumToUnlock(package, 42, isUnlocked), + "a trophy that names no platinum unlocked one"); + unlocked.insert(0); + check(!NpTrophy2::PlatinumToUnlock(package, 5, isUnlocked), "an unlocked platinum was unlocked again"); + + unlocked = {1, 6}; + const auto all = NpTrophy2::CountTrophies(package, std::nullopt, isUnlocked); + check(all.trophies == 8 && all.unlocked == 2 && all.grades[NpTrophy2::GradePlatinum] == 1 && all.grades[NpTrophy2::GradeGold] == 1 && all.grades[NpTrophy2::GradeSilver] == 1 && + all.grades[NpTrophy2::GradeBronze] == 5 && all.unlockedGrades[NpTrophy2::GradeGold] == 1 && all.unlockedGrades[NpTrophy2::GradeBronze] == 1, + "the game counts are wrong"); + check(all.Percentage() == (6 + 1) * 100 / (6 + 2 + 5), "the progress percentage does not weigh gold 6, silver 2 and bronze 1"); + const auto expansion = NpTrophy2::CountTrophies(package, 1, isUnlocked); + check(expansion.trophies == 2 && expansion.unlocked == 1 && expansion.Percentage() == 50, "the group counts are wrong"); + check(NpTrophy2::CountTrophies(package, 5, isUnlocked).trophies == 0 && NpTrophy2::CountTrophies(package, 5, isUnlocked).Percentage() == 0, "an empty group has trophies"); +} + +void testStatRules() { + const auto rules = NpTrophy2::ParseStatRules(statPackage()); + check(rules.stats.size() == 4 && rules.stats.at(10).type == NpTrophy2::StatType::UInt64 && rules.stats.at(30).type == NpTrophy2::StatType::UInt32 && rules.stats.at(30).maximum == UINT32_MAX && + rules.stats.at(40).maximum == 50 && rules.stats.at(10).minimum == 0, + "stat definitions were not read"); + check(rules.events.size() == 3 && rules.events.at("COLLECT").size() == 2 && rules.events.at("COLLECT")[1].input == "total" && rules.events.at("COLLECT")[1].statId == 20 && + rules.events.at("STEPS")[0].input == "player.steps" && rules.events.at("TIME")[0].statId == 30, + "extraction rules were not read"); + std::set skipped(rules.skipped.begin(), rules.skipped.end()); + const auto reported = [&skipped](const char* part) { + for (const auto& line : skipped) { + if (contains(line, part)) return true; + } + return false; + }; + check(rules.skipped.size() == 9, "unsupported definitions and rules were not all reported"); + check(reported("stat 50 has aggregation \"sum\"") && reported("stat 60 has dataType \"int32\"") && reported("stat 70 has maxValue \"5000000000\"") && reported("statDefinitionArray[7] has no unsigned statId"), + "unsupported definitions were not reported"); + check(reported("for SUM writes stat 50") && reported("filters FILTERED") && reported("for COUNT reads no input") && reported("for BRACKETS reads \"$['counter']\"") && reported("[8] has no condition"), + "unsupported rules were not reported"); + for (const char* input : {"$", "$.", "$..a", "$.a.", "$.a-b", "counter", "$.a b"}) { + const std::string extraction = std::string(R"({"statsExtractionRuleArray":[{"condition":{"eventName":"E"},"action":{"input":")") + input + R"(","output":{"statId":10}}}]})"; + check(NpTrophy2::ParseStatRules(ucp({{"stats_definition.json", StatsDefinition}, {"stats_extraction.json", extraction}})).events.empty(), "an unsupported input path was accepted"); + } + const auto withoutDefinitions = ucp({{"stats_extraction.json", StatsExtraction}}); + check(contains(failure([&withoutDefinitions] { NpTrophy2::ParseStatRules(withoutDefinitions); }), "no stats_definition.json"), "rules without definitions were read"); + const auto withoutRules = ucp({{"stats_definition.json", StatsDefinition}, {"stats_extraction.json", "{}"}}); + check(contains(failure([&withoutRules] { NpTrophy2::ParseStatRules(withoutRules); }), "has no statsExtractionRuleArray"), "rules without an extraction array were read"); +} + +void testConditions() { + const auto package = NpTrophy2::ParseTrophyPackage(trophyPackage(), "en-US"); + std::set unlocked; + const NpTrophy2::UnlockedQuery isUnlocked = [&unlocked](std::int32_t id) { return unlocked.contains(id); }; + const auto reached = [&](std::uint64_t stat, std::uint64_t value) { return NpTrophy2::ReachedTrophies(package, {{stat, value}}, isUnlocked); }; + check(reached(10, 9).empty() && reached(10, 10) == std::vector{1} && reached(10, 11) == std::vector{1}, "ge did not unlock at its target"); + check(reached(20, 100).empty() && reached(20, 101) == std::vector{2}, "gt did not unlock past its target"); + check(reached(30, 6).empty() && reached(30, 5) == std::vector{3} && reached(30, 3) == std::vector{3} && reached(30, 2) == (std::vector{3, 4}), + "le and lt did not unlock at and below their targets"); + check(reached(40, 4).empty() && reached(40, 5) == std::vector{5}, "the progressive trophy did not unlock at its target"); + check(reached(99, 1000).empty() && NpTrophy2::ReachedTrophies(package, {}, isUnlocked).empty(), "a stat without a trophy, or no stat, unlocked a trophy"); + unlocked.insert(1); + check(reached(10, 10).empty(), "an unlocked trophy was reached again"); + check(NpTrophy2::ReachedTrophies(package, {{10, 10}, {20, 101}, {30, 0}, {40, 9}}, isUnlocked) == (std::vector{2, 3, 4, 5}), "one set of stats did not reach every trophy"); +} + +void testStatStore() { + const std::filesystem::path directory("stats"); + { + NpTrophy2::StatStore stats(directory); + check(!stats.Value(InitialUser, 0, 10) && stats.Values(InitialUser, 0).empty(), "a new store has stats"); + stats.Set(InitialUser, 0, 10, 7); + stats.Set(InitialUser, 0, 18446744073709551615ull, 1); + stats.Set(InitialUser, 0, 10, 9); + stats.Set(InitialUser + 1, 0, 10, 3); + stats.Set(InitialUser, 1, 10, 4); + check(stats.RecordPath(InitialUser, 0) == directory / "268435456_0.stats.txt", "the stats are not stored next to the unlocks"); + } + NpTrophy2::StatStore restarted(directory); + check(restarted.Value(InitialUser, 0, 10) == std::optional(9) && restarted.Value(InitialUser + 1, 0, 10) == std::optional(3) && + restarted.Value(InitialUser, 1, 10) == std::optional(4) && restarted.Values(InitialUser, 0).size() == 2, + "stats did not survive a restart"); + for (const char* text : {"10\n", "10 x\n", "x 5\n", "-1 5\n", "10 5\n10 6\n", "10 5 7\n", "\n", "10 -5\n"}) { + std::filesystem::remove_all("damaged"); + std::filesystem::create_directories("damaged"); + std::FILE* file = std::fopen("damaged/268435456_0.stats.txt", "wb"); + check(file != nullptr, "a damaged stat record was not created"); + std::fputs(text, file); + std::fclose(file); + NpTrophy2::StatStore damaged("damaged"); + check(contains(failure([&damaged] { damaged.Values(InitialUser, 0); }), "stat record"), "a damaged stat record was read"); + } +} + +NpTrophy2Event event(const char* name, std::vector properties) { + return {name, std::move(properties)}; +} + +void testApplyStatRules() { + const auto rules = NpTrophy2::ParseStatRules(statPackage()); + NpTrophy2::StatStore stats("applied"); + const auto none = NpTrophy2::ApplyStatRules(rules, event("UNKNOWN", {{"counter", 5, "5"}}), stats, InitialUser, 0); + check(none.updates.empty() && none.problems.empty() && stats.Values(InitialUser, 0).empty(), "an unknown event changed a stat"); + const auto collect = NpTrophy2::ApplyStatRules(rules, event("COLLECT", {{"counter", 12, "12"}, {"total", 300, "300"}, {"other", 1, "1"}}), stats, InitialUser, 0); + check(collect.problems.empty() && collect.updates.size() == 2 && collect.updates[0].statId == 10 && collect.updates[0].value == 12 && collect.updates[0].input == "counter" && + collect.updates[1].statId == 20 && collect.updates[1].value == 300, + "an event did not update every stat it feeds"); + check(stats.Value(InitialUser, 0, 10) == std::optional(12) && stats.Value(InitialUser, 0, 20) == std::optional(300), "the updated stats were not stored"); + const auto missing = NpTrophy2::ApplyStatRules(rules, event("COLLECT", {{"counter", 13, "13"}}), stats, InitialUser, 0); + check(missing.updates.size() == 1 && missing.problems.size() == 1 && missing.problems[0].rule == "COLLECT $.total for stat 20" && missing.problems[0].reason == "the event has no such property" && + stats.Value(InitialUser, 0, 20) == std::optional(300), + "a missing property changed its stat"); + const auto wrong = NpTrophy2::ApplyStatRules(rules, event("TIME", {{"seconds", std::nullopt, "-3"}}), stats, InitialUser, 0); + check(wrong.updates.empty() && wrong.problems.size() == 1 && wrong.problems[0].reason == "-3 is not an unsigned integer" && !stats.Value(InitialUser, 0, 30), "a negative value changed a stat"); + const auto wide = NpTrophy2::ApplyStatRules(rules, event("TIME", {{"seconds", 4294967296ull, "4294967296"}}), stats, InitialUser, 0); + check(wide.updates.empty() && wide.problems.size() == 1 && wide.problems[0].reason == "4294967296 is not from 0 to 4294967295", "a uint32 stat took a value it cannot hold"); + const auto bounded = NpTrophy2::ApplyStatRules(rules, event("STEPS", {{"player.steps", 51, "51"}}), stats, InitialUser, 0); + check(bounded.updates.empty() && bounded.problems[0].reason == "51 is not from 0 to 50", "a value above maxValue changed a stat"); + const auto nested = NpTrophy2::ApplyStatRules(rules, event("STEPS", {{"steps", 2, "2"}, {"player.steps", 50, "50"}}), stats, InitialUser, 0); + check(nested.updates.size() == 1 && nested.updates[0].statId == 40 && stats.Value(InitialUser, 0, 40) == std::optional(50), "a nested property was not read"); + const auto latest = NpTrophy2::ApplyStatRules(rules, event("COLLECT", {{"counter", 2, "2"}, {"total", 1, "1"}}), stats, InitialUser, 0); + check(latest.updates.size() == 2 && stats.Value(InitialUser, 0, 10) == std::optional(2), "a stat did not keep the latest value"); +} + +} + +int main() { + const auto previous = std::filesystem::current_path(); + const auto root = std::filesystem::temp_directory_path() / ("anyps5-trophy-package-" + std::to_string(std::random_device{}())); + std::filesystem::create_directories(root); + std::filesystem::current_path(root); + testContainer(); + testJson(); + testPackage(); + testProgressAndPlatinum(); + testStatRules(); + testConditions(); + testStatStore(); + testApplyStatRules(); + std::filesystem::current_path(previous); + std::filesystem::remove_all(root); + std::puts("Trophy package tests passed"); + return 0; +} diff --git a/core/libs/prx/libSceNpTrophy2/tests/TrophyStore.cpp b/core/libs/prx/libSceNpTrophy2/tests/TrophyStore.cpp new file mode 100644 index 0000000000..10352c9edf --- /dev/null +++ b/core/libs/prx/libSceNpTrophy2/tests/TrophyStore.cpp @@ -0,0 +1,304 @@ +#include "prx/libSceNpTrophy2/include/NpTrophy2Types.hpp" +#include "prx/libSceNpTrophy2/include/TitleTrophies.hpp" +#include "prx/libSceNpTrophy2/include/TrophyStore.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif + +extern "C" { +int APS5_VABI sceNpTrophy2CreateContext(int*, int, uint32_t, uint64_t); +int APS5_VABI sceNpTrophy2DestroyContext(int); +int APS5_VABI sceNpTrophy2GetGameInfo(int, int, NpTrophy2GameDetails*, NpTrophy2GameData*); +int APS5_VABI sceNpTrophy2GetTrophyInfo(int, int, int, NpTrophy2Details*, NpTrophy2Data*); +int APS5_VABI sceNpTrophy2GetTrophyInfoArray(int, int, uint32_t, uint32_t, NpTrophy2Details*, NpTrophy2Data*, uint32_t*); +} + +namespace { + +constexpr std::int32_t InitialUser = 0x10000000; + +void check(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +template +bool throws(TAction action) { + try { + action(); + } catch (const std::exception&) { + return true; + } + return false; +} + +std::string read(const char* path) { + std::string text; + std::FILE* file = std::fopen(path, "rb"); + if (file == nullptr) return text; + char buffer[256]; + for (std::size_t count; (count = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, count); + std::fclose(file); + return text; +} + +void write(const char* path, const std::string& text) { + std::FILE* file = std::fopen(path, "wb"); + check(file != nullptr, "test file was not created"); + const bool written = std::fwrite(text.data(), 1, text.size(), file) == text.size(); + check(std::fclose(file) == 0 && written, "test file was not written"); +} + +void putBigEndian(std::string& data, std::size_t offset, std::uint64_t value, std::size_t size) { + for (std::size_t i = 0; i < size; ++i) data[offset + i] = static_cast((value >> (8 * (size - 1 - i))) & 0xFF); +} + +std::string ucp(const std::vector>& files) { + std::size_t offset = 0x60 + files.size() * 0x40; + std::size_t size = offset; + for (const auto& file : files) size += file.second.size(); + std::string data(size, '\0'); + putBigEndian(data, 0, 0xB228C60A, 4); + putBigEndian(data, 4, 1, 4); + putBigEndian(data, 8, size, 8); + putBigEndian(data, 0x10, files.size(), 4); + putBigEndian(data, 0x14, 0x40, 4); + std::size_t entry = 0x60; + for (const auto& [name, contents] : files) { + data.replace(entry, name.size(), name); + putBigEndian(data, entry + 0x20, offset, 8); + putBigEndian(data, entry + 0x28, contents.size(), 8); + data.replace(offset, contents.size(), contents); + offset += contents.size(); + entry += 0x40; + } + return data; +} + +class StderrCapture { +public: + StderrCapture() { + std::fflush(stderr); + file = std::tmpfile(); + check(file != nullptr, "cannot open a temporary file for stderr"); +#ifdef _WIN32 + saved = _dup(_fileno(stderr)); + check(saved >= 0 && _dup2(_fileno(file), _fileno(stderr)) == 0, "cannot capture stderr"); +#else + saved = dup(fileno(stderr)); + check(saved >= 0 && dup2(fileno(file), fileno(stderr)) == fileno(stderr), "cannot capture stderr"); +#endif + } + std::string Finish() { + std::fflush(stderr); +#ifdef _WIN32 + _dup2(saved, _fileno(stderr)); + _close(saved); +#else + dup2(saved, fileno(stderr)); + close(saved); +#endif + std::rewind(file); + std::string text; + char buffer[4096]; + for (std::size_t count; (count = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, count); + std::fclose(file); + std::fwrite(text.data(), 1, text.size(), stderr); + return text; + } + +private: + std::FILE* file = nullptr; + int saved = -1; +}; + +void testPersistence() { + const std::filesystem::path directory("store"); + { + NpTrophy2::TrophyStore store(directory); + check(!store.UnlockTick(InitialUser, 0, 12) && store.UnlockCount(InitialUser, 0) == 0, "a new store has unlocks"); + check(store.Unlock(InitialUser, 0, 12, 1000), "trophy 12 was not unlocked"); + check(!store.Unlock(InitialUser, 0, 12, 2000), "trophy 12 was unlocked twice"); + check(store.Unlock(InitialUser, 0, 3, 3000), "trophy 3 was not unlocked"); + check(store.Unlock(InitialUser + 1, 0, 12, 4000) && store.Unlock(InitialUser, 1, 12, 5000), "another user or service label shares unlocks"); + check(throws([&store] { store.Unlock(InitialUser, 0, -1, 6000); }), "a negative trophy id was unlocked"); + check(store.RecordPath(InitialUser, 0) == directory / "268435456_0.txt", "the record is not named after the user and service label"); + } + check(read("store/268435456_0.txt") == "3 3000\n12 1000\n", "the record does not list the unlocks"); + NpTrophy2::TrophyStore restarted(directory); + check(restarted.UnlockTick(InitialUser, 0, 12) == std::optional(1000) && restarted.UnlockTick(InitialUser, 0, 3) == std::optional(3000), + "unlock times did not survive a restart"); + check(restarted.UnlockCount(InitialUser, 0) == 2 && restarted.UnlockCount(InitialUser + 1, 0) == 1 && restarted.UnlockCount(InitialUser, 1) == 1 && restarted.UnlockCount(InitialUser, 2) == 0, + "unlock counts did not survive a restart"); + check(!restarted.Unlock(InitialUser, 0, 12, 7000) && restarted.UnlockTick(InitialUser, 0, 12) == std::optional(1000), "a restart forgot an unlock"); + check(restarted.Unlock(InitialUser, 0, 7, 8000) && read("store/268435456_0.txt") == "3 3000\n7 8000\n12 1000\n", "an unlock after a restart lost earlier unlocks"); +} + +void testDamagedRecords() { + const std::filesystem::path directory("damaged"); + for (const char* text : {"12\n", "12 x\n", "x 5\n", "-1 5\n", "12 5\n12 6\n", "12 5 7\n", "\n", "12 5\r\n"}) { + std::filesystem::remove_all(directory); + std::filesystem::create_directories(directory); + write("damaged/268435456_0.txt", text); + NpTrophy2::TrophyStore store(directory); + check(throws([&store] { store.UnlockCount(InitialUser, 0); }), "a damaged record was read"); + } +} + +struct Seen { + std::vector unlocks; +}; + +void remember(void* context, const NpTrophy2Unlock& unlock) { + static_cast(context)->unlocks.push_back(unlock); +} + +NpTrophy2Data trophyData(int context, int trophyId) { + NpTrophy2Data data{}; + check(sceNpTrophy2GetTrophyInfo(context, 1, trophyId, nullptr, &data) == 0, "GetTrophyInfo failed"); + check(data.trophy_id == trophyId, "GetTrophyInfo answered for another trophy"); + return data; +} + +std::uint32_t unlockedTrophies(int context) { + NpTrophy2GameData data{}; + check(sceNpTrophy2GetGameInfo(context, 1, nullptr, &data) == 0, "GetGameInfo failed"); + return data.unlocked_trophies; +} + +struct SlowListener { + std::atomic running{false}; + std::atomic finished{false}; +}; + +void slowly(void* context, const NpTrophy2Unlock&) { + auto* state = static_cast(context); + state->running = true; + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + state->finished = true; +} + +void testListenerRemovalWaitsForItsCallback() { + SlowListener state; + const auto listener = NpTrophy2AddUnlockListener_nid_no_patch(slowly, &state); + std::thread unlocking([] { NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 2, 1); }); + while (!state.running) std::this_thread::yield(); + NpTrophy2RemoveUnlockListener_nid_no_patch(listener); + const bool finished = state.finished; + unlocking.join(); + check(finished, "a listener was removed while its callback was still running, so its context could be freed under it"); +} + +void testQueries() { + int context = 0; + check(sceNpTrophy2CreateContext(&context, InitialUser, 0, 0) == 0, "context was not created"); + int other = 0; + check(sceNpTrophy2CreateContext(&other, InitialUser + 1, 0, 0) == 0 && other != context, "second context was not created"); + const auto locked = trophyData(context, 12); + check(!locked.unlocked && locked.timestamp_tick == 0, "trophy 12 is unlocked before its event"); + check(unlockedTrophies(context) == 0, "the game has unlocks before any event"); + + Seen seen; + const auto listener = NpTrophy2AddUnlockListener_nid_no_patch(remember, &seen); + const auto before = NpTrophy2::CurrentTick(); + check(NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 0, 12), "trophy 12 was not recorded"); + check(!NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 0, 12), "trophy 12 was recorded twice"); + check(NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 0, 0), "trophy 0 was not recorded"); + const auto after = NpTrophy2::CurrentTick(); + NpTrophy2RemoveUnlockListener_nid_no_patch(listener); + check(NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 0, 5) && seen.unlocks.size() == 2, "the listener did not see each new unlock once"); + const auto& first = seen.unlocks.front(); + check(first.titleId == "PPSA00000" && first.userId == InitialUser && first.serviceLabel == 0 && first.trophyId == 12 && first.tick >= before && first.tick <= after, + "the listener did not get the unlock's title, user, trophy and time"); + check(before > NpTrophy2::UnixEpochTick, "the current tick is not counted from year 1"); + + const auto unlocked = trophyData(context, 12); + check(unlocked.unlocked && unlocked.timestamp_tick == first.tick, "trophy 12 is not reported unlocked at its time"); + check(!trophyData(context, 13).unlocked, "trophy 13 is reported unlocked"); + check(unlockedTrophies(context) == 3, "the game does not count its unlocks"); + NpTrophy2Data listed{}; + std::uint32_t count = 0; + check(sceNpTrophy2GetTrophyInfoArray(context, 1, 0, 1, nullptr, &listed, &count) == 0 && count == 1 && listed.trophy_id == 0 && listed.unlocked, + "the trophy list does not report trophy 0 unlocked"); + check(!trophyData(other, 12).unlocked && unlockedTrophies(other) == 0, "another user sees the unlocks"); + check(read("_trophy/268435456_0.txt").find("12 ") != std::string::npos, "the unlock was not stored in the title folder"); + check(throws([] { NpTrophy2AddUnlockListener_nid_no_patch(nullptr, nullptr); }), "a null listener was added"); + check(throws([listener] { NpTrophy2RemoveUnlockListener_nid_no_patch(listener); }), "a removed listener was removed again"); + check(sceNpTrophy2DestroyContext(context) == 0 && sceNpTrophy2DestroyContext(other) == 0, "contexts were not destroyed"); +} + + +void testPostedUnlockRules() { + std::filesystem::create_directories("app0/sce_sys/trophy2"); + std::filesystem::create_directories("app0/sce_sys/uds"); + write("app0/sce_sys/trophy2/trophy04.ucp", ucp({{"tropconf.json", R"({"defaultLanguage":"en-US","trophies":[ + {"id":"0","grade":"P"}, + {"id":"1","grade":"G","platinumTrophyId":"0"}, + {"id":"2","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"40","comparator":"ge","targetValue":"5","progressive":true}}, + {"id":"3","grade":"B"}]})"}, + {"tropmeta_en-US.json", R"({"metadata":{"titleMetadata":{"name":"Rules"},"trophyMetadata":[]}})"}})); + write("app0/sce_sys/uds/uds04.ucp", ucp({{"stats_definition.json", R"({"statDefinitionArray":[{"statId":40,"dataType":"uint32","aggregation":"latest"}]})"}, + {"stats_extraction.json", R"({"statsExtractionRuleArray":[{"condition":{"eventName":"STEPS"},"action":{"input":"$.steps","output":{"statId":40}}}]})"}})); + check(NpTrophy2::Package(4) != nullptr, "the trophy data of service label 4 was not loaded"); + Seen seen; + const auto listener = NpTrophy2AddUnlockListener_nid_no_patch(remember, &seen); + StderrCapture capture; + const bool unknown = NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 4, 9); + const bool platinum = NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 4, 0); + const bool progressive = NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 4, 2); + const auto log = capture.Finish(); + check(!unknown && !platinum && !progressive, "an unknown, platinum or progressive trophy posted through UDS was recorded"); + check(NpTrophy2::TitleStore().UnlockCount(InitialUser, 4) == 0 && seen.unlocks.empty(), "a refused unlock reached the record or a listener"); + for (const char* line : {"[trophy] ignored the posted unlock of trophy 9 for user 268435456: the title's trophy data has no such trophy\n", + "[trophy] ignored the posted unlock of trophy 0 for user 268435456: it is the platinum, which unlocks with the last trophy that names it\n", + "[trophy] ignored the posted unlock of trophy 2 for user 268435456: it is progressive and unlocks through its statistic\n"}) { + check(log.find(line) != std::string::npos, "a refused unlock did not log its trophy and reason"); + } + check(NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 4, 1) && NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 4, 3), "an ordinary trophy posted through UDS was not recorded"); + check(!NpTrophy2::TitleStore().UnlockTick(InitialUser, 4, 0), "the platinum unlocked before every trophy that names it"); + NpTrophy2ApplyEvent_nid_no_patch(InitialUser, 4, NpTrophy2Event{"STEPS", {{"steps", 5, "5"}}}); + check(NpTrophy2::TitleStore().UnlockTick(InitialUser, 4, 2).has_value(), "the progressive trophy did not unlock through its statistic"); + check(NpTrophy2::TitleStore().UnlockTick(InitialUser, 4, 0).has_value(), "the platinum did not unlock with the last trophy that names it"); + NpTrophy2RemoveUnlockListener_nid_no_patch(listener); + std::vector seenIds; + for (const auto& unlock : seen.unlocks) seenIds.push_back(unlock.trophyId); + check(seenIds == std::vector{1, 3, 2, 0}, "the listener did not see the posted, statistic and platinum unlocks once, in order"); +} + +} + +int main() { + const auto previous = std::filesystem::current_path(); + const auto root = std::filesystem::temp_directory_path() / ("anyps5-trophy-store-" + std::to_string(std::random_device{}())); + std::filesystem::create_directories(root / "app0" / "sce_sys"); + std::filesystem::current_path(root); + write("app0/sce_sys/param.json", R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":"Example"}}})"); + testPersistence(); + testDamagedRecords(); + testQueries(); + testPostedUnlockRules(); + testListenerRemovalWaitsForItsCallback(); + std::filesystem::current_path(previous); + std::filesystem::remove_all(root); + std::puts("Trophy store tests passed"); + return 0; +} diff --git a/core/libs/prx/libSceNpTus/CMakeLists.txt b/core/libs/prx/libSceNpTus/CMakeLists.txt new file mode 100644 index 0000000000..1cb8f4a676 --- /dev/null +++ b/core/libs/prx/libSceNpTus/CMakeLists.txt @@ -0,0 +1,16 @@ +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) + if(WIN32) + set_tests_properties(guest_np_tus 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..a69e6cb5a6 --- /dev/null +++ b/core/libs/prx/libSceNpTus/Export.cpp @@ -0,0 +1,785 @@ +#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_TOO_MANY_SLOTID = static_cast(0x80550718); +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; + +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; +} +} + +extern "C" { + +int APS5_VABI sceNpTusCreateNpTitleCtxA(std::uint32_t serviceLabel, int userId) { + (void)serviceLabel; + (void)userId; + 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; +} + +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(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTssCreateNpTitleCtxA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +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 sceNpTusGetMultiUserVariable(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsVariable(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsVariableAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariable(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariable(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotDataVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotDataAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotDataVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetDataAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetDataVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsDataStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsDataStatusAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTssGetSmallStorage(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTssGetSmallStorageAsync(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 sceNpTusAddAndGetVariableA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableAVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableAVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusAVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusAVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataAVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataAVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataForCrossSaveAsync(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 sceNpTusGetMultiUserVariableA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableAVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableAVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusAVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusAVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync(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 sceNpTusSetDataA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetDataAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetDataAVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetDataAVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotDataA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotDataAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableAVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableAVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveVUserAsync(void) { + 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/libSceNpWebApi2/CMakeLists.txt b/core/libs/prx/libSceNpWebApi2/CMakeLists.txt new file mode 100644 index 0000000000..1fbafd2838 --- /dev/null +++ b/core/libs/prx/libSceNpWebApi2/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceNpWebApi2 SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceNpWebApi2 PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceNpWebApi2 PRIVATE libc libkernel) +configure_windows_unwind(libSceNpWebApi2) + +if(BUILD_TESTING) + add_test_executable(guest_np_web_api2_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpWebApi2.cpp) + target_include_directories(guest_np_web_api2_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_web_api2_tests PRIVATE libSceNpWebApi2 libkernel libc) + configure_windows_unwind(guest_np_web_api2_tests) + add_test(NAME guest_np_web_api2 COMMAND guest_np_web_api2_tests) + if(WIN32) + set_tests_properties(guest_np_web_api2 PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceNpWebApi2/Export.cpp b/core/libs/prx/libSceNpWebApi2/Export.cpp index 2c3e02d707..bb51fabeaa 100644 --- a/core/libs/prx/libSceNpWebApi2/Export.cpp +++ b/core/libs/prx/libSceNpWebApi2/Export.cpp @@ -3,12 +3,32 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include +#include +#include +#include // 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 std::atomic g_nextHandle{1}; +struct NpWebApi2MemoryPoolStats { + size_t pool_size; + size_t max_inuse_size; + size_t current_inuse_size; + int32_t reserved; +}; + +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; +} + extern "C" { int APS5_VABI sceNpWebApi2AbortRequest(int64_t request_id) { @@ -23,6 +43,11 @@ int APS5_VABI sceNpWebApi2AddHttpRequestHeader(int64_t request_id, const char* f return 0; } +int APS5_VABI sceNpWebApi2AddWebTraceTag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + void APS5_VABI sceNpWebApi2CheckTimeout(void) { } @@ -70,8 +95,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; - (void)pool_size; - return g_nextHandle.fetch_add(1, std::memory_order_relaxed); + return CreateLibraryContext(pool_size); +} + +int APS5_VABI sceNpWebApi2GetMemoryPoolStats(int lib_ctx_id, NpWebApi2MemoryPoolStats* stats) { + if (stats == nullptr) APS5_INVALID_ARG_EX; + 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}; + return 0; } int APS5_VABI sceNpWebApi2PushEventCreateFilter(int lib_ctx_id, int handle_id, const char* np_service_name, uint32_t np_service_label, const void* filter_param, size_t filter_param_num) { @@ -89,6 +122,13 @@ int APS5_VABI sceNpWebApi2PushEventCreateHandle(int lib_ctx_id) { return g_nextHandle.fetch_add(1, std::memory_order_relaxed); } +int APS5_VABI sceNpWebApi2PushEventAbortHandle(int libCtxId, int handleId) { + (void)handleId; + std::lock_guard lock(g_poolsMutex); + if (!g_pools.contains(libCtxId)) throw std::invalid_argument("sceNpWebApi2PushEventAbortHandle: unknown context"); + return 0; +} + int APS5_VABI sceNpWebApi2PushEventDeleteHandle(int lib_ctx_id, int handle_id) { (void)lib_ctx_id; (void)handle_id; @@ -125,7 +165,8 @@ int APS5_VABI sceNpWebApi2SendRequest(int64_t request_id, const void* data, size } int APS5_VABI sceNpWebApi2Terminate(int lib_ctx_id) { - (void)lib_ctx_id; + std::lock_guard lock(g_poolsMutex); + g_pools.erase(lib_ctx_id); return 0; } @@ -137,6 +178,14 @@ int APS5_VABI sceNpWebApi2PushEventDeleteFilter() { return 0; } +int APS5_VABI sceNpWebApi2PushEventSetHandleTimeout(int lib_ctx_id, int handle_id, uint32_t timeout) { + (void)handle_id; + (void)timeout; + std::lock_guard lock(g_poolsMutex); + if (!g_pools.contains(lib_ctx_id)) throw std::invalid_argument("sceNpWebApi2PushEventSetHandleTimeout: unknown context"); + return 0; +} + int APS5_VABI sceNpWebApi2PushEventRegisterPushContextCallback() { return g_nextHandle.fetch_add(1, std::memory_order_relaxed); } @@ -153,7 +202,58 @@ int APS5_VABI sceNpWebApi2PushEventUnregisterPushContextCallback() { return 0; } -int APS5_VABI sceNpWebApi2SetRequestTimeout() { +int APS5_VABI sceNpWebApi2SetRequestTimeout(int64_t request_id, uint32_t timeout) { + (void)request_id; + (void)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 new file mode 100644 index 0000000000..32be01d2dd --- /dev/null +++ b/core/libs/prx/libSceOpusCeltDec/CMakeLists.txt @@ -0,0 +1,16 @@ +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 ffmpeg) +configure_windows_unwind(libSceOpusCeltDec) + +if(BUILD_TESTING) + add_test_executable(guest_opus_celt_dec_tests ${LIBS_INCLUDE_DIR}/tests/GuestOpusCeltDec.cpp) + target_include_directories(guest_opus_celt_dec_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_opus_celt_dec_tests PRIVATE libSceOpusCeltDec libc) + configure_windows_unwind(guest_opus_celt_dec_tests) + add_test(NAME guest_opus_celt_dec COMMAND guest_opus_celt_dec_tests) + if(WIN32) + set_tests_properties(guest_opus_celt_dec PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceOpusCeltDec/Export.cpp b/core/libs/prx/libSceOpusCeltDec/Export.cpp new file mode 100644 index 0000000000..2b04cca7be --- /dev/null +++ b/core/libs/prx/libSceOpusCeltDec/Export.cpp @@ -0,0 +1,55 @@ +#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* context) { return library.Initialize(context); } + +int APS5_VABI sceOpusCeltDecTerminate(std::uint32_t* context) { return library.Terminate(context); } + +int APS5_VABI sceOpusCeltDecGetSize(int channels) { return library.GetSize(channels); } + +int APS5_VABI sceOpusCeltDecCreateEx(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 sceOpusCeltDecCtl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltDecDecodeFloat() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltDecDecodeFloatWithPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +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..df4e7d1854 --- /dev/null +++ b/core/libs/prx/libSceOpusCeltEnc/Export.cpp @@ -0,0 +1,63 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceOpusCeltEncCreate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncCreateEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncCtl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncDestroy() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncEncode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncEncodeFloat() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncEncodeFloatWithPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncEncodeWithPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncGetSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceOpusDec/Export.cpp b/core/libs/prx/libSceOpusDec/Export.cpp index 3e1729acb6..a02f1d05be 100644 --- a/core/libs/prx/libSceOpusDec/Export.cpp +++ b/core/libs/prx/libSceOpusDec/Export.cpp @@ -1,137 +1,69 @@ #include "prx/libc/include/general/VabiMacros.hpp" -#include -#include +#include "prx/libSceOpusDec/include/OpusDecoder.hpp" #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; - ~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(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; + 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->nb_samples != samples || frame->ch_layout.nb_channels != decoder.channels || frame->sample_rate != 48000 || - (format != AV_SAMPLE_FMT_FLTP && format != AV_SAMPLE_FMT_FLT)) - throw std::runtime_error("OpusDec: unexpected FFmpeg PCM format"); - for (int sample = 0; sample < samples; ++sample) { - for (int channel = 0; channel < decoder.channels; ++channel) { - const auto* source = reinterpret_cast(frame->extended_data[format == AV_SAMPLE_FMT_FLTP ? channel : 0]); - const float value = source[format == AV_SAMPLE_FMT_FLTP ? sample : sample * decoder.channels + channel]; - const auto converted = static_cast(std::lrint(std::clamp(value * 32768.0f, -32768.0f, 32767.0f))); - std::memcpy(reinterpret_cast(pcm) + (sample * decoder.channels + channel) * 2, &converted, 2); - } - } - 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..eef8d86d52 --- /dev/null +++ b/core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp @@ -0,0 +1,188 @@ +#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) fail("null context output"); + 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(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) fail("only mono and stereo are supported"); + return 640; + } + + int CreateEx(std::uint32_t* context, void* state, int sampleRate, int channels) { + if (!context || !state) fail("null context or state"); + GetSize(channels); + if (sampleRate != 8000 && sampleRate != 12000 && sampleRate != 16000 && sampleRate != 24000 && sampleRate != 48000) + fail("sample rates other than 8000, 12000, 16000, 24000 and 48000 Hz are not supported"); + std::lock_guard lock(mutex); + if (!contexts.contains(*context)) fail("unknown context"); + if (decoders.contains(state)) fail("state already owns a decoder"); + auto decoder = std::make_unique(); + decoder->context = *context; + decoder->channels = channels; + decoder->sampleRate = sampleRate; + const auto* codec = avcodec_find_decoder(AV_CODEC_ID_OPUS); + if (!codec) fail("FFmpeg Opus decoder unavailable"); + decoder->codec = avcodec_alloc_context3(codec); + 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 (!data || bytes <= 0 || bytes > std::numeric_limits::max() - AV_INPUT_BUFFER_PADDING_SIZE || !pcm || capacity < 0) + fail("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()) fail("unknown decoder state"); + auto& decoder = *found->second; + if (celtOnly && (data[0] >> 3) < 16) fail("SILK and hybrid packets are not implemented"); + const int samples = packetSamples(data, bytes) / (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) { + std::lock_guard lock(mutex); + if (decoders.erase(state) == 0) fail("unknown decoder state"); + 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; + + [[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..201dab2e4b --- /dev/null +++ b/core/libs/prx/libSceOpusSilkEnc/Export.cpp @@ -0,0 +1,78 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceOpusSilkEncCancel() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncCreate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncCreateEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncCtl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncDestroy() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncEncode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncEncodeFloat() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncEncodeFloatWithPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncEncodeFloatWithPriorityAndTimeout() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncEncodeWithPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncEncodeWithPriorityAndTimeout() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncGetSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncTerminate() { + 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 a4ed0f81f0..b979e5344c 100644 --- a/core/libs/prx/libScePad/CMakeLists.txt +++ b/core/libs/prx/libScePad/CMakeLists.txt @@ -8,7 +8,7 @@ configure_windows_unwind(libScePad) if(BUILD_TESTING) add_test_executable(guest_pad_tests ${LIBS_INCLUDE_DIR}/tests/GuestPad.cpp) - target_include_directories(guest_pad_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/SDL2/include) + target_include_directories(guest_pad_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) target_link_libraries(guest_pad_tests PRIVATE libScePad libc) configure_windows_unwind(guest_pad_tests) add_test(NAME guest_pad COMMAND guest_pad_tests) @@ -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 26bc9347ec..362519358f 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 @@ -16,6 +17,7 @@ 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; @@ -35,6 +37,21 @@ int APS5_VABI scePadClose_nid_postfix(int handle) { return PAD_OK; } +int APS5_VABI scePadVrControllerSetVibration() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadVrControllerSetVibrationMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadVrControllerReadCombinedState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI scePadDeviceClassGetExtendedInformation(int handle, PadDeviceClassExtendedInformation* info) { if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; if (info == nullptr) return PAD_ERROR_INVALID_ARG; @@ -61,8 +78,8 @@ 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->touchPadInfo.resolution.x = kTouchPadWidth; + info->touchPadInfo.resolution.y = kTouchPadHeight; info->stickInfo.deadZoneLeft = 2; info->stickInfo.deadZoneRight = 2; info->connectionType = PAD_CONNECTION_TYPE_LOCAL; @@ -73,7 +90,7 @@ int APS5_VABI scePadGetControllerInformation(int handle, PadControllerInformatio } int APS5_VABI scePadGetHandle(int user_id, int type, int index) { - if (!g_opened || !ValidPort(user_id, type, index)) return PAD_ERROR_DEVICE_NO_HANDLE; + if (!g_opened || !ValidPort(user_id, type, index) || type == PAD_PORT_TYPE_SPECIAL) return PAD_ERROR_DEVICE_NO_HANDLE; return PAD_HANDLE; } @@ -105,8 +122,8 @@ 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->touchPadInfo.resolution.x = kTouchPadWidth; + base->touchPadInfo.resolution.y = kTouchPadHeight; base->stickInfo.deadZoneLeft = 2; base->stickInfo.deadZoneRight = 2; base->connectionType = PAD_CONNECTION_TYPE_LOCAL; @@ -128,6 +145,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; } @@ -136,6 +154,7 @@ int APS5_VABI scePadOpen_nid_postfix(int userId, int type, int index, const void if (!ValidPort(userId, type, index)) { return PAD_ERROR_INVALID_ARG; } + if (type == PAD_PORT_TYPE_SPECIAL) return PAD_ERROR_DEVICE_NOT_CONNECTED; g_opened = true; return PAD_HANDLE; } @@ -156,19 +175,15 @@ int APS5_VABI scePadSetFeatureReport() { NotImplemented_nid_no_patch(__func__); int APS5_VABI scePadOutputReport() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadReadState(int handle, PadData* data); - int APS5_VABI scePadRead_nid_postfix(int handle, PadData* data, int num) { - // The title drains the queued states; one current state is reported per call. - if (data == nullptr || num <= 0) APS5_INVALID_ARG_EX; - const int result = scePadReadState(handle, data); - if (result != 0) return result; - return 1; + if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; + 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(); @@ -189,7 +204,7 @@ int APS5_VABI scePadResetOrientation(int handle) { int APS5_VABI scePadSetAngularVelocityDeadbandState(int handle, bool enable) { if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; - if (enable) NotImplemented_nid_no_patch(__func__); + Pad::SetAngularVelocityDeadband(enable); return PAD_OK; } @@ -239,8 +254,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 > 2) return PAD_ERROR_INVALID_ARG; + if (mode > 3 && mode != 5) return PAD_ERROR_INVALID_ARG; Pad::SetVibrationMode(mode); return PAD_OK; } @@ -260,6 +277,28 @@ int APS5_VABI scePadVrControllerRead() { return 0; } +int APS5_VABI scePadVrControllerGetTriggerEffectState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadVrControllerReadState(int handle, void* state) { + (void)handle; + (void)state; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadVrControllerSetTriggerEffect() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadVrControllerSetTriggerEffects() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI scePadIsRemoteController(int handle, bool* remote) { if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; if (remote == nullptr) return PAD_ERROR_INVALID_ARG; @@ -268,9 +307,772 @@ int APS5_VABI scePadIsRemoteController(int handle, bool* remote) { } -int APS5_VABI scePadSetAngularVelocityBiasCorrectionState() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI scePadSetAngularVelocityBiasCorrectionState(int handle, bool enabled) { + if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; + if (enabled) NotImplemented_nid_no_patch(__func__); + return PAD_OK; +} + +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(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +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(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadLastLotusButtonData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadReadStateExt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetExtensionReport(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetExtensionUnitInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadEnableExtensionPort(void) { + return 0; +} + +int APS5_VABI scePadEnableSpecificDeviceClass(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadOpenExt2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadDeviceOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetDataInternal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetUserColor(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadDisableVibration(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetVibrationStrength(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetLightBarBaseBrightness(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetLightBarBlinking(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetIdleCount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetControllerIdleCount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetPowerSavingState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetAutoPowerOffCount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadEnableUsbConnection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadEnableAutoDetect(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadEnablePlayPauseEmulation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadDisableTrackingLedBatterySaver(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetLoginUserNumber(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetCapability(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadShareOutputData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadShareOutputDataWithFunctionType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetVibrationForce(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetVersionInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetControllerType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadDisableDataConversion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetButtonRemappingInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetProcessPrivilegeOfButtonRemapping(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetForceIntercepted(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadIsLightBarBaseBrightnessControllable(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSwitchConnection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadDisableTriggerEffect(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetTriggerEffectStrength(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetTriggerEffectInternalState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetUserNumber(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadDisableTransportButtons(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetLotusIrCodeSetStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetLotusIrCodeSetData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetLotusIrCodeSetEnd(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetLotusIrCodeSetResult(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetLotusEnableIrKey(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetLotusDisableIrKey(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetUsbDetectionState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadUiReadState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadUiReadStateForTuning(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetStickTriggerTuningMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetStickTuningParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetTriggerTuningParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetProfileHunt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetProfileHunt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadResetHunt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetProfileInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetActiveProfileInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadDisableActiveProfileChange(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadDisableActiveProfileChangeByDeviceId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetActiveProfileSlot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetSavedSettingInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetSavedSettingInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadVrControllerGetPhysicalUsbConnectionStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadParseUniqueDataLeonardo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetControllerTypeLeonardo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetControllerTypeLeonardo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetProfileLeonardo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetProfileLeonardo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadResetLeonardo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetProfileEditMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetProfileEditMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetProfileEditModeLeonardo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetSelectButtonCustomizeMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetSelectButtonCustomizeMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetToggleAllReleaseLeonardo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadEnableControllerIndicator(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadEnableProfileIndicator(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetProfileIndicatorAnimation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadIsCoPilot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetIsCoPilot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadRegisterCollaboInputMgrProcess(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetTelemetryInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetActiveTime(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetDeviceId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetInfoByPortType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadResetLightBarAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadResetLightBarAllByPortType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetLightBarForTracker(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadReadForTracker(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadReadBlasterForTracker(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadResetOrientationForTracker(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetMotionTimerUnit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetVrTrackingMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetMotionSensorPosition(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetSphereRadius(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadIsValidHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadIsDS4Connected(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadIsBlasterConnected(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadIsMoveConnected(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetBluetoothAddress(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadIsMoveReproductionModel(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadReadAstonForTracker(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetAstonModel(void) { + 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(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadDisconnectPort(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadDisconnectDevice(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetDeviceInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetProcessFocus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadVertualDeviceAddDevice(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadVirtualDeviceAddDevice(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadVirtualDeviceDeleteDevice(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadVirtualDeviceInsertData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadVirtualDeviceInsertTriggerInternalState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadVirtualDeviceGetRemoteSetting(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadVirtualDeviceSetTriggerEffectSetting(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadVirtualDeviceDisableButtonRemapping(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetShareButtonMaskForRemotePlay(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetPS4BcVibrationMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetVirtualDeviceComponentInformation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadEnablePs4CompatibleMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetLicenseControllerInformation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadReadHistory(void) { + 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(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadRemoteControlInsertDataForAston(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetFeatureReport2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetFeatureReport2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetMuteLed(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadBondUpdateInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadBondUpdateIsReady(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadBondUpdateStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadBondUpdateGetStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadBondUpdateSendData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadBondUpdateResetBond(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadBondUpdateGetReconnectStatus(void) { + 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(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadControllerUpdateIsReady(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadControllerUpdateStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadControllerUpdateGetStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadControllerUpdateSendData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadControllerUpdateResetController(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadControllerUpdateGetReconnectStatus(void) { + 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 new file mode 100644 index 0000000000..e372a1f3f7 --- /dev/null +++ b/core/libs/prx/libScePad/include/InputCommandFile.hpp @@ -0,0 +1,148 @@ +#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 { +public: + struct Input { + 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 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 > std::numeric_limits::max() - 100000 + ? std::numeric_limits::max() : now + 100000; + auto command = read(); + if (command.sequence > lastSequence) { + lastSequence = command.sequence; + events = std::move(command.events); + started = now; + } + } + 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; + std::vector events; + }; + + Command read() const { + std::ifstream file(path); + char line[128]{}; + if (!file.getline(line, sizeof(line))) return {}; + 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; + if (count != 3 && count != 7) return {}; + std::uint64_t values[7]{}; + for (int i = 0; i < count; ++i) { + const char* first = tokens[i].data() + (tokens[i].front() == '+' ? 1 : 0); + const char* last = tokens[i].data() + tokens[i].size(); + 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 | TouchLeftBit | TouchRightBit)) != 0 || values[2] == 0 || values[2] > 5000) return {}; + Command command; + command.sequence = values[0]; + 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 {}; + input.sticks[i - 3] = static_cast(values[i]); + } + input.hasSticks = count == 7; + command.events.push_back({0, values[2] * 1000, input}); + return command; + } + + 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 started = 0; + std::vector events; +}; +} + +#endif diff --git a/core/libs/prx/libScePad/include/InputMapping.hpp b/core/libs/prx/libScePad/include/InputMapping.hpp index ea3513278e..ec4da4ab47 100644 --- a/core/libs/prx/libScePad/include/InputMapping.hpp +++ b/core/libs/prx/libScePad/include/InputMapping.hpp @@ -11,12 +11,35 @@ namespace Pad { -inline constexpr int MousePollIntervalMs = 33; inline constexpr int WheelPressDurationMs = 80; -inline constexpr double MouseSensitivity = 1.0; +inline constexpr double DefaultMouseSensitivity = 1.0; +inline constexpr double MaxMouseSensitivity = 10.0; +inline constexpr int MaxStickDeadzonePercent = 90; + +struct InputSettings { + int leftStickDeadzonePercent = 0; + int rightStickDeadzonePercent = 0; + double mouseSensitivity = DefaultMouseSensitivity; + double mouseVerticalScale = 1.0; + double mouseCurve = 1.0; + double mouseDeadzoneOffset = 0.2; + double mouseMaxSpeed = 1.0; + double mouseSmoothingMs = 10.0; + double mouseDecayMs = 30.0; + double stickModifierScale = 0.5; + double keyboardRampMs = 0.0; + double triggerRampMs = 0.0; +}; + +struct InputConfiguration { + std::vector bindings; + InputSettings settings; +}; 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}, @@ -24,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}, @@ -44,14 +68,18 @@ 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} }; -std::vector LoadInputMapping(); +InputConfiguration LoadInputMapping(); +std::array StickWithDeadzone(std::int16_t x, std::int16_t y, int deadzonePercent); } diff --git a/core/libs/prx/libScePad/include/KeyboardMouse.hpp b/core/libs/prx/libScePad/include/KeyboardMouse.hpp new file mode 100644 index 0000000000..14002488e1 --- /dev/null +++ b/core/libs/prx/libScePad/include/KeyboardMouse.hpp @@ -0,0 +1,104 @@ +#ifndef CORE_LIBS_PRX_LIBSCEPAD_KEYBOARDMOUSE_HPP +#define CORE_LIBS_PRX_LIBSCEPAD_KEYBOARDMOUSE_HPP + +#include +#include +#include +#include +#include "SDL_events.h" + +#include "InputMapping.hpp" +#include "PadState.hpp" + +namespace Pad { + +inline constexpr double MouseFullTiltCountsPerSecond = 3000.0; +inline constexpr double MouseMinimumStepSeconds = 0.0005; +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: + explicit MouseStick(const InputSettings& settings); + void Move(int deltaX, int deltaY); + 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; +}; + +class KeyboardMouse { +public: + using Clock = std::chrono::steady_clock; + + explicit KeyboardMouse(InputConfiguration configuration); + InputRequests HandleEvent(const SDL_Event& event, Clock::time_point now); + bool Advance(Clock::time_point now); + void SetMouseMode(bool enabled); + void 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); + +} + +#endif 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 c23b02c4d2..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,7 +55,9 @@ 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); void SetVibrationMode(int mode); void SetLightBar(bool valid, std::uint8_t r, std::uint8_t g, std::uint8_t b); @@ -58,10 +65,14 @@ void SetTriggerCommand(int trigger, const std::uint8_t* command); void ResetOrientation(); void SetMotionEnabled(bool enabled); void SetTiltCorrection(bool enabled); +void SetAngularVelocityDeadband(bool enabled); } extern "C" void PadPublishInput_nid_postfix(const PadInputState& input); 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 ea3e212d46..5a6795f32e 100644 --- a/core/libs/prx/libScePad/src/PadState.cpp +++ b/core/libs/prx/libScePad/src/PadState.cpp @@ -1,17 +1,37 @@ #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 +#include #include #include 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 { + PadInputState input; + std::uint64_t timestamp = 0; + }; + constexpr std::size_t kQueueCapacity = 64; + std::deque queue; std::exception_ptr failure; bool initialized = false; @@ -20,10 +40,23 @@ namespace { std::uint64_t lastFuseTime = 0; float biasIntegral[3] = {0.0f, 0.0f, 0.0f}; bool tiltCorrection = true; + bool angularVelocityDeadband = false; + constexpr float kAngularVelocityDeadband = 0.017453292f; std::uint8_t nextTouchId = 0; bool prevTouchActive[2] = {false, false}; std::uint8_t touchIds[2] = {0, 0}; + 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; } @@ -93,28 +126,41 @@ void Pad::Initialize() { if (!initialized) { timestamp = sceKernelGetProcessTime(); lastFuseTime = timestamp; + queue.clear(); + pressedSinceRead = 0; } initialized = true; } -PadData Pad::ReadState() { +bool Pad::IsInitialized() { std::lock_guard lock(stateMutex); - if (failure) std::rethrow_exception(failure); - if (!initialized) throw std::runtime_error("Pad: read before initialization"); - const std::uint64_t now = sceKernelGetProcessTime(); + return initialized; +} + +namespace { +PadData BuildData(const PadInputState& state, std::uint64_t changeTime, std::uint64_t now, const Pad::InputCommandFile::Input& injected) { PadData data{}; - data.buttons = state.buttons; - data.left_stick_x = state.sticks[0]; - data.left_stick_y = state.sticks[1]; - data.right_stick_x = state.sticks[2]; - data.right_stick_y = state.sticks[3]; - data.analog_buttons_l2 = std::max(state.analogButtonsL2, (state.buttons & 0x100) != 0 ? 255 : 0); - data.analog_buttons_r2 = std::max(state.analogButtonsR2, (state.buttons & 0x200) != 0 ? 255 : 0); + data.buttons = state.buttons | injected.buttons; + const auto& sticks = injected.hasSticks ? injected.sticks : state.sticks; + data.left_stick_x = sticks[0]; + data.left_stick_y = sticks[1]; + data.right_stick_x = sticks[2]; + data.right_stick_y = sticks[3]; + const auto trigger = [&](std::uint8_t analog, std::uint32_t bit) -> std::uint8_t { + return (injected.buttons & bit) != 0 || (analog == 0 && (state.buttons & bit) != 0) ? 255 : analog; + }; + data.analog_buttons_l2 = trigger(state.analogButtonsL2, 0x100); + data.analog_buttons_r2 = trigger(state.analogButtonsR2, 0x200); const bool live = state.hasMotion && output.motionEnabled; const std::array rest{0.0f, 9.80665f, 0.0f}; const std::array& accel = live ? state.accel : rest; - const std::array gyro = live ? state.gyro : std::array{0.0f, 0.0f, 0.0f}; + std::array gyro = live ? state.gyro : std::array{0.0f, 0.0f, 0.0f}; + if (angularVelocityDeadband) { + for (float& axis : gyro) { + if (axis < kAngularVelocityDeadband && axis > -kAngularVelocityDeadband) axis = 0.0f; + } + } if (live) { float dt = lastFuseTime != 0 && now > lastFuseTime ? static_cast(now - lastFuseTime) * 1e-6f : 0.0f; dt = std::min(dt, 0.1f); @@ -138,7 +184,7 @@ PadData Pad::ReadState() { data.connected = true; data.connected_count = 1; - data.timestamp = timestamp; + data.timestamp = changeTime; std::uint8_t touchNum = 0; for (int i = 0; i < 2; ++i) { @@ -153,17 +199,73 @@ PadData Pad::ReadState() { } ++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; return data; } +Pad::InputCommandFile::Input InjectedInput(std::uint64_t now) { + 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) { + lastInjected = injected; + timestamp = now; + } + return injected; +} +} + +PadData Pad::ReadState() { + std::lock_guard lock(stateMutex); + if (failure) std::rethrow_exception(failure); + if (!initialized) throw std::runtime_error("Pad: read before initialization"); + const std::uint64_t now = sceKernelGetProcessTime(); + const auto injected = InjectedInput(now); + 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) { + if (data == nullptr || num <= 0) throw std::invalid_argument("Pad: invalid read buffer"); + 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()) { + data[0] = BuildData(state, timestamp, now, injected); + return 1; + } + int count = 0; + while (count < num && !queue.empty()) { + const QueuedInput& entry = queue.front(); + data[count++] = BuildData(entry.input, entry.timestamp, now, injected); + queue.pop_front(); + } + return count; +} + void Pad::SetVibration(std::uint8_t large, std::uint8_t small) { std::lock_guard lock(stateMutex); if (output.vibrationLarge == large && output.vibrationSmall == small) return; @@ -263,6 +365,11 @@ void Pad::SetTiltCorrection(bool enabled) { biasIntegral[0] = biasIntegral[1] = biasIntegral[2] = 0.0f; } +void Pad::SetAngularVelocityDeadband(bool enabled) { + std::lock_guard lock(stateMutex); + angularVelocityDeadband = enabled; +} + void Pad::SetMotionEnabled(bool enabled) { std::lock_guard lock(stateMutex); if (output.motionEnabled == enabled) return; @@ -278,8 +385,18 @@ 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}); } extern "C" void PadReportInputFailure_nid_postfix(std::exception_ptr error) { @@ -290,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..fb34206c21 --- /dev/null +++ b/core/libs/prx/libScePadTracker/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/libScePatchCheckerClient/Export.cpp b/core/libs/prx/libScePatchCheckerClient/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libScePatchCheckerClient/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/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 58d081c3d0..576b4c4f0c 100644 --- a/core/libs/prx/libScePlayGo/Export.cpp +++ b/core/libs/prx/libScePlayGo/Export.cpp @@ -2,10 +2,14 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include +#include +#include #include #include #include #include +#include #include // The game is fully installed on the host, so every chunk is local and nothing is pending. @@ -16,7 +20,9 @@ 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; @@ -24,14 +30,50 @@ static constexpr int SCE_PLAYGO_ERROR_BAD_CHUNK_ID = static_cast(0x80B2000C static constexpr int SCE_PLAYGO_ERROR_BAD_SIZE = static_cast(0x80B2000B); static constexpr int SCE_PLAYGO_ERROR_BAD_LOCUS = static_cast(0x80B20010); -// The package's chunk table is not part of the dump, so the chunk set comes from the title's -// playgo-chunkdefs.xml: every listed chunk plus chunks 0 through the default chunk. Games probe -// chunk IDs and size arrays from the result, so unknown IDs must be rejected. -static const std::set& ValidChunks() { - static const std::set chunks = [] { - std::set result{0}; +struct ChunkTable { + std::set ids{0}; + bool defined = false; + bool scenario = false; +}; + +static bool ReadPackageChunks(ChunkTable& table) { + constexpr std::size_t CountOffset = 0x0A; + std::ifstream file(ResolvePath_nid_no_patch("/app0/sce_sys/playgo-chunk.dat"), std::ios::binary); + char header[CountOffset + sizeof(uint16_t)]{}; + if (!file) 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 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(); + for (uint32_t id = 0; id < count; ++id) table.ids.insert(static_cast(id)); + table.defined = true; + return true; +} + +static void RejectUnreadablePlayGoFiles() { + std::error_code error; + const std::filesystem::path directory = ResolvePath_nid_no_patch("/app0/sce_sys"); + for (const auto& entry : std::filesystem::directory_iterator(directory, error)) { + const auto name = entry.path().filename().string(); + if (name.rfind("playgo-", 0) == 0) + throw std::runtime_error("scePlayGo: sce_sys/" + name + " is present but no chunk table (playgo-chunk.dat) or scenario file (playgo-scenario.json) is, so the chunk ids are unknown"); + } +} + +static const ChunkTable& Chunks() { + static const ChunkTable chunks = [] { + ChunkTable table; std::ifstream file(ResolvePath_nid_no_patch("/app0/playgo-chunkdefs.xml")); - if (!file) return result; + if (!file) { + 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; + auto& result = table.ids; const std::string text((std::istreambuf_iterator(file)), std::istreambuf_iterator()); static const std::regex chunk(R"re(& ValidChunks() { const auto last = std::stoul(match[1].str()); for (unsigned long id = 0; id <= last && id <= 0xFFFF; ++id) result.insert(static_cast(id)); } - return result; + return table; }(); return chunks; } +enum class UnlistedChunkAnswer { Stop, Installed, Absent }; + +static UnlistedChunkAnswer UnlistedChunks() { + static const UnlistedChunkAnswer answer = [] { + const char* text = std::getenv("APS5_PLAYGO_UNLISTED_CHUNKS"); + if (text == nullptr || text[0] == '\0') return UnlistedChunkAnswer::Stop; + const std::string value(text); + if (value == "installed") return UnlistedChunkAnswer::Installed; + if (value == "absent") return UnlistedChunkAnswer::Absent; + throw std::runtime_error("scePlayGo: APS5_PLAYGO_UNLISTED_CHUNKS must be installed or absent, not \"" + value + "\""); + }(); + return answer; +} + +static bool IsKnownChunk(uint16_t id) { + const auto& chunks = Chunks(); + if (chunks.defined) return chunks.ids.contains(id); + if (chunks.scenario || id == 0) return true; + 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) { if (!chunk_ids) return SCE_PLAYGO_ERROR_BAD_POINTER; if (number_of_entries == 0) return SCE_PLAYGO_ERROR_BAD_SIZE; - const auto& valid = ValidChunks(); for (uint32_t index = 0; index < number_of_entries; ++index) - if (!valid.contains(chunk_ids[index])) return SCE_PLAYGO_ERROR_BAD_CHUNK_ID; + if (!IsKnownChunk(chunk_ids[index])) return SCE_PLAYGO_ERROR_BAD_CHUNK_ID; return 0; } @@ -66,7 +133,7 @@ int APS5_VABI scePlayGoClose(int handle) { int APS5_VABI scePlayGoGetChunkId(int handle, uint16_t* out_chunk_id_list, uint32_t number_of_entries, uint32_t* out_entries) { if (handle != PLAYGO_HANDLE) return SCE_PLAYGO_ERROR_BAD_HANDLE; if (!out_entries) return SCE_PLAYGO_ERROR_BAD_POINTER; - const auto& valid = ValidChunks(); + const auto& valid = Chunks().ids; if (!out_chunk_id_list) { *out_entries = static_cast(valid.size()); return 0; @@ -118,7 +185,7 @@ int APS5_VABI scePlayGoGetOptionalChunk(int handle, int32_t type, PlayGoOptional if (handle != PLAYGO_HANDLE) return SCE_PLAYGO_ERROR_BAD_HANDLE; (void)type; if (!option) return SCE_PLAYGO_ERROR_BAD_POINTER; - option->bitmask = 0; + option->bitmask = PLAYGO_OPTIONAL_CHUNK_MASK_ALL; return 0; } @@ -135,7 +202,7 @@ int APS5_VABI scePlayGoGetSupportedOptionalChunk(int handle, int32_t type, PlayG if (handle != PLAYGO_HANDLE) return SCE_PLAYGO_ERROR_BAD_HANDLE; (void)type; if (!option) return SCE_PLAYGO_ERROR_BAD_POINTER; - option->bitmask = 0; + option->bitmask = PLAYGO_OPTIONAL_CHUNK_MASK_ALL; return 0; } @@ -183,9 +250,8 @@ int APS5_VABI scePlayGoSetToDoList(int handle, const PlayGoToDo* todo_list, uint if (handle != PLAYGO_HANDLE) return SCE_PLAYGO_ERROR_BAD_HANDLE; if (!todo_list) return SCE_PLAYGO_ERROR_BAD_POINTER; if (number_of_entries == 0) return SCE_PLAYGO_ERROR_BAD_SIZE; - const auto& valid = ValidChunks(); for (uint32_t index = 0; index < number_of_entries; ++index) { - if (!valid.contains(todo_list[index].chunk_id)) return SCE_PLAYGO_ERROR_BAD_CHUNK_ID; + if (!IsKnownChunk(todo_list[index].chunk_id)) return SCE_PLAYGO_ERROR_BAD_CHUNK_ID; const int8_t locus = todo_list[index].locus; if (locus != PLAYGO_LOCUS_NOT_DOWNLOADED && locus != PLAYGO_LOCUS_LOCAL_SLOW && locus != PLAYGO_LOCUS_LOCAL_FAST) return SCE_PLAYGO_ERROR_BAD_LOCUS; } @@ -196,4 +262,19 @@ int APS5_VABI scePlayGoTerminate(void) { return 0; } +int APS5_VABI scePlayGoRequestNextChunk() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayGoSetLanguageMask(std::uint32_t handle, std::uint64_t languageMask) { + NotImplemented_nid_no_patch(__func__); + 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..65b0b8eba1 100644 --- a/core/libs/prx/libScePlayGoDialog/Export.cpp +++ b/core/libs/prx/libScePlayGoDialog/Export.cpp @@ -2,7 +2,6 @@ #include #include #include -#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -21,6 +20,7 @@ constexpr std::int32_t ERROR_NOT_INITIALIZED = static_cast(0x80ED0 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 COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); std::atomic g_status{STATUS_NONE}; @@ -68,6 +68,7 @@ int APS5_VABI scePlayGoDialogUpdateStatus(void) { int APS5_VABI scePlayGoDialogGetResult(void* result) { if (g_status.load() == STATUS_NONE) return ERROR_NOT_INITIALIZED; if (!result) return ERROR_PARAM_INVALID; + if (g_status.load() != STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; auto* r = static_cast(result); std::memset(r, 0, sizeof(*r)); r->result = RESULT_OK; diff --git a/core/libs/prx/libScePlayReady/Export.cpp b/core/libs/prx/libScePlayReady/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libScePlayReady/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/libScePlayReady2/Export.cpp b/core/libs/prx/libScePlayReady2/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libScePlayReady2/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/libScePlayReady4/Export.cpp b/core/libs/prx/libScePlayReady4/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libScePlayReady4/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/libScePlayerSelectionDialog/CMakeLists.txt b/core/libs/prx/libScePlayerSelectionDialog/CMakeLists.txt new file mode 100644 index 0000000000..670b5b3b0a --- /dev/null +++ b/core/libs/prx/libScePlayerSelectionDialog/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libScePlayerSelectionDialog SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libScePlayerSelectionDialog PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libScePlayerSelectionDialog) + +if(BUILD_TESTING) + add_test_executable(guest_player_selection_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestPlayerSelectionDialog.cpp) + target_include_directories(guest_player_selection_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_player_selection_dialog_tests PRIVATE libScePlayerSelectionDialog 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..4f9c50b9a7 100644 --- a/core/libs/prx/libScePlayerSelectionDialog/Export.cpp +++ b/core/libs/prx/libScePlayerSelectionDialog/Export.cpp @@ -1,11 +1,75 @@ +#include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr int COMMON_DIALOG_STATUS_NONE = 0; +constexpr int COMMON_DIALOG_STATUS_INITIALIZED = 1; +constexpr int COMMON_DIALOG_STATUS_RUNNING = 2; +constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; +constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); +constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004u); +constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); +constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast(0x80B80007u); +constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); +constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; + +std::atomic g_status{COMMON_DIALOG_STATUS_NONE}; + +} + extern "C" { +int APS5_VABI scePlayerSelectionDialogInitialize(void) { + int expected = COMMON_DIALOG_STATUS_NONE; + if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_INITIALIZED)) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; + return 0; +} + +int APS5_VABI scePlayerSelectionDialogOpen(const void* param) { + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (status == COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_BUSY; + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + g_status = COMMON_DIALOG_STATUS_FINISHED; + return 0; +} + +int APS5_VABI scePlayerSelectionDialogClose(void) { + if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + g_status = COMMON_DIALOG_STATUS_FINISHED; + return 0; +} + +int APS5_VABI scePlayerSelectionDialogUpdateStatus(void) { + return g_status.load(); +} + +int APS5_VABI scePlayerSelectionDialogGetStatus(void) { + return g_status.load(); +} + +int APS5_VABI scePlayerSelectionDialogGetResult(void* result) { + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; + const std::int32_t canceled = COMMON_DIALOG_RESULT_USER_CANCELED; + std::memcpy(result, &canceled, sizeof(canceled)); + return 0; +} + +int APS5_VABI 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 cebcc9e3ef..419c7966f0 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); @@ -55,8 +56,9 @@ static_assert(sizeof(Context) + HANDLE_ALIGNMENT - 1 <= MEMORY_SIZE); std::int32_t validateCreateParam(const PngDecCreateParam* param) { if (!param) return SCE_PNG_DEC_ERROR_INVALID_PARAM; + if (param->this_size != sizeof(PngDecCreateParam)) return SCE_PNG_DEC_ERROR_INVALID_SIZE; if (param->attribute != ATTRIBUTE_NONE && param->attribute != ATTRIBUTE_BIT_DEPTH_16) return SCE_PNG_DEC_ERROR_INVALID_PARAM; - if (param->max_image_width == 0 || param->max_image_width - 1 > MAX_IMAGE_WIDTH) return SCE_PNG_DEC_ERROR_INVALID_SIZE; + if (param->max_image_width - 1 >= MAX_IMAGE_WIDTH) return SCE_PNG_DEC_ERROR_INVALID_PARAM; return 0; } @@ -123,21 +125,30 @@ int32_t APS5_VABI scePngDecDecode(void* handle, const PngDecDecodeParam* param, 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->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 != 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 (image_info) fillImageInfo(*header, image_info); 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; @@ -157,7 +168,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); } @@ -185,4 +195,9 @@ int32_t APS5_VABI scePngDecQueryMemorySize(const PngDecCreateParam* param) { return static_cast(MEMORY_SIZE); } +int APS5_VABI scePngDecDecodeWithInputControl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } 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/CMakeLists.txt b/core/libs/prx/libScePosixForWebKit/CMakeLists.txt index c786173f62..036eb0577d 100644 --- a/core/libs/prx/libScePosixForWebKit/CMakeLists.txt +++ b/core/libs/prx/libScePosixForWebKit/CMakeLists.txt @@ -17,6 +17,15 @@ if(BUILD_TESTING) set_tests_properties(guest_posix_filesystem PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_fnmatch_tests ${LIBS_INCLUDE_DIR}/tests/GuestFnmatch.cpp) + target_include_directories(guest_fnmatch_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_fnmatch_tests PRIVATE libScePosixForWebKit libc) + configure_windows_unwind(guest_fnmatch_tests) + add_test(NAME guest_fnmatch COMMAND guest_fnmatch_tests) + if(WIN32) + set_tests_properties(guest_fnmatch PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_resolver_tests ${LIBS_INCLUDE_DIR}/tests/GuestResolver.cpp) target_include_directories(guest_resolver_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_resolver_tests PRIVATE libScePosixForWebKit libSceNet libc) @@ -27,4 +36,13 @@ if(BUILD_TESTING) set_tests_properties(guest_resolver PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_service_database_tests ${LIBS_INCLUDE_DIR}/tests/GuestServiceDatabase.cpp) + target_include_directories(guest_service_database_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_service_database_tests PRIVATE libScePosixForWebKit libc) + configure_windows_unwind(guest_service_database_tests) + add_test(NAME guest_service_database COMMAND guest_service_database_tests) + if(WIN32) + set_tests_properties(guest_service_database PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libScePosixForWebKit/Export.cpp b/core/libs/prx/libScePosixForWebKit/Export.cpp index 71f7b26f66..4ff202b864 100644 --- a/core/libs/prx/libScePosixForWebKit/Export.cpp +++ b/core/libs/prx/libScePosixForWebKit/Export.cpp @@ -1,8 +1,159 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/HostEntropy.hpp" #include +#include +#include +#include +#include + +namespace { + +constexpr int FnmNomatch = 1; +constexpr int FnmNoescape = 0x01; +constexpr int FnmPathname = 0x02; +constexpr int FnmPeriod = 0x04; +constexpr int FnmLeadingDir = 0x08; +constexpr int FnmCasefold = 0x10; + +enum class RangeResult { Match, NoMatch, Error }; + +unsigned char FoldCase(unsigned char c, int flags) { + return (flags & FnmCasefold) && c >= 'A' && c <= 'Z' ? static_cast(c - 'A' + 'a') : c; +} + +RangeResult MatchRange(const char* pattern, unsigned char test, int flags, const char** next) { + const bool negate = *pattern == '!' || *pattern == '^'; + if (negate) ++pattern; + test = FoldCase(test, flags); + bool ok = false; + const char* const origin = pattern; + for (;;) { + if (*pattern == ']' && pattern > origin) { + ++pattern; + break; + } + if (*pattern == '\0') return RangeResult::Error; + if (*pattern == '/' && (flags & FnmPathname)) return RangeResult::NoMatch; + if (*pattern == '\\' && !(flags & FnmNoescape)) ++pattern; + const unsigned char low = FoldCase(static_cast(*pattern), flags); + if (*pattern != '\0') ++pattern; + if (*pattern == '-' && pattern[1] != '\0' && pattern[1] != ']') { + if (*++pattern == '\\' && !(flags & FnmNoescape)) ++pattern; + const unsigned char high = static_cast(*pattern); + if (high == '\0') return RangeResult::Error; + ++pattern; + if (low <= test && test <= FoldCase(high, flags)) ok = true; + } else if (low == test) { + ok = true; + } + } + *next = pattern; + return ok == negate ? RangeResult::NoMatch : RangeResult::Match; +} + +int MatchPattern(const char* pattern, const char* string, int flags) { + const char* const start = string; + const char* backtrackPattern = nullptr; + const char* backtrackString = nullptr; + const auto leadingPeriod = [&](const char* at) { + return *at == '.' && (flags & FnmPeriod) && (at == start || ((flags & FnmPathname) && at[-1] == '/')); + }; + for (;;) { + const unsigned char pc = static_cast(*pattern); + if (pc != '\0') ++pattern; + const unsigned char sc = static_cast(*string); + bool mismatch = false; + bool literal = false; + unsigned char expected = pc; + switch (pc) { + case '\0': + if ((flags & FnmLeadingDir) && sc == '/') return 0; + if (sc == '\0') return 0; + mismatch = true; + break; + case '?': + if (sc == '\0') return FnmNomatch; + if ((sc == '/' && (flags & FnmPathname)) || leadingPeriod(string)) { + mismatch = true; + break; + } + ++string; + break; + case '*': { + char c = *pattern; + while (c == '*') c = *++pattern; + if (leadingPeriod(string)) { + mismatch = true; + break; + } + if (c == '\0') { + if (flags & FnmPathname) return (flags & FnmLeadingDir) || std::strchr(string, '/') == nullptr ? 0 : FnmNomatch; + return 0; + } + if (c == '/' && (flags & FnmPathname)) { + string = std::strchr(string, '/'); + if (string == nullptr) return FnmNomatch; + break; + } + backtrackPattern = pattern; + backtrackString = string; + break; + } + case '[': { + if (sc == '\0') return FnmNomatch; + if ((sc == '/' && (flags & FnmPathname)) || leadingPeriod(string)) { + mismatch = true; + break; + } + const char* next = nullptr; + const RangeResult result = MatchRange(pattern, sc, flags, &next); + if (result == RangeResult::Error) { + literal = true; + } else if (result == RangeResult::Match) { + pattern = next; + ++string; + } else { + mismatch = true; + } + break; + } + case '\\': + if (!(flags & FnmNoescape)) { + expected = static_cast(*pattern); + if (expected == '\0') return FnmNomatch; + ++pattern; + } + literal = true; + break; + default: + literal = true; + break; + } + if (literal) { + if (sc != '\0') ++string; + if (FoldCase(expected, flags) != FoldCase(sc, flags)) mismatch = true; + } + if (mismatch) { + if (backtrackPattern == nullptr) return FnmNomatch; + const unsigned char bc = static_cast(*backtrackString); + if (bc == '\0') return FnmNomatch; + if (bc == '/' && (flags & FnmPathname)) return FnmNomatch; + ++backtrackString; + pattern = backtrackPattern; + string = backtrackString; + } + } +} + +} extern "C" { +int APS5_VABI fnmatch_nid_postfix(const char* pattern, const char* string, int flags) { + if (pattern == nullptr || string == nullptr) throw std::invalid_argument("fnmatch: null pattern or string"); + return MatchPattern(pattern, string, flags); +} + char* APS5_VABI strcasestr_nid_postfix(const char* text, const char* needle) { if (*needle == '\0') return const_cast(text); for (; *text != '\0'; ++text) { @@ -19,4 +170,14 @@ char* APS5_VABI strcasestr_nid_postfix(const char* text, const char* needle) { return nullptr; } +void APS5_VABI arc4random_buf_nid_postfix(void* buffer, std::size_t size) { + HostEntropy_nid_no_patch(buffer, size); +} + +std::uint32_t APS5_VABI arc4random_nid_postfix() { + std::uint32_t value; + arc4random_buf_nid_postfix(&value, sizeof(value)); + return value; +} + } diff --git a/core/libs/prx/libScePosixForWebKit/Filesystem.cpp b/core/libs/prx/libScePosixForWebKit/Filesystem.cpp index b5245f5b1a..a7e6f01754 100644 --- a/core/libs/prx/libScePosixForWebKit/Filesystem.cpp +++ b/core/libs/prx/libScePosixForWebKit/Filesystem.cpp @@ -62,8 +62,8 @@ extern "C" int APS5_VABI mkstemp_nid_postfix(char* pattern) { } extern "C" int APS5_VABI isatty_nid_postfix(int descriptor) { - if (descriptor >= GuestSockets::FirstDescriptor) { - Failure(GuestSocketIsOpen_nid_no_patch(descriptor) ? 25 : 9); + if (GuestSocketIsOpen_nid_no_patch(descriptor)) { + Failure(25); return 0; } #ifdef _WIN32 diff --git a/core/libs/prx/libScePosixForWebKit/GuestResolver.hpp b/core/libs/prx/libScePosixForWebKit/GuestResolver.hpp index 7ed54e7160..5e6fc068cc 100644 --- a/core/libs/prx/libScePosixForWebKit/GuestResolver.hpp +++ b/core/libs/prx/libScePosixForWebKit/GuestResolver.hpp @@ -28,4 +28,15 @@ struct AddressInfo { static_assert(sizeof(AddressInfo) == 48); static_assert(offsetof(AddressInfo, canonicalName) == 24); static_assert(offsetof(AddressInfo, address) == 32); + +struct ServiceEntry { + char* name; + char** aliases; + std::int32_t port; + char* protocol; +}; +static_assert(sizeof(ServiceEntry) == 32); +static_assert(offsetof(ServiceEntry, aliases) == 8); +static_assert(offsetof(ServiceEntry, port) == 16); +static_assert(offsetof(ServiceEntry, protocol) == 24); } diff --git a/core/libs/prx/libScePosixForWebKit/Resolver.cpp b/core/libs/prx/libScePosixForWebKit/Resolver.cpp index 7c5254feeb..0dd6901832 100644 --- a/core/libs/prx/libScePosixForWebKit/Resolver.cpp +++ b/core/libs/prx/libScePosixForWebKit/Resolver.cpp @@ -8,13 +8,16 @@ #include #endif #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include +#include #include #include #include #include #include #include +#include #include #include "GuestResolver.hpp" @@ -30,6 +33,66 @@ struct HostLookup { std::vector addresses; }; +struct ServiceRecord { + const char* name; + std::uint16_t port; + const char* protocol; + std::array aliases; +}; + +constexpr ServiceRecord Services[]{ + {"ftp-data", 20, "tcp", {}}, {"ftp-data", 20, "udp", {}}, {"ftp-data", 20, "sctp", {}}, + {"ftp", 21, "tcp", {}}, {"ftp", 21, "udp", {}}, {"ftp", 21, "sctp", {}}, + {"ssh", 22, "tcp", {}}, {"ssh", 22, "udp", {}}, {"ssh", 22, "sctp", {}}, + {"telnet", 23, "tcp", {}}, {"telnet", 23, "udp", {}}, + {"smtp", 25, "tcp", {"mail"}}, {"smtp", 25, "udp", {"mail"}}, + {"domain", 53, "tcp", {}}, {"domain", 53, "udp", {}}, + {"tftp", 69, "tcp", {}}, {"tftp", 69, "udp", {}}, + {"http", 80, "tcp", {"www", "www-http"}}, {"http", 80, "udp", {"www", "www-http"}}, + {"http", 80, "sctp", {"www", "www-http"}}, + {"pop3", 110, "tcp", {}}, {"pop3", 110, "udp", {}}, + {"ntp", 123, "tcp", {}}, {"ntp", 123, "udp", {}}, + {"imap", 143, "tcp", {"imap2", "imap4"}}, {"imap", 143, "udp", {"imap2", "imap4"}}, + {"https", 443, "tcp", {}}, {"https", 443, "udp", {}}, {"https", 443, "sctp", {}}, + {"rtsp", 554, "tcp", {}}, {"rtsp", 554, "udp", {}}, + {"submission", 587, "tcp", {}}, {"submission", 587, "udp", {}}, + {"imaps", 993, "tcp", {}}, {"imaps", 993, "udp", {}}, + {"pop3s", 995, "tcp", {"spop3"}}, {"pop3s", 995, "udp", {"spop3"}}, +}; + +constexpr std::size_t ServiceNameSize = 11; +constexpr std::size_t ServiceAliasSize = 9; +constexpr std::size_t ServiceProtocolSize = 5; +constexpr std::size_t ServiceAliasCount = std::tuple_size_v; + +template +constexpr std::size_t LongestServiceText(TSelect select) { + std::size_t longest = 0; + for (const auto& service : Services) + for (const char* text : select(service)) + if (text) longest = std::max(longest, std::char_traits::length(text)); + return longest; +} + +static_assert(ServiceNameSize == LongestServiceText([](const ServiceRecord& record) { return std::array{record.name}; }) + 1); +static_assert(ServiceAliasSize == LongestServiceText([](const ServiceRecord& record) { return record.aliases; }) + 1); +static_assert(ServiceProtocolSize == LongestServiceText([](const ServiceRecord& record) { return std::array{record.protocol}; }) + 1); + +struct ServiceLookup { + GuestResolver::ServiceEntry entry; + std::array name; + std::array protocol; + std::array, ServiceAliasCount> aliasNames; + std::array aliases; +}; +static_assert(std::is_trivially_destructible_v); + +template +char* CopyServiceText(std::array& target, const char* text) { + std::memcpy(target.data(), text, std::strlen(text) + 1); + return target.data(); +} + bool NumericAddress(const char* name, std::array& address) { std::array parts{}; std::size_t count = 0; @@ -129,7 +192,8 @@ GuestResolver::HostEntry* APS5_VABI gethostbyname_nid_postfix(const char* name) return nullptr; } #endif - static thread_local HostLookup lookup; + struct LookupTag {}; + auto& lookup = HostThreadLocal(); lookup.name = native->h_name; lookup.aliasNames.clear(); lookup.aliases.clear(); @@ -153,6 +217,27 @@ GuestResolver::HostEntry* APS5_VABI gethostbyname_nid_postfix(const char* name) } } +GuestResolver::ServiceEntry* APS5_VABI getservbyname_nid_postfix(const char* name, const char* protocol) { + for (const auto& service : Services) { + bool named = std::strcmp(name, service.name) == 0; + for (const char* alias : service.aliases) + named = named || (alias && std::strcmp(name, alias) == 0); + if (!named || (protocol && std::strcmp(protocol, service.protocol) != 0)) continue; + static thread_local ServiceLookup lookup; + std::size_t count = 0; + for (const char* alias : service.aliases) { + if (!alias) continue; + lookup.aliases[count] = CopyServiceText(lookup.aliasNames[count], alias); + ++count; + } + lookup.aliases[count] = nullptr; + lookup.entry = {CopyServiceText(lookup.name, service.name), lookup.aliases.data(), htons(service.port), + CopyServiceText(lookup.protocol, service.protocol)}; + return &lookup.entry; + } + return nullptr; +} + void APS5_VABI freeaddrinfo_nid_postfix(GuestResolver::AddressInfo* first) { while (first) { auto* next = first->next; @@ -284,13 +369,13 @@ int APS5_VABI getnameinfo_nid_postfix(const void* address, std::uint32_t length, const char* APS5_VABI gai_strerror_nid_postfix(int error) { static const char* messages[] = { "Success", "Address family for hostname not supported", "Temporary failure in name resolution", - "Invalid flags", "Name resolution failed", "Address family not supported", - "Memory allocation failed", "No address for hostname", "Name or service not known", - "Service not supported for socket type", "Socket type not supported", "System error", - "Invalid hints", "Unknown protocol", "Buffer too small" + "Invalid value for ai_flags", "Non-recoverable failure in name resolution", "ai_family not supported", + "Memory allocation failure", "No address associated with hostname", "hostname nor servname provided, or not known", + "servname not supported for ai_socktype", "ai_socktype not supported", "System error returned in errno", + "Invalid value for hints", "Resolved protocol is unknown", "Argument buffer overflow" }; if (error < 0 || error >= static_cast(sizeof(messages) / sizeof(messages[0]))) - return "Unknown address resolution error"; + return "Unknown error"; return messages[error]; } } diff --git a/core/libs/prx/libSceProprietaryVoiceChatHelperPrx/CMakeLists.txt b/core/libs/prx/libSceProprietaryVoiceChatHelperPrx/CMakeLists.txt new file mode 100644 index 0000000000..4feb3ddf25 --- /dev/null +++ b/core/libs/prx/libSceProprietaryVoiceChatHelperPrx/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceProprietaryVoiceChatHelperPrx SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceProprietaryVoiceChatHelperPrx PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceProprietaryVoiceChatHelperPrx) + +if(BUILD_TESTING) + add_test_executable(guest_proprietary_voice_chat_helper_tests ${LIBS_INCLUDE_DIR}/tests/GuestProprietaryVoiceChatHelper.cpp) + target_include_directories(guest_proprietary_voice_chat_helper_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_proprietary_voice_chat_helper_tests PRIVATE libSceProprietaryVoiceChatHelperPrx libc) + configure_windows_unwind(guest_proprietary_voice_chat_helper_tests) + add_test(NAME guest_proprietary_voice_chat_helper COMMAND guest_proprietary_voice_chat_helper_tests) + if(WIN32) + set_tests_properties(guest_proprietary_voice_chat_helper PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp b/core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp index 882e6e92c0..d1c11f4133 100644 --- a/core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp +++ b/core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp @@ -1,23 +1,68 @@ +#include +#include +#include + #include "prx/libc/include/General.hpp" +namespace { + +struct Helper { + std::mutex mutex; + bool initialized = false; +}; + +Helper& State() { + static Helper helper; + return helper; +} + +[[noreturn]] void Fail(const char* function, const std::string& why) { + throw std::logic_error(std::string(function) + ": " + why); +} + +void RequireInitialized(const Helper& helper, const char* function) { + if (!helper.initialized) Fail(function, "library not initialized"); +} + +} // namespace + extern "C" { -int APS5_VABI sceProprietaryVoiceChatHelperGetVoiceChatUsageState(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceProprietaryVoiceChatHelperInitialize(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceProprietaryVoiceChatHelperSetVoiceChatState(void) { - NotImplemented_nid_no_patch(__func__); + auto& helper = State(); + std::lock_guard lock(helper.mutex); + if (helper.initialized) Fail(__func__, "already initialized"); + helper.initialized = true; return 0; } int APS5_VABI sceProprietaryVoiceChatHelperTerminate(void) { + auto& helper = State(); + std::lock_guard lock(helper.mutex); + RequireInitialized(helper, __func__); + helper.initialized = false; + return 0; +} + +int APS5_VABI sceProprietaryVoiceChatHelperSetVoiceChatState(void) { + auto& helper = State(); + std::lock_guard lock(helper.mutex); + RequireInitialized(helper, __func__); + return 0; +} + +int APS5_VABI sceProprietaryVoiceChatHelperGetVoiceChatUsageState(void) { + auto& helper = State(); + std::lock_guard lock(helper.mutex); + RequireInitialized(helper, __func__); + NotImplemented_nid_no_patch(__func__); + return 0; +} + +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..fb34206c21 --- /dev/null +++ b/core/libs/prx/libScePs2EmuMenuDialog/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/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..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libScePsmUtil/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/libScePsml/CMakeLists.txt b/core/libs/prx/libScePsml/CMakeLists.txt new file mode 100644 index 0000000000..121b19bff1 --- /dev/null +++ b/core/libs/prx/libScePsml/CMakeLists.txt @@ -0,0 +1,2 @@ +include(${CMAKE_CURRENT_SOURCE_DIR}/../libScePsml_debug/PsmlLibrary.cmake) +add_sce_psml_library(libScePsml) diff --git a/core/libs/prx/libScePsml_debug/CMakeLists.txt b/core/libs/prx/libScePsml_debug/CMakeLists.txt index 3fd929d078..aceb0af750 100644 --- a/core/libs/prx/libScePsml_debug/CMakeLists.txt +++ b/core/libs/prx/libScePsml_debug/CMakeLists.txt @@ -1,11 +1,11 @@ -add_library(libScePsml_debug SHARED EXCLUDE_FROM_ALL Export.cpp) -target_include_directories(libScePsml_debug PRIVATE ${LIBS_INCLUDE_DIR}) -configure_windows_unwind(libScePsml_debug) +include(${CMAKE_CURRENT_SOURCE_DIR}/PsmlLibrary.cmake) +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 3eaf69d18a..e8cc8f8b7a 100644 --- a/core/libs/prx/libScePsml_debug/Export.cpp +++ b/core/libs/prx/libScePsml_debug/Export.cpp @@ -1,11 +1,98 @@ #include #include +#include +#include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" namespace { constexpr std::int32_t SCE_PSML_ERROR_NOT_INITIALIZED = static_cast(0x8A810001); +constexpr std::uint64_t MfsrBlockBytes = 0x200000; +constexpr std::uint64_t MfsrBlockCount = 4; + +struct MfsrRequirement { + std::uint64_t blockBytes; + std::uint64_t alignment; + std::uint64_t blockCount; +}; + +struct MfsrBlock { + std::uint64_t address; + std::uint64_t bytes; +}; + +struct MfsrSharedResourcesParam { + std::uint32_t type; + std::uint32_t reserved; + const MfsrBlock* blocks; + std::uint64_t blockCount; + std::uint64_t reserved2; +}; + +struct MfsrContextParam { + std::uint32_t reserved0[2]; + const MfsrBlock* blocks; + std::uint64_t blockCount; + std::uint32_t reserved1[8]; + void* sharedResources; +}; + +static_assert(sizeof(MfsrRequirement) == 0x18); +static_assert(sizeof(MfsrSharedResourcesParam) == 0x20); +static_assert(offsetof(MfsrContextParam, blocks) == 0x08); +static_assert(offsetof(MfsrContextParam, blockCount) == 0x10); +static_assert(offsetof(MfsrContextParam, sharedResources) == 0x38); + +enum class MfsrObjectKind { SharedResources, Context }; + +struct MfsrObject { + MfsrObjectKind kind; + void* sharedResources; +}; + +std::atomic g_mfsrInitialized{false}; +std::mutex g_mfsrMutex; +std::unordered_map g_mfsrObjects; + +void RequireMfsr(bool condition, const char* function, const char* what) { + if (!condition) throw std::runtime_error(std::string(function) + ": " + what); +} + +void RequireBlocks(const MfsrBlock* blocks, std::uint64_t blockCount, const char* function) { + RequireMfsr(blockCount >= MfsrBlockCount, function, "fewer memory blocks than the requirement"); + RequireMfsr(blocks != nullptr, function, "null memory block list"); + for (std::uint64_t i = 0; i < MfsrBlockCount; ++i) { + RequireMfsr(blocks[i].bytes >= MfsrBlockBytes && blocks[i].address % MfsrBlockBytes == 0, function, "memory block smaller or less aligned than the requirement"); + } +} + +void* CreateMfsrObject(MfsrObjectKind kind, void* sharedResources, const char* function) { + std::lock_guard lock(g_mfsrMutex); + if (kind == MfsrObjectKind::Context) { + const auto found = g_mfsrObjects.find(sharedResources); + RequireMfsr(found != g_mfsrObjects.end() && found->second->kind == MfsrObjectKind::SharedResources, function, "shared resources handle was not created by this library"); + } + auto* object = new MfsrObject{kind, sharedResources}; + g_mfsrObjects.emplace(object, object); + return object; +} + +void ReleaseMfsrObject(void* handle, MfsrObjectKind kind, const char* function) { + std::lock_guard lock(g_mfsrMutex); + const auto found = g_mfsrObjects.find(handle); + RequireMfsr(found != g_mfsrObjects.end() && found->second->kind == kind, function, "handle was not created by this library"); + if (kind == MfsrObjectKind::SharedResources) { + for (const auto& [key, object] : g_mfsrObjects) { + RequireMfsr(object->sharedResources != handle, function, "shared resources still used by a context"); + } + } + delete found->second; + g_mfsrObjects.erase(found); +} } @@ -14,12 +101,14 @@ extern "C" { std::int32_t APS5_VABI scePsmlMfsrGetContextBufferRequirement1100(void* requirement, const void* param) { (void)requirement; (void)param; + if (g_mfsrInitialized) NotImplemented_nid_no_patch(__func__); return SCE_PSML_ERROR_NOT_INITIALIZED; } std::int32_t APS5_VABI scePsmlMfsrCreateContext1100(void** context, const void* param) { (void)context; (void)param; + if (g_mfsrInitialized) NotImplemented_nid_no_patch(__func__); return SCE_PSML_ERROR_NOT_INITIALIZED; } @@ -27,29 +116,79 @@ std::int32_t APS5_VABI scePsmlMfsrGetDispatchMfsrPacket1100(void* context, void* (void)context; (void)commandBuffer; (void)param; + if (g_mfsrInitialized) NotImplemented_nid_no_patch(__func__); return SCE_PSML_ERROR_NOT_INITIALIZED; } -int APS5_VABI scePsmlMfsrGetSharedResourcesInitRequirement() { - NotImplemented_nid_no_patch(__func__); +std::int32_t APS5_VABI scePsmlMfsrInit() { + g_mfsrInitialized = true; return 0; } -int APS5_VABI scePsmlMfsrInit() { - NotImplemented_nid_no_patch(__func__); +std::int32_t APS5_VABI scePsmlMfsrGetSharedResourcesInitRequirement(MfsrRequirement* requirement, const MfsrSharedResourcesParam* param) { + if (!g_mfsrInitialized) return SCE_PSML_ERROR_NOT_INITIALIZED; + RequireMfsr(requirement != nullptr && param != nullptr, __func__, "null requirement or parameter"); + RequireMfsr(param->type == 0, __func__, ("shared resources type " + std::to_string(param->type) + " is not supported").c_str()); + *requirement = {MfsrBlockBytes, MfsrBlockBytes, MfsrBlockCount}; return 0; } +std::int32_t APS5_VABI scePsmlMfsrCreateSharedResources(void** sharedResources, const MfsrSharedResourcesParam* param) { + if (!g_mfsrInitialized) return SCE_PSML_ERROR_NOT_INITIALIZED; + RequireMfsr(sharedResources != nullptr && param != nullptr, __func__, "null handle or parameter"); + RequireMfsr(param->type == 0, __func__, ("shared resources type " + std::to_string(param->type) + " is not supported").c_str()); + RequireBlocks(param->blocks, param->blockCount, __func__); + *sharedResources = CreateMfsrObject(MfsrObjectKind::SharedResources, nullptr, __func__); + return 0; +} + +std::int32_t APS5_VABI scePsmlMfsrReleaseSharedResources(void* sharedResources) { + if (!g_mfsrInitialized) return SCE_PSML_ERROR_NOT_INITIALIZED; + ReleaseMfsrObject(sharedResources, MfsrObjectKind::SharedResources, __func__); + return 0; +} + +std::int32_t APS5_VABI scePsmlMfsrGetContextBufferRequirement800M3_2(MfsrRequirement* requirement, const MfsrContextParam* param) { + if (!g_mfsrInitialized) return SCE_PSML_ERROR_NOT_INITIALIZED; + RequireMfsr(requirement != nullptr && param != nullptr, __func__, "null requirement or parameter"); + *requirement = {MfsrBlockBytes, MfsrBlockBytes, MfsrBlockCount}; + return 0; +} + +std::int32_t APS5_VABI scePsmlMfsrCreateContext800M3_2(void** context, const MfsrContextParam* param) { + if (!g_mfsrInitialized) return SCE_PSML_ERROR_NOT_INITIALIZED; + RequireMfsr(context != nullptr && param != nullptr, __func__, "null handle or parameter"); + RequireBlocks(param->blocks, param->blockCount, __func__); + *context = CreateMfsrObject(MfsrObjectKind::Context, param->sharedResources, __func__); + return 0; +} + +std::int32_t APS5_VABI scePsmlMfsrReleaseContext(void* context) { + if (!g_mfsrInitialized) return SCE_PSML_ERROR_NOT_INITIALIZED; + ReleaseMfsrObject(context, MfsrObjectKind::Context, __func__); + return 0; +} + +std::int32_t APS5_VABI scePsmlMfsrCreateContext1300(void** context, const void* param) { + (void)context; + (void)param; + if (g_mfsrInitialized) NotImplemented_nid_no_patch(__func__); + return SCE_PSML_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI scePsmlMfsrGetDispatchMfsrPacket1300(void* context, void* commandBuffer, const void* param) { + (void)context; + (void)commandBuffer; + (void)param; + if (g_mfsrInitialized) NotImplemented_nid_no_patch(__func__); + return SCE_PSML_ERROR_NOT_INITIALIZED; +} + int APS5_VABI scePsmlMfsrGetDispatchMfsrPacketSizeInDwords() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePsmlMfsrGetContextBufferRequirement800M3_2() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI scePsmlMfsrSelectConfig() { NotImplemented_nid_no_patch(__func__); return 0; @@ -65,34 +204,25 @@ int APS5_VABI scePsmlMfsrRequestCapture() { return 0; } -int APS5_VABI scePsmlMfsrReleaseContext() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI scePsmlMfsrIsCaptureInProgress() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI 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; -} - -int APS5_VABI scePsmlMfsrCreateContext800M3_2() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI scePsmlMfsrReleaseSharedResources() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI scePsmlMfsrGetDispatchMfsrPacket900(void* context, void* commandBuffer, const void* param) { + (void)context; + (void)commandBuffer; + (void)param; + if (g_mfsrInitialized) NotImplemented_nid_no_patch(__func__); + return SCE_PSML_ERROR_NOT_INITIALIZED; } int APS5_VABI scePsmlMfsr2ReleaseSharedResources() { @@ -140,4 +270,14 @@ int APS5_VABI scePsmlMfsr2Init() { return 0; } +int APS5_VABI scePsmlMfsrEnableSpecularAA() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePsmlMfsrEnableQuality8K() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libScePsml_debug/PsmlLibrary.cmake b/core/libs/prx/libScePsml_debug/PsmlLibrary.cmake new file mode 100644 index 0000000000..69fe16dbf3 --- /dev/null +++ b/core/libs/prx/libScePsml_debug/PsmlLibrary.cmake @@ -0,0 +1,5 @@ +function(add_sce_psml_library target) + add_library(${target} SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Export.cpp) + target_include_directories(${target} PRIVATE ${LIBS_INCLUDE_DIR}) + configure_windows_unwind(${target}) +endfunction() diff --git a/core/libs/prx/libSceRandom/Export.cpp b/core/libs/prx/libSceRandom/Export.cpp index 4e9ef26749..c61ffc4d53 100644 --- a/core/libs/prx/libSceRandom/Export.cpp +++ b/core/libs/prx/libSceRandom/Export.cpp @@ -1,10 +1,8 @@ #include #include -#include -#include -#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/HostEntropy.hpp" namespace { @@ -17,12 +15,7 @@ extern "C" { int APS5_VABI sceRandomGetRandomNumber(void* buf, size_t size) { if (!buf || size > SCE_RANDOM_MAX_SIZE) return SCE_RANDOM_ERROR_INVALID; - static thread_local std::random_device device; - auto* bytes = static_cast(buf); - for (std::size_t offset = 0; offset < size; offset += sizeof(unsigned int)) { - const unsigned int value = device(); - std::memcpy(bytes + offset, &value, std::min(sizeof(value), size - offset)); - } + HostEntropy_nid_no_patch(buf, size); 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 c837587a58..af49b0fa3c 100644 --- a/core/libs/prx/libSceRazorCpu/Export.cpp +++ b/core/libs/prx/libSceRazorCpu/Export.cpp @@ -24,6 +24,18 @@ int APS5_VABI sceRazorCpuJobManagerSequence(const void* args) { return 0; } +int APS5_VABI sceRazorCpuPushMarker(const char* label, uint32_t color, uint32_t flags) { + (void)label; + (void)color; + (void)flags; + 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; @@ -52,4 +64,68 @@ int APS5_VABI sceRazorCpuWriteBookmark(const char* label, const char* descriptio return 0; } +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; +} + } 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..4626f92e6b --- /dev/null +++ b/core/libs/prx/libSceRegMgr/Export.cpp @@ -0,0 +1,40 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceRegMgrGetBin() { + 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() { + 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; +} + +} 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/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..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceRnpsAppMgr/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/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 2e213f70b5..f4e61798a5 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,13 +100,34 @@ 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 seconds = static_cast((static_cast(utcTick) - static_cast(UNIX_EPOCH_TICK)) / TICKS_PER_SECOND); + const auto elapsed = std::chrono::microseconds{static_cast(utcTick) - static_cast(UNIX_EPOCH_TICK)}; + const auto seconds = static_cast(std::chrono::floor(elapsed).count()); std::tm local{}; #ifdef _WIN32 if (localtime_s(&local, &seconds) != 0) return 0; @@ -125,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; @@ -233,10 +270,12 @@ 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}, + {"HDT", -540}, {"CAT", -600}, {"AHST", -600}, {"NST", -150}, {"NFT", -150}, {"NDT", -90}, {"IDLW", -720}, {"CET", 60}, {"MET", 60}, + {"DNT", 60}, {"NOR", 60}, {"SET", 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}, }; @@ -283,7 +322,7 @@ bool parseRfc2822DateTime(const char* cursor, RtcDateTime& time, std::int64_t& o ++cursor; if (readDigits(cursor, 2, second) == 0) return false; if (*cursor == ' ' && !parseZone(cursor + 1, offsetMinutes)) return false; - } else if (*cursor != 0 && *cursor != ' ') { + } else if (*cursor != ' ') { return false; } time = makeDateTime(year, month, day, hour, minute, second, 0); @@ -356,7 +395,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) { @@ -369,8 +410,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; } @@ -446,6 +489,90 @@ int APS5_VABI sceRtcSetDosTime(RtcDateTime* time, uint32_t dos_time) { return 0; } +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* 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; @@ -506,31 +633,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) { @@ -538,7 +665,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 ddf3cf5e5f..87864f5971 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,8 +20,23 @@ 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_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; +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*); @@ -28,26 +47,51 @@ std::map g_ctx; // id -> bound GuestRudpEventHandler g_handler = nullptr; void* g_handler_arg = nullptr; +struct RudpPoll { + uint32_t capacity; + std::map entries; +}; + +struct RudpPollEvent { + int ctxId; + uint16_t reqEvents; + uint16_t rtnEvents; +}; +static_assert(sizeof(RudpPollEvent) == 8); + +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" { int APS5_VABI sceRudpInit_nid_postfix(void* mem_pool, int mem_pool_size) { - (void)mem_pool; - (void)mem_pool_size; std::lock_guard lk(g_mutex); if (g_inited) { return RUDP_ERROR_ALREADY_INITIALIZED; } + if (mem_pool == nullptr || mem_pool_size <= 0) { + return RUDP_ERROR_INVALID_ARGUMENT; + } + if (mem_pool_size < RUDP_POOL_MIN_SIZE || reinterpret_cast(mem_pool) % RUDP_POOL_ALIGNMENT != 0) { + return RUDP_ERROR_MEMORY; + } g_inited = true; return 0; } int APS5_VABI sceRudpActivate() { - NotImplemented_nid_no_patch(__func__); return 0; } @@ -56,27 +100,37 @@ int APS5_VABI sceRudpGetStatus(void* status, std::size_t size) { if (!g_inited) { return RUDP_ERROR_NOT_INITIALIZED; } - if (status != nullptr && size != 0) { - std::memset(status, 0, size); // state 0 = idle, no connections + if (status == nullptr || size == 0 || size > RUDP_STATUS_SIZE) { + return RUDP_ERROR_INVALID_ARGUMENT; } + unsigned char report[RUDP_STATUS_SIZE] = {}; + const auto contexts = static_cast(g_ctx.size()); + std::memcpy(report + RUDP_STATUS_CURRENT_CONTEXTS, &contexts, sizeof(contexts)); + std::memcpy(status, report, size); return 0; } -int APS5_VABI sceRudpEnd(int ctx_id) { - std::lock_guard lk(g_mutex); - if (!g_inited) { - return RUDP_ERROR_NOT_INITIALIZED; - } - return g_ctx.erase(ctx_id) != 0 ? 0 : RUDP_ERROR_INVALID_CONTEXT_ID; -} - -int APS5_VABI sceRudpTerminate(void) { +int APS5_VABI sceRudpEnd(void) { std::lock_guard lk(g_mutex); if (!g_inited) { return RUDP_ERROR_NOT_INITIALIZED; } g_inited = false; g_ctx.clear(); + g_polls.clear(); + g_polls_changed.notify_all(); + return 0; +} + +int APS5_VABI sceRudpTerminate(int ctx_id) { + std::lock_guard lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + if (g_ctx.erase(ctx_id) == 0) { + return RUDP_ERROR_INVALID_CONTEXT_ID; + } + remove_ctx_from_polls(ctx_id); return 0; } @@ -87,6 +141,14 @@ int APS5_VABI sceRudpEnableInternalIOThread(uint32_t stack_size, uint32_t priori return g_inited ? 0 : RUDP_ERROR_NOT_INITIALIZED; } +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) { std::lock_guard lk(g_mutex); g_handler = handler; @@ -136,6 +198,15 @@ int APS5_VABI sceRudpSetOption(int ctx_id, int option, const void* value, uint32 return valid_ctx(ctx_id) ? 0 : RUDP_ERROR_INVALID_CONTEXT_ID; } +int APS5_VABI sceRudpGetOption(int ctx_id, int option, void* value, uint32_t len) { + (void)ctx_id; + (void)option; + (void)value; + (void)len; + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceRudpInitiate(int ctx_id, const void* to, uint32_t tolen, uint16_t vport) { (void)to; (void)tolen; @@ -164,6 +235,53 @@ int APS5_VABI sceRudpGetContextStatus(int ctx_id, void* status, uint32_t size) { return 0; } +int APS5_VABI sceRudpGetRemoteInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRudpGetNumberOfPacketsToRead(int ctx_id) { + std::lock_guard lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + if (ctx_id < 0) { + return RUDP_ERROR_INVALID_ARGUMENT; + } + if (!valid_ctx(ctx_id)) { + return RUDP_ERROR_INVALID_CONTEXT_ID; + } + return 0; +} + +int APS5_VABI sceRudpGetSizeWritable(int ctx_id) { + std::lock_guard lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + if (ctx_id < 0) { + return RUDP_ERROR_INVALID_ARGUMENT; + } + if (!valid_ctx(ctx_id)) { + return RUDP_ERROR_INVALID_CONTEXT_ID; + } + return RUDP_ERROR_NOT_ACCEPTABLE; +} + +int APS5_VABI sceRudpGetSizeReadable(int ctx_id) { + std::lock_guard lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + if (ctx_id < 0) { + return RUDP_ERROR_INVALID_ARGUMENT; + } + if (!valid_ctx(ctx_id)) { + return RUDP_ERROR_INVALID_CONTEXT_ID; + } + return RUDP_ERROR_NOT_ACCEPTABLE; +} + int APS5_VABI sceRudpWrite(int ctx_id, const void* data, uint32_t len, int flags) { (void)data; (void)len; @@ -186,4 +304,151 @@ int APS5_VABI sceRudpRead(int ctx_id, void* data, uint32_t len, int flags, void* } return valid_ctx(ctx_id) ? RUDP_ERROR_CONN_RESET : RUDP_ERROR_INVALID_CONTEXT_ID; } + +int APS5_VABI sceRudpNetFlush(void) { + std::lock_guard lk(g_mutex); + return g_inited ? 0 : RUDP_ERROR_NOT_INITIALIZED; +} + +int APS5_VABI sceRudpProcessEvents(void) { + std::lock_guard lk(g_mutex); + return g_inited ? 0 : RUDP_ERROR_NOT_INITIALIZED; +} + +int APS5_VABI sceRudpNetReceived(void) { + std::lock_guard lk(g_mutex); + return g_inited ? 0 : RUDP_ERROR_NOT_INITIALIZED; +} + +int APS5_VABI sceRudpGetMaxSegmentSize(uint16_t* size) { + std::lock_guard lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + if (size == nullptr) { + return RUDP_ERROR_INVALID_ARGUMENT; + } + *size = RUDP_MAX_SEGMENT_SIZE; + return 0; +} + +int APS5_VABI sceRudpPollCreate(uint32_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, uint32_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..34b8330de5 --- /dev/null +++ b/core/libs/prx/libSceS3da/Export.cpp @@ -0,0 +1,28 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceS3daClose() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceS3daGetInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceS3daOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceS3daSendData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceSaveData.native/CMakeLists.txt b/core/libs/prx/libSceSaveData.native/CMakeLists.txt index 496c0b9bcd..6b03a2c86c 100644 --- a/core/libs/prx/libSceSaveData.native/CMakeLists.txt +++ b/core/libs/prx/libSceSaveData.native/CMakeLists.txt @@ -22,6 +22,15 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_savedata_convert_tests ${LIBS_INCLUDE_DIR}/tests/GuestSaveDataConvert.cpp) + target_include_directories(guest_savedata_convert_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_savedata_convert_tests PRIVATE libSceSaveData.native libc) + add_test(NAME guest_savedata_convert COMMAND guest_savedata_convert_tests) + if(WIN32) + set_tests_properties(guest_savedata_convert PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_savedata_mount_status_tests ${LIBS_INCLUDE_DIR}/tests/GuestSaveDataMountStatus.cpp) target_include_directories(guest_savedata_mount_status_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_savedata_mount_status_tests PRIVATE libSceSaveData.native libc) @@ -40,4 +49,14 @@ if(BUILD_TESTING) set_tests_properties(guest_savedata_ps4 PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + add_test_executable(guest_savedata_cancel_tests ${LIBS_INCLUDE_DIR}/tests/GuestSaveDataCancel.cpp) + target_include_directories(guest_savedata_cancel_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_savedata_cancel_tests PRIVATE libSceSaveData.native libc) + configure_windows_unwind(guest_savedata_cancel_tests) + add_test(NAME guest_savedata_cancel COMMAND guest_savedata_cancel_tests) + if(WIN32) + set_tests_properties(guest_savedata_cancel PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index f095a9e846..a95498f7d9 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -18,6 +18,8 @@ #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" @@ -25,6 +27,7 @@ static constexpr char SAVE_DIR[] = "_sd"; static std::atomic g_transaction_counter{1}; static std::atomic g_initializations{0}; +static std::atomic g_convertProgress{0.0f}; static std::string save_root() { return std::string(SAVE_DIR); @@ -62,6 +65,9 @@ static bool dir_name_match(const char* str, const char* pattern) { str++; pattern++; } + while (*pattern == '%') { + pattern++; + } return *str == '\0' && *pattern == '\0'; } @@ -114,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"; } @@ -140,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(); } } @@ -212,9 +302,29 @@ 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); } +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"); @@ -229,27 +339,13 @@ 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); + std::filesystem::remove(blocks_path(path), ec); return SAVE_DATA_OK; } @@ -261,22 +357,75 @@ 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 = 65536; + +static std::uint64_t save_blocks(const std::string& real_path) { + std::ifstream file(blocks_path(real_path)); + std::uint64_t blocks = 0; + if (!(file >> blocks) || blocks == 0) { + return SAVE_DATA_BLOCKS_MAX; + } + return std::min(blocks, SAVE_DATA_BLOCKS_MAX); +} + +static void keep_save_blocks(const std::string& real_path, std::uint64_t blocks) { + std::ofstream file(blocks_path(real_path), std::ios::trunc); + file << std::min(blocks, SAVE_DATA_BLOCKS_MAX) << '\n'; + if (!file) { + throw std::runtime_error("sceSaveDataMount3: the save's size could not be kept"); + } +} + +static std::uint64_t used_blocks(const std::string& real_path) { + std::uint64_t blocks = 0; + std::error_code error; + for (auto it = std::filesystem::recursive_directory_iterator(real_path, error); !error && it != std::filesystem::recursive_directory_iterator(); it.increment(error)) { + if (!it->is_regular_file(error)) { + continue; + } + const auto bytes = it->file_size(error); + if (!error) { + blocks += (bytes + SAVE_DATA_BLOCK_BYTES - 1) / SAVE_DATA_BLOCK_BYTES; + } + } + return blocks; +} + +static SaveDataSearchInfo search_info(const std::string& real_path) { + const auto blocks = save_blocks(real_path); + return SaveDataSearchInfo{blocks, blocks - std::min(blocks, used_blocks(real_path)), {}}; +} + static int dirNameSearch(const SaveDataDirNameSearchCond* cond, SaveDataDirNameSearchResult* result) { if (cond == nullptr || result == nullptr) { throw std::runtime_error("sceSaveDataDirNameSearch: null argument"); } result->hit_num = 0; result->set_num = 0; + const std::uint32_t key = cond->key; + if ((key > SAVE_DATA_SORT_KEY_MTIME && key != SAVE_DATA_SORT_KEY_FREE_BLOCKS) || cond->order > SAVE_DATA_SORT_ORDER_DESCENT) { + return SAVE_DATA_ERROR_PARAMETER; + } + if (cond->title_id != nullptr && std::strncmp(cond->title_id->data, GetAppTitleId_nid_postfix().value, sizeof(cond->title_id->data)) != 0) { + return SAVE_DATA_OK; + } const char* pattern = (cond->dir_name != nullptr) ? cond->dir_name->data : nullptr; const std::string root = save_root(); if (!std::filesystem::is_directory(root)) { return SAVE_DATA_OK; } - std::uint32_t hit = 0; - std::uint32_t set = 0; + struct Match { + std::string name; + std::string path; + SaveDataParam param; + SaveDataSearchInfo info; + }; + const bool sortByParam = key == SAVE_DATA_SORT_KEY_USER_PARAM || key == SAVE_DATA_SORT_KEY_MTIME; + std::vector matches; for (const auto& entry : std::filesystem::directory_iterator(root)) { if (!entry.is_directory()) { continue; @@ -285,16 +434,36 @@ static int dirNameSearch(const SaveDataDirNameSearchCond* cond, SaveDataDirNameS if (!dir_name_match(name.c_str(), pattern)) { continue; } - hit++; - if (result->dir_names != nullptr && set < result->dir_names_num) { - std::snprintf(result->dir_names[set].data, sizeof(result->dir_names[set].data), "%s", name.c_str()); - if (result->params != nullptr) { - result->params[set] = load_param(entry.path().string()); - } - set++; + Match match{name, entry.path().string(), {}, search_info(entry.path().string())}; + if (sortByParam) { + match.param = load_param(match.path); + } + matches.push_back(std::move(match)); + } + std::sort(matches.begin(), matches.end(), [](const Match& a, const Match& b) { return a.name < b.name; }); + if (key == SAVE_DATA_SORT_KEY_USER_PARAM) { + std::stable_sort(matches.begin(), matches.end(), [](const Match& a, const Match& b) { return a.param.user_param < b.param.user_param; }); + } else if (key == SAVE_DATA_SORT_KEY_MTIME) { + std::stable_sort(matches.begin(), matches.end(), [](const Match& a, const Match& b) { return a.param.mtime < b.param.mtime; }); + } else if (key == SAVE_DATA_SORT_KEY_BLOCKS) { + std::stable_sort(matches.begin(), matches.end(), [](const Match& a, const Match& b) { return a.info.blocks < b.info.blocks; }); + } else if (key == SAVE_DATA_SORT_KEY_FREE_BLOCKS) { + std::stable_sort(matches.begin(), matches.end(), [](const Match& a, const Match& b) { return a.info.free_blocks < b.info.free_blocks; }); + } + if (cond->order == SAVE_DATA_SORT_ORDER_DESCENT) { + std::reverse(matches.begin(), matches.end()); + } + const std::uint32_t set = result->dir_names == nullptr ? 0 : static_cast(std::min(result->dir_names_num, matches.size())); + for (std::uint32_t i = 0; i < set; i++) { + std::snprintf(result->dir_names[i].data, sizeof(result->dir_names[i].data), "%s", matches[i].name.c_str()); + if (result->params != nullptr) { + result->params[i] = sortByParam ? matches[i].param : load_param(matches[i].path); + } + if (result->infos != nullptr) { + result->infos[i] = matches[i].info; } } - result->hit_num = hit; + result->hit_num = static_cast(matches.size()); result->set_num = set; return SAVE_DATA_OK; } @@ -321,15 +490,17 @@ 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); - if (find_slot_by_mount_point(mount_point->data) == -1) { + const int slot = find_slot_by_mount_point(mount_point->data); + if (slot == -1) { return SAVE_DATA_ERROR_NOT_MOUNTED; } std::memset(info, 0, sizeof(*info)); - info->blocks = SAVE_DATA_BLOCKS_MAX; - info->free_blocks = SAVE_DATA_BLOCKS_MAX; + const auto search = search_info(g_slots[slot].real_path); + info->blocks = search.blocks; + info->free_blocks = search.free_blocks; return SAVE_DATA_OK; } @@ -341,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); @@ -374,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)) { @@ -419,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) { @@ -435,7 +607,6 @@ static int mount3(const SaveDataMount3* mount, SaveDataMountResult* mount_result if (mount == nullptr || mount_result == nullptr || mount->dir_name == nullptr) { throw std::runtime_error("sceSaveDataMount3: null argument"); } - std::memset(mount_result, 0, sizeof(*mount_result)); const bool create = (mount->mount_mode & SAVE_DATA_MOUNT_MODE_CREATE) != 0; const bool create2 = (mount->mount_mode & SAVE_DATA_MOUNT_MODE_CREATE2) != 0; const bool rdonly = (mount->mount_mode & SAVE_DATA_MOUNT_MODE_RDONLY) != 0; @@ -446,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; @@ -477,18 +644,23 @@ static int mount3(const SaveDataMount3* mount, SaveDataMountResult* mount_result // path resolver maps it to the save directory, whose name may be far longer. const std::string mountPoint = "/savedata" + std::to_string(slot); AddPathAlias_nid_no_patch(mountPoint.c_str(), std::filesystem::absolute(real_path).string().c_str()); + std::memset(mount_result, 0, sizeof(*mount_result)); g_slots[slot].used = true; g_slots[slot].mount_point = mountPoint; g_slots[slot].real_path = real_path; std::memcpy(mount_result->mount_point.data, mountPoint.c_str(), mountPoint.size() + 1); mount_result->required_blocks = 0; mount_result->mount_status = (create || (create2 && !exists)) ? 1u : 0u; + std::error_code sized; + if (mount->blocks != 0 && (create || (create2 && !exists) || !std::filesystem::exists(blocks_path(real_path), sized))) { + keep_save_blocks(real_path, mount->blocks); + } return SAVE_DATA_OK; } 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; } @@ -501,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) { @@ -518,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); @@ -560,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)) { @@ -576,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)); @@ -619,28 +798,37 @@ 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); if (!have) { existed = 0; } + const bool needParam = (setup_param->init_param != nullptr && (setup_param->option & 1u) != 0); + const std::string paramPath = mem_path(setup_param->user_id, setup_param->slot_id, "param"); + std::size_t paramSize = 0; + const bool haveParam = file_size_of(paramPath, ¶mSize); // First run: create a zero-filled blob and report existed size 0 so the title treats it as a new save. - if (!have || existed < setup_param->memory_size) { + if (!have || existed < setup_param->memory_size || (needParam && !haveParam)) { std::error_code ec; std::filesystem::create_directories(std::filesystem::path(path).parent_path(), ec); - std::vector data; - if (have && !read_file_all(path, data)) { - return SAVE_DATA_ERROR_INTERNAL; - } - data.resize(setup_param->memory_size, 0); - if (!write_file_replace(path, data)) { - return SAVE_DATA_ERROR_INTERNAL; - } - if (setup_param->init_param != nullptr && (setup_param->option & 1u) != 0) { + if (needParam) { std::vector pd(sizeof(SaveDataParam)); std::memcpy(pd.data(), setup_param->init_param, sizeof(SaveDataParam)); - write_file_replace(mem_path(setup_param->user_id, setup_param->slot_id, "param"), pd); + if (!write_file_replace(paramPath, pd)) { + return SAVE_DATA_ERROR_INTERNAL; + } + } + if (!have || existed < setup_param->memory_size) { + std::vector data; + if (have && !read_file_all(path, data)) { + return SAVE_DATA_ERROR_INTERNAL; + } + data.resize(setup_param->memory_size, 0); + if (!write_file_replace(path, data)) { + return SAVE_DATA_ERROR_INTERNAL; + } } } if (result != nullptr) { @@ -660,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; @@ -680,6 +872,9 @@ int APS5_VABI sceSaveDataTerminate(void) { return SAVE_DATA_ERROR_BUSY; } --g_initializations; + if (g_initializations == 0) { + g_convertProgress = 0.0f; + } return SAVE_DATA_OK; } @@ -701,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); @@ -740,13 +935,99 @@ int APS5_VABI sceSaveDataDirNameSearchPs4(const SaveDataDirNameSearchCond* cond, return rc; } -int APS5_VABI sceSaveDataConvert() { +static int convert(const SaveDataConvert* param) { + if (param == nullptr || param->dir_name == nullptr) { + throw std::runtime_error("sceSaveDataConvert: null argument"); + } + g_convertProgress = 0.0f; + const auto* nameEnd = static_cast(std::memchr(param->dir_name->data, '\0', sizeof(param->dir_name->data))); + if (nameEnd == nullptr) { + return SAVE_DATA_ERROR_PARAMETER; + } + const std::string dirName(param->dir_name->data, static_cast(nameEnd - param->dir_name->data)); + if (dirName.empty() || dirName == "." || dirName == ".." || dirName.find_first_of("/\\:") != std::string::npos) { + return SAVE_DATA_ERROR_PARAMETER; + } + if (!std::filesystem::is_directory(save_root() + "/" + dirName)) { + return SAVE_DATA_ERROR_NOT_FOUND; + } + g_convertProgress = 1.0f; + return SAVE_DATA_OK; +} + +int APS5_VABI sceSaveDataConvert(const SaveDataConvert* param) { + const int rc = convert(param); + SAVEDATA_TRACE("convert user=%d dir=%s -> 0x%08x", param != nullptr ? param->user_id : -1, param != nullptr && param->dir_name != nullptr ? param->dir_name->data : "(null)", static_cast(rc)); + return rc; +} + +static int getConvertProgress(float* progress) { + if (progress == nullptr) { + return SAVE_DATA_ERROR_PARAMETER; + } + if (g_initializations == 0) { + return SAVE_DATA_ERROR_NOT_INITIALIZED; + } + *progress = g_convertProgress.load(); + return SAVE_DATA_OK; +} + +int APS5_VABI sceSaveDataGetConvertProgress(float* progress) { + const int rc = getConvertProgress(progress); + SAVEDATA_TRACE("getConvertProgress -> 0x%08x progress=%.2f", static_cast(rc), progress != nullptr && rc == SAVE_DATA_OK ? *progress : 0.0f); + return rc; +} + +int APS5_VABI sceSaveDataCancel() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSaveDataGetConvertProgress() { +int APS5_VABI sceSaveDataDirNameSearch2(const SaveDataDirNameSearchCond* cond, SaveDataDirNameSearchResult* result) { + const int rc = dirNameSearch(cond, result); + SAVEDATA_TRACE("dirNameSearch2 user=%d pattern=%s hit=%u set=%u -> 0x%08x", cond != nullptr ? cond->user_id : -1, cond != nullptr && cond->dir_name != nullptr ? cond->dir_name->data : "(all)", result != nullptr ? result->hit_num : 0u, result != nullptr ? result->set_num : 0u, static_cast(rc)); + return rc; +} + +int APS5_VABI sceSaveDataPrepareForTransferring() { 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; +} + } diff --git a/core/libs/prx/libSceSaveData.native/SaveData.hpp b/core/libs/prx/libSceSaveData.native/SaveData.hpp index 20d02ad5a9..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; @@ -33,6 +33,14 @@ constexpr std::uint32_t SAVE_DATA_PARAM_TYPE_DETAIL = 3; constexpr std::uint32_t SAVE_DATA_PARAM_TYPE_USER_PARAM = 4; constexpr std::uint32_t SAVE_DATA_PARAM_TYPE_MTIME = 5; +constexpr std::uint32_t SAVE_DATA_SORT_KEY_DIRNAME = 0; +constexpr std::uint32_t SAVE_DATA_SORT_KEY_USER_PARAM = 1; +constexpr std::uint32_t SAVE_DATA_SORT_KEY_BLOCKS = 2; +constexpr std::uint32_t SAVE_DATA_SORT_KEY_MTIME = 3; +constexpr std::uint32_t SAVE_DATA_SORT_KEY_FREE_BLOCKS = 5; +constexpr std::uint32_t SAVE_DATA_SORT_ORDER_ASCENT = 0; +constexpr std::uint32_t SAVE_DATA_SORT_ORDER_DESCENT = 1; + constexpr std::size_t SAVE_DATA_MOUNT_SLOTS = 16; struct MountSlot { 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/Export.cpp b/core/libs/prx/libSceScreenShot/Export.cpp new file mode 100644 index 0000000000..ae99fefcf6 --- /dev/null +++ b/core/libs/prx/libSceScreenShot/Export.cpp @@ -0,0 +1,61 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceScreenShotSetOverlayImage() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceScreenShotSetOverlayImageWithOrigin() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceScreenShotCapture() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceScreenShotDisable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceScreenShotDisableNotification() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceScreenShotEnable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceScreenShotEnableNotification() { + 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 sceScreenShotIsDisabled() { + 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 0c609afb1d..a313edc364 100644 --- a/core/libs/prx/libSceShare/Export.cpp +++ b/core/libs/prx/libSceShare/Export.cpp @@ -105,9 +105,12 @@ int APS5_VABI sceShareCaptureScreenshotExtended(const void* extended_param, int3 } -int APS5_VABI sceShareCaptureVideoClipExtended(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceShareCaptureVideoClipExtended(const void* extended_param, int32_t* req_id) { + (void)extended_param; + if (req_id != nullptr) { + *req_id = REQUEST_ID_INVALID; + } + return ERROR_NOT_SUPPORTED; } int APS5_VABI sceShareGetRunningStatus(uint32_t* status) { diff --git a/core/libs/prx/libSceShareFactoryUtil/Export.cpp b/core/libs/prx/libSceShareFactoryUtil/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceShareFactoryUtil/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/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/CMakeLists.txt b/core/libs/prx/libSceSharePlay/CMakeLists.txt new file mode 100644 index 0000000000..1560813dab --- /dev/null +++ b/core/libs/prx/libSceSharePlay/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceSharePlay SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceSharePlay PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceSharePlay) + +if(BUILD_TESTING) + add_test_executable(guest_share_play_tests ${LIBS_INCLUDE_DIR}/tests/GuestSharePlay.cpp) + target_include_directories(guest_share_play_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_share_play_tests PRIVATE libSceSharePlay libc) + configure_windows_unwind(guest_share_play_tests) + add_test(NAME guest_share_play COMMAND guest_share_play_tests) + if(WIN32) + set_tests_properties(guest_share_play PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceSharePlay/Export.cpp b/core/libs/prx/libSceSharePlay/Export.cpp index d0289f4bb2..9f8cc23fee 100644 --- a/core/libs/prx/libSceSharePlay/Export.cpp +++ b/core/libs/prx/libSceSharePlay/Export.cpp @@ -3,10 +3,14 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +static constexpr int SCE_SHARE_PLAY_ERROR_INVALID_ARGS = static_cast(0x810E0001); +static constexpr std::int32_t SCE_USER_SERVICE_USER_ID_INVALID = -1; + extern "C" { -int APS5_VABI sceSharePlayInitialize(void* heap, size_t heap_size) { - if (heap == nullptr || heap_size == 0) APS5_INVALID_ARG_EX; +int APS5_VABI sceSharePlayInitialize(void* heap, size_t heapSize) { + (void)heap; + if (heapSize == 0) APS5_INVALID_ARG_EX; return 0; } @@ -14,4 +18,17 @@ int APS5_VABI sceSharePlayTerminate(void) { return 0; } +int APS5_VABI sceSharePlayGetCurrentConnectionInfoA(SharePlayConnectionInfoA* info) { + if (info == nullptr) return SCE_SHARE_PLAY_ERROR_INVALID_ARGS; + *info = {}; + info->hostUserId = SCE_USER_SERVICE_USER_ID_INVALID; + info->visitorUserId = SCE_USER_SERVICE_USER_ID_INVALID; + return 0; +} + +int APS5_VABI sceSharePlayProhibitController(int prohibited) { + if (prohibited != 0 && prohibited != 1) APS5_INVALID_ARG_EX; + return 0; +} + } diff --git a/core/libs/prx/libSceShareUtility/Export.cpp b/core/libs/prx/libSceShareUtility/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceShareUtility/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/libSceShellUIUtil/Export.cpp b/core/libs/prx/libSceShellUIUtil/Export.cpp new file mode 100644 index 0000000000..4ff433cad7 --- /dev/null +++ b/core/libs/prx/libSceShellUIUtil/Export.cpp @@ -0,0 +1,38 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceShellUIUtilCreateTaskToken() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellUIUtilGetAppUrl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellUIUtilInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellUIUtilLaunchByUri() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellUIUtilLaunchTask() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellUIUtilTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceSigninDialog/Export.cpp b/core/libs/prx/libSceSigninDialog/Export.cpp index ea21b63b23..761bf33308 100644 --- a/core/libs/prx/libSceSigninDialog/Export.cpp +++ b/core/libs/prx/libSceSigninDialog/Export.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -12,8 +13,15 @@ constexpr int COMMON_DIALOG_STATUS_RUNNING = 2; constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004u); +constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast(0x80B80007u); constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); +constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; + +struct SceSigninDialogResult { + std::int32_t result = COMMON_DIALOG_RESULT_USER_CANCELED; + std::int32_t reserved[3] = {}; +}; std::atomic g_status{COMMON_DIALOG_STATUS_NONE}; @@ -22,19 +30,23 @@ std::atomic g_status{COMMON_DIALOG_STATUS_NONE}; extern "C" { int APS5_VABI sceSigninDialogClose(void) { - NotImplemented_nid_no_patch(__func__); + 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 sceSigninDialogGetResult(void* result) { - (void)result; - NotImplemented_nid_no_patch(__func__); + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + const SceSigninDialogResult dialogResult{}; + std::memcpy(result, &dialogResult, sizeof(dialogResult)); return 0; } int APS5_VABI sceSigninDialogGetStatus(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return g_status.load(); } int APS5_VABI sceSigninDialogInitialize(void) { 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..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceSocialScreen/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/libSceSocialScreenDialog/Export.cpp b/core/libs/prx/libSceSocialScreenDialog/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceSocialScreenDialog/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/libSceSpSysCallWrapper/Export.cpp b/core/libs/prx/libSceSpSysCallWrapper/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceSpSysCallWrapper/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/libSceSrcUtl/Export.cpp b/core/libs/prx/libSceSrcUtl/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceSrcUtl/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/libSceSsl/CMakeLists.txt b/core/libs/prx/libSceSsl/CMakeLists.txt index b6cd455072..cb39ff0e4c 100644 --- a/core/libs/prx/libSceSsl/CMakeLists.txt +++ b/core/libs/prx/libSceSsl/CMakeLists.txt @@ -1,14 +1,98 @@ +function(configure_ssl_backend) + set(options ENABLE_PROGRAMS ENABLE_TESTING GEN_FILES USE_STATIC_MBEDTLS_LIBRARY + USE_SHARED_MBEDTLS_LIBRARY DISABLE_PACKAGE_CONFIG_AND_INSTALL MBEDTLS_CONFIG_FILE MBEDTLS_USER_CONFIG_FILE) + foreach(option IN LISTS options) + get_property(saved_${option}_exists CACHE ${option} PROPERTY TYPE SET) + if(saved_${option}_exists) + get_property(saved_${option}_type CACHE ${option} PROPERTY TYPE) + get_property(saved_${option}_help CACHE ${option} PROPERTY HELPSTRING) + get_property(saved_${option}_value CACHE ${option} PROPERTY VALUE) + endif() + endforeach() + set(ENABLE_PROGRAMS OFF) + set(ENABLE_TESTING OFF) + set(GEN_FILES OFF) + set(USE_STATIC_MBEDTLS_LIBRARY ON) + set(USE_SHARED_MBEDTLS_LIBRARY OFF) + set(DISABLE_PACKAGE_CONFIG_AND_INSTALL ON) + set(MBEDTLS_CONFIG_FILE "") + set(MBEDTLS_USER_CONFIG_FILE "${CMAKE_CURRENT_SOURCE_DIR}/MbedConfig.hpp") + foreach(option IN LISTS options) + if(option MATCHES "CONFIG_FILE$") + set(${option} "${${option}}" CACHE FILEPATH "" FORCE) + else() + set(${option} "${${option}}" CACHE BOOL "" FORCE) + endif() + endforeach() + if(NOT DEFINED CMAKE_POLICY_VERSION_MINIMUM OR CMAKE_POLICY_VERSION_MINIMUM VERSION_LESS 3.5) + set(CMAKE_POLICY_VERSION_MINIMUM 3.5) + endif() + add_subdirectory(${CMAKE_SOURCE_DIR}/3rdparty/mbedtls ${CMAKE_BINARY_DIR}/3rdparty/mbedtls EXCLUDE_FROM_ALL) + foreach(property LINK_LIBRARIES INTERFACE_LINK_LIBRARIES) + get_target_property(libraries mbedcrypto ${property}) + if(libraries) + list(REMOVE_ITEM libraries everest) + set_property(TARGET mbedcrypto PROPERTY ${property} "${libraries}") + endif() + endforeach() + set_target_properties(everest PROPERTIES EXCLUDE_FROM_ALL ON) + foreach(option IN LISTS options) + if(saved_${option}_exists) + set(${option} "${saved_${option}_value}" CACHE ${saved_${option}_type} "${saved_${option}_help}" FORCE) + else() + unset(${option} CACHE) + endif() + endforeach() +endfunction() + +configure_ssl_backend() +set_target_properties(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) +endif() + add_library(libSceSsl SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceSsl PRIVATE ${LIBS_INCLUDE_DIR}) +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) +endif() +if(WIN32) + target_link_libraries(libSceSsl PRIVATE bcrypt ws2_32) +endif() configure_windows_unwind(libSceSsl) if(BUILD_TESTING) add_test_executable(guest_ssl_tests ${LIBS_INCLUDE_DIR}/tests/GuestSsl.cpp) target_include_directories(guest_ssl_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_ssl_tests PRIVATE libSceSsl) + target_link_libraries(guest_ssl_tests PRIVATE libSceSsl libc) + configure_windows_unwind(guest_ssl_tests) add_test(NAME guest_ssl COMMAND guest_ssl_tests) + add_test_executable(guest_ssl_load_tests ${LIBS_INCLUDE_DIR}/tests/GuestSslLoad.cpp) + target_include_directories(guest_ssl_load_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ssl_load_tests PRIVATE libSceSsl libc) + configure_windows_unwind(guest_ssl_load_tests) + add_test(NAME guest_ssl_load COMMAND guest_ssl_load_tests) + 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 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 cd9b00548c..698fcbb800 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -1,125 +1,1172 @@ -#include -#include -#include "SceTypes.hpp" +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +#include "SslTypes.hpp" #include "prx/libc/include/General.hpp" -#include -#include +#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 +#include #include #include - -// No network is emulated: contexts, templates and requests can be created, but any request -// that would touch the network fails with the library's network error. -static constexpr int ERROR_NETWORK = static_cast(0x80435001); -static std::atomic g_nextHandle{1}; -static std::mutex g_poolsMutex; -static std::map g_pools; +#include +#include +#include +#include namespace { -constexpr int ERROR_NOT_FOUND = static_cast(0x8095F004); -constexpr int ERROR_INVALID_ARG = static_cast(0x8095177A); +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; -struct SslData { - char* ptr; - size_t size; +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; + std::mutex mutex; }; -struct SslMemoryPoolStats { - size_t pool_size; - size_t max_inuse_size; - size_t current_inuse_size; - int32_t reserved; +struct alignas(std::max_align_t) Allocation { + Pool* pool; + std::size_t size; }; -struct SslCaCerts { - SslData* certs; - size_t num; - void* pool; +thread_local Pool* currentPool = nullptr; +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) { + 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)) { + currentPoolExhausted = true; + return nullptr; + } + auto* allocation = static_cast(std::calloc(1, sizeof(Allocation) + bytes)); + if (!allocation) { + currentPoolExhausted = true; + return nullptr; + } + allocation->pool = currentPool; + allocation->size = bytes; + currentPool->current += bytes; + currentPool->peak = std::max(currentPool->peak, currentPool->current); + return allocation + 1; +} + +void PoolFree(void* pointer) noexcept { + if (!pointer) return; + auto* allocation = static_cast(pointer) - 1; + { + 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; + bool previousExhausted; + explicit PoolScope(Pool& pool) + : previous(std::exchange(currentPool, &pool)), previousExhausted(std::exchange(currentPoolExhausted, false)) {} + ~PoolScope() { + currentPool = previous; + currentPoolExhausted = previousExhausted; + } }; +struct PoolDeleter { + void operator()(std::uint8_t* bytes) const { PoolFree(bytes); } +}; + +using Block = std::unique_ptr; + +Block Allocate(std::size_t size) { + Block block(static_cast(PoolCalloc(1, std::max(1, size)))); + if (!block) throw std::bad_alloc(); + return block; +} + +std::size_t Add(std::size_t left, std::size_t right) { + if (right > std::numeric_limits::max() - left) throw std::length_error("SSL allocation size overflow"); + return left + right; +} + +void CheckBackend(int error, const char* message) { + if (error == 0) return; + if (currentPool && currentPoolExhausted) throw std::bad_alloc(); + throw std::invalid_argument(message); +} + +struct CertificateChain; + +struct CertificateHandle { + std::weak_ptr owner; + const mbedtls_x509_crt* certificate; +}; + +struct CertificateChain { + mbedtls_x509_crt root; + std::vector handles; + CertificateChain() { mbedtls_x509_crt_init(&root); } + ~CertificateChain() { mbedtls_x509_crt_free(&root); } +}; + +struct PrivateKey { + mbedtls_pk_context key; + PrivateKey() { mbedtls_pk_init(&key); } + ~PrivateKey() { mbedtls_pk_free(&key); } +}; + +struct Random { + mbedtls_entropy_context entropy; + mbedtls_ctr_drbg_context drbg; + Random() { + mbedtls_entropy_init(&entropy); + mbedtls_ctr_drbg_init(&drbg); + } + ~Random() { + mbedtls_ctr_drbg_free(&drbg); + mbedtls_entropy_free(&entropy); + } + void Seed() { + CheckBackend(mbedtls_ctr_drbg_seed(&drbg, mbedtls_entropy_func, &entropy, nullptr, 0), "SSL random generator initialization failed"); + } +}; + +struct Identity { + std::shared_ptr chain; + PrivateKey key; +}; + +struct Snapshot { + Block data; + std::vector> certificates; + std::size_t count; +}; + +struct CertNameEntry { + std::string oid; + std::string value; +}; + +struct CertName { + std::vector entries; +}; + +struct Context { + Pool pool; + std::vector> authorities; + std::shared_ptr identity; + std::vector> certificateHandles; + std::unordered_map caLists; + std::unordered_map caCerts; + 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; + 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; + 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) + CheckBackend(mbedtls_ssl_conf_own_cert(&config, &identity->chain->root, &identity->key.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 connections; + unsigned nextHandle = 1; + Runtime() { + if (mbedtls_platform_set_calloc_free(PoolCalloc, PoolFree) != 0) + throw std::runtime_error("SSL allocator initialization failed"); + } +}; + +Runtime& State() { + static auto* runtime = new Runtime; + return *runtime; +} + +Context& GetContext(int handle) { + const auto found = State().contexts.find(handle); + if (found == State().contexts.end()) throw std::invalid_argument("Invalid SSL context"); + return *found->second; +} + +SslConnection* FindConnection(int sslConnectionId) { + const auto connection = State().connections.find(sslConnectionId); + 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)); + } + session.peer = std::move(chain); +} + +struct Input { + Block bytes; + std::size_t size; +}; + +Input CopyInput(const SceSsl::Data& data) { + if (!data.ptr || !data.size) throw std::invalid_argument("Empty SSL certificate/key data"); + auto bytes = Allocate(Add(data.size, 1)); + std::memcpy(bytes.get(), data.ptr, data.size); + auto size = data.size; + std::size_t first = 0; + while (first < size && (bytes.get()[first] == ' ' || bytes.get()[first] == '\r' || bytes.get()[first] == '\n' || bytes.get()[first] == '\t')) ++first; + const bool pem = first < size && bytes.get()[first] == '-'; + if (pem) { + if (bytes.get()[size - 1] != 0) ++size; + if (std::memchr(bytes.get(), 0, size - 1)) throw std::invalid_argument("Embedded null in SSL PEM data"); + } else { + auto* position = bytes.get(); + std::size_t length = 0; + CheckBackend(mbedtls_asn1_get_tag(&position, bytes.get() + size, &length, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE), + "Invalid SSL DER sequence"); + if (length != static_cast(bytes.get() + size - position)) throw std::invalid_argument("Trailing SSL DER data"); + } + return {std::move(bytes), size}; +} + +std::shared_ptr ParseCertificate(const SceSsl::Data& data) { + auto chain = std::make_shared(); + const auto input = CopyInput(data); + CheckBackend(mbedtls_x509_crt_parse(&chain->root, input.bytes.get(), input.size), "Invalid SSL certificate chain"); + return chain; +} + +std::size_t CertificateCount(const Context& context) { + std::size_t count = 0; + for (const auto& chain : context.authorities) count = Add(count, chain->handles.size()); + return count; +} + +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); + auto& context = GetContext(sslCtxId); + const auto* crt = RequireCertificate(function, sslCtxId, sslCert); + const auto* name = subject ? &crt->subject : &crt->issuer; + auto certName = std::make_unique(); + for (const auto* node = name; node; node = node->next) { + if (!node->oid.p || node->oid.len == 0) continue; + std::string oid; + const char* shortName = nullptr; + if (mbedtls_oid_get_attr_short_name(&node->oid, &shortName) == 0 && shortName) { + oid = 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 sceSslFreeCaCerts(int ssl_ctx_id, void* ca_certs) { - (void)ssl_ctx_id; - if (!ca_certs) return ERROR_INVALID_ARG; - *static_cast(ca_certs) = {}; +int APS5_VABI sceSslInit_nid_postfix(std::uint64_t poolSize) { + 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_shared(poolSize)); + ++state.nextHandle; + return handle; +} + +int APS5_VABI sceSslTerm_nid_postfix(int sslCtxId) { + auto& state = State(); + std::lock_guard lock(state.mutex); + if (!state.contexts.erase(sslCtxId)) return ErrorInvalidId; return 0; } -int APS5_VABI sceSslGetCaCerts(int ssl_ctx_id, void* ca_certs) { - (void)ssl_ctx_id; - if (!ca_certs) return ERROR_INVALID_ARG; - *static_cast(ca_certs) = {}; - return ERROR_NOT_FOUND; +int APS5_VABI sceSslGetMemoryPoolStats(int sslCtxId, SceSsl::MemoryPoolStats* stats) { + 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 ErrorInvalidId; + } + auto& pool = found->second->pool; + std::lock_guard poolLock(pool.mutex); + *stats = {pool.size, pool.peak, pool.current, 0}; + return 0; } -int APS5_VABI sceSslInit_nid_postfix(uint64_t pool_size) { - const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); - std::lock_guard lock(g_poolsMutex); - g_pools[id] = pool_size; +int APS5_VABI sceSslLoadCert(int sslCtxId, int caCertNum, SceSsl::Data** caList, SceSsl::Data* cert, SceSsl::Data* privateKey) { + if (caCertNum < 0 || (caCertNum && !caList) || (!!cert != !!privateKey)) + throw std::invalid_argument("Invalid SSL certificate load arguments"); + auto& state = State(); + std::lock_guard lock(state.mutex); + auto& context = GetContext(sslCtxId); + PoolScope scope(context.pool); + std::vector> authorities; + std::vector> handles; + for (int index = 0; index < caCertNum; ++index) { + if (!caList[index]) throw std::invalid_argument("Missing SSL certificate descriptor"); + auto chain = ParseCertificate(*caList[index]); + for (auto* entry = &chain->root; entry; entry = entry->next) { + auto handle = std::make_unique(CertificateHandle{chain, entry}); + chain->handles.push_back(handle.get()); + handles.push_back(std::move(handle)); + } + authorities.push_back(std::move(chain)); + } + std::shared_ptr identity; + if (cert) { + identity = std::make_shared(); + identity->chain = ParseCertificate(*cert); + const auto key = CopyInput(*privateKey); + Random random; + random.Seed(); + CheckBackend(mbedtls_pk_parse_key(&identity->key.key, key.bytes.get(), key.size, nullptr, 0, + mbedtls_ctr_drbg_random, &random.drbg), "Invalid SSL private key"); + CheckBackend(mbedtls_pk_check_pair(&identity->chain->root.pk, &identity->key.key, mbedtls_ctr_drbg_random, &random.drbg), + "SSL client certificate and private key do not match"); + } + context.authorities.reserve(Add(context.authorities.size(), authorities.size())); + context.certificateHandles.reserve(Add(context.certificateHandles.size(), handles.size())); + for (auto& chain : authorities) context.authorities.push_back(std::move(chain)); + for (auto& handle : handles) context.certificateHandles.push_back(std::move(handle)); + if (identity) context.identity = std::move(identity); + return 0; +} + +int APS5_VABI sceSslUnloadCert(int sslCtxId) { + auto& state = State(); + std::lock_guard lock(state.mutex); + auto& context = GetContext(sslCtxId); + context.authorities.clear(); + context.identity.reset(); + return 0; +} + +int APS5_VABI sceSslGetCaList(int sslCtxId, SceSsl::CaList* output) { + if (!output) return ErrorInvalidArg; + auto& state = State(); + std::lock_guard lock(state.mutex); + auto& context = GetContext(sslCtxId); + PoolScope scope(context.pool); + const auto count = CertificateCount(context); + if (!count) { + *output = {}; + return ErrorNotFound; + } + if (count > static_cast(std::numeric_limits::max())) throw std::length_error("Too many SSL authorities"); + Snapshot snapshot{Allocate(count * sizeof(void*)), context.authorities, count}; + auto** pointers = reinterpret_cast(snapshot.data.get()); + std::size_t index = 0; + for (const auto& chain : snapshot.certificates) for (auto* handle : chain->handles) pointers[index++] = handle; + context.caLists.emplace(pointers, std::move(snapshot)); + *output = {pointers, static_cast(count)}; + return 0; +} + +int APS5_VABI sceSslFreeCaList(int sslCtxId, SceSsl::CaList* list) { + if (!list) return ErrorInvalidArg; + auto& state = State(); + std::lock_guard lock(state.mutex); + auto& context = GetContext(sslCtxId); + if (!list->certs && list->num == 0) return 0; + const auto found = context.caLists.find(list->certs); + if (found == context.caLists.end() || list->num < 0 || static_cast(list->num) != found->second.count) + throw std::invalid_argument("Invalid SSL CA list allocation"); + context.caLists.erase(found); + *list = {}; + return 0; +} + +int APS5_VABI sceSslGetCaCerts(int sslCtxId, SceSsl::CaCerts* output) { + if (!output) return ErrorInvalidArg; + auto& state = State(); + std::lock_guard lock(state.mutex); + const auto found = state.contexts.find(sslCtxId); + if (found == state.contexts.end()) { + *output = {}; + return ErrorNotFound; + } + auto& context = *found->second; + PoolScope scope(context.pool); + const auto count = CertificateCount(context); + if (!count) { + *output = {}; + return ErrorNotFound; + } + if (count > std::numeric_limits::max() / sizeof(SceSsl::Data)) throw std::length_error("Too many SSL authorities"); + auto total = count * sizeof(SceSsl::Data); + for (const auto& chain : context.authorities) + for (auto* entry = &chain->root; entry; entry = entry->next) total = Add(total, entry->raw.len); + Snapshot snapshot{Allocate(total), {}, count}; + auto* data = reinterpret_cast(snapshot.data.get()); + auto* next = snapshot.data.get() + count * sizeof(SceSsl::Data); + std::size_t index = 0; + for (const auto& chain : context.authorities) { + for (auto* entry = &chain->root; entry; entry = entry->next) { + data[index++] = {next, entry->raw.len}; + std::memcpy(next, entry->raw.p, entry->raw.len); + next += entry->raw.len; + } + } + context.caCerts.emplace(data, std::move(snapshot)); + *output = {data, count, data}; + return 0; +} + +int APS5_VABI sceSslFreeCaCerts(int sslCtxId, SceSsl::CaCerts* certificates) { + if (!certificates) return ErrorInvalidArg; + auto& state = State(); + std::lock_guard lock(state.mutex); + const auto found = state.contexts.find(sslCtxId); + if (found != state.contexts.end()) { + auto& snapshots = found->second->caCerts; + const auto allocation = snapshots.find(certificates->pool); + if (allocation != snapshots.end()) { + if (certificates->certs != certificates->pool || certificates->num != allocation->second.count) + throw std::invalid_argument("Invalid SSL CA certificate allocation"); + snapshots.erase(allocation); + } + } + *certificates = {}; + return 0; +} + +int APS5_VABI sceSslCreateConnection(int sslCtxId, int sockId, const char* host) { + if (!host) return ErrorInvalidValue; + std::lock_guard lock(State().mutex); + if (!State().contexts.contains(sslCtxId)) return ErrorInvalidId; + auto& state = State(); + if (state.nextHandle > static_cast(std::numeric_limits::max())) throw std::overflow_error("SSL connection handles exhausted"); + const auto id = static_cast(state.nextHandle); + state.connections.emplace(id, SslConnection{sslCtxId, sockId, host, nullptr, nullptr}); + ++state.nextHandle; return id; } -int APS5_VABI sceSslTerm_nid_postfix(int ssl_ctx_id) { - std::lock_guard lock(g_poolsMutex); - g_pools.erase(ssl_ctx_id); +int APS5_VABI sceSslDeleteConnection(int sslConnectionId) { + std::shared_ptr session; + std::lock_guard lock(State().mutex); + auto* connection = FindConnection(sslConnectionId); + if (!connection) return ErrorInvalidId; + session = std::move(connection->session); + State().connections.erase(sslConnectionId); 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 sceSslLoadCert() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceSslGetMemoryPoolStats(int ssl_ctx_id, SslMemoryPoolStats* stats) { - if (stats == nullptr) APS5_INVALID_ARG_EX; - std::lock_guard lock(g_poolsMutex); - const auto pool = g_pools.find(ssl_ctx_id); - if (pool == g_pools.end()) throw std::invalid_argument("sceSslGetMemoryPoolStats: unknown context"); - *stats = {static_cast(pool->second), 0, 0, 0}; - return 0; -} - -int APS5_VABI sceSslFreeSslCertName(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceSslGetIssuerName(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceSslGetNameEntryCount(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceSslGetNameEntryInfo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceSslGetPem(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceSslGetSubjectName(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceSslSetVerifyCallback(int sslConnectionId, SslVerifyCallback cbfunc, void* userArg) { + std::lock_guard lock(State().mutex); + 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) { + std::shared_ptr session; + return Establish(__func__, sslConnectionId, session); +} + +int APS5_VABI sceSslRead(int sslConnectionId, void* buf, size_t len, int flags) { + 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) { + const int result = mbedtls_ssl_read(&session->ssl, bytes + received, total - received); + 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) { + 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) { + if (!sboLen) return ErrorInvalidArg; + std::lock_guard lock(State().mutex); + const auto* crt = RequireCertificate(__func__, sslCtxId, sslCert); + const auto reqLen = crt->serial.len; + if (!sboData || *sboLen == 0) { + *sboLen = reqLen; + return 0; + } + if (*sboLen < reqLen) { + *sboLen = reqLen; + return ErrorInvalidArg; + } + std::memcpy(sboData, crt->serial.p, reqLen); + return 0; +} + +int APS5_VABI sceSslFreeSslCertName(int sslCtxId, void* certName) { + if (!certName) return ErrorInvalidArg; + auto& state = State(); + std::lock_guard lock(state.mutex); + auto& context = GetContext(sslCtxId); + const auto found = context.certNames.find(certName); + if (found == context.certNames.end()) throw std::invalid_argument("sceSslFreeSslCertName: unknown cert name"); + context.certNames.erase(found); + return 0; +} + +int APS5_VABI 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); + session = RequireConnection(__func__, sslConnectionId).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) { + if (!proto) throw std::invalid_argument("sceSslSetAlpn: null protocol list"); + std::lock_guard lock(State().mutex); + auto* connection = FindConnection(sslConnectionId); + if (!connection) return ErrorInvalidId; + if (!*proto) return ErrorBadLength; + connection->alpn = proto; + 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) { + if (!certName) return ErrorInvalidArg; + auto& state = State(); + std::lock_guard lock(state.mutex); + auto& context = GetContext(sslCtxId); + const auto found = context.certNames.find(certName); + if (found == context.certNames.end()) throw std::invalid_argument("sceSslGetNameEntryCount: unknown cert name"); + return static_cast(found->second->entries.size()); +} + +int APS5_VABI sceSslGetNameEntryInfo(int sslCtxId, void* certName, int entryNum, char* oidname, std::size_t maxOidnameLen, + char* value, std::size_t maxValueLen, std::size_t* valueLen) { + if (!certName) return ErrorInvalidArg; + auto& state = State(); + std::lock_guard lock(state.mutex); + auto& context = GetContext(sslCtxId); + const auto found = context.certNames.find(certName); + if (found == context.certNames.end()) throw std::invalid_argument("sceSslGetNameEntryInfo: unknown cert name"); + const auto& entries = found->second->entries; + if (entryNum < 0 || static_cast(entryNum) >= entries.size()) return ErrorInvalidArg; + const auto& entry = entries[static_cast(entryNum)]; + if (oidname && maxOidnameLen > 0) { + const auto copyLen = std::min(entry.oid.size(), maxOidnameLen - 1); + std::memcpy(oidname, entry.oid.data(), copyLen); + oidname[copyLen] = '\0'; + } + if (value && maxValueLen > 0) { + const auto copyLen = std::min(entry.value.size(), maxValueLen - 1); + std::memcpy(value, entry.value.data(), copyLen); + value[copyLen] = '\0'; + } + if (valueLen) *valueLen = entry.value.size(); + return 0; +} + +int APS5_VABI sceSslGetNotBefore(int sslCtxId, void* sslCert, SceSsl::RtcTick* begin) { + if (!begin) return ErrorInvalidArg; + std::lock_guard lock(State().mutex); + const auto* crt = RequireCertificate(__func__, sslCtxId, sslCert); + begin->tick = TimeToTick(crt->valid_from); + return 0; +} + +int APS5_VABI sceSslGetNotAfter(int sslCtxId, void* sslCert, SceSsl::RtcTick* limit) { + if (!limit) return ErrorInvalidArg; + std::lock_guard lock(State().mutex); + const auto* crt = RequireCertificate(__func__, sslCtxId, sslCert); + limit->tick = TimeToTick(crt->valid_to); + return 0; +} + +int APS5_VABI sceSslGetFingerprint(int sslCtxId, void* sslCert, int hashType, unsigned char* fingerprint) { + if (!fingerprint) return ErrorInvalidArg; + mbedtls_md_type_t mdType; + switch (hashType) { + case 0: mdType = MBEDTLS_MD_MD5; break; + case 1: mdType = MBEDTLS_MD_SHA1; break; + case 2: mdType = MBEDTLS_MD_SHA224; break; + case 3: mdType = MBEDTLS_MD_SHA256; break; + case 4: mdType = MBEDTLS_MD_SHA384; break; + case 5: mdType = MBEDTLS_MD_SHA512; break; + default: return ErrorInvalidArg; + } + const auto* info = mbedtls_md_info_from_type(mdType); + if (!info) return ErrorInvalidArg; + std::lock_guard lock(State().mutex); + const auto* crt = RequireCertificate(__func__, sslCtxId, sslCert); + if (mbedtls_md(info, crt->raw.p, crt->raw.len, fingerprint) != 0) return ErrorInvalidArg; + return 0; +} + +int APS5_VABI sceSslGetPem(int sslCtxId, void* sslCert, char* buf, std::size_t* bufLen) { + if (!bufLen) return ErrorInvalidArg; + std::lock_guard lock(State().mutex); + auto& context = GetContext(sslCtxId); + const auto* crt = RequireCertificate(__func__, sslCtxId, sslCert); + PoolScope scope(context.pool); + std::size_t required = 0; + const auto encode = [&](unsigned char* output, std::size_t capacity, std::size_t* written) { + return mbedtls_pem_write_buffer("-----BEGIN CERTIFICATE-----\n", "-----END CERTIFICATE-----\n", crt->raw.p, crt->raw.len, output, capacity, written); + }; + const int sizing = encode(nullptr, 0, &required); + if (sizing != MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL) CheckBackend(sizing, "SSL PEM sizing failed"); + if (!buf) { + *bufLen = required; + return 0; + } + if (*bufLen < required) throw std::length_error("SSL PEM buffer is too small"); + std::size_t written = 0; + CheckBackend(encode(reinterpret_cast(buf), required, &written), "SSL PEM encoding failed"); + *bufLen = written; + return 0; +} + +void* APS5_VABI sceSslGetSubjectName(int sslCtxId, void* sslCert) { + return ExtractCertName(__func__, sslCtxId, sslCert, true); +} + +} diff --git a/core/libs/prx/libSceSsl/MbedConfig.hpp b/core/libs/prx/libSceSsl/MbedConfig.hpp new file mode 100644 index 0000000000..aab4833dc9 --- /dev/null +++ b/core/libs/prx/libSceSsl/MbedConfig.hpp @@ -0,0 +1,6 @@ +#pragma once + +#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 new file mode 100644 index 0000000000..f82ac92e0e --- /dev/null +++ b/core/libs/prx/libSceSsl/SslTypes.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include + +namespace SceSsl { + +struct Data { + std::uint8_t* ptr; + std::size_t size; +}; + +struct CaCerts { + Data* certs; + std::size_t num; + void* pool; +}; + +struct CaList { + void** certs; + int num; +}; + +struct MemoryPoolStats { + std::size_t poolSize, maxInuseSize, currentInuseSize; + std::uint32_t reserved; +}; + +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..7c5cd4bbd6 --- /dev/null +++ b/core/libs/prx/libSceSysCore/Export.cpp @@ -0,0 +1,13 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceApplicationGetAppInfoByAppId() { + 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..8cdc46a182 --- /dev/null +++ b/core/libs/prx/libSceSysUtil/Export.cpp @@ -0,0 +1,13 @@ +#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; +} + +} diff --git a/core/libs/prx/libSceSysmodule/CMakeLists.txt b/core/libs/prx/libSceSysmodule/CMakeLists.txt index b9111e032f..b03a2433c9 100644 --- a/core/libs/prx/libSceSysmodule/CMakeLists.txt +++ b/core/libs/prx/libSceSysmodule/CMakeLists.txt @@ -5,8 +5,26 @@ configure_windows_unwind(libSceSysmodule) if(BUILD_TESTING) add_test_executable(guest_sysmodule_tests ${LIBS_INCLUDE_DIR}/tests/GuestSysmodule.cpp) target_include_directories(guest_sysmodule_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_sysmodule_tests PRIVATE libSceSysmodule) + target_link_libraries(guest_sysmodule_tests PRIVATE libSceSysmodule libkernel libc) + configure_windows_unwind(guest_sysmodule_tests) + set(guestSysmoduleRun ${CMAKE_CURRENT_BINARY_DIR}/guest_sysmodule) + file(MAKE_DIRECTORY ${guestSysmoduleRun}/libs) + add_custom_target(guest_sysmodule_runtime + COMMAND "${CMAKE_COMMAND}" -E copy_if_different + "$" "${guestSysmoduleRun}/libs/libSceFiber.prx" + COMMAND "${CMAKE_COMMAND}" -E copy_if_different + "$" "${guestSysmoduleRun}/libs/libSceAudioPropagation.prx" + VERBATIM + ) + add_dependencies(guest_sysmodule_runtime libSceFiber libSceAudioPropagation) + add_dependencies(guest_sysmodule_tests guest_sysmodule_runtime) add_test(NAME guest_sysmodule COMMAND guest_sysmodule_tests) + set_tests_properties(guest_sysmodule PROPERTIES WORKING_DIRECTORY ${guestSysmoduleRun}) + if(UNIX AND NOT APPLE) + add_test(NAME guest_sysmodule_maps_scan COMMAND guest_sysmodule_tests) + set_tests_properties(guest_sysmodule_maps_scan PROPERTIES ENVIRONMENT "APS5_NO_PROCMAP_QUERY=1" + WORKING_DIRECTORY ${guestSysmoduleRun}) + endif() if(WIN32) set_tests_properties(guest_sysmodule PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") diff --git a/core/libs/prx/libSceSysmodule/Export.cpp b/core/libs/prx/libSceSysmodule/Export.cpp index 85b64a2ed7..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 @@ -9,15 +10,21 @@ #include #include #else -#include +#include "prx/libc/include/specifics/linux/HostMapping.hpp" #endif #include "SceTypes.hpp" #include "ModuleTable.hpp" #include "prx/libc/include/General.hpp" +extern "C" void* APS5_VABI dlopen_nid_postfix(const char* path, int flags); + namespace { +constexpr int SCE_SYSMODULE_ERROR_INVALID_VALUE = static_cast(0x805A1000); +constexpr int SCE_SYSMODULE_ERROR_UNLOADED = static_cast(0x805A1001); +constexpr int kRtldNow = 2; + const char* findModuleName(const std::uint32_t id) { const auto it = kModuleTable.find(id); return it != kModuleTable.end() ? it->second : nullptr; @@ -25,66 +32,108 @@ const char* findModuleName(const std::uint32_t id) { std::mutex gMutex; std::unordered_map gLoadCount; +std::unordered_map gHandles; + +void* openHostModule(const char* name) { + const std::string relative = std::string("libs/") + name + ".prx"; + 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 - std::ifstream maps("/proc/self/maps"); - if (!maps) { - throw std::runtime_error("sceSysmoduleGetModuleInfoForUnwind: failed to open /proc/self/maps"); + const auto mapping = HostMapping::Find(addr, "sceSysmoduleGetModuleInfoForUnwind"); + if (!mapping) { + return false; } - std::string line; - while (std::getline(maps, line)) { - std::uint64_t start = 0; - std::uint64_t end = 0; - char perms[8] = {}; - std::uint64_t offset = 0; - unsigned int devMajor = 0; - unsigned int devMinor = 0; - std::uint64_t inode = 0; - char path[4096] = {}; - int parsed = std::sscanf( - line.c_str(), - "%llx-%llx %7s %llx %x:%x %llu %4095s", - (unsigned long long*)&start, - (unsigned long long*)&end, - perms, - (unsigned long long*)&offset, - &devMajor, - &devMinor, - (unsigned long long*)&inode, - path - ); - if (parsed < 7 || addr < start || addr >= end) { - continue; - } - info->st_size = sizeof(ModuleInfoForUnwind); - std::strncpy(info->name, parsed >= 8 ? path : "", sizeof(info->name) - 1); - info->name[sizeof(info->name) - 1] = '\0'; - info->eh_frame_hdr_addr = 0; - info->eh_frame_addr = 0; - info->eh_frame_size = 0; - info->seg0_addr = start; - info->seg0_size = end - start; - return true; - } - return false; + info->st_size = sizeof(ModuleInfoForUnwind); + std::strncpy(info->name, mapping->path.c_str(), sizeof(info->name) - 1); + info->name[sizeof(info->name) - 1] = '\0'; + info->eh_frame_hdr_addr = 0; + info->eh_frame_addr = 0; + info->eh_frame_size = 0; + info->seg0_addr = mapping->start; + info->seg0_size = mapping->end - mapping->start; + return true; #endif } @@ -100,30 +149,59 @@ int APS5_VABI sceSysmoduleGetModuleInfoForUnwind(std::uint64_t addr, int flags, return 0; } +int APS5_VABI sceSysmoduleGetModuleHandleInternal(std::uint32_t id, std::int32_t* handle) { + if ((id & 0x7fffffffu) == 0) { + throw std::runtime_error("sceSysmoduleGetModuleHandleInternal: invalid id 0"); + } + if (!handle) { + throw std::runtime_error("sceSysmoduleGetModuleHandleInternal: null handle"); + } + const char* name = findModuleName(id); + if (!name) { + throw std::runtime_error(std::string("sceSysmoduleGetModuleHandleInternal: unknown id ") + std::to_string(id)); + } + std::lock_guard lock(gMutex); + const auto loaded = gLoadCount.find(id); + if (loaded == gLoadCount.end() || loaded->second < 1) { + return SCE_SYSMODULE_ERROR_UNLOADED; + } + const auto existing = gHandles.find(id); + if (existing != gHandles.end()) { + *handle = existing->second; + return 0; + } + void* module = openHostModule(name); + if (!module) { + throw std::runtime_error(std::string("sceSysmoduleGetModuleHandleInternal: failed to open ") + name); + } + const auto value = static_cast(reinterpret_cast(module)); + gHandles.emplace(id, value); + *handle = value; + return 0; +} + int APS5_VABI sceSysmoduleIsLoaded(std::uint16_t id) { if (id == 0) { - throw std::runtime_error("sceSysmoduleIsLoaded: invalid id 0"); + return SCE_SYSMODULE_ERROR_INVALID_VALUE; } 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); auto it = gLoadCount.find(id); if (it == gLoadCount.end() || it->second < 1) { - return 0x80A90002; + return SCE_SYSMODULE_ERROR_UNLOADED; } return 0; } int APS5_VABI sceSysmoduleLoadModule(std::uint16_t id) { if (id == 0) { - throw std::runtime_error("sceSysmoduleLoadModule: invalid id 0"); + return SCE_SYSMODULE_ERROR_INVALID_VALUE; } 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); @@ -136,7 +214,7 @@ int APS5_VABI sceSysmoduleLoadModuleInternalWithArg(std::uint32_t id, int argc, (void)argv; (void)unk; if ((id & 0x7fffffffu) == 0) { - throw std::runtime_error("sceSysmoduleLoadModuleInternalWithArg: invalid id 0"); + return SCE_SYSMODULE_ERROR_INVALID_VALUE; } if (!findModuleName(id)) { throw std::runtime_error(std::string("sceSysmoduleLoadModuleInternalWithArg: unknown id ") + std::to_string(id)); @@ -151,7 +229,7 @@ int APS5_VABI sceSysmoduleLoadModuleInternalWithArg(std::uint32_t id, int argc, int APS5_VABI sceSysmoduleUnloadModule(std::uint16_t id) { if (id == 0) { - throw std::runtime_error("sceSysmoduleUnloadModule: invalid id 0"); + return SCE_SYSMODULE_ERROR_INVALID_VALUE; } if (!findModuleName(id)) { APS5_LOG_OUT("unknown id: %u", static_cast(id)); @@ -160,7 +238,7 @@ int APS5_VABI sceSysmoduleUnloadModule(std::uint16_t id) { std::lock_guard lock(gMutex); auto it = gLoadCount.find(id); if (it == gLoadCount.end() || it->second < 1) { - return 0x80A90003; + return SCE_SYSMODULE_ERROR_UNLOADED; } it->second--; return 0; @@ -168,7 +246,7 @@ int APS5_VABI sceSysmoduleUnloadModule(std::uint16_t id) { int APS5_VABI sceSysmoduleLoadModuleInternal(std::uint32_t id) { if ((id & 0x7fffffffu) == 0) { - throw std::runtime_error("sceSysmoduleLoadModuleInternal: invalid id 0"); + return SCE_SYSMODULE_ERROR_INVALID_VALUE; } if (!findModuleName(id)) { throw std::runtime_error(std::string("sceSysmoduleLoadModuleInternal: unknown id ") + std::to_string(id)); @@ -180,7 +258,7 @@ int APS5_VABI sceSysmoduleLoadModuleInternal(std::uint32_t id) { int APS5_VABI sceSysmoduleUnloadModuleInternal(std::uint32_t id) { if ((id & 0x7fffffffu) == 0) { - throw std::runtime_error("sceSysmoduleUnloadModuleInternal: invalid id 0"); + return SCE_SYSMODULE_ERROR_INVALID_VALUE; } if (!findModuleName(id)) { throw std::runtime_error(std::string("sceSysmoduleUnloadModuleInternal: unknown id ") + std::to_string(id)); @@ -188,10 +266,77 @@ int APS5_VABI sceSysmoduleUnloadModuleInternal(std::uint32_t id) { std::lock_guard lock(gMutex); auto it = gLoadCount.find(id); if (it == gLoadCount.end() || it->second < 1) { - return 0x80A90003; + return SCE_SYSMODULE_ERROR_UNLOADED; } it->second--; 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 fd0d5efbc8..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,15 +148,32 @@ 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"}, {0x00000113, "libSceNpEntitlementAccess"}, + {0x00000114, "libSceVoiceChat"}, {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"}, {0x00000139, "libSceNpAuthAuthorizedAppDialog"}, {0x80000001, "libSceAudioOut"}, @@ -267,22 +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 61f241f153..4d74061f67 100644 --- a/core/libs/prx/libSceSystemGesture/Export.cpp +++ b/core/libs/prx/libSceSystemGesture/Export.cpp @@ -2,20 +2,140 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#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); +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; +std::vector primitiveTouches; + +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(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) { - (void)gesture_handle; - (void)recognizer; - NotImplemented_nid_no_patch(__func__); - return 0; + if (gesture_handle != GESTURE_HANDLE) 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) { @@ -23,74 +143,86 @@ int APS5_VABI sceSystemGestureClose(int32_t gesture_handle) { } int APS5_VABI sceSystemGestureCreateTouchRecognizer(int32_t gesture_handle, SystemGestureTouchRecognizer* recognizer, int32_t type, const SystemGestureRectangle* rectangle, const void* param) { - (void)type; - (void)rectangle; (void)param; if (gesture_handle != GESTURE_HANDLE) 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); + primitiveTouches.clear(); return 0; } int APS5_VABI sceSystemGestureGetPrimitiveTouchEventByIndex(int32_t gesture_handle, uint32_t index, SystemGesturePrimitiveTouchEvent* event) { - (void)gesture_handle; - (void)index; - (void)event; - NotImplemented_nid_no_patch(__func__); - return 0; + if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + std::lock_guard lock(primitiveTouchMutex); + if (index >= primitiveTouches.size()) return SCE_SYSTEM_GESTURE_ERROR_INDEX_OUT_OF_ARRAY; + if (!event) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; + *event = buildPrimitiveTouchEvent(primitiveTouches[index]); + return 0; } 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; + if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + if (!event) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(primitiveTouchMutex); + const auto tracked = std::find_if(primitiveTouches.begin(), primitiveTouches.end(), + [&](const TrackedTouch& touch) { return touch.primitiveId == primitiveId; }); + if (tracked == primitiveTouches.end()) + throw std::runtime_error(std::string(__func__) + ": no primitive touch event with id " + std::to_string(primitiveId)); + *event = buildPrimitiveTouchEvent(*tracked); + return 0; } int APS5_VABI sceSystemGestureGetPrimitiveTouchEvents(int32_t gesture_handle, SystemGesturePrimitiveTouchEvent* event_buffer, uint32_t capacity_of_buffer, uint32_t* number_of_event) { - (void)gesture_handle; - (void)event_buffer; - (void)capacity_of_buffer; - (void)number_of_event; - NotImplemented_nid_no_patch(__func__); - return 0; + if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + if (!number_of_event) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(primitiveTouchMutex); + const std::uint32_t reported = static_cast(std::min(primitiveTouches.size(), capacity_of_buffer)); + if (reported > 0 && !event_buffer) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; + for (std::uint32_t i = 0; i < reported; ++i) event_buffer[i] = buildPrimitiveTouchEvent(primitiveTouches[i]); + *number_of_event = reported; + return 0; } int APS5_VABI sceSystemGestureGetPrimitiveTouchEventsCount(int32_t gesture_handle) { - (void)gesture_handle; - NotImplemented_nid_no_patch(__func__); - return 0; + if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + std::lock_guard lock(primitiveTouchMutex); + return static_cast(primitiveTouches.size()); } 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; + (void)eventId; + (void)event; + if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + if (!recognizer) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; + return SCE_SYSTEM_GESTURE_ERROR_INDEX_OUT_OF_ARRAY; } int APS5_VABI sceSystemGestureGetTouchEventByIndex(int32_t gesture_handle, const SystemGestureTouchRecognizer* recognizer, uint32_t index, SystemGestureTouchEvent* event) { - (void)gesture_handle; - (void)recognizer; (void)index; (void)event; + if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + if (!recognizer) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; return SCE_SYSTEM_GESTURE_ERROR_INDEX_OUT_OF_ARRAY; } int APS5_VABI sceSystemGestureGetTouchEvents(int32_t gesture_handle, const SystemGestureTouchRecognizer* recognizer, SystemGestureTouchEvent* event_buffer, uint32_t capacity_of_buffer, uint32_t* number_of_event) { - (void)gesture_handle; - (void)recognizer; - (void)event_buffer; - (void)capacity_of_buffer; - (void)number_of_event; - NotImplemented_nid_no_patch(__func__); - return 0; + (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; + return 0; } int APS5_VABI sceSystemGestureGetTouchEventsCount(int32_t gesture_handle, const SystemGestureTouchRecognizer* recognizer) { @@ -99,15 +231,23 @@ int APS5_VABI sceSystemGestureGetTouchEventsCount(int32_t gesture_handle, const } 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; + if (gesture_handle != GESTURE_HANDLE) 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; } int APS5_VABI sceSystemGestureInitializePrimitiveTouchRecognizer(const void* param) { (void)param; + std::lock_guard lock(primitiveTouchMutex); + primitiveTouches.clear(); return 0; } @@ -118,47 +258,60 @@ int32_t APS5_VABI sceSystemGestureOpen(int32_t input_type, const void* param) { } int APS5_VABI sceSystemGestureRemoveTouchRecognizer(int32_t gesture_handle, SystemGestureTouchRecognizer* recognizer) { - (void)gesture_handle; - (void)recognizer; - NotImplemented_nid_no_patch(__func__); - return 0; + if (gesture_handle != GESTURE_HANDLE) 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) { - (void)gesture_handle; - NotImplemented_nid_no_patch(__func__); - return 0; + if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + std::lock_guard lock(primitiveTouchMutex); + primitiveTouches.clear(); + return 0; } int APS5_VABI sceSystemGestureResetTouchRecognizer(int32_t gesture_handle, SystemGestureTouchRecognizer* recognizer) { - (void)gesture_handle; - (void)recognizer; - NotImplemented_nid_no_patch(__func__); - return 0; + if (gesture_handle != GESTURE_HANDLE) 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) { - (void)gesture_handle; - NotImplemented_nid_no_patch(__func__); - return 0; + if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + return 0; } int APS5_VABI sceSystemGestureUpdatePrimitiveTouchRecognizer(int32_t gesture_handle, const void* param) { - (void)param; - return gesture_handle == GESTURE_HANDLE ? 0 : SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + if (!param) return 0; + const auto* input = static_cast(param); + if (!input->pad_data_buffer || input->report_number <= 0) return 0; + std::lock_guard lock(primitiveTouchMutex); + for (std::int32_t i = 0; i < input->report_number; ++i) sampleReport(input->pad_data_buffer[i]); + return 0; } int APS5_VABI sceSystemGestureUpdateTouchRecognizer(int32_t gesture_handle, SystemGestureTouchRecognizer* recognizer) { - (void)recognizer; - return gesture_handle == GESTURE_HANDLE ? 0 : SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + if (gesture_handle != GESTURE_HANDLE) 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) { - (void)gesture_handle; - (void)recognizer; - (void)rectangle; - NotImplemented_nid_no_patch(__func__); - return 0; + if (gesture_handle != GESTURE_HANDLE) 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..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceSystemLogger/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/libSceSystemLogger2/Export.cpp b/core/libs/prx/libSceSystemLogger2/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceSystemLogger2/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/libSceSystemLogger2Delivery/Export.cpp b/core/libs/prx/libSceSystemLogger2Delivery/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceSystemLogger2Delivery/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/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..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceSystemLogger2NativeQueueClient/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/libSceSystemService/CMakeLists.txt b/core/libs/prx/libSceSystemService/CMakeLists.txt index e90856e4ec..3542172d1b 100644 --- a/core/libs/prx/libSceSystemService/CMakeLists.txt +++ b/core/libs/prx/libSceSystemService/CMakeLists.txt @@ -1,23 +1,70 @@ -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) + if(WIN32) + add_test_executable(guest_system_service_path_boundary_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemServicePathBoundary.cpp) + target_include_directories(guest_system_service_path_boundary_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_system_service_path_boundary_tests PRIVATE libSceSystemService libc) + configure_windows_unwind(guest_system_service_path_boundary_tests) + add_test(NAME guest_system_service_path_boundary COMMAND guest_system_service_path_boundary_tests) + set_tests_properties(guest_system_service_path_boundary PROPERTIES TIMEOUT 20) + set_tests_properties(guest_system_service_path_boundary PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_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) + add_test(NAME loadexec_arguments COMMAND ${Python3_EXECUTABLE} + ${CMAKE_SOURCE_DIR}/core/relinker/relinker/tests/test_loadexec_arguments.py + $ $ + $ $ $ $ ${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) - set_tests_properties(guest_system_exit PROPERTIES - PASS_REGULAR_EXPRESSION "Guest shutdown and atexit completed") + 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) + 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 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) @@ -29,6 +76,24 @@ if(BUILD_TESTING) set_tests_properties(guest_system_service PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_system_service_language_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemServiceLanguage.cpp) + target_include_directories(guest_system_service_language_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_system_service_language_tests PRIVATE libSceSystemService libc) + configure_windows_unwind(guest_system_service_language_tests) + add_test(NAME guest_system_service_language COMMAND guest_system_service_language_tests) + if(WIN32) + 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) @@ -36,10 +101,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 ab607a86ee..6c37c6863d 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -1,23 +1,252 @@ +#include +#include #include #include +#include #include #include +#include +#include +#include +#include +#include +#include +#include +#if defined(_WIN32) +#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/HostHdr.hpp" + +namespace { + +struct ConsoleTimeZone { + int minutes; + bool summer; +}; + +ConsoleTimeZone HostTimeZone() { + const std::time_t now = std::time(nullptr); + std::tm local{}; +#if defined(_WIN32) + const bool converted = localtime_s(&local, &now) == 0; +#else + const bool converted = localtime_r(&now, &local) != nullptr; +#endif + if (!converted) throw std::runtime_error("sceSystemServiceParamGetInt: the host local time is unavailable"); + const std::chrono::sys_days date = std::chrono::year{local.tm_year + 1900} / std::chrono::month{static_cast(local.tm_mon + 1)} / std::chrono::day{static_cast(local.tm_mday)}; + const auto localSeconds = date.time_since_epoch() + std::chrono::hours{local.tm_hour} + std::chrono::minutes{local.tm_min} + std::chrono::seconds{local.tm_sec}; + const auto offset = std::chrono::duration_cast(localSeconds - std::chrono::seconds{now}); + const bool summer = local.tm_isdst > 0; +#if defined(_WIN32) + long daylightBias = 0; + _get_dstbias(&daylightBias); + const int summerMinutes = summer ? static_cast(-daylightBias / 60) : 0; +#else + const int summerMinutes = summer ? 60 : 0; +#endif + return {static_cast(offset.count()) - summerMinutes, summer}; +} + +constexpr char MainExecutable[] = "/app0/eboot.bin"; +constexpr char ApplicationRoot[] = "/app0/"; +constexpr char MainExecutableVariable[] = "APS5_EBOOT"; +#if defined(_WIN32) +constexpr wchar_t MainExecutableVariableWide[] = L"APS5_EBOOT"; +#endif + +[[noreturn]] void UnconvertedExecutable(const std::string& message) { + NotImplemented_nid_no_patch(("sceSystemServiceLoadExec: " + message).c_str()); + std::abort(); +} + +std::string ConvertedExecutableName(const char* path) { + std::string_view relative(path); + if (!relative.starts_with(ApplicationRoot)) UnconvertedExecutable(std::string("executables outside /app0 (") + path + ")"); + relative.remove_prefix(std::size(ApplicationRoot) - 1); + std::string name; + while (true) { + const std::size_t separator = relative.find('/'); + const std::string_view component = relative.substr(0, separator); + if (component.empty() || component == "." || component == "..") UnconvertedExecutable(std::string("a path with an empty, . or .. component (") + path + ")"); + name += (name.empty() ? "" : ".") + std::string(component); + if (separator == std::string_view::npos) return name; + relative.remove_prefix(separator + 1); + } +} + +void RequireExecutableBeside(const std::filesystem::path& directory, const std::filesystem::path& target, const char* guestPath) { + if (target.lexically_normal().parent_path() != directory.lexically_normal()) { + UnconvertedExecutable(std::string("an executable outside /app0 (") + guestPath + ")"); + } +} + +#if defined(_WIN32) +std::wstring Utf16(const std::string& text) { + if (text.empty()) return {}; + const int size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.data(), static_cast(text.size()), nullptr, 0); + if (size <= 0) throw std::runtime_error("sceSystemServiceLoadExec: argument is not valid UTF-8"); + std::wstring wide(static_cast(size), L'\0'); + MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.data(), static_cast(text.size()), wide.data(), size); + return wide; +} + +std::wstring QuotedArgument(const std::wstring& argument) { + if (!argument.empty() && argument.find_first_of(L" \t\n\v\"") == std::wstring::npos) return argument; + std::wstring quoted = L"\""; + for (std::size_t index = 0;; ++index) { + std::size_t backslashes = 0; + while (index < argument.size() && argument[index] == L'\\') { + ++backslashes; + ++index; + } + if (index == argument.size()) { + quoted.append(backslashes * 2, L'\\'); + break; + } + quoted.append(argument[index] == L'"' ? backslashes * 2 + 1 : backslashes, L'\\'); + quoted.push_back(argument[index]); + } + quoted.push_back(L'"'); + return quoted; +} + +struct HostLaunch { + std::wstring executable; + std::wstring commandLine; +}; + +HostLaunch PrepareLaunch(const char* path, const char* const* arguments) { + std::wstring own(32768, L'\0'); + const DWORD length = GetModuleFileNameW(nullptr, own.data(), static_cast(own.size())); + if (length == 0 || length >= own.size()) { + throw std::runtime_error("sceSystemServiceLoadExec: GetModuleFileNameW failed with error " + std::to_string(GetLastError())); + } + own.resize(length); + HostLaunch launch; + const std::filesystem::path ownPath(own); + if (std::strcmp(path, MainExecutable) == 0) { + std::wstring main(32768, L'\0'); + const DWORD mainLength = GetEnvironmentVariableW(MainExecutableVariableWide, main.data(), static_cast(main.size())); + launch.executable = mainLength > 0 && mainLength < main.size() ? main.substr(0, mainLength) : own; + } else { + const auto target = ownPath.parent_path() / (Utf16(ConvertedExecutableName(path)) + ownPath.extension().wstring()); + RequireExecutableBeside(ownPath.parent_path(), target, path); + if (!std::filesystem::is_regular_file(target)) { + UnconvertedExecutable(std::string(path) + " without its converted executable; relink with --executable " + (path + std::size(ApplicationRoot) - 1)); + } + launch.executable = target.wstring(); + } + if (GetEnvironmentVariableW(MainExecutableVariableWide, nullptr, 0) == 0 && !SetEnvironmentVariableW(MainExecutableVariableWide, own.c_str())) { + throw std::runtime_error(std::string("sceSystemServiceLoadExec: cannot set ") + MainExecutableVariable); + } + launch.commandLine = L"\"" + launch.executable + L"\""; + for (std::size_t index = 0; arguments != nullptr && arguments[index] != nullptr; ++index) { + launch.commandLine += L" " + QuotedArgument(Utf16(arguments[index])); + } + return launch; +} + +void StartHostExecutable(HostLaunch& launch) { + STARTUPINFOW startup{}; + startup.cb = sizeof(startup); + PROCESS_INFORMATION process{}; + if (!CreateProcessW(launch.executable.c_str(), launch.commandLine.data(), nullptr, nullptr, TRUE, 0, nullptr, nullptr, &startup, &process)) { + throw std::runtime_error("sceSystemServiceLoadExec: CreateProcessW failed with error " + std::to_string(GetLastError())); + } + CloseHandle(process.hThread); + CloseHandle(process.hProcess); +} +#elif defined(__linux__) || defined(__APPLE__) +struct HostLaunch { + std::string executable; + std::vector 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); + launch.executable = main != nullptr && *main != '\0' ? main : own.string(); + } else { + const auto target = own.parent_path() / (ConvertedExecutableName(path) + own.extension().string()); + RequireExecutableBeside(own.parent_path(), target, path); + if (!std::filesystem::is_regular_file(target)) { + UnconvertedExecutable(std::string(path) + " without its converted executable; relink with --executable " + (path + std::size(ApplicationRoot) - 1)); + } + launch.executable = target.string(); + } + if (setenv(MainExecutableVariable, own.c_str(), 0) != 0) { + throw std::runtime_error(std::string("sceSystemServiceLoadExec: cannot set ") + MainExecutableVariable); + } + launch.arguments.push_back(launch.executable); + for (std::size_t index = 0; arguments != nullptr && arguments[index] != nullptr; ++index) launch.arguments.emplace_back(arguments[index]); + return launch; +} + +void StartHostExecutable(HostLaunch& launch) { + std::vector argv; + for (auto& argument : launch.arguments) argv.push_back(argument.data()); + argv.push_back(nullptr); + 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))); +} +#else +struct HostLaunch {}; + +HostLaunch PrepareLaunch(const char*, const char* const*) { + UnconvertedExecutable("starting an executable on this host"); +} + +void StartHostExecutable(HostLaunch&) {} +#endif + +} extern "C" { int APS5_VABI sceSystemServiceLoadExec(const char* path, const char* const* arguments) { if (!path || !*path) return SYSTEM_SERVICE_ERROR_PARAMETER; - if (std::strcmp(path, "exit") != 0) { - NotImplemented_nid_no_patch("sceSystemServiceLoadExec: executable replacement"); - } - (void)arguments; + if (std::strcmp(path, "exit") == 0) 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::exit(0); + std::fflush(nullptr); + StartHostExecutable(launch); + LibcTerminate_nid_no_patch(0); } int APS5_VABI sceSystemServiceDisableNoticeScreenSkipFlagAutoSet(void) { @@ -33,14 +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; } - constexpr float SdrReferenceWhiteNits = 100.0f; - luminance->max_full_frame_tone_map_luminance = SdrReferenceWhiteNits; - luminance->max_tone_map_luminance = SdrReferenceWhiteNits; - 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; } @@ -69,11 +303,12 @@ int APS5_VABI sceSystemServiceParamGetInt(int paramId, int* value) { return SYSTEM_SERVICE_ERROR_PARAMETER; } switch (paramId) { - case SYSTEM_SERVICE_PARAM_ID_LANG: *value = SYSTEM_SERVICE_PARAM_LANG_ENGLISH_US; break; + case SYSTEM_SERVICE_PARAM_ID_LANG: + case SYSTEM_SERVICE_PARAM_ID_400: *value = SystemServiceConsoleLanguage("sceSystemServiceParamGetInt"); break; case SYSTEM_SERVICE_PARAM_ID_DATE_FORMAT: *value = SYSTEM_SERVICE_PARAM_DATE_FORMAT_DDMMYYYY; break; case SYSTEM_SERVICE_PARAM_ID_TIME_FORMAT: *value = SYSTEM_SERVICE_PARAM_TIME_FORMAT_24HOUR; break; - case SYSTEM_SERVICE_PARAM_ID_TIME_ZONE: *value = 0; break; - case SYSTEM_SERVICE_PARAM_ID_SUMMERTIME: *value = 0; break; + case SYSTEM_SERVICE_PARAM_ID_TIME_ZONE: *value = HostTimeZone().minutes; break; + case SYSTEM_SERVICE_PARAM_ID_SUMMERTIME: *value = HostTimeZone().summer ? 1 : 0; break; case SYSTEM_SERVICE_PARAM_ID_GAME_PARENTAL_LEVEL: *value = SYSTEM_SERVICE_PARAM_GAME_PARENTAL_OFF; break; case SYSTEM_SERVICE_PARAM_ID_ENTER_BUTTON_ASSIGN: *value = SYSTEM_SERVICE_PARAM_ENTER_BUTTON_CROSS; break; default: *value = 0; break; @@ -82,16 +317,11 @@ int APS5_VABI sceSystemServiceParamGetInt(int paramId, int* value) { } 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)); return SYSTEM_SERVICE_OK; } @@ -116,9 +346,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() { @@ -140,16 +372,32 @@ int APS5_VABI sceSystemServiceLaunchWebBrowser(const char* uri, void* param) { return SCE_KERNEL_ERROR_EOPNOTSUPP; } +int APS5_VABI sceSystemServiceLaunchApp(const char* titleId, const char* const* arguments, const void* param) { + (void)titleId; + (void)arguments; + (void)param; + NotImplemented_nid_no_patch("sceSystemServiceLaunchApp: launching another application"); + return 0; +} + int APS5_VABI sceSystemServiceDisableMusicPlayer(void) { return SYSTEM_SERVICE_OK; } int APS5_VABI sceSystemServiceOpenChallengeActivity(void) { + return SYSTEM_SERVICE_OK; +} + +int APS5_VABI sceSystemServiceOpenTournamentOccurrence(void) { + 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 sceSystemServiceOpenTournamentOccurrence(void) { +int APS5_VABI sceSystemServiceKillLocalProcessForPsmKit(int) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -159,8 +407,7 @@ int APS5_VABI sceSystemServiceReenableMusicPlayer(void) { } int APS5_VABI sceSystemServiceShowControllerSettings(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return SYSTEM_SERVICE_OK; } int APS5_VABI sceSystemServiceGetAppIdOfRunningBigApp(void) { @@ -177,4 +424,411 @@ 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; +} + } 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..9ac7176e02 --- /dev/null +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -0,0 +1,2361 @@ +#include "prx/libc/include/General.hpp" + +namespace { + +constexpr int SystemServiceUnavailable = static_cast(0x80A10002); +constexpr int SystemServiceRejected = static_cast(0x80A10005); + +} + +extern "C" { + +int APS5_VABI sceAppMessagingClearEventFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppMessagingReceiveMsg() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppMessagingSendMsg() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppMessagingSendMsgToShellCore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppMessagingSendMsgToShellUI() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppMessagingSetEventFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppMessagingTryGetEventFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppMessagingTryReceiveMsg() { + 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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilBlockAppSuspend() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilBlockingGetEventForDaemon() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilContinueApp() { + 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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilForceKillLocalProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppCategory() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppCategoryType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppFocusedAppStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppIdListOfBigApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppIdOfRunningBigApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppLaunchedUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppStatusList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppStatusListForShellUIReboot() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppTitleId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetCdlgExec() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetCdlgType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetCoredumpState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetDbgExecutablePath() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetEventForDaemon() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetEventForShellUI() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetGpuCrashFullDumpAppStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetLocalProcessStatusList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetParentSocket() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetResultKillApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetResultLaunchAppByTitleId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetVideoFinalizeState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilIsActiveCdlg() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilIsAppLaunched() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilIsAppSuspended() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilIsCdlgActivated() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilIsCpuBudgetOfExtraAudioDevicesAvailable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilIsHostBoot() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilIsPs2Emu() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilIsShellUiFgAndGameBgCpuMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilIsSpeculativeLaunch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilKickCoredumpOnlyProcMem() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilKillLocalProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilLaunchApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilLoadExec() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilNotifyCoredumpRequestEnd() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilNotifyCoredumpRequestProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilNotifyVshReady() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilRaiseException() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilRaiseExceptionLocalProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilRegisterCdlgSharedMemoryName() { + 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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilResumeLocalProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilSetAppFocus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilSetCdlgExec() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilSetControllerFocus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilSetControllerFocusPermission() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilStartKillApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilStartLaunchAppByTitleId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilSuspendApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilSuspendBackgroundApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilSuspendLocalProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilTryBlockAppSuspend() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilUnblockAppSuspend() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilUnregisterCdlgSharedMemoryName() { + 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() { + return 0; +} + +int APS5_VABI sceShellCoreUtilActivateGetStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilActivateInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilActivateIsActivated() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilActivateRecordActivation() { + return 0; +} + +int APS5_VABI sceShellCoreUtilActivateStart() { + return 0; +} + +int APS5_VABI sceShellCoreUtilActivateStartAsync() { + return 0; +} + +int APS5_VABI sceShellCoreUtilActivateTerm() { + return 0; +} + +int APS5_VABI sceShellCoreUtilChangeRunLevel() { + 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() { + 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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDownloadHidConfigFileFromServer() { + 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() { + 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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilFormatExternalHdd() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilFormatHddForRestore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAppData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAppEnableTTS() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAppEnterButtonAssign() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAppLaunchTypeInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAppLaunchedParamInt() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAppLaunchedParamIntByBudgetType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAppLaunchedParamString() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAppLaunchedParamStringByBudgetType() { + 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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCheckerStringEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCloudClientStatus() { + 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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetDeviceIndexBehaviorWithTimeout() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetEffectiveTotalSizeOfUserPartition() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetFreeSizeOfAvContentsTmp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetFreeSizeOfUserPartition() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetFsckProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetGameLiveStreamingStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int 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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetHidConfigFileInfoString() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetHidConfigFileString() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetHidConfigName() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetHidConfigNum() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetIDUMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetImposeMenuFlagForPs2Emu() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetManifestFileStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetNeedSizeOfAppContent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetNetEvConfigFileInfoString() { + 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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPlatformPrivacyDefinitionEventData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetProgressOfFormatExternalHdd() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetProgressOfFsck() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPs4AppCategoryDefaultsListId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPs4AppCategoryForTitleId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPsStoreIconLayout() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPsStoreIconState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPsnAccountInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetRemotePlayStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetRunLevel() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetSharePlayStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetSocialScreenStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetSplashScreenState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetSupportSiteURL() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetSuspendConfirmationDialogFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetSystemBGState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetSystemBGWaveColor() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetSystemBGWaveState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetTelemetryToken() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetTitleWorkaroundFileInfoString() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetTitleWorkaroundFileString() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetUIStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetUmmStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetUserFocus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetUserIdOfMorpheusUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetVMStatsForTelemetry() { + 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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIncrementVersionNumberOfCameraCalibrationData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsAccessibilityZoomLocked() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsBgmCpuBudgetAcquired() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsBgmCpuBudgetAvailable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsBgmPlaying() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsExternalStorageAppMoveInProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsEyeToEyeDistanceAdjusted() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsGameLiveStreamingOnAir() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsIDUState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsImposeScreenOverlaid() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsInSystemSuspendBlackList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsInternalKratosUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsKilledOrSuspendedByLogout() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsNeededCrashReport() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsPowerSaveAlertRequested() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsRemotePlayCpuBudgetAcquired() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsScreenSaverOn() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int 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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsUsbMassStorageMounted() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilLaunchByUri() { + 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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilMountDownloadDataForShellUI() { + 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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNavigateToGoHome() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNavigateToLaunchedApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNotificationCancelForIDU() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNotificationRequestedForIDU() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNotifyBgmCoreTermination() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNotifyFsReadError() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNotifyImeStatusChanged() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNotifyPsnAccountInfoReceived() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNotifySystemSuspendTelemetryInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilPfAuthClientConsoleTokenClearCache() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilPostActivityForPsNow() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilPostErrorLog() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilPostLaunchConfirmResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilPostPsmEventToShellUI() { + 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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilReportUnexpectedFatalErrorToSystemTelemetry() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilRequestCameraCalibration() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilRequestEjectDevice() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilRequestRebootApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilRequestShutdown() { + 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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetBgmProhibition() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetDeviceIndexBehavior() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetGameLiveStreamingOnAirFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetGameLiveStreamingStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetGnmCompositorOnScreenProfilerFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetGpuLoadEmulationMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetGpuLoadEmulationModeByAppId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetIDUMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetImposeStatusFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetPsStoreIconLayout() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetPsStoreIconState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetRemotePlayStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetSharePlayStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetSkipUpdateCheck() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetSocialScreenStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetSplashScreenState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetSystemBGState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetSystemBGWaveColor() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetSystemBGWaveState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetUIStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetUserFocus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilShowCriticalErrorDialog() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilShowErrorDialog() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilShowErrorDialogWithFormatArgs() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilShowErrorDialogWithParam() { + 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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilStopPsNowGame() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilTalkKeyEventReceived() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilTestBusTransferSpeed() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilTickHeartBeat() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilTriggerPapcUpdate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilTurnOffScreenSaver() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilUnmountAppRight() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilUnmountDownloadDataForShellUI() { + 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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemModalDialogOpen() { + 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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcGetStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcIsActivated() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcSoft() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcSoftAbort() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcSoftGetStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcSoftInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcSoftIsActivated() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcSoftStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcSoftTerm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcTerm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateMpeg2Abort() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateMpeg2GetStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateMpeg2Init() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateMpeg2IsActivated() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateMpeg2Start() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateMpeg2Term() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceAddLocalProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceAddLocalProcessForJvm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceAddLocalProcessForPs2Emu() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceAppGetAppInstallStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeAcpClock() { + return 0; +} + +int APS5_VABI sceSystemServiceChangeClock() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeCpuClock() { + return 0; +} + +int APS5_VABI sceSystemServiceChangeCpuPstate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeGpuClock() { + return 0; +} + +int APS5_VABI sceSystemServiceChangeMemPstate() { + 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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetAppFocusedAppStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetAppId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetAppIdListOfBigApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetAppLaunchedUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetAppStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetAppTitleId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetAppType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetDbgExecutablePath() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetEventForDaemon() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetGpuLoadEmulationMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetLocalProcessStatusList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetMainAppTitleId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetPSButtonEvent() { + 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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetPlatformPrivacyDefinitionVersion() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetPlatformPrivacySetting() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetRecentLotusPushedButton() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetRemoteplayStatus() { + 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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetUIStatus() { + 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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceIsAppSuspended() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceIsBgmCpuBudgetAvailable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceIsBgmPlaying() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceIsEyeToEyeDistanceAdjusted() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceIsGameLiveStreamingOnAir() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceIsScreenSaverOn() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceIsShellUiFgAndGameBgCpuMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceKillLocalProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceKillLocalProcessForJvm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceKillLocalProcessForPs2Emu() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceLaunchEventDetails() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceLaunchStore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceLaunchTournamentList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceLaunchTournamentsTeamProfile() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceLaunchUdsApp() { + return SystemServiceRejected; +} + +int APS5_VABI sceSystemServiceLaunchWebApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceLoadExecVideoServiceWebApp() { + return SystemServiceUnavailable; +} + +int APS5_VABI sceSystemServiceNavigateToAnotherApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceNavigateToGoBack() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceNavigateToGoBackWithValue() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceNavigateToGoHome() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceNotifyBgmCoreTermination() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceRaiseExceptionLocalProcess() { + 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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceSaveVideoToken() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceSetControllerFocusPermission() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceSetGpuLoadEmulationMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceSetOutOfVrPlayAreaFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceSetOutOfVrPlayZoneWarning() { + return 0; +} + +int APS5_VABI sceSystemServiceSetPowerSaveLevel() { + 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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceShowEyeToEyeDistanceSetting() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceShowImposeMenuForPs2Emu() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceSuspendBackgroundApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceSuspendLocalProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceTelemetrySetData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceTelemetrySetData2() { + 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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageGetDeviceList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageGetdentsClose() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageGetdentsOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageGetdentsRead() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageIsExist() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageRegisterCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageRegisterCallbackForMapAvailable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageRegisterCallbackForMapUnavailable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageRequestMap() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageRequestMapWSB() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageRequestUnmap() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageTerm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageUnregisterCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageUnregisterCallbackForMapAvailable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageUnregisterCallbackForMapUnavailable() { + 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; +} + +int APS5_VABI sceSystemStateMgrIsBdDriveReady() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrIsGpuPerformanceNormal() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrIsShellUIShutdownInProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int 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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrReboot() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrRequestToKeepMainOnStandby() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrSendCecOneTouchPlayCommand() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrStartRebootTimer() { + 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() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceSystemService/SystemService.hpp b/core/libs/prx/libSceSystemService/SystemService.hpp index 109d1c269e..7b1664cc90 100644 --- a/core/libs/prx/libSceSystemService/SystemService.hpp +++ b/core/libs/prx/libSceSystemService/SystemService.hpp @@ -1,8 +1,14 @@ #ifndef CORE_LIBS_PRX_LIBSCESYSTEMSERVICE_SYSTEMSERVICE_HPP #define CORE_LIBS_PRX_LIBSCESYSTEMSERVICE_SYSTEMSERVICE_HPP +#include #include #include +#include +#include +#include +#include +#include constexpr int SYSTEM_SERVICE_OK = 0; constexpr int SYSTEM_SERVICE_ERROR_PARAMETER = -2136932349; @@ -17,14 +23,32 @@ 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_400 = 400; constexpr int SYSTEM_SERVICE_PARAM_ID_ENTER_BUTTON_ASSIGN = 1000; constexpr std::size_t SYSTEM_SERVICE_MAX_SYSTEM_NAME_LENGTH = 65; +constexpr int SYSTEM_SERVICE_PARAM_LANG_JAPANESE = 0; constexpr int SYSTEM_SERVICE_PARAM_LANG_ENGLISH_US = 1; +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..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceSystemTts/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/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..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceTextToSpeech/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/libSceTextToSpeech2/Export.cpp b/core/libs/prx/libSceTextToSpeech2/Export.cpp index 8e344ce258..09950022bc 100644 --- a/core/libs/prx/libSceTextToSpeech2/Export.cpp +++ b/core/libs/prx/libSceTextToSpeech2/Export.cpp @@ -12,8 +12,7 @@ constexpr std::int32_t SCE_KERNEL_ERROR_EOPNOTSUPP = static_cast(0 extern "C" { int APS5_VABI sceTextToSpeech2GetSystemStatus() { - NotImplemented_nid_no_patch(__func__); - return 0; + return SCE_KERNEL_ERROR_EOPNOTSUPP; } std::int32_t APS5_VABI sceTextToSpeech2Initialize(const void* param) { @@ -22,33 +21,27 @@ std::int32_t APS5_VABI sceTextToSpeech2Initialize(const void* param) { } int APS5_VABI sceTextToSpeech2Cancel() { - NotImplemented_nid_no_patch(__func__); - return 0; + return SCE_KERNEL_ERROR_EOPNOTSUPP; } int APS5_VABI sceTextToSpeech2Close() { - NotImplemented_nid_no_patch(__func__); - return 0; + return SCE_KERNEL_ERROR_EOPNOTSUPP; } int APS5_VABI sceTextToSpeech2GetSpeechStatus() { - NotImplemented_nid_no_patch(__func__); - return 0; + return SCE_KERNEL_ERROR_EOPNOTSUPP; } int APS5_VABI sceTextToSpeech2Open() { - NotImplemented_nid_no_patch(__func__); - return 0; + return SCE_KERNEL_ERROR_EOPNOTSUPP; } int APS5_VABI sceTextToSpeech2Speak() { - NotImplemented_nid_no_patch(__func__); - return 0; + return SCE_KERNEL_ERROR_EOPNOTSUPP; } int APS5_VABI sceTextToSpeech2Terminate() { - NotImplemented_nid_no_patch(__func__); - return 0; + 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 c9005a3cbf..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) @@ -20,7 +33,7 @@ if(BUILD_TESTING) target_include_directories(guest_ult_thread_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_ult_thread_tests PRIVATE libSceUlt libc) configure_windows_unwind(guest_ult_thread_tests) - foreach(testCase IN ITEMS outstanding validation tryjoin destroy) + foreach(testCase IN ITEMS outstanding validation tryjoin destroy underscoredcreate) add_test(NAME guest_ult_thread_${testCase} COMMAND guest_ult_thread_tests ${testCase}) set_tests_properties(guest_ult_thread_${testCase} PROPERTIES TIMEOUT 20) if(WIN32) @@ -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 d87bd7e5f2..10ba814b81 100644 --- a/core/libs/prx/libSceUlt/Export.cpp +++ b/core/libs/prx/libSceUlt/Export.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -13,15 +14,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; @@ -106,8 +111,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 +123,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 +136,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 +150,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,6 +181,9 @@ 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; } @@ -164,6 +196,31 @@ int APS5_VABI sceUltMutexOptParamInitialize(UltMutexOptParam* optParam, std::uin 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 sceUltMutexCreate(void* mutex, const char* name, void* waitingQueueResourcePool, const UltMutexOptParam* optParam, std::uint32_t buildVersion) { (void)name; (void)buildVersion; @@ -172,15 +229,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_BUSY; + } + state->_alive = false; + gMutexes.erase(it); + return ULT_OK; +} + int APS5_VABI sceUltMutexLock(void* mutex) { std::shared_ptr state; { @@ -191,7 +271,57 @@ 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) { + ++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,7 +335,111 @@ 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 || state->_owner != self) { + return ULT_ERROR_STATE; + } + --state->_lockCount; + if (state->_lockCount == 0) { + state->_owner = {}; + state->_available.notify_one(); + } + return ULT_OK; +} + +int APS5_VABI _sceUltConditionVariableCreate(void* condition, const char*, void* mutex, const void* optParam, std::uint32_t) { + 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 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; } @@ -226,6 +460,46 @@ 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_BUSY; + } + } + } + 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_BUSY; + } + } + } + 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_BUSY; + } + } + } + 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); @@ -239,6 +513,7 @@ std::uint64_t APS5_VABI sceUltQueueDataResourcePoolGetWorkAreaSize(std::uint32_t int APS5_VABI sceUltQueueDataResourcePoolCreate(void* pool, const char* name, std::uint32_t numData, std::uint64_t dataSize, std::uint32_t numQueueObject, void* waitingQueueResourcePool, void* workArea, const void* optParam, std::uint32_t buildVersion) { (void)name; + (void)optParam; (void)buildVersion; if (pool == nullptr) { return ULT_ERROR_NULL; @@ -246,7 +521,6 @@ int APS5_VABI sceUltQueueDataResourcePoolCreate(void* pool, const char* name, st if ((reinterpret_cast(pool) & 7u) != 0) return ULT_ERROR_ALIGNMENT; if (numData == 0 || numQueueObject == 0 || dataSize == 0 || dataSize > std::numeric_limits::max() / numData) return ULT_ERROR_RANGE; - if (optParam != nullptr) NotImplemented_nid_no_patch("sceUltQueueDataResourcePoolCreate: optParam"); std::lock_guard lock(gMutex); if (waitingQueueResourcePool != nullptr && gResourcePools.find(waitingQueueResourcePool) == gResourcePools.end()) { return ULT_ERROR_INVALID; @@ -283,13 +557,13 @@ int APS5_VABI sceUltQueueDataResourcePoolDestroy(void* pool) { int APS5_VABI sceUltQueueCreate(void* queue, const char* name, std::uint64_t dataSize, void* waitingQueueResourcePool, void* queueDataResourcePool, const void* optParam, std::uint32_t buildVersion) { (void)name; + (void)optParam; (void)buildVersion; if (queue == nullptr) { return ULT_ERROR_NULL; } if ((reinterpret_cast(queue) & 7u) != 0) return ULT_ERROR_ALIGNMENT; if (dataSize == 0) return ULT_ERROR_RANGE; - if (optParam != nullptr) NotImplemented_nid_no_patch("sceUltQueueCreate: optParam"); auto state = std::make_shared(); state->_dataSize = dataSize; std::lock_guard lock(gMutex); @@ -360,6 +634,7 @@ int APS5_VABI sceUltSemaphoreCreate(void* semaphore, const char* name, std::int3 } auto state = std::make_shared(); state->_resources = numInitialResource; + state->_waitingPool = waitingQueueResourcePool; std::lock_guard lock(gMutex); if (waitingQueueResourcePool != nullptr && gResourcePools.find(waitingQueueResourcePool) == gResourcePools.end()) { return ULT_ERROR_INVALID; @@ -491,6 +766,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); @@ -554,6 +830,34 @@ int APS5_VABI sceUltUlthreadTryJoin(void* ulthread, std::int32_t* status) { 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; @@ -572,6 +876,30 @@ int APS5_VABI sceUltUlthreadRuntimeDestroy(void* runtime) { 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 _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 _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 _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 _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 _sceUltUlthreadRuntimeOptParamInitialize(UltUlthreadRuntimeOptParam* optParam, std::uint32_t buildVersion) { return sceUltUlthreadRuntimeOptParamInitialize(optParam, buildVersion); } @@ -584,4 +912,130 @@ int APS5_VABI _sceUltUlthreadCreate(void* ulthread, const char* name, UltUlthrea return sceUltUlthreadCreate(ulthread, name, entry, arg, context, sizeContext, runtime, optParam, buildVersion); } +int APS5_VABI sceUltConditionVariableSignalAll() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetConditionVariableInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetMutexInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetQueueDataResourcePoolInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetQueueInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetReaderWriterLockInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetSemaphoreInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetUlthreadInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetUlthreadRuntimeInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetWaitingQueueResourcePoolInfo() { + 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; +} + +int APS5_VABI sceUltConditionVariableCreate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltConditionVariableOptParamInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + + +int APS5_VABI _sceUltConditionVariableOptParamInitialize() { + 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..efe53ec5a8 100644 --- a/core/libs/prx/libSceUlt/SceUltTypes.hpp +++ b/core/libs/prx/libSceUlt/SceUltTypes.hpp @@ -4,8 +4,11 @@ #include #include #include +#include #include +#include #include +#include #include #include "SceTypes.hpp" @@ -18,9 +21,41 @@ 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 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 +64,7 @@ struct UltSemaphoreState { std::int32_t _resources = 0; std::uint32_t _waiters = 0; bool _alive = true; + void* _waitingPool = nullptr; }; struct UltResourcePoolState { @@ -70,6 +106,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..7e1273394a --- /dev/null +++ b/core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp @@ -0,0 +1,200 @@ +#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; + } + auto& state = *it->second; + 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..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceUpdateService/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/libSceUrlConfigResolver/Export.cpp b/core/libs/prx/libSceUrlConfigResolver/Export.cpp new file mode 100644 index 0000000000..9d7da335dc --- /dev/null +++ b/core/libs/prx/libSceUrlConfigResolver/Export.cpp @@ -0,0 +1,31 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + + +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 new file mode 100644 index 0000000000..0704602439 --- /dev/null +++ b/core/libs/prx/libSceUsbStorage/Export.cpp @@ -0,0 +1,100 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +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, void* callback, void* arg) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceUsbStorageUnregisterCallback(int32_t eventType, void* callback) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceUsbStorageRegisterCallbackForMapAvailable(uint32_t device, const char* directory, void* callback, void* arg) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceUsbStorageUnregisterCallbackForMapAvailable(uint32_t device, const char* directory, void* callback) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceUsbStorageRegisterCallbackForMapUnavailable(uint32_t device, const char* directory, void* callback, void* arg) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceUsbStorageUnregisterCallbackForMapUnavailable(uint32_t device, const char* directory, void* callback) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceUsbStorageGetDeviceInfo(uint32_t device, void* 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; +} +int APS5_VABI sceUsbStorageIsExist(uint32_t device) { + 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, + const void* reserved, size_t reservedSize) { + 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 new file mode 100644 index 0000000000..78a410fe58 --- /dev/null +++ b/core/libs/prx/libSceUsbStorageDialog/Export.cpp @@ -0,0 +1,76 @@ +#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_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 sceUsbStorageDialogInitialize(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 sceUsbStorageDialogOpen(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_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(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 dialogResult = COMMON_DIALOG_RESULT_USER_CANCELED; + std::memcpy(result, &dialogResult, sizeof(dialogResult)); + return 0; +} + +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_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/CMakeLists.txt b/core/libs/prx/libSceUsbd/CMakeLists.txt index d7acf60da8..0ae07d11b5 100644 --- a/core/libs/prx/libSceUsbd/CMakeLists.txt +++ b/core/libs/prx/libSceUsbd/CMakeLists.txt @@ -1,5 +1,9 @@ add_library(libSceUsbd SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceUsbd PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceUsbd PRIVATE usb-1.0) +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_link_options(libSceUsbd PRIVATE LINKER:--exclude-libs,ALL) +endif() configure_windows_unwind(libSceUsbd) if(BUILD_TESTING) diff --git a/core/libs/prx/libSceUsbd/Export.cpp b/core/libs/prx/libSceUsbd/Export.cpp index c5c238411d..9518092966 100644 --- a/core/libs/prx/libSceUsbd/Export.cpp +++ b/core/libs/prx/libSceUsbd/Export.cpp @@ -3,172 +3,535 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include + #include +#include +#include +#include +#include +#include +#include #include #include +#include +#include namespace { constexpr std::int32_t SCE_USBD_ERROR_INVALID_ARG = static_cast(0x80240002); +constexpr std::int32_t SCE_USBD_ERROR_NOT_FOUND = static_cast(0x80240005); +constexpr std::int32_t SCE_USBD_ERROR_OTHER = static_cast(0x802400FF); +constexpr std::uint8_t TRANSFER_TYPE_CONTROL = 0; +constexpr std::uint8_t TRANSFER_TYPE_ISOCHRONOUS = 1; +constexpr std::uint8_t TRANSFER_TYPE_BULK = 2; +constexpr std::uint8_t TRANSFER_TYPE_INTERRUPT = 3; +constexpr std::int32_t CONTROL_SETUP_SIZE = 8; +constexpr std::uint8_t SUPPORTED_TRANSFER_FLAGS = LIBUSB_TRANSFER_SHORT_NOT_OK | LIBUSB_TRANSFER_ADD_ZERO_PACKET; struct UsbdTimeval { std::int64_t seconds; std::int64_t microseconds; }; +struct UsbdIsoPacketDescriptor { + std::uint32_t length; + std::uint32_t actualLength; + std::int32_t status; +}; + +using UsbdTransferCallback = void (APS5_VABI*)(struct UsbdTransfer*); + +struct UsbdTransfer { + void* deviceHandle; + std::uint8_t flags; + std::uint8_t endpoint; + std::uint8_t type; + std::uint32_t timeout; + std::int32_t status; + std::int32_t length; + std::int32_t actualLength; + UsbdTransferCallback callback; + void* userData; + std::uint8_t* buffer; + std::int32_t numIsoPackets; + UsbdIsoPacketDescriptor isoPacketDescriptors[1]; +}; + +static_assert(offsetof(UsbdTransfer, timeout) == 12 && offsetof(UsbdTransfer, callback) == 32 && offsetof(UsbdTransfer, numIsoPackets) == 56 && offsetof(UsbdTransfer, isoPacketDescriptors) == 60); +static_assert(sizeof(libusb_device_descriptor) == 18); +static_assert(sizeof(libusb_config_descriptor) == 40 && offsetof(libusb_config_descriptor, interface) == 16 && offsetof(libusb_config_descriptor, extra) == 24 && offsetof(libusb_config_descriptor, extra_length) == 32); +static_assert(sizeof(libusb_interface) == 16 && offsetof(libusb_interface, num_altsetting) == 8); +static_assert(sizeof(libusb_interface_descriptor) == 40 && offsetof(libusb_interface_descriptor, endpoint) == 16 && offsetof(libusb_interface_descriptor, extra) == 24 && offsetof(libusb_interface_descriptor, extra_length) == 32); +static_assert(sizeof(libusb_endpoint_descriptor) == 32 && offsetof(libusb_endpoint_descriptor, wMaxPacketSize) == 4 && offsetof(libusb_endpoint_descriptor, extra) == 16 && offsetof(libusb_endpoint_descriptor, extra_length) == 24); +static_assert(sizeof(libusb_iso_packet_descriptor) == sizeof(UsbdIsoPacketDescriptor)); + +struct UsbdState { + std::mutex mutex; + libusb_context* context = nullptr; + std::uint32_t initCount = 0; + std::unordered_map devices; + std::unordered_set lists; + std::unordered_set handles; + std::unordered_set configs; + std::unordered_map inFlight; +}; + +UsbdState& State() { + static auto* state = new UsbdState(); + return *state; +} + void* g_emptyDeviceList[1] = {nullptr}; +std::string AbsentDevice(const char* function) { + return std::string(function) + ": the device was not returned by sceUsbdGetDeviceList or was already released"; +} + +std::string UnopenedHandle(const char* function) { + return std::string(function) + ": the handle was not returned by sceUsbdOpen or was already closed"; +} + +std::int32_t ToGuestError(int result) { + if (result >= 0) return result; + if (result == LIBUSB_ERROR_OTHER) return SCE_USBD_ERROR_OTHER; + return static_cast(0x80240000u - static_cast(result)); +} + +libusb_device* KnownDevice(void* device, const char* function) { + auto& state = State(); + std::lock_guard lock(state.mutex); + auto* host = static_cast(device); + if (!state.devices.contains(host)) throw std::runtime_error(AbsentDevice(function)); + return host; +} + +libusb_device_handle* KnownHandle(void* deviceHandle, const char* function) { + auto& state = State(); + std::lock_guard lock(state.mutex); + auto* host = static_cast(deviceHandle); + if (!state.handles.contains(host)) throw std::runtime_error(UnopenedHandle(function)); + return host; +} + +void AddDeviceReference(UsbdState& state, libusb_device* device) { + ++state.devices[device]; +} + +void DropDeviceReference(UsbdState& state, libusb_device* device) { + const auto found = state.devices.find(device); + if (found == state.devices.end()) throw std::runtime_error(AbsentDevice("sceUsbdUnrefDevice")); + if (--found->second == 0) state.devices.erase(found); +} + +void LIBUSB_CALL CompleteTransfer(libusb_transfer* host) { + auto* guest = static_cast(host->user_data); + guest->status = host->status; + guest->actualLength = host->actual_length; + for (int packet = 0; packet < host->num_iso_packets; ++packet) { + guest->isoPacketDescriptors[packet].actualLength = host->iso_packet_desc[packet].actual_length; + guest->isoPacketDescriptors[packet].status = host->iso_packet_desc[packet].status; + } + { + auto& state = State(); + std::lock_guard lock(state.mutex); + state.inFlight.erase(guest); + } + libusb_free_transfer(host); + if (guest->callback != nullptr) guest->callback(guest); +} + } extern "C" { std::int32_t APS5_VABI sceUsbdInit() { + auto& state = State(); + std::lock_guard lock(state.mutex); + if (state.initCount == 0) { + libusb_context* context = nullptr; + if (libusb_init_context(&context, nullptr, 0) == 0) state.context = context; + } + ++state.initCount; return 0; } void APS5_VABI sceUsbdExit() { + auto& state = State(); + std::lock_guard lock(state.mutex); + if (state.initCount == 0 || --state.initCount != 0) return; + if (!state.inFlight.empty()) throw std::runtime_error(std::string(__func__) + ": transfers are still in flight"); + for (auto* handle : state.handles) libusb_close(handle); + for (auto* config : state.configs) libusb_free_config_descriptor(config); + for (auto* list : state.lists) libusb_free_device_list(list, 0); + for (const auto& [device, references] : state.devices) { + for (std::uint32_t reference = 0; reference < references; ++reference) libusb_unref_device(device); + } + state.handles.clear(); + state.configs.clear(); + state.lists.clear(); + state.devices.clear(); + if (state.context != nullptr) libusb_exit(state.context); + state.context = nullptr; } std::int64_t APS5_VABI sceUsbdGetDeviceList(void*** list) { if (list == nullptr) return SCE_USBD_ERROR_INVALID_ARG; - *list = g_emptyDeviceList; - return 0; + auto& state = State(); + std::lock_guard lock(state.mutex); + if (state.context == nullptr) { + *list = g_emptyDeviceList; + return 0; + } + libusb_device** devices = nullptr; + const ssize_t count = libusb_get_device_list(state.context, &devices); + if (count < 0) return ToGuestError(static_cast(count)); + for (ssize_t index = 0; index < count; ++index) AddDeviceReference(state, devices[index]); + state.lists.insert(devices); + *list = reinterpret_cast(devices); + return count; } void APS5_VABI sceUsbdFreeDeviceList(void** list, std::int32_t unrefDevices) { - (void)unrefDevices; - if (list != nullptr && list != g_emptyDeviceList) throw std::runtime_error(std::string(__func__) + ": list was not returned by sceUsbdGetDeviceList"); + if (list == nullptr || list == g_emptyDeviceList) return; + auto& state = State(); + std::lock_guard lock(state.mutex); + auto* devices = reinterpret_cast(list); + if (state.lists.erase(devices) == 0) throw std::runtime_error(std::string(__func__) + ": list was not returned by sceUsbdGetDeviceList"); + if (unrefDevices != 0) { + for (auto** device = devices; *device != nullptr; ++device) DropDeviceReference(state, *device); + } + libusb_free_device_list(devices, unrefDevices); } std::int32_t APS5_VABI sceUsbdHandleEventsTimeout(const UsbdTimeval* timeout) { if (timeout == nullptr || timeout->seconds < 0 || timeout->microseconds < 0 || timeout->microseconds >= 1000000) return SCE_USBD_ERROR_INVALID_ARG; - std::this_thread::sleep_for(std::chrono::seconds(timeout->seconds) + std::chrono::microseconds(timeout->microseconds)); + libusb_context* context; + { + auto& state = State(); + std::lock_guard lock(state.mutex); + context = state.context; + } + if (context == nullptr) { + std::this_thread::sleep_for(std::chrono::seconds(timeout->seconds) + std::chrono::microseconds(timeout->microseconds)); + return 0; + } + timeval host{}; + host.tv_sec = static_cast(timeout->seconds); + host.tv_usec = static_cast(timeout->microseconds); + return ToGuestError(libusb_handle_events_timeout_completed(context, &host, nullptr)); +} + +UsbdTransfer* APS5_VABI sceUsbdAllocTransfer(std::int32_t isoPackets) { + if (isoPackets < 0) return nullptr; + const std::size_t size = offsetof(UsbdTransfer, isoPacketDescriptors) + sizeof(UsbdIsoPacketDescriptor) * static_cast(isoPackets); + auto* transfer = static_cast(std::calloc(1, size)); + if (transfer == nullptr) return nullptr; + transfer->numIsoPackets = isoPackets; + return transfer; +} + +void APS5_VABI sceUsbdFreeTransfer(UsbdTransfer* transfer) { + if (transfer == nullptr) return; + { + auto& state = State(); + std::lock_guard lock(state.mutex); + if (state.inFlight.contains(transfer)) throw std::runtime_error(std::string(__func__) + ": transfer is still in flight"); + } + std::free(transfer); +} + +void APS5_VABI sceUsbdFillInterruptTransfer(UsbdTransfer* transfer, void* deviceHandle, std::uint8_t endpoint, std::uint8_t* buffer, std::int32_t length, UsbdTransferCallback callback, void* userData, std::uint32_t timeout) { + if (transfer == nullptr) throw std::runtime_error(std::string(__func__) + ": null transfer"); + transfer->deviceHandle = deviceHandle; + transfer->endpoint = endpoint; + transfer->type = TRANSFER_TYPE_INTERRUPT; + transfer->timeout = timeout; + transfer->buffer = buffer; + transfer->length = length; + transfer->userData = userData; + transfer->callback = callback; +} + +void APS5_VABI sceUsbdFillBulkTransfer(UsbdTransfer* transfer, void* deviceHandle, std::uint8_t endpoint, std::uint8_t* buffer, std::int32_t length, UsbdTransferCallback callback, void* userData, std::uint32_t timeout) { + if (transfer == nullptr) throw std::runtime_error(std::string(__func__) + ": null transfer"); + transfer->deviceHandle = deviceHandle; + transfer->endpoint = endpoint; + transfer->type = TRANSFER_TYPE_BULK; + transfer->timeout = timeout; + transfer->buffer = buffer; + transfer->length = length; + transfer->userData = userData; + transfer->callback = callback; +} + +void APS5_VABI sceUsbdFillIsoTransfer(UsbdTransfer* transfer, void* deviceHandle, std::uint8_t endpoint, std::uint8_t* buffer, std::int32_t length, std::int32_t numIsoPackets, UsbdTransferCallback callback, void* userData, std::uint32_t timeout) { + if (transfer == nullptr) throw std::runtime_error(std::string(__func__) + ": null transfer"); + transfer->deviceHandle = deviceHandle; + transfer->endpoint = endpoint; + transfer->type = TRANSFER_TYPE_ISOCHRONOUS; + transfer->timeout = timeout; + transfer->buffer = buffer; + transfer->length = length; + transfer->numIsoPackets = numIsoPackets; + transfer->userData = userData; + transfer->callback = callback; +} + +void APS5_VABI sceUsbdFillControlSetup(std::uint8_t* buffer, std::uint8_t requestType, std::uint8_t request, std::uint16_t value, std::uint16_t index, std::uint16_t length) { + if (buffer == nullptr) throw std::runtime_error(std::string(__func__) + ": null buffer"); + buffer[0] = requestType; + buffer[1] = request; + buffer[2] = static_cast(value); + buffer[3] = static_cast(value >> 8); + buffer[4] = static_cast(index); + buffer[5] = static_cast(index >> 8); + buffer[6] = static_cast(length); + buffer[7] = static_cast(length >> 8); +} + +void APS5_VABI sceUsbdFillControlTransfer(UsbdTransfer* transfer, void* deviceHandle, std::uint8_t* buffer, UsbdTransferCallback callback, void* userData, std::uint32_t timeout) { + if (transfer == nullptr) throw std::runtime_error(std::string(__func__) + ": null transfer"); + transfer->deviceHandle = deviceHandle; + transfer->endpoint = 0; + transfer->type = TRANSFER_TYPE_CONTROL; + transfer->timeout = timeout; + transfer->buffer = buffer; + transfer->length = buffer != nullptr ? CONTROL_SETUP_SIZE + (buffer[6] | buffer[7] << 8) : 0; + transfer->userData = userData; + transfer->callback = callback; +} + +std::uint8_t* APS5_VABI sceUsbdControlTransferGetData(UsbdTransfer* transfer) { + if (transfer == nullptr) throw std::runtime_error(std::string(__func__) + ": null transfer"); + return transfer->buffer != nullptr ? transfer->buffer + CONTROL_SETUP_SIZE : nullptr; +} + +std::uint8_t* APS5_VABI sceUsbdControlTransferGetSetup(UsbdTransfer* transfer) { + if (transfer == nullptr) throw std::runtime_error(std::string(__func__) + ": null transfer"); + return transfer->buffer; +} + +void APS5_VABI sceUsbdSetIsoPacketLengths(UsbdTransfer* transfer, std::uint32_t length) { + if (transfer == nullptr) throw std::runtime_error(std::string(__func__) + ": null transfer"); + for (std::int32_t packet = 0; packet < transfer->numIsoPackets; ++packet) transfer->isoPacketDescriptors[packet].length = length; +} + +std::uint8_t* APS5_VABI sceUsbdGetIsoPacketBuffer(UsbdTransfer* transfer, std::uint32_t packet) { + if (transfer == nullptr) throw std::runtime_error(std::string(__func__) + ": null transfer"); + if (transfer->numIsoPackets < 0 || packet >= static_cast(transfer->numIsoPackets) || transfer->buffer == nullptr) return nullptr; + std::uint8_t* pointer = transfer->buffer; + for (std::uint32_t index = 0; index < packet; ++index) pointer += transfer->isoPacketDescriptors[index].length; + return pointer; +} + +std::int32_t APS5_VABI sceUsbdBulkTransfer(void* deviceHandle, std::uint8_t endpoint, std::uint8_t* data, std::int32_t length, std::int32_t* transferred, std::uint32_t timeout) { + if (deviceHandle == nullptr || (length != 0 && data == nullptr)) return SCE_USBD_ERROR_INVALID_ARG; + if (transferred == nullptr) throw std::runtime_error(std::string(__func__) + ": null transferred output"); + *transferred = 0; + return ToGuestError(libusb_bulk_transfer(KnownHandle(deviceHandle, __func__), endpoint, data, length, transferred, timeout)); +} + +std::int32_t APS5_VABI sceUsbdInterruptTransfer(void* deviceHandle, std::uint8_t endpoint, std::uint8_t* data, std::int32_t length, std::int32_t* transferred, std::uint32_t timeout) { + if (deviceHandle == nullptr || (length != 0 && data == nullptr)) return SCE_USBD_ERROR_INVALID_ARG; + if (transferred == nullptr) throw std::runtime_error(std::string(__func__) + ": null transferred output"); + *transferred = 0; + return ToGuestError(libusb_interrupt_transfer(KnownHandle(deviceHandle, __func__), endpoint, data, length, transferred, timeout)); +} + +std::int32_t APS5_VABI sceUsbdCancelTransfer(UsbdTransfer* transfer) { + if (transfer == nullptr) throw std::runtime_error(std::string(__func__) + ": null transfer"); + auto& state = State(); + std::lock_guard lock(state.mutex); + const auto found = state.inFlight.find(transfer); + if (found == state.inFlight.end()) return SCE_USBD_ERROR_NOT_FOUND; + return ToGuestError(libusb_cancel_transfer(found->second)); +} + +std::int32_t APS5_VABI sceUsbdEventHandlingOk() { + return 1; +} + +void APS5_VABI sceUsbdFreeConfigDescriptor(void* config) { + if (config == nullptr) return; + auto& state = State(); + std::lock_guard lock(state.mutex); + auto* host = static_cast(config); + if (state.configs.erase(host) == 0) throw std::runtime_error(std::string(__func__) + ": descriptor was not returned by this library"); + libusb_free_config_descriptor(host); +} + +std::int32_t APS5_VABI sceUsbdAttachKernelDriver(void* deviceHandle, std::int32_t interfaceNumber) { + (void)interfaceNumber; + if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + KnownHandle(deviceHandle, __func__); return 0; } -int APS5_VABI sceUsbdAllocTransfer() { - NotImplemented_nid_no_patch(__func__); +std::int32_t APS5_VABI sceUsbdCheckConnected(void* deviceHandle) { + if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + int configuration = 0; + const int result = libusb_get_configuration(KnownHandle(deviceHandle, __func__), &configuration); + return result == LIBUSB_ERROR_NO_DEVICE ? ToGuestError(LIBUSB_ERROR_NO_DEVICE) : 0; +} + +std::int32_t APS5_VABI sceUsbdClaimInterface(void* deviceHandle, std::int32_t interfaceNumber) { + if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + return ToGuestError(libusb_claim_interface(KnownHandle(deviceHandle, __func__), interfaceNumber)); +} + +void APS5_VABI sceUsbdClose(void* deviceHandle) { + if (deviceHandle == nullptr) return; + auto& state = State(); + std::lock_guard lock(state.mutex); + auto* host = static_cast(deviceHandle); + if (state.handles.erase(host) == 0) throw std::runtime_error(UnopenedHandle(__func__)); + libusb_close(host); +} + +std::int32_t APS5_VABI sceUsbdControlTransfer(void* deviceHandle, std::uint8_t requestType, std::uint8_t request, std::uint16_t value, std::uint16_t index, std::uint8_t* data, std::uint16_t length, std::uint32_t timeout) { + if (deviceHandle == nullptr || (length != 0 && data == nullptr)) return SCE_USBD_ERROR_INVALID_ARG; + return ToGuestError(libusb_control_transfer(KnownHandle(deviceHandle, __func__), requestType, request, value, index, data, length, timeout)); +} + +std::int32_t APS5_VABI sceUsbdGetActiveConfigDescriptor(void* device, void** config) { + if (device == nullptr) throw std::runtime_error(std::string(__func__) + ": null device"); + if (config == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + libusb_config_descriptor* host = nullptr; + const int result = libusb_get_active_config_descriptor(KnownDevice(device, __func__), &host); + *config = nullptr; + if (result != 0) return ToGuestError(result); + auto& state = State(); + std::lock_guard lock(state.mutex); + state.configs.insert(host); + *config = host; return 0; } -int APS5_VABI sceUsbdAttachKernelDriver() { - NotImplemented_nid_no_patch(__func__); +std::uint8_t APS5_VABI sceUsbdGetBusNumber(void* device) { + if (device == nullptr) return 0; + return libusb_get_bus_number(KnownDevice(device, __func__)); +} + +std::int32_t APS5_VABI sceUsbdGetConfigDescriptor(void* device, std::uint8_t configIndex, void** config) { + if (device == nullptr || config == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + libusb_config_descriptor* host = nullptr; + const int result = libusb_get_config_descriptor(KnownDevice(device, __func__), configIndex, &host); + *config = nullptr; + if (result != 0) return ToGuestError(result); + auto& state = State(); + std::lock_guard lock(state.mutex); + state.configs.insert(host); + *config = host; return 0; } -int APS5_VABI sceUsbdCancelTransfer() { - NotImplemented_nid_no_patch(__func__); +std::uint8_t APS5_VABI sceUsbdGetDeviceAddress(void* device) { + if (device == nullptr) return 0; + return libusb_get_device_address(KnownDevice(device, __func__)); +} + +std::int32_t APS5_VABI sceUsbdGetDeviceDescriptor(void* device, void* descriptor) { + if (device == nullptr || descriptor == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + return ToGuestError(libusb_get_device_descriptor(KnownDevice(device, __func__), static_cast(descriptor))); +} + +std::int32_t APS5_VABI sceUsbdGetStringDescriptor(void* deviceHandle, std::uint8_t descriptorIndex, std::uint16_t languageId, std::uint8_t* data, std::int32_t length) { + if (deviceHandle == nullptr || data == nullptr || length < 1) return SCE_USBD_ERROR_INVALID_ARG; + auto* handle = KnownHandle(deviceHandle, __func__); + if (length > 65535) length = 65535; + data[0] = 0; + return ToGuestError(libusb_get_string_descriptor(handle, descriptorIndex, languageId, data, length)); +} + +std::int32_t APS5_VABI sceUsbdKernelDriverActive(void* deviceHandle, std::int32_t interfaceNumber) { + if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + return ToGuestError(libusb_kernel_driver_active(KnownHandle(deviceHandle, __func__), interfaceNumber)); +} + +std::int32_t APS5_VABI sceUsbdOpen(void* device, void** deviceHandle) { + if (device == nullptr) throw std::runtime_error(std::string(__func__) + ": null device"); + if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + *deviceHandle = nullptr; + libusb_device_handle* host = nullptr; + const int result = libusb_open(KnownDevice(device, __func__), &host); + if (result != 0) return ToGuestError(result); + auto& state = State(); + std::lock_guard lock(state.mutex); + state.handles.insert(host); + *deviceHandle = host; return 0; } -int APS5_VABI sceUsbdCheckConnected() { - NotImplemented_nid_no_patch(__func__); - return 0; +void* APS5_VABI sceUsbdRefDevice(void* device) { + if (device == nullptr) return nullptr; + auto& state = State(); + std::lock_guard lock(state.mutex); + auto* host = static_cast(device); + if (!state.devices.contains(host)) throw std::runtime_error(AbsentDevice(__func__)); + AddDeviceReference(state, host); + return libusb_ref_device(host); } -int APS5_VABI sceUsbdClaimInterface() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdReleaseInterface(void* deviceHandle, std::int32_t interfaceNumber) { + if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + return ToGuestError(libusb_release_interface(KnownHandle(deviceHandle, __func__), interfaceNumber)); } -int APS5_VABI sceUsbdClose() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdResetDevice(void* deviceHandle) { + if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + return ToGuestError(libusb_reset_device(KnownHandle(deviceHandle, __func__))); } -int APS5_VABI sceUsbdControlTransfer() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdSetConfiguration(void* deviceHandle, std::int32_t configuration) { + if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + return ToGuestError(libusb_set_configuration(KnownHandle(deviceHandle, __func__), configuration)); } -int APS5_VABI sceUsbdEventHandlingOk() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdSubmitTransfer(UsbdTransfer* transfer) { + if (transfer == nullptr || transfer->deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + auto* handle = KnownHandle(transfer->deviceHandle, __func__); + if ((transfer->flags & ~SUPPORTED_TRANSFER_FLAGS) != 0) throw std::runtime_error(std::string(__func__) + ": transfer flags " + std::to_string(transfer->flags) + " are not supported"); + if (transfer->numIsoPackets < 0) return SCE_USBD_ERROR_INVALID_ARG; + auto* host = libusb_alloc_transfer(transfer->numIsoPackets); + if (host == nullptr) return ToGuestError(LIBUSB_ERROR_NO_MEM); + host->dev_handle = handle; + host->flags = transfer->flags; + host->endpoint = transfer->endpoint; + host->type = transfer->type; + host->timeout = transfer->timeout; + host->length = transfer->length; + host->buffer = transfer->buffer; + host->user_data = transfer; + host->callback = CompleteTransfer; + host->num_iso_packets = transfer->numIsoPackets; + for (int packet = 0; packet < transfer->numIsoPackets; ++packet) host->iso_packet_desc[packet].length = transfer->isoPacketDescriptors[packet].length; + auto& state = State(); + { + std::lock_guard lock(state.mutex); + if (!state.inFlight.emplace(transfer, host).second) { + libusb_free_transfer(host); + return ToGuestError(LIBUSB_ERROR_BUSY); + } + } + const int result = libusb_submit_transfer(host); + if (result != 0) { + { + std::lock_guard lock(state.mutex); + state.inFlight.erase(transfer); + } + libusb_free_transfer(host); + } + return ToGuestError(result); } -int APS5_VABI sceUsbdFillInterruptTransfer() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUsbdFreeConfigDescriptor() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUsbdFreeTransfer() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUsbdGetActiveConfigDescriptor() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUsbdGetBusNumber() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUsbdGetConfigDescriptor() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUsbdGetDeviceAddress() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUsbdGetDeviceDescriptor() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUsbdGetStringDescriptor() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUsbdKernelDriverActive() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUsbdOpen() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUsbdRefDevice() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUsbdReleaseInterface() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUsbdResetDevice() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUsbdSetConfiguration() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUsbdSubmitTransfer() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUsbdUnrefDevice() { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI sceUsbdUnrefDevice(void* device) { + if (device == nullptr) return; + auto& state = State(); + std::lock_guard lock(state.mutex); + auto* host = static_cast(device); + DropDeviceReference(state, host); + libusb_unref_device(host); } } diff --git a/core/libs/prx/libSceUserService/CMakeLists.txt b/core/libs/prx/libSceUserService/CMakeLists.txt index 444c378c23..6d606a67e8 100644 --- a/core/libs/prx/libSceUserService/CMakeLists.txt +++ b/core/libs/prx/libSceUserService/CMakeLists.txt @@ -6,9 +6,32 @@ 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 "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING) + add_test_executable(guest_user_service_user_name_tests ${LIBS_INCLUDE_DIR}/tests/GuestUserServiceUserName.cpp) + target_include_directories(guest_user_service_user_name_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_user_service_user_name_tests PRIVATE libSceUserService) + add_test(NAME guest_user_service_user_name COMMAND guest_user_service_user_name_tests) + if(WIN32) + set_tests_properties(guest_user_service_user_name PROPERTIES ENVIRONMENT_MODIFICATION + "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 2a54f7c9f2..7a4c11d33e 100644 --- a/core/libs/prx/libSceUserService/Export.cpp +++ b/core/libs/prx/libSceUserService/Export.cpp @@ -6,70 +6,76 @@ #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; + } + if (setting == nullptr) { + return USER_SERVICE_ERROR_INVALID_ARGUMENT; + } + *setting = value; + return USER_SERVICE_OK; +} + extern "C" { int APS5_VABI sceUserServiceGetAccessibilityChatTranscription(int user_id, int32_t* chat_transcription) { - if (chat_transcription == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { - return USER_SERVICE_ERROR_INVALID_ARGUMENT; - } - *chat_transcription = 0; - return USER_SERVICE_OK; + return ReportAccessibilitySetting(user_id, chat_transcription, 0); +} + +int APS5_VABI sceUserServiceGetAccessibilityKeyremapEnable() { + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceUserServiceGetAccessibilityPressAndHoldDelay(int user_id, int32_t* press_and_hold_delay) { - if (press_and_hold_delay == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { - return USER_SERVICE_ERROR_INVALID_ARGUMENT; - } - *press_and_hold_delay = 0; - return USER_SERVICE_OK; + return ReportAccessibilitySetting(user_id, press_and_hold_delay, 0); } int APS5_VABI sceUserServiceGetAccessibilityTriggerEffect(int user_id, int32_t* trigger_effect) { - if (trigger_effect == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { - return USER_SERVICE_ERROR_INVALID_ARGUMENT; - } - *trigger_effect = 0; - return USER_SERVICE_OK; + return ReportAccessibilitySetting(user_id, trigger_effect, 1); } int APS5_VABI sceUserServiceGetAccessibilityVibration(int user_id, int32_t* vibration) { - if (vibration == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { - return USER_SERVICE_ERROR_INVALID_ARGUMENT; - } - *vibration = 0; - return USER_SERVICE_OK; + return ReportAccessibilitySetting(user_id, vibration, 1); +} + +int APS5_VABI sceUserServiceGetAccessibilityZoom() { + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceUserServiceGetAccessibilityZoomEnabled(int user_id, int32_t* zoom_enabled) { - if (zoom_enabled == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { - return USER_SERVICE_ERROR_INVALID_ARGUMENT; - } - *zoom_enabled = 0; - return USER_SERVICE_OK; + return ReportAccessibilitySetting(user_id, zoom_enabled, 0); } int APS5_VABI sceUserServiceGetAccessibilityZoomFollowFocus(int user_id, int32_t* zoom_follow_focus) { - if (zoom_follow_focus == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { - return USER_SERVICE_ERROR_INVALID_ARGUMENT; - } - *zoom_follow_focus = 0; - return USER_SERVICE_OK; + return ReportAccessibilitySetting(user_id, zoom_follow_focus, 1); } int APS5_VABI sceUserServiceGetAgeLevel(int user_id, uint32_t* age_level) { - if (age_level == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { + if (user_id != USER_SERVICE_INITIAL_USER_ID) { + return USER_SERVICE_ERROR_NOT_LOGGED_IN; + } + if (age_level == nullptr) { return USER_SERVICE_ERROR_INVALID_ARGUMENT; } - *age_level = 0; + constexpr std::uint32_t Unrestricted = 0xFFFFFFFFu; + *age_level = Unrestricted; 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; } @@ -106,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; } @@ -117,10 +126,14 @@ int APS5_VABI sceUserServiceGetLoginUserIdList(UserServiceLoginUserIdList* user_ } int APS5_VABI sceUserServiceGetUserName(int user_id, char* name, size_t size) { - if (name == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { + if (name == nullptr) { return USER_SERVICE_ERROR_INVALID_ARGUMENT; } + if (user_id != USER_SERVICE_INITIAL_USER_ID) { + return USER_SERVICE_ERROR_UNKNOWN_USER; + } if (size < USER_SERVICE_MAX_USER_NAME_LENGTH + 1) { + std::memset(name, 0, size); return USER_SERVICE_ERROR_BUFFER_TOO_SHORT; } std::strncpy(name, USER_SERVICE_INITIAL_USER_NAME, size); @@ -135,16 +148,40 @@ 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 void* params) { + if (params != nullptr) { + int priority = 0; + std::memcpy(&priority, params, sizeof(priority)); + if (!ValidThreadPriority(priority)) { + return USER_SERVICE_ERROR_INVALID_ARGUMENT; + } + } + return MarkInitialized(); +} + +int APS5_VABI sceUserServiceInitialize2(int priority) { + 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 dcc140dad8..1e0f926aa3 100644 --- a/core/libs/prx/libSceUserService/UserService.hpp +++ b/core/libs/prx/libSceUserService/UserService.hpp @@ -5,11 +5,17 @@ #include 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; constexpr const char* USER_SERVICE_INITIAL_USER_NAME = "Player"; 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..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceVdecCore.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/libSceVdecCore/Export.cpp b/core/libs/prx/libSceVdecCore/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceVdecCore/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/libSceVdecSavc/Export.cpp b/core/libs/prx/libSceVdecSavc/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceVdecSavc/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.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..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceVdecSavc2/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.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..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceVdecShevc/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/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..cab091bb32 100644 --- a/core/libs/prx/libSceVdecsw/Export.cpp +++ b/core/libs/prx/libSceVdecsw/Export.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -116,6 +117,13 @@ struct Videodec2AvcPictureInfo { std::uint32_t picHeightInLumaSamples; }; +struct Videodec2Crop { + std::uint32_t left; + std::uint32_t right; + std::uint32_t top; + std::uint32_t bottom; +}; + struct DecoderMemoryInfo { std::uint64_t thisSize; std::uint64_t cpuMemorySize; @@ -141,6 +149,7 @@ int APS5_VABI sceVideodec2Decode_nid_postfix(std::uint64_t handle, const InputDa int APS5_VABI sceVideodec2Flush_nid_postfix(std::uint64_t handle, Videodec2FrameBuffer* frame, Videodec2OutputInfo* output); int APS5_VABI sceVideodec2Reset_nid_postfix(std::uint64_t handle); int APS5_VABI sceVideodec2GetAvcPictureInfo_nid_postfix(const Videodec2OutputInfo* output, Videodec2AvcPictureInfo* first, Videodec2AvcPictureInfo* second); +void Videodec2PictureCrop_nid_no_patch(std::uint64_t handle, const void* frameBuffer, Videodec2Crop* crop); } namespace { @@ -183,10 +192,12 @@ std::shared_ptr findDecoder(std::uint64_t handle) { return found->second; } -AvcPictureInfo pictureInfo(const Videodec2OutputInfo& output) { +AvcPictureInfo pictureInfo(std::uint64_t handle, 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"); + Videodec2Crop crop{}; + Videodec2PictureCrop_nid_no_patch(handle, output.frameBuffer, &crop); const auto widthInMbs = (output.frameWidth + 15u) / 16u; const auto heightInMbs = (output.frameHeight + 15u) / 16u; AvcPictureInfo info{}; @@ -201,9 +212,11 @@ AvcPictureInfo pictureInfo(const Videodec2OutputInfo& output) { 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 = crop.left != 0 || crop.right != 0 || crop.top != 0 || crop.bottom != 0; + info.frameCropLeftOffset = crop.left / 2u; + info.frameCropRightOffset = crop.right / 2u; + info.frameCropTopOffset = crop.top / 2u; + info.frameCropBottomOffset = crop.bottom / 2u; return info; } @@ -226,7 +239,7 @@ void advance(std::uint64_t handle, Decoder& decoder) { return; } if (output.isValid) { - const auto info = pictureInfo(output); + const auto info = pictureInfo(handle, output); decoder.pictures.push_back({std::move(pixels), output, info}); } } @@ -299,6 +312,14 @@ int APS5_VABI sceVdecswTrySyncDecodeInput(std::uint64_t handle, InputResult* res return decoder->inputs.empty() ? VdecswErrorInputQueueEmpty : VdecswErrorDecodePending; } +int APS5_VABI sceVdecswSyncDecodeInput(std::uint64_t handle, InputResult* result) { + for (;;) { + const auto status = sceVdecswTrySyncDecodeInput(handle, result); + if (status != VdecswErrorDecodePending) return status; + std::this_thread::yield(); + } +} + int APS5_VABI sceVdecswSetDecodeOutput(std::uint64_t handle, const FrameBuffer* frame) { if (frame == nullptr || frame->thisSize < sizeof(FrameBuffer) || frame->frameBuffer == nullptr) throw std::runtime_error("Vdecsw: invalid decode output"); const auto decoder = findDecoder(handle); @@ -344,6 +365,22 @@ int APS5_VABI sceVdecswTrySyncDecodeOutput(std::uint64_t handle, OutputInfo* out return 0; } +int APS5_VABI sceVdecswSyncDecodeOutput(std::uint64_t handle, OutputInfo* output) { + for (;;) { + const auto status = sceVdecswTrySyncDecodeOutput(handle, output); + if (status != VdecswErrorOutputPending) return status; + std::this_thread::yield(); + } +} + +int APS5_VABI 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"); @@ -365,6 +402,20 @@ int APS5_VABI sceVdecswGetAvcPictureInfo(const OutputInfo* output, AvcPictureInf throw std::runtime_error("Vdecsw: picture info requested for a frame buffer no decoder returned"); } +int APS5_VABI sceVdecswGetHevcPictureInfo(const OutputInfo* output, void* picture) { + (void)output; + (void)picture; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecswGetVp9PictureInfo(const OutputInfo* output, void* picture) { + (void)output; + (void)picture; + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceVdecswFinalizeDecodeSequence(std::uint64_t handle) { const auto decoder = findDecoder(handle); std::lock_guard lock(decoder->mutex); diff --git a/core/libs/prx/libSceVdecwrap/Export.cpp b/core/libs/prx/libSceVdecwrap/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceVdecwrap/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/libSceVenc/Export.cpp b/core/libs/prx/libSceVenc/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceVenc/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/libSceVideoArbitration/Export.cpp b/core/libs/prx/libSceVideoArbitration/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceVideoArbitration/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/libSceVideoCoreInterface/Export.cpp b/core/libs/prx/libSceVideoCoreInterface/Export.cpp new file mode 100644 index 0000000000..e4fd254e95 --- /dev/null +++ b/core/libs/prx/libSceVideoCoreInterface/Export.cpp @@ -0,0 +1,26 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + + +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..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceVideoCoreServerInterface/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/libSceVideoDecoderArbitration/Export.cpp b/core/libs/prx/libSceVideoDecoderArbitration/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceVideoDecoderArbitration/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/libSceVideoNativeExtEssential/Export.cpp b/core/libs/prx/libSceVideoNativeExtEssential/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceVideoNativeExtEssential/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/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index 7bc8fe8892..2b3dd27800 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -6,19 +6,26 @@ 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 ) target_include_directories(libSceVideoOut PRIVATE ${LIBS_INCLUDE_DIR} - ${CMAKE_SOURCE_DIR}/3rdparty/SDL2/include - ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include + ${ANYPS5_SDL2_INCLUDE_DIR} + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} + ${CMAKE_BINARY_DIR}/core/libs/prx/libSceAgcDriver/generated ) -target_link_libraries(libSceVideoOut PRIVATE libSceAgcDriver libScePad libSceMouse libSceKeyboard libc libkernel decoder_png SDL2-static) +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) @@ -26,31 +33,142 @@ configure_windows_unwind(libSceVideoOut) foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") endforeach() +add_test_executable(video_out_mouse_tests EXCLUDE_FROM_ALL tests/MouseInput.cpp) +target_include_directories(video_out_mouse_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) +target_link_libraries(video_out_mouse_tests PRIVATE libSceVideoOut libSceMouse libc SDL2-static) +set_target_properties(video_out_mouse_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(video_out_mouse_tests) +if(BUILD_TESTING) + set_target_properties(video_out_mouse_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME video_out_mouse COMMAND video_out_mouse_tests) + if(WIN32) + set_tests_properties(video_out_mouse PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$;PATH=path_list_prepend:$;PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(video_out_flip_tests EXCLUDE_FROM_ALL tests/Flip.cpp) -target_include_directories(video_out_flip_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/SDL2/include ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include) +target_include_directories(video_out_flip_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) target_link_libraries(video_out_flip_tests PRIVATE libSceVideoOut libSceAgcDriver libkernel libc) set_target_properties(video_out_flip_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(video_out_flip_tests) if(BUILD_TESTING) - set_target_properties(video_out_flip_tests PROPERTIES EXCLUDE_FROM_ALL OFF) - foreach(flipMode IN ITEMS lifetime decode controls) - if(flipMode STREQUAL "lifetime") - add_test(NAME video_out_flip_${flipMode} COMMAND video_out_flip_tests) - else() - add_test(NAME video_out_flip_${flipMode} COMMAND video_out_flip_tests ${flipMode}) - endif() - set_tests_properties(video_out_flip_${flipMode} PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77 ENVIRONMENT "SDL_VIDEODRIVER=offscreen") + add_test_executable(video_out_output_mode_policy_tests tests/OutputModePolicy.cpp) + target_include_directories(video_out_output_mode_policy_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) + target_link_libraries(video_out_output_mode_policy_tests PRIVATE libSceVideoOut libc libkernel) + configure_windows_unwind(video_out_output_mode_policy_tests) + add_test(NAME video_out_output_mode_policy COMMAND video_out_output_mode_policy_tests) + set_tests_properties(video_out_output_mode_policy PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(video_out_output_mode_policy PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(guest_video_out_high_frame_rate_tests ${LIBS_INCLUDE_DIR}/tests/GuestVideoOutHighFrameRate.cpp) + target_include_directories(guest_video_out_high_frame_rate_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_video_out_high_frame_rate_tests PRIVATE libSceVideoOut libSceAgcDriver libkernel libc) + configure_windows_unwind(guest_video_out_high_frame_rate_tests) + add_test(NAME guest_video_out_high_frame_rate COMMAND guest_video_out_high_frame_rate_tests) + set_tests_properties(guest_video_out_high_frame_rate PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0) + if(WIN32) + set_tests_properties(guest_video_out_high_frame_rate PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + 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() + set(ANYPS5_VIDEO_OUT_TEST_DRIVER "offscreen" 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 ps4) + if(flipMode STREQUAL "lifetime") + add_test(NAME video_out_flip_${flipMode} COMMAND video_out_flip_tests) + else() + add_test(NAME video_out_flip_${flipMode} COMMAND video_out_flip_tests ${flipMode}) + endif() + 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() + 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) + add_test_executable(video_out_frame_dump_queue_tests tests/FrameDumpQueue.cpp) + target_include_directories(video_out_frame_dump_queue_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) + target_link_libraries(video_out_frame_dump_queue_tests PRIVATE libSceVideoOut libSceAgcDriver libkernel libc) + set_target_properties(video_out_frame_dump_queue_tests PROPERTIES CXX_EXTENSIONS OFF) + 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") endif() if(BUILD_TESTING) add_test_executable(pad_sdl_tests tests/PadSdl.cpp) target_link_libraries(pad_sdl_tests PRIVATE SDL2-static) add_test(NAME pad_sdl COMMAND pad_sdl_tests) + add_test_executable(pad_input_mapping_tests tests/InputMapping.cpp src/InputMapping.cpp) + target_include_directories(pad_input_mapping_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/SDL2/include) + target_link_libraries(pad_input_mapping_tests PRIVATE SDL2-static libc) + set_target_properties(pad_input_mapping_tests PROPERTIES CXX_EXTENSIONS OFF) + configure_windows_unwind(pad_input_mapping_tests) + add_test(NAME pad_input_mapping COMMAND pad_input_mapping_tests) + if(WIN32) + 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) + add_test_executable(pad_keyboard_mouse_tests tests/KeyboardMouse.cpp src/KeyboardMouse.cpp) + target_include_directories(pad_keyboard_mouse_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/SDL2/include) + target_link_libraries(pad_keyboard_mouse_tests PRIVATE SDL2-static) + if(MINGW) + target_compile_options(pad_keyboard_mouse_tests PRIVATE -fasynchronous-unwind-tables) + target_link_options(pad_keyboard_mouse_tests PRIVATE -static -static-libgcc -static-libstdc++) + endif() + add_test(NAME pad_keyboard_mouse COMMAND pad_keyboard_mouse_tests) endif() if(BUILD_TESTING) @@ -59,12 +177,12 @@ if(BUILD_TESTING) target_link_libraries(guest_video_out_wqhd_detection_tests PRIVATE libSceVideoOut libSceAgcDriver libkernel libc) configure_windows_unwind(guest_video_out_wqhd_detection_tests) add_test(NAME guest_video_out_wqhd_detection COMMAND guest_video_out_wqhd_detection_tests) - set_tests_properties(guest_video_out_wqhd_detection PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77) + set_tests_properties(guest_video_out_wqhd_detection PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0) if(WIN32) set_tests_properties(guest_video_out_wqhd_detection PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") add_test(NAME guest_video_out_wqhd_detection_no_vulkan COMMAND guest_video_out_wqhd_detection_tests) - set_tests_properties(guest_video_out_wqhd_detection_no_vulkan PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77 + set_tests_properties(guest_video_out_wqhd_detection_no_vulkan PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0 ENVIRONMENT "VK_DRIVER_FILES=${CMAKE_CURRENT_BINARY_DIR}/missing_icd.json;VK_ICD_FILENAMES=${CMAKE_CURRENT_BINARY_DIR}/missing_icd.json" ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() @@ -76,7 +194,7 @@ if(BUILD_TESTING) target_link_libraries(guest_video_out_resolution_status_tests PRIVATE libSceVideoOut libSceAgcDriver libkernel libc) configure_windows_unwind(guest_video_out_resolution_status_tests) add_test(NAME guest_video_out_resolution_status COMMAND guest_video_out_resolution_status_tests) - set_tests_properties(guest_video_out_resolution_status PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77) + set_tests_properties(guest_video_out_resolution_status PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0) if(WIN32) set_tests_properties(guest_video_out_resolution_status PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") @@ -89,11 +207,62 @@ if(BUILD_TESTING) target_link_libraries(guest_video_out_output_mode_tests PRIVATE libSceVideoOut libSceAgcDriver libkernel libc) configure_windows_unwind(guest_video_out_output_mode_tests) add_test(NAME guest_video_out_output_mode COMMAND guest_video_out_output_mode_tests) - set_tests_properties(guest_video_out_output_mode PROPERTIES TIMEOUT 20) + set_tests_properties(guest_video_out_output_mode PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0) if(WIN32) 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) @@ -107,3 +276,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 ed90023661..dabea7e04a 100644 --- a/core/libs/prx/libSceVideoOut/Unimplemented.cpp +++ b/core/libs/prx/libSceVideoOut/Unimplemented.cpp @@ -7,6 +7,36 @@ extern "C" { +int APS5_VABI sceVideoOutSetFlipMaster() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoOutClearFlipMaster() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI 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; +} + int APS5_VABI sceVideoOutAdjustColor_(int handle, const VideoOutColorSettings* settings, uint32_t settings_size) { if (settings_size < sizeof(VideoOutColorSettings)) throw std::runtime_error("sceVideoOutAdjustColor_: VIDEO_OUT_ERROR_INVALID_VALUE"); return sceVideoOutAdjustColor(handle, settings); @@ -17,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 903e4029c6..8fb708400b 100644 --- a/core/libs/prx/libSceVideoOut/include/Buffer.hpp +++ b/core/libs/prx/libSceVideoOut/include/Buffer.hpp @@ -8,11 +8,16 @@ 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); 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/Event.hpp b/core/libs/prx/libSceVideoOut/include/Event.hpp index af8d10be45..ca5e4f09f7 100644 --- a/core/libs/prx/libSceVideoOut/include/Event.hpp +++ b/core/libs/prx/libSceVideoOut/include/Event.hpp @@ -15,8 +15,10 @@ int APS5_VABI sceVideoOutAddVblankEvent(KernelEqueue eq, int handle, void* udata int APS5_VABI sceVideoOutAddVrrActiveStatusEvent(KernelEqueue eq, int handle, void* udata); int APS5_VABI sceVideoOutDeleteFlipEvent(KernelEqueue eq, int handle); +int APS5_VABI sceVideoOutDeleteOutputModeEvent(KernelEqueue eq, int handle); int APS5_VABI sceVideoOutDeletePreVblankStartEvent(KernelEqueue eq, int handle); int APS5_VABI sceVideoOutDeleteVblankEvent(KernelEqueue eq, int handle); +int APS5_VABI sceVideoOutDeleteVrrActiveStatusEvent(KernelEqueue eq, int handle); int APS5_VABI sceVideoOutGetEventCount(const KernelEvent* ev); int APS5_VABI sceVideoOutGetEventData(const KernelEvent* ev, int64_t* data); diff --git a/core/libs/prx/libSceVideoOut/include/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/ImeTextInput.hpp b/core/libs/prx/libSceVideoOut/include/ImeTextInput.hpp new file mode 100644 index 0000000000..81cfe59fc1 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/include/ImeTextInput.hpp @@ -0,0 +1,11 @@ +#ifndef CORE_LIBS_PRX_LIBSCEVIDEOOUT_IMETEXTINPUT_HPP +#define CORE_LIBS_PRX_LIBSCEVIDEOOUT_IMETEXTINPUT_HPP + +#include "SDL_events.h" + +class ImeTextInput { +public: + void HandleEvent(const SDL_Event& event, unsigned windowId); +}; + +#endif diff --git a/core/libs/prx/libSceVideoOut/include/MouseInput.hpp b/core/libs/prx/libSceVideoOut/include/MouseInput.hpp index 56ea31b6d7..be37a2c2cb 100644 --- a/core/libs/prx/libSceVideoOut/include/MouseInput.hpp +++ b/core/libs/prx/libSceVideoOut/include/MouseInput.hpp @@ -2,13 +2,24 @@ #define CORE_LIBS_PRX_LIBSCEVIDEOOUT_MOUSEINPUT_HPP #include "SDL_events.h" +#include class MouseInput { public: void HandleEvent(const SDL_Event& event, unsigned windowId); + void SetPresentation(std::uint32_t sourceWidth, std::uint32_t sourceHeight, std::uint32_t presentedWidth, std::uint32_t presentedHeight); + bool CaptureWanted(bool allowed) const; + void UpdateCapture(bool allowed); private: + std::int32_t scaled(std::int32_t delta, double scale, double& remainder); + bool focused = true; + bool captured = false; + double scaleX = 1.0; + double scaleY = 1.0; + double remainderX = 0.0; + double remainderY = 0.0; }; #endif diff --git a/core/libs/prx/libSceVideoOut/include/Output.hpp b/core/libs/prx/libSceVideoOut/include/Output.hpp index 9bf3e6aa3c..569e97ee77 100644 --- a/core/libs/prx/libSceVideoOut/include/Output.hpp +++ b/core/libs/prx/libSceVideoOut/include/Output.hpp @@ -5,6 +5,8 @@ #include "prx/libc/include/General.hpp" #include "SceTypes.hpp" +int ValidateVideoOutOutputMode_nid_no_patch(uint64_t mode, const VideoOutOutputOptions* options, int displayRefreshRate); + extern "C" { int APS5_VABI sceVideoOutOpen(int user_id, int bus_type, int index, const void* param); @@ -17,6 +19,7 @@ int APS5_VABI sceVideoOutSetFlipRate(int handle, int rate); int APS5_VABI sceVideoOutSetWindowModeMargins(int handle, int top, int bottom); int APS5_VABI sceVideoOutGetFlipStatus(int handle, VideoOutFlipStatus* status); int APS5_VABI sceVideoOutGetOutputStatus(int handle, VideoOutOutputStatus* status); +int APS5_VABI sceVideoOutGetOutputStatusWithVrr(int handle, void* status); int APS5_VABI sceVideoOutGetResolutionStatus(int handle, VideoOutResolutionStatus* status); int APS5_VABI sceVideoOutGetVblankStatus(int handle, VideoOutVblankStatus* status); int APS5_VABI sceVideoOutIsFlipPending(int handle); @@ -25,6 +28,15 @@ 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 f0cbea89cb..dfa068d16d 100644 --- a/core/libs/prx/libSceVideoOut/include/PadInput.hpp +++ b/core/libs/prx/libSceVideoOut/include/PadInput.hpp @@ -3,11 +3,11 @@ #include "SDL_events.h" #include "SDL_gamecontroller.h" -#include "prx/libScePad/include/InputMapping.hpp" +#include "prx/libScePad/include/KeyboardMouse.hpp" #include "prx/libScePad/include/PadState.hpp" -#include +#include "prx/libSceVideoOut/include/PadOutputPacing.hpp" #include -#include +#include "prx/libSceVideoOut/include/Telemetry.hpp" class DisplayWindow; @@ -16,10 +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: - void publish(); + explicit PadInput(Pad::InputConfiguration configuration); + void publish(std::uint64_t frame); void setMouseMode(bool enabled); void openFirstAvailableController(); void openController(int deviceIndex); @@ -28,18 +30,16 @@ private: void applyOutput(); void enableSensors(); - std::vector bindings; - std::vector pressed; - std::vector wheelReleaseTimes; - std::array mouseStick{128, 128}; - std::chrono::steady_clock::time_point nextMousePoll{}; - bool mouseEnabled = false; + Pad::KeyboardMouse keyboardMouse; SDL_GameController* controller = nullptr; PadInputState controllerState{}; std::uint32_t outputSequence = 0; 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 cc7bcfd478..7c868f3ce2 100644 --- a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp +++ b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp @@ -19,7 +19,9 @@ #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" #include "SDL.h" #include "SceTypes.hpp" @@ -42,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; @@ -86,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; @@ -101,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); @@ -123,8 +131,10 @@ struct VideoOutConfig { uint32_t width = VIDEO_OUT_DEFAULT_WIDTH; uint32_t height = VIDEO_OUT_DEFAULT_HEIGHT; uint64_t generation = 0; + 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; @@ -138,14 +148,21 @@ 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{}; - void Check() const { + bool Closed() const { return !opened || closing; } + + void CheckAlive() const { if (failure) std::rethrow_exception(failure); if (shutdownToken.stop_requested()) throw ProcessShutdown{}; - if (!opened || closing) throw std::runtime_error("VideoOut: port is closed"); + } + + void Check() const { + CheckAlive(); + if (Closed()) throw std::runtime_error("VideoOut: port is closed"); } }; @@ -165,7 +182,9 @@ struct FlipRequest final : AgcDriver::IFlipRequest, std::enable_shared_from_this uint32_t height = 0; VideoOutBuffer buffer; BufferAttributeGroup group; + bool groupCaptured = false; bool reserved = false; + bool unregistered = false; bool ready = false; bool gpuComplete = false; bool terminal = false; @@ -176,11 +195,13 @@ struct FlipRequest final : AgcDriver::IFlipRequest, std::enable_shared_from_this ~FlipRequest() override; void GpuReady(const std::shared_ptr& frameTiming) override; void Fail(std::exception_ptr error) noexcept override; + void Cancel() noexcept; + void ReleaseLocked() noexcept; }; struct FlipQueue { std::mutex mutex; - std::condition_variable changed; + TimedWait::Condition changed; std::list> requests; std::atomic reservations = 0; std::exception_ptr failure; @@ -203,17 +224,22 @@ public: std::shared_ptr GetConfig(int handle); bool IsOpen(int handle); bool HasConfig(int handle); + bool SupportsHighFrameRate(); + int GetDisplayRefreshRate(); // 0, or VIDEO_OUT_ERROR_FLIP_QUEUE_FULL when the title has VIDEO_OUT_FLIP_QUEUE_CAPACITY flips pending. int SubmitFlip(int handle, int index, int flipMode, int64_t flipArg); + int SubmitEopFlip(int handle, int index, int flipMode, int64_t flipArg, std::uint32_t* token); private: bool close(int handle); void presentLoop(std::stop_token token, std::promise& started); void vblankLoop(std::stop_token token); - void vblankEnd(); + void vblankEnd(std::uint64_t tick); + void updateDisplayRefreshRate(); void processFlip(FlipRequest& req); void triggerEvents(VideoOutConfig& cfg, int eventKind, void* triggerData); + void pollHostHdr(); std::mutex mutex; std::mutex shutdownMutex; @@ -223,6 +249,9 @@ private: std::shared_ptr flipQueue = std::make_shared(); DisplayWindow window; + HostHdrProbe hdrProbe; + std::uint64_t hdrGeneration = 0; + std::atomic displayRefreshRate{0}; std::jthread presentThread; std::jthread vblankThread; diff --git a/core/libs/prx/libSceVideoOut/src/Buffer.cpp b/core/libs/prx/libSceVideoOut/src/Buffer.cpp index 481c92c2ff..81722d188e 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"); @@ -47,6 +113,13 @@ void APS5_VABI sceVideoOutSetBufferAttribute2(VideoOutBufferAttribute2* attribut attribute->dcc_control = dccControl; } +void APS5_VABI sceVideoOutAddBufferAttributeOption(VideoOutBufferAttribute2* attribute, uint64_t option) { + if (attribute == nullptr) { + throw std::runtime_error("sceVideoOutAddBufferAttributeOption: 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) { @@ -69,40 +142,65 @@ int APS5_VABI sceVideoOutRegisterBuffers2(int handle, int setIndex, int bufferIn } AgcDriverCheckGuestMemory_nid_postfix(attribute, sizeof(*attribute), alignof(VideoOutBufferAttribute2)); AgcDriverCheckGuestMemory_nid_postfix(buffers, static_cast(bufferNum) * sizeof(*buffers), alignof(VideoOutBuffers)); - std::unique_lock lock(cfg->mutex); - cfg->Check(); - if (cfg->closing) { - throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); - } - if (cfg->groups[setIndex].occupied) { - throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_INDEX"); - } - for (int i = 0; i < bufferNum; i++) { - if (cfg->buffers[bufferIndexStart + i].Occupied()) { - throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_SLOT_OCCUPIED"); - } - } BufferAttributeGroup group{}; group.attribute = *attribute; group.category = category; group.occupied = true; - std::array registered{}; - for (int i = 0; i < bufferNum; ++i) { - registered[i] = {setIndex, reinterpret_cast(buffers[i].data), reinterpret_cast(buffers[i].metadata)}; - const auto display = DescribeVideoOutBuffer(registered[i], group); - AgcDriverCheckGuestMemory_nid_postfix(buffers[i].data, AgcDriverDisplayBufferSize_nid_postfix(display), 65536); - } - cfg->groups[setIndex] = group; - cfg->width = attribute->width; - cfg->height = attribute->height; - for (int i = 0; i < bufferNum; i++) { - cfg->buffers[bufferIndexStart + i] = registered[i]; - } + 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) { @@ -127,9 +225,8 @@ int APS5_VABI sceVideoOutSubmitChangeBufferAttribute2(int handle, int setIndex, group.attribute = *attribute; for (int i = 0; i < VIDEO_OUT_BUFFER_NUM_MAX; ++i) { if (cfg->buffers[i].groupIndex != setIndex) continue; - if (cfg->bufferPending[i] != 0) throw std::runtime_error("VideoOut: buffer attributes are in use by a pending flip"); 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; @@ -167,6 +264,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/Event.cpp b/core/libs/prx/libSceVideoOut/src/Event.cpp index 693b2f1069..d13e6494fb 100644 --- a/core/libs/prx/libSceVideoOut/src/Event.cpp +++ b/core/libs/prx/libSceVideoOut/src/Event.cpp @@ -18,6 +18,7 @@ static std::vector* getEventList(VideoOutConfig& cfg, int16_t } static int registerVideoOutEvent(int handle, KernelEqueue eq, int16_t eventKind, void* udata) { + if (!VideoOutDriver::Get().HasConfig(handle)) return VIDEO_OUT_ERROR_INVALID_HANDLE; auto cfg = VideoOutDriver::Get().GetConfig(handle); if (cfg == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); @@ -121,10 +122,7 @@ int APS5_VABI sceVideoOutAddVblankEvent(KernelEqueue eq, int handle, void* udata } int APS5_VABI sceVideoOutAddPreVblankStartEvent(KernelEqueue eq, int handle, void* udata) try { - static_cast(eq); - static_cast(udata); - VideoOutDriver::Get().GetConfig(handle); - throw std::runtime_error("VideoOut: physical pre-vblank timing is not implemented"); + return registerVideoOutEvent(handle, eq, VIDEO_OUT_EVENT_PRE_VBLANK_START, udata); } catch (const ProcessShutdown&) { LibcAwaitExit_nid_postfix(); } @@ -147,6 +145,12 @@ int APS5_VABI sceVideoOutDeleteFlipEvent(KernelEqueue eq, int handle) try { LibcAwaitExit_nid_postfix(); } +int APS5_VABI sceVideoOutDeleteOutputModeEvent(KernelEqueue eq, int handle) try { + return deleteVideoOutEvent(handle, eq, VIDEO_OUT_EVENT_SET_MODE); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); +} + int APS5_VABI sceVideoOutDeleteVblankEvent(KernelEqueue eq, int handle) try { return deleteVideoOutEvent(handle, eq, VIDEO_OUT_EVENT_VBLANK); } catch (const ProcessShutdown&) { @@ -159,6 +163,12 @@ int APS5_VABI sceVideoOutDeletePreVblankStartEvent(KernelEqueue eq, int handle) LibcAwaitExit_nid_postfix(); } +int APS5_VABI sceVideoOutDeleteVrrActiveStatusEvent(KernelEqueue eq, int handle) try { + return deleteVideoOutEvent(handle, eq, VIDEO_OUT_EVENT_VRR_STATUS); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); +} + int APS5_VABI sceVideoOutGetEventId(const KernelEvent* ev) { if (ev == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_ADDRESS"); diff --git a/core/libs/prx/libSceVideoOut/src/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/ImeTextInput.cpp b/core/libs/prx/libSceVideoOut/src/ImeTextInput.cpp new file mode 100644 index 0000000000..26ddb4b216 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/src/ImeTextInput.cpp @@ -0,0 +1,42 @@ +#include "prx/libSceVideoOut/include/ImeTextInput.hpp" +#include "prx/libSceIme/include/ImeInput.hpp" + +#include + +#include "SDL_keyboard.h" + +void ImeTextInput::HandleEvent(const SDL_Event& event, unsigned windowId) { + ImeHostInput input{}; + switch (event.type) { + case SDL_TEXTINPUT: + if (event.text.windowID != windowId) return; + static_assert(sizeof(input.text) >= sizeof(event.text.text)); + std::memcpy(input.text, event.text.text, sizeof(event.text.text)); + input.kind = ImeHostInputKind::Text; + break; + case SDL_KEYDOWN: + if (event.key.windowID != windowId) return; + switch (event.key.keysym.scancode) { + case SDL_SCANCODE_BACKSPACE: input.kind = ImeHostInputKind::Backspace; break; + case SDL_SCANCODE_DELETE: input.kind = ImeHostInputKind::Delete; break; + case SDL_SCANCODE_LEFT: input.kind = ImeHostInputKind::Left; break; + case SDL_SCANCODE_RIGHT: input.kind = ImeHostInputKind::Right; break; + case SDL_SCANCODE_HOME: input.kind = ImeHostInputKind::Home; break; + case SDL_SCANCODE_END: input.kind = ImeHostInputKind::End; break; + case SDL_SCANCODE_RETURN: + case SDL_SCANCODE_KP_ENTER: + if (event.key.repeat != 0) return; + input.kind = ImeHostInputKind::Enter; + break; + case SDL_SCANCODE_ESCAPE: + if (event.key.repeat != 0) return; + input.kind = ImeHostInputKind::Escape; + break; + default: return; + } + break; + default: + return; + } + ImePublishInput_nid_postfix(input); +} diff --git a/core/libs/prx/libSceVideoOut/src/InputMapping.cpp b/core/libs/prx/libSceVideoOut/src/InputMapping.cpp index d3d3d72752..dc9bb16da1 100644 --- a/core/libs/prx/libSceVideoOut/src/InputMapping.cpp +++ b/core/libs/prx/libSceVideoOut/src/InputMapping.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include #include #include @@ -49,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) { @@ -109,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") { @@ -121,32 +133,168 @@ Pad::InputBinding parseBinding(const InputAction& action, std::string_view sourc throw std::runtime_error("source type must be KEY, MOUSE or WHEEL"); } +enum class InputSetting { + LeftStickDeadzone, + RightStickDeadzone, + MouseSensitivity, + MouseVerticalScale, + MouseCurve, + MouseDeadzoneOffset, + MouseMaxSpeed, + MouseSmoothingMs, + MouseDecayMs, + StickModifierScale, + KeyboardRampMs, + TriggerRampMs +}; + +struct InputSettingName { + std::string_view name; + InputSetting setting; +}; + +constexpr auto settingNames = std::array{ + InputSettingName{"LeftStickDeadzone", InputSetting::LeftStickDeadzone}, + InputSettingName{"RightStickDeadzone", InputSetting::RightStickDeadzone}, + InputSettingName{"MouseSensitivity", InputSetting::MouseSensitivity}, + InputSettingName{"MouseVerticalScale", InputSetting::MouseVerticalScale}, + InputSettingName{"MouseCurve", InputSetting::MouseCurve}, + InputSettingName{"MouseDeadzoneOffset", InputSetting::MouseDeadzoneOffset}, + InputSettingName{"MouseMaxSpeed", InputSetting::MouseMaxSpeed}, + InputSettingName{"MouseSmoothingMs", InputSetting::MouseSmoothingMs}, + InputSettingName{"MouseDecayMs", InputSetting::MouseDecayMs}, + InputSettingName{"StickModifierScale", InputSetting::StickModifierScale}, + InputSettingName{"KeyboardRampMs", InputSetting::KeyboardRampMs}, + InputSettingName{"TriggerRampMs", InputSetting::TriggerRampMs} +}; + +const InputSettingName* findSetting(std::string_view name) { + const auto normalized = upper(name); + for (const auto& setting : settingNames) { + if (upper(setting.name) == normalized) return &setting; + } + return nullptr; +} + +int parseDeadzonePercent(std::string_view value) { + int parsed = 0; + const auto end = value.data() + value.size(); + const auto result = std::from_chars(value.data(), end, parsed); + if (result.ec != std::errc{} || result.ptr != end || parsed < 0 || parsed > Pad::MaxStickDeadzonePercent) { + throw std::runtime_error("deadzone must be a whole percentage from 0 to " + std::to_string(Pad::MaxStickDeadzonePercent)); + } + return parsed; +} + +double parseMouseSensitivity(std::string_view value) { + double parsed = 0.0; + const auto end = value.data() + value.size(); + const auto result = std::from_chars(value.data(), end, parsed); + if (result.ec != std::errc{} || result.ptr != end || !(parsed > 0.0) || parsed > Pad::MaxMouseSensitivity) { + throw std::runtime_error("mouse sensitivity must be a number greater than 0 and at most 10"); + } + return parsed; +} + +std::string formatNumber(double value) { + std::array buffer{}; + const auto result = std::to_chars(buffer.data(), buffer.data() + buffer.size(), value); + return std::string(buffer.data(), result.ptr); +} + +double parseNumber(std::string_view value, std::string_view name, double minimum, double maximum) { + double parsed = 0.0; + const auto end = value.data() + value.size(); + const auto result = std::from_chars(value.data(), end, parsed); + if (value.empty() || result.ec != std::errc{} || result.ptr != end || !std::isfinite(parsed) || parsed < minimum || parsed > maximum) { + throw std::runtime_error(std::string(name) + " must be a number from " + formatNumber(minimum) + " to " + formatNumber(maximum)); + } + return parsed; +} + +void applySetting(Pad::InputSettings& settings, InputSetting setting, std::string_view value) { + switch (setting) { + case InputSetting::LeftStickDeadzone: settings.leftStickDeadzonePercent = parseDeadzonePercent(value); break; + case InputSetting::RightStickDeadzone: settings.rightStickDeadzonePercent = parseDeadzonePercent(value); break; + case InputSetting::MouseSensitivity: settings.mouseSensitivity = parseMouseSensitivity(value); break; + case InputSetting::MouseVerticalScale: settings.mouseVerticalScale = parseNumber(value, "mouse vertical scale", 0.1, 10.0); break; + case InputSetting::MouseCurve: settings.mouseCurve = parseNumber(value, "mouse curve", 0.2, 5.0); break; + case InputSetting::MouseDeadzoneOffset: settings.mouseDeadzoneOffset = parseNumber(value, "mouse deadzone offset", 0.0, 0.9); break; + case InputSetting::MouseMaxSpeed: settings.mouseMaxSpeed = parseNumber(value, "mouse max speed", 0.1, 10.0); break; + case InputSetting::MouseSmoothingMs: settings.mouseSmoothingMs = parseNumber(value, "mouse smoothing", 0.0, 500.0); break; + case InputSetting::MouseDecayMs: settings.mouseDecayMs = parseNumber(value, "mouse decay", 1.0, 1000.0); break; + 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; + } +} + +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"; } } -std::vector Pad::LoadInputMapping() { - std::vector bindings(InputMapping.begin(), InputMapping.end()); - const char* configuredPath = std::getenv("ANYPS5_INPUT_CONFIG"); - const bool explicitPath = configuredPath != nullptr && configuredPath[0] != '\0'; - const std::filesystem::path path = explicitPath ? configuredPath : defaultConfigPath(); +std::array Pad::StickWithDeadzone(std::int16_t x, std::int16_t y, int deadzonePercent) { + if (deadzonePercent < 0 || deadzonePercent > MaxStickDeadzonePercent) throw std::invalid_argument("Pad: stick deadzone out of range"); + if (deadzonePercent == 0) return {stickByte(x), stickByte(y)}; + const double normalizedX = std::max(x / 32767.0, -1.0); + const double normalizedY = std::max(y / 32767.0, -1.0); + const double magnitude = std::hypot(normalizedX, normalizedY); + const double deadzone = deadzonePercent / 100.0; + if (magnitude <= deadzone) return {stickByte(0), stickByte(0)}; + const double scale = (magnitude - deadzone) / (1.0 - deadzone) / magnitude; + const auto axis = [scale](double value) { + return stickByte(static_cast(std::lround(std::clamp(value * scale, -1.0, 1.0) * 32767.0))); + }; + return {axis(normalizedX), axis(normalizedY)}; +} + +Pad::InputConfiguration Pad::LoadInputMapping() { + InputConfiguration configuration{{InputMapping.begin(), InputMapping.end()}, {}}; + auto& bindings = configuration.bindings; + 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 bindings; + return configuration; } std::unordered_set overriddenActions; + std::array configuredSettings{}; std::string line; std::size_t lineNumber = 0; while (std::getline(file, line)) { @@ -160,6 +308,17 @@ std::vector Pad::LoadInputMapping() { const auto actionName = trim(content.substr(0, separator)); const auto source = trim(content.substr(separator + 1)); + if (const auto* setting = findSetting(actionName)) { + auto& configured = configuredSettings[static_cast(setting->setting)]; + if (configured) invalidLine(path, lineNumber, std::string(setting->name) + " is set more than once"); + configured = true; + try { + applySetting(configuration.settings, setting->setting, source); + } catch (const std::runtime_error& error) { + invalidLine(path, lineNumber, error.what()); + } + continue; + } const auto* action = findAction(actionName); if (action == nullptr) invalidLine(path, lineNumber, "unknown action '" + std::string(actionName) + "'"); if (source.empty()) invalidLine(path, lineNumber, "source is empty"); @@ -180,7 +339,7 @@ std::vector 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 bindings; + return configuration; } diff --git a/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp b/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp new file mode 100644 index 0000000000..1ef93610d0 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp @@ -0,0 +1,326 @@ +#include "prx/libScePad/include/KeyboardMouse.hpp" + +#include +#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) { + if (!(seconds > 0.0)) return; + pendingSeconds += seconds; + if (pendingSeconds < MouseMinimumStepSeconds) return; + const double step = pendingSeconds; + pendingSeconds = 0.0; + const bool moved = pendingX != 0.0 || pendingY != 0.0; + const double scale = settings.mouseSensitivity / (MouseFullTiltCountsPerSecond * step); + const double targetX = pendingX * scale; + const double targetY = pendingY * scale * settings.mouseVerticalScale; + pendingX = 0.0; + pendingY = 0.0; + measureReportRate(step, moved); + idleSeconds = moved ? 0.0 : idleSeconds + step; + if (idleSeconds * 1000.0 >= settings.mouseDecayMs) { + speedX = 0.0; + speedY = 0.0; + return; + } + 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); + if (speed > settings.mouseMaxSpeed) { + speedX *= settings.mouseMaxSpeed / speed; + speedY *= settings.mouseMaxSpeed / speed; + } +} + +void Pad::MouseStick::Reset() { + pendingX = 0.0; + pendingY = 0.0; + pendingSeconds = 0.0; + pendingCounts = 0.0; + pendingReports = 0; + rateSeconds = 0.0; + rateCounts = 0.0; + rateReports = 0; + speedX = 0.0; + speedY = 0.0; + idleSeconds = 0.0; +} + +double Pad::MouseStick::Speed() const { + return std::hypot(speedX, speedY); +} + +std::array Pad::MouseStick::Deflection() const { + const double speed = std::hypot(speedX, speedY); + if (speed <= 0.0) return {0.0, 0.0}; + const double shaped = std::pow(std::min(speed, 1.0), settings.mouseCurve); + const double deflection = settings.mouseDeadzoneOffset + (1.0 - settings.mouseDeadzoneOffset) * shaped; + return {speedX / speed * deflection, speedY / speed * deflection}; +} + +std::uint8_t Pad::StickByte(double deflection) { + const double clamped = std::clamp(deflection, -1.0, 1.0); + return static_cast(std::lround(clamped < 0.0 ? 128.0 + clamped * 128.0 : 128.0 + clamped * 127.0)); +} + +Pad::KeyboardMouse::KeyboardMouse(InputConfiguration configuration) + : configuration(std::move(configuration)), + pressed(this->configuration.bindings.size()), + wheelReleaseTimes(this->configuration.bindings.size()), + mouse(this->configuration.settings) {} + +const Pad::InputConfiguration& Pad::KeyboardMouse::Configuration() const { + return configuration; +} + +bool Pad::KeyboardMouse::MouseMode() const { + return mouseMode; +} + +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; + lastAdvance = now; + } + if (now > lastAdvance) { + const double seconds = std::chrono::duration(now - lastAdvance).count(); + lastAdvance = now; + if (mouseMode) mouse.Advance(seconds); + } + 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] = {}; + changed = true; + } + return changed; +} + +void Pad::KeyboardMouse::SetMouseMode(bool enabled) { + mouseMode = 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) { + InputRequests requests; + if (event.type == SDL_WINDOWEVENT) { + if (event.window.event != SDL_WINDOWEVENT_FOCUS_LOST && event.window.event != SDL_WINDOWEVENT_CLOSE) return requests; + Release(); + requests.releaseMouse = mouseMode; + return requests; + } + if (event.type == SDL_MOUSEMOTION) { + if (mouseMode && !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]; + if (binding.wheelDirection == 0) continue; + pressed[index] = binding.wheelDirection == direction; + wheelReleaseTimes[index] = pressed[index] ? releaseTime : Clock::time_point{}; + } + return requests; + } + const bool keyboard = event.type == SDL_KEYDOWN || event.type == SDL_KEYUP; + const bool mouseButton = event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP; + if (!keyboard && !mouseButton) return requests; + if (keyboard && event.key.repeat != 0) return requests; + 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]; + const bool matches = keyboard + ? binding.key != SDL_SCANCODE_UNKNOWN && binding.key == event.key.keysym.scancode + : binding.mouseButton != MouseButton::None && binding.mouseButton == static_cast(event.button.button); + if (!matches) continue; + if (binding.control == InputControl::ToggleFullscreen && keyboard && down && !pressed[index]) requests.toggleFullscreen = true; + if (binding.control == InputControl::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; + } + return requests; +} + +PadInputState Pad::KeyboardMouse::Compose(const PadInputState& controller) const { + PadInputState state = controller; + 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::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 (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 3a5443e6e8..f1e79af366 100644 --- a/core/libs/prx/libSceVideoOut/src/MouseInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/MouseInput.cpp @@ -2,14 +2,17 @@ #include "prx/libSceMouse/include/MouseState.hpp" #include "SDL_mouse.h" +#include "SDL_video.h" +#include +#include void MouseInput::HandleEvent(const SDL_Event& event, unsigned windowId) { MouseInputEvent input{}; switch (event.type) { case SDL_MOUSEMOTION: if (event.motion.windowID != windowId || !focused) return; - input.x = event.motion.xrel; - input.y = event.motion.yrel; + input.x = scaled(event.motion.xrel, scaleX, remainderX); + input.y = scaled(event.motion.yrel, scaleY, remainderY); break; case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP: @@ -51,5 +54,52 @@ 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) { + if (sourceWidth == 0 || sourceHeight == 0 || presentedWidth == 0 || presentedHeight == 0) return; + const double x = static_cast(sourceWidth) / static_cast(presentedWidth); + const double y = static_cast(sourceHeight) / static_cast(presentedHeight); + if (x != scaleX || y != scaleY) { + std::fprintf(stderr, "[mouse] video driver %s: guest %ux%u presented at %ux%u, delta scale %.4f x %.4f\n", SDL_GetCurrentVideoDriver() ? SDL_GetCurrentVideoDriver() : "?", sourceWidth, sourceHeight, presentedWidth, presentedHeight, x, y); + scaleX = x; + scaleY = y; + remainderX = 0.0; + remainderY = 0.0; + } +} + +std::int32_t MouseInput::scaled(std::int32_t delta, double scale, double& remainder) { + const double exact = static_cast(delta) * scale + remainder; + const double whole = std::trunc(exact); + remainder = exact - whole; + return static_cast(whole); +} + +bool MouseInput::CaptureWanted(bool allowed) const { + return allowed && focused && MouseIsOpen_nid_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) { + static bool warned = false; + if (!warned) { + warned = true; + std::fprintf(stderr, "[mouse] relative mouse mode unavailable: %s\n", SDL_GetError()); + } + return; + } + int ignoredX = 0; + int ignoredY = 0; + SDL_GetRelativeMouseState(&ignoredX, &ignoredY); + remainderX = 0.0; + remainderY = 0.0; + captured = wanted; } diff --git a/core/libs/prx/libSceVideoOut/src/Output.cpp b/core/libs/prx/libSceVideoOut/src/Output.cpp index 412ab1bf23..24f9b6f15c 100644 --- a/core/libs/prx/libSceVideoOut/src/Output.cpp +++ b/core/libs/prx/libSceVideoOut/src/Output.cpp @@ -8,19 +8,33 @@ #include #include "SceTypes.hpp" +#include "prx/libSceVideoOut/include/Output.hpp" #include "prx/libc/include/General.hpp" #include "prx/libSceVideoOut/include/VideoOutDriver.hpp" +#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" +#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; -static int validateOutputConfig(int handle, uint64_t mode, const VideoOutOutputOptions* options, void* reservedPtr, uint64_t reserved) { - if (!VideoOutDriver::Get().HasConfig(handle) || !VideoOutDriver::Get().IsOpen(handle)) return VIDEO_OUT_ERROR_INVALID_HANDLE; - if (reservedPtr != nullptr || reserved != 0) return VIDEO_OUT_ERROR_INVALID_VALUE; +static VideoOutOutputStatus outputStatus(const auto& cfg) { + VideoOutOutputStatus status{}; + status.resolution = (cfg.width >= 3840 || cfg.height >= 2160) ? 2u : 1u; + 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; +} + +int ValidateVideoOutOutputMode_nid_no_patch(uint64_t mode, const VideoOutOutputOptions* options, int displayRefreshRate) { switch (mode) { case VIDEO_OUT_OUTPUT_MODE_DEFAULT: break; - case 0x4: case 0x7: case 0x8: case 0xC: case 0xD: case 0xE: case VIDEO_OUT_OUTPUT_MODE_119_88HZ: case 0x10: case 0x11: case 0x13: + case VIDEO_OUT_OUTPUT_MODE_119_88HZ: + if (displayRefreshRate < 120) return VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE; + break; + case 0x4: case 0x7: case 0x8: case 0xC: case 0xD: case 0xE: case 0x10: case 0x11: case 0x13: return VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE; case 0x12: throw std::runtime_error(std::string(__func__) + ": output mode 0x12 answers differently on two consoles"); @@ -36,6 +50,12 @@ static int validateOutputConfig(int handle, uint64_t mode, const VideoOutOutputO return 0; } +static int validateOutputConfig(int handle, uint64_t mode, const VideoOutOutputOptions* options, void* reservedPtr, uint64_t reserved) { + if (!VideoOutDriver::Get().HasConfig(handle) || !VideoOutDriver::Get().IsOpen(handle)) return VIDEO_OUT_ERROR_INVALID_HANDLE; + if (reservedPtr != nullptr || reserved != 0) return VIDEO_OUT_ERROR_INVALID_VALUE; + return ValidateVideoOutOutputMode_nid_no_patch(mode, options, VideoOutDriver::Get().GetDisplayRefreshRate()); +} + static void validateOpenParam(const void* param) { if (param == nullptr) return; VideoOutOpenParam openParam{}; @@ -60,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"); @@ -71,9 +92,7 @@ int APS5_VABI sceVideoOutOpen(int userId, int busType, int index, const void* pa throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_VALUE"); } const int handle = VideoOutDriver::Get().Open(busType); - if (handle < 0) { - throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_RESOURCE_BUSY"); - } + if (handle < 0) return VIDEO_OUT_ERROR_RESOURCE_BUSY; return handle; } catch (const ProcessShutdown&) { LibcAwaitExit_nid_postfix(); @@ -99,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; @@ -120,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(); @@ -136,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(); @@ -152,13 +181,22 @@ int APS5_VABI sceVideoOutGetOutputStatus(int handle, VideoOutOutputStatus* statu } std::unique_lock lock(cfg->mutex); cfg->Check(); - status->resolution = (cfg->width >= 3840 || cfg->height >= 2160) ? 2u : 1u; - status->dynamicRange = 1; - status->refreshRate = (cfg->outputMode == VIDEO_OUT_OUTPUT_MODE_119_88HZ) ? VIDEO_OUT_REFRESH_RATE_119_88HZ : VIDEO_OUT_REFRESH_RATE_59_94HZ; - status->flags = 0; - status->reserved[0] = 0; - status->reserved[1] = 0; - status->reserved[2] = 0; + *status = outputStatus(*cfg); + return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); +} + +int APS5_VABI sceVideoOutGetOutputStatusWithVrr(int handle, void* status) try { + if (status == nullptr) return VIDEO_OUT_ERROR_INVALID_ADDRESS; + if (!VideoOutDriver::Get().HasConfig(handle) || !VideoOutDriver::Get().IsOpen(handle)) return VIDEO_OUT_ERROR_INVALID_HANDLE; + auto cfg = VideoOutDriver::Get().GetConfig(handle); + std::unique_lock lock(cfg->mutex); + cfg->Check(); + std::array bytes{}; + const auto output = outputStatus(*cfg); + std::memcpy(bytes.data(), &output, sizeof(output)); + std::memcpy(status, bytes.data(), bytes.size()); return 0; } catch (const ProcessShutdown&) { LibcAwaitExit_nid_postfix(); @@ -234,6 +272,7 @@ int APS5_VABI sceVideoOutConfigureOutput(int handle, uint64_t mode, const VideoO if (supported < 0) { return supported; } + if (mode == VIDEO_OUT_OUTPUT_MODE_119_88HZ && (GetAppAttribute3_nid_no_patch() & 0x40u) == 0) return VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE; auto cfg = VideoOutDriver::Get().GetConfig(handle); if (cfg == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); @@ -301,15 +340,64 @@ int APS5_VABI sceVideoOutAdjustColor(int handle, const VideoOutColorSettings* se LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutVrrUnpegFromFixedRate() try { - NotImplemented_nid_no_patch(__func__); +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 sceVideoOutVrrPegToFixedRate() try { - NotImplemented_nid_no_patch(__func__); +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"); + } + return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); +} + +int APS5_VABI sceVideoOutVrrPegToFixedRate(int handle, uint64_t arg1, uint64_t arg2) try { + if (VideoOutDriver::Get().GetConfig(handle) == nullptr) { + throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); + } + if (arg1 != 0 || arg2 != 0) { + throw std::runtime_error(std::string(__func__) + ": unknown arguments " + std::to_string(arg1) + ", " + std::to_string(arg2)); + } return 0; } catch (const ProcessShutdown&) { LibcAwaitExit_nid_postfix(); diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index ab3aa11a8e..d9abeeb048 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -5,16 +5,23 @@ #include #include #include +#include #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" +#include "prx/libSceIme/include/ImeInput.hpp" -PadInput::PadInput() - : bindings(Pad::LoadInputMapping()), pressed(bindings.size()), wheelReleaseTimes(bindings.size()) { +PadInput::PadInput() : PadInput(Pad::LoadInputMapping()) { +} + +PadInput::PadInput(Pad::InputConfiguration configuration) : keyboardMouse(std::move(configuration)) { openFirstAvailableController(); } @@ -53,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() { @@ -79,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; @@ -89,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); } @@ -108,13 +123,10 @@ void PadInput::applyOutput() { } void PadInput::setMouseMode(bool enabled) { + SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_WARP, "0"); + SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_SYSTEM_SCALE, "0"); if (SDL_SetRelativeMouseMode(enabled ? SDL_TRUE : SDL_FALSE) != 0) throw std::runtime_error(std::string("Pad: relative mouse mode failed: ") + SDL_GetError()); - int deltaX = 0; - int deltaY = 0; - SDL_GetRelativeMouseState(&deltaX, &deltaY); - mouseEnabled = enabled; - mouseStick = {128, 128}; - nextMousePoll = std::chrono::steady_clock::now() + std::chrono::milliseconds(Pad::MousePollIntervalMs); + keyboardMouse.SetMouseMode(enabled); } PadInputState PadInput::sampleController() const { @@ -153,16 +165,12 @@ PadInputState PadInput::sampleController() const { if (result.analogButtonsL2 != 0) result.buttons |= static_cast(Pad::PadButton::L2); if (result.analogButtonsR2 != 0) result.buttons |= static_cast(Pad::PadButton::R2); - const auto stickValue = [this](SDL_GameControllerAxis axis) { - const auto value = static_cast(SDL_GameControllerGetAxis(controller, axis)) + 32768; - return static_cast((value * 255 + 32767) / 65535); - }; - result.sticks = { - stickValue(SDL_CONTROLLER_AXIS_LEFTX), - stickValue(SDL_CONTROLLER_AXIS_LEFTY), - stickValue(SDL_CONTROLLER_AXIS_RIGHTX), - stickValue(SDL_CONTROLLER_AXIS_RIGHTY) + const auto stick = [this](SDL_GameControllerAxis xAxis, SDL_GameControllerAxis yAxis, int deadzonePercent) { + return Pad::StickWithDeadzone(SDL_GameControllerGetAxis(controller, xAxis), SDL_GameControllerGetAxis(controller, yAxis), deadzonePercent); }; + const auto left = stick(SDL_CONTROLLER_AXIS_LEFTX, SDL_CONTROLLER_AXIS_LEFTY, keyboardMouse.Configuration().settings.leftStickDeadzonePercent); + const auto right = stick(SDL_CONTROLLER_AXIS_RIGHTX, SDL_CONTROLLER_AXIS_RIGHTY, keyboardMouse.Configuration().settings.rightStickDeadzonePercent); + result.sticks = {left[0], left[1], right[0], right[1]}; switch (SDL_GameControllerGetType(controller)) { case SDL_CONTROLLER_TYPE_PS5: result.deviceKind = 1; break; case SDL_CONTROLLER_TYPE_PS4: result.deviceKind = 2; break; @@ -184,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; } @@ -204,146 +212,52 @@ 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 && (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST || event.window.event == SDL_WINDOWEVENT_CLOSE)) { - std::fill(pressed.begin(), pressed.end(), false); - std::fill(wheelReleaseTimes.begin(), wheelReleaseTimes.end(), std::chrono::steady_clock::time_point{}); - if (mouseEnabled) setMouseMode(false); - publish(); + if (event.type == SDL_WINDOWEVENT) { + if (keyboardMouse.HandleEvent(event, std::chrono::steady_clock::now()).releaseMouse) setMouseMode(false); + publish(window.FrameNumber()); return; } // APS5_NO_PAD_INPUT=1 keeps a measurement run from reacting to keys or mouse buttons that reach // its window (a stray press advances the title into another stage). static const bool ignoreInput = std::getenv("APS5_NO_PAD_INPUT") != nullptr; - if (ignoreInput && event.type == SDL_MOUSEWHEEL) return; - if (event.type == SDL_MOUSEWHEEL) { - int direction = (event.wheel.y > 0) - (event.wheel.y < 0); - if (event.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) direction = -direction; - if (direction == 0) return; - const auto releaseTime = std::chrono::steady_clock::now() + std::chrono::milliseconds(Pad::WheelPressDurationMs); - for (std::size_t index = 0; index < bindings.size(); ++index) { - const auto& binding = bindings[index]; - if (binding.wheelDirection == 0) continue; - pressed[index] = binding.wheelDirection == direction; - wheelReleaseTimes[index] = pressed[index] ? releaseTime : std::chrono::steady_clock::time_point{}; - } - publish(); - return; - } - const bool keyboard = event.type == SDL_KEYDOWN || event.type == SDL_KEYUP; - const bool mouse = event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP; - if (!keyboard && !mouse) return; - if (ignoreInput) return; - if (keyboard && event.key.repeat != 0) return; - const bool down = event.type == SDL_KEYDOWN || event.type == SDL_MOUSEBUTTONDOWN; - for (std::size_t index = 0; index < bindings.size(); ++index) { - const auto& binding = bindings[index]; - const bool matches = keyboard - ? binding.key != SDL_SCANCODE_UNKNOWN && binding.key == event.key.keysym.scancode - : binding.mouseButton != Pad::MouseButton::None && binding.mouseButton == static_cast(event.button.button); - - if (!matches) continue; - if (binding.control == Pad::InputControl::ToggleFullscreen) { - if (keyboard && down && !pressed[index] && window.Handle() != nullptr && event.key.windowID == SDL_GetWindowID(window.Handle())) window.ToggleFullscreen(); - } - if (binding.control == Pad::InputControl::ToggleMouse && down && !pressed[index]) setMouseMode(!mouseEnabled); - pressed[index] = down; - } - publish(); + const bool input = event.type == SDL_MOUSEWHEEL || event.type == SDL_MOUSEMOTION || event.type == SDL_KEYDOWN || event.type == SDL_KEYUP || + event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP; + if (!input || ignoreInput) return; + if (event.type == SDL_KEYDOWN && ImeCapturesKeyboard_nid_postfix()) return; + 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(window.FrameNumber()); } -void PadInput::Update() { +void PadInput::Update(const DisplayWindow& window) { if (controller != nullptr) SDL_GameControllerUpdate(); applyOutput(); - if (controller != nullptr) { - controllerState = sampleController(); - publish(); - } - const auto now = std::chrono::steady_clock::now(); - bool released = false; - for (std::size_t index = 0; index < bindings.size(); ++index) { - if (bindings[index].wheelDirection == 0 || !pressed[index] || now < wheelReleaseTimes[index]) continue; - pressed[index] = false; - wheelReleaseTimes[index] = {}; - released = true; - } - if (released) publish(); - if (!mouseEnabled) return; - if (SDL_GetKeyboardFocus() == nullptr) { - std::fill(pressed.begin(), pressed.end(), false); - std::fill(wheelReleaseTimes.begin(), wheelReleaseTimes.end(), std::chrono::steady_clock::time_point{}); + if (controller != nullptr) controllerState = sampleController(); + if (keyboardMouse.MouseMode() && SDL_GetKeyboardFocus() == nullptr) { + keyboardMouse.Release(); setMouseMode(false); - publish(); + publish(window.FrameNumber()); return; } - if (now < nextMousePoll) return; - nextMousePoll = now + std::chrono::milliseconds(Pad::MousePollIntervalMs); - int deltaX = 0; - int deltaY = 0; - SDL_GetRelativeMouseState(&deltaX, &deltaY); - mouseStick = {128, 128}; - if (deltaX != 0 || deltaY != 0) { - const double distance = std::hypot(deltaX, deltaY); - const double scale = std::clamp(distance * Pad::MouseSensitivity + 16.0, 64.0, 128.0) / distance; - const auto mapAxis = [scale](int delta) { return static_cast(std::clamp(128L + std::lround(delta * scale), 0L, 255L)); }; - mouseStick = {mapAxis(deltaX), mapAxis(deltaY)}; - } - publish(); + const bool changed = keyboardMouse.Advance(std::chrono::steady_clock::now()); + if (controller != nullptr || keyboardMouse.MouseMode() || changed) publish(window.FrameNumber()); } -void PadInput::publish() { - PadInputState state; - state.buttons = controllerState.buttons; - state.sticks = controllerState.sticks; - state.analogButtonsL2 = controllerState.analogButtonsL2; - state.analogButtonsR2 = controllerState.analogButtonsR2; - state.hasMotion = controllerState.hasMotion; - state.accel = controllerState.accel; - state.gyro = controllerState.gyro; - state.touch = controllerState.touch; - state.deviceKind = controllerState.deviceKind; - - std::array negative{}; - std::array positive{}; - for (std::size_t index = 0; index < bindings.size(); ++index) { - if (!pressed[index]) continue; - const auto& binding = bindings[index]; - switch (binding.control) { - case Pad::InputControl::Button: - state.buttons |= static_cast(binding.button); - if (binding.button == Pad::PadButton::L2) state.analogButtonsL2 = 255; - if (binding.button == Pad::PadButton::R2) state.analogButtonsR2 = 255; - break; - case Pad::InputControl::LeftStickLeft: negative[0] = true; break; - case Pad::InputControl::LeftStickRight: positive[0] = true; break; - case Pad::InputControl::LeftStickUp: negative[1] = true; break; - case Pad::InputControl::LeftStickDown: positive[1] = true; break; - case Pad::InputControl::RightStickLeft: negative[2] = true; break; - case Pad::InputControl::RightStickRight: positive[2] = true; break; - case Pad::InputControl::RightStickUp: negative[3] = true; break; - case Pad::InputControl::RightStickDown: positive[3] = true; break; - case Pad::InputControl::TouchLeft: state.touchLeft = true; break; - case Pad::InputControl::TouchRight: state.touchRight = true; break; - case Pad::InputControl::ToggleMouse: break; - case Pad::InputControl::ToggleFullscreen: break; - } - } - for (std::size_t axis = 0; axis < state.sticks.size(); ++axis) { - if (negative[axis] || positive[axis]) state.sticks[axis] = negative[axis] == positive[axis] ? 128 : negative[axis] ? 0 : 255; - } - if (mouseEnabled) { - state.sticks[2] = mouseStick[0]; - state.sticks[3] = mouseStick[1]; - } - if (state.analogButtonsL2 != 0) state.buttons |= static_cast(Pad::PadButton::L2); - if (state.analogButtonsR2 != 0) state.buttons |= static_cast(Pad::PadButton::R2); +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 4d590926fe..f308f42739 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -1,33 +1,42 @@ #include #include #include +#include #include #include #include #include #include +#include #include "SDL.h" #include "SDL_vulkan.h" #include "prx/libSceVideoOut/include/PadInput.hpp" #include "prx/libSceVideoOut/include/MouseInput.hpp" +#include "prx/libSceAgcDriver/Execution/include/AspectFit.hpp" #include "prx/libSceVideoOut/include/KeyboardInput.hpp" +#include "prx/libSceVideoOut/include/ImeTextInput.hpp" #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" namespace { +constexpr std::uint64_t FLIP_ROOM_WAIT_MICROS = 60000000; +constexpr std::uint64_t PRESENT_POLL_MICROS = 10000; + void require(bool condition, const char* reason) { if (!condition) throw std::runtime_error(std::string("VideoOut: ") + reason); } @@ -64,6 +73,30 @@ void checkConfig(const VideoOutConfig& cfg) { cfg.Check(); } +void resolveFlipBuffer(FlipRequest& request, const VideoOutConfig& cfg) { + if (request.index < 0) { + request.width = cfg.width; + request.height = cfg.height; + return; + } + request.buffer = cfg.buffers[request.index]; + if (!request.buffer.Occupied()) { + static std::array, VIDEO_OUT_BUFFER_NUM_MAX> reported{}; + if (!reported[request.index].exchange(true)) std::fprintf(stderr, "[videoout] GPU flip of unregistered buffer %d on handle %u presents black\n", request.index, request.outputHandle); + request.unregistered = true; + request.width = cfg.width; + request.height = cfg.height; + return; + } + require(request.buffer.groupIndex < VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX, "invalid buffer group"); + if (!request.groupCaptured) request.group = cfg.groups[request.buffer.groupIndex]; + require(request.group.occupied, "buffer group is not registered"); + require(request.buffer.dataAddress != 0, "null registered buffer address"); + static_cast(DescribeVideoOutBuffer(request.buffer, request.group)); + request.width = request.group.attribute.width; + request.height = request.group.attribute.height; +} + class RenderingWait final : public AgcDriver::IRenderingWait { std::shared_ptr _config; std::uint32_t _index; @@ -74,10 +107,10 @@ public: void Wait() override { std::unique_lock lock(_config->mutex); _config->vblankCond.wait(lock, _config->shutdownToken, [&] { - return _config->failure || _config->closing || !_config->opened || + return _config->failure || _config->Closed() || _config->bufferReuse[_index].IsComplete(_ticket); }); - checkConfig(*_config); + _config->CheckAlive(); } }; @@ -94,7 +127,7 @@ public: void WaitForFlipRoom() override { std::unique_lock queueLock(queue->mutex); - const bool room = queue->changed.wait_for(queueLock, std::chrono::seconds(60), [&] { + const bool room = queue->changed.WaitUntil(queueLock, TimedWait::DeadlineNanos(FLIP_ROOM_WAIT_MICROS), [&] { return queue->failure || queue->stopping || cfg->shutdownToken.stop_requested() || queue->reservations.load() < VIDEO_OUT_FLIP_QUEUE_CAPACITY; }); if (queue->failure) std::rethrow_exception(queue->failure); @@ -102,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"); @@ -116,28 +152,22 @@ 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); - if (info.index >= 0) { - request->buffer = cfg->buffers[info.index]; - require(request->buffer.Occupied(), "flip buffer is not registered"); - require(request->buffer.groupIndex < VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX, "invalid buffer group"); - request->group = cfg->groups[request->buffer.groupIndex]; - require(request->group.occupied, "buffer group is not registered"); - require(request->buffer.dataAddress != 0, "null registered buffer address"); - static_cast(DescribeVideoOutBuffer(request->buffer, request->group)); - request->width = request->group.attribute.width; - request->height = request->group.attribute.height; - } else { - request->width = cfg->width; - request->height = cfg->height; - } 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]; + if (info.index >= 0 && cfg->buffers[info.index].Occupied() && cfg->buffers[info.index].groupIndex < VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX && cfg->groups[cfg->buffers[info.index].groupIndex].occupied) { + request->group = cfg->groups[cfg->buffers[info.index].groupIndex]; + request->groupCaptured = true; + } request->reserved = true; return request; } @@ -153,7 +183,7 @@ public: if (!cfg->failure) cfg->failure = error; cfg->vblankCond.notify_all(); } - queue->changed.notify_all(); + queue->changed.NotifyAll(); } private: @@ -166,15 +196,27 @@ private: FlipRequest::~FlipRequest() { if (!reserved) return; std::lock_guard lock(cfg->mutex); - if (!terminal) { - --cfg->flipStatus.flipPendingNum; - --queue->reservations; - if (index >= 0) { - --cfg->bufferPending[index]; - cfg->bufferReuse[index].Complete(reuseTicket); - } - cfg->vblankCond.notify_all(); + ReleaseLocked(); +} + +void FlipRequest::ReleaseLocked() noexcept { + if (!reserved || terminal) return; + --cfg->flipStatus.flipPendingNum; + --queue->reservations; + if (index >= 0) { + --cfg->bufferPending[index]; + cfg->bufferReuse[index].Complete(reuseTicket); } + terminal = true; + cfg->vblankCond.notify_all(); +} + +void FlipRequest::Cancel() noexcept { + { + std::lock_guard lock(cfg->mutex); + ReleaseLocked(); + } + queue->changed.NotifyAll(); } void FlipRequest::GpuReady(const std::shared_ptr& frameTiming) { @@ -187,14 +229,19 @@ void FlipRequest::GpuReady(const std::shared_ptr& frameT if (queue->failure) std::rethrow_exception(queue->failure); if (queue->stopping || cfg->shutdownToken.stop_requested()) throw ProcessShutdown{}; std::lock_guard lock(cfg->mutex); - checkConfig(*cfg); + cfg->CheckAlive(); + if (cfg->Closed()) { + ReleaseLocked(); + return; + } require(reserved && !ready && !terminal && cfg->generation == generation, "invalid flip readiness transition"); + resolveFlipBuffer(*this, *cfg); readiness.Mark("locks_validate"); queuedAt = AgcDriver::FrameTiming::Clock::now(); queue->requests.push_back(shared_from_this()); ready = true; } - queue->changed.notify_all(); + queue->changed.NotifyAll(); // The worker is done once the request is queued: hardware does not stall the command processor // on a flip, and the title observes completion through flipPendingNum, which processFlip drops // once the frame's GPU work and its presentation have completed (see Driver::Present). A @@ -212,17 +259,8 @@ void FlipRequest::Fail(std::exception_ptr error) noexcept { if (!error) std::terminate(); std::lock_guard lock(cfg->mutex); if (!cfg->failure) cfg->failure = error; - if (reserved && !terminal) { - --cfg->flipStatus.flipPendingNum; - --queue->reservations; - if (index >= 0) { - --cfg->bufferPending[index]; - cfg->bufferReuse[index].Complete(reuseTicket); - } - terminal = true; - } - cfg->vblankCond.notify_all(); - queue->changed.notify_all(); + ReleaseLocked(); + queue->changed.NotifyAll(); } VideoOutDriver& VideoOutDriver::Get() { @@ -232,6 +270,13 @@ VideoOutDriver& VideoOutDriver::Get() { VideoOutDriver::VideoOutDriver() { SDL_SetHint(SDL_HINT_WINDOWS_DPI_AWARENESS, "permonitorv2"); +#ifdef _WIN32 + SDL_SetHintWithPriority(SDL_HINT_DIRECTINPUT_ENABLED, "0", SDL_HINT_DEFAULT); + 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"); +#endif if (SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) < 0) { throw std::runtime_error(std::string("SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: ") + SDL_GetError()); } @@ -239,19 +284,27 @@ VideoOutDriver::VideoOutDriver() { AgcDriverWaitIdle_nid_postfix(); std::promise started; auto attached = started.get_future(); - presentThread = std::jthread([this, &started](std::stop_token token) { presentLoop(token, started); }); + presentThread = std::jthread([this, &started](std::stop_token token) { + try { + AgcDriverRaiseWorkerThreadPriority_nid_postfix("presenter"); + } catch (...) { + started.set_exception(std::current_exception()); + return; + } + presentLoop(token, started); + }); attached.get(); vblankThread = std::jthread([this](std::stop_token token) { vblankLoop(token); }); LibcRegisterShutdown_nid_postfix([] { VideoOutDriver::Get().Shutdown(); }); } catch (...) { if (vblankThread.joinable()) { vblankThread.request_stop(); - flipQueue->changed.notify_all(); + flipQueue->changed.NotifyAll(); vblankThread.join(); } if (presentThread.joinable()) { presentThread.request_stop(); - flipQueue->changed.notify_all(); + flipQueue->changed.NotifyAll(); presentThread.join(); } SDL_QuitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER); @@ -274,7 +327,7 @@ void VideoOutDriver::Shutdown() { } presentThread.request_stop(); vblankThread.request_stop(); - flipQueue->changed.notify_all(); + flipQueue->changed.NotifyAll(); presentThread.join(); vblankThread.join(); { @@ -302,12 +355,13 @@ int VideoOutDriver::Open(int busType) { uint64_t generation = 1; if (previous) { std::lock_guard cfgLock(previous->mutex); - require(!previous->opened, "port already open"); + if (previous->opened) return -1; require(previous->generation != std::numeric_limits::max(), "port generation overflow"); generation = previous->generation + 1; } auto cfg = std::make_shared(); cfg->generation = generation; + cfg->busType = busType; cfg->opened = true; cfg->flipStatus.flipArg = -1; cfg->flipStatus.currentBuffer = -1; @@ -349,7 +403,7 @@ bool VideoOutDriver::close(int handle) { cfg->outputModeEvents.clear(); cfg->vrrStatusEvents.clear(); cfg->vblankCond.notify_all(); - flipQueue->changed.notify_all(); + flipQueue->changed.NotifyAll(); return true; } @@ -387,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; } @@ -412,13 +481,48 @@ void VideoOutDriver::triggerEvents(VideoOutConfig& cfg, int eventKind, void* tri } } -void VideoOutDriver::vblankEnd() { +bool VideoOutDriver::SupportsHighFrameRate() { + return GetDisplayRefreshRate() >= 120; +} + +int VideoOutDriver::GetDisplayRefreshRate() { + return displayRefreshRate.load(); +} + +void VideoOutDriver::updateDisplayRefreshRate() { + const int display = SDL_GetWindowDisplayIndex(window.Handle()); + if (display < 0) throw std::runtime_error(std::string("SDL_GetWindowDisplayIndex failed: ") + SDL_GetError()); + SDL_DisplayMode mode{}; + if (SDL_GetCurrentDisplayMode(display, &mode) != 0) throw std::runtime_error(std::string("SDL_GetCurrentDisplayMode failed: ") + SDL_GetError()); + displayRefreshRate.store(mode.refresh_rate); +} + +void VideoOutDriver::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) { if (!cfg) continue; std::lock_guard cfgLock(cfg->mutex); if (!cfg->opened || cfg->failure) continue; + if (cfg->outputMode != VIDEO_OUT_OUTPUT_MODE_119_88HZ && tick % 2 != 0) continue; require(cfg->vblankStatus.count != std::numeric_limits::max(), "vblank counter overflow"); + ++cfg->preVblankStatus.count; + cfg->preVblankStatus.processTime = sceKernelGetProcessTime(); + cfg->preVblankStatus.processTimeCounter = sceKernelGetProcessTimeCounter(); + triggerEvents(*cfg, VIDEO_OUT_EVENT_PRE_VBLANK_START, reinterpret_cast(cfg->preVblankStatus.count)); ++cfg->vblankStatus.count; cfg->vblankStatus.processTime = sceKernelGetProcessTime(); cfg->vblankStatus.processTimeCounter = sceKernelGetProcessTimeCounter(); @@ -433,7 +537,16 @@ void VideoOutDriver::processFlip(FlipRequest& req) { { std::unique_lock lock(req.cfg->mutex); timing.Mark("config_mutex_wait"); - checkConfig(*req.cfg); + req.cfg->CheckAlive(); + if (req.cfg->Closed()) { + req.ReleaseLocked(); + return; + } + 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"); @@ -441,34 +554,52 @@ void VideoOutDriver::processFlip(FlipRequest& req) { timing.Mark("validate"); req.cfg->vblankCond.wait(lock, req.cfg->shutdownToken, [&] { return req.cfg->vblankStatus.count >= target || req.cfg->failure || req.cfg->closing; }); timing.Mark("vblank_wait"); - checkConfig(*req.cfg); + req.cfg->CheckAlive(); + if (req.cfg->Closed()) { + req.ReleaseLocked(); + return; + } req.cfg->lastFlipVblank = req.cfg->vblankStatus.count; } require(req.width != 0 && req.height != 0 && req.width <= static_cast(std::numeric_limits::max()) && req.height <= static_cast(std::numeric_limits::max()), "invalid window dimensions"); - window.Ensure(req.width, req.height); - const auto extensions = windowExtensions(window.Handle()); - const AgcDriver::PresentationWindow target{window.Handle(), extensions, &createWindowSurface, &windowDrawableSize, req.width, req.height, req.timing}; - timing.Mark("window_prepare"); - const auto gpuReady = [](void* context) { - auto& request = *static_cast(context); - std::lock_guard lock(request.cfg->mutex); - checkConfig(*request.cfg); - require(!request.terminal && !request.gpuComplete, "invalid GPU completion transition"); - request.gpuComplete = true; - request.cfg->vblankCond.notify_all(); - }; - if (req.index >= 0) { - const auto display = DescribeVideoOutBuffer(req.buffer, req.group); - AgcDriverPresentBuffer_nid_postfix(target, display, gpuReady, &req); + if (req.cfg->busType == VIDEO_OUT_BUS_TYPE_OVERLAY) { + std::lock_guard lock(req.cfg->mutex); + req.cfg->CheckAlive(); + if (!req.cfg->Closed()) { + require(!req.terminal && !req.gpuComplete, "invalid GPU completion transition"); + req.gpuComplete = true; + } } else { - AgcDriverPresentClear_nid_postfix(target, req.index == VIDEO_OUT_BUFFER_INDEX_BLACK, gpuReady, &req); + window.Ensure(req.width, req.height); + const auto extensions = windowExtensions(window.Handle()); + const AgcDriver::PresentationWindow target{window.Handle(), extensions, &createWindowSurface, &windowDrawableSize, req.width, req.height, req.timing}; + timing.Mark("window_prepare"); + const auto gpuReady = [](void* context) { + auto& request = *static_cast(context); + std::lock_guard lock(request.cfg->mutex); + request.cfg->CheckAlive(); + if (request.cfg->Closed()) return; + require(!request.terminal && !request.gpuComplete, "invalid GPU completion transition"); + request.gpuComplete = true; + request.cfg->vblankCond.notify_all(); + }; + if (req.index >= 0 && !req.unregistered) { + const auto display = DescribeVideoOutBuffer(req.buffer, req.group); + AgcDriverPresentBuffer_nid_postfix(target, display, gpuReady, &req); + } else { + AgcDriverPresentClear_nid_postfix(target, req.unregistered || req.index == VIDEO_OUT_BUFFER_INDEX_BLACK, gpuReady, &req); + } + timing.Mark("present"); + window.UpdateTitle(); + timing.Mark("window_title"); } - timing.Mark("present"); - window.UpdateTitle(); - timing.Mark("window_title"); std::lock_guard lock(req.cfg->mutex); timing.Mark("completion_mutex_wait"); - checkConfig(*req.cfg); + req.cfg->CheckAlive(); + if (req.cfg->Closed()) { + req.ReleaseLocked(); + return; + } require(!req.terminal && req.cfg->generation == req.generation, "flip cancelled during presentation"); require(req.gpuComplete, "flip submitted before GPU completion"); require(req.cfg->flipStatus.count != std::numeric_limits::max(), "flip counter overflow"); @@ -477,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; @@ -488,15 +624,20 @@ void VideoOutDriver::processFlip(FlipRequest& req) { } req.terminal = true; req.cfg->vblankCond.notify_all(); + KernelWakeSleepers_nid_no_patch(); timing.Mark("notify_game"); } void VideoOutDriver::presentLoop(std::stop_token token, std::promise& started) { 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; @@ -513,10 +654,11 @@ void VideoOutDriver::presentLoop(std::stop_token token, std::promise& star PadInput padInput; MouseInput mouseInput; KeyboardInput keyboardInput; + ImeTextInput imeTextInput; while (!token.stop_requested()) { { std::unique_lock lock(flipQueue->mutex); - flipQueue->changed.wait_for(lock, std::chrono::milliseconds(10), [&] { return token.stop_requested() || flipQueue->failure || !flipQueue->requests.empty(); }); + flipQueue->changed.WaitUntil(lock, TimedWait::DeadlineNanos(PRESENT_POLL_MICROS), [&] { return token.stop_requested() || flipQueue->failure || !flipQueue->requests.empty(); }); if (flipQueue->failure) std::rethrow_exception(flipQueue->failure); if (token.stop_requested()) break; if (!flipQueue->requests.empty()) { @@ -535,14 +677,25 @@ void VideoOutDriver::presentLoop(std::stop_token token, std::promise& star if (window.Handle() != nullptr) { mouseInput.HandleEvent(event, SDL_GetWindowID(window.Handle())); keyboardInput.HandleEvent(event, SDL_GetWindowID(window.Handle())); + imeTextInput.HandleEvent(event, SDL_GetWindowID(window.Handle())); } } - padInput.Update(); + padInput.Update(window); + updateDisplayRefreshRate(); + pollHostHdr(); + mouseInput.UpdateCapture(!padInput.MouseModeEnabled()); if (current) { require(current->timing != nullptr, "missing presentation timing"); const auto dequeued = AgcDriver::FrameTiming::Clock::now(); current->timing->Add(current->timing->Get("VideoOut", "queue"), dequeued - current->queuedAt); processFlip(*current); + int windowWidth = 0; + int windowHeight = 0; + if (window.Handle() != nullptr) SDL_GetWindowSize(window.Handle(), &windowWidth, &windowHeight); + if (windowWidth > 0 && windowHeight > 0) { + const auto presented = AgcDriver::ComputeContainSize_nid_postfix(current->width, current->height, static_cast(windowWidth), static_cast(windowHeight), true); + mouseInput.SetPresentation(current->width, current->height, presented.width, presented.height); + } const auto finished = AgcDriver::FrameTiming::Clock::now(); AgcDriver::FrameTiming::Clock::duration interval{}; { @@ -557,7 +710,7 @@ void VideoOutDriver::presentLoop(std::stop_token token, std::promise& star } if (current) { current.reset(); - flipQueue->changed.notify_all(); + flipQueue->changed.NotifyAll(); } } } catch (const ProcessShutdown&) { @@ -593,11 +746,12 @@ 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(); } void VideoOutDriver::vblankLoop(std::stop_token token) { - using Frame = std::chrono::duration>; + using Frame = std::chrono::duration>; const auto start = std::chrono::steady_clock::now(); try { for (int64_t frame = 1; !token.stop_requested(); ++frame) { @@ -608,7 +762,7 @@ void VideoOutDriver::vblankLoop(std::stop_token token) { std::lock_guard lock(flipQueue->mutex); if (token.stop_requested() || flipQueue->failure) return; } - vblankEnd(); + vblankEnd(static_cast(frame)); } } catch (const std::exception& error) { std::fprintf(stderr, "[videoout] vblank failed: %s\n", error.what()); diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index 9f4f5718f5..d23a315114 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -7,6 +7,7 @@ #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include "prx/libc/include/Shutdown.hpp" +#include "tests/VideoOutTestEnvironment.hpp" #include #include #include @@ -14,12 +15,12 @@ #include #include #include -#include -#include +#include #include #include #include #include +#include #include #undef main @@ -87,8 +88,16 @@ void testLifetime(bool reopen) { std::lock_guard lock(cfg->mutex); check(cfg->flipStatus.flipPendingNum == 1 && cfg->flipStatus.count == 0, "reservation status is wrong"); } + auto smaller = attribute; + smaller.width = 32; + smaller.height = 32; + check(sceVideoOutSubmitChangeBufferAttribute2(handle, 0, &smaller, nullptr) == 0, "attributes of a buffer with a pending flip could not be changed"); + { + std::lock_guard lock(cfg->mutex); + check(cfg->groups[0].attribute.width == 32 && cfg->groups[0].attribute.height == 32 && cfg->flipStatus.flipPendingNum == 1, "the attribute change did not reach later flips or dropped the pending one"); + } attribute.dcc_control = 1; - check(expectFailure([&] { sceVideoOutSubmitChangeBufferAttribute2(handle, 0, &attribute, nullptr); }).find("pending flip") != std::string::npos, "pending attributes were changed"); + check(expectFailure([&] { sceVideoOutSubmitChangeBufferAttribute2(handle, 0, &attribute, nullptr); }).find("DCC") != std::string::npos, "an invalid attribute change was accepted"); check(expectFailure([&] { sceVideoOutUnregisterBuffers(handle, 0); }).find("pending flip") != std::string::npos, "pending buffer was unregistered"); check(sceVideoOutWaitVblank(handle) == 0 && sceVideoOutWaitVblank(handle) == 0, "vblank stalled behind the GPU queue"); std::shared_ptr replacement; @@ -99,24 +108,198 @@ void testLifetime(bool reopen) { check(replacement != cfg && replacement->generation > cfg->generation, "reopen reused old port state"); } gate->Release(); - std::exception_ptr failure; { std::unique_lock lock(cfg->mutex); - check(cfg->vblankCond.wait_for(lock, std::chrono::seconds(10), [&] { return cfg->failure != nullptr; }), "flip failure did not wake waiters"); - failure = cfg->failure; - check(cfg->flipStatus.count == 0 && cfg->flipStatus.flipPendingNum == 0, "failed flip has successful or pending status"); + check(cfg->vblankCond.wait_for(lock, std::chrono::seconds(10), [&] { return cfg->flipStatus.flipPendingNum == 0; }), "closed port kept its pending flip"); + check(cfg->failure == nullptr, "closing a port failed the driver"); + check(cfg->flipStatus.count == 0, "cancelled flip has successful status"); } - const auto message = expectFailure([&] { std::rethrow_exception(failure); }); - check(message.find("closed") != std::string::npos, "flip used a closed port"); if (replacement) { std::lock_guard lock(replacement->mutex); check(replacement->flipStatus.flipPendingNum == 0 && replacement->flipStatus.count == 0, "old request changed new port counters"); + } else { + check(expectFailure([&] { sceVideoOutWaitVblank(handle); }).find("closed") != std::string::npos, "closed port accepted a vblank wait"); } - check(expectFailure([&] { sceVideoOutWaitVblank(handle); }).find("closed") != std::string::npos, "VideoOut lost worker failure"); AgcDriverUnregisterVideoOutput_nid_postfix(7, gate); if (reopen) sceVideoOutClose(handle); - const auto shutdown = expectFailure([] { LibcRunShutdown_nid_postfix(); }); - check(shutdown.find("closed") != std::string::npos, "shutdown lost asynchronous error"); + 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); + auto gate = std::make_shared(); + AgcDriverRegisterVideoOutput_nid_postfix(7, gate); + std::array held{0xc004105c, 7, 0xfffffffeu, 1, 0, 0}; + Packet heldPacket{held.data(), 6, 0, {}}; + check(sceAgcDriverSubmitDcb(&heldPacket) == 0, "the gated flip was not accepted"); + std::array flip{0xc004105c, static_cast(handle), 0, 1, 3, 0}; + Packet flipPacket{flip.data(), 6, 0, {}}; + check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "a GPU flip of a buffer registered before the GPU reaches it was rejected at submit"); + 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"); + gate->Release(); + { + std::unique_lock lock(cfg->mutex); + const bool done = cfg->vblankCond.wait_for(lock, std::chrono::seconds(30), [&] { return cfg->failure || cfg->flipStatus.count == 1; }); + if (cfg->failure) std::rethrow_exception(cfg->failure); + check(done, "the deferred flip did not complete"); + check(cfg->flipStatus.flipArg == 3 && cfg->flipStatus.currentBuffer == 0, "the deferred flip presented the wrong request"); + } + AgcDriverWaitIdle_nid_postfix(); + sceVideoOutUnregisterBuffers(handle, 0); + sceVideoOutClose(handle); + AgcDriverUnregisterVideoOutput_nid_postfix(7, gate); + 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); + 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"); + check(sceVideoOutUnregisterBuffers(handle, 0) == 0, "buffer unregistration failed"); + std::array flip{0xc004105c, static_cast(handle), 0, 1, 7, 0}; + Packet flipPacket{flip.data(), 6, 0, {}}; + check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "a GPU flip of an unregistered buffer was rejected at submit"); + { + std::unique_lock lock(cfg->mutex); + const bool done = cfg->vblankCond.wait_for(lock, std::chrono::seconds(30), [&] { return cfg->failure || cfg->flipStatus.count == 1; }); + if (cfg->failure) std::rethrow_exception(cfg->failure); + check(done, "a GPU flip of an unregistered buffer did not complete"); + check(cfg->flipStatus.flipPendingNum == 0 && cfg->flipStatus.flipArg == 7 && cfg->bufferPending[0] == 0, "a GPU flip of an unregistered buffer left pending state"); + } + AgcDriverWaitIdle_nid_postfix(); + sceVideoOutClose(handle); + LibcRunShutdown_nid_postfix(); } std::size_t tiledOffset(uint32_t x, uint32_t y, uint32_t width) { @@ -180,6 +363,16 @@ void testDecode() { group.attribute.option = option; check(expectFailure([&] { DescribeVideoOutBuffer(registered, group); }).find("buffer option " + std::to_string(option)) != std::string::npos, "an unsupported buffer option was accepted"); } + sceVideoOutSetBufferAttribute2(&group.attribute, 0x8000000000000000ull, 0, 259, 137, VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_NONE, 0, 0); + sceVideoOutAddBufferAttributeOption(&group.attribute, VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_STRICT_COLORIMETRY); + check(group.attribute.option == VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_STRICT_COLORIMETRY && group.attribute.width == 259 && group.attribute.height == 137, "an added buffer option was not stored"); + sceVideoOutAddBufferAttributeOption(&group.attribute, VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_STRICT_COLORIMETRY); + check(group.attribute.option == VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_STRICT_COLORIMETRY, "adding a buffer option twice changed it"); + check(DescribeVideoOutBuffer(registered, group).address == plain.address, "a buffer with an added supported option was rejected"); + sceVideoOutAddBufferAttributeOption(&group.attribute, 0x20); + 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"); 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; @@ -235,7 +428,8 @@ void testControls() { check(handle >= 0, "open with the highest priority on every CPU failed"); const auto cfg = VideoOutDriver::Get().GetConfig(handle); check(sceVideoOutIsOutputSupported(handle, VIDEO_OUT_OUTPUT_MODE_DEFAULT, nullptr, nullptr, 0) == 1, "default output mode is unsupported"); - check(sceVideoOutIsOutputSupported(handle, VIDEO_OUT_OUTPUT_MODE_119_88HZ, nullptr, nullptr, 0) == VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE, "unavailable output mode is supported"); + const int highFrameRate = sceVideoOutIsOutputSupported(handle, VIDEO_OUT_OUTPUT_MODE_119_88HZ, nullptr, nullptr, 0); + check(highFrameRate == 1 || highFrameRate == VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE, "unexpected high-frame-rate support result"); check(sceVideoOutConfigureOutput(handle, VIDEO_OUT_OUTPUT_MODE_119_88HZ, nullptr, nullptr, 0) == VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE, "unavailable output mode was configured"); check(sceVideoOutIsOutputSupported(handle, 0xd000000aull, nullptr, nullptr, 0) == VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE, "unknown output mode is supported"); check(sceVideoOutConfigureOutput(handle, 0xd000000aull, nullptr, nullptr, 0) == VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE, "unknown output mode was configured"); @@ -243,7 +437,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; @@ -254,6 +451,14 @@ void testControls() { check(owner->GetTriggeredEvents(&event, 1) == 1, "initial output mode event missing"); int64_t mode = 0; check(sceVideoOutGetEventData(&event, &mode) == 0 && mode == VIDEO_OUT_OUTPUT_MODE_DEFAULT && sceVideoOutGetEventCount(&event) == 1, "initial output mode event encoding is wrong"); + check(sceVideoOutDeleteOutputModeEvent(queue, handle) == 0, "output mode unsubscription failed"); + { + std::lock_guard lock(cfg->mutex); + check(cfg->outputModeEvents.empty(), "output mode unsubscription did not clear registration"); + } + check(sceVideoOutDeleteOutputModeEvent(queue, handle) == 0, "duplicate output mode unsubscription failed"); + expectFailure([&] { sceVideoOutDeleteOutputModeEvent(0, handle); }); + expectFailure([&] { sceVideoOutDeleteOutputModeEvent(queue, -1); }); check(sceVideoOutAddVblankEvent(queue, handle, nullptr) == 0 && sceVideoOutAddVblankEvent(queue, handle, &settings) == 0, "vblank subscription failed"); { std::lock_guard lock(cfg->mutex); @@ -266,6 +471,13 @@ void testControls() { } check(sceVideoOutWaitVblank(handle) == 0, "vblank wait failed"); 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"); + check(sceVideoOutDeletePreVblankStartEvent(queue, handle) == 0, "pre-vblank unsubscription failed"); std::vector allocation(65536 + 65535); const auto storage = alignedBuffer(allocation); VideoOutBuffers buffer{storage.data(), nullptr, {allocation.data(), storage.data()}}; @@ -280,7 +492,23 @@ void testControls() { vrrStatus.ident = VIDEO_OUT_EVENT_VRR_STATUS; vrrStatus.filter = EVFILT_VIDEO_OUT; check(sceVideoOutGetEventId(&vrrStatus) == VIDEO_OUT_EVENT_VRR_STATUS, "VRR status event id rejected"); + check(sceVideoOutVrrPegToFixedRate(handle, 0, 0) == 0, "VRR peg on an open handle failed"); + check(sceVideoOutVrrUnpegFromFixedRate(handle) == 0, "VRR unpeg on an open handle failed"); + check(sceVideoOutDeleteVrrActiveStatusEvent(queue, handle) == 0, "VRR status unsubscription failed"); + { + std::lock_guard lock(cfg->mutex); + check(cfg->vrrStatusEvents.empty(), "VRR status unsubscription did not clear registration"); + } + check(sceVideoOutDeleteVrrActiveStatusEvent(queue, handle) == 0, "duplicate VRR status unsubscription failed"); + expectFailure([&] { sceVideoOutDeleteVrrActiveStatusEvent(0, handle); }); + expectFailure([&] { sceVideoOutDeleteVrrActiveStatusEvent(queue, -1); }); + expectFailure([&] { sceVideoOutVrrPegToFixedRate(handle, 1, 0); }); + expectFailure([&] { sceVideoOutVrrPegToFixedRate(handle, 0, 1); }); + expectFailure([&] { sceVideoOutVrrPegToFixedRate(handle, 0x100000000ull, 0); }); + expectFailure([&] { sceVideoOutVrrPegToFixedRate(handle, 0, 0x100000000ull); }); sceVideoOutClose(handle); + expectFailure([&] { sceVideoOutVrrPegToFixedRate(handle, 0, 0); }); + expectFailure([&] { sceVideoOutVrrUnpegFromFixedRate(handle); }); check(owner->GetTriggeredEvents(&event, 1) == 0, "closed port retained pending events"); check(sceKernelDeleteEqueue(queue) == 0, "event queue deletion failed"); LibcRunShutdown_nid_postfix(); @@ -325,6 +553,58 @@ void testPresentation() { LibcRunShutdown_nid_postfix(); } +void testOverlay() { + const int mainHandle = sceVideoOutOpen(255, 0, 0, nullptr); + const int overlayHandle = sceVideoOutOpen(255, 1, 0, nullptr); + check(mainHandle != overlayHandle, "the overlay bus shares the main port"); + auto mainCfg = VideoOutDriver::Get().GetConfig(mainHandle); + auto overlayCfg = VideoOutDriver::Get().GetConfig(overlayHandle); + std::vector mainAllocation(6 * 65536 + 65535); + std::vector overlayAllocation(6 * 65536 + 65535); + const auto mainStorage = alignedBuffer(mainAllocation); + const auto overlayStorage = alignedBuffer(overlayAllocation); + fillBuffer(mainStorage, 259, 137); + fillBuffer(overlayStorage, 259, 137); + VideoOutBuffers mainBuffer{mainStorage.data(), nullptr, {nullptr, nullptr}}; + VideoOutBuffers overlayBuffer{overlayStorage.data(), nullptr, {nullptr, nullptr}}; + VideoOutBufferAttribute2 attribute{}; + sceVideoOutSetBufferAttribute2(&attribute, 0x8000000000000000ull, 0, 259, 137, 0, 0, 0); + check(sceVideoOutRegisterBuffers2(mainHandle, 0, 0, &mainBuffer, 1, &attribute, 0, nullptr) == 0, "main buffer registration failed"); + check(sceVideoOutRegisterBuffers2(overlayHandle, 0, 0, &overlayBuffer, 1, &attribute, 0, nullptr) == 0, "overlay buffer registration failed"); + const auto flip = [](int handle, const std::shared_ptr& cfg, std::int64_t argument) { + std::uint64_t target; + { + std::lock_guard lock(cfg->mutex); + target = cfg->flipStatus.count + 1; + } + check(sceVideoOutSubmitFlip(handle, 0, 1, argument) == 0, "the flip was not accepted"); + std::unique_lock lock(cfg->mutex); + check(cfg->vblankCond.wait_for(lock, std::chrono::seconds(15), [&] { return (cfg->failure && cfg->flipStatus.flipPendingNum == 0) || cfg->flipStatus.count == target; }), "the flip did not complete"); + if (cfg->failure) std::rethrow_exception(cfg->failure); + check(cfg->flipStatus.flipArg == argument && cfg->flipStatus.currentBuffer == 0 && cfg->flipStatus.flipPendingNum == 0, "flip status is wrong"); + }; + const auto presentsAfter = [](std::uint64_t previous) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (AgcDriver::VulkanDevice::PresentCounts().presents == previous && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(10)); + return AgcDriver::VulkanDevice::PresentCounts().presents; + }; + const auto start = AgcDriver::VulkanDevice::PresentCounts().presents; + flip(mainHandle, mainCfg, 1); + const auto afterMain = presentsAfter(start); + check(afterMain > start, "a main-bus flip was not presented"); + flip(overlayHandle, overlayCfg, 2); + flip(overlayHandle, overlayCfg, 3); + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + check(AgcDriver::VulkanDevice::PresentCounts().presents == afterMain, "an overlay-bus flip replaced the main output"); + flip(mainHandle, mainCfg, 4); + check(presentsAfter(afterMain) > afterMain, "a main-bus flip after overlay flips was not presented"); + sceVideoOutUnregisterBuffers(overlayHandle, 0); + sceVideoOutUnregisterBuffers(mainHandle, 0); + sceVideoOutClose(overlayHandle); + sceVideoOutClose(mainHandle); + LibcRunShutdown_nid_postfix(); +} + void testUnavailable() { const auto message = expectFailure([] { sceVideoOutOpen(255, 0, 0, nullptr); }); check(expectFailure([] { sceVideoOutOpen(255, 0, 0, nullptr); }) == message, "a second open lost the presentation failure"); @@ -402,7 +682,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}; @@ -424,13 +709,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() { @@ -444,7 +733,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); @@ -453,14 +742,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); @@ -470,8 +759,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, {}}; @@ -482,6 +771,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(); }; @@ -490,7 +788,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); @@ -510,6 +808,181 @@ 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"); + check(sceVideoOutAddFlipEvent(eq, 0, nullptr) == VIDEO_OUT_ERROR_INVALID_HANDLE, "flip event on an unopened port was accepted"); + check(sceVideoOutAddVblankEvent(eq, 1, nullptr) == VIDEO_OUT_ERROR_INVALID_HANDLE, "vblank event on an unopened port was accepted"); + const int handle = sceVideoOutOpen(255, 0, 0, nullptr); + check(sceVideoOutAddFlipEvent(eq, handle, nullptr) == 0, "flip event on an open port was refused"); + sceVideoOutClose(handle); + check(sceVideoOutAddFlipEvent(eq, handle, nullptr) == VIDEO_OUT_ERROR_INVALID_HANDLE, "flip event on a closed port was accepted"); + check(sceKernelDeleteEqueue(eq) == 0, "the event queue was not deleted"); + 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); @@ -544,9 +1017,8 @@ void testOneDevice() { } int main(int argc, char** argv) { + VideoOutTestEnvironment environment; try { - std::filesystem::create_directories("app0/sce_sys"); - std::ofstream("app0/sce_sys/param.json", std::ios::binary) << R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":"Example"}},"downloadDataSize":0})"; if (argc == 2 && std::string(argv[1]) == "decode") testDecode(); else if (argc == 2 && std::string(argv[1]) == "controls") testControls(); else if (argc == 2 && std::string(argv[1]) == "present") testPresentation(); @@ -555,7 +1027,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; diff --git a/core/libs/prx/libSceVideoOut/tests/FrameDumpQueue.cpp b/core/libs/prx/libSceVideoOut/tests/FrameDumpQueue.cpp new file mode 100644 index 0000000000..a5105da79f --- /dev/null +++ b/core/libs/prx/libSceVideoOut/tests/FrameDumpQueue.cpp @@ -0,0 +1,183 @@ +#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/libc/include/Shutdown.hpp" +#include "tests/VideoOutTestEnvironment.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#undef main + +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; +constexpr int Frames = 4; + +void check(bool condition, const char* reason) { + if (!condition) throw std::runtime_error(reason); +} + +std::string frameName(int index) { + char name[32]; + std::snprintf(name, sizeof(name), "frame_%03d.bmp", index); + return name; +} + +class HeldDump final { + int descriptor; + +public: + explicit HeldDump(int index) { + const auto name = frameName(index); + check(mkfifo(name.c_str(), 0600) == 0, "a frame dump pipe could not be created"); + descriptor = open(name.c_str(), O_RDWR); + check(descriptor >= 0, "a frame dump pipe could not be opened"); + } + + ~HeldDump() { close(descriptor); } + + HeldDump(const HeldDump&) = delete; + HeldDump& operator=(const HeldDump&) = delete; + + std::vector Release() const { + std::vector dump(DumpBytes); + for (std::size_t done = 0; done < dump.size();) { + pollfd waiting{descriptor, POLLIN, 0}; + check(poll(&waiting, 1, 15000) == 1, "a queued frame dump was not written"); + const auto count = read(descriptor, dump.data() + done, dump.size() - done); + check(count > 0, "a frame dump pipe could not be read"); + done += static_cast(count); + } + return dump; + } +}; + +std::vector readDump(int index) { + std::ifstream file(frameName(index), std::ios::binary); + check(file.is_open(), "a frame dump is missing"); + return {std::istreambuf_iterator(file), std::istreambuf_iterator()}; +} + +void checkDump(std::span dump, std::span decoded) { + check(dump.size() == DumpBytes, "a frame dump has the wrong size"); + std::uint32_t extent[2]; + std::memcpy(extent, dump.data() + 18, sizeof(extent)); + check(extent[0] == DumpWidth && extent[1] == static_cast(-static_cast(DumpHeight)), "a frame dump has the wrong extent"); + for (std::uint32_t y = 0; y < DumpHeight; ++y) { + for (std::uint32_t x = 0; x < DumpWidth; ++x) { + const auto dumped = dump.data() + 54 + (static_cast(y) * DumpWidth + x) * 4; + const auto source = decoded.data() + (static_cast(y) * Scale * Width + static_cast(x) * Scale) * 4; + check(std::memcmp(dumped, source, 4) == 0, "a frame dump pixel differs from the display buffer"); + } + } +} + +void testDumpQueue() { + const int handle = sceVideoOutOpen(255, 0, 0, nullptr); + auto cfg = VideoOutDriver::Get().GetConfig(handle); + constexpr auto tiledBytes = static_cast((Width + 127) / 128) * ((Height + 127) / 128) * 65536; + constexpr auto linearBytes = static_cast(Width) * Height * 4; + const auto makeStorage = [](std::size_t bytes, std::vector& allocation) { + allocation.resize(bytes + 65535); + const auto address = reinterpret_cast(allocation.data()); + auto* aligned = allocation.data() + ((65536u - (address & 65535u)) & 65535u); + return std::span{aligned, bytes}; + }; + std::vector tiledAllocation; + std::vector linearAllocation; + const auto tiledStorage = makeStorage(tiledBytes, tiledAllocation); + const auto linearStorage = makeStorage(linearBytes, linearAllocation); + const AgcDriver::DisplayBuffer tiled{reinterpret_cast(tiledStorage.data()), 0x8000000000000000ull, Width, Height}; + const AgcDriver::DisplayBuffer linear{reinterpret_cast(linearStorage.data()), 0x8000000000000000ull, Width, Height, 1}; + check(AgcDriver::DisplayBufferSize(tiled) == tiledStorage.size(), "incorrect padded tiled display footprint"); + check(AgcDriver::DisplayBufferSize(linear) == linearStorage.size(), "incorrect linear display footprint"); + const auto fill = [](const AgcDriver::DisplayBuffer& display, std::span storage) { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto offset = AgcDriver::DisplayBufferOffset(display, x, y); + storage[offset] = static_cast(x & 255u); + storage[offset + 1] = static_cast(y & 255u); + storage[offset + 2] = static_cast((x >> 8u) | ((y >> 8u) << 4u)); + storage[offset + 3] = std::byte{255}; + } + } + }; + fill(tiled, tiledStorage); + fill(linear, linearStorage); + const auto tiledPixels = AgcDriver::ReadDisplayBuffer(tiled); + const auto linearPixels = AgcDriver::ReadDisplayBuffer(linear); + VideoOutBuffers tiledBuffer{tiledStorage.data(), nullptr, {nullptr, nullptr}}; + VideoOutBuffers linearBuffer{linearStorage.data(), nullptr, {nullptr, nullptr}}; + VideoOutBufferAttribute2 tiledAttribute{}; + VideoOutBufferAttribute2 linearAttribute{}; + sceVideoOutSetBufferAttribute2(&tiledAttribute, tiled.pixelFormat, 0, Width, Height, 0, 0, 0); + sceVideoOutSetBufferAttribute2(&linearAttribute, linear.pixelFormat, 1, Width, Height, 0, 0, 0); + check(sceVideoOutRegisterBuffers2(handle, 0, 0, &tiledBuffer, 1, &tiledAttribute, 0, nullptr) == 0, "tiled buffer registration failed"); + check(sceVideoOutRegisterBuffers2(handle, 1, 1, &linearBuffer, 1, &linearAttribute, 0, nullptr) == 0, "linear buffer registration failed"); + const HeldDump first(0); + const HeldDump last(Frames - 1); + std::exception_ptr failure; + try { + for (int frame = 0; frame < Frames; ++frame) { + const int index = frame % 2; + check(sceVideoOutSubmitFlip(handle, index, 1, frame) == 0, "the flip was not accepted"); + std::unique_lock lock(cfg->mutex); + check(cfg->vblankCond.wait_for(lock, std::chrono::seconds(15), [&] { return (cfg->failure && cfg->flipStatus.flipPendingNum == 0) || cfg->flipStatus.count == static_cast(frame) + 1; }), "the flip did not complete"); + if (cfg->failure) std::rethrow_exception(cfg->failure); + } + } catch (...) { + failure = std::current_exception(); + } + const auto firstDump = first.Release(); + if (failure) std::rethrow_exception(failure); + checkDump(firstDump, tiledPixels); + const auto lastDump = last.Release(); + checkDump(lastDump, linearPixels); + checkDump(readDump(1), linearPixels); + checkDump(readDump(2), tiledPixels); + sceVideoOutUnregisterBuffers(handle, 0); + sceVideoOutUnregisterBuffers(handle, 1); + sceVideoOutClose(handle); + LibcRunShutdown_nid_postfix(); +} + +} + +int main() { + VideoOutTestEnvironment environment; + try { + testDumpQueue(); + std::puts("VideoOut frame dump queue test passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + try { LibcRunShutdown_nid_postfix(); } + catch (const std::exception& shutdown) { std::fprintf(stderr, "shutdown: %s\n", shutdown.what()); } + if (std::string(error.what()).find("Vulkan support") != std::string::npos && !std::getenv("ANYPS5_REQUIRE_DISPLAY")) { + std::printf("skipped, no display or Vulkan device: %s\n", error.what()); + return 77; + } + return 1; + } +} 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 new file mode 100644 index 0000000000..3497443fcc --- /dev/null +++ b/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp @@ -0,0 +1,300 @@ +#include "prx/libScePad/include/InputMapping.hpp" +#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); +} + +std::filesystem::path root; + +void useConfig(const std::string& text) { + const auto path = root / "anyps5-input.ini"; + { + std::ofstream file(path, std::ios::binary | std::ios::trunc); + 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 +} + +bool rejects(const std::string& text) { + useConfig(text); + try { + Pad::LoadInputMapping(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +std::uint8_t plainStickByte(std::int16_t axis) { + return static_cast(((static_cast(axis) + 32768) * 255 + 32767) / 65535); +} + +void settingsDefault() { + useConfig("Cross = KEY:F\n"); + const auto configuration = Pad::LoadInputMapping(); + require(configuration.settings.leftStickDeadzonePercent == 0 && configuration.settings.rightStickDeadzonePercent == 0, "deadzones default to 0"); + require(configuration.settings.mouseSensitivity == Pad::DefaultMouseSensitivity, "mouse sensitivity defaults to 1"); +} + +void settingsParse() { + useConfig("leftstickdeadzone = 12 ; worn stick\nRightStickDeadzone=5\nMouseSensitivity = 2.5\nCross = KEY:F\n"); + const auto configuration = Pad::LoadInputMapping(); + require(configuration.settings.leftStickDeadzonePercent == 12, "left deadzone is read case-insensitively"); + require(configuration.settings.rightStickDeadzonePercent == 5, "right deadzone is read"); + require(configuration.settings.mouseSensitivity == 2.5, "mouse sensitivity is read"); + bool crossOnF = false; + for (const auto& binding : configuration.bindings) crossOnF = crossOnF || (binding.control == Pad::InputControl::Button && binding.button == Pad::PadButton::Cross && binding.key == SDL_SCANCODE_F); + require(crossOnF, "bindings are read alongside settings"); +} + +void settingsReject() { + require(rejects("LeftStickDeadzone = 91\n"), "a deadzone above 90 is rejected"); + require(rejects("LeftStickDeadzone = -1\n"), "a negative deadzone is rejected"); + require(rejects("LeftStickDeadzone = 7.5\n"), "a fractional deadzone is rejected"); + require(rejects("RightStickDeadzone =\n"), "an empty deadzone is rejected"); + require(rejects("MouseSensitivity = 0\n"), "a zero mouse sensitivity is rejected"); + require(rejects("MouseSensitivity = 10.5\n"), "a mouse sensitivity above 10 is rejected"); + require(rejects("MouseSensitivity = fast\n"), "a non-numeric mouse sensitivity is rejected"); + 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; + require(settings.mouseVerticalScale == 1.0 && settings.mouseCurve == 1.0 && settings.mouseMaxSpeed == 1.0, "mouse look scales default to 1"); + require(settings.mouseDeadzoneOffset == 0.2 && settings.mouseSmoothingMs == 10.0 && settings.mouseDecayMs == 30.0, "mouse look timing and offset defaults"); +} + +void mouseLookParse() { + useConfig("MouseCurve=0.7 ; softer start\nmousedeadzoneoffset = 0.15\nMouseMaxSpeed = 1.5\nMouseSmoothingMs = 0\nMouseDecayMs = 50\nMouseVerticalScale = 0.5\n"); + const auto settings = Pad::LoadInputMapping().settings; + require(settings.mouseCurve == 0.7, "mouse curve is read"); + require(settings.mouseDeadzoneOffset == 0.15, "mouse deadzone offset is read case-insensitively"); + require(settings.mouseMaxSpeed == 1.5, "mouse max speed is read"); + require(settings.mouseSmoothingMs == 0.0, "a 0 smoothing is accepted"); + require(settings.mouseDecayMs == 50.0, "mouse decay is read"); + require(settings.mouseVerticalScale == 0.5, "mouse vertical scale is read"); +} + +void mouseLookReject() { + require(rejects("MouseCurve = 0.1\n"), "a mouse curve below 0.2 is rejected"); + require(rejects("MouseCurve = steep\n"), "a non-numeric mouse curve is rejected"); + require(rejects("MouseDeadzoneOffset = 0.95\n"), "a deadzone offset above 0.9 is rejected"); + require(rejects("MouseDecayMs =\n"), "an empty decay is rejected"); + require(rejects("MouseDecayMs = 0\n"), "a zero decay is rejected"); + require(rejects("MouseSmoothingMs = 501\n"), "a smoothing above 500 is rejected"); + require(rejects("MouseMaxSpeed = nan\n"), "a non-finite max speed is rejected"); + require(rejects("MouseVerticalScale = 11\n"), "a vertical scale above 10 is rejected"); + require(rejects("MouseDecayMs = 20\nmousedecayms = 30\n"), "a mouse setting given twice is rejected"); +} + +void 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 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); + require(stick[0] == plainStickByte(axis) && stick[1] == plainStickByte(axis), "a 0 deadzone keeps the raw mapping"); + } +} + +void deadzoneShapesSticks() { + require(Pad::StickWithDeadzone(3000, -3000, 15) == std::array{128, 128}, "a small deflection inside the deadzone is centered"); + require(Pad::StickWithDeadzone(32767, 0, 15) == std::array{255, 128}, "a full deflection still reaches the edge"); + require(Pad::StickWithDeadzone(-32768, 0, 15) == std::array{0, 128}, "a full negative deflection still reaches the edge"); + require(Pad::StickWithDeadzone(32767, 32767, 15) == std::array{255, 255}, "a full diagonal still reaches the corner"); + const auto justOutside = Pad::StickWithDeadzone(5100, 0, 15); + require(justOutside[0] >= 128 && justOutside[0] <= 130 && justOutside[1] == 128, "the output starts at the center just outside the deadzone"); + const auto half = Pad::StickWithDeadzone(16384, 0, 20); + require(half[0] > 128 && half[0] < plainStickByte(16384), "a deflection outside the deadzone is rescaled"); + bool rejected = false; + try { + Pad::StickWithDeadzone(0, 0, 91); + } catch (const std::invalid_argument&) { + rejected = true; + } + require(rejected, "an out-of-range deadzone is rejected"); +} + +} + +int main() try { + root = std::filesystem::temp_directory_path() / ("anyps5_input_mapping-" + std::to_string(std::random_device{}())); + std::filesystem::create_directories(root); + settingsDefault(); + settingsParse(); + settingsReject(); + performanceOverlayBinding(); + screenshotBinding(); + mouseLookDefault(); + mouseLookParse(); + mouseLookReject(); + stickModifier(); + keyboardRamp(); + triggerRamp(); + toggleMouseDefaults(); + nonAsciiPaths(); + deadzoneOffMatchesRawSticks(); + deadzoneShapesSticks(); + std::filesystem::remove_all(root); + std::cout << "input mapping tests passed\n"; + return 0; +} catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; +} diff --git a/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp b/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp new file mode 100644 index 0000000000..e4d9c6892f --- /dev/null +++ b/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp @@ -0,0 +1,574 @@ +#include "prx/libScePad/include/InputMapping.hpp" +#include "prx/libScePad/include/KeyboardMouse.hpp" +#include "PadSimulation.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using PadSimulation::At; +using PadSimulation::KeyEvent; +using PadSimulation::Sample; +using PadSimulation::Simulate; +using PadSimulation::TimedEvent; + +constexpr double Polls[] = {33.0, 8.0, 1.0}; + +void check(bool condition, const std::string& reason) { + if (!condition) throw std::runtime_error(reason); +} + +Pad::InputConfiguration defaults() { + return Pad::InputConfiguration{{Pad::InputMapping.begin(), Pad::InputMapping.end()}, {}}; +} + +Pad::InputConfiguration withSettings(const std::function& change) { + auto configuration = defaults(); + change(configuration.settings); + return configuration; +} + +double axis(std::uint8_t value) { + return value < 128 ? (static_cast(value) - 128.0) / 128.0 : (static_cast(value) - 128.0) / 127.0; +} + +double rightMagnitude(const Sample& sample) { + return std::hypot(axis(sample.state.sticks[2]), axis(sample.state.sticks[3])); +} + +double meanRightX(const std::vector& samples, double fromMs, double toMs) { + double sum = 0.0; + int count = 0; + for (const auto& sample : samples) { + if (sample.ms <= fromMs || sample.ms > toMs) continue; + sum += axis(sample.state.sticks[2]); + ++count; + } + check(count > 0, "no samples in window"); + return sum / count; +} + +double heldMeanRightX(const std::vector& samples, double pollMs, double fromMs, double toMs) { + double sum = 0.0; + for (const auto& sample : samples) { + if (sample.ms >= fromMs - 1e-9 && sample.ms < toMs - 1e-9) sum += axis(sample.state.sticks[2]) * pollMs; + } + return sum / (toMs - fromMs); +} + +std::vector mouseOnThenMotion(double durationMs, double countsPerSecondX, double countsPerSecondY = 0.0) { + std::vector events{PadSimulation::MouseModeAt(0.0, true)}; + PadSimulation::AppendMotion(events, 0.0, durationMs, countsPerSecondX, countsPerSecondY); + return events; +} + +double expectedDeflection(const Pad::InputSettings& settings, double normalizedSpeed) { + const double clamped = std::min(normalizedSpeed, settings.mouseMaxSpeed); + return settings.mouseDeadzoneOffset + (1.0 - settings.mouseDeadzoneOffset) * std::pow(std::min(clamped, 1.0), settings.mouseCurve); +} + +void testCenteredWithoutMotion() { + for (const double poll : Polls) { + const auto samples = Simulate(defaults(), {PadSimulation::MouseModeAt(0.0, true)}, poll, 300.0); + for (const auto& sample : samples) { + check(sample.state.sticks[2] == 128 && sample.state.sticks[3] == 128, "a resting mouse leaves the stick centered"); + } + } +} + +void testSteadyDeflectionFollowsCurve(const Pad::InputConfiguration& configuration, const char* name) { + const auto& settings = configuration.settings; + for (const double speed : {0.05, 0.25, 0.5, 0.75, 0.95, 3.0}) { + const double countsPerSecond = speed * Pad::MouseFullTiltCountsPerSecond / settings.mouseSensitivity; + const double expected = expectedDeflection(settings, speed); + for (const double poll : Polls) { + const auto samples = Simulate(configuration, mouseOnThenMotion(600.0, countsPerSecond), poll, 600.0); + const double measured = meanRightX(samples, 300.0, 600.0); + if (std::abs(measured - expected) > 0.02) { + throw std::runtime_error(std::string(name) + ": speed " + std::to_string(speed) + " at " + std::to_string(poll) + " ms polls gives " + + std::to_string(measured) + ", expected " + std::to_string(expected)); + } + } + } +} + +void testDeflectionCurve() { + testSteadyDeflectionFollowsCurve(defaults(), "defaults"); + testSteadyDeflectionFollowsCurve(withSettings([](auto& s) { s.mouseCurve = 2.0; s.mouseDeadzoneOffset = 0.1; s.mouseSensitivity = 2.0; }), "quadratic"); + testSteadyDeflectionFollowsCurve(withSettings([](auto& s) { s.mouseCurve = 0.5; s.mouseDeadzoneOffset = 0.0; }), "square root"); +} + +void testDeadzoneOffset() { + const auto configuration = defaults(); + for (const double poll : Polls) { + const auto samples = Simulate(configuration, mouseOnThenMotion(400.0, 40.0), poll, 400.0); + bool moved = false; + for (const auto& sample : samples) { + const double magnitude = rightMagnitude(sample); + if (magnitude == 0.0) continue; + moved = true; + check(magnitude >= configuration.settings.mouseDeadzoneOffset - 0.01, "the slowest movement still clears the deadzone offset at " + std::to_string(poll) + " ms polls"); + } + check(moved, "slow movement moves the stick"); + } + const auto samples = Simulate(withSettings([](auto& s) { s.mouseDeadzoneOffset = 0.0; }), mouseOnThenMotion(400.0, 40.0), 8.0, 400.0); + check(meanRightX(samples, 200.0, 400.0) < 0.05, "without an offset slow movement stays small"); +} + +double centeredAfter(const std::vector& samples, double stopMs) { + double lastMoving = stopMs; + double previous = 2.0; + for (const auto& sample : samples) { + if (sample.ms <= stopMs) continue; + const double magnitude = rightMagnitude(sample); + check(magnitude <= previous + 1e-9, "the stick never grows after the mouse stops"); + previous = magnitude; + if (magnitude != 0.0) lastMoving = sample.ms; + } + return lastMoving - stopMs; +} + +void testDecay() { + const auto configuration = defaults(); + for (const double speed : {0.3, 1.0}) { + for (const double poll : Polls) { + const auto samples = Simulate(configuration, mouseOnThenMotion(300.0, speed * Pad::MouseFullTiltCountsPerSecond), poll, 600.0); + const double tail = centeredAfter(samples, 300.0); + check(tail < configuration.settings.mouseDecayMs + poll, "stick centered within decay plus one poll at " + std::to_string(poll) + " ms polls, took " + std::to_string(tail)); + check(samples.back().state.sticks[2] == 128 && samples.back().state.sticks[3] == 128, "stick returns exactly to center"); + } + } + const auto slow = withSettings([](auto& s) { s.mouseDecayMs = 120.0; }); + const auto samples = Simulate(slow, mouseOnThenMotion(300.0, Pad::MouseFullTiltCountsPerSecond), 1.0, 600.0); + const double tail = centeredAfter(samples, 300.0); + check(tail >= 100.0 && tail < 121.0, "a longer decay keeps the stick moving longer"); +} + +void testMaxSpeedClamp() { + const auto configuration = defaults(); + constexpr double StopMs = 264.0; + for (const double poll : Polls) { + const auto flick = Simulate(configuration, mouseOnThenMotion(StopMs, 10.0 * Pad::MouseFullTiltCountsPerSecond), poll, 500.0); + const auto full = Simulate(configuration, mouseOnThenMotion(StopMs, 1.5 * Pad::MouseFullTiltCountsPerSecond), poll, 500.0); + check(flick[flick.size() / 3].state.sticks[2] == 255, "a fast flick reaches full tilt"); + check(centeredAfter(flick, StopMs) <= centeredAfter(full, StopMs) + 1e-9, "a flick faster than full tilt does not extend the tail"); + double flickSum = 0.0; + double fullSum = 0.0; + for (std::size_t index = 0; index < flick.size(); ++index) { + if (flick[index].ms <= StopMs) continue; + flickSum += rightMagnitude(flick[index]); + fullSum += rightMagnitude(full[index]); + } + check(std::abs(flickSum - fullSum) <= 0.02 * std::max(1.0, fullSum), "the stick decays from a flick like from full tilt at " + std::to_string(poll) + " ms polls"); + } + const auto headroom = withSettings([](auto& s) { s.mouseMaxSpeed = 3.0; }); + const auto flick = Simulate(headroom, mouseOnThenMotion(StopMs, 10.0 * Pad::MouseFullTiltCountsPerSecond), 1.0, 500.0); + const auto full = Simulate(defaults(), mouseOnThenMotion(StopMs, 10.0 * Pad::MouseFullTiltCountsPerSecond), 1.0, 500.0); + check(rightMagnitude(flick[275]) > rightMagnitude(full[275]), "a higher max speed lets a flick hold full tilt longer"); +} + +void testSmoothing() { + const auto instant = Simulate(withSettings([](auto& s) { s.mouseSmoothingMs = 0.0; }), mouseOnThenMotion(100.0, Pad::MouseFullTiltCountsPerSecond / 2.0), 8.0, 100.0); + check(std::abs(axis(instant[1].state.sticks[2]) - expectedDeflection(defaults().settings, 0.5)) < 0.02, "without smoothing the stick follows at once"); + const auto smoothed = Simulate(defaults(), mouseOnThenMotion(100.0, Pad::MouseFullTiltCountsPerSecond / 2.0), 1.0, 100.0); + const double early = (axis(smoothed[9].state.sticks[2]) - 0.2) / 0.8; + check(early > 0.5 * 0.55 && early < 0.5 * 0.72, "smoothing reaches about 63 percent after one time constant"); +} + +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)}; + for (int segment = 0; segment < 200; ++segment) { + const double speed = (base + swing * std::sin(2.0 * Pi * segment * 8.0 / 800.0)) * Pad::MouseFullTiltCountsPerSecond; + PadSimulation::AppendMotion(events, segment * 8.0, 8.0, speed, 0.3 * speed); + } + PadSimulation::AppendMotion(events, 1700.0, 200.0, 250.0, -120.0); + return events; +} + +double integratedRightMagnitude(const std::vector& samples, double pollMs) { + double sum = 0.0; + for (const auto& sample : samples) sum += rightMagnitude(sample) * pollMs / 1000.0; + return sum; +} + +void testPollRateIndependence() { + for (const auto& [name, events] : {std::pair{"one-way wave", waveMotion(0.5, 0.3)}, std::pair{"back-and-forth wave", waveMotion(0.0, 0.6)}}) { + std::vector> runs; + for (const double poll : Polls) runs.push_back(Simulate(defaults(), events, poll, 2112.0)); + const double reference = integratedRightMagnitude(runs[2], Polls[2]); + for (std::size_t run = 0; run < runs.size(); ++run) { + const double integrated = integratedRightMagnitude(runs[run], Polls[run]); + check(std::abs(integrated - reference) <= 0.02 * reference, std::string(name) + ": total deflection at " + std::to_string(Polls[run]) + " ms polls is " + + std::to_string(integrated) + ", 1 ms gives " + std::to_string(reference)); + const double tolerance = 0.03 + 0.0025 * Polls[run]; + for (double window = 0.0; window + 264.0 <= 2112.0; window += 264.0) { + const double mean = heldMeanRightX(runs[run], Polls[run], window, window + 264.0); + const double expected = heldMeanRightX(runs[2], Polls[2], window, window + 264.0); + check(std::abs(mean - expected) < tolerance, std::string(name) + ": held deflection at " + std::to_string(Polls[run]) + " ms polls from " + + std::to_string(window) + " ms is " + std::to_string(mean) + ", 1 ms gives " + std::to_string(expected)); + } + } + } +} + +void testVerticalScale() { + const auto configuration = withSettings([](auto& s) { s.mouseVerticalScale = 0.5; s.mouseDeadzoneOffset = 0.0; }); + const auto samples = Simulate(configuration, mouseOnThenMotion(300.0, 0.0, 0.8 * Pad::MouseFullTiltCountsPerSecond), 8.0, 300.0); + const double y = axis(samples.back().state.sticks[3]); + check(std::abs(y - 0.4) < 0.02, "vertical scale halves the vertical deflection"); +} + +void testMouseModeOff() { + std::vector events; + PadSimulation::AppendMotion(events, 0.0, 300.0, Pad::MouseFullTiltCountsPerSecond, 0.0); + for (const auto& sample : Simulate(defaults(), events, 8.0, 300.0)) { + check(sample.state.sticks[2] == 128, "motion without mouse mode leaves the stick alone"); + } + events.insert(events.begin(), At(0.0, PadSimulation::ButtonEvent(SDL_BUTTON_MIDDLE, true))); + events.insert(events.begin() + 1, At(0.0, PadSimulation::ButtonEvent(SDL_BUTTON_MIDDLE, false))); + const auto samples = Simulate(defaults(), events, 8.0, 300.0); + check(samples.back().state.sticks[2] == 255, "middle click turns mouse mode on"); +} + +void testKeyboardSticksUnchanged() { + const std::vector events{ + At(0.0, KeyEvent(SDL_SCANCODE_W, true)), + At(10.0, KeyEvent(SDL_SCANCODE_D, true)), + At(20.0, KeyEvent(SDL_SCANCODE_A, true)), + At(30.0, KeyEvent(SDL_SCANCODE_W, false)), + At(30.0, KeyEvent(SDL_SCANCODE_D, false)), + At(40.0, KeyEvent(SDL_SCANCODE_A, false))}; + const auto samples = Simulate(defaults(), events, 1.0, 50.0); + check(samples[4].state.sticks[1] == 0 && samples[4].state.sticks[0] == 128, "W tilts the left stick fully up"); + check(samples[14].state.sticks[0] == 255, "D tilts the left stick fully right"); + check(samples[24].state.sticks[0] == 128, "A and D together cancel"); + check(samples[34].state.sticks[0] == 0 && samples[34].state.sticks[1] == 128, "A alone tilts fully left"); + check(samples[44].state.sticks[0] == 128, "released keys center the stick"); +} + +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); + check((samples[40].state.buttons & up) != 0, "wheel up presses Up"); + check((samples[120].state.buttons & up) == 0, "wheel press releases after its duration"); +} + + +void testWheelFocusLoss() { + const auto samples = Simulate(defaults(), {At(0.0, PadSimulation::WheelEvent(1)), At(10.0, KeyEvent(SDL_SCANCODE_W, true)), At(20.0, PadSimulation::FocusLostEvent())}, 1.0, 40.0); + check(samples[15].state.sticks[1] == 0, "a held key tilts the stick"); + check(samples[30].state.sticks[1] == 128, "losing focus lets go of held keys"); + check((samples[30].state.buttons & static_cast(Pad::PadButton::Up)) == 0, "losing focus lets go of the wheel press"); +} + +void testFocusLossReleasesMouse() { + Pad::KeyboardMouse keyboardMouse(defaults()); + keyboardMouse.SetMouseMode(true); + const auto requests = keyboardMouse.HandleEvent(PadSimulation::FocusLostEvent(), Pad::KeyboardMouse::Clock::now()); + check(requests.releaseMouse, "losing focus asks to release the captured mouse"); + keyboardMouse.SetMouseMode(false); + check(!keyboardMouse.HandleEvent(PadSimulation::FocusLostEvent(), Pad::KeyboardMouse::Clock::now()).releaseMouse, "nothing to release without capture"); +} + +void 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"); +} + +} + +int main() { + try { + const std::pair> tests[] = { + {"centered without motion", testCenteredWithoutMotion}, + {"deflection curve", testDeflectionCurve}, + {"deadzone offset", testDeadzoneOffset}, + {"decay", testDecay}, + {"max speed clamp", testMaxSpeedClamp}, + {"smoothing", testSmoothing}, + {"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(); + std::printf("pad_keyboard_mouse: %s passed\n", name); + } + } catch (const std::exception& error) { + std::fprintf(stderr, "pad_keyboard_mouse: %s\n", error.what()); + return 1; + } + return 0; +} diff --git a/core/libs/prx/libSceVideoOut/tests/MouseInput.cpp b/core/libs/prx/libSceVideoOut/tests/MouseInput.cpp new file mode 100644 index 0000000000..759c9708e7 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/tests/MouseInput.cpp @@ -0,0 +1,127 @@ +#include "prx/libSceVideoOut/include/MouseInput.hpp" +#include "prx/libSceMouse/include/mouse_structs.h" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SDL.h" +#include "SDL_events.h" +#include "SDL_mouse.h" +#include +#include +#include +#undef main + +extern "C" { +int APS5_VABI sceMouseInit(); +int APS5_VABI sceMouseOpen(int userId, std::int32_t type, std::int32_t index, const void* param); +int APS5_VABI sceMouseRead(std::int32_t handle, MouseData* data, std::int32_t num); +int APS5_VABI sceMouseClose(std::int32_t handle); +} + +namespace { + +void Require(bool condition, const char* reason) { + if (!condition) { + std::fprintf(stderr, "mouse input: %s\n", reason); + std::abort(); + } +} + +SDL_Event Motion(unsigned windowId, int xrel, int yrel) { + SDL_Event event{}; + event.type = SDL_MOUSEMOTION; + event.motion.windowID = windowId; + event.motion.xrel = xrel; + event.motion.yrel = yrel; + return event; +} + +SDL_Event Window(unsigned windowId, std::uint8_t kind) { + SDL_Event event{}; + event.type = SDL_WINDOWEVENT; + event.window.windowID = windowId; + event.window.event = kind; + return event; +} +void CaptureDecision(MouseInput& input, std::int32_t handle) { + Require(input.CaptureWanted(true), "a focused window with an open mouse does not want the capture"); + Require(!input.CaptureWanted(false), "the pad's mouse-to-stick toggle does not release the capture"); + input.HandleEvent(Window(3, SDL_WINDOWEVENT_FOCUS_LOST), 3); + Require(!input.CaptureWanted(true), "losing focus does not release the capture"); + input.HandleEvent(Window(3, SDL_WINDOWEVENT_FOCUS_GAINED), 3); + Require(input.CaptureWanted(true), "regaining focus does not restore the capture"); + input.HandleEvent(Window(4, SDL_WINDOWEVENT_FOCUS_LOST), 3); + Require(input.CaptureWanted(true), "another window's focus loss released the capture"); + Require(sceMouseClose(handle) == 0, "close failed"); + Require(!input.CaptureWanted(true), "a closed mouse keeps the capture"); + Require(sceMouseOpen(255, 0, 0, nullptr) >= 0, "reopen failed"); + Require(input.CaptureWanted(true), "a reopened mouse does not want the capture"); + input.HandleEvent(Window(3, SDL_WINDOWEVENT_CLOSE), 3); + Require(!input.CaptureWanted(true), "a closed window keeps the capture"); + input.HandleEvent(Window(3, SDL_WINDOWEVENT_FOCUS_GAINED), 3); +} +void CaptureApplied(MouseInput& input) { + SDL_SetHint("SDL_VIDEODRIVER", "dummy"); + if (SDL_Init(SDL_INIT_VIDEO) != 0) { + std::fprintf(stderr, "mouse input: SDL video unavailable (%s): capture application not checked\n", SDL_GetError()); + return; + } + input.UpdateCapture(true); + const bool applied = SDL_GetRelativeMouseMode() == SDL_TRUE; + input.UpdateCapture(false); + 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(); +} +void Sum(std::int32_t handle, std::int32_t& x, std::int32_t& y) { + MouseData data[MOUSE_MAX_DATA_NUM]{}; + const int count = sceMouseRead(handle, data, MOUSE_MAX_DATA_NUM); + Require(count >= 0, "read failed"); + x = 0; + y = 0; + for (int index = 0; index < count; ++index) { + x += data[index].x_axis; + y += data[index].y_axis; + } +} + +} + +int main() { + Require(sceMouseInit() == 0, "init failed"); + const std::int32_t handle = sceMouseOpen(255, 0, 0, nullptr); + Require(handle >= 0, "open failed"); + MouseInput input; + std::int32_t x = 0; + std::int32_t y = 0; + + input.HandleEvent(Motion(3, 10, -7), 3); + input.HandleEvent(Motion(4, 100, 100), 3); + Sum(handle, x, y); + Require(x == 10 && y == -7, "unscaled deltas are not passed through or another window's motion was not ignored"); + + input.SetPresentation(1920, 1080, 1843, 1037); + for (int step = 0; step < 5; ++step) input.HandleEvent(Motion(3, 10, -7), 3); + Sum(handle, x, y); + Require(x == 52 && y == -36, "deltas are not scaled by the presented image with sub-pixel carry"); + + input.SetPresentation(1920, 1080, 960, 540); + input.HandleEvent(Motion(3, 1, 1), 3); + input.HandleEvent(Motion(3, 1, 1), 3); + Sum(handle, x, y); + Require(x == 4 && y == 4, "a half-size presentation does not double the deltas"); + + input.SetPresentation(0, 0, 0, 0); + input.HandleEvent(Motion(3, 3, 3), 3); + Sum(handle, x, y); + Require(x == 6 && y == 6, "an empty presentation changed the scale"); + CaptureDecision(input, handle); + CaptureApplied(input); + std::puts("mouse input tests passed"); + return 0; +} diff --git a/core/libs/prx/libSceVideoOut/tests/OutputModePolicy.cpp b/core/libs/prx/libSceVideoOut/tests/OutputModePolicy.cpp new file mode 100644 index 0000000000..5a4a600944 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/tests/OutputModePolicy.cpp @@ -0,0 +1,43 @@ +#include "prx/libSceVideoOut/include/Output.hpp" +#include +#include +#include + +static void Require(bool value, const char* reason) { + if (!value) { + std::fprintf(stderr, "%s\n", reason); + std::abort(); + } +} + +int main() { + constexpr int unsupported = static_cast(0x80290016); + constexpr int invalidValue = static_cast(0x80290001); + constexpr int unknownMode = static_cast(0x8029001e); + VideoOutOutputOptions options{}; + Require(sceVideoOutInitializeOutputOptions(&options) == 0, "output options initialization failed"); + for (int refresh : {-1, 0, 30, 59, 60, 119, 120, 144, 240}) { + Require(ValidateVideoOutOutputMode_nid_no_patch(15, &options, refresh) == (refresh < 120 ? unsupported : 0), "mode 15 did not follow the display refresh threshold"); + Require(ValidateVideoOutOutputMode_nid_no_patch(15, nullptr, refresh) == (refresh < 120 ? unsupported : 0), "mode 15 without options did not follow the display refresh threshold"); + Require(ValidateVideoOutOutputMode_nid_no_patch(1, &options, refresh) == 0, "default mode depended on the display refresh rate"); + } + for (std::uint64_t mode : {4, 7, 8, 12, 13, 14, 16, 17, 19}) { + Require(ValidateVideoOutOutputMode_nid_no_patch(mode, &options, 240) == unsupported, "unimplemented output mode was accepted on a fast display"); + } + Require(ValidateVideoOutOutputMode_nid_no_patch(0xd000000a, &options, 240) == unknownMode, "unknown output mode was accepted"); + VideoOutOutputOptions changed = options; + changed.internalData[0] = 0; + Require(ValidateVideoOutOutputMode_nid_no_patch(15, &changed, 120) == invalidValue, "mode 15 accepted invalid output options"); + Require(ValidateVideoOutOutputMode_nid_no_patch(15, &changed, 60) == unsupported, "invalid options changed unsupported-mode error precedence"); + changed = options; + changed.internalData[3] = 0xffffffff; + Require(ValidateVideoOutOutputMode_nid_no_patch(15, &changed, 120) == 0, "mode 15 rejected the free options word"); + bool thrown = false; + try { + ValidateVideoOutOutputMode_nid_no_patch(18, &options, 120); + } catch (const std::runtime_error&) { + thrown = true; + } + Require(thrown, "unsettled mode 18 did not throw"); + +} diff --git a/core/libs/prx/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/PadSimulation.hpp b/core/libs/prx/libSceVideoOut/tests/PadSimulation.hpp new file mode 100644 index 0000000000..75e0168901 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/tests/PadSimulation.hpp @@ -0,0 +1,129 @@ +#ifndef CORE_LIBS_PRX_LIBSCEVIDEOOUT_TESTS_PADSIMULATION_HPP +#define CORE_LIBS_PRX_LIBSCEVIDEOOUT_TESTS_PADSIMULATION_HPP + +#include "prx/libScePad/include/KeyboardMouse.hpp" + +#include +#include +#include + +#include "SDL_keyboard.h" + +namespace PadSimulation { + +struct TimedEvent { + double ms = 0.0; + SDL_Event event{}; + bool setMouseMode = false; + bool mouseMode = false; + bool release = false; +}; + +struct Sample { + double ms = 0.0; + PadInputState state; +}; + +inline SDL_Event MotionEvent(int deltaX, int deltaY) { + SDL_Event event{}; + event.type = SDL_MOUSEMOTION; + event.motion.xrel = deltaX; + event.motion.yrel = deltaY; + return event; +} + +inline SDL_Event KeyEvent(SDL_Scancode key, bool down) { + SDL_Event event{}; + event.type = down ? SDL_KEYDOWN : SDL_KEYUP; + event.key.keysym.scancode = key; + event.key.state = down ? SDL_PRESSED : SDL_RELEASED; + return event; +} + +inline SDL_Event ButtonEvent(Uint8 button, bool down) { + SDL_Event event{}; + event.type = down ? SDL_MOUSEBUTTONDOWN : SDL_MOUSEBUTTONUP; + event.button.button = button; + event.button.state = down ? SDL_PRESSED : SDL_RELEASED; + return event; +} + +inline SDL_Event FocusLostEvent() { + SDL_Event event{}; + event.type = SDL_WINDOWEVENT; + event.window.event = SDL_WINDOWEVENT_FOCUS_LOST; + return event; +} + +inline SDL_Event WheelEvent(int direction) { + SDL_Event event{}; + event.type = SDL_MOUSEWHEEL; + event.wheel.y = direction; + event.wheel.direction = SDL_MOUSEWHEEL_NORMAL; + return event; +} + +inline TimedEvent At(double ms, const SDL_Event& event) { + return {ms, event, false, false, false}; +} + +inline TimedEvent MouseModeAt(double ms, bool enabled) { + return {ms, SDL_Event{}, true, enabled, false}; +} + +inline TimedEvent ReleaseAt(double ms) { + return {ms, SDL_Event{}, false, false, true}; +} + +inline void AppendMotion(std::vector& events, double startMs, double durationMs, double countsPerSecondX, double countsPerSecondY, double reportHz = 1000.0) { + const double periodMs = 1000.0 / reportHz; + double owedX = 0.0; + double owedY = 0.0; + for (double ms = startMs + periodMs; ms <= startMs + durationMs + 1e-9; ms += periodMs) { + owedX += countsPerSecondX * periodMs / 1000.0; + owedY += countsPerSecondY * periodMs / 1000.0; + const int deltaX = static_cast(std::trunc(owedX)); + const int deltaY = static_cast(std::trunc(owedY)); + owedX -= deltaX; + owedY -= deltaY; + if (deltaX != 0 || deltaY != 0) events.push_back(At(ms, MotionEvent(deltaX, deltaY))); + } +} + +inline std::vector Simulate(const Pad::InputConfiguration& configuration, const std::vector& events, double pollMs, double endMs) { + using Clock = Pad::KeyboardMouse::Clock; + const auto origin = Clock::time_point{} + std::chrono::hours(1); + const auto at = [origin](double ms) { + return origin + std::chrono::duration_cast(std::chrono::duration(ms)); + }; + Pad::KeyboardMouse keyboardMouse(configuration); + keyboardMouse.Advance(at(0.0)); + std::vector samples; + std::size_t next = 0; + const auto steps = static_cast(std::floor(endMs / pollMs + 1e-9)); + for (long long step = 1; step <= steps; ++step) { + const double ms = static_cast(step) * pollMs; + const auto now = at(ms); + for (; next < events.size() && events[next].ms <= ms + 1e-9; ++next) { + const auto& timed = events[next]; + if (timed.release) { + keyboardMouse.Release(); + continue; + } + if (timed.setMouseMode) { + keyboardMouse.SetMouseMode(timed.mouseMode); + continue; + } + const auto requests = keyboardMouse.HandleEvent(timed.event, now); + if (requests.toggleMouse) keyboardMouse.SetMouseMode(!keyboardMouse.MouseMode()); + if (requests.releaseMouse) keyboardMouse.SetMouseMode(false); + } + keyboardMouse.Advance(now); + samples.push_back({ms, keyboardMouse.Compose(PadInputState{})}); + } + return samples; +} + +} + +#endif diff --git a/core/libs/prx/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/Export.cpp b/core/libs/prx/libSceVideoRecordingP/Export.cpp index 7d1cb61a29..6d2fe29052 100644 --- a/core/libs/prx/libSceVideoRecordingP/Export.cpp +++ b/core/libs/prx/libSceVideoRecordingP/Export.cpp @@ -16,12 +16,33 @@ std::map> g_info; extern "C" { +void APS5_VABI _sceVideoRecordingQueryParam_nid_postfix(void* 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 sceVideoRecordingCloseByHandle(int handle, int discard) { + (void)handle; + (void)discard; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +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(int32_t info, void* data, size_t size) { if (data == nullptr) APS5_INVALID_ARG_EX; std::lock_guard lock(g_infoMutex); @@ -32,11 +53,42 @@ int APS5_VABI sceVideoRecordingGetInfo(int32_t info, void* data, size_t size) { return 0; } +int APS5_VABI sceVideoRecordingGetInfoByHandle(int 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) { NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI sceVideoRecordingGetSupportInfo(void* info) { + (void)info; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoRecordingGetVideoOutMode(void* 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 void* param, void* heap, int heapSize) { (void)path; (void)param; @@ -46,12 +98,51 @@ int APS5_VABI sceVideoRecordingOpen(const char* path, const void* param, void* h return 0; } +int APS5_VABI sceVideoRecordingOpen2(const char* path, const void* param, void* heap, int heapSize) { + (void)path; + (void)param; + (void)heap; + (void)heapSize; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoRecordingQueryHddSize2(const void* param) { + (void)param; + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceVideoRecordingQueryMemSize(const void* param) { (void)param; NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI sceVideoRecordingQueryMemSize2(const void* 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(int32_t info, const void* data, size_t size) { if (data == nullptr && size != 0) APS5_INVALID_ARG_EX; const auto* bytes = static_cast(data); @@ -70,6 +161,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..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceVideoStreamingEngine/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/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..56919ce362 --- /dev/null +++ b/core/libs/prx/libSceVideodec/Export.cpp @@ -0,0 +1,415 @@ +#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(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{}; +}; + +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; + 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; + 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(void) { + NotImplemented_nid_no_patch(__func__); + 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 a4d91f714d..db8822b8eb 100644 --- a/core/libs/prx/libSceVideodec2/Export.cpp +++ b/core/libs/prx/libSceVideodec2/Export.cpp @@ -1,3 +1,5 @@ +#include +#include #include #include #include @@ -110,11 +112,175 @@ 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{}; }; struct FrameDeleter { @@ -128,6 +294,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 +304,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); @@ -147,6 +319,7 @@ struct Decoder { std::mutex lock; std::map> decoders; +std::map pictureOwners; std::uint64_t nextDecoder = 1; std::uint64_t nextQueue = 1; @@ -159,12 +332,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 @@ -195,30 +376,65 @@ 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; +} + void Receive(Decoder& decoder) { for (;;) { FramePointer frame(av_frame_alloc()); @@ -240,14 +456,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) { @@ -266,7 +498,7 @@ 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(Decoder& decoder, FrameBuffer* frame, OutputInfo* output) { +void EmitDecoded(std::uint64_t handle, Decoder& decoder, FrameBuffer* frame, OutputInfo* output) { output->isValid = false; output->isErrorFrame = false; output->pictureCount = 0; @@ -282,18 +514,19 @@ void EmitDecoded(Decoder& decoder, FrameBuffer* frame, OutputInfo* output) { 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); + 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; @@ -302,6 +535,8 @@ void EmitDecoded(Decoder& decoder, FrameBuffer* frame, OutputInfo* output) { output->framePitchInBytes = pitch; } decoder.pictures[frame->frameBuffer] = timestamps; + std::lock_guard guard(lock); + if (decoders.contains(handle)) pictureOwners[frame->frameBuffer] = handle; } } @@ -337,7 +572,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; } @@ -358,6 +593,7 @@ int APS5_VABI sceVideodec2CreateDecoder_nid_postfix(const DecoderConfigInfo* con int APS5_VABI sceVideodec2DeleteDecoder_nid_postfix(std::uint64_t handle) { std::lock_guard guard(lock); if (decoders.erase(handle) == 0) throw std::runtime_error("Videodec2: invalid decoder handle"); + std::erase_if(pictureOwners, [handle](const auto& owner) { return owner.second == handle; }); return 0; } @@ -370,20 +606,57 @@ int APS5_VABI sceVideodec2Decode_nid_postfix(std::uint64_t handle, const InputDa if (Trace() && decoder.decoded < 3) std::fprintf(stderr, "[videodec2] Decode input=%llu frame=%llu output=%llu au=%llu pts=%llu size=%ux%u fb=%p/%llu\n", static_cast(input->thisSize), static_cast(frame ? frame->thisSize : 0), static_cast(output->thisSize), static_cast(input->auSize), static_cast(input->ptsData), decoder.width, decoder.height, frame ? frame->frameBuffer : nullptr, static_cast(frame ? frame->frameBufferSize : 0)); ++decoder.decoded; const Picture picture{input->ptsData, input->dtsData, input->attachedData}; - if (decoder.flushing) throw std::runtime_error("Videodec2: reset required after flushing"); + if (decoder.flushing) { + avcodec_flush_buffers(decoder.context); + decoder.flushing = false; + } if (!input->auData || input->auSize == 0 || input->auSize > static_cast(std::numeric_limits::max())) throw std::runtime_error("Videodec2: invalid access unit"); - if (decoder.codec == CodecVp9) 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.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); Send(decoder, packet); - EmitDecoded(decoder, frame, output); + EmitDecoded(handle, decoder, frame, output); return 0; } @@ -397,7 +670,7 @@ int APS5_VABI sceVideodec2Flush_nid_postfix(std::uint64_t handle, FrameBuffer* f Send(decoder, nullptr); decoder.flushing = true; } - EmitDecoded(decoder, frame, output); + EmitDecoded(handle, decoder, frame, output); return 0; } @@ -407,9 +680,16 @@ 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; }); + } avcodec_flush_buffers(decoder.context); decoder.ready.clear(); decoder.inputs.clear(); + decoder.parameterSets.clear(); decoder.flushing = false; return 0; } @@ -417,16 +697,27 @@ int APS5_VABI sceVideodec2Reset_nid_postfix(std::uint64_t handle) { int APS5_VABI sceVideodec2GetPictureInfo_nid_postfix(const OutputInfo* output, AvcPictureInfo* first, void* second) { (void)second; if (!output || !first) throw std::runtime_error("Videodec2: invalid argument pointer"); - std::vector> all; - { + const auto owner = [output]() -> std::uint64_t { std::lock_guard guard(lock); - for (const auto& [handle, decoder] : decoders) all.push_back(decoder); - } - for (const auto& decoder : all) { + 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()) continue; - if (decoder->codec != CodecAvc) throw std::runtime_error("Videodec2: picture info is only implemented for H.264"); + if (found == decoder->pictures.end()) throw std::runtime_error("Videodec2: owned picture is missing"); + 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; @@ -449,4 +740,41 @@ 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 Videodec2PictureCrop_nid_no_patch(std::uint64_t handle, const void* frameBuffer, Crop* crop) { + if (!crop) 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"); + *crop = picture->second.crop; +} + } diff --git a/core/libs/prx/libSceVideodec2/Unimplemented.cpp b/core/libs/prx/libSceVideodec2/Unimplemented.cpp new file mode 100644 index 0000000000..1cad37fd28 --- /dev/null +++ b/core/libs/prx/libSceVideodec2/Unimplemented.cpp @@ -0,0 +1,58 @@ +#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__); +} + +} diff --git a/core/libs/prx/libSceVisionManager/Export.cpp b/core/libs/prx/libSceVisionManager/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceVisionManager/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/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 898511c884..6cb273c095 100644 --- a/core/libs/prx/libSceVoice/Export.cpp +++ b/core/libs/prx/libSceVoice/Export.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -19,17 +20,22 @@ constexpr std::int32_t PortOutVoice = 4; constexpr std::int32_t PortOutDevice = 5; constexpr std::int32_t PortStateReady = 1; constexpr std::uint32_t VoiceFrameMs = 20; +constexpr std::int32_t PortAttrVoiceActivity = 1001; +constexpr int VoiceErrorNotInitialized = static_cast(0x804E0801u); +constexpr int VoiceErrorAlreadyInitialized = static_cast(0x804E0802u); struct Port { std::int32_t type; float volume; std::uint32_t bitrate; + bool muted = false; }; struct Voice { std::mutex mutex; bool initialized = false; bool started = false; + bool chatEnabled = false; std::uint32_t nextId = 0; std::map ports; std::set> connections; @@ -62,7 +68,7 @@ int APS5_VABI sceVoiceInit(VoiceInitParam* param, int32_t version) { if (param == nullptr) APS5_INVALID_ARG_EX; auto& voice = State(); std::lock_guard lock(voice.mutex); - if (voice.initialized) Fail(__func__, "already initialized"); + if (voice.initialized) return VoiceErrorAlreadyInitialized; voice.initialized = true; return 0; } @@ -73,10 +79,10 @@ int APS5_VABI sceVoiceSetThreadsParams(void* params) { } 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 VoiceErrorNotInitialized; + if (param == nullptr) APS5_INVALID_ARG_EX; voice.started = true; return 0; } @@ -96,16 +102,17 @@ int APS5_VABI sceVoiceEnd_nid_postfix(void) { voice.ports.clear(); voice.connections.clear(); voice.started = false; + voice.chatEnabled = 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)); auto& voice = State(); std::lock_guard lock(voice.mutex); - if (!voice.initialized) Fail(__func__, "library not initialized"); + if (!voice.initialized) return VoiceErrorNotInitialized; + 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)); 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}); @@ -166,9 +173,10 @@ int APS5_VABI sceVoiceGetVolume(uint32_t port_id, float* volume) { } 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; @@ -178,11 +186,14 @@ int APS5_VABI sceVoiceGetPortInfo(uint32_t port_id, VoicePortInfo* info) { } int APS5_VABI sceVoiceGetPortAttr(uint32_t port_id, int32_t attr, void* value, int32_t size) { - (void)value; - (void)size; auto& voice = State(); std::lock_guard lock(voice.mutex); RequirePort(voice, port_id, __func__); + if (attr == PortAttrVoiceActivity) { + if (value == nullptr || size != 1) APS5_INVALID_ARG_EX; + std::memset(value, 0, 1); + return 0; + } Fail(__func__, "attribute " + std::to_string(attr) + " not implemented"); } @@ -204,27 +215,127 @@ int APS5_VABI sceVoiceWriteToIPort(uint32_t input_port_id, const void* data, uin return 0; } -int APS5_VABI sceVoiceSetMuteFlag() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceVoiceSetMuteFlag(uint32_t port_id, bool muted) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + RequirePort(voice, port_id, __func__).muted = muted; return 0; } -int APS5_VABI sceVoiceGetResourceInfo(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceVoiceGetMuteFlag(uint32_t port_id, bool* muted) { + if (muted == nullptr) APS5_INVALID_ARG_EX; + auto& voice = State(); + std::lock_guard lock(voice.mutex); + *muted = RequirePort(voice, port_id, __func__).muted; + return 0; +} + +int APS5_VABI sceVoiceGetResourceInfo(VoiceResourceInfo* info) { + if (info == nullptr) APS5_INVALID_ARG_EX; + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) Fail(__func__, "library not initialized"); + info->max_voice_input_ports = 7; + info->max_voice_output_ports = 7; + info->max_device_input_ports = 1; + info->max_device_output_ports = 1; + info->max_ports = 64; return 0; } int APS5_VABI sceVoiceEnableChat(void) { - NotImplemented_nid_no_patch(__func__); + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) Fail(__func__, "library not initialized"); + voice.chatEnabled = true; return 0; } -int APS5_VABI sceVoiceResetPort(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceVoiceResetPort(uint32_t port_id) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + RequirePort(voice, port_id, __func__); + return 0; +} + +int APS5_VABI sceVoicePausePort(uint32_t port_id) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + RequirePort(voice, port_id, __func__); + return 0; +} + +int APS5_VABI sceVoiceResumePort(uint32_t port_id) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + RequirePort(voice, port_id, __func__); return 0; } int APS5_VABI sceVoiceDisableChat(void) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) Fail(__func__, "library not initialized"); + voice.chatEnabled = false; + return 0; +} + +int APS5_VABI sceVoiceInitHQ(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +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(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceSetMuteFlagAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceSetBitRate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceUpdatePort(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoicePausePortAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceResumePortAll(void) { NotImplemented_nid_no_patch(__func__); 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 4ab9fb4112..96053a9e69 100644 --- a/core/libs/prx/libSceVoiceQoS/Export.cpp +++ b/core/libs/prx/libSceVoiceQoS/Export.cpp @@ -1,6 +1,12 @@ +#include #include #include +#include +#include +#include #include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -9,80 +15,244 @@ namespace { constexpr std::int32_t VOICE_QOS_APP_TYPE_GAME = 0x20000000; 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_INITIALIZED = static_cast(0x804E0905); -std::mutex g_mutex; -bool g_initialized = false; +using Endpoint = std::map>; + +struct Connection { + std::int32_t local; + std::int32_t remote; +}; + +struct QoS { + std::mutex mutex; + bool initialized = false; + std::int32_t nextId = 1; + std::map locals; + std::map remotes; + std::map connections; +}; + +QoS& State() { + static QoS qos; + return qos; +} + +std::int32_t NewId(QoS& qos) { + const std::int32_t id = qos.nextId; + qos.nextId = qos.nextId == std::numeric_limits::max() ? 1 : qos.nextId + 1; + return id; +} + +int CreateEndpoint(std::int32_t* id, std::map& endpoints, QoS& qos) { + if (!id) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + *id = NewId(qos); + endpoints.emplace(*id, Endpoint{}); + return 0; +} + +int DeleteEndpoint(std::int32_t id, std::map& endpoints) { + return endpoints.erase(id) != 0 ? 0 : SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; +} + +int SetAttribute(std::map& endpoints, std::int32_t id, std::int32_t attribute, const void* value, std::int32_t size) { + const auto endpoint = endpoints.find(id); + if (endpoint == endpoints.end() || !value) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + if (size > 0) { + const auto* bytes = static_cast(value); + endpoint->second[attribute].assign(bytes, bytes + size); + } else { + endpoint->second[attribute].clear(); + } + return 0; +} + +int GetAttribute(std::map& endpoints, std::int32_t id, std::int32_t attribute, void* value, std::int32_t size) { + const auto endpoint = endpoints.find(id); + if (endpoint == endpoints.end() || !value || size <= 0) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + const auto attr = endpoint->second.find(attribute); + if (attr != endpoint->second.end() && !attr->second.empty()) { + const auto toCopy = std::min(static_cast(size), attr->second.size()); + std::memcpy(value, attr->second.data(), toCopy); + if (static_cast(size) > toCopy) { + std::memset(static_cast(value) + toCopy, 0, static_cast(size) - toCopy); + } + } else { + std::memset(value, 0, static_cast(size)); + } + return 0; +} + +template +int Locked(Body&& body) { + auto& qos = State(); + std::lock_guard lock(qos.mutex); + if (!qos.initialized) return SCE_VOICE_ERROR_LIBVOICEQOS_NOT_INIT; + return body(qos); +} } extern "C" { -int APS5_VABI sceVoiceQoSInit(void* mem_block, uint32_t mem_size, int32_t app_type) { - std::lock_guard lock(g_mutex); - if (g_initialized) return SCE_VOICE_ERROR_LIBVOICEQOS_INITIALIZED; - if (!mem_block || mem_size != VOICE_QOS_MEMORY_SIZE || (app_type != VOICE_QOS_APP_TYPE_GAME && app_type != VOICE_QOS_APP_TYPE_10000000)) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; - g_initialized = true; +int APS5_VABI sceVoiceQoSInit(void* memBlock, uint32_t memSize, int32_t appType) { + auto& qos = State(); + std::lock_guard lock(qos.mutex); + if (qos.initialized) return SCE_VOICE_ERROR_LIBVOICEQOS_INITIALIZED; + if (!memBlock || memSize != VOICE_QOS_MEMORY_SIZE || (appType != VOICE_QOS_APP_TYPE_GAME && appType != VOICE_QOS_APP_TYPE_10000000)) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + qos.initialized = true; return 0; } int APS5_VABI sceVoiceQoSEnd() { + 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)userId; + (void)deviceInId; + (void)deviceOutId; + return Locked([&](QoS& qos) { return CreateEndpoint(localId, qos.locals, qos); }); +} + +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); }); +} + +int APS5_VABI sceVoiceQoSDeleteRemoteEndpoint(int32_t remoteId) { + return Locked([&](QoS& qos) { return DeleteEndpoint(remoteId, qos.remotes); }); +} + +int APS5_VABI sceVoiceQoSConnect(int32_t* connectionId, int32_t localId, int32_t remoteId) { + return Locked([&](QoS& qos) { + if (!connectionId || !qos.locals.contains(localId) || !qos.remotes.contains(remoteId)) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + *connectionId = NewId(qos); + qos.connections.emplace(*connectionId, Connection{localId, remoteId}); + return 0; + }); +} + +int APS5_VABI sceVoiceQoSDisconnect(int32_t connectionId) { + return Locked([&](QoS& qos) { return qos.connections.erase(connectionId) != 0 ? 0 : SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; }); +} + +int APS5_VABI sceVoiceQoSSetConnectionAttribute() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceVoiceQoSCreateLocalEndpoint() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSSetLocalEndpointAttribute(int32_t localId, int32_t attributeId, const void* value, int32_t size) { + return Locked([&](QoS& qos) { return SetAttribute(qos.locals, localId, attributeId, value, size); }); } -int APS5_VABI sceVoiceQoSDeleteLocalEndpoint() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSSetRemoteEndpointAttribute(int32_t remoteId, int32_t attributeId, const void* value, int32_t size) { + return Locked([&](QoS& qos) { return SetAttribute(qos.remotes, remoteId, attributeId, value, size); }); } -int APS5_VABI sceVoiceQoSCreateRemoteEndpoint() { - NotImplemented_nid_no_patch(__func__); - return 0; +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 sceVoiceQoSDeleteRemoteEndpoint() { - NotImplemented_nid_no_patch(__func__); - 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 sceVoiceQoSConnect() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSGetLocalEndpointAttribute(int32_t localId, int32_t attributeId, void* value, int32_t size) { + return Locked([&](QoS& qos) { return GetAttribute(qos.locals, localId, attributeId, value, size); }); } -int APS5_VABI sceVoiceQoSDisconnect() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSGetRemoteEndpointAttribute(int32_t remoteId, int32_t attributeId, const void* value, int32_t size) { + (void)remoteId; + (void)attributeId; + (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 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 sceVoiceQoSSetRemoteEndpointAttribute() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSGetLocalEndpoint(int32_t connectionId, int32_t* localId) { + return Locked([&](QoS& qos) { + if (!localId) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + const auto it = qos.connections.find(connectionId); + if (it == qos.connections.end()) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + *localId = it->second.local; + return 0; + }); } -int APS5_VABI sceVoiceQoSReadPacket() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSGetRemoteEndpoint(int32_t connectionId, int32_t* remoteId) { + return Locked([&](QoS& qos) { + if (!remoteId) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + const auto it = qos.connections.find(connectionId); + if (it == qos.connections.end()) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + *remoteId = it->second.remote; + return 0; + }); } -int APS5_VABI sceVoiceQoSWritePacket() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSEnableChat() { + NotImplemented_nid_no_patch(__func__); + return 0; } -int APS5_VABI sceVoiceQoSGetLocalEndpointAttribute() { +int APS5_VABI sceVoiceQoSDisableChat() { + 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 sceVoiceQoSDebugGetStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceQoSInitHQ() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceQoSSetMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceQoSTerminate() { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceVrServiceDialog/Export.cpp b/core/libs/prx/libSceVrServiceDialog/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceVrServiceDialog/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/libSceVrSetupDialog/CMakeLists.txt b/core/libs/prx/libSceVrSetupDialog/CMakeLists.txt new file mode 100644 index 0000000000..1f0ea1700c --- /dev/null +++ b/core/libs/prx/libSceVrSetupDialog/CMakeLists.txt @@ -0,0 +1,14 @@ +add_library(libSceVrSetupDialog SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceVrSetupDialog PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceVrSetupDialog) + +if(BUILD_TESTING) + add_test_executable(guest_vr_setup_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestVrSetupDialog.cpp) + target_include_directories(guest_vr_setup_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_vr_setup_dialog_tests PRIVATE libSceVrSetupDialog) + add_test(NAME guest_vr_setup_dialog COMMAND guest_vr_setup_dialog_tests) + if(WIN32) + set_tests_properties(guest_vr_setup_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceVrSetupDialog/Export.cpp b/core/libs/prx/libSceVrSetupDialog/Export.cpp index 5c9a5da6a0..f4b2718a30 100644 --- a/core/libs/prx/libSceVrSetupDialog/Export.cpp +++ b/core/libs/prx/libSceVrSetupDialog/Export.cpp @@ -1,38 +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 sceVrSetupDialogClose(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVrSetupDialogGetResult(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceVrSetupDialogInitialize(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + int expected = COMMON_DIALOG_STATUS_NONE; + if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_INITIALIZED)) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; + return 0; } -int APS5_VABI sceVrSetupDialogOpen(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVrSetupDialogTerminate(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVrSetupDialogOpen(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 sceVrSetupDialogUpdateStatus(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return g_status.load(); +} + +int APS5_VABI sceVrSetupDialogGetStatus(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 sceVrSetupDialogClose(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 sceVrSetupDialogTerminate(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/libSceVrTracker/Export.cpp b/core/libs/prx/libSceVrTracker/Export.cpp new file mode 100644 index 0000000000..c2e4e7c8f3 --- /dev/null +++ b/core/libs/prx/libSceVrTracker/Export.cpp @@ -0,0 +1,33 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceVrTrackerCpuProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceVrTrackerGetResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceVrTrackerGetTime() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceVrTrackerGpuSubmit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceVrTrackerGpuWait() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceVrTrackerInit() { + 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 34024033d5..e323b3c719 100644 --- a/core/libs/prx/libSceVrTracker2/Export.cpp +++ b/core/libs/prx/libSceVrTracker2/Export.cpp @@ -1,37 +1,260 @@ -#include #include -#include "SceTypes.hpp" +#include +#include +#include +#include #include "prx/libc/include/General.hpp" +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; +} + +} + extern "C" { -int APS5_VABI sceVrTracker2GetResult() { +std::int32_t APS5_VABI sceVrTracker2Finalize() { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2GetCoordinateSystem() { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2GetPlayAreaBoundaryGeometry() { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2GetPlayAreaOrientedBoundingBox() { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2GetResult() { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2LocateCoordinateSystem() { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2RegisterDevice() { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2ResetLocalCoordinate() { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2SetCoordinateSystem() { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2UnregisterDevice() { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2QueryMemory(const void* parameter, void* requirement) { + if (parameter == nullptr || requirement == nullptr) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; + const auto* bytes = static_cast(parameter); + std::uint32_t size = 0; + std::memcpy(&size, bytes, sizeof size); + if (size != 0x20) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; + for (std::size_t offset = sizeof size; offset < 0x20; ++offset) + if (bytes[offset] != 0) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; + const std::uint32_t fields[3] = {0x20, 32u << 20, 64u << 10}; + std::memset(requirement, 0, 0x20); + std::memcpy(requirement, fields, sizeof fields); + return SCE_OK; +} + +std::int32_t APS5_VABI sceVrTracker2Initialize() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceVrTracker2Initialize() { - NotImplemented_nid_no_patch(__func__); +APS5_EXPORT("1m6DD9BBmSE", VrTracker2NotInitialized); +APS5_EXPORT("SgunslIO-C0", VrTracker2NotInitialized); +APS5_EXPORT("KMIToVnHrhc", VrTracker2NotInitialized); +APS5_EXPORT("XfEBVemeJyE", VrTracker2NotInitialized); +APS5_EXPORT("y79S3-zrJKg", VrTracker2NotInitialized); +APS5_EXPORT("b777Kpfe2J8", VrTracker2NotInitialized); +APS5_EXPORT("UfUYgiT7VK0", VrTracker2NotInitialized); +APS5_EXPORT("MB7B6396K6U", VrTracker2NotInitialized); +APS5_EXPORT("H2Mnx7gz4ac", VrTracker2NotInitialized); +APS5_EXPORT("SgNHzN6XaEc", VrTracker2NotInitialized); +APS5_EXPORT("sYBNLrMxjAM", VrTracker2NotInitialized); +APS5_EXPORT("3gQncidodmw", VrTracker2NotInitialized); +APS5_EXPORT("qnCmEbUGqWY", VrTracker2NotInitialized); +APS5_EXPORT("oGNn7JIdMHw", VrTracker2NotInitialized); +APS5_EXPORT("80IxOn8swIA", VrTracker2NotInitialized); +APS5_EXPORT("NTbJNAZA4ro", VrTracker2NotInitialized); +APS5_EXPORT("914+9oJxNts", VrTracker2NotInitialized); +APS5_EXPORT("c6SZtdEjH0E", VrTracker2NotInitialized); +APS5_EXPORT("74IXIMe+Lto", VrTracker2NotInitialized); +APS5_EXPORT("hmahOYMiYNY", VrTracker2NotInitialized); +APS5_EXPORT("pNXc6ZlLq-8", VrTracker2NotInitialized); +APS5_EXPORT("omJ7b8FqAN0", VrTracker2NotInitialized); +APS5_EXPORT("G2PWj1HLqn8", VrTracker2NotInitialized); +APS5_EXPORT("73zcxAFawYk", VrTracker2NotInitialized); +APS5_EXPORT("vegrWFNiQwc", VrTracker2NotInitialized); +APS5_EXPORT("6r01IawmypA", VrTracker2NotInitialized); +APS5_EXPORT("zeof7sF2fqE", VrTracker2NotInitialized); +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", VrTracker2NotInitialized); +APS5_EXPORT("TIpYP35JzUs", VrTracker2NotInitialized); +APS5_EXPORT("LrSc+KuxIkg", VrTracker2NotInitialized); +APS5_EXPORT("Ur+M5wy+QBY", VrTracker2NotInitialized); +APS5_EXPORT("OvKC351Udqo", VrTracker2NotInitialized); +APS5_EXPORT("LQV4nz6wteY", VrTracker2NotInitialized); +APS5_EXPORT("x0pGthEwf88", VrTracker2NotInitialized); +APS5_EXPORT("d5Fjs0rHkBg", VrTracker2NotInitialized); +APS5_EXPORT("Yed3ZONcuMk", VrTracker2NotInitialized); +APS5_EXPORT("fkptW6uRdWc", VrTracker2NotInitialized); +APS5_EXPORT("HDlD-xE1Xuk", VrTracker2NotInitialized); +APS5_EXPORT("IGdE6nsMTAY", VrTracker2NotInitialized); +APS5_EXPORT("DSTergmOvvE", VrTracker2NotInitialized); +APS5_EXPORT("B7hPwvvg79E", VrTracker2NotInitialized); +APS5_EXPORT("ZwnIsDe1eNE", VrTracker2NotInitialized); +APS5_EXPORT("B2vw+s-0Dfo", VrTracker2NotInitialized); +APS5_EXPORT("4LNTLivfRuA", VrTracker2NotInitialized); +APS5_EXPORT("edajhZhz9Uo", VrTracker2NotInitialized); +APS5_EXPORT("QS+7oqiszuc", VrTracker2NotInitialized); +APS5_EXPORT("-dDyvzd2jf0", VrTracker2NotInitialized); +APS5_EXPORT("k8JDLkCrOgs", VrTracker2NotInitialized); +APS5_EXPORT("-HFx+dnDSOI", VrTracker2NotInitialized); +APS5_EXPORT("wgDeDXirl0g", VrTracker2NotInitialized); +APS5_EXPORT("a1kWSTYGBxg", VrTracker2NotInitialized); +APS5_EXPORT("zUwxBEKr3fc", VrTracker2NotInitialized); +APS5_EXPORT("TC7VTVzj0xc", VrTracker2NotInitialized); +APS5_EXPORT("1DwROxtuANs", VrTracker2NotInitialized); +APS5_EXPORT("wwbt4s8cbMQ", VrTracker2NotInitialized); +APS5_EXPORT("eGxBJ0IolOA", VrTracker2NotInitialized); +APS5_EXPORT("9BaFhspHF+E", VrTracker2NotInitialized); +APS5_EXPORT("lb9q5nJ4vzk", VrTracker2NotInitialized); +APS5_EXPORT("ZsD1-thjXS4", VrTracker2NotInitialized); +APS5_EXPORT("QdHQnM0XICI", VrTracker2NotInitialized); +APS5_EXPORT("Ek4WMOvwv6g", VrTracker2NotInitialized); +APS5_EXPORT("W1YAO6G+hNc", VrTracker2NotInitialized); +APS5_EXPORT("HAJB7irjl+0", VrTracker2NotInitialized); +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", VrTracker2NotInitialized); +APS5_EXPORT("oOLE62H66z0", VrTracker2NotInitialized); +APS5_EXPORT("MHekhnRBKAw", VrTracker2NotInitialized); +APS5_EXPORT("JnSmj+5E8WY", VrTracker2NotInitialized); +APS5_EXPORT("N1P+melAgb8", VrTracker2NotInitialized); +APS5_EXPORT("-e7A3rAM3Yk", VrTracker2NotInitialized); +APS5_EXPORT("RefT1spBleg", VrTracker2NotInitialized); +APS5_EXPORT("DsOfryhbm4U", VrTracker2NotInitialized); +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() { + NotImplemented_nid_no_patch("jRmuXjsknsI"); return 0; } -int APS5_VABI sceVrTracker2QueryMemory() { - NotImplemented_nid_no_patch(__func__); +APS5_EXPORT("NBhWy+Gl0tY", VrTracker2Unknown02); +std::int32_t APS5_VABI VrTracker2Unknown02() { + NotImplemented_nid_no_patch("NBhWy+Gl0tY"); return 0; } -int APS5_VABI sceVrTracker2RegisterDevice() { - NotImplemented_nid_no_patch(__func__); +APS5_EXPORT("9R6W33Au4B4", VrTracker2Unknown03); +std::int32_t APS5_VABI VrTracker2Unknown03() { + NotImplemented_nid_no_patch("9R6W33Au4B4"); return 0; } -int APS5_VABI sceVrTracker2SetCoordinateSystem() { - NotImplemented_nid_no_patch(__func__); +APS5_EXPORT("O8ZkTFtvUL4", VrTracker2Unknown04); +std::int32_t APS5_VABI VrTracker2Unknown04() { + NotImplemented_nid_no_patch("O8ZkTFtvUL4"); return 0; } -int APS5_VABI sceVrTracker2UnregisterDevice() { - NotImplemented_nid_no_patch(__func__); +APS5_EXPORT("940WPg+WwIk", VrTracker2Unknown05); +std::int32_t APS5_VABI VrTracker2Unknown05() { + NotImplemented_nid_no_patch("940WPg+WwIk"); + return 0; +} + +APS5_EXPORT("oZDm1bynCaA", VrTracker2Unknown06); +std::int32_t APS5_VABI VrTracker2Unknown06() { + NotImplemented_nid_no_patch("oZDm1bynCaA"); + return 0; +} + +APS5_EXPORT("U1VYczwqukY", VrTracker2Unknown07); +std::int32_t APS5_VABI VrTracker2Unknown07() { + 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/Export.cpp b/core/libs/prx/libSceWebBrowserDialog/Export.cpp index dd35e045a0..8ccf318370 100644 --- a/core/libs/prx/libSceWebBrowserDialog/Export.cpp +++ b/core/libs/prx/libSceWebBrowserDialog/Export.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -13,6 +12,7 @@ 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(0x80B80005u); constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast(0x80B80007u); constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); @@ -21,19 +21,23 @@ constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); 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)) throw std::logic_error("sceWebBrowserDialogTerminate: not initialized or still running"); - return 0; + if (g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_NONE)) return 0; + expected = COMMON_DIALOG_STATUS_FINISHED; + if (g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_NONE)) 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; + g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } @@ -69,8 +73,12 @@ int APS5_VABI sceWebBrowserDialogSetCookie(void) { return 0; } -int APS5_VABI sceWebBrowserDialogOpenForPredeterminedContent() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceWebBrowserDialogOpenForPredeterminedContent(const void* param) { + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (status == COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_BUSY; + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } @@ -79,4 +87,19 @@ int APS5_VABI sceWebBrowserDialogResetCookie() { return 0; } +int APS5_VABI sceWebBrowserDialogGetEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceWebBrowserDialogNavigate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +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/libSceXml/Export.cpp b/core/libs/prx/libSceXml/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceXml/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..22135e465c 100644 --- a/core/libs/prx/libSceZlib/Export.cpp +++ b/core/libs/prx/libSceZlib/Export.cpp @@ -67,7 +67,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}; diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index c30d4c42a4..b5ca381e89 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -1,23 +1,79 @@ +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/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/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/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 ${CMAKE_CURRENT_SOURCE_DIR}/src/GuestAllocations.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/GuestArena.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/GuestStacks.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/GuestWriteWatch.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Interfaces.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/GuestHeap.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/HeapDiagnostics.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Mspace.cpp @@ -26,28 +82,116 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/JumpBuffer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Formatting.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 ${CMAKE_CURRENT_SOURCE_DIR}/src/Environment.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Directory.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Filesystem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Rename.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Errors.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Syslog.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/RealPath.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/CxxAbiSupport.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Threads.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Backtrace.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/exception/Exports.cpp ) +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) + 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) + configure_windows_unwind(guest_array_callbacks_tests) + add_test(NAME guest_array_callbacks COMMAND guest_array_callbacks_tests) + set_tests_properties(guest_array_callbacks PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(guest_array_callbacks PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_dynamic_cast_tests ${LIBS_INCLUDE_DIR}/tests/GuestDynamicCast.cpp) + target_include_directories(guest_dynamic_cast_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_dynamic_cast_tests PRIVATE libc) + configure_windows_unwind(guest_dynamic_cast_tests) + add_test(NAME guest_dynamic_cast COMMAND guest_dynamic_cast_tests) + if(WIN32) + set_tests_properties(guest_dynamic_cast PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_time_conversion_tests ${LIBS_INCLUDE_DIR}/tests/GuestTimeConversion.cpp) target_include_directories(guest_time_conversion_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_time_conversion_tests PRIVATE libc) @@ -58,6 +202,36 @@ if(BUILD_TESTING) set_tests_properties(guest_time_conversion PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_mktime_tests ${LIBS_INCLUDE_DIR}/tests/GuestMktime.cpp) + target_include_directories(guest_mktime_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_mktime_tests PRIVATE libc) + configure_windows_unwind(guest_mktime_tests) + add_test(NAME guest_mktime COMMAND guest_mktime_tests) + set_tests_properties(guest_mktime PROPERTIES TIMEOUT 10) + if(WIN32) + 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) + configure_windows_unwind(precise_wait_tests) + add_test(NAME precise_wait COMMAND precise_wait_tests) + set_tests_properties(precise_wait PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(precise_wait PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_scan_count_tests ${LIBS_INCLUDE_DIR}/tests/GuestScanCount.cpp) target_include_directories(guest_scan_count_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_scan_count_tests PRIVATE libc) @@ -86,6 +260,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) @@ -105,6 +288,33 @@ 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) + configure_windows_unwind(guest_wide_conversion_tests) + add_test(NAME guest_wide_conversion COMMAND guest_wide_conversion_tests) + if(WIN32) + set_tests_properties(guest_wide_conversion 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) @@ -123,6 +333,42 @@ if(BUILD_TESTING) set_tests_properties(guest_math PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_random_state_tests ${LIBS_INCLUDE_DIR}/tests/GuestRandomState.cpp) + target_include_directories(guest_random_state_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_random_state_tests PRIVATE libc) + configure_windows_unwind(guest_random_state_tests) + add_test(NAME guest_random_state COMMAND guest_random_state_tests) + if(WIN32) + 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) @@ -143,6 +389,56 @@ 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) + configure_windows_unwind(guest_libc_threads_tests) + add_test(NAME guest_libc_threads COMMAND guest_libc_threads_tests) + set_tests_properties(guest_libc_threads PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_libc_threads PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_libc_static_imports_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibcStaticImports.cpp) + target_include_directories(guest_libc_static_imports_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libc_static_imports_tests PRIVATE libc) + configure_windows_unwind(guest_libc_static_imports_tests) + add_test(NAME guest_libc_static_imports COMMAND guest_libc_static_imports_tests) + set_tests_properties(guest_libc_static_imports PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_libc_static_imports PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_fiopen_tests ${LIBS_INCLUDE_DIR}/tests/GuestFiopen.cpp) target_include_directories(guest_fiopen_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_fiopen_tests PRIVATE libc) @@ -162,6 +458,16 @@ if(BUILD_TESTING) set_tests_properties(guest_working_directory PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_realpath_tests ${LIBS_INCLUDE_DIR}/tests/GuestRealpath.cpp) + target_include_directories(guest_realpath_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_realpath_tests PRIVATE libc) + configure_windows_unwind(guest_realpath_tests) + add_test(NAME guest_realpath COMMAND guest_realpath_tests) + set_tests_properties(guest_realpath PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_realpath PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_path_case_tests ${LIBS_INCLUDE_DIR}/tests/GuestPathCase.cpp) target_include_directories(guest_path_case_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_path_case_tests PRIVATE libc libkernel) @@ -171,9 +477,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) @@ -189,6 +505,52 @@ 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_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) + configure_windows_unwind(guest_hostname_tests) + add_test(NAME guest_hostname COMMAND guest_hostname_tests) + set_tests_properties(guest_hostname PROPERTIES TIMEOUT 20) + if(WIN32) + 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) + configure_windows_unwind(guest_memrchr_tests) + add_test(NAME guest_memrchr COMMAND guest_memrchr_tests) + if(WIN32) + set_tests_properties(guest_memrchr PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_daemon_tests ${LIBS_INCLUDE_DIR}/tests/GuestDaemon.cpp) + target_include_directories(guest_daemon_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_daemon_tests PRIVATE libc) + configure_windows_unwind(guest_daemon_tests) + add_test(NAME guest_daemon COMMAND guest_daemon_tests) + if(WIN32) + set_tests_properties(guest_daemon PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_lifecycle_tests ${LIBS_INCLUDE_DIR}/tests/GuestLifecycle.cpp) target_include_directories(guest_lifecycle_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_lifecycle_tests PRIVATE libc) @@ -208,6 +570,46 @@ if(BUILD_TESTING) set_tests_properties(guest_thread_callback_abi PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_interface_tests ${LIBS_INCLUDE_DIR}/tests/GuestInterfaces.cpp) + target_include_directories(guest_interface_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_interface_tests PRIVATE libc) + configure_windows_unwind(guest_interface_tests) + add_test(NAME guest_interfaces COMMAND guest_interface_tests) + if(WIN32) + target_link_libraries(guest_interface_tests PRIVATE ws2_32 iphlpapi) + set_tests_properties(guest_interfaces PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_qsort_tests ${LIBS_INCLUDE_DIR}/tests/GuestQsort.cpp) + target_include_directories(guest_qsort_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_qsort_tests PRIVATE libc) + configure_windows_unwind(guest_qsort_tests) + add_test(NAME guest_qsort COMMAND guest_qsort_tests) + set_tests_properties(guest_qsort PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_qsort PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_strftime_tests ${LIBS_INCLUDE_DIR}/tests/GuestStrftime.cpp) + target_include_directories(guest_strftime_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_strftime_tests PRIVATE libc) + configure_windows_unwind(guest_strftime_tests) + add_test(NAME guest_strftime COMMAND guest_strftime_tests) + set_tests_properties(guest_strftime PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_strftime PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_atoi_tests ${LIBS_INCLUDE_DIR}/tests/GuestAtoi.cpp) + target_include_directories(guest_atoi_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_atoi_tests PRIVATE libc) + configure_windows_unwind(guest_atoi_tests) + add_test(NAME guest_atoi COMMAND guest_atoi_tests) + set_tests_properties(guest_atoi PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_atoi PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_reallocalign_tests ${LIBS_INCLUDE_DIR}/tests/GuestReallocalign.cpp) target_include_directories(guest_reallocalign_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_reallocalign_tests PRIVATE libc) @@ -222,8 +624,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) @@ -245,6 +649,25 @@ if(BUILD_TESTING) set_tests_properties(guest_errors PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + 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) + 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${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() + endif() add_test_executable(guest_syslog_tests ${LIBS_INCLUDE_DIR}/tests/GuestSyslog.cpp) target_include_directories(guest_syslog_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_syslog_tests PRIVATE libc) @@ -272,6 +695,30 @@ if(BUILD_TESTING) set_tests_properties(guest_formatting PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_new_handler_tests ${LIBS_INCLUDE_DIR}/tests/GuestNewHandler.cpp) + target_include_directories(guest_new_handler_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_new_handler_tests PRIVATE libc) + configure_windows_unwind(guest_new_handler_tests) + add_test_executable(guest_generic_category_tests ${LIBS_INCLUDE_DIR}/tests/GuestGenericCategory.cpp) + target_include_directories(guest_generic_category_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_generic_category_tests PRIVATE libc) + configure_windows_unwind(guest_generic_category_tests) + add_test(NAME guest_generic_category COMMAND guest_generic_category_tests) + set_tests_properties(guest_generic_category PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(guest_generic_category PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test(NAME guest_new_handler COMMAND guest_new_handler_tests) + if(WIN32) + set_tests_properties(guest_new_handler PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test(NAME guest_new_handler_default_heap COMMAND guest_new_handler_tests default) + if(WIN32) + set_tests_properties(guest_new_handler_default_heap PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_search_tests ${LIBS_INCLUDE_DIR}/tests/GuestSearch.cpp) target_include_directories(guest_search_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_search_tests PRIVATE libc) @@ -317,6 +764,35 @@ 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) + configure_windows_unwind(guest_overflow_error_tests) + add_test(NAME guest_overflow_error COMMAND guest_overflow_error_tests) + if(WIN32) + 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}) @@ -326,6 +802,15 @@ if(BUILD_TESTING) target_include_directories(exception_personality_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(exception_personality_tests PRIVATE libc) add_test(NAME exception_personality COMMAND exception_personality_tests) + add_test_executable(guest_exit_order_tests ${LIBS_INCLUDE_DIR}/tests/GuestExitOrder.cpp) + 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) @@ -364,10 +849,32 @@ 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) + target_include_directories(guest_allocator_failures_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_allocator_failures_tests PRIVATE libc) + configure_windows_unwind(guest_allocator_failures_tests) + add_test(NAME guest_allocator_failures COMMAND guest_allocator_failures_tests) + set_tests_properties(guest_allocator_failures PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_allocator_failures PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(application_heap_stats_tests ${LIBS_INCLUDE_DIR}/tests/ApplicationHeapStats.cpp) target_include_directories(application_heap_stats_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(application_heap_stats_tests PRIVATE libc) @@ -384,6 +891,45 @@ 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 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() + + add_test_executable(guest_heap_report_for_game_tests ${LIBS_INCLUDE_DIR}/tests/GuestHeapReportForGame.cpp) + target_include_directories(guest_heap_report_for_game_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_heap_report_for_game_tests PRIVATE libc) + configure_windows_unwind(guest_heap_report_for_game_tests) + add_test(NAME guest_heap_report_for_game COMMAND guest_heap_report_for_game_tests) + if(WIN32) + set_tests_properties(guest_heap_report_for_game PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() if(WIN32) @@ -413,7 +959,94 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(guest_rune_locale_tests EXCLUDE_FROM_ALL ${LIBS_INCLUDE_DIR}/tests/GuestRuneLocale.cpp) +target_include_directories(guest_rune_locale_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(guest_rune_locale_tests PRIVATE libc) +configure_windows_unwind(guest_rune_locale_tests) +if(BUILD_TESTING) + set_target_properties(guest_rune_locale_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME guest_rune_locale COMMAND guest_rune_locale_tests) + if(WIN32) + set_tests_properties(guest_rune_locale PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + +add_test_executable(guest_libc_mutex_init_with_name_tests EXCLUDE_FROM_ALL ${LIBS_INCLUDE_DIR}/tests/GuestLibcMutexInitWithName.cpp) +target_include_directories(guest_libc_mutex_init_with_name_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(guest_libc_mutex_init_with_name_tests PRIVATE libc) +configure_windows_unwind(guest_libc_mutex_init_with_name_tests) +if(BUILD_TESTING) + set_target_properties(guest_libc_mutex_init_with_name_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME guest_libc_mutex_init_with_name COMMAND guest_libc_mutex_init_with_name_tests) + if(WIN32) + set_tests_properties(guest_libc_mutex_init_with_name PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + +add_test_executable(guest_libc_cnd_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) +configure_windows_unwind(guest_libc_cnd_timed_wait_tests) +if(BUILD_TESTING) + set_target_properties(guest_libc_cnd_timed_wait_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME guest_libc_cnd_timed_wait COMMAND guest_libc_cnd_timed_wait_tests) + if(WIN32) + set_tests_properties(guest_libc_cnd_timed_wait PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + +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) + add_test_executable(guest_message_destructor_abi_tests + ${LIBS_INCLUDE_DIR}/tests/GuestMessageDestructorAbi.cpp + ${LIBS_INCLUDE_DIR}/tests/GuestMessageDestructorAbi.S) + 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 + 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 + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endforeach() + foreach(kind IN ITEMS message plain) + foreach(operation IN ITEMS what destroy delete) + foreach(register IN ITEMS decoy zero) + set(mode "vtable-${kind}-${operation}-${register}") + add_test(NAME guest_exception_${mode} COMMAND guest_message_destructor_abi_tests ${mode}) + set_tests_properties(guest_exception_${mode} PROPERTIES TIMEOUT 10 + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endforeach() + endforeach() + endforeach() + endif() foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") endforeach() @@ -425,5 +1058,30 @@ if(MINGW) set_target_properties(windows_exception_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) add_test(NAME windows_exceptions COMMAND windows_exception_tests) set_tests_properties(windows_exceptions PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + add_test_executable(windows_backtrace_report_tests ${LIBS_INCLUDE_DIR}/tests/WindowsBacktraceReport.cpp) + target_link_libraries(windows_backtrace_report_tests PRIVATE libc) + add_test(NAME windows_backtrace_report COMMAND windows_backtrace_report_tests) + 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/Export.cpp b/core/libs/prx/libc/Export.cpp index cd2f35441c..6056428c98 100644 --- a/core/libs/prx/libc/Export.cpp +++ b/core/libs/prx/libc/Export.cpp @@ -25,80 +25,9 @@ int APS5_VABI _ZSt14_Atomic_assertPKcS0__nid_postfix() { return 0; } -// Live Cyberpunk 2077 import used in an fopen/fseek/ftell-like file-size -// idiom as (handle, 0, 2); returning 0 reports success. -int APS5_VABI vsnprintf_s_nid_postfix() { - NotImplemented_nid_no_patch(__func__); - return 0; -} -// Live Cyberpunk 2077 import used as (handle, 0, 0) in the same file-size -// idiom; returning 0 reports success. -int APS5_VABI vsscanf_s_nid_postfix() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - - -APS5_EXPORT("Ye20uNnlglA", libcCyberUnknown02); -std::uint64_t APS5_VABI libcCyberUnknown02(void) { - NotImplemented_nid_no_patch("Ye20uNnlglA"); - return 0; -} - -std::uint64_t APS5_VABI _Mtx_destroy_nid_postfix() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -std::uint64_t APS5_VABI _Cnd_destroy_nid_postfix() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -std::uint64_t APS5_VABI _Iswctype_nid_postfix() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -APS5_EXPORT("H+8UBOwfScI", libcCyberUnknown08); -std::uint64_t APS5_VABI libcCyberUnknown08(void) { - NotImplemented_nid_no_patch("H+8UBOwfScI"); - return 0; -} - -std::uint64_t APS5_VABI _WStoul_nid_postfix() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -std::uint64_t APS5_VABI _Cnd_init_nid_postfix() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -std::uint64_t APS5_VABI _Cnd_broadcast_nid_postfix() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -std::uint64_t APS5_VABI _Mtx_init_nid_postfix() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -std::uint64_t APS5_VABI _Mtx_unlock_nid_postfix() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -std::uint64_t APS5_VABI _Mtx_lock_nid_postfix() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -std::uint64_t APS5_VABI _Cnd_wait_nid_postfix() { - NotImplemented_nid_no_patch(__func__); - return 0; +APS5_EXPORT("H2e8t5ScQGc", libc___cxa_finalize); +void APS5_VABI libc___cxa_finalize(void* dsoHandle) { + CxaFinalize_nid_no_patch(dsoHandle); } } diff --git a/core/libs/prx/libc/include/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 22212a31dc..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); @@ -16,6 +17,7 @@ void* ApplicationHeapRealign_nid_no_patch(void* pointer, std::size_t bytes, std: void* ApplicationHeapCalloc_nid_no_patch(std::size_t count, std::size_t bytes); int ApplicationHeapPosixAlign_nid_no_patch(void** pointer, std::size_t alignment, std::size_t bytes); int ApplicationHeapStatsFast_nid_no_patch(void* stats); +std::size_t ApplicationHeapUsableSize_nid_no_patch(void* pointer); } diff --git a/core/libs/prx/libc/include/CallTrace.hpp b/core/libs/prx/libc/include/CallTrace.hpp new file mode 100644 index 0000000000..b53c1ba96e --- /dev/null +++ b/core/libs/prx/libc/include/CallTrace.hpp @@ -0,0 +1,28 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_CALLTRACE_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_CALLTRACE_HPP + +#include +#include + +struct CallTraceEntry { + std::uint64_t sequence; + std::uint64_t thread; + std::uint64_t returnAddress; + const char* name; + const char* library; +}; + +struct CallTraceCount { + std::uint64_t calls; + const char* name; + const char* library; +}; + +extern "C" { +void* TraceImport_nid_no_patch(void* target, const char* name, const char* library); +std::size_t CopyCallTrace_nid_no_patch(CallTraceEntry* entries, std::size_t capacity); +std::size_t 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); +} + +#endif diff --git a/core/libs/prx/libc/include/CrashText.hpp b/core/libs/prx/libc/include/CrashText.hpp new file mode 100644 index 0000000000..675cc7c096 --- /dev/null +++ b/core/libs/prx/libc/include/CrashText.hpp @@ -0,0 +1,59 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_CRASHTEXT_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_CRASHTEXT_HPP + +#include +#include + +namespace crash_text { + +constexpr std::size_t MaximumCharacters = 512; +constexpr std::size_t MinimumCharacters = 4; +constexpr std::size_t MaximumBytes = 2 * (MaximumCharacters + 1); + +inline std::uint16_t Unit(const std::uint8_t* bytes, std::size_t index, std::size_t width) { + if (width == 1) return bytes[index]; + return static_cast(bytes[2 * index] | bytes[2 * index + 1] << 8); +} + +inline bool IsText(std::uint16_t unit) { + return (unit >= 0x20 && unit < 0x7f) || unit == '\t' || unit == '\n' || unit == '\r'; +} + +inline bool StartsWithText(const std::uint8_t* bytes, std::size_t size, std::size_t width) { + if (size < MinimumCharacters * width) return false; + for (std::size_t index = 0; index < MinimumCharacters; ++index) { + if (!IsText(Unit(bytes, index, width))) return false; + } + return true; +} + +inline bool Format(const std::uint8_t* bytes, std::size_t size, char* out, std::size_t capacity) { + const std::size_t width = StartsWithText(bytes, size, 2) ? 2 : StartsWithText(bytes, size, 1) ? 1 : 0; + if (width == 0 || capacity < 8) return false; + std::size_t length = 0; + if (width == 2) out[length++] = 'u'; + out[length++] = '"'; + bool terminated = false; + for (std::size_t index = 0; (index + 1) * width <= size; ++index) { + const std::uint16_t unit = Unit(bytes, index, width); + if (unit == 0) { + terminated = true; + break; + } + if (index == MaximumCharacters || !IsText(unit)) break; + const char escape = unit == '\n' ? 'n' : unit == '\r' ? 'r' : unit == '\t' ? 't' : unit == '"' || unit == '\\' ? static_cast(unit) : '\0'; + if (length + (escape != '\0' ? 2 : 1) + 5 > capacity) break; + if (escape != '\0') out[length++] = '\\'; + out[length++] = escape != '\0' ? escape : static_cast(unit); + } + out[length++] = '"'; + if (!terminated) { + for (int dot = 0; dot < 3; ++dot) out[length++] = '.'; + } + out[length] = '\0'; + return true; +} + +} + +#endif diff --git a/core/libs/prx/libc/include/CreationMask.hpp b/core/libs/prx/libc/include/CreationMask.hpp new file mode 100644 index 0000000000..d6d419b020 --- /dev/null +++ b/core/libs/prx/libc/include/CreationMask.hpp @@ -0,0 +1,13 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_CREATIONMASK_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_CREATIONMASK_HPP + +#include + +namespace File { + +extern "C" std::uint16_t ExchangeCreationMask_nid_no_patch(std::uint16_t mask); +extern "C" std::uint16_t GetCreationMask_nid_no_patch(); + +} + +#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/FileLock.hpp b/core/libs/prx/libc/include/FileLock.hpp new file mode 100644 index 0000000000..63fc6be67b --- /dev/null +++ b/core/libs/prx/libc/include/FileLock.hpp @@ -0,0 +1,12 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_FILELOCK_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_FILELOCK_HPP + +#ifdef _WIN32 +extern "C" { +int GuestFileLock_nid_no_patch(int fd, int operation); +void GuestFileLockShare_nid_no_patch(int from, int to); +void GuestFileLockForget_nid_no_patch(int fd); +} +#endif + +#endif diff --git a/core/libs/prx/libc/include/FileStream.hpp b/core/libs/prx/libc/include/FileStream.hpp index 9c9c4e87a5..1b9954d873 100644 --- a/core/libs/prx/libc/include/FileStream.hpp +++ b/core/libs/prx/libc/include/FileStream.hpp @@ -1,15 +1,36 @@ #ifndef CORE_LIBS_PRX_LIBC_INCLUDE_FILESTREAM_HPP #define CORE_LIBS_PRX_LIBC_INCLUDE_FILESTREAM_HPP +#include +#include #include +#include #include +#include +#include #include #include #include #include #ifdef _WIN32 #include +#include "prx/libc/include/FileLock.hpp" +extern "C" void* HostStandardHandle_nid_no_patch(int descriptor); +extern "C" void KeepStandardHandle_nid_no_patch(int descriptor, void* handle); +struct UcrtStreamState { + char* pointer; + char* base; + int count; + long flags; +}; +#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; @@ -32,6 +53,10 @@ static constexpr const char* FOPEN_MSG_NULL_ARG = "null argument"; static constexpr const char* FOPEN_MSG_NOT_FOUND = "file not found"; static constexpr const char* FOPEN_MSG_OPEN_FAILED = "open failed"; +#ifndef _WIN32 +extern "C" int KeepHostStandardError_nid_no_patch(); +#endif + class FileStream { // Only the macro-accessed FreeBSD prefix is exposed. The remaining guest // FILE area is reserved; host state lives beyond it and is never a guest FILE*. @@ -39,7 +64,8 @@ class FileStream { std::byte _reserved[256 - sizeof(GuestFilePrefix)]{}; std::FILE* _handle; bool _dynamic; - bool encodingError = false; + bool guestError = false; + bool randomDevice = false; public: explicit FileStream(std::FILE* handle, bool dynamic = false) : _handle(handle), _dynamic(dynamic) { @@ -61,16 +87,109 @@ public: return _handle; } + bool IsOpen() const { + return _handle != nullptr; + } + bool IsDynamic() const { return _dynamic; } GuestFilePrefix& GuestState() { return _guest; } - bool Reopen(const char* 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; + } + + 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 = {}; - encodingError = false; - _handle = std::freopen(filename, mode, previous); + 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; + auto* const standard = HostStandardHandle_nid_no_patch(replaced); + GuestFileLockForget_nid_no_patch(replaced); + 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; #ifdef _WIN32 @@ -82,37 +201,101 @@ public: return true; } void SyncStatus() { + if (!_handle) return; _guest.readRemaining = 0; _guest.writeRemaining = 0; _guest.flags = static_cast((_guest.flags & ~0x60) | - (std::feof(GetHandle()) ? 0x20 : 0) | (std::ferror(GetHandle()) || encodingError ? 0x40 : 0)); + (std::feof(GetHandle()) ? 0x20 : 0) | (std::ferror(GetHandle()) || guestError ? 0x40 : 0)); } - void SetEncodingError() { - encodingError = true; + void SyncUnlockedStatus() { + if (!_handle) return; + _guest.readRemaining = 0; + _guest.writeRemaining = 0; +#ifdef _WIN32 + const bool endOfFile = std::feof(GetHandle()) != 0; + const bool error = std::ferror(GetHandle()) != 0; +#else + 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)); + } + + 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 SetError() { + guestError = true; SyncStatus(); } void ClearError() { + if (!_handle) return; std::clearerr(GetHandle()); - encodingError = false; + guestError = false; SyncStatus(); } - void Close() { + int Close() { GetHandle(); - if (std::fclose(std::exchange(_handle, nullptr)) != 0) throw std::runtime_error("FileStream: close failed"); +#ifdef _WIN32 + GuestFileLockForget_nid_no_patch(::_fileno(_handle)); +#endif + const int result = std::fclose(std::exchange(_handle, nullptr)); _guest.flags = 0; _guest.descriptor = -1; + return result; } }; 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 e31bedd2ab..bb50c8d70e 100644 --- a/core/libs/prx/libc/include/FilesystemError.hpp +++ b/core/libs/prx/libc/include/FilesystemError.hpp @@ -2,6 +2,7 @@ #define CORE_LIBS_PRX_LIBC_INCLUDE_FILESYSTEMERROR_HPP #include +#include "General.hpp" inline int FilesystemError(const std::error_code& error) { if (error == std::errc::no_such_file_or_directory) return 2; @@ -22,4 +23,30 @@ inline int FilesystemError(const std::error_code& error) { return 5; } +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 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 = 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; + return FilesystemError(error.code()); + } +} + #endif diff --git a/core/libs/prx/libc/include/General.hpp b/core/libs/prx/libc/include/General.hpp index 7dc00888d0..6dade90518 100644 --- a/core/libs/prx/libc/include/General.hpp +++ b/core/libs/prx/libc/include/General.hpp @@ -2,26 +2,61 @@ #define CORE_LIBS_PRX_LIBC_INCLUDE_GENERAL_HPP #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 StopAtBinaryPrefix_nid_no_patch(const char* str, char** endptr, int base); +extern "C" bool StopAtPrefixLetter_nid_no_patch(const char* str, char** endptr, int base); +#ifndef _WIN32 +extern "C" int KeepHostStandardError_nid_no_patch(); +extern "C" int HostStandardError_nid_no_patch(); +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); +extern "C" int RenameGuestPath_nid_no_patch(const char* from, const char* to); // Guest path aliases: a guest prefix such as "/savedata0" (a save-data mount point) resolves to a host // directory instead of the run directory; the prefix matches whole path components only. extern "C" void AddPathAlias_nid_no_patch(const char* guestPrefix, const char* hostPath); 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); #define APS5_INVALID_ARG_EX throw std::invalid_argument(std::string(__func__) + ": invalid argument") diff --git a/core/libs/prx/libc/include/GuestAllocations.hpp b/core/libs/prx/libc/include/GuestAllocations.hpp index 81c711b9e8..7811725142 100644 --- a/core/libs/prx/libc/include/GuestAllocations.hpp +++ b/core/libs/prx/libc/include/GuestAllocations.hpp @@ -18,24 +18,58 @@ struct Range { std::uint64_t allocationAddress; std::size_t allocationBytes; bool releasable = true; + bool gpu = false; + std::uint64_t backingSerial = 0; }; -using Lease = std::vector>; +class Lease { +public: + using Ranges = std::vector>; + using value_type = Ranges::value_type; + using const_iterator = Ranges::const_iterator; + using iterator = const_iterator; + + Lease() = default; + explicit Lease(std::shared_ptr ranges) : ranges(std::move(ranges)) {} + + const_iterator begin() const { return held().begin(); } + const_iterator end() const { return held().end(); } + std::size_t size() const { return held().size(); } + bool empty() const { return held().empty(); } + const value_type& operator[](std::size_t index) const { return held()[index]; } + const value_type& front() const { return held().front(); } + const value_type& back() const { return held().back(); } + void clear() { ranges.reset(); } + +private: + const Ranges& held() const { + static const Ranges none; + return ranges != nullptr ? *ranges : none; + } + + std::shared_ptr ranges; +}; +using Mapped = std::vector>; extern "C" { void* GuestAllocationsBegin_nid_postfix(); void GuestAllocationsRegisterMainImage_nid_postfix(void* mutation); +void GuestAllocationsRegisterImage_nid_postfix(void* mutation, const void* image); +void GuestAllocationsUnregisterImage_nid_postfix(void* mutation, const void* image); void GuestAllocationsEnd_nid_postfix(void* mutation) noexcept; -void GuestAllocationsAdd_nid_postfix(void* mutation, void* pointer, std::size_t bytes, bool readable, bool writable); +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, const std::function& apply); +void GuestAllocationsProtect_nid_postfix(void* mutation, const void* pointer, std::size_t bytes, bool readable, bool writable, bool gpu, const std::function& apply); void GuestAllocationsUnmap_nid_postfix(void* mutation, const void* pointer, std::size_t bytes, const std::function& apply); Lease GuestAllocationsAcquire_nid_postfix(); +Lease GuestAllocationsAcquireAll_nid_postfix(); +std::shared_ptr GuestAllocationsLookup_nid_postfix(std::uintptr_t address, bool next); // Changes whenever a mutation ends, so callers can cache facts about guest mappings between changes. std::uint64_t GuestAllocationsGeneration_nid_postfix(); // Precise invalidation for such caches: the callback runs, after the generation changed, for every @@ -47,7 +81,10 @@ void GuestAllocationsInvalidate_nid_postfix(std::uintptr_t address, std::size_t // and finishes the GPU work that holds leases; it returns whether it finished any (a round that found // nothing to wait for counts against the same bound as a plain spin). Without one the mutation spins // until the lease is dropped by another thread. -void GuestAllocationsSetPinWaiter_nid_postfix(bool (*callback)()); +void GuestAllocationsSetPinWaiter_nid_postfix(bool (*callback)(std::uintptr_t address, std::size_t bytes)); +void GuestAllocationsSetGpuMapObserver_nid_postfix(void (*callback)(const Mapped& ranges, std::uint64_t generation)); +void GuestAllocationsSetUnmapObserver_nid_postfix(void (*callback)(std::uintptr_t address, std::size_t bytes)); +void GuestAllocationsNoteGpuMapping_nid_postfix(void* mutation, const void* pointer, std::size_t bytes); } class Mutation { @@ -57,15 +94,19 @@ public: Mutation(const Mutation&) = delete; Mutation& operator=(const Mutation&) = delete; void RegisterMainImage() { GuestAllocationsRegisterMainImage_nid_postfix(handle); } - void Add(void* pointer, std::size_t bytes, bool readable, bool writable) { GuestAllocationsAdd_nid_postfix(handle, pointer, bytes, readable, writable); } + void RegisterImage(const void* image) { GuestAllocationsRegisterImage_nid_postfix(handle, image); } + void UnregisterImage(const void* image) { GuestAllocationsUnregisterImage_nid_postfix(handle, image); } + 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); } - void Protect(const void* pointer, std::size_t bytes, bool readable, bool writable, const std::function& apply) { GuestAllocationsProtect_nid_postfix(handle, pointer, bytes, readable, writable, apply); } + void Protect(const void* pointer, std::size_t bytes, bool readable, bool writable, bool gpu, const std::function& apply) { GuestAllocationsProtect_nid_postfix(handle, pointer, bytes, readable, writable, gpu, apply); } + void NoteGpuMapping(const void* pointer, std::size_t bytes) { GuestAllocationsNoteGpuMapping_nid_postfix(handle, pointer, bytes); } private: void* handle; diff --git a/core/libs/prx/libc/include/GuestArena.hpp b/core/libs/prx/libc/include/GuestArena.hpp index 7e57d242ae..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,26 +25,40 @@ 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). void GuestArenaRange_nid_postfix(std::uintptr_t* base, std::size_t* bytes); bool GuestArenaWriteWatched_nid_postfix(); -#ifdef _WIN32 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); void GuestArenaPinWritable_nid_postfix(const void* pointer, std::size_t bytes); void GuestArenaUnpinWritable_nid_postfix(const void* pointer, std::size_t bytes); bool GuestArenaProtection_nid_postfix(std::uintptr_t address, std::uint32_t* protection); bool GuestArenaCollectWrites_nid_postfix(std::uintptr_t address, std::size_t bytes, void** pages, std::size_t* count, bool clear); +bool GuestArenaCollectArmedWrites_nid_postfix(std::uintptr_t address, std::size_t bytes, void** pages, std::size_t* count, bool clear); +bool GuestArenaCollectFreshWrites_nid_postfix(std::uintptr_t address, std::size_t bytes, void** pages, std::size_t* count, bool clear); bool GuestArenaHostRegionOverlaps_nid_postfix(std::uintptr_t address, std::size_t bytes); std::uint64_t GuestArenaCommitGeneration_nid_postfix(); void GuestArenaSetPrivateMappingObserver_nid_postfix(void (*callback)(std::uintptr_t address, std::size_t bytes, std::uint64_t generation)); -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 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); @@ -47,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); } @@ -66,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 532db73fbe..3f83ab1a18 100644 --- a/core/libs/prx/libc/include/GuestDirectory.hpp +++ b/core/libs/prx/libc/include/GuestDirectory.hpp @@ -1,4 +1,5 @@ #pragma once +#include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -12,3 +13,5 @@ struct GuestDirectoryEntry { }; static_assert(offsetof(GuestDirectoryEntry, name) == 8); static_assert(sizeof(GuestDirectoryEntry) == 264); + +extern "C" void* APS5_VABI OpenDescriptorDirectory_nid_no_patch(const char* hostPath, int descriptor, int (APS5_VABI* closeDescriptor)(int), std::uint64_t position); diff --git a/core/libs/prx/libc/include/GuestHeap.hpp b/core/libs/prx/libc/include/GuestHeap.hpp index 65f9c0d427..2b0f8cebb7 100644 --- a/core/libs/prx/libc/include/GuestHeap.hpp +++ b/core/libs/prx/libc/include/GuestHeap.hpp @@ -7,11 +7,14 @@ namespace GuestHeap { extern "C" { +std::size_t GuestHeapUsableSize_nid_postfix(const void* pointer); void* GuestHeapAllocate_nid_postfix(std::size_t bytes); void GuestHeapFree_nid_postfix(void* pointer); 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/GuestStacks.hpp b/core/libs/prx/libc/include/GuestStacks.hpp new file mode 100644 index 0000000000..2b729a9bd5 --- /dev/null +++ b/core/libs/prx/libc/include/GuestStacks.hpp @@ -0,0 +1,19 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_GUESTSTACKS_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_GUESTSTACKS_HPP + +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +namespace GuestStacks { + +extern "C" { + +void GuestStackRegister_nid_no_patch(const void* pointer, std::size_t bytes); +void APS5_VABI GuestStackSwitch_nid_no_patch(std::uintptr_t stackPointer); + +} + +} + +#endif diff --git a/core/libs/prx/libc/include/GuestWriteWatch.hpp b/core/libs/prx/libc/include/GuestWriteWatch.hpp index 4e5d98dd5d..9afcf582db 100644 --- a/core/libs/prx/libc/include/GuestWriteWatch.hpp +++ b/core/libs/prx/libc/include/GuestWriteWatch.hpp @@ -12,7 +12,14 @@ 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); +void GuestWriteWatchProtectionChanged_nid_postfix(const void* pointer, std::size_t bytes, int protection); +int GuestWriteWatchProtection_nid_postfix(std::uintptr_t address, std::uintptr_t limit, std::uintptr_t* runEnd); +bool GuestWriteWatchCollectArmed_nid_postfix(std::uintptr_t address, std::size_t bytes, void (*written)(void* context, std::uintptr_t begin, std::uintptr_t end), void* context); +bool GuestWriteWatchCollectFresh_nid_postfix(std::uintptr_t address, std::size_t bytes, void (*written)(void* context, std::uintptr_t begin, std::uintptr_t end), void* context); } diff --git a/core/libs/prx/libc/include/HostEntropy.hpp b/core/libs/prx/libc/include/HostEntropy.hpp new file mode 100644 index 0000000000..3380f90236 --- /dev/null +++ b/core/libs/prx/libc/include/HostEntropy.hpp @@ -0,0 +1,8 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_HOSTENTROPY_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_HOSTENTROPY_HPP + +#include + +extern "C" void HostEntropy_nid_no_patch(void* buffer, std::size_t size); + +#endif 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 e9fc324215..b187039495 100644 --- a/core/libs/prx/libc/include/PreciseWait.hpp +++ b/core/libs/prx/libc/include/PreciseWait.hpp @@ -1,20 +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 -#endif +#include "prx/libc/include/HostThreadLocal.hpp" -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); @@ -24,24 +48,257 @@ inline void PreciseSleepUs(unsigned long long micros) { } } }; - thread_local Timer timer; - 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..4364a80d39 --- /dev/null +++ b/core/libs/prx/libc/include/RenameDot.hpp @@ -0,0 +1,28 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_RENAMEDOT_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_RENAMEDOT_HPP + +#include +#include +#include "FilesystemError.hpp" + +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(".") : 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/Shutdown.hpp b/core/libs/prx/libc/include/Shutdown.hpp index dc40070988..a59aa05ced 100644 --- a/core/libs/prx/libc/include/Shutdown.hpp +++ b/core/libs/prx/libc/include/Shutdown.hpp @@ -11,6 +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 new file mode 100644 index 0000000000..cf1879e2c0 --- /dev/null +++ b/core/libs/prx/libc/include/SonyCrt.hpp @@ -0,0 +1,277 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_SONYCRT_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_SONYCRT_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "SceTypes.hpp" +#include "prx/libc/include/GuestTime.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +extern "C" GuestTm* APS5_VABI localtime_s_nid_postfix(const std::int64_t* timer, GuestTm* result); + +using SonyConstraintHandler = void (APS5_VABI *)(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; + std::abort(); +} + +inline std::mutex& SonyConstraintMutex() { + static std::mutex mutex; + return mutex; +} + +inline SonyConstraintHandler& SonyConstraintHandlerSlot() { + static SonyConstraintHandler handler = SonyDefaultConstraintHandler; + 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; + { + std::lock_guard lock(SonyConstraintMutex()); + handler = SonyConstraintHandlerSlot(); + } + if (handler) handler(message, pointer, error); +} + +inline char* SonyStrtokS(char* str, const char* delim, char** context) { + if (!delim || !context) return nullptr; + char* cursor = str ? str : *context; + if (!cursor) return nullptr; + cursor += std::strspn(cursor, delim); + if (*cursor == '\0') { + *context = cursor; + return nullptr; + } + char* token = cursor; + cursor += std::strcspn(cursor, delim); + if (*cursor != '\0') { + *cursor = '\0'; + *context = cursor + 1; + } else { + *context = cursor; + } + return token; +} + +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; + 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, year); + return 0; +} + +inline int SonyCtimeS(char* buffer, std::size_t size, const std::int64_t* time) { + if (!buffer || size < 26) return EINVAL; + if (!time) { + buffer[0] = '\0'; + return EINVAL; + } + GuestTm parts{}; + 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) return EINVAL; + if (!time) { + buffer[0] = '\0'; + return EINVAL; + } + return SonyFormatAsctime(buffer, size, *time); +} + +inline void SonyIgnoreHandlerS(const char* message, void* pointer, int error) { + (void)message; + (void)pointer; + (void)error; +} + +inline void* SonySetConstraintHandlerS(void* handler) { + std::lock_guard lock(SonyConstraintMutex()); + void* previous = reinterpret_cast(SonyConstraintHandlerSlot()); + SonyConstraintHandlerSlot() = handler ? reinterpret_cast(handler) : SonyDefaultConstraintHandler; + return previous; +} + +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; + } + std::size_t i = 0; + while (i + 1 < size && src[i] != 0) { + dest[i] = src[i]; + ++i; + } + dest[i] = 0; + if (src[i] != 0) { + SonyInvokeConstraintHandler("wcscpy_s", dest, ERANGE); + return ERANGE; + } + return 0; +} + +inline std::size_t SonyMbstowcs(std::uint16_t* dest, const char* src, std::size_t size) { + if (!src) return 0; + if (!dest) return std::strlen(src); + std::size_t converted = 0; + while (converted < size) { + const auto value = static_cast(src[converted]); + dest[converted] = value; + if (value == 0) return converted; + ++converted; + } + return converted; +} + +inline SonyLconv* SonyLocaleconv(void) { + static char empty[] = ""; + static char point[] = "."; + static char falseText[] = "false"; + static char trueText[] = "true"; + static SonyLconv value = [] { + SonyLconv init{}; + for (auto& pointer : init.monetary) pointer = empty; + for (auto& byte : init.chars) byte = CHAR_MAX; + init.decimal_point = point; + init.grouping = empty; + init.thousands_sep = empty; + init.frac_grouping = empty; + init.frac_sep = empty; + init.false_text = falseText; + init.true_text = trueText; + init.no_text = empty; + init.yes_text = empty; + return init; + }(); + return &value; +} + +inline int SonyFpclassifyd(double value) { + if (std::isnan(value)) return 2; + if (std::isinf(value)) return 1; + if (value == 0.0) return 0; + if (std::fpclassify(value) == FP_SUBNORMAL) return -2; + return -1; +} + +inline double SonySinh(double x, double y) { + return y * std::sinh(x); +} + +inline int SonyFltrounds(void) { + switch (std::fegetround()) { + case FE_TONEAREST: return 1; + case FE_DOWNWARD: return 3; + case FE_UPWARD: return 2; + case FE_TOWARDZERO: return 0; + default: return 1; + } +} + +inline unsigned __int128 SonyUdivmod128(unsigned __int128 dividend, unsigned __int128 divisor, unsigned __int128* remainderOut) { + unsigned __int128 quotient = 0; + unsigned __int128 remainder = 0; + for (int i = 127; i >= 0; --i) { + remainder = (remainder << 1) | ((dividend >> i) & 1); + if (remainder >= divisor) { + remainder -= divisor; + quotient |= (unsigned __int128)1 << i; + } + } + *remainderOut = remainder; + return quotient; +} + +inline unsigned __int128 SonyUdivti3(unsigned __int128 dividend, unsigned __int128 divisor) { + if (divisor == 0) throw std::runtime_error("__udivti3: integer divide by zero"); + unsigned __int128 remainder = 0; + return SonyUdivmod128(dividend, divisor, &remainder); +} + +inline unsigned __int128 SonyUmodti3(unsigned __int128 dividend, unsigned __int128 divisor) { + if (divisor == 0) { + char message[96]; + std::snprintf(message, sizeof(message), "__umodti3: integer divide by zero (0x%016llx%016llx %% 0)", + (unsigned long long)(dividend >> 64), (unsigned long long)dividend); + throw std::runtime_error(message); + } + unsigned __int128 remainder = 0; + SonyUdivmod128(dividend, divisor, &remainder); + return remainder; +} + +inline __int128 SonyDivti3(__int128 dividend, __int128 divisor) { + if (divisor == 0) throw std::runtime_error("__divti3: integer divide by zero"); + const bool negative = (dividend < 0) != (divisor < 0); + const unsigned __int128 magnitudeDividend = dividend < 0 + ? (unsigned __int128)0 - (unsigned __int128)dividend + : (unsigned __int128)dividend; + const unsigned __int128 magnitudeDivisor = divisor < 0 + ? (unsigned __int128)0 - (unsigned __int128)divisor + : (unsigned __int128)divisor; + const unsigned __int128 magnitude = SonyUdivti3(magnitudeDividend, magnitudeDivisor); + 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 new file mode 100644 index 0000000000..66ebc1d9ac --- /dev/null +++ b/core/libs/prx/libc/include/WindowsDescriptor.hpp @@ -0,0 +1,54 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_WINDOWSDESCRIPTOR_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_WINDOWSDESCRIPTOR_HPP + +#include +#include +#include +#include +#include +#include + +extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); + +static inline HANDLE WindowsDescriptorHandle_nid_no_patch(int descriptor) { + const auto ignore = +[](const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {}; + const auto previous = _set_thread_local_invalid_parameter_handler(ignore); + const auto handle = ::_get_osfhandle(descriptor); + _set_thread_local_invalid_parameter_handler(previous); + return reinterpret_cast(handle); +} + +static inline 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)); + 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; + throw std::runtime_error(std::string(function) + ": descriptor " + std::to_string(descriptor) + " grants neither read nor write access"); +} + +#endif diff --git a/core/libs/prx/libc/include/WindowsFormatting.hpp b/core/libs/prx/libc/include/WindowsFormatting.hpp index 43c99d1118..0c8dd2861f 100644 --- a/core/libs/prx/libc/include/WindowsFormatting.hpp +++ b/core/libs/prx/libc/include/WindowsFormatting.hpp @@ -2,6 +2,10 @@ #define CORE_LIBS_PRX_LIBC_INCLUDE_WINDOWSFORMATTING_HPP #include "SceTypes.hpp" +#include +#include +#include +#include #include #include #include @@ -11,17 +15,53 @@ #include #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; + size_t position = 1; public: explicit FormatArguments(const void* source) { std::memcpy(&args, source, sizeof(args)); } + void Number(const std::vector& classes) { + numbered.resize(classes.size()); + for (size_t index = 0; index < classes.size(); ++index) { + const auto store = [&](auto value) { std::memcpy(numbered[index].data(), &value, sizeof(value)); }; + if (classes[index] == ArgumentClass::LongDouble) store(Fetch()); + else if (classes[index] == ArgumentClass::Double) store(Fetch()); + else store(Fetch()); + } + } + + void Seek(size_t index) { position = index; } + + template T At(size_t index) { + if (index == 0 || index > numbered.size()) throw std::out_of_range("Format argument position out of range"); + T value; + std::memcpy(&value, numbered[index - 1].data(), sizeof(value)); + return value; + } + template T Next() { + return numbered.empty() ? Fetch() : At(position++); + } + +private: + template T Fetch() { const void* address; if constexpr (std::is_same_v) { if (args.fp_offset < 176) { @@ -121,11 +161,112 @@ inline void AppendUtf8(std::string& utf8, char32_t code) { } } -inline int FormatWindows(char* buffer, size_t size, const char* format, const void* source, std::string* complete = nullptr) { +inline std::string NanSpec(const std::string& spec) { + std::string result = "%"; + std::size_t index = 1; + for (; index < spec.size() && std::strchr("-+ #0", spec[index]); ++index) + if (spec[index] == '-' && result.size() == 1) result += '-'; + for (; index < spec.size() && spec[index] >= '0' && spec[index] <= '9'; ++index) result += spec[index]; + return result + 's'; +} + +inline std::string FormatPointer(const std::string& spec, const void* pointer) { + std::size_t index = 1; + bool left = false; + bool zero = false; + for (; index < spec.size() && std::strchr("-+ #0", spec[index]); ++index) { + if (spec[index] == '-') left = true; + if (spec[index] == '0') zero = true; + } + const auto readNumber = [&] { + std::size_t number = 0; + for (; index < spec.size() && spec[index] >= '0' && spec[index] <= '9'; ++index) { + number = number * 10 + static_cast(spec[index] - '0'); + if (number > static_cast(INT_MAX)) throw std::overflow_error("Formatted output exceeds INT_MAX"); + } + return number; + }; + const std::size_t width = readNumber(); + bool hasPrecision = false; + std::size_t precision = 0; + if (index < spec.size() && spec[index] == '.') { + ++index; + hasPrecision = true; + precision = readNumber(); + } + const auto value = static_cast(reinterpret_cast(pointer)); + std::string digits; + 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 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; + +inline size_t ArgumentPosition(const char*& format) { + const char* end = format; + while (*end >= '0' && *end <= '9') ++end; + if (end == format || *end != '$') return 0; + size_t position = 0; + for (const char* digit = format; digit != end && position <= maximumArgumentPosition; ++digit) + position = position * 10 + static_cast(*digit - '0'); + if (position == 0 || position > maximumArgumentPosition) throw std::invalid_argument("Format argument position out of range"); + format = end + 1; + return position; +} + +inline std::vector NumberedArgumentClasses(const char* format) { + if (!std::strchr(format, '$')) return {}; + std::vector classes; + bool numbered = false; + size_t next = 1; + const auto take = [&](size_t position, ArgumentClass type) { + if (classes.size() < position) classes.resize(position); + classes[position - 1] = type; + }; + while ((format = std::strchr(format, '%')) != nullptr) { + ++format; + if (*format == '%') { + ++format; + continue; + } + if (const size_t position = ArgumentPosition(format)) { + next = position; + numbered = true; + } + while (*format && std::strchr("-+ #0'123456789.*", *format)) { + if (*format++ != '*') continue; + const size_t position = ArgumentPosition(format); + if (position) numbered = true; + take(position ? position : next++, ArgumentClass::Integer); + } + bool longDouble = false; + while (*format && std::strchr("hljztL", *format)) longDouble = *format++ == 'L'; + const char conversion = *format; + if (!conversion) break; + ++format; + if (std::strchr("aAeEfFgG", conversion)) take(next++, longDouble ? ArgumentClass::LongDouble : ArgumentClass::Double); + else if (std::strchr("diouxXcCsSpn", conversion)) take(next++, ArgumentClass::Integer); + } + if (!numbered) classes.clear(); + return classes; +} + +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; FormatArguments args(source); + args.Number(NumberedArgumentClasses(format)); FormatOutput output(buffer, size, complete); + const auto star = [&] { + const size_t position = ArgumentPosition(format); + return position ? args.At(position) : args.Next(); + }; while (*format) { const char* literal = format; while (*format && *format != '%') ++format; @@ -136,11 +277,13 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo output.Append(format++, 1); continue; } + if (const size_t position = ArgumentPosition(format)) args.Seek(position); std::string spec = "%"; - while (*format && std::strchr("-+ #0", *format)) spec += *format++; + for (; *format && std::strchr("-+ #0'", *format); ++format) + if (*format != '\'') spec += *format; if (*format == '*') { ++format; - const int width = args.Next(); + const int width = star(); if (width < 0) spec += '-'; spec += std::to_string(width < 0 ? -static_cast(width) : width); } else { @@ -151,7 +294,7 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo ++format; if (*format == '*') { ++format; - const int precision = args.Next(); + const int precision = star(); if (precision >= 0) { spec += "." + std::to_string(precision); precisionLimit = static_cast(precision); @@ -189,34 +332,48 @@ 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(); if (length == "h") value = static_cast(value); if (length == "hh") value = static_cast(value); } else value = args.Next(); + if (conversion == 'o' && value == 0 && precisionLimit == 0 && spec.find('#') != std::string::npos) + spec = spec.substr(0, spec.find('.')) + ".1"; output.Value(spec + "ll" + conversion, value); } else if (std::strchr("aAeEfFgG", conversion)) { + const char* nan = conversion >= 'a' ? "nan" : "NAN"; if (length == "L") { static_assert(sizeof(long double) == 16); static_assert(std::numeric_limits::digits == 64); - output.Value(spec + "L" + conversion, args.Next()); + const long double value = args.Next(); + if (std::isnan(value)) output.Value(NanSpec(spec), nan); + else output.Value(spec + "L" + conversion, value); } else { if (!length.empty() && length != "l") throw std::invalid_argument("Invalid floating length"); - output.Value(spec + conversion, args.Next()); + const double value = args.Next(); + if (std::isnan(value)) output.Value(NanSpec(spec), nan); + else output.Value(spec + conversion, value); } } else if (conversion == 'c' && (length.empty() || length == "h")) { output.Value(spec + conversion, args.Next()); } else if ((conversion == 'c' && length == "l") || (conversion == 'C' && length.empty())) { std::string utf8; AppendUtf8(utf8, static_cast(args.Next())); - output.Value(spec + 's', utf8.c_str()); + if (utf8[0] == 0) output.Value(spec + 'c', 0); + 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) { @@ -230,8 +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()) { - output.Value(spec + conversion, args.Next()); + } 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"); @@ -250,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 3c423d1485..624685e8a9 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -4,9 +4,11 @@ #ifdef _WIN32 #include #include +#include #include #include #include +#include #include #include #include @@ -19,9 +21,11 @@ namespace GuestArena { class WindowsMappings { 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) { @@ -31,6 +35,7 @@ public: std::vector> Commit(void* address, std::size_t bytes, DWORD protection, std::size_t granule, bool watched) { std::vector> created; std::lock_guard lock(mutex); + collectableRanges.clear(); const auto end = reinterpret_cast(address) + bytes; for (auto cursor = reinterpret_cast(address); cursor < end;) { const auto memory = query(cursor); @@ -55,7 +60,7 @@ public: const auto mapped = views.find(cursor & ~(pageBytes - 1)); if (mapped != views.end()) { mapped->second.protection = protection; - mapped->second.armed = false; + mapped->second.armed = 0; invalidate(*mapped->second.page); } DWORD previous; @@ -71,37 +76,88 @@ public: reset(reinterpret_cast(address), bytes); } - void Map(void* address, std::size_t bytes, HANDLE section, std::uint64_t offset, DWORD protection) { + void Map(void* address, std::size_t bytes, HANDLE section, std::uint64_t offset, DWORD protection, bool watched = false, bool resident = false, 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; if (!DuplicateHandle(GetCurrentProcess(), section, GetCurrentProcess(), &duplicate, 0, FALSE, DUPLICATE_SAME_ACCESS)) fail("keep shared guest section"); const auto owned = std::make_shared
(duplicate); - for (std::size_t done = 0; done < bytes; done += pageBytes) { - split(cursor + done, pageBytes); - void* page = reinterpret_cast(cursor + done); - if (!map(section, GetCurrentProcess(), page, offset + done, pageBytes, MEM_REPLACE_PLACEHOLDER, PAGE_EXECUTE_READWRITE, nullptr, 0)) fail("map shared guest page"); - DWORD previous; - if (!VirtualProtect(page, pageBytes, protection, &previous)) fail("protect shared guest page"); - const auto key = std::make_pair(reinterpret_cast(section), offset + done); - auto shared = physical[key].lock(); + const auto slotOf = [&](std::size_t done) -> const std::shared_ptr& { + auto& slot = physical[std::make_pair(reinterpret_cast(section), offset + done)]; + if (!slot) slot = std::make_shared(); + return slot; + }; + const auto pageOf = [](Physical& slot) { + auto shared = slot.page.lock(); if (!shared) { shared = std::make_shared(); - physical[key] = shared; + slot.page = shared; } + return shared; + }; + const auto single = [&](std::size_t done) { + const auto& slot = *slotOf(done); + const auto shared = slot.page.lock(); + return resident && !slot.stored && (!shared || shared->aliases.empty()); + }; + for (std::size_t done = 0; done < bytes;) { const auto base = cursor + done; + if (single(done)) { + auto runBytes = pageBytes; + while (done + runBytes < bytes && (base + runBytes) % chunkBytes != 0 && single(done + runBytes)) runBytes += pageBytes; + split(base, runBytes); + commitResident(base, runBytes); + 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); + const auto shared = pageOf(*slot); + if (slot->saved) { + std::memcpy(reinterpret_cast(base + at), slot->saved->bytes + slot->savedOffset, pageBytes); + slot->saved.reset(); + } + shared->aliases.push_back(base + at); + views.emplace(base + at, View{shared, protection, {}, 0, owned, offset + done + at, 0, 0, true, slot}); + invalidate(*shared); + } + remember(freshRanges, base, base + runBytes); + DWORD previous; + if (protection != PAGE_READWRITE && !VirtualProtect(reinterpret_cast(base), runBytes, protection, &previous)) fail("protect resident guest pages"); + done += runBytes; + continue; + } + const auto slot = slotOf(done); + const auto shared = pageOf(*slot); + const auto aliases = shared->aliases; + for (const auto alias : aliases) { + if (views.at(alias).resident) demote(alias); + } + const auto viewBytes = std::min(pageBytes, sectionBytes - done); + split(base, pageBytes); + mapShared(base, section, offset + done, protection, viewProtection, viewBytes); + slot->stored = true; shared->aliases.push_back(base); - views.emplace(base, View{shared, protection, 0, false, owned, offset + done, 0}); + views.emplace(base, View{shared, protection, {}, 0, owned, offset + done, 0, 0, false, slot, hostPages(viewBytes)}); + remember(freshRanges, base, base + pageBytes); invalidate(*shared); + done += pageBytes; } } void SetProtection(std::uintptr_t address, std::size_t bytes, DWORD protection) { std::lock_guard lock(mutex); + collectableRanges.clear(); for (auto it = views.lower_bound(address); it != views.end() && it->first < address + bytes; ++it) { it->second.protection = protection; - it->second.armed = false; + it->second.armed = 0; invalidate(*it->second.page); } } @@ -113,10 +169,9 @@ public: ++page.pins; for (const auto alias : page.aliases) { auto& view = views.at(alias); - if (!view.armed) continue; - DWORD previous; - if (!VirtualProtect(reinterpret_cast(alias), pageBytes, view.protection, &previous)) fail("pin shared guest page writable"); - view.armed = false; + if (view.armed == 0) continue; + protectSubpages(alias, view.armed, view.protection, "pin shared guest page writable", view.viewBytes); + view.armed = 0; } invalidate(page); } @@ -137,14 +192,49 @@ public: const auto found = views.find(base); if (found == views.end() || !writable(found->second.protection)) return false; auto& view = found->second; - invalidate(*view.page); - DWORD previous; - if (!VirtualProtect(reinterpret_cast(base), pageBytes, view.protection, &previous)) fail("resume shared memory write"); - view.armed = false; + const auto written = subpagesOf(base, address, address + 1); + if (view.resident && (view.armed & written) == 0) { + const auto memory = query(address); + return memory.State == MEM_COMMIT && writable(memory.Protect & 0xffu); + } + if (view.resident) { + resumeResidentWrites(base); + return true; + } + invalidate(*view.page, written); + protectSubpages(base, written, view.protection, "resume shared memory write", view.viewBytes); + view.armed &= static_cast(~written); return true; } - bool BeginHostWrite(std::uintptr_t address, std::size_t bytes) { + bool HandleRead(std::uintptr_t address) { + std::lock_guard lock(mutex); + if (!views.contains(address & ~(pageBytes - 1))) return false; + const auto memory = query(address); + return memory.State == MEM_COMMIT && (memory.Protect & (PAGE_NOACCESS | PAGE_GUARD)) == 0; + } + + bool ResidentSpan(std::uintptr_t address, std::size_t bytes, std::uintptr_t* start, std::uintptr_t* end) { + std::lock_guard lock(mutex); + auto run = residentRuns.upper_bound(address); + if (run != residentRuns.begin() && std::prev(run)->second.end > address) --run; + if (run == residentRuns.end() || run->first >= address + bytes) return false; + *start = run->first; + for (; run != residentRuns.end() && run->first < address + bytes; ++run) *end = run->second.end; + return true; + } + + bool ForgetSection(HANDLE section) { + std::lock_guard lock(mutex); + const auto key = reinterpret_cast(section); + 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 store = true) { std::lock_guard lock(mutex); const auto first = views.lower_bound(address & ~(pageBytes - 1)); const auto end = address + bytes; @@ -153,22 +243,27 @@ public: } for (auto it = first; it != views.end() && it->first < end; ++it) { auto& view = it->second; - ++view.hostWrites; - invalidate(*view.page); - if (!view.armed) continue; - DWORD previous; - if (!VirtualProtect(reinterpret_cast(it->first), pageBytes, view.protection, &previous)) fail("open shared memory to a host write"); - view.armed = false; + if (store) ++view.hostWrites; + else ++view.hostImports; + const auto written = subpagesOf(it->first, address, end); + 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", 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); + 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); } } @@ -180,16 +275,33 @@ public: return std::runtime_error(text); }; if (address % pageBytes != 0 || bytes % pageBytes != 0 || bytes == 0) throw refuse("the range is not made of whole shared pages"); + for (std::size_t done = 0; done < bytes; done += pageBytes) { + const auto found = views.find(address + done); + if (found == views.end() || !found->second.resident) continue; + const auto run = std::prev(residentRuns.upper_bound(address + done)); + if (run->first < address || run->second.end > address + bytes) throw refuse("a resident run of direct memory extends outside the range"); + for (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); + if (found != views.end() && found->second.resident) demote(address + done); + } auto view = views.find(address); if (view == views.end()) throw refuse("the range does not start at a shared view"); const auto section = view->second.section; const auto offset = view->second.offset; SYSTEM_INFO system{}; GetSystemInfo(&system); - for (std::size_t done = 0; done < bytes; done += pageBytes, ++view) { - if (view == views.end() || view->first != address + done || view->second.offset != offset + done) throw refuse("the range is not one contiguous run of views of a section"); - if (view->second.section != section && !sameSection(view->second.section->handle, section->handle)) throw refuse("the range spans several sections"); + bool contiguous = true; + for (std::size_t done = 0; done < bytes; done += pageBytes) { + const auto page = views.find(address + done); + if (page == views.end()) throw refuse("the range is not a run of shared views"); + if (page->second.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); const auto lead = offset % system.dwAllocationGranularity; void* alias = map(section->handle, GetCurrentProcess(), nullptr, offset - lead, lead + bytes, 0, PAGE_READWRITE, nullptr, 0); if (alias == nullptr) { @@ -202,6 +314,15 @@ public: void UnmapAlias(void* alias) { if (alias == nullptr) return; + { + std::lock_guard lock(mutex); + if (const auto found = splitAliases.find(reinterpret_cast(alias)); found != splitAliases.end()) { + const auto error = releaseAlias(found->first, found->second); + splitAliases.erase(found); + if (error != ERROR_SUCCESS) throw std::system_error(static_cast(error), std::system_category(), "unmap a run of a shared guest alias"); + return; + } + } SYSTEM_INFO system{}; GetSystemInfo(&system); const auto base = reinterpret_cast(alias) & ~(static_cast(system.dwAllocationGranularity) - 1); @@ -216,11 +337,32 @@ public: return true; } - bool Collect(std::uintptr_t address, std::size_t bytes, void** pages, std::size_t* count, bool clear) { + bool Collect(std::uintptr_t address, std::size_t bytes, void** pages, std::size_t* count, bool clear, Collection collection = Collection::All) { std::lock_guard lock(mutex); const auto capacity = *count; *count = 0; const auto end = address + bytes; + if (collection == Collection::Fresh) { + const auto fresh = freshRanges.upper_bound(address); + if ((fresh == freshRanges.begin() || std::prev(fresh)->second <= address) && (fresh == freshRanges.end() || fresh->first >= end)) return true; + for (auto it = views.lower_bound(address & ~(pageBytes - 1)); it != views.end() && it->first < end; ++it) { + if (it->second.resident) { + if (!watchResident) return false; + continue; + } + if (it->second.protection != PAGE_NOACCESS) continue; + for (auto at = std::max(address, it->first); at < std::min(end, it->first + pageBytes); at += 4096) { + if ((it->second.collected & (1u << ((at - it->first) / 4096))) == 0) return false; + } + } + } else if (clear && !collectable(address, end)) return false; + auto position = views.lower_bound(address & ~(pageBytes - 1)); + 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()) { @@ -231,39 +373,110 @@ public: } } const auto base = cursor & ~(pageBytes - 1); - const auto found = views.find(base); - if (found != views.end()) { + const auto found = viewAt(base, position); + if (found != views.end() && found->second.resident) { + 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, nextClean == cleanRanges.end() ? end : nextClean->first}); + collectResident(run->first, run->second, cursor, stop, pages, count, clear, collection); + if (*count == capacity) return true; + cursor = stop; + } else if (found != views.end()) { auto& view = found->second; - const auto stop = std::min(end, base + pageBytes); - if (view.protection == PAGE_NOACCESS) return false; - const bool pinned = view.page->pins != 0; - if (pinned || view.seen != view.page->generation) { - const auto needed = (stop - cursor + 4095) / 4096; - if (needed > capacity - *count) { - for (auto at = cursor; *count < capacity; at += 4096) pages[(*count)++] = reinterpret_cast(at); - return true; + auto stop = std::min(std::min(end, base + pageBytes), nextClean == cleanRanges.end() ? end : nextClean->first); + if (collection != Collection::All) { + const auto selected = [&](std::uintptr_t at) { + const bool collected = (view.collected & (1u << ((at - base) / 4096))) != 0; + return collection == Collection::Armed ? collected : !collected; + }; + while (cursor < stop && !selected(cursor)) cursor = std::min(stop, cursor + 4096); + if (cursor == stop) continue; + for (auto at = cursor + 4096; at < stop; at += 4096) { + if (!selected(at)) { + stop = at; + break; + } } - for (auto at = cursor; at < stop; at += 4096) pages[(*count)++] = reinterpret_cast(at); } - if (clear && !pinned) { + if (view.protection == PAGE_NOACCESS) { + flushClean(); + return false; + } + const bool pinned = view.page->pins != 0; + std::uint8_t settled = 0; + auto settledEnd = cursor; + bool full = false; + for (std::size_t sub = 0; sub < subpages; ++sub) { + const auto from = std::max(cursor, base + sub * subpageBytes); + const auto to = std::min(stop, base + (sub + 1) * subpageBytes); + if (from >= to) continue; + if (collection == Collection::Fresh || pinned || view.seen[sub] != view.page->generation[sub]) { + if (*count == capacity) { + full = true; + break; + } + pages[(*count)++] = reinterpret_cast(from); + } + settled |= static_cast(1u << sub); + settledEnd = to; + } + if (clear) { + for (std::size_t sub = 0; sub < subpages; ++sub) { + const auto bit = static_cast(1u << sub); + if ((settled & bit) == 0 || (view.collected & bit) != 0) continue; + view.collected |= bit; + forget(freshRanges, base + sub * subpageBytes, base + (sub + 1) * subpageBytes); + } + } + if (clear && !pinned && settled != 0) { + std::uint8_t untracked = 0; for (const auto alias : view.page->aliases) { auto& other = views.at(alias); - if (!writable(other.protection) || other.armed || other.hostWrites != 0) continue; - DWORD previous; + if (other.resident) continue; + const auto arming = static_cast(settled & ~other.armed); + if (!writable(other.protection) || arming == 0) continue; + if (other.hostWrites != 0) { + untracked |= arming; + continue; + } const DWORD protection = other.protection == PAGE_EXECUTE_READWRITE ? PAGE_EXECUTE_READ : PAGE_READONLY; - if (!VirtualProtect(reinterpret_cast(alias), pageBytes, protection, &previous)) fail("arm shared memory write tracking"); - other.armed = true; + protectSubpages(alias, arming, protection, "arm shared memory write tracking", other.viewBytes); + other.armed |= arming; } - view.seen = view.page->generation; - rememberClean(base, base + pageBytes); + const auto tracked = static_cast(settled & ~untracked); + 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 != cleanEnd) { + flushClean(); + cleanStart = from; + } + cleanEnd = to; + } + } + 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); + continue; + } const auto memory = query(cursor); if (memory.State != MEM_COMMIT || memory.Type != MEM_PRIVATE) return false; const auto stop = std::min(end, reinterpret_cast(memory.BaseAddress) + memory.RegionSize); - ULONG_PTR available = capacity - *count; - if (available == 0) return true; + if (capacity == *count) return true; + const auto touched = (((stop + 4095) & ~static_cast(4095)) - (cursor & ~static_cast(4095))) / 4096; + ULONG_PTR available = std::min(capacity - *count, touched); DWORD granularity = 0; if (GetWriteWatch(clear ? WRITE_WATCH_FLAG_RESET : 0, reinterpret_cast(cursor), stop - cursor, pages + *count, &available, &granularity) != 0) fail("collect private guest writes"); *count += available; @@ -271,13 +484,42 @@ public: cursor = stop; } } + flushClean(); return true; } private: + bool collectable(std::uintptr_t address, std::uintptr_t end) { + const auto proven = collectableRanges.upper_bound(address); + if (proven != collectableRanges.begin() && std::prev(proven)->second >= end) return true; + auto position = views.lower_bound(address & ~(pageBytes - 1)); + for (auto cursor = address; cursor < end;) { + const auto base = cursor & ~(pageBytes - 1); + if (const auto found = viewAt(base, position); found != views.end()) { + if (found->second.protection == PAGE_NOACCESS || (found->second.resident && !watchResident)) return false; + cursor = std::min(end, base + pageBytes); + continue; + } + const auto memory = query(cursor); + if (memory.State != MEM_COMMIT || memory.Type != MEM_PRIVATE) return false; + cursor = std::min(end, reinterpret_cast(memory.BaseAddress) + memory.RegionSize); + } + remember(collectableRanges, address, end); + return true; + } + static constexpr std::size_t pageBytes = 0x4000; + static constexpr std::size_t subpageBytes = 0x1000; + static_assert(subpageBytes == 4096, "Collect reports one page per subpage"); + static constexpr std::size_t subpages = pageBytes / subpageBytes; + static constexpr std::uint8_t allSubpages = (1u << subpages) - 1; + static constexpr std::size_t chunkBytes = 0x200000; struct SharedPage { - std::uint64_t generation = 1; + std::array generation = [] { + std::array initial{}; + initial.fill(1); + return initial; + }(); std::vector aliases; std::uint32_t pins = 0; }; @@ -288,41 +530,333 @@ private: Section& operator=(const Section&) = delete; ~Section() { CloseHandle(handle); } }; + struct Saved { + 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; + bool stored = false; + std::shared_ptr saved; + std::size_t savedOffset = 0; + }; struct View { std::shared_ptr page; DWORD protection; - std::uint64_t seen; - bool armed; + std::array seen; + std::uint8_t armed; std::shared_ptr
section; std::uint64_t offset; std::uint32_t hostWrites; + std::uint8_t collected = 0; + bool resident = false; + std::shared_ptr slot; + std::size_t viewBytes = pageBytes; + std::uint32_t hostImports = 0; }; - void forgetClean(std::uintptr_t start, std::uintptr_t end) { - auto it = cleanRanges.lower_bound(start); - if (it != cleanRanges.begin() && std::prev(it)->second > start) --it; - while (it != cleanRanges.end() && it->first < end) { + struct Run { + std::uintptr_t end; + std::array collected; + std::uint32_t fresh; + bool writeObserved = false; + }; + struct Window { + void* view = nullptr; + unsigned char* bytes = nullptr; + }; + using Ranges = std::map; + + static bool runCollected(const Run& run, std::size_t bit) { + return (run.collected[bit >> 6] >> (bit & 63) & 1) != 0; + } + + void collectResident(std::uintptr_t start, Run& run, std::uintptr_t from, std::uintptr_t stop, void** pages, std::size_t* count, bool clear, Collection collection) { + const auto first = from & ~std::uintptr_t{4095}; + 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); + ULONG_PTR reported = found.size(); + DWORD granularity = 0; + if (GetWriteWatch(clear ? WRITE_WATCH_FLAG_RESET : 0, reinterpret_cast(first), stop - first, found.data(), &reported, &granularity) != 0) fail("collect resident guest writes"); + for (ULONG_PTR index = 0; index < reported; ++index) written.push_back(reinterpret_cast(found[index])); + } + auto next = written.begin(); + std::uintptr_t resetFrom = 0; + const auto flushReset = [&](std::uintptr_t to) { + if (resetFrom != 0 && ResetWriteWatch(reinterpret_cast(resetFrom), to - resetFrom) != 0) fail("reset resident guest writes"); + resetFrom = 0; + }; + for (auto at = first; at < stop; at += 4096) { + const auto bit = (at - start) >> 12; + const bool fresh = !runCollected(run, bit); + bool wrote = false; + while (next != written.end() && *next < at) ++next; + if (next != written.end() && *next == at) wrote = true; + const bool report = collection == Collection::Fresh ? fresh : collection == Collection::Armed ? !fresh && wrote : fresh || wrote; + if (report) pages[(*count)++] = reinterpret_cast(std::max(at, from)); + const bool settle = clear && fresh && collection != Collection::Armed; + if (settle) { + run.collected[bit >> 6] |= 1ull << (bit & 63); + --run.fresh; + forget(freshRanges, at, at + 4096); + if (collection == Collection::Fresh && resetFrom == 0) resetFrom = at; + } else if (collection == Collection::Fresh) { + flushReset(at); + } + } + if (collection == Collection::Fresh) flushReset(stop); + } + + 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; + window.view = map(section, GetCurrentProcess(), nullptr, offset - lead, lead + bytes, 0, PAGE_READWRITE, nullptr, 0); + if (window.view == nullptr) fail("open a window on direct memory"); + window.bytes = static_cast(window.view) + lead; + return window; + } + + void close(Window& window) { + if (window.view != nullptr && !unmap(GetCurrentProcess(), window.view, 0)) fail("close a window on direct memory"); + window = {}; + } + + void commitResident(std::uintptr_t base, std::size_t bytes) { + const DWORD flags = MEM_RESERVE | MEM_COMMIT | MEM_REPLACE_PLACEHOLDER | (watchResident ? MEM_WRITE_WATCH : 0); + if (!allocate(GetCurrentProcess(), reinterpret_cast(base), bytes, flags, PAGE_READWRITE, nullptr, 0)) fail("replace guest placeholder with resident direct memory"); + } + + 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); + 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, 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) { + const auto* words = reinterpret_cast(page); + for (std::size_t i = 0; i < pageBytes / sizeof(std::uint64_t); ++i) { + if (words[i] != 0) return false; + } + return true; + } + + void demote(std::uintptr_t address) { + const auto run = std::prev(residentRuns.upper_bound(address)); + const auto start = run->first; + const auto stop = run->second.end; + DWORD previous; + if (!VirtualProtect(reinterpret_cast(start), stop - start, PAGE_READONLY, &previous)) fail("fence resident guest pages"); + const auto section = views.at(start).section; + auto window = open(section->handle, views.at(start).offset, stop - start); + if (query(reinterpret_cast(window.bytes)).State == MEM_RESERVE && !VirtualAlloc(window.bytes, stop - start, MEM_COMMIT, PAGE_READWRITE)) fail("commit direct memory pages"); + std::memcpy(window.bytes, reinterpret_cast(start), stop - start); + close(window); + if (!VirtualFree(reinterpret_cast(start), stop - start, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("release resident guest pages"); + for (auto page = start; page < stop; page += pageBytes) { + auto& view = views.at(page); + split(page, pageBytes); + mapShared(page, view.section->handle, view.offset, view.protection); + view.slot->stored = true; + view.resident = false; + view.seen.fill(0); + view.armed = 0; + view.collected = 0; + for (std::size_t sub = 0; sub < subpages; ++sub) { + if (runCollected(run->second, ((page - start) >> 12) + sub)) view.collected |= static_cast(1u << sub); + } + invalidate(*view.page); + } + residentRuns.erase(run); + } + + void breakRun(std::uintptr_t at) { + const auto next = residentRuns.upper_bound(at); + if (next == residentRuns.begin()) return; + const auto run = std::prev(next); + if (at > run->first && at < run->second.end) demote(at); + } + + void evict(std::map::iterator run) { + const auto start = run->first; + const auto stop = run->second.end; + DWORD previous; + if (!VirtualProtect(reinterpret_cast(start), stop - start, PAGE_READONLY, &previous)) fail("open resident guest pages"); + std::shared_ptr saved; + for (auto page = start; page < stop; page += pageBytes) { + const auto found = views.find(page); + auto& view = found->second; + if (!blank(page)) { + if (!saved) 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; + } + std::erase(view.page->aliases, page); + views.erase(found); + } + forget(freshRanges, start, stop); + 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; + while (it != ranges.end() && it->first < end) { const auto first = it->first; const auto last = it->second; - it = cleanRanges.erase(it); - if (first < start) cleanRanges.emplace(first, start); - if (last > end) it = cleanRanges.emplace(end, last).first; + it = ranges.erase(it); + if (first < start) ranges.emplace(first, start); + if (last > end) it = ranges.emplace(end, last).first; } } + static void remember(Ranges& ranges, std::uintptr_t start, std::uintptr_t end) { + auto it = ranges.lower_bound(start); + if (it != ranges.begin() && std::prev(it)->second >= start) --it; + while (it != ranges.end() && it->first <= end) { + start = std::min(start, it->first); + end = std::max(end, it->second); + it = ranges.erase(it); + } + ranges.emplace(start, end); + } + + void forgetClean(std::uintptr_t start, std::uintptr_t end) { + forget(cleanRanges, start, end); + } + void rememberClean(std::uintptr_t start, std::uintptr_t end) { - auto it = cleanRanges.lower_bound(start); - if (it != cleanRanges.begin() && std::prev(it)->second >= start) --it; - while (it != cleanRanges.end() && it->first <= end) { - start = std::min(start, it->first); - end = std::max(end, it->second); - it = cleanRanges.erase(it); - } - cleanRanges.emplace(start, end); + remember(cleanRanges, start, end); } - void invalidate(SharedPage& page) { - ++page.generation; - for (const auto alias : page.aliases) forgetClean(alias, alias + pageBytes); + void invalidate(SharedPage& page, std::uint8_t mask = allSubpages) { + for (std::size_t sub = 0; sub < subpages; ++sub) { + if ((mask & (1u << sub)) == 0) continue; + ++page.generation[sub]; + for (const auto alias : page.aliases) forgetClean(alias + sub * subpageBytes, alias + (sub + 1) * subpageBytes); + } + } + + static std::uint8_t subpagesOf(std::uintptr_t base, std::uintptr_t address, std::uintptr_t end) { + const auto from = std::max(address, base); + const auto to = std::min(end, base + pageBytes); + if (to <= from) return 0; + const auto first = (from - base) / subpageBytes; + const auto last = (to - 1 - base) / subpageBytes; + return static_cast(((1u << (last + 1)) - 1) & ~((1u << first) - 1)); + } + + static void protectSubpages(std::uintptr_t base, std::uint8_t mask, DWORD protection, const char* operation, std::size_t viewBytes = pageBytes) { + for (std::size_t sub = 0; sub < subpages;) { + if ((mask & (1u << sub)) == 0) { + ++sub; + continue; + } + 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; + } } bool sameSection(HANDLE first, HANDLE second) const { @@ -345,6 +879,9 @@ private: unmap = reinterpret_cast(GetProcAddress(module, "UnmapViewOfFile2")); if (!allocate || !map || !unmap) throw std::runtime_error("Windows placeholder memory APIs are required"); compare = reinterpret_cast(GetProcAddress(module, "CompareObjectHandles")); + SYSTEM_INFO system{}; + GetSystemInfo(&system); + granularity = system.dwAllocationGranularity; } [[noreturn]] static void fail(const char* operation) { @@ -371,8 +908,26 @@ private: } void reset(std::uintptr_t address, std::size_t bytes) { + collectableRanges.clear(); const auto end = address + bytes; forgetClean(address, end); + breakRun(address); + breakRun(end); + 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) { @@ -393,8 +948,10 @@ private: if (found != views.end()) { std::erase(found->second.page->aliases, cursor); views.erase(found); + forget(freshRanges, cursor, cursor + pageBytes); } } else if (memory.Type == MEM_PRIVATE) { + if (const auto run = residentRuns.find(cursor); run != residentRuns.end()) evict(run); if (!VirtualFree(reinterpret_cast(cursor), allocationEnd - cursor, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("release private guest memory"); } else { throw std::runtime_error("unsupported guest mapping type"); @@ -409,9 +966,63 @@ private: if (query(address).RegionSize != bytes && !VirtualFree(reinterpret_cast(address), bytes, MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS)) fail("coalesce guest placeholders"); } - std::map cleanRanges; + void* mapAliasRuns(std::uintptr_t address, std::size_t bytes) { + auto* reserved = allocate(GetCurrentProcess(), nullptr, bytes, MEM_RESERVE | MEM_RESERVE_PLACEHOLDER, PAGE_NOACCESS, nullptr, 0); + if (reserved == nullptr) fail("reserve a read-write alias of shared guest memory"); + const auto base = reinterpret_cast(reserved); + try { + for (std::size_t done = 0; done < bytes;) { + const auto& first = views.at(address + done); + auto run = pageBytes; + while (done + run < bytes) { + const auto& next = views.at(address + done + run); + if (next.offset != first.offset + run || (next.section != first.section && !sameSection(next.section->handle, first.section->handle))) break; + run += pageBytes; + } + split(base + done, run); + if (!map(first.section->handle, GetCurrentProcess(), reinterpret_cast(base + done), first.offset, run, MEM_REPLACE_PLACEHOLDER, PAGE_READWRITE, nullptr, 0)) fail("map a run of a read-write alias of shared guest memory"); + done += run; + } + } catch (...) { + releaseAlias(base, bytes); + throw; + } + splitAliases.emplace(base, bytes); + return reserved; + } + + DWORD releaseAlias(std::uintptr_t base, std::size_t bytes) { + DWORD error = ERROR_SUCCESS; + for (auto cursor = base; cursor < base + bytes;) { + const auto memory = query(cursor); + const auto released = memory.State == MEM_RESERVE ? VirtualFree(memory.BaseAddress, 0, MEM_RELEASE) : unmap(GetCurrentProcess(), memory.BaseAddress, 0); + if (!released && error == ERROR_SUCCESS) error = GetLastError(); + cursor = reinterpret_cast(memory.BaseAddress) + memory.RegionSize; + } + return error; + } + + Ranges cleanRanges; + Ranges collectableRanges; + Ranges freshRanges; std::map views; - std::map, std::weak_ptr> physical; + + using Views = std::map; + + Views::iterator viewAt(std::uintptr_t base, Views::iterator& position) { + if (position != views.end() && position->first < base) { + ++position; + if (position != views.end() && position->first < base) position = views.lower_bound(base); + } + return position != views.end() && position->first == base ? position : views.end(); + } + std::map, std::shared_ptr> physical; + std::map residentRuns; + std::vector> savedPool; + std::size_t savedPoolBytes = 0; + std::map splitAliases; + std::size_t granularity = 0x10000; + bool watchResident = false; std::mutex mutex; AllocateFunction allocate = nullptr; MapFunction map = nullptr; diff --git a/core/libs/prx/libc/include/WindowsScanning.hpp b/core/libs/prx/libc/include/WindowsScanning.hpp index e4dd53c007..77bbf1541e 100644 --- a/core/libs/prx/libc/include/WindowsScanning.hpp +++ b/core/libs/prx/libc/include/WindowsScanning.hpp @@ -3,7 +3,9 @@ #include "WindowsFormatting.hpp" #include "General.hpp" +#include #include +#include namespace LibcDetail { @@ -12,6 +14,8 @@ inline int ScanWindowsArguments_nid_no_patch(const char* format, const void* sou if (!format || !source) { errno = 22; return EOF; } std::string translated; std::vector pointers; + std::vector suppressedEnds(static_cast(std::count(format, format + std::strlen(format), '%')), -1); + size_t suppressedCount = 0; FormatArguments args(source); while (*format) { const char value = *format++; @@ -45,8 +49,15 @@ inline int ScanWindowsArguments_nid_no_patch(const char* format, const void* sou translated += *format++; } if (!suppressed) pointers.push_back(args.Next()); + else if (conversion != 'n') { + translated += "%n"; + pointers.push_back(&suppressedEnds[suppressedCount++]); + } } - return scan(translated.c_str(), reinterpret_cast(pointers.data())); + const int result = scan(translated.c_str(), reinterpret_cast(pointers.data())); + if (result == EOF && std::any_of(suppressedEnds.begin(), suppressedEnds.begin() + suppressedCount, [](int end) { return end >= 0; })) + return 0; + return result; } inline int ScanWindows(const char* input, const char* format, const void* source) { diff --git a/core/libs/prx/libc/include/WindowsWideFormatting.hpp b/core/libs/prx/libc/include/WindowsWideFormatting.hpp deleted file mode 100644 index 8d4baba34d..0000000000 --- a/core/libs/prx/libc/include/WindowsWideFormatting.hpp +++ /dev/null @@ -1,99 +0,0 @@ -#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_WINDOWSWIDEFORMATTING_HPP -#define CORE_LIBS_PRX_LIBC_INCLUDE_WINDOWSWIDEFORMATTING_HPP - -#include "WindowsFormatting.hpp" -#include "General.hpp" -#include -#include -#include - -namespace LibcDetail { - -inline int FormatWideWindows(wchar_t* output, size_t capacity, const wchar_t* format, const void* source) { - if (!capacity) return -1; - if (!output || !format || !source) { errno = 22; return -1; } - static_assert(sizeof(wchar_t) == 2); - FormatArguments args(source); - std::wstring translated; - std::wstring result; - std::vector slots; - std::deque extended; - auto append = [&slots](auto value) { - static_assert(sizeof(value) <= 8); - std::uint64_t bits = 0; - std::memcpy(&bits, &value, sizeof(value)); - slots.push_back(bits); - }; - while (*format) { - const wchar_t value = *format++; - if (value != L'%') { result += value; continue; } - if (*format == L'%') { result += *format++; continue; } - translated = L"%"; - slots.clear(); - extended.clear(); - bool nullCharacter = false; - while (*format && std::wcschr(L"-+ #0", *format)) translated += *format++; - if (*format == L'*') { translated += *format++; append(args.Next()); } - else while (*format >= L'0' && *format <= L'9') translated += *format++; - if (*format == L'.') { - translated += *format++; - if (*format == L'*') { translated += *format++; append(args.Next()); } - else while (*format >= L'0' && *format <= L'9') translated += *format++; - } - std::wstring length; - if (*format && std::wcschr(L"hljztL", *format)) { - length += *format++; - if ((length == L"h" && *format == L'h') || (length == L"l" && *format == L'l')) length += *format++; - } - const wchar_t conversion = *format; - if (!conversion) { errno = 22; return -1; } - if (!std::wcschr(L"diouxXaAeEfFgGcspn", conversion)) { - NotImplemented_nid_no_patch("swprintf format conversion"); - return -1; - } - ++format; - if (std::wcschr(L"diouxXn", conversion) && (length == L"l" || length == L"j" || length == L"z" || length == L"t")) - translated += L"ll"; - else if (conversion == L's' && length.empty()) translated += L"h"; - else if (conversion == L'c' && length.empty()) translated += L"l"; - else translated += length; - translated += conversion; - if (conversion == L'n') { - void* pointer = args.Next(); - if (length == L"hh") *static_cast(pointer) = static_cast(result.size()); - else if (length == L"h") *static_cast(pointer) = static_cast(result.size()); - else if (length.empty()) *static_cast(pointer) = static_cast(result.size()); - else *static_cast(pointer) = static_cast(result.size()); - continue; - } - if (conversion == L's' || conversion == L'p') append(args.Next()); - else if (conversion == L'c') { - const int character = args.Next(); - const int converted = length.empty() ? static_cast(static_cast(character)) : character; - nullCharacter = converted == 0; - append(nullCharacter ? 1 : converted); - } - else if (std::wcschr(L"aAeEfFgG", conversion)) { - if (length == L"L") { - extended.push_back(args.Next()); - append(&extended.back()); - } else append(args.Next()); - } else if (length.empty() || length == L"h" || length == L"hh") append(args.Next()); - else append(args.Next()); - if (result.size() >= capacity) return -1; - std::vector part(capacity - result.size()); - const int count = std::vswprintf(part.data(), part.size(), translated.c_str(), reinterpret_cast(slots.data())); - if (count < 0 || static_cast(count) >= part.size()) return -1; - if (nullCharacter) { - for (int i = 0; i < count; ++i) if (part[i] == 1) part[i] = 0; - } - result.append(part.data(), static_cast(count)); - } - if (result.size() >= capacity || result.size() > INT_MAX) return -1; - std::memcpy(output, result.c_str(), (result.size() + 1) * sizeof(wchar_t)); - return static_cast(result.size()); -} - -} - -#endif 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/exceptions/Unwind.hpp b/core/libs/prx/libc/include/exceptions/Unwind.hpp index 07d03fd3f1..bb7f08357c 100644 --- a/core/libs/prx/libc/include/exceptions/Unwind.hpp +++ b/core/libs/prx/libc/include/exceptions/Unwind.hpp @@ -9,7 +9,10 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/specifics/itanium/UnwindAbi.hpp" +extern "C" const std::uintptr_t* UnwindCursorSlots_nid_no_patch(); + struct _Unwind_Context { + const std::uintptr_t* cursor = UnwindCursorSlots_nid_no_patch(); std::uintptr_t registers[17] {}; #ifdef _WIN32 std::uintptr_t vectorRegisters[16][2] {}; @@ -95,6 +98,135 @@ inline unsigned EncodingSize(Byte encoding) { default: std::abort(); } } + +struct Lookup { Word pc; const Byte* fde {}; Word text {}; Word data {}; }; + +struct Frame { + const Byte* cieBegin {}; + const Byte* cieEnd {}; + const Byte* begin {}; + const Byte* end {}; + Word start {}, length {}, personality {}, lsda {}; + Word codeAlign {}; + std::intptr_t dataAlign {}; + unsigned returnRegister {}; + bool signal {}; +#ifdef _WIN32 + bool foreign {}; + _Unwind_Context caller {}; +#endif +}; + +inline bool DecodeRange(Frame& frame, const Lookup& query, const Byte*& p, const char*& augmentation, Byte& lsdaEncoding) { + if (!query.fde) return false; + p = query.fde; + auto length = Read(p); + if (!length || length == 0xffffffff) return false; + frame.end = p + length; + const Byte* ciePointer = p; + auto cieOffset = Read(p); + const Byte* cie = ciePointer - cieOffset; + const Byte* c = cie; + auto cieLength = Read(c); + frame.cieEnd = c + cieLength; + if (Read(c) != 0) return false; + Byte version = *c++; + if (version != 1 && version != 3 && version != 4) return false; + augmentation = reinterpret_cast(c); + while (*c++) {} + if (version == 4 && (*c++ != sizeof(Word) || *c++ != 0)) return false; + frame.codeAlign = Uleb(c); + frame.dataAlign = Sleb(c); + frame.returnRegister = version == 1 ? *c++ : Uleb(c); +#ifdef _WIN32 + if (frame.returnRegister >= 17 && frame.returnRegister != 32) return false; +#else + if (frame.returnRegister >= 17) return false; +#endif + Byte pointerEncoding = 0; + lsdaEncoding = 255; + if (*augmentation == 'z') { + Word size = Uleb(c); + const Byte* end = c + size; + for (const char* a = augmentation + 1; *a; ++a) { + switch (*a) { + case 'R': pointerEncoding = *c++; break; + case 'L': lsdaEncoding = *c++; break; + case 'P': { Byte encoding = *c++; frame.personality = Encoded(c, encoding, query.data, 0, query.text); break; } + case 'S': frame.signal = true; break; + default: return false; + } + } + c = end; + } else if (*augmentation) return false; + frame.cieBegin = c; + frame.start = Encoded(p, pointerEncoding, query.data, 0, query.text); + frame.length = Encoded(p, pointerEncoding & 15); + return true; +} + +struct Rule { unsigned kind {}; std::intptr_t value {}; const Byte* expression {}; }; +#ifdef _WIN32 +constexpr unsigned RuleCount = 33; +#else +constexpr unsigned RuleCount = 17; +#endif + +struct Rules { Rule registers[RuleCount] {}; unsigned cfaRegister {7}; std::intptr_t cfaOffset {}; const Byte* cfaExpression {}; }; + +inline bool Instructions(const Byte* p, const Byte* end, const Frame& frame, Word target, Rules& state, const Rules& initial) { + Word location = frame.start; + Rules saved[16]; + unsigned depth = 0; + while (p < end && location <= target) { + Byte opcode = *p++; + unsigned reg; + if ((opcode & 192) == 64) { location += (opcode & 63) * frame.codeAlign; continue; } + if ((opcode & 192) == 128) { + reg = opcode & 63; + auto offset = Uleb(p) * frame.dataAlign; + if (reg < RuleCount) state.registers[reg] = {1, static_cast(offset)}; + continue; + } + if ((opcode & 192) == 192) { + reg = opcode & 63; + if (reg < RuleCount) state.registers[reg] = initial.registers[reg]; + continue; + } + switch (opcode) { + case 0: break; + case 1: location = Read(p); break; + case 2: location += Read(p) * frame.codeAlign; break; + case 3: location += Read(p) * frame.codeAlign; break; + case 4: location += Read(p) * frame.codeAlign; break; + case 5: case 17: case 20: case 21: { + reg = Uleb(p); + auto offset = (opcode == 17 || opcode == 21 ? Sleb(p) : std::intptr_t(Uleb(p))) * frame.dataAlign; + if (reg < RuleCount) state.registers[reg] = {unsigned(opcode >= 20 ? 4 : 1), offset}; + break; + } + case 6: reg = Uleb(p); if (reg < RuleCount) state.registers[reg] = initial.registers[reg]; break; + case 7: case 8: reg = Uleb(p); if (reg < RuleCount) state.registers[reg] = {unsigned(opcode == 7 ? 5 : 0)}; break; + case 9: { reg = Uleb(p); auto other = Uleb(p); if (reg < RuleCount) state.registers[reg] = {2, std::intptr_t(other)}; break; } + case 10: if (depth == 16) return false; saved[depth++] = state; break; + case 11: if (!depth) return false; state = saved[--depth]; break; + case 12: case 18: + state.cfaExpression = nullptr; state.cfaRegister = Uleb(p); + state.cfaOffset = opcode == 18 ? Sleb(p) * frame.dataAlign : Uleb(p); break; + case 13: state.cfaRegister = Uleb(p); break; + case 14: case 19: state.cfaOffset = opcode == 19 ? Sleb(p) * frame.dataAlign : Uleb(p); break; + case 15: { state.cfaExpression = p; auto size = Uleb(p); p += size; break; } + case 16: case 22: { + reg = Uleb(p); const Byte* expr = p; auto size = Uleb(p); p += size; + if (reg < RuleCount) state.registers[reg] = {unsigned(opcode == 16 ? 3 : 6), 0, expr}; + break; + } + case 46: Uleb(p); break; + default: return false; + } + } + return true; +} } extern "C" { diff --git a/core/libs/prx/libc/include/general/AsmMacros.hpp b/core/libs/prx/libc/include/general/AsmMacros.hpp new file mode 100644 index 0000000000..78de31dbd6 --- /dev/null +++ b/core/libs/prx/libc/include/general/AsmMacros.hpp @@ -0,0 +1,18 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_GENERAL_ASMMACROS_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_GENERAL_ASMMACROS_HPP + +#if defined(_WIN32) +#define APS5_ASM_SYMBOL(name) name +#define APS5_ASM_FUNCTION(name) ".globl " name "\n.def " name "; .scl 2; .type 32; .endef\n" name ":\n" +#define APS5_ASM_FUNCTION_END(name) "" +#elif defined(__APPLE__) +#define APS5_ASM_SYMBOL(name) "_" name +#define APS5_ASM_FUNCTION(name) ".globl _" name "\n_" name ":\n" +#define APS5_ASM_FUNCTION_END(name) "" +#else +#define APS5_ASM_SYMBOL(name) name +#define APS5_ASM_FUNCTION(name) ".globl " name "\n.type " name ", @function\n" name ":\n" +#define APS5_ASM_FUNCTION_END(name) ".size " name ", .-" name "\n" +#endif + +#endif diff --git a/core/libs/prx/libc/include/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 f9c5f24c27..00e511b24b 100644 --- a/core/libs/prx/libc/include/general/LogMacros.hpp +++ b/core/libs/prx/libc/include/general/LogMacros.hpp @@ -1,7 +1,68 @@ #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 +[[gnu::format(printf, 2, 3)]] +#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 + prefixLength, sizeof(small) - prefixLength, format, arguments); + va_end(arguments); + std::string large; + 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; + 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); +} inline const char* TrimNidPostfix(const char* func) { return func; @@ -30,12 +91,10 @@ constexpr const char* BaseName(const char* path) { #define _APS5_FILE_ BaseName(__FILE__) #define _APS5_LOG_IMPL(stream, fmt, ...) \ -(std::fprintf(stream, "[%s:%d %s] " fmt "\n", _APS5_FILE_, __LINE__, TrimNidPostfix(__func__), __VA_ARGS__), \ -std::fflush(stream)) +WriteLogLine(stream, "[%s:%d %s] " fmt "\n", _APS5_FILE_, __LINE__, TrimNidPostfix(__func__), __VA_ARGS__) #define _APS5_LOG_IMPL_NF(stream, fmt) \ -(std::fprintf(stream, "[%s:%d %s] " fmt "\n", _APS5_FILE_, __LINE__, TrimNidPostfix(__func__)), \ -std::fflush(stream)) +WriteLogLine(stream, "[%s:%d %s] " fmt "\n", _APS5_FILE_, __LINE__, TrimNidPostfix(__func__)) #define APS5_LOG_OUT(fmt, ...) _APS5_LOG_IMPL(stdout, fmt, __VA_ARGS__) #define APS5_LOG_ERR(fmt, ...) _APS5_LOG_IMPL(stderr, fmt, __VA_ARGS__) @@ -60,7 +119,7 @@ _APS5_LOG_IMPL_NF(stdout, fmt); \ #define APS5_LOG_CHARS_OUT_TIMING(fmt) APS5_LOG_CHARS_OUT_IF(APS5_ENABLE_TIMING_LOG, fmt) #ifdef APS5_DEBUG_LOG -#define APS5_LOG_DEBUG(fmt, ...) _APS5_LOG_IMPL(stdout, fmt, __VA_ARGS__) +#define APS5_LOG_OUT_DEBUG(fmt, ...) _APS5_LOG_IMPL(stdout, fmt, __VA_ARGS__) #define APS5_LOG_CHARS_OUT_DEBUG(fmt) _APS5_LOG_IMPL_NF(stdout, fmt) #else #define APS5_LOG_OUT_DEBUG(fmt, ...) ((void)0) 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/CxxAbi.hpp b/core/libs/prx/libc/include/specifics/itanium/CxxAbi.hpp index 53d45d895b..f4957fd7b3 100644 --- a/core/libs/prx/libc/include/specifics/itanium/CxxAbi.hpp +++ b/core/libs/prx/libc/include/specifics/itanium/CxxAbi.hpp @@ -39,6 +39,8 @@ public: class __pointer_type_info : public __pbase_type_info {}; +class __function_type_info : public std::type_info {}; + class __pointer_to_member_type_info : public __pbase_type_info { public: const __class_type_info* __context; diff --git a/core/libs/prx/libc/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/include/specifics/linux/HostMapping.hpp b/core/libs/prx/libc/include/specifics/linux/HostMapping.hpp new file mode 100644 index 0000000000..11c5e5cb45 --- /dev/null +++ b/core/libs/prx/libc/include/specifics/linux/HostMapping.hpp @@ -0,0 +1,108 @@ +#ifndef PRX_LIBC_INCLUDE_SPECIFICS_LINUX_HOSTMAPPING_HPP +#define PRX_LIBC_INCLUDE_SPECIFICS_LINUX_HOSTMAPPING_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if __has_include() +#include +#endif +#if !defined(PROCMAP_QUERY) +struct procmap_query { + std::uint64_t size; + std::uint64_t query_flags; + std::uint64_t query_addr; + std::uint64_t vma_start; + std::uint64_t vma_end; + std::uint64_t vma_flags; + std::uint64_t vma_page_size; + std::uint64_t vma_offset; + std::uint64_t inode; + std::uint32_t dev_major; + std::uint32_t dev_minor; + std::uint32_t vma_name_size; + std::uint32_t build_id_size; + std::uint64_t vma_name_addr; + std::uint64_t build_id_addr; +}; +static_assert(sizeof(procmap_query) == 104); +enum : std::uint64_t { + PROCMAP_QUERY_VMA_READABLE = 0x01, + PROCMAP_QUERY_VMA_WRITABLE = 0x02, + PROCMAP_QUERY_COVERING_OR_NEXT_VMA = 0x10, +}; +#define PROCMAP_QUERY _IOWR('f', 17, struct procmap_query) +#endif + +namespace HostMapping { + +struct Mapping { + std::uint64_t start; + std::uint64_t end; + std::string path; +}; + +inline int QueryFd() { + static const int fd = [] { + if (std::getenv("APS5_NO_PROCMAP_QUERY") != nullptr) return -1; + const int opened = open("/proc/self/maps", O_RDONLY | O_CLOEXEC); + if (opened < 0) return -1; + procmap_query probe{}; + probe.size = sizeof(probe); + probe.query_flags = PROCMAP_QUERY_COVERING_OR_NEXT_VMA; + if (ioctl(opened, PROCMAP_QUERY, &probe) == 0 || errno == ENOENT) return opened; + close(opened); + return -1; + }(); + return fd; +} + +inline std::optional ScanMaps(std::uint64_t address, const char* caller) { + std::ifstream maps("/proc/self/maps"); + if (!maps) throw std::runtime_error(std::string(caller) + ": failed to open /proc/self/maps"); + std::string line; + while (std::getline(maps, line)) { + std::uint64_t start = 0; + std::uint64_t end = 0; + char perms[8] = {}; + std::uint64_t offset = 0; + unsigned int devMajor = 0; + unsigned int devMinor = 0; + std::uint64_t inode = 0; + char path[4096] = {}; + const int parsed = std::sscanf(line.c_str(), "%llx-%llx %7s %llx %x:%x %llu %4095s", + (unsigned long long*)&start, (unsigned long long*)&end, perms, + (unsigned long long*)&offset, &devMajor, &devMinor, (unsigned long long*)&inode, path); + if (parsed < 7 || address < start || address >= end) continue; + return Mapping{start, end, parsed >= 8 ? path : ""}; + } + return std::nullopt; +} + +inline std::optional Find(std::uint64_t address, const char* caller) { + const int fd = QueryFd(); + if (fd < 0) return ScanMaps(address, caller); + char name[4096] = {}; + procmap_query query{}; + query.size = sizeof(query); + query.query_addr = address; + query.vma_name_size = sizeof(name); + query.vma_name_addr = reinterpret_cast(name); + if (ioctl(fd, PROCMAP_QUERY, &query) != 0) { + if (errno == ENOENT) return std::nullopt; + throw std::runtime_error(std::string(caller) + ": PROCMAP_QUERY failed"); + } + return Mapping{query.vma_start, query.vma_end, query.vma_name_size != 0 ? name : ""}; +} + +} + +#endif diff --git a/core/libs/prx/libc/src/AllocatingStrings.cpp b/core/libs/prx/libc/src/AllocatingStrings.cpp index 74779c475f..1605fbd125 100644 --- a/core/libs/prx/libc/src/AllocatingStrings.cpp +++ b/core/libs/prx/libc/src/AllocatingStrings.cpp @@ -1,21 +1,17 @@ #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/General.hpp" +#include "SceTypes.hpp" #include #include -#include -#include #include -#include #include -#include - -#ifdef _WIN32 -#include "prx/libc/include/WindowsFormatting.hpp" -#endif 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 { @@ -35,9 +31,6 @@ char* APS5_VABI strdup_nid_postfix(const char* source) { } int APS5_VABI asprintf_nid_postfix(char** destination, const char* format, ...) { - if (!destination) { errno = 22; return -1; } - *destination = nullptr; - if (!format) { errno = 22; return -1; } #ifdef _WIN32 __builtin_sysv_va_list args; __builtin_sysv_va_start(args, format); @@ -45,36 +38,7 @@ int APS5_VABI asprintf_nid_postfix(char** destination, const char* format, ...) std::va_list args; va_start(args, format); #endif - int result = -1; - try { -#ifdef _WIN32 - std::string text; - const int count = LibcDetail::FormatWindows(nullptr, 0, format, args, &text); - const char* source = text.c_str(); -#else - char* text = nullptr; - const int count = ::vasprintf(&text, format, args); - std::unique_ptr owner(text, std::free); - const char* source = text; -#endif - if (count >= 0) { - auto* output = static_cast(ApplicationHeapAllocate_nid_no_patch(static_cast(count) + 1)); - if (output) { - std::memcpy(output, source, static_cast(count) + 1); - *destination = output; - result = count; - } else errno = 12; - } - } catch (const std::bad_alloc&) { - errno = 12; - } catch (...) { -#ifdef _WIN32 - __builtin_sysv_va_end(args); -#else - va_end(args); -#endif - throw; - } + const int result = vasprintf_nid_postfix(destination, format, reinterpret_cast(args)); #ifdef _WIN32 __builtin_sysv_va_end(args); #else 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 4812d1a342..84a8f46eb7 100644 --- a/core/libs/prx/libc/src/ApplicationHeap.cpp +++ b/core/libs/prx/libc/src/ApplicationHeap.cpp @@ -23,6 +23,7 @@ using Realign = void* (APS5_VABI *)(void*, std::size_t, std::size_t); using PosixAlign = int (APS5_VABI *)(void**, std::size_t, std::size_t); using Initialize = void (APS5_VABI *)(); using Stats = int (APS5_VABI *)(void*); +using UsableSize = std::size_t (APS5_VABI *)(void*); std::mutex heapMutex; std::array heapApi{}; @@ -47,12 +48,16 @@ int APS5_VABI defaultPosixAlign(void** pointer, std::size_t alignment, std::size if (pointer == nullptr || alignment < sizeof(void*) || (alignment & (alignment - 1)) != 0) return 22; try { *pointer = defaultAlign(alignment, bytes); return 0; } catch (const std::bad_alloc&) { return 12; } + 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 { @@ -74,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"); @@ -144,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(); @@ -174,7 +192,7 @@ void ApplicationHeapFree_nid_no_patch(void* pointer) { } void* ApplicationHeapReallocate_nid_no_patch(void* pointer, std::size_t bytes) { - if (bytes == 0) { + if (bytes == 0 && pointer != nullptr) { ApplicationHeapFree_nid_no_patch(pointer); return nullptr; } @@ -192,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); @@ -200,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"); @@ -238,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"); @@ -248,3 +284,21 @@ int ApplicationHeapStatsFast_nid_no_patch(void* stats) { CallbackScope scope; return statsFast(stats); } + +std::size_t ApplicationHeapUsableSize_nid_no_patch(void* pointer) { + UsableSize usableSize = nullptr; + { + 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"); + if (heapApi != defaultApi()) { + if (heapApi[9] == nullptr) throw std::runtime_error("malloc_usable_size: implemented only for an application allocator that provides it"); + std::memcpy(&usableSize, &heapApi[9], sizeof(usableSize)); + } + } + if (usableSize == nullptr) return GuestHeap::GuestHeapUsableSize_nid_postfix(pointer); + CallbackScope scope; + return usableSize(pointer); +} 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 new file mode 100644 index 0000000000..a07a476b4a --- /dev/null +++ b/core/libs/prx/libc/src/Backtrace.cpp @@ -0,0 +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 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 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/CreationMask.cpp b/core/libs/prx/libc/src/CreationMask.cpp new file mode 100644 index 0000000000..a4491eccde --- /dev/null +++ b/core/libs/prx/libc/src/CreationMask.cpp @@ -0,0 +1,21 @@ +#include "prx/libc/include/CreationMask.hpp" + +#include + +namespace { + +std::atomic creationMask{022}; + +} + +namespace File { + +std::uint16_t ExchangeCreationMask_nid_no_patch(std::uint16_t mask) { + return creationMask.exchange(static_cast(mask & 07777), std::memory_order_relaxed); +} + +std::uint16_t GetCreationMask_nid_no_patch() { + return creationMask.load(std::memory_order_relaxed); +} + +} diff --git a/core/libs/prx/libc/src/CxxAbiSupport.cpp b/core/libs/prx/libc/src/CxxAbiSupport.cpp index c8e7e0801c..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" @@ -62,6 +64,9 @@ void* APS5_VABI __cxa_demangle_nid_postfix(const char* mangled, char* buf, std:: } int APS5_VABI __cxa_thread_atexit_impl_nid_postfix(void (APS5_VABI *func)(void*), void* arg, void* dso) { +#ifdef _WIN32 + dso = nullptr; +#endif struct ThreadAtexitContext { void (APS5_VABI *destructor)(void*); void* object; @@ -83,13 +88,12 @@ int APS5_VABI __cxa_thread_atexit_impl_nid_postfix(void (APS5_VABI *func)(void*) return result; } +int APS5_VABI __cxa_thread_atexit_nid_postfix(void (APS5_VABI *func)(void*), void* arg, void* dso) { + return __cxa_thread_atexit_impl_nid_postfix(func, arg, dso); +} + int APS5_VABI LibcInternalExtCxaThreadAtexit_nid_postfix(void (APS5_VABI *destructor)(void*), void* object, void* module_id) { -#ifdef _WIN32 - (void)module_id; - return __cxa_thread_atexit_impl_nid_postfix(destructor, object, nullptr); -#else return __cxa_thread_atexit_impl_nid_postfix(destructor, object, module_id); -#endif } const std::error_category* _ZSt17iostream_categoryv_nid_postfix() { return &std::iostream_category(); } @@ -97,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/Directory.cpp b/core/libs/prx/libc/src/Directory.cpp index 0cc8ae972b..425669cdcf 100644 --- a/core/libs/prx/libc/src/Directory.cpp +++ b/core/libs/prx/libc/src/Directory.cpp @@ -1,10 +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) { @@ -17,15 +27,135 @@ int DirectoryError(int error) { case ENFILE: return 23; case ENOMEM: return 12; case ENAMETOOLONG: return 63; +#ifdef ELOOP + case ELOOP: return 62; +#endif default: return 5; } } +#ifdef _WIN32 +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) { + 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 = fileId != 0 ? WindowsFileNumber_nid_no_patch(fileId) : StableFileNumber(native->seed, fileName); + return true; +} +#else +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; + name = entry->d_name; + component = name; + inode = static_cast(entry->d_ino); + return true; +} +#endif struct Directory { - DIR* native = nullptr; + NativeDirectory* native = nullptr; std::filesystem::path path; GuestDirectoryEntry entry{}; std::mutex mutex; - ~Directory() { if (native) ::closedir(native); } + int descriptor = -1; + int (APS5_VABI* closeDescriptor)(int) = nullptr; + ~Directory() { if (native) CloseNative(native); } }; std::uint8_t DirectoryType(const std::filesystem::path& path) { std::error_code error; @@ -40,6 +170,32 @@ std::uint8_t DirectoryType(const std::filesystem::path& path) { default: return 0; } } +GuestDirectoryEntry* ReadEntry(Directory& directory) { + const int savedError = errno; + errno = 0; + std::filesystem::path component; + std::string name; + std::uint64_t inode = 0; + 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 = {}; + 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); + std::memcpy(directory.entry.name, name.c_str(), length + 1); + errno = savedError; + return &directory.entry; + } catch (const std::bad_alloc&) { errno = 12; return nullptr; } + catch (const std::filesystem::filesystem_error&) { errno = 5; return nullptr; } +} } extern "C" { @@ -48,9 +204,14 @@ void* APS5_VABI opendir_nid_postfix(const char* path) { if (!*path) { errno = 2; return nullptr; } try { auto directory = std::make_unique(); - directory->path = ResolvePath_nid_no_patch(path); - directory->native = ::opendir(directory->path.string().c_str()); - if (!directory->native) { errno = DirectoryError(errno); return nullptr; } + 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); + 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; } @@ -60,36 +221,34 @@ GuestDirectoryEntry* APS5_VABI readdir_nid_postfix(void* handle) { if (!handle) { errno = 9; return nullptr; } auto& directory = *static_cast(handle); std::lock_guard lock(directory.mutex); + return ReadEntry(directory); +} + +int APS5_VABI readdir_r_nid_postfix(void* handle, GuestDirectoryEntry* entry, GuestDirectoryEntry** result) { + if (!handle) return 9; + if (!entry || !result) throw std::invalid_argument("readdir_r: null entry or result"); + auto& directory = *static_cast(handle); const int savedError = errno; errno = 0; - auto* entry = ::readdir(directory.native); - if (!entry) { - errno = errno ? DirectoryError(errno) : savedError; - return nullptr; + GuestDirectoryEntry* next = nullptr; + { + std::lock_guard lock(directory.mutex); + next = ReadEntry(directory); + if (next) std::memcpy(entry, next, next->recordLength); } - const auto length = std::strlen(entry->d_name); - if (length > 255) { errno = 63; return nullptr; } - try { - directory.entry = {}; - // MinGW reports no inode identity (zero); do not fabricate one. - const auto inode = static_cast(entry->d_ino); - directory.entry.fileNumber = inode <= UINT32_MAX ? static_cast(inode) : 0; - directory.entry.recordLength = static_cast(8 + ((length + 1 + 3) & ~3)); - directory.entry.nameLength = static_cast(length); - directory.entry.type = DirectoryType(directory.path / entry->d_name); - std::memcpy(directory.entry.name, entry->d_name, length + 1); - errno = savedError; - return &directory.entry; - } catch (const std::bad_alloc&) { errno = 12; return nullptr; } - catch (const std::filesystem::filesystem_error&) { errno = 5; return nullptr; } + if (!next && errno) return errno; + errno = savedError; + *result = next ? entry : nullptr; + return 0; } int APS5_VABI closedir_nid_postfix(void* handle) { if (!handle) { errno = 9; return -1; } std::unique_ptr directory(static_cast(handle)); - const int result = ::closedir(directory->native); + int result = CloseNative(directory->native); directory->native = nullptr; if (result) errno = DirectoryError(errno); + if (directory->closeDescriptor && directory->closeDescriptor(directory->descriptor) != 0) result = -1; return result; } @@ -97,6 +256,47 @@ void APS5_VABI rewinddir_nid_postfix(void* handle) { if (!handle) { errno = 9; return; } auto& directory = *static_cast(handle); std::lock_guard lock(directory.mutex); - ::rewinddir(directory.native); + RewindNative(directory.native); +} + +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(); + } catch (const std::bad_alloc&) { errno = 12; return nullptr; } + catch (const std::filesystem::filesystem_error&) { errno = 5; return nullptr; } } } diff --git a/core/libs/prx/libc/src/Environment.cpp b/core/libs/prx/libc/src/Environment.cpp index 080c8b1103..f77b8727f9 100644 --- a/core/libs/prx/libc/src/Environment.cpp +++ b/core/libs/prx/libc/src/Environment.cpp @@ -2,21 +2,19 @@ #include #include #include +#include #include #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::string owned; + std::unique_ptr owned; char* borrowed = nullptr; - char* Data() { return borrowed ? borrowed : owned.data(); } + char* Data() { return borrowed ? borrowed : owned->data(); } bool Matches(std::string_view name) { const char* data = Data(); const char* separator = std::strchr(data, '='); @@ -25,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({*source, nullptr}); - } - return result; - }(); + static std::vector entries; return entries; } bool ValidEnvironmentName(const char* name) { @@ -48,8 +33,19 @@ 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)) return nullptr; + if (!ValidEnvironmentName(name)) { errno = 22; return nullptr; } try { std::lock_guard lock(environmentMutex); for (auto& entry : Environment()) @@ -66,7 +62,7 @@ int APS5_VABI setenv_nid_postfix(const char* name, const char* value, int overwr auto found = std::find_if(entries.begin(), entries.end(), [name](auto& entry) { return entry.Matches(name); }); if (found != entries.end() && !overwrite) return 0; - EnvironmentEntry replacement{std::string(name) + '=' + value, nullptr}; + EnvironmentEntry replacement{std::make_unique(std::string(name) + '=' + value), nullptr}; if (found != entries.end()) *found = std::move(replacement); else entries.push_back(std::move(replacement)); return 0; @@ -93,7 +89,7 @@ int APS5_VABI putenv_nid_postfix(char* text) { const std::string_view name(text, separator - text); auto found = std::find_if(entries.begin(), entries.end(), [name](auto& entry) { return entry.Matches(name); }); - EnvironmentEntry replacement{{}, text}; + EnvironmentEntry replacement{nullptr, text}; if (found != entries.end()) *found = std::move(replacement); else entries.push_back(std::move(replacement)); return 0; diff --git a/core/libs/prx/libc/src/Errors.cpp b/core/libs/prx/libc/src/Errors.cpp index 963191cf61..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 @@ -7,31 +8,39 @@ extern "C" int* APS5_VABI __error_nid_postfix(); namespace { // Guest FreeBSD errno numbering, not the host CRT's numbering. constexpr const char* messages[] = { - "No error", "Operation not permitted", "No such file or directory", "No such process", - "Interrupted system call", "Input/output error", "Device not configured", "Argument list too long", - "Exec format error", "Bad file descriptor", "No child processes", "Resource deadlock avoided", - "Cannot allocate memory", "Permission denied", "Bad address", "Block device required", - "Device busy", "File exists", "Cross-device link", "Operation not supported by device", - "Not a directory", "Is a directory", "Invalid argument", "Too many open files in system", - "Too many open files", "Inappropriate ioctl for device", "Text file busy", "File too large", - "No space left on device", "Illegal seek", "Read-only filesystem", "Too many links", "Broken pipe", + "No error: 0", "Operation not permitted", "No such file or directory", + "No such process", "Interrupted system call", "Input/output error", + "Device not configured", "Argument list too long", "Exec format error", + "Bad file descriptor", "No child processes", "Resource deadlock avoided", + "Cannot allocate memory", "Permission denied", "Bad address", + "Block device required", "Device busy", "File exists", + "Cross-device link", "Operation not supported by device", "Not a directory", + "Is a directory", "Invalid argument", "Too many open files in system", + "Too many open files", "Inappropriate ioctl for device", "Text file busy", + "File too large", "No space left on device", "Illegal seek", + "Read-only file system", "Too many links", "Broken pipe", "Numerical argument out of domain", "Result too large", "Resource temporarily unavailable", "Operation now in progress", "Operation already in progress", "Socket operation on non-socket", "Destination address required", "Message too long", "Protocol wrong type for socket", "Protocol not available", "Protocol not supported", "Socket type not supported", - "Operation not supported", "Protocol family not supported", "Address family not supported", - "Address already in use", "Cannot assign requested address", "Network is down", "Network is unreachable", - "Network connection reset", "Connection aborted", "Connection reset by peer", "No buffer space available", - "Socket is already connected", "Socket is not connected", "Socket is shut down", "Too many references", - "Operation timed out", "Connection refused", "Too many symbolic links", "File name too long", - "Host is down", "No route to host", "Directory not empty", "Too many processes", "Too many users", - "Disk quota exceeded", "Stale file handle", "Remote path has too many levels", "Invalid RPC structure", - "RPC version mismatch", "RPC program unavailable", "Program version mismatch", "Invalid RPC procedure", - "No locks available", "Function not implemented", "Inappropriate file type", "Authentication error", - "Authentication required", "Identifier removed", "No message of requested type", "Value too large", - "Operation canceled", "Invalid byte sequence", "Attribute not found", "Programming error", - "Bad message", "Multihop attempted", "Link severed", "Protocol error", "Insufficient capabilities", - "Operation not permitted in capability mode", "State not recoverable", "Previous owner died" + "Operation not supported", "Protocol family not supported", "Address family not supported by protocol family", + "Address already in use", "Can't assign requested address", "Network is down", + "Network is unreachable", "Network dropped connection on reset", "Software caused connection abort", + "Connection reset by peer", "No buffer space available", "Socket is already connected", + "Socket is not connected", "Can't send after socket shutdown", "Too many references: can't splice", + "Operation timed out", "Connection refused", "Too many levels of symbolic links", + "File name too long", "Host is down", "No route to host", + "Directory not empty", "Too many processes", "Too many users", + "Disc quota exceeded", "Stale NFS file handle", "Too many levels of remote in path", + "RPC struct is bad", "RPC version wrong", "RPC prog. not avail", + "Program version wrong", "Bad procedure for program", "No locks available", + "Function not implemented", "Inappropriate file type or format", "Authentication error", + "Need authenticator", "Identifier removed", "No message of desired type", + "Value too large to be stored in data type", "Operation canceled", "Illegal byte sequence", + "Attribute not found", "Programming error", "Bad message", + "Multihop attempted", "Link has been severed", "Protocol error", + "Capabilities insufficient", "Not permitted in capability mode", "State not recoverable", + "Previous owner died" }; static_assert(sizeof(messages) / sizeof(messages[0]) == 97); bool Known(int error) { return error >= 0 && error < 97; } @@ -54,7 +63,7 @@ int APS5_VABI strerror_r_nid_postfix(int error, char* buffer, std::size_t length else std::snprintf(temporary, sizeof(temporary), "Unknown error: %d", error); const auto required = std::strlen(temporary) + 1; int result = Known(error) ? 0 : 22; - if (!buffer || length < required) result = 34; + if (Known(error) && (!buffer || length < required)) result = 34; if (buffer && length) { const auto count = required <= length ? required - 1 : length - 1; std::memcpy(buffer, temporary, count); @@ -68,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 e6f82f47a5..410fa6f309 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -1,29 +1,271 @@ +#include #include #include #include #include #include +#include #include #include +#include +#include +#include #include #include #include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#include +#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" -static std::string NativeFileMode(const char* mode) { - std::string result(mode); -#ifdef _WIN32 - if (!result.empty() && result.find('b') == std::string::npos) result.insert(1, 1, 'b'); +#ifdef __linux__ +#include #endif + +struct GuestFileMode { + std::string native; + bool writes = false; + bool exclusive = false; + bool append = false; +}; + +static std::optional ParseFileMode(const char* mode) { + const char access = mode[0]; + if (access != 'r' && access != 'w' && access != 'a') return std::nullopt; + bool update = false; + bool exclusive = false; + for (const char* flag = mode + 1; *flag != '\0' && std::strchr("b+xev", *flag) != nullptr; ++flag) { + if (*flag == '+') update = true; + if (*flag == 'x') exclusive = true; + } + if (exclusive && access == 'r' && !update) return std::nullopt; + GuestFileMode result{std::string(1, access), access != 'r' || update, exclusive, access == 'a'}; + if (update) result.native += '+'; +#ifdef _WIN32 + result.native += 'b'; +#endif + if (exclusive) result.native += 'x'; return result; } -static bool WritesFile(const char* mode) { - return std::strpbrk(mode, "wa+") != nullptr; +#ifdef _WIN32 +static constexpr DWORD AppendOnlyAccess = FILE_GENERIC_WRITE & ~FILE_WRITE_DATA; +#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 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}; + auto filename = path.native(); + if (!filename.starts_with(L"\\\\?\\") && !filename.starts_with(L"\\\\.\\")) { + std::error_code pathError; + auto absolutePath = std::filesystem::absolute(path, pathError); + if (pathError) { errno = pathError.default_error_condition().value(); return nullptr; } + const auto absolute = absolutePath.lexically_normal().make_preferred().native(); + if (absolute.size() >= MAX_PATH) + filename = absolute.starts_with(L"\\\\") ? L"\\\\?\\UNC\\" + absolute.substr(2) : L"\\\\?\\" + absolute; + } + const auto handle = CreateFileW(filename.c_str(), desired, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, &security, + disposition, FILE_ATTRIBUTE_NORMAL, nullptr); + if (handle == INVALID_HANDLE_VALUE) { + errno = WindowsOpenError_nid_no_patch(GetLastError()); + return nullptr; + } + const int descriptor = ::_open_osfhandle(reinterpret_cast(handle), access == 'a' ? _O_APPEND : 0); + if (descriptor < 0) { + CloseHandle(handle); + return nullptr; + } + std::wstring streamMode; + for (const char character : nativeMode) + if (character != 'x') streamMode.push_back(static_cast(character)); + std::FILE* stream = ::_wfdopen(descriptor, streamMode.c_str()); + if (!stream) ::_close(descriptor); + return stream; +#else + return std::fopen(path.c_str(), nativeMode.c_str()); +#endif +} + +#ifdef _WIN32 +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(), ©, access, TRUE, options)) { + errno = EBADF; + return false; + } + const int appending = ::_open_osfhandle(reinterpret_cast(copy), _O_APPEND | translation); + if (appending < 0) { + 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]); +} + +static void SwapRanges(unsigned char* left, unsigned char* right, size_t bytes) { + if (bytes > 0) SwapElements(left, right, bytes); +} + +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)); +} + +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); +} + +template +static void SortElements(unsigned char* base, size_t count, size_t size, Compare compare) { + for (;;) { + if (count < 7) { + InsertionSort(base, count, size, compare); + return; + } + unsigned char* middle = base + (count / 2) * size; + if (count > 7) { + unsigned char* low = base; + unsigned char* high = base + (count - 1) * size; + if (count > 40) { + const size_t step = (count / 8) * size; + low = MedianOfThree(low, low + step, low + 2 * step, compare); + middle = MedianOfThree(middle - step, middle, middle + step, compare); + high = MedianOfThree(high - 2 * step, high - step, high, compare); + } + middle = MedianOfThree(low, middle, high, compare); + } + SwapElements(base, middle, size); + unsigned char* equalLow = base + size; + unsigned char* scanLow = equalLow; + unsigned char* scanHigh = base + (count - 1) * size; + unsigned char* equalHigh = scanHigh; + bool swapped = false; + for (;;) { + int result = 0; + while (scanLow <= scanHigh && (result = compare(scanLow, base)) <= 0) { + if (result == 0) { + swapped = true; + SwapElements(equalLow, scanLow, size); + equalLow += size; + } + scanLow += size; + } + while (scanLow <= scanHigh && (result = compare(scanHigh, base)) >= 0) { + if (result == 0) { + swapped = true; + SwapElements(scanHigh, equalHigh, size); + equalHigh -= size; + } + scanHigh -= size; + } + if (scanLow > scanHigh) break; + SwapElements(scanLow, scanHigh, size); + swapped = true; + scanLow += size; + scanHigh -= size; + } + if (!swapped) { + InsertionSort(base, count, size, compare); + return; + } + unsigned char* const end = base + count * size; + size_t moved = std::min(equalLow - base, scanLow - equalLow); + SwapRanges(base, scanLow - moved, moved); + moved = std::min(equalHigh - scanHigh, end - equalHigh - size); + SwapRanges(scanLow, end - moved, moved); + const size_t lowBytes = scanLow - equalLow; + const size_t highBytes = equalHigh - scanHigh; + if (lowBytes <= highBytes) { + if (lowBytes > size) SortElements(base, lowBytes / size, size, compare); + if (highBytes <= size) return; + base = end - highBytes; + count = highBytes / size; + } else { + if (highBytes > size) SortElements(end - highBytes, highBytes / size, size, compare); + if (lowBytes <= size) return; + count = lowBytes / size; + } + } +} + +namespace { + +template +void* allocationOrNull(TAllocate allocate) { + try { + return allocate(); + } catch (const std::bad_alloc&) { + } catch (const std::length_error&) { + } + errno = 12; + return nullptr; +} + +bool validAlignment(std::size_t alignment) { + return alignment != 0 && (alignment & (alignment - 1)) == 0; +} + } extern "C" { @@ -33,18 +275,24 @@ 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; } - const char* supported[] = {"r", "w", "a", "rb", "wb", "ab", "r+", "w+", "a+", - "rb+", "wb+", "ab+", "r+b", "w+b", "a+b"}; - bool valid = false; - for (const auto* candidate : supported) if (std::strcmp(mode, candidate) == 0) valid = true; - if (!valid) { errno = 22; return nullptr; } + AppArchiveRefuseDescriptor_nid_no_patch(descriptor, "fdopen"); + const auto parsed = ParseFileMode(mode); + if (!parsed) { errno = 22; return nullptr; } try { + auto nativeMode = parsed->native; + std::erase(nativeMode, 'x'); const auto release = [](void* pointer) { ::operator delete(pointer); }; std::unique_ptr storage(::operator new(sizeof(FileStream)), release); #ifdef _WIN32 - auto* native = ::_fdopen(descriptor, mode); + const auto handle = WindowsDescriptorHandle_nid_no_patch(descriptor); + if (handle == INVALID_HANDLE_VALUE) { errno = 9; return nullptr; } + const int access = WindowsDescriptorAccess_nid_no_patch(handle, descriptor, "fdopen"); + const int requested = nativeMode.find('+') != std::string::npos ? 2 : nativeMode[0] == 'r' ? 0 : 1; + if (access != 2 && access != requested) { errno = 22; return nullptr; } + if (parsed->append && !AppendThroughDescriptor(descriptor)) return nullptr; + auto* native = ::_fdopen(descriptor, nativeMode.c_str()); #else - auto* native = ::fdopen(descriptor, mode); + auto* native = ::fdopen(descriptor, nativeMode.c_str()); #endif if (!native) return nullptr; auto* stream = new (storage.get()) FileStream(native, true); @@ -53,21 +301,44 @@ 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; +} + 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. - const char* supported[] = {"r", "w", "a", "rb", "wb", "ab", "r+", "w+", "a+", - "rb+", "wb+", "ab+", "r+b", "w+b", "a+b"}; - bool valid = false; - for (const auto* candidate : supported) if (std::strcmp(mode, candidate) == 0) valid = true; - if (!valid) { errno = 22; return nullptr; } + const auto parsed = ParseFileMode(mode); + if (!parsed) { errno = 22; return nullptr; } try { - const auto path = *filename ? ResolvePath_nid_no_patch(filename).string() : std::string{}; - if (stream->Reopen(path.c_str(), NativeFileMode(mode).c_str())) { - if (!path.empty() && WritesFile(mode)) RecordWrittenPath_nid_no_patch(path); + std::filesystem::path path; + if (*filename) { + if (const int error = ResolvePathError_nid_no_patch(filename, path)) { + 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(), 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; @@ -77,33 +348,35 @@ FileStream* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode FileStream* APS5_VABI fopen_nid_postfix(const char* filename, const char* mode) { if (!filename || !mode) throw std::runtime_error(std::string(__func__) + ": " + FOPEN_MSG_NULL_ARG); - const std::filesystem::path fpath = ResolvePath_nid_no_patch(filename); - const auto abs_path = fpath.string(); - std::unique_ptr handle(std::fopen(abs_path.c_str(), NativeFileMode(mode).c_str()), std::fclose); + const auto parsed = ParseFileMode(mode); + if (!parsed) { + 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; - if (error == ENOENT) { - errno = error; - return nullptr; - } - const auto reason = std::strerror(error); - std::error_code ec; - std::filesystem::path sibling; - for (const auto& entry : std::filesystem::directory_iterator(fpath.parent_path(), ec)) { - if (ec) break; - if (entry.path().stem() == fpath.stem() && entry.path().extension() != fpath.extension()) { - sibling = std::filesystem::absolute(entry.path()); - break; - } - } - if (!sibling.empty()) { - // APS5_LOG_OUT("%s: \"%s\": %s. Sibling: \"%s\"", FOPEN_MSG_NOT_FOUND, abs_path.c_str(), reason, sibling.filename().string().c_str()); - return nullptr; - } - throw std::runtime_error(std::string(__func__) + ": " + FOPEN_MSG_OPEN_FAILED + ": \"" + abs_path + "\": " + reason); + errno = OpenFailure(fpath, parsed->writes, errno); + if (errno == 2) errno = NotFoundError_nid_no_patch(fpath); + return nullptr; } // APS5_LOG_OUT("success: \"%s\"", abs_path.c_str()); - if (WritesFile(mode)) RecordWrittenPath_nid_no_patch(fpath); + if (parsed->append) static_cast(std::fseek(handle.get(), 0, SEEK_END)); + if (parsed->writes) RecordWrittenPath_nid_no_patch(fpath); auto stream = std::make_unique(handle.get(), true); handle.release(); return stream.release(); @@ -111,17 +384,22 @@ 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 || !filename || !mode) return GuestEinval; + 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); - stream->Close(); - return 0; + return stream->Close(); } int APS5_VABI fseek_nid_postfix(FileStream* stream, std::int64_t offset, int origin); @@ -134,17 +412,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); @@ -152,7 +428,8 @@ 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 (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"); @@ -160,26 +437,38 @@ size_t APS5_VABI fread_nid_postfix(void* buffer, size_t size, size_t count, File 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(); - if (std::ferror(handle)) throw std::runtime_error("fread: read failed"); 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(); - if (result != count || std::ferror(handle)) throw std::runtime_error("fwrite: write failed"); 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 - const int result = _fseeki64(GetNativeStream(stream), offset, origin); + auto* native = GetNativeStream(stream); + if (IsPipeStream(native)) { errno = 29; return -1; } + const int result = _fseeki64(native, offset, origin); #else static_assert(sizeof(off_t) == 8); const int result = ::fseeko(GetNativeStream(stream), offset, origin); @@ -191,8 +480,11 @@ 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 - const auto result = _ftelli64(GetNativeStream(stream)); + auto* native = GetNativeStream(stream); + if (IsPipeStream(native)) { errno = 29; return -1; } + const auto result = _ftelli64(native); #else static_assert(sizeof(off_t) == 8); const auto result = ::ftello(GetNativeStream(stream)); @@ -220,24 +512,42 @@ int APS5_VABI fsetpos_nid_postfix(FileStream* stream, const std::int64_t* positi return fseeko_nid_postfix(stream, *position, SEEK_SET); } +void APS5_VABI rewind_nid_postfix(FileStream* stream) { + const int saved = errno; + if (fseeko_nid_postfix(stream, 0, SEEK_SET) == 0) errno = saved; + stream->ClearError(); +} + 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)); - if (result == EOF) throw std::runtime_error("fputs: write failed"); + stream->SyncStatus(); return result; } int APS5_VABI fflush_nid_postfix(FileStream* stream) { - if (std::fflush(stream ? GetNativeStream(stream) : nullptr) != 0) throw std::runtime_error("fflush: flush failed"); - return 0; + if (stream && !stream->IsOpen()) { errno = 9; return -1; } + std::FILE* native = stream ? GetStreamHandle(stream) : nullptr; + if (stream && stream->IsRandomDevice()) return 0; +#ifdef __linux__ + if (native && !__fwriting(native)) return 0; +#endif + const int result = std::fflush(native); + if (stream) stream->SyncStatus(); + return result; } int APS5_VABI malloc_stats_fast_nid_postfix(void* stats) { return ApplicationHeapStatsFast_nid_no_patch(stats); } +size_t APS5_VABI malloc_usable_size_nid_postfix(void* pointer) { + return ApplicationHeapUsableSize_nid_no_patch(pointer); +} + void* APS5_VABI malloc_nid_postfix(size_t size) { - return ApplicationHeapAllocate_nid_no_patch(size); + return allocationOrNull([size] { return ApplicationHeapAllocate_nid_no_patch(size); }); } void APS5_VABI free_nid_postfix(void* ptr) { @@ -245,23 +555,42 @@ void APS5_VABI free_nid_postfix(void* ptr) { } void* APS5_VABI realloc_nid_postfix(void* ptr, size_t newSize) { - return ApplicationHeapReallocate_nid_no_patch(ptr, newSize); + return allocationOrNull([ptr, newSize] { return ApplicationHeapReallocate_nid_no_patch(ptr, newSize); }); +} + +void* APS5_VABI reallocf_nid_postfix(void* ptr, size_t newSize) { + try { + return ApplicationHeapReallocate_nid_no_patch(ptr, newSize); + } catch (const std::bad_alloc&) { + } catch (const std::length_error&) { + } + ApplicationHeapFree_nid_no_patch(ptr); + errno = 12; + return nullptr; } void* APS5_VABI memalign_nid_postfix(size_t alignment, size_t size) { - return ApplicationHeapAlign_nid_no_patch(alignment, size); + if (!validAlignment(alignment)) { + errno = 22; + return nullptr; + } + return allocationOrNull([alignment, size] { return ApplicationHeapAlign_nid_no_patch(alignment, size); }); } void* APS5_VABI aligned_alloc_nid_postfix(size_t alignment, size_t size) { - return ApplicationHeapAlign_nid_no_patch(alignment, size); + return memalign_nid_postfix(alignment, size); } void* APS5_VABI reallocalign_nid_postfix(void* ptr, size_t size, size_t alignment) { - return ApplicationHeapRealign_nid_no_patch(ptr, size, alignment); + if (size != 0 && !validAlignment(alignment)) { + errno = 22; + return nullptr; + } + return allocationOrNull([ptr, size, alignment] { return ApplicationHeapRealign_nid_no_patch(ptr, size, alignment); }); } void* APS5_VABI calloc_nid_postfix(size_t count, size_t size) { - return ApplicationHeapCalloc_nid_no_patch(count, size); + return allocationOrNull([count, size] { return ApplicationHeapCalloc_nid_no_patch(count, size); }); } int APS5_VABI posix_memalign_nid_postfix(void** pointer, size_t alignment, size_t size) { @@ -293,6 +622,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"); @@ -301,27 +663,23 @@ void APS5_VABI qsort_nid_postfix(void* base, size_t count, size_t size, int (APS if (count > std::numeric_limits::max() / size) throw std::overflow_error("qsort: array size overflow"); if (count == 1) return; - auto* bytes = static_cast(base); - const auto swapElements = [bytes, size](size_t left, size_t right) { - auto* leftElement = bytes + left * size; - auto* rightElement = bytes + right * size; - for (size_t index = 0; index < size; ++index) std::swap(leftElement[index], rightElement[index]); - }; - const auto siftDown = [bytes, size, compare, &swapElements](size_t root, size_t heapSize) { - while (root < heapSize / 2) { - size_t child = root * 2 + 1; - if (child + 1 < heapSize && compare(bytes + child * size, bytes + (child + 1) * size) < 0) ++child; - if (compare(bytes + root * size, bytes + child * size) >= 0) return; - swapElements(root, child); - root = child; - } - }; + SortElements(static_cast(base), count, size, compare); +} - for (size_t parent = count / 2; parent != 0; --parent) siftDown(parent - 1, count); - for (size_t heapSize = count; heapSize > 1;) { - swapElements(0, --heapSize); - siftDown(0, heapSize); +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 new file mode 100644 index 0000000000..1c088bb0d1 --- /dev/null +++ b/core/libs/prx/libc/src/FileLock.cpp @@ -0,0 +1,104 @@ +#include "prx/libc/include/FileLock.hpp" + +#ifdef _WIN32 + +#include +#include +#include +#include +#include +#include + +#include "prx/libc/include/WindowsDescriptor.hpp" + +namespace { + +constexpr int LockShared = 1; +constexpr int LockExclusive = 2; +constexpr int LockNonblocking = 4; +constexpr int LockUnlock = 8; +constexpr DWORD LockByteLow = 0xFFFFFFFE; +constexpr DWORD LockByteHigh = 0x7FFFFFFF; + +struct LockState { + std::mutex mutex; + 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; +std::map> g_locks; + +std::shared_ptr Materialize(int fd) { + const auto found = g_locks.find(fd); + if (found != g_locks.end()) return found->second; + return g_locks.emplace(fd, std::make_shared()).first->second; +} + +} + +extern "C" { + +int GuestFileLock_nid_no_patch(int fd, int operation) { + const int mode = operation & (LockShared | LockExclusive | LockUnlock); + if (mode != LockShared && mode != LockExclusive && mode != LockUnlock) { + errno = EINVAL; + ::SetLastError(ERROR_INVALID_PARAMETER); + return -1; + } + const auto handle = reinterpret_cast(::_get_osfhandle(fd)); + if (handle == INVALID_HANDLE_VALUE) { + errno = EBADF; + ::SetLastError(ERROR_INVALID_HANDLE); + return -1; + } + const auto state = [&] { + std::lock_guard lock(g_mutex); + return Materialize(fd); + }(); + std::lock_guard lock(state->mutex); + if (state->mode == mode) return 0; + if (state->mode != 0) { + OVERLAPPED overlapped{}; + overlapped.Offset = LockByteLow; + overlapped.OffsetHigh = LockByteHigh; + 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(state->locker != nullptr ? state->locker : handle, flags, 0, 1, 0, &overlapped)) return -1; + state->mode = mode; + return 0; +} + +void GuestFileLockShare_nid_no_patch(int from, int to) { + std::lock_guard lock(g_mutex); + auto state = Materialize(from); + g_locks[to] = std::move(state); +} + +void GuestFileLockForget_nid_no_patch(int fd) { + std::lock_guard lock(g_mutex); + g_locks.erase(fd); +} + +} + +#endif diff --git a/core/libs/prx/libc/src/Filesystem.cpp b/core/libs/prx/libc/src/Filesystem.cpp index caaa9b69ac..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; @@ -75,13 +76,30 @@ extern "C" int APS5_VABI remove_nid_postfix(const char* path) { if (!*path) { errno = 2; return -1; } try { const auto resolved = ResolvePath_nid_no_patch(path); + if (FinalPathComponentIsDot_nid_no_patch(path)) { + std::error_code statusError; + const auto status = std::filesystem::status(resolved, statusError); + if (statusError) { errno = FilesystemError(statusError); return -1; } + if (!std::filesystem::exists(status)) { errno = 2; return -1; } + if (!std::filesystem::is_directory(status)) { errno = 20; return -1; } + errno = 22; + return -1; + } std::error_code error; #ifdef _WIN32 const DWORD attributes = GetFileAttributesW(resolved.c_str()); bool removed = false; if (attributes != INVALID_FILE_ATTRIBUTES) { + const DWORD writable = attributes & ~FILE_ATTRIBUTE_READONLY; + const bool unlocked = writable != attributes && + SetFileAttributesW(resolved.c_str(), writable ? writable : FILE_ATTRIBUTE_NORMAL); removed = (attributes & FILE_ATTRIBUTE_DIRECTORY) ? RemoveDirectoryW(resolved.c_str()) != 0 : DeleteFileW(resolved.c_str()) != 0; + if (!removed && unlocked) { + const DWORD nativeError = GetLastError(); + SetFileAttributesW(resolved.c_str(), attributes); + SetLastError(nativeError); + } } if (!removed) { const DWORD nativeError = GetLastError(); @@ -93,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 7395115b5a..fba7f15a33 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -15,14 +15,54 @@ #include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/ApplicationHeap.hpp" -#ifdef _WIN32 #include "prx/libc/include/WindowsFormatting.hpp" + +#ifdef _WIN32 #include "prx/libc/include/WindowsScanning.hpp" -#include "prx/libc/include/WindowsWideFormatting.hpp" +#endif + +#if defined(__GLIBC__) +extern "C" { +int __isoc99_vsscanf(const char*, const char*, std::va_list) noexcept; +int __isoc99_vfscanf(std::FILE*, const char*, std::va_list); +} #endif namespace { +#ifndef _WIN32 +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 == '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; + 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"); + if (*format == '[') { + ++format; + if (*format == '^') ++format; + if (*format == ']') ++format; + while (*format && *format != ']') ++format; + } + if (*format) ++format; + } +} +#endif + std::string HostLength(const std::string& length, char conversion) { #ifdef _WIN32 if (length == "l" && std::strchr("diouxXn", conversion) != nullptr) return "ll"; @@ -32,6 +72,32 @@ std::string HostLength(const std::string& length, char conversion) { return length; } +int HostVsscanf(const char* input, const char* format, std::va_list args) { +#if defined(__GLIBC__) + return __isoc99_vsscanf(input, format, args); +#else + return std::vsscanf(input, format, args); +#endif +} + +int HostSscanf(const char* input, const char* format, ...) { + std::va_list args; + va_start(args, format); + const int result = HostVsscanf(input, format, args); + va_end(args); + return result; +} + +#ifndef _WIN32 +int HostVfscanf(std::FILE* stream, const char* format, std::va_list args) { +#if defined(__GLIBC__) + return __isoc99_vfscanf(stream, format, args); +#else + return std::vfscanf(stream, format, args); +#endif +} +#endif + template int ScanGuest(const char* buffer, const char* format, bool secure, NextPointer nextPointer, NextCapacity nextCapacity) { if (buffer == nullptr || format == nullptr) return EOF; @@ -77,6 +143,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); @@ -98,7 +165,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); @@ -115,7 +185,7 @@ int ScanGuest(const char* buffer, const char* format, bool secure, NextPointer n directive += specifier; directive += "%n"; int consumed = -1; - const int matched = suppress ? std::sscanf(input, directive.c_str(), &consumed) : std::sscanf(input, directive.c_str(), target, &consumed); + const int matched = suppress ? HostSscanf(input, directive.c_str(), &consumed) : HostSscanf(input, directive.c_str(), target, &consumed); if (consumed < 0 || (!suppress && matched != 1)) return finish(!converted && *input == '\0' ? EOF : assigned); if (capped && conversion != 'c') { const char next = input[consumed]; @@ -142,21 +212,6 @@ int ScanGuest(const char* buffer, const char* format, bool secure, NextPointer n extern "C" { -int APS5_VABI swprintf_nid_postfix(wchar_t* output, size_t capacity, const wchar_t* format, ...) { -#ifdef _WIN32 - __builtin_sysv_va_list args; - __builtin_sysv_va_start(args, format); - const int result = LibcDetail::FormatWideWindows(output, capacity, format, args); - __builtin_sysv_va_end(args); -#else - std::va_list args; - va_start(args, format); - const int result = std::vswprintf(output, capacity, format, args); - va_end(args); -#endif - return result; -} - int APS5_VABI vasprintf_nid_postfix(char** destination, const char* format, VaList* args) { if (!destination) { errno = 22; return -1; } *destination = nullptr; @@ -167,16 +222,22 @@ int APS5_VABI vasprintf_nid_postfix(char** destination, const char* format, VaLi const int count = LibcDetail::FormatWindows(nullptr, 0, format, args, &text); const char* source = text.c_str(); #else + std::string guestText; + const bool guestFormatting = RequiresGuestFormatting(format); char* text = nullptr; std::va_list copy; va_copy(copy, *reinterpret_cast(args)); - const int count = ::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 = 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; @@ -187,6 +248,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; @@ -194,6 +256,13 @@ 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 (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; + stream->SyncStatus(); + return result; + } std::va_list copy; va_copy(copy, *reinterpret_cast(args)); const int result = std::vfprintf(native, format, copy); @@ -221,16 +290,19 @@ int APS5_VABI fprintf_nid_postfix(FileStream* stream, const char* format, ...) { } int APS5_VABI fscanf_nid_postfix(FileStream* stream, const char* format, ...) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } auto* native = GetNativeStream(stream); + if (!stream->SwitchToReading()) return EOF; #ifdef _WIN32 __builtin_sysv_va_list args; __builtin_sysv_va_start(args, format); const int result = LibcDetail::ScanFileWindows_nid_no_patch(native, format, args); __builtin_sysv_va_end(args); #else + RejectGuestWideScan(format); std::va_list args; va_start(args, format); - const int result = std::vfscanf(native, format, args); + const int result = HostVfscanf(native, format, args); va_end(args); #endif stream->SyncStatus(); @@ -240,6 +312,7 @@ int APS5_VABI fscanf_nid_postfix(FileStream* stream, const char* format, ...) { #ifdef _WIN32 int APS5_VABI printf_nid_postfix(const char* format, ...) { + if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; } __builtin_sysv_va_list args; __builtin_sysv_va_start(args, format); const int result = LibcDetail::PrintWindows(format, args); @@ -248,6 +321,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); @@ -274,17 +348,19 @@ 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 = 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 = 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; } @@ -292,7 +368,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, ...) { std::va_list args; va_start(args, format); - const int result = 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; } @@ -300,7 +376,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 = 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; } @@ -311,9 +387,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); std::va_list copy; va_copy(copy, *reinterpret_cast(args)); - const int result = std::vsscanf(input, format, copy); + const int result = HostVsscanf(input, format, copy); va_end(copy); return result; #endif @@ -329,9 +406,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 = std::vsscanf(input, format, args); + const int result = HostVsscanf(input, format, args); va_end(args); return result; } @@ -350,6 +428,7 @@ 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); }); @@ -360,12 +439,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 (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 @@ -375,6 +456,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 (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); @@ -387,6 +469,7 @@ int APS5_VABI vsnprintf_nid_postfix(char* str, size_t size, const char* format, #ifdef _WIN32 return LibcDetail::FormatWindows(str, size, format, c); #else + 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); @@ -395,12 +478,34 @@ 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) { + LibcDetail::FormatArguments arguments(args); + return ScanGuest(buffer, format, true, [&] { return arguments.Next(); }, [&] { return arguments.Next(); }); +} + 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; } @@ -415,7 +520,14 @@ 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 vsprintf_s_nid_postfix(char* buffer, size_t size, const char* format, VaList* 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); @@ -426,7 +538,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 = 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; } @@ -434,7 +546,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 = std::vsnprintf(buffer, size, format, args); + const int result = FormatSecure(buffer, size, format, args); va_end(args); return result; } @@ -442,7 +554,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 = 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; } @@ -450,7 +562,49 @@ 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(); + 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 7808ba4ba5..a2b3d0fe7d 100644 --- a/core/libs/prx/libc/src/FormattingWide.cpp +++ b/core/libs/prx/libc/src/FormattingWide.cpp @@ -91,22 +91,38 @@ void AppendPadded(std::u16string& out, const std::u16string& text, bool left, in std::u16string FormatWide(const char16_t* format, VaList* source, bool secure = false) { if (format == nullptr || source == nullptr) throw std::invalid_argument("Null formatting argument"); LibcDetail::FormatArguments args(source); + const char16_t* const start = format; + std::string ascii; + for (const char16_t* unit = format; *unit != 0; ++unit) ascii.push_back(*unit < 0x80 ? static_cast(*unit) : '?'); + args.Number(LibcDetail::NumberedArgumentClasses(ascii.c_str())); + const auto position = [&] { + const char* cursor = ascii.c_str() + (format - start); + const std::size_t found = LibcDetail::ArgumentPosition(cursor); + format = start + (cursor - ascii.c_str()); + return found; + }; + const auto star = [&] { + const std::size_t found = position(); + return found ? args.At(found) : args.Next(); + }; std::u16string out; while (*format) { if (*format != u'%') { out.push_back(*format++); continue; } ++format; if (*format == u'%') { out.push_back(*format++); continue; } + if (const std::size_t found = position()) args.Seek(found); std::string spec = "%"; bool left = false; int width = 0; int precision = -1; - while (In(*format, "-+ #0")) { + while (In(*format, "-+ #0'")) { if (*format == u'-') left = true; - spec += static_cast(*format++); + if (*format != u'\'') spec += static_cast(*format); + ++format; } if (*format == u'*') { ++format; - width = args.Next(); + width = star(); if (width < 0) { left = true; spec += '-'; width = -width; } spec += std::to_string(width); } else { @@ -117,7 +133,7 @@ std::u16string FormatWide(const char16_t* format, VaList* source, bool secure = precision = 0; if (*format == u'*') { ++format; - precision = args.Next(); + precision = star(); if (precision >= 0) spec += "." + std::to_string(precision); } else { spec += '.'; @@ -151,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()); @@ -159,7 +177,8 @@ std::u16string FormatWide(const char16_t* format, VaList* source, bool secure = AppendNumber(out, spec + static_cast(conversion), args.Next()); } } else if (conversion == u'p' && length.empty()) { - AppendNumber(out, spec + 'p', args.Next()); + const std::string text = LibcDetail::FormatPointer(spec, args.Next()); + out.append(text.begin(), text.end()); } else if (conversion == u'c' || conversion == u'C') { const int value = args.Next(); std::u16string text; @@ -198,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; } @@ -214,6 +236,13 @@ int APS5_VABI vswprintf_nid_postfix(char16_t* buffer, std::size_t size, const ch } } +int APS5_VABI swprintf_nid_postfix(char16_t* buffer, std::size_t size, const char16_t* format, ...) { + APS5_VA_BEGIN(format); + const int result = vswprintf_nid_postfix(buffer, size, format, reinterpret_cast(args)); + APS5_VA_END(); + return result; +} + int APS5_VABI snwprintf_s_nid_postfix(char16_t* buffer, std::size_t size, const char16_t* format, ...) { constexpr std::size_t RsizeMax = SIZE_MAX >> 1; int result = -1; @@ -235,13 +264,70 @@ int APS5_VABI snwprintf_s_nid_postfix(char16_t* buffer, std::size_t size, const return result; } +int APS5_VABI vswprintf_s_nid_postfix(char16_t* buffer, std::size_t size, const char16_t* format, VaList* args) { + constexpr std::size_t RsizeMax = (SIZE_MAX >> 1) / sizeof(char16_t); + int result = -1; + if (buffer != nullptr && format != nullptr && args != nullptr && size != 0 && size <= RsizeMax) { + try { + const std::u16string text = FormatWide(format, args, true); + if (text.size() + 1 <= size && text.size() <= static_cast(INT_MAX)) { + std::memcpy(buffer, text.data(), text.size() * sizeof(char16_t)); + buffer[text.size()] = 0; + result = static_cast(text.size()); + } + } catch (const std::invalid_argument&) { + } + } + if (result < 0 && buffer != nullptr && size != 0 && size <= RsizeMax) buffer[0] = 0; + return result; +} + +int APS5_VABI swprintf_s_nid_postfix(char16_t* buffer, std::size_t size, const char16_t* format, ...) { + constexpr std::size_t RsizeMax = (SIZE_MAX >> 1) / sizeof(char16_t); + int result = -1; + if (buffer != nullptr && format != nullptr && size != 0 && size <= RsizeMax) { + APS5_VA_BEGIN(format); + try { + const std::u16string text = FormatWide(format, reinterpret_cast(args), true); + if (text.size() + 1 <= size && text.size() <= static_cast(INT_MAX)) { + std::memcpy(buffer, text.data(), text.size() * sizeof(char16_t)); + buffer[text.size()] = 0; + result = static_cast(text.size()); + } + } catch (const std::invalid_argument&) { + } + APS5_VA_END(); + } + if (result < 0 && buffer != nullptr && size != 0 && size <= RsizeMax) buffer[0] = 0; + return result; +} + +int APS5_VABI vsnwprintf_s_nid_postfix(char16_t* buffer, std::size_t size, const char16_t* format, VaList* args) { + constexpr std::size_t RsizeMax = (SIZE_MAX >> 1) / sizeof(char16_t); + int result = -1; + if (buffer != nullptr && format != nullptr && args != nullptr && size != 0 && size <= RsizeMax) { + try { + const std::u16string text = FormatWide(format, args, true); + if (text.size() <= static_cast(INT_MAX)) { + const std::size_t copied = text.size() < size - 1 ? text.size() : size - 1; + std::memcpy(buffer, text.data(), copied * sizeof(char16_t)); + buffer[copied] = 0; + result = static_cast(text.size()); + } + } catch (const std::invalid_argument&) { + } + } + if (result < 0 && buffer != nullptr && size != 0 && size <= RsizeMax) buffer[0] = 0; + return result; +} + int APS5_VABI wprintf_nid_postfix(const char16_t* format, ...) { APS5_VA_BEGIN(format); int result = -1; 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; } @@ -249,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 { @@ -278,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 a190e8fcc1..26c1935c5a 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -1,3 +1,5 @@ +#include +#include #include #include #include @@ -11,6 +13,7 @@ #include #include #include +#include #include #ifdef _WIN32 #ifndef NOMINMAX @@ -20,11 +23,17 @@ #define WIN32_LEAN_AND_MEAN #endif #include +#include #else +#include +#include +#include #include #endif #include "prx/libc/include/General.hpp" -#include "prx/libc/include/GuestHeap.hpp" +#include "prx/libc/include/FilesystemError.hpp" +#include "prx/libc/include/AppArchive.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" namespace { // Guest prefixes (without leading slashes) mapped to host directories, e.g. save-data mount points: @@ -33,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) { @@ -65,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); @@ -99,9 +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)); - return result.make_preferred(); + if (relative.size() == prefix.size()) { + const auto result = std::filesystem::path(host).make_preferred(); + return module ? PathOrRelinkedModule(result) : result; + } +#ifndef _WIN32 + if (guestPath.back() == '/') { + auto result = ResolveHostPath(host, SplitNames(std::string_view(relative).substr(prefix.size() + 1))); + result /= ""; + return module ? PathOrRelinkedModule(result) : result; + } +#endif + return ResolveHostPath(host, SplitNames(std::string_view(relative).substr(prefix.size() + 1)), module).make_preferred(); } return std::nullopt; } @@ -111,41 +272,133 @@ 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 Resolve(WorkingDirectory& state, const char* path) { +WorkingDirectory& Directories() { + static auto* state = new WorkingDirectory; + return *state; +} +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; + if (!error) error = std::make_error_code(std::filesystem::exists(status) ? std::errc::not_a_directory : std::errc::no_such_file_or_directory); +#ifdef _WIN32 + if (error == std::errc::no_such_file_or_directory) { + for (auto prefix = host.parent_path(); prefix.has_relative_path(); prefix = prefix.parent_path()) { + std::error_code prefixError; + const auto prefixStatus = std::filesystem::status(prefix, prefixError); + if (!std::filesystem::exists(prefixStatus)) continue; + if (!std::filesystem::is_directory(prefixStatus)) error = std::make_error_code(std::errc::not_a_directory); + break; + } + } +#endif + throw std::filesystem::filesystem_error("Guest path component is not a directory", host, error); +} +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 - auto guest = (std::filesystem::path("/") / state.current.lexically_relative(state.root)); - guest = (input.is_absolute() ? input : guest / input).lexically_normal(); - if (auto aliased = ResolveAlias(guest.relative_path().generic_string())) return *aliased; - return ResolveHostPath(state.root, guest.relative_path()).make_preferred(); + 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(); + } + trailing = name.empty() || name == "." || name == ".."; + if (!trailing) names.push_back(name); + } + 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; +WriteMark Now() { + FILETIME now; + GetSystemTimeAsFileTime(&now); + return now; +} +bool NotBefore(const WriteMark& time, const WriteMark& mark) { + return CompareFileTime(&time, &mark) >= 0; +} +#else +using WriteMark = timespec; +WriteMark Now() { + timespec now; + clock_gettime(CLOCK_REALTIME_COARSE, &now); + return now; +} +bool NotBefore(const WriteMark& time, const WriteMark& mark) { + if (time.tv_sec == mark.tv_sec) return time.tv_nsec >= mark.tv_nsec; + return time.tv_sec >= mark.tv_sec; +} +#endif struct WrittenPathRegistry { std::mutex syncMutex; std::mutex mutex; std::map dirty; -#ifdef _WIN32 - FILETIME mark = [] { FILETIME now; GetSystemTimeAsFileTime(&now); return now; }(); -#endif + WriteMark mark = Now(); }; WrittenPathRegistry& Written() { static WrittenPathRegistry registry; return registry; } #ifdef _WIN32 -std::optional WriteTime(const std::filesystem::path& path) { +std::optional WriteTime(const std::filesystem::path& path) { WIN32_FILE_ATTRIBUTE_DATA data; if (GetFileAttributesExW(path.c_str(), GetFileExInfoStandard, &data)) return data.ftLastWriteTime; const auto error = GetLastError(); if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND) return std::nullopt; throw std::system_error(static_cast(error), std::system_category(), "Reading the write time of " + path.string()); } -bool ChangedSince(const std::filesystem::path& path, const FILETIME& mark) { - const auto time = WriteTime(path); - return time && CompareFileTime(&*time, &mark) >= 0; -} void FlushPath(const std::filesystem::path& path) { const auto handle = CreateFileW(path.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); @@ -159,7 +412,29 @@ void FlushPath(const std::filesystem::path& path) { if (!FlushFileBuffers(handle)) throw std::system_error(static_cast(GetLastError()), std::system_category(), "Flushing " + path.string()); } +#else +std::optional WriteTime(const std::filesystem::path& path) { + struct stat data; + if (::stat(path.c_str(), &data) == 0) return data.st_mtim; + if (errno == ENOENT || errno == ENOTDIR) return std::nullopt; + throw std::system_error(errno, std::generic_category(), "Reading the write time of " + path.string()); +} +void FlushPath(const std::filesystem::path& path) { + const int descriptor = ::open(path.c_str(), O_RDONLY | O_NONBLOCK | O_CLOEXEC); + if (descriptor < 0) { + if (errno == ENOENT || errno == ENOTDIR || errno == EACCES) return; + throw std::system_error(errno, std::generic_category(), "Opening " + path.string() + " for sync"); + } + const int result = ::fsync(descriptor); + const int error = errno; + ::close(descriptor); + if (result != 0 && error != EINVAL) throw std::system_error(error, std::generic_category(), "Flushing " + path.string()); +} #endif +bool ChangedSince(const std::filesystem::path& path, const WriteMark& mark) { + const auto time = WriteTime(path); + return time && NotBefore(*time, mark); +} int DirectoryFailure(const std::error_code& error) { if (error == std::errc::permission_denied) return 13; if (error == std::errc::not_a_directory) return 20; @@ -168,7 +443,121 @@ int DirectoryFailure(const std::error_code& error) { if (error == std::errc::too_many_symbolic_link_levels) return 62; return 5; } +int SetWorkingDirectory(WorkingDirectory& state, const std::filesystem::path& resolved) { + std::error_code error; + 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; + 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; } +} + +#ifndef _WIN32 +namespace { +std::mutex g_hostStandardErrorMutex; +std::atomic g_hostStandardError{-1}; +} + +extern "C" int KeepHostStandardError_nid_no_patch() { + const std::lock_guard lock(g_hostStandardErrorMutex); + if (const int kept = g_hostStandardError.load(); kept >= 0) return kept; + const int kept = ::fcntl(2, F_DUPFD_CLOEXEC, 3); + if (kept < 0) { + if (errno == EBADF) return -1; + throw std::system_error(errno, std::generic_category(), "Keeping the host standard error"); + } + std::FILE* const stream = ::fdopen(kept, "w"); + if (stream == nullptr) { + const int error = errno; + ::close(kept); + throw std::system_error(error, std::generic_category(), "Keeping the host standard error"); + } + std::setvbuf(stream, nullptr, _IONBF, 0); + std::fflush(stderr); + stderr = stream; + g_hostStandardError.store(kept); + return kept; +} + +extern "C" int HostStandardError_nid_no_patch() { + return g_hostStandardError.load(); +} + +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) { if (guestPrefix == nullptr || hostPath == nullptr) { @@ -187,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; @@ -214,10 +620,8 @@ extern "C" std::vector WrittenPaths_nid_no_patch() { extern "C" void SyncWrittenPaths_nid_no_patch() { auto& registry = Written(); std::lock_guard serial(registry.syncMutex); -#ifdef _WIN32 - FILETIME start; - GetSystemTimeAsFileTime(&start); - FILETIME mark; + const auto start = Now(); + WriteMark mark; std::vector> entries; { std::lock_guard lock(registry.mutex); @@ -233,7 +637,7 @@ extern "C" void SyncWrittenPaths_nid_no_patch() { for (const auto& [path, dirty] : entries) { const auto time = WriteTime(path); if (!time) missing.push_back(path); - if (!dirty && !(time && CompareFileTime(&*time, &mark) >= 0)) continue; + if (!dirty && !(time && NotBefore(*time, mark))) continue; if (time && flushed.insert(path).second) FlushPath(path); for (auto ancestor = path.parent_path(); ancestor.has_relative_path() && !flushed.contains(ancestor) && ChangedSince(ancestor, mark); ancestor = ancestor.parent_path()) { @@ -246,11 +650,6 @@ extern "C" void SyncWrittenPaths_nid_no_patch() { const auto entry = registry.dirty.find(path); if (entry != registry.dirty.end() && !entry->second) registry.dirty.erase(entry); } -#else - ::sync(); - std::lock_guard lock(registry.mutex); - registry.dirty.clear(); -#endif } extern "C" void BlockPathAlias_nid_no_patch(const char* guestPrefix) { @@ -264,28 +663,76 @@ 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) { + const auto relinked = RelinkedModuleName(path); + std::error_code error; + if (std::filesystem::is_regular_file(relinked, error)) return relinked; + 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) { + if (resolved == nullptr) return 14; + auto& state = Directories(); + std::lock_guard lock(state.mutex); + try { + return SetWorkingDirectory(state, std::filesystem::path(reinterpret_cast(resolved))); + } catch (const std::bad_alloc&) { + return 12; + } catch (const std::filesystem::filesystem_error& error) { + return DirectoryFailure(error.code()); + } +} + +extern "C" bool FinalPathComponentIsDot_nid_no_patch(const char* path) { + std::string_view text(path); + while (!text.empty() && (text.back() == '/' || text.back() == '\\')) text.remove_suffix(1); + const auto separator = text.find_last_of("/\\"); + const auto component = text.substr(separator == std::string_view::npos ? 0 : separator + 1); + return component == "."; } 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); - std::error_code error; - const auto resolved = std::filesystem::canonical(Resolve(state, path), error); - if (error) { errno = DirectoryFailure(error); return -1; } - if (!std::filesystem::is_directory(resolved, error)) { - errno = error ? DirectoryFailure(error) : 20; return -1; - } - const auto relative = resolved.lexically_relative(state.root); - if (relative.empty() || *relative.begin() == "..") { errno = 45; return -1; } - state.current = resolved; + 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 == 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; } @@ -303,14 +750,80 @@ 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(GuestHeap::GuestHeapAllocate_nid_postfix(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; } catch (const std::bad_alloc&) { errno = 12; return nullptr; } catch (const std::filesystem::filesystem_error& error) { errno = DirectoryFailure(error.code()); return nullptr; } } -extern "C" void NotImplemented_nid_no_patch(const char* funcName) { +#ifdef _WIN32 +extern "C" int WindowsOpenError_nid_no_patch(unsigned long error) { + switch (error) { + case ERROR_FILE_NOT_FOUND: + case ERROR_PATH_NOT_FOUND: + case ERROR_INVALID_DRIVE: + case ERROR_BAD_NETPATH: + case ERROR_BAD_NET_NAME: + case ERROR_BAD_PATHNAME: + case ERROR_FILENAME_EXCED_RANGE: + return ENOENT; + case ERROR_ACCESS_DENIED: + case ERROR_SHARING_VIOLATION: + case ERROR_LOCK_VIOLATION: + case ERROR_WRITE_PROTECT: + case ERROR_NETWORK_ACCESS_DENIED: + case ERROR_CANNOT_MAKE: + case ERROR_DRIVE_LOCKED: + return EACCES; + case ERROR_FILE_EXISTS: + case ERROR_ALREADY_EXISTS: + return EEXIST; + case ERROR_TOO_MANY_OPEN_FILES: + return EMFILE; + case ERROR_NOT_ENOUGH_MEMORY: + case ERROR_OUTOFMEMORY: + return ENOMEM; + case ERROR_DISK_FULL: + return ENOSPC; + case ERROR_CANT_RESOLVE_FILENAME: + return ELOOP; + default: + return EINVAL; + } +} + +extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); +static DWORD StandardHandleId(int descriptor) { + return descriptor == 0 ? STD_INPUT_HANDLE : descriptor == 1 ? STD_OUTPUT_HANDLE : STD_ERROR_HANDLE; +} +extern "C" void* HostStandardHandle_nid_no_patch(int descriptor) { + if (descriptor < 0 || descriptor > 2) return nullptr; + const HANDLE current = GetStdHandle(StandardHandleId(descriptor)); + if (current == nullptr || current == INVALID_HANDLE_VALUE) return nullptr; + const auto previous = _set_thread_local_invalid_parameter_handler([](const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {}); + const auto owned = ::_get_osfhandle(descriptor); + _set_thread_local_invalid_parameter_handler(previous); + if (reinterpret_cast(owned) != current) return current; + HANDLE kept = nullptr; + if (!DuplicateHandle(GetCurrentProcess(), current, GetCurrentProcess(), &kept, 0, TRUE, DUPLICATE_SAME_ACCESS)) + throw std::runtime_error("Cannot keep the host standard handle of descriptor " + std::to_string(descriptor)); + return kept; +} +extern "C" void KeepStandardHandle_nid_no_patch(int descriptor, void* handle) { + if (handle != nullptr && !SetStdHandle(StandardHandleId(descriptor), handle)) + throw std::runtime_error("Cannot keep the host standard handle of descriptor " + std::to_string(descriptor)); +} +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" [[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 a29a4cf1ce..96ff94d0bf 100644 --- a/core/libs/prx/libc/src/GuestAllocations.cpp +++ b/core/libs/prx/libc/src/GuestAllocations.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/GuestAllocations.hpp" +#include #include #include #include @@ -34,16 +35,39 @@ struct Registry { std::mutex mutex; std::map> ranges; bool mainImageRegistered = false; + std::map registeredImages; + 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}; std::atomic invalidator{nullptr}; -std::atomic pinWaiter{nullptr}; +std::atomic pinWaiter{nullptr}; +std::atomic gpuMapObserver{nullptr}; +std::atomic unmapObserver{nullptr}; +std::atomic gpuMapObserverCalls{0}; std::chrono::milliseconds pinWait() { static const std::chrono::milliseconds value{[] { @@ -63,6 +87,8 @@ void require(bool condition, const char* reason) { 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) { @@ -87,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; @@ -104,16 +142,43 @@ bool backWritableImageAnonymously() { } void* GuestAllocationsBegin_nid_postfix() { - return new MutationState{std::unique_lock(registry().mutex), {}}; + auto* state = new MutationState{std::unique_lock(registry().mutex), {}}; + registry().readableLease.reset(); + registry().allLease.reset(); + return state; } void GuestAllocationsEnd_nid_postfix(void* mutation) noexcept { auto* state = static_cast(mutation); - generation.fetch_add(1, std::memory_order_release); + 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); } + if (!state->gpuMapped.empty()) gpuMapObserverCalls.fetch_add(1); + const auto observer = state->gpuMapped.empty() ? nullptr : gpuMapObserver.load(); + Lease::Ranges mapped; + if (observer != nullptr) { + const auto& ranges = registry().ranges; + for (const auto& [address, bytes] : state->gpuMapped) { + auto it = ranges.upper_bound(address); + if (it != ranges.begin()) --it; + for (; it != ranges.end() && it->first < address + bytes; ++it) { + const auto& range = it->second; + if (range->gpu && range->readable && range->bytes != 0 && range->address + range->bytes > address) mapped.push_back(range); + } + } + std::sort(mapped.begin(), mapped.end(), [](const auto& left, const auto& right) { return left->address < right->address; }); + mapped.erase(std::unique(mapped.begin(), mapped.end()), mapped.end()); + } + const Mapped weak(mapped.begin(), mapped.end()); + mapped.clear(); + const bool noted = !state->gpuMapped.empty(); + registry().readableLease.reset(); + registry().allLease.reset(); delete state; + if (!weak.empty()) observer(weak, ended); + if (noted) gpuMapObserverCalls.fetch_sub(1); } std::uint64_t GuestAllocationsGeneration_nid_postfix() { @@ -129,22 +194,30 @@ void GuestAllocationsInvalidate_nid_postfix(std::uintptr_t address, std::size_t if (const auto callback = invalidator.load(std::memory_order_acquire)) callback(address, bytes); } -void GuestAllocationsSetPinWaiter_nid_postfix(bool (*callback)()) { +void GuestAllocationsSetGpuMapObserver_nid_postfix(void (*callback)(const Mapped& ranges, std::uint64_t generation)) { + gpuMapObserver.store(callback); + while (gpuMapObserverCalls.load() != 0) std::this_thread::yield(); +} + +void GuestAllocationsNoteGpuMapping_nid_postfix(void* mutation, const void* pointer, std::size_t bytes) { + if (mutation != nullptr && bytes != 0) static_cast(mutation)->gpuMapped.emplace_back(reinterpret_cast(pointer), bytes); +} + +void GuestAllocationsSetPinWaiter_nid_postfix(bool (*callback)(std::uintptr_t, std::size_t)) { pinWaiter.store(callback, std::memory_order_release); } #ifdef _WIN32 -void GuestAllocationsRegisterMainImage_nid_postfix(void*) { +namespace { +void registerImage(const void* image) { auto& state = registry(); - if (state.mainImageRegistered) return; - const auto image = GetModuleHandleW(nullptr); - require(image != nullptr, "cannot locate the main guest image"); + if (state.registeredImages.contains(reinterpret_cast(image))) return; auto replacement = state.ranges; auto cursor = reinterpret_cast(image); bool registered = false; for (;;) { MEMORY_BASIC_INFORMATION memory{}; - require(VirtualQuery(reinterpret_cast(cursor), &memory, sizeof(memory)) == sizeof(memory), "cannot query the main guest image"); + require(VirtualQuery(reinterpret_cast(cursor), &memory, sizeof(memory)) == sizeof(memory), "cannot query a guest image"); if (memory.AllocationBase != image) break; require(memory.Type == MEM_IMAGE && (memory.State == MEM_COMMIT || memory.State == MEM_RESERVE), "unsupported guest image mapping"); require(reinterpret_cast(memory.BaseAddress) == cursor && memory.RegionSize != 0 && memory.RegionSize <= std::numeric_limits::max() - cursor, "invalid guest image range"); @@ -160,42 +233,67 @@ void GuestAllocationsRegisterMainImage_nid_postfix(void*) { 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; } - require(registered, "main guest image has no committed pages"); + require(registered, "guest image has no committed pages"); state.ranges.swap(replacement); - state.mainImageRegistered = true; + state.registeredImages.emplace(reinterpret_cast(image), cursor); } -#else +} + void GuestAllocationsRegisterMainImage_nid_postfix(void*) { auto& state = registry(); if (state.mainImageRegistered) return; + const auto image = GetModuleHandleW(nullptr); + require(image != nullptr, "cannot locate the main guest image"); + registerImage(image); + state.mainImageRegistered = true; +} + +void GuestAllocationsRegisterImage_nid_postfix(void*, const void* image) { + require(image != nullptr, "cannot register a guest image without a base"); + registerImage(image); +} + +#else +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; @@ -208,15 +306,50 @@ 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 GuestAllocationsAdd_nid_postfix(void* mutation, void* pointer, std::size_t bytes, bool readable, bool writable) { +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); require(address != 0 && bytes <= std::numeric_limits::max() - address, "invalid guest allocation range"); @@ -228,12 +361,12 @@ 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})); + 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) { char message[160]; - std::snprintf(message, sizeof(message), "guest allocation 0x%llx+0x%llx is owned by an active GPU command (%s)", static_cast(address), static_cast(bytes), why); + std::snprintf(message, sizeof(message), "guest allocation 0x%llx+0x%llx is still pinned by GPU work or an import (%s)", static_cast(address), static_cast(bytes), why); throw std::runtime_error(message); } @@ -252,6 +385,8 @@ void GuestAllocationsRequireUnpinned_nid_postfix(void* mutation, const void* poi const auto deadline = std::chrono::steady_clock::now() + pinWait(); int syncedRounds = 0; for (;;) { + registry().readableLease.reset(); + registry().allLease.reset(); bool pinned = false; auto it = ranges.upper_bound(address); if (it != ranges.begin()) --it; @@ -263,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(); - 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(); @@ -308,15 +451,46 @@ 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"); const auto end = address + bytes; auto cursor = address; - for (const auto& [base, range] : registry().ranges) { - const auto finish = base + range->bytes; + const auto& ranges = registry().ranges; + auto it = ranges.upper_bound(address); + if (it != ranges.begin()) --it; + for (; it != ranges.end(); ++it) { + const auto base = it->first; + const auto finish = base + it->second->bytes; if (finish <= cursor) continue; - if (base > cursor || !range->releasable) return false; + if (base > cursor || !it->second->releasable) return false; cursor = finish; if (cursor >= end) return true; } @@ -329,12 +503,12 @@ Range GuestAllocationsFind_nid_postfix(void*, const void* pointer) { if (exact != registry().ranges.end() && exact->second->allocationAddress == address && exact->second->allocationBytes == exact->second->bytes) { const auto& range = *exact->second; require(range.releasable, "guest image memory is not a releasable allocation"); - return {address, range.allocationBytes, range.readable, range.writable, address, range.allocationBytes, range.releasable}; + return {address, range.allocationBytes, range.readable, range.writable, address, range.allocationBytes, range.releasable, range.gpu}; } for (const auto& [base, range] : registry().ranges) { if (range->allocationAddress == address) { require(range->releasable, "guest image memory is not a releasable allocation"); - return {address, range->allocationBytes, range->readable, range->writable, address, range->allocationBytes, range->releasable}; + return {address, range->allocationBytes, range->readable, range->writable, address, range->allocationBytes, range->releasable, range->gpu}; } } throw std::runtime_error("guest allocation is not registered"); @@ -348,47 +522,76 @@ void GuestAllocationsRemove_nid_postfix(void* mutation, const void* pointer) { const auto exact = ranges.find(range.address); if (exact != ranges.end() && exact->second->allocationBytes == exact->second->bytes) { ranges.erase(exact); - return; + } else { + std::erase_if(ranges, [&](const auto& entry) { return entry.second->allocationAddress == range.address; }); } - std::erase_if(ranges, [&](const auto& entry) { return entry.second->allocationAddress == range.address; }); + if (const auto observer = unmapObserver.load(std::memory_order_acquire)) observer(range.address, range.bytes); } namespace { -std::map> replaceRange(const void* pointer, std::size_t bytes, bool remove, bool readable, bool writable) { +struct RangePatch { + std::vector erased; + std::vector>> inserted; +}; + +RangePatch replaceRange(const void* pointer, std::size_t bytes, bool remove, bool readable, bool writable, bool gpu) { const auto address = reinterpret_cast(pointer); require(bytes != 0 && bytes <= std::numeric_limits::max() - address, "invalid guest protection or unmap range"); require(!writable || readable, "writable guest allocation must be readable"); const auto end = address + bytes; - auto replacement = registry().ranges; + RangePatch patch; auto cursor = address; - for (const auto& [base, entry] : registry().ranges) { - const auto& range = *entry; + const auto& ranges = registry().ranges; + auto it = ranges.upper_bound(address); + if (it != ranges.begin()) --it; + for (; it != ranges.end(); ++it) { + const auto base = it->first; + const auto& range = *it->second; const auto finish = base + range.bytes; if (finish <= address) continue; if (base >= end) break; require(base <= cursor, "guest protection or unmap range has a hole"); - replacement.erase(base); - const auto insert = [&](std::uint64_t first, std::uint64_t last, bool canRead, bool canWrite) { - if (first < last) replacement.emplace(first, std::make_shared(Range{first, static_cast(last - first), canRead, canWrite, range.allocationAddress, range.allocationBytes, range.releasable})); + patch.erased.push_back(base); + 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); - if (!remove) insert(std::max(base, address), std::min(finish, end), readable, writable); - insert(std::min(finish, end), finish, range.readable, range.writable); - 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 replacement; + return patch; +} + +void applyPatch(const RangePatch& patch) { + auto& ranges = registry().ranges; + for (const auto key : patch.erased) ranges.erase(key); + for (const auto& [key, range] : patch.inserted) ranges.emplace(key, range); +} + +bool allocationRemains(const Range& range, const RangePatch& patch) { + for (const auto& [key, entry] : patch.inserted) { + if (entry->allocationAddress == range.allocationAddress) return true; + } + const auto& ranges = registry().ranges; + const auto limit = range.allocationAddress + range.allocationBytes; + for (auto it = ranges.lower_bound(range.allocationAddress); it != ranges.end() && it->first < limit; ++it) { + if (it->second->allocationAddress != range.allocationAddress) continue; + if (std::find(patch.erased.begin(), patch.erased.end(), it->first) == patch.erased.end()) return true; + } + return false; } } -void GuestAllocationsProtect_nid_postfix(void* mutation, const void* pointer, std::size_t bytes, bool readable, bool writable, const std::function& apply) { +void GuestAllocationsProtect_nid_postfix(void* mutation, const void* pointer, std::size_t bytes, bool readable, bool writable, bool gpu, const std::function& apply) { GuestAllocationsRequireUnpinned_nid_postfix(mutation, pointer, bytes); recordChange(mutation, pointer, bytes); - auto replacement = replaceRange(pointer, bytes, false, readable, writable); + const auto patch = replaceRange(pointer, bytes, false, readable, writable, gpu); apply(); - registry().ranges.swap(replacement); + applyPatch(patch); } void GuestAllocationsUnmap_nid_postfix(void* mutation, const void* pointer, std::size_t bytes, const std::function& apply) { @@ -402,7 +605,7 @@ void GuestAllocationsUnmap_nid_postfix(void* mutation, const void* pointer, std: const Range* containing = nullptr; if (found != registry().ranges.begin()) { const auto& candidate = *std::prev(found)->second; - if (cursor < candidate.allocationAddress + candidate.allocationBytes) containing = &candidate; + if (cursor < candidate.address + candidate.bytes) containing = &candidate; } if (containing == nullptr) { if (found == registry().ranges.end() || found->first >= end) break; @@ -411,27 +614,60 @@ void GuestAllocationsUnmap_nid_postfix(void* mutation, const void* pointer, std: } const auto range = *containing; require(range.releasable, "guest image memory cannot be unmapped"); - const auto pieceEnd = std::min(end, range.allocationAddress + range.allocationBytes); - auto replacement = replaceRange(reinterpret_cast(cursor), pieceEnd - cursor, true, false, false); - bool last = true; - for (const auto& [base, entry] : replacement) { - if (entry->allocationAddress == range.allocationAddress) last = false; - } + const auto pieceEnd = std::min(end, range.address + range.bytes); + const auto patch = replaceRange(reinterpret_cast(cursor), pieceEnd - cursor, true, false, false, false); + const bool last = !allocationRemains(range, patch); apply(reinterpret_cast(cursor), pieceEnd - cursor, reinterpret_cast(range.allocationAddress), last); - registry().ranges.swap(replacement); + applyPatch(patch); + if (const auto observer = unmapObserver.load(std::memory_order_acquire)) observer(cursor, pieceEnd - cursor); any = true; cursor = pieceEnd; } require(any, "unmap address is not registered"); + if (const auto observer = unmapObserver.load(std::memory_order_acquire)) observer(reinterpret_cast(pointer), bytes); +} + +void GuestAllocationsSetUnmapObserver_nid_postfix(void (*callback)(std::uintptr_t, std::size_t)) { + unmapObserver.store(callback, std::memory_order_release); } Lease GuestAllocationsAcquire_nid_postfix() { std::lock_guard lock(registry().mutex); - Lease result; + if (auto shared = registry().readableLease) return Lease(std::move(shared)); + auto ranges = std::make_shared(); for (const auto& [address, range] : registry().ranges) { - if (range->readable && range->bytes != 0) result.push_back(range); + if (range->readable && range->bytes != 0) ranges->push_back(range); } - return result; + registry().readableLease = ranges; + return Lease(std::move(ranges)); +} + +std::shared_ptr GuestAllocationsLookup_nid_postfix(std::uintptr_t address, bool next) { + std::lock_guard lock(registry().mutex); + const auto& ranges = registry().ranges; + auto it = ranges.upper_bound(address); + if (it != ranges.begin()) { + const auto& previous = std::prev(it)->second; + if (previous->bytes != 0 && address < previous->address + previous->bytes) return previous; + } + if (next) { + for (; it != ranges.end(); ++it) { + if (it->second->bytes != 0) return it->second; + } + } + return nullptr; +} + +Lease GuestAllocationsAcquireAll_nid_postfix() { + std::lock_guard lock(registry().mutex); + 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) { + if (range->bytes != 0) ranges->push_back(range); + } + registry().allLease = ranges; + return Lease(std::move(ranges)); } } diff --git a/core/libs/prx/libc/src/GuestArena.cpp b/core/libs/prx/libc/src/GuestArena.cpp index 703533df07..20cdbc1820 100644 --- a/core/libs/prx/libc/src/GuestArena.cpp +++ b/core/libs/prx/libc/src/GuestArena.cpp @@ -1,10 +1,24 @@ #include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" #include "prx/libc/include/WindowsMappings.hpp" #include #include #include +#include +#ifndef _WIN32 +#include +#include +#include +#include +#include +#endif +#ifdef __APPLE__ +#include +#include +#endif #include #include +#include #include #include #include @@ -19,15 +33,23 @@ namespace GuestArena { namespace { +std::atomic hostReadHook{nullptr}; + #ifdef _WIN32 std::atomic commitGeneration{1}; std::atomic privateMappingObserver{nullptr}; #endif +std::atomic accessObserver{nullptr}; -constexpr std::uintptr_t ArenaStart = 0x0000000200000000ull; -constexpr std::uintptr_t SystemReservedStart = 0x00000007FFFFC000ull; -constexpr std::uintptr_t SystemReservedEnd = 0x0000001000000000ull; -constexpr std::uintptr_t ApplicationAreaEnd = 0x000000FC00000000ull; +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; +constexpr std::uintptr_t ApplicationAreaEnd = 0x000000FC00000000ull + GuestArenaOffset; std::uintptr_t alignUp(std::uintptr_t value, std::size_t alignment) { return (value + alignment - 1) & ~(static_cast(alignment) - 1); @@ -36,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 { @@ -54,21 +76,59 @@ 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)); + 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) { + if (_base == 0) throw std::runtime_error("guest address space arena is unavailable"); + if (bytes == 0 || alignment == 0 || (alignment & (alignment - 1)) != 0) throw std::invalid_argument("invalid guest arena window"); + std::lock_guard lock(_lock); + auto limit = _end; + while (limit >= _base && bytes <= limit - _base) { + const auto candidate = (limit - bytes) & ~(alignment - 1); + if (candidate < _base) break; + const auto next = _used.lower_bound(candidate + bytes); + if (next == _used.begin() || std::prev(next)->second <= candidate) return candidate; + limit = std::prev(next)->first; } - 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 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); + const auto end = start + bytes; + const auto exact = _used.find(start); + if (exact != _used.end() && exact->second >= end) return false; + const auto next = _used.lower_bound(start); + if ((next != _used.end() && next->first < end) || (next != _used.begin() && std::prev(next)->second > start)) { + char message[160]; + std::snprintf(message, sizeof(message), "guest arena commit 0x%llx+0x%zx overlaps a used range", static_cast(start), bytes); + throw std::runtime_error(message); + } + _used.emplace(start, end); + return true; } void MarkUsed(const void* pointer, std::size_t bytes) { @@ -107,7 +167,9 @@ public: private: Arena() { -#ifdef _WIN32 +#if defined(__linux__) || defined(__APPLE__) + SeedHostUsedRanges(); +#elif defined(_WIN32) _writeWatched = std::getenv("APS5_NO_WRITE_WATCH") == nullptr; SYSTEM_INFO system{}; GetSystemInfo(&system); @@ -138,10 +200,133 @@ private: #endif } +#if defined(__linux__) + 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; + while (std::getline(maps, line)) { + std::istringstream fields(line); + 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); + _used.emplace(first, last); + _hostRegions.emplace_back(first, last); + } + _used.emplace(SystemReservedStart, SystemReservedEnd); + _hostRegions.emplace_back(SystemReservedStart, SystemReservedEnd); + ReserveFreeRanges(); + _base = ArenaStart; + _end = ApplicationAreaEnd; + } + + static void TryReserve(std::vector>& occupied, std::uintptr_t from, std::uintptr_t to) { + 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); + 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", + static_cast(cursor), static_cast(end - cursor)); + throw std::system_error(failed, std::system_category(), message); + } + occupied.emplace_back(cursor, end); + } + } + + void ReserveFreeRanges() { + std::vector> occupied; + std::uintptr_t cursor = ArenaStart; + for (const auto& [usedStart, usedEnd] : _used) { + if (usedStart > ApplicationAreaEnd) break; + if (usedStart > cursor) TryReserve(occupied, cursor, std::min(usedStart, ApplicationAreaEnd)); + if (usedEnd > cursor) cursor = usedEnd; + if (cursor >= ApplicationAreaEnd) break; + } + if (cursor < ApplicationAreaEnd) TryReserve(occupied, cursor, ApplicationAreaEnd); + for (const auto& [from, to] : occupied) { + _used.emplace(from, to); + _hostRegions.emplace_back(from, to); + } + } +#endif + std::mutex _lock; 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; @@ -178,6 +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); } @@ -191,15 +388,30 @@ void GuestArenaRange_nid_postfix(std::uintptr_t* base, std::size_t* bytes) { *bytes = Arena::Get().Size(); } +namespace { + +std::invalid_argument OutsideArena(const char* operation, const void* pointer, std::size_t bytes) { + char message[128]; + std::snprintf(message, sizeof(message), "%s 0x%llx+0x%zx outside the guest arena", operation, static_cast(reinterpret_cast(pointer)), bytes); + return std::invalid_argument(message); +} + +} + #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) { return WindowsMappings::Get().HandleWrite(address); } +bool GuestArenaHandleRead_nid_postfix(std::uintptr_t address) { + return WindowsMappings::Get().HandleRead(address); +} + void GuestArenaPinWritable_nid_postfix(const void* pointer, std::size_t bytes) { if (!Arena::Get().Contains(pointer, bytes)) return; WindowsMappings::Get().Pin(reinterpret_cast(pointer), bytes); @@ -218,20 +430,18 @@ bool GuestArenaCollectWrites_nid_postfix(std::uintptr_t address, std::size_t byt return WindowsMappings::Get().Collect(address, bytes, pages, count, clear); } +bool GuestArenaCollectArmedWrites_nid_postfix(std::uintptr_t address, std::size_t bytes, void** pages, std::size_t* count, bool clear) { + return WindowsMappings::Get().Collect(address, bytes, pages, count, clear, WindowsMappings::Collection::Armed); +} + +bool GuestArenaCollectFreshWrites_nid_postfix(std::uintptr_t address, std::size_t bytes, void** pages, std::size_t* count, bool clear) { + return WindowsMappings::Get().Collect(address, bytes, pages, count, clear, WindowsMappings::Collection::Fresh); +} + bool GuestArenaHostRegionOverlaps_nid_postfix(std::uintptr_t address, std::size_t bytes) { return Arena::Get().OverlapsHostRegion(address, bytes); } -namespace { - -std::invalid_argument OutsideArena(const char* operation, const void* pointer, std::size_t bytes) { - char message[128]; - std::snprintf(message, sizeof(message), "%s 0x%llx+0x%zx outside the guest arena", operation, static_cast(reinterpret_cast(pointer)), bytes); - return std::invalid_argument(message); -} - -} - std::uint64_t GuestArenaCommitGeneration_nid_postfix() { return commitGeneration.load(std::memory_order_acquire); } @@ -247,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) { @@ -256,7 +467,35 @@ void GuestArenaReset_nid_postfix(void* pointer, std::size_t bytes) { void GuestArenaMap_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection) { if (!Arena::Get().Contains(pointer, bytes)) throw OutsideArena("shared mapping", pointer, bytes); - WindowsMappings::Get().Map(pointer, bytes, section, offset, protection); + WindowsMappings::Get().Map(pointer, bytes, section, offset, protection, Arena::Get().WriteWatched(), false); + 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); + 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) { @@ -266,28 +505,110 @@ void* GuestArenaMapAlias_nid_postfix(std::uintptr_t address, std::size_t bytes) void GuestArenaUnmapAlias_nid_postfix(void* alias) { WindowsMappings::Get().UnmapAlias(alias); } + +bool GuestArenaResidentSpan_nid_postfix(std::uintptr_t address, std::size_t bytes, std::uintptr_t* start, std::uintptr_t* end) { + return WindowsMappings::Get().ResidentSpan(address, bytes, start, end); +} + +void GuestArenaForgetSection_nid_postfix(void* section) { + WindowsMappings::Get().ForgetSection(section); +} + +bool GuestArenaForgetReusableSection_nid_no_patch(void* section) { + return WindowsMappings::Get().ForgetSection(section); +} +#endif + +#if defined(__linux__) || defined(__APPLE__) +namespace { +int PosixProtection(std::uint32_t windowsProtection) { + switch (windowsProtection) { + case 0x01: return PROT_NONE; + case 0x02: return PROT_READ; + case 0x04: return PROT_READ | PROT_WRITE; + case 0x08: return PROT_READ | PROT_WRITE; + case 0x10: return PROT_EXEC; + case 0x20: return PROT_READ | PROT_EXEC; + case 0x40: return PROT_READ | PROT_WRITE | PROT_EXEC; + default: throw std::runtime_error("guest arena: unmapped protection value"); + } +} + +void* PlaceAt(void* pointer, std::size_t bytes, std::uint32_t protection) { + return mmap(pointer, bytes, PosixProtection(protection), MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); +} +} +#endif + +#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) { + (void)granule; + if (!Arena::Get().Contains(pointer, bytes)) throw OutsideArena("commit", pointer, bytes); + const bool marked = Arena::Get().MarkUsedForCommit(pointer, bytes); + try { + if (PlaceAt(pointer, bytes, protection) == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "guest arena commit failed"); + } catch (...) { + if (marked) Arena::Get().Release(pointer, bytes); + throw; + } + noteAccess(pointer, bytes); +} + +void GuestArenaReset_nid_postfix(void* pointer, std::size_t bytes) { + if (!Arena::Get().Contains(pointer, bytes)) throw OutsideArena("reset", pointer, bytes); + if (PlaceAt(pointer, bytes, 0x01) == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "guest arena reset failed"); +} #endif 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); #else - (void)pointer; - (void)bytes; + GuestWriteWatch::GuestWriteWatchBeginHostWrite_nid_postfix(pointer, bytes); return true; #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); #else - (void)pointer; - (void)bytes; + GuestWriteWatch::GuestWriteWatchEndHostWrite_nid_postfix(pointer, bytes); #endif } diff --git a/core/libs/prx/libc/src/GuestHeap.cpp b/core/libs/prx/libc/src/GuestHeap.cpp index ae9d1674c4..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 @@ -22,9 +24,12 @@ namespace GuestHeap { namespace { +constexpr std::uint32_t ReadWriteProtection = 0x04; + // Each block is preceded by a header holding the raw block address and its size in bytes. constexpr std::size_t HeaderBytes = 2 * sizeof(void*); -constexpr std::size_t MinimumAlignment = 16; +constexpr std::size_t RawAlignment = 16; +constexpr std::size_t MinimumAlignment = 32; constexpr std::size_t PageBytes = 0x4000; constexpr std::size_t SpanBytes = 1u << 20; constexpr std::size_t MaximumSmallBytes = 64u * 1024u; @@ -35,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() { @@ -49,8 +76,9 @@ public: if (total > MaximumSmallBytes) { blockBytes = alignUp(total, LargeGranuleBytes); if (void* cached = _large.Take(blockBytes)) return cached; - void* raw = GuestArena::GuestArenaAllocate_nid_postfix(blockBytes, PageBytes); + void* raw = arenaAllocate(blockBytes, PageBytes); commit(raw, blockBytes); + grow(usage().system, usage().maxSystem, blockBytes); return raw; } auto& sizeClass = _classes[classIndex(total)]; @@ -62,8 +90,9 @@ public: return block; } if (sizeClass.bump == nullptr || sizeClass.bytes > static_cast(sizeClass.bumpEnd - sizeClass.bump)) { - auto* span = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SpanBytes, PageBytes)); + auto* span = static_cast(arenaAllocate(SpanBytes, PageBytes)); commit(span, SpanBytes); + grow(usage().system, usage().maxSystem, SpanBytes); sizeClass.bump = span; sizeClass.bumpEnd = span + SpanBytes; } @@ -108,12 +137,41 @@ private: return static_cast(found - _classes.begin()); } + static void* arenaAllocate(std::size_t bytes, std::size_t alignment) { + try { + return GuestArena::GuestArenaAllocate_nid_postfix(bytes, alignment); + } catch (const std::runtime_error&) { + throw std::bad_alloc(); + } + } + static void commit(void* pointer, std::size_t bytes) { + try { + GuestArena::GuestArenaCommit_nid_postfix(pointer, bytes, ReadWriteProtection, bytes); + } catch (const std::system_error& error) { + abandon(pointer, bytes); + if (outOfMemory(error)) throw std::bad_alloc(); + throw; + } catch (...) { + abandon(pointer, bytes); + throw; + } + } + + static void abandon(void* pointer, std::size_t bytes) { + GuestArena::GuestArenaReset_nid_postfix(pointer, bytes); + GuestArena::GuestArenaRelease_nid_postfix(pointer, bytes); + } + + static bool outOfMemory(const std::system_error& error) { #ifdef _WIN32 - GuestArena::GuestArenaCommit_nid_postfix(pointer, bytes, PAGE_READWRITE, bytes); + constexpr int NotEnoughMemory = 8; + constexpr int OutOfMemory = 14; + constexpr int CommitmentLimit = 1455; + return error.code().category() == std::system_category() && + (error.code().value() == NotEnoughMemory || error.code().value() == OutOfMemory || error.code().value() == CommitmentLimit); #else - (void)pointer; - (void)bytes; + return error.code() == std::errc::not_enough_memory; #endif } @@ -167,11 +225,10 @@ private: } static void release(void* raw, std::size_t bytes) { -#ifdef _WIN32 GuestArena::GuestArenaReset_nid_postfix(raw, bytes); GuestAllocations::GuestAllocationsInvalidate_nid_postfix(reinterpret_cast(raw), bytes); -#endif GuestArena::GuestArenaRelease_nid_postfix(raw, bytes); + shrink(usage().system, bytes); } std::mutex _lock; @@ -189,18 +246,23 @@ 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) { +void* allocate(GuestAllocations::Mutation& mutation, std::size_t alignment, std::size_t bytes, bool gpu) { if (alignment == 0 || (alignment & (alignment - 1)) != 0) throw std::invalid_argument("invalid guest heap alignment"); alignment = std::max(alignment, MinimumAlignment); - const std::size_t padding = alignment > MinimumAlignment ? alignment : 0; + const std::size_t padding = alignment > RawAlignment ? alignment : 0; if (bytes > std::numeric_limits::max() - HeaderBytes - padding) throw std::length_error("guest heap allocation overflow"); std::size_t blockBytes = 0; void* raw = rawAllocate(bytes + HeaderBytes + padding, blockBytes); @@ -209,24 +271,27 @@ void* allocate(GuestAllocations::Mutation& mutation, std::size_t alignment, std: reinterpret_cast(pointer)[-2] = raw; reinterpret_cast(pointer)[-1] = blockBytes; try { - mutation.Add(pointer, bytes, true, true); + mutation.Add(pointer, bytes, true, true, gpu); } catch (...) { 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); } } void* GuestHeapAllocate_nid_postfix(std::size_t bytes) { GuestAllocations::Mutation mutation; - return allocate(mutation, MinimumAlignment, bytes); + return allocate(mutation, MinimumAlignment, bytes, false); } void GuestHeapFree_nid_postfix(void* pointer) { @@ -246,7 +311,7 @@ void* GuestHeapReallocate_nid_postfix(void* pointer, std::size_t bytes) { free(mutation, pointer); return nullptr; } - void* result = allocate(mutation, MinimumAlignment, bytes); + void* result = allocate(mutation, MinimumAlignment, bytes, range.gpu); std::memcpy(result, pointer, std::min(bytes, range.bytes)); free(mutation, pointer); return result; @@ -261,15 +326,37 @@ void* GuestHeapRealign_nid_postfix(void* pointer, std::size_t bytes, std::size_t free(mutation, pointer); return nullptr; } - void* result = allocate(mutation, alignment, bytes); + void* result = allocate(mutation, alignment, bytes, range.gpu); std::memcpy(result, pointer, std::min(bytes, range.bytes)); free(mutation, pointer); return result; } +std::size_t GuestHeapUsableSize_nid_postfix(const void* pointer) { + if (pointer == nullptr) return 0; + GuestAllocations::Mutation mutation; + return mutation.Find(pointer).bytes; +} + void* GuestHeapAlign_nid_postfix(std::size_t alignment, std::size_t bytes) { GuestAllocations::Mutation mutation; - return allocate(mutation, alignment, bytes); + return allocate(mutation, alignment, bytes, false); +} + +void* GuestHeapAlignForGpu_nid_postfix(std::size_t alignment, std::size_t bytes) { + GuestAllocations::Mutation mutation; + 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/GuestStacks.cpp b/core/libs/prx/libc/src/GuestStacks.cpp new file mode 100644 index 0000000000..9b24019938 --- /dev/null +++ b/core/libs/prx/libc/src/GuestStacks.cpp @@ -0,0 +1,94 @@ +#include "prx/libc/include/GuestStacks.hpp" + +#ifdef _WIN32 +#include "prx/libc/include/GuestArena.hpp" +#include +#include +#include +#include +#endif + +namespace GuestStacks { +namespace { + +#ifdef _WIN32 +struct StackBounds { + std::uintptr_t base; + std::uintptr_t limit; + std::uintptr_t deallocation; +}; + +constexpr std::size_t DeallocationStackOffset = 0x1478; + +std::mutex rangesLock; +std::map ranges; + +StackBounds CurrentBounds() { + auto* tib = reinterpret_cast(NtCurrentTeb()); + auto* teb = reinterpret_cast(tib); + return {reinterpret_cast(tib->StackBase), reinterpret_cast(tib->StackLimit), + *reinterpret_cast(teb + DeallocationStackOffset)}; +} + +void ApplyBounds(const StackBounds& bounds) { + auto* tib = reinterpret_cast(NtCurrentTeb()); + auto* teb = reinterpret_cast(tib); + tib->StackBase = reinterpret_cast(bounds.base); + tib->StackLimit = reinterpret_cast(bounds.limit); + *reinterpret_cast(teb + DeallocationStackOffset) = bounds.deallocation; +} + +bool RegisteredBounds(std::uintptr_t stackPointer, StackBounds& bounds) { + std::lock_guard lock(rangesLock); + const auto next = ranges.upper_bound(stackPointer); + if (next == ranges.begin()) return false; + const auto& [low, high] = *std::prev(next); + if (stackPointer >= high) return false; + bounds = {high, low, low}; + return true; +} + +bool HostBounds(std::uintptr_t stackPointer, StackBounds& bounds) { + MEMORY_BASIC_INFORMATION memory{}; + if (VirtualQuery(reinterpret_cast(stackPointer), &memory, sizeof(memory)) != sizeof(memory) || memory.State != MEM_COMMIT) return false; + const auto base = reinterpret_cast(memory.BaseAddress); + bounds = {base + memory.RegionSize, base, reinterpret_cast(memory.AllocationBase)}; + return true; +} +#endif + +} + +void GuestStackRegister_nid_no_patch(const void* pointer, std::size_t bytes) { +#ifdef _WIN32 + const auto low = reinterpret_cast(pointer); + const auto high = low + bytes; + std::lock_guard lock(rangesLock); + const auto same = ranges.find(low); + if (same != ranges.end() && same->second == high) return; + auto overlap = ranges.upper_bound(low); + if (overlap != ranges.begin() && std::prev(overlap)->second > low) --overlap; + while (overlap != ranges.end() && overlap->first < high) { + GuestArena::GuestArenaUnpinWritable_nid_postfix(reinterpret_cast(overlap->first), overlap->second - overlap->first); + overlap = ranges.erase(overlap); + } + GuestArena::GuestArenaPinWritable_nid_postfix(pointer, bytes); + ranges.emplace(low, high); +#else + (void)pointer; + (void)bytes; +#endif +} + +void APS5_VABI GuestStackSwitch_nid_no_patch(std::uintptr_t stackPointer) { +#ifdef _WIN32 + const auto current = CurrentBounds(); + if (stackPointer >= current.deallocation && stackPointer < current.base) return; + StackBounds target{}; + if (RegisteredBounds(stackPointer, target) || HostBounds(stackPointer, target)) ApplyBounds(target); +#else + (void)stackPointer; +#endif +} + +} diff --git a/core/libs/prx/libc/src/GuestWriteWatch.cpp b/core/libs/prx/libc/src/GuestWriteWatch.cpp index afde2dfc58..e0a4344e67 100644 --- a/core/libs/prx/libc/src/GuestWriteWatch.cpp +++ b/core/libs/prx/libc/src/GuestWriteWatch.cpp @@ -22,6 +22,22 @@ #include #endif +#if defined(__APPLE__) +#include +#include +#include +#include +#include +#include +#include +#include +#include +#endif + +#if (defined(__linux__) && defined(PAGEMAP_SCAN) && defined(UFFD_FEATURE_WP_ASYNC)) || defined(__APPLE__) +#define APS5_GUEST_WRITE_WATCH 1 +#endif + namespace GuestWriteWatch { namespace { @@ -31,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 { @@ -74,15 +90,68 @@ 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); end = (end + PageBytes - 1) & ~(PageBytes - 1); if (end <= begin) return true; + if (!takeFresh(begin, end, true, written, context)) return false; + return scanWritten(begin, end, written, context); + } + + bool CollectFresh(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); + end = (end + PageBytes - 1) & ~(PageBytes - 1); + if (end <= begin) return true; + { + std::shared_lock lock(_lock); + auto it = _fresh.upper_bound(begin); + if (it != _fresh.begin()) --it; + while (it != _fresh.end() && it->first < end && it->second <= begin) ++it; + if (it == _fresh.end() || it->first >= end) return true; + } + return takeFresh(begin, end, false, written, context); + } + + bool CollectArmed(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); + end = (end + PageBytes - 1) & ~(PageBytes - 1); + if (end <= begin) return true; + std::vector> armed; + { + std::shared_lock lock(_lock); + if (!covers(begin, end)) return false; + auto cursor = begin; + auto it = _fresh.upper_bound(begin); + if (it != _fresh.begin()) --it; + for (; it != _fresh.end() && it->first < end; ++it) { + if (it->second <= cursor) continue; + if (it->first > cursor) armed.emplace_back(cursor, it->first); + cursor = std::max(cursor, it->second); + } + if (cursor < end) armed.emplace_back(cursor, end); + } + for (const auto& [from, to] : armed) { + if (!scanWritten(from, to, written, context)) return false; + } + return true; + } + +private: + bool takeFresh(std::uintptr_t begin, std::uintptr_t end, bool whole, void (*written)(void*, std::uintptr_t, std::uintptr_t), void* context) { std::vector> fresh; { std::unique_lock lock(_lock); - if (!covers(begin, end)) return false; + if (whole && !covers(begin, end)) return false; auto it = _fresh.upper_bound(begin); if (it != _fresh.begin()) --it; for (; it != _fresh.end() && it->first < end; ++it) { @@ -90,7 +159,7 @@ public: const auto to = std::min(it->second, end); if (from < to) fresh.emplace_back(from, to); } - if (!fresh.empty()) remove(_fresh, begin, end); + for (const auto& [from, to] : fresh) remove(_fresh, from, to); } for (const auto& [from, to] : fresh) { written(context, from, to); @@ -101,9 +170,14 @@ public: remove(_fresh, left, right); insert(_fresh, left, right); } - written(context, begin, end); + if (whole) written(context, begin, end); + else for (const auto& [left, right] : fresh) written(context, left, right); return false; } + return true; + } + + bool scanWritten(std::uintptr_t begin, std::uintptr_t end, void (*written)(void*, std::uintptr_t, std::uintptr_t), void* context) const { std::array regions; auto cursor = begin; while (cursor < end) { @@ -249,13 +323,341 @@ private: std::map _fresh; }; +const bool g_opened = (Watch::Get(), true); +#elif defined(__APPLE__) +class Watch { +public: + static Watch& Get() { + static auto* watch = new Watch; + return *watch; + } + + bool Available() const { + return _available; + } + + void Register(std::uintptr_t begin, std::uintptr_t end) { + if (!_available || end <= begin) return; + begin = pageStart(begin); + end = pageEnd(end); + if (end <= begin) return; + std::vector pages((end - begin) / _page, Written | Fresh); + std::vector queried(pages.size(), false); + const Lock lock(_lock); + for (auto cursor = begin; cursor < end;) { + mach_vm_address_t address = cursor; + 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 || address >= end) break; + const auto first = std::max(cursor, address); + const auto last = std::min(end, address + size); + const std::uint8_t protection = ((info.protection & VM_PROT_READ) ? PROT_READ : 0) | ((info.protection & VM_PROT_WRITE) ? PROT_WRITE : 0) | ((info.protection & VM_PROT_EXECUTE) ? PROT_EXEC : 0); + for (auto page = first; page < last; page += _page) { + pages[(page - begin) / _page] = Written | Fresh | protection; + queried[(page - begin) / _page] = true; + } + cursor = last; + } + forEachPage(begin, end, [&](std::uintptr_t page, std::uint8_t& state) { + auto& entry = pages[(page - begin) / _page]; + const std::uint8_t recorded = state & ProtectionMask; + const bool armed = (state & Protected) != 0 && (entry & ProtectionMask) == (recorded & ~PROT_WRITE); + if (armed || !queried[(page - begin) / _page]) entry = static_cast(Written | Fresh | recorded); + else state = static_cast(state & ~Protected); + }); + erase(begin, end); + _ranges.emplace(begin, Range{end, std::move(pages)}); + } + + bool Unregister(std::uintptr_t begin, std::uintptr_t end) { + if (!_available || end <= begin) return false; + begin = pageStart(begin); + end = pageEnd(end); + if (end <= begin) return false; + const Lock lock(_lock); + return erase(begin, end); + } + + bool Covers(std::uintptr_t begin, std::uintptr_t end) { + if (!_available) return false; + const Lock lock(_lock); + 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) { + if (!_available) return false; + begin = pageStart(begin); + end = pageEnd(end); + if (end <= begin) return true; + std::vector> runs; + { + const Lock lock(_lock); + if (take != Take::Fresh && !covers(begin, end)) return false; + forEachPage(begin, end, [&](std::uintptr_t page, std::uint8_t& state) { + if ((state & Written) == 0) return; + const bool fresh = (state & Fresh) != 0; + if ((take == Take::Armed && fresh) || (take == Take::Fresh && !fresh)) return; + state &= static_cast(~Fresh); + if (!runs.empty() && runs.back().second == page) runs.back().second = page + _page; + else runs.emplace_back(page, page + _page); + for (const auto& [first, last] : _hostWrites) + if (page >= first && page < last) return; + state &= static_cast(~Written); + if ((state & PROT_WRITE) != 0) state |= Protected; + }); + for (const auto& [first, last] : runs) protect(first, last); + } + for (const auto& [first, last] : runs) written(context, first, last); + return true; + } + + void ProtectionChanged(std::uintptr_t begin, std::uintptr_t end, int protection) { + if (!_available || end <= begin) return; + begin = pageStart(begin); + end = pageEnd(end); + 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)); + }); + } + + void BeginHostWrite(std::uintptr_t begin, std::uintptr_t end) { + if (!_available || end <= begin) return; + begin = pageStart(begin); + end = pageEnd(end); + if (end <= begin) return; + const Lock lock(_lock); + _hostWrites.emplace_back(begin, end); + forEachPage(begin, end, [&](std::uintptr_t page, std::uint8_t& state) { + if ((state & Protected) != 0) ::mprotect(reinterpret_cast(page), _page, state & ProtectionMask); + state = static_cast((state & (ProtectionMask | Fresh)) | Written); + }); + } + + void EndHostWrite(std::uintptr_t begin, std::uintptr_t end) { + if (!_available || end <= begin) return; + begin = pageStart(begin); + end = pageEnd(end); + 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); + } + + int Protection(std::uintptr_t address, std::uintptr_t limit, std::uintptr_t* runEnd) { + const auto page = pageStart(address); + const Lock lock(_lock); + auto it = _ranges.upper_bound(page); + if (_available && it != _ranges.begin() && page < std::prev(it)->second.end) { + --it; + const auto& pages = it->second.pages; + const std::uint8_t protection = pages[(page - it->first) / _page] & ProtectionMask; + auto end = page + _page; + while (end < it->second.end && end < limit && (pages[(end - it->first) / _page] & ProtectionMask) == protection) end += _page; + *runEnd = std::min(end, limit); + return protection; + } + *runEnd = it != _ranges.end() ? std::min(it->first, limit) : limit; + return -1; + } + +private: + static constexpr std::uint8_t ProtectionMask = PROT_READ | PROT_WRITE | PROT_EXEC; + static constexpr std::uint8_t Written = 0x10; + static constexpr std::uint8_t Protected = 0x20; + static constexpr std::uint8_t Fresh = 0x40; + + struct Range { + std::uintptr_t end; + std::vector pages; + }; + + 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(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() { + pthread_mutex_unlock(&_mutex.mutex); + } + 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() { + if (std::getenv("APS5_NO_WRITE_WATCH") != nullptr) return; + struct sigaction action {}; + action.sa_sigaction = &Watch::fault; + action.sa_flags = SA_SIGINFO | SA_RESTART; + sigemptyset(&action.sa_mask); + if (sigaction(SIGBUS, &action, &_previousBus) != 0 || sigaction(SIGSEGV, &action, &_previousSegv) != 0) { + std::fprintf(stderr, "[memory] write watch: cannot install the fault handler (%s); guest buffers are fully re-read every use\n", std::strerror(errno)); + return; + } + _available = true; + } + + static void fault(int signal, siginfo_t* info, void* context) { + auto& watch = Get(); + if (watch.record(reinterpret_cast(info->si_addr))) return; + const auto& previous = signal == SIGBUS ? watch._previousBus : watch._previousSegv; + if ((previous.sa_flags & SA_SIGINFO) != 0 && previous.sa_sigaction != nullptr) { + previous.sa_sigaction(signal, info, context); + return; + } + if (previous.sa_handler != SIG_DFL && previous.sa_handler != SIG_IGN && previous.sa_handler != nullptr) { + previous.sa_handler(signal); + return; + } + ::signal(signal, SIG_DFL); + } + + bool record(std::uintptr_t address) { + const auto page = pageStart(address); + const FaultLock lock(_lock); + auto it = _ranges.upper_bound(page); + if (it == _ranges.begin()) return false; + --it; + if (page >= it->second.end) return false; + auto& state = it->second.pages[(page - it->first) / _page]; + if ((state & Protected) != 0) { + state = static_cast((state & (ProtectionMask | Fresh)) | Written); + ::mprotect(reinterpret_cast(page), _page, state & ProtectionMask); + return true; + } + return (state & PROT_WRITE) != 0; + } + + std::uintptr_t pageStart(std::uintptr_t address) const { + return address & ~(_page - 1); + } + + std::uintptr_t pageEnd(std::uintptr_t address) const { + return (address + _page - 1) & ~(_page - 1); + } + + template + void forEachPage(std::uintptr_t begin, std::uintptr_t end, TFunction function) { + auto it = _ranges.upper_bound(begin); + if (it != _ranges.begin()) --it; + for (; it != _ranges.end() && it->first < end; ++it) { + const auto first = std::max(begin, it->first); + const auto last = std::min(end, it->second.end); + for (auto page = first; page < last; page += _page) function(page, it->second.pages[(page - it->first) / _page]); + } + } + + void protect(std::uintptr_t begin, std::uintptr_t end) { + std::uintptr_t runStart = 0; + std::uintptr_t previous = begin; + int runProtection = -1; + const auto flush = [&](std::uintptr_t runEnd) { + if (runProtection >= 0) ::mprotect(reinterpret_cast(runStart), runEnd - runStart, runProtection); + runProtection = -1; + }; + forEachPage(begin, end, [&](std::uintptr_t page, std::uint8_t& state) { + const int wanted = (state & Protected) != 0 ? (state & ProtectionMask & ~PROT_WRITE) : -1; + if (wanted != runProtection || page != previous) { + flush(previous); + runStart = page; + runProtection = wanted; + } + previous = page + _page; + }); + flush(previous); + } + + bool covers(std::uintptr_t begin, std::uintptr_t end) const { + auto it = _ranges.upper_bound(begin); + if (it == _ranges.begin()) return false; + --it; + auto cursor = begin; + for (; it != _ranges.end() && it->first <= cursor; ++it) { + if (it->second.end > cursor) cursor = it->second.end; + if (cursor >= end) return true; + } + return false; + } + + bool erase(std::uintptr_t begin, std::uintptr_t end) { + bool removed = false; + auto it = _ranges.upper_bound(begin); + if (it != _ranges.begin()) --it; + while (it != _ranges.end() && it->first < end) { + const auto rangeBegin = it->first; + const auto rangeEnd = it->second.end; + if (rangeEnd <= begin) { + ++it; + continue; + } + removed = true; + auto pages = std::move(it->second.pages); + it = _ranges.erase(it); + const auto cutBegin = std::max(begin, rangeBegin); + const auto cutEnd = std::min(end, rangeEnd); + for (auto page = cutBegin; page < cutEnd; page += _page) { + const auto state = pages[(page - rangeBegin) / _page]; + if ((state & Protected) != 0) ::mprotect(reinterpret_cast(page), _page, state & ProtectionMask); + } + if (rangeBegin < cutBegin) _ranges.emplace(rangeBegin, Range{cutBegin, std::vector(pages.begin(), pages.begin() + (cutBegin - rangeBegin) / _page)}); + if (cutEnd < rangeEnd) it = _ranges.emplace(cutEnd, Range{rangeEnd, std::vector(pages.begin() + (cutEnd - rangeBegin) / _page, pages.end())}).first; + } + return removed; + } + + const std::uintptr_t _page = static_cast(::getpagesize()); + bool _available = false; + Mutex _lock; + std::map _ranges; + std::vector> _hostWrites; + struct sigaction _previousBus {}; + struct sigaction _previousSegv {}; +}; + const bool g_opened = (Watch::Get(), true); #endif } bool GuestWriteWatchAvailable_nid_postfix() { -#if defined(__linux__) && defined(PAGEMAP_SCAN) && defined(UFFD_FEATURE_WP_ASYNC) +#ifdef APS5_GUEST_WRITE_WATCH return Watch::Get().Available(); #else return false; @@ -263,7 +665,7 @@ bool GuestWriteWatchAvailable_nid_postfix() { } void GuestWriteWatchRegister_nid_postfix(const void* pointer, std::size_t bytes) { -#if defined(__linux__) && defined(PAGEMAP_SCAN) && defined(UFFD_FEATURE_WP_ASYNC) +#ifdef APS5_GUEST_WRITE_WATCH const auto begin = reinterpret_cast(pointer); Watch::Get().Register(begin, begin + bytes); #else @@ -273,7 +675,7 @@ void GuestWriteWatchRegister_nid_postfix(const void* pointer, std::size_t bytes) } bool GuestWriteWatchUnregister_nid_postfix(const void* pointer, std::size_t bytes) { -#if defined(__linux__) && defined(PAGEMAP_SCAN) && defined(UFFD_FEATURE_WP_ASYNC) +#ifdef APS5_GUEST_WRITE_WATCH const auto begin = reinterpret_cast(pointer); return Watch::Get().Unregister(begin, begin + bytes); #else @@ -284,7 +686,7 @@ bool GuestWriteWatchUnregister_nid_postfix(const void* pointer, std::size_t byte } bool GuestWriteWatchCovers_nid_postfix(std::uintptr_t address, std::size_t bytes) { -#if defined(__linux__) && defined(PAGEMAP_SCAN) && defined(UFFD_FEATURE_WP_ASYNC) +#ifdef APS5_GUEST_WRITE_WATCH return bytes != 0 && address + bytes > address && Watch::Get().Covers(address, address + bytes); #else static_cast(address); @@ -293,8 +695,18 @@ 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) { -#if defined(__linux__) && defined(PAGEMAP_SCAN) && defined(UFFD_FEATURE_WP_ASYNC) +#ifdef APS5_GUEST_WRITE_WATCH if (bytes == 0 || address + bytes < address) return false; return Watch::Get().Collect(address, address + bytes, written, context); #else @@ -306,4 +718,77 @@ bool GuestWriteWatchCollect_nid_postfix(std::uintptr_t address, std::size_t byte #endif } +void GuestWriteWatchBeginHostWrite_nid_postfix(const void* pointer, std::size_t bytes) { +#if defined(__APPLE__) + const auto begin = reinterpret_cast(pointer); + Watch::Get().BeginHostWrite(begin, begin + bytes); +#else + static_cast(pointer); + static_cast(bytes); +#endif +} + +void GuestWriteWatchEndHostWrite_nid_postfix(const void* pointer, std::size_t bytes) { +#if defined(__APPLE__) + const auto begin = reinterpret_cast(pointer); + Watch::Get().EndHostWrite(begin, begin + bytes); +#else + static_cast(pointer); + static_cast(bytes); +#endif +} + +void GuestWriteWatchProtectionChanged_nid_postfix(const void* pointer, std::size_t bytes, int protection) { +#if defined(__APPLE__) + const auto begin = reinterpret_cast(pointer); + Watch::Get().ProtectionChanged(begin, begin + bytes, protection); +#else + static_cast(pointer); + static_cast(bytes); + static_cast(protection); +#endif +} + +int GuestWriteWatchProtection_nid_postfix(std::uintptr_t address, std::uintptr_t limit, std::uintptr_t* runEnd) { +#if defined(__APPLE__) + return Watch::Get().Protection(address, limit, runEnd); +#else + static_cast(address); + *runEnd = limit; + return -1; +#endif +} + +bool GuestWriteWatchCollectFresh_nid_postfix(std::uintptr_t address, std::size_t bytes, void (*written)(void* context, std::uintptr_t begin, std::uintptr_t end), void* context) { +#if defined(__linux__) && defined(PAGEMAP_SCAN) && defined(UFFD_FEATURE_WP_ASYNC) + if (bytes == 0 || address + bytes < address) return false; + return Watch::Get().CollectFresh(address, address + bytes, written, context); +#elif defined(__APPLE__) + if (bytes == 0 || address + bytes < address) return false; + return Watch::Get().Collect(address, address + bytes, written, context, Watch::Take::Fresh); +#else + static_cast(address); + static_cast(bytes); + static_cast(written); + static_cast(context); + return false; +#endif +} + +bool GuestWriteWatchCollectArmed_nid_postfix(std::uintptr_t address, std::size_t bytes, void (*written)(void* context, std::uintptr_t begin, std::uintptr_t end), void* context) { +#if defined(__linux__) && defined(PAGEMAP_SCAN) && defined(UFFD_FEATURE_WP_ASYNC) + if (bytes == 0 || address + bytes < address) return false; + return Watch::Get().CollectArmed(address, address + bytes, written, context); +#elif defined(__APPLE__) + if (bytes == 0 || address + bytes < address) return false; + return Watch::Get().Collect(address, address + bytes, written, context, Watch::Take::Armed); +#else + static_cast(address); + static_cast(bytes); + static_cast(written); + static_cast(context); + return false; +#endif +} + } diff --git a/core/libs/prx/libc/src/HeapDiagnostics.cpp b/core/libs/prx/libc/src/HeapDiagnostics.cpp index 39aaf39511..04324b8d39 100644 --- a/core/libs/prx/libc/src/HeapDiagnostics.cpp +++ b/core/libs/prx/libc/src/HeapDiagnostics.cpp @@ -3,12 +3,17 @@ #include #include #include +#include #include 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); @@ -28,18 +33,14 @@ void LibcHeapTraceInfo_nid_no_patch(LibcHeapInfo* info) { info->mstate_table = mstateTable.data(); } -// unknown signature void APS5_VABI sceLibcInternalBacktraceForGame_nid_postfix(const char* heapName) { - (void)heapName; - NotImplemented_nid_no_patch(__func__); + std::fprintf(stderr, "[%s] backtrace requested from guest 0x%llx\n", heapName ? heapName : "", + static_cast(reinterpret_cast(__builtin_return_address(0)))); } -// unknown signature void APS5_VABI sceLibcInternalHeapErrorReportForGame_nid_postfix(void* heap, void* block, std::uint32_t error) { - (void)heap; - (void)block; - (void)error; - NotImplemented_nid_no_patch(__func__); + std::fprintf(stderr, "[libc] heap error 0x%x: heap %p, block %p, reported from guest 0x%llx\n", error, heap, block, + static_cast(reinterpret_cast(__builtin_return_address(0)))); } } diff --git a/core/libs/prx/libc/src/HostEntropy.cpp b/core/libs/prx/libc/src/HostEntropy.cpp new file mode 100644 index 0000000000..d87362a221 --- /dev/null +++ b/core/libs/prx/libc/src/HostEntropy.cpp @@ -0,0 +1,33 @@ +#ifdef _WIN32 +#define _CRT_RAND_S +#endif +#include "prx/libc/include/HostEntropy.hpp" + +#include +#include +#include +#include +#include +#ifdef __APPLE__ +#include +#elif !defined(_WIN32) +#include +#endif + +void HostEntropy_nid_no_patch(void* buffer, std::size_t size) { + auto* bytes = static_cast(buffer); +#ifdef _WIN32 + for (std::size_t offset = 0; offset < size; offset += sizeof(unsigned int)) { + unsigned int value = 0; + if (const errno_t error = rand_s(&value); error != 0) + throw std::system_error(error, std::generic_category(), "HostEntropy: rand_s failed"); + std::memcpy(bytes + offset, &value, std::min(sizeof(value), size - offset)); + } +#else + constexpr std::size_t MaxChunk = 256; + for (std::size_t offset = 0; offset < size; offset += MaxChunk) { + if (getentropy(bytes + offset, std::min(MaxChunk, size - offset)) != 0) + throw std::system_error(errno, std::generic_category(), "HostEntropy: getentropy failed"); + } +#endif +} 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/Hostname.cpp b/core/libs/prx/libc/src/Hostname.cpp new file mode 100644 index 0000000000..7b205e3659 --- /dev/null +++ b/core/libs/prx/libc/src/Hostname.cpp @@ -0,0 +1,16 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +extern "C" int APS5_VABI gethostname_nid_postfix(char* name, std::size_t length) { + constexpr char hostname[] = "AnyPS5"; + if (name == nullptr) return 0; + const int savedError = errno; + const auto count = std::min(length, sizeof(hostname)); + if (count != 0) std::memcpy(name, hostname, count); + if (length < sizeof(hostname)) { errno = 63; return -1; } + errno = savedError; + return 0; +} 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 new file mode 100644 index 0000000000..8e39cfa29b --- /dev/null +++ b/core/libs/prx/libc/src/Interfaces.cpp @@ -0,0 +1,379 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#include +#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 + +extern "C" int* APS5_VABI __error_nid_postfix(); + +struct GuestInterfaceAddress { + GuestInterfaceAddress* next; + char* name; + std::uint32_t flags; + void* address; + void* netmask; + void* destination; + void* data; +}; +static_assert(sizeof(GuestInterfaceAddress) == 56); +static_assert(offsetof(GuestInterfaceAddress, flags) == 16); +static_assert(offsetof(GuestInterfaceAddress, address) == 24); +static_assert(offsetof(GuestInterfaceAddress, destination) == 40); +static_assert(offsetof(GuestInterfaceAddress, data) == 48); + +namespace { +constexpr std::uint32_t guestUp = 0x1; +constexpr std::uint32_t guestBroadcast = 0x2; +constexpr std::uint32_t guestLoopback = 0x8; +constexpr std::uint32_t guestPointToPoint = 0x10; +constexpr std::uint32_t guestRunning = 0x40; +constexpr std::uint32_t guestMulticast = 0x8000; +constexpr std::size_t guestNameCapacity = 16; + +struct Entry { + std::string name; + std::uint32_t flags; + std::vector address; + std::vector netmask; + std::vector destination; + std::vector data; +}; + +std::vector GuestIpv4(const std::uint8_t* bytes) { + std::vector guest(16, 0); + guest[0] = 16; + guest[1] = 2; + std::memcpy(guest.data() + 4, bytes, 4); + return guest; +} + +std::vector GuestIpv6(const std::uint8_t* bytes, std::uint32_t scope) { + std::vector guest(28, 0); + guest[0] = 28; + guest[1] = 28; + std::memcpy(guest.data() + 8, bytes, 16); + std::memcpy(guest.data() + 24, &scope, 4); + return guest; +} + +std::vector GuestAddress(const sockaddr* address) { + if (address->sa_family == AF_INET) + return GuestIpv4(reinterpret_cast(&reinterpret_cast(address)->sin_addr)); + const auto* v6 = reinterpret_cast(address); + return GuestIpv6(reinterpret_cast(&v6->sin6_addr), v6->sin6_scope_id); +} + +std::string CheckedName(const char* name) { + const std::string checked(name); + if (checked.empty() || checked.size() >= guestNameCapacity) + throw std::runtime_error("getifaddrs: host interface name does not fit the guest's 16-byte limit: " + checked); + return checked; +} + +std::size_t Aligned(std::size_t size) { + return (size + 7) & ~static_cast(7); +} + +std::vector BroadcastAddress(const std::vector& address, const std::vector& netmask) { + std::vector broadcast = address; + for (std::size_t index = 4; index < 8; ++index) + broadcast[index] = static_cast(address[index] | static_cast(~netmask[index])); + return broadcast; +} + +#ifdef _WIN32 +std::vector PrefixMask(int family, unsigned prefix) { + const unsigned bits = family == AF_INET ? 32 : 128; + if (prefix > bits) prefix = bits; + std::uint8_t mask[16] = {}; + for (unsigned bit = 0; bit < prefix; ++bit) mask[bit / 8] |= static_cast(0x80 >> (bit % 8)); + return family == AF_INET ? GuestIpv4(mask) : GuestIpv6(mask, 0); +} + +std::uint32_t InterfaceFlags(const IP_ADAPTER_ADDRESSES* adapter) { + std::uint32_t flags = 0; + if (adapter->OperStatus == IfOperStatusUp) flags |= guestUp | guestRunning; + if (adapter->IfType == IF_TYPE_SOFTWARE_LOOPBACK) flags |= guestLoopback; + else if (adapter->IfType != IF_TYPE_PPP && adapter->IfType != IF_TYPE_TUNNEL) flags |= guestBroadcast; + if (!(adapter->Flags & IP_ADAPTER_NO_MULTICAST)) flags |= guestMulticast; + return flags; +} + +std::string InterfaceName(const IP_ADAPTER_ADDRESSES* adapter) { + char name[NDIS_IF_MAX_STRING_SIZE + 1] = {}; + const auto status = ConvertInterfaceLuidToNameA(&adapter->Luid, name, sizeof(name)); + if (status != NO_ERROR) throw std::runtime_error("getifaddrs: ConvertInterfaceLuidToNameA failed with " + std::to_string(status)); + if (std::strlen(name) < guestNameCapacity) return CheckedName(name); + return "if" + std::to_string(adapter->IfIndex ? adapter->IfIndex : adapter->Ipv6IfIndex); +} + +int HostEntries(std::vector& entries) { + const ULONG options = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER; + ULONG size = 0; + std::vector storage; + ULONG result = GetAdaptersAddresses(AF_UNSPEC, options, nullptr, nullptr, &size); + for (unsigned calls = 1; result == ERROR_BUFFER_OVERFLOW && calls < 4; ++calls) { + storage.resize(size); + result = GetAdaptersAddresses(AF_UNSPEC, options, nullptr, reinterpret_cast(storage.data()), &size); + } + if (result == ERROR_NO_DATA) return 0; + if (result == ERROR_BUFFER_OVERFLOW) { *__error_nid_postfix() = 55; return -1; } + if (result == ERROR_NOT_ENOUGH_MEMORY) { *__error_nid_postfix() = 12; return -1; } + if (result != NO_ERROR) throw std::runtime_error("getifaddrs: GetAdaptersAddresses failed with " + std::to_string(result)); + for (auto* adapter = reinterpret_cast(storage.data()); adapter; adapter = adapter->Next) { + std::string name; + for (auto* unicast = adapter->FirstUnicastAddress; unicast; unicast = unicast->Next) { + const sockaddr* address = unicast->Address.lpSockaddr; + if (!address || (address->sa_family != AF_INET && address->sa_family != AF_INET6)) continue; + if (name.empty()) name = InterfaceName(adapter); + Entry entry{name, InterfaceFlags(adapter), GuestAddress(address), PrefixMask(address->sa_family, unicast->OnLinkPrefixLength), {}}; + if (address->sa_family == AF_INET && (entry.flags & guestBroadcast)) + entry.destination = BroadcastAddress(entry.address, entry.netmask); + entries.push_back(std::move(entry)); + } + } + 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}, + {IFF_POINTOPOINT, guestPointToPoint}, {IFF_RUNNING, guestRunning}, {IFF_NOARP, 0x80}, + {IFF_PROMISC, 0x100}, {IFF_ALLMULTI, 0x200}, {IFF_MULTICAST, guestMulticast}, + }; + std::uint32_t guest = 0; + for (const auto& [hostFlag, guestFlag] : flags) + if (host & hostFlag) guest |= guestFlag; + return guest; +} + +int HostEntries(std::vector& entries) { + ifaddrs* host = nullptr; + if (::getifaddrs(&host) != 0) { + const int hostError = errno; + switch (hostError) { + case ENOMEM: *__error_nid_postfix() = 12; return -1; + case ENOBUFS: *__error_nid_postfix() = 55; return -1; + case EMFILE: *__error_nid_postfix() = 24; return -1; + case ENFILE: *__error_nid_postfix() = 23; return -1; + 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) 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; + if (item->ifa_flags & IFF_POINTOPOINT) { + if (item->ifa_dstaddr && item->ifa_dstaddr->sa_family == family) entry.destination = GuestAddress(item->ifa_dstaddr); + else entry.flags &= ~guestPointToPoint; + } else if (family == AF_INET && (item->ifa_flags & IFF_BROADCAST)) { + entry.destination = item->ifa_broadaddr && item->ifa_broadaddr->sa_family == AF_INET + ? GuestAddress(item->ifa_broadaddr) : BroadcastAddress(entry.address, entry.netmask); + } + 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 +} + +extern "C" { + +int APS5_VABI getifaddrs_nid_postfix(GuestInterfaceAddress** list) { + if (!list) { *__error_nid_postfix() = 14; return -1; } + try { + std::vector entries; + if (HostEntries(entries) != 0) return -1; + if (entries.empty()) { + *list = nullptr; + return 0; + } + 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()) + Aligned(entry.data.size()); + auto* block = static_cast(ApplicationHeapAllocate_nid_no_patch(size)); + if (!block) { + *__error_nid_postfix() = 12; + return -1; + } + auto* items = reinterpret_cast(block); + std::size_t offset = entries.size() * sizeof(GuestInterfaceAddress); + const auto place = [&](const void* source, std::size_t length) { + void* target = block + offset; + std::memcpy(target, source, length); + offset += Aligned(length); + return target; + }; + for (std::size_t index = 0; index < entries.size(); ++index) { + const Entry& entry = entries[index]; + GuestInterfaceAddress& item = items[index]; + item.next = index + 1 < entries.size() ? &items[index + 1] : nullptr; + 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 = 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 = entry.data.empty() ? nullptr : place(entry.data.data(), entry.data.size()); + } + *list = items; + return 0; + } catch (const std::bad_alloc&) { *__error_nid_postfix() = 12; return -1; } +} + +void APS5_VABI freeifaddrs_nid_postfix(GuestInterfaceAddress* list) { + ApplicationHeapFree_nid_no_patch(list); +} + +} diff --git a/core/libs/prx/libc/src/JumpBuffer.cpp b/core/libs/prx/libc/src/JumpBuffer.cpp index 5a1d2fa6c7..77dca484e4 100644 --- a/core/libs/prx/libc/src/JumpBuffer.cpp +++ b/core/libs/prx/libc/src/JumpBuffer.cpp @@ -1,13 +1,9 @@ #include "prx/libc/include/General.hpp" +#include "prx/libc/include/general/AsmMacros.hpp" // setjmp/longjmp must capture the guest's own frame, so they are written directly in assembly with // the guest (System V) calling convention. The saved state fits the guest's 96-byte jmp_buf: // return address, rbx, rsp, rbp, r12-r15, MXCSR and the x87 control word. -#ifdef _WIN32 -#define APS5_ASM_FUNCTION(name) ".globl " name "\n.def " name "; .scl 2; .type 32; .endef\n" name ":\n" -#else -#define APS5_ASM_FUNCTION(name) ".globl " name "\n.type " name ", @function\n" name ":\n" -#endif asm(".text\n" APS5_ASM_FUNCTION("setjmp_nid_postfix") @@ -26,6 +22,15 @@ asm(".text\n" " xor %eax, %eax\n" " ret\n" APS5_ASM_FUNCTION("longjmp_nid_postfix") +#ifdef _WIN32 + " mov %rdi, %rbx\n" + " mov %esi, %r12d\n" + " mov 16(%rdi), %rdi\n" + " and $-16, %rsp\n" + " call GuestStackSwitch_nid_no_patch\n" + " mov %rbx, %rdi\n" + " mov %r12d, %esi\n" +#endif " mov %esi, %eax\n" " test %eax, %eax\n" " jnz 1f\n" diff --git a/core/libs/prx/libc/src/LocaleSupport.cpp b/core/libs/prx/libc/src/LocaleSupport.cpp index 941df32b3b..a4388c8dfe 100644 --- a/core/libs/prx/libc/src/LocaleSupport.cpp +++ b/core/libs/prx/libc/src/LocaleSupport.cpp @@ -1,12 +1,14 @@ #include +#include #include #include #include #include -#include +#include #include #include #include +#include #include #include #include @@ -17,6 +19,7 @@ #include #include "prx/libc/include/General.hpp" +#include "prx/libc/include/SonyCrt.hpp" #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/GuestLocale.hpp" #include "SceTypes.hpp" @@ -32,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"); } @@ -79,12 +87,24 @@ GuestLocale::Facet* g_classicFacets[1]{}; GuestLocale::Implementation g_classicLocale{{&g_localeVtable, 1, 0}, g_classicFacets, 1, 0, false, "C"}; constexpr std::size_t GuestStringInlineCapacity = 15; +constexpr std::size_t GuestStringCapacityMask = 15; constexpr std::size_t GuestStringLargeAllocation = 0x1000; +constexpr std::size_t GuestLargeAllocationPadding = 0x27; +constexpr std::uintptr_t GuestLargeAllocationAlignment = 0x20; void ValidateCharacterRange(const char* first, const char* last, const char* function) { if ((first == nullptr) != (last == nullptr) || last < first) throw std::invalid_argument(std::string(function) + ": invalid character range"); } +void* AllocateStringStorage(std::size_t bytes) { + if (bytes < GuestStringLargeAllocation) return _Znwm_nid_postfix(bytes); + if (bytes > std::numeric_limits::max() - GuestLargeAllocationPadding) throw std::bad_alloc(); + auto* raw = static_cast(_Znwm_nid_postfix(bytes + GuestLargeAllocationPadding)); + const auto aligned = (reinterpret_cast(raw) + GuestLargeAllocationPadding) & ~(GuestLargeAllocationAlignment - 1); + reinterpret_cast(aligned)[-1] = raw; + return reinterpret_cast(aligned); +} + void APS5_VABI DestroyCollate(GuestLocale::Facet*) {} void APS5_VABI DeleteCollate(GuestLocale::Facet* self) { @@ -119,12 +139,11 @@ GuestLocale::String* APS5_VABI CollateTransform(GuestLocale::String* result, con ValidateCharacterRange(first, last, "collate::do_transform"); const auto length = static_cast(last - first); if (length != 0 && std::memchr(first, 0, length) != nullptr) throw std::runtime_error("collate::do_transform: embedded null characters are not supported"); - if (length + 1 >= GuestStringLargeAllocation) throw std::runtime_error("collate::do_transform: results of 4095 characters or more are not supported"); char* data = result->buffer; std::size_t capacity = GuestStringInlineCapacity; if (length > GuestStringInlineCapacity) { - capacity = length; - data = static_cast(_Znwm_nid_postfix(capacity + 1)); + capacity = length | GuestStringCapacityMask; + data = static_cast(AllocateStringStorage(capacity + 1)); } if (length != 0) std::memcpy(data, first, length); data[length] = 0; @@ -143,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) { @@ -230,11 +307,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); @@ -257,8 +366,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) { @@ -321,8 +447,15 @@ mbstate_t* APS5_VABI _Getpwcstate_nid_postfix() { return &state; } -wint_t APS5_VABI _Towctrans_nid_postfix(wint_t c, wctrans_t desc) { - return std::towctrans(c, desc); +std::uint32_t APS5_VABI _Towctrans_nid_postfix(std::uint32_t c, std::uint64_t desc) { + constexpr std::uint64_t ToLower = 1; + constexpr std::uint64_t ToUpper = 2; + if (desc != ToLower && desc != ToUpper) { + errno = 22; + return c; + } + if (c > UCHAR_MAX) return c; + return static_cast((desc == ToLower ? g_lowerTable : g_upperTable)[c + 1]); } // Locale ids and stream objects the title imports besides the ones above; the streams are zeroed @@ -380,4 +513,8 @@ void APS5_VABI init_env_nid_postfix(const InitEnvParams* params) { _init_env_nid_postfix(); } +SonyLconv* APS5_VABI localeconv_nid_postfix(void) { + return SonyLocaleconv(); +} + } diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 2828d8ed42..bfed7a8a4d 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -1,12 +1,268 @@ #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" + +namespace { + +template +std::int64_t RoundToInt64(TFloat value) { + const TFloat rounded = std::round(value); + if (!(rounded >= static_cast(INT64_MIN) && rounded < -static_cast(INT64_MIN))) { + std::feraiseexcept(FE_INVALID); + return INT64_MIN; + } + return static_cast(rounded); +} + +float SinhfExpm1(float x) { + constexpr float Ln2High = 6.9313812256e-01f, Ln2Low = 9.0580006145e-06f, InverseLn2 = 1.4426950216e+00f; + constexpr float Q1 = -3.3333212137e-2f, Q2 = 1.5807170421e-3f; + const std::uint32_t magnitude = std::bit_cast(x); + int k = 0; + float c = 0.0f; + if (magnitude > 0x3eb17218u) { + float high; + float low; + if (magnitude < 0x3f851592u) { + high = x - Ln2High; + low = Ln2Low; + k = 1; + } else { + k = static_cast(InverseLn2 * x + 0.5f); + const float t = static_cast(k); + high = x - t * Ln2High; + low = t * Ln2Low; + } + x = high - low; + c = (high - x) - low; + } + const float halfX = 0.5f * x; + const float halfSquare = x * halfX; + const float r1 = 1.0f + halfSquare * (Q1 + halfSquare * Q2); + const float t = 3.0f - r1 * halfX; + float e = halfSquare * ((r1 - t) / (6.0f - x * t)); + if (k == 0) return x - (x * e - halfSquare); + e = x * (e - c) - c; + e -= halfSquare; + if (k == 1) { + if (x < -0.25f) return -2.0f * (e - (x + 0.5f)); + return 1.0f + 2.0f * (x - e); + } + const float twoPowK = std::bit_cast(static_cast(0x7f + k) << 23); + const float oneMinusTwoPowMinusK = std::bit_cast(0x3f800000u - (0x1000000u >> k)); + return (oneMinusTwoPowMinusK - (e - x)) * twoPowK; +} + +float SinhfExp(float x) { + constexpr float Ln2High = 6.9314575195e-01f, Ln2Low = 1.4286067653e-06f, InverseLn2 = 1.4426950216e+00f; + constexpr float P1 = 1.6666625440e-1f, P2 = -2.7667332906e-3f; + const int k = static_cast(InverseLn2 * x + 0.5f); + const float kFloat = static_cast(k); + const float high = x - kFloat * Ln2High; + const float low = kFloat * Ln2Low; + x = high - low; + const float t = x * x; + const float c = x - t * (P1 + t * P2); + const float y = 1.0f - ((low - (x * c) / (2.0f - c)) - high); + if (k == 128) return y * 2.0f * 0x1p127f; + return y * std::bit_cast(static_cast(0x7f + k) << 23); +} + +float AtanhfLog1p(float x) { + constexpr float Ln2High = 6.9313812256e-01f, Ln2Low = 9.0580006145e-06f; + constexpr float Lp1 = 6.6666668653e-01f, Lp2 = 4.0000000596e-01f, Lp3 = 2.8571429849e-01f, Lp4 = 2.2222198546e-01f; + constexpr float Lp5 = 1.8183572590e-01f, Lp6 = 1.5313838422e-01f, Lp7 = 1.4798198640e-01f; + const std::uint32_t bits = std::bit_cast(x); + if (bits < 0x38000000u) return bits < 0x33800000u ? x : x - x * x * 0.5f; + int k = 0; + float f = x; + float c = 0.0f; + std::uint32_t mantissa = 1; + if (bits >= 0x3ed413d0u) { + const float u = 1.0f + x; + const std::uint32_t uBits = std::bit_cast(u); + k = static_cast(uBits >> 23) - 127; + c = (k > 0 ? 1.0f - (u - x) : x - (u - 1.0f)) / u; + mantissa = uBits & 0x007fffffu; + if (mantissa < 0x3504f4u) { + f = std::bit_cast(mantissa | 0x3f800000u) - 1.0f; + } else { + ++k; + f = std::bit_cast(mantissa | 0x3f000000u) - 1.0f; + mantissa = (0x00800000u - mantissa) >> 2; + } + } + const float halfSquare = 0.5f * f * f; + const float kFloat = static_cast(k); + if (mantissa == 0) { + if (f == 0.0f) return kFloat * Ln2High + (c + kFloat * Ln2Low); + const float r = halfSquare * (1.0f - 0.66666666666666666f * f); + return kFloat * Ln2High - ((r - (kFloat * Ln2Low + c)) - f); + } + const float s = f / (2.0f + f); + const float z = s * s; + const float r = z * (Lp1 + z * (Lp2 + z * (Lp3 + z * (Lp4 + z * (Lp5 + z * (Lp6 + z * Lp7)))))); + if (k == 0) return f - (halfSquare - s * (halfSquare + r)); + return kFloat * Ln2High - ((halfSquare - (s * (halfSquare + r) + (kFloat * Ln2Low + c))) - f); +} + +static_assert(std::numeric_limits::digits == 64, "guest long double is the x87 80-bit format"); + +struct ExtendedParts { + std::uint64_t mantissa; + 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)); + ExtendedParts parts; + std::memcpy(&parts.mantissa, bytes, sizeof(parts.mantissa)); + std::memcpy(&parts.signExponent, bytes + sizeof(parts.mantissa), sizeof(parts.signExponent)); + return parts; +} + +long double ClearLowMantissaWord(long double value) { + unsigned char bytes[sizeof(long double)]; + std::memcpy(bytes, &value, sizeof(value)); + std::memset(bytes, 0, sizeof(std::uint32_t)); + std::memcpy(&value, bytes, sizeof(value)); + return value; +} + +constexpr long double AsinNumeratorCoefficients[] = { + 1.66666666666666666631e-01L, -4.16313987993683104320e-01L, 3.69068046323246813704e-01L, -1.36213932016738603108e-01L, + 1.78324189708471965733e-02L, -2.19216428382605211588e-04L, -7.10526623669075243183e-06L, +}; +constexpr long double AsinDenominatorCoefficients[] = { + -2.94788392796209867269e+00L, 3.27309890266528636716e+00L, -1.68285799854822427013e+00L, 3.90699412641738801874e-01L, + -3.14365703596053263322e-02L, +}; +constexpr long double HalfPiHigh = 1.57079632679489661926e+00L; +constexpr long double HalfPiLow = -2.50827880633416601173e-20L; +constexpr long double QuarterPiHigh = 7.85398163397448309628e-01L; + +long double AsinNumerator(long double t) { + const auto& p = AsinNumeratorCoefficients; + return t * (p[0] + t * (p[1] + t * (p[2] + t * (p[3] + t * (p[4] + t * (p[5] + t * p[6])))))); +} + +long double AsinDenominator(long double t) { + const auto& q = AsinDenominatorCoefficients; + 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" { @@ -18,38 +274,206 @@ 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) { return std::asin(x); } -float APS5_VABI acosf_nid_postfix(float x) { return std::acos(x); } +float APS5_VABI asinf_nid_postfix(float x) { + const int saved_errno = errno; + const float result = std::asin(x); + errno = saved_errno; + return result; +} +float APS5_VABI acosf_nid_postfix(float x) { + if (std::isnan(x)) { + return std::bit_cast(std::bit_cast(x) | 0x00400000u); + } + if (std::fabs(x) > 1.0f) { + return std::bit_cast(0xffc00000u); + } + const int saved_errno = errno; + const float result = std::acos(x); + errno = saved_errno; + return result; +} float APS5_VABI atan2f_nid_postfix(float y, float x) { return std::atan2(y, x); } float APS5_VABI hypotf_nid_postfix(float x, float y) { return static_cast(std::hypot(static_cast(x), static_cast(y))); } -double APS5_VABI hypot_nid_postfix(double x, double y) { return std::hypot(x, y); } +double APS5_VABI hypot_nid_postfix(double x, double y) { + const std::uint64_t hx = std::bit_cast(x); + const std::uint64_t hy = std::bit_cast(y); + const std::uint64_t ix = hx & 0x7fffffffffffffffull; + const std::uint64_t iy = hy & 0x7fffffffffffffffull; + if (ix == 0x7ff0000000000000ull || iy == 0x7ff0000000000000ull) { + return std::numeric_limits::infinity(); + } + return std::hypot(x, y); +} float APS5_VABI tanf_nid_postfix(float x) { return std::tan(x); } float APS5_VABI log10f_nid_postfix(float x) { return std::log10(x); } float APS5_VABI logbf_nid_postfix(float x) { return std::logb(x); } -double APS5_VABI exp2_nid_postfix(double x) { return std::exp2(x); } +double APS5_VABI exp2_nid_postfix(double x) { + int saved_errno = errno; + double result = std::exp2(x); + errno = saved_errno; + return result; +} 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. -std::int64_t APS5_VABI lround_nid_postfix(double x) { return std::llround(x); } -std::int64_t APS5_VABI lroundf_nid_postfix(float x) { return std::llround(x); } -std::int64_t APS5_VABI llround_nid_postfix(double x) { return std::llround(x); } +std::int64_t APS5_VABI lround_nid_postfix(double x) { return RoundToInt64(x); } +std::int64_t APS5_VABI lroundf_nid_postfix(float x) { return RoundToInt64(x); } +std::int64_t APS5_VABI llround_nid_postfix(double x) { return RoundToInt64(x); } +std::int64_t APS5_VABI llroundf_nid_postfix(float x) { return RoundToInt64(x); } +std::int64_t APS5_VABI llroundl_nid_postfix(long double x) { return RoundToInt64(x); } int APS5_VABI __isfinitef_nid_postfix(float x) { return std::isfinite(x) ? 1 : 0; } int APS5_VABI __isnormal_nid_postfix(double x) { return std::isnormal(x) ? 1 : 0; } int APS5_VABI __isnormalf_nid_postfix(float x) { return std::isnormal(x) ? 1 : 0; } int APS5_VABI __isinff_nid_postfix(float x) { return std::isinf(x) ? 1 : 0; } +float APS5_VABI nearbyintf_nid_postfix(float x) { return std::nearbyintf(x); } +long double APS5_VABI nearbyintl_nid_postfix(long double x) { return std::nearbyint(x); } +double APS5_VABI nearbyint_nid_postfix(double x) { return std::nearbyint(x); } double APS5_VABI cbrt_nid_postfix(double x) { return std::cbrt(x); } -double APS5_VABI asin_nid_postfix(double x) { return std::asin(x); } +double APS5_VABI asin_nid_postfix(double x) { + if (std::isgreater(x, 1.0) || std::isless(x, -1.0)) { + std::feraiseexcept(FE_INVALID); + return -std::numeric_limits::quiet_NaN(); + } + return std::asin(x); +} 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); } +double APS5_VABI floor_nid_postfix(double x) { return std::floor(x); } +float APS5_VABI truncf_nid_postfix(float x) { return std::trunc(x); } +float APS5_VABI ceilf_nid_postfix(float x) { return std::ceil(x); } +float APS5_VABI floorf_nid_postfix(float x) { return std::floor(x); } +double APS5_VABI fma_nid_postfix(double x, double y, double z) { return std::fma(x, y, z); } +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); + const int exponent = parts.signExponent & 0x7fff; + if (exponent >= 0x3fff) { + if (exponent == 0x3fff && (parts.mantissa & 0x7fffffffffffffffull) == 0) return x * HalfPiHigh + x * HalfPiLow; + std::feraiseexcept(FE_INVALID); + errno = EDOM; + return (x - x) / (x - x); + } + if (exponent < 0x3ffe) { + if (exponent < 0x3fff - 32 && 1.0e300L + x > 1.0L) return x; + const long double t = x * x; + return x + x * (AsinNumerator(t) / AsinDenominator(t)); + } + const long double t = (1.0L - std::fabs(x)) * 0.5L; + const long double p = AsinNumerator(t); + const long double q = AsinDenominator(t); + const long double s = std::sqrt(t); + long double result; + if ((parts.mantissa >> 32) >= 0xf3333333u) { + result = HalfPiHigh - (2.0L * (s + s * (p / q)) - HalfPiLow); + } else { + const long double w = ClearLowMantissaWord(s); + const long double c = (t - w * w) / (s + w); + const long double r = p / q; + result = QuarterPiHigh - ((2.0L * s * r - (HalfPiLow - 2.0L * c)) - (QuarterPiHigh - 2.0L * w)); + } + return (parts.signExponent & 0x8000) != 0 ? -result : result; +} + +long double APS5_VABI frexpl_nid_postfix(long double x, int* exponent) { + if (!std::isfinite(x)) return x; + return std::frexp(x, exponent); +} double APS5_VABI exp_nid_postfix(double x) { return std::exp(x); } double APS5_VABI atan_nid_postfix(double x) { return std::atan(x); } double APS5_VABI tan_nid_postfix(double x) { return std::tan(x); } double APS5_VABI log2_nid_postfix(double x) { return std::log2(x); } double APS5_VABI log_nid_postfix(double x) { return std::log(x); } +double APS5_VABI log1p_nid_postfix(double x) { return std::log1p(x); } +float APS5_VABI log1pf_nid_postfix(float x) { return std::log1p(x); } +double APS5_VABI expm1_nid_postfix(double x) { return std::expm1(x); } +float APS5_VABI expm1f_nid_postfix(float x) { return std::expm1(x); } +float APS5_VABI erff_nid_postfix(float x) { return std::erf(x); } +double APS5_VABI 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); } @@ -61,6 +485,54 @@ void APS5_VABI sincosf_nid_postfix(float x, float* sinp, float* cosp) { double APS5_VABI sin_nid_postfix(double x) { return std::sin(x); } double APS5_VABI cos_nid_postfix(double x) { return std::cos(x); } +double APS5_VABI sinh_nid_postfix(double x) { return std::sinh(x); } +double APS5_VABI cosh_nid_postfix(double x) { return std::cosh(x); } +float APS5_VABI coshf_nid_postfix(float x) { return std::cosh(x); } + +float APS5_VABI sinhf_nid_postfix(float x) { + const std::uint32_t magnitude = std::bit_cast(x) & 0x7fffffffu; + if (magnitude >= 0x7f800000u) return x + x; + const float half = std::signbit(x) ? -0.5f : 0.5f; + const float absolute = std::fabs(x); + if (magnitude < 0x41100000u) { + if (magnitude < 0x39800000u && 1.0e37f + x > 1.0f) return x; + const float t = SinhfExpm1(absolute); + if (magnitude < 0x3f800000u) return half * (2.0f * t - t * t / (t + 1.0f)); + return half * (t + t / (t + 1.0f)); + } + if (magnitude < 0x42b17217u) return half * SinhfExp(absolute); + if (magnitude <= 0x42b2d4fcu) { + const float w = SinhfExp(0.5f * absolute); + return half * w * w; + } + errno = ERANGE; + return x * 1.0e37f; +} + +double APS5_VABI asinh_nid_postfix(double x) { return std::asinh(x); } +float APS5_VABI asinhf_nid_postfix(float x) { return std::asinh(x); } +double APS5_VABI acosh_nid_postfix(double x) { return std::acosh(x); } +float APS5_VABI acoshf_nid_postfix(float x) { return std::acosh(x); } +double APS5_VABI atanh_nid_postfix(double x) { return std::atanh(x); } + +float APS5_VABI atanhf_nid_postfix(float x) { + const std::uint32_t magnitude = std::bit_cast(x) & 0x7fffffffu; + if (magnitude > 0x3f800000u) { + std::feraiseexcept(FE_INVALID); + errno = EDOM; + return (x - x) / (x - x); + } + if (magnitude == 0x3f800000u) { + errno = ERANGE; + return x / 0.0f; + } + if (magnitude < 0x31800000u && 1.0e30f + x > 0.0f) return x; + const float absolute = std::fabs(x); + const float twice = absolute + absolute; + const float t = magnitude < 0x3f000000u ? 0.5f * AtanhfLog1p(twice + twice * absolute / (1.0f - absolute)) + : 0.5f * AtanhfLog1p(twice / (1.0f - absolute)); + return std::signbit(x) ? -t : t; +} void APS5_VABI sincos_nid_postfix(double x, double* sinp, double* cosp) { *sinp = std::sin(x); @@ -69,29 +541,102 @@ void APS5_VABI sincos_nid_postfix(double x, double* sinp, double* cosp) { float APS5_VABI atanf_nid_postfix(float x) { return std::atan(x); } double APS5_VABI atan2_nid_postfix(double y, double x) { return std::atan2(y, x); } -float APS5_VABI powf_nid_postfix(float base, float exp) { return std::pow(base, exp); } +float APS5_VABI powf_nid_postfix(float base, float exp) { + if (base == 1.0f) return 1.0f; + return std::pow(base, exp); +} double APS5_VABI pow_nid_postfix(double base, double exp) { return std::pow(base, exp); } +float APS5_VABI __powisf2_nid_postfix(float base, int exponent) { + const bool reciprocal = exponent < 0; + float result = 1.0f; + unsigned int magnitude = reciprocal ? 0u - static_cast(exponent) : static_cast(exponent); + while (magnitude != 0u) { + if ((magnitude & 1u) != 0u) result *= base; + magnitude >>= 1u; + if (magnitude == 0u) break; + base *= base; + } + return reciprocal ? 1.0f / result : result; +} +double APS5_VABI __powidf2_nid_postfix(double base, int exponent) { + const bool reciprocal = exponent < 0; + double result = 1.0; + unsigned int magnitude = reciprocal ? 0u - static_cast(exponent) : static_cast(exponent); + while (magnitude != 0u) { + if ((magnitude & 1u) != 0u) result *= base; + magnitude >>= 1u; + if (magnitude == 0u) break; + base *= base; + } + return reciprocal ? 1.0 / result : result; +} float APS5_VABI expf_nid_postfix(float x) { return std::exp(x); } float APS5_VABI exp2f_nid_postfix(float x) { return std::exp2(x); } float APS5_VABI logf_nid_postfix(float x) { return std::log(x); } float APS5_VABI log2f_nid_postfix(float x) { return std::log2(x); } double APS5_VABI log10_nid_postfix(double x) { return std::log10(x); } float APS5_VABI ldexpf_nid_postfix(float x, int exp) { return std::ldexp(x, exp); } -double APS5_VABI fmod_nid_postfix(double x, double y) { return std::fmod(x, y); } +double APS5_VABI fmod_nid_postfix(double x, double y) { + int saved_errno = errno; + double result = std::fmod(x, y); + errno = saved_errno; + return result; +} float APS5_VABI roundf_nid_postfix(float x) { return std::round(x); } 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; } int APS5_VABI __signbit_nid_postfix(double x) { return std::signbit(x) ? 1 : 0; } +float APS5_VABI nanf_nid_postfix(const char* tagp) { + (void)tagp; + return std::bit_cast(0x7fc00000u); +} +double APS5_VABI nan_nid_postfix(const char* tagp) { + (void)tagp; + return std::bit_cast(0x7ff8000000000000ull); +} double APS5_VABI modf_nid_postfix(double x, double* integral) { return std::modf(x, integral); } float APS5_VABI modff_nid_postfix(float x, float* integral) { return std::modf(x, integral); } double APS5_VABI tanh_nid_postfix(double x) { return std::tanh(x); } float APS5_VABI tanhf_nid_postfix(float x) { return std::tanh(x); } -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); } +long double APS5_VABI atanl_nid_postfix(long double x) { return std::atan(x); } +long double APS5_VABI sinl_nid_postfix(long double x) { return std::sin(x); } +long double APS5_VABI cosl_nid_postfix(long double x) { return std::cos(x); } +long double APS5_VABI tanl_nid_postfix(long double x) { return std::tan(x); } +long double APS5_VABI sinhl_nid_postfix(long double x) { return std::sinh(x); } +long double APS5_VABI coshl_nid_postfix(long double x) { return std::cosh(x); } +long double APS5_VABI tanhl_nid_postfix(long double x) { return std::tanh(x); } +long double APS5_VABI expl_nid_postfix(long double x) { return std::exp(x); } +long double APS5_VABI exp2l_nid_postfix(long double x) { return std::exp2(x); } +long double APS5_VABI logl_nid_postfix(long double x) { return std::log(x); } +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); } +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}}; @@ -108,7 +653,23 @@ short APS5_VABI _FDtest_nid_postfix(const float* value) { if (exponent == 0) return fraction != 0 ? Denormal : Zero; return Finite; } + +short APS5_VABI _Dtest_nid_postfix(const double* value) { + constexpr short Denormal = -2, Finite = -1, Zero = 0, Infinite = 1, NotANumber = 2; + if (value == nullptr) throw std::invalid_argument("_Dtest: null value"); + std::uint64_t bits; + std::memcpy(&bits, value, sizeof(bits)); + const auto exponent = bits & 0x7ff0000000000000u; + const auto fraction = bits & 0x000fffffffffffffu; + if (exponent == 0x7ff0000000000000u) return fraction != 0 ? NotANumber : Infinite; + if (exponent == 0) return fraction != 0 ? Denormal : Zero; + return Finite; +} + int APS5_VABI __isnanf_nid_postfix(float x) { return std::isnan(x) ? 1 : 0; } +int APS5_VABI isnanf_nid_postfix(float x) { return __isnanf_nid_postfix(x); } +int APS5_VABI isnan_nid_postfix(double x) { return __isnan_nid_postfix(x); } +int APS5_VABI isinf_nid_postfix(double x) { return __isinf_nid_postfix(x); } int APS5_VABI __fpclassifyf_nid_postfix(float x) { constexpr int Infinite = 0x01, NotANumber = 0x02, Normal = 0x04, Subnormal = 0x08, Zero = 0x10; std::uint32_t bits; @@ -120,22 +681,112 @@ int APS5_VABI __fpclassifyf_nid_postfix(float x) { return Normal; } int APS5_VABI __signbitf_nid_postfix(float x) { return std::signbit(x) ? 1 : 0; } +int APS5_VABI __signbitl_nid_postfix(long double x) { + static_assert(sizeof(long double) == 16, "guest long double requires x87 extended precision storage"); + 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; -int APS5_VABI rand_nid_postfix() { - std::lock_guard lock(g_randLock); - const std::int64_t x = static_cast(g_randState % 0x7ffffffeu) + 1; +static int NextRand_nid_no_patch(std::uint32_t& state) { + const std::int64_t x = static_cast(state % 0x7ffffffeu) + 1; std::int64_t next = 16807 * (x % 127773) - 2836 * (x / 127773); if (next < 0) next += 0x7fffffff; - g_randState = static_cast(next - 1); + state = static_cast(next - 1); return static_cast(next - 1); } +int APS5_VABI rand_nid_postfix() { + std::lock_guard lock(g_randLock); + return NextRand_nid_no_patch(g_randState); +} + +int APS5_VABI rand_r_nid_postfix(unsigned int* state) { + if (state == nullptr) throw std::invalid_argument("rand_r: null state"); + std::uint32_t value = *state; + const int result = NextRand_nid_no_patch(value); + *state = value; + return result; +} + void APS5_VABI srand_nid_postfix(unsigned int seed) { std::lock_guard lock(g_randLock); g_randState = seed; } +int APS5_VABI __fpclassifyd_nid_postfix(double value) { + return SonyFpclassifyd(value); +} + +double APS5_VABI _Sinh_nid_postfix(double x, double y) { + return SonySinh(x, y); +} + +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; + double _Double; + long double _Long_double; +}; + +static_assert(std::endian::native == std::endian::little); +SonyDconst _Inf_nid_postfix {{0, 0, 0, 0x7ff0, 0, 0, 0, 0}}; +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 d64d02d4a8..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(); @@ -16,12 +19,12 @@ namespace { constexpr unsigned MspaceThreadUnsafe = 1; constexpr std::uintptr_t Granule = 16; +constexpr std::uintptr_t MinimumAlignment = 32; constexpr std::uintptr_t ArenaHeaderBytes = 64; struct Chunk { std::uintptr_t end; bool used; - std::size_t requested; }; struct Arena { @@ -50,7 +53,7 @@ Arena* Find(void* handle) { } void AddFree(Arena& arena, std::uintptr_t start, std::uintptr_t end) { - arena.chunks[start] = {end, false, 0}; + arena.chunks[start] = {end, false}; arena.free.emplace(end - start, start); } @@ -60,8 +63,8 @@ void RemoveFree(Arena& arena, std::map::iterator chunk) { void* Allocate(Arena* arena, std::size_t size, std::size_t alignment) { if (!arena) return nullptr; - alignment = std::max(alignment, Granule); - const auto needed = AlignUp(std::max(size, 1), Granule); + alignment = std::max(alignment, MinimumAlignment); + const auto needed = AlignUp(std::max(size, 1), MinimumAlignment); if (needed < size || needed > std::numeric_limits::max() - alignment) { Error(12); return nullptr; @@ -77,7 +80,7 @@ void* Allocate(Arena* arena, std::size_t size, std::size_t alignment) { if (aligned > start) AddFree(*arena, start, aligned); const auto finish = aligned + needed; if (finish < end) AddFree(*arena, finish, end); - arena->chunks[aligned] = {finish, true, size}; + arena->chunks[aligned] = {finish, true}; arena->inUse += needed; arena->peakInUse = std::max(arena->peakInUse, arena->inUse); return reinterpret_cast(aligned); @@ -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); @@ -215,14 +207,11 @@ void* APS5_VABI sceLibcMspaceRealloc_nid_postfix(void* handle, void* pointer, st std::map::iterator chunk; if (!FindUsed(arena, pointer, chunk)) return nullptr; if (!size) { Release(*arena, chunk); return nullptr; } - const auto needed = AlignUp(size, Granule); + const auto needed = AlignUp(size, MinimumAlignment); if (needed < size) { Error(12); return nullptr; } const auto start = chunk->first; const auto capacity = chunk->second.end - start; - if (needed <= capacity) { - chunk->second.requested = size; - return pointer; - } + if (needed <= capacity) return pointer; const auto next = std::next(chunk); if (next != arena->chunks.end() && !next->second.used && next->first == chunk->second.end && next->second.end - start >= needed) { const auto nextEnd = next->second.end; @@ -232,13 +221,12 @@ void* APS5_VABI sceLibcMspaceRealloc_nid_postfix(void* handle, void* pointer, st if (finish < nextEnd) AddFree(*arena, finish, nextEnd); arena->inUse += needed - capacity; arena->peakInUse = std::max(arena->peakInUse, arena->inUse); - arena->chunks[start] = {finish, true, size}; + arena->chunks[start] = {finish, true}; return pointer; } - const auto previousSize = chunk->second.requested; void* result = Allocate(arena, size, Granule); if (result) { - std::memcpy(result, pointer, previousSize); + std::memcpy(result, pointer, std::min(capacity, size)); Release(*arena, arena->chunks.find(start)); } return result; @@ -265,29 +253,26 @@ void* APS5_VABI sceLibcMspaceMemalign_nid_postfix(void* handle, std::size_t alig return Allocate(Find(handle), size, alignment); } -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 sceLibcMspaceAlignedAlloc_nid_postfix(void* handle, std::size_t alignment, std::size_t size) { + return sceLibcMspaceMemalign_nid_postfix(handle, alignment, size); +} + +void* APS5_VABI sceLibcMspaceReallocalign_nid_postfix(void* handle, void* pointer, std::size_t 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; - if (needed <= chunk->second.end - start && (start & (effective - 1)) == 0) { - chunk->second.requested = size; - return pointer; - } - const auto previousSize = chunk->second.requested; + const auto capacity = chunk->second.end - start; + if (needed <= capacity && (start & (effective - 1)) == 0) return pointer; void* result = Allocate(arena, size, effective); if (result) { - std::memcpy(result, pointer, std::min(previousSize, size)); + std::memcpy(result, pointer, std::min(capacity, size)); Release(*arena, arena->chunks.find(start)); } return result; @@ -314,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 10ccdb4572..e7dc400ac3 100644 --- a/core/libs/prx/libc/src/Multibyte.cpp +++ b/core/libs/prx/libc/src/Multibyte.cpp @@ -1,9 +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" { @@ -50,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; @@ -142,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; @@ -158,4 +327,8 @@ std::size_t APS5_VABI mbsrtowcs_nid_postfix(std::uint16_t* destination, const ch return converted; } +std::size_t APS5_VABI mbstowcs_nid_postfix(std::uint16_t* dest, const char* src, std::size_t size) { + return SonyMbstowcs(dest, src, size); +} + } diff --git a/core/libs/prx/libc/src/Process.cpp b/core/libs/prx/libc/src/Process.cpp index d8ce47284b..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,9 +6,13 @@ #include #include #include +#include #include #include #include +#ifdef _WIN32 +#include +#endif #include #include #include "prx/libc/include/Shutdown.hpp" @@ -15,6 +20,8 @@ #include "prx/libc/include/General.hpp" #include "SceTypes.hpp" +extern "C" int APS5_VABI __cxa_atexit_nid_postfix(void (APS5_VABI *func)(void*), void* arg, void* dsoHandle); + namespace { std::mutex shutdownMutex; @@ -28,17 +35,10 @@ std::stop_source shutdownSource; std::atomic exitRequested{false}; std::atomic exitStarted{false}; using GuestExitCallback = void (APS5_VABI*)(); -std::mutex exitCallbackMutex; -std::vector exitCallbacks; +thread_local bool runningExit = false; -void runGuestExitCallback() { - GuestExitCallback callback; - { - std::lock_guard lock(exitCallbackMutex); - callback = exitCallbacks.back(); - exitCallbacks.pop_back(); - } - callback(); +void APS5_VABI RunGuestExitCallback(void* callback) { + reinterpret_cast(callback)(); } } @@ -54,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() { @@ -98,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) { @@ -106,16 +158,55 @@ extern "C" { std::fprintf(stderr, "[libc] _Exit(%d) called from %p\n", code, __builtin_return_address(0)); std::fflush(stderr); } + LibcTerminate_nid_no_patch(code); +} + +[[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)) LibcAwaitExit_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) { @@ -135,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(); @@ -155,11 +247,7 @@ int* APS5_VABI __error_nid_postfix() { int APS5_VABI atexit_nid_postfix(GuestExitCallback func) { if (func == nullptr) return 0; - std::lock_guard lock(exitCallbackMutex); - exitCallbacks.push_back(func); - const int result = std::atexit(runGuestExitCallback); - if (result != 0) exitCallbacks.pop_back(); - return result; + return __cxa_atexit_nid_postfix(RunGuestExitCallback, reinterpret_cast(func), nullptr); } namespace { @@ -185,4 +273,11 @@ int APS5_VABI at_quick_exit_nid_postfix(GuestExitCallback func) { _Exit_nid_postfix(status); } +int APS5_VABI daemon_nid_postfix(int noChdir, int noClose) { + (void)noChdir; + (void)noClose; + NotImplemented_nid_no_patch("daemon: forking into the background"); + return -1; +} + } diff --git a/core/libs/prx/libc/src/Random.cpp b/core/libs/prx/libc/src/Random.cpp new file mode 100644 index 0000000000..06cf1d789d --- /dev/null +++ b/core/libs/prx/libc/src/Random.cpp @@ -0,0 +1,219 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr int MaxTypes = 5; +constexpr int LinearCongruentialType = 0; +constexpr int LinearCongruentialShuffles = 50; +constexpr std::array MinimumStateBytes {8, 32, 64, 128, 256}; +constexpr std::array Degrees {0, 7, 15, 31, 63}; +constexpr std::array Separations {0, 3, 1, 3, 1}; + +std::uint32_t g_defaultTable[] { + 3, + 0x2cf41758, 0x27bb3711, 0x4916d4d1, 0x7b02f59f, 0x9b8e28eb, 0xc0e80269, + 0x696f5c16, 0x878f1ff5, 0x52d9c07f, 0x916a06cd, 0xb50b3a20, 0x2776970a, + 0xee4eb2a6, 0xe94640ec, 0xb1d65612, 0x9d1ed968, 0x1043f6b7, 0xa3432a76, + 0x17eacbb9, 0x3c09e2eb, 0x04f8c2b3, 0x708a1f57, 0xee341814, 0x95d0e4d2, + 0xb06f216c, 0x8bd2e72e, 0x8f7c38d7, 0xcfc6a8fc, 0x02a59495, 0xa20d2a69, + 0xe29d12d1 +}; + +struct AdditiveFeedbackState { + char* table; + int type; + int degree; + int separation; + int front; + int rear; +}; + +std::mutex g_randomLock; +AdditiveFeedbackState g_random {reinterpret_cast(g_defaultTable), 3, 31, 3, 3, 0}; + +std::uint32_t LoadWord(const char* table, int index) { + std::uint32_t value; + std::memcpy(&value, table + sizeof(value) * static_cast(index), sizeof(value)); + return value; +} + +void StoreWord(char* table, int index, std::uint32_t value) { + std::memcpy(table + sizeof(value) * static_cast(index), &value, sizeof(value)); +} + +std::uint32_t GoodRand(std::uint32_t context) { + std::int32_t x = static_cast(context % 0x7ffffffeu) + 1; + const std::int32_t high = x / 127773; + const std::int32_t low = x % 127773; + x = 16807 * low - 2836 * high; + if (x < 0) x += 0x7fffffff; + return static_cast(x - 1); +} + +std::uint32_t NextLocked() { + auto& state = g_random; + if (state.type == LinearCongruentialType) { + const std::uint32_t value = GoodRand(LoadWord(state.table, 1)); + StoreWord(state.table, 1, value); + return value; + } + const std::uint32_t sum = LoadWord(state.table, 1 + state.front) + LoadWord(state.table, 1 + state.rear); + StoreWord(state.table, 1 + state.front, sum); + if (++state.front >= state.degree) { + state.front = 0; + ++state.rear; + } else if (++state.rear >= state.degree) { + state.rear = 0; + } + return sum >> 1; +} + +void SeedLocked(std::uint32_t seed) { + auto& state = g_random; + StoreWord(state.table, 1, seed); + int rounds = LinearCongruentialShuffles; + if (state.type != LinearCongruentialType) { + for (int i = 1; i < state.degree; ++i) StoreWord(state.table, 1 + i, GoodRand(LoadWord(state.table, i))); + state.front = state.separation; + state.rear = 0; + rounds = 10 * state.degree; + } + for (int i = 0; i < rounds; ++i) NextLocked(); +} + +void SaveTypeWordLocked() { + const auto& state = g_random; + const int word = state.type == LinearCongruentialType ? state.type : MaxTypes * state.rear + state.type; + StoreWord(state.table, 0, static_cast(word)); +} + +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 Mask = (1ull << 48) - 1; + std::uint64_t value = (static_cast(seed[2]) << 32) | (static_cast(seed[1]) << 16) | seed[0]; + value = (value * g_rand48Multiplier + g_rand48Addend) & Mask; + seed[0] = static_cast(value); + seed[1] = static_cast(value >> 16); + seed[2] = static_cast(value >> 32); + return value; +} + +} + +extern "C" { + +double APS5_VABI erand48_nid_postfix(std::uint16_t* seed) { + return std::ldexp(static_cast(NextRand48(seed, __func__)), -48); +} + +std::int64_t APS5_VABI nrand48_nid_postfix(std::uint16_t* seed) { + return static_cast(NextRand48(seed, __func__) >> 17); +} + +std::int64_t APS5_VABI jrand48_nid_postfix(std::uint16_t* seed) { + return static_cast(NextRand48(seed, __func__) >> 16); +} + +double APS5_VABI drand48_nid_postfix() { + std::lock_guard lock(g_randomLock); + return std::ldexp(static_cast(NextRand48(g_rand48Seed, __func__)), -48); +} + +std::int64_t APS5_VABI lrand48_nid_postfix() { + std::lock_guard lock(g_randomLock); + return static_cast(NextRand48(g_rand48Seed, __func__) >> 17); +} + +std::int64_t APS5_VABI mrand48_nid_postfix() { + std::lock_guard lock(g_randomLock); + return static_cast(NextRand48(g_rand48Seed, __func__) >> 16); +} + +void APS5_VABI srand48_nid_postfix(std::int64_t seedval) { + std::lock_guard lock(g_randomLock); + 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() { + std::lock_guard lock(g_randomLock); + return static_cast(NextLocked()); +} + +void APS5_VABI srandom_nid_postfix(std::uint64_t seed) { + std::lock_guard lock(g_randomLock); + SeedLocked(static_cast(seed)); +} + +char* APS5_VABI initstate_nid_postfix(std::uint64_t seed, char* state, std::int64_t size) { + if (size < MinimumStateBytes[0]) return nullptr; + if (state == nullptr) throw std::invalid_argument("initstate: null state"); + std::lock_guard lock(g_randomLock); + char* previous = g_random.table; + SaveTypeWordLocked(); + int type = MaxTypes - 1; + while (size < MinimumStateBytes[static_cast(type)]) --type; + g_random = {state, type, Degrees[static_cast(type)], Separations[static_cast(type)], 0, 0}; + SeedLocked(static_cast(seed)); + SaveTypeWordLocked(); + return previous; +} + +char* APS5_VABI setstate_nid_postfix(char* state) { + if (state == nullptr) throw std::invalid_argument("setstate: null state"); + std::lock_guard lock(g_randomLock); + const std::uint32_t word = LoadWord(state, 0); + const int type = static_cast(word % MaxTypes); + const std::uint32_t rear = word / MaxTypes; + const int degree = Degrees[static_cast(type)]; + if (type != LinearCongruentialType && rear >= static_cast(degree)) return nullptr; + char* previous = g_random.table; + SaveTypeWordLocked(); + const int separation = Separations[static_cast(type)]; + g_random.table = state; + g_random.type = type; + g_random.degree = degree; + g_random.separation = separation; + if (type != LinearCongruentialType) { + g_random.rear = static_cast(rear); + g_random.front = (static_cast(rear) + separation) % degree; + } + return previous; +} + +} diff --git a/core/libs/prx/libc/src/RealPath.cpp b/core/libs/prx/libc/src/RealPath.cpp new file mode 100644 index 0000000000..10a0193b94 --- /dev/null +++ b/core/libs/prx/libc/src/RealPath.cpp @@ -0,0 +1,145 @@ +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" char* APS5_VABI getcwd_nid_postfix(char* buffer, std::size_t size); + +namespace { +constexpr std::size_t PathMax = 1024; +constexpr std::string_view Separators = "/\\"; +constexpr int NoEntry = 2; +constexpr int InputOutput = 5; +constexpr int NoMemory = 12; +constexpr int PermissionDenied = 13; +constexpr int NotDirectory = 20; +constexpr int InvalidArgument = 22; +constexpr int TooManyLinks = 62; +constexpr int NameTooLong = 63; + +int StatusError(const std::error_code& error) { + if (error == std::errc::no_such_file_or_directory) return NoEntry; + if (error == std::errc::not_a_directory) return NotDirectory; + if (error == std::errc::permission_denied) return PermissionDenied; + if (error == std::errc::filename_too_long) return NameTooLong; + if (error == std::errc::too_many_symbolic_link_levels) return TooManyLinks; + if (error == std::errc::not_enough_memory) return NoMemory; + return InputOutput; +} + +void LeaveTrouble(char* resolved, const std::string& trouble) { + if (resolved == nullptr) return; + const auto length = trouble.size() < PathMax ? trouble.size() : PathMax - 1; + std::memcpy(resolved, trouble.data(), length); + resolved[length] = '\0'; +} + +void DropLastComponent(std::string& result) { + if (result.size() <= 1) return; + const auto slash = result.rfind('/'); + result.erase(slash == 0 ? 1 : slash); +} + +bool IsSeparator(char character) { return Separators.find(character) != std::string_view::npos; } + +bool ResolveGuest(const char* path, std::string& result, int& error) { + std::string_view left(path); + if (IsSeparator(left.front())) { + result = "/"; + left.remove_prefix(1); + } else { + char current[PathMax]; + if (getcwd_nid_postfix(current, sizeof(current)) == nullptr) { + result = "."; + error = errno; + return false; + } + result = current; + } + if (left.size() >= PathMax || result.size() >= PathMax) { + error = NameTooLong; + return false; + } + while (!left.empty()) { + const auto separator = left.find_first_of(Separators); + const auto token = left.substr(0, separator); + const bool moreFollows = separator != std::string_view::npos; + left = moreFollows ? left.substr(separator + 1) : std::string_view(); + if (result.size() > 1 && result.size() + 1 >= PathMax) { + error = NameTooLong; + return false; + } + if (token.empty() || token == ".") continue; + if (token == "..") { + DropLastComponent(result); + continue; + } + std::string candidate = result; + if (candidate.size() > 1) candidate += '/'; + candidate += token; + result = candidate; + if (candidate.size() >= PathMax) { + error = NameTooLong; + return false; + } + std::error_code code; + const auto status = std::filesystem::status(ResolvePath_nid_no_patch(candidate.c_str()), code); + if (code && code != std::errc::no_such_file_or_directory && code != std::errc::not_a_directory) { + error = StatusError(code); + return false; + } + if (!std::filesystem::exists(status)) { + error = NoEntry; + return false; + } + if (moreFollows && !std::filesystem::is_directory(status)) { + error = NotDirectory; + return false; + } + } + return true; +} +} + +extern "C" char* APS5_VABI realpath_nid_postfix(const char* path, char* resolved) { + if (path == nullptr) { + errno = InvalidArgument; + return nullptr; + } + if (*path == '\0') { + errno = NoEntry; + return nullptr; + } + std::string result; + int error = 0; + try { + if (!ResolveGuest(path, result, error)) { + LeaveTrouble(resolved, result); + errno = error; + return nullptr; + } + if (resolved == nullptr) { + resolved = static_cast(ApplicationHeapAllocate_nid_no_patch(PathMax)); + if (resolved == nullptr) { + errno = NoMemory; + return nullptr; + } + } + std::memcpy(resolved, result.c_str(), result.size() + 1); + return resolved; + } catch (const std::bad_alloc&) { + errno = NoMemory; + return nullptr; + } catch (const std::filesystem::filesystem_error& failure) { + LeaveTrouble(resolved, result); + errno = StatusError(failure.code()); + return nullptr; + } +} diff --git a/core/libs/prx/libc/src/Rename.cpp b/core/libs/prx/libc/src/Rename.cpp index 3e5c5658c4..973c142ff6 100644 --- a/core/libs/prx/libc/src/Rename.cpp +++ b/core/libs/prx/libc/src/Rename.cpp @@ -1,17 +1,126 @@ #include "prx/libc/include/FilesystemError.hpp" +#include "prx/libc/include/RenameDot.hpp" #include "prx/libc/include/General.hpp" #include +#include #include +#ifdef _WIN32 +#include +#include +#include +#include +#endif -extern "C" int APS5_VABI rename_nid_postfix(const char* from, const char* to) { - if (!from || !to) { errno = 14; return -1; } - if (!*from || !*to) { errno = 2; return -1; } +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 (replacesExisting) { + const auto target = fs::absolute(destination, error); + if (error) return error; + struct RenameInfo { DWORD flags; HANDLE root; DWORD nameBytes; WCHAR name[1]; }; + static_assert(sizeof(RenameInfo) == sizeof(FILE_RENAME_INFO) && offsetof(RenameInfo, name) == offsetof(FILE_RENAME_INFO, FileName)); + if (target.native().size() > (MAXDWORD - sizeof(RenameInfo)) / sizeof(wchar_t)) return std::make_error_code(std::errc::filename_too_long); + const auto nameBytes = target.native().size() * sizeof(wchar_t); + std::vector buffer(sizeof(RenameInfo) + nameBytes); + auto* info = reinterpret_cast(buffer.data()); + constexpr DWORD replaceExisting = 0x00000001, posixSemantics = 0x00000002, ignoreReadOnly = 0x00000040; + const auto attributes = GetFileAttributesW(destination.c_str()); + info->flags = replaceExisting | posixSemantics | (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_READONLY) ? ignoreReadOnly : 0); + info->nameBytes = static_cast(nameBytes); + std::memcpy(info->name, target.c_str(), nameBytes); + const auto handle = CreateFileW(source.c_str(), DELETE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | 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()); + CloseHandle(handle); + return error; + } +#endif + fs::rename(source, destination, error); + return error; +} + +static int RenamePath_nid_no_patch(const std::filesystem::path& source, const std::filesystem::path& destination) { + constexpr int GuestEnoent = 2; + constexpr int GuestEnotdir = 20; + constexpr int GuestEisdir = 21; + constexpr int GuestEinval = 22; + constexpr int GuestEnotempty = 66; + namespace fs = std::filesystem; + std::error_code error; + const auto sourceStatus = fs::symlink_status(source, error); + if (error && error != std::errc::no_such_file_or_directory) return FilesystemError(error); + if (sourceStatus.type() == fs::file_type::not_found) return GuestEnoent; + + const auto parent = destination.parent_path().empty() ? fs::path(".") : destination.parent_path(); + error.clear(); + const auto parentStatus = fs::status(parent, error); + if (error) return FilesystemError(error); + if (!fs::exists(parentStatus)) return GuestEnoent; + if (!fs::is_directory(parentStatus)) return GuestEnotdir; + + error.clear(); + const auto destinationStatus = fs::symlink_status(destination, error); + if (error && error != std::errc::no_such_file_or_directory) return FilesystemError(error); + const bool destinationExists = destinationStatus.type() != fs::file_type::not_found; + const bool sourceIsDirectory = fs::is_directory(sourceStatus); + const bool destinationIsDirectory = destinationExists && fs::is_directory(destinationStatus); + bool sameFile = false; + if (destinationExists) { + error.clear(); + sameFile = fs::equivalent(source, destination, error); + if (error == std::errc::no_such_file_or_directory) error.clear(); + if (error) return FilesystemError(error); + if (!sameFile && sourceIsDirectory && !destinationIsDirectory) return GuestEnotdir; + if (!sameFile && !sourceIsDirectory && destinationIsDirectory) return GuestEisdir; + } + if (sourceIsDirectory && !sameFile) { + error.clear(); + const auto canonicalSource = fs::weakly_canonical(source, error); + if (error) return FilesystemError(error); + const auto canonicalDestination = fs::weakly_canonical(destination, error); + if (error) return FilesystemError(error); + const auto relative = canonicalDestination.lexically_relative(canonicalSource); + if (!relative.empty() && *relative.begin() != "..") return GuestEinval; + } + if (destinationExists && !sameFile && destinationIsDirectory) { + error.clear(); + if (!fs::is_empty(destination, error)) { + return error ? FilesystemError(error) : GuestEnotempty; + } + } + 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) { 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); - std::error_code error; - std::filesystem::rename(source, destination, error); - if (error) { errno = FilesystemError(error); return -1; } + 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); return 0; @@ -21,3 +130,7 @@ extern "C" int APS5_VABI rename_nid_postfix(const char* from, const char* to) { return -1; } } + +extern "C" int APS5_VABI rename_nid_postfix(const char* from, const char* to) { + return RenameGuestPath_nid_no_patch(from, to); +} diff --git a/core/libs/prx/libc/src/RuneLocale.cpp b/core/libs/prx/libc/src/RuneLocale.cpp new file mode 100644 index 0000000000..fa25f0412c --- /dev/null +++ b/core/libs/prx/libc/src/RuneLocale.cpp @@ -0,0 +1,187 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +namespace GuestRune { + +struct Range { + std::int32_t count; + void* entries; +}; + +struct Locale { + char magic[8]; + char encoding[32]; + void* getRune; + void* putRune; + std::int32_t invalidRune; + std::uint64_t types[256]; + std::int32_t lower[256]; + std::int32_t upper[256]; + Range typeRanges; + Range lowerRanges; + Range upperRanges; + void* variable; + std::int32_t variableLength; +}; +static_assert(offsetof(Locale, invalidRune) == 56); +static_assert(offsetof(Locale, types) == 64); +static_assert(offsetof(Locale, lower) == 2112); +static_assert(offsetof(Locale, upper) == 3136); +static_assert(offsetof(Locale, typeRanges) == 4160); +static_assert(offsetof(Locale, variable) == 4208); +static_assert(sizeof(Locale) == 4224); + +constexpr std::uint64_t Alpha = 0x100; +constexpr std::uint64_t Control = 0x200; +constexpr std::uint64_t Digit = 0x400; +constexpr std::uint64_t Graph = 0x800; +constexpr std::uint64_t Lower = 0x1000; +constexpr std::uint64_t Punct = 0x2000; +constexpr std::uint64_t Space = 0x4000; +constexpr std::uint64_t Upper = 0x8000; +constexpr std::uint64_t Hex = 0x10000; +constexpr std::uint64_t Blank = 0x20000; +constexpr std::uint64_t Print = 0x40000; +constexpr std::uint64_t Number = 0x400000; + +constexpr std::uint64_t Type(int c) { + if (c == '\t') return Control | Space | Blank; + if (c >= '\n' && c <= '\r') return Control | Space; + if (c < ' ' || c == 0x7f) return Control; + if (c == ' ') return Space | Blank | Print; + if (c >= '0' && c <= '9') return Digit | Print | Graph | Hex | Number | static_cast(c - '0'); + if (c >= 'A' && c <= 'F') return Upper | Hex | Print | Graph | Alpha | static_cast(c - 'A' + 10); + if (c >= 'a' && c <= 'f') return Lower | Hex | Print | Graph | Alpha | static_cast(c - 'a' + 10); + if (c >= 'A' && c <= 'Z') return Upper | Print | Graph | Alpha; + if (c >= 'a' && c <= 'z') return Lower | Print | Graph | Alpha; + if (c < 0x7f) return Punct | Print | Graph; + return 0; +} + +constexpr Locale MakeDefault() { + Locale locale{{'R', 'u', 'n', 'e', 'M', 'a', 'g', 'i'}, "NONE", nullptr, nullptr, 0xfffd, {}, {}, {}, {}, {}, {}, nullptr, 0}; + for (int c = 0; c < 256; ++c) { + locale.types[c] = Type(c); + locale.lower[c] = c >= 'A' && c <= 'Z' ? c + ('a' - 'A') : c; + locale.upper[c] = c >= 'a' && c <= 'z' ? c - ('a' - 'A') : c; + } + return locale; +} + +} + +extern "C" { + +extern const GuestRune::Locale _DefaultRuneLocale_nid_postfix; +const GuestRune::Locale _DefaultRuneLocale_nid_postfix = GuestRune::MakeDefault(); +const GuestRune::Locale* _CurrentRuneLocale_nid_postfix = &_DefaultRuneLocale_nid_postfix; +int __mb_sb_limit_nid_postfix = 256; + +} + +namespace GuestRune { + +constexpr std::uint32_t Weof = 0xffffffff; + +int Is(std::uint32_t c, std::uint64_t mask) { + return c < 256 && (_CurrentRuneLocale_nid_postfix->types[c] & mask) != 0; +} + +} + +extern "C" { + +int APS5_VABI iswalnum_nid_postfix(std::uint32_t c) { return GuestRune::Is(c, GuestRune::Alpha | GuestRune::Number); } +int APS5_VABI iswalpha_nid_postfix(std::uint32_t c) { return GuestRune::Is(c, GuestRune::Alpha); } +int APS5_VABI iswblank_nid_postfix(std::uint32_t c) { return GuestRune::Is(c, GuestRune::Blank); } +int APS5_VABI iswcntrl_nid_postfix(std::uint32_t c) { return GuestRune::Is(c, GuestRune::Control); } +int APS5_VABI iswdigit_nid_postfix(std::uint32_t c) { return GuestRune::Is(c, GuestRune::Digit); } +int APS5_VABI iswgraph_nid_postfix(std::uint32_t c) { return GuestRune::Is(c, GuestRune::Graph); } +int APS5_VABI iswlower_nid_postfix(std::uint32_t c) { return GuestRune::Is(c, GuestRune::Lower); } +int APS5_VABI iswprint_nid_postfix(std::uint32_t c) { return GuestRune::Is(c, GuestRune::Print); } +int APS5_VABI iswpunct_nid_postfix(std::uint32_t c) { return GuestRune::Is(c, GuestRune::Punct); } +int APS5_VABI iswspace_nid_postfix(std::uint32_t c) { return GuestRune::Is(c, GuestRune::Space); } +int APS5_VABI iswupper_nid_postfix(std::uint32_t c) { return GuestRune::Is(c, GuestRune::Upper); } +int APS5_VABI iswxdigit_nid_postfix(std::uint32_t c) { return GuestRune::Is(c, GuestRune::Hex); } +int APS5_VABI iswctype_nid_postfix(std::uint32_t c, std::uint64_t mask) { return GuestRune::Is(c, mask); } + +int APS5_VABI _Iswctype_nid_postfix(std::uint32_t c, std::uint64_t type) { + static constexpr std::uint64_t masks[] = { + GuestRune::Alpha | GuestRune::Number, GuestRune::Alpha, GuestRune::Control, GuestRune::Digit, + GuestRune::Graph, GuestRune::Lower, GuestRune::Print, GuestRune::Punct, + GuestRune::Space, GuestRune::Upper, GuestRune::Hex, GuestRune::Blank, + }; + if (type == 0 || type > std::size(masks)) throw std::invalid_argument("_Iswctype: unknown class " + std::to_string(type)); + return GuestRune::Is(c, masks[type - 1]); +} + +std::uint64_t APS5_VABI wctype_nid_postfix(const char* property) { + static constexpr struct { + const char* name; + std::uint64_t mask; + } properties[] = { + {"alnum", GuestRune::Alpha | GuestRune::Number}, {"alpha", GuestRune::Alpha}, {"blank", GuestRune::Blank}, + {"cntrl", GuestRune::Control}, {"digit", GuestRune::Digit}, {"graph", GuestRune::Graph}, + {"lower", GuestRune::Lower}, {"print", GuestRune::Print}, {"punct", GuestRune::Punct}, + {"space", GuestRune::Space}, {"upper", GuestRune::Upper}, {"xdigit", GuestRune::Hex}, + {"ideogram", 0x80000}, {"special", 0x100000}, {"phonogram", 0x200000}, + {"number", GuestRune::Number}, {"rune", 0xffffff00}, + }; + for (const auto& entry : properties) { + if (std::strcmp(property, entry.name) == 0) return entry.mask; + } + return 0; +} + +std::uint32_t APS5_VABI towlower_nid_postfix(std::uint32_t c) { + return c < 256 ? static_cast(_CurrentRuneLocale_nid_postfix->lower[c]) : c; +} + +std::uint32_t APS5_VABI towupper_nid_postfix(std::uint32_t c) { + return c < 256 ? static_cast(_CurrentRuneLocale_nid_postfix->upper[c]) : c; +} + +std::uint32_t APS5_VABI btowc_nid_postfix(int c) { + return c == -1 ? GuestRune::Weof : static_cast(c); +} + +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 4943567c8c..02058dc4af 100644 --- a/core/libs/prx/libc/src/RuntimeSupport.cpp +++ b/core/libs/prx/libc/src/RuntimeSupport.cpp @@ -9,13 +9,17 @@ #include #include #include -#include #include #include +#ifdef _WIN32 +#include +#endif #include "prx/libc/include/General.hpp" +#include "prx/libc/include/SonyCrt.hpp" #include "prx/libc/include/specifics/gcc/AtomicOps.hpp" #include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/HostEntropy.hpp" namespace { @@ -31,6 +35,20 @@ std::mutex g_exitMutex; std::vector g_exitDestructors; bool g_exitRunnerRegistered = false; +#ifdef _WIN32 +struct UcrtStream { + char* pointer; + char* base; + int count; + long flags; + long file; + int charBuffer; + int bufferSize; + char* temporaryName; + CRITICAL_SECTION lock; +}; +#endif + } extern "C" { @@ -39,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}); @@ -95,20 +117,19 @@ 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 (StopAtBinaryPrefix_nid_no_patch(str, endptr, base)) return 0; - return std::strtoimax(str, endptr, base); + if (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 (StopAtBinaryPrefix_nid_no_patch(str, endptr, base)) return 0; - return std::strtoumax(str, endptr, base); + if (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() { - static std::random_device device; - static std::mutex mutex; - std::lock_guard lock(mutex); - return device(); + unsigned int value = 0; + HostEntropy_nid_no_patch(&value, sizeof(value)); + return value; } [[noreturn]] void APS5_VABI _ZSt14_Throw_C_errori_nid_postfix(int code) { @@ -118,24 +139,41 @@ 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 +} + +void APS5_VABI flockfile_nid_postfix(FileStream* stream) { + _Lockfilelock_nid_postfix(stream); +} + +void APS5_VABI funlockfile_nid_postfix(FileStream* stream) { + _Unlockfilelock_nid_postfix(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(GetStreamHandle(stream))->lock) ? 0 : -1; +#else + return ftrylockfile(GetStreamHandle(stream)) == 0 ? 0 : -1; #endif } std::uint64_t APS5_VABI _Stoul_nid_postfix(const char* str, char** endptr, int base) { - if (StopAtBinaryPrefix_nid_no_patch(str, endptr, base)) return 0; - return std::strtoull(str, endptr, base); + if (StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; + return ParseInteger(str, endptr, base, std::strtoull); } void APS5_VABI _Locksyslock_nid_postfix() { @@ -146,4 +184,20 @@ void APS5_VABI _Unlocksyslock_nid_postfix() { g_sysLock.unlock(); } +__int128 APS5_VABI __divti3_nid_postfix(__int128 dividend, __int128 divisor) { + return SonyDivti3(dividend, divisor); +} + +unsigned __int128 APS5_VABI __udivti3_nid_postfix(unsigned __int128 dividend, unsigned __int128 divisor) { + return SonyUdivti3(dividend, divisor); +} + +unsigned __int128 APS5_VABI __umodti3_nid_postfix(unsigned __int128 dividend, unsigned __int128 divisor) { + return SonyUmodti3(dividend, divisor); +} + +__int128 APS5_VABI __modti3_nid_postfix(__int128 dividend, __int128 divisor) { + return SonyModti3(dividend, divisor); +} + } diff --git a/core/libs/prx/libc/src/ScanningWide.cpp b/core/libs/prx/libc/src/ScanningWide.cpp new file mode 100644 index 0000000000..0b95de6e78 --- /dev/null +++ b/core/libs/prx/libc/src/ScanningWide.cpp @@ -0,0 +1,587 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "SceTypes.hpp" +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libc/include/WindowsFormatting.hpp" + +#ifdef _WIN32 +#define APS5_VA_BEGIN(last) __builtin_sysv_va_list args; __builtin_sysv_va_start(args, last) +#define APS5_VA_END() __builtin_sysv_va_end(args) +#else +#define APS5_VA_BEGIN(last) std::va_list args; va_start(args, last) +#define APS5_VA_END() va_end(args) +#endif + +extern "C" int APS5_VABI iswspace_nid_postfix(std::uint32_t c); +extern "C" std::size_t APS5_VABI wcrtomb_nid_postfix(char* destination, std::uint16_t value, void* state); +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 { + +enum class Fit { Invalid, Prefix, Valid }; + +enum class Outcome { Done, Matching, Input, Violation }; + +struct Directive { + bool suppress = false; + std::size_t width = 0; + std::string length; + char16_t conversion = 0; + const char16_t* setBegin = nullptr; + const char16_t* setEnd = nullptr; + bool setExcluded = false; +}; + +bool IsSpace(char16_t character) { + return iswspace_nid_postfix(character) != 0; +} + +int DigitValue(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 36; +} + +bool IsDecimal(char character) { + return character >= '0' && character <= '9'; +} + +bool IsHex(char character) { + return DigitValue(character) < 16; +} + +char Lower(char character) { + return character >= 'A' && character <= 'Z' ? static_cast(character - 'A' + 'a') : character; +} + +bool StartsWord(std::string_view text, std::string_view word) { + if (text.size() > word.size()) return false; + for (std::size_t index = 0; index < text.size(); ++index) { + if (Lower(text[index]) != word[index]) return false; + } + return true; +} + +std::string_view Unsigned(std::string_view text) { + if (!text.empty() && (text[0] == '+' || text[0] == '-')) text.remove_prefix(1); + return text; +} + +Fit IntegerFit(std::string_view text, int base) { + text = Unsigned(text); + if (text.empty()) return Fit::Prefix; + int radix = base; + std::size_t index = 0; + if ((base == 0 || base == 16) && text[0] == '0' && text.size() >= 2 && (text[1] == 'x' || text[1] == 'X')) { + radix = 16; + index = 2; + if (index == text.size()) return Fit::Prefix; + } else if (base == 0) { + radix = text[0] == '0' ? 8 : 10; + } + for (; index < text.size(); ++index) { + if (DigitValue(text[index]) >= radix) return Fit::Invalid; + } + return Fit::Valid; +} + +Fit SpecialFloatFit(std::string_view text) { + if (StartsWord(text, "infinity")) return text.size() == 3 || text.size() == 8 ? Fit::Valid : Fit::Prefix; + if (StartsWord(text, "nan")) return text.size() == 3 ? Fit::Valid : Fit::Prefix; + if (text.size() < 4 || !StartsWord(text.substr(0, 3), "nan") || text[3] != '(') return Fit::Invalid; + for (std::size_t index = 4; index < text.size(); ++index) { + const char character = text[index]; + if (character == ')') return index + 1 == text.size() ? Fit::Valid : Fit::Invalid; + if (DigitValue(character) == 36 && character != '_') return Fit::Invalid; + } + return Fit::Prefix; +} + +Fit FloatFit(std::string_view text) { + text = Unsigned(text); + if (text.empty()) return Fit::Prefix; + if (Lower(text[0]) == 'i' || Lower(text[0]) == 'n') return SpecialFloatFit(text); + const bool hex = text.size() >= 2 && text[0] == '0' && (text[1] == 'x' || text[1] == 'X'); + const auto digit = [hex](char character) { return hex ? IsHex(character) : IsDecimal(character); }; + std::size_t index = hex ? 2 : 0; + std::size_t digits = 0; + for (; index < text.size() && digit(text[index]); ++index) ++digits; + if (index < text.size() && text[index] == '.') { + for (++index; index < text.size() && digit(text[index]); ++index) ++digits; + } + if (index == text.size()) return digits != 0 ? Fit::Valid : Fit::Prefix; + 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; + 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; + WideInput input; + const char16_t* format; + LibcDetail::FormatArguments args; + int assigned = 0; + bool converted = false; + +public: + 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)) { + while (IsSpace(*format)) ++format; + SkipSpace(); + } else if (*format != u'%') { + outcome = Literal(*format++); + } else { + ++format; + outcome = Conversion(); + } + if (outcome == Outcome::Matching) return assigned; + if (outcome == Outcome::Input) return converted ? assigned : EOF; + if (outcome == Outcome::Violation) return EOF; + } + return assigned; + } + + [[noreturn]] void Fail(const std::string& message) const { + throw std::invalid_argument(std::string(name) + ": " + message); + } + + std::size_t IntegerSize(const std::string& length) const { + if (length.empty()) return sizeof(std::int32_t); + if (length == "hh") return sizeof(std::int8_t); + if (length == "h") return sizeof(std::int16_t); + if (length == "l" || length == "ll" || length == "j" || length == "z" || length == "t") return sizeof(std::int64_t); + Fail("invalid integer length modifier " + length); + } + + void SkipSpace() { + while (input.Peek() >= 0 && IsSpace(static_cast(input.Peek()))) input.Advance(); + } + + Outcome Literal(char16_t expected) { + if (input.Peek() < 0) return Outcome::Input; + if (input.Peek() != expected) return Outcome::Matching; + input.Advance(); + return Outcome::Done; + } + + Directive Parse() { + Directive directive; + if (*format == u'*') { + directive.suppress = true; + ++format; + } + const char16_t* digits = format; + for (; *format >= u'0' && *format <= u'9'; ++format) { + const std::size_t digit = static_cast(*format - u'0'); + const std::size_t limit = (std::numeric_limits::max() - digit) / 10; + directive.width = directive.width > limit ? std::numeric_limits::max() : directive.width * 10 + digit; + } + if (format != digits && directive.width == 0) Fail("zero field width"); + if (*format == u'h' || *format == u'l') { + directive.length += static_cast(*format++); + if (*format == directive.length[0]) directive.length += static_cast(*format++); + } else if (*format == u'j' || *format == u'z' || *format == u't' || *format == u'L') { + directive.length += static_cast(*format++); + } + directive.conversion = *format; + if (directive.conversion == 0) Fail("incomplete conversion specification"); + ++format; + if (directive.conversion == u'[') { + directive.setExcluded = *format == u'^'; + if (directive.setExcluded) ++format; + directive.setBegin = format; + if (*format == u']') ++format; + while (*format != 0 && *format != u']') ++format; + if (*format == 0) Fail("unterminated scanset"); + directive.setEnd = format++; + for (const char16_t* member = directive.setBegin + 1; member + 1 < directive.setEnd; ++member) { + if (*member == u'-') Fail("scanset range is implementation-defined"); + } + } + return directive; + } + + Outcome Conversion() { + if (*format == u'%') { + ++format; + SkipSpace(); + return Literal(u'%'); + } + const Directive directive = Parse(); + switch (directive.conversion) { + case u'd': return Integer(directive, 10, true); + case u'i': return Integer(directive, 0, true); + case u'o': return Integer(directive, 8, false); + 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': + case u'c': + case u'[': return Text(directive); + case u'n': return Count(directive); + default: Fail("unsupported conversion " + std::to_string(directive.conversion)); + } + } + + template + 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 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) { + 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.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; + 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); + ++assigned; + } + converted = true; + 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") + Fail("invalid floating length modifier " + directive.length); + void* target = nullptr; + if (!directive.suppress && (target = args.Next()) == nullptr) return Outcome::Violation; + SkipSpace(); + if (input.Peek() < 0) return Outcome::Input; + const std::string field = Field(directive.width, FloatFit); + if (FloatFit(field) != Fit::Valid) return Outcome::Matching; + if (target != nullptr) { + if (directive.length == "L") { + const long double value = std::strtold(field.c_str(), nullptr); + std::memcpy(target, &value, sizeof(value)); + } else if (directive.length == "l") { + const double value = std::strtod(field.c_str(), nullptr); + std::memcpy(target, &value, sizeof(value)); + } else { + const float value = std::strtof(field.c_str(), nullptr); + std::memcpy(target, &value, sizeof(value)); + } + ++assigned; + } + converted = true; + return Outcome::Done; + } + + static bool InSet(const Directive& directive, char16_t character) { + bool found = false; + for (const char16_t* member = directive.setBegin; member != directive.setEnd && !found; ++member) found = *member == character; + return found != directive.setExcluded; + } + + static Outcome Truncated(void* target, std::size_t capacity, bool wide) { + if (capacity != 0) { + if (wide) static_cast(target)[0] = 0; + else static_cast(target)[0] = 0; + } + return Outcome::Matching; + } + + Outcome Text(const Directive& directive) { + if (!directive.length.empty() && directive.length != "l") + Fail("invalid string length modifier " + directive.length); + void* target = nullptr; + std::size_t capacity = 0; + if (!directive.suppress) { + target = args.Next(); + if (sized) capacity = args.Next(); + if (target == nullptr) return Outcome::Violation; + } + if (directive.conversion == u's') SkipSpace(); + 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.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.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; + } + if (target == nullptr) { + converted = true; + return Outcome::Done; + } + const std::size_t terminator = directive.conversion == u'c' ? 0 : 1; + if (directive.length == "l") { + if (sized && item.size() + terminator > capacity) return Truncated(target, capacity, true); + std::memcpy(target, item.data(), item.size() * sizeof(char16_t)); + if (terminator != 0) static_cast(target)[item.size()] = 0; + } else { + std::string bytes; + for (const char16_t unit : item) { + char byte[8]; + const std::size_t written = wcrtomb_nid_postfix(byte, unit, nullptr); + if (written == static_cast(-1)) Fail("wide character has no single-byte form"); + bytes.append(byte, written); + } + if (sized && bytes.size() + terminator > capacity) return Truncated(target, capacity, false); + std::memcpy(target, bytes.data(), bytes.size()); + if (terminator != 0) static_cast(target)[bytes.size()] = 0; + } + ++assigned; + converted = true; + return Outcome::Done; + } + + Outcome Count(const Directive& directive) { + if (directive.suppress || directive.width != 0) Fail("%n with suppression or width"); + const std::size_t size = IntegerSize(directive.length); + void* target = args.Next(); + if (target == nullptr) return Outcome::Violation; + const std::int64_t count = input.Consumed(); + std::memcpy(target, &count, size); + return Outcome::Done; + } +}; + +} + +extern "C" { + +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, WideInput(buffer), format, args).Run(); + APS5_VA_END(); + return result; + } catch (...) { + APS5_VA_END(); + throw; + } +} + +int APS5_VABI swscanf_s_nid_postfix(const char16_t* buffer, const char16_t* format, ...) { + if (buffer == nullptr || format == nullptr) return EOF; + APS5_VA_BEGIN(format); + try { + const int result = WideScanner("swscanf_s", true, 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 (...) { + APS5_VA_END(); + throw; + } +} + +} diff --git a/core/libs/prx/libc/src/StandardStreams.cpp b/core/libs/prx/libc/src/StandardStreams.cpp index 856937ab4a..03cdaed4cd 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,43 +34,111 @@ 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(); 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) { errno = 86; - stream->SetEncodingError(); + 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(native); +#else + const int result = ::getc_unlocked(native); +#endif + stream->SyncUnlockedStatus(); + 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 + const int result = ::putc_unlocked(value, GetNativeStream(stream)); +#endif + stream->SyncUnlockedStatus(); + return result; +} 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) { - auto* result = std::fgets(buffer, size, GetNativeStream(stream)); + if (stream && !stream->IsOpen()) { errno = 9; return nullptr; } + if (size <= 0) { + stream->SetError(); + errno = 22; + return nullptr; + } + auto* native = GetNativeStream(stream); + if (!stream->SwitchToReading()) return nullptr; + auto* result = std::fgets(buffer, size, native); stream->SyncStatus(); return result; } @@ -63,10 +150,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 @@ -75,14 +173,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 0b35feabc6..28b5f7b33b 100644 --- a/core/libs/prx/libc/src/StdioExtras.cpp +++ b/core/libs/prx/libc/src/StdioExtras.cpp @@ -1,9 +1,11 @@ +#include #include #include #include #include #include #include +#include #include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/ApplicationHeap.hpp" @@ -18,10 +20,40 @@ namespace { using GuestNewHandler = void (APS5_VABI*)(); -GuestNewHandler g_newHandler = nullptr; +std::atomic g_newHandler{nullptr}; void* Allocate(std::size_t size) { - return ApplicationHeapAllocate_nid_no_patch(size == 0 ? 1 : size); + for (;;) { + GuestNewHandler handler = nullptr; + try { + return ApplicationHeapAllocate_nid_no_patch(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(); + } + handler(); + } +} + +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(); + } } } @@ -52,19 +84,17 @@ int APS5_VABI sprintf_s_nid_postfix(char* buffer, size_t size, const char* forma // Only the "C" locale exists. const char* APS5_VABI setlocale_nid_postfix(int category, const char* locale) { - (void)category; + if (category < 0 || category > 6) return nullptr; if (locale == nullptr || locale[0] == 0 || std::strcmp(locale, "C") == 0 || std::strcmp(locale, "POSIX") == 0) return "C"; return nullptr; } GuestNewHandler APS5_VABI _ZSt15set_new_handlerPFvvE_nid_postfix(GuestNewHandler handler) { - const auto previous = g_newHandler; - g_newHandler = handler; - return previous; + return g_newHandler.exchange(handler); } GuestNewHandler APS5_VABI _ZSt15get_new_handlerv_nid_postfix() { - return g_newHandler; + return g_newHandler.load(); } unsigned char _ZSt7nothrow_nid_postfix = 0; @@ -77,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 7c486eceda..18b9cb6f18 100644 --- a/core/libs/prx/libc/src/StringExtensions.cpp +++ b/core/libs/prx/libc/src/StringExtensions.cpp @@ -3,24 +3,99 @@ #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)]; + std::memcpy(bytes, &address, sizeof(bytes)); + char* end = buffer; + for (std::size_t index = 0; index < sizeof(bytes); ++index) { + if (index != 0) *end++ = '.'; + const unsigned value = bytes[index]; + if (value >= 100) *end++ = static_cast('0' + value / 100); + if (value >= 10) *end++ = static_cast('0' + value / 10 % 10); + *end++ = static_cast('0' + value % 10); + } + *end = 0; + return buffer; +} + +char* APS5_VABI dirname_nid_postfix(const char* path) { + thread_local char buffer[1024]; + if (path == nullptr || *path == '\0') { + buffer[0] = '.'; + buffer[1] = '\0'; + return buffer; + } + + auto length = std::strlen(path); + while (length > 0 && path[length - 1] == '/') --length; + if (length == 0) { + buffer[0] = '/'; + buffer[1] = '\0'; + return buffer; + } + auto separator = length; + while (separator > 0 && path[separator - 1] != '/') --separator; + if (separator == 0) { + buffer[0] = '.'; + buffer[1] = '\0'; + return buffer; + } + while (separator > 1 && path[separator - 1] == '/') --separator; + if (separator >= sizeof(buffer)) { + errno = 63; + return nullptr; + } + std::memcpy(buffer, path, separator); + buffer[separator] = '\0'; + return buffer; +} + std::size_t APS5_VABI strnlen_nid_postfix(const char* text, std::size_t limit) { std::size_t length = 0; while (length < limit && text[length] != '\0') ++length; @@ -31,6 +106,19 @@ std::size_t APS5_VABI strnlen_s_nid_postfix(const char* text, std::size_t limit) return text ? strnlen_nid_postfix(text, limit) : 0; } +char* APS5_VABI strsep_nid_postfix(char** text, const char* delimiters) { + char* token = *text; + if (token == nullptr) return nullptr; + char* end = token + std::strcspn(token, delimiters); + if (*end == '\0') { + *text = nullptr; + } else { + *end = '\0'; + *text = end + 1; + } + return token; +} + char* APS5_VABI strncat_nid_postfix(char* destination, const char* source, std::size_t limit) { return std::strncat(destination, source, limit); } @@ -66,11 +154,12 @@ char* APS5_VABI strtok_r_nid_postfix(char* text, const char* delimiters, char** if (text == nullptr) return nullptr; text += std::strspn(text, delimiters); if (*text == '\0') { - *state = text; + *state = nullptr; return nullptr; } char* end = text + std::strcspn(text, delimiters); if (*end != '\0') *end++ = '\0'; + else end = nullptr; *state = end; return text; } diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index f60b7ea24e..2dc4ee0028 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -6,9 +7,12 @@ #include #include #include +#include #include +#include #include "prx/libc/include/General.hpp" +#include "prx/libc/include/SonyCrt.hpp" namespace { @@ -35,6 +39,132 @@ std::string AsciiPrefix(const char16_t* text) { return prefix; } +struct NanSubject { + const char* tail; + bool negative; +}; + +NanSubject FindNanSubject(const char* text) { + while (*text == ' ' || (*text >= '\t' && *text <= '\r')) ++text; + 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}; +} + +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; +} + +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); +} + +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 auto ParseAsciiPrefix(const char16_t* text, char16_t** end, TParse parse) { const std::string prefix = AsciiPrefix(text); @@ -44,6 +174,22 @@ auto ParseAsciiPrefix(const char16_t* text, char16_t** end, TParse parse) { return value; } +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} : value; +} + } extern "C" { @@ -57,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); } @@ -73,6 +223,42 @@ const void* APS5_VABI memchr_nid_postfix(const void* s, int c, size_t n) { return std::memchr(s, c, n); } +void* APS5_VABI memrchr_nid_postfix(const void* s, int c, size_t n) { + const auto* bytes = static_cast(s); + const auto value = static_cast(c); + while (n != 0) { + --n; + if (bytes[n] == value) return const_cast(bytes + 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); } @@ -101,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) { @@ -119,48 +317,67 @@ size_t APS5_VABI strlcpy_nid_postfix(char* dest, const char* src, size_t size) { return srcLen; } -bool StopAtBinaryPrefix_nid_no_patch(const char* str, char** endptr, int base) { - if (base != 0 && base != 2) return false; +bool StopAtPrefixLetter_nid_no_patch(const char* str, char** endptr, int base) { + if (base != 0 && base != 2 && base != 16) return false; while (*str == ' ' || (*str >= '\t' && *str <= '\r')) ++str; if (*str == '+' || *str == '-') ++str; - if (str[0] != '0' || (str[1] != 'b' && str[1] != 'B') || (str[2] != '0' && str[2] != '1')) return false; + if (str[0] != '0' || str[1] == '\0') return false; + const char next = str[2]; + const bool binary = base != 16 && (str[1] == 'b' || str[1] == 'B') && (next == '0' || next == '1'); + 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); return true; } std::int64_t APS5_VABI strtol_nid_postfix(const char* str, char** endptr, int base) { - if (StopAtBinaryPrefix_nid_no_patch(str, endptr, base)) return 0; - return std::strtoll(str, endptr, base); + if (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 (StopAtBinaryPrefix_nid_no_patch(str, endptr, base)) return 0; - return std::strtoull(str, endptr, base); + if (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 (StopAtBinaryPrefix_nid_no_patch(str, endptr, base)) return 0; - return std::strtoll(str, endptr, base); + if (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 (StopAtBinaryPrefix_nid_no_patch(str, endptr, base)) return 0; - return std::strtoull(str, endptr, base); + if (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) { - return std::strtod(str, endptr); + return ParseFloat(str, endptr, std::strtod); } -double APS5_VABI atof_nid_postfix(const char* str) { return std::atof(str); } -float APS5_VABI strtof_nid_postfix(const char* str, char** endptr) { return std::strtof(str, endptr); } +double APS5_VABI atof_nid_postfix(const char* str) { return strtod_nid_postfix(str, nullptr); } +float APS5_VABI strtof_nid_postfix(const char* str, char** endptr) { + return ParseFloat(str, endptr, std::strtof); +} long double APS5_VABI strtold_nid_postfix(const char* str, char** endptr) { static_assert(sizeof(long double) == 16, "Guest long double requires x87 extended precision storage"); - return std::strtold(str, endptr); + return ParseFloat(str, endptr, std::strtold); } int APS5_VABI atoi_nid_postfix(const char* str) { - return std::atoi(str); + return static_cast(strtol_nid_postfix(str, nullptr, 10)); +} + +std::int64_t APS5_VABI atol_nid_postfix(const char* str) { + return ParseInteger(str, nullptr, 10, std::strtoll); +} + +long long APS5_VABI atoll_nid_postfix(const char* str) { + return ParseInteger(str, nullptr, 10, std::strtoll); } std::div_t APS5_VABI div_nid_postfix(int numerator, int denominator) { @@ -197,24 +414,21 @@ char16_t* APS5_VABI wmemmove_nid_postfix(char16_t* dest, const char16_t* src, si extern "C" { int APS5_VABI strcasecmp_nid_postfix(const char* s1, const char* s2) { - while (*s1 && *s2) { - unsigned char a = static_cast(std::tolower(static_cast(*s1))); - unsigned char b = static_cast(std::tolower(static_cast(*s2))); - if (a != b) return a - b; - ++s1; ++s2; + for (;; ++s1, ++s2) { + const int a = std::tolower(static_cast(*s1)); + const int b = std::tolower(static_cast(*s2)); + if (a != b || *s1 == 0) return a - b; } - return static_cast(*s1) - static_cast(*s2); } int APS5_VABI strncasecmp_nid_postfix(const char* s1, const char* s2, size_t n) { - while (n && *s1 && *s2) { - unsigned char a = static_cast(std::tolower(static_cast(*s1))); - unsigned char b = static_cast(std::tolower(static_cast(*s2))); + for (; n != 0; ++s1, ++s2, --n) { + const int a = std::tolower(static_cast(*s1)); + const int b = std::tolower(static_cast(*s2)); if (a != b) return a - b; - ++s1; ++s2; --n; + if (*s1 == 0) break; } - if (!n) return 0; - return static_cast(*s1) - static_cast(*s2); + return 0; } int APS5_VABI bcmp_nid_postfix(const void* s1, const void* s2, size_t n) { @@ -232,7 +446,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; @@ -255,7 +471,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) { @@ -280,7 +498,9 @@ int APS5_VABI strcat_s_nid_postfix(char* dest, size_t destsz, const char* src) { int APS5_VABI memcpy_s_nid_postfix(void* dest, size_t destsz, const void* src, size_t count) { constexpr int GuestEinval = 22; constexpr int GuestErange = 34; + constexpr size_t RsizeMax = SIZE_MAX >> 1; if (!dest) return GuestEinval; + if (destsz > RsizeMax) return GuestErange; if (!src || count > destsz) { std::memset(dest, 0, destsz); return src ? GuestErange : GuestEinval; @@ -299,7 +519,9 @@ int APS5_VABI memcpy_s_nid_postfix(void* dest, size_t destsz, const void* src, s int APS5_VABI memmove_s_nid_postfix(void* dest, size_t destsz, const void* src, size_t count) { constexpr int GuestEinval = 22; constexpr int GuestErange = 34; + constexpr size_t RsizeMax = SIZE_MAX >> 1; if (!dest) return GuestEinval; + if (destsz > RsizeMax) return GuestErange; if (!src || count > destsz) { std::memset(dest, 0, destsz); return src ? GuestErange : GuestEinval; @@ -310,12 +532,12 @@ int APS5_VABI memmove_s_nid_postfix(void* dest, size_t destsz, const void* src, int APS5_VABI memset_s_nid_postfix(void* dest, size_t destsz, int value, size_t count) { constexpr int GuestEinval = 22; - constexpr int GuestErange = 34; - if (!dest) return GuestEinval; + constexpr size_t RsizeMax = SIZE_MAX >> 1; + if (!dest || destsz > RsizeMax || count > RsizeMax) return GuestEinval; const auto length = count > destsz ? destsz : count; auto* bytes = static_cast(dest); for (size_t index = 0; index < length; ++index) bytes[index] = static_cast(value); - return count > destsz ? GuestErange : 0; + return count > destsz ? GuestEinval : 0; } char* APS5_VABI strnstr_nid_postfix(const char* haystack, const char* needle, size_t length) { @@ -329,14 +551,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 (StopAtBinaryPrefix_nid_no_patch(str, endptr, base)) return 0; - return std::strtoull(str, endptr, base); + if (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; @@ -357,6 +586,11 @@ char16_t* APS5_VABI wcscpy_nid_postfix(char16_t* dest, const char16_t* src) { return dest; } +char16_t* APS5_VABI wcscat_nid_postfix(char16_t* dest, const char16_t* src) { + std::memcpy(dest + Length(dest), src, (Length(src) + 1) * sizeof(char16_t)); + return dest; +} + char16_t* APS5_VABI wcsncpy_nid_postfix(char16_t* dest, const char16_t* src, size_t n) { size_t index = 0; for (; index < n && src[index] != 0; ++index) dest[index] = src[index]; @@ -403,39 +637,77 @@ 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; + while (*text != 0 && Contains(delimiters, *text)) ++text; + if (*text == 0) { + *last = nullptr; + return nullptr; + } + char16_t* end = text; + while (*end != 0 && !Contains(delimiters, *end)) ++end; + if (*end == 0) { + *last = nullptr; + } else { + *end = 0; + *last = end + 1; + } + return text; +} + char16_t* APS5_VABI wmemset_nid_postfix(char16_t* s, char16_t c, size_t n) { for (size_t index = 0; index < n; ++index) s[index] = c; return s; } double APS5_VABI wcstod_nid_postfix(const char16_t* str, char16_t** endptr) { - return ParseAsciiPrefix(str, endptr, [](const char* text, char** end) { return std::strtod(text, end); }); + return ParseAsciiPrefix(str, endptr, [](const char* text, char** end) { return ParseFloat(text, end, std::strtod); }); } float APS5_VABI wcstof_nid_postfix(const char16_t* str, char16_t** endptr) { - return ParseAsciiPrefix(str, endptr, [](const char* text, char** end) { return std::strtof(text, end); }); + return ParseAsciiPrefix(str, endptr, [](const char* text, char** end) { return ParseFloat(text, end, std::strtof); }); } long double APS5_VABI wcstold_nid_postfix(const char16_t* str, char16_t** endptr) { static_assert(sizeof(long double) == 16); static_assert(std::numeric_limits::digits == 64); - return ParseAsciiPrefix(str, endptr, [](const char* text, char** end) { return std::strtold(text, end); }); + 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 StopAtBinaryPrefix_nid_no_patch(text, end, base) ? 0LL : std::strtoll(text, end, base); }); + return ParseAsciiPrefix(str, endptr, [base](const char* text, char** end) { return StopAtPrefixLetter_nid_no_patch(text, end, base) ? 0LL : ParseInteger(text, end, base, std::strtoll); }); } 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 StopAtBinaryPrefix_nid_no_patch(text, end, base) ? 0LL : std::strtoll(text, end, base); }); + return ParseAsciiPrefix(str, endptr, [base](const char* text, char** end) { return StopAtPrefixLetter_nid_no_patch(text, end, base) ? 0LL : ParseInteger(text, end, base, std::strtoll); }); } 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 StopAtBinaryPrefix_nid_no_patch(text, end, base) ? 0ULL : std::strtoull(text, end, base); }); + return ParseAsciiPrefix(str, endptr, [base](const char* text, char** end) { return 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 StopAtBinaryPrefix_nid_no_patch(text, end, base) ? 0ULL : std::strtoull(text, end, base); }); + return ParseAsciiPrefix(str, endptr, [base](const char* text, char** end) { return StopAtPrefixLetter_nid_no_patch(text, end, base) ? 0ULL : ParseInteger(text, end, base, std::strtoull); }); +} + +long long APS5_VABI wcstoimax_nid_postfix(const char16_t* str, char16_t** endptr, int base) { + return wcstoll_nid_postfix(str, endptr, base); +} + +unsigned long long APS5_VABI wcstoumax_nid_postfix(const char16_t* str, char16_t** endptr, int base) { + return wcstoull_nid_postfix(str, endptr, base); +} + +std::uint64_t APS5_VABI _WStoul_nid_postfix(const char16_t* str, char16_t** endptr, int base) { + if (base < 0 || base == 1 || base > 36) throw std::invalid_argument("_WStoul: unsupported base " + std::to_string(base)); + return wcstoul_nid_postfix(str, endptr, base); } int APS5_VABI wcscoll_nid_postfix(const char16_t* first, const char16_t* second) { @@ -444,12 +716,125 @@ int APS5_VABI wcscoll_nid_postfix(const char16_t* first, const char16_t* second) size_t APS5_VABI wcsxfrm_nid_postfix(char16_t* destination, const char16_t* source, size_t count) { const size_t length = Length(source); - if (length < count) std::memcpy(destination, source, (length + 1) * sizeof(char16_t)); + if (count != 0) { + const size_t copied = length < count ? length : count - 1; + std::memcpy(destination, source, copied * sizeof(char16_t)); + destination[copied] = 0; + } return length; } size_t APS5_VABI strxfrm_nid_postfix(char* destination, const char* source, size_t count) { - return std::strxfrm(destination, source, count); + return strlcpy_nid_postfix(destination, source, count); +} + +char* APS5_VABI strtok_s_nid_postfix(char* str, const char* delim, char** context) { + 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/Syslog.cpp b/core/libs/prx/libc/src/Syslog.cpp index c7cc00b3eb..a94f34e141 100644 --- a/core/libs/prx/libc/src/Syslog.cpp +++ b/core/libs/prx/libc/src/Syslog.cpp @@ -4,14 +4,19 @@ #include #include #include +#include #include #ifdef _WIN32 #include "prx/libc/include/WindowsFormatting.hpp" +#include +#else +#include #endif extern "C" { int* APS5_VABI __error_nid_postfix(); int APS5_VABI strerror_r_nid_postfix(int error, char* buffer, std::size_t length); +int APS5_VABI vsnprintf_nid_postfix(char* buffer, std::size_t size, const char* format, VaList* args); } namespace { @@ -19,6 +24,11 @@ constexpr int priorityMask = 0x07; constexpr int facilityMask = 0x3f8; constexpr int userFacility = 1 << 3; constexpr int internalLogPriority = 3 | 0x02 | 0x20 | 0x01; +constexpr int logPid = 0x01; +int logMask = 0xff; +int logOptions = 0; +int logFacility = userFacility; +std::optional logIdentity; std::mutex& OutputMutex() { static std::mutex mutex; @@ -26,8 +36,17 @@ std::mutex& OutputMutex() { } void Emit(int priority, const std::string& message) { - std::lock_guard lock(OutputMutex()); - std::fprintf(stderr, "[syslog:%d] %s", priority, message.c_str()); + std::fprintf(stderr, "[syslog:%d] ", priority); + if (logIdentity) std::fputs(logIdentity->c_str(), stderr); + if (logOptions & logPid) { +#ifdef _WIN32 + std::fprintf(stderr, "[%d]", ::_getpid()); +#else + std::fprintf(stderr, "[%d]", static_cast(::getpid())); +#endif + } + if (logIdentity) std::fputs(": ", stderr); + std::fputs(message.c_str(), stderr); if (message.empty() || message.back() != '\n') std::fputc('\n', stderr); std::fflush(stderr); } @@ -59,15 +78,10 @@ std::string Format(const char* format, VaList* args) { LibcDetail::FormatWindows(nullptr, 0, format, args, &buffer); return buffer; #else - std::va_list copy; - va_copy(copy, *reinterpret_cast(args)); - const int size = std::vsnprintf(nullptr, 0, format, copy); - va_end(copy); + const int size = vsnprintf_nid_postfix(nullptr, 0, format, args); if (size <= 0) return {}; std::string buffer(static_cast(size) + 1, '\0'); - va_copy(copy, *reinterpret_cast(args)); - std::vsnprintf(buffer.data(), buffer.size(), format, copy); - va_end(copy); + vsnprintf_nid_postfix(buffer.data(), buffer.size(), format, args); buffer.resize(static_cast(size)); return buffer; #endif @@ -75,16 +89,40 @@ std::string Format(const char* format, VaList* args) { } extern "C" { +void APS5_VABI openlog_nid_postfix(const char* identity, int options, int facility) { + std::lock_guard lock(OutputMutex()); + if (identity) logIdentity = identity; + logOptions = options; + if (facility != 0 && (facility & ~facilityMask) == 0) logFacility = facility; +} + +void APS5_VABI closelog_nid_postfix() { + std::lock_guard lock(OutputMutex()); + logIdentity.reset(); +} + +int APS5_VABI setlogmask_nid_postfix(int mask) { + std::lock_guard lock(OutputMutex()); + const int previous = logMask; + if (mask != 0) logMask = mask; + return previous; +} + void APS5_VABI vsyslog_nid_postfix(int priority, const char* format, VaList* args) { const int saved = *__error_nid_postfix(); + std::lock_guard lock(OutputMutex()); if (priority & ~(priorityMask | facilityMask)) { - char complaint[64]; - std::snprintf(complaint, sizeof(complaint), "syslog: unknown facility/priority: %x", priority); - Emit(internalLogPriority, complaint); + if (logMask & (1 << (internalLogPriority & priorityMask))) { + char complaint[64]; + std::snprintf(complaint, sizeof(complaint), "syslog: unknown facility/priority: %x", priority); + Emit(internalLogPriority, complaint); + } priority &= priorityMask | facilityMask; } - if ((priority & facilityMask) == 0) priority |= userFacility; - Emit(priority, format ? Format(ExpandErrorText(format, saved).c_str(), args) : std::string()); + if (logMask & (1 << (priority & priorityMask))) { + if ((priority & facilityMask) == 0) priority |= logFacility; + Emit(priority, format ? Format(ExpandErrorText(format, saved).c_str(), args) : std::string()); + } *__error_nid_postfix() = saved; } diff --git a/core/libs/prx/libc/src/Threads.cpp b/core/libs/prx/libc/src/Threads.cpp new file mode 100644 index 0000000000..3373def131 --- /dev/null +++ b/core/libs/prx/libc/src/Threads.cpp @@ -0,0 +1,501 @@ +#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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif + +namespace { + +struct Xtime { + std::int64_t sec; + std::int64_t nsec; +}; + +constexpr std::int64_t nanosPerSecond = 1000000000; +constexpr std::int64_t longestSleepSeconds = 86400; + +timespec XtimeDeadline(const Xtime& time, const char* function) { + constexpr auto largest = std::numeric_limits::max(); + constexpr auto smallest = std::numeric_limits::min(); + const auto carry = time.nsec / nanosPerSecond; + auto nanos = time.nsec % nanosPerSecond; + auto seconds = time.sec; + if ((carry > 0 && seconds > largest - carry) || (carry < 0 && seconds < smallest - carry)) + throw std::overflow_error(std::string(function) + ": xtime seconds overflow"); + seconds += carry; + if (nanos < 0) { + if (seconds == smallest) + throw std::overflow_error(std::string(function) + ": xtime seconds overflow"); + nanos += nanosPerSecond; + --seconds; + } + return {static_cast(seconds), static_cast(nanos)}; +} + +constexpr int threadSuccess = 0; +constexpr int threadNomem = 1; +constexpr int threadTimedout = 2; +constexpr int threadBusy = 3; +constexpr int threadError = 4; + +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{}; + DinkumwareNativeMutex native; +}; + +struct DinkumwareCondition { + std::uint64_t tag = conditionTag; + 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"); + auto* mutex = static_cast(*handle); + if (!mutex) + throw std::invalid_argument(std::string(function) + ": null mutex handle"); + if (mutex->tag != mutexTag) + throw std::invalid_argument(std::string(function) + ": not a mutex created by _Mtx_init"); + return mutex; +} + +DinkumwareCondition* ResolveCondition(void** handle, const char* function) { + if (!handle) + throw std::invalid_argument(std::string(function) + ": null condition handle pointer"); + auto* condition = static_cast(*handle); + if (!condition) + throw std::invalid_argument(std::string(function) + ": null condition handle"); + if (condition->tag != conditionTag) + throw std::invalid_argument(std::string(function) + ": not a condition created by _Cnd_init"); + return condition; +} + +bool IsOwnedByCaller(const DinkumwareMutex* mutex) { + return mutex->owner.load(std::memory_order_acquire) == std::this_thread::get_id() && mutex->count > 0; +} + +} + +extern "C" { + +int APS5_VABI _Mtx_init_nid_postfix(void** handle, int type) { + if (!handle) + throw std::invalid_argument("_Mtx_init: null handle pointer"); + *handle = nullptr; + auto* mutex = new (std::nothrow) DinkumwareMutex(); + if (!mutex) + return threadNomem; + mutex->type = type; + *handle = mutex; + return threadSuccess; +} + +int APS5_VABI _Mtx_init_with_name_nid_postfix(void** handle, int type, const char*) { + return _Mtx_init_nid_postfix(handle, type); +} + +void APS5_VABI _Mtx_destroy_nid_postfix(void** handle) { + if (!handle) + throw std::invalid_argument("_Mtx_destroy: null mutex handle pointer"); + if (!*handle) + return; + auto* mutex = ResolveMutex(handle, "_Mtx_destroy"); + if (mutex->count != 0) + throw std::logic_error("_Mtx_destroy: mutex destroyed while locked"); + mutex->tag = 0; + delete mutex; + *handle = nullptr; +} + +int APS5_VABI _Mtx_lock_nid_postfix(void** handle) { + auto* mutex = ResolveMutex(handle, "_Mtx_lock"); + const auto self = std::this_thread::get_id(); + if (mutex->owner.load(std::memory_order_acquire) == self) { + if ((mutex->type & mutexRecursive) == 0) + return threadBusy; + if (mutex->count == std::numeric_limits::max()) + throw std::overflow_error("_Mtx_lock: recursive lock count overflow"); + ++mutex->count; + return threadSuccess; + } + try { + mutex->native.lock(); + } catch (const std::system_error& error) { + return error.code() == std::errc::resource_deadlock_would_occur ? threadBusy : threadError; + } + mutex->owner.store(self, std::memory_order_release); + mutex->count = 1; + return threadSuccess; +} + +int APS5_VABI _Mtx_trylock_nid_postfix(void** handle) { + auto* mutex = ResolveMutex(handle, "_Mtx_trylock"); + const auto self = std::this_thread::get_id(); + if (mutex->owner.load(std::memory_order_acquire) == self) { + if ((mutex->type & mutexRecursive) == 0) + return threadBusy; + if (mutex->count == std::numeric_limits::max()) + throw std::overflow_error("_Mtx_trylock: recursive lock count overflow"); + ++mutex->count; + return threadSuccess; + } + if (!mutex->native.try_lock()) + return threadBusy; + mutex->owner.store(self, std::memory_order_release); + mutex->count = 1; + return threadSuccess; +} + +int APS5_VABI _Mtx_unlock_nid_postfix(void** handle) { + auto* mutex = ResolveMutex(handle, "_Mtx_unlock"); + if (!IsOwnedByCaller(mutex)) + return threadSuccess; + if (--mutex->count == 0) { + mutex->owner.store(std::thread::id{}, std::memory_order_release); + mutex->native.unlock(); + } + return threadSuccess; +} + +int APS5_VABI _Cnd_init_nid_postfix(void** handle) { + if (!handle) + throw std::invalid_argument("_Cnd_init: null handle pointer"); + *handle = nullptr; + auto* condition = new (std::nothrow) DinkumwareCondition(); + if (!condition) + return threadNomem; + *handle = condition; + 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"); + if (!*handle) + return; + auto* condition = ResolveCondition(handle, "_Cnd_destroy"); + condition->tag = 0; + delete condition; + *handle = nullptr; +} + +int APS5_VABI _Cnd_wait_nid_postfix(void** conditionHandle, void** mutexHandle) { + auto* condition = ResolveCondition(conditionHandle, "_Cnd_wait"); + auto* mutex = ResolveMutex(mutexHandle, "_Cnd_wait"); + if (!IsOwnedByCaller(mutex)) + return threadSuccess; + if (mutex->count != 1) + return threadSuccess; + mutex->count = 0; + mutex->owner.store(std::thread::id{}, std::memory_order_release); + std::unique_lock lock(mutex->native, std::adopt_lock); + 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); + mutex->count = 1; + throw; + } + lock.release(); + mutex->owner.store(std::this_thread::get_id(), std::memory_order_release); + mutex->count = 1; + return threadSuccess; +} + +int APS5_VABI _Cnd_timedwait_nid_postfix(void** conditionHandle, void** mutexHandle, const Xtime* target) { + if (!target) + APS5_INVALID_ARG_EX; + const auto deadline = XtimeDeadline(*target, "_Cnd_timedwait"); + constexpr auto maxDuration = std::chrono::system_clock::duration::max(); + constexpr auto minDuration = std::chrono::system_clock::duration::min(); + constexpr auto maxSeconds = std::chrono::duration_cast(maxDuration).count(); + constexpr auto minSeconds = std::chrono::duration_cast(minDuration).count(); + constexpr auto maxNanos = std::chrono::duration_cast(maxDuration - std::chrono::seconds(maxSeconds)).count(); + if (deadline.tv_sec > maxSeconds || (deadline.tv_sec == maxSeconds && deadline.tv_nsec > maxNanos) || + deadline.tv_sec < minSeconds) + throw std::overflow_error("_Cnd_timedwait: xtime seconds overflow"); + auto* condition = ResolveCondition(conditionHandle, "_Cnd_timedwait"); + auto* mutex = ResolveMutex(mutexHandle, "_Cnd_timedwait"); + if (!IsOwnedByCaller(mutex)) + return threadSuccess; + if (mutex->count != 1) + return threadSuccess; + mutex->count = 0; + mutex->owner.store(std::thread::id{}, std::memory_order_release); + std::unique_lock lock(mutex->native, std::adopt_lock); + bool timeout = false; + try { + const auto deadlineTime = std::chrono::system_clock::time_point( + std::chrono::seconds(deadline.tv_sec) + + std::chrono::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); + mutex->count = 1; + throw; + } + lock.release(); + mutex->owner.store(std::this_thread::get_id(), std::memory_order_release); + mutex->count = 1; + return timeout ? threadTimedout : threadSuccess; +} + +int APS5_VABI _Cnd_broadcast_nid_postfix(void** handle) { +#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; +} + +int APS5_VABI _Thrd_sleep_nid_postfix(const Xtime* target, Xtime*) { + if (!target) + APS5_INVALID_ARG_EX; + const auto deadline = XtimeDeadline(*target, "_Thrd_sleep"); + for (;;) { + timespec now{}; + if (clock_gettime(CLOCK_REALTIME, &now) != 0) + throw std::runtime_error("_Thrd_sleep: reading the realtime clock failed"); + if (now.tv_sec > deadline.tv_sec || (now.tv_sec == deadline.tv_sec && now.tv_nsec >= deadline.tv_nsec)) + return threadSuccess; + timespec remaining{static_cast(deadline.tv_sec - now.tv_sec), static_cast(deadline.tv_nsec - now.tv_nsec)}; + if (remaining.tv_nsec < 0) { + remaining.tv_nsec += nanosPerSecond; + --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 + } +} + + +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); +} + +} + diff --git a/core/libs/prx/libc/src/Time.cpp b/core/libs/prx/libc/src/Time.cpp index d56ded72c5..c3499f8047 100644 --- a/core/libs/prx/libc/src/Time.cpp +++ b/core/libs/prx/libc/src/Time.cpp @@ -1,8 +1,15 @@ #include #include +#include #include #include +#include #include +#include +#include +#include +#include +#include #ifdef _WIN32 #include @@ -10,11 +17,64 @@ #include "prx/libc/include/General.hpp" #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"}; +constexpr const char* MONTH_NAMES[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; +constexpr std::size_t ASCTIME_SIZE = sizeof("Sun Jan-2147483648 -2147483648:-2147483648:-2147483648 -2147481748\n"); constexpr std::int64_t SECONDS_PER_DAY = 86400; constexpr std::int64_t DAYS_FROM_MARCH_TO_YEAR_END = 306; +#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; + const std::int64_t year = std::int64_t{time.tm_year} + 1900 + years - (month <= 2 ? 1 : 0); + const std::int64_t era = calendarCycles(year, CALENDAR_CYCLE_YEARS); + const std::int64_t yearOfEra = year - era * CALENDAR_CYCLE_YEARS; + const std::int64_t dayOfYear = (153 * (month > 2 ? month - 3 : month + 9) + 2) / 5; + const std::int64_t dayOfEra = yearOfEra * 365 + yearOfEra / 4 - yearOfEra / 100 + dayOfYear; + const std::int64_t days = era * 146097 + dayOfEra - 719468 + time.tm_mday - 1; + return days * SECONDS_PER_DAY + std::int64_t{time.tm_hour} * 3600 + std::int64_t{time.tm_min} * 60 + time.tm_sec; +} +#endif void copyCalendarFields(const std::tm& host, GuestTm& guest) { guest.tm_sec = host.tm_sec; @@ -83,10 +143,407 @@ bool utcCalendarFields(std::int64_t time, GuestTm& result) { } +#ifdef __linux__ +#include + +namespace { + +const bool timeZoneFixed = [] { + if (std::getenv("TZ") == nullptr) ::setenv("TZ", ":/etc/localtime", 0); + ::tzset(); + return true; +}(); + +} // namespace +#endif + +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"}; +constexpr int YEAR_BASE = 1900; + +enum class Padding { Default, Less, Space, Zero }; + +const char* paddingFormat(int width, Padding padding, bool spaced) { + static constexpr const char* formats[][4] = { + {"%02d", "%d", "%2d", "%02d"}, + {"%2d", "%d", "%2d", "%02d"}, + {"%03d", "%d", "%3d", "%03d"}, + {"%04d", "%d", "%4d", "%04d"}, + }; + const int row = width == 2 ? (spaced ? 1 : 0) : width == 3 ? 2 : 3; + return formats[row][static_cast(padding)]; +} + +bool isLeapYearSum(int year, int base) { + return isLeapYear(year % 400 + base % 400); +} + +struct FormatBuffer { + char* cursor; + char* limit; +}; + +void addText(FormatBuffer& buffer, const char* text) { + while (buffer.cursor < buffer.limit && (*buffer.cursor = *text++) != '\0') ++buffer.cursor; +} + +void addNumber(FormatBuffer& buffer, int value, const char* format) { + char text[16]; + std::snprintf(text, sizeof(text), format, value); + addText(buffer, text); +} + +void addYear(FormatBuffer& buffer, int year, int base, bool century, bool lastDigits) { + int trail = year % 100 + base % 100; + int lead = year / 100 + base / 100 + trail / 100; + trail %= 100; + if (trail < 0 && lead > 0) { + trail += 100; + --lead; + } else if (lead < 0 && trail > 0) { + trail -= 100; + ++lead; + } + if (century) { + if (lead == 0 && trail < 0) addText(buffer, "-0"); + else addNumber(buffer, lead, "%02d"); + } + if (lastDigits) addNumber(buffer, trail < 0 ? -trail : trail, "%02d"); +} + +const char* guestZoneName(const GuestTm& guest) { + if (guest.tm_zone != nullptr) return guest.tm_zone; + if (guest.tm_isdst < 0) return ""; +#ifdef _WIN32 + return _tzname[guest.tm_isdst != 0 ? 1 : 0]; +#else + return tzname[guest.tm_isdst != 0 ? 1 : 0]; +#endif +} + +void addIsoWeek(FormatBuffer& buffer, char conversion, Padding padding, const GuestTm& time) { + int base = YEAR_BASE; + int yday = time.tm_yday; + int week = 0; + for (;;) { + const int length = isLeapYearSum(time.tm_year, base) ? 366 : 365; + const int bottom = (yday + 11 - time.tm_wday) % 7 - 3; + int top = bottom - length % 7; + if (top < -3) top += 7; + top += length; + if (yday >= top) { + ++base; + week = 1; + break; + } + if (yday >= bottom) { + week = 1 + (yday - bottom) / 7; + break; + } + --base; + yday += isLeapYearSum(time.tm_year, base) ? 366 : 365; + } + if (conversion == 'V') addNumber(buffer, week, paddingFormat(2, padding, false)); + else addYear(buffer, time.tm_year, base, conversion == 'G', true); +} + +void formatTime(FormatBuffer& buffer, const char* format, const GuestTm& time) { + for (; *format != '\0'; ++format) { + if (*format == '%') { + bool alternative = false; + Padding padding = Padding::Default; + bool handled = true; + for (bool modifier = true; modifier;) { + modifier = false; + switch (*++format) { + case '\0': + --format; + handled = false; + break; + case 'A': + addText(buffer, time.tm_wday < 0 || time.tm_wday >= 7 ? "?" : WEEKDAY_FULL_NAMES[time.tm_wday]); + break; + case 'a': + addText(buffer, time.tm_wday < 0 || time.tm_wday >= 7 ? "?" : WEEKDAY_NAMES[time.tm_wday]); + break; + case 'B': + addText(buffer, time.tm_mon < 0 || time.tm_mon >= 12 ? "?" : MONTH_FULL_NAMES[time.tm_mon]); + break; + case 'b': + case 'h': + addText(buffer, time.tm_mon < 0 || time.tm_mon >= 12 ? "?" : MONTH_NAMES[time.tm_mon]); + break; + case 'C': + addYear(buffer, time.tm_year, YEAR_BASE, true, false); + break; + case 'c': + formatTime(buffer, "%a %b %e %H:%M:%S %Y", time); + break; + case 'D': + formatTime(buffer, "%m/%d/%y", time); + break; + case 'd': + addNumber(buffer, time.tm_mday, paddingFormat(2, padding, false)); + break; + case 'E': + case 'O': + if (alternative) { + handled = false; + break; + } + alternative = true; + modifier = true; + break; + case 'e': + addNumber(buffer, time.tm_mday, paddingFormat(2, padding, true)); + break; + case 'F': + formatTime(buffer, "%Y-%m-%d", time); + break; + case 'H': + addNumber(buffer, time.tm_hour, paddingFormat(2, padding, false)); + break; + case 'I': + addNumber(buffer, time.tm_hour % 12 != 0 ? time.tm_hour % 12 : 12, paddingFormat(2, padding, false)); + break; + case 'j': + addNumber(buffer, time.tm_yday + 1, paddingFormat(3, padding, false)); + break; + case 'k': + addNumber(buffer, time.tm_hour, paddingFormat(2, padding, true)); + break; + case 'l': + addNumber(buffer, time.tm_hour % 12 != 0 ? time.tm_hour % 12 : 12, paddingFormat(2, padding, true)); + break; + case 'M': + addNumber(buffer, time.tm_min, paddingFormat(2, padding, false)); + break; + case 'm': + addNumber(buffer, time.tm_mon + 1, paddingFormat(2, padding, false)); + break; + case 'n': + addText(buffer, "\n"); + break; + case 'p': + addText(buffer, time.tm_hour >= 12 ? "PM" : "AM"); + break; + case 'R': + formatTime(buffer, "%H:%M", time); + break; + case 'r': + formatTime(buffer, "%I:%M:%S %p", time); + break; + case 'S': + addNumber(buffer, time.tm_sec, paddingFormat(2, padding, false)); + break; + case 's': { + GuestTm copy = time; + char text[24]; + std::snprintf(text, sizeof(text), "%lld", static_cast(libc_mktime_nid_postfix(©))); + addText(buffer, text); + break; + } + case 'T': + formatTime(buffer, "%H:%M:%S", time); + break; + case 't': + addText(buffer, "\t"); + break; + case 'U': + addNumber(buffer, (time.tm_yday + 7 - time.tm_wday) / 7, paddingFormat(2, padding, false)); + break; + case 'u': + addNumber(buffer, time.tm_wday == 0 ? 7 : time.tm_wday, "%d"); + break; + case 'V': + case 'G': + case 'g': + addIsoWeek(buffer, *format, padding, time); + break; + case 'v': + formatTime(buffer, "%e-%b-%Y", time); + break; + case 'W': + addNumber(buffer, (time.tm_yday + 7 - (time.tm_wday != 0 ? time.tm_wday - 1 : 6)) / 7, + paddingFormat(2, padding, false)); + break; + case 'w': + addNumber(buffer, time.tm_wday, "%d"); + break; + case 'X': + formatTime(buffer, "%H:%M:%S", time); + break; + case 'x': + formatTime(buffer, "%m/%d/%y", time); + break; + case 'y': + addYear(buffer, time.tm_year, YEAR_BASE, false, true); + break; + case 'Y': + addYear(buffer, time.tm_year, YEAR_BASE, true, true); + break; + case 'Z': + addText(buffer, guestZoneName(time)); + break; + case 'z': { + if (time.tm_isdst < 0) break; + int difference = static_cast(time.tm_gmtoff); + addText(buffer, difference < 0 ? "-" : "+"); + if (difference < 0) difference = -difference; + difference /= 60; + addNumber(buffer, difference / 60 * 100 + difference % 60, paddingFormat(4, padding, false)); + break; + } + case '+': + formatTime(buffer, "%a %b %e %H:%M:%S %Z %Y", time); + break; + case '-': + case '_': + case '0': + if (padding != Padding::Default) { + handled = false; + break; + } + padding = *format == '-' ? Padding::Less : *format == '_' ? Padding::Space : Padding::Zero; + modifier = true; + break; + default: + handled = false; + break; + } + } + if (handled) continue; + } + if (buffer.cursor == buffer.limit) break; + *buffer.cursor++ = *format; + } +} + +std::size_t formatGuestTime(char* destination, std::size_t size, const char* format, const GuestTm& time) { + FormatBuffer buffer{destination, destination + size}; + formatTime(buffer, format == nullptr ? "%c" : format, time); + if (buffer.cursor == buffer.limit) return 0; + *buffer.cursor = '\0'; + return static_cast(buffer.cursor - destination); +} + +char** HostZoneNames() { +#ifdef _WIN32 + _tzset(); + return _tzname; +#else + ::tzset(); + return ::tzname; +#endif +} +} + 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) { std::time_t t = std::time(nullptr); @@ -112,11 +569,12 @@ int64_t APS5_VABI _Xtime_get_ticks_nid_postfix() { } double APS5_VABI libc_difftime_nid_postfix(int64_t time1, int64_t time0) { - return std::difftime(static_cast(time1), static_cast(time0)); + if (time1 >= time0) return static_cast(static_cast(time1) - static_cast(time0)); + return -static_cast(static_cast(time0) - static_cast(time1)); } double APS5_VABI difftime_nid_postfix(int64_t time1, int64_t time0) { - return std::difftime(static_cast(time1), static_cast(time0)); + return libc_difftime_nid_postfix(time1, time0); } GuestTm* APS5_VABI libc_gmtime_nid_postfix(const int64_t* timer) { @@ -135,10 +593,23 @@ GuestTm* APS5_VABI localtime_nid_postfix(const int64_t* timer) { GuestTm* APS5_VABI localtime_s_nid_postfix(const int64_t* timer, GuestTm* result) { if (timer == nullptr || result == nullptr) return nullptr; - const std::time_t t = static_cast(*timer); + const ZoneScope zone; std::tm host{}; + const int callerError = errno; #ifdef _WIN32 - if (localtime_s(&host, &t) != 0) return nullptr; + const std::int64_t cycles = *timer >= 0 && *timer <= HOST_LAST_SECOND ? 0 : calendarCycles(*timer, CALENDAR_CYCLE_SECONDS); + const std::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; + } + const std::int64_t year = host.tm_year + cycles * CALENDAR_CYCLE_YEARS; + if (year < std::numeric_limits::min() || year > std::numeric_limits::max()) { + errno = callerError; + return nullptr; + } + host.tm_year = static_cast(year); copyCalendarFields(host, *result); long standardBias = 0; long daylightBias = 0; @@ -148,7 +619,11 @@ GuestTm* APS5_VABI localtime_s_nid_postfix(const int64_t* timer, GuestTm* result result->tm_gmtoff = -static_cast(standardBias + (daylight ? daylightBias : 0)); result->tm_zone = _tzname[daylight ? 1 : 0]; #else - if (localtime_r(&t, &host) == nullptr) return nullptr; + const std::time_t t = static_cast(*timer); + if (localtime_r(&t, &host) == nullptr) { + errno = callerError; + return nullptr; + } copyCalendarFields(host, *result); result->tm_gmtoff = host.tm_gmtoff; result->tm_zone = host.tm_zone; @@ -169,11 +644,40 @@ 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; +#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; + host = toHostTm(window); + host.tm_wday = -1; + } +#endif const std::time_t t = std::mktime(&host); - if (t == static_cast(-1)) return -1; - const int64_t time = static_cast(t); - localtime_s_nid_postfix(&time, timeptr); + if (t == static_cast(-1) && host.tm_wday == -1) return -1; + const int64_t time = static_cast(t) + moved; + GuestTm normalized{}; + if (localtime_s_nid_postfix(&time, &normalized) == nullptr) return -1; + *timeptr = normalized; return time; } @@ -182,19 +686,75 @@ int64_t APS5_VABI mktime_nid_postfix(GuestTm* timeptr) { } size_t APS5_VABI libc_strftime_nid_postfix(char* str, size_t count, const char* format, const GuestTm* timeptr) { - const std::tm host = toHostTm(*timeptr); - return std::strftime(str, count, format, &host); + const ZoneScope zone; + tzset_nid_postfix(); + return formatGuestTime(str, count, format, *timeptr); } char* APS5_VABI asctime_nid_postfix(const GuestTm* timeptr) { - const std::tm host = toHostTm(*timeptr); - return std::asctime(&host); + static thread_local char result[ASCTIME_SIZE]; + if (timeptr == nullptr) { + errno = EINVAL; + std::memcpy(result, "??? ??? ?? ??:??:?? ????\n", sizeof("??? ??? ?? ??:??:?? ????\n")); + return result; + } + const char* weekday = timeptr->tm_wday >= 0 && timeptr->tm_wday < 7 ? WEEKDAY_NAMES[timeptr->tm_wday] : "???"; + const char* month = timeptr->tm_mon >= 0 && timeptr->tm_mon < 12 ? MONTH_NAMES[timeptr->tm_mon] : "???"; + char year[16]; + formatGuestTime(year, sizeof(year), "%Y", *timeptr); + std::snprintf(result, sizeof(result), std::strlen(year) <= 4 ? "%s %s%3d %.2d:%.2d:%.2d %-4s\n" : "%s %s%3d %.2d:%.2d:%.2d %s\n", + weekday, month, timeptr->tm_mday, timeptr->tm_hour, timeptr->tm_min, timeptr->tm_sec, year); + return result; +} + +char* APS5_VABI ctime_nid_postfix(const int64_t* timer) { + if (const GuestTm* local = localtime_nid_postfix(timer)) return asctime_nid_postfix(local); + static char unknown[26]; + std::memcpy(unknown, "??? ??? ?? ??:??:?? ????\n", sizeof(unknown)); + errno = EINVAL; + return unknown; } size_t APS5_VABI strftime_nid_postfix(char* str, size_t count, const char* format, const GuestTm* timeptr) { return libc_strftime_nid_postfix(str, count, format, timeptr); } +size_t APS5_VABI wcsftime_nid_postfix(std::uint16_t* str, size_t count, const std::uint16_t* format, const GuestTm* timeptr) { + constexpr int GuestEnomem = 12; + constexpr int GuestEinval = 22; + constexpr auto Failed = static_cast(-1); + if (format == nullptr) throw std::invalid_argument("wcsftime: null format"); + if (timeptr == nullptr) throw std::invalid_argument("wcsftime: null time"); + const std::uint16_t* formatCursor = format; + const std::size_t formatLength = wcsrtombs_nid_postfix(nullptr, &formatCursor, 0, nullptr); + if (formatLength == Failed) return 0; + const std::unique_ptr narrowFormat(new (std::nothrow) char[formatLength + 1]); + if (!narrowFormat) { + errno = GuestEnomem; + return 0; + } + wcsrtombs_nid_postfix(narrowFormat.get(), &formatCursor, formatLength + 1, nullptr); + const auto maxCharacterBytes = static_cast(___mb_cur_max_nid_postfix()); + if (std::numeric_limits::max() / maxCharacterBytes <= count) { + errno = GuestEinval; + return 0; + } + const std::unique_ptr narrow(new (std::nothrow) char[count * maxCharacterBytes]); + if (!narrow) { + errno = GuestEnomem; + return 0; + } + const int callerError = errno; + if (strftime_nid_postfix(narrow.get(), count, narrowFormat.get(), timeptr) == 0) { + errno = callerError; + return 0; + } + const char* narrowCursor = narrow.get(); + const std::size_t length = mbsrtowcs_nid_postfix(str, &narrowCursor, count, nullptr); + if (length == static_cast(-2) || length == Failed || narrowCursor != nullptr) return 0; + return length; +} + // The guest's CLOCKS_PER_SEC is 1000000: clock() reports process CPU time in microseconds. int64_t APS5_VABI clock_nid_postfix() { #ifdef _WIN32 @@ -207,4 +767,12 @@ int64_t APS5_VABI clock_nid_postfix() { #endif } +int APS5_VABI ctime_s_nid_postfix(char* buffer, std::size_t size, const std::int64_t* time) { + return SonyCtimeS(buffer, size, time); +} + +int APS5_VABI asctime_s_nid_postfix(char* buffer, std::size_t size, const GuestTm* time) { + return SonyAsctimeS(buffer, size, time); +} + } 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 085d945d5c..09fb1fd1c9 100644 --- a/core/libs/prx/libc/src/WideConversion.cpp +++ b/core/libs/prx/libc/src/WideConversion.cpp @@ -1,7 +1,21 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/SonyCrt.hpp" #include #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); +} + +extern "C" void* APS5_VABI set_constraint_handler_s_nid_postfix(void* handler) { + return SonySetConstraintHandlerS(handler); +} extern "C" std::size_t APS5_VABI wcstombs_nid_postfix(char* destination, const std::uint16_t* source, std::size_t capacity) { std::size_t count = 0; @@ -17,3 +31,55 @@ extern "C" std::size_t APS5_VABI wcstombs_nid_postfix(char* destination, const s } return count; } + +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/ArraySupport.cpp b/core/libs/prx/libc/src/exception/ArraySupport.cpp index 8132ade49d..6b5302ac9e 100644 --- a/core/libs/prx/libc/src/exception/ArraySupport.cpp +++ b/core/libs/prx/libc/src/exception/ArraySupport.cpp @@ -2,13 +2,13 @@ #include extern "C" { -void APS5_VABI __cxa_vec_cleanup_nid_postfix(void* array, std::size_t count, std::size_t size, void (*destroy)(void*)) { +void APS5_VABI __cxa_vec_cleanup_nid_postfix(void* array, std::size_t count, std::size_t size, void (APS5_VABI *destroy)(void*)) { if (!destroy) return; try { while (count) destroy(static_cast(array) + --count * size); } catch (...) { LibcException::Terminate(); } } -void APS5_VABI __cxa_vec_ctor_nid_postfix(void* array, std::size_t count, std::size_t size, void (*construct)(void*), void (*destroy)(void*)) { +void APS5_VABI __cxa_vec_ctor_nid_postfix(void* array, std::size_t count, std::size_t size, void (APS5_VABI *construct)(void*), void (APS5_VABI *destroy)(void*)) { if (!construct) return; std::size_t complete = 0; try { @@ -18,7 +18,7 @@ void APS5_VABI __cxa_vec_ctor_nid_postfix(void* array, std::size_t count, std::s throw; } } -void APS5_VABI __cxa_vec_cctor_nid_postfix(void* destination, void* source, std::size_t count, std::size_t size, void (*copy)(void*, void*), void (*destroy)(void*)) { +void APS5_VABI __cxa_vec_cctor_nid_postfix(void* destination, void* source, std::size_t count, std::size_t size, void (APS5_VABI *copy)(void*, void*), void (APS5_VABI *destroy)(void*)) { if (!copy) return; std::size_t complete = 0; try { @@ -29,7 +29,7 @@ void APS5_VABI __cxa_vec_cctor_nid_postfix(void* destination, void* source, std: throw; } } -void APS5_VABI __cxa_vec_dtor_nid_postfix(void* array, std::size_t count, std::size_t size, void (*destroy)(void*)) { +void APS5_VABI __cxa_vec_dtor_nid_postfix(void* array, std::size_t count, std::size_t size, void (APS5_VABI *destroy)(void*)) { if (!destroy) return; bool unwinding = LibcException::globals.uncaught != 0; try { @@ -53,8 +53,8 @@ void* ArrayAllocate(std::size_t size) { if (!result) _ZSt11_Xbad_allocv_nid_postfix(); return result; } -template -void* NewArray(std::size_t count, std::size_t size, std::size_t padding, void (*construct)(void*), void (*destroy)(void*), void* (*allocate)(std::size_t), Deallocate deallocate) { +template +void* NewArray(std::size_t count, std::size_t size, std::size_t padding, void (APS5_VABI *construct)(void*), void (APS5_VABI *destroy)(void*), TAllocate allocate, TDeallocate deallocate) { std::size_t total = ArraySize(count, size, padding); auto* storage = static_cast(allocate(total)); if (!storage) return nullptr; @@ -64,8 +64,8 @@ void* NewArray(std::size_t count, std::size_t size, std::size_t padding, void (* catch (...) { deallocate(storage, total); throw; } return array; } -template -void DeleteArray(void* array, std::size_t size, std::size_t padding, void (*destroy)(void*), Deallocate deallocate) { +template +void DeleteArray(void* array, std::size_t size, std::size_t padding, void (APS5_VABI *destroy)(void*), TDeallocate deallocate) { if (!array) return; auto* storage = static_cast(array) - padding; std::size_t count = 0; @@ -81,22 +81,22 @@ void DeleteArray(void* array, std::size_t size, std::size_t padding, void (*dest } extern "C" { -void* APS5_VABI __cxa_vec_new_nid_postfix(std::size_t n, std::size_t s, std::size_t p, void (*ctor)(void*), void (*dtor)(void*)) { +void* APS5_VABI __cxa_vec_new_nid_postfix(std::size_t n, std::size_t s, std::size_t p, void (APS5_VABI *ctor)(void*), void (APS5_VABI *dtor)(void*)) { return LibcException::NewArray(n, s, p, ctor, dtor, LibcException::ArrayAllocate, [](void* ptr, std::size_t) { std::free(ptr); }); } -void* APS5_VABI __cxa_vec_new2_nid_postfix(std::size_t n, std::size_t s, std::size_t p, void (*ctor)(void*), void (*dtor)(void*), void* (*alloc)(std::size_t), void (*dealloc)(void*)) { +void* APS5_VABI __cxa_vec_new2_nid_postfix(std::size_t n, std::size_t s, std::size_t p, void (APS5_VABI *ctor)(void*), void (APS5_VABI *dtor)(void*), void* (APS5_VABI *alloc)(std::size_t), void (APS5_VABI *dealloc)(void*)) { return LibcException::NewArray(n, s, p, ctor, dtor, alloc, [dealloc](void* ptr, std::size_t) { dealloc(ptr); }); } -void* APS5_VABI __cxa_vec_new3_nid_postfix(std::size_t n, std::size_t s, std::size_t p, void (*ctor)(void*), void (*dtor)(void*), void* (*alloc)(std::size_t), void (*dealloc)(void*, std::size_t)) { +void* APS5_VABI __cxa_vec_new3_nid_postfix(std::size_t n, std::size_t s, std::size_t p, void (APS5_VABI *ctor)(void*), void (APS5_VABI *dtor)(void*), void* (APS5_VABI *alloc)(std::size_t), void (APS5_VABI *dealloc)(void*, std::size_t)) { return LibcException::NewArray(n, s, p, ctor, dtor, alloc, dealloc); } -void APS5_VABI __cxa_vec_delete_nid_postfix(void* array, std::size_t s, std::size_t p, void (*dtor)(void*)) { +void APS5_VABI __cxa_vec_delete_nid_postfix(void* array, std::size_t s, std::size_t p, void (APS5_VABI *dtor)(void*)) { LibcException::DeleteArray(array, s, p, dtor, [](void* ptr, std::size_t) { std::free(ptr); }); } -void APS5_VABI __cxa_vec_delete2_nid_postfix(void* array, std::size_t s, std::size_t p, void (*dtor)(void*), void (*dealloc)(void*)) { +void APS5_VABI __cxa_vec_delete2_nid_postfix(void* array, std::size_t s, std::size_t p, void (APS5_VABI *dtor)(void*), void (APS5_VABI *dealloc)(void*)) { LibcException::DeleteArray(array, s, p, dtor, [dealloc](void* ptr, std::size_t) { dealloc(ptr); }); } -void APS5_VABI __cxa_vec_delete3_nid_postfix(void* array, std::size_t s, std::size_t p, void (*dtor)(void*), void (*dealloc)(void*, std::size_t)) { +void APS5_VABI __cxa_vec_delete3_nid_postfix(void* array, std::size_t s, std::size_t p, void (APS5_VABI *dtor)(void*), void (APS5_VABI *dealloc)(void*, std::size_t)) { LibcException::DeleteArray(array, s, p, dtor, dealloc); } } diff --git a/core/libs/prx/libc/src/exception/ExceptionPointer.cpp b/core/libs/prx/libc/src/exception/ExceptionPointer.cpp index eba6687492..447d85ac45 100644 --- a/core/libs/prx/libc/src/exception/ExceptionPointer.cpp +++ b/core/libs/prx/libc/src/exception/ExceptionPointer.cpp @@ -1,12 +1,23 @@ #include #ifndef _LIBCPP_VERSION +#include + extern "C" { void ExceptionPointerAddref(std::exception_ptr* self) noexcept asm("_ZNSt15__exception_ptr13exception_ptr9_M_addrefEv"); void ExceptionPointerRelease(std::exception_ptr* self) noexcept asm("_ZNSt15__exception_ptr13exception_ptr10_M_releaseEv"); void* ExceptionPointerGet(const std::exception_ptr* self) noexcept asm("_ZNKSt15__exception_ptr13exception_ptr6_M_getEv"); void ExceptionPointerConstruct(std::exception_ptr* self, void* exception) noexcept asm("_ZNSt15__exception_ptr13exception_ptrC1EPv"); const std::type_info* ExceptionPointerType(const std::exception_ptr* self) noexcept asm("_ZNKSt15__exception_ptr13exception_ptr20__cxa_exception_typeEv"); +void FutureResultDestroy(std::__future_base::_Result_base* self) noexcept asm("_ZNSt13__future_base12_Result_baseD2Ev"); + +void FutureResultDestroy(std::__future_base::_Result_base* self) noexcept { + self->_M_error.~exception_ptr(); +} + +void APS5_VABI _ZNSt13__future_base12_Result_baseD2Ev_nid_postfix(std::__future_base::_Result_base* self) noexcept { + FutureResultDestroy(self); +} void APS5_VABI _ZNSt15__exception_ptr13exception_ptr9_M_addrefEv_nid_postfix(std::exception_ptr* self) noexcept { ExceptionPointerAddref(self); diff --git a/core/libs/prx/libc/src/exception/Exports.cpp b/core/libs/prx/libc/src/exception/Exports.cpp index 462c2c479b..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); @@ -52,7 +45,7 @@ void NativeFreeException(void* object) asm("__cxa_free_exception"); void NativeFreeException(void* object) { __cxa_free_exception_nid_postfix(object); } [[noreturn]] void NativeThrow(void* object, std::type_info* type, void (*destructor)(void*)) asm("__cxa_throw"); [[noreturn]] void NativeThrow(void* object, std::type_info* type, void (*destructor)(void*)) { - __cxa_throw_nid_postfix(object, type, destructor); + ThrowFromSite(object, type, destructor, __builtin_return_address(0)); } void* NativeBeginCatch(void* exception) asm("__cxa_begin_catch"); void* NativeBeginCatch(void* exception) { return __cxa_begin_catch_nid_postfix(exception); } @@ -76,5 +69,13 @@ unsigned NativeUncaughtExceptions() asm("__cxa_uncaught_exceptions"); unsigned NativeUncaughtExceptions() { return LibcException::globals.uncaught; } bool NativeUncaughtException() asm("__cxa_uncaught_exception"); bool NativeUncaughtException() { return LibcException::globals.uncaught != 0; } +bool NativeStdUncaughtException() asm("_ZSt18uncaught_exceptionv"); +bool NativeStdUncaughtException() { return LibcException::globals.uncaught != 0; } +int NativeStdUncaughtExceptions() asm("_ZSt19uncaught_exceptionsv"); +int NativeStdUncaughtExceptions() { return static_cast(LibcException::globals.uncaught); } +void* NativeInitPrimaryException(void* object, std::type_info* type, void (*destructor)(void*)) asm("__cxa_init_primary_exception"); +void* NativeInitPrimaryException(void* object, std::type_info* type, void (*destructor)(void*)) { + return __cxa_init_primary_exception_nid_postfix(object, type, destructor); +} } #endif diff --git a/core/libs/prx/libc/src/exception/Runtime.cpp b/core/libs/prx/libc/src/exception/Runtime.cpp index 244bc1f6fb..e790d1a2db 100644 --- a/core/libs/prx/libc/src/exception/Runtime.cpp +++ b/core/libs/prx/libc/src/exception/Runtime.cpp @@ -1,15 +1,43 @@ #include "prx/libc/include/exceptions/Runtime.hpp" +#include #include #include +#include #include #ifdef _WIN32 #include +#else +#include +#include #endif namespace LibcException { // Writes straight to the stderr handle: terminate paths must not wait on the CRT stream lock, // which another (possibly suspended) thread may hold. +static void RawWrite(const char* data, std::size_t size) { +#ifdef _WIN32 + const HANDLE handle = GetStdHandle(STD_ERROR_HANDLE); + while (size != 0) { + DWORD written = 0; + const auto chunk = static_cast(std::min(size, std::numeric_limits::max())); + if (!WriteFile(handle, data, chunk, &written, nullptr) || written == 0) return; + data += written; + size -= written; + } +#else + const int kept = HostStandardError_nid_no_patch(); + const int descriptor = kept >= 0 ? kept : STDERR_FILENO; + while (size != 0) { + const auto written = ::write(descriptor, data, size); + if (written < 0 && errno == EINTR) continue; + if (written <= 0) return; + data += written; + size -= static_cast(written); + } +#endif +} + static void RawLog(const char* format, ...) { char buffer[1024]; va_list args; @@ -18,12 +46,7 @@ static void RawLog(const char* format, ...) { va_end(args); if (length <= 0) return; if (length > static_cast(sizeof(buffer)) - 1) length = sizeof(buffer) - 1; -#ifdef _WIN32 - DWORD written = 0; - WriteFile(GetStdHandle(STD_ERROR_HANDLE), buffer, static_cast(length), &written, nullptr); -#else - std::fwrite(buffer, 1, length, stderr); -#endif + RawWrite(buffer, static_cast(length)); } [[noreturn]] static void DefaultTerminate() { @@ -47,9 +70,11 @@ static void RawLog(const char* format, ...) { int status = 0; char* demangled = abi::__cxa_demangle(typeName, nullptr, nullptr, &status); const char* displayName = (status == 0 && demangled) ? demangled : typeName; - if (what) - RawLog("terminate called after throwing an instance of '%s'\n what(): %s\n", displayName, what); - else + if (what) { + RawLog("terminate called after throwing an instance of '%s'\n what(): ", displayName); + RawWrite(what, std::strlen(what)); + RawWrite("\n", 1); + } else RawLog("terminate called after throwing an instance of '%s'\n", displayName); std::free(demangled); } else { @@ -92,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) { @@ -106,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 { @@ -229,7 +274,7 @@ void APS5_VABI __cxa_free_exception_nid_postfix(void* object) { std::free(allocation); } -[[noreturn]] void APS5_VABI __cxa_throw_nid_postfix(void* object, std::type_info* type, void (*destructor)(void*)) { +[[noreturn]] static void ThrowFromSite(void* object, std::type_info* type, void (*destructor)(void*), void* throwSite) { using namespace LibcException; auto* header = FromObject(object); header->type = type; @@ -240,14 +285,19 @@ void APS5_VABI __cxa_free_exception_nid_postfix(void* object) { header->unwind.exception_cleanup = Cleanup; ++globals.uncaught; _Unwind_RaiseException_nid_postfix(&header->unwind); - RawLog("[libc] unhandled exception '%s' thrown from %p\n", type ? type->name() : "?", __builtin_return_address(0)); + RawLog("[libc] unhandled exception '%s' thrown from %p\n", type ? type->name() : "?", throwSite); __cxa_begin_catch_nid_postfix(&header->unwind); InvokeTerminate(header->terminate); } -LibcException::Header* __cxa_init_primary_exception_nid_postfix(void* object, std::type_info* type, void (*destructor)(void*)) { +[[noreturn]] void APS5_VABI __cxa_throw_nid_postfix(void* object, std::type_info* type, void (*destructor)(void*)) { + ThrowFromSite(object, type, destructor, __builtin_return_address(0)); +} + +LibcException::Header* APS5_VABI __cxa_init_primary_exception_nid_postfix(void* object, std::type_info* type, void (*destructor)(void*)) { using namespace LibcException; auto* header = FromObject(object); + AllocationOf(header)->references.store(0, std::memory_order_relaxed); header->type = type; header->destructor = destructor; header->terminate = terminateHandler.load(std::memory_order_acquire); @@ -366,8 +416,8 @@ void APS5_VABI __cxa_rethrow_primary_exception_nid_postfix(void* object) { InvokeTerminate(header->terminate); } -void* __dynamic_cast_nid_postfix(const void* source, const __cxxabiv1::__class_type_info* sourceType, - const __cxxabiv1::__class_type_info* destinationType, std::ptrdiff_t) { +void* APS5_VABI __dynamic_cast_nid_postfix(const void* source, const __cxxabiv1::__class_type_info* sourceType, + const __cxxabiv1::__class_type_info* destinationType, std::ptrdiff_t) { using namespace LibcException; if (!source) return nullptr; auto* vtable = *static_cast(source); diff --git a/core/libs/prx/libc/src/exception/StandardExceptions.cpp b/core/libs/prx/libc/src/exception/StandardExceptions.cpp index b8bf43028f..31feeb3d8e 100644 --- a/core/libs/prx/libc/src/exception/StandardExceptions.cpp +++ b/core/libs/prx/libc/src/exception/StandardExceptions.cpp @@ -1,7 +1,13 @@ #include "prx/libc/include/exceptions/Runtime.hpp" +#include "prx/libc/include/GuestLocale.hpp" +#include +#include #include #include #include +#include + +extern "C" void* APS5_VABI _Znwm_nid_postfix(std::size_t size); namespace LibcException { struct TypeRecord { const void* const* vtable; const char* name; const TypeRecord* base; }; @@ -9,9 +15,9 @@ struct ExceptionObject { const void* vtable; const char* message; }; struct ExceptionVtable { std::ptrdiff_t offset; const TypeRecord* type; - void (*destroy)(ExceptionObject*); - void (*deleteObject)(ExceptionObject*); - const char* (*what)(const ExceptionObject*); + void (APS5_VABI *destroy)(ExceptionObject*); + void (APS5_VABI *deleteObject)(ExceptionObject*); + const char* (APS5_VABI *what)(const ExceptionObject*); }; struct Message { std::size_t length; @@ -71,6 +77,9 @@ void ReleaseMessage(const char* message) { void DestroyPlain(ExceptionObject*) {} void DeletePlain(ExceptionObject* object) { std::free(object); } void DestroyMessage(ExceptionObject* object) { ReleaseMessage(object->message); object->message = nullptr; } +static void APS5_VABI DestroyGuestMessage_nid_no_patch(void* object) { + DestroyMessage(static_cast(object)); +} void DeleteMessage(ExceptionObject* object) { DestroyMessage(object); std::free(object); } const char* PlainWhat(const ExceptionObject* object) { auto* table = static_cast(object->vtable); @@ -83,9 +92,17 @@ 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 : ""; } +static void APS5_VABI GuestDestroyPlain_nid_no_patch(ExceptionObject* object) { DestroyPlain(object); } +static void APS5_VABI GuestDeletePlain_nid_no_patch(ExceptionObject* object) { DeletePlain(object); } +static const char* APS5_VABI GuestPlainWhat_nid_no_patch(const ExceptionObject* object) { return PlainWhat(object); } +static void APS5_VABI GuestDestroyMessage_nid_no_patch(ExceptionObject* object) { DestroyMessage(object); } +static void APS5_VABI GuestDeleteMessage_nid_no_patch(ExceptionObject* object) { DeleteMessage(object); } +static const char* APS5_VABI GuestMessageWhat_nid_no_patch(const ExceptionObject* object) { return MessageWhat(object); } void Construct(ExceptionObject* object, const ExceptionVtable& table, const char* message) { object->vtable = &table.destroy; object->message = CopyMessage(message); @@ -102,19 +119,154 @@ ExceptionObject* Assign(ExceptionObject* object, const ExceptionObject* source) [[noreturn]] void ThrowMessage(const ExceptionVtable& table, const char* message) { auto* object = static_cast(__cxa_allocate_exception_nid_postfix(sizeof(ExceptionObject))); Construct(object, table, message); - __cxa_throw_nid_postfix(object, reinterpret_cast(const_cast(table.type)), [](void* p) { DestroyMessage(static_cast(p)); }); + __cxa_throw_nid_postfix(object, reinterpret_cast(const_cast(table.type)), + reinterpret_cast(DestroyGuestMessage_nid_no_patch)); } [[noreturn]] void ThrowPlain(const ExceptionVtable& table) { auto* object = static_cast(__cxa_allocate_exception_nid_postfix(sizeof(void*))); object->vtable = &table.destroy; __cxa_throw_nid_postfix(object, reinterpret_cast(const_cast(table.type)), [](void* p) { DestroyPlain(static_cast(p)); }); } + +struct CategoryObject { const void* vtable; }; +struct ErrorCode { int value; const CategoryObject* category; }; +struct SystemErrorObject { ExceptionObject base; ErrorCode code; }; +static_assert(sizeof(ErrorCode) == 16 && sizeof(SystemErrorObject) == 32 && offsetof(SystemErrorObject, code) == 16); +struct CppError { int value; const char* message; }; + +static const char* GenericErrorMessage(int value) { + static constexpr CppError errors[] { + {1, "operation not permitted"}, + {2, "no such file or directory"}, + {3, "no such process"}, + {4, "interrupted"}, + {5, "io error"}, + {6, "no such device or address"}, + {7, "argument list too long"}, + {8, "executable format error"}, + {9, "bad file descriptor"}, + {10, "no child process"}, + {11, "resource deadlock would occur"}, + {12, "not enough memory"}, + {13, "permission denied"}, + {14, "bad address"}, + {16, "device or resource busy"}, + {17, "file exists"}, + {18, "cross device link"}, + {19, "no such device"}, + {20, "not a directory"}, + {21, "is a directory"}, + {22, "invalid argument"}, + {23, "too many files open in system"}, + {24, "too many files open"}, + {25, "inappropriate io control operation"}, + {26, "text file busy"}, + {27, "file too large"}, + {28, "no space on device"}, + {29, "invalid seek"}, + {30, "read only file system"}, + {31, "too many links"}, + {32, "broken pipe"}, + {33, "argument out of domain"}, + {34, "result out of range"}, + {35, "operation would block"}, + {36, "operation in progress"}, + {37, "connection already in progress"}, + {38, "not a socket"}, + {39, "destination address required"}, + {40, "message size"}, + {41, "wrong protocol type"}, + {42, "no protocol option"}, + {43, "protocol not supported"}, + {45, "not supported"}, + {47, "address family not supported"}, + {48, "address in use"}, + {49, "address not available"}, + {50, "network down"}, + {51, "network unreachable"}, + {52, "network reset"}, + {53, "connection aborted"}, + {54, "connection reset"}, + {55, "no buffer space"}, + {56, "already connected"}, + {57, "not connected"}, + {60, "timed out"}, + {61, "connection refused"}, + {62, "too many symbolic link levels"}, + {63, "filename too long"}, + {65, "host unreachable"}, + {66, "directory not empty"}, + {77, "no lock available"}, + {78, "function not supported"}, + {82, "identifier removed"}, + {83, "no message"}, + {84, "value too large"}, + {85, "operation canceled"}, + {86, "illegal byte sequence"}, + {89, "bad message"}, + {91, "no link"}, + {92, "protocol error"}, + {95, "state not recoverable"}, + {96, "owner dead"} + }; + for (const auto& error : errors) + if (error.value == value) return error.message; + return nullptr; +} + +const TypeRecord ErrorCategoryType {ClassTypeVtable + 2, "St14error_category", nullptr}; +const TypeRecord GenericCategoryType {SingleTypeVtable + 2, "St23_Generic_error_category", &ErrorCategoryType}; +static void APS5_VABI GenericCategoryDestroy_nid_no_patch(CategoryObject*) {} +static void APS5_VABI GenericCategoryDelete_nid_no_patch(CategoryObject*) { + throw std::invalid_argument("std::_Generic_error_category: the generic category object is not heap allocated"); +} +static const char* APS5_VABI GenericCategoryName_nid_no_patch(const CategoryObject*) { return "generic"; } +static GuestLocale::String* APS5_VABI GenericCategoryMessage_nid_no_patch(GuestLocale::String* result, const CategoryObject*, int value) { + if (!result) throw std::invalid_argument("std::_Generic_error_category::message: null result"); + const char* message = GenericErrorMessage(value); + if (!message) message = "unknown error"; + const auto length = std::strlen(message); + const auto capacity = length | std::size_t{15}; + char* storage = length <= 15 ? result->buffer : static_cast(_Znwm_nid_postfix(capacity + 1)); + std::memcpy(storage, message, length + 1); + result->reserved = 0; + if (length > 15) result->pointer = storage; + result->size = length; + result->capacity = capacity; + return result; +} +static ErrorCode APS5_VABI GenericCategoryCondition_nid_no_patch(const CategoryObject* self, int value) { return {value, self}; } +static bool APS5_VABI GenericCategoryEquivalentCondition_nid_no_patch(const CategoryObject* self, int value, const ErrorCode* condition) { + return condition->category == self && condition->value == value; +} +static bool APS5_VABI GenericCategoryEquivalentCode_nid_no_patch(const CategoryObject* self, const ErrorCode* code, int value) { + return code->category == self && code->value == value; +} +const void* const GenericCategoryVtable[] { + nullptr, &GenericCategoryType, + reinterpret_cast(GenericCategoryDestroy_nid_no_patch), reinterpret_cast(GenericCategoryDelete_nid_no_patch), + reinterpret_cast(GenericCategoryName_nid_no_patch), reinterpret_cast(GenericCategoryMessage_nid_no_patch), + reinterpret_cast(GenericCategoryCondition_nid_no_patch), + reinterpret_cast(GenericCategoryEquivalentCondition_nid_no_patch), + reinterpret_cast(GenericCategoryEquivalentCode_nid_no_patch) +}; +const CategoryObject GenericCategory {GenericCategoryVtable + 2}; } extern "C" { +const char* APS5_VABI _ZSt13_Syserror_mapi_nid_postfix(int value) { + return LibcException::GenericErrorMessage(value); +} +const LibcException::CategoryObject* APS5_VABI _ZSt16generic_categoryv_nid_postfix() { + return &LibcException::GenericCategory; +} +GuestLocale::String* APS5_VABI _ZNKSt23_Generic_error_category7messageEi_nid_postfix( + GuestLocale::String* result, const LibcException::CategoryObject* self, int value) { + return LibcException::GenericCategoryMessage_nid_no_patch(result, self, value); +} LibcException::TypeRecord _ZTISt8ios_base_nid_postfix {LibcException::ClassTypeVtable + 2, "St8ios_base", nullptr}; LibcException::TypeRecord _ZTISt9exception_nid_postfix {LibcException::ClassTypeVtable + 2, "St9exception", nullptr}; -LibcException::ExceptionVtable _ZTVSt9exception_nid_postfix {0, &_ZTISt9exception_nid_postfix, LibcException::DestroyPlain, LibcException::DeletePlain, LibcException::PlainWhat}; +LibcException::ExceptionVtable _ZTVSt9exception_nid_postfix {0, &_ZTISt9exception_nid_postfix, LibcException::GuestDestroyPlain_nid_no_patch, LibcException::GuestDeletePlain_nid_no_patch, LibcException::GuestPlainWhat_nid_no_patch}; void APS5_VABI _ZNSt9exceptionD1Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyPlain(self); } void APS5_VABI _ZNSt9exceptionD2Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyPlain(self); } void APS5_VABI _ZNSt9exceptionD0Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DeletePlain(self); } @@ -132,7 +284,7 @@ void APS5_VABI _ZNSt9exceptionC1Ev_nid_postfix(LibcException::ExceptionObject* s void APS5_VABI _ZNSt9exceptionC2Ev_nid_postfix(LibcException::ExceptionObject* self) { self->vtable = &_ZTVSt9exception_nid_postfix.destroy; } LibcException::TypeRecord _ZTISt8bad_cast_nid_postfix {LibcException::SingleTypeVtable + 2, "St8bad_cast", &_ZTISt9exception_nid_postfix}; -LibcException::ExceptionVtable _ZTVSt8bad_cast_nid_postfix {0, &_ZTISt8bad_cast_nid_postfix, LibcException::DestroyPlain, LibcException::DeletePlain, LibcException::PlainWhat}; +LibcException::ExceptionVtable _ZTVSt8bad_cast_nid_postfix {0, &_ZTISt8bad_cast_nid_postfix, LibcException::GuestDestroyPlain_nid_no_patch, LibcException::GuestDeletePlain_nid_no_patch, LibcException::GuestPlainWhat_nid_no_patch}; void APS5_VABI _ZNSt8bad_castD1Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyPlain(self); } void APS5_VABI _ZNSt8bad_castD2Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyPlain(self); } void APS5_VABI _ZNSt8bad_castD0Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DeletePlain(self); } @@ -141,7 +293,7 @@ void APS5_VABI _ZNSt8bad_castC1Ev_nid_postfix(LibcException::ExceptionObject* se void APS5_VABI _ZNSt8bad_castC2Ev_nid_postfix(LibcException::ExceptionObject* self) { self->vtable = &_ZTVSt8bad_cast_nid_postfix.destroy; } LibcException::TypeRecord _ZTISt10bad_typeid_nid_postfix {LibcException::SingleTypeVtable + 2, "St10bad_typeid", &_ZTISt9exception_nid_postfix}; -LibcException::ExceptionVtable _ZTVSt10bad_typeid_nid_postfix {0, &_ZTISt10bad_typeid_nid_postfix, LibcException::DestroyPlain, LibcException::DeletePlain, LibcException::PlainWhat}; +LibcException::ExceptionVtable _ZTVSt10bad_typeid_nid_postfix {0, &_ZTISt10bad_typeid_nid_postfix, LibcException::GuestDestroyPlain_nid_no_patch, LibcException::GuestDeletePlain_nid_no_patch, LibcException::GuestPlainWhat_nid_no_patch}; void APS5_VABI _ZNSt10bad_typeidD1Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyPlain(self); } void APS5_VABI _ZNSt10bad_typeidD2Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyPlain(self); } void APS5_VABI _ZNSt10bad_typeidD0Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DeletePlain(self); } @@ -150,7 +302,7 @@ void APS5_VABI _ZNSt10bad_typeidC1Ev_nid_postfix(LibcException::ExceptionObject* void APS5_VABI _ZNSt10bad_typeidC2Ev_nid_postfix(LibcException::ExceptionObject* self) { self->vtable = &_ZTVSt10bad_typeid_nid_postfix.destroy; } LibcException::TypeRecord _ZTISt9bad_alloc_nid_postfix {LibcException::SingleTypeVtable + 2, "St9bad_alloc", &_ZTISt9exception_nid_postfix}; -LibcException::ExceptionVtable _ZTVSt9bad_alloc_nid_postfix {0, &_ZTISt9bad_alloc_nid_postfix, LibcException::DestroyPlain, LibcException::DeletePlain, LibcException::PlainWhat}; +LibcException::ExceptionVtable _ZTVSt9bad_alloc_nid_postfix {0, &_ZTISt9bad_alloc_nid_postfix, LibcException::GuestDestroyPlain_nid_no_patch, LibcException::GuestDeletePlain_nid_no_patch, LibcException::GuestPlainWhat_nid_no_patch}; void APS5_VABI _ZNSt9bad_allocD1Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyPlain(self); } void APS5_VABI _ZNSt9bad_allocD2Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyPlain(self); } void APS5_VABI _ZNSt9bad_allocD0Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DeletePlain(self); } @@ -159,7 +311,7 @@ void APS5_VABI _ZNSt9bad_allocC1Ev_nid_postfix(LibcException::ExceptionObject* s void APS5_VABI _ZNSt9bad_allocC2Ev_nid_postfix(LibcException::ExceptionObject* self) { self->vtable = &_ZTVSt9bad_alloc_nid_postfix.destroy; } LibcException::TypeRecord _ZTISt20bad_array_new_length_nid_postfix {LibcException::SingleTypeVtable + 2, "St20bad_array_new_length", &_ZTISt9bad_alloc_nid_postfix}; -LibcException::ExceptionVtable _ZTVSt20bad_array_new_length_nid_postfix {0, &_ZTISt20bad_array_new_length_nid_postfix, LibcException::DestroyPlain, LibcException::DeletePlain, LibcException::PlainWhat}; +LibcException::ExceptionVtable _ZTVSt20bad_array_new_length_nid_postfix {0, &_ZTISt20bad_array_new_length_nid_postfix, LibcException::GuestDestroyPlain_nid_no_patch, LibcException::GuestDeletePlain_nid_no_patch, LibcException::GuestPlainWhat_nid_no_patch}; void APS5_VABI _ZNSt20bad_array_new_lengthD1Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyPlain(self); } void APS5_VABI _ZNSt20bad_array_new_lengthD2Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyPlain(self); } void APS5_VABI _ZNSt20bad_array_new_lengthD0Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DeletePlain(self); } @@ -168,7 +320,7 @@ void APS5_VABI _ZNSt20bad_array_new_lengthC1Ev_nid_postfix(LibcException::Except void APS5_VABI _ZNSt20bad_array_new_lengthC2Ev_nid_postfix(LibcException::ExceptionObject* self) { self->vtable = &_ZTVSt20bad_array_new_length_nid_postfix.destroy; } LibcException::TypeRecord _ZTISt17bad_function_call_nid_postfix {LibcException::SingleTypeVtable + 2, "St17bad_function_call", &_ZTISt9exception_nid_postfix}; -LibcException::ExceptionVtable _ZTVSt17bad_function_call_nid_postfix {0, &_ZTISt17bad_function_call_nid_postfix, LibcException::DestroyPlain, LibcException::DeletePlain, LibcException::PlainWhat}; +LibcException::ExceptionVtable _ZTVSt17bad_function_call_nid_postfix {0, &_ZTISt17bad_function_call_nid_postfix, LibcException::GuestDestroyPlain_nid_no_patch, LibcException::GuestDeletePlain_nid_no_patch, LibcException::GuestPlainWhat_nid_no_patch}; void APS5_VABI _ZNSt17bad_function_callD1Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyPlain(self); } void APS5_VABI _ZNSt17bad_function_callD2Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyPlain(self); } void APS5_VABI _ZNSt17bad_function_callD0Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DeletePlain(self); } @@ -177,14 +329,26 @@ void APS5_VABI _ZNSt17bad_function_callC1Ev_nid_postfix(LibcException::Exception void APS5_VABI _ZNSt17bad_function_callC2Ev_nid_postfix(LibcException::ExceptionObject* self) { self->vtable = &_ZTVSt17bad_function_call_nid_postfix.destroy; } LibcException::TypeRecord _ZTISt12bad_weak_ptr_nid_postfix {LibcException::SingleTypeVtable + 2, "St12bad_weak_ptr", &_ZTISt9exception_nid_postfix}; -LibcException::ExceptionVtable _ZTVSt12bad_weak_ptr_nid_postfix {0, &_ZTISt12bad_weak_ptr_nid_postfix, LibcException::DestroyPlain, LibcException::DeletePlain, LibcException::PlainWhat}; +LibcException::ExceptionVtable _ZTVSt12bad_weak_ptr_nid_postfix {0, &_ZTISt12bad_weak_ptr_nid_postfix, LibcException::GuestDestroyPlain_nid_no_patch, LibcException::GuestDeletePlain_nid_no_patch, LibcException::GuestPlainWhat_nid_no_patch}; void APS5_VABI _ZNSt12bad_weak_ptrD1Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyPlain(self); } void APS5_VABI _ZNSt12bad_weak_ptrD2Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyPlain(self); } 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::DestroyMessage, LibcException::DeleteMessage, LibcException::MessageWhat}; +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); } void APS5_VABI _ZNSt11logic_errorD2Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyMessage(self); } void APS5_VABI _ZNSt11logic_errorD0Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DeleteMessage(self); } @@ -194,9 +358,13 @@ 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::DestroyMessage, LibcException::DeleteMessage, LibcException::MessageWhat}; +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}; void APS5_VABI _ZNSt13runtime_errorD1Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyMessage(self); } void APS5_VABI _ZNSt13runtime_errorD2Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyMessage(self); } void APS5_VABI _ZNSt13runtime_errorD0Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DeleteMessage(self); } @@ -208,7 +376,7 @@ void APS5_VABI _ZNSt13runtime_errorC2ERKS__nid_postfix(LibcException::ExceptionO LibcException::ExceptionObject* APS5_VABI _ZNSt13runtime_erroraSERKS__nid_postfix(LibcException::ExceptionObject* self, const LibcException::ExceptionObject* source) { return LibcException::Assign(self, source); } LibcException::TypeRecord _ZTISt16invalid_argument_nid_postfix {LibcException::SingleTypeVtable + 2, "St16invalid_argument", &_ZTISt11logic_error_nid_postfix}; -LibcException::ExceptionVtable _ZTVSt16invalid_argument_nid_postfix {0, &_ZTISt16invalid_argument_nid_postfix, LibcException::DestroyMessage, LibcException::DeleteMessage, LibcException::MessageWhat}; +LibcException::ExceptionVtable _ZTVSt16invalid_argument_nid_postfix {0, &_ZTISt16invalid_argument_nid_postfix, LibcException::GuestDestroyMessage_nid_no_patch, LibcException::GuestDeleteMessage_nid_no_patch, LibcException::GuestMessageWhat_nid_no_patch}; void APS5_VABI _ZNSt16invalid_argumentD1Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyMessage(self); } void APS5_VABI _ZNSt16invalid_argumentD2Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyMessage(self); } void APS5_VABI _ZNSt16invalid_argumentD0Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DeleteMessage(self); } @@ -220,7 +388,7 @@ void APS5_VABI _ZNSt16invalid_argumentC2ERKS__nid_postfix(LibcException::Excepti LibcException::ExceptionObject* APS5_VABI _ZNSt16invalid_argumentaSERKS__nid_postfix(LibcException::ExceptionObject* self, const LibcException::ExceptionObject* source) { return LibcException::Assign(self, source); } LibcException::TypeRecord _ZTISt12out_of_range_nid_postfix {LibcException::SingleTypeVtable + 2, "St12out_of_range", &_ZTISt11logic_error_nid_postfix}; -LibcException::ExceptionVtable _ZTVSt12out_of_range_nid_postfix {0, &_ZTISt12out_of_range_nid_postfix, LibcException::DestroyMessage, LibcException::DeleteMessage, LibcException::MessageWhat}; +LibcException::ExceptionVtable _ZTVSt12out_of_range_nid_postfix {0, &_ZTISt12out_of_range_nid_postfix, LibcException::GuestDestroyMessage_nid_no_patch, LibcException::GuestDeleteMessage_nid_no_patch, LibcException::GuestMessageWhat_nid_no_patch}; void APS5_VABI _ZNSt12out_of_rangeD1Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyMessage(self); } void APS5_VABI _ZNSt12out_of_rangeD2Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyMessage(self); } void APS5_VABI _ZNSt12out_of_rangeD0Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DeleteMessage(self); } @@ -232,7 +400,7 @@ void APS5_VABI _ZNSt12out_of_rangeC2ERKS__nid_postfix(LibcException::ExceptionOb LibcException::ExceptionObject* APS5_VABI _ZNSt12out_of_rangeaSERKS__nid_postfix(LibcException::ExceptionObject* self, const LibcException::ExceptionObject* source) { return LibcException::Assign(self, source); } LibcException::TypeRecord _ZTISt12domain_error_nid_postfix {LibcException::SingleTypeVtable + 2, "St12domain_error", &_ZTISt11logic_error_nid_postfix}; -LibcException::ExceptionVtable _ZTVSt12domain_error_nid_postfix {0, &_ZTISt12domain_error_nid_postfix, LibcException::DestroyMessage, LibcException::DeleteMessage, LibcException::MessageWhat}; +LibcException::ExceptionVtable _ZTVSt12domain_error_nid_postfix {0, &_ZTISt12domain_error_nid_postfix, LibcException::GuestDestroyMessage_nid_no_patch, LibcException::GuestDeleteMessage_nid_no_patch, LibcException::GuestMessageWhat_nid_no_patch}; void APS5_VABI _ZNSt12domain_errorD1Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyMessage(self); } void APS5_VABI _ZNSt12domain_errorD2Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyMessage(self); } void APS5_VABI _ZNSt12domain_errorD0Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DeleteMessage(self); } @@ -244,7 +412,7 @@ void APS5_VABI _ZNSt12domain_errorC2ERKS__nid_postfix(LibcException::ExceptionOb LibcException::ExceptionObject* APS5_VABI _ZNSt12domain_erroraSERKS__nid_postfix(LibcException::ExceptionObject* self, const LibcException::ExceptionObject* source) { return LibcException::Assign(self, source); } LibcException::TypeRecord _ZTISt12length_error_nid_postfix {LibcException::SingleTypeVtable + 2, "St12length_error", &_ZTISt11logic_error_nid_postfix}; -LibcException::ExceptionVtable _ZTVSt12length_error_nid_postfix {0, &_ZTISt12length_error_nid_postfix, LibcException::DestroyMessage, LibcException::DeleteMessage, LibcException::MessageWhat}; +LibcException::ExceptionVtable _ZTVSt12length_error_nid_postfix {0, &_ZTISt12length_error_nid_postfix, LibcException::GuestDestroyMessage_nid_no_patch, LibcException::GuestDeleteMessage_nid_no_patch, LibcException::GuestMessageWhat_nid_no_patch}; void APS5_VABI _ZNSt12length_errorD1Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyMessage(self); } void APS5_VABI _ZNSt12length_errorD2Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyMessage(self); } void APS5_VABI _ZNSt12length_errorD0Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DeleteMessage(self); } @@ -256,17 +424,27 @@ void APS5_VABI _ZNSt12length_errorC2ERKS__nid_postfix(LibcException::ExceptionOb LibcException::ExceptionObject* APS5_VABI _ZNSt12length_erroraSERKS__nid_postfix(LibcException::ExceptionObject* self, const LibcException::ExceptionObject* source) { return LibcException::Assign(self, source); } LibcException::TypeRecord _ZTISt12system_error_nid_postfix {LibcException::SingleTypeVtable + 2, "St12system_error", &_ZTISt13runtime_error_nid_postfix}; -LibcException::ExceptionVtable _ZTVSt12system_error_nid_postfix {0, &_ZTISt12system_error_nid_postfix, LibcException::DestroyMessage, LibcException::DeleteMessage, LibcException::MessageWhat}; +LibcException::ExceptionVtable _ZTVSt12system_error_nid_postfix {0, &_ZTISt12system_error_nid_postfix, LibcException::GuestDestroyMessage_nid_no_patch, LibcException::GuestDeleteMessage_nid_no_patch, LibcException::GuestMessageWhat_nid_no_patch}; void APS5_VABI _ZNSt12system_errorD1Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyMessage(self); } void APS5_VABI _ZNSt12system_errorD2Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyMessage(self); } void APS5_VABI _ZNSt12system_errorD0Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DeleteMessage(self); } const char* APS5_VABI _ZNKSt12system_error4whatEv_nid_postfix(const LibcException::ExceptionObject* self) { return LibcException::MessageWhat(self); } -void APS5_VABI _ZNSt12system_errorC1ERKS__nid_postfix(LibcException::ExceptionObject* self, const LibcException::ExceptionObject* source) { LibcException::Copy(self, source, _ZTVSt12system_error_nid_postfix); } -void APS5_VABI _ZNSt12system_errorC2ERKS__nid_postfix(LibcException::ExceptionObject* self, const LibcException::ExceptionObject* source) { LibcException::Copy(self, source, _ZTVSt12system_error_nid_postfix); } -LibcException::ExceptionObject* APS5_VABI _ZNSt12system_erroraSERKS__nid_postfix(LibcException::ExceptionObject* self, const LibcException::ExceptionObject* source) { return LibcException::Assign(self, source); } +void APS5_VABI _ZNSt12system_errorC1ERKS__nid_postfix(LibcException::SystemErrorObject* self, const LibcException::SystemErrorObject* source) { + LibcException::Copy(&self->base, &source->base, _ZTVSt12system_error_nid_postfix); + self->code = source->code; +} +void APS5_VABI _ZNSt12system_errorC2ERKS__nid_postfix(LibcException::SystemErrorObject* self, const LibcException::SystemErrorObject* source) { + LibcException::Copy(&self->base, &source->base, _ZTVSt12system_error_nid_postfix); + self->code = source->code; +} +LibcException::SystemErrorObject* APS5_VABI _ZNSt12system_erroraSERKS__nid_postfix(LibcException::SystemErrorObject* self, const LibcException::SystemErrorObject* source) { + LibcException::Assign(&self->base, &source->base); + self->code = source->code; + return self; +} LibcException::TypeRecord _ZTISt11regex_error_nid_postfix {LibcException::SingleTypeVtable + 2, "St11regex_error", &_ZTISt13runtime_error_nid_postfix}; -LibcException::ExceptionVtable _ZTVSt11regex_error_nid_postfix {0, &_ZTISt11regex_error_nid_postfix, LibcException::DestroyMessage, LibcException::DeleteMessage, LibcException::MessageWhat}; +LibcException::ExceptionVtable _ZTVSt11regex_error_nid_postfix {0, &_ZTISt11regex_error_nid_postfix, LibcException::GuestDestroyMessage_nid_no_patch, LibcException::GuestDeleteMessage_nid_no_patch, LibcException::GuestMessageWhat_nid_no_patch}; void APS5_VABI _ZNSt11regex_errorD1Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyMessage(self); } void APS5_VABI _ZNSt11regex_errorD2Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyMessage(self); } void APS5_VABI _ZNSt11regex_errorD0Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DeleteMessage(self); } @@ -276,13 +454,17 @@ void APS5_VABI _ZNSt11regex_errorC2ERKS__nid_postfix(LibcException::ExceptionObj LibcException::ExceptionObject* APS5_VABI _ZNSt11regex_erroraSERKS__nid_postfix(LibcException::ExceptionObject* self, const LibcException::ExceptionObject* source) { return LibcException::Assign(self, source); } LibcException::TypeRecord _ZTISt11range_error_nid_postfix {LibcException::SingleTypeVtable + 2, "St11range_error", &_ZTISt13runtime_error_nid_postfix}; -LibcException::ExceptionVtable _ZTVSt11range_error_nid_postfix {0, &_ZTISt11range_error_nid_postfix, LibcException::DestroyMessage, LibcException::DeleteMessage, LibcException::MessageWhat}; +LibcException::ExceptionVtable _ZTVSt11range_error_nid_postfix {0, &_ZTISt11range_error_nid_postfix, LibcException::GuestDestroyMessage_nid_no_patch, LibcException::GuestDeleteMessage_nid_no_patch, LibcException::GuestMessageWhat_nid_no_patch}; void APS5_VABI _ZNSt11range_errorD1Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyMessage(self); } void APS5_VABI _ZNSt11range_errorD2Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyMessage(self); } void APS5_VABI _ZNSt11range_errorD0Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DeleteMessage(self); } +LibcException::TypeRecord _ZTISt14overflow_error_nid_postfix {LibcException::SingleTypeVtable + 2, "St14overflow_error", &_ZTISt13runtime_error_nid_postfix}; +LibcException::ExceptionVtable _ZTVSt14overflow_error_nid_postfix {0, &_ZTISt14overflow_error_nid_postfix, LibcException::GuestDestroyMessage_nid_no_patch, LibcException::GuestDeleteMessage_nid_no_patch, LibcException::GuestMessageWhat_nid_no_patch}; +void APS5_VABI _ZNSt14overflow_errorD1Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyMessage(self); } + LibcException::TypeRecord _ZTINSt8ios_base7failureE_nid_postfix {LibcException::SingleTypeVtable + 2, "NSt8ios_base7failureE", &_ZTISt12system_error_nid_postfix}; -LibcException::ExceptionVtable _ZTVNSt8ios_base7failureE_nid_postfix {0, &_ZTINSt8ios_base7failureE_nid_postfix, LibcException::DestroyMessage, LibcException::DeleteMessage, LibcException::MessageWhat}; +LibcException::ExceptionVtable _ZTVNSt8ios_base7failureE_nid_postfix {0, &_ZTINSt8ios_base7failureE_nid_postfix, LibcException::GuestDestroyMessage_nid_no_patch, LibcException::GuestDeleteMessage_nid_no_patch, LibcException::GuestMessageWhat_nid_no_patch}; void APS5_VABI _ZNSt8ios_base7failureD1Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyMessage(self); } void APS5_VABI _ZNSt8ios_base7failureD2Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyMessage(self); } void APS5_VABI _ZNSt8ios_base7failureD0Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DeleteMessage(self); } @@ -295,15 +477,40 @@ 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))); LibcException::Construct(&object->base, _ZTVSt11regex_error_nid_postfix, "regular expression error"); object->code = code; - __cxa_throw_nid_postfix(object, reinterpret_cast(&_ZTISt11regex_error_nid_postfix), [](void* p) { LibcException::DestroyMessage(static_cast(p)); }); + __cxa_throw_nid_postfix(object, reinterpret_cast(&_ZTISt11regex_error_nid_postfix), + reinterpret_cast(LibcException::DestroyGuestMessage_nid_no_patch)); +} +[[noreturn]] void ThrowCppError_nid_no_patch(int code) { + static constexpr LibcException::CppError errors[] { + {16, "device or resource busy"}, + {22, "invalid argument"}, + {3, "no such process"}, + {12, "not enough memory"}, + {1, "operation not permitted"}, + {11, "resource deadlock would occur"}, + {35, "resource unavailable try again"} + }; + if (code < 0 || code >= static_cast(std::size(errors))) + throw std::out_of_range("_Throw_Cpp_error: " + std::to_string(code) + " is not a _Cpp_error code"); + const auto& error = errors[code]; + char message[128]; + std::snprintf(message, sizeof(message), "%s: %s", error.message, LibcException::GenericErrorMessage(error.value)); + auto* object = static_cast(__cxa_allocate_exception_nid_postfix(sizeof(LibcException::SystemErrorObject))); + LibcException::Construct(&object->base, _ZTVSt12system_error_nid_postfix, message); + object->code = {error.value, &LibcException::GenericCategory}; + __cxa_throw_nid_postfix(object, reinterpret_cast(&_ZTISt12system_error_nid_postfix), + reinterpret_cast(LibcException::DestroyGuestMessage_nid_no_patch)); } } diff --git a/core/libs/prx/libc/src/exception/TypeInfo.cpp b/core/libs/prx/libc/src/exception/TypeInfo.cpp index c745eda722..49ba0e47ee 100644 --- a/core/libs/prx/libc/src/exception/TypeInfo.cpp +++ b/core/libs/prx/libc/src/exception/TypeInfo.cpp @@ -13,6 +13,8 @@ void APS5_VABI DeleteTypeInfo(std::type_info*) { } bool APS5_VABI IsPointerTypeInfo(const std::type_info*) { return false; } +bool APS5_VABI IsPointerTypeInfoTrue(const std::type_info*) { return true; } +bool APS5_VABI IsFunctionTypeInfoTrue(const std::type_info*) { return true; } bool APS5_VABI CatchTypeInfo(const std::type_info* self, const std::type_info* thrown, void** object, unsigned) { if (!self || !thrown || !object) throw std::invalid_argument("invalid RTTI catch arguments"); @@ -65,4 +67,19 @@ extern "C" { LibcException::TypeInfoVtable _ZTVN10__cxxabiv117__class_type_infoE_nid_postfix {0, &typeid(__cxxabiv1::__class_type_info)}; LibcException::TypeInfoVtable _ZTVN10__cxxabiv120__si_class_type_infoE_nid_postfix {0, &typeid(__cxxabiv1::__si_class_type_info)}; LibcException::TypeInfoVtable _ZTVN10__cxxabiv121__vmi_class_type_infoE_nid_postfix {0, &typeid(__cxxabiv1::__vmi_class_type_info)}; +LibcException::TypeInfoVtable _ZTVN10__cxxabiv119__pointer_type_infoE_nid_postfix { + 0, + &typeid(__cxxabiv1::__pointer_type_info), + LibcException::DestroyTypeInfo, + LibcException::DeleteTypeInfo, + LibcException::IsPointerTypeInfoTrue +}; +LibcException::TypeInfoVtable _ZTVN10__cxxabiv120__function_type_infoE_nid_postfix { + 0, + &typeid(__cxxabiv1::__function_type_info), + LibcException::DestroyTypeInfo, + LibcException::DeleteTypeInfo, + LibcException::IsPointerTypeInfo, + LibcException::IsFunctionTypeInfoTrue +}; } diff --git a/core/libs/prx/libc/src/exception/Unwind.cpp b/core/libs/prx/libc/src/exception/Unwind.cpp index 9da514f5b2..c86389adfe 100644 --- a/core/libs/prx/libc/src/exception/Unwind.cpp +++ b/core/libs/prx/libc/src/exception/Unwind.cpp @@ -1,9 +1,27 @@ #include "prx/libc/include/exceptions/Unwind.hpp" +#include "prx/libc/include/exceptions/Runtime.hpp" #include "prx/libc/include/specifics/linux/ElfTypes.hpp" #include "prx/libc/src/specifics/x86_64/RegisterContext.cpp" +#if defined(__linux__) +#include +#endif #ifdef _WIN32 #include +#include +#include +#include +#include +#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 @@ -11,10 +29,80 @@ #include #endif +namespace { +constexpr int CursorInstructionRegister = -1; +constexpr int CursorStackRegister = -2; +constexpr int CursorRegisterCount = 17; + +std::uintptr_t* CursorRegister(_Unwind_Context* context, int number) { + if (number == CursorInstructionRegister) return &context->registers[16]; + if (number == CursorStackRegister) return &context->registers[7]; + if (number >= 0 && number < CursorRegisterCount) return &context->registers[number]; + return nullptr; +} + +bool APS5_VABI CursorValidRegister(_Unwind_Context* context, int number) { + return CursorRegister(context, number) != nullptr; +} + +std::uintptr_t APS5_VABI CursorGetRegister(_Unwind_Context* context, int number) { + auto* value = CursorRegister(context, number); + if (!value) NotImplemented_nid_no_patch("libunwind cursor: read of an unknown register"); + return value ? *value : 0; +} + +void APS5_VABI CursorSetRegister(_Unwind_Context* context, int number, std::uintptr_t value) { + auto* slot = CursorRegister(context, number); + if (!slot) NotImplemented_nid_no_patch("libunwind cursor: write of an unknown register"); + else *slot = value; +} + +bool APS5_VABI CursorValidFloatRegister(_Unwind_Context*, int) { + return false; +} + +bool APS5_VABI CursorIsSignalFrame(_Unwind_Context* context) { + return context->signalFrame; +} + +void APS5_VABI CursorUnsupported(_Unwind_Context*) { + NotImplemented_nid_no_patch("libunwind cursor: operation other than register access or isSignalFrame"); +} + +template std::uintptr_t Slot(Function function) { + return reinterpret_cast(function); +} + +const std::uintptr_t CursorSlots[] = { + Slot(&CursorUnsupported), + Slot(&CursorUnsupported), + Slot(&CursorValidRegister), + Slot(&CursorGetRegister), + Slot(&CursorSetRegister), + Slot(&CursorValidFloatRegister), + Slot(&CursorUnsupported), + Slot(&CursorUnsupported), + Slot(&CursorUnsupported), + Slot(&CursorUnsupported), + Slot(&CursorUnsupported), + Slot(&CursorIsSignalFrame), + Slot(&CursorUnsupported), + Slot(&CursorUnsupported), + Slot(&CursorUnsupported), +}; +} + +const std::uintptr_t* UnwindCursorSlots_nid_no_patch() { + return CursorSlots; +} + #if defined(__linux__) || defined(_WIN32) || defined(__APPLE__) extern "C" _Unwind_Reason_Code __gxx_personality_v0(int, _Unwind_Action, std::uint64_t, _Unwind_Exception*, _Unwind_Context*); 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) { @@ -32,9 +120,9 @@ _Unwind_Reason_Code CallPersonality(Word personality, _Unwind_Action actions, _U return __gcc_personality_v0_nid_postfix(1, actions, exception->exception_class, exception, context); return (actions & _UA_SEARCH_PHASE) ? _URC_FATAL_PHASE1_ERROR : _URC_FATAL_PHASE2_ERROR; } -struct Lookup { Word pc; const Byte* fde {}; Word text {}; Word data {}; }; - #if defined(__linux__) || defined(__APPLE__) +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; @@ -48,10 +136,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; @@ -65,67 +158,47 @@ 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 -struct Frame { - const Byte* cieBegin {}; - const Byte* cieEnd {}; - const Byte* begin {}; - const Byte* end {}; - Word start {}, length {}, personality {}, lsda {}; - Word codeAlign {}; - std::intptr_t dataAlign {}; - unsigned returnRegister {}; - bool signal {}; -}; +#endif bool DecodeCandidate(_Unwind_Context& context, Frame& frame, const Lookup& query) { - if (!query.fde) return false; - const Byte* p = query.fde; - auto length = Read(p); - if (!length || length == 0xffffffff) return false; - frame.end = p + length; - const Byte* ciePointer = p; - auto cieOffset = Read(p); - const Byte* cie = ciePointer - cieOffset; - const Byte* c = cie; - auto cieLength = Read(c); - frame.cieEnd = c + cieLength; - if (Read(c) != 0) return false; - Byte version = *c++; - if (version != 1 && version != 3 && version != 4) return false; - const char* augmentation = reinterpret_cast(c); - while (*c++) {} - if (version == 4 && (*c++ != sizeof(Word) || *c++ != 0)) return false; - frame.codeAlign = Uleb(c); - frame.dataAlign = Sleb(c); - frame.returnRegister = version == 1 ? *c++ : Uleb(c); -#ifdef _WIN32 - if (frame.returnRegister >= 17 && frame.returnRegister != 32) return false; -#else - if (frame.returnRegister >= 17) return false; -#endif - Byte pointerEncoding = 0, lsdaEncoding = 255; - if (*augmentation == 'z') { - Word size = Uleb(c); - const Byte* end = c + size; - for (const char* a = augmentation + 1; *a; ++a) { - switch (*a) { - case 'R': pointerEncoding = *c++; break; - case 'L': lsdaEncoding = *c++; break; - case 'P': { Byte encoding = *c++; frame.personality = Encoded(c, encoding, query.data, 0, query.text); break; } - case 'S': frame.signal = true; break; - default: return false; - } - } - c = end; - } else if (*augmentation) return false; - frame.cieBegin = c; - frame.start = Encoded(p, pointerEncoding, query.data, 0, query.text); - frame.length = Encoded(p, pointerEncoding & 15); + const Byte* p; + const char* augmentation; + Byte lsdaEncoding; + if (!DecodeRange(frame, query, p, augmentation, lsdaEncoding)) return false; if (query.pc < frame.start || query.pc - frame.start >= frame.length) return false; if (*augmentation == 'z') { Word size = Uleb(p); @@ -142,10 +215,82 @@ bool DecodeCandidate(_Unwind_Context& context, Frame& frame, const Lookup& query return true; } +#ifdef _WIN32 +using FrameIndex = std::vector>; +using ImageKey = std::array; + +const FrameIndex& IndexFrames(const ImageKey& key, const Byte* begin, const Byte* end) { + static std::mutex mutex; + static std::map indexes; + std::lock_guard lock(mutex); + const auto [entry, inserted] = indexes.try_emplace(key); + auto& index = entry->second; + if (!inserted) return index; + const Byte* p = begin; + while (end - p >= 8) { + const Byte* record = p; + const auto length = Read(p); + if (!length) continue; + if (length == 0xffffffff || length < 4 || Word(end - p) < length) break; + const Byte* next = p + length; + if (Read(p)) { + Frame candidate {}; + const Byte* cursor; + const char* augmentation; + Byte lsdaEncoding; + if (DecodeRange(candidate, Lookup {0, record}, cursor, augmentation, lsdaEncoding)) + index.emplace_back(candidate.start, record); + } + p = next; + } + std::sort(index.begin(), index.end()); + return index; +} + +const IMAGE_NT_HEADERS64* ImageHeaders(Word pc, const Byte*& base) { + void* image = nullptr; + if (!RtlPcToFileHeader(reinterpret_cast(pc), &image) || !image) return nullptr; + base = static_cast(image); + const auto* dos = reinterpret_cast(base); + if (dos->e_magic != IMAGE_DOS_SIGNATURE) return nullptr; + const auto* nt = reinterpret_cast(base + dos->e_lfanew); + return nt->Signature == IMAGE_NT_SIGNATURE ? nt : nullptr; +} + +bool VirtualUnwind(const _Unwind_Context& context, _Unwind_Context& caller) { + 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; + DWORD64 imageBase = 0; + auto* function = RtlLookupFunctionEntry(pc, &imageBase, nullptr); + if (!function) return false; + CONTEXT state {}; + DWORD64* const registers[17] {&state.Rax, &state.Rdx, &state.Rcx, &state.Rbx, &state.Rsi, &state.Rdi, &state.Rbp, &state.Rsp, + &state.R8, &state.R9, &state.R10, &state.R11, &state.R12, &state.R13, &state.R14, &state.R15, &state.Rip}; + for (unsigned i = 0; i < 17; ++i) *registers[i] = context.registers[i]; + 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; + 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; + return true; +} +#endif + 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 {}; @@ -172,14 +317,9 @@ bool DecodeFrame(_Unwind_Context& context, Frame& frame) { #endif return DecodeCandidate(context, frame, query); #else - MEMORY_BASIC_INFORMATION memory{}; - if (!VirtualQuery(reinterpret_cast(query.pc), &memory, sizeof(memory)) || memory.Type != MEM_IMAGE) - return false; - const auto* base = static_cast(memory.AllocationBase); - const auto* dos = reinterpret_cast(base); - if (dos->e_magic != IMAGE_DOS_SIGNATURE) return false; - const auto* nt = reinterpret_cast(base + dos->e_lfanew); - if (nt->Signature != IMAGE_NT_SIGNATURE) return false; + const Byte* base = nullptr; + const auto* nt = ImageHeaders(query.pc, base); + if (!nt) return false; const auto* sections = IMAGE_FIRST_SECTION(nt); for (unsigned i = 0; i < nt->FileHeader.NumberOfSections; ++i) { const auto& section = sections[i]; @@ -205,89 +345,19 @@ bool DecodeFrame(_Unwind_Context& context, Frame& frame) { return DecodeCandidate(context, frame, query); } if (std::memcmp(section.Name, ".ehfram", 8) != 0) continue; - const Byte* p = base + section.VirtualAddress; - const Byte* end = p + section.Misc.VirtualSize; - while (end - p >= 8) { - const Byte* record = p; - const auto length = Read(p); - if (!length) continue; - if (length == 0xffffffff || length < 4 || Word(end - p) < length) return false; - const Byte* next = p + length; - if (Read(p)) { - query.fde = record; - frame = {}; - if (DecodeCandidate(context, frame, query)) return true; - } - p = next; - } + const Byte* frames = base + section.VirtualAddress; + const ImageKey key {Word(base), nt->FileHeader.TimeDateStamp, nt->OptionalHeader.CheckSum, nt->OptionalHeader.SizeOfImage, section.VirtualAddress, section.Misc.VirtualSize}; + const auto& index = IndexFrames(key, frames, frames + section.Misc.VirtualSize); + auto found = std::upper_bound(index.begin(), index.end(), query.pc, [](Word pc, const std::pair& entry) { return pc < entry.first; }); + if (found == index.begin()) continue; + query.fde = std::prev(found)->second; + frame = {}; + if (DecodeCandidate(context, frame, query)) return true; } return false; #endif } -struct Rule { unsigned kind {}; std::intptr_t value {}; const Byte* expression {}; }; -#ifdef _WIN32 -constexpr unsigned RuleCount = 33; -#else -constexpr unsigned RuleCount = 17; -#endif - -struct Rules { Rule registers[RuleCount] {}; unsigned cfaRegister {7}; std::intptr_t cfaOffset {}; const Byte* cfaExpression {}; }; - -bool Instructions(const Byte* p, const Byte* end, const Frame& frame, Word target, Rules& state, const Rules& initial) { - Word location = frame.start; - Rules saved[16]; - unsigned depth = 0; - while (p < end && location <= target) { - Byte opcode = *p++; - unsigned reg; - if ((opcode & 192) == 64) { location += (opcode & 63) * frame.codeAlign; continue; } - if ((opcode & 192) == 128) { - reg = opcode & 63; - auto offset = Uleb(p) * frame.dataAlign; - if (reg < RuleCount) state.registers[reg] = {1, static_cast(offset)}; - continue; - } - if ((opcode & 192) == 192) { - reg = opcode & 63; - if (reg < RuleCount) state.registers[reg] = initial.registers[reg]; - continue; - } - switch (opcode) { - case 0: break; - case 1: location = Read(p); break; - case 2: location += Read(p) * frame.codeAlign; break; - case 3: location += Read(p) * frame.codeAlign; break; - case 4: location += Read(p) * frame.codeAlign; break; - case 5: case 17: case 20: case 21: { - reg = Uleb(p); - auto offset = (opcode == 17 || opcode == 21 ? Sleb(p) : std::intptr_t(Uleb(p))) * frame.dataAlign; - if (reg < RuleCount) state.registers[reg] = {unsigned(opcode >= 20 ? 4 : 1), offset}; - break; - } - case 6: reg = Uleb(p); if (reg < RuleCount) state.registers[reg] = initial.registers[reg]; break; - case 7: case 8: reg = Uleb(p); if (reg < RuleCount) state.registers[reg] = {unsigned(opcode == 7 ? 5 : 0)}; break; - case 9: { reg = Uleb(p); auto other = Uleb(p); if (reg < RuleCount) state.registers[reg] = {2, std::intptr_t(other)}; break; } - case 10: if (depth == 16) return false; saved[depth++] = state; break; - case 11: if (!depth) return false; state = saved[--depth]; break; - case 12: case 18: - state.cfaExpression = nullptr; state.cfaRegister = Uleb(p); - state.cfaOffset = opcode == 18 ? Sleb(p) * frame.dataAlign : Uleb(p); break; - case 13: state.cfaRegister = Uleb(p); break; - case 14: case 19: state.cfaOffset = opcode == 19 ? Sleb(p) * frame.dataAlign : Uleb(p); break; - case 15: { state.cfaExpression = p; auto size = Uleb(p); p += size; break; } - case 16: case 22: { - reg = Uleb(p); const Byte* expr = p; auto size = Uleb(p); p += size; - if (reg < RuleCount) state.registers[reg] = {unsigned(opcode == 16 ? 3 : 6), 0, expr}; - break; - } - case 46: Uleb(p); break; - default: return false; - } - } - return true; -} - bool Expression(const Byte* p, const _Unwind_Context& context, Word cfa, Word& result) { Word length = Uleb(p); const Byte* end = p + length; @@ -341,7 +411,18 @@ bool Expression(const Byte* p, const _Unwind_Context& context, Word cfa, Word& r } bool GetRules(_Unwind_Context& context, Frame& frame, Rules& rules) { - if (!DecodeFrame(context, frame)) return false; + if (!DecodeFrame(context, frame)) { +#ifdef _WIN32 + frame = {}; + if (!VirtualUnwind(context, frame.caller)) 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; +#endif + } Rules initial; if (!Instructions(frame.cieBegin, frame.cieEnd, frame, ~Word(0), initial, {})) return false; #ifdef _WIN32 @@ -362,6 +443,14 @@ bool GetRules(_Unwind_Context& context, Frame& frame, Rules& rules) { bool Step(_Unwind_Context& context) { Frame frame; Rules rules; if (!GetRules(context, frame, rules)) return false; +#ifdef _WIN32 + if (frame.foreign) { + const auto& next = frame.caller; + if (!next.registers[16] || (next.registers[7] == context.registers[7] && next.registers[16] == context.registers[16])) return false; + context = next; + return true; + } +#endif _Unwind_Context next = context; for (unsigned i = 0; i < 17; ++i) { const auto& rule = rules.registers[i]; @@ -462,6 +551,14 @@ _Unwind_Reason_Code APS5_VABI _Unwind_RaiseException_nid_postfix(_Unwind_Excepti } [[noreturn]] void APS5_VABI _Unwind_Resume_nid_postfix(_Unwind_Exception* exception) { +#if defined(__linux__) + if (!LibcException::Native(exception->exception_class) && !exception->private_1) { + using Resume = void (*)(_Unwind_Exception*); + const auto resume = reinterpret_cast(dlsym(RTLD_NEXT, "_Unwind_Resume")); + if (!resume) std::abort(); + resume(exception); + } +#endif _Unwind_Context context; LibcCaptureRegisters(context.registers); if (!LibcUnwind::Step(context)) std::abort(); @@ -475,6 +572,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/CrashReport.cpp b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp index 099d6392ab..7b4cc729ed 100644 --- a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp +++ b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp @@ -1,7 +1,12 @@ #ifdef _WIN32 #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 #include @@ -10,6 +15,7 @@ #include #include #include +#include #include #include #include "Sse4aEmulation.hpp" @@ -31,6 +37,8 @@ void Report(const char* format, ...) { } void ReportAllThreads(); +void ReportStackCandidates(std::uint64_t rsp, int limit, std::uint64_t span); +void ReportCallTrace(); void DescribeAddress(std::uint64_t address, char* buffer, std::size_t size) { HMODULE module = nullptr; @@ -222,6 +230,102 @@ void TraceSse4a(std::uint64_t rip, const sse4a::Instruction& instruction, const Report("[sse4a] #%llu %s %s (length %u, index %u, %u bytes) at %s on thread %lu\n", count, mnemonic, operands, field.length, field.index, static_cast(instruction.size), line, GetCurrentThreadId()); } +constexpr DWORD GccExceptionMask = 0x20ffffffu; +constexpr DWORD GccException = 0x20474343u; +constexpr std::uint64_t GccExceptionClass = 0x474e5543432b2b00ull; +constexpr std::uint64_t CxaExceptionTypeOffset = 80; + +std::uint64_t FrameWithoutUnwindData(const CONTEXT& start) { + const HMODULE runtime[] = {GetModuleHandleA("KERNELBASE.dll"), GetModuleHandleA("libgcc_s_seh-1.dll"), GetModuleHandleA("libstdc++-6.dll")}; + CONTEXT context = start; + for (int depth = 0; depth < 64 && context.Rip != 0; ++depth) { + HMODULE module = nullptr; + GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(context.Rip), &module); + DWORD64 imageBase = 0; + const auto entry = RtlLookupFunctionEntry(context.Rip, &imageBase, nullptr); + if (module == nullptr || std::find(std::begin(runtime), std::end(runtime), module) == std::end(runtime)) return entry == nullptr ? context.Rip : 0; + if (entry == nullptr) return 0; + PVOID handlerData = nullptr; + DWORD64 establisher = 0; + RtlVirtualUnwind(UNW_FLAG_NHANDLER, imageBase, context.Rip, entry, &context, &handlerData, &establisher, nullptr); + } + return 0; +} + +bool ReadWord(std::uint64_t address, std::uint64_t* value) { + return ReadCode(address, reinterpret_cast(value), sizeof(*value)) == sizeof(*value); +} + +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"; + const auto header = record->NumberParameters >= 1 ? static_cast(record->ExceptionInformation[0]) : 0; + std::uint64_t exceptionClass = 0; + std::uint64_t typeInfo = 0; + std::uint64_t namePointer = 0; + if (ReadWord(header, &exceptionClass) && exceptionClass == GccExceptionClass && ReadWord(header - CxaExceptionTypeOffset, &typeInfo) && ReadWord(typeInfo + 8, &namePointer)) { + std::uint8_t name[sizeof(type)]; + const auto copied = ReadCode(namePointer, name, sizeof(name)); + if (const auto* end = static_cast(std::memchr(name, 0, copied))) std::memcpy(type, name, static_cast(end - name) + 1); + } + char line[MAX_PATH + 64]; + DescribeAddress(frame, line, sizeof(line)); + Report("\nFATAL: C++ exception %s raised on thread %lu cannot be delivered: %s has no unwind data\n", type, GetCurrentThreadId(), line); + ReportStackCandidates(info->ContextRecord->Rsp, 32, 0x4000); +} + bool HandleSse4a(EXCEPTION_POINTERS* info) { if (!g_sse4aEmulation) return false; const auto* record = info->ExceptionRecord; @@ -242,14 +346,20 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { static std::atomic reported{false}; const auto* fault = info->ExceptionRecord; if (fault->ExceptionCode == EXCEPTION_ACCESS_VIOLATION && fault->NumberParameters >= 2 && fault->ExceptionInformation[0] == 1 && GuestArena::GuestArenaHandleWrite_nid_postfix(fault->ExceptionInformation[1])) return EXCEPTION_CONTINUE_EXECUTION; + if (fault->ExceptionCode == EXCEPTION_ACCESS_VIOLATION && fault->NumberParameters >= 2 && fault->ExceptionInformation[0] == 0 && GuestArena::GuestArenaHandleRead_nid_postfix(fault->ExceptionInformation[1])) return EXCEPTION_CONTINUE_EXECUTION; if (HandleWatch(info)) return EXCEPTION_CONTINUE_EXECUTION; if (HandleSse4a(info)) return EXCEPTION_CONTINUE_EXECUTION; const auto* record = info->ExceptionRecord; + if ((record->ExceptionCode & GccExceptionMask) == GccException) { + if (const auto frame = FrameWithoutUnwindData(*info->ContextRecord)) ReportUndeliverableException(info, frame); + return EXCEPTION_CONTINUE_SEARCH; + } if (!IsFatal(record->ExceptionCode)) return EXCEPTION_CONTINUE_SEARCH; if (reported.exchange(true)) { // Another thread is already reporting; let it finish before this fault ends the process. Sleep(INFINITE); } + LibcRunEndFlush_nid_no_patch(); const auto* context = info->ContextRecord; char line[MAX_PATH + 64]; char threadName[128] = ""; @@ -271,6 +381,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); @@ -278,11 +393,33 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { Report(" r12 %016llx r13 %016llx r14 %016llx r15 %016llx\n", context->R12, context->R13, context->R14, context->R15); // The words each register points at, to recognise which structure a bad pointer came from. { + const DWORD indirectLength = GetEnvironmentVariableA("APS5_CRASH_INDIRECT_MEMORY", nullptr, 0); + const bool indirectMemory = indirectLength > 1; const std::pair pointers[] = {{"rax", context->Rax}, {"rbx", context->Rbx}, {"rcx", context->Rcx}, {"rdx", context->Rdx}, {"rsi", context->Rsi}, {"rdi", context->Rdi}, {"r12", context->R12}, {"r13", context->R13}, {"r14", context->R14}, {"r15", context->R15}}; for (const auto& [name, value] : pointers) { if (value < 0x10000 || (value & 7) != 0 || !IsReadable(value) || !IsReadable(value + 0x38)) continue; const auto* words = reinterpret_cast(value); Report(" [%s] %016llx %016llx %016llx %016llx %016llx %016llx %016llx %016llx\n", name, words[0], words[1], words[2], words[3], words[4], words[5], words[6], words[7]); + if (!indirectMemory) continue; + for (std::size_t i = 0; i < 8; ++i) { + const auto address = words[i]; + if (address < 0x10000 || IsExecutable(address)) continue; + std::uint64_t content[8]{}; + SIZE_T copied = 0; + if (!ReadProcessMemory(GetCurrentProcess(), reinterpret_cast(address), content, sizeof(content), &copied) || copied != sizeof(content)) continue; + Report(" [[%s+0x%llx]] @%016llx %016llx %016llx %016llx %016llx %016llx %016llx %016llx %016llx\n", + name, static_cast(i * sizeof(*words)), static_cast(address), + content[0], content[1], content[2], content[3], content[4], content[5], content[6], content[7]); + } + } + } + { + 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"); @@ -296,17 +433,9 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { if (next <= frame) break; frame = next; } - Report(" stack return address candidates:\n"); - int printed = 0; - for (std::uint64_t slot = context->Rsp; printed < 16 && slot < context->Rsp + 0x2000; slot += 8) { - if (!IsReadable(slot)) break; - const auto value = *reinterpret_cast(slot); - if (!IsExecutable(value)) continue; - DescribeAddress(value, line, sizeof(line)); - Report(" [rsp+0x%llx] %s\n", static_cast(slot - context->Rsp), line); - ++printed; - } - if (context->Rip == 0 || !IsExecutable(context->Rip)) { + ReportStackCandidates(context->Rsp, 16, 0x2000); + 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"); @@ -333,12 +462,45 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { } } } + ReportCallTrace(); std::fflush(stderr); ReportAllThreads(); + LibcRunFailureReports_nid_no_patch(); std::fflush(stderr); return EXCEPTION_CONTINUE_SEARCH; } +void ReportCallTrace() { + static CallTraceEntry entries[64]; + const std::size_t count = CopyCallTrace_nid_no_patch(entries, std::size(entries)); + if (count == 0) return; + char line[MAX_PATH + 64]; + Report(" last traced system library calls (APS5_TRACE_CALLS):\n"); + for (std::size_t index = 0; index < count; ++index) { + DescribeAddress(entries[index].returnAddress, line, sizeof(line)); + Report(" #%llu thread %llu %s %s from %s\n", static_cast(entries[index].sequence), + static_cast(entries[index].thread), entries[index].library, entries[index].name, line); + } + static 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); + if (counted == 0) return; + Report(" most called traced imports (%llu calls in total):\n", static_cast(total)); + for (std::size_t index = 0; index < counted; ++index) { + Report(" %12llu %s %s\n", static_cast(counts[index].calls), counts[index].library, counts[index].name); + } +} + std::size_t CaptureFrames(const CONTEXT* context, std::uint64_t* frames, std::size_t capacity) { std::size_t count = 0; std::uint64_t frame = context->Rbp; @@ -353,7 +515,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)); @@ -362,6 +534,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() { @@ -377,6 +554,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; @@ -399,6 +578,8 @@ 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; @@ -413,13 +594,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)); } @@ -433,6 +616,20 @@ DWORD WINAPI HangWatchdog(LPVOID param) { return 0; } +void ReportStackCandidates(std::uint64_t rsp, int limit, std::uint64_t span) { + char line[256]; + Report(" stack return address candidates:\n"); + int printed = 0; + for (std::uint64_t slot = rsp; printed < limit && slot < rsp + span; slot += 8) { + if (!IsReadable(slot)) break; + const auto value = *reinterpret_cast(slot); + if (!IsExecutable(value)) continue; + DescribeAddress(value, line, sizeof(line)); + Report(" [rsp+0x%llx] %s\n", static_cast(slot - rsp), line); + ++printed; + } +} + // abort() and the UCRT's invalid-parameter path end the process with a silent fast fail // (0xc0000409); both are reported with the calling thread's stack first. void ReportBacktrace(const char* what, bool reportThreads) { @@ -444,16 +641,20 @@ void ReportBacktrace(const char* what, bool reportThreads) { DescribeAddress(reinterpret_cast(frames[i]), line, sizeof(line)); Report(" #%u %s\n", static_cast(i), line); } + ReportStackCandidates(reinterpret_cast(__builtin_frame_address(0)), 32, 0x4000); + ReportCallTrace(); if (reportThreads) ReportAllThreads(); std::fflush(stderr); } 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/x86_64/CallTrace.cpp b/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp new file mode 100644 index 0000000000..96d6c872a9 --- /dev/null +++ b/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp @@ -0,0 +1,448 @@ +#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 + +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; + 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{}; + 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; + 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; + 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 d8e8087d03..9926ac1dd6 100644 --- a/core/libs/prx/libkernel/Aio/src/Aio.cpp +++ b/core/libs/prx/libkernel/Aio/src/Aio.cpp @@ -6,11 +6,13 @@ #include #include #include -#include #include "SceTypes.hpp" #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" #ifndef _WIN32 @@ -28,6 +30,10 @@ constexpr std::int32_t AioRequestNumMax = 128; constexpr std::int32_t AioIdNumMax = 128; constexpr std::uint32_t AioWaitAnd = 1; constexpr std::uint32_t AioWaitOr = 2; +constexpr std::int32_t AioSchedulingWindowMax = 128; +constexpr std::int32_t AioDelayedCountMax = 128; +constexpr std::uint32_t AioSplitSizeMax = 0x1000000; +constexpr std::uint32_t AioSplitChunkSizeMax = 0x1000000; std::mutex g_mutex; std::array g_states{}; @@ -57,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; @@ -69,14 +80,16 @@ 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 - return static_cast(::pwrite(fd, buf, nbyte, static_cast(offset))); + const auto result = File::PwriteSharedMemory_nid_no_patch(fd, buf, nbyte, offset); + return result == -2 ? static_cast(::pwrite(fd, buf, nbyte, static_cast(offset))) : result; #endif } @@ -89,6 +102,7 @@ bool RunRequest(KernelAioRwRequest& req, bool write) { req.result->return_value = static_cast(SCE_KERNEL_ERROR_EIO); if (error == EBADF) req.result->return_value = static_cast(SCE_KERNEL_ERROR_EBADF); if (error == EFAULT) req.result->return_value = static_cast(SCE_KERNEL_ERROR_EFAULT); + if (error == EINVAL) req.result->return_value = static_cast(SCE_KERNEL_ERROR_EINVAL); req.result->state = AioAborted; return false; } @@ -120,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); @@ -203,6 +217,20 @@ void APS5_VABI sceKernelAioInitializeParam(void* param) { if (param == nullptr) throw std::invalid_argument("sceKernelAioInitializeParam: param is null"); } +int APS5_VABI sceKernelAioSetParam(KernelAioSchedulingParam* param, int32_t schedulingWindowSize, int32_t delayedCountLimit, uint32_t enableSplit, uint32_t splitSize, uint32_t splitChunkSize) { + if (param == nullptr) throw std::invalid_argument("sceKernelAioSetParam: param is null"); + if (schedulingWindowSize < 0 || schedulingWindowSize > AioSchedulingWindowMax) throw std::runtime_error("sceKernelAioSetParam: a scheduling window outside 0..128 is not modelled"); + if (delayedCountLimit < 0 || delayedCountLimit > AioDelayedCountMax) throw std::runtime_error("sceKernelAioSetParam: a delayed count limit outside 0..128 is not modelled"); + if (enableSplit > 1) throw std::runtime_error("sceKernelAioSetParam: a split flag other than 0 or 1 is not modelled"); + if (splitSize > AioSplitSizeMax || splitChunkSize > AioSplitChunkSizeMax) throw std::runtime_error("sceKernelAioSetParam: a split size above 0x1000000 is not modelled"); + param->schedulingWindowSize = schedulingWindowSize; + param->delayedCountLimit = delayedCountLimit; + param->enableSplit = enableSplit; + param->splitSize = splitSize; + param->splitChunkSize = splitChunkSize; + return 0; +} + int APS5_VABI sceKernelAioSubmitReadCommands(KernelAioRwRequest* req, int32_t size, int32_t prio, int32_t* id) { (void)prio; return SubmitCommands(req, size, false, id); @@ -252,7 +280,7 @@ int APS5_VABI sceKernelAioWaitRequests(int32_t* id, int32_t num, int32_t* state, const auto elapsed = std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count(); if (elapsed > static_cast(*usec)) return SCE_KERNEL_ERROR_ETIMEDOUT; } - std::this_thread::sleep_for(std::chrono::microseconds(10)); + PreciseSleepUs(10); } } @@ -277,7 +305,7 @@ int APS5_VABI sceKernelAioWaitRequest(int32_t id, int32_t* state, uint32_t* usec return SCE_KERNEL_ERROR_ETIMEDOUT; } } - std::this_thread::sleep_for(std::chrono::microseconds(10)); + PreciseSleepUs(10); } } diff --git a/core/libs/prx/libkernel/AppMetadata/include/AppMetadata.hpp b/core/libs/prx/libkernel/AppMetadata/include/AppMetadata.hpp index d18f2e5202..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,9 +24,12 @@ 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(); +std::uint32_t GetAppAttribute3_nid_no_patch(); std::int32_t GetAppUserDefinedParam_nid_postfix(std::uint32_t index); } diff --git a/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp b/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp index 1737a427dd..65e2c6c0a8 100644 --- a/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp +++ b/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp @@ -3,12 +3,16 @@ #include #include +#include #include struct ParsedParamJson { std::string title; std::string titleId; + std::string contentVersion; std::uint64_t downloadDataSizeMiB = 0; + std::uint32_t attribute3 = 0; + std::optional flexibleMemorySizeBytes; std::int32_t userDefinedParams[4] = {}; }; diff --git a/core/libs/prx/libkernel/AppMetadata/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 79df79827a..3f46122f14 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp @@ -1,27 +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) { @@ -30,14 +36,21 @@ 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)); g_titleLoaded = true; } @@ -45,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&) { @@ -54,7 +68,20 @@ const bool g_downloadDataMounted = [] { } }(); +const bool g_dataMounted = [] { + try { + const auto run = std::filesystem::current_path(); + 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&) { + return false; + } +}(); + void ensureIconLoaded() { + std::lock_guard lock(g_metadataMutex); if (g_iconAttempted) return; g_iconAttempted = true; const auto resolvedPath = ResolvePath_nid_no_patch(AppMetadataIconGuestPath); @@ -63,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); @@ -89,6 +117,11 @@ std::uint64_t GetAppDownloadDataSizeMiB_nid_postfix() { return g_downloadDataSizeMiB; } +std::uint32_t GetAppAttribute3_nid_no_patch() { + ensureTitleLoaded(); + return g_attribute3; +} + std::int32_t GetAppUserDefinedParam_nid_postfix(std::uint32_t index) { if (index >= 4) throw std::out_of_range("user defined param index out of range"); ensureTitleLoaded(); @@ -102,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 3645c476dd..3150e1419a 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp @@ -1,9 +1,11 @@ #include "prx/libkernel/AppMetadata/include/ParamJsonParser.hpp" +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include #include #include #include +#include #include #include #include @@ -95,6 +97,27 @@ std::uint32_t parseHex4(const std::string& text, std::size_t& position) { return value; } +void appendRawUtf8(const std::string& text, std::size_t& position, unsigned char first, std::string& result) { + unsigned count; + std::uint32_t codepoint; + std::uint32_t minimum; + if (first >= 0xC2 && first <= 0xDF) { count = 1; codepoint = first & 0x1F; minimum = 0x80; } + else if (first >= 0xE0 && first <= 0xEF) { count = 2; codepoint = first & 0x0F; minimum = 0x800; } + else if (first >= 0xF0 && first <= 0xF4) { count = 3; codepoint = first & 0x07; minimum = 0x10000; } + else throw std::runtime_error("invalid UTF-8 in param.json"); + if (count > text.size() - position) throw std::runtime_error("truncated UTF-8 in param.json"); + for (unsigned index = 0; index < count; ++index) { + const auto byte = static_cast(text[position + index]); + if ((byte & 0xC0) != 0x80) throw std::runtime_error("invalid UTF-8 continuation in param.json"); + codepoint = (codepoint << 6) | (byte & 0x3F); + } + if (codepoint < minimum || codepoint > 0x10FFFF || (codepoint >= 0xD800 && codepoint <= 0xDFFF)) + throw std::runtime_error("invalid UTF-8 codepoint in param.json"); + result.push_back(static_cast(first)); + result.append(text, position, count); + position += count; +} + std::string parseJsonString(const std::string& text, std::size_t& position) { expectChar(text, position, '"'); std::string result; @@ -103,7 +126,11 @@ std::string parseJsonString(const std::string& text, std::size_t& position) { const char c = text[position++]; if (c == '"') break; if (static_cast(c) < 0x20) throw std::runtime_error("control character in param.json string"); - if (c != '\\') { result.push_back(c); continue; } + if (c != '\\') { + if (static_cast(c) < 0x80) result.push_back(c); + else appendRawUtf8(text, position, static_cast(c), result); + continue; + } if (position >= text.size()) throw std::runtime_error("unterminated escape in param.json"); const char escape = text[position++]; switch (escape) { @@ -135,9 +162,12 @@ std::string parseJsonString(const std::string& text, std::size_t& position) { return result; } -JsonValue parseJsonValue(const std::string& text, std::size_t& position); +constexpr std::size_t MaxNestingDepth = 512; -JsonValue parseJsonObject(const std::string& text, std::size_t& position) { +JsonValue parseJsonValue(const std::string& text, std::size_t& position, std::size_t depth); + +JsonValue parseJsonObject(const std::string& text, std::size_t& position, std::size_t depth) { + if (depth >= MaxNestingDepth) throw std::runtime_error("param.json is nested too deeply"); expectChar(text, position, '{'); JsonValue value; value.type = JsonType::Object; @@ -148,7 +178,7 @@ JsonValue parseJsonObject(const std::string& text, std::size_t& position) { std::string key = parseJsonString(text, position); skipWhitespace(text, position); expectChar(text, position, ':'); - JsonValue member = parseJsonValue(text, position); + JsonValue member = parseJsonValue(text, position, depth + 1); value.objectValue.emplace_back(std::move(key), std::move(member)); skipWhitespace(text, position); const char next = peekChar(text, position); @@ -159,14 +189,15 @@ JsonValue parseJsonObject(const std::string& text, std::size_t& position) { return value; } -JsonValue parseJsonArray(const std::string& text, std::size_t& position) { +JsonValue parseJsonArray(const std::string& text, std::size_t& position, std::size_t depth) { + if (depth >= MaxNestingDepth) throw std::runtime_error("param.json is nested too deeply"); expectChar(text, position, '['); JsonValue value; value.type = JsonType::Array; skipWhitespace(text, position); if (peekChar(text, position) == ']') { ++position; return value; } while (true) { - JsonValue element = parseJsonValue(text, position); + JsonValue element = parseJsonValue(text, position, depth + 1); value.arrayValue.push_back(std::move(element)); skipWhitespace(text, position); const char next = peekChar(text, position); @@ -177,6 +208,21 @@ JsonValue parseJsonArray(const std::string& text, std::size_t& position) { return value; } +double parseJsonNumberValue(const std::string& number) { + double value = 0.0; +#ifdef __cpp_lib_to_chars + const auto converted = std::from_chars(number.data(), number.data() + number.size(), value); + const bool parsed = converted.ec == std::errc{} && converted.ptr == number.data() + number.size(); +#else + std::istringstream stream(number); + stream.imbue(std::locale::classic()); + stream >> value; + const bool parsed = !stream.fail() && stream.eof(); +#endif + if (!parsed) throw std::runtime_error("number outside supported range in param.json"); + return value; +} + JsonValue parseJsonNumber(const std::string& text, std::size_t& position) { const std::size_t start = position; if (peekChar(text, position) == '-') ++position; @@ -201,15 +247,15 @@ JsonValue parseJsonNumber(const std::string& text, std::size_t& position) { JsonValue value; value.type = JsonType::Number; value.numberText = text.substr(start, position - start); - value.numberValue = std::stod(value.numberText); + value.numberValue = parseJsonNumberValue(value.numberText); return value; } -JsonValue parseJsonValue(const std::string& text, std::size_t& position) { +JsonValue parseJsonValue(const std::string& text, std::size_t& position, std::size_t depth) { skipWhitespace(text, position); const char c = peekChar(text, position); - if (c == '{') return parseJsonObject(text, position); - if (c == '[') return parseJsonArray(text, position); + if (c == '{') return parseJsonObject(text, position, depth); + if (c == '[') return parseJsonArray(text, position, depth); if (c == '"') { JsonValue value; value.type = JsonType::String; value.stringValue = parseJsonString(text, position); return value; } if (c == 't') { expectLiteral(text, position, "true"); JsonValue value; value.type = JsonType::Boolean; value.boolValue = true; return value; } if (c == 'f') { expectLiteral(text, position, "false"); JsonValue value; value.type = JsonType::Boolean; value.boolValue = false; return value; } @@ -220,7 +266,7 @@ JsonValue parseJsonValue(const std::string& text, std::size_t& position) { JsonValue parseJsonDocument(const std::string& text) { std::size_t position = 0; - JsonValue root = parseJsonValue(text, position); + JsonValue root = parseJsonValue(text, position, 0); skipWhitespace(text, position); if (position != text.size()) throw std::runtime_error("unexpected trailing data in param.json"); return root; @@ -276,6 +322,27 @@ 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"); + const auto parsed = std::from_chars(number.data(), number.data() + number.size(), result.attribute3); + if (parsed.ec != std::errc{} || parsed.ptr != number.data() + number.size()) throw std::runtime_error("param.json attribute3 exceeds uint32 range"); + } + if (const JsonValue* kernel = findObjectMember(root, "kernel")) { + if (const JsonValue* flexible = findObjectMember(*kernel, "flexibleMemorySize")) { + const auto& number = flexible->numberText; + if (flexible->type != JsonType::Number || number.find_first_of("-.eE") != std::string::npos) + throw std::runtime_error("param.json kernel.flexibleMemorySize is not an unsigned integer"); + std::uint64_t bytes = 0; + const auto parsed = std::from_chars(number.data(), number.data() + number.size(), bytes); + if (parsed.ec != std::errc{} || parsed.ptr != number.data() + number.size()) + throw std::runtime_error("param.json kernel.flexibleMemorySize exceeds uint64 range"); + if ((bytes & (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")) { if (downloadData->type != JsonType::Number || downloadData->numberValue < 0) throw std::runtime_error("param.json downloadDataSize is not a size"); if (downloadData->numberText.find_first_of("-.eE") == std::string::npos) { diff --git a/core/libs/prx/libkernel/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/include/AprCommandBuffer.hpp b/core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp index ebd001627f..7b12ec2ea2 100644 --- a/core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp +++ b/core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp @@ -5,7 +5,7 @@ #include // Shared between libSceAmpr (which records commands) and libkernel (which executes them). -// The guest treats both the command buffer object and its memory as opaque, so the encoding is ours. +// The object layout is guest ABI; command records use the shared host encoding. namespace Apr { enum class BufferType : std::uint16_t { @@ -14,15 +14,22 @@ enum class BufferType : std::uint16_t { }; struct CommandBufferObject { - std::uint8_t* base; - std::uint32_t size; - std::uint32_t offset; - std::uint32_t numCommands; BufferType type; std::uint16_t recording; + std::uint32_t offset; + std::uint32_t numCommands; + std::uint32_t size; + std::uint8_t* base; }; static_assert(sizeof(CommandBufferObject) == 0x18, "guest reserves 0x18 bytes for sce::Ampr::CommandBuffer"); +static_assert(offsetof(CommandBufferObject, type) == 0x00); +static_assert(offsetof(CommandBufferObject, recording) == 0x02); +static_assert(offsetof(CommandBufferObject, numCommands) == 0x08); +static_assert(offsetof(CommandBufferObject, offset) == 0x04); +static_assert(offsetof(CommandBufferObject, size) == 0x0c); +static_assert(offsetof(CommandBufferObject, base) == 0x10); + inline constexpr std::uint16_t ScatterGatherValid = 1; inline constexpr std::uint16_t MapActive = 2; @@ -56,6 +63,7 @@ enum class Opcode : std::uint32_t { AmmAllocatePaForPrt = 26, AmmRemapIntoPrt = 27, AmmUnmapToPrt = 28, + CompactReadFileGather = 29, }; struct CommandHeader { @@ -72,6 +80,13 @@ struct ReadFileCommand { std::uint64_t offset; }; +struct CompactReadFileGatherCommand { + std::uint32_t opcodeAndOffsetHigh; + std::uint32_t sizeMinusOne; + std::uint32_t offsetLow; +}; +static_assert(sizeof(CompactReadFileGatherCommand) == 12); + struct ResetGatherScatterStateCommand { CommandHeader header; }; diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index a363671b96..42e4274d9e 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -1,7 +1,13 @@ +#include +#include +#include +#include #include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/FilesystemError.hpp" +#include "prx/libc/include/PreciseWait.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/Apr/include/AprCommandBuffer.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" @@ -29,6 +35,8 @@ #include #endif +extern "C" int* APS5_VABI __error_nid_postfix(); + namespace { constexpr int GUEST_ENOENT = 2; @@ -39,15 +47,15 @@ struct AprFile { std::uint64_t size; }; -std::mutex g_filesLock; -std::vector g_files; -std::unordered_map g_idsByPath; +std::mutex& g_filesLock = *new std::mutex(); +std::vector& g_files = *new std::vector(); +std::unordered_map& g_idsByPath = *new std::unordered_map(); // File sizes by directory, listed on first use. Titles resolve thousands of package paths at startup, // and one size query per path took over 20 s; a listing costs one directory read. Names are compared // case-insensitively, like the host file system. -std::mutex g_directoriesLock; -std::unordered_map> g_directories; +std::mutex& g_directoriesLock = *new std::mutex(); +std::unordered_map>& g_directories = *new std::unordered_map>(); std::string _foldCase(std::string text) { for (auto& character : text) { @@ -69,7 +77,7 @@ bool _fileSize(const std::filesystem::path& path, std::uint64_t& bytes) { const HANDLE find = FindFirstFileExW((path.parent_path() / L"*").c_str(), FindExInfoBasic, &entry, FindExSearchNameMatch, nullptr, FIND_FIRST_EX_LARGE_FETCH); if (find != INVALID_HANDLE_VALUE) { do { - if ((entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) continue; + if ((entry.dwFileAttributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT)) != 0) continue; const auto size = (static_cast(entry.nFileSizeHigh) << 32u) | entry.nFileSizeLow; sizes.emplace(_foldCase(std::filesystem::path(entry.cFileName).string()), size); } while (FindNextFileW(find, &entry)); @@ -93,18 +101,22 @@ bool _fileSize(const std::filesystem::path& path, std::uint64_t& bytes) { } // Not in the listing (created since, or an unusual name): ask the file system directly. std::error_code error; - bytes = std::filesystem::file_size(path, error); - return !error; + if (!std::filesystem::is_regular_file(path, error)) return false; + FileStat stat{}; + File::FillFileStat(path, &stat); + bytes = static_cast(stat.st_size); + return true; } int _fail(int guestErrno) { - errno = guestErrno; + *__error_nid_postfix() = guestErrno; return -1; } bool _resolve(const char* guestPath, std::uint32_t* id, std::uint64_t* size) { if (!guestPath) return false; - auto hostPath = ResolvePath_nid_no_patch(guestPath); + std::filesystem::path hostPath; + if (ResolvePathError_nid_no_patch(guestPath, hostPath) != 0) return false; std::uint64_t bytes = 0; if (!_fileSize(hostPath, bytes)) return false; std::lock_guard lock(g_filesLock); @@ -130,6 +142,7 @@ constexpr int SCE_KERNEL_ERROR_ENOENT = static_cast(0x80020002); int _resolveForEach(const char* prefix, const char** paths, uint32_t count, uint32_t* ids, uint64_t* sizes, int* results) { if (!paths || !ids) return _fail(GUEST_EINVAL); + std::uint32_t resolvedCount = 0; for (uint32_t index = 0; index < count; ++index) { const std::string path = prefix ? (paths[index] ? std::string(prefix) + paths[index] : std::string()) : (paths[index] ? std::string(paths[index]) : std::string()); const bool resolved = !path.empty() && _resolve(path.c_str(), &ids[index], sizes ? &sizes[index] : nullptr); @@ -137,9 +150,10 @@ int _resolveForEach(const char* prefix, const char** paths, uint32_t count, uint ids[index] = INVALID_FILE_ID; if (sizes) sizes[index] = 0; } + if (resolved) ++resolvedCount; if (results) results[index] = resolved ? 0 : SCE_KERNEL_ERROR_ENOENT; } - return 0; + return static_cast(resolvedCount); } void _readFile(const Apr::ReadFileCommand& command) { @@ -151,10 +165,25 @@ 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()); - if (read != command.size) throw std::runtime_error("APR: read of " + file.path.string() + " at offset " + std::to_string(command.offset) + " returned " + std::to_string(read) + " of " + std::to_string(command.size) + " bytes"); +#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)); } void _writeAddress(const Apr::WriteAddressCommand& command) { @@ -164,7 +193,7 @@ void _writeAddress(const Apr::WriteAddressCommand& command) { std::array, 256> g_counters{}; -std::mutex g_counterWrites; +std::mutex& g_counterWrites = *new std::mutex(); std::uint32_t _counter(std::uint32_t index) { return g_counters[index % g_counters.size()].load(std::memory_order_acquire); @@ -237,13 +266,20 @@ bool _waitSatisfied(std::uint32_t compare, std::uint64_t value, std::uint64_t re template TCommand _read(const Apr::CommandBufferObject& buffer, std::uint32_t cursor) { + Apr::CommandHeader header; + std::memcpy(&header, buffer.base + cursor, sizeof(header)); + if (header.bytes < sizeof(TCommand)) throw std::runtime_error("APR: truncated command payload (opcode " + std::to_string(static_cast(header.opcode)) + ", has " + std::to_string(header.bytes) + ", needs " + std::to_string(sizeof(TCommand)) + ")"); TCommand command; std::memcpy(&command, buffer.base + cursor, sizeof(command)); return command; } -constexpr std::uint64_t AmmRangeBytes = 32ull << 30; +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; struct AmmPage { std::uint64_t physical; @@ -263,14 +299,20 @@ struct AmmState { }; AmmState& _amm() { - static AmmState state; + static auto& state = *new AmmState(); return state; } std::uintptr_t _ammBase(AmmState& state) { if (state.base == 0) { - void* address = nullptr; - if (DoReserveVirtual(&address, 2 * AmmRangeBytes, 0, 0x200000) != 0) throw std::runtime_error("AMM: cannot reserve the virtual address range"); + std::uintptr_t arenaBase = 0; + std::size_t arenaBytes = 0; + GuestArena::GuestArenaRange_nid_postfix(&arenaBase, &arenaBytes); + 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, AmmReservedBytes, GuestMapFixed | GuestMapNoOverwrite, 0) != 0) throw std::runtime_error("AMM: cannot reserve the virtual address range"); state.base = reinterpret_cast(address); } return state.base; @@ -278,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) { @@ -322,15 +364,38 @@ void _addPage(AmmState& state, std::uint64_t address, const AmmPage& page) { if (page.pooled) ++state.pooledUses[page.physical]; } -void _forgetPages(AmmState& state, std::uint64_t address, std::uint64_t size, bool release) { - for (auto it = state.pages.lower_bound(address); it != state.pages.end() && it->first < address + size;) { - if (it->second.pooled) { - const auto uses = state.pooledUses.find(it->second.physical); - if (--uses->second == 0 && release) { - state.pooledUses.erase(uses); - _returnPoolPages(state, it->second.physical, PS5_PAGE_SIZE); - } +void _releasePage(AmmState& state, const AmmPage& page) { + if (!page.pooled) return; + const auto uses = state.pooledUses.find(page.physical); + if (--uses->second == 0) { + state.pooledUses.erase(uses); + _returnPoolPages(state, page.physical, PS5_PAGE_SIZE); + } +} + +class AmmPageReferences { +public: + AmmPageReferences(AmmState& state, const std::vector& pages) : state(state), pages(pages) { + for (const auto& page : pages) { + if (page.pooled) ++state.pooledUses.find(page.physical)->second; } + } + + AmmPageReferences(const AmmPageReferences&) = delete; + AmmPageReferences& operator=(const AmmPageReferences&) = delete; + + ~AmmPageReferences() { + for (const auto& page : pages) _releasePage(state, page); + } + +private: + AmmState& state; + const std::vector& pages; +}; + +void _forgetPages(AmmState& state, std::uint64_t address, std::uint64_t size) { + for (auto it = state.pages.lower_bound(address); it != state.pages.end() && it->first < address + size;) { + _releasePage(state, it->second); it = state.pages.erase(it); } } @@ -360,9 +425,9 @@ void _mapPages(AmmState& state, std::uint64_t address, const std::vector _execute(const Apr::CommandBufferObject& buffer, std::uint32_t start = 0, ReadCursor read = {}, bool defer = false) { + std::uint32_t cursor = start; while (cursor < buffer.offset) { if (cursor + sizeof(Apr::CommandHeader) > buffer.offset) throw std::runtime_error("APR: truncated command buffer"); Apr::CommandHeader header; std::memcpy(&header, buffer.base + cursor, sizeof(header)); - if (header.bytes < sizeof(header) || cursor + header.bytes > buffer.offset) throw std::runtime_error("APR: malformed command"); + if ((static_cast(header.opcode) & 0xffu) == static_cast(Apr::Opcode::CompactReadFileGather)) { + if (sizeof(Apr::CompactReadFileGatherCommand) > buffer.offset - cursor) throw std::runtime_error("APR: truncated compact gather command"); + Apr::CompactReadFileGatherCommand command; + std::memcpy(&command, buffer.base + cursor, sizeof(command)); + const auto offset = static_cast(command.opcodeAndOffsetHigh >> 8u) << 32u | command.offsetLow; + if (offset >= 0x10000000000ull) throw std::runtime_error("APR: malformed compact gather offset"); + _readResolved(Apr::Opcode::ReadFileGather, Apr::ReadFileCommand{{}, 0, 0, 0, static_cast(command.sizeMinusOne) + 1u, offset}, read); + cursor += sizeof(command); + continue; + } + if (header.bytes < sizeof(header) || header.bytes > buffer.offset - cursor) throw std::runtime_error("APR: malformed command"); switch (header.opcode) { case Apr::Opcode::Nop: case Apr::Opcode::PushMarker: @@ -566,8 +649,7 @@ void _execute(const Apr::CommandBufferObject& buffer) { case Apr::Opcode::MapEnd: break; case Apr::Opcode::WriteAddress: { - Apr::WriteAddressCommand command; - std::memcpy(&command, buffer.base + cursor, sizeof(command)); + const auto command = _read(buffer, cursor); _writeAddress(command); break; } @@ -585,7 +667,8 @@ void _execute(const Apr::CommandBufferObject& buffer) { return std::atomic_ref(*reinterpret_cast(command.address)).load(std::memory_order_acquire); }; const std::uint64_t reference = (command.reference & command.mask) << unused; - while (!_waitSatisfied(command.compare, (current() & command.mask) << unused, reference)) std::this_thread::sleep_for(std::chrono::microseconds(50)); + if (defer && !_waitSatisfied(command.compare, (current() & command.mask) << unused, reference)) return Deferred{cursor, read}; + while (!_waitSatisfied(command.compare, (current() & command.mask) << unused, reference)) PreciseSleepUs(50); break; } case Apr::Opcode::WriteKernelEventQueue: { @@ -611,6 +694,7 @@ void _execute(const Apr::CommandBufferObject& buffer) { } cursor += header.bytes; } + return std::nullopt; } } @@ -636,19 +720,110 @@ 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 { + std::mutex submit; + std::mutex mutex; + std::condition_variable ready; + std::condition_variable done; + struct Work { + std::vector commands; + Deferred resume; + std::uint32_t id = 0; + }; + std::deque pending; + bool running = false; + std::uint32_t submitted = 0; + std::uint32_t completed = 0; +}; + +AprQueue* _startQueue() { + auto* created = new AprQueue(); + std::thread([created] { + for (;;) { + AprQueue::Work work; + { + std::unique_lock lock(created->mutex); + created->ready.wait(lock, [&] { return !created->pending.empty(); }); + work = std::move(created->pending.front()); + created->pending.pop_front(); + created->running = true; + } + try { + const auto bytes = static_cast(work.commands.size()); + _execute(Apr::CommandBufferObject{Apr::BufferType::Generic, 0, bytes, 0, bytes, work.commands.data()}, work.resume.cursor, work.resume.read); + } catch (const std::exception& error) { + std::fprintf(stderr, "APR: %s\n", error.what()); + std::fflush(stderr); + std::abort(); + } + { + std::lock_guard lock(created->mutex); + created->running = false; + created->completed = std::max(created->completed, work.id); + } + created->done.notify_all(); + } + }).detach(); + return created; +} + +AprQueue& _queue(bool amm) { + static AprQueue* const queues[2] = {_startQueue(), _startQueue()}; + return *queues[amm ? 1 : 0]; +} + +std::uint32_t _submit(const std::uint8_t* base, std::uint32_t bytes, bool amm) { + auto& queue = _queue(amm); + std::lock_guard submitLock(queue.submit); + std::uint32_t id; + bool queued; + { + std::lock_guard lock(queue.mutex); + id = ++queue.submitted; + queued = queue.running || !queue.pending.empty(); + } + const auto enqueue = [&](Deferred resume) { + { + std::lock_guard lock(queue.mutex); + queue.pending.push_back({std::vector(base, base + bytes), resume, id}); + } + queue.ready.notify_one(); + }; + if (queued) { + enqueue({}); + return id; + } + if (const auto deferred = _execute(Apr::CommandBufferObject{Apr::BufferType::Generic, 0, bytes, 0, bytes, const_cast(base)}, 0, {}, true)) { + enqueue(*deferred); + return id; + } + { + std::lock_guard lock(queue.mutex); + queue.completed = std::max(queue.completed, id); + } + queue.done.notify_all(); + return id; +} + +void _waitSubmitted(std::uint32_t id, bool amm) { + auto& queue = _queue(amm); + std::unique_lock lock(queue.mutex); + queue.done.wait(lock, [&] { return queue.completed >= id || id > queue.submitted; }); } std::uint32_t AmmSubmit_nid_no_patch(void* base, std::uint32_t bytes) { - _execute(Apr::CommandBufferObject{static_cast(base), bytes, bytes, 0, Apr::BufferType::Generic, 0}); - auto& last = _amm().lastSubmit; - std::uint32_t id = last.fetch_add(1) + 1; - while (id == 0) id = last.fetch_add(1) + 1; + const auto id = _submit(static_cast(base), bytes, true); + _amm().lastSubmit.store(id); return id; } bool AmmSubmitted_nid_no_patch(std::uint32_t id) { - return id != 0 && id <= _amm().lastSubmit.load(); + if (id == 0 || id > _amm().lastSubmit.load()) return false; + _waitSubmitted(id, true); + return true; } @@ -656,6 +831,7 @@ int APS5_VABI sceKernelAprResolveFilepathsToIds(const char** paths, uint32_t cou if (!paths || !ids) return _fail(GUEST_EINVAL); for (uint32_t index = 0; index < count; ++index) { if (!_resolve(paths[index], &ids[index], nullptr)) { + ids[index] = INVALID_FILE_ID; if (error_index) *error_index = index; return _fail(GUEST_ENOENT); } @@ -667,6 +843,8 @@ int APS5_VABI sceKernelAprResolveFilepathsToIdsAndFileSizes(const char** paths, if (!paths || !ids || !sizes) return _fail(GUEST_EINVAL); for (uint32_t index = 0; index < count; ++index) { if (!_resolve(paths[index], &ids[index], &sizes[index])) { + ids[index] = INVALID_FILE_ID; + if (sizes) sizes[index] = 0; if (error_index) *error_index = index; return _fail(GUEST_ENOENT); } @@ -679,6 +857,8 @@ int APS5_VABI sceKernelAprResolveFilepathsWithPrefixToIdsAndFileSizes(const char for (uint32_t index = 0; index < count; ++index) { const std::string path = paths[index] ? std::string(prefix) + paths[index] : std::string(); if (path.empty() || !_resolve(path.c_str(), &ids[index], sizes ? &sizes[index] : nullptr)) { + ids[index] = INVALID_FILE_ID; + if (sizes) sizes[index] = 0; if (error_index) *error_index = index; return _fail(GUEST_ENOENT); } @@ -704,17 +884,16 @@ int APS5_VABI sceKernelAprGetFileStat(uint32_t id, FileStat* stat) { int APS5_VABI sceKernelAprSubmitCommandBuffer(const Apr::CommandBufferObject* buffer, uint32_t priority) { (void)priority; - if (!buffer || buffer->type != Apr::BufferType::Apr) return _fail(GUEST_EINVAL); - _execute(*buffer); + if (!buffer) return _fail(GUEST_EINVAL); + _submit(buffer->base, buffer->offset, false); return 0; } int APS5_VABI sceKernelAprSubmitCommandBufferAndGetId(const Apr::CommandBufferObject* buffer, uint32_t priority, uint32_t* id) { if (!id) return _fail(GUEST_EINVAL); - const int result = sceKernelAprSubmitCommandBuffer(buffer, priority); - if (result != 0) return result; - static std::atomic nextId{1}; - *id = nextId.fetch_add(1); + (void)priority; + if (!buffer) return _fail(GUEST_EINVAL); + *id = _submit(buffer->base, buffer->offset, false); return 0; } @@ -727,7 +906,7 @@ int APS5_VABI sceKernelAprSubmitCommandBufferAndGetResult(const Apr::CommandBuff } int APS5_VABI sceKernelAprWaitCommandBuffer(uint32_t id) { - (void)id; + _waitSubmitted(id, false); return 0; } @@ -750,4 +929,29 @@ int APS5_VABI sceKernelAprResolveFilepathsWithPrefixToIdsAndFileSizesForEach(con return _resolveForEach(prefix, paths, count, ids, sizes, results); } +bool _validWriteRange(std::uint64_t address, std::uint64_t size) { + constexpr std::uint64_t pageMask = 0x3FFFu; + return address != 0u && size != 0u && ((address | size) & pageMask) == 0u && address + size > address; +} + +bool _validWriteProtection(std::uint64_t protection) { + return (protection & ~0x3F3ULL) == 0u; +} + +int APS5_VABI sceKernelWriteModifyMtypeProtectCommand(void* dst, std::uint64_t address, std::uint64_t size, std::uint64_t type, std::uint64_t protection, std::uint64_t mask, std::uint64_t* outSize) { + if (!_validWriteRange(address, size) || !_validWriteProtection(protection) || !_validWriteProtection(mask)) return SCE_KERNEL_ERROR_EINVAL; + const Apr::AmmProtectCommand command{{Apr::Opcode::AmmModifyMtypeProtect, sizeof(command)}, address, size, static_cast(type), static_cast(protection), static_cast(mask), 0}; + if (dst) std::memcpy(dst, &command, sizeof(command)); + if (outSize) *outSize = sizeof(command); + return 0; +} + +int APS5_VABI sceKernelWriteRemapIntoPrtCommand(std::uint32_t* dst, std::uint64_t address, std::uint64_t source, std::uint64_t size, int protection, int opcode, std::uint64_t* outSize) { + if (!_validWriteRange(address, size) || !_validWriteRange(source, size) || !_validWriteProtection(static_cast(protection))) return SCE_KERNEL_ERROR_EINVAL; + const Apr::AmmRemapCommand command{{Apr::Opcode::AmmRemapIntoPrt, sizeof(command)}, address, source, size, protection, opcode ? opcode : 1011}; + if (dst) std::memcpy(dst, &command, sizeof(command)); + if (outSize) *outSize = sizeof(command); + return 0; +} + } diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 8e5c79987d..d025918ba0 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -2,17 +2,29 @@ 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 ${CMAKE_CURRENT_SOURCE_DIR}/File/src/DirectoryDescriptor.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/File/src/FileLock.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/NativeStat.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/Terminal.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/Open.cpp + ${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 ${CMAKE_CURRENT_SOURCE_DIR}/SyncOnAddress/src/SyncOnAddress.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DirectMemory/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DirectMemory/DirectMemory.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/DirectMemory/Jit.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DirectMemory/PosixMemory.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DirectMemory/MemoryPool.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DirectMemory/MemoryPoolExport.cpp @@ -23,6 +35,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/Tsd.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/Rwlock.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/Sem.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Atfork.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Attr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Barrier.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Cond.cpp @@ -32,9 +45,12 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Once.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Tsd.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Sem.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Spinlock.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/Socket.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/Udp.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/SocketOptions.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/AddressConversion.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/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 @@ -48,14 +64,22 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Rtld.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Process.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Sandbox.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/System/src/HostCpu.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/SystemConfiguration.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Login.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/System/src/SoftwareVersion.cpp + ${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}) @@ -63,22 +87,64 @@ 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) + 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) + 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 "invalid") + 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}}") else() file(WRITE ${downloadDataRun}/app0/sce_sys/param.json @@ -101,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) @@ -117,10 +192,26 @@ if(BUILD_TESTING) configure_windows_unwind(guest_process_time_tests) add_test(NAME guest_process_time COMMAND guest_process_time_tests) add_test(NAME guest_process_time_counter COMMAND guest_process_time_tests counter) + add_test(NAME guest_process_time_scaled COMMAND guest_process_time_tests) + add_test(NAME guest_process_time_counter_scaled COMMAND guest_process_time_tests counter) + set_tests_properties(guest_process_time_scaled guest_process_time_counter_scaled PROPERTIES ENVIRONMENT "APS5_TIME_SCALE=0.5") + if(WIN32) + set_tests_properties(guest_process_time_scaled guest_process_time_counter_scaled PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() if(WIN32) set_tests_properties(guest_process_time guest_process_time_counter PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_process_arguments_tests ${LIBS_INCLUDE_DIR}/tests/GuestProcessArguments.cpp) + target_include_directories(guest_process_arguments_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_process_arguments_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_process_arguments_tests) + add_test(NAME guest_process_arguments COMMAND guest_process_arguments_tests plain "two words" "ü日") + if(WIN32) + set_tests_properties(guest_process_arguments PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_library(guest_module_fixture SHARED ${LIBS_INCLUDE_DIR}/tests/GuestModuleFixture.cpp) target_include_directories(guest_module_fixture PRIVATE ${LIBS_INCLUDE_DIR}) add_custom_command(TARGET guest_module_fixture POST_BUILD @@ -142,15 +233,71 @@ if(BUILD_TESTING) target_link_libraries(guest_dynamic_loader_tests PRIVATE libkernel libc) configure_windows_unwind(guest_dynamic_loader_tests) add_dependencies(guest_dynamic_loader_tests guest_module_fixture) - if(NOT WIN32) - set_target_properties(guest_dynamic_loader_tests PROPERTIES ENABLE_EXPORTS ON) + set_target_properties(guest_dynamic_loader_tests PROPERTIES ENABLE_EXPORTS ON) + if(WIN32) + file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/GuestDynamicLoader.def" CONTENT "EXPORTS\nGuestDefaultScopeOnly\n") + target_sources(guest_dynamic_loader_tests PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/GuestDynamicLoader.def") 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:$") + enable_language(ASM) + add_library(guest_deferred_module_fixture SHARED + ${LIBS_INCLUDE_DIR}/tests/GuestDeferredModuleFixture.cpp + ${LIBS_INCLUDE_DIR}/tests/GuestDeferredModuleFixture.S) + target_include_directories(guest_deferred_module_fixture PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_options(guest_deferred_module_fixture PRIVATE -Wl,--image-base,0x38000000 -Wl,--disable-dynamicbase) + configure_windows_unwind(guest_deferred_module_fixture) + set_target_properties(guest_deferred_module_fixture PROPERTIES PREFIX "" SUFFIX ".guest.prx") + add_test_executable(guest_deferred_module_tests ${LIBS_INCLUDE_DIR}/tests/GuestDeferredModule.cpp) + target_include_directories(guest_deferred_module_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_deferred_module_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_deferred_module_tests) + add_dependencies(guest_deferred_module_tests guest_deferred_module_fixture) + foreach(mode IN ITEMS reload pinned parallel reentrant) + add_test(NAME guest_deferred_module_${mode} COMMAND guest_deferred_module_tests $ ${mode}) + set_tests_properties(guest_deferred_module_${mode} PROPERTIES TIMEOUT 20 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endforeach() + endif() + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_test_executable(guest_dynamic_loader_fixed_base_tests ${LIBS_INCLUDE_DIR}/tests/GuestDynamicLoader.cpp) + target_include_directories(guest_dynamic_loader_fixed_base_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_dynamic_loader_fixed_base_tests PRIVATE libkernel libc) + add_dependencies(guest_dynamic_loader_fixed_base_tests guest_module_fixture) + set_target_properties(guest_dynamic_loader_fixed_base_tests PROPERTIES ENABLE_EXPORTS ON POSITION_INDEPENDENT_CODE OFF) + target_compile_options(guest_dynamic_loader_fixed_base_tests PRIVATE -fno-pie) + target_link_options(guest_dynamic_loader_fixed_base_tests PRIVATE -no-pie -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 "$" 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}) @@ -182,6 +329,57 @@ if(BUILD_TESTING) set_tests_properties(guest_filesystem PROPERTIES ENVIRONMENT_MODIFICATION "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}) + target_link_libraries(guest_truncate_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_truncate_tests) + add_test(NAME guest_truncate COMMAND guest_truncate_tests) + set_tests_properties(guest_truncate PROPERTIES TIMEOUT 20) + if(WIN32) + 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_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) @@ -201,6 +399,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) @@ -217,9 +434,21 @@ if(BUILD_TESTING) configure_windows_unwind(guest_aio_tests) add_test(NAME guest_aio COMMAND guest_aio_tests) if(WIN32) + add_test(NAME guest_aio_bad_descriptors COMMAND guest_aio_tests bad-descriptors) + set_tests_properties(guest_aio_bad_descriptors PROPERTIES TIMEOUT 20 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") set_tests_properties(guest_aio PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_positioned_race_tests ${LIBS_INCLUDE_DIR}/tests/GuestPositionedRace.cpp) + target_include_directories(guest_positioned_race_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_positioned_race_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_positioned_race_tests) + add_test(NAME guest_positioned_race COMMAND guest_positioned_race_tests) + if(WIN32) + set_tests_properties(guest_positioned_race PROPERTIES TIMEOUT 60 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_once_tests ${LIBS_INCLUDE_DIR}/tests/GuestOnce.cpp) target_include_directories(guest_once_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_once_tests PRIVATE libkernel libc) @@ -246,20 +475,67 @@ 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) + configure_windows_unwind(guest_app_info_tests) + add_test(NAME guest_app_info COMMAND guest_app_info_tests) + set_tests_properties(guest_app_info PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_app_info PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_kernel_stack_protect_tests ${LIBS_INCLUDE_DIR}/tests/GuestKernelStackProtect.cpp) + target_include_directories(guest_kernel_stack_protect_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_kernel_stack_protect_tests PRIVATE libkernel libc) + 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:$") + endif() 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:$") endif() + add_test_executable(guest_pthread_spin_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadSpin.cpp) + target_include_directories(guest_pthread_spin_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_spin_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_spin_tests) + add_test(NAME guest_pthread_spin COMMAND guest_pthread_spin_tests) + set_tests_properties(guest_pthread_spin PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_spin PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_pthread_getname_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadGetname.cpp) target_include_directories(guest_pthread_getname_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_pthread_getname_tests PRIVATE libkernel libc) @@ -280,6 +556,67 @@ if(BUILD_TESTING) set_tests_properties(guest_pthread_attr PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_schedparam_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadSchedparam.cpp) + target_include_directories(guest_pthread_schedparam_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_schedparam_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_schedparam_tests) + add_test(NAME guest_pthread_schedparam COMMAND guest_pthread_schedparam_tests) + set_tests_properties(guest_pthread_schedparam PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_schedparam PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_pthread_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) + configure_windows_unwind(guest_pthread_mutexattr_tests) + add_test(NAME guest_pthread_mutexattr COMMAND guest_pthread_mutexattr_tests) + set_tests_properties(guest_pthread_mutexattr PROPERTIES TIMEOUT 20) + if(WIN32) + 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) + configure_windows_unwind(guest_pthread_mutex_errors_tests) + add_test(NAME guest_pthread_mutex_errors COMMAND guest_pthread_mutex_errors_tests) + set_tests_properties(guest_pthread_mutex_errors PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_mutex_errors PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_pthread_start_tests ${LIBS_INCLUDE_DIR}/tests/PthreadStart.cpp) target_include_directories(guest_pthread_start_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_pthread_start_tests PRIVATE libkernel libc) @@ -287,7 +624,38 @@ if(BUILD_TESTING) add_test(NAME guest_pthread_start COMMAND guest_pthread_start_tests) set_tests_properties(guest_pthread_start PROPERTIES TIMEOUT 20) if(WIN32) - set_tests_properties(guest_pthread_start PROPERTIES TIMEOUT 60 ENVIRONMENT_MODIFICATION + set_tests_properties(guest_pthread_start PROPERTIES TIMEOUT 60 RUN_SERIAL TRUE ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_pthread_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) + configure_windows_unwind(guest_pthread_attr_errors_tests) + add_test(NAME guest_pthread_attr_errors COMMAND guest_pthread_attr_errors_tests) + set_tests_properties(guest_pthread_attr_errors PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_attr_errors PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_pthread_stack_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadStack.cpp) + target_include_directories(guest_pthread_stack_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_stack_tests PRIVATE libkernel libc) + 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:$") endif() add_test_executable(guest_cond_timedwait_tests ${LIBS_INCLUDE_DIR}/tests/GuestCondTimedwait.cpp) @@ -300,6 +668,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) @@ -310,6 +688,26 @@ if(BUILD_TESTING) set_tests_properties(guest_cond_recursive_mutex PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_cond_unowned_mutex_tests ${LIBS_INCLUDE_DIR}/tests/GuestCondUnownedMutex.cpp) + target_include_directories(guest_cond_unowned_mutex_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_cond_unowned_mutex_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_cond_unowned_mutex_tests) + add_test(NAME guest_cond_unowned_mutex COMMAND guest_cond_unowned_mutex_tests) + set_tests_properties(guest_cond_unowned_mutex PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_cond_unowned_mutex PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_getitimer_tests ${LIBS_INCLUDE_DIR}/tests/GuestGetitimer.cpp) + target_include_directories(guest_getitimer_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_getitimer_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_getitimer_tests) + add_test(NAME guest_getitimer COMMAND guest_getitimer_tests) + set_tests_properties(guest_getitimer PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_getitimer PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_cpu_time_clocks_tests ${LIBS_INCLUDE_DIR}/tests/GuestCpuTimeClocks.cpp) target_include_directories(guest_cpu_time_clocks_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_cpu_time_clocks_tests PRIVATE libkernel libc) @@ -340,6 +738,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) @@ -349,6 +757,21 @@ 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_pthread_sem_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadSem.cpp) target_include_directories(guest_pthread_sem_tests PRIVATE ${LIBS_INCLUDE_DIR}) @@ -360,16 +783,72 @@ if(BUILD_TESTING) set_tests_properties(guest_pthread_sem PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_cancel_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadCancel.cpp) + target_include_directories(guest_pthread_cancel_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_cancel_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_cancel_tests) + add_test(NAME guest_pthread_cancel COMMAND guest_pthread_cancel_tests) + set_tests_properties(guest_pthread_cancel PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_cancel PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_pthread_cancel_recovery_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadCancelRecovery.cpp) + target_include_directories(guest_pthread_cancel_recovery_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_cancel_recovery_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_cancel_recovery_tests) + add_test(NAME guest_pthread_cancel_recovery COMMAND guest_pthread_cancel_recovery_tests) + add_test(NAME guest_pthread_cancel_recovery_coarse COMMAND guest_pthread_cancel_recovery_tests) + set_tests_properties(guest_pthread_cancel_recovery guest_pthread_cancel_recovery_coarse PROPERTIES TIMEOUT 20) + set_tests_properties(guest_pthread_cancel_recovery_coarse PROPERTIES ENVIRONMENT "APS5_COARSE_TIMED_WAITS=1") + if(WIN32) + set_tests_properties(guest_pthread_cancel_recovery guest_pthread_cancel_recovery_coarse PROPERTIES + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() add_test_executable(guest_semaphore_tests ${LIBS_INCLUDE_DIR}/tests/GuestSemaphore.cpp) target_include_directories(guest_semaphore_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_semaphore_tests PRIVATE libkernel libc) 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) @@ -380,6 +859,36 @@ 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) + configure_windows_unwind(guest_pthread_join_self_tests) + add_test(NAME guest_pthread_join_self COMMAND guest_pthread_join_self_tests) + set_tests_properties(guest_pthread_join_self PROPERTIES TIMEOUT 20) + if(WIN32) + 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) @@ -390,6 +899,29 @@ if(BUILD_TESTING) set_tests_properties(guest_pthread_key PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_destructor_lifetime_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadDestructorLifetime.cpp) + target_include_directories(guest_pthread_destructor_lifetime_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_destructor_lifetime_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_destructor_lifetime_tests) + add_test(NAME guest_pthread_destructor_lifetime COMMAND guest_pthread_destructor_lifetime_tests) + set_tests_properties(guest_pthread_destructor_lifetime PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_destructor_lifetime PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + 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) + add_test(NAME guest_user_context_exit COMMAND guest_user_context_tests exit-on-return) + set_tests_properties(guest_user_context guest_user_context_exit PROPERTIES TIMEOUT 20) + if(WIN32) + target_compile_definitions(guest_user_context_tests PRIVATE _WIN32_WINNT=0x0602) + set_tests_properties(guest_user_context guest_user_context_exit PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_signal_tests ${LIBS_INCLUDE_DIR}/tests/GuestSignals.cpp) target_include_directories(guest_signal_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_signal_tests PRIVATE libkernel libc) @@ -399,6 +931,55 @@ if(BUILD_TESTING) set_tests_properties(guest_signals PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_sigtimedwait_tests ${LIBS_INCLUDE_DIR}/tests/GuestSigtimedwait.cpp) + target_include_directories(guest_sigtimedwait_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_sigtimedwait_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_sigtimedwait_tests) + add_test(NAME guest_sigtimedwait COMMAND guest_sigtimedwait_tests) + if(WIN32) + set_tests_properties(guest_sigtimedwait PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_itimer_tests ${LIBS_INCLUDE_DIR}/tests/GuestItimer.cpp) + target_include_directories(guest_itimer_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_itimer_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_itimer_tests) + add_test(NAME guest_itimer COMMAND guest_itimer_tests) + 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 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) + target_include_directories(guest_parent_process_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_parent_process_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_parent_process_tests) + add_test(NAME guest_parent_process COMMAND guest_parent_process_tests) + if(WIN32) + set_tests_properties(guest_parent_process PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_pthread_atfork_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadAtfork.cpp) + target_include_directories(guest_pthread_atfork_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_atfork_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_atfork_tests) + add_test(NAME guest_pthread_atfork COMMAND guest_pthread_atfork_tests) + if(WIN32) + set_tests_properties(guest_pthread_atfork PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_kill_tests ${LIBS_INCLUDE_DIR}/tests/GuestKill.cpp) + target_include_directories(guest_kill_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_kill_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_kill_tests) + add_test(NAME guest_kill COMMAND guest_kill_tests) + set_tests_properties(guest_kill PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_kill PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_signal_set_tests ${LIBS_INCLUDE_DIR}/tests/GuestSignalSets.cpp) target_include_directories(guest_signal_set_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_signal_set_tests PRIVATE libkernel libc) @@ -427,13 +1008,15 @@ if(BUILD_TESTING) set_tests_properties(guest_exception_handler PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_raise_exception_tests ${LIBS_INCLUDE_DIR}/tests/GuestRaiseException.cpp) + target_include_directories(guest_raise_exception_tests PRIVATE ${LIBS_INCLUDE_DIR}) + 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) + add_asan_test_options(guest_raise_exception detect_stack_use_after_return=0) if(WIN32) - add_test_executable(guest_raise_exception_tests ${LIBS_INCLUDE_DIR}/tests/GuestRaiseException.cpp) - target_include_directories(guest_raise_exception_tests PRIVATE ${LIBS_INCLUDE_DIR}) - 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 ENVIRONMENT_MODIFICATION + set_tests_properties(guest_raise_exception PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") add_test_executable(guest_raise_nested_sleep_tests ${LIBS_INCLUDE_DIR}/tests/GuestRaiseNestedSleep.cpp) target_include_directories(guest_raise_nested_sleep_tests PRIVATE ${LIBS_INCLUDE_DIR}) @@ -485,6 +1068,16 @@ if(BUILD_TESTING) set_tests_properties(guest_sync_on_address PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_umtx_op_tests ${LIBS_INCLUDE_DIR}/tests/GuestUmtxOp.cpp) + target_include_directories(guest_umtx_op_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_umtx_op_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_umtx_op_tests) + add_test(NAME guest_umtx_op COMMAND guest_umtx_op_tests) + set_tests_properties(guest_umtx_op PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_umtx_op PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_available_cpumask_tests ${LIBS_INCLUDE_DIR}/tests/GuestAvailableCpumask.cpp) target_include_directories(guest_available_cpumask_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_available_cpumask_tests PRIVATE libkernel libc) @@ -515,6 +1108,83 @@ if(BUILD_TESTING) set_tests_properties(guest_getrusage PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_thermal_sensors_tests ${LIBS_INCLUDE_DIR}/tests/GuestThermalSensors.cpp) + target_include_directories(guest_thermal_sensors_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_thermal_sensors_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_thermal_sensors_tests) + add_test(NAME guest_thermal_sensors COMMAND guest_thermal_sensors_tests) + set_tests_properties(guest_thermal_sensors PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_thermal_sensors PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_getlogin_tests ${LIBS_INCLUDE_DIR}/tests/GuestGetlogin.cpp) + target_include_directories(guest_getlogin_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_getlogin_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_getlogin_tests) + add_test(NAME guest_getlogin COMMAND guest_getlogin_tests) + set_tests_properties(guest_getlogin PROPERTIES TIMEOUT 20) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_test_executable(guest_getlogin_no_session_tests + ${LIBS_INCLUDE_DIR}/tests/GuestGetloginNoSession.cpp + System/src/Login.cpp) + target_include_directories(guest_getlogin_no_session_tests PRIVATE ${LIBS_INCLUDE_DIR}) + add_test(NAME guest_getlogin_no_session COMMAND guest_getlogin_no_session_tests) + set_tests_properties(guest_getlogin_no_session PROPERTIES TIMEOUT 20) + endif() + if(WIN32) + target_link_libraries(guest_getlogin_tests PRIVATE advapi32) + set_tests_properties(guest_getlogin PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_sched_priority_tests ${LIBS_INCLUDE_DIR}/tests/GuestSchedPriority.cpp) + target_include_directories(guest_sched_priority_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_sched_priority_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_sched_priority_tests) + add_test(NAME guest_sched_priority COMMAND guest_sched_priority_tests) + set_tests_properties(guest_sched_priority PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_sched_priority PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_operation_mode_tests ${LIBS_INCLUDE_DIR}/tests/GuestOperationMode.cpp) + target_include_directories(guest_operation_mode_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_operation_mode_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_operation_mode_tests) + add_test(NAME guest_operation_mode COMMAND guest_operation_mode_tests) + if(WIN32) + set_tests_properties(guest_operation_mode PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_waitpid_tests ${LIBS_INCLUDE_DIR}/tests/GuestWaitpid.cpp) + target_include_directories(guest_waitpid_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_waitpid_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_waitpid_tests) + add_test(NAME guest_waitpid COMMAND guest_waitpid_tests) + set_tests_properties(guest_waitpid PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_waitpid PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_execvp_tests ${LIBS_INCLUDE_DIR}/tests/GuestExecvp.cpp) + target_include_directories(guest_execvp_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_execvp_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_execvp_tests) + add_test(NAME guest_execvp COMMAND guest_execvp_tests) + if(WIN32) + set_tests_properties(guest_execvp PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_sendfile_tests ${LIBS_INCLUDE_DIR}/tests/GuestSendfile.cpp) + target_include_directories(guest_sendfile_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_sendfile_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_sendfile_tests) + add_test(NAME guest_sendfile COMMAND guest_sendfile_tests) + set_tests_properties(guest_sendfile PROPERTIES TIMEOUT 60) + if(WIN32) + set_tests_properties(guest_sendfile PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_kernel_error_tests ${LIBS_INCLUDE_DIR}/tests/GuestKernelErrors.cpp) target_include_directories(guest_kernel_error_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_kernel_error_tests PRIVATE libkernel libc) @@ -535,6 +1205,26 @@ 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) + configure_windows_unwind(guest_setrlimit_tests) + add_test(NAME guest_setrlimit COMMAND guest_setrlimit_tests) + set_tests_properties(guest_setrlimit PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_setrlimit PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_equeue_event_tests ${LIBS_INCLUDE_DIR}/tests/GuestEqueueEvents.cpp) target_include_directories(guest_equeue_event_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_equeue_event_tests PRIVATE libkernel libc) @@ -545,6 +1235,65 @@ if(BUILD_TESTING) set_tests_properties(guest_equeue_events PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_shm_open_tests ${LIBS_INCLUDE_DIR}/tests/GuestShmOpen.cpp) + target_include_directories(guest_shm_open_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_shm_open_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_shm_open_tests) + add_test(NAME guest_shm_open COMMAND guest_shm_open_tests) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_test_executable(guest_named_shm_open_tests ${LIBS_INCLUDE_DIR}/tests/GuestNamedShmOpen.cpp) + target_include_directories(guest_named_shm_open_tests PRIVATE ${LIBS_INCLUDE_DIR} ${LIBS_INCLUDE_DIR}/nid/include) + target_link_libraries(guest_named_shm_open_tests PRIVATE libkernel libc rt) + add_test(NAME guest_named_shm_open COMMAND guest_named_shm_open_tests) + set_tests_properties(guest_named_shm_open PROPERTIES TIMEOUT 20) + add_test_executable(guest_named_shm_lifecycle_tests ${LIBS_INCLUDE_DIR}/tests/GuestNamedShmLifecycle.cpp) + target_include_directories(guest_named_shm_lifecycle_tests PRIVATE ${LIBS_INCLUDE_DIR} ${LIBS_INCLUDE_DIR}/nid/include) + target_link_libraries(guest_named_shm_lifecycle_tests PRIVATE libkernel libc rt ${CMAKE_DL_LIBS}) + add_test(NAME guest_named_shm_lifecycle COMMAND guest_named_shm_lifecycle_tests) + set_tests_properties(guest_named_shm_lifecycle PROPERTIES TIMEOUT 20) + add_test_executable(guest_named_shm_descriptor_reuse_tests ${LIBS_INCLUDE_DIR}/tests/GuestNamedShmDescriptorReuse.cpp) + target_include_directories(guest_named_shm_descriptor_reuse_tests PRIVATE ${LIBS_INCLUDE_DIR} ${LIBS_INCLUDE_DIR}/nid/include) + target_link_libraries(guest_named_shm_descriptor_reuse_tests PRIVATE libkernel libc rt ${CMAKE_DL_LIBS}) + add_test(NAME guest_named_shm_descriptor_reuse COMMAND guest_named_shm_descriptor_reuse_tests) + set_tests_properties(guest_named_shm_descriptor_reuse PROPERTIES TIMEOUT 20) + add_test_executable(guest_named_shm_permissions_tests ${LIBS_INCLUDE_DIR}/tests/GuestNamedShmPermissions.cpp) + target_include_directories(guest_named_shm_permissions_tests PRIVATE ${LIBS_INCLUDE_DIR} ${LIBS_INCLUDE_DIR}/nid/include) + target_link_libraries(guest_named_shm_permissions_tests PRIVATE libkernel_sys libkernel libc rt) + add_test(NAME guest_named_shm_permissions COMMAND guest_named_shm_permissions_tests) + set_tests_properties(guest_named_shm_permissions PROPERTIES TIMEOUT 20) + add_test_executable(guest_named_shm_io_descriptor_reuse_tests ${LIBS_INCLUDE_DIR}/tests/GuestNamedShmIoDescriptorReuse.cpp) + target_include_directories(guest_named_shm_io_descriptor_reuse_tests PRIVATE ${LIBS_INCLUDE_DIR} ${LIBS_INCLUDE_DIR}/nid/include) + target_link_libraries(guest_named_shm_io_descriptor_reuse_tests PRIVATE libkernel libc rt) + add_test(NAME guest_named_shm_io_descriptor_reuse COMMAND guest_named_shm_io_descriptor_reuse_tests) + set_tests_properties(guest_named_shm_io_descriptor_reuse PROPERTIES TIMEOUT 20) + add_test_executable(guest_named_shm_concurrency_tests ${LIBS_INCLUDE_DIR}/tests/GuestNamedShmConcurrency.cpp) + target_include_directories(guest_named_shm_concurrency_tests PRIVATE ${LIBS_INCLUDE_DIR} ${LIBS_INCLUDE_DIR}/nid/include) + target_link_libraries(guest_named_shm_concurrency_tests PRIVATE libkernel libc rt ${CMAKE_DL_LIBS}) + add_test(NAME guest_named_shm_concurrency COMMAND guest_named_shm_concurrency_tests) + set_tests_properties(guest_named_shm_concurrency PROPERTIES TIMEOUT 20) + endif() + if(WIN32) + set_tests_properties(guest_shm_open PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_jit_shared_memory_tests ${LIBS_INCLUDE_DIR}/tests/GuestJitSharedMemory.cpp) + target_include_directories(guest_jit_shared_memory_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_jit_shared_memory_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_jit_shared_memory_tests) + add_test(NAME guest_jit_shared_memory COMMAND guest_jit_shared_memory_tests) + if(WIN32) + set_tests_properties(guest_jit_shared_memory PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(host_cpu_tests ${LIBS_INCLUDE_DIR}/tests/HostCpu.cpp) + target_include_directories(host_cpu_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(host_cpu_tests PRIVATE libkernel libc) + configure_windows_unwind(host_cpu_tests) + add_test(NAME host_cpu COMMAND host_cpu_tests) + if(WIN32) + set_tests_properties(host_cpu PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_system_configuration_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemConfiguration.cpp) target_include_directories(guest_system_configuration_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_system_configuration_tests PRIVATE libkernel libc) @@ -554,6 +1303,24 @@ if(BUILD_TESTING) set_tests_properties(guest_system_configuration PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_software_version_tests ${LIBS_INCLUDE_DIR}/tests/GuestSoftwareVersion.cpp) + target_include_directories(guest_software_version_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_software_version_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_software_version_tests) + add_test(NAME guest_software_version COMMAND guest_software_version_tests) + if(WIN32) + set_tests_properties(guest_software_version PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_thread_sanitizer_tests ${LIBS_INCLUDE_DIR}/tests/GuestThreadSanitizer.cpp) + target_include_directories(guest_thread_sanitizer_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_thread_sanitizer_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_thread_sanitizer_tests) + add_test(NAME guest_thread_sanitizer COMMAND guest_thread_sanitizer_tests) + if(WIN32) + set_tests_properties(guest_thread_sanitizer PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_system_command_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemCommand.cpp) target_include_directories(guest_system_command_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_system_command_tests PRIVATE libkernel libc) @@ -563,6 +1330,40 @@ if(BUILD_TESTING) set_tests_properties(guest_system_command PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_socket_error_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketError.cpp) + target_include_directories(guest_socket_error_tests PRIVATE ${LIBS_INCLUDE_DIR}) + configure_windows_unwind(guest_socket_error_tests) + add_test(NAME guest_socket_error COMMAND guest_socket_error_tests) + add_test_executable(guest_socket_options_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketOptions.cpp) + target_include_directories(guest_socket_options_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_socket_options_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_socket_options_tests) + add_test(NAME guest_socket_options COMMAND guest_socket_options_tests) + set_tests_properties(guest_socket_options PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_socket_options PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_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) @@ -573,6 +1374,26 @@ 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) + configure_windows_unwind(guest_socket_read_write_tests) + add_test(NAME guest_socket_read_write COMMAND guest_socket_read_write_tests) + set_tests_properties(guest_socket_read_write PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_socket_read_write PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_poll_tests ${LIBS_INCLUDE_DIR}/tests/GuestPoll.cpp) target_include_directories(guest_poll_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_poll_tests PRIVATE libkernel libc) @@ -580,9 +1401,30 @@ if(BUILD_TESTING) add_test(NAME guest_poll COMMAND guest_poll_tests) set_tests_properties(guest_poll PROPERTIES TIMEOUT 20) if(WIN32) + target_link_libraries(guest_poll_tests PRIVATE ws2_32) set_tests_properties(guest_poll PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_select_wait_tests ${LIBS_INCLUDE_DIR}/tests/GuestSelectWait.cpp) + target_include_directories(guest_select_wait_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_select_wait_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_select_wait_tests) + add_test(NAME guest_select_wait COMMAND guest_select_wait_tests) + set_tests_properties(guest_select_wait PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_select_wait PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_select_tests ${LIBS_INCLUDE_DIR}/tests/GuestSelect.cpp) + target_include_directories(guest_select_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_select_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_select_tests) + add_test(NAME guest_select COMMAND guest_select_tests) + set_tests_properties(guest_select PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_select PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_tcp_tests ${LIBS_INCLUDE_DIR}/tests/GuestTcp.cpp) target_include_directories(guest_tcp_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_tcp_tests PRIVATE libkernel libc) @@ -593,14 +1435,108 @@ 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) + target_include_directories(guest_dup_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_dup_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_dup_tests) + add_test(NAME guest_dup COMMAND guest_dup_tests) + set_tests_properties(guest_dup PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_dup PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_kqueue_tests ${LIBS_INCLUDE_DIR}/tests/GuestKqueue.cpp) + target_include_directories(guest_kqueue_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_kqueue_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_kqueue_tests) + add_test(NAME guest_kqueue COMMAND guest_kqueue_tests) + set_tests_properties(guest_kqueue PROPERTIES TIMEOUT 20) + if(WIN32) + target_link_libraries(guest_kqueue_tests PRIVATE advapi32) + set_tests_properties(guest_kqueue PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_socket_descriptors_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketDescriptors.cpp) + target_include_directories(guest_socket_descriptors_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_socket_descriptors_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_socket_descriptors_tests) + add_test(NAME guest_socket_descriptors COMMAND guest_socket_descriptors_tests) + set_tests_properties(guest_socket_descriptors PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_socket_descriptors PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_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) + configure_windows_unwind(guest_socket_event_tests) + add_test(NAME guest_socket_events COMMAND guest_socket_event_tests) + set_tests_properties(guest_socket_events PROPERTIES TIMEOUT 20) + if(WIN32) + 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) @@ -636,9 +1572,80 @@ if(BUILD_TESTING) target_include_directories(guest_memory_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_memory_tests PRIVATE libkernel libc) configure_windows_unwind(guest_memory_tests) - add_test(NAME guest_memory COMMAND guest_memory_tests) + add_library(guest_memory_module SHARED ${LIBS_INCLUDE_DIR}/tests/GuestMemoryModule.cpp) + set_target_properties(guest_memory_module PROPERTIES PREFIX "" SUFFIX ".guest.prx") + add_dependencies(guest_memory_tests guest_memory_module) + target_compile_definitions(guest_memory_tests PRIVATE GUEST_MEMORY_MODULE="$") if(WIN32) - set_tests_properties(guest_memory PROPERTIES ENVIRONMENT_MODIFICATION + add_library(guest_memory_elf0_module SHARED + ${LIBS_INCLUDE_DIR}/tests/GuestMemoryModule.cpp + ${LIBS_INCLUDE_DIR}/tests/GuestMemoryModule.S) + set_target_properties(guest_memory_elf0_module PROPERTIES PREFIX "" SUFFIX ".guest.prx") + add_dependencies(guest_memory_tests guest_memory_elf0_module) + target_compile_definitions(guest_memory_tests PRIVATE GUEST_MEMORY_ELF0_MODULE="$") + endif() + add_test(NAME guest_memory COMMAND guest_memory_tests) + add_test(NAME guest_mmap_fixed_anonymous COMMAND guest_memory_tests fixed-anonymous) + if(WIN32) + 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) + target_include_directories(guest_flexible_memory_budget_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_flexible_memory_budget_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_flexible_memory_budget_tests) + add_library(guest_flexible_memory_module SHARED ${LIBS_INCLUDE_DIR}/tests/GuestFlexibleMemoryModule.cpp) + set_target_properties(guest_flexible_memory_module PROPERTIES PREFIX "" SUFFIX ".guest.prx") + add_dependencies(guest_flexible_memory_budget_tests guest_flexible_memory_module) + target_compile_definitions(guest_flexible_memory_budget_tests PRIVATE GUEST_FLEXIBLE_MEMORY_MODULE="$") + foreach(flexibleMemoryCase IN ITEMS declared:838860800 limited:16777216 none:0 missing:0 invalid:true) + string(REPLACE ":" ";" flexibleMemoryCase ${flexibleMemoryCase}) + list(GET flexibleMemoryCase 0 flexibleMemoryName) + list(GET flexibleMemoryCase 1 flexibleMemorySize) + set(flexibleMemoryRun ${CMAKE_CURRENT_BINARY_DIR}/guest_flexible_memory_${flexibleMemoryName}) + file(MAKE_DIRECTORY ${flexibleMemoryRun}) + if(NOT flexibleMemoryName STREQUAL "missing") + set(flexibleMemoryField "") + if(NOT flexibleMemoryName STREQUAL "none") + set(flexibleMemoryField ",\"kernel\":{\"flexibleMemorySize\":${flexibleMemorySize}}") + endif() + file(WRITE ${flexibleMemoryRun}/app0/sce_sys/param.json + "{\"titleId\":\"PPSA00000\",\"localizedParameters\":{\"en-US\":{\"titleName\":\"Example\"}}${flexibleMemoryField}}") + endif() + add_test(NAME guest_flexible_memory_${flexibleMemoryName} COMMAND guest_flexible_memory_budget_tests ${flexibleMemoryName} + WORKING_DIRECTORY ${flexibleMemoryRun}) + if(WIN32) + set_tests_properties(guest_flexible_memory_${flexibleMemoryName} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + add_test_executable(guest_page_table_stats_tests ${LIBS_INCLUDE_DIR}/tests/GuestPageTableStats.cpp) + target_include_directories(guest_page_table_stats_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_page_table_stats_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_page_table_stats_tests) + add_test(NAME guest_page_table_stats COMMAND guest_page_table_stats_tests) + if(WIN32) + set_tests_properties(guest_page_table_stats PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(guest_mlock_tests ${LIBS_INCLUDE_DIR}/tests/GuestMlock.cpp) @@ -670,12 +1677,26 @@ if(BUILD_TESTING) set_tests_properties(guest_directory_buffer PROPERTIES 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(WIN32) + add_test_executable(guest_directory_rewind_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectoryRewind.cpp) + target_include_directories(guest_directory_rewind_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_directory_rewind_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_directory_rewind_tests) + add_test(NAME guest_directory_rewind COMMAND guest_directory_rewind_tests) + set_tests_properties(guest_directory_rewind PROPERTIES TIMEOUT 15 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + 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) @@ -686,6 +1707,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}) @@ -696,6 +1737,91 @@ 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) + configure_windows_unwind(guest_fstatfs_tests) + add_test(NAME guest_fstatfs COMMAND guest_fstatfs_tests) + set_tests_properties(guest_fstatfs PROPERTIES TIMEOUT 15) + if(WIN32) + 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 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) + 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:$") + endforeach() + endif() add_test_executable(guest_sandbox_random_word_tests ${LIBS_INCLUDE_DIR}/tests/GuestSandboxRandomWord.cpp) target_include_directories(guest_sandbox_random_word_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_sandbox_random_word_tests PRIVATE libkernel libc) @@ -705,5 +1831,80 @@ if(BUILD_TESTING) if(WIN32) set_tests_properties(guest_sandbox_random_word PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + add_test_executable(guest_mutex_raise_exception_tests ${LIBS_INCLUDE_DIR}/tests/GuestMutexRaiseException.cpp) + target_include_directories(guest_mutex_raise_exception_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_mutex_raise_exception_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_mutex_raise_exception_tests) + 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) + configure_windows_unwind(guest_mutex_contention_tests) + add_test(NAME guest_mutex_contention COMMAND guest_mutex_contention_tests) + set_tests_properties(guest_mutex_contention PROPERTIES TIMEOUT 20 RUN_SERIAL TRUE ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_pthread_rename_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadRename.cpp) + target_include_directories(guest_pthread_rename_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_rename_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_rename_tests) + add_test(NAME guest_pthread_rename COMMAND guest_pthread_rename_tests) + set_tests_properties(guest_pthread_rename PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_rename PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_coredump_attach_tests ${LIBS_INCLUDE_DIR}/tests/GuestCoredumpAttach.cpp) + target_include_directories(guest_coredump_attach_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_coredump_attach_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_coredump_attach_tests) + add_test(NAME guest_coredump_attach COMMAND guest_coredump_attach_tests) + set_tests_properties(guest_coredump_attach PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_coredump_attach PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_coredump_user_data_tests ${LIBS_INCLUDE_DIR}/tests/GuestCoredumpUserData.cpp) + target_include_directories(guest_coredump_user_data_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_coredump_user_data_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_coredump_user_data_tests) + add_test(NAME guest_coredump_user_data COMMAND guest_coredump_user_data_tests) + set_tests_properties(guest_coredump_user_data PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_coredump_user_data PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") endif() endif() + +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 b10c4571b5..e3bddd7224 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -1,4 +1,5 @@ #include "prx/libkernel/DirectMemory/DirectMemory.hpp" +#include "prx/libkernel/Pthread/include/Pthread.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/GuestWriteWatch.hpp" @@ -11,14 +12,19 @@ #include #include #include +#include #include +#include #include #include +#include +#include #include #include #if defined(__linux__) #include +#include #include #include #include @@ -27,6 +33,7 @@ #include #else #include +#include "prx/libc/include/WindowsDescriptor.hpp" static constexpr int PROT_NONE = 0; static constexpr int PROT_READ = 1; @@ -53,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); } @@ -64,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)); @@ -113,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) { @@ -136,9 +177,36 @@ 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; +constexpr int GuestProtGpuReadWrite = 0x30; #if defined(__linux__) void* MapAtOrAbove(std::uintptr_t start, size_t len, int prot, size_t alignment) { @@ -221,12 +289,101 @@ void Trace(const char* format, ...) { va_end(args); } +#if defined(__linux__) +int PhysicalFile() { + static const int file = [] { + const int created = memfd_create("direct memory", MFD_CLOEXEC | MFD_ALLOW_SEALING); + if (created < 0) throw std::system_error(errno, std::generic_category(), "create direct memory backing"); + if (ftruncate(created, static_cast(DIRECT_MEMORY_SIZE)) != 0 || fcntl(created, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW) != 0) { + const int error = errno; + ::close(created); + throw std::system_error(error, std::generic_category(), "size direct memory backing"); + } + return created; + }(); + return file; +} + +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 (!PhysicalUsed()[first + page]) { + PhysicalUsed()[first + page] = true; + ++page; + continue; + } + std::size_t run = 0; + 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"); + std::memset(view, 0, run * PS5_PAGE_SIZE); + ::munmap(view, run * PS5_PAGE_SIZE); + page += run; + } +} +#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: - explicit PhysicalBacking(std::size_t bytes, int memoryType) : memoryType(memoryType), bytes(bytes) { + 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, 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]; @@ -234,18 +391,9 @@ public: throw std::system_error(error, std::system_category(), message); } #else - file = memfd_create("direct memory", MFD_CLOEXEC | MFD_ALLOW_SEALING); - if (file < 0) throw std::system_error(errno, std::generic_category(), "create direct memory backing"); - if (ftruncate(file, static_cast(bytes)) != 0) { - const int error = errno; - ::close(file); - throw std::system_error(error, std::generic_category(), "size direct memory backing"); - } - if (fcntl(file, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW) != 0) { - const int error = errno; - ::close(file); - throw std::system_error(error, std::generic_category(), "seal direct memory backing"); - } + file = PhysicalFile(); + fileOffset = start; + ZeroReusedPhysical(start, bytes); #endif } @@ -256,20 +404,25 @@ public: void* HostWriteData(std::uint64_t offset, std::size_t length) { if (offset > bytes || length > bytes - offset) return nullptr; if (hostWriteView == nullptr) { - auto* mapped = ::mmap(nullptr, bytes, PROT_READ | PROT_WRITE, MAP_SHARED, file, 0); + auto* mapped = ::mmap(nullptr, bytes, PROT_READ | PROT_WRITE, MAP_SHARED, file, static_cast(fileOffset)); if (mapped == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "Direct memory write view mmap failed"); hostWriteView = mapped; } return static_cast(hostWriteView) + offset; } + std::uint64_t FileOffset() const { return fileOffset; } #endif ~PhysicalBacking() { #ifdef _WIN32 - 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); - ::close(file); #endif } @@ -278,9 +431,9 @@ public: void Map(std::uintptr_t address, std::size_t bytes, std::uint64_t offset, int protection) const { #ifdef _WIN32 - GuestArena::GuestArenaMap_nid_postfix(reinterpret_cast(address), bytes, section, offset, WinProtFromPosix(protection)); + GuestArena::GuestArenaMapDirect_nid_postfix(reinterpret_cast(address), bytes, section, offset, WinProtFromPosix(protection)); #else - if (::mmap(reinterpret_cast(address), bytes, protection, MAP_SHARED | MAP_FIXED, file, static_cast(offset)) == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "map direct memory backing"); + if (::mmap(reinterpret_cast(address), bytes, protection, MAP_SHARED | MAP_FIXED, file, static_cast(fileOffset + offset)) == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "map direct memory backing"); #endif } @@ -292,6 +445,7 @@ private: #else int file = -1; void* hostWriteView = nullptr; + std::uint64_t fileOffset = 0; #endif }; @@ -300,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; @@ -310,7 +461,105 @@ struct DirectMapping { std::shared_ptr backing; }; -std::map g_directMappings; +struct PhysicalSpan { + std::uint64_t end; + std::vector bases; +}; + +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); + if (span == g_physicalSpans.begin()) return; + --span; + if (span->first == at || span->second.end <= at) return; + g_physicalSpans.emplace_hint(std::next(span), at, PhysicalSpan{span->second.end, span->second.bases}); + span->second.end = at; +} + +std::map::iterator InsertMapping(std::uintptr_t base, DirectMapping mapping) { + const auto [inserted, added] = g_directMappings.emplace(base, std::move(mapping)); + if (!added) throw std::runtime_error("direct mapping inserted over a live one"); + const auto first = inserted->second.phys; + const auto last = first + (inserted->second.end - base); + SplitPhysicalSpan(first); + SplitPhysicalSpan(last); + auto span = g_physicalSpans.lower_bound(first); + for (auto cursor = first; cursor < last; ++span) { + if (span == g_physicalSpans.end() || span->first > cursor) span = g_physicalSpans.emplace_hint(span, cursor, PhysicalSpan{span == g_physicalSpans.end() ? last : std::min(last, span->first), {}}); + auto& bases = span->second.bases; + bases.insert(std::upper_bound(bases.begin(), bases.end(), base), base); + cursor = span->second.end; + } + return inserted; +} + +std::map::iterator RemoveMapping(std::map::iterator mapping) { + const auto base = mapping->first; + const auto first = mapping->second.phys; + const auto last = first + (mapping->second.end - base); + for (auto span = g_physicalSpans.lower_bound(first); span != g_physicalSpans.end() && span->first < last;) { + auto& bases = span->second.bases; + bases.erase(std::lower_bound(bases.begin(), bases.end(), base)); + span = bases.empty() ? g_physicalSpans.erase(span) : std::next(span); + } + for (auto span = g_physicalSpans.lower_bound(first); span != g_physicalSpans.end() && span->first <= last;) { + if (span != g_physicalSpans.begin()) { + const auto before = std::prev(span); + if (before->second.end == span->first && before->second.bases == span->second.bases) { + before->second.end = span->second.end; + span = g_physicalSpans.erase(span); + continue; + } + } + ++span; + } + return g_directMappings.erase(mapping); +} + +std::vector MappingsSharingPhys(std::uint64_t first, std::uint64_t last) { + std::vector bases; + for (auto span = g_physicalSpans.lower_bound(first); span != g_physicalSpans.end() && span->first < last; ++span) bases.insert(bases.end(), span->second.bases.begin(), span->second.bases.end()); + if (auto span = g_physicalSpans.lower_bound(first); span != g_physicalSpans.begin() && (--span)->second.end > first) bases.insert(bases.end(), span->second.bases.begin(), span->second.bases.end()); + std::sort(bases.begin(), bases.end()); + bases.erase(std::unique(bases.begin(), bases.end()), bases.end()); + return bases; +} void EraseMappings(std::uintptr_t start, std::uintptr_t end) { auto it = g_directMappings.lower_bound(start); @@ -318,34 +567,31 @@ void EraseMappings(std::uintptr_t start, std::uintptr_t end) { while (it != g_directMappings.end() && it->first < end) { const auto base = it->first; const auto mapping = it->second; - it = g_directMappings.erase(it); - if (base < start) g_directMappings.emplace(base, DirectMapping{start, mapping.phys, mapping.memoryType, mapping.backing}); - if (mapping.end > end) it = g_directMappings.emplace(end, DirectMapping{mapping.end, mapping.phys + end - base, mapping.memoryType, mapping.backing}).first; + it = RemoveMapping(it); + if (base < start) InsertMapping(base, DirectMapping{start, mapping.phys, mapping.memoryType, mapping.backing}); + if (mapping.end > end) it = InsertMapping(end, DirectMapping{mapping.end, mapping.phys + end - base, mapping.memoryType, mapping.backing}); } } void ErasePhysMappings(std::uint64_t first, std::uint64_t last) { - for (auto it = g_directMappings.begin(); it != g_directMappings.end();) { - const auto base = it->first; + for (const auto base : MappingsSharingPhys(first, last)) { + const auto it = g_directMappings.find(base); + if (it == g_directMappings.end()) continue; const auto mapping = it->second; const auto mappingLast = mapping.phys + (mapping.end - base); - if (mapping.phys >= last || mappingLast <= first) { - ++it; - continue; - } - it = g_directMappings.erase(it); + if (mapping.phys >= last || mappingLast <= first) continue; + RemoveMapping(it); if (mapping.phys < first) { const auto keep = first - mapping.phys; - g_directMappings.emplace(base, DirectMapping{base + keep, mapping.phys, mapping.memoryType, mapping.backing}); + InsertMapping(base, DirectMapping{base + keep, mapping.phys, mapping.memoryType, mapping.backing}); } if (mappingLast > last) { const auto skip = last - mapping.phys; - it = g_directMappings.emplace(base + skip, DirectMapping{mapping.end, last, mapping.memoryType, mapping.backing}).first; + InsertMapping(base + skip, DirectMapping{mapping.end, last, mapping.memoryType, mapping.backing}); } } } - 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)) { @@ -363,7 +609,8 @@ void WatchMapping(std::uintptr_t address, std::size_t len) { for (auto added = g_directMappings.lower_bound(address); added != g_directMappings.end() && added->first < end; ++added) { const auto addedPhys = added->second.phys; const auto addedPhysEnd = addedPhys + (added->second.end - added->first); - for (const auto& [base, other] : g_directMappings) { + for (const auto base : MappingsSharingPhys(addedPhys, addedPhysEnd)) { + const auto& other = g_directMappings.at(base); if ((base >= address && base < end) || other.backing != added->second.backing) continue; const auto first = std::max(addedPhys, other.phys); const auto last = std::min(addedPhysEnd, other.phys + (other.end - base)); @@ -396,7 +643,7 @@ bool SharedBacking(std::uintptr_t address, std::size_t bytes, int* file, std::ui const int duplicate = fcntl(backing->File(), F_DUPFD_CLOEXEC, 0); if (duplicate < 0) throw std::system_error(errno, std::generic_category(), "duplicate direct memory backing"); *file = duplicate; - *offset = page->second.offset + phys % PS5_PAGE_SIZE; + *offset = backing->FileOffset() + page->second.offset + phys % PS5_PAGE_SIZE; return true; } @@ -445,7 +692,7 @@ void AddMapping(std::uintptr_t address, std::size_t len, std::uint64_t phys, int bytes += PS5_PAGE_SIZE; } page.backing->Map(address + offset, bytes, page.offset, nativeProt); - g_directMappings.emplace(address + offset, DirectMapping{address + offset + bytes, phys + offset, page.backing->MemoryType(), page.backing}); + InsertMapping(address + offset, DirectMapping{address + offset + bytes, phys + offset, page.backing->MemoryType(), page.backing}); offset += bytes; } #if defined(__linux__) @@ -453,14 +700,48 @@ void AddMapping(std::uintptr_t address, std::size_t len, std::uint64_t phys, int #endif } +void RequireResidentUnpinned(GuestAllocations::Mutation& mutation, std::uintptr_t address, std::size_t len) { +#ifdef _WIN32 + std::uintptr_t start = 0; + std::uintptr_t end = 0; + if (GuestArena::GuestArenaResidentSpan_nid_postfix(address, len, &start, &end)) mutation.RequireUnpinned(reinterpret_cast(start), end - start); +#else + static_cast(mutation); + static_cast(address); + static_cast(len); +#endif +} + +void RequireAliasesUnpinned(GuestAllocations::Mutation& mutation, std::uint64_t phys, std::size_t len) { +#ifdef _WIN32 + std::vector> aliases; + { + std::lock_guard lock(g_directLock); + for (const auto base : 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); + } + } + for (const auto& [address, bytes] : aliases) RequireResidentUnpinned(mutation, address, bytes); +#else + static_cast(mutation); + static_cast(phys); + static_cast(len); +#endif +} + bool RemapFixedIntoRegistered(GuestAllocations::Mutation& mutation, void* addr, size_t len, int prot, int flags, int64_t physStart = -1) { constexpr int GuestMapFixed = 0x10; constexpr int GuestMapNoOverwrite = 0x80; if (addr == nullptr || (flags & GuestMapFixed) == 0 || (flags & GuestMapNoOverwrite) != 0 || !mutation.Covers(addr, len)) return false; ValidateRange(addr, len, PS5_PAGE_SIZE); + RequireResidentUnpinned(mutation, reinterpret_cast(addr), len); + if (physStart >= 0) RequireAliasesUnpinned(mutation, static_cast(physStart), len); Trace("remap fixed %p+0x%zx prot=0x%x phys=0x%llx", addr, len, prot, static_cast(physStart)); const auto nativeProtection = LinuxProtFromSce(prot); - mutation.Protect(addr, len, (prot & 3) != 0, (prot & 2) != 0, [&] { + mutation.Protect(addr, len, (prot & 3) != 0, (prot & 2) != 0, (prot & GuestProtGpuReadWrite) != 0, [&] { const auto address = reinterpret_cast(addr); std::lock_guard lock(g_directLock); if (physStart >= 0) { @@ -476,12 +757,28 @@ bool RemapFixedIntoRegistered(GuestAllocations::Mutation& mutation, void* addr, #endif } }); + if ((prot & GuestProtGpuReadWrite) != 0) mutation.NoteGpuMapping(addr, len); return true; } +#if defined(__linux__) +bool ArenaAvailable() { return GuestArena::GuestArenaAvailable_nid_postfix(); } + +void* ArenaCommit(void* addr, size_t len, int prot) { + return mmap(addr, len, prot, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); +} + +void ArenaForget(void* addr, size_t len) { + if (GuestArena::GuestArenaContains_nid_postfix(addr, len)) GuestArena::GuestArenaRelease_nid_postfix(addr, len); +} +#endif + void Unmap(void* addr, size_t len) { #if defined(__linux__) - if (munmap(addr, len) != 0) throw std::system_error(errno, std::generic_category(), "munmap failed"); + if (ArenaAvailable() && GuestArena::GuestArenaContains_nid_postfix(addr, len)) + GuestArena::GuestArenaReset_nid_postfix(addr, len); + else if (munmap(addr, len) != 0) throw std::system_error(errno, std::generic_category(), "munmap failed"); + ArenaForget(addr, len); GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(addr, len); #else if (KernelArena::Get().Contains(addr, len)) munmap(addr, len); @@ -530,6 +827,14 @@ void* MapPlaced(void* addr, size_t len, int prot, int flags, size_t alignment) { } #endif #if defined(__linux__) + if (ArenaAvailable() && !GuestArena::GuestArenaContains_nid_postfix(addr, len)) + throw std::invalid_argument("fixed mapping is outside the guest address space arena"); + if (ArenaAvailable()) { + GuestArena::GuestArenaMarkUsed_nid_postfix(addr, len); + if (void* placed = ArenaCommit(addr, len, prot); placed != MAP_FAILED) return placed; + GuestArena::GuestArenaRelease_nid_postfix(addr, len); + throw std::system_error(errno, std::generic_category(), "Arena mapping failed"); + } const int placement = (flags & GuestMapNoOverwrite) != 0 ? MAP_FIXED_NOREPLACE : MAP_FIXED; #else const int placement = MAP_FIXED; @@ -543,6 +848,13 @@ void* MapPlaced(void* addr, size_t len, int prot, int flags, size_t alignment) { } if (addr) { #if defined(__linux__) + if (ArenaAvailable()) { + 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); + throw std::system_error(errno, std::generic_category(), "Arena mapping failed"); + } return MapAtOrAbove(reinterpret_cast(addr), len, prot, alignment); #else return mmap_aligned(len, prot, alignment, reinterpret_cast(addr)); @@ -550,6 +862,14 @@ void* MapPlaced(void* addr, size_t len, int prot, int flags, size_t alignment) { } #ifdef _WIN32 return mmap_aligned(len, prot, alignment); +#endif +#if defined(__linux__) + if (ArenaAvailable()) { + void* placed = GuestArena::GuestArenaAllocate_nid_postfix(len, alignment); + if (void* committed = ArenaCommit(placed, len, prot); committed != MAP_FAILED) return committed; + ArenaForget(placed, len); + throw std::system_error(errno, std::generic_category(), "Arena mapping failed"); + } #endif if (len > std::numeric_limits::max() - alignment) { throw std::overflow_error("Aligned mapping size overflow"); @@ -577,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; @@ -621,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) { @@ -660,6 +977,7 @@ bool Reserved(const void* addr, size_t len) { } void UnmapRegistered(GuestAllocations::Mutation& mutation, void* addr, size_t len) { + RequireResidentUnpinned(mutation, reinterpret_cast(addr), len); mutation.Unmap(addr, len, [&](const void* piece, std::size_t pieceBytes, const void* allocation, bool last) { auto* pieceAddress = const_cast(piece); std::lock_guard lock(g_directLock); @@ -689,6 +1007,11 @@ bool FixedNoOverwriteConflict(const GuestAllocations::Mutation& mutation, void* return mutation.Overlaps(addr, len) && !Reserved(addr, len); } +int PlaceHintInReservation(void* addr, size_t len, int flags) { + if (addr == nullptr || (flags & GuestMapFixedFlag) != 0 || !Reserved(addr, len)) return flags; + return flags | GuestMapFixedFlag; +} + int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, size_t alignment) { ValidateOutput(addr); if (len == 0 || (len & (PS5_PAGE_SIZE - 1)) != 0) return SCE_KERNEL_ERROR_EINVAL; @@ -696,6 +1019,7 @@ int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, return SCE_KERNEL_ERROR_EINVAL; } GuestAllocations::Mutation mutation; + flags = PlaceHintInReservation(*addr, len, flags); if (FixedNoOverwriteConflict(mutation, *addr, len, flags)) return SCE_KERNEL_ERROR_ENOMEM; if (RemapFixedIntoRegistered(mutation, *addr, len, prot, flags, physStart)) { EraseReservations(*addr, len); @@ -703,12 +1027,14 @@ int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, return 0; } ReplaceFixedOverlap(mutation, *addr, len, flags); + RequireAliasesUnpinned(mutation, static_cast(physStart), len); std::lock_guard lock(g_directLock); ValidatePhysicalRange(static_cast(physStart), len); void* mapped = MapAligned(*addr, len, PROT_NONE, flags, alignment); try { AddMapping(reinterpret_cast(mapped), len, static_cast(physStart), LinuxProtFromSce(prot)); - mutation.Add(mapped, len, (prot & 3) != 0, (prot & 2) != 0); + mutation.Add(mapped, len, (prot & 3) != 0, (prot & 2) != 0, (prot & GuestProtGpuReadWrite) != 0); + if ((prot & GuestProtGpuReadWrite) != 0) mutation.NoteGpuMapping(mapped, len); } catch (...) { EraseMappings(reinterpret_cast(mapped), reinterpret_cast(mapped) + len); Unmap(mapped, len); @@ -721,10 +1047,59 @@ 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) { +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)) { EraseReservations(*addr, len); @@ -732,9 +1107,10 @@ int DoMapAnon(void** addr, size_t len, int prot, int flags) { return 0; } ReplaceFixedOverlap(mutation, *addr, len, flags); - void* mapped = MapAligned(*addr, len, LinuxProtFromSce(prot), flags, PS5_PAGE_SIZE); + void* mapped = MapAligned(*addr, len, LinuxProtFromSce(prot), flags, alignment); try { - mutation.Add(mapped, len, (prot & 3) != 0, (prot & 2) != 0); + mutation.Add(mapped, len, (prot & 3) != 0, (prot & 2) != 0, (prot & GuestProtGpuReadWrite) != 0); + if ((prot & GuestProtGpuReadWrite) != 0) mutation.NoteGpuMapping(mapped, len); } catch (...) { Unmap(mapped, len); throw; @@ -746,6 +1122,163 @@ int DoMapAnon(void** addr, size_t len, int prot, int flags) { return 0; } +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__) + const bool shared = (flags & 3) == 1; + GuestAllocations::Mutation mutation; + const int placementFlags = flags & (GuestMapFixedFlag | 0x80 | 0x400000); + flags = PlaceHintInReservation(*addr, len, placementFlags); + if (FixedNoOverwriteConflict(mutation, *addr, len, flags)) return SCE_KERNEL_ERROR_ENOMEM; + ReplaceFixedOverlap(mutation, *addr, len, flags); + void* mapped = MapPlaced(*addr, len, PROT_NONE, flags, alignment); + 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) { + const int error = errno; + Unmap(mapped, len); + throw std::system_error(error, std::generic_category(), "map guest shared memory"); + } + try { + mutation.Add(mapped, len, (prot & 3) != 0, (prot & 2) != 0, false); + } catch (...) { + Unmap(mapped, len); + throw; + } + *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; +#else + 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 +} + +int DoMapShared(void** addr, size_t len, int prot, HostSharedMemory object) { + ValidateOutput(addr); + if (len == 0 || (len & (PS5_PAGE_SIZE - 1)) != 0) return SCE_KERNEL_ERROR_EINVAL; + const auto nativeProtection = LinuxProtFromSce(prot); + GuestAllocations::Mutation mutation; + void* mapped = MapPlaced(*addr, len, PROT_NONE, 0, PS5_PAGE_SIZE); + try { +#ifdef _WIN32 + GuestArena::GuestArenaMap_nid_postfix(mapped, len, object, 0, WinProtFromPosix(nativeProtection)); +#else + if (::mmap(mapped, len, nativeProtection, MAP_SHARED | MAP_FIXED, object, 0) == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "map shared memory"); +#endif + mutation.Add(mapped, len, (prot & 3) != 0, (prot & 2) != 0, false); + } catch (...) { + Unmap(mapped, len); + throw; + } + *addr = mapped; + RecordProtection(mapped, len, prot); + Trace("map shared %p+0x%zx prot=0x%x", mapped, len, prot); + return 0; +} + +#ifdef _WIN32 +static 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); + if (dos->e_magic != IMAGE_DOS_SIGNATURE || dos->e_lfanew <= 0) return false; + const auto* nt = reinterpret_cast(address + static_cast(dos->e_lfanew)); + if (nt->Signature != IMAGE_NT_SIGNATURE || nt->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC) return false; + const auto* sections = IMAGE_FIRST_SECTION(nt); + const auto sectionCount = nt->FileHeader.NumberOfSections; + for (unsigned i = 0; i < sectionCount; ++i) { + if (std::strncmp(reinterpret_cast(sections[i].Name), ".elf0", IMAGE_SIZEOF_SHORT_NAME) == 0) return true; + } + 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) { Trace("protect %p+0x%zx prot=0x%x", addr, len, prot); const auto address = reinterpret_cast(addr); @@ -757,26 +1290,88 @@ int DoMprotect(const void* addr, size_t len, int prot) { const auto bytes = static_cast(end - first); const auto* pointer = reinterpret_cast(first); const auto nativeProtection = LinuxProtFromSce(prot); + if (GuestThreadStackHostRangeOverlaps(address, address + len)) + throw std::system_error(EACCES, std::generic_category(), "mprotect overlaps host-only guest thread stack pages"); + std::uintptr_t stackStart = 0; + std::uintptr_t stackEnd = 0; + if ((prot & GuestProtGpuReadWrite) == 0 && GuestThreadStack(address, &stackStart, &stackEnd) && address >= stackStart && address + len <= stackEnd) { + constexpr std::uintptr_t hostPageMask = 0xfff; + const auto requested = std::max(first, (stackStart + hostPageMask) & ~hostPageMask); + const auto insideEnd = std::min(end, stackEnd & ~hostPageMask); + 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); +#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; + } GuestAllocations::Mutation mutation; #ifdef _WIN32 MEMORY_BASIC_INFORMATION memory{}; if (VirtualQuery(pointer, &memory, sizeof(memory)) != sizeof(memory)) throw std::runtime_error("Cannot query guest memory protection range"); if (memory.Type == MEM_IMAGE) { - if (memory.AllocationBase != GetModuleHandleW(nullptr)) throw std::invalid_argument("Memory protection of a foreign image is not supported"); - mutation.RegisterMainImage(); + if (memory.AllocationBase == GetModuleHandleW(nullptr)) mutation.RegisterMainImage(); + 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, [&] { - 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; @@ -789,13 +1384,13 @@ int DoMtypeprotect(const void* addr, size_t len, int type, int prot) { while (it != g_directMappings.end() && it->first < end) { const auto base = it->first; const auto mapping = it->second; - it = g_directMappings.erase(it); - if (base < first) g_directMappings.emplace(base, DirectMapping{first, mapping.phys, mapping.memoryType, mapping.backing}); + it = RemoveMapping(it); + if (base < first) InsertMapping(base, DirectMapping{first, mapping.phys, mapping.memoryType, mapping.backing}); const auto low = std::max(base, first); const auto high = std::min(mapping.end, end); - g_directMappings.emplace(low, DirectMapping{high, mapping.phys + low - base, type, mapping.backing}); + InsertMapping(low, DirectMapping{high, mapping.phys + low - base, type, mapping.backing}); physical.emplace_back(mapping.phys + low - base, high - low); - if (mapping.end > end) it = g_directMappings.emplace(end, DirectMapping{mapping.end, mapping.phys + end - base, mapping.memoryType, mapping.backing}).first; + if (mapping.end > end) it = InsertMapping(end, DirectMapping{mapping.end, mapping.phys + end - base, mapping.memoryType, mapping.backing}); } } for (const auto& [phys, bytes] : physical) DirectMemoryRetype(static_cast(phys), bytes, type); @@ -806,6 +1401,7 @@ int DoMunmap(void* addr, size_t len) { Trace("unmap %p+0x%zx", addr, len); if (len == 0 || (len & (PS5_PAGE_SIZE - 1)) != 0 || !addr) return SCE_KERNEL_ERROR_EINVAL; GuestAllocations::Mutation mutation; + if (!mutation.Overlaps(addr, len)) return 0; UnmapRegistered(mutation, addr, len); return 0; } @@ -826,9 +1422,9 @@ int DoReserveVirtual(void** addr, size_t len, int flags, size_t alignment) { } if (fixed) mutation.RequireAvailable(*addr, len); constexpr int GuestMapNoCoalesce = 0x400000; - void* mapped = MapAligned(fixed ? *addr : nullptr, len, PROT_NONE, fixed ? GuestMapFixedFlag | (flags & GuestMapNoCoalesce) : 0, alignment); + void* mapped = MapAligned(*addr, len, PROT_NONE, fixed ? GuestMapFixedFlag | (flags & GuestMapNoCoalesce) : 0, alignment); try { - mutation.Add(mapped, len, false, false); + mutation.Add(mapped, len, false, false, false); } catch (...) { Unmap(mapped, len); throw; @@ -852,7 +1448,7 @@ void CreateDirectMemoryBacking(int64_t start, size_t len, int memoryType) { GuestArena::GuestArenaSetSharedBackingWriter_nid_no_patch(&WriteSharedBacking); }); #endif - const auto backing = std::make_shared(len, memoryType); + const auto backing = std::make_shared(len, memoryType, first); std::map pages; for (std::size_t offset = 0; offset < len; offset += PS5_PAGE_SIZE) pages.emplace(first + offset, PhysicalPage{backing, offset}); g_physPages.merge(pages); @@ -889,6 +1485,52 @@ bool GuestProtection(uintptr_t addr, int* prot) { return true; } +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 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; +} + bool QueryDirectMapping(std::uintptr_t address, std::uintptr_t* start, std::uintptr_t* end, std::uint64_t* offset, int* memoryType) { std::lock_guard lock(g_directLock); const auto next = g_directMappings.upper_bound(address); @@ -901,3 +1543,29 @@ bool QueryDirectMapping(std::uintptr_t address, std::uintptr_t* start, std::uint *memoryType = it->second.memoryType; return true; } + +std::vector DirectMemoryViews(int64_t start, size_t len) { + const auto first = static_cast(start); + const auto last = first + len; + std::vector views; + std::lock_guard lock(g_directLock); + for (const auto base : MappingsSharingPhys(first, last)) { + const auto& mapping = g_directMappings.at(base); + const auto mappingLast = mapping.phys + (mapping.end - base); + if (mapping.phys >= last || mappingLast <= first) continue; + const auto viewStart = base + (std::max(first, mapping.phys) - mapping.phys); + const auto viewEnd = base + (std::min(last, mappingLast) - mapping.phys); + if (!views.empty() && views.back().address + views.back().bytes == viewStart) views.back().bytes += viewEnd - viewStart; + else views.push_back({viewStart, viewEnd - viewStart}); + } + return views; +} + +void UnmapDirectMemoryViews(const std::vector& views) { + if (views.empty()) return; + GuestAllocations::Mutation mutation; + for (const auto& view : views) { + Trace("unmap released view %p+0x%zx", reinterpret_cast(view.address), view.bytes); + UnmapRegistered(mutation, reinterpret_cast(view.address), view.bytes); + } +} diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp index e2310818d9..604e85b34c 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp @@ -3,28 +3,54 @@ #include #include +#include #include "prx/libkernel/KernelErrors.hpp" static constexpr size_t DIRECT_MEMORY_SIZE = 13824ULL * 1024 * 1024; static constexpr size_t PS5_PAGE_SIZE = 0x4000; +#ifdef _WIN32 +using HostSharedMemory = void*; +#else +using HostSharedMemory = int; +#endif + int DirectMemoryAlloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int memoryType, int64_t* physOut); void DirectMemoryFree(int64_t start, size_t len); bool DirectMemoryCheckedFree(int64_t start, size_t len); +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); + +struct DirectMemoryView { + std::uintptr_t address; + std::size_t bytes; +}; + +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); +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); +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); int DoMunmap(void* addr, size_t len); int DoReserveVirtual(void** addr, size_t len, int flags, size_t alignment); bool GuestProtection(uintptr_t addr, int* prot); bool GuestReservation(std::uintptr_t addr, std::uintptr_t* start, std::uintptr_t* end); +bool GuestRangeMapped(std::uintptr_t start, std::uintptr_t end); +void ForgetJitSharedMemory(int descriptor); +void DuplicateJitSharedMemory(int from, int to); +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 d032fc90a0..79d07266cb 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -5,6 +5,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" +#include "prx/libkernel/AppMetadata/include/ParamJsonParser.hpp" #include "prx/libkernel/Pthread/include/Pthread.hpp" #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN @@ -20,6 +21,7 @@ #include #endif #include +#include #include #include #include @@ -29,9 +31,24 @@ #include #include +extern "C" std::uint64_t GuestImageSegmentBytes_nid_no_patch(); +extern "C" void* APS5_VABI mmap_nid_postfix(void* address, std::size_t length, int protection, int flags, int descriptor, std::int64_t offset) noexcept; +extern "C" int* APS5_VABI __error_nid_postfix(); + namespace { -constexpr size_t FLEXIBLE_MEMORY_SIZE = 448ULL * 1024 * 1024; +size_t _configuredFlexibleSize() { + static const size_t size = [] { + constexpr std::uint64_t defaultSize = 448ULL * 1024 * 1024; + const auto path = ResolvePath_nid_no_patch("/app0/sce_sys/param.json"); + const auto bytes = std::filesystem::exists(path) ? parseParamJson(path).flexibleMemorySizeBytes.value_or(defaultSize) : defaultSize; + if (bytes > std::numeric_limits::max()) + throw std::runtime_error("param.json kernel.flexibleMemorySize exceeds host size range"); + std::fprintf(stderr, "[memory] configured flexible memory: 0x%llx bytes (%llu MiB)\n", static_cast(bytes), static_cast(bytes >> 20)); + return static_cast(bytes); + }(); + return size; +} constexpr int PRT_APERTURE_COUNT = 3; @@ -76,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] = {}; @@ -88,19 +115,7 @@ size_t _flexibleUsedLocked() { return used; } -int _mapFlexible(void** addr, size_t len, int prot, int flags) { - std::lock_guard lock(g_flexibleLock); - if (len > FLEXIBLE_MEMORY_SIZE - std::min(FLEXIBLE_MEMORY_SIZE, _flexibleUsedLocked())) { - std::fprintf(stderr, "[memory] flexible memory exhausted: request 0x%zx with 0x%zx of 0x%zx in use\n", len, _flexibleUsedLocked(), static_cast(FLEXIBLE_MEMORY_SIZE)); - 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; @@ -114,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" { @@ -132,12 +219,12 @@ int APS5_VABI sceKernelAllocateMainDirectMemory(size_t len, size_t alignment, in int APS5_VABI sceKernelAvailableDirectMemorySize(int64_t search_start, int64_t search_end, size_t alignment, int64_t* phys_addr_out, size_t* size_out) { if (!phys_addr_out || !size_out) return SCE_KERNEL_ERROR_EINVAL; - int64_t tmpPhys = 0; - int ret = DirectMemoryAlloc(search_start, search_end, PS5_PAGE_SIZE, alignment, -1, &tmpPhys); - if (ret != 0) { *phys_addr_out = 0; *size_out = 0; return ret; } - DirectMemoryFree(tmpPhys, PS5_PAGE_SIZE); - *phys_addr_out = tmpPhys; - *size_out = DirectMemoryFreeRun(static_cast(tmpPhys), static_cast(search_end)); + int64_t start = 0; + size_t size = 0; + 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; } @@ -167,24 +254,34 @@ size_t APS5_VABI sceKernelGetDirectMemorySize(void) { return DIRECT_MEMORY_SIZE; } +int APS5_VABI sceKernelEnableDmemAliasing(void) { + return 0; +} + 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) { - return _mapFlexible(addr_in_out, len, prot, flags); + constexpr int IgnoredHeapFlag = 0x8000; + return _mapFlexible(addr_in_out, len, prot, flags & ~IgnoredHeapFlag); } int APS5_VABI sceKernelSetVirtualRangeName(const void* addr, uint64_t len, const char* name); 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; } @@ -201,36 +298,51 @@ 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; } +int APS5_VABI sceKernelMmap(void* addr, size_t len, int prot, int flags, int fd, int64_t offset, void** res) { + void* mapped = mmap_nid_postfix(addr, len, prot, flags, fd, offset); + if (mapped == reinterpret_cast(static_cast(-1))) return SceKernelError(*__error_nid_postfix()); + *res = mapped; + return 0; +} + int APS5_VABI sceKernelReleaseFlexibleMemory(void* addr, size_t len) { return sceKernelMunmap(reinterpret_cast(addr), len); } int APS5_VABI sceKernelReleaseDirectMemory(int64_t start, size_t len) { if (start < 0 || len == 0) return SCE_KERNEL_ERROR_EINVAL; - DirectMemoryFree(start, 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; + 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) { if (!info || info_size < sizeof(VirtualQueryInfo)) return SCE_KERNEL_ERROR_EINVAL; memset(info, 0, sizeof(VirtualQueryInfo)); const auto address = reinterpret_cast(addr); - // The mapping that contains the address, or with SCE_KERNEL_VQ_FIND_NEXT (flags bit 0) the first - // mapping at or above it: titles walk their mappings and check that a mapping covers a whole - // allocation, so the answer must be the registered allocation, not a page. constexpr int findNext = 1; std::uintptr_t reservedStart = 0; std::uintptr_t reservedEnd = 0; @@ -239,17 +351,10 @@ int APS5_VABI sceKernelVirtualQuery(const void* addr, int flags, VirtualQueryInf info->end = reservedEnd; return 0; } - const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); - const GuestAllocations::Range* best = nullptr; - for (const auto& range : lease) { - const auto begin = range->allocationAddress; - const auto end = begin + range->allocationBytes; - if (address >= begin && address < end) { best = range.get(); break; } - if ((flags & findNext) != 0 && begin > address && (best == nullptr || begin < best->allocationAddress)) best = range.get(); - } + const auto best = GuestAllocations::GuestAllocationsLookup_nid_postfix(address, (flags & findNext) != 0); if (best != nullptr) { - info->start = best->allocationAddress; - info->end = best->allocationAddress + best->allocationBytes; + info->start = best->address; + info->end = best->address + best->bytes; info->protection = (best->readable ? 1 : 0) | (best->writable ? 2 : 0) | (!best->releasable ? 4 : 0); int recorded = 0; if (GuestProtection(std::max(address, info->start), &recorded)) info->protection = recorded; @@ -313,10 +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; - return DirectMemoryCheckedFree(start, len) ? 0 : SCE_KERNEL_ERROR_ENOENT; + 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); } @@ -344,14 +452,22 @@ int APS5_VABI sceKernelIsStack(void* addr, void** start, void** end) { int APS5_VABI sceKernelAvailableFlexibleMemorySize(size_t* size) { if (!size) return SCE_KERNEL_ERROR_EINVAL; + const size_t configured = _configuredFlexibleSize(); + const size_t images = GuestImageSegmentBytes_nid_no_patch(); std::lock_guard lock(g_flexibleLock); - *size = FLEXIBLE_MEMORY_SIZE - std::min(FLEXIBLE_MEMORY_SIZE, _flexibleUsedLocked()); + *size = configured - std::min(configured, images + _flexibleUsedLocked()); return 0; } int APS5_VABI sceKernelConfiguredFlexibleMemorySize(size_t* size) { if (!size) return SCE_KERNEL_ERROR_EINVAL; - *size = FLEXIBLE_MEMORY_SIZE; + *size = _configuredFlexibleSize(); + return 0; +} + +int APS5_VABI sceKernelInternalMemoryGetAvailableSize(size_t* size) { + if (!size) return SCE_KERNEL_ERROR_EINVAL; + *size = 0; return 0; } @@ -373,11 +489,16 @@ int APS5_VABI sceKernelClearVirtualRangeName(const void* addr, uint64_t len) { } int APS5_VABI sceKernelGetPageTableStats(int* cpu_total, int* cpu_available, int* gpu_total, int* gpu_available) { - (void)cpu_total; - (void)cpu_available; - (void)gpu_total; - (void)gpu_available; - NotImplemented_nid_no_patch(__func__); + if (!cpu_total || !cpu_available || !gpu_total || !gpu_available) return SCE_KERNEL_ERROR_EFAULT; + constexpr std::uint64_t PageTableGranularity = 2ULL * 1024 * 1024; + constexpr std::uint64_t PageTablePoolEntries = 8192; + std::uint64_t cpuUsed = 0; + std::uint64_t gpuUsed = 0; + GuestPageTableUsage(PageTableGranularity, &cpuUsed, &gpuUsed); + *cpu_total = static_cast(PageTablePoolEntries); + *gpu_total = static_cast(PageTablePoolEntries); + *cpu_available = static_cast(cpuUsed >= PageTablePoolEntries ? 0 : PageTablePoolEntries - cpuUsed); + *gpu_available = static_cast(gpuUsed >= PageTablePoolEntries ? 0 : PageTablePoolEntries - gpuUsed); return 0; } @@ -421,6 +542,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); @@ -443,6 +565,16 @@ int APS5_VABI sceKernelBatchMap2(KernelBatchMapEntry* entries, int num_entries, } +bool GuestRangeMapped(std::uintptr_t start, std::uintptr_t end) { + for (auto cursor = start; cursor < end;) { + VirtualQueryInfo info{}; + if (sceKernelVirtualQuery(reinterpret_cast(cursor), 0, &info, sizeof(info)) != 0) return false; + if (info.end <= cursor) return false; + cursor = info.end; + } + return true; +} + namespace { bool PageRange(void* address, std::uint64_t length, std::uintptr_t& start, std::uintptr_t& end) { @@ -472,7 +604,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; @@ -480,7 +615,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; } @@ -529,19 +667,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; } } @@ -554,7 +699,11 @@ int LockHostPages(std::uintptr_t start, std::uintptr_t end) { } int UnlockHostPages(std::uintptr_t start, std::uintptr_t end) { - if (::munlock(reinterpret_cast(start), end - start) == 0) return 0; + if (!GuestRangeMapped(start, end)) return SCE_KERNEL_ERROR_ENOMEM; + 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"); @@ -581,4 +730,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/Jit.cpp b/core/libs/prx/libkernel/DirectMemory/Jit.cpp new file mode 100644 index 0000000000..fb3ad0405c --- /dev/null +++ b/core/libs/prx/libkernel/DirectMemory/Jit.cpp @@ -0,0 +1,219 @@ +#include "prx/libc/include/General.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#include +#include +#include +#else +#include +#include +#include +#endif + +namespace { + +constexpr int CpuRead = 0x1; +constexpr int CpuWrite = 0x2; +constexpr int CpuExecute = 0x4; +constexpr int CpuProtection = CpuRead | CpuWrite | CpuExecute; + +int Granted(int protection) { + return (protection & (CpuWrite | CpuExecute)) != 0 ? protection | CpuRead : protection; +} + +int KernelError(int error) { + switch (error) { + case EMFILE: + return SCE_KERNEL_ERROR_EMFILE; + case ENFILE: + return SCE_KERNEL_ERROR_ENFILE; + case ENOMEM: + case ENOSPC: + return SCE_KERNEL_ERROR_ENOMEM; + default: + return 0; + } +} + +int HostFailure(const char* operation, int error) { + if (const int kernel = KernelError(error)) return kernel; + throw std::system_error(error, std::generic_category(), operation); +} + +class JitObject { +public: +#ifdef _WIN32 + JitObject(std::size_t bytes, HANDLE section) : bytes(bytes), section(section) {} + ~JitObject() { + GuestArena::GuestArenaForgetSection_nid_postfix(section); + CloseHandle(section); + } +#else + explicit JitObject(std::size_t bytes) : bytes(bytes) {} +#endif + JitObject(const JitObject&) = delete; + JitObject& operator=(const JitObject&) = delete; + + std::size_t Bytes() const { return bytes; } + + HostSharedMemory Host(int descriptor) const { +#ifdef _WIN32 + static_cast(descriptor); + return section; +#else + return descriptor; +#endif + } + +private: + std::size_t bytes; +#ifdef _WIN32 + HANDLE section; +#endif +}; + +struct JitDescriptor { + std::shared_ptr object; + int maxProtection; +}; + +std::mutex g_jitLock; +std::map g_jitDescriptors; + +#ifdef _WIN32 +int WindowsFailure(const char* operation, DWORD error) { + if (error == ERROR_NOT_ENOUGH_MEMORY || error == ERROR_COMMITMENT_LIMIT || error == ERROR_DISK_FULL) return SCE_KERNEL_ERROR_ENOMEM; + throw std::system_error(static_cast(error), std::system_category(), operation); +} + +std::filesystem::path BackingPath() { + static std::atomic sequence{0}; + LARGE_INTEGER counter{}; + QueryPerformanceCounter(&counter); + const auto name = "anyps5-jit-" + std::to_string(GetCurrentProcessId()) + "-" + std::to_string(sequence++) + "-" + std::to_string(counter.QuadPart); + return std::filesystem::temp_directory_path() / name; +} + +int CreateBacking(std::size_t bytes, int* descriptor, std::shared_ptr* object) { + HANDLE file = CreateFileW(BackingPath().c_str(), GENERIC_READ | GENERIC_WRITE | GENERIC_EXECUTE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, CREATE_NEW, + FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE, nullptr); + if (file == INVALID_HANDLE_VALUE) throw std::system_error(static_cast(GetLastError()), std::system_category(), "create JIT shared memory file"); + const auto size = static_cast(bytes); + HANDLE section = CreateFileMappingW(file, nullptr, PAGE_EXECUTE_READWRITE, static_cast(size >> 32), static_cast(size), nullptr); + if (section == nullptr) { + const DWORD error = GetLastError(); + CloseHandle(file); + return WindowsFailure("create JIT shared memory section", error); + } + auto created = std::make_shared(bytes, section); + const int opened = _open_osfhandle(reinterpret_cast(file), _O_BINARY); + if (opened < 0) { + const int error = errno; + CloseHandle(file); + return HostFailure("open JIT shared memory descriptor", error); + } + *descriptor = opened; + *object = std::move(created); + return 0; +} + +int DuplicateDescriptor(int descriptor) { + return _dup(descriptor); +} +#else +int CreateBacking(std::size_t bytes, int* descriptor, std::shared_ptr* object) { + const int file = memfd_create("anyps5 jit shared memory", MFD_CLOEXEC); + if (file < 0) return HostFailure("create JIT shared memory", errno); + const bool representable = bytes <= static_cast(std::numeric_limits::max()); + if (!representable || ftruncate(file, static_cast(bytes)) != 0) { + const int error = !representable || errno == EFBIG ? ENOMEM : errno; + ::close(file); + return HostFailure("size JIT shared memory", error); + } + *descriptor = file; + *object = std::make_shared(bytes); + return 0; +} + +int DuplicateDescriptor(int descriptor) { + return fcntl(descriptor, F_DUPFD_CLOEXEC, 0); +} +#endif + +} + +void ForgetJitSharedMemory(int descriptor) { + std::lock_guard lock(g_jitLock); + g_jitDescriptors.erase(descriptor); +} + +void DuplicateJitSharedMemory(int from, int to) { + std::lock_guard lock(g_jitLock); + g_jitDescriptors.erase(to); + const auto source = g_jitDescriptors.find(from); + if (source != g_jitDescriptors.end()) g_jitDescriptors.emplace(to, source->second); +} + +extern "C" { + +int APS5_VABI sceKernelJitCreateSharedMemory(const char* name, std::size_t len, int maxProt, int* fdOut) { + static_cast(name); + if (fdOut == nullptr || len == 0 || (len & (PS5_PAGE_SIZE - 1)) != 0 || (maxProt & ~CpuProtection) != 0) return SCE_KERNEL_ERROR_EINVAL; + int descriptor = -1; + std::shared_ptr object; + if (const int result = CreateBacking(len, &descriptor, &object)) return result; + std::lock_guard lock(g_jitLock); + g_jitDescriptors.insert_or_assign(descriptor, JitDescriptor{std::move(object), maxProt}); + *fdOut = descriptor; + return 0; +} + +int APS5_VABI sceKernelJitCreateAliasOfSharedMemory(int fd, int maxProt, int* fdOut) { + if (fdOut == nullptr || (maxProt & ~CpuProtection) != 0) return SCE_KERNEL_ERROR_EINVAL; + std::lock_guard lock(g_jitLock); + const auto source = g_jitDescriptors.find(fd); + if (source == g_jitDescriptors.end()) return SCE_KERNEL_ERROR_EBADF; + if ((Granted(maxProt) & ~Granted(source->second.maxProtection)) != 0) return SCE_KERNEL_ERROR_EACCES; + const int alias = DuplicateDescriptor(fd); + if (alias < 0) return HostFailure("duplicate JIT shared memory descriptor", errno); + g_jitDescriptors.insert_or_assign(alias, JitDescriptor{source->second.object, maxProt}); + *fdOut = alias; + return 0; +} + +int APS5_VABI sceKernelJitMapSharedMemory(int fd, int prot, void** startOut) { + if (startOut == nullptr || (prot & ~CpuProtection) != 0) return SCE_KERNEL_ERROR_EINVAL; + std::lock_guard lock(g_jitLock); + const auto found = g_jitDescriptors.find(fd); + if (found == g_jitDescriptors.end()) return SCE_KERNEL_ERROR_EBADF; + if ((Granted(prot) & ~Granted(found->second.maxProtection)) != 0) return SCE_KERNEL_ERROR_EACCES; + const auto& object = *found->second.object; + void* address = *startOut; + std::uintptr_t arenaBase = 0; + std::size_t arenaBytes = 0; + GuestArena::GuestArenaRange_nid_postfix(&arenaBase, &arenaBytes); + if (arenaBytes != 0 && reinterpret_cast(address) - arenaBase >= arenaBytes) address = nullptr; + if (const int result = DoMapShared(&address, object.Bytes(), prot, object.Host(fd))) return result; + *startOut = address; + return 0; +} + +} diff --git a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp index 645098b713..54cdbaf12d 100644 --- a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp +++ b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp @@ -1,148 +1,350 @@ #include "prx/libkernel/DirectMemory/MemoryPool.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include +#include +#include +#include +#include #include #include +#include +#include +#include -static constexpr size_t NUM_PAGES = DIRECT_MEMORY_SIZE / PS5_PAGE_SIZE; +#include "prx/libkernel/KernelErrors.hpp" + +namespace { + +constexpr uint64_t kPage = static_cast(PS5_PAGE_SIZE); +constexpr uint64_t kPoolBytes = static_cast(DIRECT_MEMORY_SIZE); +constexpr size_t kNumPages = static_cast(kPoolBytes / kPage); + +static_assert(kPage != 0 && (kPage & (kPage - 1)) == 0, "PS5_PAGE_SIZE must be a power of two"); +static_assert(kPoolBytes % kPage == 0, "DIRECT_MEMORY_SIZE must be a multiple of PS5_PAGE_SIZE"); + +bool RoundUp(uint64_t v, uint64_t a, uint64_t& out) { + const uint64_t r = v % a; + if (r == 0) { + out = v; + return true; + } + if (v > UINT64_MAX - (a - r)) return false; + out = v + (a - r); + return true; +} + +bool NormalizeAlignment(size_t alignment, uint64_t& out) { + return RoundUp(std::max(static_cast(alignment), kPage), kPage, out); +} + +bool NormalizeLength(size_t len, uint64_t& out) { + uint64_t rounded; + if (!RoundUp(static_cast(len), kPage, rounded) || rounded > kPoolBytes) return false; + out = rounded; + return true; +} + +bool PageSpan(uint64_t start, size_t len, uint64_t& lo, uint64_t& hi) { + if (len == 0 || start >= kPoolBytes || len > kPoolBytes - start) return false; + lo = start & ~(kPage - 1); + hi = (start + len + kPage - 1) & ~(kPage - 1); + return true; +} struct PhysicalMemoryPool { static PhysicalMemoryPool& Instance() { - 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) { + if (!physOut) return SCE_KERNEL_ERROR_EFAULT; + if (len == 0) return SCE_KERNEL_ERROR_EINVAL; + + uint64_t align, alignedLen; + if (!NormalizeAlignment(alignment, align) || !NormalizeLength(len, alignedLen)) return SCE_KERNEL_ERROR_EAGAIN; + + const uint64_t start = static_cast(searchStart); + const uint64_t end = std::min(static_cast(searchEnd), kPoolBytes); + uint64_t cur; + if (start >= end || !RoundUp(start, align, cur)) return SCE_KERNEL_ERROR_EAGAIN; + std::lock_guard lock(_mutex); - size_t align = (alignment == 0) ? PS5_PAGE_SIZE : alignment; - uint64_t start = static_cast(searchStart); - uint64_t end = static_cast(searchEnd); - uint64_t cur = (start + align - 1) & ~(align - 1); - while (cur + len <= end && cur + len <= DIRECT_MEMORY_SIZE) { - if (_isFree(cur, len)) { - CreateDirectMemoryBacking(static_cast(cur), len, memoryType); - _mark(cur, len, true); - _blocks[cur] = {cur + len, memoryType}; + const size_t pageCount = static_cast(alignedLen / kPage); + + while (cur <= end && alignedLen <= end - cur) { + size_t lastUsed; + if (!_lastUsedPage(static_cast(cur / kPage), pageCount, lastUsed)) { + _commit(cur, alignedLen, memoryType); *physOut = static_cast(cur); return 0; } - cur += align; + if (!RoundUp((static_cast(lastUsed) + 1) * kPage, align, cur)) break; } return SCE_KERNEL_ERROR_EAGAIN; } - void Free(uint64_t start, size_t len) { - std::lock_guard lock(_mutex); - _free(start, len); - } + bool Free(uint64_t start, size_t len) { return _release(start, len, false); } - bool CheckedFree(uint64_t start, size_t len) { - std::lock_guard lock(_mutex); - if (start > DIRECT_MEMORY_SIZE || len > DIRECT_MEMORY_SIZE - start) return false; - const size_t first = start / PS5_PAGE_SIZE; - for (size_t i = 0; i < len / PS5_PAGE_SIZE; ++i) - if (!_used[first + i]) return false; - _free(start, len); + 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); - auto it = _blocks.upper_bound(offset); + BlockMap::const_iterator it = _blocks.upper_bound(offset); if (it != _blocks.begin() && std::prev(it)->second.end > offset) --it; else if (!findNext || it == _blocks.end()) return false; + *start = static_cast(it->first); *end = static_cast(it->second.end); *memoryType = it->second.type; return true; } - void Retype(uint64_t start, size_t len, int memoryType) { + bool Retype(uint64_t start, size_t len, int memoryType) { + uint64_t lo, hi; + if (!PageSpan(start, len, lo, hi)) return false; + std::lock_guard lock(_mutex); - const uint64_t end = start + len; - auto it = _blocks.upper_bound(start); - if (it != _blocks.begin() && std::prev(it)->second.end > start) --it; - while (it != _blocks.end() && it->first < end) { - const uint64_t blockStart = it->first; - const Block block = it->second; - it = _blocks.erase(it); - if (blockStart < start) _blocks[blockStart] = {start, block.type}; - _blocks[std::max(blockStart, start)] = {std::min(block.end, end), memoryType}; - if (block.end > end) it = _blocks.emplace(end, Block{block.end, block.type}).first; + const bool splitLo = _splitAt(lo); + try { + _splitAt(hi); + } catch (...) { + if (splitLo) _unsplitAt(lo); + throw; } + for (BlockMap::iterator it = _blocks.lower_bound(lo); it != _blocks.end() && it->first < hi; ++it) { + it->second.type = memoryType; + } + return true; + } + + 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 SCE_KERNEL_ERROR_EAGAIN; + + uint64_t cur; + 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); + uint64_t bestStart = 0; + uint64_t bestSize = 0; + + while (cur < limit) { + if (_used[cur / kPage]) { + cur += kPage; + continue; + } + uint64_t runEnd = cur; + while (runEnd < limit && !_used[runEnd / kPage]) runEnd += kPage; + + uint64_t alignedStart; + if (!RoundUp(cur, align, alignedStart)) break; + if (alignedStart < runEnd && runEnd - alignedStart > bestSize) { + bestStart = alignedStart; + bestSize = runEnd - alignedStart; + } + cur = runEnd; + } + + if (bestSize == 0) return SCE_KERNEL_ERROR_ENOMEM; + *startOut = static_cast(bestStart); + *sizeOut = static_cast(bestSize); + return 0; } size_t FreeRun(uint64_t offset, uint64_t limit) { + const uint64_t lim = std::min(limit, kPoolBytes); + if (offset >= lim) return 0; + std::lock_guard lock(_mutex); - uint64_t cur = offset & ~static_cast(PS5_PAGE_SIZE - 1); + uint64_t cur = offset & ~(kPage - 1); size_t run = 0; - while (cur + PS5_PAGE_SIZE <= limit && cur + PS5_PAGE_SIZE <= DIRECT_MEMORY_SIZE - && _isFree(cur, PS5_PAGE_SIZE)) { - cur += PS5_PAGE_SIZE; - run += PS5_PAGE_SIZE; + while (cur <= lim && kPage <= lim - cur && !_used[cur / kPage]) { + cur += kPage; + run += static_cast(kPage); } return run; } private: - void _free(uint64_t start, size_t len) { - ForgetDirectMemory(static_cast(start), len); - _mark(start, len, false); - const uint64_t end = start + len; - auto it = _blocks.upper_bound(start); - if (it != _blocks.begin() && std::prev(it)->second.end > start) --it; - while (it != _blocks.end() && it->first < end) { - const uint64_t blockStart = it->first; - const Block block = it->second; - it = _blocks.erase(it); - if (blockStart < start) _blocks[blockStart] = {start, block.type}; - if (block.end > end) it = _blocks.emplace(end, Block{block.end, block.type}).first; - } - } - - bool _isFree(uint64_t offset, size_t len) const { - size_t first = offset / PS5_PAGE_SIZE; - size_t count = len / PS5_PAGE_SIZE; - for (size_t i = 0; i < count; ++i) - if (_used[first + i]) return false; - return true; - } - - void _mark(uint64_t offset, size_t len, bool used) { - size_t first = offset / PS5_PAGE_SIZE; - size_t count = len / PS5_PAGE_SIZE; - for (size_t i = 0; i < count; ++i) - _used[first + i] = used; - } - struct Block { uint64_t end; int type; }; + typedef std::map BlockMap; + + bool _lastUsedPage(size_t first, size_t count, size_t& out) const { + for (size_t i = count; i-- > 0;) { + if (_used[first + i]) { + out = first + i; + return true; + } + } + return false; + } + + bool _allUsed(size_t first, size_t count) const { + for (size_t i = 0; i < count; ++i) { + if (!_used[first + i]) return false; + } + return true; + } + + void _mark(size_t first, size_t count, bool used) { + for (size_t i = 0; i < count; ++i) _used[first + i] = used; + } + + void _commit(uint64_t start, uint64_t len, int type) { + std::pair ins = _blocks.emplace(start, Block{start + len, type}); + if (!ins.second) throw std::logic_error("DirectMemory pool: block map and page flags disagree"); + + try { + CreateDirectMemoryBacking(static_cast(start), static_cast(len), type); + } catch (...) { + _blocks.erase(ins.first); + throw; + } + + _mark(static_cast(start / kPage), static_cast(len / kPage), true); + } + + bool _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); + if (requireAllocated && !_allUsed(first, count)) return false; + + const bool splitLo = _splitAt(lo); + bool splitHi = false; + try { + splitHi = _splitAt(hi); + ForgetDirectMemory(static_cast(lo), static_cast(hi - lo)); + } catch (...) { + if (splitHi) _unsplitAt(hi); + if (splitLo) _unsplitAt(lo); + throw; + } + + _blocks.erase(_blocks.lower_bound(lo), _blocks.lower_bound(hi)); + _mark(first, count, false); + return true; + } + + bool _splitAt(uint64_t at) { + BlockMap::iterator it = _blocks.upper_bound(at); + if (it == _blocks.begin()) return false; + --it; + if (it->first == at || it->second.end <= at) return false; + + const Block tail = {it->second.end, it->second.type}; + _blocks.emplace(at, tail); + it->second.end = at; + return true; + } + + void _unsplitAt(uint64_t at) noexcept { + BlockMap::iterator right = _blocks.find(at); + BlockMap::iterator left = std::prev(right); + left->second.end = right->second.end; + _blocks.erase(right); + } std::mutex _mutex; - bool _used[NUM_PAGES] = {}; - std::map _blocks; + std::condition_variable _claimFinished; + bool _used[kNumPages] = {}; + BlockMap _blocks; + std::map _claims; }; +} + int DirectMemoryAlloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int memoryType, int64_t* physOut) { - return PhysicalMemoryPool::Instance().Alloc(searchStart, searchEnd, len, alignment, memoryType, physOut); + try { + return PhysicalMemoryPool::Instance().Alloc(searchStart, searchEnd, len, alignment, memoryType, physOut); + } catch (const std::bad_alloc&) { + return SCE_KERNEL_ERROR_ENOMEM; + } } void DirectMemoryFree(int64_t start, size_t len) { - PhysicalMemoryPool::Instance().Free(static_cast(start), len); + (void)PhysicalMemoryPool::Instance().Free(static_cast(start), len); } bool DirectMemoryCheckedFree(int64_t start, size_t len) { 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); } void DirectMemoryRetype(int64_t start, size_t len, int memoryType) { - PhysicalMemoryPool::Instance().Retype(static_cast(start), len, memoryType); + (void)PhysicalMemoryPool::Instance().Retype(static_cast(start), len, memoryType); +} + +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); } size_t DirectMemoryFreeRun(uint64_t offset, uint64_t limit) { diff --git a/core/libs/prx/libkernel/DirectMemory/MemoryPoolExport.cpp b/core/libs/prx/libkernel/DirectMemory/MemoryPoolExport.cpp index 27076063a5..7f15f884a1 100644 --- a/core/libs/prx/libkernel/DirectMemory/MemoryPoolExport.cpp +++ b/core/libs/prx/libkernel/DirectMemory/MemoryPoolExport.cpp @@ -23,9 +23,9 @@ PoolState& Pool() { bool PageAligned(uint64_t v) { return (v & (PS5_PAGE_SIZE - 1)) == 0; } -int PoolCommit(void* addr, uint64_t len, int prot) { +int PoolCommit(void* addr, uint64_t len, int type, int prot) { if (!addr || len == 0 || !PageAligned(reinterpret_cast(addr)) || !PageAligned(len)) return SCE_KERNEL_ERROR_EINVAL; - const int ret = DoMprotect(addr, static_cast(len), prot); + const int ret = DoMtypeprotect(addr, static_cast(len), type, prot); if (ret == 0) { PoolState& pool = Pool(); std::lock_guard lock(pool.mutex); @@ -67,10 +67,10 @@ int APS5_VABI sceKernelMemoryPoolBatch(const KernelMemoryPoolBatchEntry* entries const KernelMemoryPoolBatchEntry& e = entries[done]; ret = Guarded([&] { switch (e.op) { - case 1: return PoolCommit(e.commit.addr, e.commit.len, e.commit.prot); + case 1: return PoolCommit(e.commit.addr, e.commit.len, e.commit.type, e.commit.prot); case 2: return PoolDecommit(e.decommit.addr, e.decommit.len); case 3: return DoMprotect(e.protect.addr, static_cast(e.protect.len), e.protect.prot); - case 4: return DoMprotect(e.type_protect.addr, static_cast(e.type_protect.len), e.type_protect.prot); + case 4: return DoMtypeprotect(e.type_protect.addr, static_cast(e.type_protect.len), e.type_protect.type, e.type_protect.prot); default: return SCE_KERNEL_ERROR_EINVAL; } }); @@ -81,9 +81,8 @@ int APS5_VABI sceKernelMemoryPoolBatch(const KernelMemoryPoolBatchEntry* entries } int APS5_VABI sceKernelMemoryPoolCommit(void* addr, size_t len, int type, int prot, int flags) { - (void)type; (void)flags; - return Guarded([&] { return PoolCommit(addr, len, prot); }); + return Guarded([&] { return PoolCommit(addr, len, type, prot); }); } int APS5_VABI sceKernelMemoryPoolDecommit(void* addr, size_t len, int flags) { diff --git a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp index 12b4b4d970..7c69af40fa 100644 --- a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp @@ -1,24 +1,85 @@ #include "DirectMemory.hpp" #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__) +#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. 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; constexpr int GuestSyncInvalidate = 0x2; +constexpr int GuestAdviceCore = 9; +constexpr int GuestAdviceProtect = 10; +constexpr int GuestLockCurrent = 0x1; +constexpr int GuestLockFuture = 0x2; +constexpr std::uintptr_t GuestUserAddressEnd = 0x800000000000; +constexpr int GuestAlignedSuper = 0x1000000; +constexpr std::size_t SuperpageSize = std::size_t{1} << 21; + +#if defined(__linux__) +class ScopedDescriptor { +public: + explicit ScopedDescriptor(int value) noexcept : descriptor(fcntl(value, F_DUPFD_CLOEXEC, 0)), error(descriptor < 0 ? errno : 0) {} + ~ScopedDescriptor() { + const int savedError = errno; + if (descriptor >= 0) close(descriptor); + errno = savedError; + } + ScopedDescriptor(const ScopedDescriptor&) = delete; + ScopedDescriptor& operator=(const ScopedDescriptor&) = delete; + int Get() const noexcept { return descriptor; } + int Error() const noexcept { return error; } +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) { constexpr auto mask = PS5_PAGE_SIZE - 1; @@ -37,15 +98,49 @@ bool Mapped(std::uintptr_t address) { return sceKernelVirtualQuery(reinterpret_cast(address), 0, &info, sizeof(info)) == 0; } -bool RangeMapped(std::uintptr_t start, std::uintptr_t end) { - for (auto cursor = start; cursor < end;) { - VirtualQueryInfo info{}; - if (sceKernelVirtualQuery(reinterpret_cast(cursor), 0, &info, sizeof(info)) != 0) return false; - if (info.end <= cursor) return false; - cursor = info.end; +#ifdef _WIN32 +int DescriptorAccess(HANDLE handle, int descriptor) noexcept { + try { + return WindowsDescriptorAccess_nid_no_patch(handle, descriptor, "mmap"); + } catch (const std::exception&) { + return -1; } - return true; } + +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" { @@ -59,17 +154,92 @@ 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); - if (flags != (GuestPrivate | GuestAnonymous)) + if ((flags & (GuestMapExclusive | GuestMapFixed)) == GuestMapExclusive) return failed(GuestInvalid); + const int mappingFlags = flags & ~GuestAlignedSuper; + const bool fixedAnonymous = (mappingFlags & ~(GuestMapFixed | GuestMapExclusive)) == (GuestPrivate | GuestAnonymous) + && (mappingFlags & GuestMapFixed) != 0; + if (mappingFlags == (GuestPrivate | GuestAnonymous) || fixedAnonymous) { + if (descriptor != -1 || offset != 0) return failed(GuestInvalid); + 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, 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&) { + return failed(GuestNoMemory); + } + } + const int kind = mappingFlags & (GuestShared | GuestPrivate); + int placement = mappingFlags & (GuestMapFixed | GuestMapNoOverwrite | GuestMapNoCoalesce); + if ((mappingFlags & GuestAnonymous) != 0 || + (kind != GuestShared && kind != GuestPrivate) || + (mappingFlags & ~(GuestShared | GuestPrivate | GuestMapFixed | GuestMapNoOverwrite | GuestMapNoCoalesce)) != 0) return failed(GuestNotSupported); - if (descriptor != -1 || offset != 0) + 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); - // A non-fixed address is a hint; the kernel may choose another address. - (void)address; +#if defined(__linux__) + const ScopedDescriptor pinnedDescriptor(descriptor); + if (pinnedDescriptor.Get() < 0) return failed(pinnedDescriptor.Error()); + const int mappingDescriptor = pinnedDescriptor.Get(); + const int descriptorFlags = fcntl(mappingDescriptor, F_GETFL); + if (descriptorFlags < 0) return failed(errno); + 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) && !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); +#endif + const std::size_t alignment = (flags & GuestAlignedSuper) != 0 ? SuperpageSize : PS5_PAGE_SIZE; void* mapped = nullptr; try { - const auto result = DoMapAnon(&mapped, rounded, protection, 0); - if (result != 0) return failed(GuestNoMemory); + 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); + } catch (const std::system_error& error) { + const int hostError = error.code().value(); + if (hostError == ENOTSUP || hostError == EOPNOTSUPP) return failed(GuestNotSupported); + if (hostError == EACCES || hostError == EPERM) return failed(GuestAccess); + if (hostError == EBADF) return failed(GuestBadDescriptor); + return failed(hostError > 0 && hostError <= 34 ? hostError : GuestIoError); } catch (const std::bad_alloc&) { return failed(GuestNoMemory); } catch (const std::exception&) { @@ -89,7 +259,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); @@ -113,6 +283,9 @@ int APS5_VABI mprotect_nid_postfix(void* address, std::size_t length, int protec return 0; } catch (const std::bad_alloc&) { return failed(GuestNoMemory); + } catch (const std::system_error& error) { + const int hostError = error.code().value(); + return failed(hostError == EACCES || hostError == EPERM ? GuestAccess : GuestInvalid); } catch (const std::exception&) { return failed(GuestInvalid); } @@ -132,6 +305,22 @@ int APS5_VABI munlock_nid_postfix(const void* address, std::size_t length) { return -1; } +int APS5_VABI mlockall_nid_postfix(int flags) { + if (flags == 0 || (flags & ~(GuestLockCurrent | GuestLockFuture)) != 0) { + SetError(GuestInvalid); + return -1; + } + if ((flags & GuestLockFuture) != 0) throw std::runtime_error("mlockall: MCL_FUTURE is not implemented: later mappings are not locked"); + throw std::runtime_error("mlockall: MCL_CURRENT is not implemented: the process also holds host stacks, heaps, modules and driver mappings"); +} + +int APS5_VABI 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); @@ -144,9 +333,43 @@ int APS5_VABI msync_nid_postfix(void* address, std::size_t length, int flags) { const auto end = start + size; if (end < start) return failed(GuestInvalid); if ((flags & (GuestSyncAsync | GuestSyncInvalidate)) == (GuestSyncAsync | GuestSyncInvalidate)) return failed(GuestInvalid); - if (start == end ? !Mapped(start) : !RangeMapped(start, end)) return failed(GuestNoMemory); + if (start == end ? !Mapped(start) : !GuestRangeMapped(start, end)) return failed(GuestNoMemory); if ((flags & GuestSyncInvalidate) != 0) throw std::runtime_error("msync: MS_INVALIDATE is not implemented"); return 0; } +int APS5_VABI madvise_nid_postfix(void* address, std::size_t length, int advice) { + if (advice == GuestAdviceProtect) return 0; + const auto start = reinterpret_cast(address); + if (advice < 0 || advice > GuestAdviceCore || start > GuestUserAddressEnd || length > GuestUserAddressEnd - start) { + SetError(GuestInvalid); + return -1; + } + 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 b8d41b7627..ec37c3ac94 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.cpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.cpp @@ -1,5 +1,9 @@ #include "Equeue.hpp" #include "prx/libkernel/Time/include/Time.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libkernel/Socket/include/SocketRuntime.hpp" +#include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/File/include/HostStreams.hpp" #include #include @@ -14,7 +18,7 @@ static uint64_t g_nextEqueue = 1; extern "C" { -KernelEqueuePrivate::KernelEqueuePrivate(KernelEqueue handle) : m_handle(handle) {} +KernelEqueuePrivate::KernelEqueuePrivate(KernelEqueue handle, bool reportOncePerWait) : m_handle(handle), m_reportOncePerWait(reportOncePerWait) {} KernelEqueuePrivate::~KernelEqueuePrivate() { Close(); @@ -42,11 +46,12 @@ void KernelEqueuePrivate::Close() { } m_events.clear(); m_cond.NotifyAll(); + WakePollers(); } 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) @@ -62,7 +67,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); } } @@ -75,22 +80,73 @@ bool KernelEqueuePrivate::NextTimerWaitMicros(uint64_t nowNs, uint32_t* out) con return true; } -int KernelEqueuePrivate::GetTriggeredEvents(KernelEvent* ev, int num) { - std::unique_lock lock(m_mutex); - if (m_closed) { - return SCE_KERNEL_ERROR_EBADF; +bool KernelEqueuePrivate::PollEvents() { + bool polled = false; + for (auto& ev : m_events) { + if (ev.enabled && ev.filter.pollFunc != nullptr) { + ev.triggered = ev.filter.pollFunc(&ev); + polled = true; + } + } + return polled; +} + +void KernelEqueuePrivate::WakePollers() { + for (const auto waker : m_pollers) { + GuestSockets::Wake(waker); + } +} + +void KernelEqueuePrivate::WaitForDescriptors(std::unique_lock& lock, std::uint64_t deadlineNanos) { + std::vector interests; + std::vector> held; + for (const auto& e : m_events) { + if (!e.enabled || e.filter.pollFunc == nullptr) { + continue; + } + if (e.filter.waitDescriptor >= 0) { + interests.push_back({e.filter.waitDescriptor, false}); + held.push_back(e.filter.owner); + } else { + interests.push_back({static_cast(e.event.ident), e.event.filter == EVFILT_WRITE}); + } + } + const auto waker = GuestSockets::CurrentWaker(); + m_pollers.push_back(waker); + lock.unlock(); + const bool waited = GuestSockets::WaitAny(interests, deadlineNanos); + lock.lock(); + m_pollers.erase(std::find(m_pollers.begin(), m_pollers.end(), waker)); + if (!waited) { + m_cond.WaitUntil(lock, deadlineNanos); + } +} + +int KernelEqueuePrivate::CollectTriggered(KernelEvent* ev, int num) { + if (m_reportOncePerWait) { + return CollectOncePerWait(ev, num); } - TriggerExpiredTimers(MonotonicNs()); int ret = 0; - for (auto it = m_events.begin(); it != m_events.end();) { + 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) { + while (e.triggered && ret < num) { ev[ret++] = e.event; if ((e.event.flags & EV_ONESHOT) != 0) { 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) { @@ -98,69 +154,105 @@ int KernelEqueuePrivate::GetTriggeredEvents(KernelEvent* ev, int num) { e.event.fflags = 0; e.event.data = 0; } - if (!e.pendingEvents.empty()) { - e.event = e.pendingEvents.front(); - e.pendingEvents.pop_front(); - e.triggered = true; - } - if (ret >= num) { + if (e.pendingEvents.empty()) { break; } + e.event = e.pendingEvents.front(); + e.pendingEvents.pop_front(); + e.triggered = true; } it = erase ? m_events.erase(it) : std::next(it); - if (ret >= num) { - break; - } } return ret; } +int KernelEqueuePrivate::CollectOncePerWait(KernelEvent* ev, int num) { + int ret = 0; + std::list requeued; + for (auto it = m_events.begin(); it != m_events.end() && ret < num;) { + auto& e = *it; + if (!e.triggered || !e.enabled) { + ++it; + continue; + } + ev[ret++] = e.event; + if ((e.event.flags & EV_ONESHOT) != 0) { + if (e.filter.deleteEventFunc != nullptr) { + auto owner = e.filter.owner; + e.filter.deleteEventFunc(m_handle, &e); + } + it = m_events.erase(it); + continue; + } + if ((e.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; + e.event.fflags = 0; + e.event.data = 0; + } + const auto next = std::next(it); + if (e.triggered) { + requeued.splice(requeued.end(), m_events, it); + } + it = next; + } + m_events.splice(m_events.end(), requeued); + return ret; +} + +int KernelEqueuePrivate::GetTriggeredEvents(KernelEvent* ev, int num) { + std::unique_lock lock(m_mutex); + if (m_closed) { + return SCE_KERNEL_ERROR_EBADF; + } + TriggerExpiredTimers(MonotonicNs()); + PollEvents(); + return CollectTriggered(ev, num); +} + +bool KernelEqueuePrivate::HasTriggeredEvents() { + std::unique_lock lock(m_mutex); + if (m_closed) { + return false; + } + TriggerExpiredTimers(MonotonicNs()); + PollEvents(); + return std::any_of(m_events.begin(), m_events.end(), [](const KernelEqueueEvent& event) { return event.enabled && event.triggered; }); +} + int KernelEqueuePrivate::WaitForEvents(KernelEvent* ev, int num, uint32_t micros) { + return WaitForEventsUntil(ev, num, micros == 0 ? 0 : TimedWait::DeadlineNanos(micros)); +} + +int KernelEqueuePrivate::WaitForEventsUntil(KernelEvent* ev, int num, std::uint64_t deadline) { std::unique_lock lock(m_mutex); if (m_closed) { return SCE_KERNEL_ERROR_EBADF; } - const std::uint64_t deadline = TimedWait::DeadlineNanos(micros); for (;;) { TriggerExpiredTimers(MonotonicNs()); - int ret = 0; - for (auto it = m_events.begin(); it != m_events.end() && ret < num;) { - auto& e = *it; - bool erase = false; - while (e.triggered) { - ev[ret++] = e.event; - if ((e.event.flags & EV_ONESHOT) != 0) { - erase = true; - break; - } - if (e.filter.resetFunc != nullptr) { - e.filter.resetFunc(&e); - } else if ((e.event.flags & EV_CLEAR) != 0) { - e.triggered = false; - e.event.fflags = 0; - e.event.data = 0; - } - if (!e.pendingEvents.empty()) { - e.event = e.pendingEvents.front(); - e.pendingEvents.pop_front(); - e.triggered = true; - } - if (ret >= num) { - break; - } - } - it = erase ? m_events.erase(it) : std::next(it); - } + const bool polling = PollEvents(); + const int ret = CollectTriggered(ev, num); if (ret != 0) { return ret; } if (m_closed) { return SCE_KERNEL_ERROR_EBADF; } - if (micros == 0) { + if (deadline == 0) { uint32_t timerWait = 0; - if (NextTimerWaitMicros(MonotonicNs(), &timerWait)) { - m_cond.WaitUntil(lock, TimedWait::NowNanos() + static_cast(timerWait) * 1000ULL); + const bool hasTimer = NextTimerWaitMicros(MonotonicNs(), &timerWait); + const std::uint64_t wake = hasTimer ? TimedWait::NowNanos() + static_cast(timerWait) * 1000ULL : 0; + if (polling) { + WaitForDescriptors(lock, wake); + } else if (hasTimer) { + m_cond.WaitUntil(lock, wake); } else { m_cond.Wait(lock); } @@ -172,7 +264,12 @@ int KernelEqueuePrivate::WaitForEvents(KernelEvent* ev, int num, uint32_t micros return 0; } const std::uint64_t timerDeadline = now + static_cast(timerWait) * 1000ULL; - m_cond.WaitUntil(lock, hasTimer ? std::min(deadline, timerDeadline) : deadline); + const std::uint64_t wake = hasTimer ? std::min(deadline, timerDeadline) : deadline; + if (polling) { + WaitForDescriptors(lock, wake); + } else { + m_cond.WaitUntil(lock, wake); + } } } } @@ -201,6 +298,7 @@ int KernelEqueuePrivate::AddEvent(const KernelEqueueEvent& event) { m_events.push_back(event); } m_cond.NotifyOne(); + WakePollers(); return EQUEUE_OK; } @@ -217,12 +315,41 @@ 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(); + return EQUEUE_OK; +} + +bool KernelEqueuePrivate::HasEvent(uintptr_t ident, int16_t filter) { + std::unique_lock lock(m_mutex); + return std::any_of(m_events.begin(), m_events.end(), [ident, filter](const auto& e) { + return e.event.ident == ident && e.event.filter == filter; + }); +} + +int KernelEqueuePrivate::EditEvent(uintptr_t ident, int16_t filter, const std::function& edit) { + std::unique_lock lock(m_mutex); + if (m_closed) { + return SCE_KERNEL_ERROR_EBADF; + } + auto it = std::find_if(m_events.begin(), m_events.end(), + [ident, filter](const auto& e) { + return e.event.ident == ident && e.event.filter == filter; + } + ); + if (it == m_events.end()) { + return SCE_KERNEL_ERROR_ENOENT; + } + edit(*it); + m_cond.NotifyOne(); + WakePollers(); return EQUEUE_OK; } @@ -244,9 +371,30 @@ int KernelEqueuePrivate::DeleteEvent(uintptr_t ident, int16_t filter) { it->filter.deleteEventFunc(m_handle, &*it); } m_events.erase(it); + WakePollers(); return EQUEUE_OK; } +void KernelEqueuePrivate::RemoveDescriptorEvents(uintptr_t ident) { + std::unique_lock lock(m_mutex); + bool removed = false; + for (auto it = m_events.begin(); it != m_events.end();) { + if (it->event.ident != ident || (it->event.filter != EVFILT_READ && it->event.filter != EVFILT_WRITE && it->event.filter != EVFILT_VNODE)) { + ++it; + continue; + } + if (it->filter.deleteEventFunc != nullptr) { + auto owner = it->filter.owner; + it->filter.deleteEventFunc(m_handle, &*it); + } + it = m_events.erase(it); + removed = true; + } + if (removed) { + WakePollers(); + } +} + KernelEqueueRef EqueuePin_nid_postfix(KernelEqueue eq) { if (eq == 0) { return {}; @@ -481,4 +629,76 @@ int APS5_VABI sceKernelDeleteAmprSystemEvent(KernelEqueue eq, int id) { return sceKernelDeleteAmprEvent(eq, id); } +static int AddDescriptorEvent(KernelEqueue eq, int fd, std::size_t size, void* udata, int16_t filter) { + const bool isSocket = GuestSockets::IsOpen(fd); + 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 && !isPipe) { + NotImplemented_nid_no_patch(filter == EVFILT_READ ? "sceKernelAddReadEvent: low watermark" : "sceKernelAddWriteEvent: low watermark"); + } + if (!isSocket && !isPipe) { + return SCE_KERNEL_ERROR_EBADF; + } + return EqueueAddEvent_nid_postfix(eq, EqueueDescriptorEvent(fd, filter, EV_ADD, udata)); +} + +int APS5_VABI sceKernelAddReadEvent(KernelEqueue eq, int fd, std::size_t size, void* udata) { + return AddDescriptorEvent(eq, fd, size, udata, EVFILT_READ); +} + +int APS5_VABI sceKernelDeleteReadEvent(KernelEqueue eq, int fd) { + return EqueueDeleteEvent_nid_postfix(eq, static_cast(fd), EVFILT_READ); +} + +int APS5_VABI sceKernelAddWriteEvent(KernelEqueue eq, int fd, std::size_t size, void* udata) { + return AddDescriptorEvent(eq, fd, size, udata, EVFILT_WRITE); +} + +int APS5_VABI sceKernelDeleteWriteEvent(KernelEqueue eq, int fd) { + return EqueueDeleteEvent_nid_postfix(eq, static_cast(fd), EVFILT_WRITE); +} + +} + +KernelEqueueEvent EqueueDescriptorEvent(int descriptor, int16_t filter, uint16_t flags, void* udata) { + KernelEqueueEvent event{}; + event.event.ident = static_cast(descriptor); + event.event.filter = filter; + event.event.flags = flags; + event.event.udata = udata; + event.filter.pollFunc = [](KernelEqueueEvent* e) { + std::int64_t data = 0; + bool eof = false; + if (!GuestSockets::Ready(static_cast(e->event.ident), e->event.filter == EVFILT_WRITE, &data, &eof)) { + return false; + } + 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; + }; + event.filter.resetFunc = [](KernelEqueueEvent* e) { + e->triggered = false; + }; + return event; +} + +void EqueueDescriptorClosed(int descriptor) { + KqueueDescriptorClosed(descriptor); + std::vector owners; + { + std::unique_lock lock(g_equeueMutex); + for (const auto& [handle, owner] : g_equeues) { + owners.push_back(owner); + } + } + for (const auto& owner : owners) { + owner->RemoveDescriptorEvents(static_cast(descriptor)); + } } diff --git a/core/libs/prx/libkernel/Equeue/Equeue.hpp b/core/libs/prx/libkernel/Equeue/Equeue.hpp index dd3a477064..860dabf3e5 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.hpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.hpp @@ -3,17 +3,23 @@ #include #include +#include #include #include #include +#include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" +static constexpr int16_t EVFILT_READ = -1; +static constexpr int16_t EVFILT_WRITE = -2; +static constexpr int16_t EVFILT_VNODE = -4; static constexpr int16_t EVFILT_TIMER = -7; static constexpr int16_t EVFILT_USER = -11; static constexpr int16_t EVFILT_VIDEO_OUT = -13; @@ -21,9 +27,17 @@ static constexpr int16_t EVFILT_HRTIMER = -15; static constexpr int16_t EVFILT_AMPR = -25; static constexpr uint16_t EV_ADD = 0x0001; +static constexpr uint16_t EV_DELETE = 0x0002; +static constexpr uint16_t EV_ENABLE = 0x0004; +static constexpr uint16_t EV_DISABLE = 0x0008; static constexpr uint16_t EV_ONESHOT = 0x0010; static constexpr uint16_t EV_CLEAR = 0x0020; +static constexpr uint16_t EV_RECEIPT = 0x0040; +static constexpr uint16_t EV_DISPATCH = 0x0080; +static constexpr uint16_t EV_FORCEONESHOT = 0x0100; +static constexpr uint16_t EV_SYSFLAGS = 0xF000; static constexpr uint16_t EV_ERROR = 0x4000; +static constexpr uint16_t EV_EOF = 0x8000; static constexpr int EQUEUE_OK = 0; @@ -32,6 +46,7 @@ struct KernelEqueueEvent; using EqueueTriggerFunc = void (*)(KernelEqueueEvent* event, void* triggerData); using EqueueResetFunc = void (*)(KernelEqueueEvent* event); using EqueueDeleteFunc = void (*)(KernelEqueue eq, KernelEqueueEvent* event); +using EqueuePollFunc = bool (*)(KernelEqueueEvent* event); struct KernelFilter { void* data = nullptr; @@ -39,12 +54,16 @@ struct KernelFilter { EqueueTriggerFunc triggerFunc = nullptr; EqueueResetFunc resetFunc = nullptr; EqueueDeleteFunc deleteEventFunc = nullptr; + EqueuePollFunc pollFunc = nullptr; + int waitDescriptor = -1; }; 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; @@ -52,7 +71,7 @@ struct KernelEqueueEvent { class KernelEqueuePrivate { public: - explicit KernelEqueuePrivate(KernelEqueue handle); + explicit KernelEqueuePrivate(KernelEqueue handle, bool reportOncePerWait = false); ~KernelEqueuePrivate(); KernelEqueuePrivate(const KernelEqueuePrivate&) = delete; @@ -64,24 +83,44 @@ public: int AddEvent(const KernelEqueueEvent& event); int TriggerEvent(uintptr_t ident, int16_t filter, void* triggerData); int DeleteEvent(uintptr_t ident, int16_t filter); + bool HasEvent(uintptr_t ident, int16_t filter); + int EditEvent(uintptr_t ident, int16_t filter, const std::function& edit); int GetTriggeredEvents(KernelEvent* ev, int num); + bool HasTriggeredEvents(); int WaitForEvents(KernelEvent* ev, int num, uint32_t micros); + int WaitForEventsUntil(KernelEvent* ev, int num, std::uint64_t deadlineNanos); + void RemoveDescriptorEvents(uintptr_t ident); void Close(); static uint64_t MonotonicNs(); private: void TriggerExpiredTimers(uint64_t nowNs); bool NextTimerWaitMicros(uint64_t nowNs, uint32_t* out) const; + int CollectTriggered(KernelEvent* ev, int num); + int CollectOncePerWait(KernelEvent* ev, int num); + bool PollEvents(); + void WakePollers(); + void WaitForDescriptors(std::unique_lock& lock, std::uint64_t deadlineNanos); std::list m_events; + std::vector m_pollers; std::mutex m_mutex; TimedWait::Condition m_cond; std::string m_name; KernelEqueue m_handle; + bool m_reportOncePerWait; bool m_closed = false; }; using KernelEqueueRef = std::shared_ptr; +void EqueueDescriptorClosed(int descriptor); +void KqueueDescriptorClosed(int descriptor); +bool IsKqueueDescriptor(int descriptor); +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); +int AddVnodeEvent(const KernelEqueueRef& queue, int descriptor, uint32_t fflags, uint16_t flags, void* udata); extern "C" { KernelEqueueRef EqueuePin_nid_postfix(KernelEqueue eq); diff --git a/core/libs/prx/libkernel/Equeue/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 2cce0e556b..e72bfe3900 100644 --- a/core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp +++ b/core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp @@ -4,6 +4,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/Equeue/Equeue.hpp" static const KernelEvent& requireEvent(const KernelEvent* ev, const char* caller) { if (ev == nullptr) { @@ -19,9 +20,8 @@ intptr_t APS5_VABI sceKernelGetEventData(const KernelEvent* ev) { } int APS5_VABI sceKernelGetEventError(const KernelEvent* ev) { - (void)ev; - NotImplemented_nid_no_patch(__func__); - return 0; + const KernelEvent& event = requireEvent(ev, __func__); + return (event.flags & EV_ERROR) != 0 ? static_cast(event.data) : 0; } intptr_t APS5_VABI sceKernelGetEventFflags(const KernelEvent* ev) { diff --git a/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp b/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp new file mode 100644 index 0000000000..04fe98539f --- /dev/null +++ b/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp @@ -0,0 +1,785 @@ +#include "prx/libkernel/Equeue/Equeue.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/Socket/include/SocketRuntime.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#include "prx/libkernel/File/include/DirectoryDescriptor.hpp" +#elif defined(__APPLE__) +#include +#include +#include +#include +#else +#include +#include +#include +#include +#include +#include +#include +#include +#endif + +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 { +public: + VnodeWatch(const KernelEqueueRef& owner, std::uintptr_t descriptor, std::uint32_t notes) : queue(owner), ident(descriptor), interest(notes) { +#ifdef _WIN32 + stopEvent = ::CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (stopEvent == nullptr) { + throw std::runtime_error("EVFILT_VNODE: CreateEvent failed, error=" + std::to_string(::GetLastError())); + } +#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) { + throw std::runtime_error("EVFILT_VNODE: eventfd failed, errno=" + std::to_string(errno)); + } +#endif + } + + ~VnodeWatch() { +#ifdef _WIN32 + ::CloseHandle(stopEvent); +#elif defined(__APPLE__) + ::close(stopEvent); + ::close(stopSignal); +#else + ::close(stopEvent); +#endif + } + + VnodeWatch(const VnodeWatch&) = delete; + VnodeWatch& operator=(const VnodeWatch&) = delete; + + void Stop() { + 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)); + static_cast(written); +#endif + } + + void Deliver(std::uint32_t hint); + + std::uint32_t Interest() const { return interest.load(); } + void SetInterest(std::uint32_t notes) { interest.store(notes); } + +#ifdef _WIN32 + HANDLE StopEvent() const { return stopEvent; } +#else + int StopEvent() const { return stopEvent; } +#endif + +private: + std::weak_ptr queue; + std::uintptr_t ident; + std::atomic interest; + std::atomic stopped{false}; +#ifdef _WIN32 + HANDLE stopEvent = nullptr; +#elif defined(__APPLE__) + int stopEvent = -1; + int stopSignal = -1; +#else + int stopEvent = -1; +#endif +}; + +struct VnodeHint { + const VnodeWatch* watch; + std::uint32_t hint; +}; + +void VnodeWatch::Deliver(std::uint32_t hint) { + if (hint == 0 || stopped.load()) { + return; + } + const auto owner = queue.lock(); + if (!owner) { + return; + } + VnodeHint message{this, hint}; + owner->TriggerEvent(ident, EVFILT_VNODE, &message); +} + +#ifdef _WIN32 + +constexpr DWORD VNODE_POLL_MILLISECONDS = 50; +constexpr DWORD PARENT_CHANGES = FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_ATTRIBUTES | + FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_CREATION | FILE_NOTIFY_CHANGE_SECURITY; + +struct Snapshot { + std::uint64_t size = 0; + std::uint32_t links = 0; + std::uint32_t attributes = 0; + std::int64_t creation = 0; + std::int64_t lastWrite = 0; + std::int64_t change = 0; + bool deletePending = false; + std::wstring path; +}; + +bool TakeSnapshot(HANDLE handle, Snapshot& snapshot) { + FILE_BASIC_INFO basic{}; + FILE_STANDARD_INFO standard{}; + if (!::GetFileInformationByHandleEx(handle, FileBasicInfo, &basic, sizeof(basic)) || + !::GetFileInformationByHandleEx(handle, FileStandardInfo, &standard, sizeof(standard))) { + return false; + } + snapshot.size = static_cast(standard.EndOfFile.QuadPart); + snapshot.links = standard.NumberOfLinks; + snapshot.deletePending = standard.DeletePending != FALSE; + snapshot.attributes = basic.FileAttributes; + snapshot.creation = basic.CreationTime.QuadPart; + snapshot.lastWrite = basic.LastWriteTime.QuadPart; + snapshot.change = basic.ChangeTime.QuadPart; + std::wstring path(MAX_PATH, L'\0'); + auto length = ::GetFinalPathNameByHandleW(handle, path.data(), static_cast(path.size()), FILE_NAME_NORMALIZED); + if (length >= path.size()) { + path.resize(length); + length = ::GetFinalPathNameByHandleW(handle, path.data(), length, FILE_NAME_NORMALIZED); + } + if (length != 0 && length < path.size()) { + path.resize(length); + snapshot.path = std::move(path); + } + return true; +} + +bool IsDeleted(const Snapshot& snapshot) { + return snapshot.deletePending || snapshot.links == 0; +} + +std::vector SnapshotHints(const Snapshot& before, const Snapshot& after, bool directory) { + std::vector hints; + if (!directory) { + if (after.size > before.size) { + hints.push_back(NOTE_WRITE | NOTE_EXTEND); + } else if (after.size == before.size && after.lastWrite != before.lastWrite) { + hints.push_back(NOTE_WRITE); + } + if (after.links > before.links) { + hints.push_back(NOTE_LINK); + } + } + if (after.size < before.size || after.attributes != before.attributes || after.creation != before.creation) { + hints.push_back(NOTE_ATTRIB); + } else if (!directory && hints.empty() && after.change != before.change) { + hints.push_back(NOTE_ATTRIB); + } + if ((after.deletePending && !before.deletePending) || after.links < before.links) { + hints.push_back(NOTE_DELETE); + } + if (!after.path.empty() && after.path != before.path && !IsDeleted(after)) { + hints.push_back(NOTE_RENAME); + } + 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) { + return INVALID_HANDLE_VALUE; + } + const auto parent = path.substr(0, separator + 1); + const HANDLE handle = ::FindFirstChangeNotificationW(parent.c_str(), FALSE, PARENT_CHANGES); + const DWORD error = handle == INVALID_HANDLE_VALUE ? ::GetLastError() : ERROR_SUCCESS; + if (handle == INVALID_HANDLE_VALUE && error != ERROR_INVALID_FUNCTION) { + throw std::runtime_error(changeNotificationFailure("parent", parent, error)); + } + return handle; +} + +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(changeNotificationFailure(watcher, path, error)); + } + return handle; +} + +struct HostWatch { + HANDLE target = INVALID_HANDLE_VALUE; + HANDLE parent = INVALID_HANDLE_VALUE; + HANDLE entries = INVALID_HANDLE_VALUE; + HANDLE subdirectories = INVALID_HANDLE_VALUE; + bool directory = false; + Snapshot last; +}; + +void CloseChange(HANDLE& handle) { + if (handle != INVALID_HANDLE_VALUE) { + ::FindCloseChangeNotification(handle); + handle = INVALID_HANDLE_VALUE; + } +} + +void CloseHostWatch(HostWatch& host) { + CloseChange(host.parent); + CloseChange(host.entries); + CloseChange(host.subdirectories); + if (host.target != INVALID_HANDLE_VALUE) { + ::CloseHandle(host.target); + host.target = INVALID_HANDLE_VALUE; + } +} + +bool Signaled(HANDLE handle) { + return handle != INVALID_HANDLE_VALUE && ::WaitForSingleObject(handle, 0) == WAIT_OBJECT_0; +} + +void RunHostWatch(std::shared_ptr watch, HostWatch host) { + for (;;) { + std::vector handles{watch->StopEvent()}; + for (const HANDLE handle : {host.parent, host.entries, host.subdirectories}) { + if (handle != INVALID_HANDLE_VALUE) { + handles.push_back(handle); + } + } + const DWORD result = ::WaitForMultipleObjects(static_cast(handles.size()), handles.data(), FALSE, VNODE_POLL_MILLISECONDS); + if (result == WAIT_OBJECT_0) { + break; + } + if (result == WAIT_FAILED) { + CloseHostWatch(host); + throw std::runtime_error("EVFILT_VNODE: WaitForMultipleObjects failed, error=" + std::to_string(::GetLastError())); + } + std::vector hints; + if (Signaled(host.entries)) { + hints.push_back(NOTE_WRITE); + ::FindNextChangeNotification(host.entries); + } + if (Signaled(host.subdirectories)) { + hints.push_back(NOTE_WRITE | NOTE_LINK); + ::FindNextChangeNotification(host.subdirectories); + } + if (Signaled(host.parent)) { + ::FindNextChangeNotification(host.parent); + } + Snapshot current = host.last; + if (TakeSnapshot(host.target, current)) { + for (const auto hint : SnapshotHints(host.last, current, host.directory)) { + hints.push_back(hint); + } + if (current.path != host.last.path) { + CloseChange(host.parent); + if (!IsDeleted(current)) { + host.parent = WatchParent(current.path); + } + } + host.last = std::move(current); + } + for (const auto hint : hints) { + watch->Deliver(hint); + } + } + CloseHostWatch(host); +} + +int OpenHostWatch(int descriptor, std::uint32_t notes, HostWatch& host) { + if (const auto directory = File::DirectoryDescriptorPath(descriptor)) { + host.directory = true; + host.target = ::CreateFileW(directory->wstring().c_str(), FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + } else { + const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); + if (handle == INVALID_HANDLE_VALUE) { + return GUEST_EBADF; + } + const DWORD type = ::GetFileType(handle); + if (type == FILE_TYPE_PIPE) { + return GUEST_EINVAL; + } + if (type != FILE_TYPE_DISK) { + NotImplemented_nid_no_patch("EVFILT_VNODE: a descriptor that is not a file or a directory"); + } + host.target = ::ReOpenFile(handle, FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 0); + } + if (host.target == INVALID_HANDLE_VALUE) { + throw std::runtime_error("EVFILT_VNODE: opening the watched file failed, error=" + std::to_string(::GetLastError())); + } + if (!TakeSnapshot(host.target, host.last) || host.last.path.empty()) { + const auto error = ::GetLastError(); + CloseHostWatch(host); + throw std::runtime_error("EVFILT_VNODE: reading the watched file failed, error=" + std::to_string(error)); + } + try { + if (!IsDeleted(host.last)) { + host.parent = WatchParent(host.last.path); + } + if (host.directory) { + 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); + throw; + } + 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 { + int notify = -1; + int target = -1; + int entries = -1; + bool directory = false; + struct stat last {}; +}; + +struct HostEvent { + std::uint32_t mask; + std::uint32_t cookie; +}; + +void CloseHostWatch(HostWatch& host) { + for (int* descriptor : {&host.notify, &host.target, &host.entries}) { + if (*descriptor >= 0) { + ::close(*descriptor); + *descriptor = -1; + } + } +} + +std::uint32_t AttributeHint(HostWatch& host) { + struct stat current {}; + if (::fstat(host.target, ¤t) != 0) { + return NOTE_ATTRIB; + } + std::uint32_t hint = NOTE_ATTRIB; + if (current.st_nlink > host.last.st_nlink) { + hint = NOTE_LINK; + } else if (current.st_nlink < host.last.st_nlink) { + hint = host.directory && current.st_nlink != 0 ? NOTE_LINK : NOTE_DELETE; + } + host.last = current; + return hint; +} + +std::uint32_t ModifyHint(HostWatch& host) { + struct stat current {}; + if (::fstat(host.target, ¤t) != 0) { + return NOTE_WRITE; + } + std::uint32_t hint = NOTE_WRITE; + if (current.st_size > host.last.st_size) { + hint = NOTE_WRITE | NOTE_EXTEND; + } else if (current.st_size < host.last.st_size) { + hint = NOTE_ATTRIB; + } + host.last = current; + return hint; +} + +int ReadHostEvents(const HostWatch& host, std::vector& events) { + alignas(inotify_event) char buffer[16384]; + for (;;) { + const auto length = ::read(host.notify, buffer, sizeof(buffer)); + if (length < 0) { + if (errno == EINTR) { + continue; + } + return errno == EAGAIN ? 0 : errno; + } + if (length == 0) { + return 0; + } + for (char* cursor = buffer; cursor < buffer + length;) { + const auto* event = reinterpret_cast(cursor); + events.push_back({event->mask, event->cookie}); + cursor += sizeof(inotify_event) + event->len; + } + } +} + +std::unordered_map CountMoves(const std::vector& events) { + std::unordered_map moves; + for (const auto& event : events) { + if ((event.mask & (IN_MOVED_FROM | IN_MOVED_TO)) != 0) { + ++moves[event.cookie]; + } + } + return moves; +} + +bool HasUnpairedMove(const std::vector& events, std::size_t first) { + const auto moves = CountMoves(events); + for (std::size_t index = first; index < events.size(); ++index) { + if ((events[index].mask & IN_MOVED_FROM) != 0 && moves.at(events[index].cookie) == 1) { + return true; + } + } + return false; +} + +int WaitForRenamesInProgress(const HostWatch& host) { + char entries[1024]; + if (::getdents64(host.entries, entries, sizeof(entries)) < 0 && errno != ENOENT) { + return errno; + } + return 0; +} + +int ReadSettledHostEvents(const HostWatch& host, std::vector& events) { + if (const int error = ReadHostEvents(host, events)) { + return error; + } + for (std::size_t settled = 0; HasUnpairedMove(events, settled);) { + settled = events.size(); + if (const int error = WaitForRenamesInProgress(host)) { + return error; + } + if (const int error = ReadHostEvents(host, events)) { + return error; + } + } + return 0; +} + +void RunHostWatch(std::shared_ptr watch, HostWatch host) { + pollfd waits[2] = {{host.notify, POLLIN, 0}, {watch->StopEvent(), POLLIN, 0}}; + bool watching = true; + while (watching) { + if (::poll(waits, 2, -1) < 0) { + if (errno == EINTR) { + continue; + } + CloseHostWatch(host); + throw std::runtime_error("EVFILT_VNODE: poll failed, errno=" + std::to_string(errno)); + } + if (waits[1].revents != 0) { + break; + } + std::vector events; + if (const int error = ReadSettledHostEvents(host, events)) { + CloseHostWatch(host); + throw std::runtime_error("EVFILT_VNODE: reading inotify failed, errno=" + std::to_string(error)); + } + const auto moves = CountMoves(events); + std::vector hints; + for (const auto& event : events) { + const std::uint32_t link = (event.mask & IN_ISDIR) != 0 ? NOTE_LINK : 0; + if ((event.mask & IN_MODIFY) != 0) { + hints.push_back(ModifyHint(host)); + } + if ((event.mask & IN_ATTRIB) != 0) { + hints.push_back(AttributeHint(host)); + } + if ((event.mask & (IN_CREATE | IN_DELETE)) != 0) { + hints.push_back(NOTE_WRITE | link); + } + if ((event.mask & (IN_MOVED_FROM | IN_MOVED_TO)) != 0) { + hints.push_back(moves.at(event.cookie) > 1 ? NOTE_WRITE : NOTE_WRITE | NOTE_EXTEND | link); + } + if ((event.mask & IN_MOVE_SELF) != 0) { + hints.push_back(NOTE_RENAME); + } + if ((event.mask & IN_DELETE_SELF) != 0) { + hints.push_back(NOTE_DELETE); + } + if ((event.mask & IN_IGNORED) != 0) { + watching = false; + } + } + for (const auto hint : hints) { + watch->Deliver(hint); + } + } + CloseHostWatch(host); +} + +int OpenHostWatch(int descriptor, std::uint32_t, HostWatch& host) { + struct stat status {}; + if (::fstat(descriptor, &status) != 0) { + return GUEST_EBADF; + } + if (S_ISFIFO(status.st_mode) || S_ISSOCK(status.st_mode)) { + return GUEST_EINVAL; + } + if (!S_ISREG(status.st_mode) && !S_ISDIR(status.st_mode)) { + NotImplemented_nid_no_patch("EVFILT_VNODE: a descriptor that is not a file or a directory"); + } + host.directory = S_ISDIR(status.st_mode); + const auto path = "/proc/self/fd/" + std::to_string(descriptor); + host.target = ::open(path.c_str(), O_PATH | O_CLOEXEC); + if (host.target < 0 || ::fstat(host.target, &host.last) != 0) { + const int error = errno; + CloseHostWatch(host); + throw std::runtime_error("EVFILT_VNODE: opening the watched file failed, errno=" + std::to_string(error)); + } + if (host.directory) { + host.entries = ::open(path.c_str(), O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (host.entries < 0) { + const int error = errno; + CloseHostWatch(host); + throw std::runtime_error("EVFILT_VNODE: opening the watched directory failed, errno=" + std::to_string(error)); + } + } + host.notify = ::inotify_init1(IN_CLOEXEC | IN_NONBLOCK); + std::uint32_t mask = IN_MODIFY | IN_ATTRIB | IN_MOVE_SELF | IN_DELETE_SELF; + if (host.directory) { + mask |= IN_CREATE | IN_DELETE | IN_MOVED_FROM | IN_MOVED_TO; + } + if (host.notify < 0 || ::inotify_add_watch(host.notify, path.c_str(), mask) < 0) { + const int error = errno; + CloseHostWatch(host); + throw std::runtime_error("EVFILT_VNODE: inotify failed, errno=" + std::to_string(error)); + } + return 0; +} + +#endif + +void TriggerVnode(KernelEqueueEvent* event, void* data) { + const auto* message = static_cast(data); + if (event->filter.data != message->watch) { + return; + } + if ((message->watch->Interest() & message->hint) != 0) { + event->event.fflags |= message->hint; + } + event->triggered = event->event.fflags != 0; +} + +void StopVnode(KernelEqueue, KernelEqueueEvent* event) { + static_cast(event->filter.data)->Stop(); +} + +int VnodeDescriptorError(int descriptor) { + if (descriptor < 0) { + return GUEST_EBADF; + } + if (GuestSockets::IsOpen(descriptor)) { + return GUEST_EINVAL; + } + if (!DescriptorIsOpen_nid_no_patch(descriptor)) { + return GUEST_EBADF; + } + return IsKqueueDescriptor(descriptor) ? GUEST_EINVAL : 0; +} + +} + +void RequireVnodeNotes(std::uint32_t fflags) { + if ((fflags & ~MAPPED_NOTES) != 0) { + NotImplemented_nid_no_patch(("EVFILT_VNODE: fflags " + std::to_string(fflags & ~MAPPED_NOTES)).c_str()); + } +} + +void SetVnodeNotes(KernelEqueueEvent& event, std::uint32_t fflags) { + static_cast(event.filter.data)->SetInterest(fflags); +} + +int AddVnodeEvent(const KernelEqueueRef& queue, int descriptor, std::uint32_t fflags, std::uint16_t flags, void* udata) { + RequireVnodeNotes(fflags); + if (const int error = VnodeDescriptorError(descriptor)) { + return error; + } + HostWatch host; + if (const int error = OpenHostWatch(descriptor, fflags, host)) { + return error; + } + std::shared_ptr watch; + try { + watch = std::make_shared(queue, static_cast(descriptor), fflags); + } catch (...) { + CloseHostWatch(host); + throw; + } + KernelEqueueEvent event{}; + event.event.ident = static_cast(descriptor); + event.event.filter = EVFILT_VNODE; + event.event.flags = flags; + event.event.udata = udata; + event.filter.data = watch.get(); + event.filter.owner = watch; + event.filter.triggerFunc = TriggerVnode; + event.filter.deleteEventFunc = StopVnode; + const int result = queue->AddEvent(event); + bool registered = false; + if (result == EQUEUE_OK) { + queue->EditEvent(event.event.ident, EVFILT_VNODE, [®istered, &watch](KernelEqueueEvent& added) { + registered = added.filter.data == watch.get(); + }); + } + if (!registered) { + CloseHostWatch(host); + return result == EQUEUE_OK ? 0 : result & 0xffff; + } + std::thread(RunHostWatch, watch, std::move(host)).detach(); + return 0; +} + +extern "C" { + +int APS5_VABI sceKernelAddFileEvent(KernelEqueue eq, int fd, int watch, void* udata) { + const auto queue = EqueuePin_nid_postfix(eq); + if (!queue) { + return SCE_KERNEL_ERROR_EBADF; + } + const auto notes = static_cast(watch); + RequireVnodeNotes(notes); + if (const int error = VnodeDescriptorError(fd)) { + return SceKernelError(error); + } + const int edited = queue->EditEvent(static_cast(fd), EVFILT_VNODE, [notes, udata](KernelEqueueEvent& event) { + event.event.udata = udata; + SetVnodeNotes(event, notes); + }); + if (edited != SCE_KERNEL_ERROR_ENOENT) { + return edited; + } + const int error = AddVnodeEvent(queue, fd, notes, EV_CLEAR, udata); + return error == 0 ? EQUEUE_OK : SceKernelError(error); +} + +int APS5_VABI sceKernelDeleteFileEvent(KernelEqueue eq, int fd) { + if (!EqueuePin_nid_postfix(eq)) { + return SCE_KERNEL_ERROR_EBADF; + } + if (fd < 0 || (!GuestSockets::IsOpen(fd) && !DescriptorIsOpen_nid_no_patch(fd))) { + return SCE_KERNEL_ERROR_EBADF; + } + return EqueueDeleteEvent_nid_postfix(eq, static_cast(fd), EVFILT_VNODE); +} + +} diff --git a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp new file mode 100644 index 0000000000..0e7f1e05e0 --- /dev/null +++ b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp @@ -0,0 +1,486 @@ +#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" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#else +#include +#include +#endif + +extern "C" int* APS5_VABI __error_nid_postfix(); + +namespace { + +constexpr int GUEST_ENOENT = 2; +constexpr int GUEST_EBADF = 9; +constexpr int GUEST_EFAULT = 14; +constexpr int GUEST_EINVAL = 22; +constexpr int GUEST_ENFILE = 23; +constexpr int GUEST_EMFILE = 24; +constexpr int GUEST_EPIPE = 32; + +constexpr int KQ_NEVENTS = 8; +constexpr std::uint32_t NOTE_FFCTRLMASK = 0xc0000000; +constexpr std::uint32_t NOTE_FFLAGSMASK = 0x00ffffff; +constexpr std::uint32_t NOTE_FFAND = 0x40000000; +constexpr std::uint32_t NOTE_FFOR = 0x80000000; +constexpr std::uint32_t NOTE_FFCOPY = 0xc0000000; +constexpr std::uint32_t NOTE_TRIGGER = 0x01000000; +constexpr std::uint32_t NOTE_LOWAT = 0x0001; +constexpr std::uint32_t NOTE_SECONDS = 0x0001; +constexpr std::uint32_t NOTE_MSECONDS = 0x0002; +constexpr std::uint32_t NOTE_USECONDS = 0x0004; +constexpr std::uint32_t NOTE_NSECONDS = 0x0008; +constexpr std::uint32_t NOTE_TIMER_PRECMASK = NOTE_SECONDS | NOTE_MSECONDS | NOTE_USECONDS | NOTE_NSECONDS; +constexpr std::uint16_t KNOTE_CLEARED_FLAGS = EV_ADD | EV_DELETE | EV_ENABLE | EV_DISABLE | EV_FORCEONESHOT; + +std::mutex g_kqueueMutex; +std::unordered_map g_kqueues; + +int PosixFailure(int error) { + *__error_nid_postfix() = error; + return -1; +} + +int GuestErrno(int sceError) { + return sceError & 0xffff; +} + +KernelEqueueRef FindKqueue(int descriptor) { + std::lock_guard lock(g_kqueueMutex); + const auto found = g_kqueues.find(descriptor); + return found != g_kqueues.end() ? found->second : KernelEqueueRef{}; +} + +int ReserveDescriptor() { +#ifdef _WIN32 + return ::_open("NUL", _O_RDONLY | _O_BINARY); +#else + return ::open("/dev/null", O_RDONLY | O_CLOEXEC); +#endif +} + +void ReleaseDescriptor(int descriptor) { +#ifdef _WIN32 + ::_close(descriptor); +#else + ::close(descriptor); +#endif +} + +std::optional TimerPeriodNanos(std::int64_t data, std::uint32_t fflags) { + std::uint64_t unit = 0; + switch (fflags & NOTE_TIMER_PRECMASK) { + case NOTE_SECONDS: + unit = 1000000000ULL; + break; + case NOTE_MSECONDS: + case 0: + unit = 1000000ULL; + break; + case NOTE_USECONDS: + unit = 1000ULL; + break; + case NOTE_NSECONDS: + unit = 1; + break; + default: + return std::nullopt; + } + const auto value = static_cast(data); + if (value > std::numeric_limits::max() / unit) { + return std::numeric_limits::max(); + } + return value * unit; +} + +int DescriptorFilterError(int kqueueDescriptor, std::uintptr_t ident) { + if (ident > static_cast(INT_MAX)) { + return GUEST_EBADF; + } + const int descriptor = static_cast(ident); + if (GuestSockets::IsOpen(descriptor)) { + return 0; + } + if (!DescriptorIsOpen_nid_no_patch(descriptor)) { + return GUEST_EBADF; + } + return descriptor == kqueueDescriptor ? GUEST_EINVAL : 0; +} + +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; +} + +int AddDescriptorKnote(const KernelEqueueRef& queue, KernelEvent& change, std::uint16_t flags) { + const int descriptor = static_cast(change.ident); + if (IsKqueueDescriptor(descriptor)) { + if (change.filter != EVFILT_READ) { + return GUEST_EINVAL; + } + NotImplemented_nid_no_patch("kevent: EVFILT_READ on a kqueue descriptor"); + } + if (descriptor == 0 && change.filter == EVFILT_READ && File::IsHostStream(descriptor)) { + return AddStandardInputKnote(queue, change, flags); + } + 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 (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)); +} + +int AddTimerKnote(const KernelEqueueRef& queue, KernelEvent& change, std::uint16_t flags) { + std::int64_t period = change.data; + if (period < 0) { + return GUEST_EINVAL; + } + if (period == 0 && (flags & EV_ONESHOT) == 0) { + period = 1; + } + if ((change.fflags & ~NOTE_TIMER_PRECMASK) != 0) { + return GUEST_EINVAL; + } + const auto periodNs = TimerPeriodNanos(period, change.fflags); + if (!periodNs) { + return GUEST_EINVAL; + } + const std::uint64_t now = KernelEqueuePrivate::MonotonicNs(); + KernelEqueueEvent event{}; + event.deadlineNs = *periodNs <= std::numeric_limits::max() - now ? now + *periodNs : std::numeric_limits::max(); + event.intervalNs = (flags & EV_ONESHOT) != 0 ? 0 : *periodNs; + event.event = change; + event.event.flags = static_cast(flags | EV_CLEAR); + event.event.fflags = 0; + event.event.data = 0; + change.fflags = 0; + change.data = 0; + return GuestErrno(queue->AddEvent(event)); +} + +int AddUserKnote(const KernelEqueueRef& queue, KernelEvent& change, std::uint16_t flags) { + KernelEqueueEvent event{}; + event.event = change; + event.event.flags = flags; + event.filter.resetFunc = [](KernelEqueueEvent* e) { + if ((e->event.flags & EV_CLEAR) != 0) { + e->triggered = false; + } + }; + change.fflags = 0; + change.data = 0; + return GuestErrno(queue->AddEvent(event)); +} + +void TouchUserKnote(KernelEqueueEvent& event, KernelEvent& change) { + if ((change.fflags & NOTE_TRIGGER) != 0) { + event.triggered = true; + } + const std::uint32_t control = change.fflags & NOTE_FFCTRLMASK; + change.fflags &= NOTE_FFLAGSMASK; + if (control == NOTE_FFAND) { + event.event.fflags &= change.fflags; + } else if (control == NOTE_FFOR) { + event.event.fflags |= change.fflags; + } else if (control == NOTE_FFCOPY) { + event.event.fflags = change.fflags; + } + event.event.data = change.data; + if ((change.flags & EV_CLEAR) != 0) { + event.triggered = false; + } +} + +int ModifyKnote(const KernelEqueueRef& queue, KernelEvent& change) { + if (change.filter == EVFILT_TIMER) { + NotImplemented_nid_no_patch("kevent: changing an existing EVFILT_TIMER"); + } + 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, 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) { + SetVnodeNotes(event, change.fflags); + } + })); +} + +int Register(const KernelEqueueRef& queue, int kqueueDescriptor, KernelEvent& change) { + if ((change.flags & (EV_ENABLE | EV_DISABLE)) == (EV_ENABLE | EV_DISABLE)) { + return GUEST_EINVAL; + } + if (change.filter > 0) { + return GUEST_EINVAL; + } + if (change.filter != EVFILT_READ && change.filter != EVFILT_WRITE && change.filter != EVFILT_VNODE && change.filter != EVFILT_TIMER && change.filter != EVFILT_USER) { + NotImplemented_nid_no_patch(("kevent: filter " + std::to_string(change.filter)).c_str()); + } + if (change.filter == EVFILT_READ || change.filter == EVFILT_WRITE || change.filter == EVFILT_VNODE) { + if (const int error = DescriptorFilterError(kqueueDescriptor, change.ident)) { + return error; + } + } + if (!queue->HasEvent(change.ident, change.filter)) { + if ((change.flags & EV_ADD) == 0) { + return GUEST_ENOENT; + } + const auto flags = static_cast(change.flags & ~KNOTE_CLEARED_FLAGS); + int error; + if (change.filter == EVFILT_TIMER) { + 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; + error = AddVnodeEvent(queue, static_cast(change.ident), notes, flags, change.udata); + } else { + error = 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)); + } + return ModifyKnote(queue, change); +} + +std::optional ScanDeadline(const KernelTimespec& timeout) { + if (timeout.tv_sec > INT32_MAX) { + return std::nullopt; + } + const std::uint64_t span = static_cast(timeout.tv_sec) * 1000000000ULL + static_cast(timeout.tv_nsec); + return TimedWait::NowNanos() + span; +} + +} + +bool IsKqueueDescriptor(int descriptor) { + return static_cast(FindKqueue(descriptor)); +} + +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; + { + std::lock_guard lock(g_kqueueMutex); + const auto found = g_kqueues.find(descriptor); + if (found != g_kqueues.end()) { + closed = std::move(found->second); + g_kqueues.erase(found); + } + for (const auto& [kqueueDescriptor, queue] : g_kqueues) { + queues.push_back(queue); + } + } + if (closed) { + closed->Close(); + } + for (const auto& queue : queues) { + queue->RemoveDescriptorEvents(static_cast(descriptor)); + } +} + +extern "C" { + +int APS5_VABI kqueue_nid_postfix() { + const int descriptor = ReserveDescriptor(); + if (descriptor < 0) { + if (errno == EMFILE) return PosixFailure(GUEST_EMFILE); + if (errno == ENFILE) return PosixFailure(GUEST_ENFILE); + throw std::runtime_error(std::string(__func__) + ": reserving a descriptor failed, errno=" + std::to_string(errno)); + } + auto queue = std::make_shared(static_cast(descriptor), true); + queue->SetName("kqueue"); + std::lock_guard lock(g_kqueueMutex); + g_kqueues[descriptor] = std::move(queue); + return descriptor; +} + +int APS5_VABI kevent_nid_postfix(int descriptor, const KernelEvent* changelist, int nchanges, KernelEvent* eventlist, int nevents, const KernelTimespec* timeout) { + const auto queue = FindKqueue(descriptor); + if (!queue) { + return PosixFailure(GUEST_EBADF); + } + if (nevents < 0) { + return PosixFailure(GUEST_EINVAL); + } + if (nchanges > 0 && changelist == nullptr) { + return PosixFailure(GUEST_EFAULT); + } + int errors = 0; + for (int i = 0; i < nchanges; ++i) { + KernelEvent change = changelist[i]; + if (change.filter == 0) { + continue; + } + change.flags = static_cast(change.flags & ~EV_SYSFLAGS); + const int error = Register(queue, descriptor, change); + if (error == 0 && (change.flags & EV_RECEIPT) == 0) { + continue; + } + if (nevents == 0) { + return error == 0 ? 0 : PosixFailure(error); + } + change.flags = EV_ERROR; + change.data = error; + if (eventlist != nullptr) { + *eventlist++ = change; + } + --nevents; + ++errors; + } + if (errors != 0) { + return errors; + } + if (nevents == 0) { + return 0; + } + std::optional deadline; + bool poll = false; + if (timeout != nullptr) { + if (timeout->tv_sec < 0 || timeout->tv_nsec < 0 || timeout->tv_nsec >= 1000000000LL) { + return PosixFailure(GUEST_EINVAL); + } + poll = timeout->tv_sec == 0 && timeout->tv_nsec == 0; + if (!poll) { + deadline = ScanDeadline(*timeout); + } + } + KernelEvent scratch[KQ_NEVENTS]; + KernelEvent* target = eventlist != nullptr ? eventlist : scratch; + const int capacity = eventlist != nullptr ? nevents : std::min(nevents, KQ_NEVENTS); + const int count = poll ? queue->GetTriggeredEvents(target, capacity) : queue->WaitForEventsUntil(target, capacity, deadline.value_or(0)); + if (count < 0) { + return PosixFailure(GuestErrno(count)); + } + if (count > 0 && eventlist == nullptr) { + return PosixFailure(GUEST_EFAULT); + } + return count; +} + +} diff --git a/core/libs/prx/libkernel/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 new file mode 100644 index 0000000000..341324638c --- /dev/null +++ b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp @@ -0,0 +1,23 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_FILE_DESCRIPTORCONTROL_HPP +#define CORE_LIBS_PRX_LIBKERNEL_FILE_DESCRIPTORCONTROL_HPP + +#include + +namespace File { + +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); + +} + +#endif diff --git a/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp index 00c3b5895f..6978a7998a 100644 --- a/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp +++ b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp @@ -1,15 +1,26 @@ #ifndef CORE_LIBS_PRX_LIBKERNEL_FILE_DIRECTORYDESCRIPTOR_HPP #define CORE_LIBS_PRX_LIBKERNEL_FILE_DIRECTORYDESCRIPTOR_HPP +#include #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); -int ReadDirectoryDescriptor(int fd, char* buf, int nbytes); +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, 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 5b067ab776..6c8fbfadc2 100644 --- a/core/libs/prx/libkernel/File/include/File.hpp +++ b/core/libs/prx/libkernel/File/include/File.hpp @@ -3,6 +3,9 @@ #include #include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -16,10 +19,25 @@ std::int64_t APS5_VABI sceKernelWrite(int d, const void* buf, std::size_t nbytes std::int64_t APS5_VABI sceKernelLseek(int d, std::int64_t offset, int whence); int APS5_VABI sceKernelStat(const char* path, FileStat* sb); int APS5_VABI sceKernelUnlink(const char* path); +int APS5_VABI sceKernelFcntl(int d, int command, ...); #ifdef _WIN32 std::int64_t NativePositioned_nid_no_patch(int descriptor, void* buf, std::size_t nbytes, std::int64_t offset, bool write); #endif } +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 f97ccc9d25..374abfc4c5 100644 --- a/core/libs/prx/libkernel/File/include/FileFlags.hpp +++ b/core/libs/prx/libkernel/File/include/FileFlags.hpp @@ -9,6 +9,7 @@ constexpr std::int32_t SCE_KERNEL_O_RDWR = 0x00000002; constexpr std::int32_t SCE_KERNEL_O_ACCMODE = 0x00000003; constexpr std::int32_t SCE_KERNEL_O_APPEND = 0x00000008; constexpr std::int32_t SCE_KERNEL_O_FSYNC = 0x00000080; +constexpr std::int32_t SCE_KERNEL_O_NOFOLLOW = 0x00000100; constexpr std::int32_t SCE_KERNEL_O_SYNC = 0x00000080; constexpr std::int32_t SCE_KERNEL_O_CREAT = 0x00000200; constexpr std::int32_t SCE_KERNEL_O_TRUNC = 0x00000400; @@ -18,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/FileLock.hpp b/core/libs/prx/libkernel/File/include/FileLock.hpp new file mode 100644 index 0000000000..0cb00071a0 --- /dev/null +++ b/core/libs/prx/libkernel/File/include/FileLock.hpp @@ -0,0 +1,11 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_FILE_FILELOCK_HPP +#define CORE_LIBS_PRX_LIBKERNEL_FILE_FILELOCK_HPP + +namespace File { + +int Flock(int fd, int operation); +void ForgetFileLock(int fd); + +} + +#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/KernelStatfs.hpp b/core/libs/prx/libkernel/File/include/KernelStatfs.hpp new file mode 100644 index 0000000000..7ecfe985f5 --- /dev/null +++ b/core/libs/prx/libkernel/File/include/KernelStatfs.hpp @@ -0,0 +1,33 @@ +#pragma once +#include +#include + +struct KernelStatfs { + std::uint32_t f_version; + std::uint32_t f_type; + std::uint64_t f_flags; + std::uint64_t f_bsize; + std::uint64_t f_iosize; + std::uint64_t f_blocks; + std::uint64_t f_bfree; + std::int64_t f_bavail; + std::uint64_t f_files; + std::int64_t f_ffree; + std::uint64_t f_syncwrites; + std::uint64_t f_asyncwrites; + std::uint64_t f_syncreads; + std::uint64_t f_asyncreads; + std::uint64_t f_spare[10]; + std::uint32_t f_namemax; + std::uint32_t f_owner; + std::int32_t f_fsid[2]; + char f_charspare[80]; + char f_fstypename[16]; + char f_mntfromname[88]; + char f_mntonname[88]; +}; +static_assert(sizeof(KernelStatfs) == 472); +static_assert(offsetof(KernelStatfs, f_bavail) == 48); +static_assert(offsetof(KernelStatfs, f_namemax) == 184); +static_assert(offsetof(KernelStatfs, f_fstypename) == 280); +static_assert(offsetof(KernelStatfs, f_mntonname) == 384); diff --git a/core/libs/prx/libkernel/File/include/NativeStat.hpp b/core/libs/prx/libkernel/File/include/NativeStat.hpp index 5715f02119..b60abe0b89 100644 --- a/core/libs/prx/libkernel/File/include/NativeStat.hpp +++ b/core/libs/prx/libkernel/File/include/NativeStat.hpp @@ -7,6 +7,7 @@ namespace File { 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/RandomDevice.hpp b/core/libs/prx/libkernel/File/include/RandomDevice.hpp new file mode 100644 index 0000000000..f637b52be0 --- /dev/null +++ b/core/libs/prx/libkernel/File/include/RandomDevice.hpp @@ -0,0 +1,18 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_FILE_RANDOMDEVICE_HPP +#define CORE_LIBS_PRX_LIBKERNEL_FILE_RANDOMDEVICE_HPP + +#include +#include + +namespace File { + +bool IsRandomDevicePath(std::string_view path); +int OpenRandomDevice(); +bool IsRandomDevice(int fd); +bool ReadRandomDevice(int fd, void* buf, std::size_t nbytes); +void RememberRandomDevice(int fd); +void ForgetRandomDevice(int fd); + +} + +#endif diff --git a/core/libs/prx/libkernel/File/include/SharedMemory.hpp b/core/libs/prx/libkernel/File/include/SharedMemory.hpp new file mode 100644 index 0000000000..5c9ffc9f47 --- /dev/null +++ b/core/libs/prx/libkernel/File/include/SharedMemory.hpp @@ -0,0 +1,24 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_FILE_SHAREDMEMORY_HPP +#define CORE_LIBS_PRX_LIBKERNEL_FILE_SHAREDMEMORY_HPP + +#include +#include +#ifndef _WIN32 +#include +#endif + +namespace File { + +bool IsSharedMemoryDescriptor_nid_no_patch(int descriptor); +std::int64_t WriteSharedMemory_nid_no_patch(int descriptor, const void* buffer, std::size_t size); +std::int64_t PwriteSharedMemory_nid_no_patch(int descriptor, const void* buffer, std::size_t size, std::int64_t offset); +std::int64_t SeekSharedMemory_nid_no_patch(int descriptor, std::int64_t offset, int whence); +int TruncateSharedMemory_nid_no_patch(int descriptor, std::int64_t length); +#ifndef _WIN32 +std::int64_t WritevSharedMemory_nid_no_patch(int descriptor, const struct iovec* vectors, int count); +std::int64_t PwritevSharedMemory_nid_no_patch(int descriptor, const struct iovec* vectors, int count, std::int64_t offset); +#endif + +} + +#endif diff --git a/core/libs/prx/libkernel/File/include/TrustedRoots.hpp b/core/libs/prx/libkernel/File/include/TrustedRoots.hpp new file mode 100644 index 0000000000..950d25dcff --- /dev/null +++ b/core/libs/prx/libkernel/File/include/TrustedRoots.hpp @@ -0,0 +1,17 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_FILE_TRUSTEDROOTS_HPP +#define CORE_LIBS_PRX_LIBKERNEL_FILE_TRUSTEDROOTS_HPP + +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include +#include + +extern "C" std::string ExportHostRoots_nid_no_patch(HCERTSTORE roots, HCERTSTORE disallowed); +extern "C" std::string HostTrustedTlsRoots_nid_no_patch(); + +#endif diff --git a/core/libs/prx/libkernel/File/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..cdc089e731 --- /dev/null +++ b/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp @@ -0,0 +1,113 @@ +#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 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 new file mode 100644 index 0000000000..a83f899801 --- /dev/null +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -0,0 +1,598 @@ +#include "prx/libkernel/File/include/DescriptorControl.hpp" +#include "prx/libkernel/File/include/DirectoryDescriptor.hpp" +#include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/File/include/RandomDevice.hpp" +#include "prx/libkernel/File/include/ArchivedFile.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/FileLock.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#include "prx/libc/include/WindowsDescriptor.hpp" +#else +#include +#include +#include +#include +#endif + +extern "C" int* APS5_VABI __error_nid_postfix(); +extern "C" int APS5_VABI dup2_nid_postfix(int from, int to); + +namespace { + +constexpr int GuestEintr = 4; +constexpr int GuestEbadf = 9; +constexpr int GuestEdeadlk = 11; +constexpr int GuestEfault = 14; +constexpr int GuestEinval = 22; +constexpr int GuestEnotty = 25; +constexpr int GuestEagain = 35; +constexpr int GuestEnolck = 77; +constexpr int GuestEoverflow = 84; + +constexpr int DuplicateCommand = 0; +constexpr int GetDescriptorFlags = 1; +constexpr int SetDescriptorFlags = 2; +constexpr int GetStatusFlags = 3; +constexpr int SetStatusFlags = 4; +constexpr int GetOwner = 5; +constexpr int SetOwner = 6; +constexpr int OldGetLock = 7; +constexpr int OldSetLock = 8; +constexpr int OldSetLockWait = 9; +constexpr int DuplicateToCommand = 10; +constexpr int GetLock = 11; +constexpr int SetLock = 12; +constexpr int SetLockWait = 13; +constexpr int SetLockRemote = 14; +constexpr int ReadAhead = 15; +constexpr int DarwinReadAhead = 16; +constexpr int DuplicateCloseOnExec = 17; +constexpr int DuplicateToCloseOnExec = 18; + +constexpr int CloseOnExec = 1; +constexpr int OpenCloseOnExec = 0x00100000; + +constexpr int ReadOnly = 0; +constexpr int WriteOnly = 1; +constexpr int ReadWrite = 2; +constexpr int Nonblocking = 0x0004; +constexpr int Append = 0x0008; +constexpr int Asynchronous = 0x0040; +constexpr int Synchronous = 0x0080; +constexpr int ReadAheadFlag = 0x0200; +constexpr int Direct = 0x00010000; +constexpr int OpenStatusFlags = Nonblocking | Append | Asynchronous | Synchronous | Direct; +constexpr int SettableStatusFlags = OpenStatusFlags | ReadAheadFlag; + +constexpr std::int16_t ReadLock = 1; +constexpr std::int16_t Unlock = 2; +constexpr std::int16_t WriteLock = 3; +constexpr std::int16_t FromStart = 0; +constexpr std::int16_t FromCurrent = 1; +constexpr std::int16_t FromEnd = 2; + +constexpr std::uint32_t CloseOnExecRequest = 0x20006601; +constexpr std::uint32_t KeepOnExecRequest = 0x20006602; +constexpr std::uint32_t NonblockingRequest = 0x8004667e; +constexpr std::uint32_t ReadableRequest = 0x4004667f; +constexpr std::uint32_t RequestIn = 0x80000000u; +constexpr std::uint32_t RequestOut = 0x40000000u; +constexpr std::uint32_t RequestVoid = 0x20000000u; +constexpr std::uint32_t TerminalGroup = 't'; + +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); +static_assert(offsetof(GuestLock, type) == 20); +constexpr std::size_t OldGuestLockSize = 24; + +enum class Kind { File, Directory, Pipe, Socket, Device, RandomDevice, ArchivedFile }; + +struct Entry { + bool closeOnExec = false; + std::shared_ptr status; +}; + +std::mutex g_mutex; +std::map g_entries; + +int Fail(int error) { + *__error_nid_postfix() = error; + return -1; +} + +const char* KindName(Kind kind) { + switch (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"; +} + +[[noreturn]] void Unsupported(const char* function, const std::string& what, Kind kind, int fd) { + throw std::runtime_error(std::string(function) + ": " + what + " on a " + KindName(kind) + " is not implemented, fd=" + std::to_string(fd)); +} + +#ifdef _WIN32 +std::optional Classify(int fd, const char* function) { + if (fd < 0 || !DescriptorIsOpen_nid_no_patch(fd)) return std::nullopt; + 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; + case FILE_TYPE_CHAR: return Kind::Device; + default: throw std::runtime_error(std::string(function) + ": descriptor " + std::to_string(fd) + " has an unknown Windows file type, error=" + std::to_string(::GetLastError())); + } +} + +int AccessMode(int fd, const char* function) { + return WindowsDescriptorAccess_nid_no_patch(WindowsDescriptorHandle_nid_no_patch(fd), fd, function); +} + +DWORD PipeState(int fd, const char* function) { + DWORD state = 0; + if (!::GetNamedPipeHandleStateW(WindowsDescriptorHandle_nid_no_patch(fd), &state, nullptr, nullptr, nullptr, nullptr, 0)) + throw std::runtime_error(std::string(function) + ": cannot read the mode of pipe descriptor " + std::to_string(fd) + ", error=" + std::to_string(::GetLastError())); + return state; +} + +void SetPipeNonblocking(int fd, bool nonblocking, const char* function) { + DWORD mode = (PipeState(fd, function) & PIPE_READMODE_MESSAGE) | (nonblocking ? PIPE_NOWAIT : PIPE_WAIT); + if (!::SetNamedPipeHandleState(WindowsDescriptorHandle_nid_no_patch(fd), &mode, nullptr, nullptr)) + throw std::runtime_error(std::string(function) + ": cannot change the mode of pipe descriptor " + std::to_string(fd) + ", error=" + std::to_string(::GetLastError())); +} + +Entry InitialEntry(int fd) { + int status = 0; + if (::GetFileType(WindowsDescriptorHandle_nid_no_patch(fd)) == FILE_TYPE_PIPE && (PipeState(fd, "fcntl") & PIPE_NOWAIT) != 0) status |= Nonblocking; + return {false, std::make_shared(status)}; +} + +void ApplyHostStatus(int fd, Kind kind, int changed, int next, const char* function) { + if ((changed & Append) && kind == Kind::File) Unsupported(function, "changing O_APPEND", kind, fd); + if ((changed & Nonblocking) && kind == Kind::Device) Unsupported(function, "changing O_NONBLOCK", kind, fd); + if ((changed & Nonblocking) && kind == Kind::Pipe) SetPipeNonblocking(fd, (next & Nonblocking) != 0, function); +} + +int Readable(int fd, Kind kind, const char* function) { + if (kind == Kind::File) { + const auto size = ::_filelengthi64(fd); + const auto offset = ::_telli64(fd); + if (size < 0 || offset < 0) + throw std::runtime_error(std::string(function) + ": cannot read the size and offset of descriptor " + std::to_string(fd) + ", errno=" + std::to_string(errno)); + return static_cast(size - offset); + } + if (AccessMode(fd, function) == WriteOnly) return 0; + DWORD available = 0; + if (!::PeekNamedPipe(WindowsDescriptorHandle_nid_no_patch(fd), nullptr, 0, nullptr, &available, nullptr)) { + const DWORD error = ::GetLastError(); + if (error == ERROR_BROKEN_PIPE) return 0; + throw std::runtime_error(std::string(function) + ": cannot peek pipe descriptor " + std::to_string(fd) + ", error=" + std::to_string(error)); + } + return static_cast(available); +} + +std::int64_t LockOffset(int fd, Kind kind) { + if (kind == Kind::Directory) { + const auto cursor = File::SeekDirectoryDescriptor(fd, 0, FromCurrent); + if (!cursor) throw std::runtime_error("fcntl: cannot read the offset of directory descriptor " + std::to_string(fd)); + return *cursor; + } + const auto offset = ::_telli64(fd); + if (offset < 0) throw std::runtime_error("fcntl: cannot read the offset of descriptor " + std::to_string(fd) + ", errno=" + std::to_string(errno)); + return offset; +} + +std::int64_t LockFileSize(int fd, Kind kind) { + if (kind == Kind::Directory) Unsupported("fcntl", "a record lock from the end", kind, fd); + const auto size = ::_filelengthi64(fd); + if (size < 0) throw std::runtime_error("fcntl: cannot read the size of descriptor " + std::to_string(fd) + ", errno=" + std::to_string(errno)); + return size; +} + +int LockRangeError(int fd, Kind kind, const GuestLock& lock) { + constexpr auto Largest = std::numeric_limits::max(); + std::int64_t start = lock.start; + if (lock.whence == FromEnd) { + const auto size = LockFileSize(fd, kind); + if (lock.start > 0 && size > Largest - lock.start) return GuestEoverflow; + start = size + lock.start; + } else if (lock.whence != FromStart && lock.whence != FromCurrent) { + return GuestEinval; + } + if (start < 0) return GuestEinval; + if (lock.length < 0 && (start == 0 || start + lock.length < 0)) return GuestEinval; + if (lock.length > 0 && lock.length - 1 > Largest - start) return GuestEoverflow; + return 0; +} + +int ProcessRecordLock(int fd, Kind kind, int command, GuestLock& lock, void* pointer, std::size_t size) { + const bool known = lock.type == ReadLock || lock.type == WriteLock || lock.type == Unlock; + if (command == GetLock && !known) return Fail(GuestEinval); + if (lock.whence == FromCurrent) { + const auto offset = LockOffset(fd, kind); + if (lock.start > 0 && offset > std::numeric_limits::max() - lock.start) return Fail(GuestEoverflow); + lock.start += offset; + } + if (command != GetLock) { + if (!known) return Fail(GuestEinval); + const int access = lock.type == Unlock ? ReadWrite : AccessMode(fd, "fcntl"); + if ((lock.type == ReadLock && access == WriteOnly) || (lock.type == WriteLock && access == ReadOnly)) return Fail(GuestEbadf); + } + if (const int error = LockRangeError(fd, kind, lock); error != 0) return Fail(error); + if (command != GetLock) return 0; + lock.type = Unlock; + const GuestArena::HostWrite destination(pointer, size); + if (!destination.Open()) return Fail(GuestEfault); + std::memcpy(pointer, &lock, size); + return 0; +} +#else +std::optional Classify(int fd, const char* function) { + if (fd < 0) return std::nullopt; + struct stat status{}; + if (::fstat(fd, &status) != 0) { + if (errno == EBADF) return std::nullopt; + 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; + 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) { + const int flags = ::fcntl(fd, F_GETFL); + if (flags < 0) throw std::runtime_error(std::string(function) + ": host F_GETFL failed, fd=" + std::to_string(fd) + ", errno=" + std::to_string(errno)); + return flags; +} + +int AccessMode(int fd, const char* function) { + return HostStatus(fd, function) & O_ACCMODE; +} + +Entry InitialEntry(int fd) { + const int descriptorFlags = ::fcntl(fd, F_GETFD); + if (descriptorFlags < 0) throw std::runtime_error("fcntl: host F_GETFD failed, fd=" + std::to_string(fd) + ", errno=" + std::to_string(errno)); + const int host = HostStatus(fd, "fcntl"); + int status = 0; + if (host & O_NONBLOCK) status |= Nonblocking; + if (host & O_APPEND) status |= Append; + if (host & O_ASYNC) status |= Asynchronous; + if ((host & O_SYNC) == O_SYNC) status |= Synchronous; +#ifdef O_DIRECT + if (host & O_DIRECT) status |= Direct; +#endif + return {(descriptorFlags & FD_CLOEXEC) != 0, std::make_shared(status)}; +} + +void ApplyHostStatus(int fd, Kind, int changed, int next, const char* function) { + if ((changed & (Nonblocking | Append)) == 0) return; + const int host = HostStatus(fd, function); + const int wanted = (host & ~(O_NONBLOCK | O_APPEND)) | ((next & Nonblocking) ? O_NONBLOCK : 0) | ((next & Append) ? O_APPEND : 0); + if (::fcntl(fd, F_SETFL, wanted) != 0) + throw std::runtime_error(std::string(function) + ": host F_SETFL failed, fd=" + std::to_string(fd) + ", errno=" + std::to_string(errno)); +} + +int Readable(int fd, Kind kind, const char* function) { + if (kind == Kind::File) { + struct stat status{}; + const auto offset = ::lseek(fd, 0, SEEK_CUR); + if (::fstat(fd, &status) != 0 || offset < 0) + throw std::runtime_error(std::string(function) + ": cannot read the size and offset of descriptor " + std::to_string(fd) + ", errno=" + std::to_string(errno)); + return static_cast(status.st_size - offset); + } + if (AccessMode(fd, function) == WriteOnly) return 0; + int available = 0; + if (::ioctl(fd, FIONREAD, &available) != 0) + throw std::runtime_error(std::string(function) + ": host FIONREAD failed, fd=" + std::to_string(fd) + ", errno=" + std::to_string(errno)); + return available; +} + +int LockError(int error) { + switch (error) { + case EACCES: + case EAGAIN: return GuestEagain; + case EDEADLK: return GuestEdeadlk; + case EINTR: return GuestEintr; + case ENOLCK: return GuestEnolck; + case EBADF: return GuestEbadf; + case EINVAL: return GuestEinval; + case EOVERFLOW: return GuestEoverflow; + default: throw std::runtime_error("fcntl: host record lock failed, errno=" + std::to_string(error)); + } +} +#endif + +Entry& Materialize(int fd) { + const auto found = g_entries.find(fd); + if (found != g_entries.end()) return found->second; + return g_entries.emplace(fd, InitialEntry(fd)).first->second; +} + +int ChangeStatus(int fd, Kind kind, int mask, int value, const char* function) { + std::lock_guard lock(g_mutex); + 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::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; + return 0; +} + +int CurrentStatus(int fd) { + std::lock_guard lock(g_mutex); + return *Materialize(fd).status; +} + +void SetCloseOnExec(int fd, bool value) { + std::lock_guard lock(g_mutex); + Materialize(fd).closeOnExec = value; +} + +int RecordLock(int fd, int command, std::intptr_t argument) { + const bool old = command <= OldSetLockWait; + const int current = old ? command + (GetLock - OldGetLock) : command; + const std::size_t size = old ? OldGuestLockSize : sizeof(GuestLock); + auto* pointer = reinterpret_cast(argument); + if (pointer == nullptr) return Fail(GuestEfault); + GuestLock lock{}; + std::memcpy(&lock, pointer, size); + const auto kind = Classify(fd, "fcntl"); + 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); +#else + 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 = current == GetLock ? F_RDLCK : F_UNLCK; + else return Fail(GuestEinval); + host.l_whence = lock.whence; + host.l_start = static_cast(lock.start); + host.l_len = static_cast(lock.length); + const int hostCommand = current == GetLock ? F_GETLK : current == SetLock ? F_SETLK : F_SETLKW; + if (::fcntl(fd, hostCommand, &host) != 0) return Fail(LockError(errno)); + if (current != GetLock) return 0; + if (host.l_type == F_UNLCK) { + if (lock.whence == FromCurrent) { + const auto offset = ::lseek(fd, 0, SEEK_CUR); + if (offset < 0) throw std::runtime_error("fcntl: cannot read the offset of descriptor " + std::to_string(fd) + ", errno=" + std::to_string(errno)); + lock.start += offset; + } + lock.type = Unlock; + } else { + lock.type = host.l_type == F_RDLCK ? ReadLock : WriteLock; + lock.whence = SEEK_SET; + lock.start = host.l_start; + lock.length = host.l_len; + lock.pid = host.l_pid; + lock.system = 0; + } + const GuestArena::HostWrite destination(pointer, size); + if (!destination.Open()) return Fail(GuestEfault); + std::memcpy(pointer, &lock, size); + return 0; +#endif +} + +int ReadAheadControl(int fd, int command, std::intptr_t argument) { + const auto kind = Classify(fd, "fcntl"); + 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; + else status &= ~ReadAheadFlag; + return 0; +} + +} + +namespace File { + +FcntlArgument FcntlArgumentOf(int command) { + switch (command) { + case DuplicateCommand: + case SetDescriptorFlags: + case SetStatusFlags: + case SetOwner: + case DuplicateToCommand: + case ReadAhead: + case DarwinReadAhead: + case DuplicateCloseOnExec: + case DuplicateToCloseOnExec: + return FcntlArgument::Int; + case OldGetLock: + case OldSetLock: + case OldSetLockWait: + case GetLock: + case SetLock: + case SetLockWait: + case SetLockRemote: + return FcntlArgument::Pointer; + default: + return FcntlArgument::None; + } +} + +int Fcntl(int fd, int command, std::intptr_t argument) { + switch (command) { + case DuplicateCommand: + case DuplicateCloseOnExec: { + if (fd < 0) return Fail(GuestEbadf); + if (static_cast(argument) < 0) return Fail(GuestEinval); + if (!Classify(fd, "fcntl")) return Fail(GuestEbadf); + const int duplicate = DuplicateFrom(fd, static_cast(argument)); + if (duplicate >= 0 && command == DuplicateCloseOnExec) SetCloseOnExec(duplicate, true); + return duplicate; + } + case DuplicateToCommand: + case DuplicateToCloseOnExec: { + const int duplicate = dup2_nid_postfix(fd, static_cast(argument)); + if (duplicate >= 0 && command == DuplicateToCloseOnExec) SetCloseOnExec(duplicate, true); + return duplicate; + } + case GetDescriptorFlags: { + if (!Classify(fd, "fcntl")) return Fail(GuestEbadf); + std::lock_guard lock(g_mutex); + return Materialize(fd).closeOnExec ? CloseOnExec : 0; + } + case SetDescriptorFlags: + if (!Classify(fd, "fcntl")) return Fail(GuestEbadf); + SetCloseOnExec(fd, (argument & CloseOnExec) != 0); + return 0; + case GetStatusFlags: { + if (!Classify(fd, "fcntl")) return Fail(GuestEbadf); + const int access = AccessMode(fd, "fcntl"); + return access | CurrentStatus(fd); + } + case SetStatusFlags: { + const auto kind = Classify(fd, "fcntl"); + if (!kind) return Fail(GuestEbadf); + return ChangeStatus(fd, *kind, SettableStatusFlags, static_cast(argument), "fcntl"); + } + case GetOwner: + case SetOwner: { + const auto kind = Classify(fd, "fcntl"); + if (!kind) return Fail(GuestEbadf); + Unsupported("fcntl", command == GetOwner ? "F_GETOWN" : "F_SETOWN", *kind, fd); + } + case OldGetLock: + case OldSetLock: + case OldSetLockWait: + case GetLock: + case SetLock: + case SetLockWait: + return RecordLock(fd, command, argument); + case SetLockRemote: + throw std::runtime_error("fcntl: F_SETLK_REMOTE needs a privilege whose state on the console is not known, fd=" + std::to_string(fd)); + case ReadAhead: + case DarwinReadAhead: + return ReadAheadControl(fd, command, argument); + default: + return Fail(GuestEinval); + } +} + +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); + const auto kind = Classify(fd, "ioctl"); + if (!kind) return Fail(GuestEbadf); + if (command == CloseOnExecRequest || command == KeepOnExecRequest) { + SetCloseOnExec(fd, command == CloseOnExecRequest); + return 0; + } + if (command == NonblockingRequest) { + const bool enable = *static_cast(argument) != 0; + return ChangeStatus(fd, *kind, Nonblocking, enable ? Nonblocking : 0, "ioctl"); + } + if (command == ReadableRequest) { + if (*kind == Kind::RandomDevice) return Fail(GuestEnotty); + 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); + std::memcpy(argument, &available, sizeof(available)); + return 0; + } + if (((command >> 8) & 0xff) == TerminalGroup) return Fail(GuestEnotty); + char hex[11]; + std::snprintf(hex, sizeof(hex), "0x%08X", command); + Unsupported("ioctl", std::string("request ") + hex, *kind, fd); +} + +void RememberDescriptor(int fd, int openFlags) { + std::lock_guard lock(g_mutex); + g_entries[fd] = Entry{(openFlags & OpenCloseOnExec) != 0, std::make_shared(openFlags & OpenStatusFlags)}; +#ifdef _WIN32 + GuestFileLockForget_nid_no_patch(fd); +#endif +} + +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)}; +#ifdef _WIN32 + File::SharePositionedTransfer(from, to); + GuestFileLockShare_nid_no_patch(from, to); +#endif +} + +void ForgetDescriptor(int fd) { + File::ForgetArchivedFile(fd); + std::lock_guard lock(g_mutex); + g_entries.erase(fd); +} + +bool Appends(int fd) { + std::lock_guard lock(g_mutex); + const auto found = g_entries.find(fd); + return found != g_entries.end() && found->second.status && (*found->second.status & Append) != 0; +} + +bool IsVnode(int fd) { + if (fd < 0 || File::IsRandomDevice(fd)) return false; + if (File::IsArchivedFile(fd)) return true; +#ifdef _WIN32 + if (!DescriptorIsOpen_nid_no_patch(fd)) return false; + return File::DirectoryDescriptorPath(fd) || ::GetFileType(WindowsDescriptorHandle_nid_no_patch(fd)) == FILE_TYPE_DISK; +#else + struct stat status{}; + return ::fstat(fd, &status) == 0 && (S_ISREG(status.st_mode) || S_ISDIR(status.st_mode)); +#endif +} + +} diff --git a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp index 77f060966a..7727bb9000 100644 --- a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp +++ b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp @@ -1,14 +1,28 @@ #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" +#include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/KernelErrors.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" + +#include +#include #ifdef _WIN32 +#include "prx/libc/include/General.hpp" + +#include #include +#include #include +#include #include #include +#include #include +#include #include +#include +#include #include #include #include @@ -19,57 +33,139 @@ constexpr std::uint8_t GuestDirectoryType = 4; constexpr std::uint8_t GuestRegularType = 8; struct DirectoryState { - std::filesystem::path path; + std::mutex mutex; std::vector> entries; std::size_t cursor = 0; bool loaded = false; }; std::mutex g_mutex; -std::map g_directories; +std::map> g_directories; +std::atomic g_directoryCount{0}; + +std::shared_ptr Find(int fd) { + std::lock_guard lock(g_mutex); + const auto found = g_directories.find(fd); + return found == g_directories.end() ? nullptr : found->second; +} + +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(*path, error)) { + std::error_code typeError; + state.entries.emplace_back(entry.path().filename().string(), entry.is_directory(typeError) ? GuestDirectoryType : GuestRegularType); + } +} } 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); - g_directories[fd] = DirectoryState{path}; + const auto state = std::make_shared(); + 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) { - std::lock_guard lock(g_mutex); - const auto found = g_directories.find(fd); - if (found == g_directories.end()) return std::nullopt; - return found->second.path; + if (!IsDirectoryDescriptor(fd)) return std::nullopt; + return WindowsDescriptorPath_nid_no_patch(fd); } void ForgetDirectoryDescriptor(int fd) { + if (g_directoryCount == 0) return; std::lock_guard lock(g_mutex); g_directories.erase(fd); + g_directoryCount = g_directories.size(); } -int ReadDirectoryDescriptor(int fd, char* buf, int nbytes) { +void DuplicateDirectoryDescriptor(int from, int to) { + if (g_directoryCount == 0) return; std::lock_guard lock(g_mutex); - const auto found = g_directories.find(fd); - if (found == g_directories.end()) return SCE_KERNEL_ERROR_ENOTDIR; - auto& state = found->second; - if (!state.loaded) { - 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)) { - std::error_code typeError; - state.entries.emplace_back(entry.path().filename().string(), entry.is_directory(typeError) ? GuestDirectoryType : GuestRegularType); - } + const auto found = g_directories.find(from); + if (found == g_directories.end()) return; + g_directories[to] = found->second; + g_directoryCount = g_directories.size(); +} + +std::optional SeekDirectoryDescriptor(int fd, std::int64_t offset, int whence) { + constexpr int SeekSet = 0; + constexpr int SeekCurrent = 1; + 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 && 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 || whence == SeekEnd) && offset > std::numeric_limits::max() - cursor) { + errno = EOVERFLOW; + return std::int64_t{-1}; + } + const std::int64_t target = whence == SeekSet ? offset : cursor + offset; + if (target < 0) { + errno = EINVAL; + return std::int64_t{-1}; + } + if (target == 0) { + state.entries.clear(); + state.loaded = false; + state.cursor = 0; + return std::int64_t{0}; + } + Load(state, fd); + state.cursor = static_cast(target); + return target; +} + +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, 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]; + if (name.size() > 255) { + throw std::runtime_error("Directory entry name exceeds the guest 255-byte limit"); + } const std::size_t record = (8 + name.size() + 1 + 3) & ~std::size_t{3}; if (used + record > static_cast(nbytes)) { if (used == 0) return SCE_KERNEL_ERROR_EINVAL; @@ -92,4 +188,53 @@ int ReadDirectoryDescriptor(int fd, char* buf, int nbytes) { } +#else + +namespace File { + +std::optional DirectoryDescriptorPath(int fd) { + if (fd < 0) return std::nullopt; + std::error_code error; + const auto link = std::filesystem::path("/proc/self/fd") / std::to_string(fd); + const auto target = std::filesystem::read_symlink(link, error); + if (error) return std::nullopt; + if (!std::filesystem::is_directory(target, error)) return std::nullopt; + return target; +} + +} + #endif + +extern "C" const char* GetDirectoryDescriptorPath_nid_no_patch(int fd, int* error) { + if (error == nullptr) return nullptr; + if (!DescriptorIsOpen_nid_no_patch(fd)) { + *error = EBADF; + return nullptr; + } + const auto directory = File::DirectoryDescriptorPath(fd); + if (!directory) { +#ifdef _WIN32 + *error = File::IsDirectoryDescriptor(fd) ? errno : ENOTDIR; +#else + *error = ENOTDIR; +#endif + return nullptr; + } + struct PathTag {}; + auto& path = HostThreadLocal(); + const auto utf8Path = directory->u8string(); + path.assign(reinterpret_cast(utf8Path.data()), utf8Path.size()); + *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/FileLock.cpp b/core/libs/prx/libkernel/File/src/FileLock.cpp new file mode 100644 index 0000000000..1ebc7e409b --- /dev/null +++ b/core/libs/prx/libkernel/File/src/FileLock.cpp @@ -0,0 +1,18 @@ +#include "prx/libkernel/File/include/FileLock.hpp" +#include "prx/libc/include/FileLock.hpp" + +#ifdef _WIN32 + +namespace File { + +int Flock(int fd, int operation) { + return GuestFileLock_nid_no_patch(fd, operation); +} + +void ForgetFileLock(int fd) { + GuestFileLockForget_nid_no_patch(fd); +} + +} + +#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 a6fb3304ae..75e7d82619 100644 --- a/core/libs/prx/libkernel/File/src/NativeStat.cpp +++ b/core/libs/prx/libkernel/File/src/NativeStat.cpp @@ -1,44 +1,71 @@ #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 ReadTimes(HANDLE handle, NativeStat* st) { +static int ReadHandleInfo(HANDLE handle, NativeStat* st) { if (GetFileType(handle) != FILE_TYPE_DISK) return 0; if (!GetFileInformationByHandleEx(handle, FileBasicInfo, &st->times, sizeof(st->times))) return File::WindowsFileTime::Failure(GetLastError()); st->hasTimes = true; + st->hasIdentity = GetFileInformationByHandle(handle, &st->identity) != 0; return 0; } 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()); - const int result = ReadTimes(handle, st); + const int result = ReadHandleInfo(handle, st); CloseHandle(handle); 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 ReadTimes(reinterpret_cast(::_get_osfhandle(fd)), st); + return ReadHandleInfo(reinterpret_cast(::_get_osfhandle(fd)), st); } static int DoLstat(const std::filesystem::path& p, NativeStat* st) { - std::error_code error; - if (std::filesystem::is_symlink(std::filesystem::symlink_status(p, error))) NotImplemented_nid_no_patch("lstat of a Windows symbolic link"); - return DoStat(p, st); + const auto attributes = GetFileAttributesW(p.c_str()); + if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) return DoStat(p, st); + std::string target; + if (!File::WindowsSymbolicLink::Read(p, target, "lstat") && !File::WindowsSymbolicLink::ReadJunction(p, target, "lstat")) + NotImplemented_nid_no_patch("lstat of a Windows reparse point other than a symbolic link"); + 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; + st->st_mode = SymbolicLinkMode; + st->st_size = static_cast<__int64>(target.size()); + st->isSymbolicLink = true; + return 0; } #else #include @@ -59,6 +86,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 && !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); @@ -81,6 +109,11 @@ static void CopyNativeStat(const NativeStat& st, FileStat* sb) { sb->st_ctim = File::WindowsFileTime::Decode(st.times.ChangeTime.QuadPart); sb->st_birthtim = File::WindowsFileTime::Decode(st.times.CreationTime.QuadPart); } + if (st.hasIdentity) { + sb->st_dev = st.identity.dwVolumeSerialNumber; + sb->st_ino = WindowsFileNumber_nid_no_patch(st.identity); + sb->st_nlink = static_cast(std::min(st.identity.nNumberOfLinks, 0xffff)); + } #else sb->st_dev = static_cast(st.st_dev); sb->st_ino = static_cast(st.st_ino); @@ -111,11 +144,16 @@ static void CopyNativeStat(const NativeStat& st, FileStat* sb) { namespace File { 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) { @@ -127,6 +165,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 2e323a8d90..7743c86733 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -1,13 +1,24 @@ #include "prx/libkernel/File/include/FileFlags.hpp" #include "prx/libkernel/File/include/NativeStat.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/FilesystemError.hpp" #include "prx/libc/include/GuestArena.hpp" +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/File/include/SharedMemory.hpp" #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" +#include "prx/libkernel/File/include/DescriptorControl.hpp" +#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" #include +#include #include #include #include @@ -16,23 +27,61 @@ #include #include #include +#include +#include "prx/libkernel/File/include/WindowsSymbolicLink.hpp" static int NativeOpen(const std::filesystem::path& p, int nativeFlags, std::uint16_t mode) { - return ::_wopen(p.wstring().c_str(), nativeFlags, static_cast(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; + DWORD disposition = OPEN_EXISTING; + if (nativeFlags & _O_CREAT) disposition = (nativeFlags & _O_EXCL) ? CREATE_NEW : (nativeFlags & _O_TRUNC) ? CREATE_ALWAYS : OPEN_ALWAYS; + else if (nativeFlags & _O_TRUNC) disposition = TRUNCATE_EXISTING; + const DWORD attributes = (nativeFlags & _O_CREAT) && !(mode & _S_IWRITE) ? FILE_ATTRIBUTE_READONLY : FILE_ATTRIBUTE_NORMAL; + SECURITY_ATTRIBUTES security{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE}; + const auto handle = CreateFileW(p.c_str(), desired, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, &security, disposition, attributes, nullptr); + if (handle == INVALID_HANDLE_VALUE) { + errno = WindowsOpenError_nid_no_patch(GetLastError()); + return -1; + } + const int descriptor = ::_open_osfhandle(reinterpret_cast(handle), nativeFlags & (_O_APPEND | _O_RDONLY | _O_BINARY)); + if (descriptor < 0) CloseHandle(handle); + return descriptor; } static std::int64_t NativeLseek(int fd, std::int64_t offset, int whence) { - return ::_lseeki64(fd, offset, whence); + const auto positioned = File::LockPositionedTransfer(fd); + const auto result = File::SeekSharedMemory_nid_no_patch(fd, offset, whence); + if (result != -2) return result; + const auto position = ::_lseeki64(fd, offset, whence); + if (position < 0 || GetFileType(reinterpret_cast(::_get_osfhandle(fd))) != FILE_TYPE_PIPE) return position; + errno = ESPIPE; + return -1; } static int NativeRead(int fd, void* buf, std::size_t n) { if (n > static_cast(std::numeric_limits::max())) { throw std::runtime_error("sceKernelRead: nbytes exceeds platform limit"); } - return ::_read(fd, buf, static_cast(n)); + 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) { if (n > static_cast(std::numeric_limits::max())) { throw std::runtime_error("sceKernelWrite: nbytes exceeds platform limit"); } - return ::_write(fd, buf, static_cast(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; } 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) {} @@ -43,7 +92,33 @@ static int NativeClose(int fd) { return result; } static int NativeUnlink(const std::filesystem::path& p) { - return ::_wunlink(p.wstring().c_str()); + const auto path = p.wstring(); + struct _stat64 status{}; + const bool unlocked = ::_wstat64(path.c_str(), &status) == 0 && (status.st_mode & _S_IFMT) == _S_IFREG && + !(status.st_mode & _S_IWRITE) && ::_wchmod(path.c_str(), _S_IREAD | _S_IWRITE) == 0; + if (::_wunlink(path.c_str()) == 0) return 0; + const int error = errno; + if (unlocked) ::_wchmod(path.c_str(), _S_IREAD); + 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; @@ -66,13 +141,15 @@ static int NativeOpen(const std::filesystem::path& p, int nativeFlags, std::uint return ::open(p.c_str(), nativeFlags, static_cast(mode)); } static std::int64_t NativeLseek(int fd, std::int64_t offset, int whence) { - return ::lseek(fd, static_cast(offset), whence); + const auto result = File::SeekSharedMemory_nid_no_patch(fd, offset, whence); + return result == -2 ? ::lseek(fd, static_cast(offset), whence) : result; } static std::int64_t NativeRead(int fd, void* buf, std::size_t n) { return ::read(fd, buf, n); } static std::int64_t NativeWrite(int fd, const void* buf, std::size_t n) { - return ::write(fd, buf, n); + const auto result = File::WriteSharedMemory_nid_no_patch(fd, buf, n); + return result == -2 ? ::write(fd, buf, n) : result; } static int NativeClose(int fd) { return ::close(fd); } static int NativeUnlink(const std::filesystem::path& p) { @@ -95,35 +172,143 @@ static int MapFlags(int sceFlags) { } #endif +extern "C" int* APS5_VABI __error_nid_postfix(); +extern "C" int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...); + 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; +#ifdef EDEADLK + else if (error == EDEADLK) guest = 11; +#endif +#ifdef EDQUOT + else if (error == EDQUOT) guest = 69; +#endif +#ifdef EOVERFLOW + else if (error == EOVERFLOW) guest = 84; +#endif + return static_cast(0x80020000u | static_cast(guest)); +} + +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; + const auto status = std::filesystem::status(native, error); + if (std::filesystem::exists(status) && !std::filesystem::is_directory(status)) return SceErrorFromErrno(ENOTDIR); + } +#endif + if ((flags & SCE_KERNEL_O_NOFOLLOW) != 0 && (flags & (SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL)) != (SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL)) { + 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; + if (std::filesystem::is_directory(directory, error)) { + 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 == 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); + File::RememberDescriptor(fd, flags); + return fd; +} + 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); - auto native = ResolvePath_nid_no_patch(path); - int fd = NativeOpen(native, MapFlags(flags), mode); -#ifdef _WIN32 - if (fd < 0 && errno != ENOENT) { - std::error_code error; - if (std::filesystem::is_directory(native, error)) fd = File::OpenDirectoryDescriptor(native); + if (path != nullptr && File::IsRandomDevicePath(path)) { + if ((flags & SCE_KERNEL_O_ACCMODE) != SCE_KERNEL_O_RDONLY) throw std::runtime_error(std::string(__func__) + ": writing to " + path + " is not implemented"); + const int fd = File::OpenRandomDevice(); + if (fd < 0) return SceErrorFromErrno(errno); + File::RememberDescriptor(fd, flags); + return fd; } -#endif - if (fd < 0) { - return SceErrorFromErrno(errno); + std::filesystem::path native; + const bool module = (flags & (SCE_KERNEL_O_ACCMODE | SCE_KERNEL_O_CREAT)) == SCE_KERNEL_O_RDONLY; + if (const int error = ResolvePathError_nid_no_patch(path, native, module, 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; } - if ((flags & SCE_KERNEL_O_ACCMODE) != SCE_KERNEL_O_RDONLY || (flags & (SCE_KERNEL_O_CREAT | SCE_KERNEL_O_TRUNC))) - RecordWrittenPath_nid_no_patch(native); - return fd; + 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); + return OpenNative(std::filesystem::path(reinterpret_cast(base)) / std::filesystem::path(reinterpret_cast(path)), flags, mode); } 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); + ForgetJitSharedMemory(d); #ifdef _WIN32 File::ForgetDirectoryDescriptor(d); + File::ForgetFileLock(d); #endif if (NativeClose(d) != 0) { if (errno == EBADF) return SCE_KERNEL_ERROR_EBADF; @@ -133,71 +318,150 @@ int APS5_VABI sceKernelClose(int d) { } std::int64_t APS5_VABI sceKernelRead(int d, void* buf, std::size_t nbytes) { + if (GuestSockets::IsOpen(d)) { + const auto n = GuestSockets::Read(d, buf, nbytes); + return n < 0 ? SceKernelError(*__error_nid_postfix()) : n; + } if (buf == nullptr) { - throw std::invalid_argument(std::string(__func__) + ": buf is null"); + if (nbytes != 0) return SceIoErrorFromErrno(EFAULT); + char empty = 0; + return sceKernelRead(d, &empty, 0); } const GuestArena::HostWrite destination(buf, nbytes); if (!destination.Open()) errno = EFAULT; + else if (File::ReadRandomDevice(d, buf, nbytes)) return static_cast(nbytes); + else if (const auto archived = File::ReadArchivedFile(d, buf, nbytes, nullptr)) return *archived; auto n = destination.Open() ? NativeRead(d, buf, nbytes) : -1; - if (n < 0) { - throw std::runtime_error(std::string(__func__) + ": read failed, fd=" + std::to_string(d) + ", errno=" + std::to_string(errno)); - } + if (n < 0) return SceIoErrorFromErrno(errno); return static_cast(n); } std::int64_t APS5_VABI sceKernelWrite(int d, const void* buf, std::size_t nbytes) { + if (GuestSockets::IsOpen(d)) { + const auto n = GuestSockets::Write(d, buf, nbytes); + return n < 0 ? SceKernelError(*__error_nid_postfix()) : n; + } if (buf == nullptr) { - throw std::invalid_argument(std::string(__func__) + ": buf is null"); + if (nbytes != 0) return SceIoErrorFromErrno(EFAULT); + const char empty = 0; + return sceKernelWrite(d, &empty, 0); } + GuestArena::GuestArenaPrepareHostRead_nid_no_patch(buf, nbytes); auto n = NativeWrite(d, buf, nbytes); - if (n < 0) { - throw std::runtime_error(std::string(__func__) + ": write failed, fd=" + std::to_string(d) + ", errno=" + std::to_string(errno)); - } + if (n < 0) return SceIoErrorFromErrno(errno); return static_cast(n); } 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)); } - std::int64_t result = NativeLseek(d, offset, whence); - if (result < 0) { - throw std::runtime_error(std::string(__func__) + ": lseek failed, fd=" + std::to_string(d) + ", errno=" + std::to_string(errno)); + 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); + return *directory; } +#endif + std::int64_t result = NativeLseek(d, offset, whence); + if (result < 0) return SceIoErrorFromErrno(errno); 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 (sb == nullptr) { - throw std::invalid_argument(std::string(__func__) + ": sb is null"); - } - const auto native = ResolvePath_nid_no_patch(path); +constexpr int GuestEfault = 14; + +static int StatNative(const std::filesystem::path& native, bool directory, FileStat* sb) { std::error_code error; - if (!std::filesystem::exists(native, error)) { - return SceErrorFromErrno(2); + constexpr int GuestEnotdir = 20; + 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; } - File::FillFileStat(native, sb); + 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); + 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"); - } - auto native = ResolvePath_nid_no_patch(path); + if (path == nullptr) return SceErrorFromErrno(GuestEfault); + if (*path == '\0') return SceErrorFromErrno(2); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); + 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; } -int APS5_VABI sceKernelFcntl() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceKernelFcntl(int d, int command, ...) { + 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); +#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); +#endif + } + int result; + if (GuestSockets::IsOpen(d)) { + 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); + } + return result < 0 ? SceKernelError(*__error_nid_postfix()) : result; } } diff --git a/core/libs/prx/libkernel/File/src/OpenFileLimit.cpp b/core/libs/prx/libkernel/File/src/OpenFileLimit.cpp new file mode 100644 index 0000000000..a5af366a2e --- /dev/null +++ b/core/libs/prx/libkernel/File/src/OpenFileLimit.cpp @@ -0,0 +1,48 @@ +#if defined(__linux__) || defined(__APPLE__) + +#include +#include +#include +#ifdef __APPLE__ +#include +#endif + +namespace { + +struct OpenFileLimit { + OpenFileLimit() { + rlimit limit{}; + if (getrlimit(RLIMIT_NOFILE, &limit) != 0) throw std::system_error(errno, std::generic_category(), "read open file limit"); + 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/RandomDevice.cpp b/core/libs/prx/libkernel/File/src/RandomDevice.cpp new file mode 100644 index 0000000000..1f4cfdda97 --- /dev/null +++ b/core/libs/prx/libkernel/File/src/RandomDevice.cpp @@ -0,0 +1,62 @@ +#include "prx/libkernel/File/include/RandomDevice.hpp" +#include "prx/libc/include/HostEntropy.hpp" + +#include +#include + +#ifdef _WIN32 +#include +#include +#else +#include +#include +#endif + +namespace { + +std::mutex g_mutex; +std::set g_descriptors; + +} + +namespace File { + +bool IsRandomDevicePath(std::string_view path) { + return path == "/dev/random" || path == "/dev/urandom"; +} + +int OpenRandomDevice() { +#ifdef _WIN32 + const int fd = ::_open("NUL", _O_RDONLY | _O_BINARY); +#else + const int fd = ::open("/dev/null", O_RDONLY); +#endif + if (fd < 0) return -1; + std::lock_guard lock(g_mutex); + g_descriptors.insert(fd); + return fd; +} + +bool IsRandomDevice(int fd) { + std::lock_guard lock(g_mutex); + return g_descriptors.contains(fd); +} + +bool ReadRandomDevice(int fd, void* buf, std::size_t nbytes) { + std::lock_guard lock(g_mutex); + if (!g_descriptors.contains(fd)) return false; + HostEntropy_nid_no_patch(buf, nbytes); + return true; +} + +void RememberRandomDevice(int fd) { + std::lock_guard lock(g_mutex); + g_descriptors.insert(fd); +} + +void ForgetRandomDevice(int fd) { + std::lock_guard lock(g_mutex); + g_descriptors.erase(fd); +} + +} 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/SharedMemory.cpp b/core/libs/prx/libkernel/File/src/SharedMemory.cpp new file mode 100644 index 0000000000..c5b133d941 --- /dev/null +++ b/core/libs/prx/libkernel/File/src/SharedMemory.cpp @@ -0,0 +1,498 @@ +#include "prx/libc/include/General.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" +#include "prx/libkernel/File/include/SharedMemory.hpp" +#include "prx/libc/include/CreationMask.hpp" +#include "prx/libkernel/File/include/File.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#include +#include +#include +#include +#else +#include "nid/Sha1.hpp" +#include +#include +#include +#include +#include +#include +#include +#endif + +extern "C" int* APS5_VABI __error_nid_postfix(); + +namespace { + +bool IsHostSharedMemoryDescriptor(int descriptor) { +#ifdef __linux__ + char descriptorPath[64]; + std::snprintf(descriptorPath, sizeof(descriptorPath), "/proc/self/fd/%d", descriptor); + char objectPath[4096]; + const auto length = ::readlink(descriptorPath, objectPath, sizeof(objectPath) - 1); + if (length < 0) return false; + objectPath[length] = '\0'; + return std::string_view(objectPath).starts_with("/dev/shm/anyps5-shm-") || + std::string_view(objectPath).starts_with("/memfd:anyps5 shm_open"); +#else + (void)descriptor; + return false; +#endif +} + +class HostShmLock { +public: + HostShmLock() { +#ifdef __linux__ + descriptor = ::open("/dev/shm", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (descriptor < 0 || ::flock(descriptor, LOCK_EX) != 0) error = errno; +#endif + } + + ~HostShmLock() { +#ifdef __linux__ + const int savedError = errno; + if (descriptor >= 0) { + if (!error) ::flock(descriptor, LOCK_UN); + ::close(descriptor); + } + errno = savedError; +#endif + } + + int Error() const { return error; } + +private: + int descriptor = -1; + int error = 0; +}; + +constexpr int GuestNoMemory = 12; +constexpr int GuestInvalid = 22; +constexpr int GuestTooManyFilesInSystem = 23; +constexpr int GuestTooManyFiles = 24; +constexpr int GuestCloseOnExec = 0x00100000; +constexpr int AcceptedFlags = SCE_KERNEL_O_ACCMODE | SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL | SCE_KERNEL_O_TRUNC | GuestCloseOnExec; + +const char* const anonymousObject = reinterpret_cast(1); + +int Fail(int error) { + *__error_nid_postfix() = error; + return -1; +} + +int GuestError(int error) { + switch (error) { + case EMFILE: + return GuestTooManyFiles; + case ENFILE: + return GuestTooManyFilesInSystem; + case ENOMEM: + return GuestNoMemory; + default: + return 0; + } +} + +#ifndef _WIN32 +int NamedGuestError(int error) { + switch (error) { + case ENOENT: + return 2; + case EINTR: + return 4; + case EACCES: + return 13; + case EFAULT: + return 14; + case EEXIST: + return 17; + case EINVAL: + return GuestInvalid; + case ENOSPC: + return 28; + case EROFS: + return 30; + case ENAMETOOLONG: + return 63; + default: + return GuestError(error); + } +} + +std::string NamedHostPath(const char* path, int& error) { + if (path == nullptr) { error = EFAULT; return {}; } + std::size_t length = 0; + while (length < 1024 && path[length] != '\0') ++length; + if (length == 1024) { error = ENAMETOOLONG; return {}; } + if (length == 0 || path[0] != '/') { error = EINVAL; return {}; } + const auto digest = Nid::Sha1(std::vector(path, path + length)); + std::string hostName = "/anyps5-shm-"; + for (const auto byte : digest) { + hostName += "0123456789abcdef"[byte >> 4]; + hostName += "0123456789abcdef"[byte & 15]; + } + error = 0; + return hostName; +} + +int OpenNamedFile(const char* path, int flags, int mode) { + int validationError = 0; + const auto hostName = NamedHostPath(path, validationError); + if (validationError != 0) return Fail(NamedGuestError(validationError) ? NamedGuestError(validationError) : GuestInvalid); + HostShmLock namespaceLock; + if (namespaceLock.Error() != 0) { + errno = namespaceLock.Error(); + if (const int guest = NamedGuestError(errno)) return Fail(guest); + throw std::runtime_error("shm_open: locking the host shared-memory namespace failed, errno=" + std::to_string(errno)); + } + int hostFlags = (flags & SCE_KERNEL_O_ACCMODE) == SCE_KERNEL_O_RDWR ? O_RDWR : O_RDONLY; + if (flags & SCE_KERNEL_O_CREAT) hostFlags |= O_CREAT; + if (flags & SCE_KERNEL_O_EXCL) hostFlags |= O_EXCL; + if ((flags & SCE_KERNEL_O_TRUNC) && (flags & SCE_KERNEL_O_ACCMODE) == SCE_KERNEL_O_RDWR) hostFlags |= O_TRUNC; + const auto permissions = (mode & 0777) & ~File::GetCreationMask_nid_no_patch(); + bool created = false; + int descriptor; + if (flags & SCE_KERNEL_O_CREAT) { + descriptor = ::shm_open(hostName.c_str(), hostFlags | O_CLOEXEC | O_EXCL, permissions); + created = descriptor >= 0; + if (descriptor < 0 && errno == EEXIST && !(flags & SCE_KERNEL_O_EXCL)) { + descriptor = ::shm_open(hostName.c_str(), (hostFlags & ~(O_CREAT | O_EXCL)) | O_CLOEXEC, 0); + } + } else { + descriptor = ::shm_open(hostName.c_str(), hostFlags | O_CLOEXEC, 0); + } + if (descriptor >= 0) { + if (!created || ::fchmod(descriptor, permissions) == 0) return descriptor; + const int error = errno; + ::close(descriptor); + ::shm_unlink(hostName.c_str()); + if (const int guest = NamedGuestError(error)) return Fail(guest); + throw std::runtime_error("shm_open: setting guest creation permissions failed, errno=" + std::to_string(error)); + } + const int error = errno; + if (const int guest = NamedGuestError(error)) return Fail(guest); + throw std::runtime_error("shm_open: opening the host named object failed, errno=" + std::to_string(error)); +} +#endif + +int CreateAnonymousFile() { +#ifdef _WIN32 + static std::atomic sequence{0}; + const auto directory = std::filesystem::temp_directory_path(); + const auto seed = std::to_string(std::random_device{}()); + for (int attempt = 0; attempt < 64; ++attempt) { + const auto path = directory / ("anyps5-shm-" + seed + "-" + std::to_string(sequence++)); + int descriptor = -1; + const int error = _wsopen_s(&descriptor, path.c_str(), + _O_CREAT | _O_EXCL | _O_RDWR | _O_BINARY | _O_TEMPORARY | _O_SHORT_LIVED | _O_NOINHERIT, + _SH_DENYNO, _S_IREAD | _S_IWRITE); + if (error == 0) return descriptor; + if (error != EEXIST) { + errno = error; + return -1; + } + } + errno = EEXIST; + return -1; +#else + const int descriptor = memfd_create("anyps5 shm_open", MFD_CLOEXEC); + return descriptor; +#endif +} + +} + +namespace File { + +bool IsSharedMemoryDescriptor_nid_no_patch(int descriptor) { + return IsHostSharedMemoryDescriptor(descriptor); +} + +namespace { + +class SharedMemoryLock { +public: + explicit SharedMemoryLock(int value) { +#ifdef __linux__ + struct stat originalStatus{}; + if (::fstat(value, &originalStatus) != 0) { shared = true; error = errno; return; } +#endif + if (!IsHostSharedMemoryDescriptor(value)) { +#ifdef __linux__ + struct stat currentStatus{}; + if (::fstat(value, ¤tStatus) != 0) { shared = true; error = errno; return; } + if (currentStatus.st_dev != originalStatus.st_dev || currentStatus.st_ino != originalStatus.st_ino) { + shared = true; + error = EBADF; + } +#endif + return; + } + shared = true; + hostLock.emplace(); + if (hostLock->Error() != 0) { error = hostLock->Error(); return; } + descriptor = Duplicate(value); + if (descriptor < 0) { error = errno; return; } + if (!IsHostSharedMemoryDescriptor(descriptor)) { error = EBADF; return; } + if (!Refresh()) { error = errno; return; } +#ifdef __linux__ + if (status.st_dev != originalStatus.st_dev || status.st_ino != originalStatus.st_ino) error = EBADF; +#endif + } + + ~SharedMemoryLock() { + if (descriptor >= 0) { + const int savedError = errno; +#ifdef _WIN32 + ::_close(descriptor); +#else + ::close(descriptor); +#endif + errno = savedError; + } + } + + bool IsShared() const { return shared; } + int Error() const { return error; } + int Descriptor() const { return descriptor; } + bool Refresh() { +#ifdef _WIN32 + struct _stat64 nativeStatus{}; + if (::_fstat64(descriptor, &nativeStatus) != 0) return false; + status.st_size = nativeStatus.st_size; + return true; +#else + return ::fstat(descriptor, &status) == 0; +#endif + } + struct stat status{}; + +private: + static int Duplicate(int value) { +#ifdef _WIN32 + return ::_dup(value); +#else + return ::fcntl(value, F_DUPFD_CLOEXEC, 0); +#endif + } + + int descriptor = -1; + std::optional hostLock; + bool shared = false; + int error = 0; +}; +} + +std::int64_t WriteSharedMemory_nid_no_patch(int descriptor, const void* buffer, std::size_t size) { + SharedMemoryLock guard(descriptor); + if (!guard.IsShared()) return -2; + if (guard.Error() != 0) { errno = guard.Error(); return -1; } + if (!guard.Refresh()) return -1; + const auto fd = guard.Descriptor(); +#ifndef _WIN32 + const int access = ::fcntl(fd, F_GETFL); + if (access < 0) return -1; + if ((access & O_ACCMODE) == O_RDONLY) { errno = EBADF; return -1; } +#endif + const auto& status = guard.status; +#ifdef _WIN32 + const auto position = ::_lseeki64(fd, 0, SEEK_CUR); +#else + const auto position = ::lseek(fd, 0, SEEK_CUR); +#endif + if (position < 0) return -1; + if (position >= status.st_size) return 0; + const auto count = std::min(size, static_cast(status.st_size - position)); +#ifdef _WIN32 + const auto result = ::_write(fd, buffer, static_cast(std::min(count, std::numeric_limits::max()))); +#else + const auto result = ::write(fd, buffer, static_cast(count)); +#endif + return static_cast(result); +} + +std::int64_t PwriteSharedMemory_nid_no_patch(int descriptor, const void* buffer, std::size_t size, std::int64_t offset) { + SharedMemoryLock guard(descriptor); + if (!guard.IsShared()) return -2; + if (guard.Error() != 0) { errno = guard.Error(); return -1; } + if (!guard.Refresh()) return -1; + const auto fd = guard.Descriptor(); +#ifndef _WIN32 + const int access = ::fcntl(fd, F_GETFL); + if (access < 0) return -1; + if ((access & O_ACCMODE) == O_RDONLY) { errno = EBADF; return -1; } +#endif + const auto& status = guard.status; + if (offset < 0) { errno = EINVAL; return -1; } + if (offset >= status.st_size) return 0; + const auto count = std::min(size, static_cast(status.st_size - offset)); +#ifdef _WIN32 + const auto result = NativePositioned_nid_no_patch(fd, const_cast(buffer), static_cast(count), offset, true); +#else + const auto result = ::pwrite(fd, buffer, static_cast(count), static_cast(offset)); +#endif + return static_cast(result); +} + +std::int64_t SeekSharedMemory_nid_no_patch(int descriptor, std::int64_t offset, int whence) { + SharedMemoryLock guard(descriptor); + if (!guard.IsShared()) return -2; + if (guard.Error() != 0) { errno = guard.Error(); return -1; } + if (!guard.Refresh()) return -1; + const auto fd = guard.Descriptor(); + const auto& status = guard.status; + std::int64_t base = 0; + if (whence == SEEK_CUR) { +#ifdef _WIN32 + base = ::_lseeki64(fd, 0, SEEK_CUR); +#else + base = ::lseek(fd, 0, SEEK_CUR); +#endif + if (base < 0) return -1; + } else if (whence == SEEK_END) { + base = static_cast(status.st_size); + } else if (whence != SEEK_SET) { + errno = EINVAL; + return -1; + } + if ((offset > 0 && base > std::numeric_limits::max() - offset) || + (offset < 0 && base < std::numeric_limits::min() - offset)) { + errno = EOVERFLOW; + return -1; + } + const auto target = base + offset; + if (target < 0 || target > status.st_size) { + errno = EINVAL; + return -1; + } +#ifdef _WIN32 + return ::_lseeki64(fd, target, SEEK_SET); +#else + return ::lseek(fd, static_cast(target), SEEK_SET); +#endif +} + +#ifndef _WIN32 +static std::int64_t TransferSharedMemoryVectors(int descriptor, const struct iovec* vectors, int count, + std::int64_t offset, bool positioned) { + SharedMemoryLock guard(descriptor); + if (!guard.IsShared()) return -2; + if (guard.Error() != 0) { errno = guard.Error(); return -1; } + const auto fd = guard.Descriptor(); + const int access = ::fcntl(fd, F_GETFL); + if (access < 0) return -1; + if ((access & O_ACCMODE) == O_RDONLY) { errno = EBADF; return -1; } + if (positioned && offset < 0) { errno = EINVAL; return -1; } + struct stat status{}; + if (::fstat(fd, &status) != 0) return -1; + std::uint64_t total = 0; + constexpr auto maxTransfer = static_cast(std::numeric_limits::max()); + for (int i = 0; i < count; ++i) { + if (vectors[i].iov_len > maxTransfer - total) { errno = EINVAL; return -1; } + total += vectors[i].iov_len; + } + std::uint64_t available = 0; + if (positioned) { + if (offset < status.st_size) available = static_cast(status.st_size - offset); + if (total > static_cast(std::numeric_limits::max() - offset)) { errno = EINVAL; return -1; } + } else { + const auto position = ::lseek(fd, 0, SEEK_CUR); + if (position < 0) return -1; + if (position < status.st_size) available = static_cast(status.st_size - position); + } + std::vector bounded; + bounded.reserve(static_cast(count)); + for (int i = 0; i < count && available > 0; ++i) { + const auto length = static_cast(std::min(vectors[i].iov_len, available)); + if (length != 0) bounded.push_back({vectors[i].iov_base, length}); + available -= length; + } + if (bounded.empty()) return 0; + return positioned ? ::pwritev(fd, bounded.data(), static_cast(bounded.size()), static_cast(offset)) + : ::writev(fd, bounded.data(), static_cast(bounded.size())); +} + +std::int64_t WritevSharedMemory_nid_no_patch(int descriptor, const struct iovec* vectors, int count) { + return TransferSharedMemoryVectors(descriptor, vectors, count, 0, false); +} + +std::int64_t PwritevSharedMemory_nid_no_patch(int descriptor, const struct iovec* vectors, int count, std::int64_t offset) { + return TransferSharedMemoryVectors(descriptor, vectors, count, offset, true); +} +#endif + +int TruncateSharedMemory_nid_no_patch(int descriptor, std::int64_t length) { + SharedMemoryLock guard(descriptor); + if (!guard.IsShared()) return -2; + if (guard.Error() != 0) { errno = guard.Error(); return -1; } + const auto fd = guard.Descriptor(); +#ifdef _WIN32 + const int result = static_cast(::_chsize_s(fd, length)); +#else + const int result = ::ftruncate(fd, static_cast(length)); +#endif + return result; +} + +} + +extern "C" int APS5_VABI shm_open_nid_postfix(const char* path, int flags, int mode) { + const int accessMode = flags & SCE_KERNEL_O_ACCMODE; + if (accessMode != SCE_KERNEL_O_RDONLY && accessMode != SCE_KERNEL_O_RDWR) return Fail(GuestInvalid); + if ((flags & ~AcceptedFlags) != 0) return Fail(GuestInvalid); + if (path != anonymousObject) { +#ifdef _WIN32 + NotImplemented_nid_no_patch("shm_open of a named object"); +#else + return OpenNamedFile(path, flags, mode); +#endif + } + if (accessMode == SCE_KERNEL_O_RDONLY) return Fail(GuestInvalid); + const int descriptor = CreateAnonymousFile(); + if (descriptor >= 0) return descriptor; + const int error = errno; + if (const int guest = GuestError(error)) return Fail(guest); + throw std::runtime_error("shm_open: creating the host anonymous file failed, errno=" + std::to_string(error)); +} + +extern "C" int APS5_VABI shm_unlink_nid_postfix(const char* path) { +#ifdef _WIN32 + (void)path; + NotImplemented_nid_no_patch("shm_unlink of a named object"); + return 0; +#else + int validationError = 0; + const auto hostName = NamedHostPath(path, validationError); + if (validationError != 0) return Fail(NamedGuestError(validationError) ? NamedGuestError(validationError) : GuestInvalid); + HostShmLock namespaceLock; + if (namespaceLock.Error() != 0) { + errno = namespaceLock.Error(); + if (const int guest = NamedGuestError(errno)) return Fail(guest); + throw std::runtime_error("shm_unlink: locking the host shared-memory namespace failed, errno=" + std::to_string(errno)); + } + const auto objectPath = "/dev/shm" + hostName; + if (::faccessat(AT_FDCWD, objectPath.c_str(), R_OK | W_OK, AT_EACCESS) != 0) { + const int error = errno; + if (const int guest = NamedGuestError(error)) return Fail(guest); + throw std::runtime_error("shm_unlink: checking shared-memory permissions failed, errno=" + std::to_string(error)); + } + if (::shm_unlink(hostName.c_str()) == 0) return 0; + const int error = errno; + if (const int guest = NamedGuestError(error)) return Fail(guest); + throw std::runtime_error("shm_unlink: removing the host named object failed, errno=" + std::to_string(error)); +#endif +} diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 60a0abd66b..2146bbe2d6 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -4,40 +4,88 @@ #include #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" +#include "prx/libkernel/File/include/SharedMemory.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" #include "prx/libkernel/File/include/NativeStat.hpp" +#include "prx/libkernel/File/include/KernelStatfs.hpp" #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" +#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" +#include "prx/libkernel/Socket/include/SocketPoll.hpp" +#include #include #include #include #include #include +#include +#include #include #include #include +#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; +static constexpr std::uint32_t KERNEL_STATFS_VERSION = 0x20030518; +static constexpr std::uint64_t KERNEL_MNT_RDONLY = 0x1; +static constexpr std::uint64_t KERNEL_MNT_NOEXEC = 0x4; +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) { + const auto previous = _set_thread_local_invalid_parameter_handler(IgnoreDescriptorParameter); + const auto result = call(); + _set_thread_local_invalid_parameter_handler(previous); + return result; +} static int NativeRmdir(const std::filesystem::path& path) { return ::_wrmdir(path.wstring().c_str()); } +static bool RemoveEmptyDirectory(const std::filesystem::path& path) { + struct _stat64 status{}; + const bool unlocked = ::_wstat64(path.c_str(), &status) == 0 && !(status.st_mode & _S_IWRITE) && + ::_wchmod(path.c_str(), _S_IREAD | _S_IWRITE) == 0; + std::error_code error; + if (std::filesystem::remove(path, error)) return true; + if (unlocked) ::_wchmod(path.c_str(), _S_IREAD); + return false; +} static int NativeMkdir(const std::filesystem::path& path, std::uint16_t mode) { (void)mode; return ::_wmkdir(path.wstring().c_str()); @@ -46,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; @@ -59,19 +106,60 @@ 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) { - return static_cast(::_chsize_s(descriptor, 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) 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); + if (descriptor < 0) return errno; + const int error = NativeFtruncate(descriptor, length); + ::_close(descriptor); + return error; } static int SetTimes(HANDLE handle, const KernelTimeval* times) { FILETIME access{}, modified{}; @@ -79,14 +167,16 @@ 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; } return SetFileTime(handle, nullptr, &access, &modified) ? 0 : File::WindowsFileTime::Failure(GetLastError()); } -static int NativeUtimes(const std::filesystem::path& path, const KernelTimeval* times) { +static int NativeUtimes(const std::filesystem::path& path, const KernelTimeval* times, bool followLink = true) { const auto handle = CreateFileW(path.c_str(), FILE_WRITE_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, - nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | (followLink ? 0 : FILE_FLAG_OPEN_REPARSE_POINT), nullptr); if (handle == INVALID_HANDLE_VALUE) return File::WindowsFileTime::Failure(GetLastError()); const int result = SetTimes(handle, times); CloseHandle(handle); @@ -103,33 +193,71 @@ 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; } -static int NativeFlock(int descriptor, int operation) { - HANDLE handle = reinterpret_cast(::_get_osfhandle(descriptor)); - if (handle == INVALID_HANDLE_VALUE) { - return -1; +static int NativeSync(int descriptor) { + constexpr DWORD share = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; + HANDLE handle = INVALID_HANDLE_VALUE; + if (const auto directory = File::DirectoryDescriptorPath(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; } + handle = ReOpenFile(original, GENERIC_WRITE, share, 0); + errno = previous; } - OVERLAPPED overlapped{}; - if (operation & 8) { - return ::UnlockFileEx(handle, 0, MAXDWORD, MAXDWORD, &overlapped) ? 0 : -1; - } - DWORD flags = 0; - if (operation & 2) flags |= LOCKFILE_EXCLUSIVE_LOCK; - if (operation & 4) flags |= LOCKFILE_FAIL_IMMEDIATELY; - return ::LockFileEx(handle, flags, 0, MAXDWORD, MAXDWORD, &overlapped) ? 0 : -1; + if (handle == INVALID_HANDLE_VALUE) return GetLastError() == ERROR_ACCESS_DENIED ? 0 : File::WindowsFileTime::Failure(GetLastError()); + const int result = FlushFileBuffers(handle) ? 0 : File::WindowsFileTime::Failure(GetLastError()); + CloseHandle(handle); + return result; } -std::int64_t NativePositioned_nid_no_patch(int descriptor, void* buf, std::size_t nbytes, std::int64_t offset, bool write) { +static int NativeFlock(int descriptor, int operation) { + return File::Flock(descriptor, operation); +} +struct PositionedLock { + std::mutex mutex; +}; +std::mutex g_positionedTableLock; +std::unordered_map> g_positionedLocks; +std::unique_lock File::LockPositionedTransfer(int descriptor) { + std::shared_ptr entry; + { + std::lock_guard table(g_positionedTableLock); + auto& slot = g_positionedLocks[descriptor]; + if (!slot) slot = std::make_shared(); + entry = slot; + } + return std::unique_lock(entry->mutex); +} +void File::SharePositionedTransfer(int from, int to) { + std::lock_guard table(g_positionedTableLock); + auto& source = g_positionedLocks[from]; + if (!source) source = std::make_shared(); + g_positionedLocks[to] = source; +} +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"); } - const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); + if (write) { + const auto result = File::PwriteSharedMemory_nid_no_patch(descriptor, buf, nbytes, offset); + if (result != -2) return result; + } + const auto handle = reinterpret_cast(WithoutParameterHandler([descriptor] { return ::_get_osfhandle(descriptor); })); if (handle == INVALID_HANDLE_VALUE) { errno = EBADF; return -1; } + 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 = ESPIPE; @@ -147,21 +275,64 @@ std::int64_t NativePositioned_nid_no_patch(int descriptor, void* buf, std::size_ } if (!ok) { if (!write && error == ERROR_HANDLE_EOF) return 0; - errno = EIO; + errno = error == ERROR_ACCESS_DENIED ? EBADF : EIO; return -1; } 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"); } + 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; } +static bool NativePipeHasData(int descriptor) { + const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); + DWORD available = 0; + return handle != INVALID_HANDLE_VALUE && ::GetFileType(handle) == FILE_TYPE_PIPE + && ::PeekNamedPipe(handle, nullptr, 0, nullptr, &available, nullptr) && available != 0; +} static std::int64_t NativePread(int descriptor, void* buf, std::size_t nbytes, std::int64_t offset) { return NativePositioned_nid_no_patch(descriptor, buf, nbytes, offset, false); } @@ -170,6 +341,7 @@ static std::int64_t NativePwrite(int descriptor, const void* buf, std::size_t nb } #else #include +#include #include #include #include @@ -179,6 +351,10 @@ static std::int64_t NativePwrite(int descriptor, const void* buf, std::size_t nb static int NativeRmdir(const std::filesystem::path& path) { return ::rmdir(path.c_str()); } +static bool RemoveEmptyDirectory(const std::filesystem::path& path) { + std::error_code error; + return std::filesystem::remove(path, error); +} static int NativeMkdir(const std::filesystem::path& path, std::uint16_t mode) { return ::mkdir(path.c_str(), static_cast(mode)); } @@ -189,13 +365,17 @@ static int NativeFchmod(int descriptor, int mode) { return ::fchmod(descriptor, static_cast(mode)); } static int NativeFtruncate(int descriptor, std::int64_t length) { - return ::ftruncate(descriptor, static_cast(length)); + const int result = File::TruncateSharedMemory_nid_no_patch(descriptor, length); + return result == -2 ? ::ftruncate(descriptor, static_cast(length)) : result; } -static int NativeUtimes(const std::filesystem::path& path, const KernelTimeval* times) { - if (times == nullptr) return ::utimes(path.c_str(), nullptr); +static int NativeTruncate(const std::filesystem::path& path, std::int64_t length) { + return ::truncate(path.c_str(), static_cast(length)) == 0 ? 0 : errno; +} +static int NativeUtimes(const std::filesystem::path& path, const KernelTimeval* times, bool followLink = true) { + if (times == nullptr) return followLink ? ::utimes(path.c_str(), nullptr) : ::lutimes(path.c_str(), nullptr); struct timeval values[2]{{static_cast(times[0].tv_sec), static_cast(times[0].tv_usec)}, {static_cast(times[1].tv_sec), static_cast(times[1].tv_usec)}}; - return ::utimes(path.c_str(), values); + return followLink ? ::utimes(path.c_str(), values) : ::lutimes(path.c_str(), values); } static int NativeFutimes(int descriptor, const KernelTimeval* times) { if (times == nullptr) return ::futimes(descriptor, nullptr); @@ -210,7 +390,8 @@ static std::int64_t NativePread(int descriptor, void* buf, std::size_t nbytes, s return static_cast(::pread(descriptor, buf, nbytes, static_cast(offset))); } static std::int64_t NativePwrite(int descriptor, const void* buf, std::size_t nbytes, std::int64_t offset) { - return static_cast(::pwrite(descriptor, buf, nbytes, static_cast(offset))); + const auto result = File::PwriteSharedMemory_nid_no_patch(descriptor, buf, nbytes, offset); + return result == -2 ? static_cast(::pwrite(descriptor, buf, nbytes, static_cast(offset))) : result; } static_assert(sizeof(KernelIovec) == sizeof(struct iovec)); static_assert(offsetof(KernelIovec, base) == offsetof(struct iovec, iov_base)); @@ -220,18 +401,60 @@ static const struct iovec* NativeIovecs(const KernelIovec* iov) { } #endif +static constexpr int GUEST_EPERM = 1; static constexpr int GUEST_ENOENT = 2; static constexpr int GUEST_EIO = 5; static constexpr int GUEST_EBADF = 9; static constexpr int GUEST_EFAULT = 14; static constexpr int GUEST_EEXIST = 17; static constexpr int GUEST_EINVAL = 22; +static constexpr int GUEST_ESPIPE = 29; static constexpr int GUEST_ENAMETOOLONG = 63; static constexpr int GUEST_ENOTDIR = 20; +static constexpr int GUEST_EISDIR = 21; +static constexpr int GUEST_EWOULDBLOCK = 35; +static constexpr int GUEST_EOPNOTSUPP = 45; static constexpr int GUEST_ENOTEMPTY = 66; +static constexpr int GUEST_EDEADLK = 11; +static constexpr int GUEST_ELOOP = 62; +static constexpr int GUEST_EDQUOT = 69; +static constexpr int GUEST_EOVERFLOW = 84; + +static int SceErrorFromErrno(int error); + +static std::int64_t PositionedGuestSocketError(int descriptor) { + return GuestSockets::IsOpen(descriptor) ? SceErrorFromErrno(GUEST_ESPIPE) : 0; +} + +static bool FailPosixPositionedGuestSocket(int descriptor) { + if (!GuestSockets::IsOpen(descriptor)) return false; + errno = GUEST_ESPIPE; + return true; +} static int SceErrorFromErrno(int error) { - return static_cast(0x80020000u | static_cast(error > 0 && error <= 34 ? error : error == GUEST_ENOTEMPTY ? error : GUEST_EIO)); + int guest = error > 0 && error <= 34 ? error : error == GUEST_ENOTEMPTY ? error : GUEST_EIO; + if (error == EAGAIN) guest = GUEST_EWOULDBLOCK; +#ifdef EDEADLK + else if (error == EDEADLK) guest = GUEST_EDEADLK; +#endif + else if (error == ENAMETOOLONG) guest = GUEST_ENAMETOOLONG; + else if (error == ENOTEMPTY) guest = GUEST_ENOTEMPTY; +#ifdef ELOOP + else if (error == ELOOP) guest = GUEST_ELOOP; +#endif +#ifdef EDQUOT + else if (error == EDQUOT) guest = GUEST_EDQUOT; +#endif +#ifdef EOVERFLOW + else if (error == EOVERFLOW) guest = GUEST_EOVERFLOW; +#endif + 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(); @@ -241,21 +464,102 @@ 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; } +static std::int64_t PosixResult(std::int64_t result) { + return result < 0 ? PosixFailure(static_cast(result) & 0xffff) : result; +} + +#ifdef _WIN32 +static int NativeFstatfs(int descriptor, KernelStatfs* fs) { + 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_DISK) + throw std::runtime_error("_fstatfs: descriptor " + std::to_string(descriptor) + " is not a file on a Windows volume"); + } + 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()))) + throw std::runtime_error("_fstatfs: GetVolumePathNameW failed for " + path->string() + ", error=" + std::to_string(::GetLastError())); + DWORD sectorsPerCluster = 0, bytesPerSector = 0, freeClusters = 0, totalClusters = 0; + ULARGE_INTEGER available{}, total{}, totalFree{}; + DWORD nameMax = 0, volumeFlags = 0; + if (!::GetDiskFreeSpaceW(root.c_str(), §orsPerCluster, &bytesPerSector, &freeClusters, &totalClusters) || + !::GetDiskFreeSpaceExW(root.c_str(), &available, &total, &totalFree) || + !::GetVolumeInformationW(root.c_str(), nullptr, 0, nullptr, &nameMax, &volumeFlags, nullptr, 0)) + throw std::runtime_error("_fstatfs: volume query failed for " + path->string() + ", error=" + std::to_string(::GetLastError())); + const std::uint64_t cluster = static_cast(sectorsPerCluster) * bytesPerSector; + if (cluster == 0) throw std::runtime_error("_fstatfs: volume of " + path->string() + " reports no cluster size"); + fs->f_flags = (volumeFlags & FILE_READ_ONLY_VOLUME) ? KERNEL_MNT_RDONLY : 0; + fs->f_bsize = cluster; + fs->f_iosize = cluster; + fs->f_blocks = total.QuadPart / cluster; + fs->f_bfree = totalFree.QuadPart / cluster; + fs->f_bavail = static_cast(available.QuadPart / cluster); + 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 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 (NativeIsPipe(descriptor)) return GUEST_EINVAL; + struct statvfs host{}; + if (::fstatvfs(descriptor, &host) != 0) return SceErrorFromErrno(errno) & 0xffff; + if (host.f_flag & ST_RDONLY) fs->f_flags |= KERNEL_MNT_RDONLY; + if (host.f_flag & ST_NOSUID) fs->f_flags |= KERNEL_MNT_NOSUID; + if (host.f_flag & ST_NOEXEC) fs->f_flags |= KERNEL_MNT_NOEXEC; + fs->f_bsize = host.f_frsize != 0 ? host.f_frsize : host.f_bsize; + fs->f_iosize = host.f_bsize; + fs->f_blocks = host.f_blocks; + fs->f_bfree = host.f_bfree; + fs->f_bavail = static_cast(host.f_bavail); + fs->f_files = host.f_files; + fs->f_ffree = static_cast(host.f_favail); + fs->f_namemax = static_cast(host.f_namemax); + return 0; +} +#endif + extern "C" int APS5_VABI pipe_nid_postfix(int* descriptors) { if (!descriptors) return PosixFailure(GUEST_EFAULT); const GuestArena::HostWrite destination(descriptors, 2 * sizeof(int)); 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 if (result != 0) return PosixFailure(SceErrorFromErrno(errno) & 0xffff); + File::RememberDescriptor(native[0], 0); + File::RememberDescriptor(native[1], 0); std::memcpy(descriptors, native, sizeof(native)); return 0; } @@ -265,24 +569,145 @@ static int PathError(const char* path) { return *path == '\0' ? GUEST_ENOENT : 0; } -extern "C" { +#ifdef _WIN32 +bool DescriptorIsOpen_nid_no_patch(int descriptor) { + return descriptor >= 0 && WithoutParameterHandler([descriptor] { return ::_get_osfhandle(descriptor) == -1 ? -1 : 0; }) == 0; +} +#else +bool DescriptorIsOpen_nid_no_patch(int descriptor) { + return descriptor >= 0 && ::fcntl(descriptor, F_GETFD) >= 0; +} +#endif -int APS5_VABI chmod_nid_postfix(const char* path, int mode) { - if (path == nullptr) { - APS5_INVALID_ARG_EX; +static std::filesystem::path WithoutTrailingSeparator(const std::filesystem::path& native) { + return native.has_filename() ? native : native.parent_path(); +} + +static bool HasNonDirectoryPrefix(const std::filesystem::path& native) { + std::error_code error; + for (auto prefix = WithoutTrailingSeparator(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); } - auto native = ResolvePath_nid_no_patch(path); - if (NativeChmod(native, mode) != 0) { - throw std::runtime_error(std::string(__func__) + ": chmod failed for " + native.string() + ", errno=" + std::to_string(errno)); + return false; +} + +extern "C" int APS5_VABI dup_nid_postfix(int d); +extern "C" int APS5_VABI close_nid_postfix(int d); + +int File::DuplicateFrom(int fd, int start) { +#ifdef _WIN32 + std::vector below; + int duplicate; + while ((duplicate = dup_nid_postfix(fd)) >= 0 && duplicate < start) below.push_back(duplicate); + const int error = *__error_nid_postfix(); + for (const int descriptor : below) close_nid_postfix(descriptor); + return duplicate < 0 ? PosixFailure(error) : duplicate; +#else + const int duplicate = ::fcntl(fd, F_DUPFD, start); + if (duplicate < 0) return PosixFailure(SceErrorFromErrno(errno) & 0xffff); + if (File::IsRandomDevice(fd)) File::RememberRandomDevice(duplicate); + File::ShareDescriptor(fd, duplicate); + return duplicate; +#endif +} + +static int LookupError(const std::filesystem::path& native) { + std::error_code error; + const auto status = std::filesystem::status(native, error); + if (std::filesystem::exists(status)) return native.has_filename() || std::filesystem::is_directory(status) ? 0 : GUEST_ENOTDIR; + if (status.type() != std::filesystem::file_type::not_found) return SceErrorFromErrno(error.default_error_condition().value()) & 0xffff; + const bool fileWithSeparator = !native.has_filename() && std::filesystem::exists(WithoutTrailingSeparator(native), error); + return fileWithSeparator || HasNonDirectoryPrefix(native) ? GUEST_ENOTDIR : GUEST_ENOENT; +} + +static constexpr std::pair GUEST_FILE_FLAGS[] = { + {0x00000001, "UF_NODUMP"}, {0x00000002, "UF_IMMUTABLE"}, {0x00000004, "UF_APPEND"}, {0x00000008, "UF_OPAQUE"}, + {0x00000010, "UF_NOUNLINK"}, {0x00000080, "UF_SYSTEM"}, {0x00000100, "UF_SPARSE"}, {0x00000200, "UF_OFFLINE"}, + {0x00000400, "UF_REPARSE"}, {0x00000800, "UF_ARCHIVE"}, {0x00001000, "UF_READONLY"}, {0x00008000, "UF_HIDDEN"}, + {0x00010000, "SF_ARCHIVED"}, {0x00020000, "SF_IMMUTABLE"}, {0x00040000, "SF_APPEND"}, {0x00100000, "SF_NOUNLINK"}, + {0x00200000, "SF_SNAPSHOT"}}; +static constexpr std::uint64_t GUEST_SF_SNAPSHOT = 0x00200000; + +static std::string FileFlagNames(std::uint64_t flags) { + std::string names; + for (const auto& [flag, name] : GUEST_FILE_FLAGS) { + if ((flags & flag) == 0) continue; + if (!names.empty()) names += '|'; + names += name; } + return names; +} + +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 == 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); + 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 (d >= GuestSockets::FirstDescriptor) return GuestSockets::Close(d); + if (File::IsHostStream(d)) return PosixFailure(GUEST_EBADF); + int socketError = 0; + if (KernelCloseSocket_nid_no_patch(d, &socketError)) { + if (socketError == 0) return 0; + *__error_nid_postfix() = socketError; + return -1; + } + if (GuestSockets::IsOpen(d)) return GuestSockets::Close(d); +#ifdef _WIN32 + const auto positioned = File::LockPositionedTransfer(d); +#endif + EqueueDescriptorClosed(d); + File::ForgetRandomDevice(d); + File::ForgetDescriptor(d); + ForgetJitSharedMemory(d); #ifdef _WIN32 File::ForgetDirectoryDescriptor(d); - return _close(d); + File::ForgetFileLock(d); + return WithoutParameterHandler([d] { return ::_close(d); }); #else return ::close(d); #endif @@ -292,65 +717,230 @@ int APS5_VABI _close_nid_postfix(int descriptor) { return close_nid_postfix(descriptor); } -int APS5_VABI flock_nid_postfix(int d, int operation) { - if (NativeFlock(d, operation) != 0) { +} + +namespace { +std::atomic g_hostStandardError{-1}; +std::atomic g_hostStandardOutput{-1}; + +void PreserveHostStream(int standard, std::atomic& slot) { + if (slot.load() >= 0) return; #ifdef _WIN32 - throw std::runtime_error(std::string(__func__) + ": flock failed, fd=" + std::to_string(d) + ", error=" + std::to_string(::GetLastError())); + const int saved = WithoutParameterHandler([standard] { return ::_dup(standard); }); #else - throw std::runtime_error(std::string(__func__) + ": flock failed, fd=" + std::to_string(d) + ", errno=" + std::to_string(errno)); + const int saved = standard == 1 ? KeepHostStandardOutput_nid_no_patch() : KeepHostStandardError_nid_no_patch(); +#endif + 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 (!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() { + return g_hostStandardError.load(); +} + +extern "C" { + +int APS5_VABI dup_nid_postfix(int d) { + if (GuestSockets::IsOpen(d)) return GuestSockets::Duplicate(d); + if (d < 0) return PosixFailure(GUEST_EBADF); + if (IsKqueueDescriptor(d)) throw std::runtime_error(std::string(__func__) + ": duplicating a kqueue descriptor is not supported"); +#ifdef _WIN32 + const int duplicate = WithoutParameterHandler([d] { return ::_dup(d); }); + if (duplicate >= 0) File::DuplicateDirectoryDescriptor(d, duplicate); +#else + const int duplicate = ::dup(d); +#endif + if (duplicate < 0) return PosixFailure(SceErrorFromErrno(errno) & 0xffff); + if (File::IsRandomDevice(d)) File::RememberRandomDevice(duplicate); + File::ShareDescriptor(d, duplicate); + DuplicateJitSharedMemory(d, duplicate); + return duplicate; +} + +int APS5_VABI dup2_nid_postfix(int from, int to) { + if (from < 0 || to < 0) return PosixFailure(GUEST_EBADF); + if (GuestSockets::IsOpen(from)) { + if (from != to && to <= 2) + throw std::runtime_error(std::string(__func__) + ": moving a guest socket onto standard descriptor " + std::to_string(to) + " is not supported"); + if (GuestSockets::DuplicateTo(from, to) < 0) return -1; + if (from == to) return to; + File::ForgetRandomDevice(to); + File::ForgetArchivedFile(to); +#ifdef _WIN32 + File::ForgetDirectoryDescriptor(to); + File::ForgetFileLock(to); +#endif + return to; + } + const bool sourceIsRandom = File::IsRandomDevice(from); + if (from != to && IsKqueueDescriptor(from)) throw std::runtime_error(std::string(__func__) + ": duplicating a kqueue descriptor is not supported"); +#ifdef _WIN32 + if (WithoutParameterHandler([from] { return ::_get_osfhandle(from) == -1 ? -1 : 0; }) != 0) return PosixFailure(GUEST_EBADF); + if (from == to) return to; + 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); }); + KeepStandardHandle_nid_no_patch(to, standard); + if (duplicated != 0) return PosixFailure(SceErrorFromErrno(errno) & 0xffff); + EqueueDescriptorClosed(to); + GuestSockets::Forget(to); + File::ForgetDirectoryDescriptor(to); + File::DuplicateDirectoryDescriptor(from, to); + File::ForgetFileLock(to); + File::ForgetRandomDevice(to); + if (sourceIsRandom) File::RememberRandomDevice(to); + File::ShareDescriptor(from, to); + DuplicateJitSharedMemory(from, to); + return to; +#else + if (from == to) { + 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); + EqueueDescriptorClosed(to); + GuestSockets::Forget(to); + File::ForgetRandomDevice(to); + if (sourceIsRandom) File::RememberRandomDevice(to); + File::ShareDescriptor(from, to); + DuplicateJitSharedMemory(from, to); + return result; +#endif +} + +int APS5_VABI flock_nid_postfix(int d, int operation) { + if (GuestSockets::IsOpen(d)) return PosixFailure(GUEST_EOPNOTSUPP); + 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); + if (NativeFlock(d, type | (operation & 4)) != 0) { +#ifdef _WIN32 + const auto error = ::GetLastError(); + if ((operation & 4) && error == ERROR_LOCK_VIOLATION) return PosixFailure(GUEST_EWOULDBLOCK); + throw std::runtime_error(std::string(__func__) + ": flock failed, fd=" + std::to_string(d) + ", error=" + std::to_string(error)); +#else + const int error = errno; + if ((operation & 4) && error == EWOULDBLOCK) return PosixFailure(GUEST_EWOULDBLOCK); + throw std::runtime_error(std::string(__func__) + ": flock failed, fd=" + std::to_string(d) + ", errno=" + std::to_string(error)); #endif } return 0; } -int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int d, FileStat* sb) { - if (sb == nullptr) { - APS5_INVALID_ARG_EX; - } - File::FillFileStat(d, sb); +int APS5_VABI _fstatfs_nid_postfix(int d, KernelStatfs* buf) { + if (GuestSockets::IsOpen(d)) return PosixFailure(GUEST_EINVAL); + if (d < 0) return PosixFailure(GUEST_EBADF); + File::RefuseArchivedFile(d, "fstatfs"); + KernelStatfs result{}; + result.f_version = KERNEL_STATFS_VERSION; + if (const int error = NativeFstatfs(d, &result)) return PosixFailure(error); + if (buf == nullptr) return PosixFailure(GUEST_EFAULT); + const GuestArena::HostWrite destination(buf, sizeof(result)); + if (!destination.Open()) return PosixFailure(GUEST_EFAULT); + std::memcpy(buf, &result, sizeof(result)); return 0; } -int APS5_VABI ftruncate_nid_postfix(int d, int64_t length) { - if (length < 0) { - APS5_INVALID_ARG_EX; +int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int d, FileStat* sb) { + if (sb == nullptr) { + 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); + return 0; + } + if (!File::FillFileStatFromDescriptor(d, sb)) return PosixFailure(SceErrorFromErrno(errno) & 0xffff); + 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) { - throw std::runtime_error(std::string(__func__) + ": ftruncate failed, fd=" + std::to_string(d) + ", error=" + std::to_string(error)); + if (error == EACCES) error = GUEST_EINVAL; + return PosixFailure(SceErrorFromErrno(error) & 0xffff); } #else if (NativeFtruncate(d, length) != 0) { - throw std::runtime_error(std::string(__func__) + ": ftruncate failed, fd=" + std::to_string(d) + ", errno=" + std::to_string(errno)); + return PosixFailure(SceErrorFromErrno(errno) & 0xffff); } #endif return 0; } int APS5_VABI sceKernelFtruncate(int d, int64_t length) { - return ftruncate_nid_postfix(d, length); + return ftruncate_nid_postfix(d, length) == 0 ? 0 : SceErrorFromErrno(*__error_nid_postfix()); } int64_t APS5_VABI lseek_nid_postfix(int d, int64_t offset, int whence) { - return static_cast(sceKernelLseek(d, offset, 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)); } int APS5_VABI mkdir_nid_postfix(const char* path, uint16_t mode) { if (const int error = PathError(path)) return PosixFailure(error); - auto native = ResolvePath_nid_no_patch(path); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return PosixFailure(error); if (NativeMkdir(native, mode) != 0) { - return PosixResult(SceErrorFromErrno(errno)); + const int nativeError = errno; + const bool blockedByFile = (nativeError == ENOENT || nativeError == EINVAL) && HasNonDirectoryPrefix(native); + return PosixResult(SceErrorFromErrno(blockedByFile ? GUEST_ENOTDIR : nativeError)); } RecordWrittenPath_nid_no_patch(native); return 0; } 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))); } +int APS5_VABI creat_nid_postfix(const char* path, int mode) { + return open_nid_postfix(path, SCE_KERNEL_O_WRONLY | SCE_KERNEL_O_CREAT | SCE_KERNEL_O_TRUNC, mode); +} + int APS5_VABI _open_nid_postfix(const char* path, int flags, ...) { int mode = 0; if (flags & SCE_KERNEL_O_CREAT) { @@ -370,52 +960,60 @@ int APS5_VABI _open_nid_postfix(const char* path, int flags, ...) { } int64_t APS5_VABI pread_nid_postfix(int d, void* buf, size_t nbytes, int64_t offset) { - if (buf == nullptr) { - APS5_INVALID_ARG_EX; - } - if (offset < 0) { - APS5_INVALID_ARG_EX; - } + if (ExceedsTransferLimit(nbytes)) return PosixFailure(GUEST_EINVAL); + if (buf == nullptr && nbytes != 0) return PosixFailure(GUEST_EFAULT); + if (offset < 0) return PosixFailure(GUEST_EINVAL); + if (FailPosixPositionedGuestSocket(d)) return -1; const GuestArena::HostWrite destination(buf, nbytes); - if (!destination.Open()) errno = EFAULT; - auto n = destination.Open() ? NativePread(d, buf, nbytes, offset) : -1; - if (n < 0) { - throw std::runtime_error(std::string(__func__) + ": pread failed, fd=" + std::to_string(d) + ", errno=" + std::to_string(errno)); - } - return n; + if (!destination.Open()) return PosixFailure(GUEST_EFAULT); + if (File::ReadRandomDevice(d, buf, nbytes)) return static_cast(nbytes); + 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) { - APS5_INVALID_ARG_EX; - } - if (offset < 0) { - APS5_INVALID_ARG_EX; - } - auto n = NativePwrite(d, buf, nbytes, offset); - if (n < 0) { - throw std::runtime_error(std::string(__func__) + ": pwrite failed, fd=" + std::to_string(d) + ", errno=" + std::to_string(errno)); - } - return n; + if (ExceedsTransferLimit(nbytes)) return PosixFailure(GUEST_EINVAL); + if (buf == nullptr && nbytes != 0) return PosixFailure(GUEST_EFAULT); + if (offset < 0) return PosixFailure(GUEST_EINVAL); + if (FailPosixPositionedGuestSocket(d)) return -1; + 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) { - return sceKernelRead(d, buf, static_cast(nbytes)); + if (ExceedsTransferLimit(nbytes)) return PosixFailure(GUEST_EINVAL); + if (GuestSockets::IsOpen(d)) return GuestSockets::Read(d, buf, static_cast(nbytes)); + if (buf == nullptr && nbytes != 0) return PosixFailure(GUEST_EFAULT); + char emptyBuffer = 0; + 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) { - return sceKernelRead(descriptor, buffer, count); + if (ExceedsTransferLimit(count)) return PosixFailure(GUEST_EINVAL); + if (GuestSockets::IsOpen(descriptor)) return GuestSockets::Read(descriptor, buffer, count); + if (buffer == nullptr && count != 0) return PosixFailure(GUEST_EFAULT); + char emptyBuffer = 0; + 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; - } - return sceKernelWrite(d, str, static_cast(size)); + if (size < 0) return PosixFailure(GUEST_EINVAL); + if (GuestSockets::IsOpen(d)) return GuestSockets::Write(d, str, static_cast(size)); + if (str == nullptr && size != 0) return PosixFailure(GUEST_EFAULT); + const char emptyBuffer = 0; + 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) { - return sceKernelWrite(descriptor, buffer, count); + if (ExceedsTransferLimit(count)) return PosixFailure(GUEST_EINVAL); + if (GuestSockets::IsOpen(descriptor)) return GuestSockets::Write(descriptor, buffer, count); + if (buffer == nullptr && count != 0) return PosixFailure(GUEST_EFAULT); + const char emptyBuffer = 0; + return PosixResult(sceKernelWrite(descriptor, buffer == nullptr ? &emptyBuffer : buffer, count)); } int APS5_VABI stat_nid_postfix(const char* path, FileStat* sb) { @@ -427,7 +1025,9 @@ int APS5_VABI stat_nid_postfix(const char* path, FileStat* sb) { int APS5_VABI lstat_nid_postfix(const char* path, FileStat* sb) { if (sb == nullptr) return PosixFailure(GUEST_EFAULT); if (const int error = PathError(path)) return PosixFailure(error); - if (!File::FillLinkStat(ResolvePath_nid_no_patch(path), sb)) return PosixResult(SceErrorFromErrno(errno)); + 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 == ENOENT ? NotFoundError_nid_no_patch(native) : errno)); return 0; } @@ -438,27 +1038,53 @@ int APS5_VABI unlink_nid_postfix(const char* path) { int APS5_VABI sceKernelCheckReachability(const char* path) { if (path == nullptr) return SCE_KERNEL_ERROR_EINVAL; + if (*path == '\0') return SCE_KERNEL_ERROR_ENOENT; if (std::strlen(path) > 255) return SCE_KERNEL_ERROR_ENAMETOOLONG; + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native, true, true)) return SceKernelError(error); + if (AppArchiveOnly_nid_no_patch(native)) return 0; std::error_code error; - return std::filesystem::exists(ResolvePath_nid_no_patch(path), error) ? 0 : SCE_KERNEL_ERROR_ENOENT; + 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); + } if (!File::FillFileStatFromDescriptor(d, sb)) return SceErrorFromErrno(errno); return 0; } int APS5_VABI sceKernelFsync(int fd) { + if (GuestSockets::IsOpen(fd)) return SceErrorFromErrno(GUEST_EINVAL); + File::RefuseArchivedFile(fd, "fsync"); #ifdef _WIN32 - return ::_commit(fd); + if (NativeSync(fd) != 0) return SceErrorFromErrno(errno); #else - return ::fsync(fd); + if (::fsync(fd) != 0) return SceErrorFromErrno(errno); #endif + return 0; +} + +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 + if (::fdatasync(fd) != 0) return SceErrorFromErrno(errno); +#endif + return 0; } 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; @@ -469,10 +1095,18 @@ int APS5_VABI sceKernelWriteThrottlingStatus(std::uint64_t* status) { #ifdef _WIN32 int APS5_VABI sceKernelGetdirentries(int fd, char* buf, int nbytes, int64_t* basep) { + if (GuestSockets::IsOpen(fd)) return SceErrorFromErrno(GUEST_EINVAL); + 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) { @@ -482,10 +1116,18 @@ int APS5_VABI sceKernelGetdents(int fd, char* buf, int nbytes) { #else int APS5_VABI sceKernelGetdirentries(int fd, char* buf, int nbytes, int64_t* basep) { + if (GuestSockets::IsOpen(fd)) return SceErrorFromErrno(GUEST_EINVAL); + 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); @@ -537,22 +1179,58 @@ 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"); - const auto native = ResolvePath_nid_no_patch(path); + 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 (!std::filesystem::exists(native.parent_path(), error)) return SceErrorFromErrno(GUEST_ENOENT); + 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); + } if (!std::filesystem::create_directory(native, error)) return SceErrorFromErrno(error.value() ? error.value() : GUEST_EIO); RecordWrittenPath_nid_no_patch(native); 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; } +static std::int64_t ReadRandomIovecs(int d, const KernelIovec* iov, int iovcnt) { + std::int64_t total = 0; + for (int i = 0; i < iovcnt; ++i) { + File::ReadRandomDevice(d, iov[i].base, iov[i].length); + total += static_cast(iov[i].length); + } + return total; +} + static bool OpenIovecs(const KernelIovec* iov, int iovcnt, std::deque& destinations) { for (int i = 0; i < iovcnt; ++i) { if (!destinations.emplace_back(iov[i].base, iov[i].length).Open()) return false; @@ -560,18 +1238,39 @@ 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) { - return pwrite_nid_disambig1_nid_postfix(d, buf, nbytes, offset); + if (buf == nullptr && nbytes != 0) return SceErrorFromErrno(GUEST_EFAULT); + if (offset < 0) return SceErrorFromErrno(GUEST_EINVAL); + 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) { @@ -580,24 +1279,49 @@ static std::int64_t TransferIovecs(int d, const KernelIovec* iov, int iovcnt, co total += iov[i].length; } if (offset != nullptr && total > static_cast(std::numeric_limits::max() - *offset)) return SceErrorFromErrno(GUEST_EINVAL); + if (offset != nullptr) { + if (const auto error = PositionedGuestSocketError(d); error != 0) return error; + } std::deque destinations; if (!write && !OpenIovecs(iov, iovcnt, destinations)) return SceErrorFromErrno(GUEST_EFAULT); + if (offset == nullptr && GuestSockets::IsOpen(d)) { + const auto result = write ? GuestSockets::Writev(d, reinterpret_cast(iov), iovcnt) + : GuestSockets::Readv(d, reinterpret_cast(iov), iovcnt); + return result < 0 ? SceKernelError(*__error_nid_postfix()) : result; + } + 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::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); - if (static_cast(result) < chunk || !whole) return done; + if (static_cast(result) < chunk || (!whole && !NativePipeHasData(d))) return done; } } return done; @@ -624,8 +1348,11 @@ int64_t APS5_VABI sceKernelPwritev(int d, const KernelIovec* iov, int iovcnt, in 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 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; } @@ -633,62 +1360,105 @@ int64_t APS5_VABI sceKernelPread(int d, void* buf, size_t nbytes, int64_t offset 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 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)) { + const auto result = GuestSockets::Readv(d, reinterpret_cast(iov), 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))); return result < 0 ? SceErrorFromErrno(errno) : result; } #endif +int64_t APS5_VABI preadv_nid_postfix(int d, const KernelIovec* iov, int iovcnt, int64_t offset) { + const auto result = sceKernelPreadv(d, iov, iovcnt, offset); + return result < 0 ? PosixFailure(static_cast(result) & 0xffff) : result; +} + +int64_t APS5_VABI pwritev_nid_postfix(int d, const KernelIovec* iov, int iovcnt, int64_t offset) { + const auto result = sceKernelPwritev(d, iov, iovcnt, offset); + return result < 0 ? PosixFailure(static_cast(result) & 0xffff) : result; +} + int APS5_VABI sceKernelRename(const char* from, const char* to) { - if (from == nullptr || to == nullptr) throw std::invalid_argument("sceKernelRename: path is null"); - const auto source = ResolvePath_nid_no_patch(from); - std::error_code error; - if (!std::filesystem::exists(source, error)) return SceErrorFromErrno(GUEST_ENOENT); - const auto destination = ResolvePath_nid_no_patch(to); - std::filesystem::rename(source, destination, error); - 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"); - const auto native = ResolvePath_nid_no_patch(path); + 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; - if (!std::filesystem::is_directory(native, error)) return SceErrorFromErrno(std::filesystem::exists(native, error) ? GUEST_ENOTDIR : GUEST_ENOENT); + 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 (!std::filesystem::remove(native, error)) return SceErrorFromErrno(GUEST_EIO); + if (!RemoveEmptyDirectory(native)) return SceErrorFromErrno(GUEST_EIO); RecordWrittenPath_nid_no_patch(native); return 0; } @@ -703,11 +1473,17 @@ int APS5_VABI rmdir_nid_postfix(const char* path) { extern "C" { int APS5_VABI sceKernelChmod_nid_postfix(const char* path, std::uint16_t mode) { - return chmod_nid_postfix(path, mode); + 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); + if (NativeChmod(native, mode & 07777) != 0) return SceErrorFromErrno(errno); + return 0; } int APS5_VABI sceKernelFchmod(int d, std::uint16_t mode) { - if (d >= GuestSockets::FirstDescriptor) return SceErrorFromErrno(GuestSockets::IsOpen(d) ? GUEST_EINVAL : GUEST_EBADF); + if (GuestSockets::IsOpen(d)) return SceErrorFromErrno(GUEST_EINVAL); + File::RefuseArchivedFile(d, "fchmod"); if (NativeFchmod(d, mode & 07777) != 0) return SceErrorFromErrno(errno); return 0; } @@ -716,59 +1492,97 @@ 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 (length < 0) return SceErrorFromErrno(GUEST_EINVAL); - const auto native = ResolvePath_nid_no_patch(path); + 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; - if (!std::filesystem::exists(native, error)) return SceErrorFromErrno(GUEST_ENOENT); - std::filesystem::resize_file(native, static_cast(length), error); - if (error) return SceErrorFromErrno(GUEST_EIO); + const auto status = std::filesystem::status(native, error); + 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); return 0; } +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 (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; - } - } - const auto native = ResolvePath_nid_no_patch(path); - if (NativeUtimes(native, times) != 0) return SceErrorFromErrno(errno); + if (InvalidTimes(times)) return SCE_KERNEL_ERROR_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); + 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 futimes_nid_postfix(int d, const KernelTimeval* times) { - if (d >= GuestSockets::FirstDescriptor) return PosixFailure(GuestSockets::IsOpen(d) ? GUEST_EINVAL : GUEST_EBADF); +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); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return PosixFailure(error); + if (NativeUtimes(native, times, false) != 0) return PosixResult(SceErrorFromErrno(errno)); + return 0; +} + +int APS5_VABI sceKernelFutimes(int d, const KernelTimeval* times) { + if (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 PosixFailure(GUEST_EINVAL); + if (times[i].tv_usec < 0 || times[i].tv_usec >= 1000000) return SceErrorFromErrno(GUEST_EINVAL); } } - if (NativeFutimes(d, times) != 0) return PosixResult(SceErrorFromErrno(errno)); + if (NativeFutimes(d, times) != 0) return SceErrorFromErrno(errno); return 0; } +int APS5_VABI futimes_nid_postfix(int d, const KernelTimeval* times) { + return PosixResult(sceKernelFutimes(d, times)); +} + int APS5_VABI fsync_nid_postfix(int fd) { - if (sceKernelFsync(fd) != 0) return PosixFailure(errno); - return 0; + return PosixResult(sceKernelFsync(fd)); } int APS5_VABI fdatasync_nid_postfix(int fd) { - if (fd >= GuestSockets::FirstDescriptor) return PosixFailure(GuestSockets::IsOpen(fd) ? GUEST_EINVAL : GUEST_EBADF); -#ifdef _WIN32 - if (::_commit(fd) != 0) return PosixResult(SceErrorFromErrno(errno)); -#else - if (::fdatasync(fd) != 0) return PosixResult(SceErrorFromErrno(errno)); -#endif - return 0; + return PosixResult(sceKernelFdatasync(fd)); +} + +int64_t APS5_VABI readv_nid_postfix(int d, const KernelIovec* iov, int iovcnt) { + const auto result = sceKernelReadv(d, iov, iovcnt); + return result < 0 ? PosixFailure(static_cast(result) & 0xffff) : result; +} + +int64_t APS5_VABI writev_nid_postfix(int d, const KernelIovec* iov, int iovcnt) { + const auto result = sceKernelWritev(d, iov, 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 f013a865fb..648c63eb7c 100644 --- a/core/libs/prx/libkernel/File/src/Terminal.cpp +++ b/core/libs/prx/libkernel/File/src/Terminal.cpp @@ -7,6 +7,7 @@ #include #else #include +#include #include #endif @@ -27,6 +28,12 @@ constexpr int SetDrain = 1; constexpr int SetFlush = 2; constexpr int SetSoft = 0x10; constexpr int InternetFamily = 2; +constexpr int InputQueue = 1; +constexpr int OutputQueue = 2; +constexpr int BothQueues = 3; +constexpr int OutputOff = 1; +constexpr int InputOn = 4; +constexpr int StandardStreams = 3; int Fail(int error) { *__error_nid_postfix() = error; @@ -35,25 +42,34 @@ int Fail(int error) { #ifdef _WIN32 void IgnoreInvalidParameter(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {} -bool HostOpen(int descriptor) { +HANDLE HostHandle(int descriptor) { const auto previous = _set_thread_local_invalid_parameter_handler(IgnoreInvalidParameter); const auto handle = ::_get_osfhandle(descriptor); _set_thread_local_invalid_parameter_handler(previous); - return reinterpret_cast(handle) != INVALID_HANDLE_VALUE; + return reinterpret_cast(handle); } +bool HostOpen(int descriptor) { return HostHandle(descriptor) != INVALID_HANDLE_VALUE; } +bool HostPipe(int descriptor) { return ::GetFileType(HostHandle(descriptor)) == FILE_TYPE_PIPE; } #else bool HostOpen(int descriptor) { return ::fcntl(descriptor, F_GETFD) != -1; } +bool HostPipe(int descriptor) { + struct stat status{}; + return ::fstat(descriptor, &status) == 0 && S_ISFIFO(status.st_mode); +} #endif int TerminalError(int descriptor) { - if (descriptor >= GuestSockets::FirstDescriptor) { - const int family = GuestSockets::Family(descriptor); - if (family < 0) return BadDescriptor; - return family == InternetFamily ? AddressNotAvailable : OperationNotSupported; - } + const int family = GuestSockets::Family(descriptor); + if (family >= 0) return family == InternetFamily ? AddressNotAvailable : OperationNotSupported; if (descriptor < 0 || !HostOpen(descriptor)) return BadDescriptor; return NotTerminal; } + +int ProcessGroupError(int descriptor, const char* pipeMessage) { + const int error = TerminalError(descriptor); + if (error == NotTerminal && descriptor >= StandardStreams && HostPipe(descriptor)) throw std::runtime_error(pipeMessage); + return error; +} } extern "C" { @@ -69,4 +85,30 @@ int APS5_VABI tcsetattr_nid_postfix(int descriptor, int action, const void* attr if (attributes == nullptr) return Fail(BadAddress); return Fail(TerminalError(descriptor)); } + +int APS5_VABI tcsendbreak_nid_postfix(int descriptor, int duration) { + static_cast(duration); + return Fail(TerminalError(descriptor)); +} + +int APS5_VABI tcdrain_nid_postfix(int descriptor) { return Fail(TerminalError(descriptor)); } + +int APS5_VABI tcflush_nid_postfix(int descriptor, int queue) { + if (queue != InputQueue && queue != OutputQueue && queue != BothQueues) return Fail(InvalidArgument); + return Fail(TerminalError(descriptor)); +} + +int APS5_VABI tcflow_nid_postfix(int descriptor, int action) { + if (action < OutputOff || action > InputOn) return Fail(InvalidArgument); + return Fail(TerminalError(descriptor)); +} + +int APS5_VABI tcgetpgrp_nid_postfix(int descriptor) { + return Fail(ProcessGroupError(descriptor, "tcgetpgrp: the SIGIO owner of a pipe is not supported")); +} + +int APS5_VABI tcsetpgrp_nid_postfix(int descriptor, int group) { + static_cast(group); + return Fail(ProcessGroupError(descriptor, "tcsetpgrp: the SIGIO owner of a pipe is not supported")); +} } diff --git a/core/libs/prx/libkernel/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/ModuleArgs.hpp b/core/libs/prx/libkernel/Module/ModuleArgs.hpp new file mode 100644 index 0000000000..67e8471147 --- /dev/null +++ b/core/libs/prx/libkernel/Module/ModuleArgs.hpp @@ -0,0 +1,16 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_MODULE_MODULEARGS_HPP +#define CORE_LIBS_PRX_LIBKERNEL_MODULE_MODULEARGS_HPP + +#include + +struct PendingModuleArgs { + std::size_t args = 0; + const void* argp = nullptr; +}; + +extern "C" { +const void* __aps5_get_pending_module_args_nid_no_patch(); +void __aps5_set_module_init_result_nid_no_patch(int result); +} + +#endif diff --git a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp index 509194a508..fdead458de 100644 --- a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp +++ b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp @@ -1,12 +1,19 @@ #include "prx/libc/include/General.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libkernel/Module/ModuleArgs.hpp" #include +#include #include +#include #include #include #include #include +#include #include #include +#include +#include #include #ifdef _WIN32 #ifndef NOMINMAX @@ -18,6 +25,10 @@ #include #endif +#ifdef _WIN32 +extern "C" void ForgetUnloadedUnwindImages_nid_no_patch(); +#endif + namespace { thread_local std::array loaderError{}; thread_local bool pendingError = false; @@ -29,10 +40,14 @@ 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 FreeLibrary(static_cast(native)); + ForgetUnloadedUnwindImages_nid_no_patch(); #else ::dlclose(native); #endif @@ -51,28 +66,23 @@ void* Symbol(Module& module, const char* name) { #endif } #ifndef _WIN32 -std::uintptr_t MainProgramBase() { - static const std::uintptr_t base = [] { - struct Scan { - std::uintptr_t value = 0; - }; - Scan scan; - ::dl_iterate_phdr( - [](dl_phdr_info* info, size_t, void* data) { - auto* found = static_cast(data); - if (found->value == 0 && info->dlpi_name != nullptr && info->dlpi_name[0] == '\0') found->value = info->dlpi_addr; - return found->value == 0 ? 0 : 1; - }, - &scan); - return scan.value; +const link_map* MainProgramMap() { + static const link_map* const map = [] { + void* handle = ::dlopen(nullptr, RTLD_LAZY); + link_map* found = nullptr; + const bool resolved = handle != nullptr && ::dlinfo(handle, RTLD_DI_LINKMAP, &found) == 0 && found != nullptr; + if (handle != nullptr) ::dlclose(handle); + if (!resolved) throw std::runtime_error("dlsym: cannot find the link map of the main program"); + return found; }(); - return base; + return map; } bool FromGuestModule(void* address) { Dl_info info{}; - if (address == nullptr || !::dladdr(address, &info) || info.dli_fname == nullptr) return false; - if (reinterpret_cast(info.dli_fbase) == MainProgramBase()) return true; + link_map* image = nullptr; + if (address == nullptr || !::dladdr1(address, &info, reinterpret_cast(&image), RTLD_DL_LINKMAP) || info.dli_fname == nullptr || image == nullptr) return false; + if (image == MainProgramMap()) return true; const std::string path(info.dli_fname); return path.ends_with(".prx"); } @@ -103,12 +113,58 @@ char* APS5_VABI dlerror_nid_postfix() { pendingError = false; return loaderError.data(); } -static std::filesystem::path RelinkedModulePath(const std::filesystem::path& path) { - auto relinked = path; - relinked += ".guest.prx"; - std::error_code error; - return std::filesystem::is_regular_file(relinked, error) ? relinked : path; +#ifdef _WIN32 +static void InitializeDeferredModule(HMODULE native) { + using Entry = int (APS5_VABI *)(std::size_t, const void*, void*); + using Initializer = void (APS5_VABI *)(int, char**, char**); + constexpr auto initializing = std::numeric_limits::max(); + static std::mutex initializedLock; + static std::condition_variable initializedChanged; + auto* table = reinterpret_cast(GetProcAddress(native, "__aps5_guest_initialize")); + if (!table) return; + const auto owner = GetCurrentThreadId(); + std::unique_lock lock(initializedLock); + while (table[1] == initializing) { + if (table[0] == 0) throw std::runtime_error("dlopen: deferred module initialization failed"); + if (table[0] == owner) return; + initializedChanged.wait(lock); + } + const auto entry = table[0]; + const auto count = table[1]; + if (entry == 0 && count == 0) return; + DWORD protection = 0; + if (!VirtualProtect(table, 2 * sizeof(*table), PAGE_READWRITE, &protection)) + throw std::system_error(static_cast(GetLastError()), std::system_category(), "open deferred module initialization metadata"); + table[0] = owner; + table[1] = initializing; + lock.unlock(); + const auto finish = [&](bool succeeded) { + lock.lock(); + table[0] = 0; + table[1] = succeeded ? 0 : initializing; + DWORD previous = 0; + const bool restored = VirtualProtect(table, 2 * sizeof(*table), protection, &previous); + const auto error = restored ? ERROR_SUCCESS : GetLastError(); + if (!restored) table[1] = initializing; + lock.unlock(); + initializedChanged.notify_all(); + if (!restored) throw std::system_error(static_cast(error), std::system_category(), "restore deferred module initialization metadata"); + }; + try { + auto* base = reinterpret_cast(native); + const auto* pending = static_cast(__aps5_get_pending_module_args_nid_no_patch()); + if (entry != 0) __aps5_set_module_init_result_nid_no_patch(reinterpret_cast(base + entry)(pending->args, pending->argp, nullptr)); + for (std::uint32_t index = 0; index < count; ++index) { + const auto initializer = *reinterpret_cast(base + table[2 + index]); + if (initializer) initializer(0, nullptr, nullptr); + } + } catch (...) { + finish(false); + throw; + } + finish(true); } +#endif void* APS5_VABI dlopen_nid_postfix(const char* path, int flags) { if ((flags & ~0x103) || (flags & 3) == 0 || (flags & 3) == 3) { @@ -123,8 +179,9 @@ void* APS5_VABI dlopen_nid_postfix(const char* path, int flags) { module->owned = false; } else { if (!*path) { Error("dlopen: empty module path"); return nullptr; } - const auto resolved = RelinkedModulePath(ResolvePath_nid_no_patch(path)); + const auto resolved = RelinkedModulePath_nid_no_patch(ResolvePath_nid_no_patch(path)); module->native = LoadLibraryExW(resolved.c_str(), nullptr, LOAD_WITH_ALTERED_SEARCH_PATH); + if (module->native) InitializeDeferredModule(static_cast(module->native)); } if (!module->native) { char message[128]; @@ -132,7 +189,7 @@ void* APS5_VABI dlopen_nid_postfix(const char* path, int flags) { Error(message); return nullptr; } #else - const auto resolved = path ? RelinkedModulePath(ResolvePath_nid_no_patch(path)).string() : std::string{}; + const auto resolved = path ? RelinkedModulePath_nid_no_patch(ResolvePath_nid_no_patch(path)).string() : std::string{}; const int nativeFlags = ((flags & 3) == 1 ? RTLD_LAZY : RTLD_NOW) | ((flags & 0x100) ? RTLD_GLOBAL : RTLD_LOCAL); module->native = ::dlopen(path ? resolved.c_str() : nullptr, nativeFlags); @@ -140,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; } } @@ -152,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; } @@ -164,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"); @@ -171,12 +248,30 @@ void* APS5_VABI dlsym_nid_postfix(void* handle, const char* name) { } catch (const std::exception& error) { Error(error.what()); return nullptr; } } int APS5_VABI dlclose_nid_postfix(void* handle) { + const auto key = reinterpret_cast(handle); std::shared_ptr module; + bool lastReference = true; { std::lock_guard lock(modulesMutex); - auto found = modules.find(reinterpret_cast(handle)); + auto found = modules.find(key); + if (found == modules.end()) { Error("dlclose: invalid module handle"); return -1; } + module = found->second; + for (const auto& [other, entry] : modules) { + if (other != key && entry->native == module->native) lastReference = false; + } + } + 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); if (found == modules.end()) { Error("dlclose: invalid module handle"); return -1; } - module = std::move(found->second); modules.erase(found); } // Unload outside the registry lock: module destructors may call loader APIs. diff --git a/core/libs/prx/libkernel/Module/src/Module.cpp b/core/libs/prx/libkernel/Module/src/Module.cpp index 76dd6d63e0..9e71a3183c 100644 --- a/core/libs/prx/libkernel/Module/src/Module.cpp +++ b/core/libs/prx/libkernel/Module/src/Module.cpp @@ -1,15 +1,21 @@ #include #include #include +#include #include #include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/specifics/linux/ElfTypes.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Module/EhFrame.hpp" +#include "prx/libkernel/Module/ModuleArgs.hpp" #ifdef _WIN32 +#include +#include #include #include #elif defined(__APPLE__) @@ -17,25 +23,92 @@ #include #include #else -#include #include +#include "prx/libc/include/specifics/linux/HostMapping.hpp" #endif #ifdef _WIN32 namespace { -void FillGuestUnwindInfo(const std::uint8_t* base, ModuleInfoForUnwind* info) { +struct UnwindImage { + DWORD size = 0; + bool hasTables = false; + EhFrame::Tables tables; + std::string name; +}; + +std::mutex unwindImagesMutex; +std::unordered_map& UnwindImages() { + static auto& images = *new std::unordered_map(); + return images; +} + +void WriteUnwindInfo(const UnwindImage& image, const std::uint8_t* base, ModuleInfoForUnwind* info) { + if (image.hasTables) { + info->eh_frame_hdr_addr = image.tables.header; + info->eh_frame_addr = image.tables.frames; + info->eh_frame_size = image.tables.framesSize; + info->seg0_addr = reinterpret_cast(base); + info->seg0_size = image.size; + } + std::strncpy(info->name, image.name.c_str(), sizeof(info->name) - 1); + info->name[sizeof(info->name) - 1] = '\0'; +} + +void FillUnwindImage(void* allocationBase, ModuleInfoForUnwind* info) { + const auto* base = static_cast(allocationBase); + const auto key = reinterpret_cast(base); + { + std::lock_guard lock(unwindImagesMutex); + const auto found = UnwindImages().find(key); + if (found != UnwindImages().end()) { + WriteUnwindInfo(found->second, base, info); + return; + } + } const auto* dos = reinterpret_cast(base); if (dos->e_magic != IMAGE_DOS_SIGNATURE) throw std::runtime_error("sceKernelGetModuleInfoForUnwind: image without a DOS header"); const auto* nt = reinterpret_cast(base + dos->e_lfanew); if (nt->Signature != IMAGE_NT_SIGNATURE) throw std::runtime_error("sceKernelGetModuleInfoForUnwind: image without an NT header"); - EhFrame::Tables tables; - if (!EhFrame::ReadEhmeta(reinterpret_cast(base), *nt, "sceKernelGetModuleInfoForUnwind", tables)) return; - info->eh_frame_hdr_addr = tables.header; - info->eh_frame_addr = tables.frames; - info->eh_frame_size = tables.framesSize; - info->seg0_addr = reinterpret_cast(base); - info->seg0_size = nt->OptionalHeader.SizeOfImage; + UnwindImage fresh; + fresh.size = nt->OptionalHeader.SizeOfImage; + fresh.hasTables = EhFrame::ReadEhmeta(key, *nt, "sceKernelGetModuleInfoForUnwind", fresh.tables); + char path[4096] = {}; + const DWORD length = GetMappedFileNameA(GetCurrentProcess(), allocationBase, path, sizeof(path) - 1); + fresh.name.assign(path, length); + std::lock_guard lock(unwindImagesMutex); + const auto& image = UnwindImages().insert_or_assign(key, std::move(fresh)).first->second; + WriteUnwindInfo(image, base, info); } + +bool FillKnownUnwindImage(std::uint64_t address, ModuleInfoForUnwind* info) { + PVOID allocationBase = nullptr; + if (RtlPcToFileHeader(reinterpret_cast(address), &allocationBase) == nullptr || allocationBase == nullptr) return false; + const auto* base = static_cast(allocationBase); + std::lock_guard lock(unwindImagesMutex); + const auto found = UnwindImages().find(reinterpret_cast(base)); + if (found == UnwindImages().end() || !found->second.hasTables) return false; + info->st_size = sizeof(ModuleInfoForUnwind); + WriteUnwindInfo(found->second, base, info); + return true; +} +} + +extern "C" void ForgetUnloadedUnwindImages_nid_no_patch() { + std::vector bases; + { + std::lock_guard lock(unwindImagesMutex); + for (const auto& image : UnwindImages()) bases.push_back(image.first); + } + std::vector unloaded; + for (const auto base : bases) { + HMODULE loaded = nullptr; + const bool present = GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(base), &loaded) != 0 && + reinterpret_cast(loaded) == base; + if (!present) unloaded.push_back(base); + } + if (unloaded.empty()) return; + std::lock_guard lock(unwindImagesMutex); + for (const auto base : unloaded) UnwindImages().erase(base); } #elif !defined(__APPLE__) extern "C" int APS5_VABI sceKernelGetModuleInfoFromAddr(std::uint64_t address, int flags, ModuleInfoEx* info); @@ -82,17 +155,31 @@ 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" { int APS5_VABI sceKernelDlsym(KernelModule handle, const char* symbol, void** addr) { if (!symbol || !addr) return SCE_KERNEL_ERROR_EFAULT; - void* found = dlsym_nid_postfix(reinterpret_cast(static_cast(handle)), symbol); + void* module = handle == 0 ? dlopen_nid_postfix(nullptr, kRtldNow) : reinterpret_cast(static_cast(handle)); + void* found = dlsym_nid_postfix(module, symbol); + if (handle == 0 && module) dlclose_nid_postfix(module); if (!found) return SCE_KERNEL_ERROR_ESRCH; *addr = found; return 0; @@ -102,6 +189,7 @@ int APS5_VABI sceKernelGetModuleInfoForUnwind(uint64_t addr, int flags, ModuleIn (void)flags; if (!info) return SCE_KERNEL_ERROR_EFAULT; #ifdef _WIN32 + if (FillKnownUnwindImage(addr, info)) return 0; MEMORY_BASIC_INFORMATION mbi{}; if (!VirtualQuery(reinterpret_cast(addr), &mbi, sizeof(mbi))) return SCE_KERNEL_ERROR_ESRCH; info->st_size = sizeof(ModuleInfoForUnwind); @@ -110,9 +198,12 @@ int APS5_VABI sceKernelGetModuleInfoForUnwind(uint64_t addr, int flags, ModuleIn info->eh_frame_size = 0; info->seg0_addr = reinterpret_cast(mbi.BaseAddress); info->seg0_size = mbi.RegionSize; - if (mbi.Type == MEM_IMAGE) FillGuestUnwindInfo(static_cast(mbi.AllocationBase), info); + if (mbi.Type == MEM_IMAGE) { + FillUnwindImage(mbi.AllocationBase, info); + return 0; + } char path[4096] = {}; - DWORD len = GetMappedFileNameA(GetCurrentProcess(), mbi.BaseAddress, path, sizeof(path) - 1); + const DWORD len = GetMappedFileNameA(GetCurrentProcess(), mbi.BaseAddress, path, sizeof(path) - 1); path[len] = '\0'; std::strncpy(info->name, path, sizeof(info->name) - 1); info->name[sizeof(info->name) - 1] = '\0'; @@ -163,43 +254,23 @@ int APS5_VABI sceKernelGetModuleInfoForUnwind(uint64_t addr, int flags, ModuleIn info->seg0_size = textSize; return 0; #else - std::ifstream maps("/proc/self/maps"); - if (!maps) throw std::runtime_error("sceKernelGetModuleInfoForUnwind: failed to open /proc/self/maps"); - std::string line; - while (std::getline(maps, line)) { - std::uint64_t start = 0; - std::uint64_t end = 0; - char perms[8] = {}; - std::uint64_t offset = 0; - unsigned int devMajor = 0; - unsigned int devMinor = 0; - std::uint64_t inode = 0; - char path[4096] = {}; - int parsed = std::sscanf(line.c_str(), "%llx-%llx %7s %llx %x:%x %llu %4095s", - (unsigned long long*)&start, (unsigned long long*)&end, perms, - (unsigned long long*)&offset, &devMajor, &devMinor, (unsigned long long*)&inode, path); - if (parsed < 7 || addr < start || addr >= end) continue; - info->st_size = sizeof(ModuleInfoForUnwind); - std::strncpy(info->name, parsed >= 8 ? path : "", sizeof(info->name) - 1); - info->name[sizeof(info->name) - 1] = '\0'; - info->eh_frame_hdr_addr = 0; - info->eh_frame_addr = 0; - info->eh_frame_size = 0; - info->seg0_addr = start; - info->seg0_size = end - start; - if (IsRelinkedImage(addr)) FillGuestUnwindInfo(addr, info); - return 0; - } - return SCE_KERNEL_ERROR_ESRCH; + const auto mapping = HostMapping::Find(addr, "sceKernelGetModuleInfoForUnwind"); + if (!mapping) return SCE_KERNEL_ERROR_ESRCH; + info->st_size = sizeof(ModuleInfoForUnwind); + std::strncpy(info->name, mapping->path.c_str(), sizeof(info->name) - 1); + info->name[sizeof(info->name) - 1] = '\0'; + info->eh_frame_hdr_addr = 0; + info->eh_frame_addr = 0; + info->eh_frame_size = 0; + info->seg0_addr = mapping->start; + info->seg0_size = mapping->end - mapping->start; + if (IsRelinkedImage(addr)) FillGuestUnwindInfo(addr, info); + return 0; #endif } namespace { -struct PendingModuleArgs { - std::size_t args = 0; - const void* argp = nullptr; -}; thread_local PendingModuleArgs pendingModuleArgs; thread_local int pendingModuleInitResult = 0; @@ -224,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; @@ -249,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; @@ -258,11 +341,4 @@ int APS5_VABI __elf_phdr_match_addr_nid_postfix(dl_phdr_info* phdrInfo, void* ad return 0; } -// unknown signature -std::int32_t APS5_VABI sceKernelInternalMemoryGetModuleSegmentInfo_nid_postfix(void* result) { - (void)result; - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp b/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp index 2a8d8a5ac3..63451dbcad 100644 --- a/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp +++ b/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp @@ -10,6 +10,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Module/EhFrame.hpp" #ifdef _WIN32 @@ -22,7 +23,9 @@ #else #include #include +#include #include +#include #endif extern "C" std::int32_t ModuleIdForImage_nid_no_patch(const void* native); @@ -63,6 +66,11 @@ struct GuestImage { std::uintptr_t address; }; +std::uint64_t PageSpan(std::uint64_t address, std::uint64_t size) { + constexpr std::uint64_t pageMask = PS5_PAGE_SIZE - 1; + return ((address + size + pageMask) & ~pageMask) - (address & ~pageMask); +} + #ifndef _WIN32 template struct ImageSearch { @@ -81,6 +89,24 @@ bool Contains(const dl_phdr_info& image, std::uintptr_t begin, std::uint64_t siz return false; } +struct FrameTableCache { + std::mutex mutex; + unsigned long long unloads = 0; + std::unordered_map tables; +}; + +EhFrame::Tables FrameTables(const dl_phdr_info& image, std::uintptr_t header) { + static auto& cache = *new FrameTableCache(); + std::lock_guard lock(cache.mutex); + if (cache.unloads != image.dlpi_subs) { + cache.tables.clear(); + cache.unloads = image.dlpi_subs; + } + if (const auto found = cache.tables.find(header); found != cache.tables.end()) return found->second; + const auto contains = [&](std::uintptr_t begin, std::uint64_t size) { return Contains(image, begin, size); }; + return cache.tables.emplace(header, EhFrame::ReadTables(contains, header, Caller)).first->second; +} + std::string ImageName(const dl_phdr_info& image) { std::string path = image.dlpi_name ? image.dlpi_name : ""; if (path.empty()) { @@ -111,8 +137,7 @@ void Fill(const dl_phdr_info& image, TInfo& info) { info.tls_size = ToU32(header.p_memsz, "TLS size"); info.tls_align = ToU32(header.p_align, "TLS alignment"); } else if (header.p_type == PT_GNU_EH_FRAME) { - const auto contains = [&](std::uintptr_t begin, std::uint64_t size) { return Contains(image, begin, size); }; - const auto tables = EhFrame::ReadTables(contains, address, Caller); + const auto tables = FrameTables(image, address); info.eh_frame_hdr_addr = address; info.eh_frame_hdr_size = ToU32(header.p_memsz, "eh_frame_hdr size"); info.eh_frame_addr = tables.frames; @@ -162,6 +187,30 @@ std::vector GuestImages() { } return images; } + +int AddGuestSegmentBytes(dl_phdr_info* image, std::size_t, void* data) { + const std::string_view name = image->dlpi_name ? image->dlpi_name : ""; + if (!name.empty() && !name.ends_with(GuestModuleSuffix)) return 0; + constexpr auto allAccess = PF_R | PF_W | PF_X; + for (std::uint16_t i = 0; i < image->dlpi_phnum; ++i) { + const auto& header = image->dlpi_phdr[i]; + if (header.p_type != PT_LOAD || header.p_offset == 0 || (header.p_flags & allAccess) == allAccess) continue; + *static_cast(data) += PageSpan(header.p_vaddr, header.p_memsz); + } + return 0; +} + +std::uint64_t GuestSegmentBytes() { + std::uint64_t bytes = 0; + dl_iterate_phdr(AddGuestSegmentBytes, &bytes); + return bytes; +} + +bool SystemImage(const GuestImage& image) { + const auto* name = static_cast(image.native)->l_name; + const std::string_view path = name ? name : ""; + return !path.empty() && !path.ends_with(GuestModuleSuffix); +} #else bool SectionNamed(const IMAGE_SECTION_HEADER& section, const char* name) { return std::strncmp(reinterpret_cast(section.Name), name, IMAGE_SIZEOF_SHORT_NAME) == 0; @@ -267,8 +316,77 @@ std::vector GuestImages() { } return images; } + +std::uint64_t ImageSegmentBytes(std::uintptr_t base) { + const auto* dos = reinterpret_cast(base); + const auto* nt = reinterpret_cast(base + static_cast(dos->e_lfanew)); + const auto* sections = IMAGE_FIRST_SECTION(nt); + const auto sectionCount = nt->FileHeader.NumberOfSections; + bool relinked = false; + for (unsigned i = 0; i < sectionCount; ++i) relinked = relinked || SectionNamed(sections[i], ".elf0"); + std::uint64_t bytes = 0; + for (unsigned i = 0; i < sectionCount; ++i) { + const auto& section = sections[i]; + if (SectionProtection(section) == 0 || (relinked && std::strncmp(reinterpret_cast(section.Name), ".elf", 4) != 0)) continue; + bytes += PageSpan(section.VirtualAddress, section.Misc.VirtualSize); + } + return bytes; +} + +std::uint64_t GuestSegmentBytes() { + const auto executable = reinterpret_cast(GetModuleHandleW(nullptr)); + constexpr std::wstring_view suffix = L".guest.prx"; + std::uint64_t bytes = 0; + for (const auto& image : GuestImages()) { + if (image.address != executable) { + const auto path = ModulePath(reinterpret_cast(image.address)); + if (path.size() < suffix.size() || CompareStringOrdinal(path.data() + path.size() - suffix.size(), static_cast(suffix.size()), suffix.data(), static_cast(suffix.size()), TRUE) != CSTR_EQUAL) + continue; + } + bytes += ImageSegmentBytes(image.address); + } + return bytes; +} + +bool SystemImage(const GuestImage& image) { + if (image.address == reinterpret_cast(GetModuleHandleW(nullptr))) return false; + constexpr std::wstring_view suffix = L".guest.prx"; + const auto path = ModulePath(reinterpret_cast(image.address)); + return path.size() < suffix.size() || CompareStringOrdinal(path.data() + path.size() - suffix.size(), static_cast(suffix.size()), suffix.data(), static_cast(suffix.size()), TRUE) != CSTR_EQUAL; +} #endif +int FillModuleInfo(KernelModule handle, ModuleInfo* info, bool titleOnly) { + if (!info) return SCE_KERNEL_ERROR_EFAULT; + if (info->st_size != sizeof(ModuleInfo)) return SCE_KERNEL_ERROR_EINVAL; + for (const auto& image : GuestImages()) { + if (ModuleIdForImage_nid_no_patch(image.native) != handle) continue; + if (titleOnly && SystemImage(image)) return SCE_KERNEL_ERROR_EPERM; + ModuleInfo result{}; + result.st_size = sizeof(ModuleInfo); +#ifdef _WIN32 + if (!Fill(image.address, result)) return SCE_KERNEL_ERROR_ESRCH; +#else + ImageSearch search{image.address, &result, false}; + dl_iterate_phdr(FindImage, &search); + if (!search.found) return SCE_KERNEL_ERROR_ESRCH; +#endif + *info = result; + return 0; + } + return SCE_KERNEL_ERROR_ESRCH; +} + +int FillModuleList(KernelModule* handles, std::size_t count, std::size_t* actual, bool titleOnly) { + if (!handles || !actual) return SCE_KERNEL_ERROR_EFAULT; + auto images = GuestImages(); + if (titleOnly) std::erase_if(images, SystemImage); + for (std::size_t i = 0; i < images.size() && i < count; ++i) handles[i] = ModuleIdForImage_nid_no_patch(images[i].native); + if (images.size() > count) return SCE_KERNEL_ERROR_ENOMEM; + *actual = images.size(); + return 0; +} + } extern "C" { @@ -276,8 +394,6 @@ extern "C" { int APS5_VABI sceKernelGetModuleInfoFromAddr(std::uint64_t address, int flags, ModuleInfoEx* info) { if (!info) return SCE_KERNEL_ERROR_EFAULT; if (flags != 2) throw std::invalid_argument("sceKernelGetModuleInfoFromAddr: unsupported flags " + std::to_string(flags)); - if (info->st_size != sizeof(ModuleInfoEx)) - throw std::invalid_argument("sceKernelGetModuleInfoFromAddr: unsupported st_size " + std::to_string(info->st_size)); ModuleInfoEx result{}; result.st_size = sizeof(ModuleInfoEx); #ifdef _WIN32 @@ -297,32 +413,52 @@ int APS5_VABI sceKernelGetModuleInfoFromAddr(std::uint64_t address, int flags, M } int APS5_VABI sceKernelGetModuleList(KernelModule* handles, std::size_t count, std::size_t* actual) { - if (!handles || !actual) return SCE_KERNEL_ERROR_EFAULT; - const auto images = GuestImages(); - for (std::size_t i = 0; i < images.size() && i < count; ++i) handles[i] = ModuleIdForImage_nid_no_patch(images[i].native); - if (images.size() > count) return SCE_KERNEL_ERROR_ENOMEM; - *actual = images.size(); - return 0; + return FillModuleList(handles, count, actual, false); +} + +int APS5_VABI sceKernelGetModuleList2(KernelModule* handles, std::size_t count, std::size_t* actual) { + return FillModuleList(handles, count, actual, true); } int APS5_VABI sceKernelGetModuleInfo(KernelModule handle, ModuleInfo* info) { - if (!info) return SCE_KERNEL_ERROR_EFAULT; - if (info->st_size != sizeof(ModuleInfo)) return SCE_KERNEL_ERROR_EINVAL; - for (const auto& image : GuestImages()) { - if (ModuleIdForImage_nid_no_patch(image.native) != handle) continue; - ModuleInfo result{}; - result.st_size = sizeof(ModuleInfo); + return FillModuleInfo(handle, info, false); +} + +int APS5_VABI sceKernelGetModuleInfo2(KernelModule handle, ModuleInfo* info) { + return FillModuleInfo(handle, info, true); +} + +std::int32_t APS5_VABI sceKernelGetLibkernelTextLocation_nid_postfix(void** address, std::size_t* size) { + if (!address || !size) throw std::invalid_argument("sceKernelGetLibkernelTextLocation: null argument"); + const auto images = GuestImages(); + if (images.empty()) throw std::runtime_error("sceKernelGetLibkernelTextLocation: the main program is not loaded"); + ModuleInfo info{}; #ifdef _WIN32 - if (!Fill(image.address, result)) return SCE_KERNEL_ERROR_ESRCH; + const bool found = Fill(images.front().address, info); #else - ImageSearch search{image.address, &result, false}; - dl_iterate_phdr(FindImage, &search); - if (!search.found) return SCE_KERNEL_ERROR_ESRCH; + ImageSearch search{images.front().address, &info, false}; + dl_iterate_phdr(FindImage, &search); + const bool found = search.found; #endif - *info = result; - return 0; - } - return SCE_KERNEL_ERROR_ESRCH; + if (!found || info.segment_count == 0) + throw std::runtime_error("sceKernelGetLibkernelTextLocation: the main program has no segments"); + *address = reinterpret_cast(static_cast(info.segments[0].address)); + *size = info.segments[0].size; + return 0; +} + +std::int32_t APS5_VABI sceKernelInternalMemoryGetModuleSegmentInfo_nid_postfix(InternalSegmentInfo* result) { + if (!result) throw std::invalid_argument("sceKernelInternalMemoryGetModuleSegmentInfo: result is null"); + void* address = nullptr; + std::size_t size = 0; + const auto status = sceKernelGetLibkernelTextLocation_nid_postfix(&address, &size); + if (status != 0) return status; + *result = {reinterpret_cast(address), size}; + return 0; +} + +std::uint64_t GuestImageSegmentBytes_nid_no_patch() { + return GuestSegmentBytes(); } } diff --git a/core/libs/prx/libkernel/Pthread/Posix/Atfork.cpp b/core/libs/prx/libkernel/Pthread/Posix/Atfork.cpp new file mode 100644 index 0000000000..2becc20064 --- /dev/null +++ b/core/libs/prx/libkernel/Pthread/Posix/Atfork.cpp @@ -0,0 +1,24 @@ +#include "../include/Atfork.hpp" +#include +#include + +namespace { +constexpr int GuestNoMemory = 12; +std::mutex atforkLock; +std::vector atforkHandlers; +} + +extern "C" int APS5_VABI pthread_atfork_nid_postfix(PthreadAtforkHandler prepare, PthreadAtforkHandler parent, PthreadAtforkHandler child) { + std::lock_guard lock(atforkLock); + try { + atforkHandlers.push_back({prepare, parent, child}); + } catch (const std::bad_alloc&) { + return GuestNoMemory; + } + return 0; +} + +std::vector PthreadAtforkHandlers_nid_no_patch() { + std::lock_guard lock(atforkLock); + return atforkHandlers; +} diff --git a/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp b/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp index a05f784021..86fd01c134 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp @@ -1,5 +1,7 @@ #include #include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "../include/Pthread.hpp" @@ -10,8 +12,14 @@ int APS5_VABI scePthreadAttrInit(PthreadAttr* attr); 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; } @@ -30,7 +38,10 @@ 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; return 0; } @@ -46,6 +57,13 @@ int APS5_VABI pthread_attr_getguardsize_nid_postfix(const PthreadAttr* attr, siz return 0; } +int APS5_VABI pthread_attr_getinheritsched_nid_postfix(const PthreadAttr* attr, int* inherit_sched) { + if (!Valid(attr)) return PosixThread::GUEST_EINVAL; + if (!inherit_sched) throw std::runtime_error(std::string(__func__) + ": null output"); + *inherit_sched = (*attr)->_inheritsched; + return 0; +} + int APS5_VABI pthread_attr_getschedparam_nid_postfix(const PthreadAttr* attr, KernelSchedParam* param) { if (!Valid(attr) || !param) return PosixThread::GUEST_EINVAL; param->sched_priority = (*attr)->_schedpriority; @@ -58,6 +76,12 @@ int APS5_VABI pthread_attr_getschedpolicy_nid_postfix(const PthreadAttr* attr, i return 0; } +int APS5_VABI pthread_attr_getscope_nid_postfix(const PthreadAttr* attr, int* scope) { + if (!Valid(attr) || !scope) return PosixThread::GUEST_EINVAL; + *scope = (*attr)->_scope; + return 0; +} + int APS5_VABI pthread_attr_getstack_nid_postfix(const PthreadAttr* __restrict attr, void** __restrict stack_addr, size_t* __restrict stack_size) { if (!Valid(attr) || !stack_addr || !stack_size) return PosixThread::GUEST_EINVAL; *stack_addr = (*attr)->stackAddress; @@ -65,6 +89,12 @@ int APS5_VABI pthread_attr_getstack_nid_postfix(const PthreadAttr* __restrict at return 0; } +int APS5_VABI pthread_attr_getstackaddr_nid_postfix(const PthreadAttr* __restrict attr, void** __restrict stack_addr) { + if (!Valid(attr) || !stack_addr) return PosixThread::GUEST_EINVAL; + *stack_addr = (*attr)->stackAddress; + return 0; +} + int APS5_VABI pthread_attr_getstacksize_nid_postfix(const PthreadAttr* attr, size_t* stack_size) { if (!Valid(attr) || !stack_size) return PosixThread::GUEST_EINVAL; *stack_size = (*attr)->_stacksize; @@ -90,22 +120,35 @@ int APS5_VABI pthread_attr_setguardsize_nid_postfix(PthreadAttr* attr, size_t gu int APS5_VABI pthread_attr_setinheritsched_nid_postfix(PthreadAttr* attr, int inherit_sched) { if (!Valid(attr)) return PosixThread::GUEST_EINVAL; + if (inherit_sched != GUEST_INHERIT_SCHED && inherit_sched != GUEST_EXPLICIT_SCHED) return PosixThread::GUEST_ENOTSUP; (*attr)->_inheritsched = inherit_sched; return 0; } 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; } +int APS5_VABI pthread_attr_setscope_nid_postfix(PthreadAttr* attr, int scope) { + if (!Valid(attr) || (scope != GUEST_SCOPE_PROCESS && scope != GUEST_SCOPE_SYSTEM)) return PosixThread::GUEST_EINVAL; + (*attr)->_scope = scope; + return 0; +} + int APS5_VABI pthread_attr_setstacksize_nid_postfix(PthreadAttr* attr, size_t stack_size) { if (!Valid(attr)) return PosixThread::GUEST_EINVAL; return PosixThread::ToErrno(scePthreadAttrSetstacksize(attr, stack_size)); @@ -116,9 +159,9 @@ int APS5_VABI pthread_attr_setstack_nid_postfix(PthreadAttr* attr, void* stack_a return PosixThread::ToErrno(scePthreadAttrSetstack(attr, stack_addr, stack_size)); } -int APS5_VABI pthread_attr_setsolosched_np_nid_postfix() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI pthread_attr_setsolosched_np_nid_postfix(PthreadAttr* attr, int solosched) { + if (!Valid(attr)) return PosixThread::GUEST_EINVAL; + return PosixThread::ToErrno(scePthreadAttrSetsolosched(attr, solosched)); } } diff --git a/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp b/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp index 140b6900ea..8a4c613626 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp @@ -18,7 +18,6 @@ struct PthreadBarrierPrivate { namespace { constexpr int PROCESS_PRIVATE = 0; -constexpr int PROCESS_SHARED = 1; } @@ -54,7 +53,7 @@ int APS5_VABI pthread_barrierattr_getpshared_nid_postfix(PthreadBarrierattrPriva int APS5_VABI pthread_barrierattr_setpshared_nid_postfix(PthreadBarrierattrPrivate** attr, int pshared) { if (!attr || !*attr) return 22; - if (pshared != PROCESS_PRIVATE && pshared != PROCESS_SHARED) return 22; + if (pshared != PROCESS_PRIVATE) return 22; (*attr)->pshared = pshared; return 0; } @@ -86,7 +85,7 @@ int APS5_VABI pthread_barrier_wait_nid_postfix(PthreadBarrierPrivate** barrier) return -1; } value.changed.wait(lock, [&] { return value.generation != generation; }); - --value.active; + if (--value.active == 0) value.changed.notify_all(); return 0; } @@ -94,8 +93,9 @@ int APS5_VABI pthread_barrier_destroy_nid_postfix(PthreadBarrierPrivate** barrie if (!barrier || !*barrier) return 22; auto* value = *barrier; { - std::lock_guard lock(value->mutex); - if (value->active != 0) return 16; + std::unique_lock lock(value->mutex); + if (value->arrived != 0) return 16; + value->changed.wait(lock, [&] { return value->active == 0; }); } delete value; *barrier = nullptr; diff --git a/core/libs/prx/libkernel/Pthread/Posix/Common.hpp b/core/libs/prx/libkernel/Pthread/Posix/Common.hpp index cf95f707e0..b89859ae7e 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Common.hpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Common.hpp @@ -13,6 +13,7 @@ namespace PosixThread { constexpr int GUEST_ESRCH = 3; constexpr int GUEST_EFAULT = 14; constexpr int GUEST_EINVAL = 22; +constexpr int GUEST_ENOTSUP = 45; constexpr int GUEST_ETIMEDOUT = 60; inline int ToErrno(int sceResult) { @@ -24,10 +25,18 @@ inline bool RelativeMicroseconds(int clockId, const KernelTimespec* abstime, Ker if (!abstime || abstime->tv_nsec < 0 || abstime->tv_nsec >= 1000000000) return false; KernelTimespec now{}; clock_gettime_nid_postfix(clockId, &now); - const auto deadline = std::chrono::seconds(abstime->tv_sec) + std::chrono::nanoseconds(abstime->tv_nsec); - const auto current = std::chrono::seconds(now.tv_sec) + std::chrono::nanoseconds(now.tv_nsec); - const auto remaining = std::chrono::duration_cast(deadline - current).count(); - *usec = remaining <= 0 ? 0 : remaining >= std::numeric_limits::max() ? std::numeric_limits::max() : static_cast(remaining); + constexpr auto maximum = std::numeric_limits::max(); + if (abstime->tv_sec < now.tv_sec - 1) { + *usec = 0; + return true; + } + const std::int64_t seconds = abstime->tv_sec - now.tv_sec; + if (seconds > static_cast(maximum / 1000000) + 1) { + *usec = maximum; + return true; + } + const std::int64_t remaining = (seconds * 1000000000 + abstime->tv_nsec - now.tv_nsec) / 1000; + *usec = remaining <= 0 ? 0 : remaining >= maximum ? maximum : static_cast(remaining); return true; } diff --git a/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp b/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp index b9a7b211cc..6dd1e09509 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp @@ -6,6 +6,8 @@ namespace { +constexpr int POSIX_PROCESS_PRIVATE = 0; + int toPosix(int result) { if (result == 0) return 0; @@ -41,6 +43,10 @@ int APS5_VABI pthread_cond_timedwait_nid_postfix(PthreadCond* cond, PthreadMutex return toPosix(CondOperations::AbsoluteTimedwait(cond, mutex, abstime)); } +int APS5_VABI pthread_cond_reltimedwait_np_nid_postfix(PthreadCond* cond, PthreadMutex* mutex, KernelUseconds usec) { + return toPosix(scePthreadCondTimedwait(cond, mutex, usec)); +} + int APS5_VABI pthread_cond_wait_nid_postfix(PthreadCond* cond, PthreadMutex* mutex) { return toPosix(scePthreadCondWait(cond, mutex)); } @@ -60,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 b34390ef79..3a5d38dc3f 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp @@ -6,15 +6,16 @@ #include "../include/Mutex.hpp" #include "Common.hpp" #include -#include -#include #include #include 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; constexpr std::uintptr_t POSIX_ADAPTIVE_MUTEX_INITIALIZER = 1; bool _isStaticInitializer(PthreadMutex mutex) { @@ -37,6 +38,8 @@ void _initializeStatic(PthreadMutex* mutex, const char* funcName) { extern "C" { +int APS5_VABI sched_get_priority_max_nid_postfix(int policy); + int APS5_VABI pthread_mutex_destroy_nid_postfix(PthreadMutex* mutex) { if (!mutex) throw std::runtime_error("pthread_mutex_destroy: null mutex"); if (_isStaticInitializer(*mutex)) { @@ -58,13 +61,11 @@ 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->tv_nsec < 0 || abstime->tv_nsec >= 1000000000) return POSIX_EINVAL; + if (!abstime) return pthread_mutex_lock_nid_postfix(mutex); _initializeStatic(mutex, __func__); - const auto deadline = std::chrono::seconds(abstime->tv_sec) + std::chrono::nanoseconds(abstime->tv_nsec); - const auto now = std::chrono::system_clock::now().time_since_epoch(); - const auto remaining = std::chrono::duration_cast(deadline - now).count(); - const auto usec = remaining <= 0 ? 0 : remaining >= std::numeric_limits::max() ? std::numeric_limits::max() : static_cast(remaining); + if (scePthreadMutexTrylock(mutex) == 0) return 0; + KernelUseconds usec = 0; + if (!PosixThread::RelativeMicroseconds(GUEST_REALTIME_CLOCK, abstime, &usec)) return POSIX_EINVAL; return PosixThread::ToErrno(scePthreadMutexTimedlock(mutex, usec)); } @@ -88,9 +89,47 @@ 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) 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) 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; return 0; } @@ -98,4 +137,11 @@ int APS5_VABI pthread_mutexattr_settype_nid_postfix(PthreadMutexattr* attr, int return PosixThread::ToErrno(scePthreadMutexattrSettype(attr, type)); } +int APS5_VABI pthread_mutexattr_gettype_nid_postfix(const PthreadMutexattr* attr, int* type) { + if (!attr || !*attr) return POSIX_EINVAL; + if (!type) throw std::runtime_error("pthread_mutexattr_gettype: null type"); + *type = static_cast((*attr)->type); + return 0; +} + } diff --git a/core/libs/prx/libkernel/Pthread/Posix/Once.cpp b/core/libs/prx/libkernel/Pthread/Posix/Once.cpp index 32ed2ee791..6919e008be 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Once.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Once.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/Pthread/include/Cancel.hpp" #include #include #include @@ -14,6 +15,14 @@ struct GuestOnce { static_assert(offsetof(GuestOnce, mutex) == 8 && sizeof(GuestOnce) == 16); std::mutex onceMutex; std::condition_variable onceChanged; + +void APS5_VABI ResetOnce(void* control) { + { + std::lock_guard lock(onceMutex); + static_cast(control)->state = 0; + } + onceChanged.notify_all(); +} } extern "C" int APS5_VABI pthread_once_nid_postfix( @@ -27,15 +36,17 @@ extern "C" int APS5_VABI pthread_once_nid_postfix( lock.unlock(); // Initializers may initialize other once controls. Never run guest code // under the shared lock. The lock publishes all initializer writes. + CleanupHandler reset{}; + void* const mark = ThreadCancel::CleanupMark(); + __pthread_cleanup_push_imp_nid_postfix(ResetOnce, control, &reset); try { initialize(); } catch (...) { - lock.lock(); - control->state = 0; - lock.unlock(); - onceChanged.notify_all(); + ThreadCancel::CleanupRestore(mark); + ResetOnce(control); throw; } + ThreadCancel::CleanupRestore(mark); lock.lock(); control->state = 1; lock.unlock(); diff --git a/core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp b/core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp index 9a79b5ecba..c07eadf645 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp @@ -12,6 +12,7 @@ namespace { constexpr int GUEST_REALTIME_CLOCK = 0; +constexpr int PROCESS_PRIVATE = 0; int toPosix(int result) { if (result == 0) @@ -36,6 +37,8 @@ void initializeStatic(PthreadRwlock* rwlock, const char* funcName) { extern "C" { +int APS5_VABI pthread_rwlock_wrlock_nid_postfix(PthreadRwlock* rwlock); + int APS5_VABI pthread_rwlock_destroy_nid_postfix(PthreadRwlock* rwlock) { if (!rwlock) throw std::runtime_error("pthread_rwlock_destroy: null rwlock"); if (*rwlock == nullptr) return 0; @@ -46,13 +49,34 @@ int APS5_VABI pthread_rwlock_init_nid_postfix(PthreadRwlock* rwlock, const Pthre return toPosix(scePthreadRwlockInit(rwlock, attr, nullptr)); } +int APS5_VABI pthread_rwlockattr_destroy_nid_postfix(PthreadRwlockattr* attr) { + return toPosix(scePthreadRwlockattrDestroy(attr)); +} + +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; @@ -61,7 +85,7 @@ int APS5_VABI pthread_rwlock_timedrdlock_nid_postfix(PthreadRwlock* rwlock, cons } int APS5_VABI pthread_rwlock_timedwrlock_nid_postfix(PthreadRwlock* rwlock, const KernelTimespec* abstime) { - if (!abstime) throw std::runtime_error("pthread_rwlock_timedwrlock: null abstime"); + if (!abstime) return pthread_rwlock_wrlock_nid_postfix(rwlock); initializeStatic(rwlock, __func__); if (scePthreadRwlockTrywrlock(rwlock) == 0) return 0; KernelUseconds usec = 0; diff --git a/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp b/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp index 2672b045ba..e780d7f59f 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp @@ -3,6 +3,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "Common.hpp" +#include "../include/Cancel.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" #include #include @@ -34,12 +35,13 @@ static PosixSemaphore* Get(void* sem) { } static int WaitFor(PosixSemaphore* semaphore, const KernelUseconds* usec) { + ThreadCancel::Check(); std::unique_lock lock(semaphore->lock); const auto ready = [&] { return semaphore->count > 0; }; if (usec) { - if (!semaphore->available.WaitUntil(lock, TimedWait::DeadlineNanos(*usec), ready)) return Fail(PosixThread::GUEST_ETIMEDOUT); + if (!ThreadCancel::WaitUntil(semaphore->available, lock, TimedWait::DeadlineNanos(*usec), ready)) return Fail(PosixThread::GUEST_ETIMEDOUT); } else { - semaphore->available.Wait(lock, ready); + ThreadCancel::WaitUntil(semaphore->available, lock, std::nullopt, ready); } --semaphore->count; return 0; @@ -82,6 +84,15 @@ 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) { + --semaphore->count; + return 0; + } + } + if (!abstime) return WaitFor(semaphore, nullptr); KernelUseconds usec = 0; if (!PosixThread::RelativeMicroseconds(GUEST_REALTIME_CLOCK, abstime, &usec)) return Fail(PosixThread::GUEST_EINVAL); return WaitFor(semaphore, &usec); diff --git a/core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp b/core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp new file mode 100644 index 0000000000..9101c62da8 --- /dev/null +++ b/core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp @@ -0,0 +1,75 @@ +#include "SceTypes.hpp" +#include "Common.hpp" +#include +#include +#include +#include + +struct PthreadSpinlockPrivate { + std::atomic owner{}; +}; + +namespace { + +constexpr int GUEST_EPERM = 1; +constexpr int GUEST_ENOMEM = 12; +constexpr int GUEST_EBUSY = 16; +constexpr int PROCESS_PRIVATE = 0; +constexpr int SPIN_COUNT = 100000; + +bool tryAcquire(PthreadSpinlockPrivate* spinlock) { + std::thread::id unowned{}; + return spinlock->owner.compare_exchange_strong(unowned, std::this_thread::get_id(), std::memory_order_acquire, std::memory_order_relaxed); +} + +} + +extern "C" { + +int APS5_VABI pthread_spin_init_nid_postfix(PthreadSpinlockPrivate** lock, int pshared) { + if (!lock) 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; + return 0; +} + +int APS5_VABI pthread_spin_destroy_nid_postfix(PthreadSpinlockPrivate** lock) { + if (!lock || !*lock) return PosixThread::GUEST_EINVAL; + delete *lock; + *lock = nullptr; + return 0; +} + +int APS5_VABI pthread_spin_trylock_nid_postfix(PthreadSpinlockPrivate** lock) { + if (!lock || !*lock) return PosixThread::GUEST_EINVAL; + return tryAcquire(*lock) ? 0 : GUEST_EBUSY; +} + +int APS5_VABI pthread_spin_lock_nid_postfix(PthreadSpinlockPrivate** lock) { + if (!lock || !*lock) return PosixThread::GUEST_EINVAL; + auto* spinlock = *lock; + if (spinlock->owner.load(std::memory_order_relaxed) == std::this_thread::get_id()) + throw std::runtime_error("pthread_spin_lock: the calling thread already holds the lock and would spin forever"); + int count = SPIN_COUNT; + while (!tryAcquire(spinlock)) { + while (spinlock->owner.load(std::memory_order_relaxed) != std::thread::id{}) { + if (--count <= 0) { + count = SPIN_COUNT; + std::this_thread::yield(); + } + } + } + return 0; +} + +int APS5_VABI pthread_spin_unlock_nid_postfix(PthreadSpinlockPrivate** lock) { + if (!lock || !*lock) return PosixThread::GUEST_EINVAL; + auto owner = std::this_thread::get_id(); + if (!(*lock)->owner.compare_exchange_strong(owner, std::thread::id{}, std::memory_order_release, std::memory_order_relaxed)) + return GUEST_EPERM; + return 0; +} + +} diff --git a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp index 599105c66a..3a0ed90496 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp @@ -1,12 +1,16 @@ #include #include +#include #include "SceTypes.hpp" #include "../include/ThreadLifecycle.hpp" +#include "../include/Cancel.hpp" #include "prx/libc/include/General.hpp" #include "../include/Pthread.hpp" #include "Common.hpp" #include "prx/libkernel/KernelErrors.hpp" -#include +#include +#include +#include #include extern "C" { @@ -17,15 +21,144 @@ 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 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); +} +static constexpr std::size_t GUEST_CPUSET_BYTES = 32; +static constexpr int GUEST_ERANGE = 34; extern "C" { +int* APS5_VABI __error_nid_postfix(); +int APS5_VABI scePthreadGetaffinity(Pthread thread, KernelCpumask* mask); +int APS5_VABI scePthreadSetaffinity(Pthread thread, KernelCpumask mask); +KernelCpumask APS5_VABI sceKernelGetAvailableCpumask(void); +int APS5_VABI getpid_nid_postfix(void); + +int APS5_VABI cpuset_getaffinity_nid_postfix(int level, int which, std::int64_t id, std::size_t size, void* mask) { + constexpr int LevelWhich = 3, WhichThread = 1, WhichProcess = 2; + constexpr int GuestEsrch = 3, GuestEfault = 14, GuestEinval = 22, GuestErange = 34; + constexpr std::size_t MaximumSize = 256 / 8; + const auto fail = [](int error) { + *__error_nid_postfix() = error; + return -1; + }; + if (size < sizeof(KernelCpumask) || size > MaximumSize) return fail(GuestErange); + if (!mask) return fail(GuestEfault); + auto* self = scePthreadSelf(); + KernelCpumask affinity = 0; + if (which == WhichThread) { + 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) { + NotImplemented_nid_no_patch("cpuset_getaffinity other than the calling thread or process"); + } else { + return fail(GuestEinval); + } + if (level < 1 || level > 3) return fail(GuestEinval); + if (level != LevelWhich) NotImplemented_nid_no_patch("cpuset_getaffinity other than the calling thread or process"); + if (which == WhichProcess) affinity = LiveGuestThreadsAffinity(); + std::memset(mask, 0, size); + std::memcpy(mask, &affinity, sizeof(affinity)); + return 0; +} + +int APS5_VABI cpuset_setaffinity_nid_postfix(int level, int which, std::int64_t id, std::size_t size, const void* mask) { + constexpr int LevelWhich = 3, WhichThread = 1, WhichProcess = 2; + constexpr int GuestEsrch = 3, GuestEdeadlk = 11, GuestEfault = 14, GuestEinval = 22, GuestErange = 34; + constexpr std::size_t MaximumSize = 256 / 8; + const auto fail = [](int error) { + *__error_nid_postfix() = error; + return -1; + }; + if (size < sizeof(KernelCpumask) || size > MaximumSize) return fail(GuestErange); + if (!mask) return fail(GuestEfault); + const auto* bytes = static_cast(mask); + for (std::size_t index = sizeof(KernelCpumask); index < size; ++index) { + if (bytes[index] != 0) return fail(GuestEinval); + } + auto* self = scePthreadSelf(); + if (which == WhichThread) { + 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) { + NotImplemented_nid_no_patch("cpuset_setaffinity other than the calling thread or process"); + } else { + return fail(GuestEinval); + } + if (level < 1 || level > 3) return fail(GuestEinval); + if (level != LevelWhich) NotImplemented_nid_no_patch("cpuset_setaffinity other than the calling thread or process"); + KernelCpumask requested = 0; + std::memcpy(&requested, bytes, sizeof(requested)); + const KernelCpumask affinity = requested & sceKernelGetAvailableCpumask(); + if (affinity == 0) return fail(GuestEdeadlk); + if (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)); @@ -45,40 +178,102 @@ void APS5_VABI pthread_exit_nid_postfix(void* value) { int APS5_VABI scePthreadGetschedparam(Pthread thread, int* policy, KernelSchedParam* param) { if (!policy || !param) return SCE_KERNEL_ERROR_EINVAL; - *policy = GUEST_SCHED_FIFO; - return scePthreadGetprio(thread, ¶m->sched_priority); + const int result = scePthreadGetprio(thread, ¶m->sched_priority); + if (result == 0) *policy = thread->schedPolicy.load(std::memory_order_relaxed); + return result; } int APS5_VABI scePthreadSetschedparam(Pthread thread, int policy, const KernelSchedParam* param) { - (void)policy; - if (!param) return SCE_KERNEL_ERROR_EINVAL; - return scePthreadSetprio(thread, param->sched_priority); + if (!param || !AcceptsSchedParam(policy, param->sched_priority)) return SCE_KERNEL_ERROR_EINVAL; + const int result = scePthreadSetprio(thread, param->sched_priority); + if (result == 0) thread->schedPolicy.store(policy, std::memory_order_relaxed); + return result; } int APS5_VABI pthread_getschedparam_nid_postfix(Pthread thread, int* policy, KernelSchedParam* param) { if (!policy || !param) return PosixThread::GUEST_EINVAL; - *policy = GUEST_SCHED_FIFO; - return PosixThread::ToErrno(scePthreadGetprio(thread, ¶m->sched_priority)); + return PosixThread::ToErrno(scePthreadGetschedparam(thread, policy, param)); +} + +int APS5_VABI pthread_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; + if (!cpuset) return PosixThread::GUEST_EFAULT; + KernelCpumask mask = 0; + const int result = scePthreadGetaffinity(thread, &mask); + if (result != 0) return PosixThread::ToErrno(result); + std::memset(cpuset, 0, GUEST_CPUSET_BYTES); + std::memcpy(cpuset, &mask, sizeof(mask)); + return 0; +} + +int APS5_VABI pthread_setaffinity_np_nid_postfix(Pthread thread, std::size_t cpusetSize, const void* cpuset) { + constexpr int GuestEdeadlk = 11; + if (thread != scePthreadSelf()) { + if (!thread) return PosixThread::GUEST_EINVAL; + if (!GuestThreadIsLive(thread)) return PosixThread::GUEST_ESRCH; + } + if (cpusetSize != GUEST_CPUSET_BYTES) return GUEST_ERANGE; + if (!cpuset) return PosixThread::GUEST_EFAULT; + KernelCpumask requested = 0; + std::memcpy(&requested, cpuset, sizeof(requested)); + const KernelCpumask affinity = requested & sceKernelGetAvailableCpumask(); + if (affinity == 0) return GuestEdeadlk; + return PosixThread::ToErrno(scePthreadSetaffinity(thread, affinity)); } int APS5_VABI pthread_join_nid_postfix(Pthread thread, void** value) { return PosixThread::ToErrno(scePthreadJoin(thread, value)); } +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)); + } +}; +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)); } 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)); } +void APS5_VABI pthread_set_name_np_nid_postfix(Pthread thread, const char* name) { + scePthreadRename(thread, name != nullptr ? name : ""); +} + Pthread APS5_VABI pthread_self_nid_postfix(void) { return scePthreadSelf(); } +int APS5_VABI scePthreadGetthreadid(void); + +int APS5_VABI pthread_getthreadid_np_nid_postfix(void) { + return scePthreadGetthreadid(); +} + int APS5_VABI pthread_equal_nid_postfix(Pthread first, Pthread second) { return first == second; } @@ -92,22 +287,29 @@ 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; } +int APS5_VABI pthread_cancel_nid_postfix(Pthread thread) { + return PosixThread::ToErrno(scePthreadCancel(thread)); +} + int APS5_VABI pthread_setcancelstate_nid_postfix(int state, int* old_state) { return PosixThread::ToErrno(scePthreadSetcancelstate(state, old_state)); } +int APS5_VABI pthread_setcanceltype_nid_postfix(int type, int* old_type) { + return PosixThread::ToErrno(scePthreadSetcanceltype(type, old_type)); +} + int APS5_VABI pthread_setprio_nid_postfix(Pthread thread, int prio) { return PosixThread::ToErrno(scePthreadSetprio(thread, prio)); } int APS5_VABI pthread_setschedparam_nid_postfix(Pthread thread, int policy, const KernelSchedParam* param) { - (void)policy; if (!param) return PosixThread::GUEST_EINVAL; - return PosixThread::ToErrno(scePthreadSetprio(thread, param->sched_priority)); + return PosixThread::ToErrno(scePthreadSetschedparam(thread, policy, param)); } void APS5_VABI pthread_testcancel_nid_postfix(void) { @@ -115,11 +317,21 @@ 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) { - std::this_thread::sleep_for(std::chrono::seconds(seconds)); + ThreadCancel::Check(); + { + 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/Atfork.hpp b/core/libs/prx/libkernel/Pthread/include/Atfork.hpp new file mode 100644 index 0000000000..fa50990a6e --- /dev/null +++ b/core/libs/prx/libkernel/Pthread/include/Atfork.hpp @@ -0,0 +1,19 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_PTHREAD_ATFORK_HPP +#define CORE_LIBS_PRX_LIBKERNEL_PTHREAD_ATFORK_HPP + +#include "prx/libc/include/general/VabiMacros.hpp" +#include + +using PthreadAtforkHandler = void (APS5_VABI*)(); + +struct PthreadAtforkHandlers { + PthreadAtforkHandler prepare; + PthreadAtforkHandler parent; + PthreadAtforkHandler child; +}; + +extern "C" int APS5_VABI pthread_atfork_nid_postfix(PthreadAtforkHandler prepare, PthreadAtforkHandler parent, PthreadAtforkHandler child); + +std::vector PthreadAtforkHandlers_nid_no_patch(); + +#endif diff --git a/core/libs/prx/libkernel/Pthread/include/Cancel.hpp b/core/libs/prx/libkernel/Pthread/include/Cancel.hpp new file mode 100644 index 0000000000..8f2df852ca --- /dev/null +++ b/core/libs/prx/libkernel/Pthread/include/Cancel.hpp @@ -0,0 +1,75 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_PTHREAD_CANCEL_HPP +#define CORE_LIBS_PRX_LIBKERNEL_PTHREAD_CANCEL_HPP + +#include "SceTypes.hpp" +#include "prx/libkernel/Time/include/TimedWait.hpp" +#include +#include + +inline constexpr int GUEST_CANCEL_ENABLE = 0; +inline constexpr int GUEST_CANCEL_DISABLE = 1; +inline constexpr int GUEST_CANCEL_DEFERRED = 0; +inline constexpr int GUEST_CANCEL_ASYNCHRONOUS = 2; +inline void* const GUEST_CANCELED = reinterpret_cast(std::uintptr_t{1}); + +struct CleanupHandler { + CleanupHandler* previous; + void (APS5_VABI* routine)(void*); + void* argument; + int onHeap; +}; + +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); +} + +namespace ThreadCancel { + +bool Requested(); +void Check(); +void* CleanupMark(); +void CleanupRestore(void* mark); +void BeginWait(TimedWait::Condition* condition); +void EndWait(); + +template +struct WaitLock { + TLock& inner; + void lock() { + EndWait(); + inner.lock(); + } + void unlock() { inner.unlock(); } +}; + +template +bool Wait(TimedWait::Condition& condition, TLock& lock, std::optional deadlineNanos) { + BeginWait(&condition); + if (Requested()) { + EndWait(); + return true; + } + WaitLock waitLock{lock}; + if (deadlineNanos) return condition.WaitUntil(waitLock, *deadlineNanos); + condition.Wait(waitLock); + return true; +} + +template +bool WaitUntil(TimedWait::Condition& condition, TLock& lock, std::optional deadlineNanos, TPredicate predicate) { + while (!predicate()) { + const bool signaled = Wait(condition, lock, deadlineNanos); + if (Requested()) { + condition.NotifyOne(); + lock.unlock(); + Check(); + } + if (!signaled) return predicate(); + } + return true; +} + +} + +#endif diff --git a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp index 72a7d3d197..03024d9d7f 100644 --- a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp @@ -10,10 +10,18 @@ #include #include #include +#include +#include #include -#include #include #include +#ifndef _WIN32 +#include +#endif + +#ifndef _WIN32 +#include +#endif enum class MutexType : std::uint32_t { ErrorCheck = 1, @@ -24,6 +32,9 @@ enum class MutexType : std::uint32_t { struct PthreadMutexattrPrivate { MutexType type; + int protocol = 0; + int ceiling = 0; + int pshared = 0; }; struct PthreadMutexPrivate { @@ -41,16 +52,20 @@ struct PthreadRwlockattrPrivate { }; struct PthreadRwlockPrivate { - std::shared_timed_mutex _lock; + std::mutex _state; + TimedWait::Condition _changed; std::atomic _writer; + std::atomic _readers{0}; + int _waitingWriters = 0; }; struct PthreadCondattrPrivate { int _clockid; + int _pshared = 0; }; struct PthreadCondPrivate { - TimedWait::Condition _cv; + std::shared_ptr _cv = std::make_shared(); int _clockid = 0; }; @@ -67,6 +82,10 @@ static constexpr int DEFAULT_THREAD_PRIORITY = 700; static constexpr std::uint32_t CPU_CLOCK_BIT = 0x80000000u; static constexpr std::uint32_t CPU_CLOCK_PROCESS_BIT = 0x40000000u; static constexpr std::uint32_t CPU_CLOCK_ID_MASK = ~(CPU_CLOCK_BIT | CPU_CLOCK_PROCESS_BIT); +static constexpr int GUEST_EXPLICIT_SCHED = 0; +static constexpr int GUEST_INHERIT_SCHED = 4; +static constexpr int GUEST_SCOPE_PROCESS = 0; +static constexpr int GUEST_SCOPE_SYSTEM = 2; inline int GuestCpuFromHost(unsigned hostCpu, KernelCpumask threadAffinity) { KernelCpumask mask = threadAffinity & DEFAULT_THREAD_AFFINITY; @@ -83,27 +102,40 @@ struct PthreadAttrPrivate { int _schedpolicy; int _inheritsched; KernelCpumask _affinity = DEFAULT_THREAD_AFFINITY; + bool _affinityExplicit = false; std::size_t _guardsize = 0x1000; int _solosched = 0; + int _scope = GUEST_SCOPE_SYSTEM; }; +inline std::atomic nextThreadId{100000}; + struct PthreadPrivate { + std::int64_t tid = nextThreadId.fetch_add(1, std::memory_order_relaxed); #ifdef _WIN32 void* nativeHandle = nullptr; #else - std::thread _thr; + pthread_t hostThread{}; #endif std::thread::id threadId; std::atomic references{2}; + std::atomic inWait{false}; + std::atomic pendingException{0}; + std::atomic schedPolicy{1}; + void* wakeEvent = nullptr; void* stackAddress = nullptr; std::size_t stackSize = 0; std::atomic waitCount{0}; std::atomic affinity{DEFAULT_THREAD_AFFINITY}; std::atomic priority{DEFAULT_THREAD_PRIORITY}; + int scope = GUEST_SCOPE_SYSTEM; int cpuClockThread = 0; #ifndef _WIN32 clockid_t hostCpuClock{}; bool hostCpuClockBound = false; + void* guestStack = nullptr; + std::size_t guestStackBytes = 0; + int hostTid = 0; #endif std::mutex nameLock; std::string name; @@ -111,17 +143,35 @@ struct PthreadPrivate { void* _retval; bool _detached; bool _adopted; + Pthread* _handleStorage; + std::atomic _joined; + bool _joining = false; std::mutex _join_mtx; - std::condition_variable _join_cv; + TimedWait::Condition _join_cv; + std::atomic cancelPending{false}; + std::mutex cancelLock; + TimedWait::Condition* cancelWait = nullptr; - PthreadPrivate(); + explicit PthreadPrivate(const PthreadAttrPrivate* attr = nullptr); ~PthreadPrivate(); PthreadPrivate(const PthreadPrivate&) = delete; PthreadPrivate& operator=(const PthreadPrivate&) = delete; }; 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); #endif diff --git a/core/libs/prx/libkernel/Pthread/include/ThreadLifecycle.hpp b/core/libs/prx/libkernel/Pthread/include/ThreadLifecycle.hpp index f342bb0ca0..68deb724f5 100644 --- a/core/libs/prx/libkernel/Pthread/include/ThreadLifecycle.hpp +++ b/core/libs/prx/libkernel/Pthread/include/ThreadLifecycle.hpp @@ -8,6 +8,7 @@ public: static void SetThreadDtors(thread_dtors_func_t callback); static void SetThreadAtexitCount(get_thread_atexit_count_func_t callback); static void SetThreadAtexitReport(thread_atexit_report_func_t callback); + static void RunKeyDtors(); }; extern "C" void APS5_VABI scePthreadExit(void* retval); diff --git a/core/libs/prx/libkernel/Pthread/include/WindowsThreadLocal.hpp b/core/libs/prx/libkernel/Pthread/include/WindowsThreadLocal.hpp new file mode 100644 index 0000000000..1cd1f7ca70 --- /dev/null +++ b/core/libs/prx/libkernel/Pthread/include/WindowsThreadLocal.hpp @@ -0,0 +1,30 @@ +#pragma once + +#ifdef _WIN32 +#include +#include +#include +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(); + if (!FlsSetValue(slot, value.get())) throw std::runtime_error("Cannot bind native thread state"); + return *value.release(); + } + +private: + static void WINAPI Destroy(void* value) { + FlsSetValue(slotIndex(), value); + delete static_cast(value); + FlsSetValue(slotIndex(), nullptr); + } + static DWORD slotIndex() { + static const DWORD slot = FlsAlloc(Destroy); + return slot; + } +}; +#endif diff --git a/core/libs/prx/libkernel/Pthread/src/Attr.cpp b/core/libs/prx/libkernel/Pthread/src/Attr.cpp index b31c92b747..ce6ccac93c 100644 --- a/core/libs/prx/libkernel/Pthread/src/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Attr.cpp @@ -33,29 +33,31 @@ int APS5_VABI scePthreadAttrInit(PthreadAttr* attr) { } int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; delete *attr; *attr = nullptr; return SCE_OK; } int APS5_VABI scePthreadAttrSetdetachstate(PthreadAttr* attr, int detachstate) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); - if (detachstate != DETACH_JOINABLE && detachstate != DETACH_DETACHED) - throw std::runtime_error(std::string(__func__) + ": invalid state"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; + if (detachstate != DETACH_JOINABLE && detachstate != DETACH_DETACHED) return SCE_KERNEL_ERROR_EINVAL; (*attr)->_detachstate = detachstate; return SCE_OK; } int APS5_VABI scePthreadAttrSetschedparam(PthreadAttr* attr, const KernelSchedParam* param) { - if (!attr || !*attr || !param) - throw std::runtime_error(std::string(__func__) + ": null arg"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; + constexpr int HighestPriority = 256; + constexpr int LowestPriority = 767; + if (!param || param->sched_priority < HighestPriority || param->sched_priority > LowestPriority) + return SCE_KERNEL_ERROR_EOPNOTSUPP; (*attr)->_schedpriority = param->sched_priority; return SCE_OK; } int APS5_VABI scePthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stacksize) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; if (stacksize < MIN_STACK_SIZE) return SCE_KERNEL_ERROR_EINVAL; #ifdef _WIN32 if (stacksize > std::numeric_limits::max() - 0xffffu) @@ -66,8 +68,7 @@ int APS5_VABI scePthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stacksiz } int APS5_VABI scePthreadAttrGetstack(const PthreadAttr* attr, void** stackaddr, std::size_t* stacksize) { - if (!attr || !*attr || !stackaddr || !stacksize) - throw std::runtime_error(std::string(__func__) + ": null arg"); + if (!attr || !*attr || !stackaddr || !stacksize) return SCE_KERNEL_ERROR_EINVAL; *stackaddr = (*attr)->stackAddress; *stacksize = (*attr)->_stacksize; return SCE_OK; @@ -76,104 +77,125 @@ 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; + (*attr)->_scope = thread->scope; return SCE_OK; } int APS5_VABI scePthreadAttrGetaffinity(const PthreadAttr* attr, KernelCpumask* mask) { - if (!attr || !*attr || !mask) throw std::runtime_error("scePthreadAttrGetaffinity: null argument"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; + if (!mask) throw std::runtime_error("scePthreadAttrGetaffinity: null argument"); *mask = (*attr)->_affinity; return SCE_OK; } int APS5_VABI scePthreadAttrGetdetachstate(const PthreadAttr* attr, int* state) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); - if (!state) throw std::runtime_error(std::string(__func__) + ": null output"); + if (!attr || !*attr || !state) return SCE_KERNEL_ERROR_EINVAL; *state = (*attr)->_detachstate; return SCE_OK; } 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) throw std::runtime_error(std::string(__func__) + ": null attr"); - if (!param) throw std::runtime_error(std::string(__func__) + ": null output"); + 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) throw std::runtime_error(std::string(__func__) + ": null attr"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; if (!solosched) throw std::runtime_error(std::string(__func__) + ": null output"); *solosched = (*attr)->_solosched; return SCE_OK; } 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; } int APS5_VABI scePthreadAttrGetstacksize(const PthreadAttr* attr, size_t* stack_size) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); - if (!stack_size) throw std::runtime_error(std::string(__func__) + ": null output"); + if (!attr || !*attr || !stack_size) return SCE_KERNEL_ERROR_EINVAL; *stack_size = (*attr)->_stacksize; return SCE_OK; } int APS5_VABI scePthreadAttrSetaffinity(PthreadAttr* attr, KernelCpumask mask) { - if (!attr || !*attr) throw std::runtime_error("scePthreadAttrSetaffinity: null attr"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; (*attr)->_affinity = mask; + (*attr)->_affinityExplicit = true; return SCE_OK; } 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; } int APS5_VABI scePthreadAttrSetinheritsched(PthreadAttr* attr, int inherit_sched) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; + constexpr int ExplicitSched = 0; + constexpr int InheritSched = 4; + if (inherit_sched != ExplicitSched && inherit_sched != InheritSched) return SCE_KERNEL_ERROR_EOPNOTSUPP; (*attr)->_inheritsched = inherit_sched; return SCE_OK; } int APS5_VABI scePthreadAttrSetschedpolicy(PthreadAttr* attr, int policy) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; + constexpr int SchedRr = 3; + if (policy != SCHED_FIFO_PS5 && policy != SchedRr) return SCE_KERNEL_ERROR_EOPNOTSUPP; (*attr)->_schedpolicy = policy; return SCE_OK; } int APS5_VABI scePthreadAttrSetsolosched(PthreadAttr* attr, int solosched) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; + constexpr int SoloSchedOff = 0; + constexpr int SoloSchedOn = 16; + if (solosched != SoloSchedOff && solosched != SoloSchedOn) return SCE_KERNEL_ERROR_EOPNOTSUPP; (*attr)->_solosched = solosched; return SCE_OK; } 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 c18ad3d53b..609acd98aa 100644 --- a/core/libs/prx/libkernel/Pthread/src/Cond.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Cond.cpp @@ -1,7 +1,10 @@ #include "prx/libkernel/Pthread/include/Pthread.hpp" #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" #include "prx/libkernel/Time/include/TimedWait.hpp" #include @@ -11,6 +14,7 @@ #include #include #include +#include namespace { @@ -21,35 +25,70 @@ PthreadCond destroyedCond() { return reinterpret_cast(std::uintptr_t{2}); } -PthreadCond resolveCond(PthreadCond* cond) { +PthreadMutex destroyedMutex() { + return reinterpret_cast(std::uintptr_t{2}); +} + +PthreadMutex adaptiveInitializer() { + return reinterpret_cast(std::uintptr_t{1}); +} + +std::shared_ptr resolveCond(PthreadCond* cond, int* clockId = nullptr) { if (!cond) throw std::invalid_argument("Condition variable pointer is null"); - 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); + std::atomic_ref slot(*cond); + 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; } -PthreadMutex lockedMutex(PthreadMutex* mutex) { - if (!mutex || !*mutex) +int mutexOwnership(PthreadMutex* mutex, PthreadMutex* owned) { + if (!mutex) throw std::invalid_argument("Mutex pointer is null"); - auto* current = *mutex; - if (current->_owner.load(std::memory_order_acquire) != std::this_thread::get_id()) - throw std::runtime_error("Condition wait mutex is not owned by the current thread"); - return current; + const auto current = std::atomic_ref(*mutex).load(std::memory_order_acquire); + if (current == destroyedMutex()) + return SCE_KERNEL_ERROR_EINVAL; + if (!current || current == adaptiveInitializer() || current->_owner.load(std::memory_order_acquire) != std::this_thread::get_id()) + return SCE_KERNEL_ERROR_EPERM; + *owned = current; + return 0; } -int waitUntil(PthreadCond* cond, PthreadMutex* mutex, std::optional deadlineNanos, const void* caller) { - auto* c = resolveCond(cond); - auto* m = lockedMutex(mutex); +template +struct WakingLock { + PthreadMutex mutex; + TNative& native; + void lock() { native.lock(); } + void unlock() { + native.unlock(); +#ifdef _WIN32 + TimedWait::Unpark(mutex); +#endif + } +}; + +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; bool timedOut = false; const auto waitStart = std::chrono::steady_clock::now(); struct Trace { @@ -57,37 +96,41 @@ int waitUntil(PthreadCond* cond, PthreadMutex* mutex, std::optional(std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count()), timedOut); } } trace{caller, timedOut, waitStart}; if (m->_type == MutexType::Recursive) { - std::unique_lock lock(m->_rmtx, std::adopt_lock); + WakingLock waking{m, m->_rmtx}; + std::unique_lock lock(waking, std::adopt_lock); const auto previousCount = m->_count; m->_count = 0; m->_owner.store(std::thread::id{}, std::memory_order_release); for (int level = 1; level < previousCount; ++level) m->_rmtx.unlock(); - if (deadlineNanos) timedOut = !c->_cv.WaitUntil(lock, *deadlineNanos); - else c->_cv.Wait(lock); + 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(); - return timedOut ? sceTimedOut : 0; + return finishWait(condition, timedOut); } - std::unique_lock lock(m->_mtx, std::adopt_lock); + WakingLock waking{m, m->_mtx}; + std::unique_lock lock(waking, std::adopt_lock); m->_owner.store(std::thread::id{}, std::memory_order_release); - if (deadlineNanos) timedOut = !c->_cv.WaitUntil(lock, *deadlineNanos); - else c->_cv.Wait(lock); + timedOut = !ThreadCancel::Wait(*condition, lock, deadlineNanos); m->_owner.store(std::this_thread::get_id(), std::memory_order_release); lock.release(); - 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" { @@ -100,16 +143,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; } @@ -117,10 +158,8 @@ int APS5_VABI scePthreadCondattrSetclock(PthreadCondattr* attr, KernelClockid cl int APS5_VABI scePthreadCondInit(PthreadCond* cond, const PthreadCondattr* attr, const char*) { if (!cond) throw std::invalid_argument("Condition variable pointer is null"); - if (attr && !*attr) - throw std::invalid_argument("Condition attributes are not initialized"); auto replacement = std::make_unique(); - if (attr) + if (attr && *attr) replacement->_clockid = (*attr)->_clockid; std::lock_guard lock(condInitializationMutex); std::atomic_ref(*cond).store(replacement.release(), std::memory_order_release); @@ -132,34 +171,43 @@ int APS5_VABI scePthreadCondDestroy(PthreadCond* cond) { throw std::invalid_argument("Condition variable pointer is null"); std::lock_guard lock(condInitializationMutex); 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 9d5ac1c423..876bd9ee20 100644 --- a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp @@ -2,6 +2,7 @@ #include "../include/Mutex.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" +#include #include #include #include @@ -38,7 +39,7 @@ PthreadMutex resolveMutex(PthreadMutex* mutex, bool initialize) { 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) @@ -52,6 +53,8 @@ PthreadMutex resolveMutex(PthreadMutex* mutex, bool initialize) { 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) { @@ -87,7 +90,21 @@ int MutexOperations::Timedlock(PthreadMutex* mutex, const KernelTimespec* abstim if (std::chrono::duration(duration) >= std::chrono::duration(std::chrono::system_clock::duration::max())) throw std::overflow_error("Absolute mutex timeout exceeds the host clock range"); const auto deadline = std::chrono::system_clock::time_point(std::chrono::duration_cast(duration)); - return acquireMutex(resolveMutex(mutex, true), [&](auto& native) { return native.try_lock_until(deadline); }, sceTimedOut, false); + auto* current = resolveMutex(mutex, true); + return acquireMutex(current, [&](auto& native) { +#ifdef _WIN32 + for (;;) { + const auto now = std::chrono::system_clock::now(); + if (now >= deadline) return native.try_lock(); + const auto remaining = std::chrono::duration_cast(deadline - now).count(); + const std::uint64_t slice = TimedWait::NowNanos() + static_cast(std::min(remaining, 100000000)); + if (TimedWait::ParkAcquire(current, [&] { return native.try_lock(); }, &slice)) return true; + } +#else + static_cast(current); + return native.try_lock_until(deadline); +#endif + }, sceTimedOut, false); } extern "C" { @@ -101,7 +118,7 @@ int APS5_VABI scePthreadMutexattrInit(PthreadMutexattr* attr) { int APS5_VABI scePthreadMutexattrDestroy(PthreadMutexattr* attr) { if (!attr || !*attr) - throw std::invalid_argument("Mutex attributes are not initialized"); + return SCE_KERNEL_ERROR_EINVAL; delete *attr; *attr = nullptr; return 0; @@ -109,7 +126,7 @@ int APS5_VABI scePthreadMutexattrDestroy(PthreadMutexattr* attr) { int APS5_VABI scePthreadMutexattrSettype(PthreadMutexattr* attr, int type) { if (!attr || !*attr) - throw std::invalid_argument("Mutex attributes are not initialized"); + return SCE_KERNEL_ERROR_EINVAL; switch (type) { case 1: (*attr)->type = MutexType::ErrorCheck; break; case 2: (*attr)->type = MutexType::Recursive; break; @@ -122,21 +139,19 @@ int APS5_VABI scePthreadMutexattrSettype(PthreadMutexattr* attr, int type) { int APS5_VABI scePthreadMutexattrSetprotocol(PthreadMutexattr* attr, int protocol) { if (!attr || !*attr) - throw std::invalid_argument("Mutex attributes are not initialized"); + return SCE_KERNEL_ERROR_EINVAL; constexpr int PrioNone = 0; - constexpr int PrioInherit = 1; - if (protocol != PrioNone && protocol != PrioInherit) - throw std::invalid_argument("Mutex priority protection is unsupported"); + constexpr int PrioProtect = 2; + if (protocol < PrioNone || protocol > PrioProtect) + return SCE_KERNEL_ERROR_EINVAL; return 0; } int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* attr, const char*) { if (!mutex) throw std::invalid_argument("Mutex pointer is null"); - if (attr && !*attr) - throw std::invalid_argument("Mutex attributes are not initialized"); auto replacement = std::make_unique(); - if (attr) + if (attr && *attr) replacement->_type = (*attr)->type; std::lock_guard lock(initializationMutex); std::atomic_ref(*mutex).store(replacement.release(), std::memory_order_release); @@ -148,7 +163,7 @@ int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex) { throw std::invalid_argument("Mutex pointer is null"); std::lock_guard lock(initializationMutex); if (*mutex == destroyedMutex()) - throw std::runtime_error("Mutex has already been destroyed"); + return SCE_KERNEL_ERROR_EINVAL; if (*mutex == adaptiveInitializer()) { std::atomic_ref(*mutex).store(destroyedMutex(), std::memory_order_release); return 0; @@ -161,28 +176,47 @@ int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex) { } int APS5_VABI scePthreadMutexLock(PthreadMutex* mutex) { - return acquireMutex(resolveMutex(mutex, true), [](auto& native) { native.lock(); return true; }, 0, false); + auto* current = resolveMutex(mutex, true); + return acquireMutex(current, [current](auto& native) { +#ifdef _WIN32 + return TimedWait::ParkAcquire(current, [&] { return native.try_lock(); }, nullptr); +#else + static_cast(current); + native.lock(); + return true; +#endif + }, 0, false); } 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; if (current->_type == MutexType::Recursive) { - if (--current->_count == 0) + released = --current->_count == 0; + if (released) current->_owner.store(std::thread::id{}, std::memory_order_release); current->_rmtx.unlock(); } else { current->_owner.store(std::thread::id{}, std::memory_order_release); current->_mtx.unlock(); } +#ifdef _WIN32 + if (released) TimedWait::Unpark(current); +#else + static_cast(released); +#endif return 0; } int APS5_VABI scePthreadMutexTimedlock(PthreadMutex* mutex, KernelUseconds usec) { const auto deadline = TimedWait::DeadlineNanos(usec); - return acquireMutex(resolveMutex(mutex, true), [=](auto& native) { - return TimedWait::AcquireUntil(deadline, [&] { return native.try_lock(); }, [&](std::uint64_t micros) { return native.try_lock_for(std::chrono::microseconds(micros)); }); + auto* current = resolveMutex(mutex, true); + return acquireMutex(current, [=](auto& native) { + return TimedWait::AcquireUntil(current, deadline, [&] { return native.try_lock(); }, [&](std::uint64_t micros) { return native.try_lock_for(std::chrono::microseconds(micros)); }); }, sceTimedOut, false); } diff --git a/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp b/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp index 37a7668a6d..35a043a8e9 100644 --- a/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp @@ -2,30 +2,73 @@ #include "../include/Rwlock.hpp" #include "prx/libc/include/General.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" +#include #include #include #include static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_EPERM = 0x80020001; static constexpr int SCE_KERNEL_ERROR_ENOMEM = 0x8002000C; static constexpr int SCE_KERNEL_ERROR_EDEADLK = 0x8002000B; static constexpr int SCE_KERNEL_ERROR_EBUSY = 0x80020010; static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = 0x8002003C; +static constexpr int SCE_KERNEL_ERROR_EINVAL = 0x80020016; + +static PthreadRwlockPrivate* const DestroyedRwlock = reinterpret_cast(std::uintptr_t{1}); static PthreadRwlockPrivate* RequireRwlock(PthreadRwlock* rwlock, const char* funcName) { - if (!rwlock || !*rwlock) throw std::runtime_error(std::string(funcName) + ": null rwlock"); - return *rwlock; + if (!rwlock) throw std::runtime_error(std::string(funcName) + ": null rwlock"); + std::atomic_ref slot(*rwlock); + PthreadRwlock current = slot.load(std::memory_order_acquire); + if (current == nullptr) { + auto* created = new PthreadRwlockPrivate(); + if (slot.compare_exchange_strong(current, created, std::memory_order_acq_rel, std::memory_order_acquire)) + current = created; + else + delete created; + } + return current == DestroyedRwlock ? nullptr : current; } static bool OwnsWrite(const PthreadRwlockPrivate* lock) { return lock->_writer.load(std::memory_order_acquire) == std::this_thread::get_id(); } +static thread_local int readLocksHeld = 0; + +static bool ReaderMayEnter(const PthreadRwlockPrivate* lock) { + return lock->_writer.load(std::memory_order_acquire) == std::thread::id{} && (lock->_waitingWriters == 0 || readLocksHeld != 0); +} + +static bool WriterMayEnter(const PthreadRwlockPrivate* lock) { + return lock->_writer.load(std::memory_order_acquire) == std::thread::id{} && lock->_readers.load(std::memory_order_acquire) == 0; +} + +static int EnterRead(PthreadRwlockPrivate* lock) { + lock->_readers.fetch_add(1, std::memory_order_release); + ++readLocksHeld; + return SCE_OK; +} + +static int EnterWrite(PthreadRwlockPrivate* lock) { + lock->_writer.store(std::this_thread::get_id(), std::memory_order_release); + return SCE_OK; +} + extern "C" { int APS5_VABI scePthreadRwlockDestroy(PthreadRwlock* rwlock) { - delete RequireRwlock(rwlock, __func__); - *rwlock = nullptr; + if (!rwlock) throw std::runtime_error("scePthreadRwlockDestroy: null rwlock"); + std::atomic_ref slot(*rwlock); + PthreadRwlock lock = slot.load(std::memory_order_acquire); + do { + if (lock == nullptr) return SCE_OK; + if (lock == DestroyedRwlock) return SCE_KERNEL_ERROR_EINVAL; + } while (!slot.compare_exchange_weak(lock, DestroyedRwlock, std::memory_order_acq_rel, std::memory_order_acquire)); + if (lock->_writer.load(std::memory_order_acquire) == std::thread::id{} && + lock->_readers.load(std::memory_order_acquire) == 0) + delete lock; return SCE_OK; } @@ -41,56 +84,77 @@ int APS5_VABI scePthreadRwlockInit(PthreadRwlock* rwlock, const PthreadRwlockatt int APS5_VABI scePthreadRwlockRdlock(PthreadRwlock* rwlock) { auto* lock = RequireRwlock(rwlock, __func__); + if (!lock) return SCE_KERNEL_ERROR_EINVAL; if (OwnsWrite(lock)) return SCE_KERNEL_ERROR_EDEADLK; - lock->_lock.lock_shared(); - return SCE_OK; + std::unique_lock state(lock->_state); + lock->_changed.Wait(state, [&] { return ReaderMayEnter(lock); }); + return EnterRead(lock); } int APS5_VABI scePthreadRwlockTryrdlock(PthreadRwlock* rwlock) { auto* lock = RequireRwlock(rwlock, __func__); + if (!lock) return SCE_KERNEL_ERROR_EINVAL; if (OwnsWrite(lock)) return SCE_KERNEL_ERROR_EBUSY; - return lock->_lock.try_lock_shared() ? SCE_OK : SCE_KERNEL_ERROR_EBUSY; + std::lock_guard state(lock->_state); + return ReaderMayEnter(lock) ? EnterRead(lock) : SCE_KERNEL_ERROR_EBUSY; } int APS5_VABI scePthreadRwlockTimedrdlock(PthreadRwlock* rwlock, KernelUseconds usec) { auto* lock = RequireRwlock(rwlock, __func__); + if (!lock) return SCE_KERNEL_ERROR_EINVAL; if (OwnsWrite(lock)) return SCE_KERNEL_ERROR_EDEADLK; - const bool locked = TimedWait::AcquireUntil(TimedWait::DeadlineNanos(usec), [&] { return lock->_lock.try_lock_shared(); }, [&](std::uint64_t micros) { return lock->_lock.try_lock_shared_for(std::chrono::microseconds(micros)); }); - return locked ? SCE_OK : SCE_KERNEL_ERROR_ETIMEDOUT; + const std::uint64_t deadline = TimedWait::DeadlineNanos(usec); + std::unique_lock state(lock->_state); + if (!lock->_changed.WaitUntil(state, deadline, [&] { return ReaderMayEnter(lock); })) return SCE_KERNEL_ERROR_ETIMEDOUT; + return EnterRead(lock); } int APS5_VABI scePthreadRwlockWrlock(PthreadRwlock* rwlock) { auto* lock = RequireRwlock(rwlock, __func__); + if (!lock) return SCE_KERNEL_ERROR_EINVAL; if (OwnsWrite(lock)) return SCE_KERNEL_ERROR_EDEADLK; - lock->_lock.lock(); - lock->_writer.store(std::this_thread::get_id(), std::memory_order_release); - return SCE_OK; + std::unique_lock state(lock->_state); + ++lock->_waitingWriters; + lock->_changed.Wait(state, [&] { return WriterMayEnter(lock); }); + --lock->_waitingWriters; + return EnterWrite(lock); } int APS5_VABI scePthreadRwlockTrywrlock(PthreadRwlock* rwlock) { auto* lock = RequireRwlock(rwlock, __func__); - if (OwnsWrite(lock) || !lock->_lock.try_lock()) return SCE_KERNEL_ERROR_EBUSY; - lock->_writer.store(std::this_thread::get_id(), std::memory_order_release); - return SCE_OK; + if (!lock) return SCE_KERNEL_ERROR_EINVAL; + if (OwnsWrite(lock)) return SCE_KERNEL_ERROR_EBUSY; + std::lock_guard state(lock->_state); + return WriterMayEnter(lock) ? EnterWrite(lock) : SCE_KERNEL_ERROR_EBUSY; } int APS5_VABI scePthreadRwlockTimedwrlock(PthreadRwlock* rwlock, KernelUseconds usec) { auto* lock = RequireRwlock(rwlock, __func__); + if (!lock) return SCE_KERNEL_ERROR_EINVAL; if (OwnsWrite(lock)) return SCE_KERNEL_ERROR_EDEADLK; - const bool locked = TimedWait::AcquireUntil(TimedWait::DeadlineNanos(usec), [&] { return lock->_lock.try_lock(); }, [&](std::uint64_t micros) { return lock->_lock.try_lock_for(std::chrono::microseconds(micros)); }); - if (!locked) return SCE_KERNEL_ERROR_ETIMEDOUT; - lock->_writer.store(std::this_thread::get_id(), std::memory_order_release); - return SCE_OK; + const std::uint64_t deadline = TimedWait::DeadlineNanos(usec); + std::unique_lock state(lock->_state); + ++lock->_waitingWriters; + const bool acquired = lock->_changed.WaitUntil(state, deadline, [&] { return WriterMayEnter(lock); }); + --lock->_waitingWriters; + if (acquired) return EnterWrite(lock); + lock->_changed.NotifyAll(); + return SCE_KERNEL_ERROR_ETIMEDOUT; } int APS5_VABI scePthreadRwlockUnlock(PthreadRwlock* rwlock) { auto* lock = RequireRwlock(rwlock, __func__); + if (!lock) return SCE_KERNEL_ERROR_EINVAL; + std::unique_lock state(lock->_state); if (OwnsWrite(lock)) { lock->_writer.store(std::thread::id{}, std::memory_order_release); - lock->_lock.unlock(); } else { - lock->_lock.unlock_shared(); + if (lock->_writer.load(std::memory_order_acquire) != std::thread::id{}) return SCE_KERNEL_ERROR_EPERM; + if (lock->_readers.load(std::memory_order_acquire) == 0) return SCE_KERNEL_ERROR_EPERM; + if (readLocksHeld != 0) --readLocksHeld; + if (lock->_readers.fetch_sub(1, std::memory_order_acq_rel) != 1) return SCE_OK; } + lock->_changed.NotifyAll(); return SCE_OK; } diff --git a/core/libs/prx/libkernel/Pthread/src/Sem.cpp b/core/libs/prx/libkernel/Pthread/src/Sem.cpp index 7a1d26c3f4..ea9006380e 100644 --- a/core/libs/prx/libkernel/Pthread/src/Sem.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Sem.cpp @@ -1,5 +1,7 @@ #include "../include/Pthread.hpp" +#include "../include/Cancel.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" #include #include @@ -10,6 +12,7 @@ namespace { constexpr int sceBusy = static_cast(0x80020010u); constexpr int sceTimedOut = static_cast(0x8002003cu); +constexpr int sceSemValueMax = 0x7fffffff; std::mutex semInitializationMutex; PthreadSem destroyedSem() { @@ -33,6 +36,8 @@ extern "C" { int APS5_VABI scePthreadSemInit(PthreadSem* sem, int flag, unsigned int value, const char* name) { if (!sem) throw std::invalid_argument("Semaphore pointer is null"); + if (value > static_cast(sceSemValueMax)) + return SCE_KERNEL_ERROR_EINVAL; (void)flag; (void)name; auto replacement = std::make_unique(value); @@ -57,6 +62,8 @@ int APS5_VABI scePthreadSemPost(PthreadSem* sem) { auto* current = resolveSem(sem); { std::lock_guard lock(current->_mutex); + if (current->_count == sceSemValueMax) + return SCE_KERNEL_ERROR_EOVERFLOW; ++current->_count; } current->_cv.NotifyOne(); @@ -65,8 +72,9 @@ int APS5_VABI scePthreadSemPost(PthreadSem* sem) { int APS5_VABI scePthreadSemWait(PthreadSem* sem) { auto* current = resolveSem(sem); + ThreadCancel::Check(); std::unique_lock lock(current->_mutex); - current->_cv.Wait(lock, [&] { return current->_count > 0; }); + ThreadCancel::WaitUntil(current->_cv, lock, std::nullopt, [&] { return current->_count > 0; }); --current->_count; return 0; } @@ -82,8 +90,9 @@ int APS5_VABI scePthreadSemTrywait(PthreadSem* sem) { int APS5_VABI scePthreadSemTimedwait(PthreadSem* sem, KernelUseconds usec) { auto* current = resolveSem(sem); + ThreadCancel::Check(); std::unique_lock lock(current->_mutex); - const auto acquired = current->_cv.WaitUntil(lock, TimedWait::DeadlineNanos(usec), [&] { return current->_count > 0; }); + const auto acquired = ThreadCancel::WaitUntil(current->_cv, lock, TimedWait::DeadlineNanos(usec), [&] { return current->_count > 0; }); if (!acquired) return sceTimedOut; --current->_count; diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index ff49d02551..cd2cab33c0 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -1,14 +1,22 @@ #include "../include/Pthread.hpp" #include "../include/ThreadLifecycle.hpp" +#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 #include #include #include +#include #include #include +#include #include #include #include @@ -19,54 +27,198 @@ #ifndef _WIN32 #include #include +#include #include +#include +#include #endif 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; #ifdef _WIN32 #include #include -#include +#include #endif struct ThreadArgs { PthreadEntry entry; void* arg; PthreadPrivate* self; +#ifndef _WIN32 + sigset_t signalMask; +#endif }; static std::atomic threadDtors{nullptr}; static std::atomic threadAtexitCount{nullptr}; static std::atomic threadAtexitReport{nullptr}; -static thread_local PthreadPrivate* currentThread = nullptr; -static thread_local bool threadFinishing = false; + +struct GuestThreadState { + PthreadPrivate* current = nullptr; + bool finishing = false; + bool exiting = false; + int cancelState = GUEST_CANCEL_ENABLE; + int cancelType = GUEST_CANCEL_DEFERRED; + CleanupHandler* cleanupHandlers = nullptr; +}; +static GuestThreadState& CurrentGuestState() { +#ifdef _WIN32 + return WindowsThreadLocal::Get(); +#else + static thread_local GuestThreadState state; + return state; +#endif +} static std::mutex stackLock; static std::map> liveStacks; +static std::map protectedStackPages; +#if defined(__linux__) +struct GuestStackMapping { + std::uintptr_t mappingEnd; + std::uintptr_t stackStart; + std::uintptr_t stackEnd; +}; +static std::mutex guestStackMappingsLock; +static std::map guestStackMappings; + +static void RegisterGuestStackMapping(PthreadPrivate* thread) { + const auto mappingStart = reinterpret_cast(thread->guestStack); + const auto stackStart = reinterpret_cast(thread->stackAddress); + std::lock_guard lock(guestStackMappingsLock); + guestStackMappings[mappingStart] = {mappingStart + thread->guestStackBytes, stackStart, stackStart + thread->stackSize}; +} + +static void UnregisterGuestStackMapping(void* mapping) { + std::lock_guard lock(guestStackMappingsLock); + guestStackMappings.erase(reinterpret_cast(mapping)); +} +#endif + +#if defined(__linux__) +static constexpr std::size_t GUEST_STACK_GUARD = PS5_PAGE_SIZE; + +struct RetiredStack { + void* address; + std::size_t bytes; + int hostTid; +}; + +static std::mutex retiredStacksLock; +static std::vector retiredStacks; + +static void AllocateGuestStack(PthreadPrivate* thread, std::size_t usableBytes) { + const std::size_t bytes = (usableBytes + PS5_PAGE_SIZE - 1) / PS5_PAGE_SIZE * PS5_PAGE_SIZE + GUEST_STACK_GUARD; + void* stack = nullptr; + if (DoMapAnon(&stack, bytes, 3, 0) != 0) + throw std::runtime_error("scePthreadCreate: cannot map the guest thread stack"); + if (DoMprotect(stack, GUEST_STACK_GUARD, 0) != 0) { + DoMunmap(stack, bytes); + throw std::runtime_error("scePthreadCreate: cannot protect the guest thread stack guard"); + } + thread->guestStack = stack; + thread->guestStackBytes = bytes; +} + +static void FreeGuestStack(PthreadPrivate* thread) { + if (thread->guestStack == nullptr) return; + void* const mapping = thread->guestStack; + UnregisterGuestStackMapping(mapping); + DoMunmap(thread->guestStack, thread->guestStackBytes); + thread->guestStack = nullptr; + thread->guestStackBytes = 0; +} + +static bool HostThreadGone(int hostTid) { + if (hostTid == 0) return false; + char path[48]; + std::snprintf(path, sizeof(path), "/proc/self/task/%d", hostTid); + return access(path, F_OK) != 0; +} + +static void ReapRetiredStacks() { + std::vector ready; + { + std::lock_guard lock(retiredStacksLock); + for (auto it = retiredStacks.begin(); it != retiredStacks.end();) { + if (HostThreadGone(it->hostTid)) { + ready.push_back(*it); + it = retiredStacks.erase(it); + } else { + ++it; + } + } + } + for (const auto& stack : ready) { + UnregisterGuestStackMapping(stack.address); + DoMunmap(stack.address, stack.bytes); + } +} + +static void RetireGuestStack(PthreadPrivate* thread) { + if (thread->guestStack == nullptr) return; + { + std::lock_guard lock(retiredStacksLock); + retiredStacks.push_back({thread->guestStack, thread->guestStackBytes, thread->hostTid}); + } + thread->guestStack = nullptr; + thread->guestStackBytes = 0; +} +#else +static void FreeGuestStack(PthreadPrivate*) {} +static void RetireGuestStack(PthreadPrivate*) {} +#endif + +#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; } @@ -80,9 +232,9 @@ static void SetStackFromHost(PthreadPrivate* thread) { } static bool CurrentStack(std::uintptr_t* low, std::uintptr_t* high) { - if (currentThread && currentThread->stackAddress) { - *low = reinterpret_cast(currentThread->stackAddress); - *high = *low + currentThread->stackSize; + if (CurrentGuestState().current && CurrentGuestState().current->stackAddress) { + *low = reinterpret_cast(CurrentGuestState().current->stackAddress); + *high = *low + CurrentGuestState().current->stackSize; return true; } return HostStackLimits(low, high); @@ -97,8 +249,83 @@ static void RegisterStack(PthreadPrivate* self) { liveStacks[self] = {low, high}; } +static void RestoreStackPages(std::uintptr_t start, std::size_t bytes) noexcept { +#ifdef _WIN32 + DWORD 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 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()) { + const auto [low, high] = found->second; + for (auto it = protectedStackPages.begin(); it != protectedStackPages.end();) { + if (it->first < high && it->first + it->second > low) { + RestoreStackPages(it->first, it->second); + it = protectedStackPages.erase(it); + } else { + ++it; + } + } + } liveStacks.erase(self); } @@ -121,10 +348,29 @@ bool GuestThreadStack(std::uintptr_t address, std::uintptr_t* start, std::uintpt return false; } +bool GuestThreadStackHostRangeOverlaps(std::uintptr_t start, std::uintptr_t end) { +#if defined(__linux__) + std::lock_guard lock(guestStackMappingsLock); + auto it = guestStackMappings.upper_bound(start); + if (it != guestStackMappings.begin()) --it; + for (; it != guestStackMappings.end() && it->first < end; ++it) { + const auto mappingStart = it->first; + const auto& stack = it->second; + if (start < stack.stackStart && end > mappingStart) return true; + if (start < stack.mappingEnd && end > stack.stackEnd) return true; + } +#else + (void)start; + (void)end; +#endif + return false; +} + struct CpuClockRegistry { std::mutex lock; std::map threads; int nextThread = 100000; + KernelCpumask processAffinity = DEFAULT_THREAD_AFFINITY; }; static CpuClockRegistry& CpuClocks() { @@ -132,9 +378,18 @@ static CpuClockRegistry& CpuClocks() { return *registry; } -PthreadPrivate::PthreadPrivate() : _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(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++; @@ -166,6 +421,69 @@ int GuestThreadCpuClockId(const PthreadPrivate* thread) { return static_cast(CPU_CLOCK_BIT | static_cast(thread->cpuClockThread)); } +void SetLiveGuestThreadsAffinity(KernelCpumask mask) { + auto& registry = CpuClocks(); + std::lock_guard lock(registry.lock); + registry.processAffinity = mask; + for (const auto& [id, thread] : registry.threads) { + if (!thread->_finished.load(std::memory_order_acquire)) thread->affinity.store(mask, std::memory_order_relaxed); + } +} + +KernelCpumask LiveGuestThreadsAffinity() { + auto& registry = CpuClocks(); + std::lock_guard lock(registry.lock); + KernelCpumask mask = 0; + for (const auto& [id, thread] : registry.threads) { + if (!thread->_finished.load(std::memory_order_acquire)) mask |= thread->affinity.load(std::memory_order_relaxed); + } + 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, thread] : registry.threads) { + if (thread->tid != tid || thread->_finished.load(std::memory_order_acquire)) continue; + mask = thread->affinity.load(std::memory_order_relaxed); + return true; + } + return false; +} + +bool SetLiveGuestThreadAffinity_nid_no_patch(std::int64_t tid, KernelCpumask mask) { + auto& registry = CpuClocks(); + std::lock_guard lock(registry.lock); + for (const auto& [id, thread] : registry.threads) { + if (thread->tid != tid || thread->_finished.load(std::memory_order_acquire)) continue; + thread->affinity.store(mask, std::memory_order_relaxed); + return true; + } + return false; +} + +bool LiveGuestThreadExists(std::int64_t tid) { + auto& registry = CpuClocks(); + std::lock_guard lock(registry.lock); + for (const auto& [id, thread] : registry.threads) { + if (!thread->_finished.load(std::memory_order_acquire) && thread->tid == tid) return true; + } + return false; +} + bool GuestThreadCpuNanos(int cpuClockThread, std::uint64_t* nanos) { auto& registry = CpuClocks(); std::lock_guard lock(registry.lock); @@ -211,6 +529,29 @@ static thread_local std::unique_ptr adoptedThread; // and aborts. scePthreadExit instead jumps back to the thread start routine, which, // like _endthreadex on Windows, skips the guest frames without unwinding them. static thread_local std::jmp_buf* threadExitJump = nullptr; + +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(__builtin_frame_address(0)); + return nullptr; +} + +static std::size_t HostStackReserve() { + static const std::size_t reserve = [] { + std::size_t measured = 0; + pthread_t probe{}; + if (const int result = pthread_create(&probe, nullptr, MeasureStackReserve, &measured); result != 0) + throw std::system_error(result, std::generic_category(), "Measuring the host thread stack reserve"); + if (const int result = pthread_join(probe, nullptr); result != 0) + throw std::system_error(result, std::generic_category(), "Measuring the host thread stack reserve"); + if (measured == 0) + throw std::runtime_error("Cannot measure the host thread stack reserve"); + return measured; + }(); + return reserve; +} #endif void ThreadLifecycle::SetThreadDtors(thread_dtors_func_t callback) { @@ -238,22 +579,75 @@ void ThreadLifecycle::SetThreadAtexitReport(thread_atexit_report_func_t callback } static void finishThread(PthreadPrivate* self, void* retval) { - if (!self || self != currentThread) + if (!self || self != CurrentGuestState().current) throw std::runtime_error("Finishing an unregistered guest thread"); - if (threadFinishing) + if (CurrentGuestState().finishing) throw std::runtime_error("Guest thread is already finishing"); - threadFinishing = true; + CurrentGuestState().finishing = true; + CurrentGuestState().exiting = true; UnregisterStack(self); if (const auto callback = threadDtors.load()) callback(); + ThreadLifecycle::RunKeyDtors(); { std::unique_lock lk(self->_join_mtx); self->_retval = retval; self->_finished.store(true, std::memory_order_release); } - self->_join_cv.notify_all(); + self->_join_cv.NotifyAll(); } +bool ThreadCancel::Requested() { + return CurrentGuestState().current && !CurrentGuestState().exiting && CurrentGuestState().cancelState == GUEST_CANCEL_ENABLE && CurrentGuestState().current->cancelPending.load(); +} + +void ThreadCancel::Check() { + if (Requested()) scePthreadExit(GUEST_CANCELED); +} + +void* ThreadCancel::CleanupMark() { + return CurrentGuestState().cleanupHandlers; +} + +void ThreadCancel::CleanupRestore(void* mark) { + CurrentGuestState().cleanupHandlers = static_cast(mark); +} + +void ThreadCancel::BeginWait(TimedWait::Condition* condition) { + if (!CurrentGuestState().current) return; + std::lock_guard lock(CurrentGuestState().current->cancelLock); + CurrentGuestState().current->cancelWait = condition; +} + +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; @@ -261,12 +655,17 @@ static void RunThread(std::unique_ptr args) { PthreadPrivate* self = args->self; BindHostCpuClock(self); TimedWait::BindThreadWaitState(&self->waitCount); - if (!self->stackAddress) SetStackFromHost(self); - currentThread = self; + CurrentGuestState().current = self; RegisterStack(self); +#ifndef _WIN32 + const sigset_t signalMask = args->signalMask; +#endif args.reset(); - finishThread(self, entry(arg)); - currentThread = nullptr; +#ifndef _WIN32 + pthread_sigmask(SIG_SETMASK, &signalMask, nullptr); +#endif + finishThread(self, Aps5CallGuestEntry_nid_no_patch(entry, arg)); + CurrentGuestState().current = nullptr; } static void ReleaseThread(PthreadPrivate* thread) { @@ -275,12 +674,34 @@ static void ReleaseThread(PthreadPrivate* thread) { if (thread->references.fetch_sub(1, std::memory_order_acq_rel) != 1) return; #ifdef _WIN32 + if (thread->wakeEvent) + CloseHandle(static_cast(thread->wakeEvent)); if (!CloseHandle(thread->nativeHandle)) throw std::system_error(GetLastError(), std::system_category(), "Closing guest thread handle"); #endif 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 @@ -311,6 +732,7 @@ static void ApplyJobAffinity(PthreadPrivate& thread, void* handle) { }); CpuTopology::PinTraced(thread.name.c_str(), handle, mask); } +#endif struct NativeThreadArgs { std::unique_ptr guest; @@ -318,6 +740,7 @@ struct NativeThreadArgs { std::promise initialized; }; +#ifdef _WIN32 static unsigned __stdcall StartNativeThread(void* opaque) { std::unique_ptr args(static_cast(opaque)); auto* self = args->guest->self; @@ -328,14 +751,10 @@ 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(); - currentThread = self; + CurrentGuestState().current = self; if (!self->name.empty()) { const std::wstring description(self->name.begin(), self->name.end()); SetThreadDescription(GetCurrentThread(), description.c_str()); @@ -351,25 +770,69 @@ static unsigned __stdcall StartNativeThread(void* opaque) { auto guest = std::move(args->guest); args.reset(); RunThread(std::move(guest)); - currentThread = nullptr; + CurrentGuestState().current = nullptr; ReleaseThread(self); return 0; } +#else +static void* StartNativeThread(void* opaque) { + std::unique_ptr args(static_cast(opaque)); + auto* self = args->guest->self; + try { + std::uintptr_t low = 0; + std::uintptr_t high = 0; + const auto reserve = HostStackReserve(); + if (!HostStackLimits(&low, &high) || high - low < reserve + self->stackSize) + throw std::runtime_error("Cannot query guest thread stack"); + self->stackAddress = reinterpret_cast(high - reserve - self->stackSize); + self->threadId = std::this_thread::get_id(); +#if defined(__linux__) + self->hostTid = static_cast(gettid()); + RegisterGuestStackMapping(self); +#endif + args->initialized.set_value(); + } catch (...) { + args->initialized.set_exception(std::current_exception()); + return nullptr; + } + if (!args->start.get()) + return nullptr; + auto* guest = args->guest.release(); + args.reset(); + struct ThreadGuard { + PthreadPrivate* self; + ~ThreadGuard() { + CurrentGuestState().current = nullptr; + ReleaseThread(self); + } + } guard{self}; + std::jmp_buf exitJump; + if (setjmp(exitJump) == 0) { + threadExitJump = &exitJump; + RunThread(std::unique_ptr(guest)); + } + threadExitJump = nullptr; + return nullptr; +} #endif extern "C" { int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name) { if (!thread || !entry) throw std::runtime_error("scePthreadCreate: null arg"); - if (attr && !*attr) throw std::runtime_error("scePthreadCreate: null attributes"); - auto p = std::make_unique(); + if (attr && !*attr) attr = nullptr; + auto p = std::make_unique(attr ? *attr : nullptr); bool detached = false; if (attr && *attr) detached = ((*attr)->_detachstate == DETACH_DETACHED); p->_detached = detached; p->stackSize = attr ? (*attr)->_stacksize : DEFAULT_STACK_SIZE; if (attr) { - p->affinity.store((*attr)->_affinity, std::memory_order_relaxed); - p->priority.store((*attr)->_schedpriority, std::memory_order_relaxed); + if ((*attr)->_inheritsched != GUEST_INHERIT_SCHED) + p->scope = (*attr)->_scope; + else if (CurrentGuestState().current) + p->scope = CurrentGuestState().current->scope; + } else if (CurrentGuestState().current) { + p->scope = CurrentGuestState().current->scope; } if (name) p->name = name; std::promise start; @@ -377,9 +840,9 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread #ifdef _WIN32 SYSTEM_INFO system{}; GetSystemInfo(&system); - const std::size_t nativeStack = (p->stackSize + system.dwPageSize - 1) / system.dwPageSize * system.dwPageSize; - if (p->stackSize < 16384 || nativeStack > std::numeric_limits::max()) + if (p->stackSize < 16384 || p->stackSize > std::numeric_limits::max() - HOST_STACK_MARGIN - system.dwPageSize) throw std::runtime_error("scePthreadCreate: invalid Windows stack size"); + const std::size_t nativeStack = (p->stackSize + HOST_STACK_MARGIN + system.dwPageSize - 1) / system.dwPageSize * system.dwPageSize; auto native = std::make_unique(NativeThreadArgs{std::move(args), start.get_future(), {}}); auto initialized = native->initialized.get_future(); const auto handle = _beginthreadex(nullptr, static_cast(nativeStack), StartNativeThread, native.get(), 0, nullptr); @@ -396,39 +859,68 @@ 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 - auto* self = p.get(); - p->_thr = std::thread([self, args = std::move(args), ready = start.get_future()]() mutable { - if (!ready.get()) return; - self->threadId = std::this_thread::get_id(); - struct ThreadGuard { - PthreadPrivate* self; - ~ThreadGuard() { - currentThread = nullptr; - ReleaseThread(self); - } - } guard{self}; - std::jmp_buf exitJump; - if (setjmp(exitJump) == 0) { - threadExitJump = &exitJump; - RunThread(std::move(args)); - } - threadExitJump = nullptr; - }); + const auto reserve = HostStackReserve(); + const auto page = static_cast(sysconf(_SC_PAGESIZE)); + if (p->stackSize < 16384 || p->stackSize > std::numeric_limits::max() - reserve - HOST_STACK_MARGIN - page) + throw std::runtime_error("scePthreadCreate: invalid stack size"); + const std::size_t nativeStack = (p->stackSize + reserve + HOST_STACK_MARGIN + page - 1) / page * page; + sigset_t creatorMask; + pthread_sigmask(SIG_SETMASK, nullptr, &creatorMask); + args->signalMask = creatorMask; +#if defined(__linux__) + ReapRetiredStacks(); + AllocateGuestStack(p.get(), nativeStack); +#endif + auto native = std::make_unique(NativeThreadArgs{std::move(args), start.get_future(), {}}); + auto initialized = native->initialized.get_future(); + pthread_attr_t nativeAttr; + if (const int result = pthread_attr_init(&nativeAttr); result != 0) { + FreeGuestStack(p.get()); + throw std::system_error(result, std::generic_category(), "Creating guest thread"); + } +#if defined(__linux__) + int created = pthread_attr_setstack(&nativeAttr, static_cast(p->guestStack) + GUEST_STACK_GUARD, p->guestStackBytes - GUEST_STACK_GUARD); +#else + int created = pthread_attr_setstacksize(&nativeAttr, nativeStack); +#endif + if (created == 0) { + sigset_t allSignals; + sigfillset(&allSignals); + pthread_sigmask(SIG_BLOCK, &allSignals, nullptr); + created = pthread_create(&p->hostThread, &nativeAttr, StartNativeThread, native.get()); + pthread_sigmask(SIG_SETMASK, &creatorMask, nullptr); + } + pthread_attr_destroy(&nativeAttr); + if (created != 0) { + FreeGuestStack(p.get()); + throw std::system_error(created, std::generic_category(), "Creating guest thread"); + } + native.release(); try { - if (detached) p->_thr.detach(); + initialized.get(); + if (detached) { + if (const int result = pthread_detach(p->hostThread); result != 0) + throw std::system_error(result, std::generic_category(), "Detaching guest thread"); + RetireGuestStack(p.get()); + } } catch (...) { start.set_value(false); - p->_thr.join(); + pthread_join(p->hostThread, nullptr); + FreeGuestStack(p.get()); throw; } auto* published = p.release(); + published->_handleStorage = thread; *thread = published; start.set_value(true); + RecycleJoinedThread(thread); if (detached) ReleaseThread(published); #endif @@ -436,42 +928,69 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread } int APS5_VABI scePthreadJoin(Pthread thread, void** retval) { - if (!thread) throw std::runtime_error("scePthreadJoin: null thread"); + if (!thread) return SCE_KERNEL_ERROR_EINVAL; + if (thread == CurrentGuestState().current) return SCE_KERNEL_ERROR_EDEADLK; if (thread->_detached) return SCE_KERNEL_ERROR_EINVAL; - if (thread == currentThread) - throw std::runtime_error("scePthreadJoin: cannot join current thread"); + if (thread->_joined.load(std::memory_order_acquire)) return SCE_KERNEL_ERROR_ESRCH; + ThreadCancel::Check(); + { + std::unique_lock lock(thread->_join_mtx); + 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 (thread->_thr.joinable()) thread->_thr.join(); + 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) throw std::runtime_error("scePthreadDetach: null thread"); - if (thread->_detached) return SCE_KERNEL_ERROR_EINVAL; - thread->_detached = true; + if (!thread) return SCE_KERNEL_ERROR_EINVAL; + { + 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 - if (thread->_thr.joinable()) thread->_thr.detach(); + if (const int result = pthread_detach(thread->hostThread); result != 0) + throw std::system_error(result, std::generic_category(), "Detaching guest thread"); + RetireGuestStack(thread); ReleaseThread(thread); #endif return SCE_OK; } void APS5_VABI scePthreadExit(void* retval) { - if (!currentThread) + if (!CurrentGuestState().current) throw std::runtime_error("scePthreadExit: current thread is not registered"); - auto* self = currentThread; + auto* self = CurrentGuestState().current; + CurrentGuestState().exiting = true; + while (CurrentGuestState().cleanupHandlers) __pthread_cleanup_pop_imp_nid_postfix(1); finishThread(self, retval); - currentThread = nullptr; + CurrentGuestState().current = nullptr; #ifdef _WIN32 ReleaseThread(self); _endthreadex(0); @@ -485,7 +1004,7 @@ void APS5_VABI scePthreadExit(void* retval) { Pthread APS5_VABI scePthreadSelf() { #ifdef _WIN32 - if (!currentThread) { + if (!CurrentGuestState().current) { auto adopted = std::make_unique(); HANDLE handle = nullptr; if (!DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), &handle, 0, FALSE, DUPLICATE_SAME_ACCESS)) @@ -496,30 +1015,75 @@ Pthread APS5_VABI scePthreadSelf() { adopted->references.store(1, std::memory_order_relaxed); TimedWait::BindThreadWaitState(&adopted->waitCount); SetStackFromHost(adopted.get()); - currentThread = adopted.release(); + CurrentGuestState().current = adopted.release(); } #else - if (!currentThread) { + if (!CurrentGuestState().current) { adoptedThread = std::make_unique(); adoptedThread->_detached = true; adoptedThread->_adopted = true; BindHostCpuClock(adoptedThread.get()); adoptedThread->threadId = std::this_thread::get_id(); + adoptedThread->hostThread = pthread_self(); SetStackFromHost(adoptedThread.get()); - currentThread = adoptedThread.get(); + CurrentGuestState().current = adoptedThread.get(); } #endif - return currentThread; + return CurrentGuestState().current; } 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) { - (void)thread; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!thread) return SCE_KERNEL_ERROR_ESRCH; + thread->cancelPending.store(true); + for (;;) { + { + std::lock_guard lock(thread->cancelLock); + if (!thread->cancelWait) return SCE_OK; + thread->cancelWait->NotifyAll(); + } + scePthreadYield(); + } +} + +void APS5_VABI __pthread_cleanup_push_imp_nid_postfix(void (APS5_VABI* routine)(void*), void* argument, void* info) { + auto* handler = static_cast(info); + handler->routine = routine; + handler->argument = argument; + handler->onHeap = 0; + handler->previous = CurrentGuestState().cleanupHandlers; + CurrentGuestState().cleanupHandlers = handler; +} + +void APS5_VABI __pthread_cleanup_pop_imp_nid_postfix(int execute) { + auto* handler = CurrentGuestState().cleanupHandlers; + if (!handler) return; + CurrentGuestState().cleanupHandlers = handler->previous; + if (execute) handler->routine(handler->argument); } int APS5_VABI scePthreadEqual(Pthread thread1, Pthread thread2) { @@ -552,11 +1116,7 @@ int APS5_VABI scePthreadGetprio(Pthread thread, int* prio) { } int APS5_VABI scePthreadGetthreadid(void) { -#ifdef _WIN32 - return static_cast(GetCurrentThreadId()); -#else - return static_cast(std::hash{}(std::this_thread::get_id()) & 0x7fffffff); -#endif + return static_cast(scePthreadSelf()->tid); } int APS5_VABI scePthreadRename(Pthread thread, const char* name) { @@ -578,20 +1138,23 @@ int APS5_VABI scePthreadSetaffinity(Pthread thread, KernelCpumask mask) { } int APS5_VABI scePthreadSetcancelstate(int state, int* old_state) { - static thread_local int cancelState = 0; - if (old_state) *old_state = cancelState; - cancelState = state; + if (state != GUEST_CANCEL_ENABLE && state != GUEST_CANCEL_DISABLE) return SCE_KERNEL_ERROR_EINVAL; + if (old_state) *old_state = CurrentGuestState().cancelState; + CurrentGuestState().cancelState = state; + if (CurrentGuestState().cancelType == GUEST_CANCEL_ASYNCHRONOUS) ThreadCancel::Check(); return SCE_OK; } int APS5_VABI scePthreadSetcanceltype(int type, int* old_type) { - static thread_local int cancelType = 0; - if (old_type) *old_type = cancelType; - cancelType = type; + if (type != GUEST_CANCEL_DEFERRED && type != GUEST_CANCEL_ASYNCHRONOUS) return SCE_KERNEL_ERROR_EINVAL; + if (old_type) *old_type = CurrentGuestState().cancelType; + CurrentGuestState().cancelType = type; + if (CurrentGuestState().cancelType == GUEST_CANCEL_ASYNCHRONOUS) ThreadCancel::Check(); return SCE_OK; } void APS5_VABI scePthreadTestcancel() { + ThreadCancel::Check(); } int APS5_VABI scePthreadSetprio(Pthread thread, int prio) { @@ -600,20 +1163,36 @@ int APS5_VABI scePthreadSetprio(Pthread thread, int prio) { return SCE_OK; } +static void APS5_VABI ResetOnce(void* once) { + constexpr int32_t Never = 0; + std::atomic_ref state(*static_cast(once)); + state.store(Never, std::memory_order_release); + state.notify_all(); +} + int APS5_VABI scePthreadOnce(int32_t* once, void (APS5_VABI* init)(void)) { if (!once || !init) return SCE_KERNEL_ERROR_EINVAL; constexpr int32_t Never = 0; constexpr int32_t Done = 1; constexpr int32_t Running = 2; std::atomic_ref state(*once); - int32_t expected = Never; - if (state.compare_exchange_strong(expected, Running, std::memory_order_acq_rel)) { - init(); - state.store(Done, std::memory_order_release); - state.notify_all(); - return SCE_OK; + for (int32_t expected = Never; !state.compare_exchange_strong(expected, Running, std::memory_order_acq_rel); expected = Never) { + if (expected != Running) return SCE_OK; + state.wait(Running, std::memory_order_acquire); } - while ((expected = state.load(std::memory_order_acquire)) == Running) state.wait(Running, std::memory_order_acquire); + CleanupHandler reset{}; + void* const mark = ThreadCancel::CleanupMark(); + __pthread_cleanup_push_imp_nid_postfix(ResetOnce, once, &reset); + try { + init(); + } catch (...) { + ThreadCancel::CleanupRestore(mark); + ResetOnce(once); + throw; + } + ThreadCancel::CleanupRestore(mark); + state.store(Done, std::memory_order_release); + state.notify_all(); return SCE_OK; } diff --git a/core/libs/prx/libkernel/Pthread/src/Tsd.cpp b/core/libs/prx/libkernel/Pthread/src/Tsd.cpp index b737b2f161..03c706c202 100644 --- a/core/libs/prx/libkernel/Pthread/src/Tsd.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Tsd.cpp @@ -1,4 +1,6 @@ #include "../include/Pthread.hpp" +#include "../include/ThreadLifecycle.hpp" +#include "../include/WindowsThreadLocal.hpp" #include "prx/libc/include/General.hpp" #include #include @@ -31,8 +33,15 @@ static std::uint64_t g_lastSequence = FREE_KEY; struct ThreadValues { std::array values{}; + bool destroyed = false; ~ThreadValues() { + RunDestructors(); + } + + void RunDestructors() { + if (destroyed) return; + destroyed = true; for (int round = 0; round < DESTRUCTOR_ITERATIONS; ++round) { bool called = false; for (int key = 0; key < MAX_KEYS; ++key) { @@ -51,10 +60,27 @@ struct ThreadValues { } if (!called) return; } + values = {}; } }; -static thread_local ThreadValues g_values; +static int KeySlot(PthreadKey key) { + const unsigned slot = static_cast(key) - 1u; + return slot < static_cast(MAX_KEYS) ? static_cast(slot) : -1; +} + +static ThreadValues& Values() { +#ifdef _WIN32 + return WindowsThreadLocal::Get(); +#else + static thread_local ThreadValues values; + return values; +#endif +} + +void ThreadLifecycle::RunKeyDtors() { + Values().RunDestructors(); +} extern "C" { @@ -65,7 +91,7 @@ int APS5_VABI scePthreadKeyCreate(PthreadKey* key, pthread_key_destructor_func_t if (g_keys[index].sequence.load(std::memory_order_relaxed) == FREE_KEY) { g_keys[index].destructor = reinterpret_cast(destructor); g_keys[index].sequence.store(++g_lastSequence, std::memory_order_release); - *key = index; + *key = index + 1; return SCE_OK; } } @@ -73,25 +99,28 @@ int APS5_VABI scePthreadKeyCreate(PthreadKey* key, pthread_key_destructor_func_t } int APS5_VABI scePthreadKeyDelete(PthreadKey key) { - if (key < 0 || key >= MAX_KEYS) return SCE_KERNEL_ERROR_EINVAL; + const int slot = KeySlot(key); + if (slot < 0) return SCE_KERNEL_ERROR_EINVAL; std::lock_guard lock(g_keyLock); - if (g_keys[key].sequence.load(std::memory_order_relaxed) == FREE_KEY) return SCE_KERNEL_ERROR_EINVAL; - g_keys[key].sequence.store(FREE_KEY, std::memory_order_release); - g_keys[key].destructor = nullptr; + if (g_keys[slot].sequence.load(std::memory_order_relaxed) == FREE_KEY) return SCE_KERNEL_ERROR_EINVAL; + g_keys[slot].sequence.store(FREE_KEY, std::memory_order_release); + g_keys[slot].destructor = nullptr; return SCE_OK; } void* APS5_VABI scePthreadGetspecific(PthreadKey key) { - if (key < 0 || key >= MAX_KEYS) return nullptr; - const ThreadValue& entry = g_values.values[key]; - return entry.sequence == g_keys[key].sequence.load(std::memory_order_acquire) ? entry.value : nullptr; + const int slot = KeySlot(key); + if (slot < 0) return nullptr; + const ThreadValue& entry = Values().values[slot]; + return entry.sequence == g_keys[slot].sequence.load(std::memory_order_acquire) ? entry.value : nullptr; } int APS5_VABI scePthreadSetspecific(PthreadKey key, void* value) { - if (key < 0 || key >= MAX_KEYS) return SCE_KERNEL_ERROR_EINVAL; - const std::uint64_t sequence = g_keys[key].sequence.load(std::memory_order_acquire); + const int slot = KeySlot(key); + if (slot < 0) return SCE_KERNEL_ERROR_EINVAL; + const std::uint64_t sequence = g_keys[slot].sequence.load(std::memory_order_acquire); if (sequence == FREE_KEY) return SCE_KERNEL_ERROR_EINVAL; - g_values.values[key] = {value, sequence}; + Values().values[slot] = {value, sequence}; return SCE_OK; } diff --git a/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp b/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp index 46490e9a7f..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; } @@ -36,7 +44,7 @@ int APS5_VABI sceKernelPollSema(KernelSema sem, int need) { } int APS5_VABI sceKernelSignalSema(KernelSema sem, int count) { - if (sem == nullptr || count <= 0) { + if (sem == nullptr || count < 0) { APS5_INVALID_ARG_EX; } @@ -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,35 +81,49 @@ 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; } - const bool acquired = sem->condition.WaitUntil(lock, TimedWait::DeadlineNanos(*time), released); + const std::uint64_t deadline = TimedWait::DeadlineNanos(*time); + const bool acquired = sem->condition.WaitUntil(lock, deadline, released); + *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/SocketError.hpp b/core/libs/prx/libkernel/Socket/include/SocketError.hpp new file mode 100644 index 0000000000..a7438c651c --- /dev/null +++ b/core/libs/prx/libkernel/Socket/include/SocketError.hpp @@ -0,0 +1,99 @@ +#pragma once +#ifdef _WIN32 +#include +#else +#include +#endif + +namespace GuestSockets { + +constexpr int GuestErrorIo = 5; + +constexpr int GuestErrorFromHost(int hostError) { + switch (hostError) { +#ifdef _WIN32 + case WSAEINTR: return 4; + case WSAEBADF: return 9; + case WSAEACCES: return 13; + case WSAEFAULT: return 14; + case WSAEINVAL: return 22; + case WSAEMFILE: return 24; + case WSAEWOULDBLOCK: return 35; + case WSAEINPROGRESS: return 36; + case WSAEALREADY: return 37; + case WSAENOTSOCK: return 38; + case WSAEDESTADDRREQ: return 39; + case WSAEMSGSIZE: return 40; + case WSAEPROTOTYPE: return 41; + case WSAENOPROTOOPT: return 42; + case WSAEPROTONOSUPPORT: return 43; + case WSAESOCKTNOSUPPORT: return 44; + case WSAEOPNOTSUPP: return 45; + case WSAEPFNOSUPPORT: return 46; + case WSAEAFNOSUPPORT: return 47; + case WSAEADDRINUSE: return 48; + case WSAEADDRNOTAVAIL: return 49; + case WSAENETDOWN: return 50; + case WSAENETUNREACH: return 51; + case WSAENETRESET: return 52; + case WSAECONNABORTED: return 53; + case WSAECONNRESET: return 54; + case WSAENOBUFS: return 55; + case WSAEISCONN: return 56; + case WSAENOTCONN: return 57; + case WSAESHUTDOWN: return 32; + case WSAETOOMANYREFS: return 59; + case WSAETIMEDOUT: return 60; + case WSAECONNREFUSED: return 61; + case WSAEHOSTDOWN: return 64; + case WSAEHOSTUNREACH: return 65; +#else + case EPERM: return 1; + case EINTR: return 4; + case EBADF: return 9; + case ENOMEM: return 12; + case EACCES: return 13; + case EFAULT: return 14; + case EINVAL: return 22; + case ENFILE: return 23; + case EMFILE: return 24; + case EPIPE: return 32; + case EAGAIN: return 35; +#if EWOULDBLOCK != EAGAIN + case EWOULDBLOCK: return 35; +#endif + case EINPROGRESS: return 36; + case EALREADY: return 37; + case ENOTSOCK: return 38; + case EDESTADDRREQ: return 39; + case EMSGSIZE: return 40; + case EPROTOTYPE: return 41; + case ENOPROTOOPT: return 42; + case EPROTONOSUPPORT: return 43; + case ESOCKTNOSUPPORT: return 44; + case EOPNOTSUPP: return 45; + case EPFNOSUPPORT: return 46; + case EAFNOSUPPORT: return 47; + case EADDRINUSE: return 48; + case EADDRNOTAVAIL: return 49; + case ENETDOWN: return 50; + case ENETUNREACH: return 51; + case ENETRESET: return 52; + case ECONNABORTED: return 53; + case ECONNRESET: return 54; + case ENOBUFS: return 55; + case EISCONN: return 56; + case ENOTCONN: return 57; + case ESHUTDOWN: return 58; + case ETOOMANYREFS: return 59; + case ETIMEDOUT: return 60; + case ECONNREFUSED: return 61; + case EHOSTDOWN: return 64; + case EHOSTUNREACH: return 65; + case EPROTO: return 92; +#endif + default: return GuestErrorIo; + } +} + +} diff --git a/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp b/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp new file mode 100644 index 0000000000..56bbe00efe --- /dev/null +++ b/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp @@ -0,0 +1,22 @@ +#pragma once +#include + +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 71d5f62e78..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; @@ -16,6 +17,25 @@ 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); +extern "C" void KernelSetSocketCloser_nid_no_patch(KernelSocketPoll::Closer closer); +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 a750d55f13..d59cfc4304 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp @@ -1,9 +1,43 @@ #pragma once +#include +#include +#include +#include "prx/libkernel/Socket/include/SocketPoll.hpp" + +struct FileStat; namespace GuestSockets { -constexpr int FirstDescriptor = 0x10000000; +struct Iovec { + void* base; + std::size_t length; +}; +int Duplicate(int descriptor); +int DuplicateTo(int descriptor, int target); int Close(int descriptor); +void Forget(int descriptor); bool IsOpen(int descriptor); int Family(int descriptor); +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); +bool Ready(int descriptor, bool write, std::int64_t* data, bool* eof); +int Poll(KernelSocketPoll::Entry* entries, int count, int timeoutMilliseconds); +#ifdef _WIN32 +bool PipeReady(int descriptor, short events, short& revents); +#endif +struct Interest { + int descriptor; + bool write; +}; +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); } extern "C" bool GuestSocketIsOpen_nid_no_patch(int descriptor); diff --git a/core/libs/prx/libkernel/Socket/include/SocketSelectDeadline.hpp b/core/libs/prx/libkernel/Socket/include/SocketSelectDeadline.hpp new file mode 100644 index 0000000000..c643de4945 --- /dev/null +++ b/core/libs/prx/libkernel/Socket/include/SocketSelectDeadline.hpp @@ -0,0 +1,18 @@ +#pragma once +#include +#include + +namespace KernelSocketSelect { + +inline std::chrono::steady_clock::time_point DeadlineAfter(std::chrono::steady_clock::time_point start, + std::int64_t seconds, std::int64_t microseconds) { + using Clock = std::chrono::steady_clock; + constexpr std::int64_t MicrosecondsPerSecond = 1000000; + const auto remaining = std::chrono::duration_cast(Clock::time_point::max() - start).count(); + if (seconds > remaining / MicrosecondsPerSecond) return Clock::time_point::max(); + const auto requested = seconds * MicrosecondsPerSecond; + if (microseconds > remaining - requested) return Clock::time_point::max(); + return start + std::chrono::duration_cast(std::chrono::microseconds(requested + microseconds)); +} + +} diff --git a/core/libs/prx/libkernel/Socket/src/AddressConversion.cpp b/core/libs/prx/libkernel/Socket/src/AddressConversion.cpp index b10fb96339..bbcb17e637 100644 --- a/core/libs/prx/libkernel/Socket/src/AddressConversion.cpp +++ b/core/libs/prx/libkernel/Socket/src/AddressConversion.cpp @@ -1,47 +1,238 @@ -#ifdef _WIN32 -#define WIN32_LEAN_AND_MEAN -#include -#include -#else -#include -#endif #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include +#include extern "C" int* APS5_VABI __error_nid_postfix(); +namespace { +constexpr int kGuestInet = 2; +constexpr int kGuestInet6 = 28; +constexpr std::size_t kMaxText = 46; + +bool ParseDecimalPart(const char* begin, const char* end, std::uint8_t& value) { + const std::ptrdiff_t length = end - begin; + if (length < 1 || length > 3) return false; + if (length > 1 && *begin == '0') return false; + unsigned total = 0; + for (const char* cursor = begin; cursor != end; ++cursor) { + if (*cursor < '0' || *cursor > '9') return false; + total = total * 10 + static_cast(*cursor - '0'); + } + if (total > 255) return false; + value = static_cast(total); + return true; +} + +bool ParseQuad(const char* begin, const char* end, std::uint8_t (&bytes)[4]) { + const char* partBegin = begin; + int parts = 0; + for (const char* cursor = begin;; ++cursor) { + if (cursor != end && *cursor != '.') continue; + if (parts == 4 || !ParseDecimalPart(partBegin, cursor, bytes[parts])) return false; + ++parts; + if (cursor == end) break; + partBegin = cursor + 1; + } + return parts == 4; +} + +int HexValue(char symbol) { + if (symbol >= '0' && symbol <= '9') return symbol - '0'; + if (symbol >= 'a' && symbol <= 'f') return symbol - 'a' + 10; + if (symbol >= 'A' && symbol <= 'F') return symbol - 'A' + 10; + return -1; +} + +bool ParseHexGroup(const char* begin, const char* end, std::uint16_t& value) { + const std::ptrdiff_t length = end - begin; + if (length < 1 || length > 4) return false; + unsigned total = 0; + for (const char* cursor = begin; cursor != end; ++cursor) { + const int digit = HexValue(*cursor); + if (digit < 0) return false; + total = total * 16 + static_cast(digit); + } + value = static_cast(total); + return true; +} + +bool ParseGroupList(const char* begin, const char* end, bool mayEndWithQuad, + std::uint16_t (&groups)[8], int& count) { + count = 0; + if (begin == end) return true; + const char* groupBegin = begin; + for (const char* cursor = begin;; ++cursor) { + if (cursor != end && *cursor != ':') continue; + const bool last = cursor == end; + bool hasDot = false; + for (const char* probe = groupBegin; probe != cursor; ++probe) hasDot = hasDot || *probe == '.'; + if (hasDot) { + std::uint8_t quad[4]; + if (!last || !mayEndWithQuad || count > 6 || !ParseQuad(groupBegin, cursor, quad)) return false; + groups[count++] = static_cast(quad[0] << 8 | quad[1]); + groups[count++] = static_cast(quad[2] << 8 | quad[3]); + return true; + } + if (count == 8 || !ParseHexGroup(groupBegin, cursor, groups[count])) return false; + ++count; + if (last) return true; + groupBegin = cursor + 1; + } +} + +bool TextToV6(const char* text, std::uint8_t (&bytes)[16]) { + const char* const end = text + std::strlen(text); + const char* gap = std::strstr(text, "::"); + std::uint16_t head[8]; + std::uint16_t tail[8]; + int headCount = 0; + int tailCount = 0; + if (!gap) { + if (!ParseGroupList(text, end, true, head, headCount) || headCount != 8) return false; + } else { + if (std::strstr(gap + 1, "::")) return false; + if (!ParseGroupList(text, gap, false, head, headCount)) return false; + if (!ParseGroupList(gap + 2, end, true, tail, tailCount)) return false; + if (headCount + tailCount > 7) return false; + } + std::uint16_t words[8]{}; + for (int index = 0; index < headCount; ++index) words[index] = head[index]; + for (int index = 0; index < tailCount; ++index) words[8 - tailCount + index] = tail[index]; + for (int index = 0; index < 8; ++index) { + bytes[index * 2] = static_cast(words[index] >> 8); + bytes[index * 2 + 1] = static_cast(words[index]); + } + return true; +} + +std::size_t AppendDecimal(char* out, unsigned value) { + char digits[3]; + std::size_t count = 0; + do { + digits[count++] = static_cast('0' + value % 10); + value /= 10; + } while (value); + for (std::size_t index = 0; index < count; ++index) out[index] = digits[count - 1 - index]; + return count; +} + +std::size_t FormatQuad(const std::uint8_t* bytes, char* out) { + std::size_t length = 0; + for (int index = 0; index < 4; ++index) { + if (index) out[length++] = '.'; + length += AppendDecimal(out + length, bytes[index]); + } + return length; +} + +std::size_t AppendHex(char* out, unsigned value) { + static const char symbols[] = "0123456789abcdef"; + char digits[4]; + std::size_t count = 0; + do { + digits[count++] = symbols[value & 15]; + value >>= 4; + } while (value); + for (std::size_t index = 0; index < count; ++index) out[index] = digits[count - 1 - index]; + return count; +} + +void FindLongestZeroRun(const std::uint16_t (&words)[8], int& start, int& length) { + start = -1; + length = 0; + for (int from = 0; from < 8; ++from) { + if (words[from] != 0) continue; + int run = 0; + while (from + run < 8 && words[from + run] == 0) ++run; + if (run > length) { + start = from; + length = run; + } + from += run; + } + if (length < 2) { + start = -1; + length = 0; + } +} + +std::size_t V6ToText(const std::uint8_t* bytes, char* out) { + std::uint16_t words[8]; + for (int index = 0; index < 8; ++index) + words[index] = static_cast(bytes[index * 2] << 8 | bytes[index * 2 + 1]); + int runStart; + int runLength; + FindLongestZeroRun(words, runStart, runLength); + std::size_t length = 0; + if (runStart == 0 && (runLength == 6 || (runLength == 5 && words[5] == 0xffff))) { + const char* prefix = runLength == 6 ? "::" : "::ffff:"; + length = std::strlen(prefix); + std::memcpy(out, prefix, length); + return length + FormatQuad(bytes + 12, out + length); + } + for (int index = 0; index < 8;) { + if (index == runStart) { + out[length++] = ':'; + if (index + runLength == 8) out[length++] = ':'; + index += runLength; + continue; + } + if (index) out[length++] = ':'; + length += AppendHex(out + length, words[index]); + ++index; + } + return length; +} +} + extern "C" { int APS5_VABI __inet_pton_nid_postfix(int family, const char* text, void* destination) { - if (family != 2 && family != 28) { *__error_nid_postfix() = 47; return -1; } - if (!text || !destination) { *__error_nid_postfix() = 14; return -1; } - unsigned char address[16]{}; - const int nativeFamily = family == 2 ? AF_INET : AF_INET6; -#ifdef _WIN32 - const int result = InetPtonA(nativeFamily, text, address); -#else - const int result = ::inet_pton(nativeFamily, text, address); -#endif - if (result == 1) std::memcpy(destination, address, family == 2 ? 4 : 16); - else if (result == -1) *__error_nid_postfix() = 22; - return result; + if (family != kGuestInet && family != kGuestInet6) { *__error_nid_postfix() = 47; 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; } const char* APS5_VABI __inet_ntop_nid_postfix(int family, const void* source, char* destination, std::uint32_t capacity) { - if (family != 2 && family != 28) { *__error_nid_postfix() = 47; return nullptr; } - if (!source || !destination) { *__error_nid_postfix() = 14; return nullptr; } - char text[INET6_ADDRSTRLEN]{}; - const int nativeFamily = family == 2 ? AF_INET : AF_INET6; -#ifdef _WIN32 - const auto* result = InetNtopA(nativeFamily, const_cast(source), text, sizeof(text)); -#else - const auto* result = ::inet_ntop(nativeFamily, source, text, sizeof(text)); -#endif - if (!result) { *__error_nid_postfix() = 22; return nullptr; } - const auto size = std::strlen(text) + 1; - if (size > capacity) { *__error_nid_postfix() = 28; return nullptr; } - std::memcpy(destination, text, size); + if (family != kGuestInet && family != kGuestInet6) { *__error_nid_postfix() = 47; 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 d2f176af1e..6cc428f617 100644 --- a/core/libs/prx/libkernel/Socket/src/Socket.cpp +++ b/core/libs/prx/libkernel/Socket/src/Socket.cpp @@ -2,18 +2,33 @@ #include #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" +#include "prx/libkernel/Socket/include/SocketSelectDeadline.hpp" +#include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include #include +#include #include -#include +#include +#include #include +#ifndef _WIN32 +#include +#endif + namespace { +constexpr int GuestEbadf = 9; constexpr int GuestEinval = 22; constexpr int SelectDescriptorLimit = 1024; +constexpr int SelectPollSliceMilliseconds = 10; struct GuestTimeval { std::int64_t seconds; @@ -21,6 +36,10 @@ 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; @@ -30,6 +49,84 @@ void ClearDescriptor(void* set, int descriptor) { if (set != nullptr) static_cast(set)[descriptor / 64] &= ~(std::uint64_t{1} << (descriptor % 64)); } +struct PollGroup { + KernelSocketPoll::Poller poll; + std::vector entries; + std::vector slots; +}; + +int KernelSockets(KernelSocketPoll::Entry* entries, int count, int timeoutMilliseconds) { + return GuestSockets::Poll(entries, count, timeoutMilliseconds); +} + +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) { + short flags = 0; + if ((events & KernelSocketPoll::Readable) != 0) flags |= POLLIN; + if ((events & KernelSocketPoll::Writable) != 0) flags |= POLLOUT; + return flags; +} + +short KernelEventFlags(short revents) { + short flags = 0; + if ((revents & POLLIN) != 0) flags |= KernelSocketPoll::Readable; + if ((revents & POLLOUT) != 0) flags |= KernelSocketPoll::Writable; + if ((revents & POLLERR) != 0) flags |= KernelSocketPoll::Error; + if ((revents & POLLHUP) != 0) flags |= KernelSocketPoll::HangUp; + return flags; +} + +int HostPoll(KernelSocketPoll::Entry* descriptors, int count, int timeoutMilliseconds) { + std::vector native(static_cast(count)); + for (std::size_t index = 0; index < native.size(); ++index) { + native[index].fd = descriptors[index].descriptor; + native[index].events = HostEventFlags(descriptors[index].events); + native[index].revents = 0; + } + const int result = ::poll(native.data(), static_cast(native.size()), timeoutMilliseconds); + if (result < 0) return -errno; + for (std::size_t index = 0; index < native.size(); ++index) { + if ((native[index].revents & POLLNVAL) != 0) return -EBADF; + descriptors[index].revents = KernelEventFlags(native[index].revents); + } + return result; +} + +#else + +int HostPoll(KernelSocketPoll::Entry* descriptors, int count, int timeoutMilliseconds) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMilliseconds); + for (;;) { + int ready = 0; + for (int index = 0; index < count; ++index) { + auto& entry = descriptors[index]; + if (!DescriptorIsOpen_nid_no_patch(entry.descriptor)) return -GuestEbadf; + if (!GuestSockets::PipeReady(entry.descriptor, entry.events, entry.revents)) entry.revents = KernelSocketPoll::Unknown; + 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))); + } +} + +#endif + } extern "C" { @@ -51,59 +148,176 @@ void KernelSetSocketPoller_nid_no_patch(KernelSocketPoll::Poller poller) { g_socketPoller.store(poller); } +void KernelSetSocketCloser_nid_no_patch(KernelSocketPoll::Closer closer) { + g_socketCloser.store(closer); +} + +bool KernelCloseSocket_nid_no_patch(int descriptor, int* error) { + const auto closer = g_socketCloser.load(); + return closer != nullptr && closer(descriptor, error); +} + +void KernelSetSocketOptionSetter_nid_no_patch(KernelSocketPoll::OptionSetter setter) { + g_socketOptionSetter.store(setter); +} + +bool KernelSetSocketOption_nid_no_patch(int descriptor, int level, int option, const void* value, std::uint32_t length, int* error) { + const auto setter = g_socketOptionSetter.load(); + 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))) { *__error_nid_postfix() = GuestEinval; return -1; } - std::vector entries; + PollGroup kernel{&KernelSockets, {}, {}}; + PollGroup kqueues{&Kqueues, {}, {}}; + PollGroup net{nullptr, {}, {}}; + std::vector devices; + std::vector requested; for (int descriptor = 0; descriptor < nfds; ++descriptor) { short events = 0; if (DescriptorSet(readfds, descriptor)) events |= KernelSocketPoll::Readable; if (DescriptorSet(writefds, descriptor)) events |= KernelSocketPoll::Writable; if (DescriptorSet(exceptfds, descriptor)) events |= KernelSocketPoll::Urgent; - if (events != 0) entries.push_back({descriptor, events, 0}); + if (events == 0) continue; + if (GuestSockets::IsOpen(descriptor)) { + kernel.entries.push_back({descriptor, events, 0}); + kernel.slots.push_back(requested.size()); + requested.push_back({descriptor, events, 0}); + } else if (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()); + requested.push_back({descriptor, events, 0}); + } + } + for (const auto& device : devices) requested.push_back(device); + const bool devicesReady = std::any_of(devices.begin(), devices.end(), [](const auto& entry) { return entry.revents != 0; }); + 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(); + if (net.poll != nullptr) { + for (auto& socket : net.entries) socket.revents = 0; + const int probe = net.poll(net.entries.data(), static_cast(net.entries.size()), 0); + if (probe < 0) { + *__error_nid_postfix() = -probe; + return -1; + } + } + for (std::size_t index = 0; index < net.entries.size();) { + if (net.poll != nullptr && (net.entries[index].revents & KernelSocketPoll::Unknown) == 0) { + ++index; + continue; + } + const int descriptor = net.entries[index].descriptor; + hosts.entries.push_back({descriptor, net.entries[index].events, 0}); + hosts.slots.push_back(net.slots[index]); + net.entries.erase(net.entries.begin() + static_cast(index)); + net.slots.erase(net.slots.begin() + static_cast(index)); + } + if (!net.entries.empty()) groups.push_back(std::move(net)); + if (!hosts.entries.empty()) { + 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() - : start + std::chrono::seconds(limit->seconds) + std::chrono::microseconds(limit->microseconds); + : KernelSocketSelect::DeadlineAfter(start, limit->seconds, limit->microseconds); for (;;) { int waitMilliseconds = 100; if (limit != nullptr) { const auto remaining = std::chrono::duration_cast(deadline - std::chrono::steady_clock::now()).count(); - waitMilliseconds = static_cast(std::clamp((remaining + 999) / 1000, 0, 100)); + const auto roundedMilliseconds = remaining > 0 ? remaining / 1000 + (remaining % 1000 != 0) : 0; + waitMilliseconds = static_cast(std::clamp(roundedMilliseconds, 0, 100)); } - if (!entries.empty()) { - const auto poller = g_socketPoller.load(); - if (poller == nullptr) NotImplemented_nid_no_patch("select on descriptors that are not libSceNet sockets"); - for (auto& entry : entries) entry.revents = 0; - const int result = poller(entries.data(), static_cast(entries.size()), waitMilliseconds); - if (result < 0) { - *__error_nid_postfix() = -result; + if (devicesReady) waitMilliseconds = 0; + if (groups.empty()) { + if (waitMilliseconds > 0) PreciseSleepUntil(std::min(deadline, std::chrono::steady_clock::now() + std::chrono::milliseconds(waitMilliseconds))); + } else { + const int slice = groups.size() == 1 ? waitMilliseconds : std::min(waitMilliseconds, SelectPollSliceMilliseconds); + const auto sliceEnd = std::chrono::steady_clock::now() + std::chrono::milliseconds(waitMilliseconds); + for (;;) { + int groupReady = 0; + for (auto& group : groups) { + for (auto& entry : group.entries) entry.revents = 0; + const int result = group.poll(group.entries.data(), static_cast(group.entries.size()), slice); + if (result < 0) { + *__error_nid_postfix() = -result; + return -1; + } + for (std::size_t index = 0; index < group.entries.size(); ++index) requested[group.slots[index]].revents = group.entries[index].revents; + groupReady += result; + } + 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; } - int ready = 0; - for (const auto& entry : entries) { - if ((entry.revents & KernelSocketPoll::Unknown) != 0) NotImplemented_nid_no_patch("select on descriptors that are not libSceNet sockets"); - 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; - ready += static_cast(readable) + static_cast(writable) + static_cast(urgent); + 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) { + 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; + ready += static_cast(readable) + static_cast(writable) + static_cast(urgent); + } + if (ready > 0) { + for (const auto& entry : requested) { + if ((entry.revents & (KernelSocketPoll::Readable | KernelSocketPoll::HangUp | KernelSocketPoll::Error)) == 0) ClearDescriptor(readfds, entry.descriptor); + if ((entry.revents & (KernelSocketPoll::Writable | KernelSocketPoll::Error)) == 0) ClearDescriptor(writefds, entry.descriptor); + if ((entry.revents & KernelSocketPoll::Urgent) == 0) ClearDescriptor(exceptfds, entry.descriptor); } - if (ready > 0) { - for (const auto& entry : entries) { - if ((entry.revents & (KernelSocketPoll::Readable | KernelSocketPoll::HangUp | KernelSocketPoll::Error)) == 0) ClearDescriptor(readfds, entry.descriptor); - if ((entry.revents & (KernelSocketPoll::Writable | KernelSocketPoll::Error)) == 0) ClearDescriptor(writefds, entry.descriptor); - if ((entry.revents & KernelSocketPoll::Urgent) == 0) ClearDescriptor(exceptfds, entry.descriptor); - } - return ready; - } - } else if (waitMilliseconds > 0) { - std::this_thread::sleep_for(std::chrono::milliseconds(waitMilliseconds)); + return ready; } if (limit != nullptr && std::chrono::steady_clock::now() >= deadline) { - for (const auto& entry : entries) { + for (const auto& entry : requested) { ClearDescriptor(readfds, entry.descriptor); ClearDescriptor(writefds, entry.descriptor); ClearDescriptor(exceptfds, entry.descriptor); 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 new file mode 100644 index 0000000000..37fce2af2e --- /dev/null +++ b/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp @@ -0,0 +1,272 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#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" +#include +#include + +namespace GuestSockets { +namespace { + +constexpr int GuestAcceptConnection = 0x2; +constexpr int GuestReuseAddress = 0x4; +constexpr int GuestKeepAlive = 0x8; +constexpr int GuestDontRoute = 0x10; +constexpr int GuestBroadcast = 0x20; +constexpr int GuestLinger = 0x80; +constexpr int GuestOobInline = 0x100; +constexpr int 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*; +using NativeOptionOutput = char*; +int HostError() { return GuestErrorFromHost(WSAGetLastError()); } +#else +using NativeHandle = int; +using NativeOptionValue = const void*; +using NativeOptionOutput = void*; +int HostError() { return GuestErrorFromHost(errno); } +#endif + +int NativeBoolean(int option) { + switch (option) { + case GuestReuseAddress: return SO_REUSEADDR; + case GuestKeepAlive: return SO_KEEPALIVE; + case GuestDontRoute: return SO_DONTROUTE; + case GuestBroadcast: return SO_BROADCAST; + case GuestOobInline: return SO_OOBINLINE; + default: return -1; + } +} + +int Copy(void* output, std::uint32_t* length, const void* value, std::uint32_t size) { + const std::uint32_t count = std::min(*length, size); + if (count != 0 && !output) return ErrorInvalid; + if (count != 0) std::memcpy(output, value, count); + *length = count; + return 0; +} + +} + +int SetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int option, const void* value, std::uint32_t length) { + const auto handle = static_cast(native); + if (option == GuestLinger) { + if (length < sizeof(GuestLingerValue)) return ErrorInvalid; + if (!value) return ErrorInvalid; + GuestLingerValue guest; + std::memcpy(&guest, value, sizeof(guest)); + if (guest.seconds < 0 || guest.seconds > MaximumLinger) return ErrorDomain; +#ifdef _WIN32 + linger request{}; + request.l_onoff = guest.enabled != 0 ? 1 : 0; + request.l_linger = static_cast(guest.seconds); +#else + linger request{}; + request.l_onoff = guest.enabled != 0 ? 1 : 0; + request.l_linger = guest.seconds; +#endif + if (::setsockopt(handle, SOL_SOCKET, SO_LINGER, reinterpret_cast(&request), sizeof(request))) return HostError(); + return 0; + } + 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; + if (!value) return ErrorInvalid; + int number; + std::memcpy(&number, value, sizeof(number)); + if (option == GuestSendBuffer || option == GuestReceiveBuffer) { + if (number < 1) return ErrorInvalid; + const int nativeOption = option == GuestSendBuffer ? SO_SNDBUF : SO_RCVBUF; + if (::setsockopt(handle, SOL_SOCKET, nativeOption, reinterpret_cast(&number), sizeof(number))) return HostError(); + return 0; + } + const int enabled = number != 0 ? 1 : 0; + if (::setsockopt(handle, SOL_SOCKET, NativeBoolean(option), reinterpret_cast(&enabled), sizeof(enabled))) return HostError(); + return 0; + } + return ErrorNoProtocolOption; +} + +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) { + linger current{}; + socklen_t size = sizeof(current); + if (::getsockopt(handle, SOL_SOCKET, SO_LINGER, reinterpret_cast(¤t), &size)) return HostError(); + const GuestLingerValue guest{current.l_onoff != 0 ? GuestLinger : 0, static_cast(current.l_linger)}; + return Copy(value, length, &guest, sizeof(guest)); + } + 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; + socklen_t size = sizeof(enabled); + if (::getsockopt(handle, SOL_SOCKET, nativeOption, reinterpret_cast(&enabled), &size)) return HostError(); + number = enabled != 0 ? option : 0; + } else if (option == GuestAcceptConnection) { + 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); + if (::getsockopt(handle, SOL_SOCKET, SO_ERROR, reinterpret_cast(&pending), &size)) return HostError(); +#ifdef _WIN32 + if (pending != 0 && pending == state.reportedError) pending = 0; + else if (pending != 0) state.reportedError = pending; +#endif + number = pending == 0 ? 0 : GuestErrorFromHost(pending); + } else if (option == GuestSendBuffer || option == GuestReceiveBuffer) { + socklen_t size = sizeof(number); + if (::getsockopt(handle, SOL_SOCKET, option == GuestSendBuffer ? SO_SNDBUF : SO_RCVBUF, reinterpret_cast(&number), &size)) return HostError(); + } else { + return ErrorNoProtocolOption; + } + return Copy(value, length, &number, sizeof(number)); +} + +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 1e52e3dafa..f9a28df4fd 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -2,17 +2,45 @@ #define WIN32_LEAN_AND_MEAN #include #include +#include +#include #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" +#include "prx/libc/include/HostThreadLocal.hpp" +#include "prx/libkernel/Socket/include/SocketError.hpp" +#include "prx/libkernel/Socket/include/SocketOptions.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" +#include "prx/libkernel/File/include/DescriptorControl.hpp" +#include "prx/libkernel/Equeue/Equeue.hpp" +#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 @@ -20,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; @@ -39,65 +68,56 @@ 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 GuestDontWait = 0x80; constexpr int GuestNoSignal = 0x20000; +constexpr int GuestSendFlags = GuestNoSignal | GuestDontWait; +constexpr int GuestReceiveFlags = GuestPeek | 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) { +#ifdef _WIN32 + return flags & GuestPeek ? MSG_PEEK : 0; +#else + return (flags & GuestPeek ? MSG_PEEK : 0) | (flags & GuestDontWait ? MSG_DONTWAIT : 0); #endif } int NativeError() { #ifdef _WIN32 - switch (WSAGetLastError()) { - case WSAEWOULDBLOCK: return 35; - case WSAEADDRINUSE: return 48; - case WSAEADDRNOTAVAIL: return 49; - case WSAEACCES: return 13; - case WSAEMSGSIZE: return 40; - case WSAENETUNREACH: return 51; - case WSAEHOSTUNREACH: return 65; - case WSAECONNRESET: return 54; - case WSAECONNABORTED: return 53; - case WSAEISCONN: return 56; - case WSAENOTCONN: return 57; - case WSAENOBUFS: return 55; - case WSAETIMEDOUT: return 60; - case WSAECONNREFUSED: return 61; - case WSAEINTR: return 4; - case WSAEINVAL: return 22; - default: return 5; - } + return GuestSockets::GuestErrorFromHost(WSAGetLastError()); #else - switch (errno) { - case EAGAIN: return 35; - case EADDRINUSE: return 48; - case EADDRNOTAVAIL: return 49; - case EACCES: return 13; - case EMSGSIZE: return 40; - case ENETUNREACH: return 51; - case EHOSTUNREACH: return 65; - case ECONNREFUSED: return 61; - case ECONNRESET: return 54; - case ECONNABORTED: return 53; - case EISCONN: return 56; - case ENOTCONN: return 57; - case ENOBUFS: return 55; - case ETIMEDOUT: return 60; - case EINTR: return 4; - case EINVAL: return 22; - case EPIPE: return 32; - default: return 5; - } + return GuestSockets::GuestErrorFromHost(errno); #endif } -struct Socket { +int ConnectError() { +#ifdef _WIN32 + if (WSAGetLastError() == WSAEWOULDBLOCK) return 36; +#endif + return NativeError(); +} +int SendError(int type) { +#ifdef _WIN32 + if (type == 2 && WSAGetLastError() == WSAENOTCONN) return 39; +#else + static_cast(type); +#endif + return NativeError(); +} +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; Socket(NativeSocket value, int family, int type) : value(value), family(family), type(type) {} ~Socket() { if (value == Invalid) return; @@ -108,28 +128,147 @@ struct Socket { #endif } }; +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; + address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + socklen_t size = sizeof(address); +#ifdef _WIN32 + u_long nonblocking = 1; + const bool configured = value != Invalid && ioctlsocket(value, FIONBIO, &nonblocking) == 0; +#else + const bool configured = value != Invalid && ::fcntl(value, F_SETFL, ::fcntl(value, F_GETFL) | O_NONBLOCK) == 0; +#endif + if (!configured || bind(value, reinterpret_cast(&address), size) != 0 || + getsockname(value, reinterpret_cast(&address), &size) != 0 || + connect(value, reinterpret_cast(&address), size) != 0) { + const int error = NativeError(); + Close(); + throw std::runtime_error("equeue waker: loopback socket setup failed, guest errno " + std::to_string(error)); + } + } + ~Waker() { Close(); } + void Close() { + if (value == Invalid) return; +#ifdef _WIN32 + closesocket(value); +#else + ::close(value); +#endif + value = Invalid; + } +}; +Waker& ThreadWaker() { + struct WakerTag {}; + return HostThreadLocal(); +} std::mutex socketsMutex; std::map> sockets; -int nextDescriptor = GuestSockets::FirstDescriptor; +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); + if (placeholder == INVALID_HANDLE_VALUE) + throw std::runtime_error("socket: opening the NUL descriptor placeholder failed, error " + std::to_string(::GetLastError())); + const int number = ::_open_osfhandle(reinterpret_cast(placeholder), _O_RDONLY); + if (number < 0) { + ::CloseHandle(placeholder); + return Fail(24); + } + if (number >= GuestSocketDescriptorLimit) { + ::_close(number); + return Fail(24); + } + return number; +} +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) { + ::_close(number); +} +#else +int ReserveNumber(NativeSocket native) { + const int number = ::fcntl(native, F_DUPFD_CLOEXEC, 0); + if (number < 0) return Fail(NativeError()); + if (number >= GuestSocketDescriptorLimit) { + ::close(number); + return Fail(24); + } + return number; +} +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) { + ::close(number); +} +#endif +int Install(std::shared_ptr socket) { + const int number = ReserveNumber(socket->value); + if (number < 0) return -1; + try { + std::lock_guard lock(socketsMutex); + sockets.insert_or_assign(number, std::move(socket)); + } catch (const std::bad_alloc&) { + ReleaseNumber(number); + return Fail(12); + } + return number; +} std::shared_ptr Lookup(int descriptor) { std::lock_guard lock(socketsMutex); const auto found = sockets.find(descriptor); if (found != sockets.end()) return found->second; - Fail(9); + Fail(descriptor >= 0 && DescriptorIsOpen_nid_no_patch(descriptor) ? 38 : 9); return {}; } -int Option(int guest) { - switch (guest) { - case 0x4: return SO_REUSEADDR; - case 0x20: return SO_BROADCAST; - case 0x1001: return SO_SNDBUF; - case 0x1002: return SO_RCVBUF; - default: return -1; - } -} bool Address(const void* input, std::uint32_t length, sockaddr_storage& native, socklen_t& size) { - if (!input || length < 2) { Fail(14); return false; } + if (length > 255) { Fail(63); return false; } + if (length < 2) { Fail(22); return false; } + if (!input) { Fail(14); 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; @@ -151,6 +290,49 @@ bool Address(const void* input, std::uint32_t length, sockaddr_storage& native, Fail(47); return false; } +std::uint16_t& PortOf(sockaddr_storage& address) { + return address.ss_family == AF_INET ? reinterpret_cast(address).sin_port : reinterpret_cast(address).sin6_port; +} +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; +} +int Disconnect(const Socket& socket) { + sockaddr_storage peer{}; + socklen_t peerSize = sizeof(peer); + if (::getpeername(socket.value, reinterpret_cast(&peer), &peerSize) != 0) return 0; + sockaddr_storage local{}; + socklen_t localSize = sizeof(local); + if (::getsockname(socket.value, reinterpret_cast(&local), &localSize) != 0) return Fail(NativeError()); +#ifdef _WIN32 + sockaddr_storage unspecified{}; + unspecified.ss_family = local.ss_family; + if (::connect(socket.value, reinterpret_cast(&unspecified), localSize) != 0) return Fail(NativeError()); +#else + sockaddr unspecified{}; + unspecified.sa_family = AF_UNSPEC; + if (::connect(socket.value, &unspecified, sizeof(unspecified)) != 0) return Fail(NativeError()); +#endif + sockaddr_storage current{}; + socklen_t currentSize = sizeof(current); + if (::getsockname(socket.value, reinterpret_cast(¤t), ¤tSize) != 0) return Fail(NativeError()); + if (PortOf(current) != 0) return 0; + sockaddr_storage wildcard{}; + wildcard.ss_family = local.ss_family; + PortOf(wildcard) = PortOf(local); + if (::bind(socket.value, reinterpret_cast(&wildcard), localSize) != 0) return Fail(NativeError()); + return 0; +} void GuestAddress(const sockaddr_storage& native, void* output, std::uint32_t* length) { unsigned char bytes[28]{}; bytes[0] = native.ss_family == AF_INET ? 16 : 28; @@ -166,14 +348,109 @@ void GuestAddress(const sockaddr_storage& native, void* output, std::uint32_t* l std::memcpy(bytes + 8, &v6.sin6_addr, 16); std::memcpy(bytes + 24, &v6.sin6_scope_id, 4); } - std::memcpy(output, bytes, std::min(*length, bytes[0])); - *length = bytes[0]; + const auto copied = std::min(*length, bytes[0]); + std::memcpy(output, bytes, copied); + *length = copied; } } int GuestSockets::Close(int descriptor) { + std::shared_ptr released; + { + std::lock_guard lock(socketsMutex); + const auto found = sockets.find(descriptor); + if (found == sockets.end()) return Fail(9); + released = std::move(found->second); + sockets.erase(found); + closeOnExecDescriptors.erase(descriptor); + } + EqueueDescriptorClosed(descriptor); + ReleaseNumber(descriptor); + return 0; +} + +void GuestSockets::Forget(int descriptor) { + std::shared_ptr released; + { + std::lock_guard lock(socketsMutex); + const auto found = sockets.find(descriptor); + if (found == sockets.end()) return; + released = std::move(found->second); + sockets.erase(found); + closeOnExecDescriptors.erase(descriptor); + } + EqueueDescriptorClosed(descriptor); +} + +std::uintptr_t GuestSockets::CurrentWaker() { + return static_cast(ThreadWaker().value); +} + +void GuestSockets::Wake(std::uintptr_t waker) { + const char signal = 0; + if (send(static_cast(waker), &signal, 1, 0) < 0 && NativeError() != 35) + throw std::runtime_error("equeue waker: send failed, guest errno " + std::to_string(NativeError())); +} + +bool GuestSockets::WaitAny(const std::vector& interests, std::uint64_t deadlineNanos) { + const NativeSocket waker = ThreadWaker().value; + std::vector> held; +#ifdef _WIN32 + std::vector entries; + constexpr SHORT ReadMask = POLLRDNORM; + constexpr SHORT WriteMask = POLLWRNORM; + bool pipes = false; +#else + std::vector entries; + constexpr short ReadMask = POLLIN; + constexpr short WriteMask = POLLOUT; +#endif + { + std::lock_guard lock(socketsMutex); + for (const auto& interest : interests) { + const auto found = sockets.find(interest.descriptor); + 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}); + } + } + entries.push_back({waker, ReadMask, 0}); + std::uint64_t remaining = 0; + if (deadlineNanos != 0) { + const std::uint64_t now = TimedWait::NowNanos(); + if (now >= deadlineNanos) return true; + remaining = deadlineNanos - now; + } +#ifdef _WIN32 + INT timeout = -1; + if (deadlineNanos != 0) { + if (remaining < 1000000) return false; + timeout = static_cast(std::min(remaining / 1000000, INT_MAX)); + } + if (pipes && (timeout < 0 || timeout > 1)) timeout = 1; + if (WSAPoll(entries.data(), static_cast(entries.size()), timeout) == SOCKET_ERROR) + throw std::runtime_error("equeue wait: WSAPoll failed, guest errno " + std::to_string(NativeError())); +#else + timespec timeout{static_cast(remaining / 1000000000), static_cast(remaining % 1000000000)}; + if (::ppoll(entries.data(), entries.size(), deadlineNanos != 0 ? &timeout : nullptr, nullptr) < 0 && errno != EINTR) + throw std::runtime_error("equeue wait: ppoll failed, guest errno " + std::to_string(NativeError())); +#endif + char drained[16]; + while (recv(waker, drained, sizeof(drained), 0) > 0) {} + return true; +} + +std::intptr_t GuestSockets::NativeDescriptor(int descriptor) { std::lock_guard lock(socketsMutex); - return sockets.erase(descriptor) ? 0 : Fail(9); + const auto found = sockets.find(descriptor); + return found == sockets.end() ? -1 : static_cast(found->second->value); } bool GuestSockets::IsOpen(int descriptor) { @@ -185,11 +462,35 @@ 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); } +extern "C" int KernelReserveSocketNumber_nid_no_patch(std::uintptr_t native) { + const int number = ReserveNumber(static_cast(native)); + return number < 0 ? -*__error_nid_postfix() : number; +} + +extern "C" void KernelReleaseSocketNumber_nid_no_patch(int number) { + ReleaseNumber(number); +} + namespace { #ifdef _WIN32 using NativePollDescriptor = WSAPOLLFD; @@ -208,35 +509,153 @@ struct PollFlag { constexpr short GuestPollPriority = 0x2; constexpr short GuestPollInvalid = 0x20; constexpr short GuestPollAccepted = 0x1 | 0x2 | 0x4 | 0x8 | 0x10 | 0x20 | 0x40 | 0x80 | 0x100; +#ifdef _WIN32 +constexpr short NativePollIn = POLLRDNORM; +constexpr short NativePollPriority = POLLRDBAND; +#else +constexpr short NativePollIn = POLLIN; +constexpr short NativePollPriority = POLLPRI; +#endif constexpr PollFlag RequestFlags[] = { - {0x1, POLLIN}, {0x2, POLLPRI}, {0x4, POLLOUT}, {0x40, POLLRDNORM}, {0x80, POLLRDBAND}, {0x100, POLLWRBAND}}; + {0x1, NativePollIn}, {GuestPollPriority, NativePollPriority}, {0x4, POLLOUT}, {0x40, POLLRDNORM}, {0x80, POLLRDBAND}, {0x100, POLLWRBAND}}; constexpr PollFlag StatusFlags[] = {{0x8, POLLERR}, {0x10, POLLHUP}, {GuestPollInvalid, POLLNVAL}}; std::shared_ptr Find(int descriptor) { std::lock_guard lock(socketsMutex); const auto found = sockets.find(descriptor); return found != sockets.end() ? found->second : nullptr; } +constexpr short GuestPollReadable = 0x1 | 0x40; +constexpr short GuestPollWritable = 0x4; +constexpr short GuestPollVnodeReady = GuestPollReadable | GuestPollWritable; +constexpr short GuestPollHangUp = 0x10; +constexpr short GuestPollError = 0x8; +[[noreturn]] void ThrowReadEndWritable(int descriptor) { + throw std::runtime_error("poll: POLLOUT on the read end of pipe descriptor " + std::to_string(descriptor) + " is not supported"); +} +#ifdef _WIN32 +struct PipeIoStatus { + void* status; + ULONG_PTR information; +}; +struct PipeLocalInformation { + ULONG type; + ULONG configuration; + ULONG maximumInstances; + ULONG currentInstances; + ULONG inboundQuota; + ULONG readDataAvailable; + ULONG outboundQuota; + ULONG writeQuotaAvailable; + ULONG state; + ULONG end; +}; +using QueryInformationFile = LONG(NTAPI*)(HANDLE, PipeIoStatus*, void*, ULONG, int); +constexpr int FileAccessInformationClass = 8; +constexpr int FilePipeLocalInformationClass = 24; +constexpr ULONG PipeClosingState = 4; +constexpr ULONG PipeAtomicBytes = 512; +bool 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(std::string(function) + ": NtQueryInformationFile is unavailable"); + PipeIoStatus status{}; + if (query(handle, &status, &access, sizeof(access), FileAccessInformationClass) < 0 || + query(handle, &status, &pipe, sizeof(pipe), FilePipeLocalInformationClass) < 0) + 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; + if ((access & FILE_READ_DATA) && pipe.readDataAvailable > 0) + revents = static_cast(revents | (events & GuestPollReadable)); + if ((access & FILE_WRITE_DATA) && !closed && pipe.writeQuotaAvailable >= PipeAtomicBytes) + revents = static_cast(revents | (events & GuestPollWritable)); + if (closed) revents = static_cast(revents | (events & GuestPollReadable) | GuestPollHangUp); + return true; +} +#else +bool IsPipe(int descriptor, short events) { + struct stat status{}; + if (::fstat(descriptor, &status) != 0 || !S_ISFIFO(status.st_mode)) return false; + const int flags = ::fcntl(descriptor, F_GETFL); + if (flags < 0) throw std::runtime_error("poll: querying pipe descriptor " + std::to_string(descriptor) + " failed"); + if ((events & GuestPollWritable) && (flags & O_ACCMODE) == O_RDONLY) ThrowReadEndWritable(descriptor); + return true; +} +#endif +} + +#ifdef _WIN32 +bool GuestSockets::PipeReady(int descriptor, short events, short& revents) { + return PipeReadiness(descriptor, events, revents); +} +#endif + +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, 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; + { + std::lock_guard lock(socketsMutex); + const auto found = sockets.find(descriptor); + if (found == sockets.end()) return Fail(9); + if (target == descriptor) return target; + if (CopyNumberTo(descriptor, target) != 0) return -1; + const auto previous = sockets.find(target); + if (previous != sockets.end()) { + replaced = std::move(previous->second); + previous->second = found->second; + } else { + try { + sockets.emplace(target, found->second); + } catch (const std::bad_alloc&) { + ReleaseNumber(target); + return Fail(12); + } + } + } + if (replaced) EqueueDescriptorClosed(target); + { + std::lock_guard lock(socketsMutex); + closeOnExecDescriptors.erase(target); + } + return target; } namespace { constexpr int GuestUnix = 1; constexpr int GuestCloseOnExec = 0x10000000; constexpr int GuestNonBlocking = 0x20000000; -int PairError() { -#ifdef _WIN32 - switch (WSAGetLastError()) { - case WSAEMFILE: return 24; - default: return NativeError(); - } -#else - switch (errno) { - case EMFILE: return 24; - case ENFILE: return 23; - case ENOMEM: return 12; - default: return NativeError(); - } -#endif -} int SetNonBlocking(Socket& socket) { #ifdef _WIN32 unsigned long value = 1; @@ -245,9 +664,50 @@ 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 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); @@ -262,23 +722,23 @@ int NativePair(int type, NativeSocket (&pair)[2]) { loopback.sin_addr.s_addr = htonl(INADDR_LOOPBACK); const int nativeType = type == 1 ? SOCK_STREAM : SOCK_DGRAM; Socket first(::socket(AF_INET, nativeType, 0), 2, type); - if (first.value == Invalid) return PairError(); + if (first.value == Invalid) return NativeError(); const int exclusive = 1; setsockopt(first.value, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, reinterpret_cast(&exclusive), sizeof(exclusive)); - if (bind(first.value, reinterpret_cast(&loopback), sizeof(loopback))) return PairError(); + if (bind(first.value, reinterpret_cast(&loopback), sizeof(loopback))) return NativeError(); sockaddr_in firstName{}; - if (!LoopbackName(first.value, firstName)) return PairError(); + if (!LoopbackName(first.value, firstName)) return NativeError(); Socket second(::socket(AF_INET, nativeType, 0), 2, type); - if (second.value == Invalid) return PairError(); + if (second.value == Invalid) return NativeError(); if (type == 1) { - if (listen(first.value, 1)) return PairError(); - if (connect(second.value, reinterpret_cast(&firstName), sizeof(firstName))) return PairError(); + if (listen(first.value, 1)) return NativeError(); + if (connect(second.value, reinterpret_cast(&firstName), sizeof(firstName))) return NativeError(); sockaddr_in peer{}; int peerSize = sizeof(peer); Socket accepted(accept(first.value, reinterpret_cast(&peer), &peerSize), 2, type); - if (accepted.value == Invalid) return PairError(); + if (accepted.value == Invalid) return NativeError(); sockaddr_in secondName{}; - if (!LoopbackName(second.value, secondName)) return PairError(); + if (!LoopbackName(second.value, secondName)) return NativeError(); if (peer.sin_port != secondName.sin_port) return 53; const int noDelay = 1; setsockopt(second.value, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast(&noDelay), sizeof(noDelay)); @@ -289,11 +749,11 @@ int NativePair(int type, NativeSocket (&pair)[2]) { accepted.value = Invalid; return 0; } - if (bind(second.value, reinterpret_cast(&loopback), sizeof(loopback))) return PairError(); + if (bind(second.value, reinterpret_cast(&loopback), sizeof(loopback))) return NativeError(); sockaddr_in secondName{}; - if (!LoopbackName(second.value, secondName)) return PairError(); - if (connect(first.value, reinterpret_cast(&secondName), sizeof(secondName))) return PairError(); - if (connect(second.value, reinterpret_cast(&firstName), sizeof(firstName))) return PairError(); + if (!LoopbackName(second.value, secondName)) return NativeError(); + if (connect(first.value, reinterpret_cast(&secondName), sizeof(secondName))) return NativeError(); + if (connect(second.value, reinterpret_cast(&firstName), sizeof(firstName))) return NativeError(); pair[0] = first.value; pair[1] = second.value; first.value = Invalid; @@ -304,7 +764,7 @@ int NativePair(int type, NativeSocket (&pair)[2]) { int NativePair(int type, NativeSocket (&pair)[2]) { const int nativeType = type == 1 ? SOCK_STREAM : type == 2 ? SOCK_DGRAM : SOCK_SEQPACKET; int native[2]; - if (::socketpair(AF_UNIX, nativeType, 0, native)) return PairError(); + if (::socketpair(AF_UNIX, nativeType, 0, native)) return NativeError(); pair[0] = native[0]; pair[1] = native[1]; return 0; @@ -318,24 +778,247 @@ int UnnamedAddress(void* address, std::uint32_t* length) { } } +int GuestSockets::Stat(int descriptor, FileStat* status) { + std::shared_ptr socket; + { + std::lock_guard lock(socketsMutex); + const auto found = sockets.find(descriptor); + if (found == sockets.end()) return 9; + socket = found->second; + } + bool receiveShut = false; + bool sendShut = false; + { + std::lock_guard lock(socket->modeMutex); + receiveShut = socket->receiveShut; + sendShut = socket->sendShut; + } + std::int64_t available = 0; +#ifdef _WIN32 + unsigned long count = 0; + if (ioctlsocket(socket->value, FIONREAD, &count) == 0) available = static_cast(count); +#else + int count = 0; + if (::ioctl(socket->value, FIONREAD, &count) == 0) available = count; +#endif + int sendBuffer = 0; + socklen_t length = sizeof(sendBuffer); + if (getsockopt(socket->value, SOL_SOCKET, SO_SNDBUF, reinterpret_cast(&sendBuffer), &length) != 0) sendBuffer = 0; + *status = FileStat{}; + status->st_mode = 0140000; + if (!receiveShut || available > 0) status->st_mode |= 0444; + if (!sendShut) status->st_mode |= 0222; + status->st_size = available; + status->st_blksize = sendBuffer > 0 ? static_cast(sendBuffer) : 0; + return 0; +} + +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() && !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; +#else + pollfd entry{socket->value, static_cast(write ? POLLOUT : (POLLIN | POLLRDHUP)), 0}; + if (::poll(&entry, 1, 0) <= 0) return false; + if (!write && (entry.revents & POLLRDHUP)) entry.revents |= POLLHUP; +#endif + *eof = (entry.revents & (POLLHUP | POLLERR)) != 0; + *data = 0; + if (write) { + 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; +#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; +#ifdef _WIN32 + constexpr short NativeReadable = POLLRDNORM; + constexpr short NativeUrgent = POLLRDBAND; + constexpr short NativeWritable = POLLWRNORM; +#else + constexpr short NativeReadable = POLLIN; + constexpr short NativeUrgent = POLLPRI; + constexpr short NativeWritable = POLLOUT; +#endif + for (int index = 0; index < count; ++index) { + auto socket = Find(entries[index].descriptor); + if (!socket) return -9; + NativePollDescriptor request{}; + request.fd = socket->value; + if (entries[index].events & KernelSocketPoll::Readable) request.events = static_cast(request.events | NativeReadable); + if (entries[index].events & KernelSocketPoll::Urgent) request.events = static_cast(request.events | NativeUrgent); + if (entries[index].events & KernelSocketPoll::Writable) request.events = static_cast(request.events | NativeWritable); + native.push_back(request); + held.push_back(std::move(socket)); + } + if (native.empty()) return 0; +#ifdef _WIN32 + const int result = WSAPoll(native.data(), static_cast(native.size()), timeoutMilliseconds); +#else + const int result = ::poll(native.data(), static_cast(native.size()), timeoutMilliseconds); +#endif + if (result < 0) return -NativeError(); + int ready = 0; + for (int index = 0; index < count; ++index) { + const short events = native[static_cast(index)].revents; + short revents = 0; + if (events & NativeReadable) revents = static_cast(revents | KernelSocketPoll::Readable); + if (events & NativeUrgent) revents = static_cast(revents | KernelSocketPoll::Urgent); + if (events & NativeWritable) revents = static_cast(revents | KernelSocketPoll::Writable); + if (events & (POLLERR | POLLNVAL)) revents = static_cast(revents | KernelSocketPoll::Error); + if (events & POLLHUP) revents = static_cast(revents | KernelSocketPoll::HangUp); + entries[index].revents = revents; + if (revents != 0) ++ready; + } + return ready; +} + +extern "C" 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, ...) { + 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); +#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); +#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; @@ -344,58 +1027,89 @@ 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; + if (!GuestSockets::IsOpen(descriptor) && KernelSetSocketOption_nid_no_patch(descriptor, level, option, value, length, &netError)) { + return netError == 0 ? 0 : Fail(netError); + } const auto socket = Lookup(descriptor); if (!socket) return -1; - const int nativeOption = Option(option); - if (level != 0xffff || nativeOption == -1) return Fail(42); - if (!value || length != sizeof(int)) return Fail(22); - return ::setsockopt(socket->value, SOL_SOCKET, nativeOption, - static_cast(value), sizeof(int)) ? Fail(NativeError()) : 0; + if (level != 0xffff && level != 6 && level != 41) return Fail(42); + std::lock_guard lock(socket->modeMutex); + 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; - const int nativeOption = Option(option); - if (level != 0xffff || nativeOption == -1) return Fail(42); - if (!value || !length || *length < sizeof(int)) return Fail(22); - int result = 0; - socklen_t size = sizeof(result); - if (::getsockopt(socket->value, SOL_SOCKET, nativeOption, reinterpret_cast(&result), &size)) - return Fail(NativeError()); - std::memcpy(value, &result, sizeof(result)); - *length = sizeof(result); - return 0; + if (level != 0xffff && level != 6 && level != 41) return Fail(42); + if (!length) return Fail(22); + std::lock_guard lock(socket->modeMutex); + 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) { + const bool nonblocking = (type & GuestNonBlocking) != 0; + const int plainType = type & ~(GuestCloseOnExec | GuestNonBlocking); if (family != 2 && family != 28) return Fail(47); - const int nativeType = type == 1 ? SOCK_STREAM : type == 2 ? SOCK_DGRAM : -1; - const int expectedProtocol = type == 1 ? 6 : 17; - if (nativeType == -1 || (protocol != 0 && protocol != expectedProtocol)) return Fail(43); + const int nativeType = plainType == 1 ? SOCK_STREAM : plainType == 2 ? SOCK_DGRAM : -1; + const int expectedProtocol = plainType == 1 ? 6 : 17; + 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); #endif const auto native = ::socket(family == 2 ? AF_INET : AF_INET6, nativeType, protocol); if (native == Invalid) return Fail(NativeError()); - Socket guard(native, family, type); + Socket guard(native, family, plainType); + const int ipv6Only = 1; + if (family == 28 && ::setsockopt(native, IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast(&ipv6Only), sizeof(ipv6Only))) + return Fail(NativeError()); try { - auto socket = std::make_shared(native, family, type); + auto socket = std::make_shared(native, family, plainType); guard.value = Invalid; - std::lock_guard lock(socketsMutex); - if (nextDescriptor == INT_MAX) return Fail(24); - const int descriptor = nextDescriptor++; - sockets.emplace(descriptor, std::move(socket)); - return descriptor; + if (nonblocking && SetNonBlocking(*socket)) return -1; + 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); @@ -420,16 +1134,18 @@ int APS5_VABI socketpair_nid_postfix(int family, int type, int protocol, int* de auto second = std::make_shared(native[1], GuestUnix, plainType); secondGuard.value = Invalid; if (nonblocking && (SetNonBlocking(*first) || SetNonBlocking(*second))) return -1; - std::lock_guard lock(socketsMutex); - if (nextDescriptor >= INT_MAX - 1) return Fail(24); - const int firstDescriptor = nextDescriptor++; - const int secondDescriptor = nextDescriptor++; - sockets.emplace(firstDescriptor, std::move(first)); - try { - sockets.emplace(secondDescriptor, std::move(second)); - } catch (const std::bad_alloc&) { - sockets.erase(firstDescriptor); - return Fail(12); + const int firstDescriptor = Install(std::move(first)); + if (firstDescriptor < 0) return -1; + const int secondDescriptor = Install(std::move(second)); + if (secondDescriptor < 0) { + const int error = *__error_nid_postfix(); + GuestSockets::Close(firstDescriptor); + return Fail(error); + } + if ((type & GuestCloseOnExec) != 0) { + std::lock_guard registry(socketsMutex); + closeOnExecDescriptors.insert(firstDescriptor); + closeOnExecDescriptors.insert(secondDescriptor); } descriptors[0] = firstDescriptor; descriptors[1] = secondDescriptor; @@ -441,17 +1157,31 @@ int APS5_VABI socketpair_nid_postfix(int family, int type, int protocol, int* de int APS5_VABI connect_nid_postfix(int descriptor, const void* address, std::uint32_t length) { const auto socket = Lookup(descriptor); if (!socket) return -1; + if (socket->family != GuestUnix && socket->type == 2 && address && length >= 2 && static_cast(address)[1] == 0 + && Disconnect(*socket) != 0) + return -1; sockaddr_storage native{}; socklen_t size; if (!Address(address, length, native, size)) return -1; +#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); - return ::connect(socket->value, reinterpret_cast(&native), size) ? Fail(NativeError()) : 0; + { + std::lock_guard lock(socket->modeMutex); + socket->options.reportedError = 0; + } + return ::connect(socket->value, reinterpret_cast(&native), size) ? Fail(ConnectError()) : 0; } int APS5_VABI listen_nid_postfix(int descriptor, int backlog) { const auto socket = Lookup(descriptor); 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); @@ -466,10 +1196,17 @@ int APS5_VABI accept_nid_postfix(int descriptor, void* address, std::uint32_t* l try { auto accepted = std::make_shared(native, listener->family, listener->type); guard.value = Invalid; - std::lock_guard lock(socketsMutex); - if (nextDescriptor == INT_MAX) return Fail(24); - const int acceptedDescriptor = nextDescriptor++; - sockets.emplace(acceptedDescriptor, std::move(accepted)); + bool inheritNonblocking; + { + std::lock_guard modeLock(listener->modeMutex); + inheritNonblocking = listener->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)); + if (acceptedDescriptor < 0) return -1; if (address) GuestAddress(peer, address, length); return acceptedDescriptor; } catch (const std::bad_alloc&) { @@ -479,20 +1216,27 @@ 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)); - return result < 0 ? Fail(NativeError()) : result; + 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 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)); +#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) { @@ -505,9 +1249,13 @@ 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); - return ::shutdown(socket->value, how) ? Fail(NativeError()) : 0; + if (::shutdown(socket->value, how)) return Fail(NativeError()); + std::lock_guard lock(socket->modeMutex); + if (how != 1) socket->receiveShut = true; + if (how != 0) socket->sendShut = true; + return 0; } int APS5_VABI getsockname_nid_postfix(int descriptor, void* address, std::uint32_t* length) { const auto socket = Lookup(descriptor); @@ -516,7 +1264,15 @@ int APS5_VABI getsockname_nid_postfix(int descriptor, void* address, std::uint32 if (socket->family == GuestUnix) return UnnamedAddress(address, length); sockaddr_storage native{}; socklen_t size = sizeof(native); - if (::getsockname(socket->value, reinterpret_cast(&native), &size)) return Fail(NativeError()); + if (::getsockname(socket->value, reinterpret_cast(&native), &size)) { +#ifdef _WIN32 + if (WSAGetLastError() != WSAEINVAL) return Fail(NativeError()); + native = {}; + native.ss_family = socket->family == 2 ? AF_INET : AF_INET6; +#else + return Fail(NativeError()); +#endif + } GuestAddress(native, address, length); return 0; } @@ -532,6 +1288,8 @@ int APS5_VABI getpeername_nid_postfix(int descriptor, void* address, std::uint32 return 0; } int APS5_VABI ioctl_nid_postfix(int descriptor, std::uint64_t request, void* argument) { + if (!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); @@ -547,48 +1305,61 @@ int APS5_VABI ioctl_nid_postfix(int descriptor, std::uint64_t request, void* arg 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; } +int APS5_VABI _ioctl_nid_postfix(int descriptor, std::uint64_t request, void* argument) { + return ioctl_nid_postfix(descriptor, request, argument); +} 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) { 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), + result = static_cast(::sendto(socket->value, static_cast(buffer), static_cast(length), SocketSendFlags(*socket, flags), reinterpret_cast(&native), size)); } - return result < 0 ? Fail(NativeError()) : result; + return result < 0 ? Fail(SendError(socket->type)) : result; } std::int64_t APS5_VABI recvfrom_nid_postfix(int descriptor, void* buffer, std::uint64_t length, 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); sockaddr_storage native{}; socklen_t size = sizeof(native); - const auto result = ::recvfrom(socket->value, static_cast(buffer), static_cast(length), - flags & 2 ? MSG_PEEK : 0, reinterpret_cast(&native), &size); + if (const int waited = WaitForTransfer(*socket, false, flags)) return Fail(waited); + auto result = ::recvfrom(socket->value, static_cast(buffer), static_cast(length), + NativeReceiveFlags(flags), reinterpret_cast(&native), &size); +#ifdef _WIN32 + if (result < 0 && socket->type == 2 && WSAGetLastError() == WSAEMSGSIZE) result = static_cast(length); +#endif if (result < 0) return Fail(NativeError()); if (address && socket->family == GuestUnix) { if (socket->type == 1) *addressLength = 0; else UnnamedAddress(address, addressLength); return result; } - if (address) GuestAddress(native, address, addressLength); + if (address) { + if (native.ss_family == AF_INET || native.ss_family == AF_INET6) GuestAddress(native, address, addressLength); + else *addressLength = 0; + } return result; } struct GuestIovec { @@ -635,60 +1406,220 @@ 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); - const bool datagram = socket->type == 2; + if ((flags & ~GuestReceiveFlags) != 0) return Fail(45); + const bool records = socket->type == 2 || socket->type == 5; std::vector buffer; try { - buffer.resize(static_cast(total) + (datagram ? 1u : 0u)); + buffer.resize(static_cast(total) + (records ? 1u : 0u)); } catch (const std::bad_alloc&) { return Fail(55); } 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), reinterpret_cast(&native), &size); if (received < 0) { #ifdef _WIN32 - if (!datagram || WSAGetLastError() != WSAEMSGSIZE) return Fail(NativeError()); + if (!records || WSAGetLastError() != WSAEMSGSIZE) return Fail(NativeError()); received = static_cast(buffer.size()); #else 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 { + GuestIovec* headers; + int headerCount; + GuestIovec* trailers; + int trailerCount; +}; +static_assert(sizeof(GuestSendfileHeaders) == 32 && offsetof(GuestSendfileHeaders, trailers) == 16); +std::int64_t APS5_VABI sceKernelPread(int descriptor, void* buffer, std::size_t length, std::int64_t offset); +static bool SendfileIovecs(const GuestIovec* iov, int count) { + if (!iov) return true; + if (count < 0 || count > 1024) return false; + std::uint64_t total = 0; + for (int i = 0; i < count; ++i) { + if (iov[i].length > INT_MAX - total) return false; + total += iov[i].length; + } + return true; +} +static int SendAll(const Socket& socket, const void* data, std::uint64_t length, std::int64_t& sent) { + if (!data && length) return 14; + const char* bytes = static_cast(data); + while (length) { + const auto result = ::send(socket.value, bytes, static_cast(std::min(length, INT_MAX)), NativeSendFlags(GuestNoSignal)); + if (result < 0) return NativeError(); + bytes += result; + length -= static_cast(result); + sent += result; + } + return 0; +} +static int SendIovecs(const Socket& socket, const GuestIovec* iov, int count, std::int64_t& sent) { + for (int i = 0; iov && i < count; ++i) + if (const int error = SendAll(socket, iov[i].base, iov[i].length, sent)) return error; + return 0; +} +int APS5_VABI sendfile_nid_postfix(int file, int descriptor, std::int64_t offset, std::uint64_t length, + const GuestSendfileHeaders* headers, std::int64_t* sbytes, int flags) { + (void)flags; + if (offset < 0) return Fail(22); + if (headers && (!SendfileIovecs(headers->headers, headers->headerCount) || !SendfileIovecs(headers->trailers, headers->trailerCount))) + return Fail(22); + std::vector chunk; + try { + chunk.resize(0x10000); + } catch (const std::bad_alloc&) { + return Fail(12); + } + const auto read = [&](std::int64_t position, std::uint64_t remaining) { + const auto result = sceKernelPread(file, chunk.data(), static_cast(std::min(remaining, chunk.size())), position); + return result < 0 ? -(result & 0xffff) : result; + }; + std::uint64_t remaining = length ? length : UINT64_MAX; + auto received = read(offset, remaining); + if (received == -9) return Fail(9); + std::int64_t sent = 0; + const auto finish = [&](int error) { + if (sbytes) *sbytes = sent; + return error ? Fail(error) : 0; + }; + if (received < 0) return finish(received == -21 || received == -29 ? 22 : static_cast(-received)); + const auto socket = Lookup(descriptor); + if (!socket) return finish(9); + if (socket->type != 1) return finish(22); + sockaddr_storage peer{}; + socklen_t size = sizeof(peer); + if (::getpeername(socket->value, reinterpret_cast(&peer), &size)) return finish(57); + if (headers) + if (const int error = SendIovecs(*socket, headers->headers, headers->headerCount, sent)) return finish(error); + while (received > 0) { + if (const int error = SendAll(*socket, chunk.data(), static_cast(received), sent)) return finish(error); + offset += received; + remaining -= static_cast(received); + if (remaining == 0) break; + received = read(offset, remaining); + if (received < 0) return finish(static_cast(-received)); + } + if (headers) + if (const int error = SendIovecs(*socket, headers->trailers, headers->trailerCount, sent)) return finish(error); + return finish(0); +} 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; int ready = 0; for (std::uint32_t i = 0; i < count; ++i) { auto& entry = descriptors[i]; @@ -696,47 +1627,177 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c if (entry.descriptor < 0) continue; if ((entry.events & ~GuestPollAccepted) != 0) throw std::runtime_error("poll: unsupported event flags " + std::to_string(entry.events)); - if (entry.descriptor < GuestSockets::FirstDescriptor) - throw std::runtime_error("poll: descriptor " + std::to_string(entry.descriptor) + " is not a socket"); - auto socket = Find(entry.descriptor); - if (!socket) { - entry.revents = GuestPollInvalid; - ++ready; + 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, a pipe, a file nor a directory"); + pipes.push_back(i); continue; } -#ifdef _WIN32 - if (entry.events & GuestPollPriority) throw std::runtime_error("poll: POLLPRI is not supported by WSAPoll"); -#endif NativePollDescriptor request{}; - request.fd = socket->value; + 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)); } - if (native.empty()) { +#ifndef _WIN32 + const std::size_t socketCount = native.size(); + for (const auto index : pipes) { + NativePollDescriptor request{}; + request.fd = descriptors[index].descriptor; + for (const auto& flag : RequestFlags) + if (descriptors[index].events & flag.guest) request.events = static_cast(request.events | flag.native); + native.push_back(request); + owners.push_back(index); + } +#endif + if (native.empty() && pipes.empty()) { if (ready || timeout == 0) return ready; - if (timeout < 0) throw std::runtime_error("poll: an infinite wait without sockets never returns"); - std::this_thread::sleep_for(std::chrono::milliseconds(timeout)); + if (timeout < 0) throw std::runtime_error("poll: an infinite wait without sockets or pipes never returns"); + PreciseSleepUs(static_cast(timeout) * 1000); return 0; } - const int wait = ready ? 0 : timeout; -#ifdef _WIN32 - const int result = WSAPoll(native.data(), static_cast(native.size()), wait); -#else - const int result = ::poll(native.data(), static_cast(native.size()), wait); -#endif - if (result < 0) return Fail(NativeError()); - for (std::size_t i = 0; i < native.size(); ++i) { + const auto collect = [&](std::size_t i, bool pipe) { auto& entry = descriptors[owners[i]]; for (const auto& flag : RequestFlags) if ((entry.events & flag.guest) && (native[i].revents & flag.native)) entry.revents = static_cast(entry.revents | flag.guest); for (const auto& flag : StatusFlags) if (native[i].revents & flag.native) entry.revents = static_cast(entry.revents | flag.guest); + if (pipe && (entry.revents & GuestPollError)) entry.revents = static_cast((entry.revents & ~GuestPollError) | GuestPollHangUp); + if (pipe && (entry.revents & GuestPollHangUp)) + entry.revents = static_cast((entry.revents & ~GuestPollWritable) | (entry.events & GuestPollReadable)); if (entry.revents) ++ready; + }; +#ifdef _WIN32 + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout < 0 ? 0 : timeout); + for (;;) { + int pipeReady = 0; + for (const auto index : pipes) { + auto& entry = descriptors[index]; + PipeReadiness(entry.descriptor, entry.events, entry.revents); + if (entry.revents) ++pipeReady; + } + int slice = 0; + if (!ready && !pipeReady) { + if (pipes.empty()) slice = timeout; + else if (timeout < 0) slice = 1; + else { + const auto left = std::chrono::duration_cast(deadline - std::chrono::steady_clock::now()).count(); + slice = static_cast(std::clamp(left, 0, 1)); + } + } + if (!native.empty()) { + for (auto& request : native) request.revents = 0; + if (WSAPoll(native.data(), static_cast(native.size()), slice) < 0) return Fail(NativeError()); + } else if (!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) + if (request.revents) ++socketReady; + const bool expired = timeout >= 0 && std::chrono::steady_clock::now() >= deadline; + if (ready || pipeReady || socketReady || expired || pipes.empty()) { + ready += pipeReady; + for (std::size_t i = 0; i < native.size(); ++i) collect(i, false); + return ready; + } } +#else + const int wait = ready ? 0 : timeout; + const int result = ::poll(native.data(), static_cast(native.size()), wait); + if (result < 0) return Fail(NativeError()); + for (std::size_t i = 0; i < native.size(); ++i) collect(i, i >= socketCount); return ready; +#endif } } + +std::int64_t GuestSockets::Read(int descriptor, void* buffer, std::size_t length) { + if (length == 0) return IsOpen(descriptor) ? 0 : Fail(9); + return recv_nid_postfix(descriptor, buffer, std::min(length, INT_MAX), 0); +} + +std::int64_t GuestSockets::Write(int descriptor, const void* buffer, std::size_t length) { + return send_nid_postfix(descriptor, buffer, length, 0); +} + +std::int64_t GuestSockets::Readv(int descriptor, const Iovec* iov, int iovcnt) { + std::size_t total = 0; + for (int i = 0; i < iovcnt; ++i) { + if (iov[i].base == nullptr && iov[i].length != 0) return Fail(14); + if (iov[i].length > static_cast(INT_MAX) - total) return Fail(22); + total += iov[i].length; + } + if (total == 0) return IsOpen(descriptor) ? 0 : Fail(9); + std::vector buffer; + try { + buffer.resize(total); + } catch (const std::bad_alloc&) { + return Fail(12); + } + const auto result = Read(descriptor, buffer.data(), total); + if (result < 0) return result; + std::size_t copied = 0; + for (int i = 0; i < iovcnt && copied < static_cast(result); ++i) { + const auto amount = std::min(iov[i].length, static_cast(result) - copied); + if (amount != 0) std::memcpy(iov[i].base, buffer.data() + copied, amount); + copied += amount; + } + return result; +} + +std::int64_t GuestSockets::Writev(int descriptor, const Iovec* iov, int iovcnt) { + std::size_t total = 0; + for (int i = 0; i < iovcnt; ++i) { + if (iov[i].base == nullptr && iov[i].length != 0) return Fail(14); + if (iov[i].length > static_cast(INT_MAX) - total) return Fail(22); + total += iov[i].length; + } + if (total == 0) return Write(descriptor, nullptr, 0); + std::vector buffer; + try { + buffer.resize(total); + } catch (const std::bad_alloc&) { + return Fail(12); + } + std::size_t copied = 0; + for (int i = 0; i < iovcnt; ++i) { + if (iov[i].length != 0) std::memcpy(buffer.data() + copied, iov[i].base, iov[i].length); + copied += iov[i].length; + } + return Write(descriptor, buffer.data(), total); +} diff --git a/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp b/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp index 563c67f585..98da8ea9e9 100644 --- a/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp +++ b/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp @@ -2,8 +2,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -14,10 +16,45 @@ #include "prx/libkernel/Time/include/Time.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" +extern "C" int* APS5_VABI __error_nid_postfix(); + namespace { constexpr int SYNC_ON_ADDRESS_OK = 0; +constexpr int UMTX_OP_RESERVED1 = 1; +constexpr int UMTX_OP_WAIT = 2; +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; +constexpr int GUEST_CLOCK_REALTIME = 0; + +constexpr int GUEST_EFAULT = 14; +constexpr int GUEST_EINVAL = 22; +constexpr int GUEST_EOPNOTSUPP = 45; +constexpr int GUEST_ETIMEDOUT = 60; + +constexpr std::int64_t NANOS_PER_SECOND = 1000000000; +constexpr std::uint64_t UNBOUNDED_WAIT_NANOS = 1ULL << 62; + +struct UmtxTime { + KernelTimespec timeout; + std::uint32_t flags; + 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; @@ -42,13 +79,26 @@ bool IsAlignedAddress(std::uintptr_t address) { return address != 0 && address % alignof(TValue) == 0; } -template -int WaitOnAddress(TValue* address, TValue expected, const KernelUseconds* timeout, const void* caller) { - const auto key = reinterpret_cast(address); +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 (std::atomic_ref(*address).load() != expected) { - return SYNC_ON_ADDRESS_OK; + if (ready()) { + woken = true; + return std::nullopt; } AddressWaiter waiter; @@ -56,23 +106,203 @@ int WaitOnAddress(TValue* address, TValue expected, const KernelUseconds* timeou const auto position = queue.insert(queue.end(), &waiter); const auto isWoken = [&] { return waiter.woken; }; const auto waitStart = std::chrono::steady_clock::now(); - if (timeout == nullptr) { + if (!deadlineNanos) { waiter.condition.Wait(lock, isWoken); } else { - waiter.condition.WaitUntil(lock, TimedWait::DeadlineNanos(*timeout), isWoken); + waiter.condition.WaitUntil(lock, *deadlineNanos, isWoken); } const auto waited = std::chrono::duration_cast(std::chrono::steady_clock::now() - waitStart); - const bool woken = waiter.woken; + woken = waiter.woken; if (!woken) { queue.erase(position); if (queue.empty()) { bucket.waiters.erase(key); } } - lock.unlock(); - KernelTraceWait_nid_postfix("addr", caller, static_cast(waited.count()), !woken); - return woken ? SYNC_ON_ADDRESS_OK : SCE_KERNEL_ERROR_ETIMEDOUT; + 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 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; + for (std::int32_t woken = 0; !queue.empty() && (woken < count || (wakesAtLeastOne && woken == 0)); ++woken) { + AddressWaiter* waiter = queue.front(); + queue.pop_front(); + waiter->woken = true; + waiter->condition.NotifyOne(); + } + if (queue.empty()) { + 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; + } + if (seconds >= static_cast(UNBOUNDED_WAIT_NANOS / NANOS_PER_SECOND)) { + return UNBOUNDED_WAIT_NANOS; + } + const std::int64_t total = seconds * NANOS_PER_SECOND + nanos; + return total <= 0 ? 0 : static_cast(total); +} + +int UmtxDeadline(const void* sizeArgument, const void* timeArgument, std::optional& deadlineNanos) { + deadlineNanos.reset(); + 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); + } else { + time = *static_cast(timeArgument); + } + if (time.timeout.tv_sec < 0 || time.timeout.tv_nsec < 0 || time.timeout.tv_nsec >= NANOS_PER_SECOND) { + return GUEST_EINVAL; + } + + std::uint64_t remaining = 0; + if ((time.flags & UMTX_ABSTIME) == 0) { + remaining = SaturatingNanos(time.timeout.tv_sec, time.timeout.tv_nsec); + } else { + KernelTimespec now{}; + clock_gettime_nid_postfix(static_cast(time.clockId), &now); + remaining = SaturatingNanos(time.timeout.tv_sec - now.tv_sec, time.timeout.tv_nsec - now.tv_nsec); + } + if (remaining < UNBOUNDED_WAIT_NANOS) { + deadlineNanos = TimedWait::NowNanos() + remaining; + } + return 0; +} + +template +int UmtxWait(void* object, std::uint64_t expected, 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 (object == nullptr) { + return GUEST_EFAULT; + } + if (!IsAlignedAddress(reinterpret_cast(object))) { + APS5_INVALID_ARG_EX; + } + if (expected > std::numeric_limits::max()) { + return 0; + } + return WaitOnAddressUntil(static_cast(object), static_cast(expected), deadline, caller) ? 0 : GUEST_ETIMEDOUT; +} + +int UmtxWake(void* object, std::uint64_t count, bool isPrivate) { + if (object == nullptr) { + return isPrivate ? 0 : GUEST_EFAULT; + } + WakeAddress(reinterpret_cast(object), static_cast(count), true); + return 0; +} + +int UmtxNwakePrivate(void* object, std::uint64_t value) { + const auto count = static_cast(value); + if (count <= 0) { + return 0; + } + if (object == nullptr) { + return GUEST_EFAULT; + } + const auto* addresses = static_cast(object); + for (std::int32_t index = 0; index < count; ++index) { + WakeAddress(reinterpret_cast(addresses[index]), std::numeric_limits::max(), true); + } + 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 @@ -125,23 +355,49 @@ int APS5_VABI sceKernelSyncOnAddressWake(void* address, std::int32_t count) { APS5_INVALID_ARG_EX; } - auto& bucket = BucketOf(key); - std::lock_guard lock(bucket.mutex); - const auto entry = bucket.waiters.find(key); - if (entry == bucket.waiters.end()) { - return SYNC_ON_ADDRESS_OK; - } - auto& queue = entry->second; - for (; count > 0 && !queue.empty(); --count) { - AddressWaiter* waiter = queue.front(); - queue.pop_front(); - waiter->woken = true; - waiter->condition.NotifyOne(); - } - if (queue.empty()) { - bucket.waiters.erase(entry); - } + WakeAddress(key, count, false); return SYNC_ON_ADDRESS_OK; } +int APS5_VABI _umtx_op_nid_postfix(void* object, int operation, std::uint64_t value, void* sizeArgument, void* timeArgument) { + const void* caller = __builtin_return_address(0); + int error = 0; + switch (operation) { + case UMTX_OP_WAIT: + error = UmtxWait(object, value, sizeArgument, timeArgument, caller); + break; + case UMTX_OP_WAIT_UINT: + case UMTX_OP_WAIT_UINT_PRIVATE: + error = UmtxWait(object, value, sizeArgument, timeArgument, caller); + break; + case UMTX_OP_WAKE: + case UMTX_OP_WAKE_PRIVATE: + error = UmtxWake(object, value, operation == UMTX_OP_WAKE_PRIVATE); + break; + 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) { + error = GUEST_EOPNOTSUPP; + } else { + NotImplemented_nid_no_patch(("_umtx_op operation " + std::to_string(operation)).c_str()); + } + break; + } + if (error == 0) { + return 0; + } + *__error_nid_postfix() = error; + return -1; +} + } diff --git a/core/libs/prx/libkernel/System/include/HostCpu.hpp b/core/libs/prx/libkernel/System/include/HostCpu.hpp new file mode 100644 index 0000000000..11b3368dbf --- /dev/null +++ b/core/libs/prx/libkernel/System/include/HostCpu.hpp @@ -0,0 +1,31 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_SYSTEM_HOSTCPU_HPP +#define CORE_LIBS_PRX_LIBKERNEL_SYSTEM_HOSTCPU_HPP + +#include +#include +#include + +struct HostCpuRegisters { + std::uint32_t Leaf1Ecx = 0; + std::uint32_t Leaf1Edx = 0; + std::uint32_t Leaf7Ebx = 0; + std::uint32_t Leaf7Ecx = 0; + std::uint32_t LeafDSub1Eax = 0; + std::uint32_t Leaf80000001Ecx = 0; + std::uint32_t Leaf80000008Ebx = 0; + std::uint64_t Xcr0 = 0; +}; + +struct HostCpuFeature { + const char* Name; + bool Required; + bool Present; +}; + +HostCpuRegisters ReadHostCpuRegisters(); +std::string HostCpuModel(); +std::vector HostCpuFeatures(const HostCpuRegisters& registers); +std::vector MissingHostCpuFeatures(const HostCpuRegisters& registers); +void CheckHostCpu(const HostCpuRegisters& registers, const std::string& model); + +#endif diff --git a/core/libs/prx/libkernel/System/include/ThermalSensors.hpp b/core/libs/prx/libkernel/System/include/ThermalSensors.hpp new file mode 100644 index 0000000000..7f8c299d01 --- /dev/null +++ b/core/libs/prx/libkernel/System/include/ThermalSensors.hpp @@ -0,0 +1,6 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_SYSTEM_THERMALSENSORS_HPP +#define CORE_LIBS_PRX_LIBKERNEL_SYSTEM_THERMALSENSORS_HPP + +extern "C" void ReadThermalSensors_nid_no_patch(const char* root, int celsius[3]); + +#endif diff --git a/core/libs/prx/libkernel/System/include/UserContext.hpp b/core/libs/prx/libkernel/System/include/UserContext.hpp new file mode 100644 index 0000000000..485c68cf3c --- /dev/null +++ b/core/libs/prx/libkernel/System/include/UserContext.hpp @@ -0,0 +1,70 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_SYSTEM_INCLUDE_USERCONTEXT_HPP +#define CORE_LIBS_PRX_LIBKERNEL_SYSTEM_INCLUDE_USERCONTEXT_HPP + +#include +#include + +struct UserMcontext { + std::uint64_t onstack; + std::uint64_t rdi; + std::uint64_t rsi; + std::uint64_t rdx; + std::uint64_t rcx; + std::uint64_t r8; + std::uint64_t r9; + std::uint64_t rax; + std::uint64_t rbx; + std::uint64_t rbp; + std::uint64_t r10; + std::uint64_t r11; + std::uint64_t r12; + std::uint64_t r13; + std::uint64_t r14; + std::uint64_t r15; + std::uint32_t trapno; + std::uint16_t fs; + std::uint16_t gs; + std::uint64_t addr; + std::uint32_t flags; + std::uint16_t es; + std::uint16_t ds; + std::uint64_t err; + std::uint64_t rip; + std::uint64_t cs; + std::uint64_t rflags; + std::uint64_t rsp; + std::uint64_t ss; + std::uint64_t len; + std::uint64_t fpformat; + std::uint64_t ownedfp; + alignas(16) std::uint64_t fpstate[64]; + std::uint64_t fsbase; + std::uint64_t gsbase; + std::uint64_t xfpustate; + std::uint64_t xfpustateLength; + std::uint64_t spare[4]; +}; + +struct UserUcontext { + std::uint32_t sigmask[4]; + UserMcontext mcontext; + UserUcontext* link; + void* stackPointer; + std::uint64_t stackSize; + std::int32_t stackFlags; + std::int32_t stackAlign; + std::int32_t flags; + std::int32_t spare[4]; +}; + +static_assert(offsetof(UserMcontext, rsp) == 0xb8 && offsetof(UserMcontext, len) == 0xc8); +static_assert(offsetof(UserMcontext, fpstate) == 0xe0 && offsetof(UserMcontext, fsbase) == 0x2e0); +static_assert(sizeof(UserMcontext) == 0x320); +static_assert(offsetof(UserUcontext, mcontext) == 0x10); +static_assert(offsetof(UserUcontext, link) == 0x330); +static_assert(offsetof(UserUcontext, stackPointer) == 0x338); +static_assert(offsetof(UserUcontext, stackSize) == 0x340); +static_assert(offsetof(UserUcontext, flags) == 0x350); +static_assert(sizeof(UserUcontext) == 0x370); + +#endif diff --git a/core/libs/prx/libkernel/System/src/Coredump.cpp b/core/libs/prx/libkernel/System/src/Coredump.cpp index 7673711237..3bc229914c 100644 --- a/core/libs/prx/libkernel/System/src/Coredump.cpp +++ b/core/libs/prx/libkernel/System/src/Coredump.cpp @@ -2,6 +2,8 @@ #include #include #include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -9,6 +11,23 @@ static std::atomic g_coredumpHandler{0}; static std::atomic g_coredumpContext{0}; +static constexpr int SCE_COREDUMP_ERROR_INVALID_ARGUMENT = static_cast(0x81180000u); +static constexpr int SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER = static_cast(0x81180003u); + +static void requireRegionOutsideHandler(const char* function, const void* mem, size_t size) { + if (mem == nullptr || size == 0 || size > UINTPTR_MAX - reinterpret_cast(mem)) + throw std::runtime_error(std::string(function) + ": null, empty or wrapping region outside the core dump handler"); +} + +static int attachOutsideCoredumpHandler(const char* function, const void* mem, size_t size) { + requireRegionOutsideHandler(function, mem, size); + return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; +} + +static void requireTextOutsideHandler(const char* function, const char* argument, const char* text) { + if (text == nullptr) throw std::runtime_error(std::string(function) + ": null " + argument + " outside the core dump handler"); +} + extern "C" { int APS5_VABI sceCoredumpRegisterCoredumpHandler(uint64_t handler, size_t stack_size, uint64_t context) { @@ -31,19 +50,23 @@ int APS5_VABI sceKernelDebugWriteCppExceptionInfo(const void* exception, uint64_ } -int APS5_VABI sceCoredumpAttachUserFile(void) { - NotImplemented_nid_no_patch("5nc2gdLNsok"); - return 0; +int APS5_VABI sceCoredumpAttachUserFile(uint32_t userValue, const char* path) { + (void)userValue; + requireTextOutsideHandler(__func__, "path", path); + return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; } -int APS5_VABI sceCoredumpGetStopInfoGpu_Agc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceCoredumpGetStopInfoGpu_Agc(void* info, size_t size) { + (void)size; + if (info == nullptr) return SCE_COREDUMP_ERROR_INVALID_ARGUMENT; + return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; } -int APS5_VABI sceCoredumpAttachMemoryRegionAsUserFile(void) { - NotImplemented_nid_no_patch("MEJ7tc7ThwM"); - return 0; +int APS5_VABI sceCoredumpAttachMemoryRegionAsUserFile(uint32_t userValue, const void* mem, size_t size, const char* name) { + (void)userValue; + requireRegionOutsideHandler(__func__, mem, size); + requireTextOutsideHandler(__func__, "name", name); + return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; } int APS5_VABI sceCoredumpSetUserDataType(void) { @@ -51,28 +74,49 @@ int APS5_VABI sceCoredumpSetUserDataType(void) { return 0; } -int APS5_VABI sceCoredumpDebugTextOut(void) { - NotImplemented_nid_no_patch("dei8oUx6DbU"); +void APS5_VABI sceCoredumpDebugTextOut(const char* str, int len) { + if (len < 0 || (str == nullptr && len != 0)) + throw std::runtime_error(std::string(__func__) + ": null text or negative length"); + if (len == 0) return; + const int shown = str[len - 1] == '\n' ? len - 1 : len; + std::fprintf(stderr, "[coredump] %.*s\n", shown, str); +} + +int64_t APS5_VABI sceCoredumpWriteUserData(const void* data, size_t size) { + requireRegionOutsideHandler(__func__, data, size); + return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; +} + +int APS5_VABI sceCoredumpGetStopInfoCpu(void* info, size_t size) { + const auto address = reinterpret_cast(info); + if (info == nullptr || size != 16 || address % alignof(uint64_t) != 0 || size > UINTPTR_MAX - address) + throw std::runtime_error(std::string(__func__) + ": null, unaligned, wrapping or unsupported CPU stop-info buffer outside the core dump handler"); + return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; +} + +int APS5_VABI sceCoredumpGetThreadContextInfo(void* threadContextInfo, size_t threadContextInfoSize) { + const auto address = reinterpret_cast(threadContextInfo); + if (threadContextInfo == nullptr || threadContextInfoSize != 144 || address % alignof(uint64_t) != 0 || threadContextInfoSize > UINTPTR_MAX - address) + throw std::runtime_error(std::string(__func__) + ": null, unaligned, wrapping or unsupported thread context buffer outside the core dump handler"); + return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; +} + +int APS5_VABI sceCoredumpWriteUserString(uint32_t first, const char* text, int third) { + if (text == nullptr || first > 15 || third < 0 || third > 256) return SCE_COREDUMP_ERROR_INVALID_ARGUMENT; return 0; } -int APS5_VABI sceCoredumpGetStopInfoCpu(void) { - NotImplemented_nid_no_patch("kK0DUW1Ukgc"); - return 0; +int APS5_VABI sceCoredumpConfigDumpMode(int mode) { + return mode == 1 ? 0 : SCE_COREDUMP_ERROR_INVALID_ARGUMENT; } -int APS5_VABI sceCoredumpWriteUserString() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceCoredumpAttachUserMemoryFile(uint32_t userValue, const void* mem, size_t size) { + (void)userValue; + return attachOutsideCoredumpHandler(__func__, mem, size); } -int APS5_VABI sceCoredumpAttachUserMemoryFile(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCoredumpAttachMemoryRegion(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceCoredumpAttachMemoryRegion(uint32_t userValue, const void* mem, size_t size) { + (void)userValue; + return attachOutsideCoredumpHandler(__func__, mem, size); } } diff --git a/core/libs/prx/libkernel/System/src/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 718e4119d3..b7eb4f65f7 100644 --- a/core/libs/prx/libkernel/System/src/Exception.cpp +++ b/core/libs/prx/libkernel/System/src/Exception.cpp @@ -4,14 +4,26 @@ #include "prx/libc/include/General.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Pthread/include/Pthread.hpp" +#include +#include #include #include +#include #include #include #include #include #ifdef _WIN32 +#include #include +#else +#include +#include +#include +#include +#include +#include +#include #endif extern "C" Pthread APS5_VABI scePthreadSelf(); @@ -65,7 +77,7 @@ struct GuestMcontext { std::uint64_t spare[6]; }; -struct GuestUcontext { +struct alignas(16) GuestUcontext { std::uint32_t sigmask[4]; std::int32_t reserved[12]; GuestMcontext mcontext; @@ -85,9 +97,10 @@ static_assert(offsetof(GuestUcontext, mcontext) + offsetof(GuestMcontext, rsp) = using GuestExceptionHandler = void (APS5_VABI *)(int, void*); constexpr std::array AllowedSignals{1, 4, 8, 10, 11, 30}; +constexpr int RaisedSignal = 30; -std::mutex handlersLock; -std::array handlers{}; +std::array, 32> handlers{}; +static_assert(std::atomic::is_always_lock_free); bool Allowed(int signum) { for (const int allowed : AllowedSignals) @@ -96,14 +109,90 @@ bool Allowed(int signum) { } GuestExceptionHandler Handler(int signum) { - std::lock_guard lock(handlersLock); - return reinterpret_cast(handlers[signum]); + return reinterpret_cast(handlers[signum].load(std::memory_order_acquire)); +} + +constexpr std::size_t MxcsrOffset = 24; +constexpr std::size_t MxcsrMaskOffset = 28; +constexpr std::uint32_t DefaultMxcsrMask = 0xffbf; + +std::uint32_t FpWord(const GuestMcontext& m, std::size_t offset) { + std::uint32_t value = 0; + std::memcpy(&value, reinterpret_cast(m.fpstate) + offset, sizeof(value)); + return value; +} + +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); } #ifdef _WIN32 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 { @@ -145,7 +234,7 @@ void Deliver(GuestExceptionHandler handler, int signum, CONTEXT& context) { m.len = sizeof(GuestMcontext); static_assert(sizeof(context.FltSave) <= sizeof(m.fpstate)); std::memcpy(m.fpstate, &context.FltSave, sizeof(context.FltSave)); - handler(signum, &ucontext); + CallHandler(handler, signum, ucontext); context.Rdi = m.rdi; context.Rsi = m.rsi; context.Rdx = m.rdx; @@ -165,6 +254,7 @@ void Deliver(GuestExceptionHandler handler, int signum, CONTEXT& context) { context.Rsp = m.rsp; context.EFlags = static_cast(m.rflags); std::memcpy(&context.FltSave, m.fpstate, sizeof(context.FltSave)); + context.MxCsr = context.FltSave.MxCsr; } [[noreturn]] void RedirectedEntry(Delivery* delivery) { @@ -217,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{}; @@ -227,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; @@ -249,9 +435,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); @@ -270,6 +465,119 @@ bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { ResumeThread(native); return true; } +#else +constexpr int HostSignal = SIGUSR1; +constexpr std::size_t FxsaveRegisterBytes = 416; + +void FromHost(const mcontext_t& host, GuestMcontext& m) { + const auto* g = host.gregs; + m.rdi = g[REG_RDI]; + m.rsi = g[REG_RSI]; + m.rdx = g[REG_RDX]; + m.rcx = g[REG_RCX]; + m.r8 = g[REG_R8]; + m.r9 = g[REG_R9]; + m.rax = g[REG_RAX]; + m.rbx = g[REG_RBX]; + m.rbp = g[REG_RBP]; + m.r10 = g[REG_R10]; + m.r11 = g[REG_R11]; + m.r12 = g[REG_R12]; + m.r13 = g[REG_R13]; + m.r14 = g[REG_R14]; + m.r15 = g[REG_R15]; + m.rip = g[REG_RIP]; + m.rsp = g[REG_RSP]; + m.rflags = g[REG_EFL]; + m.cs = static_cast(g[REG_CSGSFS]) & 0xffffu; + m.ss = static_cast(g[REG_CSGSFS]) >> 48; + m.len = sizeof(GuestMcontext); + static_assert(sizeof(*host.fpregs) <= sizeof(m.fpstate) && FxsaveRegisterBytes <= sizeof(*host.fpregs)); + if (host.fpregs != nullptr) std::memcpy(m.fpstate, host.fpregs, sizeof(*host.fpregs)); +} + +void ToHost(const GuestMcontext& m, mcontext_t& host) { + auto* g = host.gregs; + g[REG_RDI] = static_cast(m.rdi); + g[REG_RSI] = static_cast(m.rsi); + g[REG_RDX] = static_cast(m.rdx); + g[REG_RCX] = static_cast(m.rcx); + g[REG_R8] = static_cast(m.r8); + g[REG_R9] = static_cast(m.r9); + g[REG_RAX] = static_cast(m.rax); + g[REG_RBX] = static_cast(m.rbx); + g[REG_RBP] = static_cast(m.rbp); + g[REG_R10] = static_cast(m.r10); + g[REG_R11] = static_cast(m.r11); + g[REG_R12] = static_cast(m.r12); + g[REG_R13] = static_cast(m.r13); + g[REG_R14] = static_cast(m.r14); + g[REG_R15] = static_cast(m.r15); + g[REG_RIP] = static_cast(m.rip); + g[REG_RSP] = static_cast(m.rsp); + g[REG_EFL] = static_cast(m.rflags); + if (host.fpregs != nullptr) std::memcpy(host.fpregs, m.fpstate, FxsaveRegisterBytes); +} + +void Deliver(int, siginfo_t*, void* context) { + const auto handler = Handler(RaisedSignal); + if (handler == nullptr) { + static constexpr char message[] = "sceKernelRaiseException: the handler was removed before the signal was delivered\n"; + const auto written = write(STDERR_FILENO, message, sizeof(message) - 1); + (void)written; + std::abort(); + } + auto& host = static_cast(context)->uc_mcontext; + GuestUcontext ucontext{}; + FromHost(host, ucontext.mcontext); + CallHandler(handler, RaisedSignal, ucontext); + ToHost(ucontext.mcontext, host); +} + +void InstallDelivery() { + static const bool installed = [] { + struct sigaction action{}; + action.sa_sigaction = Deliver; + action.sa_flags = SA_SIGINFO | SA_RESTART; + sigemptyset(&action.sa_mask); + if (sigaction(HostSignal, &action, nullptr) != 0) + throw std::system_error(errno, std::generic_category(), "sceKernelRaiseException: cannot install the host signal handler"); + return true; + }(); + (void)installed; +} + +bool Send(Pthread thread) { + const int result = pthread_kill(thread->hostThread, HostSignal); + if (result == ESRCH) return false; + if (result != 0) throw std::system_error(result, std::generic_category(), "sceKernelRaiseException: cannot signal the target thread"); + return true; +} + +class HostSignalBlock { +public: + HostSignalBlock() { + sigset_t blocked; + sigemptyset(&blocked); + sigaddset(&blocked, HostSignal); + pthread_sigmask(SIG_BLOCK, &blocked, &previous); + } + ~HostSignalBlock() { pthread_sigmask(SIG_SETMASK, &previous, nullptr); } + HostSignalBlock(const HostSignalBlock&) = delete; + HostSignalBlock& operator=(const HostSignalBlock&) = delete; + +private: + sigset_t previous; +}; + +bool RaiseOn(Pthread thread) { + InstallDelivery(); + if (thread == scePthreadSelf()) return Send(thread); + HostSignalBlock block; + std::lock_guard lock(thread->_join_mtx); + if (thread->_finished.load(std::memory_order_acquire)) return false; + return Send(thread); +} #endif } @@ -346,38 +654,35 @@ extern "C" { int APS5_VABI sceKernelInstallExceptionHandler(int signum, void* handler) { if (!Allowed(signum) || handler == nullptr) return SCE_KERNEL_ERROR_EINVAL; - std::lock_guard lock(handlersLock); - if (handlers[signum] != nullptr) return SCE_KERNEL_ERROR_EAGAIN; - handlers[signum] = handler; + void* expected = nullptr; + if (!handlers[signum].compare_exchange_strong(expected, handler, std::memory_order_acq_rel)) return SCE_KERNEL_ERROR_EAGAIN; return 0; } int APS5_VABI sceKernelRemoveExceptionHandler(int signum) { if (!Allowed(signum)) return SCE_KERNEL_ERROR_EINVAL; - std::lock_guard lock(handlersLock); - handlers[signum] = nullptr; + handlers[signum].store(nullptr, std::memory_order_release); return 0; } int APS5_VABI sceKernelRaiseException(Pthread thread, int signum) { - if (signum != 30) return SCE_KERNEL_ERROR_EINVAL; + if (signum != RaisedSignal) return SCE_KERNEL_ERROR_EINVAL; if (thread == nullptr || thread->_finished.load(std::memory_order_acquire)) return SCE_KERNEL_ERROR_ESRCH; const auto handler = Handler(signum); if (handler == nullptr) throw std::runtime_error("sceKernelRaiseException: no handler installed for the signal"); #ifdef _WIN32 return RaiseOn(thread, handler, signum) ? 0 : SCE_KERNEL_ERROR_ESRCH; #else - NotImplemented_nid_no_patch(__func__); - return 0; + return RaiseOn(thread) ? 0 : SCE_KERNEL_ERROR_ESRCH; #endif } 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/HostCpu.cpp b/core/libs/prx/libkernel/System/src/HostCpu.cpp new file mode 100644 index 0000000000..a27d29b6dc --- /dev/null +++ b/core/libs/prx/libkernel/System/src/HostCpu.cpp @@ -0,0 +1,199 @@ +#include "prx/libkernel/System/include/HostCpu.hpp" +#include "prx/libc/include/general/LogMacros.hpp" +#if defined(__x86_64__) || defined(__i386__) +#include +#endif +#include +#include +#include +#include +#include +#include + +namespace { + +enum class Register : std::uint8_t { + Leaf1Ecx, + Leaf1Edx, + Leaf7Ebx, + Leaf7Ecx, + LeafDSub1Eax, + Leaf80000001Ecx, + Leaf80000008Ebx +}; + +struct FeatureBit { + const char* Name; + Register Source; + unsigned Bit; + bool Required; +}; + +constexpr FeatureBit kFeatures[] = { + {"SSE", Register::Leaf1Edx, 25, true}, + {"SSE2", Register::Leaf1Edx, 26, true}, + {"SSE3", Register::Leaf1Ecx, 0, true}, + {"SSSE3", Register::Leaf1Ecx, 9, true}, + {"SSE4.1", Register::Leaf1Ecx, 19, true}, + {"SSE4.2", Register::Leaf1Ecx, 20, true}, + {"POPCNT", Register::Leaf1Ecx, 23, true}, + {"LZCNT", Register::Leaf80000001Ecx, 5, true}, + {"BMI1", Register::Leaf7Ebx, 3, true}, + {"BMI2", Register::Leaf7Ebx, 8, true}, + {"MOVBE", Register::Leaf1Ecx, 22, true}, + {"CMPXCHG16B", Register::Leaf1Ecx, 13, true}, + {"LAHF/SAHF", Register::Leaf80000001Ecx, 0, true}, + {"AVX", Register::Leaf1Ecx, 28, true}, + {"AVX2", Register::Leaf7Ebx, 5, true}, + {"FMA", Register::Leaf1Ecx, 12, true}, + {"F16C", Register::Leaf1Ecx, 29, true}, + {"AES", Register::Leaf1Ecx, 25, false}, + {"PCLMULQDQ", Register::Leaf1Ecx, 1, false}, + {"RDRAND", Register::Leaf1Ecx, 30, false}, + {"RDSEED", Register::Leaf7Ebx, 18, false}, + {"ADX", Register::Leaf7Ebx, 19, false}, + {"SHA", Register::Leaf7Ebx, 29, false}, + {"CLFLUSHOPT", Register::Leaf7Ebx, 23, false}, + {"CLWB", Register::Leaf7Ebx, 24, false}, + {"RDPID", Register::Leaf7Ecx, 22, false}, + {"XSAVEOPT", Register::LeafDSub1Eax, 0, false}, + {"XSAVEC", Register::LeafDSub1Eax, 1, false}, + {"SSE4A", Register::Leaf80000001Ecx, 6, false}, + {"PREFETCHW", Register::Leaf80000001Ecx, 8, false}, + {"CLZERO", Register::Leaf80000008Ebx, 0, false}, +}; + +constexpr unsigned kOsxsaveBit = 27; +constexpr std::uint64_t kXcr0SseAvxState = 0x6; + +std::uint32_t Value(const HostCpuRegisters& registers, const Register source) { + switch (source) { + case Register::Leaf1Ecx: + return registers.Leaf1Ecx; + case Register::Leaf1Edx: + return registers.Leaf1Edx; + case Register::Leaf7Ebx: + return registers.Leaf7Ebx; + case Register::Leaf7Ecx: + return registers.Leaf7Ecx; + case Register::LeafDSub1Eax: + return registers.LeafDSub1Eax; + case Register::Leaf80000001Ecx: + return registers.Leaf80000001Ecx; + case Register::Leaf80000008Ebx: + return registers.Leaf80000008Ebx; + } + throw std::logic_error("unknown CPUID register"); +} + +bool AvxStateEnabled(const HostCpuRegisters& registers) { + return (registers.Leaf1Ecx >> kOsxsaveBit & 1) != 0 && (registers.Xcr0 & kXcr0SseAvxState) == kXcr0SseAvxState; +} + +std::string DisplayModel(const std::string& model) { + const auto end = model.find_last_not_of(' '); + return end == std::string::npos ? std::string("unknown model") : model.substr(0, end + 1); +} + +std::string Join(const std::vector& names) { + std::string text; + for (const auto& name : names) { + if (!text.empty()) text += ", "; + text += name; + } + return text; +} + +} + +HostCpuRegisters ReadHostCpuRegisters() { + HostCpuRegisters registers; +#if defined(__x86_64__) || defined(__i386__) + unsigned int eax = 0, ebx = 0, ecx = 0, edx = 0; + const unsigned int basic = __get_cpuid_max(0, nullptr); + if (basic >= 1) { + __cpuid(1, eax, ebx, ecx, edx); + registers.Leaf1Ecx = ecx; + registers.Leaf1Edx = edx; + } + if (basic >= 7) { + __cpuid_count(7, 0, eax, ebx, ecx, edx); + registers.Leaf7Ebx = ebx; + registers.Leaf7Ecx = ecx; + } + if (basic >= 0xD) { + __cpuid_count(0xD, 1, eax, ebx, ecx, edx); + registers.LeafDSub1Eax = eax; + } + const unsigned int extended = __get_cpuid_max(0x80000000, nullptr); + if (extended >= 0x80000001) { + __cpuid(0x80000001, eax, ebx, ecx, edx); + registers.Leaf80000001Ecx = ecx; + } + if (extended >= 0x80000008) { + __cpuid(0x80000008, eax, ebx, ecx, edx); + registers.Leaf80000008Ebx = ebx; + } + if ((registers.Leaf1Ecx >> kOsxsaveBit & 1) != 0) { + std::uint32_t low = 0, high = 0; + __asm__ volatile("xgetbv" : "=a"(low), "=d"(high) : "c"(0)); + registers.Xcr0 = static_cast(high) << 32 | low; + } +#endif + return registers; +} + +std::string HostCpuModel() { +#if defined(__x86_64__) || defined(__i386__) + if (__get_cpuid_max(0x80000000, nullptr) < 0x80000004) return {}; + unsigned int brand[12] = {}; + for (unsigned int leaf = 0; leaf < 3; ++leaf) + __get_cpuid(0x80000002 + leaf, &brand[leaf * 4], &brand[leaf * 4 + 1], &brand[leaf * 4 + 2], &brand[leaf * 4 + 3]); + std::string model(reinterpret_cast(brand), strnlen(reinterpret_cast(brand), sizeof(brand))); + model.erase(0, model.find_first_not_of(' ')); + return model; +#else + return {}; +#endif +} + +std::vector HostCpuFeatures(const HostCpuRegisters& registers) { + std::vector features; + features.reserve(std::size(kFeatures) + 1); + for (const auto& feature : kFeatures) + features.push_back({feature.Name, feature.Required, (Value(registers, feature.Source) >> feature.Bit & 1) != 0}); + features.push_back({"OS AVX state (XCR0)", true, AvxStateEnabled(registers)}); + return features; +} + +std::vector MissingHostCpuFeatures(const HostCpuRegisters& registers) { + std::vector missing; + for (const auto& feature : HostCpuFeatures(registers)) + if (feature.Required && !feature.Present) missing.emplace_back(feature.Name); + return missing; +} + +void CheckHostCpu(const HostCpuRegisters& registers, const std::string& model) { + const auto missing = MissingHostCpuFeatures(registers); + if (missing.empty()) return; + throw std::runtime_error("The host CPU (" + DisplayModel(model) + ") lacks " + Join(missing) + + ". The PS5's Zen 2 CPU has these extensions and titles run their instructions natively, so this CPU cannot run them."); +} + +#if (defined(__x86_64__) || defined(__i386__)) && !defined(__APPLE__) +namespace { + +const bool HostCpuChecked = [] { + const auto registers = ReadHostCpuRegisters(); + const auto model = HostCpuModel(); + std::vector present; + std::vector absent; + for (const auto& feature : HostCpuFeatures(registers)) + (feature.Present ? present : absent).emplace_back(feature.Name); + APS5_LOG_OUT("host CPU %s: has %s; lacks %s", DisplayModel(model).c_str(), Join(present).c_str(), absent.empty() ? "none" : Join(absent).c_str()); + CheckHostCpu(registers, model); + return true; +}(); + +} +#endif diff --git a/core/libs/prx/libkernel/System/src/Login.cpp b/core/libs/prx/libkernel/System/src/Login.cpp new file mode 100644 index 0000000000..80f9eaabc9 --- /dev/null +++ b/core/libs/prx/libkernel/System/src/Login.cpp @@ -0,0 +1,53 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#ifdef _WIN32 +#include +#include +#else +#include +#include +#endif + +namespace { +int ReadLogin(char* output, int size) { +#ifdef _WIN32 + std::array name{}; + DWORD count = static_cast(name.size()); + if (!GetUserNameW(name.data(), &count)) + throw std::system_error(GetLastError(), std::system_category(), "getlogin_r: GetUserNameW"); + std::array utf8{}; + const int length = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, name.data(), + static_cast(count), utf8.data(), static_cast(utf8.size()), nullptr, nullptr); + if (length == 0) + throw std::system_error(GetLastError(), std::system_category(), "getlogin_r: UTF-8 conversion"); + if (length > size) return 34; + std::memcpy(output, utf8.data(), static_cast(length)); + return 0; +#else + std::array storage{}; + passwd entry{}; + passwd* account = nullptr; + const int error = ::getpwuid_r(::geteuid(), &entry, storage.data(), storage.size(), &account); + if (error != 0) + throw std::system_error(error, std::generic_category(), "getlogin_r: getpwuid_r"); + if (account == nullptr || account->pw_name == nullptr || account->pw_name[0] == '\0') + throw std::runtime_error("getlogin_r: host effective user has no account name"); + const std::size_t length = std::strlen(account->pw_name) + 1; + if (length > static_cast(size)) return 34; + std::memcpy(output, account->pw_name, length); + return 0; +#endif +} +} + +extern "C" int APS5_VABI getlogin_r_nid_postfix(char* output, int size) { + if (size <= 0) return 34; + if (output == nullptr) return 14; + const int saved = errno; + const int result = ReadLogin(output, size); + errno = saved; + return result; +} diff --git a/core/libs/prx/libkernel/System/src/Notification.cpp b/core/libs/prx/libkernel/System/src/Notification.cpp index 8303ed619b..9d94d2b291 100644 --- a/core/libs/prx/libkernel/System/src/Notification.cpp +++ b/core/libs/prx/libkernel/System/src/Notification.cpp @@ -1,4 +1,6 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/File.hpp" +#include #include #include #include @@ -8,6 +10,7 @@ #include #ifdef _WIN32 #include +#include #else #include #endif @@ -17,6 +20,15 @@ extern "C" int APS5_VABI sceKernelDebugOutText(int channel, const char* text) { if (!text) return static_cast(0x8002000eu); static std::mutex outputMutex; const std::lock_guard lock(outputMutex); + if (const int host = File::HostStandardError(); host >= 0) { + const auto line = "[debug:" + std::to_string(channel) + "] " + text; +#ifdef _WIN32 + const auto written = _write(host, line.data(), static_cast(line.size())); +#else + const auto written = ::write(host, line.data(), line.size()); +#endif + return written == static_cast(line.size()) ? 0 : static_cast(0x80020005u); + } if (std::fprintf(stderr, "[debug:%d] %s", channel, text) < 0 || std::fflush(stderr) != 0) return static_cast(0x80020005u); return 0; diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 540d281bd8..348464a177 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -3,17 +3,27 @@ #define NOMINMAX #endif #include +#include +#include #else #include #include #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/Shutdown.hpp" +#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" +#include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Pthread/include/Pthread.hpp" +#include #include #include #include @@ -25,7 +35,7 @@ #include #include #include -#include +#include #include #include #include @@ -34,24 +44,55 @@ #include #endif +extern "C" int* APS5_VABI __error_nid_postfix(); +extern "C" int APS5_VABI stat_nid_postfix(const char* path, FileStat* sb); + namespace { constexpr int sceInvalidArgument = static_cast(0x80020016u); +constexpr int errnoNoSuchProcess = 3; +constexpr int errnoNoChild = 10; +constexpr int errnoBadAddress = 14; +constexpr int errnoInvalidArgument = 22; +constexpr int freebsdGroupsMax = 1023; +std::mutex supplementaryGroupsMutex; +std::vector supplementaryGroups{0}; +constexpr unsigned freebsdWaitOptions = 0x8000003Fu; std::atomic gpoBits{0}; constexpr std::array openPsId{'A', 'n', 'y', 'P', 'S', '5', 'O', 'p', 'e', 'n', 'P', 's', 'I', 'd', 0, 1}; +#ifdef _WIN32 +std::string toUtf8(const wchar_t* value) { + const int size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, value, -1, nullptr, 0, nullptr, nullptr); + if (size <= 0) + throw std::system_error(GetLastError(), std::system_category(), "Converting a process argument to UTF-8"); + std::string text(static_cast(size), '\0'); + if (WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, value, -1, text.data(), size, nullptr, nullptr) != size) + throw std::system_error(GetLastError(), std::system_category(), "Converting a process argument to UTF-8"); + text.pop_back(); + return text; +} +#endif + class ProcessArguments { public: ProcessArguments() { #ifdef _WIN32 - std::array path{}; - const auto size = GetModuleFileNameA(nullptr, path.data(), static_cast(path.size())); - if (size == 0) - throw std::system_error(GetLastError(), std::system_category(), "Reading executable path"); - if (size >= path.size()) - throw std::runtime_error("Executable path exceeds the guest argument buffer"); - arguments.emplace_back(path.data(), size); + int count = 0; + const std::unique_ptr values(CommandLineToArgvW(GetCommandLineW(), &count), + [](LPWSTR* parsed) { LocalFree(parsed); }); + if (!values) + 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) @@ -85,16 +126,38 @@ ProcessArguments& getProcessArguments() { return arguments; } -void validateSchedulingPolicy(int policy) { - if (policy != 1 && policy != 3) - throw std::invalid_argument("Unsupported guest scheduling policy"); +bool hasPriorityRange(int policy) { + return policy == 1 || policy == 3; } +constexpr int guestNoProcess = 3; constexpr int guestFault = 14; constexpr int guestInvalid = 22; +constexpr int guestRlimitCpu = 0; +constexpr int guestRlimitFsize = 1; +constexpr int guestPriorityProcess = 0; +constexpr int guestPriorityGroup = 1; +constexpr int guestPriorityUser = 2; +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; +constexpr int guestRlimitSwap = 12; +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() { @@ -127,6 +190,11 @@ struct GuestResourceLimit { std::int64_t rlim_max; }; +namespace { +std::mutex openFileLimitMutex; +std::optional guestOpenFileLimit; +} + struct GuestResourceUsage { KernelTimeval ru_utime; KernelTimeval ru_stime; @@ -152,6 +220,8 @@ extern "C" { // unknown data const char* __progname_nid_postfix = "eboot.bin"; +static char* emptyEnvironment[1]; +char** environ_nid_postfix = emptyEnvironment; int APS5_VABI getargc_nid_postfix(void) { return getProcessArguments().GetCount(); @@ -176,6 +246,54 @@ int APS5_VABI getpid_nid_postfix(void) { return static_cast(pid); } +int APS5_VABI getsid_nid_postfix(int pid) { + const int self = getpid_nid_postfix(); + if (pid == 0 || pid == self) return self; + *__error_nid_postfix() = errnoNoSuchProcess; + return -1; +} + +int APS5_VABI getppid_nid_postfix(void) { +#ifdef _WIN32 + const DWORD self = GetCurrentProcessId(); + const HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (snapshot == INVALID_HANDLE_VALUE) + throw std::system_error(static_cast(GetLastError()), std::system_category(), "getppid: CreateToolhelp32Snapshot"); + PROCESSENTRY32W entry{}; + entry.dwSize = sizeof(entry); + bool found = false; + DWORD pid = 0; + for (BOOL more = Process32FirstW(snapshot, &entry); more && !found; more = Process32NextW(snapshot, &entry)) { + if (entry.th32ProcessID == self) { + pid = entry.th32ParentProcessID; + found = true; + } + } + CloseHandle(snapshot); + if (!found) throw std::runtime_error("getppid: the process is missing from the process snapshot"); +#else + const auto pid = ::getppid(); +#endif + if (static_cast(pid) > static_cast(std::numeric_limits::max())) + throw std::runtime_error("Parent process identifier is outside the guest range"); + return static_cast(pid); +} + +int APS5_VABI sceKernelGetAppInfo(int pid, void* info) { + if (pid != getpid_nid_postfix()) return SCE_KERNEL_ERROR_EPERM; + if (info == nullptr) return 0; + constexpr std::size_t TitleIdOffset = 0x10; + constexpr std::size_t TitleIdBytes = 10; + const auto titleId = GetAppTitleId_nid_postfix(); + char field[TitleIdBytes] = {}; + std::strncpy(field, titleId.value, sizeof(field) - 1); + auto* bytes = static_cast(info); + const std::uint32_t appId = 1; + std::memcpy(bytes, &appId, sizeof(appId)); + std::memcpy(bytes + TitleIdOffset, field, TitleIdBytes); + return 0; +} + int APS5_VABI getuid_nid_postfix(void) { return 0; } @@ -201,7 +319,7 @@ void APS5_VABI exit_nid_postfix(int code) { } [[noreturn]] void APS5_VABI _exit_nid_postfix(int status) { - std::_Exit(status); + LibcTerminate_nid_no_patch(status); } int APS5_VABI system_nid_postfix(const char* command) { @@ -209,6 +327,75 @@ int APS5_VABI system_nid_postfix(const char* command) { return command == nullptr ? 1 : shellNotExecuted; } +int APS5_VABI waitpid_nid_postfix(int pid, int* status, int options) { + (void)pid; + (void)status; + *__error_nid_postfix() = (static_cast(options) & ~freebsdWaitOptions) != 0 ? errnoInvalidArgument : errnoNoChild; + return -1; +} + +int APS5_VABI getrlimit_nid_postfix(int resource, GuestResourceLimit* limit); + +int APS5_VABI setrlimit_nid_postfix(int resource, const GuestResourceLimit* limit) { + if (limit == nullptr) { + *__error_nid_postfix() = guestFault; + return -1; + } + if (resource < 0 || resource >= guestRlimitCount) { + *__error_nid_postfix() = guestInvalid; + return -1; + } + + GuestResourceLimit requested = *limit; + if (requested.rlim_cur < 0) + requested.rlim_cur = guestRlimitInfinity; + if (requested.rlim_max < 0) + requested.rlim_max = guestRlimitInfinity; + if (requested.rlim_cur > requested.rlim_max) + requested.rlim_cur = requested.rlim_max; + + GuestResourceLimit current{}; + getrlimit_nid_postfix(resource, ¤t); + if (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; +} + +int APS5_VABI wait_nid_postfix(int* status) { + return waitpid_nid_postfix(-1, status, 0); +} + +int APS5_VABI execvp_nid_postfix(const char* file, char* const* arguments) { + (void)file; + (void)arguments; + NotImplemented_nid_no_patch("execvp: executable replacement"); + return -1; +} + +int APS5_VABI execv_nid_postfix(const char* path, char* const* arguments) { + (void)path; + (void)arguments; + NotImplemented_nid_no_patch("execv: executable replacement"); + return -1; +} + +int APS5_VABI execve_nid_postfix(const char* path, char* const* arguments, char* const* environment) { + (void)path; + (void)arguments; + (void)environment; + NotImplemented_nid_no_patch("execve: executable replacement"); + return -1; +} + int APS5_VABI sceKernelGetCurrentCpu(void) { #ifdef _WIN32 PROCESSOR_NUMBER processor{}; @@ -229,6 +416,14 @@ int APS5_VABI sceKernelGetCurrentCpu(void) { return GuestCpuFromHost(index, scePthreadSelf()->affinity.load(std::memory_order_relaxed)); } +int APS5_VABI sceKernelGetMainSocId(void) { + return 0; +} + +int APS5_VABI sceKernelGetCpumode(void) { + return 0; +} + std::uint64_t APS5_VABI sceKernelGetGPI(void) { return gpoBits.load(std::memory_order_relaxed); } @@ -250,36 +445,108 @@ void* APS5_VABI sceKernelGetProcParam(void) { int APS5_VABI sceKernelUuidCreate(std::uint32_t* uuid) { if (!uuid) return sceInvalidArgument; - static thread_local std::random_device device; - std::uniform_int_distribution distribution; std::array value; - for (auto& word : value) - word = distribution(device); + HostEntropy_nid_no_patch(value.data(), sizeof(value)); value[1] = (value[1] & 0x0fffffffu) | 0x40000000u; value[2] = (value[2] & 0xffffff3fu) | 0x80u; std::memcpy(uuid, value.data(), sizeof(value)); 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(); } -int APS5_VABI sched_get_priority_max_nid_postfix(int policy) { - validateSchedulingPolicy(policy); - return 256; +void APS5_VABI sync_nid_postfix(void) { + sceKernelSync(); } -int APS5_VABI sched_get_priority_min_nid_postfix(int policy) { - validateSchedulingPolicy(policy); +int APS5_VABI sched_get_priority_max_nid_postfix(int policy) { + if (!hasPriorityRange(policy)) { + *__error_nid_postfix() = guestInvalid; + return -1; + } return 767; } +int APS5_VABI sched_get_priority_min_nid_postfix(int policy) { + if (!hasPriorityRange(policy)) { + *__error_nid_postfix() = guestInvalid; + return -1; + } + 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 != 0 && who != 1) - throw std::invalid_argument("getrusage: unsupported who"); + 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; + } #ifdef _WIN32 FILETIME creation{}; FILETIME exitTime{}; @@ -350,32 +617,115 @@ int APS5_VABI getrlimit_nid_postfix(int resource, GuestResourceLimit* limit) { *__error_nid_postfix() = guestFault; return -1; } - if (resource != guestRlimitData) - throw std::runtime_error("getrlimit: unsupported resource " + std::to_string(resource)); + switch (resource) { + case guestRlimitData: { #ifdef _WIN32 - const std::int64_t memory = hostMemoryLimit(); - limit->rlim_cur = memory; - limit->rlim_max = memory; + const std::int64_t memory = hostMemoryLimit(); + limit->rlim_cur = memory; + limit->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); + 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); #endif + return 0; + } + 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 + } + *limit = *guestOpenFileLimit; + return 0; + } + case guestRlimitStack: + limit->rlim_cur = guestStackLimit; + limit->rlim_max = guestStackLimit; + return 0; + case guestRlimitNproc: + limit->rlim_cur = guestProcessLimit; + limit->rlim_max = guestProcessLimit; + return 0; + 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; + default: + throw std::runtime_error("getrlimit: unsupported resource " + std::to_string(resource)); + } +} + +int APS5_VABI getpriority_nid_postfix(int which, int who) { + if (which != guestPriorityProcess && which != guestPriorityGroup && which != guestPriorityUser) { + *__error_nid_postfix() = guestInvalid; + return -1; + } + const bool found = which == guestPriorityUser ? (who == 0 ? getuid_nid_postfix() : who) == getuid_nid_postfix() + : who == 0 || who == getpid_nid_postfix(); + if (!found) { + *__error_nid_postfix() = guestNoProcess; + return -1; + } + return guestNice.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) { + 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) { return 0; } -int APS5_VABI sceKernelGetOperationMode(int* mode, int* submode) { +void APS5_VABI sceKernelGetOperationMode(int* mode, int* submode) { if (!mode || !submode) throw std::invalid_argument("sceKernelGetOperationMode: null output"); - *mode = 0; + *mode = 2; *submode = 0; - return 0; } int APS5_VABI seteuid_nid_postfix(std::uint32_t euid) { @@ -390,4 +740,70 @@ int APS5_VABI setegid_nid_postfix(std::uint32_t egid) { return 0; } +int APS5_VABI setuid_nid_postfix(std::uint32_t uid) { + if (uid != 0) + throw std::runtime_error("setuid: unsupported user id " + std::to_string(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) + throw std::runtime_error("setreuid: unsupported real user id " + std::to_string(ruid)); + if (euid != 0 && euid != unchanged) + throw std::runtime_error("setreuid: unsupported effective user id " + std::to_string(euid)); + return 0; +} + +int APS5_VABI setregid_nid_postfix(std::uint32_t rgid, std::uint32_t egid) { + constexpr std::uint32_t unchanged = 0xffffffffu; + if (rgid != 0 && rgid != unchanged) + throw std::runtime_error("setregid: unsupported real group id " + std::to_string(rgid)); + if (egid != 0 && egid != unchanged) + throw std::runtime_error("setregid: unsupported effective group id " + std::to_string(egid)); + return 0; +} + +int APS5_VABI setgroups_nid_postfix(int count, const std::uint32_t* groups) { + if (count < 0 || count > freebsdGroupsMax) { + *__error_nid_postfix() = errnoInvalidArgument; + return -1; + } + if (count > 0 && groups == nullptr) { + *__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::lock_guard lock(supplementaryGroupsMutex); + if (count == 0) { + supplementaryGroups.resize(1); + return 0; + } + supplementaryGroups.assign(groups, groups + count); + return 0; +} + +int APS5_VABI getgroups_nid_postfix(int capacity, std::uint32_t* groups) { + std::lock_guard lock(supplementaryGroupsMutex); + const int count = static_cast(supplementaryGroups.size()); + if (capacity == 0) return count; + if (capacity < count) { + *__error_nid_postfix() = errnoInvalidArgument; + return -1; + } + if (groups == nullptr) { + *__error_nid_postfix() = errnoBadAddress; + return -1; + } + std::copy(supplementaryGroups.begin(), supplementaryGroups.end(), groups); + return count; +} + } diff --git a/core/libs/prx/libkernel/System/src/Rtld.cpp b/core/libs/prx/libkernel/System/src/Rtld.cpp index bcf6eff7f7..90472da544 100644 --- a/core/libs/prx/libkernel/System/src/Rtld.cpp +++ b/core/libs/prx/libkernel/System/src/Rtld.cpp @@ -19,6 +19,10 @@ #include #endif +#ifdef _WIN32 +extern "C" void ForgetUnloadedUnwindImages_nid_no_patch(); +#endif + namespace { std::mutex rtldHeapMutex; @@ -69,7 +73,9 @@ int releaseThreadAtexitImage(const void* address, const char* caller) { } if (!handle) return 0; #ifdef _WIN32 - if (!FreeLibrary(static_cast(handle))) + const bool released = FreeLibrary(static_cast(handle)); + ForgetUnloadedUnwindImages_nid_no_patch(); + if (!released) #else if (dlclose(handle) != 0) #endif diff --git a/core/libs/prx/libkernel/System/src/Sanitizer.cpp b/core/libs/prx/libkernel/System/src/Sanitizer.cpp index 15f2d3f5fb..0586eef7d7 100644 --- a/core/libs/prx/libkernel/System/src/Sanitizer.cpp +++ b/core/libs/prx/libkernel/System/src/Sanitizer.cpp @@ -6,6 +6,10 @@ int APS5_VABI sceKernelIsAddressSanitizerEnabled(void) { return 0; } +int APS5_VABI sceKernelIsThreadSanitizerEnabled(void) { + return 0; +} + MallocReplace* APS5_VABI sceKernelGetSanitizerMallocReplaceExternal(void) { static MallocReplace replacement{}; return &replacement; diff --git a/core/libs/prx/libkernel/System/src/SignalSets.cpp b/core/libs/prx/libkernel/System/src/SignalSets.cpp index f2831d86c8..688ad49ce8 100644 --- a/core/libs/prx/libkernel/System/src/SignalSets.cpp +++ b/core/libs/prx/libkernel/System/src/SignalSets.cpp @@ -1,3 +1,4 @@ +#include "prx/libc/include/general/AsmMacros.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -60,3 +61,78 @@ int APS5_VABI sigismember_nid_postfix(const GuestSignalSet* set, int signal) { return (set->bits[WordIndex(signal)] & Bit(signal)) != 0 ? 1 : 0; } } +#define APS5_SIGNAL_ASM_FUNCTION(name) APS5_ASM_FUNCTION(name) +#if defined(_WIN32) || defined(__APPLE__) +#define APS5_SIGNAL_ASM_CALL(name) " call " APS5_ASM_SYMBOL(name) "\n" +#else +#define APS5_SIGNAL_ASM_CALL(name) " call " name "@PLT\n" +#endif + +asm(".text\n" + APS5_SIGNAL_ASM_FUNCTION("sigsetjmp_nid_postfix") + " movl %esi, 88(%rdi)\n" + " testl %esi, %esi\n" + " jz 2f\n" + " pushq %rdi\n" + " movq %rdi, %rcx\n" + " movl $1, %edi\n" + " xorl %esi, %esi\n" + " leaq 72(%rcx), %rdx\n" + APS5_SIGNAL_ASM_CALL("_sigprocmask_nid_postfix") + " popq %rdi\n" + "2:\n" + " movq %rdi, %rcx\n" + " movq 0(%rsp), %rdx\n" + " movq %rdx, 0(%rcx)\n" + " movq %rbx, 8(%rcx)\n" + " movq %rsp, 16(%rcx)\n" + " movq %rbp, 24(%rcx)\n" + " movq %r12, 32(%rcx)\n" + " movq %r13, 40(%rcx)\n" + " movq %r14, 48(%rcx)\n" + " movq %r15, 56(%rcx)\n" + " fnstcw 64(%rcx)\n" + " xorl %eax, %eax\n" + " ret\n" + APS5_SIGNAL_ASM_FUNCTION("siglongjmp_nid_postfix") + " cmpl $0, 88(%rdi)\n" + " jz 2f\n" + " movq %rdi, %rdx\n" + " pushq %rdi\n" + " pushq %rsi\n" + " movl $3, %edi\n" + " leaq 72(%rdx), %rsi\n" + " xorl %edx, %edx\n" + " subq $8, %rsp\n" + APS5_SIGNAL_ASM_CALL("_sigprocmask_nid_postfix") + " addq $8, %rsp\n" + " popq %rsi\n" + " popq %rdi\n" + "2:\n" +#ifdef _WIN32 + " pushq %rdi\n" + " pushq %rsi\n" + " movq 16(%rdi), %rdi\n" + " subq $8, %rsp\n" + " call GuestStackSwitch_nid_no_patch\n" + " addq $8, %rsp\n" + " popq %rsi\n" + " popq %rdi\n" +#endif + " movq %rdi, %rdx\n" + " movl %esi, %eax\n" + " movq 0(%rdx), %rcx\n" + " movq 8(%rdx), %rbx\n" + " movq 16(%rdx), %rsp\n" + " movq 24(%rdx), %rbp\n" + " movq 32(%rdx), %r12\n" + " movq 40(%rdx), %r13\n" + " movq 48(%rdx), %r14\n" + " movq 56(%rdx), %r15\n" + " fldcw 64(%rdx)\n" + " testl %eax, %eax\n" + " jnz 1f\n" + " incl %eax\n" + "1:\n" + " movq %rcx, 0(%rsp)\n" + " ret\n"); diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index e6a955b740..231d3d2643 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -1,13 +1,25 @@ +#include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/Pthread/Posix/Common.hpp" +#include "prx/libkernel/Pthread/include/Pthread.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include +#include +#include #include #include #include +#include +#include #include +#include #include +#include +#include extern "C" int* APS5_VABI __error_nid_postfix(); +extern "C" int APS5_VABI getpid_nid_postfix(void); +extern "C" Pthread APS5_VABI scePthreadSelf(void); struct GuestSignalSet { std::uint32_t bits[4]; @@ -25,14 +37,106 @@ using GuestHandler = void (APS5_VABI *)(int); constexpr int MaxSignal = 128; constexpr int GuestSigkill = 9; constexpr int GuestSigstop = 17; +constexpr int GuestSigalrm = 14; +constexpr int GuestSigcont = 19; constexpr int SaRestart = 0x2; constexpr int SaSiginfo = 0x40; std::atomic handlers[MaxSignal + 1]{}; static_assert(std::atomic::is_always_lock_free); std::atomic blockedMask{0}; +std::atomic blockedMaskExtra[3]{}; static_assert(std::atomic::is_always_lock_free); std::mutex registration; GuestSigaction dispositions[MaxSignal + 1]{}; +constexpr int GuestSigblock = 1; +constexpr int GuestSigunblock = 2; +constexpr int GuestSigsetmask = 3; +constexpr int GuestEintr = 4; +constexpr int GuestEagain = 35; +constexpr int GuestSiuser = 0; +struct GuestSiginfo { + std::int32_t signo; + std::int32_t error; + std::int32_t code; + std::int32_t pid; + std::uint32_t uid; + std::int32_t status; + std::uintptr_t address; + std::int32_t value; + std::int32_t valueTail; + std::int64_t band; + std::int32_t spare[7]; + std::int32_t tail; +}; +static_assert(sizeof(GuestSiginfo) == 80 && offsetof(GuestSiginfo, code) == 8 && offsetof(GuestSiginfo, pid) == 12 && + offsetof(GuestSiginfo, uid) == 16 && offsetof(GuestSiginfo, address) == 24 && offsetof(GuestSiginfo, value) == 32 && + offsetof(GuestSiginfo, band) == 40); +struct GuestTimespec { + std::int64_t seconds; + std::int64_t nanoseconds; +}; +static_assert(sizeof(GuestTimespec) == 16 && offsetof(GuestTimespec, nanoseconds) == 8); +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) { + 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; + std::condition_variable changed; + std::atomic words[4]{}; +}; + +PendingSignals& Pending() { + static auto* state = new PendingSignals(); + return *state; +} + +void MarkPending(int guest) { + auto& pending = Pending(); + { + // under the same lock the waits check the set with: without it a signal raised + // between the check and the wait is lost and the wait never returns + std::lock_guard lock(pending.lock); + pending.words[SignalWord(guest)].fetch_or(SignalBit(guest), std::memory_order_relaxed); + } + pending.changed.notify_all(); +} + +bool TakePending(int guest) { + auto& pending = Pending(); + auto& word = pending.words[SignalWord(guest)]; + const std::uint32_t bit = SignalBit(guest); + if ((word.load(std::memory_order_relaxed) & bit) == 0) return false; + return (word.fetch_and(~bit, std::memory_order_relaxed) & bit) != 0; +} + +bool PendingInSet(const GuestSignalSet& set, int* guest) { + for (int candidate = 1; candidate <= MaxSignal; ++candidate) { + if (!SignalInSet(set, candidate) || !TakePending(candidate)) continue; + *guest = candidate; + return true; + } + return false; +} + +bool PendingInSetPresent(const GuestSignalSet& set) { + for (int candidate = 1; candidate <= MaxSignal; ++candidate) { + if (!SignalInSet(set, candidate)) continue; + if ((Pending().words[SignalWord(candidate)].load(std::memory_order_relaxed) & SignalBit(candidate)) != 0) return true; + } + return false; +} + int NativeSignal(int guest) { switch (guest) { case 2: return SIGINT; @@ -53,7 +157,10 @@ void Dispatch(int native) { // Preserve the guest's persistent registration across CRT delivery. std::signal(native, Dispatch); #endif - if ((blockedMask.load() & (1u << guest)) != 0) return; + if (SignalBlocked(guest)) { + MarkPending(guest); + return; + } const auto callback = handlers[guest].load(); if (reinterpret_cast(callback) > 1) callback(guest); } @@ -73,6 +180,290 @@ bool Install(int guest, const GuestSigaction& action) { } } +namespace { + +GuestSigaction Disposition(int guest) { + std::lock_guard lock(registration); + return dispositions[guest]; +} + +bool CaughtByGuest(int guest) { + return Disposition(guest).handler > 1; +} + +bool 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) || TerminatesWithoutHandler(guest); +} + +bool TakePendingUnblocked(int* guest) { + for (int candidate = 1; candidate <= MaxSignal; ++candidate) { + if (SignalBlocked(candidate) || !Deliverable(candidate)) continue; + if (!TakePending(candidate)) continue; + *guest = candidate; + return true; + } + return false; +} + +bool DeliverPendingSignal(int guest) { + const GuestSigaction action = Disposition(guest); + if (action.handler == 1) return false; + if (action.handler > 1 && !Delivered(action)) throw std::runtime_error("signal: SA_SIGINFO handlers are not delivered"); + 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"); + return action.handler > 1; + } + if (action.handler > 1) { + reinterpret_cast(action.handler)(guest); + return true; + } + return false; +} + +int WaitForPendingInSet(const GuestSignalSet& set, bool waitForever, const GuestTimespec* timeout, int* guest) { + auto& pending = Pending(); + std::unique_lock lock(pending.lock); + std::optional deadline; + for (;;) { + if (PendingInSet(set, guest)) return 0; + if (waitForever) { + pending.changed.wait(lock); + continue; + } + 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; + return PendingInSet(set, guest) ? 0 : GuestEagain; + } +} + +void FillSiginfo(GuestSiginfo* info, int guest) { + *info = {}; + info->signo = guest; + info->code = GuestSiuser; + 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 std::int64_t MaxItimerSeconds = 0x7fffffff / 2; +struct AlarmState { + std::mutex lock; + std::condition_variable changed; + std::optional deadline; + std::chrono::microseconds interval{0}; + bool threadStarted = false; +}; + +AlarmState& Alarms() { + static auto* state = new AlarmState(); + return *state; +} + +std::string DescribeInterval(std::chrono::microseconds interval) { + char text[64]; + std::snprintf(text, sizeof(text), "%lld.%06lld s", static_cast(interval.count() / 1000000), + static_cast(interval.count() % 1000000)); + return text; +} + +void ExpireAlarm(std::chrono::microseconds interval) { + std::uintptr_t handler = 0; + { + std::lock_guard lock(registration); + handler = dispositions[GuestSigalrm].handler; + } + if (handler == 1) return; + if (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"); +} + +void RunAlarms() { + auto& alarms = Alarms(); + std::unique_lock lock(alarms.lock); + for (;;) { + if (!alarms.deadline) { + alarms.changed.wait(lock); + continue; + } + const auto deadline = *alarms.deadline; + if (alarms.changed.wait_until(lock, deadline) != std::cv_status::timeout || alarms.deadline != deadline) continue; + const auto interval = alarms.interval; + if (interval.count() == 0) alarms.deadline.reset(); + else { + const auto now = std::chrono::steady_clock::now(); + const auto periods = now < deadline ? 1 : (now - deadline) / interval + 1; + alarms.deadline = deadline + periods * interval; + } + lock.unlock(); + ExpireAlarm(interval); + lock.lock(); + } +} + +std::chrono::microseconds RemainingAlarm(const AlarmState& alarms, std::chrono::steady_clock::time_point now) { + if (!alarms.deadline || *alarms.deadline <= now) return std::chrono::microseconds(0); + return std::chrono::duration_cast(*alarms.deadline - now); +} + +void NotifyAlarms(AlarmState& alarms) { + if (!alarms.threadStarted) { + std::thread(RunAlarms).detach(); + alarms.threadStarted = true; + } + alarms.changed.notify_all(); +} +} + +extern "C" unsigned int APS5_VABI GuestAlarm_nid_no_patch(unsigned int seconds) { + 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(); + const auto micros = RemainingAlarm(alarms, now).count(); + const auto remaining = static_cast((micros + 999999) / 1000000); + alarms.interval = std::chrono::microseconds(0); + if (seconds == 0) alarms.deadline.reset(); + else alarms.deadline = now + std::chrono::seconds(seconds); + NotifyAlarms(alarms); + return remaining; +} + +struct GuestItimerval { + KernelTimeval interval; + KernelTimeval value; +}; +static_assert(sizeof(GuestItimerval) == 32 && offsetof(GuestItimerval, value) == 16); + +extern "C" int APS5_VABI getitimer_nid_postfix(int which, GuestItimerval* output) { + if (which < 0 || which > 2) { + *__error_nid_postfix() = 22; + return -1; + } + if (output == nullptr) { + *__error_nid_postfix() = 14; + return -1; + } + if (which != 0) { + *output = {}; + return 0; + } + GuestItimerval result{}; + auto& alarms = Alarms(); + { + std::lock_guard lock(alarms.lock); + const auto now = std::chrono::steady_clock::now(); + if (alarms.deadline && *alarms.deadline > now) { + const auto micros = std::chrono::duration_cast(*alarms.deadline - now).count(); + result.value.tv_sec = micros / 1000000; + result.value.tv_usec = micros % 1000000; + } + const auto interval = alarms.interval.count(); + result.interval.tv_sec = interval / 1000000; + result.interval.tv_usec = interval % 1000000; + } + *output = result; + return 0; +} + +namespace { +constexpr int ItimerReal = 0; +constexpr int ItimerProf = 2; +constexpr std::int64_t MinExactIntervalMicros = 1000; + +bool ValidItimer(const KernelTimeval& time) { + return time.tv_sec >= 0 && time.tv_usec >= 0 && time.tv_usec < 1000000 && time.tv_sec <= MaxItimerSeconds; +} + +bool ItimerSet(const KernelTimeval& time) { + return time.tv_sec != 0 || time.tv_usec != 0; +} + +std::chrono::microseconds ItimerDuration(const KernelTimeval& time) { + return std::chrono::seconds(time.tv_sec) + std::chrono::microseconds(time.tv_usec); +} + +KernelTimeval ItimerTimeval(std::chrono::microseconds duration) { + return {duration.count() / 1000000, duration.count() % 1000000}; +} +} + +extern "C" int APS5_VABI setitimer_nid_postfix(int which, const GuestItimerval* value, GuestItimerval* previous) { + if (which < ItimerReal || which > ItimerProf) { *__error_nid_postfix() = 22; return -1; } + if (value == nullptr && previous == nullptr) { *__error_nid_postfix() = 14; return -1; } + GuestItimerval requested{}; + if (value != nullptr) { + requested = *value; + if (!ValidItimer(requested.value)) { *__error_nid_postfix() = 22; return -1; } + if (!ItimerSet(requested.value)) requested.interval = {}; + else if (!ValidItimer(requested.interval)) { *__error_nid_postfix() = 22; return -1; } + } + if (which != ItimerReal) { + if (ItimerSet(requested.value)) + throw std::runtime_error(std::string("setitimer: ") + (which == ItimerProf ? "ITIMER_PROF" : "ITIMER_VIRTUAL") + " needs process CPU time accounting, which is not implemented"); + if (previous != nullptr) *previous = {}; + return 0; + } + if (requested.interval.tv_sec == 0 && requested.interval.tv_usec != 0 && requested.interval.tv_usec < MinExactIntervalMicros) + throw std::runtime_error("setitimer: it_interval " + DescribeInterval(ItimerDuration(requested.interval)) + " is below 1 ms, where the kernel rounds it up to a clock tick that is not known"); + GuestItimerval old{}; + { + auto& alarms = Alarms(); + std::lock_guard lock(alarms.lock); + const auto now = std::chrono::steady_clock::now(); + old.interval = ItimerTimeval(alarms.interval); + old.value = ItimerTimeval(RemainingAlarm(alarms, now)); + if (value != nullptr) { + alarms.interval = ItimerDuration(requested.interval); + if (ItimerSet(requested.value)) alarms.deadline = now + ItimerDuration(requested.value); + else alarms.deadline.reset(); + NotifyAlarms(alarms); + } + } + if (previous != nullptr) *previous = old; + return 0; +} + struct GuestStack { void* sp; std::size_t size; @@ -88,8 +479,7 @@ thread_local GuestStack alternateStack{nullptr, 0, SsDisable}; extern "C" { GuestHandler APS5_VABI signal_nid_postfix(int guest, GuestHandler handler) { const auto invalid = reinterpret_cast(static_cast(-1)); - const int native = NativeSignal(guest); - if (!native || handler == invalid) { *__error_nid_postfix() = 22; return invalid; } + 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, {}}; if (!Install(guest, action)) { *__error_nid_postfix() = 22; return invalid; } @@ -110,15 +500,44 @@ int APS5_VABI sigaction_nid_postfix(int guest, const GuestSigaction* action, Gue return 0; } int APS5_VABI raise_nid_postfix(int guest) { - const int native = NativeSignal(guest); - if (!native) { *__error_nid_postfix() = 22; return -1; } - { - std::lock_guard lock(registration); - if (dispositions[guest].handler > 1 && !Delivered(dispositions[guest])) throw std::runtime_error("raise: SA_SIGINFO handlers are not delivered"); + if (guest == 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)) { + MarkPending(guest); + return 0; } - const int result = std::raise(native); - if (result) *__error_nid_postfix() = 22; - return result ? -1 : 0; + if (action.handler == 0 && IgnoredByDefault(guest)) return 0; + const int native = NativeSignal(guest); + if (native != 0) { + const int result = std::raise(native); + if (result) *__error_nid_postfix() = 22; + return result ? -1 : 0; + } + if (!CaughtByGuest(guest)) { *__error_nid_postfix() = 22; return -1; } + DeliverPendingSignal(guest); + return 0; +} +int APS5_VABI kill_nid_postfix(int pid, int guest) { + if (guest < 0 || guest > MaxSignal) { *__error_nid_postfix() = 22; return -1; } + const int self = getpid_nid_postfix(); + if (pid != self && pid != 0 && pid != -self) { *__error_nid_postfix() = 3; return -1; } + return guest == 0 ? 0 : raise_nid_postfix(guest); +} +int APS5_VABI killpg_nid_postfix(int pgrp, int guest) { + if (pgrp < 0) { *__error_nid_postfix() = 22; return -1; } + return kill_nid_postfix(-pgrp, guest); +} +int APS5_VABI pthread_kill_nid_postfix(Pthread thread, int guest) { + if (guest < 0 || guest > MaxSignal) return PosixThread::GUEST_EINVAL; + if (thread == nullptr) return PosixThread::GUEST_ESRCH; + if (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; } int APS5_VABI sigaltstack_nid_postfix(const GuestStack* stack, GuestStack* previous) { GuestStack replacement = alternateStack; @@ -133,20 +552,34 @@ int APS5_VABI sigaltstack_nid_postfix(const GuestStack* stack, GuestStack* previ return 0; } int APS5_VABI _sigprocmask_nid_postfix(int how, const GuestSignalSet* set, GuestSignalSet* previousSet) { - std::lock_guard lock(registration); - if (previousSet != nullptr) { - previousSet->bits[0] = blockedMask.load(); - previousSet->bits[1] = 0; - previousSet->bits[2] = 0; - previousSet->bits[3] = 0; - } - if (set != nullptr) { - switch (how) { - case 1: blockedMask.fetch_or(set->bits[0]); break; - case 2: blockedMask.fetch_and(~set->bits[0]); break; - case 3: blockedMask.store(set->bits[0]); break; - default: throw std::invalid_argument("_sigprocmask: invalid how"); + 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(); + 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]); + for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].fetch_or(set->bits[word]); + break; + case 2: + blockedMask.fetch_and(~set->bits[0]); + for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].fetch_and(~set->bits[word]); + break; + case 3: + blockedMask.store(set->bits[0]); + for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].store(set->bits[word]); + break; + } + } + } + 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); } return 0; } @@ -155,6 +588,71 @@ int APS5_VABI sigprocmask_nid_postfix(int how, const void* set, void* previousSe return _sigprocmask_nid_postfix(how, static_cast(set), static_cast(previousSet)); } + +int APS5_VABI pthread_sigmask_nid_postfix(int how, const GuestSignalSet* set, GuestSignalSet* previousSet) { + if (set != nullptr && (how < 1 || how > 3)) return 22; + return _sigprocmask_nid_postfix(how, set, previousSet); +} + +int APS5_VABI sigtimedwait_nid_postfix(const GuestSignalSet* set, void* info, const GuestTimespec* timeout) { + if (set == nullptr || !SetHasSignal(*set)) { *__error_nid_postfix() = 22; return -1; } + if (timeout != nullptr && (timeout->nanoseconds < 0 || timeout->nanoseconds >= 1000000000) && !PendingInSetPresent(*set)) { + *__error_nid_postfix() = 22; + return -1; + } + int guest = 0; + const int result = WaitForPendingInSet(*set, timeout == nullptr, timeout, &guest); + if (result != 0) { *__error_nid_postfix() = result; return -1; } + if (info != nullptr) FillSiginfo(static_cast(info), guest); + return guest; +} + +int APS5_VABI sigwaitinfo_nid_postfix(const GuestSignalSet* set, void* info) { + if (set == nullptr || !SetHasSignal(*set)) { *__error_nid_postfix() = 22; return -1; } + int guest = 0; + if (WaitForPendingInSet(*set, true, nullptr, &guest) != 0) { *__error_nid_postfix() = GuestEagain; return -1; } + if (info != nullptr) FillSiginfo(static_cast(info), guest); + return guest; +} + +int APS5_VABI sigwait_nid_postfix(const GuestSignalSet* set, int* received) { + if (set == nullptr || received == nullptr || !SetHasSignal(*set)) return 22; + int guest = 0; + const int result = WaitForPendingInSet(*set, true, nullptr, &guest); + if (result != 0) return result; + *received = guest; + return 0; +} + +int APS5_VABI pause_nid_postfix(void) { + 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]); + 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) { + if (set == nullptr) { *__error_nid_postfix() = 14; return -1; } + auto& pending = Pending(); + std::lock_guard lock(pending.lock); + set->bits[0] = pending.words[0].load(std::memory_order_relaxed) & blockedMask.load(); + for (int word = 1; word < 4; ++word) + set->bits[word] = pending.words[word].load(std::memory_order_relaxed) & blockedMaskExtra[word - 1].load(); + return 0; +} } extern "C" { diff --git a/core/libs/prx/libkernel/System/src/SoftwareVersion.cpp b/core/libs/prx/libkernel/System/src/SoftwareVersion.cpp new file mode 100644 index 0000000000..6f834ce2ff --- /dev/null +++ b/core/libs/prx/libkernel/System/src/SoftwareVersion.cpp @@ -0,0 +1,43 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include +#include +#include + +struct SceKernelProsperoSystemSwVersion { + std::uint64_t size; + char versionString[0x1C]; + std::uint32_t version; +}; + +static_assert(offsetof(SceKernelProsperoSystemSwVersion, size) == 0x00); +static_assert(offsetof(SceKernelProsperoSystemSwVersion, versionString) == 0x08); +static_assert(offsetof(SceKernelProsperoSystemSwVersion, version) == 0x24); +static_assert(sizeof(SceKernelProsperoSystemSwVersion) == 0x28); + +namespace { + +constexpr std::uint32_t SystemSoftwareVersion = 0x01000000; +constexpr char SystemSoftwareVersionString[] = "01.000.000"; +constexpr std::uint32_t SdkPatchMask = 0xfff; + +} + +extern "C" { + +int APS5_VABI sceKernelGetProsperoSystemSwVersion_nid_postfix(SceKernelProsperoSystemSwVersion* version) { + if (version == nullptr) return SCE_KERNEL_ERROR_EFAULT; + if (version->size != sizeof(SceKernelProsperoSystemSwVersion)) return SCE_KERNEL_ERROR_EINVAL; + std::memset(version->versionString, 0, sizeof(version->versionString)); + std::memcpy(version->versionString, SystemSoftwareVersionString, sizeof(SystemSoftwareVersionString)); + version->version = SystemSoftwareVersion; + return 0; +} + +int APS5_VABI sceKernelGetAllowedSdkVersionOnSystem_nid_postfix(std::uint32_t* version) { + if (version == nullptr) return SCE_KERNEL_ERROR_EINVAL; + *version = SystemSoftwareVersion | SdkPatchMask; + return 0; +} + +} diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index 3b245aa976..da87fe7bb2 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -1,11 +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 @@ -14,6 +21,20 @@ #endif 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 { @@ -32,6 +53,130 @@ 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 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; }); +} + } extern "C" { @@ -40,9 +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 @@ -82,19 +243,133 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void *__error_nid_postfix() = 14; return -1; } - if (nameLength != 2 || name[0] != 6 || name[1] != 3) { - std::string mib; - for (std::uint32_t i = 0; i < nameLength; ++i) mib += (i == 0 ? "" : ".") + std::to_string(name[i]); - throw std::runtime_error(std::string(__func__) + ": unsupported MIB " + mib); - } - if (newValue != nullptr) { - *__error_nid_postfix() = 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; } - const std::int64_t processors = sysconf_nid_postfix(58); - if (processors <= 0) return -1; - const int value = static_cast(processors); - return CopyOut(&value, sizeof(value), oldValue, oldLength); + 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; + return -1; + } + const std::int64_t processors = sysconf_nid_postfix(58); + if (processors <= 0) return -1; + const int value = static_cast(processors); + return CopyOut(&value, sizeof(value), oldValue, oldLength); + } + if (nameLength == 2 && name[0] == 6 && name[1] == 2) { + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + static const std::string model = HostCpuModel(); + if (model.empty()) throw std::runtime_error(std::string(__func__) + ": the host CPU has no CPUID brand string for hw.model"); + return CopyOut(model.c_str(), model.size() + 1, oldValue, oldLength); + } + if (nameLength == 2 && name[0] == 6 && (name[1] == 5 || name[1] == 12)) { + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + const std::int64_t pages = sysconf_nid_postfix(121); + if (pages <= 0) return -1; + 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; + return -1; + } + if (nameLength != 4) { + *__error_nid_postfix() = 22; + return -1; + } + const int pid = name[3]; + if (pid != -1 && pid != getpid_nid_postfix()) { + *__error_nid_postfix() = 3; + return -1; + } + 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; + return -1; + } + if (oldLength == nullptr) return 0; + const std::size_t count = std::min(*oldLength, static_cast(256)); + if (oldValue != nullptr) { + static std::random_device device; + auto* bytes = static_cast(oldValue); + for (std::size_t i = 0; i < count; ++i) bytes[i] = static_cast(device()); + } + *oldLength = count; + return 0; + } + 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; + } + std::string mib; + for (std::uint32_t i = 0; i < nameLength; ++i) mib += (i == 0 ? "" : ".") + std::to_string(name[i]); + throw std::runtime_error(std::string(__func__) + ": unsupported MIB " + mib); } int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::size_t* oldLength, const void* newValue, std::size_t newLength) { @@ -102,8 +377,143 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si *__error_nid_postfix() = 14; return -1; } - if (std::strcmp(name, "hw.ncpu") != 0) throw std::runtime_error(std::string(__func__) + ": unsupported name " + name); - static constexpr int mib[] = {6, 3}; - return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); + if (std::strcmp(name, "machdep.cacheline_size") == 0) { + *__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); + } + if (std::strcmp(name, "kern.arandom") == 0) { + static constexpr int mib[] = {1, 37}; + return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); + } + if (std::strcmp(name, "kern.rng_pseudo") == 0) { + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + static std::random_device device; + 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"); + static constexpr int overcommit = 0; + return CopyOut(&overcommit, sizeof(overcommit), oldValue, oldLength); + } + if (std::strcmp(name, "kern.sdk_version") == 0) { + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + static constexpr std::uint32_t sdkVersion = 0x01000000; + return CopyOut(&sdkVersion, sizeof(sdkVersion), oldValue, oldLength); + } + if (std::strcmp(name, "machdep.tsc_freq") == 0) { + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + const std::uint64_t frequency = sceKernelGetTscFrequency(); + return CopyOut(&frequency, sizeof(frequency), oldValue, oldLength); + } + if (std::strcmp(name, "hw.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); + } + if (std::strcmp(name, "hw.model") == 0) { + static constexpr int mib[] = {6, 2}; + return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); + } + if (std::strcmp(name, "hw.physmem") == 0) { + static constexpr int mib[] = {6, 5}; + return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); + } + if (std::strcmp(name, "hw.realmem") == 0) { + static constexpr int mib[] = {6, 12}; + return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); + } + throw std::runtime_error(std::string(__func__) + ": unsupported name " + name); +} + +int APS5_VABI sceKernelGetSystemSwVersion_nid_postfix(SceKernelSystemSwVersion* version) { + if (version == nullptr) { + return SCE_KERNEL_ERROR_EFAULT; + } + std::memset(version->versionString, 0, sizeof(version->versionString)); + std::memcpy(version->versionString, "01.000.000", sizeof("01.000.000")); + 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 new file mode 100644 index 0000000000..bd83c4282c --- /dev/null +++ b/core/libs/prx/libkernel/System/src/ThermalSensors.cpp @@ -0,0 +1,280 @@ +#include "prx/libkernel/System/include/ThermalSensors.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include +#include +#endif + +namespace { + +std::optional ReadCelsius(const std::filesystem::path& file) { + std::ifstream stream(file); + long long millidegrees = 0; + if (!(stream >> millidegrees)) return std::nullopt; + if (millidegrees < -273150 || millidegrees > 2000000) return std::nullopt; + return static_cast(millidegrees / 1000); +} + +std::string ReadName(const std::filesystem::path& file) { + std::ifstream stream(file); + std::string name; + std::getline(stream, name); + return name; +} + +bool Contains(const std::string& text, const char* part) { + return text.find(part) != std::string::npos; +} + +std::string Lowercase(std::string text) { + for (char& character : text) character = static_cast(std::tolower(static_cast(character))); + return text; +} + +bool ContainsAny(const std::string& text, std::initializer_list parts) { + for (const char* part : parts) + if (text.find(part) != std::string::npos) return true; + return false; +} + +bool IsPowerOrNetwork(const std::string& name) { + if (name.rfind("bat", 0) == 0) return true; + if (name == "ac" || name.rfind("acad", 0) == 0 || name.rfind("ucsi", 0) == 0) return true; + for (const char* part : {"iwl", "mt79", "ath", "rtw", "wlan", "wifi", "wireless"}) + if (name.find(part) != std::string::npos) return true; + return false; +} + +std::filesystem::directory_iterator OpenDirectory(const std::filesystem::path& path) { + std::error_code error; + std::filesystem::directory_iterator entries(path, error); + if (error) return {}; + return entries; +} + +std::vector SortedEntries(const std::filesystem::path& directory) { + std::vector entries; + for (const auto& entry : OpenDirectory(directory)) entries.push_back(entry.path()); + std::sort(entries.begin(), entries.end()); + return entries; +} + +void Plausible(int& value) { + if (value < 0 || value > 130) value = -1; +} + +} // namespace + +extern "C" void ReadThermalSensors_nid_no_patch(const char* root, int celsius[3]) { + celsius[0] = celsius[1] = celsius[2] = -1; + if (root == nullptr) return; + const std::filesystem::path base(root); + + int gpu = -1; + int board = -1; + for (const auto& path : SortedEntries(base / "hwmon")) { + const auto value = ReadCelsius(path / "temp1_input"); + if (!value) continue; + const std::string name = Lowercase(ReadName(path / "name")); + if (IsPowerOrNetwork(name)) continue; + if (ContainsAny(name, {"coretemp", "k10temp", "zenpower", "cpu"})) { + if (celsius[0] < 0) celsius[0] = *value; + } else if (ContainsAny(name, {"amdgpu", "i915", "radeon", "nvidia", "gpu"}) || name == "xe") { + if (gpu < 0) gpu = *value; + } else if (Contains(name, "nvme")) { + if (celsius[2] < 0) celsius[2] = *value; + } else if (board < 0) { + board = *value; + } + } + for (const auto& path : SortedEntries(base / "thermal")) { + const auto value = ReadCelsius(path / "temp"); + if (!value) continue; + const std::string type = Lowercase(ReadName(path / "type")); + if (ContainsAny(type, {"x86_pkg_temp", "cpu-thermal"})) { + if (celsius[0] < 0) celsius[0] = *value; + } else if (Contains(type, "gpu")) { + if (gpu < 0) gpu = *value; + } else if (board < 0) { + board = *value; + } + } + if (celsius[0] < 0) celsius[0] = gpu; + if (celsius[1] < 0) celsius[1] = board; + Plausible(celsius[0]); + Plausible(celsius[1]); + Plausible(celsius[2]); +} + +namespace { + +#ifdef _WIN32 + +void ReadAcpiThermalZones(int celsius[2]) { + celsius[0] = celsius[1] = -1; + const HRESULT started = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + const bool uninitialize = SUCCEEDED(started); + if (FAILED(started) && started != RPC_E_CHANGED_MODE) return; + IWbemLocator* locator = nullptr; + IWbemServices* services = nullptr; + if (SUCCEEDED(CoCreateInstance(CLSID_WbemLocator, nullptr, CLSCTX_INPROC_SERVER, IID_IWbemLocator, reinterpret_cast(&locator))) && locator != nullptr) { + BSTR path = SysAllocString(L"ROOT\\WMI"); + if (path != nullptr && SUCCEEDED(locator->ConnectServer(path, nullptr, nullptr, nullptr, 0, nullptr, nullptr, &services)) && services != nullptr) { + CoSetProxyBlanket(services, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, nullptr, RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, EOAC_NONE); + IEnumWbemClassObject* results = nullptr; + BSTR language = SysAllocString(L"WQL"); + BSTR query = SysAllocString(L"SELECT CurrentTemperature FROM MSAcpi_ThermalZoneTemperature"); + if (language != nullptr && query != nullptr && SUCCEEDED(services->ExecQuery(language, query, WBEM_FLAG_FORWARD_ONLY, nullptr, &results)) && results != nullptr) { + int found = 0; + while (found < 2) { + IWbemClassObject* object = nullptr; + ULONG returned = 0; + if (results->Next(1000, 1, &object, &returned) != S_OK || returned != 1 || object == nullptr) break; + VARIANT value; + VariantInit(&value); + if (SUCCEEDED(object->Get(L"CurrentTemperature", 0, &value, nullptr, nullptr))) { + if (value.vt == VT_I4 || value.vt == VT_INT) { + const long tenthsKelvin = value.vt == VT_I4 ? value.lVal : value.intVal; + int reading = static_cast(tenthsKelvin / 10) - 273; + Plausible(reading); + if (reading >= 0) celsius[found++] = reading; + } + } + VariantClear(&value); + object->Release(); + } + results->Release(); + } + if (language != nullptr) SysFreeString(language); + if (query != nullptr) SysFreeString(query); + services->Release(); + } + if (path != nullptr) SysFreeString(path); + locator->Release(); + } + if (uninitialize) CoUninitialize(); +} + +int ReadStorageTemperature() { + constexpr ULONG kStorageDeviceTemperatureProperty = 52; + constexpr ULONG kLegacyStorageDeviceTemperatureProperty = 22; + constexpr ULONG kPropertyStandardQuery = 0; + constexpr DWORD kIoctlStorageQueryProperty = 0x2D1400; + struct Information { + USHORT index; + SHORT temperature; + SHORT overThreshold; + SHORT underThreshold; + UCHAR overThresholdChangable; + UCHAR underThresholdChangable; + UCHAR eventGenerated; + UCHAR reserved0; + ULONG reserved1; + }; + struct Descriptor { + ULONG version; + ULONG size; + SHORT criticalTemperature; + SHORT warningTemperature; + USHORT infoCount; + UCHAR reserved0[2]; + ULONG reserved1[2]; + Information information[16]; + }; + constexpr std::size_t kTemperatureBytes = + offsetof(Descriptor, information) + offsetof(Information, temperature) + sizeof(SHORT); + static_assert(offsetof(Descriptor, infoCount) == 12 && offsetof(Descriptor, information) == 24 && + offsetof(Information, temperature) == 2); + struct Query { + ULONG property; + ULONG query; + UCHAR extra[1]; + }; + constexpr ULONG kProperties[] = {kStorageDeviceTemperatureProperty, kLegacyStorageDeviceTemperatureProperty}; + for (int index = 0; index < 16; ++index) { + char name[32]; + std::snprintf(name, sizeof(name), "\\\\.\\PhysicalDrive%d", index); + HANDLE handle = CreateFileA(name, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr); + if (handle == INVALID_HANDLE_VALUE) continue; + for (ULONG property : kProperties) { + Query query{}; + query.property = property; + query.query = kPropertyStandardQuery; + Descriptor descriptor{}; + DWORD returned = 0; + const bool ok = DeviceIoControl(handle, kIoctlStorageQueryProperty, &query, sizeof(query), &descriptor, sizeof(descriptor), &returned, nullptr) != 0; + if (!ok || returned < kTemperatureBytes || descriptor.infoCount == 0) continue; + int reading = descriptor.information[0].temperature; + Plausible(reading); + if (reading >= 0) { + CloseHandle(handle); + return reading; + } + } + CloseHandle(handle); + } + return -1; +} + +#endif + +void ReadHostSensors(int celsius[3]) { +#ifdef _WIN32 + celsius[2] = ReadStorageTemperature(); + ReadAcpiThermalZones(celsius); +#elif defined(__linux__) + ReadThermalSensors_nid_no_patch("/sys/class", celsius); +#else + celsius[0] = celsius[1] = celsius[2] = -1; +#endif +} + +} + +extern "C" { + +int APS5_VABI sceKernelGetCpuTemperature(int* outCelsius) { + if (outCelsius == nullptr) return SCE_KERNEL_ERROR_EFAULT; + int celsius[3]; + ReadHostSensors(celsius); + if (celsius[0] < 0) return SCE_KERNEL_ERROR_EINVAL; + *outCelsius = celsius[0]; + return 0; +} + +int APS5_VABI sceKernelGetSocSensorTemperature(int sensorId, int* outCelsius) { + if (outCelsius == nullptr) return SCE_KERNEL_ERROR_EFAULT; + if (sensorId < 0 || sensorId >= 3) return SCE_KERNEL_ERROR_EINVAL; + int celsius[3]; + ReadHostSensors(celsius); + if (celsius[sensorId] < 0) return SCE_KERNEL_ERROR_EINVAL; + *outCelsius = celsius[sensorId]; + return 0; +} + +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/System/src/UserContext.cpp b/core/libs/prx/libkernel/System/src/UserContext.cpp new file mode 100644 index 0000000000..92eaa7a389 --- /dev/null +++ b/core/libs/prx/libkernel/System/src/UserContext.cpp @@ -0,0 +1,219 @@ +#include "prx/libc/include/GuestStacks.hpp" +#include "prx/libc/include/general/AsmMacros.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/System/include/UserContext.hpp" +#include +#include +#include +#include + +extern "C" int* APS5_VABI __error_nid_postfix(); +extern "C" int APS5_VABI sigprocmask_nid_postfix(int how, const void* set, void* previousSet); + +namespace { + +struct RegisterFrame { + std::uint64_t rax; + std::uint64_t rbx; + std::uint64_t rcx; + std::uint64_t rdx; + std::uint64_t rsi; + std::uint64_t rdi; + std::uint64_t rbp; + std::uint64_t rsp; + std::uint64_t r8; + std::uint64_t r9; + std::uint64_t r10; + std::uint64_t r11; + std::uint64_t r12; + std::uint64_t r13; + std::uint64_t r14; + std::uint64_t r15; + std::uint64_t rip; + std::uint64_t rflags; + std::uint64_t cs; + std::uint64_t ss; + alignas(16) std::uint8_t fxsave[512]; +}; + +static_assert(offsetof(RegisterFrame, rsp) == 0x38 && offsetof(RegisterFrame, r8) == 0x40 && offsetof(RegisterFrame, r12) == 0x60 && offsetof(RegisterFrame, r15) == 0x78); +static_assert(offsetof(RegisterFrame, rip) == 0x80 && offsetof(RegisterFrame, rflags) == 0x88 && offsetof(RegisterFrame, cs) == 0x90 && offsetof(RegisterFrame, ss) == 0x98); +static_assert(offsetof(RegisterFrame, fxsave) == 0xa0 && sizeof(RegisterFrame) == 0x2a0); + +constexpr int GuestEinval = 22; +constexpr int GuestSigBlock = 1; +constexpr int GuestSigSetmask = 3; +constexpr std::uint64_t CarryFlag = 0x1; +constexpr std::uint32_t McontextFlagMask = 0x7; +constexpr std::uint64_t FpFormatXmm = 0x10002; +constexpr std::uint64_t FpOwnedFpu = 0x20001; +constexpr std::size_t FxsaveMxcsr = 24; +constexpr std::size_t FxsaveMxcsrMask = 28; +constexpr std::uint32_t DefaultMxcsrMask = 0xffbf; + +int Fail(int error) { + *__error_nid_postfix() = error; + return -1; +} + +std::uint32_t HostMxcsrMask() { + static const std::uint32_t mask = [] { + alignas(16) std::uint8_t image[512]{}; + _fxsave64(image); + std::uint32_t value = 0; + std::memcpy(&value, image + FxsaveMxcsrMask, sizeof(value)); + return value != 0 ? value : DefaultMxcsrMask; + }(); + return mask; +} + +} + +extern "C" [[noreturn]] void APS5_VABI Aps5ContextRestore_nid_no_patch(const RegisterFrame* frame); + +asm(".text\n" + APS5_ASM_FUNCTION("getcontext_nid_postfix") + " pushfq\n" + " push %rbp\n" + " mov %rsp, %rbp\n" + " sub $0x2a0, %rsp\n" + " and $-16, %rsp\n" + " mov %rax, 0x00(%rsp)\n" + " mov %rbx, 0x08(%rsp)\n" + " mov %rcx, 0x10(%rsp)\n" + " mov %rdx, 0x18(%rsp)\n" + " mov %rsi, 0x20(%rsp)\n" + " mov %rdi, 0x28(%rsp)\n" + " mov 0(%rbp), %rax\n" + " mov %rax, 0x30(%rsp)\n" + " lea 24(%rbp), %rax\n" + " mov %rax, 0x38(%rsp)\n" + " mov %r8, 0x40(%rsp)\n" + " mov %r9, 0x48(%rsp)\n" + " mov %r10, 0x50(%rsp)\n" + " mov %r11, 0x58(%rsp)\n" + " mov %r12, 0x60(%rsp)\n" + " mov %r13, 0x68(%rsp)\n" + " mov %r14, 0x70(%rsp)\n" + " mov %r15, 0x78(%rsp)\n" + " mov 16(%rbp), %rax\n" + " mov %rax, 0x80(%rsp)\n" + " mov 8(%rbp), %rax\n" + " mov %rax, 0x88(%rsp)\n" + " xor %eax, %eax\n" + " mov %cs, %ax\n" + " mov %rax, 0x90(%rsp)\n" + " mov %ss, %ax\n" + " mov %rax, 0x98(%rsp)\n" + " fxsave64 0xa0(%rsp)\n" + " mov %rsp, %rsi\n" + " call " APS5_ASM_SYMBOL("Aps5GetcontextFinish_nid_no_patch") "\n" + " mov %rbp, %rsp\n" + " pop %rbp\n" + " lea 8(%rsp), %rsp\n" + " ret\n" + APS5_ASM_FUNCTION("Aps5ContextRestore_nid_no_patch") + " mov %rdi, %r12\n" + " xor %eax, %eax\n" + " mov %ss, %ax\n" + " push %rax\n" + " pushq 0x38(%r12)\n" + " pushfq\n" + " pop %rax\n" + " and $-0x254dd6, %rax\n" + " mov 0x88(%r12), %rcx\n" + " and $0x254dd5, %rcx\n" + " or %rcx, %rax\n" + " push %rax\n" + " xor %eax, %eax\n" + " mov %cs, %ax\n" + " push %rax\n" + " pushq 0x80(%r12)\n" +#ifdef _WIN32 + " mov 0x38(%r12), %rdi\n" + " call GuestStackSwitch_nid_no_patch\n" +#endif + " fxrstor64 0xa0(%r12)\n" + " mov 0x00(%r12), %rax\n" + " mov 0x08(%r12), %rbx\n" + " mov 0x10(%r12), %rcx\n" + " mov 0x18(%r12), %rdx\n" + " mov 0x20(%r12), %rsi\n" + " mov 0x28(%r12), %rdi\n" + " mov 0x30(%r12), %rbp\n" + " mov 0x40(%r12), %r8\n" + " mov 0x48(%r12), %r9\n" + " mov 0x50(%r12), %r10\n" + " mov 0x58(%r12), %r11\n" + " mov 0x68(%r12), %r13\n" + " mov 0x70(%r12), %r14\n" + " mov 0x78(%r12), %r15\n" + " mov 0x60(%r12), %r12\n" + " iretq\n"); + +extern "C" { + +int APS5_VABI Aps5GetcontextFinish_nid_no_patch(UserUcontext* context, const RegisterFrame* frame) { + if (context == nullptr) return Fail(GuestEinval); + UserUcontext captured{}; + sigprocmask_nid_postfix(GuestSigBlock, nullptr, captured.sigmask); + auto& machine = captured.mcontext; + machine.rdi = frame->rdi; + machine.rsi = frame->rsi; + machine.rcx = frame->rcx; + machine.r8 = frame->r8; + machine.r9 = frame->r9; + machine.rbx = frame->rbx; + machine.rbp = frame->rbp; + machine.r10 = frame->r10; + machine.r11 = frame->r11; + machine.r12 = frame->r12; + machine.r13 = frame->r13; + machine.r14 = frame->r14; + machine.r15 = frame->r15; + machine.rip = frame->rip; + machine.cs = frame->cs; + machine.rflags = frame->rflags & ~CarryFlag; + machine.rsp = frame->rsp; + machine.ss = frame->ss; + machine.len = sizeof(UserMcontext); + machine.fpformat = FpFormatXmm; + machine.ownedfp = FpOwnedFpu; + std::memcpy(machine.fpstate, frame->fxsave, sizeof(frame->fxsave)); + std::memcpy(context, &captured, offsetof(UserUcontext, link)); + return 0; +} + +int APS5_VABI setcontext_nid_postfix(const UserUcontext* context) { + if (context == nullptr) return Fail(GuestEinval); + const auto& machine = context->mcontext; + if (machine.len != sizeof(UserMcontext) || (machine.flags & ~McontextFlagMask) != 0) return Fail(GuestEinval); + RegisterFrame frame{}; + frame.rax = machine.rax; + frame.rbx = machine.rbx; + frame.rcx = machine.rcx; + frame.rdx = machine.rdx; + frame.rsi = machine.rsi; + frame.rdi = machine.rdi; + frame.rbp = machine.rbp; + frame.rsp = machine.rsp; + frame.r8 = machine.r8; + frame.r9 = machine.r9; + frame.r10 = machine.r10; + frame.r11 = machine.r11; + frame.r12 = machine.r12; + frame.r13 = machine.r13; + frame.r14 = machine.r14; + frame.r15 = machine.r15; + frame.rip = machine.rip; + frame.rflags = machine.rflags; + std::memcpy(frame.fxsave, machine.fpstate, sizeof(frame.fxsave)); + std::uint32_t mxcsr = 0; + std::memcpy(&mxcsr, frame.fxsave + FxsaveMxcsr, sizeof(mxcsr)); + mxcsr &= HostMxcsrMask(); + std::memcpy(frame.fxsave + FxsaveMxcsr, &mxcsr, sizeof(mxcsr)); + sigprocmask_nid_postfix(GuestSigSetmask, context->sigmask, nullptr); + Aps5ContextRestore_nid_no_patch(&frame); +} + +} 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 035fa45820..67ed023339 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -1,5 +1,7 @@ #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" #include "prx/libkernel/KernelErrors.hpp" @@ -9,6 +11,8 @@ #include #include #include +#include +#include #include #include #include @@ -26,11 +30,17 @@ #include #include #include +#ifdef __linux__ +#include +#include +#endif // __linux__ #endif extern "C" int* APS5_VABI __error_nid_postfix(); +static constexpr int GUEST_EFAULT = 14; static constexpr int GUEST_EINVAL = 22; +static constexpr int GUEST_CLOCK_REALTIME = 0; static constexpr int GUEST_CLOCK_THREAD_CPUTIME_ID = 14; static bool IsCpuClock(int clockId) { @@ -70,9 +80,9 @@ static std::uint64_t ClockOrigin() { } static std::uint64_t GetMonotonicNanos() { - const auto raw = RawMonotonicNanos(); - if (TimeScale() == 1.0) return raw; + if (TimeScale() == 1.0) return RawMonotonicNanos(); const auto origin = ClockOrigin(); + const auto raw = RawMonotonicNanos(); return origin + static_cast(static_cast(raw - origin) * TimeScale()); } @@ -81,6 +91,8 @@ static std::uint64_t GetStartNanos() { return start; } +static const std::uint64_t g_processStart = GetStartNanos(); + #ifdef _WIN32 // Every relative wait in the process (winpthreads nanosleep, std::condition_variable::wait_for, the // GPU driver's poll sleeps) rounds up to the scheduler tick, 15.6 ms by default; a console game and @@ -165,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 @@ -186,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"); } @@ -205,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; @@ -236,35 +274,37 @@ void KernelTraceWait_nid_postfix(const char* kind, const void* caller, std::uint for (auto& [name, s] : sites) s = Site{}; } +static void CancellableSleep(std::uint64_t nanos) { + ThreadCancel::Check(); + SleepWake::Sleep(nanos); + ThreadCancel::Check(); +} + int APS5_VABI sceKernelUsleep_nid_postfix(KernelUseconds microseconds) { TraceSleep(__builtin_return_address(0), microseconds); - TimedWait::SleepNanos(static_cast(microseconds) * 1000ULL); + CancellableSleep(static_cast(microseconds) * 1000ULL); return 0; } -static int SleepForRequest(const KernelTimespec* rqtp, KernelTimespec* rmtp) { +static int SleepForRequest(const KernelTimespec* rqtp) { constexpr int guestFault = 14; constexpr int guestInvalid = 22; if (rqtp == nullptr) return guestFault; if (rqtp->tv_nsec < 0 || rqtp->tv_nsec >= 1000000000LL) return guestInvalid; if (rqtp->tv_sec >= 0) { - TimedWait::SleepNanos(static_cast(rqtp->tv_sec) * 1000000000ULL + - static_cast(rqtp->tv_nsec)); - } - if (rmtp != nullptr) { - rmtp->tv_sec = 0; - rmtp->tv_nsec = 0; + CancellableSleep(static_cast(rqtp->tv_sec) * 1000000000ULL + + static_cast(rqtp->tv_nsec)); } return 0; } int APS5_VABI sceKernelNanosleep(const KernelTimespec* rqtp, KernelTimespec* rmtp) { - const int error = SleepForRequest(rqtp, rmtp); + const int error = SleepForRequest(rqtp); return error == 0 ? 0 : SceKernelError(error); } int APS5_VABI nanosleep_nid_postfix(const KernelTimespec* rqtp, KernelTimespec* rmtp) { - const int error = SleepForRequest(rqtp, rmtp); + const int error = SleepForRequest(rqtp); if (error == 0) return 0; *__error_nid_postfix() = error; return -1; @@ -275,46 +315,59 @@ int APS5_VABI _nanosleep_nid_postfix(const KernelTimespec* rqtp, KernelTimespec* } int APS5_VABI usleep_nid_postfix(KernelUseconds microseconds) { - TimedWait::SleepNanos(static_cast(microseconds) * 1000ULL); + CancellableSleep(static_cast(microseconds) * 1000ULL); return 0; } #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() { @@ -334,16 +387,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)) { @@ -355,7 +406,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; @@ -388,12 +444,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) { @@ -426,7 +483,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) { @@ -438,10 +495,20 @@ int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp) { #endif } -int APS5_VABI gettimeofday_nid_postfix(KernelTimeval* tv, KernelTimezone* tz) { - if (tv == nullptr) { - APS5_INVALID_ARG_EX; +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; #ifdef _WIN32 FILETIME ft{}; GetSystemTimePreciseAsFileTime(&ft); @@ -465,13 +532,12 @@ int APS5_VABI gettimeofday_nid_postfix(KernelTimeval* tv, KernelTimezone* tz) { } int APS5_VABI clock_getres_nid_postfix(int clockId, KernelTimespec* res) { - if (res == nullptr) { - APS5_INVALID_ARG_EX; - } + if (res == nullptr) return 0; 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; @@ -499,7 +565,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; @@ -548,36 +615,96 @@ int APS5_VABI clock_getres_nid_postfix(int clockId, KernelTimespec* res) { #endif } +int APS5_VABI clock_settime_nid_postfix(int clockId, const KernelTimespec* tp) { + if (tp == nullptr) { + *__error_nid_postfix() = GUEST_EFAULT; + return -1; + } + if (clockId != GUEST_CLOCK_REALTIME || tp->tv_nsec < 0 || tp->tv_nsec >= 1000000000 || tp->tv_sec < 0) { + *__error_nid_postfix() = GUEST_EINVAL; + return -1; + } + char requested[48]; + std::snprintf(requested, sizeof(requested), "%lld.%09lld", static_cast(tp->tv_sec), static_cast(tp->tv_nsec)); + throw std::runtime_error(std::string("clock_settime: setting CLOCK_REALTIME to ") + requested + " is not supported, the host clock is not changed"); +} + // --------------------------------------------------------------------------- // Moved as-is (not yet implemented) from the monolithic libkernel/Export.cpp. // --------------------------------------------------------------------------- -// 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; - return clock_getres_nid_postfix(static_cast(clock_id), tp); + if (clock_getres_nid_postfix(static_cast(clock_id), tp) != 0) return SceKernelError(*__error_nid_postfix()); + return 0; } int APS5_VABI sceKernelClockGettime(KernelClockid clock_id, KernelTimespec* tp) { if (tp == nullptr) return SceKernelErrorEfault; - return clock_gettime_nid_postfix(static_cast(clock_id), tp); -} - -int APS5_VABI sceKernelConvertLocaltimeToUtc(int64_t local_time, int64_t reserved, int64_t* utc_time, KernelTimezone* timezone, int32_t* dst_seconds) { - (void)reserved; - if (utc_time != nullptr) *utc_time = local_time; - if (timezone != nullptr) *timezone = {0, 0}; - if (dst_seconds != nullptr) *dst_seconds = 0; + if (clock_gettime_nid_postfix(static_cast(clock_id), tp) != 0) return SceKernelError(*__error_nid_postfix()); return 0; } -int APS5_VABI sceKernelConvertUtcToLocaltime(int64_t utc_time, int64_t* local_time, KernelTimesec* st, uint64_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; +int APS5_VABI sceKernelConvertLocaltimeToUtc(int64_t local_time, int64_t reserved, int64_t* utc_time, KernelTimesec* timezone, int32_t* dst_seconds) { + (void)reserved; + 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) { + 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; } @@ -610,8 +737,12 @@ uint64_t APS5_VABI sceKernelGetTscFrequency(void) { return frequency; } +std::int64_t APS5_VABI sceKernelGetCpuFrequency(void) { + return static_cast(sceKernelGetTscFrequency()); +} + unsigned int APS5_VABI sceKernelSleep(unsigned int seconds) { - TimedWait::SleepNanos(static_cast(seconds) * 1000000000ULL); + CancellableSleep(static_cast(seconds) * 1000000000ULL); return 0; } diff --git a/core/libs/prx/libkernel/Time/TimedWait.cpp b/core/libs/prx/libkernel/Time/TimedWait.cpp index 8a8f5759f1..7c171db399 100644 --- a/core/libs/prx/libkernel/Time/TimedWait.cpp +++ b/core/libs/prx/libkernel/Time/TimedWait.cpp @@ -1,7 +1,11 @@ #include "prx/libkernel/Time/include/TimedWait.hpp" +#include "prx/libkernel/Pthread/include/WindowsThreadLocal.hpp" +#include #include #include +#include +#include #ifdef _WIN32 #include @@ -17,8 +21,40 @@ namespace { constexpr std::uint64_t YIELD_BELOW_NANOS = 50000ULL; constexpr std::uint64_t LEAD_NANOS = 500000ULL; -thread_local std::atomic ownWaitState{0}; -thread_local std::atomic* waitState = &ownWaitState; +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; +}; +ThreadWaitState& WaitState() { +#ifdef _WIN32 + return WindowsThreadLocal::Get(); +#else + static thread_local ThreadWaitState state; + return state; +#endif +} #ifdef _WIN32 @@ -31,29 +67,24 @@ void DrainApcs() { } DWORD AlertableWait(DWORD count, const HANDLE* handles, DWORD milliseconds) { - waitState->fetch_add(1, std::memory_order_seq_cst); + WaitState().bound->fetch_add(1, std::memory_order_seq_cst); DWORD result; do { result = WaitForMultipleObjectsEx(count, handles, FALSE, milliseconds, TRUE); } while (result == WAIT_IO_COMPLETION); - waitState->fetch_sub(1, std::memory_order_seq_cst); + WaitState().bound->fetch_sub(1, std::memory_order_seq_cst); DrainApcs(); return result; } void AlertableSleep(DWORD milliseconds) { - waitState->fetch_add(1, std::memory_order_seq_cst); + WaitState().bound->fetch_add(1, std::memory_order_seq_cst); SleepEx(milliseconds, TRUE); - waitState->fetch_sub(1, std::memory_order_seq_cst); + WaitState().bound->fetch_sub(1, std::memory_order_seq_cst); DrainApcs(); } -void WINAPI DestroyWaiter(void* value); - -DWORD WaiterSlotIndex() { - static const DWORD index = FlsAlloc(DestroyWaiter); - return index; -} +HANDLE ThreadTimer(unsigned kind); HANDLE CreateHighResolutionTimer() { return CreateWaitableTimerExW(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS); @@ -88,7 +119,7 @@ void SleepNanosCoarse(std::uint64_t nanos) { SwitchToThread(); return; } - thread_local HANDLE timer = CreateHighResolutionTimer(); + const HANDLE timer = ThreadTimer(0); if (TimerWait(timer, nanos)) return; AlertableSleep(static_cast((nanos + 999999ULL) / 1000000ULL)); } @@ -102,33 +133,34 @@ void SleepNanosCoarse(std::uint64_t nanos) { struct Waiter { HANDLE event = CreateEventW(nullptr, FALSE, FALSE, nullptr); HANDLE timer = CreateHighResolutionTimer(); + HANDLE sleepTimers[2]{}; Waiter* previous = nullptr; 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); } }; namespace { -void WINAPI DestroyWaiter(void* value) { - delete static_cast(value); +Waiter* ThisThreadWaiter() { + return &WindowsThreadLocal::Get(); } -Waiter* ThisThreadWaiter() { - thread_local Waiter* waiter = nullptr; - if (!waiter) { - waiter = new Waiter(); - const DWORD slot = WaiterSlotIndex(); - if (slot != FLS_OUT_OF_INDEXES) FlsSetValue(slot, waiter); - } - return waiter; +HANDLE ThreadTimer(unsigned kind) { + auto& timer = ThisThreadWaiter()->sleepTimers[kind]; + if (!timer) timer = CreateHighResolutionTimer(); + return timer; } Waiter* AcquireWaiter() { @@ -235,6 +267,118 @@ 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 { + std::mutex lock; + Waiter* head = nullptr; + Waiter* tail = nullptr; + std::atomic parked{0}; +}; + +ParkBucket parkBuckets[256]; + +ParkBucket& ParkBucketFor(const void* key) { + return parkBuckets[(reinterpret_cast(key) * 0x9E3779B97F4A7C15ULL) >> 56]; +} + +void UnlinkParked(ParkBucket& bucket, Waiter* waiter) { + if (waiter->previous) waiter->previous->next = waiter->next; + else bucket.head = waiter->next; + if (waiter->next) waiter->next->previous = waiter->previous; + else bucket.tail = waiter->previous; + waiter->previous = nullptr; + waiter->next = nullptr; + waiter->queued = false; +} + +bool LeavePark(ParkBucket& bucket, Waiter* waiter) { + bool signaled; + { + DeliveryDeferral deferral; + std::lock_guard lock(bucket.lock); + signaled = !waiter->queued; + if (!signaled) UnlinkParked(bucket, waiter); + } + if (signaled) WaitForSingleObject(waiter->event, 0); + return signaled; +} + +void ReleaseParked(ParkBucket& bucket, Waiter* waiter) { + bucket.parked.fetch_sub(1, std::memory_order_seq_cst); + waiter->busy = false; + DrainApcs(); +} + +} // namespace + +void SpinPause() { + YieldProcessor(); +} + +Waiter* BeginPark(const void* key) { + auto& bucket = ParkBucketFor(key); + Waiter* waiter; + { + DeliveryDeferral deferral; + waiter = AcquireWaiter(); + waiter->key = key; + bucket.parked.fetch_add(1, std::memory_order_seq_cst); + std::lock_guard lock(bucket.lock); + waiter->queued = true; + waiter->next = nullptr; + waiter->previous = bucket.tail; + if (bucket.tail) bucket.tail->next = waiter; + else bucket.head = waiter; + bucket.tail = waiter; + } + DrainApcs(); + return waiter; +} + +bool FinishPark(const void* key, Waiter* waiter, const std::uint64_t* deadlineNanos) { + auto& bucket = ParkBucketFor(key); + bool signaled = deadlineNanos ? WaitEventUntil(waiter, *deadlineNanos) : AlertableWait(1, &waiter->event, INFINITE) == WAIT_OBJECT_0; + if (!signaled) signaled = LeavePark(bucket, waiter); + ReleaseParked(bucket, waiter); + if (!signaled && !deadlineNanos) throw std::runtime_error("TimedWait: an untimed parked wait ended without a wake"); + return signaled; +} + +void CancelPark(const void* key, Waiter* waiter) { + auto& bucket = ParkBucketFor(key); + LeavePark(bucket, waiter); + ReleaseParked(bucket, waiter); +} + +void Unpark(const void* key) { + auto& bucket = ParkBucketFor(key); + if (bucket.parked.load(std::memory_order_seq_cst) == 0) return; + { + DeliveryDeferral deferral; + std::lock_guard lock(bucket.lock); + for (Waiter* waiter = bucket.head; waiter; waiter = waiter->next) { + if (waiter->key != key) continue; + UnlinkParked(bucket, waiter); + SetEvent(waiter->event); + break; + } + } + DrainApcs(); +} + #else void Condition::NotifyOne() { @@ -248,7 +392,8 @@ void Condition::NotifyAll() { #endif void BindThreadWaitState(std::atomic* state) { - waitState = state != nullptr ? state : &ownWaitState; + auto& local = WaitState(); + local.bound = state != nullptr ? state : &local.own; } bool Coarse() { @@ -293,7 +438,7 @@ void SleepUntil(std::uint64_t deadlineNanos) { #ifdef _WIN32 std::uint64_t now = NowNanos(); if (deadlineNanos > now + LEAD_NANOS) { - thread_local HANDLE timer = CreateHighResolutionTimer(); + const HANDLE timer = ThreadTimer(1); if (!TimerWait(timer, deadlineNanos - now - LEAD_NANOS)) AlertableSleep(WholeMilliseconds(deadlineNanos - now - LEAD_NANOS)); now = NowNanos(); } @@ -307,14 +452,69 @@ void SleepUntil(std::uint64_t deadlineNanos) { #endif } -void PollSleepUntil(std::uint64_t deadlineNanos) { +bool SleepNanosUnlessChanged(std::uint64_t nanos, const std::atomic& counter, std::uint64_t observed) { + const std::uint64_t deadlineNanos = NowNanos() + nanos; #ifdef _WIN32 - const std::uint64_t now = NowNanos(); - if (deadlineNanos <= now) return; - thread_local HANDLE timer = CreateHighResolutionTimer(); - if (!TimerWait(timer, deadlineNanos - now)) AlertableSleep(WholeMilliseconds(deadlineNanos - now)); + 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 - SleepUntil(deadlineNanos); + 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 } 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 bddd4189e0..2f1725819d 100644 --- a/core/libs/prx/libkernel/Time/include/TimedWait.hpp +++ b/core/libs/prx/libkernel/Time/include/TimedWait.hpp @@ -16,31 +16,57 @@ std::uint64_t DeadlineNanos(std::uint64_t microseconds); std::uint64_t RemainingMicros(std::uint64_t deadlineNanos); void SleepNanos(std::uint64_t nanos); void SleepUntil(std::uint64_t deadlineNanos); -void PollSleepUntil(std::uint64_t deadlineNanos); - -constexpr std::uint64_t LOCK_BULK_NANOS = 40000000ULL; -constexpr std::uint64_t LOCK_POLL_NANOS = 500000ULL; -constexpr std::uint64_t LOCK_TAIL_NANOS = 1000000ULL; - -template -bool AcquireUntil(std::uint64_t deadlineNanos, TTryLock tryLock, TTryLockFor tryLockFor) { - if (Coarse()) return tryLockFor(RemainingMicros(deadlineNanos)); - for (;;) { - if (tryLock()) return true; - const std::uint64_t now = NowNanos(); - if (now >= deadlineNanos) return false; - const std::uint64_t remaining = deadlineNanos - now; - if (remaining > LOCK_BULK_NANOS) { - if (tryLockFor((remaining - LOCK_BULK_NANOS) / 1000ULL)) return true; - continue; - } - if (remaining <= LOCK_TAIL_NANOS) SleepUntil(deadlineNanos); - else PollSleepUntil(now + LOCK_POLL_NANOS); - } -} +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); +void CancelPark(const void* key, Waiter* waiter); +void Unpark(const void* key); + +template +bool ParkAcquire(const void* key, TTryLock tryLock, const std::uint64_t* deadlineNanos) { + for (unsigned spin = 0; spin < PARK_SPINS; ++spin) { + if (tryLock()) return true; + SpinPause(); + } + for (;;) { + Waiter* waiter = BeginPark(key); + if (tryLock()) { + CancelPark(key, waiter); + return true; + } + if (!FinishPark(key, waiter, deadlineNanos)) return tryLock(); + if (tryLock()) return true; + } +} +#endif + +template +bool AcquireUntil(const void* key, std::uint64_t deadlineNanos, TTryLock tryLock, TTryLockFor tryLockFor) { +#ifdef _WIN32 + if (!Coarse()) return ParkAcquire(key, tryLock, &deadlineNanos); +#endif + static_cast(key); + static_cast(tryLock); + return tryLockFor(RemainingMicros(deadlineNanos)); +} + class Condition { public: Condition() = default; diff --git a/core/libs/prx/libkernel/Unimplemented.cpp b/core/libs/prx/libkernel/Unimplemented.cpp index 18df014884..3dd75f72d8 100644 --- a/core/libs/prx/libkernel/Unimplemented.cpp +++ b/core/libs/prx/libkernel/Unimplemented.cpp @@ -5,21 +5,131 @@ extern "C" { -int APS5_VABI sceCoredumpWriteUserData() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI __tls_get_addr_nid_postfix(void) { NotImplemented_nid_no_patch("vNe1w4diLCs"); return 0; } -// Canonical lib is libScePosix (dead import of Cyberpunk 2077): 35 of its -// 36 sibling imports resolve to libkernel, and libScePosix is not in the -// game's NEEDED list so only a NEEDED module can satisfy the loader here. -int APS5_VABI pthread_cancel_nid_postfix(void) { - NotImplemented_nid_no_patch("0D4-FVvEikw"); +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"); + return 0; +} + +APS5_EXPORT("0iEKNAvT600", sceKernelUnknown04); +int APS5_VABI sceKernelUnknown04(void) { + NotImplemented_nid_no_patch("0iEKNAvT600"); + return 0; +} + +APS5_EXPORT("2cELEBPdYQ0", sceKernelUnknown05); +int APS5_VABI sceKernelUnknown05(void) { + NotImplemented_nid_no_patch("2cELEBPdYQ0"); + return 0; +} + +APS5_EXPORT("5xYxiOtOccA", sceKernelUnknown06); +int APS5_VABI sceKernelUnknown06(void) { + NotImplemented_nid_no_patch("5xYxiOtOccA"); + return 0; +} + +APS5_EXPORT("6W+shIH315Q", sceKernelUnknown07); +int APS5_VABI sceKernelUnknown07(void) { + NotImplemented_nid_no_patch("6W+shIH315Q"); + return 0; +} + +APS5_EXPORT("7ilObq815O4", sceKernelUnknown08); +int APS5_VABI sceKernelUnknown08(void) { + NotImplemented_nid_no_patch("7ilObq815O4"); + return 0; +} + +APS5_EXPORT("7iq7Hfs8HBk", sceKernelUnknown09); +int APS5_VABI sceKernelUnknown09(void) { + NotImplemented_nid_no_patch("7iq7Hfs8HBk"); + return 0; +} + +APS5_EXPORT("AMJZhpC+siY", sceKernelUnknown10); +int APS5_VABI sceKernelUnknown10(void) { + NotImplemented_nid_no_patch("AMJZhpC+siY"); + return 0; +} + +APS5_EXPORT("E9XkHowt+dY", sceKernelUnknown11); +int APS5_VABI sceKernelUnknown11(void) { + NotImplemented_nid_no_patch("E9XkHowt+dY"); + return 0; +} + +APS5_EXPORT("GADjreszOgM", sceKernelUnknown12); +int APS5_VABI sceKernelUnknown12(void) { + NotImplemented_nid_no_patch("GADjreszOgM"); + return 0; +} + +APS5_EXPORT("Giyg2xGrXDU", sceKernelUnknown13); +int APS5_VABI sceKernelUnknown13(void) { + NotImplemented_nid_no_patch("Giyg2xGrXDU"); + return 0; +} + +APS5_EXPORT("KC3y21wrqzc", sceKernelUnknown14); +int APS5_VABI sceKernelUnknown14(void) { + NotImplemented_nid_no_patch("KC3y21wrqzc"); + return 0; +} + +APS5_EXPORT("Qb8wS4GZUsw", sceKernelUnknown15); +int APS5_VABI sceKernelUnknown15(void) { + NotImplemented_nid_no_patch("Qb8wS4GZUsw"); + return 0; +} + +APS5_EXPORT("aAQZSjb2fho", sceKernelUnknown16); +int APS5_VABI sceKernelUnknown16(void) { + NotImplemented_nid_no_patch("aAQZSjb2fho"); + return 0; +} + +APS5_EXPORT("pnz21I6VjDs", sceKernelUnknown17); +int APS5_VABI sceKernelUnknown17(void) { + NotImplemented_nid_no_patch("pnz21I6VjDs"); + return 0; +} + +APS5_EXPORT("qk043QPsrZU", sceKernelUnknown18); +int APS5_VABI sceKernelUnknown18(void) { + NotImplemented_nid_no_patch("qk043QPsrZU"); + return 0; +} + +APS5_EXPORT("uDGXwUKqi8s", sceKernelUnknown19); +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; +} + } 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..8b3da583e4 --- /dev/null +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -0,0 +1,2641 @@ +#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 fstat_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 lchmod_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_reltimedrdlock_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_rwlock_reltimedwrlock_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_defaultstacksize_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 sceKernelGetCompiledSdkVersion(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 scePthreadSetDefaultstacksize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadSetName(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 34a27e9b1d..9c7d6fc2b6 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -1,21 +1,119 @@ #include +#include #include #include #include +#include #include #include +#include #include +#include #include +#include +#ifndef _WIN32 +#include +#include +#include +#include +#endif #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/FilesystemError.hpp" +#include "prx/libkernel/File/include/DirectoryDescriptor.hpp" +#include "prx/libc/include/CreationMask.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#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 +#endif 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; constexpr int GuestEinval = 22; +constexpr int GuestEexist = 17; +constexpr int GuestExdev = 18; +constexpr int GuestEdeadlk = 11; +constexpr int GuestEagain = 35; +constexpr int GuestEloop = 62; +constexpr int GuestEnametoolong = 63; +constexpr int GuestEnotempty = 66; +constexpr int GuestEoverflow = 84; +constexpr int GuestAtFdcwd = -100; +constexpr int GuestAtRemovedir = 0x800; +constexpr std::size_t GuestPathMax = 1024; +constexpr std::size_t GuestNameMax = 255; + +constexpr int PcLinkMax = 1; +constexpr int PcMaxCanon = 2; +constexpr int PcMaxInput = 3; +constexpr int PcNameMax = 4; +constexpr int PcPathMax = 5; +constexpr int PcPipeBuf = 6; +constexpr int PcChownRestricted = 7; +constexpr int PcNoTrunc = 8; +constexpr int PcVdisable = 9; +constexpr int PcAllocSizeMin = 10; +constexpr int PcFileSizeBits = 12; +constexpr int PcRecIncrXferSize = 14; +constexpr int PcRecMaxXferSize = 15; +constexpr int PcRecMinXferSize = 16; +constexpr int PcRecXferAlign = 17; +constexpr int PcSymlinkMax = 18; +constexpr int PcMinHoleSize = 21; +constexpr int PcAsyncIo = 53; +constexpr int PcPrioIo = 54; +constexpr int PcSyncIo = 55; +constexpr int PcAclExtended = 59; +constexpr int PcAclPathMax = 60; +constexpr int PcMacPresent = 63; +constexpr int PcAclNfs4 = 64; +constexpr int GuestAtSymlinkNofollow = 0x0200; + +int GuestFileError(int error) { + switch (error) { + case ENOENT: return GuestEnoent; + case EACCES: return 13; + case ENOTDIR: return 20; + case EINVAL: return GuestEinval; + case EPERM: return GuestEperm; + case EROFS: return 30; + case ENAMETOOLONG: return GuestEnametoolong; + case EIO: return GuestEio; +#ifdef ELOOP + case ELOOP: return GuestEloop; +#endif + default: return GuestEio; + } +} std::int64_t Fail(int error) { *__error_nid_postfix() = error; @@ -23,29 +121,573 @@ std::int64_t Fail(int error) { } int GuestError(const std::error_code& error) { - const int value = error.default_error_condition().value(); + const auto condition = error.default_error_condition(); + if (condition == std::errc::resource_unavailable_try_again) return GuestEagain; + if (condition == std::errc::resource_deadlock_would_occur) return GuestEdeadlk; + if (condition == std::errc::too_many_symbolic_link_levels) return GuestEloop; + if (condition == std::errc::filename_too_long) return GuestEnametoolong; + if (condition == std::errc::directory_not_empty) return GuestEnotempty; + if (condition == std::errc::value_too_large) return GuestEoverflow; + const int value = condition.value(); return value > 0 && value <= 34 ? value : GuestEio; } + +std::int64_t CopyLinkTarget(const std::string& target, char* buffer, std::size_t size) { + const auto count = std::min(size, target.size()); + if (count != 0 && buffer == nullptr) return Fail(GuestEfault); + if (count != 0) std::memcpy(buffer, target.data(), count); + return static_cast(count); +} + +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; +} + +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 (!File::WindowsSymbolicLink::Read(native, target, "readlink") && !File::WindowsSymbolicLink::ReadJunction(native, target, "readlink")) { + NotImplemented_nid_no_patch("readlink of a Windows reparse point other than a symbolic link"); + return -1; + } + return CopyLinkTarget(target, buffer, size); + } +#endif + std::error_code error; + const auto status = std::filesystem::symlink_status(native, error); + if (error) + return Fail(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)); + return CopyLinkTarget(target, buffer, size); +} + +bool HasLongComponent(std::string_view path) { + for (std::size_t start = 0; start < path.size();) { + const auto end = std::min(path.find('/', start), path.size()); + if (end - start > GuestNameMax) { + if (start == 0) return true; + std::filesystem::path prefix; + if (ResolvePathError_nid_no_patch(std::string(path.substr(0, start)).c_str(), prefix)) return false; + std::error_code error; + return std::filesystem::is_directory(prefix, error); + } + start = end + 1; + } + return false; +} + +bool IsDotOrDotDot(std::string_view path) { + while (!path.empty() && (path.back() == '/' || path.back() == '\\')) path.remove_suffix(1); + const auto separator = path.find_last_of("/\\"); + const auto component = path.substr(separator == std::string_view::npos ? 0 : separator + 1); + return component == "." || component == ".."; +} + +int RenamePath(const std::filesystem::path& source, const std::filesystem::path& destination) { + namespace fs = std::filesystem; + std::error_code error; + const auto sourceStatus = fs::symlink_status(source, error); + if (error && error != std::errc::no_such_file_or_directory) return FilesystemError(error); + if (sourceStatus.type() == fs::file_type::not_found) + return HasNonDirectoryPrefix(source) ? GuestEnotdir : GuestEnoent; + + const auto parent = destination.parent_path().empty() ? fs::path(".") : destination.parent_path(); + error.clear(); + const auto parentStatus = fs::status(parent, error); + if (error) return FilesystemError(error); + if (!fs::exists(parentStatus)) + return HasNonDirectoryPrefix(destination) ? GuestEnotdir : GuestEnoent; + if (!fs::is_directory(parentStatus)) return GuestEnotdir; + + error.clear(); + const auto destinationStatus = fs::symlink_status(destination, error); + if (error && error != std::errc::no_such_file_or_directory) return FilesystemError(error); + const bool destinationExists = destinationStatus.type() != fs::file_type::not_found; + const bool sourceIsDirectory = fs::is_directory(sourceStatus); + const bool destinationIsDirectory = destinationExists && fs::is_directory(destinationStatus); + bool sameFile = false; + if (destinationExists) { + error.clear(); + sameFile = fs::equivalent(source, destination, error); + if (error == std::errc::no_such_file_or_directory) error.clear(); + if (error) return FilesystemError(error); + if (!sameFile && sourceIsDirectory && !destinationIsDirectory) return GuestEnotdir; + if (!sameFile && !sourceIsDirectory && destinationIsDirectory) return GuestEisdir; + } + if (sourceIsDirectory && !sameFile) { + error.clear(); + const auto canonicalSource = fs::weakly_canonical(source, error); + if (error) return FilesystemError(error); + const auto canonicalDestination = fs::weakly_canonical(destination, error); + if (error) return FilesystemError(error); + const auto relative = canonicalDestination.lexically_relative(canonicalSource); + if (!relative.empty() && *relative.begin() != "..") return GuestEinval; + } + if (destinationExists && !sameFile && destinationIsDirectory) { + error.clear(); + if (!fs::is_empty(destination, error)) { + return error ? FilesystemError(error) : GuestEnotempty; + } + } +#ifdef _WIN32 + if (!sameFile && sourceIsDirectory && destinationIsDirectory) { + error.clear(); + if (!fs::remove(destination, error)) return error ? FilesystemError(error) : GuestEio; + } +#endif + + error.clear(); + fs::rename(source, destination, error); + return error ? FilesystemError(error) : 0; +} } extern "C" { +int APS5_VABI fork_nid_postfix(void) { + NotImplemented_nid_no_patch("fork: child processes"); + return -1; +} + +int APS5_VABI setsid_nid_postfix(void) { + return static_cast(Fail(GuestEperm)); +} + +std::uint16_t APS5_VABI umask_nid_postfix(std::uint16_t mask) { + return File::ExchangeCreationMask_nid_no_patch(mask); +} + +int APS5_VABI _fstatfs_nid_postfix(int d, void* buf); + +int APS5_VABI fstatfs_nid_postfix(int d, void* buf) { + return _fstatfs_nid_postfix(d, buf); +} + 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); + 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); - const auto native = ResolvePath_nid_no_patch(path); + if (path == nullptr) return Fail(GuestEfault); + if (*path == '\0') return Fail(GuestEnoent); + if (fd == GuestAtFdcwd || *path == '/') return readlink_nid_postfix(path, buffer, size); + int error = 0; + const char* base = GetDirectoryDescriptorPath_nid_no_patch(fd, &error); + if (base == nullptr) return Fail(error); + return ReadNativeLink(std::filesystem::path(reinterpret_cast(base)) / + std::filesystem::path(reinterpret_cast(path)), buffer, size); +} + +std::int64_t APS5_VABI pathconf_nid_postfix(const char* path, int name) { + if (path == nullptr) return Fail(GuestEfault); + if (*path == '\0') return Fail(GuestEnoent); + if (std::strlen(path) >= GuestPathMax || HasLongComponent(path)) return Fail(GuestEnametoolong); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return Fail(error); std::error_code error; - const auto status = std::filesystem::symlink_status(native, error); + const auto status = std::filesystem::status(native, error); if (error) return Fail(GuestError(error)); - if (status.type() == std::filesystem::file_type::not_found) return Fail(GuestEnoent); - if (!std::filesystem::is_symlink(status)) return Fail(GuestEinval); - const auto target = std::filesystem::read_symlink(native, error).generic_string(); - if (error) return Fail(GuestError(error)); - const auto count = std::min(size, target.size()); - if (count != 0 && buffer == nullptr) return Fail(GuestEfault); - std::memcpy(buffer, target.data(), count); - return static_cast(count); + if (!std::filesystem::exists(status)) return Fail(GuestEnoent); + switch (name) { + case PcLinkMax: return 32767; + case PcNameMax: return static_cast(GuestNameMax); + case PcPathMax: return static_cast(GuestPathMax); + case PcPipeBuf: return 512; + case PcChownRestricted: return 1; + case PcNoTrunc: return 1; + case PcFileSizeBits: return 64; + case PcRecMaxXferSize: return -1; + case PcSymlinkMax: return static_cast(GuestPathMax); + case PcPrioIo: return 0; + case PcSyncIo: return 0; + case PcMaxCanon: + case PcMaxInput: + case PcVdisable: + case PcAllocSizeMin: + case PcRecIncrXferSize: + case PcRecMinXferSize: + case PcRecXferAlign: + case PcMinHoleSize: + case PcAsyncIo: + case PcAclExtended: + case PcAclPathMax: + case PcMacPresent: + case PcAclNfs4: + throw std::runtime_error("pathconf: name " + std::to_string(name) + " depends on the console filesystem or kernel configuration"); + default: return Fail(GuestEinval); + } +} + +int APS5_VABI link_nid_postfix(const char* existing, const char* created) { + if (existing == nullptr || created == nullptr) return static_cast(Fail(GuestEfault)); + if (*existing == '\0' || *created == '\0') return static_cast(Fail(GuestEnoent)); + std::filesystem::path source, destination; + if (const int error = ResolvePathError_nid_no_patch(existing, source)) return static_cast(Fail(error)); + if (const int error = ResolvePathError_nid_no_patch(created, destination)) return static_cast(Fail(error)); + std::error_code error; + const auto status = std::filesystem::status(source, error); + if (!std::filesystem::exists(status)) return static_cast(Fail(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)); + const auto target = std::filesystem::is_symlink(source, error) ? std::filesystem::canonical(source, error) : source; + if (!error) std::filesystem::create_hard_link(target, destination, error); + if (error == std::errc::cross_device_link) return static_cast(Fail(GuestExdev)); + if (error) throw std::system_error(error, "link"); + RecordWrittenPath_nid_no_patch(destination); + return 0; +} + +int APS5_VABI fchdir_nid_postfix(int fd) { + int error = 0; + const char* path = GetDirectoryDescriptorPath_nid_no_patch(fd, &error); + if (path == nullptr) return static_cast(Fail(error)); +#ifndef _WIN32 + if (::faccessat(fd, ".", X_OK, AT_EACCESS) != 0) return static_cast(Fail(errno)); +#endif + error = SetWorkingDirectory_nid_no_patch(path); + if (error != 0) return static_cast(Fail(error)); + return 0; +} + +int APS5_VABI chroot_nid_postfix(const char* path) { + if (path == nullptr) return static_cast(Fail(GuestEfault)); + if (*path == '\0') return static_cast(Fail(GuestEnoent)); + if (std::strlen(path) >= GuestPathMax || HasLongComponent(path)) return static_cast(Fail(GuestEnametoolong)); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return static_cast(Fail(error)); + std::error_code error; + const auto status = std::filesystem::status(native, error); + if (status.type() == std::filesystem::file_type::not_found) return static_cast(Fail(HasNonDirectoryPrefix(native) ? GuestEnotdir : GuestEnoent)); + if (error) return static_cast(Fail(GuestError(error))); + if (!std::filesystem::is_directory(status)) return static_cast(Fail(GuestEnotdir)); + const bool root = std::filesystem::equivalent(native, ResolvePath_nid_no_patch("/"), error); + if (error) return static_cast(Fail(GuestError(error))); + if (root) return 0; + throw std::runtime_error(std::string("chroot: changing the root directory to ") + path + " is not supported, the guest path resolver keeps a fixed root"); +} + +int APS5_VABI symlink_nid_postfix(const char* target, const char* created) { + if (target == nullptr || created == nullptr) return static_cast(Fail(GuestEfault)); + if (*target == '\0' || *created == '\0') return static_cast(Fail(GuestEnoent)); + 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; +#ifdef _WIN32 + if (GetFileAttributesW(native.wstring().c_str()) != INVALID_FILE_ATTRIBUTES) return static_cast(Fail(GuestEexist)); +#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 bool directory = std::filesystem::is_directory(native.parent_path() / contents, error); +#ifdef _WIN32 + DWORD flags = SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE; + if (directory) flags |= SYMBOLIC_LINK_FLAG_DIRECTORY; + if (!CreateSymbolicLinkW(native.wstring().c_str(), std::filesystem::path(contents).make_preferred().wstring().c_str(), flags)) { + const DWORD code = GetLastError(); + if (code == ERROR_PRIVILEGE_NOT_HELD) return static_cast(Fail(GuestEperm)); + if (code == ERROR_ALREADY_EXISTS || code == ERROR_FILE_EXISTS) return static_cast(Fail(GuestEexist)); + if (code == ERROR_PATH_NOT_FOUND) return static_cast(Fail(GuestEnoent)); + throw std::system_error(static_cast(code), std::system_category(), "symlink"); + } +#else + if (directory) + std::filesystem::create_directory_symlink(contents, native, error); + else + std::filesystem::create_symlink(contents, native, error); + if (error) throw std::system_error(error, "symlink"); +#endif + RecordWrittenPath_nid_no_patch(native); + return 0; +} + +int APS5_VABI open_nid_postfix(const char* path, int flags, int mode); + +int APS5_VABI openat_nid_postfix(int fd, const char* path, int flags, ...) { + int mode = 0; + if (flags & SCE_KERNEL_O_CREAT) { +#ifdef _WIN32 + __builtin_sysv_va_list arguments; + __builtin_sysv_va_start(arguments, flags); + mode = __builtin_va_arg(arguments, int); + __builtin_sysv_va_end(arguments); +#else + std::va_list arguments; + va_start(arguments, flags); + mode = va_arg(arguments, int); + 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); + const int result = OpenInDirectory_nid_no_patch(fd, path, flags, static_cast(mode)); + if (result < 0) return static_cast(Fail(result & 0xffff)); + return result; +} + +int APS5_VABI mkdir_nid_postfix(const char* path, std::uint16_t mode); + +int APS5_VABI mkdirat_nid_postfix(int fd, const char* path, std::uint16_t mode) { + if (path == nullptr) return static_cast(Fail(GuestEfault)); + if (*path == '\0') return static_cast(Fail(GuestEnoent)); + if (fd == GuestAtFdcwd || *path == '/') return mkdir_nid_postfix(path, mode); + if (std::strlen(path) >= GuestPathMax || HasLongComponent(path)) return static_cast(Fail(GuestEnametoolong)); + int error = 0; + const char* base = GetDirectoryDescriptorPath_nid_no_patch(fd, &error); + if (base == nullptr) return static_cast(Fail(error)); + auto native = std::filesystem::path(reinterpret_cast(base)) / + std::filesystem::path(reinterpret_cast(path)); + while (!native.has_filename() && native.has_relative_path()) native = native.parent_path(); +#ifdef _WIN32 + if (!CreateDirectoryW(native.wstring().c_str(), nullptr)) { + const DWORD code = GetLastError(); + if (code == ERROR_ALREADY_EXISTS || code == ERROR_FILE_EXISTS) return static_cast(Fail(GuestEexist)); + if (code == ERROR_ACCESS_DENIED) return static_cast(Fail(GuestEperm)); + if (code == ERROR_FILENAME_EXCED_RANGE) return static_cast(Fail(GuestEnametoolong)); + if (code == ERROR_PATH_NOT_FOUND) { + if (HasNonDirectoryPrefix(native)) return static_cast(Fail(GuestEnotdir)); + return static_cast(Fail(GuestEnoent)); + } + return static_cast(Fail(GuestEio)); + } +#else + if (::mkdir(native.c_str(), static_cast(mode & 0777)) != 0) { + const int nativeError = errno; + if ((nativeError == ENOENT || nativeError == ENOTDIR) && HasNonDirectoryPrefix(native)) + return static_cast(Fail(GuestEnotdir)); + return static_cast(Fail(GuestError(std::error_code(nativeError, std::generic_category())))); + } +#endif + RecordWrittenPath_nid_no_patch(native); + return 0; +} + +int APS5_VABI fchmodat_nid_postfix(int fd, const char* path, int mode, int flag) { + if (flag != 0 && flag != GuestAtSymlinkNofollow) return static_cast(Fail(GuestEinval)); + if (path == nullptr) return static_cast(Fail(GuestEfault)); + if (*path == '\0') return static_cast(Fail(GuestEnoent)); + std::filesystem::path native; + if (fd == GuestAtFdcwd || *path == '/') { + if (const int error = ResolvePathError_nid_no_patch(path, native)) return static_cast(Fail(error)); + } else { + int error = 0; + const char* base = GetDirectoryDescriptorPath_nid_no_patch(fd, &error); + if (base == nullptr) return static_cast(Fail(error)); + native = std::filesystem::path(reinterpret_cast(base)) / std::filesystem::path(reinterpret_cast(path)); + } + if (flag == GuestAtSymlinkNofollow) { +#ifdef _WIN32 + const DWORD attributes = GetFileAttributesW(native.wstring().c_str()); + const bool symbolic = attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0; +#else + std::error_code status; + const bool symbolic = std::filesystem::is_symlink(std::filesystem::symlink_status(native, status)); +#endif + if (symbolic) { +#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))); +#else + if (::chmod(native.c_str(), static_cast(mode & 07777)) != 0) return static_cast(Fail(GuestFileError(errno))); +#endif + RecordWrittenPath_nid_no_patch(native); + return 0; +} + +int APS5_VABI 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); + +int APS5_VABI unlinkat_nid_postfix(int fd, const char* path, int flag) { + if (flag & ~GuestAtRemovedir) return static_cast(Fail(GuestEinval)); + if (path == nullptr) return static_cast(Fail(GuestEfault)); + if (*path == '\0') return static_cast(Fail(GuestEnoent)); + if (fd == GuestAtFdcwd || *path == '/') return (flag & GuestAtRemovedir) ? rmdir_nid_postfix(path) : unlink_nid_postfix(path); + if (std::strlen(path) >= GuestPathMax || HasLongComponent(path)) return static_cast(Fail(GuestEnametoolong)); + int error = 0; + const char* base = GetDirectoryDescriptorPath_nid_no_patch(fd, &error); + if (base == nullptr) return static_cast(Fail(error)); + auto native = std::filesystem::path(reinterpret_cast(base)) / + std::filesystem::path(reinterpret_cast(path)); + const bool trailingSlash = !native.has_filename() && native.has_relative_path(); + while (!native.has_filename() && native.has_relative_path()) native = native.parent_path(); + std::error_code statusError; + const auto symlinkStatus = std::filesystem::symlink_status(native, statusError); + if (flag & GuestAtRemovedir) { + if (IsDotOrDotDot(path)) return static_cast(Fail(GuestEinval)); + if (std::filesystem::is_symlink(symlinkStatus)) return static_cast(Fail(GuestEnotdir)); + if (!std::filesystem::is_directory(symlinkStatus)) { + if (HasNonDirectoryPrefix(native) || std::filesystem::exists(symlinkStatus)) + return static_cast(Fail(GuestEnotdir)); + return static_cast(Fail(GuestEnoent)); + } + if (!std::filesystem::is_empty(native, statusError)) return static_cast(Fail(GuestEnotempty)); +#ifdef _WIN32 + struct _stat64 status{}; + const bool unlocked = ::_wstat64(native.c_str(), &status) == 0 && !(status.st_mode & _S_IWRITE) && + ::_wchmod(native.c_str(), _S_IREAD | _S_IWRITE) == 0; + if (!std::filesystem::remove(native, statusError)) { + if (unlocked) ::_wchmod(native.c_str(), _S_IREAD); + return static_cast(Fail(GuestEio)); + } +#else + if (::rmdir(native.c_str()) != 0) { + const int nativeError = errno; + if ((nativeError == ENOENT || nativeError == ENOTDIR) && HasNonDirectoryPrefix(native)) + return static_cast(Fail(GuestEnotdir)); + return static_cast(Fail(GuestError(std::error_code(nativeError, std::generic_category())))); + } +#endif + } else { + if (trailingSlash) { + if (std::filesystem::is_directory(symlinkStatus)) return static_cast(Fail(GuestEperm)); + if (std::filesystem::exists(symlinkStatus) || HasNonDirectoryPrefix(native)) + return static_cast(Fail(GuestEnotdir)); + return static_cast(Fail(GuestEnoent)); + } + if (std::filesystem::is_directory(symlinkStatus)) return static_cast(Fail(GuestEperm)); +#ifdef _WIN32 + struct _stat64 status{}; + const bool unlocked = ::_wstat64(native.c_str(), &status) == 0 && (status.st_mode & _S_IFMT) == _S_IFREG && + !(status.st_mode & _S_IWRITE) && ::_wchmod(native.c_str(), _S_IREAD | _S_IWRITE) == 0; + if (::_wunlink(native.c_str()) != 0) { + const int nativeError = errno; + if (unlocked) ::_wchmod(native.c_str(), _S_IREAD); + if (HasNonDirectoryPrefix(native)) return static_cast(Fail(GuestEnotdir)); + return static_cast(Fail(GuestError(std::error_code(nativeError, std::generic_category())))); + } +#else + if (::unlink(native.c_str()) != 0) { + const int nativeError = errno; + if ((nativeError == ENOENT || nativeError == ENOTDIR) && HasNonDirectoryPrefix(native)) + return static_cast(Fail(GuestEnotdir)); + return static_cast(Fail(GuestError(std::error_code(nativeError, std::generic_category())))); + } +#endif + } + RecordWrittenPath_nid_no_patch(native); + return 0; +} + +int APS5_VABI renameat_nid_postfix(int oldfd, const char* from, int newfd, const char* to) { + if (from == nullptr || to == nullptr) return static_cast(Fail(GuestEfault)); + if (*from == '\0' || *to == '\0') return static_cast(Fail(GuestEnoent)); + if ((oldfd == GuestAtFdcwd || *from == '/') && (newfd == GuestAtFdcwd || *to == '/')) + return RenameGuestPath_nid_no_patch(from, to); + if (std::strlen(from) >= GuestPathMax || HasLongComponent(from) || + std::strlen(to) >= GuestPathMax || HasLongComponent(to)) + return static_cast(Fail(GuestEnametoolong)); + if (IsDotOrDotDot(from) || IsDotOrDotDot(to)) return static_cast(Fail(GuestEinval)); + + std::filesystem::path source; + if (oldfd == GuestAtFdcwd || *from == '/') { + if (const int error = ResolvePathError_nid_no_patch(from, source)) return static_cast(Fail(error)); + } else { + int error = 0; + const char* base = GetDirectoryDescriptorPath_nid_no_patch(oldfd, &error); + if (base == nullptr) return static_cast(Fail(error)); + source = std::filesystem::path(reinterpret_cast(base)) / + std::filesystem::path(reinterpret_cast(from)); + } + + std::filesystem::path destination; + if (newfd == GuestAtFdcwd || *to == '/') { + if (const int error = ResolvePathError_nid_no_patch(to, destination)) return static_cast(Fail(error)); + } else { + int error = 0; + const char* base = GetDirectoryDescriptorPath_nid_no_patch(newfd, &error); + if (base == nullptr) return static_cast(Fail(error)); + destination = std::filesystem::path(reinterpret_cast(base)) / + std::filesystem::path(reinterpret_cast(to)); + } + + const std::string_view fromView(from); + const std::string_view toView(to); + const bool oldTrailingSlash = !fromView.empty() && fromView.back() == '/'; + const bool newTrailingSlash = !toView.empty() && toView.back() == '/'; + + while (!source.has_filename() && source.has_relative_path()) source = source.parent_path(); + while (!destination.has_filename() && destination.has_relative_path()) destination = destination.parent_path(); + + std::error_code statusError; + const auto sourceStatus = std::filesystem::symlink_status(source, statusError); + if (oldTrailingSlash) { + if (sourceStatus.type() == std::filesystem::file_type::not_found) + return static_cast(Fail(HasNonDirectoryPrefix(source) ? GuestEnotdir : GuestEnoent)); + if (!std::filesystem::is_directory(sourceStatus)) return static_cast(Fail(GuestEnotdir)); + } + if (newTrailingSlash && sourceStatus.type() != std::filesystem::file_type::not_found && + !std::filesystem::is_directory(sourceStatus)) + return static_cast(Fail(GuestEnotdir)); + + const int error = RenamePath(source, destination); + if (error) return static_cast(Fail(error)); + RecordWrittenPath_nid_no_patch(source); + RecordWrittenPath_nid_no_patch(destination); + return 0; } } diff --git a/core/libs/prx/libmdbg_syscore/Export.cpp b/core/libs/prx/libmdbg_syscore/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libmdbg_syscore/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/tests/ApertureSources.cpp b/core/libs/tests/ApertureSources.cpp new file mode 100644 index 0000000000..30fa83ebe4 --- /dev/null +++ b/core/libs/tests/ApertureSources.cpp @@ -0,0 +1,107 @@ +#include "IntermediateRepresentation/IrProgram.hpp" +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" +#include "RdnaDecoder/RdnaScalarOpDecoder.hpp" +#include "Translation/TranslationContext.hpp" +#include +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +namespace { + +struct ApertureSource { + std::uint32_t code; + RdnaOperandKind kind; + const char* name; + std::uint32_t low; + std::uint32_t high; +}; + +const std::array ApertureSources{{ + {235u, RdnaOperandKind::SrcSharedBase, "src_shared_base", 0x00000000u, 0x80000000u}, + {236u, RdnaOperandKind::SrcSharedLimit, "src_shared_limit", 0xffffffffu, 0x80000000u}, + {237u, RdnaOperandKind::SrcPrivateBase, "src_private_base", 0x00000000u, 0x70000000u}, + {238u, RdnaOperandKind::SrcPrivateLimit, "src_private_limit", 0xffffffffu, 0x70000000u}, +}}; + +constexpr std::uint32_t MovB32 = 0x03u; +constexpr std::uint32_t MovB64 = 0x04u; + +void Require(bool value, const std::string& message) { + if (!value) throw std::runtime_error(message); +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +RdnaInstruction ScalarMove(std::uint32_t opcode, std::uint32_t source) { + const std::array words{0x80000000u | (0x7du << 23u) | (opcode << 8u) | source, 0u}; + return DecodeRdnaSop1(0u, words, 0u); +} + +std::map TranslateWrites(const RdnaInstruction& instruction) { + IrProgram program; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder() = {&block}; + program.Metadata().blockInfo.resize(1); + TranslationContext context(program, block, 256); + context.TranslateInstruction(instruction); + std::map writes; + for (const IrValue* value : block.Instructions()) { + if (value->Opcode() != IrOpcode::SetScalarRegister) continue; + const IrValue* written = value->Argument(1u)->Resolve(); + Require(written->HasImmediate(), "the write of s" + std::to_string(value->Argument(0u)->Register().index) + " is not a constant"); + writes[value->Argument(0u)->Register().index] = written->ImmediateU32(); + } + return writes; +} + +void CheckDecode() { + for (const auto& source : ApertureSources) { + const auto operand = DecodeRdnaScalarSource(source.code, 0u); + Require(operand.kind == source.kind, + "scalar source " + std::to_string(source.code) + " decoded to the wrong operand kind"); + Require(RdnaOperandToString(operand) == source.name, + "scalar source " + std::to_string(source.code) + " is named " + RdnaOperandToString(operand)); + } +} + +void CheckWord(const std::map& writes, std::uint32_t reg, std::uint32_t expected, const std::string& read) { + const auto found = writes.find(reg); + Require(found != writes.end(), read + " did not write s" + std::to_string(reg)); + Require(found->second == expected, read + " wrote " + Hex(found->second) + " to s" + std::to_string(reg) + ", expected " + Hex(expected)); +} + +void CheckReadValues() { + for (const auto& source : ApertureSources) { + const RdnaInstruction narrow = ScalarMove(MovB32, source.code); + Require(narrow.op == RdnaOpcode::SMovB32, "the 32-bit scalar move decoded to another opcode"); + Require(narrow.source0.kind == source.kind, "the 32-bit scalar move decodes source0 as the wrong operand kind"); + const auto narrowWrites = TranslateWrites(narrow); + CheckWord(narrowWrites, 0u, source.low, std::string("s_mov_b32 of ") + source.name); + Require(narrowWrites.size() == 1u, std::string("s_mov_b32 of ") + source.name + " wrote more than one register"); + const RdnaInstruction wide = ScalarMove(MovB64, source.code); + Require(wide.op == RdnaOpcode::SMovB64, "the 64-bit scalar move decoded to another opcode"); + Require(wide.source0.kind == source.kind, "the 64-bit scalar move decodes source0 as the wrong operand kind"); + const auto wideWrites = TranslateWrites(wide); + CheckWord(wideWrites, 0u, source.low, std::string("s_mov_b64 of ") + source.name); + CheckWord(wideWrites, 1u, source.high, std::string("s_mov_b64 of ") + source.name); + Require(wideWrites.size() == 2u, std::string("s_mov_b64 of ") + source.name + " wrote more than two registers"); + } +} + +} + +int main() { + CheckDecode(); + CheckReadValues(); + std::puts("aperture sources tests passed"); + 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 7ea5cec5bc..be99345d50 100644 --- a/core/libs/tests/ApplicationHeap.cpp +++ b/core/libs/tests/ApplicationHeap.cpp @@ -8,9 +8,23 @@ #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); 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; @@ -21,24 +35,86 @@ void APS5_VABI _ZdlPvmSt11align_val_t_nid_postfix(void*, std::size_t, std::size_ void* ApplicationHeapRealign_nid_no_patch(void*, std::size_t, std::size_t); char* APS5_VABI strdup_nid_postfix(const char*); 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 @@ -49,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"); } @@ -101,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()); @@ -123,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()); @@ -147,6 +307,30 @@ int main(int argc, char** argv) { pointer = static_cast(ApplicationHeapReallocate_nid_no_patch(nullptr, 32)); require(pointer != nullptr); ApplicationHeapFree_nid_no_patch(pointer); + pointer = static_cast(reallocf_nid_postfix(nullptr, 24)); + require(pointer != nullptr && ApplicationHeapUsableSize_nid_no_patch(pointer) == 24); + std::memset(pointer, 0x3c, 24); + pointer = static_cast(reallocf_nid_postfix(pointer, 200)); + require(pointer != nullptr && ApplicationHeapUsableSize_nid_no_patch(pointer) == 200); + for (unsigned i = 0; i < 24; ++i) require(pointer[i] == 0x3c); + pointer = static_cast(reallocf_nid_postfix(pointer, 9)); + require(pointer != nullptr && ApplicationHeapUsableSize_nid_no_patch(pointer) == 9); + for (unsigned i = 0; i < 9; ++i) require(pointer[i] == 0x3c); + *__error_nid_postfix() = 0; + require(reallocf_nid_postfix(pointer, 0) == nullptr && *__error_nid_postfix() == 0); + reject([pointer] { ApplicationHeapUsableSize_nid_no_patch(pointer); }); + pointer = static_cast(reallocf_nid_postfix(nullptr, 40)); + require(pointer != nullptr); + require(reallocf_nid_postfix(pointer, SIZE_MAX - 4096) == nullptr && *__error_nid_postfix() == 12); + reject([pointer] { ApplicationHeapUsableSize_nid_no_patch(pointer); }); + *__error_nid_postfix() = 0; + require(reallocf_nid_postfix(nullptr, SIZE_MAX) == nullptr && *__error_nid_postfix() == 12); + void* empty = ApplicationHeapAllocate_nid_no_patch(0); + require(empty != nullptr); + ApplicationHeapFree_nid_no_patch(empty); + empty = ApplicationHeapReallocate_nid_no_patch(nullptr, 0); + require(empty != nullptr); + ApplicationHeapFree_nid_no_patch(empty); for (std::size_t alignment : {16, 64, 4096}) { auto* aligned = ApplicationHeapAlign_nid_no_patch(alignment, 37); require(reinterpret_cast(aligned) % alignment == 0); @@ -156,6 +340,9 @@ int main(int argc, char** argv) { require(ApplicationHeapPosixAlign_nid_no_patch(&aligned, 256, 99) == 0); require(reinterpret_cast(aligned) % 256 == 0); ApplicationHeapFree_nid_no_patch(aligned); + void* refused = nullptr; + require(ApplicationHeapPosixAlign_nid_no_patch(&refused, 256, SIZE_MAX) == 12 && refused == nullptr); + require(posix_memalign_nid_postfix(&refused, 4096, SIZE_MAX - 4096) == 12 && refused == nullptr); ApplicationHeapFree_nid_no_patch(nullptr); reject([] { ApplicationHeapCalloc_nid_no_patch(SIZE_MAX, 2); }); reject([] { ApplicationHeapAlign_nid_no_patch(3, 16); }); @@ -207,7 +394,14 @@ int main(int argc, char** argv) { require(frees == 6); _ZdlPvmSt11align_val_t_nid_postfix(nullptr, 37, 64); require(frees == 6); + require(reallocf_nid_postfix(storage.data(), 80) == storage.data() && lastSize == 80 && frees == 6); + require(reallocf_nid_postfix(storage.data(), 16) == storage.data() && lastSize == 16 && frees == 6); + require(reallocf_nid_postfix(storage.data(), 0) == nullptr && lastSize == 16 && frees == 7); fail = true; + *__error_nid_postfix() = 0; + require(reallocf_nid_postfix(storage.data(), 48) == nullptr && lastSize == 48 && frees == 8 && *__error_nid_postfix() == 12); + reject([] { ApplicationHeapReallocate_nid_no_patch(storage.data(), 48); }); + require(frees == 8); require(_ZnwmRKSt9nothrow_t_nid_postfix(8, nullptr) == nullptr); require(_ZnamRKSt9nothrow_t_nid_postfix(8, nullptr) == nullptr); reject([] { _Znwm_nid_postfix(8); }); @@ -260,6 +454,12 @@ int main(int argc, char** argv) { copy = strndup_nid_postfix(text, 5); require(lastSize == 6 && std::strcmp(copy, "guest") == 0); ApplicationHeapFree_nid_no_patch(copy); + char* directory = getcwd_nid_postfix(nullptr, 0); + 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 == std::strlen(directory) + 1); + ApplicationHeapFree_nid_no_patch(directory); fail = true; *__error_nid_postfix() = 0; require(strdup_nid_postfix(text) == nullptr && *__error_nid_postfix() == 12); @@ -267,5 +467,27 @@ int main(int argc, char** argv) { require(strdup_nid_postfix("") == nullptr && *__error_nid_postfix() == 12); *__error_nid_postfix() = 0; require(strndup_nid_postfix(text, 5) == nullptr && *__error_nid_postfix() == 12); + *__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 42d97d01d6..1345d218d1 100644 --- a/core/libs/tests/ApplicationHeapStats.cpp +++ b/core/libs/tests/ApplicationHeapStats.cpp @@ -8,6 +8,7 @@ extern "C" { int APS5_VABI malloc_stats_fast_nid_postfix(void*); +std::size_t APS5_VABI malloc_usable_size_nid_postfix(void*); } namespace { @@ -46,12 +47,29 @@ int APS5_VABI statsFast(void* stats) { return 0x2a; } +unsigned usableCalls = 0; +void* lastUsable = nullptr; + +std::size_t APS5_VABI usableSize(void* pointer) { + ++usableCalls; + lastUsable = pointer; + return 0x55; +} + template void write(std::array& data, std::size_t offset, TValue value) { require(offset <= data.size() && sizeof(value) <= data.size() - offset); 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) { @@ -75,19 +93,47 @@ int main(int argc, char** argv) { write(replacement, 0x48, &realign); write(replacement, 0x50, &posixAlign); write(replacement, 0x60, &statsFast); + write(replacement, 0x68, &usableSize); if (argc > 1 && std::strcmp(argv[1], "default") == 0) { 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) { write(replacement, 0x60, static_cast(nullptr)); + write(replacement, 0x68, static_cast(nullptr)); ApplicationHeapInitialize_nid_no_patch(process.data()); require(ApplicationHeapAllocate_nid_no_patch(16) == storage.data()); reject([&] { malloc_stats_fast_nid_postfix(stats.data()); }); - require(statsCalls == 0); + reject([&] { malloc_usable_size_nid_postfix(storage.data()); }); + reject([&] { malloc_usable_size_nid_postfix(nullptr); }); + require(statsCalls == 0 && usableCalls == 0); return 0; } ApplicationHeapInitialize_nid_no_patch(process.data()); @@ -99,4 +145,6 @@ int main(int argc, char** argv) { require(statsCalls == 3); require(malloc_stats_fast_nid_postfix(stats.data()) == 0x2a && statsCalls == 4); require(ApplicationHeapAllocate_nid_no_patch(16) == storage.data()); + require(malloc_usable_size_nid_postfix(storage.data()) == 0x55 && usableCalls == 1 && lastUsable == storage.data()); + require(malloc_usable_size_nid_postfix(nullptr) == 0x55 && usableCalls == 2 && lastUsable == nullptr); } diff --git a/core/libs/tests/BitfieldMask.cpp b/core/libs/tests/BitfieldMask.cpp new file mode 100644 index 0000000000..fd5e851a11 --- /dev/null +++ b/core/libs/tests/BitfieldMask.cpp @@ -0,0 +1,83 @@ +#include "Translation/TranslationContext.hpp" +#include "Optimization/ConstantFolder.hpp" +#include +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +namespace { + +std::uint32_t expectedMask(std::uint32_t width, std::uint32_t offset) { + width &= 31u; + offset &= 31u; + std::uint32_t mask = 0u; + for (std::uint32_t bit = offset; bit < 32u && bit - offset < width; ++bit) { + mask |= 1u << bit; + } + return mask; +} + +void check(RdnaOpcode opcode, std::uint32_t width, std::uint32_t offset) { + const bool scalar = opcode == RdnaOpcode::SBfmB32; + IrProgram program; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + RdnaInstruction instruction{}; + instruction.op = opcode; + instruction.family = scalar ? RdnaInstructionFamily::SOP2 : RdnaInstructionFamily::VOP3; + instruction.sourceCount = 2u; + instruction.source0.kind = RdnaOperandKind::LiteralConstant; + instruction.source0.value = width; + instruction.source1.kind = RdnaOperandKind::LiteralConstant; + instruction.source1.value = offset; + instruction.destination.kind = scalar ? RdnaOperandKind::ScalarRegister : RdnaOperandKind::VectorRegister; + instruction.destination.reg = 10u; + TranslationContext context(program, block, 256u); + context.TranslateInstruction(instruction); + ConstantFolder{}.Fold(program); + const IrValue* result = nullptr; + for (const auto* value : block.Instructions()) { + if (value->Opcode() == IrOpcode::SetScc) { + throw std::runtime_error("bitfield mask changed SCC"); + } + if (value->Opcode() != (scalar ? IrOpcode::SetScalarRegister : IrOpcode::SetVectorRegister)) continue; + result = value->Argument(1u)->Resolve(); + } + if (result == nullptr) throw std::runtime_error("bitfield mask did not write its destination"); + if (!scalar) { + if (result->Opcode() != IrOpcode::SelectU32) throw std::runtime_error("bitfield mask lost EXEC masking"); + result = result->Argument(1u)->Resolve(); + } + if (!result->HasImmediate() || result->ImmediateU32() != expectedMask(width, offset)) { + throw std::runtime_error(std::string(scalar ? "s_bfm_b32" : "v_bfm_b32") + + " width=" + std::to_string(width) + " offset=" + std::to_string(offset) + " did not fold to the expected mask"); + } +} + +} + +int main() { + try { + const std::array upperBits{0u, 32u, 0x80000000u, 0xffffffe0u}; + for (const auto opcode : {RdnaOpcode::SBfmB32, RdnaOpcode::VBfmB32}) { + for (std::uint32_t width = 0u; width < 32u; ++width) { + for (std::uint32_t offset = 0u; offset < 32u; ++offset) { + for (const auto widthBits : upperBits) { + for (const auto offsetBits : upperBits) { + check(opcode, width | widthBits, offset | offsetBits); + } + } + } + } + } + std::puts("32768 bitfield mask translation cases passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} 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 a6d0715d8c..54111f196c 100644 --- a/core/libs/tests/ExceptionRuntime.cpp +++ b/core/libs/tests/ExceptionRuntime.cpp @@ -9,9 +9,16 @@ #include #include #include +#include +#include +#include +#include #include #include #include +#ifdef __linux__ +#include +#endif #include extern "C" unsigned __cxa_uncaught_exceptions_nid_postfix(); @@ -24,18 +31,25 @@ 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)); extern "C" _Unwind_Reason_Code _Unwind_Backtrace_nid_postfix(_Unwind_Trace_Fn, void*); extern "C" _Unwind_Reason_Code _Unwind_ForcedUnwind_nid_postfix(_Unwind_Exception*, _Unwind_Stop_Fn, void*); extern "C" std::uintptr_t _Unwind_GetIP_nid_postfix(_Unwind_Context*); +extern "C" std::uintptr_t _Unwind_GetGR_nid_postfix(_Unwind_Context*, int); extern "C" void (*_ZSt13set_terminatePFvvE_nid_postfix(void(*)()))(); thread_local int destroyed = 0; struct Guard { ~Guard() { assert(__cxa_uncaught_exceptions_nid_postfix() > 0); ++destroyed; } }; +thread_local int counted = 0; +struct Counted { + ~Counted() { ++counted; } +}; struct Base { virtual ~Base() = default; int value = 7; }; struct Other { virtual ~Other() = default; int padding = 9; }; struct Derived : Other, Base {}; @@ -156,6 +170,30 @@ void CheckTerminate(void (*function)()) { assert(waitpid(child, &status, 0) == child); assert(WIFEXITED(status) && WEXITSTATUS(status) == 61); } +void CheckDefaultTerminateMessage() { + const std::string message = std::string(3000, 'w') + " end of message"; + int channel[2]; + assert(pipe(channel) == 0); + pid_t child = fork(); + assert(child >= 0); + 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]); + std::string output; + char buffer[4096]; + for (ssize_t count; (count = read(channel[0], buffer, sizeof(buffer))) > 0;) output.append(buffer, static_cast(count)); + close(channel[0]); + int status; + assert(waitpid(child, &status, 0) == child); + assert(WIFSIGNALED(status) && WTERMSIG(status) == SIGABRT); + assert(output.find(" what(): " + message + "\n") != std::string::npos); +} struct ForcedGuard { ~ForcedGuard() { ++destroyed; } }; [[gnu::noinline]] void ForceUnwind() { ForcedGuard guard; @@ -188,6 +226,11 @@ int main() { try { __cxa_rethrow_primary_exception_nid_postfix(retained); assert(false); } catch (int value) { assert(value == 27); } __cxa_decrement_exception_refcount_nid_postfix(retained); + { + auto made = std::make_exception_ptr(Counted()); + try { std::rethrow_exception(made); assert(false); } catch (const Counted&) {} + } + assert(counted == 2); Derived object; Base* pointer = &object; try { throw &object; } catch (Base* value) { assert(value == pointer); } @@ -211,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); } @@ -222,6 +269,7 @@ int main() { CheckThreads(); CheckTerminate(UncaughtThrow); CheckTerminate(NoexceptThrow); + CheckDefaultTerminateMessage(); CheckForcedUnwind(); int frames = 0; auto backtrace = _Unwind_Backtrace_nid_postfix([](_Unwind_Context* context, void* argument) { @@ -230,5 +278,25 @@ int main() { return _URC_NO_REASON; }, &frames); assert(backtrace == _URC_END_OF_STACK && frames >= 2); + frames = 0; + backtrace = _Unwind_Backtrace_nid_postfix([](_Unwind_Context* context, void* argument) { + const auto* slots = *reinterpret_cast(context); + const auto validRegister = reinterpret_cast(slots[2]); + const auto getRegister = reinterpret_cast(slots[3]); + const auto setRegister = reinterpret_cast(slots[4]); + const auto validFloatRegister = reinterpret_cast(slots[5]); + const auto isSignalFrame = reinterpret_cast(slots[11]); + assert(validRegister(context, -1) && validRegister(context, -2) && validRegister(context, 16) && !validRegister(context, 17)); + assert(getRegister(context, -1) == _Unwind_GetIP_nid_postfix(context) && getRegister(context, -1) != 0); + assert(getRegister(context, -2) == _Unwind_GetGR_nid_postfix(context, 7)); + const auto rbx = getRegister(context, 3); + setRegister(context, 3, rbx + 1); + assert(_Unwind_GetGR_nid_postfix(context, 3) == rbx + 1); + setRegister(context, 3, rbx); + assert(!validFloatRegister(context, 17) && !isSignalFrame(context)); + ++*static_cast(argument); + return _URC_NO_REASON; + }, &frames); + assert(backtrace == _URC_END_OF_STACK && frames >= 2); std::puts("exception runtime tests passed"); } diff --git a/core/libs/tests/Fiber.cpp b/core/libs/tests/Fiber.cpp index 707a59a80c..73b6a9afb1 100644 --- a/core/libs/tests/Fiber.cpp +++ b/core/libs/tests/Fiber.cpp @@ -6,6 +6,9 @@ #include #include #include +#ifdef _WIN32 +#include +#endif using Entry = void (APS5_VABI*)(std::uint64_t, std::uint64_t); @@ -18,6 +21,7 @@ std::int32_t APS5_VABI sceFiberReturnToThread(std::uint64_t, std::uint64_t*); std::int32_t APS5_VABI sceFiberGetSelf(FiberObject**); std::int32_t APS5_VABI sceFiberGetInfo(FiberObject*, FiberInfo*); std::int32_t APS5_VABI sceFiberGetThreadFramePointerAddress(std::uint64_t*); +std::int32_t APS5_VABI _sceFiberGetThreadFramePointerAddress(std::uint64_t*); std::int32_t APS5_VABI sceFiberStartContextSizeCheck(std::uint32_t); std::int32_t APS5_VABI sceFiberStopContextSizeCheck(void); } @@ -47,17 +51,38 @@ FiberStorage g_secondStorage; auto* const g_first = reinterpret_cast(&g_firstStorage); auto* const g_second = reinterpret_cast(&g_secondStorage); +[[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"); + Require(info.size_context == expectedSize, "reported context keeps the requested size"); + volatile std::uint64_t marker = 0; + const auto address = reinterpret_cast(&marker); + const auto low = reinterpret_cast(info.addr_context); + const auto high = low + info.size_context; + Require(address >= low && address < high, "reported fiber context contains the executing stack"); + for (auto cursor = address; cursor < high; cursor += sizeof(std::uint64_t)) { + const volatile auto word = *reinterpret_cast(cursor); + (void)word; + } +} + void APS5_VABI FirstEntry(std::uint64_t argOnInitialize, std::uint64_t argOnRun) { Require(argOnInitialize == 11 && argOnRun == 100, "first fiber arguments"); FiberObject* self = nullptr; Require(sceFiberGetSelf(&self) == 0 && self == g_first, "first fiber self"); + CheckActiveContext(g_first, g_firstContext.size()); std::uint64_t framePointer = 0; Require(sceFiberGetThreadFramePointerAddress(nullptr) == FiberErrorNull, "frame pointer null"); Require(sceFiberGetThreadFramePointerAddress(&framePointer) == 0 && framePointer == g_threadFramePointer, "thread frame pointer"); + framePointer = 0; + Require(_sceFiberGetThreadFramePointerAddress(nullptr) == FiberErrorNull, "underscore frame pointer null"); + Require(_sceFiberGetThreadFramePointerAddress(&framePointer) == 0 && framePointer == g_threadFramePointer, "underscore thread frame pointer"); volatile double carried = 1.5; std::uint64_t received = 0; Require(sceFiberSwitch(g_second, 200, &received) == 0, "switch to second fiber"); Require(received == 300 && carried == 1.5, "switch back to first fiber"); + CheckActiveContext(g_first, g_firstContext.size()); for (;;) { Require(sceFiberReturnToThread(received + 1, &received) == 0, "first fiber return"); } @@ -65,10 +90,12 @@ void APS5_VABI FirstEntry(std::uint64_t argOnInitialize, std::uint64_t argOnRun) void APS5_VABI SecondEntry(std::uint64_t argOnInitialize, std::uint64_t argOnRun) { Require(argOnInitialize == 22 && argOnRun == 200, "second fiber arguments"); + CheckActiveContext(g_second, g_secondContext.size()); Require(sceFiberSwitch(g_second, 0, nullptr) == FiberErrorState, "switch to self"); _mm_setcsr(_mm_getcsr() | 0x8000u); std::uint64_t received = 0; Require(sceFiberReturnToThread(250, &received) == 0 && received == 260, "second fiber resumed on another thread"); + CheckActiveContext(g_second, g_secondContext.size()); Require((_mm_getcsr() & 0x8000u) != 0, "second fiber keeps its MXCSR"); Require(sceFiberSwitch(g_first, 300, nullptr) == 0, "switch to first fiber"); Require(false, "second fiber resumed after its last switch"); @@ -81,6 +108,133 @@ void APS5_VABI CheckedEntry(std::uint64_t, std::uint64_t) { for (;;) sceFiberReturnToThread(0, &received); } +constexpr std::size_t SmallContextSize = 2048; +constexpr unsigned char SmallGuardFill = 0xa5; +alignas(16) std::array g_smallArea; +FiberStorage g_smallStorage; + +void APS5_VABI SmallEntry(std::uint64_t, std::uint64_t) { + std::uint64_t received = 0; + for (;;) sceFiberReturnToThread(0, &received); +} + +void RunSmallContextFirst() { + auto* small = reinterpret_cast(&g_smallStorage); + auto* context = g_smallArea.data() + g_smallArea.size() - SmallContextSize; + g_smallArea.fill(SmallGuardFill); + Require(sceFiberReturnToThread(0, nullptr) == FiberErrorPermission, "return to thread outside a fiber"); + Require(_sceFiberInitializeImpl_nid_postfix(small, "small", SmallEntry, 0, context, SmallContextSize, nullptr, 0) == 0, "initialize small"); + Require(sceFiberRun_nid_postfix(small, 0, nullptr) == 0, "first fiber run of the process on a 2 KiB context"); + Require(sceFiberRun_nid_postfix(small, 0, nullptr) == 0, "resume on a 2 KiB context"); + Require(sceFiberFinalize(small) == 0, "finalize small"); + for (auto* byte = g_smallArea.data(); byte != context; ++byte) { + Require(*byte == SmallGuardFill, "nothing is written below a 2 KiB context"); + } +} + +constexpr std::size_t HostCallContextSize = 16 * 1024; +constexpr std::size_t HostCallStack = 256 * 1024; +constexpr std::size_t HostCallFrameSize = 4096; +constexpr unsigned char HostCallGuardFill = 0x5c; +alignas(16) std::array g_hostCallArea; +FiberStorage g_hostCallStorage; + +std::size_t DescendHostStack(std::size_t remaining); +std::size_t (*volatile g_descendHostStack)(std::size_t) = DescendHostStack; + +std::size_t DescendHostStack(std::size_t remaining) { + volatile unsigned char frame[HostCallFrameSize]; + frame[0] = 0x11; + frame[HostCallFrameSize - 1] = 0x22; + const std::size_t frames = remaining > HostCallFrameSize ? g_descendHostStack(remaining - HostCallFrameSize) : 0; + Require(frame[0] == 0x11 && frame[HostCallFrameSize - 1] == 0x22, "host call frame intact"); + return frames + 1; +} + +#ifdef _WIN32 +void CheckWindowsHostStack() { + const auto* tib = reinterpret_cast(NtCurrentTeb()); + const auto low = reinterpret_cast(tib->StackLimit); + const auto high = reinterpret_cast(tib->StackBase); + const volatile unsigned char marker = 0; + const auto frame = reinterpret_cast(&marker); + Require(frame > low && frame < high && high - low >= HostCallContextSize + HostCallStack, "the thread's stack bounds are the fiber's host stack"); +} +#endif + +void APS5_VABI HostCallEntry(std::uint64_t, std::uint64_t) { + CheckActiveContext(reinterpret_cast(&g_hostCallStorage), HostCallContextSize); +#ifdef _WIN32 + CheckWindowsHostStack(); +#endif + Require(g_descendHostStack(HostCallStack) == HostCallStack / HostCallFrameSize, "host call depth"); + std::uint64_t received = 0; + 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; + g_hostCallArea.fill(HostCallGuardFill); + Require(_sceFiberInitializeImpl_nid_postfix(fiber, "host call", HostCallEntry, 0, context, HostCallContextSize, nullptr, 0) == 0, "initialize host call fiber"); + Require(sceFiberRun_nid_postfix(fiber, 0, nullptr) == 0, "run a 256 KiB host call from a 16 KiB fiber"); + Require(sceFiberFinalize(fiber) == 0, "finalize host call fiber"); + for (auto* byte = g_hostCallArea.data(); byte != context; ++byte) { + Require(*byte == HostCallGuardFill, "a host call from a 16 KiB fiber writes nothing below its context"); + } +} + +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); @@ -91,6 +245,7 @@ void APS5_VABI CheckedEntry(std::uint64_t, std::uint64_t) { } int main() { + RunSmallContextFirst(); FiberObject* self = nullptr; Require(sceFiberGetSelf(&self) == FiberErrorPermission, "no fiber on the thread"); Require(_sceFiberInitializeImpl_nid_postfix(g_first, "first", FirstEntry, 11, g_firstContext.data(), g_firstContext.size(), nullptr, 0) == 0, "initialize first"); @@ -99,6 +254,7 @@ int main() { std::uint64_t returned = 0; std::uint64_t framePointer = 0; Require(sceFiberGetThreadFramePointerAddress(&framePointer) == FiberErrorPermission, "frame pointer outside a fiber"); + Require(_sceFiberGetThreadFramePointerAddress(&framePointer) == FiberErrorPermission, "underscore frame pointer outside a fiber"); Require(RunFirst(&returned) == 0 && returned == 250, "run first until second returns"); Require(_mm_getcsr() == threadCsr, "thread MXCSR restored after the fibers"); std::thread([&] { @@ -124,4 +280,8 @@ int main() { Require(info.size_context_margin > 0 && info.size_context_margin < g_checkedContext.size() - 4096, "margin below the used stack"); Require(info.size_context_margin % 8 == 0, "margin in whole words"); Require(sceFiberStopContextSizeCheck() == 0, "stop size check"); + + RunHostCallOnSmallContext(); + RunFrameChain(); + RunOnLargeContext(); } diff --git a/core/libs/tests/FileStreams.cpp b/core/libs/tests/FileStreams.cpp index d0696fb688..f53419748c 100644 --- a/core/libs/tests/FileStreams.cpp +++ b/core/libs/tests/FileStreams.cpp @@ -1,14 +1,30 @@ #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 +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#include +#else +#include +#endif 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); std::size_t APS5_VABI fwrite_nid_postfix(const void* buffer, std::size_t size, std::size_t count, FileStream* stream); @@ -16,10 +32,20 @@ int APS5_VABI fseek_nid_postfix(FileStream* stream, std::int64_t offset, int ori std::int64_t APS5_VABI ftell_nid_postfix(FileStream* stream); int APS5_VABI fputs_nid_postfix(const char* str, FileStream* stream); 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 @@ -32,8 +58,148 @@ static void ExpectException(TAction action) { throw std::runtime_error("Expected a stream exception"); } +static void ReplaceWithReadOnlyDescriptor(FileStream* stream, const char* path) { +#ifdef _WIN32 + const int descriptor = _open(path, _O_RDONLY | _O_BINARY); + Require(descriptor >= 0 && _dup2(descriptor, _fileno(GetNativeStream(stream))) == 0); + Require(_close(descriptor) == 0); +#else + const int descriptor = open(path, O_RDONLY); + Require(descriptor >= 0 && dup2(descriptor, fileno(GetNativeStream(stream))) >= 0); + Require(close(descriptor) == 0); +#endif +} + +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); @@ -63,10 +229,97 @@ 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(std::filesystem::remove(directoryName)); + const std::u8string unicodeName = u8"セーブé.tmp"; + const std::string guestName(unicodeName.begin(), unicodeName.end()); + const std::filesystem::path hostName(unicodeName); + constexpr char unicodePayload[] = "unicode"; + stream = fopen_nid_postfix(guestName.c_str(), "wb"); + Require(stream != nullptr); + Require(fwrite_nid_postfix(unicodePayload, 1, sizeof(unicodePayload), stream) == sizeof(unicodePayload)); + Require(fclose_nid_postfix(stream) == 0); + Require(std::filesystem::exists(hostName)); + Require(std::filesystem::file_size(hostName) == sizeof(unicodePayload)); + stream = fopen_nid_postfix(guestName.c_str(), "rb"); + Require(stream != nullptr); + std::array unicodeBuffer{}; + Require(fread_nid_postfix(unicodeBuffer.data(), 1, unicodeBuffer.size(), stream) == unicodeBuffer.size()); + Require(std::strcmp(unicodeBuffer.data(), unicodePayload) == 0); + stream = freopen_nid_postfix(guestName.c_str(), "wb", stream); + Require(stream != nullptr); + Require(fclose_nid_postfix(stream) == 0); + Require(std::filesystem::file_size(hostName) == 0); + std::filesystem::remove(hostName); + const std::string heldName = std::string(argv[1]) + ".held"; + const std::string movedName = std::string(argv[1]) + ".moved"; + stream = fopen_nid_postfix(heldName.c_str(), "w+b"); + Require(stream != nullptr && fwrite_nid_postfix(payload, 1, sizeof(payload), stream) == sizeof(payload)); + Require(fflush_nid_postfix(stream) == 0); + Require(rename_nid_postfix(heldName.c_str(), movedName.c_str()) == 0 && !std::filesystem::exists(heldName)); + Require(remove_nid_postfix(movedName.c_str()) == 0 && !std::filesystem::exists(movedName)); + Require(fseek_nid_postfix(stream, 0, SEEK_SET) == 0); + std::array heldBuffer{}; + Require(fread_nid_postfix(heldBuffer.data(), 1, heldBuffer.size(), stream) == heldBuffer.size()); + Require(std::strcmp(heldBuffer.data(), payload) == 0 && fclose_nid_postfix(stream) == 0); + stream = fopen_nid_postfix(heldName.c_str(), "ab"); + Require(stream != nullptr && fputs_nid_postfix("ab", stream) >= 0 && fclose_nid_postfix(stream) == 0); + stream = fopen_nid_postfix(heldName.c_str(), "ab"); + Require(stream != nullptr && fputs_nid_postfix("cd", stream) >= 0 && fclose_nid_postfix(stream) == 0); + errno = 0; + Require(fopen_nid_postfix(heldName.c_str(), "wbx") == nullptr && errno == EEXIST); + stream = fopen_nid_postfix(heldName.c_str(), "rb"); + 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); + std::array markerBuffer{}; + Require(fread_nid_postfix(markerBuffer.data(), 1, markerBuffer.size(), stream) == 0); + Require(ferror_nid_postfix(stream) != 0); + Require(fclose_nid_postfix(stream) == 0); + stream = fopen_nid_postfix(argv[1], "rb"); + Require(stream != nullptr); + Require(fwrite_nid_postfix(marker, 1, sizeof(marker), stream) == 0); + Require(ferror_nid_postfix(stream) != 0); + clearerr_nid_postfix(stream); + Require(fputs_nid_postfix(marker, stream) == EOF); + Require(ferror_nid_postfix(stream) != 0); + Require(fclose_nid_postfix(stream) == 0); + stream = fopen_nid_postfix(argv[1], "wb"); + Require(stream != nullptr && fputs_nid_postfix(marker, stream) >= 0); + ReplaceWithReadOnlyDescriptor(stream, argv[1]); + Require(fflush_nid_postfix(stream) == EOF); + Require(ferror_nid_postfix(stream) != 0); + static_cast(fclose_nid_postfix(stream)); + Require(std::remove(argv[1]) == 0); std::cout << "PASS: stream objects, file operations, EOF and error handling\n"; } diff --git a/core/libs/tests/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 6cccc4a31e..f7eae1cd6a 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -1,6 +1,13 @@ #include "SceTypes.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/GuestHeap.hpp" +#include +#include +#include +#include #include #include +#include #include extern "C" { @@ -10,7 +17,14 @@ int APS5_VABI printf_nid_postfix(const char*, ...); 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*); +int APS5_VABI asprintf_nid_postfix(char**, const char*, ...); +void APS5_VABI free_nid_postfix(void*); +int* APS5_VABI __error_nid_postfix(); } static void Require(bool condition) { @@ -29,6 +43,36 @@ static int APS5_VABI FormatList(char* buffer, size_t size, const char* format, . return result; } +static int APS5_VABI FormatListS(char* buffer, size_t size, const char* format, ...) { + __builtin_sysv_va_list args; + __builtin_sysv_va_start(args, format); + VaList list; + std::memcpy(&list, args, sizeof(list)); + const int result = vsnprintf_s_nid_postfix(buffer, size, format, &list); + __builtin_sysv_va_end(args); + return result; +} + +static int APS5_VABI ScanList(const char* buffer, const char* format, ...) { + __builtin_sysv_va_list args; + __builtin_sysv_va_start(args, format); + VaList list; + std::memcpy(&list, args, sizeof(list)); + const int result = vsscanf_nid_postfix(buffer, format, &list); + __builtin_sysv_va_end(args); + return result; +} + +static int APS5_VABI ScanListS(const char* buffer, const char* format, ...) { + __builtin_sysv_va_list args; + __builtin_sysv_va_start(args, format); + VaList list; + std::memcpy(&list, args, sizeof(list)); + const int result = vsscanf_s_nid_postfix(buffer, format, &list); + __builtin_sysv_va_end(args); + return result; +} + static int APS5_VABI PrintList(const char* format, ...) { __builtin_sysv_va_list args; __builtin_sysv_va_start(args, format); @@ -39,7 +83,6 @@ static int APS5_VABI PrintList(const char* format, ...) { return result; } -#ifdef _WIN32 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", @@ -60,14 +103,66 @@ 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); Require(snprintf_nid_postfix(nullptr, 0, "%.1ls", u"\u00e9") == 0); Require(snprintf_nid_postfix(output, sizeof(output), "%.1s", "\xc3\xa9") == 1 && static_cast(output[0]) == 0xc3 && output[1] == 0); + Require(FormatList(output, sizeof(output), "%S:%lc:%C:%d", u"\u00e9", 0x20ac, 0x41, 7) == 10 && std::strcmp(output, "\xc3\xa9:\xe2\x82\xac:A:7") == 0); + Require(snprintf_nid_postfix(output, sizeof(output), "%.*S", 3, u"\u00e9\u20ac") == 2 && std::strcmp(output, "\xc3\xa9") == 0); + Require(snprintf_nid_postfix(output, sizeof(output), "[%ls]", static_cast(nullptr)) == 8 && std::strcmp(output, "[(null)]") == 0); + Require(snprintf_nid_postfix(output, sizeof(output), "a%lcb%C", 0, 0) == 4 && std::memcmp(output, "a\0b\0", 5) == 0); + Require(FormatList(output, sizeof(output), "%%ls:%2$d:%1$d", 3, 7) == 7 && std::strcmp(output, "%ls:7:3") == 0); + Require(FormatList(output, sizeof(output), "%ls[%f][%+f][% F][%5.1e]", u"", -NAN, -NAN, NAN, -NAN) == 22 + && std::strcmp(output, "[nan][nan][NAN][ nan]") == 0); + Require(FormatList(output, sizeof(output), "%ls[%-6g][%06a][%LG][%+f]", u"", NAN, -NAN, -static_cast(NAN), -INFINITY) == 27 + && std::strcmp(output, "[nan ][ nan][NAN][-inf]") == 0); + for (std::size_t capacity = 0; capacity <= 6; ++capacity) { + std::memset(output, '!', sizeof(output)); + int count = -1; + Require(FormatList(capacity ? output : nullptr, capacity, "%ls%n", u"A\u00e9Z", &count) == 4 && count == 4); + const auto copied = capacity == 0 ? 0 : std::min(capacity - 1, 4); + Require(std::memcmp(output, "A\xc3\xa9Z", copied) == 0 && output[capacity] == '!'); + if (capacity) Require(output[copied] == 0); + } return correct; } -#endif + +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]; @@ -89,10 +184,56 @@ __attribute__((noinline)) static void APS5_VABI RunChecks() { Require(std::strcmp(buffer, "1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 10.0") == 0); snprintf_nid_postfix(buffer, sizeof(buffer), "%.3Lf %d %.1f", 1.125L, 7, 2.5); Require(std::strcmp(buffer, "1.125 7 2.5") == 0); + Require(snprintf_nid_postfix(buffer, sizeof(buffer), "%'d|%'u|%'.2f|%'-8ld|%'+05d|%'g", 1234567, 4000000000u, 1234567.891, 1234567LL, 42, 1e6) == 50); + Require(std::strcmp(buffer, "1234567|4000000000|1234567.89|1234567 |+0042|1e+06") == 0); + Require(FormatList(buffer, sizeof(buffer), "%'lld %'zu", 1234567890123LL, static_cast(9876543210)) == 24 && std::strcmp(buffer, "1234567890123 9876543210") == 0); + Require(FormatList(buffer, sizeof(buffer), "%'d %ls", 1234567, u"x") == 9 && std::strcmp(buffer, "1234567 x") == 0); + Require(sprintf_nid_postfix(buffer, "%2$s %1$d %2$s", 7, "x") == 5 && std::strcmp(buffer, "x 7 x") == 0); + Require(snprintf_nid_postfix(buffer, sizeof(buffer), "%3$.1Lf|%2$.2f|%1$hhd|%4$s", 257, 2.5, 1.5L, "end") == 14); + Require(std::strcmp(buffer, "1.5|2.50|1|end") == 0); + Require(sprintf_nid_postfix(buffer, "%9$d%8$d%7$d%6$d%5$d%4$d%3$d%2$d%1$d", 1, 2, 3, 4, 5, 6, 7, 8, 9) == 9 && std::strcmp(buffer, "987654321") == 0); + Require(FormatList(buffer, sizeof(buffer), "%11$d %10$g %9$g %8$g %7$g %6$g %5$g %4$g %3$g %2$g %1$g", 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11) == 23 && + std::strcmp(buffer, "11 10 9 8 7 6 5 4 3 2 1") == 0); + Require(snprintf_nid_postfix(buffer, sizeof(buffer), "[%1$*2$.*3$f][%1$-*2$.1f][%4$*2$c]", 3.5, 7, 2, 'z') == 27); + Require(std::strcmp(buffer, "[ 3.50][3.5 ][ z]") == 0); + Require(sprintf_nid_postfix(buffer, "[%2$*1$d][%2$.*1$d]", -4, 5) == 9 && std::strcmp(buffer, "[5 ][5]") == 0); + Require(sprintf_nid_postfix(buffer, "[%1$*2$d]", 5, 3) == 5 && std::strcmp(buffer, "[ 5]") == 0); + Require(FormatList(buffer, sizeof(buffer), "%2$ls|%1$C|%3$d", 0x41, u"\u00e9", 7) == 6 && std::strcmp(buffer, "\xc3\xa9|A|7") == 0); + Require(FormatList(buffer, sizeof(buffer), "%2$d %d %ls|%*5$d|%1$d", 1, 2, 3, u"x", 4) == 12 && std::strcmp(buffer, "2 3 x| 4|1") == 0); + Require(FormatList(buffer, sizeof(buffer), "[%*3$d]%ls", 5, u"", 3) == 5 && std::strcmp(buffer, "[ 5]") == 0); + const char* const invalidPositions[] = {"%0$d%ls", "%100$d%ls", "%*0$d%ls"}; + for (const char* format : invalidPositions) { + bool threw = false; + try { + FormatList(buffer, sizeof(buffer), format, 1, u""); + } catch (const std::invalid_argument&) { + threw = true; + } + Require(threw); + } char expected[1024]; - std::snprintf(expected, sizeof(expected), "%#08x %.3e %a %g %p", 42u, 1.25, 1.25, 1.25, static_cast(buffer)); + 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); + 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)) == 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] = '!'; @@ -104,6 +245,10 @@ __attribute__((noinline)) static void APS5_VABI RunChecks() { Require(snprintf_nid_postfix(buffer, 1, "%d", 123) == 3 && buffer[0] == 0); Require(sprintf_nid_postfix(buffer, "%hhd %hhu %hd %hu %%", 255, 257, 65535, 65537) == 11); Require(std::strcmp(buffer, "-1 1 -1 1 %") == 0); + Require(sprintf_nid_postfix(buffer, "[%#.0o][%#.o][%#5.0o][%-#4.0o][%#.*o][%#.0o][%.0o][%#o][%#.3o]", 0u, 0u, 0u, 0u, 0, 0u, 8u, 0u, 0u, 0u) == 37); + Require(std::strcmp(buffer, "[0][0][ 0][0 ][0][010][][0][000]") == 0); + Require(FormatList(buffer, sizeof(buffer), "[%#.0lo][%#.0hho][%#.0x]", 0ull, 0u, 0u) == 8 && std::strcmp(buffer, "[0][0][]") == 0); + Require(FormatList(buffer, sizeof(buffer), "[%#.0o][%ls]", 0u, u"x") == 6 && std::strcmp(buffer, "[0][x]") == 0); Require(sprintf_nid_postfix(buffer, "%d %d %d %d %d %d", 1, 2, 3, 4, 5, 6) == 11); Require(std::strcmp(buffer, "1 2 3 4 5 6") == 0); int first = 0, second = 0, third = 0, fourth = 0, fifth = 0, sixth = 0; @@ -113,21 +258,80 @@ __attribute__((noinline)) static void APS5_VABI RunChecks() { &first, &second, &third, &fourth, &fifth, &sixth, word, &real) == 8); Require(first == 1 && second == 2 && third == 3 && fourth == 4 && fifth == 5 && sixth == 6); Require(std::strcmp(word, "seven") == 0 && real == 8.5); + int binary = -1, binaryEnd = -1; + char binaryRest[8] = {}; + Require(sscanf_nid_postfix("0b101", "%i%n%7s", &binary, &binaryEnd, binaryRest) == 2); + Require(binary == 0 && binaryEnd == 1 && std::strcmp(binaryRest, "b101") == 0); + Require(ScanList(" -0B11", "%i", &binary) == 1 && binary == 0); + Require(ScanListS("0b1", "%i", &binary) == 1 && binary == 0); + Require(sscanf_nid_postfix("0x1f", "%i", &binary) == 1 && binary == 31); +#ifdef _WIN32 + int scanned = -1; + int position = -1; + Require(sscanf_nid_postfix("12", "%*d%d", &scanned) == 0 && scanned == -1); + Require(sscanf_nid_postfix("12 ", "%*d %d", &scanned) == 0); + Require(sscanf_nid_postfix("abc", "%*s%d", &scanned) == 0); + Require(sscanf_nid_postfix("a", "%*c%d", &scanned) == 0); + Require(sscanf_nid_postfix("abc", "%*[a-z]%d", &scanned) == 0); + Require(sscanf_nid_postfix("12", "%*d%n%d", &position, &scanned) == 0 && position == 2); + Require(sscanf_nid_postfix("12", "%*d%d%d", &scanned, &first) == 0); + Require(sscanf_nid_postfix("1 2", "%d%*d%d", &scanned, &first) == 1 && scanned == 1); + Require(sscanf_nid_postfix("1 2 3", "%*d %d %*d", &scanned) == 1 && scanned == 2); + Require(sscanf_nid_postfix("", "%*d", &scanned) == EOF); + 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")); + char narrow[8] = {}; + Require(sscanf_nid_postfix("ab%", "%7[^%ls]", narrow) == 1 && std::strcmp(narrow, "ab") == 0); +#endif long long count = -1; int smallCount = -1; Require(snprintf_nid_postfix(buffer, 3, "abcd%lnEF%n", &count, &smallCount) == 6); Require(count == 4 && smallCount == 6 && std::strcmp(buffer, "ab") == 0); Require(snprintf_nid_postfix(buffer, sizeof(buffer), "a%cb", 0) == 3); Require(buffer[0] == 'a' && buffer[1] == 0 && buffer[2] == 'b' && buffer[3] == 0); + *__error_nid_postfix() = 35; + Require(snprintf_nid_postfix(buffer, sizeof(buffer), "[%m][%.8m][%-10.5m][%d]", 7) == 59); + Require(std::strcmp(buffer, "[Resource temporarily unavailable][Resource][Resou ][7]") == 0); + Require(*__error_nid_postfix() == 35); + *__error_nid_postfix() = 63; + Require(FormatList(buffer, sizeof(buffer), "%s: %m", "open") == 24 && std::strcmp(buffer, "open: File name too long") == 0); Require(printf_nid_postfix("printf: %d %.1f\n", 7, 2.5) == 14); Require(libc_printf_nid_postfix("libc_printf: %s\n", "OK") == 16); 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); + char tooSmall[4] = {'x', 'x', 'x', 'x'}; + Require(ScanListS("abcdef", "%s", tooSmall, 4u) == 0 && tooSmall[0] == '\0'); std::puts("Formatting checks passed: 10000 iterations"); } -#ifdef _WIN32 -int main() { RunChecks(); return CheckWidePrecision() ? 0 : 1; } -#else -int main() { RunChecks(); } -#endif +static void* APS5_VABI Allocate(std::size_t bytes) { return GuestHeap::GuestHeapAllocate_nid_postfix(bytes); } +static void APS5_VABI Release(void* pointer) { GuestHeap::GuestHeapFree_nid_postfix(pointer); } +static void* APS5_VABI Unused() { std::abort(); } + +static void CheckAllocatingFormat() { + const std::array api{reinterpret_cast(&Allocate), reinterpret_cast(&Release), reinterpret_cast(&Unused), + reinterpret_cast(&Unused), reinterpret_cast(&Unused), reinterpret_cast(&Unused), reinterpret_cast(&Unused)}; + ApplicationHeapRegister_nid_no_patch(api.data()); + char* allocated = nullptr; + Require(asprintf_nid_postfix(&allocated, "%ls:%S:%lc:%d", u"\u00e9", u"ab", 0x20ac, 7) == 11 && std::strcmp(allocated, "\xc3\xa9:ab:\xe2\x82\xac:7") == 0); + free_nid_postfix(allocated); +} + +int main() { 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 new file mode 100644 index 0000000000..c6691f898d --- /dev/null +++ b/core/libs/tests/GuestAcm.cpp @@ -0,0 +1,66 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI sceAcmContextCreate(std::uint32_t*); +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); +} + +static void Require(bool value) { + if (!value) std::abort(); +} + +constexpr int BadContext = static_cast(0x81940002); +constexpr int BadPointer = static_cast(0x81940006); + +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(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(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 458ec1b136..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{}; @@ -31,4 +596,59 @@ int main() { Require(__inet_pton_nid_postfix(2, "256.1.2.3", address.data()) == 0); Require(__inet_pton_nid_postfix(28, "::ffff:192.0.2.17", address.data()) == 1); Require(address[10] == 255 && address[11] == 255 && address[12] == 192 && address[15] == 17); + + for (const char* rejected : {"01.2.3.4", "1.2.3", "1.2.3.4.5", "1.2.3.4.", " 1.2.3.4", "0x1.2.3.4", "1..3.4", "1.2.3.-4"}) { + address.fill(0xA5); + Require(__inet_pton_nid_postfix(2, rejected, address.data()) == 0); + Require(address[0] == 0xA5); + } + for (const char* rejected : {"::ffff:01.2.3.4", "::ffff:1.2.3.04", "1:2:3:4:5:6:7:8::", "::1:2:3:4:5:6:7:8", "1::2::3", ":::", ":1::", "1::2:", ":1:2:3:4:5:6:7", + "1:2:3:4:5:6:7", "1:2:3:4:5:6:7:8:9", "12345::", "fe80::1%eth0", "1.2.3.4::", "::1.2.3.4:1", "::g", "1:2:3:4:5:6:1.2.3.4:7", + "1:2:3:4:5:6:7:1.2.3.4", ""}) { + address.fill(0xA5); + Require(__inet_pton_nid_postfix(28, rejected, address.data()) == 0); + Require(address[0] == 0xA5 && address[15] == 0xA5); + } + Require(__inet_pton_nid_postfix(28, "1:2:3:4:5:6:7::", address.data()) == 1); + Require(address[13] == 7 && address[14] == 0 && address[1] == 1); + Require(__inet_pton_nid_postfix(28, "ABCD:ef01::1", address.data()) == 1); + Require(address[0] == 0xAB && address[1] == 0xCD && address[2] == 0xEF && address[3] == 0x01 && address[15] == 1); + + struct Text { const char* bytesAsText; const char* expected; }; + const Text samples[] = { + {"::ffff:0.0.0.1", "::ffff:0.0.0.1"}, {"::ffff:0:1:0", "::ffff:0:1:0"}, {"::1", "::1"}, {"::", "::"}, {"1::", "1::"}, + {"::2", "::2"}, {"::1.2.3.4", "::1.2.3.4"}, {"1:0:0:2:0:0:0:3", "1:0:0:2::3"}, {"1:0:2:0:3:0:4:0", "1:0:2:0:3:0:4:0"}, + {"0:0:1:0:0:2:0:0", "::1:0:0:2:0:0"}, {"::ffff:1.2.3.4", "::ffff:1.2.3.4"}, {"2001:DB8::17", "2001:db8::17"}}; + for (const Text& sample : samples) { + Require(__inet_pton_nid_postfix(28, sample.bytesAsText, address.data()) == 1); + char output[64]{}; + Require(__inet_ntop_nid_postfix(28, address.data(), output, sizeof(output)) == output); + Require(std::strcmp(output, sample.expected) == 0); + const auto length = std::strlen(sample.expected); + std::memset(output, 'x', sizeof(output)); + Require(__inet_ntop_nid_postfix(28, address.data(), output, static_cast(length + 1)) == output); + Require(std::strcmp(output, sample.expected) == 0); + std::memset(output, 'x', sizeof(output)); + *__error_nid_postfix() = 0; + Require(__inet_ntop_nid_postfix(28, address.data(), output, static_cast(length)) == nullptr); + Require(*__error_nid_postfix() == 28 && output[0] == 'x'); + } + address.fill(0xFF); + char widest[64]{}; + Require(__inet_ntop_nid_postfix(28, address.data(), widest, sizeof(widest)) == widest); + Require(std::strcmp(widest, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff") == 0); + const unsigned char fourth[4] = {255, 0, 7, 100}; + 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 new file mode 100644 index 0000000000..72b97bd56f --- /dev/null +++ b/core/libs/tests/GuestAgcDriverDebugUnavailable.cpp @@ -0,0 +1,85 @@ +#include "SceTypes.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI sceAgcDriverGetShaderDebuggingStatus(); +int APS5_VABI sceAgcDriverSetValidationErrorOutputFrequency(std::uint32_t); +int APS5_VABI sceAgcDriverSetSubmitValidationMode(std::uint32_t); +int APS5_VABI sceAgcDriverGetSubmitValidationMode(std::uint32_t*); +int APS5_VABI sceAgcDriverSetSubmitValidationConfig(const void*); +int APS5_VABI sceAgcDriverGetSubmitValidationConfig(void*); +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() { + Require(sceAgcDriverGetShaderDebuggingStatus() == 1); + + Require(sceAgcDriverSetValidationErrorOutputFrequency(0u) == DebugUnavailable); + Require(sceAgcDriverSetValidationErrorOutputFrequency(60u) == DebugUnavailable); + Require(sceAgcDriverSetSubmitValidationMode(0u) == DebugUnavailable); + Require(sceAgcDriverSetSubmitValidationMode(1u) == DebugUnavailable); + + std::uint32_t mode = 0xA5A5A5A5u; + Require(sceAgcDriverGetSubmitValidationMode(&mode) == DebugUnavailable); + Require(sceAgcDriverGetSubmitValidationMode(nullptr) == DebugUnavailable); + Require(mode == 0xA5A5A5A5u); + + std::array config{}; + config.fill(0x5A); + Require(sceAgcDriverSetSubmitValidationConfig(config.data()) == DebugUnavailable); + Require(sceAgcDriverSetSubmitValidationConfig(nullptr) == DebugUnavailable); + Require(sceAgcDriverGetSubmitValidationConfig(config.data()) == DebugUnavailable); + Require(sceAgcDriverGetSubmitValidationConfig(nullptr) == DebugUnavailable); + for (const std::uint8_t byte : config) Require(byte == 0x5A); + + 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 eb97484c7d..fc55b15a8a 100644 --- a/core/libs/tests/GuestAgcDriverResources.cpp +++ b/core/libs/tests/GuestAgcDriverResources.cpp @@ -1,12 +1,16 @@ #include "SceTypes.hpp" +#include #include #include #include +#include #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); int APS5_VABI sceAgcDriverRegisterGdsResource(std::uint32_t*, std::uint32_t, std::uint32_t, std::uint32_t, const char*, std::uint32_t, std::uint64_t); int APS5_VABI sceAgcDriverUnregisterAllResourcesForOwner(std::uint32_t); int APS5_VABI sceAgcDriverRegisterWorkloadStream(std::uint32_t, const void*); @@ -24,19 +28,51 @@ 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); static void Require(bool value) { if (!value) std::abort(); } +struct ResourceDescriptor { + const void* memory; + std::size_t size; + const char* name; + std::uint32_t type; + std::uint32_t reserved; + std::uint64_t userData; +}; + +static_assert(sizeof(ResourceDescriptor) == 40); + 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); Require(owner == 7u && resource == 9u); + std::array bulkMemory{0x11223344u, 0x55667788u, 0x99aabbccu, 0xddeeff00u}; + const auto originalBulkMemory = bulkMemory; + std::array descriptors{{ + {bulkMemory.data(), sizeof(bulkMemory), "default", 3u, 0u, 0u}, + {bulkMemory.data(), sizeof(bulkMemory), "second", 5u, 0u, 23u}, + }}; + const auto originalDescriptors = descriptors; + std::uint32_t bulkHandle = 0xffffffffu; + Require(sceAgcDriverRegisterMultipleResources(&bulkHandle, 0u, descriptors.data(), 1u) == Unavailable); + Require(bulkHandle == 0xffffffffu); + bulkHandle = 17u; + Require(sceAgcDriverRegisterMultipleResources(&bulkHandle, owner, descriptors.data(), 2u) == Unavailable); + Require(sceAgcDriverRegisterMultipleResources(&bulkHandle, owner, nullptr, 0u) == Unavailable); + Require(bulkHandle == 17u && owner == 7u); + Require(std::memcmp(descriptors.data(), originalDescriptors.data(), sizeof(descriptors)) == 0); + Require(bulkMemory == originalBulkMemory); Require(sceAgcDriverGetDefaultOwner(&owner) == Unavailable && owner == 7u); std::uint32_t maxNameLength = 8u; Require(sceAgcDriverGetResourceRegistrationMaxNameLength(&maxNameLength) == 0 && maxNameLength == 0xfcu); @@ -61,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 b1e1eda10e..cad5b3c0b7 100644 --- a/core/libs/tests/GuestAio.cpp +++ b/core/libs/tests/GuestAio.cpp @@ -10,17 +10,25 @@ #include #include #include +#include #include #include #include #include +#ifdef _WIN32 +#include +extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); +#endif 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 sceKernelPread(int, void*, std::size_t, std::int64_t); +std::int64_t APS5_VABI sceKernelPwrite(int, const void*, std::size_t, std::int64_t); std::int64_t APS5_VABI sceKernelLseek(int d, std::int64_t offset, int whence); int APS5_VABI sceKernelAioDeleteRequest(std::int32_t id, std::int32_t* ret); int APS5_VABI sceKernelAioInitializeImpl(void* param, std::int32_t size); void APS5_VABI sceKernelAioInitializeParam(void* param); +int APS5_VABI sceKernelAioSetParam(KernelAioSchedulingParam* param, std::int32_t schedulingWindowSize, std::int32_t delayedCountLimit, std::uint32_t enableSplit, std::uint32_t splitSize, std::uint32_t splitChunkSize); int APS5_VABI sceKernelAioSubmitReadCommands(KernelAioRwRequest* req, std::int32_t size, std::int32_t prio, std::int32_t* id); int APS5_VABI sceKernelAioSubmitWriteCommands(KernelAioRwRequest* req, std::int32_t size, std::int32_t prio, std::int32_t* id); int APS5_VABI sceKernelAioPollRequest(std::int32_t id, std::int32_t* state); @@ -40,14 +48,85 @@ static void Require(bool value, int line) { } } #define Check(value) Require((value), __LINE__) -int main() { +#ifdef _WIN32 +static int g_parameterReports = 0; +static void CountParameterReport(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) { + ++g_parameterReports; +} +static void CheckBadPositionedDescriptors() { + const auto file = std::filesystem::path("anyps5-aio-bad-descriptor-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + { std::ofstream stream(file, std::ios::binary); stream << "data"; Check(static_cast(stream)); } + const int closed = sceKernelOpen(file.string().c_str(), SCE_KERNEL_O_RDWR, 0); + Check(closed >= 0 && sceKernelClose(closed) == 0); + const auto previous = _set_thread_local_invalid_parameter_handler(CountParameterReport); + for (int descriptor : {-1, closed}) { + for (std::size_t count : {std::size_t{0}, std::size_t{1}}) { + char buffer = 'x'; + Check(sceKernelPread(descriptor, &buffer, count, 0) == SCE_KERNEL_ERROR_EBADF); + Check(sceKernelPwrite(descriptor, &buffer, count, 0) == SCE_KERNEL_ERROR_EBADF); + for (bool write : {false, true}) { + KernelAioResult result{-1, 0}; + KernelAioRwRequest request{0, count, &buffer, &result, descriptor}; + std::int32_t id = 0; + Check((write ? sceKernelAioSubmitWriteCommands(&request, 1, 0, &id) + : sceKernelAioSubmitReadCommands(&request, 1, 0, &id)) == 0); + Check(result.state == 4 && result.return_value == SCE_KERNEL_ERROR_EBADF); + std::int32_t removed = -1; + Check(sceKernelAioDeleteRequest(id, &removed) == 0); + } + Check(buffer == 'x'); + Check(g_parameterReports == 0); + } + } + Check(::_get_osfhandle(closed) == -1); + Check(g_parameterReports == 1); + Check(_set_thread_local_invalid_parameter_handler(previous) == CountParameterReport); + Check(std::filesystem::remove(file)); +} +#endif +int main(int argc, char** argv) { +#ifdef _WIN32 + if (argc > 1 && std::strcmp(argv[1], "bad-descriptors") == 0) { + CheckBadPositionedDescriptors(); + return 0; + } +#endif const auto root = std::filesystem::path("anyps5-aio-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); Check(std::filesystem::create_directory(root)); const auto file = root / "data.bin"; const std::string seed = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; { std::ofstream stream(file, std::ios::binary); stream << seed; } - Check(sceKernelAioInitializeImpl(nullptr, 0) == 0); + static_assert(sizeof(KernelAioSchedulingParam) == 20); + std::array schedulingParams{}; + schedulingParams.fill({7, 7, 7, 7, 7}); + Check(sceKernelAioSetParam(&schedulingParams[1], 64, 16, 1, 0x200000, 0x40000) == 0); + Check(schedulingParams[1].schedulingWindowSize == 64 && schedulingParams[1].delayedCountLimit == 16); + Check(schedulingParams[1].enableSplit == 1 && schedulingParams[1].splitSize == 0x200000 && schedulingParams[1].splitChunkSize == 0x40000); + Check(schedulingParams[0].schedulingWindowSize == 7 && schedulingParams[0].splitChunkSize == 7); + Check(schedulingParams[2].schedulingWindowSize == 7 && schedulingParams[2].splitChunkSize == 7); + Check(sceKernelAioSetParam(&schedulingParams[2], 128, 128, 0, 0x1000000, 0x1000000) == 0); + Check(schedulingParams[2].schedulingWindowSize == 128 && schedulingParams[2].delayedCountLimit == 128); + Check(schedulingParams[2].enableSplit == 0 && schedulingParams[2].splitSize == 0x1000000 && schedulingParams[2].splitChunkSize == 0x1000000); + const auto setParamThrows = [&](std::int32_t window, std::int32_t delayed, std::uint32_t split, std::uint32_t size, std::uint32_t chunk) { + KernelAioSchedulingParam untouchedParam{7, 7, 7, 7, 7}; + bool thrown = false; + try { sceKernelAioSetParam(&untouchedParam, window, delayed, split, size, chunk); } catch (const std::exception&) { thrown = true; } + return thrown && untouchedParam.schedulingWindowSize == 7 && untouchedParam.delayedCountLimit == 7 && + untouchedParam.enableSplit == 7 && untouchedParam.splitSize == 7 && untouchedParam.splitChunkSize == 7; + }; + Check(setParamThrows(129, 32, 1, 0x100000, 0x100000)); + Check(setParamThrows(-1, 32, 1, 0x100000, 0x100000)); + Check(setParamThrows(32, 129, 1, 0x100000, 0x100000)); + Check(setParamThrows(32, -1, 1, 0x100000, 0x100000)); + Check(setParamThrows(32, 32, 2, 0x100000, 0x100000)); + Check(setParamThrows(32, 32, 1, 0x1000001, 0x100000)); + Check(setParamThrows(32, 32, 1, 0x100000, 0x1000001)); + bool nullThrew = false; + try { sceKernelAioSetParam(nullptr, 32, 32, 1, 0x100000, 0x100000); } catch (const std::invalid_argument&) { nullThrew = true; } + Check(nullThrew); + Check(sceKernelAioInitializeImpl(schedulingParams.data(), static_cast(sizeof(schedulingParams))) == 0); int initParam = 0; sceKernelAioInitializeParam(&initParam); const int fd = sceKernelOpen(file.string().c_str(), SCE_KERNEL_O_RDONLY, 0); @@ -70,6 +149,29 @@ int main() { Check(secondResult.state == 3 && secondResult.return_value == 16); Check(std::memcmp(first.data(), seed.data(), 16) == 0); Check(std::memcmp(second.data(), seed.data() + 32, 16) == 0); + KernelAioResult invalidReadResult{-1, 0}; + KernelAioRwRequest invalidReadRequest{-1, first.size(), first.data(), &invalidReadResult, fd}; + std::int32_t invalidReadId = 0; + Check(sceKernelAioSubmitReadCommands(&invalidReadRequest, 1, 0, &invalidReadId) == 0); + std::int32_t invalidReadState = 0; + Check(sceKernelAioWaitRequest(invalidReadId, &invalidReadState, nullptr) == 0); + Check(invalidReadState == 4); + Check(invalidReadResult.state == 4 && invalidReadResult.return_value == SCE_KERNEL_ERROR_EINVAL); + std::array aggregateReadBuffer{}; + KernelAioResult aggregateReadSuccessResult{-1, 0}; + KernelAioResult aggregateReadErrorResult{-1, 0}; + KernelAioRwRequest aggregateReads[2] = { + {4, aggregateReadBuffer.size(), aggregateReadBuffer.data(), &aggregateReadSuccessResult, fd}, + {-1, aggregateReadBuffer.size(), aggregateReadBuffer.data(), &aggregateReadErrorResult, fd}, + }; + std::int32_t aggregateReadId = 0; + Check(sceKernelAioSubmitReadCommands(aggregateReads, 2, 0, &aggregateReadId) == 0); + std::int32_t aggregateReadState = 0; + Check(sceKernelAioWaitRequest(aggregateReadId, &aggregateReadState, nullptr) == 0); + Check(aggregateReadState == 4); + Check(aggregateReadSuccessResult.state == 3 && aggregateReadSuccessResult.return_value == 4); + Check(aggregateReadErrorResult.state == 4 && aggregateReadErrorResult.return_value == SCE_KERNEL_ERROR_EINVAL); + Check(std::memcmp(aggregateReadBuffer.data(), seed.data() + 4, aggregateReadBuffer.size()) == 0); std::int32_t polled = 0; Check(sceKernelAioPollRequest(id, &polled) == 0); Check(polled == 3); @@ -87,9 +189,37 @@ int main() { std::int32_t writeState = 0; Check(sceKernelAioWaitRequest(writeId, &writeState, nullptr) == 0); Check(writeState == 3 && writeResult.state == 3 && writeResult.return_value == 5); + KernelAioResult invalidWriteResult{-1, 0}; + KernelAioRwRequest invalidWriteRequest{-1, written.size(), written.data(), &invalidWriteResult, writeFd}; + std::int32_t invalidWriteId = 0; + Check(sceKernelAioSubmitWriteCommands(&invalidWriteRequest, 1, 0, &invalidWriteId) == 0); + std::int32_t invalidWriteState = 0; + Check(sceKernelAioWaitRequest(invalidWriteId, &invalidWriteState, nullptr) == 0); + Check(invalidWriteState == 4); + Check(invalidWriteResult.state == 4 && invalidWriteResult.return_value == SCE_KERNEL_ERROR_EINVAL); + std::array aggregateWriteBuffer = {'G', 'O', 'O', 'D'}; + KernelAioResult aggregateWriteSuccessResult{-1, 0}; + KernelAioResult aggregateWriteErrorResult{-1, 0}; + KernelAioRwRequest aggregateWrites[2] = { + {8, aggregateWriteBuffer.size(), aggregateWriteBuffer.data(), &aggregateWriteSuccessResult, writeFd}, + {-1, aggregateWriteBuffer.size(), aggregateWriteBuffer.data(), &aggregateWriteErrorResult, writeFd}, + }; + std::int32_t aggregateWriteId = 0; + Check(sceKernelAioSubmitWriteCommands(aggregateWrites, 2, 0, &aggregateWriteId) == 0); + std::int32_t aggregateWriteState = 0; + Check(sceKernelAioWaitRequest(aggregateWriteId, &aggregateWriteState, nullptr) == 0); + Check(aggregateWriteState == 4); + Check(aggregateWriteSuccessResult.state == 3 && aggregateWriteSuccessResult.return_value == 4); + Check(aggregateWriteErrorResult.state == 4 && aggregateWriteErrorResult.return_value == SCE_KERNEL_ERROR_EINVAL); Check(sceKernelClose(writeFd) == 0); { std::ifstream stream(file, std::ios::binary); std::string contents; std::getline(stream, contents); Check(contents.rfind("HELLO", 0) == 0); } + Check(std::filesystem::file_size(file) == seed.size()); + { std::ifstream stream(file, std::ios::binary); + std::string contents{std::istreambuf_iterator(stream), std::istreambuf_iterator()}; + Check(contents.size() == seed.size()); + Check(contents.substr(8, aggregateWriteBuffer.size()) == "GOOD"); + Check(contents.substr(12, 4) == seed.substr(12, 4)); } Check(sceKernelAioSubmitReadCommands(nullptr, 1, 0, &id) == SCE_KERNEL_ERROR_EFAULT); Check(sceKernelAioSubmitReadCommands(requests, 0, 0, &id) == SCE_KERNEL_ERROR_EINVAL); Check(sceKernelAioSubmitReadCommands(requests, 1, 0, nullptr) == SCE_KERNEL_ERROR_EFAULT); @@ -101,6 +231,36 @@ int main() { const int batchFd = sceKernelOpen(file.string().c_str(), SCE_KERNEL_O_RDWR, 0); Check(batchFd >= 0); Check(sceKernelLseek(batchFd, 2, 0) == 2); + std::array batchReadBuffer{}; + KernelAioResult batchReadSuccessResult{-1, 0}; + KernelAioResult batchReadErrorResult{-1, 0}; + KernelAioRwRequest batchReads[2] = { + {0, batchReadBuffer.size(), batchReadBuffer.data(), &batchReadSuccessResult, batchFd}, + {-1, batchReadBuffer.size(), batchReadBuffer.data(), &batchReadErrorResult, batchFd}, + }; + std::int32_t batchReadIds[2] = {0, 0}; + Check(sceKernelAioSubmitReadCommandsMultiple(batchReads, 2, 0, batchReadIds) == 0); + Check(batchReadIds[0] > 0 && batchReadIds[1] > 0 && batchReadIds[0] != batchReadIds[1]); + Check(batchReadSuccessResult.state == 3 && batchReadSuccessResult.return_value == 4); + Check(batchReadErrorResult.state == 4 && batchReadErrorResult.return_value == SCE_KERNEL_ERROR_EINVAL); + std::int32_t batchReadStates[2] = {0, 0}; + Check(sceKernelAioWaitRequests(batchReadIds, 2, batchReadStates, 1, nullptr) == 0); + Check(batchReadStates[0] == 3 && batchReadStates[1] == 4); + std::array batchWriteData = {'A', 'B', 'C', 'D'}; + KernelAioResult batchWriteSuccessResult{-1, 0}; + KernelAioResult batchWriteErrorResult{-1, 0}; + KernelAioRwRequest batchWrites[2] = { + {12, batchWriteData.size(), batchWriteData.data(), &batchWriteSuccessResult, batchFd}, + {-1, batchWriteData.size(), batchWriteData.data(), &batchWriteErrorResult, batchFd}, + }; + std::int32_t batchWriteIds[2] = {0, 0}; + Check(sceKernelAioSubmitWriteCommandsMultiple(batchWrites, 2, 0, batchWriteIds) == 0); + Check(batchWriteIds[0] > 0 && batchWriteIds[1] > 0 && batchWriteIds[0] != batchWriteIds[1]); + Check(batchWriteSuccessResult.state == 3 && batchWriteSuccessResult.return_value == 4); + Check(batchWriteErrorResult.state == 4 && batchWriteErrorResult.return_value == SCE_KERNEL_ERROR_EINVAL); + std::int32_t batchWriteStates[2] = {0, 0}; + Check(sceKernelAioWaitRequests(batchWriteIds, 2, batchWriteStates, 1, nullptr) == 0); + Check(batchWriteStates[0] == 3 && batchWriteStates[1] == 4); std::array patch = {'W', 'X', 'Y', 'Z'}; KernelAioResult patchResult{-1, 0}; KernelAioResult badResult{-1, 0}; @@ -168,6 +328,16 @@ int main() { 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 a389552cd3..017b6304a8 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -6,8 +6,16 @@ #include #include #include +#include #include +#ifdef _WIN32 +#include +#else +#include +#include +#endif + extern "C" { int APS5_VABI sceAjmInitialize(std::int64_t, std::uint32_t*); int APS5_VABI sceAjmFinalize(std::uint32_t); @@ -20,6 +28,7 @@ int APS5_VABI sceAjmBatchJobInitialize(AjmBatchInfo*, std::uint32_t, const void* int APS5_VABI sceAjmBatchJobDecode(AjmBatchInfo*, std::uint32_t, const void*, std::size_t, void*, std::size_t, void*); int APS5_VABI sceAjmBatchJobDecodeSingle(AjmBatchInfo*, std::uint32_t, const void*, std::size_t, 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 sceAjmBatchJobRunSplit(AjmBatchInfo*, std::uint32_t, std::uint64_t, const AjmBuffer*, std::size_t, const AjmBuffer*, std::size_t, void*, std::size_t); int APS5_VABI sceAjmBatchJobSetGaplessDecode(AjmBatchInfo*, std::uint32_t, const void*, int, void*); int APS5_VABI sceAjmBatchJobControl(AjmBatchInfo*, std::uint32_t, std::uint64_t, const void*, std::size_t, void*, std::size_t); int APS5_VABI sceAjmBatchJobGetGaplessDecode(AjmBatchInfo*, std::uint32_t, void*); @@ -31,6 +40,8 @@ int APS5_VABI sceAjmBatchCancel(std::uint32_t, std::uint32_t); int APS5_VABI sceAjmBatchJobClearContext(AjmBatchInfo*, std::uint32_t, void*); int APS5_VABI sceAjmBatchJobSetResampleParameters(AjmBatchInfo*, std::uint32_t, float, std::uint32_t, void*); int APS5_VABI sceAjmBatchJobGetResampleInfo(AjmBatchInfo*, std::uint32_t, void*); +int APS5_VABI sceAjmBatchJobSetResampleParametersEx(AjmBatchInfo*, std::uint32_t, float, float, std::uint32_t, void*); +int APS5_VABI sceAjmBatchJobDecodeSplit(AjmBatchInfo*, std::uint32_t, const AjmBuffer*, std::size_t, const AjmBuffer*, std::size_t, void*); } static void Require(bool value) { if (!value) std::abort(); } @@ -79,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; @@ -121,10 +173,10 @@ void RunDecode(std::uint32_t context, std::uint32_t instance, const std::uint8_t Require(sceAjmBatchStart(context, &info, 0, &error, &id) == 0 && sceAjmBatchWait(context, id, 0, &error) == 0); } -void TestOpus(std::uint32_t context) { +void TestOpus(std::uint32_t context, std::uint32_t mappingFamily = 0) { std::uint32_t instance = 0; Require(sceAjmInstanceCreate(context, 24, 0, &instance) == 0); - const std::uint32_t parameters[3] = {2, 48000, 0}; + const std::uint32_t parameters[3] = {2, 48000, mappingFamily}; std::vector batch(4096); AjmBatchInfo info{}; std::int64_t initResult[2] = {-1, -1}; @@ -161,6 +213,140 @@ void TestOpus(std::uint32_t context) { Require(sceAjmInstanceDestroy(context, instance) == 0); } +void TestOpusSurround(std::uint32_t context, std::uint32_t channels, std::size_t activeStream) { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 24, 0, &instance) == 0); + const std::uint32_t parameters[3] = {channels, 48000, 1}; + std::vector batch(4096); + AjmBatchInfo info{}; + std::int64_t initResult[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobInitialize(&info, instance, parameters, sizeof(parameters), initResult) == 0); + std::uint32_t id = 0; + AjmBatchError error{}; + Require(sceAjmBatchStart(context, &info, 0, &error, &id) == 0 && sceAjmBatchWait(context, id, 0, &error) == 0); + Require(initResult[0] == 0); + const std::size_t streamCount = channels == 3 || channels == 4 ? 2 : channels == 5 ? 3 : channels == 8 ? 5 : 4; + const std::size_t coupledCount = channels == 3 ? 1 : channels >= 7 ? 3 : 2; + std::size_t offset = 0; + for (std::size_t packetIndex = 0; packetIndex < 3; ++packetIndex) { + const std::size_t bytes = OPUS_STEREO[offset] | (std::size_t{OPUS_STEREO[offset + 1]} << 8u); + Require((OPUS_STEREO[offset + 2] & 3u) == 0 && bytes < 252); + std::vector packet{0, 0}; + for (std::size_t stream = 0; stream < streamCount; ++stream) { + if (stream == activeStream) { + packet.push_back(OPUS_STEREO[offset + 2]); + if (stream + 1 < streamCount) packet.push_back(static_cast(bytes - 1)); + packet.insert(packet.end(), OPUS_STEREO + offset + 3, OPUS_STEREO + offset + 2 + bytes); + } else { + packet.push_back(stream < coupledCount ? 0xfc : 0xf8); + if (stream + 1 < streamCount) packet.push_back(2); + packet.push_back(0xff); + packet.push_back(0xfe); + } + } + const std::size_t payload = packet.size() - 2; + packet[0] = static_cast(payload); + packet[1] = static_cast(payload >> 8u); + std::vector pcm(512 * channels, -12345); + DecodeSideband first{}; + RunDecode(context, instance, packet.data(), packet.size(), pcm.data(), pcm.size() * sizeof(std::int16_t), first); + Require(first.result == 0 && static_cast(first.inputConsumed) == packet.size() && first.outputWritten == static_cast(pcm.size() * sizeof(std::int16_t))); + std::vector rest(448 * channels, -12345); + DecodeSideband remaining{}; + RunDecode(context, instance, packet.data() + packet.size(), 0, rest.data(), rest.size() * sizeof(std::int16_t), remaining); + Require(remaining.result == 0 && remaining.inputConsumed == 0 && remaining.outputWritten == static_cast(rest.size() * sizeof(std::int16_t))); + Require(remaining.totalDecodedSamples == (packetIndex + 1) * 960); + pcm.insert(pcm.end(), rest.begin(), rest.end()); + for (std::size_t channel = 0; channel < channels; ++channel) { + int peak = 0; + for (std::size_t sample = channel; sample < pcm.size(); sample += channels) peak = std::max(peak, std::abs(static_cast(pcm[sample]))); + const std::size_t activeChannel = activeStream == 0 ? 0 : channels == 4 ? 2 : channels == 5 ? 3 : channels == 6 ? 4 : channels == 7 ? activeStream == 1 ? 5 : 2 : activeStream == 1 ? 6 : 4; + const std::size_t secondChannel = channels == 7 && activeStream == 2 ? 4 : activeChannel + 1; + const bool active = channel == activeChannel || channel == secondChannel; + Require(active ? packetIndex == 0 || peak > 900 : peak <= 2); + } + offset += 2 + bytes; + } + Require(sceAjmInstanceDestroy(context, instance) == 0); +} + +void TestOpusFamilyZeroChannels(std::uint32_t context) { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 24, 0, &instance) == 0); + const std::uint32_t parameters[3] = {4, 48000, 0}; + 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, sizeof(parameters), result) == 0); + std::uint32_t id = 0; + AjmBatchError error{}; + Require(sceAjmBatchStart(context, &info, 0, &error, &id) == 0 && sceAjmBatchWait(context, id, 0, &error) == 0); + Require(result[0] == 4 && result[1] == 0); + Require(sceAjmInstanceDestroy(context, instance) == 0); +} + +void TestOpusInputBeyondStatedData(std::uint32_t context, bool multipleFrames) { +#ifdef _WIN32 + SYSTEM_INFO system{}; + GetSystemInfo(&system); + const std::size_t pageSize = system.dwPageSize; + auto* pages = static_cast(VirtualAlloc(nullptr, 2 * pageSize, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + Require(pages != nullptr); + DWORD oldProtection = 0; + Require(VirtualProtect(pages + pageSize, pageSize, PAGE_NOACCESS, &oldProtection) != 0); +#else + const long hostPageSize = sysconf(_SC_PAGESIZE); + Require(hostPageSize > 0); + const std::size_t pageSize = static_cast(hostPageSize); + auto* pages = static_cast(mmap(nullptr, 2 * pageSize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(pages != MAP_FAILED); + Require(mprotect(pages + pageSize, pageSize, PROT_NONE) == 0); +#endif + const std::size_t packetBytes = 2 + (OPUS_STEREO[0] | (std::size_t{OPUS_STEREO[1]} << 8u)); + auto* packet = pages + pageSize - packetBytes; + std::memcpy(packet, OPUS_STEREO, packetBytes); + + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 24, 0, &instance) == 0); + const std::uint32_t parameters[3] = {2, 48000, 0}; + std::vector batch(4096); + AjmBatchInfo info{}; + std::int64_t initResult[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobInitialize(&info, instance, parameters, sizeof(parameters), initResult) == 0); + std::uint32_t id = 0; + AjmBatchError error{}; + Require(sceAjmBatchStart(context, &info, 0, &error, &id) == 0 && sceAjmBatchWait(context, id, 0, &error) == 0); + Require(initResult[0] == 0); + + std::vector pcm(960 * 2); + if (multipleFrames) { + constexpr std::uint64_t runMultipleFrames = 1ull << 12; + constexpr std::uint64_t sidebandStream = 1ull << 47; + struct { + DecodeSideband stream; + std::uint32_t frames; + std::uint32_t reserved; + } decoded{{-1, -1, 0, 0, 0}, 0, 0}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobRun(&info, instance, sidebandStream | runMultipleFrames, packet, 0xFFFFFE74u, pcm.data(), pcm.size() * sizeof(std::int16_t), &decoded, sizeof(decoded)) == 0); + Require(sceAjmBatchStart(context, &info, 0, &error, &id) == 0 && sceAjmBatchWait(context, id, 0, &error) == 0); + Require(decoded.stream.result == 0 && static_cast(decoded.stream.inputConsumed) == packetBytes && decoded.stream.outputWritten == 960 * 2 * 2 && decoded.frames == 1); + } else { + DecodeSideband sideband{}; + RunDecode(context, instance, packet, 0xFFFFFE74u, pcm.data(), pcm.size() * sizeof(std::int16_t), sideband); + Require(sideband.result == 0 && static_cast(sideband.inputConsumed) == packetBytes && sideband.outputWritten == 960 * 2 * 2); + } + Require(sceAjmInstanceDestroy(context, instance) == 0); +#ifdef _WIN32 + Require(VirtualFree(pages, 0, MEM_RELEASE) != 0); +#else + Require(munmap(pages, 2 * pageSize) == 0); +#endif +} + bool ParsesTo(const std::uint8_t* stream, std::uint32_t streamSize, std::uint64_t frameSize, std::uint32_t channels, std::uint32_t samples, std::uint32_t bitrate, std::uint32_t sampleRate) { AjmDecMp3ParseFrame frame; std::memset(&frame, 0xFF, sizeof(frame)); @@ -352,6 +538,116 @@ 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) { + Require(sceAjmInstanceCreate(context, 24, 0, &instances[index]) == 0); + const std::uint32_t parameters[3] = {1, 48000, index == 0 ? 0u : 255u}; + std::vector batch(4096); + AjmBatchInfo info{}; + std::int64_t initResult[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobInitialize(&info, instances[index], parameters, sizeof(parameters), initResult) == 0); + Submit(context, info); + Require(initResult[0] == 0); + } + std::size_t offset = 0; + int peak = 0; + for (int packet = 0; packet < 3; ++packet) { + const std::size_t bytes = OPUS_STEREO[offset] | (std::size_t{OPUS_STEREO[offset + 1]} << 8u); + std::vector pcm[2] = {std::vector(960, -12345), std::vector(960, -12345)}; + for (std::size_t index = 0; index < 2; ++index) { + DecodeSideband sideband{}; + RunDecode(context, instances[index], OPUS_STEREO + offset, 2 + bytes, pcm[index].data(), pcm[index].size() * sizeof(std::int16_t), sideband); + Require(sideband.result == 0 && static_cast(sideband.inputConsumed) == 2 + bytes && sideband.outputWritten == 960 * 2); + Require(sideband.totalDecodedSamples == static_cast(packet + 1) * 960); + } + Require(pcm[0] == pcm[1]); + if (packet > 0) { + for (const std::int16_t sample : pcm[1]) peak = std::max(peak, std::abs(static_cast(sample))); + } + offset += 2 + bytes; + } + Require(peak > 900 && peak < 1200); + + const std::uint32_t refusedParameters[1][3] = {{1, 48000, 2}}; + for (const auto& parameters : refusedParameters) { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 24, 0, &instance) == 0); + std::vector batch(4096); + AjmBatchInfo info{}; + std::int64_t initResult[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobInitialize(&info, instance, parameters, sizeof(parameters), initResult) == 0); + Require(Refused(context, info)); + Require(sceAjmInstanceDestroy(context, instance) == 0); + } + for (const std::uint32_t instance : instances) Require(sceAjmInstanceDestroy(context, instance) == 0); +} + void TestDecodeSingle(std::uint32_t context) { std::uint32_t instance = 0; Require(sceAjmInstanceCreate(context, 0, 0, &instance) == 0); @@ -366,6 +662,28 @@ void TestDecodeSingle(std::uint32_t context) { Require(sceAjmInstanceDestroy(context, instance) == 0); } +void TestDecodeSplit(std::uint32_t context) { + std::uint32_t whole = 0; + std::uint32_t split = 0; + Require(sceAjmInstanceCreate(context, 0, 0, &whole) == 0 && sceAjmInstanceCreate(context, 0, 0, &split) == 0); + std::vector reference(1152); + DecodeSideband expected{}; + RunDecode(context, whole, MP3_MONO, sizeof(MP3_MONO), reference.data(), reference.size() * sizeof(std::int16_t), expected); + std::vector first(512); + std::vector second(640); + const AjmBuffer input{const_cast(MP3_MONO), sizeof(MP3_MONO)}; + const AjmBuffer outputs[2] = {{first.data(), first.size() * sizeof(std::int16_t)}, {second.data(), second.size() * sizeof(std::int16_t)}}; + std::vector batch(0x100); + AjmBatchInfo info{}; + DecodeSideband sideband{}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobDecodeSplit(&info, split, &input, 1, outputs, 2, &sideband) == 0); + Submit(context, info); + Require(expected.result == 0 && sideband.result == 0 && sideband.inputConsumed == expected.inputConsumed && sideband.outputWritten == expected.outputWritten && sideband.totalDecodedSamples == expected.totalDecodedSamples); + Require(std::equal(first.begin(), first.end(), reference.begin()) && std::equal(second.begin(), second.end(), reference.begin() + 512)); + Require(sceAjmInstanceDestroy(context, whole) == 0 && sceAjmInstanceDestroy(context, split) == 0); +} + void TestGaplessDecode(std::uint32_t context) { std::uint32_t instance = 0; Require(sceAjmInstanceCreate(context, 0, 0, &instance) == 0); @@ -515,6 +833,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; @@ -687,11 +1036,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) { @@ -745,6 +1089,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; @@ -768,6 +1377,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)); @@ -920,6 +1589,55 @@ void TestResampleMp3(std::uint32_t context) { for (const std::uint32_t instance : {plain, faster, slower}) Require(sceAjmInstanceDestroy(context, instance) == 0); } +void TestResampleParametersEx(std::uint32_t context) { + std::uint32_t plain = 0; + std::uint32_t extended = 0; + Require(sceAjmInstanceCreate(context, 0, 0, &plain) == 0 && sceAjmInstanceCreate(context, 0, 0, &extended) == 0); + std::vector plainPcm(512); + std::vector extendedPcm(512); + for (const bool ex : {false, true}) { + std::vector batch(4096); + AjmBatchInfo info{}; + std::int64_t setResult[2] = {-1, -1}; + DecodeSideband sideband{}; + auto& pcm = ex ? extendedPcm : plainPcm; + const auto instance = ex ? extended : plain; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + if (ex) Require(sceAjmBatchJobSetResampleParametersEx(&info, instance, 2.0f, 0.0f, 1, setResult) == 0); + else Require(sceAjmBatchJobSetResampleParameters(&info, instance, 2.0f, 1, setResult) == 0); + Require(sceAjmBatchJobDecode(&info, instance, MP3_MONO, sizeof(MP3_MONO), pcm.data(), pcm.size() * sizeof(std::int16_t), &sideband) == 0); + Submit(context, info); + Require(setResult[0] == 0 && sideband.result == 0); + } + Require(extendedPcm == plainPcm); + Require(GetResampleInfo(context, extended).ratio == 2.0f && GetResampleInfo(context, extended).samples == GetResampleInfo(context, plain).samples); + + std::vector batch(4096); + AjmBatchInfo info{}; + 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)); + 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); + } + 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[] = { 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, @@ -955,6 +1673,238 @@ void TestResampleAt9(std::uint32_t context) { for (const std::uint32_t instance : {plain, faster, slower}) Require(sceAjmInstanceDestroy(context, instance) == 0); } +struct At9Run { + DecodeSideband stream; + std::uint32_t frames; + std::uint32_t reserved; +}; + +At9Run RunAt9Job(std::uint32_t context, std::uint32_t instance, std::uint64_t flags, const std::uint8_t* input, std::size_t inputSize) { + constexpr std::uint64_t runMultipleFrames = 1ull << 12; + constexpr std::uint64_t sidebandStream = 1ull << 47; + std::vector batch(4096); + std::vector pcm(1024); + At9Run decoded{{-1, -1, 0, 0, 0}, 0, 0}; + AjmBatchInfo info{}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + const std::size_t sidebandSize = (flags & runMultipleFrames) != 0 ? sizeof(decoded) : sizeof(decoded.stream); + Require(sceAjmBatchJobRun(&info, instance, sidebandStream | flags, input, inputSize, pcm.data(), pcm.size() * sizeof(std::int16_t), &decoded, sidebandSize) == 0); + Submit(context, info); + Require(decoded.stream.result == 0); + return decoded; +} + +void TestAt9GaplessSegments(std::uint32_t context) { + constexpr std::uint64_t runMultipleFrames = 1ull << 12; + std::vector stream(1024, 0); + std::memcpy(stream.data(), AT9_MONO_SILENT_SUPERFRAME, sizeof(AT9_MONO_SILENT_SUPERFRAME)); + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 1, 0, &instance) == 0); + const At9Control start{{512, 0, 0}, {0xFE, 0x70, 0x1F, 0xF0}, 0}; + std::int32_t result[2] = {-1, -1}; + RunControl(context, instance, CONTROL_START, &start, sizeof(start), result); + Require(result[0] == 0); + const auto first = RunAt9Job(context, instance, runMultipleFrames, stream.data(), stream.size()); + Require(first.stream.outputWritten == 512 * 2 && first.frames == 2); + Require(first.stream.inputConsumed > 0 && static_cast(first.stream.inputConsumed) < stream.size()); + const auto consumed = static_cast(first.stream.inputConsumed); + const auto second = RunAt9Job(context, instance, runMultipleFrames, stream.data() + consumed, stream.size() - consumed); + Require(second.stream.outputWritten == 512 * 2 && second.frames == 2); + Require(consumed + static_cast(second.stream.inputConsumed) == stream.size()); + Require(sceAjmInstanceDestroy(context, instance) == 0); +} + +void TestAt9RunDecodesWholeInput(std::uint32_t context) { + std::vector stream(1024, 0); + std::memcpy(stream.data(), AT9_MONO_SILENT_SUPERFRAME, sizeof(AT9_MONO_SILENT_SUPERFRAME)); + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 1, 0, &instance) == 0); + const At9Control start{{0, 0, 0}, {0xFE, 0x70, 0x1F, 0xF0}, 0}; + std::int32_t result[2] = {-1, -1}; + RunControl(context, instance, CONTROL_START, &start, sizeof(start), result); + Require(result[0] == 0); + const auto run = RunAt9Job(context, instance, 0, stream.data(), stream.size()); + Require(static_cast(run.stream.inputConsumed) == stream.size() && run.stream.outputWritten == 1024 * 2); + 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() { @@ -971,22 +1921,62 @@ int main() { const std::uint8_t badHeader[4] = {0xFD, 0x72, 0x1F, 0xF0}; Require(sceAjmDecAt9ParseConfigData(badHeader, &info) == invalidParameter); Require(sceAjmDecAt9ParseConfigData(nullptr, &info) == invalidParameter); + std::vector batch(4096); + AjmBatchInfo batchInfo{}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &batchInfo) == 0); + bool nullBuffersThrow = false; + try { + sceAjmBatchJobRunSplit(&batchInfo, 0, 0, nullptr, 1, nullptr, 0, nullptr, 0); + } catch (const std::runtime_error&) { + nullBuffersThrow = true; + } + Require(nullBuffersThrow && batchInfo.offset == 0); TestMp3ParseFrame(); TestMp3ParseOfl(); 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) { + TestOpusSurround(context, 4, stream); + TestOpusSurround(context, 5, stream); + TestOpusSurround(context, 6, stream); + } + for (std::size_t stream = 0; stream < 3; ++stream) { + TestOpusSurround(context, 7, stream); + TestOpusSurround(context, 8, stream); + } + TestOpusInputBeyondStatedData(context, false); + TestOpusInputBeyondStatedData(context, true); TestDecodeSingle(context); + TestDecodeSplit(context); TestGaplessDecode(context); 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/GuestAjmTeardown.cpp b/core/libs/tests/GuestAjmTeardown.cpp new file mode 100644 index 0000000000..c62f9ffff8 --- /dev/null +++ b/core/libs/tests/GuestAjmTeardown.cpp @@ -0,0 +1,81 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +namespace { + +using Initialize = int (APS5_VABI*)(std::int64_t, std::uint32_t*); +using InstanceCreate = int (APS5_VABI*)(std::uint32_t, std::uint32_t, std::uint64_t, std::uint32_t*); +using InstanceDestroy = int (APS5_VABI*)(std::uint32_t, std::uint32_t); +using BatchInitialize = int (APS5_VABI*)(void*, std::size_t, AjmBatchInfo*); +using BatchJobClearContext = int (APS5_VABI*)(AjmBatchInfo*, std::uint32_t, void*); +using BatchStart = int (APS5_VABI*)(std::uint32_t, const AjmBatchInfo*, int, AjmBatchError*, std::uint32_t*); +using BatchWait = int (APS5_VABI*)(std::uint32_t, std::uint32_t, std::uint32_t, AjmBatchError*); + +struct Functions { + Initialize initialize; + InstanceCreate instanceCreate; + InstanceDestroy instanceDestroy; + BatchInitialize batchInitialize; + BatchJobClearContext batchJobClearContext; + BatchStart batchStart; + BatchWait batchWait; +}; + +struct SidebandResult { + std::int32_t result; + std::int32_t internalResult; +}; + +Functions ajm{}; +std::uint32_t context = 0; +std::uint32_t instance = 0; + +[[noreturn]] void Fail(const char* message) { + std::fprintf(stderr, "%s\n", message); + std::fflush(stderr); + std::_Exit(1); +} + +template +T Resolve(void* library, const char* name) { + auto* symbol = dlsym(library, name); + if (symbol == nullptr) Fail(name); + return reinterpret_cast(symbol); +} + +void UseAfterStaticTeardown() { + alignas(16) std::uint8_t buffer[0x100]{}; + AjmBatchInfo info{}; + SidebandResult sideband{-1, -1}; + if (ajm.batchInitialize(buffer, sizeof(buffer), &info) != 0 || ajm.batchJobClearContext(&info, instance, &sideband) != 0) Fail("a batch cannot be built after static teardown"); + std::uint32_t batch = 0; + AjmBatchError error{}; + if (ajm.batchStart(context, &info, 0, &error, &batch) != 0) Fail("sceAjmBatchStart failed after static teardown"); + if (sideband.result != 0) Fail("an instance created before exit is unknown after static teardown"); + if (ajm.batchWait(context, batch, 0, &error) != 0) Fail("sceAjmBatchWait does not find a batch started after static teardown"); + if (ajm.instanceDestroy(context, instance) != 0) Fail("sceAjmInstanceDestroy does not find the instance after static teardown"); +} + +} + +int main(int argc, char** argv) { + if (argc != 2) Fail("usage: guest_ajm_teardown_tests "); + if (std::atexit(UseAfterStaticTeardown) != 0) Fail("atexit failed"); + auto* library = dlopen(argv[1], RTLD_NOW); + if (library == nullptr) Fail(dlerror()); + ajm = { + Resolve(library, "sceAjmInitialize"), + Resolve(library, "sceAjmInstanceCreate"), + Resolve(library, "sceAjmInstanceDestroy"), + Resolve(library, "sceAjmBatchInitialize"), + Resolve(library, "sceAjmBatchJobClearContext"), + Resolve(library, "sceAjmBatchStart"), + Resolve(library, "sceAjmBatchWait"), + }; + if (ajm.initialize(0, &context) != 0 || ajm.instanceCreate(context, 0, 0, &instance) != 0) Fail("AJM setup failed"); + return 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/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 new file mode 100644 index 0000000000..5312249a50 --- /dev/null +++ b/core/libs/tests/GuestAllocatorFailures.cpp @@ -0,0 +1,219 @@ +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#ifdef __linux__ +#include +#endif + +extern "C" { +void* APS5_VABI malloc_nid_postfix(std::size_t); +void* APS5_VABI calloc_nid_postfix(std::size_t, std::size_t); +void* APS5_VABI realloc_nid_postfix(void*, std::size_t); +void* APS5_VABI memalign_nid_postfix(std::size_t, std::size_t); +void* APS5_VABI aligned_alloc_nid_postfix(std::size_t, std::size_t); +int APS5_VABI posix_memalign_nid_postfix(void**, std::size_t, std::size_t); +void APS5_VABI free_nid_postfix(void*); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +constexpr int NoMemory = 12; +constexpr int InvalidArgument = 22; +constexpr std::size_t Max = std::numeric_limits::max(); +constexpr std::size_t HalfPlusOne = static_cast(std::numeric_limits::max()) + 1; + +int failures = 0; + +#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; +} + +template +void expectNull(const char* what, int expectedError, TCall call) { + *__error_nid_postfix() = 0; + void* result = nullptr; + try { + result = call(); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s: threw %s instead of returning NULL\n", what, error.what()); + ++failures; + return; + } catch (...) { + report(what, "threw a non-standard exception instead of returning NULL"); + return; + } + if (result != nullptr) { + report(what, "returned a block"); + free_nid_postfix(result); + return; + } + if (*__error_nid_postfix() != expectedError) { + std::fprintf(stderr, "%s: errno %d, expected %d\n", what, *__error_nid_postfix(), expectedError); + ++failures; + } +} + +template +void* expectBlock(const char* what, TCall call) { + *__error_nid_postfix() = 77; + void* result = nullptr; + try { + result = call(); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s: threw %s\n", what, error.what()); + ++failures; + return nullptr; + } + if (result == nullptr) report(what, "returned NULL"); + if (*__error_nid_postfix() != 77) report(what, "changed errno on success"); + return result; +} + +} + +int main() { + void* api[10]{}; + ApplicationHeapRegister_nid_no_patch(api); + + expectNull("malloc(SIZE_MAX)", NoMemory, [] { return malloc_nid_postfix(Max); }); + expectNull("malloc(PTRDIFF_MAX + 1)", NoMemory, [] { return malloc_nid_postfix(HalfPlusOne); }); + expectNull("calloc(SIZE_MAX, 2)", NoMemory, [] { return calloc_nid_postfix(Max, 2); }); + expectNull("calloc(2^33, 2^33)", NoMemory, [] { return calloc_nid_postfix(std::size_t{1} << 33, std::size_t{1} << 33); }); + expectNull("calloc(PTRDIFF_MAX + 1, 1)", NoMemory, [] { return calloc_nid_postfix(HalfPlusOne, 1); }); + expectNull("realloc(NULL, SIZE_MAX)", NoMemory, [] { return realloc_nid_postfix(nullptr, Max); }); + expectNull("aligned_alloc(64, SIZE_MAX)", NoMemory, [] { return aligned_alloc_nid_postfix(64, Max); }); + expectNull("memalign(64, PTRDIFF_MAX + 1)", NoMemory, [] { return memalign_nid_postfix(64, HalfPlusOne); }); + expectNull("aligned_alloc(24, 64)", InvalidArgument, [] { return aligned_alloc_nid_postfix(24, 64); }); + expectNull("aligned_alloc(0, 64)", InvalidArgument, [] { return aligned_alloc_nid_postfix(0, 64); }); + expectNull("memalign(24, 64)", InvalidArgument, [] { return memalign_nid_postfix(24, 64); }); + + auto* block = static_cast(expectBlock("malloc(40)", [] { return malloc_nid_postfix(40); })); + if (block != nullptr) { + std::memset(block, 0x6b, 40); + expectNull("realloc(block, SIZE_MAX)", NoMemory, [block] { return realloc_nid_postfix(block, Max); }); + expectNull("realloc(block, PTRDIFF_MAX + 1)", NoMemory, [block] { return realloc_nid_postfix(block, HalfPlusOne); }); + for (unsigned index = 0; index < 40; ++index) { + if (block[index] != 0x6b) report("realloc failure", "damaged the original block"); + } + free_nid_postfix(block); + } + +#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{}; + if (getrlimit(RLIMIT_DATA, &originalLimit) != 0) { + report("failed commit setup", "could not read RLIMIT_DATA"); + return 1; + } + auto limited = originalLimit; + limited.rlim_cur = std::min(limited.rlim_cur, rlim_t{128} << 20); + if (setrlimit(RLIMIT_DATA, &limited) != 0) { + report("failed commit setup", "could not limit RLIMIT_DATA"); + return 1; + } +#endif + constexpr std::size_t beyondCommitLimit = std::size_t{300} << 30; + constexpr std::size_t probeBytes = std::size_t{100} << 30; + void* before = GuestArena::GuestArenaAllocate_nid_postfix(probeBytes, 0x4000); + GuestArena::GuestArenaRelease_nid_postfix(before, probeBytes); + bool committed = false; + int failedCommits = 0; + for (int attempt = 0; attempt < 10 && !committed; ++attempt) { + *__error_nid_postfix() = 0; + void* huge = nullptr; + try { + huge = malloc_nid_postfix(beyondCommitLimit); + } catch (const std::exception& error) { + std::fprintf(stderr, "malloc(300 GiB): threw %s\n", error.what()); + ++failures; + break; + } + if (huge != nullptr) { + free_nid_postfix(huge); + committed = true; + } else if (*__error_nid_postfix() != NoMemory) { + report("malloc(300 GiB)", "returned NULL without ENOMEM"); + } else { + ++failedCommits; + } + } +#ifdef __linux__ + if (setrlimit(RLIMIT_DATA, &originalLimit) != 0) report("failed commit cleanup", "could not restore RLIMIT_DATA"); +#endif + if (committed) { +#ifdef __linux__ + 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 + } else { + if (failedCommits != 10) report("failed heap commits", "did not return ENOMEM for all ten attempts"); + void* after = nullptr; + try { + after = GuestArena::GuestArenaAllocate_nid_postfix(probeBytes, 0x4000); + GuestArena::GuestArenaRelease_nid_postfix(after, probeBytes); + } catch (const std::exception& error) { + std::fprintf(stderr, "failed commit range probe: %s\n", error.what()); + ++failures; + } + if (after != before) report("malloc(300 GiB)", "left its arena range reserved after failing"); + std::fprintf(stderr, "Checked %d failed heap commits and arena range reuse\n", failedCommits); + } + void* small = malloc_nid_postfix(100); + if (small == nullptr) report("malloc(100) after a failed huge malloc", "returned NULL"); + free_nid_postfix(small); + void* large = malloc_nid_postfix(std::size_t{1} << 20); + if (large == nullptr) report("malloc(1 MiB) after a failed huge malloc", "returned NULL"); + free_nid_postfix(large); + } +#endif + + void* untouched = &failures; + *__error_nid_postfix() = 77; + if (posix_memalign_nid_postfix(&untouched, HalfPlusOne, 8) != NoMemory) report("posix_memalign(PTRDIFF_MAX + 1)", "did not return ENOMEM"); + if (posix_memalign_nid_postfix(&untouched, 64, HalfPlusOne) != NoMemory) report("posix_memalign(64, PTRDIFF_MAX + 1)", "did not return ENOMEM"); + if (posix_memalign_nid_postfix(&untouched, 64, Max) != NoMemory) report("posix_memalign(64, SIZE_MAX)", "did not return ENOMEM"); + if (untouched != &failures || *__error_nid_postfix() != 77) report("posix_memalign failure", "touched the output or errno"); + + free_nid_postfix(expectBlock("calloc(3, 5)", [] { return calloc_nid_postfix(3, 5); })); + free_nid_postfix(expectBlock("calloc(0, 5)", [] { return calloc_nid_postfix(0, 5); })); + free_nid_postfix(expectBlock("aligned_alloc(128, 1)", [] { return aligned_alloc_nid_postfix(128, 1); })); + free_nid_postfix(expectBlock("memalign(4096, 1)", [] { return memalign_nid_postfix(4096, 1); })); + void* first = expectBlock("malloc(0)", [] { return malloc_nid_postfix(0); }); + void* second = expectBlock("malloc(0)", [] { return malloc_nid_postfix(0); }); + if (first != nullptr && first == second) report("malloc(0)", "returned the same block twice"); + free_nid_postfix(first); + free_nid_postfix(second); + + return failures == 0 ? 0 : 1; +} diff --git a/core/libs/tests/GuestAmmPlacement.cpp b/core/libs/tests/GuestAmmPlacement.cpp new file mode 100644 index 0000000000..db65b897bf --- /dev/null +++ b/core/libs/tests/GuestAmmPlacement.cpp @@ -0,0 +1,41 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceAmprAmmGetVirtualAddressRanges(std::uint64_t*, std::uint64_t*, std::uint64_t*, std::uint64_t*); +} + +static void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +int main() { + try { + std::uintptr_t arenaBase = 0; + std::size_t arenaBytes = 0; + GuestArena::GuestArenaRange_nid_postfix(&arenaBase, &arenaBytes); + Require(arenaBase != 0 && arenaBytes != 0, "the guest arena is unavailable"); + const std::uint64_t arenaEnd = arenaBase + arenaBytes; + constexpr std::uint64_t occupied = 4ull << 20; + GuestArena::GuestArenaMarkUsed_nid_postfix(reinterpret_cast(arenaEnd - occupied), occupied); + + std::uint64_t start = 0; + std::uint64_t end = 0; + std::uint64_t multimapStart = 0; + std::uint64_t multimapEnd = 0; + Require(sceAmprAmmGetVirtualAddressRanges(&start, &end, &multimapStart, &multimapEnd) == 0, "AMM ranges were not reported"); + 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(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) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index 21fa65958d..ca17fc2c23 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -1,21 +1,30 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/Apr/include/AprCommandBuffer.hpp" #include +#include #include #include #include #include #include +#include #include #include +#include +#include #include #include +#ifdef _WIN32 +#include +#endif extern "C" { int APS5_VABI sceAmprCommandBufferConstructor(Apr::CommandBufferObject*); int APS5_VABI sceAmprAprCommandBufferConstructor(Apr::CommandBufferObject*, std::uint64_t*, std::uint64_t*); int APS5_VABI sceAmprCommandBufferSetBuffer(Apr::CommandBufferObject*, void*, std::uint32_t); +int APS5_VABI sceAmprCommandBufferReset(Apr::CommandBufferObject*); void* APS5_VABI sceAmprCommandBufferClearBuffer(Apr::CommandBufferObject*); std::uint32_t APS5_VABI sceAmprCommandBufferGetCurrentOffset(const Apr::CommandBufferObject*); std::uint32_t APS5_VABI sceAmprCommandBufferGetNumCommands(const Apr::CommandBufferObject*); @@ -31,6 +40,8 @@ std::uint64_t APS5_VABI sceAmprMeasureCommandSizePopMarker(); 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); +int APS5_VABI sceKernelAprSubmitCommandBufferAndGetId(const Apr::CommandBufferObject*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceKernelAprWaitCommandBuffer(std::uint32_t); int APS5_VABI sceAmprCommandBufferWaitOnAddress(Apr::CommandBufferObject*, volatile std::uint64_t*, std::uint64_t, std::uint8_t, std::uint8_t); int APS5_VABI sceAmprCommandBufferWaitOnCounter(Apr::CommandBufferObject*, std::uint8_t, std::uint32_t, std::uint8_t, std::uint8_t); int APS5_VABI sceAmprCommandBufferWriteCounterOnCompletion(Apr::CommandBufferObject*, std::uint8_t, std::uint32_t); @@ -58,7 +69,11 @@ std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteCounter_04_00(std::uint8_t int APS5_VABI sceAmprCommandBufferConstructNop(Apr::CommandBufferObject*, std::int16_t, const void*, std::uint32_t, const std::uint32_t*); int APS5_VABI sceAmprCommandBufferConstructMarker(Apr::CommandBufferObject*, std::uint32_t, const char*, const std::uint32_t*); int APS5_VABI sceKernelAprResolveFilepathsToIds(const char**, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceKernelAprResolveFilepathsToIdsAndFileSizes(const char**, std::uint32_t, std::uint32_t*, std::uint64_t*, std::uint32_t*); +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); @@ -68,6 +83,7 @@ std::uint64_t APS5_VABI sceAmprMeasureCommandSizeReadFileScatter(void*, std::uin std::uint64_t APS5_VABI sceAmprMeasureCommandSizeReadFileGatherScatter(void*, std::uint64_t, std::uint64_t); std::uint64_t APS5_VABI sceAmprMeasureCommandSizeResetGatherScatterState(); std::size_t APS5_VABI sceKernelGetDirectMemorySize(); +int APS5_VABI sceAmprAmmGetUsageStatsData(void*); int APS5_VABI sceAmprAmmCommandBufferConstructor(Apr::CommandBufferObject*); int APS5_VABI sceAmprAmmCommandBufferMap(Apr::CommandBufferObject*, std::uint64_t, std::uint64_t, std::int32_t, std::int32_t); int APS5_VABI sceAmprAmmCommandBufferMapWithGpuMaskId(Apr::CommandBufferObject*, std::uint64_t, std::uint64_t, std::int32_t, std::int32_t, std::uint8_t); @@ -92,9 +108,14 @@ std::uint64_t APS5_VABI sceAmprMeasureCommandSizeMapBegin(std::uint64_t, std::ui std::uint64_t APS5_VABI sceAmprMeasureCommandSizeMapDirectBegin(std::uint64_t, std::uint64_t, std::uint64_t, std::uint32_t, std::uint32_t); std::uint64_t APS5_VABI sceAmprMeasureCommandSizeMapEnd(); int APS5_VABI sceKernelQueryMemoryProtection(void*, void**, void**, int*); +int APS5_VABI sceKernelReserveVirtualRange(void**, std::size_t, int, std::size_t); +int APS5_VABI sceKernelMapFlexibleMemory(void**, std::size_t, int, int); +int APS5_VABI sceKernelMunmap(void*, std::size_t); int APS5_VABI sceAmprAmmCommandBufferMapAsPrt(Apr::CommandBufferObject*, std::uint64_t, std::uint64_t); int APS5_VABI sceAmprAmmCommandBufferAllocatePaForPrt(Apr::CommandBufferObject*, std::uint64_t, std::uint64_t, std::int32_t, std::int32_t); int APS5_VABI sceAmprAmmCommandBufferRemapIntoPrt(Apr::CommandBufferObject*, std::uint64_t, std::uint64_t, std::uint64_t, std::int32_t, std::uint32_t); +int APS5_VABI sceKernelWriteModifyMtypeProtectCommand(void*, std::uint64_t, std::uint64_t, std::uint64_t, std::uint64_t, std::uint64_t, std::uint64_t*); +int APS5_VABI sceKernelWriteRemapIntoPrtCommand(std::uint32_t*, std::uint64_t, std::uint64_t, std::uint64_t, int, int, std::uint64_t*); int APS5_VABI sceAmprAmmCommandBufferUnmapToPrt(Apr::CommandBufferObject*, std::uint64_t, std::uint64_t); std::int64_t APS5_VABI sceAmprAmmMeasureAmmCommandSizeMapAsPrt(std::uint64_t, std::uint64_t); std::int64_t APS5_VABI sceAmprAmmMeasureAmmCommandSizeAllocatePaForPrt(std::uint64_t, std::uint64_t, std::int32_t, std::int32_t); @@ -136,7 +157,8 @@ static void RequireAt(bool value, int line) { namespace { constexpr int invalidArgument = static_cast(0x80020016); -constexpr int bufferFull = static_cast(0x8002001C); +constexpr int permissionDenied = static_cast(0x80020001); +constexpr int busy = static_cast(0x80020010); constexpr std::uint32_t color = 0xFF8040u; struct Recorder { @@ -160,7 +182,10 @@ void RequireAppended(const Recorder& recorder, std::uint32_t offset, std::uint32 Require(recorder.Commands() == commands + 1); Apr::CommandHeader header; std::memcpy(&header, recorder.memory.data() + offset, sizeof(header)); - Require(header.opcode == opcode && header.bytes == measured); + if (opcode == Apr::Opcode::CompactReadFileGather) { + Require((static_cast(header.opcode) & 0xffu) == static_cast(opcode)); + Require(measured == sizeof(Apr::CompactReadFileGatherCommand)); + } else Require(header.opcode == opcode && header.bytes == measured); } void RequireRecorded(const Recorder& recorder, std::uint32_t offset, std::uint32_t commands, Apr::Opcode opcode, std::uint64_t measured, const std::string& text) { @@ -218,6 +243,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() { @@ -226,19 +261,66 @@ 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() { + constexpr std::array invalidOffsets{33u, 64u, 0xffffffffu}; + for (const auto offset : invalidOffsets) { + Recorder recorder(32); + recorder.memory.fill(0xA5); + recorder.buffer.offset = offset; + recorder.buffer.numCommands = 7; + const auto before = recorder.buffer; + const auto memory = recorder.memory; + const auto rejected = [&](const auto& record) { + bool threw = false; + try { + record(); + } catch (const std::invalid_argument&) { + threw = true; + } + Require(threw); + Require(recorder.memory == memory); + Require(recorder.buffer.base == before.base && recorder.buffer.size == before.size); + Require(recorder.buffer.offset == before.offset && recorder.buffer.numCommands == before.numCommands); + Require(recorder.buffer.type == before.type && recorder.buffer.recording == before.recording); + }; + rejected([&] { sceAmprCommandBufferPopMarker(&recorder.buffer); }); + rejected([&] { sceAmprCommandBufferNop(&recorder.buffer, 1); }); + rejected([&] { sceAmprCommandBufferPushMarker(&recorder.buffer, "bounds"); }); + rejected([&] { sceAmprAmmCommandBufferMap(&recorder.buffer, 0x4000, 0x4000, 0, 3); }); + rejected([&] { sceAmprAmmCommandBufferMapAsPrt(&recorder.buffer, 0x4000, 0x4000); }); + } } void TestSubmission() { @@ -290,6 +372,53 @@ void TestWaits() { Require(submitted.get() == 0 && done == 1); } +void TestWaitSatisfiedAfterSubmission() { + Recorder first; + Recorder second; + alignas(8) std::uint64_t flag = 0; + alignas(8) std::uint64_t done = 0; + alignas(8) std::uint64_t later = 0; + Require(sceAmprCommandBufferWaitOnAddress(&first.buffer, &flag, 1, 0, 0) == 0); + Require(sceAmprCommandBufferWriteAddressOnCompletion(&first.buffer, &done, 1) == 0); + Require(sceAmprCommandBufferWriteAddressOnCompletion(&second.buffer, &later, 2) == 0); + std::uint32_t firstId = 0; + auto submitted = std::async(std::launch::async, [&]() { return sceKernelAprSubmitCommandBufferAndGetId(&first.buffer, 0, &firstId); }); + const bool returned = submitted.wait_for(std::chrono::seconds(5)) == std::future_status::ready; + if (!returned) std::atomic_ref(flag).store(1, std::memory_order_release); + Require(returned); + Require(submitted.get() == 0 && firstId != 0); + std::uint32_t secondId = 0; + Require(sceKernelAprSubmitCommandBufferAndGetId(&second.buffer, 0, &secondId) == 0 && secondId > firstId); + Require(std::atomic_ref(done).load(std::memory_order_acquire) == 0); + Require(std::atomic_ref(later).load(std::memory_order_acquire) == 0); + std::atomic_ref(flag).store(1, std::memory_order_release); + Require(sceKernelAprWaitCommandBuffer(secondId) == 0); + Require(done == 1 && later == 2); + Require(sceKernelAprWaitCommandBuffer(firstId) == 0); +} + +void TestAmmNotBehindAprWait() { + Recorder reader; + Recorder maps; + alignas(8) std::uint64_t flag = 0; + alignas(8) std::uint64_t done = 0; + Require(sceAmprCommandBufferWaitOnAddress(&reader.buffer, &flag, 1, 0, 0) == 0); + Require(sceAmprCommandBufferWriteAddressOnCompletion(&reader.buffer, &done, 1) == 0); + Require(sceAmprAmmCommandBufferConstructor(&maps.buffer) == 0); + Require(sceAmprCommandBufferWriteAddressOnCompletion(&maps.buffer, &flag, 1) == 0); + std::uint32_t readerId = 0; + Require(sceKernelAprSubmitCommandBufferAndGetId(&reader.buffer, 0, &readerId) == 0 && readerId != 0); + std::uint32_t mapsId = 0; + Require(sceAmprAmmSubmitCommandBuffer3(maps.memory.data(), maps.Offset(), 0, &mapsId) == 0 && mapsId != 0); + auto completed = std::async(std::launch::async, [&]() { return sceAmprAmmWaitCommandBufferCompletion(mapsId); }); + const bool returned = completed.wait_for(std::chrono::seconds(5)) == std::future_status::ready; + if (!returned) std::atomic_ref(flag).store(1, std::memory_order_release); + Require(returned); + Require(completed.get() == 0); + Require(sceKernelAprWaitCommandBuffer(readerId) == 0); + Require(done == 1); +} + void TestCounters() { Recorder recorder; alignas(8) std::uint64_t single = 0; @@ -514,6 +643,187 @@ bool MatchesFile(const std::uint8_t* data, std::size_t offset, std::size_t bytes return true; } +void TestZeroFilledBuffer() { + const char* path = "ampr_zero_filled.bin"; + { + std::ofstream file(path, std::ios::binary); + for (std::size_t offset = 0; offset < 256; ++offset) file.put(static_cast(FileByte(offset))); + } + std::uint32_t fileId = 0; + std::uint32_t failed = 0; + Require(sceKernelAprResolveFilepathsToIds(&path, 1, &fileId, &failed) == 0); + KernelEqueue eq = 0; + Require(sceKernelCreateEqueue(&eq, "ampr") == 0); + Require(sceKernelAddAmprEvent(eq, 0, nullptr) == 0); + + alignas(8) std::array memory{}; + Apr::CommandBufferObject buffer{}; + std::uint64_t gatherState = 0; + std::uint64_t scatterState = 0; + Require(sceAmprCommandBufferSetBuffer(&buffer, memory.data(), static_cast(memory.size())) == 0); + Require(sceAmprCommandBufferReset(&buffer) == 0); + std::array data{}; + std::uint64_t done = 0; + Require(sceAmprAprCommandBufferReadFile(&buffer, &gatherState, &scatterState, fileId, data.data(), data.size(), 64) == 0); + Require(sceAmprCommandBufferWriteAddressOnCompletion(&buffer, &done, 0x1234567) == 0); + Require(sceAmprCommandBufferWriteKernelEventQueue_04_00(&buffer, static_cast(eq), 0, 0, 0) == 0); + Require(sceKernelAprSubmitCommandBuffer(&buffer, 1) == 0); + Require(MatchesFile(data.data(), 64, data.size()) && done == 0x1234567); + + KernelEvent event{}; + int count = 0; + const KernelUseconds poll = 0; + Require(sceKernelWaitEqueue(eq, &event, 1, &count, &poll) == 0 && count == 1); + Require(sceKernelGetEventFilter(&event) == -25 && sceKernelGetEventId(&event) == 0); + Require(sceKernelDeleteAmprEvent(eq, 0) == 0); + Require(sceKernelDeleteEqueue(eq) == 0); + std::remove(path); +} + +void TestReadPastEndOfFileZeroFills() { + const std::filesystem::path directory = "ampr_read_past_end"; + std::filesystem::remove_all(directory); + Require(std::filesystem::create_directory(directory)); + const auto file = directory / "settings.ini"; + { + std::ofstream stream(file, std::ios::binary); + for (std::size_t offset = 0; offset < 64; ++offset) stream.put(static_cast(FileByte(offset))); + } + const std::string filePath = file.generic_string(); + const char* path = filePath.c_str(); + std::uint32_t fileId = 0; + std::uint64_t size = 0; + std::uint32_t failed = 0; + Require(sceKernelAprResolveFilepathsToIdsAndFileSizes(&path, 1, &fileId, &size, &failed) == 0 && size == 64); + { + std::ofstream stream(file, std::ios::binary | std::ios::trunc); + for (std::size_t offset = 1; offset < size; ++offset) stream.put(static_cast(FileByte(offset))); + } + + std::array whole; + std::array continued; + std::array atEnd; + std::array pastEnd; + whole.fill(0xA5); + continued.fill(0xA5); + atEnd.fill(0xA5); + pastEnd.fill(0xA5); + Recorder recorder; + Require(sceAmprAprCommandBufferReadFile(&recorder.buffer, &recorder.gatherState, &recorder.scatterState, fileId, whole.data(), size, 0) == 0); + Require(sceAmprAprCommandBufferReadFileScatter(&recorder.buffer, &recorder.gatherState, &recorder.scatterState, continued.data(), 16) == 0); + Require(sceAmprAprCommandBufferReadFileGather(&recorder.buffer, &recorder.gatherState, &recorder.scatterState, 8, 0) == 0); + Require(sceAmprAprCommandBufferReadFile(&recorder.buffer, &recorder.gatherState, &recorder.scatterState, fileId, atEnd.data(), atEnd.size(), size - 1) == 0); + Require(sceAmprAprCommandBufferReadFile(&recorder.buffer, &recorder.gatherState, &recorder.scatterState, fileId, pastEnd.data(), pastEnd.size(), 4096) == 0); + Require(sceKernelAprSubmitCommandBuffer(&recorder.buffer, 0) == 0); + Require(MatchesFile(whole.data(), 1, size - 1) && whole[size - 1] == 0); + for (std::size_t index = 0; index < 16; ++index) Require(continued[index] == 0); + Require(MatchesFile(continued.data() + 16, 1, 8)); + for (const auto byte : atEnd) Require(byte == 0); + for (const auto byte : pastEnd) Require(byte == 0); + std::filesystem::remove_all(directory); +} + +void TestReadAtUnseekableOffsetThrows() { + const char* path = "ampr_unseekable_offset.bin"; + { std::ofstream file(path, std::ios::binary); file << "data"; } + std::uint32_t fileId = 0; + std::uint32_t failed = 0; + Require(sceKernelAprResolveFilepathsToIds(&path, 1, &fileId, &failed) == 0); + std::array data{}; + const Apr::ReadFileCommand command{{Apr::Opcode::ReadFile, sizeof(Apr::ReadFileCommand)}, fileId, 0, reinterpret_cast(data.data()), data.size(), std::uint64_t{1} << 63u}; + alignas(8) std::array memory{}; + std::memcpy(memory.data(), &command, sizeof(command)); + Apr::CommandBufferObject buffer{Apr::BufferType::Generic, 0, sizeof(memory), 1, sizeof(memory), memory.data()}; + bool rejected = false; + try { + sceKernelAprSubmitCommandBuffer(&buffer, 0); + } catch (const std::runtime_error& error) { + rejected = std::string(error.what()).find("returned 0 of 4 bytes") != std::string::npos; + } + Require(rejected); + std::remove(path); +} + +void TestResolveFailures() { + const char* paths[]{"ampr_resolve_existing.bin", "ampr_resolve_missing.bin"}; + { std::ofstream file(paths[0], std::ios::binary); file << "data"; } + std::uint32_t ids[]{123, 123}; + std::uint64_t sizes[]{123, 123}; + std::uint32_t failed = 99; + *__error_nid_postfix() = 0; + Require(sceKernelAprResolveFilepathsToIdsAndFileSizes(paths, 2, ids, sizes, &failed) == -1); + Require(*__error_nid_postfix() == 2 && failed == 1 && ids[1] == 0xffffffffu && sizes[1] == 0); + Require(sizes[0] == 4); + int results[]{123, 123}; + Require(sceKernelAprResolveFilepathsToIdsAndFileSizesForEach(paths, 2, ids, sizes, results) == 1); + Require(results[0] == 0 && results[1] == static_cast(0x80020002)); + Require(ids[1] == 0xffffffffu && sizes[0] == 4 && sizes[1] == 0); + for (const char* invalid : {"ampr_resolve_missing.bin/../ampr_resolve_existing.bin", "ampr_resolve_existing.bin/../ampr_resolve_existing.bin"}) { + Require(sceKernelAprResolveFilepathsToIds(&invalid, 1, ids, &failed) == -1); + Require(*__error_nid_postfix() == 2 && failed == 0 && ids[0] == 0xffffffffu); + } + std::remove(paths[0]); +} + +void TestMalformedCommandSizes() { + const auto requireFailure = [](Apr::Opcode opcode, std::uint32_t recordBytes, std::uint32_t bufferBytes, const char* expected) { + std::array memory{}; + const Apr::CommandHeader header{opcode, recordBytes}; + std::memcpy(memory.data(), &header, sizeof(header)); + Apr::CommandBufferObject buffer{Apr::BufferType::Generic, 0, bufferBytes, 1, bufferBytes, memory.data()}; + bool rejected = false; + try { + sceKernelAprSubmitCommandBuffer(&buffer, 0); + } catch (const std::runtime_error& error) { + rejected = std::string(error.what()).starts_with(expected); + } + Require(rejected); + }; + + requireFailure(Apr::Opcode::ReadFile, sizeof(Apr::CommandHeader), sizeof(Apr::CommandHeader), "APR: truncated command payload"); + requireFailure(Apr::Opcode::WriteAddress, sizeof(Apr::CommandHeader), sizeof(Apr::CommandHeader), "APR: truncated command payload"); + requireFailure(Apr::Opcode::Nop, std::numeric_limits::max(), 2u * sizeof(Apr::CommandHeader), "APR: malformed command"); + + std::array memory{}; + const Apr::CommandHeader first{Apr::Opcode::Nop, sizeof(Apr::CommandHeader)}; + const Apr::CommandHeader overflowing{Apr::Opcode::Nop, std::numeric_limits::max()}; + std::memcpy(memory.data(), &first, sizeof(first)); + std::memcpy(memory.data() + sizeof(first), &overflowing, sizeof(overflowing)); + Apr::CommandBufferObject buffer{Apr::BufferType::Generic, 0, sizeof(memory), 2, sizeof(memory), memory.data()}; + bool rejected = false; + try { + sceKernelAprSubmitCommandBuffer(&buffer, 0); + } catch (const std::runtime_error& error) { + rejected = std::string(error.what()).starts_with("APR: malformed command"); + } + Require(rejected); +} + +void TestResolveSymbolicLink() { + const std::filesystem::path directory = "ampr_resolve_links"; + std::filesystem::remove_all(directory); + Require(std::filesystem::create_directory(directory)); + { std::ofstream file(directory / "target.bin", std::ios::binary); file << "linked data"; } + const auto link = directory / "link.bin"; +#ifdef _WIN32 + if (CreateSymbolicLinkW(link.c_str(), L"target.bin", SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) == 0) { + Require(GetLastError() == ERROR_PRIVILEGE_NOT_HELD); + std::fprintf(stderr, "APR symbolic link check skipped: creating symbolic links needs Developer Mode or administrator rights\n"); + std::filesystem::remove_all(directory); + return; + } +#else + std::filesystem::create_symlink("target.bin", link); +#endif + const std::string linkPath = link.generic_string(); + const char* path = linkPath.c_str(); + std::uint32_t id = 0; + std::uint64_t size = 0; + std::uint32_t failed = 0; + Require(sceKernelAprResolveFilepathsToIdsAndFileSizes(&path, 1, &id, &size, &failed) == 0 && size == 11); + std::filesystem::remove_all(directory); +} + void TestGatherScatter() { const char* path = "ampr_gather_scatter.bin"; { @@ -524,6 +834,15 @@ void TestGatherScatter() { std::uint32_t failed = 0; Require(sceKernelAprResolveFilepathsToIds(&path, 1, &fileId, &failed) == 0); + Recorder compact(64); + std::array compactData{}; + Require(sceAmprAprCommandBufferReadFile(&compact.buffer, &compact.gatherState, &compact.scatterState, fileId, compactData.data(), 8, 100) == 0); + Require(sceAmprAprCommandBufferReadFileGather(&compact.buffer, &compact.gatherState, &compact.scatterState, 8, 108) == 0); + Require(sceAmprAprCommandBufferReadFileGather(&compact.buffer, &compact.gatherState, &compact.scatterState, 8, 116) == 0); + Require(compact.Offset() == 64); + Require(sceKernelAprSubmitCommandBuffer(&compact.buffer, 0) == 0); + Require(MatchesFile(compactData.data(), 100, compactData.size())); + Recorder recorder; auto* buffer = &recorder.buffer; auto* map = &recorder.gatherState; @@ -539,7 +858,7 @@ void TestGatherScatter() { auto offset = recorder.Offset(); auto commands = recorder.Commands(); Require(sceAmprAprCommandBufferReadFileGather(buffer, map, state, 8, 500) == 0); - RequireAppended(recorder, offset, commands, Apr::Opcode::ReadFileGather, sceAmprMeasureCommandSizeReadFileGather(8, 500)); + RequireAppended(recorder, offset, commands, Apr::Opcode::CompactReadFileGather, sceAmprMeasureCommandSizeReadFileGather(8, 500)); offset = recorder.Offset(); commands = recorder.Commands(); Require(sceAmprAprCommandBufferReadFileScatter(buffer, map, state, second.data(), 8) == 0); @@ -579,7 +898,7 @@ void TestGatherScatter() { Require(sceAmprAprCommandBufferReadFile(nullptr, map, state, fileId, first.data(), 4, 0) == invalidArgument); Require(empty.Offset() == 0 && empty.Commands() == 0); Require(sceAmprMeasureCommandSizeReadFileGather(0, 0) == rejected); - Require(sceAmprMeasureCommandSizeReadFileGather(0x100000000ull, 0x10000000000ull - 1u) == sizeof(Apr::ReadFileCommand)); + Require(sceAmprMeasureCommandSizeReadFileGather(0x100000000ull, 0x10000000000ull - 1u) == sizeof(Apr::CompactReadFileGatherCommand)); Require(sceAmprMeasureCommandSizeReadFileGather(4, 0x10000000000ull) == rejected); Require(sceAmprMeasureCommandSizeReadFileScatter(first.data(), 0x100000001ull) == rejected); Require(sceAmprMeasureCommandSizeReadFileScatter(high, 4) == rejected); @@ -588,8 +907,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; @@ -599,6 +916,53 @@ volatile std::uint64_t& At(std::uint64_t address) { return *reinterpret_cast(address); } +void TestAmmRangesAboveHintedMaps() { + constexpr std::uint64_t step = 0x400000; + void* span = nullptr; + Require(sceKernelReserveVirtualRange(&span, 4 * step, 0, 0) == 0); + Require(sceKernelMunmap(span, 4 * step) == 0); + const auto heap = reinterpret_cast(span); + void* placed = span; + Require(sceKernelMapFlexibleMemory(&placed, page, 3, 0) == 0 && placed == span); + + std::uint64_t start = 0; + std::uint64_t end = 0; + std::uint64_t multimapStart = 0; + std::uint64_t multimapEnd = 0; + Require(sceAmprAmmGetVirtualAddressRanges(&start, &end, &multimapStart, &multimapEnd) == 0); + std::uintptr_t arenaBase = 0; + std::size_t arenaBytes = 0; + GuestArena::GuestArenaRange_nid_postfix(&arenaBase, &arenaBytes); + Require(start >= arenaBase && multimapEnd <= arenaBase + arenaBytes && heap + 4 * step <= start); + + for (std::uint64_t hint = heap + step; hint < heap + 4 * step; hint += step) { + void* mapped = reinterpret_cast(hint); + Require(sceKernelMapFlexibleMemory(&mapped, page, 3, 0) == 0); + Require(reinterpret_cast(mapped) == hint); + Require(hint + page <= start || hint >= multimapEnd); + Require(sceKernelMunmap(mapped, page) == 0); + } + Require(sceKernelMunmap(span, page) == 0); +} + +void TestAmmUsageStats() { + std::array stats{}; + Require(sceAmprAmmGetUsageStatsData(nullptr) == static_cast(0x80020016)); + stats[0] = 0x19; + Require(sceAmprAmmGetUsageStatsData(stats.data()) == static_cast(0x80020016)); + stats = {0x18, 0xdeadbeef, 0xdeadbeef, 0xdeadbeef}; + Require(sceAmprAmmGetUsageStatsData(stats.data()) == 0); + Require(stats[0] == 0x18); + Require((stats[1] & 0xffffu) == 512u && (stats[1] >> 16u) == 0u); + Require((stats[2] & 0xffffffffu) == 7u && (stats[2] >> 32u) == 0u); + stats = {0x8, 0xdeadbeef, 0xdeadbeef, 0xdeadbeef}; + Require(sceAmprAmmGetUsageStatsData(stats.data()) == 0); + Require(stats[0] == 0x8 && stats[1] == 0xdeadbeef && stats[2] == 0xdeadbeef); + stats = {0x0, 0xdeadbeef, 0xdeadbeef, 0xdeadbeef}; + Require(sceAmprAmmGetUsageStatsData(stats.data()) == 0); + Require(stats[0] == 0 && stats[1] == 0xdeadbeef); +} + void TestAmm() { std::uint64_t start = 0; std::uint64_t end = 0; @@ -740,6 +1104,102 @@ void SubmitAmm(Recorder& recorder) { Require(sceAmprAmmWaitCommandBufferCompletion(id) == 0); } +void TestAmmRemapKeepsPoolReferences() { + std::uint64_t start = 0; + std::uint64_t end = 0; + std::uint64_t multimapStart = 0; + std::uint64_t multimapEnd = 0; + Require(sceAmprAmmGetVirtualAddressRanges(&start, &end, &multimapStart, &multimapEnd) == 0); + std::int64_t pool = -1; + Require(sceAmprAmmGiveDirectMemory(0, static_cast(sceKernelGetDirectMemorySize()), page, page, 1, &pool) == 0); + + const auto source = start + 256 * page; + const auto alias = multimapStart + 256 * page; + const auto fresh = start + 257 * page; + const auto lastAlias = multimapStart + 257 * page; + const auto overlappingAlias = multimapStart + 258 * page; + Recorder maps; + Require(sceAmprAmmCommandBufferMap(&maps.buffer, source, page, 0, cpuReadWrite) == 0); + Require(sceAmprAmmCommandBufferMultiMap(&maps.buffer, alias, source, page, cpuReadWrite) == 0); + SubmitAmm(maps); + At(source) = 0xA1; + Require(At(alias) == 0xA1); + + Recorder move; + Require(sceAmprAmmCommandBufferRemap(&move.buffer, alias, source, page, cpuReadWrite) == 0); + SubmitAmm(move); + Require(Protection(source) == 0 && At(alias) == 0xA1); + + Recorder exhausted; + Require(sceAmprAmmCommandBufferMap(&exhausted.buffer, fresh, page, 0, cpuReadWrite) == 0); + const auto requireExhausted = [&] { + bool rejected = false; + try { + SubmitAmm(exhausted); + } catch (const std::runtime_error& error) { + rejected = std::string(error.what()).starts_with("AMM: no free run of "); + } + Require(rejected); + }; + requireExhausted(); + At(alias) = 0xB1; + Require(At(alias) == 0xB1); + + Recorder retain; + Require(sceAmprAmmCommandBufferRemap(&retain.buffer, alias, alias, page, cpuReadWrite) == 0); + Require(sceAmprAmmCommandBufferMultiMap(&retain.buffer, lastAlias, alias, page, cpuReadWrite) == 0); + Require(sceAmprAmmCommandBufferRemap(&retain.buffer, lastAlias, alias, 2 * page, cpuReadWrite) == 0); + Require(sceAmprAmmCommandBufferUnmap(&retain.buffer, lastAlias, page) == 0); + SubmitAmm(retain); + Require(At(overlappingAlias) == 0xB1); + requireExhausted(); + + Recorder release; + Require(sceAmprAmmCommandBufferUnmap(&release.buffer, overlappingAlias, page) == 0); + Require(sceAmprAmmCommandBufferMap(&release.buffer, fresh, page, 0, cpuReadWrite) == 0); + SubmitAmm(release); + Require(At(fresh) == 0xB1); + At(fresh) = 0xC1; + Require(At(fresh) == 0xC1); +} + +void TestAmmMapFailureKeepsPoolOwnership() { + std::uint64_t start = 0; + std::uint64_t end = 0; + std::uint64_t multimapStart = 0; + std::uint64_t multimapEnd = 0; + Require(sceAmprAmmGetVirtualAddressRanges(&start, &end, &multimapStart, &multimapEnd) == 0); + std::int64_t pool = -1; + Require(sceAmprAmmGiveDirectMemory(0, static_cast(sceKernelGetDirectMemorySize()), page, page, 1, &pool) == 0); + const auto original = start + 512 * page; + const auto fresh = start + 516 * page; + + Recorder mapping; + Require(sceAmprAmmCommandBufferMap(&mapping.buffer, original, page, 0, cpuReadWrite) == 0); + SubmitAmm(mapping); + At(original) = 0xA1; + SubmitAmm(mapping); + Require(At(original) == 0xA1 && Protection(original) == cpuReadWrite); + + Recorder oversized; + Require(sceAmprAmmCommandBufferMap(&oversized.buffer, original, 2 * page, 0, cpuReadWrite) == 0); + bool rejected = false; + try { + SubmitAmm(oversized); + } catch (const std::runtime_error& error) { + rejected = std::string(error.what()).starts_with("AMM: no free run of "); + } + Require(rejected); + Require(Protection(original) == 0 && Protection(original + page) == 0); + + Recorder reuse; + Require(sceAmprAmmCommandBufferMap(&reuse.buffer, fresh, page, 0, cpuReadWrite) == 0); + SubmitAmm(reuse); + Require(At(fresh) == 0xA1); + At(fresh) = 0xB1; + Require(At(fresh) == 0xB1 && Protection(original) == 0); +} + void TestAmmRemapAndProtect() { std::uint64_t start = 0; std::uint64_t end = 0; @@ -922,13 +1382,94 @@ void TestAmmPrt() { Require(sceAmprAmmCommandBufferRemapIntoPrt(&unbound, prt, source, page, cpuReadWrite, 0) == permissionDenied); Require(sceAmprAmmCommandBufferUnmapToPrt(&unbound, prt, page) == permissionDenied); std::array loose{}; - Apr::CommandBufferObject sized{nullptr, static_cast(loose.size()), 0, 0, Apr::BufferType::Generic, 0}; + Apr::CommandBufferObject sized{Apr::BufferType::Generic, 0, 0, 0, static_cast(loose.size()), nullptr}; Require(sceAmprAmmCommandBufferMapAsPrt(&sized, prt, page) == permissionDenied); Require(sceAmprAmmMeasureAmmCommandSizeMapAsPrt(prt, 0) == ammRejected); Require(sceAmprAmmMeasureAmmCommandSizeAllocatePaForPrt(prt, page, 0, 0x08) == ammRejected); Require(sceAmprAmmMeasureAmmCommandSizeAllocatePaForPrt(prt + 8, page, 0, cpuReadWrite) == ammRejected); } +void TestKernelWriteCommands() { + std::uint64_t start = 0; + std::uint64_t end = 0; + std::uint64_t multimapStart = 0; + std::uint64_t multimapEnd = 0; + Require(sceAmprAmmGetVirtualAddressRanges(&start, &end, &multimapStart, &multimapEnd) == 0); + std::int64_t pool = -1; + Require(sceAmprAmmGiveDirectMemory(0, static_cast(sceKernelGetDirectMemorySize()), 4 * page, page, 1, &pool) == 0); + + const std::uint64_t target = start + 0x400000; + const std::uint64_t source = start + 0x440000; + const std::uint64_t prt = start + 0x480000; + + Recorder setup; + Require(sceAmprAmmCommandBufferMap(&setup.buffer, target, 2 * page, 0, cpuReadWrite) == 0); + Require(sceAmprAmmCommandBufferMap(&setup.buffer, source, page, 0, cpuReadWrite) == 0); + Require(sceAmprAmmCommandBufferMapAsPrt(&setup.buffer, prt, 2 * page) == 0); + SubmitAmm(setup); + At(source) = 0x9C; + + std::uint64_t measured = 0; + Require(sceKernelWriteModifyMtypeProtectCommand(nullptr, target, 2 * page, 5, 0x13, 0x10, &measured) == 0); + Require(measured == sizeof(Apr::AmmProtectCommand)); + std::array written{}; + std::uint64_t reported = 0; + Require(sceKernelWriteModifyMtypeProtectCommand(written.data(), target, 2 * page, 5, 0x13, 0x10, &reported) == 0); + Require(reported == sizeof(Apr::AmmProtectCommand)); + Apr::CommandHeader header; + std::memcpy(&header, written.data(), sizeof(header)); + Require(header.opcode == Apr::Opcode::AmmModifyMtypeProtect && header.bytes == sizeof(Apr::AmmProtectCommand)); + Recorder sibling; + Require(sceAmprAmmCommandBufferModifyMtypeProtect(&sibling.buffer, target, 2 * page, 5, 0x13, 0x10) == 0); + Require(std::memcmp(written.data(), sibling.memory.data(), sizeof(Apr::AmmProtectCommand)) == 0); + Require(sceKernelWriteModifyMtypeProtectCommand(written.data(), target, 2 * page, 5, 0x13, 0x10, nullptr) == 0); + std::uint32_t id = 0; + Require(sceAmprAmmSubmitCommandBuffer3(written.data(), static_cast(reported), 0, &id) == 0); + Require(sceAmprAmmWaitCommandBufferCompletion(id) == 0); + Require(Protection(target) == 0x13 && Protection(target + page) == 0x13); + At(target) = 0x9D; + Require(At(target) == 0x9D); + + std::uint64_t prtMeasured = 0; + Require(sceKernelWriteRemapIntoPrtCommand(nullptr, prt + page, source, page, cpuReadWrite, 0, &prtMeasured) == 0); + Require(prtMeasured == sizeof(Apr::AmmRemapCommand)); + std::array prtWritten{}; + Require(sceKernelWriteRemapIntoPrtCommand(reinterpret_cast(prtWritten.data()), prt + page, source, page, cpuReadWrite, 0, &prtMeasured) == 0); + Apr::AmmRemapCommand remap; + std::memcpy(&remap, prtWritten.data(), sizeof(remap)); + Require(remap.header.opcode == Apr::Opcode::AmmRemapIntoPrt && remap.header.bytes == sizeof(remap)); + Require(remap.address == prt + page && remap.source == source && remap.size == page && remap.protection == cpuReadWrite && remap.reserved == 1011); + Require(sceKernelWriteRemapIntoPrtCommand(reinterpret_cast(prtWritten.data()), prt + page, source, page, cpuReadWrite, 42, &prtMeasured) == 0); + std::memcpy(&remap, prtWritten.data(), sizeof(remap)); + Require(remap.reserved == 42); + Recorder prtSibling; + Require(sceAmprAmmCommandBufferRemapIntoPrt(&prtSibling.buffer, prt + page, source, page, cpuReadWrite, 0) == 0); + std::array prtSubmit{}; + Require(sceKernelWriteRemapIntoPrtCommand(reinterpret_cast(prtSubmit.data()), prt + page, source, page, cpuReadWrite, 0, nullptr) == 0); + Require(std::memcmp(prtSubmit.data(), prtSibling.memory.data(), sizeof(Apr::AmmRemapCommand)) == 0); + Require(sceAmprAmmSubmitCommandBuffer3(prtSubmit.data(), static_cast(sizeof(Apr::AmmRemapCommand)), 0, &id) == 0); + Require(sceAmprAmmWaitCommandBufferCompletion(id) == 0); + Require(At(prt + page) == 0x9C); + + std::array untouched{}; + untouched.fill(0xCC); + std::uint64_t sentinel = 0xDDDDDDDDDDDDDDDDULL; + Require(sceKernelWriteModifyMtypeProtectCommand(untouched.data(), target + 8, page, 0, 0x03, 0x03, &sentinel) == invalidArgument); + Require(sceKernelWriteModifyMtypeProtectCommand(untouched.data(), target, 0, 0, 0x03, 0x03, &sentinel) == invalidArgument); + Require(sceKernelWriteModifyMtypeProtectCommand(untouched.data(), 0, page, 0, 0x03, 0x03, &sentinel) == invalidArgument); + Require(sceKernelWriteModifyMtypeProtectCommand(untouched.data(), target, page, 0, 0x04, 0x03, &sentinel) == invalidArgument); + Require(sceKernelWriteModifyMtypeProtectCommand(untouched.data(), target, page, 0, 0x03, 0x04, &sentinel) == invalidArgument); + Require(sceKernelWriteModifyMtypeProtectCommand(untouched.data(), ~std::uint64_t(0), page, 0, 0x03, 0x03, &sentinel) == invalidArgument); + Require(sceKernelWriteModifyMtypeProtectCommand(untouched.data(), target, page, 0, 0x100000000ULL, 0x03, &sentinel) == invalidArgument); + Require(sceKernelWriteRemapIntoPrtCommand(reinterpret_cast(untouched.data()), prt, source + 8, page, cpuReadWrite, 0, &sentinel) == invalidArgument); + Require(sceKernelWriteRemapIntoPrtCommand(reinterpret_cast(untouched.data()), prt, source, 0, cpuReadWrite, 0, &sentinel) == invalidArgument); + Require(sceKernelWriteRemapIntoPrtCommand(reinterpret_cast(untouched.data()), prt, source, page, 0x04, 0, &sentinel) == invalidArgument); + Require(sentinel == 0xDDDDDDDDDDDDDDDDULL); + for (const auto byte : untouched) Require(byte == 0xCC); + Require(sceKernelWriteModifyMtypeProtectCommand(nullptr, target, page, 0, 0x03, 0x03, nullptr) == 0); + Require(sceKernelWriteRemapIntoPrtCommand(nullptr, prt, source, page, cpuReadWrite, 0, nullptr) == 0); +} + } int main() { @@ -939,9 +1480,12 @@ int main() { TestRecording(std::string(300, 'a')); TestRejectedArguments(); TestFullBuffer(); + TestOffsetPastBuffer(); TestSubmission(); TestKernelEventQueue(); TestWaits(); + TestWaitSatisfiedAfterSubmission(); + TestAmmNotBehindAprWait(); TestCounters(); TestRejectedWaitsAndCounters(); TestNops(); @@ -949,9 +1493,20 @@ int main() { TestVersionedCounters(); TestClearBuffer(); TestConstructed(); + TestZeroFilledBuffer(); + TestReadPastEndOfFileZeroFills(); + TestReadAtUnseekableOffsetThrows(); + TestResolveFailures(); + TestMalformedCommandSizes(); + TestResolveSymbolicLink(); TestGatherScatter(); + TestAmmRangesAboveHintedMaps(); + TestAmmUsageStats(); + TestAmmRemapKeepsPoolReferences(); + TestAmmMapFailureKeepsPoolOwnership(); TestAmm(); TestAmmRemapAndProtect(); TestAmmPrt(); + TestKernelWriteCommands(); return 0; } diff --git a/core/libs/tests/GuestAmprCppError.S b/core/libs/tests/GuestAmprCppError.S new file mode 100644 index 0000000000..f5e0769766 --- /dev/null +++ b/core/libs/tests/GuestAmprCppError.S @@ -0,0 +1,75 @@ +.cfi_sections .eh_frame +.text +.globl GuestCatch +GuestCatch: + .cfi_startproc +#ifdef _WIN32 + .cfi_personality 0x00, __gxx_personality_v0_nid_postfix + .cfi_lsda 0x00, .Lcatch_lsda +#else + .cfi_personality 0x9b, .Lcatch_personality + .cfi_lsda 0x1b, .Lcatch_lsda +#endif + pushq %rbp + .cfi_def_cfa_offset 16 + .cfi_offset %rbp, -16 + movq %rsp, %rbp + .cfi_def_cfa_register %rbp + subq $16, %rsp +.Lcatch_call: + call _ZSt16_Throw_Cpp_errori_nid_postfix +.Lcatch_call_end: + ud2 +.Lcatch_landing: + cmpq $2, %rdx + jne .Lcatch_bad + movq %rax, %rdi + call __cxa_begin_catch_nid_postfix + movq %rax, %rdi + call FixtureInspect + call __cxa_end_catch_nid_postfix + leave + .cfi_def_cfa %rsp, 8 + ret +.Lcatch_bad: + movl $1, %edi + call FixtureFail + ud2 + .cfi_endproc +#ifdef _WIN32 +.section .rdata,"dr" +#else +.section .data.rel.ro,"aw",@progbits +.p2align 3 +.Lcatch_personality: + .quad __gxx_personality_v0_nid_postfix +.section .rodata +#endif +.Lcatch_lsda: +#ifdef _WIN32 + .byte 0xff, 0x80 +#else + .byte 0xff, 0x9b +#endif + .uleb128 .Lcatch_types_end-.Lcatch_types_offset +.Lcatch_types_offset: + .byte 0x01 + .uleb128 .Lcatch_calls_end-.Lcatch_calls +.Lcatch_calls: + .uleb128 .Lcatch_call-GuestCatch + .uleb128 .Lcatch_call_end-.Lcatch_call + .uleb128 .Lcatch_landing-GuestCatch + .uleb128 1 +.Lcatch_calls_end: + .byte 1, 1, 2, 0 +#ifdef _WIN32 + .quad FixtureCatchType + .quad FixtureOtherType +#else + .long FixtureCatchType-. + .long FixtureOtherType-. +#endif +.Lcatch_types_end: +#ifndef _WIN32 +.section .note.GNU-stack,"",@progbits +#endif diff --git a/core/libs/tests/GuestAmprCppError.cpp b/core/libs/tests/GuestAmprCppError.cpp new file mode 100644 index 0000000000..8e2407c199 --- /dev/null +++ b/core/libs/tests/GuestAmprCppError.cpp @@ -0,0 +1,162 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/GuestLocale.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +struct TypeRecord { const void* vtable; const char* name; const TypeRecord* base; }; +struct Category { const void* const* vtable; }; +struct ErrorCode { int value; const Category* category; }; +struct SystemError { const void* const* vtable; const char* message; ErrorCode code; }; +static_assert(sizeof(SystemError) == 32 && offsetof(SystemError, code) == 16); + +using GuestWhat = const char* (APS5_VABI *)(const SystemError*); +using CategoryDelete = void (APS5_VABI *)(const Category*); +using CategoryName = const char* (APS5_VABI *)(const Category*); +using CategoryMessage = void* (APS5_VABI *)(void*, const Category*, int); +using CategoryCondition = ErrorCode (APS5_VABI *)(const Category*, int); +using CategoryEquivalentCondition = bool (APS5_VABI *)(const Category*, int, const ErrorCode*); +using CategoryEquivalentCode = bool (APS5_VABI *)(const Category*, const ErrorCode*, int); + +struct Expected { int value; const char* what; }; +static constexpr Expected Errors[] { + {16, "device or resource busy: device or resource busy"}, + {22, "invalid argument: invalid argument"}, + {3, "no such process: no such process"}, + {12, "not enough memory: not enough memory"}, + {1, "operation not permitted: operation not permitted"}, + {11, "resource deadlock would occur: resource deadlock would occur"}, + {35, "resource unavailable try again: operation would block"} +}; + +extern "C" { +extern const unsigned char _ZTISt9exception_nid_postfix[]; +extern const unsigned char _ZTISt11logic_error_nid_postfix[]; +extern const unsigned char _ZTISt13runtime_error_nid_postfix[]; +extern const unsigned char _ZTISt12system_error_nid_postfix[]; +[[noreturn]] void APS5_VABI _ZSt16_Throw_Cpp_errori_nid_postfix(int code); +void APS5_VABI _ZNSt12system_errorC1ERKS__nid_postfix(SystemError* self, const SystemError* source); +SystemError* APS5_VABI _ZNSt12system_erroraSERKS__nid_postfix(SystemError* self, const SystemError* source); +void APS5_VABI _ZNSt12system_errorD1Ev_nid_postfix(SystemError* self); +void APS5_VABI _ZdlPv_nid_postfix(void* pointer); +void APS5_VABI GuestCatch(int code); + +const void* FixtureCatchType = nullptr; +const void* FixtureOtherType = nullptr; +const Expected* FixtureExpected = nullptr; +unsigned FixtureInspected = 0; + +[[noreturn]] void APS5_VABI FixtureFail(unsigned code) { + std::fprintf(stderr, "guest _Throw_Cpp_error failure %u\n", code); + std::exit(static_cast(code)); +} +} + +static void Require(bool value, unsigned code) { + if (!value) FixtureFail(code); +} + +template +static TFunction Slot(const void* const* vtable, int index) { + return reinterpret_cast(const_cast(vtable[index])); +} + +static const TypeRecord* TypeOf(const void* const* vtable, int destroySlot) { + return static_cast(vtable[destroySlot - 1]); +} + +static void CheckCategory(const SystemError& error) { + const Category* category = error.code.category; + Require(category != nullptr, 20); + const TypeRecord* type = TypeOf(category->vtable, 0); + Require(std::strcmp(type->name, "St23_Generic_error_category") == 0, 21); + Require(type->base && std::strcmp(type->base->name, "St14error_category") == 0, 22); + Require(std::strcmp(Slot(category->vtable, 2)(category), "generic") == 0, 23); + const ErrorCode condition = Slot(category->vtable, 4)(category, error.code.value); + Require(condition.value == error.code.value && condition.category == category, 24); + const ErrorCode other {error.code.value + 1, category}; + const ErrorCode foreign {error.code.value, nullptr}; + auto equivalentCondition = Slot(category->vtable, 5); + Require(equivalentCondition(category, error.code.value, &condition), 25); + Require(!equivalentCondition(category, error.code.value, &other), 26); + Require(!equivalentCondition(category, error.code.value, &foreign), 27); + auto equivalentCode = Slot(category->vtable, 6); + Require(equivalentCode(category, &error.code, error.code.value), 28); + Require(!equivalentCode(category, &error.code, error.code.value + 1), 29); + Require(!equivalentCode(category, &foreign, error.code.value), 30); + GuestLocale::String result {}; + Require(Slot(category->vtable, 3)(&result, category, error.code.value) == &result, 33); + const char* data = result.capacity <= 15 ? result.buffer : result.pointer; + Require(std::strcmp(data, std::strstr(FixtureExpected->what, ": ") + 2) == 0, 34); + if (result.capacity > 15) _ZdlPv_nid_postfix(result.pointer); + bool deleteThrew = false; + try { + Slot(category->vtable, 1)(category); + } catch (const std::invalid_argument&) { + deleteThrew = true; + } + Require(deleteThrew, 32); +} + +static void CheckCopies(const SystemError& error) { + SystemError copy {}; + _ZNSt12system_errorC1ERKS__nid_postfix(©, &error); + Require(copy.vtable == error.vtable && copy.message == error.message, 40); + Require(copy.code.value == error.code.value && copy.code.category == error.code.category, 41); + SystemError assigned {}; + _ZNSt12system_errorC1ERKS__nid_postfix(&assigned, &error); + assigned.code = {0, nullptr}; + Require(_ZNSt12system_erroraSERKS__nid_postfix(&assigned, &error) == &assigned, 42); + Require(assigned.code.value == error.code.value && assigned.code.category == error.code.category, 43); + _ZNSt12system_errorD1Ev_nid_postfix(&assigned); + _ZNSt12system_errorD1Ev_nid_postfix(©); + Require(copy.message == nullptr && assigned.message == nullptr, 44); +} + +extern "C" void APS5_VABI FixtureInspect(void* object) { + const auto& error = *static_cast(object); + Require(std::strcmp(TypeOf(error.vtable, 0)->name, "St12system_error") == 0, 10); + const char* what = Slot(error.vtable, 2)(&error); + Require(what && std::strcmp(what, FixtureExpected->what) == 0, 11); + Require(error.code.value == FixtureExpected->value, 12); + CheckCategory(error); + CheckCopies(error); + ++FixtureInspected; +} + +static void Catch(const void* type, int code) { + FixtureCatchType = type; + FixtureExpected = &Errors[code]; + const auto before = FixtureInspected; + GuestCatch(code); + Require(FixtureInspected == before + 1, 2); +} + +static void CheckOutOfRange(int code) { + bool threw = false; + try { + _ZSt16_Throw_Cpp_errori_nid_postfix(code); + } catch (const std::out_of_range& error) { + threw = std::string(error.what()).find(std::to_string(code)) != std::string::npos; + } + Require(threw, 3); +} + +int main() { + const std::array api {}; + ApplicationHeapRegister_nid_no_patch(api.data()); + FixtureOtherType = _ZTISt11logic_error_nid_postfix; + for (int code = 0; code < static_cast(std::size(Errors)); ++code) Catch(_ZTISt12system_error_nid_postfix, code); + Catch(_ZTISt13runtime_error_nid_postfix, 5); + Catch(_ZTISt9exception_nid_postfix, 6); + CheckOutOfRange(-1); + CheckOutOfRange(static_cast(std::size(Errors))); + std::puts("guest _Throw_Cpp_error system_error, generic category and range: PASS"); + return 0; +} diff --git a/core/libs/tests/GuestAmprThreads.cpp b/core/libs/tests/GuestAmprThreads.cpp new file mode 100644 index 0000000000..403c445304 --- /dev/null +++ b/core/libs/tests/GuestAmprThreads.cpp @@ -0,0 +1,584 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/exceptions/Runtime.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifndef _WIN32 +#include +#include +#endif + +struct GuestXtime { + std::int64_t sec; + std::int64_t nsec; +}; + +struct PadTarget { + std::atomic* order; + std::atomic* destroyed; + int beforeRelease; + int afterRelease; + unsigned exitCode; +}; + +struct GuestTypeInfo { + const void* vtable; + const char* name; +}; + +struct GuestSystemError { + const void* vtable; + const char* message; + int code; + const void* category; +}; + +using GuestTerminate = void (APS5_VABI*)(); + +struct TestPad { + unsigned (APS5_VABI* const* vtable)(TestPad*); + void* condition; + void* mutex; + bool started; + PadTarget target; +}; + +extern "C" { +void APS5_VABI _ZNSt4_PadC2Ev_nid_postfix(TestPad* pad); +void APS5_VABI _ZNSt4_PadD2Ev_nid_postfix(TestPad* pad); +void APS5_VABI _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(TestPad* pad, Pthread* thread); +void APS5_VABI _ZNSt4_Pad8_ReleaseEv_nid_postfix(TestPad* pad); +int APS5_VABI _Thrd_join_nid_postfix(Pthread thread, int* code); +int APS5_VABI _Thrd_sleep_nid_postfix(const GuestXtime* target, GuestXtime* remaining); +void APS5_VABI _Lock_shared_ptr_spin_lock_nid_postfix(void); +void APS5_VABI _Unlock_shared_ptr_spin_lock_nid_postfix(void); +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); +int APS5_VABI _Cnd_signal_nid_postfix(void** handle); +int APS5_VABI _Cnd_init_with_name_nid_postfix(void** handle, const char* name); +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 _Mtx_init_nid_postfix(void** handle, int type); +void APS5_VABI _Mtx_destroy_nid_postfix(void** handle); +int APS5_VABI _Mtx_lock_nid_postfix(void** handle); +int APS5_VABI _Mtx_unlock_nid_postfix(void** handle); +GuestTerminate APS5_VABI _ZSt13set_terminatePFvvE_nid_postfix(GuestTerminate handler); +const GuestTypeInfo* APS5_VABI __cxa_current_exception_type_nid_postfix(); +void* APS5_VABI __cxa_current_primary_exception_nid_postfix(); +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; +static constexpr int ThrdError = 4; +static constexpr int GuestEbusy = 16; +static constexpr int GuestEinval = 22; +static constexpr int GuestErange = 34; +static constexpr int GuestEilseq = 86; +static constexpr std::int64_t NanosPerSecond = 1000000000; +static constexpr auto Failed = static_cast(-1); + +#define Require(value) RequireAt((value), __LINE__) + +static void RequireAt(bool value, int line) { + if (value) return; + std::fprintf(stderr, "requirement failed at line %d\n", line); + std::abort(); +} + +static std::int64_t RealtimeNanos() { + KernelTimespec now{}; + Require(clock_gettime_nid_postfix(0, &now) == 0); + return now.tv_sec * NanosPerSecond + now.tv_nsec; +} + +static GuestXtime XtimeAfter(std::int64_t nanos) { + const auto target = RealtimeNanos() + nanos; + return {target / NanosPerSecond, target % NanosPerSecond}; +} + +static void* APS5_VABI ReturnArgument(void* arg) { + return arg; +} + +static void* APS5_VABI WaitForRelease(void* arg) { + auto* release = static_cast*>(arg); + while (!release->load()) std::this_thread::yield(); + return nullptr; +} + +struct SpinContext { + std::atomic inside{0}; + std::atomic overlapped{false}; + int counter = 0; +}; + +static constexpr int SpinThreads = 4; +static constexpr int SpinIterations = 20000; + +static void* APS5_VABI SpinWorker(void* arg) { + auto& context = *static_cast(arg); + for (int iteration = 0; iteration < SpinIterations; ++iteration) { + _Lock_shared_ptr_spin_lock_nid_postfix(); + if (context.inside.fetch_add(1) != 0) context.overlapped = true; + ++context.counter; + context.inside.fetch_sub(1); + _Unlock_shared_ptr_spin_lock_nid_postfix(); + } + return nullptr; +} + +static Pthread Start(PthreadEntry entry, void* arg) { + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, entry, arg, nullptr) == 0); + return thread; +} + +static void TestThreadJoin() { + int code = 0; + Require(_Thrd_join_nid_postfix(Start(ReturnArgument, reinterpret_cast(std::intptr_t{42})), &code) == ThrdSuccess); + Require(code == 42); + code = 0; + Require(_Thrd_join_nid_postfix(Start(ReturnArgument, reinterpret_cast(std::intptr_t{-7})), &code) == ThrdSuccess); + Require(code == -7); + code = 5; + Require(_Thrd_join_nid_postfix(Start(ReturnArgument, reinterpret_cast(std::intptr_t{0x100000003})), &code) == ThrdSuccess); + Require(code == 3); + Require(_Thrd_join_nid_postfix(Start(ReturnArgument, nullptr), nullptr) == ThrdSuccess); + + static std::atomic release{false}; + const auto detached = Start(WaitForRelease, &release); + Require(pthread_detach_nid_postfix(detached) == 0); + code = 9; + Require(_Thrd_join_nid_postfix(detached, &code) == ThrdError); + Require(code == 9); + release = true; +} + +static void TestThreadSleep() { + const auto target = XtimeAfter(50000000); + Require(_Thrd_sleep_nid_postfix(&target, nullptr) == 0); + Require(RealtimeNanos() >= target.sec * NanosPerSecond + target.nsec); + + const auto past = XtimeAfter(-NanosPerSecond); + const auto before = RealtimeNanos(); + Require(_Thrd_sleep_nid_postfix(&past, nullptr) == 0); + Require(RealtimeNanos() - before < NanosPerSecond / 2); + + const auto carried = XtimeAfter(30000000); + const GuestXtime unnormalized{carried.sec - 2, carried.nsec + 2 * NanosPerSecond}; + Require(_Thrd_sleep_nid_postfix(&unnormalized, nullptr) == 0); + Require(RealtimeNanos() >= carried.sec * NanosPerSecond + carried.nsec); + + const auto borrowed = XtimeAfter(20000000); + const GuestXtime negative{borrowed.sec + 1, borrowed.nsec - NanosPerSecond}; + 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); + } catch (const std::invalid_argument&) { + rejected = true; + } + Require(rejected); +} + +static void TestSpinLock() { + SpinContext context; + std::array threads{}; + for (auto& thread : threads) thread = Start(SpinWorker, &context); + for (auto thread : threads) Require(_Thrd_join_nid_postfix(thread, nullptr) == ThrdSuccess); + Require(!context.overlapped); + Require(context.counter == SpinThreads * SpinIterations); + _Lock_shared_ptr_spin_lock_nid_postfix(); + _Unlock_shared_ptr_spin_lock_nid_postfix(); +} + +static void TestWcstombs() { + const std::uint16_t text[] = {u'a', u'b', u'c', 0}; + const std::uint16_t wide[] = {u'a', 0x100, 0}; + std::size_t result = 0; + char buffer[8]; + + std::memset(buffer, 'x', sizeof(buffer)); + 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(sceAmprWcstombsS(&result, buffer, sizeof(buffer), text, 2) == 0); + Require(result == 2 && std::strcmp(buffer, "ab") == 0); + + result = 0; + Require(sceAmprWcstombsS(&result, nullptr, 0, text, 0) == 0); + Require(result == 3); + + std::memset(buffer, 'x', sizeof(buffer)); + Require(sceAmprWcstombsS(&result, buffer, 3, text, 3) == GuestErange); + Require(result == Failed && buffer[0] == '\0'); + + result = 0; + Require(sceAmprWcstombsS(&result, nullptr, 4, text, 4) == GuestEinval); + Require(result == Failed); + + std::memset(buffer, 'x', sizeof(buffer)); + Require(sceAmprWcstombsS(&result, buffer, sizeof(buffer), nullptr, sizeof(buffer)) == GuestEinval); + Require(result == Failed && buffer[0] == '\0'); + + std::memset(buffer, 'x', sizeof(buffer)); + Require(sceAmprWcstombsS(nullptr, buffer, sizeof(buffer), text, sizeof(buffer)) == GuestEinval); + Require(buffer[0] == '\0'); + + std::memset(buffer, 'x', sizeof(buffer)); + Require(sceAmprWcstombsS(&result, buffer, 0, text, 4) == GuestEinval); + Require(result == Failed && buffer[0] == 'x'); + + std::memset(buffer, 'x', sizeof(buffer)); + Require(sceAmprWcstombsS(&result, buffer, sizeof(buffer), wide, sizeof(buffer)) == GuestEilseq); + Require(result == Failed && buffer[0] == 'a' && buffer[1] == '\0'); + + Require(sceAmprWcstombsS(&result, nullptr, 0, wide, 0) == GuestEilseq); + Require(result == Failed); +} + +template +static void RequireThrows(TCall call) { + try { + call(); + } catch (const TException&) { + return; + } + std::abort(); +} + +struct SignalContext { + void* condition = nullptr; + void* mutex = nullptr; + int waiting = 0; + int tokens = 0; + int consumed = 0; + int wakeups = 0; +}; + +static void Lock(SignalContext& context) { + Require(_Mtx_lock_nid_postfix(&context.mutex) == ThrdSuccess); +} + +static void Unlock(SignalContext& context) { + Require(_Mtx_unlock_nid_postfix(&context.mutex) == ThrdSuccess); +} + +static void* APS5_VABI ConsumeToken(void* arg) { + auto& context = *static_cast(arg); + Lock(context); + ++context.waiting; + while (context.tokens == 0) { + Require(_Cnd_wait_nid_postfix(&context.condition, &context.mutex) == ThrdSuccess); + ++context.wakeups; + } + --context.tokens; + ++context.consumed; + Unlock(context); + return nullptr; +} + +template +static void WaitUntil(SignalContext& context, TPredicate predicate) { + for (;;) { + Lock(context); + const bool done = predicate(); + Unlock(context); + if (done) return; + std::this_thread::yield(); + } +} + +static void Signal(SignalContext& context) { + Lock(context); + ++context.tokens; + Require(_Cnd_signal_nid_postfix(&context.condition) == ThrdSuccess); + Unlock(context); +} + +static void TestConditionSignal() { + SignalContext context; + context.condition = reinterpret_cast(std::uintptr_t{1}); + Require(_Cnd_init_with_name_nid_postfix(&context.condition, "AmprSignal") == ThrdSuccess && context.condition != nullptr); + Require(_Mtx_init_nid_postfix(&context.mutex, 0) == ThrdSuccess); + Require(_Cnd_signal_nid_postfix(&context.condition) == ThrdSuccess); + + const auto single = Start(ConsumeToken, &context); + WaitUntil(context, [&] { return context.waiting == 1; }); + Signal(context); + Require(_Thrd_join_nid_postfix(single, nullptr) == ThrdSuccess); + Require(context.consumed == 1 && context.tokens == 0); + + context.waiting = 0; + context.consumed = 0; + context.wakeups = 0; + const std::array pair{Start(ConsumeToken, &context), Start(ConsumeToken, &context)}; + WaitUntil(context, [&] { return context.waiting == 2; }); + Signal(context); + WaitUntil(context, [&] { return context.consumed == 1; }); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Lock(context); + Require(context.consumed == 1 && context.wakeups == 1 && context.tokens == 0); + Unlock(context); + Signal(context); + for (auto thread : pair) Require(_Thrd_join_nid_postfix(thread, nullptr) == ThrdSuccess); + Require(context.consumed == 2 && context.tokens == 0); + Require(_Cnd_broadcast_nid_postfix(&context.condition) == ThrdSuccess); + + RequireThrows([] { _Cnd_signal_nid_postfix(nullptr); }); + void* empty = nullptr; + RequireThrows([&] { _Cnd_signal_nid_postfix(&empty); }); + RequireThrows([&] { _Cnd_signal_nid_postfix(&context.mutex); }); + RequireThrows([] { _Cnd_init_with_name_nid_postfix(nullptr, "AmprSignal"); }); + _Cnd_destroy_nid_postfix(&context.condition); + Require(context.condition == nullptr); + RequireThrows([&] { _Cnd_signal_nid_postfix(&context.condition); }); + + void* unnamed = nullptr; + Require(_Cnd_init_with_name_nid_postfix(&unnamed, nullptr) == ThrdSuccess && unnamed != nullptr); + Require(_Cnd_signal_nid_postfix(&unnamed) == ThrdSuccess); + _Cnd_destroy_nid_postfix(&unnamed); + void* plain = nullptr; + Require(_Cnd_init_nid_postfix(&plain) == ThrdSuccess); + Require(_Cnd_signal_nid_postfix(&plain) == ThrdSuccess); + _Cnd_destroy_nid_postfix(&plain); + _Mtx_destroy_nid_postfix(&context.mutex); +} + +static unsigned APS5_VABI PadGo(TestPad* pad) { + const auto local = pad->target; + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + local.order->store(local.beforeRelease); + _ZNSt4_Pad8_ReleaseEv_nid_postfix(pad); + while (!local.destroyed->load()) std::this_thread::yield(); + local.order->store(local.afterRelease); + return local.exitCode; +} + +static constexpr std::array PadVtable{PadGo}; + +template +static bool Throws(TCall call) { + try { + call(); + } catch (const TException&) { + return true; + } + return false; +} + +static void ConstructPad(TestPad& pad, std::atomic& order, std::atomic& destroyed, int index, unsigned exitCode) { + std::memset(&pad, 0xCD, sizeof(pad)); + pad.vtable = PadVtable.data(); + _ZNSt4_PadC2Ev_nid_postfix(&pad); + Require(pad.vtable == PadVtable.data()); + Require(pad.condition && pad.mutex && !pad.started); + pad.target = {&order, &destroyed, 2 * index + 1, 2 * index + 2, exitCode}; +} + +static void TestPadLaunch() { + std::atomic order{0}; + std::atomic destroyed{false}; + TestPad pad; + ConstructPad(pad, order, destroyed, 0, 0); + Pthread thread = nullptr; + _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(&pad, &thread); + Require(thread != nullptr); + Require(pad.started); + Require(order.load() == 1); + _ZNSt4_PadD2Ev_nid_postfix(&pad); + Require(!pad.condition && !pad.mutex); + Require(order.load() == 1); + destroyed = true; + int code = -1; + Require(_Thrd_join_nid_postfix(thread, &code) == ThrdSuccess); + Require(code == 0); + Require(order.load() == 2); +} + +static void TestPadLaunchMany() { + constexpr int Count = 4; + std::array, Count> orders{}; + std::atomic destroyed{false}; + std::array pads{}; + std::array threads{}; + for (int index = 0; index < Count; ++index) { + ConstructPad(pads[index], orders[index], destroyed, index, 100u + index); + _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(&pads[index], &threads[index]); + Require(orders[index].load() == 2 * index + 1); + _ZNSt4_PadD2Ev_nid_postfix(&pads[index]); + } + destroyed = true; + for (int index = 0; index < Count; ++index) { + int code = 0; + Require(_Thrd_join_nid_postfix(threads[index], &code) == ThrdSuccess); + Require(code == 100 + index); + Require(orders[index].load() == 2 * index + 2); + } +} + +static unsigned APS5_VABI PadGoImmediate(TestPad* pad) { + const auto local = pad->target; + local.order->store(local.beforeRelease); + _ZNSt4_Pad8_ReleaseEv_nid_postfix(pad); + while (!local.destroyed->load()) std::this_thread::yield(); + local.order->store(local.afterRelease); + return local.exitCode; +} + +static void TestPadConcurrentLaunch() { + constexpr int Count = 4; + constexpr std::array immediateVtable{PadGoImmediate}; + std::atomic ready{0}; + std::array parents; + for (int index = 0; index < Count; ++index) { + parents[index] = std::thread([&, index] { + std::atomic order{0}; + std::atomic destroyed{false}; + TestPad pad; + ConstructPad(pad, order, destroyed, index, 0x80000000u + index); + pad.vtable = immediateVtable.data(); + ready.fetch_add(1); + while (ready.load() != Count) std::this_thread::yield(); + Pthread thread = nullptr; + _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(&pad, &thread); + Require(thread != nullptr && pad.started && order.load() == 2 * index + 1); + _ZNSt4_PadD2Ev_nid_postfix(&pad); + std::memset(&pad, 0xcd, sizeof(pad)); + destroyed = true; + int code = 0; + Require(_Thrd_join_nid_postfix(thread, &code) == ThrdSuccess); + Require(static_cast(code) == 0x80000000u + index); + Require(order.load() == 2 * index + 2); + }); + } + for (auto& parent : parents) parent.join(); +} + +struct ForeignDestroy { + TestPad* pad; + bool rejected; +}; + +static void* APS5_VABI DestroyPad(void* arg) { + auto& context = *static_cast(arg); + context.rejected = Throws([&] { _ZNSt4_PadD2Ev_nid_postfix(context.pad); }); + return nullptr; +} + +#ifndef _WIN32 +static void APS5_VABI ExitWithSystemErrorCode() { + const auto* type = __cxa_current_exception_type_nid_postfix(); + const auto* error = static_cast(__cxa_current_primary_exception_nid_postfix()); + _exit(type && error && std::strcmp(type->name, "St12system_error") == 0 ? error->code : 1); +} + +template +static int TerminateExitCode(GuestTerminate handler, TCall call) { + const pid_t child = fork(); + Require(child >= 0); + if (child == 0) { + _ZSt13set_terminatePFvvE_nid_postfix(handler); + call(); + _exit(2); + } + int status = 0; + 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() { + Require(Throws([] { _ZNSt4_PadC2Ev_nid_postfix(nullptr); })); + Require(Throws([] { _ZNSt4_PadD2Ev_nid_postfix(nullptr); })); + Require(Throws([] { _ZNSt4_Pad8_ReleaseEv_nid_postfix(nullptr); })); + Pthread thread = nullptr; + Require(Throws([&] { _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(nullptr, &thread); })); + + std::atomic order{0}; + std::atomic destroyed{true}; + TestPad pad; + ConstructPad(pad, order, destroyed, 0, 0); + Require(Throws([&] { _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(&pad, nullptr); })); + Require(!pad.started && order.load() == 0); +#ifndef _WIN32 + Require(TerminateExitCode(ExitWithSystemErrorCode, [&] { _ZNSt4_Pad8_ReleaseEv_nid_postfix(&pad); }) == GuestEbusy); +#endif + Require(!pad.started); + + ForeignDestroy context{&pad, false}; + Require(_Thrd_join_nid_postfix(Start(DestroyPad, &context), nullptr) == ThrdSuccess); + Require(context.rejected); + Require(pad.condition && pad.mutex); + _ZNSt4_PadD2Ev_nid_postfix(&pad); + Require(!pad.condition && !pad.mutex); + Require(Throws([&] { _ZNSt4_PadD2Ev_nid_postfix(&pad); })); +} + +int main() { + TestPadLaunch(); + TestPadLaunchMany(); + TestPadConcurrentLaunch(); + TestPadErrors(); + TestThreadJoin(); + TestThreadSleep(); + TestSpinLock(); + 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 6780ff8140..6d3745cd70 100644 --- a/core/libs/tests/GuestAppContent.cpp +++ b/core/libs/tests/GuestAppContent.cpp @@ -10,6 +10,9 @@ extern "C" { int APS5_VABI sceAppContentInitialize(const AppContentInitParam*, AppContentBootParam*); int APS5_VABI sceAppContentAddcontMount(uint32_t, const NpUnifiedEntitlementLabel*, AppContentMountPoint*); int APS5_VABI sceAppContentAddcontUnmount(const AppContentMountPoint*); +int APS5_VABI sceAppContentAddcontEnqueueDownload(uint32_t, const NpUnifiedEntitlementLabel*); +int APS5_VABI sceAppContentAddcontDelete(uint32_t, const NpUnifiedEntitlementLabel*); +int APS5_VABI sceAppContentGetAddcontDownloadProgress(uint32_t, const NpUnifiedEntitlementLabel*, void*); int APS5_VABI sceAppContentTemporaryDataMount2(uint32_t, AppContentMountPoint*); int APS5_VABI sceAppContentTemporaryDataUnmount(const AppContentMountPoint*); int APS5_VABI sceAppContentTemporaryDataFormat(const AppContentMountPoint*); @@ -19,6 +22,7 @@ int* APS5_VABI __error_nid_postfix(); int APS5_VABI sceAppContentGetAddcontInfo(uint32_t, const NpUnifiedEntitlementLabel*, void*); int APS5_VABI sceAppContentGetAddcontInfoList(uint32_t, void*, uint32_t, uint32_t*); int APS5_VABI sceAppContentDownloadDataGetAvailableSpaceKb(const AppContentMountPoint*, size_t*); +int APS5_VABI sceAppContentDownloadDataFormat(const AppContentMountPoint*); int APS5_VABI sceAppContentAppParamGetInt(uint32_t, int32_t*); } @@ -47,6 +51,20 @@ int main() { Require(sceAppContentDownloadDataGetAvailableSpaceKb(&download, &availableKb) == 0); Require(availableKb == 0); Require(sceAppContentDownloadDataGetAvailableSpaceKb(&download, nullptr) == ErrorParameter); + Require(sceAppContentDownloadDataGetAvailableSpaceKb(nullptr, &availableKb) == 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)); + std::filesystem::create_directory(downloadData / "cache"); + { std::ofstream file(downloadData / "cache" / "old.bin"); file << "downloaded"; } + { std::ofstream file(downloadData / "index.dat"); file << "downloaded"; } + Require(sceAppContentDownloadDataFormat(nullptr) == ErrorParameter); + Require(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)); const int32_t expectedParams[] = {3, 23566, -7, 0, 2147483647}; for (uint32_t id = 0; id < 5; ++id) { @@ -71,6 +89,21 @@ int main() { std::memcpy(mountPoint.data, "/addcont0", 10); Require(sceAppContentAddcontUnmount(&mountPoint) == ErrorNotFound); Require(sceAppContentAddcontUnmount(nullptr) == ErrorParameter); + Require(sceAppContentAddcontEnqueueDownload(0, &label) == ErrorNotFound); + Require(sceAppContentAddcontEnqueueDownload(7, &label) == ErrorNotFound); + Require(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"; } 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 new file mode 100644 index 0000000000..860ff13718 --- /dev/null +++ b/core/libs/tests/GuestAppInfo.cpp @@ -0,0 +1,120 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +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); +static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); + +static void Require(bool value) { if (!value) std::abort(); } + +struct AppInfo { + std::uint32_t appId; + std::uint64_t unknown1; + char titleId[14]; + 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); + std::filesystem::create_directories(root); + std::filesystem::current_path(root); + return root; +} + +int main(int argc, char** argv) { + if (argc == 3 && std::string_view(argv[1]) == "--without-param-json") { + std::filesystem::current_path(argv[2]); + AppInfo info{}; + try { sceKernelGetAppInfo(getpid_nid_postfix(), &info); } + catch (const std::runtime_error& error) { + 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)); + const int self = getpid_nid_postfix(); + Require(sceKernelGetAppInfo(self, &info) == 0); + Require(info.appId == 1); + Require(std::strcmp(info.titleId, "PPSA97358") == 0); + Require(info.unknown1 == 0x5a5a5a5a5a5a5a5aull); + Require(info.unknown2[0] == 0x5a); + Require(sceKernelGetAppInfo(self + 1, &info) == SCE_KERNEL_ERROR_EPERM); + Require(sceKernelGetAppInfo(self, nullptr) == 0); + std::filesystem::current_path(previous); + std::filesystem::remove_all(root); + 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); + 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]{}; + std::uint32_t second[4]{}; + 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/GuestArrayCallbacks.cpp b/core/libs/tests/GuestArrayCallbacks.cpp new file mode 100644 index 0000000000..92bfdfdef7 --- /dev/null +++ b/core/libs/tests/GuestArrayCallbacks.cpp @@ -0,0 +1,201 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +using ElementCallback = void (APS5_VABI *)(void*); +using CopyCallback = void (APS5_VABI *)(void*, void*); +using AllocateCallback = void* (APS5_VABI *)(std::size_t); +using FreeCallback = void (APS5_VABI *)(void*); +using SizedFreeCallback = void (APS5_VABI *)(void*, std::size_t); + +extern "C" { +void APS5_VABI __cxa_vec_ctor_nid_postfix(void*, std::size_t, std::size_t, ElementCallback, ElementCallback); +void APS5_VABI __cxa_vec_cctor_nid_postfix(void*, void*, std::size_t, std::size_t, CopyCallback, ElementCallback); +void APS5_VABI __cxa_vec_dtor_nid_postfix(void*, std::size_t, std::size_t, ElementCallback); +void APS5_VABI __cxa_vec_cleanup_nid_postfix(void*, std::size_t, std::size_t, ElementCallback); +void* APS5_VABI __cxa_vec_new_nid_postfix(std::size_t, std::size_t, std::size_t, ElementCallback, ElementCallback); +void* APS5_VABI __cxa_vec_new2_nid_postfix(std::size_t, std::size_t, std::size_t, ElementCallback, ElementCallback, AllocateCallback, FreeCallback); +void* APS5_VABI __cxa_vec_new3_nid_postfix(std::size_t, std::size_t, std::size_t, ElementCallback, ElementCallback, AllocateCallback, SizedFreeCallback); +void APS5_VABI __cxa_vec_delete_nid_postfix(void*, std::size_t, std::size_t, ElementCallback); +void APS5_VABI __cxa_vec_delete2_nid_postfix(void*, std::size_t, std::size_t, ElementCallback, FreeCallback); +void APS5_VABI __cxa_vec_delete3_nid_postfix(void*, std::size_t, std::size_t, ElementCallback, SizedFreeCallback); +} + +namespace { + +constexpr std::size_t Count = 4; +constexpr std::size_t ElementBytes = 16; +constexpr std::size_t Padding = 16; +constexpr std::size_t TotalBytes = Count * ElementBytes + Padding; +alignas(16) std::array storage{}; +alignas(16) std::array source{}; +std::byte* destination = nullptr; +std::size_t constructed = 0; +std::size_t copied = 0; +std::size_t destroyed = 0; +std::size_t allocated = 0; +std::size_t freed = 0; +bool failAllocation = false; +bool failConstruct = false; +bool failCopy = false; +bool failDestroy = false; + +void require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +void reset(std::byte* first) { + destination = first; + constructed = copied = destroyed = allocated = freed = 0; + failAllocation = false; + failConstruct = failCopy = failDestroy = false; +} + +void APS5_VABI construct(void* pointer) { + require(constructed < Count && pointer == destination + constructed * ElementBytes, "constructor received the wrong element"); + if (failConstruct && constructed == 2) throw 73; + std::memset(pointer, static_cast(++constructed), ElementBytes); +} + +void APS5_VABI copy(void* target, void* original) { + require(copied < Count && target == destination + copied * ElementBytes, "copy constructor received the wrong destination"); + require(original == source.data() + copied * ElementBytes, "copy constructor received the wrong source"); + if (failCopy && copied == 2) throw 73; + std::memcpy(target, original, ElementBytes); + ++copied; +} + +void APS5_VABI destroy(void* pointer) { + const auto count = copied != 0 ? copied : constructed; + require(destroyed < count && pointer == destination + (count - destroyed - 1) * ElementBytes, "destructor received the wrong element or order"); + require(*static_cast(pointer) == std::byte(count - destroyed), "destructor received the wrong element contents"); + ++destroyed; + if (failDestroy && destroyed == 1) throw 73; +} + +void* APS5_VABI allocate(std::size_t bytes) { + require(bytes == TotalBytes, "allocator received the wrong size"); + ++allocated; + return failAllocation ? nullptr : storage.data(); +} + +void APS5_VABI release(void* pointer) { + require(pointer == storage.data(), "deallocator received the wrong storage"); + require(destroyed == constructed, "deallocator ran before element destruction finished"); + ++freed; +} + +void APS5_VABI releaseSized(void* pointer, std::size_t bytes) { + require(bytes == TotalBytes, "sized deallocator received the wrong size"); + release(pointer); +} + +void checkCookie(const void* array) { + std::size_t count = 0; + std::memcpy(&count, static_cast(array) - sizeof(count), sizeof(count)); + require(count == Count, "array cookie has the wrong count"); +} + +void checkElements() { + reset(source.data()); + __cxa_vec_ctor_nid_postfix(source.data(), Count, ElementBytes, construct, destroy); + require(constructed == Count, "constructor count is wrong"); + __cxa_vec_dtor_nid_postfix(source.data(), Count, ElementBytes, destroy); + require(destroyed == Count, "destructor count is wrong"); + reset(storage.data()); + __cxa_vec_cctor_nid_postfix(storage.data(), source.data(), Count, ElementBytes, copy, destroy); + require(copied == Count && std::memcmp(storage.data(), source.data(), source.size()) == 0, "copy construction failed"); + __cxa_vec_cleanup_nid_postfix(storage.data(), Count, ElementBytes, destroy); + require(destroyed == Count, "cleanup count is wrong"); + __cxa_vec_ctor_nid_postfix(nullptr, Count, ElementBytes, nullptr, nullptr); + __cxa_vec_cctor_nid_postfix(nullptr, nullptr, Count, ElementBytes, nullptr, nullptr); + __cxa_vec_dtor_nid_postfix(nullptr, Count, ElementBytes, nullptr); + __cxa_vec_cleanup_nid_postfix(nullptr, Count, ElementBytes, nullptr); +} + +void checkAllocation(bool sized) { + reset(storage.data() + Padding); + void* array = sized + ? __cxa_vec_new3_nid_postfix(Count, ElementBytes, Padding, construct, destroy, allocate, releaseSized) + : __cxa_vec_new2_nid_postfix(Count, ElementBytes, Padding, construct, destroy, allocate, release); + require(array == destination && constructed == Count && allocated == 1, "array allocation failed"); + checkCookie(array); + if (sized) __cxa_vec_delete3_nid_postfix(array, ElementBytes, Padding, destroy, releaseSized); + else __cxa_vec_delete2_nid_postfix(array, ElementBytes, Padding, destroy, release); + require(destroyed == Count && freed == 1, "array deletion failed"); + reset(storage.data() + Padding); + failAllocation = true; + array = sized + ? __cxa_vec_new3_nid_postfix(Count, ElementBytes, Padding, construct, destroy, allocate, releaseSized) + : __cxa_vec_new2_nid_postfix(Count, ElementBytes, Padding, construct, destroy, allocate, release); + require(array == nullptr && allocated == 1 && constructed == 0 && freed == 0, "failed allocation invoked element callbacks"); + if (sized) __cxa_vec_delete3_nid_postfix(nullptr, ElementBytes, Padding, destroy, releaseSized); + else __cxa_vec_delete2_nid_postfix(nullptr, ElementBytes, Padding, destroy, release); + require(freed == 0, "null array invoked deallocation"); +} + +void checkDefaultAllocation() { + void* array = __cxa_vec_new_nid_postfix(Count, ElementBytes, Padding, nullptr, nullptr); + require(array != nullptr, "default array allocation failed"); + checkCookie(array); + reset(static_cast(array)); + __cxa_vec_ctor_nid_postfix(array, Count, ElementBytes, construct, destroy); + __cxa_vec_delete_nid_postfix(array, ElementBytes, Padding, destroy); + require(constructed == Count && destroyed == Count, "default array deletion failed"); + __cxa_vec_delete_nid_postfix(nullptr, ElementBytes, Padding, destroy); +} + +template +void expectException(TAction action) { + bool caught = false; + try { action(); } + catch (int value) { require(value == 73, "callback exception changed"); caught = true; } + require(caught, "callback exception was swallowed"); +} + +void checkRollback(bool sized) { + reset(storage.data() + Padding); + failConstruct = true; + expectException([&] { + if (sized) __cxa_vec_new3_nid_postfix(Count, ElementBytes, Padding, construct, destroy, allocate, releaseSized); + else __cxa_vec_new2_nid_postfix(Count, ElementBytes, Padding, construct, destroy, allocate, release); + }); + require(constructed == 2 && destroyed == 2 && allocated == 1 && freed == 1, "constructor rollback failed"); + reset(storage.data() + Padding); + void* array = sized + ? __cxa_vec_new3_nid_postfix(Count, ElementBytes, Padding, construct, destroy, allocate, releaseSized) + : __cxa_vec_new2_nid_postfix(Count, ElementBytes, Padding, construct, destroy, allocate, release); + failDestroy = true; + expectException([&] { + if (sized) __cxa_vec_delete3_nid_postfix(array, ElementBytes, Padding, destroy, releaseSized); + else __cxa_vec_delete2_nid_postfix(array, ElementBytes, Padding, destroy, release); + }); + require(constructed == Count && destroyed == Count && allocated == 1 && freed == 1, "destructor rollback failed"); +} + +void checkCopyRollback() { + reset(storage.data()); + failCopy = true; + expectException([] { + __cxa_vec_cctor_nid_postfix(storage.data(), source.data(), Count, ElementBytes, copy, destroy); + }); + require(copied == 2 && destroyed == 2, "copy constructor rollback failed"); +} + +} + +int main() { + checkElements(); + checkAllocation(false); + checkAllocation(true); + checkDefaultAllocation(); + checkRollback(false); + checkRollback(true); + checkCopyRollback(); + std::puts("guest array callback arguments and lifecycle: PASS"); +} 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 new file mode 100644 index 0000000000..0c4d77ddfa --- /dev/null +++ b/core/libs/tests/GuestAtoi.cpp @@ -0,0 +1,55 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI atoi_nid_postfix(const char*); +int* APS5_VABI __error_nid_postfix(); +} + +int main() { + struct Case { + const char* text; + int value; + int error; + }; + constexpr int SavedError = 1234; + constexpr Case cases[] = { + {"0", 0, SavedError}, + {"+42", 42, SavedError}, + {" \t\n\r\f\v-0012tail", -12, SavedError}, + {"010", 10, SavedError}, + {"0x10", 0, SavedError}, + {"0b10", 0, SavedError}, + {"", 0, SavedError}, + {"words", 0, SavedError}, + {" +", 0, SavedError}, + {"2147483647", INT32_MAX, SavedError}, + {"-2147483648", INT32_MIN, SavedError}, + {"2147483648", INT32_MIN, SavedError}, + {"-2147483649", INT32_MAX, SavedError}, + {"4294967295", -1, SavedError}, + {"-4294967295", 1, SavedError}, + {"4294967296", 0, SavedError}, + {"-4294967296", 0, SavedError}, + {" \t+4294967297tail", 1, SavedError}, + {"9223372036854775807", -1, SavedError}, + {"-9223372036854775808", 0, SavedError}, + {"9223372036854775808", -1, 34}, + {"-9223372036854775809", 0, 34}, + {"999999999999999999999999999999999999", -1, 34}, + {"-999999999999999999999999999999999999", 0, 34}, + }; + bool correct = true; + for (const auto& test : cases) { + *__error_nid_postfix() = SavedError; + const int value = atoi_nid_postfix(test.text); + const int error = *__error_nid_postfix(); + if (value != test.value || error != test.error) { + std::fprintf(stderr, "atoi(%s): expected %d, errno %d; received %d, errno %d\n", + test.text, test.value, test.error, value, error); + correct = false; + } + } + return correct ? 0 : 1; +} 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..5b5467437d 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,19 @@ void CheckOpenClose() { void CheckAttributes() { float value = 0.5f; int flag = 1; + constexpr std::uint32_t supported[]{0x10001, 0x10002, 0x10003}; + std::uint32_t num_attributes = 99; + Require(sceAudio3dPortGetAttributesSupported(0, nullptr, &num_attributes) == 0 && num_attributes == 3, "null output reports the supported attribute count"); + Require(sceAudio3dPortGetAttributesSupported(1, nullptr, &num_attributes) == INVALID_PORT, "supported attributes reject an invalid port"); + Require(sceAudio3dPortGetAttributesSupported(0, nullptr, nullptr) == INVALID_PARAMETER, "supported attributes require a count pointer"); + num_attributes = 2; + std::uint32_t partial[3]{0, 0, 99}; + Require(sceAudio3dPortGetAttributesSupported(0, partial, &num_attributes) == 0 && num_attributes == 2, "supported attributes respect the caller capacity"); + Require(partial[0] == supported[0] && partial[1] == supported[1] && partial[2] == 99, "supported attributes copy only the requested entries"); + num_attributes = 3; + std::uint32_t all[3]{}; + Require(sceAudio3dPortGetAttributesSupported(0, all, &num_attributes) == 0 && num_attributes == 3, "all supported attributes are returned"); + Require(std::equal(std::begin(all), std::end(all), std::begin(supported)), "the reported attributes match the port setter"); Require(sceAudio3dPortSetAttribute(0, 0x10001, nullptr, 4) == INVALID_PARAMETER, "null attribute"); Require(sceAudio3dPortSetAttribute(1, 0x10001, &value, 4) == INVALID_PORT, "attribute on port 1"); Require(sceAudio3dPortSetAttribute(0, 0x10001, &value, 4) == 0, "late reverb level"); @@ -176,11 +202,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 +217,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 +280,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 +334,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 +414,12 @@ int main() { CheckOpenClose(); CheckObjects(); CheckQueue(); + CheckMixAndPush(); + CheckPushKeepsRealTime(); CheckFlush(); + CheckAudioOutPorts(); CheckObjectAttributes(); + CheckBed(); + CheckBedQueue(); CheckTerminate(); } diff --git a/core/libs/tests/GuestAudioIn.cpp b/core/libs/tests/GuestAudioIn.cpp index 8786835e8a..124bc9a880 100644 --- a/core/libs/tests/GuestAudioIn.cpp +++ b/core/libs/tests/GuestAudioIn.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -6,11 +7,18 @@ #include #include #include +#include #include #include extern "C" { +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); +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); @@ -18,6 +26,26 @@ int APS5_VABI sceAudioInClose(int); 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; +} + +template +static bool ThrowsRuntimeError(TCall call) { + try { + call(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + static void SetEnvironment(const char* name, const std::string& value) { #ifdef _WIN32 _putenv_s(name, value.c_str()); @@ -31,6 +59,7 @@ namespace { constexpr int user = 0x10000000; 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); @@ -41,6 +70,50 @@ void TestValidation() { Require(sceAudioInOpen(user, 0, 0, 256, 44100, 2) == invalidFreq); Require(sceAudioInOpen(user, 0, 0, 256, 48000, 3) == invalidParam); Require(sceAudioInOpen(user, 0, 0, 256, 48000, 0) == invalidParam); + Require(sceAudioInHqOpen(user, 2, 0, 128, 48000, 2) == invalidType); + Require(sceAudioInHqOpen(user, 0, 1, 128, 48000, 2) == invalidParam); + Require(sceAudioInHqOpen(user, 0, 0, 512, 48000, 2) == invalidSize); + Require(sceAudioInHqOpen(user, 0, 0, 128, 44100, 2) == invalidFreq); + Require(sceAudioInHqOpen(user, 0, 0, 128, 48000, 3) == invalidParam); + 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(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(ThrowsRuntimeError([badHandle] { sceAudioInGetGain(badHandle, nullptr); })); + } + int port = sceAudioInAsyncOpen(user, 0, 0, 384, 48000, 2); + float gain = 0.0f; + Require(port > 0 && sceAudioInGetGain(port, &gain) == 0 && gain == 1.0f && sceAudioInGetRerouteCount(port) == 0); + Require(ThrowsRuntimeError([port] { sceAudioInGetGain(port, nullptr); })); + 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); + port = sceAudioInHqOpen(user, 0, 0, 128, 48000, 0x10); + gain = 2.0f; + Require(port > 0 && sceAudioInGetGain(port, &gain) == 0 && gain == 1.0f && sceAudioInGetRerouteCount(port) == 0 && sceAudioInClose(port) == 0); + port = sceAudioInAsyncOpen(user, 0, 0, 384, 48000, 0x10); + gain = 0.5f; + Require(port > 0 && sceAudioInGetGain(port, &gain) == 0 && gain == 1.0f && sceAudioInGetRerouteCount(port) == 0 && sceAudioInClose(port) == 0); + Require(ReportsNotImplemented("sceAudioInIsPadEmbeddedMic", [] { sceAudioInIsPadEmbeddedMic(1); })); +} + +void InputSound(int handle, std::vector& block) { + for (int attempt = 0; attempt < 200; ++attempt) { + Require(sceAudioInInput(handle, block.data()) == 256); + if (std::any_of(block.begin(), block.end() - 1, [](std::int16_t sample) { return sample != 0; })) return; + } + Require(false); } void TestCapture() { @@ -57,7 +130,7 @@ void TestCapture() { 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); } @@ -65,6 +138,11 @@ 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); + Require(async == handle && sceAudioInGetSilentState(async) == 0); + InputSound(async, block); + Require(std::equal(block.begin(), block.end() - 1, recorded.begin()) && block.back() == 0x5555); + Require(sceAudioInClose(async) == 0); std::filesystem::remove(path); } @@ -86,8 +164,30 @@ void TestNoDevice() { for (std::size_t i = 0; i < 256 * 8; ++i) Require(buffer[i] == 0); Require(buffer[256 * 8] == 0xCC); Require(sceAudioInInput(handle, nullptr) == 0); - for (int port = 1; port < 8; ++port) Require(sceAudioInOpen(user, 1, 0, 128, 48000, 1) > 0); + const int hq = sceAudioInHqOpen(user, 0, 0, 128, 48000, 2); + const int async = sceAudioInAsyncOpen(user, 1, 0, 256, 16000, 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); + Require(sceAudioInInput(hq, buffer.data()) == 128); + 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, 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(sceAudioInAsyncOpen(user, 1, 0, 128, 48000, 1) == portFull); + Require(sceAudioInClose(hq) == 0); + Require(sceAudioInClose(hq) == invalidHandle); + Require(sceAudioInGetSilentState(hq) == invalidHandle); + 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(sceAudioInClose(handle) == 0); Require(sceAudioInClose(handle) == invalidHandle); Require(sceAudioInInput(handle, buffer.data()) == invalidHandle); @@ -98,6 +198,7 @@ void TestNoDevice() { int main(int argc, char** argv) { Require(argc == 2); + Require(sceAudioInInit() == 0); if (std::strcmp(argv[1], "capture") == 0) TestCapture(); else TestNoDevice(); } diff --git a/core/libs/tests/GuestAudioPropagation.cpp b/core/libs/tests/GuestAudioPropagation.cpp index 8930cf679d..c4d112d342 100644 --- a/core/libs/tests/GuestAudioPropagation.cpp +++ b/core/libs/tests/GuestAudioPropagation.cpp @@ -6,6 +6,12 @@ #include #include +extern "C" { +int APS5_VABI sceAudioPropagationPathGetNumPoints(); +int APS5_VABI sceAudioPropagationSystemLock(); +int APS5_VABI sceAudioPropagationMaterialInit(); +} + namespace { void Require(bool condition, const char* message) { @@ -143,20 +149,16 @@ int main() { 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; + float expected[2][64]; + const auto fill = [&] { + for (std::size_t channel = 0; channel < 2; ++channel) { + for (std::size_t sample = 0; sample < 64; ++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,13 +167,26 @@ 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"); reset(); infos[0].outputSize = sizeof(float) * 16; 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) * 32; + infos[1].output = outputs[0] + 16; + infos[1].outputSize = sizeof(float) * 32; + 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"); @@ -227,6 +242,10 @@ int main() { 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"); + RequireThrows([&] { sceAudioPropagationMaterialInit(); }, "material init throw"); + std::puts("AudioPropagation tests passed"); return 0; } diff --git a/core/libs/tests/GuestAudiodec.cpp b/core/libs/tests/GuestAudiodec.cpp index 2e6af3008a..5e9ee471f1 100644 --- a/core/libs/tests/GuestAudiodec.cpp +++ b/core/libs/tests/GuestAudiodec.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include extern "C" { @@ -143,6 +144,60 @@ const std::uint8_t MP3_MONO[] = { 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, }; +const std::uint8_t HEAAC_V2_MONO[] = { + 0xFF, 0xF1, 0x58, 0x40, 0x10, 0x02, 0x80, 0x01, 0x40, 0x22, 0x80, 0xA3, 0x6E, 0xEF, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4A, 0x80, + 0x00, 0x06, 0xF5, 0x61, 0x0A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5E, 0xFF, 0xF1, 0x58, 0x40, 0x10, 0x02, 0x80, 0x01, 0x40, 0x42, 0x80, 0xA3, 0x78, 0x46, 0x81, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x38, 0x76, 0xE4, 0x89, 0xB5, 0x65, 0xE5, 0xDF, 0xFE, 0xB7, 0x32, 0x7C, 0x7F, 0xF0, 0x00, 0x06, 0xF4, 0xC1, 0x0A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5E, 0xFF, 0xF1, 0x58, 0x40, 0x18, 0xA2, 0x38, 0x01, + 0x0A, 0x97, 0x98, 0xD8, 0x73, 0x75, 0x18, 0xBB, 0x6D, 0xCD, 0xA6, 0xE8, 0x7F, 0x09, 0xD5, 0xA0, 0x7E, 0x99, 0xC7, 0x42, 0xF2, 0x57, 0xBB, 0xC8, + 0xAB, 0x51, 0x57, 0x0A, 0xBF, 0x56, 0x8A, 0x40, 0x48, 0xFB, 0xCC, 0xF1, 0xFF, 0x4E, 0x4C, 0x21, 0xEE, 0x7F, 0x10, 0xFD, 0xA6, 0xB4, 0xE2, 0xCC, + 0x52, 0xA9, 0x38, 0x00, 0x00, 0xC4, 0x1A, 0x9A, 0xD2, 0x05, 0xE8, 0x94, 0xDB, 0xA5, 0x47, 0xFE, 0x92, 0xFC, 0x12, 0x41, 0x11, 0x08, 0x01, 0x22, + 0x65, 0x54, 0x28, 0x24, 0x16, 0x72, 0xE9, 0xAA, 0xC7, 0x45, 0x33, 0xEC, 0x74, 0x13, 0xAC, 0x84, 0xD5, 0xD4, 0x95, 0xCB, 0x41, 0x9B, 0x55, 0x2D, + 0xD5, 0xAC, 0x5C, 0xEF, 0xD4, 0x75, 0xF8, 0xD5, 0xCE, 0xC9, 0x64, 0xD9, 0xF5, 0x80, 0xD3, 0xD9, 0xAA, 0x5F, 0x1E, 0x45, 0x4F, 0xEA, 0x5F, 0xF8, + 0xBF, 0x48, 0x1F, 0xAD, 0xDF, 0x0C, 0xE5, 0xF1, 0x7F, 0x65, 0xC0, 0x12, 0xFA, 0xBF, 0xE6, 0x40, 0x0D, 0xAA, 0xFD, 0x40, 0x02, 0x58, 0x64, 0xA4, + 0x03, 0x2A, 0xC1, 0xAA, 0x79, 0x01, 0x6D, 0x37, 0x60, 0x78, 0x7B, 0x2B, 0x1B, 0x62, 0x6C, 0x87, 0xA7, 0xA2, 0xDB, 0x65, 0x00, 0xDE, 0x0F, 0xA0, + 0x50, 0x00, 0x00, 0x00, 0x00, 0x01, 0xDB, 0x92, 0x26, 0xD5, 0x97, 0x97, 0x7F, 0xFA, 0xDC, 0xC9, 0xF1, 0xFF, 0xC0, 0x01, 0xC0, 0xFF, 0xF1, 0x58, + 0x40, 0x15, 0x02, 0x10, 0x01, 0x3C, 0xF3, 0x08, 0xB6, 0xA1, 0x99, 0x0C, 0x42, 0x0A, 0xA1, 0x40, 0xF6, 0x05, 0x0A, 0x6C, 0x0B, 0xC0, 0x1F, 0x1F, + 0x19, 0x9D, 0x15, 0x2A, 0x56, 0x97, 0x6C, 0x63, 0x88, 0xF4, 0x89, 0x13, 0x53, 0xA1, 0x9D, 0x8E, 0x9A, 0x93, 0x0C, 0x6A, 0x18, 0x0F, 0x2C, 0xCB, + 0x53, 0xB3, 0x93, 0xA9, 0xA9, 0x32, 0x63, 0xC3, 0x0C, 0x07, 0xF0, 0xF1, 0x3C, 0x98, 0xB9, 0x6B, 0x75, 0xB0, 0x95, 0xAD, 0x62, 0xDC, 0xA4, 0xB4, + 0x75, 0xE1, 0xAD, 0xE9, 0x64, 0x31, 0x51, 0x24, 0xAE, 0x6E, 0x20, 0xC0, 0x9D, 0x70, 0x25, 0x47, 0x1D, 0x67, 0x4D, 0x5C, 0xE1, 0xDA, 0x4E, 0x8A, + 0x9E, 0xF9, 0xFA, 0x62, 0x16, 0xDA, 0x99, 0x9F, 0xD0, 0xDC, 0x7D, 0xBD, 0x5F, 0x31, 0x8B, 0xC2, 0xE8, 0xD0, 0xB8, 0xE5, 0xC6, 0xC0, 0x37, 0x8F, + 0x68, 0xBC, 0x92, 0x4C, 0xD0, 0x00, 0x01, 0xFC, 0xFD, 0x43, 0xF2, 0xF5, 0xFF, 0x9F, 0xBF, 0xAD, 0x6D, 0x56, 0x0D, 0x3F, 0xF4, 0xA5, 0xD9, 0x17, + 0xFF, 0x20, 0x02, 0x7C, 0x02, 0xCC, 0x8B, 0x26, 0x49, 0xFF, 0xFB, 0x9E, 0xA2, 0x00, 0x0F, 0xE4, 0xDF, 0xF0, 0x00, 0x00, 0x70, +}; + +const std::uint8_t HEAAC_STEREO[] = { + 0xFF, 0xF1, 0x58, 0x80, 0x10, 0x02, 0xC0, 0x21, 0x21, 0x45, 0x00, 0x14, 0x50, 0x01, 0x46, 0xAD, 0xD5, 0x81, 0x75, 0x00, 0x00, 0x00, 0x1C, 0x60, + 0x18, 0x0D, 0xEA, 0xC2, 0x14, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, + 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, + 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, + 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, + 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xBC, 0xFF, 0xF1, 0x58, 0x80, 0x1B, 0x62, 0x90, 0x21, 0x4A, 0xCC, 0x80, 0x1F, 0xEA, 0xFE, 0xBF, 0xC8, + 0x87, 0x77, 0x07, 0x5B, 0x6B, 0x0A, 0xDA, 0x70, 0x6A, 0xC6, 0x8F, 0xE1, 0xE6, 0xC4, 0x36, 0x01, 0x48, 0xB1, 0x94, 0xF0, 0x67, 0x71, 0x13, 0x21, + 0x28, 0xB0, 0x74, 0xC1, 0x02, 0x0B, 0xED, 0x1B, 0x75, 0xF2, 0x1D, 0x89, 0x8A, 0xFA, 0xCB, 0x17, 0x0D, 0x51, 0xAE, 0x81, 0x2D, 0xB3, 0x27, 0x07, + 0x00, 0x69, 0x11, 0x1E, 0x51, 0x2E, 0x56, 0x4C, 0xD1, 0x9C, 0x39, 0x65, 0xED, 0x43, 0x53, 0x4E, 0x4E, 0x22, 0x8D, 0x5D, 0x48, 0x56, 0x50, 0x66, + 0x75, 0x4B, 0x3D, 0x6B, 0x10, 0xE5, 0x38, 0x97, 0x8D, 0x5C, 0xEC, 0xCC, 0x6F, 0xDA, 0x1D, 0xD5, 0x99, 0x0C, 0xBD, 0x50, 0xDB, 0x92, 0xDE, 0x06, + 0xD1, 0x0A, 0xE2, 0xD5, 0x32, 0x9C, 0xEA, 0x82, 0x1A, 0x30, 0x72, 0x92, 0xC1, 0x30, 0x88, 0xAC, 0x38, 0x15, 0xD9, 0xE7, 0x22, 0x5A, 0x97, 0x94, + 0x0D, 0xE4, 0xFF, 0xFF, 0xFC, 0xC8, 0xD0, 0xEE, 0xDB, 0x60, 0x26, 0x6D, 0x11, 0x0D, 0x69, 0x10, 0x61, 0x63, 0xA7, 0xE7, 0xB7, 0x58, 0x11, 0x63, + 0x29, 0xE0, 0xFB, 0xBD, 0x8E, 0x3C, 0x98, 0xBC, 0x01, 0x7E, 0x8F, 0x7B, 0xF7, 0xCB, 0x3A, 0xC2, 0xC0, 0xC2, 0xC2, 0xCB, 0x01, 0x1F, 0xA3, 0x94, + 0xE8, 0x28, 0x44, 0x40, 0x52, 0x10, 0x42, 0x48, 0x0A, 0x94, 0x53, 0x9D, 0x57, 0xB0, 0x4B, 0x3E, 0x50, 0x80, 0xDF, 0xD2, 0x70, 0x30, 0x95, 0x33, + 0x92, 0x2C, 0xAD, 0x9D, 0x59, 0x74, 0x83, 0xC0, 0x00, 0x00, 0x38, 0xFF, 0xF1, 0x58, 0x80, 0x16, 0xC2, 0x74, 0x21, 0x79, 0x08, 0x00, 0x87, 0xFF, + 0xFF, 0xCF, 0x11, 0x6C, 0xCC, 0x21, 0x92, 0x14, 0x02, 0x80, 0xE8, 0x05, 0x50, 0x10, 0x50, 0x3F, 0xC7, 0xC6, 0x67, 0x50, 0xB0, 0xBB, 0x63, 0x1A, + 0x77, 0x48, 0xF3, 0x0E, 0x0A, 0x02, 0x2F, 0xC6, 0xDA, 0xD2, 0xFA, 0xDA, 0x2D, 0x32, 0x15, 0x6E, 0xEE, 0xF0, 0x11, 0x7F, 0x0F, 0x13, 0xC9, 0x48, + 0xEB, 0x84, 0x2D, 0x63, 0xAD, 0xAD, 0x61, 0xCA, 0x43, 0x5E, 0x1A, 0xDC, 0x31, 0x76, 0x4B, 0x9C, 0xBB, 0xD1, 0xF7, 0xD4, 0x84, 0xF0, 0x35, 0x83, + 0x78, 0xEE, 0x95, 0x05, 0x2E, 0x61, 0x76, 0xDB, 0xE1, 0x68, 0xC9, 0x23, 0xA4, 0x1E, 0xAA, 0x71, 0xCA, 0x9A, 0xA5, 0xEE, 0x88, 0x0D, 0x0F, 0xE7, + 0xFE, 0x55, 0x6D, 0x0F, 0x61, 0xA3, 0xAE, 0xEE, 0xF3, 0x08, 0xCB, 0x13, 0x08, 0x28, 0x0A, 0x1E, 0x28, 0x40, 0x70, 0xA0, 0xA7, 0x84, 0x14, 0x15, + 0xF5, 0xD0, 0x50, 0x53, 0x51, 0x5F, 0x32, 0x89, 0xA8, 0xAC, 0x1F, 0x73, 0x62, 0xD1, 0x67, 0x53, 0x57, 0x86, 0x2C, 0x80, 0x44, 0xFB, 0xC2, 0xF4, + 0xAF, 0x24, 0x93, 0x1D, 0x40, 0x00, 0xFF, 0x89, 0x36, 0xFF, 0xC6, 0x45, 0x23, 0x7F, 0xF0, 0x5B, 0xCC, 0x00, 0x00, 0x00, 0x03, 0x20, 0x0C, 0x01, + 0x38, +}; constexpr std::int32_t errorApiFail = static_cast(0x807F0000); constexpr std::int32_t errorInvalidType = static_cast(0x807F0001); constexpr std::int32_t errorArg = static_cast(0x807F0002); @@ -225,6 +280,46 @@ void TestAacRaw51(std::uint32_t maxChannels) { Require(sceAudiodecDeleteDecoder(handle) == 0); } +double UpperHalfEnergyShare(const std::int16_t* samples, std::size_t count, std::size_t stride) { + double upper = 0; + double total = 0; + for (std::size_t bin = 1; bin < count / 2; ++bin) { + double real = 0; + double imaginary = 0; + for (std::size_t i = 0; i < count; ++i) { + const double phase = 2 * std::numbers::pi * static_cast(bin * i % count) / static_cast(count); + real += samples[i * stride] * std::cos(phase); + imaginary += samples[i * stride] * std::sin(phase); + } + total += real * real + imaginary * imaginary; + if (bin >= count / 4) upper += real * real + imaginary * imaginary; + } + return upper / total; +} + +void TestHeAac(const std::uint8_t* stream, std::size_t streamSize, std::uint32_t channels, bool adts, bool highEfficiency) { + AudiodecParamM4aac param{sizeof(param), 1, adts ? 1u : 2u, 6, channels, highEfficiency ? 1u : 0u}; + AudiodecM4aacInfo info{sizeof(info), 0, 0, 0, 0}; + AudiodecCtrl ctrl{¶m, &info, nullptr, nullptr}; + const std::int32_t handle = sceAudiodecCreateDecoder(&ctrl, 3); + Require(handle > 0); + Streams streams; + const std::uint32_t samples = highEfficiency ? 2048 : 1024; + const std::uint32_t room = samples * channels * 2; + for (std::size_t offset = 0; offset < streamSize; offset += AdtsLength(stream + offset)) { + const std::size_t length = AdtsLength(stream + offset); + Require(Decode(handle, ctrl, streams, adts ? stream + offset : stream + offset + 7, adts ? length : length - 7, room) == 0); + Require(streams.au.ui_au_size == (adts ? length : length - 7)); + Require(streams.pcm.ui_pcm_size == room && info.i_result == 0); + Require(info.ui_sampling_freq == (highEfficiency ? 48000u : 24000u) && info.ui_number_of_channels == channels); + } + if (highEfficiency) { + Require(info.ui_heaac == 1); + Require(UpperHalfEnergyShare(reinterpret_cast(streams.output.data()), samples, channels) > 0.02); + } + Require(sceAudiodecDeleteDecoder(handle) == 0); +} + void TestMp3() { AudiodecParamMp3 param{sizeof(param), 2}; AudiodecMp3Info info{}; @@ -315,6 +410,12 @@ int main() { TestAac(false, 8); TestAacRaw51(6); TestAacRaw51(8); + for (const bool adts : {true, false}) { + for (const bool highEfficiency : {true, false}) { + TestHeAac(HEAAC_V2_MONO, sizeof(HEAAC_V2_MONO), 1, adts, highEfficiency); + TestHeAac(HEAAC_STEREO, sizeof(HEAAC_STEREO), 2, adts, highEfficiency); + } + } TestMp3(); TestAt9(); for (std::uint32_t type = 1; type <= 3; ++type) Require(sceAudiodecTermLibrary(type) == 0); diff --git a/core/libs/tests/GuestAvPlayer.cpp b/core/libs/tests/GuestAvPlayer.cpp index b89d0776f6..9eb5252045 100644 --- a/core/libs/tests/GuestAvPlayer.cpp +++ b/core/libs/tests/GuestAvPlayer.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include "SceTypes.hpp" @@ -72,6 +73,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 +454,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); } @@ -535,15 +641,24 @@ int TextureCount() { struct Events { std::mutex mutex; std::vector> received; + std::chrono::steady_clock::time_point stopAt{}; + bool hasStop = false; bool Seen(std::int32_t id, std::int32_t warning = 0) { std::lock_guard lock(mutex); 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(); + stopAt = {}; + hasStop = false; } }; @@ -551,6 +666,10 @@ void APS5_VABI OnEvent(void* object, std::int32_t id, std::int32_t source, void* auto* events = static_cast(object); if (source != 0 || (id == EventWarning) != (data != nullptr)) std::abort(); std::lock_guard lock(events->mutex); + if (id == EventStop && !events->hasStop) { + events->stopAt = std::chrono::steady_clock::now(); + events->hasStop = true; + } events->received.emplace_back(id, id == EventWarning ? *static_cast(data) : 0); } @@ -715,6 +834,8 @@ void TestPlayback() { Check(sceAvPlayerStreamCount(player) == OperationFailed, "stream count without a source"); Check(!sceAvPlayerIsActive(player), "active without a source"); Check(sceAvPlayerAddSource(player, "/app0/missing.mp4") == OperationFailed, "missing file accepted"); + Check(sceAvPlayerAddSource(player, "/app0/missing/../avplayer.mp4") == OperationFailed, "missing parent accepted"); + Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4/../avplayer.mp4") == OperationFailed, "file parent accepted"); Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == 0, "add source failed"); Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == OperationFailed, "second source accepted"); Check(WaitFor([&] { return events.Seen(EventReady); }), "ready event missing"); @@ -744,7 +865,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"); @@ -819,7 +940,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"); @@ -832,6 +953,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); @@ -1018,8 +1189,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)); @@ -1028,18 +1213,399 @@ 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); run(true); } +void TestAutoStartKeepsStartedPlayback() { + bool startedByTitle = false; + for (int attempt = 0; attempt < 10 && !startedByTitle; ++attempt) { + AvPlayerInitData init = InitData(nullptr); + auto* player = sceAvPlayerInit(&init); + Check(player != nullptr, "init failed"); + Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == 0, "add source failed"); + startedByTitle = sceAvPlayerEnableStream(player, VideoStream) == 0 && sceAvPlayerEnableStream(player, EnglishAudioStream) == 0; + if (startedByTitle) { + const auto releasedTextures = [] { + std::lock_guard lock(allocations.mutex); + return allocations.textures - ActiveTextureCountLocked(); + }; + Check(sceAvPlayerStart(player) == 0, "start failed"); + const int released = releasedTextures(); + AvPlayerFrameInfoEx frame{}; + Check(WaitFor([&] { return sceAvPlayerGetVideoDataEx(player, &frame) != 0; }), "no video after the title's start"); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + Check(releasedTextures() == released, "auto-start restarted playback the title had started"); + Check(IsAllocation(frame.p_data, true), "auto-start released the frame the title holds"); + } + Check(sceAvPlayerClose(player) == 0, "close failed"); + } + Check(startedByTitle, "auto-start won every race against the title's own start"); } -int main() { +void TestRestartAfterPausedJumpKeepsClockRunning() { + Events events; + AvPlayerInitData init = InitData(&events); + auto* player = sceAvPlayerInit(&init); + Check(player != nullptr, "init failed"); + Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == 0, "add source failed"); + Check(sceAvPlayerEnableStream(player, VideoStream) == 0, "enable video failed"); + Check(sceAvPlayerEnableStream(player, EnglishAudioStream) == 0, "enable audio failed"); + Check(sceAvPlayerStart(player) == 0, "start failed"); + AvPlayerFrameInfoEx frame{}; + Check(WaitFor([&] { return sceAvPlayerGetVideoDataEx(player, &frame) != 0; }), "no video after start"); + Check(sceAvPlayerPause(player) == 0, "pause failed"); + Check(sceAvPlayerSetLooping(player, 1) == 0, "looping rejected"); + Check(sceAvPlayerJumpToTime(player, 0) == 0, "jump while paused failed"); + Check(sceAvPlayerStart(player) == 0, "restart after the paused jump failed"); + std::uint64_t last = 0; + Check(WaitFor([&] { + if (sceAvPlayerGetVideoDataEx(player, &frame)) last = frame.timestamp; + return last >= 200; + }), "playback froze after a restart that followed a paused jump"); + Check(sceAvPlayerCurrentTime(player) > 0, "clock stayed frozen after the restart"); + Check(sceAvPlayerClose(player) == 0, "close failed"); +} + +void 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); + auto* player = sceAvPlayerInit(&init); + Check(player != nullptr, "init failed"); + Check(sceAvPlayerSetAvSyncMode(player, 1) == 0, "sync mode rejected"); + Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == 0, "add source failed"); + Check(sceAvPlayerEnableStream(player, VideoStream) == 0, "enable video failed"); + Check(sceAvPlayerEnableStream(player, EnglishAudioStream) == 0, "enable audio failed"); + Check(sceAvPlayerStart(player) == 0, "start failed"); + AvPlayerFrameInfo sound{}; + Check(WaitFor([&] { return sceAvPlayerGetAudioData(player, &sound) != 0; }), "no audio"); + const auto start = std::chrono::steady_clock::now(); + const auto first = sound.timestamp; + std::uint64_t worst = 0; + while (sceAvPlayerIsActive(player) && std::chrono::steady_clock::now() - start < std::chrono::milliseconds(900)) { + const auto elapsed = static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count()); + while (sound.timestamp <= first + elapsed && sceAvPlayerGetAudioData(player, &sound)) CheckEnglishAudio(sound); + AvPlayerFrameInfoEx frame{}; + if (sceAvPlayerGetVideoDataEx(player, &frame)) { + CheckVideoFrame(frame); + if (sound.timestamp > frame.timestamp) worst = std::max(worst, sound.timestamp - frame.timestamp); + } + std::this_thread::sleep_for(std::chrono::milliseconds(66)); + } + Check(worst <= 150, "video fell " + std::to_string(worst) + " ms behind the audio"); + 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{}; + for (int round = 0; round < Rounds; ++round) { + Events events; + AvPlayerInitData init = InitData(&events); + auto* player = sceAvPlayerInit(&init); + Check(player != nullptr, "eof latency init failed"); + Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == 0, "eof latency add source failed"); + Check(WaitFor([&] { return events.Seen(EventReady); }), "eof latency ready event missing"); + Check(sceAvPlayerEnableStream(player, VideoStream) == 0, "eof latency enable video failed"); + Check(sceAvPlayerEnableStream(player, EnglishAudioStream) == 0, "eof latency enable audio failed"); + Check(sceAvPlayerStart(player) == 0, "eof latency start failed"); + Check(WaitFor([&] { return events.Seen(EventPlay); }), "eof latency play event missing"); + AvPlayerFrameInfoEx frame{}; + AvPlayerFrameInfo sound{}; + const auto drainDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + bool ended = false; + while (std::chrono::steady_clock::now() < drainDeadline) { + while (sceAvPlayerGetAudioData(player, &sound)) CheckEnglishAudio(sound); + while (sceAvPlayerGetVideoDataEx(player, &frame)) CheckVideoFrame(frame); + if (!sceAvPlayerIsActive(player)) { + ended = true; + break; + } + std::this_thread::yield(); + } + Check(ended, "eof latency playback never ended"); + const auto inactive = std::chrono::steady_clock::now(); + Check(WaitFor([&] { return events.Seen(EventStop); }), "eof latency stop event missing"); + std::chrono::steady_clock::time_point stopAt{}; + { + std::lock_guard lock(events.mutex); + Check(events.hasStop, "eof latency stop timestamp missing"); + stopAt = events.stopAt; + const int stops = static_cast(std::count_if(events.received.begin(), events.received.end(), [](const auto& event) { return event.first == EventStop; })); + Check(stops == 1, "eof latency stop delivered " + std::to_string(stops) + " times"); + } + elapsed[static_cast(round)] = stopAt > inactive ? stopAt - inactive : std::chrono::steady_clock::duration::zero(); + Check(sceAvPlayerCurrentTime(player) >= 950, "eof latency clock not at the end"); + Check(!sceAvPlayerIsActive(player), "eof latency active after the end"); + Check(sceAvPlayerClose(player) == 0, "eof latency close failed"); + std::lock_guard lock(allocations.mutex); + Check(allocations.blocks.empty(), "eof latency allocations leaked"); + } + std::sort(elapsed.begin(), elapsed.end()); + const auto median = elapsed[elapsed.size() / 2]; + const auto medianMs = std::chrono::duration_cast(median).count() / 1000.0; + std::printf("EofStopLatency median %.3f ms:", medianMs); + for (const auto& sample : elapsed) std::printf(" %.3f", std::chrono::duration_cast(sample).count() / 1000.0); + std::printf("\n"); + std::fflush(stdout); + 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; +} + +void TestCloseFromEventCallback(std::int32_t event) { + ClosingEvents events; + events.closeEvent = event; + AvPlayerInitData init = InitData(nullptr); + init.event_replacement = {&events, CloseOnEvent}; + init.auto_start = event == EventStop; + { + std::lock_guard lock(events.mutex); + events.player = sceAvPlayerInit(&init); + Check(events.player != nullptr, "callback-close init failed"); + Check(sceAvPlayerAddSource(events.player, "/app0/avplayer.mp4") == 0, "callback-close source failed"); + } + Check(WaitFor([&] { + std::lock_guard lock(events.mutex); + if (events.closed) return true; + if (event == EventStop) { + AvPlayerFrameInfoEx video{}; + AvPlayerFrameInfo audio{}; + if (sceAvPlayerGetVideoDataEx(events.player, &video)) CheckVideoFrame(video); + if (sceAvPlayerGetAudioData(events.player, &audio)) CheckEnglishAudio(audio); + } + return false; + }), "event callback did not finish closing the player"); + { + std::lock_guard lock(events.mutex); + Check(sceAvPlayerClose(events.replacement) == 0, "replacement player close failed"); + } + std::lock_guard lock(allocations.mutex); + Check(allocations.blocks.empty(), "callback close leaked media allocations"); +} + +} + +int main(int argc, char** argv) { try { + if (argc == 2 && std::strcmp(argv[1], "--callback-close") == 0) { + WriteMovie(); + TestCloseFromEventCallback(EventReady); + TestCloseFromEventCallback(EventStop); + std::puts("AvPlayer callback close tests passed"); + return 0; + } + 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(); @@ -1047,6 +1613,12 @@ int main() { TestFileReplacementAutoStart(); TestHandedOutFramesStayIntact(); TestPs5ExtendedInitLayout(); + TestAutoStartKeepsStartedPlayback(); + TestRestartAfterPausedJumpKeepsClockRunning(); + TestUnsyncedVideoKeepsUpWithAudio(); + TestNoSyncVideoCanLeadAudioClock(); + TestLoopWarningWaitsForPlayback(); + TestEofStopLatency(); 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 7cc5066612..31775180e5 100644 --- a/core/libs/tests/GuestAvailableCpumask.cpp +++ b/core/libs/tests/GuestAvailableCpumask.cpp @@ -1,16 +1,34 @@ #include "SceTypes.hpp" +#include +#include +#include #include +#include #include +#include +#include +#include extern "C" { KernelCpumask APS5_VABI sceKernelGetAvailableCpumask(void); int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +Pthread APS5_VABI scePthreadSelf(); int APS5_VABI scePthreadGetaffinity(Pthread thread, KernelCpumask* mask); int APS5_VABI scePthreadSetaffinity(Pthread thread, KernelCpumask mask); int APS5_VABI scePthreadAttrInit(PthreadAttr* attr); int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr); int APS5_VABI scePthreadAttrGetaffinity(const PthreadAttr* attr, KernelCpumask* mask); +int APS5_VABI scePthreadAttrSetaffinity(PthreadAttr* attr, KernelCpumask mask); +int APS5_VABI cpuset_getaffinity_nid_postfix(int level, int which, std::int64_t id, std::size_t size, void* mask); +int APS5_VABI cpuset_setaffinity_nid_postfix(int level, int which, std::int64_t id, std::size_t size, const void* mask); +int APS5_VABI getpid_nid_postfix(void); +int* APS5_VABI __error_nid_postfix(); +Pthread APS5_VABI pthread_self_nid_postfix(void); +int APS5_VABI pthread_getaffinity_np_nid_postfix(Pthread thread, std::size_t cpusetSize, void* cpuset); +int APS5_VABI pthread_setaffinity_np_nid_postfix(Pthread thread, std::size_t cpusetSize, const void* cpuset); +int APS5_VABI scePthreadGetthreadid(void); +int APS5_VABI sceKernelGetCurrentCpu(void); } static constexpr int SCE_OK = 0; @@ -18,11 +36,106 @@ static constexpr int PS5_LOGICAL_CPUS = 16; static void Require(bool value) { if (!value) std::abort(); } +struct RestoreProcessMask { + KernelCpumask mask; + bool* restored; + + ~RestoreProcessMask() { + const std::uint64_t requested[1] = {mask}; + *restored = cpuset_setaffinity_nid_postfix(3, 2, -1, 8, requested) == 0; + } +}; + +static constexpr std::size_t GUEST_CPUSET_BYTES = 32; +static constexpr int GUEST_ESRCH = 3; +static constexpr int GUEST_EDEADLK = 11; +static constexpr int GUEST_EINVAL = 22; +static constexpr int GUEST_EFAULT = 14; +static constexpr int GUEST_ERANGE = 34; + +static KernelCpumask ReadCpuset(Pthread thread) { + std::uint8_t cpuset[GUEST_CPUSET_BYTES + 8]; + std::memset(cpuset, 0xAB, sizeof(cpuset)); + Require(pthread_getaffinity_np_nid_postfix(thread, GUEST_CPUSET_BYTES, cpuset) == 0); + for (std::size_t i = sizeof(KernelCpumask); i < GUEST_CPUSET_BYTES; ++i) Require(cpuset[i] == 0); + for (std::size_t i = GUEST_CPUSET_BYTES; i < sizeof(cpuset); ++i) Require(cpuset[i] == 0xAB); + KernelCpumask mask = 0; + std::memcpy(&mask, cpuset, sizeof(mask)); + return mask; +} + static void* APS5_VABI Worker(void* arg) { static_cast*>(arg)->get(); 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(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; +} + +static void* APS5_VABI ReportCpuset(void* arg) { + auto* result = static_cast(arg); + std::uint64_t set[4] = {~0ull, ~0ull, ~0ull, ~0ull}; + result[0] = cpuset_getaffinity_nid_postfix(3, 1, -1, 8, set); + result[1] = set[0]; + result[2] = set[1]; + result[3] = cpuset_getaffinity_nid_postfix(3, 1, -1, sizeof(set), set); + result[4] = set[0] | (set[1] != 0 || set[2] != 0 || set[3] != 0 ? 1ull << 63 : 0); + result[5] = cpuset_getaffinity_nid_postfix(3, 1, -1, 4, set) == -1 && *__error_nid_postfix() == 34; + result[6] = cpuset_getaffinity_nid_postfix(3, 1, -1, 33, set) == -1 && *__error_nid_postfix() == 34; + result[7] = cpuset_getaffinity_nid_postfix(3, 1, -1, 8, nullptr) == -1 && *__error_nid_postfix() == 14; + return nullptr; +} + +struct SetCpusetResult { + KernelCpumask cpu; + int results[8]; + KernelCpumask affinity[3]; +}; + +static void* APS5_VABI SetCpuset(void* arg) { + auto* result = static_cast(arg); + const KernelCpumask cpu = result->cpu; + std::uint64_t set[4] = {cpu | (1ull << 40), 0, 0, 0}; + result->results[0] = cpuset_setaffinity_nid_postfix(3, 1, -1, 8, set); + scePthreadGetaffinity(scePthreadSelf(), &result->affinity[0]); + set[1] = 1; + result->results[1] = cpuset_setaffinity_nid_postfix(3, 1, -1, 16, set) == -1 && *__error_nid_postfix() == 22; + set[1] = 0; + result->results[2] = cpuset_setaffinity_nid_postfix(3, 1, -1, sizeof(set), set); + result->results[3] = cpuset_setaffinity_nid_postfix(3, 1, -1, 4, set) == -1 && *__error_nid_postfix() == 34; + result->results[4] = cpuset_setaffinity_nid_postfix(3, 1, -1, 33, set) == -1 && *__error_nid_postfix() == 34; + result->results[5] = cpuset_setaffinity_nid_postfix(3, 1, -1, 8, nullptr) == -1 && *__error_nid_postfix() == 14; + const std::uint64_t unavailable[1] = {1ull << 40}; + result->results[6] = cpuset_setaffinity_nid_postfix(3, 1, -1, 8, unavailable) == -1 && *__error_nid_postfix() == 11; + scePthreadGetaffinity(scePthreadSelf(), &result->affinity[1]); + const std::uint64_t process[1] = {cpu}; + result->results[7] = cpuset_setaffinity_nid_postfix(3, 2, getpid_nid_postfix(), 8, process); + scePthreadGetaffinity(scePthreadSelf(), &result->affinity[2]); + return nullptr; +} + static void* APS5_VABI ReportMask(void* arg) { *static_cast(arg) = sceKernelGetAvailableCpumask(); return nullptr; @@ -53,6 +166,7 @@ int main() { Require(scePthreadSetaffinity(thread, cpu) == SCE_OK); Require(scePthreadGetaffinity(thread, &threadAffinity) == SCE_OK); Require(threadAffinity == cpu); + Require(ReadCpuset(thread) == cpu); } release.set_value(); Require(scePthreadJoin(thread, nullptr) == SCE_OK); @@ -61,5 +175,190 @@ int main() { Require(scePthreadCreate(&thread, &attr, ReportMask, &fromThread, nullptr) == SCE_OK); Require(scePthreadJoin(thread, nullptr) == SCE_OK); Require(fromThread == available); + + std::uint64_t cpuset[8] = {}; + Require(scePthreadCreate(&thread, &attr, ReportCpuset, cpuset, nullptr) == SCE_OK); + Require(scePthreadJoin(thread, nullptr) == SCE_OK); + Require(cpuset[0] == 0 && cpuset[1] == available && cpuset[2] == ~0ull); + Require(cpuset[3] == 0 && cpuset[4] == available); + Require(cpuset[5] == 1 && cpuset[6] == 1 && cpuset[7] == 1); + + KernelCpumask first = available & (~available + 1); + KernelCpumask last = first; + for (KernelCpumask cpu = first; cpu != 0; cpu <<= 1) if ((available & cpu) != 0) last = cpu; + std::promise hold; + auto held = hold.get_future(); + Pthread bystander = nullptr; + Require(scePthreadCreate(&bystander, &attr, Worker, &held, nullptr) == SCE_OK); + SetCpusetResult set{last, {}, {}}; + Require(scePthreadCreate(&thread, &attr, SetCpuset, &set, nullptr) == SCE_OK); + Require(scePthreadJoin(thread, nullptr) == SCE_OK); + Require(set.results[0] == 0 && set.affinity[0] == last); + Require(set.results[1] == 1 && set.results[2] == 0); + Require(set.results[3] == 1 && set.results[4] == 1 && set.results[5] == 1); + Require(set.results[6] == 1 && set.affinity[1] == last); + Require(set.results[7] == 0 && set.affinity[2] == last); + KernelCpumask bystanderAffinity = 0; + Require(scePthreadGetaffinity(bystander, &bystanderAffinity) == SCE_OK); + Require(bystanderAffinity == last); + const std::uint64_t everything[1] = {available}; + Require(cpuset_setaffinity_nid_postfix(3, 2, -1, 8, everything) == 0); + Require(scePthreadGetaffinity(bystander, &bystanderAffinity) == SCE_OK); + Require(bystanderAffinity == available); + hold.set_value(); + Require(scePthreadJoin(bystander, nullptr) == SCE_OK); + + if (first != available) { + const std::uint64_t single[1] = {last}; + const bool changed = cpuset_setaffinity_nid_postfix(3, 2, -1, 8, single) == 0; + bool restored = false; + bool created = false; + bool joined = false; + bool explicitSet = false; + bool explicitCreated = false; + bool explicitJoined = false; + std::uint64_t inherited[8]{}; + std::uint64_t explicitAffinity[8]{}; + if (changed) { + RestoreProcessMask restore{available, &restored}; + Pthread child = nullptr; + created = scePthreadCreate(&child, &attr, ReportCpuset, inherited, nullptr) == SCE_OK; + joined = created && scePthreadJoin(child, nullptr) == SCE_OK; + explicitSet = scePthreadAttrSetaffinity(&attr, available) == SCE_OK; + child = nullptr; + explicitCreated = explicitSet && scePthreadCreate(&child, &attr, ReportCpuset, explicitAffinity, nullptr) == SCE_OK; + explicitJoined = explicitCreated && scePthreadJoin(child, nullptr) == SCE_OK; + } + Require(changed && restored && created && joined && inherited[0] == 0 && inherited[1] == last); + Require(explicitSet && explicitCreated && explicitJoined && explicitAffinity[0] == 0 && explicitAffinity[1] == available); + } + + std::uint64_t processSet[4] = {~0ull, ~0ull, ~0ull, ~0ull}; + Require(cpuset_getaffinity_nid_postfix(3, 2, -1, sizeof(processSet), processSet) == 0); + Require(processSet[0] == available && processSet[1] == 0 && processSet[2] == 0 && processSet[3] == 0); + processSet[0] = 0; + Require(cpuset_getaffinity_nid_postfix(3, 2, getpid_nid_postfix(), 8, processSet) == 0 && processSet[0] == available); + Require(cpuset_getaffinity_nid_postfix(3, 2, -1, 4, processSet) == -1 && *__error_nid_postfix() == GUEST_ERANGE); + Require(cpuset_getaffinity_nid_postfix(3, 2, -1, 8, nullptr) == -1 && *__error_nid_postfix() == GUEST_EFAULT); + if (first != last) { + std::promise park; + auto parked = park.get_future(); + Pthread pinned = nullptr; + Require(scePthreadCreate(&pinned, &attr, Worker, &parked, nullptr) == SCE_OK); + Require(scePthreadSetaffinity(pinned, first) == SCE_OK); + Require(scePthreadSetaffinity(scePthreadSelf(), last) == SCE_OK); + std::uint64_t unionSet[1] = {0}; + const int unionResult = cpuset_getaffinity_nid_postfix(3, 2, -1, 8, unionSet); + std::uint64_t ownSet[1] = {0}; + const int ownResult = cpuset_getaffinity_nid_postfix(3, 1, -1, 8, ownSet); + Require(scePthreadSetaffinity(scePthreadSelf(), available) == SCE_OK); + park.set_value(); + Require(scePthreadJoin(pinned, nullptr) == SCE_OK); + Require(unionResult == 0 && unionSet[0] == (first | last)); + Require(ownResult == 0 && ownSet[0] == last); + } + std::uint64_t request[GUEST_CPUSET_BYTES / sizeof(std::uint64_t)] = {first, 0, 0, 0}; + Require(pthread_setaffinity_np_nid_postfix(pthread_self_nid_postfix(), GUEST_CPUSET_BYTES, request) == 0); + Require(ReadCpuset(pthread_self_nid_postfix()) == first); + std::promise settle; + auto settled = settle.get_future(); + Pthread target = nullptr; + Require(scePthreadCreate(&target, &attr, Worker, &settled, nullptr) == SCE_OK); + request[0] = last; + Require(pthread_setaffinity_np_nid_postfix(target, GUEST_CPUSET_BYTES, request) == 0); + Require(ReadCpuset(target) == last && ReadCpuset(pthread_self_nid_postfix()) == first); + request[0] = available | (1ull << 40); + request[3] = 1ull << 63; + Require(pthread_setaffinity_np_nid_postfix(target, GUEST_CPUSET_BYTES, request) == 0); + Require(ReadCpuset(target) == available); + const std::uint64_t outside[GUEST_CPUSET_BYTES / sizeof(std::uint64_t)] = {1ull << 40, 1, 0, 0}; + Require(pthread_setaffinity_np_nid_postfix(target, GUEST_CPUSET_BYTES, outside) == GUEST_EDEADLK); + Require(pthread_setaffinity_np_nid_postfix(pthread_self_nid_postfix(), GUEST_CPUSET_BYTES, outside) == GUEST_EDEADLK); + Require(ReadCpuset(target) == available && ReadCpuset(pthread_self_nid_postfix()) == first); + for (const std::size_t size : {std::size_t{0}, std::size_t{8}, std::size_t{31}, std::size_t{33}, std::size_t{64}}) { + Require(pthread_setaffinity_np_nid_postfix(pthread_self_nid_postfix(), size, request) == GUEST_ERANGE); + Require(pthread_setaffinity_np_nid_postfix(target, size, request) == GUEST_ERANGE); + } + Require(pthread_setaffinity_np_nid_postfix(target, GUEST_CPUSET_BYTES, nullptr) == GUEST_EFAULT); + Require(pthread_setaffinity_np_nid_postfix(nullptr, GUEST_CPUSET_BYTES, request) == GUEST_EINVAL); + std::uint64_t unknownThread[8]{}; + Require(pthread_setaffinity_np_nid_postfix(reinterpret_cast(unknownThread), GUEST_CPUSET_BYTES, request) == GUEST_ESRCH); + Require(pthread_setaffinity_np_nid_postfix(reinterpret_cast(unknownThread), 8, request) == GUEST_ESRCH); + settle.set_value(); + Require(scePthreadJoin(target, nullptr) == SCE_OK); + Require(pthread_setaffinity_np_nid_postfix(target, GUEST_CPUSET_BYTES, request) == GUEST_ESRCH); + request[0] = available; + request[3] = 0; + Require(pthread_setaffinity_np_nid_postfix(pthread_self_nid_postfix(), GUEST_CPUSET_BYTES, request) == 0); Require(scePthreadAttrDestroy(&attr) == SCE_OK); + + Require(ReadCpuset(pthread_self_nid_postfix()) == available); + std::uint8_t rawSet[GUEST_CPUSET_BYTES]{}; + for (const std::size_t size : {std::size_t{0}, std::size_t{8}, std::size_t{31}, std::size_t{33}, std::size_t{64}}) + Require(pthread_getaffinity_np_nid_postfix(pthread_self_nid_postfix(), size, rawSet) == GUEST_ERANGE); + Require(pthread_getaffinity_np_nid_postfix(nullptr, GUEST_CPUSET_BYTES, rawSet) == GUEST_EINVAL); + Require(pthread_getaffinity_np_nid_postfix(nullptr, 0, rawSet) == GUEST_EINVAL); + Require(pthread_getaffinity_np_nid_postfix(pthread_self_nid_postfix(), GUEST_CPUSET_BYTES, nullptr) == GUEST_EFAULT); + Require(cpuset_getaffinity_nid_postfix(3, 1, scePthreadGetthreadid(), 8, processSet) == 0 && processSet[0] == available); + Require(cpuset_getaffinity_nid_postfix(3, 1, 999999, 8, processSet) == -1 && *__error_nid_postfix() == GUEST_ESRCH); + Require(cpuset_getaffinity_nid_postfix(3, 2, 999999, 8, processSet) == -1 && *__error_nid_postfix() == GUEST_ESRCH); + Require(cpuset_getaffinity_nid_postfix(99, 1, -1, 8, processSet) == -1 && *__error_nid_postfix() == GUEST_EINVAL); + Require(cpuset_getaffinity_nid_postfix(99, 1, 999999, 8, processSet) == -1 && *__error_nid_postfix() == GUEST_ESRCH); + Require(cpuset_getaffinity_nid_postfix(99, 2, 999999, 8, processSet) == -1 && *__error_nid_postfix() == GUEST_ESRCH); + Require(cpuset_getaffinity_nid_postfix(3, 99, -1, 8, processSet) == -1 && *__error_nid_postfix() == GUEST_EINVAL); + Require(cpuset_setaffinity_nid_postfix(3, 1, scePthreadGetthreadid(), 8, processSet) == 0); + Require(cpuset_setaffinity_nid_postfix(3, 1, 999999, 8, processSet) == -1 && *__error_nid_postfix() == GUEST_ESRCH); + Require(cpuset_setaffinity_nid_postfix(3, 2, 999999, 8, processSet) == -1 && *__error_nid_postfix() == GUEST_ESRCH); + Require(cpuset_setaffinity_nid_postfix(99, 1, -1, 8, processSet) == -1 && *__error_nid_postfix() == GUEST_EINVAL); + Require(cpuset_setaffinity_nid_postfix(99, 1, 999999, 8, processSet) == -1 && *__error_nid_postfix() == GUEST_ESRCH); + Require(cpuset_setaffinity_nid_postfix(99, 2, 999999, 8, processSet) == -1 && *__error_nid_postfix() == GUEST_ESRCH); + Require(cpuset_setaffinity_nid_postfix(3, 99, -1, 8, processSet) == -1 && *__error_nid_postfix() == GUEST_EINVAL); + const auto throws = [](auto&& call) { + try { + call(); + return false; + } catch (const std::runtime_error&) { + return true; + } + }; + Require(throws([&] { cpuset_getaffinity_nid_postfix(1, 1, -1, 8, processSet); })); + Require(throws([&] { cpuset_getaffinity_nid_postfix(2, 1, -1, 8, processSet); })); + Require(throws([&] { cpuset_getaffinity_nid_postfix(3, 3, -1, 8, processSet); })); + Require(throws([&] { cpuset_getaffinity_nid_postfix(3, 4, -1, 8, processSet); })); + Require(throws([&] { cpuset_setaffinity_nid_postfix(1, 1, -1, 8, processSet); })); + Require(throws([&] { cpuset_setaffinity_nid_postfix(2, 1, -1, 8, processSet); })); + Require(throws([&] { cpuset_setaffinity_nid_postfix(3, 3, -1, 8, processSet); })); + Require(throws([&] { cpuset_setaffinity_nid_postfix(3, 4, -1, 8, processSet); })); + std::promise holdOther; + auto heldOther = holdOther.get_future(); + RemoteThread otherArg; + otherArg.release = &heldOther; + Pthread otherThread = nullptr; + Require(scePthreadCreate(&otherThread, nullptr, RecordThreadId, &otherArg, nullptr) == SCE_OK); + 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(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..e118532944 100644 --- a/core/libs/tests/GuestBluetoothHid.cpp +++ b/core/libs/tests/GuestBluetoothHid.cpp @@ -12,6 +12,11 @@ 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 sceBluetoothHidDebugGetVersion(); +int APS5_VABI sceBluetoothHidGetDeviceInfo(); +int APS5_VABI sceBluetoothHidDisconnectDevice(); +int APS5_VABI sceBluetoothHidGetFeatureReport(); +int APS5_VABI sceBluetoothHidSetFeatureReport(); } namespace { @@ -51,6 +56,35 @@ int main() { 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(); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw, "get device info did not throw"); + threw = false; + try { + sceBluetoothHidDisconnectDevice(); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw, "disconnect device did not throw"); + threw = false; + try { + sceBluetoothHidGetFeatureReport(); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw, "get feature report did not throw"); + threw = false; + try { + sceBluetoothHidSetFeatureReport(); + } 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/GuestCallTrace.cpp b/core/libs/tests/GuestCallTrace.cpp new file mode 100644 index 0000000000..4b7e2091e9 --- /dev/null +++ b/core/libs/tests/GuestCallTrace.cpp @@ -0,0 +1,202 @@ +#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 +#include +#include +#include +#include +#include + +struct Block { + std::uint64_t words[4]; +}; + +static void Require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "Call trace check failed at line %u\n", location.line()); + std::abort(); + } +} + +static std::uint64_t APS5_VABI Integers(std::uint64_t a, std::uint64_t b, std::uint64_t c, std::uint64_t d, std::uint64_t e, std::uint64_t f, std::uint64_t g, std::uint64_t h) { + return a + 2 * b + 3 * c + 4 * d + 5 * e + 6 * f + 7 * g + 8 * h; +} + +static double APS5_VABI Doubles(double a, double b, double c, double d, double e, double f, double g, double h, double i, double j) { + return a + 2 * b + 3 * c + 4 * d + 5 * e + 6 * f + 7 * g + 8 * h + 9 * i + 10 * j; +} + +static double APS5_VABI Mixed(int a, double b, const char* c, float d, std::uint64_t e) { + return a + b + static_cast(std::strlen(c)) + d + static_cast(e); +} + +static double APS5_VABI Variadic(int count, ...) { + __builtin_sysv_va_list arguments; + __builtin_sysv_va_start(arguments, count); + double sum = 0; + for (int index = 0; index < count; ++index) { + sum += static_cast(__builtin_va_arg(arguments, std::uint64_t)) * (index + 1); + sum += __builtin_va_arg(arguments, double) * (index + 1); + } + __builtin_sysv_va_end(arguments); + return sum; +} + +static Block APS5_VABI Blocks(std::uint64_t seed) { + return {{seed, seed + 1, seed + 2, seed + 3}}; +} + +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); +#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); + std::atomic go{false}; + std::thread([&go] { + while (!go.load()) { + } + std::abort(); + }).detach(); + go.store(true); + } +#endif + std::abort(); + } + CallTraceEntry entries[16]; + CallTraceCount counts[8]; + std::uint64_t total = 1; + Require(CopyCallTrace_nid_no_patch(entries, 16) == 0); + Require(CopyCallCounts_nid_no_patch(counts, 8, &total) == 0 && total == 0); + Require(Traced(&Integers, "Integers", "libUnselected.prx") == &Integers); + Require(TraceImport_nid_no_patch(nullptr, "Integers", "libTraced.prx") == nullptr); + Require(CopyCallTrace_nid_no_patch(entries, 16) == 0); + + const auto integers = Traced(&Integers, "Integers", "libTraced.prx"); + const auto doubles = Traced(&Doubles, "Doubles", "libTraced"); + const auto mixed = Traced(&Mixed, "Mixed", "libOther.prx"); + const auto variadic = Traced(&Variadic, "Variadic", "libOther"); + const auto blocks = Traced(&Blocks, "Blocks", "libTraced.prx"); + Require(integers != &Integers && doubles != &Doubles && mixed != &Mixed && variadic != &Variadic && blocks != &Blocks); + Require(Traced(&Integers, "Alias", "libOther.prx") == integers); + + Require(integers(1, 2, 3, 4, 5, 6, 7, 8) == Integers(1, 2, 3, 4, 5, 6, 7, 8)); + Require(doubles(1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5, 10.5) == Doubles(1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5, 10.5)); + Require(mixed(-3, 0.25, "trace", 1.5f, 1ull << 40) == Mixed(-3, 0.25, "trace", 1.5f, 1ull << 40)); + const double expected = Variadic(8, 1ull, 0.5, 2ull, 1.5, 3ull, 2.5, 4ull, 3.5, 5ull, 4.5, 6ull, 5.5, 7ull, 6.5, 8ull, 7.5); + Require(variadic(8, 1ull, 0.5, 2ull, 1.5, 3ull, 2.5, 4ull, 3.5, 5ull, 4.5, 6ull, 5.5, 7ull, 6.5, 8ull, 7.5) == expected); + const Block block = blocks(40); + Require(block.words[0] == 40 && block.words[1] == 41 && block.words[2] == 42 && block.words[3] == 43); + + const char* const names[] = {"Integers", "Doubles", "Mixed", "Variadic", "Blocks"}; + const char* const libraries[] = {"libTraced.prx", "libTraced", "libOther.prx", "libOther", "libTraced.prx"}; + 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 == CurrentThread()); + Require(std::strcmp(entries[index].name, names[index]) == 0 && std::strcmp(entries[index].library, libraries[index]) == 0); + 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); + for (std::size_t index = 0; index < 5; ++index) { + Require(counts[index].calls == 1 && std::strcmp(counts[index].name, names[index]) == 0 && std::strcmp(counts[index].library, libraries[index]) == 0); + } + Require(doubles(1, 1, 1, 1, 1, 1, 1, 1, 1, 1) == 55 && blocks(1).words[0] == 1 && blocks(2).words[0] == 2); + Require(CopyCallCounts_nid_no_patch(counts, 2, nullptr) == 2); + Require(counts[0].calls == 3 && std::strcmp(counts[0].name, "Blocks") == 0 && counts[1].calls == 2 && std::strcmp(counts[1].name, "Doubles") == 0); + Require(CopyCallCounts_nid_no_patch(nullptr, 8, &total) == 0 && total == 8); + 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, &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)); + }); + } + for (auto& worker : workers) worker.join(); + constexpr std::size_t Window = 65536; + const auto recent = std::make_unique(Window); + Require(CopyCallTrace_nid_no_patch(recent.get(), Window) == Window); + Require(recent[Window - 1].sequence == 8 + Workers * Calls && recent[0].sequence == 8 + Workers * Calls - Window + 1); + for (std::size_t index = 0; index < Window; ++index) Require(std::strcmp(recent[index].name, "Integers") == 0); + Require(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 985713bcbf..92646677b6 100644 --- a/core/libs/tests/GuestCes.cpp +++ b/core/libs/tests/GuestCes.cpp @@ -5,8 +5,22 @@ 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); +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*); } static void Require(bool value) { if (!value) std::abort(); } @@ -80,4 +94,230 @@ int main() { Require(Utf8ToSbc(profile, "\xED\xA0\x80", 3) == invalidEncode); Require(Utf8ToSbc(profile, "\xE3\x81\x82", 3) == unassignedCode); Require(Utf8ToSbc(profile, "\xF0\x9F\x98\x80", 4) == unassignedCode); + + const std::uint8_t* cyrillic = sceCesRefersUcsProfileCp1251(); + Require(cyrillic != nullptr && cyrillic == sceCesRefersUcsProfileCp1251() && cyrillic != profile); + RequireSbcToUtf8(cyrillic, 'A', "A"); + RequireSbcToUtf8(cyrillic, 0x88, "\xE2\x82\xAC"); + RequireSbcToUtf8(cyrillic, 0xC0, "\xD0\x90"); + RequireSbcToUtf8(cyrillic, 0xFF, "\xD1\x8F"); + RequireSbcToUtf8(cyrillic, 0xA8, "\xD0\x81"); + RequireSbcToUtf8(cyrillic, 0xB9, "\xE2\x84\x96"); + RequireUtf8ToSbc(cyrillic, "\xD0\x9F", 0xCF); + RequireUtf8ToSbc(cyrillic, "\xD1\x91", 0xB8); + for (int sbc = 1; sbc < 256; ++sbc) { + std::uint8_t cp[4] = {}; + std::uint32_t cpLength = 0; + std::uint8_t back = 0; + Require(sceCesSbcToUtf8(cyrillic, static_cast(sbc), cp, sizeof(cp), &cpLength) == 0); + Require(sceCesUtf8ToSbc(cp, cpLength, &cpLength, cyrillic, &back) == 0 && back == sbc); + } + 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 sjisSheet[256] = {}; + Require(sceCesUcsProfileInitSJis1997Cp932(sjisSheet) == sjisSheet); + Require(sceCesUcsProfileInitSJis1997Cp932(nullptr) == nullptr); + 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] = {}; + 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(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/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/GuestCharacter.cpp b/core/libs/tests/GuestCharacter.cpp index 60331fc87e..7601716b77 100644 --- a/core/libs/tests/GuestCharacter.cpp +++ b/core/libs/tests/GuestCharacter.cpp @@ -3,6 +3,7 @@ #include #include #include +#include extern "C" { int APS5_VABI isupper_nid_postfix(int); int APS5_VABI islower_nid_postfix(int); @@ -20,10 +21,24 @@ int APS5_VABI toupper_nid_postfix(int); int APS5_VABI tolower_nid_postfix(int); const short* APS5_VABI _Getptolower_nid_postfix(); const short* APS5_VABI _Getptoupper_nid_postfix(); +const char* APS5_VABI setlocale_nid_postfix(int, const char*); } static void Require(bool value) { if (!value) std::abort(); } int main() { Require(std::setlocale(LC_CTYPE, "C") != nullptr); + for (int category = 0; category <= 6; ++category) { + const char* result = setlocale_nid_postfix(category, "C"); + Require(result != nullptr && std::strcmp(result, "C") == 0); + result = setlocale_nid_postfix(category, "POSIX"); + Require(result != nullptr && std::strcmp(result, "C") == 0); + result = setlocale_nid_postfix(category, nullptr); + Require(result != nullptr && std::strcmp(result, "C") == 0); + result = setlocale_nid_postfix(category, ""); + Require(result != nullptr && std::strcmp(result, "C") == 0); + Require(setlocale_nid_postfix(category, "en_US.UTF-8") == nullptr); + } + Require(setlocale_nid_postfix(-1, "C") == nullptr); + Require(setlocale_nid_postfix(7, "C") == nullptr); for (int c = EOF; c <= 255; ++c) { Require(bool(isupper_nid_postfix(c)) == bool(std::isupper(c))); Require(bool(islower_nid_postfix(c)) == bool(std::islower(c))); diff --git a/core/libs/tests/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 ed4b1976d4..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() { @@ -127,30 +264,40 @@ int main() { const std::string longText = "collation of a longer text"; require(Vtable(facet).transform(&result, collate, longText.data(), longText.data() + longText.size()) == &result); - require(allocations == 2 && lastSize == longText.size() + 1 && result.pointer == lastAllocation); - require(result.size == longText.size() && result.capacity == longText.size() && std::string(result.pointer) == longText); + require(allocations == 2 && lastSize == 32 && result.pointer == lastAllocation); + require(result.size == longText.size() && result.capacity == 31 && std::string(result.pointer) == longText); std::free(result.pointer); - const std::string largest(4094, 'z'); - require(Vtable(facet).transform(&result, collate, largest.data(), largest.data() + largest.size()) == &result); - require(allocations == 3 && lastSize == 4095 && result.capacity == 4094 && std::string(result.pointer) == largest); + const std::string largestSmall(4079, 'z'); + require(Vtable(facet).transform(&result, collate, largestSmall.data(), largestSmall.data() + largestSmall.size()) == &result); + require(allocations == 3 && lastSize == 4080 && result.pointer == lastAllocation && result.capacity == 4079 && std::string(result.pointer) == largestSmall); std::free(result.pointer); - const std::string tooLong(4095, 'z'); - reject([&] { Vtable(facet).transform(&result, collate, tooLong.data(), tooLong.data() + tooLong.size()); }); + const std::string smallestLarge(4080, 'y'); + require(Vtable(facet).transform(&result, collate, smallestLarge.data(), smallestLarge.data() + smallestLarge.size()) == &result); + const auto raw = reinterpret_cast(lastAllocation); + const auto aligned = reinterpret_cast(result.pointer); + require(allocations == 4 && lastSize == 4096 + 0x27); + require(aligned % 0x20 == 0 && aligned >= raw + 8 && aligned <= raw + 0x27 && reinterpret_cast(result.pointer)[-1] == lastAllocation); + require(result.size == 4080 && result.capacity == 4095 && std::string(result.pointer) == smallestLarge); + std::free(lastAllocation); + const std::string embedded("a\0b", 3); reject([&] { Vtable(facet).transform(&result, collate, embedded.data(), embedded.data() + embedded.size()); }); reject([&] { Vtable(facet).transform(nullptr, collate, text, text + 1); }); - require(allocations == 3); + require(allocations == 4); Vtable(facet).facet.retain(facet); Vtable(facet).facet.retain(facet); 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..1638caf9ec --- /dev/null +++ b/core/libs/tests/GuestCommonDialog.cpp @@ -0,0 +1,24 @@ +#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(); +} + +} + +int main() { + Require(!sceCommonDialogIsUsed()); + Require(sceCommonDialogInitialize() == COMMON_DIALOG_OK); + Require(!sceCommonDialogIsUsed()); + Require(sceCommonDialogInitialize() == COMMON_DIALOG_ERROR_ALREADY_INITIALIZED); + Require(!sceCommonDialogIsUsed()); +} diff --git a/core/libs/tests/GuestCondTimedwait.cpp b/core/libs/tests/GuestCondTimedwait.cpp index 310812871b..91c18ac93e 100644 --- a/core/libs/tests/GuestCondTimedwait.cpp +++ b/core/libs/tests/GuestCondTimedwait.cpp @@ -1,5 +1,6 @@ #include "SceTypes.hpp" #include +#include #include #include @@ -10,9 +11,14 @@ 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 scePthreadMutexTrylock(PthreadMutex* mutex); int APS5_VABI scePthreadCondInit(PthreadCond* cond, const PthreadCondattr* attr, const char* name); int APS5_VABI scePthreadCondDestroy(PthreadCond* cond); int APS5_VABI scePthreadCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, unsigned int usec); +int APS5_VABI scePthreadCondSignal(PthreadCond* cond); +int APS5_VABI pthread_cond_init_nid_postfix(PthreadCond* cond, const PthreadCondattr* attr); +int APS5_VABI pthread_cond_destroy_nid_postfix(PthreadCond* cond); +int APS5_VABI pthread_cond_reltimedwait_np_nid_postfix(PthreadCond* cond, PthreadMutex* mutex, unsigned int usec); int APS5_VABI __cxa_atexit_nid_postfix(void (APS5_VABI *)(void*), void*, void*); void APS5_VABI __pthread_cxa_finalize_nid_postfix(void*); unsigned int APS5_VABI sceKernelSleep(unsigned int seconds); @@ -23,6 +29,9 @@ static void APS5_VABI Cleanup(void*) { cleanupAt = 0; } 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_ETIMEDOUT = 60; static void Require(bool value) { if (!value) std::abort(); } @@ -39,7 +48,67 @@ static void* APS5_VABI Contender(void* arg) { return nullptr; } +struct Signalled { + PthreadMutex mutex = nullptr; + PthreadCond cond = nullptr; + bool ready = false; + int trylock = SCE_OK; +}; + +static void* APS5_VABI Trylocker(void* arg) { + auto& signalled = *static_cast(arg); + signalled.trylock = scePthreadMutexTrylock(&signalled.mutex); + return nullptr; +} + +static void* APS5_VABI Signaller(void* arg) { + auto& signalled = *static_cast(arg); + Require(scePthreadMutexLock(&signalled.mutex) == SCE_OK); + signalled.ready = true; + Require(scePthreadCondSignal(&signalled.cond) == SCE_OK); + Require(scePthreadMutexUnlock(&signalled.mutex) == SCE_OK); + return nullptr; +} + +static void RequireHeldByCaller(Signalled& signalled) { + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, Trylocker, &signalled, nullptr) == SCE_OK); + Require(scePthreadJoin(thread, nullptr) == SCE_OK); + Require(signalled.trylock == SCE_KERNEL_ERROR_EBUSY); +} + +static void RelativeWait() { + Signalled signalled; + Require(scePthreadMutexInit(&signalled.mutex, nullptr, nullptr) == SCE_OK); + Require(scePthreadCondInit(&signalled.cond, nullptr, nullptr) == SCE_OK); + + Require(pthread_cond_reltimedwait_np_nid_postfix(&signalled.cond, &signalled.mutex, 1000) == POSIX_EPERM); + + Require(scePthreadMutexLock(&signalled.mutex) == SCE_OK); + const auto start = std::chrono::steady_clock::now(); + Require(pthread_cond_reltimedwait_np_nid_postfix(&signalled.cond, &signalled.mutex, 50000) == POSIX_ETIMEDOUT); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(40)); + RequireHeldByCaller(signalled); + Require(pthread_cond_reltimedwait_np_nid_postfix(&signalled.cond, &signalled.mutex, 0) == POSIX_ETIMEDOUT); + RequireHeldByCaller(signalled); + + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, Signaller, &signalled, nullptr) == SCE_OK); + const auto waitStart = std::chrono::steady_clock::now(); + while (!signalled.ready) + Require(pthread_cond_reltimedwait_np_nid_postfix(&signalled.cond, &signalled.mutex, 10000000) == 0); + Require(std::chrono::steady_clock::now() - waitStart < std::chrono::seconds(5)); + RequireHeldByCaller(signalled); + Require(scePthreadMutexUnlock(&signalled.mutex) == SCE_OK); + Require(scePthreadJoin(thread, nullptr) == SCE_OK); + + Require(scePthreadCondDestroy(&signalled.cond) == SCE_OK); + Require(scePthreadMutexDestroy(&signalled.mutex) == SCE_OK); +} + int main() { + RelativeWait(); + Context context; PthreadCond cond = nullptr; Require(scePthreadMutexInit(&context.mutex, nullptr, nullptr) == SCE_OK); @@ -54,6 +123,15 @@ int main() { Require(scePthreadJoin(thread, nullptr) == SCE_OK); Require(scePthreadCondDestroy(&cond) == SCE_OK); + + PthreadCondattr unset = nullptr; + Require(scePthreadCondInit(&cond, &unset, nullptr) == SCE_OK); + Require(scePthreadMutexLock(&context.mutex) == SCE_OK); + Require(scePthreadCondTimedwait(&cond, &context.mutex, 1000) == SCE_KERNEL_ERROR_ETIMEDOUT); + Require(scePthreadMutexUnlock(&context.mutex) == SCE_OK); + Require(scePthreadCondDestroy(&cond) == SCE_OK); + Require(pthread_cond_init_nid_postfix(&cond, &unset) == 0); + Require(pthread_cond_destroy_nid_postfix(&cond) == 0); Require(scePthreadMutexDestroy(&context.mutex) == SCE_OK); Require(sceKernelSleep(0) == SCE_OK); diff --git a/core/libs/tests/GuestCondUnownedMutex.cpp b/core/libs/tests/GuestCondUnownedMutex.cpp new file mode 100644 index 0000000000..f68fe17391 --- /dev/null +++ b/core/libs/tests/GuestCondUnownedMutex.cpp @@ -0,0 +1,106 @@ +#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 scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* attr, const char* name); +int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex); +int APS5_VABI scePthreadMutexLock(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 scePthreadCondWait(PthreadCond* cond, PthreadMutex* mutex); +int APS5_VABI scePthreadCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, unsigned int usec); +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* tp); +} + +static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_EPERM = static_cast(0x80020001); +static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); +static constexpr int POSIX_EPERM = 1; +static constexpr int POSIX_EINVAL = 22; +static constexpr std::uintptr_t ADAPTIVE_INITIALIZER = 1; + +static void Require(bool value) { if (!value) std::abort(); } + +static int PosixTimed(PthreadCond* cond, PthreadMutex* mutex) { + KernelTimespec deadline{}; + Require(clock_gettime_nid_postfix(0, &deadline) == 0); + deadline.tv_sec += 60; + return pthread_cond_timedwait_nid_postfix(cond, mutex, &deadline); +} + +struct Wait { + int (*run)(PthreadCond*, PthreadMutex*); + int notOwned; + int destroyed; +}; + +static int SceWait(PthreadCond* cond, PthreadMutex* mutex) { return scePthreadCondWait(cond, mutex); } +static int SceTimed(PthreadCond* cond, PthreadMutex* mutex) { return scePthreadCondTimedwait(cond, mutex, 60000000); } +static int PosixWait(PthreadCond* cond, PthreadMutex* mutex) { return pthread_cond_wait_nid_postfix(cond, mutex); } + +struct Holder { + PthreadMutex mutex = nullptr; + std::atomic locked{false}; + std::atomic release{false}; +}; + +static void* APS5_VABI Hold(void* arg) { + auto& holder = *static_cast(arg); + Require(scePthreadMutexLock(&holder.mutex) == SCE_OK); + holder.locked.store(true); + while (!holder.release.load()) + std::this_thread::yield(); + Require(scePthreadMutexUnlock(&holder.mutex) == SCE_OK); + return nullptr; +} + +static void Check(const Wait& wait) { + PthreadCond cond = nullptr; + Require(scePthreadCondInit(&cond, nullptr, nullptr) == SCE_OK); + + PthreadMutex unlocked = nullptr; + Require(scePthreadMutexInit(&unlocked, nullptr, nullptr) == SCE_OK); + Require(wait.run(&cond, &unlocked) == wait.notOwned); + Require(scePthreadMutexLock(&unlocked) == SCE_OK); + Require(scePthreadMutexUnlock(&unlocked) == SCE_OK); + Require(scePthreadMutexDestroy(&unlocked) == SCE_OK); + + PthreadMutex uninitialized = nullptr; + Require(wait.run(&cond, &uninitialized) == wait.notOwned); + + PthreadMutex adaptive = reinterpret_cast(ADAPTIVE_INITIALIZER); + Require(wait.run(&cond, &adaptive) == wait.notOwned); + + PthreadMutex destroyed = nullptr; + Require(scePthreadMutexInit(&destroyed, nullptr, nullptr) == SCE_OK); + Require(scePthreadMutexDestroy(&destroyed) == SCE_OK); + Require(wait.run(&cond, &destroyed) == wait.destroyed); + + Holder holder; + Require(scePthreadMutexInit(&holder.mutex, nullptr, nullptr) == SCE_OK); + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, Hold, &holder, nullptr) == SCE_OK); + while (!holder.locked.load()) + std::this_thread::yield(); + Require(wait.run(&cond, &holder.mutex) == wait.notOwned); + holder.release.store(true); + Require(scePthreadJoin(thread, nullptr) == SCE_OK); + Require(scePthreadMutexDestroy(&holder.mutex) == SCE_OK); + + Require(scePthreadCondDestroy(&cond) == SCE_OK); +} + +int main() { + Check({SceWait, SCE_KERNEL_ERROR_EPERM, SCE_KERNEL_ERROR_EINVAL}); + Check({SceTimed, SCE_KERNEL_ERROR_EPERM, SCE_KERNEL_ERROR_EINVAL}); + Check({PosixWait, POSIX_EPERM, POSIX_EINVAL}); + Check({PosixTimed, POSIX_EPERM, POSIX_EINVAL}); +} diff --git a/core/libs/tests/GuestContentDelete.cpp b/core/libs/tests/GuestContentDelete.cpp new file mode 100644 index 0000000000..9353f3a578 --- /dev/null +++ b/core/libs/tests/GuestContentDelete.cpp @@ -0,0 +1,53 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include + +extern "C" int APS5_VABI sceContentDeleteInitialize(const ContentDeleteInitParam* init_param); +extern "C" int APS5_VABI sceContentDeleteTerminate(void); +extern "C" int APS5_VABI sceContentDeleteById(std::int64_t contentId); +extern "C" int APS5_VABI sceContentDeleteByPath(const char* contentPath); + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +template +std::string LogicErrorMessage(TAction action) { + try { + action(); + } catch (const std::invalid_argument&) { + return "invalid argument"; + } catch (const std::logic_error& error) { + return error.what(); + } + return "returned"; +} + +bool Contains(const std::string& text, const std::string& part) { return text.find(part) != std::string::npos; } + +} + +int main() { + Require(LogicErrorMessage([] { sceContentDeleteTerminate(); }) == "sceContentDeleteTerminate: not initialized"); + Require(LogicErrorMessage([] { sceContentDeleteInitialize(nullptr); }) == "invalid argument"); + Require(LogicErrorMessage([] { sceContentDeleteById(1); }) == "sceContentDeleteById: not initialized"); + Require(LogicErrorMessage([] { sceContentDeleteByPath("/user/av_contents/photo/a.png"); }) == "sceContentDeleteByPath: not initialized"); + Require(LogicErrorMessage([] { sceContentDeleteByPath(nullptr); }) == "invalid argument"); + + ContentDeleteInitParam param{}; + param.heap_size = 0x10000; + Require(sceContentDeleteInitialize(¶m) == 0); + Require(LogicErrorMessage([&] { sceContentDeleteInitialize(¶m); }) == "sceContentDeleteInitialize: already initialized"); + + Require(LogicErrorMessage([] { sceContentDeleteByPath(nullptr); }) == "invalid argument"); + Require(Contains(LogicErrorMessage([] { sceContentDeleteByPath("/user/av_contents/photo/a.png"); }), "sceContentDeleteByPath: /user/av_contents/photo/a.png is not in the host content library")); + Require(Contains(LogicErrorMessage([] { sceContentDeleteById(0x123456789); }), "sceContentDeleteById: content id 4886718345 is not in the host content library")); + Require(Contains(LogicErrorMessage([] { sceContentDeleteById(-1); }), "sceContentDeleteById: content id -1 is not in the host content library")); + + Require(sceContentDeleteTerminate() == 0); + Require(LogicErrorMessage([] { sceContentDeleteTerminate(); }) == "sceContentDeleteTerminate: not initialized"); + Require(LogicErrorMessage([] { sceContentDeleteById(1); }) == "sceContentDeleteById: not initialized"); + return 0; +} diff --git a/core/libs/tests/GuestContentExport.cpp b/core/libs/tests/GuestContentExport.cpp index 12038531a8..40c6aa941d 100644 --- a/core/libs/tests/GuestContentExport.cpp +++ b/core/libs/tests/GuestContentExport.cpp @@ -1,25 +1,54 @@ #include "SceTypes.hpp" +#include #include +#include #include +extern "C" int APS5_VABI sceContentExportAddToFavorites(void); +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(void); +extern "C" int APS5_VABI sceContentExportFromFileWithContentIdList(void); +extern "C" int APS5_VABI sceContentExportFromFileWithTitleIdList(void); +extern "C" int APS5_VABI sceContentExportInit(void); +extern "C" int APS5_VABI sceContentExportMalloc(void); +extern "C" int APS5_VABI sceContentExportRemoveFromFavorites(void); +extern "C" int APS5_VABI sceContentExportRenameContent(void); +extern "C" int APS5_VABI sceContentExportValidateContents(void); + 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); namespace { void Require(bool value) { if (!value) std::abort(); } -template +template bool Throws(TAction action) { try { action(); - } catch (const std::logic_error&) { + } catch (const TException&) { return true; } return false; } int allocator = 0; +char path[1024]{}; + +void Export(std::int32_t expId) { + ContentExportParam param{}; + ContentExportCallbackParam content{}; + ContentExportCallbackParam thumbnail{}; + sceContentExportFromDataWithThumbnail(expId, ¶m, &content, "image/png", &thumbnail, path, sizeof(path)); +} } @@ -27,6 +56,8 @@ int main() { ContentExportInitParam2 param{&allocator, &allocator, nullptr, 0x10000, 0, 0}; Require(Throws([] { sceContentExportInit2(nullptr); })); Require(Throws([] { sceContentExportTerm(); })); + Require(Throws([] { sceContentExportStart(); })); + Require(Throws([] { sceContentExportFinish(1); })); ContentExportInitParam2 missing = param; missing.free_func = nullptr; Require(Throws([&] { sceContentExportInit2(&missing); })); @@ -38,5 +69,46 @@ int main() { Require(sceContentExportTerm() == 0); Require(Throws([] { sceContentExportTerm(); })); Require(sceContentExportInit2(¶m) == 0); + + std::set sessions; + for (int i = 0; i < 10; ++i) { + int session = sceContentExportStart(); + Require(session > 0); + Require(sessions.insert(session).second); + } + Require(Throws([] { sceContentExportStart(); })); + Require(Throws([] { sceContentExportTerm(); })); + int first = *sessions.begin(); + Require(Throws([&] { sceContentExportGetProgress(first); })); + Require(Throws([&] { sceContentExportCancel(first); })); + Require(Throws([&] { Export(first); })); + Require(path[0] == 0); + Require(sceContentExportFinish(first) == 0); + Require(Throws([&] { sceContentExportFinish(first); })); + Require(Throws([&] { sceContentExportGetProgress(first); })); + Require(Throws([&] { sceContentExportCancel(first); })); + Require(Throws([&] { Export(first); })); + Require(Throws([] { sceContentExportFinish(0); })); + Require(Throws([] { sceContentExportFinish(-1); })); + sessions.erase(first); + int reopened = sceContentExportStart(); + Require(reopened > 0 && reopened != first); + Require(sessions.insert(reopened).second); + for (int session : sessions) Require(sceContentExportFinish(session) == 0); Require(sceContentExportTerm() == 0); + Require(Throws([] { sceContentExportStart(); })); + Require(Throws([&] { sceContentExportFinish(reopened); })); + Require(Throws([] { sceContentExportAddToFavorites(); })); + Require(Throws([] { sceContentExportDataProvideFunction(); })); + Require(Throws([] { sceContentExportFree(); })); + Require(Throws([] { sceContentExportFrom(); })); + Require(Throws([] { sceContentExportFromFileForSf(); })); + Require(Throws([] { sceContentExportFromFileWithContentIdList(); })); + Require(Throws([] { sceContentExportFromFileWithTitleIdList(); })); + Require(Throws([] { sceContentExportInit(); })); + Require(Throws([] { sceContentExportMalloc(); })); + Require(Throws([] { sceContentExportRemoveFromFavorites(); })); + Require(Throws([] { sceContentExportRenameContent(); })); + Require(Throws([] { sceContentExportValidateContents(); })); } + diff --git a/core/libs/tests/GuestContentSearch.cpp b/core/libs/tests/GuestContentSearch.cpp new file mode 100644 index 0000000000..2447383112 --- /dev/null +++ b/core/libs/tests/GuestContentSearch.cpp @@ -0,0 +1,121 @@ +#include "SceTypes.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI sceContentSearchInit(const ContentSearchInitParam* init_param); +int APS5_VABI sceContentSearchTerm(void); +int APS5_VABI sceContentSearchGetContentLastUpdateId(std::int64_t* lastUpdateId); +int APS5_VABI sceContentSearchGetNumOfContent(const ContentSearchContentColumnSet* columnSet, std::uint32_t columnSetLength, std::int64_t* numOfContent, std::int64_t* lastUpdateId); +int APS5_VABI sceContentSearchGetTotalContentSize(const ContentSearchContentColumnSet* columnSet, std::uint32_t columnSetLength, std::int64_t* size, std::int64_t* lastUpdateId); +int APS5_VABI sceContentSearchSearchContent(const ContentSearchContentColumnSet* columnSet, std::uint32_t columnSetLength, const ContentSearchContentOrderByCondition* orderByConditions, std::uint32_t orderByConditionsLength, std::uint32_t offset, std::uint32_t limit, std::int64_t* numOfContent, void* infos, std::int64_t* lastUpdateId); +int APS5_VABI sceContentSearchOpenMetadata(const char* filePath, std::int32_t* metadataId); +int APS5_VABI sceContentSearchOpenMetadataByContentId(std::int64_t contentId, std::int32_t* metadataId); +int APS5_VABI sceContentSearchCloseMetadata(std::int32_t metadataId); +int APS5_VABI sceContentSearchGetMetadataFieldInfo(std::int32_t metadataId, const char* field, std::int32_t* metadataType, std::int32_t* size); +int APS5_VABI sceContentSearchGetMetadataValue(std::int32_t metadataId, const char* field, void* value); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +template +bool Throws(TAction action) { + try { + action(); + } catch (const std::logic_error&) { + return true; + } + return false; +} + +constexpr int ErrorNotInitialized = static_cast(0x809D1001u); +constexpr int ErrorLimitTooBig = static_cast(0x809D100Au); + +struct MetadataValue { + std::int32_t size; + std::int32_t type; + std::int64_t value; +}; + +} + +int main() { + ContentSearchInitParam param{3 * 1024 * 1024}; + ContentSearchColumnValue photo{8, 1}; + ContentSearchContentColumn column{3, 0, &photo, 2}; + ContentSearchContentColumnSet columnSet{&column, 1, 2}; + ContentSearchContentOrderByCondition orderBy{4, 0}; + unsigned char infos[0x960 * 2] = {}; + std::int64_t count = -1; + std::int64_t size = -1; + std::int64_t updateId = -1; + std::int32_t metadataId = -1; + std::int32_t metadataType = -1; + std::int32_t fieldSize = -1; + MetadataValue value{-1, -1, -1}; + + Require(sceContentSearchGetContentLastUpdateId(&updateId) == ErrorNotInitialized); + Require(sceContentSearchGetNumOfContent(&columnSet, 1, &count, &updateId) == ErrorNotInitialized); + Require(sceContentSearchGetTotalContentSize(&columnSet, 1, &size, &updateId) == ErrorNotInitialized); + Require(sceContentSearchSearchContent(&columnSet, 1, &orderBy, 1, 0, 2, &count, infos, &updateId) == ErrorNotInitialized); + Require(sceContentSearchOpenMetadata("/data/photo.jpg", &metadataId) == ErrorNotInitialized); + Require(sceContentSearchOpenMetadataByContentId(1, &metadataId) == ErrorNotInitialized); + Require(sceContentSearchCloseMetadata(1) == ErrorNotInitialized); + Require(count == -1 && size == -1 && updateId == -1 && metadataId == -1); + + Require(sceContentSearchInit(¶m) == 0); + + Require(sceContentSearchGetContentLastUpdateId(&updateId) == 0); + Require(updateId == 0); + updateId = -1; + Require(sceContentSearchGetNumOfContent(&columnSet, 1, &count, &updateId) == 0); + Require(count == 0 && updateId == 0); + count = -1; + updateId = -1; + Require(sceContentSearchGetNumOfContent(nullptr, 0, &count, &updateId) == 0); + Require(count == 0 && updateId == 0); + updateId = -1; + Require(sceContentSearchGetTotalContentSize(&columnSet, 1, &size, &updateId) == 0); + Require(size == 0 && updateId == 0); + count = -1; + updateId = -1; + Require(sceContentSearchSearchContent(&columnSet, 1, &orderBy, 1, 0, 92, &count, infos, &updateId) == 0); + Require(count == 0 && updateId == 0); + count = -1; + updateId = -1; + Require(sceContentSearchSearchContent(&columnSet, 1, &orderBy, 1, 0, 93, &count, infos, &updateId) == ErrorLimitTooBig); + Require(count == -1 && updateId == -1); + + Require(Throws([&] { sceContentSearchGetContentLastUpdateId(nullptr); })); + Require(Throws([&] { sceContentSearchGetNumOfContent(&columnSet, 1, nullptr, &updateId); })); + Require(Throws([&] { sceContentSearchGetNumOfContent(&columnSet, 1, &count, nullptr); })); + Require(Throws([&] { sceContentSearchGetTotalContentSize(nullptr, 1, &size, &updateId); })); + Require(Throws([&] { sceContentSearchSearchContent(&columnSet, 1, &orderBy, 1, 0, 2, &count, nullptr, &updateId); })); + ContentSearchContentColumn unknownColumn{8, 0, &photo, 2}; + ContentSearchContentColumnSet unknownColumnSet{&unknownColumn, 1, 2}; + Require(Throws([&] { sceContentSearchGetNumOfContent(&unknownColumnSet, 1, &count, &updateId); })); + ContentSearchContentColumn missingValue{3, 0, nullptr, 2}; + ContentSearchContentColumnSet missingValueSet{&missingValue, 1, 2}; + Require(Throws([&] { sceContentSearchGetTotalContentSize(&missingValueSet, 1, &size, &updateId); })); + ContentSearchContentColumn unknownOperator{3, 6, &photo, 2}; + ContentSearchContentColumnSet unknownOperatorSet{&unknownOperator, 1, 2}; + Require(Throws([&] { sceContentSearchSearchContent(&unknownOperatorSet, 1, &orderBy, 1, 0, 2, &count, infos, &updateId); })); + ContentSearchContentOrderByCondition unknownSort{3, 0}; + Require(Throws([&] { sceContentSearchSearchContent(&columnSet, 1, &unknownSort, 1, 0, 2, &count, infos, &updateId); })); + Require(count == -1 && updateId == -1); + + Require(Throws([&] { sceContentSearchOpenMetadata("/data/photo.jpg", &metadataId); })); + Require(Throws([&] { sceContentSearchOpenMetadataByContentId(1, &metadataId); })); + Require(metadataId == -1); + Require(Throws([] { sceContentSearchCloseMetadata(1); })); + Require(Throws([&] { sceContentSearchGetMetadataFieldInfo(1, "title", &metadataType, &fieldSize); })); + Require(Throws([&] { sceContentSearchGetMetadataValue(1, "title", &value); })); + Require(metadataType == -1 && fieldSize == -1 && value.size == -1); + + Require(sceContentSearchTerm() == 0); + Require(sceContentSearchGetContentLastUpdateId(&updateId) == ErrorNotInitialized); + Require(updateId == -1); +} diff --git a/core/libs/tests/GuestConvertKeycode.cpp b/core/libs/tests/GuestConvertKeycode.cpp new file mode 100644 index 0000000000..0e7095e097 --- /dev/null +++ b/core/libs/tests/GuestConvertKeycode.cpp @@ -0,0 +1,59 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI sceConvertKeycodeGetImeKeyboardType(std::int32_t, std::uint32_t*); +int APS5_VABI sceConvertKeycodeGetCharacter(std::uint16_t, std::uint32_t, std::uint32_t, char16_t*); +} + +static void Require(bool value) { if (!value) std::abort(); } + +int main() { + constexpr int invalidAddress = static_cast(0x80BC0031u); + constexpr int invalidUserId = static_cast(0x80BC0010u); + + Require(sceConvertKeycodeGetImeKeyboardType(0, nullptr) == invalidAddress); + Require(sceConvertKeycodeGetImeKeyboardType(-1, nullptr) == invalidAddress); + + std::uint32_t type = 0xFFFFFFFFu; + Require(sceConvertKeycodeGetImeKeyboardType(-1, &type) == invalidUserId); + Require(type == 0xFFFFFFFFu); + + Require(sceConvertKeycodeGetImeKeyboardType(0, &type) == 0); + Require(type == 0); + + type = 0xFFFFFFFFu; + Require(sceConvertKeycodeGetImeKeyboardType(1, &type) == 0); + Require(type == 0); + + Require(sceConvertKeycodeGetCharacter(0x04, 0, 0, nullptr) == invalidAddress); + for (std::uint32_t state : {0u, 0x4000000u}) { + char16_t character[3] = {u'?', 0, 0}; + Require(sceConvertKeycodeGetCharacter(0x04, state, 0, character) == 0); + Require(character[0] == u'a'); + Require(sceConvertKeycodeGetCharacter(0x1E, state, 0, character) == 0); + Require(character[0] == u'1'); + Require(sceConvertKeycodeGetCharacter(0x3A, state, 0, character) == 0); + Require(character[0] == 0); + Require(sceConvertKeycodeGetCharacter(0x64, state, 0, character) == 0); + Require(character[0] == u'\\'); + Require(sceConvertKeycodeGetCharacter(0x32, state, 0, character) == 0); + Require(character[0] == 0); + Require(sceConvertKeycodeGetCharacter(0x27, state, 0, character) == 0); + Require(character[0] == u'0'); + Require(sceConvertKeycodeGetCharacter(0x2C, state, 0, character) == 0); + Require(character[0] == u' '); + Require(sceConvertKeycodeGetCharacter(0x2D, state, 0, character) == 0); + Require(character[0] == u'-'); + Require(sceConvertKeycodeGetCharacter(0x54, state, 0, character) == 0); + Require(character[0] == u'/'); + Require(sceConvertKeycodeGetCharacter(0x59, state, 0, character) == 0); + Require(character[0] == u'1'); + Require(sceConvertKeycodeGetCharacter(0x63, state, 0, character) == 0); + Require(character[0] == u'.'); + } + return 0; +} diff --git a/core/libs/tests/GuestCoredumpAttach.cpp b/core/libs/tests/GuestCoredumpAttach.cpp new file mode 100644 index 0000000000..43e32238e0 --- /dev/null +++ b/core/libs/tests/GuestCoredumpAttach.cpp @@ -0,0 +1,111 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceCoredumpRegisterCoredumpHandler(uint64_t handler, size_t stackSize, uint64_t context); +int APS5_VABI sceCoredumpUnregisterCoredumpHandler(void); +int APS5_VABI sceCoredumpAttachMemoryRegion(uint32_t userValue, const void* mem, size_t size); +int APS5_VABI sceCoredumpAttachUserMemoryFile(uint32_t userValue, const void* mem, size_t size); +int APS5_VABI sceCoredumpGetStopInfoCpu(void* info, size_t size); +int APS5_VABI sceCoredumpGetThreadContextInfo(void* threadContextInfo, size_t threadContextInfoSize); +int APS5_VABI sceCoredumpGetStopInfoGpu_Agc(void* info, size_t size); +int APS5_VABI sceCoredumpWriteUserString(uint32_t first, const char* text, int third); +int APS5_VABI sceCoredumpConfigDumpMode(int mode); +} + +namespace { +constexpr int InvalidArgument = static_cast(0x81180000u); +constexpr int NotInCoredumpHandler = static_cast(0x81180003u); +constexpr uint32_t UserFileTruncateIfExceedLimit = 0x80000000u; +bool handlerRan = false; +int handlerContext = 0; + +int APS5_VABI Handler(void*) { + handlerRan = true; + return 0; +} + +void Require(bool value) { if (!value) std::abort(); } + +template +bool Throws(TAttach attach, const char* function) { + try { attach(); } + catch (const std::runtime_error& error) { return std::strstr(error.what(), function) != nullptr; } + return false; +} +} + +int main() { + alignas(uint64_t) std::array region{}; + for (size_t index = 0; index < region.size(); ++index) region[index] = static_cast(index * 7 + 3); + const auto original = region; + 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(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); + Require(sceCoredumpAttachUserMemoryFile(0xFFFFFFFFu, region.data(), region.size()) == NotInCoredumpHandler); + Require(sceCoredumpGetStopInfoCpu(region.data() + 16, 16) == NotInCoredumpHandler); + int fromThread = 0; + std::thread([&] { fromThread = sceCoredumpAttachMemoryRegion(3, region.data(), region.size()); }).join(); + Require(fromThread == NotInCoredumpHandler); + std::thread([&] { fromThread = sceCoredumpGetStopInfoCpu(region.data() + 16, 16); }).join(); + Require(fromThread == NotInCoredumpHandler); + Require(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")); + for (const size_t size : {size_t{0}, size_t{1}, size_t{15}, size_t{17}, SIZE_MAX}) + Require(Throws([&] { sceCoredumpGetStopInfoCpu(region.data() + 16, size); }, "sceCoredumpGetStopInfoCpu")); + Require(Throws([&] { sceCoredumpGetThreadContextInfo(nullptr, 144); }, "sceCoredumpGetThreadContextInfo")); + Require(Throws([&] { sceCoredumpGetThreadContextInfo(context.data() + 1, 144); }, "sceCoredumpGetThreadContextInfo")); + Require(Throws([&] { sceCoredumpGetThreadContextInfo(reinterpret_cast(UINTPTR_MAX - 7u), 144); }, "sceCoredumpGetThreadContextInfo")); + for (const size_t size : {size_t{0}, size_t{1}, size_t{143}, size_t{145}, SIZE_MAX}) + Require(Throws([&] { sceCoredumpGetThreadContextInfo(context.data(), size); }, "sceCoredumpGetThreadContextInfo")); + + Require(sceCoredumpUnregisterCoredumpHandler() == 0); + Require(sceCoredumpAttachMemoryRegion(1, region.data(), region.size()) == NotInCoredumpHandler); + Require(sceCoredumpAttachUserMemoryFile(1, region.data(), region.size()) == NotInCoredumpHandler); + Require(sceCoredumpGetStopInfoCpu(region.data() + 16, 16) == NotInCoredumpHandler); + Require(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 new file mode 100644 index 0000000000..30776ebbcc --- /dev/null +++ b/core/libs/tests/GuestCoredumpUserData.cpp @@ -0,0 +1,94 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceCoredumpRegisterCoredumpHandler(uint64_t handler, size_t stackSize, uint64_t context); +int APS5_VABI sceCoredumpUnregisterCoredumpHandler(void); +int64_t APS5_VABI sceCoredumpWriteUserData(const void* data, size_t size); +void APS5_VABI sceCoredumpDebugTextOut(const char* str, int len); +int APS5_VABI sceCoredumpAttachUserFile(uint32_t userValue, const char* path); +int APS5_VABI sceCoredumpAttachMemoryRegionAsUserFile(uint32_t userValue, const void* mem, size_t size, const char* name); +} + +namespace { +constexpr int64_t NotInCoredumpHandler = static_cast(0x81180003u); +bool handlerRan = false; +int handlerContext = 0; + +int APS5_VABI Handler(void*) { + handlerRan = true; + return 0; +} + +void Require(bool value) { if (!value) std::abort(); } + +template +bool Throws(TCall call, const char* function) { + try { call(); } + catch (const std::runtime_error& error) { return std::strstr(error.what(), function) != nullptr; } + return false; +} + +void RequireWritesRefused(const std::array& region) { + Require(sceCoredumpWriteUserData(region.data(), region.size()) == NotInCoredumpHandler); + Require(sceCoredumpWriteUserData(region.data(), 1) == NotInCoredumpHandler); + int64_t fromThread = 0; + std::thread([&] { fromThread = sceCoredumpWriteUserData(region.data(), region.size()); }).join(); + Require(fromThread == NotInCoredumpHandler); +} + +void RequireAttachesRefused(const std::array& region) { + Require(sceCoredumpAttachUserFile(1, "/app0/eboot.bin") == NotInCoredumpHandler); + Require(sceCoredumpAttachUserFile(0xFFFFFFFFu, "") == NotInCoredumpHandler); + Require(sceCoredumpAttachMemoryRegionAsUserFile(999, region.data(), region.size(), "region.txt") == NotInCoredumpHandler); + Require(sceCoredumpAttachMemoryRegionAsUserFile(0, region.data(), 1, "") == NotInCoredumpHandler); + int fromThread = 0; + std::thread([&] { fromThread = sceCoredumpAttachMemoryRegionAsUserFile(2, region.data(), region.size(), "thread.txt"); }).join(); + Require(fromThread == NotInCoredumpHandler); +} +} + +int main() { + std::array region{}; + for (size_t index = 0; index < region.size(); ++index) region[index] = static_cast(index * 5 + 1); + const auto original = region; + const char text[] = "coredump text\n"; + + Require(NotInCoredumpHandler < 0); + RequireWritesRefused(region); + RequireAttachesRefused(region); + sceCoredumpDebugTextOut(text, static_cast(sizeof(text) - 1)); + sceCoredumpDebugTextOut(text, 8); + sceCoredumpDebugTextOut(text, 0); + sceCoredumpDebugTextOut(nullptr, 0); + + Require(sceCoredumpRegisterCoredumpHandler(reinterpret_cast(&Handler), 16 * 1024, reinterpret_cast(&handlerContext)) == 0); + RequireWritesRefused(region); + RequireAttachesRefused(region); + sceCoredumpDebugTextOut(text, static_cast(sizeof(text) - 1)); + Require(!handlerRan); + + Require(Throws([&] { sceCoredumpWriteUserData(nullptr, region.size()); }, "sceCoredumpWriteUserData")); + Require(Throws([&] { sceCoredumpWriteUserData(region.data(), 0); }, "sceCoredumpWriteUserData")); + Require(Throws([&] { sceCoredumpWriteUserData(region.data(), SIZE_MAX); }, "sceCoredumpWriteUserData")); + Require(Throws([&] { sceCoredumpDebugTextOut(nullptr, 4); }, "sceCoredumpDebugTextOut")); + Require(Throws([&] { sceCoredumpDebugTextOut(text, -1); }, "sceCoredumpDebugTextOut")); + Require(Throws([&] { sceCoredumpAttachUserFile(1, nullptr); }, "sceCoredumpAttachUserFile")); + Require(Throws([&] { sceCoredumpAttachMemoryRegionAsUserFile(1, nullptr, region.size(), "a.txt"); }, "sceCoredumpAttachMemoryRegionAsUserFile")); + Require(Throws([&] { sceCoredumpAttachMemoryRegionAsUserFile(1, region.data(), 0, "a.txt"); }, "sceCoredumpAttachMemoryRegionAsUserFile")); + Require(Throws([&] { sceCoredumpAttachMemoryRegionAsUserFile(1, region.data(), SIZE_MAX, "a.txt"); }, "sceCoredumpAttachMemoryRegionAsUserFile")); + Require(Throws([&] { sceCoredumpAttachMemoryRegionAsUserFile(1, region.data(), region.size(), nullptr); }, "sceCoredumpAttachMemoryRegionAsUserFile")); + + Require(sceCoredumpUnregisterCoredumpHandler() == 0); + RequireWritesRefused(region); + RequireAttachesRefused(region); + Require(region == original); + Require(!handlerRan); + return 0; +} diff --git a/core/libs/tests/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 aa2c8544fe..887f69c727 100644 --- a/core/libs/tests/GuestCredentials.cpp +++ b/core/libs/tests/GuestCredentials.cpp @@ -1,14 +1,30 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include + 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 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 __error_nid_postfix(); } -using SetId = int (APS5_VABI *)(std::uint32_t); + static void Require(bool value) { if (!value) std::abort(); } +using SetId = int (APS5_VABI *)(std::uint32_t); +using SetReId = int (APS5_VABI *)(std::uint32_t, std::uint32_t); + static bool Throws(SetId function, std::uint32_t id) { try { function(id); @@ -17,6 +33,25 @@ static bool Throws(SetId function, std::uint32_t id) { } return false; } + +static bool ThrowsRe(SetReId function, std::uint32_t r, std::uint32_t e) { + try { + function(r, e); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +constexpr int GuestEfault = 14; +constexpr int GuestEinval = 22; +constexpr std::uint32_t Unchanged = 0xffffffffu; + +static void RequireFailure(int result, int error) { + Require(result == -1); + Require(*__error_nid_postfix() == error); +} + int main() { *__error_nid_postfix() = 13; Require(seteuid_nid_postfix(0) == 0); @@ -24,8 +59,48 @@ int main() { Require(*__error_nid_postfix() == 13); Require(Throws(seteuid_nid_postfix, 1000)); Require(Throws(setegid_nid_postfix, 1000)); - Require(Throws(seteuid_nid_postfix, 0xFFFFFFFFu)); - Require(Throws(setegid_nid_postfix, 0xFFFFFFFFu)); - Require(seteuid_nid_postfix(0) == 0); - Require(setegid_nid_postfix(0) == 0); + Require(Throws(seteuid_nid_postfix, Unchanged)); + 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); + Require(setreuid_nid_postfix(Unchanged, Unchanged) == 0); + Require(setreuid_nid_postfix(0, Unchanged) == 0); + Require(setreuid_nid_postfix(Unchanged, 0) == 0); + Require(setreuid_nid_postfix(0, 0) == 0); + Require(ThrowsRe(setreuid_nid_postfix, 1000, 0)); + Require(ThrowsRe(setreuid_nid_postfix, 0, 1000)); + + Require(setregid_nid_postfix(Unchanged, Unchanged) == 0); + Require(setregid_nid_postfix(0, Unchanged) == 0); + Require(setregid_nid_postfix(Unchanged, 0) == 0); + Require(setregid_nid_postfix(0, 0) == 0); + Require(ThrowsRe(setregid_nid_postfix, 1000, 0)); + Require(ThrowsRe(setregid_nid_postfix, 0, 1000)); + + std::array groups{}; + Require(getgroups_nid_postfix(0, nullptr) == 1); + Require(getgroups_nid_postfix(static_cast(groups.size()), groups.data()) == 1 && groups[0] == 0); + Require(groups[0] == static_cast(getegid_nid_postfix())); + + const std::array wanted{0, 5, 7}; + Require(setgroups_nid_postfix(static_cast(wanted.size()), wanted.data()) == 0); + Require(getgroups_nid_postfix(0, nullptr) == 3); + RequireFailure(getgroups_nid_postfix(2, groups.data()), GuestEinval); + RequireFailure(getgroups_nid_postfix(3, nullptr), GuestEfault); + Require(getgroups_nid_postfix(static_cast(groups.size()), groups.data()) == 3); + Require(groups[0] == 0 && groups[1] == 5 && groups[2] == 7); + + Require(setgroups_nid_postfix(0, nullptr) == 0); + Require(getgroups_nid_postfix(static_cast(groups.size()), groups.data()) == 1 && groups[0] == 0); + + RequireFailure(setgroups_nid_postfix(-1, wanted.data()), GuestEinval); + RequireFailure(setgroups_nid_postfix(1024, wanted.data()), GuestEinval); + RequireFailure(setgroups_nid_postfix(1, nullptr), GuestEfault); + Require(getgroups_nid_postfix(0, nullptr) == 1); + return 0; } diff --git a/core/libs/tests/GuestCrt.cpp b/core/libs/tests/GuestCrt.cpp new file mode 100644 index 0000000000..d2fe45d744 --- /dev/null +++ b/core/libs/tests/GuestCrt.cpp @@ -0,0 +1,241 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/SonyCrt.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +SonyLconv* APS5_VABI sceAmprLocaleconv(void); +double APS5_VABI sceAmprSinh(double x, double y); +char* APS5_VABI sceAmprStrtokS(char* str, const char* delim, char** context); +int APS5_VABI sceAmprCtimeS(char* buffer, std::size_t size, const std::int64_t* time); +int APS5_VABI sceAmprWcscpyS(std::uint16_t* dest, std::size_t size, const std::uint16_t* src); +void* APS5_VABI sceAmprSetConstraintHandlerS(void* handler); +int APS5_VABI sceAmprFltrounds(void); +void APS5_VABI sceAmprIgnoreHandlerS(const char* message, void* pointer, int error); +std::size_t APS5_VABI sceAmprMbstowcs(std::uint16_t* dest, const char* src, std::size_t size); +int APS5_VABI sceAmprAsctimeS(char* buffer, std::size_t size, const GuestTm* time); +int APS5_VABI ctime_s_nid_postfix(char* buffer, std::size_t size, const std::int64_t* time); +int APS5_VABI asctime_s_nid_postfix(char* buffer, std::size_t size, const GuestTm* time); +std::size_t APS5_VABI mbstowcs_nid_postfix(std::uint16_t* dest, const char* src, std::size_t size); +char* APS5_VABI strtok_s_nid_postfix(char* str, const char* delim, char** context); +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(); +} + +#define Require(condition) do { if (!(condition)) { std::fprintf(stderr, "guest_crt: line %d: %s\n", __LINE__, #condition); std::abort(); } } while (0) + +static int g_handlerCalls = 0; +static void CountingHandler(const char* message, void* pointer, int error) { + (void)message; + (void)pointer; + (void)error; + ++g_handlerCalls; +} + +int main() { + Require(sceAmprFpclassifyd(std::numeric_limits::quiet_NaN()) == 2); + Require(sceAmprFpclassifyd(std::numeric_limits::infinity()) == 1); + Require(sceAmprFpclassifyd(0.0) == 0); + Require(sceAmprFpclassifyd(std::numeric_limits::denorm_min()) == -2); + Require(sceAmprFpclassifyd(1.5) == -1); + Require(__fpclassifyd_nid_postfix(0.0) == 0); + + Require(sceAmprFltrounds() == 1); + Require(std::fesetround(FE_DOWNWARD) == 0); + Require(sceAmprFltrounds() == 3); + Require(std::fesetround(FE_TONEAREST) == 0); + Require(sceAmprFltrounds() == 1); + + Require(sceAmprSinh(1.0, 2.0) == 2.0 * std::sinh(1.0)); + Require(sceAmprSinh(0.0, 5.0) == 0.0); + + SonyLconv* locale = sceAmprLocaleconv(); + Require(reinterpret_cast(&locale->decimal_point) - reinterpret_cast(locale) == 0x48); + Require(std::strcmp(locale->decimal_point, ".") == 0); + Require(std::memcmp(localeconv_nid_postfix()->chars, locale->chars, sizeof(locale->chars)) == 0); + Require(std::strcmp(localeconv_nid_postfix()->decimal_point, ".") == 0); + for (const char* text : locale->monetary) Require(text != nullptr && text[0] == '\0'); + for (char value : locale->chars) Require(value == CHAR_MAX); + Require(std::strcmp(locale->false_text, "false") == 0 && std::strcmp(locale->true_text, "true") == 0); + Require(locale->grouping[0] == '\0' && locale->thousands_sep[0] == '\0'); + + Require(__udivti3_nid_postfix(100, 3) == 33); + Require(SonyUdivti3(100, 3) == 33); + Require(SonyUdivti3(0, 5) == 0); + Require(SonyUdivti3((unsigned __int128)1 << 100, 3) == (((unsigned __int128)1 << 100) / 3)); + bool trapped = false; + try { + SonyUdivti3(1, 0); + } catch (const std::exception&) { + trapped = true; + } + Require(trapped); + + Require(__divti3_nid_postfix(100, 3) == 33); + Require(__divti3_nid_postfix(-100, 3) == -33); + Require(__divti3_nid_postfix(100, -3) == -33); + Require(__divti3_nid_postfix(-100, -3) == 33); + Require(SonyDivti3(0, 5) == 0); + const __int128 large = (__int128)1 << 100; + Require(SonyDivti3(large, 3) == large / 3); + Require(SonyDivti3(-large, 3) == -(large / 3)); + Require(SonyDivti3(large, -3) == -(large / 3)); + Require(SonyDivti3(-large, -3) == large / 3); + const __int128 minimum = (__int128)((unsigned __int128)1 << 127); + const __int128 maximum = (__int128)(((unsigned __int128)1 << 127) - 1); + Require(SonyDivti3(minimum, 1) == minimum); + Require(SonyDivti3(minimum, -1) == minimum); + Require(SonyDivti3(maximum, -1) == -maximum); + trapped = false; + try { + SonyDivti3(1, 0); + } catch (const std::exception&) { + trapped = true; + } + 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); + Require(__umodti3_nid_postfix(0, 5) == 0); + Require(__umodti3_nid_postfix(3, 100) == 3); + Require(__umodti3_nid_postfix(unsignedMaximum, 1) == 0); + Require(__umodti3_nid_postfix(unsignedMaximum, unsignedMaximum) == 0); + Require(__umodti3_nid_postfix(largeDivisor, largeDivisor) == 0); + Require(__umodti3_nid_postfix(unsignedMaximum, 10) == 5); + Require(__umodti3_nid_postfix((unsigned __int128)1 << 100, 3) == 1); + Require(__umodti3_nid_postfix(unsignedMaximum, (unsigned __int128)1 << 64) == 0xFFFFFFFFFFFFFFFFull); + Require(__umodti3_nid_postfix(unsignedMaximum, largeDivisor) == 8); + Require(__umodti3_nid_postfix((unsigned __int128)1 << 127, largeDivisor) == 0x8000000000000006ull); + Require(__umodti3_nid_postfix(unsignedMaximum - 1, unsignedMaximum) == unsignedMaximum - 1); + Require(__umodti3_nid_postfix(unsignedMaximum, (unsigned __int128)1 << 127) == ((unsigned __int128)1 << 127) - 1); + trapped = false; + try { + __umodti3_nid_postfix(((unsigned __int128)0x1234 << 64) | 0x5678, 0); + } catch (const std::runtime_error& error) { + trapped = std::strcmp(error.what(), "__umodti3: integer divide by zero (0x00000000000012340000000000005678 % 0)") == 0; + } + Require(trapped); + + void* previous = sceAmprSetConstraintHandlerS(reinterpret_cast(&CountingHandler)); + Require(previous != nullptr); + Require(previous != reinterpret_cast(&CountingHandler)); + const std::uint16_t hello[] = {'h', 'i', 0}; + std::uint16_t buffer[8] = {}; + Require(sceAmprWcscpyS(buffer, 8, hello) == 0); + Require(buffer[0] == 'h' && buffer[1] == 'i' && buffer[2] == 0); + const std::uint16_t longText[] = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 0}; + g_handlerCalls = 0; + Require(sceAmprWcscpyS(buffer, 4, longText) == ERANGE); + Require(g_handlerCalls == 1); + Require(sceAmprWcscpyS(nullptr, 4, longText) == EINVAL); + previous = sceAmprSetConstraintHandlerS(nullptr); + Require(previous == reinterpret_cast(&CountingHandler)); + previous = sceAmprSetConstraintHandlerS(reinterpret_cast(&CountingHandler)); + Require(previous != nullptr && previous != reinterpret_cast(&CountingHandler)); + sceAmprIgnoreHandlerS("msg", nullptr, 0); + + char text[64]; + GuestTm known{}; + known.tm_year = 70; + known.tm_mon = 0; + known.tm_mday = 1; + known.tm_wday = 4; + Require(sceAmprAsctimeS(text, sizeof(text), &known) == 0); + Require(std::strcmp(text, "Thu Jan 1 00:00:00 1970\n") == 0); + std::memset(text, 0, sizeof(text)); + 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; + 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); + 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); + Require(std::strlen(text) == 25 && text[24] == '\n'); + char libcText[64]; + Require(ctime_s_nid_postfix(libcText, sizeof(libcText), &epoch) == 0); + Require(std::strcmp(libcText, text) == 0); + Require(sceAmprCtimeS(text, 25, &epoch) == EINVAL); + text[0] = 'x'; + Require(sceAmprCtimeS(text, sizeof(text), nullptr) == EINVAL && text[0] == '\0'); + + char line[] = "a,b,,c"; + char* context = nullptr; + Require(std::strcmp(sceAmprStrtokS(line, ",", &context), "a") == 0); + Require(std::strcmp(sceAmprStrtokS(nullptr, ",", &context), "b") == 0); + Require(std::strcmp(sceAmprStrtokS(nullptr, ",", &context), "c") == 0); + Require(sceAmprStrtokS(nullptr, ",", &context) == nullptr); + Require(sceAmprStrtokS(line, nullptr, &context) == nullptr); + char again[] = "x y"; + char* again_context = nullptr; + Require(std::strcmp(strtok_s_nid_postfix(again, " ", &again_context), "x") == 0); + + std::uint16_t wide[8] = {}; + Require(sceAmprMbstowcs(wide, "ABC", 8) == 3); + Require(wide[0] == 65 && wide[1] == 66 && wide[2] == 67 && wide[3] == 0); + Require(sceAmprMbstowcs(nullptr, "ABC", 8) == 3); + Require(sceAmprMbstowcs(nullptr, "ABC", 0) == 3 && sceAmprMbstowcs(nullptr, "ABC", 1) == 3); + Require(mbstowcs_nid_postfix(nullptr, "ABC", 0) == 3 && mbstowcs_nid_postfix(nullptr, "ABC", 1) == 3); + Require(sceAmprMbstowcs(wide, "ABC", 2) == 2); + Require(mbstowcs_nid_postfix(wide, "ABC", 8) == 3); + + const unsigned int firstRandom = _ZSt14_Random_devicev_nid_postfix(); + bool randomChanged = false; + for (int draw = 0; draw < 8; ++draw) randomChanged |= _ZSt14_Random_devicev_nid_postfix() != firstRandom; + Require(randomChanged); +} diff --git a/core/libs/tests/GuestCurrentCpu.cpp b/core/libs/tests/GuestCurrentCpu.cpp index b7d7d75cc0..389ad302ff 100644 --- a/core/libs/tests/GuestCurrentCpu.cpp +++ b/core/libs/tests/GuestCurrentCpu.cpp @@ -6,6 +6,10 @@ extern "C" { int APS5_VABI sceKernelGetCurrentCpu(void); +int APS5_VABI sceKernelGetMainSocId(void); +int APS5_VABI sceKernelGetCpumode(void); +std::int64_t APS5_VABI sceKernelGetCpuFrequency(void); +std::uint64_t APS5_VABI sceKernelGetTscFrequency(void); KernelCpumask APS5_VABI sceKernelGetAvailableCpumask(void); Pthread APS5_VABI scePthreadSelf(); int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); @@ -38,6 +42,8 @@ static void* APS5_VABI ReportWhilePinned(void* arg) { int main() { const KernelCpumask available = sceKernelGetAvailableCpumask(); + Require(sceKernelGetMainSocId() == 0 && sceKernelGetCpumode() == 0); + Require(sceKernelGetCpuFrequency() > 0 && static_cast(sceKernelGetCpuFrequency()) == sceKernelGetTscFrequency()); const std::array affinities{available, 0x1FFB, 0x3, 0x3F, 0x1000, 0x1, 0xFFFF, 0x10000}; for (const KernelCpumask affinity : affinities) { diff --git a/core/libs/tests/GuestDaemon.cpp b/core/libs/tests/GuestDaemon.cpp new file mode 100644 index 0000000000..0e87384eb6 --- /dev/null +++ b/core/libs/tests/GuestDaemon.cpp @@ -0,0 +1,19 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +extern "C" int APS5_VABI daemon_nid_postfix(int, int); +static void Require(bool value) { if (!value) std::abort(); } +static bool ReportsNotImplemented(int noChdir, int noClose) { + try { + daemon_nid_postfix(noChdir, noClose); + } catch (const std::runtime_error& error) { + return std::string(error.what()) == "daemon: forking into the background not implemented"; + } + return false; +} +int main() { + Require(ReportsNotImplemented(1, 0)); + Require(ReportsNotImplemented(0, 0)); + Require(ReportsNotImplemented(1, 1)); +} diff --git a/core/libs/tests/GuestDatagramReceive.cpp b/core/libs/tests/GuestDatagramReceive.cpp new file mode 100644 index 0000000000..2c7bc9e0c5 --- /dev/null +++ b/core/libs/tests/GuestDatagramReceive.cpp @@ -0,0 +1,154 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI socket_nid_postfix(int, int, int); +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 getsockname_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI fcntl_nid_postfix(int, 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*); +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 sceNetBind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI sceNetConnect(int, const void*, std::uint32_t); +int APS5_VABI sceNetGetsockname(int, void*, std::uint32_t*); +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*); +int APS5_VABI sceNetSocketClose(int); +int* APS5_VABI sceNetErrnoLoc(); +} + +static void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "%s\n", message); + std::exit(1); + } +} + +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 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; + destination[0] = static_cast(addressSize); + destination[1] = static_cast(family); + if (family == 2) { destination[4] = 127; destination[7] = 1; } + else destination[23] = 1; + const auto bind = [&](int descriptor) { + return net ? sceNetBind_nid_postfix(descriptor, destination.data(), addressSize) : + bind_nid_postfix(descriptor, destination.data(), addressSize); + }; + 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) : + getsockname_nid_postfix(sender, senderAddress.data(), &size)) == 0, "get sender address"); + size = addressSize; + Require((net ? sceNetGetsockname(receiver, destination.data(), &size) : + getsockname_nid_postfix(receiver, destination.data(), &size)) == 0, "get receiver address"); + Require((net ? sceNetConnect(receiver, senderAddress.data(), addressSize) : + connect_nid_postfix(receiver, senderAddress.data(), addressSize)) == 0, "connect receiver"); + const int enabled = 1; + Require((net ? sceNetSetsockopt(receiver, 0xffff, 0x1200, &enabled, sizeof(enabled)) : + fcntl_nid_postfix(receiver, 4, 6)) == 0, "make receiver nonblocking"); + 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); + return from ? recvfrom_nid_postfix(receiver, buffer, length, flags, address, addressLength) : + recv_nid_postfix(receiver, buffer, length, flags); + }; + const auto send = [&](const char* data, std::size_t length) { + Require((net ? sceNetSendto(sender, data, length, 0, destination.data(), addressSize) : + sendto_nid_postfix(sender, data, length, 0, destination.data(), addressSize)) == + static_cast(length), "send datagram"); + }; + const char message[] = "UDP header/body"; + for (const std::size_t capacity : {std::size_t{4}, sizeof(message), sizeof(message) + 4, std::size_t{0}}) { + send(message, sizeof(message)); + std::array buffer{}; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + std::int64_t result; + do { + result = receive(buffer.data(), buffer.size(), 2, nullptr, nullptr); + if (result >= 0 || *error != 35) break; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } while (std::chrono::steady_clock::now() < deadline); + Require(result == sizeof(message), "wait for complete datagram"); + for (const int flags : {2, 0}) { + buffer.fill('!'); + std::array address{}; + address.fill(0xa5); + size = address.size(); + *error = 123; + result = receive(buffer.data() + 1, capacity, flags, address.data(), &size); + const auto expected = std::min(capacity, sizeof(message)); + if (result != static_cast(expected)) { + std::fprintf(stderr, "%s%s IPv%d capacity=%zu flags=%d: expected %zu, got %lld errno=%d\n", + net ? "sceNetRecv" : "recv", from ? "from" : "", family == 2 ? 4 : 6, + capacity, flags, expected, static_cast(result), *error); + std::exit(1); + } + Require(*error == 123, "successful receive preserves errno"); + Require(buffer[0] == '!' && std::memcmp(buffer.data() + 1, message, expected) == 0 && + std::all_of(buffer.begin() + 1 + expected, buffer.end(), [](char value) { return value == '!'; }), + "copied prefix and buffer guards"); + if (from) { + Require(size == addressSize && std::memcmp(address.data(), senderAddress.data(), addressSize) == 0, + "source address on truncated receive"); + Require(std::all_of(address.begin() + addressSize, address.end(), [](unsigned char value) { return value == 0xa5; }), + "source address guards"); + } + if (flags == 2) { + Require(receive(buffer.data(), buffer.size(), 2, nullptr, nullptr) == sizeof(message) && + std::memcmp(buffer.data(), message, sizeof(message)) == 0, "short peek preserves complete packet"); + } + } + Require(receive(buffer.data(), buffer.size(), 0, nullptr, nullptr) < 0 && *error == 35, + "consuming receive discards remainder"); + } + send("", 0); + char byte = '!'; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + std::int64_t result; + do { + result = receive(&byte, 1, 0, nullptr, nullptr); + if (result >= 0 || *error != 35) break; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } while (std::chrono::steady_clock::now() < deadline); + Require(result == 0 && byte == '!', "empty datagram"); + Require(closeSocket(receiver) == 0, "close receiver"); + Require(closeSocket(sender) == 0, "close sender"); +} + +int main(int argc, char** argv) { + Require(argc == 2, "expected receive API argument"); + const bool net = std::strncmp(argv[1], "sce", 3) == 0; + const bool from = std::strstr(argv[1], "from") != nullptr; + if (net) Require(sceNetInit_nid_postfix() == 0, "initialize libSceNet"); + Check(net, from, 2); + Check(net, from, 28); +} diff --git a/core/libs/tests/GuestDeferredModule.cpp b/core/libs/tests/GuestDeferredModule.cpp new file mode 100644 index 0000000000..719a68829f --- /dev/null +++ b/core/libs/tests/GuestDeferredModule.cpp @@ -0,0 +1,142 @@ +#define WIN32_LEAN_AND_MEAN +#include "GuestDeferredModule.hpp" +#include +#include +#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*); +char* APS5_VABI dlerror_nid_postfix(); +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 char* what) { + if (!value) { + std::fprintf(stderr, "%s\n", what); + std::exit(1); + } +} + +static void* Handle(int id) { return reinterpret_cast(static_cast(id)); } + +static int Open(const std::string& path, const GuestDeferredModuleArgs* args = nullptr, bool initialized = true) { + int result = -1; + const int id = sceKernelLoadStartModule(path.c_str(), args ? sizeof(*args) : 0, args, 0, nullptr, &result); + Require(id >= 0, "load deferred module"); + Require(result == (initialized ? GuestDeferredModuleResult : 0), "module start result"); + return id; +} + +static void Close(int id) { Require(sceKernelStopUnloadModule(id, 0, nullptr, 0, nullptr, nullptr) == 0, "close module"); } + +static void Counts(int id, int expectedStarts, int expectedConstructors) { + using Count = int (APS5_VABI*)(); + const auto starts = reinterpret_cast(dlsym_nid_postfix(Handle(id), "DeferredStarts_nid_no_patch")); + const auto constructors = reinterpret_cast(dlsym_nid_postfix(Handle(id), "DeferredConstructors_nid_no_patch")); + Require(starts && constructors, "find fixture counters"); + if (starts() != expectedStarts || constructors() != expectedConstructors) { + std::fprintf(stderr, "starts=%d constructors=%d, expected=%d/%d\n", starts(), constructors(), expectedStarts, expectedConstructors); + std::exit(1); + } +} + +static HMODULE Native(int id) { + const auto symbol = dlsym_nid_postfix(Handle(id), "DeferredStarts_nid_no_patch"); + HMODULE native = nullptr; + Require(GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(symbol), &native), "native image handle"); + return native; +} + +static void MetadataProtected(int id) { + const auto table = GetProcAddress(Native(id), "__aps5_guest_initialize"); + MEMORY_BASIC_INFORMATION information{}; + Require(table && VirtualQuery(reinterpret_cast(table), &information, sizeof(information)), "query init metadata"); + Require(information.Protect == PAGE_READONLY, "init metadata protection restored"); +} + +static void APS5_VABI Reenter(const char* path) { + void* nested = dlopen_nid_postfix(path, 2); + Require(nested != nullptr, "recursive module load"); + Require(dlclose_nid_postfix(nested) == 0, "recursive module close"); +} + +int main(int argc, char** argv) { + Require(argc == 3, "fixture and mode required"); + const auto file = std::filesystem::absolute(argv[1]); + const auto path = file.string(); + const std::string mode = argv[2]; + if (mode == "reload") { + HMODULE firstBase = nullptr; + for (unsigned cycle = 0; cycle < 3; ++cycle) { + const int first = Open(path); + Counts(first, 1, 1); + const auto native = Native(first); + if (cycle == 0) firstBase = native; + Require(native == firstBase, "reload reuses the fixture base"); + MetadataProtected(first); + const int second = Open(path, nullptr, false); + Counts(second, 1, 1); + Close(first); + Require(GetModuleHandleW(file.c_str()) != nullptr, "second reference retains the module"); + Counts(second, 1, 1); + Close(second); + Require(GetModuleHandleW(file.c_str()) == nullptr, "last close unloads the module"); + } + } else if (mode == "pinned") { + HMODULE pin = LoadLibraryW(file.c_str()); + Require(pin != nullptr, "native pin"); + const int first = Open(path); + Counts(first, 1, 1); + Close(first); + const int second = Open(path, nullptr, false); + Counts(second, 1, 1); + MetadataProtected(second); + Close(second); + Require(FreeLibrary(pin) && GetModuleHandleW(file.c_str()) == nullptr, "release native pin"); + const int reloaded = Open(path); + Counts(reloaded, 1, 1); + Close(reloaded); + } else if (mode == "parallel") { + GuestDeferredModuleArgs args; + args.Mode = 1; + args.Entered = CreateEventW(nullptr, TRUE, FALSE, nullptr); + args.Release = CreateEventW(nullptr, TRUE, FALSE, nullptr); + Require(args.Entered && args.Release, "create initializer gates"); + auto first = std::async(std::launch::async, [&] { return Open(path, &args); }); + Require(WaitForSingleObject(args.Entered, 3000) == WAIT_OBJECT_0, "first initializer entered"); + std::promise attempting; + auto started = attempting.get_future(); + auto second = std::async(std::launch::async, [&] { + attempting.set_value(); + return Open(path, nullptr, false); + }); + started.wait(); + const bool returnedEarly = second.wait_for(std::chrono::milliseconds(200)) == std::future_status::ready; + Require(SetEvent(args.Release), "release initializer"); + const int one = first.get(); + const int two = second.get(); + Require(!returnedEarly, "concurrent load waits for initialization"); + Counts(one, 1, 1); + Counts(two, 1, 1); + MetadataProtected(one); + Close(one); + Close(two); + CloseHandle(args.Entered); + CloseHandle(args.Release); + } else if (mode == "reentrant") { + GuestDeferredModuleArgs args; + args.Mode = 2; + args.Reenter = Reenter; + args.Path = path.c_str(); + const int id = Open(path, &args); + Counts(id, 1, 1); + MetadataProtected(id); + Close(id); + } else Require(false, "unknown fixture mode"); +} diff --git a/core/libs/tests/GuestDeferredModule.hpp b/core/libs/tests/GuestDeferredModule.hpp new file mode 100644 index 0000000000..2b1256aac9 --- /dev/null +++ b/core/libs/tests/GuestDeferredModule.hpp @@ -0,0 +1,18 @@ +#ifndef CORE_LIBS_TESTS_GUESTDEFERREDMODULE_HPP +#define CORE_LIBS_TESTS_GUESTDEFERREDMODULE_HPP + +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +struct GuestDeferredModuleArgs { + unsigned Mode = 0; + HANDLE Entered = nullptr; + HANDLE Release = nullptr; + void (APS5_VABI* Reenter)(const char*) = nullptr; + const char* Path = nullptr; +}; + +inline constexpr int GuestDeferredModuleResult = 0x5a17; + +#endif diff --git a/core/libs/tests/GuestDeferredModuleFixture.S b/core/libs/tests/GuestDeferredModuleFixture.S new file mode 100644 index 0000000000..c2b27310af --- /dev/null +++ b/core/libs/tests/GuestDeferredModuleFixture.S @@ -0,0 +1,7 @@ +.section .ginit,"dr" +.p2align 3 +.globl __aps5_guest_initialize +__aps5_guest_initialize: +.rva DeferredStart_nid_no_patch +.long 1 +.rva DeferredInitializerSlot_nid_no_patch diff --git a/core/libs/tests/GuestDeferredModuleFixture.cpp b/core/libs/tests/GuestDeferredModuleFixture.cpp new file mode 100644 index 0000000000..8091cc4fc8 --- /dev/null +++ b/core/libs/tests/GuestDeferredModuleFixture.cpp @@ -0,0 +1,32 @@ +#include "GuestDeferredModule.hpp" +#include +#include +#include + +static std::atomic starts{0}; +static std::atomic constructors{0}; + +extern "C" int APS5_VABI DeferredStart_nid_no_patch(std::size_t bytes, const void* argument, void*) { + ++starts; + if (bytes != 0) { + if (bytes != sizeof(GuestDeferredModuleArgs) || !argument) std::abort(); + const auto& args = *static_cast(argument); + if (args.Mode == 1) { + if (!SetEvent(args.Entered) || WaitForSingleObject(args.Release, 10000) != WAIT_OBJECT_0) + std::abort(); + } else if (args.Mode == 2) args.Reenter(args.Path); + } + return GuestDeferredModuleResult; +} + +extern "C" void APS5_VABI DeferredConstructor_nid_no_patch(int, char**, char**) { + ++constructors; +} + +using Initializer = void (APS5_VABI*)(int, char**, char**); +extern "C" { +Initializer DeferredInitializerSlot_nid_no_patch = DeferredConstructor_nid_no_patch; +} + +extern "C" int APS5_VABI DeferredStarts_nid_no_patch() { return starts.load(); } +extern "C" int APS5_VABI DeferredConstructors_nid_no_patch() { return constructors.load(); } 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/GuestDirectory.cpp b/core/libs/tests/GuestDirectory.cpp index 6d3288f0d8..4df4394622 100644 --- a/core/libs/tests/GuestDirectory.cpp +++ b/core/libs/tests/GuestDirectory.cpp @@ -1,37 +1,106 @@ #include "prx/libc/include/GuestDirectory.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" #include +#include +#include #include #include #include #include #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); @@ -39,13 +108,100 @@ int main() { std::size_t count = 0; while (readdir_nid_postfix(directory)) ++count; Require(count == entries.size()); + rewinddir_nid_postfix(directory); + std::map copies; + GuestDirectoryEntry buffer; + GuestDirectoryEntry* result = nullptr; + *__error_nid_postfix() = 13; + for (;;) { + std::memset(&buffer, 0x7f, sizeof(buffer)); + Require(readdir_r_nid_postfix(directory, &buffer, &result) == 0); + if (!result) break; + Require(result == &buffer); + Require(buffer.nameLength == std::strlen(buffer.name)); + Require(buffer.recordLength == 8 + ((buffer.nameLength + 4) & ~3)); + for (std::size_t i = buffer.recordLength; i < sizeof(buffer); ++i) + Require(reinterpret_cast(&buffer)[i] == 0x7f); + copies[buffer.name] = buffer.type; + } + Require(copies == entries); + Require(*__error_nid_postfix() == 13); + result = &buffer; + Require(readdir_r_nid_postfix(directory, &buffer, &result) == 0 && result == nullptr); + rewinddir_nid_postfix(directory); + *__error_nid_postfix() = 13; + auto* first = readdir_nid_postfix(directory); + Require(first != nullptr); + const std::string firstName = first->name; + Require(readdir_r_nid_postfix(directory, &buffer, &result) == 0 && result == &buffer); + Require(firstName != buffer.name && entries.count(buffer.name) == 1); + result = &buffer; + Require(readdir_r_nid_postfix(nullptr, &buffer, &result) == 9); + Require(result == &buffer && *__error_nid_postfix() == 13); + bool nullEntryThrows = false; + try { + readdir_r_nid_postfix(directory, nullptr, &result); + } catch (const std::invalid_argument&) { + nullEntryThrows = true; + } + Require(nullEntryThrows); + bool nullResultThrows = false; + try { + readdir_r_nid_postfix(directory, &buffer, nullptr); + } catch (const std::invalid_argument&) { + nullResultThrows = true; + } + Require(nullResultThrows); + 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); +#ifndef _WIN32 + const auto symlinkLoop = root / "symlink-loop"; + std::filesystem::create_symlink("symlink-loop", symlinkLoop); + Require(opendir_nid_postfix(symlinkLoop.string().c_str()) == nullptr); + Require(*__error_nid_postfix() == 62); + Require(std::filesystem::remove(symlinkLoop)); +#endif Require(opendir_nid_postfix((root / "sample.txt").string().c_str()) == nullptr); Require(*__error_nid_postfix() == 20); Require(opendir_nid_postfix("") == nullptr && *__error_nid_postfix() == 2); Require(closedir_nid_postfix(nullptr) == -1 && *__error_nid_postfix() == 9); + const std::u8string folder = u8"セーブ"; + const std::u8string file = u8"été.txt"; + Require(std::filesystem::create_directory(root / std::filesystem::path(folder))); + { std::ofstream stream(root / std::filesystem::path(folder) / std::filesystem::path(file)); stream << "x"; } + const std::string guestFolder = root.string() + "/" + std::string(folder.begin(), folder.end()); + void* unicode = opendir_nid_postfix(guestFolder.c_str()); + Require(unicode != nullptr); + std::map names; + while (auto* entry = readdir_nid_postfix(unicode)) { + Require(entry->nameLength == std::strlen(entry->name)); + names[entry->name] = entry->type; + } + 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 9f2bf60d08..e5d0c6c122 100644 --- a/core/libs/tests/GuestDirectoryBuffer.cpp +++ b/core/libs/tests/GuestDirectoryBuffer.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include extern "C" { @@ -17,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) { @@ -27,15 +33,15 @@ static void Check(bool value, int line) { } #define Require(value) Check((value), __LINE__) -int main() { +static void CheckName(const std::u8string& utf8Name) { const auto root = std::filesystem::temp_directory_path() / ("anyps5-directory-buffer-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); Require(std::filesystem::create_directory(root)); - const std::string name = "a-long-directory-entry-name.bin"; - { std::ofstream file(root / name); Require(static_cast(file)); } + const std::string name(utf8Name.begin(), utf8Name.end()); + { std::ofstream file(root / std::filesystem::path(utf8Name)); Require(static_cast(file)); } const int directory = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); Require(directory >= 0); - std::array buffer; + std::array buffer; buffer.fill('x'); std::int64_t base = -1; Require(sceKernelGetdirentries(directory, buffer.data(), 8, &base) == SCE_KERNEL_ERROR_EINVAL); @@ -54,9 +60,81 @@ int main() { const int recordSize = static_cast((8 + name.size() + 1 + 3) & ~std::size_t{3}); Require(sceKernelGetdirentries(directory, buffer.data(), recordSize, &base) == recordSize); Require(std::strcmp(buffer.data() + 8, name.c_str()) == 0); + Require(static_cast(buffer[7]) == name.size()); + std::uint16_t storedSize = 0; + std::memcpy(&storedSize, buffer.data() + 4, sizeof(storedSize)); + Require(storedSize == recordSize); + const auto rootUtf8 = root.u8string(); + const std::string enumeratedPath = std::string(rootUtf8.begin(), rootUtf8.end()) + '/' + (buffer.data() + 8); + const int file = sceKernelOpen(enumeratedPath.c_str(), SCE_KERNEL_O_RDONLY, 0); + Require(file >= 0 && sceKernelClose(file) == 0); 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); } + +static void CheckOversizedName() { + const auto root = std::filesystem::temp_directory_path() / + ("anyps5-directory-name-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + std::u8string name; + for (int i = 0; i < 86; ++i) name += u8"\u30bb"; + { std::ofstream file(root / std::filesystem::path(name)); Require(static_cast(file)); } + const int directory = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(directory >= 0); + std::array buffer; + bool threw = false; + try { + sceKernelGetdents(directory, buffer.data(), static_cast(buffer.size())); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + Require(sceKernelClose(directory) == 0); + 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"); + CheckName(u8"\u30bb\u30fc\u30d6-music.bin"); + CheckName(u8"\U0001f3ae-music.bin"); + std::u8string longest; + 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/GuestDirectoryRewind.cpp b/core/libs/tests/GuestDirectoryRewind.cpp new file mode 100644 index 0000000000..00217b646e --- /dev/null +++ b/core/libs/tests/GuestDirectoryRewind.cpp @@ -0,0 +1,156 @@ +#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 +#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); +std::int64_t APS5_VABI sceKernelLseek(int, std::int64_t, int); +int APS5_VABI dup_nid_postfix(int); +int APS5_VABI dup2_nid_postfix(int, int); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Directory rewind check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + + +static std::set ReadAll(int directory) { + std::set names; + std::array buffer{}; + for (;;) { + const int bytes = sceKernelGetdents(directory, buffer.data(), static_cast(buffer.size())); + Require(bytes >= 0); + if (bytes == 0) return names; + for (int offset = 0; offset < bytes;) { + names.emplace(buffer.data() + offset + 8); + offset += static_cast(buffer[offset + 4]) | (static_cast(buffer[offset + 5]) << 8); + } + } +} + +int main() { + const auto root = std::filesystem::temp_directory_path() / + ("anyps5-directory-rewind-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + { std::ofstream file(root / "a"); Require(static_cast(file)); } + { std::ofstream file(root / "b"); Require(static_cast(file)); } + + const int directory = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(directory >= 0); + const std::set expected{".", "..", "a", "b"}; + Require(ReadAll(directory) == expected); + Require(ReadAll(directory).empty()); + + Require(sceKernelLseek(directory, 0, 0) == 0); + Require(ReadAll(directory) == expected); + + { std::ofstream file(root / "c"); Require(static_cast(file)); } + Require(sceKernelLseek(directory, 0, 0) == 0); + Require(ReadAll(directory) == (std::set{".", "..", "a", "b", "c"})); + + std::array two{}; + Require(sceKernelLseek(directory, 0, 0) == 0); + Require(sceKernelGetdents(directory, two.data(), static_cast(two.size())) == 24); + const auto position = sceKernelLseek(directory, 0, 1); + Require(position == 2); + Require(sceKernelLseek(directory, 0, 1) == 2); + const auto rest = ReadAll(directory); + Require(sceKernelLseek(directory, position, 0) == position); + Require(ReadAll(directory) == rest); + Require(!rest.contains(".") && !rest.contains("..")); + + Require(sceKernelLseek(directory, 100, 0) == 100); + Require(sceKernelGetdents(directory, two.data(), static_cast(two.size())) == 0); + Require(sceKernelLseek(directory, 0, 1) == 100); + 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()); + + Require(sceKernelLseek(directory, 0, 0) == 0); + Require(sceKernelGetdents(directory, two.data(), static_cast(two.size())) == 24); + Require(sceKernelLseek(directory, 0, 1) == 2); + const auto afterQuery = ReadAll(directory); + Require(afterQuery == rest); + + std::array tiny{}; + Require(sceKernelLseek(directory, 0, 0) == 0); + Require(sceKernelGetdents(directory, tiny.data(), static_cast(tiny.size())) < 0); + { std::ofstream file(root / "d"); Require(static_cast(file)); } + Require(sceKernelLseek(directory, 0, 1) == 0); + Require(ReadAll(directory) == (std::set{".", "..", "a", "b", "c"})); + Require(sceKernelLseek(directory, 0, 0) == 0); + Require(sceKernelGetdents(directory, tiny.data(), static_cast(tiny.size())) < 0); + Require(sceKernelLseek(directory, 0, 0) == 0); + Require(ReadAll(directory) == (std::set{".", "..", "a", "b", "c", "d"})); + + Require(sceKernelLseek(directory, 0, 0) == 0); + Require(sceKernelGetdents(directory, two.data(), static_cast(two.size())) == 24); + const int duplicate = dup_nid_postfix(directory); + Require(duplicate >= 0 && duplicate != directory); + Require(sceKernelLseek(duplicate, 0, 1) == 2); + Require(ReadAll(duplicate) == (std::set{"a", "b", "c", "d"})); + Require(sceKernelLseek(directory, 0, 1) == sceKernelLseek(duplicate, 0, 1)); + Require(sceKernelLseek(directory, 0, 0) == 0); + Require(sceKernelLseek(duplicate, 0, 1) == 0); + const int target = sceKernelOpen((root / "a").string().c_str(), SCE_KERNEL_O_RDONLY, 0); + Require(target >= 0); + Require(dup2_nid_postfix(duplicate, target) == target); + Require(sceKernelClose(duplicate) == 0); + Require(ReadAll(target) == (std::set{".", "..", "a", "b", "c", "d"})); + Require(sceKernelLseek(directory, 0, 1) == sceKernelLseek(target, 0, 1)); + Require(sceKernelClose(target) == 0); + + Require(sceKernelClose(directory) == 0); + + const int reused = sceKernelOpen((root / "a").string().c_str(), SCE_KERNEL_O_RDWR, 0); + Require(reused == directory); + Require(sceKernelLseek(reused, 5, 0) == 5); + Require(sceKernelLseek(reused, 0, 1) == 5); + Require(sceKernelLseek(reused, 0, 2) == 0); + Require(sceKernelClose(reused) == 0); + std::filesystem::remove_all(root); +} diff --git a/core/libs/tests/GuestDiscMap.cpp b/core/libs/tests/GuestDiscMap.cpp new file mode 100644 index 0000000000..41f711baff --- /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, void* flags, void* out1, void* 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 e588f786da..1c3abe8c03 100644 --- a/core/libs/tests/GuestDownloadDataMount.cpp +++ b/core/libs/tests/GuestDownloadDataMount.cpp @@ -1,16 +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" && 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 { @@ -21,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 new file mode 100644 index 0000000000..4e0451ae85 --- /dev/null +++ b/core/libs/tests/GuestDup.cpp @@ -0,0 +1,223 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +extern "C" { +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 close_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 socketpair_nid_postfix(int, int, int, int*); +std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); +std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); +int APS5_VABI open_nid_postfix(const char*, int, int); +int APS5_VABI fcntl_nid_postfix(int, int, ...); +int* APS5_VABI __error_nid_postfix(); +} + +static void Require(bool value) { if (!value) std::abort(); } + +constexpr int GuestEbadf = 9; +constexpr int Unix = 1; +constexpr int Stream = 1; +constexpr int DescriptorSetSize = 1024; +constexpr int Unopened = 0x1ffffff0; + +static void RequireFailure(int result, int error) { + Require(result == -1); + Require(*__error_nid_postfix() == error); +} + +static void RequirePipeCarries(int writer, int reader, const char* text) { + const auto length = static_cast(std::strlen(text)); + Require(write_nid_postfix(writer, text, length) == length); + char buffer[16] = {}; + Require(read_nid_postfix(reader, buffer, sizeof(buffer)) == length); + Require(std::memcmp(buffer, text, static_cast(length)) == 0); +} + +static void RequireSocketCarries(int writer, int reader, const char* text) { + const auto length = std::strlen(text); + Require(send_nid_postfix(writer, text, length, 0) == static_cast(length)); + char buffer[16] = {}; + Require(recv_nid_postfix(reader, buffer, sizeof(buffer), 0) == static_cast(length)); + Require(std::memcmp(buffer, text, length) == 0); +} + +template static bool Throws(TCall call) { + try { + call(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +static void DupOutlivesOriginal() { + int pipe[2]; + Require(pipe_nid_postfix(pipe) == 0); + const int duplicate = dup_nid_postfix(pipe[1]); + Require(duplicate >= 0 && duplicate != pipe[1]); + Require(close_nid_postfix(pipe[1]) == 0); + RequirePipeCarries(duplicate, pipe[0], "dup"); + Require(close_nid_postfix(duplicate) == 0); + char end = 0; + Require(read_nid_postfix(pipe[0], &end, 1) == 0); + Require(close_nid_postfix(pipe[0]) == 0); +} + +static void Dup2ReplacesTarget() { + int first[2]; + int second[2]; + Require(pipe_nid_postfix(first) == 0); + Require(pipe_nid_postfix(second) == 0); + Require(dup2_nid_postfix(first[1], second[1]) == second[1]); + RequirePipeCarries(second[1], first[0], "dup2"); + char end = 0; + Require(read_nid_postfix(second[0], &end, 1) == 0); + Require(dup2_nid_postfix(first[1], first[1]) == first[1]); + for (const int descriptor : {first[0], first[1], second[0], second[1]}) Require(close_nid_postfix(descriptor) == 0); +} + +#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 BadDescriptors() { + int pipe[2]; + Require(pipe_nid_postfix(pipe) == 0); + const int closed = pipe[1]; + Require(close_nid_postfix(closed) == 0); + RequireFailure(dup_nid_postfix(-1), GuestEbadf); + RequireFailure(dup_nid_postfix(closed), GuestEbadf); + RequireFailure(dup2_nid_postfix(closed, pipe[0]), GuestEbadf); + RequireFailure(dup2_nid_postfix(closed, closed), GuestEbadf); + RequireFailure(dup2_nid_postfix(pipe[0], -1), GuestEbadf); + RequireFailure(dup2_nid_postfix(-1, pipe[0]), GuestEbadf); + RequireFailure(dup_nid_postfix(Unopened), GuestEbadf); + RequireFailure(dup2_nid_postfix(Unopened, Unopened + 1), GuestEbadf); +#ifdef _WIN32 + const auto previous = _set_invalid_parameter_handler(RecordInvalidParameter); +#endif + RequireFailure(close_nid_postfix(closed), GuestEbadf); + RequireFailure(close_nid_postfix(-1), GuestEbadf); +#ifdef _WIN32 + _set_invalid_parameter_handler(previous); + Require(!g_invalidParameter); +#endif + Require(close_nid_postfix(pipe[0]) == 0); +} + +static void SocketDuplicates() { + int pair[2]; + Require(socketpair_nid_postfix(Unix, Stream, 0, pair) == 0); + const int duplicate = dup_nid_postfix(pair[0]); + Require(duplicate >= 0 && duplicate < DescriptorSetSize && duplicate != pair[0] && duplicate != pair[1]); + Require(close_nid_postfix(pair[0]) == 0); + RequireSocketCarries(duplicate, pair[1], "socket"); + int pipe[2]; + Require(pipe_nid_postfix(pipe) == 0); + const int target = pipe[0]; + Require(dup2_nid_postfix(duplicate, target) == target); + Require(close_nid_postfix(duplicate) == 0); + RequireSocketCarries(target, pair[1], "moved"); + Require(dup2_nid_postfix(target, target) == target); + int next[2]; + Require(socketpair_nid_postfix(Unix, Stream, 0, next) == 0); + Require(next[0] < DescriptorSetSize && next[1] < DescriptorSetSize); + RequireSocketCarries(target, pair[1], "kept"); + Require(dup2_nid_postfix(next[0], target) == target); + RequireSocketCarries(target, next[1], "over"); + Require(Throws([&] { dup2_nid_postfix(target, 1); })); + int other[2]; + Require(pipe_nid_postfix(other) == 0); + Require(dup2_nid_postfix(other[1], target) == target); + RequirePipeCarries(target, other[0], "file"); + Require(send_nid_postfix(target, "x", 1, 0) == -1); + for (const int descriptor : {target, pair[1], next[0], next[1], pipe[1], other[0], other[1]}) Require(close_nid_postfix(descriptor) == 0); +} + +static void RandomDeviceDuplicates() { + const int random = open_nid_postfix("/dev/urandom", 0, 0); + Require(random >= 0); + const int dupRandom = dup_nid_postfix(random); + Require(dupRandom >= 0 && dupRandom != random); + unsigned char randomBuf1[16] = {}; + unsigned char randomBuf2[16] = {}; + Require(read_nid_postfix(random, randomBuf1, sizeof(randomBuf1)) == sizeof(randomBuf1)); + Require(read_nid_postfix(dupRandom, randomBuf2, sizeof(randomBuf2)) == sizeof(randomBuf2)); + Require(std::memcmp(randomBuf1, randomBuf2, sizeof(randomBuf1)) != 0); + + Require(dup2_nid_postfix(random, random) == random); + Require(read_nid_postfix(random, randomBuf1, sizeof(randomBuf1)) == sizeof(randomBuf1)); + + int pipe[2]; + Require(pipe_nid_postfix(pipe) == 0); + Require(write_nid_postfix(pipe[1], "hello", 5) == 5); + Require(dup2_nid_postfix(pipe[0], random) == random); + char textBuf[8] = {}; + Require(read_nid_postfix(random, textBuf, 5) == 5); + Require(std::memcmp(textBuf, "hello", 5) == 0); + + const int targetHost = pipe[0]; + Require(dup2_nid_postfix(dupRandom, targetHost) == targetHost); + unsigned char randomBuf3[16] = {}; + Require(read_nid_postfix(targetHost, randomBuf3, sizeof(randomBuf3)) == sizeof(randomBuf3)); + Require(std::memcmp(randomBuf3, randomBuf2, sizeof(randomBuf3)) != 0); + + const int secondRandom = open_nid_postfix("/dev/random", 0, 0); + Require(secondRandom >= 0); + Require(dup2_nid_postfix(dupRandom, secondRandom) == secondRandom); + unsigned char randomBuf4[16] = {}; + Require(read_nid_postfix(secondRandom, randomBuf4, sizeof(randomBuf4)) == sizeof(randomBuf4)); + Require(std::memcmp(randomBuf4, randomBuf3, sizeof(randomBuf4)) != 0); + + const int invalidFd = Unopened - 100; + RequireFailure(dup2_nid_postfix(invalidFd, secondRandom), GuestEbadf); + unsigned char randomBuf5[16] = {}; + Require(read_nid_postfix(secondRandom, randomBuf5, sizeof(randomBuf5)) == sizeof(randomBuf5)); + + for (const int descriptor : {random, dupRandom, pipe[1], secondRandom}) Require(close_nid_postfix(descriptor) == 0); +} + +static void DuplicatesShareStatusFlags() { + constexpr int GetDescriptorFlags = 1; + constexpr int SetDescriptorFlags = 2; + constexpr int GetFlags = 3; + constexpr int SetFlags = 4; + constexpr int NonBlocking = 4; + constexpr int GuestEagain = 35; + int pipe[2]; + int other[2]; + Require(pipe_nid_postfix(pipe) == 0); + Require(pipe_nid_postfix(other) == 0); + Require(fcntl_nid_postfix(pipe[0], SetDescriptorFlags, 1) == 0); + const int duplicate = dup_nid_postfix(pipe[0]); + Require(duplicate >= 0); + Require(fcntl_nid_postfix(duplicate, GetDescriptorFlags) == 0); + Require(fcntl_nid_postfix(duplicate, SetFlags, NonBlocking) == 0); + Require(fcntl_nid_postfix(pipe[0], GetFlags) == NonBlocking); + Require(dup2_nid_postfix(pipe[0], other[0]) == other[0]); + Require(fcntl_nid_postfix(other[0], GetDescriptorFlags) == 0); + Require(fcntl_nid_postfix(other[0], GetFlags) == NonBlocking); + char buffer[4] = {}; + RequireFailure(static_cast(read_nid_postfix(other[0], buffer, sizeof(buffer))), GuestEagain); + Require(fcntl_nid_postfix(pipe[0], SetFlags, 0) == 0); + Require(fcntl_nid_postfix(duplicate, GetFlags) == 0); + Require(fcntl_nid_postfix(pipe[0], GetDescriptorFlags) == 1); + for (const int descriptor : {pipe[0], pipe[1], other[0], other[1], duplicate}) Require(close_nid_postfix(descriptor) == 0); +} + +int main() { + DupOutlivesOriginal(); + Dup2ReplacesTarget(); + BadDescriptors(); + SocketDuplicates(); + RandomDeviceDuplicates(); + DuplicatesShareStatusFlags(); +} diff --git a/core/libs/tests/GuestDynamicCast.cpp b/core/libs/tests/GuestDynamicCast.cpp new file mode 100644 index 0000000000..c7892a5e96 --- /dev/null +++ b/core/libs/tests/GuestDynamicCast.cpp @@ -0,0 +1,45 @@ +#include "prx/libc/include/exceptions/Runtime.hpp" +#include +#include +#include +#include + +struct TypeRecord { const void* vtable; const char* name; const TypeRecord* base; }; +struct Vtable { std::intptr_t offsetToTop; const TypeRecord* type; const void* first; }; +struct Object { const void* vtable; }; + +extern "C" { +extern const unsigned char _ZTVN10__cxxabiv117__class_type_infoE_nid_postfix[]; +extern const unsigned char _ZTVN10__cxxabiv120__si_class_type_infoE_nid_postfix[]; +void* APS5_VABI __dynamic_cast_nid_postfix(const void*, const void*, const void*, std::ptrdiff_t); +LibcException::Header* APS5_VABI __cxa_init_primary_exception_nid_postfix(void*, std::type_info*, void (*)(void*)); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +void Destroy(void*) {} + +const TypeRecord BaseType{_ZTVN10__cxxabiv117__class_type_infoE_nid_postfix + 16, "4Base", nullptr}; +const TypeRecord DerivedType{_ZTVN10__cxxabiv120__si_class_type_infoE_nid_postfix + 16, "7Derived", &BaseType}; +const TypeRecord OtherType{_ZTVN10__cxxabiv120__si_class_type_infoE_nid_postfix + 16, "5Other", &BaseType}; +const Vtable BaseVtable{0, &BaseType, nullptr}; +const Vtable DerivedVtable{0, &DerivedType, nullptr}; + +} // namespace + +int main() { + const Object derived{&DerivedVtable.first}; + const Object base{&BaseVtable.first}; + Require(__dynamic_cast_nid_postfix(&derived, &BaseType, &DerivedType, 0) == &derived); + Require(__dynamic_cast_nid_postfix(&derived, &BaseType, &OtherType, 0) == nullptr); + Require(__dynamic_cast_nid_postfix(&base, &BaseType, &DerivedType, 0) == nullptr); + Require(__dynamic_cast_nid_postfix(nullptr, &BaseType, &DerivedType, 0) == nullptr); + void* object = __cxa_allocate_exception_nid_postfix(16); + auto* type = reinterpret_cast(const_cast(&DerivedType)); + LibcException::Header* header = __cxa_init_primary_exception_nid_postfix(object, type, Destroy); + Require(header == LibcException::FromObject(object)); + Require(header->type == type && header->destructor == Destroy && header->adjusted == object); + __cxa_free_exception_nid_postfix(object); +} diff --git a/core/libs/tests/GuestDynamicLoader.cpp b/core/libs/tests/GuestDynamicLoader.cpp index 74223730fb..8ea9fe7c93 100644 --- a/core/libs/tests/GuestDynamicLoader.cpp +++ b/core/libs/tests/GuestDynamicLoader.cpp @@ -1,24 +1,33 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include +#include +#include #include #include #ifndef _WIN32 #include #endif #include +#include #include extern "C" { +int APS5_VABI sceKernelDlsym(std::int32_t, const char*, void**); 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*); 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; + std::fprintf(stderr, "guest_dynamic_loader: failed at %s:%u\n", location.file_name(), location.line()); + std::abort(); } -static void Require(bool value) { if (!value) std::abort(); } -#ifndef _WIN32 extern "C" int APS5_VABI GuestDefaultScopeOnly(int a, int b) { return a * b; } -#endif template static bool ThrowsRuntimeError(TFunction function) { try { @@ -28,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); @@ -36,8 +109,22 @@ int main(int argc, char** argv) { Require(dlerror_nid_postfix() == nullptr); Require(dlopen_nid_postfix("anyps5-missing-module-for-test.prx", 2) == nullptr); Require(dlerror_nid_postfix() != nullptr); + void* mainSymbol = nullptr; + const int mainResult = sceKernelDlsym(0, "GuestDefaultScopeOnly", &mainSymbol); + std::fprintf(stderr, "sceKernelDlsym(0, GuestDefaultScopeOnly): result=0x%08x, symbol=%p\n", static_cast(mainResult), mainSymbol); + Require(mainResult == 0 && mainSymbol != nullptr); + using MainFunction = int (APS5_VABI *)(int, int); + Require(reinterpret_cast(mainSymbol)(6, 7) == 42); + Require(sceKernelDlsym(0, "anyps5_missing_main_symbol", &mainSymbol) == static_cast(0x80020003u)); + 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); @@ -81,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); @@ -108,7 +214,18 @@ 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 + std::puts("guest_dynamic_loader: all checks passed"); } 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/GuestEnvironment.cpp b/core/libs/tests/GuestEnvironment.cpp index df1910cf45..46e2b2b11b 100644 --- a/core/libs/tests/GuestEnvironment.cpp +++ b/core/libs/tests/GuestEnvironment.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include extern "C" { char* APS5_VABI getenv_nid_postfix(const char*); int APS5_VABI setenv_nid_postfix(const char*, const char*, int); @@ -12,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"; @@ -36,6 +44,13 @@ int main() { Require(std::strcmp(getenv_nid_postfix(key), "two") == 0); Require(setenv_nid_postfix("bad=name", "x", 1) == -1); Require(*__error_nid_postfix() == 22); + const char* const invalidNames[] = {nullptr, "", "bad=name"}; + for (const char* name : invalidNames) { + *__error_nid_postfix() = 0; + Require(getenv_nid_postfix(name) == nullptr && *__error_nid_postfix() == 22); + } + *__error_nid_postfix() = 34; + Require(getenv_nid_postfix("ANYPS5_GUEST_ENV_ABSENT_4C27") == nullptr && *__error_nid_postfix() == 34); Require(unsetenv_nid_postfix("") == -1); char invalid[] = "no-separator"; Require(putenv_nid_postfix(invalid) == -1); @@ -43,4 +58,11 @@ int main() { Require(getenv_nid_postfix(key) == nullptr); Require(unsetenv_nid_postfix(key) == 0); Require(unsetenv_nid_postfix("anyps5_guest_env_test_4c27") == 0); + Require(setenv_nid_postfix("Q7A", "a", 1) == 0); + Require(setenv_nid_postfix("Q7B", "b", 1) == 0); + const char* kept = getenv_nid_postfix("Q7B"); + Require(unsetenv_nid_postfix("Q7A") == 0); + for (int index = 0; index < 64; ++index) + Require(setenv_nid_postfix(("ANYPS5_GUEST_ENV_GROW_4C27_" + std::to_string(index)).c_str(), "x", 1) == 0); + Require(getenv_nid_postfix("Q7B") == kept && std::strcmp(kept, "b") == 0); } diff --git a/core/libs/tests/GuestEqueueEvents.cpp b/core/libs/tests/GuestEqueueEvents.cpp index b0f74d8ccd..8e97329913 100644 --- a/core/libs/tests/GuestEqueueEvents.cpp +++ b/core/libs/tests/GuestEqueueEvents.cpp @@ -10,12 +10,17 @@ 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 sceKernelAddUserEvent(KernelEqueue eq, int id); +int APS5_VABI sceKernelAddUserEventEdge(KernelEqueue eq, int id); int APS5_VABI sceKernelTriggerUserEvent(KernelEqueue eq, int id, void* udata); int APS5_VABI sceKernelDeleteUserEvent(KernelEqueue eq, int id); +int APS5_VABI sceKernelAddAmprEvent(KernelEqueue eq, int id, void* udata); +int APS5_VABI sceKernelDeleteAmprEvent(KernelEqueue eq, int id); +int APS5_VABI EqueueTriggerEvent_nid_postfix(KernelEqueue eq, uintptr_t ident, int16_t filter, void* triggerData); int APS5_VABI sceKernelAddHRTimerEvent(KernelEqueue eq, int id, const KernelTimespec* ts, void* udata); int APS5_VABI sceKernelAddTimerEvent(KernelEqueue eq, int id, KernelUseconds usec, void* udata); int APS5_VABI sceKernelDeleteTimerEvent(KernelEqueue eq, int id); intptr_t APS5_VABI sceKernelGetEventData(const KernelEvent* ev); +int APS5_VABI sceKernelGetEventError(const KernelEvent* ev); intptr_t APS5_VABI sceKernelGetEventFflags(const KernelEvent* ev); int APS5_VABI sceKernelGetEventFilter(const KernelEvent* ev); uintptr_t APS5_VABI sceKernelGetEventId(const KernelEvent* ev); @@ -29,6 +34,8 @@ static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = static_cast(0x8002003c); static constexpr int EVFILT_TIMER = -7; static constexpr int EVFILT_USER = -11; static constexpr int EVFILT_HRTIMER = -15; +static constexpr int EVFILT_AMPR = -25; +static constexpr std::uint16_t EV_ERROR = 0x4000; static void Require(bool value) { if (!value) std::abort(); } @@ -72,6 +79,7 @@ static void VerifyPeriodicTimer() { Require(sceKernelGetEventFilter(&events[0]) == EVFILT_TIMER); Require(sceKernelGetEventData(&events[0]) >= 5); Require(sceKernelGetEventUserData(&events[0]) == &first); + Require(sceKernelGetEventError(&events[0]) == 0); Require(sceKernelWaitEqueue(eq, events, 2, &count, &wait) == SCE_OK); Require(count == 1 && sceKernelGetEventData(&events[0]) >= 1); @@ -124,8 +132,126 @@ static void VerifyPeriodicTimer() { Require(sceKernelDeleteEqueue(eq) == SCE_OK); } +static void VerifyUserEventReportedOncePerWait() { + KernelEqueue eq = 0; + Require(sceKernelCreateEqueue(&eq, "user") == SCE_OK); + KernelEvent events[4]{}; + int count = 0; + const KernelUseconds poll = 0; + int payload = 0; + + Require(sceKernelAddUserEvent(eq, 5) == SCE_OK); + Require(sceKernelWaitEqueue(eq, events, 4, &count, &poll) == SCE_KERNEL_ERROR_ETIMEDOUT); + Require(sceKernelTriggerUserEvent(eq, 5, &payload) == SCE_OK); + for (int i = 0; i < 2; ++i) { + count = 0; + Require(sceKernelWaitEqueue(eq, events, 4, &count, &poll) == SCE_OK); + Require(count == 1 && sceKernelGetEventId(&events[0]) == 5); + } + Require(sceKernelDeleteUserEvent(eq, 5) == SCE_OK); + + Require(sceKernelAddUserEventEdge(eq, 6) == SCE_OK); + Require(sceKernelTriggerUserEvent(eq, 6, &payload) == SCE_OK); + Require(sceKernelAddUserEvent(eq, 8) == SCE_OK); + Require(sceKernelTriggerUserEvent(eq, 8, &payload) == SCE_OK); + Require(sceKernelWaitEqueue(eq, events, 4, &count, &poll) == SCE_OK); + Require(count == 2); + Require(sceKernelGetEventId(&events[0]) + sceKernelGetEventId(&events[1]) == 14); + Require(sceKernelGetEventId(&events[0]) != sceKernelGetEventId(&events[1])); + Require(sceKernelWaitEqueue(eq, events, 4, &count, &poll) == SCE_OK); + Require(count == 1 && sceKernelGetEventId(&events[0]) == 8); + Require(sceKernelDeleteEqueue(eq) == SCE_OK); +} + +static void VerifyBlockingWaitReportsLevelEventOnce() { + KernelEqueue eq = 0; + Require(sceKernelCreateEqueue(&eq, "blocking") == SCE_OK); + KernelEvent events[4]{}; + int count = 0; + const KernelUseconds wait = 1000000; + int payload = 0; + + Require(sceKernelAddUserEvent(eq, 5) == SCE_OK); + Require(sceKernelTriggerUserEvent(eq, 5, &payload) == SCE_OK); + for (int i = 0; i < 2; ++i) { + count = 0; + Require(sceKernelWaitEqueue(eq, events, 4, &count, &wait) == SCE_OK); + Require(count == 1 && sceKernelGetEventId(&events[0]) == 5); + Require(sceKernelGetEventUserData(&events[0]) == &payload); + } + Require(sceKernelDeleteUserEvent(eq, 5) == SCE_OK); + + Require(sceKernelAddUserEvent(eq, 6) == SCE_OK); + std::thread trigger([eq, &payload] { + SleepAtLeast(std::chrono::milliseconds(10)); + Require(sceKernelTriggerUserEvent(eq, 6, &payload) == SCE_OK); + }); + count = 0; + Require(sceKernelWaitEqueue(eq, events, 4, &count, &wait) == SCE_OK); + trigger.join(); + Require(count == 1 && sceKernelGetEventId(&events[0]) == 6); + Require(sceKernelDeleteEqueue(eq) == SCE_OK); +} + +static void VerifyEdgeUserEventReportedOnce() { + KernelEqueue eq = 0; + Require(sceKernelCreateEqueue(&eq, "edge") == SCE_OK); + KernelEvent events[4]{}; + int count = 0; + const KernelUseconds poll = 0; + const KernelUseconds wait = 1000000; + int payload = 0; + + Require(sceKernelAddUserEventEdge(eq, 6) == SCE_OK); + Require(sceKernelTriggerUserEvent(eq, 6, &payload) == SCE_OK); + Require(sceKernelWaitEqueue(eq, events, 4, &count, &wait) == SCE_OK); + Require(count == 1 && sceKernelGetEventId(&events[0]) == 6); + Require(sceKernelWaitEqueue(eq, events, 4, &count, &poll) == SCE_KERNEL_ERROR_ETIMEDOUT); + Require(sceKernelTriggerUserEvent(eq, 6, &payload) == SCE_OK); + Require(sceKernelWaitEqueue(eq, events, 4, &count, &poll) == SCE_OK); + Require(count == 1 && sceKernelGetEventId(&events[0]) == 6); + Require(sceKernelWaitEqueue(eq, events, 4, &count, &poll) == SCE_KERNEL_ERROR_ETIMEDOUT); + Require(sceKernelDeleteEqueue(eq) == SCE_OK); +} + +static void VerifyRepeatedAmprCompletionsAreNotLost() { + KernelEqueue eq = 0; + Require(sceKernelCreateEqueue(&eq, "ampr") == SCE_OK); + KernelEvent events[4]{}; + int count = 0; + const KernelUseconds poll = 0; + int udata = 0; + auto data = [](std::uintptr_t value) { return reinterpret_cast(value); }; + + Require(sceKernelAddAmprEvent(eq, 3, &udata) == SCE_OK); + Require(EqueueTriggerEvent_nid_postfix(eq, 3, EVFILT_AMPR, data(0x11)) == SCE_OK); + Require(EqueueTriggerEvent_nid_postfix(eq, 3, EVFILT_AMPR, data(0x22)) == SCE_OK); + Require(sceKernelWaitEqueue(eq, events, 4, &count, &poll) == SCE_OK); + Require(count == 2); + Require(sceKernelGetEventFilter(&events[0]) == EVFILT_AMPR && sceKernelGetEventFilter(&events[1]) == EVFILT_AMPR); + Require(sceKernelGetEventId(&events[0]) == 3 && sceKernelGetEventId(&events[1]) == 3); + Require(sceKernelGetEventData(&events[0]) == 0x11 && sceKernelGetEventData(&events[1]) == 0x22); + Require(sceKernelGetEventUserData(&events[0]) == &udata && sceKernelGetEventUserData(&events[1]) == &udata); + Require(sceKernelWaitEqueue(eq, events, 4, &count, &poll) == SCE_KERNEL_ERROR_ETIMEDOUT); + + Require(EqueueTriggerEvent_nid_postfix(eq, 3, EVFILT_AMPR, data(0x33)) == SCE_OK); + Require(EqueueTriggerEvent_nid_postfix(eq, 3, EVFILT_AMPR, data(0x44)) == SCE_OK); + Require(EqueueTriggerEvent_nid_postfix(eq, 3, EVFILT_AMPR, data(0x55)) == SCE_OK); + for (const intptr_t expected : {0x33, 0x44, 0x55}) { + Require(sceKernelWaitEqueue(eq, events, 1, &count, &poll) == SCE_OK); + Require(count == 1 && sceKernelGetEventData(&events[0]) == expected); + } + Require(sceKernelWaitEqueue(eq, events, 4, &count, &poll) == SCE_KERNEL_ERROR_ETIMEDOUT); + Require(sceKernelDeleteAmprEvent(eq, 3) == SCE_OK); + Require(sceKernelDeleteEqueue(eq) == SCE_OK); +} + int main() { VerifyPeriodicTimer(); + VerifyUserEventReportedOncePerWait(); + VerifyBlockingWaitReportsLevelEventOnce(); + VerifyEdgeUserEventReportedOnce(); + VerifyRepeatedAmprCompletionsAreNotLost(); KernelEqueue eq = 0; Require(sceKernelCreateEqueue(&eq, "events") == SCE_OK); @@ -165,8 +291,15 @@ int main() { Require(sceKernelGetEventFflags(&rawEvent) == static_cast(0x80000001LL)); Require(sceKernelGetEventData(&rawEvent) == -5); Require(sceKernelGetEventUserData(&rawEvent) == nullptr); + Require(sceKernelGetEventError(&rawEvent) == 0); + rawEvent.flags = static_cast(~EV_ERROR); + Require(sceKernelGetEventError(&rawEvent) == 0); + rawEvent.flags = EV_ERROR; + rawEvent.data = 22; + Require(sceKernelGetEventError(&rawEvent) == 22); Require(RejectsNull(sceKernelGetEventData)); + Require(RejectsNull(sceKernelGetEventError)); Require(RejectsNull(sceKernelGetEventFflags)); Require(RejectsNull(sceKernelGetEventFilter)); Require(RejectsNull(sceKernelGetEventId)); diff --git a/core/libs/tests/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..106819b6a8 100644 --- a/core/libs/tests/GuestErrorDialog.cpp +++ b/core/libs/tests/GuestErrorDialog.cpp @@ -34,11 +34,16 @@ struct ErrorDialogParam { int main() { ErrorDialogParam param{16, static_cast(0x80020001), 0, 0}; + Require(sceErrorDialogGetStatus() == 0); + Require(sceErrorDialogUpdateStatus() == 0); Require(sceErrorDialogTerminate() == kErrNotInitialized); + Require(sceErrorDialogClose() == kErrInvalidState); Require(sceErrorDialogOpen(¶m) == kErrInvalidState); Require(sceErrorDialogInitialize() == 0); Require(sceErrorDialogInitialize() == kErrAlreadyInitialized); + Require(sceErrorDialogGetStatus() == 1); + Require(sceErrorDialogUpdateStatus() == 1); Require(sceErrorDialogClose() == kErrInvalidState); @@ -60,4 +65,7 @@ int main() { Require(sceErrorDialogTerminate() == 0); Require(sceErrorDialogTerminate() == kErrNotInitialized); + Require(sceErrorDialogGetStatus() == 0); + Require(sceErrorDialogUpdateStatus() == 0); + Require(sceErrorDialogClose() == kErrInvalidState); } diff --git a/core/libs/tests/GuestErrors.cpp b/core/libs/tests/GuestErrors.cpp index 52160d5626..b66b25b00d 100644 --- a/core/libs/tests/GuestErrors.cpp +++ b/core/libs/tests/GuestErrors.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -9,6 +10,41 @@ int APS5_VABI strerror_r_nid_postfix(int, char*, std::size_t); int* APS5_VABI __error_nid_postfix(); } static void Require(bool value) { if (!value) std::abort(); } +static const char* const freebsd[97] = { + "No error: 0", "Operation not permitted", "No such file or directory", + "No such process", "Interrupted system call", "Input/output error", + "Device not configured", "Argument list too long", "Exec format error", + "Bad file descriptor", "No child processes", "Resource deadlock avoided", + "Cannot allocate memory", "Permission denied", "Bad address", + "Block device required", "Device busy", "File exists", + "Cross-device link", "Operation not supported by device", "Not a directory", + "Is a directory", "Invalid argument", "Too many open files in system", + "Too many open files", "Inappropriate ioctl for device", "Text file busy", + "File too large", "No space left on device", "Illegal seek", + "Read-only file system", "Too many links", "Broken pipe", + "Numerical argument out of domain", "Result too large", "Resource temporarily unavailable", + "Operation now in progress", "Operation already in progress", "Socket operation on non-socket", + "Destination address required", "Message too long", "Protocol wrong type for socket", + "Protocol not available", "Protocol not supported", "Socket type not supported", + "Operation not supported", "Protocol family not supported", "Address family not supported by protocol family", + "Address already in use", "Can't assign requested address", "Network is down", + "Network is unreachable", "Network dropped connection on reset", "Software caused connection abort", + "Connection reset by peer", "No buffer space available", "Socket is already connected", + "Socket is not connected", "Can't send after socket shutdown", "Too many references: can't splice", + "Operation timed out", "Connection refused", "Too many levels of symbolic links", + "File name too long", "Host is down", "No route to host", + "Directory not empty", "Too many processes", "Too many users", + "Disc quota exceeded", "Stale NFS file handle", "Too many levels of remote in path", + "RPC struct is bad", "RPC version wrong", "RPC prog. not avail", + "Program version wrong", "Bad procedure for program", "No locks available", + "Function not implemented", "Inappropriate file type or format", "Authentication error", + "Need authenticator", "Identifier removed", "No message of desired type", + "Value too large to be stored in data type", "Operation canceled", "Illegal byte sequence", + "Attribute not found", "Programming error", "Bad message", + "Multihop attempted", "Link has been severed", "Protocol error", + "Capabilities insufficient", "Not permitted in capability mode", "State not recoverable", + "Previous owner died" +}; int main() { *__error_nid_postfix() = 13; Require(std::strcmp(strerror_nid_postfix(35), "Resource temporarily unavailable") == 0); @@ -25,15 +61,32 @@ int main() { char buffer[128]; for (int error = 0; error <= 96; ++error) { Require(strerror_r_nid_postfix(error, buffer, sizeof(buffer)) == 0); - Require(buffer[0] && std::strstr(buffer, "Unknown") == nullptr); + Require(std::strcmp(buffer, freebsd[error]) == 0); + Require(std::strcmp(strerror_nid_postfix(error), freebsd[error]) == 0); } Require(strerror_r_nid_postfix(-1, buffer, sizeof(buffer)) == 22); - Require(std::strstr(buffer, "-1") != nullptr); + Require(std::strcmp(buffer, "Unknown error: -1") == 0); Require(*__error_nid_postfix() == 13); + struct UnknownError { + int error; + const char* text; + }; + for (const UnknownError& unknown : {UnknownError{97, "Unknown error: 97"}, UnknownError{INT_MAX, "Unknown error: 2147483647"}, + UnknownError{INT_MIN, "Unknown error: -2147483648"}}) { + Require(strerror_r_nid_postfix(unknown.error, buffer, sizeof(buffer)) == 22); + Require(std::strcmp(buffer, unknown.text) == 0); + Require(*__error_nid_postfix() == 13); + Require(std::strcmp(strerror_nid_postfix(unknown.error), unknown.text) == 0); + Require(*__error_nid_postfix() == 22); + *__error_nid_postfix() = 13; + } char sentinel[] = "xyz"; Require(strerror_r_nid_postfix(22, sentinel, 1) == 34); Require(sentinel[0] == 0 && sentinel[1] == 'y'); sentinel[0] = 'x'; Require(strerror_r_nid_postfix(22, sentinel, 0) == 34 && sentinel[0] == 'x'); Require(strerror_r_nid_postfix(22, nullptr, 0) == 34); + char truncated[4]; + Require(strerror_r_nid_postfix(97, truncated, sizeof(truncated)) == 22 && std::strcmp(truncated, "Unk") == 0); + Require(strerror_r_nid_postfix(-1, truncated, sizeof(truncated)) == 22 && std::strcmp(truncated, "Unk") == 0); } 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/GuestExecvp.cpp b/core/libs/tests/GuestExecvp.cpp new file mode 100644 index 0000000000..24044df2f7 --- /dev/null +++ b/core/libs/tests/GuestExecvp.cpp @@ -0,0 +1,29 @@ +#include "prx/libc/include/General.hpp" +#include +#include +#include +extern "C" { +int APS5_VABI execvp_nid_postfix(const char*, char* const*); +int APS5_VABI execv_nid_postfix(const char*, char* const*); +int APS5_VABI execve_nid_postfix(const char*, char* const*, char* const*); +} +template static bool Throws(TCall call, const std::string& message) { + try { + call(); + } catch (const std::runtime_error& error) { + return error.what() == message; + } + return false; +} +static void Require(bool value) { if (!value) std::abort(); } +int main() { + char name[] = "sh"; + char* arguments[] = {name, nullptr}; + char variable[] = "HOME=/"; + char* environment[] = {variable, nullptr}; + Require(Throws([&] { execvp_nid_postfix("sh", arguments); }, "execvp: executable replacement not implemented")); + Require(Throws([&] { execvp_nid_postfix("/bin/sh", arguments); }, "execvp: executable replacement not implemented")); + Require(Throws([&] { execvp_nid_postfix(nullptr, nullptr); }, "execvp: executable replacement not implemented")); + Require(Throws([&] { execv_nid_postfix("/bin/sh", arguments); }, "execv: executable replacement not implemented")); + Require(Throws([&] { execve_nid_postfix("/bin/sh", arguments, environment); }, "execve: executable replacement not implemented")); +} diff --git a/core/libs/tests/GuestExitOrder.cpp b/core/libs/tests/GuestExitOrder.cpp new file mode 100644 index 0000000000..889e6f0f26 --- /dev/null +++ b/core/libs/tests/GuestExitOrder.cpp @@ -0,0 +1,60 @@ +#include "prx/libc/include/Shutdown.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +extern "C" int APS5_VABI __cxa_atexit_nid_postfix(void (APS5_VABI *func)(void*), void* arg, void* dsoHandle); +extern "C" int APS5_VABI atexit_nid_postfix(void (APS5_VABI *func)()); + +namespace { + +int events[2]; + +void Record(char value) { + static_cast(!write(events[1], &value, 1)); +} + +void APS5_VABI GuestHandler(void*) { Record('G'); } + +void HostHandler() { Record('H'); } + +void APS5_VABI AtexitHandler() { Record('A'); } + +template +bool ExitOrder(Setup setup, const char* expected) { + if (pipe(events) != 0) return false; + const pid_t child = fork(); + if (child < 0) return false; + if (child == 0) { + close(events[0]); + setup(); + LibcExit_nid_no_patch(0); + } + close(events[1]); + char order[3] = {}; + ssize_t length = 0; + while (length < 2) { + const ssize_t count = read(events[0], order + length, 2 - length); + if (count <= 0) break; + length += count; + } + close(events[0]); + int status = 0; + return waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0 && length == 2 && order[0] == expected[0] && order[1] == expected[1]; +} + +} + +int main() { + if (!ExitOrder([] { __cxa_atexit_nid_postfix(GuestHandler, nullptr, nullptr); std::atexit(HostHandler); }, "GH")) { + std::fputs("guest exit handlers must run before the host exit handlers\n", stderr); + return 1; + } + if (!ExitOrder([] { __cxa_atexit_nid_postfix(GuestHandler, nullptr, nullptr); atexit_nid_postfix(AtexitHandler); }, "AG")) { + std::fputs("atexit and __cxa_atexit handlers must run in reverse order of registration\n", stderr); + return 1; + } + return 0; +} diff --git a/core/libs/tests/GuestFdopendir.cpp b/core/libs/tests/GuestFdopendir.cpp new file mode 100644 index 0000000000..551ad063f0 --- /dev/null +++ b/core/libs/tests/GuestFdopendir.cpp @@ -0,0 +1,66 @@ +#include "prx/libc/include/GuestDirectory.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/File.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +extern "C" { +void* APS5_VABI fdopendir_nid_postfix(int); +GuestDirectoryEntry* APS5_VABI readdir_nid_postfix(void*); +int APS5_VABI closedir_nid_postfix(void*); +int APS5_VABI open_nid_postfix(const char*, int, int); +int APS5_VABI close_nid_postfix(int); +int APS5_VABI 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(); } +int main() { + const auto root = std::filesystem::path("anyps5-fdopendir-test-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + Require(std::filesystem::create_directory(root / "subdirectory")); + { std::ofstream file(root / "sample.txt"); file << "test"; } + const int fd = open_nid_postfix(root.string().c_str(), 0x20000, 0); + Require(fd >= 0); + 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)) { + entries[entry->name] = entry->type; + } + Require(entries.at("sample.txt") == 8); + Require(entries.at("subdirectory") == 4); + Require(DescriptorIsOpen_nid_no_patch(fd)); + Require(closedir_nid_postfix(directory) == 0); + Require(!DescriptorIsOpen_nid_no_patch(fd)); + Require(fdopendir_nid_postfix(fd) == nullptr && *__error_nid_postfix() == 9); + Require(fdopendir_nid_postfix(-1) == nullptr && *__error_nid_postfix() == 9); + const int file = open_nid_postfix((root / "sample.txt").string().c_str(), 0, 0); + Require(file >= 0); + Require(fdopendir_nid_postfix(file) == nullptr && *__error_nid_postfix() == 20); + Require(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 new file mode 100644 index 0000000000..3fb7ac7b71 --- /dev/null +++ b/core/libs/tests/GuestFileLock.cpp @@ -0,0 +1,324 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class FileStream; + +extern "C" { +int APS5_VABI open_nid_postfix(const char*, int, int); +int APS5_VABI close_nid_postfix(int); +int APS5_VABI sceKernelClose(int); +int APS5_VABI dup_nid_postfix(int); +int APS5_VABI dup2_nid_postfix(int, int); +int APS5_VABI fcntl_nid_postfix(int, int, ...); +int APS5_VABI flock_nid_postfix(int, int); +std::int64_t APS5_VABI sceKernelWrite(int, const void*, std::size_t); +std::int64_t APS5_VABI sceKernelPread(int, void*, std::size_t, std::int64_t); +std::int64_t APS5_VABI sceKernelPwrite(int, const void*, std::size_t, std::int64_t); +int* APS5_VABI __error_nid_postfix(); +FileStream* APS5_VABI fopen_nid_postfix(const char*, const char*); +FileStream* APS5_VABI fdopen_nid_postfix(int, const char*); +FileStream* APS5_VABI freopen_nid_postfix(const char*, const char*, FileStream*); +int APS5_VABI fclose_nid_postfix(FileStream*); +int APS5_VABI fileno_nid_postfix(FileStream*); +} + +static void Require(bool condition) { + if (!condition) std::abort(); +} + +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, target); + return fcntl_nid_postfix(source, 0, target); +} + +static bool Acquires(int descriptor, int mode) { + const int result = flock_nid_postfix(descriptor, mode | 4); + if (result == 0) { + Require(flock_nid_postfix(descriptor, 8) == 0); + return true; + } + Require(result == -1 && *__error_nid_postfix() == 35); + return false; +} + +struct Fixture { + static inline unsigned serial = 0; + std::string path = (std::filesystem::temp_directory_path() / ("anyps5-flock-" + std::to_string(::GetCurrentProcessId()) + "-" + std::to_string(serial++) + ".bin")).string(); + std::set descriptors; + int source = Open(true); + int other = Open(false); + + int Open(bool create) { + const int descriptor = open_nid_postfix(path.c_str(), create ? 0x602 : 2, 0644); + Require(descriptor >= 0); + descriptors.insert(descriptor); + return descriptor; + } + 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; + } + void Close(int descriptor, bool kernel = false) { + Require((kernel ? sceKernelClose(descriptor) : close_nid_postfix(descriptor)) == 0); + descriptors.erase(descriptor); + } + ~Fixture() { + for (const int descriptor : descriptors) Require(close_nid_postfix(descriptor) == 0); + Require(std::remove(path.c_str()) == 0); + } +}; + +static void Duplicates() { + for (int kind = 0; kind != 3; ++kind) { + for (bool before : {false, true}) { + for (int mode : {1, 2}) { + Fixture fixture; + int copy = before ? fixture.Copy(kind) : -1; + Require(flock_nid_postfix(fixture.source, mode | 4) == 0); + if (!before) copy = fixture.Copy(kind); + Require(flock_nid_postfix(copy, mode | 4) == 0); + Require(flock_nid_postfix(fixture.source, mode | 4) == 0); + Require(!Acquires(fixture.other, 2)); + Require(flock_nid_postfix(copy, 8) == 0); + Require(Acquires(fixture.other, 2)); + Require(flock_nid_postfix(fixture.source, 8) == 0); + } + } + } +} + +static void Lifetime() { + for (int kind = 0; kind != 3; ++kind) { + for (bool kernel : {false, true}) { + Fixture fixture; + const int copy = fixture.Copy(kind); + Require(flock_nid_postfix(fixture.source, 2 | 4) == 0); + const int chain = fixture.Copy(0, copy); + fixture.Close(fixture.source, kernel); + fixture.Close(copy, !kernel); + Require(!Acquires(fixture.other, 2)); + Require(flock_nid_postfix(chain, 8) == 0); + Require(Acquires(fixture.other, 2)); + Require(flock_nid_postfix(chain, 2 | 4) == 0); + fixture.Close(chain, kernel); + Require(Acquires(fixture.other, 2)); + } + } + { + Fixture fixture; + const int copy = fixture.Copy(0); + Require(flock_nid_postfix(fixture.source, 2 | 4) == 0); + Require(dup2_nid_postfix(copy, fixture.source) == fixture.source); + Require(dup2_nid_postfix(fixture.source, fixture.source) == fixture.source); + Require(!Acquires(fixture.other, 2)); + Require(flock_nid_postfix(fixture.source, 8) == 0); + Require(Acquires(fixture.other, 2)); + } + { + Fixture first; + Fixture second; + const int copy = second.Copy(0); + Require(flock_nid_postfix(second.source, 2 | 4) == 0); + Require(dup2_nid_postfix(first.source, second.source) == second.source); + Require(!Acquires(second.other, 2)); + Require(flock_nid_postfix(copy, 8) == 0); + Require(Acquires(second.other, 2)); + Require(flock_nid_postfix(second.source, 2 | 4) == 0); + Require(!Acquires(first.other, 2)); + Require(flock_nid_postfix(first.source, 8) == 0); + Require(Acquires(first.other, 2)); + } + { + Fixture fixture; + const int previous = fixture.source; + Require(flock_nid_postfix(previous, 2 | 4) == 0); + fixture.Close(previous); + const int recycled = fixture.Open(false); + Require(recycled == previous && Acquires(recycled, 2)); + Require(Acquires(fixture.other, 2)); + } +} + +static void Conversions() { + for (int kind = 0; kind != 3; ++kind) { + Fixture fixture; + const int copy = fixture.Copy(kind); + for (int repeat = 0; repeat != 20; ++repeat) { + Require(flock_nid_postfix(fixture.source, 2 | 4) == 0); + Require(flock_nid_postfix(copy, 1 | 4) == 0); + Require(Acquires(fixture.other, 1) && !Acquires(fixture.other, 2)); + Require(flock_nid_postfix(fixture.source, 2 | 4) == 0); + Require(!Acquires(fixture.other, 1)); + Require(flock_nid_postfix(copy, 8) == 0 && Acquires(fixture.other, 2)); + } + Require(flock_nid_postfix(fixture.source, 1 | 4) == 0); + Require(flock_nid_postfix(fixture.other, 1 | 4) == 0); + Require(flock_nid_postfix(copy, 2 | 4) == -1 && *__error_nid_postfix() == 35); + Require(flock_nid_postfix(fixture.other, 8) == 0); + Require(Acquires(fixture.other, 2)); + Require(flock_nid_postfix(fixture.source, 2 | 4) == 0); + char byte = 'x'; + Require(sceKernelPwrite(fixture.other, &byte, 1, 0) == 1 && sceKernelWrite(fixture.other, "yz", 2) == 2); + Require(sceKernelPread(fixture.other, &byte, 1, 1) == 1 && byte == 'z'); + Require(flock_nid_postfix(fixture.other, 1 | 4) == -1 && *__error_nid_postfix() == 35); + Require(flock_nid_postfix(fixture.source, 8) == 0); + } +} + +static void Threads() { + for (int kind = 0; kind != 3; ++kind) { + Fixture fixture; + Require(flock_nid_postfix(fixture.source, 2 | 4) == 0); + const int copy = fixture.Copy(kind); + auto repeated = std::async(std::launch::async, [&] { return flock_nid_postfix(copy, 2); }); + Require(repeated.wait_for(std::chrono::seconds(2)) == std::future_status::ready); + Require(repeated.get() == 0 && flock_nid_postfix(copy, 8) == 0); + } + for (bool duplicate : {false, true}) { + Fixture fixture; + const int copy = duplicate ? fixture.Copy(0) : fixture.source; + std::barrier gate(3); + int results[2] = {-2, -2}; + std::thread first([&] { gate.arrive_and_wait(); results[0] = flock_nid_postfix(fixture.source, 1 | 4); }); + std::thread second([&] { gate.arrive_and_wait(); results[1] = flock_nid_postfix(copy, 1 | 4); }); + gate.arrive_and_wait(); + first.join(); + second.join(); + Require(results[0] == 0 && results[1] == 0); + Require(flock_nid_postfix(copy, 8) == 0 && Acquires(fixture.other, 2)); + } + Fixture first; + Fixture unrelated; + Require(flock_nid_postfix(first.source, 2 | 4) == 0); + std::promise entered; + auto entry = entered.get_future(); + auto waiting = std::async(std::launch::async, [&] { + entered.set_value(); + return flock_nid_postfix(first.other, 2); + }); + entry.wait(); + Require(waiting.wait_for(std::chrono::milliseconds(200)) == std::future_status::timeout); + Require(Acquires(unrelated.source, 2)); + Require(flock_nid_postfix(first.source, 8) == 0); + Require(waiting.wait_for(std::chrono::seconds(2)) == std::future_status::ready && waiting.get() == 0); + Require(flock_nid_postfix(first.other, 8) == 0); +} + +static void Streams() { + Fixture first; + Fixture second; + auto* stream = fopen_nid_postfix(first.path.c_str(), "r+b"); + Require(stream != nullptr); + const int descriptor = fileno_nid_postfix(stream); + Require(flock_nid_postfix(descriptor, 2 | 4) == 0); + Require(fclose_nid_postfix(stream) == 0 && Acquires(first.other, 2)); + for (const auto* fixture : {&second, &first}) { + stream = fopen_nid_postfix(fixture->path.c_str(), "r+b"); + Require(stream != nullptr && fileno_nid_postfix(stream) == descriptor); + Require(flock_nid_postfix(descriptor, 2 | 4) == 0 && !Acquires(fixture->other, 2)); + Require(fclose_nid_postfix(stream) == 0); + } + const int recycled = first.Open(false); + Require(recycled == descriptor && flock_nid_postfix(recycled, 2 | 4) == 0); + Require(!Acquires(first.other, 2)); + const int copy = first.Copy(0, recycled); + stream = fdopen_nid_postfix(recycled, "r+"); + Require(stream != nullptr && !Acquires(first.other, 2)); + Require(fclose_nid_postfix(stream) == 0); + first.descriptors.erase(recycled); + Require(!Acquires(first.other, 2)); + Require(flock_nid_postfix(copy, 8) == 0 && Acquires(first.other, 2)); + stream = fopen_nid_postfix(first.path.c_str(), "r+b"); + Require(stream != nullptr); + const int reopened = fileno_nid_postfix(stream); + Require(flock_nid_postfix(reopened, 2 | 4) == 0); + const int oldCopy = first.Copy(0, reopened); + Require(freopen_nid_postfix(second.path.c_str(), "r+b", stream) == stream); + Require(!Acquires(first.other, 2)); + Require(flock_nid_postfix(oldCopy, 8) == 0 && Acquires(first.other, 2)); + Require(flock_nid_postfix(fileno_nid_postfix(stream), 2 | 4) == 0 && !Acquires(second.other, 2)); + const auto missing = first.path + ".missing"; + Require(freopen_nid_postfix(missing.c_str(), "r+b", stream) == nullptr); + Require(Acquires(second.other, 2)); + stream = fopen_nid_postfix(second.path.c_str(), "r+b"); + Require(stream != nullptr); + Require(flock_nid_postfix(fileno_nid_postfix(stream), 2 | 4) == 0 && !Acquires(second.other, 2)); + Require(fclose_nid_postfix(stream) == 0); +} + +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::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, 30000) != WAIT_OBJECT_0) { + ::TerminateProcess(process.hProcess, 99); + std::abort(); + } + DWORD result = 99; + Require(::GetExitCodeProcess(process.hProcess, &result) != 0); + ::CloseHandle(process.hProcess); + return static_cast(result); +} + +static void Processes() { + 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); +} + +int main(int argc, char** argv) { + 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]; + if (mode == "duplicate") Duplicates(); + else if (mode == "lifetime") Lifetime(); + else if (mode == "conversion") Conversions(); + else if (mode == "threads") Threads(); + else if (mode == "streams") Streams(); + else if (mode == "process") Processes(); + else std::abort(); +} diff --git a/core/libs/tests/GuestFileTimestamps.cpp b/core/libs/tests/GuestFileTimestamps.cpp index 48a1758954..62750ec5f3 100644 --- a/core/libs/tests/GuestFileTimestamps.cpp +++ b/core/libs/tests/GuestFileTimestamps.cpp @@ -12,11 +12,14 @@ extern "C" { int APS5_VABI utimes_nid_postfix(const char*, const KernelTimeval*); +int APS5_VABI lutimes_nid_postfix(const char*, const KernelTimeval*); int APS5_VABI futimes_nid_postfix(int, const KernelTimeval*); int APS5_VABI sceKernelUtimes_nid_postfix(const char*, const KernelTimeval*); int APS5_VABI stat_nid_postfix(const char*, FileStat*); +int APS5_VABI lstat_nid_postfix(const char*, FileStat*); int APS5_VABI sceKernelFstat(int, FileStat*); std::int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int, FileStat*); +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*); @@ -35,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"); } @@ -72,6 +76,8 @@ int main() { CheckTimes(path, fd, second); Require(sceKernelUtimes_nid_postfix(path.c_str(), first) == 0, "sceKernelUtimes fractional timestamp"); CheckTimes(path, fd, first); + Require(lutimes_nid_postfix(path.c_str(), second) == 0, "lutimes fractional timestamp"); + CheckTimes(path, fd, second); for (int index = 0; index < 2; ++index) { for (const auto micros : {-1LL, 1000000LL}) { @@ -80,10 +86,20 @@ int main() { Require(utimes_nid_postfix(path.c_str(), invalid) == -1 && *__error_nid_postfix() == 22, "utimes rejects invalid microseconds"); Require(futimes_nid_postfix(fd, invalid) == -1 && *__error_nid_postfix() == 22, "futimes rejects invalid microseconds"); Require(sceKernelUtimes_nid_postfix(path.c_str(), invalid) == static_cast(0x80020016u), "sceKernelUtimes rejects invalid microseconds"); - CheckTimes(path, fd, first); + Require(lutimes_nid_postfix(path.c_str(), invalid) == -1 && *__error_nid_postfix() == 22, "lutimes rejects invalid microseconds"); + CheckTimes(path, fd, second); } } + 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); @@ -137,6 +153,35 @@ int main() { Require(close_nid_postfix(directoryFd) == 0, "close directory descriptor"); Require(utimes_nid_postfix((root / "missing").string().c_str(), first) == -1 && *__error_nid_postfix() == 2, "missing path reports ENOENT"); + Require(lutimes_nid_postfix(nullptr, first) == -1 && *__error_nid_postfix() == 14, "lutimes rejects null path"); + Require(lutimes_nid_postfix("", first) == -1 && *__error_nid_postfix() == 2, "lutimes rejects empty path"); + Require(lutimes_nid_postfix((root / "missing").string().c_str(), first) == -1 && *__error_nid_postfix() == 2, "lutimes missing path reports ENOENT"); + Require(utimes_nid_postfix(path.c_str(), second) == 0, "utimes target to second"); + std::error_code symlinkError; + const auto linkPath = (root / "link.txt").string(); + std::filesystem::create_symlink(path, linkPath, symlinkError); + if (!symlinkError) { + Require(lutimes_nid_postfix(linkPath.c_str(), first) == 0, "lutimes modifies symlink"); + FileStat targetStatus{}; + Require(stat_nid_postfix(path.c_str(), &targetStatus) == 0 && Same(targetStatus.st_mtim, second[1]), "target file unchanged"); +#ifdef _WIN32 + const HANDLE handle = CreateFileW(std::filesystem::path(linkPath).c_str(), FILE_READ_ATTRIBUTES, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, + FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + Require(handle != INVALID_HANDLE_VALUE, "open symlink"); + FILETIME modified{}; + Require(GetFileTime(handle, nullptr, nullptr, &modified), "get symlink time"); + CloseHandle(handle); + const auto ticks = [](FILETIME time) { + return (static_cast(time.dwHighDateTime) << 32) | time.dwLowDateTime; + }; + Require(ticks(modified) == static_cast(first[1].tv_sec + 11644473600LL) * 10000000 + first[1].tv_usec * 10, + "symlink mtime updated"); +#else + FileStat linkStatus{}; + Require(lstat_nid_postfix(linkPath.c_str(), &linkStatus) == 0 && Same(linkStatus.st_mtim, first[1]), "symlink mtime updated"); +#endif + } int pipe[2]; Require(pipe_nid_postfix(pipe) == 0, "create pipe"); FileStat pipeStatus{}; diff --git a/core/libs/tests/GuestFileVector.cpp b/core/libs/tests/GuestFileVector.cpp index 1851fb20b1..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,12 +12,15 @@ #include #include #include +#include +#include #ifdef _WIN32 #include #include static int MakePipe(int* ends) { return ::_pipe(ends, 64, _O_BINARY); } static int ClosePipe(int end) { return ::_close(end); } #else +#include #include static int MakePipe(int* ends) { return ::pipe(ends); } static int ClosePipe(int end) { return ::close(end); } @@ -29,12 +34,27 @@ 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); std::int64_t APS5_VABI sceKernelPwritev(int, const GuestIovec*, int, std::int64_t); +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(); +std::int64_t APS5_VABI sceKernelPread(int, void*, std::size_t, std::int64_t); +std::int64_t APS5_VABI sceKernelPwrite(int, const void*, std::size_t, std::int64_t); +std::int64_t APS5_VABI pread_nid_postfix(int, void*, std::size_t, std::int64_t); +std::int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int, const void*, std::size_t, std::int64_t); +int APS5_VABI sceKernelFtruncate(int, std::int64_t); } static void Check(bool value, int line) { @@ -49,16 +69,118 @@ static constexpr std::int64_t ErrorEbadf = static_cast(0x80020009u); static constexpr std::int64_t ErrorEfault = static_cast(0x8002000Eu); static constexpr std::int64_t ErrorEinval = static_cast(0x80020016u); static constexpr std::int64_t ErrorEspipe = static_cast(0x8002001Du); +static constexpr std::int64_t ErrorEagain = static_cast(0x80020023u); static std::string Contents(const std::filesystem::path& path) { std::ifstream stream(path, std::ios::binary); return std::string(std::istreambuf_iterator(stream), std::istreambuf_iterator()); } +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"; } @@ -74,6 +196,16 @@ int main() { Require(sceKernelReadv(file, reads, 2) == 3); Require(std::memcmp(first, "789", 3) == 0); Require(sceKernelReadv(file, reads, 2) == 0); + Require(sceKernelLseek(file, 0, 0) == 0); + Require(readv_nid_postfix(file, reads, 2) == 7); + Require(std::memcmp(first, "012", 3) == 0 && std::memcmp(second, "3456", 4) == 0); + 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); Require(sceKernelPreadv(file, reads, 2, 1) == 7); Require(std::memcmp(first, "123", 3) == 0 && std::memcmp(second, "4567", 4) == 0); @@ -91,6 +223,14 @@ int main() { Require(sceKernelWritev(file, writes, 2) == 5); Require(sceKernelLseek(file, 0, 1) == 13); Require(Contents(path) == "01ABCDE7ABCDE"); + GuestIovec swapped[2] = {{cde, 3}, {ab, 2}}; + 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); + 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); @@ -119,11 +259,69 @@ int main() { Require(sceKernelPwritev(file, writes, 2, -1) == ErrorEinval); Require(Contents(path) == "01ABCDE7ABCDE"); + Require(sceKernelLseek(file, 4, 0) == 4); + Require(preadv_nid_postfix(file, reads, 2, 1) == 7); + Require(std::memcmp(first, "1AB", 3) == 0 && std::memcmp(second, "CDE7", 4) == 0); + Require(sceKernelLseek(file, 0, 1) == 4); + Require(preadv_nid_postfix(file, reads, 2, 13) == 0); + Require(preadv_nid_postfix(file, reads, 2, -1) == -1 && *__error_nid_postfix() == 22); + Require(preadv_nid_postfix(file, reads, -1, 0) == -1 && *__error_nid_postfix() == 22); + Require(preadv_nid_postfix(file, nullptr, 1, 0) == -1 && *__error_nid_postfix() == 14); + Require(pwritev_nid_postfix(file, writes, 2, 8) == 5); + Require(sceKernelLseek(file, 0, 1) == 4); + Require(Contents(path) == "01ABCDE7ABCDE"); + char x[] = "X"; + char y[] = "Y"; + GuestIovec marks[2] = {{x, 1}, {y, 1}}; + Require(pwritev_nid_postfix(file, marks, 2, 0) == 2); + Require(Contents(path) == "XYABCDE7ABCDE"); + char zero[] = "0"; + char one[] = "1"; + GuestIovec restore[2] = {{zero, 1}, {one, 1}}; + Require(pwritev_nid_postfix(file, restore, 2, 0) == 2); + Require(sceKernelLseek(file, 0, 1) == 4); + Require(pwritev_nid_postfix(file, writes, 2, -1) == -1 && *__error_nid_postfix() == 22); + Require(pwritev_nid_postfix(file, nullptr, 1, 0) == -1 && *__error_nid_postfix() == 14); + + char byte = 0; + Require(sceKernelPread(file, &byte, 1, 2) == 1 && byte == 'A'); + Require(sceKernelPread(file, nullptr, 0, 0) == 0); + Require(sceKernelPwrite(file, nullptr, 0, 0) == 0); + Require(sceKernelPread(file, nullptr, 1, 0) == ErrorEfault); + Require(sceKernelPwrite(file, nullptr, 1, 0) == ErrorEfault); + Require(sceKernelPread(file, &byte, 1, -1) == ErrorEinval); + Require(sceKernelPwrite(file, &byte, 1, -1) == ErrorEinval); + Require(pread_nid_postfix(file, &byte, 1, 3) == 1 && byte == 'B'); + Require(pread_nid_postfix(file, nullptr, 0, 0) == 0); + Require(pread_nid_postfix(file, nullptr, 1, 0) == -1 && *__error_nid_postfix() == 14); + Require(pwrite_nid_disambig1_nid_postfix(file, nullptr, 1, 0) == -1 && *__error_nid_postfix() == 14); + Require(pread_nid_postfix(file, &byte, 1, -1) == -1 && *__error_nid_postfix() == 22); + Require(pwrite_nid_disambig1_nid_postfix(file, &byte, 1, -1) == -1 && *__error_nid_postfix() == 22); + Require(pread_nid_postfix(9999, &byte, 1, 0) == -1 && *__error_nid_postfix() == 9); + Require(pwrite_nid_disambig1_nid_postfix(9999, &byte, 1, 0) == -1 && *__error_nid_postfix() == 9); + Require(sceKernelFtruncate(file, -1) == ErrorEinval); + const int readOnly = sceKernelOpen(path.string().c_str(), SCE_KERNEL_O_RDONLY, 0); + Require(readOnly >= 0); + Require(sceKernelPwrite(readOnly, "X", 1, 0) == ErrorEbadf); + Require(sceKernelFtruncate(readOnly, 0) == ErrorEinval); + Require(sceKernelClose(readOnly) == 0); + const int writeOnly = sceKernelOpen(path.string().c_str(), SCE_KERNEL_O_WRONLY, 0); + Require(writeOnly >= 0); + Require(sceKernelPread(writeOnly, &byte, 1, 0) == ErrorEbadf); + Require(pread_nid_postfix(writeOnly, &byte, 1, 0) == -1 && *__error_nid_postfix() == 9); + Require(sceKernelClose(writeOnly) == 0); + Require(Contents(path) == "01ABCDE7ABCDE"); + Require(sceKernelFtruncate(file, 5) == 0); + Require(Contents(path) == "01ABC"); + Require(sceKernelClose(file) == 0); Require(sceKernelReadv(file, reads, 2) == ErrorEbadf); Require(sceKernelWritev(file, writes, 2) == ErrorEbadf); Require(sceKernelPreadv(file, reads, 2, 0) == ErrorEbadf); Require(sceKernelPwritev(file, writes, 2, 0) == ErrorEbadf); + Require(sceKernelPread(file, &byte, 1, 0) == ErrorEbadf); + Require(sceKernelPwrite(file, &byte, 1, 0) == ErrorEbadf); + Require(sceKernelFtruncate(file, 0) == ErrorEbadf); int ends[2] = {}; Require(MakePipe(ends) == 0); @@ -132,10 +330,34 @@ int main() { Require(sceKernelWritev(ends[1], message, 1) == 3); Require(sceKernelReadv(ends[0], reads, 2) == 3); Require(std::memcmp(first, "xyz", 3) == 0); + char stuvw[] = "stuvw"; + GuestIovec longer[1] = {{stuvw, 5}}; + Require(sceKernelWritev(ends[1], longer, 1) == 5); + Require(sceKernelReadv(ends[0], reads, 2) == 5); + Require(std::memcmp(first, "stu", 3) == 0 && std::memcmp(second, "vw", 2) == 0); + Require(sceKernelWritev(ends[1], longer, 1) == 5 && sceKernelWritev(ends[1], longer, 1) == 5); + Require(readv_nid_postfix(ends[0], reads, 2) == 7); + Require(std::memcmp(first, "stu", 3) == 0 && std::memcmp(second, "vwst", 4) == 0); + Require(sceKernelReadv(ends[0], reads, 2) == 3 && std::memcmp(first, "uvw", 3) == 0); Require(sceKernelPreadv(ends[0], reads, 2, 0) == ErrorEspipe); Require(sceKernelPwritev(ends[1], message, 1, 0) == ErrorEspipe); +#ifndef _WIN32 + Require(::fcntl(ends[0], F_SETFL, ::fcntl(ends[0], F_GETFL) | O_NONBLOCK) == 0); + Require(sceKernelReadv(ends[0], reads, 2) == ErrorEagain); + Require(readv_nid_postfix(ends[0], reads, 2) == -1 && *__error_nid_postfix() == 35); +#endif Require(ClosePipe(ends[0]) == 0 && ClosePipe(ends[1]) == 0); + const int random = sceKernelOpen("/dev/urandom", 0, 0); + Require(random >= 0); + unsigned char noise[48]{}; + GuestIovec pieces[2] = {{noise, 16}, {noise + 16, 16}}; + Require(sceKernelReadv(random, pieces, 2) == 32); + Require(sceKernelPreadv(random, pieces, 1, 1000) == 16); + Require(sceKernelPread(random, noise + 32, 16, 1000) == 16); + Require(std::memcmp(noise, noise + 16, 16) != 0 && std::memcmp(noise + 16, noise + 32, 16) != 0); + Require(sceKernelClose(random) == 0); + std::filesystem::remove_all(root); return 0; } 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 777896f18e..c1a1a863bb 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -1,33 +1,58 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libc/include/FileStream.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 #include #include #include +#include +#include +#include +#include +#include #ifdef _WIN32 #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*); int APS5_VABI sceKernelChmod_nid_postfix(const char*, unsigned short); int APS5_VABI sceKernelFchmod(int, unsigned short); int APS5_VABI fchmod_nid_postfix(int, int); +int APS5_VABI chmod_nid_postfix(const char*, int); +int APS5_VABI chflags_nid_postfix(const char*, std::uint64_t); +int APS5_VABI 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); int APS5_VABI sceKernelFsync(int); int APS5_VABI fdatasync_nid_postfix(int); +int APS5_VABI fsync_nid_postfix(int); +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); int APS5_VABI _open_nid_postfix(const char*, int, ...); int APS5_VABI close_nid_postfix(int); +int APS5_VABI flock_nid_postfix(int, int); int APS5_VABI stat_nid_postfix(const char*, FileStat*); int APS5_VABI lstat_nid_postfix(const char*, FileStat*); int APS5_VABI unlink_nid_postfix(const char*); @@ -35,27 +60,422 @@ int APS5_VABI rmdir_nid_postfix(const char*); 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); +std::int64_t APS5_VABI sceKernelPreadv(int, const void*, int, std::int64_t); +std::int64_t APS5_VABI sceKernelPwrite(int, const void*, std::size_t, std::int64_t); int APS5_VABI sceKernelStat(const char*, FileStat*); +int APS5_VABI sceKernelCheckReachability(const char*); +int APS5_VABI sceKernelFstat(int, FileStat*); int APS5_VABI sceKernelUnlink(const char*); +int APS5_VABI sceKernelRename(const char*, const char*); int APS5_VABI sceKernelRmdir(const char*); +int APS5_VABI sceKernelMkdir(const char*, unsigned short); +int APS5_VABI sceKernelRename(const char*, const char*); +int APS5_VABI sceKernelCheckReachability(const char*); +int APS5_VABI access_nid_postfix(const char*, int); +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 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); 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) { - std::fprintf(stderr, "Filesystem check failed at line %d (guest errno %d)\n", line, *__error_nid_postfix()); +#ifdef _WIN32 + const auto nativeError = ::GetLastError(); +#else + const auto nativeError = errno; +#endif + const auto guestError = *__error_nid_postfix(); +#ifdef _WIN32 + std::fprintf(stderr, "Filesystem check failed at line %d (guest errno %d, Win32 error %lu)\n", + line, guestError, static_cast(nativeError)); +#else + std::fprintf(stderr, "Filesystem check failed at line %d (guest errno %d, host errno %d)\n", + line, guestError, nativeError); +#endif std::abort(); } } #define Require(value) Check((value), __LINE__) + +static bool ChflagsThrows(const std::filesystem::path& path, std::uint64_t flags, const char* names) { + try { + chflags_nid_postfix(path.string().c_str(), flags); + } catch (const std::runtime_error& error) { + return std::strstr(error.what(), names) != nullptr; + } + return false; +} + +static 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())); + Require(std::filesystem::create_directories(root / "Existing")); + const auto present = (root / "present.txt").generic_string(); + { std::ofstream file(present); file << "preserved"; } + const auto directory = (root / "victim").generic_string(); + Require(std::filesystem::create_directory(directory)); + const auto checkInvalid = [&](const std::string& prefix, int error) { + const auto path = prefix + "/present.txt"; + const auto missingLeaf = prefix + "/new.txt"; + const int kernelError = static_cast(0x80020000u | static_cast(error)); + FileStat status{}; + Require(stat_nid_postfix(path.c_str(), &status) == -1 && *__error_nid_postfix() == error); + Require(lstat_nid_postfix(path.c_str(), &status) == -1 && *__error_nid_postfix() == error); + Require(sceKernelStat(path.c_str(), &status) == kernelError); + Require(open_nid_postfix(path.c_str(), 0, 0) == -1 && *__error_nid_postfix() == error); + Require(sceKernelOpen(path.c_str(), 0x601, 0600) == kernelError); + Require(sceKernelOpen(missingLeaf.c_str(), 0x601, 0600) == kernelError); + Require(access_nid_postfix(path.c_str(), 0) == -1 && *__error_nid_postfix() == error); + Require(sceKernelCheckReachability(path.c_str()) == kernelError); + Require(chmod_nid_postfix(path.c_str(), 0400) == -1 && *__error_nid_postfix() == error); + Require(sceKernelChmod_nid_postfix(path.c_str(), 0400) == kernelError); + Require(sceKernelTruncate_nid_postfix(path.c_str(), 0) == kernelError); + Require(sceKernelUtimes_nid_postfix(path.c_str(), nullptr) == kernelError); + Require(unlink_nid_postfix(path.c_str()) == -1 && *__error_nid_postfix() == error); + Require(sceKernelUnlink(path.c_str()) == kernelError); + Require(remove_nid_postfix(path.c_str()) == -1 && *__error_nid_postfix() == error); + Require(rename_nid_postfix(path.c_str(), missingLeaf.c_str()) == -1 && *__error_nid_postfix() == error); + Require(rename_nid_postfix(present.c_str(), missingLeaf.c_str()) == -1 && *__error_nid_postfix() == error); + Require(sceKernelRename(path.c_str(), missingLeaf.c_str()) == kernelError); + Require(sceKernelRename(present.c_str(), missingLeaf.c_str()) == kernelError); + const auto victim = prefix + "/victim"; + Require(rmdir_nid_postfix(victim.c_str()) == -1 && *__error_nid_postfix() == error); + Require(sceKernelRmdir(victim.c_str()) == kernelError); + Require(remove_nid_postfix(victim.c_str()) == -1 && *__error_nid_postfix() == error); + Require(mkdir_nid_postfix(missingLeaf.c_str(), 0700) == -1 && *__error_nid_postfix() == error); + Require(sceKernelMkdir(missingLeaf.c_str(), 0700) == kernelError); + Require(chdir_nid_postfix(prefix.c_str()) == -1 && *__error_nid_postfix() == error); + Require(opendir_nid_postfix(prefix.c_str()) == nullptr && *__error_nid_postfix() == error); + Require(fopen_nid_postfix(path.c_str(), "wb") == nullptr && *__error_nid_postfix() == error); + auto* stream = fopen_nid_postfix(present.c_str(), "rb"); + Require(stream != nullptr); + const int descriptor = stream->GuestState().descriptor; + Require(freopen_nid_postfix(path.c_str(), "wb", stream) == nullptr && *__error_nid_postfix() == error); + Require(!DescriptorIsOpen_nid_no_patch(descriptor)); + std::ifstream file(present); + std::string contents; + std::getline(file, contents); + Require(contents == "preserved" && std::filesystem::is_directory(directory)); + Require(!std::filesystem::exists(root / "new.txt")); + }; + checkInvalid(root.generic_string() + "/missing/..", 2); + checkInvalid(root.generic_string() + "/present.txt/..", 20); + checkInvalid(root.generic_string() + "/present.txt/child/../..", 20); + checkInvalid("/" + root.generic_string() + "/missing/..", 2); + checkInvalid(root.generic_string() + "/missing/../../" + root.generic_string(), 2); + AddPathAlias_nid_no_patch("parent-test-mount", std::filesystem::absolute(root).string().c_str()); + checkInvalid("/PARENT-TEST-MOUNT/missing/..", 2); + 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); +#ifndef _WIN32 + for (const char* name : {"slash-unlink", "slash-remove", "slash-rename", "slash-open", "slash-kernel"}) std::ofstream(root / name) << "kept"; + Require(unlink_nid_postfix("/parent-test-mount/slash-unlink/") == -1 && *__error_nid_postfix() == 20); + Require(remove_nid_postfix("/parent-test-mount/slash-remove/") == -1 && *__error_nid_postfix() == 20); + Require(rename_nid_postfix("/parent-test-mount/slash-rename/", "/parent-test-mount/slash-renamed") == -1 && *__error_nid_postfix() == 20); + Require(open_nid_postfix("/parent-test-mount/slash-open/", 0, 0) == -1 && *__error_nid_postfix() == 20); + Require(sceKernelUnlink("/parent-test-mount/slash-kernel/") == static_cast(0x80020014u)); + for (const char* name : {"slash-unlink", "slash-remove", "slash-rename", "slash-open", "slash-kernel"}) Require(std::filesystem::is_regular_file(root / name)); + Require(!std::filesystem::exists(root / "slash-renamed")); + Require(sceKernelStat("/parent-test-mount/Existing/", &status) == 0 && (status.st_mode & 0170000) == 0040000); + Require(mkdir_nid_postfix("/parent-test-mount/slash-directory/", 0755) == 0 && std::filesystem::is_directory(root / "slash-directory")); + Require(rmdir_nid_postfix("/parent-test-mount/slash-directory/") == 0 && !std::filesystem::exists(root / "slash-directory")); +#endif + RemovePathAlias_nid_no_patch("parent-test-mount"); + std::error_code linkError; + std::filesystem::create_directory_symlink("Existing", root / "directory-link", linkError); + if (!linkError) { + Require(sceKernelStat((root.generic_string() + "/directory-link/../present.txt").c_str(), &status) == 0 && status.st_size == 9); + std::filesystem::create_symlink("present.txt", root / "file-link"); + std::filesystem::create_directory_symlink("missing", root / "dangling-link"); + checkInvalid(root.generic_string() + "/file-link/..", 20); + checkInvalid(root.generic_string() + "/dangling-link/..", 2); + } + Require(chdir_nid_postfix(root.generic_string().c_str()) == 0); + Require(sceKernelStat("missing/../present.txt", &status) == static_cast(0x80020002u)); + Require(sceKernelStat("present.txt/..", &status) == static_cast(0x80020014u)); + Require(sceKernelStat("EXISTING/../present.txt", &status) == 0 && status.st_size == 9); + const int created = sceKernelOpen("Existing/../new.txt", 0x601, 0600); + Require(created >= 0 && sceKernelClose(created) == 0); + Require(chdir_nid_postfix("/") == 0); + std::filesystem::remove_all(root); +} + +static void TestRelinkedModuleNames() { + const auto root = std::filesystem::path("anyps5-relinked-names-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directories(root)); + const auto original = (root / "Plugin.prx").generic_string(); + const auto relinked = original + ".guest.prx"; + FileStat status{}; + Require(sceKernelStat(original.c_str(), &status) == static_cast(0x80020002u)); + Require(sceKernelCheckReachability(original.c_str()) == static_cast(0x80020002u)); + Require(sceKernelOpen(original.c_str(), 0, 0) == static_cast(0x80020002u)); + 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); + Require(sceKernelCheckReachability(original.c_str()) == 0); + const int opened = sceKernelOpen(original.c_str(), 0, 0); + Require(opened >= 0 && sceKernelClose(opened) == 0); + Require(sceKernelStat((root / "Other.prx").generic_string().c_str(), &status) == static_cast(0x80020002u)); + Require(!std::filesystem::exists(original)); + { std::ofstream file(original); file << "original-bytes"; } + Require(sceKernelStat(original.c_str(), &status) == 0 && status.st_size == 14); + Require(std::filesystem::remove(original)); + const int created = sceKernelOpen(original.c_str(), 0x601, 0600); + Require(created >= 0 && sceKernelClose(created) == 0); + Require(std::filesystem::file_size(original) == 0 && std::filesystem::file_size(relinked) == 8); + std::filesystem::remove_all(root); +} + +static void TestRelinkedModuleNameCase() { + const auto root = std::filesystem::path("anyps5-relinked-case-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directories(root)); + { std::ofstream file(root / "librenoircore.ps5.prx.guest.prx"); file << "relinked"; } + const auto requested = (root / "libRenoirCore.PS5.prx").generic_string(); + FileStat status{}; + Require(sceKernelStat(requested.c_str(), &status) == 0 && status.st_size == 8); + Require(stat_nid_postfix(requested.c_str(), &status) == 0 && status.st_size == 8); + Require(sceKernelCheckReachability(requested.c_str()) == 0); + const int opened = sceKernelOpen(requested.c_str(), 0, 0); + Require(opened >= 0 && sceKernelClose(opened) == 0); + Require(sceKernelStat((root / "libRenoirCore.PS5.prx2").generic_string().c_str(), &status) == static_cast(0x80020002u)); + std::filesystem::remove_all(root); +} + +#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); + Require(chmod_nid_postfix("anyps5_missing_chmod_path", 0600) == -1 && *__error_nid_postfix() == 2); + Require(sceKernelChmod_nid_postfix("anyps5_missing_chmod_path", 0600) == static_cast(0x80020002u)); + Require(sceKernelStat("anyps5_missing_chmod_path", nullptr) == static_cast(0x80020002u)); + Require(sceKernelStat(".", nullptr) == static_cast(0x8002000eu)); Require(sceKernelDebugOutText(-1, "text") == static_cast(0x80020016u)); std::uint64_t throttling[4] = {1, 2, 3, 4}; Require(sceKernelWriteThrottlingStatus(throttling) == 0); 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); @@ -88,21 +508,167 @@ int main() { Require(read_nid_postfix(descriptors[0], received, sizeof(received)) == sizeof(received)); Require(std::memcmp(received, payload, sizeof(payload)) == 0); Require(read_nid_postfix(descriptors[0], received, sizeof(received)) == 0); + { + int unseekable[2] = {-1, -1}; + Require(pipe_nid_postfix(unseekable) == 0 && write_nid_postfix(unseekable[1], "pipe", 4) == 4); + for (const int end : unseekable) { + for (int whence = 0; whence < 3; ++whence) { + Require(lseek_nid_postfix(end, 0, whence) == -1 && *__error_nid_postfix() == 29); + Require(sceKernelLseek(end, 1, whence) == static_cast(0x8002001du)); + } + } + char piped[8]{}; + Require(read_nid_postfix(unseekable[0], piped, sizeof(piped)) == 4 && std::memcmp(piped, "pipe", 4) == 0); + Require(close_nid_postfix(unseekable[0]) == 0 && close_nid_postfix(unseekable[1]) == 0); + Require(lseek_nid_postfix(unseekable[0], 0, 1) == -1 && *__error_nid_postfix() == 9); + } + { + std::filesystem::remove("creat_probe.bin"); + int created = creat_nid_postfix("creat_probe.bin", 0644); + Require(created >= 0 && write_nid_postfix(created, "abc", 3) == 3); + Require(close_nid_postfix(created) == 0 && std::filesystem::file_size("creat_probe.bin") == 3); + created = creat_nid_postfix("creat_probe.bin", 0644); + Require(created >= 0 && close_nid_postfix(created) == 0 && std::filesystem::file_size("creat_probe.bin") == 0); + Require(std::filesystem::remove("creat_probe.bin")); + } + { + int ends[2] = {-1, -1}; + Require(pipe_nid_postfix(ends) == 0 && write_nid_postfix(ends[1], "abc", 3) == 3); + for (const int end : ends) { + Require(sceKernelFtruncate(end, 0) == static_cast(0x80020016u) && sceKernelFtruncate(end, 8) == static_cast(0x80020016u)); + } + char kept[4]{}; + Require(read_nid_postfix(ends[0], kept, sizeof(kept)) == 3 && std::memcmp(kept, "abc", 3) == 0); + Require(close_nid_postfix(ends[0]) == 0 && close_nid_postfix(ends[1]) == 0); + for (const char* name : {".", "/dev/urandom"}) { + const int opened = open_nid_postfix(name, 0, 0); + Require(opened >= 0 && sceKernelFtruncate(opened, 0) == static_cast(0x80020016u) && close_nid_postfix(opened) == 0); + } + } Require(close_nid_postfix(descriptors[0]) == 0); + auto* hostCapture = std::tmpfile(); + Require(hostCapture != nullptr && pipe_nid_postfix(descriptors) == 0); +#ifdef _WIN32 + const int hostError = ::_dup(2); + Require(hostError >= 0 && ::_dup2(::_fileno(hostCapture), 2) == 0); +#else + const int hostError = ::dup(2); + Require(hostError >= 0 && ::dup2(::fileno(hostCapture), 2) == 2); +#endif + Require(dup2_nid_postfix(descriptors[1], 2) == 2); + Require(sceKernelDebugOutText(1, "kept\n") == 0); + Require(write_nid_postfix(2, "P", 1) == 1); +#ifdef _WIN32 + Require(::_dup2(hostError, 2) == 0 && ::_close(hostError) == 0); +#else + Require(::dup2(hostError, 2) == 2 && ::close(hostError) == 0); +#endif + Require(close_nid_postfix(descriptors[1]) == 0); + Require(read_nid_postfix(descriptors[0], received, sizeof(received)) == 1 && received[0] == 'P'); + Require(close_nid_postfix(descriptors[0]) == 0); + std::rewind(hostCapture); + Require(std::fread(debugText, 1, sizeof(debugText), hostCapture) == 15 && std::memcmp(debugText, "[debug:1] kept\n", 15) == 0); + Require(std::fclose(hostCapture) == 0); +#ifdef _WIN32 + for (const bool reopen : {false, true}) { + auto* hostCapture = std::tmpfile(); + const int hostError = ::_dup(2); + Require(hostCapture != nullptr && hostError >= 0); + int redirect[2] = {-1, -1}; + Require(reopen || pipe_nid_postfix(redirect) == 0); + const bool captured = ::_dup2(::_fileno(hostCapture), 2) == 0 && SetStdHandle(STD_ERROR_HANDLE, reinterpret_cast(::_get_osfhandle(2))); + const bool redirected = reopen ? freopen_nid_postfix("stderr_probe.txt", "w", &_Stderr_nid_postfix) == &_Stderr_nid_postfix : dup2_nid_postfix(redirect[1], 2) == 2; + const HANDLE kept = GetStdHandle(STD_ERROR_HANDLE); + DWORD count = 0; + const bool separate = kept != reinterpret_cast(::_get_osfhandle(2)) && WriteFile(kept, "kept", 4, &count, nullptr) && count == 4; + const bool written = write_nid_postfix(2, "G", 1) == 1; + const bool restored = ::_dup2(hostError, 2) == 0 && ::_close(hostError) == 0 && SetStdHandle(STD_ERROR_HANDLE, reinterpret_cast(::_get_osfhandle(2))); + Require(captured && redirected && separate && written && restored && CloseHandle(kept)); + std::rewind(hostCapture); + char keptText[8]{}; + Require(std::fread(keptText, 1, sizeof(keptText), hostCapture) == 4 && std::memcmp(keptText, "kept", 4) == 0); + Require(std::fclose(hostCapture) == 0); + char guestText = 0; + if (reopen) { + Require(std::filesystem::file_size("stderr_probe.txt") == 1 && std::filesystem::remove("stderr_probe.txt")); + } else { + Require(close_nid_postfix(redirect[1]) == 0 && read_nid_postfix(redirect[0], &guestText, 1) == 1 && guestText == 'G'); + Require(close_nid_postfix(redirect[0]) == 0); + } + } +#endif const auto root = std::filesystem::path("anyps5-filesystem-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); Require(mkdir_nid_postfix(root.string().c_str(), 0700) == 0); 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); + } 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"; { std::ofstream stream(file); stream << "retained until removal"; } + Require(chflags_nid_postfix(nullptr, 0) == -1 && *__error_nid_postfix() == 14); + Require(chflags_nid_postfix("", 0) == -1 && *__error_nid_postfix() == 2); + Require(chflags_nid_postfix((root / "missing").string().c_str(), 0) == -1 && *__error_nid_postfix() == 2); + Require(chflags_nid_postfix((root / "missing").string().c_str(), ~0ULL) == -1 && *__error_nid_postfix() == 2); + Require(chflags_nid_postfix((file / "child").string().c_str(), 0) == -1 && *__error_nid_postfix() == 20); + Require(chflags_nid_postfix((file.string() + "/").c_str(), 0) == -1 && *__error_nid_postfix() == 20); + Require(chflags_nid_postfix(file.string().c_str(), 0) == 0); + Require(chflags_nid_postfix(root.string().c_str(), 0) == 0); + Require(chflags_nid_postfix(file.string().c_str(), 0x20) == -1 && *__error_nid_postfix() == 45); + Require(chflags_nid_postfix(file.string().c_str(), 1ULL << 32) == -1 && *__error_nid_postfix() == 45); + Require(chflags_nid_postfix(file.string().c_str(), ~0ULL) == -1 && *__error_nid_postfix() == 45); + Require(chflags_nid_postfix(file.string().c_str(), 0x200000) == -1 && *__error_nid_postfix() == 1); + Require(ChflagsThrows(file, 0x2, "UF_IMMUTABLE")); + Require(ChflagsThrows(file, 0x1000 | 0x40000, "UF_READONLY|SF_APPEND")); + const 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"; } + const int heldDescriptor = sceKernelOpen(heldName.c_str(), 2, 0); + Require(heldDescriptor >= 0 && sceKernelUnlink(heldName.c_str()) == 0 && !std::filesystem::exists(held)); + char heldBytes[4]{}; + Require(read_nid_postfix(heldDescriptor, heldBytes, sizeof(heldBytes)) == sizeof(heldBytes) && std::memcmp(heldBytes, "held", 4) == 0); + const int recreated = sceKernelOpen(heldName.c_str(), 0x0a01, 0644); + Require(recreated >= 0 && sceKernelClose(heldDescriptor) == 0 && std::filesystem::file_size(held) == 0); + const auto moved = root / "moved.txt"; + Require(rename_nid_postfix(heldName.c_str(), moved.string().c_str()) == 0 && !std::filesystem::exists(held)); + Require(write_nid_postfix(recreated, "moved", 5) == 5 && sceKernelClose(recreated) == 0 && std::filesystem::file_size(moved) == 5); + const int removing = open_nid_postfix(moved.string().c_str(), 0, 0); + Require(removing >= 0 && remove_nid_postfix(moved.string().c_str()) == 0 && !std::filesystem::exists(moved)); + Require(close_nid_postfix(removing) == 0); Require(remove_nid_postfix(root.string().c_str()) == -1); Require(*__error_nid_postfix() == 66); Require(std::filesystem::is_regular_file(file)); Require(remove_nid_postfix((file / "invalid").string().c_str()) == -1); Require(remove_nid_postfix("") == -1 && *__error_nid_postfix() == 2); Require(remove_nid_postfix(nullptr) == -1 && *__error_nid_postfix() == 14); + const auto readOnly = root / "read-only.txt"; + { std::ofstream stream(readOnly); stream << "removable"; } + Require(sceKernelChmod_nid_postfix(readOnly.string().c_str(), 0400) == 0); + Require(remove_nid_postfix(readOnly.string().c_str()) == 0 && !std::filesystem::exists(readOnly)); const auto renamed = root / "renamed.txt"; { std::ofstream stream(renamed); stream << "old contents"; } Require(rename_nid_postfix(file.string().c_str(), renamed.string().c_str()) == 0); @@ -113,6 +679,132 @@ int main() { Require(rename_nid_postfix(file.string().c_str(), renamed.string().c_str()) == -1); Require(*__error_nid_postfix() == 2); Require(rename_nid_postfix(renamed.string().c_str(), file.string().c_str()) == 0); + { + const auto area = root / "libc_rename"; + const auto sourceFile = area / "source.txt"; + const auto targetFile = area / "target.txt"; + const auto sourceDirectory = area / "source_directory"; + const auto emptyDirectory = area / "empty_directory"; + const auto fullDirectory = area / "full_directory"; + Require(std::filesystem::create_directories(area)); + { std::ofstream stream(sourceFile); stream << "source"; } + { std::ofstream stream(targetFile); stream << "target"; } + Require(std::filesystem::create_directories(sourceDirectory / "child")); + Require(std::filesystem::create_directories(emptyDirectory)); + Require(std::filesystem::create_directories(fullDirectory)); + { std::ofstream stream(fullDirectory / "entry"); stream << "entry"; } + const auto name = [](const std::filesystem::path& path) { return path.string(); }; + const auto missing = name(area / "missing"); + const auto absentParent = name(area / "absent" / "target"); + const auto fileParent = name(sourceFile / "target"); + Require(rename_nid_postfix(missing.c_str(), name(targetFile).c_str()) == -1 && *__error_nid_postfix() == 2); + Require(rename_nid_postfix(name(sourceFile).c_str(), absentParent.c_str()) == -1 && *__error_nid_postfix() == 2); + Require(rename_nid_postfix(name(sourceFile).c_str(), fileParent.c_str()) == -1 && *__error_nid_postfix() == 20); + Require(rename_nid_postfix(name(sourceFile).c_str(), name(fullDirectory).c_str()) == -1 && *__error_nid_postfix() == 21); + Require(rename_nid_postfix(name(sourceDirectory).c_str(), name(targetFile).c_str()) == -1 && *__error_nid_postfix() == 20); + Require(rename_nid_postfix(name(sourceDirectory).c_str(), name(fullDirectory).c_str()) == -1 && *__error_nid_postfix() == 66); + const auto childDestination = name(sourceDirectory / "child" / "moved"); + Require(rename_nid_postfix(name(sourceDirectory).c_str(), childDestination.c_str()) == -1 && *__error_nid_postfix() == 22); + const auto childDirectory = name(sourceDirectory / "child"); + Require(rename_nid_postfix(name(sourceDirectory).c_str(), childDirectory.c_str()) == -1 && *__error_nid_postfix() == 22); + Require(std::filesystem::is_regular_file(sourceFile) && std::filesystem::is_regular_file(fullDirectory / "entry")); + Require(rename_nid_postfix(name(sourceFile).c_str(), name(targetFile).c_str()) == 0); + { std::ifstream stream(targetFile); std::string contents; std::getline(stream, contents); Require(contents == "source"); } + Require(!std::filesystem::exists(sourceFile)); + 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"); + const auto replace = [&] { return kernel ? sceKernelRename(incoming.c_str(), occupied.c_str()) : rename_nid_postfix(incoming.c_str(), occupied.c_str()); }; + const auto contents = [&] { std::ifstream stream(occupied); std::string text; std::getline(stream, text); return text; }; + { std::ofstream stream(incoming); stream << "incoming"; } + { std::ofstream stream(occupied); stream << "occupied"; } + const int occupiedDescriptor = sceKernelOpen(occupied.c_str(), 2, 0); + Require(occupiedDescriptor >= 0 && replace() == 0 && !std::filesystem::exists(incoming) && contents() == "incoming"); + char occupiedBytes[8]{}; + Require(read_nid_postfix(occupiedDescriptor, occupiedBytes, sizeof(occupiedBytes)) == sizeof(occupiedBytes) && std::memcmp(occupiedBytes, "occupied", 8) == 0); + Require(sceKernelClose(occupiedDescriptor) == 0 && contents() == "incoming"); + { std::ofstream stream(incoming); stream << "replaced"; } + Require(sceKernelChmod_nid_postfix(occupied.c_str(), 0400) == 0); + Require(replace() == 0 && !std::filesystem::exists(incoming) && contents() == "replaced"); + Require(remove_nid_postfix(occupied.c_str()) == 0); + } +#ifndef _WIN32 + const auto dangling = area / "dangling"; + std::filesystem::create_symlink(area / "nonexistent", dangling); + Require(std::filesystem::is_symlink(dangling)); + const auto replacement = area / "replacement"; + { std::ofstream stream(replacement); stream << "replacement"; } + 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::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)); Require(remove_nid_postfix(file.string().c_str()) == -1 && *__error_nid_postfix() == 2); @@ -123,7 +815,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); @@ -134,6 +826,7 @@ int main() { #endif Require(descriptor >= 0 && sceKernelFsync(descriptor) == 0); Require(fdatasync_nid_postfix(descriptor) == 0); + Require(sceKernelFdatasync(descriptor) == 0); const auto ownerWrite = [&] { return (std::filesystem::status(sized).permissions() & std::filesystem::perms::owner_write) != std::filesystem::perms::none; }; @@ -142,43 +835,109 @@ int main() { Require(sceKernelFtruncate(descriptor, 3) == 0); FileStat times{}; const KernelTimeval past[2]{{1000000000, 0}, {1000000000, 500000}}; - Require(futimes_nid_postfix(descriptor, past) == 0); + Require(sceKernelFutimes(descriptor, past) == 0); Require(stat_nid_postfix(sized.string().c_str(), ×) == 0 && times.st_mtim.tv_sec == 1000000000); Require(futimes_nid_postfix(descriptor, nullptr) == 0); Require(stat_nid_postfix(sized.string().c_str(), ×) == 0 && times.st_mtim.tv_sec > 1000000000); const KernelTimeval overflow[2]{{0, 0}, {0, 1000000}}; + Require(sceKernelFutimes(descriptor, overflow) == static_cast(0x80020016u)); Require(futimes_nid_postfix(descriptor, overflow) == -1 && *__error_nid_postfix() == 22); const KernelTimeval negative[2]{{0, -1}, {0, 0}}; Require(futimes_nid_postfix(descriptor, negative) == -1 && *__error_nid_postfix() == 22); Require(std::fclose(native) == 0); Require(std::filesystem::file_size(sized) == 3); -#ifndef _WIN32 Require(sceKernelFchmod(descriptor, 0600) == static_cast(0x80020009u)); Require(fchmod_nid_postfix(descriptor, 0600) == -1 && *__error_nid_postfix() == 9); + Require(sceKernelFutimes(descriptor, nullptr) == static_cast(0x80020009u)); Require(futimes_nid_postfix(descriptor, nullptr) == -1 && *__error_nid_postfix() == 9); Require(fdatasync_nid_postfix(descriptor) == -1 && *__error_nid_postfix() == 9); -#endif + Require(sceKernelFdatasync(descriptor) == static_cast(0x80020009u)); + Require(fsync_nid_postfix(descriptor) == -1 && *__error_nid_postfix() == 9); + Require(sceKernelFsync(descriptor) == static_cast(0x80020009u)); const int socket = socket_nid_postfix(2, 2, 0); Require(socket >= 0); Require(sceKernelFchmod(socket, 0600) == static_cast(0x80020016u)); Require(fchmod_nid_postfix(socket, 0600) == -1 && *__error_nid_postfix() == 22); Require(futimes_nid_postfix(socket, nullptr) == -1 && *__error_nid_postfix() == 22); Require(fdatasync_nid_postfix(socket) == -1 && *__error_nid_postfix() == 22); + 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); Require(fdatasync_nid_postfix(socket) == -1 && *__error_nid_postfix() == 9); + Require(sceKernelFdatasync(socket) == static_cast(0x80020009u)); + Require(fsync_nid_postfix(socket) == -1 && *__error_nid_postfix() == 9); + 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(); - const auto missingName = (root / "missing.txt").string(); + const auto missing = root / "missing.txt"; + const auto missingName = missing.string(); const auto rootName = root.string(); { std::ofstream stream(present); stream << "posix"; } FileStat status{}; + Require(sceKernelStat("", &status) == static_cast(0x80020002u)); + 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; + std::filesystem::last_write_time(".", cwdWriteTime, restoreTimeError); + Require(emptyUtimes == static_cast(0x80020002u)); Require(stat_nid_postfix(presentName.c_str(), &status) == 0 && status.st_size == 5); Require(stat_nid_postfix(missingName.c_str(), &status) == -1 && *__error_nid_postfix() == 2); Require(sceKernelStat(missingName.c_str(), &status) == static_cast(0x80020002u)); Require(stat_nid_postfix("", &status) == -1 && *__error_nid_postfix() == 2); + Require(stat_nid_postfix((presentName + "/").c_str(), &status) == -1 && *__error_nid_postfix() == 20); + Require(sceKernelStat((presentName + "/").c_str(), &status) == static_cast(0x80020014u)); + Require(sceKernelStat("\xff\xfe", &status) == static_cast(0x80020002u)); + Require(stat_nid_postfix("\xff\xfe", &status) == -1 && *__error_nid_postfix() == 2); + FileStat dirStatus{}; + Require(stat_nid_postfix((rootName + "/").c_str(), &dirStatus) == 0 && (dirStatus.st_mode & 0170000) == 0040000); + const auto viaParent = rootName + "/../" + root.filename().string() + "/present.txt"; + Require(stat_nid_postfix(viaParent.c_str(), &status) == 0 && status.st_size == 5); Require(stat_nid_postfix(nullptr, &status) == -1 && *__error_nid_postfix() == 14); Require(stat_nid_postfix(presentName.c_str(), nullptr) == -1 && *__error_nid_postfix() == 14); FileStat linkStatus{}; @@ -194,45 +953,451 @@ int main() { std::filesystem::create_symlink("present.txt", link); Require(lstat_nid_postfix(link.string().c_str(), &linkStatus) == 0); Require((linkStatus.st_mode & 0170000) == 0120000 && linkStatus.st_size == 11); - Require(stat_nid_postfix(link.string().c_str(), &status) == 0 && (status.st_mode & 0170000) == 0100000); + 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)); + Require(open_nid_postfix(link.string().c_str(), noFollow, 0) == -1 && *__error_nid_postfix() == 31); + Require(open_nid_postfix(dangling.string().c_str(), noFollow | 0x201, 0600) == -1 && *__error_nid_postfix() == 31); + Require(!std::filesystem::exists(root / "missing.txt")); + Require(open_nid_postfix(dangling.string().c_str(), noFollow | 0xA01, 0600) == -1 && *__error_nid_postfix() == 17); + const int followed = open_nid_postfix(link.string().c_str(), 0, 0); + Require(followed >= 0 && close_nid_postfix(followed) == 0); + const int plain = open_nid_postfix(presentName.c_str(), noFollow, 0); + Require(plain >= 0 && close_nid_postfix(plain) == 0); Require(stat_nid_postfix(dangling.string().c_str(), &status) == -1 && *__error_nid_postfix() == 2); Require(lstat_nid_postfix((root / "present.txt" / "child").string().c_str(), &linkStatus) == -1 && *__error_nid_postfix() == 20); - Require(unlink_nid_postfix(link.string().c_str()) == 0 && unlink_nid_postfix(dangling.string().c_str()) == 0); + const auto viaMissing = rootName + "/missing/../present.txt"; + Require(stat_nid_postfix(viaMissing.c_str(), &status) == -1 && *__error_nid_postfix() == 2); + Require(open_nid_postfix(viaMissing.c_str(), 0, 0) == -1 && *__error_nid_postfix() == 2); + 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 && + 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) { + Require(GetLastError() == ERROR_PRIVILEGE_NOT_HELD); + std::fprintf(stderr, "Windows lstat link checks skipped: creating symbolic links needs Developer Mode or administrator rights\n"); + } else { + const auto targetDirectory = root / "target-directory"; + 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(), 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); + 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)); #endif + FileStat identity{}; + FileStat byDescriptor{}; + const int identityDescriptor = open_nid_postfix(presentName.c_str(), 0, 0); + Require(identityDescriptor >= 0 && stat_nid_postfix(presentName.c_str(), &identity) == 0); + Require(sceKernelFstat(identityDescriptor, &byDescriptor) == 0 && close_nid_postfix(identityDescriptor) == 0); + Require(identity.st_ino != 0 && byDescriptor.st_dev == identity.st_dev && byDescriptor.st_ino == identity.st_ino); + const auto sibling = root / "sibling.txt"; + { std::ofstream stream(sibling); stream << "other"; } + FileStat siblingStatus{}; + Require(stat_nid_postfix(sibling.string().c_str(), &siblingStatus) == 0); + Require(siblingStatus.st_dev == identity.st_dev && siblingStatus.st_ino != identity.st_ino); + Require(unlink_nid_postfix(sibling.string().c_str()) == 0); + const auto hardLink = root / "hardlink.txt"; + std::error_code linkError; + std::filesystem::create_hard_link(present, hardLink, linkError); + if (!linkError) { + FileStat linked{}; + Require(stat_nid_postfix(hardLink.string().c_str(), &linked) == 0); + Require(linked.st_dev == identity.st_dev && linked.st_ino == identity.st_ino && linked.st_nlink == 2); + Require(unlink_nid_postfix(hardLink.string().c_str()) == 0); + } const int opened = open_nid_postfix(presentName.c_str(), 0, 0); Require(opened >= 0 && close_nid_postfix(opened) == 0); const int reopened = _open_nid_postfix(presentName.c_str(), 0); Require(reopened >= 0 && close_nid_postfix(reopened) == 0); + const int locked = open_nid_postfix(presentName.c_str(), 0, 0); + const int other = open_nid_postfix(presentName.c_str(), 0, 0); + Require(locked >= 0 && other >= 0); + Require(flock_nid_postfix(locked, 8) == 0); + Require(flock_nid_postfix(locked, 2 | 4) == 0); + Require(flock_nid_postfix(locked, 2 | 4) == 0); + Require(flock_nid_postfix(locked, 1 | 4) == 0); + Require(flock_nid_postfix(other, 1 | 4) == 0); + Require(flock_nid_postfix(other, 8) == 0); + Require(flock_nid_postfix(locked, 1 | 4) == 0); + Require(flock_nid_postfix(locked, 2 | 4) == 0); + Require(flock_nid_postfix(locked, 8) == 0); + Require(flock_nid_postfix(locked, 8) == 0); + 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); + 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); + Require(flock_nid_postfix(recycled, 2 | 4) == 0 && sceKernelClose(recycled) == 0); + const int kernelRecycled = sceKernelOpen(presentName.c_str(), 0, 0); + Require(kernelRecycled >= 0 && flock_nid_postfix(kernelRecycled, 8) == 0); + Require(flock_nid_postfix(other, 0) == -1 && *__error_nid_postfix() == 9); + Require(flock_nid_postfix(other, 4 | 0x10) == -1 && *__error_nid_postfix() == 9); + Require(flock_nid_postfix(kernelRecycled, 2 | 4 | 0x10) == 0); + Require(flock_nid_postfix(kernelRecycled, 8 | 2) == 0); + Require(flock_nid_postfix(other, 2 | 4) == 0 && flock_nid_postfix(other, 8) == 0); + Require(sceKernelClose(kernelRecycled) == 0 && close_nid_postfix(other) == 0); + const int holder = open_nid_postfix(presentName.c_str(), 0, 0); + const int contender = open_nid_postfix(presentName.c_str(), 0, 0); + Require(holder >= 0 && contender >= 0 && flock_nid_postfix(holder, 2 | 4) == 0); + Require(flock_nid_postfix(contender, 2 | 4) == -1 && *__error_nid_postfix() == 35); + Require(flock_nid_postfix(contender, 1 | 4) == -1 && *__error_nid_postfix() == 35); + Require(flock_nid_postfix(holder, 1 | 4) == 0 && flock_nid_postfix(contender, 1 | 4) == 0); + Require(flock_nid_postfix(contender, 2 | 4) == -1 && *__error_nid_postfix() == 35); + Require(flock_nid_postfix(holder, 8) == 0 && flock_nid_postfix(contender, 2 | 4) == 0); + Require(flock_nid_postfix(contender, 8) == 0); + Require(flock_nid_postfix(holder, 1 | 2 | 4 | 0x10) == 0); + Require(flock_nid_postfix(contender, 1 | 4) == -1 && *__error_nid_postfix() == 35); + Require(flock_nid_postfix(holder, 8) == 0); + Require(close_nid_postfix(contender) == 0 && close_nid_postfix(holder) == 0); Require(open_nid_postfix(missingName.c_str(), 0, 0) == -1 && *__error_nid_postfix() == 2); Require(_open_nid_postfix(missingName.c_str(), 0) == -1 && *__error_nid_postfix() == 2); Require(sceKernelOpen(missingName.c_str(), 0, 0) == static_cast(0x80020002u)); Require(open_nid_postfix(presentName.c_str(), 0x0a02, 0644) == -1 && *__error_nid_postfix() == 17); Require(_open_nid_postfix(presentName.c_str(), 0x0a02, 0644) == -1 && *__error_nid_postfix() == 17); + Require(open_nid_postfix(rootName.c_str(), 0x0a02, 0644) == -1 && *__error_nid_postfix() == 17); + Require(open_nid_postfix(rootName.c_str(), 0x0a00, 0644) == -1 && *__error_nid_postfix() == 17); + Require(open_nid_postfix(rootName.c_str(), 0x0200, 0644) == -1 && *__error_nid_postfix() == 21); + Require(open_nid_postfix(rootName.c_str(), 0x0002, 0) == -1 && *__error_nid_postfix() == 21); + Require(open_nid_postfix(rootName.c_str(), 0x0001, 0) == -1 && *__error_nid_postfix() == 21); + const int syncedDirectory = open_nid_postfix(rootName.c_str(), 0, 0); + Require(syncedDirectory >= 0 && sceKernelFsync(syncedDirectory) == 0 && fdatasync_nid_postfix(syncedDirectory) == 0); + Require(sceKernelFdatasync(syncedDirectory) == 0 && close_nid_postfix(syncedDirectory) == 0); + const int syncedReader = open_nid_postfix(presentName.c_str(), 0, 0); + Require(syncedReader >= 0 && sceKernelFsync(syncedReader) == 0 && fdatasync_nid_postfix(syncedReader) == 0); + Require(sceKernelFdatasync(syncedReader) == 0 && close_nid_postfix(syncedReader) == 0); + Require(sceKernelChmod_nid_postfix(presentName.c_str(), 0400) == 0); + const int syncedLocked = open_nid_postfix(presentName.c_str(), 0, 0); + Require(syncedLocked >= 0 && sceKernelFsync(syncedLocked) == 0 && close_nid_postfix(syncedLocked) == 0); + Require(sceKernelChmod_nid_postfix(presentName.c_str(), 0600) == 0); + const int directoryDescriptor = open_nid_postfix(rootName.c_str(), 0, 0); + Require(directoryDescriptor >= 0); + char directoryBytes[8]{}; + struct { void* base; std::size_t length; } directoryVector[2] = {{directoryBytes, 3}, {directoryBytes + 3, 5}}; + Require(read_nid_postfix(directoryDescriptor, directoryBytes, sizeof(directoryBytes)) == -1 && *__error_nid_postfix() == 21); + Require(sceKernelRead(directoryDescriptor, directoryBytes, sizeof(directoryBytes)) == static_cast(0x80020015u)); + Require(sceKernelReadv(directoryDescriptor, directoryVector, 2) == static_cast(0x80020015u)); + Require(sceKernelPread(directoryDescriptor, directoryBytes, sizeof(directoryBytes), 0) == static_cast(0x80020015u)); + Require(sceKernelPreadv(directoryDescriptor, directoryVector, 2, 0) == static_cast(0x80020015u)); + Require(sceKernelPwrite(directoryDescriptor, "x", 1, 0) == static_cast(0x80020009u)); + std::vector listing(4096); + Require(sceKernelGetdents(directoryDescriptor, listing.data(), static_cast(listing.size())) > 0); + 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); + std::vector entries(4096); + for (const auto& directoryName : {std::string("/"), std::string("."), rootName + "/", rootName + "/.."}) { + const int listed = sceKernelOpen(directoryName.c_str(), 0x20000, 0); + Require(listed >= 0); + int read = sceKernelGetdents(listed, entries.data(), static_cast(entries.size())); + Require(read > 0); + while (read > 0) read = sceKernelGetdents(listed, entries.data(), static_cast(entries.size())); + Require(read == 0 && sceKernelClose(listed) == 0); + } + Require(open_nid_postfix(presentName.c_str(), 0x20000, 0) == -1 && *__error_nid_postfix() == 20); + Require(sceKernelOpen(presentName.c_str(), 0x20000, 0) == static_cast(0x80020014u)); + 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)); Require(open_nid_postfix(nullptr, 0, 0) == -1 && *__error_nid_postfix() == 14); Require(_open_nid_postfix(nullptr, 0) == -1 && *__error_nid_postfix() == 14); + const auto created = root / "created.txt"; + const int createdDescriptor = creat_nid_postfix(created.string().c_str(), 0644); + Require(createdDescriptor >= 0); + Require(write_nid_postfix(createdDescriptor, "created", 7) == 7 && close_nid_postfix(createdDescriptor) == 0); + Require(std::filesystem::file_size(created) == 7); + const int truncatedDescriptor = creat_nid_postfix(created.string().c_str(), 0600); + Require(truncatedDescriptor >= 0 && close_nid_postfix(truncatedDescriptor) == 0); + Require(std::filesystem::file_size(created) == 0); + Require(unlink_nid_postfix(created.string().c_str()) == 0); + Require(creat_nid_postfix((root / "absent" / "child.txt").string().c_str(), 0644) == -1 && + *__error_nid_postfix() == 2); + Require(creat_nid_postfix(rootName.c_str(), 0644) == -1 && *__error_nid_postfix() == 21); + Require(creat_nid_postfix("", 0644) == -1 && *__error_nid_postfix() == 2); + Require(creat_nid_postfix(nullptr, 0644) == -1 && *__error_nid_postfix() == 14); Require(rmdir_nid_postfix(rootName.c_str()) == -1 && *__error_nid_postfix() == 66); Require(sceKernelRmdir(rootName.c_str()) == static_cast(0x80020042u)); Require(rmdir_nid_postfix(presentName.c_str()) == -1 && *__error_nid_postfix() == 20); Require(rmdir_nid_postfix(missingName.c_str()) == -1 && *__error_nid_postfix() == 2); + const auto lockedFile = [&](const char* name) { + const auto locked = root / name; + { std::ofstream stream(locked); stream << "removable"; } + Require(sceKernelChmod_nid_postfix(locked.string().c_str(), 0400) == 0); + return locked; + }; + const auto lockedPosix = lockedFile("locked-posix"); + Require(unlink_nid_postfix(lockedPosix.string().c_str()) == 0 && !std::filesystem::exists(lockedPosix)); + const auto lockedKernel = lockedFile("locked-kernel"); + Require(sceKernelUnlink(lockedKernel.string().c_str()) == 0 && !std::filesystem::exists(lockedKernel)); + const auto lockedDirectory = [&](const char* name) { + const auto locked = root / name; + Require(mkdir_nid_postfix(locked.string().c_str(), 0700) == 0); + Require(sceKernelChmod_nid_postfix(locked.string().c_str(), 0555) == 0); + return locked; + }; + const auto lockedDirectoryPosix = lockedDirectory("locked-directory-posix"); + Require(rmdir_nid_postfix(lockedDirectoryPosix.string().c_str()) == 0 && !std::filesystem::exists(lockedDirectoryPosix)); + const auto lockedDirectoryKernel = lockedDirectory("locked-directory-kernel"); + Require(sceKernelRmdir(lockedDirectoryKernel.string().c_str()) == 0 && !std::filesystem::exists(lockedDirectoryKernel)); + const auto underFile = presentName + "/child"; + const auto deepUnderFile = presentName + "/child/leaf"; + Require(mkdir_nid_postfix(underFile.c_str(), 0700) == -1 && *__error_nid_postfix() == 20); + Require(mkdir_nid_postfix(deepUnderFile.c_str(), 0700) == -1 && *__error_nid_postfix() == 20); + Require(sceKernelMkdir(underFile.c_str(), 0700) == static_cast(0x80020014u)); + Require(sceKernelMkdir(deepUnderFile.c_str(), 0700) == static_cast(0x80020014u)); + Require(rmdir_nid_postfix(underFile.c_str()) == -1 && *__error_nid_postfix() == 20); + Require(sceKernelRmdir(deepUnderFile.c_str()) == static_cast(0x80020014u)); + Require(rmdir_nid_postfix((presentName + "/").c_str()) == -1 && *__error_nid_postfix() == 20); + 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"; + const auto danglingDirectoryLink = root / "dangling-directory-link"; + Require(std::filesystem::create_directory(linkedDirectory)); + std::filesystem::create_directory_symlink("linked-directory", directoryLink); + std::filesystem::create_directory_symlink("missing-directory", danglingDirectoryLink); + Require(rmdir_nid_postfix(directoryLink.string().c_str()) == -1 && *__error_nid_postfix() == 20); + Require(sceKernelRmdir(directoryLink.string().c_str()) == static_cast(0x80020014u)); + Require(rmdir_nid_postfix(danglingDirectoryLink.string().c_str()) == -1 && *__error_nid_postfix() == 20); + Require(std::filesystem::is_symlink(directoryLink) && std::filesystem::is_directory(linkedDirectory)); + Require(unlink_nid_postfix(directoryLink.string().c_str()) == 0 && unlink_nid_postfix(danglingDirectoryLink.string().c_str()) == 0); + Require(rmdir_nid_postfix(linkedDirectory.string().c_str()) == 0 && !std::filesystem::exists(linkedDirectory)); +#endif + const auto dotDirectory = root / "dot-directory"; + Require(std::filesystem::create_directory(dotDirectory)); + const auto dotDirectoryName = dotDirectory.string() + "/."; + const auto dotDirectoryTrailingName = dotDirectoryName + "/"; + Require(rmdir_nid_postfix(dotDirectoryName.c_str()) == -1 && *__error_nid_postfix() == 22); + Require(sceKernelRmdir(dotDirectoryName.c_str()) == static_cast(0x80020016u)); + Require(remove_nid_postfix(dotDirectoryName.c_str()) == -1 && *__error_nid_postfix() == 22); + Require(rmdir_nid_postfix(dotDirectoryTrailingName.c_str()) == -1 && *__error_nid_postfix() == 22); + Require(sceKernelRmdir(dotDirectoryTrailingName.c_str()) == static_cast(0x80020016u)); + Require(remove_nid_postfix(dotDirectoryTrailingName.c_str()) == -1 && *__error_nid_postfix() == 22); + Require(std::filesystem::is_directory(dotDirectory)); + const auto dotFileName = presentName + "/."; + Require(rmdir_nid_postfix(dotFileName.c_str()) == -1 && *__error_nid_postfix() == 20); + Require(sceKernelRmdir(dotFileName.c_str()) == static_cast(0x80020014u)); + Require(remove_nid_postfix(dotFileName.c_str()) == -1 && *__error_nid_postfix() == 20); + const auto dotMissingName = missingName + "/."; + Require(rmdir_nid_postfix(dotMissingName.c_str()) == -1 && *__error_nid_postfix() == 2); + Require(sceKernelRmdir(dotMissingName.c_str()) == static_cast(0x80020002u)); + Require(remove_nid_postfix(dotMissingName.c_str()) == -1 && *__error_nid_postfix() == 2); + const auto missingParentLeaf = rootName + "/missing/leaf"; + Require(sceKernelMkdir(missingParentLeaf.c_str(), 0700) == static_cast(0x80020002u)); +#ifndef _WIN32 + 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); + Require(sceKernelMkdir(invalidNameUnderFile.c_str(), 0700) == static_cast(0x80020014u)); + const auto trailingPosix = root / "trailing_posix"; + const auto trailingKernel = root / "trailing_kernel"; + const auto trailingPosixName = trailingPosix.string() + "/"; + const auto trailingKernelName = trailingKernel.string() + "/"; + Require(mkdir_nid_postfix(trailingPosixName.c_str(), 0700) == 0 && std::filesystem::is_directory(trailingPosix)); + Require(sceKernelMkdir(trailingKernelName.c_str(), 0700) == 0 && std::filesystem::is_directory(trailingKernel)); + Require(rmdir_nid_postfix(trailingPosix.string().c_str()) == 0 && sceKernelRmdir(trailingKernel.string().c_str()) == 0); + Require(!std::filesystem::exists(trailingPosix) && !std::filesystem::exists(trailingKernel)); + Require(std::filesystem::is_regular_file(present)); Require(rmdir_nid_postfix("") == -1 && *__error_nid_postfix() == 2); Require(rmdir_nid_postfix(nullptr) == -1 && *__error_nid_postfix() == 14); Require(unlink_nid_postfix(missingName.c_str()) == -1 && *__error_nid_postfix() == 2); Require(sceKernelUnlink(missingName.c_str()) == static_cast(0x80020002u)); + Require(sceKernelUnlink("") == static_cast(0x80020002u)); Require(unlink_nid_postfix("") == -1 && *__error_nid_postfix() == 2); Require(unlink_nid_postfix(nullptr) == -1 && *__error_nid_postfix() == 14); + Require(unlink_nid_postfix(rootName.c_str()) == -1 && *__error_nid_postfix() == 1 && std::filesystem::is_directory(root)); + Require(sceKernelUnlink(rootName.c_str()) == static_cast(0x80020001u)); const int closable = sceKernelOpen(presentName.c_str(), 0, 0); Require(closable >= 0 && sceKernelClose(closable) == 0); Require(sceKernelClose(closable) == static_cast(0x80020009u)); Require(sceKernelClose(-1) == static_cast(0x80020009u)); + for (const char* device : {"/dev/urandom", "/dev/random"}) { + const int random = open_nid_postfix(device, 0, 0); + Require(random >= 0); + unsigned char first[64]{}; + unsigned char second[64]{}; + Require(read_nid_postfix(random, first, sizeof(first)) == sizeof(first)); + 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"; Require(std::filesystem::create_directory(empty)); Require(rmdir_nid_postfix(empty.string().c_str()) == 0 && !std::filesystem::exists(empty)); + Require(rmdir_nid_postfix(dotDirectory.string().c_str()) == 0 && !std::filesystem::exists(dotDirectory)); Require(remove_nid_postfix(root.string().c_str()) == 0); Require(!std::filesystem::exists(root)); } diff --git a/core/libs/tests/GuestFiopen.cpp b/core/libs/tests/GuestFiopen.cpp index 6c2636ae3d..276459ec07 100644 --- a/core/libs/tests/GuestFiopen.cpp +++ b/core/libs/tests/GuestFiopen.cpp @@ -10,6 +10,7 @@ extern "C" { FileStream* APS5_VABI _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(const char*, int, int); FileStream* APS5_VABI fopen_nid_postfix(const char*, const char*); +int APS5_VABI fopen_s_nid_postfix(FileStream**, const char*, const char*); int APS5_VABI fclose_nid_postfix(FileStream*); std::int64_t APS5_VABI ftello_nid_postfix(FileStream*); } @@ -24,6 +25,8 @@ static void Check(bool value, int line) { constexpr int In = 0x01, Out = 0x02, Ate = 0x04, App = 0x08, Trunc = 0x10, Nocreate = 0x20, Noreplace = 0x40, Binary = 0x80; +constexpr int GuestEinval = 22; +constexpr int GuestEnoent = 2; static FileStream* Open(const char* name, int mode) { return _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(name, mode, 0x1b6); @@ -66,6 +69,21 @@ int main() { Write(Open(file, Out), "abc"); Require(Contents(file) == "abc"); + FileStream* checkedStream = &_Stdout_nid_postfix; + Require(fopen_s_nid_postfix(&checkedStream, nullptr, "rb") == GuestEinval); + Require(checkedStream == nullptr); + checkedStream = &_Stderr_nid_postfix; + Require(fopen_s_nid_postfix(&checkedStream, file, nullptr) == GuestEinval); + Require(checkedStream == nullptr); + Require(fopen_s_nid_postfix(nullptr, file, "rb") == GuestEinval); + checkedStream = &_Stdout_nid_postfix; + Require(fopen_s_nid_postfix(&checkedStream, file, "rb") == 0); + Require(checkedStream != nullptr); + Require(fclose_nid_postfix(checkedStream) == 0); + checkedStream = &_Stdout_nid_postfix; + Require(fopen_s_nid_postfix(&checkedStream, missing, "rb") == GuestEnoent); + Require(checkedStream == nullptr); + auto* stream = Open(file, In); Require(stream != nullptr && std::fgetc(stream->GetHandle()) == 'a'); fclose_nid_postfix(stream); @@ -94,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)); @@ -131,6 +152,14 @@ int main() { Require(stream != nullptr && std::fgetc(stream->GetHandle()) == 'k'); fclose_nid_postfix(stream); +#ifndef _WIN32 + auto* full = std::fopen("/dev/full", "wb"); + Require(full != nullptr); + auto* fullStream = new FileStream(full, true); + Require(std::fwrite("buffered", 1, 8, full) == 8); + Require(fclose_nid_postfix(fullStream) == EOF); +#endif + std::filesystem::remove_all(dir); return 0; } diff --git a/core/libs/tests/GuestFlexibleMemoryBudget.cpp b/core/libs/tests/GuestFlexibleMemoryBudget.cpp new file mode 100644 index 0000000000..cba381e734 --- /dev/null +++ b/core/libs/tests/GuestFlexibleMemoryBudget.cpp @@ -0,0 +1,251 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceKernelConfiguredFlexibleMemorySize(std::size_t*); +int APS5_VABI sceKernelAvailableFlexibleMemorySize(std::size_t*); +int APS5_VABI sceKernelMapFlexibleMemory(void**, std::size_t, int, int); +int APS5_VABI sceKernelReleaseFlexibleMemory(void*, std::size_t); +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()) { + if (!condition) { + std::fprintf(stderr, "Flexible memory budget check failed at %s:%u\n", location.file_name(), static_cast(location.line())); + std::abort(); + } +} + +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]; + Require(sceKernelConfiguredFlexibleMemorySize(nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelAvailableFlexibleMemorySize(nullptr) == SCE_KERNEL_ERROR_EINVAL); + std::size_t configured = 0; + if (mode == "invalid") { + bool rejected = false; + try { sceKernelConfiguredFlexibleMemorySize(&configured); } + catch (const std::runtime_error&) { rejected = true; } + Require(rejected && configured == 0); + return 0; + } + const std::size_t expected = (mode == "declared" ? 800u : mode == "limited" ? 16u : 448u) * 1024u * 1024u; + Require(sceKernelConfiguredFlexibleMemorySize(&configured) == 0 && configured == expected); + std::size_t before = 0; + Require(sceKernelAvailableFlexibleMemorySize(&before) == 0); + Require(before + GuestImageSegmentBytes_nid_no_patch() == configured); + void* module = nullptr; + if (mode == "declared") { + const std::filesystem::path fixture = GUEST_FLEXIBLE_MEMORY_MODULE; + AddPathAlias_nid_no_patch("budget_module", fixture.parent_path().string().c_str()); + const auto path = "/budget_module/" + fixture.filename().generic_string(); + module = dlopen_nid_postfix(path.c_str(), 2); + if (!module) std::fprintf(stderr, "Cannot load budget fixture: %s\n", dlerror_nid_postfix()); + Require(module != nullptr); + Require(sceKernelAvailableFlexibleMemorySize(&before) == 0); + Require(GuestImageSegmentBytes_nid_no_patch() > 448u * 1024u * 1024u); + Require(before + GuestImageSegmentBytes_nid_no_patch() == configured && before > 0x80000); + } + constexpr std::size_t length = 0x80000; + void* mapped = nullptr; + Require(sceKernelMapFlexibleMemory(&mapped, length, 3, 0) == 0 && mapped != nullptr); + static_cast(mapped)[length - 1] = 7; + std::size_t available = 0; + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before - length); + Require(sceKernelReleaseFlexibleMemory(mapped, length / 2) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before - length / 2); + Require(sceKernelReleaseFlexibleMemory(static_cast(mapped) + length / 2, length / 2) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); + 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); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0); + Require(available > before && available + GuestImageSegmentBytes_nid_no_patch() == configured); + } + std::puts("Flexible memory budget checks passed"); +} diff --git a/core/libs/tests/GuestFlexibleMemoryModule.cpp b/core/libs/tests/GuestFlexibleMemoryModule.cpp new file mode 100644 index 0000000000..1bf220a810 --- /dev/null +++ b/core/libs/tests/GuestFlexibleMemoryModule.cpp @@ -0,0 +1,3 @@ +extern "C" { +unsigned char guestFlexibleMemoryModuleData[512u * 1024u * 1024u] = {}; +} diff --git a/core/libs/tests/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 new file mode 100644 index 0000000000..fcf48171a6 --- /dev/null +++ b/core/libs/tests/GuestFminf.cpp @@ -0,0 +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, const char* name, int line) { + if (!condition) { + std::fprintf(stderr, "%s check failed at line %d\n", name, line); + std::abort(); + } +} +#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() { + 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/GuestFnmatch.cpp b/core/libs/tests/GuestFnmatch.cpp new file mode 100644 index 0000000000..32b4e5b16e --- /dev/null +++ b/core/libs/tests/GuestFnmatch.cpp @@ -0,0 +1,79 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +extern "C" int APS5_VABI fnmatch_nid_postfix(const char*, const char*, int); +struct Case { + const char* pattern; + const char* string; + int flags; + int result; +}; +int main() { + static const Case cases[] = { + {"*.txt", "notes.txt", 0x00, 0}, + {"*.txt", "notes.TXT", 0x00, 1}, + {"*.txt", "notes.TXT", 0x10, 0}, + {"a?c", "abc", 0x00, 0}, + {"a?c", "ac", 0x00, 1}, + {"a[bc]d", "acd", 0x00, 0}, + {"a[!bc]d", "acd", 0x00, 1}, + {"a[^bc]d", "aed", 0x00, 0}, + {"a[a-c]d", "abd", 0x00, 0}, + {"a[a-c]d", "add", 0x00, 1}, + {"a[]]d", "a]d", 0x00, 0}, + {"a[a-]d", "a-d", 0x00, 0}, + {"[A-C]x", "bx", 0x10, 0}, + {"*", "dir/file", 0x00, 0}, + {"*", "dir/file", 0x02, 1}, + {"dir/*", "dir/file", 0x02, 0}, + {"*/file", "dir/file", 0x02, 0}, + {"dir?file", "dir/file", 0x02, 1}, + {"dir[/]file", "dir/file", 0x02, 1}, + {"*", ".hidden", 0x04, 1}, + {".*", ".hidden", 0x04, 0}, + {"?hidden", ".hidden", 0x04, 1}, + {"dir/*", "dir/.hidden", 0x06, 1}, + {"dir/*", "dir/.hidden", 0x04, 0}, + {"dir/.*", "dir/.hidden", 0x06, 0}, + {"dir", "dir/sub/file", 0x08, 0}, + {"dir/*", "dir/sub/file", 0x0a, 0}, + {"dir", "dirx", 0x08, 1}, + {"\\*", "*", 0x00, 0}, + {"\\*", "x", 0x00, 1}, + {"\\*", "\\*", 0x01, 0}, + {"a\\", "a", 0x00, 1}, + {"a\\", "a\\", 0x01, 0}, + {"a*b*c", "axxbyyc", 0x00, 0}, + {"a*b*c", "axxbyy", 0x00, 1}, + {"*a*", "bbb", 0x00, 1}, + {"**a", "ba", 0x00, 0}, + {"", "", 0x00, 0}, + {"", "x", 0x00, 1}, + {"x", "", 0x00, 1}, + {"[", "[", 0x00, 0}, + {"[a", "[a", 0x00, 0}, + {"a[", "a[", 0x00, 0}, + {"[!]x", "x", 0x00, 1}, + {"*/", "dir/", 0x02, 0}, + {"*/", "dir", 0x02, 1}, + {"[[:digit:]]", "7", 0x00, 1}, + {"[[:digit:]]", "d]", 0x00, 0}, + {"[[:digit:]]", ":]", 0x00, 0}, + {"a[[:alpha:]]", "ab", 0x00, 1}, + }; + for (const Case& entry : cases) { + if (fnmatch_nid_postfix(entry.pattern, entry.string, entry.flags) != entry.result) { + std::fprintf(stderr, "fnmatch(\"%s\", \"%s\", 0x%x) != %d\n", entry.pattern, entry.string, entry.flags, entry.result); + std::abort(); + } + } + bool threw = false; + try { + fnmatch_nid_postfix(nullptr, "x", 0); + } catch (const std::invalid_argument&) { + threw = true; + } + if (!threw) std::abort(); + return 0; +} diff --git a/core/libs/tests/GuestFont.cpp b/core/libs/tests/GuestFont.cpp index 97e814da71..80184223b8 100644 --- a/core/libs/tests/GuestFont.cpp +++ b/core/libs/tests/GuestFont.cpp @@ -1,10 +1,15 @@ #include "prx/libSceFont/include/FontDriver.hpp" +#include "tests/FontFixture.hpp" #include #include #include #include +#include +#include #include #include +#include +#include #include #include @@ -23,7 +28,14 @@ int APS5_VABI sceFontSupportSystemFonts(FontLibrary); int APS5_VABI sceFontSupportExternalFonts(FontLibrary, std::uint32_t, std::uint32_t); int APS5_VABI sceFontOpenFontSet(FontLibrary, std::uint32_t, std::uint32_t, const FontOpenDetail*, FontHandle*); int APS5_VABI sceFontOpenFontMemory(FontLibrary, const void*, std::uint32_t, const FontOpenDetail*, FontHandle*); +int APS5_VABI sceFontOpenFontFile(FontLibrary, const char*, std::uint32_t, const FontOpenDetail*, FontHandle*); int APS5_VABI sceFontCloseFont(FontHandle); +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); @@ -37,10 +49,33 @@ 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, int, int*); +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*); +int APS5_VABI sceFontDestroyString(FontString*); +FontTextCharacter* APS5_VABI sceFontStringRefersTextCharacters(FontString, std::uint32_t*); +int APS5_VABI sceFontWritingInit(FontWriting*, FontString, const FontTextCharacter*); +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(); +int APS5_VABI sceFontFtSupportCid(); +int APS5_VABI sceFontFtSupportOpenType(); +int APS5_VABI sceFontFtSupportOpenTypeOtf(); +int APS5_VABI sceFontFtSupportOpenTypeTtf(); +int APS5_VABI sceFontFtSupportPcf(); +int APS5_VABI sceFontFtSupportPfr(); +int APS5_VABI sceFontFtSupportTrueType(); +int APS5_VABI sceFontFtSupportType1(); +int APS5_VABI sceFontFtSupportType42(); +int APS5_VABI sceFontFtSupportWinFonts(); +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) { @@ -51,6 +86,14 @@ static void Check(bool value, int line) { } #define Require(value) Check((value), __LINE__) +static void SetFontDirectory(const std::filesystem::path& directory) { +#ifdef _WIN32 + Require(_putenv_s("ANYPS5_SYSTEM_FONTS", directory.string().c_str()) == 0); +#else + Require(::setenv("ANYPS5_SYSTEM_FONTS", directory.string().c_str(), 1) == 0); +#endif +} + static int allocations = 0; static void* APS5_VABI Allocate(void*, std::uint32_t size) { ++allocations; @@ -65,41 +108,8 @@ 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 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, {}); @@ -112,17 +122,94 @@ static std::vector KerningFont() { }); } +struct ParsedText { + const char* text; + std::size_t at; + FontHandle font; +}; + +static std::int32_t APS5_VABI ParseText(FontTextSource* source, void** order, FontTextParseResult* result) { + auto* parsed = static_cast(source->textObject); + *order = const_cast(parsed->text + parsed->at); + if (parsed->text[parsed->at] == 0) { + result->Terminate.terminateCode = 0; + return 0; + } + result->FontCode.font = parsed->font; + result->FontCode.code = static_cast(parsed->text[parsed->at++]); + 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; } int main() { constexpr std::uint32_t SystemFontSet = 0x18070043u; + const std::filesystem::path fontRoot = std::filesystem::temp_directory_path() / ("anyps5_guest_font-" + std::to_string(std::random_device{}())); + std::filesystem::remove_all(fontRoot); + std::filesystem::create_directories(fontRoot); + SetFontDirectory(fontRoot); const FontMemoryInterface iface{Allocate, Release, nullptr, nullptr, nullptr, nullptr}; FontMemory memory{}; Require(sceFontMemoryInit(&memory, nullptr, 0, &iface, nullptr, nullptr, nullptr) == SCE_FONT_OK); Require(sceFontSelectLibraryFt(0) != nullptr && sceFontSelectLibraryFt(1) == nullptr); Require(sceFontSelectRendererFt(0) != nullptr && sceFontSelectRendererFt(1) == nullptr); + Require(sceFontFtSupportBdf() == 0 && sceFontFtSupportCid() == 0 && sceFontFtSupportOpenType() == 0 && sceFontFtSupportOpenTypeOtf() == 0 && sceFontFtSupportOpenTypeTtf() == 0 && sceFontFtSupportPcf() == 0 && sceFontFtSupportPfr() == 0 && sceFontFtSupportTrueType() == 0 && sceFontFtSupportType1() == 0 && sceFontFtSupportType42() == 0 && sceFontFtSupportWinFonts() == 0); FontLibrary library = nullptr; Require(sceFontCreateLibrary(&memory, nullptr, &library) == SCE_FONT_ERROR_INVALID_PARAMETER && library == nullptr); @@ -184,11 +271,46 @@ int main() { Require(sceFontOpenFontMemory(library, notAFont, sizeof(notAFont), nullptr, &font) == SCE_FONT_ERROR_NO_SUPPORT_FUNCTION && font == nullptr); Require(sceFontSupportExternalFonts(library, 4, 0x52) == SCE_FONT_OK); Require(sceFontSupportExternalFonts(library, 4, 0x52) == SCE_FONT_ERROR_ALREADY_SPECIFIED); + font = reinterpret_cast(&memory); + Require(sceFontOpenFontFile(library, "anyps5-missing-font-parent/../font.ttf", 1, nullptr, &font) == SCE_FONT_ERROR_FS_OPEN_FAILED && font == nullptr); Require(sceFontOpenFontMemory(library, nullptr, 0, nullptr, &font) == SCE_FONT_ERROR_INVALID_PARAMETER && font == nullptr); Require(sceFontOpenFontMemory(library, notAFont, sizeof(notAFont), nullptr, &font) == SCE_FONT_ERROR_NO_SUPPORT_FORMAT && font == nullptr); const std::vector fontData = EmptyGlyphFont(); 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(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); + 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); std::uint32_t hDpi = 1; std::uint32_t vDpi = 1; Require(sceFontGetResolutionDpi(font, &hDpi, &vDpi) == SCE_FONT_OK && hDpi == 72 && vDpi == 72); @@ -251,10 +373,30 @@ 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); + int glyphAttribute = -1; + Require(sceFontGlyphDefineAttribute(glyph, 0x11, &glyphAttribute) == SCE_FONT_OK && glyphAttribute == 0); + Require(sceFontGlyphGetAttribute(glyph, 0x11, &glyphAttribute) == SCE_FONT_OK && glyphAttribute == 0x11); + glyphAttribute = -1; + Require(sceFontGlyphDefineAttribute(glyph, 0, &glyphAttribute) == SCE_FONT_OK && glyphAttribute == 0x11); + Require(sceFontGlyphDefineAttribute(glyph, 5, nullptr) == SCE_FONT_OK); + glyphAttribute = -1; + Require(sceFontGlyphGetAttribute(glyph, 0, &glyphAttribute) == SCE_FONT_OK && glyphAttribute == 5); + Require(sceFontGlyphGetAttribute(glyph, 0x11, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + glyphAttribute = -1; + Require(sceFontGlyphDefineAttribute(nullptr, 0x11, &glyphAttribute) == SCE_FONT_ERROR_INVALID_GLYPH && glyphAttribute == -1); + Require(sceFontGlyphGetAttribute(nullptr, 0x11, &glyphAttribute) == SCE_FONT_ERROR_INVALID_GLYPH && glyphAttribute == -1); FontGlyphOpaque notAGlyph{}; - Require(sceFontGlyphDefineAttribute(¬AGlyph, 0x11, 0) == SCE_FONT_ERROR_INVALID_GLYPH); + Require(sceFontGlyphDefineAttribute(¬AGlyph, 0x11, &glyphAttribute) == SCE_FONT_ERROR_INVALID_GLYPH); + Require(sceFontGlyphGetAttribute(¬AGlyph, 0x11, &glyphAttribute) == SCE_FONT_ERROR_INVALID_GLYPH); + FontGlyph otherGlyph = nullptr; + Require(sceFontGenerateCharGlyph(font, 'V', nullptr, &otherGlyph) == SCE_FONT_OK && otherGlyph != nullptr); + glyphAttribute = -1; + Require(sceFontGlyphGetAttribute(otherGlyph, 0, &glyphAttribute) == SCE_FONT_OK && glyphAttribute == 0); + Require(sceFontGlyphDefineAttribute(otherGlyph, 9, &glyphAttribute) == SCE_FONT_OK && glyphAttribute == 0); + glyphAttribute = -1; + Require(sceFontGlyphGetAttribute(otherGlyph, 0, &glyphAttribute) == SCE_FONT_OK && glyphAttribute == 9); + Require(sceFontGlyphGetAttribute(glyph, 0, &glyphAttribute) == SCE_FONT_OK && glyphAttribute == 5); + Require(sceFontDeleteGlyph(&memory, &otherGlyph) == SCE_FONT_OK && otherGlyph == nullptr); 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)); @@ -278,6 +420,34 @@ int main() { Require(sceFontGetRenderScaledKerning(&unopened, 'A', 'V', &kerning) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && KerningIs(kerning, 0.0f)); Require(sceFontUnbindRenderer(font) == SCE_FONT_OK); Require(sceFontGetRenderScaledKerning(font, 'A', 'V', &kerning) == SCE_FONT_ERROR_NOT_BOUND_RENDERER); + + ParsedText parsed{"AV", 0, font}; + FontTextSource source{}; + Require(sceFontTextSourceInit(&source, parsed.text, 2, ParseText, &parsed) == SCE_FONT_OK); + FontString string = nullptr; + Require(sceFontCreateString(&memory, &source, nullptr, &string) == SCE_FONT_OK && string != nullptr); + std::uint32_t characterCount = 0; + FontTextCharacter* characters = sceFontStringRefersTextCharacters(string, &characterCount); + Require(characters != nullptr && characterCount == 2); + FontWriting writing{}; + Require(sceFontWritingInit(&writing, string, characters) == SCE_FONT_OK); + constexpr std::uint64_t guard = 0x5a5a5a5a5a5a5a5aull; + struct { + const FontWritingLetterStep* letter; + std::uint64_t guard; + } out{reinterpret_cast(&memory), guard}; + Require(sceFontWritingRefersRenderStepCharacter(&writing, &out.letter) == nullptr && out.letter == nullptr && out.guard == guard); + const FontWritingStep* step = sceFontWritingRefersRenderStep(&writing); + Require(step != nullptr); + Require(sceFontWritingRefersRenderStepCharacter(&writing, &out.letter) == characters && out.letter != nullptr && out.guard == guard); + Require(out.letter->x == step->x && out.letter->advanceX == step->advanceX && out.letter->Components.textsIndex == 0 && out.letter->Components.characterTextCount == 1); + step = sceFontWritingRefersRenderStep(&writing); + Require(step != nullptr); + Require(sceFontWritingRefersRenderStepCharacter(&writing, &out.letter) == characters + 1 && out.guard == guard); + 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); @@ -288,4 +458,12 @@ int main() { Require(allocations == 0); Require(sceFontMemoryTerm(&memory) == SCE_FONT_OK); Require(sceFontMemoryTerm(&memory) == SCE_FONT_ERROR_INVALID_MEMORY); + FontTextCharacter character{}; + character.characterCode = 'A'; + int syllableState = -1; + Require(sceFontCharacterGetSyllableStringState(&character, &syllableState) == SCE_FONT_OK && syllableState == 0); + syllableState = -1; + Require(sceFontCharacterGetSyllableStringState(nullptr, &syllableState) == SCE_FONT_ERROR_INVALID_PARAMETER && syllableState == 0); + Require(sceFontCharacterGetSyllableStringState(&character, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + std::filesystem::remove_all(fontRoot); } diff --git a/core/libs/tests/GuestFontWritingLine.cpp b/core/libs/tests/GuestFontWritingLine.cpp index 7d5e40f08a..7b40b889ca 100644 --- a/core/libs/tests/GuestFontWritingLine.cpp +++ b/core/libs/tests/GuestFontWritingLine.cpp @@ -38,31 +38,52 @@ struct WritingLineDetail { void* reservedPointers[3]; }; +constexpr std::uint64_t SPACE_BEFORE = std::uint64_t{1} << 30; +constexpr std::uint64_t SPACE_AFTER = std::uint64_t{1} << 31; + +static std::uint64_t Attribute(std::uint64_t level, std::uint64_t flags = 0) { + return 0xFA | (level << 8) | flags; +} + static bool Same(const FontWritingMetrics& a, const FontWritingMetrics& b) { return a.advanceX == b.advanceX && a.advanceY == b.advanceY && a.Extent.top == b.Extent.top && a.Extent.bottom == b.Extent.bottom && a.Extent.left == b.Extent.left && a.Extent.right == b.Extent.right; } -static bool NoSpace(void* line) { - float head = 1.0f; - float inner = 1.0f; - float tail = 1.0f; - float advance = 1.0f; - return sceFontWritingLineGetOrderingSpace(line, &head, &inner, &tail, &advance) == SCE_FONT_OK && head == 0.0f && inner == 0.0f && - tail == 0.0f && advance == 0.0f; +static bool SpaceIs(void* line, float advance, float head, float inner, float tail) { + float gotAdvance = -1.0f; + float gotHead = -1.0f; + float gotInner = -1.0f; + float gotTail = -1.0f; + return sceFontWritingLineGetOrderingSpace(line, &gotAdvance, &gotHead, &gotInner, &gotTail) == SCE_FONT_OK && gotAdvance == advance && + gotHead == head && gotInner == inner && gotTail == tail; } -static bool StepIs(const FontWritingLineStep* step, float x, const FontWritingMetrics& run, void* orderer) { - return step && step->x == x && step->y == 0.0f && step->advanceX == run.advanceX && step->advanceY == run.advanceY && - step->spacingProgress == 0.0f && step->writingOrderer == orderer && step->Adjusting.x == 0.0f && +static bool StepIs(const FontWritingLineStep* step, float x, float progress, const FontWritingMetrics& run, void* orderer) { + return step && step->x == x && step->y == 0.0f && step->advanceX == 0.0f && step->advanceY == 0.0f && + step->spacingProgress == progress && step->writingOrderer == orderer && step->Adjusting.x == 0.0f && step->Adjusting.y == 0.0f && Same(step->Metrics, run); } +static bool OrderIs(void* line, const std::uint64_t* levels, const std::size_t* order, std::size_t count) { + static int orderers[16]; + const FontWritingMetrics run{1.0f, 0.0f, {-1.0f, 1.0f, 0.0f, 1.0f}}; + if (sceFontWritingLineClear(line) != SCE_FONT_OK) return false; + for (std::size_t i = 0; i < count; ++i) { + if (sceFontWritingLineWritesOrder(line, Attribute(levels[i]), &run, &orderers[i]) != SCE_FONT_OK) return false; + } + for (std::size_t i = 0; i < count; ++i) { + const FontWritingLineStep* step = sceFontWritingLineRefersRenderStep(line); + if (!step || step->writingOrderer != &orderers[order[i]]) return false; + } + return sceFontWritingLineRefersRenderStep(line) == nullptr; +} + int main() { FontMemory memory{}; void* line = nullptr; Require(Throws([&] { sceFontCreateWritingLine(&memory, 0x10, nullptr, nullptr); })); - Require(Throws([&] { sceFontCreateWritingLine(&memory, 0x11, nullptr, &line); }) && line == 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); @@ -70,58 +91,143 @@ int main() { Require(sceFontCreateWritingLine(&memory, 0x10, &detail, &line) == SCE_FONT_OK && line != nullptr); void* ltr = nullptr; Require(sceFontCreateWritingLine(&memory, 0x12, nullptr, <r) == SCE_FONT_OK && ltr != nullptr && ltr != line); + void* rtl = nullptr; + Require(sceFontCreateWritingLine(&memory, 0x11, nullptr, &rtl) == SCE_FONT_OK && rtl != nullptr); - FontWritingMetrics metrics{}; - Require(NoSpace(line)); + FontWritingMetrics metrics{1.0f, 1.0f, {1.0f, 1.0f, 1.0f, 1.0f}}; + Require(SpaceIs(line, 0.0f, 0.0f, 0.0f, 0.0f)); Require(sceFontWritingLineRefersRenderStep(line) == nullptr); - Require(Throws([&] { sceFontWritingLineGetRenderMetrics(line, &metrics); })); + Require(sceFontWritingLineGetRenderMetrics(line, &metrics) == SCE_FONT_OK && Same(metrics, {})); - int ordererA = 0; - int ordererB = 0; + int a = 0; + int b = 0; + int c = 0; + int d = 0; const FontWritingMetrics runA{10.0f, 0.0f, {-8.0f, 2.0f, 1.0f, 9.0f}}; const FontWritingMetrics runB{6.0f, 0.0f, {-12.0f, 3.0f, -1.0f, 5.0f}}; - Require(sceFontWritingLineWritesOrder(line, 0, &runA, &ordererA) == SCE_FONT_OK); - Require(sceFontWritingLineGetRenderMetrics(line, &metrics) == SCE_FONT_OK && Same(metrics, runA)); - Require(sceFontWritingLineWritesOrder(line, 0, &runB, &ordererB) == SCE_FONT_OK); - Require(sceFontWritingLineGetRenderMetrics(line, &metrics) == SCE_FONT_OK); - Require(Same(metrics, {16.0f, 0.0f, {-12.0f, 3.0f, 1.0f, 15.0f}})); - Require(NoSpace(line)); - float space = 0.0f; - Require(Throws([&] { sceFontWritingLineGetOrderingSpace(line, nullptr, &space, &space, &space); })); - Require(Throws([&] { sceFontWritingLineGetOrderingSpace(line, &space, &space, &space, nullptr); })); + 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(StepIs(sceFontWritingLineRefersRenderStep(line), 0.0f, runA, &ordererA)); - Require(StepIs(sceFontWritingLineRefersRenderStep(line), 10.0f, runB, &ordererB)); - Require(sceFontWritingLineRefersRenderStep(line) == nullptr); + 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(sceFontWritingLineRefersRenderStep(line) == nullptr); - const FontWritingMetrics runC{4.0f, 0.0f, {-4.0f, 1.0f, 0.0f, 20.0f}}; - Require(sceFontWritingLineWritesOrder(line, 0, &runC, nullptr) == SCE_FONT_OK); - Require(sceFontWritingLineGetRenderMetrics(line, &metrics) == SCE_FONT_OK); - Require(Same(metrics, {20.0f, 0.0f, {-12.0f, 3.0f, 1.0f, 36.0f}})); - Require(StepIs(sceFontWritingLineRefersRenderStep(line), 0.0f, runA, &ordererA)); - Require(StepIs(sceFontWritingLineRefersRenderStep(line), 10.0f, runB, &ordererB)); - Require(StepIs(sceFontWritingLineRefersRenderStep(line), 16.0f, runC, nullptr)); + Require(sceFontWritingLineWritesOrder(line, 0, &runA, &a) == SCE_FONT_OK); + 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, {})); + Require(StepIs(sceFontWritingLineRefersRenderStep(line), 0.0f, 0.0f, runA, &a)); + Require(sceFontWritingLineGetRenderMetrics(line, &metrics) == SCE_FONT_OK && Same(metrics, {})); + Require(StepIs(sceFontWritingLineRefersRenderStep(line), 10.0f, 0.0f, runB, &b)); + Require(sceFontWritingLineGetRenderMetrics(line, &metrics) == SCE_FONT_OK && Same(metrics, {10.0f, 0.0f, {-8.0f, 2.0f, 1.0f, 9.0f}})); Require(sceFontWritingLineRefersRenderStep(line) == nullptr); + Require(sceFontWritingLineGetRenderMetrics(line, &metrics) == SCE_FONT_OK && Same(metrics, {16.0f, 0.0f, {-12.0f, 3.0f, 1.0f, 15.0f}})); + Require(StepIs(sceFontWritingLineRefersRenderStep(line), 0.0f, 0.0f, runA, &a)); + Require(sceFontWritingLineGetRenderMetrics(line, &metrics) == SCE_FONT_OK && Same(metrics, {})); - Require(Throws([&] { sceFontWritingLineWritesOrder(line, 1, &runA, nullptr); })); - Require(Throws([&] { sceFontWritingLineWritesOrder(line, 0, nullptr, nullptr); })); - Require(Throws([&] { sceFontWritingLineGetRenderMetrics(line, nullptr); })); - Require(sceFontWritingLineGetRenderMetrics(line, &metrics) == SCE_FONT_OK && metrics.advanceX == 20.0f); + Require(sceFontWritingLineWritesOrder(line, Attribute(0x7E), nullptr, &d) == SCE_FONT_OK); + Require(SpaceIs(line, 0.0f, 0.0f, 0.0f, 0.0f)); + Require(StepIs(sceFontWritingLineRefersRenderStep(line), 0.0f, 0.0f, {}, &d)); + Require(sceFontWritingLineRefersRenderStep(line) == nullptr); Require(sceFontWritingLineClear(line) == SCE_FONT_OK); Require(sceFontWritingLineRefersRenderStep(line) == nullptr); - Require(Throws([&] { sceFontWritingLineGetRenderMetrics(line, &metrics); })); - Require(sceFontWritingLineWritesOrder(line, 0, &runB, &ordererB) == SCE_FONT_OK); - Require(sceFontWritingLineGetRenderMetrics(line, &metrics) == SCE_FONT_OK && Same(metrics, runB)); - Require(StepIs(sceFontWritingLineRefersRenderStep(line), 0.0f, runB, &ordererB)); + Require(sceFontWritingLineWritesOrder(line, Attribute(0), &runA, &a) == SCE_FONT_OK); + Require(sceFontWritingLineWritesOrder(line, Attribute(1), &runB, &b) == SCE_FONT_OK); + Require(sceFontWritingLineWritesOrder(line, Attribute(1), &runC, &c) == SCE_FONT_OK); + Require(sceFontWritingLineWritesOrder(line, Attribute(0), &runD, &d) == SCE_FONT_OK); + Require(SpaceIs(line, 28.0f, 1.0f, 0.0f, -16.0f)); + Require(StepIs(sceFontWritingLineRefersRenderStep(line), 0.0f, 0.0f, runA, &a)); + Require(StepIs(sceFontWritingLineRefersRenderStep(line), 10.0f, 0.0f, runC, &c)); + Require(StepIs(sceFontWritingLineRefersRenderStep(line), 18.0f, 0.0f, runB, &b)); + Require(StepIs(sceFontWritingLineRefersRenderStep(line), 24.0f, 0.0f, runD, &d)); + Require(sceFontWritingLineRefersRenderStep(line) == nullptr); - Require(sceFontWritingLineWritesOrder(ltr, 0, &runC, nullptr) == SCE_FONT_OK); - Require(sceFontWritingLineGetRenderMetrics(ltr, &metrics) == SCE_FONT_OK && Same(metrics, runC)); + Require(sceFontWritingLineClear(line) == SCE_FONT_OK); + Require(sceFontWritingLineWritesOrder(line, Attribute(0), &runA, &a) == SCE_FONT_OK); + Require(sceFontWritingLineWritesOrder(line, Attribute(1), &runB, &b) == SCE_FONT_OK); + Require(sceFontWritingLineWritesOrder(line, Attribute(2), &runC, &c) == SCE_FONT_OK); + Require(sceFontWritingLineWritesOrder(line, Attribute(2), &runD, &d) == SCE_FONT_OK); + Require(sceFontWritingLineWritesOrder(line, Attribute(1), &runA, &b) == SCE_FONT_OK); + Require(sceFontWritingLineWritesOrder(line, Attribute(0), &runB, &a) == SCE_FONT_OK); + Require(StepIs(sceFontWritingLineRefersRenderStep(line), 0.0f, 0.0f, runA, &a)); + Require(StepIs(sceFontWritingLineRefersRenderStep(line), 10.0f, 0.0f, runA, &b)); + Require(StepIs(sceFontWritingLineRefersRenderStep(line), 20.0f, 0.0f, runC, &c)); + Require(StepIs(sceFontWritingLineRefersRenderStep(line), 28.0f, 0.0f, runD, &d)); + Require(StepIs(sceFontWritingLineRefersRenderStep(line), 32.0f, 0.0f, runB, &b)); + Require(StepIs(sceFontWritingLineRefersRenderStep(line), 38.0f, 0.0f, runB, &a)); + Require(sceFontWritingLineRefersRenderStep(line) == nullptr); + + Require(sceFontWritingLineWritesOrder(ltr, Attribute(0, SPACE_AFTER), &runA, &a) == SCE_FONT_OK); + Require(SpaceIs(ltr, 10.0f, 1.0f, 0.0f, 1.0f)); + Require(sceFontWritingLineWritesOrder(ltr, Attribute(0), &runC, &c) == SCE_FONT_OK); + Require(SpaceIs(ltr, 18.0f, 1.0f, 3.0f, 3.0f)); + Require(sceFontWritingLineWritesOrder(ltr, Attribute(0, SPACE_BEFORE), &runB, &b) == SCE_FONT_OK); + Require(SpaceIs(ltr, 24.0f, 1.0f, 5.0f, 1.0f)); + Require(StepIs(sceFontWritingLineRefersRenderStep(ltr), 0.0f, 0.0f, runA, &a)); + Require(StepIs(sceFontWritingLineRefersRenderStep(ltr), 10.0f, 3.0f / 5.0f, runC, &c)); + Require(StepIs(sceFontWritingLineRefersRenderStep(ltr), 18.0f, 1.0f, runB, &b)); + Require(sceFontWritingLineRefersRenderStep(ltr) == nullptr); + + Require(sceFontWritingLineClear(ltr) == SCE_FONT_OK); + Require(sceFontWritingLineWritesOrder(ltr, Attribute(0, SPACE_AFTER), &runA, &a) == SCE_FONT_OK); + Require(sceFontWritingLineWritesOrder(ltr, Attribute(1), &runC, &c) == SCE_FONT_OK); + Require(SpaceIs(ltr, 18.0f, 1.0f, 3.0f, 3.0f)); + Require(sceFontWritingLineWritesOrder(ltr, Attribute(1), &runB, &b) == SCE_FONT_OK); + Require(SpaceIs(ltr, 24.0f, 1.0f, 0.0f, 3.0f)); + Require(sceFontWritingLineWritesOrder(ltr, Attribute(1, SPACE_AFTER), &runD, &d) == SCE_FONT_OK); + Require(SpaceIs(ltr, 28.0f, 1.0f, -16.0f, 3.0f)); + Require(StepIs(sceFontWritingLineRefersRenderStep(ltr), 0.0f, 0.0f, runA, &a)); + Require(StepIs(sceFontWritingLineRefersRenderStep(ltr), 10.0f, 1.0f / -16.0f, runD, &d)); + Require(StepIs(sceFontWritingLineRefersRenderStep(ltr), 14.0f, 1.0f, runB, &b)); + Require(StepIs(sceFontWritingLineRefersRenderStep(ltr), 20.0f, 1.0f, runC, &c)); + Require(sceFontWritingLineRefersRenderStep(ltr) == nullptr); + + Require(sceFontWritingLineWritesOrder(rtl, Attribute(0, SPACE_AFTER), &runA, &a) == SCE_FONT_OK); + Require(sceFontWritingLineWritesOrder(rtl, Attribute(0), &runC, &c) == SCE_FONT_OK); + Require(SpaceIs(rtl, 18.0f, 5.0f, 15.0f, 9.0f)); + Require(StepIs(sceFontWritingLineRefersRenderStep(rtl), 0.0f, 0.0f, runC, &c)); + Require(StepIs(sceFontWritingLineRefersRenderStep(rtl), 8.0f, 1.0f, runA, &a)); + Require(sceFontWritingLineRefersRenderStep(rtl) == nullptr); + Require(sceFontWritingLineGetRenderMetrics(rtl, &metrics) == SCE_FONT_OK && Same(metrics, {18.0f, 0.0f, {-8.0f, 2.0f, 2.0f, 17.0f}})); + + const std::uint64_t nested[] = {0, 1, 2, 3, 1, 1, 0}; + const std::size_t nestedOrder[] = {0, 5, 4, 2, 3, 1, 6}; + Require(OrderIs(ltr, nested, nestedOrder, 7)); + const std::uint64_t repeated[] = {0, 1, 1, 2, 2, 0, 1, 2, 2, 0}; + const std::size_t repeatedOrder[] = {0, 3, 4, 2, 1, 5, 7, 8, 6, 9}; + Require(OrderIs(ltr, repeated, repeatedOrder, 10)); + const std::uint64_t skipped[] = {0, 2, 3, 3, 2, 1, 0}; + const std::size_t skippedOrder[] = {0, 5, 1, 3, 2, 4, 6}; + Require(OrderIs(ltr, skipped, skippedOrder, 7)); + + const FontWritingMetrics runE{10.0f, 3.0f, {-8.0f, 2.0f, 1.0f, 9.0f}}; + const FontWritingMetrics runF{6.0f, -1.0f, {-12.0f, 3.0f, -1.0f, 5.0f}}; + Require(sceFontWritingLineClear(ltr) == SCE_FONT_OK); + Require(sceFontWritingLineWritesOrder(ltr, Attribute(0), &runE, &a) == SCE_FONT_OK); + Require(sceFontWritingLineWritesOrder(ltr, Attribute(0), &runF, &b) == SCE_FONT_OK); + auto* adjusted = const_cast(sceFontWritingLineRefersRenderStep(ltr)); + Require(adjusted && adjusted->x == 0.0f && adjusted->y == 0.0f && adjusted->writingOrderer == &a); + adjusted->Adjusting = {2.0f, 5.0f}; + const FontWritingLineStep* second = sceFontWritingLineRefersRenderStep(ltr); + Require(second && second->x == 10.0f && second->y == 3.0f && second->Adjusting.x == 0.0f && second->Adjusting.y == 0.0f); + Require(sceFontWritingLineGetRenderMetrics(ltr, &metrics) == SCE_FONT_OK && Same(metrics, {12.0f, 8.0f, {-3.0f, 7.0f, 3.0f, 11.0f}})); + Require(sceFontWritingLineRefersRenderStep(ltr) == nullptr); + Require(sceFontWritingLineGetRenderMetrics(ltr, &metrics) == SCE_FONT_OK && Same(metrics, {16.0f, 2.0f, {-9.0f, 7.0f, 3.0f, 15.0f}})); + + 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); })); int notALine = 0; Require(sceFontWritingLineRefersRenderStep(nullptr) == nullptr); - Require(Throws([&] { sceFontWritingLineClear(nullptr); })); Require(Throws([&] { sceFontWritingLineClear(¬ALine); })); Require(Throws([&] { sceFontWritingLineRefersRenderStep(¬ALine); })); Require(Throws([&] { sceFontWritingLineGetOrderingSpace(¬ALine, &space, &space, &space, &space); })); @@ -133,7 +239,8 @@ int main() { Require(sceFontDestroyWritingLine(&line) == SCE_FONT_OK && line == nullptr); Require(sceFontDestroyWritingLine(nullptr) == SCE_FONT_OK); Require(Throws([&] { sceFontWritingLineClear(destroyed); })); - Require(Throws([&] { sceFontWritingLineWritesOrder(destroyed, 0, &runA, nullptr); })); + Require(Throws([&] { sceFontWritingLineWritesOrder(destroyed, Attribute(0), &runA, &a); })); Require(sceFontDestroyWritingLine(<r) == SCE_FONT_OK && ltr == nullptr); + Require(sceFontDestroyWritingLine(&rtl) == SCE_FONT_OK && rtl == nullptr); 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/GuestForceTlsDestructor.cpp b/core/libs/tests/GuestForceTlsDestructor.cpp index 49264c9297..6f900b9396 100644 --- a/core/libs/tests/GuestForceTlsDestructor.cpp +++ b/core/libs/tests/GuestForceTlsDestructor.cpp @@ -1,10 +1,15 @@ #include "SceTypes.hpp" #include #include -#include #include #include +#ifdef _WIN32 +#include +#else +#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); @@ -69,7 +74,11 @@ bool ForceThrows(KernelModule handle) { int main() { self = ModuleOf(&dsoHandle); +#ifdef _WIN32 + void* otherDso = reinterpret_cast(GetProcAddress(GetModuleHandleA("libSceLibcInternal.prx"), "_sceLibcInternalThreadDtors_nid_postfix")); +#else void* otherDso = dlsym(RTLD_NEXT, "_sceLibcInternalThreadDtors_nid_postfix"); +#endif Require(otherDso != nullptr); const KernelModule other = ModuleOf(otherDso); Require(other != self && other != 0); diff --git a/core/libs/tests/GuestFreeType.cpp b/core/libs/tests/GuestFreeType.cpp new file mode 100644 index 0000000000..bfb51cb340 --- /dev/null +++ b/core/libs/tests/GuestFreeType.cpp @@ -0,0 +1,162 @@ +#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 + +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**); +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_Render_Glyph_nid_postfix(GuestGlyphSlotRec*, std::int32_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); +} + +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); + + 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); + + 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_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); + + FT_Done_Face(host); + FT_Done_FreeType(hostLibrary); + 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/GuestFreebsdClocks.cpp b/core/libs/tests/GuestFreebsdClocks.cpp index 75a69daa61..40972ab464 100644 --- a/core/libs/tests/GuestFreebsdClocks.cpp +++ b/core/libs/tests/GuestFreebsdClocks.cpp @@ -1,15 +1,25 @@ #include "SceTypes.hpp" +#include +#include #include +#include #include +#include +#include +#include extern "C" { int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp); int APS5_VABI clock_getres_nid_postfix(int clockId, KernelTimespec* res); +int APS5_VABI clock_settime_nid_postfix(int clockId, const KernelTimespec* tp); int APS5_VABI sceKernelClockGettime(KernelClockid clockId, KernelTimespec* tp); int APS5_VABI sceKernelClockGetres(KernelClockid clockId, KernelTimespec* tp); +int* APS5_VABI __error_nid_postfix(); int APS5_VABI sceKernelUsleep_nid_postfix(KernelUseconds microseconds); int APS5_VABI gettimeofday_nid_postfix(KernelTimeval* tv, KernelTimezone* tz); std::int64_t APS5_VABI _Xtime_get_ticks_nid_postfix(); +int APS5_VABI sceKernelConvertLocaltimeToUtc(std::int64_t, std::int64_t, std::int64_t*, KernelTimesec*, std::int32_t*); +int APS5_VABI sceKernelConvertUtcToLocaltime(std::int64_t, std::int64_t*, KernelTimesec*, std::int32_t*); } static constexpr int SCE_OK = 0; @@ -18,7 +28,9 @@ static constexpr int GUEST_CLOCK_REALTIME = 0; static constexpr int GUEST_CLOCK_VIRTUAL = 1; static constexpr int GUEST_CLOCK_PROF = 2; static constexpr int GUEST_CLOCK_MONOTONIC = 4; +static constexpr int GUEST_CLOCK_REALTIME_PRECISE = 9; static constexpr int GUEST_CLOCK_SECOND = 13; +static constexpr int GUEST_CLOCK_THREAD_CPUTIME_ID = 14; static constexpr int GUEST_CLOCK_PROCESS_CPUTIME_ID = 15; static constexpr std::int64_t NANOS_PER_SECOND = 1000000000LL; @@ -29,6 +41,59 @@ 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) { + *__error_nid_postfix() = 77; + try { + if (clock_getres_nid_postfix(clock, nullptr) == SCE_OK && *__error_nid_postfix() == 77) continue; + } catch (const std::runtime_error&) { + } + std::fprintf(stderr, "clock_getres(%d, nullptr) must succeed without changing errno\n", clock); + return false; + } + return true; +} + +static void UnsupportedResolutionStillThrows() { + KernelTimespec resolution{-1, -1}; + bool rejected = false; + try { + clock_getres_nid_postfix(3, &resolution); + } catch (const std::runtime_error&) { + rejected = true; + } + Require(rejected && resolution.tv_sec == -1 && resolution.tv_nsec == -1); + constexpr int fault = static_cast(0x8002000eu); + Require(sceKernelClockGetres(GUEST_CLOCK_REALTIME, nullptr) == fault); + 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); @@ -137,8 +202,126 @@ static void XtimeTicksAreGettimeofdayMicroseconds() { } } +static void CalendarConversionsInitializeTheirState() { + SetTimeZone("UTC0"); + constexpr std::uint64_t sentinel = 0xa5a5a5a5a5a5a5a5ull; + struct GuardedState { + std::uint64_t before; + KernelTimesec state; + std::uint64_t after; + }; + struct GuardedDst { + std::int32_t seconds; + std::uint32_t after; + }; + static_assert(sizeof(GuardedDst) == 8); + for (const auto seconds : std::array{-86400, 0, 1767225600, 2147483648}) { + GuardedState local{sentinel, {-1, 0xa5a5a5a5u, 0xa5a5a5a5u}, sentinel}; + std::int64_t utc = -1; + std::int32_t dst = -1; + Require(sceKernelConvertLocaltimeToUtc(seconds, 0, &utc, &local.state, &dst) == SCE_OK); + Require(utc == seconds && dst == 0); + Require(local.state.t == seconds && local.state.west_sec == 0 && local.state.dst_sec == 0); + Require(local.before == sentinel && local.after == sentinel); + GuardedState universal{sentinel, {-1, 0xa5a5a5a5u, 0xa5a5a5a5u}, sentinel}; + std::int64_t converted = -1; + GuardedDst universalDst{-1, 0xa5a5a5a5u}; + Require(sceKernelConvertUtcToLocaltime(utc, &converted, &universal.state, &universalDst.seconds) == SCE_OK); + Require(converted == seconds && universalDst.seconds == 0); + Require(universalDst.after == 0xa5a5a5a5u); + Require(universal.state.t == seconds && universal.state.west_sec == 0 && universal.state.dst_sec == 0); + Require(universal.before == sentinel && universal.after == sentinel); + } + Require(sceKernelConvertLocaltimeToUtc(0, 0, nullptr, nullptr, nullptr) == SCE_OK); + Require(sceKernelConvertUtcToLocaltime(0, nullptr, nullptr, nullptr) == SCE_OK); +} + +static void RequireSettimeError(int clockId, const KernelTimespec* time, int error) { + *__error_nid_postfix() = 0; + Require(clock_settime_nid_postfix(clockId, time) == -1 && *__error_nid_postfix() == error); +} + +static bool SettimeThrows(const KernelTimespec& time, const char* requested) { + try { + clock_settime_nid_postfix(GUEST_CLOCK_REALTIME, &time); + } catch (const std::runtime_error& error) { + return std::strstr(error.what(), requested) != nullptr; + } + return false; +} + +static void SettimeValidatesLikeFreebsd() { + const KernelTimespec valid{1767225600, 5}; + RequireSettimeError(GUEST_CLOCK_REALTIME, nullptr, 14); + RequireSettimeError(GUEST_CLOCK_MONOTONIC, nullptr, 14); + for (const int clockId : {GUEST_CLOCK_VIRTUAL, GUEST_CLOCK_PROF, GUEST_CLOCK_MONOTONIC, GUEST_CLOCK_REALTIME_PRECISE, + GUEST_CLOCK_SECOND, GUEST_CLOCK_THREAD_CPUTIME_ID, GUEST_CLOCK_PROCESS_CPUTIME_ID, -1, 16}) + RequireSettimeError(clockId, &valid, 22); + const KernelTimespec negativeNanoseconds{0, -1}; + const KernelTimespec wholeSecondOfNanoseconds{0, NANOS_PER_SECOND}; + const KernelTimespec negativeSeconds{-1, 0}; + RequireSettimeError(GUEST_CLOCK_REALTIME, &negativeNanoseconds, 22); + RequireSettimeError(GUEST_CLOCK_REALTIME, &wholeSecondOfNanoseconds, 22); + RequireSettimeError(GUEST_CLOCK_REALTIME, &negativeSeconds, 22); + RequireSettimeError(GUEST_CLOCK_MONOTONIC, &negativeSeconds, 22); + Require(SettimeThrows(valid, "1767225600.000000005")); + Require(SettimeThrows(KernelTimespec{0, 0}, "0.000000000")); + Require(SettimeThrows(KernelTimespec{1, NANOS_PER_SECOND - 1}, "1.999999999")); +} + +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); + Require(gettimeofday_nid_postfix(nullptr, &zone) == SCE_OK && zone.tz_minuteswest == 0 && zone.tz_dsttime == 0); + const int nullResolutionClocks[] = {GUEST_CLOCK_REALTIME, GUEST_CLOCK_VIRTUAL, GUEST_CLOCK_MONOTONIC, GUEST_CLOCK_SECOND, GUEST_CLOCK_PROCESS_CPUTIME_ID}; + for (const int clockId : nullResolutionClocks) Require(clock_getres_nid_postfix(clockId, nullptr) == SCE_OK); KernelTimespec time{-1, -1}; Require(sceKernelClockGettime(GUEST_CLOCK_VIRTUAL, &time) == SCE_OK); diff --git a/core/libs/tests/GuestFstatFault.cpp b/core/libs/tests/GuestFstatFault.cpp new file mode 100644 index 0000000000..4f0aa66bb9 --- /dev/null +++ b/core/libs/tests/GuestFstatFault.cpp @@ -0,0 +1,69 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" + +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +std::int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int, FileStat*); +int APS5_VABI sceKernelFstat(int, FileStat*); +int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); +int APS5_VABI close_nid_postfix(int); +int APS5_VABI socketpair_nid_postfix(int, int, int, int*); +int* APS5_VABI __error_nid_postfix(); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "fstat check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static constexpr int GuestEbadf = 9; +static constexpr int GuestEfault = 14; + +static void RequireFailure(int descriptor, int error) { + *__error_nid_postfix() = 0; + Require(fstat_nid_disambig1_nid_postfix(descriptor, nullptr) == -1); + Require(*__error_nid_postfix() == error); + Require(sceKernelFstat(descriptor, nullptr) == static_cast(0x80020000u | static_cast(error))); +} + +int main() { + const auto root = std::filesystem::temp_directory_path() / + ("anyps5-fstat-fault-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + { + std::ofstream file(root / "file.txt", std::ios::binary); + file << "fstat"; + Require(static_cast(file)); + } + + AddPathAlias_nid_no_patch("/anyps5-fstat-fault-test", root.string().c_str()); + const int file = sceKernelOpen("/anyps5-fstat-fault-test/file.txt", SCE_KERNEL_O_RDONLY, 0); + Require(file >= 0); + FileStat status{}; + Require(fstat_nid_disambig1_nid_postfix(file, &status) == 0 && status.st_size == 5); + RequireFailure(file, GuestEfault); + + int sockets[2]; + Require(socketpair_nid_postfix(1, 1, 0, sockets) == 0); + RequireFailure(sockets[0], GuestEfault); + Require(close_nid_postfix(sockets[0]) == 0); + Require(close_nid_postfix(sockets[1]) == 0); + Require(close_nid_postfix(file) == 0); +#ifndef _WIN32 + RequireFailure(file, GuestEbadf); + RequireFailure(-1, GuestEbadf); +#endif + + std::filesystem::remove_all(root); +} diff --git a/core/libs/tests/GuestFstatfs.cpp b/core/libs/tests/GuestFstatfs.cpp new file mode 100644 index 0000000000..0c86546ccb --- /dev/null +++ b/core/libs/tests/GuestFstatfs.cpp @@ -0,0 +1,132 @@ +#include "prx/libc/include/General.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +struct GuestStatfs { + std::uint32_t f_version; + std::uint32_t f_type; + std::uint64_t f_flags; + std::uint64_t f_bsize; + std::uint64_t f_iosize; + std::uint64_t f_blocks; + std::uint64_t f_bfree; + std::int64_t f_bavail; + std::uint64_t f_files; + std::int64_t f_ffree; + std::uint64_t f_syncwrites; + std::uint64_t f_asyncwrites; + std::uint64_t f_syncreads; + std::uint64_t f_asyncreads; + std::uint64_t f_spare[10]; + std::uint32_t f_namemax; + std::uint32_t f_owner; + std::int32_t f_fsid[2]; + char f_charspare[80]; + char f_fstypename[16]; + char f_mntfromname[88]; + char f_mntonname[88]; +}; +static_assert(sizeof(GuestStatfs) == 472); + +extern "C" { +int APS5_VABI _fstatfs_nid_postfix(int, GuestStatfs*); +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 __error_nid_postfix(); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "fstatfs check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static constexpr std::uint32_t StatfsVersion = 0x20030518; +static constexpr int GuestEbadf = 9; +static constexpr int GuestEfault = 14; +static constexpr int GuestEinval = 22; +static constexpr int DescriptorSetSize = 1024; + +static void RequireFailure(int descriptor, int error) { + GuestStatfs fs; + std::memset(&fs, 0x5a, sizeof(fs)); + *__error_nid_postfix() = 0; + Require(_fstatfs_nid_postfix(descriptor, &fs) == -1); + Require(*__error_nid_postfix() == error); + for (std::size_t i = 0; i < sizeof(fs); ++i) Require(reinterpret_cast(&fs)[i] == 0x5a); +} + +static void RequireVolume(int descriptor, const std::filesystem::path& path) { + GuestStatfs fs; + std::memset(&fs, 0x5a, sizeof(fs)); + Require(_fstatfs_nid_postfix(descriptor, &fs) == 0); + const auto space = std::filesystem::space(path); + Require(fs.f_version == StatfsVersion); + Require(fs.f_bsize > 0 && fs.f_iosize > 0); + Require(fs.f_blocks > 0 && fs.f_bfree <= fs.f_blocks); + Require(fs.f_bavail >= 0 && static_cast(fs.f_bavail) <= fs.f_bfree); + Require(fs.f_blocks * fs.f_bsize <= space.capacity && space.capacity - fs.f_blocks * fs.f_bsize < fs.f_bsize); + Require((fs.f_flags & 0x1) == 0); + Require(fs.f_namemax >= 255); + Require(fs.f_fstypename[0] == '\0' && fs.f_mntfromname[0] == '\0' && fs.f_mntonname[0] == '\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())); + Require(std::filesystem::create_directory(root)); + const auto filePath = root / "file.txt"; + { + std::ofstream file(filePath, std::ios::binary); + file << "fstatfs"; + Require(static_cast(file)); + } + + AddPathAlias_nid_no_patch("/anyps5-fstatfs-test", root.string().c_str()); + const int file = sceKernelOpen("/anyps5-fstatfs-test/file.txt", SCE_KERNEL_O_RDONLY, 0); + Require(file >= 0); + RequireVolume(file, filePath); + *__error_nid_postfix() = 0; + Require(_fstatfs_nid_postfix(file, nullptr) == -1); + Require(*__error_nid_postfix() == GuestEfault); + + const int directory = sceKernelOpen("/anyps5-fstatfs-test", SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(directory >= 0); + RequireVolume(directory, root); + Require(close_nid_postfix(directory) == 0); + + int pipe[2]; + Require(pipe_nid_postfix(pipe) == 0); + RequireFailure(pipe[0], GuestEinval); + RequireFailure(pipe[1], GuestEinval); + Require(close_nid_postfix(pipe[0]) == 0); + Require(close_nid_postfix(pipe[1]) == 0); + + int sockets[2]; + Require(socketpair_nid_postfix(1, 1, 0, sockets) == 0); + Require(sockets[0] >= 0 && sockets[0] < DescriptorSetSize); + RequireFailure(sockets[0], GuestEinval); + Require(close_nid_postfix(sockets[0]) == 0); + Require(close_nid_postfix(sockets[1]) == 0); + RequireFailure(sockets[0], GuestEbadf); + + 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/GuestFstatvfs.cpp b/core/libs/tests/GuestFstatvfs.cpp new file mode 100644 index 0000000000..487e8bacc7 --- /dev/null +++ b/core/libs/tests/GuestFstatvfs.cpp @@ -0,0 +1,97 @@ +#include "prx/libc/include/General.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI fstatvfs_nid_postfix(int, void*); +int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); +std::int64_t APS5_VABI sceKernelLseek(int, std::int64_t, int); +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 __error_nid_postfix(); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "fstatvfs check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static void RequireFailure(int descriptor, int error) { + std::array output; + output.fill(0x5A5A5A5A5A5A5A5Aull); + const auto saved = output; + *__error_nid_postfix() = 0; + Require(fstatvfs_nid_postfix(descriptor, output.data()) == -1); + Require(*__error_nid_postfix() == error); + Require(output == saved); +} + +static void RequireVolume(int descriptor, const std::filesystem::path& path) { + std::array guarded; + guarded.fill(0xA5A5A5A5A5A5A5A5ull); + *__error_nid_postfix() = 12345; + Require(fstatvfs_nid_postfix(descriptor, guarded.data() + 1) == 0); + Require(*__error_nid_postfix() == 12345); + Require(guarded.front() == 0xA5A5A5A5A5A5A5A5ull && guarded.back() == guarded.front()); + const auto* fields = guarded.data() + 1; + const auto capacity = std::filesystem::space(path).capacity; + Require(fields[6] > 0 && fields[8] > 0); + Require(fields[2] > 0 && fields[2] == capacity / fields[8]); + Require(fields[0] <= fields[1] && fields[1] <= fields[2]); + Require(fields[3] == fields[4]); + Require((fields[7] & ~std::uint64_t{3}) == 0); + Require(fields[9] == 0 && fields[10] >= 255); +} + +int main() { + const auto root = std::filesystem::temp_directory_path() / + ("anyps5-fstatvfs-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + const auto filePath = root / "file.txt"; + { + std::ofstream file(filePath, std::ios::binary); + file << "fstatvfs"; + Require(static_cast(file)); + } + AddPathAlias_nid_no_patch("/anyps5-fstatvfs-test", root.string().c_str()); + const int file = sceKernelOpen("/anyps5-fstatvfs-test/file.txt", SCE_KERNEL_O_RDONLY, 0); + Require(file >= 0); + Require(sceKernelLseek(file, 3, 0) == 3); + RequireVolume(file, filePath); + Require(sceKernelLseek(file, 0, 1) == 3); + Require(fstatvfs_nid_postfix(file, nullptr) == -1 && *__error_nid_postfix() == 14); + + const int directory = sceKernelOpen("/anyps5-fstatvfs-test", SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(directory >= 0); + RequireVolume(directory, root); + Require(close_nid_postfix(directory) == 0); + + int pipes[2]; + Require(pipe_nid_postfix(pipes) == 0); + RequireFailure(pipes[0], 22); + RequireFailure(pipes[1], 22); + for (int fd : pipes) Require(close_nid_postfix(fd) == 0); + int sockets[2]; + Require(socketpair_nid_postfix(1, 1, 0, sockets) == 0); + RequireFailure(sockets[0], 22); + for (int fd : sockets) Require(close_nid_postfix(fd) == 0); + RequireFailure(sockets[0], 9); + RequireFailure(-1, 9); + Require(close_nid_postfix(file) == 0); + RemovePathAlias_nid_no_patch("/anyps5-fstatvfs-test"); + Require(std::filesystem::remove(filePath)); + Require(std::filesystem::remove(root)); +} diff --git a/core/libs/tests/GuestFstatvfsConversion.cpp b/core/libs/tests/GuestFstatvfsConversion.cpp new file mode 100644 index 0000000000..f93e221e6b --- /dev/null +++ b/core/libs/tests/GuestFstatvfsConversion.cpp @@ -0,0 +1,78 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/KernelStatfs.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI fstatvfs_nid_postfix(int, void*); + +namespace { +KernelStatfs backend; +int backendError; +int guestError; +int calls; + +void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "fstatvfs conversion check failed at line %d\n", line); + std::abort(); + } +} +} +#define Require(value) Check((value), __LINE__) + +extern "C" int* APS5_VABI __error_nid_postfix() { return &guestError; } + +extern "C" int APS5_VABI _fstatfs_nid_postfix(int descriptor, KernelStatfs* output) { + Require(descriptor == 42); + ++calls; + *output = backend; + guestError = backendError ? backendError : 71; + return backendError ? -1 : 0; +} + +int main() { + backend.f_blocks = 0x100000002ull; + backend.f_bfree = 0x100000001ull; + backend.f_bavail = 0x100000000ll; + backend.f_files = 0x200000002ull; + backend.f_ffree = 0x200000001ll; + backend.f_iosize = 65536; + backend.f_bsize = 4096; + backend.f_namemax = 255; + backend.f_fsid[0] = 123; + for (std::uint64_t flags = 0; flags != 16; ++flags) { + backend.f_flags = flags; + std::array output; + output.fill(0xA5A5A5A5A5A5A5A5ull); + const std::array expected{ + 0x100000000ull, 0x100000001ull, 0x100000002ull, + 0x200000001ull, 0x200000001ull, 0x200000002ull, + 65536, (flags & 1) | ((flags & 8) ? 2u : 0u), 4096, 0, 255 + }; + const int before = calls; + guestError = 12345; + Require(fstatvfs_nid_postfix(42, output.data() + 1) == 0); + Require(calls == before + 1 && guestError == 12345); + Require(output.front() == 0xA5A5A5A5A5A5A5A5ull && output.back() == output.front()); + Require(std::memcmp(output.data() + 1, expected.data(), sizeof(expected)) == 0); + } + backend.f_bavail = -1; + backend.f_ffree = -2; + std::array output{}; + Require(fstatvfs_nid_postfix(42, output.data()) == 0); + Require(output[0] == std::numeric_limits::max()); + Require(output[3] == std::numeric_limits::max() - 1 && output[4] == output[3]); + Require(fstatvfs_nid_postfix(42, nullptr) == -1 && guestError == 14); + for (int failure : {5, 9, 22, 45}) { + backendError = failure; + output.fill(0x5A5A5A5A5A5A5A5Aull); + const auto saved = output; + Require(fstatvfs_nid_postfix(42, output.data()) == -1 && guestError == failure); + Require(output == saved); + Require(fstatvfs_nid_postfix(42, nullptr) == -1 && guestError == failure); + } +} diff --git a/core/libs/tests/GuestG2PDialog.cpp b/core/libs/tests/GuestG2PDialog.cpp new file mode 100644 index 0000000000..b3e0cb2b85 --- /dev/null +++ b/core/libs/tests/GuestG2PDialog.cpp @@ -0,0 +1,71 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI sceG2PDialogInitialize(void); +int APS5_VABI sceG2PDialogOpen(const void* param); +int APS5_VABI sceG2PDialogUpdateStatus(void); +int APS5_VABI sceG2PDialogGetStatus(void); +int APS5_VABI sceG2PDialogGetResult(void* result); +int APS5_VABI sceG2PDialogClose(void); +int APS5_VABI sceG2PDialogTerminate(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(sceG2PDialogTerminate() == kErrNotInitialized); + Require(sceG2PDialogOpen(¶m) == kErrNotInitialized); + Require(sceG2PDialogClose() == kErrNotInitialized); + Require(sceG2PDialogGetResult(result) == kErrNotInitialized); + Require(sceG2PDialogGetStatus() == kStatusNone); + Require(sceG2PDialogUpdateStatus() == kStatusNone); + + Require(sceG2PDialogInitialize() == 0); + Require(sceG2PDialogInitialize() == kErrAlreadyInitialized); + Require(sceG2PDialogGetStatus() == kStatusInitialized); + Require(sceG2PDialogUpdateStatus() == kStatusInitialized); + + Require(sceG2PDialogOpen(nullptr) == kErrArgNull); + Require(sceG2PDialogGetResult(result) == kErrNotFinished); + + Require(sceG2PDialogOpen(¶m) == 0); + Require(sceG2PDialogGetStatus() == kStatusFinished); + Require(sceG2PDialogUpdateStatus() == kStatusFinished); + + Require(sceG2PDialogGetResult(nullptr) == kErrArgNull); + Require(sceG2PDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(result[1] == -1); + + Require(sceG2PDialogClose() == 0); + Require(sceG2PDialogTerminate() == 0); + Require(sceG2PDialogTerminate() == kErrNotInitialized); + Require(sceG2PDialogGetStatus() == kStatusNone); + + Require(sceG2PDialogInitialize() == 0); + Require(sceG2PDialogGetStatus() == kStatusInitialized); + Require(sceG2PDialogClose() == 0); + Require(sceG2PDialogGetStatus() == kStatusFinished); + result[0] = -1; + Require(sceG2PDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(sceG2PDialogTerminate() == 0); +} diff --git a/core/libs/tests/GuestGameLiveStreaming.cpp b/core/libs/tests/GuestGameLiveStreaming.cpp new file mode 100644 index 0000000000..a60c2538cd --- /dev/null +++ b/core/libs/tests/GuestGameLiveStreaming.cpp @@ -0,0 +1,79 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#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); +int APS5_VABI sceGameLiveStreamingGetProgramInfo(void); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr std::int32_t kInvalidUserId = -1; + +} + +int main() { + bool threwZeroHeap = false; + try { + sceGameLiveStreamingInitialize(0); + } catch (const std::invalid_argument&) { + threwZeroHeap = true; + } + Require(threwZeroHeap); + + Require(sceGameLiveStreamingInitialize(1024 * 1024) == 0); + + bool threwNullStatus = false; + try { + sceGameLiveStreamingGetCurrentStatus2(nullptr); + } catch (const std::invalid_argument&) { + threwNullStatus = true; + } + Require(threwNullStatus); + + 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); + } + + bool threwProgramInfo = false; + try { + sceGameLiveStreamingGetProgramInfo(); + } catch (const std::runtime_error&) { + threwProgramInfo = true; + } + Require(threwProgramInfo); + + Require(sceGameLiveStreamingTerminate() == 0); +} diff --git a/core/libs/tests/GuestGameUpdate.cpp b/core/libs/tests/GuestGameUpdate.cpp index 12aa20e597..13fcac85c2 100644 --- a/core/libs/tests/GuestGameUpdate.cpp +++ b/core/libs/tests/GuestGameUpdate.cpp @@ -3,16 +3,60 @@ #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 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(); } +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); @@ -23,14 +67,20 @@ int main() { GameUpdateAddcontVersionInfo info{}; info.size = sizeof(info); + constexpr int alreadyInitialized = static_cast(0x80412802); + constexpr int notFound = static_cast(0x80412805); + + 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); - bool threw = false; - try { sceGameUpdateGetAddcontLatestVersion(0, nullptr, &info); } - catch (const std::runtime_error&) { threw = true; } - Require(threw); + Require(sceGameUpdateGetAddcontLatestVersion(0, nullptr, &info) == invalidArgument); info.size = sizeof(info) - 1; Require(sceGameUpdateGetAddcontLatestVersion(0, label, &info) == invalidSize); @@ -43,6 +93,7 @@ int main() { for (std::size_t i = sizeof(info.size); i < sizeof(info); ++i) Require(bytes[i] == 0); Require(sceGameUpdateTerminate() == 0); + Require(sceGameUpdateTerminate() == notInitialized); info.size = sizeof(info); Require(sceGameUpdateGetAddcontLatestVersion(0, label, &info) == notInitialized); } diff --git a/core/libs/tests/GuestGenericCategory.cpp b/core/libs/tests/GuestGenericCategory.cpp new file mode 100644 index 0000000000..b4f3f98687 --- /dev/null +++ b/core/libs/tests/GuestGenericCategory.cpp @@ -0,0 +1,99 @@ +#include "prx/libc/include/GuestLocale.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" +#include +#include +#include +#include +#include +#include +#include + +struct Category { void* const* vtable; }; +struct ErrorCode { int value; const Category* category; }; +using Message = GuestLocale::String* (APS5_VABI *)(GuestLocale::String*, const Category*, int); +using Name = const char* (APS5_VABI *)(const Category*); +using Condition = ErrorCode (APS5_VABI *)(const Category*, int); + +extern "C" { +const char* APS5_VABI _ZSt13_Syserror_mapi_nid_postfix(int value); +const Category* APS5_VABI _ZSt16generic_categoryv_nid_postfix(); +GuestLocale::String* APS5_VABI _ZNKSt23_Generic_error_category7messageEi_nid_postfix( + GuestLocale::String* result, const Category* self, int value); +int* APS5_VABI __error_nid_postfix(); +void APS5_VABI _ZdlPv_nid_postfix(void* pointer); +} + +static void Require(bool value, int line) { + if (value) return; + std::fprintf(stderr, "generic category check failed at line %d\n", line); + std::exit(1); +} +#define REQUIRE(value) Require((value), __LINE__) + +static void CheckMessage(Message message, const Category* category, int value, const char* expected) { + constexpr std::uint64_t marker = 0xA55AA55AA55AA55Aull; + struct Guarded { std::uint64_t before; GuestLocale::String result; std::uint64_t after; }; + Guarded guarded {marker, {}, marker}; + std::memset(&guarded.result, 0xcd, sizeof(guarded.result)); + *__error_nid_postfix() = 61; + REQUIRE(message(&guarded.result, category, value) == &guarded.result); + REQUIRE(*__error_nid_postfix() == 61); + REQUIRE(guarded.before == marker && guarded.after == marker); + const auto length = std::strlen(expected); + REQUIRE(guarded.result.reserved == 0 && guarded.result.size == length); + REQUIRE(guarded.result.capacity >= length && (guarded.result.capacity & 15) == 15); + const char* text = guarded.result.capacity <= 15 ? guarded.result.buffer : guarded.result.pointer; + REQUIRE(std::memcmp(text, expected, length + 1) == 0); + REQUIRE((guarded.result.capacity <= 15) == (length <= 15)); + if (length > 15) _ZdlPv_nid_postfix(guarded.result.pointer); +} + +int main() { + const std::array api {}; + ApplicationHeapRegister_nid_no_patch(api.data()); + const Category* category = _ZSt16generic_categoryv_nid_postfix(); + REQUIRE(category && category == _ZSt16generic_categoryv_nid_postfix()); + REQUIRE(std::strcmp(reinterpret_cast(category->vtable[2])(category), "generic") == 0); + const auto message = reinterpret_cast(category->vtable[3]); + const auto condition = reinterpret_cast(category->vtable[4])(category, 35); + REQUIRE(condition.value == 35 && condition.category == category); + + struct Expected { int value; const char* text; }; + const Expected expected[] { + {3, "no such process"}, {14, "bad address"}, {13, "permission denied"}, + {25, "inappropriate io control operation"}, {35, "operation would block"}, + {45, "not supported"}, {47, "address family not supported"}, {60, "timed out"}, + {78, "function not supported"}, {95, "state not recoverable"}, {96, "owner dead"} + }; + for (const auto& error : expected) { + *__error_nid_postfix() = 61; + const char* mapped = _ZSt13_Syserror_mapi_nid_postfix(error.value); + REQUIRE(mapped && std::strcmp(mapped, error.text) == 0 && *__error_nid_postfix() == 61); + CheckMessage(message, category, error.value, error.text); + CheckMessage(_ZNKSt23_Generic_error_category7messageEi_nid_postfix, category, error.value, error.text); + } + + unsigned known = 0; + for (int value = 1; value <= 96; ++value) { + const char* mapped = _ZSt13_Syserror_mapi_nid_postfix(value); + if (mapped) ++known; + CheckMessage(message, category, value, mapped ? mapped : "unknown error"); + } + REQUIRE(known == 72); + for (int value : {std::numeric_limits::min(), -1, 0, 15, 44, 97, std::numeric_limits::max()}) { + *__error_nid_postfix() = 61; + REQUIRE(_ZSt13_Syserror_mapi_nid_postfix(value) == nullptr && *__error_nid_postfix() == 61); + CheckMessage(message, category, value, "unknown error"); + } + GuestLocale::String first {}, second {}; + REQUIRE(message(&first, category, 25) == &first && message(&second, category, 11) == &second); + REQUIRE(first.pointer != second.pointer); + _ZdlPv_nid_postfix(second.pointer); + REQUIRE(std::strcmp(first.pointer, "inappropriate io control operation") == 0); + _ZdlPv_nid_postfix(first.pointer); + bool rejected = false; + try { message(nullptr, category, 22); } + catch (const std::invalid_argument&) { rejected = true; } + REQUIRE(rejected); + std::puts("guest generic category, FreeBSD error map and message ownership: PASS"); +} diff --git a/core/libs/tests/GuestGetitimer.cpp b/core/libs/tests/GuestGetitimer.cpp new file mode 100644 index 0000000000..1fa5f82516 --- /dev/null +++ b/core/libs/tests/GuestGetitimer.cpp @@ -0,0 +1,105 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct GuestItimerval { + KernelTimeval interval; + KernelTimeval value; +}; + +static_assert(sizeof(GuestItimerval) == 32 && offsetof(GuestItimerval, value) == 16); + +extern "C" { +int APS5_VABI getitimer_nid_postfix(int, GuestItimerval*); +unsigned int APS5_VABI GuestAlarm_nid_no_patch(unsigned int); +int* APS5_VABI __error_nid_postfix(); +using Handler = void (APS5_VABI *)(int); +Handler APS5_VABI signal_nid_postfix(int, Handler); +} + +static void Require(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +static std::int64_t ReadRemaining() { + struct { + std::uint64_t before; + GuestItimerval timer; + std::uint64_t after; + } guarded; + std::memset(&guarded, 0xA5, sizeof(guarded)); + *__error_nid_postfix() = 12345; + Require(getitimer_nid_postfix(0, &guarded.timer) == 0, "getitimer failed"); + Require(*__error_nid_postfix() == 12345, "getitimer changed errno on success"); + Require(guarded.before == 0xA5A5A5A5A5A5A5A5ull && guarded.after == guarded.before, "getitimer wrote outside itimerval"); + Require(guarded.timer.interval.tv_sec == 0 && guarded.timer.interval.tv_usec == 0, "alarm acquired a repeat interval"); + Require(guarded.timer.value.tv_sec >= 0 && guarded.timer.value.tv_usec >= 0 + && guarded.timer.value.tv_usec < 1000000, "invalid timeval normalization"); + return guarded.timer.value.tv_sec * 1000000 + guarded.timer.value.tv_usec; +} + +int main() { + GuestAlarm_nid_no_patch(0); + Require(ReadRemaining() == 0, "inactive alarm is not zero"); + + for (int invalid : {-1, 3, std::numeric_limits::min(), std::numeric_limits::max()}) { + GuestItimerval output{{11, 22}, {33, 44}}; + const auto saved = output; + Require(getitimer_nid_postfix(invalid, &output) == -1 && *__error_nid_postfix() == 22, "invalid timer did not report EINVAL"); + Require(std::memcmp(&output, &saved, sizeof(output)) == 0, "invalid timer changed output"); + 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 disarmed : {1, 2}) { + GuestItimerval output{{11, 22}, {33, 44}}; + *__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}); + Require(signal_nid_postfix(14, ignore) == nullptr, "SIGALRM was not initially defaulted"); + Require(GuestAlarm_nid_no_patch(30) == 0, "alarm was already active"); + const auto first = ReadRemaining(); + Require(first > 0 && first <= 30000000, "armed alarm is not visible"); + std::this_thread::sleep_for(std::chrono::milliseconds(25)); + Require(ReadRemaining() < first, "getitimer does not count down"); + std::array readers; + for (auto& reader : readers) reader = std::thread([] { + for (int i = 0; i < 100; ++i) Require(ReadRemaining() > 0, "reading consumed the alarm"); + }); + for (auto& reader : readers) reader.join(); + Require(GuestAlarm_nid_no_patch(0) > 0, "getitimer cancelled the alarm"); + Require(ReadRemaining() == 0, "cancelled alarm is not zero"); + + 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"); + GuestAlarm_nid_no_patch(0); +} diff --git a/core/libs/tests/GuestGetlogin.cpp b/core/libs/tests/GuestGetlogin.cpp new file mode 100644 index 0000000000..b006a283fc --- /dev/null +++ b/core/libs/tests/GuestGetlogin.cpp @@ -0,0 +1,81 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#include +#else +#include +#include +#endif + +extern "C" int APS5_VABI getlogin_r_nid_postfix(char*, int); +extern "C" int* APS5_VABI __error_nid_postfix(); + +static void Check(bool condition, int line) { + if (!condition) { + std::fprintf(stderr, "getlogin_r check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static std::string HostLogin() { +#ifdef _WIN32 + wchar_t name[UNLEN + 1]; + DWORD count = UNLEN + 1; + Require(GetUserNameW(name, &count) != 0); + std::array output{}; + Require(WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, name, -1, + output.data(), static_cast(output.size()), nullptr, nullptr) > 0); + return output.data(); +#else + const passwd* account = ::getpwuid(::geteuid()); + Require(account != nullptr && account->pw_name != nullptr && account->pw_name[0] != '\0'); + return account->pw_name; +#endif +} + +static void RequireLogin(const std::string& expected) { + std::vector output(expected.size() + 3, '\x5A'); + *__error_nid_postfix() = 12345; + Require(getlogin_r_nid_postfix(output.data() + 1, static_cast(expected.size() + 1)) == 0); + Require(*__error_nid_postfix() == 12345); + Require(output.front() == '\x5A' && output.back() == '\x5A'); + Require(std::memcmp(output.data() + 1, expected.c_str(), expected.size() + 1) == 0); + output.assign(output.size(), '\x5A'); + const auto saved = output; + Require(getlogin_r_nid_postfix(output.data() + 1, static_cast(expected.size())) == 34); + Require(*__error_nid_postfix() == 12345 && output == saved); +} + +int main() { + std::array output; + output.fill('\x5A'); + const auto untouched = output; + for (int size : {0, -1, std::numeric_limits::min()}) { + *__error_nid_postfix() = 12345; + Require(getlogin_r_nid_postfix(output.data(), size) == 34); + Require(*__error_nid_postfix() == 12345 && output == untouched); + } + Require(getlogin_r_nid_postfix(nullptr, 1) == 14); + Require(*__error_nid_postfix() == 12345); + const std::string expected = HostLogin(); + *__error_nid_postfix() = 12345; + Require(getlogin_r_nid_postfix(output.data(), static_cast(output.size())) == 0); + Require(*__error_nid_postfix() == 12345); + Require(std::strcmp(output.data(), expected.c_str()) == 0); + RequireLogin(expected); + std::array readers; + for (auto& reader : readers) reader = std::thread([&] { + for (int i = 0; i != 20; ++i) RequireLogin(expected); + }); + for (auto& reader : readers) reader.join(); +} diff --git a/core/libs/tests/GuestGetloginNoSession.cpp b/core/libs/tests/GuestGetloginNoSession.cpp new file mode 100644 index 0000000000..c9f29f5513 --- /dev/null +++ b/core/libs/tests/GuestGetloginNoSession.cpp @@ -0,0 +1,39 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI getlogin_r_nid_postfix(char*, int); + +static int loginError = ENXIO; + +extern "C" int getlogin_r(char* output, std::size_t size) { + if (loginError != 0) return loginError; + constexpr char sessionName[] = "anyps5-other-login-session"; + if (size < sizeof(sessionName)) return ERANGE; + std::memcpy(output, sessionName, sizeof(sessionName)); + return 0; +} + +int main() { + const passwd* account = ::getpwuid(::geteuid()); + if (account == nullptr || account->pw_name == nullptr || account->pw_name[0] == '\0') + std::abort(); + const std::string expected(account->pw_name); + for (int error : {ENXIO, ENOENT, 0}) { + loginError = error; + std::array output{}; + errno = 12345; + const int result = getlogin_r_nid_postfix(output.data(), static_cast(output.size())); + if (result != 0 || errno != 12345 || output.data() != expected) { + std::fprintf(stderr, "getlogin_r changed with host login-session result %d: guest result %d\n", + error, result); + return 1; + } + } +} diff --git a/core/libs/tests/GuestGetopt.cpp b/core/libs/tests/GuestGetopt.cpp new file mode 100644 index 0000000000..26e0ca0803 --- /dev/null +++ b/core/libs/tests/GuestGetopt.cpp @@ -0,0 +1,434 @@ +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +struct GuestOption { + const char* name; + int hasArgument; + int* flag; + int value; +}; + +extern "C" { +int APS5_VABI getopt_nid_postfix(int, char* const*, const char*); +int APS5_VABI getopt_long_nid_postfix(int, char* const*, const char*, const GuestOption*, int*); +int APS5_VABI 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; +extern int optind_nid_postfix; +extern int optopt_nid_postfix; +extern int optreset_nid_postfix; +extern char* optarg_nid_postfix; +extern FileStream* __stderrp_nid_postfix; +} + +namespace { + +void Expect(bool value, const std::string& what) { + if (value) return; + std::fprintf(stderr, "failed: %s\n", what.c_str()); + std::abort(); +} + +void ExpectText(const std::string& actual, const std::string& expected, const std::string& what) { + Expect(actual == expected, what + ": `" + actual + "' != `" + expected + "'"); +} + +class ArgumentVector { + std::vector storage; + std::vector pointers; + +public: + explicit ArgumentVector(const std::string& line) { + std::istringstream words(line); + for (std::string word; words >> word;) storage.push_back(word); + for (auto& word : storage) pointers.push_back(word.data()); + pointers.push_back(nullptr); + } + int Count() const { return static_cast(storage.size()); } + char* const* Values() { return pointers.data(); } + std::string Joined() const { + std::string result; + for (std::size_t i = 0; i + 1 < pointers.size(); ++i) result += (i ? " " : "") + std::string(pointers[i]); + return result; + } +}; + +class StderrCapture { + FileStream* previous; + FileStream stream; + +public: + StderrCapture() : previous(__stderrp_nid_postfix), stream(std::tmpfile()) { __stderrp_nid_postfix = &stream; } + ~StderrCapture() { + __stderrp_nid_postfix = previous; + stream.Close(); + } + std::string Text() { + std::FILE* handle = stream.GetHandle(); + std::fflush(handle); + std::rewind(handle); + std::string text; + for (int character; (character = std::fgetc(handle)) != EOF;) text.push_back(static_cast(character)); + return text; + } +}; + +void Reset() { + optind_nid_postfix = optreset_nid_postfix = 1; + opterr_nid_postfix = 1; +} + +std::string Finish(std::string result, int count) { + if (result.empty()) result = "|"; + else result.back() = '|'; + return result + std::to_string(count - optind_nid_postfix); +} + +std::string RunGetopt(const char* options, ArgumentVector& arguments) { + Reset(); + std::string result; + for (int character; (character = getopt_nid_postfix(arguments.Count(), arguments.Values(), options)) != -1;) { + Expect(character != ':', "getopt returned `:'"); + const char* found = std::strchr(options, character); + if (!found) result += "!" + std::string(1, static_cast(character)) + ","; + else if (found[1] != ':') result += std::string(1, static_cast(character)) + ","; + else result += std::string(1, static_cast(character)) + "=" + optarg_nid_postfix + ","; + } + return Finish(result, arguments.Count()); +} + +std::string RunGetoptLong(const char* options, const GuestOption* longOptions, ArgumentVector& arguments) { + Reset(); + std::string result; + int index = -2; + for (int character; (character = getopt_long_nid_postfix(arguments.Count(), arguments.Values(), options, longOptions, &index)) != -1;) { + Expect(character != ':', "getopt_long returned `:'"); + if (index == -2) { + const char* found = std::strchr(options, character); + if (!found) result += "!" + std::string(1, static_cast(character)) + ","; + else if (found[1] != ':') result += std::string(1, static_cast(character)) + ","; + else result += std::string(1, static_cast(character)) + "=" + optarg_nid_postfix + ","; + } else { + const GuestOption& option = longOptions[index]; + result += "-" + std::string(option.name); + if (option.hasArgument == 1) result += "=" + std::string(optarg_nid_postfix); + if (option.hasArgument == 2 && optarg_nid_postfix) result += "=" + std::string(optarg_nid_postfix); + result += ","; + } + index = -2; + } + return Finish(result, arguments.Count()); +} + +struct GetoptCase { + const char* options; + const char* arguments; + const char* result; +}; + +void FreeBsdGetoptCases() { + const GetoptCase cases[] = { + {"c:d", "foo -c 1 -d foo", "c=1,d|1"}, + {"c:d", "foo -d foo bar", "d|2"}, + {"c:d", "foo -c 2 foo bar", "c=2|2"}, + {"c:d", "foo -e 1 foo bar", "!?|3"}, + {"c:d", "foo -d -- -c 1", "d|2"}, + {"c:d", "foo -c- 1", "c=-|1"}, + {"c:d", "foo -d - 1", "d|2"}, + }; + for (const auto& test : cases) { + ArgumentVector arguments(test.arguments); + StderrCapture capture; + ExpectText(RunGetopt(test.options, arguments), test.result, std::string("getopt ") + test.arguments); + const std::string expectedError = std::strcmp(test.result, "!?|3") == 0 ? "eboot.bin: illegal option -- e\n" : ""; + ExpectText(capture.Text(), expectedError, std::string("getopt stderr ") + test.arguments); + } +} + +void FreeBsdGetoptLongCases() { + const GuestOption gnu[] = { + {"required", 1, nullptr, 'r'}, + {"optional", 2, nullptr, 'o'}, + {"none", 0, nullptr, 'n'}, + {"color", 0, nullptr, 'C'}, + {"colour", 0, nullptr, 'C'}, + {nullptr, 0, nullptr, 0}, + }; + const GetoptCase cases[] = { + {"abc:", "foo --req foobar", "-required=foobar|0"}, + {"abc:", "foo --opt=bazbug", "-optional=bazbug|0"}, + {"abc:", "foo --col", "-color|0"}, + {"abc:", "foo --colour", "-colour|0"}, + {"abc:", "foo -a -b -cfoobar --required foobar --optional=bazbug --none random --col --color --colour", + "a,b,c=foobar,-required=foobar,-optional=bazbug,-none,-color,-color,-colour|1"}, + }; + for (const auto& test : cases) { + ArgumentVector arguments(test.arguments); + StderrCapture capture; + ExpectText(RunGetoptLong(test.options, gnu, arguments), test.result, std::string("getopt_long ") + test.arguments); + ExpectText(capture.Text(), "", std::string("getopt_long stderr ") + test.arguments); + } + + int flags[3] {}; + const GuestOption lists[] = { + {"list-foobar", 0, &flags[0], 'f'}, + {"list-goobar", 0, &flags[1], 'g'}, + {"list", 0, &flags[2], 'l'}, + {nullptr, 0, nullptr, 0}, + }; + ArgumentVector arguments("foo --list"); + ExpectText(RunGetoptLong("fgl", lists, arguments), "-list|0", "exact match among prefixes"); + Expect(flags[0] == 0 && flags[1] == 0 && flags[2] == 'l', "flag set by an exact match"); +} + +void ShortOptions() { + ArgumentVector grouped("foo -dcvalue -dd rest"); + ExpectText(RunGetopt("c:d", grouped), "d,c=value,d,d|1", "grouped options"); + + ArgumentVector optional("foo -afoo -a bar"); + Reset(); + Expect(getopt_nid_postfix(optional.Count(), optional.Values(), "a::") == 'a' && std::strcmp(optarg_nid_postfix, "foo") == 0, "optional attached"); + Expect(getopt_nid_postfix(optional.Count(), optional.Values(), "a::") == 'a' && optarg_nid_postfix == nullptr && optind_nid_postfix == 3, "optional absent"); + Expect(getopt_nid_postfix(optional.Count(), optional.Values(), "a::") == -1 && optind_nid_postfix == 3, "optional stops at operand"); + + ArgumentVector dash("foo - -x"); + Reset(); + Expect(getopt_nid_postfix(dash.Count(), dash.Values(), "-x") == '-' && optind_nid_postfix == 2, "lone dash as option"); + Expect(getopt_nid_postfix(dash.Count(), dash.Values(), "-x") == 'x' && optind_nid_postfix == 3, "after lone dash"); + + ArgumentVector unknown("foo -x"); + { + StderrCapture capture; + Reset(); + opterr_nid_postfix = 0; + Expect(getopt_nid_postfix(unknown.Count(), unknown.Values(), "ab") == '?', "unknown option returns ?"); + Expect(optopt_nid_postfix == 'x' && optind_nid_postfix == 2, "optopt of unknown option"); + ExpectText(capture.Text(), "", "opterr 0 is silent"); + } + { + StderrCapture capture; + Reset(); + Expect(getopt_nid_postfix(unknown.Count(), unknown.Values(), ":ab") == '?' && optopt_nid_postfix == 'x', "leading colon unknown"); + ExpectText(capture.Text(), "", "leading colon is silent"); + } + + ArgumentVector missing("foo -c"); + { + StderrCapture capture; + Reset(); + Expect(getopt_nid_postfix(missing.Count(), missing.Values(), "c:") == '?' && optopt_nid_postfix == 'c' && optind_nid_postfix == 2, "missing argument"); + ExpectText(capture.Text(), "eboot.bin: option requires an argument -- c\n", "missing argument message"); + } + { + StderrCapture capture; + Reset(); + Expect(getopt_nid_postfix(missing.Count(), missing.Values(), ":c:") == ':' && optopt_nid_postfix == 'c', "missing argument with leading colon"); + ExpectText(capture.Text(), "", "leading colon missing argument is silent"); + } + + ArgumentVector first("foo -ab"); + ArgumentVector second("bar -c"); + Reset(); + Expect(getopt_nid_postfix(first.Count(), first.Values(), "abc") == 'a', "first parse"); + optind_nid_postfix = optreset_nid_postfix = 1; + Expect(getopt_nid_postfix(second.Count(), second.Values(), "abc") == 'c', "optreset restarts getopt"); + Expect(getopt_nid_postfix(second.Count(), second.Values(), "abc") == -1 && optind_nid_postfix == 2, "restarted parse ends"); + + ArgumentVector permuted("foo a -b c"); + ExpectText(RunGetopt("bc", permuted), "|3", "getopt does not permute"); +} + +void 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[] = { + {"required", 1, nullptr, 'r'}, + {"optional", 2, nullptr, 'o'}, + {"none", 0, nullptr, 'n'}, + {"verbose", 0, nullptr, 'v'}, + {"version", 0, nullptr, 'V'}, + {"flag", 0, &flag, 7}, + {nullptr, 0, nullptr, 0}, + }; + + ArgumentVector kinds("foo --required=1 --required 2 --optional=3 --optional 4 --none --flag"); + ExpectText(RunGetoptLong("", options, kinds), "-required=1,-required=2,-optional=3,-optional,-none,-flag|1", "three argument kinds"); + Expect(flag == 7, "flag pointer set"); + ExpectText(kinds.Joined(), "foo --required=1 --required 2 --optional=3 --optional --none --flag 4", "operand permuted to the end"); + + ArgumentVector abbreviations("foo --verb --vers --req=x --opt"); + ExpectText(RunGetoptLong("", options, abbreviations), "-verbose,-version,-required=x,-optional|0", "unique abbreviations"); + + struct ErrorCase { + const char* options; + const char* arguments; + int result; + int optopt; + const char* message; + }; + const ErrorCase errors[] = { + {"", "foo --ver", '?', 0, "eboot.bin: option `--ver' is ambiguous\n"}, + {"", "foo --none=1", '?', 'n', "eboot.bin: option `--none' doesn't allow an argument\n"}, + {"", "foo --flag=1", '?', 0, "eboot.bin: option `--flag' doesn't allow an argument\n"}, + {"", "foo --required", '?', 'r', "eboot.bin: option `--required' requires an argument\n"}, + {":", "foo --required", ':', 'r', ""}, + {"", "foo --nope=1", '?', 0, "eboot.bin: unrecognized option `--nope=1'\n"}, + {"", "foo -x", '?', 'x', "eboot.bin: invalid option -- x\n"}, + {"c:", "foo -c", '?', 'c', "eboot.bin: option requires an argument -- c\n"}, + {":c:", "foo -c", ':', 'c', ""}, + {"W;", "foo -W ver", '?', 0, "eboot.bin: option `-W ver' is ambiguous\n"}, + {"W;", "foo -W", '?', 'W', "eboot.bin: option requires an argument -- W\n"}, + }; + for (const auto& test : errors) { + ArgumentVector arguments(test.arguments); + StderrCapture capture; + Reset(); + optopt_nid_postfix = -1; + const int result = getopt_long_nid_postfix(arguments.Count(), arguments.Values(), test.options, options, nullptr); + Expect(result == test.result, std::string("getopt_long result ") + test.arguments); + Expect(optopt_nid_postfix == test.optopt, std::string("getopt_long optopt ") + test.arguments); + ExpectText(capture.Text(), test.message, std::string("getopt_long message ") + test.arguments); + } + { + ArgumentVector arguments("foo --ver -x"); + StderrCapture capture; + Reset(); + opterr_nid_postfix = 0; + Expect(getopt_long_nid_postfix(arguments.Count(), arguments.Values(), "", options, nullptr) == '?' && optopt_nid_postfix == 0, "silent ambiguity"); + Expect(getopt_long_nid_postfix(arguments.Count(), arguments.Values(), "", options, nullptr) == '?' && optopt_nid_postfix == 'x', "silent unknown"); + ExpectText(capture.Text(), "", "opterr 0 silences getopt_long"); + } + + ArgumentVector wOption("foo -W verbose -Wversion"); + ExpectText(RunGetoptLong("W;", options, wOption), "-verbose,-version|0", "W; long options"); + + ArgumentVector terminated("foo a -b -- -d c"); + ExpectText(RunGetoptLong("bd", options, terminated), "b|3", "-- ends getopt_long"); + ExpectText(terminated.Joined(), "foo -b -- a -d c", "-- permutes skipped operands"); + + ArgumentVector interleaved("foo a -b c -d"); + ExpectText(RunGetoptLong("bd", options, interleaved), "b,d|2", "permutation"); + ExpectText(interleaved.Joined(), "foo -b -d a c", "permuted order"); + + ArgumentVector plus("foo a -b"); + ExpectText(RunGetoptLong("+b", options, plus), "|2", "+ stops at the first operand"); + + ArgumentVector inOrder("foo a -b c"); + Reset(); + std::string sequence; + for (int character; (character = getopt_long_nid_postfix(inOrder.Count(), inOrder.Values(), "-b", options, nullptr)) != -1;) + sequence += character == 1 ? std::string("1:") + optarg_nid_postfix + "," : std::string(1, static_cast(character)) + ","; + ExpectText(sequence, "1:a,b,1:c,", "- returns operands in order"); + + ArgumentVector restartFirst("foo -bd"); + ArgumentVector restartSecond("bar -b"); + Reset(); + Expect(getopt_long_nid_postfix(restartFirst.Count(), restartFirst.Values(), "bd", options, nullptr) == 'b', "getopt_long first parse"); + optind_nid_postfix = 0; + Expect(getopt_long_nid_postfix(restartSecond.Count(), restartSecond.Values(), "bd", options, nullptr) == 'b', "optind 0 restarts getopt_long"); + Expect(getopt_long_nid_postfix(restartSecond.Count(), restartSecond.Values(), "bd", options, nullptr) == -1 && optind_nid_postfix == 2, "restarted getopt_long ends"); + Reset(); + Expect(getopt_long_nid_postfix(restartFirst.Count(), restartFirst.Values(), "bd", options, nullptr) == 'b', "getopt_long parse before optreset"); + optind_nid_postfix = optreset_nid_postfix = 1; + Expect(getopt_long_nid_postfix(restartSecond.Count(), restartSecond.Values(), "bd", options, nullptr) == 'b', "optreset restarts getopt_long"); + + Expect(setenv_nid_postfix("POSIXLY_CORRECT", "1", 1) == 0, "setenv POSIXLY_CORRECT"); + { + ArgumentVector posix("foo a -b"); + ExpectText(RunGetoptLong("b", options, posix), "|2", "POSIXLY_CORRECT stops at the first operand"); + ArgumentVector illegal("foo -x"); + StderrCapture capture; + Reset(); + Expect(getopt_long_nid_postfix(illegal.Count(), illegal.Values(), "b", options, nullptr) == '?', "POSIXLY_CORRECT unknown option"); + ExpectText(capture.Text(), "eboot.bin: illegal option -- x\n", "POSIXLY_CORRECT message"); + } + Expect(unsetenv_nid_postfix("POSIXLY_CORRECT") == 0, "unsetenv POSIXLY_CORRECT"); + ArgumentVector gnu("foo a -b"); + ExpectText(RunGetoptLong("b", options, gnu), "b|1", "permutation after POSIXLY_CORRECT is unset"); + + Reset(); + Expect(getopt_long_nid_postfix(gnu.Count(), gnu.Values(), nullptr, options, nullptr) == -1, "null option string"); +} + +} + +int main() { + Expect(opterr_nid_postfix == 1 && optind_nid_postfix == 1 && optopt_nid_postfix == 0 && + optreset_nid_postfix == 0 && optarg_nid_postfix == nullptr, "initial values"); + FreeBsdGetoptCases(); + FreeBsdGetoptLongCases(); + ShortOptions(); + LongOptions(); + LongOnlyOptions(); +} diff --git a/core/libs/tests/GuestGetrlimit.cpp b/core/libs/tests/GuestGetrlimit.cpp index 598ae8ed94..5d6d5d2999 100644 --- a/core/libs/tests/GuestGetrlimit.cpp +++ b/core/libs/tests/GuestGetrlimit.cpp @@ -1,10 +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 { @@ -14,12 +25,30 @@ 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; +static constexpr int GUEST_RLIMIT_CPU = 0; +static constexpr int GUEST_RLIMIT_FSIZE = 1; static constexpr int GUEST_RLIMIT_DATA = 2; +static constexpr int GUEST_RLIMIT_STACK = 3; +static constexpr int GUEST_RLIMIT_CORE = 4; +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_RLIMIT_SBSIZE = 9; static constexpr int GUEST_RLIMIT_AS = 10; +static constexpr int GUEST_RLIMIT_NPTS = 11; +static constexpr int GUEST_RLIMIT_SWAP = 12; +static constexpr int GUEST_RLIMIT_KQUEUES = 13; +static constexpr int GUEST_RLIMIT_UMTXP = 14; static constexpr int GUEST_RLIM_NLIMITS = 15; static constexpr std::int64_t GUEST_RLIM_INFINITY = std::numeric_limits::max(); @@ -32,6 +61,80 @@ 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); @@ -62,17 +165,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); @@ -80,11 +172,44 @@ int main() { RequireHostLimit(GUEST_RLIMIT_DATA, RLIMIT_DATA); LoweredHostLimitIsReported(); #endif + for (int resource : { + GUEST_RLIMIT_CPU, + GUEST_RLIMIT_FSIZE, + GUEST_RLIMIT_CORE, + GUEST_RLIMIT_RSS, + GUEST_RLIMIT_MEMLOCK, + GUEST_RLIMIT_SBSIZE, + GUEST_RLIMIT_AS, + GUEST_RLIMIT_NPTS, + GUEST_RLIMIT_SWAP, + GUEST_RLIMIT_KQUEUES, + GUEST_RLIMIT_UMTXP, + }) { + const ResourceLimit guest = GuestLimit(resource); + Require(guest.rlim_cur == GUEST_RLIM_INFINITY); + Require(guest.rlim_max == GUEST_RLIM_INFINITY); + RequireError(resource, nullptr, GUEST_EFAULT); + } ResourceLimit limit{}; RequireError(GUEST_RLIM_NLIMITS, &limit, GUEST_EINVAL); RequireError(-1, &limit, GUEST_EINVAL); RequireError(GUEST_RLIM_NLIMITS, nullptr, GUEST_EINVAL); + RequireError(-1, nullptr, GUEST_EINVAL); RequireError(GUEST_RLIMIT_DATA, nullptr, GUEST_EFAULT); - RequireUnsupported(GUEST_RLIMIT_NOFILE); - RequireUnsupported(GUEST_RLIMIT_AS); + 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 a549808cf4..7fefbdca94 100644 --- a/core/libs/tests/GuestGetrusage.cpp +++ b/core/libs/tests/GuestGetrusage.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include #include struct Timeval { @@ -13,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{}; @@ -27,7 +29,27 @@ 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); + const ResourceUsage empty{}; + Require(std::memcmp(&children, &empty, sizeof(children)) == 0); SpinUntil(0, 400000); const std::int64_t mainThread = CpuMicros(1); Require(mainThread >= 200000); diff --git a/core/libs/tests/GuestHeapReportForGame.cpp b/core/libs/tests/GuestHeapReportForGame.cpp new file mode 100644 index 0000000000..9238d7bdb1 --- /dev/null +++ b/core/libs/tests/GuestHeapReportForGame.cpp @@ -0,0 +1,44 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +void APS5_VABI sceLibcInternalBacktraceForGame_nid_postfix(const char* heapName); +void APS5_VABI sceLibcInternalHeapErrorReportForGame_nid_postfix(void* heap, void* block, std::uint32_t error); +} + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stdout, "%s\n", message); + std::abort(); +} + +int main() { + const auto path = std::filesystem::temp_directory_path() / ("anyps5-heap-report-" + std::to_string(std::rand()) + ".txt"); + Require(std::freopen(path.string().c_str(), "w", stderr) != nullptr, "stderr redirection failed"); + + char heapName[] = "SceLibcHeap"; + sceLibcInternalBacktraceForGame_nid_postfix(heapName); + sceLibcInternalBacktraceForGame_nid_postfix(nullptr); + int heap = 0; + int block = 0; + sceLibcInternalHeapErrorReportForGame_nid_postfix(&heap, &block, 0x1234u); + std::fflush(stderr); + std::fclose(stderr); + + std::ifstream file(path); + std::stringstream text; + text << file.rdbuf(); + file.close(); + std::filesystem::remove(path); + const std::string report = text.str(); + Require(report.find("[SceLibcHeap] backtrace requested from guest 0x") != std::string::npos, "heap name not reported"); + Require(report.find("[] backtrace requested from guest 0x") != std::string::npos, "null heap name not reported"); + Require(report.find("[libc] heap error 0x1234: heap ") != std::string::npos, "heap error not reported"); + Require(std::string(heapName) == "SceLibcHeap", "heap name modified"); +} diff --git a/core/libs/tests/GuestHeapRounding.cpp b/core/libs/tests/GuestHeapRounding.cpp index 1fd9527a6d..d888f7311f 100644 --- a/core/libs/tests/GuestHeapRounding.cpp +++ b/core/libs/tests/GuestHeapRounding.cpp @@ -20,6 +20,7 @@ extern "C" { void* APS5_VABI malloc_nid_postfix(std::size_t); void APS5_VABI free_nid_postfix(void*); void* APS5_VABI realloc_nid_postfix(void*, std::size_t); +int* APS5_VABI __error_nid_postfix(); } namespace { @@ -76,13 +77,14 @@ DWORD runChild(const wchar_t* mode) { int runMallocOverflow() { registerDefaultHeap(); + *__error_nid_postfix() = 0; try { auto* pointer = malloc_nid_postfix(std::numeric_limits::max() - HeaderBytes); - free_nid_postfix(pointer); - return 1; - } catch (const std::length_error& error) { - std::fprintf(stderr, "malloc overflow rejected: %s\n", error.what()); - return 0; + if (pointer != nullptr) { + free_nid_postfix(pointer); + return 1; + } + return *__error_nid_postfix() == 12 ? 0 : 4; } catch (const std::exception& error) { std::fprintf(stderr, "malloc failed with an unexpected exception: %s\n", error.what()); return 2; @@ -98,15 +100,14 @@ int runReallocOverflow() { auto* original = static_cast(malloc_nid_postfix(bytes)); std::memset(original, 0x5a, bytes); bool rejectedWithExpectedError = false; + *__error_nid_postfix() = 0; try { auto* result = realloc_nid_postfix(original, std::numeric_limits::max() - HeaderBytes); if (result != nullptr) { free_nid_postfix(result); return 4; } - } catch (const std::length_error& error) { - rejectedWithExpectedError = true; - std::fprintf(stderr, "realloc overflow rejected: %s\n", error.what()); + rejectedWithExpectedError = *__error_nid_postfix() == 12; } catch (const std::exception& error) { std::fprintf(stderr, "realloc failed with an unexpected exception: %s\n", error.what()); } catch (...) { diff --git a/core/libs/tests/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/GuestHmd2.cpp b/core/libs/tests/GuestHmd2.cpp index 85af8b0350..4f05d57f02 100644 --- a/core/libs/tests/GuestHmd2.cpp +++ b/core/libs/tests/GuestHmd2.cpp @@ -1,17 +1,66 @@ -#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 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(); } namespace { +alignas(16) std::array process{}; + void Require(bool condition, const char* message) { if (!condition) { std::fprintf(stderr, "HMD2: %s\n", message); @@ -19,19 +68,237 @@ 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"); - bool threw = false; - try { - sceHmd2Open(); - } catch (const std::runtime_error&) { - threw = true; - } - Require(threw, "open after failed initialization did not throw"); + SizeQueriesReturnTheConsoleSizes(); + InstallProcessParameters(); + TestBeforeInitialize(); + TestWithoutHeadset(); std::puts("HMD2 tests passed"); return 0; } diff --git a/core/libs/tests/GuestHostStandardError.cpp b/core/libs/tests/GuestHostStandardError.cpp new file mode 100644 index 0000000000..4c3fdc2309 --- /dev/null +++ b/core/libs/tests/GuestHostStandardError.cpp @@ -0,0 +1,174 @@ +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef __linux__ +#include +#endif + +extern "C" { +FileStream* APS5_VABI freopen_nid_postfix(const char*, const char*, FileStream*); +int APS5_VABI setvbuf_nid_postfix(FileStream*, char*, int, std::size_t); +std::size_t APS5_VABI fwrite_nid_postfix(const void*, std::size_t, std::size_t, FileStream*); +int APS5_VABI fflush_nid_postfix(FileStream*); +int APS5_VABI pipe_nid_postfix(int*); +int APS5_VABI dup2_nid_postfix(int, int); +std::int64_t APS5_VABI write_nid_postfix(int, const void*, std::size_t); +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 { + std::string host; + int status = 0; +}; + +static void RedirectGuest(Redirect redirect, const std::filesystem::path& probe) { + if (redirect == Redirect::Duplicate) { + int guest[2] = {-1, -1}; + Require(pipe_nid_postfix(guest) == 0); + Require(dup2_nid_postfix(guest[1], 2) == 2); + Require(write_nid_postfix(2, "guest", 5) == 5); + return; + } + Require(freopen_nid_postfix(probe.c_str(), "w", __stderrp_nid_postfix) == __stderrp_nid_postfix); + Require(setvbuf_nid_postfix(__stderrp_nid_postfix, nullptr, 0, 0) == 0); + Require(fwrite_nid_postfix("guest", 1, 5, __stderrp_nid_postfix) == 5); +} + +static Outcome Run(Redirect redirect, const std::filesystem::path& probe, bool terminate) { + int host[2] = {-1, -1}; + Require(::pipe(host) == 0); + const pid_t child = ::fork(); + Require(child >= 0); + if (child == 0) { + ::close(host[0]); + if (::dup2(host[1], 2) != 2) ::_exit(70); + ::close(host[1]); + RedirectGuest(redirect, probe); + std::fputs("host\n", stderr); + if (terminate) { + SkipCrashHandler(); + throw std::runtime_error("report kept"); + } + if (redirect == Redirect::Reopen && fflush_nid_postfix(__stderrp_nid_postfix) != 0) ::_exit(71); + ::_exit(0); + } + ::close(host[1]); + Outcome outcome; + char buffer[4096]; + for (ssize_t count; (count = ::read(host[0], buffer, sizeof(buffer))) > 0;) outcome.host.append(buffer, static_cast(count)); + ::close(host[0]); + Require(::waitpid(child, &outcome.status, 0) == child); + return outcome; +} + +static std::string Contents(const std::filesystem::path& path) { + std::ifstream stream(path, std::ios::binary); + return std::string(std::istreambuf_iterator(stream), std::istreambuf_iterator()); +} + +int main() { + const std::filesystem::path probe = "host_stderr_probe_" + std::to_string(::getpid()) + ".txt"; + for (const auto redirect : {Redirect::Duplicate, Redirect::Reopen}) { + const auto written = Run(redirect, probe, false); + Require(WIFEXITED(written.status) && WEXITSTATUS(written.status) == 0); + Require(written.host == "host\n"); + if (redirect == Redirect::Reopen) Require(Contents(probe) == "guest"); + + const auto terminated = Run(redirect, probe, true); + Require(WIFSIGNALED(terminated.status) && WTERMSIG(terminated.status) == SIGABRT); + Require(terminated.host.rfind("host\n", 0) == 0); + Require(terminated.host.find(" what(): report kept\n") != std::string::npos); + if (redirect == Redirect::Reopen) Require(Contents(probe).find("report kept") == std::string::npos); + } + std::filesystem::remove(probe); + + 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/GuestHostname.cpp b/core/libs/tests/GuestHostname.cpp new file mode 100644 index 0000000000..36e18845dc --- /dev/null +++ b/core/libs/tests/GuestHostname.cpp @@ -0,0 +1,60 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI gethostname_nid_postfix(char*, std::size_t); +int* APS5_VABI __error_nid_postfix(); +} + +static void Require(bool condition, int line) { + if (!condition) { + std::fprintf(stderr, "Hostname check failed at line %d\n", line); + std::abort(); + } +} +#define Check(value) Require((value), __LINE__) + +int main() { + constexpr std::string_view expected = "AnyPS5"; + for (std::size_t length = 0; length <= expected.size() + 2; ++length) { + std::vector buffer(length + 2, '#'); + *__error_nid_postfix() = 71; + const int result = gethostname_nid_postfix(buffer.data() + 1, length); + Check(buffer.front() == '#' && buffer.back() == '#'); + if (length <= expected.size()) { + Check(result == -1 && *__error_nid_postfix() == 63); + Check(std::memcmp(buffer.data() + 1, expected.data(), length) == 0); + } else { + Check(result == 0 && *__error_nid_postfix() == 71); + Check(std::strcmp(buffer.data() + 1, expected.data()) == 0); + for (std::size_t index = expected.size() + 2; index < buffer.size(); ++index) Check(buffer[index] == '#'); + } + } + std::array oversized; + oversized.fill('#'); + *__error_nid_postfix() = 71; + Check(gethostname_nid_postfix(oversized.data() + 1, std::numeric_limits::max()) == 0); + Check(*__error_nid_postfix() == 71); + Check(std::strcmp(oversized.data() + 1, expected.data()) == 0); + Check(oversized.front() == '#' && oversized.back() == '#'); + *__error_nid_postfix() = 22; + Check(gethostname_nid_postfix(nullptr, 0) == 0 && *__error_nid_postfix() == 22); + Check(gethostname_nid_postfix(nullptr, 1024) == 0 && *__error_nid_postfix() == 22); + std::thread worker([expected] { + char byte = '#'; + *__error_nid_postfix() = 0; + Check(gethostname_nid_postfix(&byte, 0) == -1 && *__error_nid_postfix() == 63 && byte == '#'); + std::array name{}; + Check(gethostname_nid_postfix(name.data(), name.size()) == 0); + Check(std::strcmp(name.data(), expected.data()) == 0 && *__error_nid_postfix() == 63); + }); + worker.join(); + Check(*__error_nid_postfix() == 22); +} diff --git a/core/libs/tests/GuestHttp.cpp b/core/libs/tests/GuestHttp.cpp index 2faab2957c..93eb0119d3 100644 --- a/core/libs/tests/GuestHttp.cpp +++ b/core/libs/tests/GuestHttp.cpp @@ -1,14 +1,22 @@ #include "SceTypes.hpp" #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*); +int APS5_VABI sceHttpSendRequest(int, const void*, std::size_t); int APS5_VABI sceHttpUriBuild(char*, std::size_t*, std::size_t, const SceHttpUriElement*, std::uint32_t); int APS5_VABI sceHttpUriEscape(char*, std::size_t*, std::size_t, const char*); int APS5_VABI sceHttpUriUnescape(char*, std::size_t*, std::size_t, const char*); @@ -16,12 +24,18 @@ int APS5_VABI sceHttpUriSweepPath(char*, const char*, std::size_t); int APS5_VABI sceHttpCreateEpoll(int, HttpEpollHandle*); int APS5_VABI sceHttpDestroyEpoll(int, HttpEpollHandle); 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); int APS5_VABI sceHttpsLoadCert(int, int, void*, void*, void*); int APS5_VABI sceHttpGetLastErrno(int, int*); int APS5_VABI sceHttpSetResponseHeaderMaxSize(int, std::uint64_t); +int APS5_VABI sceHttpSetRecvBlockSize(int, std::uint32_t); int APS5_VABI sceHttpRedirectCacheFlush(int); int APS5_VABI sceHttpsUnloadCert(int); int APS5_VABI sceHttpsSetSslVersion(int, int); @@ -36,9 +50,61 @@ static void Require(bool value) { if (!value) std::abort(); } static bool Equal(const char* left, const char* right) { return std::strcmp(left, right) == 0; } +static void 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)); + const std::string urls[] = { + "http://" + byte + "/", "http://[" + byte + "]/", byte + "://host/", + "a" + byte + "://host/", "http://host:" + byte + "/" + }; + for (const auto& url : urls) { + 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)); + } + } +} + +static void CheckUriEscapeBytes() { + constexpr char unreserved[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"; + constexpr char hex[] = "0123456789ABCDEF"; + for (unsigned value = 1; value <= 255; ++value) { + const char input[] = {static_cast(value), '\0'}; + char expected[] = {'%', hex[value >> 4], hex[value & 15], '\0'}; + if (std::strchr(unreserved, input[0])) { + expected[0] = input[0]; + expected[1] = '\0'; + } + const auto size = std::strlen(expected) + 1; + std::size_t required = 0; + Require(sceHttpUriEscape(nullptr, &required, 0, input) == 0 && required == size); + char output[5] = {'Z', 'Z', 'Z', 'Z', 'Z'}; + Require(sceHttpUriEscape(output, &required, size - 1, input) == static_cast(0x80431022)); + for (char byte : output) Require(byte == 'Z'); + Require(sceHttpUriEscape(output, &required, size, input) == 0 && required == size); + Require(Equal(output, expected) && output[size] == 'Z'); + } +} + static int AuthInfo(int, int, const char*, char*, char*, int, std::uint8_t**, std::uint64_t*, int*, void*) { std::abort(); } +static void APS5_VABI RequestStatusCallback(int, int, void* argument) { + ++*static_cast(argument); +} int main() { + int callbackCount = 0; + const auto request = sceHttpCreateRequest2(1, "GET", "/", 0); + Require(sceHttpSetRequestStatusCallback(request, RequestStatusCallback, &callbackCount) == 0); + Require(sceHttpSendRequest(request, nullptr, 0) == static_cast(0x80431063u)); + Require(callbackCount == 0); + Require(sceHttpSetRequestStatusCallback(request, nullptr, nullptr) == 0); + Require(sceHttpSetRequestStatusCallback(-1, RequestStatusCallback, &callbackCount) == static_cast(0x80431100u)); constexpr int outOfMemory = static_cast(0x80431022); constexpr int invalidValue = static_cast(0x804311FE); constexpr int invalidUrl = static_cast(0x80433060); @@ -65,8 +131,19 @@ int main() { Require(sceHttpUriBuild(built, nullptr, required - 1, &element, buildAll) == outOfMemory); Require(sceHttpUriBuild(built, nullptr, sizeof(built), &element, buildAll) == 0); Require(Equal(built, "https://user:secret@example.com:8443/a/c?x=1&y=2#top")); + Require(sceHttpUriBuild(nullptr, &required, 0, &element, 0x01) == 0); + Require(required == sizeof("https://")); + std::memset(built, 'Z', sizeof(built)); + Require(sceHttpUriBuild(built, nullptr, required - 1, &element, 0x01) == outOfMemory); + for (char c : built) Require(c == 'Z'); + Require(sceHttpUriBuild(built, nullptr, required, &element, 0x01) == 0); + Require(Equal(built, "https://") && built[required] == 'Z'); + Require(sceHttpUriBuild(built, nullptr, sizeof(built), &element, 0) == 0); + Require(Equal(built, "//")); + Require(sceHttpUriBuild(built, nullptr, sizeof(built), &element, 0x02) == 0); + Require(Equal(built, "//example.com")); Require(sceHttpUriBuild(built, nullptr, sizeof(built), &element, 0x08 | 0x40) == 0); - Require(Equal(built, "/a/c?x=1&y=2")); + Require(Equal(built, "///a/c?x=1&y=2")); Require(sceHttpUriParse(&element, "HTTP://Example.com/", pool, nullptr, sizeof(pool)) == 0); Require(element.port == 80 && Equal(element.username, "") && Equal(element.query, "")); @@ -83,6 +160,10 @@ int main() { Require(Equal(element.username, "someone") && Equal(element.hostname, "example.com")); Require(sceHttpUriBuild(built, nullptr, sizeof(built), &element, buildAll) == 0); Require(Equal(built, "mailto:someone@example.com")); + Require(sceHttpUriBuild(built, nullptr, sizeof(built), &element, 0x01) == 0); + Require(Equal(built, "mailto:")); + Require(sceHttpUriBuild(built, nullptr, sizeof(built), &element, 0) == 0); + Require(Equal(built, "")); Require(sceHttpUriParse(&element, "/path/only?q", pool, nullptr, sizeof(pool)) == 0); Require(Equal(element.scheme, "") && Equal(element.hostname, "") && Equal(element.path, "/path/only")); @@ -103,27 +184,49 @@ 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 = 77; + Require(sceHttpUriUnescape(escaped, &required, size - 1, row[0]) == outOfMemory && required == 77); + Require(std::string_view(escaped, size - 1) == row[1] && 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")); @@ -186,16 +289,89 @@ int main() { Require(sceHttpUriMerge(merged, base, "http://bad host/", &required, sizeof(merged), 0) == invalidUrl); Require(required == 123); + 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; - Require(sceHttpCreateEpoll(1, nullptr) == invalidValue); - Require(sceHttpCreateEpoll(1, &epoll) == 0 && epoll != nullptr); HttpNBEvent events[2]{}; + Require(sceHttpCreateEpoll(1, &epoll) == beforeInit); + Require(sceHttpAbortWaitRequest(nullptr) == beforeInit); + 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); Require(sceHttpWaitRequest(epoll, events, 2, 0) == 0); Require(sceHttpWaitRequest(epoll, events, 2, 1000) == 0); Require(sceHttpWaitRequest(epoll, nullptr, 2, 0) == invalidValue); Require(sceHttpWaitRequest(epoll, events, 0, 0) == invalidValue); Require(sceHttpWaitRequest(nullptr, events, 2, 0) == invalidValue); - Require(sceHttpDestroyEpoll(1, epoll) == 0); + const auto unknownEpoll = reinterpret_cast(&events); + Require(sceHttpAbortWaitRequest(nullptr) == invalidValue); + Require(sceHttpAbortWaitRequest(unknownEpoll) == invalidId); + Require(sceHttpWaitRequest(unknownEpoll, events, 2, -1) == invalidId); + Require(sceHttpDestroyEpoll(context, nullptr) == invalidValue); + Require(sceHttpDestroyEpoll(context, unknownEpoll) == invalidId); + Require(sceHttpDestroyEpoll(-1, epoll) == invalidId); + + auto waitWokenBy = [&](auto wake) { + std::atomic started{false}; + std::atomic result{1}; + std::thread waiter([&] { + started = true; + result = sceHttpWaitRequest(epoll, events, 2, -1); + }); + while (!started) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + Require(result == 1); + wake(); + waiter.join(); + return result.load(); + }; + Require(waitWokenBy([&] { Require(sceHttpAbortWaitRequest(epoll) == 0); }) == aborted); + Require(sceHttpWaitRequest(epoll, events, 2, 0) == aborted); + Require(sceHttpWaitRequest(epoll, events, 2, -1) == aborted); + Require(sceHttpDestroyEpoll(context, epoll) == 0); + Require(sceHttpAbortWaitRequest(epoll) == invalidId); + Require(sceHttpDestroyEpoll(context, epoll) == invalidId); + + Require(sceHttpCreateEpoll(context, &epoll) == 0); + Require(waitWokenBy([&] { Require(sceHttpDestroyEpoll(context, epoll) == 0); }) == 0); + + 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); @@ -209,6 +385,7 @@ int main() { Require(sceHttpsLoadCert(1, 0, nullptr, nullptr, nullptr) == 0); Require(sceHttpsUnloadCert(1) == 0); Require(sceHttpSetResponseHeaderMaxSize(1, 8192) == 0); + Require(sceHttpSetRecvBlockSize(1, 0x4000) == 0); Require(sceHttpRedirectCacheFlush(1) == 0); int authUserArg = 0; Require(sceHttpSetAuthInfoCallback(1, AuthInfo, &authUserArg) == 0); @@ -296,7 +473,33 @@ 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); + } } diff --git a/core/libs/tests/GuestHttp2.cpp b/core/libs/tests/GuestHttp2.cpp index dcc8347c32..d29c0021e4 100644 --- a/core/libs/tests/GuestHttp2.cpp +++ b/core/libs/tests/GuestHttp2.cpp @@ -8,6 +8,7 @@ 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 sceHttp2DeleteCookieBox(int); int APS5_VABI sceHttp2SetCookieBox(int, int); int APS5_VABI sceHttp2CookieFlush(int); int APS5_VABI sceHttp2SetRequestNoContentLength(int); @@ -15,6 +16,10 @@ int APS5_VABI sceHttp2SendRequest(int, const void*, std::size_t); int APS5_VABI sceHttp2SendRequestAsync(int, const void*, std::size_t, Http2AsyncOption*, void*); int APS5_VABI sceHttp2WaitAsync(int, Http2AsyncResult*, std::uint32_t*, void*); int APS5_VABI sceHttp2DeleteRequest(int); +int APS5_VABI sceHttp2DeleteTemplate(int); +int APS5_VABI sceHttp2SetMinSslVersion(int, std::uint32_t); +int APS5_VABI sceHttp2SslEnableOption(int, std::uint32_t); +int APS5_VABI sceHttp2SslDisableOption(int, std::uint32_t); int APS5_VABI sceHttp2Term(int); int APS5_VABI sceKernelCreateEqueue(KernelEqueue* eq, const char* name); int APS5_VABI sceKernelDeleteEqueue(KernelEqueue eq); @@ -35,10 +40,19 @@ struct Http2MemoryPoolStats { extern "C" { int APS5_VABI sceHttp2GetMemoryPoolStats(int, Http2MemoryPoolStats*); int APS5_VABI sceHttp2SetResolveRetry(int, std::int32_t); +int APS5_VABI sceHttp2WebSocketSetPingInterval(); } static void Require(bool value) { if (!value) std::abort(); } +static constexpr int InvalidId = static_cast(0x817B1100); + +static void RequireSslSetters(int id, int expected) { + Require(sceHttp2SetMinSslVersion(id, 0) == expected); + Require(sceHttp2SslEnableOption(id, 0) == expected); + Require(sceHttp2SslDisableOption(id, 0) == expected); +} + template static bool Throws(TCall call) { try { @@ -58,6 +72,11 @@ int main() { const int box = sceHttp2CreateCookieBox(context); const int other = sceHttp2CreateCookieBox(context); Require(box > 0 && other > 0 && box != other && box != context); + const int discarded = sceHttp2CreateCookieBox(context); + Require(discarded > 0 && discarded != box && discarded != other); + Require(sceHttp2DeleteCookieBox(discarded) == 0); + Require(sceHttp2DeleteCookieBox(discarded) == InvalidId); + Require(sceHttp2DeleteCookieBox(context) == InvalidId); const int tmpl = sceHttp2CreateTemplate(context, "agent", 2, 0); Require(tmpl > 0 && tmpl != box && tmpl != other); Require(sceHttp2SetResolveRetry(tmpl, 3) == 0); @@ -67,9 +86,55 @@ int main() { Require(request > 0); Require(sceHttp2SetCookieBox(request, other) == 0); Require(sceHttp2SetRequestNoContentLength(request) == 0); + Require(Throws([] { sceHttp2WebSocketSetPingInterval(); })); Require(sceHttp2CookieFlush(context) == 0); Require(sceHttp2SendRequest(request, nullptr, 0) == static_cast(0x80436063)); + RequireSslSetters(0, InvalidId); + RequireSslSetters(-1, InvalidId); + RequireSslSetters(0x7fffffff, InvalidId); + RequireSslSetters(context, 0); + RequireSslSetters(box, 0); + RequireSslSetters(other, 0); + RequireSslSetters(tmpl, 0); + RequireSslSetters(request, 0); + + const int doomedRequest = sceHttp2CreateRequestWithURL(tmpl, "GET", "https://example.com/doomed", 0); + Require(doomedRequest > 0); + Require(sceHttp2DeleteRequest(doomedRequest) == 0); + RequireSslSetters(doomedRequest, InvalidId); + Require(sceHttp2DeleteRequest(doomedRequest) == InvalidId); + + const int doomedTemplate = sceHttp2CreateTemplate(context, "agent", 2, 0); + Require(doomedTemplate > 0); + const int doomedTemplateRequest = sceHttp2CreateRequestWithURL(doomedTemplate, "GET", "https://example.com/child", 0); + Require(doomedTemplateRequest > 0); + Require(sceHttp2DeleteTemplate(doomedTemplate) == 0); + RequireSslSetters(doomedTemplate, InvalidId); + RequireSslSetters(doomedTemplateRequest, InvalidId); + Require(sceHttp2DeleteTemplate(doomedTemplate) == InvalidId); + + const int doomedContext = sceHttp2Init(1, 1, 0x2000, 4); + Require(doomedContext > 0); + const int doomedContextBox = sceHttp2CreateCookieBox(doomedContext); + const int doomedContextTemplate = sceHttp2CreateTemplate(doomedContext, "agent", 2, 0); + Require(doomedContextBox > 0 && doomedContextTemplate > 0); + const int doomedContextRequest = sceHttp2CreateRequestWithURL(doomedContextTemplate, "GET", "https://example.com/descendant", 0); + Require(doomedContextRequest > 0); + RequireSslSetters(doomedContext, 0); + Require(sceHttp2Term(doomedContext) == 0); + RequireSslSetters(doomedContext, InvalidId); + RequireSslSetters(doomedContextBox, InvalidId); + Require(sceHttp2DeleteCookieBox(doomedContextBox) == InvalidId); + RequireSslSetters(doomedContextTemplate, InvalidId); + RequireSslSetters(doomedContextRequest, InvalidId); + Require(sceHttp2Term(doomedContext) == InvalidId); + + const int freshContext = sceHttp2Init(1, 1, 0x2000, 4); + Require(freshContext > 0); + RequireSslSetters(freshContext, 0); + Require(sceHttp2Term(freshContext) == 0); + KernelEqueue eq = 0; Require(sceKernelCreateEqueue(&eq, "http2") == 0); Require(sceKernelAddUserEventEdge(eq, request) == 0); @@ -89,11 +154,93 @@ int main() { Require(sceHttp2WaitAsync(request, &result, nullptr, nullptr) == 0); Require(result.req_id == request && result.result == static_cast(0x80436063)); Require(Throws([&] { sceHttp2WaitAsync(request, &result, nullptr, nullptr); })); - Require(Throws([&] { sceHttp2SendRequestAsync(request, nullptr, 0, nullptr, nullptr); })); + Require(sceHttp2SendRequestAsync(request, nullptr, 0, nullptr, nullptr) == 0); + std::uint32_t wait_timeout = 2000000; + Require(sceHttp2WaitAsync(request, &result, &wait_timeout, nullptr) == 0); + Require(result.req_id == request && result.result == static_cast(0x80436063)); + Require(Throws([&] { sceHttp2WaitAsync(request, &result, nullptr, nullptr); })); + const int invalidRequests[] = {0, -1, 0x7fffffff, context, tmpl, box, doomedRequest, doomedTemplateRequest, doomedContextRequest}; + for (const int invalid : invalidRequests) { + Require(sceHttp2SendRequestAsync(invalid, nullptr, 0, nullptr, nullptr) == InvalidId); + Require(Throws([&] { sceHttp2WaitAsync(invalid, &result, nullptr, nullptr); })); + } + const int independent = sceHttp2CreateRequestWithURL(tmpl, "GET", "https://example.com/", 0); + Require(independent > 0 && independent != request); + Require(sceHttp2SendRequestAsync(request, nullptr, 0, nullptr, nullptr) == 0); + Require(sceHttp2SendRequestAsync(independent, nullptr, 0, nullptr, nullptr) == 0); + Require(sceHttp2SendRequestAsync(request, nullptr, 0, nullptr, nullptr) == 0); + Require(Throws([&] { sceHttp2SendRequestAsync(request, nullptr, 0, nullptr, &tag); })); + Require(sceHttp2WaitAsync(independent, &result, nullptr, nullptr) == 0); + Require(result.req_id == independent && result.result == static_cast(0x80436063)); + Require(Throws([&] { sceHttp2WaitAsync(independent, &result, nullptr, nullptr); })); + for (int i = 0; i < 2; ++i) { + Require(sceHttp2WaitAsync(request, &result, nullptr, nullptr) == 0); + Require(result.req_id == request && result.result == static_cast(0x80436063)); + } + Require(Throws([&] { sceHttp2WaitAsync(request, &result, nullptr, nullptr); })); + Require(sceHttp2SendRequestAsync(independent, nullptr, 0, nullptr, nullptr) == 0); + Require(sceHttp2DeleteRequest(independent) == 0); + Require(sceHttp2SendRequestAsync(independent, nullptr, 0, nullptr, nullptr) == InvalidId); + Require(Throws([&] { sceHttp2WaitAsync(independent, &result, nullptr, nullptr); })); Require(sceKernelDeleteUserEvent(eq, request) == 0); Require(Throws([&] { sceHttp2SendRequestAsync(request, nullptr, 0, &option, nullptr); })); Require(Throws([&] { sceHttp2WaitAsync(request, &result, nullptr, nullptr); })); Require(sceHttp2DeleteRequest(request) == 0); Require(sceKernelDeleteEqueue(eq) == 0); + Require(sceHttp2DeleteCookieBox(box) == 0 && sceHttp2DeleteCookieBox(other) == 0); Require(sceHttp2Term(context) == 0); + + KernelEqueue templateEqueue = 0; + Require(sceKernelCreateEqueue(&templateEqueue, "http2-template") == 0); + const int cascadeContext = sceHttp2Init(1, 1, 0x3000, 4); + Require(cascadeContext > 0); + const int cascadeTemplate = sceHttp2CreateTemplate(cascadeContext, "agent", 2, 0); + Require(cascadeTemplate > 0); + const int cascadeRequest = sceHttp2CreateRequestWithURL(cascadeTemplate, "GET", "https://example.com/cascade", 0); + Require(cascadeRequest > 0); + Require(sceKernelAddUserEventEdge(templateEqueue, cascadeRequest) == 0); + Http2AsyncOption cascadeOption{}; + cascadeOption.equeue = templateEqueue; + cascadeOption.user_event_id = cascadeRequest; + cascadeOption.user_data = &tag; + Require(sceHttp2SendRequestAsync(cascadeRequest, nullptr, 0, &cascadeOption, nullptr) == 0); + KernelEvent cascadeEvent{}; + int cascadeCount = 0; + Require(sceKernelWaitEqueue(templateEqueue, &cascadeEvent, 1, &cascadeCount, &timeout) == 0 && cascadeCount == 1); + Require(sceHttp2DeleteTemplate(cascadeTemplate) == 0); + Require(Throws([&] { sceHttp2WaitAsync(cascadeRequest, &result, nullptr, nullptr); })); + Require(sceHttp2SendRequestAsync(cascadeRequest, nullptr, 0, &cascadeOption, nullptr) == InvalidId); + Require(Throws([&] { sceHttp2WaitAsync(cascadeRequest, &result, nullptr, nullptr); })); + Require(sceHttp2DeleteRequest(cascadeRequest) == InvalidId); + RequireSslSetters(cascadeRequest, InvalidId); + Require(sceHttp2Term(cascadeContext) == 0); + Require(sceKernelDeleteUserEvent(templateEqueue, cascadeRequest) == 0); + Require(sceKernelDeleteEqueue(templateEqueue) == 0); + + KernelEqueue termEqueue = 0; + Require(sceKernelCreateEqueue(&termEqueue, "http2-term") == 0); + const int terminatedContext = sceHttp2Init(1, 1, 0x4000, 4); + Require(terminatedContext > 0); + const int terminatedTemplate = sceHttp2CreateTemplate(terminatedContext, "agent", 2, 0); + Require(terminatedTemplate > 0); + const int terminatedRequest = sceHttp2CreateRequestWithURL(terminatedTemplate, "GET", "https://example.com/terminated", 0); + Require(terminatedRequest > 0); + Require(sceKernelAddUserEventEdge(termEqueue, terminatedRequest) == 0); + Http2AsyncOption termOption{}; + termOption.equeue = termEqueue; + termOption.user_event_id = terminatedRequest; + termOption.user_data = &tag; + Require(sceHttp2SendRequestAsync(terminatedRequest, nullptr, 0, &termOption, nullptr) == 0); + KernelEvent termEvent{}; + int termCount = 0; + Require(sceKernelWaitEqueue(termEqueue, &termEvent, 1, &termCount, &timeout) == 0 && termCount == 1); + Require(sceHttp2Term(terminatedContext) == 0); + Require(sceHttp2SendRequestAsync(terminatedRequest, nullptr, 0, &termOption, nullptr) == InvalidId); + Require(Throws([&] { sceHttp2WaitAsync(terminatedRequest, &result, nullptr, nullptr); })); + Require(sceHttp2DeleteRequest(terminatedRequest) == InvalidId); + Require(sceHttp2DeleteTemplate(terminatedTemplate) == InvalidId); + RequireSslSetters(terminatedContext, InvalidId); + RequireSslSetters(terminatedRequest, InvalidId); + Require(sceKernelDeleteUserEvent(termEqueue, terminatedRequest) == 0); + Require(sceKernelDeleteEqueue(termEqueue) == 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..574398c192 --- /dev/null +++ b/core/libs/tests/GuestImeDialog.cpp @@ -0,0 +1,189 @@ +#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); +} + +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 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()); +} diff --git a/core/libs/tests/GuestImeFilterText.cpp b/core/libs/tests/GuestImeFilterText.cpp new file mode 100644 index 0000000000..4bc3daf1b9 --- /dev/null +++ b/core/libs/tests/GuestImeFilterText.cpp @@ -0,0 +1,106 @@ +#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, 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"); +} + +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 new file mode 100644 index 0000000000..ce516d5530 --- /dev/null +++ b/core/libs/tests/GuestImeOpen.cpp @@ -0,0 +1,577 @@ +#include "SceTypes.hpp" +#include "prx/libSceIme/include/ImeInput.hpp" +#include "prx/libSceVideoOut/include/ImeTextInput.hpp" + +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceImeOpen_nid_postfix(const Param* param, const ExtendedParam* extended); +int APS5_VABI sceImeClose_nid_postfix(void); +int APS5_VABI sceImeUpdate(EventHandler handler); +int APS5_VABI sceImeSetText(const char16_t* text, uint32_t length); +int APS5_VABI sceImeSetCaret(const Caret* caret); +int APS5_VABI sceImeGetPanelPositionAndForm(PositionAndForm* form); +void APS5_VABI sceImeParamInit(Param* param); +} + +namespace { + +constexpr int Busy = static_cast(0x80bc0001u); +constexpr int NotOpened = static_cast(0x80bc0002u); +constexpr int InvalidText = static_cast(0x80bc0006u); +constexpr int InvalidUserId = static_cast(0x80bc0010u); +constexpr int InvalidType = static_cast(0x80bc0011u); +constexpr int InvalidEnterLabel = static_cast(0x80bc0013u); +constexpr int InvalidInputMethod = static_cast(0x80bc0014u); +constexpr int InvalidOption = static_cast(0x80bc0015u); +constexpr int InvalidMaxTextLength = static_cast(0x80bc0016u); +constexpr int InvalidInputTextBuffer = static_cast(0x80bc0017u); +constexpr int InvalidPosX = static_cast(0x80bc0018u); +constexpr int InvalidPosY = static_cast(0x80bc0019u); +constexpr int InvalidHorizontalAlignment = static_cast(0x80bc001au); +constexpr int InvalidVerticalAlignment = static_cast(0x80bc001bu); +constexpr int InvalidExtended = static_cast(0x80bc001cu); +constexpr int InvalidWork = static_cast(0x80bc0020u); +constexpr int InvalidHandler = static_cast(0x80bc0022u); +constexpr int InvalidParam = static_cast(0x80bc0030u); +constexpr int InvalidAddress = static_cast(0x80bc0031u); +constexpr int InvalidReserved = static_cast(0x80bc0032u); + +constexpr uint32_t EventOpen = 0; +constexpr uint32_t EventUpdateText = 1; +constexpr uint32_t EventUpdateCaret = 2; +constexpr uint32_t EventPressClose = 4; +constexpr uint32_t EventPressEnter = 5; + +struct Recorded { + uint32_t id; + void* arg; + ImeEventParam param; + std::u16string text; +}; + +std::vector g_events; +bool g_closeOnPressClose = false; +int g_reentrantClose = 1; + +void APS5_VABI Record(void* arg, const ImeEvent* event) { + Recorded recorded{event->id, arg, event->param, {}}; + if (event->id == EventUpdateText || event->id == EventPressEnter || event->id == EventPressClose) { + recorded.text = event->param.text.str; + } + g_events.push_back(recorded); + if (g_closeOnPressClose && event->id == EventPressClose) g_reentrantClose = sceImeClose_nid_postfix(); +} + +void APS5_VABI Other(void*, const ImeEvent*) { + std::abort(); +} + +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); + std::abort(); +} + +struct Buffers { + char16_t text[2049] = {}; + alignas(16) unsigned char work[0x5000] = {}; + int arg = 0; +}; + +Param MakeParam(Buffers& buffers, uint32_t maxLength) { + Param param; + sceImeParamInit(¶m); + param.user_id = 1; + param.max_text_length = maxLength; + param.input_text_buffer = buffers.text; + param.posx = 100.0f; + param.posy = 200.0f; + param.work = buffers.work; + param.arg = &buffers.arg; + param.handler = Record; + return param; +} + +std::vector Drain() { + g_events.clear(); + Require(sceImeUpdate(Record) == 0, "update failed"); + return g_events; +} + +void Publish(ImeHostInputKind kind, const char* text = "") { + ImeHostInput input{}; + input.kind = kind; + std::strncpy(input.text, text, sizeof(input.text) - 1); + ImePublishInput_nid_postfix(input); +} + +void RequireText(const Recorded& event, uint32_t id, const std::u16string& text, uint32_t caret, uint32_t index, int32_t delta) { + Require(event.id == id, "event id"); + Require(event.text == text, "event text"); + Require(event.param.text.caret_index == caret, "event caret"); + Require(event.param.text.area_num == 1, "event area count"); + Require(event.param.text.text_area[0].mode == TextAreaMode::Edit, "event area mode"); + Require(event.param.text.text_area[0].index == index, "event edit index"); + Require(event.param.text.text_area[0].length == delta, "event edit length"); +} + +void RequireCaretSteps(const std::vector& events, uint32_t direction, std::size_t count) { + Require(events.size() == count, "caret step count"); + for (const auto& event : events) { + Require(event.id == EventUpdateCaret && event.param.caret_move == direction, "caret step"); + } +} + +void CheckValidation() { + Buffers buffers; + Require(sceImeOpen_nid_postfix(nullptr, nullptr) == InvalidAddress, "null param"); + auto expect = [&](auto change, int error, const char* message) { + Param param = MakeParam(buffers, 16); + change(param); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == error, message); + }; + expect([](Param& p) { p.user_id = -1; }, InvalidUserId, "invalid user"); + expect([](Param& p) { p.type = 5; }, InvalidType, "invalid type"); + expect([](Param& p) { p.enter_label = 4; }, InvalidEnterLabel, "invalid enter label"); + expect([](Param& p) { p.input_method = 1; }, InvalidInputMethod, "invalid input method"); + expect([](Param& p) { p.option = 0x80000000u; }, InvalidOption, "invalid option"); + expect([](Param& p) { p.max_text_length = 0; }, InvalidMaxTextLength, "zero length"); + expect([](Param& p) { p.max_text_length = 2049; }, InvalidMaxTextLength, "long length"); + expect([](Param& p) { p.input_text_buffer = nullptr; }, InvalidInputTextBuffer, "null buffer"); + expect([](Param& p) { p.posx = -1.0f; }, InvalidPosX, "negative x"); + expect([](Param& p) { p.posx = 1920.0f; }, InvalidPosX, "x past 1080p"); + expect([](Param& p) { p.posy = 1080.0f; }, InvalidPosY, "y past 1080p"); + expect([](Param& p) { p.horizontal_alignment = 3; }, InvalidHorizontalAlignment, "horizontal alignment"); + expect([](Param& p) { p.vertical_alignment = 3; }, InvalidVerticalAlignment, "vertical alignment"); + expect([](Param& p) { p.work = nullptr; }, InvalidWork, "null work"); + expect([&](Param& p) { p.work = buffers.work + 2; }, InvalidWork, "misaligned work"); + expect([](Param& p) { p.handler = nullptr; }, InvalidHandler, "null handler"); + expect([](Param& p) { p.reserved[7] = 1; }, InvalidReserved, "reserved bytes"); + buffers.text[0] = 0xdc00; + expect([](Param&) {}, InvalidText, "lone low surrogate"); + buffers.text[0] = u'\n'; + expect([](Param&) {}, InvalidText, "newline without multiline"); + buffers.text[0] = 0; + + Param param = MakeParam(buffers, 16); + ExtendedParam extended{}; + extended.priority = 4; + Require(sceImeOpen_nid_postfix(¶m, &extended) == InvalidExtended, "extended priority"); + extended.priority = 0; + extended.reserved[59] = 1; + Require(sceImeOpen_nid_postfix(¶m, &extended) == InvalidExtended, "extended reserved"); + + param.posx = 3000.0f; + param.option = 0x4000; + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "over-2K coordinates"); + Require(sceImeClose_nid_postfix() == 0, "close after over-2K open"); + + param = MakeParam(buffers, 16); + param.filter = reinterpret_cast(&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() { + Buffers buffers; + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + Param param = MakeParam(buffers, 8); + Require(!ImeCapturesKeyboard_nid_postfix(), "keyboard captured before open"); + Require(sceImeUpdate(Record) == 0 && g_events.empty(), "update before open"); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open"); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == Busy, "second open"); + Require(ImeCapturesKeyboard_nid_postfix(), "keyboard not captured"); + Require(sceImeUpdate(Other) == 0, "update with another handler"); + + auto events = Drain(); + Require(events.size() == 1 && events[0].id == EventOpen, "open event"); + Require(events[0].arg == &buffers.arg, "handler argument"); + Require(events[0].param.rect.x == 100.0f && events[0].param.rect.y == 200.0f, "open position"); + Require(events[0].param.rect.width == 793 && events[0].param.rect.height == 408, "open size"); + Require(Drain().empty(), "events delivered twice"); + + PositionAndForm form{}; + Require(sceImeGetPanelPositionAndForm(nullptr) == InvalidAddress, "null panel form"); + Require(sceImeGetPanelPositionAndForm(&form) == 0, "panel form"); + Require(form.posx == 100.0f && form.posy == 200.0f && form.width == 793 && form.height == 408, "panel form values"); + + Publish(ImeHostInputKind::Text, "c"); + events = Drain(); + Require(events.size() == 1, "one text event"); + RequireText(events[0], EventUpdateText, u"abc", 3, 2, 1); + Require(std::u16string(buffers.text) == u"abc", "input buffer"); + Require(std::u16string(reinterpret_cast(buffers.work)) == u"abc", "work copy"); + + Publish(ImeHostInputKind::Text, "\xc3\xa9\xf0\x9f\x98\x80"); + events = Drain(); + RequireText(events.at(0), EventUpdateText, u"abcé\U0001F600", 6, 3, 3); + + Publish(ImeHostInputKind::Left); + RequireCaretSteps(Drain(), 1, 2); + Publish(ImeHostInputKind::Backspace); + events = Drain(); + RequireText(events.at(0), EventUpdateText, u"abc\U0001F600", 3, 3, -1); + Publish(ImeHostInputKind::Delete); + events = Drain(); + RequireText(events.at(0), EventUpdateText, u"abc", 3, 3, -2); + Publish(ImeHostInputKind::Delete); + Require(Drain().empty(), "delete at the end"); + Publish(ImeHostInputKind::Home); + RequireCaretSteps(Drain(), 1, 3); + Publish(ImeHostInputKind::Left); + Require(Drain().empty(), "left at the start"); + Publish(ImeHostInputKind::Backspace); + Require(Drain().empty(), "backspace at the start"); + Publish(ImeHostInputKind::Right); + RequireCaretSteps(Drain(), 2, 1); + Publish(ImeHostInputKind::End); + RequireCaretSteps(Drain(), 2, 2); + + Publish(ImeHostInputKind::Text, "x\ny"); + events = Drain(); + RequireText(events.at(0), EventUpdateText, u"abcxy", 5, 3, 2); + Publish(ImeHostInputKind::Text, "12\xf0\x9f\x98\x80"); + events = Drain(); + RequireText(events.at(0), EventUpdateText, u"abcxy12", 7, 5, 2); + Publish(ImeHostInputKind::Text, "\xf0\x9f\x98\x80"); + Require(Drain().empty(), "a pair past the limit must not be split"); + Publish(ImeHostInputKind::Text, "z"); + events = Drain(); + RequireText(events.at(0), EventUpdateText, u"abcxy12z", 8, 7, 1); + Publish(ImeHostInputKind::Text, "q"); + Require(Drain().empty(), "text past the limit"); + + Require(sceImeSetText(nullptr, 1) == InvalidAddress, "null text"); + Require(sceImeSetText(u"a\nb", 3) == InvalidText, "newline in single-line text"); + Require(sceImeSetText(u"hello", 5) == 0, "set text"); + Require(std::u16string(buffers.text) == u"hello", "set text buffer"); + Require(Drain().empty(), "set text queued an event"); + Caret caret{}; + caret.index = 6; + Require(sceImeSetCaret(nullptr) == InvalidAddress, "null caret"); + Require(sceImeSetCaret(&caret) == InvalidParam, "caret past the text"); + caret.index = 1; + Require(sceImeSetCaret(&caret) == 0, "set caret"); + Publish(ImeHostInputKind::Text, "X"); + events = Drain(); + RequireText(events.at(0), EventUpdateText, u"hXello", 2, 1, 1); + + Publish(ImeHostInputKind::Enter); + events = Drain(); + RequireText(events.at(0), EventPressEnter, u"hXello", 2, 2, 0); + g_closeOnPressClose = true; + Publish(ImeHostInputKind::Escape); + Publish(ImeHostInputKind::Enter); + events = Drain(); + g_closeOnPressClose = false; + Require(events.size() == 1, "events after a close inside the handler"); + RequireText(events[0], EventPressClose, u"hXello", 2, 2, 0); + Require(g_reentrantClose == 0, "close inside the handler"); + Require(!ImeCapturesKeyboard_nid_postfix(), "keyboard captured after close"); + Require(sceImeClose_nid_postfix() == NotOpened, "second close"); + Publish(ImeHostInputKind::Text, "ignored"); + Require(std::u16string(buffers.text) == u"hXello", "input after close"); + Require(sceImeSetText(u"a", 1) == NotOpened, "set text after close"); +} + +void CheckMultiline() { + Buffers buffers; + Param param = MakeParam(buffers, 16); + param.option = 1; + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "multiline open"); + Drain(); + Publish(ImeHostInputKind::Text, "a"); + Publish(ImeHostInputKind::Enter); + Publish(ImeHostInputKind::Text, "b\nc"); + const auto events = Drain(); + Require(events.size() == 3, "multiline events"); + RequireText(events[1], EventUpdateText, u"a\nb\nc", 2, 1, 1); + RequireText(events[2], EventUpdateText, u"a\nb\nc", 5, 2, 3); + Require(sceImeSetText(u"x\ny", 3) == 0, "multiline set text"); + Require(sceImeClose_nid_postfix() == 0, "multiline close"); +} + +void 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); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "host events open"); + Drain(); + ImeTextInput input; + SDL_Event event{}; + event.type = SDL_TEXTINPUT; + event.text.windowID = 7; + std::strcpy(event.text.text, "hi"); + input.HandleEvent(event, 7); + event.text.windowID = 8; + input.HandleEvent(event, 7); + event = {}; + event.type = SDL_KEYDOWN; + event.key.windowID = 7; + event.key.keysym.scancode = SDL_SCANCODE_BACKSPACE; + event.key.repeat = 1; + input.HandleEvent(event, 7); + event.key.keysym.scancode = SDL_SCANCODE_RETURN; + input.HandleEvent(event, 7); + event.key.repeat = 0; + input.HandleEvent(event, 7); + event.key.keysym.scancode = SDL_SCANCODE_A; + input.HandleEvent(event, 7); + event.type = SDL_KEYUP; + event.key.keysym.scancode = SDL_SCANCODE_ESCAPE; + input.HandleEvent(event, 7); + const auto events = Drain(); + Require(events.size() == 3, "host event count"); + RequireText(events[0], EventUpdateText, u"h", 2, 0, 2); + RequireText(events[1], EventUpdateText, u"h", 1, 1, -1); + Require(events[2].id == EventPressEnter, "host enter"); + Require(sceImeClose_nid_postfix() == 0, "host events close"); +} + +} + +int main() { + CheckValidation(); + CheckEditing(); + CheckMultiline(); + 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 new file mode 100644 index 0000000000..5b69f12016 --- /dev/null +++ b/core/libs/tests/GuestInterfaces.cpp @@ -0,0 +1,308 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#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 +#include +#include + +struct GuestInterfaceAddress { + GuestInterfaceAddress* next; + char* name; + std::uint32_t flags; + void* address; + void* netmask; + void* destination; + void* data; +}; +static_assert(sizeof(GuestInterfaceAddress) == 56); +static_assert(offsetof(GuestInterfaceAddress, flags) == 16); +static_assert(offsetof(GuestInterfaceAddress, address) == 24); +static_assert(offsetof(GuestInterfaceAddress, netmask) == 32); +static_assert(offsetof(GuestInterfaceAddress, destination) == 40); +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(); +} + +static void Require(bool value) { if (!value) std::abort(); } + +struct HostInterfaceAddress { + int family; + 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) { + HostInterfaceAddress entry{}; + entry.family = address->sa_family; + if (entry.family == AF_INET) { + std::memcpy(entry.address, &reinterpret_cast(address)->sin_addr, 4); + } else { + const auto* v6 = reinterpret_cast(address); + std::memcpy(entry.address, &v6->sin6_addr, 16); + entry.scope = v6->sin6_scope_id; + } + 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 + const ULONG options = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER; + ULONG size = 0; + std::vector storage; + ULONG result = GetAdaptersAddresses(AF_UNSPEC, options, nullptr, nullptr, &size); + for (unsigned calls = 1; result == ERROR_BUFFER_OVERFLOW && calls < 4; ++calls) { + storage.resize(size); + result = GetAdaptersAddresses(AF_UNSPEC, options, nullptr, reinterpret_cast(storage.data()), &size); + } + if (result == ERROR_NO_DATA) return entries; + Require(result == NO_ERROR); + for (auto* adapter = reinterpret_cast(storage.data()); adapter; adapter = adapter->Next) + for (auto* unicast = adapter->FirstUnicastAddress; unicast; unicast = unicast->Next) { + const sockaddr* address = unicast->Address.lpSockaddr; + if (!address || (address->sa_family != AF_INET && address->sa_family != AF_INET6)) continue; + HostInterfaceAddress entry = HostAddress(address); + const unsigned bits = entry.family == AF_INET ? 32 : 128; + unsigned prefix = unicast->OnLinkPrefixLength; + if (prefix > bits) prefix = bits; + for (unsigned bit = 0; bit < prefix; ++bit) entry.netmask[bit / 8] |= static_cast(0x80 >> (bit % 8)); + entries.push_back(entry); + } +#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) 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); + 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; + Require(getifaddrs_nid_postfix(&list) == 0); + std::size_t entries = 0; + bool equal = true; + bool loopback = false; + for (auto* item = list; item; item = item->next) { + Require(++entries < 100000); + Require(item->name != nullptr); + const std::size_t nameLength = std::strlen(item->name); + Require(nameLength >= 1 && nameLength <= 15); + Require((item->flags & ~knownFlags) == 0); + 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); + Require(reinterpret_cast(netmask) % 8 == 0); + Require((address[0] == 16 && address[1] == 2) || (address[0] == 28 && address[1] == 28)); + Require(netmask[0] == address[0] && netmask[1] == address[1]); + Require(address[2] == 0 && address[3] == 0 && netmask[2] == 0 && netmask[3] == 0); + if (address[1] == 2) { + for (std::size_t index = 8; index < 16; ++index) Require(address[index] == 0 && netmask[index] == 0); + } else { + for (std::size_t index = 4; index < 8; ++index) Require(address[index] == 0 && netmask[index] == 0); + } + if (item->destination) { + const auto* destination = static_cast(item->destination); + Require(reinterpret_cast(destination) % 8 == 0); + Require(destination[0] == address[0] && destination[1] == address[1]); + } + if (address[1] == 2 && (item->flags & 0x2)) Require(item->destination != nullptr); + 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; + equal = equal && expected.family == (ipv4 ? AF_INET : AF_INET6) && + std::memcmp(address + offset, expected.address, length) == 0 && + std::memcmp(netmask + offset, expected.netmask, length) == 0; + if (!ipv4) { + std::uint32_t scope; + std::memcpy(&scope, address + 24, 4); + equal = equal && scope == expected.scope; + } + } else { + equal = false; + } + if (address[1] == 2 && std::memcmp(address + 4, loopbackAddress, 4) == 0) + loopback = loopback || (std::memcmp(netmask + 4, loopbackMask, 4) == 0 && (item->flags & 0x9) == 0x9 && !(item->flags & 0x2)); + } + matched = equal && entries == host.size() && loopback; + 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); + Require(*__error_nid_postfix() == 14); +} diff --git a/core/libs/tests/GuestItimer.cpp b/core/libs/tests/GuestItimer.cpp new file mode 100644 index 0000000000..4e1e9d5845 --- /dev/null +++ b/core/libs/tests/GuestItimer.cpp @@ -0,0 +1,228 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct GuestItimerval { + KernelTimeval interval; + KernelTimeval value; +}; +static_assert(sizeof(GuestItimerval) == 32 && offsetof(GuestItimerval, value) == 16); +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 (*)(); +extern "C" { +int APS5_VABI setitimer_nid_postfix(int, const GuestItimerval*, GuestItimerval*); +int APS5_VABI getitimer_nid_postfix(int, GuestItimerval*); +unsigned int APS5_VABI GuestAlarm_nid_no_patch(unsigned int); +Handler APS5_VABI signal_nid_postfix(int, Handler); +int APS5_VABI _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(); +TerminateHandler APS5_VABI _ZSt13set_terminatePFvvE_nid_postfix(TerminateHandler); +} + +static void Require(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +static std::int64_t Micros(const KernelTimeval& time) { + Require(time.tv_sec >= 0 && time.tv_usec >= 0 && time.tv_usec < 1000000, "timeval is not normalized"); + return time.tv_sec * 1000000 + time.tv_usec; +} + +static GuestItimerval Read(int which) { + GuestItimerval current{{11, 22}, {33, 44}}; + *__error_nid_postfix() = 12345; + Require(setitimer_nid_postfix(which, nullptr, ¤t) == 0, "reading the timer failed"); + Require(*__error_nid_postfix() == 12345, "reading the timer changed errno"); + return current; +} + +static void RequireFailure(int which, const GuestItimerval* value, int error, const char* message) { + GuestItimerval previous{{11, 22}, {33, 44}}; + const auto saved = previous; + *__error_nid_postfix() = 0; + Require(setitimer_nid_postfix(which, value, &previous) == -1 && *__error_nid_postfix() == error, message); + Require(std::memcmp(&previous, &saved, sizeof(previous)) == 0, "a failed setitimer wrote the previous value"); +} + +volatile std::sig_atomic_t delivered = 0; +static void APS5_VABI OnAlarm(int signal) { + delivered = signal; +} + +static 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()); + if (type != nullptr && error != nullptr && std::strcmp(type->name(), typeid(std::runtime_error).name()) == 0 + && std::strstr(error->what(), "it_interval 0.250000 s") != nullptr && delivered == 0) { + std::printf("expiry threw: %s\n", error->what()); + std::fflush(stdout); + std::_Exit(0); + } + std::fprintf(stderr, "unexpected termination at expiry\n"); + std::_Exit(1); +} + +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}}; + for (int invalid : {-1, 3, INT_MIN, INT_MAX}) { + RequireFailure(invalid, &valid, 22, "an unknown timer did not report EINVAL"); + RequireFailure(invalid, nullptr, 22, "an unknown timer read did not report EINVAL"); + *__error_nid_postfix() = 0; + Require(setitimer_nid_postfix(invalid, nullptr, nullptr) == -1 && *__error_nid_postfix() == 22, "EINVAL is not checked before EFAULT"); + } + for (int which : {0, 1, 2}) { + *__error_nid_postfix() = 0; + Require(setitimer_nid_postfix(which, nullptr, nullptr) == -1 && *__error_nid_postfix() == 14, "a read into null did not report EFAULT"); + for (const GuestItimerval bad : {GuestItimerval{{0, 0}, {-1, 0}}, GuestItimerval{{0, 0}, {0, -1}}, + GuestItimerval{{0, 0}, {0, 1000000}}, GuestItimerval{{0, 0}, {INT_MAX / 2 + 1, 0}}, + GuestItimerval{{-1, 0}, {1, 0}}, GuestItimerval{{0, 1000000}, {1, 0}}, GuestItimerval{{INT_MAX / 2 + 1, 0}, {1, 0}}}) + RequireFailure(which, &bad, 22, "an invalid timeval did not report EINVAL"); + const GuestItimerval disarm{{-5, 7}, {0, 0}}; + GuestItimerval previous{{11, 22}, {33, 44}}; + Require(setitimer_nid_postfix(which, &disarm, &previous) == 0, "disarming with an invalid interval failed"); + Require(Micros(previous.interval) == 0 && Micros(previous.value) == 0, "an idle timer reported a value"); + Require(Micros(Read(which).interval) == 0, "disarming kept the interval"); + } + for (int cpu : {1, 2}) { + GuestItimerval previous{{11, 22}, {33, 44}}; + const auto saved = previous; + bool threw = false; + try { + setitimer_nid_postfix(cpu, &valid, &previous); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw && std::memcmp(&previous, &saved, sizeof(previous)) == 0, "a CPU timer was armed"); + Require(Micros(Read(cpu).value) == 0, "a CPU timer kept a value"); + } + bool threw = false; + const GuestItimerval tiny{{0, 999}, {1, 0}}; + try { + setitimer_nid_postfix(0, &tiny, nullptr); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw && Micros(Read(0).value) == 0, "an interval below the clock tick was armed"); + + GuestItimerval previous{}; + Require(setitimer_nid_postfix(0, &valid, &previous) == 0 && Micros(previous.value) == 0, "arming ITIMER_REAL failed"); + const auto first = Micros(Read(0).value); + Require(first > 29000000 && first <= 30000000, "the armed value is not visible"); + std::this_thread::sleep_for(std::chrono::milliseconds(25)); + const auto second = Micros(Read(0).value); + Require(second > 0 && second < first, "the remaining time does not count down"); + const GuestItimerval repeating{{2, 500000}, {20, 0}}; + Require(setitimer_nid_postfix(0, &repeating, &previous) == 0, "re-arming ITIMER_REAL failed"); + Require(Micros(previous.interval) == 0 && Micros(previous.value) > 0 && Micros(previous.value) <= second, "re-arming did not return the previous value"); + GuestItimerval queried{{11, 22}, {33, 44}}; + Require(getitimer_nid_postfix(0, &queried) == 0, "getitimer failed"); + Require(Micros(queried.interval) == 2500000 && Micros(queried.value) > 19000000 && Micros(queried.value) <= 20000000, + "getitimer did not report the setitimer interval"); + const GuestItimerval zero{{0, 0}, {0, 0}}; + Require(setitimer_nid_postfix(0, &zero, &previous) == 0, "disarming ITIMER_REAL failed"); + Require(Micros(previous.interval) == 2500000 && Micros(previous.value) > 19000000 && Micros(previous.value) <= 20000000, + "disarming did not return the previous value"); + const auto idle = Read(0); + Require(Micros(idle.interval) == 0 && Micros(idle.value) == 0, "the disarmed timer is still set"); + + const GuestItimerval periodic{{0, 300000}, {0, 100000}}; + Require(setitimer_nid_postfix(0, &periodic, nullptr) == 0, "arming a periodic timer failed"); + std::this_thread::sleep_for(std::chrono::milliseconds(250)); + const auto rearmed = Read(0); + Require(Micros(rearmed.interval) == 300000 && Micros(rearmed.value) > 0 && Micros(rearmed.value) <= 300000, + "the periodic timer was not re-armed by its interval"); + Require(GuestAlarm_nid_no_patch(5) == 1, "alarm did not report the periodic timer"); + const auto alarmed = Read(0); + Require(Micros(alarmed.interval) == 0 && Micros(alarmed.value) > 4000000 && Micros(alarmed.value) <= 5000000, "alarm did not replace the timer"); + Require(setitimer_nid_postfix(0, &zero, &previous) == 0 && Micros(previous.value) > 0, "disarming the alarm failed"); + Require(GuestAlarm_nid_no_patch(0) == 0, "setitimer did not cancel the alarm"); + + 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}}; + Require(setitimer_nid_postfix(0, &handled, nullptr) == 0, "arming the handled timer failed"); + std::this_thread::sleep_for(std::chrono::seconds(2)); + std::fprintf(stderr, "the expiry with a handler neither delivered nor threw\n"); + return 1; +} diff --git a/core/libs/tests/GuestJitSharedMemory.cpp b/core/libs/tests/GuestJitSharedMemory.cpp new file mode 100644 index 0000000000..66ed8a4838 --- /dev/null +++ b/core/libs/tests/GuestJitSharedMemory.cpp @@ -0,0 +1,192 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceKernelJitCreateSharedMemory(const char*, std::size_t, int, int*); +int APS5_VABI sceKernelJitCreateAliasOfSharedMemory(int, int, int*); +int APS5_VABI sceKernelJitMapSharedMemory(int, int, void**); +int APS5_VABI sceKernelMunmap(void*, std::size_t); +int APS5_VABI close_nid_postfix(int); +int APS5_VABI dup_nid_postfix(int); +int APS5_VABI dup2_nid_postfix(int, int); +int APS5_VABI shm_open_nid_postfix(const char*, int, int); +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 { + +constexpr std::size_t Size = 0x10000; +constexpr int Read = 0x1; +constexpr int Write = 0x2; +constexpr int Execute = 0x4; + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "requirement failed at line %u\n", static_cast(location.line())); + std::abort(); +} + +int Create(const char* name, int maxProt) { + int fd = -1; + Require(sceKernelJitCreateSharedMemory(name, Size, maxProt, &fd) == 0); + Require(fd >= 0); + return fd; +} + +void* Map(int fd, int prot, void* hint = nullptr) { + void* address = hint; + Require(sceKernelJitMapSharedMemory(fd, prot, &address) == 0); + Require(address != nullptr && (reinterpret_cast(address) & 0x3fff) == 0); + return address; +} + +int Call(const void* code) { + using Function = int (*)(); + Function function; + std::memcpy(&function, &code, sizeof(function)); + return function(); +} + +void WriteReturn(void* code, std::uint32_t value) { + unsigned char bytes[] = {0xb8, 0, 0, 0, 0, 0xc3}; + std::memcpy(bytes + 1, &value, sizeof(value)); + std::memcpy(code, bytes, sizeof(bytes)); +} + +void WriteThroughAliasRunsThroughExecutableView() { + const int fd = Create(nullptr, Read | Write | Execute); + int alias = -1; + Require(sceKernelJitCreateAliasOfSharedMemory(fd, Read | Write, &alias) == 0); + Require(alias >= 0 && alias != fd); + auto* writable = static_cast(Map(alias, Read | Write)); + auto* executable = static_cast(Map(fd, Read | Execute)); + Require(writable != executable); + Require(executable[0] == 0 && executable[Size - 1] == 0); + WriteReturn(writable, 0x12345678); + Require(std::memcmp(writable, executable, 6) == 0); + Require(Call(executable) == 0x12345678); + WriteReturn(writable + 0x4000, 0x0badcafe); + Require(Call(executable + 0x4000) == 0x0badcafe); + writable[Size - 1] = 0x5a; + Require(executable[Size - 1] == 0x5a); + Require(close_nid_postfix(fd) == 0); + auto* again = static_cast(Map(alias, Read)); + Require(again[Size - 1] == 0x5a); + Require(sceKernelMunmap(again, Size) == 0); + Require(sceKernelMunmap(writable, Size) == 0); + Require(Call(executable) == 0x12345678); + Require(sceKernelMunmap(executable, Size) == 0); + Require(close_nid_postfix(alias) == 0); +} + +void OneViewWritesAndRuns() { + const int fd = Create("ps5-platform-probe", Read | Write | Execute); + void* hint = reinterpret_cast(std::uintptr_t{0x1000000000}); + auto* code = static_cast(Map(fd, Read | Write | Execute, hint)); + Require(reinterpret_cast(code) >= 0x1000000000); + WriteReturn(code, 42); + Require(Call(code) == 42); + const int duplicate = dup_nid_postfix(fd); + Require(duplicate >= 0); + Require(close_nid_postfix(fd) == 0); + auto* view = static_cast(Map(duplicate, Read | Execute)); + Require(Call(view) == 42); + Require(sceKernelMunmap(view, Size) == 0); + Require(sceKernelMunmap(code, Size) == 0); + Require(close_nid_postfix(duplicate) == 0); +} + +void 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); + Require(target >= 0 && target != fd); + Require(dup2_nid_postfix(fd, target) == target); + auto* writable = static_cast(Map(fd, Read | Write)); + auto* readable = static_cast(Map(target, Read)); + writable[0x100] = 0x77; + Require(readable[0x100] == 0x77); + void* address = nullptr; + Require(sceKernelJitMapSharedMemory(target, Read | Execute, &address) == SCE_KERNEL_ERROR_EACCES); + Require(dup2_nid_postfix(fd, fd) == fd); + auto* same = static_cast(Map(fd, Read)); + Require(same[0x100] == 0x77); + const int plain = shm_open_nid_postfix(reinterpret_cast(1), 0x2, 0600); + Require(plain >= 0); + Require(dup2_nid_postfix(plain, target) == target); + address = nullptr; + Require(sceKernelJitMapSharedMemory(target, Read, &address) == SCE_KERNEL_ERROR_EBADF); + Require(address == nullptr); + Require(sceKernelMunmap(same, Size) == 0); + Require(sceKernelMunmap(readable, Size) == 0); + Require(sceKernelMunmap(writable, Size) == 0); + Require(close_nid_postfix(plain) == 0); + Require(close_nid_postfix(target) == 0); + Require(close_nid_postfix(fd) == 0); +} + +void InvalidArgumentsAreRejected() { + int fd = -1; + Require(sceKernelJitCreateSharedMemory(nullptr, 0, Read | Write | Execute, &fd) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelJitCreateSharedMemory(nullptr, 0x1000, Read | Write | Execute, &fd) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelJitCreateSharedMemory(nullptr, Size + 0x2000, Read | Write | Execute, &fd) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelJitCreateSharedMemory(nullptr, Size, 0x10, &fd) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelJitCreateSharedMemory(nullptr, Size, Read | Write | Execute, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(fd == -1); + + fd = Create(nullptr, Read | Execute); + int alias = -1; + void* address = nullptr; + Require(sceKernelJitCreateAliasOfSharedMemory(fd, Read | Write, &alias) == SCE_KERNEL_ERROR_EACCES); + Require(sceKernelJitCreateAliasOfSharedMemory(fd, 0x20, &alias) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelJitCreateAliasOfSharedMemory(fd, Read, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelJitCreateAliasOfSharedMemory(-1, Read, &alias) == SCE_KERNEL_ERROR_EBADF); + Require(alias == -1); + Require(sceKernelJitMapSharedMemory(fd, Read | Write, &address) == SCE_KERNEL_ERROR_EACCES); + Require(sceKernelJitMapSharedMemory(fd, 0x8, &address) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelJitMapSharedMemory(fd, Read, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelJitMapSharedMemory(-1, Read, &address) == SCE_KERNEL_ERROR_EBADF); + Require(address == nullptr); + Require(sceKernelJitCreateAliasOfSharedMemory(fd, Read, &alias) == 0); + Require(sceKernelJitMapSharedMemory(alias, Read | Execute, &address) == SCE_KERNEL_ERROR_EACCES); + Require(close_nid_postfix(alias) == 0); + Require(close_nid_postfix(fd) == 0); + Require(sceKernelJitMapSharedMemory(fd, Read, &address) == SCE_KERNEL_ERROR_EBADF); + Require(sceKernelJitCreateAliasOfSharedMemory(alias, Read, &alias) == SCE_KERNEL_ERROR_EBADF); + + const int shared = shm_open_nid_postfix(reinterpret_cast(1), 0x2, 0600); + Require(shared >= 0); + Require(sceKernelJitMapSharedMemory(shared, Read, &address) == SCE_KERNEL_ERROR_EBADF); + Require(close_nid_postfix(shared) == 0); +} + +} + +int main() { + WriteThroughAliasRunsThroughExecutableView(); + OneViewWritesAndRuns(); + Dup2KeepsTheDescriptorTableInStep(); + MmapOfTheDescriptorSharesTheObject(); + InvalidArgumentsAreRejected(); +} diff --git a/core/libs/tests/GuestJobManager.cpp b/core/libs/tests/GuestJobManager.cpp new file mode 100644 index 0000000000..7258ae6694 --- /dev/null +++ b/core/libs/tests/GuestJobManager.cpp @@ -0,0 +1,468 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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; +}; + +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*); +void APS5_VABI _ZN3sce3Job10JobManagerD1Ev(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 _ZN3sce3Job10JobManager10initializeEPvmjjPKNS1_12ConfigParamsE(void*, void*, std::uint64_t, std::uint32_t, std::uint32_t, const ConfigParams*); +int APS5_VABI _ZN3sce3Job10JobManager8shutdownEv(void*); +SequenceFactory* APS5_VABI _ZN3sce3Job10JobManager27getSequenceFactoryInterfaceENS1_8PriorityE(void*, std::uint32_t); +SequenceFactory* APS5_VABI _ZN3sce3Job10JobManager34getUnifiedSequenceFactoryInterfaceENS1_8PriorityE(void*, std::uint32_t); +int APS5_VABI _ZN3sce3Job10JobManager24setPersistentThreadCountEj(void*, std::uint32_t); +} + +#define Construct _ZN3sce3Job10JobManagerC1Ev +#define Destruct _ZN3sce3Job10JobManagerD1Ev +#define RequiredSize _ZN3sce3Job10JobManager27calculateRequiredMemorySizeEPKNS1_21MemorySizeQueryParamsE +#define InitializeUnified _ZN3sce3Job10JobManager10initializeEPvmjjjmPKNS1_12ConfigParamsE +#define Initialize _ZN3sce3Job10JobManager10initializeEPvmjjPKNS1_12ConfigParamsE +#define Shutdown _ZN3sce3Job10JobManager8shutdownEv +#define Factories _ZN3sce3Job10JobManager27getSequenceFactoryInterfaceENS1_8PriorityE +#define UnifiedFactories _ZN3sce3Job10JobManager34getUnifiedSequenceFactoryInterfaceENS1_8PriorityE +#define SetPersistentThreadCount _ZN3sce3Job10JobManager24setPersistentThreadCountEj + +static constexpr int InvalidArgument = static_cast(0x81581000u); +static constexpr int NullMemory = static_cast(0x81581001u); +static constexpr int MemoryTooSmall = static_cast(0x81581002u); +static constexpr int NotInitialized = static_cast(0x81581010u); +static constexpr int AlreadyInitialized = static_cast(0x81581020u); +static constexpr int MisalignedMemory = static_cast(0x81581030u); +static constexpr int OutOfRange = static_cast(0x81580000u); +static constexpr int NullArgument = static_cast(0x81580001u); +static constexpr int NoNodes = static_cast(0x81580002u); +static constexpr int LimitExceeded = static_cast(0x81580003u); +static constexpr int NoneFree = static_cast(0x81580006u); +static constexpr int Invalid = static_cast(0x81580007u); + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "job manager check failed at line %d\n", line); + std::abort(); + } +} + +#define Require(value) Check((value), __LINE__) + +struct Block { + explicit Block(std::uint64_t size) : bytes(size), memory(::operator new((size + 255) & ~std::uint64_t{255}, std::align_val_t{256})) {} + ~Block() { ::operator delete(memory, std::align_val_t{256}); } + std::uint64_t bytes; + void* memory; +}; + +struct ManagerObject { + ManagerObject() { Construct(storage); } + ~ManagerObject() { Destruct(storage); } + alignas(64) unsigned char storage[0x100]{}; +}; + +static std::uint64_t SizeFor(std::uint32_t workers, std::uint32_t sequences, std::uint32_t dispatches, std::uint32_t syncPoints, std::uint32_t waits) { + const MemorySizeQueryParams query{0, workers, sequences, 0, dispatches, syncPoints, waits, 0, 0}; + return RequiredSize(&query); +} + +static void MemoryCase() { + const MemorySizeQueryParams query{0, 4, 2, 100, 8, 4, 2, 0, 0}; + Require(RequiredSize(&query) == 0x17c8); + const MemorySizeQueryParams unified{1, 4, 2, 100, 8, 4, 2, 0, 0}; + Require(RequiredSize(&unified) == 0x17c8); + const MemorySizeQueryParams single{0, 1, 1, 0, 0, 0, 0, 0, 0}; + Require(RequiredSize(&single) == 0x1090); + Require(RequiredSize(nullptr) == 0); + for (const MemorySizeQueryParams& bad : { + MemorySizeQueryParams{2, 4, 2, 0, 0, 0, 0, 0, 0}, MemorySizeQueryParams{0, 0, 2, 0, 0, 0, 0, 0, 0}, + MemorySizeQueryParams{0, 14, 2, 0, 0, 0, 0, 0, 0}, MemorySizeQueryParams{0, 4, 0, 0, 0, 0, 0, 0, 0}, + MemorySizeQueryParams{0, 4, 33, 0, 0, 0, 0, 0, 0}, MemorySizeQueryParams{0, 4, 2, 2 * 0x7fff + 1, 0, 0, 0, 0, 0}, + MemorySizeQueryParams{0, 4, 2, 0, 2 * 0x3ff + 1, 0, 0, 0, 0}, MemorySizeQueryParams{0, 4, 2, 0, 0, 2 * 0xff + 1, 0, 0, 0}, + MemorySizeQueryParams{0, 4, 2, 0, 0, 0, 2 * 0x3f + 1, 0, 0}, MemorySizeQueryParams{1, 4, 0, 0, 0, 0, 0, 0, 0}, + MemorySizeQueryParams{1, 4, 2, 0, 0, 0, 0, 0, std::uint64_t{1} << 56}}) { + Require(RequiredSize(&bad) == 0); + } +} + +static void APS5_VABI Nothing(void*, std::uint32_t) { +} + +static void ValidationCase() { + Block block(SizeFor(2, 2, 16, 4, 4)); + ManagerObject manager; + Require(Shutdown(manager.storage) == NotInitialized); + Require(SetPersistentThreadCount(manager.storage, 0) == NotInitialized); + Require(Factories(manager.storage, 0) == nullptr && UnifiedFactories(manager.storage, 1) == nullptr); + Require(Initialize(manager.storage, nullptr, block.bytes, 2, 2, nullptr) == NullMemory); + Require(Initialize(manager.storage, static_cast(block.memory) + 16, block.bytes - 16, 2, 2, nullptr) == MisalignedMemory); + Require(Initialize(manager.storage, block.memory, 0, 2, 2, nullptr) == InvalidArgument); + Require(Initialize(manager.storage, block.memory, block.bytes, 0, 2, nullptr) == InvalidArgument); + Require(Initialize(manager.storage, block.memory, block.bytes, 14, 2, nullptr) == InvalidArgument); + Require(Initialize(manager.storage, block.memory, block.bytes, 2, 0, nullptr) == InvalidArgument); + Require(Initialize(manager.storage, block.memory, block.bytes, 2, 33, nullptr) == InvalidArgument); + Require(InitializeUnified(manager.storage, block.memory, block.bytes, 2, 0, 0, 0, nullptr) == InvalidArgument); + Require(InitializeUnified(manager.storage, block.memory, block.bytes, 2, 2, 0, std::uint64_t{1} << 56, nullptr) == InvalidArgument); + Require(Initialize(manager.storage, block.memory, 8, 2, 2, nullptr) == MemoryTooSmall); + Require(Initialize(manager.storage, block.memory, 0x400, 2, 2, nullptr) == MemoryTooSmall); + ConfigParams config{0, 0x00021fff, 0x10000, 0x2bc, nullptr, 0, 0x1fff, 0x100, 0}; + for (int field = 0; field < 7; ++field) { + ConfigParams bad = config; + if (field == 0) bad.version = 2; + if (field == 1) bad.threadFlags = 0x00001fff; + if (field == 2) bad.threadFlags = 0x00020001; + if (field == 3) bad.stackSize = 0x3fff; + if (field == 4) bad.priority = 0x300; + if (field == 5) bad.spinMicroseconds = 0x33; + if (field == 6) bad.reserved = 1; + Require(Initialize(manager.storage, block.memory, block.bytes, 2, 2, &bad) == InvalidArgument); + Require(Factories(manager.storage, 0) == nullptr); + } + config.threadFlags = 0x00010003; + config.threadName = "JobManagerTestWorker"; + Require(Initialize(manager.storage, block.memory, block.bytes, 2, 2, &config) == 0); + std::uint16_t magic = 0; + std::memcpy(&magic, block.memory, sizeof(magic)); + Require(magic == 0x4d4a); + Require(Initialize(manager.storage, block.memory, block.bytes, 2, 2, nullptr) == AlreadyInitialized); + Require(SetPersistentThreadCount(manager.storage, 3) == InvalidArgument); + Require(SetPersistentThreadCount(manager.storage, 2) == 0); + SequenceFactory* factory = Factories(manager.storage, 0); + Require(factory != nullptr && Factories(manager.storage, 1) != nullptr && factory != Factories(manager.storage, 1)); + Require(UnifiedFactories(manager.storage, 0) != nullptr); + Sequence* sequence = nullptr; + Require(factory->Create(nullptr, 1, 1, 1, 1, nullptr) == NullArgument); + Require(factory->Create(&sequence, 0, 0, 0, 0, nullptr) == OutOfRange); + Require(factory->Create(&sequence, 0x8000, 1, 1, 1, nullptr) == OutOfRange); + Require(factory->Create(&sequence, 1, 0x400, 1, 1, nullptr) == OutOfRange); + Require(factory->Create(&sequence, 1, 1, 0x100, 1, nullptr) == OutOfRange); + Require(factory->Create(&sequence, 1, 1, 1, 0x40, nullptr) == OutOfRange); + Require(factory->CreateCompute(&sequence, 1, 1, 1, 1, nullptr) == NoneFree && sequence == nullptr); + Sequence* first = nullptr; + Sequence* second = nullptr; + Require(factory->Create(&first, 8, 8, 2, 2, "first") == 0 && first != nullptr); + Require(factory->Create(&second, 8, 8, 2, 2, "second") == 0 && second != nullptr && second != first); + Require(factory->Create(&sequence, 8, 8, 2, 2, "third") == NoneFree); + Require(factory->DestroyCompute(first) == Invalid); + Require(factory->Destroy(reinterpret_cast(block.memory)) == Invalid); + Require(factory->Destroy(first) == 0 && factory->Destroy(first) == Invalid); + Require(factory->Create(&sequence, 8, 8, 2, 2, "again") == 0 && sequence == first); + Require(sequence->Dispatch(nullptr, nullptr, 1, nullptr) == NullArgument); + Require(sequence->Dispatch(Nothing, nullptr, 0, nullptr) == OutOfRange); + Require(sequence->Dispatch(Nothing, nullptr, 0x8000, nullptr) == OutOfRange); + Require(factory->Destroy(sequence) == 0 && factory->Destroy(second) == 0); + Require(Shutdown(manager.storage) == 0); +} + +struct Tally { + std::atomic calls{0}; + std::vector> seen; + explicit Tally(std::size_t count) : seen(count) {} +}; + +static void APS5_VABI CountJob(void* argument, std::uint32_t index) { + auto& tally = *static_cast(argument); + ++tally.seen[index]; + ++tally.calls; +} + +static void DispatchCase() { + Block block(SizeFor(4, 1, 8, 2, 2)); + ManagerObject manager; + Require(Initialize(manager.storage, block.memory, block.bytes, 4, 1, nullptr) == 0); + SequenceFactory* factory = Factories(manager.storage, 0); + Sequence* sequence = nullptr; + Require(factory->Create(&sequence, 0x7fff, 8, 2, 2, "dispatch") == 0); + Tally tally(1000); + Tally single(1); + Require(sequence->Dispatch(CountJob, &tally, 1000, "many") == 0); + Require(sequence->Dispatch(CountJob, &single, 1, "one") == 0); + Require(sequence->Wait() == 0); + Require(tally.calls == 1000 && single.calls == 1); + for (const auto& count : tally.seen) Require(count == 1); + Require(sequence->Wait() == 0); + Require(factory->Destroy(sequence) == 0); + Require(Shutdown(manager.storage) == 0); +} + +struct Stage { + std::atomic* finished; + std::uint32_t expected; + std::atomic* early; +}; + +static void APS5_VABI StageJob(void* argument, std::uint32_t) { + auto& stage = *static_cast(argument); + if (stage.finished->load() < stage.expected) ++*stage.early; + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + ++*stage.finished; +} + +static void OrderingCase() { + Block block(SizeFor(4, 1, 8, 4, 2)); + ManagerObject manager; + Require(Initialize(manager.storage, block.memory, block.bytes, 4, 1, nullptr) == 0); + SequenceFactory* factory = Factories(manager.storage, 1); + Sequence* sequence = nullptr; + Require(factory->Create(&sequence, 256, 8, 4, 2, "ordering") == 0); + std::atomic finished{0}; + std::atomic early{0}; + Stage first{&finished, 0, &early}; + Stage second{&finished, 16, &early}; + Stage third{&finished, 32, &early}; + Require(sequence->Dispatch(StageJob, &first, 8, "a") == 0); + Require(sequence->Dispatch(StageJob, &first, 8, "b") == 0); + Require(sequence->SyncPoint() == 0); + Require(sequence->Dispatch(StageJob, &second, 16, "c") == 0); + Require(sequence->SyncPoint() == 0); + Require(sequence->SyncPoint() == 0); + Require(sequence->Dispatch(StageJob, &third, 4, "d") == 0); + Require(sequence->Wait() == 0); + Require(finished == 36 && early == 0); + Require(factory->Destroy(sequence) == 0); + Require(Shutdown(manager.storage) == 0); +} + +struct Nested { + Sequence* sequence; + std::atomic barrierAppended{false}; + std::atomic followerAppended{false}; + std::atomic leaderFinished{false}; + std::atomic followerResult{-1}; + std::atomic followerSawLeaderFinished{-1}; +}; + +static void APS5_VABI FollowerJob(void* argument, std::uint32_t) { + auto& nested = *static_cast(argument); + nested.followerSawLeaderFinished = nested.leaderFinished ? 1 : 0; +} + +static void APS5_VABI LeaderJob(void* argument, std::uint32_t) { + auto& nested = *static_cast(argument); + while (!nested.barrierAppended) std::this_thread::yield(); + nested.followerResult = nested.sequence->Dispatch(FollowerJob, &nested, 1, "follower"); + nested.followerAppended = true; + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + nested.leaderFinished = true; +} + +static void NestedCase() { + Block block(SizeFor(2, 1, 4, 2, 2)); + ManagerObject manager; + Require(Initialize(manager.storage, block.memory, block.bytes, 2, 1, nullptr) == 0); + SequenceFactory* factory = Factories(manager.storage, 0); + Nested nested{}; + Require(factory->Create(&nested.sequence, 16, 4, 2, 2, "nested") == 0); + Require(nested.sequence->Dispatch(LeaderJob, &nested, 1, "leader") == 0); + Require(nested.sequence->SyncPoint() == 0); + nested.barrierAppended = true; + while (!nested.followerAppended) std::this_thread::yield(); + Require(nested.sequence->Wait() == 0); + Require(nested.followerResult == 0 && nested.followerSawLeaderFinished == 1); + nested.sequence->CompleteDestructor(); + Require(nested.sequence->Wait() == 0); + Require(factory->Destroy(nested.sequence) == 0); + Require(factory->Create(&nested.sequence, 1, 1, 0, 0, "no waits") == 0); + Require(nested.sequence->Wait() == LimitExceeded); + Require(nested.sequence->Dispatch(Nothing, nullptr, 1, nullptr) == Invalid); + Require(factory->Destroy(nested.sequence) == 0); + Require(Factories(manager.storage, 0x10000) == factory); + Require(Shutdown(manager.storage) == 0); +} + +static void LimitsCase() { + Block block(SizeFor(2, 2, 2, 1, 1)); + ManagerObject manager; + Require(Initialize(manager.storage, block.memory, block.bytes, 2, 2, nullptr) == 0); + SequenceFactory* factory = Factories(manager.storage, 0); + Sequence* sequence = nullptr; + Require(factory->Create(&sequence, 4, 2, 1, 1, "limits") == 0); + for (int round = 0; round < 3; ++round) { + Require(sequence->Dispatch(Nothing, nullptr, 2, nullptr) == 0); + Require(sequence->Dispatch(Nothing, nullptr, 2, nullptr) == 0); + Require(sequence->SyncPoint() == 0); + Require(sequence->Wait() == 0); + } + Require(sequence->Dispatch(Nothing, nullptr, 2, nullptr) == 0); + Require(sequence->Dispatch(Nothing, nullptr, 3, nullptr) == LimitExceeded); + Require(sequence->Dispatch(Nothing, nullptr, 1, nullptr) == Invalid); + Require(sequence->SyncPoint() == Invalid); + Require(sequence->Wait() == Invalid); + Require(factory->Destroy(sequence) == 0); + Require(factory->Create(&sequence, 4, 2, 1, 1, "dispatches") == 0); + Require(sequence->Dispatch(Nothing, nullptr, 1, nullptr) == 0 && sequence->Dispatch(Nothing, nullptr, 1, nullptr) == 0); + Require(sequence->Dispatch(Nothing, nullptr, 1, nullptr) == LimitExceeded); + Require(factory->Destroy(sequence) == 0); + Require(factory->Create(&sequence, 4, 2, 1, 1, "sync points") == 0); + Require(sequence->SyncPoint() == 0 && sequence->SyncPoint() == LimitExceeded); + Require(factory->Destroy(sequence) == 0); + Require(factory->Create(&sequence, 4, 2, 1, 1, "waits") == 0); + Require(sequence->Wait() == 0 && sequence->Wait() == 0); + Require(factory->Destroy(sequence) == 0); + Require(factory->Create(&sequence, 0x7fff, 0x3ff, 0xff, 1, "nodes") == 0); + int result = 0; + int accepted = 0; + 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); +} + +struct Order { + std::atomic release{false}; + std::atomic started{false}; + std::atomic next{0}; + std::uint32_t low = 0; + std::uint32_t high = 0; +}; + +static void APS5_VABI GateJob(void* argument, std::uint32_t) { + auto& order = *static_cast(argument); + order.started = true; + while (!order.release) std::this_thread::yield(); +} + +static void APS5_VABI LowJob(void* argument, std::uint32_t) { + auto& order = *static_cast(argument); + order.low = ++order.next; +} + +static void APS5_VABI HighJob(void* argument, std::uint32_t) { + auto& order = *static_cast(argument); + order.high = ++order.next; +} + +static void PriorityCase() { + Block block(SizeFor(1, 3, 8, 2, 4)); + ManagerObject manager; + Require(Initialize(manager.storage, block.memory, block.bytes, 1, 3, nullptr) == 0); + Sequence* gate = nullptr; + Sequence* low = nullptr; + Sequence* high = nullptr; + Require(Factories(manager.storage, 0)->Create(&gate, 4, 4, 1, 1, "gate") == 0); + Require(Factories(manager.storage, 0)->Create(&low, 4, 4, 1, 1, "low") == 0); + Require(Factories(manager.storage, 1)->Create(&high, 4, 4, 1, 1, "high") == 0); + Order order; + Require(gate->Dispatch(GateJob, &order, 1, nullptr) == 0); + while (!order.started) std::this_thread::yield(); + Require(low->Dispatch(LowJob, &order, 1, nullptr) == 0); + Require(high->Dispatch(HighJob, &order, 1, nullptr) == 0); + order.release = true; + Require(gate->Wait() == 0 && low->Wait() == 0 && high->Wait() == 0); + Require(order.high == 1 && order.low == 2); + Require(Factories(manager.storage, 0)->Destroy(gate) == 0 && Factories(manager.storage, 0)->Destroy(low) == 0); + Require(Factories(manager.storage, 1)->Destroy(high) == 0); + Require(Shutdown(manager.storage) == 0); +} + +static void ShutdownCase() { + Block block(SizeFor(3, 2, 8, 2, 2)); + ManagerObject manager; + for (int round = 0; round < 3; ++round) { + Require(Initialize(manager.storage, block.memory, block.bytes, 3, 2, nullptr) == 0); + SequenceFactory* factory = UnifiedFactories(manager.storage, 0); + Sequence* sequence = nullptr; + Tally tally(64); + Require(factory->Create(&sequence, 64, 8, 2, 2, "shutdown") == 0); + Require(sequence->Dispatch(CountJob, &tally, 64, nullptr) == 0 && sequence->Wait() == 0 && tally.calls == 64); + Require(Shutdown(manager.storage) == 0); + 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) { + const std::string name = argc > 1 ? argv[1] : ""; + if (name == "memory") MemoryCase(); + else if (name == "validation") ValidationCase(); + else if (name == "dispatch") DispatchCase(); + else if (name == "ordering") OrderingCase(); + else if (name == "limits") LimitsCase(); + else if (name == "nested") NestedCase(); + else if (name == "priority") PriorityCase(); + else if (name == "shutdown") ShutdownCase(); + else return 2; + return 0; +} diff --git a/core/libs/tests/GuestJpegDec.cpp b/core/libs/tests/GuestJpegDec.cpp index 1268bd57d2..018dba662d 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,21 +35,35 @@ 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; } @@ -43,63 +75,172 @@ int main() { constexpr std::int32_t invalidWorkMemory = static_cast(0x80690005); constexpr std::int32_t invalidData = static_cast(0x80690010); - 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) == invalidParam); + 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(ThrowsRuntimeError([&] { sceJpegDecParseHeader(&unknownScaleParse, &info); })); + } + 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(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); + + const JpegDecCreateParam param{sizeof(JpegDecCreateParam), info.suitable_csc_attribute, info.image_width}; const std::int32_t memorySize = sceJpegDecQueryMemorySize(¶m); Require(memorySize > 0); + const JpegDecCreateParam fullParam{sizeof(JpegDecCreateParam), 2, info.image_width}; + Require(sceJpegDecQueryMemorySize(&fullParam) == memorySize); Require(sceJpegDecQueryMemorySize(nullptr) == invalidParam); - const JpegDecCreateParam badSize{sizeof(JpegDecCreateParam) - 1, 0}; + 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); void* handle = nullptr; Require(sceJpegDecCreate(¶m, memory.data(), 0, &handle) == invalidWorkMemory); Require(sceJpegDecCreate(¶m, memory.data(), static_cast(memorySize), nullptr) == invalidAddr); - Require(sceJpegDecCreate(¶m, memory.data(), static_cast(memorySize), &handle) == 0); + Require(sceJpegDecCreate(&fullParam, memory.data(), static_cast(memorySize), &handle) == 0); Require(handle != nullptr); 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); Require(sceJpegDecDecode(nullptr, &decodeParam, &info) == invalidHandle); Require(sceJpegDecDecode(handle, nullptr, &info) == invalidParam); 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(ThrowsRuntimeError([&] { sceJpegDecDecode(handle, &unknownScale, &info); })); + 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(4 * pitch + 7 * 4, 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); + 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 - 1) * paddedPitch + width * 4, 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/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index 8150bc2491..16de775009 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -21,6 +21,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,12 +32,25 @@ 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*); +const Value* APS5_VABI _ZNK3sce4Json5Array5frontEv(const Array*); +Value* APS5_VABI _ZNK3sce4Json5Array8iteratorptEv(const Iterator*); void APS5_VABI _ZN3sce4Json5ArrayD1Ev(Array*); void APS5_VABI _ZN3sce4Json5Array9push_backERKNS0_5ValueE(Array*, const Value*); Iterator* APS5_VABI _ZNK3sce4Json5Array5beginEv(Iterator*, const Array*); @@ -53,7 +69,28 @@ Iterator* APS5_VABI _ZNK3sce4Json6Object3endEv(Iterator*, const Object*); void APS5_VABI _ZN3sce4Json6Object8iteratorD1Ev(Iterator*); Iterator* APS5_VABI _ZN3sce4Json6Object8iteratorppEv(Iterator*); 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); @@ -75,6 +112,7 @@ Value* APS5_VABI _ZN3sce4Json5Value10referValueERKNS0_6StringE(Value*, const Str const Value* APS5_VABI _ZNK3sce4Json5ValueixEm(const Value*, std::size_t); int APS5_VABI _ZN3sce4Json5Value9serializeERNS0_6StringE(Value*, String*); int APS5_VABI _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(Value*, const char*, std::size_t); +int APS5_VABI _ZN3sce4Json6Parser5parseERNS0_5ValueEPFiRcPvES5_(Value*, int (APS5_VABI*)(char&, void*), void*); bool APS5_VABI _ZNK3sce4Json6Object5emptyEv(const Object*); std::size_t APS5_VABI _ZNK3sce4Json6Object4sizeEv(const Object*); const Object* APS5_VABI _ZNK3sce4Json5Value9getObjectEv(const Value*); @@ -83,9 +121,31 @@ const Value* APS5_VABI _ZNK3sce4Json5Value8getValueEm(const Value*, std::size_t) const Value* APS5_VABI _ZNK3sce4Json5Value8getValueERKNS0_6StringE(const Value*, const String*); Value* APS5_VABI _ZN3sce4Json5Value10referValueEm(Value*, std::size_t); void APS5_VABI _ZN3sce4Json14InitParameter2C1Ev(void*); +void APS5_VABI _ZN3sce4Json14InitParameter2C2Ev(void*); void APS5_VABI _ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv(void*, void*, void*); void APS5_VABI _ZN3sce4Json14InitParameter217setFileBufferSizeEm(void*, std::size_t); int APS5_VABI _ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E(void*, const 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); +std::size_t APS5_VABI _ZN3sce4Json6Object5eraseERKNS0_6StringE(Object*, const String*); } static void Check(bool value, int line) { @@ -97,6 +157,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(0x80920101); static Value fallback{}; static int callbackCalls = 0; @@ -182,6 +243,10 @@ static void ParseAndRoundTrip() { Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, invalid, std::strlen(invalid)) < 0); Require(_ZNK3sce4Json5Value7getTypeEv(&root) == TypeObject && _ZNK3sce4Json5Value5countEv(&root) == 8); } + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, nullptr, 0) == JsonErrorParse); + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, nullptr, 10) == JsonErrorParse); + 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 { @@ -268,9 +333,393 @@ static void ObjectsAndArrays() { _ZNK3sce4Json5Array3endEv(&end, &array); for (; _ZNK3sce4Json5Array8iteratorneERKS2_(&it, &end); _ZN3sce4Json5Array8iteratorppEv(&it)) ++visited; Require(visited == 2); + Require(*_ZNK3sce4Json5Value10getBooleanEv(_ZNK3sce4Json5Array5frontEv(&array))); + _ZNK3sce4Json5Array5beginEv(&it, &array); + Require(_ZNK3sce4Json5Array8iteratorptEv(&it) == _ZNK3sce4Json5Array8iteratordeEv(&it)); + Array copy{}; + _ZN3sce4Json5ArrayC1ERKS1_(©, &array); + _ZN3sce4Json5Array5clearEv(&array); + Require(_ZNK3sce4Json5Array4sizeEv(&array) == 0); + bool threw = false; + try { + _ZNK3sce4Json5Array5frontEv(&array); + } catch (const std::out_of_range&) { + threw = true; + } + Require(threw); + Require(_ZNK3sce4Json5Array4sizeEv(©) == 2); + Require(*_ZNK3sce4Json5Value10getBooleanEv(_ZNK3sce4Json5Array5frontEv(©))); + Require(*_ZNK3sce4Json5Value7getRealEv(_ZNK3sce4Json5Array4backEv(©)) == 2.5); + _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)) { + 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); + + _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); + String alpha{}, beta{}, gamma{}; + _ZN3sce4Json6StringC1EPKc(&alpha, "alpha"); + _ZN3sce4Json6StringC1EPKc(&beta, "beta"); + _ZN3sce4Json6StringC1EPKc(&gamma, "gamma"); + Iterator found{}, end{}; + _ZNK3sce4Json6Object3endEv(&end, &object); + _ZNK3sce4Json6Object4findERKNS0_6StringE(&found, &object, &alpha); + Require(!_ZNK3sce4Json6Object8iteratorneERKS2_(&found, &end)); + _ZN3sce4Json6Object8iteratorD1Ev(&found); + _ZN3sce4Json6Object8iteratorD1Ev(&end); + + Require(_ZN3sce4Json5Value3setEl(_ZN3sce4Json6ObjectixERKNS0_6StringE(&object, &alpha), 1) == 0); + Value* second = _ZN3sce4Json6ObjectixERKNS0_6StringE(&object, &beta); + Require(_ZN3sce4Json5Value3setEPKc(second, "b") == 0); + _ZNK3sce4Json6Object3endEv(&end, &object); + Iterator* returned = _ZNK3sce4Json6Object4findERKNS0_6StringE(&found, &object, &beta); + Require(returned == &found); + Require(_ZNK3sce4Json6Object8iteratorneERKS2_(&found, &end)); + const Pair* pair = _ZNK3sce4Json6Object8iteratorptEv(&found); + Require(pair == _ZNK3sce4Json6Object8iteratordeEv(&found)); + Require(&pair->value == second); + Require(std::strcmp(_ZNK3sce4Json6String5c_strEv(&pair->key), "beta") == 0); + _ZN3sce4Json6Object8iteratorppEv(&found); + Require(!_ZNK3sce4Json6Object8iteratorneERKS2_(&found, &end)); + _ZNK3sce4Json6Object4findERKNS0_6StringE(&found, &object, &gamma); + Require(!_ZNK3sce4Json6Object8iteratorneERKS2_(&found, &end)); + Require(_ZNK3sce4Json6Object4sizeEv(&object) == 2); + _ZN3sce4Json6Object8iteratorD1Ev(&found); + _ZN3sce4Json6Object8iteratorD1Ev(&end); + + Require(_ZNK3sce4Json6String7compareEPKc(&beta, "beta") == 0); + Require(_ZNK3sce4Json6String7compareEPKc(&beta, "betb") < 0); + Require(_ZNK3sce4Json6String7compareEPKc(&beta, "bet") > 0); + Require(_ZNK3sce4Json6String7compareEPKc(&beta, "alpha") > 0); + bool threw = false; + try { + static_cast(_ZNK3sce4Json6String7compareEPKc(&beta, nullptr)); + } catch (const std::invalid_argument&) { + threw = true; + } + Require(threw); + _ZN3sce4Json6StringD1Ev(&alpha); + _ZN3sce4Json6StringD1Ev(&beta); + _ZN3sce4Json6StringD1Ev(&gamma); + _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); + + 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{}; + 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); + + 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); + + _ZN3sce4Json6Object8iteratorD1Ev(&otherInserted); + _ZN3sce4Json6Object4PairD1Ev(&otherPair); + _ZN3sce4Json5ValueD1Ev(&otherValue); + _ZN3sce4Json6StringD1Ev(&otherKey); + _ZN3sce4Json6Object8iteratorD1Ev(&inserted); + _ZN3sce4Json6Object8iteratorD1Ev(&end); + _ZN3sce4Json6Object4PairD1Ev(&pair); + _ZN3sce4Json5ValueD1Ev(&value); + _ZN3sce4Json6StringD1Ev(&key); + _ZN3sce4Json6ObjectD1Ev(&object); +} + +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); + _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); + _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, &p1); + _ZN3sce4Json6Object8iteratorD1Ev(&ins); + _ZN3sce4Json6Object6insertERKNS1_4PairE(&ins, &object, &p2); + _ZN3sce4Json6Object8iteratorD1Ev(&ins); + _ZN3sce4Json6Object6insertERKNS1_4PairE(&ins, &object, &p3); + _ZN3sce4Json6Object8iteratorD1Ev(&ins); + + Iterator begin{}; + _ZNK3sce4Json6Object5beginEv(&begin, &object); + Iterator copy{}; + _ZN3sce4Json6Object8iteratorC1ERKS2_(©, &begin); + Require(_ZN3sce4Json6Object8iterator7advanceEm(©, 2) == ©); + Require(std::strcmp(_ZNK3sce4Json6String5c_strEv(&_ZNK3sce4Json6Object8iteratordeEv(©)->key), "gamma") == 0); + _ZN3sce4Json6Object8iteratorD1Ev(&begin); + _ZN3sce4Json6Object8iteratorD1Ev(©); + + Require(_ZNK3sce4Json6Object4sizeEv(&object) == 3); + Require(_ZN3sce4Json6Object5eraseERKNS0_6StringE(&object, &k2) == 1); + Require(_ZNK3sce4Json6Object4sizeEv(&object) == 2); + Require(_ZN3sce4Json6Object5eraseERKNS0_6StringE(&object, &k2) == 0); + + 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; @@ -361,6 +810,106 @@ static void ValueAccess() { Require(_ZN3sce4Json11InitializerD1Ev(initializer) == 0); } +static void InitParameter2BaseConstructor() { + alignas(16) std::uint8_t parameter[40]; + std::memset(parameter, 0xff, sizeof(parameter)); + _ZN3sce4Json14InitParameter2C1Ev(parameter); + alignas(16) std::uint8_t baseStorage[72]; + std::memset(baseStorage, 0xff, sizeof(baseStorage)); + auto* baseParameter = baseStorage + 16; + _ZN3sce4Json14InitParameter2C2Ev(baseParameter); + Require(std::memcmp(parameter, baseParameter, sizeof(parameter)) == 0); + void* defaultAllocator; + void* defaultUserData; + std::size_t defaultFileBufferSize; + std::memcpy(&defaultAllocator, baseParameter, sizeof(defaultAllocator)); + std::memcpy(&defaultUserData, baseParameter + sizeof(void*), sizeof(defaultUserData)); + std::memcpy(&defaultFileBufferSize, baseParameter + 2 * sizeof(void*), sizeof(defaultFileBufferSize)); + Require(defaultAllocator == nullptr && defaultUserData == nullptr && defaultFileBufferSize == 0); + int allocator = 0; + int userData = 0; + _ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv(baseParameter, &allocator, &userData); + _ZN3sce4Json14InitParameter217setFileBufferSizeEm(baseParameter, 4096); + void* storedAllocator; + void* storedUserData; + std::size_t storedFileBufferSize; + std::memcpy(&storedAllocator, baseParameter, sizeof(storedAllocator)); + std::memcpy(&storedUserData, baseParameter + sizeof(void*), sizeof(storedUserData)); + std::memcpy(&storedFileBufferSize, baseParameter + 2 * sizeof(void*), sizeof(storedFileBufferSize)); + Require(storedAllocator == &allocator && storedUserData == &userData && storedFileBufferSize == 4096); + for (std::size_t i = 0; i < 16; ++i) Require(baseStorage[i] == 0xff); + for (std::size_t i = 16 + sizeof(parameter); i < sizeof(baseStorage); ++i) + Require(baseStorage[i] == 0xff); + alignas(16) std::uint8_t initializer[16]{}; + Require(_ZN3sce4Json11InitializerC1Ev(initializer) == 0); + Require(_ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E(initializer, baseParameter) == 0); + Require(_ZN3sce4Json11Initializer9terminateEv(initializer) == 0); + Require(_ZN3sce4Json11InitializerD1Ev(initializer) == 0); +} + +struct StoredInitParameter2 { + void* allocator; + void* userData; + std::size_t fileBufferSize; + std::int32_t specialFloatFormatType; + std::uint32_t reserved[3]; +}; +static_assert(sizeof(StoredInitParameter2) == 40); + +static StoredInitParameter2 Stored(const std::uint8_t* parameter) { + StoredInitParameter2 stored{}; + std::memcpy(&stored, parameter, sizeof(stored)); + return stored; +} + +static bool Untouched(const std::uint8_t* parameter, std::size_t from) { + for (std::size_t i = from; i < 40; ++i) + if (parameter[i] != 0xff) return false; + return true; +} + +static void InitParameters() { + using Constructor = void (APS5_VABI*)(void*); + for (Constructor construct : {_ZN3sce4Json14InitParameter2C1Ev, _ZN3sce4Json14InitParameter2C2Ev}) { + alignas(16) std::uint8_t parameter[40]; + std::memset(parameter, 0xff, sizeof(parameter)); + construct(parameter); + StoredInitParameter2 stored = Stored(parameter); + Require(stored.allocator == nullptr && stored.userData == nullptr && stored.fileBufferSize == 0); + Require(stored.specialFloatFormatType == 0); + Require(stored.reserved[0] == 0 && stored.reserved[1] == 0 && stored.reserved[2] == 0); + + int allocParam = 0; + int userData = 0; + _ZN3sce4Json14InitParameter217setFileBufferSizeEm(parameter, 8192); + _ZN3sce4Json14InitParameter225setSpecialFloatFormatTypeENS0_22SpecialFloatFormatTypeE(parameter, 2); + _ZN3sce4Json18InitParameterRtti216setAllocatorRttiEPNS0_14AllocParamRttiEPv(parameter, &allocParam, &userData); + stored = Stored(parameter); + Require(stored.allocator == &allocParam && stored.userData == &userData && stored.fileBufferSize == 8192); + Require(stored.specialFloatFormatType == 2); + Require(stored.reserved[0] == 0 && stored.reserved[1] == 0 && stored.reserved[2] == 0); + + _ZN3sce4Json14InitParameter225setSpecialFloatFormatTypeENS0_22SpecialFloatFormatTypeE(parameter, 0); + Require(Stored(parameter).specialFloatFormatType == 0 && Stored(parameter).fileBufferSize == 8192); + + alignas(16) std::uint8_t initializer[16]{}; + Require(_ZN3sce4Json11InitializerC1Ev(initializer) == 0); + Require(_ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E(initializer, parameter) == 0); + Require(_ZN3sce4Json11Initializer9terminateEv(initializer) == 0); + Require(_ZN3sce4Json11InitializerD1Ev(initializer) == 0); + } + + alignas(16) std::uint8_t rttiParameter[40]; + std::memset(rttiParameter, 0xff, sizeof(rttiParameter)); + int allocParam = 0; + int userData = 0; + _ZN3sce4Json17InitParameterRttiC1EPNS0_14AllocParamRttiEPvm(rttiParameter, &allocParam, &userData, 4096); + void* rttiStored[3]{}; + std::memcpy(rttiStored, rttiParameter, sizeof(rttiStored)); + Require(rttiStored[0] == &allocParam && rttiStored[1] == &userData && reinterpret_cast(rttiStored[2]) == 4096); + Require(Untouched(rttiParameter, sizeof(rttiStored))); +} + static void ValueClear() { Value value{}; _ZN3sce4Json5ValueC1Ev(&value); @@ -384,6 +933,7 @@ static void ValueClear() { _ZN3sce4Json6StringC1EPKc(&key, "prop"); Value* item = _ZN3sce4Json6ObjectixERKNS0_6StringE(&obj, &key); _ZN3sce4Json5Value3setEl(item, 999); + _ZN3sce4Json5ValueD1Ev(&value); _ZN3sce4Json5ValueC1ERKNS0_6ObjectE(&value, &obj); Require(_ZNK3sce4Json5Value7getTypeEv(&value) == TypeObject); _ZN3sce4Json5Value5clearEv(&value); @@ -398,12 +948,271 @@ static void ValueClear() { _ZN3sce4Json5ValueD1Ev(&value); } +struct ReadState { + const char* text; + std::size_t position; + int calls; +}; + +static int APS5_VABI ReadNext(char& ch, void* context) { + auto* state = static_cast(context); + ++state->calls; + if (state->text[state->position] == '\0') { + ch = '\0'; + return -1; + } + ch = state->text[state->position++]; + return 0; +} + +static void StreamParse() { + Value root{}; + _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(_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); + _ZN3sce4Json5ValueD1Ev(&root); +} + +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 cleared{}; + _ZN3sce4Json5ValueC1Eb(&cleared, true); + Require(_ZNK3sce4Json5ValuecvbEv(&cleared)); + _ZN3sce4Json5Value5clearEv(&cleared); + Require(!_ZNK3sce4Json5ValuecvbEv(&cleared)); + _ZN3sce4Json5ValueD1Ev(&cleared); +} + int main() { ParseAndRoundTrip(); NestingDepth(); ObjectsAndArrays(); + ObjectFind(); + ObjectPair(); + ObjectInsert(); + ArrayIteratorCopies(); + ObjectIteratorCopies(); + AllocatorNotifyError(); + StringMethods(); + ValueSwap(); + ArrayIteratorAdvance(); + ObjectOperations(); NullAccess(_ZN3sce4Json11Initializer27setGlobalNullAccessCallBackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_); NullAccess(_ZN3sce4Json11Initializer27setGlobalNullAccessCallbackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_); + StaticNullAccess(); + ValueTypeMismatchHandler(); + ValueElementAccessFailureHandler(); + ValueSpecialFloatHandler(); ValueAccess(); + InitParameter2BaseConstructor(); + InitParameters(); ValueClear(); + ValueOperatorBool(); + StreamParse(); } 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 ddb8828a5b..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; @@ -62,7 +92,10 @@ int main() { Require(sceKernelCreateSema(&semaphore, "limit", 1, 1, 3, nullptr) == SCE_OK); Require(sceKernelSignalSema(semaphore, 3) == SCE_KERNEL_ERROR_EINVAL); Require(sceKernelSignalSema(semaphore, 2) == SCE_OK); + Require(sceKernelSignalSema(semaphore, 0) == SCE_OK); Require(sceKernelPollSema(semaphore, 3) == SCE_OK); + Require(sceKernelSignalSema(semaphore, 0) == SCE_OK); + Require(sceKernelPollSema(semaphore, 1) == SCE_KERNEL_ERROR_EBUSY); Require(sceKernelDeleteSema(semaphore) == SCE_OK); KernelEqueue eq = 0; @@ -87,13 +120,7 @@ int main() { Require(scePthreadMutexattrSettype(&attr, 5) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadMutexattrSetprotocol(&attr, PRIO_NONE) == SCE_OK); Require(scePthreadMutexattrSetprotocol(&attr, PRIO_INHERIT) == SCE_OK); - bool protectionRejected = false; - try { - scePthreadMutexattrSetprotocol(&attr, PRIO_PROTECT); - } catch (const std::invalid_argument&) { - protectionRejected = true; - } - Require(protectionRejected); + Require(scePthreadMutexattrSetprotocol(&attr, PRIO_PROTECT) == SCE_OK); PthreadMutex mutex = nullptr; Require(scePthreadMutexInit(&mutex, &attr, nullptr) == SCE_OK); Require(scePthreadMutexattrDestroy(&attr) == SCE_OK); @@ -159,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 new file mode 100644 index 0000000000..b75d3c22a9 --- /dev/null +++ b/core/libs/tests/GuestKernelFcntl.cpp @@ -0,0 +1,517 @@ +#include "prx/libc/include/General.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#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); +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(); } +static constexpr int DuplicateCommand = 0; +static constexpr int GetDescriptorFlags = 1; +static constexpr int SetDescriptorFlags = 2; +static constexpr int GetFlags = 3; +static constexpr int SetFlags = 4; +static constexpr int OldGetLock = 7; +static constexpr int OldSetLock = 8; +static constexpr int OldSetLockWait = 9; +static constexpr int 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; +static constexpr int NonBlocking = 4; +static constexpr int Append = 8; +static constexpr int ReadAheadFlag = 0x200; +static constexpr int CreateReadWrite = 0x202; +static constexpr int CreateWriteAppend = 0x209; +static constexpr int OpenDirectory = 0x20000; +static constexpr std::int16_t ReadLock = 1; +static constexpr std::int16_t Unlock = 2; +static constexpr std::int16_t WriteLock = 3; +static constexpr std::int16_t FromStart = 0; +static constexpr std::int16_t FromCurrent = 1; +static constexpr std::int16_t FromEnd = 2; +static constexpr int GuestEbadf = 9; +static constexpr int GuestEfault = 14; +static constexpr int GuestEinval = 22; +static constexpr int GuestEnotty = 25; +static constexpr int GuestEagain = 35; +static constexpr int GuestEoverflow = 84; +static constexpr std::uint64_t NonblockingRequest = 0x8004667e; +static constexpr std::uint64_t ReadableRequest = 0x4004667f; +static constexpr std::uint64_t WindowSizeRequest = 0x40087468; +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); + +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 void RequireFailure(int result, int error) { + Require(result == -1); + Require(*__error_nid_postfix() == error); +} + +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 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); + Require(sceKernelFcntl(socket, GetFlags) == ReadWrite); + Require(sceKernelFcntl(socket, SetFlags, ReadWrite | NonBlocking) == 0); + 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); + 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() { + const int file = sceKernelOpen("kernel_fcntl_probe.bin", CreateReadWrite, 0644); + Require(file >= 0); + Require(sceKernelFcntl(file, GetFlags) == ReadWrite); + Require(fcntl_nid_postfix(file, SetFlags, ReadWrite | NonBlocking | 0x8000) == 0); + Require(fcntl_nid_postfix(file, GetFlags) == (ReadWrite | NonBlocking)); + Require(fcntl_nid_postfix(file, SetFlags, 0) == 0); + Require(fcntl_nid_postfix(file, GetFlags) == ReadWrite); + Require(fcntl_nid_postfix(file, GetDescriptorFlags) == 0); + Require(fcntl_nid_postfix(file, SetDescriptorFlags, 1) == 0); + Require(fcntl_nid_postfix(file, GetDescriptorFlags) == 1); + Require(sceKernelFcntl(file, SetDescriptorFlags, 0) == 0); + Require(sceKernelFcntl(file, GetDescriptorFlags) == 0); + Require(ioctl_nid_postfix(file, CloseOnExecRequest, nullptr) == 0); + Require(fcntl_nid_postfix(file, GetDescriptorFlags) == 1); + Require(ioctl_nid_postfix(file, KeepOnExecRequest, nullptr) == 0); + Require(fcntl_nid_postfix(file, GetDescriptorFlags) == 0); + Require(fcntl_nid_postfix(file, DarwinReadAhead, 1) == 0); + Require(fcntl_nid_postfix(file, GetFlags) == (ReadWrite | ReadAheadFlag)); + Require(fcntl_nid_postfix(file, ReadAhead, -1) == 0); + Require(fcntl_nid_postfix(file, GetFlags) == ReadWrite); + Require(sceKernelFcntl(file, ReadAhead, 4096) == 0); + Require(fcntl_nid_postfix(file, GetFlags) == (ReadWrite | ReadAheadFlag)); + Require(sceKernelFcntl(file, ReadAhead, -1) == 0); + Require(sceKernelFcntl(file, GetFlags) == ReadWrite); + Require(fcntl_nid_postfix(file, DarwinReadAhead, 1) == 0); + + const int duplicate = fcntl_nid_postfix(file, DuplicateCommand, 20); + Require(duplicate >= 20); + Require(fcntl_nid_postfix(duplicate, GetDescriptorFlags) == 0); + Require(fcntl_nid_postfix(file, SetFlags, NonBlocking) == 0); + Require(fcntl_nid_postfix(duplicate, GetFlags) == (ReadWrite | NonBlocking)); + const int closing = fcntl_nid_postfix(file, DuplicateCloseOnExec, duplicate); + Require(closing > duplicate); + Require(fcntl_nid_postfix(closing, GetDescriptorFlags) == 1); + Require(fcntl_nid_postfix(file, GetDescriptorFlags) == 0); + + Require(write_nid_postfix(file, "abcde", 5) == 5); + Require(sceKernelLseek(file, 2, 0) == 2); + int readable = -1; + 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")); + + RequireFailure(fcntl_nid_postfix(file, DuplicateCommand, -1), GuestEinval); + Require(sceKernelFcntl(file, DuplicateCommand, -1) == SceEinval); + RequireFailure(fcntl_nid_postfix(file, 99), GuestEinval); + RequireFailure(fcntl_nid_postfix(file, GetLock, nullptr), GuestEfault); + GuestLock lock{0, 0, 0, 3, 0, 0}; + 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; + RequireFailure(fcntl_nid_postfix(file, SetLock, &lock), GuestEinval); + + for (const int descriptor : {closing, duplicate, file}) Require(close_nid_postfix(descriptor) == 0); + 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); + Require(appending >= 0); + Require(fcntl_nid_postfix(appending, GetFlags) == (WriteOnly | Append)); + Require(fcntl_nid_postfix(appending, SetFlags, Append | NonBlocking) == 0); + Require(fcntl_nid_postfix(appending, GetFlags) == (WriteOnly | Append | NonBlocking)); +#ifdef _WIN32 + Require(ThrowsWith([appending] { fcntl_nid_postfix(appending, SetFlags, 0); }, "fcntl: changing O_APPEND on a file is not implemented")); +#else + Require(fcntl_nid_postfix(appending, SetFlags, 0) == 0); + Require(fcntl_nid_postfix(appending, GetFlags) == WriteOnly); +#endif + Require(ThrowsWith([appending] { fcntl_nid_postfix(appending, SetFlags, Append | 0x80); }, "fcntl: changing O_FSYNC on a file is not implemented")); + Require(sceKernelClose(appending) == 0); + std::remove("kernel_fcntl_probe.bin"); +} + +static bool NoConflict(int fd, GuestLock request) { + const GuestLock asked = request; + if (fcntl_nid_postfix(fd, GetLock, &request) != 0) return false; + return request.type == Unlock && request.start == asked.start && request.length == asked.length && request.pid == asked.pid && request.whence == asked.whence && request.system == asked.system; +} + +static void RecordLocksBelongToTheProcess() { + const int file = sceKernelOpen("kernel_fcntl_locks.bin", CreateReadWrite, 0644); + Require(file >= 0); + Require(write_nid_postfix(file, "0123456789", 10) == 10); + + GuestLock whole{0, 0, 0, WriteLock, FromStart, 0}; + Require(fcntl_nid_postfix(file, SetLock, &whole) == 0); + Require(NoConflict(file, {4, 2, 0, WriteLock, FromStart, 77})); + GuestLock middle{2, 3, 0, ReadLock, FromStart, 0}; + Require(fcntl_nid_postfix(file, SetLock, &middle) == 0); + GuestLock tail{5, 0, 0, Unlock, FromStart, 0}; + Require(fcntl_nid_postfix(file, SetLock, &tail) == 0); + GuestLock merged{0, 10, 0, WriteLock, FromStart, 0}; + Require(fcntl_nid_postfix(file, SetLockWait, &merged) == 0); + Require(NoConflict(file, {0, 0, 0, WriteLock, FromStart, 0})); + + const int other = sceKernelOpen("kernel_fcntl_locks.bin", ReadWrite, 0); + Require(other >= 0); + GuestLock same{0, 10, 0, WriteLock, FromStart, 0}; + Require(fcntl_nid_postfix(other, SetLock, &same) == 0); + Require(fcntl_nid_postfix(other, SetLockWait, &same) == 0); + Require(NoConflict(other, {0, 0, 0, WriteLock, FromStart, 0})); + + int threadSet = -1; + int threadWait = -1; + bool threadSeesNoConflict = false; + std::thread thread([&] { + GuestLock request{3, 4, 0, WriteLock, FromStart, 0}; + threadSet = fcntl_nid_postfix(other, SetLock, &request); + threadWait = fcntl_nid_postfix(file, SetLockWait, &request); + threadSeesNoConflict = NoConflict(file, {0, 0, 0, WriteLock, FromStart, 0}); + }); + thread.join(); + Require(threadSet == 0 && threadWait == 0 && threadSeesNoConflict); + + Require(sceKernelLseek(file, 6, 0) == 6); + GuestLock current{-2, 0, 0, ReadLock, FromCurrent, 5}; + Require(fcntl_nid_postfix(file, GetLock, ¤t) == 0); + Require(current.type == Unlock && current.start == 4 && current.whence == FromCurrent && current.length == 0 && current.system == 5); + GuestLock end{-3, 0, 0, WriteLock, FromEnd, 0}; + Require(fcntl_nid_postfix(file, SetLock, &end) == 0); + GuestLock backwards{5, -5, 0, ReadLock, FromStart, 0}; + Require(fcntl_nid_postfix(file, SetLock, &backwards) == 0); + + unsigned char old[32]; + std::memset(old, 0xa5, sizeof(old)); + GuestLock oldLock{0, 1, 0, WriteLock, FromStart, 0}; + std::memcpy(old, &oldLock, 24); + Require(fcntl_nid_postfix(file, OldSetLock, old) == 0); + Require(fcntl_nid_postfix(file, OldSetLockWait, old) == 0); + Require(fcntl_nid_postfix(file, OldGetLock, old) == 0); + std::memcpy(&oldLock, old, 24); + Require(oldLock.type == Unlock && oldLock.start == 0 && oldLock.length == 1); + for (std::size_t index = 24; index < sizeof(old); ++index) Require(old[index] == 0xa5); + + GuestLock badType{0, 0, 0, 0, FromStart, 0}; + RequireFailure(fcntl_nid_postfix(file, SetLock, &badType), GuestEinval); + badType.type = 4; + RequireFailure(fcntl_nid_postfix(file, SetLock, &badType), GuestEinval); + badType.type = 7; + RequireFailure(fcntl_nid_postfix(file, GetLock, &badType), GuestEinval); + GuestLock negative{-1, 0, 0, WriteLock, FromStart, 0}; + RequireFailure(fcntl_nid_postfix(file, SetLock, &negative), GuestEinval); + GuestLock beforeStart{0, -1, 0, WriteLock, FromStart, 0}; + RequireFailure(fcntl_nid_postfix(file, SetLock, &beforeStart), GuestEinval); + GuestLock pastStart{5, -6, 0, WriteLock, FromStart, 0}; + RequireFailure(fcntl_nid_postfix(file, SetLock, &pastStart), GuestEinval); + GuestLock unknownWhence{0, 0, 0, WriteLock, 3, 0}; + RequireFailure(fcntl_nid_postfix(file, SetLock, &unknownWhence), GuestEinval); + Require(sceKernelFcntl(file, SetLock, &unknownWhence) == SceEinval); + + const int reader = sceKernelOpen("kernel_fcntl_locks.bin", ReadOnly, 0); + Require(reader >= 0); + GuestLock write{0, 0, 0, WriteLock, FromStart, 0}; + RequireFailure(fcntl_nid_postfix(reader, SetLock, &write), GuestEbadf); + RequireFailure(fcntl_nid_postfix(reader, SetLockWait, &write), GuestEbadf); + Require(sceKernelFcntl(reader, SetLock, &write) == SceEbadf); + Require(NoConflict(reader, write)); + GuestLock read{0, 0, 0, ReadLock, FromStart, 0}; + Require(fcntl_nid_postfix(reader, SetLock, &read) == 0); + const int writer = sceKernelOpen("kernel_fcntl_locks.bin", WriteOnly, 0); + Require(writer >= 0); + RequireFailure(fcntl_nid_postfix(writer, SetLock, &read), GuestEbadf); + Require(fcntl_nid_postfix(writer, SetLock, &write) == 0); + GuestLock release{0, 0, 0, Unlock, FromStart, 0}; + Require(fcntl_nid_postfix(writer, SetLock, &release) == 0); + Require(fcntl_nid_postfix(reader, SetLock, &release) == 0); + + 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); + GuestLock pastEnd{std::numeric_limits::max(), 0, 0, WriteLock, FromEnd, 0}; + RequireFailure(fcntl_nid_postfix(file, SetLock, &pastEnd), GuestEoverflow); + GuestLock pastCurrent{std::numeric_limits::max(), 0, 0, ReadLock, FromCurrent, 0}; + Require(fcntl_nid_postfix(reader, SetLock, &pastCurrent) == 0); + pastCurrent.type = WriteLock; + Require(sceKernelLseek(reader, 1, 0) == 1); + RequireFailure(fcntl_nid_postfix(reader, SetLock, &pastCurrent), GuestEoverflow); + GuestLock writeBeforeWhence{0, 0, 0, WriteLock, 3, 0}; + RequireFailure(fcntl_nid_postfix(reader, SetLock, &writeBeforeWhence), GuestEbadf); + RequireFailure(fcntl_nid_postfix(reader, GetLock, &writeBeforeWhence), GuestEinval); + + std::filesystem::create_directory("kernel_fcntl_locks_dir"); + const int directory = sceKernelOpen("kernel_fcntl_locks_dir", OpenDirectory, 0); + Require(directory >= 0); + Require(fcntl_nid_postfix(directory, SetLock, &read) == 0); + RequireFailure(fcntl_nid_postfix(directory, SetLock, &write), GuestEbadf); + Require(NoConflict(directory, {0, 0, 0, WriteLock, FromCurrent, 0})); + GuestLock directoryEnd{0, 0, 0, ReadLock, FromEnd, 0}; + Require(ThrowsWith([directory, &directoryEnd] { fcntl_nid_postfix(directory, SetLock, &directoryEnd); }, "fcntl: a record lock from the end on a directory is not implemented")); + Require(close_nid_postfix(directory) == 0); + std::filesystem::remove("kernel_fcntl_locks_dir"); +#endif + + for (const int descriptor : {writer, reader, other}) Require(close_nid_postfix(descriptor) == 0); + Require(fcntl_nid_postfix(file, SetLock, &whole) == 0); + Require(NoConflict(file, {0, 0, 0, WriteLock, FromStart, 0})); + Require(close_nid_postfix(file) == 0); + RequireFailure(fcntl_nid_postfix(file, SetLock, &whole), GuestEbadf); + RequireFailure(fcntl_nid_postfix(file, GetLock, &whole), GuestEbadf); + std::remove("kernel_fcntl_locks.bin"); +} + +static void PipeFlagsRoundTrip() { + int pipe[2]; + Require(pipe_nid_postfix(pipe) == 0); + Require(fcntl_nid_postfix(pipe[0], GetFlags) == ReadOnly); + Require(fcntl_nid_postfix(pipe[1], GetFlags) == WriteOnly); + Require(fcntl_nid_postfix(pipe[1], SetDescriptorFlags, 1) == 0); + Require(fcntl_nid_postfix(pipe[1], GetDescriptorFlags) == 1); + Require(fcntl_nid_postfix(pipe[0], GetDescriptorFlags) == 0); + Require(ioctl_nid_postfix(pipe[0], CloseOnExecRequest, nullptr) == 0); + Require(fcntl_nid_postfix(pipe[0], GetDescriptorFlags) == 1); + Require(ioctl_nid_postfix(pipe[1], KeepOnExecRequest, nullptr) == 0); + Require(fcntl_nid_postfix(pipe[1], GetDescriptorFlags) == 0); + + Require(fcntl_nid_postfix(pipe[0], SetFlags, fcntl_nid_postfix(pipe[0], GetFlags) | NonBlocking) == 0); + Require(fcntl_nid_postfix(pipe[0], GetFlags) == (ReadOnly | NonBlocking)); + char buffer[8] = {}; + RequireFailure(static_cast(read_nid_postfix(pipe[0], buffer, sizeof(buffer))), GuestEagain); + Require(write_nid_postfix(pipe[1], "abc", 3) == 3); + int readable = -1; + Require(ioctl_nid_postfix(pipe[0], ReadableRequest, &readable) == 0 && readable == 3); + Require(ioctl_nid_postfix(pipe[1], ReadableRequest, &readable) == 0 && readable == 0); + Require(read_nid_postfix(pipe[0], buffer, sizeof(buffer)) == 3); + RequireFailure(static_cast(read_nid_postfix(pipe[0], buffer, sizeof(buffer))), GuestEagain); + + int off = 0; + Require(ioctl_nid_postfix(pipe[0], NonblockingRequest, &off) == 0); + Require(fcntl_nid_postfix(pipe[0], GetFlags) == ReadOnly); + int on = 1; + Require(ioctl_nid_postfix(pipe[0], NonblockingRequest, &on) == 0); + Require(fcntl_nid_postfix(pipe[0], GetFlags) == (ReadOnly | NonBlocking)); + + Require(fcntl_nid_postfix(pipe[1], SetFlags, NonBlocking) == 0); + char block[1024] = {}; + std::int64_t written = 0; + std::int64_t result = 0; + while (written < (1 << 22) && (result = write_nid_postfix(pipe[1], block, sizeof(block))) > 0) written += result; + Require(written > 0); + RequireFailure(static_cast(result), GuestEagain); + + RequireFailure(fcntl_nid_postfix(pipe[0], DarwinReadAhead, 1), GuestEbadf); + GuestLock lock{0, 0, 0, 3, 0, 0}; + 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); + Require((flags & 3) != WriteOnly); + Require(fcntl_nid_postfix(0, SetFlags, fcntl_nid_postfix(0, GetFlags) & ~NonBlocking) == 0); + Require(fcntl_nid_postfix(0, GetFlags) == (flags & ~NonBlocking)); + Require(fcntl_nid_postfix(0, SetDescriptorFlags, 1) == 0); + Require(fcntl_nid_postfix(0, GetDescriptorFlags) == 1); + Require(fcntl_nid_postfix(0, SetDescriptorFlags, 0) == 0); + Require(fcntl_nid_postfix(0, GetDescriptorFlags) == 0); +} + +int main() { + SocketsKeepTheirPath(); + 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 b2c1097d2a..968cdc981f 100644 --- a/core/libs/tests/GuestKernelIsStack.cpp +++ b/core/libs/tests/GuestKernelIsStack.cpp @@ -9,9 +9,15 @@ int APS5_VABI scePthreadJoin(Pthread thread, void** retval); int APS5_VABI sceKernelIsStack(void* addr, void** start, void** end); int APS5_VABI sceKernelMapFlexibleMemory(void** addr, std::size_t len, int prot, int flags); int APS5_VABI sceKernelMunmap(std::uint64_t addr, std::size_t len); +int APS5_VABI sceKernelVirtualQuery(const void* addr, int flags, VirtualQueryInfo* info, std::uint64_t infoSize); } static constexpr int SCE_OK = 0; +#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; @@ -24,6 +30,8 @@ struct WorkerContext { void* start = nullptr; void* end = nullptr; int result = -1; + VirtualQueryInfo region{}; + int regionResult = -1; }; static void* APS5_VABI Worker(void* arg) { @@ -31,6 +39,7 @@ static void* APS5_VABI Worker(void* arg) { int local = 0; context.local = &local; context.result = sceKernelIsStack(&local, &context.start, &context.end); + context.regionResult = sceKernelVirtualQuery(&local, 0, &context.region, sizeof(context.region)); context.queried.store(true); while (!context.release.load()) {} return nullptr; @@ -55,6 +64,11 @@ int main() { while (!context.queried.load()) {} Require(context.result == SCE_OK); Require(context.start <= context.local && context.local < context.end); + Require(context.regionResult == SCE_OK); + Require(context.region.start <= reinterpret_cast(context.local) && reinterpret_cast(context.local) < context.region.end); +#if defined(__linux__) + Require(reinterpret_cast(context.local) < APPLICATION_AREA_END); +#endif Require(sceKernelIsStack(&local, &start, &end) == SCE_OK); Require(context.end <= start || context.start >= end); void* workerStart = nullptr; diff --git a/core/libs/tests/GuestKernelStackProtect.cpp b/core/libs/tests/GuestKernelStackProtect.cpp new file mode 100644 index 0000000000..a980ea9d09 --- /dev/null +++ b/core/libs/tests/GuestKernelStackProtect.cpp @@ -0,0 +1,558 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#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; +static constexpr std::uintptr_t Page = 0x4000; +static constexpr std::size_t GuardBytes = 0x10000; +static constexpr std::size_t HostBytes = 0x40000; + +#define Require(value) do { if (!(value)) { std::fprintf(stderr, "GuestKernelStackProtect failed at line %d\n", __LINE__); std::abort(); } } while (false) + +struct PageState { + bool mapped; + bool writable; + std::uint64_t raw; +}; + +static PageState QueryPage(std::uintptr_t address) { +#ifdef _WIN32 + MEMORY_BASIC_INFORMATION memory{}; + Require(VirtualQuery(reinterpret_cast(address), &memory, sizeof(memory)) == sizeof(memory)); + const bool mapped = memory.State == MEM_COMMIT; + return {mapped, mapped && (memory.Protect & (PAGE_READWRITE | PAGE_EXECUTE_READWRITE)) != 0, (static_cast(memory.State) << 32) | memory.Protect}; +#else + std::FILE* maps = std::fopen("/proc/self/maps", "r"); + Require(maps != nullptr); + PageState state{false, false, 0}; + char line[512]; + while (std::fgets(line, sizeof(line), maps) != nullptr) { + unsigned long long low = 0; + unsigned long long high = 0; + char permissions[5] = {}; + if (std::sscanf(line, "%llx-%llx %4s", &low, &high, permissions) != 3 || address < low || address >= high) continue; + state = {true, permissions[0] == 'r' && permissions[1] == 'w', 1ull | (permissions[0] == 'r' ? 2ull : 0) | (permissions[1] == 'w' ? 4ull : 0) | (permissions[2] == 'x' ? 8ull : 0)}; + break; + } + std::fclose(maps); + return state; +#endif +} + +static void* MapHostPages() { +#ifdef _WIN32 + return VirtualAlloc(nullptr, HostBytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); +#else + void* mapped = mmap(nullptr, HostBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + return mapped == MAP_FAILED ? nullptr : mapped; +#endif +} + +static void UnmapHostPages(void* pages) { +#ifdef _WIN32 + Require(VirtualFree(pages, 0, MEM_RELEASE) != 0); +#else + Require(munmap(pages, HostBytes) == 0); +#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; + void* start = nullptr; + void* end = nullptr; + Require(sceKernelIsStack(&local, &start, &end) == SCE_OK); + const auto low = (reinterpret_cast(start) + Page - 1) & ~(Page - 1); + Require(low + GuardBytes + Page < reinterpret_cast(&local)); +#ifdef __linux__ + std::uintptr_t hostGuard = 0; + for (auto cursor = low; cursor >= Page && low - cursor <= 4 * 1024 * 1024; cursor -= Page) { + const auto page = QueryPage(cursor); + if (!page.mapped) break; + if (!page.writable) { hostGuard = cursor; break; } + } + Require(hostGuard != 0); + const auto hostGuardBefore = QueryPage(hostGuard).raw; + Require(mprotect_nid_postfix(reinterpret_cast(hostGuard), Page, 3) == -1); + Require(QueryPage(hostGuard).raw == hostGuardBefore); +#endif + auto* guard = reinterpret_cast(low); + Require(mprotect_nid_postfix(const_cast(guard), GuardBytes, 0) == 0); +#ifdef _WIN32 + MEMORY_BASIC_INFORMATION memory{}; + Require(VirtualQuery(const_cast(guard), &memory, sizeof(memory)) == sizeof(memory)); + Require(memory.Protect == PAGE_NOACCESS && memory.RegionSize >= GuardBytes); +#endif + Require(mprotect_nid_postfix(const_cast(guard), GuardBytes, 3) == 0); + guard[0] = 1; + guard[GuardBytes - 1] = 2; + Require(guard[0] == 1 && guard[GuardBytes - 1] == 2); + const auto below = reinterpret_cast(start) - 1; + const auto belowBefore = QueryPage(below).raw; + Require(mprotect_nid_postfix(start, GuardBytes, 0) == 0); + Require(QueryPage(below).raw == belowBefore); + Require(mprotect_nid_postfix(start, GuardBytes, 3) == 0); + Require(QueryPage(below).raw == belowBefore); + static_cast(start)[0] = 3; + const auto outside = reinterpret_cast(start) & ~(Page - 1); + if (outside >= Page) Require(mprotect_nid_postfix(reinterpret_cast(outside - Page), 2 * Page, 3) == -1); + if (outside >= 2 * Page) Require(mprotect_nid_postfix(reinterpret_cast(outside - 2 * Page), Page, 3) == -1); + const auto stackBytes = reinterpret_cast(end) - reinterpret_cast(start); + Require(mprotect_nid_postfix(start, stackBytes, 0xc2) == 0); + 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); + Require(QueryPage(reinterpret_cast(host)).writable); + static_cast(host)[HostBytes - 1] = 5; + UnmapHostPages(host); + passed.store(true); + return nullptr; +} + +enum class GuardMode { Protect, Remap, Split }; + +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(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; +} + +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()); + CheckGuardCleanup(GuardMode::Protect); +#ifdef _WIN32 + CheckPeerStackRemap(); + CheckGuardCleanup(GuardMode::Remap); + CheckGuardCleanup(GuardMode::Split); +#endif +} diff --git a/core/libs/tests/GuestKernelSync.cpp b/core/libs/tests/GuestKernelSync.cpp index 526218a991..fbe8391437 100644 --- a/core/libs/tests/GuestKernelSync.cpp +++ b/core/libs/tests/GuestKernelSync.cpp @@ -21,6 +21,7 @@ int APS5_VABI sceKernelRename(const char*, const char*); int APS5_VABI sceKernelUnlink(const char*); int APS5_VABI sceKernelChmod_nid_postfix(const char*, std::uint16_t); void APS5_VABI sceKernelSync(); +void APS5_VABI sync_nid_postfix(); void* APS5_VABI fopen_nid_postfix(const char*, const char*); int APS5_VABI fclose_nid_postfix(void*); } @@ -37,9 +38,16 @@ static bool Recorded(const char* guest) { } int main() { std::filesystem::remove_all("kernel_sync_probe"); + Require(sceKernelMkdir("", 0777) == static_cast(0x80020002u)); + Require(sceKernelMkdir("/", 0777) == static_cast(0x80020011u)); Require(sceKernelMkdir("kernel_sync_probe", 0777) == 0); Require(Recorded("kernel_sync_probe")); std::ofstream("kernel_sync_probe/existing.bin") << "existing"; + Require(sceKernelMkdir("kernel_sync_probe/nested/", 0777) == 0); + Require(std::filesystem::is_directory("kernel_sync_probe/nested") && Recorded("kernel_sync_probe/nested")); + Require(sceKernelMkdir("kernel_sync_probe/nested/", 0777) == static_cast(0x80020011u)); + Require(sceKernelMkdir("kernel_sync_probe/./", 0777) == static_cast(0x80020011u)); + Require(sceKernelMkdir("kernel_sync_probe/existing.bin/child", 0777) == static_cast(0x80020014u)); const int reader = sceKernelOpen("kernel_sync_probe/existing.bin", 0x0, 0); Require(reader >= 0 && sceKernelClose(reader) == 0); void* stream = fopen_nid_postfix("kernel_sync_probe/existing.bin", "r"); @@ -71,5 +79,11 @@ int main() { CloseHandle(exclusive); #endif sceKernelSync(); + const int synced = sceKernelOpen("kernel_sync_probe/synced.bin", 0x1 | 0x200 | 0x400, 0644); + Require(synced >= 0 && sceKernelWrite(synced, "data", 4) == 4 && sceKernelClose(synced) == 0); + Require(Recorded("kernel_sync_probe/synced.bin")); + Require(sceKernelUnlink("kernel_sync_probe/synced.bin") == 0); + sync_nid_postfix(); + Require(!Recorded("kernel_sync_probe/synced.bin")); std::filesystem::remove_all("kernel_sync_probe"); } diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 6b523f4711..a60abca020 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -1,4 +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 @@ -7,10 +12,53 @@ #include #include #include +#include +#include #include +#ifdef _WIN32 +#include +#ifndef SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE +#define SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE 0x2 +#endif +#endif +#ifndef _WIN32 +#include +#include +#endif extern "C" { +int APS5_VABI fstatfs_nid_postfix(int, void*); +int APS5_VABI _fstatfs_nid_postfix(int, void*); +int APS5_VABI open_nid_postfix(const char*, int, int); +int APS5_VABI close_nid_postfix(int); std::int64_t APS5_VABI readlink_nid_postfix(const char*, char*, std::size_t); +std::int64_t APS5_VABI pathconf_nid_postfix(const char*, int); +int APS5_VABI fchdir_nid_postfix(int); +int APS5_VABI chdir_nid_postfix(const char*); +int APS5_VABI chroot_nid_postfix(const char*); +char* APS5_VABI getcwd_nid_postfix(char*, std::size_t); +int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); +int APS5_VABI sceKernelClose(int); +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*); +int APS5_VABI openat_nid_postfix(int, const char*, int, ...); +int APS5_VABI unlinkat_nid_postfix(int, const char*, int); +std::int64_t APS5_VABI readlinkat_nid_postfix(int, const char*, char*, std::size_t); +int APS5_VABI mkdirat_nid_postfix(int, const char*, std::uint16_t); +int APS5_VABI renameat_nid_postfix(int, const char*, int, const char*); +int APS5_VABI fchmodat_nid_postfix(int, const char*, int, int); +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(); } @@ -25,18 +73,244 @@ int Error() { return *__error_nid_postfix(); } +bool PathconfThrows(const char* path, int name) { + try { + pathconf_nid_postfix(path, name); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +bool ChrootThrows(const std::string& path) { + try { + chroot_nid_postfix(path.c_str()); + } catch (const std::runtime_error& error) { + return std::strstr(error.what(), path.c_str()) != nullptr; + } + return false; +} + +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}) { + Require(pathconf_nid_postfix(path.c_str(), 1) == 32767); + Require(pathconf_nid_postfix(path.c_str(), 4) == 255); + Require(pathconf_nid_postfix(path.c_str(), 5) == 1024); + Require(pathconf_nid_postfix(path.c_str(), 6) == 512); + Require(pathconf_nid_postfix(path.c_str(), 7) == 1); + Require(pathconf_nid_postfix(path.c_str(), 8) == 1); + Require(pathconf_nid_postfix(path.c_str(), 12) == 64); + *__error_nid_postfix() = 0; + Require(pathconf_nid_postfix(path.c_str(), 15) == -1 && Error() == 0); + Require(pathconf_nid_postfix(path.c_str(), 18) == 1024); + Require(pathconf_nid_postfix(path.c_str(), 54) == 0); + Require(pathconf_nid_postfix(path.c_str(), 55) == 0); + } + for (const int name : {0, 11, 61, 62, 65, -1}) + Require(pathconf_nid_postfix(file.c_str(), name) == -1 && Error() == 22); + for (const int name : {2, 3, 9, 10, 14, 16, 17, 21, 53, 59, 60, 63, 64}) + Require(PathconfThrows(file.c_str(), name)); + const auto missing = (root / "missing").string(); + Require(pathconf_nid_postfix(missing.c_str(), 4) == -1 && Error() == 2); + Require(pathconf_nid_postfix(missing.c_str(), 0) == -1 && Error() == 2); + Require(pathconf_nid_postfix((root / "missing" / "file").string().c_str(), 4) == -1 && Error() == 2); + Require(pathconf_nid_postfix("", 4) == -1 && Error() == 2); + Require(pathconf_nid_postfix(nullptr, 4) == -1 && Error() == 14); + const auto longName = directory + "/" + std::string(256, 'n'); + Require(pathconf_nid_postfix(longName.c_str(), 4) == -1 && Error() == 63); + Require(pathconf_nid_postfix((directory + "/missing/" + std::string(256, 'n')).c_str(), 4) == -1 && Error() == 2); + const auto longPath = directory + "/" + std::string(1024, 'p'); + Require(pathconf_nid_postfix(longPath.c_str(), 4) == -1 && Error() == 63); +} + +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); + Require(umask_nid_postfix(022) == 07777); 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); + const int descriptor = open_nid_postfix(file.c_str(), 0, 0); + Require(descriptor >= 0); + unsigned char viaPublic[472] = {}; + unsigned char viaSyscall[472] = {}; + Require(fstatfs_nid_postfix(descriptor, viaPublic) == 0 && _fstatfs_nid_postfix(descriptor, viaSyscall) == 0); + 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); + Require(close_nid_postfix(descriptor) == 0); + Require(fstatfs_nid_postfix(-1, viaPublic) == -1 && Error() == 9); char buffer[64]; + for (const auto& component : {std::string("missing"), std::string("file.txt")}) { + const auto prefix = root.generic_string() + "/" + component + "/../"; + const auto invalid = prefix + "file.txt"; + const auto created = prefix + "new.txt"; + const int expected = component == "missing" ? 2 : 20; + Require(readlink_nid_postfix(invalid.c_str(), buffer, sizeof(buffer)) == -1 && Error() == expected); + Require(link_nid_postfix(invalid.c_str(), created.c_str()) == -1 && Error() == expected); + Require(link_nid_postfix(file.c_str(), created.c_str()) == -1 && Error() == expected); + 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")); + } 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); +#endif std::error_code error; const auto link = (root / "link").string(); std::filesystem::create_symlink("sub/target.txt", link, error); @@ -50,9 +324,381 @@ int main() { Require(readlink_nid_postfix(link.c_str(), nullptr, 0) == 0); Require(readlink_nid_postfix(link.c_str(), nullptr, sizeof(buffer)) == -1 && Error() == 14); Require(readlink_nid_postfix((root / "LINK").string().c_str(), buffer, sizeof(buffer)) == 14); + const int linkDir = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(linkDir >= 0); + std::memset(buffer, '#', sizeof(buffer)); + Require(readlinkat_nid_postfix(linkDir, "link", buffer, sizeof(buffer)) == 14); + Require(std::memcmp(buffer, "sub/target.txt", 14) == 0 && buffer[14] == '#'); + Require(readlinkat_nid_postfix(linkDir, "link", buffer, 3) == 3 && std::memcmp(buffer, "sub", 3) == 0); + Require(readlinkat_nid_postfix(linkDir, "link", nullptr, 0) == 0); + Require(readlinkat_nid_postfix(linkDir, "link", nullptr, sizeof(buffer)) == -1 && Error() == 14); + const auto guestLink = "/" + root.filename().string() + "/link"; + Require(readlinkat_nid_postfix(-1, guestLink.c_str(), buffer, sizeof(buffer)) == 14 && std::memcmp(buffer, "sub/target.txt", 14) == 0); + Require(readlinkat_nid_postfix(-100, link.c_str(), buffer, sizeof(buffer)) == 14 && std::memcmp(buffer, "sub/target.txt", 14) == 0); + Require(sceKernelClose(linkDir) == 0); +#ifndef _WIN32 + std::filesystem::create_symlink("loopB", root / "loopA"); + std::filesystem::create_symlink("loopA", root / "loopB"); + Require(readlink_nid_postfix((root / "loopA" / "inner").string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 62); +#endif } else { std::fprintf(stderr, "symbolic links unavailable: the link cases not tested\n"); } - std::filesystem::remove_all(root); + const auto original = (root / "original.bin").string(); + const auto linked = (root / "linked.bin").string(); + std::ofstream(original) << "abc"; + Require(link_nid_postfix(original.c_str(), linked.c_str()) == 0); + Require(std::filesystem::hard_link_count(original) == 2 && std::filesystem::file_size(linked) == 3); + std::ofstream(linked, std::ios::trunc).close(); + Require(std::filesystem::file_size(original) == 0); + Require(link_nid_postfix(original.c_str(), linked.c_str()) == -1 && Error() == 17); + Require(link_nid_postfix((root / "missing").string().c_str(), (root / "other").string().c_str()) == -1 && Error() == 2); + Require(link_nid_postfix(original.c_str(), (root / "missing" / "linked.bin").string().c_str()) == -1 && Error() == 2); + Require(link_nid_postfix(root.string().c_str(), (root / "directory").string().c_str()) == -1 && Error() == 1); + Require(link_nid_postfix(nullptr, linked.c_str()) == -1 && Error() == 14); + Require(link_nid_postfix(original.c_str(), "") == -1 && Error() == 2); + Require(std::filesystem::remove(linked) && std::filesystem::hard_link_count(original) == 1); + const int directory = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(directory >= 0); + const int fileDescriptor = sceKernelOpen(file.c_str(), SCE_KERNEL_O_RDONLY, 0); + Require(fileDescriptor >= 0); + Require(fchdir_nid_postfix(-1) == -1 && Error() == 9); + Require(fchdir_nid_postfix(fileDescriptor) == -1 && Error() == 20); + 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); +#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); + const bool searchDenied = ::access(root.c_str(), X_OK) == -1 && errno == EACCES; + const int deniedResult = searchDenied ? fchdir_nid_postfix(directory) : 0; + const int deniedError = Error(); + const bool cwdUnchanged = getcwd_nid_postfix(directoryPath, sizeof(directoryPath)) == directoryPath && std::strcmp(directoryPath, "/") == 0; + std::filesystem::permissions(root, std::filesystem::perms::owner_exec); + const int searchOnlyResult = fchdir_nid_postfix(directory); + std::filesystem::permissions(root, originalPermissions); + if (searchDenied) Require(deniedResult == -1 && deniedError == EACCES && cwdUnchanged); + Require(searchOnlyResult == 0); +#endif + Require(sceKernelClose(fileDescriptor) == 0); + Require(sceKernelClose(directory) == 0); + Require(fchdir_nid_postfix(directory) == -1 && Error() == 9); + Require(chdir_nid_postfix("/") == 0); + Require(chroot_nid_postfix(nullptr) == -1 && Error() == 14); + Require(chroot_nid_postfix("") == -1 && Error() == 2); + Require(chroot_nid_postfix((root / "missing").string().c_str()) == -1 && Error() == 2); + Require(chroot_nid_postfix(file.c_str()) == -1 && Error() == 20); + Require(chroot_nid_postfix((root / "file.txt" / "child").string().c_str()) == -1 && Error() == 20); + Require(chroot_nid_postfix((file + "/").c_str()) == -1 && Error() == 20); + Require(chroot_nid_postfix((root / "file.txt" / "..").string().c_str()) == -1 && Error() == 20); + Require(chroot_nid_postfix((root / std::string(256, 'n')).string().c_str()) == -1 && Error() == 63); + Require(chroot_nid_postfix(("/" + std::string(1024, 'p')).c_str()) == -1 && Error() == 63); + Require(ChrootThrows(root.string())); + Require(ChrootThrows("/" + root.filename().string() + "/.")); + const auto guestRoot = "/" + root.filename().string(); + const auto chrootFile = guestRoot + "/file.txt"; + const int top = sceKernelOpen("/", SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(top >= 0); + Require(chdir_nid_postfix(guestRoot.c_str()) == 0); + Require(fchdir_nid_postfix(top) == 0 && chroot_nid_postfix(".") == 0); + Require(getcwd_nid_postfix(directoryPath, sizeof(directoryPath)) == directoryPath && std::strcmp(directoryPath, "/") == 0); + Require(sceKernelClose(top) == 0); + Require(chroot_nid_postfix("/") == 0 && chroot_nid_postfix("/..") == 0 && chroot_nid_postfix("//./") == 0); + Require(chdir_nid_postfix(guestRoot.c_str()) == 0); + Require(chroot_nid_postfix("..") == 0 && chroot_nid_postfix("../..") == 0); + Require(getcwd_nid_postfix(directoryPath, sizeof(directoryPath)) == directoryPath && guestRoot == directoryPath); + Require(ChrootThrows(".")); + for (const auto& path : {std::string("file.txt"), chrootFile, "/../../.." + chrootFile, "../.." + chrootFile}) { + const int opened = sceKernelOpen(path.c_str(), SCE_KERNEL_O_RDONLY, 0); + char byte = 0; + Require(opened >= 0 && sceKernelRead(opened, &byte, 1) == 1 && byte == 'x'); + Require(sceKernelClose(opened) == 0); + } + Require(chdir_nid_postfix("/") == 0); + Require(getcwd_nid_postfix(directoryPath, sizeof(directoryPath)) == directoryPath && std::strcmp(directoryPath, "/") == 0); + Require(readlink_nid_postfix(file.c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); + const auto symbolic = (root / "symbolic").string(); + const int created = symlink_nid_postfix("original.bin", symbolic.c_str()); + if (created == 0) { + const auto dangling = (root / "dangling").string(); +#ifdef _WIN32 + Require((GetFileAttributesW(std::filesystem::path(symbolic).wstring().c_str()) & FILE_ATTRIBUTE_REPARSE_POINT) != 0); + Require(symlink_nid_postfix("original.bin", symbolic.c_str()) == -1 && Error() == 17); + Require(symlink_nid_postfix("missing.bin", dangling.c_str()) == 0); + Require((GetFileAttributesW(std::filesystem::path(dangling).wstring().c_str()) & FILE_ATTRIBUTE_REPARSE_POINT) != 0); + Require(symlink_nid_postfix("missing.bin", dangling.c_str()) == -1 && Error() == 17); + Require(readlink_nid_postfix(symbolic.c_str(), buffer, sizeof(buffer)) == 12 && std::memcmp(buffer, "original.bin", 12) == 0); + Require(readlink_nid_postfix(dangling.c_str(), buffer, sizeof(buffer)) == 11 && std::memcmp(buffer, "missing.bin", 11) == 0); + const auto nested = (root / "nested").string(); + Require(symlink_nid_postfix("sub/target.txt", nested.c_str()) == 0); + std::memset(buffer, '#', sizeof(buffer)); + Require(readlink_nid_postfix(nested.c_str(), buffer, sizeof(buffer)) == 14); + Require(std::memcmp(buffer, "sub/target.txt", 14) == 0 && buffer[14] == '#'); + Require(readlink_nid_postfix(nested.c_str(), buffer, 3) == 3 && std::memcmp(buffer, "sub", 3) == 0); + Require(readlink_nid_postfix(nested.c_str(), nullptr, 0) == 0); + Require(readlink_nid_postfix(nested.c_str(), nullptr, sizeof(buffer)) == -1 && Error() == 14); + const auto accented = (root / "accented").string(); + Require(symlink_nid_postfix("a\xc3\xa7\xc3\xa3o.bin", accented.c_str()) == 0); + Require(readlink_nid_postfix(accented.c_str(), buffer, sizeof(buffer)) == 10 && std::memcmp(buffer, "a\xc3\xa7\xc3\xa3o.bin", 10) == 0); + Require(std::filesystem::create_directory(root / "sub")); + const auto directoryLink = (root / "directory-link").string(); + Require(symlink_nid_postfix("sub", directoryLink.c_str()) == 0); + Require(readlink_nid_postfix(directoryLink.c_str(), buffer, sizeof(buffer)) == 3 && std::memcmp(buffer, "sub", 3) == 0); + 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); + 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); + Require(std::filesystem::file_size(symbolic) == std::filesystem::file_size(original)); + Require(symlink_nid_postfix("original.bin", symbolic.c_str()) == -1 && Error() == 17); + Require(symlink_nid_postfix("missing.bin", dangling.c_str()) == 0); + Require(readlink_nid_postfix(dangling.c_str(), buffer, sizeof(buffer)) == 11 && !std::filesystem::exists(dangling)); + Require(symlink_nid_postfix("missing.bin", dangling.c_str()) == -1 && Error() == 17); +#endif + } else { + Require(created == -1 && Error() == 1); + std::fprintf(stderr, "symbolic links unavailable: the symlink success cases not tested\n"); + } + 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); + char byte = 0; + Require(relative >= 0 && sceKernelRead(relative, &byte, 1) == 1 && byte == 'x'); + Require(sceKernelClose(relative) == 0); + const int made = openat_nid_postfix(base, "made.bin", SCE_KERNEL_O_WRONLY | SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL, 0644); + Require(made >= 0 && std::filesystem::is_regular_file(root / "made.bin")); + 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); + const int current = openat_nid_postfix(-100, file.c_str(), SCE_KERNEL_O_RDONLY); + Require(current >= 0 && sceKernelClose(current) == 0); + const int plain = sceKernelOpen(file.c_str(), SCE_KERNEL_O_RDONLY, 0); + Require(plain >= 0); + Require(openat_nid_postfix(plain, "file.txt", SCE_KERNEL_O_RDONLY) == -1 && Error() == 20); + Require(readlinkat_nid_postfix(base, "file.txt", buffer, sizeof(buffer)) == -1 && Error() == 22); + Require(readlinkat_nid_postfix(base, "missing.bin", buffer, sizeof(buffer)) == -1 && Error() == 2); + Require(readlinkat_nid_postfix(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); + Require(readlinkat_nid_postfix(base, "", buffer, sizeof(buffer)) == -1 && Error() == 2); + Require(readlinkat_nid_postfix(-1, guestFile.c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); + Require(readlinkat_nid_postfix(-100, file.c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); + Require(mkdirat_nid_postfix(base, "dir", 0700) == 0 && std::filesystem::is_directory(root / "dir")); + Require(mkdirat_nid_postfix(base, "dir", 0700) == -1 && Error() == 17); + Require(mkdirat_nid_postfix(base, "file.txt", 0700) == -1 && Error() == 17); + Require(mkdirat_nid_postfix(base, "missing/child", 0700) == -1 && Error() == 2); + Require(mkdirat_nid_postfix(base, "file.txt/child", 0700) == -1 && Error() == 20); + Require(mkdirat_nid_postfix(base, "slash_dir/", 0700) == 0 && std::filesystem::is_directory(root / "slash_dir")); + const auto guestSubdir = "/" + root.filename().string() + "/abs_dir"; + Require(mkdirat_nid_postfix(-1, guestSubdir.c_str(), 0700) == 0 && std::filesystem::is_directory(root / "abs_dir")); + const auto cwdDir = (root / "cwd_dir").string(); + Require(mkdirat_nid_postfix(-100, cwdDir.c_str(), 0700) == 0 && std::filesystem::is_directory(root / "cwd_dir")); + Require(mkdirat_nid_postfix(plain, "child", 0700) == -1 && Error() == 20); + Require(mkdirat_nid_postfix(-1, "child", 0700) == -1 && Error() == 9); + Require(mkdirat_nid_postfix(base, nullptr, 0700) == -1 && Error() == 14); + Require(mkdirat_nid_postfix(base, "", 0700) == -1 && Error() == 2); + const std::string longName(256, 'a'); + Require(mkdirat_nid_postfix(base, longName.c_str(), 0700) == -1 && Error() == 63); + Require(mkdirat_nid_postfix(base, "dir/../dotdot_dir", 0700) == 0 && std::filesystem::is_directory(root / "dotdot_dir")); + Require(unlinkat_nid_postfix(plain, "file.txt", 0) == -1 && Error() == 20); + Require(unlinkat_nid_postfix(-1, "file.txt", 0) == -1 && Error() == 9); + Require(sceKernelClose(plain) == 0); + Require(unlinkat_nid_postfix(base, "file.txt", 1) == -1 && Error() == 22); + Require(unlinkat_nid_postfix(base, nullptr, 1) == -1 && Error() == 22); + Require(unlinkat_nid_postfix(base, nullptr, 0) == -1 && Error() == 14); + Require(unlinkat_nid_postfix(base, "", 0) == -1 && Error() == 2); + Require(unlinkat_nid_postfix(base, "missing.bin", 0) == -1 && Error() == 2); + Require(unlinkat_nid_postfix(base, "missing.bin", 0x800) == -1 && Error() == 2); + Require(unlinkat_nid_postfix(base, "missing/child", 0) == -1 && Error() == 2); + Require(unlinkat_nid_postfix(base, "file.txt/child", 0) == -1 && Error() == 20); + Require(unlinkat_nid_postfix(base, "file.txt/child", 0x800) == -1 && Error() == 20); + Require(unlinkat_nid_postfix(base, "file.txt/", 0) == -1 && Error() == 20); + Require(unlinkat_nid_postfix(base, longName.c_str(), 0) == -1 && Error() == 63); + Require(unlinkat_nid_postfix(base, "file.txt", 0x800) == -1 && Error() == 20); + Require(unlinkat_nid_postfix(base, ".", 0x800) == -1 && Error() == 22); + Require(unlinkat_nid_postfix(base, "..", 0x800) == -1 && Error() == 22); + Require(unlinkat_nid_postfix(base, ".", 0) == -1 && Error() == 1); + std::filesystem::create_directory(root / "usub"); + Require(unlinkat_nid_postfix(base, "usub", 0) == -1 && Error() == 1); + std::filesystem::create_directory(root / "nonempty"); + { std::ofstream out(root / "nonempty" / "f.bin"); out << 'x'; } + Require(unlinkat_nid_postfix(base, "nonempty", 0x800) == -1 && Error() == 66); + Require(unlinkat_nid_postfix(base, "nonempty/f.bin", 0) == 0 && !std::filesystem::exists(root / "nonempty" / "f.bin")); + Require(unlinkat_nid_postfix(base, "nonempty", 0x800) == 0 && !std::filesystem::exists(root / "nonempty")); + { std::ofstream out(root / "to_unlink.bin"); out << 'x'; } + Require(unlinkat_nid_postfix(base, "to_unlink.bin", 0) == 0 && !std::filesystem::exists(root / "to_unlink.bin")); + { std::ofstream out(root / "dotdot.bin"); out << 'x'; } + Require(unlinkat_nid_postfix(base, "usub/../dotdot.bin", 0) == 0 && !std::filesystem::exists(root / "dotdot.bin")); + std::filesystem::create_directory(root / "uslash_sub"); + Require(unlinkat_nid_postfix(base, "uslash_sub/", 0x800) == 0 && !std::filesystem::exists(root / "uslash_sub")); + Require(unlinkat_nid_postfix(base, "usub", 0x800) == 0 && !std::filesystem::exists(root / "usub")); + { std::ofstream out(root / "abs.bin"); out << 'x'; } + const auto guestAbsFile = "/" + root.filename().string() + "/abs.bin"; + Require(unlinkat_nid_postfix(-1, guestAbsFile.c_str(), 0) == 0 && !std::filesystem::exists(root / "abs.bin")); + std::filesystem::create_directory(root / "unlink_abs_dir"); + const auto guestAbsDir = "/" + root.filename().string() + "/unlink_abs_dir"; + Require(unlinkat_nid_postfix(-1, guestAbsDir.c_str(), 0x800) == 0 && !std::filesystem::exists(root / "unlink_abs_dir")); + { std::ofstream out(root / "cwd.bin"); out << 'x'; } + Require(unlinkat_nid_postfix(-100, (root / "cwd.bin").string().c_str(), 0) == 0 && !std::filesystem::exists(root / "cwd.bin")); + std::filesystem::create_directory(root / "unlink_cwd_dir"); + Require(unlinkat_nid_postfix(-100, (root / "unlink_cwd_dir").string().c_str(), 0x800) == 0 && !std::filesystem::exists(root / "unlink_cwd_dir")); + Require(openat_nid_postfix(-1, "file.txt", SCE_KERNEL_O_RDONLY) == -1 && Error() == 9); + Require(openat_nid_postfix(base, nullptr, SCE_KERNEL_O_RDONLY) == -1 && Error() == 14); + Require(openat_nid_postfix(base, "", SCE_KERNEL_O_RDONLY) == -1 && Error() == 2); + Require(renameat_nid_postfix(base, nullptr, base, "renamed.txt") == -1 && Error() == 14); + Require(renameat_nid_postfix(base, "made.bin", base, nullptr) == -1 && Error() == 14); + Require(renameat_nid_postfix(base, "", base, "renamed.txt") == -1 && Error() == 2); + Require(renameat_nid_postfix(base, "made.bin", base, "") == -1 && Error() == 2); + const int bad = sceKernelOpen(file.c_str(), SCE_KERNEL_O_RDONLY, 0); + Require(bad >= 0); + Require(renameat_nid_postfix(-1, "made.bin", base, "renamed.txt") == -1 && Error() == 9); + Require(renameat_nid_postfix(base, "made.bin", -1, "renamed.txt") == -1 && Error() == 9); + Require(renameat_nid_postfix(bad, "made.bin", base, "renamed.txt") == -1 && Error() == 20); + Require(renameat_nid_postfix(base, "made.bin", bad, "renamed.txt") == -1 && Error() == 20); + Require(sceKernelClose(bad) == 0); + const auto longPart = std::string(256, 'y'); + Require(renameat_nid_postfix(base, longPart.c_str(), base, "renamed.txt") == -1 && Error() == 63); + Require(renameat_nid_postfix(base, "made.bin", base, longPart.c_str()) == -1 && Error() == 63); + Require(renameat_nid_postfix(base, ".", base, "renamed.txt") == -1 && Error() == 22); + Require(renameat_nid_postfix(base, "..", base, "renamed.txt") == -1 && Error() == 22); + Require(renameat_nid_postfix(base, "made.bin", base, ".") == -1 && Error() == 22); + Require(renameat_nid_postfix(base, "made.bin", base, "..") == -1 && Error() == 22); + Require(renameat_nid_postfix(base, "missing.bin", base, "renamed.txt") == -1 && Error() == 2); + Require(renameat_nid_postfix(base, "made.bin", base, "renamed.bin") == 0); + Require(!std::filesystem::exists(root / "made.bin")); + Require(std::filesystem::is_regular_file(root / "renamed.bin")); + Require(renameat_nid_postfix(base, "renamed.bin", base, "renamed.bin") == 0); + const auto subPath = root / "sub_dir"; + std::filesystem::create_directory(subPath); + Require(renameat_nid_postfix(base, "renamed.bin", base, "sub_dir") == -1 && Error() == 21); + Require(renameat_nid_postfix(base, "sub_dir", base, "renamed.bin") == -1 && Error() == 20); + Require(renameat_nid_postfix(base, "sub_dir", base, "sub_renamed") == 0); + Require(!std::filesystem::exists(subPath)); + Require(std::filesystem::is_directory(root / "sub_renamed")); + const int subFd = sceKernelOpen((root / "sub_renamed").string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(subFd >= 0); + Require(renameat_nid_postfix(base, "renamed.bin", subFd, "nested.bin") == 0); + Require(!std::filesystem::exists(root / "renamed.bin")); + Require(std::filesystem::is_regular_file(root / "sub_renamed" / "nested.bin")); + Require(renameat_nid_postfix(base, "sub_renamed", subFd, "child") == -1 && Error() == 22); + const auto otherDir = root / "other_dir"; + std::filesystem::create_directory(otherDir); + { std::ofstream stream(otherDir / "item.txt"); } + Require(renameat_nid_postfix(base, "sub_renamed", base, "other_dir") == -1 && Error() == 66); + Require(renameat_nid_postfix(subFd, "nested.bin/", base, "back.bin") == -1 && Error() == 20); + Require(renameat_nid_postfix(subFd, "nested.bin", base, "nonexistent/") == -1 && Error() == 20); + Require(renameat_nid_postfix(subFd, "nested.bin", base, "made.bin") == 0); + Require(std::filesystem::is_regular_file(root / "made.bin")); + Require(sceKernelClose(subFd) == 0); + Require(renameat_nid_postfix(-100, (root / "made.bin").string().c_str(), -100, (root / "final.bin").string().c_str()) == 0); + Require(std::filesystem::is_regular_file(root / "final.bin")); + Require(fchmodat_nid_postfix(base, "file.txt", 0600, 0x0200) == 0); + Require(fchmodat_nid_postfix(-100, (root / "file.txt").string().c_str(), 0644, 0) == 0); +#ifndef _WIN32 + Require((std::filesystem::status(root / "file.txt").permissions() & std::filesystem::perms::all) == + (std::filesystem::perms::owner_read | std::filesystem::perms::owner_write | std::filesystem::perms::group_read | std::filesystem::perms::others_read)); +#endif + Require(fchmodat_nid_postfix(base, "missing.bin", 0600, 0) == -1 && Error() == 2); + Require(fchmodat_nid_postfix(base, nullptr, 0600, 0) == -1 && Error() == 14); + Require(fchmodat_nid_postfix(base, "", 0600, 0) == -1 && Error() == 2); + Require(fchmodat_nid_postfix(base, "file.txt", 0600, 0x4000) == -1 && Error() == 22); + Require(fchmodat_nid_postfix(-1, "file.txt", 0600, 0) == -1 && Error() == 9); +#ifndef _WIN32 + Require(symlink_nid_postfix("loop", (root / "loop").string().c_str()) == 0); + Require(fchmodat_nid_postfix(base, "loop/x", 0600, 0) == -1 && Error() == 62); + Require(fchmodat_nid_postfix(base, longName.c_str(), 0600, 0) == -1 && Error() == 63); +#endif + Require(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 +#ifndef _WIN32 + if (created == 0) { + const auto throughLink = (root / "through-link.bin").string(); + Require(link_nid_postfix(symbolic.c_str(), throughLink.c_str()) == 0); + Require(!std::filesystem::is_symlink(std::filesystem::symlink_status(throughLink))); + Require(std::filesystem::equivalent(throughLink, original) && std::filesystem::hard_link_count(original) == 2); + Require(std::filesystem::remove(throughLink) && std::filesystem::hard_link_count(original) == 1); + Require(link_nid_postfix((root / "dangling").string().c_str(), throughLink.c_str()) == -1 && Error() == 2); + const auto directoryLink = (root / "directory-link").string(); + Require(symlink_nid_postfix(".", directoryLink.c_str()) == 0); + Require(link_nid_postfix(directoryLink.c_str(), throughLink.c_str()) == -1 && Error() == 1); + Require(!std::filesystem::exists(std::filesystem::symlink_status(throughLink))); + } +#endif + RemoveTree(root); return 0; } diff --git a/core/libs/tests/GuestKill.cpp b/core/libs/tests/GuestKill.cpp new file mode 100644 index 0000000000..7b75694ea5 --- /dev/null +++ b/core/libs/tests/GuestKill.cpp @@ -0,0 +1,70 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +using Handler = void (APS5_VABI *)(int); +extern "C" { +Handler APS5_VABI signal_nid_postfix(int, Handler); +int APS5_VABI kill_nid_postfix(int, int); +int APS5_VABI killpg_nid_postfix(int, int); +int APS5_VABI getpid_nid_postfix(void); +int* APS5_VABI __error_nid_postfix(); +} +volatile std::sig_atomic_t received = 0; +void APS5_VABI Callback(int value) { received = value; } +static void Require(bool value) { if (!value) std::abort(); } +static void Fails(int pid, int guest, int error) { + *__error_nid_postfix() = 0; + Require(kill_nid_postfix(pid, guest) == -1 && *__error_nid_postfix() == error); +} +static void KillpgFails(int pgrp, int guest, int error) { + *__error_nid_postfix() = 0; + Require(killpg_nid_postfix(pgrp, guest) == -1 && *__error_nid_postfix() == error); +} +int main() { + const int self = getpid_nid_postfix(); + const auto invalid = reinterpret_cast(static_cast(-1)); + const auto ignore = reinterpret_cast(std::uintptr_t{1}); + for (int target : {self, 0, -self}) Require(kill_nid_postfix(target, 0) == 0); + for (int target : {self, 0}) Require(killpg_nid_postfix(target, 0) == 0); + for (int target : {self + 1, -1, -(self + 1)}) { + Fails(target, 0, 3); + Fails(target, 15, 3); + } + for (int target : {self + 1, self + 1000}) { + KillpgFails(target, 0, 3); + KillpgFails(target, 15, 3); + } + for (int target : {-1, -self}) { + KillpgFails(target, 0, 22); + KillpgFails(target, 15, 22); + } + for (int guest : {-1, 129}) { + Fails(self, guest, 22); + Fails(self + 1, guest, 22); + KillpgFails(self, guest, 22); + KillpgFails(self + 1, guest, 22); + } + Require(signal_nid_postfix(15, Callback) != invalid); + for (int target : {self, 0, -self}) { + received = 0; + Require(kill_nid_postfix(target, 0) == 0 && received == 0); + Require(kill_nid_postfix(target, 15) == 0 && received == 15); + } + for (int target : {self, 0}) { + received = 0; + Require(killpg_nid_postfix(target, 0) == 0 && received == 0); + Require(killpg_nid_postfix(target, 15) == 0 && received == 15); + } + received = 0; + Fails(self + 1, 15, 3); + KillpgFails(self + 1, 15, 3); + Require(received == 0); + Require(signal_nid_postfix(15, ignore) == Callback); + Require(kill_nid_postfix(self, 15) == 0 && received == 0); + Require(killpg_nid_postfix(self, 15) == 0 && received == 0); + Require(signal_nid_postfix(15, nullptr) == ignore); + Fails(self, 100, 22); + KillpgFails(self, 100, 22); +} diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp new file mode 100644 index 0000000000..2ff23394f4 --- /dev/null +++ b/core/libs/tests/GuestKqueue.cpp @@ -0,0 +1,1040 @@ +#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(); +int APS5_VABI kevent_nid_postfix(int, const KernelEvent*, int, KernelEvent*, int, const KernelTimespec*); +int APS5_VABI close_nid_postfix(int); +int APS5_VABI dup_nid_postfix(int); +int APS5_VABI pipe_nid_postfix(int*); +int APS5_VABI 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*); +std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); +int APS5_VABI ftruncate_nid_postfix(int, std::int64_t); +int APS5_VABI unlink_nid_postfix(const char*); +int APS5_VABI sceKernelRename(const char*, const char*); +int APS5_VABI sceKernelCreateEqueue(KernelEqueue* eq, const char* name); +int APS5_VABI sceKernelDeleteEqueue(KernelEqueue eq); +int APS5_VABI sceKernelWaitEqueue(KernelEqueue eq, KernelEvent* ev, int num, int* out, const KernelUseconds* timo); +int APS5_VABI sceKernelAddFileEvent(KernelEqueue eq, int fd, int watch, void* udata); +int APS5_VABI sceKernelDeleteFileEvent(KernelEqueue eq, int fd); +int* APS5_VABI __error_nid_postfix(); +} + +static constexpr int GuestEnoent = 2; +static constexpr int GuestEbadf = 9; +static constexpr int GuestEinval = 22; +static constexpr int 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; +static constexpr std::int16_t EvfiltRead = -1; +static constexpr std::int16_t EvfiltWrite = -2; +static constexpr std::int16_t EvfiltVnode = -4; +static constexpr std::int16_t EvfiltProc = -5; +static constexpr std::int16_t EvfiltTimer = -7; +static constexpr std::int16_t EvfiltUser = -11; +static constexpr std::uint16_t EvAdd = 0x0001; +static constexpr std::uint16_t EvDelete = 0x0002; +static constexpr std::uint16_t EvEnable = 0x0004; +static constexpr std::uint16_t EvDisable = 0x0008; +static constexpr std::uint16_t EvOneshot = 0x0010; +static constexpr std::uint16_t EvClear = 0x0020; +static constexpr std::uint16_t EvReceipt = 0x0040; +static constexpr std::uint16_t 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; +static constexpr std::uint32_t NoteAttrib = 0x0008; +static constexpr std::uint32_t NoteLink = 0x0010; +static constexpr std::uint32_t NoteRename = 0x0020; +static constexpr std::uint32_t NoteRevoke = 0x0040; +static constexpr int ReadWrite = 0x0002; +static constexpr int Create = 0x0200; +static constexpr int Directory = 0x00020000; +static constexpr int SceOk = 0; +static constexpr int SceEnoent = static_cast(0x80020002); +static constexpr int SceEbadf = static_cast(0x80020009); +static constexpr int SceEinval = static_cast(0x80020016); + +static void Require(bool value) { if (!value) std::abort(); } + +static KernelEvent Change(std::uintptr_t ident, std::int16_t filter, std::uint16_t flags, std::uint32_t fflags = 0, std::intptr_t data = 0, void* udata = nullptr) { + KernelEvent change{}; + change.ident = ident; + change.filter = filter; + change.flags = flags; + change.fflags = fflags; + change.data = data; + change.udata = udata; + return change; +} + +static int Apply(int kq, const KernelEvent& change) { + return kevent_nid_postfix(kq, &change, 1, nullptr, 0, nullptr); +} + +static void RequireFailure(int result, int error) { + Require(result == -1); + Require(*__error_nid_postfix() == error); +} + +static int Poll(int kq, KernelEvent* events, int count) { + const KernelTimespec zero{0, 0}; + return kevent_nid_postfix(kq, nullptr, 0, events, count, &zero); +} + +static int Wait(int kq, KernelEvent* events, int count) { + const KernelTimespec second{1, 0}; + return kevent_nid_postfix(kq, nullptr, 0, events, count, &second); +} + +template static bool Throws(TCall call) { + try { + call(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +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); + const int other = kqueue_nid_postfix(); + Require(other >= 0 && other != kq); + KernelEvent event{}; + Require(Poll(kq, &event, 1) == 0); + Require(Throws([kq] { dup_nid_postfix(kq); })); + Require(close_nid_postfix(kq) == 0); + RequireFailure(Poll(kq, &event, 1), GuestEbadf); + Require(close_nid_postfix(other) == 0); + int pipe[2]; + Require(pipe_nid_postfix(pipe) == 0); + RequireFailure(Poll(pipe[0], &event, 1), GuestEbadf); + Require(close_nid_postfix(pipe[0]) == 0 && close_nid_postfix(pipe[1]) == 0); +} + +static void Timers() { + const int kq = kqueue_nid_postfix(); + int marker = 0; + KernelEvent events[4]{}; + Require(Apply(kq, Change(1, EvfiltTimer, EvAdd, 0, 10, &marker)) == 0); + Require(Poll(kq, events, 4) == 0); + Require(Wait(kq, events, 4) == 1); + Require(events[0].ident == 1 && events[0].filter == EvfiltTimer && events[0].data >= 1 && events[0].udata == &marker); + Require((events[0].flags & EvClear) != 0); + Require(Wait(kq, events, 4) == 1 && events[0].ident == 1); + Require(Apply(kq, Change(1, EvfiltTimer, EvDelete)) == 0); + RequireFailure(Apply(kq, Change(1, EvfiltTimer, EvDelete)), GuestEnoent); + + Require(Apply(kq, Change(2, EvfiltTimer, EvAdd | EvOneshot, NoteUseconds, 0)) == 0); + Require(Wait(kq, events, 4) == 1 && events[0].ident == 2 && events[0].data == 1); + Require(Poll(kq, events, 4) == 0); + RequireFailure(Apply(kq, Change(2, EvfiltTimer, EvDelete)), GuestEnoent); + + 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); + Require(Apply(kq, Change(4, EvfiltTimer, EvAdd, 0, 100000)) == 0); + Require(Throws([kq] { Apply(kq, Change(4, EvfiltTimer, EvAdd, 0, 5)); })); + Require(close_nid_postfix(kq) == 0); +} + +static void UserEvents() { + const int kq = kqueue_nid_postfix(); + int marker = 0; + KernelEvent events[4]{}; + Require(Apply(kq, Change(5, EvfiltUser, EvAdd | EvClear, 0, 0, &marker)) == 0); + Require(Poll(kq, events, 4) == 0); + std::thread trigger([kq, &marker] { + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + Require(Apply(kq, Change(5, EvfiltUser, 0, NoteTrigger | NoteFfcopy | 0x123, 77, &marker)) == 0); + }); + Require(kevent_nid_postfix(kq, nullptr, 0, events, 4, nullptr) == 1); + trigger.join(); + Require(events[0].ident == 5 && events[0].filter == EvfiltUser && events[0].fflags == 0x123 && events[0].data == 77); + Require(events[0].udata == &marker && events[0].flags == EvClear); + Require(Poll(kq, events, 4) == 0); + Require(Apply(kq, Change(5, EvfiltUser, 0, NoteTrigger | NoteFfor | 0x400, 78)) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].fflags == 0x523 && events[0].data == 78); + + Require(Apply(kq, Change(6, EvfiltUser, EvAdd)) == 0); + Require(Apply(kq, Change(6, EvfiltUser, 0, NoteTrigger)) == 0); + for (int i = 0; i < 2; ++i) { + Require(Poll(kq, events, 4) == 1 && events[0].ident == 6); + } + Require(Apply(kq, Change(6, EvfiltUser, EvClear)) == 0); + Require(Poll(kq, events, 4) == 0); + Require(Apply(kq, Change(6, EvfiltUser, EvDelete)) == 0); + + KernelEvent receipt[2]{}; + const KernelEvent change = Change(7, EvfiltUser, EvAdd | EvReceipt, 0x55, 9, &marker); + Require(kevent_nid_postfix(kq, &change, 1, receipt, 2, nullptr) == 1); + Require(receipt[0].ident == 7 && receipt[0].flags == EvError && receipt[0].data == 0 && receipt[0].fflags == 0); + Require(Apply(kq, Change(7, EvfiltUser, 0, NoteTrigger)) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].fflags == 0x55 && events[0].data == 0 && events[0].flags == EvReceipt); + Require(close_nid_postfix(kq) == 0); +} + +static void 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]; + Require(socketpair_nid_postfix(Unix, Stream, 0, pair) == 0); + int marker = 0; + KernelEvent events[4]{}; + Require(Apply(kq, Change(static_cast(pair[0]), EvfiltRead, EvAdd, 0, 0, &marker)) == 0); + Require(Poll(kq, events, 4) == 0); + Require(send_nid_postfix(pair[1], "abc", 3, 0) == 3); + Require(Wait(kq, events, 4) == 1); + Require(events[0].ident == static_cast(pair[0]) && events[0].filter == EvfiltRead && events[0].data == 3 && events[0].udata == &marker); + Require(Poll(kq, events, 4) == 1); + char buffer[4]{}; + Require(recv_nid_postfix(pair[0], buffer, sizeof(buffer), 0) == 3); + Require(Poll(kq, events, 4) == 0); + Require(Apply(kq, Change(static_cast(pair[1]), EvfiltWrite, EvAdd | EvOneshot)) == 0); + Require(Wait(kq, events, 4) == 1 && events[0].filter == EvfiltWrite); + RequireFailure(Apply(kq, Change(static_cast(pair[1]), EvfiltWrite, EvDelete)), GuestEnoent); +#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 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]{}; + const KernelEvent changes[3] = { + Change(FirstSocket + 4000, EvfiltRead, EvAdd), + Change(8, EvfiltUser, EvDelete), + Change(9, EvfiltUser, EvAdd | EvEnable | EvDisable), + }; + Require(kevent_nid_postfix(kq, changes, 3, events, 4, nullptr) == 3); + Require(events[0].flags == EvError && events[0].data == GuestEbadf && events[0].ident == FirstSocket + 4000); + Require(events[1].flags == EvError && events[1].data == GuestEnoent); + Require(events[2].flags == EvError && events[2].data == GuestEinval); + RequireFailure(kevent_nid_postfix(kq, changes, 3, events, 1, nullptr), GuestEnoent); + Require(events[0].flags == EvError && events[0].data == GuestEbadf); + RequireFailure(kevent_nid_postfix(kq, changes, 3, nullptr, 0, nullptr), GuestEbadf); + RequireFailure(Apply(kq, Change(static_cast(kq), EvfiltRead, EvAdd)), GuestEinval); + RequireFailure(Apply(kq, Change(1, 1, EvAdd)), GuestEinval); + Require(Throws([kq] { Apply(kq, Change(1, EvfiltProc, EvAdd)); })); + Require(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[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}; + Require(kevent_nid_postfix(kq, &skipped, 1, events, 4, &zero) == 0); + const KernelTimespec invalid{0, 1000000000}; + RequireFailure(kevent_nid_postfix(kq, nullptr, 0, events, 4, &invalid), GuestEinval); + const KernelTimespec negative{-1, 0}; + RequireFailure(kevent_nid_postfix(kq, nullptr, 0, events, 4, &negative), GuestEinval); + Require(close_nid_postfix(kq) == 0); +} + +static std::string TemporaryName(const char* stem) { + return std::string("anyps5-kqueue-") + stem + "-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); +} + +static void FileWatch() { + const auto path = TemporaryName("file"); + const int file = open_nid_postfix(path.c_str(), ReadWrite | Create, 0644); + Require(file >= 0); + const int kq = kqueue_nid_postfix(); + int marker = 0; + KernelEvent events[4]{}; + const auto ident = static_cast(file); + Require(Apply(kq, Change(ident, EvfiltVnode, 0x25, NoteDelete | NoteWrite | NoteExtend | NoteLink | NoteRename, 0, &marker)) == 0); + Require(Poll(kq, events, 4) == 0); + Require(write_nid_postfix(file, "abc", 3) == 3); + Require(Wait(kq, events, 4) == 1); + Require(events[0].ident == ident && events[0].filter == EvfiltVnode && events[0].flags == EvClear && events[0].udata == &marker); + Require(events[0].fflags == (NoteWrite | NoteExtend) && events[0].data == 0); + Require(Poll(kq, events, 4) == 0); + Require(ftruncate_nid_postfix(file, 0) == 0); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + Require(Poll(kq, events, 4) == 0); + Require(Apply(kq, Change(ident, EvfiltVnode, EvAdd, NoteAttrib)) == 0); + Require(write_nid_postfix(file, "abc", 3) == 3); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + Require(ftruncate_nid_postfix(file, 0) == 0); + Require(Wait(kq, events, 4) == 1 && events[0].fflags == NoteAttrib && events[0].udata == nullptr); + Require(Throws([kq, ident] { Apply(kq, Change(ident, EvfiltVnode, EvAdd, NoteRevoke)); })); + Require(Apply(kq, Change(ident, EvfiltVnode, EvDelete)) == 0); + RequireFailure(Apply(kq, Change(ident, EvfiltVnode, EvDelete)), GuestEnoent); + + KernelEqueue eq = 0; + Require(sceKernelCreateEqueue(&eq, "file") == SceOk); + Require(sceKernelAddFileEvent(eq, file, static_cast(NoteWrite), &marker) == SceOk); + Require(write_nid_postfix(file, "defg", 4) == 4); + int count = 0; + const KernelUseconds second = 1000000; + Require(sceKernelWaitEqueue(eq, events, 4, &count, &second) == SceOk && count == 1); + Require(events[0].ident == ident && events[0].filter == EvfiltVnode && events[0].fflags == (NoteWrite | NoteExtend) && events[0].udata == &marker); + Require(sceKernelDeleteFileEvent(eq, file) == SceOk); + Require(sceKernelDeleteFileEvent(eq, file) == SceEnoent); + Require(sceKernelAddFileEvent(eq, FirstSocket + 4000, static_cast(NoteWrite), nullptr) == SceEbadf); + int pair[2]; + Require(socketpair_nid_postfix(Unix, Stream, 0, pair) == 0); + Require(sceKernelAddFileEvent(eq, pair[0], static_cast(NoteWrite), nullptr) == SceEinval); + RequireFailure(Apply(kq, Change(static_cast(pair[0]), EvfiltVnode, EvAdd, NoteWrite)), GuestEinval); + RequireFailure(Apply(kq, Change(static_cast(kq), EvfiltVnode, EvAdd, NoteWrite)), GuestEinval); + Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0); + int pipe[2]; + Require(pipe_nid_postfix(pipe) == 0); + RequireFailure(Apply(kq, Change(static_cast(pipe[0]), EvfiltVnode, EvAdd, NoteWrite)), GuestEinval); + Require(close_nid_postfix(pipe[0]) == 0 && close_nid_postfix(pipe[1]) == 0); + + Require(Apply(kq, Change(ident, EvfiltVnode, EvAdd, NoteWrite)) == 0); + Require(sceKernelAddFileEvent(eq, file, static_cast(NoteWrite), nullptr) == SceOk); + Require(close_nid_postfix(file) == 0); + RequireFailure(Apply(kq, Change(ident, EvfiltVnode, EvDelete)), GuestEbadf); + Require(sceKernelDeleteFileEvent(eq, file) == SceEbadf); + Require(sceKernelDeleteEqueue(eq) == SceOk); + Require(close_nid_postfix(kq) == 0); + Require(std::filesystem::remove(path)); +} + +#ifndef _WIN32 +static void RenameAndDeleteWatchedFile() { + const auto path = TemporaryName("moved"); + const auto renamed = path + "-renamed"; + const int file = open_nid_postfix(path.c_str(), ReadWrite | Create, 0644); + Require(file >= 0); + const int kq = kqueue_nid_postfix(); + KernelEvent events[4]{}; + const auto ident = static_cast(file); + Require(Apply(kq, Change(ident, EvfiltVnode, EvAdd | EvClear, NoteDelete | NoteRename)) == 0); + Require(sceKernelRename(path.c_str(), renamed.c_str()) == SceOk); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + Require(Poll(kq, events, 4) == 1 && events[0].ident == ident && events[0].fflags == NoteRename); + Require(unlink_nid_postfix(renamed.c_str()) == 0); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + Require(Poll(kq, events, 4) == 1 && events[0].ident == ident && events[0].fflags == NoteDelete); + Require(close_nid_postfix(file) == 0); + Require(close_nid_postfix(kq) == 0); +} +#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]{}; + while ((notes & last) == 0) { + Require(Wait(kq, events, 4) == 1 && events[0].ident == ident); + notes |= events[0].fflags; + } + return notes; +} + +static void MoveDirectoryEntries() { + const auto path = TemporaryName("moves"); + const auto outside = path + "-outside"; + Require(mkdir_nid_postfix(path.c_str(), 0755) == 0 && mkdir_nid_postfix(outside.c_str(), 0755) == 0); + const int child = open_nid_postfix((path + "/first.bin").c_str(), ReadWrite | Create, 0644); + Require(child >= 0 && close_nid_postfix(child) == 0); + const int directory = open_nid_postfix(path.c_str(), Directory, 0); + Require(directory >= 0); + const int kq = kqueue_nid_postfix(); + KernelEvent events[4]{}; + const auto ident = static_cast(directory); + Require(Apply(kq, Change(ident, EvfiltVnode, EvAdd | EvClear, NoteWrite | NoteExtend | NoteLink)) == 0); + for (int i = 0; i < 64; ++i) { + Require(sceKernelRename((path + "/first.bin").c_str(), (path + "/second.bin").c_str()) == SceOk); + Require(sceKernelRename((path + "/second.bin").c_str(), (path + "/first.bin").c_str()) == SceOk); + } + Require(mkdir_nid_postfix((path + "/sub").c_str(), 0755) == 0); + Require(NotesThrough(kq, ident, NoteLink) == (NoteWrite | NoteLink)); + Require(sceKernelRename((path + "/first.bin").c_str(), (outside + "/first.bin").c_str()) == SceOk); + Require(Wait(kq, events, 4) == 1 && events[0].ident == ident && events[0].fflags == (NoteWrite | NoteExtend)); + Require(sceKernelRename((outside + "/first.bin").c_str(), (path + "/first.bin").c_str()) == SceOk); + Require(Wait(kq, events, 4) == 1 && events[0].ident == ident && events[0].fflags == (NoteWrite | NoteExtend)); + Require(sceKernelRename((path + "/sub").c_str(), (outside + "/sub").c_str()) == SceOk); + Require(Wait(kq, events, 4) == 1 && events[0].ident == ident && events[0].fflags == (NoteWrite | NoteExtend | NoteLink)); + Require(close_nid_postfix(directory) == 0); + Require(close_nid_postfix(kq) == 0); + Require(std::filesystem::remove_all(path) == 2 && std::filesystem::remove_all(outside) == 2); +} +#endif + +static void DirectoryWatch() { + const auto path = TemporaryName("directory"); + Require(mkdir_nid_postfix(path.c_str(), 0755) == 0); + const int directory = open_nid_postfix(path.c_str(), Directory, 0); + Require(directory >= 0); + const int kq = kqueue_nid_postfix(); + KernelEvent events[4]{}; + const auto ident = static_cast(directory); + Require(Apply(kq, Change(ident, EvfiltVnode, EvAdd | EvClear, NoteWrite | NoteLink | NoteDelete | NoteRename)) == 0); + Require(Poll(kq, events, 4) == 0); + const int child = open_nid_postfix((path + "/child.bin").c_str(), ReadWrite | Create, 0644); + Require(child >= 0 && close_nid_postfix(child) == 0); + Require(Wait(kq, events, 4) == 1 && events[0].ident == ident && events[0].fflags == NoteWrite); + Require(mkdir_nid_postfix((path + "/sub").c_str(), 0755) == 0); + Require(Wait(kq, events, 4) == 1 && events[0].fflags == (NoteWrite | NoteLink)); + Require(sceKernelRename((path + "/child.bin").c_str(), (path + "/renamed.bin").c_str()) == SceOk); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + Require(Poll(kq, events, 4) == 1 && events[0].ident == ident && events[0].fflags == NoteWrite); + Require(unlink_nid_postfix((path + "/renamed.bin").c_str()) == 0); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + Require(Poll(kq, events, 4) == 1 && events[0].fflags == NoteWrite); + Require(rmdir_nid_postfix((path + "/sub").c_str()) == 0); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + Require(Poll(kq, events, 4) == 1 && events[0].fflags == (NoteWrite | NoteLink)); + Require(close_nid_postfix(directory) == 0); + Require(close_nid_postfix(kq) == 0); + Require(std::filesystem::remove_all(path) == 1); +} + +static void DeleteWatchedFile() { + const auto path = TemporaryName("deleted"); + const int file = open_nid_postfix(path.c_str(), ReadWrite | Create, 0644); + Require(file >= 0); + const int kq = kqueue_nid_postfix(); + KernelEvent events[4]{}; + const auto ident = static_cast(file); + Require(Apply(kq, Change(ident, EvfiltVnode, EvAdd | EvClear, NoteDelete | NoteWrite | NoteRename)) == 0); + Require(Poll(kq, events, 4) == 0); + Require(unlink_nid_postfix(path.c_str()) == 0); + Require(Wait(kq, events, 4) == 1 && events[0].ident == ident && events[0].fflags == NoteDelete); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + Require(Poll(kq, events, 4) == 0); + Require(close_nid_postfix(file) == 0); + Require(close_nid_postfix(kq) == 0); + Require(!std::filesystem::exists(path)); +} + +#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{}; + const KernelTimespec timeout{0, 30000000}; + const auto start = std::chrono::steady_clock::now(); + Require(kevent_nid_postfix(kq, nullptr, 0, &event, 1, &timeout) == 0); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(29)); + Require(kevent_nid_postfix(kq, nullptr, 0, &event, 0, nullptr) == 0); + Require(close_nid_postfix(kq) == 0); +} + +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(); + 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 new file mode 100644 index 0000000000..e85144f158 --- /dev/null +++ b/core/libs/tests/GuestLibcCndTimedWait.cpp @@ -0,0 +1,184 @@ +#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; + std::int64_t nsec; +}; + +extern "C" { +int APS5_VABI _Mtx_init_nid_postfix(void** handle, int type); +void APS5_VABI _Mtx_destroy_nid_postfix(void** handle); +int APS5_VABI _Mtx_lock_nid_postfix(void** handle); +int APS5_VABI _Mtx_unlock_nid_postfix(void** handle); +int APS5_VABI _Cnd_init_nid_postfix(void** handle); +void APS5_VABI _Cnd_destroy_nid_postfix(void** handle); +int APS5_VABI _Cnd_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 { + call(); + } catch (const TException&) { + return; + } + std::abort(); +} + +constexpr int success = 0; +constexpr int timedout = 2; +constexpr int plain = 0x01; + +int main() { + void* mutex = nullptr; + void* condition = nullptr; + Require(_Mtx_init_nid_postfix(&mutex, plain) == success); + Require(_Cnd_init_nid_postfix(&condition) == success); + + Require(_Mtx_lock_nid_postfix(&mutex) == success); + + const auto now = std::chrono::system_clock::now(); + const auto nowSec = std::chrono::duration_cast(now.time_since_epoch()).count(); + const auto nowNsec = std::chrono::duration_cast(now.time_since_epoch()).count() % 1000000000; + + Xtime pastTime{nowSec - 1, nowNsec}; + Require(_Cnd_timedwait_nid_postfix(&condition, &mutex, &pastTime) == timedout); + + Xtime unnormalizedPastTime{nowSec - 2, nowNsec + 1000000000}; + Require(_Cnd_timedwait_nid_postfix(&condition, &mutex, &unnormalizedPastTime) == timedout); + + Xtime negativePastTime{nowSec, nowNsec - 2000000000}; + Require(_Cnd_timedwait_nid_postfix(&condition, &mutex, &negativePastTime) == timedout); + + const auto timeoutDeadline = std::chrono::system_clock::now() + std::chrono::milliseconds(20); + const auto timeoutSec = std::chrono::duration_cast(timeoutDeadline.time_since_epoch()).count(); + const auto timeoutNsec = std::chrono::duration_cast(timeoutDeadline.time_since_epoch()).count() % 1000000000; + const Xtime timeoutTime{timeoutSec, timeoutNsec}; + while (true) { + const int result = _Cnd_timedwait_nid_postfix(&condition, &mutex, &timeoutTime); + if (result == timedout) { + Require(std::chrono::system_clock::now() >= timeoutDeadline); + break; + } + Require(result == success); + } + +#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; + const Xtime unnormalizedTimeoutTime{unnormalizedSec - 1, unnormalizedNsec + 1000000000}; + while (true) { + const int result = _Cnd_timedwait_nid_postfix(&condition, &mutex, &unnormalizedTimeoutTime); + if (result == timedout) { + Require(std::chrono::system_clock::now() >= unnormalizedDeadline); + break; + } + Require(result == success); + } + + Xtime futureTime{nowSec + 5, nowNsec}; + bool ready = false; + std::thread signaler([&] { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(_Mtx_lock_nid_postfix(&mutex) == success); + ready = true; + Require(_Cnd_broadcast_nid_postfix(&condition) == success); + Require(_Mtx_unlock_nid_postfix(&mutex) == success); + }); + while (!ready) { + Require(_Cnd_timedwait_nid_postfix(&condition, &mutex, &futureTime) == success); + } + signaler.join(); + + 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}; + RequireThrows([&] { _Cnd_timedwait_nid_postfix(&condition, &mutex, &overflowCarryTime); }); + const Xtime overflowSecTime{std::numeric_limits::max(), 0}; + RequireThrows([&] { _Cnd_timedwait_nid_postfix(&condition, &mutex, &overflowSecTime); }); + _Cnd_destroy_nid_postfix(&condition); + _Mtx_destroy_nid_postfix(&mutex); +} diff --git a/core/libs/tests/GuestLibcMutexInitWithName.cpp b/core/libs/tests/GuestLibcMutexInitWithName.cpp new file mode 100644 index 0000000000..2501aef36b --- /dev/null +++ b/core/libs/tests/GuestLibcMutexInitWithName.cpp @@ -0,0 +1,41 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI _Mtx_init_with_name_nid_postfix(void** handle, int type, const char* name); +void APS5_VABI _Mtx_destroy_nid_postfix(void** handle); +int APS5_VABI _Mtx_lock_nid_postfix(void** handle); +int APS5_VABI _Mtx_unlock_nid_postfix(void** handle); +} + +static void Require(bool value) { if (!value) std::abort(); } + +template +static void RequireThrows(TCall call) { + try { + call(); + } catch (const TException&) { + return; + } + std::abort(); +} + +constexpr int success = 0; +constexpr int busy = 3; +constexpr int plain = 0x01; + +int main() { + void* mutex = nullptr; + Require(_Mtx_init_with_name_nid_postfix(&mutex, plain, "GameMutex") == success); + Require(mutex != nullptr); + Require(_Mtx_lock_nid_postfix(&mutex) == success); + Require(_Mtx_lock_nid_postfix(&mutex) == busy); + Require(_Mtx_unlock_nid_postfix(&mutex) == success); + _Mtx_destroy_nid_postfix(&mutex); + Require(mutex == nullptr); + + RequireThrows([] { + _Mtx_init_with_name_nid_postfix(nullptr, plain, "NullHandle"); + }); +} diff --git a/core/libs/tests/GuestLibcStaticImports.cpp b/core/libs/tests/GuestLibcStaticImports.cpp new file mode 100644 index 0000000000..a8068575dd --- /dev/null +++ b/core/libs/tests/GuestLibcStaticImports.cpp @@ -0,0 +1,169 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +struct Xtime { + std::int64_t sec; + std::int64_t nsec; +}; + +extern "C" { +float APS5_VABI nearbyintf_nid_postfix(float x); +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 _Mtx_trylock_nid_postfix(void** handle); +int APS5_VABI _Thrd_sleep_nid_postfix(const Xtime* target, Xtime* remaining); +} + +namespace { + +void Require(bool value) { + if (!value) + std::abort(); +} + +template +void RequireThrows(TCall call) { + try { + call(); + } catch (const TException&) { + return; + } + std::abort(); +} + +constexpr int threadSuccess = 0; +constexpr int threadBusy = 3; +constexpr int mutexTryable = 0x02; +constexpr int mutexRecursive = 0x100; +constexpr std::int64_t nanosPerSecond = 1000000000LL; + +void TestNearbyintf() { + const float half = nearbyintf_nid_postfix(0.5f); + Require(half == 0.0f); + Require(!std::signbit(half)); + + const float negHalf = nearbyintf_nid_postfix(-0.5f); + Require(negHalf == 0.0f); + Require(std::signbit(negHalf)); + + Require(nearbyintf_nid_postfix(1.5f) == 2.0f); + Require(nearbyintf_nid_postfix(-1.5f) == -2.0f); + + Require(nearbyintf_nid_postfix(2.5f) == 2.0f); + Require(nearbyintf_nid_postfix(-2.5f) == -2.0f); + + const float negZero = nearbyintf_nid_postfix(-0.0f); + Require(negZero == 0.0f); + Require(std::signbit(negZero)); + + const float inf = std::numeric_limits::infinity(); + const float posInf = nearbyintf_nid_postfix(inf); + Require(std::isinf(posInf) && posInf > 0.0f); + + const float negInf = nearbyintf_nid_postfix(-inf); + Require(std::isinf(negInf) && negInf < 0.0f); + + const float nan = std::numeric_limits::quiet_NaN(); + Require(std::isnan(nearbyintf_nid_postfix(nan))); +} + +void TestMtxTrylock() { + void* mutex = nullptr; + Require(_Mtx_init_nid_postfix(&mutex, mutexTryable) == threadSuccess && mutex != nullptr); + + Require(_Mtx_trylock_nid_postfix(&mutex) == threadSuccess); + Require(_Mtx_trylock_nid_postfix(&mutex) == threadBusy); + Require(_Mtx_unlock_nid_postfix(&mutex) == threadSuccess); + + std::atomic held{false}; + std::atomic release{false}; + std::thread other([&] { + Require(_Mtx_lock_nid_postfix(&mutex) == threadSuccess); + held.store(true, std::memory_order_release); + while (!release.load(std::memory_order_acquire)) + std::this_thread::yield(); + Require(_Mtx_unlock_nid_postfix(&mutex) == threadSuccess); + }); + + while (!held.load(std::memory_order_acquire)) + std::this_thread::yield(); + + Require(_Mtx_trylock_nid_postfix(&mutex) == threadBusy); + + release.store(true, std::memory_order_release); + other.join(); + + Require(_Mtx_trylock_nid_postfix(&mutex) == threadSuccess); + Require(_Mtx_unlock_nid_postfix(&mutex) == threadSuccess); + _Mtx_destroy_nid_postfix(&mutex); + + void* recMutex = nullptr; + Require(_Mtx_init_nid_postfix(&recMutex, mutexTryable | mutexRecursive) == threadSuccess && recMutex != nullptr); + Require(_Mtx_trylock_nid_postfix(&recMutex) == threadSuccess); + Require(_Mtx_trylock_nid_postfix(&recMutex) == threadSuccess); + Require(_Mtx_trylock_nid_postfix(&recMutex) == threadSuccess); + + std::thread([&] { + Require(_Mtx_trylock_nid_postfix(&recMutex) == threadBusy); + }).join(); + + Require(_Mtx_unlock_nid_postfix(&recMutex) == threadSuccess); + std::thread([&] { + Require(_Mtx_trylock_nid_postfix(&recMutex) == threadBusy); + }).join(); + + Require(_Mtx_unlock_nid_postfix(&recMutex) == threadSuccess); + std::thread([&] { + Require(_Mtx_trylock_nid_postfix(&recMutex) == threadBusy); + }).join(); + + Require(_Mtx_unlock_nid_postfix(&recMutex) == threadSuccess); + + std::thread([&] { + Require(_Mtx_trylock_nid_postfix(&recMutex) == threadSuccess); + Require(_Mtx_unlock_nid_postfix(&recMutex) == threadSuccess); + }).join(); + + _Mtx_destroy_nid_postfix(&recMutex); +} + +void TestThrdSleep() { + timespec before{}; + Require(clock_gettime(CLOCK_REALTIME, &before) == 0); + + const std::int64_t sleepNanos = 20000000LL; + const std::int64_t targetTotal = static_cast(before.tv_sec) * nanosPerSecond + before.tv_nsec + sleepNanos; + const Xtime target{targetTotal / nanosPerSecond, targetTotal % nanosPerSecond}; + + Require(_Thrd_sleep_nid_postfix(&target, nullptr) == 0); + + timespec after{}; + Require(clock_gettime(CLOCK_REALTIME, &after) == 0); + const std::int64_t afterTotal = static_cast(after.tv_sec) * nanosPerSecond + after.tv_nsec; + Require(afterTotal >= targetTotal); + + const Xtime past{before.tv_sec - 1, before.tv_nsec}; + Require(_Thrd_sleep_nid_postfix(&past, nullptr) == 0); + + RequireThrows([] { + _Thrd_sleep_nid_postfix(nullptr, nullptr); + }); +} + +} + +int main() { + TestNearbyintf(); + TestMtxTrylock(); + TestThrdSleep(); + return 0; +} diff --git a/core/libs/tests/GuestLibcThreads.cpp b/core/libs/tests/GuestLibcThreads.cpp new file mode 100644 index 0000000000..ac1015b41f --- /dev/null +++ b/core/libs/tests/GuestLibcThreads.cpp @@ -0,0 +1,310 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI _Mtx_init_nid_postfix(void** handle, int type); +void APS5_VABI _Mtx_destroy_nid_postfix(void** handle); +int APS5_VABI _Mtx_lock_nid_postfix(void** handle); +int APS5_VABI _Mtx_unlock_nid_postfix(void** handle); +int APS5_VABI _Cnd_init_nid_postfix(void** handle); +void APS5_VABI _Cnd_destroy_nid_postfix(void** handle); +int APS5_VABI _Cnd_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*)); +} + +static void Require(bool value) { if (!value) std::abort(); } + +template +static void RequireThrows(TCall call) { + try { + call(); + } catch (const TException&) { + return; + } + std::abort(); +} + +constexpr int success = 0; +constexpr int busy = 3; +constexpr int plain = 0x01; +constexpr int tryable = 0x02; +constexpr int timed = 0x04; +constexpr int recursive = 0x100; + +static void* CreateMutex(int type) { + void* mutex = reinterpret_cast(std::uintptr_t{1}); + Require(_Mtx_init_nid_postfix(&mutex, type) == success && mutex != nullptr); + return mutex; +} + +static void MutexBasics() { + void* mutex = CreateMutex(tryable); + Require(_Mtx_lock_nid_postfix(&mutex) == success); + Require(_Mtx_lock_nid_postfix(&mutex) == busy); + RequireThrows([&] { _Mtx_destroy_nid_postfix(&mutex); }); + std::thread([&] { Require(_Mtx_unlock_nid_postfix(&mutex) == success); }).join(); + Require(_Mtx_unlock_nid_postfix(&mutex) == success); + Require(_Mtx_unlock_nid_postfix(&mutex) == success); + void* invalidMutex = nullptr; + RequireThrows([&] { _Mtx_lock_nid_postfix(&invalidMutex); }); + void* foreignCondition = nullptr; + Require(_Cnd_init_nid_postfix(&foreignCondition) == success && foreignCondition != nullptr); + void* foreignMutex = foreignCondition; + RequireThrows([&] { _Mtx_lock_nid_postfix(&foreignMutex); }); + _Cnd_destroy_nid_postfix(&foreignCondition); + _Mtx_destroy_nid_postfix(&mutex); + Require(mutex == nullptr); + RequireThrows([&] { _Mtx_lock_nid_postfix(&mutex); }); + void* emptyMutex = nullptr; + _Mtx_destroy_nid_postfix(&emptyMutex); + RequireThrows([] { _Mtx_destroy_nid_postfix(nullptr); }); + + void* plainMutex = CreateMutex(plain); + Require(_Mtx_lock_nid_postfix(&plainMutex) == success && _Mtx_lock_nid_postfix(&plainMutex) == busy); + Require(_Mtx_unlock_nid_postfix(&plainMutex) == success); + _Mtx_destroy_nid_postfix(&plainMutex); + + void* timedMutex = CreateMutex(timed | tryable); + Require(_Mtx_lock_nid_postfix(&timedMutex) == success && _Mtx_lock_nid_postfix(&timedMutex) == busy); + Require(_Mtx_unlock_nid_postfix(&timedMutex) == success); + _Mtx_destroy_nid_postfix(&timedMutex); + + void* defaultMutex = nullptr; + Require(_Mtx_init_nid_postfix(&defaultMutex, 0) == success && defaultMutex != nullptr); + Require(_Mtx_lock_nid_postfix(&defaultMutex) == success && _Mtx_lock_nid_postfix(&defaultMutex) == busy); + Require(_Mtx_unlock_nid_postfix(&defaultMutex) == success); + _Mtx_destroy_nid_postfix(&defaultMutex); + void* unknownBitsMutex = nullptr; + Require(_Mtx_init_nid_postfix(&unknownBitsMutex, tryable | 0x8) == success && unknownBitsMutex != nullptr); + Require(_Mtx_lock_nid_postfix(&unknownBitsMutex) == success && _Mtx_lock_nid_postfix(&unknownBitsMutex) == busy); + Require(_Mtx_unlock_nid_postfix(&unknownBitsMutex) == success); + _Mtx_destroy_nid_postfix(&unknownBitsMutex); + RequireThrows([] { _Mtx_init_nid_postfix(nullptr, tryable); }); + RequireThrows([] { _Mtx_lock_nid_postfix(nullptr); }); + RequireThrows([] { _Mtx_unlock_nid_postfix(nullptr); }); +} + +static void RecursiveMutex() { + void* mutex = CreateMutex(tryable | recursive); + for (int i = 0; i < 3; ++i) Require(_Mtx_lock_nid_postfix(&mutex) == success); + std::atomic acquired{false}; + std::thread other([&] { + Require(_Mtx_lock_nid_postfix(&mutex) == success); + acquired = true; + Require(_Mtx_unlock_nid_postfix(&mutex) == success); + }); + for (int i = 0; i < 2; ++i) Require(_Mtx_unlock_nid_postfix(&mutex) == success); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + Require(!acquired); + Require(_Mtx_unlock_nid_postfix(&mutex) == success); + other.join(); + Require(acquired); + _Mtx_destroy_nid_postfix(&mutex); +} + +static void MutualExclusion() { + void* mutex = CreateMutex(tryable); + long counter = 0; + std::array workers; + for (auto& worker : workers) worker = std::thread([&] { + for (int i = 0; i < 20000; ++i) { + Require(_Mtx_lock_nid_postfix(&mutex) == success); + ++counter; + Require(_Mtx_unlock_nid_postfix(&mutex) == success); + } + }); + for (auto& worker : workers) worker.join(); + Require(counter == 80000); + _Mtx_destroy_nid_postfix(&mutex); +} + +static void ConditionBroadcast() { + void* condition = reinterpret_cast(std::uintptr_t{1}); + Require(_Cnd_init_nid_postfix(&condition) == success && condition != nullptr); + void* mutex = CreateMutex(tryable); + bool ready = false; + int waiting = 0, woken = 0; + std::array waiters; + for (auto& waiter : waiters) waiter = std::thread([&] { + Require(_Mtx_lock_nid_postfix(&mutex) == success); + ++waiting; + while (!ready) Require(_Cnd_wait_nid_postfix(&condition, &mutex) == success); + ++woken; + Require(_Mtx_lock_nid_postfix(&mutex) == busy); + Require(_Mtx_unlock_nid_postfix(&mutex) == success); + }); + for (;;) { + Require(_Mtx_lock_nid_postfix(&mutex) == success); + const bool all = waiting == 2; + Require(_Mtx_unlock_nid_postfix(&mutex) == success); + if (all) break; + std::this_thread::yield(); + } + Require(_Mtx_lock_nid_postfix(&mutex) == success); + ready = true; + Require(_Cnd_broadcast_nid_postfix(&condition) == success); + Require(_Mtx_unlock_nid_postfix(&mutex) == success); + for (auto& waiter : waiters) waiter.join(); + Require(woken == 2); + Require(_Cnd_broadcast_nid_postfix(&condition) == success); + + Require(_Cnd_wait_nid_postfix(&condition, &mutex) == success); + void* recursiveMutex = CreateMutex(tryable | recursive); + Require(_Mtx_lock_nid_postfix(&recursiveMutex) == success && _Mtx_lock_nid_postfix(&recursiveMutex) == success); + Require(_Cnd_wait_nid_postfix(&condition, &recursiveMutex) == success); + Require(_Mtx_unlock_nid_postfix(&recursiveMutex) == success && _Mtx_unlock_nid_postfix(&recursiveMutex) == success); + _Mtx_destroy_nid_postfix(&recursiveMutex); + + RequireThrows([&] { _Cnd_wait_nid_postfix(&mutex, &mutex); }); + void* invalidCondition = nullptr; + RequireThrows([&] { _Cnd_broadcast_nid_postfix(&invalidCondition); }); + RequireThrows([] { _Cnd_broadcast_nid_postfix(nullptr); }); + RequireThrows([] { _Cnd_init_nid_postfix(nullptr); }); + _Cnd_destroy_nid_postfix(&condition); + Require(condition == nullptr); + RequireThrows([&] { _Cnd_broadcast_nid_postfix(&condition); }); + void* emptyCondition = nullptr; + _Cnd_destroy_nid_postfix(&emptyCondition); + RequireThrows([] { _Cnd_destroy_nid_postfix(nullptr); }); + _Mtx_destroy_nid_postfix(&mutex); +} + +static void ConditionSignal() { + void* condition = nullptr; + Require(_Cnd_init_nid_postfix(&condition) == success); + Require(_Cnd_signal_nid_postfix(&condition) == success); + _Cnd_destroy_nid_postfix(&condition); +} + +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); +} + +int main() { + MutexBasics(); + RecursiveMutex(); + MutualExclusion(); + ConditionBroadcast(); + ConditionSignal(); + PadLaunchAndJoin(); + PadDetach(); +} 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 632670181b..71b71a1669 100644 --- a/core/libs/tests/GuestLocale.cpp +++ b/core/libs/tests/GuestLocale.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/GuestLocale.hpp" #include +#include #include #include #include @@ -12,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(); @@ -23,6 +32,8 @@ int APS5_VABI _Mbtowcx_nid_postfix(std::uint16_t* dst, const char* src, std::siz int APS5_VABI _Wctombx_nid_postfix(char* dst, std::uint16_t src, std::mbstate_t* st); 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(); } @@ -46,6 +57,9 @@ static void CheckGuestCalls() { Require(locale != nullptr && locale == _ZNSt6locale16_GetgloballocaleEv_nid_postfix()); Require(_ZNSt6locale5_InitEv_nid_postfix() == locale); Require(_ZNSt6locale5_InitEv_nid_postfix() == locale); + auto* classic = _ZNSt6locale7classicEv_nid_postfix(); + Require(classic != nullptr && classic->implementation == locale); + Require(_ZNSt6locale7classicEv_nid_postfix() == classic); const std::byte* table = nullptr; std::memcpy(&table, locale, sizeof(table)); void (APS5_VABI *retain)(GuestLocale::Facet*) = nullptr; @@ -144,12 +158,97 @@ 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(); + Require(_Towctrans_nid_postfix(u'A', 1) == u'a' && _Towctrans_nid_postfix(u'a', 2) == u'A'); + *__error_nid_postfix() = 0; + for (std::uint32_t value = 0; value < 256; ++value) { + Require(_Towctrans_nid_postfix(value, 1) == static_cast(lower[value])); + Require(_Towctrans_nid_postfix(value, 2) == static_cast(upper[value])); + } + const std::uint32_t uncased[] = {0x100u, 0x391u, 0x3b1u, 0xffffu, 0xffffffffu}; + for (const std::uint32_t value : uncased) { + Require(_Towctrans_nid_postfix(value, 1) == value && _Towctrans_nid_postfix(value, 2) == value); + } + Require(*__error_nid_postfix() == 0); + const std::uint64_t invalidDescriptors[] = {0, 3}; + for (const std::uint64_t descriptor : invalidDescriptors) { + *__error_nid_postfix() = 0; + Require(_Towctrans_nid_postfix(u'A', descriptor) == u'A' && _Towctrans_nid_postfix(u'7', descriptor) == u'7'); + Require(*__error_nid_postfix() == 22); + } +} + int main() { CheckGuestCalls(); CheckLocinfoAlignment(); CheckCharacterTables(); CheckCharacterConversions(); CheckStreamDestruction(); + CheckStreamWidth(); + CheckStandardStreams(); + CheckWideCaseTranslation(); _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..0289d86fda 100644 --- a/core/libs/tests/GuestLoginDialog.cpp +++ b/core/libs/tests/GuestLoginDialog.cpp @@ -1,6 +1,9 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include +#include +#include extern "C" { int APS5_VABI sceLoginDialogInitialize(void); @@ -16,39 +19,76 @@ 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 kErrParamInvalid = static_cast(0x81340003); +constexpr int kErrBusy = static_cast(0x81340005); +constexpr int kErrNoService = static_cast(0x81340007); constexpr int kResultUserCanceled = 1; +void RequireRejected(const std::uint32_t (&valid)[16], std::size_t word, std::uint32_t value) { + std::uint32_t param[16]; + std::memcpy(param, valid, sizeof(param)); + param[word] = value; + Require(sceLoginDialogOpen(param) == kErrParamInvalid); +} + +void RequireResult(const std::int32_t (&result)[4]) { + Require(result[0] == kResultUserCanceled); + Require(result[1] == kErrNoService); + Require(result[2] == -1 && result[3] == -1); +} + } int main() { - std::int64_t param = 0; - std::int32_t result[2] = {-1, -1}; + const std::uint32_t param[16] = {0x40}; + std::int32_t result[4] = {-1, -1, -1, -1}; Require(sceLoginDialogTerminate() == kErrNotInitialized); - Require(sceLoginDialogOpen(¶m) == kErrNotInitialized); + Require(sceLoginDialogOpen(param) == kErrNotInitialized); + Require(sceLoginDialogClose() == kErrNotInitialized); Require(sceLoginDialogGetResult(result) == kErrNotInitialized); + Require(result[0] == -1 && result[1] == -1); + 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(sceLoginDialogOpen(nullptr) == kErrParamInvalid); + for (std::uint32_t size : {0u, 0x3Cu, 0x3Fu, 0x41u}) RequireRejected(param, 0, size); + for (std::uint32_t selector : {2u, 0xFFFFFFFFu}) RequireRejected(param, 1, selector); + for (std::size_t word = 0x2C / 4; word < 16; ++word) RequireRejected(param, word, 1); + Require(sceLoginDialogGetStatus() == kStatusInitialized); + Require(sceLoginDialogGetResult(result) == kErrBusy); + RequireResult(result); + std::memset(result, 0xFF, sizeof(result)); - Require(sceLoginDialogOpen(¶m) == 0); + Require(sceLoginDialogOpen(param) == 0); Require(sceLoginDialogGetStatus() == kStatusFinished); Require(sceLoginDialogUpdateStatus() == kStatusFinished); - Require(sceLoginDialogGetResult(nullptr) == kErrArgNull); + Require(sceLoginDialogGetResult(nullptr) == kErrParamInvalid); Require(sceLoginDialogGetResult(result) == 0); - Require(result[0] == kResultUserCanceled); - Require(result[1] == -1); + RequireResult(result); 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[0] = -1; + Require(sceLoginDialogGetResult(result) == 0); + Require(result[0] == 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 ee0b4d20ff..b027130f96 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -1,15 +1,33 @@ #include "prx/libc/include/general/VabiMacros.hpp" #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**); +float APS5_VABI wcstof_nid_postfix(const char16_t*, char16_t**); +long double APS5_VABI wcstold_nid_postfix(const char16_t*, char16_t**); float APS5_VABI strtof_nid_postfix(const char*, char**); long double APS5_VABI strtold_nid_postfix(const char*, char**); std::int64_t APS5_VABI strtol_nid_postfix(const char*, char**, int); @@ -20,40 +38,589 @@ 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); +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(); +double APS5_VABI sqrt_nid_postfix(double); +float APS5_VABI sqrtf_nid_postfix(float); +double APS5_VABI fabs_nid_postfix(double); +int APS5_VABI abs_nid_postfix(int); +double APS5_VABI trunc_nid_postfix(double); +double APS5_VABI ceil_nid_postfix(double); +double APS5_VABI floor_nid_postfix(double); +float APS5_VABI truncf_nid_postfix(float); +float APS5_VABI ceilf_nid_postfix(float); +float APS5_VABI floorf_nid_postfix(float); +double APS5_VABI nearbyint_nid_postfix(double); +double APS5_VABI fma_nid_postfix(double, double, double); +long double APS5_VABI acosl_nid_postfix(long double); +long double APS5_VABI asinl_nid_postfix(long double); +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); +int APS5_VABI isnanf_nid_postfix(float); +int APS5_VABI __signbitl_nid_postfix(long double); float APS5_VABI fmodf_nid_postfix(float, float); +double APS5_VABI fmod_nid_postfix(double, double); float APS5_VABI asinf_nid_postfix(float); float APS5_VABI acosf_nid_postfix(float); +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); +int APS5_VABI rand_r_nid_postfix(unsigned int*); float APS5_VABI log10f_nid_postfix(float); +float APS5_VABI powf_nid_postfix(float, float); +double APS5_VABI log1p_nid_postfix(double); +float APS5_VABI log1pf_nid_postfix(float); +double APS5_VABI expm1_nid_postfix(double); +float APS5_VABI expm1f_nid_postfix(float); +float APS5_VABI erff_nid_postfix(float); +double APS5_VABI 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); 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); +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); +float APS5_VABI acoshf_nid_postfix(float); +double APS5_VABI atanh_nid_postfix(double); +float APS5_VABI atanhf_nid_postfix(float); std::int64_t APS5_VABI lroundf_nid_postfix(float); std::int64_t APS5_VABI llround_nid_postfix(double); +std::int64_t APS5_VABI llroundf_nid_postfix(float); +std::int64_t APS5_VABI llroundl_nid_postfix(long double); int APS5_VABI __isfinitef_nid_postfix(float); +int APS5_VABI __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); +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*); +long double APS5_VABI atanl_nid_postfix(long double); +long double APS5_VABI sinl_nid_postfix(long double); +long double APS5_VABI cosl_nid_postfix(long double); +long double APS5_VABI tanl_nid_postfix(long double); +long double APS5_VABI sinhl_nid_postfix(long double); +long double APS5_VABI coshl_nid_postfix(long double); +long double APS5_VABI tanhl_nid_postfix(long double); +long double APS5_VABI expl_nid_postfix(long double); +long double APS5_VABI exp2l_nid_postfix(long double); +long double APS5_VABI logl_nid_postfix(long double); +long double APS5_VABI log10l_nid_postfix(long double); +long double APS5_VABI powl_nid_postfix(long double, long double); +long double APS5_VABI ldexpl_nid_postfix(long double, int); +long double APS5_VABI truncl_nid_postfix(long double); +long double APS5_VABI nearbyintl_nid_postfix(long double); +double APS5_VABI logb_nid_postfix(double); +double APS5_VABI remainder_nid_postfix(double, double); +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(); } +static void CheckSquareRoot() { + Require(sqrt_nid_postfix(4.) == 2. && sqrtf_nid_postfix(6.25f) == 2.5f); + Require(std::bit_cast(sqrt_nid_postfix(2.)) == 0x3ff6a09e667f3bcdull); + Require(std::bit_cast(sqrtf_nid_postfix(2.f)) == 0x3fb504f3u); + Require(std::bit_cast(sqrt_nid_postfix(0x1p-1074)) == 0x1e60000000000000ull); + Require(std::bit_cast(sqrt_nid_postfix(-0.)) == 0x8000000000000000ull); + Require(std::bit_cast(sqrtf_nid_postfix(-0.f)) == 0x80000000u); + Require(sqrt_nid_postfix(INFINITY) == INFINITY && sqrtf_nid_postfix(INFINITY) == INFINITY); + Require(std::isnan(sqrt_nid_postfix(-1.)) && std::isnan(sqrtf_nid_postfix(-1.f))); +} + +static void CheckExactOperations() { + const double nan = std::numeric_limits::quiet_NaN(); + 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); + Require(trunc_nid_postfix(-INFINITY) == -INFINITY && std::isnan(trunc_nid_postfix(nan))); + Require(ceil_nid_postfix(2.2) == 3. && ceil_nid_postfix(-2.7) == -2. && ceil_nid_postfix(5.) == 5.); + Require(std::bit_cast(ceil_nid_postfix(-0.5)) == 0x8000000000000000ull); + Require(std::bit_cast(ceil_nid_postfix(-0.)) == 0x8000000000000000ull && ceil_nid_postfix(0x1p-1074) == 1.); + Require(ceil_nid_postfix(4503599627370495.5) == 4503599627370496. && ceil_nid_postfix(-1e300) == -1e300); + Require(ceil_nid_postfix(INFINITY) == INFINITY && ceil_nid_postfix(-INFINITY) == -INFINITY && std::isnan(ceil_nid_postfix(nan))); + Require(std::fesetround(FE_DOWNWARD) == 0); + const bool ceilIgnoresRounding = ceil_nid_postfix(2.2) == 3. && ceil_nid_postfix(-2.7) == -2.; + Require(std::fesetround(FE_TONEAREST) == 0 && ceilIgnoresRounding); + Require(floor_nid_postfix(2.7) == 2. && floor_nid_postfix(-2.2) == -3. && floor_nid_postfix(5.) == 5.); + Require(std::bit_cast(floor_nid_postfix(-0.5)) == 0xbff0000000000000ull); + Require(std::bit_cast(floor_nid_postfix(-0.)) == 0x8000000000000000ull && floor_nid_postfix(-0x1p-1074) == -1.); + Require(std::bit_cast(floor_nid_postfix(0.5)) == 0); + Require(floor_nid_postfix(4503599627370495.5) == 4503599627370495. && floor_nid_postfix(-1e300) == -1e300); + Require(floor_nid_postfix(INFINITY) == INFINITY && floor_nid_postfix(-INFINITY) == -INFINITY && std::isnan(floor_nid_postfix(nan))); + Require(std::fesetround(FE_UPWARD) == 0); + const bool floorIgnoresRounding = floor_nid_postfix(2.7) == 2. && floor_nid_postfix(-2.2) == -3.; + Require(std::fesetround(FE_TONEAREST) == 0 && floorIgnoresRounding); + Require(fma_nid_postfix(2., 3., 4.) == 10.); + volatile double tenth = 0.1; + Require(fma_nid_postfix(tenth, 10., -1.) == 0x1p-54); + 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() { + std::feclearexcept(FE_ALL_EXCEPT); + const double zero = nearbyint_nid_postfix(0.5); + Require(zero == 0.0 && !std::signbit(zero)); + Require(nearbyint_nid_postfix(1.5) == 2.0 && nearbyint_nid_postfix(2.5) == 2.0); + Require(nearbyint_nid_postfix(2.7) == 3.0 && nearbyint_nid_postfix(-2.7) == -3.0); + Require(std::bit_cast(nearbyint_nid_postfix(-0.5)) == 0x8000000000000000ull); + Require(nearbyint_nid_postfix(0x1p53) == 0x1p53); + Require(nearbyint_nid_postfix(INFINITY) == INFINITY && nearbyint_nid_postfix(-INFINITY) == -INFINITY); + Require(std::isnan(nearbyint_nid_postfix(NAN))); + Require(std::fetestexcept(FE_INEXACT) == 0); +} + +static 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)); + std::uint64_t actualMantissa = 0; + std::uint16_t actualSignExponent = 0; + std::memcpy(&actualMantissa, bytes, 8); + std::memcpy(&actualSignExponent, bytes + 8, 2); + return actualMantissa == mantissa && actualSignExponent == signExponent; +} + +static void CheckNanConversions() { + 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) { + char* end = nullptr; + const auto value = std::bit_cast(strtod_nid_postfix(test.text, &end)); + if (value != test.bits || end != test.text + test.used) { + std::fprintf(stderr, "strtod(\"%s\") = 0x%016llx\n", test.text, static_cast(value)); + correct = false; + } + 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 != 0x7fc00000u || end != test.text + test.sequenceUsed) { + std::fprintf(stderr, "strtof(\"%s\") = 0x%08x\n", test.text, static_cast(single)); + 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() { + Require(std::bit_cast(hypot_nid_postfix(INFINITY, std::bit_cast(0x7ff0000000000001ull))) == 0x7ff0000000000000ull); + Require(std::bit_cast(hypot_nid_postfix(std::bit_cast(0x7ff0000000000001ull), INFINITY)) == 0x7ff0000000000000ull); + Require(std::bit_cast(hypot_nid_postfix(-INFINITY, std::bit_cast(0x7ff0000000000001ull))) == 0x7ff0000000000000ull); + Require(std::bit_cast(hypot_nid_postfix(std::bit_cast(0x7ff0000000000001ull), -INFINITY)) == 0x7ff0000000000000ull); +} + +static void CheckAsinfErrno() { + errno = 0; + (void)asinf_nid_postfix(2.0f); + Require(errno == 0); +} + +static void CheckExp2Errno() { + errno = 0; + std::feclearexcept(FE_ALL_EXCEPT); + double result = exp2_nid_postfix(1024.0); + Require(errno == 0); + Require(std::isinf(result) && result > 0); + Require(std::fetestexcept(FE_OVERFLOW | FE_INEXACT) == (FE_OVERFLOW | FE_INEXACT)); +} + +static void CheckPowfSpecialValue() { + Require(std::bit_cast(powf_nid_postfix(1.0f, std::bit_cast(0x7f800001u))) == 0x3f800000u); +} + +static void CheckAcosfErrno() { + errno = 0; + Require(std::bit_cast(acosf_nid_postfix(2.0f)) == 0xffc00000u); + Require(errno == 0); + Require(std::bit_cast(acosf_nid_postfix(-2.0f)) == 0xffc00000u); + Require(errno == 0); + Require(std::bit_cast(acosf_nid_postfix(0.5f)) == 0x3f860a92u); + Require(errno == 0); +} + +static void CheckLongDouble() { + Require(ExtendedBits(acosl_nid_postfix(-1.0L), 0xc90fdaa22168c235ull, 0x4000)); + Require(ExtendedBits(acosl_nid_postfix(0.0L), 0xc90fdaa22168c235ull, 0x3fff)); + Require(ExtendedBits(acosl_nid_postfix(1.0L), 0, 0)); + Require(std::isnan(acosl_nid_postfix(1.5L)) && std::isnan(acosl_nid_postfix(-2.0L))); + + int exponent = 77; + Require(frexpl_nid_postfix(8.0L, &exponent) == 0.5L && exponent == 4); + Require(frexpl_nid_postfix(-3.0L, &exponent) == -0.75L && exponent == 2); + Require(frexpl_nid_postfix(0.375L, &exponent) == 0.75L && exponent == -1); + long double smallest = 0; + const std::uint64_t one = 1; + std::memcpy(&smallest, &one, sizeof(one)); + Require(ExtendedBits(frexpl_nid_postfix(smallest, &exponent), 0x8000000000000000ull, 0x3ffe) && exponent == -16444); + exponent = 77; + Require(ExtendedBits(frexpl_nid_postfix(-0.0L, &exponent), 0, 0x8000) && exponent == 0); + exponent = 77; + 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() { + std::feclearexcept(FE_ALL_EXCEPT); + const long double zero = nearbyintl_nid_postfix(0.5L); + Require(zero == 0.0L && !std::signbit(zero)); + Require(nearbyintl_nid_postfix(1.5L) == 2.0L && nearbyintl_nid_postfix(2.5L) == 2.0L); + Require(nearbyintl_nid_postfix(2.7L) == 3.0L && nearbyintl_nid_postfix(-2.7L) == -3.0L); + Require(ExtendedBits(nearbyintl_nid_postfix(-0.5L), 0, 0x8000)); + Require(nearbyintl_nid_postfix(0x1p200L) == 0x1p200L); + Require(nearbyintl_nid_postfix(INFINITY) == INFINITY && nearbyintl_nid_postfix(-INFINITY) == -INFINITY); + Require(std::isnan(nearbyintl_nid_postfix(NAN))); + Require(std::fetestexcept(FE_INEXACT) == 0); +} + +static void CheckFmodErrno() { + errno = 0; + (void)fmod_nid_postfix(1.0, 0.0); + Require(errno == 0); + Require(fmod_nid_postfix(1e300, 3.0) == std::fmod(1e300, 3.0)); +} + +static long double MakeExtended(std::uint64_t mantissa, std::uint16_t signExponent) { + unsigned char bytes[sizeof(long double)]{}; + std::memcpy(bytes, &mantissa, 8); + std::memcpy(bytes + 8, &signExponent, 2); + long double value = 0; + std::memcpy(&value, bytes, sizeof(value)); + return value; +} + +static void CheckAsinl() { + const struct { std::uint64_t inputMantissa; std::uint16_t inputSignExponent; std::uint64_t outputMantissa; std::uint16_t outputSignExponent; } cases[] = { + {0, 0x0000, 0, 0x0000}, {0, 0x8000, 0, 0x8000}, + {0x8000000000000000ull, 0x3fd7, 0x8000000000000000ull, 0x3fd7}, {0x8000000000000000ull, 0xbfd7, 0x8000000000000000ull, 0xbfd7}, + {0x8000000000000000ull, 0x3ffd, 0x815f4e705c3e68f3ull, 0x3ffd}, {0x8000000000000000ull, 0xbffd, 0x815f4e705c3e68f3ull, 0xbffd}, + {0xcccccccccccccccdull, 0x3ffb, 0xcd2493b01bc41e02ull, 0x3ffb}, + {0x8000000000000000ull, 0x3ffe, 0x860a91c16b9b2c23ull, 0x3ffe}, {0x8000000000000000ull, 0xbffe, 0x860a91c16b9b2c23ull, 0xbffe}, + {0xc000000000000000ull, 0xbffe, 0xd91a98ae3406e041ull, 0xbffe}, {0xe666666666666666ull, 0x3ffe, 0x8f549b82fd0b8fa3ull, 0x3fff}, + {0xf333333333333333ull, 0x3ffe, 0xa06a08acf9a04f78ull, 0x3fff}, {0xfd70a3d70a3d70a4ull, 0xbffe, 0xb6f1e3799ea8ecc3ull, 0xbfff}, + {0xffffffffffffffffull, 0x3ffe, 0xc90fdaa16c63cf01ull, 0x3fff}, + {0x8000000000000000ull, 0x3fff, 0xc90fdaa22168c235ull, 0x3fff}, {0x8000000000000000ull, 0xbfff, 0xc90fdaa22168c235ull, 0xbfff}, + }; + for (const auto& test : cases) { + *__error_nid_postfix() = 12345; + const long double result = asinl_nid_postfix(MakeExtended(test.inputMantissa, test.inputSignExponent)); + if (!ExtendedBits(result, test.outputMantissa, test.outputSignExponent) || *__error_nid_postfix() != 12345) { + std::fprintf(stderr, "Guest asinl failed for %016llx:%04x\n", static_cast(test.inputMantissa), test.inputSignExponent); + std::abort(); + } + } + Require(std::fabs(asinl_nid_postfix(0.5L) - 0.523598775598298873077107230547L) <= 0x1p-64L); + const long double domainErrors[] = { + 1.5L, -2.0L, MakeExtended(0x8000000000000001ull, 0x3fff), MakeExtended(0x8000000000000001ull, 0xbfff), + std::numeric_limits::max(), std::numeric_limits::infinity(), -std::numeric_limits::infinity(), + std::numeric_limits::quiet_NaN(), + }; + for (const long double input : domainErrors) { + std::feclearexcept(FE_ALL_EXCEPT); + *__error_nid_postfix() = 0; + Require(std::isnan(asinl_nid_postfix(input)) && *__error_nid_postfix() == EDOM && std::fetestexcept(FE_INVALID) != 0); + } + *__error_nid_postfix() = 0; +} + static void CheckIntegerConversions() { for (const long long numerator : {4294967301LL, -4294967301LL}) { for (const long long denominator : {3LL, -3LL}) { @@ -93,6 +660,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}, {"z!", 36, 35, 1, 0}, {"", 10, 0, 0, 0}, {" \t+!", 10, 0, 0, 0}, @@ -138,6 +708,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}, {"z!", 36, 35, 1, 0}, {"", 10, 0, 0, 0}, {" \t-!", 10, 0, 0, 0}, @@ -161,6 +733,11 @@ static void CheckIntegerConversions() { Require(_Stoull_nid_postfix(text, &end, base) == 0 && end == text + 3); Require(_Stoul_nid_postfix(text, &end, base) == 0 && end == text + 3); } + Require(atol_nid_postfix(" \t-4294967296tail") == -INT64_C(4294967296)); + Require(atol_nid_postfix("9223372036854775807") == INT64_MAX && atol_nid_postfix("+12") == 12); + Require(atol_nid_postfix("0x10") == 0 && atol_nid_postfix("010") == 10 && atol_nid_postfix("") == 0); + Require(atoll_nid_postfix("-9223372036854775808") == INT64_MIN && atoll_nid_postfix("4294967297x") == 4294967297LL); + Require(atoll_nid_postfix(" -0012") == -12 && atoll_nid_postfix("z1") == 0); *__error_nid_postfix() = 13; Require(strtol_nid_postfix("-4294967296", nullptr, 10) == -INT64_C(4294967296)); Require(*__error_nid_postfix() == 13); @@ -173,6 +750,251 @@ static void CheckIntegerConversions() { *__error_nid_postfix() = 0; } +static bool Near(double value, double expected, double tolerance) { return std::abs(value - expected) <= tolerance * std::abs(expected); } + +static void CheckAsin() { + const double pi = 3.14159265358979323846; + const double pi_2 = pi / 2; + const double nan = std::numeric_limits::quiet_NaN(); + const double infinity = std::numeric_limits::infinity(); + + Require(asin_nid_postfix(0.) == 0.); + Require(std::signbit(asin_nid_postfix(-0.))); + Require(Near(asin_nid_postfix(1.), pi_2, 1e-15)); + Require(Near(asin_nid_postfix(-1.), -pi_2, 1e-15)); + Require(std::isnan(asin_nid_postfix(2.0)) && std::signbit(asin_nid_postfix(2.0))); + Require(std::isnan(asin_nid_postfix(-2.0)) && std::signbit(asin_nid_postfix(-2.0))); + Require(std::isnan(asin_nid_postfix(infinity)) && std::signbit(asin_nid_postfix(infinity))); + Require(std::isnan(asin_nid_postfix(-infinity)) && std::signbit(asin_nid_postfix(-infinity))); + Require(std::isnan(asin_nid_postfix(nan))); + Require(Near(asin_nid_postfix(0.5), pi/6, 1e-15)); + Require(Near(asin_nid_postfix(-0.5), -pi/6, 1e-15)); + Require(Near(asin_nid_postfix(1./std::sqrt(2)), pi/4, 1e-15)); + Require(asin_nid_postfix(1e-300) == 1e-300); + Require(asin_nid_postfix(-1e-300) == -1e-300); + std::feclearexcept(FE_ALL_EXCEPT); + errno = 0; + asin_nid_postfix(2.0); + Require(std::fetestexcept(FE_INVALID)); + Require(errno == 0); + std::feclearexcept(FE_ALL_EXCEPT); + asin_nid_postfix(nan); + Require(!std::fetestexcept(FE_INVALID)); +} + +static void CheckExtendedMath() { + Require(ExtendedBits(atanl_nid_postfix(INFINITY), 0xc90fdaa22168c235ull, 0x3fff)); + Require(std::fabs(atanl_nid_postfix(1.0L) - 0.785398163397448309616L) < 1e-18L); + Require(std::fabs(sinl_nid_postfix(0.5L) - 0.479425538604203000273L) < 1e-18L); + Require(std::fabs(cosl_nid_postfix(0.5L) - 0.877582561890372716116L) < 1e-18L); + Require(std::fabs(tanl_nid_postfix(0.5L) - 0.546302489843790513255L) < 1e-18L); + Require(std::fabs(sinhl_nid_postfix(1.0L) - 1.17520119364380145688L) < 1e-18L); + Require(std::fabs(coshl_nid_postfix(1.0L) - 1.54308063481524377848L) < 1e-18L); + Require(std::fabs(tanhl_nid_postfix(0.5L) - 0.462117157260009758502L) < 1e-18L); + Require(std::fabs(expl_nid_postfix(1.0L) - 2.71828182845904523536L) < 1e-18L); + Require(std::fabs(logl_nid_postfix(2.0L) - 0.693147180559945309417L) < 1e-18L); + Require(sinl_nid_postfix(0.0L) == 0.0L && std::signbit(sinl_nid_postfix(-0.0L)) && cosl_nid_postfix(0.0L) == 1.0L); + Require(std::isnan(sinl_nid_postfix(INFINITY)) && std::isnan(cosl_nid_postfix(-INFINITY)) && std::isnan(tanl_nid_postfix(INFINITY))); + Require(sinhl_nid_postfix(-INFINITY) == -INFINITY && coshl_nid_postfix(-INFINITY) == INFINITY && tanhl_nid_postfix(INFINITY) == 1.0L); + Require(sinhl_nid_postfix(11358.0L) == INFINITY && std::isfinite(sinhl_nid_postfix(11356.0L)) && std::isfinite(coshl_nid_postfix(11356.0L))); + Require(expl_nid_postfix(0.0L) == 1.0L && expl_nid_postfix(-INFINITY) == 0.0L && expl_nid_postfix(11357.0L) == INFINITY); + Require(exp2l_nid_postfix(10.0L) == 1024.0L && exp2l_nid_postfix(-16446.0L) == 0.0L && exp2l_nid_postfix(16384.0L) == INFINITY); + Require(ExtendedBits(exp2l_nid_postfix(-16445.0L), 1, 0) && ExtendedBits(exp2l_nid_postfix(-16444.0L), 2, 0)); + Require(logl_nid_postfix(1.0L) == 0.0L && logl_nid_postfix(0.0L) == -INFINITY && std::isnan(logl_nid_postfix(-1.0L))); + Require(std::fabs(log10l_nid_postfix(1000.0L) - 3.0L) < 1e-18L && log10l_nid_postfix(1.0L) == 0.0L); + Require(log10l_nid_postfix(0.0L) == -INFINITY && std::isnan(log10l_nid_postfix(-1.0L))); + Require(powl_nid_postfix(2.0L, 64.0L) == 18446744073709551616.0L && powl_nid_postfix(-2.0L, 3.0L) == -8.0L); + Require(powl_nid_postfix(0.0L, -1.0L) == INFINITY && powl_nid_postfix(1.0L, NAN) == 1.0L && std::isnan(powl_nid_postfix(-2.0L, 0.5L))); + Require(ldexpl_nid_postfix(1.0L, 16383) == 0x1p16383L && ldexpl_nid_postfix(1.0L, 16384) == INFINITY && ldexpl_nid_postfix(3.0L, -2) == 0.75L); + Require(ExtendedBits(ldexpl_nid_postfix(1.0L, -16445), 1, 0) && ldexpl_nid_postfix(1.0L, -16446) == 0.0L); + Require(truncl_nid_postfix(2.7L) == 2.0L && truncl_nid_postfix(-2.7L) == -2.0L && ExtendedBits(truncl_nid_postfix(-0.5L), 0, 0x8000)); + Require(truncl_nid_postfix(9223372036854775807.5L) == 9223372036854775807.0L && truncl_nid_postfix(-INFINITY) == -INFINITY); + Require(logb_nid_postfix(8.0) == 3.0 && logb_nid_postfix(0.75) == -1.0 && logb_nid_postfix(0x1p-1074) == -1074.0); + Require(logb_nid_postfix(0.0) == -INFINITY && logb_nid_postfix(-INFINITY) == INFINITY && std::isnan(logb_nid_postfix(NAN))); + Require(remainder_nid_postfix(5.0, 3.0) == -1.0 && remainder_nid_postfix(7.0, 2.0) == -1.0 && remainder_nid_postfix(5.0, 2.0) == 1.0); + Require(std::signbit(remainder_nid_postfix(-4.0, 2.0)) && remainder_nid_postfix(1.0, INFINITY) == 1.0); + Require(std::isnan(remainder_nid_postfix(1.0, 0.0)) && std::isnan(remainder_nid_postfix(INFINITY, 1.0))); +} + +static void CheckHyperbolic() { + const double infinity = std::numeric_limits::infinity(); + 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))); + Require(acosh_nid_postfix(1.) == 0. && acoshf_nid_postfix(1.f) == 0.f); + Require(atanh_nid_postfix(0.) == 0. && atanh_nid_postfix(1.) == infinity && atanh_nid_postfix(-1.) == -infinity); + Require(std::isnan(acosh_nid_postfix(0.5)) && std::isnan(acoshf_nid_postfix(0.5f)) && std::isnan(atanh_nid_postfix(2.))); + Require(Near(sinh_nid_postfix(1.), 1.1752011936438014, 1e-15)); + Require(Near(cosh_nid_postfix(1.), 1.5430806348152437, 1e-15)); + Require(Near(coshf_nid_postfix(1.f), 1.5430806348152437, 1e-6)); + Require(Near(asinh_nid_postfix(1.), 0.881373587019543, 1e-15)); + Require(Near(asinhf_nid_postfix(1.f), 0.881373587019543, 1e-6)); + Require(Near(acosh_nid_postfix(2.), 1.3169578969248166, 1e-15)); + Require(Near(acoshf_nid_postfix(2.f), 1.3169578969248166, 1e-6)); + Require(Near(atanh_nid_postfix(0.5), 0.5493061443340549, 1e-15)); + Require(sinh_nid_postfix(1e-300) == 1e-300 && asinh_nid_postfix(1e-300) == 1e-300 && atanh_nid_postfix(1e-300) == 1e-300); + Require(std::isfinite(cosh_nid_postfix(710.4)) && std::isfinite(sinh_nid_postfix(-710.4)) && sinh_nid_postfix(-710.4) < 0.); + Require(Near(asinh_nid_postfix(-1e300), -691.4686750787736, 1e-15)); + 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[] = { + {0x00000000u, 0x00000000u}, {0x80000000u, 0x80000000u}, {0x00000001u, 0x00000001u}, {0x39000000u, 0x39000000u}, + {0x3e800000u, 0x3e815667u}, {0xbf000000u, 0xbf056680u}, {0x3f800000u, 0x3f966cfeu}, {0xbf800000u, 0xbf966cfeu}, + {0x40000000u, 0x40681e7bu}, {0x41100000u, 0x457d38acu}, {0xc1a00000u, 0xcd675844u}, {0x42b17217u, 0x7effff84u}, + {0x42b20000u, 0x7f28e166u}, {0xc2b20000u, 0xff28e166u}, {0x42b2d4fcu, 0x7f7fffecu}, + {0x7f800000u, 0x7f800000u}, {0xff800000u, 0xff800000u}, + }; + for (const auto& test : cases) { + *__error_nid_postfix() = 12345; + const float result = sinhf_nid_postfix(std::bit_cast(test.input)); + if (std::bit_cast(result) != test.output || *__error_nid_postfix() != 12345) { + std::fprintf(stderr, "Guest sinhf failed for %08x\n", test.input); + std::abort(); + } + } + Require(std::isnan(sinhf_nid_postfix(std::numeric_limits::quiet_NaN())) && *__error_nid_postfix() == 12345); + Require(Near(sinhf_nid_postfix(1.f), 1.1752011936438014, 1e-6) && Near(sinhf_nid_postfix(-2.f), -3.626860407847019, 1e-6)); + Require(Near(sinhf_nid_postfix(0.25f), 0.25261231680816831, 1e-6) && Near(sinhf_nid_postfix(20.f), 242582597.70489514, 1e-6)); + for (const std::uint32_t input : {0x42b2d4fdu, 0x42c80000u, 0x7f7fffffu}) { + for (const std::uint32_t sign : {0u, 0x80000000u}) { + std::feclearexcept(FE_ALL_EXCEPT); + *__error_nid_postfix() = 0; + const float result = sinhf_nid_postfix(std::bit_cast(input | sign)); + Require(result == (sign != 0 ? -infinity : infinity) && *__error_nid_postfix() == ERANGE); + Require(std::fetestexcept(FE_OVERFLOW) != 0); + } + } + *__error_nid_postfix() = 0; +} + +static void CheckAtanhf() { + const struct { std::uint32_t input; std::uint32_t output; } cases[] = { + {0x00000000u, 0x00000000u}, {0x80000000u, 0x80000000u}, {0x00000001u, 0x00000001u}, {0x0da24260u, 0x0da24260u}, + {0x31000000u, 0x31000000u}, {0xb1000000u, 0xb1000000u}, {0x31800000u, 0x31800000u}, {0x35800000u, 0x35800000u}, + {0x3dcccccdu, 0x3dcd7c9eu}, {0x3e800000u, 0x3e82c578u}, {0xbe800000u, 0xbe82c578u}, {0x3eaaaaaau, 0x3eb17217u}, + {0x3eaaaaabu, 0x3eb17218u}, {0x3effffffu, 0x3f0c9f53u}, {0x3f000000u, 0x3f0c9f54u}, {0xbf000000u, 0xbf0c9f54u}, + {0x3f199999u, 0x3f317217u}, {0x3f19999au, 0x3f317218u}, {0x3f400000u, 0x3f791396u}, {0xbf400000u, 0xbf791396u}, + {0x3f666666u, 0x3fbc71afu}, {0x3f7fffffu, 0x410aa123u}, {0xbf7fffffu, 0xc10aa123u}, + }; + for (const auto& test : cases) { + *__error_nid_postfix() = 12345; + const float result = atanhf_nid_postfix(std::bit_cast(test.input)); + if (std::bit_cast(result) != test.output || *__error_nid_postfix() != 12345) { + std::fprintf(stderr, "Guest atanhf failed for %08x\n", test.input); + std::abort(); + } + } + for (const std::uint32_t input : {0x3f800000u, 0xbf800000u}) { + std::feclearexcept(FE_ALL_EXCEPT); + *__error_nid_postfix() = 0; + const float result = atanhf_nid_postfix(std::bit_cast(input)); + Require(std::bit_cast(result) == (input | 0x7f800000u) && *__error_nid_postfix() == ERANGE); + Require(std::fetestexcept(FE_DIVBYZERO) != 0); + } + for (const std::uint32_t input : {0x3f800001u, 0xbf800001u, 0x3fc00000u, 0xbfc00000u, 0x7f7fffffu, 0x7f800000u, 0xff800000u, 0x7fc00000u, 0xffc00000u}) { + std::feclearexcept(FE_ALL_EXCEPT); + *__error_nid_postfix() = 0; + Require(std::isnan(atanhf_nid_postfix(std::bit_cast(input))) && *__error_nid_postfix() == EDOM); + Require(std::fetestexcept(FE_INVALID) != 0); + } + *__error_nid_postfix() = 0; +} + static void CheckFloatClassification() { Require(_FInf_nid_postfix.bits[0] == 0x7f800000u && _FNan_nid_postfix.bits[0] == 0x7fc00000u); for (int word = 1; word < 4; ++word) Require(_FInf_nid_postfix.bits[word] == 0 && _FNan_nid_postfix.bits[word] == 0); @@ -198,9 +1020,266 @@ 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(); + Require(isnanf_nid_postfix(nan) == 1); + Require(isnanf_nid_postfix(infinity) == 0 && isnanf_nid_postfix(-infinity) == 0); + Require(isnanf_nid_postfix(0.f) == 0 && isnanf_nid_postfix(-1.f) == 0); + + const long double negativeNan = std::copysign(std::numeric_limits::quiet_NaN(), -1.L); + Require(__signbitl_nid_postfix(0.L) == 0 && __signbitl_nid_postfix(-0.L) == 1); + Require(__signbitl_nid_postfix(1.L) == 0 && __signbitl_nid_postfix(-1.L) == 1); + Require(__signbitl_nid_postfix(std::numeric_limits::infinity()) == 0); + Require(__signbitl_nid_postfix(-std::numeric_limits::infinity()) == 1); + Require(__signbitl_nid_postfix(negativeNan) == 1); +} + +static void CheckFloatMathExports() { + Require(ceilf_nid_postfix(2.25f) == 3.f && ceilf_nid_postfix(-2.25f) == -2.f); + Require(floorf_nid_postfix(2.75f) == 2.f && floorf_nid_postfix(-2.25f) == -3.f); + Require(truncf_nid_postfix(2.75f) == 2.f && truncf_nid_postfix(-2.75f) == -2.f); + Require(ceilf_nid_postfix(2.f) == 2.f && floorf_nid_postfix(-2.f) == -2.f && truncf_nid_postfix(2.f) == 2.f); + Require(std::bit_cast(ceilf_nid_postfix(-0.f)) == 0x80000000u); + Require(std::bit_cast(floorf_nid_postfix(-0.f)) == 0x80000000u); + Require(std::bit_cast(truncf_nid_postfix(-0.f)) == 0x80000000u); + Require(ceilf_nid_postfix(std::numeric_limits::infinity()) == std::numeric_limits::infinity()); + Require(floorf_nid_postfix(-std::numeric_limits::infinity()) == -std::numeric_limits::infinity()); + Require(std::isnan(ceilf_nid_postfix(std::numeric_limits::quiet_NaN()))); + + const double nan = std::numeric_limits::quiet_NaN(); + const double infinity = std::numeric_limits::infinity(); + Require(isnan_nid_postfix(nan) == 1 && isnan_nid_postfix(infinity) == 0 && isnan_nid_postfix(0.0) == 0); + Require(isnan_nid_postfix(-nan) == 1 && isnan_nid_postfix(-1.0) == 0); + Require(isinf_nid_postfix(infinity) == 1 && isinf_nid_postfix(-infinity) == 1); + Require(isinf_nid_postfix(nan) == 0 && isinf_nid_postfix(1.0) == 0); +} + +static void 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}, + {0x0000000000000001ull, -2}, {0x800fffffffffffffull, -2}, + {0x0010000000000000ull, -1}, {0xbff0000000000000ull, -1}, {0x7fefffffffffffffull, -1}, + {0x7ff0000000000000ull, 1}, {0xfff0000000000000ull, 1}, + {0x7ff0000000000001ull, 2}, {0x7ff8000000000000ull, 2}, {0xfff8000000000000ull, 2}, + }; + for (const auto& test : cases) { + double value; + std::memcpy(&value, &test.bits, sizeof(value)); + if (_Dtest_nid_postfix(&value) != test.code) { + std::fprintf(stderr, "Guest _Dtest failed for %016llx\n", static_cast(test.bits)); + std::abort(); + } + } +} + +static void CheckDoubleRounding(double value, std::int64_t expected, bool invalid) { + for (const bool useLlround : {false, true}) { + std::feclearexcept(FE_ALL_EXCEPT); + errno = 12345; + const std::int64_t result = useLlround ? llround_nid_postfix(value) : lround_nid_postfix(value); + Require(result == expected); + Require((std::fetestexcept(FE_INVALID) != 0) == invalid); + Require(errno == 12345); + } +} + +static void CheckFloatRounding(float value, std::int64_t expected, bool invalid) { + for (const bool useLlround : {false, true}) { + std::feclearexcept(FE_ALL_EXCEPT); + errno = 12345; + const std::int64_t result = useLlround ? llroundf_nid_postfix(value) : lroundf_nid_postfix(value); + Require(result == expected); + Require((std::fetestexcept(FE_INVALID) != 0) == invalid); + Require(errno == 12345); + } +} + +static void CheckLongDoubleRounding(long double value, std::int64_t expected, bool invalid) { + std::feclearexcept(FE_ALL_EXCEPT); + errno = 12345; + const std::int64_t result = llroundl_nid_postfix(value); + if (result != expected || (std::fetestexcept(FE_INVALID) != 0) != invalid || errno != 12345) { + std::fprintf(stderr, "llroundl failed for %La: expected %lld, got %lld\n", value, + static_cast(expected), static_cast(result)); + std::abort(); + } +} + +static void CheckNoConversionIsPositiveZero() { + for (const char* text : {"", " ", "-", "+", "-.", "+.", "-x", "-.e1", "-e5", "- 1", " -."}) { + char* end = nullptr; + const double parsed = strtod_nid_postfix(text, &end); + Require(parsed == 0. && !std::signbit(parsed) && end == text); + const float parsedFloat = strtof_nid_postfix(text, &end); + Require(parsedFloat == 0.f && !std::signbit(parsedFloat) && end == text); + const long double parsedLong = strtold_nid_postfix(text, &end); + Require(parsedLong == 0.L && !std::signbit(parsedLong) && end == text); + Require(!std::signbit(atof_nid_postfix(text))); + } + char* end = nullptr; + const char negativeZero[] = "-0.0x"; + const double parsed = strtod_nid_postfix(negativeZero, &end); + Require(parsed == 0. && std::signbit(parsed) && end == negativeZero + 4); + for (const char* text : {"-0", "-.0", "-0e5", "-0x0p0", "-0x"}) { + const double keptDouble = strtod_nid_postfix(text, &end); + Require(keptDouble == 0. && std::signbit(keptDouble) && end != text); + const float keptFloat = strtof_nid_postfix(text, &end); + Require(keptFloat == 0.f && std::signbit(keptFloat) && end != text); + const long double keptLong = strtold_nid_postfix(text, nullptr); + Require(keptLong == 0.L && std::signbit(keptLong)); + } + Require(strtod_nid_postfix("-inf", nullptr) == -std::numeric_limits::infinity()); + Require(std::isnan(atof_nid_postfix("nan"))); + const double noEndPointer = strtod_nid_postfix("-.", nullptr); + Require(noEndPointer == 0. && !std::signbit(noEndPointer)); + const char16_t wide[] = u"-."; + char16_t* wideEnd = nullptr; + const char16_t wideKept[] = u" -0.0x"; + const double keptWide = wcstod_nid_postfix(wideKept, &wideEnd); + Require(keptWide == 0. && std::signbit(keptWide) && wideEnd == wideKept + 5); + const double parsedWide = wcstod_nid_postfix(wide, &wideEnd); + Require(parsedWide == 0. && !std::signbit(parsedWide) && wideEnd == wide); + const float parsedWideFloat = wcstof_nid_postfix(wide, &wideEnd); + Require(parsedWideFloat == 0.f && !std::signbit(parsedWideFloat) && wideEnd == wide); + const long double parsedWideLong = wcstold_nid_postfix(wide, &wideEnd); + 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(); CheckIntegerConversions(); + CheckExtendedMath(); Require(atof_nid_postfix(" -12.5tail") == -12.5); char* end = nullptr; const char input[] = "0x1.8p+2 remainder"; @@ -211,15 +1290,52 @@ int main() { Require(std::isinf(strtof_nid_postfix("1e1000", nullptr))); Require(*__error_nid_postfix() == 34); Require(strtold_nid_postfix("1.0000000000000000001!", &end) > 1.L && *end == '!'); + CheckNanConversions(); Require(fmodf_nid_postfix(5.5f, 2.f) == 1.5f); Require(fmodf_nid_postfix(-5.5f, 2.f) == -1.5f); Require(std::signbit(fmodf_nid_postfix(-4.f, 2.f))); 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); + Require(!std::signbit(nanf_nid_postfix(""))); + Require(std::bit_cast(nan_nid_postfix(nullptr)) == 0x7ff8000000000000ull); + Require(std::bit_cast(nan_nid_postfix("")) == 0x7ff8000000000000ull); + Require(std::bit_cast(nan_nid_postfix("123")) == 0x7ff8000000000000ull); + Require(!std::signbit(nan_nid_postfix(""))); Require(std::abs(asinf_nid_postfix(0.5f) - 0.5235988f) < 0.000001f); Require(std::abs(acosf_nid_postfix(0.5f) - 1.0471976f) < 0.000001f); Require(std::abs(atan2f_nid_postfix(1.f, -1.f) - 2.3561945f) < 0.000001f); Require(tanf_nid_postfix(0.f) == 0.f); + unsigned int randomState = 1; + Require(rand_r_nid_postfix(&randomState) == 33613 && randomState == 33613u); + randomState = 0; + Require(rand_r_nid_postfix(&randomState) == 16806 && randomState == 16806u); + for (const unsigned int seed : {0u, 1u, 12345u, 0x7ffffffeu, 0xffffffffu}) { + srand_nid_postfix(seed); + randomState = seed; + for (int step = 0; step < 8; ++step) Require(rand_r_nid_postfix(&randomState) == rand_nid_postfix()); + } + bool threw = false; + 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()))); @@ -233,15 +1349,108 @@ int main() { Require(std::isinf(result) && result > 0.f && std::fetestexcept(FE_INVALID) != 0); } Require(log10f_nid_postfix(100.f) == 2.f); + Require(log1p_nid_postfix(0.) == 0. && std::signbit(log1p_nid_postfix(-0.))); + Require(log1p_nid_postfix(-1.) == -std::numeric_limits::infinity() && std::isnan(log1p_nid_postfix(-2.))); + Require(log1p_nid_postfix(std::numeric_limits::infinity()) == std::numeric_limits::infinity()); + Require(log1p_nid_postfix(1e-300) == 1e-300 && log1pf_nid_postfix(1e-30f) == 1e-30f); + Require(std::abs(log1p_nid_postfix(1.) - 0.6931471805599453) < 1e-16); + Require(std::abs(log1p_nid_postfix(1e-10) - 9.9999999995e-11) < 1e-25); + Require(std::abs(log1pf_nid_postfix(1.f) - 0.6931472f) < 1e-7f && std::isnan(log1pf_nid_postfix(-2.f))); + Require(expm1_nid_postfix(0.) == 0. && std::signbit(expm1_nid_postfix(-0.))); + Require(expm1_nid_postfix(1e-300) == 1e-300 && expm1_nid_postfix(-1000.) == -1.); + Require(expm1_nid_postfix(-std::numeric_limits::infinity()) == -1.); + Require(std::abs(expm1_nid_postfix(1.) - 1.718281828459045) < 1e-15); + Require(std::abs(expm1_nid_postfix(1e-10) - 1.00000000005e-10) < 1e-25); + Require(expm1f_nid_postfix(0.f) == 0.f && std::signbit(expm1f_nid_postfix(-0.f)) && expm1f_nid_postfix(-1000.f) == -1.f); + Require(std::abs(expm1f_nid_postfix(1.f) - 1.7182818f) < 1e-6f && std::isinf(expm1f_nid_postfix(std::numeric_limits::infinity()))); + Require(erff_nid_postfix(0.f) == 0.f && std::signbit(erff_nid_postfix(-0.f)) && erff_nid_postfix(10.f) == 1.f && erff_nid_postfix(-10.f) == -1.f); + Require(std::abs(erff_nid_postfix(0.5f) - 0.5204999f) < 1e-6f && std::isnan(erff_nid_postfix(std::numeric_limits::quiet_NaN()))); + Require(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}) { + Require(std::fesetround(mode) == 0); + CheckLongDoubleRounding(9223372036854775807.0L, INT64_MAX, false); + CheckLongDoubleRounding(9223372036854775806.5L, INT64_MAX, false); + CheckLongDoubleRounding(9223372036854775806.0L, INT64_MAX - 1, false); + CheckLongDoubleRounding(-0x1p63L, INT64_MIN, false); + CheckLongDoubleRounding(-9223372036854775807.5L, INT64_MIN, false); + CheckLongDoubleRounding(-9223372036854775807.0L, INT64_MIN + 1, false); + for (const long double value : {9223372036854775807.5L, 0x1p63L, -9223372036854775809.0L, + std::numeric_limits::infinity(), -std::numeric_limits::infinity(), + std::numeric_limits::quiet_NaN()}) { + CheckLongDoubleRounding(value, INT64_MIN, true); + } + } + Require(std::fesetround(FE_TONEAREST) == 0); Require(logbf_nid_postfix(8.f) == 3.f && logbf_nid_postfix(-0.75f) == -1.f); Require(logbf_nid_postfix(std::numeric_limits::denorm_min()) == -149.f); Require(logbf_nid_postfix(0.f) == -std::numeric_limits::infinity()); 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(); Require(scalbn_nid_postfix(0.75, -2) == 0.1875); Require(scalbnf_nid_postfix(0.75f, 4) == 12.f); + Require(nextafterf_nid_postfix(1.f, 2.f) == 0x1.000002p0f && nextafterf_nid_postfix(1.f, 0.f) == 0x1.fffffep-1f); + Require(nextafterf_nid_postfix(0.f, 1.f) == std::numeric_limits::denorm_min()); + Require(nextafterf_nid_postfix(0.f, -1.f) == -std::numeric_limits::denorm_min()); + Require(nextafterf_nid_postfix(std::numeric_limits::denorm_min(), -1.f) == 0.f); + 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); @@ -250,17 +1459,58 @@ int main() { Require(lroundf_nid_postfix(4294967296.f) == INT64_C(4294967296)); Require(lroundf_nid_postfix(2.5f) == 3); Require(llround_nid_postfix(-4294967296.5) == -INT64_C(4294967297)); + Require(llroundf_nid_postfix(2.5f) == 3 && llroundf_nid_postfix(-2.5f) == -3 && llroundf_nid_postfix(-0.4f) == 0); + Require(llroundf_nid_postfix(8589934592.f) == INT64_C(8589934592) && llroundf_nid_postfix(-0x1p62f) == -(INT64_C(1) << 62)); + Require(llroundf_nid_postfix(0.49999997f) == 0 && llroundf_nid_postfix(16777215.f) == 16777215); + Require(lround_nid_postfix(0.49999999999999994) == 0 && lround_nid_postfix(-0.49999999999999994) == 0); + constexpr double kTwo63 = 9223372036854775808.0; + constexpr double kTwo63Below = 9223372036854774784.0; + constexpr float kTwo63f = 9223372036854775808.f; + constexpr float kTwo63fBelow = 9223371487098961920.f; + CheckDoubleRounding(-kTwo63, INT64_MIN, false); + CheckDoubleRounding(kTwo63Below, INT64_C(9223372036854774784), false); + CheckDoubleRounding(-kTwo63Below, -INT64_C(9223372036854774784), false); + CheckDoubleRounding(4503599627370497.0, INT64_C(4503599627370497), false); + CheckFloatRounding(-kTwo63f, INT64_MIN, false); + CheckFloatRounding(kTwo63fBelow, INT64_C(9223371487098961920), false); + CheckFloatRounding(-kTwo63fBelow, -INT64_C(9223371487098961920), false); + for (const double value : {kTwo63, -9223374235878031360.0, 1e30, -1e30, std::numeric_limits::infinity(), -std::numeric_limits::infinity(), std::numeric_limits::quiet_NaN()}) { + CheckDoubleRounding(value, INT64_MIN, true); + CheckFloatRounding(static_cast(value), INT64_MIN, true); + } + CheckFloatRounding(kTwo63f, INT64_MIN, true); + std::feclearexcept(FE_ALL_EXCEPT); const auto infinity = std::numeric_limits::infinity(); const auto nan = std::numeric_limits::quiet_NaN(); Require(__isinff_nid_postfix(infinity) == 1 && __isinff_nid_postfix(-infinity) == 1); Require(__isinff_nid_postfix(nan) == 0 && __isinff_nid_postfix(1.f) == 0); + Require(__isinf_nid_postfix(std::numeric_limits::infinity()) == 1 && __isinf_nid_postfix(-std::numeric_limits::infinity()) == 1); + Require(__isinf_nid_postfix(std::numeric_limits::quiet_NaN()) == 0 && __isinf_nid_postfix(std::numeric_limits::max()) == 0); + 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); Require(__isnormal_nid_postfix(std::numeric_limits::denorm_min()) == 0); + CheckAsin(); const auto quotient = div_nid_postfix(7, 2); + CheckAsinfErrno(); Require(quotient.quot == 3 && quotient.rem == 1); const auto negativeNumerator = div_nid_postfix(-7, 2); Require(negativeNumerator.quot == -3 && negativeNumerator.rem == -1); @@ -268,4 +1518,53 @@ int main() { Require(negativeDenominator.quot == -3 && negativeDenominator.rem == 1); const auto minimum = div_nid_postfix(std::numeric_limits::min(), 10); Require(minimum.quot == -214748364 && minimum.rem == -8); + CheckPowfSpecialValue(); + Require(abs_nid_postfix(0) == 0); + CheckAcosfErrno(); + Require(abs_nid_postfix(42) == 42); + Require(abs_nid_postfix(-42) == 42); + Require(abs_nid_postfix(std::numeric_limits::max()) == std::numeric_limits::max()); + Require(abs_nid_postfix(std::numeric_limits::min()) == std::numeric_limits::min()); + Require(__powisf2_nid_postfix(2.0f, 10) == 1024.0f); + Require(__powisf2_nid_postfix(2.0f, -10) == 0.0009765625f); + Require(__powisf2_nid_postfix(-2.0f, 3) == -8.0f); + Require(__powisf2_nid_postfix(-2.0f, -3) == -0.125f); + Require(__powisf2_nid_postfix(-2.0f, 4) == 16.0f); + Require(__powisf2_nid_postfix(0.0f, 0) == 1.0f); + Require(__powisf2_nid_postfix(std::numeric_limits::quiet_NaN(), 0) == 1.0f); + Require(std::isnan(__powisf2_nid_postfix(std::numeric_limits::quiet_NaN(), 1))); + Require(__powisf2_nid_postfix(-1.0f, std::numeric_limits::min()) == 1.0f); + Require(__powisf2_nid_postfix(-1.0f, std::numeric_limits::max()) == -1.0f); + Require(__powisf2_nid_postfix(2.0f, std::numeric_limits::min()) == 0.0f); + Require(std::isinf(__powisf2_nid_postfix(0.0f, -1))); + Require(std::signbit(__powisf2_nid_postfix(-0.0f, 3))); + Require(std::isinf(__powisf2_nid_postfix(-0.0f, -3)) && std::signbit(__powisf2_nid_postfix(-0.0f, -3))); + Require(__powisf2_nid_postfix(2.0f, -127) == 0x1p-127f); + Require(__powidf2_nid_postfix(2.0, 10) == 1024.0); + Require(__powidf2_nid_postfix(2.0, -10) == 0.0009765625); + Require(__powidf2_nid_postfix(-2.0, 3) == -8.0); + Require(__powidf2_nid_postfix(-2.0, -3) == -0.125); + Require(__powidf2_nid_postfix(-2.0, 4) == 16.0); + Require(__powidf2_nid_postfix(0.0, 0) == 1.0); + Require(__powidf2_nid_postfix(std::numeric_limits::quiet_NaN(), 0) == 1.0); + Require(std::isnan(__powidf2_nid_postfix(std::numeric_limits::quiet_NaN(), 1))); + Require(__powidf2_nid_postfix(-1.0, std::numeric_limits::min()) == 1.0); + Require(__powidf2_nid_postfix(-1.0, std::numeric_limits::max()) == -1.0); + Require(__powidf2_nid_postfix(2.0, std::numeric_limits::min()) == 0.0); + Require(std::isinf(__powidf2_nid_postfix(0.0, -1))); + Require(std::signbit(__powidf2_nid_postfix(-0.0, 3))); + Require(std::isinf(__powidf2_nid_postfix(-0.0, -3)) && std::signbit(__powidf2_nid_postfix(-0.0, -3))); + Require(__powidf2_nid_postfix(2.0, -127) == 0x1p-127); + CheckHyperbolic(); + 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/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 892cefb408..a52c5a5e2d 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -5,7 +5,10 @@ #include "prx/libc/include/GuestWriteWatch.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" #include "prx/libkernel/KernelErrors.hpp" +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" +#include #include +#include #include "SceTypes.hpp" #include #include @@ -18,48 +21,64 @@ #include #include #include +#include #include #include #include #if defined(__linux__) #include +#include #include #include #include +#include #include #endif #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN #include +#include +#include +#include #endif extern "C" { void* APS5_VABI mmap_nid_postfix(void*, std::size_t, int, int, int, std::int64_t) noexcept; int APS5_VABI munmap_nid_postfix(void*, std::size_t) noexcept; +int APS5_VABI sceKernelMmap(void*, std::size_t, int, int, int, std::int64_t, void**); int APS5_VABI mprotect_nid_postfix(void*, std::size_t, int) noexcept; +int APS5_VABI madvise_nid_postfix(void*, std::size_t, int); +int APS5_VABI 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*); int APS5_VABI sceKernelAvailableFlexibleMemorySize(std::size_t*); +int APS5_VABI sceKernelInternalMemoryGetAvailableSize(std::size_t*); int APS5_VABI sceKernelMapFlexibleMemory(void**, std::size_t, int, int); int APS5_VABI sceKernelMunmap(void*, std::size_t); int APS5_VABI sceKernelReleaseFlexibleMemory(void*, std::size_t); int APS5_VABI sceKernelMprotect(const void*, std::size_t, int); int APS5_VABI sceKernelMtypeprotect(const void*, std::size_t, int, int); int APS5_VABI sceKernelBatchMap2(KernelBatchMapEntry*, int, int*, int); +int APS5_VABI sceKernelMemoryPoolBatch(const KernelMemoryPoolBatchEntry*, int, int*, int); int APS5_VABI sceKernelVirtualQuery(const void*, int, VirtualQueryInfo*, std::uint64_t); int APS5_VABI sceKernelSetVirtualRangeName(const void*, std::uint64_t, const char*); int APS5_VABI sceKernelClearVirtualRangeName(const void*, std::uint64_t); +int APS5_VABI sceKernelGetPageTableStats(int*, int*, int*, int*); int APS5_VABI sceKernelAllocateDirectMemory(std::int64_t, std::int64_t, std::size_t, std::size_t, int, std::int64_t*); int APS5_VABI sceKernelMapDirectMemory(void**, std::size_t, int, int, std::int64_t, std::size_t); +int APS5_VABI sceKernelEnableDmemAliasing(); int APS5_VABI sceKernelReleaseDirectMemory(std::int64_t, std::size_t); +int APS5_VABI sceKernelAvailableDirectMemorySize(std::int64_t, std::int64_t, std::size_t, std::int64_t*, std::size_t*); int APS5_VABI sceKernelCheckedReleaseDirectMemory(std::int64_t, std::size_t); int APS5_VABI sceKernelReserveVirtualRange(void**, std::size_t, int, std::size_t); int APS5_VABI sceKernelMemoryPoolReserve(void*, std::size_t, std::size_t, int, void**); +int APS5_VABI sceKernelMemoryPoolCommit(void*, std::size_t, int, 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 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*); @@ -67,6 +86,9 @@ int APS5_VABI sceKernelAioDeleteRequest(std::int32_t, std::int32_t*); int APS5_VABI sceKernelMlock_nid_postfix(void*, std::uint64_t); int APS5_VABI sceKernelGetDirectMemoryType(std::int64_t, int*, std::int64_t*, std::int64_t*); int APS5_VABI sceKernelBatchMap2(KernelBatchMapEntry*, int, int*, int); +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 condition, std::source_location location = std::source_location::current()) { @@ -99,6 +121,46 @@ static void CheckReleaseFlexibleMemory() { Require(sceKernelMunmap(again, length) == 0); } +static void CheckFlexibleMappingIgnoresFlag8000() { + constexpr std::size_t length = 0x10000; + std::size_t before = 0; + std::size_t available = 0; + Require(sceKernelAvailableFlexibleMemorySize(&before) == 0); + void* mapped = nullptr; + Require(sceKernelMapFlexibleMemory(&mapped, length, 3, 0x8000) == 0 && mapped != nullptr); + static_cast(mapped)[length - 1] = 1; + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before - length); + Require(sceKernelMunmap(mapped, length) == 0); + void* again = mapped; + Require(sceKernelMapFlexibleMemory(&again, length, 3, 0x8090) == 0 && again == mapped); + Require(static_cast(again)[length - 1] == 0); + Require(sceKernelMunmap(again, length) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); + bool rejected = false; + void* unknown = nullptr; + try { sceKernelMapFlexibleMemory(&unknown, length, 3, 0x20000); } catch (const std::exception&) { rejected = true; } + Require(rejected && unknown == nullptr); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); +} + +static void CheckPageTableStats() { + int cpuTotal = -1, cpuAvailable = -1, gpuTotal = -1, gpuAvailable = -1; + Require(sceKernelGetPageTableStats(&cpuTotal, &cpuAvailable, &gpuTotal, &gpuAvailable) == 0); + Require(cpuTotal > 0 && gpuTotal > 0); + Require(cpuAvailable >= 0 && cpuAvailable <= cpuTotal); + Require(gpuAvailable >= 0 && gpuAvailable <= gpuTotal); + Require(sceKernelGetPageTableStats(nullptr, &cpuAvailable, &gpuTotal, &gpuAvailable) == SCE_KERNEL_ERROR_EFAULT); + Require(sceKernelGetPageTableStats(&cpuTotal, nullptr, &gpuTotal, &gpuAvailable) == SCE_KERNEL_ERROR_EFAULT); + Require(sceKernelGetPageTableStats(&cpuTotal, &cpuAvailable, nullptr, &gpuAvailable) == SCE_KERNEL_ERROR_EFAULT); + Require(sceKernelGetPageTableStats(&cpuTotal, &cpuAvailable, &gpuTotal, nullptr) == SCE_KERNEL_ERROR_EFAULT); +} + +static void CheckInternalMemoryAvailableSize() { + std::size_t available = 1; + Require(sceKernelInternalMemoryGetAvailableSize(&available) == 0 && available == 0); + Require(sceKernelInternalMemoryGetAvailableSize(nullptr) == static_cast(0x80020016u)); +} + static void CheckNamedAndHintedMappings() { constexpr std::size_t length = 0x10000; void* first = nullptr; @@ -144,6 +206,87 @@ 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 CheckInternalNamedFlexibleMapping() { constexpr std::size_t length = 0x10000; std::size_t before = 0; @@ -168,6 +311,74 @@ 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(); + std::size_t before = 0; + std::size_t available = 0; + Require(sceKernelAvailableFlexibleMemorySize(&before) == 0); + void* module = dlopen_nid_postfix(path.c_str(), 2); + Require(module != nullptr); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available + moduleData <= before); + void* mapped = nullptr; + Require(sceKernelMapFlexibleMemory(&mapped, before, 3, 0) == SCE_KERNEL_ERROR_ENOMEM && mapped == nullptr); + Require(dlclose_nid_postfix(module) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); +} + static void CheckBatchMapStopsAtInvalidEntry() { constexpr std::size_t page = 0x4000; constexpr int mapFlexible = 3; @@ -200,6 +411,57 @@ static void CheckBatchMapStopsAtInvalidEntry() { Require(sceKernelMunmap(entries[0].start, page) == 0); } +static void CheckVirtualQueryFindsNeighbours() { + constexpr std::size_t page = 0x4000; + void* low = nullptr; + void* high = nullptr; + Require(sceKernelMapNamedFlexibleMemory(&low, page * 4, 3, 0, "neighbours") == 0 && low != nullptr); + Require(sceKernelMapFlexibleMemory(&high, page, 3, 0) == 0 && high != nullptr); + auto* lowBytes = static_cast(low); + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(lowBytes + page, 0, &info, sizeof(info)) == 0); + Require(info.start == reinterpret_cast(low) && info.end == reinterpret_cast(low) + page * 4); + Require(sceKernelMprotect(lowBytes + page, page, 1) == 0); + Require(sceKernelVirtualQuery(lowBytes + page, 0, &info, sizeof(info)) == 0); + Require(info.start == reinterpret_cast(low) + page && info.end == reinterpret_cast(low) + page * 2 && info.protection == 1); + Require(sceKernelVirtualQuery(lowBytes + page * 2, 0, &info, sizeof(info)) == 0); + Require(info.start == reinterpret_cast(low) + page * 2 && info.end == reinterpret_cast(low) + page * 4 && info.protection == 3); + Require(sceKernelMunmap(lowBytes + page * 2, page) == 0); + Require(sceKernelVirtualQuery(lowBytes + page * 2, 0, &info, sizeof(info)) != 0); + Require(sceKernelVirtualQuery(lowBytes + page * 2, 1, &info, sizeof(info)) == 0); + Require(info.start == reinterpret_cast(low) + page * 3 && info.end == reinterpret_cast(low) + page * 4); + Require(sceKernelVirtualQuery(lowBytes + page * 3, 0, &info, sizeof(info)) == 0 && info.start == reinterpret_cast(low) + page * 3); + Require(sceKernelMunmap(low, page * 2) == 0); + Require(sceKernelMunmap(lowBytes + page * 3, page) == 0); + Require(sceKernelMunmap(high, page) == 0); +} + +static void CheckReleaseFindsEveryDirectMapping() { + constexpr std::size_t page = 0x4000; + constexpr std::size_t pieces = 48; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * pieces, 0, 0, &phys) == 0); + void* mapped[pieces] = {}; + for (std::size_t index = 0; index < pieces; ++index) { + const auto piece = pieces - 1 - index; + Require(sceKernelMapDirectMemory(&mapped[piece], page, 3, 0, phys + static_cast(piece * page), 0) == 0); + static_cast(mapped[piece])[0] = static_cast(piece); + } + void* alias = nullptr; + Require(sceKernelMapDirectMemory(&alias, page * 2, 3, 0, phys + static_cast(page * 10), 0) == 0); + Require(static_cast(alias)[0] == 10 && static_cast(alias)[page] == 11); + Require(sceKernelReleaseDirectMemory(phys + static_cast(page * 10), page * 3) == 0); + VirtualQueryInfo info{}; + for (std::size_t index = 0; index < pieces; ++index) { + const bool released = index >= 10 && index < 13; + Require((sceKernelVirtualQuery(mapped[index], 0, &info, sizeof(info)) == 0) != released); + } + Require(sceKernelVirtualQuery(alias, 0, &info, sizeof(info)) != 0); + Require(sceKernelReleaseDirectMemory(phys, page * 10) == 0); + Require(sceKernelReleaseDirectMemory(phys + static_cast(page * 13), page * (pieces - 13)) == 0); + for (std::size_t index = 0; index < pieces; ++index) Require(sceKernelVirtualQuery(mapped[index], 0, &info, sizeof(info)) != 0); +} + static void CheckCheckedReleaseDirectMemory() { constexpr std::size_t page = 0x4000; std::int64_t phys = 0; @@ -225,6 +487,7 @@ static void CheckDirectMemoryFollowsPhysicalPages() { Require(sceKernelMapDirectMemory(&first, page * 2, 3, 0, phys, 0) == 0); static_cast(first)[0] = 11; static_cast(first)[page + 5] = 22; + Require(sceKernelEnableDmemAliasing() == 0); void* alias = nullptr; Require(sceKernelMapDirectMemory(&alias, page, 3, 0, phys + page, 0) == 0); Require(alias != first && static_cast(alias)[5] == 22); @@ -264,6 +527,147 @@ static void CheckDirectMemoryFollowsPhysicalPages() { Require(sceKernelReleaseDirectMemory(again, page * 2) == 0); } +static void CheckSparseDirectMemoryBacking() { + constexpr std::size_t page = 0x4000; + constexpr std::size_t size = 256u * 1024u * 1024u; + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, size, 0, 0, &physical) == 0); + void* first = nullptr; + void* last = nullptr; + Require(sceKernelMapDirectMemory(&first, page, 3, 0, physical, 0) == 0); + Require(sceKernelMapDirectMemory(&last, page, 3, 0, physical + size - page, 0) == 0); + auto* firstBytes = static_cast(first); + auto* lastBytes = static_cast(last); + Require(firstBytes[0] == 0 && firstBytes[page - 1] == 0 && lastBytes[0] == 0 && lastBytes[page - 1] == 0); + firstBytes[0] = 17; + lastBytes[page - 1] = 29; + void* alias = nullptr; + Require(sceKernelMapDirectMemory(&alias, page, 3, 0, physical + size - page, 0) == 0); + auto* aliasBytes = static_cast(alias); + Require(aliasBytes[page - 1] == 29 && aliasBytes[0] == 0); + aliasBytes[page - 1] = 41; + Require(lastBytes[page - 1] == 41 && firstBytes[0] == 17); + Require(sceKernelMunmap(last, page) == 0 && sceKernelMunmap(alias, page) == 0); + last = nullptr; + Require(sceKernelMapDirectMemory(&last, page, 3, 0, physical + size - page, 0) == 0); + Require(static_cast(last)[page - 1] == 41); + Require(sceKernelMunmap(first, page) == 0 && sceKernelMunmap(last, page) == 0); + 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; @@ -278,13 +682,30 @@ static void CheckReleaseDirectMemoryClearsMappings() { Require(sceKernelVirtualQuery(mapped, 0, &split, sizeof(split)) == 0); Require(split.is_direct && split.offset == static_cast(phys)); VirtualQueryInfo dropped{}; - Require(sceKernelVirtualQuery(static_cast(mapped) + page, 0, &dropped, sizeof(dropped)) == 0); - Require(!dropped.is_direct && dropped.offset == 0); + const int droppedResult = sceKernelVirtualQuery(static_cast(mapped) + page, 0, &dropped, sizeof(dropped)); + Require(droppedResult != 0 || !dropped.is_direct); + void* alias = nullptr; + Require(sceKernelMapDirectMemory(&alias, page, 3, 0, phys, 0) == 0); Require(sceKernelReleaseDirectMemory(phys, page) == 0); - VirtualQueryInfo cleared{}; - Require(sceKernelVirtualQuery(mapped, 0, &cleared, sizeof(cleared)) == 0); - Require(!cleared.is_direct && cleared.offset == 0); - Require(sceKernelMunmap(mapped, page * 2) == 0); + for (void* view : {mapped, alias}) { + VirtualQueryInfo cleared{}; + Require(sceKernelVirtualQuery(view, 0, &cleared, sizeof(cleared)) != 0); + VirtualQueryInfo next{}; + if (sceKernelVirtualQuery(view, 1, &next, sizeof(next)) == 0) Require(next.start != reinterpret_cast(view)); + } +} + +static void CheckReleaseDirectMemoryRejectsInvalidRanges() { + constexpr std::size_t page = 0x4000; + Require(sceKernelReleaseDirectMemory(-1, page) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelReleaseDirectMemory(0, 0) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelReleaseDirectMemory(0, 1) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelReleaseDirectMemory(0, page - 1) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelReleaseDirectMemory(1, page) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelReleaseDirectMemory(0, std::numeric_limits::max()) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelReleaseDirectMemory(0, DIRECT_MEMORY_SIZE + page) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelReleaseDirectMemory(DIRECT_MEMORY_SIZE - page, page * 2) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelReleaseDirectMemory(DIRECT_MEMORY_SIZE, page) == SCE_KERNEL_ERROR_EINVAL); } static void CheckFixedMappingReplacesPartialOverlap() { @@ -308,6 +729,26 @@ static void CheckFixedMappingReplacesPartialOverlap() { Require(sceKernelReleaseDirectMemory(phys, page * 3) == 0); } +static void CheckFixedMappingOverHoleInsideAllocation() { + 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* bytes = static_cast(mapped); + bytes[7] = 0x11; + bytes[page * 3 + 7] = 0x44; + Require(sceKernelMunmap(bytes + page, page) == 0); + void* fixed = bytes + page; + Require(sceKernelMapDirectMemory(&fixed, page * 2, 3, 0x10, phys + static_cast(page), 0) == 0); + Require(fixed == bytes + page); + bytes[page * 2 + 7] = 0x33; + Require(bytes[7] == 0x11 && bytes[page * 2 + 7] == 0x33 && bytes[page * 3 + 7] == 0x44); + Require(sceKernelMunmap(bytes + page * 2, page * 2) == 0); + Require(sceKernelMunmap(mapped, page * 4) == 0); + Require(sceKernelReleaseDirectMemory(phys, page * 4) == 0); +} + static void CheckGetDirectMemoryType() { constexpr std::size_t page = 0x4000; std::int64_t first = 0; @@ -374,6 +815,29 @@ static void CheckMtypeprotect() { check(0, 0, 3); check(1, 3, 1); check(2, 5, 3); + KernelMemoryPoolBatchEntry poolEntry{}; + poolEntry.op = 4; + poolEntry.type_protect.addr = bytes + page * 2; + poolEntry.type_protect.len = page; + poolEntry.type_protect.prot = 1; + poolEntry.type_protect.type = 4; + processed = -1; + Require(sceKernelMemoryPoolBatch(&poolEntry, 1, &processed, 0) == 0 && processed == 1); + check(1, 3, 1); + check(2, 4, 1); + Require(sceKernelMemoryPoolCommit(bytes + page * 2, page, 6, 3, 0) == 0); + check(2, 6, 3); + check(1, 3, 1); + KernelMemoryPoolBatchEntry commitEntry{}; + commitEntry.op = 1; + commitEntry.commit.addr = bytes + page * 2; + commitEntry.commit.len = page; + commitEntry.commit.prot = 1; + commitEntry.commit.type = 7; + processed = -1; + Require(sceKernelMemoryPoolBatch(&commitEntry, 1, &processed, 0) == 0 && processed == 1); + check(1, 3, 1); + check(2, 7, 1); Require(sceKernelMtypeprotect(bytes, page * 3, 2, 3) == 0); VirtualQueryInfo whole{}; Require(sceKernelVirtualQuery(bytes, 0, &whole, sizeof(whole)) == 0); @@ -398,6 +862,148 @@ static void CheckDirectMemoryGpuProtBits() { Require(sceKernelReleaseDirectMemory(phys, page) == 0); } +static bool GpuMapped(const void* pointer) { + GuestAllocations::Mutation mutation; + return mutation.Find(pointer).gpu; +} + +static void CheckGpuAccessFollowsProtection() { + constexpr std::size_t page = 0x4000; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 0, &phys) == 0); + void* cpu = nullptr; + Require(sceKernelMapDirectMemory(&cpu, page, 3, 0, phys, 0) == 0); + Require(!GpuMapped(cpu)); + void* gpu = nullptr; + Require(sceKernelMapDirectMemory(&gpu, page, 0x32, 0, phys + static_cast(page), 0) == 0); + Require(GpuMapped(gpu)); + Require(sceKernelMprotect(cpu, page, 0x13) == 0); + Require(GpuMapped(cpu)); + Require(sceKernelMprotect(gpu, page, 3) == 0); + Require(!GpuMapped(gpu)); + static int imageProbe = 0; + const auto probe = reinterpret_cast(&imageProbe); + { + GuestAllocations::Mutation mutation; + mutation.RegisterMainImage(); + } + { + const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + const auto image = std::find_if(lease.begin(), lease.end(), [&](const auto& range) { return probe >= range->address && probe - range->address < range->bytes; }); + Require(image != lease.end() && !(*image)->releasable && !(*image)->gpu); + } + Require(sceKernelMunmap(cpu, page) == 0); + Require(sceKernelMunmap(gpu, page) == 0); + Require(sceKernelReleaseDirectMemory(phys, page * 2) == 0); + void* flexible = nullptr; + Require(sceKernelMapNamedFlexibleMemory(&flexible, page, 3, 0, "cpu") == 0); + Require(!GpuMapped(flexible)); + Require(sceKernelMunmap(flexible, page) == 0); + Require(sceKernelMapNamedFlexibleMemory(&flexible, page, 0x33, 0, "gpu") == 0); + Require(GpuMapped(flexible)); + Require(sceKernelMunmap(flexible, page) == 0); +} + +static std::vector> gpuMapped; + +static std::atomic observerEntered{false}; +static std::atomic observerDone{false}; + +static void CheckClearingTheObserverWaitsForItsCalls() { + GuestAllocations::GuestAllocationsSetGpuMapObserver_nid_postfix([](const GuestAllocations::Mapped&, std::uint64_t) { + observerEntered = true; + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + observerDone = true; + }); + constexpr std::size_t page = 0x4000; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page, 0, 0, &phys) == 0); + void* gpu = nullptr; + std::thread mapper([&] { Require(sceKernelMapDirectMemory(&gpu, page, 0x32, 0, phys, 0) == 0); }); + while (!observerEntered) std::this_thread::yield(); + GuestAllocations::GuestAllocationsSetGpuMapObserver_nid_postfix(nullptr); + Require(observerDone); + mapper.join(); + Require(sceKernelMunmap(gpu, page) == 0); + Require(sceKernelReleaseDirectMemory(phys, page) == 0); +} + +static void CheckGpuMapsAreObserved() { + GuestAllocations::GuestAllocationsSetGpuMapObserver_nid_postfix([](const GuestAllocations::Mapped& mapped, std::uint64_t) { + const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + for (const auto& weak : mapped) { + const auto range = weak.lock(); + if (range == nullptr) continue; + const bool registered = std::any_of(lease.begin(), lease.end(), [&](const auto& current) { return current == range; }); + gpuMapped.emplace_back(registered && range->gpu ? range->address : 0, range->bytes); + } + }); + constexpr std::size_t page = 0x4000; + void* heap = GuestHeap::GuestHeapAlign_nid_postfix(page, page); + Require(heap != nullptr && gpuMapped.empty()); + GuestHeap::GuestHeapFree_nid_postfix(heap); + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 0, &phys) == 0); + void* cpu = nullptr; + Require(sceKernelMapDirectMemory(&cpu, page, 3, 0, phys, 0) == 0); + Require(gpuMapped.empty()); + void* gpu = nullptr; + Require(sceKernelMapDirectMemory(&gpu, page, 0x32, 0, phys + static_cast(page), 0) == 0); + Require(gpuMapped.size() == 1 && gpuMapped.front().first == reinterpret_cast(gpu) && gpuMapped.front().second == page); + Require(sceKernelMprotect(cpu, page, 0x13) == 0); + Require(gpuMapped.size() == 1); + std::int64_t remapPhys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 0, &remapPhys) == 0); + void* reserved = nullptr; + Require(sceKernelReserveVirtualRange(&reserved, page * 2, 0, 0) == 0); + void* cpuRemap = reserved; + Require(sceKernelMapDirectMemory(&cpuRemap, page, 3, 0x10, remapPhys, 0) == 0 && cpuRemap == reserved); + Require(gpuMapped.size() == 1); + void* gpuRemap = static_cast(reserved) + page; + void* const gpuTarget = gpuRemap; + Require(sceKernelMapDirectMemory(&gpuRemap, page, 0x32, 0x10, remapPhys + static_cast(page), 0) == 0 && gpuRemap == gpuTarget); + Require(gpuMapped.size() == 2 && gpuMapped.back().first == reinterpret_cast(gpuTarget) && gpuMapped.back().second == page); + GuestAllocations::GuestAllocationsSetGpuMapObserver_nid_postfix(nullptr); + Require(sceKernelMunmap(reserved, page * 2) == 0); + Require(sceKernelReleaseDirectMemory(remapPhys, page * 2) == 0); + Require(sceKernelMunmap(cpu, page) == 0); + Require(sceKernelMunmap(gpu, page) == 0); + Require(sceKernelReleaseDirectMemory(phys, page * 2) == 0); +} + +static void CheckHintedVirtualReservation() { + constexpr std::size_t page = 0x4000; + void* probe = nullptr; + Require(sceKernelReserveVirtualRange(&probe, page * 8, 0, 0) == 0); + Require(sceKernelMunmap(probe, page * 8) == 0); + void* const hint = static_cast(probe) + page * 2; + void* placed = hint; + Require(sceKernelReserveVirtualRange(&placed, page * 2, 0, 0) == 0); + Require(placed == hint); + void* above = hint; + Require(sceKernelReserveVirtualRange(&above, page * 2, 0, 0) == 0); + Require(above > hint); + Require(sceKernelMunmap(above, page * 2) == 0); + 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; @@ -446,6 +1052,37 @@ static void CheckFixedVirtualReservation() { Require(sceKernelMunmap(pooled, page * 2) == 0); } +static void CheckHintInsideReservation() { + constexpr std::size_t page = 0x4000; + void* reserved = nullptr; + Require(sceKernelReserveVirtualRange(&reserved, page * 4, 0, 0) == 0); + auto* base = static_cast(reserved); + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 3, 0, 0, &phys) == 0); + void* direct = base + page; + Require(sceKernelMapDirectMemory(&direct, page, 3, 0, phys, 0) == 0); + Require(direct == base + page); + static_cast(direct)[0] = 21; + void* flexible = base + page * 2; + Require(sceKernelMapFlexibleMemory(&flexible, page, 3, 0) == 0); + Require(flexible == base + page * 2); + static_cast(flexible)[0] = 22; + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(direct, 0, &info, sizeof(info)) == 0); + Require(info.is_direct && info.offset == static_cast(phys)); + Require(sceKernelVirtualQuery(flexible, 0, &info, sizeof(info)) == 0); + Require(info.is_flexible); + Require(sceKernelVirtualQuery(base + page * 3, 0, &info, sizeof(info)) == 0); + Require(!info.is_committed && info.protection == 0); + void* overlapping = base + page; + Require(sceKernelMapDirectMemory(&overlapping, page * 2, 3, 0, phys + page, 0) == 0); + Require(overlapping != base + page); + Require(static_cast(direct)[0] == 21 && static_cast(flexible)[0] == 22); + Require(sceKernelMunmap(overlapping, page * 2) == 0); + Require(sceKernelMunmap(reserved, page * 4) == 0); + Require(sceKernelReleaseDirectMemory(phys, page * 3) == 0); +} + static void CheckReservedRangeIsNotCommitted() { constexpr std::size_t page = 0x4000; void* reserved = nullptr; @@ -493,13 +1130,18 @@ static void CheckNoOverwriteRefusesLiveMapping() { } #ifdef _WIN32 +constexpr std::uint32_t ReadWriteProtection = PAGE_READWRITE; +#else +constexpr std::uint32_t ReadWriteProtection = 0x04; +#endif + static void CheckNoOverwriteRejectsHostOccupiedMapping() { constexpr std::size_t page = 0x4000; void* reservation = nullptr; Require(sceKernelReserveVirtualRange(&reservation, page * 4, 0, 0) == 0); Require(sceKernelMunmap(reservation, page * 4) == 0); void* target = static_cast(reservation) + page; - GuestArena::GuestArenaCommit_nid_postfix(target, page, PAGE_READWRITE, page); + GuestArena::GuestArenaCommit_nid_postfix(target, page, ReadWriteProtection, page); std::int64_t phys = 0; Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page, 0, 0, &phys) == 0); void* fixed = target; @@ -514,13 +1156,275 @@ static void CheckNoOverwriteRejectsHostOccupiedMapping() { Require(sceKernelReleaseDirectMemory(phys, page) == 0); } -static void CheckFixedMappingsReachTheApplicationAreaEnd() { +#ifdef _WIN32 +static DWORD ImageProtection(const void* pointer) { + MEMORY_BASIC_INFORMATION memory{}; + 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")); + Require(data != nullptr); + auto* target = reinterpret_cast((reinterpret_cast(data) + page - 1) & ~(page - 1)); + Require(target + page <= data + 0x10000); + return target; +} + +static bool RegisteredGuestRange(const void* pointer, std::size_t bytes) { + GuestAllocations::Mutation mutation; + 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(); + 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(ImageProtection(target) == PAGE_READONLY); + Require(sceKernelMprotect(target, page, 3) == 0); + Require(ImageProtection(target) == PAGE_READWRITE || ImageProtection(target) == PAGE_WRITECOPY); + target[1] = 7; + Require(target[1] == 7); + Require(sceKernelMprotect(target, page, 1) == 0); + Require(ImageProtection(target) == PAGE_READONLY); + + { + auto held = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + bool failed = false; + try { + dlclose_nid_postfix(module); + } catch (const std::runtime_error&) { + failed = true; + } + Require(failed); + Require(GuestModulePage(module, page) == target); + Require(RegisteredGuestRange(target, page)); + Require(ImageProtection(target) == PAGE_READONLY); + } + + auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + std::atomic closed{false}; + std::thread closer([&] { + Require(dlclose_nid_postfix(module) == 0); + closed = true; + }); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + Require(!closed); + Require(ImageProtection(target) == PAGE_READONLY); + lease.clear(); + closer.join(); + Require(!RegisteredGuestRange(target, page)); + { + GuestAllocations::Mutation mutation; + mutation.Add(target, page, true, true, false); + mutation.Remove(target); + } + + 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(ImageProtection(target) == PAGE_READONLY); + Require(RegisteredGuestRange(target, page)); + Require(dlclose_nid_postfix(module) == 0); + Require(!RegisteredGuestRange(target, page)); + + bool rejected = false; + try { + sceKernelMprotect(GetModuleHandleA("kernel32.dll"), page, 1); + } catch (const std::invalid_argument&) { + rejected = true; + } + Require(rejected); + + rejected = false; + try { + const auto libkernel = GetModuleHandleA("libkernel.prx"); + Require(libkernel != nullptr); + sceKernelMprotect(libkernel, page, 1); + } catch (const std::invalid_argument&) { + rejected = true; + } + Require(rejected); + + const auto prxPath = std::filesystem::path(path).parent_path() / "guest_memory_module_foreign.prx"; + std::filesystem::copy_file(path, prxPath, std::filesystem::copy_options::overwrite_existing); + void* foreignModule = dlopen_nid_postfix(prxPath.generic_string().c_str(), 2); + Require(foreignModule != nullptr); + auto* foreignTarget = GuestModulePage(foreignModule, page); + Require(!RegisteredGuestRange(foreignTarget, page)); + Require(sceKernelMprotect(foreignTarget, page, 1) == 0); + Require(ImageProtection(foreignTarget) == PAGE_READONLY); + Require(RegisteredGuestRange(foreignTarget, page)); + Require(sceKernelMprotect(foreignTarget, page, 3) == 0); + Require(ImageProtection(foreignTarget) == PAGE_READWRITE || ImageProtection(foreignTarget) == PAGE_WRITECOPY); + Require(dlclose_nid_postfix(foreignModule) == 0); + Require(!RegisteredGuestRange(foreignTarget, page)); + std::filesystem::remove(prxPath); + + const auto plainPath = std::filesystem::relative(GUEST_MEMORY_MODULE).generic_string(); + void* plainModule = dlopen_nid_postfix(plainPath.c_str(), 2); + Require(plainModule != nullptr); + auto* plainTarget = GuestModulePage(plainModule, page); + rejected = false; + try { + sceKernelMprotect(plainTarget, page, 1); + } catch (const std::invalid_argument&) { + rejected = true; + } + Require(rejected); + Require(!RegisteredGuestRange(plainTarget, page)); + Require(dlclose_nid_postfix(plainModule) == 0); +} +#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() { + constexpr std::size_t page = 0x4000; + std::uintptr_t windowStart = 0; + std::size_t windowSize = 0; + GuestArena::GuestArenaRange_nid_postfix(&windowStart, &windowSize); + const auto windowEnd = windowStart + windowSize; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page, 0, 0, &phys) == 0); + void* direct = nullptr; + Require(sceKernelMapDirectMemory(&direct, page, 3, 0, phys, 0) == 0); + Require(direct != nullptr); + const auto directAddress = reinterpret_cast(direct); + Require(directAddress >= windowStart); + Require(directAddress + page <= windowEnd); + void* flexible = nullptr; + Require(sceKernelMapNamedFlexibleMemory(&flexible, page, 3, 0, "arena-window-test") == 0); + Require(flexible != nullptr); + const auto flexibleAddress = reinterpret_cast(flexible); + Require(flexibleAddress >= windowStart); + Require(flexibleAddress + page <= windowEnd); + Require(sceKernelMunmap(direct, page) == 0); + Require(sceKernelMunmap(flexible, page) == 0); + Require(sceKernelReleaseDirectMemory(phys, page) == 0); +} + +#if defined(__linux__) +static void CheckUnmappedArenaRangeStaysReserved() { + constexpr std::size_t page = 0x4000; + void* address = nullptr; + Require(sceKernelMapNamedFlexibleMemory(&address, page, 3, 0, "reserved-after-unmap") == 0); + Require(address != nullptr); + Require(sceKernelMunmap(address, page) == 0); + void* hostile = mmap(address, page, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0); + Require(hostile == MAP_FAILED && errno == EEXIST); + void* again = nullptr; + Require(sceKernelMapNamedFlexibleMemory(&again, page, 3, 0, "reserved-after-unmap-2") == 0); + Require(again != nullptr); + Require(sceKernelMunmap(again, page) == 0); +} +#endif + +static void CheckArenaPlacesUnhintedMappingsInsideTheWindow() { constexpr std::size_t page = 0x4000; - constexpr std::uintptr_t applicationAreaEnd = 0xFC00000000ull; std::uintptr_t base = 0; std::size_t size = 0; GuestArena::GuestArenaRange_nid_postfix(&base, &size); - Require(base == 0x200000000ull && base + size == applicationAreaEnd); + Require(size != 0); + auto* placed = GuestArena::GuestArenaAllocate_nid_postfix(page, page); + Require(placed != nullptr); + const auto address = reinterpret_cast(placed); + Require(address >= base); + Require(address + page <= base + size); + GuestArena::GuestArenaRelease_nid_postfix(placed, page); +} + +static void CheckArenaReusesAFreedRange() { + constexpr std::size_t page = 0x4000; + auto* first = GuestArena::GuestArenaAllocate_nid_postfix(page, page); + Require(first != nullptr); + GuestArena::GuestArenaRelease_nid_postfix(first, page); + auto* second = GuestArena::GuestArenaAllocate_nid_postfix(page, page); + Require(second == first); + GuestArena::GuestArenaRelease_nid_postfix(second, page); +} + +static void CheckArenaHonoursHintsWithoutGoingBelowThem() { + constexpr std::size_t page = 0x4000; + std::uintptr_t base = 0; + std::size_t size = 0; + GuestArena::GuestArenaRange_nid_postfix(&base, &size); + const auto hint = base + page; + auto* placed = GuestArena::GuestArenaAllocateAtOrAbove_nid_postfix(hint, page, page); + Require(placed != nullptr); + Require(reinterpret_cast(placed) >= hint); + Require(GuestArena::GuestArenaContains_nid_postfix(placed, page)); + GuestArena::GuestArenaRelease_nid_postfix(placed, page); +} + +static void CheckFixedMappingsReachTheApplicationAreaEnd() { + constexpr std::size_t page = 0x4000; + std::uintptr_t base = 0; + std::size_t size = 0; + GuestArena::GuestArenaRange_nid_postfix(&base, &size); + constexpr std::uintptr_t expectedOffset = +#ifdef ANYPS5_ENABLE_ASAN + 0x0000100000000000ull; +#else + 0; +#endif + constexpr std::uintptr_t applicationAreaEnd = 0xFC00000000ull + expectedOffset; + Require(base == 0x200000000ull + expectedOffset && base + size == applicationAreaEnd); std::int64_t phys = 0; Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 0, &phys) == 0); void* const requested = reinterpret_cast(applicationAreaEnd - page * 2); @@ -545,9 +1449,105 @@ static void CheckFixedMappingsReachTheApplicationAreaEnd() { Require(sceKernelMunmap(mapped, page * 2) == 0); Require(sceKernelReleaseDirectMemory(phys, page * 2) == 0); } +#ifdef _WIN32 + +static void CheckAliasOfReorderedSharedPages() { + constexpr std::size_t page = 0x4000; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 0, &phys) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, page * 2, 3, 0, phys, 0) == 0); + auto* bytes = static_cast(mapped); + bytes[5] = 0x11; + bytes[page + 5] = 0x22; + void* low = mapped; + Require(sceKernelMapDirectMemory(&low, page, 3, 0x10, phys + page, 0) == 0); + void* high = static_cast(mapped) + page; + Require(sceKernelMapDirectMemory(&high, page, 3, 0x10, phys, 0) == 0); + Require(bytes[5] == 0x22 && bytes[page + 5] == 0x11); + auto* alias = static_cast(GuestArena::GuestArenaMapAlias_nid_postfix(reinterpret_cast(mapped), page * 2)); + Require(alias != nullptr && alias[5] == 0x22 && alias[page + 5] == 0x11); + alias[6] = 0x33; + alias[page + 6] = 0x44; + Require(bytes[6] == 0x33 && bytes[page + 6] == 0x44); + GuestArena::GuestArenaUnmapAlias_nid_postfix(const_cast(alias)); + Require(bytes[5] == 0x22 && bytes[page + 5] == 0x11); + Require(sceKernelMunmap(mapped, page * 2) == 0); + Require(sceKernelReleaseDirectMemory(phys, page * 2) == 0); +} + +static void CheckAliasAcrossDirectAllocations() { + constexpr std::size_t page = 0x4000; + std::int64_t first = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 0, &first) == 0); + std::int64_t second = 0; + Require(sceKernelAllocateDirectMemory(first + page * 2, first + page * 3, page, 0, 0, &second) == 0 && second == first + page * 2); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, page * 3, 3, 0, first, 0) == 0); + auto* bytes = static_cast(mapped); + for (std::size_t index = 0; index < 3; ++index) bytes[page * index + 5] = static_cast(0x11 * (index + 1)); + auto* alias = static_cast(GuestArena::GuestArenaMapAlias_nid_postfix(reinterpret_cast(mapped), page * 3)); + Require(alias != nullptr); + MEMORY_BASIC_INFORMATION run{}; + Require(VirtualQuery(alias, &run, sizeof(run)) == sizeof(run) && run.RegionSize == page * 2); + Require(VirtualQuery(alias + page * 2, &run, sizeof(run)) == sizeof(run) && run.AllocationBase == alias + page * 2 && run.RegionSize == page); + auto* through = static_cast(alias); + for (std::size_t index = 0; index < 3; ++index) { + Require(through[page * index + 5] == 0x11 * (index + 1)); + through[page * index + 6] = static_cast(0x44 + index); + Require(bytes[page * index + 6] == 0x44 + index); + } + GuestArena::GuestArenaUnmapAlias_nid_postfix(alias); + for (std::size_t offset = 0; offset < page * 3; offset += page) { + MEMORY_BASIC_INFORMATION released{}; + Require(VirtualQuery(alias + offset, &released, sizeof(released)) == sizeof(released) && released.State == MEM_FREE); + } + Require(bytes[5] == 0x11 && bytes[page + 5] == 0x22 && bytes[page * 2 + 5] == 0x33); + Require(sceKernelMunmap(mapped, page * 3) == 0); + Require(sceKernelReleaseDirectMemory(first, page * 3) == 0); +} + +static void 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; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page, 0, 0, &phys) == 0); + void* reserved = nullptr; + Require(sceKernelReserveVirtualRange(&reserved, page * 2, 0, 0) == 0); + void* shared = reserved; + Require(sceKernelMapDirectMemory(&shared, page, 3, 0x10, phys, 0) == 0); + void* privatePage = static_cast(reserved) + page; + Require(sceKernelMapFlexibleMemory(&privatePage, page, 3, 0x10) == 0); + bool refused = false; + try { GuestArena::GuestArenaMapAlias_nid_postfix(reinterpret_cast(reserved), page * 2); } catch (const std::runtime_error&) { refused = true; } + Require(refused); + Require(sceKernelMunmap(reserved, page * 2) == 0); + Require(sceKernelReleaseDirectMemory(phys, page) == 0); +} #endif -#if defined(__linux__) +#if defined(__linux__) && !defined(ANYPS5_ENABLE_ASAN) static std::size_t LockedKilobytes() { std::ifstream status("/proc/self/status"); std::string line; @@ -571,7 +1571,7 @@ static void CheckMlock() { Require(sceKernelMapFlexibleMemory(&mapped, length, 3, 0) == 0); auto* bytes = static_cast(mapped); Require(sceKernelMlock_nid_postfix(mapped, 0) == 0); -#if defined(__linux__) +#if defined(__linux__) && !defined(ANYPS5_ENABLE_ASAN) const auto lockedBefore = LockedKilobytes(); #endif Require(sceKernelMlock_nid_postfix(bytes + 1, length - page) == 0); @@ -582,7 +1582,7 @@ static void CheckMlock() { Require(GetProcessWorkingSetSizeEx(GetCurrentProcess(), &minimum, &maximum, &limits) && minimum >= length && maximum > minimum); Require(VirtualUnlock(mapped, length)); Require(!VirtualUnlock(mapped, length) && GetLastError() == ERROR_NOT_LOCKED); -#elif defined(__linux__) +#elif defined(__linux__) && !defined(ANYPS5_ENABLE_ASAN) Require(LockedKilobytes() - lockedBefore == length / 1024); #endif Require(sceKernelMlock_nid_postfix(mapped, length) == 0); @@ -591,10 +1591,14 @@ static void CheckMlock() { Require(sceKernelMlock_nid_postfix(reinterpret_cast(std::numeric_limits::max() - page + 1), page * 2) == invalid); void* reserved = nullptr; Require(sceKernelReserveVirtualRange(&reserved, page, 0, 0) == 0); +#ifndef ANYPS5_ENABLE_ASAN Require(sceKernelMlock_nid_postfix(reserved, page) == outOfMemory); +#endif Require(sceKernelMunmap(reserved, page) == 0); Require(sceKernelMunmap(mapped, length) == 0); +#ifndef ANYPS5_ENABLE_ASAN Require(sceKernelMlock_nid_postfix(mapped, page) == outOfMemory); +#endif } static void CheckSharedDirectMemoryLifecycle() { @@ -664,6 +1668,24 @@ static void CheckSharedDirectMemoryLifecycle() { Require(sceKernelReleaseDirectMemory(phys, page * 3) == 0); } +static void CheckHeapAlignment() { + std::vector> blocks; + for (std::size_t bytes = 1; bytes <= 600; ++bytes) blocks.emplace_back(static_cast(GuestHeap::GuestHeapAllocate_nid_postfix(bytes)), bytes); + for (const std::size_t bytes : {std::size_t{4000}, std::size_t{70000}, std::size_t{0x30000}}) blocks.emplace_back(static_cast(GuestHeap::GuestHeapAllocate_nid_postfix(bytes)), bytes); + auto* grown = static_cast(GuestHeap::GuestHeapReallocate_nid_postfix(GuestHeap::GuestHeapAllocate_nid_postfix(8), 333)); + blocks.emplace_back(grown, 333); + for (std::size_t index = 0; index < blocks.size(); ++index) { + const auto [pointer, bytes] = blocks[index]; + Require((reinterpret_cast(pointer) & 31u) == 0); + std::memset(pointer, static_cast(index & 0xffu), bytes); + } + for (std::size_t index = 0; index < blocks.size(); ++index) { + const auto [pointer, bytes] = blocks[index]; + for (std::size_t at = 0; at < bytes; ++at) Require(pointer[at] == static_cast(index & 0xffu)); + GuestHeap::GuestHeapFree_nid_postfix(pointer); + } +} + static void CheckHeapAfterMappingReuse() { constexpr std::size_t bytes = 0x30000; auto* pointer = static_cast(GuestHeap::GuestHeapAllocate_nid_postfix(bytes)); @@ -676,6 +1698,456 @@ static void CheckHeapAfterMappingReuse() { GuestHeap::GuestHeapFree_nid_postfix(pointer); } +#ifdef _WIN32 +static int g_accessViolations = 0; + +static LONG WINAPI CountAccessViolations(EXCEPTION_POINTERS* info) { + if (info->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) ++g_accessViolations; + return EXCEPTION_CONTINUE_SEARCH; +} +#endif + +static void CheckSingleDirectMappingTracksWritesWithoutFaults() { +#ifdef _WIN32 + constexpr std::size_t page = 0x4000; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 4, 0, 0, &phys) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, page * 4, 3, 0, phys, 0) == 0); + const auto collect = [](void* address, std::size_t bytes) { + std::array pages{}; + std::size_t count = pages.size(); + Require(GuestArena::GuestArenaCollectWrites_nid_postfix(reinterpret_cast(address), bytes, pages.data(), &count, true)); + return count; + }; + auto* bytes = static_cast(mapped); + Require(collect(mapped, page * 4) == 16); + Require(collect(mapped, page * 4) == 0); + void* const handler = AddVectoredExceptionHandler(1, CountAccessViolations); + Require(handler != nullptr); + bytes[page + 0x1000] = 7; + bytes[page * 3] = 9; + Require(RemoveVectoredExceptionHandler(handler) != 0); + Require(g_accessViolations == 0); + Require(collect(mapped, page * 4) == 2); + Require(collect(mapped, page * 4) == 0); + Require(sceKernelMunmap(static_cast(mapped) + page * 2, page) == 0); + Require(bytes[page + 0x1000] == 7 && bytes[page * 3] == 9); + bytes[0] = 5; + Require(collect(mapped, page * 2) != 0); + Require(collect(mapped, page * 2) == 0); + void* second = nullptr; + Require(sceKernelMapDirectMemory(&second, page, 3, 0, phys + page, 0) == 0); + Require(static_cast(second)[0x1000] == 7); + static_cast(second)[0x1001] = 11; + Require(bytes[page + 0x1001] == 11); + Require(collect(mapped, page * 2) != 0); + Require(sceKernelMunmap(second, page) == 0); + Require(sceKernelMunmap(mapped, page * 2) == 0); + Require(sceKernelMunmap(static_cast(mapped) + page * 3, page) == 0); + void* again = nullptr; + Require(sceKernelMapDirectMemory(&again, page * 4, 3, 0, phys, 0) == 0); + const auto* data = static_cast(again); + Require(data[0] == 5 && data[page + 0x1000] == 7 && data[page + 0x1001] == 11 && data[page * 2] == 0 && data[page * 3] == 9); + Require(sceKernelMunmap(again, page * 4) == 0); + Require(sceKernelReleaseDirectMemory(phys, page * 4) == 0); +#endif +} + +static void 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; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 4, 0, 0, &phys) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, page * 4, 3, 0, phys, 0) == 0); + auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + Require(std::count_if(lease.begin(), lease.end(), [&](const auto& range) { return range->address == reinterpret_cast(mapped); }) == 1); + std::atomic released{false}; + std::thread holder([&] { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + released = true; + lease.clear(); + }); + void* second = nullptr; + Require(sceKernelMapDirectMemory(&second, page, 3, 0, phys + page, 0) == 0); + Require(released); + holder.join(); + static_cast(second)[8] = 3; + Require(static_cast(mapped)[page + 8] == 3); + static_cast(mapped)[page * 2 + 8] = 4; + Require(sceKernelMunmap(second, page) == 0); + Require(sceKernelMunmap(mapped, page * 4) == 0); + Require(sceKernelReleaseDirectMemory(phys, page * 4) == 0); +#endif +} + +#ifdef _WIN32 +static void RequireResidentMapping(const void* address) { + MEMORY_BASIC_INFORMATION info{}; + Require(VirtualQuery(address, &info, sizeof(info)) == sizeof(info)); + Require(info.State == MEM_COMMIT && info.Type == MEM_PRIVATE); +} +#endif + +static void CheckBreakingAResidentRunKeepsConcurrentAccesses() { +#ifdef _WIN32 + constexpr std::size_t page = 0x4000; + constexpr std::size_t chunk = 0x200000; + constexpr std::size_t chunks = 4; + for (int round = 0; round < 20; ++round) { + const bool unmap = round % 2 != 0; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, chunk * chunks, 0, 0, &phys) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, chunk * chunks, 3, 0, phys, 0) == 0); + RequireResidentMapping(mapped); + auto* memory = static_cast(mapped); + const auto value = [&](std::size_t at) { return static_cast((at * 31 + static_cast(round)) | 1); }; + const auto skipped = [&](std::size_t offset) { return unmap && offset >= page && offset < page * 2; }; + std::atomic go{false}; + std::atomic done{false}; + std::atomic consistent{true}; + std::thread writer([&] { + while (!go) {} + for (std::size_t offset = 0; offset < chunk; ++offset) { + if (skipped(offset)) continue; + for (std::size_t index = 0; index < chunks; ++index) memory[index * chunk + offset] = value(index * chunk + offset); + } + }); + std::thread reader([&] { + while (!go) {} + while (!done) { + for (std::size_t offset = 0; offset < chunk && !done; offset += 61) { + if (skipped(offset)) continue; + for (std::size_t index = 0; index < chunks; ++index) { + const unsigned char seen = memory[index * chunk + offset]; + if (seen != 0 && seen != value(index * chunk + offset)) consistent = false; + } + } + } + }); + std::array second{}; + go = true; + for (std::size_t index = 0; index < chunks; ++index) { + if (unmap) Require(sceKernelMunmap(static_cast(mapped) + index * chunk + page, page) == 0); + else Require(sceKernelMapDirectMemory(&second[index], page, 3, 0, phys + static_cast(index * chunk + page), 0) == 0); + } + writer.join(); + done = true; + reader.join(); + Require(consistent); + for (std::size_t index = 0; index < chunks; ++index) { + for (std::size_t offset = 0; offset < chunk; ++offset) { + if (!skipped(offset)) Require(memory[index * chunk + offset] == value(index * chunk + offset)); + } + if (unmap) continue; + for (std::size_t offset = 0; offset < page; ++offset) Require(static_cast(second[index])[offset] == value(index * chunk + page + offset)); + Require(sceKernelMunmap(second[index], page) == 0); + } + for (std::size_t index = 0; index < chunks; ++index) { + auto* base = static_cast(mapped) + index * chunk; + if (!unmap) { + Require(sceKernelMunmap(base, chunk) == 0); + continue; + } + Require(sceKernelMunmap(base, page) == 0); + Require(sceKernelMunmap(base + page * 2, chunk - page * 2) == 0); + } + Require(sceKernelReleaseDirectMemory(phys, chunk * chunks) == 0); + } +#endif +} + +static void CheckRemappedDirectMemoryIsChargedOnce() { +#ifdef _WIN32 + constexpr std::size_t bytes = 0x10000000; + const auto committed = [] { + PERFORMANCE_INFORMATION info{}; + info.cb = sizeof(info); + Require(K32GetPerformanceInfo(&info, sizeof(info)) != 0); + return static_cast(info.CommitTotal) * static_cast(info.PageSize); + }; + bool once = false; + for (int attempt = 0; attempt < 3 && !once; ++attempt) { + const auto before = committed(); + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, 0, 0, &phys) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, bytes, 3, 0, phys, 0) == 0); + RequireResidentMapping(mapped); + std::memset(mapped, 0xa5, bytes); + Require(sceKernelMunmap(mapped, bytes) == 0); + void* again = nullptr; + Require(sceKernelMapDirectMemory(&again, bytes, 3, 0, phys, 0) == 0); + const auto* data = static_cast(again); + Require(data[0] == 0xa5 && data[bytes / 2] == 0xa5 && data[bytes - 1] == 0xa5); + once = committed() - before < static_cast(bytes + bytes / 2); + Require(sceKernelMunmap(again, bytes) == 0); + Require(sceKernelReleaseDirectMemory(phys, bytes) == 0); + } + Require(once); +#endif +} + static void CheckSharedWriteTracking() { #ifdef _WIN32 constexpr std::size_t page = 0x4000; @@ -699,20 +2171,20 @@ static void CheckSharedWriteTracking() { auto* right = static_cast(second); left[page + 5] = 21; Require(right[page + 5] == 21); - Require(collect(first, page * 3, false) == 4); - Require(collect(first, page * 3, false) == 4); - Require(collect(first, page * 3) == 4); - Require(collect(second, page * 3) == 4); + Require(collect(first, page * 3, false) == 1); + Require(collect(first, page * 3, false) == 1); + Require(collect(first, page * 3) == 1); + Require(collect(second, page * 3) == 1); Require(collect(first, page * 3) == 0); right[page * 2] = 42; - Require(collect(first, page * 3) == 4); - Require(collect(second, page * 3) == 4); + Require(collect(first, page * 3) == 1); + Require(collect(second, page * 3) == 1); Require(collect(second, page * 3) == 0); Require(sceKernelMprotect(first, page * 3, 1) == 0); collect(first, page * 3); collect(second, page * 3); right[0] = 63; - Require(left[0] == 63 && collect(first, page * 3) == 4); + Require(left[0] == 63 && collect(first, page * 3) == 1); Require(sceKernelMprotect(first, page * 3, 3) == 0); collect(first, page * 3); left[0] = 84; @@ -722,7 +2194,7 @@ static void CheckSharedWriteTracking() { collect(first, page * 3); collect(second, page * 3); static_cast(third)[0] = 105; - Require(collect(first, page * 3) == 4 && collect(second, page * 3) == 4); + Require(collect(first, page * 3) == 1 && collect(second, page * 3) == 1); Require(sceKernelMunmap(third, page) == 0); Require(sceKernelMunmap(first, page * 3) == 0); Require(sceKernelMunmap(second, page * 3) == 0); @@ -730,13 +2202,172 @@ static void CheckSharedWriteTracking() { #endif } +static void CheckSharedWritePages() { +#ifdef _WIN32 + constexpr std::size_t page = 0x4000; + constexpr std::size_t small = 0x1000; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 0, &phys) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, page * 2, 3, 0, phys, 0) == 0); + auto* bytes = static_cast(mapped); + const auto collect = [&](std::size_t offset, std::size_t size) { + std::array pages{}; + std::size_t count = pages.size(); + Require(GuestArena::GuestArenaCollectWrites_nid_postfix(reinterpret_cast(mapped) + offset, size, pages.data(), &count, true)); + Require(count == 0 || reinterpret_cast(pages[0]) >= reinterpret_cast(mapped) + offset); + return count; + }; + collect(0, page * 2); + Require(collect(0, page * 2) == 0); + static_cast(bytes)[small + 3] = 1; + Require(collect(small * 2, small * 2) == 0); + Require(collect(0, page * 2) == 1); + Require(collect(0, page * 2) == 0); + std::memset(bytes + small * 2 - 8, 7, 16); + Require(collect(0, page * 2) == 2); + Require(collect(0, page * 2) == 0); + const auto path = std::filesystem::path("anyps5-shared-pages-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".bin"); + { + std::ofstream stream(path, std::ios::binary); + const std::vector seed(small, 5); + stream.write(seed.data(), static_cast(seed.size())); + } + const int fd = sceKernelOpen(path.string().c_str(), SCE_KERNEL_O_RDONLY, 0); + Require(fd >= 0); + Require(sceKernelPread(fd, bytes + page + small, small, 0) == static_cast(small)); + Require(sceKernelClose(fd) == 0); + Require(bytes[page + small] == 5 && bytes[page + small * 2 - 1] == 5); + Require(collect(page, small) == 0); + Require(collect(0, page * 2) == 1); + Require(sceKernelMunmap(mapped, page * 2) == 0); + Require(sceKernelReleaseDirectMemory(phys, page * 2) == 0); + std::filesystem::remove(path); +#endif +} + +static void CheckSharedWritesDuringHostWrite() { +#ifdef _WIN32 + constexpr std::size_t page = 0x4000; + constexpr std::size_t small = 0x1000; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page, 0, 0, &phys) == 0); + void* first = nullptr; + void* second = nullptr; + Require(sceKernelMapDirectMemory(&first, page, 3, 0, phys, 0) == 0); + Require(sceKernelMapDirectMemory(&second, page, 3, 0, phys, 0) == 0); + auto* left = static_cast(first); + auto* right = static_cast(second); + const auto collect = [](void* address) { + std::array pages{}; + std::size_t count = pages.size(); + Require(GuestArena::GuestArenaCollectWrites_nid_postfix(reinterpret_cast(address), page, pages.data(), &count, true)); + std::vector offsets; + for (std::size_t i = 0; i < count; ++i) offsets.push_back(reinterpret_cast(pages[i]) - reinterpret_cast(address)); + return offsets; + }; + const std::vector open{small, small * 2}; + collect(first); + collect(second); + Require(collect(first).empty() && collect(second).empty()); + left[small] = 1; + auto* target = const_cast(left + small * 2); + Require(GuestArena::GuestArenaBeginHostWrite_nid_postfix(target, small)); + Require(collect(first) == open); + Require(collect(second) == open); + left[small + 8] = 2; + Require(collect(first) == open); + Require(collect(second) == open); + GuestArena::GuestArenaEndHostWrite_nid_postfix(target, small); + Require(collect(first) == open && collect(second) == open); + Require(collect(first).empty() && collect(second).empty()); + left[small + 16] = 3; + Require(right[small + 16] == 3 && collect(second) == std::vector{small}); + Require(sceKernelMunmap(first, page) == 0); + Require(sceKernelMunmap(second, page) == 0); + Require(sceKernelReleaseDirectMemory(phys, page) == 0); +#endif +} + +static void CheckFailedCollectKeepsWrites() { +#ifdef _WIN32 + constexpr std::size_t page = 0x4000; + const auto collect = [](void* address, std::size_t bytes, std::size_t& count) { + std::array pages{}; + count = pages.size(); + return GuestArena::GuestArenaCollectWrites_nid_postfix(reinterpret_cast(address), bytes, pages.data(), &count, true); + }; + std::size_t count = 0; + auto* block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(page * 3, page)); + GuestArena::GuestArenaCommit_nid_postfix(block, page, PAGE_READWRITE, page); + GuestArena::GuestArenaCommit_nid_postfix(block + page * 2, page, PAGE_READWRITE, page); + Require(collect(block, page, count) && collect(block + page * 2, page, count)); + static_cast(block)[8] = 1; + Require(!collect(block, page * 3, count)); + Require(collect(block, page, count) && count == 1); + GuestArena::GuestArenaRelease_nid_postfix(block, page * 3); + + 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* shared = static_cast(mapped); + Require(collect(mapped, page * 2, count)); + shared[8] = 2; + Require(sceKernelMprotect(const_cast(shared + page), page, 0) == 0); + Require(!collect(mapped, page * 2, count)); + Require(collect(mapped, page, count) && count == 1); + Require(sceKernelMunmap(mapped, page * 2) == 0); + Require(sceKernelReleaseDirectMemory(phys, page * 2) == 0); +#endif +} + +static void CheckSplitPrivateCollect() { +#ifdef _WIN32 + constexpr std::size_t granule = 0x4000; + constexpr std::size_t regions = 8; + constexpr std::size_t bytes = granule * regions; + constexpr std::size_t pages = bytes / 4096; + auto* block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, granule)); + GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, granule); + const auto address = reinterpret_cast(block); + const auto collect = [&](std::size_t capacity) { + std::vector found; + std::array buffer{}; + for (auto cursor = address; cursor < address + bytes;) { + std::size_t count = capacity; + Require(GuestArena::GuestArenaCollectWrites_nid_postfix(cursor, address + bytes - cursor, buffer.data(), &count, true)); + for (std::size_t i = 0; i < count; ++i) found.push_back(reinterpret_cast(buffer[i])); + if (count < capacity) break; + cursor = found.back() + 4096; + } + return found; + }; + collect(pages); + for (const std::size_t capacity : {pages, std::size_t{5}, std::size_t{1}}) { + for (std::size_t page = 0; page < pages; ++page) static_cast(block)[page * 4096 + 7] = static_cast(capacity + page); + const auto found = collect(capacity); + Require(found.size() == pages); + for (std::size_t page = 0; page < pages; ++page) Require(found[page] == address + page * 4096); + Require(collect(pages).empty()); + } + static_cast(block)[granule * 3 + 4096 + 1] = 9; + const auto one = collect(pages); + Require(one.size() == 1 && one[0] == address + granule * 3 + 4096); + GuestArena::GuestArenaReset_nid_postfix(block, bytes); + GuestArena::GuestArenaRelease_nid_postfix(block, bytes); +#endif +} + static void CheckPinnedSharedPages() { #ifdef _WIN32 constexpr std::size_t page = 0x4000; std::int64_t phys = 0; Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 3, 0, 0, &phys) == 0); void* mapped = nullptr; + void* alias = nullptr; Require(sceKernelMapDirectMemory(&mapped, page * 3, 3, 0, phys, 0) == 0); + Require(sceKernelMapDirectMemory(&alias, page * 3, 3, 0, phys, 0) == 0); auto* bytes = static_cast(mapped); const auto collect = [&] { std::array pages{}; @@ -759,7 +2390,7 @@ static void CheckPinnedSharedPages() { Require(protection(page) == PAGE_READWRITE); bytes[page + 8] = 7; bytes[0] = 9; - Require(collect() == 8); + Require(collect() == 5); Require(collect() == 4); Require(protection(0) == PAGE_READONLY && protection(page) == PAGE_READWRITE); GuestArena::GuestArenaUnpinWritable_nid_postfix(const_cast(bytes + page), page); @@ -767,7 +2398,8 @@ static void CheckPinnedSharedPages() { Require(collect() == 0); Require(protection(page) == PAGE_READONLY); bytes[page + 8] = 11; - Require(collect() == 4); + Require(collect() == 1); + Require(sceKernelMunmap(alias, page * 3) == 0); Require(sceKernelMunmap(mapped, page * 3) == 0); Require(sceKernelReleaseDirectMemory(phys, page * 3) == 0); #endif @@ -820,13 +2452,7 @@ static void CheckReadsIntoSharedWriteTracking() { std::int32_t deleted = -1; Require(sceKernelAioDeleteRequest(id, &deleted) == 0); Require(sceKernelMprotect(mapped, page * 2, 1) == 0); - bool refused = false; - try { - sceKernelPread(fd, mapped, page, 0); - } catch (const std::exception&) { - refused = true; - } - Require(refused); + Require(sceKernelPread(fd, mapped, page, 0) == static_cast(SCE_KERNEL_ERROR_EFAULT)); KernelAioResult refusedResult{-1, 0}; KernelAioRwRequest refusedRequest{0, page, bytes, &refusedResult, fd}; Require(sceKernelAioSubmitReadCommands(&refusedRequest, 1, 0, &id) == 0); @@ -864,6 +2490,62 @@ static bool Written(const void* base, std::size_t bytes, PageRuns expected) { return false; } +static bool CollectArmedRuns(const void* base, std::size_t offset, std::size_t bytes, PageRuns& runs) { + runs.clear(); + const auto address = reinterpret_cast(base); + std::pair context{address, &runs}; + return GuestWriteWatch::GuestWriteWatchCollectArmed_nid_postfix(address + offset, bytes, [](void* context, std::uintptr_t begin, std::uintptr_t end) { + auto& [origin, into] = *static_cast*>(context); + if (!into->empty() && into->back().second == (begin - origin) / 4096) into->back().second = (end - origin) / 4096; + else into->emplace_back((begin - origin) / 4096, (end - origin) / 4096); + }, &context); +} + +static void CheckWriteWatchArmedCollect() { + if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) return; + constexpr std::size_t length = 0x100000; + constexpr std::size_t small = 4096; + void* mapping = nullptr; + Require(sceKernelMapFlexibleMemory(&mapping, length, 3, 0) == 0); + auto* bytes = static_cast(mapping); + PageRuns runs; + Require(CollectRuns(mapping, 0, length / 2, runs) && runs == PageRuns{{0, length / small / 2}}); + bytes[5 * small] = 1; + bytes[200 * small] = 1; + Require(CollectArmedRuns(mapping, 0, length, runs) && runs == PageRuns{{5, 6}}); + Require(Written(mapping, length, {{length / small / 2, length / small}})); + Require(Written(mapping, length, {})); + Require(sceKernelMunmap(mapping, length) == 0); +} + +static bool CollectFreshRuns(const void* base, std::size_t offset, std::size_t bytes, PageRuns& runs) { + runs.clear(); + const auto address = reinterpret_cast(base); + std::pair context{address, &runs}; + return GuestWriteWatch::GuestWriteWatchCollectFresh_nid_postfix(address + offset, bytes, [](void* context, std::uintptr_t begin, std::uintptr_t end) { + auto& [origin, into] = *static_cast*>(context); + if (!into->empty() && into->back().second == (begin - origin) / 4096) into->back().second = (end - origin) / 4096; + else into->emplace_back((begin - origin) / 4096, (end - origin) / 4096); + }, &context); +} + +static void CheckWriteWatchFreshCollect() { + if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) return; + constexpr std::size_t length = 0x100000; + constexpr std::size_t small = 4096; + void* mapping = nullptr; + Require(sceKernelMapFlexibleMemory(&mapping, length, 3, 0) == 0); + auto* bytes = static_cast(mapping); + PageRuns runs; + Require(CollectRuns(mapping, 0, length / 2, runs) && runs == PageRuns{{0, length / small / 2}}); + bytes[5 * small] = 1; + Require(CollectFreshRuns(mapping, 0, length, runs) && runs == PageRuns{{length / small / 2, length / small}}); + Require(CollectFreshRuns(mapping, 0, length, runs) && runs.empty()); + bytes[200 * small] = 1; + Require(Written(mapping, length, {{5, 6}, {200, 201}})); + Require(sceKernelMunmap(mapping, length) == 0); +} + static void CheckWriteWatch() { if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) { std::puts("write watch unavailable: not tested"); @@ -975,15 +2657,16 @@ static void CheckDirectMemorySharedBacking() { static_cast(first)[page + 5] = 0x5a; int file = -1; std::uint64_t offset = 0; + const auto base = static_cast(phys); Require(GuestArena::GuestArenaSharedBacking_nid_postfix(address + page, page, &file, &offset)); - Require(offset == page * 2); + Require(offset == base + page * 2); unsigned char byte = 0; Require(pread(file, &byte, 1, static_cast(offset + 5)) == 1 && byte == 0x5a); const int seals = fcntl(file, F_GET_SEALS); Require(seals >= 0 && (seals & F_SEAL_SHRINK) != 0 && (seals & F_SEAL_GROW) != 0); Require((fcntl(file, F_GETFD) & FD_CLOEXEC) != 0); close(file); - Require(GuestArena::GuestArenaSharedBacking_nid_postfix(address, page * 2, &file, &offset) && offset == page); + Require(GuestArena::GuestArenaSharedBacking_nid_postfix(address, page * 2, &file, &offset) && offset == base + page); close(file); Require(GuestArena::GuestArenaSharedBacking_nid_postfix(address + page + 0x1000, 0x1000, &file, &offset) && offset == page * 2 + 0x1000); close(file); @@ -997,11 +2680,11 @@ static void CheckDirectMemorySharedBacking() { Require(sceKernelMapDirectMemory(&low, page, 3, 0x10, phys, 0) == 0); void* high = static_cast(flexible) + page; Require(sceKernelMapDirectMemory(&high, page, 3, 0x10, phys + page, 0) == 0); - Require(GuestArena::GuestArenaSharedBacking_nid_postfix(flexibleAddress, page * 2, &file, &offset) && offset == 0); + Require(GuestArena::GuestArenaSharedBacking_nid_postfix(flexibleAddress, page * 2, &file, &offset) && offset == base); close(file); Require(sceKernelMapDirectMemory(&high, page, 3, 0x10, phys + page * 2, 0) == 0); Require(!GuestArena::GuestArenaSharedBacking_nid_postfix(flexibleAddress, page * 2, &file, &offset)); - Require(GuestArena::GuestArenaSharedBacking_nid_postfix(flexibleAddress + page, page, &file, &offset) && offset == page * 2); + Require(GuestArena::GuestArenaSharedBacking_nid_postfix(flexibleAddress + page, page, &file, &offset) && offset == base + page * 2); close(file); Require(sceKernelMunmap(high, page) == 0); Require(!GuestArena::GuestArenaSharedBacking_nid_postfix(flexibleAddress + page, page, &file, &offset)); @@ -1009,6 +2692,38 @@ static void CheckDirectMemorySharedBacking() { Require(sceKernelMunmap(first, page * 2) == 0); Require(!GuestArena::GuestArenaSharedBacking_nid_postfix(address, page, &file, &offset)); Require(sceKernelReleaseDirectMemory(phys, page * 3) == 0); + + std::int64_t left = 0; + std::int64_t right = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page, 0, 0, &left) == 0); + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page, 0, 0, &right) == 0); + void* leftView = nullptr; + void* rightView = nullptr; + Require(sceKernelMapDirectMemory(&leftView, page, 3, 0, left, 0) == 0); + Require(sceKernelMapDirectMemory(&rightView, page, 3, 0, right, 0) == 0); + int leftFile = -1; + int rightFile = -1; + std::uint64_t leftOffset = 0; + std::uint64_t rightOffset = 0; + Require(GuestArena::GuestArenaSharedBacking_nid_postfix(reinterpret_cast(leftView), page, &leftFile, &leftOffset) && leftOffset == static_cast(left)); + Require(GuestArena::GuestArenaSharedBacking_nid_postfix(reinterpret_cast(rightView), page, &rightFile, &rightOffset) && rightOffset == static_cast(right)); + struct stat leftInfo {}; + struct stat rightInfo {}; + Require(fstat(leftFile, &leftInfo) == 0 && fstat(rightFile, &rightInfo) == 0 && leftInfo.st_ino == rightInfo.st_ino && leftInfo.st_dev == rightInfo.st_dev); + close(leftFile); + close(rightFile); + std::memset(leftView, 0x77, page); + Require(sceKernelMunmap(leftView, page) == 0); + Require(sceKernelReleaseDirectMemory(left, page) == 0); + std::int64_t again = 0; + Require(sceKernelAllocateDirectMemory(left, left + static_cast(page), page, 0, 0, &again) == 0 && again == left); + void* againView = nullptr; + Require(sceKernelMapDirectMemory(&againView, page, 3, 0, again, 0) == 0); + for (std::size_t i = 0; i < page; ++i) Require(static_cast(againView)[i] == 0); + Require(sceKernelMunmap(againView, page) == 0); + Require(sceKernelMunmap(rightView, page) == 0); + Require(sceKernelReleaseDirectMemory(again, page) == 0); + Require(sceKernelReleaseDirectMemory(right, page) == 0); } static void CheckDirectMemoryWriteWatch() { @@ -1060,39 +2775,594 @@ static void CheckDirectMemoryWriteWatch() { Require(sceKernelMunmap(flexible, page * 3) == 0); Require(sceKernelReleaseDirectMemory(phys, page * 3) == 0); } + +static void CheckDirectMemoryWriteWatchFollowsRemovedMappings() { + if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) { + std::puts("write watch unavailable: removed direct mappings not tested"); + return; + } + constexpr std::size_t page = 0x4000; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 4, 0, 0, &phys) == 0); + void* whole = nullptr; + Require(sceKernelMapDirectMemory(&whole, page * 4, 3, 0, phys, 0) == 0); + const auto address = reinterpret_cast(whole); + const auto watched = [](const void* pointer, std::size_t offset) { return GuestWriteWatch::GuestWriteWatchCovers_nid_postfix(reinterpret_cast(pointer) + offset, page); }; + void* pair = nullptr; + Require(sceKernelMapDirectMemory(&pair, page * 2, 3, 0, phys + page, 0) == 0); + Require(watched(whole, 0) && !watched(whole, page) && !watched(whole, page * 2) && watched(whole, page * 3)); + Require(!watched(pair, 0) && !watched(pair, page)); + Require(sceKernelMunmap(pair, page) == 0); + void* single = nullptr; + Require(sceKernelMapDirectMemory(&single, page, 3, 0, phys + page, 0) == 0); + Require(!watched(single, 0) && watched(whole, 0) && watched(whole, page * 3)); + Require(sceKernelMunmap(single, page) == 0); + Require(sceKernelMunmap(static_cast(pair) + page, page) == 0); + void* replaced = whole; + Require(sceKernelMapDirectMemory(&replaced, page, 3, 0x10, phys + page * 3, 0) == 0 && replaced == whole); + Require(!watched(whole, 0) && !watched(whole, page * 3)); + std::uintptr_t start = 0; + std::uintptr_t end = 0; + std::uint64_t offset = 0; + int type = 0; + Require(QueryDirectMapping(address, &start, &end, &offset, &type) && start == address && end == address + page && offset == static_cast(phys) + page * 3); + void* alone = nullptr; + Require(sceKernelMapDirectMemory(&alone, page, 3, 0, phys, 0) == 0); + Require(watched(alone, 0)); + void* again = nullptr; + Require(sceKernelMapDirectMemory(&again, page, 3, 0, phys, 0) == 0); + Require(!watched(alone, 0) && !watched(again, 0)); + Require(sceKernelMunmap(again, page) == 0); + Require(sceKernelMunmap(alone, page) == 0); + Require(sceKernelMunmap(whole, page * 4) == 0); + Require(sceKernelReleaseDirectMemory(phys, page * 4) == 0); +} + +static double MapAcrossBackingsSeconds(std::int64_t phys, std::size_t backings) { + constexpr std::size_t page = 0x4000; + const auto threadSeconds = [] { + timespec now{}; + Require(clock_gettime(CLOCK_THREAD_CPUTIME_ID, &now) == 0); + return static_cast(now.tv_sec) + static_cast(now.tv_nsec) * 1e-9; + }; + auto best = std::numeric_limits::max(); + for (int attempt = 0; attempt < 3; ++attempt) { + void* mapped = nullptr; + const auto before = threadSeconds(); + Require(sceKernelMapDirectMemory(&mapped, page * backings, 3, 0, phys, 0) == 0); + best = std::min(best, threadSeconds() - before); + const auto last = reinterpret_cast(mapped) + page * (backings - 1); + std::uintptr_t start = 0; + std::uintptr_t end = 0; + std::uint64_t offset = 0; + int type = 0; + Require(QueryDirectMapping(last, &start, &end, &offset, &type) && start == last && end == last + page); + Require(sceKernelMunmap(mapped, page * backings) == 0); + } + return best; +} + +static void CheckMappingCostFollowsTheMappingCount() { + constexpr std::size_t page = 0x4000; + constexpr std::size_t few = 1024; + constexpr std::size_t many = few * 16; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * many, 0, 0, &phys) == 0); + Require(sceKernelReleaseDirectMemory(phys, page * many) == 0); + for (std::size_t index = 0; index < many; ++index) { + const auto wanted = phys + static_cast(page * index); + std::int64_t piece = 0; + Require(sceKernelAllocateDirectMemory(wanted, wanted + static_cast(page), page, 0, 0, &piece) == 0 && piece == wanted); + } + const auto small = MapAcrossBackingsSeconds(phys, few); + const auto large = MapAcrossBackingsSeconds(phys, many); + Require(large <= small * 48); + Require(sceKernelReleaseDirectMemory(phys, page * many) == 0); +} #endif -int main() { +#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; + Require(sceKernelMapFlexibleMemory(&mapped, page * 3, 3, 0) == 0); + auto* bytes = static_cast(mapped); + bytes[0] = 17; + bytes[page * 2] = 29; + Require(sceKernelMprotect(bytes + page, page, 1) == 0); + for (std::size_t index = 0; index < 3; ++index) { + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(bytes + page * index, 0, &info, sizeof(info)) == 0); + Require(info.start == reinterpret_cast(bytes + page * index)); + Require(info.end == info.start + page); + Require(info.protection == (index == 1 ? 1 : 3)); + Require(info.is_committed && info.is_flexible && !info.is_direct); + } + Require(sceKernelMunmap(bytes + page, page) == 0); + for (std::size_t index : {std::size_t{0}, std::size_t{2}}) { + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(bytes + page * index, 0, &info, sizeof(info)) == 0); + Require(info.start == reinterpret_cast(bytes + page * index)); + Require(info.end == info.start + page); + } + VirtualQueryInfo next{}; + Require(sceKernelVirtualQuery(bytes + page, 1, &next, sizeof(next)) == 0); + Require(next.start == reinterpret_cast(bytes + page * 2)); + Require(next.end == next.start + page && next.protection == 3); + Require(bytes[0] == 17 && bytes[page * 2] == 29); + Require(sceKernelMunmap(bytes, page) == 0); + 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)); + const auto failed = reinterpret_cast(static_cast(-1)); + Require(mapped != failed); + Require(mprotect_nid_postfix(mapped, page, 0) == 0); + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(mapped, 0, &info, sizeof(info)) == 0); + Require(info.start == reinterpret_cast(mapped)); + Require(info.end == info.start + page && info.protection == 0); + 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; + Require(sceKernelMmap(nullptr, page, 3, 0x1002, -1, 0, &mapped) == 0 && mapped != nullptr); + static_cast(mapped)[page - 1] = 7; + Require(static_cast(mapped)[page - 1] == 7); + Require(sceKernelMunmap(mapped, page) == 0); + void* untouched = &mapped; + Require(sceKernelMmap(nullptr, 0, 3, 0x1002, -1, 0, &untouched) == static_cast(0x80020016u)); + Require(untouched == &mapped); +} + +static void CheckVirtualQueryPartialMunmap() { + constexpr std::size_t page = 0x4000; + const auto failed = reinterpret_cast(static_cast(-1)); + void* raw = mmap_nid_postfix(nullptr, page * 3, 3, 0x1002, -1, 0); + Require(raw != failed); + auto* memory = static_cast(raw); + VirtualQueryInfo before{}; + Require(sceKernelVirtualQuery(memory, 0, &before, sizeof(before)) == 0); + Require(before.start == reinterpret_cast(memory)); + Require(before.end == reinterpret_cast(memory) + page * 3); + Require(munmap_nid_postfix(memory + page, page) == 0); + VirtualQueryInfo first{}; + Require(sceKernelVirtualQuery(memory, 0, &first, sizeof(first)) == 0); + Require(first.start == reinterpret_cast(memory)); + Require(first.end == reinterpret_cast(memory) + page); + VirtualQueryInfo middle{}; + Require(sceKernelVirtualQuery(memory + page, 0, &middle, sizeof(middle)) == SCE_KERNEL_ERROR_EACCES); + VirtualQueryInfo next{}; + Require(sceKernelVirtualQuery(memory + page, 1, &next, sizeof(next)) == 0); + Require(next.start == reinterpret_cast(memory) + page * 2); + Require(next.end == reinterpret_cast(memory) + page * 3); + VirtualQueryInfo tail{}; + Require(sceKernelVirtualQuery(memory + page * 2, 0, &tail, sizeof(tail)) == 0); + Require(tail.start == reinterpret_cast(memory) + page * 2); + Require(tail.end == reinterpret_cast(memory) + page * 3); + Require(munmap_nid_postfix(memory, page) == 0); + Require(munmap_nid_postfix(memory + page * 2, page) == 0); +} + + +static void CheckAvailableDirectMemoryIsLargestFreeBlock() { + constexpr std::int64_t window = 0x4000000; + const std::int64_t top = static_cast(DIRECT_MEMORY_SIZE) - window; + std::int64_t first = -1; + std::int64_t gap = -1; + std::int64_t last = -1; + Require(sceKernelAllocateDirectMemory(top, top + window, 0x200000, 0x200000, 0, &first) == 0 && first == top); + Require(sceKernelAllocateDirectMemory(top, top + window, 0x400000, 0x200000, 0, &gap) == 0 && gap == top + 0x200000); + Require(sceKernelAllocateDirectMemory(top, top + window, 0x200000, 0x200000, 0, &last) == 0 && last == top + 0x600000); + Require(sceKernelReleaseDirectMemory(gap, 0x400000) == 0); + + std::int64_t start = -1; + std::size_t size = 0; + Require(sceKernelAvailableDirectMemorySize(top, top + window, 0, &start, &size) == 0); + Require(start == top + 0x800000 && size == static_cast(window - 0x800000)); + Require(sceKernelAvailableDirectMemorySize(top, top + 0x800000, 0, &start, &size) == 0); + 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_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); + Require(sceKernelReleaseDirectMemory(last, 0x200000) == 0); + Require(sceKernelAvailableDirectMemorySize(top, top + window, 0, &start, &size) == 0); + Require(start == top && size == static_cast(window)); +} + +static void CheckMunmapOverUnmappedPages() { + constexpr std::size_t page = 0x4000; + const auto failed = reinterpret_cast(static_cast(-1)); + void* raw = mmap_nid_postfix(nullptr, page * 3, 3, 0x1002, -1, 0); + Require(raw != failed); + auto* memory = static_cast(raw); + memory[0] = 1; + memory[page * 2] = 2; + Require(munmap_nid_postfix(memory + page, page) == 0); + Require(munmap_nid_postfix(memory, page * 3) == 0); + { + GuestAllocations::Mutation mutation; + Require(!mutation.Overlaps(memory, page * 3)); + } + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(memory, 0, &info, sizeof(info)) == SCE_KERNEL_ERROR_EACCES); + Require(sceKernelVirtualQuery(memory + page * 2, 0, &info, sizeof(info)) == SCE_KERNEL_ERROR_EACCES); + Require(munmap_nid_postfix(memory, page * 3) == 0); + Require(sceKernelMunmap(memory, page * 3) == 0); + Require(sceKernelMunmap(memory, 0) == SCE_KERNEL_ERROR_EINVAL); +} + +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(); CheckReleaseFlexibleMemory(); + CheckFlexibleMappingIgnoresFlag8000(); + CheckSceKernelMmap(); + CheckInternalMemoryAvailableSize(); + CheckVirtualQueryPartialMunmap(); + CheckMunmapOverUnmappedPages(); + CheckPageTableStats(); + CheckVirtualQueryWireLayout(); CheckNamedAndHintedMappings(); CheckInternalNamedFlexibleMapping(); + CheckFlexibleFixedReplacement(); + CheckMunmapScopedFlexibleRelease(); + CheckGuestModuleUsesFlexibleMemory(); CheckBatchMapStopsAtInvalidEntry(); CheckCheckedReleaseDirectMemory(); CheckAudioCoprocessorProtection(); + CheckMprotectIgnoresUndefinedBits(); + CheckStrictMemoryProtectionBits(); CheckDirectMemoryFollowsPhysicalPages(); + CheckSparseDirectMemoryBacking(); + CheckSavedDirectMemorySurvivesCachePressure(); + CheckReusedDirectMemorySections(); CheckReleaseDirectMemoryClearsMappings(); + CheckReleaseDirectMemoryRejectsInvalidRanges(); CheckFixedMappingReplacesPartialOverlap(); + CheckFixedMappingOverHoleInsideAllocation(); CheckDirectMemoryGpuProtBits(); + CheckSplitPrivateCollect(); + CheckGpuAccessFollowsProtection(); + CheckGpuMapsAreObserved(); + CheckClearingTheObserverWaitsForItsCalls(); + CheckHintedVirtualReservation(); + CheckReservationHintWithoutRoomAbove(); CheckFixedVirtualReservation(); CheckReservedRangeIsNotCommitted(); + CheckHintInsideReservation(); CheckNoOverwriteRefusesLiveMapping(); CheckMlock(); CheckSharedDirectMemoryLifecycle(); CheckGetDirectMemoryType(); CheckMtypeprotect(); + CheckHeapAlignment(); CheckHeapAfterMappingReuse(); -#ifdef _WIN32 CheckNoOverwriteRejectsHostOccupiedMapping(); CheckFixedMappingsReachTheApplicationAreaEnd(); +#if defined(_WIN32) || defined(__linux__) + CheckGuestModuleImageProtection(); #endif +#ifdef _WIN32 + CheckAliasAcrossDirectAllocations(); + CheckAliasOfReorderedSharedPages(); + CheckAliasRefusesPagesThatAreNotSharedViews(); + CheckAliasLeavesResidentRunsUnderAHostWrite(); +#endif + CheckUnhintedKernelMappingsLandInTheWindow(); +#if defined(__linux__) + CheckUnmappedArenaRangeStaysReserved(); +#endif + CheckArenaPlacesUnhintedMappingsInsideTheWindow(); + CheckArenaReusesAFreedRange(); + CheckArenaHonoursHintsWithoutGoingBelowThem(); CheckSharedWriteTracking(); + CheckSharedWritePages(); + CheckSharedWritesDuringHostWrite(); CheckReadsIntoSharedWriteTracking(); CheckPinnedSharedPages(); + CheckFailedCollectKeepsWrites(); + CheckSingleDirectMappingTracksWritesWithoutFaults(); + CheckLargeResidentWritesStopFaulting(); + CheckImportAccessDoesNotInventSharedWrites(); + CheckResidentCollectPreservesPartialWrites(); + CheckResidentCollectionDuringHostWrites(); + CheckSecondDirectMappingWaitsForLeases(); + CheckReleaseKeepsPhysicalUntilViewsUnmapped(sceKernelReleaseDirectMemory); + CheckReleaseKeepsPhysicalUntilViewsUnmapped(sceKernelCheckedReleaseDirectMemory); + CheckConcurrentCheckedReleasesUnmapViewsOnce(); + CheckBreakingAResidentRunKeepsConcurrentAccesses(); + CheckRemappedDirectMemoryIsChargedOnce(); #if defined(__linux__) CheckWriteWatch(); + CheckWriteWatchArmedCollect(); + CheckWriteWatchFreshCollect(); CheckDirectMemoryWriteWatch(); + CheckDirectMemoryWriteWatchFollowsRemovedMappings(); + CheckMappingCostFollowsTheMappingCount(); CheckDirectMemoryBackingNeedsNoFilesystem(); 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)); @@ -1108,7 +3378,7 @@ 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 @@ -1136,15 +3406,56 @@ int main() { GuestAllocations::Mutation mutation; Require(mutation.Find(memory).writable); } + for (int advice = 0; advice <= 10; ++advice) Require(madvise_nid_postfix(memory, page * 3, advice) == 0); + Require(madvise_nid_postfix(reinterpret_cast(0x800000000000), 1, 10) == 0); + Require(madvise_nid_postfix(memory, std::numeric_limits::max(), 10) == 0); + Require(memory[0] == 42 && memory[page * 2] == 73); + Require(madvise_nid_postfix(memory + 1, 0, 4) == 0); + Require(madvise_nid_postfix(memory, page, 11) == -1 && *__error_nid_postfix() == 22); + *__error_nid_postfix() = 0; + Require(madvise_nid_postfix(memory, page, -1) == -1 && *__error_nid_postfix() == 22); + *__error_nid_postfix() = 0; + Require(madvise_nid_postfix(memory, 0, 11) == -1 && *__error_nid_postfix() == 22); + *__error_nid_postfix() = 0; + Require(madvise_nid_postfix(memory, std::numeric_limits::max(), 0) == -1 && *__error_nid_postfix() == 22); + *__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) == -1); + 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; @@ -1161,4 +3472,17 @@ int main() { } Require(munmap_nid_postfix(mapped, 1) == 0); } + + reject(page, 3, 0x2001002, -1, 0, 45); + constexpr std::size_t superpage = std::size_t{1} << 21; + auto* aligned = static_cast(mmap_nid_postfix(nullptr, superpage + page, 3, 0x1001002, -1, 0)); + Require(aligned != failed && (reinterpret_cast(aligned) & (superpage - 1)) == 0); + aligned[0] = 1; + aligned[superpage + page - 1] = 2; + { + GuestAllocations::Mutation mutation; + const auto range = mutation.Find(aligned); + Require(range.bytes == superpage + page && range.readable && range.writable); + } + Require(munmap_nid_postfix(aligned, superpage + page) == 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/GuestMemoryModule.S b/core/libs/tests/GuestMemoryModule.S new file mode 100644 index 0000000000..a16839b3a9 --- /dev/null +++ b/core/libs/tests/GuestMemoryModule.S @@ -0,0 +1,3 @@ +.section .elf0,"dr" +.p2align 4 +.long 0 diff --git a/core/libs/tests/GuestMemoryModule.cpp b/core/libs/tests/GuestMemoryModule.cpp new file mode 100644 index 0000000000..ed8642ed65 --- /dev/null +++ b/core/libs/tests/GuestMemoryModule.cpp @@ -0,0 +1 @@ +extern "C" unsigned char guestMemoryModuleData[0x10000] = {1}; diff --git a/core/libs/tests/GuestMemoryPoolStats.cpp b/core/libs/tests/GuestMemoryPoolStats.cpp new file mode 100644 index 0000000000..7aec461e18 --- /dev/null +++ b/core/libs/tests/GuestMemoryPoolStats.cpp @@ -0,0 +1,90 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include + +struct PoolStats { + std::size_t pool_size; + std::size_t max_inuse_size; + std::size_t current_inuse_size; + std::int32_t reserved; +}; + +struct NetPoolStats { + std::size_t pool_size; + std::size_t max_inuse_size; + std::size_t current_inuse_size; +}; + +extern "C" { +int APS5_VABI sceSslInit_nid_postfix(std::size_t); +int APS5_VABI sceSslTerm_nid_postfix(int); +int APS5_VABI sceSslGetMemoryPoolStats(int, void*); +int APS5_VABI sceHttp2Init(int, int, std::size_t, int); +int APS5_VABI sceHttp2Term(int); +int APS5_VABI sceHttp2GetMemoryPoolStats(int, void*); +int APS5_VABI sceNetPoolCreate(const char*, int, int); +int APS5_VABI sceNetPoolDestroy(int); +int APS5_VABI sceNetGetMemoryPoolStats(int, void*); +} + +static void Require(bool value) { if (!value) std::abort(); } + +using StatsCall = int(APS5_VABI*)(int, void*); + +static void CheckRejected(StatsCall call, int id, int expected) { + std::array buffer; + buffer.fill(0x5a); + Require(call(id, buffer.data()) == expected); + for (std::size_t i = 0; i < sizeof(PoolStats); ++i) Require(buffer[i] == 0); + for (std::size_t i = sizeof(PoolStats); i < buffer.size(); ++i) Require(buffer[i] == 0x5a); +} + +int main() { + constexpr int sslInvalidId = static_cast(0x8095F006); + constexpr int sslInvalidValue = static_cast(0x8095F007); + constexpr int http2InvalidId = static_cast(0x817B1100); + constexpr int http2NullPointer = static_cast(0x817B1225); + constexpr int netBadDescriptor = static_cast(0x80410109); + constexpr int netInvalid = static_cast(0x80410116); + static_assert(sizeof(PoolStats) == 0x20 && sizeof(NetPoolStats) == 0x18); + + const int ssl = sceSslInit_nid_postfix(0x40000); + Require(ssl > 0); + PoolStats stats{}; + Require(sceSslGetMemoryPoolStats(ssl, &stats) == 0 && stats.pool_size == 0x40000); + Require(sceSslGetMemoryPoolStats(ssl, nullptr) == sslInvalidValue); + Require(sceSslGetMemoryPoolStats(12345, nullptr) == sslInvalidValue); + CheckRejected(sceSslGetMemoryPoolStats, 12345, sslInvalidId); + CheckRejected(sceSslGetMemoryPoolStats, -1, sslInvalidId); + + const int http2 = sceHttp2Init(1, ssl, 0x40000, 1); + Require(http2 > 0); + Require(sceHttp2GetMemoryPoolStats(http2, &stats) == 0 && stats.pool_size == 0x40000); + Require(sceHttp2GetMemoryPoolStats(http2, nullptr) == http2NullPointer); + Require(sceHttp2GetMemoryPoolStats(12345, nullptr) == http2NullPointer); + CheckRejected(sceHttp2GetMemoryPoolStats, 12345, http2InvalidId); + CheckRejected(sceHttp2GetMemoryPoolStats, -1, http2InvalidId); + Require(sceHttp2Term(http2) == 0); + CheckRejected(sceHttp2GetMemoryPoolStats, http2, http2InvalidId); + + Require(sceSslTerm_nid_postfix(ssl) == 0); + CheckRejected(sceSslGetMemoryPoolStats, ssl, sslInvalidId); + + const int pool = sceNetPoolCreate("stats", 0x20000, 0); + Require(pool > 0); + NetPoolStats netStats{}; + Require(sceNetGetMemoryPoolStats(pool, &netStats) == 0 && netStats.pool_size == 0x20000); + Require(sceNetGetMemoryPoolStats(pool, nullptr) == netInvalid); + Require(sceNetGetMemoryPoolStats(12345, nullptr) == netBadDescriptor); + std::array untouched; + untouched.fill(0x5a); + auto buffer = untouched; + Require(sceNetGetMemoryPoolStats(12345, buffer.data()) == netBadDescriptor && buffer == untouched); + Require(sceNetPoolDestroy(pool) == 0); + Require(sceNetGetMemoryPoolStats(pool, nullptr) == netBadDescriptor); + return 0; +} diff --git a/core/libs/tests/GuestMemrchr.cpp b/core/libs/tests/GuestMemrchr.cpp new file mode 100644 index 0000000000..8715e30bf2 --- /dev/null +++ b/core/libs/tests/GuestMemrchr.cpp @@ -0,0 +1,17 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include + +extern "C" { +void* APS5_VABI memrchr_nid_postfix(const void*, int, std::size_t); +} + +int main() { + const unsigned char bytes[] = {0xff, 0x01, 0xff, 0x00}; + const auto* found = static_cast(memrchr_nid_postfix(bytes, 0xff, sizeof(bytes))); + if (found != bytes + 2) std::abort(); + found = static_cast(memrchr_nid_postfix(bytes, 0x1ff, sizeof(bytes))); + if (found != bytes + 2) std::abort(); + found = static_cast(memrchr_nid_postfix(bytes, 0xff, 2)); + if (found != bytes) std::abort(); + if (memrchr_nid_postfix(bytes, 0x02, sizeof(bytes)) != nullptr) std::abort(); +} diff --git a/core/libs/tests/GuestMessageDestructorAbi.S b/core/libs/tests/GuestMessageDestructorAbi.S new file mode 100644 index 0000000000..57ac11e827 --- /dev/null +++ b/core/libs/tests/GuestMessageDestructorAbi.S @@ -0,0 +1,194 @@ +.cfi_sections .eh_frame +.text +.macro PROLOGUE + pushq %rbp + .cfi_def_cfa_offset 16 + .cfi_offset %rbp, -16 + movq %rsp, %rbp + .cfi_def_cfa_register %rbp + subq $16, %rsp +.endm +.macro WHAT + movq %rax, %rdi + movq (%rax), %r11 + xorl %ecx, %ecx + call *16(%r11) + movq %rax, %rdi + call FixtureCheckWhat +.endm +.globl GuestThrow +GuestThrow: + .cfi_startproc + PROLOGUE + movl $16, %edi + call __cxa_allocate_exception_nid_postfix + movq %rax, FixtureLastObject(%rip) + leaq FixtureVtable+16(%rip), %rdx + movq %rdx, (%rax) + leaq FixtureMessage(%rip), %rdx + movq %rdx, 8(%rax) + movq %rax, %rdi + leaq FixtureErrorType(%rip), %rsi + leaq FixtureDestroy(%rip), %rdx + call __cxa_throw_nid_postfix + ud2 + .cfi_endproc + +.globl GuestInner +GuestInner: + .cfi_startproc + .cfi_personality 0x00, __gxx_personality_v0_nid_postfix + .cfi_lsda 0x00, .Linner_lsda + PROLOGUE +.Linner_call: + call GuestThrow +.Linner_call_end: + ud2 +.Linner_catch: + cmpq $1, %rdx + jne .Linner_bad + movq %rax, %rdi + call __cxa_begin_catch_nid_postfix + incl FixtureCaught(%rip) + WHAT +.Linner_rethrow: + call __cxa_rethrow_nid_postfix +.Linner_rethrow_end: + ud2 +.Linner_cleanup: + movq %rax, -8(%rbp) + call __cxa_end_catch_nid_postfix + incl FixtureGuardDestroyed(%rip) + movq -8(%rbp), %rdi + call _Unwind_Resume_nid_postfix + ud2 +.Linner_bad: + movl $31, %edi + call FixtureFail + ud2 + .cfi_endproc + +.globl GuestOuter +GuestOuter: + .cfi_startproc + .cfi_personality 0x00, __gxx_personality_v0_nid_postfix + .cfi_lsda 0x00, .Louter_lsda + PROLOGUE +.Louter_call: + call GuestInner +.Louter_call_end: + ud2 +.Louter_catch: + cmpq $2, %rdx + jne .Louter_bad + movq %rax, %rdi + call __cxa_begin_catch_nid_postfix + cmpq FixtureLastObject(%rip), %rax + jne .Louter_bad + incl FixtureCaught(%rip) + WHAT + call __cxa_end_catch_nid_postfix + xorl %eax, %eax + leave + .cfi_def_cfa %rsp, 8 + ret +.Louter_bad: + movl $32, %edi + call FixtureFail + ud2 + .cfi_endproc + +.globl GuestStdCapture +GuestStdCapture: + .cfi_startproc + .cfi_personality 0x00, __gxx_personality_v0_nid_postfix + .cfi_lsda 0x00, .Lstd_lsda + PROLOGUE + movq FixtureArgument(%rip), %rdi +.Lstd_call: + call *FixtureThrow(%rip) +.Lstd_call_end: + ud2 +.Lstd_catch: + cmpq $1, %rdx + jne .Lstd_bad + movq %rax, %rdi + call __cxa_begin_catch_nid_postfix + movq %rax, %rdi + call FixtureInspect + call __cxa_end_catch_nid_postfix + leave + .cfi_def_cfa %rsp, 8 + ret +.Lstd_bad: + movl $33, %edi + call FixtureFail + ud2 + .cfi_endproc + +.globl ProbeWhat +ProbeWhat: +.globl ProbeDestroy +ProbeDestroy: + .cfi_startproc + PROLOGUE + subq $32, %rsp + movq %rdi, %r11 + movq %rsi, %rdi + movq %rdx, %rcx + call *%r11 + leave + .cfi_def_cfa %rsp, 8 + ret + .cfi_endproc +.section .rdata,"dr" +.Linner_lsda: + .byte 0xff, 0x00 + .uleb128 .Linner_types_end-.Linner_types_offset +.Linner_types_offset: + .byte 0x01 + .uleb128 .Linner_calls_end-.Linner_calls +.Linner_calls: + .uleb128 .Linner_call-GuestInner + .uleb128 .Linner_call_end-.Linner_call + .uleb128 .Linner_catch-GuestInner + .uleb128 1 + .uleb128 .Linner_rethrow-GuestInner + .uleb128 .Linner_rethrow_end-.Linner_rethrow + .uleb128 .Linner_cleanup-GuestInner + .uleb128 0 +.Linner_calls_end: + .byte 1, 0 + .quad FixtureErrorType +.Linner_types_end: +.Louter_lsda: + .byte 0xff, 0x00 + .uleb128 .Louter_types_end-.Louter_types_offset +.Louter_types_offset: + .byte 0x01 + .uleb128 .Louter_calls_end-.Louter_calls +.Louter_calls: + .uleb128 .Louter_call-GuestOuter + .uleb128 .Louter_call_end-.Louter_call + .uleb128 .Louter_catch-GuestOuter + .uleb128 1 +.Louter_calls_end: + .byte 1, 1, 2, 0 + .quad FixtureBaseType + .quad FixtureOtherType +.Louter_types_end: +.Lstd_lsda: + .byte 0xff, 0x80 + .uleb128 .Lstd_types_end-.Lstd_types_offset +.Lstd_types_offset: + .byte 0x01 + .uleb128 .Lstd_calls_end-.Lstd_calls +.Lstd_calls: + .uleb128 .Lstd_call-GuestStdCapture + .uleb128 .Lstd_call_end-.Lstd_call + .uleb128 .Lstd_catch-GuestStdCapture + .uleb128 1 +.Lstd_calls_end: + .byte 1, 0 + .quad FixtureStdOutType +.Lstd_types_end: diff --git a/core/libs/tests/GuestMessageDestructorAbi.cpp b/core/libs/tests/GuestMessageDestructorAbi.cpp new file mode 100644 index 0000000000..68e398ef4d --- /dev/null +++ b/core/libs/tests/GuestMessageDestructorAbi.cpp @@ -0,0 +1,251 @@ +#include "prx/libc/include/exceptions/Runtime.hpp" +#include +#include +#include +#include +#include + +using GuestWhat = const char* (APS5_VABI *)(const void*); +using GuestDestroy = void (APS5_VABI *)(void*); +using HostDestroy = void (*)(void*); +using GuestThrowFunction = void (APS5_VABI *)(std::uintptr_t); +struct TypeRecord { const void* vtable; const char* name; const TypeRecord* base; }; +struct Object { const void* vtable; const char* message; }; +struct Table { std::ptrdiff_t offset; const TypeRecord* type; GuestDestroy destroy; GuestDestroy deleteObject; GuestWhat what; }; +struct Message { std::size_t length; std::size_t capacity; std::atomic references; }; +static_assert(sizeof(Object) == 16); + +extern "C" { +extern const unsigned char _ZTVN10__cxxabiv117__class_type_infoE_nid_postfix[]; +extern const unsigned char _ZTVN10__cxxabiv120__si_class_type_infoE_nid_postfix[]; +extern const unsigned char _ZTISt12out_of_range_nid_postfix[]; +extern const unsigned char _ZTISt11regex_error_nid_postfix[]; +extern const unsigned char _ZTISt13runtime_error_nid_postfix[]; +extern const unsigned char _ZTISt11logic_error_nid_postfix[]; +void APS5_VABI _ZNSt12out_of_rangeC1EPKc_nid_postfix(Object*, const char*); +void APS5_VABI _ZNSt12out_of_rangeC1ERKS__nid_postfix(Object*, const Object*); +void APS5_VABI _ZNSt12out_of_rangeD1Ev_nid_postfix(Object*); +void APS5_VABI _ZNSt11logic_errorC1EPKc_nid_postfix(Object*, const char*); +void APS5_VABI _ZNSt11logic_errorD1Ev_nid_postfix(Object*); +[[noreturn]] void APS5_VABI _ZSt14_Xout_of_rangePKc_nid_postfix(const char*); +[[noreturn]] void APS5_VABI _ZSt15_Xruntime_errorPKc_nid_postfix(const char*); +[[noreturn]] void APS5_VABI _ZNKSt11logic_error8_DoraiseEv_nid_postfix(const Object*); +[[noreturn]] void APS5_VABI _ZSt13_Xregex_errorNSt15regex_constants10error_typeE_nid_postfix(std::regex_constants::error_type); +int APS5_VABI GuestOuter(); +void APS5_VABI GuestStdCapture(); +void APS5_VABI ProbeDestroy(GuestDestroy, void*, void*); +const char* APS5_VABI ProbeWhat(GuestWhat, const void*, const void*); +void APS5_VABI _ZNSt8bad_castC1Ev_nid_postfix(Object*); +void APS5_VABI _ZNSt9bad_allocC1Ev_nid_postfix(Object*); + +TypeRecord FixtureBaseType{_ZTVN10__cxxabiv117__class_type_infoE_nid_postfix + 16, "11FixtureBase", nullptr}; +TypeRecord FixtureErrorType{_ZTVN10__cxxabiv120__si_class_type_infoE_nid_postfix + 16, "12FixtureError", &FixtureBaseType}; +TypeRecord FixtureOtherType{_ZTVN10__cxxabiv117__class_type_infoE_nid_postfix + 16, "12FixtureOther", nullptr}; +const void* FixtureStdOutType = _ZTISt12out_of_range_nid_postfix; +GuestThrowFunction FixtureThrow = reinterpret_cast(_ZSt14_Xout_of_rangePKc_nid_postfix); +std::uintptr_t FixtureArgument; +extern const char FixtureMessage[] = "own System-V guest message"; +void* FixtureLastObject = nullptr; +unsigned FixtureDestroyed = 0; +unsigned FixtureGuardDestroyed = 0; +unsigned FixtureCaught = 0; +unsigned FixtureWhatChecked = 0; + +[[noreturn]] void APS5_VABI FixtureFail(unsigned code) { + std::fprintf(stderr, "guest exception failure %u\n", code); + ExitProcess(code); +} +const char* APS5_VABI FixtureWhat(const void* object) { return static_cast(object)->message; } +void APS5_VABI FixtureDestroy(void* object) { + if (object != FixtureLastObject) FixtureFail(11); + ++FixtureDestroyed; +} +Table FixtureVtable{0, &FixtureErrorType, FixtureDestroy, FixtureDestroy, FixtureWhat}; +void APS5_VABI FixtureCheckWhat(const char* message) { + if (!message || std::strcmp(message, FixtureMessage)) FixtureFail(12); + ++FixtureWhatChecked; +} +} + +static Object decoy{}, retained{}; +static GuestDestroy registeredDestructor; +static void* expectedObject; +static void* poison; +static unsigned callbacks; +static bool regularRelease; +static bool keepMessage; +static const char* expectedMessage = FixtureMessage; + +using NativeFree = void (*)(void*); +static NativeFree originalFree; +static std::uintptr_t* freeSlot; +static void* watchedObject; +static void* watchedMessage; +static void* foreignObject; +static void* foreignMessage; +static unsigned objectFrees, messageFrees; + +static void TrackedFree(void* pointer) { + if (pointer && (pointer == foreignObject || pointer == foreignMessage)) FixtureFail(50); + if (pointer && pointer == watchedObject && ++objectFrees != 1) FixtureFail(51); + if (pointer && pointer == watchedMessage && ++messageFrees != 1) FixtureFail(52); + originalFree(pointer); +} + +static void ReplaceFree(std::uintptr_t address) { + DWORD protection; + if (!VirtualProtect(freeSlot, sizeof(*freeSlot), PAGE_READWRITE, &protection)) FixtureFail(53); + *freeSlot = address; + DWORD ignored; + if (!VirtualProtect(freeSlot, sizeof(*freeSlot), protection, &ignored)) FixtureFail(54); +} + +static void TrackFree() { + auto* image = reinterpret_cast(GetModuleHandleW(L"libc.prx")); + if (!image) FixtureFail(55); + auto* dos = reinterpret_cast(image); + auto* nt = reinterpret_cast(image + dos->e_lfanew); + auto* imports = reinterpret_cast(image + + nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress); + for (; imports->Name; ++imports) { + if (!imports->OriginalFirstThunk) continue; + auto* names = reinterpret_cast(image + imports->OriginalFirstThunk); + auto* slots = reinterpret_cast(image + imports->FirstThunk); + for (; names->u1.AddressOfData; ++names, ++slots) { + if (IMAGE_SNAP_BY_ORDINAL64(names->u1.Ordinal)) continue; + auto* name = reinterpret_cast(image + names->u1.AddressOfData); + if (std::strcmp(name->Name, "free")) continue; + freeSlot = reinterpret_cast(&slots->u1.Function); + originalFree = reinterpret_cast(*freeSlot); + ReplaceFree(reinterpret_cast(TrackedFree)); + return; + } + } + FixtureFail(56); +} + +static int TestVtable(const char* mode) { + const bool plain = std::strstr(mode, "plain") != nullptr; + const bool deleting = std::strstr(mode, "delete") != nullptr; + const bool destroying = deleting || std::strstr(mode, "destroy") != nullptr; + auto* actual = static_cast(std::malloc(sizeof(Object))); + auto* other = static_cast(std::malloc(sizeof(Object))); + if (!actual || !other) FixtureFail(57); + actual->message = other->message = nullptr; + if (plain) { + _ZNSt9bad_allocC1Ev_nid_postfix(actual); + _ZNSt8bad_castC1Ev_nid_postfix(other); + } else { + _ZNSt12out_of_rangeC1EPKc_nid_postfix(actual, "actual virtual message"); + _ZNSt12out_of_rangeC1EPKc_nid_postfix(other, "unrelated virtual message"); + } + const Object originalOther = *other; + watchedObject = actual; + watchedMessage = plain ? nullptr : reinterpret_cast(const_cast(actual->message)) - 1; + foreignObject = other; + foreignMessage = plain ? nullptr : reinterpret_cast(const_cast(other->message)) - 1; + TrackFree(); + auto* unused = std::strstr(mode, "zero") ? nullptr : other; + GuestDestroy destroy, deleteObject; + GuestWhat what; + auto* slots = static_cast(actual->vtable); + std::memcpy(&destroy, slots, sizeof(destroy)); + std::memcpy(&deleteObject, slots + sizeof(void*), sizeof(deleteObject)); + std::memcpy(&what, slots + 2 * sizeof(void*), sizeof(what)); + if (destroying) { + ProbeDestroy(deleting ? deleteObject : destroy, actual, unused); + if (objectFrees != (deleting ? 1u : 0u) || messageFrees != (plain ? 0u : 1u)) FixtureFail(58); + if (!deleting && actual->message) FixtureFail(59); + } else { + const char* result = ProbeWhat(what, actual, unused); + if (!result || std::strcmp(result, plain ? "std::bad_alloc" : "actual virtual message") || + objectFrees || messageFrees) FixtureFail(60); + } + if (other->vtable != originalOther.vtable || other->message != originalOther.message || + (!plain && std::strcmp(other->message, "unrelated virtual message"))) FixtureFail(61); + ReplaceFree(reinterpret_cast(originalFree)); + if (!plain) { + if (!deleting) _ZNSt12out_of_rangeD1Ev_nid_postfix(actual); + _ZNSt12out_of_rangeD1Ev_nid_postfix(other); + } + if (!deleting) std::free(actual); + std::free(other); + return 0; +} + +static Message* MessageHeader(const Object& object) { + return reinterpret_cast(const_cast(object.message)) - 1; +} + +extern "C" void APS5_VABI ObserveDestroy(void* pointer) { + if (pointer != expectedObject || ++callbacks != 1) FixtureFail(40); + ProbeDestroy(registeredDestructor, pointer, poison); + if (static_cast(pointer)->message != nullptr || + !decoy.message || std::strcmp(decoy.message, "unrelated message") || + (keepMessage && MessageHeader(retained)->references.load() != 0)) FixtureFail(41); +} + +extern "C" void APS5_VABI FixtureInspect(void* pointer) { + auto* header = LibcException::FromObject(pointer); + if (header->_pad != 0 || !header->destructor) FixtureFail(42); + expectedObject = pointer; + GuestWhat what; + std::memcpy(&what, static_cast(static_cast(pointer)->vtable) + + 2 * sizeof(void*), sizeof(what)); + const char* message = ProbeWhat(what, pointer, poison); + if (!message || std::strcmp(message, expectedMessage)) FixtureFail(62); + if (keepMessage) { + _ZNSt12out_of_rangeC1ERKS__nid_postfix(&retained, static_cast(pointer)); + if (MessageHeader(retained)->references.load() != 1) FixtureFail(43); + } + registeredDestructor = reinterpret_cast(header->destructor); + if (!regularRelease) header->destructor = reinterpret_cast(ObserveDestroy); +} + +int main(int argc, char** argv) { + SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); + if (argc != 2) return 2; + 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 || + FixtureCaught != 2 || FixtureWhatChecked != 2) return 10; + 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, expected))) || + !decoy.message || std::strcmp(decoy.message, "unrelated message")) return 44; + _ZNSt12out_of_rangeD1Ev_nid_postfix(&retained); + _ZNSt12out_of_rangeD1Ev_nid_postfix(&decoy); + 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 new file mode 100644 index 0000000000..97d72aeaa9 --- /dev/null +++ b/core/libs/tests/GuestMktime.cpp @@ -0,0 +1,84 @@ +#include "prx/libc/include/GuestTime.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +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*); + +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 + && left.tm_gmtoff == right.tm_gmtoff && left.tm_zone != nullptr && right.tm_zone != nullptr + && std::strcmp(left.tm_zone, right.tm_zone) == 0; +} + +static void CheckNormalization(Converter convert, std::int64_t timer) { + GuestTm expected{}; + Require(localtime_s_nid_postfix(&timer, &expected) == &expected, "Local conversion failed"); + GuestTm input = expected; + input.tm_sec += 60; + --input.tm_min; + input.tm_wday = -1; + input.tm_yday = -1; + input.tm_isdst = -1; + input.tm_gmtoff = -1; + input.tm_zone = nullptr; + errno = EOVERFLOW; + Require(convert(&input) == timer, "mktime returned the wrong timestamp"); + Require(Equal(input, expected), "mktime did not normalize a valid timestamp"); +} + +static void CheckFailure(Converter convert) { + GuestTm input{}; + input.tm_year = std::numeric_limits::max(); + input.tm_mon = std::numeric_limits::max(); + input.tm_mday = 1; + input.tm_wday = 4; + input.tm_yday = 12; + input.tm_isdst = -1; + input.tm_gmtoff = 123; + input.tm_zone = "unchanged"; + const GuestTm original = input; + Require(convert(&input) == -1, "mktime accepted an unrepresentable calendar"); + Require(Equal(input, original), "Failed mktime changed the guest calendar"); +} + +static void CheckZone(const char* zone) { + 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}; + for (auto convert : converters) { + for (const auto timer : timers) { + CheckNormalization(convert, timer); + } + CheckFailure(convert); + } +} + +int main() { + CheckZone("UTC0"); + CheckZone("UTC-2"); + CheckZone("UTC5"); +} diff --git a/core/libs/tests/GuestMlock.cpp b/core/libs/tests/GuestMlock.cpp index 044cb68286..79b571e9b2 100644 --- a/core/libs/tests/GuestMlock.cpp +++ b/core/libs/tests/GuestMlock.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #ifdef _WIN32 #ifndef NOMINMAX #define NOMINMAX @@ -20,12 +21,30 @@ int APS5_VABI sceKernelReserveVirtualRange(void**, std::size_t, int, std::size_t int APS5_VABI sceKernelMunmap(void*, std::size_t); int APS5_VABI mlock_nid_postfix(const void*, std::size_t); int APS5_VABI munlock_nid_postfix(const void*, std::size_t); +int APS5_VABI mlockall_nid_postfix(int); +int APS5_VABI munlockall_nid_postfix(); int* APS5_VABI __error_nid_postfix(); } static void Require(bool value) { if (!value) std::abort(); } -#if defined(__linux__) +static void RequireLockAllInvalid(int flags) { + *__error_nid_postfix() = 0; + Require(mlockall_nid_postfix(flags) == -1 && *__error_nid_postfix() == 22); +} + +static void RequireLockAllUnsupported(int flags) { + bool rejected = false; + try { + mlockall_nid_postfix(flags); + } catch (const std::runtime_error&) { + rejected = true; + } + Require(rejected); +} + +#if defined(__linux__) && !defined(ANYPS5_ENABLE_ASAN) +#define HAS_LOCKED_KILOBYTES static std::size_t LockedKilobytes() { std::ifstream status("/proc/self/status"); std::string line; @@ -51,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); @@ -66,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); @@ -76,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); @@ -102,17 +133,33 @@ 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); + RequireLockAllInvalid(1 | 4); + RequireLockAllInvalid(-1); + 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 9cfc631e0a..6c70763d2a 100644 --- a/core/libs/tests/GuestModuleInfo.cpp +++ b/core/libs/tests/GuestModuleInfo.cpp @@ -16,6 +16,10 @@ int APS5_VABI dlclose_nid_postfix(void*); int APS5_VABI sceKernelGetModuleInfoFromAddr(std::uint64_t, int, ModuleInfoEx*); int APS5_VABI sceKernelGetModuleList(KernelModule*, std::size_t, std::size_t*); int APS5_VABI sceKernelGetModuleInfo(KernelModule, ModuleInfo*); +int APS5_VABI sceKernelGetModuleList2(KernelModule*, std::size_t, std::size_t*); +int APS5_VABI sceKernelGetModuleInfo2(KernelModule, ModuleInfo*); +std::int32_t APS5_VABI sceKernelGetLibkernelTextLocation_nid_postfix(void**, std::size_t*); +std::int32_t APS5_VABI sceKernelInternalMemoryGetModuleSegmentInfo_nid_postfix(InternalSegmentInfo*); } static void Require(bool value) { if (!value) std::abort(); } template @@ -94,6 +98,20 @@ int main(int argc, char** argv) { Require(sceKernelGetModuleInfo(handle, &listed) == 0 && listed.name[0] != '\0'); if (handle == self.id) Require(std::string(listed.name) == self.name); } + ModuleInfo program{}; + program.st_size = sizeof(ModuleInfo); + Require(sceKernelGetModuleInfo(self.id, &program) == 0 && program.segment_count > 0); + void* textAddress = nullptr; + std::size_t textSize = 0; + Require(sceKernelGetLibkernelTextLocation_nid_postfix(&textAddress, &textSize) == 0); + Require(reinterpret_cast(textAddress) == program.segments[0].address); + Require(textSize == program.segments[0].size); + Require(ThrowsInvalidArgument([&] { sceKernelGetLibkernelTextLocation_nid_postfix(nullptr, &textSize); })); + Require(ThrowsInvalidArgument([&] { sceKernelGetLibkernelTextLocation_nid_postfix(&textAddress, nullptr); })); + InternalSegmentInfo segment{}; + Require(sceKernelInternalMemoryGetModuleSegmentInfo_nid_postfix(&segment) == 0); + Require(segment.address == program.segments[0].address && segment.size == program.segments[0].size); + Require(ThrowsInvalidArgument([] { sceKernelInternalMemoryGetModuleSegmentInfo_nid_postfix(nullptr); })); for (const std::uint64_t size : {0x158, 0x1a8, 0x1b0, 0x200, 0}) { ModuleInfo sized{}; sized.st_size = size; @@ -105,13 +123,55 @@ int main(int argc, char** argv) { unknown.st_size = sizeof(ModuleInfo); Require(sceKernelGetModuleInfo(info.id, &unknown) == SCE_KERNEL_ERROR_ESRCH); Require(sceKernelGetModuleInfo(-1, &unknown) == SCE_KERNEL_ERROR_ESRCH && unknown.name[0] == '\0'); + std::vector titleHandles(handles.size() + 1, -1); + std::size_t titleCount = 0; + Require(sceKernelGetModuleList2(titleHandles.data(), titleHandles.size(), &titleCount) == 0); + Require(titleCount >= 1 && titleCount < count && titleHandles[titleCount] == -1); + titleHandles.resize(titleCount); + Require(titleHandles.front() == self.id); + for (const auto handle : titleHandles) { + Require(std::count(handles.begin(), handles.end(), handle) == 1); + ModuleInfo title{}, listed{}; + title.st_size = listed.st_size = sizeof(ModuleInfo); + Require(sceKernelGetModuleInfo2(handle, &title) == 0 && sceKernelGetModuleInfo(handle, &listed) == 0); + Require(std::string(title.name) == listed.name && title.segment_count == listed.segment_count); + } + std::size_t titleUntouched = 99; + Require(sceKernelGetModuleList2(titleHandles.data(), titleCount - 1, &titleUntouched) == SCE_KERNEL_ERROR_ENOMEM && titleUntouched == 99); + Require(sceKernelGetModuleList2(nullptr, 0, &titleUntouched) == SCE_KERNEL_ERROR_EFAULT); + Require(sceKernelGetModuleList2(titleHandles.data(), titleHandles.size(), nullptr) == SCE_KERNEL_ERROR_EFAULT); + for (const auto handle : handles) { + if (std::count(titleHandles.begin(), titleHandles.end(), handle) != 0) continue; + ModuleInfo system{}; + system.st_size = sizeof(ModuleInfo); + system.name[0] = 'x'; + Require(sceKernelGetModuleInfo2(handle, &system) == SCE_KERNEL_ERROR_EPERM && system.name[0] == 'x'); + Require(sceKernelGetModuleInfo2(handle, nullptr) == SCE_KERNEL_ERROR_EFAULT); + system.st_size = 0x158; + Require(sceKernelGetModuleInfo2(handle, &system) == SCE_KERNEL_ERROR_EINVAL); + } + ModuleInfo absent{}; + absent.st_size = sizeof(ModuleInfo); + Require(sceKernelGetModuleInfo2(-1, &absent) == SCE_KERNEL_ERROR_ESRCH && absent.name[0] == 0); int local = 0; Query(&local, SCE_KERNEL_ERROR_ESRCH); Require(sceKernelGetModuleInfoFromAddr(reinterpret_cast(add), 2, nullptr) == SCE_KERNEL_ERROR_EFAULT); ModuleInfoEx invalid{}; invalid.st_size = sizeof(ModuleInfoEx); Require(ThrowsInvalidArgument([&] { sceKernelGetModuleInfoFromAddr(reinterpret_cast(add), 1, &invalid); })); - invalid.st_size = sizeof(ModuleInfoEx) - 8; - Require(ThrowsInvalidArgument([&] { sceKernelGetModuleInfoFromAddr(reinterpret_cast(add), 2, &invalid); })); + ModuleInfoEx unset{}; + unset.st_size = 0xf0000; + Require(sceKernelGetModuleInfoFromAddr(reinterpret_cast(add), 2, &unset) == 0 && unset.st_size == sizeof(ModuleInfoEx)); + const auto again = Query(add, 0); + Require(again.eh_frame_hdr_addr == info.eh_frame_hdr_addr && again.eh_frame_hdr_size == info.eh_frame_hdr_size); + Require(again.eh_frame_addr == info.eh_frame_addr && again.eh_frame_size == info.eh_frame_size); Require(dlclose_nid_postfix(module) == 0); + void* reloaded = dlopen_nid_postfix(argv[1], 2); + Require(reloaded != nullptr); + const void* reloadedAdd = dlsym_nid_postfix(reloaded, "GuestModuleAdd"); + 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); + 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..b114030e35 --- /dev/null +++ b/core/libs/tests/GuestMove.cpp @@ -0,0 +1,149 @@ +#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) == kInvalidHandle); + Require(sceMoveReadStateRecent(handle, -1, nullptr, nullptr) == kInvalidHandle); + 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/GuestMutexContention.cpp b/core/libs/tests/GuestMutexContention.cpp new file mode 100644 index 0000000000..dbb0da5e2f --- /dev/null +++ b/core/libs/tests/GuestMutexContention.cpp @@ -0,0 +1,200 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI scePthreadCreate(Pthread*, const PthreadAttr*, PthreadEntry, void*, const char*); +int APS5_VABI scePthreadJoin(Pthread, void**); +int APS5_VABI scePthreadMutexattrInit(PthreadMutexattr*); +int APS5_VABI scePthreadMutexattrSettype(PthreadMutexattr*, int); +int APS5_VABI scePthreadMutexattrDestroy(PthreadMutexattr*); +int APS5_VABI scePthreadMutexInit(PthreadMutex*, const PthreadMutexattr*, const char*); +int APS5_VABI scePthreadMutexDestroy(PthreadMutex*); +int APS5_VABI scePthreadMutexLock(PthreadMutex*); +int APS5_VABI scePthreadMutexTimedlock(PthreadMutex*, KernelUseconds); +int APS5_VABI scePthreadMutexUnlock(PthreadMutex*); +} + +namespace { + +using Clock = std::chrono::steady_clock; +constexpr int TimedOut = static_cast(0x8002003cu); + +void Require(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +std::uint64_t ThreadCycles() { + ULONG64 cycles = 0; + Require(QueryThreadCycleTime(GetCurrentThread(), &cycles) != 0, "QueryThreadCycleTime failed"); + return cycles; +} + +std::uint64_t BusyCycles(int milliseconds) { + const auto before = ThreadCycles(); + const auto until = Clock::now() + std::chrono::milliseconds(milliseconds); + while (Clock::now() < until) YieldProcessor(); + return ThreadCycles() - before; +} + +struct Handoff { + PthreadMutex mutex = nullptr; + bool timed = false; + bool destroy = false; + std::atomic waiting{false}; + std::atomic released{0}; + std::int64_t latencyNanos = 0; + std::uint64_t waitCycles = 0; +}; + +struct HandoffResult { + std::int64_t latencyNanos; + std::uint64_t waitCycles; +}; + +void* APS5_VABI HandoffWorker(void* opaque) { + auto& context = *static_cast(opaque); + const auto cyclesBefore = ThreadCycles(); + context.waiting.store(true); + Require((context.timed ? scePthreadMutexTimedlock(&context.mutex, 5000000) : scePthreadMutexLock(&context.mutex)) == 0, "the waiter did not acquire the mutex"); + const auto acquired = Clock::now().time_since_epoch().count(); + context.waitCycles = ThreadCycles() - cyclesBefore; + context.latencyNanos = acquired - context.released.load(); + Require(scePthreadMutexUnlock(&context.mutex) == 0, "the waiter could not unlock"); + if (context.destroy) Require(scePthreadMutexDestroy(&context.mutex) == 0, "the waiter could not destroy the mutex"); + return nullptr; +} + +HandoffResult RunHandoff(PthreadMutex mutex, bool timed, bool destroy, int holdMilliseconds, int extraMicroseconds = 0) { + Handoff context; + context.mutex = mutex; + context.timed = timed; + context.destroy = destroy; + PthreadMutex holder = mutex; + Require(scePthreadMutexLock(&holder) == 0, "the holder could not lock"); + Pthread worker = nullptr; + Require(scePthreadCreate(&worker, nullptr, HandoffWorker, &context, "mutex waiter") == 0, "cannot create the waiter"); + while (!context.waiting.load()) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(holdMilliseconds)); + const auto extra = Clock::now() + std::chrono::microseconds(extraMicroseconds); + while (Clock::now() < extra) YieldProcessor(); + context.released.store(Clock::now().time_since_epoch().count()); + Require(scePthreadMutexUnlock(&holder) == 0, "the holder could not unlock"); + Require(scePthreadJoin(worker, nullptr) == 0, "cannot join the waiter"); + return {context.latencyNanos, context.waitCycles}; +} + +PthreadMutex CreateMutex(bool recursive) { + PthreadMutexattr attributes = nullptr; + Require(scePthreadMutexattrInit(&attributes) == 0 && scePthreadMutexattrSettype(&attributes, recursive ? 2 : 1) == 0, "cannot prepare mutex attributes"); + PthreadMutex mutex = nullptr; + Require(scePthreadMutexInit(&mutex, &attributes, nullptr) == 0, "cannot create the mutex"); + Require(scePthreadMutexattrDestroy(&attributes) == 0, "cannot destroy mutex attributes"); + return mutex; +} + +struct Contention { + PthreadMutex mutex = nullptr; + std::uint64_t rounds = 0; + std::atomic start{false}; + std::atomic stop{false}; + std::uint64_t shared = 0; + std::array acquired{}; + std::atomic next{0}; +}; + +void* APS5_VABI ContentionWorker(void* opaque) { + auto& context = *static_cast(opaque); + const int index = context.next.fetch_add(1); + PthreadMutex mutex = context.mutex; + while (!context.start.load()) std::this_thread::yield(); + std::uint64_t count = 0; + while (context.rounds ? count < context.rounds : !context.stop.load(std::memory_order_relaxed)) { + Require(scePthreadMutexLock(&mutex) == 0, "contended lock failed"); + const std::uint64_t seen = context.shared; + for (int spin = 0; spin < 64; ++spin) YieldProcessor(); + context.shared = seen + 1; + Require(scePthreadMutexUnlock(&mutex) == 0, "contended unlock failed"); + ++count; + for (int spin = 0; spin < 64; ++spin) YieldProcessor(); + } + context.acquired[index] = count; + return nullptr; +} + +void* APS5_VABI TimeoutWorker(void* opaque) { + PthreadMutex mutex = *static_cast(opaque); + const auto started = Clock::now(); + Require(scePthreadMutexTimedlock(&mutex, 20000) == TimedOut, "a timed lock of a held mutex did not time out"); + Require(Clock::now() - started >= std::chrono::milliseconds(20), "a timed lock of a held mutex timed out before its deadline"); + return nullptr; +} + +} + +int main(int argc, char** argv) { + const bool report = argc > 1 && std::strcmp(argv[1], "--report") == 0; + const std::uint64_t busyCycles = report ? BusyCycles(20) * 10 : 0; + for (const bool recursive : {false, true}) { + PthreadMutex mutex = CreateMutex(recursive); + for (const bool timed : {false, true}) { + if (!report) { + for (int round = 0; round < 4; ++round) RunHandoff(mutex, timed, false, 3); + continue; + } + std::vector latencies; + for (int round = 0; round < 31; ++round) latencies.push_back(RunHandoff(mutex, timed, false, 3, round * 617 % 1000).latencyNanos); + std::sort(latencies.begin(), latencies.end()); + const auto blocked = RunHandoff(mutex, timed, false, 200); + const double share = 100.0 * static_cast(blocked.waitCycles) / static_cast(busyCycles); + std::printf("%s %s: handoff median %.1f us, p90 %.1f us, max %.1f us; a waiter blocked for 200 ms used %.1f %% of one processor\n", + recursive ? "recursive" : "errorcheck", timed ? "timed lock" : "lock", + latencies[15] / 1000.0, latencies[27] / 1000.0, latencies[30] / 1000.0, share); + } + PthreadMutex held = mutex; + Require(scePthreadMutexLock(&held) == 0, "cannot hold the mutex for the timeout"); + Pthread timeout = nullptr; + Require(scePthreadCreate(&timeout, nullptr, TimeoutWorker, &mutex, "mutex timeout") == 0, "cannot create the timeout waiter"); + Require(scePthreadJoin(timeout, nullptr) == 0, "cannot join the timeout waiter"); + Require(scePthreadMutexUnlock(&held) == 0, "cannot release the mutex after the timeout"); + Contention context; + context.mutex = mutex; + context.rounds = report ? 0 : 20000; + std::array workers{}; + for (auto& worker : workers) Require(scePthreadCreate(&worker, nullptr, ContentionWorker, &context, "mutex contender") == 0, "cannot create a contender"); + const auto started = Clock::now(); + context.start.store(true); + if (report) { + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + context.stop.store(true); + } + for (auto& worker : workers) Require(scePthreadJoin(worker, nullptr) == 0, "cannot join a contender"); + const double seconds = std::chrono::duration(Clock::now() - started).count(); + std::uint64_t total = 0; + for (const auto count : context.acquired) total += count; + Require(total == context.shared, "the mutex did not exclude its contenders"); + const auto fewest = *std::min_element(context.acquired.begin(), context.acquired.end()); + if (report) { + std::printf("%s: four contenders made %.0f acquisitions per second, the slowest %.0f per second\n", + recursive ? "recursive" : "errorcheck", total / seconds, fewest / seconds); + } else { + Require(fewest == context.rounds, "a contender did not finish its acquisitions"); + } + Require(scePthreadMutexDestroy(&mutex) == 0, "cannot destroy the mutex"); + for (int round = 0; round < 200; ++round) RunHandoff(CreateMutex(recursive), round % 2 != 0, true, 0); + } + std::puts("guest mutex contention: PASS"); + return 0; +} diff --git a/core/libs/tests/GuestMutexRaiseException.cpp b/core/libs/tests/GuestMutexRaiseException.cpp new file mode 100644 index 0000000000..ecdd471b4e --- /dev/null +++ b/core/libs/tests/GuestMutexRaiseException.cpp @@ -0,0 +1,52 @@ +#include "SceTypes.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 scePthreadMutexInit(PthreadMutex*, const PthreadMutexattr*, const char*); +int APS5_VABI scePthreadMutexDestroy(PthreadMutex*); +int APS5_VABI scePthreadMutexLock(PthreadMutex*); +int APS5_VABI scePthreadMutexUnlock(PthreadMutex*); +int APS5_VABI sceKernelInstallExceptionHandler(int, void*); +int APS5_VABI sceKernelRemoveExceptionHandler(int); +int APS5_VABI sceKernelRaiseException(Pthread, int); +} + +static constexpr int SigUsr1 = 30; +static void Require(bool value) { if (!value) std::abort(); } +static std::atomic deliveries{0}; +static void APS5_VABI Handler(int, void*) { deliveries.fetch_add(1); } + +struct Context { PthreadMutex mutex = nullptr; std::atomic started{false}; std::atomic acquired{false}; }; +static void* APS5_VABI Worker(void* opaque) { + auto& context = *static_cast(opaque); + context.started.store(true); + Require(scePthreadMutexLock(&context.mutex) == 0); + context.acquired.store(true); + Require(scePthreadMutexUnlock(&context.mutex) == 0); + return nullptr; +} + +int main() { + Require(sceKernelInstallExceptionHandler(SigUsr1, reinterpret_cast(&Handler)) == 0); + Context context; + Require(scePthreadMutexInit(&context.mutex, nullptr, nullptr) == 0); + Require(scePthreadMutexLock(&context.mutex) == 0); + Pthread worker = nullptr; + Require(scePthreadCreate(&worker, nullptr, Worker, &context, "mutex wait") == 0); + while (!context.started.load()) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + Require(sceKernelRaiseException(worker, SigUsr1) == 0); + for (int i = 0; i < 5000 && deliveries.load() == 0; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + Require(deliveries.load() == 1); + Require(scePthreadMutexUnlock(&context.mutex) == 0); + Require(scePthreadJoin(worker, nullptr) == 0); + Require(context.acquired.load()); + Require(scePthreadMutexDestroy(&context.mutex) == 0); + Require(sceKernelRemoveExceptionHandler(SigUsr1) == 0); +} \ No newline at end of file diff --git a/core/libs/tests/GuestNamedShmConcurrency.cpp b/core/libs/tests/GuestNamedShmConcurrency.cpp new file mode 100644 index 0000000000..60d91794a2 --- /dev/null +++ b/core/libs/tests/GuestNamedShmConcurrency.cpp @@ -0,0 +1,167 @@ +#include "SceTypes.hpp" +#include "nid/Sha1.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 shm_open_nid_postfix(const char*, int, int); +int APS5_VABI ftruncate_nid_postfix(int, 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 fstat_nid_disambig1_nid_postfix(int, FileStat*); +int APS5_VABI fchmod_nid_postfix(int, int); +int APS5_VABI close_nid_postfix(int); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { +using Unlink = int (*)(const char*); +constexpr std::int64_t Page = 0x4000; +constexpr int ReadWriteCreate = 0xa02; + +struct Object { + std::string name = "/anyps5-concurrent-" + std::to_string(getpid()); + std::string hostName = "/anyps5-shm-"; + Object() { + constexpr char hex[] = "0123456789abcdef"; + const auto digest = Nid::Sha1(std::vector(name.begin(), name.end())); + for (const auto byte : digest) { + hostName += hex[byte >> 4]; + hostName += hex[byte & 15]; + } + ::shm_unlink(hostName.c_str()); + } + ~Object() { ::shm_unlink(hostName.c_str()); } +}; + +bool Race(int descriptor) { + for (int iteration = 0; iteration < 500; ++iteration) { + const auto size = (iteration & 1) ? 0 : Page; + if (ftruncate_nid_postfix(descriptor, size) != 0) return false; + const auto written = pwrite_nid_disambig1_nid_postfix(descriptor, "x", 1, Page / 2); + if (written < 0) return false; + FileStat info{}; + if (fstat_nid_disambig1_nid_postfix(descriptor, &info) != 0 || + (info.st_size != 0 && info.st_size != Page)) return false; + } + return true; +} + +bool NoDescriptorFallback(int descriptor) { + if (ftruncate_nid_postfix(descriptor, Page) != 0) return false; + const pid_t child = fork(); + if (child < 0) return false; + if (child == 0) { + const struct rlimit limit{0, 0}; + if (setrlimit(RLIMIT_NOFILE, &limit) != 0) _exit(2); + std::int64_t written = -2; + try { + written = pwrite_nid_disambig1_nid_postfix(descriptor, "x", 1, Page); + } catch (const std::exception& exception) { + std::fprintf(stderr, "low-RLIMIT child: fd=%d pwrite threw=%s guest_errno=%d native_errno=%d\n", + descriptor, exception.what(), *__error_nid_postfix(), errno); + _exit(3); + } catch (...) { + std::fprintf(stderr, "low-RLIMIT child: fd=%d pwrite threw unknown exception guest_errno=%d native_errno=%d\n", + descriptor, *__error_nid_postfix(), errno); + _exit(3); + } + const int guestError = *__error_nid_postfix(); + const int nativeError = errno; + struct stat info{}; + if (::fstat(descriptor, &info) != 0) { + std::fprintf(stderr, "low-RLIMIT child: fd=%d pwrite=%lld guest_errno=%d native_errno=%d fstat_errno=%d\n", + descriptor, static_cast(written), guestError, nativeError, errno); + _exit(4); + } + if (written != 0 && !(written == -1 && guestError == 24)) { + std::fprintf(stderr, "low-RLIMIT child: fd=%d pwrite=%lld guest_errno=%d native_errno=%d st_size=%lld\n", + descriptor, static_cast(written), guestError, nativeError, static_cast(info.st_size)); + _exit(5); + } + if (info.st_size != Page) { + std::fprintf(stderr, "low-RLIMIT child: fd=%d pwrite=%lld guest_errno=%d native_errno=%d st_size=%lld expected=%lld\n", + descriptor, static_cast(written), guestError, nativeError, + static_cast(info.st_size), static_cast(Page)); + _exit(6); + } + _exit(0); + } + int status = 0; + return waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0; +} + +bool ChildrenSucceeded(const pid_t* children, int count) { + bool okay = true; + for (int index = 0; index < count; ++index) { + int status = 0; + const auto result = waitpid(children[index], &status, 0); + okay = result == children[index] && WIFEXITED(status) && WEXITSTATUS(status) == 0 && okay; + } + return okay; +} + +void Run() { + Object object; + const auto unlinkObject = reinterpret_cast(dlsym(RTLD_DEFAULT, "shm_unlink_nid_postfix")); + if (unlinkObject == nullptr) throw std::runtime_error("guest shm_unlink export is missing"); + const int descriptor = shm_open_nid_postfix(object.name.c_str(), ReadWriteCreate, 0600); + if (descriptor < 0) throw std::runtime_error("shared object creation failed"); + + pid_t children[4]{}; + int childCount = 0; + bool okay = true; + for (int index = 0; index < 4; ++index) { + const pid_t child = fork(); + if (child < 0) { + okay = false; + break; + } + if (child == 0) { + const int own = (index & 1) ? shm_open_nid_postfix(object.name.c_str(), 2, 0600) : descriptor; + const bool childOkay = own >= 0 && Race(own); + if ((index & 1) && own >= 0) close_nid_postfix(own); + _exit(childOkay ? 0 : 1); + } + children[childCount++] = child; + } + okay = Race(descriptor) && okay; + okay = ChildrenSucceeded(children, childCount) && okay; + if (!okay) throw std::runtime_error("interleaved resize/write produced an invalid shared-object size"); + if (!NoDescriptorFallback(descriptor)) + throw std::runtime_error("EOF write with no spare descriptors grew the shared object or returned an unexpected error"); + + if (fchmod_nid_postfix(descriptor, 0400) != 0) + throw std::runtime_error("changing shared-object permissions to read-only failed"); + *__error_nid_postfix() = 0; + if (unlinkObject(object.name.c_str()) != -1 || *__error_nid_postfix() != 13) + throw std::runtime_error("unlink of a read-only shared object did not return EACCES"); + if (fchmod_nid_postfix(descriptor, 0600) != 0) + throw std::runtime_error("restoring shared-object permissions failed"); + if (unlinkObject(object.name.c_str()) != 0) + throw std::runtime_error("unlink after restoring write permission failed"); + if (close_nid_postfix(descriptor) != 0) + throw std::runtime_error("shared-object close failed"); + std::puts("cross-process truncate/write, descriptor sharing and unlink permissions passed"); +} +} + +int main() { + try { + Run(); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestNamedShmDescriptorReuse.cpp b/core/libs/tests/GuestNamedShmDescriptorReuse.cpp new file mode 100644 index 0000000000..e2b7263e23 --- /dev/null +++ b/core/libs/tests/GuestNamedShmDescriptorReuse.cpp @@ -0,0 +1,177 @@ +#include "SceTypes.hpp" +#include "nid/Sha1.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI shm_open_nid_postfix(const char*, int, int); +int APS5_VABI close_nid_postfix(int); +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); +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(); +} + +namespace { +constexpr int ReadWrite = 2; +constexpr int Create = 0x200; +constexpr int Exclusive = 0x800; +constexpr int Shared = 0x1; +constexpr std::size_t Page = 0x4000; +int armedDescriptor = -1; +int replacementDescriptor = -1; +bool armed = false; +bool swapped = false; + +int CreateOrdinaryFile() { + char path[] = "/tmp/anyps5-descriptor-reuse-XXXXXX"; + const int descriptor = ::mkstemp(path); + if (descriptor < 0) throw std::runtime_error("ordinary file creation failed"); + if (::unlink(path) != 0) { + ::close(descriptor); + throw std::runtime_error("ordinary file unlink failed"); + } + constexpr char contents[] = "ordinary file contents"; + if (::write(descriptor, contents, sizeof(contents)) != static_cast(sizeof(contents)) || + ::lseek(descriptor, 0, SEEK_SET) != 0) { + ::close(descriptor); + throw std::runtime_error("ordinary file initialization failed"); + } + return descriptor; +} + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(std::string(message) + " (guest errno=" + std::to_string(*__error_nid_postfix()) + ")"); +} + +struct Object { + std::string name = "/anyps5-descriptor-reuse-" + std::to_string(getpid()); + std::string hostName = "/anyps5-shm-"; + Object() { + constexpr char hex[] = "0123456789abcdef"; + const auto digest = Nid::Sha1(std::vector(name.begin(), name.end())); + for (const auto byte : digest) { + hostName += hex[byte >> 4]; + hostName += hex[byte & 15]; + } + ::shm_unlink(hostName.c_str()); + } + ~Object() { ::shm_unlink(hostName.c_str()); } + int Open(int flags) const { return shm_open_nid_postfix(name.c_str(), flags, 0600); } +}; +} + +extern "C" int fcntl(int descriptor, int command, ...) { + long argument = 0; + switch (command) { + case F_GETFD: + case F_GETFL: + case F_GETOWN: +#ifdef F_GETSIG + case F_GETSIG: +#endif +#ifdef F_GETLEASE + case F_GETLEASE: +#endif +#ifdef F_GETPIPE_SZ + case F_GETPIPE_SZ: +#endif +#ifdef F_GET_SEALS + case F_GET_SEALS: +#endif + break; +#ifdef F_GETLK + case F_GETLK: + case F_SETLK: + case F_SETLKW: { + va_list args; + va_start(args, command); + argument = reinterpret_cast(va_arg(args, void*)); + va_end(args); + break; + } +#endif + case F_DUPFD: + case F_DUPFD_CLOEXEC: + case F_SETFD: + case F_SETFL: + case F_SETOWN: +#ifdef F_SETSIG + case F_SETSIG: +#endif +#ifdef F_SETLEASE + case F_SETLEASE: +#endif +#ifdef F_NOTIFY + case F_NOTIFY: +#endif +#ifdef F_SETPIPE_SZ + case F_SETPIPE_SZ: +#endif +#ifdef F_ADD_SEALS + case F_ADD_SEALS: +#endif + { + va_list args; + va_start(args, command); + argument = va_arg(args, int); + va_end(args); + break; + } + default: + errno = EINVAL; + return -1; + } + const long result = ::syscall(SYS_fcntl, descriptor, command, argument); + if (command == F_GETFL && armed && !swapped) { + swapped = true; + if (::syscall(SYS_dup2, replacementDescriptor, armedDescriptor) != armedDescriptor) return -1; + } + return static_cast(result); +} + +int main() { + try { + Object object; + const int descriptor = object.Open(ReadWrite | Create | Exclusive); + Require(descriptor >= 0 && ftruncate_nid_postfix(descriptor, Page) == 0, "shared-memory setup failed"); + Require(pwrite_nid_disambig1_nid_postfix(descriptor, "original", 8, 0) == 8, "shared-memory seed failed"); + const int verifier = object.Open(ReadWrite); + Require(verifier >= 0, "shared-memory verifier open failed"); + const int replacement = CreateOrdinaryFile(); + Require(replacement >= 0, "ordinary-file setup failed"); + armedDescriptor = descriptor; + replacementDescriptor = replacement; + armed = true; + auto* mapped = static_cast(mmap_nid_postfix(nullptr, Page, 3, Shared, descriptor, 0)); + armed = false; + Require(swapped, "native F_GETFL interposer did not replace the guest descriptor"); + Require(mapped != reinterpret_cast(static_cast(-1)), "guest mmap failed after descriptor replacement"); + Require(std::memcmp(mapped, "original", 8) == 0, "mapping followed the replacement descriptor instead of the pinned shared-memory object"); + mapped[0] = 0x70; + char byte = 0; + Require(pread_nid_postfix(verifier, &byte, 1, 0) == 1 && byte == 0x70, "mapping did not update the original shared-memory object"); + Require(munmap_nid_postfix(mapped, Page) == 0, "guest mapping cleanup failed"); + Require(close_nid_postfix(descriptor) == 0 && close_nid_postfix(verifier) == 0, "guest descriptor cleanup failed"); + Require(::close(replacement) == 0, "replacement descriptor cleanup failed"); + std::puts("named shared-memory descriptor reuse checks passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestNamedShmIoDescriptorReuse.cpp b/core/libs/tests/GuestNamedShmIoDescriptorReuse.cpp new file mode 100644 index 0000000000..368fb3cc60 --- /dev/null +++ b/core/libs/tests/GuestNamedShmIoDescriptorReuse.cpp @@ -0,0 +1,139 @@ +#include "nid/Sha1.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct GuestIovec { void* base; std::size_t length; }; +extern "C" { +int APS5_VABI shm_open_nid_postfix(const char*, int, int); +int APS5_VABI close_nid_postfix(int); +int APS5_VABI dup2_nid_postfix(int, int); +int APS5_VABI ftruncate_nid_postfix(int, std::int64_t); +std::int64_t APS5_VABI write_nid_postfix(int, const char*, 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 lseek_nid_postfix(int, std::int64_t, int); +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 __error_nid_postfix(); +} +namespace { +int original = -1; +int replacement = -1; +bool armed = false; +bool replaceAtProbe = false; +bool replaceBeforeProbe = false; +bool swapped = false; +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} +struct Object { + explicit Object(const char* suffix) : name("/anyps5-io-reuse-" + std::to_string(getpid()) + suffix) { + const auto digest = Nid::Sha1(std::vector(name.begin(), name.end())); + for (const auto byte : digest) { + hostName += "0123456789abcdef"[byte >> 4]; + hostName += "0123456789abcdef"[byte & 15]; + } + descriptor = shm_open_nid_postfix(name.c_str(), 2 | 0x200 | 0x800, 0600); + Require(descriptor >= 0, "object creation failed"); + Require(::ftruncate(descriptor, 0x4000) == 0, "object sizing failed"); + Require(::pwrite(descriptor, "safe", 4, 0) == 4, "object seeding failed"); + } + ~Object() { if (descriptor >= 0) ::close(descriptor); ::shm_unlink(hostName.c_str()); } + Object(const Object&) = delete; + Object& operator=(const Object&) = delete; + std::string name; + std::string hostName = "/anyps5-shm-"; + int descriptor = -1; +}; +void Run(int operation, int boundary) { + replaceAtProbe = boundary == 1; + replaceBeforeProbe = boundary == 2; + Object first(("-first-" + std::to_string(operation)).c_str()); + Object second(("-second-" + std::to_string(operation)).c_str()); + if (replaceBeforeProbe) { + char path[] = "/tmp/anyps5-io-reuse-XXXXXX"; + const int temporary = ::mkstemp(path); + Require(temporary >= 0, "ordinary replacement creation failed"); + Require(::unlink(path) == 0, "ordinary replacement unlink failed"); + Require(::ftruncate(temporary, 0x4000) == 0 && ::pwrite(temporary, "safe", 4, 0) == 4, + "ordinary replacement initialization failed"); + Require(::dup2(temporary, second.descriptor) == second.descriptor, "ordinary replacement installation failed"); + ::close(temporary); + } + original = first.descriptor; + replacement = second.descriptor; + swapped = false; + armed = true; + char payload[] = "bad!"; + GuestIovec vector{payload, 4}; + std::int64_t result = 0; + switch (operation) { + case 0: result = write_nid_postfix(original, payload, 4); break; + case 1: result = pwrite_nid_disambig1_nid_postfix(original, payload, 4, 0); break; + case 2: result = ftruncate_nid_postfix(original, 7); break; + case 3: result = lseek_nid_postfix(original, 7, SEEK_SET); break; + case 4: result = writev_nid_postfix(original, &vector, 1); break; + case 5: result = pwritev_nid_postfix(original, &vector, 1, 0); break; + } + armed = false; + Require(swapped, "descriptor replacement did not run at the selected acquisition boundary"); + struct stat status{}; + char bytes[4]{}; + Require(::fstat(replacement, &status) == 0 && status.st_size == 0x4000, "replacement object size changed"); + Require(::pread(replacement, bytes, 4, 0) == 4 && std::memcmp(bytes, "safe", 4) == 0, "replacement object data changed"); + Require(::lseek(replacement, 0, SEEK_CUR) == 0, "replacement object offset changed"); + Require(result == -1 && *__error_nid_postfix() == 9, "descriptor identity change was not rejected with EBADF"); +} +} +static bool ReplaceDescriptor() { + swapped = true; + bool replaced = false; + std::thread reuse([&] { + replaced = close_nid_postfix(original) == 0 && dup2_nid_postfix(replacement, original) == original; + }); + reuse.join(); + return replaced; +} +extern "C" ssize_t readlink(const char* path, char* buffer, std::size_t length) { + if (armed && replaceBeforeProbe && !swapped) { + if (!ReplaceDescriptor()) return -1; + } + const auto result = static_cast(::syscall(SYS_readlink, path, buffer, length)); + if (result >= 0 && armed && replaceAtProbe && !swapped) { + if (!ReplaceDescriptor()) return -1; + } + return result; +} +extern "C" int flock(int descriptor, int operation) { + const int result = static_cast(::syscall(SYS_flock, descriptor, operation)); + if (result == 0 && operation == LOCK_EX && armed && !replaceAtProbe && !replaceBeforeProbe && !swapped) { + if (!ReplaceDescriptor()) return -1; + } + return result; +} +int main() { + int failures = 0; + for (int scenario = 0; scenario < 18; ++scenario) { + const int operation = scenario % 6; + try { + Run(operation, scenario / 6); + } catch (const std::exception& error) { + std::fprintf(stderr, "scenario %d: %s\n", scenario, error.what()); + ++failures; + } + } + if (failures != 0) return 1; + std::puts("guest shared-memory I/O descriptor reuse passed"); + return 0; +} diff --git a/core/libs/tests/GuestNamedShmLifecycle.cpp b/core/libs/tests/GuestNamedShmLifecycle.cpp new file mode 100644 index 0000000000..a5c26b8bee --- /dev/null +++ b/core/libs/tests/GuestNamedShmLifecycle.cpp @@ -0,0 +1,228 @@ +#include "SceTypes.hpp" +#include "nid/Sha1.hpp" +#include "prx/libc/include/GuestWriteWatch.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 shm_open_nid_postfix(const char*, int, int); +int APS5_VABI close_nid_postfix(int); +int APS5_VABI ftruncate_nid_postfix(int, std::int64_t); +std::int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int, FileStat*); +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 read_nid_postfix(int, void*, std::uint64_t); +std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); +std::int64_t APS5_VABI lseek_nid_postfix(int, std::int64_t, int); +int APS5_VABI dup2_nid_postfix(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; +int APS5_VABI mprotect_nid_postfix(void*, std::size_t, int); +int APS5_VABI open_nid_postfix(const char*, int, int); +std::int64_t APS5_VABI writev_nid_postfix(int, const struct GuestIovec*, int); +std::int64_t APS5_VABI pwritev_nid_postfix(int, const struct GuestIovec*, int, std::int64_t); +int* APS5_VABI __error_nid_postfix(); +} + +struct GuestIovec { + void* base; + std::size_t length; +}; + +namespace { +constexpr int ReadOnly = 0; +constexpr int ReadWrite = 2; +constexpr int Create = 0x200; +constexpr int Exclusive = 0x800; +constexpr int Shared = 0x1; +constexpr int Private = 0x2; +constexpr std::size_t Page = 0x4000; +using Unlink = int (*)(const char*); + +int CreateOrdinaryFile() { + char path[] = "/tmp/anyps5-lifecycle-XXXXXX"; + const int descriptor = ::mkstemp(path); + if (descriptor < 0) throw std::runtime_error("ordinary file creation failed"); + if (::unlink(path) != 0) { + ::close(descriptor); + throw std::runtime_error("ordinary file unlink failed"); + } + constexpr char contents[] = "ordinary file contents"; + if (::write(descriptor, contents, sizeof(contents)) != static_cast(sizeof(contents)) || + ::lseek(descriptor, 0, SEEK_SET) != 0) { + ::close(descriptor); + throw std::runtime_error("ordinary file initialization failed"); + } + return descriptor; +} + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(std::string(message) + " (guest errno=" + std::to_string(*__error_nid_postfix()) + ")"); +} + +struct Object { + std::string name = "/anyps5-lifecycle-" + std::to_string(getpid()); + std::string hostName = "/anyps5-shm-"; + Object() { + constexpr char hex[] = "0123456789abcdef"; + const auto digest = Nid::Sha1(std::vector(name.begin(), name.end())); + for (const auto byte : digest) { + hostName += hex[byte >> 4]; + hostName += hex[byte & 15]; + } + ::shm_unlink(hostName.c_str()); + } + ~Object() { ::shm_unlink(hostName.c_str()); } + int Open(int flags) const { return shm_open_nid_postfix(name.c_str(), flags, 0600); } +}; + +void Run() { + Object object; + const char* anonymousName = reinterpret_cast(1); + const int anonymous = shm_open_nid_postfix(anonymousName, 0x100e02, 0600); + Require(anonymous >= 0 && ftruncate_nid_postfix(anonymous, Page) == 0, "anonymous shared-memory setup failed"); + auto* anonymousMap = static_cast(mmap_nid_postfix(nullptr, Page, 3, Shared, anonymous, 0)); + Require(anonymousMap != reinterpret_cast(static_cast(-1)), "anonymous MAP_SHARED failed"); + anonymousMap[0] = 'a'; + char anonymousByte = 0; + Require(pread_nid_postfix(anonymous, &anonymousByte, 1, 0) == 1 && anonymousByte == 'a', + "anonymous MAP_SHARED write was not visible through the descriptor"); + Require(munmap_nid_postfix(anonymousMap, Page) == 0 && close_nid_postfix(anonymous) == 0, "anonymous shared-memory cleanup failed"); + const int descriptor = object.Open(ReadWrite | Create | Exclusive); + Require(descriptor >= 0, "named object creation failed"); + Require(ftruncate_nid_postfix(descriptor, Page) == 0, "guest resize failed"); + Require(pwrite_nid_disambig1_nid_postfix(descriptor, "old", 4, 0) == 4, "guest positioned write failed"); + char bytes[8]{}; + Require(pread_nid_postfix(descriptor, bytes, sizeof(bytes), Page) == 0, "read at EOF was not empty"); + Require(pwrite_nid_disambig1_nid_postfix(descriptor, nullptr, 0, Page) == 0, "zero-length write failed"); + *__error_nid_postfix() = 0; + Require(lseek_nid_postfix(descriptor, Page + 2, SEEK_SET) == -1 && *__error_nid_postfix() == 22, + "seek past EOF did not fail with EINVAL"); + Require(lseek_nid_postfix(descriptor, 0, SEEK_END) == static_cast(Page), "seek to EOF returned the wrong position"); + Require(write_nid_postfix(descriptor, "z", 1) == 0, "write at EOF grew the shared object"); + Require(lseek_nid_postfix(descriptor, Page - 1, SEEK_SET) == static_cast(Page - 1), "seek near EOF failed"); + Require(write_nid_postfix(descriptor, "xy", 2) == 1, "cross-EOF write did not return its short length"); + Require(lseek_nid_postfix(descriptor, 0, SEEK_END) == static_cast(Page), "cross-EOF write changed object size"); + char vectorBytes[] = "uv"; + GuestIovec vectorWrite{vectorBytes, 2}; + Require(pwritev_nid_postfix(descriptor, nullptr, 0, Page) == 0, "zero-vector positioned write on a writable descriptor failed"); + Require(pwritev_nid_postfix(descriptor, &vectorWrite, 1, Page) == 0, "positioned vector write at EOF grew the object"); + Require(pwritev_nid_postfix(descriptor, &vectorWrite, 1, Page - 1) == 1, "positioned vector write past EOF was not short"); + Require(lseek_nid_postfix(descriptor, Page - 1, SEEK_SET) == static_cast(Page - 1), "seek before vector write failed"); + Require(writev_nid_postfix(descriptor, &vectorWrite, 1) == 1, "vector write past EOF was not short"); + Require(lseek_nid_postfix(descriptor, 0, SEEK_END) == static_cast(Page), "vector write changed object size"); + *__error_nid_postfix() = 0; + Require(lseek_nid_postfix(descriptor, -1, SEEK_SET) == -1 && *__error_nid_postfix() == 22, + "negative seek did not return EINVAL"); + Require(lseek_nid_postfix(descriptor, std::numeric_limits::max(), SEEK_CUR) == -1 && + *__error_nid_postfix() == 84, "overflowing seek did not return EOVERFLOW"); + + const int second = object.Open(ReadWrite); + Require(second >= 0 && pread_nid_postfix(second, bytes, 4, 0) == 4 && std::memcmp(bytes, "old", 4) == 0, + "independent open did not see the shared object"); + auto* firstMap = static_cast(mmap_nid_postfix(nullptr, Page, 3, Shared, descriptor, 0)); + Require(firstMap != reinterpret_cast(static_cast(-1)), "guest shared mapping failed"); + Require(!GuestWriteWatch::GuestWriteWatchCovers_nid_postfix(reinterpret_cast(firstMap), Page), + "file-backed shared mapping was registered with write watch"); + auto* secondMap = static_cast(mmap_nid_postfix(nullptr, Page, 3, Shared, second, 0)); + Require(secondMap != reinterpret_cast(static_cast(-1)), "second guest shared mapping failed"); + Require(!GuestWriteWatch::GuestWriteWatchCovers_nid_postfix(reinterpret_cast(secondMap), Page), + "file-backed shared alias was registered with write watch"); + *__error_nid_postfix() = 0; + Require(mmap_nid_postfix(nullptr, Page, 1, Shared, second, 1) == reinterpret_cast(static_cast(-1)) && + *__error_nid_postfix() == 22, "unaligned shared mapping offset did not return EINVAL"); + firstMap[0] = 'n'; + Require(secondMap[0] == 'n', "shared mappings did not observe each other's writes"); + auto* privateMap = static_cast(mmap_nid_postfix(nullptr, Page, 3, Private, second, 0)); + Require(privateMap != reinterpret_cast(static_cast(-1)), "guest private mapping failed"); + firstMap[1] = 'v'; + Require(privateMap[1] == 'v', "private mapping did not reflect backing changes before its first write"); + privateMap[0] = 'p'; + Require(firstMap[0] == 'n', "private mapping write escaped copy-on-write isolation"); + Require(pread_nid_postfix(second, bytes, 1, 0) == 1 && bytes[0] == 'n', "private mapping write changed the backing object"); + const int readOnly = object.Open(ReadOnly); + Require(readOnly >= 0, "read-only open failed"); + *__error_nid_postfix() = 0; + Require(pwrite_nid_disambig1_nid_postfix(readOnly, "x", 1, 0) == -1 && *__error_nid_postfix() == 9, + "positioned write on a read-only descriptor did not return EBADF"); + *__error_nid_postfix() = 0; + Require(pwrite_nid_disambig1_nid_postfix(readOnly, "x", 1, Page) == -1 && *__error_nid_postfix() == 9, + "positioned write at EOF on a read-only descriptor did not return EBADF"); + *__error_nid_postfix() = 0; + Require(pwritev_nid_postfix(readOnly, nullptr, 0, 0) == -1 && *__error_nid_postfix() == 9, + "zero-vector positioned write on a read-only descriptor did not return EBADF"); + FileStat readOnlyInfo{}; + Require(fstat_nid_disambig1_nid_postfix(readOnly, &readOnlyInfo) == 0 && readOnlyInfo.st_size == Page, + "read-only EOF write changed the shared-object size"); + void* rejected = mmap_nid_postfix(nullptr, Page, 3, Shared, readOnly, 0); + Require(rejected == reinterpret_cast(static_cast(-1)), "writable shared mapping accepted a read-only descriptor"); + auto* writableMap = static_cast(mmap_nid_postfix(nullptr, Page, 1, Shared, descriptor, 0)); + Require(writableMap != reinterpret_cast(static_cast(-1)), "read-protected writable-fd mapping failed"); + Require(mprotect_nid_postfix(writableMap, Page, 3) == 0, "mprotect could not restore write access from a writable descriptor"); + writableMap[2] = 'm'; + Require(firstMap[2] == 'm', "mprotect-upgraded mapping did not remain shared"); + Require(munmap_nid_postfix(writableMap, Page) == 0, "upgraded mapping unmap failed"); + auto* readOnlyMap = static_cast(mmap_nid_postfix(nullptr, Page, 1, Shared, readOnly, 0)); + Require(readOnlyMap != reinterpret_cast(static_cast(-1)), "read-only shared mapping failed"); + *__error_nid_postfix() = 0; + Require(mprotect_nid_postfix(readOnlyMap, Page, 3) == -1 && *__error_nid_postfix() == 13, + "mprotect on a read-only shared mapping did not return EACCES"); + Require(munmap_nid_postfix(readOnlyMap, Page) == 0, "read-only mapping unmap failed"); + + const int ordinary = CreateOrdinaryFile(); + Require(ordinary >= 0, "ordinary file setup failed"); + Require(dup2_nid_postfix(ordinary, second) == second, "dup2 onto shared descriptor failed"); + Require(lseek_nid_postfix(second, 0, SEEK_SET) == 0 && read_nid_postfix(second, bytes, sizeof(bytes)) > 0, + "dup2 target did not become the ordinary file"); + Require(close_nid_postfix(second) == 0, "close after dup2 failed"); + Require(lseek_nid_postfix(ordinary, 0, SEEK_SET) == 0 && read_nid_postfix(ordinary, bytes, sizeof(bytes)) > 0, + "closing reused descriptor damaged the ordinary file"); + Require(close_nid_postfix(ordinary) == 0, "ordinary file close failed"); + const int reused = object.Open(ReadWrite); + Require(reused >= 0, "shared object reopen after descriptor close failed"); + Require(dup2_nid_postfix(reused, second) == second, "guest dup2 into the freed descriptor slot failed"); + Require(pread_nid_postfix(second, bytes, 3, 0) == 3 && std::memcmp(bytes, "nvm", 3) == 0, + "reused descriptor slot retained ordinary-file state"); + Require(close_nid_postfix(second) == 0, "reused descriptor slot close failed"); + if (reused != second) Require(close_nid_postfix(reused) == 0, "reopened descriptor close failed"); + + Require(close_nid_postfix(descriptor) == 0 && close_nid_postfix(readOnly) == 0, "descriptor close failed"); + Require(firstMap[0] == 'n', "mapping did not survive descriptor close"); + const auto unlinkObject = reinterpret_cast(dlsym(RTLD_DEFAULT, "shm_unlink_nid_postfix")); + Require(unlinkObject != nullptr, "guest shm_unlink export is missing"); + Require(unlinkObject(object.name.c_str()) == 0, "guest unlink failed"); + *__error_nid_postfix() = 0; + Require(unlinkObject(object.name.c_str()) == -1 && *__error_nid_postfix() == 2, "missing-name unlink did not return ENOENT"); + const int recreated = object.Open(ReadWrite | Create | Exclusive); + Require(recreated >= 0, "name recreation after unlink failed"); + Require(ftruncate_nid_postfix(recreated, Page) == 0, "recreated object resize failed"); + Require(pread_nid_postfix(recreated, bytes, 4, 0) == 4 && std::memcmp(bytes, "\0\0\0\0", 4) == 0, + "recreated name still refers to the unlinked object"); + Require(firstMap[0] == 'n', "unlink or recreation changed the old mapping"); + Require(close_nid_postfix(recreated) == 0, "recreated descriptor close failed"); + Require(munmap_nid_postfix(firstMap, Page) == 0 && munmap_nid_postfix(secondMap, Page) == 0 && munmap_nid_postfix(privateMap, Page) == 0, + "guest unmap failed"); + std::puts("named shared-memory lifecycle checks passed"); +} +} + +int main() { + try { + Run(); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestNamedShmOpen.cpp b/core/libs/tests/GuestNamedShmOpen.cpp new file mode 100644 index 0000000000..dc6cebf447 --- /dev/null +++ b/core/libs/tests/GuestNamedShmOpen.cpp @@ -0,0 +1,126 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "nid/Sha1.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI shm_open_nid_postfix(const char*, int, int); +extern "C" int* APS5_VABI __error_nid_postfix(); + +static void Require(bool value, const char* message) { + if (!value) { + throw std::runtime_error(std::string(message) + " (guest errno=" + std::to_string(*__error_nid_postfix()) + ")"); + } +} + +struct Object { + std::string name; + std::string hostName = "/anyps5-shm-"; + explicit Object(std::string suffix) : name("/anyps5-test-" + std::to_string(getpid()) + suffix) { + const auto digest = Nid::Sha1(std::vector(name.begin(), name.end())); + for (const auto byte : digest) { + hostName += "0123456789abcdef"[byte >> 4]; + hostName += "0123456789abcdef"[byte & 15]; + } + ::shm_unlink(hostName.c_str()); + } + ~Object() { ::shm_unlink(hostName.c_str()); } + int Open(int flags, int mode = 0600) const { return shm_open_nid_postfix(name.c_str(), flags, mode); } +}; + +static void RequireError(const char* name, int flags, int error) { + *__error_nid_postfix() = 0; + Require(shm_open_nid_postfix(name, flags, 0600) == -1, "expected open failure"); + Require(*__error_nid_postfix() == error, "incorrect guest errno"); +} + +static void Run() { + constexpr int readWrite = 2, create = 0x200, exclusive = 0x800, truncate = 0x400; + Object object("/nested"); + RequireError(object.name.c_str(), readWrite, 2); + *__error_nid_postfix() = 73; + const int first = object.Open(readWrite | create | exclusive); + Require(first >= 0, "named creation failed"); + Require(*__error_nid_postfix() == 73, "successful creation overwrote guest errno"); + Require((fcntl(first, F_GETFD) & FD_CLOEXEC) != 0, "descriptor is not close-on-exec"); + struct stat info{}; + Require(fstat(first, &info) == 0 && info.st_size == 0, "new object is not empty"); + Require(ftruncate(first, 0x4000) == 0, "resize failed"); + auto* mapped = static_cast(::mmap(nullptr, 0x4000, PROT_READ | PROT_WRITE, MAP_SHARED, first, 0)); + Require(mapped != MAP_FAILED, "host shared mapping failed"); + std::memcpy(mapped, "shared", 7); + RequireError(object.name.c_str(), readWrite | create | exclusive, 17); + const int reader = object.Open(truncate); + Require(reader >= 0, "read-only reopen failed"); + Require(fstat(reader, &info) == 0 && info.st_size == 0x4000, "read-only O_TRUNC truncated the object"); + char bytes[7]{}; + Require(pread(reader, bytes, sizeof(bytes), 0) == sizeof(bytes) && std::memcmp(bytes, "shared", 7) == 0, "reopened object has different contents"); + Require(pwrite(reader, bytes, sizeof(bytes), 0) == -1, "read-only descriptor accepted a write"); + Require(::mmap(nullptr, 0x4000, PROT_READ | PROT_WRITE, MAP_SHARED, reader, 0) == MAP_FAILED, "read-only descriptor accepted a writable shared mapping"); + Object separate("/different"); + const int different = separate.Open(readWrite | create | exclusive, 0400); + Require(different >= 0 && fstat(different, &info) == 0 && info.st_size == 0, "different names share an object"); + Require(ftruncate(different, 0x4000) == 0 && pwrite(different, "y", 1, 0) == 1, "creation permissions incorrectly restrict the new descriptor"); + Require(mapped[0] == 's', "different object write changed the first object"); + Require(close(different) == 0, "different object close failed"); + const auto child = fork(); + Require(child >= 0, "fork failed"); + if (child == 0) { + const int reopened = object.Open(readWrite); + char contents[7]{}; + const bool okay = reopened >= 0 && pread(reopened, contents, sizeof(contents), 0) == sizeof(contents) && std::memcmp(contents, "shared", 7) == 0; + if (reopened >= 0) close(reopened); + _exit(okay ? 0 : 1); + } + int status = 0; + Require(waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0, "cross-process reopen failed"); + Require(::munmap(mapped, 0x4000) == 0, "host unmap failed"); + Require(close(reader) == 0 && close(first) == 0, "close failed"); + const int truncated = object.Open(readWrite | truncate); + Require(truncated >= 0 && fstat(truncated, &info) == 0 && info.st_size == 0, "read-write truncation failed"); + Require(close(truncated) == 0, "close after truncation failed"); + RequireError(nullptr, readWrite, 14); + RequireError("", readWrite, 22); + RequireError("relative", readWrite, 22); + RequireError(object.name.c_str(), 1, 22); + RequireError(object.name.c_str(), readWrite | 8, 22); + const std::string tooLong(1024, '/'); + RequireError(tooLong.c_str(), readWrite, 63); + const auto prefixLength = std::string("/anyps5-test-").size() + std::to_string(getpid()).size(); + Object longName(std::string(1023 - prefixLength, 'x')); + Require(longName.name.size() == 1023, "incorrect boundary test length"); + const auto previousMask = umask(0077); + const int longDescriptor = longName.Open(create | readWrite, 07777); + umask(previousMask); + Require(longDescriptor >= 0 && fstat(longDescriptor, &info) == 0, "long name creation failed"); + Require((info.st_mode & 07777) == 0755, "default guest umask was not applied independently of host umask"); + Require(close(longDescriptor) == 0, "long name close failed"); + const int longReader = longName.Open(0); + Require(longReader >= 0 && close(longReader) == 0, "maximum-length name reopen failed"); + const auto* anonymous = reinterpret_cast(1); + const int anonymousFirst = shm_open_nid_postfix(anonymous, readWrite, 0); + const int anonymousSecond = shm_open_nid_postfix(anonymous, readWrite | create | exclusive | truncate, 0400); + Require(anonymousFirst >= 0 && anonymousSecond >= 0, "anonymous creation regressed"); + Require(ftruncate(anonymousFirst, 0x4000) == 0 && pwrite(anonymousFirst, "x", 1, 0) == 1, "anonymous write failed"); + Require(pread(anonymousSecond, bytes, 1, 0) == 0, "anonymous objects share contents"); + Require(close(anonymousFirst) == 0 && close(anonymousSecond) == 0, "anonymous close failed"); + RequireError(anonymous, 0, 22); + RequireError(anonymous, 3, 22); + std::puts("named shared-memory checks passed"); +} + +int main() { + try { + Run(); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestNamedShmPermissions.cpp b/core/libs/tests/GuestNamedShmPermissions.cpp new file mode 100644 index 0000000000..c6f8d4c1bf --- /dev/null +++ b/core/libs/tests/GuestNamedShmPermissions.cpp @@ -0,0 +1,83 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "nid/Sha1.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI shm_open_nid_postfix(const char*, int, int); +extern "C" std::uint16_t APS5_VABI umask_nid_postfix(std::uint16_t); + +static void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +class Masks { +public: + Masks(std::uint16_t guest, mode_t host) : guest(umask_nid_postfix(guest)), host(::umask(host)) {} + ~Masks() { ::umask(host); umask_nid_postfix(guest); } + Masks(const Masks&) = delete; + Masks& operator=(const Masks&) = delete; +private: + std::uint16_t guest; + mode_t host; +}; + +class Object { +public: + explicit Object(const char* suffix) : name("/anyps5-permissions-" + std::to_string(getpid()) + suffix) { + const auto digest = Nid::Sha1(std::vector(name.begin(), name.end())); + for (auto byte : digest) { + hostName += "0123456789abcdef"[byte >> 4]; + hostName += "0123456789abcdef"[byte & 15]; + } + ::shm_unlink(hostName.c_str()); + } + ~Object() { if (descriptor >= 0) ::close(descriptor); ::shm_unlink(hostName.c_str()); } + Object(const Object&) = delete; + Object& operator=(const Object&) = delete; + std::string name; + int descriptor = -1; +private: + std::string hostName = "/anyps5-shm-"; +}; + +static void CheckMode(const char* suffix, std::uint16_t guestMask, mode_t hostMask, int requested, mode_t expected) { + Masks masks(guestMask, hostMask); + Object object(suffix); + object.descriptor = shm_open_nid_postfix(object.name.c_str(), 2 | 0x200 | 0x800, requested); + Require(object.descriptor >= 0, "shared-memory creation failed"); + struct stat info{}; + Require(::fstat(object.descriptor, &info) == 0, "stat failed"); + if ((info.st_mode & 07777) != expected) { + std::fprintf(stderr, "requested=%o guest-mask=%o host-mask=%o actual=%o expected=%o\n", requested, guestMask, hostMask, info.st_mode & 07777, expected); + throw std::runtime_error("shared-memory mode did not follow the guest umask"); + } + const auto observedHostMask = ::umask(hostMask); + Require(observedHostMask == hostMask, "guest creation changed host umask"); + if (expected == 0600) { + const int reopened = shm_open_nid_postfix(object.name.c_str(), 2 | 0x200, 0777); + Require(reopened >= 0, "existing-object reopen failed"); + const int result = ::fstat(reopened, &info); + ::close(reopened); + Require(result == 0 && (info.st_mode & 07777) == expected, "reopen changed existing permissions"); + } +} + +int main() { + try { + CheckMode("-guest-restricts", 0077, 0000, 0666, 0600); + CheckMode("-host-restricts", 0000, 0077, 0666, 0666); + CheckMode("-guest-denies", 0777, 0000, 0777, 0000); + CheckMode("-special-bits", 0022, 0077, 07777, 0755); + std::puts("guest shared-memory permissions passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestNanosleep.cpp b/core/libs/tests/GuestNanosleep.cpp index db2e4ee8b3..c97b6c1f13 100644 --- a/core/libs/tests/GuestNanosleep.cpp +++ b/core/libs/tests/GuestNanosleep.cpp @@ -85,6 +85,22 @@ static void SleepsForTheRequest(Nanosleep sleep) { Require(MonotonicNanos() - start >= SLEEP_NANOS - EARLY_WAKE_MARGIN_NANOS); } +static void CompletedSleepLeavesTheRemainingTimeAlone(Nanosleep sleep) { + const KernelTimespec request{0, NANOS_PER_MILLISECOND}; + KernelTimespec remaining{7, 9}; + Require(sleep(&request, &remaining) == SCE_OK); + Require(remaining.tv_sec == 7 && remaining.tv_nsec == 9); + KernelTimespec shared = request; + for (int i = 0; i < 3; ++i) { + Require(sleep(&shared, &shared) == SCE_OK); + Require(shared.tv_sec == request.tv_sec && shared.tv_nsec == request.tv_nsec); + } + const KernelTimespec invalid{0, NANOS_PER_SECOND}; + remaining = {7, 9}; + Require(sleep(&invalid, &remaining) != SCE_OK); + Require(remaining.tv_sec == 7 && remaining.tv_nsec == 9); +} + int main() { PosixRejectsInvalidRequests(nanosleep_nid_postfix); PosixRejectsInvalidRequests(_nanosleep_nid_postfix); @@ -97,4 +113,8 @@ int main() { SleepsForTheRequest(nanosleep_nid_postfix); SleepsForTheRequest(_nanosleep_nid_postfix); SleepsForTheRequest(sceKernelNanosleep); + + CompletedSleepLeavesTheRemainingTimeAlone(nanosleep_nid_postfix); + CompletedSleepLeavesTheRemainingTimeAlone(_nanosleep_nid_postfix); + CompletedSleepLeavesTheRemainingTimeAlone(sceKernelNanosleep); } diff --git a/core/libs/tests/GuestNetCtl.cpp b/core/libs/tests/GuestNetCtl.cpp new file mode 100644 index 0000000000..ea68f8eb02 --- /dev/null +++ b/core/libs/tests/GuestNetCtl.cpp @@ -0,0 +1,340 @@ +#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 + +extern "C" { +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(int, void*); +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 == 1); + + 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 == 1 && 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 + 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; + Require(sceNetCtlGetResult(0, &errorCode) == 0 && errorCode == 0); + 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, infoV6) == kErrInvalidCode); + Require(sceNetCtlGetInfoV6(6, infoV6) == kErrInvalidCode); + Require(sceNetCtlGetInfoV6(1, nullptr) == kErrInvalidAddr); + if (stateV6 != kStateIpObtained) Require(sceNetCtlGetInfoV6(1, 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 nat_info{}; + Require(sceNetCtlGetNatInfo(&nat_info) == kErrNotConnected); +} diff --git a/core/libs/tests/GuestNetCtlAp.cpp b/core/libs/tests/GuestNetCtlAp.cpp new file mode 100644 index 0000000000..fbb8b0f01d --- /dev/null +++ b/core/libs/tests/GuestNetCtlAp.cpp @@ -0,0 +1,134 @@ +#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(std::uint8_t* 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)); + std::uint8_t option[] = "opt"; + Require(sceNetCtlApGetConnectInfo(option, connect) == kErrNotStarted); + Require(sceNetCtlApGetConnectInfo(nullptr, connect) == kErrNotStarted); + 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); + } + + 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 new file mode 100644 index 0000000000..bdce6ca102 --- /dev/null +++ b/core/libs/tests/GuestNetCtlApDialog.cpp @@ -0,0 +1,81 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI sceNetCtlApDialogInitialize(void); +int APS5_VABI sceNetCtlApDialogTerminate(void); +int APS5_VABI sceNetCtlApDialogOpen(const void* param); +int APS5_VABI sceNetCtlApDialogClose(void); +int APS5_VABI sceNetCtlApDialogUpdateStatus(void); +int APS5_VABI sceNetCtlApDialogGetStatus(void); +int APS5_VABI sceNetCtlApDialogGetResult(void* result); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kStatusNone = 0; +constexpr int kStatusInitialized = 1; +constexpr int kStatusFinished = 3; +constexpr int kErrNotInitialized = static_cast(0x80B80003); +constexpr int kErrAlreadyInitialized = static_cast(0x80B80004); +constexpr int kErrNotFinished = static_cast(0x80B80005); +constexpr int kErrArgNull = static_cast(0x80B8000D); +constexpr int kResultUserCanceled = 1; + +void RequireCanceled(const std::uint8_t (&result)[44]) { + std::int32_t code = 0; + std::memcpy(&code, result, sizeof(code)); + Require(code == kResultUserCanceled); + for (int i = 4; i < 40; ++i) Require(result[i] == 0); + for (int i = 40; i < 44; ++i) Require(result[i] == 0x5a); +} + +} + +int main() { + std::uint8_t param[104] = {}; + std::uint8_t result[44]; + std::memset(result, 0x5a, sizeof(result)); + + Require(sceNetCtlApDialogTerminate() == kErrNotInitialized); + Require(sceNetCtlApDialogOpen(param) == kErrNotInitialized); + Require(sceNetCtlApDialogClose() == kErrNotInitialized); + Require(sceNetCtlApDialogGetResult(result) == kErrNotInitialized); + Require(sceNetCtlApDialogGetStatus() == kStatusNone); + Require(sceNetCtlApDialogUpdateStatus() == kStatusNone); + + Require(sceNetCtlApDialogInitialize() == 0); + Require(sceNetCtlApDialogInitialize() == kErrAlreadyInitialized); + Require(sceNetCtlApDialogGetStatus() == kStatusInitialized); + Require(sceNetCtlApDialogUpdateStatus() == kStatusInitialized); + + Require(sceNetCtlApDialogOpen(nullptr) == kErrArgNull); + Require(sceNetCtlApDialogGetStatus() == kStatusInitialized); + Require(sceNetCtlApDialogGetResult(result) == kErrNotFinished); + + Require(sceNetCtlApDialogOpen(param) == 0); + Require(sceNetCtlApDialogGetStatus() == kStatusFinished); + Require(sceNetCtlApDialogUpdateStatus() == kStatusFinished); + + Require(sceNetCtlApDialogGetResult(nullptr) == kErrArgNull); + Require(sceNetCtlApDialogGetResult(result) == 0); + RequireCanceled(result); + + Require(sceNetCtlApDialogOpen(param) == 0); + Require(sceNetCtlApDialogClose() == 0); + Require(sceNetCtlApDialogTerminate() == 0); + Require(sceNetCtlApDialogTerminate() == kErrNotInitialized); + Require(sceNetCtlApDialogGetStatus() == kStatusNone); + + Require(sceNetCtlApDialogInitialize() == 0); + Require(sceNetCtlApDialogClose() == 0); + Require(sceNetCtlApDialogGetStatus() == kStatusFinished); + std::memset(result, 0x5a, sizeof(result)); + Require(sceNetCtlApDialogGetResult(result) == 0); + RequireCanceled(result); + Require(sceNetCtlApDialogTerminate() == 0); +} diff --git a/core/libs/tests/GuestNewHandler.cpp b/core/libs/tests/GuestNewHandler.cpp new file mode 100644 index 0000000000..04a5d9a8a1 --- /dev/null +++ b/core/libs/tests/GuestNewHandler.cpp @@ -0,0 +1,201 @@ +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +using Handler = void (APS5_VABI*)(); +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 { + +alignas(16) std::array storage{}; +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) { + std::fprintf(stderr, "Guest new handler check failed at line %d\n", line); + std::abort(); + } +} + +#define REQUIRE(condition) Require((condition), __LINE__) + +void* APS5_VABI allocate(std::size_t bytes) { + ++allocations; + lastSize = bytes; + if (failuresLeft != 0) { + --failuresLeft; + return nullptr; + } + return storage.data(); +} + +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 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); + return *pointer == nullptr ? 12 : 0; +} + +void APS5_VABI countingHandler() { ++handlerCalls; } +void APS5_VABI uninstallingHandler() { + if (++handlerCalls == 3) _ZSt15set_new_handlerPFvvE_nid_postfix(nullptr); +} +void APS5_VABI throwingHandler() { + ++handlerCalls; + throw std::bad_alloc(); +} +void APS5_VABI allocatingHandler() { + ++handlerCalls; + failuresLeft = 0; + REQUIRE(_Znwm_nid_postfix(8) == storage.data()); +} + +void Reset(unsigned failures) { + failuresLeft = failures; + allocations = 0; + handlerCalls = 0; +} + +template +bool Throws(TAction action) { + try { + action(); + } catch (const std::bad_alloc&) { + return true; + } + return false; +} + +} + +int RunDefaultHeap() { + std::array none{}; + ApplicationHeapRegister_nid_no_patch(none.data()); + constexpr std::size_t huge = std::numeric_limits::max(); + + REQUIRE(Throws([] { _Znwm_nid_postfix(huge); })); + REQUIRE(Throws([] { _Znam_nid_postfix(huge); })); + REQUIRE(_ZnwmRKSt9nothrow_t_nid_postfix(huge, nullptr) == nullptr); + REQUIRE(_ZnamRKSt9nothrow_t_nid_postfix(huge, nullptr) == nullptr); + + handlerCalls = 0; + REQUIRE(_ZSt15set_new_handlerPFvvE_nid_postfix(&uninstallingHandler) == nullptr); + REQUIRE(Throws([] { _Znwm_nid_postfix(huge); })); + REQUIRE(handlerCalls == 3 && _ZSt15get_new_handlerv_nid_postfix() == nullptr); + + handlerCalls = 0; + REQUIRE(_ZSt15set_new_handlerPFvvE_nid_postfix(&throwingHandler) == nullptr); + REQUIRE(_ZnwmRKSt9nothrow_t_nid_postfix(huge, nullptr) == nullptr && handlerCalls == 1); + REQUIRE(_ZSt15set_new_handlerPFvvE_nid_postfix(nullptr) == &throwingHandler); + + void* small = _Znwm_nid_postfix(24); + REQUIRE(small != nullptr); + std::memset(small, 0x5a, 24); + _ZdlPv_nid_postfix(small); + + std::puts("Guest new handler default heap checks passed"); + return 0; +} + +int main(int argc, char** argv) { + if (argc > 1 && std::strcmp(argv[1], "default") == 0) return RunDefaultHeap(); + std::array api{}; + api[0] = reinterpret_cast(&allocate); + api[1] = reinterpret_cast(&release); + api[2] = reinterpret_cast(&allocateZeroed); + api[3] = reinterpret_cast(&reallocate); + api[4] = reinterpret_cast(&align); + api[5] = reinterpret_cast(&realign); + api[6] = reinterpret_cast(&posixAlign); + ApplicationHeapRegister_nid_no_patch(api.data()); + + REQUIRE(_ZSt15get_new_handlerv_nid_postfix() == nullptr); + + Reset(1); + REQUIRE(Throws([] { _Znwm_nid_postfix(8); }) && handlerCalls == 0 && allocations == 1); + Reset(1); + REQUIRE(Throws([] { _Znam_nid_postfix(8); }) && handlerCalls == 0 && allocations == 1); + Reset(1); + REQUIRE(_ZnwmRKSt9nothrow_t_nid_postfix(8, nullptr) == nullptr && handlerCalls == 0); + Reset(1); + REQUIRE(_ZnamRKSt9nothrow_t_nid_postfix(8, nullptr) == nullptr && handlerCalls == 0); + + REQUIRE(_ZSt15set_new_handlerPFvvE_nid_postfix(&countingHandler) == nullptr); + REQUIRE(_ZSt15get_new_handlerv_nid_postfix() == &countingHandler); + Reset(2); + REQUIRE(_Znwm_nid_postfix(8) == storage.data() && handlerCalls == 2 && allocations == 3); + Reset(3); + REQUIRE(_Znam_nid_postfix(8) == storage.data() && handlerCalls == 3 && allocations == 4); + Reset(1); + REQUIRE(_ZnwmRKSt9nothrow_t_nid_postfix(8, nullptr) == storage.data() && handlerCalls == 1 && allocations == 2); + Reset(1); + REQUIRE(_ZnamRKSt9nothrow_t_nid_postfix(8, nullptr) == storage.data() && handlerCalls == 1 && allocations == 2); + Reset(0); + REQUIRE(_Znwm_nid_postfix(8) == storage.data() && handlerCalls == 0 && allocations == 1); + Reset(1); + REQUIRE(_Znwm_nid_postfix(0) == storage.data() && lastSize == 1 && handlerCalls == 1); + + REQUIRE(_ZSt15set_new_handlerPFvvE_nid_postfix(&uninstallingHandler) == &countingHandler); + Reset(100); + REQUIRE(Throws([] { _Znwm_nid_postfix(8); })); + REQUIRE(handlerCalls == 3 && allocations == 4 && _ZSt15get_new_handlerv_nid_postfix() == nullptr); + + REQUIRE(_ZSt15set_new_handlerPFvvE_nid_postfix(&throwingHandler) == nullptr); + Reset(1); + REQUIRE(Throws([] { _Znwm_nid_postfix(8); }) && handlerCalls == 1 && allocations == 1); + Reset(1); + REQUIRE(_ZnwmRKSt9nothrow_t_nid_postfix(8, nullptr) == nullptr && handlerCalls == 1 && allocations == 1); + Reset(1); + REQUIRE(_ZnamRKSt9nothrow_t_nid_postfix(8, nullptr) == nullptr && handlerCalls == 1 && allocations == 1); + + REQUIRE(_ZSt15set_new_handlerPFvvE_nid_postfix(&allocatingHandler) == &throwingHandler); + Reset(1); + REQUIRE(_Znwm_nid_postfix(16) == storage.data() && handlerCalls == 1); + + 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 new file mode 100644 index 0000000000..e9352d3f8c --- /dev/null +++ b/core/libs/tests/GuestNpAuth.cpp @@ -0,0 +1,130 @@ +#include "SceTypes.hpp" + +#include +#include + +extern "C" { +int APS5_VABI sceNpAuthAbortRequest(int req_id); +int APS5_VABI sceNpAuthCreateAsyncRequest(const void* param); +int APS5_VABI sceNpAuthCreateRequest(void); +int APS5_VABI sceNpAuthDeleteRequest(int req_id); +int APS5_VABI sceNpAuthGetAuthorizationCode(int req_id, const void* param, void* auth_code); +int APS5_VABI sceNpAuthGetAuthorizationCodeV3(int req_id, const void* param, void* auth_code, int* issuer_id); +int APS5_VABI sceNpAuthGetAuthorizedAppCode(); +int APS5_VABI sceNpAuthGetIdToken(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); +int APS5_VABI sceNpAuthWaitAsync(int req_id, int* result); +} + +static void Require(bool condition) { + if (!condition) { + std::abort(); + } +} + +int main() { + constexpr int invalidArgument = static_cast(0x80550301); + constexpr int signedOut = static_cast(0x80550006); + constexpr int aborted = static_cast(0x80550304); + constexpr int requestNotFound = static_cast(0x80550306); + constexpr int invalidId = static_cast(0x80550307); + constexpr int pollRunning = 1; + + const int syncReq = sceNpAuthCreateRequest(); + const int syncReq2 = sceNpAuthCreateRequest(); + Require(syncReq > 0); + Require(syncReq2 > syncReq); + + const int asyncReq = sceNpAuthCreateAsyncRequest(nullptr); + const int asyncReq2 = sceNpAuthCreateAsyncRequest(nullptr); + Require(asyncReq > syncReq2); + Require(asyncReq2 > asyncReq); + + alignas(8) char param[16] = {}; + alignas(8) char token[16] = {}; + alignas(8) char authCode[16] = {}; + int issuerId = 0; + + Require(sceNpAuthSetTimeout(syncReq, 2, 5000000, 10000000, 10000000, 10000000) == 0); + Require(sceNpAuthSetTimeout(asyncReq, 0, 0, 0, 0, 0) == 0); + + int pollUnprocessed = 0; + Require(sceNpAuthPollAsync(asyncReq, &pollUnprocessed) == invalidId); + int waitUnprocessed = 0; + Require(sceNpAuthWaitAsync(asyncReq, &waitUnprocessed) == invalidId); + + Require(sceNpAuthGetAuthorizedAppCode() == signedOut); + + 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(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(sceNpAuthGetAuthorizationCode(syncReq2, param, authCode) == signedOut); + Require(sceNpAuthGetAuthorizationCode(syncReq2, param, authCode) == invalidArgument); + Require(sceNpAuthGetAuthorizationCodeV3(syncReq2, param, authCode, &issuerId) == invalidArgument); + + Require(sceNpAuthGetIdTokenV3(asyncReq, param, token) == 0); + Require(sceNpAuthGetIdTokenV3(asyncReq, param, token) == invalidArgument); + int asyncPoll = 0; + Require(sceNpAuthPollAsync(asyncReq, &asyncPoll) == pollRunning); + Require(asyncPoll == 0); + Require(sceNpAuthPollAsync(asyncReq, &asyncPoll) == 0); + Require(asyncPoll == signedOut); + Require(sceNpAuthPollAsync(asyncReq, &asyncPoll) == 0); + Require(asyncPoll == signedOut); + int asyncWait = 0; + Require(sceNpAuthWaitAsync(asyncReq, &asyncWait) == 0); + Require(asyncWait == signedOut); + + Require(sceNpAuthGetAuthorizationCodeV3(asyncReq2, param, authCode, &issuerId) == 0); + Require(sceNpAuthGetAuthorizationCodeV3(asyncReq2, param, authCode, &issuerId) == invalidArgument); + int asyncWait2 = 0; + Require(sceNpAuthWaitAsync(asyncReq2, &asyncWait2) == 0); + Require(asyncWait2 == signedOut); + + constexpr int unknownReq = 999999; + Require(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); + int waitResult = 0; + Require(sceNpAuthWaitAsync(unknownReq, &waitResult) == requestNotFound); + Require(sceNpAuthSetTimeout(unknownReq, 2, 5000000, 10000000, 10000000, 10000000) == requestNotFound); + + Require(sceNpAuthWaitAsync(syncReq, nullptr) == invalidArgument); + Require(sceNpAuthWaitAsync(syncReq, &waitResult) == invalidId); + Require(sceNpAuthPollAsync(syncReq, &pollResult) == invalidId); + + Require(sceNpAuthAbortRequest(asyncReq) == 0); + Require(sceNpAuthGetIdTokenV3(asyncReq, param, token) == invalidArgument); + Require(sceNpAuthAbortRequest(unknownReq) == requestNotFound); + + const int abortedReq = sceNpAuthCreateAsyncRequest(nullptr); + Require(sceNpAuthAbortRequest(abortedReq) == 0); + Require(sceNpAuthGetIdTokenV3(abortedReq, param, token) == aborted); + Require(sceNpAuthGetAuthorizationCodeV3(abortedReq, param, authCode, &issuerId) == aborted); + int abortedWait = 0; + Require(sceNpAuthWaitAsync(abortedReq, &abortedWait) == 0); + Require(abortedWait == aborted); + + Require(sceNpAuthDeleteRequest(syncReq) == 0); + Require(sceNpAuthGetIdTokenV3(syncReq, param, token) == requestNotFound); + Require(sceNpAuthWaitAsync(syncReq, &waitResult) == requestNotFound); + Require(sceNpAuthSetTimeout(syncReq, 2, 5000000, 10000000, 10000000, 10000000) == requestNotFound); + + return 0; +} diff --git a/core/libs/tests/GuestNpAuthAuthorizedAppCode.cpp b/core/libs/tests/GuestNpAuthAuthorizedAppCode.cpp new file mode 100644 index 0000000000..f6dfce0de8 --- /dev/null +++ b/core/libs/tests/GuestNpAuthAuthorizedAppCode.cpp @@ -0,0 +1,13 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include + +extern "C" { +int APS5_VABI sceNpAuthGetAuthorizedAppCode(); +} + +int main() { + constexpr int kSignedOut = static_cast(0x80550006); + if (sceNpAuthGetAuthorizedAppCode() != kSignedOut) std::abort(); + if (sceNpAuthGetAuthorizedAppCode() != kSignedOut) std::abort(); + return 0; +} diff --git a/core/libs/tests/GuestNpAuthAuthorizedAppDialog.cpp b/core/libs/tests/GuestNpAuthAuthorizedAppDialog.cpp new file mode 100644 index 0000000000..7eecd54c43 --- /dev/null +++ b/core/libs/tests/GuestNpAuthAuthorizedAppDialog.cpp @@ -0,0 +1,71 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI sceNpAuthAuthorizedAppDialogInitialize(void); +int APS5_VABI sceNpAuthAuthorizedAppDialogOpen(const void* param); +int APS5_VABI sceNpAuthAuthorizedAppDialogUpdateStatus(void); +int APS5_VABI sceNpAuthAuthorizedAppDialogGetStatus(void); +int APS5_VABI sceNpAuthAuthorizedAppDialogGetResult(void* result); +int APS5_VABI sceNpAuthAuthorizedAppDialogClose(void); +int APS5_VABI sceNpAuthAuthorizedAppDialogTerminate(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(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusNone); + Require(sceNpAuthAuthorizedAppDialogTerminate() == kErrNotInitialized); + Require(sceNpAuthAuthorizedAppDialogClose() == kErrNotInitialized); + Require(sceNpAuthAuthorizedAppDialogOpen(¶m) == kErrNotInitialized); + Require(sceNpAuthAuthorizedAppDialogGetResult(result) == kErrNotInitialized); + + Require(sceNpAuthAuthorizedAppDialogInitialize() == 0); + Require(sceNpAuthAuthorizedAppDialogInitialize() == kErrAlreadyInitialized); + Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusInitialized); + Require(sceNpAuthAuthorizedAppDialogUpdateStatus() == kStatusInitialized); + + Require(sceNpAuthAuthorizedAppDialogOpen(nullptr) == kErrArgNull); + Require(sceNpAuthAuthorizedAppDialogGetResult(result) == kErrNotFinished); + + Require(sceNpAuthAuthorizedAppDialogOpen(¶m) == 0); + Require(sceNpAuthAuthorizedAppDialogUpdateStatus() == kStatusFinished); + Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusFinished); + + Require(sceNpAuthAuthorizedAppDialogGetResult(nullptr) == kErrArgNull); + Require(sceNpAuthAuthorizedAppDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(result[1] == -1); + + Require(sceNpAuthAuthorizedAppDialogClose() == 0); + Require(sceNpAuthAuthorizedAppDialogTerminate() == 0); + Require(sceNpAuthAuthorizedAppDialogTerminate() == kErrNotInitialized); + Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusNone); + + Require(sceNpAuthAuthorizedAppDialogInitialize() == 0); + Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusInitialized); + Require(sceNpAuthAuthorizedAppDialogClose() == 0); + Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusFinished); + result[0] = -1; + Require(sceNpAuthAuthorizedAppDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(sceNpAuthAuthorizedAppDialogTerminate() == 0); + return 0; +} diff --git a/core/libs/tests/GuestNpCommerceDialog.cpp b/core/libs/tests/GuestNpCommerceDialog.cpp new file mode 100644 index 0000000000..7b7239959f --- /dev/null +++ b/core/libs/tests/GuestNpCommerceDialog.cpp @@ -0,0 +1,64 @@ +#include "SceTypes.hpp" + +#include +#include +#include + +extern "C" { +int APS5_VABI sceNpCommerceDialogInitialize(); +int APS5_VABI sceNpCommerceDialogOpen(const void* param); +int APS5_VABI sceNpCommerceDialogOpen2(void); +int APS5_VABI sceNpCommerceDialogUpdateStatus(void); +int APS5_VABI sceNpCommerceDialogGetStatus(void); +int APS5_VABI sceNpCommerceDialogGetResult(void* result); +int APS5_VABI sceNpCommerceDialogClose(void); +int APS5_VABI sceNpCommerceDialogTerminate(); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int statusNone = 0; +constexpr int statusInitialized = 1; +constexpr int statusFinished = 3; +constexpr std::int32_t resultUserCanceled = 1; + +void RequireCanceledResult() { + std::uint8_t result[0x20]; + std::memset(result, 0x33, sizeof(result)); + Require(sceNpCommerceDialogGetResult(result) == 0); + std::int32_t value = 0; + std::memcpy(&value, result, sizeof(value)); + Require(value == resultUserCanceled); + Require(result[4] == 0); + Require(result[5] == 0x33); +} + +} + +int main() { + Require(sceNpCommerceDialogGetStatus() == statusNone); + Require(sceNpCommerceDialogInitialize() == 0); + Require(sceNpCommerceDialogGetStatus() == statusInitialized); + + Require(sceNpCommerceDialogOpen2() == 0); + Require(sceNpCommerceDialogUpdateStatus() == statusFinished); + Require(sceNpCommerceDialogGetStatus() == statusFinished); + RequireCanceledResult(); + + Require(sceNpCommerceDialogClose() == 0); + Require(sceNpCommerceDialogGetStatus() == statusFinished); + Require(sceNpCommerceDialogTerminate() == 0); + Require(sceNpCommerceDialogGetStatus() == statusNone); + + alignas(8) std::uint8_t param[0x40] = {}; + Require(sceNpCommerceDialogInitialize() == 0); + Require(sceNpCommerceDialogOpen(param) == 0); + Require(sceNpCommerceDialogOpen2() == 0); + Require(sceNpCommerceDialogUpdateStatus() == statusFinished); + RequireCanceledResult(); + Require(sceNpCommerceDialogTerminate() == 0); + Require(sceNpCommerceDialogGetStatus() == statusNone); + return 0; +} diff --git a/core/libs/tests/GuestNpCppWebApiIntrusivePtr.cpp b/core/libs/tests/GuestNpCppWebApiIntrusivePtr.cpp new file mode 100644 index 0000000000..99f3f83152 --- /dev/null +++ b/core/libs/tests/GuestNpCppWebApiIntrusivePtr.cpp @@ -0,0 +1,321 @@ +#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*); +} + +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, + }; + 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, + }; + for (const auto access : accessors) TestAccess(access); + + const Copy copies[] = { + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEC1ERS7_, + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEC1ERS7_, + }; + 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}, + }; + for (const auto& testCase : destructors) TestDestroy(testCase); + + TestSharedOwnership(); + TestVectorPushBackReleasesElements(); + TestAdoptedVectorBodyDestroyedThroughDeleter(); + 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/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 106a1e05b8..ca4daa5da4 100644 --- a/core/libs/tests/GuestNpManager.cpp +++ b/core/libs/tests/GuestNpManager.cpp @@ -4,15 +4,44 @@ #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, void* buffer); +int APS5_VABI sceNpUnknown_WxLcN76q19o(void); +} + +extern "C" { +int APS5_VABI sceNpCreateRequest(void); +int APS5_VABI sceNpCreateAsyncRequest(const NpCreateAsyncRequestParameter* param); +int APS5_VABI sceNpDeleteRequest(int reqId); +int APS5_VABI sceNpSetTimeout(int reqId, std::int32_t resolveRetry, std::uint32_t resolveTimeout, + std::uint32_t connTimeout, std::uint32_t sendTimeout, std::uint32_t recvTimeout); +} + +extern "C" { +int APS5_VABI sceNpCheckCallback(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); } namespace { constexpr int InvalidArgument = static_cast(0x80550003u); constexpr int SignedOut = static_cast(0x80550006u); +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) { if (!condition) { @@ -21,9 +50,171 @@ 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(sceNpCheckCallback() == 0, "sceNpCheckCallback 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"); + } + 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"); + const int requestId = sceNpCreateRequest(); + Require(requestId > 0, "sceNpCreateRequest must return a positive request id"); + Require(sceNpSetTimeout(requestId, 2, 1000000, 10000000, 10000000, 10000000) == 0, "minimum timeouts"); + Require(sceNpSetTimeout(requestId, 0, 0, 0, 0, 30000000) == 0, "only the receive timeout"); + Require(sceNpSetTimeout(requestId, 3, 0, 0, 0, 0) == 0, "only the resolve retries"); + Require(sceNpSetTimeout(requestId, 0, 0, 0, 0, 0) == InvalidArgument, "every value left at its default"); + Require(sceNpSetTimeout(0, 2, 0, 0, 0, 0) == InvalidArgument, "request id 0"); + Require(sceNpSetTimeout(-1, 2, 0, 0, 0, 0) == InvalidArgument, "negative request id"); + Require(sceNpSetTimeout(requestId, -1, 0, 0, 0, 30000000) == InvalidArgument, "negative resolve retries"); + Require(sceNpSetTimeout(requestId, 0, 999999, 0, 0, 0) == InvalidArgument, "resolve timeout below 1 s"); + Require(sceNpSetTimeout(requestId, 0, 0, 9999999, 0, 0) == InvalidArgument, "connect timeout below 10 s"); + Require(sceNpSetTimeout(requestId, 0, 0, 0, 9999999, 0) == InvalidArgument, "send timeout below 10 s"); + Require(sceNpSetTimeout(requestId, 0, 0, 0, 0, 1) == InvalidArgument, "receive timeout below 10 s"); + Require(sceNpDeleteRequest(requestId) == 0, "sceNpDeleteRequest failed"); + Require(sceNpSetTimeout(requestId, 2, 0, 0, 0, 0) == RequestNotFound, "deleted request"); + NpCreateAsyncRequestParameter param{}; + const int asyncId = sceNpCreateAsyncRequest(¶m); + Require(asyncId > 0, "sceNpCreateAsyncRequest must return a positive request id"); + Require(sceNpSetTimeout(asyncId, 2, 0, 0, 0, 0) == 0, "async request"); + Require(sceNpDeleteRequest(asyncId) == 0, "sceNpDeleteRequest failed on the async request"); + Require(sceNpSetTimeout(asyncId, 2, 0, 0, 0, 0) == RequestNotFound, "deleted async request"); +} + } int main() { + CheckStateCallbacksA(); + CheckLegacyStateCallback(); + CheckRequestTimeouts(); NpId npId{}; std::memset(&npId, 0x5a, sizeof(npId)); NpId untouched{}; @@ -31,5 +222,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"); + unsigned char buffer[0x11]{}; + try { + sceNpUnknown_8weiQyIdI3A(0x10000, buffer); + Require(false, "sceNpUnknown_8weiQyIdI3A must report that it is not implemented"); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()) == "sceNpUnknown_8weiQyIdI3A not implemented", + "sceNpUnknown_8weiQyIdI3A threw"); + } + try { + sceNpUnknown_WxLcN76q19o(); + Require(false, "sceNpUnknown_WxLcN76q19o must report that it is not implemented"); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()) == "sceNpUnknown_WxLcN76q19o not implemented", + "sceNpUnknown_WxLcN76q19o threw"); + } return 0; } diff --git a/core/libs/tests/GuestNpPartner.cpp b/core/libs/tests/GuestNpPartner.cpp new file mode 100644 index 0000000000..700570470a --- /dev/null +++ b/core/libs/tests/GuestNpPartner.cpp @@ -0,0 +1,44 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI sceNpEAAccessInitialize(void); +int APS5_VABI sceNpEAAccessTerminate(void); +int APS5_VABI sceNpHasEAAccessSubscription(int, bool*); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +template +bool Throws(TAction action) { + try { + action(); + } catch (const std::logic_error&) { + return true; + } + return false; +} + +} + +int main() { + 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..d47398d76a --- /dev/null +++ b/core/libs/tests/GuestNpParty.cpp @@ -0,0 +1,26 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +extern "C" int APS5_VABI sceNpPartyGetState(std::uint16_t* state); + +int main() { + std::uint16_t states[3]; + std::memset(states, 0xa5, sizeof(states)); + if (sceNpPartyGetState(nullptr) != static_cast(0x80552504u)) { + std::fprintf(stderr, "a null state was not refused with INVALID_ARGUMENT\n"); + return 1; + } + if (sceNpPartyGetState(&states[1]) != 0 || states[1] != 2) { + std::fprintf(stderr, "the state is not NOT_IN_PARTY\n"); + return 1; + } + if (states[0] != 0xa5a5 || states[2] != 0xa5a5) { + std::fprintf(stderr, "sceNpPartyGetState wrote past its 16-bit state\n"); + return 1; + } + std::puts("NpParty state tests passed"); + return 0; +} diff --git a/core/libs/tests/GuestNpSessionSignaling.cpp b/core/libs/tests/GuestNpSessionSignaling.cpp index 834f15fb46..27bc077144 100644 --- a/core/libs/tests/GuestNpSessionSignaling.cpp +++ b/core/libs/tests/GuestNpSessionSignaling.cpp @@ -9,6 +9,7 @@ int APS5_VABI sceNpSessionSignalingInitialize(void* param); int APS5_VABI sceNpSessionSignalingCreateContext2(const void* param, std::uint32_t* contextId); std::int32_t APS5_VABI sceNpSessionSignalingGetLocalNetInfo(std::int32_t contextId, void* info); int APS5_VABI sceNpSessionSignalingRequestPrepare(std::uint32_t contextId, std::uint32_t* requestId); +std::int32_t APS5_VABI sceNpSessionSignalingGetConnectionStatus(std::int32_t contextId, std::int32_t connectionId, std::int32_t* status, void* peerAddress, std::uint16_t* peerPort); int APS5_VABI sceNpSessionSignalingTerminate(void); } @@ -16,6 +17,7 @@ namespace { constexpr int InvalidArgument = static_cast(0x80553303u); constexpr int Unavailable = static_cast(0x80552D06u); +constexpr int ConnectionNotFound = static_cast(0x8055330Cu); void Require(bool condition, const char* message) { if (!condition) { @@ -47,5 +49,13 @@ int main() { Require(sceNpSessionSignalingGetLocalNetInfo(netContextId, info) == Unavailable, "local net info without network"); Require(std::memcmp(info, untouched, sizeof(info)) == 0, "failed query modified the info"); + std::int32_t status = 0x5a5a5a5a; + std::uint32_t peerAddress = 0xa5a5a5a5u; + std::uint16_t peerPort = 0xa5a5; + Require(sceNpSessionSignalingGetConnectionStatus(netContextId, 1, nullptr, &peerAddress, &peerPort) == InvalidArgument, "null connection status"); + Require(sceNpSessionSignalingGetConnectionStatus(netContextId, 1, &status, &peerAddress, &peerPort) == ConnectionNotFound, "connection without network"); + Require(sceNpSessionSignalingGetConnectionStatus(netContextId, 1, &status, nullptr, nullptr) == ConnectionNotFound, "connection without peer outputs"); + Require(status == 0x5a5a5a5a && peerAddress == 0xa5a5a5a5u && peerPort == 0xa5a5, "failed status query wrote an output"); + Require(sceNpSessionSignalingTerminate() == 0, "terminate failed"); } diff --git a/core/libs/tests/GuestNpTrophy2.cpp b/core/libs/tests/GuestNpTrophy2.cpp index d52cc50ba2..165d4347f1 100644 --- a/core/libs/tests/GuestNpTrophy2.cpp +++ b/core/libs/tests/GuestNpTrophy2.cpp @@ -7,12 +7,24 @@ extern "C" { int APS5_VABI sceNpTrophy2RegisterUnlockCallback(void*, void*); int APS5_VABI sceNpTrophy2UnregisterUnlockCallback(); int APS5_VABI sceNpTrophy2GetGameInfo(int, int, NpTrophy2GameDetails*, NpTrophy2GameData*); +int APS5_VABI sceNpTrophy2GetGroupInfo(int, int, int, NpTrophy2GroupDetails*, NpTrophy2GroupData*); +int APS5_VABI sceNpTrophy2GetTrophyInfo(int, int, int, NpTrophy2Details*, NpTrophy2Data*); +int APS5_VABI sceNpTrophy2ShowTrophyList(int); } static void Require(bool value) { if (!value) std::abort(); } static void APS5_VABI OnUnlock(int, int, void*) {} +template static bool ThrowsInvalidArgument(Call call) { + try { + call(); + } catch (const std::invalid_argument&) { + return true; + } + return false; +} + int main() { int userdata = 0; Require(sceNpTrophy2UnregisterUnlockCallback() == 0); @@ -21,16 +33,28 @@ int main() { Require(sceNpTrophy2RegisterUnlockCallback(reinterpret_cast(&OnUnlock), &userdata) == 0); Require(sceNpTrophy2UnregisterUnlockCallback() == 0); + Require(sceNpTrophy2ShowTrophyList(1) == 0); + NpTrophy2GameDetails details{}; NpTrophy2GameData data{}; Require(sceNpTrophy2GetGameInfo(1, 1, &details, nullptr) == 0); Require(details.num_trophies != 0); Require(sceNpTrophy2GetGameInfo(1, 1, nullptr, &data) == 0); - bool threw = false; - try { - sceNpTrophy2GetGameInfo(1, 1, nullptr, nullptr); - } catch (const std::invalid_argument&) { - threw = true; - } - Require(threw); + Require(ThrowsInvalidArgument([] { sceNpTrophy2GetGameInfo(1, 1, nullptr, nullptr); })); + + NpTrophy2GroupDetails groupDetails{}; + NpTrophy2GroupData groupData{}; + Require(sceNpTrophy2GetGroupInfo(1, 1, 2, &groupDetails, nullptr) == 0); + Require(groupDetails.group_id == 2); + Require(sceNpTrophy2GetGroupInfo(1, 1, 2, nullptr, &groupData) == 0); + Require(groupData.group_id == 2); + Require(ThrowsInvalidArgument([] { sceNpTrophy2GetGroupInfo(1, 1, 2, nullptr, nullptr); })); + + NpTrophy2Details trophyDetails{}; + NpTrophy2Data trophyData{}; + Require(sceNpTrophy2GetTrophyInfo(1, 1, 3, &trophyDetails, nullptr) == 0); + Require(trophyDetails.trophy_id == 3); + Require(sceNpTrophy2GetTrophyInfo(1, 1, 3, nullptr, &trophyData) == 0); + Require(trophyData.trophy_id == 3); + Require(ThrowsInvalidArgument([] { sceNpTrophy2GetTrophyInfo(1, 1, 3, nullptr, nullptr); })); } diff --git a/core/libs/tests/GuestNpTrophyData.cpp b/core/libs/tests/GuestNpTrophyData.cpp new file mode 100644 index 0000000000..5c4eaf025a --- /dev/null +++ b/core/libs/tests/GuestNpTrophyData.cpp @@ -0,0 +1,248 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceNpTrophy2/include/NpTrophy2Types.hpp" +#include "prx/libSceNpTrophy2/include/TrophyStore.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct NpUniversalDataSystemEvent; +struct NpUniversalDataSystemEventPropertyObject; + +extern "C" { +int APS5_VABI sceNpUniversalDataSystemCreateContext(int*, int, uint32_t, uint64_t); +int APS5_VABI sceNpUniversalDataSystemRegisterContext(int, int, uint64_t); +int APS5_VABI sceNpUniversalDataSystemCreateHandle(int*); +int APS5_VABI sceNpUniversalDataSystemCreateEvent(const char*, const NpUniversalDataSystemEventPropertyObject*, NpUniversalDataSystemEvent**, NpUniversalDataSystemEventPropertyObject**); +int APS5_VABI sceNpUniversalDataSystemDestroyEvent(NpUniversalDataSystemEvent*); +int APS5_VABI sceNpUniversalDataSystemPostEvent(int, int, const void*, uint64_t); +int APS5_VABI sceNpUniversalDataSystemCreateEventPropertyObject(NpUniversalDataSystemEventPropertyObject**); +int APS5_VABI sceNpUniversalDataSystemDestroyEventPropertyObject(NpUniversalDataSystemEventPropertyObject*); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetInt32(NpUniversalDataSystemEventPropertyObject*, const char*, int32_t); +int APS5_VABI sceNpTrophy2CreateContext(int*, int, uint32_t, uint64_t); +int APS5_VABI sceNpTrophy2GetGameInfo(int, int, NpTrophy2GameDetails*, NpTrophy2GameData*); +int APS5_VABI sceNpTrophy2GetGroupInfo(int, int, int, NpTrophy2GroupDetails*, NpTrophy2GroupData*); +int APS5_VABI sceNpTrophy2GetGroupInfoArray(int, int, uint32_t, uint32_t, NpTrophy2GroupDetails*, NpTrophy2GroupData*, uint32_t*); +int APS5_VABI sceNpTrophy2GetTrophyInfo(int, int, int, NpTrophy2Details*, NpTrophy2Data*); +int APS5_VABI sceNpTrophy2GetTrophyInfoArray(int, int, uint32_t, uint32_t, NpTrophy2Details*, NpTrophy2Data*, uint32_t*); +} + +static constexpr int InitialUser = 0x10000000; + +static const char TropConf[] = R"({"defaultLanguage":"en-US","trophies":[ + {"id":"0","grade":"P"}, + {"id":"1","grade":"G","platinumTrophyId":"0","unlockCondition":{"udsStatId":"10","comparator":"ge","targetValue":"10","progressive":false}}, + {"id":"2","grade":"S","platinumTrophyId":"0","unlockCondition":{"udsStatId":"20","comparator":"gt","targetValue":"100"}}, + {"id":"3","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"30","comparator":"le","targetValue":"5"}}, + {"id":"4","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"30","comparator":"lt","targetValue":"3"}}, + {"id":"5","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"40","comparator":"ge","targetValue":"5","progressive":true}}, + {"id":"6","grade":"B","groupId":"1","hidden":true,"hasReward":true}, + {"id":"7","grade":"B","groupId":"1"} +]})"; + +static const char TropMeta[] = R"({"metadata":{"titleMetadata":{"name":"Synthetic Game"}, + "trophyMetadata":[{"id":"0","name":"All of them"},{"id":"1","name":"Ten","detail":"Reach ten"},{"id":"5","name":"Five steps"}, + {"id":"6","name":"Secret","detail":"A hidden one","reward":"A hat"}], + "groupMetadata":[{"id":"1","name":"Expansion"}]}})"; + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +template +static bool ThrowsInvalidArgument(TCall call) { + try { + call(); + } catch (const std::invalid_argument&) { + return true; + } + return false; +} + +static void PutBigEndian(std::string& data, std::size_t offset, std::uint64_t value, std::size_t size) { + for (std::size_t i = 0; i < size; ++i) data[offset + i] = static_cast((value >> (8 * (size - 1 - i))) & 0xFF); +} + +static std::string Ucp(const std::vector>& files) { + std::size_t offset = 0x60 + files.size() * 0x40; + std::size_t size = offset; + for (const auto& file : files) size += file.second.size(); + std::string data(size, '\0'); + PutBigEndian(data, 0, 0xB228C60A, 4); + PutBigEndian(data, 4, 1, 4); + PutBigEndian(data, 8, size, 8); + PutBigEndian(data, 0x10, files.size(), 4); + PutBigEndian(data, 0x14, 0x40, 4); + std::size_t entry = 0x60; + for (const auto& [name, contents] : files) { + data.replace(entry, name.size(), name); + PutBigEndian(data, entry + 0x20, offset, 8); + PutBigEndian(data, entry + 0x28, contents.size(), 8); + data.replace(offset, contents.size(), contents); + offset += contents.size(); + entry += 0x40; + } + return data; +} + +static void Write(const std::filesystem::path& path, const std::string& text) { + std::filesystem::create_directories(path.parent_path()); + std::FILE* file = std::fopen(path.string().c_str(), "wb"); + Require(file != nullptr, "a test file was not created"); + const bool written = std::fwrite(text.data(), 1, text.size(), file) == text.size(); + Require(std::fclose(file) == 0 && written, "a test file was not written"); +} + +static void PostUnlock(int trophyId) { + static int context = 0; + static int handle = 0; + if (context == 0) { + Require(sceNpUniversalDataSystemCreateContext(&context, InitialUser, 0, 0) == 0, "UDS context was not created"); + Require(sceNpUniversalDataSystemCreateHandle(&handle) == 0, "UDS handle was not created"); + Require(sceNpUniversalDataSystemRegisterContext(context, handle, 0) == 0, "UDS context was not registered"); + } + NpUniversalDataSystemEventPropertyObject* properties = nullptr; + Require(sceNpUniversalDataSystemCreateEventPropertyObject(&properties) == 0, "property object was not created"); + Require(sceNpUniversalDataSystemEventPropertyObjectSetInt32(properties, "_trophy_id", trophyId) == 0, "trophy id was not set"); + NpUniversalDataSystemEvent* event = nullptr; + Require(sceNpUniversalDataSystemCreateEvent("_UnlockTrophy", properties, &event, nullptr) == 0, "event was not created"); + Require(sceNpUniversalDataSystemPostEvent(context, handle, event, 0) == 0, "event was not posted"); + Require(sceNpUniversalDataSystemDestroyEvent(event) == 0, "event was not destroyed"); + Require(sceNpUniversalDataSystemDestroyEventPropertyObject(properties) == 0, "property object was not destroyed"); +} + +static int TrophyContext(std::uint32_t serviceLabel) { + int context = 0; + Require(sceNpTrophy2CreateContext(&context, InitialUser, serviceLabel, 0) == 0, "trophy context was not created"); + return context; +} + +static NpTrophy2Data Trophy(int context, int trophyId, NpTrophy2Details* details = nullptr) { + NpTrophy2Data data{}; + Require(sceNpTrophy2GetTrophyInfo(context, 1, trophyId, details, &data) == 0, "trophy info was not returned"); + Require(data.trophy_id == trophyId, "trophy info is for another trophy"); + return data; +} + +static NpTrophy2GameData Game(int context, NpTrophy2GameDetails* details = nullptr) { + NpTrophy2GameData data{}; + Require(sceNpTrophy2GetGameInfo(context, 1, details, &data) == 0, "game info was not returned"); + return data; +} + +struct Unlocks { + std::vector trophies; +}; + +static void Remember(void* context, const NpTrophy2Unlock& unlock) { + Require(unlock.titleId == "PPSA00000" && unlock.userId == InitialUser && unlock.serviceLabel == 0 && unlock.tick != 0, "an unlock does not carry its title, user, label and time"); + static_cast(context)->trophies.push_back(unlock.trophyId); +} + +static void ReportTrophyData(int context) { + NpTrophy2GameDetails game{}; + const auto progress = Game(context, &game); + Require(std::strcmp(game.title, "Synthetic Game") == 0 && game.num_groups == 2 && game.num_trophies == 8 && game.num_platinum == 1 && game.num_gold == 1 && game.num_silver == 1 && game.num_bronze == 5, + "the game details are not the title's"); + Require(progress.unlocked_trophies == 0 && progress.progress_percentage == 0, "the game has unlocks before any event"); + + NpTrophy2Details details{}; + Trophy(context, 1, &details); + Require(std::strcmp(details.name, "Ten") == 0 && std::strcmp(details.description, "Reach ten") == 0 && details.trophy_grade == 2 && details.group_id == -1 && !details.hidden && + details.target.type == 0 && details.target.value == 0, + "trophy 1 does not report its name, grade and group"); + Trophy(context, 6, &details); + Require(std::strcmp(details.name, "Secret") == 0 && std::strcmp(details.reward, "A hat") == 0 && details.trophy_grade == 4 && details.group_id == 1 && details.hidden && details.has_reward, + "trophy 6 does not report its hidden flag, reward and group"); + Trophy(context, 0, &details); + Require(details.trophy_grade == 1 && std::strcmp(details.name, "All of them") == 0, "the platinum is not reported"); + const auto steps = Trophy(context, 5, &details); + Require(details.target.type == 1 && details.target.value == 5 && !steps.unlocked && steps.progress.type == 1 && steps.progress.value == 0, "the progressive trophy does not report its target"); + NpTrophy2Data unknown{}; + Require(ThrowsInvalidArgument([context, &unknown] { sceNpTrophy2GetTrophyInfo(context, 1, 99, nullptr, &unknown); }), "a trophy that is not in the package was reported"); + + std::vector list(10); + std::vector listed(10); + std::uint32_t count = 0; + Require(sceNpTrophy2GetTrophyInfoArray(context, 1, 0, 10, list.data(), listed.data(), &count) == 0 && count == 8, "the trophy list does not hold every trophy"); + for (std::uint32_t i = 0; i < count; ++i) Require(list[i].trophy_id == static_cast(i) && listed[i].trophy_id == static_cast(i), "the trophy list is not in id order"); + Require(std::strcmp(list[5].name, "Five steps") == 0 && listed[5].progress.type == 1, "the trophy list does not carry names and progress"); + Require(sceNpTrophy2GetTrophyInfoArray(context, 1, 6, 5, list.data(), nullptr, &count) == 0 && count == 2 && list[0].trophy_id == 6 && list[1].trophy_id == 7, + "the trophy list does not start at its offset"); + Require(sceNpTrophy2GetTrophyInfoArray(context, 1, 8, 5, nullptr, listed.data(), &count) == 0 && count == 0, "the trophy list goes past its end"); + + NpTrophy2GroupDetails group{}; + NpTrophy2GroupData groupData{}; + Require(sceNpTrophy2GetGroupInfo(context, 1, 1, &group, &groupData) == 0 && std::strcmp(group.title, "Expansion") == 0 && group.num_trophies == 2 && group.num_bronze == 2 && groupData.group_id == 1, + "group 1 is not reported"); + NpTrophy2GroupData unknownGroup{}; + Require(ThrowsInvalidArgument([context, &unknownGroup] { sceNpTrophy2GetGroupInfo(context, 1, 7, nullptr, &unknownGroup); }), "a group that is not in the package was reported"); + std::vector groups(4); + Require(sceNpTrophy2GetGroupInfoArray(context, 1, 0, 4, groups.data(), nullptr, &count) == 0 && count == 2 && groups[0].group_id == -1 && std::strcmp(groups[0].title, "Synthetic Game") == 0 && + groups[0].num_trophies == 6 && groups[1].group_id == 1, + "the group list does not hold the base game and group 1"); +} + +static void PostUnlocks(int context) { + Unlocks unlocks; + const auto listener = NpTrophy2AddUnlockListener_nid_no_patch(Remember, &unlocks); + for (const int trophyId : {1, 2, 3, 6}) PostUnlock(trophyId); + const auto partway = Game(context); + Require(!Trophy(context, 0).unlocked && partway.unlocked_trophies == 4 && partway.unlocked_gold == 1 && partway.unlocked_silver == 1 && partway.unlocked_bronze == 2 && + partway.progress_percentage == (6 + 2 + 2) * 100 / (6 + 2 + 5), + "the game does not count the unlocks by grade"); + PostUnlock(4); + Require(!Trophy(context, 0).unlocked, "the platinum unlocked before every trophy that names it"); + for (const int trophyId : {5, 0, 42}) PostUnlock(trophyId); + const auto refused = Game(context); + Require(!Trophy(context, 5).unlocked && !Trophy(context, 0).unlocked && refused.unlocked_trophies == 5 && refused.unlocked_platinum == 0 && refused.unlocked_bronze == 3 && + refused.progress_percentage == (6 + 2 + 3) * 100 / (6 + 2 + 5), + "a progressive, platinum or unknown trophy posted through UDS unlocked, or changed the counts"); + NpTrophy2RemoveUnlockListener_nid_no_patch(listener); + Require(unlocks.trophies == (std::vector{1, 2, 3, 6, 4}), "the listener did not see each posted unlock once, in order, or saw a refused one"); +} + +static void ReportPlaceholders() { + for (const std::uint32_t serviceLabel : {1u, 2u}) { + const auto context = TrophyContext(serviceLabel); + NpTrophy2GameDetails game{}; + Game(context, &game); + NpTrophy2Details details{}; + Trophy(context, 12, &details); + std::uint32_t count = 0; + NpTrophy2Data listed{}; + Require(std::strcmp(game.title, "Game") == 0 && std::strcmp(details.name, "Trophy") == 0 && + sceNpTrophy2GetTrophyInfoArray(context, 1, 0, 10, nullptr, &listed, &count) == 0 && count == 1, + "a missing or malformed trophy package does not leave the placeholders"); + } +} + +int main() { + const auto previous = std::filesystem::current_path(); + const auto root = std::filesystem::temp_directory_path() / ("anyps5-trophy-data-" + std::to_string(std::random_device{}())); + std::filesystem::create_directories(root); + std::filesystem::current_path(root); + Write("app0/sce_sys/param.json", R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":"Example"}}})"); + Write("app0/sce_sys/trophy2/trophy00.ucp", Ucp({{"tropconf.json", TropConf}, {"tropmeta_en-US.json", TropMeta}})); + Write("app0/sce_sys/trophy2/trophy02.ucp", "not a package"); + + const auto context = TrophyContext(0); + ReportTrophyData(context); + PostUnlocks(context); + ReportPlaceholders(); + + std::filesystem::current_path(previous); + std::filesystem::remove_all(root); + std::puts("Trophy data tests passed"); + return 0; +} diff --git a/core/libs/tests/GuestNpTrophyStats.cpp b/core/libs/tests/GuestNpTrophyStats.cpp new file mode 100644 index 0000000000..a8e94f7516 --- /dev/null +++ b/core/libs/tests/GuestNpTrophyStats.cpp @@ -0,0 +1,365 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceNpTrophy2/include/NpTrophy2Types.hpp" +#include "prx/libSceNpTrophy2/include/TrophyStore.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif + +struct NpUniversalDataSystemEvent; +struct NpUniversalDataSystemEventPropertyObject; + +extern "C" { +int APS5_VABI sceNpUniversalDataSystemCreateContext(int*, int, uint32_t, uint64_t); +int APS5_VABI sceNpUniversalDataSystemRegisterContext(int, int, uint64_t); +int APS5_VABI sceNpUniversalDataSystemCreateHandle(int*); +int APS5_VABI sceNpUniversalDataSystemCreateEvent(const char*, const NpUniversalDataSystemEventPropertyObject*, NpUniversalDataSystemEvent**, NpUniversalDataSystemEventPropertyObject**); +int APS5_VABI sceNpUniversalDataSystemDestroyEvent(NpUniversalDataSystemEvent*); +int APS5_VABI sceNpUniversalDataSystemPostEvent(int, int, const void*, uint64_t); +int APS5_VABI sceNpUniversalDataSystemCreateEventPropertyObject(NpUniversalDataSystemEventPropertyObject**); +int APS5_VABI sceNpUniversalDataSystemDestroyEventPropertyObject(NpUniversalDataSystemEventPropertyObject*); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetInt32(NpUniversalDataSystemEventPropertyObject*, const char*, int32_t); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetUInt32(NpUniversalDataSystemEventPropertyObject*, const char*, uint32_t); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetInt64(NpUniversalDataSystemEventPropertyObject*, const char*, int64_t); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetUInt64(NpUniversalDataSystemEventPropertyObject*, const char*, uint64_t); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetFloat32(NpUniversalDataSystemEventPropertyObject*, const char*, float); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetObject(NpUniversalDataSystemEventPropertyObject*, const char*, const NpUniversalDataSystemEventPropertyObject*, NpUniversalDataSystemEventPropertyObject**); +int APS5_VABI sceNpTrophy2CreateContext(int*, int, uint32_t, uint64_t); +int APS5_VABI sceNpTrophy2GetGameInfo(int, int, NpTrophy2GameDetails*, NpTrophy2GameData*); +int APS5_VABI sceNpTrophy2GetTrophyInfo(int, int, int, NpTrophy2Details*, NpTrophy2Data*); +} + +static constexpr int InitialUser = 0x10000000; + +static const char TropConf[] = R"({"defaultLanguage":"en-US","trophies":[ + {"id":"0","grade":"P"}, + {"id":"1","grade":"G","platinumTrophyId":"0","unlockCondition":{"udsStatId":"10","comparator":"ge","targetValue":"10","progressive":false}}, + {"id":"2","grade":"S","platinumTrophyId":"0","unlockCondition":{"udsStatId":"20","comparator":"gt","targetValue":"100"}}, + {"id":"3","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"30","comparator":"le","targetValue":"5"}}, + {"id":"4","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"30","comparator":"lt","targetValue":"3"}}, + {"id":"5","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"40","comparator":"ge","targetValue":"5","progressive":true}}, + {"id":"6","grade":"B","groupId":"1","hidden":true,"hasReward":true}, + {"id":"7","grade":"B","groupId":"1"} +]})"; + +static const char TropMeta[] = R"({"metadata":{"titleMetadata":{"name":"Synthetic Game"}, + "trophyMetadata":[{"id":"0","name":"All of them"},{"id":"1","name":"Ten","detail":"Reach ten"},{"id":"5","name":"Five steps"}, + {"id":"6","name":"Secret","detail":"A hidden one","reward":"A hat"}], + "groupMetadata":[{"id":"1","name":"Expansion"}]}})"; + +static const char StatsDefinition[] = R"({"statDefinitionArray":[ + {"statId":10,"dataType":"uint64","aggregation":"latest"},{"statId":20,"dataType":"uint64","aggregation":"latest"}, + {"statId":30,"dataType":"uint32","aggregation":"latest"},{"statId":40,"dataType":"uint32","aggregation":"latest"}]})"; + +static const char SumDefinition[] = R"({"statDefinitionArray":[{"statId":10,"dataType":"uint64","aggregation":"sum"}]})"; + +static const char StatsExtraction[] = R"({"statsExtractionRuleArray":[ + {"condition":{"eventName":"COLLECT"},"action":{"input":"$.counter","output":{"statId":10}}}, + {"condition":{"eventName":"COLLECT"},"action":{"input":"$.total","output":{"statId":20}}}, + {"condition":{"eventName":"TIME"},"action":{"input":"$.seconds","output":{"statId":30}}}, + {"condition":{"eventName":"STEPS"},"action":{"input":"$.player.steps","output":{"statId":40}}}]})"; + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +static void PutBigEndian(std::string& data, std::size_t offset, std::uint64_t value, std::size_t size) { + for (std::size_t i = 0; i < size; ++i) data[offset + i] = static_cast((value >> (8 * (size - 1 - i))) & 0xFF); +} + +static std::string Ucp(const std::vector>& files) { + std::size_t offset = 0x60 + files.size() * 0x40; + std::size_t size = offset; + for (const auto& file : files) size += file.second.size(); + std::string data(size, '\0'); + PutBigEndian(data, 0, 0xB228C60A, 4); + PutBigEndian(data, 4, 1, 4); + PutBigEndian(data, 8, size, 8); + PutBigEndian(data, 0x10, files.size(), 4); + PutBigEndian(data, 0x14, 0x40, 4); + std::size_t entry = 0x60; + for (const auto& [name, contents] : files) { + data.replace(entry, name.size(), name); + PutBigEndian(data, entry + 0x20, offset, 8); + PutBigEndian(data, entry + 0x28, contents.size(), 8); + data.replace(offset, contents.size(), contents); + offset += contents.size(); + entry += 0x40; + } + return data; +} + +static void Write(const std::filesystem::path& path, const std::string& text) { + std::filesystem::create_directories(path.parent_path()); + std::FILE* file = std::fopen(path.string().c_str(), "wb"); + Require(file != nullptr, "a test file was not created"); + const bool written = std::fwrite(text.data(), 1, text.size(), file) == text.size(); + Require(std::fclose(file) == 0 && written, "a test file was not written"); +} + +static std::string Read(const char* path) { + std::string text; + std::FILE* file = std::fopen(path, "rb"); + if (file == nullptr) return text; + char buffer[256]; + for (std::size_t count; (count = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, count); + std::fclose(file); + return text; +} + +static bool Exists(const char* path) { + std::FILE* file = std::fopen(path, "rb"); + if (file == nullptr) return false; + std::fclose(file); + return true; +} + +class StderrCapture { +public: + StderrCapture() { + std::fflush(stderr); + file = std::tmpfile(); + Require(file != nullptr, "cannot open a temporary file for stderr"); +#ifdef _WIN32 + saved = _dup(_fileno(stderr)); + Require(saved >= 0 && _dup2(_fileno(file), _fileno(stderr)) == 0, "cannot capture stderr"); +#else + saved = dup(fileno(stderr)); + Require(saved >= 0 && dup2(fileno(file), fileno(stderr)) == fileno(stderr), "cannot capture stderr"); +#endif + } + std::string Finish() { + std::fflush(stderr); +#ifdef _WIN32 + _dup2(saved, _fileno(stderr)); + _close(saved); +#else + dup2(saved, fileno(stderr)); + close(saved); +#endif + std::rewind(file); + std::string text; + char buffer[4096]; + for (std::size_t count; (count = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, count); + std::fclose(file); + std::fwrite(text.data(), 1, text.size(), stderr); + return text; + } + +private: + std::FILE* file = nullptr; + int saved = -1; +}; + +struct Uds { + int context = 0; + int handle = 0; +}; + +static Uds OpenUds(std::uint32_t serviceLabel) { + Uds uds; + Require(sceNpUniversalDataSystemCreateContext(&uds.context, InitialUser, serviceLabel, 0) == 0, "UDS context was not created"); + Require(sceNpUniversalDataSystemCreateHandle(&uds.handle) == 0, "UDS handle was not created"); + Require(sceNpUniversalDataSystemRegisterContext(uds.context, uds.handle, 0) == 0, "UDS context was not registered"); + return uds; +} + +template +static void Post(const Uds& uds, const char* name, TSet set) { + NpUniversalDataSystemEventPropertyObject* properties = nullptr; + Require(sceNpUniversalDataSystemCreateEventPropertyObject(&properties) == 0, "property object was not created"); + set(properties); + NpUniversalDataSystemEvent* event = nullptr; + Require(sceNpUniversalDataSystemCreateEvent(name, properties, &event, nullptr) == 0, "event was not created"); + Require(sceNpUniversalDataSystemPostEvent(uds.context, uds.handle, event, 0) == 0, "event was not posted"); + Require(sceNpUniversalDataSystemDestroyEvent(event) == 0, "event was not destroyed"); + Require(sceNpUniversalDataSystemDestroyEventPropertyObject(properties) == 0, "property object was not destroyed"); +} + +static int TrophyContext(std::uint32_t serviceLabel) { + int context = 0; + Require(sceNpTrophy2CreateContext(&context, InitialUser, serviceLabel, 0) == 0, "trophy context was not created"); + return context; +} + +static NpTrophy2Data Trophy(int context, int trophyId, NpTrophy2Details* details = nullptr) { + NpTrophy2Data data{}; + Require(sceNpTrophy2GetTrophyInfo(context, 1, trophyId, details, &data) == 0, "trophy info was not returned"); + Require(data.trophy_id == trophyId, "trophy info is for another trophy"); + return data; +} + +static NpTrophy2GameData Game(int context, NpTrophy2GameDetails* details = nullptr) { + NpTrophy2GameData data{}; + Require(sceNpTrophy2GetGameInfo(context, 1, details, &data) == 0, "game info was not returned"); + return data; +} + +struct Unlocks { + std::vector> trophies; +}; + +static void Remember(void* context, const NpTrophy2Unlock& unlock) { + Require(unlock.titleId == "PPSA00000" && unlock.userId == InitialUser && unlock.tick != 0, "an unlock does not carry its title, user and time"); + static_cast(context)->trophies.emplace_back(unlock.serviceLabel, unlock.trophyId); +} + +static void KeepStatsAcrossRuns(int context) { + NpTrophy2Details details{}; + const auto steps = Trophy(context, 5, &details); + Require(details.target.type == 1 && details.target.value == 5 && !steps.unlocked && steps.progress.type == 1 && steps.progress.value == 2, + "the progressive trophy does not report the stat kept from the last run"); +} + +static void UnlockThroughStats(int context, Unlocks& unlocks) { + const auto uds = OpenUds(0); + Post(uds, "COLLECT", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt64(properties, "counter", 9) == 0, "counter was not set"); + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt64(properties, "total", 100) == 0, "total was not set"); + }); + Require(!Trophy(context, 1).unlocked && !Trophy(context, 2).unlocked && unlocks.trophies.empty(), "ge or gt unlocked below its target"); + Post(uds, "COLLECT", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt32(properties, "counter", 10) == 0, "counter was not set"); + }); + Require(Trophy(context, 1).unlocked && !Trophy(context, 2).unlocked, "ge did not unlock at its target"); + Post(uds, "COLLECT", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetInt32(properties, "counter", 3) == 0, "counter was not set"); + Require(sceNpUniversalDataSystemEventPropertyObjectSetInt64(properties, "total", 101) == 0, "total was not set"); + }); + Require(Trophy(context, 2).unlocked, "gt did not unlock past its target"); + + Post(uds, "TIME", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt32(properties, "seconds", 6) == 0, "seconds was not set"); + }); + Require(!Trophy(context, 3).unlocked && !Trophy(context, 4).unlocked, "le or lt unlocked above its target"); + Post(uds, "TIME", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt32(properties, "seconds", 5) == 0, "seconds was not set"); + }); + Require(Trophy(context, 3).unlocked && !Trophy(context, 4).unlocked, "le did not unlock at its target"); + Post(uds, "TIME", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetInt32(properties, "seconds", -1) == 0, "seconds was not set"); + }); + Post(uds, "TIME", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetFloat32(properties, "seconds", 1.0f) == 0, "seconds was not set"); + }); + Require(!Trophy(context, 4).unlocked, "a negative or fractional value reached lt"); + Post(uds, "TIME", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt32(properties, "seconds", 2) == 0, "seconds was not set"); + }); + Require(Trophy(context, 4).unlocked, "lt did not unlock below its target"); + + Post(uds, "NOTHING", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt32(properties, "steps", 50) == 0, "steps was not set"); + }); + const auto stepped = [&uds](std::uint32_t steps) { + Post(uds, "STEPS", [steps](NpUniversalDataSystemEventPropertyObject* properties) { + NpUniversalDataSystemEventPropertyObject* player = nullptr; + Require(sceNpUniversalDataSystemEventPropertyObjectSetObject(properties, "player", nullptr, &player) == 0, "player was not set"); + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt32(player, "steps", steps) == 0, "steps was not set"); + }); + }; + stepped(4); + const auto partway = Trophy(context, 5); + Require(!partway.unlocked && partway.progress.type == 1 && partway.progress.value == 4 && !Trophy(context, 0).unlocked, "the progressive trophy does not report its progress"); + stepped(5); + const auto done = Trophy(context, 5); + Require(done.unlocked && done.progress.value == 5, "the progressive trophy did not unlock at its target"); + Require(Trophy(context, 0).unlocked, "the platinum did not unlock with the last trophy that names it"); + + const std::vector> expected{{0, 1}, {0, 2}, {0, 3}, {0, 4}, {0, 5}, {0, 0}}; + Require(unlocks.trophies == expected, "the listener did not see each stat unlock and the platinum once, in order"); + const auto game = Game(context); + Require(game.unlocked_trophies == 6 && game.unlocked_platinum == 1 && game.unlocked_gold == 1 && game.unlocked_silver == 1 && game.unlocked_bronze == 3 && + game.progress_percentage == (6 + 2 + 3) * 100 / (6 + 2 + 5), + "the game does not count the unlocks by grade"); + Require(Read("_trophy/268435456_0.stats.txt") == "10 3\n20 101\n30 2\n40 5\n", "the stats were not stored next to the unlocks"); + const auto record = Read("_trophy/268435456_0.txt"); + for (const char* line : {"0 ", "1 ", "2 ", "3 ", "4 ", "5 "}) Require(record.find(std::string("\n") + line) != std::string::npos || record.starts_with(line), "an unlock was not stored"); +} + +static void IgnoreMissingAndMalformedFiles(Unlocks& unlocks) { + const auto missing = TrophyContext(1); + NpTrophy2GameDetails game{}; + Game(missing, &game); + Require(std::strcmp(game.title, "Game") == 0, "a title without trophy files does not report placeholders"); + Post(OpenUds(1), "COLLECT", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt32(properties, "counter", 10) == 0, "counter was not set"); + }); + Require(!Exists("_trophy/268435456_1.stats.txt"), "a title without stat rules stored a stat"); + + const auto brokenTrophies = TrophyContext(2); + Game(brokenTrophies, &game); + Require(std::strcmp(game.title, "Game") == 0, "a malformed trophy package was used"); + Post(OpenUds(2), "COLLECT", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt32(properties, "counter", 10) == 0, "counter was not set"); + }); + Require(Read("_trophy/268435456_2.stats.txt") == "10 10\n" && !Exists("_trophy/268435456_2.txt"), "a malformed trophy package disabled the stat rules, or stats unlocked a trophy without one"); + + const auto brokenRules = TrophyContext(3); + Game(brokenRules, &game); + Require(std::strcmp(game.title, "Synthetic Game") == 0, "malformed stat rules disabled the trophy package"); + Post(OpenUds(3), "COLLECT", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt32(properties, "counter", 10) == 0, "counter was not set"); + }); + Require(!Exists("_trophy/268435456_3.stats.txt") && !Trophy(brokenRules, 1).unlocked, "malformed stat rules were used"); + Require(unlocks.trophies.size() == 6, "a missing or malformed file unlocked a trophy"); +} + +static void IgnoreOtherAggregations(Unlocks& unlocks) { + StderrCapture capture; + const auto context = TrophyContext(5); + Post(OpenUds(5), "COLLECT", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt32(properties, "counter", 10) == 0, "counter was not set"); + }); + const auto log = capture.Finish(); + Require(log.find("[trophy] /app0/sce_sys/uds/uds05.ucp: stat 10 has aggregation \"sum\"; only \"latest\" is supported") != std::string::npos, + "a stat with a sum aggregation was not reported"); + Require(!Trophy(context, 1).unlocked && !Exists("_trophy/268435456_5.stats.txt") && unlocks.trophies.size() == 6, "a stat with a sum aggregation was stored or unlocked a trophy"); +} + +int main() { + const auto previous = std::filesystem::current_path(); + const auto root = std::filesystem::temp_directory_path() / ("anyps5-trophy-stats-" + std::to_string(std::random_device{}())); + std::filesystem::create_directories(root); + std::filesystem::current_path(root); + Write("app0/sce_sys/param.json", R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":"Example"}}})"); + const auto trophies = Ucp({{"tropconf.json", TropConf}, {"tropmeta_en-US.json", TropMeta}}); + const auto rules = Ucp({{"stats_definition.json", StatsDefinition}, {"stats_extraction.json", StatsExtraction}}); + Write("app0/sce_sys/trophy2/trophy00.ucp", trophies); + Write("app0/sce_sys/uds/uds00.ucp", rules); + Write("app0/sce_sys/trophy2/trophy02.ucp", "not a package"); + Write("app0/sce_sys/uds/uds02.ucp", rules); + Write("app0/sce_sys/trophy2/trophy03.ucp", trophies); + Write("app0/sce_sys/uds/uds03.ucp", rules.substr(0, rules.size() - 1)); + Write("app0/sce_sys/trophy2/trophy05.ucp", trophies); + Write("app0/sce_sys/uds/uds05.ucp", Ucp({{"stats_definition.json", SumDefinition}, {"stats_extraction.json", StatsExtraction}})); + Write("_trophy/268435456_0.stats.txt", "40 2\n"); + + Unlocks unlocks; + const auto listener = NpTrophy2AddUnlockListener_nid_no_patch(Remember, &unlocks); + const auto context = TrophyContext(0); + KeepStatsAcrossRuns(context); + UnlockThroughStats(context, unlocks); + IgnoreMissingAndMalformedFiles(unlocks); + IgnoreOtherAggregations(unlocks); + NpTrophy2RemoveUnlockListener_nid_no_patch(listener); + + std::filesystem::current_path(previous); + std::filesystem::remove_all(root); + std::puts("Trophy stat tests passed"); + return 0; +} diff --git a/core/libs/tests/GuestNpTrophyUnlocks.cpp b/core/libs/tests/GuestNpTrophyUnlocks.cpp new file mode 100644 index 0000000000..be3d6e2859 --- /dev/null +++ b/core/libs/tests/GuestNpTrophyUnlocks.cpp @@ -0,0 +1,109 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceNpTrophy2/include/NpTrophy2Types.hpp" + +#include +#include +#include +#include +#include +#include +#include + +struct NpUniversalDataSystemEvent; +struct NpUniversalDataSystemEventPropertyObject; + +extern "C" { +int APS5_VABI sceNpUniversalDataSystemCreateContext(int*, int, uint32_t, uint64_t); +int APS5_VABI sceNpUniversalDataSystemRegisterContext(int, int, uint64_t); +int APS5_VABI sceNpUniversalDataSystemCreateHandle(int*); +int APS5_VABI sceNpUniversalDataSystemCreateEvent(const char*, const NpUniversalDataSystemEventPropertyObject*, NpUniversalDataSystemEvent**, NpUniversalDataSystemEventPropertyObject**); +int APS5_VABI sceNpUniversalDataSystemDestroyEvent(NpUniversalDataSystemEvent*); +int APS5_VABI sceNpUniversalDataSystemPostEvent(int, int, const void*, uint64_t); +int APS5_VABI sceNpUniversalDataSystemCreateEventPropertyObject(NpUniversalDataSystemEventPropertyObject**); +int APS5_VABI sceNpUniversalDataSystemDestroyEventPropertyObject(NpUniversalDataSystemEventPropertyObject*); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetInt32(NpUniversalDataSystemEventPropertyObject*, const char*, int32_t); +int APS5_VABI sceNpTrophy2CreateContext(int*, int, uint32_t, uint64_t); +int APS5_VABI sceNpTrophy2GetGameInfo(int, int, NpTrophy2GameDetails*, NpTrophy2GameData*); +int APS5_VABI sceNpTrophy2GetTrophyInfo(int, int, int, NpTrophy2Details*, NpTrophy2Data*); +} + +static constexpr int InitialUser = 0x10000000; + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +static std::string Read(const char* path) { + std::string text; + std::FILE* file = std::fopen(path, "rb"); + if (file == nullptr) return text; + char buffer[256]; + for (std::size_t count; (count = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, count); + std::fclose(file); + return text; +} + +static void Post(int context, int handle, const char* name, int trophyId) { + NpUniversalDataSystemEventPropertyObject* properties = nullptr; + Require(sceNpUniversalDataSystemCreateEventPropertyObject(&properties) == 0, "property object was not created"); + Require(sceNpUniversalDataSystemEventPropertyObjectSetInt32(properties, "_trophy_id", trophyId) == 0, "trophy id was not set"); + NpUniversalDataSystemEvent* event = nullptr; + Require(sceNpUniversalDataSystemCreateEvent(name, properties, &event, nullptr) == 0, "event was not created"); + Require(sceNpUniversalDataSystemPostEvent(context, handle, event, 0) == 0, "event was not posted"); + Require(sceNpUniversalDataSystemDestroyEvent(event) == 0, "event was not destroyed"); + Require(sceNpUniversalDataSystemDestroyEventPropertyObject(properties) == 0, "property object was not destroyed"); +} + +static NpTrophy2Data Trophy(int context, int handle, int trophyId) { + NpTrophy2Data data{}; + Require(sceNpTrophy2GetTrophyInfo(context, handle, trophyId, nullptr, &data) == 0, "trophy info was not returned"); + Require(data.trophy_id == trophyId, "trophy info is for another trophy"); + return data; +} + +static std::uint32_t Unlocked(int context, int handle) { + NpTrophy2GameData data{}; + Require(sceNpTrophy2GetGameInfo(context, handle, nullptr, &data) == 0, "game info was not returned"); + return data.unlocked_trophies; +} + +static void UnlockThroughEvents() { + std::filesystem::create_directories("app0/sce_sys"); + std::FILE* param = std::fopen("app0/sce_sys/param.json", "wb"); + Require(param != nullptr, "param.json was not created"); + std::fputs(R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":"Example"}}})", param); + std::fclose(param); + int udsContext = 0; + int handle = 0; + Require(sceNpUniversalDataSystemCreateContext(&udsContext, InitialUser, 0, 0) == 0, "UDS context was not created"); + Require(sceNpUniversalDataSystemCreateHandle(&handle) == 0, "UDS handle was not created"); + Require(sceNpUniversalDataSystemRegisterContext(udsContext, handle, 0) == 0, "UDS context was not registered"); + int trophyContext = 0; + Require(sceNpTrophy2CreateContext(&trophyContext, InitialUser, 0, 0) == 0, "trophy context was not created"); + + Require(!Trophy(trophyContext, handle, 12).unlocked && Unlocked(trophyContext, handle) == 0, "trophy 12 is unlocked before its event"); + Post(udsContext, handle, "_UnlockTrophy", 12); + const auto unlocked = Trophy(trophyContext, handle, 12); + Require(unlocked.unlocked && unlocked.timestamp_tick != 0, "the posted unlock is not reported with its time"); + Require(Unlocked(trophyContext, handle) == 1, "the game does not count the posted unlock"); + + Post(udsContext, handle, "_ChallengeComplete", 13); + Require(!Trophy(trophyContext, handle, 13).unlocked, "an unknown event unlocked a trophy"); + Post(udsContext, handle, "_UnlockTrophy", 12); + Require(Trophy(trophyContext, handle, 12).timestamp_tick == unlocked.timestamp_tick && Unlocked(trophyContext, handle) == 1, "a repeated unlock changed the record"); + + Require(Read("_trophy/268435456_0.txt") == "12 " + std::to_string(unlocked.timestamp_tick) + "\n", "the unlock was not stored in the title folder"); +} + +int main() { + const auto previous = std::filesystem::current_path(); + const auto root = std::filesystem::temp_directory_path() / ("anyps5-trophy-unlocks-" + std::to_string(std::random_device{}())); + std::filesystem::create_directories(root); + std::filesystem::current_path(root); + UnlockThroughEvents(); + std::filesystem::current_path(previous); + std::filesystem::remove_all(root); + return 0; +} diff --git a/core/libs/tests/GuestNpTus.cpp b/core/libs/tests/GuestNpTus.cpp new file mode 100644 index 0000000000..11d9cf5cf5 --- /dev/null +++ b/core/libs/tests/GuestNpTus.cpp @@ -0,0 +1,274 @@ +#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 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 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); +} diff --git a/core/libs/tests/GuestNpWebApi2.cpp b/core/libs/tests/GuestNpWebApi2.cpp new file mode 100644 index 0000000000..fdebb7853e --- /dev/null +++ b/core/libs/tests/GuestNpWebApi2.cpp @@ -0,0 +1,85 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include + +struct NpWebApi2MemoryPoolStats { + std::size_t pool_size; + std::size_t max_inuse_size; + std::size_t current_inuse_size; + std::int32_t reserved; +}; + +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 sceNpWebApi2PushEventSetHandleTimeout(int, int, std::uint32_t); +int APS5_VABI sceNpWebApi2CreateUserContext(int, int); +int APS5_VABI sceNpWebApi2CreateRequest(int, const char*, const char*, const char*, const void*, std::int64_t*); +int APS5_VABI sceNpWebApi2SetRequestTimeout(std::int64_t, std::uint32_t); +int APS5_VABI sceNpWebApi2DeleteRequest(std::int64_t); +} + +static void Require(bool value) { if (!value) std::abort(); } + +template +static bool Throws(TCall call) { + try { + call(); + } catch (const std::exception&) { + return true; + } + return false; +} + +int main() { + 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}; + Require(sceNpWebApi2GetMemoryPoolStats(context, &stats) == 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); })); + const int handle = sceNpWebApi2PushEventCreateHandle(context); + Require(handle > 0); + Require(sceNpWebApi2PushEventAbortHandle(context, handle) == 0); + Require(sceNpWebApi2PushEventAbortHandle(context, handle + 1000) == 0); + Require(sceNpWebApi2PushEventSetHandleTimeout(context, handle, 30000) == 0); + const int user = sceNpWebApi2CreateUserContext(context, 1); + std::int64_t request = 0; + Require(user > 0 && sceNpWebApi2CreateRequest(user, "sessionManager", "/v1/sessions", "GET", nullptr, &request) == 0); + Require(sceNpWebApi2SetRequestTimeout(request, 30000000) == 0); + Require(sceNpWebApi2DeleteRequest(request) == 0); + Require(sceNpWebApi2Terminate(context) == 0); + Require(Throws([&] { sceNpWebApi2GetMemoryPoolStats(context, &stats); })); + Require(Throws([&] { sceNpWebApi2PushEventAbortHandle(context, handle); })); + Require(Throws([&] { sceNpWebApi2PushEventSetHandleTimeout(context, handle, 30000); })); + 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(Throws([&] { sceNpWebApi2GetMemoryPoolStats(created, &stats); })); + } + return 0; +} diff --git a/core/libs/tests/GuestOfflineImports.cpp b/core/libs/tests/GuestOfflineImports.cpp index b8e227cd6e..32dcdd9acf 100644 --- a/core/libs/tests/GuestOfflineImports.cpp +++ b/core/libs/tests/GuestOfflineImports.cpp @@ -13,13 +13,20 @@ 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 sceRudpInit_nid_postfix(void*, int); +int APS5_VABI sceRudpActivate(); int APS5_VABI sceRudpGetStatus(void*, std::size_t); -int APS5_VABI sceRudpTerminate(); +int APS5_VABI sceRudpEnd(); } static void Require(bool value) { if (!value) std::abort(); } @@ -61,22 +68,39 @@ int main() { Require(std::memcmp(&output, original.data(), sizeof(output)) == 0); } + std::array trialsFlag{0xa5a5a5a5u, 0x5a5a5a5au}; + Require(sceNpEntitlementAccessGetGameTrialsFlag(trialsFlag.data()) == 0); + Require(trialsFlag[0] == 0 && trialsFlag[1] == 0x5a5a5a5au); + Require(sceNpEntitlementAccessGetGameTrialsFlag(nullptr) == parameter); + + std::array pftFlag{0xa5a5a5a5u, 0x5a5a5a5au}; + Require(sceNpEntitlementAccessGetPftFlag(pftFlag.data()) == 0); + Require(pftFlag[0] == 0 && pftFlag[1] == 0x5a5a5a5au); + Require(sceNpEntitlementAccessGetPftFlag(nullptr) == parameter); + constexpr int signedOut = static_cast(0x80550006); Require(sceNpEntitlementAccessRequestUnifiedEntitlementInfoList() == signedOut); Require(sceNpEntitlementAccessPollUnifiedEntitlementInfoList() == signedOut); Require(sceNpEntitlementAccessRequestServiceEntitlementInfoList() == signedOut); Require(sceNpEntitlementAccessPollServiceEntitlementInfoList() == signedOut); + Require(sceNpEntitlementAccessRequestUnifiedEntitlementInfo() == signedOut); + Require(sceNpEntitlementAccessPollUnifiedEntitlementInfo() == signedOut); + Require(sceNpEntitlementAccessRequestServiceEntitlementInfo() == signedOut); + Require(sceNpEntitlementAccessPollServiceEntitlementInfo() == signedOut); + Require(sceRudpActivate() == 0); std::array status; status.fill(0x5a); const auto originalStatus = status; constexpr int rudpNotInitialized = static_cast(0x80770001); Require(sceRudpGetStatus(status.data(), status.size()) == rudpNotInitialized); Require(status == originalStatus); - Require(sceRudpInit_nid_postfix(nullptr, 0) == 0); + alignas(8) static unsigned char rudpPool[0xC58]; + Require(sceRudpInit_nid_postfix(rudpPool, sizeof(rudpPool)) == 0); + Require(sceRudpActivate() == 0); Require(sceRudpGetStatus(status.data(), status.size()) == 0); for (unsigned char byte : status) Require(byte == 0); - Require(sceRudpGetStatus(nullptr, 0) == 0); - Require(sceRudpTerminate() == 0); + Require(sceRudpGetStatus(nullptr, 0) == static_cast(0x80770004)); + Require(sceRudpEnd() == 0); return 0; } diff --git a/core/libs/tests/GuestOnce.cpp b/core/libs/tests/GuestOnce.cpp index b82ec22ef6..3e60f58ec3 100644 --- a/core/libs/tests/GuestOnce.cpp +++ b/core/libs/tests/GuestOnce.cpp @@ -1,4 +1,4 @@ -#include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" #include #include #include @@ -9,7 +9,17 @@ struct GuestOnce { std::int32_t state; void* mutex; }; static_assert(sizeof(GuestOnce) == 16 && offsetof(GuestOnce, mutex) == 8); using Initializer = void (APS5_VABI *)(); -extern "C" int APS5_VABI pthread_once_nid_postfix(GuestOnce*, Initializer); +extern "C" { +int APS5_VABI pthread_once_nid_postfix(GuestOnce*, Initializer); +int APS5_VABI scePthreadOnce(std::int32_t* once, Initializer init); +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +void APS5_VABI scePthreadExit(void* retval); +int APS5_VABI scePthreadCancel(Pthread thread); +void APS5_VABI scePthreadTestcancel(); +Pthread APS5_VABI scePthreadSelf(); +void APS5_VABI __pthread_cleanup_push_imp_nid_postfix(void (APS5_VABI* routine)(void*), void* argument, void* info); +} static void Require(bool value) { if (!value) std::abort(); } static GuestOnce control{}, nested{}, retry{}; static std::atomic calls{0}, arrived{0}; @@ -24,6 +34,35 @@ static void APS5_VABI Initialize() { static void APS5_VABI Retry() { if (++attempts == 1) throw 7; } +static GuestOnce exited{}, cancelled{}; +static std::int32_t sceExited = 0, sceCancelled = 0; +static int retried = 0; +static void APS5_VABI ExitInsideInitializer() { scePthreadExit(nullptr); } +static void APS5_VABI CancelInsideInitializer() { + Require(scePthreadCancel(scePthreadSelf()) == 0); + scePthreadTestcancel(); +} +static void APS5_VABI CountRetry() { ++retried; } +static void APS5_VABI AbortCleanup(void*) { std::abort(); } +static void APS5_VABI PushAndThrow() { + std::uintptr_t info[8]; + __pthread_cleanup_push_imp_nid_postfix(AbortCleanup, nullptr, info); + throw 7; +} +static GuestOnce pushed{}; +static std::int32_t scePushed = 0; +struct OnceCall { GuestOnce* control; std::int32_t* sceControl; Initializer initialize; }; +static void* APS5_VABI CallOnce(void* argument) { + const auto& call = *static_cast(argument); + if (call.control) pthread_once_nid_postfix(call.control, call.initialize); + else scePthreadOnce(call.sceControl, call.initialize); + return nullptr; +} +static void LeaveInsideInitializer(OnceCall call) { + Pthread thread{}; + Require(scePthreadCreate(&thread, nullptr, CallOnce, &call, nullptr) == 0); + Require(scePthreadJoin(thread, nullptr) == 0); +} int main() { std::array workers; for (auto& worker : workers) worker = std::thread([] { @@ -41,4 +80,22 @@ int main() { Require(pthread_once_nid_postfix(&control, nullptr) == 22); GuestOnce invalid{9, nullptr}; Require(pthread_once_nid_postfix(&invalid, Initialize) == 22); + LeaveInsideInitializer({&exited, nullptr, ExitInsideInitializer}); + Require(exited.state == 0); + Require(pthread_once_nid_postfix(&exited, CountRetry) == 0 && exited.state == 1 && retried == 1); + LeaveInsideInitializer({&cancelled, nullptr, CancelInsideInitializer}); + Require(cancelled.state == 0); + Require(pthread_once_nid_postfix(&cancelled, CountRetry) == 0 && cancelled.state == 1 && retried == 2); + LeaveInsideInitializer({nullptr, &sceExited, ExitInsideInitializer}); + Require(sceExited == 0); + Require(scePthreadOnce(&sceExited, CountRetry) == 0 && sceExited == 1 && retried == 3); + LeaveInsideInitializer({nullptr, &sceCancelled, CancelInsideInitializer}); + Require(sceCancelled == 0); + Require(scePthreadOnce(&sceCancelled, CountRetry) == 0 && sceCancelled == 1 && retried == 4); + try { pthread_once_nid_postfix(&pushed, PushAndThrow); std::abort(); } + catch (int value) { Require(value == 7 && pushed.state == 0); } + Require(pthread_once_nid_postfix(&pushed, CountRetry) == 0 && pushed.state == 1 && retried == 5); + try { scePthreadOnce(&scePushed, PushAndThrow); std::abort(); } + catch (int value) { Require(value == 7 && scePushed == 0); } + Require(scePthreadOnce(&scePushed, CountRetry) == 0 && scePushed == 1 && retried == 6); } diff --git a/core/libs/tests/GuestOperationMode.cpp b/core/libs/tests/GuestOperationMode.cpp new file mode 100644 index 0000000000..b3fe29f518 --- /dev/null +++ b/core/libs/tests/GuestOperationMode.cpp @@ -0,0 +1,22 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#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) { + try { sceKernelGetOperationMode(mode, submode); } catch (const std::invalid_argument&) { return true; } + return false; +} +int main() { + int out[4] = {-1, -1, -1, -1}; + sceKernelGetOperationMode(&out[0], &out[2]); + Require(out[0] == 2 && out[1] == -1); + Require(out[2] == 0 && out[3] == -1); + int value = -1; + 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 new file mode 100644 index 0000000000..aa3572b7e1 --- /dev/null +++ b/core/libs/tests/GuestOpusCeltDec.cpp @@ -0,0 +1,135 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceOpusCeltDecInitialize(std::uint32_t*); +int APS5_VABI sceOpusCeltDecTerminate(std::uint32_t*); +int APS5_VABI sceOpusCeltDecGetSize(int); +int APS5_VABI sceOpusCeltDecCreateEx(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*); +} + +namespace { + +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() { + std::uint32_t context = 0; + Require(sceOpusCeltDecInitialize(&context) == 0); + RequireThrows([&] { sceOpusCeltDecInitialize(&context); }); + Require(sceOpusCeltDecGetSize(1) == 640 && sceOpusCeltDecGetSize(2) == 640); + RequireThrows([&] { sceOpusCeltDecGetSize(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}) + RequireThrows([&] { sceOpusCeltDecCreateEx(&context, unsupported.data(), rate, 2); }); + for (int rate : {8000, 12000, 16000, 24000, 48000}) { + std::array toneState{}; + Require(sceOpusCeltDecCreateEx(&context, toneState.data(), rate, 2) == 0); + const int samples = 960 * rate / 48000; + std::array tone{}; + for (int frame = 0; frame < 4; ++frame) { + tone.fill(12345); + RequireThrows([&] { sceOpusCeltDecDecode(toneState.data(), CELT_TONE[frame], 60, tone.data() + 1, samples * 4 - 1); }); + Require(sceOpusCeltDecDecode(toneState.data(), CELT_TONE[frame], 60, tone.data() + 1, samples * 4) == samples * 4); + Require(tone[0] == 12345 && tone[samples * 2 + 1] == 12345); + if (frame == 1 || frame == 2) RequireTone(tone.data() + 1, samples, 2, rate); + } + Require(sceOpusCeltDecDestroy(toneState.data()) == 0); + } + + Require(sceOpusCeltDecDestroy(state.data()) == 0); + RequireThrows([&] { sceOpusCeltDecDestroy(state.data()); }); + RequireThrows([&] { sceOpusCeltDecDecode(state.data(), CELT_STEREO + 2, CELT_STEREO[0], pcm.data(), 3840); }); + 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 ef1bf7304d..a5824ee855 100644 --- a/core/libs/tests/GuestOpusDec.cpp +++ b/core/libs/tests/GuestOpusDec.cpp @@ -40,6 +40,12 @@ const std::uint8_t OPUS_STEREO[] = { 0xA7, 0xFA, 0x99, 0xF9, 0x47, 0x32, 0x07, 0xAF, 0x24, 0xBF, 0x14, 0xC5, }; +const std::uint8_t OPUS_NARROWBAND[] = { + 11, 0, 0x08, 0x83, 0x9C, 0xE1, 0x7B, 0x2F, 0x52, 0xCA, 0x9E, 0x75, 0xD8, + 13, 0, 0x08, 0x96, 0x51, 0x3A, 0x33, 0x4A, 0xF8, 0x29, 0xF5, 0xBE, 0x40, 0x15, 0x98, + 10, 0, 0x08, 0x85, 0x89, 0x9D, 0x73, 0x0F, 0xC2, 0xB1, 0xE2, 0x48, +}; + } // namespace int main() { @@ -56,7 +62,7 @@ int main() { RequireThrows([&] { sceOpusDecCreateEx(&context, state.data(), 48000, 2); }); RequireThrows([&] { sceOpusDecGetSize(3); }); std::array unsupported{}; - for (int rate : {8000, 12000, 16000, 24000}) + for (int rate : {0, 22050, 44100, 96000}) RequireThrows([&] { sceOpusDecCreateEx(&context, unsupported.data(), rate, 2); }); std::array pcm{}, reference{}; pcm.fill(12345); @@ -93,6 +99,25 @@ int main() { Require(std::any_of(mono.begin() + 1, mono.end() - 1, [](auto value) { return value != 0; })); offset += bytes + 2; } + for (int rate : {8000, 12000, 16000, 24000}) { + std::array rateState{}; + Require(sceOpusDecCreateEx(&context, rateState.data(), rate, 2) == 0); + const int samples = 960 * rate / 48000; + std::array resampled{}; + int peak = 0; + for (std::size_t at = 0, frame = 0; frame < 3; ++frame) { + const int bytes = OPUS_STEREO[at] | (OPUS_STEREO[at + 1] << 8); + resampled.fill(12345); + RequireThrows([&] { sceOpusDecDecode(rateState.data(), OPUS_STEREO + at + 2, bytes, resampled.data() + 1, samples * 4 - 1); }); + Require(sceOpusDecDecode(rateState.data(), OPUS_STEREO + at + 2, bytes, resampled.data() + 1, samples * 4) == samples * 4); + Require(resampled[0] == 12345 && resampled[samples * 2 + 1] == 12345); + for (int index = 1; index <= samples * 2; ++index) peak = std::max(peak, std::abs(static_cast(resampled[index]))); + at += bytes + 2; + } + Require(peak > 900 && peak < 1200); + Require(sceOpusDecDestroy(rateState.data()) == 0); + } + Require(sceOpusDecDestroy(state.data()) == 0); RequireThrows([&] { sceOpusDecDestroy(state.data()); }); RequireThrows([&] { sceOpusDecDecode(state.data(), OPUS_STEREO + 2, 70, pcm.data(), 3840); }); @@ -111,4 +136,25 @@ int main() { Require(sceOpusDecDecode(otherState.data(), OPUS_STEREO + offset + 2, OPUS_STEREO[offset], reference.data(), 3840) == 3840); Require(sceOpusDecDestroy(otherState.data()) == 0); Require(sceOpusDecTerminate(&otherContext) == 0); + + std::uint32_t narrowContext = 0; + std::array narrowState{}; + Require(sceOpusDecInitialize(&narrowContext) == 0); + Require(sceOpusDecCreateEx(&narrowContext, narrowState.data(), 48000, 1) == 0); + std::array narrow{}; + bool audible = false; + for (std::size_t at = 0; at < sizeof(OPUS_NARROWBAND);) { + const int bytes = OPUS_NARROWBAND[at] | (OPUS_NARROWBAND[at + 1] << 8); + narrow.fill(12345); + Require(sceOpusDecDecode(narrowState.data(), OPUS_NARROWBAND + at + 2, bytes, narrow.data() + 1, 1920) == 1920); + Require(narrow.front() == 12345 && narrow.back() == 12345); + audible = audible || std::any_of(narrow.begin() + 1, narrow.end() - 1, [](auto value) { return value != 0 && value != 12345; }); + at += bytes + 2; + } + Require(audible); + narrow.fill(12345); + Require(sceOpusDecDecode(narrowState.data(), OPUS_STEREO + 2, 70, narrow.data() + 1, 1920) == 1920); + Require(narrow.front() == 12345 && narrow.back() == 12345); + Require(sceOpusDecDestroy(narrowState.data()) == 0); + Require(sceOpusDecTerminate(&narrowContext) == 0); } diff --git a/core/libs/tests/GuestOverflowError.cpp b/core/libs/tests/GuestOverflowError.cpp new file mode 100644 index 0000000000..9bfb21a082 --- /dev/null +++ b/core/libs/tests/GuestOverflowError.cpp @@ -0,0 +1,38 @@ +#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 void* what; }; + +extern "C" { +extern const TypeRecord _ZTISt13runtime_error_nid_postfix; +extern const TypeRecord _ZTISt14overflow_error_nid_postfix; +extern const Vtable _ZTVSt13runtime_error_nid_postfix; +extern const Vtable _ZTVSt14overflow_error_nid_postfix; +void APS5_VABI _ZNSt13runtime_errorC1EPKc_nid_postfix(Object*, const char*); +void APS5_VABI _ZNSt14overflow_errorD1Ev_nid_postfix(Object*); +} + +static void Require(bool value) { if (!value) std::abort(); } + +int main() { + const auto& type = _ZTISt14overflow_error_nid_postfix; + const auto& runtimeType = _ZTISt13runtime_error_nid_postfix; + Require(std::strcmp(type.name, "St14overflow_error") == 0); + Require(type.base == &runtimeType && type.vtable == runtimeType.vtable); + const auto& table = _ZTVSt14overflow_error_nid_postfix; + const auto& runtimeTable = _ZTVSt13runtime_error_nid_postfix; + Require(table.offset == 0 && table.type == &type); + Require(table.destroy == runtimeTable.destroy && table.deleteObject == runtimeTable.deleteObject && table.what == runtimeTable.what); + Object object{}; + _ZNSt13runtime_errorC1EPKc_nid_postfix(&object, "value too large"); + Require(object.message != nullptr && std::strcmp(object.message, "value too large") == 0); + _ZNSt14overflow_errorD1Ev_nid_postfix(&object); + Require(object.message == nullptr); + _ZNSt14overflow_errorD1Ev_nid_postfix(&object); + Require(object.message == nullptr); + return 0; +} diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index e8696ac012..461ef23b7b 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -1,10 +1,17 @@ #include "prx/libScePad/include/Pad.hpp" #include "prx/libScePad/include/PadInputTypes.hpp" #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*); @@ -14,16 +21,90 @@ 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); 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*); +int APS5_VABI scePadGetControllerInformation(int, PadControllerInformation*); +int APS5_VABI scePadGetExtControllerInformation(int, void*); } static void Require(bool value) { if (!value) std::abort(); } +static void CheckInputCommands() { + const auto path = std::filesystem::temp_directory_path() / ("anyps5-pad-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + struct Cleanup { + std::filesystem::path path; + ~Cleanup() { std::error_code error; std::filesystem::remove(path, error); } + } cleanup{path}; + const auto write = [&](const char* command) { std::ofstream(path) << command << '\n'; }; + write("1 16384 200"); + Pad::InputCommandFile commands(path); + Require(commands.Buttons(1000) == 0); + write("2 16384 200"); + Require(commands.Buttons(101000) == 0x4000); + Require(commands.Buttons(300999) == 0x4000); + Require(commands.Buttons(301000) == 0); + Require(commands.Buttons(401000) == 0); + write("3 8192 5001"); + Require(commands.Buttons(501000) == 0); + write("-3 8192 300"); + Require(commands.Buttons(601000) == 0); + write("3 1 300"); + Require(commands.Buttons(701000) == 0); + write("3 8192 300 trailing"); + Require(commands.Buttons(801000) == 0); + write("3 8192 300"); + Require(commands.Buttons(901000) == 0x2000); + write("4 0 1"); + Require(commands.Buttons(1001000) == 0); + write("2 16384 200"); + Require(commands.Buttons(1101000) == 0); + write("5 0 300 0 128 255 64"); + const auto tilted = commands.Poll(1201000); + Require(tilted.buttons == 0 && tilted.hasSticks); + Require(tilted.sticks[0] == 0 && tilted.sticks[1] == 128 && tilted.sticks[2] == 255 && tilted.sticks[3] == 64); + Require(!commands.Poll(1501000).hasSticks); + write("6 16384 300"); + const auto pressed = commands.Poll(1601000); + Require(pressed.buttons == 0x4000 && !pressed.hasSticks); + const Pad::InputCommandFile::Input none; + write("7 16384 300 128 128"); + Require(commands.Poll(1901000) == none); + write("7 16384 300 128 128 128 256"); + Require(commands.Poll(2001000) == none); + write("7 16384 300 128 128 128 128 128"); + Require(commands.Poll(2101000) == none); + write("7 16384 300 128 128 128 up"); + Require(commands.Poll(2201000) == none); + write("7 16384 300 +"); + Require(commands.Poll(2301000) == none); + write("7 16384 300 4294967296"); + Require(commands.Poll(2401000) == none); + write("7 16384 300 128 128 128 128x"); + Require(commands.Poll(2501000) == none); + 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(4201000) == 0); + Pad::InputCommandFile disabled(std::filesystem::path{}); + Require(disabled.Buttons(0) == 0); +} + static float SettleOrientationW() { float w = 1.0f; for (int i = 0; i < 10; ++i) { @@ -34,7 +115,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}; @@ -48,6 +128,24 @@ static void CheckTiltCorrection(int handle) { PadPublishInput_nid_postfix(PadInputState{}); } +static void CheckAngularVelocityDeadband(int handle) { + PadInputState moving; + moving.hasMotion = true; + moving.accel = {0.0f, 9.80665f, 0.0f}; + moving.gyro = {0.01f, 0.0175f, -0.017f}; + PadPublishInput_nid_postfix(moving); + PadData data{}; + Require(scePadReadState(handle, &data) == PAD_OK); + Require(data.angular_velocity_x == 0.01f && data.angular_velocity_y == 0.0175f && data.angular_velocity_z == -0.017f); + Require(scePadSetAngularVelocityDeadbandState(handle, true) == PAD_OK); + Require(scePadReadState(handle, &data) == PAD_OK); + Require(data.angular_velocity_x == 0.0f && data.angular_velocity_y == 0.0175f && data.angular_velocity_z == 0.0f); + Require(scePadSetAngularVelocityDeadbandState(handle, false) == PAD_OK); + Require(scePadReadState(handle, &data) == PAD_OK); + Require(data.angular_velocity_x == 0.01f && data.angular_velocity_z == -0.017f); + PadPublishInput_nid_postfix(PadInputState{}); +} + static void CheckTouchContact() { PadInputState touch; touch.buttons = static_cast(Pad::PadButton::TouchPad); @@ -62,12 +160,137 @@ 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); + partial.analogButtonsL2 = 1; + partial.analogButtonsR2 = 73; + PadPublishInput_nid_postfix(partial); + const auto data = Pad::ReadState(); + Require(data.buttons == partial.buttons); + Require(data.analog_buttons_l2 == 1 && data.analog_buttons_r2 == 73); + PadInputState digital; + digital.buttons = static_cast(Pad::PadButton::L2); + PadPublishInput_nid_postfix(digital); + Require(Pad::ReadState().analog_buttons_l2 == 255); + PadPublishInput_nid_postfix(PadInputState{}); + Pad::ReadState(); +} + +static void CheckReadQueue(int handle) { + PadData drained[64]{}; + scePadRead_nid_postfix(handle, drained, 64); + PadInputState pressed; + pressed.buttons = static_cast(Pad::PadButton::Cross); + PadPublishInput_nid_postfix(pressed); + PadPublishInput_nid_postfix(PadInputState{}); + PadData data[4]{}; + Require(scePadRead_nid_postfix(handle, data, 4) == 2); + Require((data[0].buttons & pressed.buttons) != 0); + Require((data[1].buttons & pressed.buttons) == 0); + Require(data[0].timestamp <= data[1].timestamp); + Require(scePadRead_nid_postfix(handle, data, 4) == 1); + 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) { @@ -79,7 +302,115 @@ static void CheckRemoteController(int handle) { Require(!remote); } +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; + physical.analogButtonsR2 = 73; + physical.sticks[0] = 42; + PadPublishInput_nid_postfix(physical); + const auto before = Pad::ReadState(); + std::ofstream(path) << "1 16640 300\n"; + PadData pressed{}; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + do { + pressed = Pad::ReadState(); + if ((pressed.buttons & 0x4000) != 0) break; + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } while (std::chrono::steady_clock::now() < deadline); + Require(pressed.buttons == (0x2000 | 0x4000 | 0x100)); + Require(pressed.analog_buttons_l2 == 255 && pressed.analog_buttons_r2 == 73); + Require(pressed.left_stick_x == 42 && pressed.timestamp > before.timestamp); + std::this_thread::sleep_for(std::chrono::milliseconds(310)); + const auto released = Pad::ReadState(); + Require(released.buttons == 0x2000 && released.analog_buttons_l2 == 0); + Require(released.timestamp > pressed.timestamp); + physical.sticks[3] = 17; + PadPublishInput_nid_postfix(physical); + const auto still = Pad::ReadState(); + std::ofstream(path) << "2 0 300 200 0 128 128\n"; + PadData moved{}; + const auto stickDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + do { + moved = Pad::ReadState(); + if (moved.left_stick_x == 200) break; + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } while (std::chrono::steady_clock::now() < stickDeadline); + Require(moved.left_stick_x == 200 && moved.left_stick_y == 0); + Require(moved.right_stick_x == 128 && moved.right_stick_y == 128); + Require(moved.buttons == 0x2000 && moved.timestamp > still.timestamp); + std::this_thread::sleep_for(std::chrono::milliseconds(310)); + 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); } + }; + auto commandPath = path; + commandPath += std::filesystem::path(u8"-žluťoučký-漢字"); + Cleanup cleanup{commandPath}; +#ifdef _WIN32 + Require(_wputenv_s(L"APS5_PAD_INPUT_FILE", commandPath.c_str()) == 0); +#else + 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; @@ -92,24 +423,47 @@ int main() { Require(scePadOpenExt(user, PAD_PORT_TYPE_SPECIAL, 1, wheel) == PAD_ERROR_INVALID_ARG); Require(scePadOpenExt(user, PAD_PORT_TYPE_SPECIAL, 0, nullptr) == PAD_ERROR_INVALID_ARG); 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); Require(scePadGetHandle(user, 0, 0) == handle); - Require(scePadGetHandle(user, 2, 0) == handle); CheckTiltCorrection(handle); + CheckAngularVelocityDeadband(handle); CheckTouchContact(); + CheckVirtualTouchContact(); + CheckTouchPadInformation(handle); + CheckPartialTrigger(); + CheckReadQueue(handle); + CheckTapBetweenReads(); + CheckLatchedPressLifetime(handle); CheckReadStateHandle(handle); CheckRemoteController(handle); + CheckAccessibilityControllerState(handle); + CheckInjectedState(commandPath); Require(scePadSetVibrationMode(handle, 1) == 0); Require(scePadSetVibrationMode(handle, 2) == 0); - Require(scePadSetVibrationMode(handle, 3) == PAD_ERROR_INVALID_ARG); + 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); + Require(scePadSetAngularVelocityDeadbandState(handle + 1, true) == PAD_ERROR_INVALID_HANDLE); + Require(scePadSetAngularVelocityBiasCorrectionState(handle, false) == PAD_OK); + Require(scePadSetAngularVelocityBiasCorrectionState(handle + 1, false) == PAD_ERROR_INVALID_HANDLE); } diff --git a/core/libs/tests/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 new file mode 100644 index 0000000000..eba38dd12c --- /dev/null +++ b/core/libs/tests/GuestPageTableStats.cpp @@ -0,0 +1,120 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/KernelErrors.hpp" + +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceKernelGetPageTableStats(int*, int*, int*, int*); +int APS5_VABI sceKernelMapFlexibleMemory(void**, std::size_t, int, int); +int APS5_VABI sceKernelMapDirectMemory(void**, std::size_t, int, int, std::int64_t, std::size_t); +int APS5_VABI sceKernelAllocateDirectMemory(std::int64_t, std::int64_t, std::size_t, std::size_t, int, std::int64_t*); +int APS5_VABI sceKernelReleaseDirectMemory(std::int64_t, std::size_t); +int APS5_VABI sceKernelReserveVirtualRange(void**, std::size_t, int, std::size_t); +int APS5_VABI sceKernelMprotect(const void*, std::size_t, int); +int APS5_VABI sceKernelMunmap(void*, std::size_t); +} + +static void Require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "Page table stats check failed at %s:%u\n", location.file_name(), static_cast(location.line())); + std::abort(); + } +} + +constexpr std::size_t PageTableGranule = 0x200000; +constexpr std::size_t GpuDirectorySpan = 0x40000000; +constexpr int PageTableTotal = 8192; + +struct Usage { + int cpu; + int gpu; +}; + +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 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() { + int value = 0; + Require(sceKernelGetPageTableStats(nullptr, &value, &value, &value) == SCE_KERNEL_ERROR_EFAULT); + Require(sceKernelGetPageTableStats(&value, nullptr, &value, &value) == SCE_KERNEL_ERROR_EFAULT); + Require(sceKernelGetPageTableStats(&value, &value, nullptr, &value) == SCE_KERNEL_ERROR_EFAULT); + Require(sceKernelGetPageTableStats(&value, &value, &value, nullptr) == SCE_KERNEL_ERROR_EFAULT); +} + +static void 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 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() { + auto* base = Reserve(); + const auto before = Used(); + std::int64_t phys = 0; + 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(); + CheckCpuGranules(); + CheckGpuEntries(); + CheckGpuDirectMapping(); + return 0; +} diff --git a/core/libs/tests/GuestParentProcess.cpp b/core/libs/tests/GuestParentProcess.cpp new file mode 100644 index 0000000000..203cfe3a00 --- /dev/null +++ b/core/libs/tests/GuestParentProcess.cpp @@ -0,0 +1,35 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#ifdef _WIN32 +#include +#else +#include +#endif +extern "C" { +int APS5_VABI getpid_nid_postfix(void); +int APS5_VABI getppid_nid_postfix(void); +int APS5_VABI getsid_nid_postfix(int); +int* APS5_VABI __error_nid_postfix(); +} +static void Require(bool value) { if (!value) std::abort(); } +int main() { + const int self = getpid_nid_postfix(); + Require(getsid_nid_postfix(0) == self); + Require(getsid_nid_postfix(self) == self); + *__error_nid_postfix() = 0; + Require(getsid_nid_postfix(123456) == -1); + Require(*__error_nid_postfix() == 3); + Require(getsid_nid_postfix(-1) == -1); + Require(*__error_nid_postfix() == 3); + const int parent = getppid_nid_postfix(); + Require(parent > 0 && parent != getpid_nid_postfix()); + Require(getppid_nid_postfix() == parent); +#ifdef _WIN32 + const HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, static_cast(parent)); + Require(process != nullptr); + CloseHandle(process); +#else + Require(parent == static_cast(::getppid())); +#endif + 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 84ee10f08f..590e20232f 100644 --- a/core/libs/tests/GuestPlayGo.cpp +++ b/core/libs/tests/GuestPlayGo.cpp @@ -2,24 +2,94 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include +#include +#include +#include +#include +#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 scePlayGoGetSupportedOptionalChunk(int, std::int32_t, PlayGoOptionalChunk*); +int APS5_VABI scePlayGoGetOptionalChunk(int, std::int32_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(); } -int main() { +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); + std::filesystem::current_path(root); + PlayGoInitParams init{}; + int handle = 0; + Require(scePlayGoInitialize(&init) == 0); + Require(scePlayGoOpen(&handle, nullptr) == 0); + const std::uint16_t known[3] = {0, 1, 2}; + std::int8_t loci[3] = {0, 0, 0}; + Require(scePlayGoGetLocus(handle, known, 3, loci) == 0 && loci[0] == 3 && loci[1] == 3 && loci[2] == 3); + const std::uint16_t outside[2] = {1, 3}; + Require(scePlayGoGetLocus(handle, outside, 2, loci) == badChunkId); + std::uint32_t chunkCount = 0; + Require(scePlayGoGetChunkId(handle, nullptr, 0, &chunkCount) == 0 && chunkCount == 3); + Require(scePlayGoClose(handle) == 0); + return 0; +} + +static int CheckScenarioWithoutChunkTable(const char* root) { + std::filesystem::current_path(root); + PlayGoInitParams init{}; + int handle = 0; + Require(scePlayGoInitialize(&init) == 0); + Require(scePlayGoOpen(&handle, nullptr) == 0); + const std::uint16_t anyIds[3] = {1, 10, 0xFFFF}; + std::int8_t loci[3] = {0, 0, 0}; + Require(scePlayGoGetLocus(handle, anyIds, 3, loci) == 0 && loci[0] == 3 && loci[1] == 3 && loci[2] == 3); + const PlayGoToDo todo[2] = {{0, 3, 0}, {0xFFFF, 3, 0}}; + Require(scePlayGoSetToDoList(handle, todo, 2) == 0); + std::uint32_t chunkCount = 0; + Require(scePlayGoGetChunkId(handle, nullptr, 0, &chunkCount) == 0 && chunkCount == 1000); + std::vector ids(chunkCount + 1, 0xFFFF); + Require(scePlayGoGetChunkId(handle, ids.data(), static_cast(ids.size()), &chunkCount) == 0 && chunkCount == 1000); + for (std::uint32_t index = 0; index < chunkCount; ++index) Require(ids[index] == index); + 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; +} + +static int CheckNoPlayGoFile(const char* root) { + std::filesystem::current_path(root); constexpr int badHandle = static_cast(0x80B20009); constexpr int badPointer = static_cast(0x80B2000A); constexpr int badSize = static_cast(0x80B2000B); - constexpr int badChunkId = static_cast(0x80B2000C); constexpr int badLocus = static_cast(0x80B20010); PlayGoInitParams init{}; @@ -34,6 +104,13 @@ int main() { Require(scePlayGoGetLanguageMask(handle, nullptr) == badPointer); 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); + } + PlayGoToDo todo[2] = {{0, 3, 0}, {0, 0, 0}}; Require(scePlayGoSetToDoList(handle + 1, todo, 2) == badHandle); Require(scePlayGoSetToDoList(handle, nullptr, 2) == badPointer); @@ -41,8 +118,10 @@ int main() { Require(scePlayGoSetToDoList(handle, todo, 2) == 0); todo[1].locus = 1; Require(scePlayGoSetToDoList(handle, todo, 2) == badLocus); + todo[1] = {1, 3, 0}; + 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); @@ -50,6 +129,129 @@ int main() { std::int8_t locus = 0; Require(scePlayGoGetLocus(handle, &chunk, 0, &locus) == badSize); Require(scePlayGoGetLocus(handle, &chunk, 1, &locus) == 0 && locus == 3); + const std::uint16_t undeclared[2] = {1, 10}; + std::int8_t loci[2] = {0, 0}; + 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; + Require(scePlayGoGetChunkId(handle, nullptr, 0, &chunkCount) == 0 && chunkCount == 1); Require(scePlayGoClose(handle + 1) == badHandle); Require(scePlayGoClose(handle) == 0); + return 0; +} + +static int CheckRejectedLayout(const char* root, const char* fileName) { + std::filesystem::current_path(root); + PlayGoInitParams init{}; + int handle = 0; + Require(scePlayGoInitialize(&init) == 0); + Require(scePlayGoOpen(&handle, nullptr) == 0); + const std::uint16_t chunk = 0; + std::int8_t locus = 0; + bool threw = false; + try { + scePlayGoGetLocus(handle, &chunk, 1, &locus); + } catch (const std::runtime_error& error) { + threw = std::string_view(error.what()).find(fileName) != std::string_view::npos; + } + Require(threw); + return 0; +} + +static void 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); + std::filesystem::create_directories(root / "app0" / "sce_sys"); + for (const auto& [name, content] : files) std::ofstream(root / "app0" / "sce_sys" / name, std::ios::binary).write(content.data(), static_cast(content.size())); + const std::string arguments = "\"" + std::string(self) + "\" " + mode + " \"" + root.string() + "\""; +#ifdef _WIN32 + const std::string command = "\"" + arguments + "\""; +#else + const std::string command = arguments; +#endif + const int status = std::system(command.c_str()); + std::filesystem::remove_all(root); + Require(status == 0); +} + +static std::string ChunkTableFile(const char* magic = "plgx", char chunks = 3) { + std::string header(0x100, '\0'); + header.replace(0, 4, magic); + header[0x0A] = chunks; + return header; +} + +int main(int argc, char** argv) { + if (argc == 3) { + const std::string_view mode = argv[1]; + if (mode == "--no-file") return CheckNoPlayGoFile(argv[2]); + if (mode == "--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"); + if (mode == "--hash-table-only") return CheckRejectedLayout(argv[2], "playgo-hash-table.dat"); + if (mode == "--invalid-chunk-table") return CheckRejectedLayout(argv[2], "playgo-chunk.dat"); + } + RunLayout(argv[0], "--no-file", {}); + RunLayout(argv[0], "--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..470cfee79d 100644 --- a/core/libs/tests/GuestPlayGoDialog.cpp +++ b/core/libs/tests/GuestPlayGoDialog.cpp @@ -21,9 +21,11 @@ int main() { 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 notFinished = static_cast(0x80B80005u); Require(scePlayGoDialogGetStatus() == 0); Require(scePlayGoDialogTerminate() == notInitialized); + Require(scePlayGoDialogClose() == notInitialized); Require(scePlayGoDialogOpen(nullptr) == paramInvalid); Require(scePlayGoDialogGetResult(nullptr) == notInitialized); @@ -36,6 +38,9 @@ int main() { Require(scePlayGoDialogOpen(nullptr) == paramInvalid); Require(scePlayGoDialogUpdateStatus() == 1); + PlayGoDialogResult unfinishedResult{}; + Require(scePlayGoDialogGetResult(&unfinishedResult) == notFinished); + Require(scePlayGoDialogOpen(&openParam) == 0); Require(scePlayGoDialogGetStatus() == 3); Require(scePlayGoDialogUpdateStatus() == 3); diff --git a/core/libs/tests/GuestPlayerReviewDialog.cpp b/core/libs/tests/GuestPlayerReviewDialog.cpp index 7093323415..c7917268a4 100644 --- a/core/libs/tests/GuestPlayerReviewDialog.cpp +++ b/core/libs/tests/GuestPlayerReviewDialog.cpp @@ -18,6 +18,7 @@ constexpr int COMMON_DIALOG_STATUS_NONE = 0; constexpr int COMMON_DIALOG_STATUS_INITIALIZED = 1; constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; +constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004u); constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); @@ -29,19 +30,23 @@ void Require(bool value) { if (!value) std::abort(); } int main() { std::uint8_t param[64] = {}; std::int32_t result[8] = {}; + Require(scePlayerReviewDialogTerminate() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); Require(scePlayerReviewDialogOpen(param) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(scePlayerReviewDialogClose() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(scePlayerReviewDialogGetResult(result) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(scePlayerReviewDialogInitialize() == 0); + Require(scePlayerReviewDialogInitialize() == COMMON_DIALOG_ERROR_ALREADY_INITIALIZED); Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_INITIALIZED); Require(scePlayerReviewDialogUpdateStatus() == COMMON_DIALOG_STATUS_INITIALIZED); Require(scePlayerReviewDialogGetResult(result) == COMMON_DIALOG_ERROR_NOT_FINISHED); Require(scePlayerReviewDialogTerminate() == 0); + Require(scePlayerReviewDialogTerminate() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); Require(scePlayerReviewDialogInitialize() == 0); + Require(scePlayerReviewDialogUpdateStatus() == COMMON_DIALOG_STATUS_INITIALIZED); Require(scePlayerReviewDialogOpen(nullptr) == COMMON_DIALOG_ERROR_ARG_NULL); Require(scePlayerReviewDialogOpen(param) == 0); Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_FINISHED); @@ -50,8 +55,18 @@ int main() { Require(scePlayerReviewDialogGetResult(result) == 0); Require(result[0] == COMMON_DIALOG_RESULT_USER_CANCELED); Require(scePlayerReviewDialogClose() == 0); + 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() == 0); + Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_FINISHED); + Require(scePlayerReviewDialogUpdateStatus() == COMMON_DIALOG_STATUS_FINISHED); + result[0] = -1; + Require(scePlayerReviewDialogGetResult(result) == 0); + Require(result[0] == COMMON_DIALOG_RESULT_USER_CANCELED); Require(scePlayerReviewDialogTerminate() == 0); + Require(scePlayerReviewDialogUpdateStatus() == COMMON_DIALOG_STATUS_NONE); } diff --git a/core/libs/tests/GuestPlayerSelectionDialog.cpp b/core/libs/tests/GuestPlayerSelectionDialog.cpp new file mode 100644 index 0000000000..8814165e4f --- /dev/null +++ b/core/libs/tests/GuestPlayerSelectionDialog.cpp @@ -0,0 +1,72 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI scePlayerSelectionDialogInitialize(void); +int APS5_VABI scePlayerSelectionDialogOpen(const void* param); +int APS5_VABI scePlayerSelectionDialogUpdateStatus(void); +int APS5_VABI scePlayerSelectionDialogGetStatus(void); +int APS5_VABI scePlayerSelectionDialogGetResult(void* result); +int APS5_VABI 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() { + std::int64_t param = 0; + std::int32_t result[2] = {-1, -1}; + + 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 == 0); + Require(scePlayerSelectionDialogGetResult(result) == kErrNotInitialized); + Require(scePlayerSelectionDialogGetStatus() == 0); + Require(scePlayerSelectionDialogUpdateStatus() == 0); + + Require(scePlayerSelectionDialogInitialize() == 0); + Require(scePlayerSelectionDialogInitialize() == kErrAlreadyInitialized); + Require(scePlayerSelectionDialogGetStatus() == 1); + Require(scePlayerSelectionDialogUpdateStatus() == 1); + + Require(scePlayerSelectionDialogOpen(nullptr) == kErrArgNull); + Require(scePlayerSelectionDialogGetResult(result) == kErrNotFinished); + + Require(scePlayerSelectionDialogOpen(¶m) == 0); + Require(scePlayerSelectionDialogGetStatus() == kStatusFinished); + Require(scePlayerSelectionDialogUpdateStatus() == kStatusFinished); + + Require(scePlayerSelectionDialogGetResult(nullptr) == kErrArgNull); + Require(scePlayerSelectionDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(result[1] == -1); + + Require(scePlayerSelectionDialogClose() == 0); + Require(scePlayerSelectionDialogTerminate() == 0); + Require(scePlayerSelectionDialogTerminate() == kErrNotInitialized); + Require(scePlayerSelectionDialogGetStatus() == 0); + Require(scePlayerSelectionDialogClose() == kErrNotInitialized); +} diff --git a/core/libs/tests/GuestPngDec.cpp b/core/libs/tests/GuestPngDec.cpp index d75515f8c6..83281c1c1c 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(); @@ -76,19 +83,25 @@ int main() { const std::int32_t memorySize = scePngDecQueryMemorySize(¶m); Require(memorySize > 0); Require(scePngDecQueryMemorySize(nullptr) == invalidParam); + const PngDecCreateParam badSize{sizeof(PngDecCreateParam) - 1, 0, 1920}; + Require(scePngDecQueryMemorySize(&badSize) == invalidSize); const PngDecCreateParam badAttribute{sizeof(PngDecCreateParam), 2, 1920}; Require(scePngDecQueryMemorySize(&badAttribute) == invalidParam); const PngDecCreateParam zeroWidth{sizeof(PngDecCreateParam), 0, 0}; - 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(&hugeWidth) == invalidParam); + const PngDecCreateParam overWidth{sizeof(PngDecCreateParam), 1, 1000001}; + Require(scePngDecQueryMemorySize(&overWidth) == invalidParam); + const PngDecCreateParam maxWidth{sizeof(PngDecCreateParam), 1, 1000000}; Require(scePngDecQueryMemorySize(&maxWidth) == memorySize); 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(&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); @@ -105,6 +118,13 @@ int main() { const PngDecParseParam parsePalette{PALETTE_TRNS, sizeof(PALETTE_TRNS), 0}; Require(scePngDecParseHeader(&parsePalette, &info) == 0); Require(info.color_space == 4 && info.bit_depth == 2 && info.image_flag == 2); + for (const std::uint32_t size : {69u, 71u}) { + const PngDecParseParam truncatedPalette{PALETTE_TRNS, size, 0}; + Require(scePngDecParseHeader(&truncatedPalette, &info) == 0); + Require(info.color_space == 4 && info.bit_depth == 2 && info.image_flag == 0); + } + const PngDecParseParam completeTransparency{PALETTE_TRNS, 73, 0}; + Require(scePngDecParseHeader(&completeTransparency, &info) == 0 && info.image_flag == 2); Require(scePngDecParseHeader(nullptr, &info) == invalidParam); Require(scePngDecParseHeader(&parse, nullptr) == invalidAddr); const PngDecParseParam parseNull{nullptr, 16, 0}; @@ -125,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; @@ -141,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); @@ -163,12 +219,26 @@ int main() { 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); diff --git a/core/libs/tests/GuestPngEnc.cpp b/core/libs/tests/GuestPngEnc.cpp index 7535dae853..7984c2fff5 100644 --- a/core/libs/tests/GuestPngEnc.cpp +++ b/core/libs/tests/GuestPngEnc.cpp @@ -68,7 +68,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 +84,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()); } } diff --git a/core/libs/tests/GuestPoll.cpp b/core/libs/tests/GuestPoll.cpp index a32abd9a69..131e75eb9d 100644 --- a/core/libs/tests/GuestPoll.cpp +++ b/core/libs/tests/GuestPoll.cpp @@ -1,8 +1,21 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#else +#include +#include +#include +#endif #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include #include +#include +#include +#include +#include struct PollDescriptor { int descriptor; short events; @@ -18,15 +31,142 @@ int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); int APS5_VABI close_nid_postfix(int); +int APS5_VABI open_nid_postfix(const char*, int, int); +int APS5_VABI 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, ...); int* APS5_VABI __error_nid_postfix(); } +#ifdef _WIN32 +using NativeSocket = SOCKET; +constexpr NativeSocket NativeInvalid = INVALID_SOCKET; +static void CloseNative(NativeSocket socket) { closesocket(socket); } +#else +using NativeSocket = int; +constexpr NativeSocket NativeInvalid = -1; +static void CloseNative(NativeSocket socket) { close(socket); } +#endif static void Require(bool value) { if (!value) std::abort(); } constexpr short In = 0x1; +constexpr short Priority = 0x2; +constexpr short ReadBand = 0x80; constexpr short Out = 0x4; constexpr short Hup = 0x10; constexpr short Invalid = 0x20; -int main() { +constexpr short ReadNormal = 0x40; +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); + PollDescriptor empty[2]{{ends[0], In, -1}, {ends[1], Out, -1}}; + Require(poll_nid_postfix(empty, 2, 0) == 1 && empty[0].revents == 0 && empty[1].revents == Out); + PollDescriptor reader{ends[0], In | ReadNormal, -1}; + const auto start = std::chrono::steady_clock::now(); + Require(poll_nid_postfix(&reader, 1, 50) == 0 && reader.revents == 0); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(40)); + 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}; + Require(poll_nid_postfix(&writer, 1, 0) == 0 && writer.revents == 0); + PollDescriptor readerOut{ends[0], Out, -1}; + bool threw = false; + try { poll_nid_postfix(&readerOut, 1, 0); } catch (const std::runtime_error&) { threw = true; } + Require(threw); + PollDescriptor mixed[2]{{socket, In, -1}, {ends[0], In, -1}}; + Require(poll_nid_postfix(mixed, 2, 1000) == 1 && mixed[0].revents == 0 && mixed[1].revents == In); + char buffer[8]{}; + Require(read_nid_postfix(ends[0], buffer, sizeof(buffer)) == 4); + Require(poll_nid_postfix(mixed, 2, 0) == 0 && mixed[1].revents == 0); + std::thread late([&] { + std::this_thread::sleep_for(std::chrono::milliseconds(30)); + Require(write_nid_postfix(ends[1], "x", 1) == 1); + }); + Require(poll_nid_postfix(mixed, 2, 2000) == 1 && mixed[1].revents == In); + late.join(); + std::thread lateInfinite([&] { + std::this_thread::sleep_for(std::chrono::milliseconds(30)); + Require(write_nid_postfix(ends[1], "y", 1) == 1); + }); + Require(read_nid_postfix(ends[0], buffer, sizeof(buffer)) == 1); + const auto waitStart = std::chrono::steady_clock::now(); + Require(poll_nid_postfix(&reader, 1, -1) == 1 && reader.revents == (In | ReadNormal)); + Require(std::chrono::steady_clock::now() - waitStart >= std::chrono::milliseconds(20)); + lateInfinite.join(); + Require(read_nid_postfix(ends[0], buffer, sizeof(buffer)) == 1); + Require(write_nid_postfix(ends[1], "z", 1) == 1); + Require(close_nid_postfix(ends[1]) == 0); + Require(poll_nid_postfix(&reader, 1, 0) == 1 && reader.revents == (In | ReadNormal | Hup)); + Require(read_nid_postfix(ends[0], buffer, sizeof(buffer)) == 1); + Require(poll_nid_postfix(&reader, 1, 0) == 1 && reader.revents == (In | ReadNormal | Hup)); + Require(read_nid_postfix(ends[0], buffer, sizeof(buffer)) == 0); + PollDescriptor closedPipe{ends[1], In, -1}; + Require(poll_nid_postfix(&closedPipe, 1, 0) == 1 && closedPipe.revents == Invalid); + Require(close_nid_postfix(ends[0]) == 0); + int full[2]; + Require(pipe_nid_postfix(full) == 0); + static char block[4096]; + std::int64_t queued = 0; + PollDescriptor fill{full[1], Out, -1}; + while (poll_nid_postfix(&fill, 1, 0) == 1) { + Require(fill.revents == Out && queued < (1 << 20)); + Require(write_nid_postfix(full[1], block, 256) == 256); + queued += 256; + } + Require(queued > 0 && fill.revents == 0); + while (queued > 0) { + const std::int64_t taken = read_nid_postfix(full[0], block, static_cast(std::min(queued, sizeof(block)))); + Require(taken > 0); + queued -= taken; + } + Require(poll_nid_postfix(&fill, 1, 0) == 1 && fill.revents == Out); + Require(close_nid_postfix(full[0]) == 0); + Require(close_nid_postfix(full[1]) == 0); + int other[2]; + Require(pipe_nid_postfix(other) == 0); + Require(close_nid_postfix(other[0]) == 0); + PollDescriptor orphan{other[1], Out, -1}; + Require(poll_nid_postfix(&orphan, 1, 0) == 1 && orphan.revents == Hup); + orphan.events = In | Out; + Require(poll_nid_postfix(&orphan, 1, 0) == 1 && orphan.revents == (In | Hup)); + Require(close_nid_postfix(other[1]) == 0); +} +static void PollsUrgentData() { const int listener = socket_nid_postfix(2, 1, 0); Require(listener >= 0); std::array address{16, 2, 0, 0, 127, 0, 0, 1}; @@ -34,6 +174,53 @@ int main() { std::uint32_t size = address.size(); Require(getsockname_nid_postfix(listener, address.data(), &size) == 0); Require(listen_nid_postfix(listener, 4) == 0); + const int client = socket_nid_postfix(2, 1, 0); + Require(client >= 0 && fcntl_nid_postfix(client, 4, 4) == 0); + if (connect_nid_postfix(client, address.data(), address.size()) != 0) + Require(*__error_nid_postfix() == 35 || *__error_nid_postfix() == 36); + PollDescriptor connecting{client, Priority | Out, -1}; + Require(poll_nid_postfix(&connecting, 1, 1000) == 1 && connecting.revents == Out); + const int server = accept_nid_postfix(listener, nullptr, nullptr); + Require(server >= 0); + PollDescriptor pair[2]{{server, Priority | Out, -1}, {client, In | Priority | Out | ReadBand, -1}}; + Require(poll_nid_postfix(pair, 2, 0) == 2 && pair[0].revents == Out && pair[1].revents == Out); + char byte = 'u'; + Require(send_nid_postfix(client, &byte, 1, 1) == -1 && *__error_nid_postfix() == 45); + Require(recv_nid_postfix(server, &byte, 1, 1) == -1 && *__error_nid_postfix() == 45); + const NativeSocket peer = ::socket(AF_INET, SOCK_STREAM, 0); + Require(peer != NativeInvalid); + sockaddr_in native{}; + native.sin_family = AF_INET; + std::memcpy(&native.sin_port, address.data() + 2, 2); + std::memcpy(&native.sin_addr, address.data() + 4, 4); + Require(::connect(peer, reinterpret_cast(&native), sizeof(native)) == 0); + const int urgent = accept_nid_postfix(listener, nullptr, nullptr); + Require(urgent >= 0); + PollDescriptor waiting{urgent, Priority, -1}; + Require(poll_nid_postfix(&waiting, 1, 0) == 0 && waiting.revents == 0); + Require(::send(peer, "u", 1, MSG_OOB) == 1); + Require(poll_nid_postfix(&waiting, 1, 1000) == 1 && waiting.revents == Priority); + PollDescriptor bands{urgent, In | Priority | Out, -1}; + Require(poll_nid_postfix(&bands, 1, 0) == 1 && bands.revents == (Priority | Out)); +#ifdef _WIN32 + PollDescriptor band{urgent, ReadBand, -1}; + Require(poll_nid_postfix(&band, 1, 0) == 1 && band.revents == ReadBand); +#endif + CloseNative(peer); + Require(close_nid_postfix(urgent) == 0); + Require(close_nid_postfix(server) == 0); + Require(close_nid_postfix(client) == 0); + Require(close_nid_postfix(listener) == 0); +} +int main() { + PollsUrgentData(); + const int listener = socket_nid_postfix(2, 1, 0); + Require(listener >= 0 && listener < 1024); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(listener, address.data(), address.size()) == 0); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(listener, address.data(), &size) == 0); + Require(listen_nid_postfix(listener, 4) == 0); PollDescriptor single{listener, In, -1}; Require(poll_nid_postfix(&single, 1, 0) == 0 && single.revents == 0); const auto start = std::chrono::steady_clock::now(); @@ -59,8 +246,45 @@ int main() { PollDescriptor closed[2]{{client, In, 0}, {listener, In, 0}}; Require(poll_nid_postfix(closed, 2, -1) == 1 && closed[0].revents == Invalid && closed[1].revents == 0); Require(poll_nid_postfix(nullptr, 0, 10) == 0); + const auto shortWaitStart = std::chrono::steady_clock::now(); + for (int i = 0; i < 20; ++i) Require(poll_nid_postfix(nullptr, 0, 1) == 0); + const auto shortWait = std::chrono::steady_clock::now() - shortWaitStart; + Require(shortWait >= std::chrono::milliseconds(20) && shortWait < std::chrono::milliseconds(200)); Require(poll_nid_postfix(nullptr, 1, 0) == -1 && *__error_nid_postfix() == 14); Require(poll_nid_postfix(&single, 1, -2) == -1 && *__error_nid_postfix() == 22); + TestPipes(listener); + PollsFilesAndDirectories(listener); Require(close_nid_postfix(server) == 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(unknown, 2, 0); + } catch (const std::runtime_error&) { + unsupported = true; + } + Require(unsupported); + 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/GuestPositionedRace.cpp b/core/libs/tests/GuestPositionedRace.cpp new file mode 100644 index 0000000000..6c936c56b4 --- /dev/null +++ b/core/libs/tests/GuestPositionedRace.cpp @@ -0,0 +1,77 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +extern "C" { +int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode); +int APS5_VABI sceKernelClose(int d); +std::int64_t APS5_VABI sceKernelRead(int d, void* buf, std::size_t nbytes); +std::int64_t APS5_VABI sceKernelPread(int d, void* buf, std::size_t nbytes, std::int64_t offset); +std::int64_t APS5_VABI sceKernelLseek(int d, std::int64_t offset, int whence); +} +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Positioned race check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) +int main() { + const auto root = std::filesystem::path("anyps5-positioned-race-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + const auto file = root / "blocks.bin"; + constexpr std::int64_t kBlocks = 8; + constexpr std::size_t kBlockSize = 4096; + { + std::ofstream stream(file, std::ios::binary); + Require(static_cast(stream)); + for (std::int64_t block = 0; block < kBlocks; ++block) { + stream << std::string(kBlockSize, static_cast('A' + block)); + } + } + const int fd = sceKernelOpen(file.string().c_str(), SCE_KERNEL_O_RDONLY, 0); + Require(fd >= 0); + std::array expected{}; + expected.fill('A'); + std::atomic stop{false}; + std::atomic misread{false}; + std::array seeked{}; + seeked.fill('H'); + std::thread seeker([&] { + std::array sink{}; + while (!stop.load()) { + if (sceKernelLseek(fd, (kBlocks - 1) * kBlockSize, 0) != (kBlocks - 1) * kBlockSize) { + misread = true; + return; + } + if (sceKernelRead(fd, sink.data(), sink.size()) != static_cast(sink.size()) || + std::memcmp(sink.data(), seeked.data(), seeked.size()) != 0) { + misread = true; + return; + } + } + }); + for (int i = 0; i < 5000 && !misread.load(); ++i) { + std::array bytes{}; + const auto got = sceKernelPread(fd, bytes.data(), bytes.size(), 0); + if (got != static_cast(bytes.size()) || std::memcmp(bytes.data(), expected.data(), expected.size()) != 0) { + misread = true; + } + } + stop = true; + seeker.join(); + Require(!misread); + Require(sceKernelClose(fd) == 0); + Require(std::filesystem::remove_all(root) > 0); +} diff --git a/core/libs/tests/GuestPosixFilesystem.cpp b/core/libs/tests/GuestPosixFilesystem.cpp index 540ca69d5d..0e3326d3d0 100644 --- a/core/libs/tests/GuestPosixFilesystem.cpp +++ b/core/libs/tests/GuestPosixFilesystem.cpp @@ -1,5 +1,4 @@ #include "prx/libc/include/General.hpp" -#include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include #include #include @@ -111,7 +110,7 @@ int main() { Require(close_nid_postfix(pipe) == 0); } const int socket = socket_nid_postfix(2, 2, 0); - Require(socket >= GuestSockets::FirstDescriptor); + Require(socket >= 0 && socket < 1024); Require(isatty_nid_postfix(socket) == 0 && *__error_nid_postfix() == 25); Require(close_nid_postfix(socket) == 0); Require(isatty_nid_postfix(socket) == 0 && *__error_nid_postfix() == 9); diff --git a/core/libs/tests/GuestPosixRandom.cpp b/core/libs/tests/GuestPosixRandom.cpp new file mode 100644 index 0000000000..396fcc3aa1 --- /dev/null +++ b/core/libs/tests/GuestPosixRandom.cpp @@ -0,0 +1,57 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" void APS5_VABI arc4random_buf_nid_postfix(void*, std::size_t); +extern "C" std::uint32_t APS5_VABI arc4random_nid_postfix(); + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "Posix random check failed at line %u\n", location.line()); + std::abort(); + } +} + +int main() { + std::array first{}; + std::array second{}; + first.fill(0xa5); + arc4random_buf_nid_postfix(first.data(), 0); + Require(std::all_of(first.begin(), first.end(), [](unsigned char value) { return value == 0xa5; })); + for (const std::size_t size : {1u, 3u, 5u, 4098u}) { + first.fill(0xa5); + arc4random_buf_nid_postfix(first.data(), size); + Require(std::all_of(first.begin() + static_cast(size), first.end(), + [](unsigned char value) { return value == 0xa5; })); + } + arc4random_buf_nid_postfix(first.data(), first.size()); + arc4random_buf_nid_postfix(second.data(), second.size()); + Require(first != second); + std::array counts{}; + for (const auto value : first) ++counts[value]; + Require(*std::max_element(counts.begin(), counts.end()) < 64); + + constexpr std::size_t Draws = 65536; + static std::array words{}; + for (auto& word : words) word = arc4random_nid_postfix(); + std::array bits{}; + std::array nibbles{}; + for (const auto word : words) { + for (std::size_t bit = 0; bit < bits.size(); ++bit) bits[bit] += (word >> bit) & 1u; + ++nibbles[word >> 28]; + } + for (const auto count : bits) Require(count > Draws / 2 - 1500 && count < Draws / 2 + 1500); + for (const auto count : nibbles) Require(count > Draws / 16 - 600 && count < Draws / 16 + 600); + Require(*std::min_element(words.begin(), words.end()) < 0x00100000u); + Require(*std::max_element(words.begin(), words.end()) > 0xfff00000u); + std::sort(words.begin(), words.end()); + std::size_t repeats = 0; + for (std::size_t index = 1; index < Draws; ++index) repeats += words[index] == words[index - 1]; + Require(repeats < 16); + return 0; +} diff --git a/core/libs/tests/GuestPosixScalarIo.cpp b/core/libs/tests/GuestPosixScalarIo.cpp new file mode 100644 index 0000000000..ec96da483a --- /dev/null +++ b/core/libs/tests/GuestPosixScalarIo.cpp @@ -0,0 +1,224 @@ +#include "SceTypes.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 +#include +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +extern "C" { +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 __error_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); +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) { + if (!value) { + std::fprintf(stderr, "POSIX scalar I/O check failed at line %d (guest errno %d)\n", line, *__error_nid_postfix()); + std::abort(); + } +} + +#define Require(value) Check((value), __LINE__) + +#ifndef _WIN32 +static constexpr int GuestEagain = 35; +static constexpr int GuestEisdir = 21; +#endif + +template +static void CheckPosixFailure(Operation operation, int expectedError, int line) { + std::int64_t result = 0; + try { + result = operation(); + } catch (const std::exception& error) { + std::fprintf(stderr, "POSIX syscall threw instead of returning -1: %s\n", error.what()); + Check(false, line); + return; + } + Check(result == -1 && *__error_nid_postfix() == expectedError, line); +} + +#define RequirePosixFailure(operation, error) CheckPosixFailure((operation), (error), __LINE__) + +int main() { + const auto root = std::filesystem::path("anyps5-posix-scalar-io-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + const auto file = root / "scalar.txt"; + { std::ofstream stream(file, std::ios::binary); stream << "posix"; } + const auto fileName = file.string(); + const int descriptor = open_nid_postfix(fileName.c_str(), SCE_KERNEL_O_RDWR, 0); + Require(descriptor >= 0); + int* guestErrno = __error_nid_postfix(); + char buffer[3]{}; + *guestErrno = EACCES; + Require(lseek_nid_postfix(descriptor, 0, SEEK_SET) == 0); + Require(*guestErrno == EACCES); + *guestErrno = EACCES; + Require(read_nid_postfix(descriptor, buffer, 1) == 1 && buffer[0] == 'p'); + Require(*guestErrno == EACCES); + *guestErrno = EACCES; + Require(_read_nid_postfix(descriptor, buffer + 1, 1) == 1 && buffer[1] == 'o'); + Require(*guestErrno == EACCES); + constexpr std::int64_t sparseOffset = (std::int64_t{1} << 32) + 17; + *guestErrno = EACCES; + Require(lseek_nid_postfix(descriptor, sparseOffset, SEEK_SET) == sparseOffset); + Require(*guestErrno == EACCES); + *guestErrno = EACCES; + Require(lseek_nid_postfix(descriptor, 0, SEEK_SET) == 0); + Require(*guestErrno == EACCES); + const char payload[] = {'P', 'S'}; + *guestErrno = EACCES; + Require(write_nid_postfix(descriptor, payload, 1) == 1); + Require(*guestErrno == EACCES); + *guestErrno = EACCES; + Require(_write_nid_postfix(descriptor, payload + 1, 1) == 1); + Require(*guestErrno == EACCES); + *guestErrno = EACCES; + Require(lseek_nid_postfix(descriptor, 0, SEEK_SET) == 0); + Require(*guestErrno == EACCES); + Require(_read_nid_postfix(descriptor, buffer, 2) == 2 && buffer[0] == 'P' && buffer[1] == 'S'); + RequirePosixFailure([&] { return read_nid_postfix(-1, buffer, 1); }, EBADF); + RequirePosixFailure([&] { return _read_nid_postfix(-1, buffer, 1); }, EBADF); + RequirePosixFailure([&] { return write_nid_postfix(-1, payload, 1); }, EBADF); + RequirePosixFailure([&] { return _write_nid_postfix(-1, payload, 1); }, EBADF); + RequirePosixFailure([&] { return lseek_nid_postfix(-1, 0, SEEK_SET); }, EBADF); + RequirePosixFailure([&] { return lseek_nid_postfix(descriptor, 0, -1); }, EINVAL); + RequirePosixFailure([&] { return lseek_nid_postfix(descriptor, 0, 3); }, EINVAL); + RequirePosixFailure([&] { return read_nid_postfix(descriptor, nullptr, 1); }, EFAULT); + RequirePosixFailure([&] { return _read_nid_postfix(descriptor, nullptr, 1); }, EFAULT); + RequirePosixFailure([&] { return write_nid_postfix(descriptor, nullptr, 1); }, EFAULT); + RequirePosixFailure([&] { return _write_nid_postfix(descriptor, nullptr, 1); }, EFAULT); + Require(read_nid_postfix(descriptor, nullptr, 0) == 0); + Require(_read_nid_postfix(descriptor, nullptr, 0) == 0); + Require(write_nid_postfix(descriptor, nullptr, 0) == 0); + Require(_write_nid_postfix(descriptor, nullptr, 0) == 0); + RequirePosixFailure([&] { return read_nid_postfix(-1, nullptr, 0); }, EBADF); + 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; + try { + write_nid_postfix(descriptor, payload, static_cast(std::numeric_limits::max()) + 1); + } catch (const std::runtime_error& error) { + limitDiagnosticPreserved = std::string(error.what()).find("nbytes exceeds platform limit") != std::string::npos; + } + Require(limitDiagnosticPreserved && errno == EACCES); +#endif + char direct[1]{}; + 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); + RequirePosixFailure([&] { return write_nid_postfix(readOnly, payload, 1); }, EBADF); + RequirePosixFailure([&] { return _write_nid_postfix(readOnly, payload, 1); }, EBADF); + Require(close_nid_postfix(readOnly) == 0); + const int writeOnly = open_nid_postfix(fileName.c_str(), SCE_KERNEL_O_WRONLY, 0); + Require(writeOnly >= 0); + RequirePosixFailure([&] { return read_nid_postfix(writeOnly, buffer, 1); }, EBADF); + RequirePosixFailure([&] { return _read_nid_postfix(writeOnly, buffer, 1); }, EBADF); + Require(close_nid_postfix(writeOnly) == 0); + int descriptors[2] = {-1, -1}; + Require(pipe_nid_postfix(descriptors) == 0); + const char pipePayload[] = {'p', 'i', 'p', 'e'}; + Require(write_nid_postfix(descriptors[1], pipePayload, sizeof(pipePayload)) == sizeof(pipePayload)); + Require(close_nid_postfix(descriptors[1]) == 0); + char pipeBuffer[sizeof(pipePayload)]{}; + 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); + const int pipeFlags = fcntl(nonBlockingPipe[0], F_GETFL); + Require(pipeFlags >= 0 && fcntl(nonBlockingPipe[0], F_SETFL, pipeFlags | O_NONBLOCK) == 0); + RequirePosixFailure([&] { return read_nid_postfix(nonBlockingPipe[0], buffer, 1); }, GuestEagain); + RequirePosixFailure([&] { return _read_nid_postfix(nonBlockingPipe[0], buffer, 1); }, GuestEagain); + Require(sceKernelRead(nonBlockingPipe[0], direct, 1) == static_cast(0x80020000u | GuestEagain)); + Require(close_nid_postfix(nonBlockingPipe[0]) == 0); + Require(close_nid_postfix(nonBlockingPipe[1]) == 0); +#endif +#ifndef _WIN32 + const std::string rootName = root.string(); + const int directory = open_nid_postfix(rootName.c_str(), SCE_KERNEL_O_RDONLY, 0); + Require(directory >= 0); + RequirePosixFailure([&] { return read_nid_postfix(directory, buffer, 1); }, GuestEisdir); + RequirePosixFailure([&] { return _read_nid_postfix(directory, buffer, 1); }, GuestEisdir); + Require(close_nid_postfix(directory) == 0); +#endif + Require(std::filesystem::remove_all(root) == 2); + return 0; +} diff --git a/core/libs/tests/GuestPosixSem.cpp b/core/libs/tests/GuestPosixSem.cpp index aae90c1aea..f6a2963120 100644 --- a/core/libs/tests/GuestPosixSem.cpp +++ b/core/libs/tests/GuestPosixSem.cpp @@ -1,16 +1,20 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include +#include #include #include #include #include #include +#include extern "C" { int APS5_VABI sem_init_nid_postfix(void*, int, unsigned int); int APS5_VABI sem_destroy_nid_postfix(void*); int APS5_VABI sem_post_nid_postfix(void*); int APS5_VABI sem_trywait_nid_postfix(void*); +int APS5_VABI sem_timedwait_nid_postfix(void*, const KernelTimespec*); int APS5_VABI sem_getvalue_nid_postfix(void*, int*); int* APS5_VABI __error_nid_postfix(); } @@ -58,4 +62,41 @@ int main(int argc, char** argv) { Require(sem_trywait_nid_postfix(&sem) == 0, "normal wait must succeed"); Require(sem_getvalue_nid_postfix(&sem, &value) == 0 && value == 0, "normal wait must decrement"); Require(sem_destroy_nid_postfix(&sem) == 0, "normal destroy must succeed"); + + KernelTimespec invalidTimeout{0, 1000000000}; + Require(sem_init_nid_postfix(&sem, 0, 1) == 0, "available semaphore must initialize"); + Require(sem_timedwait_nid_postfix(&sem, nullptr) == 0, "available semaphore must ignore null timeout"); + Require(sem_getvalue_nid_postfix(&sem, &value) == 0 && value == 0, "null-timeout wait must consume the available count"); + Require(sem_destroy_nid_postfix(&sem) == 0, "null-timeout semaphore destroy must succeed"); + + Require(sem_init_nid_postfix(&sem, 0, 1) == 0, "invalid-timeout semaphore must initialize"); + Require(sem_timedwait_nid_postfix(&sem, &invalidTimeout) == 0, "available semaphore must ignore invalid timeout"); + Require(sem_getvalue_nid_postfix(&sem, &value) == 0 && value == 0, "invalid-timeout wait must consume the available count"); + Require(sem_destroy_nid_postfix(&sem) == 0, "invalid-timeout semaphore destroy must succeed"); + + Require(sem_init_nid_postfix(&sem, 0, 0) == 0, "blocked semaphore must initialize"); + std::atomic entered = false; + std::atomic completed = false; + int waitResult = -2; + std::thread waiter([&] { + entered.store(true, std::memory_order_release); + waitResult = sem_timedwait_nid_postfix(&sem, nullptr); + completed.store(true, std::memory_order_release); + }); + while (!entered.load(std::memory_order_acquire)) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + Require(!completed.load(std::memory_order_acquire), "blocked null-timeout wait must remain blocked"); + Require(sem_post_nid_postfix(&sem) == 0, "post must release a null-timeout waiter"); + waiter.join(); + Require(waitResult == 0, "null-timeout waiter must succeed after a post"); + Require(sem_getvalue_nid_postfix(&sem, &value) == 0 && value == 0, "null-timeout waiter must consume the posted count"); + + Require(sem_init_nid_postfix(&sem, 0, 0) == 0, "invalid-timeout blocked semaphore must initialize"); + *__error_nid_postfix() = 0; + Require(sem_timedwait_nid_postfix(&sem, &invalidTimeout) == -1 && *__error_nid_postfix() == 22, "blocked invalid-timeout wait must report EINVAL"); + KernelTimespec expiredTimeout{0, 0}; + *__error_nid_postfix() = 0; + Require(sem_timedwait_nid_postfix(&sem, &expiredTimeout) == -1 && *__error_nid_postfix() == 60, "blocked expired-timeout wait must report ETIMEDOUT"); + Require(sem_getvalue_nid_postfix(&sem, &value) == 0 && value == 0, "failed timed waits must preserve the count"); + Require(sem_destroy_nid_postfix(&sem) == 0, "blocked semaphore destroy must succeed"); } diff --git a/core/libs/tests/GuestPosixSpawn.cpp b/core/libs/tests/GuestPosixSpawn.cpp new file mode 100644 index 0000000000..5b714f2fb0 --- /dev/null +++ b/core/libs/tests/GuestPosixSpawn.cpp @@ -0,0 +1,222 @@ +#include "prx/libSceLibcInternal/include/Spawn.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI posix_spawnattr_init_nid_postfix(PosixSpawnAttributes**); +int APS5_VABI posix_spawnattr_destroy_nid_postfix(PosixSpawnAttributes**); +int APS5_VABI posix_spawnattr_setflags_nid_postfix(PosixSpawnAttributes**, std::int16_t); +int APS5_VABI posix_spawnattr_setsigdefault_nid_postfix(PosixSpawnAttributes**, const GuestSignalSet*); +int APS5_VABI posix_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); +int APS5_VABI posix_spawn_file_actions_adddup2_nid_postfix(PosixSpawnFileActions**, int, int); +int APS5_VABI posix_spawn_file_actions_addclose_nid_postfix(PosixSpawnFileActions**, int); +int APS5_VABI posix_spawn_nid_postfix(std::int32_t*, const char*, PosixSpawnFileActions* const*, PosixSpawnAttributes* const*, char* const*, char* const*); +int APS5_VABI posix_spawnp_nid_postfix(std::int32_t*, const char*, PosixSpawnFileActions* const*, PosixSpawnAttributes* const*, char* const*, char* const*); +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) { + if (!condition) { + std::fprintf(stderr, "Posix spawn check failed at line %d\n", line); + std::abort(); + } +} +#define Check(value) Require((value), __LINE__) + +constexpr int NoSuchFile = 2; +constexpr int BadDescriptor = 9; + +template +static bool ThrowsInvalidArgument(TCall call) { + try { + call(); + } catch (const std::invalid_argument&) { + return true; + } + return false; +} + +template +static bool ReportsNotImplemented(TCall call, const std::string& message) { + try { + call(); + } catch (const std::runtime_error& error) { + return std::string(error.what()) == message; + } + return false; +} + +static void CheckAttributes() { + PosixSpawnAttributes* attributes = nullptr; + Check(posix_spawnattr_init_nid_postfix(&attributes) == 0 && attributes != nullptr); + Check(attributes->flags == 0 && attributes->processGroup == 0 && attributes->schedulingPriority == 0 && attributes->schedulingPolicy == 0); + const GuestSignalSet empty{}; + Check(std::memcmp(&attributes->signalDefault, &empty, sizeof(empty)) == 0 && std::memcmp(&attributes->signalMask, &empty, sizeof(empty)) == 0); + + Check(posix_spawnattr_setflags_nid_postfix(&attributes, 0x30) == 0 && attributes->flags == 0x30); + Check(posix_spawnattr_setflags_nid_postfix(&attributes, -1) == 0 && attributes->flags == -1); + Check(posix_spawnattr_setflags_nid_postfix(&attributes, 0) == 0 && attributes->flags == 0); + + const GuestSignalSet defaults{{0x00004002u, 0x80000000u, 1u, 0xffffffffu}}; + Check(posix_spawnattr_setsigdefault_nid_postfix(&attributes, &defaults) == 0); + Check(std::memcmp(&attributes->signalDefault, &defaults, sizeof(defaults)) == 0); + Check(std::memcmp(&attributes->signalMask, &empty, sizeof(empty)) == 0); + + 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); })); + Check(ThrowsInvalidArgument([] { posix_spawnattr_init_nid_postfix(nullptr); })); + + Check(posix_spawnattr_destroy_nid_postfix(&attributes) == 0); + Check(posix_spawnattr_destroy_nid_postfix(&uninitialized) == 0); +} + +static void CheckFileActions() { + PosixSpawnFileActions* actions = nullptr; + Check(posix_spawn_file_actions_init_nid_postfix(&actions) == 0 && actions != nullptr && actions->actions.empty()); + + char path[] = "/app0/log.txt"; + Check(posix_spawn_file_actions_addopen_nid_postfix(&actions, 3, path, 0x601, 0644) == 0); + std::strcpy(path, "/changed"); + Check(posix_spawn_file_actions_adddup2_nid_postfix(&actions, 3, 1) == 0); + Check(posix_spawn_file_actions_addclose_nid_postfix(&actions, 3) == 0); + Check(posix_spawn_file_actions_adddup2_nid_postfix(&actions, 0, 0) == 0); + + Check(posix_spawn_file_actions_addopen_nid_postfix(&actions, -1, "/app0/other", 0, 0) == BadDescriptor); + Check(posix_spawn_file_actions_adddup2_nid_postfix(&actions, -1, 1) == BadDescriptor); + Check(posix_spawn_file_actions_adddup2_nid_postfix(&actions, 1, -1) == BadDescriptor); + Check(posix_spawn_file_actions_addclose_nid_postfix(&actions, -2) == BadDescriptor); + + const auto& recorded = actions->actions; + Check(recorded.size() == 4); + Check(recorded[0].kind == PosixSpawnFileActionKind::Open && recorded[0].descriptor == 3); + Check(recorded[0].path == "/app0/log.txt" && recorded[0].openFlags == 0x601 && recorded[0].mode == 0644); + Check(recorded[1].kind == PosixSpawnFileActionKind::Duplicate && recorded[1].descriptor == 3 && recorded[1].newDescriptor == 1); + Check(recorded[2].kind == PosixSpawnFileActionKind::Close && recorded[2].descriptor == 3); + Check(recorded[3].kind == PosixSpawnFileActionKind::Duplicate && recorded[3].descriptor == 0 && recorded[3].newDescriptor == 0); + + PosixSpawnFileActions* uninitialized = nullptr; + Check(posix_spawn_file_actions_addclose_nid_postfix(&uninitialized, -1) == BadDescriptor); + Check(ThrowsInvalidArgument([&] { posix_spawn_file_actions_addclose_nid_postfix(&uninitialized, 1); })); + Check(ThrowsInvalidArgument([&] { posix_spawn_file_actions_adddup2_nid_postfix(&uninitialized, 1, 2); })); + Check(ThrowsInvalidArgument([&] { posix_spawn_file_actions_addopen_nid_postfix(&uninitialized, 1, "/app0/a", 0, 0); })); + Check(ThrowsInvalidArgument([&] { posix_spawn_file_actions_addopen_nid_postfix(&actions, 1, nullptr, 0, 0); })); + Check(ThrowsInvalidArgument([&] { posix_spawn_file_actions_destroy_nid_postfix(&uninitialized); })); + Check(ThrowsInvalidArgument([] { posix_spawn_file_actions_init_nid_postfix(nullptr); })); + Check(actions->actions.size() == 4); + + Check(posix_spawn_file_actions_destroy_nid_postfix(&actions) == 0); +} + +static void CheckSpawn() { + 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); + 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, 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() { + CheckAttributes(); + CheckFileActions(); + CheckSpawn(); +} diff --git a/core/libs/tests/GuestProcessArguments.cpp b/core/libs/tests/GuestProcessArguments.cpp new file mode 100644 index 0000000000..fa0f5584b8 --- /dev/null +++ b/core/libs/tests/GuestProcessArguments.cpp @@ -0,0 +1,29 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" int APS5_VABI getargc_nid_postfix(void); +extern "C" const char** APS5_VABI getargv_nid_postfix(void); + +static void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +int main() { + const char* const expected[] = {"plain", "two words", "ü日"}; + constexpr int expectedCount = 1 + static_cast(sizeof(expected) / sizeof(expected[0])); + const int count = getargc_nid_postfix(); + const char** values = getargv_nid_postfix(); + Require(count == expectedCount, "getargc does not count every command-line argument"); + Require(values != nullptr && values[0] != nullptr && values[0][0] != '\0', "getargv has no program name"); + for (int index = 1; index < expectedCount; ++index) { + Require(values[index] != nullptr && std::strcmp(values[index], expected[index - 1]) == 0, + "getargv does not return a command-line argument as UTF-8"); + } + Require(values[count] == nullptr, "getargv is not terminated by a null pointer"); + return 0; +} diff --git a/core/libs/tests/GuestProcessTime.cpp b/core/libs/tests/GuestProcessTime.cpp index b24cf8249c..cfff04c3bd 100644 --- a/core/libs/tests/GuestProcessTime.cpp +++ b/core/libs/tests/GuestProcessTime.cpp @@ -1,8 +1,10 @@ #include "prx/libkernel/Time/include/Time.hpp" +#include #include #include #include #include +#include static void Require(bool condition, const char* message) { if (!condition) { @@ -13,12 +15,26 @@ static void Require(bool condition, const char* message) { int main(int argc, char** argv) { constexpr std::uint64_t minuteMicros = 60ULL * 1000 * 1000; + constexpr std::uint64_t sleptMicros = 50ULL * 1000; + const char* scaleText = std::getenv("APS5_TIME_SCALE"); + const double scale = scaleText ? std::strtod(scaleText, nullptr) : 1.0; + const auto elapsedMicros = static_cast(static_cast(sleptMicros) * (scale > 0.0 ? scale : 1.0)); + const auto minimumElapsedMicros = elapsedMicros * 19 / 20; + const auto slept = std::chrono::steady_clock::now() + std::chrono::microseconds(sleptMicros); + while (std::chrono::steady_clock::now() < slept) std::this_thread::sleep_until(slept); if (argc > 1 && std::strcmp(argv[1], "counter") == 0) { - Require(sceKernelGetProcessTimeCounter() < minuteMicros * 1000, "first process time counter wrapped"); + const std::uint64_t counter = sceKernelGetProcessTimeCounter(); + 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(); Require(first < minuteMicros, "first process time wrapped"); + Require(first >= minimumElapsedMicros, "first process time does not include the time before the first call"); Require(sceKernelGetProcessTime() >= first, "process time went backwards"); return 0; } diff --git a/core/libs/tests/GuestProgramName.cpp b/core/libs/tests/GuestProgramName.cpp new file mode 100644 index 0000000000..48be372cc8 --- /dev/null +++ b/core/libs/tests/GuestProgramName.cpp @@ -0,0 +1,32 @@ +#include "SceTypes.hpp" +#include +#include + +extern "C" { +const char* APS5_VABI getprogname_nid_postfix(); +extern const char* __progname_nid_postfix; +int* APS5_VABI __error_nid_postfix(); +} + +static void Require(bool value) { if (!value) std::abort(); } + +int main() { + const char* const original = __progname_nid_postfix; + *__error_nid_postfix() = 45; + Require(getprogname_nid_postfix() == original); + Require(std::strcmp(getprogname_nid_postfix(), "eboot.bin") == 0); + Require(*__error_nid_postfix() == 45); + + char renamed[] = "quake2"; + __progname_nid_postfix = renamed; + Require(getprogname_nid_postfix() == renamed); + renamed[0] = 'Q'; + Require(std::strcmp(getprogname_nid_postfix(), "Quake2") == 0); + + __progname_nid_postfix = ""; + Require(getprogname_nid_postfix()[0] == '\0'); + + __progname_nid_postfix = original; + Require(getprogname_nid_postfix() == original); + Require(*__error_nid_postfix() == 45); +} diff --git a/core/libs/tests/GuestProprietaryVoiceChatHelper.cpp b/core/libs/tests/GuestProprietaryVoiceChatHelper.cpp new file mode 100644 index 0000000000..27f0e6dff8 --- /dev/null +++ b/core/libs/tests/GuestProprietaryVoiceChatHelper.cpp @@ -0,0 +1,60 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include + +extern "C" { +int APS5_VABI sceProprietaryVoiceChatHelperInitialize(); +int APS5_VABI sceProprietaryVoiceChatHelperTerminate(); +int APS5_VABI sceProprietaryVoiceChatHelperSetVoiceChatState(); +int APS5_VABI sceProprietaryVoiceChatHelperGetVoiceChatUsageState(); +int APS5_VABI sceProprietaryVoiceChatHelperGetVoiceChat(); +} + +static void Require(bool value, const char* what) { + if (value) return; + std::printf("ProprietaryVoiceChatHelper: %s\n", what); + std::abort(); +} + +template +static bool Throws(TCall call) { + try { + call(); + } catch (const TError&) { + return true; + } + return false; +} + +int main() { + Require(Throws([] { sceProprietaryVoiceChatHelperSetVoiceChatState(); }), + "setting the state before initialization did not throw"); + Require(Throws([] { sceProprietaryVoiceChatHelperGetVoiceChatUsageState(); }), + "reading the usage state before initialization did not throw"); + Require(Throws([] { sceProprietaryVoiceChatHelperGetVoiceChat(); }), + "getting voice chat before initialization did not throw"); + Require(Throws([] { sceProprietaryVoiceChatHelperTerminate(); }), + "terminating before initialization did not throw"); + + Require(sceProprietaryVoiceChatHelperInitialize() == 0, "initialization failed"); + Require(Throws([] { sceProprietaryVoiceChatHelperInitialize(); }), + "initializing twice did not throw"); + Require(sceProprietaryVoiceChatHelperSetVoiceChatState() == 0, "setting the state failed"); + Require(Throws([] { sceProprietaryVoiceChatHelperGetVoiceChatUsageState(); }), + "reading the unknown usage state did not throw"); + Require(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"); + + std::puts("ProprietaryVoiceChatHelper tests passed"); + return 0; +} 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..c545232c9b 100644 --- a/core/libs/tests/GuestPsml.cpp +++ b/core/libs/tests/GuestPsml.cpp @@ -2,11 +2,25 @@ #include #include #include +#include +#include +#include extern "C" { std::int32_t APS5_VABI scePsmlMfsrGetContextBufferRequirement1100(void* requirement, const void* param); std::int32_t APS5_VABI scePsmlMfsrCreateContext1100(void** context, const void* param); std::int32_t APS5_VABI scePsmlMfsrGetDispatchMfsrPacket1100(void* context, void* commandBuffer, const void* param); +std::int32_t APS5_VABI scePsmlMfsrGetContextBufferRequirement800M3_2(void* requirement, const void* param); +std::int32_t APS5_VABI scePsmlMfsrCreateContext800M3_2(void** context, const void* param); +std::int32_t APS5_VABI scePsmlMfsrCreateContext1300(void** context, const void* param); +std::int32_t APS5_VABI scePsmlMfsrGetDispatchMfsrPacket900(void* context, void* commandBuffer, const void* param); +std::int32_t APS5_VABI scePsmlMfsrGetDispatchMfsrPacket1000(void* context, void* commandBuffer, const void* param); +std::int32_t APS5_VABI scePsmlMfsrGetDispatchMfsrPacket1300(void* context, void* commandBuffer, const void* param); +std::int32_t APS5_VABI scePsmlMfsrInit(); +std::int32_t APS5_VABI scePsmlMfsrGetSharedResourcesInitRequirement(void* requirement, const void* param); +std::int32_t APS5_VABI scePsmlMfsrCreateSharedResources(void** sharedResources, const void* param); +std::int32_t APS5_VABI scePsmlMfsrReleaseSharedResources(void* sharedResources); +std::int32_t APS5_VABI scePsmlMfsrReleaseContext(void* context); } namespace { @@ -20,6 +34,164 @@ struct CommandBuffer { std::uint32_t sizeInDwords; }; +using CreateContext = std::int32_t (APS5_VABI*)(void**, const void*); +using GetDispatchPacket = std::int32_t (APS5_VABI*)(void*, void*, const void*); + +void CreateContextUntouched(CreateContext create) { + std::array param{}; + std::uint64_t contextStorage = 0; + void* const sentinel = &contextStorage; + void* context = sentinel; + Require(create(&context, param.data()) == kErrNotInitialized); + Require(context == sentinel); + Require(create(nullptr, nullptr) == kErrNotInitialized); +} + +void DispatchPacketUntouched(GetDispatchPacket dispatch) { + std::array param{}; + std::uint64_t contextStorage = 0; + std::array dwords; + dwords.fill(0xa5a5a5a5u); + const auto untouchedDwords = dwords; + CommandBuffer commandBuffer{dwords.data(), static_cast(dwords.size())}; + Require(dispatch(&contextStorage, &commandBuffer, param.data()) == kErrNotInitialized); + Require(dispatch(nullptr, &commandBuffer, nullptr) == kErrNotInitialized); + Require(commandBuffer.cursor == dwords.data()); + Require(commandBuffer.sizeInDwords == dwords.size()); + Require(dwords == untouchedDwords); +} + +template +bool Throws(TCall call, const char* text) { + try { + call(); + } catch (const std::runtime_error& error) { + return std::string(error.what()).find(text) != std::string::npos; + } + return false; +} + +struct Requirement { + std::uint64_t blockBytes; + std::uint64_t alignment; + std::uint64_t blockCount; +}; + +struct Block { + std::uint64_t address; + std::uint64_t bytes; +}; + +struct SharedParam { + std::uint32_t type; + std::uint32_t reserved; + const Block* blocks; + std::uint64_t blockCount; + std::uint64_t reserved2; +}; + +struct ContextParam { + std::array bytes{}; + + void Set(const Block* blocks, std::uint64_t count, void* shared) { + std::memcpy(bytes.data() + 0x08, &blocks, sizeof(blocks)); + std::memcpy(bytes.data() + 0x10, &count, sizeof(count)); + std::memcpy(bytes.data() + 0x38, &shared, sizeof(shared)); + } +}; + +std::array Blocks(const Requirement& requirement, std::uint64_t first) { + std::array blocks{}; + for (std::size_t i = 0; i < blocks.size(); ++i) blocks[i] = {first + i * requirement.blockBytes, requirement.blockBytes}; + return blocks; +} + +void SetupBeforeInit() { + std::uint64_t storage = 0; + void* const sentinel = &storage; + void* handle = sentinel; + Requirement requirement{1, 2, 3}; + SharedParam sharedParam{}; + Require(scePsmlMfsrGetSharedResourcesInitRequirement(&requirement, &sharedParam) == kErrNotInitialized); + Require(requirement.blockBytes == 1 && requirement.alignment == 2 && requirement.blockCount == 3); + Require(scePsmlMfsrCreateSharedResources(&handle, &sharedParam) == kErrNotInitialized); + Require(handle == sentinel); + Require(scePsmlMfsrReleaseContext(sentinel) == kErrNotInitialized); + Require(scePsmlMfsrReleaseSharedResources(sentinel) == kErrNotInitialized); +} + +void SetupAfterInit() { + Require(scePsmlMfsrInit() == 0); + Require(scePsmlMfsrInit() == 0); + + SharedParam sharedParam{}; + Requirement shared{}; + Require(scePsmlMfsrGetSharedResourcesInitRequirement(&shared, &sharedParam) == 0); + Require(shared.blockBytes == 0x200000 && shared.alignment == 0x200000 && shared.blockCount == 4); + + const auto sharedBlocks = Blocks(shared, 0x10000000); + sharedParam.blocks = sharedBlocks.data(); + sharedParam.blockCount = sharedBlocks.size(); + void* sharedResources = nullptr; + Require(scePsmlMfsrCreateSharedResources(&sharedResources, &sharedParam) == 0); + Require(sharedResources != nullptr); + + ContextParam contextParam; + contextParam.Set(nullptr, 0, sharedResources); + Requirement context{}; + Require(scePsmlMfsrGetContextBufferRequirement800M3_2(&context, contextParam.bytes.data()) == 0); + Require(context.blockBytes == 0x200000 && context.alignment == 0x200000 && context.blockCount == 4); + + const auto contextBlocks = Blocks(context, 0x20000000); + contextParam.Set(contextBlocks.data(), contextBlocks.size(), sharedResources); + void* mfsrContext = nullptr; + Require(scePsmlMfsrCreateContext800M3_2(&mfsrContext, contextParam.bytes.data()) == 0); + Require(mfsrContext != nullptr && mfsrContext != sharedResources); + + Require(Throws([&] { scePsmlMfsrReleaseSharedResources(sharedResources); }, "still used by a context")); + Require(Throws([&] { scePsmlMfsrReleaseContext(sharedResources); }, "not created by this library")); + Require(scePsmlMfsrReleaseContext(mfsrContext) == 0); + Require(Throws([&] { scePsmlMfsrReleaseContext(mfsrContext); }, "not created by this library")); + Require(scePsmlMfsrReleaseSharedResources(sharedResources) == 0); +} + +void RefusedAfterInit() { + SharedParam sharedParam{}; + Requirement requirement{}; + sharedParam.type = 1; + Require(Throws([&] { scePsmlMfsrGetSharedResourcesInitRequirement(&requirement, &sharedParam); }, "type 1 is not supported")); + + sharedParam.type = 0; + Require(scePsmlMfsrGetSharedResourcesInitRequirement(&requirement, &sharedParam) == 0); + const auto blocks = Blocks(requirement, 0x10000000); + sharedParam.blocks = blocks.data(); + sharedParam.blockCount = 3; + void* sharedResources = nullptr; + Require(Throws([&] { scePsmlMfsrCreateSharedResources(&sharedResources, &sharedParam); }, "fewer memory blocks")); + auto small = blocks; + small[2].bytes = 0x100000; + sharedParam.blocks = small.data(); + sharedParam.blockCount = small.size(); + Require(Throws([&] { scePsmlMfsrCreateSharedResources(&sharedResources, &sharedParam); }, "smaller or less aligned")); + Require(sharedResources == nullptr); + + std::uint64_t foreign = 0; + ContextParam contextParam; + contextParam.Set(blocks.data(), blocks.size(), &foreign); + void* context = nullptr; + Require(Throws([&] { scePsmlMfsrCreateContext800M3_2(&context, contextParam.bytes.data()); }, "shared resources handle was not created")); + Require(context == nullptr); + + std::array param{}; + std::array dwords{}; + CommandBuffer commandBuffer{dwords.data(), static_cast(dwords.size())}; + Require(Throws([&] { scePsmlMfsrGetContextBufferRequirement1100(&requirement, param.data()); }, "not implemented")); + Require(Throws([&] { scePsmlMfsrCreateContext1300(&context, param.data()); }, "not implemented")); + Require(Throws([&] { scePsmlMfsrGetDispatchMfsrPacket900(&foreign, &commandBuffer, param.data()); }, "not implemented")); + Require(Throws([&] { scePsmlMfsrGetDispatchMfsrPacket1000(&foreign, &commandBuffer, param.data()); }, "not implemented")); + Require(Throws([&] { scePsmlMfsrGetDispatchMfsrPacket1300(&foreign, &commandBuffer, param.data()); }, "not implemented")); +} + } int main() { @@ -50,4 +222,20 @@ int main() { Require(scePsmlMfsrGetDispatchMfsrPacket1100(nullptr, &commandBuffer, param.data()) == kErrNotInitialized); Require(commandBuffer.cursor == dwords.data()); Require(dwords == untouchedDwords); + + std::array olderRequirement; + olderRequirement.fill(0xa5); + const auto untouchedOlderRequirement = olderRequirement; + Require(scePsmlMfsrGetContextBufferRequirement800M3_2(olderRequirement.data(), param.data()) == kErrNotInitialized); + Require(olderRequirement == untouchedOlderRequirement); + Require(scePsmlMfsrGetContextBufferRequirement800M3_2(nullptr, nullptr) == kErrNotInitialized); + + CreateContextUntouched(scePsmlMfsrCreateContext800M3_2); + CreateContextUntouched(scePsmlMfsrCreateContext1300); + DispatchPacketUntouched(scePsmlMfsrGetDispatchMfsrPacket900); + DispatchPacketUntouched(scePsmlMfsrGetDispatchMfsrPacket1000); + DispatchPacketUntouched(scePsmlMfsrGetDispatchMfsrPacket1300); + SetupBeforeInit(); + SetupAfterInit(); + RefusedAfterInit(); } diff --git a/core/libs/tests/GuestPthreadAtfork.cpp b/core/libs/tests/GuestPthreadAtfork.cpp new file mode 100644 index 0000000000..0cf6b91fec --- /dev/null +++ b/core/libs/tests/GuestPthreadAtfork.cpp @@ -0,0 +1,48 @@ +#include "prx/libkernel/Pthread/include/Atfork.hpp" +#include +#include +#include +#include +#include + +static void Require(bool value) { if (!value) std::abort(); } + +static std::atomic calls{0}; + +static void APS5_VABI Prepare() { ++calls; } +static void APS5_VABI Parent() { ++calls; } +static void APS5_VABI Child() { ++calls; } + +static bool Same(const PthreadAtforkHandlers& entry, PthreadAtforkHandler prepare, PthreadAtforkHandler parent, PthreadAtforkHandler child) { + return entry.prepare == prepare && entry.parent == parent && entry.child == child; +} + +int main() { + Require(PthreadAtforkHandlers_nid_no_patch().empty()); + Require(pthread_atfork_nid_postfix(Prepare, Parent, Child) == 0); + Require(pthread_atfork_nid_postfix(nullptr, Parent, nullptr) == 0); + Require(pthread_atfork_nid_postfix(nullptr, nullptr, nullptr) == 0); + Require(pthread_atfork_nid_postfix(Prepare, Parent, Child) == 0); + auto handlers = PthreadAtforkHandlers_nid_no_patch(); + Require(handlers.size() == 4); + Require(Same(handlers[0], Prepare, Parent, Child)); + Require(Same(handlers[1], nullptr, Parent, nullptr)); + Require(Same(handlers[2], nullptr, nullptr, nullptr)); + Require(Same(handlers[3], Prepare, Parent, Child)); + + constexpr std::size_t threadCount = 4; + constexpr std::size_t perThread = 64; + std::vector threads; + for (std::size_t index = 0; index < threadCount; ++index) { + threads.emplace_back([] { + for (std::size_t count = 0; count < perThread; ++count) Require(pthread_atfork_nid_postfix(nullptr, nullptr, Child) == 0); + }); + } + for (auto& thread : threads) thread.join(); + handlers = PthreadAtforkHandlers_nid_no_patch(); + Require(handlers.size() == 4 + threadCount * perThread); + Require(Same(handlers[3], Prepare, Parent, Child)); + for (std::size_t index = 4; index < handlers.size(); ++index) Require(Same(handlers[index], nullptr, nullptr, Child)); + Require(calls == 0); + return 0; +} diff --git a/core/libs/tests/GuestPthreadAttr.cpp b/core/libs/tests/GuestPthreadAttr.cpp index 8588e2ae12..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); @@ -19,12 +21,30 @@ int APS5_VABI scePthreadAttrGetschedparam(const PthreadAttr* attr, KernelSchedPa int APS5_VABI scePthreadAttrGetaffinity(const PthreadAttr* attr, KernelCpumask* mask); int APS5_VABI scePthreadAttrGetstacksize(const PthreadAttr* attr, std::size_t* stackSize); int APS5_VABI scePthreadAttrGetdetachstate(const PthreadAttr* attr, int* state); +int APS5_VABI scePthreadAttrGetsolosched(const PthreadAttr* attr, int* solosched); int APS5_VABI pthread_attr_setstacksize_nid_postfix(PthreadAttr* attr, std::size_t stackSize); +int APS5_VABI pthread_attr_setsolosched_np_nid_postfix(PthreadAttr* attr, int solosched); +int APS5_VABI pthread_attr_getinheritsched_nid_postfix(const PthreadAttr* attr, int* inherit_sched); +int APS5_VABI pthread_attr_setinheritsched_nid_postfix(PthreadAttr* attr, int inherit_sched); +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; static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); static constexpr int EXPLICIT_SCHED = 0; +static constexpr int INHERIT_SCHED = 4; +static constexpr int SCOPE_PROCESS = 0; +static constexpr int SCOPE_SYSTEM = 2; static constexpr int DETACH_JOINABLE = 0; static constexpr int CREATION_PRIORITY = 256; static constexpr int UPDATED_PRIORITY = 767; @@ -33,6 +53,10 @@ static constexpr KernelCpumask UPDATED_AFFINITY = 0x1000; 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(); } @@ -46,6 +70,7 @@ struct ReportedAttributes { extern "C" { int APS5_VABI pthread_attr_setstack_nid_postfix(PthreadAttr* attr, void* addr, std::size_t size); int APS5_VABI pthread_attr_getstack_nid_postfix(const PthreadAttr* attr, void** addr, std::size_t* size); +int APS5_VABI pthread_attr_getstackaddr_nid_postfix(const PthreadAttr* attr, void** addr); } static ReportedAttributes Query(Pthread thread) { @@ -76,13 +101,251 @@ static void CheckStackSizeLimit() { Require(scePthreadAttrDestroy(&attr) == SCE_OK); } +extern "C" int APS5_VABI scePthreadAttrSetsolosched(PthreadAttr* attr, int solosched); + +static void CheckSoloSched() { + PthreadAttr attr = nullptr; + Require(pthread_attr_setsolosched_np_nid_postfix(nullptr, 1) == GUEST_EINVAL); + Require(pthread_attr_setsolosched_np_nid_postfix(&attr, 1) == GUEST_EINVAL); + Require(scePthreadAttrInit(&attr) == SCE_OK); + int soloSched = -1; + Require(scePthreadAttrGetsolosched(&attr, &soloSched) == SCE_OK && soloSched == 0); + Require(pthread_attr_setsolosched_np_nid_postfix(&attr, 16) == 0); + Require(scePthreadAttrGetsolosched(&attr, &soloSched) == SCE_OK && soloSched == 16); + constexpr int GuestEopnotsupp = 45; + constexpr int SceKernelErrorEopnotsupp = static_cast(0x8002002Du); + for (int rejected : {-1, 1, 2, 4, 8, 15, 17, 32, 100, 0x7fffffff}) { + Require(pthread_attr_setsolosched_np_nid_postfix(&attr, rejected) == GuestEopnotsupp); + Require(scePthreadAttrSetsolosched(&attr, rejected) == SceKernelErrorEopnotsupp); + Require(scePthreadAttrGetsolosched(&attr, &soloSched) == SCE_OK && soloSched == 16); + } + Require(pthread_attr_setsolosched_np_nid_postfix(&attr, 0) == 0); + Require(scePthreadAttrGetsolosched(&attr, &soloSched) == SCE_OK && soloSched == 0); + PthreadAttr unset = nullptr; + Require(scePthreadAttrSetsolosched(&unset, 16) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrSetsolosched(nullptr, 16) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGetsolosched(&unset, &soloSched) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrDestroy(&attr) == SCE_OK); + 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; } +static void CheckSchedulingAttributes() { + int inherit = -1; + int scope = -1; + Require(pthread_attr_getinheritsched_nid_postfix(nullptr, &inherit) == GUEST_EINVAL); + Require(pthread_attr_setinheritsched_nid_postfix(nullptr, EXPLICIT_SCHED) == GUEST_EINVAL); + Require(pthread_attr_getscope_nid_postfix(nullptr, &scope) == GUEST_EINVAL); + Require(pthread_attr_setscope_nid_postfix(nullptr, SCOPE_SYSTEM) == GUEST_EINVAL); + + PthreadAttr attr = nullptr; + Require(pthread_attr_getinheritsched_nid_postfix(&attr, &inherit) == GUEST_EINVAL); + Require(pthread_attr_setinheritsched_nid_postfix(&attr, EXPLICIT_SCHED) == GUEST_EINVAL); + Require(pthread_attr_getscope_nid_postfix(&attr, &scope) == GUEST_EINVAL); + Require(pthread_attr_setscope_nid_postfix(&attr, SCOPE_SYSTEM) == GUEST_EINVAL); + + Require(scePthreadAttrInit(&attr) == SCE_OK); + Require(pthread_attr_getinheritsched_nid_postfix(&attr, &inherit) == 0 && inherit == INHERIT_SCHED); + Require(pthread_attr_setinheritsched_nid_postfix(&attr, 1) == GUEST_ENOTSUP); + Require(pthread_attr_setinheritsched_nid_postfix(&attr, -1) == GUEST_ENOTSUP); + Require(pthread_attr_setinheritsched_nid_postfix(&attr, EXPLICIT_SCHED) == 0); + Require(pthread_attr_getinheritsched_nid_postfix(&attr, &inherit) == 0 && inherit == EXPLICIT_SCHED); + Require(pthread_attr_setinheritsched_nid_postfix(&attr, INHERIT_SCHED) == 0); + Require(pthread_attr_getinheritsched_nid_postfix(&attr, &inherit) == 0 && inherit == INHERIT_SCHED); + + Require(pthread_attr_getscope_nid_postfix(&attr, &scope) == 0 && scope == SCOPE_SYSTEM); + Require(pthread_attr_setscope_nid_postfix(&attr, 1) == GUEST_EINVAL); + Require(pthread_attr_setscope_nid_postfix(&attr, 3) == GUEST_EINVAL); + Require(pthread_attr_setscope_nid_postfix(&attr, -1) == GUEST_EINVAL); + Require(pthread_attr_setscope_nid_postfix(&attr, SCOPE_PROCESS) == 0); + Require(pthread_attr_getscope_nid_postfix(&attr, &scope) == 0 && scope == SCOPE_PROCESS); + Require(pthread_attr_setscope_nid_postfix(&attr, SCOPE_SYSTEM) == 0); + Require(pthread_attr_getscope_nid_postfix(&attr, &scope) == 0 && scope == SCOPE_SYSTEM); + + Require(pthread_attr_getscope_nid_postfix(&attr, nullptr) == GUEST_EINVAL); + + bool threw = false; + try { + pthread_attr_getinheritsched_nid_postfix(&attr, nullptr); + } catch (const std::exception&) { + threw = true; + } + Require(threw); + + Require(scePthreadAttrDestroy(&attr) == SCE_OK); + Require(pthread_attr_getinheritsched_nid_postfix(&attr, &inherit) == GUEST_EINVAL); + Require(pthread_attr_setinheritsched_nid_postfix(&attr, EXPLICIT_SCHED) == GUEST_EINVAL); + Require(pthread_attr_getscope_nid_postfix(&attr, &scope) == GUEST_EINVAL); + Require(pthread_attr_setscope_nid_postfix(&attr, SCOPE_SYSTEM) == GUEST_EINVAL); +} + +static void CheckThreadScope() { + PthreadAttr attr = nullptr; + Require(scePthreadAttrInit(&attr) == SCE_OK); + Require(pthread_attr_setinheritsched_nid_postfix(&attr, EXPLICIT_SCHED) == 0); + Require(pthread_attr_setscope_nid_postfix(&attr, SCOPE_PROCESS) == 0); + + std::promise release; + auto released = release.get_future(); + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, &attr, Worker, &released, nullptr) == SCE_OK); + Require(scePthreadAttrDestroy(&attr) == SCE_OK); + + PthreadAttr queryAttr = nullptr; + Require(scePthreadAttrInit(&queryAttr) == SCE_OK); + Require(scePthreadAttrGet(thread, &queryAttr) == SCE_OK); + int scope = -1; + Require(pthread_attr_getscope_nid_postfix(&queryAttr, &scope) == 0 && scope == SCOPE_PROCESS); + + Require(pthread_attr_get_np_nid_postfix(thread, &queryAttr) == 0); + Require(pthread_attr_getscope_nid_postfix(&queryAttr, &scope) == 0 && scope == SCOPE_PROCESS); + Require(scePthreadAttrDestroy(&queryAttr) == SCE_OK); + + release.set_value(); + Require(scePthreadJoin(thread, nullptr) == SCE_OK); + + Require(scePthreadAttrInit(&attr) == SCE_OK); + Require(pthread_attr_setinheritsched_nid_postfix(&attr, INHERIT_SCHED) == 0); + Require(pthread_attr_setscope_nid_postfix(&attr, SCOPE_PROCESS) == 0); + + std::promise releaseInherited; + auto releasedInherited = releaseInherited.get_future(); + Pthread inheritedThread = nullptr; + Require(scePthreadCreate(&inheritedThread, &attr, Worker, &releasedInherited, nullptr) == SCE_OK); + Require(scePthreadAttrDestroy(&attr) == SCE_OK); + + Require(scePthreadAttrInit(&queryAttr) == SCE_OK); + Require(scePthreadAttrGet(inheritedThread, &queryAttr) == SCE_OK); + scope = -1; + Require(pthread_attr_getscope_nid_postfix(&queryAttr, &scope) == 0 && scope == SCOPE_SYSTEM); + Require(pthread_attr_get_np_nid_postfix(inheritedThread, &queryAttr) == 0); + Require(pthread_attr_getscope_nid_postfix(&queryAttr, &scope) == 0 && scope == SCOPE_SYSTEM); + Require(scePthreadAttrDestroy(&queryAttr) == SCE_OK); + + releaseInherited.set_value(); + 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); @@ -123,16 +386,23 @@ int main() { PthreadAttr stackAttr = nullptr; char stackMarker; + void* reportedAddress = nullptr; + std::size_t reportedSize = 0; Require(pthread_attr_setstack_nid_postfix(nullptr, &stackMarker, STACK_SIZE) == 22); Require(pthread_attr_setstack_nid_postfix(&stackAttr, &stackMarker, STACK_SIZE) == 22); + Require(pthread_attr_getstackaddr_nid_postfix(nullptr, &reportedAddress) == 22); + Require(pthread_attr_getstackaddr_nid_postfix(&stackAttr, &reportedAddress) == 22); Require(scePthreadAttrInit(&stackAttr) == SCE_OK); + Require(pthread_attr_getstackaddr_nid_postfix(&stackAttr, nullptr) == 22); + Require(pthread_attr_getstackaddr_nid_postfix(&stackAttr, &reportedAddress) == 0 && reportedAddress == nullptr); Require(pthread_attr_setstack_nid_postfix(&stackAttr, &stackMarker, STACK_SIZE) == 0); Require(pthread_attr_setstack_nid_postfix(&stackAttr, nullptr, STACK_SIZE) == 22); Require(pthread_attr_setstack_nid_postfix(&stackAttr, &stackMarker, 16383) == 22); - void* reportedAddress = nullptr; - std::size_t reportedSize = 0; Require(pthread_attr_getstack_nid_postfix(&stackAttr, &reportedAddress, &reportedSize) == 0); Require(reportedAddress == &stackMarker && reportedSize == STACK_SIZE); + reportedAddress = nullptr; + Require(pthread_attr_getstackaddr_nid_postfix(&stackAttr, &reportedAddress) == 0 && reportedAddress == &stackMarker); Require(scePthreadAttrDestroy(&stackAttr) == SCE_OK); Require(pthread_attr_setstack_nid_postfix(&stackAttr, &stackMarker, STACK_SIZE) == 22); + Require(pthread_attr_getstackaddr_nid_postfix(&stackAttr, &reportedAddress) == 22); } diff --git a/core/libs/tests/GuestPthreadAttrErrors.cpp b/core/libs/tests/GuestPthreadAttrErrors.cpp new file mode 100644 index 0000000000..06c2e15832 --- /dev/null +++ b/core/libs/tests/GuestPthreadAttrErrors.cpp @@ -0,0 +1,109 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI scePthreadAttrInit(PthreadAttr* attr); +int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr); +int APS5_VABI scePthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stackSize); +int APS5_VABI scePthreadAttrGetstacksize(const PthreadAttr* attr, std::size_t* stackSize); +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); +int APS5_VABI scePthreadAttrGetaffinity(const PthreadAttr* attr, KernelCpumask* mask); +} + +static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016u); +static constexpr int SCE_KERNEL_ERROR_EOPNOTSUPP = static_cast(0x8002002Du); + +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); + Require(scePthreadAttrSetstacksize(&attr, 16384) == SCE_OK); + + 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); + 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); + 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}) { + param.sched_priority = priority; + Require(scePthreadAttrSetschedparam(&attr, ¶m) == SCE_KERNEL_ERROR_EOPNOTSUPP); + } + Require(scePthreadAttrSetschedparam(&attr, nullptr) == SCE_KERNEL_ERROR_EOPNOTSUPP); + for (int priority : {256, 767}) { + param.sched_priority = priority; + Require(scePthreadAttrSetschedparam(&attr, ¶m) == SCE_OK); + KernelSchedParam read{}; + Require(scePthreadAttrGetschedparam(&attr, &read) == SCE_OK && read.sched_priority == priority); + } + for (KernelCpumask mask : {KernelCpumask{0}, KernelCpumask{1}, ~KernelCpumask{0}}) + Require(scePthreadAttrSetaffinity(&attr, mask) == SCE_OK); + + std::size_t size = 0; + int state = 0; + KernelCpumask mask = 0; + 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; + PthreadAttr* target = missing ? &unset : nullptr; + Require(scePthreadAttrSetstacksize(target, 16384) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrSetdetachstate(target, 0) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrSetschedpolicy(target, 1) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrSetinheritsched(target, 0) == SCE_KERNEL_ERROR_EINVAL); + param.sched_priority = 700; + Require(scePthreadAttrSetschedparam(target, ¶m) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrSetaffinity(target, 1) == SCE_KERNEL_ERROR_EINVAL); + 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); + } + + Require(scePthreadAttrDestroy(&attr) == SCE_OK); + Require(scePthreadAttrDestroy(&attr) == 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 64d67e201a..6fbdcb4bad 100644 --- a/core/libs/tests/GuestPthreadBarrier.cpp +++ b/core/libs/tests/GuestPthreadBarrier.cpp @@ -42,6 +42,13 @@ static void* APS5_VABI Waiter(void* arg) { return nullptr; } +static void* APS5_VABI DestroyingWaiter(void* arg) { + auto& context = *static_cast(arg); + if (pthread_barrier_wait_nid_postfix(&context.barrier) == BARRIER_SERIAL_THREAD) + context.serial = pthread_barrier_destroy_nid_postfix(&context.barrier) == 0 && context.barrier == nullptr ? 1 : -1; + return nullptr; +} + static void RunWaiters(Context& context) { std::array threads{}; for (auto& thread : threads) Require(scePthreadCreate(&thread, nullptr, Waiter, &context, nullptr) == 0); @@ -66,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); @@ -101,6 +109,15 @@ int main() { RunWaiters(withoutAttr); Require(pthread_barrier_destroy_nid_postfix(&withoutAttr.barrier) == 0); + for (int round = 0; round < 50; ++round) { + Context destroyed; + Require(pthread_barrier_init_nid_postfix(&destroyed.barrier, nullptr, WAITERS) == 0); + std::array threads{}; + for (auto& thread : threads) Require(scePthreadCreate(&thread, nullptr, DestroyingWaiter, &destroyed, nullptr) == 0); + for (auto& thread : threads) Require(scePthreadJoin(thread, nullptr) == 0); + Require(destroyed.serial == 1); + } + GuestBarrier* single = nullptr; Require(pthread_barrierattr_init_nid_postfix(&attr) == 0); Require(pthread_barrier_init_nid_postfix(&single, &attr, 1) == 0); diff --git a/core/libs/tests/GuestPthreadCancel.cpp b/core/libs/tests/GuestPthreadCancel.cpp new file mode 100644 index 0000000000..576414c6c3 --- /dev/null +++ b/core/libs/tests/GuestPthreadCancel.cpp @@ -0,0 +1,282 @@ +#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 APS5_VABI scePthreadExit(void* retval); +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 scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* attr, const char* name); +int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex); +int APS5_VABI scePthreadMutexLock(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 scePthreadCondWait(PthreadCond* cond, PthreadMutex* mutex); +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); +int APS5_VABI sceKernelUsleep_nid_postfix(KernelUseconds microseconds); +int APS5_VABI pthread_cancel_nid_postfix(Pthread thread); +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); +} + +static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_ESRCH = static_cast(0x80020003); +static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); +static constexpr int GUEST_ESRCH = 3; +static constexpr int GUEST_EINVAL = 22; +static constexpr int CANCEL_ENABLE = 0; +static constexpr int CANCEL_DISABLE = 1; +static constexpr int CANCEL_DEFERRED = 0; +static constexpr int CANCEL_ASYNCHRONOUS = 2; +static void* const CANCELED = reinterpret_cast(std::uintptr_t{1}); + +static void Require(bool value) { if (!value) std::abort(); } + +struct Shared { + PthreadMutex mutex = nullptr; + PthreadCond cond = nullptr; + PthreadSem sem = nullptr; + void* posixSem = nullptr; + Pthread target = nullptr; + std::atomic ready{false}; + std::atomic release{false}; + std::atomic returned{false}; + std::atomic survived{false}; + std::atomic cleanups{0}; + int order[2] = {0, 0}; +}; + +static Shared shared; + +static void APS5_VABI UnlockMutex(void*) { + Require(scePthreadMutexUnlock(&shared.mutex) == SCE_OK); + shared.cleanups.fetch_add(1); +} + +static void APS5_VABI RecordOrder(void* arg) { + shared.order[shared.cleanups.fetch_add(1)] = static_cast(reinterpret_cast(arg)); +} + +static void* APS5_VABI CondWaiter(void*) { + std::uintptr_t info[8]; + Require(scePthreadMutexLock(&shared.mutex) == SCE_OK); + __pthread_cleanup_push_imp_nid_postfix(UnlockMutex, nullptr, info); + shared.ready.store(true); + for (;;) Require(scePthreadCondWait(&shared.cond, &shared.mutex) == SCE_OK); +} + +static void* APS5_VABI SemWaiter(void*) { + shared.ready.store(true); + scePthreadSemWait(&shared.sem); + shared.returned.store(true); + return nullptr; +} + +static void* APS5_VABI PosixSemWaiter(void*) { + shared.ready.store(true); + sem_wait_nid_postfix(&shared.posixSem); + shared.returned.store(true); + 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; +} + +static void* APS5_VABI Joiner(void*) { + shared.ready.store(true); + scePthreadJoin(shared.target, nullptr); + shared.returned.store(true); + return nullptr; +} + +static void* APS5_VABI Sleeper(void*) { + shared.ready.store(true); + for (;;) sceKernelUsleep_nid_postfix(1000); +} + +static void* APS5_VABI Disabled(void*) { + int old = -1; + Require(scePthreadSetcancelstate(CANCEL_DISABLE, &old) == SCE_OK && old == CANCEL_ENABLE); + shared.ready.store(true); + while (!shared.release.load()) { + sceKernelUsleep_nid_postfix(1000); + scePthreadTestcancel(); + } + shared.survived.store(true); + Require(scePthreadSetcancelstate(CANCEL_ENABLE, &old) == SCE_OK && old == CANCEL_DISABLE); + scePthreadTestcancel(); + shared.returned.store(true); + return nullptr; +} + +static void* APS5_VABI Asynchronous(void*) { + int old = -1; + Require(scePthreadSetcancelstate(CANCEL_DISABLE, nullptr) == SCE_OK); + Require(scePthreadSetcanceltype(CANCEL_ASYNCHRONOUS, &old) == SCE_OK && old == CANCEL_DEFERRED); + shared.ready.store(true); + while (!shared.release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + scePthreadSetcancelstate(CANCEL_ENABLE, nullptr); + shared.returned.store(true); + return nullptr; +} + +static void* APS5_VABI Handlers(void*) { + std::uintptr_t first[8]; + std::uintptr_t second[8]; + __pthread_cleanup_push_imp_nid_postfix(RecordOrder, reinterpret_cast(std::intptr_t{1}), first); + __pthread_cleanup_push_imp_nid_postfix(RecordOrder, reinterpret_cast(std::intptr_t{2}), second); + __pthread_cleanup_pop_imp_nid_postfix(0); + __pthread_cleanup_push_imp_nid_postfix(RecordOrder, reinterpret_cast(std::intptr_t{3}), second); + __pthread_cleanup_pop_imp_nid_postfix(1); + scePthreadExit(reinterpret_cast(std::intptr_t{7})); + return nullptr; +} + +static Pthread Start(PthreadEntry entry) { + shared.ready.store(false); + shared.release.store(false); + shared.returned.store(false); + shared.cleanups.store(0); + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, entry, nullptr, nullptr) == SCE_OK); + while (!shared.ready.load()) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + return thread; +} + +static void JoinCanceled(Pthread thread) { + void* result = nullptr; + Require(scePthreadJoin(thread, &result) == SCE_OK); + Require(result == CANCELED); + Require(!shared.returned.load()); +} + +int main() { + Require(scePthreadCancel(nullptr) == SCE_KERNEL_ERROR_ESRCH); + Require(pthread_cancel_nid_postfix(nullptr) == GUEST_ESRCH); + Require(scePthreadSetcancelstate(7, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadSetcanceltype(1, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(pthread_setcanceltype_nid_postfix(1, nullptr) == GUEST_EINVAL); + + Require(scePthreadMutexInit(&shared.mutex, nullptr, nullptr) == SCE_OK); + Require(scePthreadCondInit(&shared.cond, nullptr, nullptr) == SCE_OK); + Require(scePthreadSemInit(&shared.sem, 0, 0, nullptr) == SCE_OK); + Require(sem_init_nid_postfix(&shared.posixSem, 0, 0) == 0); + + Pthread thread = Start(CondWaiter); + Require(scePthreadMutexLock(&shared.mutex) == SCE_OK); + Require(scePthreadCancel(thread) == SCE_OK); + Require(shared.cleanups.load() == 0); + Require(scePthreadMutexUnlock(&shared.mutex) == SCE_OK); + JoinCanceled(thread); + Require(shared.cleanups.load() == 1); + Require(scePthreadMutexLock(&shared.mutex) == SCE_OK); + Require(scePthreadMutexUnlock(&shared.mutex) == SCE_OK); + + thread = Start(SemWaiter); + Require(scePthreadCancel(thread) == SCE_OK); + JoinCanceled(thread); + + thread = Start(PosixSemWaiter); + 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); + JoinCanceled(thread); + shared.release.store(true); + Require(scePthreadJoin(shared.target, nullptr) == SCE_OK); + + thread = Start(Sleeper); + Require(scePthreadCancel(thread) == SCE_OK); + JoinCanceled(thread); + + shared.survived.store(false); + thread = Start(Disabled); + Require(scePthreadCancel(thread) == SCE_OK); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + shared.release.store(true); + JoinCanceled(thread); + Require(shared.survived.load()); + + thread = Start(Asynchronous); + Require(scePthreadCancel(thread) == SCE_OK); + shared.release.store(true); + JoinCanceled(thread); + + shared.ready.store(true); + shared.cleanups.store(0); + void* result = nullptr; + Require(scePthreadCreate(&thread, nullptr, Handlers, nullptr, nullptr) == SCE_OK); + Require(scePthreadJoin(thread, &result) == SCE_OK); + Require(reinterpret_cast(result) == 7); + Require(shared.cleanups.load() == 2 && shared.order[0] == 3 && shared.order[1] == 1); + + Require(sem_destroy_nid_postfix(&shared.posixSem) == 0); + Require(scePthreadSemDestroy(&shared.sem) == SCE_OK); + Require(scePthreadCondDestroy(&shared.cond) == SCE_OK); + Require(scePthreadMutexDestroy(&shared.mutex) == SCE_OK); +} diff --git a/core/libs/tests/GuestPthreadCancelRecovery.cpp b/core/libs/tests/GuestPthreadCancelRecovery.cpp new file mode 100644 index 0000000000..297f761a92 --- /dev/null +++ b/core/libs/tests/GuestPthreadCancelRecovery.cpp @@ -0,0 +1,466 @@ +#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); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +int APS5_VABI scePthreadCancel(Pthread thread); +int APS5_VABI scePthreadSetcancelstate(int state, int* oldState); +int APS5_VABI scePthreadSetcanceltype(int type, int* oldType); +void APS5_VABI scePthreadTestcancel(); +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 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); +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 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); +int APS5_VABI scePthreadSemTimedwait(PthreadSem* sem, KernelUseconds usec); +int APS5_VABI scePthreadSemGetvalue(PthreadSem* sem, int* value); +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_reltimedwait_np_nid_postfix(void* sem, std::uint32_t usec); +int APS5_VABI sem_getvalue_nid_postfix(void* sem, int* value); +int APS5_VABI sceKernelUsleep_nid_postfix(KernelUseconds microseconds); +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); +} + +static constexpr int CANCEL_ENABLE = 0; +static constexpr int CANCEL_DISABLE = 1; +static constexpr int CANCEL_DEFERRED = 0; +static constexpr int KERNEL_EINVAL = static_cast(0x80020016); +static constexpr KernelUseconds WAIT_MICROSECONDS = 60000000; +static void* const CANCELED = reinterpret_cast(std::uintptr_t{1}); + +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 PthreadSem sem = nullptr; +static void* posixSem = nullptr; +static PthreadMutex mutex = nullptr; +static PthreadCond cond = nullptr; +static PthreadKey key = -1; +static Pthread target = nullptr; +static std::atomic ready{false}, release{false}, targetRelease{false}; +static std::atomic events{0}; +static std::array order{}; +static bool usePosix = false, useTimed = false, disabled = false; +static bool conditionReleased = false, returned = false; +static int stage = 0, normalResult = 0, sentinel = 0; +static void* joinResult = &sentinel; + +static void Signal(std::atomic& flag) { + flag.store(true); + flag.notify_all(); +} + +static void Gate() { + Signal(ready); + release.wait(false); +} + +static Pthread Start(PthreadEntry entry) { + ready = false; + release = false; + returned = false; + events = 0; + order = {}; + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, entry, nullptr, nullptr) == 0); + ready.wait(false); + return thread; +} + +static void Join(Pthread thread, void* expected) { + void* result = &sentinel; + Require(scePthreadJoin(thread, &result) == 0); + Require(result == expected); +} + +static void APS5_VABI Record(void* value) { + const auto index = events.fetch_add(1); + Require(index < static_cast(order.size())); + order[index] = static_cast(reinterpret_cast(value)); +} + +static void APS5_VABI CleanupWithPoints(void*) { + Record(reinterpret_cast(std::uintptr_t{2})); + scePthreadTestcancel(); + Require(sceKernelUsleep_nid_postfix(1) == 0); + Record(reinterpret_cast(std::uintptr_t{3})); +} + +static void APS5_VABI DestroySpecific(void* value) { + Require(value == &normalResult); + Record(reinterpret_cast(std::uintptr_t{4})); +} + +static void WaitForToken() { + if (usePosix) { + Require((useTimed ? sem_reltimedwait_np_nid_postfix(&posixSem, WAIT_MICROSECONDS) + : sem_wait_nid_postfix(&posixSem)) == 0); + } else { + Require((useTimed ? scePthreadSemTimedwait(&sem, WAIT_MICROSECONDS) : scePthreadSemWait(&sem)) == 0); + } +} + +static int TokenCount() { + int count = -1; + Require((usePosix ? sem_getvalue_nid_postfix(&posixSem, &count) : scePthreadSemGetvalue(&sem, &count)) == 0); + return count; +} + +static void* APS5_VABI PendingToken(void*) { + std::uintptr_t first[8], second[8]; + Require(scePthreadSetspecific(key, &normalResult) == 0); + __pthread_cleanup_push_imp_nid_postfix(Record, reinterpret_cast(std::uintptr_t{1}), first); + __pthread_cleanup_push_imp_nid_postfix(CleanupWithPoints, nullptr, second); + Gate(); + WaitForToken(); + returned = true; + return nullptr; +} + +static void CheckDefaults() { + int old = -1; + Require(scePthreadSetcancelstate(CANCEL_ENABLE, &old) == 0 && old == CANCEL_ENABLE); + Require(scePthreadSetcanceltype(CANCEL_DEFERRED, &old) == 0 && old == CANCEL_DEFERRED); + scePthreadTestcancel(); +} + +static void* APS5_VABI ConsumeToken(void*) { + CheckDefaults(); + WaitForToken(); + return &normalResult; +} + +static void PendingTokenRecovery() { + Require((usePosix ? sem_init_nid_postfix(&posixSem, 0, 1) : scePthreadSemInit(&sem, 0, 1, nullptr)) == 0); + const auto thread = Start(PendingToken); + Require(scePthreadCancel(thread) == 0); + Require(scePthreadCancel(thread) == 0); + Require(events == 0 && TokenCount() == 1); + Signal(release); + Join(thread, CANCELED); + Require(!returned && events == 4); + Require((order == std::array{2, 3, 1, 4})); + Require(TokenCount() == 1); + Pthread consumer = nullptr; + Require(scePthreadCreate(&consumer, nullptr, ConsumeToken, nullptr, nullptr) == 0); + Join(consumer, &normalResult); + Require(TokenCount() == 0); + Require((usePosix ? sem_destroy_nid_postfix(&posixSem) : scePthreadSemDestroy(&sem)) == 0); +} + +static void APS5_VABI Unlock(void*) { + Require(scePthreadMutexUnlock(&mutex) == 0); + Record(reinterpret_cast(std::uintptr_t{1})); +} + +static void* APS5_VABI ConditionWaiter(void*) { + CheckDefaults(); + if (disabled) Require(scePthreadSetcancelstate(CANCEL_DISABLE, nullptr) == 0); + Require(scePthreadMutexLock(&mutex) == 0); + std::uintptr_t info[8]; + __pthread_cleanup_push_imp_nid_postfix(Unlock, nullptr, info); + Signal(ready); + while (!conditionReleased) Require(scePthreadCondTimedwait(&cond, &mutex, WAIT_MICROSECONDS) == 0); + Require(scePthreadMutexUnlock(&mutex) == 0); + __pthread_cleanup_pop_imp_nid_postfix(0); + if (disabled) { + stage = 1; + scePthreadTestcancel(); + int old = -1; + Require(scePthreadSetcancelstate(CANCEL_ENABLE, &old) == 0 && old == CANCEL_DISABLE); + stage = 2; + scePthreadTestcancel(); + } + returned = true; + return &normalResult; +} + +static void ConditionRecovery() { + Require(scePthreadMutexInit(&mutex, nullptr, nullptr) == 0); + Require(scePthreadCondInit(&cond, nullptr, nullptr) == 0); + conditionReleased = false; + stage = 0; + const auto thread = Start(ConditionWaiter); + Require(scePthreadMutexLock(&mutex) == 0); + Require(scePthreadCancel(thread) == 0); + Require(events == 0); + if (disabled) { + conditionReleased = true; + Require(scePthreadCondSignal(&cond) == 0); + } + Require(scePthreadMutexUnlock(&mutex) == 0); + Join(thread, CANCELED); + Require(!returned); + Require(disabled ? stage == 2 && events == 0 : events == 1 && order[0] == 1); + disabled = false; + conditionReleased = false; + const auto next = Start(ConditionWaiter); + Require(scePthreadMutexLock(&mutex) == 0); + conditionReleased = true; + Require(scePthreadCondSignal(&cond) == 0); + Require(scePthreadMutexUnlock(&mutex) == 0); + Join(next, &normalResult); + Require(returned && events == 0); + Require(scePthreadCondDestroy(&cond) == 0); + 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); + return &normalResult; +} + +static void* APS5_VABI PendingJoin(void*) { + std::uintptr_t info[8]; + __pthread_cleanup_push_imp_nid_postfix(Record, reinterpret_cast(std::uintptr_t{1}), info); + Gate(); + Require(scePthreadJoin(target, &joinResult) == 0); + returned = true; + return nullptr; +} + +static void JoinRecovery() { + targetRelease = false; + joinResult = &sentinel; + Require(scePthreadCreate(&target, nullptr, HeldTarget, nullptr, nullptr) == 0); + const auto joiner = Start(PendingJoin); + Require(scePthreadCancel(joiner) == 0); + Signal(release); + Join(joiner, CANCELED); + Require(!returned && joinResult == &sentinel && events == 1 && order[0] == 1); + Signal(targetRelease); + Join(target, &normalResult); + Require(scePthreadCreate(&target, nullptr, HeldTarget, nullptr, nullptr) == 0); + Join(target, &normalResult); +} + +int main() { + int old = 17; + Require(scePthreadSetcancelstate(CANCEL_DISABLE, nullptr) == 0); + Require(scePthreadSetcancelstate(7, &old) == KERNEL_EINVAL && old == 17); + Require(scePthreadSetcancelstate(CANCEL_ENABLE, &old) == 0 && old == CANCEL_DISABLE); + old = 17; + Require(scePthreadSetcanceltype(1, &old) == KERNEL_EINVAL && old == 17); + Require(scePthreadSetcanceltype(CANCEL_DEFERRED, &old) == 0 && old == CANCEL_DEFERRED); + Require(scePthreadKeyCreate(&key, reinterpret_cast(DestroySpecific)) == 0); + for (bool posix : {false, true}) { + usePosix = posix; + for (bool timed : {false, true}) { + useTimed = timed; + PendingTokenRecovery(); + } + } + 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/GuestPthreadDestructorLifetime.cpp b/core/libs/tests/GuestPthreadDestructorLifetime.cpp new file mode 100644 index 0000000000..4bddcb951c --- /dev/null +++ b/core/libs/tests/GuestPthreadDestructorLifetime.cpp @@ -0,0 +1,96 @@ +#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); +int APS5_VABI scePthreadCancel(Pthread thread); +void APS5_VABI scePthreadExit(void* retval); +Pthread APS5_VABI scePthreadSelf(); +int APS5_VABI scePthreadGetthreadid(); +int APS5_VABI scePthreadGetname(Pthread thread, char* name); +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 sceKernelUsleep_nid_postfix(KernelUseconds microseconds); +} + +struct Context { + PthreadKey key = -1; + Pthread expected = nullptr; + int expectedId = 0; + unsigned calls = 0; + unsigned failures = 0; + bool exitExplicitly = false; + bool pendingCancel = false; + + void Check(bool passed, const char* message) { + if (passed) return; + std::fprintf(stderr, "%s: %s\n", exitExplicitly ? "explicit exit" : "return", message); + ++failures; + } +}; + +static void APS5_VABI DestroyValue(void* value) { + auto& context = *static_cast(value); + ++context.calls; + context.Check(scePthreadGetspecific(context.key) == nullptr, "key value was not cleared before its destructor"); + if (context.expected) { + const auto self = scePthreadSelf(); + context.Check(self == context.expected, "key destructor lost the exiting thread identity"); + context.Check(scePthreadGetthreadid() == context.expectedId, "key destructor lost the exiting thread ID"); + char name[32]{}; + context.Check(scePthreadGetname(self, name) == 0 && std::strcmp(name, "KeyDestructor") == 0, + "key destructor lost the exiting thread name"); + context.Check(sceKernelUsleep_nid_postfix(1000) == 0, "key destructor could not wait"); + } + context.Check(scePthreadSetspecific(context.key, value) == 0, "key destructor could not restore its value"); +} + +static void* APS5_VABI Worker(void* value) { + auto& context = *static_cast(value); + context.expected = scePthreadSelf(); + context.expectedId = scePthreadGetthreadid(); + context.Check(scePthreadSetspecific(context.key, value) == 0, "worker could not bind its key value"); + if (context.pendingCancel) context.Check(scePthreadCancel(context.expected) == 0, "worker could not request cancellation"); + if (context.exitExplicitly) scePthreadExit(value); + return value; +} + +static bool RunGuest(bool exitExplicitly, bool pendingCancel = false) { + Context context; + context.exitExplicitly = exitExplicitly; + context.pendingCancel = pendingCancel; + if (scePthreadKeyCreate(&context.key, reinterpret_cast(DestroyValue)) != 0) return false; + Pthread thread = nullptr; + if (scePthreadCreate(&thread, nullptr, Worker, &context, "KeyDestructor") != 0) return false; + void* result = nullptr; + context.Check(scePthreadJoin(thread, &result) == 0 && result == &context, "thread did not join with its return value"); + context.Check(context.calls == 4, "key destructor did not stop after four iterations"); + context.Check(scePthreadKeyDelete(context.key) == 0, "key deletion failed"); + return context.failures == 0; +} + +static bool RunHost() { + Context context; + if (scePthreadKeyCreate(&context.key, reinterpret_cast(DestroyValue)) != 0) return false; + std::thread worker([&] { + context.Check(scePthreadSetspecific(context.key, &context) == 0, "host worker could not bind its key value"); + }); + worker.join(); + context.Check(context.calls == 4, "host thread did not run four key destructor iterations"); + context.Check(scePthreadKeyDelete(context.key) == 0, "host key deletion failed"); + return context.failures == 0; +} + +int main() { + const bool returned = RunGuest(false); + const bool exited = RunGuest(true); + const bool canceled = RunGuest(false, true); + const bool host = RunHost(); + if (!returned || !exited || !canceled || !host) return 1; + std::puts("PASS: pthread key destructors retain thread identity on return and explicit exit; pending cancellation, four iterations and host cleanup preserved"); +} 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 new file mode 100644 index 0000000000..2fec26bef7 --- /dev/null +++ b/core/libs/tests/GuestPthreadJoinSelf.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); +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; +static constexpr int SCE_KERNEL_ERROR_EDEADLK = static_cast(0x8002000Bu); +static constexpr int GUEST_EDEADLK = 11; + +static void Require(bool value) { if (!value) std::abort(); } + +static void* APS5_VABI JoinSelf(void*) { + void* result = reinterpret_cast(std::uintptr_t{0x55}); + Require(scePthreadJoin(scePthreadSelf(), &result) == SCE_KERNEL_ERROR_EDEADLK); + Require(pthread_join_nid_postfix(scePthreadSelf(), &result) == GUEST_EDEADLK); + Require(result == reinterpret_cast(std::uintptr_t{0x55})); + 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; + Require(scePthreadCreate(&thread, nullptr, JoinSelf, nullptr, "join-self") == SCE_OK); + Require(scePthreadJoin(thread, &result) == SCE_OK); + Require(result == reinterpret_cast(std::uintptr_t{0x1234})); + + PthreadAttr unset = nullptr; + thread = nullptr; + result = nullptr; + 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/GuestPthreadKey.cpp b/core/libs/tests/GuestPthreadKey.cpp index e9bdd16534..ae87651a61 100644 --- a/core/libs/tests/GuestPthreadKey.cpp +++ b/core/libs/tests/GuestPthreadKey.cpp @@ -10,6 +10,8 @@ int APS5_VABI scePthreadKeyCreate(PthreadKey* key, pthread_key_destructor_func_t int APS5_VABI scePthreadKeyDelete(PthreadKey key); void* APS5_VABI scePthreadGetspecific(PthreadKey key); int APS5_VABI scePthreadSetspecific(PthreadKey key, void* value); +int APS5_VABI pthread_key_create_nid_postfix(PthreadKey* key, pthread_key_destructor_func_t destructor); +int APS5_VABI pthread_key_delete_nid_postfix(PthreadKey key); } static constexpr int SCE_OK = 0; @@ -62,8 +64,16 @@ static void* APS5_VABI HoldAcrossReuse(void*) { } int main() { + PthreadKey posixKey = 0; + Require(pthread_key_create_nid_postfix(&posixKey, nullptr) == 0 && posixKey != 0); + Require(scePthreadSetspecific(0, &first) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadGetspecific(0) == nullptr); + Require(scePthreadKeyDelete(0) == SCE_KERNEL_ERROR_EINVAL); + Require(pthread_key_delete_nid_postfix(posixKey) == 0); + PthreadKey key = -1; Require(scePthreadKeyCreate(&key, nullptr) == SCE_OK); + Require(key != 0); Require(scePthreadGetspecific(key) == nullptr); Require(scePthreadSetspecific(key, &first) == SCE_OK); Require(scePthreadGetspecific(key) == &first); diff --git a/core/libs/tests/GuestPthreadMutexErrors.cpp b/core/libs/tests/GuestPthreadMutexErrors.cpp new file mode 100644 index 0000000000..064469d42d --- /dev/null +++ b/core/libs/tests/GuestPthreadMutexErrors.cpp @@ -0,0 +1,50 @@ +#include "SceTypes.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI scePthreadMutexattrInit(PthreadMutexattr* attr); +int APS5_VABI scePthreadMutexattrDestroy(PthreadMutexattr* attr); +int APS5_VABI scePthreadMutexattrSettype(PthreadMutexattr* attr, int type); +int APS5_VABI scePthreadMutexattrSetprotocol(PthreadMutexattr* attr, int protocol); +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 scePthreadMutexUnlock(PthreadMutex* mutex); +} + +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 void Require(bool value) { if (!value) std::abort(); } + +int main() { + PthreadMutexattr attr = nullptr; + Require(scePthreadMutexattrInit(&attr) == SCE_OK); + for (int type : {-1, 0, 5}) Require(scePthreadMutexattrSettype(&attr, type) == SCE_KERNEL_ERROR_EINVAL); + for (int type : {1, 2, 3, 4}) Require(scePthreadMutexattrSettype(&attr, type) == SCE_OK); + for (int protocol : {-1, 3}) Require(scePthreadMutexattrSetprotocol(&attr, protocol) == SCE_KERNEL_ERROR_EINVAL); + for (int protocol : {0, 1, 2}) Require(scePthreadMutexattrSetprotocol(&attr, protocol) == SCE_OK); + + PthreadMutexattr unset = nullptr; + Require(scePthreadMutexattrSettype(&unset, 1) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadMutexattrSetprotocol(&unset, 0) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadMutexattrDestroy(&unset) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadMutexattrSettype(nullptr, 1) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadMutexattrDestroy(nullptr) == SCE_KERNEL_ERROR_EINVAL); + + PthreadMutex mutex = nullptr; + Require(scePthreadMutexInit(&mutex, &unset, "unset") == SCE_OK); + Require(scePthreadMutexLock(&mutex) == SCE_OK); + Require(scePthreadMutexDestroy(&mutex) == SCE_KERNEL_ERROR_EBUSY); + Require(scePthreadMutexUnlock(&mutex) == SCE_OK); + Require(scePthreadMutexDestroy(&mutex) == SCE_OK); + Require(mutex == reinterpret_cast(std::uintptr_t{2})); + Require(scePthreadMutexDestroy(&mutex) == SCE_KERNEL_ERROR_EINVAL); + + Require(scePthreadMutexInit(&mutex, &attr, "attr") == SCE_OK); + Require(scePthreadMutexDestroy(&mutex) == SCE_OK); + Require(scePthreadMutexattrDestroy(&attr) == SCE_OK); +} diff --git a/core/libs/tests/GuestPthreadMutexattr.cpp b/core/libs/tests/GuestPthreadMutexattr.cpp new file mode 100644 index 0000000000..48dd291a5b --- /dev/null +++ b/core/libs/tests/GuestPthreadMutexattr.cpp @@ -0,0 +1,165 @@ +#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); +int APS5_VABI pthread_mutexattr_gettype_nid_postfix(const PthreadMutexattr* attr, int* type); +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); +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 sched_get_priority_max_nid_postfix(int policy); +} + +static constexpr int GUEST_EINVAL = 22; +static constexpr int PRIO_NONE = 0; +static constexpr int PRIO_INHERIT = 1; +static constexpr int PRIO_PROTECT = 2; +static constexpr int SCHED_RR = 3; + +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; +} + +static constexpr KernelTimespec INVALID_DEADLINE{0, 1000000000}; + +static void* APS5_VABI TimedlockInvalidDeadline(void* arg) { + auto* mutex = static_cast(arg); + return reinterpret_cast(static_cast(pthread_mutex_timedlock_nid_postfix(mutex, &INVALID_DEADLINE))); +} + +static void CheckTimedlockInvalidDeadline() { + PthreadMutex mutex = nullptr; + Require(pthread_mutex_init_nid_postfix(&mutex, nullptr) == 0); + Require(pthread_mutex_timedlock_nid_postfix(&mutex, &INVALID_DEADLINE) == 0); + Pthread thread{}; + void* result = nullptr; + Require(scePthreadCreate(&thread, nullptr, TimedlockInvalidDeadline, &mutex, nullptr) == 0); + Require(scePthreadJoin(thread, &result) == 0 && reinterpret_cast(result) == GUEST_EINVAL); + Require(pthread_mutex_unlock_nid_postfix(&mutex) == 0); + Require(pthread_mutex_destroy_nid_postfix(&mutex) == 0); +} + +static void CheckType() { + PthreadMutexattr attr = nullptr; + Require(pthread_mutexattr_init_nid_postfix(&attr) == 0); + int type = -1; + Require(pthread_mutexattr_gettype_nid_postfix(&attr, &type) == 0 && type == 1); + for (const int set : {2, 3, 4, 1}) { + Require(pthread_mutexattr_settype_nid_postfix(&attr, set) == 0); + Require(pthread_mutexattr_gettype_nid_postfix(&attr, &type) == 0 && type == set); + } + Require(pthread_mutexattr_settype_nid_postfix(&attr, 5) == GUEST_EINVAL); + Require(pthread_mutexattr_gettype_nid_postfix(&attr, &type) == 0 && type == 1); + Require(Throws([&] { pthread_mutexattr_gettype_nid_postfix(&attr, nullptr); })); + Require(pthread_mutexattr_destroy_nid_postfix(&attr) == 0); + type = -1; + Require(pthread_mutexattr_gettype_nid_postfix(&attr, &type) == GUEST_EINVAL && type == -1); + 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); + PthreadMutex mutex = nullptr; + Require(pthread_mutex_init_nid_postfix(&mutex, &attr) == 0); + Require(pthread_mutex_lock_nid_postfix(&mutex) == 0); + 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); + CheckInvalidPriorityAttribute(&attr); + return 0; +} diff --git a/core/libs/tests/GuestPthreadRename.cpp b/core/libs/tests/GuestPthreadRename.cpp new file mode 100644 index 0000000000..e3fdbd18b1 --- /dev/null +++ b/core/libs/tests/GuestPthreadRename.cpp @@ -0,0 +1,66 @@ +#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(); +int APS5_VABI scePthreadGetname(Pthread thread, char* name); +int APS5_VABI pthread_rename_np_nid_postfix(Pthread thread, const char* name); +int* APS5_VABI __error_nid_postfix(); +} + +static void Require(bool value) { if (!value) std::abort(); } + +static void CheckStoredName(Pthread thread, const std::string& expected) { + std::array buffer; + buffer.fill(0xaa); + Require(scePthreadGetname(thread, reinterpret_cast(buffer.data())) == 0); + Require(std::memcmp(buffer.data(), expected.c_str(), expected.size() + 1) == 0); + for (std::size_t i = expected.size() + 1; i < buffer.size(); ++i) Require(buffer[i] == 0xaa); +} + +static void CheckThreadRename(Pthread thread) { + const int savedError = *__error_nid_postfix(); + const std::string names[] = {"", "A", "AnyPS5Probe", "0123456789012345678901234567890"}; + for (const auto& name : names) { + *__error_nid_postfix() = 77; + Require(pthread_rename_np_nid_postfix(thread, name.c_str()) == 0); + Require(*__error_nid_postfix() == 77); + CheckStoredName(thread, name); + } + for (const std::size_t length : {32u, 40u, 1024u}) { + const std::string name(length, 'x'); + *__error_nid_postfix() = 77; + const int error = pthread_rename_np_nid_postfix(thread, name.c_str()); + if (error != 63) { + std::fprintf(stderr, "pthread_rename_np: %zu-byte name returned %d; expected ENAMETOOLONG=63\n", length, error); + std::abort(); + } + Require(*__error_nid_postfix() == 77); + CheckStoredName(thread, names[3]); + } + Require(pthread_rename_np_nid_postfix(thread, "renamed") == 0); + CheckStoredName(thread, "renamed"); + *__error_nid_postfix() = savedError; +} + +static void* APS5_VABI Worker(void*) { + const Pthread self = scePthreadSelf(); + Require(self != nullptr); + CheckThreadRename(self); + return nullptr; +} + +int main() { + const Pthread self = scePthreadSelf(); + Require(self != nullptr); + CheckThreadRename(self); + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, Worker, nullptr, "rename-test") == 0); + Require(scePthreadJoin(thread, nullptr) == 0); +} diff --git a/core/libs/tests/GuestPthreadRwlock.cpp b/core/libs/tests/GuestPthreadRwlock.cpp index 915d7d1ab1..b7f54df37e 100644 --- a/core/libs/tests/GuestPthreadRwlock.cpp +++ b/core/libs/tests/GuestPthreadRwlock.cpp @@ -11,6 +11,11 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread int APS5_VABI scePthreadJoin(Pthread thread, void** retval); int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp); 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); @@ -18,20 +23,31 @@ 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 scePthreadRwlockInit(PthreadRwlock* rwlock, const PthreadRwlockattr* attr, const char* name); +int APS5_VABI scePthreadRwlockDestroy(PthreadRwlock* rwlock); +int APS5_VABI scePthreadRwlockRdlock(PthreadRwlock* rwlock); +int APS5_VABI scePthreadRwlockWrlock(PthreadRwlock* rwlock); +int APS5_VABI scePthreadRwlockTryrdlock(PthreadRwlock* rwlock); +int APS5_VABI scePthreadRwlockUnlock(PthreadRwlock* rwlock); } using TimedLock = int (APS5_VABI *)(PthreadRwlock*, const KernelTimespec*); static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_EINVAL = 0x80020016; +static constexpr int GUEST_EPERM = 1; static constexpr int GUEST_EDEADLK = 11; 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(); } +static const PthreadRwlock DestroyedRwlock = reinterpret_cast(std::uintptr_t{1}); + static KernelTimespec After(std::int64_t millis) { KernelTimespec now{}; Require(clock_gettime_nid_postfix(GUEST_REALTIME_CLOCK, &now) == 0); @@ -75,7 +91,269 @@ static void ExpectTimeout(TimedLock lock, PthreadRwlock* rwlock) { Require(lock(rwlock, &negative) == GUEST_EINVAL); } +struct FirstLockRace { + PthreadRwlock rwlock = nullptr; + std::atomic ready{0}; + std::atomic inside{0}; + std::atomic overlaps{0}; +}; + +static void* APS5_VABI FirstWrlock(void* arg) { + auto& race = *static_cast(arg); + race.ready.fetch_add(1); + while (race.ready.load() < 2) {} + Require(pthread_rwlock_wrlock_nid_postfix(&race.rwlock) == 0); + if (race.inside.fetch_add(1) != 0) race.overlaps.fetch_add(1); + for (int spin = 0; spin < 2000; ++spin) std::atomic_signal_fence(std::memory_order_seq_cst); + race.inside.fetch_sub(1); + Require(pthread_rwlock_unlock_nid_postfix(&race.rwlock) == 0); + return nullptr; +} + +static void CheckConcurrentFirstWrlockExcludes() { + int overlaps = 0; + for (int round = 0; round < 500; ++round) { + FirstLockRace race; + Pthread first = nullptr; + Pthread second = nullptr; + Require(scePthreadCreate(&first, nullptr, FirstWrlock, &race, nullptr) == SCE_OK); + Require(scePthreadCreate(&second, nullptr, FirstWrlock, &race, nullptr) == SCE_OK); + Require(scePthreadJoin(first, nullptr) == SCE_OK); + Require(scePthreadJoin(second, nullptr) == SCE_OK); + overlaps += race.overlaps.load(); + Require(pthread_rwlock_destroy_nid_postfix(&race.rwlock) == 0); + } + Require(overlaps == 0); +} + +struct HandoffState { + PthreadRwlock rwlock = nullptr; + std::atomic firstHeld{false}; + std::atomic firstRelease{false}; + std::atomic secondAcquired{false}; + std::atomic secondRelease{false}; + Pthread firstThread = nullptr; + Pthread secondThread = nullptr; +}; + +static void* APS5_VABI HandoffFirst(void* arg) { + auto& state = *static_cast(arg); + Require(pthread_rwlock_wrlock_nid_postfix(&state.rwlock) == 0); + state.firstHeld.store(true); + while (!state.firstRelease.load()) std::this_thread::yield(); + Require(pthread_rwlock_unlock_nid_postfix(&state.rwlock) == 0); + return nullptr; +} + +static void* APS5_VABI HandoffSecond(void* arg) { + auto& state = *static_cast(arg); + while (!state.firstHeld.load()) std::this_thread::yield(); + Require(pthread_rwlock_wrlock_nid_postfix(&state.rwlock) == 0); + state.secondAcquired.store(true); + while (!state.secondRelease.load()) std::this_thread::yield(); + Require(pthread_rwlock_unlock_nid_postfix(&state.rwlock) == 0); + return nullptr; +} + +static void CheckWriterHandoff() { + HandoffState state; + Require(scePthreadCreate(&state.firstThread, nullptr, HandoffFirst, &state, nullptr) == SCE_OK); + Require(scePthreadCreate(&state.secondThread, nullptr, HandoffSecond, &state, nullptr) == SCE_OK); + while (!state.firstHeld.load()) std::this_thread::yield(); + Require(state.rwlock != nullptr); + state.firstRelease.store(true); + Require(scePthreadJoin(state.firstThread, nullptr) == SCE_OK); + while (!state.secondAcquired.load()) std::this_thread::yield(); + Require(state.rwlock != nullptr); + state.secondRelease.store(true); + Require(scePthreadJoin(state.secondThread, nullptr) == SCE_OK); + Require(pthread_rwlock_destroy_nid_postfix(&state.rwlock) == 0); + Require(state.rwlock == DestroyedRwlock); +} + +static void CheckDestroyLifecycle() { + PthreadRwlock held = nullptr; + Require(scePthreadRwlockInit(&held, nullptr, nullptr) == SCE_OK); + Require(scePthreadRwlockWrlock(&held) == SCE_OK); + Require(scePthreadRwlockDestroy(&held) == SCE_OK); + Require(held == DestroyedRwlock); + Require(scePthreadRwlockUnlock(&held) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadRwlockRdlock(&held) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadRwlockTryrdlock(&held) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadRwlockDestroy(&held) == SCE_KERNEL_ERROR_EINVAL); + Require(pthread_rwlock_rdlock_nid_postfix(&held) == GUEST_EINVAL); + Require(held == DestroyedRwlock); + Require(scePthreadRwlockInit(&held, nullptr, nullptr) == SCE_OK); + Require(held != nullptr && held != DestroyedRwlock); + Require(scePthreadRwlockRdlock(&held) == SCE_OK); + Require(scePthreadRwlockRdlock(&held) == SCE_OK); + Require(scePthreadRwlockDestroy(&held) == SCE_OK); + Require(held == DestroyedRwlock); + + PthreadRwlock zero = nullptr; + Require(scePthreadRwlockDestroy(&zero) == SCE_OK); + Require(zero == nullptr); + Require(scePthreadRwlockRdlock(&zero) == SCE_OK); + Require(zero != nullptr && zero != DestroyedRwlock); + Require(scePthreadRwlockUnlock(&zero) == SCE_OK); + Require(scePthreadRwlockDestroy(&zero) == SCE_OK); + Require(zero == DestroyedRwlock); +} + +static void CheckFailedLockAttempts() { + PthreadRwlock rwlock = nullptr; + Require(pthread_rwlock_wrlock_nid_postfix(&rwlock) == 0); + const KernelTimespec timeout = After(10); + Require(pthread_rwlock_timedrdlock_nid_postfix(&rwlock, &timeout) == GUEST_EDEADLK); + Require(pthread_rwlock_tryrdlock_nid_postfix(&rwlock) == GUEST_EBUSY); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); + Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == 0); + Require(rwlock == DestroyedRwlock); + rwlock = nullptr; + + Holder reader{&rwlock, false}; + Start(reader); + const KernelTimespec past = After(-1000); + Require(pthread_rwlock_timedwrlock_nid_postfix(&rwlock, &past) == GUEST_ETIMEDOUT); + Require(pthread_rwlock_trywrlock_nid_postfix(&rwlock) == GUEST_EBUSY); + reader.release.store(true); + Require(scePthreadJoin(reader.thread, nullptr) == SCE_OK); + Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == 0); + Require(rwlock == DestroyedRwlock); +} + +static void CheckDeadlinesBeyondTheClockRange() { + PthreadRwlock rwlock = nullptr; + Holder reader{&rwlock, false}; + Start(reader); + const KernelTimespec farPast{INT64_MIN, 0}; + Require(pthread_rwlock_timedwrlock_nid_postfix(&rwlock, &farPast) == GUEST_ETIMEDOUT); + reader.release.store(true); + const KernelTimespec farFuture{INT64_MAX, 0}; + Require(pthread_rwlock_timedwrlock_nid_postfix(&rwlock, &farFuture) == 0); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); + Require(scePthreadJoin(reader.thread, nullptr) == SCE_OK); + Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == 0); +} + +struct WaitingWriter { + PthreadRwlock* rwlock; + std::atomic started{false}; + std::atomic acquired{false}; + Pthread thread = nullptr; +}; + +static void* APS5_VABI WriteOnceStarted(void* arg) { + auto& writer = *static_cast(arg); + writer.started.store(true); + Require(pthread_rwlock_wrlock_nid_postfix(writer.rwlock) == 0); + writer.acquired.store(true); + Require(pthread_rwlock_unlock_nid_postfix(writer.rwlock) == 0); + return nullptr; +} + +static void StartWaitingWriter(WaitingWriter& writer) { + Require(scePthreadCreate(&writer.thread, nullptr, WriteOnceStarted, &writer, nullptr) == SCE_OK); + while (!writer.started.load()) std::this_thread::yield(); +} + +static int TryReadUntilRefused(PthreadRwlock* rwlock) { + for (int attempt = 0; attempt < 2000; ++attempt) { + const int result = pthread_rwlock_tryrdlock_nid_postfix(rwlock); + if (result != 0) return result; + Require(pthread_rwlock_unlock_nid_postfix(rwlock) == 0); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + return 0; +} + +struct RefusalProbe { + PthreadRwlock* rwlock; + std::atomic result{-1}; +}; + +static void* APS5_VABI ProbeRefusal(void* arg) { + auto& probe = *static_cast(arg); + probe.result.store(TryReadUntilRefused(probe.rwlock)); + return nullptr; +} + +static void CheckWaitingWriterHoldsBackNewReaders() { + PthreadRwlock rwlock = nullptr; + Holder reader{&rwlock, false}; + Start(reader); + WaitingWriter writer{&rwlock}; + StartWaitingWriter(writer); + Require(TryReadUntilRefused(&rwlock) == GUEST_EBUSY); + const KernelTimespec soon = After(20); + Require(pthread_rwlock_timedrdlock_nid_postfix(&rwlock, &soon) == GUEST_ETIMEDOUT); + Require(!writer.acquired.load()); + reader.release.store(true); + Require(scePthreadJoin(reader.thread, nullptr) == SCE_OK); + Require(scePthreadJoin(writer.thread, nullptr) == SCE_OK); + Require(writer.acquired.load()); + Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == 0); + + PthreadRwlock recursive = nullptr; + Require(pthread_rwlock_rdlock_nid_postfix(&recursive) == 0); + WaitingWriter queued{&recursive}; + StartWaitingWriter(queued); + RefusalProbe probe{&recursive}; + Pthread prober = nullptr; + Require(scePthreadCreate(&prober, nullptr, ProbeRefusal, &probe, nullptr) == SCE_OK); + Require(scePthreadJoin(prober, nullptr) == SCE_OK); + Require(probe.result.load() == GUEST_EBUSY); + Require(pthread_rwlock_rdlock_nid_postfix(&recursive) == 0); + Require(pthread_rwlock_unlock_nid_postfix(&recursive) == 0); + Require(!queued.acquired.load()); + Require(pthread_rwlock_unlock_nid_postfix(&recursive) == 0); + Require(scePthreadJoin(queued.thread, nullptr) == SCE_OK); + Require(queued.acquired.load()); + Require(pthread_rwlock_destroy_nid_postfix(&recursive) == 0); +} + +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(); const KernelTimespec invalid{0, NANOS_PER_SECOND}; PthreadRwlock fresh[4] = {}; Require(pthread_rwlock_tryrdlock_nid_postfix(&fresh[0]) == 0); @@ -88,6 +366,17 @@ int main() { Require(pthread_rwlock_destroy_nid_postfix(&lock) == 0); } + PthreadRwlockattr attr = nullptr; + Require(pthread_rwlockattr_init_nid_postfix(&attr) == 0); + Require(attr != nullptr); + PthreadRwlock withAttr = nullptr; + Require(pthread_rwlock_init_nid_postfix(&withAttr, &attr) == 0); + Require(pthread_rwlock_wrlock_nid_postfix(&withAttr) == 0); + Require(pthread_rwlock_tryrdlock_nid_postfix(&withAttr) == GUEST_EBUSY); + Require(pthread_rwlock_unlock_nid_postfix(&withAttr) == 0); + Require(pthread_rwlock_destroy_nid_postfix(&withAttr) == 0); + Require(pthread_rwlockattr_destroy_nid_postfix(&attr) == 0); + PthreadRwlock rwlock = nullptr; Require(pthread_rwlock_trywrlock_nid_postfix(&rwlock) == 0); Require(pthread_rwlock_tryrdlock_nid_postfix(&rwlock) == GUEST_EBUSY); @@ -96,6 +385,12 @@ int main() { Require(pthread_rwlock_timedrdlock_nid_postfix(&rwlock, &deadline) == GUEST_EDEADLK); Require(pthread_rwlock_timedwrlock_nid_postfix(&rwlock, &deadline) == GUEST_EDEADLK); Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == GUEST_EPERM); + Require(pthread_rwlock_tryrdlock_nid_postfix(&rwlock) == 0); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == GUEST_EPERM); + Require(pthread_rwlock_trywrlock_nid_postfix(&rwlock) == 0); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); Holder reader{&rwlock, false}; Start(reader); @@ -113,6 +408,7 @@ int main() { Holder writer{&rwlock, true}; Start(writer); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == GUEST_EPERM); Require(pthread_rwlock_tryrdlock_nid_postfix(&rwlock) == GUEST_EBUSY); Require(pthread_rwlock_trywrlock_nid_postfix(&rwlock) == GUEST_EBUSY); ExpectTimeout(pthread_rwlock_timedrdlock_nid_postfix, &rwlock); @@ -122,10 +418,56 @@ int main() { Require(pthread_rwlock_timedrdlock_nid_postfix(&rwlock, &deadline) == 0); Require(scePthreadJoin(writer.thread, nullptr) == SCE_OK); 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); + + PthreadRwlock unheld = nullptr; + Require(scePthreadRwlockInit(&unheld, nullptr, nullptr) == SCE_OK); + Require(unheld != nullptr); + Require(pthread_rwlock_destroy_nid_postfix(&unheld) == 0); + Require(unheld == DestroyedRwlock); + + PthreadRwlock rd = nullptr; + Require(pthread_rwlock_rdlock_nid_postfix(&rd) == 0); + Require(pthread_rwlock_destroy_nid_postfix(&rd) == 0); + Require(rd == DestroyedRwlock); + Require(pthread_rwlock_unlock_nid_postfix(&rd) == GUEST_EINVAL); + Require(pthread_rwlock_tryrdlock_nid_postfix(&rd) == GUEST_EINVAL); + Require(pthread_rwlock_destroy_nid_postfix(&rd) == GUEST_EINVAL); + Require(rd == DestroyedRwlock); + + PthreadRwlock wr = nullptr; + Require(pthread_rwlock_wrlock_nid_postfix(&wr) == 0); + Require(pthread_rwlock_destroy_nid_postfix(&wr) == 0); + Require(wr == DestroyedRwlock); + Require(pthread_rwlock_unlock_nid_postfix(&wr) == GUEST_EINVAL); + Require(pthread_rwlock_trywrlock_nid_postfix(&wr) == GUEST_EINVAL); + Require(pthread_rwlock_destroy_nid_postfix(&wr) == GUEST_EINVAL); + Require(wr == DestroyedRwlock); + + PthreadRwlock remoteRd = nullptr; + Holder bgReader{&remoteRd, false}; + Start(bgReader); + Require(remoteRd != nullptr); + bgReader.release.store(true); + Require(scePthreadJoin(bgReader.thread, nullptr) == SCE_OK); + Require(pthread_rwlock_destroy_nid_postfix(&remoteRd) == 0); + Require(remoteRd == DestroyedRwlock); + + PthreadRwlock remoteWr = nullptr; + Holder bgWriter{&remoteWr, true}; + Start(bgWriter); + Require(remoteWr != nullptr); + bgWriter.release.store(true); + Require(scePthreadJoin(bgWriter.thread, nullptr) == SCE_OK); + Require(pthread_rwlock_destroy_nid_postfix(&remoteWr) == 0); + Require(remoteWr == DestroyedRwlock); + + CheckWriterHandoff(); + CheckFailedLockAttempts(); + CheckDestroyLifecycle(); } diff --git a/core/libs/tests/GuestPthreadSchedparam.cpp b/core/libs/tests/GuestPthreadSchedparam.cpp new file mode 100644 index 0000000000..171d3fc5be --- /dev/null +++ b/core/libs/tests/GuestPthreadSchedparam.cpp @@ -0,0 +1,216 @@ +#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); +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 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{}; + Require(scePthreadGetschedparam(scePthreadSelf(), ¤t, ¶m) == SCE_OK); + Require(current == policy && param.sched_priority == priority); + current = -1; + param.sched_priority = -1; + Require(pthread_getschedparam_nid_postfix(scePthreadSelf(), ¤t, ¶m) == 0); + Require(current == policy && param.sched_priority == priority); + RequireAttributes(scePthreadSelf(), policy, priority); +} + +static int SceSet(int policy, int priority) { + const KernelSchedParam param{priority}; + return scePthreadSetschedparam(scePthreadSelf(), policy, ¶m); +} + +static int PosixSet(int policy, int priority) { + const KernelSchedParam param{priority}; + return pthread_setschedparam_nid_postfix(scePthreadSelf(), policy, ¶m); +} + +static void* APS5_VABI Run(void*) { + int policy = -1; + KernelSchedParam param{}; + Require(scePthreadGetschedparam(scePthreadSelf(), &policy, ¶m) == SCE_OK); + Require(policy == GUEST_SCHED_FIFO); + + for (int priority : {256, 767, 896, 959}) { + 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(GUEST_SCHED_FIFO, priority) == SCE_KERNEL_ERROR_EINVAL); + Require(PosixSet(GUEST_SCHED_FIFO, priority) == GUEST_EINVAL); + RequireSched(GUEST_SCHED_FIFO, 959); + } + + 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(GUEST_SCHED_RR, 300); + } + 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); + RequireAttributes(scePthreadSelf(), policy, param.sched_priority); + request.ready.set_value(policy); + request.released.wait(); + return nullptr; +} + +static int CreatedPolicy(int inheritSched, int policy) { + PthreadAttr attr = nullptr; + Require(scePthreadAttrInit(&attr) == SCE_OK); + Require(scePthreadAttrSetinheritsched(&attr, inheritSched) == SCE_OK); + Require(scePthreadAttrSetschedpolicy(&attr, policy) == SCE_OK); + std::promise release; + PolicyRequest request{{}, release.get_future()}; + auto ready = request.ready.get_future(); + Pthread thread = nullptr; + 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); +} 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/GuestPthreadSem.cpp b/core/libs/tests/GuestPthreadSem.cpp index 001fe44fe5..574228ddf4 100644 --- a/core/libs/tests/GuestPthreadSem.cpp +++ b/core/libs/tests/GuestPthreadSem.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -20,10 +21,39 @@ int APS5_VABI scePthreadSemGetvalue(PthreadSem* sem, int* value); static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_EBUSY = static_cast(0x80020010); +static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016u); +static constexpr int SCE_KERNEL_ERROR_EOVERFLOW = static_cast(0x80020054u); static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = static_cast(0x8002003C); static void Require(bool value) { if (!value) std::abort(); } +static void CheckValueLimit() { + constexpr unsigned int maximum = 0x7fffffffu; + PthreadSem sem = nullptr; + Require(scePthreadSemInit(&sem, 0, 7, nullptr) == SCE_OK); + const PthreadSem original = sem; + const unsigned int invalidValues[] = {maximum + 1u, std::numeric_limits::max()}; + int value = -1; + for (const unsigned int initial : invalidValues) { + Require(scePthreadSemInit(&sem, 0, initial, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sem == original); + Require(scePthreadSemGetvalue(&sem, &value) == SCE_OK && value == 7); + } + Require(scePthreadSemDestroy(&sem) == SCE_OK); + + Require(scePthreadSemInit(&sem, 0, maximum, nullptr) == SCE_OK); + Require(scePthreadSemGetvalue(&sem, &value) == SCE_OK && value == static_cast(maximum)); + Require(scePthreadSemPost(&sem) == SCE_KERNEL_ERROR_EOVERFLOW); + Require(scePthreadSemGetvalue(&sem, &value) == SCE_OK && value == static_cast(maximum)); + Require(scePthreadSemTrywait(&sem) == SCE_OK); + Require(scePthreadSemGetvalue(&sem, &value) == SCE_OK && value == static_cast(maximum - 1)); + Require(scePthreadSemPost(&sem) == SCE_OK); + Require(scePthreadSemGetvalue(&sem, &value) == SCE_OK && value == static_cast(maximum)); + Require(scePthreadSemPost(&sem) == SCE_KERNEL_ERROR_EOVERFLOW); + Require(scePthreadSemGetvalue(&sem, &value) == SCE_OK && value == static_cast(maximum)); + Require(scePthreadSemDestroy(&sem) == SCE_OK); +} + struct Context { PthreadSem sem = nullptr; }; @@ -37,6 +67,8 @@ static void* APS5_VABI Poster(void* arg) { } int main() { + CheckValueLimit(); + PthreadSem sem = nullptr; Require(scePthreadSemInit(&sem, 0, 2, nullptr) == SCE_OK); int value = -1; diff --git a/core/libs/tests/GuestPthreadSpin.cpp b/core/libs/tests/GuestPthreadSpin.cpp new file mode 100644 index 0000000000..ca7e2b6926 --- /dev/null +++ b/core/libs/tests/GuestPthreadSpin.cpp @@ -0,0 +1,122 @@ +#include "SceTypes.hpp" +#include +#include + +struct PthreadSpinlockPrivate; +using PthreadSpinlock = PthreadSpinlockPrivate*; + +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_spin_init_nid_postfix(PthreadSpinlock* lock, int pshared); +int APS5_VABI pthread_spin_destroy_nid_postfix(PthreadSpinlock* lock); +int APS5_VABI pthread_spin_lock_nid_postfix(PthreadSpinlock* lock); +int APS5_VABI pthread_spin_trylock_nid_postfix(PthreadSpinlock* lock); +int APS5_VABI pthread_spin_unlock_nid_postfix(PthreadSpinlock* lock); +} + +static constexpr int GUEST_EPERM = 1; +static constexpr int GUEST_EBUSY = 16; +static constexpr int GUEST_EINVAL = 22; +static constexpr int PTHREAD_PROCESS_PRIVATE = 0; +static constexpr int PTHREAD_PROCESS_SHARED = 1; +static constexpr int Increments = 200000; + +static void Require(bool value) { if (!value) std::abort(); } + +struct Shared { + PthreadSpinlock lock = nullptr; + long counter = 0; + int trylock = -1; + int unlock = -1; +}; + +static void* APS5_VABI Contender(void* arg) { + auto& shared = *static_cast(arg); + shared.trylock = pthread_spin_trylock_nid_postfix(&shared.lock); + shared.unlock = pthread_spin_unlock_nid_postfix(&shared.lock); + return nullptr; +} + +static void* APS5_VABI Incrementer(void* arg) { + auto& shared = *static_cast(arg); + for (int i = 0; i < Increments; ++i) { + Require(pthread_spin_lock_nid_postfix(&shared.lock) == 0); + ++shared.counter; + Require(pthread_spin_unlock_nid_postfix(&shared.lock) == 0); + } + return nullptr; +} + +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); + Require(pthread_spin_lock_nid_postfix(nullptr) == GUEST_EINVAL); + Require(pthread_spin_trylock_nid_postfix(nullptr) == GUEST_EINVAL); + Require(pthread_spin_unlock_nid_postfix(nullptr) == GUEST_EINVAL); + Require(pthread_spin_destroy_nid_postfix(nullptr) == GUEST_EINVAL); + Require(pthread_spin_lock_nid_postfix(&lock) == GUEST_EINVAL); + Require(pthread_spin_trylock_nid_postfix(&lock) == GUEST_EINVAL); + Require(pthread_spin_unlock_nid_postfix(&lock) == GUEST_EINVAL); + Require(pthread_spin_destroy_nid_postfix(&lock) == GUEST_EINVAL); +} + +static void SingleThread(int pshared) { + PthreadSpinlock lock = nullptr; + Require(pthread_spin_init_nid_postfix(&lock, pshared) == 0); + Require(lock != nullptr); + Require(pthread_spin_unlock_nid_postfix(&lock) == GUEST_EPERM); + Require(pthread_spin_lock_nid_postfix(&lock) == 0); + Require(pthread_spin_trylock_nid_postfix(&lock) == GUEST_EBUSY); + bool threw = false; + try { + pthread_spin_lock_nid_postfix(&lock); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + Require(pthread_spin_unlock_nid_postfix(&lock) == 0); + Require(pthread_spin_unlock_nid_postfix(&lock) == GUEST_EPERM); + Require(pthread_spin_trylock_nid_postfix(&lock) == 0); + Require(pthread_spin_unlock_nid_postfix(&lock) == 0); + Require(pthread_spin_destroy_nid_postfix(&lock) == 0); + Require(lock == nullptr); + Require(pthread_spin_destroy_nid_postfix(&lock) == GUEST_EINVAL); +} + +static void HeldByAnotherThread() { + Shared shared; + Require(pthread_spin_init_nid_postfix(&shared.lock, PTHREAD_PROCESS_PRIVATE) == 0); + Require(pthread_spin_lock_nid_postfix(&shared.lock) == 0); + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, Contender, &shared, nullptr) == 0); + Require(scePthreadJoin(thread, nullptr) == 0); + Require(shared.trylock == GUEST_EBUSY); + Require(shared.unlock == GUEST_EPERM); + Require(pthread_spin_unlock_nid_postfix(&shared.lock) == 0); + Require(pthread_spin_destroy_nid_postfix(&shared.lock) == 0); +} + +static void MutualExclusion() { + Shared shared; + Require(pthread_spin_init_nid_postfix(&shared.lock, PTHREAD_PROCESS_PRIVATE) == 0); + Pthread first = nullptr; + Pthread second = nullptr; + Require(scePthreadCreate(&first, nullptr, Incrementer, &shared, nullptr) == 0); + Require(scePthreadCreate(&second, nullptr, Incrementer, &shared, nullptr) == 0); + Require(scePthreadJoin(first, nullptr) == 0); + Require(scePthreadJoin(second, nullptr) == 0); + Require(shared.counter == 2L * Increments); + Require(pthread_spin_destroy_nid_postfix(&shared.lock) == 0); +} + +int main() { + ArgumentErrors(); + SingleThread(PTHREAD_PROCESS_PRIVATE); + HeldByAnotherThread(); + MutualExclusion(); +} diff --git a/core/libs/tests/GuestPthreadStack.cpp b/core/libs/tests/GuestPthreadStack.cpp new file mode 100644 index 0000000000..5fcbe7ac52 --- /dev/null +++ b/core/libs/tests/GuestPthreadStack.cpp @@ -0,0 +1,147 @@ +#include "SceTypes.hpp" +#ifdef __linux__ +#include "prx/libc/include/GuestAllocations.hpp" +#include +#include +#include +#include +#include +#include +#include +#endif +#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 scePthreadAttrInit(PthreadAttr* attr); +int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr); +int APS5_VABI scePthreadAttrGet(Pthread thread, PthreadAttr* attr); +int APS5_VABI scePthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stackSize); +int APS5_VABI scePthreadAttrGetstack(const PthreadAttr* attr, void** stackAddress, std::size_t* stackSize); +Pthread APS5_VABI scePthreadSelf(); +#ifdef __linux__ +int APS5_VABI scePthreadDetach(Pthread thread); +#endif +} + +static constexpr int SCE_OK = 0; +static constexpr std::size_t FRAME_SIZE = 4096; +static constexpr std::size_t FRAME_MARGIN = 2 * FRAME_SIZE; +static constexpr std::size_t HOST_CALL_STACK = 256 * 1024; +static constexpr std::size_t STACK_SIZES[] = {16384, 65536, 1u << 20, 16u << 20}; + +thread_local unsigned char threadLocalBlock[512 * 1024]; + +static void Require(bool value) { if (!value) std::abort(); } + +static std::uintptr_t Descend(std::uintptr_t floor); +static std::uintptr_t (*volatile descend)(std::uintptr_t) = Descend; + +static std::uintptr_t Descend(std::uintptr_t floor) { + volatile unsigned char frame[FRAME_SIZE]; + frame[0] = 0x5a; + frame[FRAME_SIZE - 1] = 0xa5; + const auto here = reinterpret_cast(&frame[0]); + const auto deepest = here < floor + FRAME_MARGIN ? here : descend(floor); + Require(frame[0] == 0x5a && frame[FRAME_SIZE - 1] == 0xa5); + return deepest; +} + +static void* APS5_VABI Worker(void* arg) { + const auto requested = *static_cast(arg); + threadLocalBlock[0] = 1; + threadLocalBlock[sizeof(threadLocalBlock) - 1] = 2; + + PthreadAttr attr = nullptr; + Require(scePthreadAttrInit(&attr) == SCE_OK); + Require(scePthreadAttrGet(scePthreadSelf(), &attr) == SCE_OK); + void* address = nullptr; + std::size_t size = 0; + Require(scePthreadAttrGetstack(&attr, &address, &size) == SCE_OK); + Require(scePthreadAttrDestroy(&attr) == SCE_OK); + Require(size == requested); + + const auto begin = reinterpret_cast(address); + const auto local = reinterpret_cast(&size); + Require(local > begin && local < begin + size); + Require(local - begin + FRAME_SIZE >= requested); + const auto deepest = Descend(begin); + Require(deepest >= begin && deepest < begin + FRAME_MARGIN); + const auto hostDeepest = descend(begin - HOST_CALL_STACK); + Require(hostDeepest < begin - HOST_CALL_STACK + FRAME_MARGIN); + + Require(threadLocalBlock[0] == 1 && threadLocalBlock[sizeof(threadLocalBlock) - 1] == 2); + return arg; +} + +#ifdef __linux__ +bool GuestThreadStackHostRangeOverlaps(std::uintptr_t start, std::uintptr_t end); +static std::atomic observedUnmaps{0}; +static std::atomic retiredStackWord{0}; +static std::atomic retiredUnmapped{false}; +static std::atomic detachedDone{false}; +static int detachedTid = 0; + +static void* APS5_VABI DetachedWorker(void*) { + int local = 0; + detachedTid = static_cast(syscall(SYS_gettid)); + retiredStackWord.store(reinterpret_cast(&local), std::memory_order_relaxed); + detachedDone.store(true, std::memory_order_release); + return nullptr; +} + +static void* APS5_VABI ReaperWorker(void*) { + return nullptr; +} + +static void ObserveUnmap(std::uintptr_t address, std::size_t bytes) { + if (GuestThreadStackHostRangeOverlaps(address, address + bytes)) { + std::fprintf(stderr, "Retired stack still owns host-only pages after unmap\n"); + std::abort(); + } + observedUnmaps.fetch_add(1, std::memory_order_relaxed); + const auto retired = retiredStackWord.load(std::memory_order_relaxed); + if (retired >= address && retired - address < bytes) retiredUnmapped.store(true, std::memory_order_relaxed); +} +#endif + +int main() { +#ifdef __linux__ + GuestAllocations::GuestAllocationsSetUnmapObserver_nid_postfix(&ObserveUnmap); +#endif + for (auto stackSize : STACK_SIZES) { + PthreadAttr attr = nullptr; + Require(scePthreadAttrInit(&attr) == SCE_OK); + Require(scePthreadAttrSetstacksize(&attr, stackSize) == SCE_OK); + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, &attr, Worker, &stackSize, nullptr) == SCE_OK); + Require(scePthreadAttrDestroy(&attr) == SCE_OK); + void* result = nullptr; + Require(scePthreadJoin(thread, &result) == SCE_OK); + Require(result == &stackSize); + } +#ifdef __linux__ + Pthread detached = nullptr; + Require(scePthreadCreate(&detached, nullptr, DetachedWorker, nullptr, nullptr) == SCE_OK); + Require(scePthreadDetach(detached) == SCE_OK); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + while (!detachedDone.load(std::memory_order_acquire)) { + Require(std::chrono::steady_clock::now() < deadline); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + const auto task = "/proc/self/task/" + std::to_string(detachedTid); + while (access(task.c_str(), F_OK) == 0) { + Require(std::chrono::steady_clock::now() < deadline); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + Pthread reaper = nullptr; + Require(scePthreadCreate(&reaper, nullptr, ReaperWorker, nullptr, nullptr) == SCE_OK); + Require(scePthreadJoin(reaper, nullptr) == SCE_OK); + Require(retiredUnmapped.load(std::memory_order_relaxed)); + GuestAllocations::GuestAllocationsSetUnmapObserver_nid_postfix(nullptr); + Require(observedUnmaps.load(std::memory_order_relaxed) != 0); +#endif +} diff --git a/core/libs/tests/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 new file mode 100644 index 0000000000..f27a089c63 --- /dev/null +++ b/core/libs/tests/GuestQsort.cpp @@ -0,0 +1,239 @@ +#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; +}; + +int APS5_VABI CompareKeys(const void* left, const void* right) { + const int first = static_cast(left)->key; + const int second = static_cast(right)->key; + return (first > second) - (first < second); +} + +int APS5_VABI CompareFirstByte(const void* left, const void* right) { + const int first = *static_cast(left); + const int second = *static_cast(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)}); + qsort_nid_postfix(elements.data(), elements.size(), sizeof(Element), CompareKeys); + for (std::size_t index = 0; index < tags.size(); ++index) { + if (elements[index].tag != tags[index]) { + std::fprintf(stderr, "%s: element %zu has tag %d, expected %d\n", name, index, elements[index].tag, tags[index]); + return false; + } + } + return true; +} + +bool ExpectSorted(std::size_t size, std::size_t count) { + std::vector bytes(size * count); + unsigned int state = static_cast(size * 31 + count); + for (auto& byte : bytes) { + state = state * 1103515245u + 12345u; + byte = static_cast(state >> 16); + } + std::vector counts(256); + for (std::size_t index = 0; index < count; ++index) ++counts[bytes[index * size]]; + qsort_nid_postfix(bytes.data(), count, size, CompareFirstByte); + for (std::size_t index = 0; index < count; ++index) { + if (index > 0 && bytes[(index - 1) * size] > bytes[index * size]) { + std::fprintf(stderr, "qsort of %zu %zu-byte elements is not sorted at %zu\n", count, size, index); + return false; + } + --counts[bytes[index * size]]; + } + for (const unsigned char remaining : counts) { + if (remaining != 0) { + std::fprintf(stderr, "qsort of %zu %zu-byte elements lost or duplicated elements\n", count, size); + return false; + } + } + 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, + {21, 11, 36, 6, 1, 31, 16, 46, 41, 26, 29, 4, 24, 44, 9, 39, 34, 14, 19, 12, 2, 7, 17, 22, + 27, 32, 37, 42, 47, 5, 40, 30, 15, 35, 25, 20, 0, 45, 10, 28, 33, 13, 38, 8, 3, 23, 43, 18}); + 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 59d2bc4559..fd74e36771 100644 --- a/core/libs/tests/GuestRaiseException.cpp +++ b/core/libs/tests/GuestRaiseException.cpp @@ -1,13 +1,17 @@ #include "SceTypes.hpp" #include #include +#include #include +#include #include #include #include #include #include +#ifdef _WIN32 #include +#endif extern "C" { int APS5_VABI sceKernelInstallExceptionHandler(int signum, void* handler); @@ -20,14 +24,34 @@ int APS5_VABI sceKernelCreateSema(KernelSema* sem, const char* name, uint32_t at int APS5_VABI sceKernelDeleteSema(KernelSema sem); int APS5_VABI sceKernelSignalSema(KernelSema sem, int count); int APS5_VABI sceKernelWaitSema(KernelSema sem, int need, KernelUseconds* time); +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 scePthreadMutexTimedlock(PthreadMutex* mutex, KernelUseconds usec); +int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex); +int APS5_VABI scePthreadRwlockInit(PthreadRwlock* rwlock, const PthreadRwlockattr* attr, const char* name); +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; +static constexpr std::size_t MxcsrOffset = 0x140 + 24; +static constexpr std::uint32_t FlushToZero = 0x8000; +static constexpr std::uint32_t ReservedMxcsrBit = 0x10000; +static constexpr int StartingRounds = 50; +static constexpr std::uint64_t SpinMarker = 0x0123456789abcdefULL; +static constexpr std::uint64_t PatchedR12 = 0x5a5a1234abcd0001ULL; +static constexpr std::uint64_t PatchedXmm8 = 0x7e7e5678dcba0002ULL; static void Require(bool value) { if (!value) std::abort(); } @@ -37,11 +61,30 @@ static std::atomic handlerRsp{0}; static std::atomic handlerFrame{0}; static std::atomic handlerSelf{nullptr}; static std::atomic clobberVectors{false}; +static std::atomic patchContext{false}; +static std::atomic observedR13{0}; +static std::atomic observedXmm8{0}; +static std::atomic observedMxcsr{0}; static void APS5_VABI Handler(int signum, void* context) { Require(signum == SIGUSR1); + auto* bytes = static_cast(context); std::uint64_t rsp = 0; - std::memcpy(&rsp, static_cast(context) + 0xf8, sizeof(rsp)); + std::memcpy(&rsp, bytes + 0xf8, sizeof(rsp)); + if (patchContext.load()) { + std::uint64_t value = 0; + std::memcpy(&value, bytes + R13Offset, sizeof(value)); + observedR13.store(value); + std::memcpy(&value, bytes + Xmm8Offset, sizeof(value)); + observedXmm8.store(value); + std::uint32_t mxcsr = 0; + std::memcpy(&mxcsr, bytes + MxcsrOffset, sizeof(mxcsr)); + observedMxcsr.store(mxcsr); + mxcsr |= FlushToZero | ReservedMxcsrBit; + std::memcpy(bytes + R12Offset, &PatchedR12, sizeof(PatchedR12)); + std::memcpy(bytes + Xmm8Offset, &PatchedXmm8, sizeof(PatchedXmm8)); + std::memcpy(bytes + MxcsrOffset, &mxcsr, sizeof(mxcsr)); + } int local = 0; handlerFrame.store(reinterpret_cast(&local)); handlerRsp.store(rsp); @@ -51,6 +94,41 @@ static void APS5_VABI Handler(int signum, void* context) { calls.fetch_add(1); } +struct SpinState { + std::atomic started{0}; + std::uint32_t padding = 0; + std::uint64_t r12 = 0; + std::uint64_t xmm8 = 0; + std::uint32_t mxcsr = 0; +}; + +static_assert(offsetof(SpinState, r12) == 8 && offsetof(SpinState, xmm8) == 16 && offsetof(SpinState, mxcsr) == 24); + +extern "C" void APS5_VABI RaiseTestSpinUntilR12(SpinState* state); +asm(".text\n" + ".globl RaiseTestSpinUntilR12\n" + "RaiseTestSpinUntilR12:\n" + " pushq %r12\n" + " pushq %r13\n" + " subq $8, %rsp\n" + " stmxcsr (%rsp)\n" + " xorl %r12d, %r12d\n" + " movabsq $0x0123456789abcdef, %r13\n" + " movq %r13, %xmm8\n" + " movl $1, (%rdi)\n" + "1:\n" + " pause\n" + " testq %r12, %r12\n" + " jz 1b\n" + " movq %r12, 8(%rdi)\n" + " movq %xmm8, 16(%rdi)\n" + " stmxcsr 24(%rdi)\n" + " ldmxcsr (%rsp)\n" + " addq $8, %rsp\n" + " popq %r13\n" + " popq %r12\n" + " ret\n"); + struct Worker { std::atomic started{false}; std::atomic stop{false}; @@ -94,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) { @@ -109,6 +187,7 @@ static void* APS5_VABI Leaving(void* arg) { return nullptr; } +#ifdef _WIN32 static constexpr DWORD ContinuedCode = 0xe0000001u; static constexpr int ContinuingRounds = 50; static std::atomic continued{0}; @@ -119,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) { @@ -160,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]; @@ -213,6 +292,22 @@ static void* APS5_VABI Vectors(void* arg) { : "memory", "cc", "xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7", "xmm8", "xmm9", "xmm10", "xmm11", "xmm12", "xmm13", "xmm14", "xmm15"); return nullptr; } +#endif + +static std::atomic spinningId; + +static void* APS5_VABI Spinning(void* arg) { + spinningId.store(std::this_thread::get_id()); + RaiseTestSpinUntilR12(static_cast(arg)); + return nullptr; +} + +static std::atomic startingRelease{false}; + +static void* APS5_VABI Starting(void*) { + while (!startingRelease.load()) std::this_thread::yield(); + return nullptr; +} static std::atomic finishedReturned{false}; @@ -309,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); @@ -316,6 +492,60 @@ static void ExpectDelivery(int before, std::thread::id thread) { Require(handlerRsp.load() != 0 && handlerFrame.load() < handlerRsp.load()); } +static constexpr int LockRounds = 10; +static PthreadMutex guestMutex = nullptr; +static PthreadRwlock guestRwlock = nullptr; + +static int LockMutex() { return scePthreadMutexLock(&guestMutex); } +static int TimedLockMutex() { return scePthreadMutexTimedlock(&guestMutex, 10000000); } +static int UnlockMutex() { return scePthreadMutexUnlock(&guestMutex); } +static int LockRwlock() { return scePthreadRwlockWrlock(&guestRwlock); } +static int UnlockRwlock() { return scePthreadRwlockUnlock(&guestRwlock); } + +struct LockWorker { + int (*lock)() = nullptr; + int (*unlock)() = nullptr; + std::atomic blocking{0}; + std::atomic acquired{0}; + std::atomic released{0}; + std::thread::id id; +}; + +static void* APS5_VABI LockBlocked(void* arg) { + auto& worker = *static_cast(arg); + worker.id = std::this_thread::get_id(); + for (int round = 1; round <= LockRounds; ++round) { + while (worker.released.load() != round - 1) std::this_thread::yield(); + worker.blocking.store(round); + Require(worker.lock() == 0); + worker.acquired.store(round); + Require(worker.unlock() == 0); + } + return nullptr; +} + +static void ExpectDeliveryWhileLockHeld(int (*lock)(), int (*unlock)(), int before) { + LockWorker worker; + worker.lock = lock; + worker.unlock = unlock; + Require(lock() == 0); + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, LockBlocked, &worker, "lock blocked") == 0); + for (int round = 1; round <= LockRounds; ++round) { + while (worker.blocking.load() != round) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + Require(sceKernelRaiseException(thread, SIGUSR1) == 0); + ExpectDelivery(before + round - 1, worker.id); + Require(worker.acquired.load() == round - 1); + Require(unlock() == 0); + while (worker.acquired.load() != round) std::this_thread::yield(); + Require(lock() == 0); + worker.released.store(round); + } + Require(unlock() == 0); + Require(scePthreadJoin(thread, nullptr) == 0); +} + int main() { Require(sceKernelRaiseException(scePthreadSelf(), 11) == SCE_KERNEL_ERROR_EINVAL); bool rejected = false; @@ -383,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); @@ -391,6 +622,7 @@ int main() { Require(scePthreadJoin(leavingThread, nullptr) == 0); Require(sceKernelDeleteSema(leaving.sem) == 0); +#ifdef _WIN32 void* const vectored = AddVectoredExceptionHandler(1, ContinueRaised); Require(vectored != nullptr); Worker continuing; @@ -461,6 +693,43 @@ int main() { Require(scePthreadJoin(vectorsThread, nullptr) == 0); Require(std::memcmp(vectorPattern, vectorResult, VectorBytes) == 0); } +#endif + + SpinState spin; + Pthread spinningThread = nullptr; + Require(scePthreadCreate(&spinningThread, nullptr, Spinning, &spin, "spinning") == 0); + while (spin.started.load() == 0) std::this_thread::yield(); + patchContext.store(true); + const int spinBefore = calls.load(); + Require(sceKernelRaiseException(spinningThread, SIGUSR1) == 0); + ExpectDelivery(spinBefore, spinningId.load()); + Require(scePthreadJoin(spinningThread, nullptr) == 0); + patchContext.store(false); + Require(observedR13.load() == SpinMarker && observedXmm8.load() == SpinMarker); + Require(spin.r12 == PatchedR12 && spin.xmm8 == PatchedXmm8); + Require(spin.mxcsr == (observedMxcsr.load() | FlushToZero)); + + for (int round = 0; round < StartingRounds; ++round) { + const int before = calls.load(); + startingRelease.store(false); + Pthread startingThread = nullptr; + Require(scePthreadCreate(&startingThread, nullptr, Starting, nullptr, "starting") == 0); + Require(sceKernelRaiseException(startingThread, SIGUSR1) == 0); + for (int attempt = 0; attempt < 5000 && calls.load() == before; ++attempt) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + Require(calls.load() == before + 1); + Require(handlerSelf.load() == startingThread); + startingRelease.store(true); + Require(scePthreadJoin(startingThread, nullptr) == 0); + } + + const int beforeLocks = calls.load(); + Require(scePthreadMutexInit(&guestMutex, nullptr, "raise") == 0); + ExpectDeliveryWhileLockHeld(LockMutex, UnlockMutex, beforeLocks); + ExpectDeliveryWhileLockHeld(TimedLockMutex, UnlockMutex, beforeLocks + LockRounds); + Require(scePthreadMutexDestroy(&guestMutex) == 0); + Require(scePthreadRwlockInit(&guestRwlock, nullptr, "raise") == 0); + ExpectDeliveryWhileLockHeld(LockRwlock, UnlockRwlock, beforeLocks + 2 * LockRounds); + Require(scePthreadRwlockDestroy(&guestRwlock) == 0); Pthread finishedThread = nullptr; Require(scePthreadCreate(&finishedThread, nullptr, Finished, nullptr, "finished") == 0); @@ -475,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..955d4605b6 --- /dev/null +++ b/core/libs/tests/GuestRaiseInHostCode.cpp @@ -0,0 +1,69 @@ +#include "SceTypes.hpp" +#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(); + 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); + Require(scePthreadJoin(writer, nullptr) == 0); + Require(sceKernelRemoveExceptionHandler(SigUsr1) == 0); + std::fclose(shared); + 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 new file mode 100644 index 0000000000..71511241be --- /dev/null +++ b/core/libs/tests/GuestRandomState.cpp @@ -0,0 +1,236 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +std::int64_t APS5_VABI random_nid_postfix(); +void APS5_VABI srandom_nid_postfix(std::uint64_t); +char* APS5_VABI initstate_nid_postfix(std::uint64_t, char*, std::int64_t); +char* APS5_VABI setstate_nid_postfix(char*); +int* APS5_VABI __error_nid_postfix(); +double APS5_VABI erand48_nid_postfix(std::uint16_t*); +std::int64_t APS5_VABI nrand48_nid_postfix(std::uint16_t*); +std::int64_t APS5_VABI jrand48_nid_postfix(std::uint16_t*); +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; + +static void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "%s\n", message); + ++failures; + } +} + +using Sequence = std::array; + +static Sequence Draw() { + Sequence values{}; + for (auto& value : values) value = random_nid_postfix(); + return values; +} + +static std::uint32_t Word(const char* table, std::size_t index) { + std::uint32_t value; + std::memcpy(&value, table + sizeof(value) * index, sizeof(value)); + return value; +} + +static bool SeedIs(const std::uint16_t* seed, std::uint16_t low, std::uint16_t middle, std::uint16_t high) { + return seed[0] == low && seed[1] == middle && seed[2] == high; +} + +static void CheckRand48() { + std::uint16_t seed[3] = {0x330e, 0xabcd, 0x1234}; + Require(erand48_nid_postfix(seed) == 0x657eb7255101p-48 && SeedIs(seed, 0x5101, 0xb725, 0x657e), "erand48 first value"); + Require(nrand48_nid_postfix(seed) == 1804928587 && SeedIs(seed, 0x6378, 0x0c96, 0xd72a), "nrand48 second value"); + 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) == 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"); + std::uint16_t top[3] = {0xffff, 0xffff, 0xffff}; + const double value = erand48_nid_postfix(top); + Require(value >= 0.0 && value < 1.0, "erand48 stays below 1"); + + srand48_nid_postfix(0x1234abcd); + std::uint16_t expectedSeed[3] = {0x330e, 0xabcd, 0x1234}; + Require(drand48_nid_postfix() == erand48_nid_postfix(expectedSeed), "drand48 matches erand48 on initialized seed"); + Require(lrand48_nid_postfix() == nrand48_nid_postfix(expectedSeed), "lrand48 matches nrand48 on initialized seed"); + 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) == 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() == static_cast(::mrand48()), "mrand48 matches the host's 32 bits"); + } +#endif +} + +int main() { + CheckRand48(); + constexpr Sequence seedOne {1408992891, 1638182910, 83848093, 145962118, 627851256}; + constexpr Sequence seed12345 {1052984060, 1639322404, 1971525204, 2097956667, 2012147915}; + constexpr std::array seed42ByType {{ + {2060028338, 1164936638, 481681973, 1763071474, 962919018}, + {660273387, 1356916325, 287587791, 1132382070, 1497594867}, + {1015209508, 12952757, 1418769128, 2005808989, 1212998434}, + {745791173, 418148062, 949332769, 1480536337, 177711995}, + {996638292, 1209745284, 1148231710, 400392963, 1149215164}, + }}; + constexpr std::array seed7Type1 {1271472800, 27686551, 1252069488, 1609001632, 28873941, 508017694}; + + Require(Draw() == seedOne, "unseeded random must match srandom(1)"); + srandom_nid_postfix(1); + Require(Draw() == seedOne, "srandom(1) sequence"); + srandom_nid_postfix(0x100000001ull); + Require(Draw() == seedOne, "srandom must use the low 32 bits of the seed"); + srandom_nid_postfix(12345); + Require(Draw() == seed12345, "srandom(12345) sequence"); + + struct SizeCase { + std::int64_t size; + int type; + std::size_t usedBytes; + }; + constexpr std::array sizes {{ + {8, 0, 8}, {31, 0, 8}, {32, 1, 32}, {63, 1, 32}, {64, 2, 64}, + {127, 2, 64}, {128, 3, 128}, {255, 3, 128}, {256, 4, 256}, {1000, 4, 256}, + }}; + std::vector> buffers; + char* defaultTable = nullptr; + char* active = nullptr; + for (const auto& sizeCase : sizes) { + auto& buffer = buffers.emplace_back(static_cast(sizeCase.size) + 4, static_cast(0xa5)); + char* previous = initstate_nid_postfix(42, buffer.data(), sizeCase.size); + if (defaultTable == nullptr) defaultTable = previous; + else Require(previous == active, "initstate must return the previous state"); + Require(Word(buffer.data(), 0) == static_cast(sizeCase.type), "initstate type word"); + Require(Draw() == seed42ByType[static_cast(sizeCase.type)], "initstate(42) sequence for its size"); + Require(std::all_of(buffer.begin() + static_cast(sizeCase.usedBytes), buffer.end(), + [](char value) { return value == static_cast(0xa5); }), "initstate must use only the rounded state size"); + active = buffer.data(); + } + Require(defaultTable != nullptr && Word(defaultTable, 0) == 5 * 5 + 3, "default table must hold its rear position"); + Require(setstate_nid_postfix(defaultTable) == active, "setstate must return the previous state"); + Require(random_nid_postfix() == 1533042628 && random_nid_postfix() == 1225903521, + "setstate on the default table must continue the srandom(12345) sequence"); + + alignas(4) std::array small{}; + alignas(4) std::array large{}; + initstate_nid_postfix(7, small.data(), static_cast(small.size())); + for (std::size_t i = 0; i < 3; ++i) Require(random_nid_postfix() == seed7Type1[i], "initstate(7) on 32 bytes"); + initstate_nid_postfix(7, large.data(), static_cast(large.size())); + Require(setstate_nid_postfix(small.data()) == large.data(), "setstate must return the 64-byte state"); + Require(setstate_nid_postfix(large.data()) == small.data(), "setstate must return the 32-byte state"); + Require(Word(small.data(), 0) == 5 * 3 + 1, "setstate must save the rear position of the old state"); + Require(setstate_nid_postfix(small.data()) == large.data(), "setstate back to the 32-byte state"); + for (std::size_t i = 3; i < seed7Type1.size(); ++i) Require(random_nid_postfix() == seed7Type1[i], "setstate must resume the sequence"); + + initstate_nid_postfix(7, small.data(), static_cast(small.size())); + random_nid_postfix(); + alignas(4) std::array garbled{}; + const std::uint32_t garbledWord = 5 * 7 + 1; + std::memcpy(garbled.data(), &garbledWord, sizeof(garbledWord)); + Require(setstate_nid_postfix(garbled.data()) == nullptr, "setstate must reject a rear position past the degree"); + Require(random_nid_postfix() == seed7Type1[1], "a rejected setstate must keep the current state"); + + *__error_nid_postfix() = 1234; + std::array tiny{}; + tiny.fill(static_cast(0x5a)); + for (const std::int64_t size : {std::int64_t{7}, std::int64_t{0}, std::int64_t{-1}}) + Require(initstate_nid_postfix(1, tiny.data(), size) == nullptr, "initstate below 8 bytes must return null"); + Require(initstate_nid_postfix(1, nullptr, 4) == nullptr, "initstate below 8 bytes must return null before reading the state"); + Require(*__error_nid_postfix() == 1234, "initstate below 8 bytes must leave errno unchanged"); + Require(std::all_of(tiny.begin(), tiny.end(), [](char value) { return value == static_cast(0x5a); }), + "initstate below 8 bytes must not write the state"); + Require(random_nid_postfix() == seed7Type1[2], "a rejected initstate must keep the current state"); + + bool threw = false; + try { + initstate_nid_postfix(1, nullptr, 128); + } catch (const std::invalid_argument&) { + threw = true; + } + Require(threw, "initstate with a null state must throw"); + threw = false; + try { + setstate_nid_postfix(nullptr); + } catch (const std::invalid_argument&) { + threw = true; + } + Require(threw, "setstate with a null state must throw"); + + constexpr std::size_t threadCount = 4; + constexpr std::size_t callsPerThread = 2000; + srandom_nid_postfix(99); + std::vector serial(threadCount * callsPerThread); + for (auto& value : serial) value = random_nid_postfix(); + srandom_nid_postfix(99); + std::vector concurrent(threadCount * callsPerThread); + std::vector threads; + for (std::size_t t = 0; t < threadCount; ++t) + threads.emplace_back([&concurrent, t] { + for (std::size_t i = 0; i < callsPerThread; ++i) concurrent[t * callsPerThread + i] = random_nid_postfix(); + }); + for (auto& thread : threads) thread.join(); + std::sort(serial.begin(), serial.end()); + std::sort(concurrent.begin(), concurrent.end()); + Require(serial == concurrent, "concurrent calls must draw the serial sequence"); + + return failures == 0 ? 0 : 1; +} diff --git a/core/libs/tests/GuestRazorCpu.cpp b/core/libs/tests/GuestRazorCpu.cpp index f8fdab4d88..13572d5feb 100644 --- a/core/libs/tests/GuestRazorCpu.cpp +++ b/core/libs/tests/GuestRazorCpu.cpp @@ -1,17 +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 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); 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 { @@ -25,10 +41,13 @@ int main() { Require(sceRazorCpuJobManagerDispatch(nullptr) == 0); Require(sceRazorCpuJobManagerJob(nullptr) == 0); Require(sceRazorCpuJobManagerSequence(nullptr) == 0); + Require(sceRazorCpuNamedSync("frame") == 0); Require(sceRazorCpuPushMarkerStatic("outer", 0x80ffffffu, 2) == 0); Require(sceRazorCpuPushMarkerStatic("inner", 0x80ffffffu, 2) == 0); Require(sceRazorCpuPopMarker() == 0); Require(sceRazorCpuPopMarker() == 0); + Require(sceRazorCpuPushMarker("copied", 0xff00ff00u, 1) == 0); + Require(sceRazorCpuPopMarker() == 0); Require(sceRazorCpuPushMarkerStatic("unbalanced", 0x80ffffffu, 2) == 0); Require(sceRazorCpuIsCapturing() == 0); std::uint64_t timeSpentInFlush = 0x123456789abcdef0ull; @@ -38,4 +57,42 @@ int main() { Require(sceRazorCpuPlotValue("read time (ms)", 1.5f) == 0); Require(sceRazorCpuWriteBookmark("read timeout", "lba=0x10") == 0); Require(sceRazorCpuWriteBookmark("read timeout", nullptr) == 0); + Require(sceRazorCpuSync() == 0); + Require(sceRazorCpuSync() == 0); + Require(sceRazorCpuIsCapturing() == 0); + + 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/GuestReallocalign.cpp b/core/libs/tests/GuestReallocalign.cpp index bb66c3979c..fded34f311 100644 --- a/core/libs/tests/GuestReallocalign.cpp +++ b/core/libs/tests/GuestReallocalign.cpp @@ -2,10 +2,12 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include extern "C" { void* APS5_VABI reallocalign_nid_postfix(void*, std::size_t, std::size_t); +int* APS5_VABI __error_nid_postfix(); } namespace { @@ -14,13 +16,6 @@ void Require(bool value) { if (!value) throw std::runtime_error("reallocalign check failed"); } -template -void Reject(TAction action) { - bool rejected = false; - try { action(); } catch (const std::exception&) { rejected = true; } - Require(rejected); -} - } int main() { @@ -35,7 +30,12 @@ int main() { auto* shrunk = static_cast(reallocalign_nid_postfix(grown, 16, 16)); Require(shrunk != nullptr && (reinterpret_cast(shrunk) % 16) == 0); for (int index = 0; index < 16; ++index) Require(shrunk[index] == 0x5a); - Reject([&] { reallocalign_nid_postfix(shrunk, 16, 0); }); - Reject([&] { reallocalign_nid_postfix(shrunk, 16, 3); }); + *__error_nid_postfix() = 0; + Require(reallocalign_nid_postfix(shrunk, 16, 0) == nullptr && *__error_nid_postfix() == 22); + *__error_nid_postfix() = 0; + Require(reallocalign_nid_postfix(shrunk, 16, 3) == nullptr && *__error_nid_postfix() == 22); + *__error_nid_postfix() = 0; + Require(reallocalign_nid_postfix(shrunk, std::numeric_limits::max(), 16) == nullptr && *__error_nid_postfix() == 12); + for (int index = 0; index < 16; ++index) Require(shrunk[index] == 0x5a); Require(reallocalign_nid_postfix(shrunk, 0, 16) == nullptr); } diff --git a/core/libs/tests/GuestRealpath.cpp b/core/libs/tests/GuestRealpath.cpp new file mode 100644 index 0000000000..faae73050f --- /dev/null +++ b/core/libs/tests/GuestRealpath.cpp @@ -0,0 +1,106 @@ +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +char* APS5_VABI realpath_nid_postfix(const char*, char*); +int APS5_VABI chdir_nid_postfix(const char*); +void APS5_VABI free_nid_postfix(void*); +int* APS5_VABI __error_nid_postfix(); +} + +static void Require(bool value) { if (!value) std::abort(); } + +static bool Resolves(const std::string& path, const std::string& expected) { + char out[1024]; + std::memset(out, 'x', sizeof(out)); + return realpath_nid_postfix(path.c_str(), out) == out && expected == out; +} + +static bool Fails(const char* path, int error, const std::string& trouble = {}) { + char out[1024]; + std::memset(out, 'x', sizeof(out)); + *__error_nid_postfix() = 0; + if (realpath_nid_postfix(path, out) != nullptr || *__error_nid_postfix() != error) return false; + return trouble.empty() || trouble == out; +} + +int main() { + std::array api{}; + ApplicationHeapRegister_nid_no_patch(api.data()); + const auto host = std::filesystem::canonical(std::filesystem::current_path()); + const auto name = "anyps5-realpath-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + const auto directory = host / name; + Require(std::filesystem::create_directories(directory / "sub")); + { std::ofstream file(directory / "file.txt"); file << "file"; } + { std::ofstream file(directory / "sub" / "inner.txt"); file << "inner"; } + const std::string guest = "/" + name; + + Require(Resolves("/", "/")); + Require(Resolves(guest, guest)); + Require(Resolves(guest + "/", guest)); + Require(Resolves(guest + "/sub/../file.txt", guest + "/file.txt")); + Require(Resolves(guest + "/./sub//inner.txt", guest + "/sub/inner.txt")); + Require(Resolves("/../.." + guest + "/sub/", guest + "/sub")); + Require(Resolves(guest + "/sub/..", guest)); + Require(Resolves("/..", "/")); + Require(Resolves(guest + "\\sub\\..\\file.txt", guest + "/file.txt")); + Require(Resolves("\\" + name + "\\sub\\", guest + "/sub")); + + std::string upper = guest; + for (auto& character : upper) character = static_cast(std::toupper(static_cast(character))); + Require(Resolves(upper + "/file.txt", upper + "/file.txt")); + + Require(chdir_nid_postfix(guest.c_str()) == 0); + Require(Resolves("sub/inner.txt", guest + "/sub/inner.txt")); + Require(Resolves("../" + name + "/file.txt", guest + "/file.txt")); + Require(Resolves(".", guest)); + Require(Resolves("sub/./../sub", guest + "/sub")); + Require(Resolves("..", "/")); + Require(chdir_nid_postfix("/") == 0); + + AddPathAlias_nid_no_patch("realpath-alias", directory.string().c_str()); + Require(Resolves("/realpath-alias/sub/../file.txt", "/realpath-alias/file.txt")); + Require(Fails("/realpath-alias/missing", 2, "/realpath-alias/missing")); + RemovePathAlias_nid_no_patch("realpath-alias"); + BlockPathAlias_nid_no_patch("realpath-blocked"); + Require(Fails("/realpath-blocked/x", 2, "/realpath-blocked")); + + char* allocated = realpath_nid_postfix((guest + "/file.txt").c_str(), nullptr); + Require(allocated != nullptr && guest + "/file.txt" == allocated); + free_nid_postfix(allocated); + + Require(Fails(nullptr, 22)); + Require(Fails("", 2)); + Require(Fails((guest + "/missing").c_str(), 2, guest + "/missing")); + Require(Fails((guest + "/missing/../file.txt").c_str(), 2, guest + "/missing")); + Require(Fails((guest + "/file.txt/").c_str(), 20, guest + "/file.txt")); + Require(Fails((guest + "/file.txt/x").c_str(), 20, guest + "/file.txt")); + Require(Fails((guest + "/file.txt/.").c_str(), 20)); + Require(Fails((guest + "/file.txt/..").c_str(), 20)); + + const std::string overflowing = "/" + std::string(1023, 'a'); + Require(Fails(overflowing.c_str(), 63, overflowing.substr(0, 1023))); + const std::string longInput = guest + "/sub/" + std::string(1024, '.') + "/inner.txt"; + Require(Fails(longInput.c_str(), 63)); + std::string boundary = guest; + while (boundary.size() + 8 <= 1023) boundary += "/missing"; + boundary += "/" + std::string(1023 - boundary.size() - 1, 'm'); + Require(boundary.size() == 1023); + Require(Fails(boundary.c_str(), 2, guest + "/missing")); + + *__error_nid_postfix() = 0; + Require(realpath_nid_postfix((guest + "/missing").c_str(), nullptr) == nullptr && *__error_nid_postfix() == 2); + + std::filesystem::remove_all(directory); + return 0; +} diff --git a/core/libs/tests/GuestRecordLockOwners.cpp b/core/libs/tests/GuestRecordLockOwners.cpp new file mode 100644 index 0000000000..cbffd82062 --- /dev/null +++ b/core/libs/tests/GuestRecordLockOwners.cpp @@ -0,0 +1,148 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode); +int APS5_VABI sceKernelClose(int d); +int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +void Require(bool value, int line) { + if (!value) { + std::fprintf(stderr, "record lock owner check failed at line %d, errno %d\n", line, *__error_nid_postfix()); + std::exit(1); + } +} +#define Check(value) Require((value), __LINE__) + +constexpr int GetLock = 11; +constexpr int SetLock = 12; +constexpr std::int16_t ReadLock = 1; +constexpr std::int16_t Unlock = 2; +constexpr std::int16_t WriteLock = 3; +constexpr std::int16_t FromStart = 0; +constexpr int GuestEagain = 35; + +struct GuestLock { + std::int64_t start; + std::int64_t length; + std::int32_t pid; + std::int16_t type; + std::int16_t whence; + std::int32_t system; +}; +static_assert(sizeof(GuestLock) == 32); + +const char* const FileName = "owners.bin"; + +pid_t HoldLocks(int ready[2], int release[2]) { + const pid_t child = ::fork(); + Check(child >= 0); + if (child != 0) { + ::close(ready[1]); + ::close(release[0]); + return child; + } + ::close(ready[0]); + ::close(release[1]); + const int fd = ::open(FileName, O_RDWR); + struct flock read{}; + read.l_type = F_RDLCK; + read.l_whence = SEEK_SET; + read.l_start = 0; + read.l_len = 10; + struct flock write = read; + write.l_type = F_WRLCK; + write.l_start = 20; + write.l_len = 10; + struct flock tail = write; + tail.l_start = 50; + tail.l_len = 0; + const bool held = fd >= 0 && ::fcntl(fd, F_SETLK, &read) == 0 && ::fcntl(fd, F_SETLK, &write) == 0 && ::fcntl(fd, F_SETLK, &tail) == 0; + const char state = held ? 'y' : 'n'; + if (::write(ready[1], &state, 1) != 1) ::_exit(2); + char done = 0; + if (::read(release[0], &done, 1) != 1) ::_exit(3); + ::_exit(held ? 0 : 1); +} + +GuestLock Query(int fd, GuestLock request) { + Check(fcntl_nid_postfix(fd, GetLock, &request) == 0); + return request; +} + +bool Unchanged(const GuestLock& reply, const GuestLock& asked) { + return reply.type == Unlock && reply.start == asked.start && reply.length == asked.length && reply.pid == asked.pid && + reply.whence == asked.whence && reply.system == asked.system; +} + +bool Reports(const GuestLock& reply, std::int16_t type, std::int64_t start, std::int64_t length, pid_t owner) { + return reply.type == type && reply.start == start && reply.length == length && reply.pid == owner && reply.whence == FromStart; +} + +void OtherOwner() { + int ready[2]; + int release[2]; + Check(::pipe(ready) == 0 && ::pipe(release) == 0); + const pid_t child = HoldLocks(ready, release); + char state = 0; + Check(::read(ready[0], &state, 1) == 1 && state == 'y'); + + const int fd = sceKernelOpen(FileName, SCE_KERNEL_O_RDWR, 0); + Check(fd >= 0); + + const GuestLock readRange{0, 10, 7, Unlock, FromStart, 3}; + Check(Unchanged(Query(fd, readRange), readRange)); + Check(Reports(Query(fd, {0, 0, 0, Unlock, FromStart, 0}), WriteLock, 20, 10, child)); + Check(Reports(Query(fd, {25, 1, 0, Unlock, FromStart, 0}), WriteLock, 20, 10, child)); + Check(Reports(Query(fd, {60, 5, 0, Unlock, FromStart, 0}), WriteLock, 50, 0, child)); + const GuestLock between{10, 10, 0, Unlock, FromStart, 0}; + Check(Unchanged(Query(fd, between), between)); + + const GuestLock sharedRead{0, 15, 0, ReadLock, FromStart, 0}; + Check(Unchanged(Query(fd, sharedRead), sharedRead)); + Check(Reports(Query(fd, {0, 15, 0, WriteLock, FromStart, 0}), ReadLock, 0, 10, child)); + Check(Reports(Query(fd, {22, 2, 0, ReadLock, FromStart, 0}), WriteLock, 20, 10, child)); + + GuestLock blocked{5, 1, 0, WriteLock, FromStart, 0}; + *__error_nid_postfix() = 0; + Check(fcntl_nid_postfix(fd, SetLock, &blocked) == -1 && *__error_nid_postfix() == GuestEagain); + + const char done = 'd'; + Check(::write(release[1], &done, 1) == 1); + int status = 0; + Check(::waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0); + + const GuestLock afterExit{0, 0, 0, Unlock, FromStart, 0}; + Check(Unchanged(Query(fd, afterExit), afterExit)); + Check(sceKernelClose(fd) == 0); +} + +} + +int main() { + const auto root = std::filesystem::temp_directory_path() / + ("anyps5-record-lock-owners-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + std::filesystem::create_directories(root); + std::filesystem::current_path(root); + { std::ofstream file(FileName, std::ios::binary); file << std::string(100, 'x'); } + OtherOwner(); + std::error_code error; + std::filesystem::current_path(std::filesystem::temp_directory_path(), error); + std::filesystem::remove_all(root, error); + return 0; +} diff --git a/core/libs/tests/GuestRefusedConnect.cpp b/core/libs/tests/GuestRefusedConnect.cpp new file mode 100644 index 0000000000..409f8ad97c --- /dev/null +++ b/core/libs/tests/GuestRefusedConnect.cpp @@ -0,0 +1,108 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI listen_nid_postfix(int, int); +int APS5_VABI accept_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI close_nid_postfix(int); +int* APS5_VABI __error_nid_postfix(); +int APS5_VABI sceNetInit_nid_postfix(); +int APS5_VABI sceNetSocket(const char*, int, int, int); +int APS5_VABI sceNetConnect(int, const void*, std::uint32_t); +int APS5_VABI sceNetSocketClose(int); +int* APS5_VABI sceNetErrnoLoc(); +} + +namespace { + +constexpr int GuestInet = 2; +constexpr int GuestStream = 1; +constexpr int GuestConnectionRefused = 61; +constexpr auto RefusalLimit = std::chrono::milliseconds(1000); + +void Require(bool value) { if (!value) std::abort(); } + +std::array ClosedLoopbackPort() { + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + const int descriptor = socket_nid_postfix(GuestInet, GuestStream, 0); + Require(descriptor >= 0); + Require(bind_nid_postfix(descriptor, address.data(), address.size()) == 0); + std::uint32_t length = address.size(); + Require(getsockname_nid_postfix(descriptor, address.data(), &length) == 0); + Require(close_nid_postfix(descriptor) == 0); + Require(address[2] != 0 || address[3] != 0); + return address; +} + +template +bool ConnectsOnceBacklogDrains(TConnect connect) { + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + const int listener = socket_nid_postfix(GuestInet, GuestStream, 0); + Require(listener >= 0); + Require(bind_nid_postfix(listener, address.data(), address.size()) == 0); + std::uint32_t length = address.size(); + Require(getsockname_nid_postfix(listener, address.data(), &length) == 0); + Require(listen_nid_postfix(listener, 1) == 0); + const int queued = socket_nid_postfix(GuestInet, GuestStream, 0); + Require(queued >= 0 && connect_nid_postfix(queued, address.data(), address.size()) == 0); + std::thread acceptor([listener] { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + const int accepted = accept_nid_postfix(listener, nullptr, nullptr); + Require(accepted >= 0 && close_nid_postfix(accepted) == 0); + }); + const bool connected = connect(address); + acceptor.join(); + Require(close_nid_postfix(queued) == 0 && close_nid_postfix(listener) == 0); + return connected; +} + +template +bool RefusedAtOnce(TConnect connect) { + const auto start = std::chrono::steady_clock::now(); + const bool refused = connect(); + return refused && std::chrono::steady_clock::now() - start < RefusalLimit; +} + +} + +int main() { + Require(sceNetInit_nid_postfix() == 0); + const auto address = ClosedLoopbackPort(); + Require(RefusedAtOnce([&] { + const int descriptor = socket_nid_postfix(GuestInet, GuestStream, 0); + Require(descriptor >= 0); + const bool refused = connect_nid_postfix(descriptor, address.data(), address.size()) == -1 && *__error_nid_postfix() == GuestConnectionRefused; + Require(close_nid_postfix(descriptor) == 0); + return refused; + })); + Require(RefusedAtOnce([&] { + const int descriptor = sceNetSocket("refused", GuestInet, GuestStream, 0); + Require(descriptor >= 0); + const bool refused = sceNetConnect(descriptor, address.data(), address.size()) == static_cast(0x80410100u | GuestConnectionRefused) + && *sceNetErrnoLoc() == GuestConnectionRefused; + Require(sceNetSocketClose(descriptor) == 0); + return refused; + })); + Require(ConnectsOnceBacklogDrains([](const std::array& listening) { + const int descriptor = socket_nid_postfix(GuestInet, GuestStream, 0); + Require(descriptor >= 0); + const bool connected = connect_nid_postfix(descriptor, listening.data(), listening.size()) == 0; + Require(close_nid_postfix(descriptor) == 0); + return connected; + })); + Require(ConnectsOnceBacklogDrains([](const std::array& listening) { + const int descriptor = sceNetSocket("backlog", GuestInet, GuestStream, 0); + Require(descriptor >= 0); + const bool connected = sceNetConnect(descriptor, listening.data(), listening.size()) == 0; + Require(sceNetSocketClose(descriptor) == 0); + return connected; + })); +} diff --git a/core/libs/tests/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/GuestResolver.cpp b/core/libs/tests/GuestResolver.cpp index 33bbed3d4a..20f02e1d16 100644 --- a/core/libs/tests/GuestResolver.cpp +++ b/core/libs/tests/GuestResolver.cpp @@ -113,8 +113,13 @@ int main() { Require(getnameinfo_nid_postfix(v4.data(), 2, host, sizeof(host), nullptr, 0, 10) == 5); Require(getnameinfo_nid_postfix(v4.data(), v4.size(), host, sizeof(host), nullptr, 0, 32) == 3); Require(getnameinfo_nid_postfix(v4.data(), v4.size(), host, 1, nullptr, 0, 10) == 14); - Require(std::strlen(gai_strerror_nid_postfix(14)) > 0); - Require(std::strstr(gai_strerror_nid_postfix(-1), "Unknown") != nullptr); + Require(std::strcmp(gai_strerror_nid_postfix(0), "Success") == 0); + Require(std::strcmp(gai_strerror_nid_postfix(3), "Invalid value for ai_flags") == 0); + Require(std::strcmp(gai_strerror_nid_postfix(8), "hostname nor servname provided, or not known") == 0); + Require(std::strcmp(gai_strerror_nid_postfix(10), "ai_socktype not supported") == 0); + Require(std::strcmp(gai_strerror_nid_postfix(14), "Argument buffer overflow") == 0); + Require(std::strcmp(gai_strerror_nid_postfix(-1), "Unknown error") == 0); + Require(std::strcmp(gai_strerror_nid_postfix(15), "Unknown error") == 0); GuestResolver::AddressInfo hints{}; hints.flags = 12; // numeric host and service: no external DNS dependency hints.socketType = 1; diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index bf3528b2c6..e6bc5bd2d5 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,45 @@ 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*); } static void Require(bool value) { if (!value) std::abort(); } @@ -55,6 +86,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 +127,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 +158,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 +190,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})); @@ -246,6 +366,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}, @@ -262,6 +387,9 @@ int main() { {"Thu, 01 Mar 2024 12:34:56 ", 0, march, 0}, {"Thu, 01 Mar 2024 12:34:567 EST", 0, march, 0}, {"Thu, 01 Mar 2024 12:34 EST", 0, at(2024, 3, 1, 12, 34, 0), 0}, + {"Thu, 29 Feb 2024 12:34", badParse, untouched, 0}, + {"Thu, 01 Mar 2024 12:34", badParse, untouched, 0}, + {"Thu, 29 Feb 2024 12:34 ", 0, at(2024, 2, 29, 12, 34, 0), 0}, {"Thu, 01 Mar 2024 12:34EST", badParse, untouched, 0}, {"Thu, 01 Mar 2024 1:2:3 EST", 0, at(2024, 3, 1, 1, 2, 3), -300}, {"Thu, 01 Mar 2024 26:00:00 GMT", badParse, untouched, 0}, @@ -299,6 +427,35 @@ int main() { Require(tick.tick == dateTime.local - static_cast(dateTime.minutes * 60000000)); } + const struct { + const char* name; + std::int64_t minutes; + } additionalZones[] = { + {"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}, + }; + for (const auto& zone : additionalZones) { + char text[80]; + std::snprintf(text, sizeof(text), "Thu, 29 Feb 2024 12:34:56 %s", zone.name); + tick.tick = untouched; + Require(sceRtcParseDateTime(&tick, text) == 0); + Require(tick.tick == leapDayTick - 789000ull - static_cast(zone.minutes * 60000000)); + char lower[16]{}; + for (std::size_t i = 0; zone.name[i] != 0; ++i) lower[i] = static_cast(zone.name[i] + ('a' - 'A')); + std::snprintf(text, sizeof(text), "Thu, 29 Feb 2024 12:34:56 %s", lower); + tick.tick = untouched; + Require(sceRtcParseDateTime(&tick, text) == 0); + Require(tick.tick == leapDayTick - 789000ull - static_cast(zone.minutes * 60000000)); + std::snprintf(text, sizeof(text), "Thu, 29 Feb 2024 12:34:56 %sextra", zone.name); + tick.tick = untouched; + Require(sceRtcParseDateTime(&tick, text) == 0); + Require(tick.tick == leapDayTick - 789000ull - static_cast(zone.minutes * 60000000)); + std::snprintf(text, sizeof(text), "Wed, 01 Mar 2000 08:09:10 %s", zone.name); + tick.tick = untouched; + Require(sceRtcParseDateTime(&tick, text) == 0); + Require(tick.tick == at(2000, 3, 1, 8, 9, 10) - static_cast(zone.minutes * 60000000)); + } + RtcTick source{leapDayTick}; RtcTick result{}; Require(sceRtcTickAddSeconds(&result, &source, 3600) == 0 && result.tick == leapDayTick + 3600000000ull); @@ -310,31 +467,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); @@ -342,13 +488,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); @@ -427,4 +802,71 @@ int main() { Require(sceRtcConvertLocalTimeToUtc(&result, &result) == 0 && result.tick == source.tick); } SetTimeZone("UTC0"); + +#ifndef _WIN32 + SetTimeZone("STD0DST,J1/0,J2/0"); + const std::time_t beforeTransition = -1; + const std::time_t atTransition = 0; + std::tm beforeState{}, afterState{}; + Require(localtime_r(&beforeTransition, &beforeState) != nullptr && beforeState.tm_isdst == 0); + Require(localtime_r(&atTransition, &afterState) != nullptr && afterState.tm_isdst == 1); + for (const std::uint64_t distance : {1000000ull, 500000ull, 1ull}) { + source.tick = unixEpochTick - distance; + Require(sceRtcConvertUtcToLocalTime(&source, &result) == 0 && result.tick == source.tick); + Require(sceRtcConvertUtcToLocalTime(&source, &source) == 0 && source.tick == unixEpochTick - distance); + } + for (const std::uint64_t distance : {0ull, 500000ull}) { + source.tick = unixEpochTick + distance; + Require(sceRtcConvertUtcToLocalTime(&source, &result) == 0 && result.tick == source.tick + 3600000000ull); + } + tick.tick = unixEpochTick - 500000ull; + Require(sceRtcFormatRFC3339LocalTime(text, &tick) == 0 && std::strcmp(text, "1969-12-31T23:59:59.50Z") == 0); + 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); + } } diff --git a/core/libs/tests/GuestRudpContextLifecycle.cpp b/core/libs/tests/GuestRudpContextLifecycle.cpp new file mode 100644 index 0000000000..f043d30a99 --- /dev/null +++ b/core/libs/tests/GuestRudpContextLifecycle.cpp @@ -0,0 +1,113 @@ +#include "SceTypes.hpp" + +#include +#include +#include +#include +#include + +extern "C" { +using GuestRudpEventHandler = void(APS5_VABI*)(int, int, int, void*); +int APS5_VABI sceRudpInit_nid_postfix(void*, int); +int APS5_VABI sceRudpEnd(); +int APS5_VABI sceRudpActivate(); +int APS5_VABI sceRudpEnableInternalIOThread(std::uint32_t, std::uint32_t); +int APS5_VABI sceRudpSetEventHandler(GuestRudpEventHandler, void*); +int APS5_VABI sceRudpCreateContext(GuestRudpEventHandler, void*, int*); +int APS5_VABI sceRudpBind(int, std::uint16_t, int, std::uint16_t); +int APS5_VABI sceRudpSetOption(int, int, const void*, std::uint32_t); +int APS5_VABI sceRudpInitiate(int, const void*, std::uint32_t, std::uint16_t); +int APS5_VABI sceRudpGetContextStatus(int, void*, std::uint32_t); +int APS5_VABI sceRudpWrite(int, const void*, std::uint32_t, int); +int APS5_VABI sceRudpRead(int, void*, std::uint32_t, int, void*); +int APS5_VABI sceRudpTerminate(int); +} + +static void Require(bool value) { if (!value) std::abort(); } + +static void APS5_VABI Handler(int, int, int, void*) {} + +int main() { + constexpr int notInitialized = static_cast(0x80770001u); + constexpr int invalidContextId = static_cast(0x80770003u); + constexpr int connRefused = static_cast(0x8077000Au); + constexpr int connReset = static_cast(0x80770009u); + alignas(16) static unsigned char pool[0x10000]; + + Require(sceRudpActivate() == 0); + + Require(sceRudpEnableInternalIOThread(0, 0) == notInitialized); + + Require(sceRudpSetEventHandler(Handler, nullptr) == 0); + + Require(sceRudpBind(0, 0, 0, 0) == notInitialized); + Require(sceRudpBind(12345, 0, 0, 0) == notInitialized); + + Require(sceRudpSetOption(0, 0, nullptr, 0) == notInitialized); + Require(sceRudpSetOption(12345, 0, nullptr, 0) == notInitialized); + + Require(sceRudpInitiate(0, nullptr, 0, 0) == notInitialized); + Require(sceRudpInitiate(12345, nullptr, 0, 0) == notInitialized); + + { + unsigned char status[64] = {}; + Require(sceRudpGetContextStatus(0, status, sizeof(status)) == notInitialized); + Require(sceRudpGetContextStatus(12345, status, sizeof(status)) == notInitialized); + } + + Require(sceRudpWrite(0, nullptr, 0, 0) == notInitialized); + Require(sceRudpWrite(12345, nullptr, 0, 0) == notInitialized); + + Require(sceRudpRead(0, nullptr, 0, 0, nullptr) == notInitialized); + Require(sceRudpRead(12345, nullptr, 0, 0, nullptr) == notInitialized); + + Require(sceRudpInit_nid_postfix(pool, sizeof(pool)) == 0); + + Require(sceRudpActivate() == 0); + + Require(sceRudpEnableInternalIOThread(0, 0) == 0); + Require(sceRudpEnableInternalIOThread(1024, 1) == 0); + + Require(sceRudpBind(12345, 0, 0, 0) == invalidContextId); + + Require(sceRudpSetOption(12345, 0, nullptr, 0) == invalidContextId); + + Require(sceRudpInitiate(12345, nullptr, 0, 0) == invalidContextId); + + { + unsigned char status[64] = {}; + Require(sceRudpGetContextStatus(12345, status, sizeof(status)) == invalidContextId); + } + + Require(sceRudpWrite(12345, nullptr, 0, 0) == invalidContextId); + + Require(sceRudpRead(12345, nullptr, 0, 0, nullptr) == invalidContextId); + + int ctx = -1; + Require(sceRudpCreateContext(Handler, nullptr, &ctx) == 0); + Require(ctx >= 0); + + Require(sceRudpBind(ctx, 0, 0, 0) == 0); + + Require(sceRudpSetOption(ctx, 0, nullptr, 0) == 0); + + Require(sceRudpInitiate(ctx, nullptr, 0, 0) == connRefused); + + { + unsigned char status[64]; + std::memset(status, 0x5a, sizeof(status)); + Require(sceRudpGetContextStatus(ctx, status, sizeof(status)) == 0); + for (std::size_t i = 0; i < sizeof(status); ++i) Require(status[i] == 0); + } + Require(sceRudpGetContextStatus(ctx, nullptr, 64) == 0); + + Require(sceRudpWrite(ctx, nullptr, 0, 0) == connReset); + + Require(sceRudpRead(ctx, nullptr, 0, 0, nullptr) == connReset); + + Require(sceRudpSetEventHandler(Handler, nullptr) == 0); + + Require(sceRudpTerminate(ctx) == 0); + Require(sceRudpEnd() == 0); + return 0; +} \ No newline at end of file diff --git a/core/libs/tests/GuestRudpContextQueries.cpp b/core/libs/tests/GuestRudpContextQueries.cpp new file mode 100644 index 0000000000..76374d4b86 --- /dev/null +++ b/core/libs/tests/GuestRudpContextQueries.cpp @@ -0,0 +1,55 @@ +#include "SceTypes.hpp" + +#include +#include + +extern "C" { +using GuestRudpEventHandler = void(APS5_VABI*)(int, int, int, void*); +int APS5_VABI sceRudpInit_nid_postfix(void*, int); +int APS5_VABI sceRudpCreateContext(GuestRudpEventHandler, void*, int*); +int APS5_VABI sceRudpTerminate(int); +int APS5_VABI sceRudpEnd(); +int APS5_VABI sceRudpGetNumberOfPacketsToRead(int); +int APS5_VABI sceRudpGetSizeWritable(int); +int APS5_VABI sceRudpGetSizeReadable(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 notAcceptable = static_cast(0x80770011u); + alignas(16) static std::array pool{}; + + Require(sceRudpGetNumberOfPacketsToRead(0) == notInitialized); + Require(sceRudpGetSizeWritable(0) == notInitialized); + Require(sceRudpGetSizeReadable(0) == notInitialized); + Require(sceRudpInit_nid_postfix(pool.data(), static_cast(pool.size())) == 0); + Require(sceRudpGetNumberOfPacketsToRead(0) == invalidContext); + Require(sceRudpGetSizeWritable(0) == invalidContext); + Require(sceRudpGetSizeReadable(0) == invalidContext); + Require(sceRudpGetNumberOfPacketsToRead(12345) == invalidContext); + Require(sceRudpGetSizeWritable(12345) == invalidContext); + Require(sceRudpGetSizeReadable(12345) == invalidContext); + Require(sceRudpGetNumberOfPacketsToRead(-1) == invalidArgument); + Require(sceRudpGetSizeWritable(-1) == invalidArgument); + Require(sceRudpGetSizeReadable(-1) == invalidArgument); + + int context = -1; + Require(sceRudpCreateContext(Handler, nullptr, &context) == 0); + Require(sceRudpGetNumberOfPacketsToRead(context) == 0); + Require(sceRudpGetSizeWritable(context) == notAcceptable); + Require(sceRudpGetSizeReadable(context) == notAcceptable); + Require(sceRudpTerminate(context) == 0); + Require(sceRudpGetNumberOfPacketsToRead(context) == invalidContext); + Require(sceRudpGetSizeWritable(context) == invalidContext); + Require(sceRudpGetSizeReadable(context) == invalidContext); + 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..d797fad444 --- /dev/null +++ b/core/libs/tests/GuestRudpPoll.cpp @@ -0,0 +1,124 @@ +#include "SceTypes.hpp" + +#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::uint32_t); +int APS5_VABI sceRudpPollControl(int, int, int, std::uint16_t); +int APS5_VABI sceRudpPollWait(int, GuestRudpPollEvent*, std::uint32_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 new file mode 100644 index 0000000000..d9e350ae23 --- /dev/null +++ b/core/libs/tests/GuestRudpPump.cpp @@ -0,0 +1,58 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#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 sceRudpNetFlush(); +int APS5_VABI sceRudpGetMaxSegmentSize(std::uint16_t*); +int APS5_VABI sceRudpNetReceived(); +int APS5_VABI sceRudpEnableInternalIOThread2(std::uint32_t, std::uint32_t, std::uint64_t); +} + +static void Require(bool value, int line) { + if (!value) { + std::fprintf(stderr, "rudp check failed at line %d\n", line); + std::abort(); + } +} +#define CHECK(value) Require((value), __LINE__) + +int main() { + constexpr int notInitialized = static_cast(0x80770001); + constexpr int invalidArgument = static_cast(0x80770004); + std::uint16_t segmentSize = 0x5a5a; + CHECK(sceRudpProcessEvents() == notInitialized); + CHECK(sceRudpNetFlush() == notInitialized); + CHECK(sceRudpNetReceived() == 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(sceRudpNetFlush() == 0); + CHECK(sceRudpNetReceived() == 0); + CHECK(sceRudpEnableInternalIOThread2(0x4000, 0, 0x3) == 0); + CHECK(sceRudpGetMaxSegmentSize(nullptr) == invalidArgument); + CHECK(sceRudpGetMaxSegmentSize(&segmentSize) == 0); + CHECK(segmentSize == 1410); + std::uint16_t segment[3] = {0, 0, 0x5a5a}; + CHECK(sceRudpGetMaxSegmentSize(&segment[1]) == 0); + CHECK(segment[0] == 0 && segment[1] == 1410 && segment[2] == 0x5a5a); + CHECK(sceRudpEnd() == 0); + + segmentSize = 0x5a5a; + CHECK(sceRudpGetMaxSegmentSize(&segmentSize) == notInitialized); + CHECK(segmentSize == 0x5a5a); + + CHECK(sceRudpNetReceived() == notInitialized); + return 0; +} diff --git a/core/libs/tests/GuestRudpStatus.cpp b/core/libs/tests/GuestRudpStatus.cpp new file mode 100644 index 0000000000..ab845fffac --- /dev/null +++ b/core/libs/tests/GuestRudpStatus.cpp @@ -0,0 +1,92 @@ +#include "SceTypes.hpp" + +#include +#include +#include +#include +#include + +extern "C" { +using GuestRudpEventHandler = void(APS5_VABI*)(int, int, int, void*); +int APS5_VABI sceRudpInit_nid_postfix(void*, int); +int APS5_VABI sceRudpGetStatus(void*, std::size_t); +int APS5_VABI sceRudpCreateContext(GuestRudpEventHandler, void*, int*); +int APS5_VABI sceRudpTerminate(int); +int APS5_VABI sceRudpEnd(); +} + +static void Require(bool value) { if (!value) std::abort(); } + +static void APS5_VABI Handler(int, int, int, void*) {} + +int main() { + constexpr int notInitialized = static_cast(0x80770001); + constexpr int alreadyInitialized = static_cast(0x80770002); + constexpr int invalidContextId = static_cast(0x80770003); + constexpr int invalidArgument = static_cast(0x80770004); + constexpr int memory = static_cast(0x80770007); + constexpr std::size_t statusSize = 0xF8; + constexpr std::size_t currentContexts = 0x64; + alignas(16) static unsigned char pool[0x10000]; + + std::array status; + status.fill(0x5a); + const auto untouched = status; + Require(sceRudpGetStatus(status.data(), statusSize) == notInitialized); + Require(sceRudpGetStatus(nullptr, 0) == notInitialized); + Require(sceRudpGetStatus(status.data(), statusSize + 1) == notInitialized); + Require(status == untouched); + Require(sceRudpEnd() == notInitialized); + Require(sceRudpTerminate(0) == notInitialized); + + Require(sceRudpInit_nid_postfix(nullptr, sizeof(pool)) == invalidArgument); + Require(sceRudpInit_nid_postfix(pool, 0) == invalidArgument); + Require(sceRudpInit_nid_postfix(pool, -1) == invalidArgument); + Require(sceRudpInit_nid_postfix(pool, 0xC57) == memory); + for (std::size_t offset : {1u, 2u, 4u}) Require(sceRudpInit_nid_postfix(pool + offset, 0x1000) == memory); + Require(sceRudpInit_nid_postfix(pool + 8, 0xC58) == 0); + Require(sceRudpInit_nid_postfix(nullptr, 0) == alreadyInitialized); + + Require(sceRudpGetStatus(nullptr, statusSize) == invalidArgument); + Require(sceRudpGetStatus(nullptr, 0) == invalidArgument); + Require(sceRudpGetStatus(status.data(), 0) == invalidArgument); + Require(sceRudpGetStatus(status.data(), statusSize + 1) == invalidArgument); + Require(status == untouched); + + Require(sceRudpGetStatus(status.data(), statusSize) == 0); + for (std::size_t i = 0; i < statusSize; ++i) Require(status[i] == 0); + for (std::size_t i = statusSize; i < status.size(); ++i) Require(status[i] == 0x5a); + + int first = -1; + int second = -1; + Require(sceRudpCreateContext(Handler, nullptr, &first) == 0); + Require(sceRudpCreateContext(Handler, nullptr, &second) == 0); + status.fill(0x5a); + Require(sceRudpGetStatus(status.data(), statusSize) == 0); + std::uint32_t contexts = 0; + std::memcpy(&contexts, status.data() + currentContexts, sizeof(contexts)); + Require(contexts == 2); + for (std::size_t i = 0; i < statusSize; ++i) { + if (i < currentContexts || i >= currentContexts + sizeof(contexts)) Require(status[i] == 0); + } + + status.fill(0x5a); + Require(sceRudpGetStatus(status.data(), 1) == 0); + Require(status[0] == 0); + for (std::size_t i = 1; i < status.size(); ++i) Require(status[i] == 0x5a); + + Require(sceRudpTerminate(first) == 0); + Require(sceRudpTerminate(first) == invalidContextId); + Require(sceRudpGetStatus(status.data(), statusSize) == 0); + std::memcpy(&contexts, status.data() + currentContexts, sizeof(contexts)); + Require(contexts == 1); + + Require(sceRudpEnd() == 0); + Require(sceRudpEnd() == notInitialized); + Require(sceRudpTerminate(second) == notInitialized); + Require(sceRudpGetStatus(status.data(), statusSize) == notInitialized); + Require(sceRudpInit_nid_postfix(pool + 8, 0xC58) == 0); + Require(sceRudpTerminate(second) == invalidContextId); + Require(sceRudpEnd() == 0); + return 0; +} diff --git a/core/libs/tests/GuestRuneLocale.cpp b/core/libs/tests/GuestRuneLocale.cpp new file mode 100644 index 0000000000..e677153ff7 --- /dev/null +++ b/core/libs/tests/GuestRuneLocale.cpp @@ -0,0 +1,151 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +extern const unsigned char _DefaultRuneLocale_nid_postfix[4224]; +extern const unsigned char* _CurrentRuneLocale_nid_postfix; +extern int __mb_sb_limit_nid_postfix; +int APS5_VABI isalpha_nid_postfix(int); +int APS5_VABI iscntrl_nid_postfix(int); +int APS5_VABI isdigit_nid_postfix(int); +int APS5_VABI isgraph_nid_postfix(int); +int APS5_VABI islower_nid_postfix(int); +int APS5_VABI isprint_nid_postfix(int); +int APS5_VABI ispunct_nid_postfix(int); +int APS5_VABI isspace_nid_postfix(int); +int APS5_VABI isupper_nid_postfix(int); +int APS5_VABI isxdigit_nid_postfix(int); +int APS5_VABI isblank_nid_postfix(int); +int APS5_VABI tolower_nid_postfix(int); +int APS5_VABI toupper_nid_postfix(int); +int APS5_VABI iswalnum_nid_postfix(std::uint32_t); +int APS5_VABI iswalpha_nid_postfix(std::uint32_t); +int APS5_VABI iswblank_nid_postfix(std::uint32_t); +int APS5_VABI iswcntrl_nid_postfix(std::uint32_t); +int APS5_VABI iswdigit_nid_postfix(std::uint32_t); +int APS5_VABI iswgraph_nid_postfix(std::uint32_t); +int APS5_VABI iswlower_nid_postfix(std::uint32_t); +int APS5_VABI iswprint_nid_postfix(std::uint32_t); +int APS5_VABI iswpunct_nid_postfix(std::uint32_t); +int APS5_VABI iswspace_nid_postfix(std::uint32_t); +int APS5_VABI iswupper_nid_postfix(std::uint32_t); +int APS5_VABI iswxdigit_nid_postfix(std::uint32_t); +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); +} + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "Rune locale check failed at line %u\n", location.line()); + std::abort(); + } +} + +template +T Field(const unsigned char* locale, std::size_t offset) { + T value; + std::memcpy(&value, locale + offset, sizeof(value)); + return value; +} + +int main() { + const unsigned char* locale = _DefaultRuneLocale_nid_postfix; + Require(_CurrentRuneLocale_nid_postfix == locale); + Require(__mb_sb_limit_nid_postfix == 256); + Require(std::memcmp(locale, "RuneMagi", 8) == 0); + Require(std::strcmp(reinterpret_cast(locale + 8), "NONE") == 0); + Require(Field(locale, 40) == nullptr && Field(locale, 48) == nullptr); + Require(Field(locale, 56) == 0xfffd); + for (std::size_t offset = 4160; offset < 4224; ++offset) Require(locale[offset] == 0); + const struct { + std::uint64_t bit; + int (APS5_VABI* classify)(int); + } classes[] = { + {0x100, isalpha_nid_postfix}, {0x200, iscntrl_nid_postfix}, {0x400, isdigit_nid_postfix}, + {0x800, isgraph_nid_postfix}, {0x1000, islower_nid_postfix}, {0x2000, ispunct_nid_postfix}, + {0x4000, isspace_nid_postfix}, {0x8000, isupper_nid_postfix}, {0x10000, isxdigit_nid_postfix}, + {0x20000, isblank_nid_postfix}, {0x40000, isprint_nid_postfix}, + }; + for (int c = 0; c < 256; ++c) { + const auto type = Field(locale, 64 + 8 * c); + for (const auto& entry : classes) Require(((type & entry.bit) != 0) == (entry.classify(c) != 0)); + Require(((type & 0x400000) != 0) == (isdigit_nid_postfix(c) != 0)); + Require((type & ~std::uint64_t{0x47ffff}) == 0); + if (isxdigit_nid_postfix(c)) { + const int value = c <= '9' ? c - '0' : (c | 0x20) - 'a' + 10; + Require(static_cast(type & 0xff) == value); + } else { + Require((type & 0xff) == 0); + } + Require(Field(locale, 2112 + 4 * c) == tolower_nid_postfix(c)); + Require(Field(locale, 3136 + 4 * c) == toupper_nid_postfix(c)); + } + Require(Field(locale, 64 + 8 * 0x80) == 0 && Field(locale, 64 + 8 * 0xff) == 0); + + const struct { + const char* name; + std::uint64_t mask; + int (APS5_VABI* wide)(std::uint32_t); + int (APS5_VABI* narrow)(int); + } wide[] = { + {"alnum", 0x400100, iswalnum_nid_postfix, nullptr}, {"alpha", 0x100, iswalpha_nid_postfix, isalpha_nid_postfix}, + {"blank", 0x20000, iswblank_nid_postfix, isblank_nid_postfix}, {"cntrl", 0x200, iswcntrl_nid_postfix, iscntrl_nid_postfix}, + {"digit", 0x400, iswdigit_nid_postfix, isdigit_nid_postfix}, {"graph", 0x800, iswgraph_nid_postfix, isgraph_nid_postfix}, + {"lower", 0x1000, iswlower_nid_postfix, islower_nid_postfix}, {"print", 0x40000, iswprint_nid_postfix, isprint_nid_postfix}, + {"punct", 0x2000, iswpunct_nid_postfix, ispunct_nid_postfix}, {"space", 0x4000, iswspace_nid_postfix, isspace_nid_postfix}, + {"upper", 0x8000, iswupper_nid_postfix, isupper_nid_postfix}, {"xdigit", 0x10000, iswxdigit_nid_postfix, isxdigit_nid_postfix}, + }; + for (const auto& entry : wide) { + Require(wctype_nid_postfix(entry.name) == entry.mask); + for (std::uint32_t c = 0; c < 300; ++c) { + const int expected = c < 256 && (entry.narrow ? entry.narrow(static_cast(c)) : isalpha_nid_postfix(static_cast(c)) || isdigit_nid_postfix(static_cast(c))); + Require((entry.wide(c) != 0) == (expected != 0)); + Require((iswctype_nid_postfix(c, entry.mask) != 0) == (expected != 0)); + } + Require(entry.wide(0xffffffff) == 0 && iswctype_nid_postfix(0xffffffff, entry.mask) == 0); + } + Require(wctype_nid_postfix("ideogram") == 0x80000 && wctype_nid_postfix("special") == 0x100000); + Require(wctype_nid_postfix("phonogram") == 0x200000 && wctype_nid_postfix("number") == 0x400000); + Require(wctype_nid_postfix("rune") == 0xffffff00 && wctype_nid_postfix("") == 0 && wctype_nid_postfix("Alpha") == 0); + Require(iswctype_nid_postfix('A', 0) == 0 && iswctype_nid_postfix('7', wctype_nid_postfix("number")) != 0); + for (std::uint32_t c = 0; c < 300; ++c) { + Require(towlower_nid_postfix(c) == (c < 256 ? static_cast(tolower_nid_postfix(static_cast(c))) : c)); + Require(towupper_nid_postfix(c) == (c < 256 ? static_cast(toupper_nid_postfix(static_cast(c))) : c)); + } + Require(towlower_nid_postfix(0xffffffff) == 0xffffffff && towupper_nid_postfix(0xffffffff) == 0xffffffff); + Require(btowc_nid_postfix(-1) == 0xffffffff && btowc_nid_postfix(0) == 0); + 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 new file mode 100644 index 0000000000..9b8087c031 --- /dev/null +++ b/core/libs/tests/GuestSaveDataCancel.cpp @@ -0,0 +1,18 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" int APS5_VABI sceSaveDataCancel(); + +int main() { + try { + sceSaveDataCancel(); + } 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()); + return 1; + } + std::fprintf(stderr, "sceSaveDataCancel returned instead of reporting that it is not implemented\n"); + return 1; +} diff --git a/core/libs/tests/GuestSaveDataConvert.cpp b/core/libs/tests/GuestSaveDataConvert.cpp new file mode 100644 index 0000000000..580d22e892 --- /dev/null +++ b/core/libs/tests/GuestSaveDataConvert.cpp @@ -0,0 +1,92 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceSaveDataConvert(const SaveDataConvert*); +int APS5_VABI sceSaveDataGetConvertProgress(float*); +int APS5_VABI sceSaveDataInitialize3(const void*); +int APS5_VABI sceSaveDataTerminate(); +} + +namespace { + +constexpr int SaveDataErrorParameter = -2137063424; +constexpr int SaveDataErrorNotInitialized = -2137063423; +constexpr int SaveDataErrorNotFound = -2137063416; + +int failures = 0; + +void Check(bool condition, const std::string& what) { + if (condition) return; + std::fprintf(stderr, "savedata convert check failed: %s\n", what.c_str()); + ++failures; +} + +int Convert(const char* data, std::size_t size) { + SceSaveDataDirName name{}; + std::memcpy(name.data, data, size); + SaveDataConvert convert{}; + convert.user_id = 0x10000000; + convert.dir_name = &name; + return sceSaveDataConvert(&convert); +} + +std::vector Read(const std::filesystem::path& path) { + std::ifstream stream(path, std::ios::binary); + return {std::istreambuf_iterator(stream), std::istreambuf_iterator()}; +} + +} + +int main() { + const auto root = std::filesystem::temp_directory_path() / ("anyps5-savedata-convert-" + std::to_string(std::random_device{}())); + const auto work = root / "work"; + const auto kept = work / "_sd" / "kept" / "data.bin"; + std::filesystem::create_directories(kept.parent_path()); + std::ofstream(kept) << "data"; + const auto previous = std::filesystem::current_path(); + std::filesystem::current_path(work); + + float progress = 0.0f; + Check(sceSaveDataGetConvertProgress(&progress) == SaveDataErrorNotInitialized, "query before initialize fails with not initialized"); + Check(sceSaveDataInitialize3(nullptr) == 0, "SaveData initializes"); + Check(sceSaveDataGetConvertProgress(&progress) == 0, "get convert progress succeeds"); + Check(progress == 0.0f, "convert progress reports 0.0f initially"); + Check(sceSaveDataGetConvertProgress(nullptr) == SaveDataErrorParameter, "null progress pointer is rejected"); + Check(Convert("missing", sizeof("missing")) == SaveDataErrorNotFound, "a missing save is not found"); + Check(sceSaveDataGetConvertProgress(&progress) == 0, "get convert progress succeeds after failed convert"); + Check(progress < 1.0f, "convert progress is below 1.0f after failed convert"); + Check(progress == 0.0f, "convert progress reports 0.0f after failed convert"); + Check(Convert("kept", sizeof("kept")) == 0, "an existing save converts"); + Check(Read(kept) == std::vector{'d', 'a', 't', 'a'}, "the converted save keeps its files"); + Check(sceSaveDataGetConvertProgress(&progress) == 0, "get convert progress succeeds after successful convert"); + Check(progress == 1.0f, "convert progress reports 1.0f after successful convert"); + Check(Convert("missing", sizeof("missing")) == SaveDataErrorNotFound, "a missing save is not found"); + Check(sceSaveDataGetConvertProgress(&progress) == 0, "get convert progress succeeds after subsequent failed convert"); + Check(progress < 1.0f, "convert progress drops below 1.0f after subsequent failed convert"); + Check(progress == 0.0f, "convert progress reports 0.0f after subsequent failed convert"); + for (const char* invalid : {"../kept", "", ".", "..", "a\\b", "c:d"}) { + Check(Convert(invalid, std::strlen(invalid) + 1) == SaveDataErrorParameter, std::string("\"") + invalid + "\" is rejected"); + } + char unterminated[sizeof(SceSaveDataDirName::data)]; + std::memset(unterminated, 'a', sizeof(unterminated)); + Check(Convert(unterminated, sizeof(unterminated)) == SaveDataErrorParameter, "an unterminated name is rejected"); + Check(sceSaveDataTerminate() == 0, "SaveData terminates"); + Check(sceSaveDataGetConvertProgress(&progress) == SaveDataErrorNotInitialized, "query after terminate fails with not initialized"); + + std::filesystem::current_path(previous); + std::error_code error; + std::filesystem::remove_all(root, error); + if (failures != 0) return 1; + std::printf("savedata convert tests passed\n"); + return 0; +} 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 c51a1cb5c1..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,10 @@ 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; int failures = 0; @@ -27,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); @@ -38,9 +112,112 @@ 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"); } +void CheckFailedMountKeepsResult() { + SceSaveDataDirName held{}; + std::memcpy(held.data, "held", sizeof("held")); + SaveDataMount3 mount{}; + mount.dir_name = &held; + mount.mount_mode = MountReadWrite | MountCreate2; + SaveDataMountResult result{}; + Check(sceSaveDataMount3(&mount, &result) == 0, "first mount of a save"); + const SaveDataMountResult mounted = result; + Check(sceSaveDataMount3(&mount, &result) == SaveDataErrorBusy, "second mount of a mounted save is BUSY"); + Check(std::memcmp(&result, &mounted, sizeof(result)) == 0, "a BUSY mount leaves the result as it was"); + SceSaveDataDirName absent{}; + std::memcpy(absent.data, "absent", sizeof("absent")); + mount.dir_name = &absent; + mount.mount_mode = MountReadWrite; + Check(sceSaveDataMount3(&mount, &result) == SaveDataErrorNotFound, "open of a missing save is NOT_FOUND"); + Check(std::memcmp(&result, &mounted, sizeof(result)) == 0, "a NOT_FOUND mount leaves the result as it was"); + 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() { @@ -48,11 +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/GuestSaveDataSearch.cpp b/core/libs/tests/GuestSaveDataSearch.cpp index cc24049afd..47613690d3 100644 --- a/core/libs/tests/GuestSaveDataSearch.cpp +++ b/core/libs/tests/GuestSaveDataSearch.cpp @@ -1,24 +1,177 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include #include +#include #include #include #include +#include extern "C" { int APS5_VABI sceSaveDataDirNameSearch(const SaveDataDirNameSearchCond*, SaveDataDirNameSearchResult*); +int APS5_VABI sceSaveDataDirNameSearch2(const SaveDataDirNameSearchCond*, SaveDataDirNameSearchResult*); int APS5_VABI sceSaveDataMount3(const SaveDataMount3*, SaveDataMountResult*); int APS5_VABI sceSaveDataUmount2(std::uint32_t, const SaveDataMountPoint*); +int APS5_VABI sceSaveDataSetParam(const SaveDataMountPoint*, std::uint32_t, const void*, std::size_t); +int APS5_VABI sceSaveDataGetMountInfo(const SaveDataMountPoint*, SaveDataMountInfo*); } +static constexpr int SaveDataErrorParameter = -2137063424; + 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_%%"); + SaveDataDirNameSearchCond cond{}; + cond.user_id = 1; + cond.dir_name = &pattern; + cond.key = key; + cond.order = order; + std::array names{}; + SaveDataDirNameSearchResult result{}; + result.dir_names = names.data(); + result.dir_names_num = capacity; + Require(sceSaveDataDirNameSearch(&cond, &result) == 0, "sorted search failed"); + *hits = result.hit_num; + std::vector found; + for (std::uint32_t i = 0; i < result.set_num; ++i) found.emplace_back(names[i].data); + return found; +} + +static void SortByKey() { + struct Save { + const char* name; + std::uint32_t userParam; + std::int64_t mtime; + }; + const std::array saves{{{"SORT_B", 1, 1000}, {"SORT_A", 3, 2000}, {"SORT_C", 2, 3000}}}; + for (const auto& save : saves) { + std::filesystem::create_directories(std::filesystem::path("_sd") / save.name); + SceSaveDataDirName dirName{}; + std::snprintf(dirName.data, sizeof(dirName.data), "%s", save.name); + SaveDataMount3 mount{}; + mount.user_id = 1; + mount.dir_name = &dirName; + mount.mount_mode = 2; + SaveDataMountResult mounted{}; + Require(sceSaveDataMount3(&mount, &mounted) == 0, "mount for parameters failed"); + Require(sceSaveDataSetParam(&mounted.mount_point, 4, &save.userParam, sizeof(save.userParam)) == 0, "user parameter was not set"); + Require(sceSaveDataSetParam(&mounted.mount_point, 5, &save.mtime, sizeof(save.mtime)) == 0, "mtime was not set"); + Require(sceSaveDataUmount2(0, &mounted.mount_point) == 0, "unmount failed"); + } + using Names = std::vector; + std::uint32_t hits = 0; + Require(Search(0, 0, 3, &hits) == Names{"SORT_A", "SORT_B", "SORT_C"} && hits == 3, "directory names must sort ascending"); + Require(Search(0, 1, 3, &hits) == Names{"SORT_C", "SORT_B", "SORT_A"}, "directory names must sort descending"); + Require(Search(1, 0, 3, &hits) == Names{"SORT_B", "SORT_C", "SORT_A"}, "user parameters must sort ascending"); + Require(Search(1, 1, 3, &hits) == Names{"SORT_A", "SORT_C", "SORT_B"}, "user parameters must sort descending"); + Require(Search(3, 0, 3, &hits) == Names{"SORT_B", "SORT_A", "SORT_C"}, "modification times must sort ascending"); + Require(Search(3, 1, 1, &hits) == Names{"SORT_C"} && hits == 3, "the newest save must come first in descending modification time"); + SceSaveDataDirName pattern{}; + std::snprintf(pattern.data, sizeof(pattern.data), "SORT_%%"); + SaveDataDirNameSearchCond second{}; + second.user_id = 1; + second.dir_name = &pattern; + second.key = 1; + second.order = 1; + std::array secondNames{}; + SaveDataDirNameSearchResult secondResult{}; + secondResult.dir_names = secondNames.data(); + secondResult.dir_names_num = 3; + Require(sceSaveDataDirNameSearch2(&second, &secondResult) == 0, "the second search failed"); + Require(secondResult.hit_num == 3 && secondResult.set_num == 3, "the second search must return all three saves"); + Require(std::string(secondNames[0].data) == "SORT_A" && std::string(secondNames[1].data) == "SORT_C" && std::string(secondNames[2].data) == "SORT_B", "the second search must sort like the first"); + second.key = 6; + Require(sceSaveDataDirNameSearch2(&second, &secondResult) == SaveDataErrorParameter, "the second search must reject an unknown sort key"); + SaveDataDirNameSearchCond cond{}; + cond.user_id = 1; + cond.key = 6; + SaveDataDirNameSearchResult result{}; + Require(sceSaveDataDirNameSearch(&cond, &result) == SaveDataErrorParameter, "an unknown sort key must be rejected"); + cond.key = 0; + cond.order = 2; + Require(sceSaveDataDirNameSearch(&cond, &result) == SaveDataErrorParameter, "an unknown sort order must be rejected"); +} + +static std::vector SearchPattern(const char* text) { + SceSaveDataDirName pattern{}; + std::snprintf(pattern.data, sizeof(pattern.data), "%s", text); + SaveDataDirNameSearchCond cond{}; + cond.user_id = 1; + cond.dir_name = &pattern; + std::array names{}; + SaveDataDirNameSearchResult result{}; + result.dir_names = names.data(); + result.dir_names_num = static_cast(names.size()); + Require(sceSaveDataDirNameSearch(&cond, &result) == 0, "pattern search failed"); + std::vector found; + for (std::uint32_t i = 0; i < result.set_num; ++i) found.emplace_back(names[i].data); + return found; +} + +static void TrailingWildcards() { + for (const char* name : std::array{"TRAIL", "TRAIL1"}) std::filesystem::create_directories(std::filesystem::path("_sd") / name); + using Names = std::vector; + Require(SearchPattern("TRAIL%") == Names{"TRAIL", "TRAIL1"}, "a trailing % must also match no characters"); + Require(SearchPattern("TRAIL%%") == Names{"TRAIL", "TRAIL1"}, "trailing %% must also match no characters"); + Require(SearchPattern("TRAI_%") == Names{"TRAIL", "TRAIL1"}, "_ then % must match one character and then none"); + Require(SearchPattern("TRAIL_%") == Names{"TRAIL1"}, "_ must still match exactly one character"); +} + +static std::uint32_t SearchTitle(const char* titleId, bool second) { + SceSaveDataTitleId title{}; + std::snprintf(title.data, sizeof(title.data), "%s", titleId != nullptr ? titleId : ""); + SceSaveDataDirName pattern{}; + std::snprintf(pattern.data, sizeof(pattern.data), "OTHER_%%"); + SaveDataDirNameSearchCond cond{}; + cond.user_id = 1; + cond.title_id = titleId != nullptr ? &title : nullptr; + cond.dir_name = &pattern; + std::array names{}; + names[0].data[0] = '!'; + SaveDataDirNameSearchResult result{}; + result.dir_names = names.data(); + result.dir_names_num = 1; + result.hit_num = 7; + result.set_num = 7; + Require((second ? sceSaveDataDirNameSearch2(&cond, &result) : sceSaveDataDirNameSearch(&cond, &result)) == 0, "title search failed"); + Require(result.set_num == std::min(result.hit_num, 1), "title search must set the names it found"); + Require(result.set_num == 1 ? std::string(names[0].data) == "OTHER_A" : names[0].data[0] == '!', "title search returned the wrong name"); + return result.hit_num; +} + +static void OtherTitle() { + std::filesystem::create_directories("app0/sce_sys"); + std::FILE* param = std::fopen("app0/sce_sys/param.json", "wb"); + Require(param != nullptr, "param.json was not created"); + std::fputs("{\"titleId\":\"PPSA00000\",\"localizedParameters\":{\"en-US\":{\"titleName\":\"Example\"}}}", param); + std::fclose(param); + std::filesystem::create_directories(std::filesystem::path("_sd") / "OTHER_A"); + for (const bool second : {false, true}) { + Require(SearchTitle(nullptr, second) == 1, "a search without a title id must find the running title's save"); + Require(SearchTitle("PPSA00000", second) == 1, "a search for the running title must find its save"); + Require(SearchTitle("PPSA17221", second) == 0, "a search for another title must find none of the running title's saves"); + } +} + static void SearchAndMount() { const std::array names{std::string(31, 'A'), std::string(31, 'B')}; for (const auto& name : names) std::filesystem::create_directories(std::filesystem::path("_sd") / name); @@ -30,11 +183,23 @@ static void SearchAndMount() { SaveDataDirNameSearchResult result{}; result.dir_names = reinterpret_cast(buffer.data()); result.dir_names_num = 2; + alignas(SaveDataSearchInfo) std::array infos; + infos.fill(0x21); + result.infos = reinterpret_cast(infos.data()); Require(sceSaveDataDirNameSearch(&cond, &result) == 0, "search failed"); Require(result.hit_num == 2 && result.set_num == 2, "search must return both saves"); for (std::size_t i = 64; i < buffer.size(); ++i) { Require(buffer[i] == '!', "directory search overran two 32-byte guest records"); } + for (std::size_t i = 96; i < infos.size(); ++i) { + Require(infos[i] == 0x21, "directory search overran two 48-byte search infos"); + } + for (std::size_t i = 0; i < 2; ++i) { + SaveDataSearchInfo info{}; + std::memcpy(&info, infos.data() + i * 48, sizeof(info)); + 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{}; for (int i = 0; i < 2; ++i) { const char* record = buffer.data() + i * 32; @@ -55,6 +220,94 @@ static void SearchAndMount() { } } +static void CreateSized(const char* name, std::uint64_t blocks, std::size_t bytes) { + SceSaveDataDirName dirName{}; + std::snprintf(dirName.data, sizeof(dirName.data), "%s", name); + SaveDataMount3 mount{}; + mount.user_id = 1; + mount.dir_name = &dirName; + mount.blocks = blocks; + mount.mount_mode = 2 | 4; + SaveDataMountResult mounted{}; + Require(sceSaveDataMount3(&mount, &mounted) == 0, "creating a sized save failed"); + if (bytes != 0) { + std::ofstream file(std::filesystem::path("_sd") / name / "data", std::ios::binary); + file << std::string(bytes, 'x'); + } + Require(sceSaveDataUmount2(0, &mounted.mount_point) == 0, "unmount failed"); +} + +static std::vector SizeSearch(std::uint32_t key, std::vector& found) { + SceSaveDataDirName pattern{}; + std::snprintf(pattern.data, sizeof(pattern.data), "SIZE_%%"); + SaveDataDirNameSearchCond cond{}; + cond.user_id = 1; + cond.dir_name = &pattern; + cond.key = key; + std::array names{}; + std::array infos{}; + SaveDataDirNameSearchResult result{}; + result.dir_names = names.data(); + result.dir_names_num = 2; + result.infos = infos.data(); + Require(sceSaveDataDirNameSearch(&cond, &result) == 0 && result.set_num == 2, "the sized search must return both saves"); + found.clear(); + for (const auto& name : names) found.emplace_back(name.data); + return {infos.begin(), infos.end()}; +} + +static SaveDataMountInfo MountInfo(const char* name) { + SceSaveDataDirName dirName{}; + std::snprintf(dirName.data, sizeof(dirName.data), "%s", name); + SaveDataMount3 mount{}; + mount.user_id = 1; + mount.dir_name = &dirName; + mount.mount_mode = 1; + SaveDataMountResult mounted{}; + Require(sceSaveDataMount3(&mount, &mounted) == 0, "mounting the sized save failed"); + SaveDataMountInfo info{}; + Require(sceSaveDataGetMountInfo(&mounted.mount_point, &info) == 0, "mount info failed"); + Require(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() { const auto previous = std::filesystem::current_path(); const auto root = std::filesystem::temp_directory_path() / @@ -64,6 +317,12 @@ int main() { int status = 0; try { SearchAndMount(); + SortByKey(); + 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 dadcd86b50..b3499c1492 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -1,4 +1,18 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#include +#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 "SceTypes.hpp" #include #include @@ -8,17 +22,26 @@ #include #include #include +#include #include +#include #include +#include extern "C" { extern const std::uint8_t in6addr_any_nid_postfix[16]; extern const std::uint8_t in6addr_loopback_nid_postfix[16]; int APS5_VABI sceNetInit_nid_postfix(void); +int APS5_VABI sceNetShowNetstatWithMemory(int); +int APS5_VABI sceNetShowNetstat(void); +int APS5_VABI sceNetShowIfconfig(void); +int APS5_VABI sceNetShowRoute(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); @@ -26,23 +49,58 @@ 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 sceNetEpollCreate(const char*, int); int APS5_VABI sceNetEpollControl(int, int, int, const NetEpollEvent*); int APS5_VABI sceNetEpollWait(int, NetEpollEvent*, int, int); int APS5_VABI sceNetEpollDestroy(int); int APS5_VABI sceNetEpollAbort(int, int); +int APS5_VABI sceNetGetsockopt(int, int, int, void*, std::uint32_t*); extern const std::uint32_t sce_net_in6addr_any[4]; +extern const std::uint8_t sce_net_in6addr_linklocal_allnodes[16]; int APS5_VABI sceNetResolverCreate(const char*, int, int); int APS5_VABI sceNetResolverStartNtoa(int, const char*, void*, int, int, int); +int APS5_VABI sceNetResolverStartNtoa6(int, const char*, void*, int, int, int); int APS5_VABI sceNetResolverStartNtoaMultipleRecordsEx(int, const char*, void*, int, int, int); +int APS5_VABI sceNetResolverStartNtoaMultipleRecords(int, const char*, void*, int, int, int); +int APS5_VABI sceNetResolverStartAton(int, const void*, char*, int, int, int, int); +int APS5_VABI 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 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*); int APS5_VABI select_nid_postfix(int, void*, void*, void*, const void*); +int APS5_VABI open_nid_postfix(const char*, int, int); +int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI close_nid_postfix(int); +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); } @@ -74,16 +132,140 @@ 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::int64_t, int); +void APS5_VABI sceNetMemoryFree(void*); } static void Require(bool condition) { if (!condition) std::abort(); } +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); + Require(listener >= 0); + std::array destination{}; + destination[0] = static_cast(addressSize); + destination[1] = static_cast(family); + if (family == 2) { destination[4] = 127; destination[7] = 1; } + else destination[23] = 1; + Require(sceNetBind_nid_postfix(listener, destination.data(), addressSize) == 0 && sceNetListen(listener, 1) == 0); + std::uint32_t size = destination.size(); + Require(sceNetGetsockname(listener, destination.data(), &size) == 0 && size == addressSize); + const int client = sceNetSocket(nullptr, family, 1, 6); + Require(client >= 0 && sceNetConnect(client, destination.data(), addressSize) == 0); + const int accepted = sceNetAccept(listener, nullptr, nullptr); + Require(accepted >= 0); + const char payload[] = "stream"; + for (const auto endpoints : {std::array{accepted, client}, std::array{client, accepted}}) { + const auto receiver = endpoints[0], sender = endpoints[1]; + for (const std::uint32_t capacity : {0u, 1u, 16u, 28u, UINT32_MAX}) { + Require(sceNetSend(sender, payload, sizeof(payload), 0) == sizeof(payload)); + std::array address; + address.fill(0xa5); + char received[sizeof(payload)]{}; + size = capacity; + *sceNetErrnoLoc() = 123; + Require(sceNetRecvfrom(receiver, received, 0, 2, address.data(), &size) == 0); + Require(size == 0 && received[0] == 0 && *sceNetErrnoLoc() == 123); + Require(std::all_of(address.begin(), address.end(), [](auto byte) { return byte == 0xa5; })); + for (const int flags : {2, 0}) { + size = capacity; + *sceNetErrnoLoc() = 123; + Require(sceNetRecvfrom(receiver, received, sizeof(received), flags, address.data(), &size) == sizeof(payload)); + Require(size == 0 && std::memcmp(received, payload, sizeof(payload)) == 0 && *sceNetErrnoLoc() == 123); + Require(std::all_of(address.begin(), address.end(), [](auto byte) { return byte == 0xa5; })); + } + } + Require(sceNetSend(sender, payload, sizeof(payload), 0) == sizeof(payload)); + std::array address; + address.fill(0xa5); + char received[sizeof(payload)]{}; + Require(Failed(sceNetRecvfrom(receiver, received, sizeof(received), 0, address.data(), nullptr), 22)); + size = 123; + Require(sceNetRecvfrom(receiver, received, sizeof(received), 0, nullptr, &size) == sizeof(payload)); + Require(size == 123 && std::memcmp(received, payload, sizeof(payload)) == 0); + Require(sceNetShutdown(sender, 1) == 0); + for (const int flags : {2, 0}) { + size = address.size(); + *sceNetErrnoLoc() = 123; + Require(sceNetRecvfrom(receiver, received, sizeof(received), flags, address.data(), &size) == 0); + Require(size == 0 && *sceNetErrnoLoc() == 123); + Require(std::all_of(address.begin(), address.end(), [](auto byte) { return byte == 0xa5; })); + } + } + for (const int descriptor : {client, accepted, listener}) Require(sceNetSocketClose(descriptor) == 0); +} + +static std::string CaptureNetstat(int pool) { + auto* file = std::tmpfile(); + Require(file != nullptr); + Require(std::fflush(stdout) == 0); +#ifdef _WIN32 + const int output = _fileno(stdout); + const int saved = _dup(output); + Require(saved >= 0 && _dup2(_fileno(file), output) == 0); +#else + const int output = fileno(stdout); + const int saved = dup(output); + Require(saved >= 0 && dup2(fileno(file), output) == output); +#endif + const int result = sceNetShowNetstatWithMemory(pool); + Require(std::fflush(stdout) == 0); +#ifdef _WIN32 + Require(_dup2(saved, output) == 0 && _close(saved) == 0); +#else + Require(dup2(saved, output) == output && close(saved) == 0); +#endif + Require(result == 0 && std::fseek(file, 0, SEEK_SET) == 0); + std::string text; + std::array bytes{}; + for (std::size_t count; (count = std::fread(bytes.data(), 1, bytes.size(), file)) != 0;) text.append(bytes.data(), count); + Require(std::ferror(file) == 0 && std::fclose(file) == 0); + return text; +} + +static void CheckNetstat() { + Require(sceNetShowNetstat() == 0); + Require(sceNetShowIfconfig() == 0); + Require(sceNetShowRoute() == 0); + Require(Failed(sceNetShowNetstatWithMemory(-1), 9)); + const int pool = sceNetPoolCreate("netstat", 4096, 0); + Require(pool > 0); + const auto empty = CaptureNetstat(pool); + Require(empty.find("bytes=4096 sockets=0") != std::string::npos); + const int socket = sceNetSocket("netstat", 2, 1, 0); + Require(socket >= 0); + const int nonblocking = 1; + Require(sceNetSetsockopt(socket, 0xffff, 0x1200, &nonblocking, sizeof(nonblocking)) == 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(socket, address.data(), address.size()) == 0); + Require(sceNetListen(socket, 1) == 0); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(socket, address.data(), &size) == 0 && size == address.size()); + const auto before = address; + const auto snapshot = CaptureNetstat(pool); + Require(snapshot.find("bytes=4096 sockets=1") != std::string::npos); + Require(snapshot.find("socket=" + std::to_string(socket) + " family=2 type=1 nonblock=1 bound=1 listening=1") != std::string::npos); + Require(sceNetGetsockname(socket, address.data(), &size) == 0 && address == before); + Require(sceNetSocketClose(socket) == 0); + Require(CaptureNetstat(pool).find("sockets=0") != std::string::npos); + Require(sceNetPoolDestroy(pool) == 0); + Require(Failed(sceNetShowNetstatWithMemory(pool), 9)); +} + static void CheckPoolStats() { const int pool = sceNetPoolCreate("stats", 0x4000, 0); Require(pool > 0); @@ -95,6 +277,203 @@ 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); + Require(std::any_of(values.begin() + 1, values.end(), [&](std::uint32_t value) { return value != values[0]; })); +} + +static void CheckNetMemory() { + Require(sceNetMemoryAllocate(0, 0) == nullptr); + Require(sceNetMemoryAllocate(-1, 2) == nullptr); + 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); + sceNetMemoryFree(nullptr); + bool threw = false; + try { + sceNetMemoryAllocate(16, 1); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); +} + +static void CheckUnboundSocketName() { + for (const int family : {2, 28}) { + for (const int type : {1, 2}) { + const int socket = sceNetSocket("unbound", family, type, type == 1 ? 6 : 17); + if (socket < 0 && family == 28 && *sceNetErrnoLoc() == 47) continue; + Require(socket >= 0); + const std::uint32_t addressSize = family == 2 ? 16 : 28; + std::array address; + address.fill(0xa5); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(socket, address.data(), &size) == 0 && size == addressSize); + Require(address[0] == addressSize && address[1] == family); + Require(std::all_of(address.begin() + 2, address.begin() + addressSize, [](auto byte) { return byte == 0; })); + Require(sceNetSocketClose(socket) == 0); + } + } +} + +static void 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); @@ -133,6 +512,89 @@ static void CheckEmptyEpoll() { } } +static void CheckDontWait() { + constexpr int DontWait = 0x80; + constexpr int Peek = 0x2; + const int receiver = sceNetSocket("dontwait-receiver", 2, 2, 17); + const int sender = sceNetSocket("dontwait-sender", 2, 2, 17); + Require(receiver >= 0 && sender >= 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(receiver, address.data(), address.size()) == 0); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(receiver, address.data(), &size) == 0); + const int timeout = 2000000; + Require(sceNetSetsockopt(receiver, 0xffff, 0x1106, &timeout, sizeof(timeout)) == 0); + char buffer[16]{}; + NetIovec in[] = {{buffer, sizeof(buffer)}}; + NetMsghdr receive{nullptr, 0, in, 1, nullptr, 0, 0}; + 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)); + Require(Failed(sceNetRecv(receiver, buffer, sizeof(buffer), DontWait | Peek), 35)); + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(1)); + const char datagram[] = "nowait"; + Require(sceNetSendto(sender, datagram, sizeof(datagram), DontWait, address.data(), address.size()) == sizeof(datagram)); + Require(sceNetRecvfrom(receiver, buffer, sizeof(buffer), Peek, nullptr, nullptr) == sizeof(datagram)); + std::memset(buffer, 0, sizeof(buffer)); + Require(sceNetRecv(receiver, buffer, sizeof(buffer), DontWait | Peek) == sizeof(datagram) && std::strcmp(buffer, datagram) == 0); + std::memset(buffer, 0, sizeof(buffer)); + Require(sceNetRecv(receiver, buffer, sizeof(buffer), DontWait) == sizeof(datagram) && std::strcmp(buffer, datagram) == 0); + Require(sceNetConnect(sender, address.data(), address.size()) == 0); + Require(sceNetSend(sender, datagram, sizeof(datagram), DontWait) == sizeof(datagram)); + Require(sceNetRecvmsg(receiver, &receive, 0) == sizeof(datagram) && std::strcmp(buffer, datagram) == 0); + Require(Failed(sceNetRecv(receiver, buffer, sizeof(buffer), DontWait), 35)); + 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 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(); + NetIovec in[] = {{buffer, sizeof(buffer)}}; + NetMsghdr receive{source.data(), static_cast(source.size()), in, 1, nullptr, 0, 0}; + Require(Failed(sceNetRecv(receiver, buffer, sizeof(buffer), 0), 35)); + Require(Failed(sceNetRecvfrom(receiver, buffer, sizeof(buffer), 0, source.data(), &size), 35)); + Require(Failed(sceNetRecvmsg(receiver, &receive, 0), 35)); + const char datagram[] = "late"; + Require(sceNetSendto(sender, datagram, sizeof(datagram), 0, address.data(), address.size()) == sizeof(datagram)); + Require(sceNetRecv(receiver, buffer, sizeof(buffer), 0) == sizeof(datagram) && std::strcmp(buffer, datagram) == 0); + const KernelTimeval disabled{0, 0}; + Require(sceNetSetsockopt(receiver, 0xffff, 0x1006, &disabled, sizeof(disabled)) == 0); +} + +static void CheckMicrosecondTimeouts(int receiver) { + const int receiveTimeout = 50000; + const int sendTimeout = 70000; + Require(sceNetSetsockopt(receiver, 0xffff, 0x1106, &receiveTimeout, sizeof(receiveTimeout)) == 0); + Require(sceNetSetsockopt(receiver, 0xffff, 0x1105, &sendTimeout, sizeof(sendTimeout)) == 0); + for (const auto [option, expected] : {std::array{0x1106, receiveTimeout}, std::array{0x1105, sendTimeout}}) { + int value = -1; + std::uint32_t size = sizeof(value); + Require(sceNetGetsockopt(receiver, 0xffff, option, &value, &size) == 0 && size == sizeof(value) && value == expected); + } + char buffer[8]{}; + Require(Failed(sceNetRecv(receiver, buffer, sizeof(buffer), 0), 35)); + const int disabled = 0; + Require(sceNetSetsockopt(receiver, 0xffff, 0x1106, &disabled, sizeof(disabled)) == 0); + Require(sceNetSetsockopt(receiver, 0xffff, 0x1105, &disabled, sizeof(disabled)) == 0); +} + static void CheckMessages(int receiver, int sender, const std::array& address) { char head[] = "scatter "; char tail[] = "gather"; @@ -190,6 +652,115 @@ static void CheckMessages(int receiver, int sender, const std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + const int receiver = sceNetSocket("epoll-receiver", 2, 2, 17); + const int sender = sceNetSocket("epoll-sender", 2, 2, 17); + Require(receiver >= 0 && sender >= 0); + Require(sceNetBind_nid_postfix(receiver, address.data(), address.size()) == 0); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(receiver, address.data(), &size) == 0 && size == address.size()); + const int epoll = sceNetEpollCreate("epoll-writable", 0); + Require(epoll >= 0); + NetEpollEvent registration{}; + registration.events = 2; + registration.ident = static_cast(sender); + registration.data.u32 = 41; + Require(sceNetEpollControl(epoll, 1, sender, ®istration) == 0); + NetEpollEvent ready{}; + Require(sceNetEpollWait(epoll, &ready, 1, 1000000) == 1); + Require(ready.events == 2 && ready.ident == static_cast(sender) && ready.data.u32 == 41); + Require(sceNetEpollControl(epoll, 3, sender, nullptr) == 0); + const char payload[] = "epoll"; + char peeked[sizeof(payload)]{}; + Require(sceNetSendto(sender, payload, sizeof(payload), 0, address.data(), address.size()) == sizeof(payload)); + Require(sceNetRecv(receiver, peeked, sizeof(peeked), 2) == sizeof(payload)); + registration.events = 3; + registration.ident = static_cast(receiver); + Require(sceNetEpollControl(epoll, 1, receiver, ®istration) == 0); + Require(sceNetEpollWait(epoll, &ready, 1, 1000000) == 1); + Require(ready.events == 3 && ready.ident == static_cast(receiver) && ready.data.u32 == 41); + Require(sceNetEpollDestroy(epoll) == 0); + Require(sceNetSocketClose(sender) == 0 && sceNetSocketClose(receiver) == 0); +} + +static void RequireTimeout(int socket, int option, int expected) { + 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 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(); + } +} + +static void CheckKernelTimeouts() { + const int descriptor = sceNetSocket("kernel-timeouts", 2, 2, 17); + Require(descriptor >= 0); + const KernelTimeval receive{1, 250000}; + Require(setsockopt_nid_postfix(descriptor, 0xffff, 0x1006, &receive, sizeof(receive)) == 0); + RequireTimeout(descriptor, 0x1006, 1250000); + const KernelTimeval send{0, 500000}; + Require(setsockopt_nid_postfix(descriptor, 0xffff, 0x1005, &send, sizeof(send)) == 0); + RequireTimeout(descriptor, 0x1005, 500000); + const KernelTimeval invalid{0, 1000000}; + Require(setsockopt_nid_postfix(descriptor, 0xffff, 0x1006, &invalid, sizeof(invalid)) == -1 && *__error_nid_postfix() == 33); + const KernelTimeval negative{-1, 0}; + Require(setsockopt_nid_postfix(descriptor, 0xffff, 0x1005, &negative, sizeof(negative)) == -1 && *__error_nid_postfix() == 33); + const int microseconds = 1000; + Require(setsockopt_nid_postfix(descriptor, 0xffff, 0x1006, µseconds, sizeof(microseconds)) == -1 && *__error_nid_postfix() == 22); + RequireTimeout(descriptor, 0x1006, 1250000); + RequireTimeout(descriptor, 0x1005, 500000); + 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}; + Require(sceNetBind_nid_postfix(descriptor, loopback.data(), loopback.size()) == 0); + const KernelTimeval brief{0, 50000}; + Require(setsockopt_nid_postfix(descriptor, 0xffff, 0x1006, &brief, sizeof(brief)) == 0); + char buffer[8]{}; + const auto start = std::chrono::steady_clock::now(); + Require(Failed(sceNetRecv(descriptor, buffer, sizeof(buffer), 0), 35)); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(40)); +#endif + 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); @@ -210,8 +781,337 @@ 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 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)); +} + +static void CheckEpollIdent() { + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + const int receiver = sceNetSocket("epoll-ident", 2, 2, 17); + const int sender = sceNetSocket("epoll-ident-sender", 2, 2, 17); + Require(receiver >= 0 && sender >= 0); + Require(sceNetBind_nid_postfix(receiver, address.data(), address.size()) == 0); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(receiver, address.data(), &size) == 0 && size == address.size()); + const int epoll = sceNetEpollCreate("epoll-ident", 0); + Require(epoll >= 0); + NetEpollEvent registration{}; + registration.events = 1; + registration.reserved = 0x5a5a5a5a; + registration.ident = 0x123456789abcdef0; + registration.data.u64 = 0x1122334455667788; + Require(sceNetEpollControl(epoll, 1, receiver, ®istration) == 0); + const char payload[] = "ident"; + Require(sceNetSendto(sender, payload, sizeof(payload), 0, address.data(), address.size()) == sizeof(payload)); + NetEpollEvent ready{}; + Require(sceNetEpollWait(epoll, &ready, 1, 1000000) == 1); + Require(ready.events == 1 && ready.reserved == 0 && ready.ident == static_cast(receiver)); + Require(ready.data.u64 == 0x1122334455667788); + Require(sceNetEpollDestroy(epoll) == 0); + Require(sceNetSocketClose(sender) == 0 && sceNetSocketClose(receiver) == 0); +} + +static 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() { + const auto hostSocket = ::socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP); +#ifdef _WIN32 + if (hostSocket == INVALID_SOCKET) { + const int error = WSAGetLastError(); + return error == WSAEAFNOSUPPORT || error == WSAEPROTONOSUPPORT; + } +#else + if (hostSocket < 0) return errno == EAFNOSUPPORT || errno == EPROTONOSUPPORT; +#endif + + sockaddr_in6 loopback{}; + loopback.sin6_family = AF_INET6; + loopback.sin6_port = htons(9); + loopback.sin6_addr = in6addr_loopback; + const char payload = 0; + const auto sent = ::sendto(hostSocket, &payload, sizeof(payload), 0, + reinterpret_cast(&loopback), sizeof(loopback)); +#ifdef _WIN32 + const int error = sent < 0 ? WSAGetLastError() : 0; + closesocket(hostSocket); + return sent < 0 && (error == WSAEADDRNOTAVAIL || error == WSAENETUNREACH || error == WSAEHOSTUNREACH); +#else + const int error = sent < 0 ? errno : 0; + close(hostSocket); + return sent < 0 && (error == EADDRNOTAVAIL || error == ENETUNREACH || error == EHOSTUNREACH); +#endif +} + +static void CheckLinkLocalAllNodesIpv6() { + Require(reinterpret_cast(sce_net_in6addr_linklocal_allnodes) % alignof(std::uint32_t) == 0); + std::array address{}; + Require(sceNetInetPton(28, "ff02::1", address.data()) == 1); + Require(std::memcmp(sce_net_in6addr_linklocal_allnodes, address.data(), address.size()) == 0); + std::array text; + text.fill('x'); + Require(sceNetInetNtop(28, sce_net_in6addr_linklocal_allnodes, text.data(), 7) == nullptr && *sceNetErrnoLoc() == 28); + Require(sceNetInetNtop(28, sce_net_in6addr_linklocal_allnodes, text.data(), 8) == text.data()); + Require(std::strcmp(text.data(), "ff02::1") == 0 && text[8] == 'x'); + address.fill(0xa5); + Require(sceNetInetPton(28, text.data(), address.data()) == 1); + Require(std::memcmp(sce_net_in6addr_linklocal_allnodes, address.data(), address.size()) == 0); +} + +template +static bool Untouched(const std::array& bytes, std::uint32_t from) { + return std::all_of(bytes.begin() + from, bytes.end(), [](auto byte) { return byte == 0xa5; }); +} + +static void CheckDatagramAddressLengths(int family) { + if (family == 28 && ipv6Unavailable) return; + const std::uint32_t addressSize = family == 2 ? 16 : 28; + std::array destination{}; + destination[0] = static_cast(addressSize); + destination[1] = static_cast(family); + if (family == 2) { destination[4] = 127; destination[7] = 1; } + else destination[23] = 1; + const int receiver = sceNetSocket("length-receiver", family, 2, 17); + const int sender = sceNetSocket("length-sender", family, 2, 17); + Require(receiver >= 0 && sender >= 0); + Require(sceNetBind_nid_postfix(receiver, destination.data(), addressSize) == 0); + std::uint32_t size = destination.size(); + Require(sceNetGetsockname(receiver, destination.data(), &size) == 0 && size == addressSize); + Require(sceNetConnect(sender, destination.data(), addressSize) == 0); + std::array source{}; + size = source.size(); + Require(sceNetGetsockname(sender, source.data(), &size) == 0 && size == addressSize); + const char payload[] = "address length"; + for (const std::uint32_t capacity : {0u, 1u, 2u, addressSize - 1, addressSize, 32u, UINT32_MAX}) { + const auto copied = std::min(capacity, addressSize); + std::array address; + address.fill(0xa5); + size = capacity; + Require(sceNetGetsockname(receiver, address.data(), &size) == 0 && size == copied); + Require(std::memcmp(address.data(), destination.data(), copied) == 0 && Untouched(address, copied)); + char received[sizeof(payload)]{}; + Require(sceNetSend(sender, payload, sizeof(payload), 0) == sizeof(payload)); + address.fill(0xa5); + size = capacity; + Require(sceNetRecvfrom(receiver, received, sizeof(received), 2, address.data(), &size) == sizeof(payload)); + Require(size == copied && std::memcmp(address.data(), source.data(), copied) == 0 && Untouched(address, copied)); + address.fill(0xa5); + NetIovec in[] = {{received, sizeof(received)}}; + NetMsghdr message{address.data(), capacity, in, 1, nullptr, 0, -1}; + Require(sceNetRecvmsg(receiver, &message, 0) == sizeof(payload) && std::memcmp(received, payload, sizeof(payload)) == 0); + Require(message.name_length == copied && std::memcmp(address.data(), source.data(), copied) == 0 && Untouched(address, copied)); + } + Require(sceNetSocketClose(sender) == 0 && sceNetSocketClose(receiver) == 0); +} + +static void CheckStreamAddressLengths() { + const int listener = sceNetSocket("length-listener", 2, 1, 6); + Require(listener >= 0); + std::array listening{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(listener, listening.data(), listening.size()) == 0 && sceNetListen(listener, 1) == 0); + std::uint32_t size = listening.size(); + Require(sceNetGetsockname(listener, listening.data(), &size) == 0 && size == 16); + const int client = sceNetSocket("length-client", 2, 1, 6); + Require(client >= 0 && sceNetConnect(client, listening.data(), listening.size()) == 0); + std::array clientAddress{}; + size = clientAddress.size(); + Require(sceNetGetsockname(client, clientAddress.data(), &size) == 0 && size == 16); + std::array peer; + peer.fill(0xa5); + size = 1; + const int accepted = sceNetAccept(listener, peer.data(), &size); + Require(accepted >= 0 && size == 1 && peer[0] == 16 && Untouched(peer, 1)); + for (const std::uint32_t capacity : {0u, 1u, 15u, 16u, 28u, UINT32_MAX}) { + const auto copied = std::min(capacity, std::uint32_t{16}); + peer.fill(0xa5); + size = capacity; + Require(sceNetGetpeername(accepted, peer.data(), &size) == 0 && size == copied); + Require(std::memcmp(peer.data(), clientAddress.data(), copied) == 0 && Untouched(peer, copied)); + } + for (const int descriptor : {accepted, client, listener}) Require(sceNetSocketClose(descriptor) == 0); +} + +static void 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); @@ -238,31 +1138,354 @@ static void CheckUnspecifiedIpv6() { Require(sceNetInetPton(28, "::1", address.data() + 8) == 1); const char payload[] = "IPv6 wildcard receive"; - Require(sceNetSendto(sender, payload, sizeof(payload), 0, address.data(), address.size()) == sizeof(payload)); + const auto sent = sceNetSendto(sender, payload, sizeof(payload), 0, address.data(), address.size()); + if (sent < 0 && (*sceNetErrnoLoc() == 49 || *sceNetErrnoLoc() == 51 || *sceNetErrnoLoc() == 65) && HostIpv6LoopbackUnavailable()) { + Require(sceNetSocketClose(sender) == 0); + Require(sceNetSocketClose(receiver) == 0); + ipv6Unavailable = true; + return; + } + Require(sent == sizeof(payload)); char received[sizeof(payload)]{}; std::array peer{}; size = peer.size(); Require(sceNetRecvfrom(receiver, received, sizeof(received), 0, peer.data(), &size) == sizeof(received)); Require(std::memcmp(received, payload, sizeof(payload)) == 0 && size == peer.size() && peer[1] == 28); Require(std::memcmp(peer.data() + 8, address.data() + 8, 16) == 0); + address[0] = 0; + Require(sceNetSendto(sender, payload, sizeof(payload), 0, address.data(), address.size()) == sizeof(payload)); + Require(sceNetRecvfrom(receiver, received, sizeof(received), 0, nullptr, nullptr) == sizeof(received)); Require(std::memcmp(sce_net_in6addr_any, unspecified.data(), unspecified.size()) == 0); Require(sceNetSocketClose(sender) == 0); 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); + std::array any{16, 2}; + Require(sceNetBind_nid_postfix(inet, any.data(), any.size()) == 0); + std::uint32_t size = any.size(); + Require(sceNetGetsockname(inet, any.data(), &size) == 0 && size == any.size()); + const int inet6 = sceNetSocket("ipv6-any-same-port", 28, 2, 17); + Require(inet6 >= 0); + std::array any6{28, 28, any[2], any[3]}; + if (sceNetBind_nid_postfix(inet6, any6.data(), any6.size()) != 0) { + std::fprintf(stderr, "sceNetBind of [::]:%d failed with errno %d while an IPv4 socket holds 0.0.0.0:%d\n", + (any[2] << 8) | any[3], *sceNetErrnoLoc(), (any[2] << 8) | any[3]); + std::abort(); + } + Require(sceNetSocketClose(inet6) == 0); + Require(sceNetSocketClose(inet) == 0); +} + +static void CheckSocketError() { + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + const int listener = sceNetSocket(nullptr, 2, 1, 6); + Require(listener >= 0 && sceNetBind_nid_postfix(listener, address.data(), address.size()) == 0 && sceNetListen(listener, 1) == 0); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(listener, address.data(), &size) == 0 && size == address.size()); + const int connected = sceNetSocket(nullptr, 2, 1, 6); + Require(connected >= 0 && sceNetConnect(connected, address.data(), address.size()) == 0); + int error = -1; + size = sizeof(error); + Require(sceNetGetsockopt(connected, 0xffff, 0x1007, &error, &size) == 0 && size == sizeof(error) && error == 0); + for (const int descriptor : {connected, listener}) Require(sceNetSocketClose(descriptor) == 0); + + const int refused = sceNetSocket(nullptr, 2, 1, 6); + const int nonblocking = 1; + Require(refused >= 0 && sceNetSetsockopt(refused, 0xffff, 0x1200, &nonblocking, sizeof(nonblocking)) == 0); + const int started = sceNetConnect(refused, address.data(), address.size()); + Require(started == static_cast(0x80410124) && *sceNetErrnoLoc() == 36); + error = 0; + for (int attempt = 0; attempt < 500 && error == 0; ++attempt) { + size = sizeof(error); + Require(sceNetGetsockopt(refused, 0xffff, 0x1007, &error, &size) == 0 && size == sizeof(error)); + if (error == 0) std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + Require(error == 61); + size = sizeof(error); + Require(sceNetGetsockopt(refused, 0xffff, 0x1007, &error, &size) == 0 && error == 0); + Require(sceNetSocketClose(refused) == 0); +} + +static void 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); + const int dissolved = sceNetSocket("disconnect", 2, 2, 17); + Require(peer >= 0 && other >= 0 && dissolved >= 0); + std::array peerAddress{16, 2, 0, 0, 127, 0, 0, 1}; + std::array dissolvedAddress{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(peer, peerAddress.data(), peerAddress.size()) == 0); + Require(sceNetBind_nid_postfix(dissolved, dissolvedAddress.data(), dissolvedAddress.size()) == 0); + std::uint32_t size = peerAddress.size(); + Require(sceNetGetsockname(peer, peerAddress.data(), &size) == 0); + size = dissolvedAddress.size(); + Require(sceNetGetsockname(dissolved, dissolvedAddress.data(), &size) == 0); + Require(sceNetConnect(dissolved, peerAddress.data(), peerAddress.size()) == 0); + std::array name{}; + size = name.size(); + Require(sceNetGetpeername(dissolved, name.data(), &size) == 0 && name[2] == peerAddress[2] && name[3] == peerAddress[3]); + const std::array unspecified{16, 0}; + Require(Failed(sceNetConnect(dissolved, unspecified.data(), unspecified.size()), 47)); + size = name.size(); + Require(Failed(sceNetGetpeername(dissolved, name.data(), &size), 57)); + std::array local{}; + size = local.size(); + Require(sceNetGetsockname(dissolved, local.data(), &size) == 0 && local[2] == dissolvedAddress[2] && local[3] == dissolvedAddress[3]); +#ifndef _WIN32 + Require(local[4] == 0 && local[5] == 0 && local[6] == 0 && local[7] == 0); +#endif + const int timeout = 2000000; + Require(sceNetSetsockopt(dissolved, 0xffff, 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]{}; + Require(sceNetRecvfrom(dissolved, buffer, sizeof(buffer), 0, nullptr, nullptr) == sizeof(unrelated)); + Require(std::strcmp(buffer, unrelated) == 0); + for (const int descriptor : {peer, other, dissolved}) Require(sceNetSocketClose(descriptor) == 0); +} + int main() { + const auto epoch = std::chrono::steady_clock::time_point{}; + Require(KernelSocketSelect::DeadlineAfter(epoch, 2, 250000) == epoch + std::chrono::milliseconds(2250)); + Require(KernelSocketSelect::DeadlineAfter(epoch, INT64_MAX, 0) == std::chrono::steady_clock::time_point::max()); + Require(KernelSocketSelect::DeadlineAfter(epoch, 10000000000LL, 0) == std::chrono::steady_clock::time_point::max()); + const auto almostMax = std::chrono::steady_clock::time_point::max() - std::chrono::microseconds(1); + Require(KernelSocketSelect::DeadlineAfter(almostMax, 0, 1) == std::chrono::steady_clock::time_point::max()); + Require(KernelSocketSelect::DeadlineAfter(almostMax, 0, 2) == std::chrono::steady_clock::time_point::max()); for (int i = 0; i < 16; ++i) { Require(in6addr_any_nid_postfix[i] == 0); Require(in6addr_loopback_nid_postfix[i] == (i == 15 ? 1 : 0)); } Require(sceNetInit_nid_postfix() == 0); + CheckEpollWritable(); + CheckSocketHostErrors(); CheckPoolStats(); + CheckIfName(); + CheckRandom(); + CheckNetMemory(); + CheckNetstat(); CheckEmptyEpoll(); + CheckDontWait(); CheckAddressText(2, "127.0.0.1"); CheckAddressText(2, "255.255.255.255"); CheckAddressText(28, "::1"); CheckAddressText(28, "1234:5678:9abc:def0:1234:5678:9abc:def0"); + 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); + const int enabled = 1; + Require(setsockopt_nid_postfix(closed, 0xffff, 0x20, &enabled, sizeof(enabled)) == 0); + Require(sceNetSetsockopt(closed, 0xffff, 0x20, &enabled, sizeof(enabled)) == 0); + Require(sceNetSetsockopt(closed, 0xffff, 0x4, &enabled, sizeof(enabled)) == 0); + Require(close_nid_postfix(closed) == 0); + Require(Failed(sceNetSocketClose(closed), 9)); + CheckKernelTimeouts(); + CheckSocketError(); + CheckKernelStatusFlags(); const int listener = sceNetSocket(nullptr, 2, 1, 6); Require(listener >= 0); @@ -292,6 +1515,13 @@ int main() { Require(started == 0 || (started == static_cast(0x80410124) && *sceNetErrnoLoc() == 36)); Require(sceNetSocketClose(connecting) == 0); + CheckDatagramAddressLengths(2); + CheckDatagramAddressLengths(28); + CheckStreamAddressLengths(); + CheckBufferSizes(); + CheckOnesBroadcast(); + CheckKernelSocketOption(); + const int epoll = sceNetEpollCreate("guest-sce-net", 0); Require(epoll >= 0); NetEpollEvent registration{}; @@ -324,6 +1554,14 @@ int main() { Require(sceNetSend(client, request, sizeof(request), 0) == sizeof(request)); ready = {}; Require(sceNetEpollWait(epoll, &ready, 1, 1000000) == 1 && (ready.events & 1) && ready.ident == static_cast(accepted)); + Require(sceNetShutdown(accepted, 1) == 0); + Require(Failed(sceNetSend(accepted, request, sizeof(request), 0), 32)); + registration.data.u32 = 78; + Require(sceNetEpollControl(epoll, 2, accepted, ®istration) == 0); + Require(Failed(sceNetEpollControl(epoll, 2, accepted, nullptr), 22)); + Require(sceNetEpollControl(epoll, 3, accepted, nullptr) == 0); + Require(Failed(sceNetEpollControl(epoll, 3, accepted, nullptr), 2)); + Require(Failed(sceNetEpollControl(epoll, 2, accepted, ®istration), 2)); Require(sceNetEpollDestroy(epoll) == 0); Require(sceNetSocketClose(accepted) == 0); bool send_failed = false; @@ -334,11 +1572,13 @@ int main() { Require(send_failed); Require(sceNetSocketClose(client) == 0); Require(sceNetSocketClose(listener) == 0); + *__error_nid_postfix() = 0; Require(sceNetSocketClose(listener) == static_cast(0x80410109) && *sceNetErrnoLoc() == 9); + Require(*__error_nid_postfix() == 9); const int udp_receiver = sceNetSocket(nullptr, 2, 2, 17); const int udp_sender = sceNetSocket(nullptr, 2, 2, 17); - Require(udp_receiver >= 0 && udp_sender >= 0); + Require(udp_receiver >= 0 && udp_sender >= 0 && udp_receiver < 1024 && udp_sender < 1024); address = {16, 2, 0, 0, 127, 0, 0, 1}; Require(sceNetBind_nid_postfix(udp_receiver, address.data(), address.size()) == 0); address_size = address.size(); @@ -351,6 +1591,10 @@ int main() { Require(sceNetRecvfrom(udp_receiver, datagram_result, sizeof(datagram_result), 0, source.data(), &address_size) == sizeof(datagram_result)); Require(std::strcmp(datagram, datagram_result) == 0 && source[1] == 2); + std::array unsized = address; + unsized[0] = 0; + Require(sceNetSendto(udp_sender, datagram, sizeof(datagram), 0, unsized.data(), unsized.size()) == sizeof(datagram)); + Require(sceNetRecvfrom(udp_receiver, datagram_result, sizeof(datagram_result), 0, nullptr, nullptr) == sizeof(datagram_result)); std::uint64_t readable[16]{}; readable[udp_receiver / 64] |= std::uint64_t{1} << (udp_receiver % 64); const std::int64_t poll_now[2]{0, 0}; @@ -365,6 +1609,44 @@ int main() { const std::int64_t bad_timeout[2]{0, 1000000}; Require(select_nid_postfix(udp_receiver + 1, readable, nullptr, nullptr, bad_timeout) == -1 && *__error_nid_postfix() == 22); Require(select_nid_postfix(1025, nullptr, nullptr, nullptr, poll_now) == -1 && *__error_nid_postfix() == 22); + const int random = open_nid_postfix("/dev/urandom", 0, 0); + Require(random >= 0 && random < 1024); + std::uint64_t random_readable[16]{}; + std::uint64_t random_urgent[16]{}; + random_readable[random / 64] |= std::uint64_t{1} << (random % 64); + random_readable[udp_receiver / 64] |= std::uint64_t{1} << (udp_receiver % 64); + random_urgent[random / 64] |= std::uint64_t{1} << (random % 64); + Require(select_nid_postfix(std::max(random, udp_receiver) + 1, random_readable, nullptr, random_urgent, nullptr) == 1); + Require(((random_readable[random / 64] >> (random % 64)) & 1u) != 0); + Require(((random_readable[udp_receiver / 64] >> (udp_receiver % 64)) & 1u) == 0); + Require(random_urgent[random / 64] == 0); + Require(close_nid_postfix(random) == 0); + const int kernel_socket = socket_nid_postfix(2, 2, 0); + Require(kernel_socket >= 0 && kernel_socket < 1024 && kernel_socket != udp_receiver && kernel_socket != udp_sender); + std::array kernel_address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(kernel_socket, kernel_address.data(), kernel_address.size()) == 0); + std::uint64_t mixed_writable[16]{}; + mixed_writable[kernel_socket / 64] |= std::uint64_t{1} << (kernel_socket % 64); + mixed_writable[udp_sender / 64] |= std::uint64_t{1} << (udp_sender % 64); + Require(select_nid_postfix(std::max(kernel_socket, udp_sender) + 1, nullptr, mixed_writable, nullptr, wait_second) == 2); + Require(((mixed_writable[kernel_socket / 64] >> (kernel_socket % 64)) & 1u) != 0); + Require(((mixed_writable[udp_sender / 64] >> (udp_sender % 64)) & 1u) != 0); + 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}; + Require(select_nid_postfix(3, stdio_readable, stdio_writable, nullptr, poll_now) == 2); + Require(stdio_readable[0] == 0 && stdio_writable[0] == 6); + stdio_readable[0] = 1; + Require(select_nid_postfix(1, stdio_readable, nullptr, nullptr, poll_now) == 0 && stdio_readable[0] == 0); + CheckReceiveTimeout(udp_receiver, udp_sender, address); + CheckMicrosecondTimeouts(udp_receiver); CheckMessages(udp_receiver, udp_sender, address); Require(sceNetSocketClose(udp_sender) == 0); Require(sceNetSocketClose(udp_receiver) == 0); @@ -374,7 +1656,7 @@ 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) == + Require(sceNetResolverStartNtoa(resolver, "guest-sce-net.invalid.", ipv4.data(), 5000000, 1, 0) == static_cast(0x804101E1)); Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == static_cast(0x804101E1)); @@ -383,6 +1665,15 @@ int main() { resolver_error == static_cast(0x804101E1)); Require(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), 5000000, 1, 0) == 0); Require(ipv4[0] == 127); + std::array resolved6{}; + resolved6.fill(0xA5); + Require(sceNetResolverStartNtoa6(resolver, "::1", resolved6.data(), 5000000, 1, 0) == 0); + Require(std::all_of(resolved6.begin(), resolved6.end() - 1, [](std::uint8_t byte) { return byte == 0; }) && resolved6[15] == 1); + Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == 0); + Require(sceNetResolverStartNtoa6(resolver, nullptr, resolved6.data(), 5000000, 1, 0) == static_cast(0x80410116)); + Require(sceNetResolverStartNtoa6(resolver, "127.0.0.1", resolved6.data(), 5000000, 1, 0) == static_cast(0x804101E1)); + Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == static_cast(0x804101E1)); + Require(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), 5000000, 1, 0) == 0); Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == 0); std::array records{}; records.fill(0xA5); @@ -407,15 +1698,143 @@ 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) == + Require(sceNetResolverStartNtoaMultipleRecordsEx(resolver, "guest-sce-net.invalid.", records.data(), 5000000, 1, 0) == static_cast(0x804101E1) && records == resolved); + + std::array plain{}; + plain.fill(0xA5); + Require(sceNetResolverStartNtoaMultipleRecords(resolver, "localhost", plain.data(), 5000000, 1, 0) == 0); + Require(plain == resolved); + Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == 0); + Require(sceNetResolverStartNtoaMultipleRecords(resolver, nullptr, plain.data(), 5000000, 1, 0) == + static_cast(0x80410116) && *sceNetErrnoLoc() == 22 && plain == resolved); + Require(sceNetResolverStartNtoaMultipleRecords(resolver, "localhost", nullptr, 5000000, 1, 0) == + static_cast(0x80410116) && *sceNetErrnoLoc() == 22); + Require(sceNetResolverStartNtoaMultipleRecords(resolver, "guest-sce-net.invalid", plain.data(), 5000000, 1, 0) == + static_cast(0x804101E1) && plain == resolved); + Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == static_cast(0x804101E1)); + for (const int flags : {0x1, 0x10000}) { + bool threw = false; + try { sceNetResolverStartNtoaMultipleRecords(resolver, "localhost", plain.data(), 5000000, 1, flags); } + catch (const std::runtime_error&) { threw = true; } + Require(threw && plain == resolved); + } + + const std::array loopback{127, 0, 0, 1}; + char reverse[256]{}; + Require(sceNetResolverAbort(resolver, 0) == 0); + Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == static_cast(0x804101E1)); + Require(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), 5000000, 1, 0) == 0); + Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == 0); + Require(sceNetResolverAbort(resolver, 1) == 0); + Require(sceNetResolverAbort(resolver, 1) == 0); + Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == 0); + const int reverse_result = sceNetResolverStartAton(resolver, loopback.data(), reverse, sizeof(reverse), 5000000, 1, 0); + Require(reverse_result == 0 || reverse_result == static_cast(0x804101E1)); + Require(sceNetResolverStartNtoa(resolver, nullptr, ipv4.data(), 5000000, 1, 0) == static_cast(0x80410116)); + ipv4.fill(0xA5); + Require(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), 5000000, 1, 0) == static_cast(0x80410104) && + *sceNetErrnoLoc() == 4 && ipv4[0] == 0xA5); + Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == static_cast(0x80410104)); + Require(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), 5000000, 1, 0) == 0 && ipv4[0] == 127); + Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == 0); + Require(sceNetResolverAbort(resolver, 1) == 0); + Require(sceNetResolverStartNtoaMultipleRecords(resolver, "localhost", plain.data(), 5000000, 1, 0) == + static_cast(0x80410104) && *sceNetErrnoLoc() == 4); + Require(sceNetResolverStartNtoaMultipleRecords(resolver, "localhost", plain.data(), 5000000, 1, 0) == 0 && plain == resolved); + Require(sceNetResolverAbort(resolver, 2) == 0); + Require(sceNetResolverStartNtoaMultipleRecordsEx(resolver, "localhost", records.data(), 5000000, 1, 0) == 0 && records == resolved); + std::memset(reverse, 0x5A, sizeof(reverse)); + Require(sceNetResolverStartAton(resolver, loopback.data(), reverse, sizeof(reverse), 5000000, 1, 0) == + static_cast(0x80410104) && *sceNetErrnoLoc() == 4 && reverse[0] == 0x5A); + Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == static_cast(0x80410104)); + Require(sceNetResolverStartAton(resolver, loopback.data(), reverse, sizeof(reverse), 5000000, 1, 0) == reverse_result); + Require(sceNetResolverAbort(resolver, 3) == 0); + Require(sceNetResolverStartAton(resolver, loopback.data(), reverse, sizeof(reverse), 5000000, 1, 0) == + static_cast(0x80410104) && *sceNetErrnoLoc() == 4); + Require(sceNetResolverStartNtoaMultipleRecordsEx(resolver, "localhost", records.data(), 5000000, 1, 0) == + static_cast(0x80410104) && *sceNetErrnoLoc() == 4 && records == resolved); + Require(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), 5000000, 1, 0) == 0); + for (const int flags : {0x4, -1}) { + bool threw = false; + try { sceNetResolverAbort(resolver, flags); } catch (const std::runtime_error&) { threw = true; } + Require(threw); + } + Require(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), 5000000, 1, 0) == 0); + + 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); + Require(sceNetResolverStartNtoaMultipleRecords(resolver, "localhost", plain.data(), 5000000, 1, 0) == 0); + Require(sceNetResolverDestroy(other) == 0); + Require(sceNetResolverAbort(other, 0) == static_cast(0x80410109) && *sceNetErrnoLoc() == 9); + Require(sceNetResolverAbort(-1, 1) == static_cast(0x80410109) && *sceNetErrnoLoc() == 9); Require(sceNetResolverGetError(resolver, nullptr) == static_cast(0x80410116) && *sceNetErrnoLoc() == 22); + Require(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 : {0x1, -1}) { + bool threw = false; + try { sceNetClearDnsCache(flags); } catch (const std::runtime_error&) { threw = true; } + Require(threw); + } Require(sceNetResolverDestroy(resolver) == 0); resolver_error = -1; Require(sceNetResolverGetError(resolver, &resolver_error) == static_cast(0x80410109) && *sceNetErrnoLoc() == 9 && resolver_error == -1); Require(sceNetResolverStartNtoaMultipleRecordsEx(resolver, "localhost", records.data(), 5000000, 1, 0) == static_cast(0x80410109) && *sceNetErrnoLoc() == 9 && records == resolved); + Require(sceNetResolverStartNtoaMultipleRecords(resolver, "localhost", plain.data(), 5000000, 1, 0) == + static_cast(0x80410109) && *sceNetErrnoLoc() == 9 && plain == resolved); + Require(sceNetResolverAbort(resolver, 1) == static_cast(0x80410109) && *sceNetErrnoLoc() == 9); std::array ipv6{}; Require(sceNetInetPton(28, "::1", ipv6.data()) == 1); @@ -423,11 +1842,110 @@ int main() { Require(sceNetInetNtop(28, ipv6.data(), text, sizeof(text)) == text); Require(std::strcmp(text, "::1") == 0); + CheckStreamSender(2); + if (!ipv6Unavailable) CheckStreamSender(28); + int state = -1; Require(sceNetCtlGetState(&state) == 0); Require(state == 0 || state == 3); + Require(sceNetCtlGetIfStat(nullptr) == static_cast(0x80412107)); + { + const char* counterPath = "guest_sce_net_byte_counter.txt"; + std::FILE* counter = std::fopen(counterPath, "w"); + Require(counter != nullptr); + Require(std::fputs("1099511627776", counter) >= 0); + Require(std::fclose(counter) == 0); + Require(NetCtlReadByteCounter_nid_no_patch(counterPath) == 1099511627776ull); + Require(std::remove(counterPath) == 0); + bool missing = false; + try { + NetCtlReadByteCounter_nid_no_patch(counterPath); + } catch (const std::runtime_error&) { + missing = true; + } + Require(missing); + } + { + 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; + const int statResult = sceNetCtlGetIfStat(&first); + Require(statResult == (state == 3 ? 0 : static_cast(0x80412108))); + if (statResult == 0) { + Require(first.device == 0 && first.reserved[7] == 0); + NetCtlIfStat second{}; + Require(sceNetCtlGetIfStat(&second) == 0); + Require(second.tx_bytes >= first.tx_bytes && second.rx_bytes >= first.rx_bytes); + } + { + int cid = -1; + Require(sceNetCtlRegisterCallbackV6(nullptr, nullptr, &cid) == static_cast(0x80412107)); + Require(sceNetCtlRegisterCallbackV6(IgnoreNetCtlEvent, nullptr, nullptr) == static_cast(0x80412107)); + int cids[8] = {}; + for (int i = 0; i < 8; ++i) { + Require(sceNetCtlRegisterCallbackV6(IgnoreNetCtlEvent, nullptr, &cids[i]) == 0); + Require(cids[i] == i); + } + 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(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 IPv6"); + 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 new file mode 100644 index 0000000000..5c175d9f37 --- /dev/null +++ b/core/libs/tests/GuestSchedPriority.cpp @@ -0,0 +1,101 @@ +#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(); +} +static constexpr int PrioProcess = 0; +static constexpr int PrioGroup = 1; +static constexpr int PrioUser = 2; +static void Require(bool value) { if (!value) std::abort(); } +static bool Rejects(int policy) { + *__error_nid_postfix() = 0; + const bool max = sched_get_priority_max_nid_postfix(policy) == -1 && *__error_nid_postfix() == 22; + *__error_nid_postfix() = 0; + return max && sched_get_priority_min_nid_postfix(policy) == -1 && *__error_nid_postfix() == 22; +} +static void RequireNice(int which, int who) { + *__error_nid_postfix() = 13; + Require(getpriority_nid_postfix(which, who) == 0 && *__error_nid_postfix() == 13); +} +static void RequireError(int which, int who, int error) { + *__error_nid_postfix() = 0; + Require(getpriority_nid_postfix(which, who) == -1 && *__error_nid_postfix() == error); +} +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}) { + Require(sched_get_priority_max_nid_postfix(policy) == 767); + Require(sched_get_priority_min_nid_postfix(policy) == 256); + } + Require(Rejects(0) && Rejects(4)); + const int self = getpid_nid_postfix(); + RequireNice(PrioProcess, 0); + RequireNice(PrioProcess, self); + RequireNice(PrioGroup, 0); + RequireNice(PrioGroup, self); + RequireNice(PrioUser, 0); + RequireError(PrioProcess, self + 1, 3); + RequireError(PrioProcess, -1, 3); + RequireError(PrioGroup, self + 1, 3); + RequireError(PrioUser, 1000, 3); + RequireError(3, 0, 22); + RequireError(-1, self, 22); + 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/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 new file mode 100644 index 0000000000..979415e15d --- /dev/null +++ b/core/libs/tests/GuestSelect.cpp @@ -0,0 +1,269 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include + +struct GuestTimeval { + std::int64_t seconds; + std::int64_t microseconds; +}; + +struct GuestDescriptorSet { + std::array words{}; + + void Add(int descriptor) { + words[descriptor / 64] |= (std::uint64_t{1} << (descriptor % 64)); + } + + bool Has(int descriptor) const { + return ((words[descriptor / 64] >> (descriptor % 64)) & 1u) != 0; + } +}; + +extern "C" { +int APS5_VABI select_nid_postfix(int, void*, void*, void*, const void*); +int APS5_VABI pipe_nid_postfix(int*); +int APS5_VABI close_nid_postfix(int); +int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t); +std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); +std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); +std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); +std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); +int APS5_VABI 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(); +} + +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]; + const int writer = descriptors[1]; + Require(reader >= 0 && writer >= 0 && reader != writer); + + { + GuestDescriptorSet read{}; + read.Add(reader); + GuestTimeval limit{0, 200000}; + const auto start = std::chrono::steady_clock::now(); + Require(select_nid_postfix(reader + 1, read.words.data(), nullptr, nullptr, &limit) == 0); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(150)); + 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); + GuestDescriptorSet read{}; + GuestDescriptorSet except{}; + read.Add(reader); + except.Add(reader); + GuestTimeval limit{0, 200000}; + Require(select_nid_postfix(reader + 1, read.words.data(), nullptr, except.words.data(), &limit) == 1); + Require(read.Has(reader)); + Require(!except.Has(reader)); + char buffer[1]{}; + Require(read_nid_postfix(reader, buffer, sizeof(buffer)) == 1); + 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); + GuestTimeval limit{0, 200000}; + Require(select_nid_postfix(writer + 1, nullptr, write.words.data(), nullptr, &limit) == 1); + Require(write.Has(writer)); + } + + { + const char byte = 'e'; + Require(write_nid_postfix(writer, &byte, 1) == 1); + GuestDescriptorSet read{}; + GuestDescriptorSet write{}; + read.Add(reader); + write.Add(writer); + const int highest = reader > writer ? reader : writer; + GuestTimeval limit{0, 200000}; + Require(select_nid_postfix(highest + 1, read.words.data(), write.words.data(), nullptr, &limit) == 2); + Require(read.Has(reader) && write.Has(writer)); + } + + { + GuestDescriptorSet read{}; + read.Add(reader); + GuestTimeval limit{0, 100000}; + Require(select_nid_postfix(reader, read.words.data(), nullptr, nullptr, &limit) == 0); + Require(read.Has(reader)); + } + + { + GuestDescriptorSet read{}; + read.Add(reader); + GuestTimeval limit{0, 50000}; + Require(select_nid_postfix(GuestDescriptorLimit + 1, read.words.data(), nullptr, nullptr, &limit) == -1); + Require(*__error_nid_postfix() == GuestEinval); + } + + { + int pipe[2]{-1, -1}; + Require(pipe_nid_postfix(pipe) == 0); + const int receiver = socket_nid_postfix(2, 2, 0); + const int sender = socket_nid_postfix(2, 2, 0); + Require(receiver >= 0 && receiver < GuestDescriptorLimit && sender >= 0 && sender < GuestDescriptorLimit); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(receiver, address.data(), address.size()) == 0); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(receiver, address.data(), &size) == 0); + Require(connect_nid_postfix(sender, address.data(), address.size()) == 0); + const int highest = receiver > pipe[0] ? receiver : pipe[0]; + GuestDescriptorSet idle{}; + idle.Add(receiver); + idle.Add(pipe[0]); + GuestTimeval none{0, 0}; + Require(select_nid_postfix(highest + 1, idle.words.data(), nullptr, nullptr, &none) == 0); + Require(!idle.Has(receiver) && !idle.Has(pipe[0])); + const char byte = 'm'; + Require(send_nid_postfix(sender, &byte, 1, 0) == 1); + Require(write_nid_postfix(pipe[1], &byte, 1) == 1); + GuestDescriptorSet read{}; + GuestTimeval limit{2, 0}; + const auto start = std::chrono::steady_clock::now(); + int ready = 0; + while (ready < 2 && std::chrono::steady_clock::now() - start < std::chrono::seconds(2)) { + read = {}; + read.Add(receiver); + read.Add(pipe[0]); + ready = select_nid_postfix(highest + 1, read.words.data(), nullptr, nullptr, &limit); + } + Require(ready == 2 && read.Has(receiver) && read.Has(pipe[0])); + char buffer[1]{}; + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0) == 1 && buffer[0] == byte); + Require(read_nid_postfix(pipe[0], buffer, sizeof(buffer)) == 1 && buffer[0] == byte); + for (const int descriptor : {receiver, sender, pipe[0], pipe[1]}) Require(close_nid_postfix(descriptor) == 0); + } + + Require(close_nid_postfix(reader) == 0); + Require(close_nid_postfix(writer) == 0); + + { + GuestDescriptorSet read{}; + read.Add(reader); + GuestTimeval limit{0, 50000}; + Require(select_nid_postfix(reader + 1, read.words.data(), nullptr, nullptr, &limit) == -1); + Require(*__error_nid_postfix() == GuestEbadf); + } +} diff --git a/core/libs/tests/GuestSelectWait.cpp b/core/libs/tests/GuestSelectWait.cpp new file mode 100644 index 0000000000..a21b7b320a --- /dev/null +++ b/core/libs/tests/GuestSelectWait.cpp @@ -0,0 +1,73 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +struct GuestTimeval { + std::int64_t seconds; + std::int64_t microseconds; +}; + +extern "C" { +int APS5_VABI select_nid_postfix(int, void*, void*, void*, const void*); +int* APS5_VABI __error_nid_postfix(); +} + +static void Require(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +int main() { + using Clock = std::chrono::steady_clock; + for (const std::int64_t micros : {0, 50, 500, 1000, 2000, 16667}) { + const GuestTimeval limit{0, micros}; + for (const int count : {0, 1024}) { + const auto start = Clock::now(); + Require(select_nid_postfix(count, nullptr, nullptr, nullptr, &limit) == 0, + "select without descriptors did not time out"); + Require(Clock::now() - start >= std::chrono::microseconds(micros), + "select returned before its deadline"); + Require(limit.seconds == 0 && limit.microseconds == micros, + "select changed the guest timeout"); + } + } + const GuestTimeval shortLimit{0, 1000}; + const auto shortWaitStart = Clock::now(); + for (int i = 0; i < 20; ++i) { + Require(select_nid_postfix(0, nullptr, nullptr, nullptr, &shortLimit) == 0, + "repeated select without descriptors did not time out"); + } + const auto shortWait = Clock::now() - shortWaitStart; + Require(shortWait >= std::chrono::milliseconds(20) && shortWait < std::chrono::milliseconds(200), + "twenty 1 ms selects did not stay close to their deadlines"); + for (const GuestTimeval limit : {GuestTimeval{-1, 0}, GuestTimeval{0, -1}, GuestTimeval{0, 1000000}}) { + Require(select_nid_postfix(0, nullptr, nullptr, nullptr, &limit) == -1 && *__error_nid_postfix() == 22, + "select accepted an invalid timeout"); + } + const GuestTimeval zero{}; + for (const int count : {-1, 1025}) { + Require(select_nid_postfix(count, nullptr, nullptr, nullptr, &zero) == -1 && *__error_nid_postfix() == 22, + "select accepted an invalid descriptor limit"); + } + std::array read{}; + read[0] = 1 | (std::uint64_t{1} << 63); + const GuestTimeval limit{0, 500}; + const auto start = Clock::now(); + Require(select_nid_postfix(1, read.data(), nullptr, nullptr, &limit) == 0, + "select with no ready device did not time out"); + Require(Clock::now() - start >= std::chrono::microseconds(limit.microseconds), + "select with no ready device returned before its deadline"); + Require(read[0] == (std::uint64_t{1} << 63), + "select did not preserve bits outside the descriptor limit"); + std::array write{}; + write[0] = 2; + const GuestTimeval longLimit{1, 0}; + Require(select_nid_postfix(2, nullptr, write.data(), nullptr, &longLimit) == 1 && write[0] == 2, + "select did not report a ready device"); + std::puts("20 select wait scenarios passed"); +} diff --git a/core/libs/tests/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/GuestSemaphore.cpp b/core/libs/tests/GuestSemaphore.cpp index fd90d1c804..5392fa1603 100644 --- a/core/libs/tests/GuestSemaphore.cpp +++ b/core/libs/tests/GuestSemaphore.cpp @@ -18,6 +18,7 @@ static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_EBUSY = static_cast(0x80020010); static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); static constexpr int SCE_KERNEL_ERROR_ECANCELED = static_cast(0x80020055); +static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = static_cast(0x8002003C); static void Require(bool value) { if (!value) std::abort(); } @@ -75,6 +76,12 @@ int main() { Require(sceKernelCancelSema(sem, 0, &waiters) == SCE_OK && waiters == 0); Require(sceKernelPollSema(sem, 1) == SCE_KERNEL_ERROR_EBUSY); + KernelUseconds remaining = 1000; + Require(sceKernelWaitSema(sem, 1, &remaining) == SCE_KERNEL_ERROR_ETIMEDOUT && remaining == 0); + Require(sceKernelSignalSema(sem, 1) == SCE_OK); + remaining = 5000000; + Require(sceKernelWaitSema(sem, 1, &remaining) == SCE_OK && remaining > 0 && remaining <= 5000000); + Require(sceKernelSignalSema(sem, 1) == SCE_OK); Require(sceKernelWaitSema(sem, 1, nullptr) == SCE_OK); Require(sceKernelDeleteSema(sem) == SCE_OK); diff --git a/core/libs/tests/GuestSendfile.cpp b/core/libs/tests/GuestSendfile.cpp new file mode 100644 index 0000000000..d8aa5f363e --- /dev/null +++ b/core/libs/tests/GuestSendfile.cpp @@ -0,0 +1,118 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +struct Iovec { + const void* base; + std::uint64_t length; +}; +struct Hdtr { + const Iovec* headers; + int headerCount; + const Iovec* trailers; + int trailerCount; +}; +extern "C" { +int APS5_VABI open_nid_postfix(const char*, int, int); +int APS5_VABI close_nid_postfix(int); +std::int64_t APS5_VABI lseek_nid_postfix(int, std::int64_t, int); +int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI socketpair_nid_postfix(int, int, int, int*); +std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); +int APS5_VABI ioctl_nid_postfix(int, std::uint64_t, void*); +int APS5_VABI sendfile_nid_postfix(int, int, std::int64_t, std::uint64_t, const Hdtr*, std::int64_t*, int); +int* APS5_VABI __error_nid_postfix(); +} +static void Require(bool value) { if (!value) std::abort(); } +static bool Fails(int result, int error) { return result == -1 && *__error_nid_postfix() == error; } +static std::string Receive(int descriptor, std::size_t length) { + std::string data(length, 0); + std::size_t done = 0; + while (done < length) { + const auto result = recv_nid_postfix(descriptor, data.data() + done, length - done, 0); + Require(result > 0); + done += static_cast(result); + } + return data; +} +static std::string Content(std::size_t size) { + std::string content(size, 0); + for (std::size_t i = 0; i < size; ++i) content[i] = static_cast(i * 7 % 251); + return content; +} +static int OpenWritten(const char* path, const std::string& content) { + { + std::ofstream stream(path, std::ios::binary); + stream.write(content.data(), static_cast(content.size())); + } + const int file = open_nid_postfix(path, 0, 0); + Require(file >= 0); + return file; +} +int main() { + const std::string content = Content(200000); + const auto size = static_cast(content.size()); + const int file = OpenWritten("sendfile_probe.bin", content); + int pair[2]; + Require(socketpair_nid_postfix(1, 1, 0, pair) == 0); + const Iovec header[] = {{"GET", 3}, {" ", 1}}; + const Iovec trailer[] = {{"END", 3}}; + const Hdtr parts{header, 2, trailer, 1}; + std::int64_t sent = -1; + Require(sendfile_nid_postfix(file, pair[0], 5, 10, &parts, &sent, 0) == 0 && sent == 17); + Require(Receive(pair[1], 17) == "GET " + content.substr(5, 10) + "END"); + Require(lseek_nid_postfix(file, 0, 1) == 0); + std::string rest; + std::thread reader([&] { rest = Receive(pair[1], content.size() - 100); }); + Require(sendfile_nid_postfix(file, pair[0], 100, 0, nullptr, &sent, 0) == 0 && sent == size - 100); + reader.join(); + Require(rest == content.substr(100)); + Require(sendfile_nid_postfix(file, pair[0], size - 4, 1000, nullptr, &sent, 0) == 0 && sent == 4); + Require(Receive(pair[1], 4) == content.substr(content.size() - 4)); + Require(sendfile_nid_postfix(file, pair[0], size + 10, 0, &parts, &sent, 0) == 0 && sent == 7); + Require(Receive(pair[1], 7) == "GET END"); + const Hdtr nullHeaders{nullptr, 5, trailer, 1}; + Require(sendfile_nid_postfix(file, pair[0], 0, 3, &nullHeaders, nullptr, 0) == 0); + Require(Receive(pair[1], 6) == content.substr(0, 3) + "END"); + Require(lseek_nid_postfix(file, 0, 1) == 0); + + sent = 77; + Require(Fails(sendfile_nid_postfix(file, pair[0], -1, 0, nullptr, &sent, 0), 22) && sent == 77); + const std::vector many(1025, Iovec{"x", 1}); + const Hdtr tooMany{many.data(), 1025, nullptr, 0}; + Require(Fails(sendfile_nid_postfix(file, pair[0], 0, 0, &tooMany, &sent, 0), 22) && sent == 77); + const Hdtr negative{nullptr, 0, trailer, -1}; + Require(Fails(sendfile_nid_postfix(file, pair[0], 0, 0, &negative, &sent, 0), 22) && sent == 77); + Require(Fails(sendfile_nid_postfix(pair[1], pair[0], 0, 0, nullptr, &sent, 0), 22) && sent == 0); + sent = 77; + Require(Fails(sendfile_nid_postfix(file, file, 0, 0, nullptr, &sent, 0), 9) && sent == 0); + const int datagram = socket_nid_postfix(2, 2, 0); + Require(datagram >= 0); + sent = 77; + Require(Fails(sendfile_nid_postfix(file, datagram, 0, 0, nullptr, &sent, 0), 22) && sent == 0); + const int unconnected = socket_nid_postfix(2, 1, 0); + Require(unconnected >= 0); + sent = 77; + Require(Fails(sendfile_nid_postfix(file, unconnected, size + 10, 0, nullptr, &sent, 0), 57) && sent == 0); + + const std::string large = Content(32 << 20); + const int largeFile = OpenWritten("sendfile_large.bin", large); + int on = 1; + Require(ioctl_nid_postfix(pair[0], 0x8004667e, &on) == 0); + sent = -1; + Require(Fails(sendfile_nid_postfix(largeFile, pair[0], 0, 0, nullptr, &sent, 0), 35)); + Require(sent > 0 && sent < static_cast(large.size())); + Require(Receive(pair[1], static_cast(sent)) == large.substr(0, static_cast(sent))); + + Require(close_nid_postfix(largeFile) == 0 && close_nid_postfix(file) == 0); + sent = 77; + Require(Fails(sendfile_nid_postfix(file, pair[0], 0, 0, nullptr, &sent, 0), 9) && sent == 77); + Require(close_nid_postfix(datagram) == 0 && close_nid_postfix(unconnected) == 0); + Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0); + std::filesystem::remove("sendfile_probe.bin"); + std::filesystem::remove("sendfile_large.bin"); +} diff --git a/core/libs/tests/GuestServiceDatabase.cpp b/core/libs/tests/GuestServiceDatabase.cpp new file mode 100644 index 0000000000..30caec9095 --- /dev/null +++ b/core/libs/tests/GuestServiceDatabase.cpp @@ -0,0 +1,62 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libScePosixForWebKit/GuestResolver.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" GuestResolver::ServiceEntry* APS5_VABI getservbyname_nid_postfix(const char*, const char*); + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "Service database check failed at line %u\n", location.line()); + std::abort(); + } +} + +bool NetworkPort(const GuestResolver::ServiceEntry* entry, std::uint16_t port) { + unsigned char bytes[4]; + std::memcpy(bytes, &entry->port, sizeof(bytes)); + return bytes[0] == port >> 8 && bytes[1] == (port & 0xff) && bytes[2] == 0 && bytes[3] == 0; +} + +int main() { + auto* http = getservbyname_nid_postfix("http", "tcp"); + Require(http && std::strcmp(http->name, "http") == 0 && std::strcmp(http->protocol, "tcp") == 0); + Require(NetworkPort(http, 80)); + Require(http->aliases && std::strcmp(http->aliases[0], "www") == 0); + Require(std::strcmp(http->aliases[1], "www-http") == 0 && http->aliases[2] == nullptr); + + auto* https = getservbyname_nid_postfix("https", nullptr); + Require(https && std::strcmp(https->name, "https") == 0 && std::strcmp(https->protocol, "tcp") == 0); + Require(NetworkPort(https, 443) && https->aliases && https->aliases[0] == nullptr); + + auto* domain = getservbyname_nid_postfix("domain", "udp"); + Require(domain && std::strcmp(domain->name, "domain") == 0 && std::strcmp(domain->protocol, "udp") == 0); + Require(NetworkPort(domain, 53)); + + auto* www = getservbyname_nid_postfix("www", "sctp"); + Require(www && std::strcmp(www->name, "http") == 0 && std::strcmp(www->protocol, "sctp") == 0); + Require(NetworkPort(www, 80)); + + auto* mail = getservbyname_nid_postfix("mail", nullptr); + Require(mail && std::strcmp(mail->name, "smtp") == 0 && NetworkPort(mail, 25)); + + errno = 0; + Require(getservbyname_nid_postfix("no-such-service", nullptr) == nullptr && errno == 0); + Require(getservbyname_nid_postfix("domain", "sctp") == nullptr); + Require(getservbyname_nid_postfix("ntp", "TCP") == nullptr); + Require(getservbyname_nid_postfix("HTTP", "tcp") == nullptr); + + auto* ntp = getservbyname_nid_postfix("ntp", "udp"); + Require(ntp && NetworkPort(ntp, 123)); + std::thread([] { + auto* ssh = getservbyname_nid_postfix("ssh", "tcp"); + Require(ssh && std::strcmp(ssh->name, "ssh") == 0 && NetworkPort(ssh, 22)); + }).join(); + Require(std::strcmp(ntp->name, "ntp") == 0 && std::strcmp(ntp->protocol, "udp") == 0 && NetworkPort(ntp, 123)); + return 0; +} diff --git a/core/libs/tests/GuestSetrlimit.cpp b/core/libs/tests/GuestSetrlimit.cpp new file mode 100644 index 0000000000..a869eec59c --- /dev/null +++ b/core/libs/tests/GuestSetrlimit.cpp @@ -0,0 +1,92 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include + +struct ResourceLimit { + std::int64_t rlim_cur; + std::int64_t rlim_max; +}; + +extern "C" { +int APS5_VABI getrlimit_nid_postfix(int, ResourceLimit*); +int APS5_VABI setrlimit_nid_postfix(int, const ResourceLimit*); +int* APS5_VABI __error_nid_postfix(); +} + +static constexpr int GUEST_EFAULT = 14; +static constexpr int GUEST_EINVAL = 22; +static constexpr int GUEST_RLIMIT_DATA = 2; +static constexpr int GUEST_RLIMIT_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(); + +static void Require(bool value) { if (!value) std::abort(); } + +static bool Throws(int resource, const ResourceLimit& limit) { + try { + setrlimit_nid_postfix(resource, &limit); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +static void RequireError(int resource, const ResourceLimit* limit, int error) { + *__error_nid_postfix() = 0; + Require(setrlimit_nid_postfix(resource, limit) == -1); + Require(*__error_nid_postfix() == error); +} + +int main() { + const ResourceLimit infinity{GUEST_RLIM_INFINITY, GUEST_RLIM_INFINITY}; + RequireError(-1, nullptr, GUEST_EFAULT); + RequireError(GUEST_RLIM_NLIMITS, nullptr, GUEST_EFAULT); + RequireError(-1, &infinity, GUEST_EINVAL); + RequireError(GUEST_RLIM_NLIMITS, &infinity, GUEST_EINVAL); + + 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; + Require(setrlimit_nid_postfix(GUEST_RLIMIT_DATA, ¤t) == 0); + Require(*__error_nid_postfix() == 13); + + if (current.rlim_cur > 0) { + const ResourceLimit changed{current.rlim_cur - 1, current.rlim_max}; + Require(Throws(GUEST_RLIMIT_DATA, changed)); + } + const ResourceLimit softAboveHard{GUEST_RLIM_INFINITY, current.rlim_max}; + if (current.rlim_cur == current.rlim_max) + Require(setrlimit_nid_postfix(GUEST_RLIMIT_DATA, &softAboveHard) == 0); + else + Require(Throws(GUEST_RLIMIT_DATA, softAboveHard)); + + const ResourceLimit negative{-1, -2}; + if (current.rlim_cur == GUEST_RLIM_INFINITY && current.rlim_max == GUEST_RLIM_INFINITY) + Require(setrlimit_nid_postfix(GUEST_RLIMIT_DATA, &negative) == 0); + else + Require(Throws(GUEST_RLIMIT_DATA, negative)); + + Require(Throws(GUEST_RLIMIT_NOFILE, infinity)); + return 0; +} diff --git a/core/libs/tests/GuestShaderLink.cpp b/core/libs/tests/GuestShaderLink.cpp index 20ba2f0cdf..f5b510652b 100644 --- a/core/libs/tests/GuestShaderLink.cpp +++ b/core/libs/tests/GuestShaderLink.cpp @@ -16,7 +16,9 @@ static unsigned links = 0; static const Shader* mappedPixel = nullptr; extern "C" void AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span stages, std::span context, std::span primitive) { - Require(!stages.empty() && stages[0] != nullptr && context.size() == 2u && primitive.size() == 3u); + Require(!stages.empty() && stages[0] != nullptr && primitive.size() == 3u); + Require((stages.size() == 1u && context.size() == 2u) || + (stages.size() == 2u && stages[1] == mappedPixel && context.size() == 34u)); ++preparations; } @@ -34,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) { @@ -50,7 +54,7 @@ int main() { } Require(hasRenderTarget && hasRasterizer); ShaderSpecialRegs special{}; - special.vgt_shader_stages_en = {VGT_SHADER_STAGES_EN, VGT_SHADER_STAGES_NGG_BIT}; + special.vgt_shader_stages_en = {VGT_SHADER_STAGES_EN, 0x2000u}; special.vgt_gs_out_prim_type = {VGT_GS_OUT_PRIM_TYPE, 0}; special.ge_cntl = {GE_CNTL, 0x123}; special.ge_user_vgpr_en = {GE_USER_VGPR_EN, 7}; @@ -64,7 +68,7 @@ int main() { context.back() = {0xdeadbeef, 0xcafebabe}; primitive.back() = context.back(); Require(sceAgcLinkShaders(context.data(), primitive.data(), nullptr, &vertex, &pixel, 4) == 0); - Require(context[0].offset == VGT_SHADER_STAGES_EN && context[0].value == VGT_SHADER_STAGES_NGG_BIT); + Require(context[0].offset == VGT_SHADER_STAGES_EN && context[0].value == 0x2000u); Require(context[1].offset == VGT_GS_OUT_PRIM_TYPE && context[1].value == 2); for (unsigned i = 0; i < 32; ++i) Require(context[i + 2].offset == SPI_PS_INPUT_CNTL_0 + i && context[i + 2].value == i); @@ -104,5 +108,5 @@ int main() { input.default_value = 2; Require(sceAgcLinkShaders(context.data(), primitive.data(), nullptr, &vertex, &pixel, 4) == 0); Require(context[2].value == 0x220); - Require(preparations == 4u && mappings == 3u && links == 7u); + Require(preparations == 7u && mappings == 3u && links == 7u); } diff --git a/core/libs/tests/GuestShare.cpp b/core/libs/tests/GuestShare.cpp index 400e2f5a73..698b357e1e 100644 --- a/core/libs/tests/GuestShare.cpp +++ b/core/libs/tests/GuestShare.cpp @@ -8,6 +8,7 @@ extern "C" { int APS5_VABI sceShareCaptureScreenshotExtended(const void* extended_param, std::int32_t* req_id); int APS5_VABI sceShareCaptureScreenshot(const void* param, std::int32_t* req_id); int APS5_VABI sceShareCaptureVideoClip(const void* param, std::int32_t* req_id); +int APS5_VABI sceShareCaptureVideoClipExtended(const void* extended_param, std::int32_t* req_id); int APS5_VABI sceShareGetCurrentStatus(std::uint32_t feature_flag, void* status); int APS5_VABI sceShareOpenMenuForContent(const void* content_id); int APS5_VABI sceShareGetRunningStatus(std::uint32_t* status); @@ -24,18 +25,21 @@ int main() { constexpr std::int32_t notSupported = static_cast(0x81960007); std::uint8_t param[64]{}; - std::int32_t reqId = 7; - Require(sceShareCaptureScreenshotExtended(param, &reqId) == notSupported); - Require(reqId == -1); - - reqId = 7; - Require(sceShareCaptureScreenshotExtended(nullptr, &reqId) == notSupported); - Require(reqId == -1); - - Require(sceShareCaptureScreenshotExtended(param, nullptr) == notSupported); - Require(sceShareCaptureScreenshotExtended(nullptr, nullptr) == notSupported); - using Capture = int (APS5_VABI*)(const void*, std::int32_t*); + std::int32_t reqId = 7; + for (Capture capture : {sceShareCaptureScreenshotExtended, sceShareCaptureVideoClipExtended}) { + reqId = 7; + Require(capture(param, &reqId) == notSupported); + Require(reqId == -1); + + reqId = 7; + Require(capture(nullptr, &reqId) == notSupported); + Require(reqId == -1); + + Require(capture(param, nullptr) == notSupported); + Require(capture(nullptr, nullptr) == notSupported); + } + for (Capture capture : {sceShareCaptureScreenshot, sceShareCaptureVideoClip}) { reqId = 7; Require(capture(param, &reqId) == notSupported); diff --git a/core/libs/tests/GuestSharePlay.cpp b/core/libs/tests/GuestSharePlay.cpp new file mode 100644 index 0000000000..fe2819c276 --- /dev/null +++ b/core/libs/tests/GuestSharePlay.cpp @@ -0,0 +1,47 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#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); +} + +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() { + alignas(16) static unsigned char heap[0x1800]; + Require(sceSharePlayInitialize(nullptr, sizeof(heap)) == 0); + Require(sceSharePlayTerminate() == 0); + Require(sceSharePlayInitialize(heap, sizeof(heap)) == 0); + Require(sceSharePlayTerminate() == 0); + Require(Throws([&] { sceSharePlayInitialize(heap, 0); })); + Require(Throws([&] { sceSharePlayInitialize(nullptr, 0); })); + SharePlayConnectionInfoA info; + std::memset(&info, 0x5a, sizeof(info)); + Require(sceSharePlayGetCurrentConnectionInfoA(&info) == 0); + Require(info.status == 0 && info.mode == 0 && info.hostAccountId == 0 && info.visitorAccountId == 0); + Require(info.hostOnlineId.data[0] == 0 && info.visitorOnlineId.data[0] == 0 && info.hostOnlineId.pad[2] == 0); + Require(info.hostUserId == -1 && info.visitorUserId == -1); + Require(sceSharePlayGetCurrentConnectionInfoA(nullptr) == static_cast(0x810E0001)); + Require(sceSharePlayProhibitController(1) == 0); + Require(sceSharePlayProhibitController(1) == 0); + Require(sceSharePlayProhibitController(0) == 0); + Require(Throws([] { sceSharePlayProhibitController(2); })); + Require(Throws([] { sceSharePlayProhibitController(-1); })); +} diff --git a/core/libs/tests/GuestShmOpen.cpp b/core/libs/tests/GuestShmOpen.cpp new file mode 100644 index 0000000000..bc2fde5cb1 --- /dev/null +++ b/core/libs/tests/GuestShmOpen.cpp @@ -0,0 +1,73 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +extern "C" { +int APS5_VABI shm_open_nid_postfix(const char*, int, int); +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(); } +static const char* const anonymous = reinterpret_cast(1); +static void RequireInvalid(const char* path, int flags) { + *__error_nid_postfix() = 0; + Require(shm_open_nid_postfix(path, flags, 0600) == -1); + Require(*__error_nid_postfix() == 22); +} +int main() { + *__error_nid_postfix() = 13; + const int first = shm_open_nid_postfix(anonymous, 0x100202, 0600); + const int second = shm_open_nid_postfix(anonymous, 0x2, 0); + const int third = shm_open_nid_postfix(anonymous, 0x100e02, 0400); + Require(first >= 0 && second >= 0 && third >= 0); + Require(first != second && second != third && first != third); + Require(*__error_nid_postfix() == 13); + Require(ftruncate_nid_postfix(first, 0x4000) == 0); + const char text[] = "anonymous"; + Require(pwrite_nid_disambig1_nid_postfix(first, text, sizeof(text), 0x1000) == static_cast(sizeof(text))); + char back[sizeof(text)] = {}; + Require(pread_nid_postfix(first, back, sizeof(back), 0x1000) == static_cast(sizeof(back))); + Require(std::memcmp(back, text, sizeof(text)) == 0); + char last = 1; + 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 + Require(pwrite_nid_disambig1_nid_postfix(third, text, sizeof(text), 0) == 0); +#endif + Require(ftruncate_nid_postfix(third, 0x4000) == 0); + Require(pwrite_nid_disambig1_nid_postfix(third, text, sizeof(text), 0) == static_cast(sizeof(text))); + Require(close_nid_postfix(first) == 0 && close_nid_postfix(second) == 0 && close_nid_postfix(third) == 0); + RequireInvalid(anonymous, 0x0); + RequireInvalid(anonymous, 0x200); + RequireInvalid(anonymous, 0x1); + RequireInvalid(anonymous, 0x3); + RequireInvalid(anonymous, 0x2 | 0x8); + RequireInvalid(anonymous, 0x2 | 0x4); + RequireInvalid("/anyps5-shm", 0x1); + RequireInvalid("/anyps5-shm", 0x202 | 0x8); +#ifdef _WIN32 + bool threw = false; + try { + shm_open_nid_postfix("/anyps5-shm", 0x202, 0600); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); +#else + RequireInvalid("", 0x202); +#endif +} diff --git a/core/libs/tests/GuestSignalList.cpp b/core/libs/tests/GuestSignalList.cpp new file mode 100644 index 0000000000..8035660662 --- /dev/null +++ b/core/libs/tests/GuestSignalList.cpp @@ -0,0 +1,25 @@ +#include +#include + +extern "C" { +extern const char* const sys_siglist_nid_postfix[32]; +extern const int sys_nsig_nid_postfix; +} + +static constexpr int GUEST_NSIG = 32; +static constexpr int GUEST_SIGHUP = 1; +static constexpr int GUEST_SIGINT = 2; +static constexpr int GUEST_SIGSEGV = 11; +static constexpr int GUEST_SIGUSR2 = 31; + +static void Require(bool value) { if (!value) std::abort(); } + +int main() { + Require(sys_nsig_nid_postfix == GUEST_NSIG); + Require(std::strcmp(sys_siglist_nid_postfix[0], "Signal 0") == 0); + Require(std::strcmp(sys_siglist_nid_postfix[GUEST_SIGHUP], "Hangup") == 0); + Require(std::strcmp(sys_siglist_nid_postfix[GUEST_SIGINT], "Interrupt") == 0); + Require(std::strcmp(sys_siglist_nid_postfix[GUEST_SIGSEGV], "Segmentation fault") == 0); + Require(std::strcmp(sys_siglist_nid_postfix[GUEST_SIGUSR2], "User defined signal 2") == 0); + for (int signal = 0; signal < sys_nsig_nid_postfix; ++signal) Require(sys_siglist_nid_postfix[signal] != nullptr && sys_siglist_nid_postfix[signal][0] != '\0'); +} diff --git a/core/libs/tests/GuestSignalSets.cpp b/core/libs/tests/GuestSignalSets.cpp index f9df0f1531..786ddae94b 100644 --- a/core/libs/tests/GuestSignalSets.cpp +++ b/core/libs/tests/GuestSignalSets.cpp @@ -1,7 +1,10 @@ +#include "prx/libc/include/general/AsmMacros.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include +#include #include struct GuestSignalSet { @@ -15,15 +18,175 @@ int APS5_VABI sigaddset_nid_postfix(GuestSignalSet*, int); 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); } +#define TEST_ASM_FUNCTION(name) APS5_ASM_FUNCTION(name) +#if defined(_WIN32) || defined(__APPLE__) +#define TEST_ASM_CALL(name) " call " APS5_ASM_SYMBOL(name) "\n" +#else +#define TEST_ASM_CALL(name) " call " name "@PLT\n" +#endif + +asm(".text\n" + TEST_ASM_FUNCTION("JumpKeepingCalleeSaved") + " pushq %rbx\n" + " pushq %rbp\n" + " pushq %r12\n" + " pushq %r13\n" + " pushq %r14\n" + " pushq %r15\n" + " pushq %rdi\n" + " movabsq $0x1111111111111111, %rbx\n" + " movabsq $0x2222222222222222, %rbp\n" + " movabsq $0x3333333333333333, %r12\n" + " movabsq $0x4444444444444444, %r13\n" + " movabsq $0x5555555555555555, %r14\n" + " movabsq $0x6666666666666666, %r15\n" + " movl $1, %esi\n" + TEST_ASM_CALL("sigsetjmp_nid_postfix") + " testl %eax, %eax\n" + " jnz 1f\n" + " xorl %ebx, %ebx\n" + " xorl %ebp, %ebp\n" + " xorl %r12d, %r12d\n" + " xorl %r13d, %r13d\n" + " xorl %r14d, %r14d\n" + " xorl %r15d, %r15d\n" + " movq 0(%rsp), %rdi\n" + " movl $42, %esi\n" + TEST_ASM_CALL("siglongjmp_nid_postfix") + "1:\n" + " movabsq $0x1111111111111111, %rcx\n" + " cmpq %rcx, %rbx\n" + " jne 2f\n" + " movabsq $0x2222222222222222, %rcx\n" + " cmpq %rcx, %rbp\n" + " jne 2f\n" + " movabsq $0x3333333333333333, %rcx\n" + " cmpq %rcx, %r12\n" + " jne 2f\n" + " movabsq $0x4444444444444444, %rcx\n" + " cmpq %rcx, %r13\n" + " jne 2f\n" + " movabsq $0x5555555555555555, %rcx\n" + " cmpq %rcx, %r14\n" + " jne 2f\n" + " movabsq $0x6666666666666666, %rcx\n" + " cmpq %rcx, %r15\n" + " je 3f\n" + "2:\n" + " movl $-1, %eax\n" + "3:\n" + " popq %rdi\n" + " popq %r15\n" + " popq %r14\n" + " popq %r13\n" + " popq %r12\n" + " popq %rbp\n" + " popq %rbx\n" + " ret\n"); + static void Require(bool value) { if (!value) std::abort(); } static bool Equals(const GuestSignalSet& set, std::uint32_t w0, std::uint32_t w1, std::uint32_t w2, std::uint32_t w3) { return set.bits[0] == w0 && set.bits[1] == w1 && set.bits[2] == w2 && set.bits[3] == w3; } +static std::uint32_t BlockedMask() { + GuestSignalSet current{}; + Require(_sigprocmask_nid_postfix(1, nullptr, ¤t) == 0); + return current.bits[0]; +} + +static void SetBlockedMask(std::uint32_t mask) { + const GuestSignalSet set{{mask, 0, 0, 0}}; + Require(_sigprocmask_nid_postfix(3, &set, nullptr) == 0); +} + +static int JumpBack(int saveMask, int value, std::uint32_t maskAtSet, std::uint32_t maskAtJump) { + std::uint64_t buffer[12]{}; + volatile int jumped = 0; + SetBlockedMask(maskAtSet); + const int result = sigsetjmp_nid_postfix(buffer, saveMask); + if (jumped == 0) { + Require(result == 0); + jumped = 1; + SetBlockedMask(maskAtJump); + siglongjmp_nid_postfix(buffer, value); + } + return result; +} + +static void CheckSiglongjmp() { + Require(JumpBack(1, 0, 0x5u, 0x30u) == 1); + Require(BlockedMask() == 0x5u); + Require(JumpBack(1, 7, 0x9u, 0u) == 7); + Require(BlockedMask() == 0x9u); + Require(JumpBack(0, -3, 0x5u, 0x30u) == -3); + Require(BlockedMask() == 0x30u); + Require(JumpBack(2, 5, 0x3u, 0x40u) == 5); + Require(BlockedMask() == 0x3u); + SetBlockedMask(0); +} + +static std::uint32_t BufferWord(const std::uint64_t* buffer, std::size_t offset) { + std::uint32_t word = 0; + std::memcpy(&word, reinterpret_cast(buffer) + offset, sizeof(word)); + return word; +} + +static void CheckSigsetjmp() { + static constexpr std::uint32_t Filler = 0xa5a5a5a5u; + std::uint64_t buffer[12]; + for (auto& word : buffer) word = 0xa5a5a5a5a5a5a5a5ull; + SetBlockedMask(0x30u); + Require(sigsetjmp_nid_postfix(buffer, 0) == 0); + for (std::size_t offset = 72; offset < 88; offset += 4) Require(BufferWord(buffer, offset) == Filler); + Require(BufferWord(buffer, 88) == 0); + Require(BufferWord(buffer, 92) == Filler); + Require(BlockedMask() == 0x30u); + + SetBlockedMask(0x80000005u); + Require(sigsetjmp_nid_postfix(buffer, 1) == 0); + Require(BufferWord(buffer, 72) == 0x80000005u); + for (std::size_t offset = 76; offset < 88; offset += 4) Require(BufferWord(buffer, offset) == 0); + Require(BufferWord(buffer, 88) == 1); + Require(BufferWord(buffer, 92) == Filler); + Require(BlockedMask() == 0x80000005u); + + SetBlockedMask(0x6u); + Require(JumpKeepingCalleeSaved(buffer) == 42); + Require(BlockedMask() == 0x6u); + 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 8c4548a814..5183ec41b0 100644 --- a/core/libs/tests/GuestSignals.cpp +++ b/core/libs/tests/GuestSignals.cpp @@ -1,21 +1,35 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include #include #include #include +#include +#include +#include using Handler = void (APS5_VABI *)(int); -extern "C" { -Handler APS5_VABI signal_nid_postfix(int, Handler); -int APS5_VABI raise_nid_postfix(int); -int APS5_VABI sigaction_nid_postfix(int, const void*, void*); -int APS5_VABI sigprocmask_nid_postfix(int, const void*, void*); -int* APS5_VABI __error_nid_postfix(); -int APS5_VABI _is_signal_return_nid_postfix(std::uint64_t); -} struct GuestSignalSet { std::uint32_t bits[4]; }; +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*); +} struct GuestSigaction { std::uintptr_t handler; int flags; @@ -28,6 +42,35 @@ static bool Same(const GuestSigaction& left, const GuestSigaction& right) { 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)); @@ -45,11 +88,42 @@ int main() { Require(_is_signal_return_nid_postfix(0) == 0); received = 0; Require(raise_nid_postfix(15) == 0 && received == 15); + received = 0; + Require(pthread_kill_nid_postfix(scePthreadSelf(), 15) == 0 && received == 15); + received = 0; + Require(pthread_kill_nid_postfix(scePthreadSelf(), 0) == 0 && received == 0); + Require(pthread_kill_nid_postfix(scePthreadSelf(), 200) == 22); + Require(pthread_kill_nid_postfix(scePthreadSelf(), -1) == 22); + Require(pthread_kill_nid_postfix(nullptr, 15) == 3); + 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); @@ -59,6 +133,16 @@ int main() { Require(sigprocmask_nid_postfix(2, &blocked, nullptr) == 0); Require(sigprocmask_nid_postfix(1, nullptr, &previous) == 0); Require(previous.bits[0] == 0); + *__error_nid_postfix() = 0; + Require(pthread_sigmask_nid_postfix(1, &blocked, nullptr) == 0); + Require(pthread_sigmask_nid_postfix(0, &blocked, &previous) == 22); + Require(pthread_sigmask_nid_postfix(4, &blocked, &previous) == 22); + Require(*__error_nid_postfix() == 0); + Require(pthread_sigmask_nid_postfix(0, nullptr, &previous) == 0); + Require(previous.bits[0] == 0x20); + 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 auto callback = reinterpret_cast(Callback); const auto infoCallback = reinterpret_cast(InfoCallback); @@ -105,4 +189,180 @@ int main() { Require(sigaction_nid_postfix(fixed, &defaults, ¤t) == 0 && current.handler == 0); } Require(sigaction_nid_postfix(15, nullptr, nullptr) == 0); + Require(signal_nid_postfix(15, Callback) != invalid); + GuestSignalSet termMask{{1u << 14, 0, 0, 0}}; + received = 0; + Require(sigprocmask_nid_postfix(1, &termMask, nullptr) == 0); + Require(raise_nid_postfix(15) == 0 && received == 0); + Require(sigprocmask_nid_postfix(2, &termMask, nullptr) == 0); + Require(raise_nid_postfix(15) == 0 && received == 15); + GuestSignalSet urgMask{{1u << 15, 0, 0, 0}}; + received = 0; + Require(sigprocmask_nid_postfix(1, &urgMask, nullptr) == 0); + 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]); + 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); + Require(GuestAlarm_nid_no_patch(0) == 3); + Require(GuestAlarm_nid_no_patch(0) == 0); + const GuestSigaction ignoreAlarm{1, 0, {}}; + Require(sigaction_nid_postfix(14, &ignoreAlarm, nullptr) == 0); + Require(GuestAlarm_nid_no_patch(1) == 0); + std::this_thread::sleep_for(std::chrono::milliseconds(1500)); + Require(GuestAlarm_nid_no_patch(0) == 0); + for (int unmapped : {1, 13, 30, 31, 32, 128}) { + *__error_nid_postfix() = 0; + Require(signal_nid_postfix(unmapped, ignore) == nullptr); + Require(sigaction_nid_postfix(unmapped, nullptr, ¤t) == 0 && current.handler == 1 && current.flags == 0x2); + Require(signal_nid_postfix(unmapped, Callback) == ignore); + Require(sigaction_nid_postfix(unmapped, nullptr, ¤t) == 0 && current.handler == callback && current.flags == 0x2); + Require(signal_nid_postfix(unmapped, nullptr) == Callback); + } + for (int outOfRange : {0, 129, -1}) { + *__error_nid_postfix() = 0; + Require(signal_nid_postfix(outOfRange, nullptr) == invalid && *__error_nid_postfix() == 22); + *__error_nid_postfix() = 0; + Require(signal_nid_postfix(outOfRange, Callback) == invalid && *__error_nid_postfix() == 22); + } + for (int fixed : {9, 17}) { + const GuestSigaction flagged{0, 0x1 | 0x4, {{0x5, 0x6, 0x7, 0x80000000u}}}; + Require(sigaction_nid_postfix(fixed, &flagged, nullptr) == 0); + *__error_nid_postfix() = 0; + Require(signal_nid_postfix(fixed, Callback) == invalid && *__error_nid_postfix() == 22); + *__error_nid_postfix() = 0; + Require(signal_nid_postfix(fixed, ignore) == invalid && *__error_nid_postfix() == 22); + *__error_nid_postfix() = 0; + Require(signal_nid_postfix(fixed, invalid) == invalid && *__error_nid_postfix() == 22); + Require(sigaction_nid_postfix(fixed, nullptr, ¤t) == 0 && current.handler == 0); + *__error_nid_postfix() = 12345; + Require(signal_nid_postfix(fixed, nullptr) == nullptr && *__error_nid_postfix() == 12345); + Require(sigaction_nid_postfix(fixed, nullptr, ¤t) == 0 && current.handler == 0 && current.flags == 0x2 && current.mask.bits[0] == 0); + Require(sigaction_nid_postfix(fixed, &defaults, nullptr) == 0); + } + { + const GuestSigaction masked{callback, 0x1 | 0x4, {{0x5, 0x6, 0x7, 0x80000000u}}}; + Require(sigaction_nid_postfix(13, &masked, nullptr) == 0); + *__error_nid_postfix() = 12345; + Require(signal_nid_postfix(13, ignore) == Callback && *__error_nid_postfix() == 12345); + Require(sigaction_nid_postfix(13, nullptr, ¤t) == 0); + Require(current.handler == 1 && current.flags == 0x2 && current.mask.bits[0] == 0 && current.mask.bits[1] == 0 && current.mask.bits[2] == 0 && current.mask.bits[3] == 0); + Require(sigaction_nid_postfix(13, &masked, nullptr) == 0); + Require(signal_nid_postfix(13, Callback) == Callback); + Require(sigaction_nid_postfix(13, nullptr, ¤t) == 0); + Require(current.handler == callback && current.flags == 0x2 && current.mask.bits[3] == 0); + } + Require(signal_nid_postfix(13, invalid) == Callback); + Require(sigaction_nid_postfix(13, nullptr, ¤t) == 0 && current.handler == static_cast(-1) && current.flags == 0x2); + Require(signal_nid_postfix(13, nullptr) == invalid); + Require(sigaction_nid_postfix(13, nullptr, ¤t) == 0 && current.handler == 0); + *__error_nid_postfix() = 0; + Require(sigpending_nid_postfix(nullptr) == -1); + Require(*__error_nid_postfix() == 14); + *__error_nid_postfix() = 42; + GuestSignalSet pendingSignals{{0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff}}; + Require(sigpending_nid_postfix(&pendingSignals) == 0); + Require(pendingSignals.bits[0] == 0 && pendingSignals.bits[1] == 0 && pendingSignals.bits[2] == 0 && pendingSignals.bits[3] == 0); + Require(*__error_nid_postfix() == 42); + Require(signal_nid_postfix(15, nullptr) == Callback); + Require(signal_nid_postfix(15, Callback) == nullptr); + received = 0; + Require(sigprocmask_nid_postfix(1, &termMask, nullptr) == 0); + Require(raise_nid_postfix(15) == 0 && received == 0); + pendingSignals = {{0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff}}; + Require(sigpending_nid_postfix(&pendingSignals) == 0); + Require(pendingSignals.bits[0] == (1u << 14) && pendingSignals.bits[1] == 0 && pendingSignals.bits[2] == 0 && pendingSignals.bits[3] == 0); + Require(sigprocmask_nid_postfix(2, &termMask, nullptr) == 0 && received == 15); + Require(sigpending_nid_postfix(&pendingSignals) == 0); + Require(pendingSignals.bits[0] == 0 && pendingSignals.bits[1] == 0 && pendingSignals.bits[2] == 0 && pendingSignals.bits[3] == 0); + Require(signal_nid_postfix(15, nullptr) == Callback); + + 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); } diff --git a/core/libs/tests/GuestSigninDialog.cpp b/core/libs/tests/GuestSigninDialog.cpp new file mode 100644 index 0000000000..d8410fcbd5 --- /dev/null +++ b/core/libs/tests/GuestSigninDialog.cpp @@ -0,0 +1,77 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI sceSigninDialogInitialize(void); +int APS5_VABI sceSigninDialogOpen(const void* param); +int APS5_VABI sceSigninDialogUpdateStatus(void); +int APS5_VABI sceSigninDialogGetStatus(void); +int APS5_VABI sceSigninDialogGetResult(void* result); +int APS5_VABI sceSigninDialogClose(void); +int APS5_VABI sceSigninDialogTerminate(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[4]; + std::memset(result, 0x5a, sizeof(result)); + + Require(sceSigninDialogTerminate() == kErrNotInitialized); + Require(sceSigninDialogOpen(¶m) == kErrNotInitialized); + Require(sceSigninDialogClose() == kErrNotInitialized); + Require(sceSigninDialogGetResult(result) == kErrNotInitialized); + Require(sceSigninDialogGetStatus() == kStatusNone); + Require(sceSigninDialogUpdateStatus() == kStatusNone); + + Require(sceSigninDialogInitialize() == 0); + Require(sceSigninDialogInitialize() == kErrAlreadyInitialized); + Require(sceSigninDialogGetStatus() == kStatusInitialized); + + Require(sceSigninDialogOpen(nullptr) == kErrArgNull); + Require(sceSigninDialogGetResult(result) == kErrNotFinished); + + Require(sceSigninDialogOpen(¶m) == 0); + Require(sceSigninDialogGetStatus() == kStatusFinished); + Require(sceSigninDialogUpdateStatus() == kStatusFinished); + + Require(sceSigninDialogGetResult(nullptr) == kErrArgNull); + Require(sceSigninDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(result[1] == 0); + Require(result[2] == 0); + Require(result[3] == 0); + + Require(sceSigninDialogClose() == 0); + Require(sceSigninDialogTerminate() == 0); + Require(sceSigninDialogTerminate() == kErrNotInitialized); + Require(sceSigninDialogGetStatus() == kStatusNone); + + Require(sceSigninDialogInitialize() == 0); + Require(sceSigninDialogGetStatus() == kStatusInitialized); + Require(sceSigninDialogClose() == 0); + Require(sceSigninDialogGetStatus() == kStatusFinished); + std::memset(result, 0xa5, sizeof(result)); + Require(sceSigninDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(result[1] == 0); + Require(result[2] == 0); + Require(result[3] == 0); + Require(sceSigninDialogTerminate() == 0); +} diff --git a/core/libs/tests/GuestSigtimedwait.cpp b/core/libs/tests/GuestSigtimedwait.cpp new file mode 100644 index 0000000000..0663a3ce2f --- /dev/null +++ b/core/libs/tests/GuestSigtimedwait.cpp @@ -0,0 +1,216 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +struct GuestSignalSet { + std::uint32_t bits[4]; +}; +struct GuestTimespec { + std::int64_t seconds; + std::int64_t nanoseconds; +}; +struct GuestSiginfo { + std::int32_t signo; + std::int32_t error; + std::int32_t code; + std::int32_t pid; + std::uint32_t uid; + std::int32_t status; + std::uintptr_t address; + std::int32_t value; + std::int32_t valueTail; + std::int64_t band; + std::int32_t spare[7]; + std::int32_t tail; +}; +using Handler = void (APS5_VABI *)(int); +extern "C" { +int APS5_VABI sigtimedwait_nid_postfix(const GuestSignalSet*, void*, const GuestTimespec*); +int APS5_VABI sigwaitinfo_nid_postfix(const GuestSignalSet*, void*); +int APS5_VABI sigwait_nid_postfix(const GuestSignalSet*, int*); +int APS5_VABI pause_nid_postfix(void); +Handler APS5_VABI signal_nid_postfix(int, Handler); +int APS5_VABI raise_nid_postfix(int); +int APS5_VABI kill_nid_postfix(int, int); +int APS5_VABI _sigprocmask_nid_postfix(int, const GuestSignalSet*, GuestSignalSet*); +int APS5_VABI getpid_nid_postfix(void); +Pthread APS5_VABI scePthreadSelf(void); +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +int APS5_VABI pthread_kill_nid_postfix(Pthread thread, int guest); +int* APS5_VABI __error_nid_postfix(); +} +volatile int received = 0; +void APS5_VABI Callback(int value) { received = value; } +static void Require(bool value) { if (!value) std::abort(); } +static void RequireUntouched(const GuestSiginfo& info) { + const GuestSiginfo empty{}; + Require(std::memcmp(&info, &empty, sizeof(info)) == 0); +} +struct Sender { + Pthread target = nullptr; + int guest = 0; + int result = -1; +}; +static void* APS5_VABI Send(void* arg) { + auto& sender = *static_cast(arg); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + sender.result = pthread_kill_nid_postfix(sender.target, sender.guest); + return nullptr; +} +static void* APS5_VABI SendWithoutDelay(void* arg) { + auto& sender = *static_cast(arg); + sender.result = pthread_kill_nid_postfix(sender.target, sender.guest); + return nullptr; +} +static Pthread StartSender(Sender& sender) { + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, Send, &sender, nullptr) == 0); + return thread; +} +static Pthread StartSenderWithoutDelay(Sender& sender) { + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, SendWithoutDelay, &sender, nullptr) == 0); + return thread; +} +static void JoinSender(Pthread thread, const Sender& sender) { + Require(scePthreadJoin(thread, nullptr) == 0 && sender.result == 0); +} +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}}; + const GuestSignalSet interrupt{{1u << 1, 0, 0, 0}}; + const GuestTimespec poll{0, 0}; + const GuestTimespec expired{0, 1000000000}; + GuestSiginfo info{}; + int taken = -1; + *__error_nid_postfix() = 0; + Require(sigtimedwait_nid_postfix(nullptr, &info, &poll) == -1 && *__error_nid_postfix() == 22); + *__error_nid_postfix() = 0; + Require(sigtimedwait_nid_postfix(&empty, &info, &poll) == -1 && *__error_nid_postfix() == 22); + *__error_nid_postfix() = 0; + Require(sigwaitinfo_nid_postfix(nullptr, &info) == -1 && *__error_nid_postfix() == 22); + *__error_nid_postfix() = 0; + Require(sigwaitinfo_nid_postfix(&empty, &info) == -1 && *__error_nid_postfix() == 22); + *__error_nid_postfix() = 71; + Require(sigwait_nid_postfix(nullptr, &taken) == 22 && *__error_nid_postfix() == 71 && taken == -1); + *__error_nid_postfix() = 72; + Require(sigwait_nid_postfix(&empty, &taken) == 22 && *__error_nid_postfix() == 72 && taken == -1); + *__error_nid_postfix() = 73; + Require(sigwait_nid_postfix(&interrupt, nullptr) == 22 && *__error_nid_postfix() == 73); + RequireUntouched(info); + *__error_nid_postfix() = 0; + Require(sigtimedwait_nid_postfix(&interrupt, &info, &poll) == -1 && *__error_nid_postfix() == 35); + *__error_nid_postfix() = 0; + Require(sigtimedwait_nid_postfix(&interrupt, &info, &expired) == -1 && *__error_nid_postfix() == 22); + RequireUntouched(info); + + GuestSignalSet previous{}; + Require(signal_nid_postfix(2, Callback) != reinterpret_cast(static_cast(-1))); + Require(_sigprocmask_nid_postfix(1, &interrupt, &previous) == 0); + received = 0; + Require(raise_nid_postfix(2) == 0 && received == 0); + Require(sigwait_nid_postfix(&interrupt, &taken) == 0 && taken == 2 && received == 0); + Require(raise_nid_postfix(2) == 0 && received == 0); + Require(sigtimedwait_nid_postfix(&interrupt, &info, &poll) == 2 && received == 0); + Require(info.signo == 2 && info.error == 0 && info.code == 0 && info.pid == getpid_nid_postfix() && info.uid == 0); + Require(info.status == 0 && info.address == 0 && info.value == 0 && info.band == 0); + Require(raise_nid_postfix(2) == 0 && received == 0); + Require(sigwaitinfo_nid_postfix(&interrupt, &info) == 2 && received == 0); + Require(info.signo == 2 && info.code == 0 && info.pid == getpid_nid_postfix()); + Require(raise_nid_postfix(2) == 0 && received == 0); + Require(sigwaitinfo_nid_postfix(&interrupt, nullptr) == 2 && received == 0); + Require(raise_nid_postfix(2) == 0 && received == 0); + Require(sigtimedwait_nid_postfix(&interrupt, nullptr, nullptr) == 2 && received == 0); + GuestSignalSet current{}; + Require(_sigprocmask_nid_postfix(1, nullptr, ¤t) == 0 && current.bits[0] == (previous.bits[0] | interrupt.bits[0])); + Require(_sigprocmask_nid_postfix(3, &previous, nullptr) == 0); + received = 0; + Require(raise_nid_postfix(2) == 0 && received == 2); + + Require(_sigprocmask_nid_postfix(1, &pipe, &previous) == 0); + received = 0; + Require(raise_nid_postfix(13) == 0 && received == 0); + Require(sigwaitinfo_nid_postfix(&pipe, &info) == 13 && info.signo == 13 && info.pid == getpid_nid_postfix()); + Require(_sigprocmask_nid_postfix(3, &previous, nullptr) == 0); + + const Pthread self = scePthreadSelf(); + *__error_nid_postfix() = 0; + Require(pthread_kill_nid_postfix(nullptr, 13) == 3 && *__error_nid_postfix() == 0); + Require(pthread_kill_nid_postfix(self, 129) == 22); + Sender sender{self, 13, -1}; + Pthread senderThread = StartSender(sender); + Require(sigwaitinfo_nid_postfix(&pipe, &info) == 13); + JoinSender(senderThread, sender); + Require(info.signo == 13 && info.code == 0 && info.pid == getpid_nid_postfix()); + senderThread = StartSender(sender); + Require(sigtimedwait_nid_postfix(&pipe, nullptr, nullptr) == 13); + JoinSender(senderThread, sender); + 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; + *__error_nid_postfix() = 0; + Require(pause_nid_postfix() == -1 && *__error_nid_postfix() == 4 && received == 15); + JoinSender(senderThread, sender); + Require(signal_nid_postfix(15, nullptr) == Callback); + + // The senders above sleep 50 ms first, so the signal always arrives while the wait is + // already blocked. A raise landing between the check of the pending set and the wait + // used to be lost, and the wait then never returned, so raise with no delay instead, + // many times. + Require(_sigprocmask_nid_postfix(1, &interrupt, &previous) == 0); + for (int round = 0; round < 500; ++round) { + Sender tight{self, 2, -1}; + const Pthread senderThread = StartSenderWithoutDelay(tight); + Require(sigwait_nid_postfix(&interrupt, &taken) == 0 && taken == 2); + JoinSender(senderThread, tight); + } + Require(_sigprocmask_nid_postfix(3, &previous, nullptr) == 0); + 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/GuestSocketAddressLength.cpp b/core/libs/tests/GuestSocketAddressLength.cpp new file mode 100644 index 0000000000..930a8626d6 --- /dev/null +++ b/core/libs/tests/GuestSocketAddressLength.cpp @@ -0,0 +1,73 @@ +#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 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(); + 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 new file mode 100644 index 0000000000..9a0ac4f5e6 --- /dev/null +++ b/core/libs/tests/GuestSocketDescriptors.cpp @@ -0,0 +1,234 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +struct GuestTimeval { + std::int64_t seconds; + std::int64_t microseconds; +}; + +struct GuestDescriptorSet { + std::array words{}; + + void Add(int descriptor) { + words[descriptor / 64] |= (std::uint64_t{1} << (descriptor % 64)); + } + + bool Has(int descriptor) const { + return ((words[descriptor / 64] >> (descriptor % 64)) & 1u) != 0; + } +}; + +struct PollDescriptor { + int descriptor; + short events; + short revents; +}; + +extern "C" { +int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t); +std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); +std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); +int APS5_VABI close_nid_postfix(int); +int APS5_VABI sceKernelClose(int); +std::int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int, FileStat*); +int APS5_VABI dup_nid_postfix(int); +int APS5_VABI 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*); +int APS5_VABI poll_nid_postfix(PollDescriptor*, std::uint32_t, int); +int* APS5_VABI __error_nid_postfix(); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "socket descriptor check failed at line %d, errno %d\n", line, *__error_nid_postfix()); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +constexpr int DescriptorSetSize = 1024; +constexpr int DuplicateCommand = 0; +constexpr int DuplicateCloseOnExec = 17; +constexpr int Inet = 2; +constexpr int Datagram = 2; +constexpr short In = 0x1; +constexpr int KernelBadDescriptor = static_cast(0x80020009u); + +template static bool Throws(TCall call) { + try { + call(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +static int Socket() { + const int descriptor = socket_nid_postfix(Inet, Datagram, 0); + Require(descriptor >= 0 && descriptor < DescriptorSetSize); + return descriptor; +} + +static int BoundSocket(std::array& address) { + const int descriptor = Socket(); + address = {16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(descriptor, address.data(), address.size()) == 0); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(descriptor, address.data(), &size) == 0); + return descriptor; +} + +static int ConnectedSocket(const std::array& address) { + const int descriptor = Socket(); + Require(connect_nid_postfix(descriptor, address.data(), address.size()) == 0); + return descriptor; +} + +static void WaitReadable(int descriptor) { + GuestDescriptorSet read{}; + read.Add(descriptor); + GuestTimeval limit{2, 0}; + Require(select_nid_postfix(descriptor + 1, read.words.data(), nullptr, nullptr, &limit) == 1); + Require(read.Has(descriptor)); +} + +static void SelectSocketAndFile() { + std::array address{}; + const int receiver = BoundSocket(address); + const int sender = ConnectedSocket(address); + const int file = open_nid_postfix("/dev/urandom", 0, 0); + Require(file >= 0 && file < DescriptorSetSize && file != receiver && file != sender); + const int highest = std::max({receiver, sender, file}); + { + GuestDescriptorSet read{}; + read.Add(receiver); + GuestTimeval none{0, 0}; + Require(select_nid_postfix(receiver + 1, read.words.data(), nullptr, nullptr, &none) == 0); + Require(!read.Has(receiver)); + } + { + GuestDescriptorSet read{}; + read.Add(receiver); + read.Add(file); + GuestTimeval none{0, 0}; + Require(select_nid_postfix(highest + 1, read.words.data(), nullptr, nullptr, &none) == 1); + Require(read.Has(file) && !read.Has(receiver)); + } + const char byte = 'd'; + Require(send_nid_postfix(sender, &byte, 1, 0) == 1); + WaitReadable(receiver); + { + GuestDescriptorSet read{}; + GuestDescriptorSet write{}; + read.Add(receiver); + read.Add(file); + write.Add(sender); + GuestTimeval none{0, 0}; + Require(select_nid_postfix(highest + 1, read.words.data(), write.words.data(), nullptr, &none) == 3); + Require(read.Has(receiver) && read.Has(file) && write.Has(sender)); + } + char received = 0; + Require(recv_nid_postfix(receiver, &received, 1, 0) == 1 && received == byte); + for (const int descriptor : {receiver, sender, file}) Require(close_nid_postfix(descriptor) == 0); +} + +static void CloseReleasesNumber() { + const int socket = Socket(); + Require(close_nid_postfix(socket) == 0); + FileStat status{}; + Require(fstat_nid_disambig1_nid_postfix(socket, &status) == -1 && *__error_nid_postfix() == 9); + std::vector files; + while (files.size() < 8 && (files.empty() || files.back() != socket)) { + const int file = open_nid_postfix("/dev/urandom", 0, 0); + Require(file >= 0); + files.push_back(file); + } + Require(files.back() == socket); + unsigned char bytes[4]{}; + Require(read_nid_postfix(socket, bytes, sizeof(bytes)) == sizeof(bytes)); + PollDescriptor entry{socket, In, 0}; + Require(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); + Require(sceKernelClose(kernelClosed) == KernelBadDescriptor); + entry = {kernelClosed, In, 0}; + Require(poll_nid_postfix(&entry, 1, 0) == 1 && entry.revents == 0x20); +} + +static void DuplicateKeepsSocket() { + std::array address{}; + const int receiver = BoundSocket(address); + const int sender = ConnectedSocket(address); + const int duplicate = dup_nid_postfix(receiver); + Require(duplicate >= 0 && duplicate < DescriptorSetSize && duplicate != receiver && duplicate != sender); + Require(close_nid_postfix(receiver) == 0); + const char byte = 'u'; + Require(send_nid_postfix(sender, &byte, 1, 0) == 1); + WaitReadable(duplicate); + char received = 0; + Require(recv_nid_postfix(duplicate, &received, 1, 0) == 1 && received == byte); + Require(close_nid_postfix(duplicate) == 0); + Require(close_nid_postfix(sender) == 0); +} + +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/GuestSocketError.cpp b/core/libs/tests/GuestSocketError.cpp new file mode 100644 index 0000000000..466bb1b8ec --- /dev/null +++ b/core/libs/tests/GuestSocketError.cpp @@ -0,0 +1,126 @@ +#include "prx/libkernel/Socket/include/SocketError.hpp" +#include +#include +#include + +struct ErrorCase { + const char* name; + int host; + int guest; +}; + +#define HostError(prefix, name, guest) {#prefix #name, prefix##name, guest} + +#ifdef _WIN32 +static constexpr ErrorCase Cases[] = { + HostError(WSA, EINTR, 4), + HostError(WSA, EBADF, 9), + HostError(WSA, EACCES, 13), + HostError(WSA, EFAULT, 14), + HostError(WSA, EINVAL, 22), + HostError(WSA, EMFILE, 24), + HostError(WSA, EWOULDBLOCK, 35), + HostError(WSA, EINPROGRESS, 36), + HostError(WSA, EALREADY, 37), + HostError(WSA, ENOTSOCK, 38), + HostError(WSA, EDESTADDRREQ, 39), + HostError(WSA, EMSGSIZE, 40), + HostError(WSA, EPROTOTYPE, 41), + HostError(WSA, ENOPROTOOPT, 42), + HostError(WSA, EPROTONOSUPPORT, 43), + HostError(WSA, ESOCKTNOSUPPORT, 44), + HostError(WSA, EOPNOTSUPP, 45), + HostError(WSA, EPFNOSUPPORT, 46), + HostError(WSA, EAFNOSUPPORT, 47), + HostError(WSA, EADDRINUSE, 48), + HostError(WSA, EADDRNOTAVAIL, 49), + HostError(WSA, ENETDOWN, 50), + HostError(WSA, ENETUNREACH, 51), + HostError(WSA, ENETRESET, 52), + HostError(WSA, ECONNABORTED, 53), + HostError(WSA, ECONNRESET, 54), + HostError(WSA, ENOBUFS, 55), + HostError(WSA, EISCONN, 56), + HostError(WSA, ENOTCONN, 57), + HostError(WSA, ETOOMANYREFS, 59), + HostError(WSA, ETIMEDOUT, 60), + HostError(WSA, ECONNREFUSED, 61), + HostError(WSA, EHOSTDOWN, 64), + HostError(WSA, EHOSTUNREACH, 65), +}; +static constexpr int UnmappedHost = WSAEDISCON; +static constexpr int ShutdownHost = WSAESHUTDOWN; +#else +static constexpr ErrorCase Cases[] = { + HostError(, EPERM, 1), + HostError(, EINTR, 4), + HostError(, EBADF, 9), + HostError(, ENOMEM, 12), + HostError(, EACCES, 13), + HostError(, EFAULT, 14), + HostError(, EINVAL, 22), + HostError(, ENFILE, 23), + HostError(, EMFILE, 24), + HostError(, EPIPE, 32), + HostError(, EAGAIN, 35), + HostError(, EWOULDBLOCK, 35), + HostError(, EINPROGRESS, 36), + HostError(, EALREADY, 37), + HostError(, ENOTSOCK, 38), + HostError(, EDESTADDRREQ, 39), + HostError(, EMSGSIZE, 40), + HostError(, EPROTOTYPE, 41), + HostError(, ENOPROTOOPT, 42), + HostError(, EPROTONOSUPPORT, 43), + HostError(, ESOCKTNOSUPPORT, 44), + HostError(, EOPNOTSUPP, 45), + HostError(, EPFNOSUPPORT, 46), + HostError(, EAFNOSUPPORT, 47), + HostError(, EADDRINUSE, 48), + HostError(, EADDRNOTAVAIL, 49), + HostError(, ENETDOWN, 50), + HostError(, ENETUNREACH, 51), + HostError(, ENETRESET, 52), + HostError(, ECONNABORTED, 53), + HostError(, ECONNRESET, 54), + HostError(, ENOBUFS, 55), + HostError(, EISCONN, 56), + HostError(, ENOTCONN, 57), + HostError(, ESHUTDOWN, 58), + HostError(, ETOOMANYREFS, 59), + HostError(, ETIMEDOUT, 60), + HostError(, ECONNREFUSED, 61), + HostError(, EHOSTDOWN, 64), + HostError(, EHOSTUNREACH, 65), + HostError(, EPROTO, 92), +}; +static constexpr int UnmappedHost = ENOENT; +#endif + +int main() { + int failures = 0; + for (const auto& entry : Cases) { + const int guest = GuestSockets::GuestErrorFromHost(entry.host); + if (guest != entry.guest) { + std::fprintf(stderr, "%s (%d): guest %d, FreeBSD %d\n", entry.name, entry.host, guest, entry.guest); + ++failures; + } + } + if (GuestSockets::GuestErrorFromHost(UnmappedHost) != 5) { + std::fprintf(stderr, "a host error with no FreeBSD socket counterpart is not EIO\n"); + ++failures; + } + for (const int host : {0, -1}) { + if (GuestSockets::GuestErrorFromHost(host) != 5) { + std::fprintf(stderr, "host error %d is not EIO\n", host); + ++failures; + } + } +#ifdef _WIN32 + if (GuestSockets::GuestErrorFromHost(ShutdownHost) != 32) { + std::fprintf(stderr, "WSAESHUTDOWN is not EPIPE\n"); + ++failures; + } +#endif + return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/core/libs/tests/GuestSocketEvents.cpp b/core/libs/tests/GuestSocketEvents.cpp new file mode 100644 index 0000000000..1ef4eea859 --- /dev/null +++ b/core/libs/tests/GuestSocketEvents.cpp @@ -0,0 +1,208 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI listen_nid_postfix(int, int); +int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI accept_nid_postfix(int, void*, std::uint32_t*); +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); +int APS5_VABI sceKernelAddReadEvent(KernelEqueue eq, int fd, std::size_t size, void* udata); +int APS5_VABI sceKernelDeleteReadEvent(KernelEqueue eq, int fd); +int APS5_VABI sceKernelAddWriteEvent(KernelEqueue eq, int fd, std::size_t size, void* udata); +int APS5_VABI sceKernelDeleteWriteEvent(KernelEqueue eq, int fd); +} + +static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_ENOENT = static_cast(0x80020002); +static constexpr int SCE_KERNEL_ERROR_EBADF = static_cast(0x80020009); +static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = static_cast(0x8002003c); +static constexpr int EVFILT_READ = -1; +static constexpr int EVFILT_WRITE = -2; +static constexpr std::uint16_t EV_EOF = 0x8000; + +static void Require(bool value) { if (!value) std::abort(); } + +static bool Rejects(int (APS5_VABI *add)(KernelEqueue, int, std::size_t, void*), KernelEqueue eq, int fd, std::size_t size) { + try { + add(eq, fd, size, nullptr); + } catch (const std::exception&) { + return true; + } + 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}; + 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); + + KernelEqueue eq = 0; + Require(sceKernelCreateEqueue(&eq, "sockets") == SCE_OK); + const KernelUseconds shortTimeout = 20000; + const KernelUseconds timeout = 1000000; + int marker = 0; + int count = 0; + std::array events{}; + + Require(sceKernelAddReadEvent(eq, listener, 1, &marker) == SCE_OK); + Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &shortTimeout) == SCE_KERNEL_ERROR_ETIMEDOUT); + Require(count == 0); + + const int client = socket_nid_postfix(2, 1, 0); + Require(client >= 0); + Require(connect_nid_postfix(client, address.data(), address.size()) == 0); + Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &timeout) == SCE_OK); + Require(count == 1); + 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); + + Require(sceKernelAddWriteEvent(eq, client, 0, nullptr) == SCE_OK); + Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &timeout) == SCE_OK); + Require(count == 1 && events[0].filter == EVFILT_WRITE && events[0].data > 0); + Require(sceKernelDeleteWriteEvent(eq, client) == SCE_OK); + + Require(sceKernelAddReadEvent(eq, accepted, 1, nullptr) == SCE_OK); + const char message[] = "ready"; + Require(send_nid_postfix(client, message, sizeof(message), 0) == sizeof(message)); + Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &timeout) == SCE_OK); + Require(count == 1 && events[0].ident == static_cast(accepted)); + Require(events[0].data == sizeof(message) && (events[0].flags & EV_EOF) == 0); + char received[sizeof(message)]{}; + Require(recv_nid_postfix(accepted, received, sizeof(received), 0) == sizeof(received)); + Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &shortTimeout) == SCE_KERNEL_ERROR_ETIMEDOUT); + + Require(close_nid_postfix(client) == 0); + Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &timeout) == SCE_OK); + Require(count == 1 && events[0].data == 0 && (events[0].flags & EV_EOF) != 0); + + KernelEqueue blocking = 0; + Require(sceKernelCreateEqueue(&blocking, "blocking") == SCE_OK); + const int writer = socket_nid_postfix(2, 1, 0); + Require(writer >= 0 && connect_nid_postfix(writer, address.data(), address.size()) == 0); + 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)); + }); + Require(sceKernelWaitEqueue(blocking, events.data(), events.size(), &count, nullptr) == SCE_OK); + sender.join(); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(40)); + Require(count == 1 && events[0].ident == static_cast(reader) && events[0].data == sizeof(message)); + Require(recv_nid_postfix(reader, received, sizeof(received), 0) == sizeof(received)); + + std::thread waiter([&] { + std::array woken{}; + int wokenCount = 0; + Require(sceKernelWaitEqueue(blocking, woken.data(), woken.size(), &wokenCount, nullptr) == SCE_OK); + Require(wokenCount == 1 && woken[0].filter == EVFILT_WRITE && woken[0].ident == static_cast(writer)); + }); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(sceKernelAddWriteEvent(blocking, writer, 0, nullptr) == SCE_OK); + waiter.join(); + Require(sceKernelDeleteWriteEvent(blocking, writer) == SCE_OK); + + Require(close_nid_postfix(reader) == 0); + Require(sceKernelDeleteReadEvent(blocking, reader) == SCE_KERNEL_ERROR_ENOENT); + Require(close_nid_postfix(writer) == 0); + Require(sceKernelDeleteEqueue(blocking) == SCE_OK); + + Require(sceKernelAddReadEvent(eq, client, 1, nullptr) == SCE_KERNEL_ERROR_EBADF); + Require(Rejects(sceKernelAddReadEvent, eq, 0, 1)); + Require(Rejects(sceKernelAddWriteEvent, eq, accepted, 64)); + + Require(close_nid_postfix(accepted) == 0); + Require(close_nid_postfix(listener) == 0); + Require(sceKernelDeleteEqueue(eq) == SCE_OK); +} diff --git a/core/libs/tests/GuestSocketHostWrite.cpp b/core/libs/tests/GuestSocketHostWrite.cpp new file mode 100644 index 0000000000..df022d1f8a --- /dev/null +++ b/core/libs/tests/GuestSocketHostWrite.cpp @@ -0,0 +1,417 @@ +#define WIN32_LEAN_AND_MEAN +#include +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#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); +int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI listen_nid_postfix(int, int); +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 fcntl_nid_postfix(int, 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 read_nid_postfix(int, void*, std::uint64_t); +std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); +int APS5_VABI close_nid_postfix(int); +int* APS5_VABI __error_nid_postfix(); +int APS5_VABI sceNetSocket(const char*, int, 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); +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*); +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 Require(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "%s\n", message); + std::exit(1); + } +} + +static DWORD Protection(const void* address) { + MEMORY_BASIC_INFORMATION information{}; + Require(VirtualQuery(address, &information, sizeof(information)) != 0, "query host protection"); + return information.Protect; +} + +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", "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; + const auto selected = std::find_if(std::begin(names), std::end(names), [&](const char* name) { + return std::strcmp(name, argv[1]) == 0; + }); + Require(selected != std::end(names), "unknown receive API"); + const auto api = selected - std::begin(names); + const bool net = api >= 4; + const bool from = api == 1 || api == 5; + const bool peek = api != 2 && api != 3; + const auto create = [&] { + return net ? sceNetSocket(nullptr, 2, stream ? 1 : 2, stream ? 6 : 17) : + socket_nid_postfix(2, stream ? 1 : 2, stream ? 6 : 17); + }; + int receiver = create(); + const int sender = create(); + Require(receiver >= 0 && sender >= 0, "create sockets"); + std::array destination{16, 2, 0, 0, 127, 0, 0, 1}; + Require((net ? sceNetBind_nid_postfix(receiver, destination.data(), destination.size()) : + bind_nid_postfix(receiver, destination.data(), destination.size())) == 0, "bind receiver"); + auto senderAddress = destination; + Require((net ? sceNetBind_nid_postfix(sender, senderAddress.data(), senderAddress.size()) : + bind_nid_postfix(sender, senderAddress.data(), senderAddress.size())) == 0, "bind sender"); + std::uint32_t size = destination.size(); + Require((net ? sceNetGetsockname(receiver, destination.data(), &size) : + getsockname_nid_postfix(receiver, destination.data(), &size)) == 0, "receiver address"); + size = senderAddress.size(); + Require((net ? sceNetGetsockname(sender, senderAddress.data(), &size) : + getsockname_nid_postfix(sender, senderAddress.data(), &size)) == 0, "sender address"); + if (stream) { + Require((net ? sceNetListen(receiver, 1) : listen_nid_postfix(receiver, 1)) == 0, "listen on loopback"); + Require((net ? sceNetConnect(sender, destination.data(), destination.size()) : + connect_nid_postfix(sender, destination.data(), destination.size())) == 0, "connect sender"); + const int accepted = net ? sceNetAccept(receiver, nullptr, nullptr) : accept_nid_postfix(receiver, nullptr, nullptr); + Require(accepted >= 0, "accept sender"); + Require((net ? sceNetSocketClose(receiver) : close_nid_postfix(receiver)) == 0, "close listener"); + receiver = accepted; + } + const auto nonblocking = [&](bool enabled) { + const int value = enabled; + Require((net ? sceNetSetsockopt(receiver, 0xffff, 0x1200, &value, sizeof(value)) : + fcntl_nid_postfix(receiver, 4, enabled ? 4 : 0)) == 0, "set receiver mode"); + }; + nonblocking(true); + const char payload[] = "watched socket data"; + const auto send = [&] { + Require((net ? sceNetSendto(sender, payload, sizeof(payload), 0, stream ? nullptr : destination.data(), + stream ? 0 : destination.size()) : sendto_nid_postfix(sender, payload, sizeof(payload), 0, + stream ? nullptr : destination.data(), stream ? 0 : destination.size())) == sizeof(payload), + "send packet"); + }; + const auto receive = [&](void* buffer, std::size_t bytes, int flags, void* address, std::uint32_t* addressLength) { + switch (api) { + case 0: return recv_nid_postfix(receiver, buffer, bytes, flags); + case 1: return recvfrom_nid_postfix(receiver, buffer, bytes, flags, address, addressLength); + case 2: return read_nid_postfix(receiver, buffer, bytes); + case 3: return sceKernelRead(receiver, buffer, bytes); + case 4: return sceNetRecv(receiver, buffer, bytes, flags); + 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 = [&] { + std::array buffer{}; + int* error = net ? sceNetErrnoLoc() : __error_nid_postfix(); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + std::int64_t result; + do { + result = net ? sceNetRecv(receiver, buffer.data(), buffer.size(), 2) : + recv_nid_postfix(receiver, buffer.data(), buffer.size(), 2); + if (result == sizeof(payload) || (result < 0 && *error != 35)) break; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } while (std::chrono::steady_clock::now() < deadline); + Require(result == sizeof(payload), "wait for complete packet"); + }; + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 0, &physical) == 0, "allocate direct memory"); + void* mapping = nullptr; + Require(sceKernelMapDirectMemory(&mapping, page * 2, 3, 0, physical, 0) == 0, "map read/write memory"); + void* twin = nullptr; + Require(sceKernelMapDirectMemory(&twin, page * 2, 3, 0, physical, 0) == 0, "map read/write memory a second time"); + auto* buffer = static_cast(mapping) + page - 7; + const auto collect = [&] { + std::array pages{}; + std::size_t count = pages.size(); + Require(GuestArena::GuestArenaCollectWrites_nid_postfix(reinterpret_cast(mapping), page * 2, + pages.data(), &count, true), "collect tracked writes"); + return count; + }; + const auto reset = [&] { + const GuestArena::HostWrite destination(buffer - 1, capacity + 2); + Require(destination.Open(), "reset writable receive buffer"); + std::memset(buffer - 1, '!', capacity + 2); + }; + int* error = net ? sceNetErrnoLoc() : __error_nid_postfix(); + for (const std::size_t bytes : {std::size_t{0}, std::size_t{4}, sizeof(payload), capacity}) { + send(); + waitForPacket(); + for (const int flags : {2, 0}) { + if (flags == 2 && !peek) continue; + reset(); + collect(); + Require(Protection(buffer) == PAGE_READONLY && Protection(buffer + capacity - 1) == PAGE_READONLY, + "write tracking arms both guest pages"); + std::array address{}; + size = address.size(); + *error = 123; + const auto result = receive(buffer, bytes, flags, stream ? nullptr : address.data(), stream ? nullptr : &size); + const auto expected = std::min(bytes, sizeof(payload)); + if (result != static_cast(expected)) { + std::fprintf(stderr, "%s capacity=%zu flags=%d result=%lld errno=%d\n", argv[1], bytes, flags, + static_cast(result), *error); + return 1; + } + Require(*error == 123, "successful receive preserves errno"); + Require(buffer[-1] == '!' && std::memcmp(buffer, payload, expected) == 0 && + std::all_of(buffer + expected, buffer + capacity + 1, [](char value) { return value == '!'; }), + "received data and buffer guards"); + if (from && !stream) Require(size == senderAddress.size() && std::memcmp(address.data(), senderAddress.data(), size) == 0, + "sender address preserved"); + if (flags == 0 && ((bytes == 0 && !peek) || (stream && expected < sizeof(payload)))) { + std::array retained{}; + Require(receive(retained.data(), retained.size(), 0, nullptr, nullptr) == sizeof(payload) - expected && + std::memcmp(retained.data(), payload + expected, sizeof(payload) - expected) == 0, + "short stream receive or zero-byte read retains unread data"); + } + if (bytes != 0) Require(collect() != 0, "receive reported as a tracked write"); + collect(); + Require(Protection(buffer) == PAGE_READONLY && Protection(buffer + capacity - 1) == PAGE_READONLY, + "receive releases the host write guard"); + } + } + if (from && !stream) { + std::int64_t outputPhysical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 0, &outputPhysical) == 0, + "allocate sender output memory"); + void* outputMapping = nullptr; + Require(sceKernelMapDirectMemory(&outputMapping, page * 2, 3, 0, outputPhysical, 0) == 0, + "map sender output memory"); + void* outputTwin = nullptr; + Require(sceKernelMapDirectMemory(&outputTwin, page * 2, 3, 0, outputPhysical, 0) == 0, + "map sender output memory a second time"); + const auto collectOutputs = [&] { + std::array pages{}; + std::size_t count = pages.size(); + Require(GuestArena::GuestArenaCollectWrites_nid_postfix(reinterpret_cast(outputMapping), page * 2, + pages.data(), &count, true), "collect sender output writes"); + return count; + }; + for (const int mask : {1, 2, 3}) { + std::array hostAddress{}; + std::uint32_t hostLength = hostAddress.size(); + auto* address = (mask & 1) ? static_cast(outputMapping) + 16 : hostAddress.data(); + auto* length = (mask & 2) ? reinterpret_cast(static_cast(outputMapping) + page + 16) : + &hostLength; + { + const GuestArena::HostWrite addressDestination(address, hostAddress.size()); + const GuestArena::HostWrite lengthDestination(length, sizeof(*length)); + Require(addressDestination.Open() && lengthDestination.Open(), "initialize sender outputs"); + std::memset(address, 0xa5, hostAddress.size()); + *length = hostAddress.size(); + } + collectOutputs(); + Require(collectOutputs() == 0, "sender outputs initially clean"); + if (mask & 1) Require(Protection(address) == PAGE_READONLY, "sender address is watched and armed"); + if (mask & 2) Require(Protection(length) == PAGE_READONLY, "sender length is watched and armed"); + send(); + waitForPacket(); + reset(); + collect(); + *error = 123; + Require(receive(buffer, capacity, 0, address, length) == sizeof(payload) && *error == 123 && + std::memcmp(buffer, payload, sizeof(payload)) == 0, "receive into watched sender outputs"); + Require(*length == senderAddress.size() && std::memcmp(address, senderAddress.data(), *length) == 0, + "watched sender outputs are correct"); + const auto expectedPages = static_cast(((mask & 1) != 0) + ((mask & 2) != 0)); + Require(collectOutputs() == expectedPages, "each watched sender output is tracked"); + Require(collectOutputs() == 0, "sender output writes cleared"); + if (mask & 1) Require(Protection(address) == PAGE_READONLY, "sender address rearmed"); + 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"); + } + reset(); + collect(); + *error = 0; + const auto empty = receive(buffer, capacity, 0, nullptr, nullptr); + const auto expectedAgain = api == 3 ? static_cast(0x80020023u) : + net ? static_cast(0x80410123u) : -1; + Require(empty == expectedAgain && *error == 35, "empty nonblocking receive reports EAGAIN"); + 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(); + *error = 0; + const auto rejected = receive(buffer, capacity, 0, nullptr, nullptr); + const auto expectedError = api == 3 ? static_cast(0x8002000eu) : + net ? static_cast(0x8041010eu) : -1; + Require(rejected == expectedError && *error == 14, "read-only destination fails with EFAULT"); + Require(std::all_of(buffer - 1, buffer + capacity + 1, [](char value) { return value == '!'; }), + "read-only destination unchanged"); + std::array recovery{}; + Require(receive(recovery.data(), recovery.size(), 0, nullptr, nullptr) == sizeof(payload) && + std::memcmp(recovery.data(), payload, sizeof(payload)) == 0, "rejected write preserves queued packet"); + send(); + waitForPacket(); + *error = 123; + Require(receive(buffer, 0, peek ? 2 : 0, nullptr, nullptr) == 0 && *error == 123, + "zero-byte receive does not require a writable destination"); + Require(Protection(buffer) == PAGE_READONLY && Protection(buffer + capacity - 1) == PAGE_READONLY, + "zero-byte receive preserves read-only protection"); + Require(receive(recovery.data(), recovery.size(), 0, nullptr, nullptr) == sizeof(payload) && + std::memcmp(recovery.data(), payload, sizeof(payload)) == 0, "zero-byte peek or read retains queued data"); + Require(sceKernelMprotect(mapping, page * 2, 3) == 0, "restore guest write permission"); + reset(); + collect(); + nonblocking(false); + std::int64_t result = -1; + int workerError = 0; + std::thread worker([&] { + int* slot = net ? sceNetErrnoLoc() : __error_nid_postfix(); + *slot = 123; + result = receive(buffer, capacity, 0, nullptr, nullptr); + workerError = *slot; + }); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + while (Protection(buffer) != PAGE_READWRITE && std::chrono::steady_clock::now() < deadline) + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + Require(Protection(buffer) == PAGE_READWRITE, "blocking receive opens its destination"); + collect(); + Require(Protection(buffer) == PAGE_READWRITE && Protection(buffer + capacity - 1) == PAGE_READWRITE, + "collection does not rearm an in-flight receive"); + send(); + worker.join(); + Require(result == sizeof(payload) && workerError == 123 && std::memcmp(buffer, payload, sizeof(payload)) == 0, + "blocking receive completes and preserves errno"); + Require(collect() != 0, "completion remains dirty after an in-flight collection"); + Require(Protection(buffer) == PAGE_READONLY, "completed receive can be rearmed"); + Require((net ? sceNetSocketClose(sender) : close_nid_postfix(sender)) == 0, "close sender"); + Require((net ? sceNetSocketClose(receiver) : close_nid_postfix(receiver)) == 0, "close receiver"); + Require(sceKernelMunmap(twin, page * 2) == 0, "unmap the second direct memory mapping"); + Require(sceKernelMunmap(mapping, page * 2) == 0, "unmap direct memory"); + Require(sceKernelReleaseDirectMemory(physical, page * 2) == 0, "release direct memory"); +} diff --git a/core/libs/tests/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 new file mode 100644 index 0000000000..7ab20ba2f4 --- /dev/null +++ b/core/libs/tests/GuestSocketOptions.cpp @@ -0,0 +1,229 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI listen_nid_postfix(int, int); +int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI accept_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI setsockopt_nid_postfix(int, int, int, const void*, std::uint32_t); +int APS5_VABI getsockopt_nid_postfix(int, int, int, void*, std::uint32_t*); +int APS5_VABI fcntl_nid_postfix(int, int, ...); +int APS5_VABI poll_nid_postfix(void*, std::uint32_t, int); +int APS5_VABI close_nid_postfix(int); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +constexpr int Stream = 1; +constexpr int Datagram = 2; +constexpr int SolSocket = 0xffff; +constexpr int SoAcceptConnection = 0x2; +constexpr int SoDebug = 0x1; +constexpr int SoKeepAlive = 0x8; +constexpr int SoBroadcast = 0x20; +constexpr int SoUseLoopback = 0x40; +constexpr int SoLinger = 0x80; +constexpr int SoReusePort = 0x200; +constexpr int SoTimestamp = 0x400; +constexpr int SoNoSigpipe = 0x800; +constexpr int SoSendBuffer = 0x1001; +constexpr int SoError = 0x1007; +constexpr int SoType = 0x1008; +constexpr int ErrorInvalid = 22; +constexpr int ErrorDomain = 33; +constexpr int ErrorNoProtocolOption = 42; +constexpr int ErrorConnectionRefused = 61; +constexpr int ErrorBadDescriptor = 9; + +struct Linger { + std::int32_t enabled; + std::int32_t seconds; +}; + +struct PollDescriptor { + std::int32_t descriptor; + std::int16_t events; + std::int16_t revents; +}; + +void Require(bool condition, const char* what) { + if (condition) return; + std::fprintf(stderr, "%s (errno %d)\n", what, *__error_nid_postfix()); + std::abort(); +} + +int GetInt(int descriptor, int option) { + int value = -12345; + std::uint32_t size = sizeof(value); + Require(getsockopt_nid_postfix(descriptor, SolSocket, option, &value, &size) == 0 && size == sizeof(value), "getsockopt of an integer option failed"); + return value; +} + +void SetInt(int descriptor, int option, int value) { + Require(setsockopt_nid_postfix(descriptor, SolSocket, option, &value, sizeof(value)) == 0, "setsockopt of an integer option failed"); +} + +bool SetFails(int descriptor, int level, int option, const void* value, std::uint32_t length, int error) { + return setsockopt_nid_postfix(descriptor, level, option, value, length) == -1 && *__error_nid_postfix() == error; +} + +std::array Loopback(std::uint16_t portNetworkOrder) { + std::array address{16, 2}; + std::memcpy(address.data() + 2, &portNetworkOrder, 2); + address[4] = 127; + address[7] = 1; + return address; +} + +std::array Listen(int* listener) { + *listener = socket_nid_postfix(2, Stream, 0); + Require(*listener >= 0, "socket failed"); + auto address = Loopback(0); + Require(bind_nid_postfix(*listener, address.data(), address.size()) == 0, "bind failed"); + Require(listen_nid_postfix(*listener, 4) == 0, "listen failed"); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(*listener, address.data(), &size) == 0, "getsockname failed"); + return address; +} + +void WaitUntilSettled(int descriptor) { + PollDescriptor entry{descriptor, 0x1 | 0x4, 0}; + Require(poll_nid_postfix(&entry, 1, 6000) == 1, "the nonblocking connect did not settle within 6 s"); +} + +void ReportsTypeAndListening() { + const int stream = socket_nid_postfix(2, Stream, 0); + const int datagram = socket_nid_postfix(2, Datagram, 0); + Require(stream >= 0 && datagram >= 0, "socket failed"); + Require(GetInt(stream, SoType) == Stream, "SO_TYPE of a stream socket must be SOCK_STREAM (1)"); + Require(GetInt(datagram, SoType) == Datagram, "SO_TYPE of a datagram socket must be SOCK_DGRAM (2)"); + Require(GetInt(stream, SoAcceptConnection) == 0, "SO_ACCEPTCONN must be 0 before listen"); + auto address = Loopback(0); + Require(bind_nid_postfix(stream, address.data(), address.size()) == 0 && listen_nid_postfix(stream, 4) == 0, "bind or listen failed"); + Require(GetInt(stream, SoAcceptConnection) == SoAcceptConnection, "SO_ACCEPTCONN of a listener is the option bit (2), as FreeBSD returns so_options & SO_ACCEPTCONN"); + Require(close_nid_postfix(stream) == 0 && close_nid_postfix(datagram) == 0, "close failed"); +} + +void ReportsAndClearsTheConnectError() { + int listener = -1; + auto address = Listen(&listener); + const int client = socket_nid_postfix(2, Stream, 0); + Require(client >= 0 && fcntl_nid_postfix(client, 4, 4) == 0, "nonblocking socket setup failed"); + const int result = connect_nid_postfix(client, address.data(), address.size()); + Require(result == 0 || (result == -1 && *__error_nid_postfix() == 36), "nonblocking connect to a listener must succeed or be in progress"); + WaitUntilSettled(client); + Require(GetInt(client, SoError) == 0, "SO_ERROR after a successful connect must be 0"); + const int accepted = accept_nid_postfix(listener, nullptr, nullptr); + Require(accepted >= 0, "accept failed"); + for (const int descriptor : {client, accepted, listener}) Require(close_nid_postfix(descriptor) == 0, "close failed"); + + int closed = -1; + auto refused = Listen(&closed); + Require(close_nid_postfix(closed) == 0, "close failed"); + const int failing = socket_nid_postfix(2, Stream, 0); + Require(failing >= 0 && fcntl_nid_postfix(failing, 4, 4) == 0, "nonblocking socket setup failed"); + const int attempt = connect_nid_postfix(failing, refused.data(), refused.size()); + if (attempt == -1 && *__error_nid_postfix() != 36) { + Require(*__error_nid_postfix() == ErrorConnectionRefused, "a refused connect must fail with ECONNREFUSED (61)"); + } else { + WaitUntilSettled(failing); + Require(GetInt(failing, SoError) == ErrorConnectionRefused, "SO_ERROR after a refused connect must be ECONNREFUSED (61)"); + Require(GetInt(failing, SoError) == 0, "reading SO_ERROR must clear it"); + } + Require(close_nid_postfix(failing) == 0, "close failed"); +} + +void BooleanOptionsReportTheirBit() { + const int stream = socket_nid_postfix(2, Stream, 0); + const int datagram = socket_nid_postfix(2, Datagram, 0); + Require(stream >= 0 && datagram >= 0, "socket failed"); + Require(GetInt(stream, SoKeepAlive) == 0, "SO_KEEPALIVE is off by default"); + SetInt(stream, SoKeepAlive, 1); + Require(GetInt(stream, SoKeepAlive) == SoKeepAlive, "an enabled boolean option reads back as its own bit (FreeBSD returns so_options & option)"); + SetInt(stream, SoKeepAlive, 0); + Require(GetInt(stream, SoKeepAlive) == 0, "SO_KEEPALIVE must turn off"); + SetInt(datagram, SoBroadcast, 1); + Require(GetInt(datagram, SoBroadcast) == SoBroadcast, "SO_BROADCAST reads back as its bit"); + 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; + std::uint32_t size = sizeof(value); + Require(getsockopt_nid_postfix(stream, SolSocket, option, &value, &size) == -1 && *__error_nid_postfix() == ErrorNoProtocolOption, "getsockopt of an option no title needs must fail with ENOPROTOOPT (42)"); + } + Require(close_nid_postfix(stream) == 0 && close_nid_postfix(datagram) == 0, "close failed"); +} + +void LingerFollowsFreeBsd() { + const int stream = socket_nid_postfix(2, Stream, 0); + Require(stream >= 0, "socket failed"); + Linger linger{1, 5}; + Require(setsockopt_nid_postfix(stream, SolSocket, SoLinger, &linger, sizeof(linger)) == 0, "SO_LINGER must be accepted"); + Linger read{}; + std::uint32_t size = sizeof(read); + Require(getsockopt_nid_postfix(stream, SolSocket, SoLinger, &read, &size) == 0 && size == sizeof(read), "getsockopt SO_LINGER failed"); + Require(read.enabled == SoLinger && read.seconds == 5, "SO_LINGER reads back as {SO_LINGER bit, seconds}"); + linger = {0, 0}; + Require(setsockopt_nid_postfix(stream, SolSocket, SoLinger, &linger, sizeof(linger)) == 0, "SO_LINGER off must be accepted"); + size = sizeof(read); + Require(getsockopt_nid_postfix(stream, SolSocket, SoLinger, &read, &size) == 0 && read.enabled == 0, "SO_LINGER must read back as off"); + linger = {1, -1}; + Require(SetFails(stream, SolSocket, SoLinger, &linger, sizeof(linger), ErrorDomain), "a negative linger time must fail with EDOM (33)"); + linger = {1, 65536}; + Require(SetFails(stream, SolSocket, SoLinger, &linger, sizeof(linger), ErrorDomain), "a linger time above 65535 must fail with EDOM (33)"); + Require(SetFails(stream, SolSocket, SoLinger, &linger, sizeof(std::int32_t), ErrorInvalid), "a linger value shorter than struct linger must fail with EINVAL (22)"); + Require(close_nid_postfix(stream) == 0, "close failed"); +} + +void LengthsFollowFreeBsd() { + const int stream = socket_nid_postfix(2, Stream, 0); + Require(stream >= 0, "socket failed"); + const int one = 1; + Require(SetFails(stream, SolSocket, SoKeepAlive, &one, 2, ErrorInvalid), "an integer option shorter than an int must fail with EINVAL (22)"); + const std::array wide{1}; + Require(setsockopt_nid_postfix(stream, SolSocket, SoKeepAlive, wide.data(), sizeof(wide)) == 0, "a value longer than an int is read from its first int"); + std::array narrow{0xaa, 0xaa}; + std::uint32_t size = narrow.size(); + Require(getsockopt_nid_postfix(stream, SolSocket, SoKeepAlive, narrow.data(), &size) == 0 && size == narrow.size(), "a short buffer gets a truncated value and its own length (sooptcopyout)"); + Require(narrow[0] == SoKeepAlive && narrow[1] == 0, "the truncated value is the low bytes of the int"); + const int zero = 0; + Require(SetFails(stream, SolSocket, SoSendBuffer, &zero, sizeof(zero), ErrorInvalid), "SO_SNDBUF below 1 must fail with EINVAL (22)"); + const int negative = -5; + Require(SetFails(stream, SolSocket, SoSendBuffer, &negative, sizeof(negative), ErrorInvalid), "a negative SO_SNDBUF must fail with EINVAL (22)"); + SetInt(stream, SoSendBuffer, 65536); + Require(close_nid_postfix(stream) == 0, "close failed"); +} + +void UnknownRequestsFail() { + const int stream = socket_nid_postfix(2, Stream, 0); + Require(stream >= 0, "socket failed"); + const int one = 1; + Require(SetFails(stream, SolSocket, 0x7fff, &one, sizeof(one), ErrorNoProtocolOption), "an unknown SOL_SOCKET option must fail with ENOPROTOOPT (42)"); + int value = 0; + std::uint32_t size = sizeof(value); + Require(getsockopt_nid_postfix(stream, SolSocket, 0x7fff, &value, &size) == -1 && *__error_nid_postfix() == ErrorNoProtocolOption, "getsockopt of an unknown option must fail with ENOPROTOOPT (42)"); + Require(setsockopt_nid_postfix(-1, SolSocket, SoKeepAlive, &one, sizeof(one)) == -1 && *__error_nid_postfix() == ErrorBadDescriptor, "setsockopt on a bad descriptor must fail with EBADF (9)"); + Require(close_nid_postfix(stream) == 0, "close failed"); +} + +} + +int main() { + ReportsTypeAndListening(); + ReportsAndClearsTheConnectError(); + BooleanOptionsReportTheirBit(); + LingerFollowsFreeBsd(); + LengthsFollowFreeBsd(); + UnknownRequestsFail(); + return 0; +} diff --git a/core/libs/tests/GuestSocketPair.cpp b/core/libs/tests/GuestSocketPair.cpp index f6a29a8461..9a40c92040 100644 --- a/core/libs/tests/GuestSocketPair.cpp +++ b/core/libs/tests/GuestSocketPair.cpp @@ -1,6 +1,8 @@ +#include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include #include @@ -10,17 +12,38 @@ struct PollDescriptor { short revents; }; +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 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 recvfrom_nid_postfix(int, void*, std::uint64_t, int, void*, std::uint32_t*); +std::int64_t APS5_VABI recvmsg_nid_postfix(int, Msghdr*, int); int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); int APS5_VABI getpeername_nid_postfix(int, void*, std::uint32_t*); int APS5_VABI shutdown_nid_postfix(int, int); int APS5_VABI fcntl_nid_postfix(int, int, ...); int APS5_VABI poll_nid_postfix(PollDescriptor*, std::uint32_t, int); int APS5_VABI close_nid_postfix(int); +std::int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int, FileStat*); +int APS5_VABI sceKernelFstat(int, FileStat*); +int APS5_VABI 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(); } @@ -34,7 +57,7 @@ constexpr int CloseOnExec = 0x10000000; constexpr int NonBlocking = 0x20000000; constexpr int NoSignal = 0x20000; constexpr int Peek = 2; -constexpr int FirstSocket = 0x10000000; +constexpr int DescriptorSetSize = 1024; constexpr short In = 0x1; constexpr short Out = 0x4; constexpr short Hup = 0x10; @@ -70,10 +93,86 @@ 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] >= FirstSocket && pair[1] >= FirstSocket && pair[0] != pair[1]); + Require(pair[0] >= 0 && pair[0] < DescriptorSetSize && pair[1] >= 0 && pair[1] < DescriptorSetSize && pair[0] != pair[1]); Require(Exchange(pair[0], pair[1], "one way")); Require(Exchange(pair[1], pair[0], "other way")); Require(Unnamed(pair[0], getsockname_nid_postfix) && Unnamed(pair[1], getpeername_nid_postfix)); @@ -93,6 +192,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); @@ -110,10 +210,33 @@ int main() { 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); + Require(socketpair_nid_postfix(Unix, Stream, 0, pair) == 0); + FileStat status{}; + Require(fstat_nid_disambig1_nid_postfix(pair[0], &status) == 0); + Require(status.st_mode == 0140666 && status.st_size == 0 && status.st_blksize != 0 && status.st_nlink == 0); + Require(send_nid_postfix(pair[0], "hello", 5, 0) == 5 && Readable(pair[1])); + Require(sceKernelFstat(pair[1], &status) == 0 && status.st_mode == 0140666 && status.st_size == 5); + Require(shutdown_nid_postfix(pair[0], 1) == 0); + Require(fstat_nid_disambig1_nid_postfix(pair[0], &status) == 0 && status.st_mode == 0140444); +#ifndef _WIN32 + Require(shutdown_nid_postfix(pair[1], 0) == 0); + Require(fstat_nid_disambig1_nid_postfix(pair[1], &status) == 0 && status.st_mode == 0140666 && status.st_size == 5); + Require(recv_nid_postfix(pair[1], received, sizeof(received), 0) == 5); +#else + Require(recv_nid_postfix(pair[1], received, sizeof(received), 0) == 5); + Require(shutdown_nid_postfix(pair[1], 0) == 0); +#endif + Require(fstat_nid_disambig1_nid_postfix(pair[1], &status) == 0 && status.st_mode == 0140222 && status.st_size == 0); + Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0); + *__error_nid_postfix() = 0; + Require(fstat_nid_disambig1_nid_postfix(pair[0], &status) == -1 && *__error_nid_postfix() == 9); + Require(sceKernelFstat(pair[1], &status) == static_cast(0x80020009u)); + Require(socketpair_nid_postfix(Unix, Datagram, 0, pair) == 0); Require(fcntl_nid_postfix(pair[0], 3) == 2); Require(send_nid_postfix(pair[0], "a", 1, 0) == 1); @@ -134,8 +257,25 @@ int main() { Require(send_nid_postfix(pair[0], "bb", 2, 0) == 2); Require(recv_nid_postfix(pair[1], datagram, sizeof(datagram), 0) == 1); Require(recv_nid_postfix(pair[1], datagram, sizeof(datagram), 0) == 2); + Require(send_nid_postfix(pair[0], "record", 6, 0) == 6); + Require(send_nid_postfix(pair[0], "next", 4, 0) == 4); + char part[2]{}; + Iovec piece{part, sizeof(part)}; + Msghdr message{nullptr, 0, &piece, 1, nullptr, 0, -1}; + const auto truncated = recvmsg_nid_postfix(pair[1], &message, 0); + if (truncated != 2 || message.flags != 0x10) { + std::fprintf(stderr, "recvmsg of a 6-byte SOCK_SEQPACKET record into 2 bytes returned %lld with msg_flags 0x%x, expected 2 with MSG_TRUNC (0x10)\n", + static_cast(truncated), static_cast(message.flags)); + std::abort(); + } + Require(std::memcmp(part, "re", 2) == 0); + char whole[8]{}; + piece = {whole, sizeof(whole)}; + message.flags = -1; + 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 new file mode 100644 index 0000000000..5a8d48781b --- /dev/null +++ b/core/libs/tests/GuestSocketReadWrite.cpp @@ -0,0 +1,175 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#ifndef _WIN32 +#include +#endif + +struct PollDescriptor { + int descriptor; + short events; + short revents; +}; + +struct Iovec { + void* base; + std::size_t length; +}; + +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 fcntl_nid_postfix(int, int, ...); +int APS5_VABI poll_nid_postfix(PollDescriptor*, std::uint32_t, int); +int APS5_VABI close_nid_postfix(int); +int APS5_VABI sceKernelClose(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); +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 sceKernelRead(int, void*, std::size_t); +std::int64_t APS5_VABI sceKernelWrite(int, const void*, std::size_t); +std::int64_t APS5_VABI sceKernelReadv(int, const Iovec*, int); +std::int64_t APS5_VABI sceKernelWritev(int, const Iovec*, 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 preadv_nid_postfix(int, const Iovec*, int, std::int64_t); +std::int64_t APS5_VABI pwritev_nid_postfix(int, const Iovec*, int, std::int64_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); +std::int64_t APS5_VABI sceKernelPreadv(int, const Iovec*, int, std::int64_t); +std::int64_t APS5_VABI sceKernelPwritev(int, const Iovec*, int, std::int64_t); +int* APS5_VABI __error_nid_postfix(); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "socket read/write check failed at line %d, errno %d\n", line, *__error_nid_postfix()); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +constexpr short In = 0x1; +constexpr short Hup = 0x10; +constexpr int KernelAgain = static_cast(0x80020023u); +constexpr int KernelBadDescriptor = static_cast(0x80020009u); +constexpr int KernelIllegalSeek = static_cast(0x8002001du); + +static bool Readable(int descriptor, short accepted = In) { + PollDescriptor waited{descriptor, In, 0}; + return poll_nid_postfix(&waited, 1, 2000) == 1 && (waited.revents & accepted) != 0; +} + +int main() { +#ifndef _WIN32 + std::signal(SIGPIPE, SIG_IGN); +#endif + 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); + + const char request[] = "written to a socket"; + char received[32]{}; + Require(write_nid_postfix(client, request, sizeof(request)) == sizeof(request)); + Require(Readable(server) && read_nid_postfix(server, received, sizeof(received)) == sizeof(request)); + Require(std::strcmp(received, request) == 0); + + const char reply[] = "and back"; + std::memset(received, 0, sizeof(received)); + Require(_write_nid_postfix(server, reply, sizeof(reply)) == sizeof(reply)); + Require(Readable(client) && _read_nid_postfix(client, received, sizeof(received)) == sizeof(reply)); + Require(std::strcmp(received, reply) == 0); + + const char kernel[] = "through sceKernelWrite"; + std::memset(received, 0, sizeof(received)); + Require(sceKernelWrite(client, kernel, sizeof(kernel)) == sizeof(kernel)); + Require(Readable(server) && sceKernelRead(server, received, sizeof(received)) == sizeof(kernel)); + Require(std::strcmp(received, kernel) == 0); + + const char vectorFirst[] = "vector "; + const char vectorSecond[] = "write"; + Iovec vectorWrite[] = {{const_cast(vectorFirst), sizeof(vectorFirst) - 1}, + {const_cast(vectorSecond), sizeof(vectorSecond)}}; + Require(sceKernelWritev(client, vectorWrite, 2) == sizeof(vectorFirst) + sizeof(vectorSecond) - 1); + char vectorReadFirst[sizeof(vectorFirst) - 1]{}; + char vectorReadSecond[sizeof(vectorSecond)]{}; + Iovec vectorRead[] = {{vectorReadFirst, sizeof(vectorReadFirst)}, {vectorReadSecond, sizeof(vectorReadSecond)}}; + Require(Readable(server) && sceKernelReadv(server, vectorRead, 2) == sizeof(vectorFirst) + sizeof(vectorSecond) - 1); + Require(std::memcmp(vectorReadFirst, "vector ", sizeof(vectorReadFirst)) == 0 && std::strcmp(vectorReadSecond, "write") == 0); + + const char sceVectorFirst[] = "sceKernel"; + const char sceVectorSecond[] = "Writev"; + Iovec sceVectorWrite[] = {{const_cast(sceVectorFirst), sizeof(sceVectorFirst) - 1}, + {const_cast(sceVectorSecond), sizeof(sceVectorSecond)}}; + Require(sceKernelWritev(client, sceVectorWrite, 2) == sizeof(sceVectorFirst) + sizeof(sceVectorSecond) - 1); + char sceVectorReadFirst[sizeof(sceVectorFirst) - 1]{}; + char sceVectorReadSecond[sizeof(sceVectorSecond)]{}; + Iovec sceVectorRead[] = {{sceVectorReadFirst, sizeof(sceVectorReadFirst)}, {sceVectorReadSecond, sizeof(sceVectorReadSecond)}}; + Require(Readable(server) && sceKernelReadv(server, sceVectorRead, 2) == sizeof(sceVectorFirst) + sizeof(sceVectorSecond) - 1); + Require(std::memcmp(sceVectorReadFirst, "sceKernel", sizeof(sceVectorReadFirst)) == 0 && std::strcmp(sceVectorReadSecond, "Writev") == 0); + + const char positionedWrite[] = "positioned"; + Iovec positionedIov[] = {{const_cast(positionedWrite), sizeof(positionedWrite)}}; + *__error_nid_postfix() = 0; + Require(pread_nid_postfix(server, received, 1, 0) == -1 && *__error_nid_postfix() == 29); + Require(pwrite_nid_disambig1_nid_postfix(client, positionedWrite, 1, 0) == -1 && *__error_nid_postfix() == 29); + Require(preadv_nid_postfix(server, positionedIov, 1, 0) == -1 && *__error_nid_postfix() == 29); + Require(pwritev_nid_postfix(client, positionedIov, 1, 0) == -1 && *__error_nid_postfix() == 29); + Require(sceKernelPread(server, received, 1, 0) == KernelIllegalSeek); + Require(sceKernelPwrite(client, positionedWrite, 1, 0) == KernelIllegalSeek); + Require(sceKernelPreadv(server, positionedIov, 1, 0) == KernelIllegalSeek); + Require(sceKernelPwritev(client, positionedIov, 1, 0) == KernelIllegalSeek); + + Require(fcntl_nid_postfix(server, 4, 4) == 0); + *__error_nid_postfix() = 0; + Require(read_nid_postfix(server, received, sizeof(received)) == -1 && *__error_nid_postfix() == 35); + Require(sceKernelRead(server, received, sizeof(received)) == KernelAgain); + Iovec emptyRead[] = {{received, sizeof(received)}}; + Require(sceKernelReadv(server, emptyRead, 1) == KernelAgain); + + Require(read_nid_postfix(server, received, 0) == 0 && sceKernelRead(server, received, 0) == 0); + 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); + Require(read_nid_postfix(server, received, 0) == -1 && *__error_nid_postfix() == 9); + Require(write_nid_postfix(server, request, sizeof(request)) == -1 && *__error_nid_postfix() == 9); + Require(_write_nid_postfix(server, request, sizeof(request)) == -1 && *__error_nid_postfix() == 9); + Require(_read_nid_postfix(server, received, sizeof(received)) == -1 && *__error_nid_postfix() == 9); + Require(sceKernelRead(server, received, sizeof(received)) == KernelBadDescriptor); + Require(sceKernelWrite(server, request, sizeof(request)) == KernelBadDescriptor); + Require(sceKernelPread(server, received, 1, 0) == KernelBadDescriptor); + Require(sceKernelPwrite(server, request, 1, 0) == KernelBadDescriptor); + Require(pread_nid_postfix(server, received, 1, 0) == -1 && *__error_nid_postfix() == 9); + Require(pwrite_nid_disambig1_nid_postfix(server, request, 1, 0) == -1 && *__error_nid_postfix() == 9); + Require(preadv_nid_postfix(server, positionedIov, 1, 0) == -1 && *__error_nid_postfix() == 9); + Require(pwritev_nid_postfix(server, positionedIov, 1, 0) == -1 && *__error_nid_postfix() == 9); + Require(sceKernelClose(listener) == 0); + Require(sceKernelClose(listener) == KernelBadDescriptor); + Require(close_nid_postfix(listener) == -1 && *__error_nid_postfix() == 9); + return 0; +} 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/GuestSoftwareVersion.cpp b/core/libs/tests/GuestSoftwareVersion.cpp new file mode 100644 index 0000000000..d572e1722b --- /dev/null +++ b/core/libs/tests/GuestSoftwareVersion.cpp @@ -0,0 +1,64 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include +#include +#include +#include + +struct SceKernelProsperoSystemSwVersion { + std::uint64_t size; + char versionString[0x1C]; + std::uint32_t version; +}; + +static_assert(offsetof(SceKernelProsperoSystemSwVersion, versionString) == 0x08); +static_assert(offsetof(SceKernelProsperoSystemSwVersion, version) == 0x24); +static_assert(sizeof(SceKernelProsperoSystemSwVersion) == 0x28); + +extern "C" { +int* APS5_VABI __error_nid_postfix(); +int APS5_VABI sceKernelGetProsperoSystemSwVersion_nid_postfix(SceKernelProsperoSystemSwVersion*); +int APS5_VABI sceKernelGetAllowedSdkVersionOnSystem_nid_postfix(std::uint32_t*); +} + +static void Require(bool value) { if (!value) std::abort(); } + +static void CheckProsperoSystemSwVersion() { + *__error_nid_postfix() = 13; + Require(sceKernelGetProsperoSystemSwVersion_nid_postfix(nullptr) == SCE_KERNEL_ERROR_EFAULT); + const std::uint64_t sizes[] = {0, 0x18, 0x27, 0x30}; + for (const std::uint64_t size : sizes) { + SceKernelProsperoSystemSwVersion version{}; + std::memset(&version, 0xaa, sizeof(version)); + version.size = size; + Require(sceKernelGetProsperoSystemSwVersion_nid_postfix(&version) == SCE_KERNEL_ERROR_EINVAL); + Require(version.size == size && version.version == 0xaaaaaaaa && static_cast(version.versionString[0]) == 0xaa); + } + SceKernelProsperoSystemSwVersion version{}; + std::memset(&version, 0xaa, sizeof(version)); + version.size = sizeof(version); + Require(sceKernelGetProsperoSystemSwVersion_nid_postfix(&version) == 0); + Require(version.size == sizeof(version)); + Require(std::strcmp(version.versionString, "01.000.000") == 0); + for (std::size_t i = sizeof("01.000.000"); i < sizeof(version.versionString); ++i) Require(version.versionString[i] == 0); + Require(version.version == 0x01000000); + Require(*__error_nid_postfix() == 13); +} + +static void CheckAllowedSdkVersionOnSystem() { + *__error_nid_postfix() = 13; + Require(sceKernelGetAllowedSdkVersionOnSystem_nid_postfix(nullptr) == SCE_KERNEL_ERROR_EINVAL); + std::uint32_t allowed = 0xaaaaaaaa; + Require(sceKernelGetAllowedSdkVersionOnSystem_nid_postfix(&allowed) == 0); + SceKernelProsperoSystemSwVersion system{}; + system.size = sizeof(system); + Require(sceKernelGetProsperoSystemSwVersion_nid_postfix(&system) == 0); + Require(allowed == (system.version | 0xfff) && allowed >> 16 == system.version >> 16); + Require(*__error_nid_postfix() == 13); +} + +int main() { + CheckProsperoSystemSwVersion(); + CheckAllowedSdkVersionOnSystem(); + return 0; +} diff --git a/core/libs/tests/GuestSsl.cpp b/core/libs/tests/GuestSsl.cpp index 327e6b0b55..4becc68d8c 100644 --- a/core/libs/tests/GuestSsl.cpp +++ b/core/libs/tests/GuestSsl.cpp @@ -2,11 +2,28 @@ #include #include #include +#include extern "C" { int APS5_VABI sceSslInit_nid_postfix(std::size_t); +int APS5_VABI sceSslTerm_nid_postfix(int); int APS5_VABI sceSslGetCaCerts(int, void*); int APS5_VABI sceSslFreeCaCerts(int, void*); +int APS5_VABI sceSslGetCaList(int, void*); +int APS5_VABI sceSslFreeCaList(int, void*); +int APS5_VABI sceSslUnloadCert(int); +int APS5_VABI 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 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 { @@ -20,17 +37,33 @@ extern "C" int APS5_VABI sceSslGetMemoryPoolStats(int, SslMemoryPoolStats*); 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; +} + +static int APS5_VABI VerifyCallback(int, unsigned int, void**, int, void*) { return 0; } + struct SslCaCerts { void* certs; std::size_t num; void* pool; }; +struct SslCaList { + void** certs; + int num; +}; + int main() { constexpr int notFound = static_cast(0x8095F004); constexpr int invalidArg = static_cast(0x8095177A); int marker = 0; - const int context = sceSslInit_nid_postfix(0x10000); Require(context > 0); SslMemoryPoolStats stats{1, 1, 1, 1}; @@ -46,4 +79,95 @@ int main() { certs = {&marker, 3, &marker}; Require(sceSslFreeCaCerts(context, &certs) == 0); Require(certs.certs == nullptr && certs.num == 0 && certs.pool == nullptr); + + Require(sceSslGetCaList(context, nullptr) == invalidArg); + Require(sceSslFreeCaList(context, nullptr) == invalidArg); + + void* handle = ▮ + SslCaList list{&handle, 3}; + Require(sceSslGetCaList(context, &list) == notFound); + Require(list.certs == nullptr && list.num == 0); + Require(sceSslFreeCaList(context, &list) == 0); + Require(list.certs == nullptr && list.num == 0); + + SslCaList foreign{&handle, 1}; + Require(Throws([&] { sceSslFreeCaList(context, &foreign); })); + foreign = {nullptr, 1}; + Require(Throws([&] { sceSslFreeCaList(context, &foreign); })); + foreign = {&handle, 0}; + Require(Throws([&] { sceSslFreeCaList(context, &foreign); })); + + Require(sceSslUnloadCert(context) == 0); + Require(sceSslGetCaList(context, &list) == notFound); + Require(list.certs == nullptr && list.num == 0); + + constexpr int invalidId = static_cast(0x8095F006); + constexpr int invalidValue = static_cast(0x8095F007); + 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(Throws([&] { sceSslSetAlpn(connection, nullptr); })); + Require(sceSslGetAlpnSelected(connection) == nullptr); + char buffer[4]{}; + 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(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(Throws([&] { sceSslGetAlpnSelected(unknown); })); + 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(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(Throws([&] { sceSslGetAlpnSelected(connection); })); + Require(sceSslConnect(connection) == invalidId); + Require(sceSslClose(connection) == invalidId); + Require(sceSslCreateConnection(context, 0, "example.com") == invalidId); + Require(sceSslDeleteConnection(connection) == invalidId); + + Require(sceSslGetCaList(context, nullptr) == invalidArg); + Require(sceSslFreeCaList(context, nullptr) == invalidArg); + list = {&handle, 3}; + Require(Throws([&] { sceSslGetCaList(context, &list); })); + Require(Throws([&] { sceSslFreeCaList(context, &list); })); + Require(Throws([&] { sceSslUnloadCert(context); })); } diff --git a/core/libs/tests/GuestSslLoad.cpp b/core/libs/tests/GuestSslLoad.cpp new file mode 100644 index 0000000000..28b894ddb4 --- /dev/null +++ b/core/libs/tests/GuestSslLoad.cpp @@ -0,0 +1,350 @@ +#include "prx/libSceSsl/SslTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SslCertificateFixture.hpp" + +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceSslInit_nid_postfix(std::uint64_t); +int APS5_VABI sceSslTerm_nid_postfix(int); +int APS5_VABI sceSslGetMemoryPoolStats(int, SceSsl::MemoryPoolStats*); +int APS5_VABI sceSslLoadCert(int, int, SceSsl::Data**, SceSsl::Data*, SceSsl::Data*); +int APS5_VABI sceSslUnloadCert(int); +int APS5_VABI sceSslGetCaList(int, SceSsl::CaList*); +int APS5_VABI sceSslFreeCaList(int, SceSsl::CaList*); +int APS5_VABI sceSslGetCaCerts(int, SceSsl::CaCerts*); +int APS5_VABI sceSslFreeCaCerts(int, SceSsl::CaCerts*); +int APS5_VABI sceSslCreateConnection(int, int, const char*); +int APS5_VABI sceSslDeleteConnection(int); +int APS5_VABI sceSslSetVerifyCallback(int, int (APS5_VABI *)(int, unsigned int, void**, int, void*), void*); +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); +} + +template +void Throws(TAction action) { + bool caught = false; + try { action(); } catch (const std::exception&) { caught = true; } + Check(caught, "unsupported or malformed load was accepted"); +} + +SceSsl::Data Data(const char* text) { + return {reinterpret_cast(const_cast(text)), std::strlen(text)}; +} + +SceSsl::MemoryPoolStats Stats(int id) { + SceSsl::MemoryPoolStats stats{}; + Check(sceSslGetMemoryPoolStats(id, &stats) == 0, "stats failed"); + Check(stats.currentInuseSize <= stats.maxInuseSize && stats.maxInuseSize <= stats.poolSize && !stats.reserved, + "pool accounting bounds failed"); + return stats; +} + +void CheckRoot(const SceSsl::CaCerts& snapshot) { + Check(snapshot.num == 1 && snapshot.certs[0].size == RootDer.size(), "CA count or DER size differs"); + Check(std::memcmp(snapshot.certs[0].ptr, RootDer.data(), RootDer.size()) == 0, "CA bytes changed"); +} + +void Lifecycle() { + const int id = sceSslInit_nid_postfix(1024 * 1024); + std::string original = RootPem; + auto data = Data(original.c_str()); + auto* pointer = &data; + Check(sceSslLoadCert(id, 1, &pointer, nullptr, nullptr) == 0, "initial CA load failed"); + original.assign(original.size(), '?'); + Check(Stats(id).currentInuseSize > RootDer.size(), "loaded certificate is not retained/accounted"); + SceSsl::CaCerts bytes{}; + Check(sceSslGetCaCerts(id, &bytes) == 0, "encoded CA query failed"); + CheckRoot(bytes); + SceSsl::CaList handles{}; + Check(sceSslGetCaList(id, &handles) == 0 && handles.num == 1 && handles.certs[0], "CA handle absent"); + const int other = sceSslInit_nid_postfix(1024 * 1024); + SceSsl::CaList empty{}; + Check(sceSslGetCaList(other, &empty) == NotFound, "CA escaped its context"); + auto foreign = handles; + Throws([&] { sceSslFreeCaList(other, &foreign); }); + Check(sceSslUnloadCert(id) == 0, "unload failed"); + Check(sceSslGetCaList(id, &empty) == NotFound && !empty.certs && !empty.num, "unload retained active CA"); + CheckRoot(bytes); + Check(sceSslFreeCaList(id, &handles) == 0 && !handles.certs && !handles.num, "handle snapshot release failed"); + Check(sceSslFreeCaCerts(id, &bytes) == 0 && !bytes.certs && !bytes.pool && !bytes.num, "encoded snapshot release failed"); + Check(Stats(id).currentInuseSize == 0, "certificate allocation leaked after release"); + Throws([&] { sceSslFreeCaList(id, &foreign); }); + Check(sceSslTerm_nid_postfix(id) == 0 && sceSslTerm_nid_postfix(other) == 0, "term failed"); +} + +void ConnectionLifetime() { + const int id = sceSslInit_nid_postfix(1024 * 1024); + const int connection = sceSslCreateConnection(id, -1, "example.invalid"); + const int other = sceSslInit_nid_postfix(1024 * 1024); + Check(connection > 0 && connection != id && connection != other, "SSL handles overlap"); + auto data = Data(RootPem); + auto* pointer = &data; + Check(sceSslLoadCert(id, 1, &pointer, nullptr, nullptr) == 0, "CA load with a connection failed"); + Check(sceSslSetVerifyCallback(connection, nullptr, nullptr) == 0, "certificate load invalidated the connection"); + SceSsl::CaCerts bytes{}; + Check(sceSslGetCaCerts(id, &bytes) == 0, "CA query with a connection failed"); + CheckRoot(bytes); + SceSsl::CaCerts empty{}; + Check(sceSslGetCaCerts(other, &empty) == NotFound, "CA escaped into another context"); + SceSsl::MemoryPoolStats stats{}; + Check(sceSslGetMemoryPoolStats(connection, &stats) == static_cast(0x8095F006), "connection accepted as context"); + const int retained = sceSslCreateConnection(id, -1, "example.invalid"); + Check(retained > 0 && retained != connection && retained != other, "SSL connection handle was reused"); + Check(sceSslDeleteConnection(connection) == 0, "connection deletion failed"); + Check(sceSslUnloadCert(id) == 0, "credential unload failed with a retained connection"); + CheckRoot(bytes); + Check(sceSslSetVerifyCallback(retained, nullptr, nullptr) == 0, "credential unload invalidated the connection"); + Check(sceSslFreeCaCerts(id, &bytes) == 0 && Stats(id).currentInuseSize == 0, "CA snapshot leaked with a retained connection"); + Check(sceSslTerm_nid_postfix(id) == 0, "context termination failed with a retained connection"); + 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"); +} + +void AtomicFailures() { + const int id = sceSslInit_nid_postfix(1024 * 1024); + SceSsl::Data der{const_cast(RootDer.data()), RootDer.size()}; + auto* pointer = &der; + Check(sceSslLoadCert(id, 1, &pointer, nullptr, nullptr) == 0, "DER CA load failed"); + const auto before = Stats(id).currentInuseSize; + auto bad = Data("not a certificate"); + SceSsl::Data* mixed[]{&der, &bad}; + Throws([&] { sceSslLoadCert(id, 2, mixed, nullptr, nullptr); }); + Check(Stats(id).currentInuseSize == before, "failed chain changed live allocation"); + SceSsl::CaCerts bytes{}; + Check(sceSslGetCaCerts(id, &bytes) == 0, "CA vanished after failure"); + CheckRoot(bytes); + Check(sceSslFreeCaCerts(id, &bytes) == 0, "snapshot free failed"); + Throws([&] { sceSslLoadCert(id, -1, nullptr, nullptr, nullptr); }); + Throws([&] { sceSslLoadCert(id, 1, nullptr, nullptr, nullptr); }); + SceSsl::Data* absent = nullptr; + Throws([&] { sceSslLoadCert(id, 1, &absent, nullptr, nullptr); }); + Throws([&] { sceSslLoadCert(id, 0, nullptr, &der, nullptr); }); + Check(Stats(id).currentInuseSize == before, "argument failure changed retained CA"); + Check(sceSslTerm_nid_postfix(id) == 0, "term failed"); + Throws([&] { sceSslLoadCert(id, 1, &pointer, nullptr, nullptr); }); + const int tiny = sceSslInit_nid_postfix(32); + Throws([&] { sceSslLoadCert(tiny, 1, &pointer, nullptr, nullptr); }); + Check(Stats(tiny).currentInuseSize == 0, "failed small-pool load leaked"); + Check(sceSslTerm_nid_postfix(tiny) == 0, "small-pool term failed"); +} + +void ClientIdentity() { + const int id = sceSslInit_nid_postfix(1024 * 1024); + auto client = Data(ClientPem); + auto key = Data(ClientKey); + auto wrong = Data(RootKey); + Check(sceSslLoadCert(id, 0, nullptr, &client, &key) == 0, "client identity load failed"); + const auto before = Stats(id).currentInuseSize; + Check(before > 0, "identity not retained"); + Throws([&] { sceSslLoadCert(id, 0, nullptr, &client, &wrong); }); + Check(Stats(id).currentInuseSize == before, "mismatched key changed identity"); + Check(sceSslUnloadCert(id) == 0 && Stats(id).currentInuseSize == 0, "identity release failed"); + Check(sceSslTerm_nid_postfix(id) == 0, "identity term failed"); +} + +void CertificateReaders() { + const int id = sceSslInit_nid_postfix(1024 * 1024); + auto data = Data(RootPem); + auto* pointer = &data; + Check(sceSslLoadCert(id, 1, &pointer, nullptr, nullptr) == 0, "CA load failed"); + SceSsl::CaList handles{}; + Check(sceSslGetCaList(id, &handles) == 0 && handles.num == 1 && handles.certs[0], "CA handle absent"); + void* cert = handles.certs[0]; + + std::size_t querySize = 0; + Check(sceSslGetSerialNumber(id, cert, nullptr, &querySize) == 0 && querySize == 3, "query serial size failed"); + querySize = 0; + unsigned char shortBuf[2]{}; + Check(sceSslGetSerialNumber(id, cert, shortBuf, &querySize) == 0 && querySize == 3, "zero-len serial query failed"); + querySize = sizeof(shortBuf); + Check(sceSslGetSerialNumber(id, cert, shortBuf, &querySize) == static_cast(0x8095177a) && querySize == 3, + "short serial buffer accepted"); + unsigned char serialData[8]{}; + std::size_t serialLen = sizeof(serialData); + Check(sceSslGetSerialNumber(id, cert, serialData, &serialLen) == 0, "get serial failed"); + Check(serialLen == 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"); + Check(sceSslGetSerialNumber(id, cert, serialData, nullptr) == static_cast(0x8095177a), "null serial length accepted"); + Throws([&] { sceSslGetSerialNumber(id, nullptr, serialData, &serialLen); }); + void* fake = &serialLen; + Throws([&] { sceSslGetSerialNumber(id, fake, serialData, &serialLen); }); + Throws([&] { sceSslGetSerialNumber(9999, cert, serialData, &serialLen); }); + + void* subject = sceSslGetSubjectName(id, cert); + Check(subject != nullptr, "get subject failed"); + void* issuer = sceSslGetIssuerName(id, cert); + Check(issuer != nullptr, "get issuer failed"); + Throws([&] { sceSslGetSubjectName(id, nullptr); }); + Throws([&] { sceSslGetIssuerName(id, nullptr); }); + Throws([&] { sceSslGetSubjectName(id, fake); }); + Throws([&] { sceSslGetIssuerName(id, fake); }); + Throws([&] { sceSslGetSubjectName(9999, cert); }); + Throws([&] { sceSslGetIssuerName(9999, cert); }); + + Check(sceSslGetNameEntryCount(id, subject) == 3, "subject entry count mismatch"); + Check(sceSslGetNameEntryCount(id, issuer) == 3, "issuer entry count mismatch"); + Check(sceSslGetNameEntryCount(id, nullptr) == static_cast(0x8095177a), "null cert name count accepted"); + Throws([&] { sceSslGetNameEntryCount(id, fake); }); + Throws([&] { sceSslGetNameEntryCount(9999, subject); }); + + char oid[32]{}; + char val[64]{}; + std::size_t vlen = 0; + Check(sceSslGetNameEntryInfo(id, subject, 0, oid, sizeof(oid), val, sizeof(val), &vlen) == 0, "entry 0 info failed"); + Check(std::strcmp(oid, "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, nullptr, 0, smallVal, sizeof(smallVal), &fullLen) == 0, "truncated query failed"); + Check(std::strcmp(smallVal, "Any") == 0 && fullLen == 12, "buffer truncation mismatch"); + + Check(sceSslGetNameEntryInfo(id, subject, -1, oid, sizeof(oid), val, sizeof(val), &vlen) == static_cast(0x8095177a), "negative index accepted"); + Check(sceSslGetNameEntryInfo(id, subject, 3, oid, sizeof(oid), val, sizeof(val), &vlen) == static_cast(0x8095177a), "out of bounds index accepted"); + Check(sceSslGetNameEntryInfo(id, nullptr, 0, oid, sizeof(oid), val, sizeof(val), &vlen) == static_cast(0x8095177a), "null certName accepted"); + Throws([&] { sceSslGetNameEntryInfo(id, fake, 0, oid, sizeof(oid), val, sizeof(val), &vlen); }); + Throws([&] { sceSslGetNameEntryInfo(9999, subject, 0, oid, sizeof(oid), val, sizeof(val), &vlen); }); + + Check(sceSslFreeSslCertName(id, subject) == 0, "free subject name failed"); + Check(sceSslFreeSslCertName(id, issuer) == 0, "free issuer name failed"); + Check(sceSslFreeSslCertName(id, nullptr) == static_cast(0x8095177a), "null cert name free accepted"); + Throws([&] { sceSslFreeSslCertName(id, subject); }); + Throws([&] { sceSslFreeSslCertName(id, fake); }); + Throws([&] { sceSslFreeSslCertName(9999, subject); }); + + SceSsl::RtcTick begin{}; + Check(sceSslGetNotBefore(id, cert, &begin) == 0, "get not before failed"); + Check(begin.tick == 63927094442000000ULL, "not before tick mismatch"); + Check(sceSslGetNotBefore(id, cert, nullptr) == static_cast(0x8095177a), "null begin accepted"); + Throws([&] { sceSslGetNotBefore(id, nullptr, &begin); }); + Throws([&] { sceSslGetNotBefore(id, fake, &begin); }); + Throws([&] { sceSslGetNotBefore(9999, cert, &begin); }); + + SceSsl::RtcTick limit{}; + Check(sceSslGetNotAfter(id, cert, &limit) == 0, "get not after failed"); + Check(limit.tick == 64242454442000000ULL, "not after tick mismatch"); + Check(sceSslGetNotAfter(id, cert, nullptr) == static_cast(0x8095177a), "null limit accepted"); + Throws([&] { sceSslGetNotAfter(id, nullptr, &limit); }); + Throws([&] { sceSslGetNotAfter(id, fake, &limit); }); + Throws([&] { sceSslGetNotAfter(9999, cert, &limit); }); + + unsigned char md5[16]{}; + Check(sceSslGetFingerprint(id, cert, 0, md5) == 0, "get md5 fingerprint failed"); + constexpr unsigned char expectedMd5[16] = {0x71, 0x3c, 0xe5, 0x20, 0x88, 0x2b, 0x6b, 0xd0, 0x17, 0x85, 0x07, 0xde, 0xbd, 0xdc, 0xa9, 0x4f}; + Check(std::memcmp(md5, expectedMd5, sizeof(md5)) == 0, "md5 digest mismatch"); + + unsigned char sha1[20]{}; + Check(sceSslGetFingerprint(id, cert, 1, sha1) == 0, "get sha1 fingerprint failed"); + constexpr unsigned char expectedSha1[20] = {0x47, 0xe0, 0x89, 0xab, 0xb7, 0x02, 0xee, 0xb6, 0x2f, 0x8b, 0x5b, 0x7a, 0x13, 0xfe, 0x82, 0x6d, 0x2b, 0xe8, 0xee, 0xa7}; + Check(std::memcmp(sha1, expectedSha1, sizeof(sha1)) == 0, "sha1 digest mismatch"); + + unsigned char sha256[32]{}; + Check(sceSslGetFingerprint(id, cert, 3, sha256) == 0, "get sha256 fingerprint failed"); + constexpr unsigned char expectedSha256[32] = {0x3d, 0x7d, 0xac, 0xd3, 0x04, 0x90, 0x42, 0xe1, 0x57, 0x19, 0x87, 0x27, 0x54, 0x45, 0xae, 0xed, 0xc4, 0xea, 0xd6, 0xd4, 0x20, 0xa0, 0x7f, 0xc4, 0xf5, 0x26, 0xc1, 0xa0, 0x85, 0x87, 0x5a, 0x4c}; + Check(std::memcmp(sha256, expectedSha256, sizeof(sha256)) == 0, "sha256 digest mismatch"); + + Check(sceSslGetFingerprint(id, cert, 3, nullptr) == static_cast(0x8095177a), "null fingerprint buffer accepted"); + Check(sceSslGetFingerprint(id, cert, -1, sha256) == static_cast(0x8095177a), "negative hash type accepted"); + Check(sceSslGetFingerprint(id, cert, 99, sha256) == static_cast(0x8095177a), "invalid hash type accepted"); + Throws([&] { sceSslGetFingerprint(id, nullptr, 3, sha256); }); + Throws([&] { sceSslGetFingerprint(id, fake, 3, sha256); }); + Throws([&] { sceSslGetFingerprint(9999, cert, 3, sha256); }); + + 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-----\n") != nullptr, "pem header missing"); + Check(std::strstr(pemBuf.data(), "-----END CERTIFICATE-----\n") != nullptr, "pem footer missing"); + + char smallPem[8]{}; + std::size_t smallPemLen = sizeof(smallPem); + Throws([&] { sceSslGetPem(id, cert, smallPem, &smallPemLen); }); + Check(sceSslGetPem(id, cert, pemBuf.data(), nullptr) == static_cast(0x8095177a), "null pem len accepted"); + Throws([&] { sceSslGetPem(id, nullptr, pemBuf.data(), &writtenLen); }); + 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"); + Throws([&] { sceSslGetSerialNumber(id, cert, serialData, &serialLen); }); + Throws([&] { sceSslGetSubjectName(id, cert); }); + Throws([&] { sceSslGetIssuerName(id, cert); }); + Throws([&] { sceSslGetNotBefore(id, cert, &begin); }); + Throws([&] { sceSslGetNotAfter(id, cert, &limit); }); + Throws([&] { sceSslGetFingerprint(id, cert, 3, sha256); }); + Throws([&] { sceSslGetPem(id, cert, pemBuf.data(), &writtenLen); }); + + 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"); + Throws([&] { sceSslGetNameEntryCount(id2, subject2); }); + Throws([&] { sceSslFreeSslCertName(id2, subject2); }); +} + +} + +int main() { + try { + Lifecycle(); + 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(); + std::puts("SSL load lifecycle passed"); + } catch (const std::exception& error) { + std::fprintf(stderr, "SSL load lifecycle failed: %s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestSslPs5Readers.cpp b/core/libs/tests/GuestSslPs5Readers.cpp new file mode 100644 index 0000000000..caffe1cf3e --- /dev/null +++ b/core/libs/tests/GuestSslPs5Readers.cpp @@ -0,0 +1,103 @@ +#include "prx/libSceSsl/SslTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SslCertificateFixture.hpp" + +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceSslInit_nid_postfix(std::uint64_t); +int APS5_VABI sceSslTerm_nid_postfix(int); +int APS5_VABI 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) == static_cast(0x8095177a) && length == 3, "short serial buffer did not report its required size"); + Check(std::all_of(serial.begin(), serial.end(), [](auto byte) { return byte == 0xa5; }), "short serial buffer was modified"); + 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); }); + Throws([&] { sceSslFreeSslCertName(other, subject); }); + 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"); + + length = SIZE_MAX; + Check(sceSslGetPem(context, certificate, nullptr, &length) == 0 && length == std::strlen(RootPem) + 1, "PS5 PEM size query failed"); + std::vector pem(length + 2, static_cast(0x5a)); + Check(sceSslGetPem(context, certificate, pem.data(), &length) == 0 && length == std::strlen(RootPem) + 1, "PS5 PEM copy failed"); + Check(std::strcmp(pem.data(), RootPem) == 0 && pem[length] == 0x5a && pem[length + 1] == 0x5a, "PEM copy changed bytes or guards"); + std::array small{'x', 'x', 'x', 'x'}; + length = 2; + Throws([&] { sceSslGetPem(context, certificate, small.data(), &length); }); + Check(std::all_of(small.begin(), small.end(), [](char byte) { return byte == 'x'; }), "short PEM buffer was modified"); + 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); }); + Check(sceSslGetNameEntryCount(context, subject) == 3, "owned name depended on freed certificate storage"); + Check(sceSslFreeSslCertName(context, subject) == 0, "name free failed"); + Throws([&] { sceSslGetNameEntryCount(context, subject); }); + Check(sceSslTerm_nid_postfix(context) == 0, "context term failed"); + Throws([&] { sceSslGetNameEntryCount(context, issuer); }); + 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..93fcb1b901 --- /dev/null +++ b/core/libs/tests/GuestSslTransport.cpp @@ -0,0 +1,578 @@ +#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 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(sceSslWrite(connection, request, 4, 0) == 4, "write 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(sceSslRead(connection, buffer.data(), buffer.size(), 0) == 2 && std::memcmp(buffer.data(), "ng", 2) == 0, "a full read 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 99f3445362..c37c580e6b 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -1,19 +1,30 @@ #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 -#ifndef _WIN32 +#include +#include +#include +#ifdef _WIN32 +#include +#else #include #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*); @@ -27,26 +38,47 @@ 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*); int APS5_VABI fputc_nid_postfix(int, FileStream*); +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*); 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*); int APS5_VABI setvbuf_nid_postfix(FileStream*, char*, int, std::size_t); void APS5_VABI setbuf_nid_postfix(FileStream*, char*); FileStream* APS5_VABI fdopen_nid_postfix(int, const char*); int APS5_VABI fclose_nid_postfix(FileStream*); +int APS5_VABI fflush_nid_postfix(FileStream*); int APS5_VABI _Getmbcurmax_nid_postfix(); int APS5_VABI ___mb_cur_max_nid_postfix(); +void APS5_VABI flockfile_nid_postfix(FileStream*); +void APS5_VABI funlockfile_nid_postfix(FileStream*); +int APS5_VABI ftrylockfile_nid_postfix(FileStream*); } static void Require(bool value) { if (!value) std::abort(); } static int APS5_VABI WriteFormatted(FileStream* stream, const char* format, ...) { @@ -65,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; @@ -89,13 +137,255 @@ static bool CheckFileBytes(const std::string& filename, const std::string& expec return false; } +static void CheckUnlockedStreams() { + FileStream stream(std::tmpfile()); + auto& guest = stream.GuestState(); + const auto originalFlags = guest.flags; + for (const int value : {0, 0x7f, 0x80, 0xff, 0x1ab, -2}) { + guest.readRemaining = 13; + guest.writeRemaining = 19; + Require(putc_unlocked_nid_postfix(value, &stream) == static_cast(value)); + Require(guest.readRemaining == 0 && guest.writeRemaining == 0 && guest.flags == originalFlags); + } + std::rewind(stream.GetHandle()); + for (const int value : {0, 0x7f, 0x80, 0xff, 0x1ab, -2}) { + guest.readRemaining = 13; + guest.writeRemaining = 19; + Require(getc_unlocked_nid_postfix(&stream) == static_cast(value)); + Require(guest.readRemaining == 0 && guest.writeRemaining == 0 && guest.flags == originalFlags); + } + Require(getc_unlocked_nid_postfix(&stream) == EOF && (guest.flags & 0x20)); + std::clearerr(stream.GetHandle()); + Require(std::ungetc('Q', stream.GetHandle()) == 'Q'); + Require(getc_unlocked_nid_postfix(&stream) == 'Q' && !(guest.flags & 0x20)); + std::rewind(stream.GetHandle()); + stream.SetError(); + Require(putc_unlocked_nid_postfix('R', &stream) == 'R' && (guest.flags & 0x40)); + clearerr_nid_postfix(&stream); + Require(!(guest.flags & 0x40)); + stream.Close(); + + const auto filename = "anyps5-unlocked-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + { 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 = [&] { + int result = 0; + std::thread other([&] { + result = ftrylockfile_nid_postfix(&stream); + if (result == 0) funlockfile_nid_postfix(&stream); + }); + other.join(); + return result; + }; + Require(ftrylockfile_nid_postfix(&stream) == 0); + Require(ftrylockfile_nid_postfix(&stream) == 0); + Require(tryFromOtherThread() == -1); + funlockfile_nid_postfix(&stream); + Require(tryFromOtherThread() == -1); + std::atomic written = false; + std::thread writer([&] { + Require(fputc_nid_postfix('B', &stream) == 'B'); + written = true; + }); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(!written && fputc_nid_postfix('A', &stream) == 'A'); + funlockfile_nid_postfix(&stream); + writer.join(); + Require(written && tryFromOtherThread() == 0); + std::atomic held = false, release = false; + std::thread owner([&] { + flockfile_nid_postfix(&stream); + held = true; + while (!release) std::this_thread::yield(); + funlockfile_nid_postfix(&stream); + }); + while (!held) std::this_thread::yield(); + const int contended = ftrylockfile_nid_postfix(&stream); + release = true; + owner.join(); + Require(contended == -1 && ftrylockfile_nid_postfix(&stream) == 0); + funlockfile_nid_postfix(&stream); + Require(tryFromOtherThread() == 0); + Require(fseeko_nid_postfix(&stream, 0, SEEK_SET) == 0); + Require(fgetc_nid_postfix(&stream) == 'A' && fgetc_nid_postfix(&stream) == 'B'); + stream.Close(); + bool threw = false; + try { ftrylockfile_nid_postfix(&stream); } catch (const std::runtime_error&) { threw = true; } + Require(threw); + threw = false; + try { ftrylockfile_nid_postfix(nullptr); } catch (const std::invalid_argument&) { threw = true; } + Require(threw); +} + +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()); + std::atomic held = false, release = false, entered = false, done = false; + int result = EOF; + std::thread owner([&] { + flockfile_nid_postfix(&stream); + held = true; + while (!release) std::this_thread::yield(); + funlockfile_nid_postfix(&stream); + }); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + while (!held && std::chrono::steady_clock::now() < deadline) std::this_thread::yield(); + if (!held) { + release = true; + owner.join(); + Require(false); + } + std::thread caller([&] { + entered = true; + 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); + while (!entered && std::chrono::steady_clock::now() < enteredDeadline) std::this_thread::yield(); + if (!entered) { + release = true; + owner.join(); + caller.join(); + Require(false); + } + const auto completionDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + while (!done && std::chrono::steady_clock::now() < completionDeadline) std::this_thread::yield(); + const bool completedWhileHeld = done; + release = true; + owner.join(); + caller.join(); + Require(completedWhileHeld && result == (query ? 0 : write ? 'B' : 'A')); + stream.Close(); +} + +static void CheckFgetsSize() { + FileStream stream(std::tmpfile()); + Require(fwrite_nid_postfix("line\n", 1, 5, &stream) == 5); + rewind_nid_postfix(&stream); + for (const int size : {0, -1, std::numeric_limits::min()}) { + char buffer[8]; + std::memset(buffer, '!', sizeof(buffer)); + *__error_nid_postfix() = 0; + Require(fgets_nid_postfix(buffer, size, &stream) == nullptr); + Require(*__error_nid_postfix() == 22 && ferror_nid_postfix(&stream) != 0 && feof_nid_postfix(&stream) == 0); + Require(buffer[0] == '!' && ftello_nid_postfix(&stream) == 0); + Require(ferror_nid_postfix(&stream) != 0); + clearerr_nid_postfix(&stream); + Require(ferror_nid_postfix(&stream) == 0); + } + char line[8]{}; + Require(fgets_nid_postfix(line, sizeof(line), &stream) == line && std::strcmp(line, "line\n") == 0); + Require(ferror_nid_postfix(&stream) == 0); + stream.Close(); +} + +static void CheckRewind() { + FileStream stream(std::tmpfile()); + Require(fwrite_nid_postfix("ab", 1, 2, &stream) == 2); + *__error_nid_postfix() = 7; + rewind_nid_postfix(&stream); + Require(*__error_nid_postfix() == 7 && ftello_nid_postfix(&stream) == 0); + Require(fgetc_nid_postfix(&stream) == 'a' && ungetc_nid_postfix('z', &stream) == 'z'); + rewind_nid_postfix(&stream); + Require(fgetc_nid_postfix(&stream) == 'a' && fgetc_nid_postfix(&stream) == 'b' && fgetc_nid_postfix(&stream) == EOF); + Require(feof_nid_postfix(&stream) != 0); + stream.SetError(); + Require(ferror_nid_postfix(&stream) != 0); + rewind_nid_postfix(&stream); + Require(feof_nid_postfix(&stream) == 0 && ferror_nid_postfix(&stream) == 0 && fgetc_nid_postfix(&stream) == 'a'); + stream.Close(); + int pipeEnds[2]; +#ifdef _WIN32 + Require(_pipe(pipeEnds, 4096, _O_BINARY) == 0 && _write(pipeEnds[1], "pq", 2) == 2 && _close(pipeEnds[1]) == 0); + FileStream pipe(_fdopen(pipeEnds[0], "rb")); +#else + Require(::pipe(pipeEnds) == 0); + Require(::write(pipeEnds[1], "pq", 2) == 2); + ::close(pipeEnds[1]); + FileStream pipe(::fdopen(pipeEnds[0], "r")); +#endif + Require(fgetc_nid_postfix(&pipe) == 'p'); + for (const int origin : {SEEK_SET, SEEK_CUR, SEEK_END}) { + *__error_nid_postfix() = 0; + Require(fseeko_nid_postfix(&pipe, 0, origin) == -1 && *__error_nid_postfix() == 29); + } + *__error_nid_postfix() = 0; + Require(ftello_nid_postfix(&pipe) == -1 && *__error_nid_postfix() == 29 && ferror_nid_postfix(&pipe) == 0); + Require(fgetc_nid_postfix(&pipe) == 'q' && fgetc_nid_postfix(&pipe) == EOF && feof_nid_postfix(&pipe) != 0); + *__error_nid_postfix() = 0; + rewind_nid_postfix(&pipe); + Require(*__error_nid_postfix() == 29 && feof_nid_postfix(&pipe) == 0 && ferror_nid_postfix(&pipe) == 0); + pipe.Close(); +} + +static void CheckFlushWhileReading() { + const auto filename = "anyps5-flush-read-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + { std::ofstream seed(filename, std::ios::binary); seed << "abcdef\n"; Require(seed.good()); } + for (const char* mode : {"r", "r+"}) { + FileStream* stream = fopen_nid_postfix(filename.c_str(), mode); + Require(stream != nullptr); + Require(fgetc_nid_postfix(stream) == 'a' && ungetc_nid_postfix('Z', stream) == 'Z'); + Require(fflush_nid_postfix(stream) == 0); + char rest[16]{}; + const auto count = fread_nid_postfix(rest, 1, sizeof(rest) - 1, stream); + if (count != 7 || std::strcmp(rest, "Zbcdef\n") != 0) { + std::string shown; + for (std::size_t index = 0; index < count; ++index) shown += rest[index] == '\n' ? std::string("\\n") : std::string(1, rest[index]); + std::fprintf(stderr, "fflush of a \"%s\" stream that is reading changed the data read next: \"%s\" (%zu bytes), expected \"Zbcdef\\n\" (7 bytes)\n", + mode, shown.c_str(), count); + std::abort(); + } + Require(fclose_nid_postfix(stream) == 0); + } + std::filesystem::remove(filename); +} + static bool CheckBinaryModes() { const auto directory = "anyps5-byte-stream-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); Require(std::filesystem::create_directory(directory)); const auto filename = directory + "/bytes"; const std::string original("A\r\n\x1a" "B\0C", 7); const std::string written("D\n\x1a" "E\0F", 6); - const char* modes[] = {"r", "r+", "w", "w+", "a", "a+", "rb", "rb+", "r+b", "wb", "wb+", "w+b", "ab", "ab+", "a+b"}; + const char* modes[] = {"r", "r+", "w", "w+", "a", "a+", "rb", "rb+", "r+b", "wb", "wb+", "w+b", "ab", "ab+", "a+b", "rt", "r+t", "wt", "w+e", "ae", "rbv"}; bool correct = true; for (const auto* mode : modes) { for (const bool reopen : {false, true}) { @@ -104,6 +394,12 @@ static bool CheckBinaryModes() { FileStream* stream = reopen ? freopen_nid_postfix(filename.c_str(), mode, &redirected) : fopen_nid_postfix(filename.c_str(), mode); Require(stream != nullptr); const bool update = std::strchr(mode, '+') != nullptr; + if (*mode == 'a') { + const bool atEnd = ftello_nid_postfix(stream) == static_cast(original.size()) && + (!update || fgetc_nid_postfix(stream) == EOF); + if (!atEnd) std::fprintf(stderr, "%s %s: the append stream does not start at the end of the file\n", reopen ? "freopen" : "fopen", mode); + correct &= atEnd; + } if (*mode == 'r' || (*mode == 'a' && update)) { Require(fseeko_nid_postfix(stream, 0, SEEK_SET) == 0); char bytes[16]{}; @@ -124,13 +420,211 @@ static bool CheckBinaryModes() { correct &= CheckFileBytes(filename, expected, mode, reopen); } } + for (const auto* mode : {"", "q", "tr", "rx", "rbx"}) { + FileStream redirected(std::tmpfile()); + Require(fopen_nid_postfix(filename.c_str(), mode) == nullptr && *__error_nid_postfix() == 22); + Require(freopen_nid_postfix(filename.c_str(), mode, &redirected) == nullptr && *__error_nid_postfix() == 22); + } + Require(fopen_nid_postfix(filename.c_str(), "wx") == nullptr && *__error_nid_postfix() == 17); + FileStream* readOnly = fopen_nid_postfix(filename.c_str(), "rt+"); + Require(readOnly != nullptr && fputc_nid_postfix('X', readOnly) == EOF); + Require(fclose_nid_postfix(readOnly) == 0); + Require(std::filesystem::remove(filename)); + FileStream* created = fopen_nid_postfix(filename.c_str(), "wx"); + Require(created != nullptr && fputc_nid_postfix('Y', created) == 'Y'); + Require(fclose_nid_postfix(created) == 0); + correct &= CheckFileBytes(filename, "Y", "wx", false); Require(std::filesystem::remove(filename)); Require(std::filesystem::remove(directory)); return correct; } -int main() { +static int DuplicateDescriptor(std::FILE* stream) { +#ifdef _WIN32 + return _dup(_fileno(stream)); +#else + return ::dup(::fileno(stream)); +#endif +} + +static void CheckDescriptorModes(std::FILE* original) { + for (const char* mode : {"rt", "r+t", "re", "rbe", "rw", "r+x"}) { + const int descriptor = DuplicateDescriptor(original); + Require(descriptor >= 0); + auto* wrapped = fdopen_nid_postfix(descriptor, mode); + Require(wrapped != nullptr && fseek_nid_postfix(wrapped, 0, SEEK_SET) == 0); + char contents[32]{}; + Require(fgets_nid_postfix(contents, sizeof(contents), wrapped) == contents && std::strcmp(contents, "retained") == 0); + Require(fclose_nid_postfix(wrapped) == 0); + } + const int descriptor = DuplicateDescriptor(original); + Require(descriptor >= 0); + *__error_nid_postfix() = 0; + Require(fdopen_nid_postfix(descriptor, "rx") == nullptr && *__error_nid_postfix() == 22); +#ifdef _WIN32 + Require(_close(descriptor) == 0); +#else + Require(::close(descriptor) == 0); +#endif +} + +static void CheckDescriptorAppend(std::FILE* original) { + std::string expected = "retained"; + for (const char* mode : {"a", "ab", "a+", "ae"}) { + const int descriptor = DuplicateDescriptor(original); +#ifdef _WIN32 + Require(descriptor >= 0 && _lseek(descriptor, 2, SEEK_SET) == 2); +#else + Require(descriptor >= 0 && ::lseek(descriptor, 2, SEEK_SET) == 2); +#endif + auto* wrapped = fdopen_nid_postfix(descriptor, mode); + Require(wrapped != nullptr && fileno_nid_postfix(wrapped) == descriptor); + Require(fputc_nid_postfix('+', wrapped) == '+' && fflush_nid_postfix(wrapped) == 0); + Require(fseek_nid_postfix(wrapped, 0, SEEK_SET) == 0 && fputc_nid_postfix('+', wrapped) == '+'); + Require(fclose_nid_postfix(wrapped) == 0); + expected += "++"; + const int reader = DuplicateDescriptor(original); + Require(reader >= 0); + auto* written = fdopen_nid_postfix(reader, "r"); + Require(written != nullptr && fseek_nid_postfix(written, 0, SEEK_SET) == 0); + char contents[32]{}; + Require(fgets_nid_postfix(contents, sizeof(contents), written) == contents && expected == contents); + Require(fclose_nid_postfix(written) == 0); + } +} + +static void CheckDescriptorAccessModes() { + const auto filename = "anyps5-fdopen-modes-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + for (const int access : {0, 1, 2}) { + for (const char* mode : {"r", "w", "a", "r+", "w+", "a+"}) { + { std::ofstream output(filename); output << "retained"; } +#ifdef _WIN32 + const int descriptor = _open(filename.c_str(), access | _O_BINARY); +#else + const int descriptor = ::open(filename.c_str(), access); +#endif + Require(descriptor >= 0); + const int requested = std::strchr(mode, '+') ? 2 : *mode == 'r' ? 0 : 1; + const bool incompatible = access != 2 && access != requested; +#ifdef _WIN32 + const auto originalHandle = _get_osfhandle(descriptor); + if (incompatible) Require(_lseek(descriptor, 2, SEEK_SET) == 2); +#else + const int originalFlags = ::fcntl(descriptor, F_GETFL); + Require(originalFlags >= 0); + if (incompatible) Require(::lseek(descriptor, 2, SEEK_SET) == 2); +#endif + *__error_nid_postfix() = 0; + auto* stream = fdopen_nid_postfix(descriptor, mode); + if (incompatible) { + if (stream != nullptr || *__error_nid_postfix() != 22) + std::fprintf(stderr, "fdopen access=%d mode=%s returned %p with errno=%d\n", access, mode, + static_cast(stream), *__error_nid_postfix()); + Require(stream == nullptr && *__error_nid_postfix() == 22); +#ifdef _WIN32 + Require(_get_osfhandle(descriptor) == originalHandle && _lseek(descriptor, 0, SEEK_CUR) == 2); +#else + Require(::fcntl(descriptor, F_GETFL) == originalFlags && ::lseek(descriptor, 0, SEEK_CUR) == 2); +#endif + Require(CheckFileBytes(filename, "retained", mode, false)); + const bool probeAppend = access == 1 && *mode == 'a'; +#ifdef _WIN32 + if (probeAppend) Require(_write(descriptor, "X", 1) == 1 && _lseek(descriptor, 0, SEEK_CUR) == 3); + Require(_close(descriptor) == 0); +#else + if (probeAppend) Require(::write(descriptor, "X", 1) == 1 && ::lseek(descriptor, 0, SEEK_CUR) == 3); + Require(::close(descriptor) == 0); +#endif + Require(CheckFileBytes(filename, probeAppend ? "reXained" : "retained", mode, false)); + } else { + Require(stream != nullptr); + if (requested == 0) Require(fgetc_nid_postfix(stream) == 'r'); + else Require(fputc_nid_postfix('X', stream) == 'X'); + Require(fclose_nid_postfix(stream) == 0); + } + } + } +#ifdef _WIN32 + const int closed = _open(filename.c_str(), _O_RDONLY | _O_BINARY); + Require(closed >= 0 && _close(closed) == 0); +#else + const int closed = ::open(filename.c_str(), O_RDONLY); + Require(closed >= 0 && ::close(closed) == 0); +#endif + *__error_nid_postfix() = 0; + Require(fdopen_nid_postfix(closed, "r") == nullptr && *__error_nid_postfix() == 9); + Require(std::filesystem::remove(filename)); +} + +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(); Require(fdopen_nid_postfix(-1, "rb") == nullptr && *__error_nid_postfix() == 9); Require(fdopen_nid_postfix(0, nullptr) == nullptr && *__error_nid_postfix() == 22); Require(fdopen_nid_postfix(0, "invalid") == nullptr && *__error_nid_postfix() == 22); @@ -156,6 +650,8 @@ int main() { #else Require(::close(descriptor) == -1); #endif + CheckDescriptorModes(original); + CheckDescriptorAppend(original); Require(std::fclose(original) == 0); char stringOutput[256]; std::memset(stringOutput, '!', sizeof(stringOutput)); @@ -175,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()); @@ -186,9 +701,11 @@ int main() { Require(setvbuf_nid_postfix(&fullyBuffered, nullptr, 0, 0) == 0); Require(setvbuf_nid_postfix(&stream, nullptr, 2, 0) == 0); Require(fputc_nid_postfix('A', &stream) == 'A'); + Require(putc_unlocked_nid_postfix('C', &stream) == 'C'); Require(--guest.writeRemaining < 0 && __swbuf_nid_postfix('\n', &stream) == '\n'); std::rewind(stream.GetHandle()); Require(--guest.readRemaining < 0 && __srget_nid_postfix(&stream) == 'A'); + Require(getc_unlocked_nid_postfix(&stream) == 'C'); Require(ungetc_nid_postfix('B', &stream) == 'B'); char text[8]{}; Require(fgets_nid_postfix(text, sizeof(text), &stream) == text); @@ -209,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", @@ -238,6 +811,24 @@ int main() { Require(feof_nid_postfix(&scanned) && (scanned.GuestState().flags & 0x20)); scanned.Close(); + FileStream scanBinary(std::tmpfile()); + Require(std::fputs("0b101", scanBinary.GetHandle()) >= 0); + std::rewind(scanBinary.GetHandle()); + int binaryValue = -1; + Require(fscanf_nid_postfix(&scanBinary, "%i", &binaryValue) == 1 && binaryValue == 0); + Require(fgetc_nid_postfix(&scanBinary) == 'b'); + scanBinary.Close(); + +#ifdef _WIN32 + FileStream scanSuppressed(std::tmpfile()); + Require(std::fputs("12", scanSuppressed.GetHandle()) >= 0); + std::rewind(scanSuppressed.GetHandle()); + int suppressedTarget = 321; + Require(fscanf_nid_postfix(&scanSuppressed, "%*d%d", &suppressedTarget) == 0 && suppressedTarget == 321); + Require(fscanf_nid_postfix(&scanSuppressed, "%*d") == EOF); + scanSuppressed.Close(); +#endif + FileStream scanMany(std::tmpfile()); Require(std::fputs("7 1 2 3 4 5 6 7 8 4294967297 -4294967298 4294967299 abc %!", scanMany.GetHandle()) >= 0); std::rewind(scanMany.GetHandle()); @@ -297,5 +888,57 @@ int main() { Require(freopen_nid_postfix(filename.c_str(), "rb", &failed) == nullptr); Require(*__error_nid_postfix() == 2); Require(failed.GuestState().flags == 0 && failed.GuestState().descriptor == -1); - return CheckBinaryModes() ? 0 : 1; + FileStream locked(std::tmpfile()); + flockfile_nid_postfix(&locked); + flockfile_nid_postfix(&locked); + std::atomic lockedWrite = false; + std::thread writer([&] { + Require(fputc_nid_postfix('B', &locked) == 'B'); + lockedWrite = true; + }); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(!lockedWrite && fputc_nid_postfix('A', &locked) == 'A'); + funlockfile_nid_postfix(&locked); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(!lockedWrite); + funlockfile_nid_postfix(&locked); + writer.join(); + Require(lockedWrite && fseeko_nid_postfix(&locked, 0, SEEK_SET) == 0); + Require(fgetc_nid_postfix(&locked) == 'A' && fgetc_nid_postfix(&locked) == 'B'); + locked.Close(); + CheckRewind(); + CheckFgetsSize(); + CheckUnlockedStreams(); + CheckUnlockedStreamLock(false); + CheckUnlockedStreamLock(true); + CheckUnlockedStreamLock(false, feof_unlocked_nid_postfix); + CheckUnlockedStreamLock(false, ferror_unlocked_nid_postfix); + CheckTryLock(); + 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 new file mode 100644 index 0000000000..2019801dc0 --- /dev/null +++ b/core/libs/tests/GuestStatvfs.cpp @@ -0,0 +1,164 @@ +#include "prx/libc/include/General.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#include +#include +#include +#endif + +extern "C" int APS5_VABI statvfs_nid_postfix(const char*, void*); +extern "C" int* APS5_VABI __error_nid_postfix(); + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "statvfs check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static void RequireVolume(const char* path, const std::filesystem::path& native) { + std::array output; + output.fill(0xA5A5A5A5A5A5A5A5ull); + *__error_nid_postfix() = 12345; + Require(statvfs_nid_postfix(path, output.data() + 1) == 0); + Require(*__error_nid_postfix() == 12345); + Require(output.front() == 0xA5A5A5A5A5A5A5A5ull && output.back() == output.front()); + const auto* fields = output.data() + 1; + Require(fields[6] > 0 && fields[8] > 0); + Require(fields[2] == std::filesystem::space(native).capacity / fields[8]); + Require(fields[0] <= fields[1] && fields[1] <= fields[2]); + Require((fields[7] & ~std::uint64_t{3}) == 0); + Require(fields[9] == 0 && fields[10] >= 255); +} + +static void RequireError(const char* path, int error) { + std::array output; + output.fill(0x5A5A5A5A5A5A5A5Aull); + const auto saved = output; + *__error_nid_postfix() = 0; + const int result = statvfs_nid_postfix(path, output.data()); + if (result != -1) + std::fprintf(stderr, "path %s: expected failure, got %d\n", path ? path : "(null)", result); + Require(result == -1); + if (*__error_nid_postfix() != error) + std::fprintf(stderr, "path %s: expected errno %d, got %d\n", path ? path : "(null)", error, *__error_nid_postfix()); + Require(*__error_nid_postfix() == error && output == saved); +} + +static std::size_t OpenHandles() { +#ifdef _WIN32 + DWORD count = 0; + Require(GetProcessHandleCount(GetCurrentProcess(), &count) != 0); + return count; +#else +#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 +} + +int main() { + const auto root = std::filesystem::temp_directory_path() / + ("anyps5-statvfs-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + const auto file = root / "file.txt"; + { + std::ofstream stream(file, std::ios::binary); + stream << "unchanged"; + Require(static_cast(stream)); + } + AddPathAlias_nid_no_patch("/anyps5-statvfs-test", root.string().c_str()); + RequireVolume("/anyps5-statvfs-test", root); + RequireVolume("/anyps5-statvfs-test/file.txt", file); + RequireVolume("/anyps5-statvfs-test/", root); + RequireError(nullptr, 14); + RequireError("", 2); + RequireError("/anyps5-statvfs-test/missing", 2); + RequireError("/anyps5-statvfs-test/file.txt/child", 20); + RequireError("/anyps5-statvfs-test/file.txt/", 20); + RequireError("/anyps5-statvfs-test/file.txt/.", 20); + RequireError("/anyps5-statvfs-test/file.txt/../file.txt", 20); + RequireError("/anyps5-statvfs-test/missing/../file.txt", 2); + BlockPathAlias_nid_no_patch("/anyps5-statvfs-unavailable"); + RequireError("/anyps5-statvfs-unavailable", 2); + AddPathAlias_nid_no_patch("/anyps5-statvfs-unavailable", root.string().c_str()); + RemovePathAlias_nid_no_patch("/anyps5-statvfs-unavailable"); + Require(statvfs_nid_postfix("/anyps5-statvfs-test/file.txt", nullptr) == -1); + Require(*__error_nid_postfix() == 14); + 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"); + { + std::ofstream stream(unicodeFile); + stream << "unicode path"; + Require(static_cast(stream)); + } + RequireVolume("/anyps5-statvfs-test/\xE6\x97\xA5.txt", unicodeFile); + Require(std::filesystem::remove(unicodeFile)); +#ifdef _WIN32 + const HANDLE exclusive = CreateFileW(file.c_str(), GENERIC_READ | GENERIC_WRITE, 0, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + Require(exclusive != INVALID_HANDLE_VALUE); + RequireVolume("/anyps5-statvfs-test/file.txt", file); + Require(CloseHandle(exclusive) != 0); +#else + Require(::chmod(file.c_str(), 0) == 0); + RequireVolume("/anyps5-statvfs-test/file.txt", file); + Require(::chmod(file.c_str(), 0600) == 0); + const auto fifo = root / "fifo"; + Require(::mkfifo(fifo.c_str(), 0600) == 0); + RequireVolume("/anyps5-statvfs-test/fifo", fifo); + const auto socketPath = root / "socket"; + const int socket = ::socket(AF_UNIX, SOCK_STREAM, 0); + Require(socket >= 0); + sockaddr_un address{}; + address.sun_family = AF_UNIX; + const auto name = socketPath.string(); + Require(name.size() < sizeof(address.sun_path)); + std::memcpy(address.sun_path, name.c_str(), name.size() + 1); + Require(::bind(socket, reinterpret_cast(&address), sizeof(address)) == 0); + RequireVolume("/anyps5-statvfs-test/socket", socketPath); + Require(::close(socket) == 0); + Require(std::filesystem::remove(socketPath)); + Require(std::filesystem::remove(fifo)); + std::filesystem::create_symlink(file.filename(), root / "link"); + RequireVolume("/anyps5-statvfs-test/link", file); + Require(std::filesystem::remove(root / "link")); + std::filesystem::create_symlink("missing", root / "dangling"); + RequireError("/anyps5-statvfs-test/dangling", 2); + Require(std::filesystem::remove(root / "dangling")); +#endif + const auto before = OpenHandles(); + for (int i = 0; i != 32; ++i) { + RequireVolume("/anyps5-statvfs-test/file.txt", file); + Require(statvfs_nid_postfix("/anyps5-statvfs-test/file.txt", nullptr) == -1); + Require(*__error_nid_postfix() == 14); + } + Require(OpenHandles() == before); + { + std::ifstream stream(file, std::ios::binary); + std::string text((std::istreambuf_iterator(stream)), std::istreambuf_iterator()); + Require(text == "unchanged"); + } + RemovePathAlias_nid_no_patch("/anyps5-statvfs-test"); + Require(std::filesystem::remove(file)); + Require(std::filesystem::remove(root)); +} diff --git a/core/libs/tests/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/GuestStrftime.cpp b/core/libs/tests/GuestStrftime.cpp new file mode 100644 index 0000000000..eef399eda0 --- /dev/null +++ b/core/libs/tests/GuestStrftime.cpp @@ -0,0 +1,95 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/GuestTime.hpp" +#include +#include +#include + +extern "C" { +std::size_t APS5_VABI strftime_nid_postfix(char*, std::size_t, const char*, const GuestTm*); +char* APS5_VABI asctime_nid_postfix(const GuestTm*); +} + +namespace { + +GuestTm MakeTime(int year, int hour, int wday, int yday, int isdst) { + GuestTm time{}; + time.tm_sec = 5; + time.tm_min = 4; + time.tm_hour = hour; + time.tm_mday = 2; + time.tm_mon = 0; + time.tm_year = year; + time.tm_wday = wday; + time.tm_yday = yday; + time.tm_isdst = isdst; + time.tm_gmtoff = 19800; + time.tm_zone = "IST"; + return time; +} + +} + +int main() { + struct Case { + const char* name; + int year; + int hour; + int wday; + int yday; + int isdst; + const char* format; + const char* expected; + }; + constexpr Case cases[] = { + {"alternative", 126, 3, 5, 1, 0, "%Ea %OX %OD %EF %EEY", "Fri 03:04:05 01/02/26 2026-01-02 EY"}, + {"padding", 126, 3, 5, 1, 0, "%-d|%_d|%0e|%-k|%_+|%--d", "2| 2|02|3|Fri Jan 2 03:04:05 IST 2026|-d"}, + {"zone", 126, 3, 5, 1, 0, "%z %Z %0z", "+0530 IST +0530"}, + {"year 899", -1001, 3, 5, 1, 0, "%Y %C %y %G %g %F", "0899 08 99 0899 99 0899-01-02"}, + {"year -1", -1901, 3, 5, 1, 0, "%Y %C %y %c", "-001 -0 01 Fri Jan 2 03:04:05 -001"}, + {"year 12345", 10445, 3, 5, 1, 0, "%Y %C %y %D %v", "12345 123 45 01/02/45 2-Jan-12345"}, + {"out of range", 126, 100, 8, 400, 0, "%I %l %u %W %U %j %a %p", "04 4 8 57 57 401 ? PM"}, + {"unknown dst", 126, 3, 5, 1, -1, "[%z][%Z]", "[][IST]"}, + {"literals", 126, 3, 5, 1, 0, "%q %% %", "q % %"}, + }; + bool correct = true; + for (const auto& test : cases) { + const GuestTm time = MakeTime(test.year, test.hour, test.wday, test.yday, test.isdst); + char text[128]; + const std::size_t length = strftime_nid_postfix(text, sizeof(text), test.format, &time); + if (length != std::strlen(test.expected) || std::strcmp(text, test.expected) != 0) { + std::fprintf(stderr, "strftime %s (\"%s\"): expected \"%s\", received \"%s\" (%zu)\n", + test.name, test.format, test.expected, length == 0 ? "" : text, length); + correct = false; + } + } + const GuestTm now = MakeTime(126, 3, 5, 1, 0); + char text[32]; + if (strftime_nid_postfix(text, 10, "%Y-%m-%d", &now) != 0 || strftime_nid_postfix(text, 11, "%Y-%m-%d", &now) != 10) { + std::fprintf(stderr, "strftime does not return 0 when the result and its terminator do not fit\n"); + correct = false; + } + if (strftime_nid_postfix(text, sizeof(text), nullptr, &now) == 0 || std::strcmp(text, "Fri Jan 2 03:04:05 2026") != 0) { + std::fprintf(stderr, "strftime with a null format does not format %%c\n"); + correct = false; + } + struct AsctimeCase { + int year; + const char* expected; + }; + constexpr AsctimeCase asctimeCases[] = { + {126, "Fri Jan 2 03:04:05 2026\n"}, + {-1001, "Fri Jan 2 03:04:05 0899\n"}, + {-1901, "Fri Jan 2 03:04:05 -001\n"}, + {10445, "Fri Jan 2 03:04:05 12345\n"}, + {-2147483647 - 1, "Fri Jan 2 03:04:05 -2147481748\n"}, + }; + for (const auto& test : asctimeCases) { + const GuestTm time = MakeTime(test.year, 3, 5, 1, 0); + const char* result = asctime_nid_postfix(&time); + if (std::strcmp(result, test.expected) != 0) { + std::fprintf(stderr, "asctime of tm_year %d: expected \"%s\", received \"%s\"\n", test.year, test.expected, result); + correct = false; + } + } + return correct ? 0 : 1; +} diff --git a/core/libs/tests/GuestStrings.cpp b/core/libs/tests/GuestStrings.cpp index d65ea9a148..ef2f2f0a69 100644 --- a/core/libs/tests/GuestStrings.cpp +++ b/core/libs/tests/GuestStrings.cpp @@ -1,31 +1,47 @@ #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*); char* APS5_VABI strtok_r_nid_postfix(char*, const char*, char**); 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*, ...); int APS5_VABI sscanf_s_nid_postfix(const char*, const char*, ...); int APS5_VABI __inet_aton_nid_postfix(const char*, void*); @@ -66,6 +82,40 @@ static bool CheckMemcpyOverlap() { return correct; } +static void CheckSecureCopySizeLimits() { + using Copy = int (APS5_VABI*)(void*, std::size_t, const void*, std::size_t); + constexpr std::size_t RsizeMax = SIZE_MAX >> 1; + const std::array original = {1, 2, 3, 4, 5, 6, 7, 8}; + const unsigned char source[] = {9, 10, 11, 12}; + for (Copy copy : {memcpy_s_nid_postfix, memmove_s_nid_postfix}) { + for (const auto size : {RsizeMax + 1, SIZE_MAX}) { + for (const auto count : {std::size_t{1}, std::size_t{0}, RsizeMax + 1, SIZE_MAX}) { + for (const void* input : {static_cast(source), static_cast(nullptr)}) { + auto bytes = original; + Require(copy(bytes.data() + 2, size, input, count) != 0); + Require(bytes == original); + } + } + } + for (const auto count : {RsizeMax + 1, SIZE_MAX}) { + auto bytes = original; + Require(copy(bytes.data() + 2, 4, source, count) != 0); + const std::array cleared = {1, 2, 0, 0, 0, 0, 7, 8}; + Require(bytes == cleared); + } + auto bytes = original; + Require(copy(bytes.data() + 2, RsizeMax, source, 1) == 0); + const std::array copied = {1, 2, 9, 4, 5, 6, 7, 8}; + Require(bytes == copied); + Require(copy(bytes.data() + 2, 4, source, 0) == 0 && bytes == copied); + Require(copy(nullptr, SIZE_MAX, source, 1) == 22); + bytes = original; + Require(copy(bytes.data() + 2, 4, nullptr, 1) == 22); + const std::array cleared = {1, 2, 0, 0, 0, 0, 7, 8}; + Require(bytes == cleared); + } +} + static void CheckBoundsCheckedFunctions() { char small[4] = "zz"; Require(strcpy_s_nid_postfix(small, sizeof(small), "abc") == 0 && std::strcmp(small, "abc") == 0); @@ -81,7 +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); - Require(memset_s_nid_postfix(bytes, sizeof(bytes), 9, 8) == 34 && bytes[3] == 9); + 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); + Require(memset_s_nid_postfix(sentinel, (SIZE_MAX >> 1) + 1, 0, 1) == 22 && sentinel[0] == 7); + Require(memset_s_nid_postfix(sentinel, sizeof(sentinel), 0, (SIZE_MAX >> 1) + 1) == 22 && sentinel[0] == 7 && sentinel[3] == 4); + Require(memset_s_nid_postfix(sentinel, SIZE_MAX >> 1, 5, 1) == 0 && sentinel[0] == 5 && sentinel[1] == 7); + Require(memset_s_nid_postfix(nullptr, sizeof(sentinel), 0, 1) == 22); Require(strnstr_nid_postfix("haystack", "st", 4) == nullptr); const char haystack[] = "haystack"; Require(strnstr_nid_postfix(haystack, "st", 6) == haystack + 3); @@ -89,6 +145,45 @@ 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'); + Require(strcasecmp_nid_postfix("ab", "A") == 'b' && strcasecmp_nid_postfix("AB", "a") == 'b'); + Require(strcasecmp_nid_postfix("A", "aB") == -'b' && strcasecmp_nid_postfix("a", "AB") == -'b'); + Require(strcasecmp_nid_postfix("", "Z") == -'z' && strcasecmp_nid_postfix("Z", "") == 'z'); + Require(strncasecmp_nid_postfix("abc", "ABD", 2) == 0 && strncasecmp_nid_postfix("abc", "ABD", 3) == 'c' - 'd'); + Require(strncasecmp_nid_postfix("Ab", "a", 2) == 'b' && strncasecmp_nid_postfix("a", "AB", 5) == -'b'); + Require(strncasecmp_nid_postfix("Q", "", 1) == 'q' && strncasecmp_nid_postfix("", "Q", 1) == -'q'); + Require(strncasecmp_nid_postfix("x", "y", 0) == 0 && strncasecmp_nid_postfix("same", "SAME", 99) == 0); +} + static void CheckSscanfS() { #ifndef _WIN32 int number = 0; @@ -110,9 +205,155 @@ static void CheckSscanfS() { #endif } +static bool CheckDirnameLengths() { + const std::string longLeaf(4096, 'x'); + const std::string longestParent(1023, 'p'); + const std::string tooLongParent(1024, 'q'); + struct Case { + std::string path; + const char* expected; + }; + const Case cases[] = { + {std::string(1023, 'x'), "."}, + {std::string(1024, 'x'), "."}, + {longLeaf, "."}, + {"/" + longLeaf, "/"}, + {"one/" + longLeaf, "one"}, + {"/one/" + longLeaf, "/one"}, + {"one//" + longLeaf + "///", "one"}, + {"/one//" + longLeaf + "///", "/one"}, + {longLeaf + "///", "."}, + {std::string(4096, '/'), "/"}, + {longestParent + "/leaf", longestParent.c_str()}, + {longestParent + "//leaf///", longestParent.c_str()}, + {longestParent + "/", "."}, + {tooLongParent + "/leaf", nullptr}, + {tooLongParent + "//leaf///", nullptr}, + {longLeaf + "/leaf", nullptr}, + }; + bool passed = true; + for (const auto& test : cases) { + const std::string original = test.path; + *__error_nid_postfix() = 77; + const char* result = dirname_nid_postfix(test.path.c_str()); + const bool matches = test.expected + ? result != nullptr && std::strcmp(result, test.expected) == 0 && *__error_nid_postfix() == 77 + : result == nullptr && *__error_nid_postfix() == 63; + if (!matches || test.path != original) { + std::fprintf(stderr, "dirname input length %zu: expected output length %zu, received %s, errno %d\n", + test.path.size(), test.expected ? std::strlen(test.expected) : 0, + result ? "a directory" : "null", *__error_nid_postfix()); + passed = false; + } + } + 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(); + Require(std::strcmp(dirname_nid_postfix(nullptr), ".") == 0); + char emptyPath[] = ""; + Require(std::strcmp(dirname_nid_postfix(emptyPath), ".") == 0); + Require(std::strcmp(dirname_nid_postfix("file"), ".") == 0); + const char nested[] = "one/two///"; + Require(std::strcmp(dirname_nid_postfix(nested), "one") == 0 && std::strcmp(nested, "one/two///") == 0); + char repeatedSeparators[] = "one//two"; + Require(std::strcmp(dirname_nid_postfix(repeatedSeparators), "one") == 0); + char absoluteRepeatedSeparators[] = "/one//two"; + Require(std::strcmp(dirname_nid_postfix(absoluteRepeatedSeparators), "/one") == 0); + char rootPath[] = "/"; + Require(std::strcmp(dirname_nid_postfix(rootPath), "/") == 0); + char trailingSlash[] = "foo/"; + Require(std::strcmp(dirname_nid_postfix(trailingSlash), ".") == 0); + char absolute[] = "/one/two"; + Require(std::strcmp(dirname_nid_postfix(absolute), "/one") == 0); + char rootChild[] = "/file"; + Require(std::strcmp(dirname_nid_postfix(rootChild), "/") == 0); + char onlySlashes[] = "////"; + Require(std::strcmp(dirname_nid_postfix(onlySlashes), "/") == 0); Require(std::strcmp(basename_nid_postfix(nullptr), ".") == 0); Require(std::strcmp(basename_nid_postfix(""), ".") == 0); Require(std::strcmp(basename_nid_postfix("////"), "/") == 0); @@ -122,6 +363,30 @@ 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)); + char* const dotted = __inet_ntoa_nid_postfix(loopback); + Require(std::strcmp(dotted, "127.0.0.1") == 0); + Require(__inet_ntoa_nid_postfix(0xffffffffu) == dotted && std::strcmp(dotted, "255.255.255.255") == 0); + Require(std::strcmp(__inet_ntoa_nid_postfix(0), "0.0.0.0") == 0); + const unsigned char mixedBytes[4] = {10, 200, 3, 45}; + unsigned int mixed; + std::memcpy(&mixed, mixedBytes, sizeof(mixed)); + Require(std::strcmp(__inet_ntoa_nid_postfix(mixed), "10.200.3.45") == 0); + const unsigned char paddingBytes[4] = {100, 9, 99, 0}; + unsigned int padding; + std::memcpy(&padding, paddingBytes, sizeof(padding)); + Require(std::strcmp(__inet_ntoa_nid_postfix(padding), "100.9.99.0") == 0); const char bounded[] = {'a', 'b', 'c'}; Require(strnlen_nid_postfix(bounded, 0) == 0); Require(strnlen_nid_postfix(bounded, sizeof(bounded)) == 3); @@ -145,6 +410,20 @@ int main() { Require(stpcpy_nid_postfix(end, "") == end && chained[5] == 'z'); Require(strncat_nid_postfix(buffer, "xyz", 2) == buffer); Require(std::strcmp(buffer, "xy") == 0); + char transformed[8] = "zzzzzzz"; + Require(strxfrm_nid_postfix(transformed, "abc", sizeof(transformed)) == 3); + Require(std::strcmp(transformed, "abc") == 0 && transformed[4] == 'z'); + std::memset(transformed, 'z', sizeof(transformed)); + Require(strxfrm_nid_postfix(transformed, "abcdefgh", 4) == 8); + Require(std::strcmp(transformed, "abc") == 0 && transformed[4] == 'z'); + std::memset(transformed, 'z', sizeof(transformed)); + Require(strxfrm_nid_postfix(transformed, "abcdefg", sizeof(transformed)) == 7 && std::strcmp(transformed, "abcdefg") == 0); + std::memset(transformed, 'z', sizeof(transformed)); + Require(strxfrm_nid_postfix(transformed, "abcdefgh", sizeof(transformed)) == 8 && std::strcmp(transformed, "abcdefg") == 0); + std::memset(transformed, 'z', sizeof(transformed)); + Require(strxfrm_nid_postfix(transformed, "", sizeof(transformed)) == 0 && transformed[0] == '\0' && transformed[1] == 'z'); + Require(strxfrm_nid_postfix(transformed, "abc", 1) == 3 && transformed[0] == '\0' && transformed[1] == 'z'); + Require(strxfrm_nid_postfix(nullptr, "abc", 0) == 3); Require(strpbrk_nid_postfix(buffer, "ay") == buffer + 1); Require(strpbrk_nid_postfix(buffer, "") == nullptr); Require(strcspn_nid_postfix(buffer, "y") == 1); @@ -189,11 +468,32 @@ int main() { char* firstState = nullptr; char* secondState = nullptr; Require(std::strcmp(strtok_r_nid_postfix(first, ",", &firstState), "a") == 0); + Require(firstState == first + 3); Require(std::strcmp(strtok_r_nid_postfix(second, ":", &secondState), "x") == 0); + Require(secondState == second + 2); Require(std::strcmp(strtok_r_nid_postfix(nullptr, ",", &firstState), "b") == 0); + Require(firstState == first + 6); Require(strtok_r_nid_postfix(nullptr, ",", &firstState) == nullptr); + if (firstState != nullptr) std::fputs("strtok_r retained the state after exhausting delimiters\n", stderr); + Require(firstState == nullptr); Require(strtok_r_nid_postfix(nullptr, ",", &firstState) == nullptr); Require(std::strcmp(strtok_r_nid_postfix(nullptr, "", &secondState), "y") == 0); + Require(secondState == nullptr); + char tokenEnd[8] = "last"; + char* finishedState = first; + *__error_nid_postfix() = 77; + Require(strtok_r_nid_postfix(tokenEnd, ",", &finishedState) == tokenEnd && finishedState == nullptr); + tokenEnd[4] = 'x'; + tokenEnd[5] = '\0'; + Require(strtok_r_nid_postfix(nullptr, ",", &finishedState) == nullptr && finishedState == nullptr); + for (const char* input : {"", ",", ",,,"}) { + char noTokens[8]; + std::strcpy(noTokens, input); + finishedState = first; + Require(strtok_r_nid_postfix(noTokens, ",", &finishedState) == nullptr && finishedState == nullptr); + Require(strtok_r_nid_postfix(nullptr, ",", &finishedState) == nullptr); + } + Require(*__error_nid_postfix() == 77); char hostTokens[] = "host:next"; Require(std::strcmp(std::strtok(hostTokens, ":"), "host") == 0); char guestTokens[] = ",one,,two:three"; @@ -203,6 +503,21 @@ int main() { Require(strtok_nid_postfix(nullptr, ",") == nullptr); Require(strtok_nid_postfix(nullptr, ",") == nullptr); Require(std::strcmp(std::strtok(nullptr, ":"), "next") == 0); + char fields[] = ",a,,b;c"; + char* cursor = fields; + Require(strsep_nid_postfix(&cursor, ",;") == fields && fields[0] == '\0' && cursor == fields + 1); + Require(std::strcmp(strsep_nid_postfix(&cursor, ",;"), "a") == 0); + Require(std::strcmp(strsep_nid_postfix(&cursor, ",;"), "") == 0); + Require(std::strcmp(strsep_nid_postfix(&cursor, ",;"), "b") == 0); + char* last = cursor; + Require(strsep_nid_postfix(&cursor, ",;") == last && std::strcmp(last, "c") == 0 && cursor == nullptr); + Require(strsep_nid_postfix(&cursor, ",;") == nullptr && cursor == nullptr); + char whole[] = "a,b"; + cursor = whole; + Require(strsep_nid_postfix(&cursor, "") == whole && std::strcmp(whole, "a,b") == 0 && cursor == nullptr); + char empty[] = ""; + cursor = empty; + Require(strsep_nid_postfix(&cursor, ",") == empty && cursor == nullptr); const char text[] = "aABAbC"; Require(strcasestr_nid_postfix(text, "ababc") == text + 1); Require(strcasestr_nid_postfix(text, "") == text); @@ -210,5 +525,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/GuestSyslog.cpp b/core/libs/tests/GuestSyslog.cpp index a4634c5d65..0d55e1a440 100644 --- a/core/libs/tests/GuestSyslog.cpp +++ b/core/libs/tests/GuestSyslog.cpp @@ -2,14 +2,20 @@ #include #include #include +#include +#include #ifdef _WIN32 #include +#include #else #include #endif extern "C" { void APS5_VABI syslog_nid_postfix(int, const char*, ...); +void APS5_VABI openlog_nid_postfix(const char*, int, int); +void APS5_VABI closelog_nid_postfix(); +int APS5_VABI setlogmask_nid_postfix(int); int* APS5_VABI __error_nid_postfix(); } static void Require(bool value) { if (!value) std::abort(); } @@ -25,11 +31,48 @@ int main() { Require(saved >= 0 && ::dup2(::fileno(captured), ::fileno(stderr)) == ::fileno(stderr)); #endif *__error_nid_postfix() = 2; + Require(setlogmask_nid_postfix(0) == 0xff); syslog_nid_postfix(6, "station %s at %d kbps", "radio", 128); const int afterFirst = *__error_nid_postfix(); syslog_nid_postfix(16 | 3, "open: %m, %%m kept, %d%%\n", 50); syslog_nid_postfix(0x10000 | 4, "masked"); syslog_nid_postfix(7, ""); + Require(setlogmask_nid_postfix(1 << 3) == 0xff); + Require(setlogmask_nid_postfix(0) == (1 << 3)); + int count = -1; + syslog_nid_postfix(7, "suppressed %m%n", &count); + Require(count == -1 && *__error_nid_postfix() == 2); + syslog_nid_postfix(16 | 3, "allowed: %m"); + Require(*__error_nid_postfix() == 2); + syslog_nid_postfix(0x10000 | 7, "suppressed %n", &count); + Require(count == -1 && *__error_nid_postfix() == 2); + Require(setlogmask_nid_postfix(1 << 4) == (1 << 3)); + syslog_nid_postfix(0x10000 | 4, "warning without diagnostic"); + syslog_nid_postfix(0x10000 | 7, "both suppressed %n", &count); + Require(count == -1 && *__error_nid_postfix() == 2); + Require(setlogmask_nid_postfix(0x100) == (1 << 4)); + Require(setlogmask_nid_postfix(0) == 0x100); + syslog_nid_postfix(0x10000 | 3, "no priorities enabled %n", &count); + Require(count == -1 && *__error_nid_postfix() == 2); + Require(setlogmask_nid_postfix(-1) == 0x100); + Require(setlogmask_nid_postfix(0) == -1); + syslog_nid_postfix(7, "negative mask"); + Require(setlogmask_nid_postfix(0x55) == -1); + for (int priority = 0; priority < 8; ++priority) + syslog_nid_postfix(16 | priority, "level %d", priority); + std::thread([] { Require(setlogmask_nid_postfix(0xff) == 0x55); }).join(); + Require(setlogmask_nid_postfix(0) == 0xff); + syslog_nid_postfix(7, "mask shared across threads"); + syslog_nid_postfix(7, "wide %ls %d %S", u"guest", 5, u"text"); + openlog_nid_postfix("worker", 1, 16 << 3); + syslog_nid_postfix(6, "configured default facility"); + openlog_nid_postfix(nullptr, 1, 0); + syslog_nid_postfix(6, "null identity keeps tag"); + openlog_nid_postfix("daemon", 0, 24 << 3); + syslog_nid_postfix(16 | 3, "explicit facility"); + openlog_nid_postfix("closing", 1, 0); + closelog_nid_postfix(); + syslog_nid_postfix(6, "identity cleared, options and facility retained"); std::fflush(stderr); #ifdef _WIN32 Require(::_dup2(saved, ::_fileno(stderr)) == 0 && ::_close(saved) == 0); @@ -38,14 +81,36 @@ int main() { #endif Require(afterFirst == 2 && *__error_nid_postfix() == 2); std::rewind(captured); - char text[512]{}; + char text[1024]{}; const auto length = std::fread(text, 1, sizeof(text) - 1, captured); const char* expected = "[syslog:14] station radio at 128 kbps\n" "[syslog:19] open: No such file or directory, %m kept, 50%\n" "[syslog:35] syslog: unknown facility/priority: 10004\n" "[syslog:12] masked\n" - "[syslog:15] \n"; - Require(length == std::strlen(expected) && std::strcmp(text, expected) == 0); + "[syslog:15] \n" + "[syslog:19] allowed: No such file or directory\n" + "[syslog:35] syslog: unknown facility/priority: 10007\n" + "[syslog:12] warning without diagnostic\n" + "[syslog:15] negative mask\n" + "[syslog:16] level 0\n" + "[syslog:18] level 2\n" + "[syslog:20] level 4\n" + "[syslog:22] level 6\n" + "[syslog:15] mask shared across threads\n" + "[syslog:15] wide guest 5 text\n"; + const std::string prefix = expected; + const std::string pid = std::to_string( +#ifdef _WIN32 + ::_getpid() +#else + ::getpid() +#endif + ); + const std::string configured = "[syslog:134] worker[" + pid + "]: configured default facility\n" + "[syslog:134] worker[" + pid + "]: null identity keeps tag\n" + "[syslog:19] daemon: explicit facility\n" + "[syslog:198] [" + pid + "]identity cleared, options and facility retained\n"; + Require(length == prefix.size() + configured.size() && std::string(text, length) == prefix + configured); Require(std::fclose(captured) == 0); } diff --git a/core/libs/tests/GuestSysmodule.cpp b/core/libs/tests/GuestSysmodule.cpp index bf6dbaa007..f98548f2ce 100644 --- a/core/libs/tests/GuestSysmodule.cpp +++ b/core/libs/tests/GuestSysmodule.cpp @@ -1,25 +1,384 @@ +#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 +#include +#include +#endif extern "C" { +int APS5_VABI sceSysmoduleIsLoaded(std::uint16_t id); +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); } namespace { void Require(bool value) { if (!value) std::abort(); } -constexpr std::uint32_t kFiberModuleId = 0x00000006; -constexpr int kModuleNotLoaded = static_cast(0x80A90003); +template +bool ThrowsRuntimeError(TFunction function) { + try { + function(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +constexpr std::uint16_t kFiberModuleId = 0x0006; +constexpr std::uint16_t kUltModuleId = 0x0007; +constexpr int kInvalidValue = static_cast(0x805A1000); +constexpr int kModuleUnloaded = static_cast(0x805A1001); +constexpr std::uint32_t kInternalFlag = 0x80000000u; +constexpr std::uint16_t kAcmModuleId = 0x010b; +constexpr std::uint16_t kTrophy2ModuleId = 0x0110; +constexpr std::uint16_t kVoiceChatModuleId = 0x0114; +constexpr std::uint16_t 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 { + std::uint64_t start = 0; + std::uint64_t end = 0; + std::string path; +}; + +Mapping MapsEntry(std::uint64_t address) { + std::ifstream maps("/proc/self/maps"); + std::string line; + while (std::getline(maps, line)) { + unsigned long long start = 0; + unsigned long long end = 0; + char path[4096] = {}; + const int parsed = std::sscanf(line.c_str(), "%llx-%llx %*s %*x %*x:%*x %*u %4095s", &start, &end, path); + if (parsed >= 2 && address >= start && address < end) return Mapping{start, end, parsed == 3 ? path : ""}; + } + std::abort(); +} + +void RequireUnwindMatchesMaps(const void* pointer) { + const auto address = reinterpret_cast(pointer); + const auto expected = MapsEntry(address); + ModuleInfoForUnwind info{}; + Require(sceSysmoduleGetModuleInfoForUnwind(address, 0, &info) == 0); + Require(info.st_size == sizeof(ModuleInfoForUnwind)); + Require(info.seg0_addr == expected.start && info.seg0_size == expected.end - expected.start); + Require(std::string(info.name) == expected.path.substr(0, sizeof(info.name) - 1)); + Require(info.eh_frame_hdr_addr == 0 && info.eh_frame_addr == 0 && info.eh_frame_size == 0); +} + +void CheckUnwindInfo() { + RequireUnwindMatchesMaps(reinterpret_cast(&RequireUnwindMatchesMaps)); + RequireUnwindMatchesMaps(reinterpret_cast(&sceSysmoduleGetModuleInfoForUnwind)); + int local = 0; + RequireUnwindMatchesMaps(&local); + const auto page = static_cast(sysconf(_SC_PAGESIZE)); + void* anonymous = mmap(nullptr, page, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(anonymous != MAP_FAILED); + RequireUnwindMatchesMaps(anonymous); + Require(munmap(anonymous, page) == 0); + void* gap = mmap(nullptr, page, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(gap != MAP_FAILED && munmap(gap, page) == 0); + ModuleInfoForUnwind info{}; + bool threw = false; + try { + sceSysmoduleGetModuleInfoForUnwind(reinterpret_cast(gap), 0, &info); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); +} +#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; + Require(sceSysmoduleLoadModuleInternalWithArg(id, 0, nullptr, 0, &result) == kInvalidValue); + 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); + Require(sceSysmoduleIsLoaded(kFiberModuleId) == 0); Require(sceSysmoduleUnloadModuleInternal(kFiberModuleId) == 0); - Require(sceSysmoduleUnloadModuleInternal(kFiberModuleId) == kModuleNotLoaded); + Require(sceSysmoduleIsLoaded(kFiberModuleId) == kModuleUnloaded); + Require(sceSysmoduleUnloadModuleInternal(kFiberModuleId) == kModuleUnloaded); + + Require(sceSysmoduleIsLoaded(kUltModuleId) == kModuleUnloaded); + Require(sceSysmoduleUnloadModule(kUltModuleId) == kModuleUnloaded); + Require(sceSysmoduleLoadModule(kUltModuleId) == 0); + Require(sceSysmoduleIsLoaded(kUltModuleId) == 0); + Require(sceSysmoduleUnloadModule(kUltModuleId) == 0); + Require(sceSysmoduleIsLoaded(kUltModuleId) == kModuleUnloaded); + + Require(sceSysmoduleIsLoaded(kAcmModuleId) == kModuleUnloaded); + Require(sceSysmoduleUnloadModule(kAcmModuleId) == kModuleUnloaded); + Require(sceSysmoduleLoadModule(kAcmModuleId) == 0); + Require(sceSysmoduleIsLoaded(kAcmModuleId) == 0); + Require(sceSysmoduleUnloadModule(kAcmModuleId) == 0); + Require(sceSysmoduleIsLoaded(kAcmModuleId) == kModuleUnloaded); + + constexpr std::uint16_t kShareModuleId = 0x010f; + Require(sceSysmoduleIsLoaded(kShareModuleId) == kModuleUnloaded); + Require(sceSysmoduleLoadModule(kShareModuleId) == 0); + Require(sceSysmoduleIsLoaded(kShareModuleId) == 0); + Require(sceSysmoduleUnloadModule(kShareModuleId) == 0); + Require(sceSysmoduleIsLoaded(kShareModuleId) == kModuleUnloaded); + + Require(sceSysmoduleIsLoaded(kTrophy2ModuleId) == kModuleUnloaded); + Require(sceSysmoduleLoadModuleInternal(kTrophy2ModuleId) == 0); + Require(sceSysmoduleIsLoaded(kTrophy2ModuleId) == 0); + Require(sceSysmoduleLoadModuleInternal(kTrophy2ModuleId) == 0); + Require(sceSysmoduleUnloadModuleInternal(kTrophy2ModuleId) == 0); + Require(sceSysmoduleIsLoaded(kTrophy2ModuleId) == 0); + Require(sceSysmoduleUnloadModuleInternal(kTrophy2ModuleId) == 0); + Require(sceSysmoduleIsLoaded(kTrophy2ModuleId) == kModuleUnloaded); + Require(sceSysmoduleUnloadModuleInternal(kTrophy2ModuleId) == kModuleUnloaded); + + Require(sceSysmoduleIsLoaded(kVoiceChatModuleId) == kModuleUnloaded); + Require(sceSysmoduleUnloadModule(kVoiceChatModuleId) == kModuleUnloaded); + Require(sceSysmoduleLoadModule(kVoiceChatModuleId) == 0); + Require(sceSysmoduleIsLoaded(kVoiceChatModuleId) == 0); + Require(sceSysmoduleUnloadModule(kVoiceChatModuleId) == 0); + Require(sceSysmoduleIsLoaded(kVoiceChatModuleId) == kModuleUnloaded); + + Require(sceSysmoduleIsLoaded(kPsmlModuleId) == kModuleUnloaded); + Require(sceSysmoduleUnloadModule(kPsmlModuleId) == kModuleUnloaded); + Require(sceSysmoduleLoadModule(kPsmlModuleId) == 0); + Require(sceSysmoduleIsLoaded(kPsmlModuleId) == 0); + Require(sceSysmoduleUnloadModule(kPsmlModuleId) == 0); + Require(sceSysmoduleIsLoaded(kPsmlModuleId) == kModuleUnloaded); + + Require(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); +#if defined(__linux__) || defined(_WIN32) + CheckUnwindInfo(); +#endif + + std::int32_t handle = -1; + Require(sceSysmoduleGetModuleHandleInternal(kFiberModuleId, &handle) == kModuleUnloaded); + Require(handle == -1); + Require(ThrowsRuntimeError([] { sceSysmoduleGetModuleHandleInternal(0, nullptr); })); + Require(ThrowsRuntimeError([&] { sceSysmoduleGetModuleHandleInternal(0, &handle); })); + Require(ThrowsRuntimeError([] { sceSysmoduleGetModuleHandleInternal(kFiberModuleId, nullptr); })); + Require(ThrowsRuntimeError([&] { sceSysmoduleGetModuleHandleInternal(0x00ffffffu, &handle); })); + + Require(sceSysmoduleLoadModule(kFiberModuleId) == 0); + Require(sceSysmoduleGetModuleHandleInternal(kFiberModuleId, &handle) == 0); + Require(handle >= 0); + std::int32_t again = -1; + Require(sceSysmoduleGetModuleHandleInternal(kFiberModuleId, &again) == 0); + Require(again == handle); + void* symbol = nullptr; + Require(sceKernelDlsym(handle, "sceFiberOptParamInitialize", &symbol) == 0); + Require(symbol != nullptr); + Require(sceSysmoduleUnloadModule(kFiberModuleId) == 0); + Require(sceSysmoduleGetModuleHandleInternal(kFiberModuleId, &handle) == kModuleUnloaded); + + constexpr std::uint16_t kAudioPropagationModuleId = 0x0120; + std::int32_t audioPropagationHandle = -1; + Require(sceSysmoduleGetModuleHandleInternal(kAudioPropagationModuleId, &audioPropagationHandle) == kModuleUnloaded); + Require(audioPropagationHandle == -1); + + Require(sceSysmoduleIsLoaded(kAudioPropagationModuleId) == kModuleUnloaded); + Require(sceSysmoduleLoadModule(kAudioPropagationModuleId) == 0); + Require(sceSysmoduleIsLoaded(kAudioPropagationModuleId) == 0); + Require(sceSysmoduleGetModuleHandleInternal(kAudioPropagationModuleId, &audioPropagationHandle) == 0); + Require(audioPropagationHandle >= 0); + void* audioPropagationSymbol = nullptr; + Require(sceKernelDlsym(audioPropagationHandle, "sceAudioPropagationSystemCreate", &audioPropagationSymbol) == 0); + Require(audioPropagationSymbol != nullptr); + + Require(sceSysmoduleUnloadModule(kAudioPropagationModuleId) == 0); + Require(sceSysmoduleIsLoaded(kAudioPropagationModuleId) == kModuleUnloaded); + Require(sceSysmoduleGetModuleHandleInternal(kAudioPropagationModuleId, &audioPropagationHandle) == kModuleUnloaded); } diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index f43650ca4c..c6726534a9 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -1,14 +1,39 @@ #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]; + std::uint32_t version; +}; +static_assert(offsetof(SceKernelSystemSwVersion, size) == 0x00); +static_assert(offsetof(SceKernelSystemSwVersion, versionString) == 0x08); +static_assert(offsetof(SceKernelSystemSwVersion, version) == 0x24); +static_assert(sizeof(SceKernelSystemSwVersion) == 0x28); extern "C" { std::int64_t APS5_VABI sysconf_nid_postfix(int); 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(); } static void CheckProcessorCountSysctl() { @@ -36,14 +61,7 @@ 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, 5}; + const int unknown[] = {6, 99}; try { sysctl_nid_postfix(unknown, 2, &value, &length, nullptr, 0); } catch (const std::runtime_error&) { @@ -51,10 +69,749 @@ 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; + *__error_nid_postfix() = 13; + std::size_t length = 0; + Require(sysctl_nid_postfix(mib, 2, nullptr, &length, nullptr, 0) == 0 && length == sizeof(std::uint64_t)); + std::uint64_t value = 0; + length = sizeof(value); + Require(sysctl_nid_postfix(mib, 2, &value, &length, nullptr, 0) == 0 && value == expected && length == sizeof(std::uint64_t)); + value = 0; + length = 16; + Require(sysctlbyname_nid_postfix(name, &value, &length, nullptr, 0) == 0 && value == expected && length == sizeof(std::uint64_t)); + Require(*__error_nid_postfix() == 13); + unsigned char bytes[8] = {0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa}; + length = 4; + Require(sysctlbyname_nid_postfix(name, bytes, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + Require(length == 4 && bytes[4] == 0xaa); + length = sizeof(value); + Require(sysctl_nid_postfix(mib, 2, &value, &length, &value, sizeof(value)) == -1 && *__error_nid_postfix() == 1); + Require(sysctlbyname_nid_postfix(name, &value, &length, &value, sizeof(value)) == -1 && *__error_nid_postfix() == 1); +} +#if defined(__x86_64__) || defined(__i386__) +static void CheckModelSysctl() { + const int mib[] = {6, 2}; + *__error_nid_postfix() = 13; + std::size_t length = 0; + Require(sysctl_nid_postfix(mib, 2, nullptr, &length, nullptr, 0) == 0 && length > 1); + const std::size_t size = length; + char model[128] = {}; + length = sizeof(model); + Require(sysctlbyname_nid_postfix("hw.model", model, &length, nullptr, 0) == 0 && length == size); + Require(std::strlen(model) + 1 == size && model[0] != ' '); + Require(*__error_nid_postfix() == 13); + char truncated[4] = {}; + length = sizeof(truncated); + Require(sysctl_nid_postfix(mib, 2, truncated, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + Require(length == sizeof(truncated) && std::memcmp(truncated, model, sizeof(truncated)) == 0); + length = sizeof(model); + Require(sysctlbyname_nid_postfix("hw.model", model, &length, model, size) == -1 && *__error_nid_postfix() == 1); +} +#endif +static void CheckTscFrequencySysctl() { + *__error_nid_postfix() = 13; + std::size_t length = 0; + Require(sysctlbyname_nid_postfix("machdep.tsc_freq", nullptr, &length, nullptr, 0) == 0 && length == sizeof(std::uint64_t)); + std::uint64_t value = 0; + length = sizeof(value); + Require(sysctlbyname_nid_postfix("machdep.tsc_freq", &value, &length, nullptr, 0) == 0); + Require(value == sceKernelGetTscFrequency() && value != 0 && length == sizeof(value)); + Require(*__error_nid_postfix() == 13); + std::uint32_t truncated = 0; + length = sizeof(truncated); + Require(sysctlbyname_nid_postfix("machdep.tsc_freq", &truncated, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + length = sizeof(value); + Require(sysctlbyname_nid_postfix("machdep.tsc_freq", &value, &length, &value, sizeof(value)) == -1 && *__error_nid_postfix() == 1); +} +static 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; + 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; + char buffer[32] = {}; + std::size_t length = sizeof(buffer); + Require(sysctlbyname_nid_postfix("kern.proc.pathname", buffer, &length, nullptr, 0) == -1); + Require(*__error_nid_postfix() == 22); + + const int mibTooShort[] = {1, 14, 12}; + Require(sysctl_nid_postfix(mibTooShort, 3, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 22); + + const int mibTooLong[] = {1, 14, 12, -1, 0}; + Require(sysctl_nid_postfix(mibTooLong, 5, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 22); + + Require(sysctl_nid_postfix(mibTooShort, 3, buffer, &length, buffer, sizeof(buffer)) == -1 && *__error_nid_postfix() == 1); + Require(sysctl_nid_postfix(mibTooLong, 5, buffer, &length, buffer, sizeof(buffer)) == -1 && *__error_nid_postfix() == 1); + Require(sysctlbyname_nid_postfix("kern.proc.pathname", buffer, &length, buffer, sizeof(buffer)) == -1 && *__error_nid_postfix() == 1); + + const int mibSelf[] = {1, 14, 12, -1}; + length = 0; + Require(sysctl_nid_postfix(mibSelf, 4, nullptr, &length, nullptr, 0) == 0 && length == sizeof("/app0/eboot.bin")); + + std::memset(buffer, 0xaa, sizeof(buffer)); + length = sizeof(buffer); + Require(sysctl_nid_postfix(mibSelf, 4, buffer, &length, nullptr, 0) == 0); + Require(length == sizeof("/app0/eboot.bin")); + Require(std::strcmp(buffer, "/app0/eboot.bin") == 0); + + const int callerPid = getpid_nid_postfix(); + const int mibPid[] = {1, 14, 12, callerPid}; + std::memset(buffer, 0xaa, sizeof(buffer)); + length = sizeof(buffer); + Require(sysctl_nid_postfix(mibPid, 4, buffer, &length, nullptr, 0) == 0); + Require(length == sizeof("/app0/eboot.bin")); + Require(std::strcmp(buffer, "/app0/eboot.bin") == 0); + + const int mibForeign[] = {1, 14, 12, callerPid + 1000}; + Require(sysctl_nid_postfix(mibForeign, 4, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 3); + + length = 5; + std::memset(buffer, 0xaa, sizeof(buffer)); + Require(sysctl_nid_postfix(mibSelf, 4, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + Require(length == 5 && buffer[4] == '0' && buffer[5] == static_cast(0xaa)); + + 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}, {6, 7}, {1, 24}, {1, 37}, {1, 50}}; + for (const auto& leaf : leaves) { + const int mib[] = {leaf[0], leaf[1], 0}; + char buffer[8] = {}; + std::size_t length = sizeof(buffer); + *__error_nid_postfix() = 13; + Require(sysctl_nid_postfix(mib, 3, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 20); + Require(sysctl_nid_postfix(mib, 3, buffer, &length, buffer, sizeof(buffer)) == -1 && *__error_nid_postfix() == 20); + Require(length == sizeof(buffer)); + } +} +static void 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); + const std::uint64_t sizes[] = {0, 24, 39, 40, 41, 48, 64}; + for (const std::uint64_t size : sizes) { + struct { + std::uint64_t prefix; + SceKernelSystemSwVersion version; + unsigned char suffix[88]; + } storage; + std::memset(&storage, 0xaa, sizeof(storage)); + storage.version.size = size; + *__error_nid_postfix() = 13; + Require(sceKernelGetSystemSwVersion_nid_postfix(&storage.version) == 0 && *__error_nid_postfix() == 13); + Require(storage.prefix == 0xaaaaaaaaaaaaaaaaull && storage.version.size == size); + Require(std::strcmp(storage.version.versionString, "01.000.000") == 0); + for (std::size_t i = sizeof("01.000.000"); i < sizeof(storage.version.versionString); ++i) { + Require(storage.version.versionString[i] == 0); + } + Require(storage.version.version == 0x01000000); + for (unsigned char byte : storage.suffix) Require(byte == 0xaa); + } +} +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; + 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); + 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(*__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; + std::size_t length = 0; + Require(sysctlbyname_nid_postfix("vm.overcommit", nullptr, &length, nullptr, 0) == 0 && length == sizeof(int)); + int value = -1; + length = sizeof(value); + Require(sysctlbyname_nid_postfix("vm.overcommit", &value, &length, nullptr, 0) == 0 && value == 0 && length == sizeof(int)); + Require(*__error_nid_postfix() == 13); + unsigned char bytes[4] = {0xaa, 0xaa, 0xaa, 0xaa}; + length = 2; + Require(sysctlbyname_nid_postfix("vm.overcommit", bytes, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + Require(length == 2 && bytes[2] == 0xaa && bytes[3] == 0xaa); + bool threw = false; + length = sizeof(value); + try { + sysctlbyname_nid_postfix("vm.overcommit", &value, &length, &value, sizeof(value)); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); +} +static void CheckSdkVersionSysctl() { + *__error_nid_postfix() = 13; + std::size_t length = 0; + Require(sysctlbyname_nid_postfix("kern.sdk_version", nullptr, &length, nullptr, 0) == 0 && length == sizeof(std::uint32_t)); + std::uint32_t version = 0; + length = sizeof(version); + Require(sysctlbyname_nid_postfix("kern.sdk_version", &version, &length, nullptr, 0) == 0 && version == 0x01000000 && length == sizeof(std::uint32_t)); + Require(*__error_nid_postfix() == 13); + unsigned char bytes[4] = {0xaa, 0xaa, 0xaa, 0xaa}; + length = 2; + Require(sysctlbyname_nid_postfix("kern.sdk_version", bytes, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + Require(length == 2 && bytes[2] == 0xaa && bytes[3] == 0xaa); + length = sizeof(version); + Require(sysctlbyname_nid_postfix("kern.sdk_version", &version, &length, &version, sizeof(version)) == -1 && *__error_nid_postfix() == 1); +} +static void CheckArandomSysctl() { + const int mib[] = {1, 37}; + *__error_nid_postfix() = 13; + std::size_t length = 0; + Require(sysctl_nid_postfix(mib, 2, nullptr, &length, nullptr, 0) == 0 && length == 0); + length = 64; + Require(sysctl_nid_postfix(mib, 2, nullptr, &length, nullptr, 0) == 0 && length == 64); + length = 300; + Require(sysctl_nid_postfix(mib, 2, nullptr, &length, nullptr, 0) == 0 && length == 256); + Require(sysctl_nid_postfix(mib, 2, nullptr, nullptr, nullptr, 0) == 0); + Require(*__error_nid_postfix() == 13); + unsigned char first[64] = {}; + unsigned char second[64] = {}; + length = sizeof(first); + Require(sysctl_nid_postfix(mib, 2, first, &length, nullptr, 0) == 0 && length == sizeof(first)); + length = sizeof(second); + Require(sysctl_nid_postfix(mib, 2, second, &length, nullptr, 0) == 0 && length == sizeof(second)); + Require(std::memcmp(first, second, sizeof(first)) != 0); + Require(*__error_nid_postfix() == 13); + unsigned char named[64] = {}; + length = sizeof(named); + Require(sysctlbyname_nid_postfix("kern.arandom", named, &length, nullptr, 0) == 0 && length == sizeof(named)); + Require(*__error_nid_postfix() == 13); + length = sizeof(first); + Require(sysctl_nid_postfix(mib, 2, first, &length, first, sizeof(first)) == -1 && *__error_nid_postfix() == 1); + *__error_nid_postfix() = 13; + unsigned char big[300] = {}; + length = sizeof(big); + Require(sysctl_nid_postfix(mib, 2, big, &length, nullptr, 0) == 0 && length == 256); + unsigned char small[16] = {}; + length = sizeof(small); + Require(sysctl_nid_postfix(mib, 2, small, &length, nullptr, 0) == 0 && length == sizeof(small)); + 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(); + 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); @@ -63,4 +820,5 @@ int main() { Require(sysconf_nid_postfix(-1) == -1); // verifies full-width signed return Require(*__error_nid_postfix() == 22); Require(sysconf_nid_postfix(0x7fffffff) == -1); + Require(environ_nid_postfix && !environ_nid_postfix[0]); } diff --git a/core/libs/tests/GuestSystemExit.cpp b/core/libs/tests/GuestSystemExit.cpp index 3a22d960f2..4cf3ad3931 100644 --- a/core/libs/tests/GuestSystemExit.cpp +++ b/core/libs/tests/GuestSystemExit.cpp @@ -1,23 +1,61 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/Shutdown.hpp" #include "prx/libSceSystemService/SystemService.hpp" +#include +#include #include #include +#include +#include +#include #include #include +#include +#include +#ifdef _WIN32 +#include +#else +#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); +extern "C" void APS5_VABI exit_nid_postfix(int); extern "C" void APS5_VABI catchReturnFromMain_nid_postfix(int); using GuestQuickExitCallback = void (APS5_VABI*)(); extern "C" int APS5_VABI at_quick_exit_nid_postfix(GuestQuickExitCallback); extern "C" void APS5_VABI quick_exit_nid_postfix(int); namespace { -bool cleaned = false; -void Cleanup() { cleaned = true; } -void VerifyExit() { - if (!cleaned) 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() { @@ -27,34 +65,269 @@ void APS5_VABI QuickFirst() { } void Require(bool value) { if (!value) std::abort(); } void UnexpectedCleanup() { std::_Exit(3); } +#ifdef _WIN32 +void WINAPI UnexpectedDetach(PVOID) { TerminateProcess(GetCurrentProcess(), 4); } +#endif +constexpr char RelaunchMarker[] = "ANYPS5_TEST_RELAUNCH_MARKER"; +constexpr char MainExecutable[] = "ANYPS5_TEST_MAIN_EXECUTABLE"; +void SetVariable(const char* name, const char* value) { +#ifdef _WIN32 + Require(SetEnvironmentVariableA(name, value) != 0); + Require(_putenv_s(name, value) == 0); +#else + Require(setenv(name, value, 1) == 0); +#endif +} +std::filesystem::path OwnExecutable() { +#ifdef _WIN32 + std::wstring executable(32768, L'\0'); + executable.resize(GetModuleFileNameW(nullptr, executable.data(), static_cast(executable.size()))); + return executable; +#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 +} +void AppendMarker(const std::string& line) { + const char* marker = std::getenv(RelaunchMarker); + Require(marker != nullptr); + std::ofstream(marker, std::ios::app) << line << '\n'; +} +void MainShutdown() { + AppendMarker("shutdown"); + 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)); + STARTUPINFOW startup{}; + startup.cb = sizeof(startup); + PROCESS_INFORMATION process{}; + Require(CreateProcessW(self.c_str(), commandLine.data(), nullptr, nullptr, TRUE, 0, nullptr, nullptr, &startup, &process) != 0); + WaitForSingleObject(process.hProcess, INFINITE); + DWORD code = 1; + GetExitCodeProcess(process.hProcess, &code); + CloseHandle(process.hThread); + CloseHandle(process.hProcess); + return static_cast(code); +#else + const pid_t child = fork(); + Require(child >= 0); + if (child == 0) { + execl(self.c_str(), self.c_str(), mode, static_cast(nullptr)); + std::_Exit(127); + } + int status = 0; + Require(waitpid(child, &status, 0) == child); + return WIFEXITED(status) ? WEXITSTATUS(status) : 1; +#endif +} +int DriveNestedExit() { + const auto self = OwnExecutable(); + const auto marker = std::filesystem::temp_directory_path() / ("anyps5-nested-exit-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".txt"); + SetVariable(RelaunchMarker, marker.string().c_str()); + const int code = RunChild(self, "--nested-exit"); + std::string text; + { + std::ifstream file(marker); + text.assign(std::istreambuf_iterator(file), std::istreambuf_iterator()); + } + std::error_code ignored; + std::filesystem::remove(marker, ignored); + Require(code == 7); + Require(text == "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()); + std::filesystem::copy_file(self, secondary, std::filesystem::copy_options::overwrite_existing); + const auto marker = std::filesystem::temp_directory_path() / ("anyps5-relaunch-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".txt"); + SetVariable(RelaunchMarker, marker.string().c_str()); + SetVariable(MainExecutable, self.string().c_str()); + Require(RunChild(self, "--relaunch") == 0); + std::string text; + for (int attempt = 0; attempt < 200 && text.find("relaunched") == std::string::npos; ++attempt) { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + std::ifstream file(marker); + text.assign(std::istreambuf_iterator(file), std::istreambuf_iterator()); + } + std::error_code ignored; + std::filesystem::remove(marker, ignored); + for (int attempt = 0; attempt < 100 && std::filesystem::exists(secondary) && !std::filesystem::remove(secondary, ignored); ++attempt) + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(text == "shutdown\narguments: --secondary|two words|quote\"d\\|\nsecondary shutdown\nrelaunched\n"); + std::puts("Guest relaunch completed"); + return 0; +} + +int 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 + auto slot = FlsAlloc(UnexpectedDetach); + Require(slot != FLS_OUT_OF_INDEXES && FlsSetValue(slot, &slot)); +#endif _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) { + if (argc > firstArgument && std::strcmp(argv[firstArgument], "--nested-exit-driver") == 0) { + return DriveNestedExit(); + } + if (argc > firstArgument && std::strcmp(argv[firstArgument], "--nested-exit") == 0) { + LibcRegisterShutdown_nid_postfix(GuestExitShutdown); + Require(__cxa_atexit_nid_postfix(RemainingHandler, nullptr, nullptr) == 0); + Require(__cxa_atexit_nid_postfix(CheckShutdownStillRunning, nullptr, nullptr) == 0); + Require(__cxa_atexit_nid_postfix(ExitingHandler, nullptr, nullptr) == 0); + exit_nid_postfix(0); + return 2; + } + if (argc > firstArgument && std::strcmp(argv[firstArgument], "--exit-order-driver") == 0) { + return DriveExitOrder(); + } + if (argc > firstArgument && std::strcmp(argv[firstArgument], "--exit-order") == 0) { + LibcRegisterShutdown_nid_postfix(GuestExitShutdown); + Require(__cxa_atexit_nid_postfix(CheckShutdownStillRunning, nullptr, nullptr) == 0); + exit_nid_postfix(0); + return 2; + } + if (argc > firstArgument && std::strcmp(argv[firstArgument], "--relaunch-driver") == 0) { + return DriveRelaunch(); + } + 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 > firstArgument && std::strcmp(argv[firstArgument], "--secondary") == 0) { + Require(OwnExecutable().filename().string().starts_with("picker.picker.bin")); + std::string received = "arguments:"; + 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 > 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 > firstArgument) { LibcRegisterShutdown_nid_postfix(Cleanup); - 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; } Require(sceSystemServiceLoadExec(nullptr, nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); Require(sceSystemServiceLoadExec("", nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); - bool rejected = false; - try { sceSystemServiceLoadExec("/app0/another.bin", nullptr); } - catch (const std::runtime_error&) { rejected = true; } - Require(rejected); + for (const char* path : {"/app0/another.bin", "/data/eboot.bin", "/app0/../eboot.bin", "/app0//eboot.bin"}) { + bool rejected = false; + try { sceSystemServiceLoadExec(path, nullptr); } + catch (const std::runtime_error& error) { rejected = std::string(error.what()).find(path) != std::string::npos; } + Require(rejected); + } +#ifdef _WIN32 + Require(GetEnvironmentVariableA("APS5_EBOOT", nullptr, 0) == 0); +#else + Require(std::getenv("APS5_EBOOT") == nullptr); +#endif } diff --git a/core/libs/tests/GuestSystemFont.cpp b/core/libs/tests/GuestSystemFont.cpp index 40a1c5588a..f7f07156cf 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) { @@ -118,12 +133,70 @@ static void SetFontDirectory(const std::filesystem::path& directory) { #endif } +template +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; @@ -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 { @@ -173,6 +251,9 @@ int main() { Require(sceFontSetScalePixel(font, 100.0f, 100.0f) == SCE_FONT_OK); Require(AdvanceOf(font, 'A') == 70.0f); FontGlyphMetrics metrics{}; + Require(sceFontGetCharGlyphMetrics(font, 'A', &metrics) == SCE_FONT_OK); + Require(metrics.width == 50.0f && metrics.height == 70.0f && metrics.Horizontal.bearingX == 10.0f && metrics.Horizontal.bearingY == 70.0f); + Require(metrics.Vertical.bearingX == -25.0f && metrics.Vertical.bearingY == 15.0f && metrics.Vertical.advance == 100.0f); Require(sceFontGetCharGlyphMetrics(font, 'B', &metrics) == SCE_FONT_ERROR_NO_SUPPORT_GLYPH); FontHorizontalLayout layout{}; Require(sceFontGetHorizontalLayout(font, &layout) == SCE_FONT_OK && layout.baselineOffset > 0.0f && layout.lineAdvance >= layout.baselineOffset); @@ -215,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 new file mode 100644 index 0000000000..01d5123939 --- /dev/null +++ b/core/libs/tests/GuestSystemGesture.cpp @@ -0,0 +1,275 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +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 sceSystemGestureInitializePrimitiveTouchRecognizer(const void*); +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(nullptr) == 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(Throws([&] { sceSystemGestureGetPrimitiveTouchEventByPrimitiveID(handle, 9, &byId); })); + + std::array events{}; + std::uint32_t reported = 7; + Require(sceSystemGestureGetPrimitiveTouchEvents(handle, events.data(), 1, &reported) == 0); + Require(reported == 1); + 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) == 0); + Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle) == 2); + + const std::array sequence{TouchReport(7000, {{3, 110, 200}}), TouchReport(8000, {{3, 130, 210}})}; + Require(UpdateWithReports(handle, sequence.data(), 2) == 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) == 0); +} + +int main() { + constexpr int invalidArgument = static_cast(0x80D10002); + constexpr int invalidHandle = static_cast(0x80D10003); + const std::int32_t handle = sceSystemGestureOpen(0, nullptr); + + std::array buffer; + buffer.fill(0x5a); + const auto original = buffer; + std::uint32_t count = 7; + Require(sceSystemGestureGetPrimitiveTouchEvents(handle, buffer.data(), 4, &count) == 0); + Require(count == 0); + Require(buffer == original); + + count = 7; + Require(sceSystemGestureGetPrimitiveTouchEvents(handle, buffer.data(), 4, nullptr) == invalidArgument); + Require(sceSystemGestureGetPrimitiveTouchEvents(handle + 1, buffer.data(), 4, &count) == invalidHandle); + Require(count == 7); + + Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle) == 0); + Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle + 1) == invalidHandle); + + constexpr int indexOutOfArray = static_cast(0x80D10005); + 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); + Require(sceSystemGestureRemoveTouchRecognizer(handle, &recognizer) == 0); + Require(sceSystemGestureResetTouchRecognizer(handle, &recognizer) == 0); + Require(sceSystemGestureUpdateTouchRecognizer(handle, &recognizer) == 0); + Require(sceSystemGestureUpdateTouchRecognizerRectangle(handle, &recognizer, &rectangle) == 0); + Require(sceSystemGestureResetPrimitiveTouchRecognizer(handle) == 0); + Require(sceSystemGestureUpdateAllTouchRecognizer(handle) == 0); + Require(sceSystemGestureUpdatePrimitiveTouchRecognizer(handle, nullptr) == 0); + Require(sceSystemGestureAppendTouchRecognizer(handle, nullptr) == invalidArgument); + Require(sceSystemGestureUpdateTouchRecognizerRectangle(handle, &recognizer, nullptr) == invalidArgument); + Require(sceSystemGestureAppendTouchRecognizer(handle + 1, &recognizer) == invalidHandle); + Require(sceSystemGestureRemoveTouchRecognizer(handle, nullptr) == invalidArgument); + Require(sceSystemGestureResetTouchRecognizer(handle, nullptr) == invalidArgument); + Require(sceSystemGestureUpdateTouchRecognizer(handle, nullptr) == invalidArgument); + Require(sceSystemGestureUpdateTouchRecognizerRectangle(handle, nullptr, &rectangle) == invalidArgument); + Require(sceSystemGestureRemoveTouchRecognizer(handle + 1, &recognizer) == invalidHandle); + Require(sceSystemGestureResetTouchRecognizer(handle + 1, &recognizer) == invalidHandle); + Require(sceSystemGestureUpdateTouchRecognizer(handle + 1, &recognizer) == invalidHandle); + Require(sceSystemGestureUpdateTouchRecognizerRectangle(handle + 1, &recognizer, &rectangle) == invalidHandle); + Require(sceSystemGestureResetPrimitiveTouchRecognizer(handle + 1) == invalidHandle); + Require(sceSystemGestureUpdateAllTouchRecognizer(handle + 1) == invalidHandle); + Require(sceSystemGestureUpdatePrimitiveTouchRecognizer(handle + 1, nullptr) == invalidHandle); + + TestTouchRecognizerInformation(handle, invalidArgument, invalidHandle); + TestPrimitiveTouchEvents(handle, invalidArgument, invalidHandle, indexOutOfArray); + 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 84d4454e99..113cb6aa2d 100644 --- a/core/libs/tests/GuestSystemService.cpp +++ b/core/libs/tests/GuestSystemService.cpp @@ -7,6 +7,9 @@ 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 { @@ -14,9 +17,19 @@ 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; + } + return false; +} + +bool LaunchAppThrows(const char* titleId, const char* const* arguments, const void* param) { + try { + static_cast(sceSystemServiceLaunchApp(titleId, arguments, param)); } catch (const std::runtime_error&) { return true; } @@ -32,6 +45,38 @@ extern "C" int APS5_VABI sceSystemServiceReenableMusicPlayer(void); extern "C" int APS5_VABI sceSystemServiceDisableMediaPlay(void); extern "C" int APS5_VABI sceSystemServiceReenableMediaPlay(void); extern "C" int APS5_VABI sceSystemServiceLaunchWebBrowser(const char* uri, void* param); +extern "C" int APS5_VABI sceSystemServiceShowControllerSettings(void); +extern "C" int APS5_VABI 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); @@ -47,6 +92,9 @@ int main() { 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)); @@ -54,6 +102,21 @@ int main() { for (unsigned char byte : browserParam) Require(byte == 0x5a); Require(sceSystemServiceLaunchWebBrowser("", nullptr) == browserUnavailable); Require(sceSystemServiceLaunchWebBrowser(nullptr, nullptr) == browserUnavailable); + Require(LaunchAppThrows("PPSA99109", nullptr, nullptr)); + Require(LaunchAppThrows(nullptr, nullptr, nullptr)); + const char* launchArguments[] = {"--backup", nullptr}; + unsigned char launchParam[0x20]; + std::memset(launchParam, 0x5a, sizeof(launchParam)); + 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); @@ -61,13 +124,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 new file mode 100644 index 0000000000..efd37f499b --- /dev/null +++ b/core/libs/tests/GuestSystemServiceLanguage.cpp @@ -0,0 +1,68 @@ +#include "SceTypes.hpp" +#include "prx/libSceSystemService/SystemService.hpp" +#include +#include +#include + +extern "C" int APS5_VABI sceSystemServiceParamGetInt(int paramId, int* value); + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +void SetLanguage(const char* value) { +#ifdef _WIN32 + Require(_putenv_s("ANYPS5_LANGUAGE", value != nullptr ? value : "") == 0); +#else + Require((value != nullptr ? ::setenv("ANYPS5_LANGUAGE", value, 1) : ::unsetenv("ANYPS5_LANGUAGE")) == 0); +#endif +} + +int Language() { + int value = -1; + Require(sceSystemServiceParamGetInt(SYSTEM_SERVICE_PARAM_ID_LANG, &value) == SYSTEM_SERVICE_OK); + int sameValue = -1; + Require(sceSystemServiceParamGetInt(SYSTEM_SERVICE_PARAM_ID_400, &sameValue) == SYSTEM_SERVICE_OK); + Require(sameValue == value); + return value; +} + +bool ParameterRejected(int paramId) { + int language = -1; + try { + static_cast(sceSystemServiceParamGetInt(paramId, &language)); + } catch (const std::runtime_error& error) { + return language == -1 && std::string(error.what()).find("ANYPS5_LANGUAGE") != std::string::npos; + } + return false; +} + +bool Rejected(const char* value) { + SetLanguage(value); + return ParameterRejected(SYSTEM_SERVICE_PARAM_ID_LANG) && ParameterRejected(SYSTEM_SERVICE_PARAM_ID_400); +} + +} + +int main() { + SetLanguage(nullptr); + Require(Language() == SYSTEM_SERVICE_PARAM_LANG_ENGLISH_US); + SetLanguage(""); + Require(Language() == SYSTEM_SERVICE_PARAM_LANG_ENGLISH_US); + SetLanguage("21"); + Require(Language() == SYSTEM_SERVICE_PARAM_LANG_ARABIC); + SetLanguage("0"); + Require(Language() == SYSTEM_SERVICE_PARAM_LANG_JAPANESE); + SetLanguage("30"); + Require(Language() == SYSTEM_SERVICE_PARAM_LANG_UKRAINIAN); + for (const char* value : {"31", "-1", "+21", " 21", "21 ", "21a", "0x15", "arabic", "4294967317"}) { + Require(Rejected(value)); + } + + 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); +} 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/GuestSystemServicePathBoundary.cpp b/core/libs/tests/GuestSystemServicePathBoundary.cpp new file mode 100644 index 0000000000..da8e6927f0 --- /dev/null +++ b/core/libs/tests/GuestSystemServicePathBoundary.cpp @@ -0,0 +1,82 @@ +#include "prx/libc/include/Shutdown.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI sceSystemServiceLoadExec(const char*, const char* const*); + +namespace { + +void Require(bool value) { + if (!value) std::abort(); +} + +[[noreturn]] void UnexpectedLaunch() { + std::_Exit(86); +} + +int Probe(const char* name) { + LibcRegisterShutdown_nid_postfix(UnexpectedLaunch); + const std::string path = std::string("/app0/..\\") + name; + try { + sceSystemServiceLoadExec(path.c_str(), nullptr); + } catch (const std::runtime_error& error) { + return std::string(error.what()).find(path) != std::string::npos ? 0 : 2; + } + return 1; +} + +DWORD RunProbe(const std::filesystem::path& executable, const std::string& name) { + STARTUPINFOW startup{}; + startup.cb = sizeof(startup); + PROCESS_INFORMATION process{}; + std::wstring command = L"\"" + executable.wstring() + L"\" --probe "; + command.append(name.begin(), name.end()); + Require(CreateProcessW(executable.c_str(), command.data(), nullptr, nullptr, TRUE, 0, nullptr, nullptr, &startup, &process) != 0); + const DWORD waitResult = WaitForSingleObject(process.hProcess, 10000); + if (waitResult != WAIT_OBJECT_0) { + TerminateProcess(process.hProcess, ERROR_TIMEOUT); + WaitForSingleObject(process.hProcess, 5000); + CloseHandle(process.hThread); + CloseHandle(process.hProcess); + return ERROR_TIMEOUT; + } + DWORD code = 1; + if (!GetExitCodeProcess(process.hProcess, &code)) code = ERROR_GEN_FAILURE; + CloseHandle(process.hThread); + CloseHandle(process.hProcess); + return code; +} + +int Test() { + std::wstring executable(32768, L'\0'); + const DWORD length = GetModuleFileNameW(nullptr, executable.data(), static_cast(executable.size())); + Require(length > 0 && length < executable.size()); + executable.resize(length); + const std::filesystem::path own(executable); + const std::string name = "anyps5-path-boundary-" + std::to_string(GetCurrentProcessId()) + "-" + std::to_string(GetTickCount64()); + const auto fixture = own.parent_path().parent_path() / (name + own.extension().string()); + struct Cleanup { + std::filesystem::path path; + ~Cleanup() { + std::error_code error; + std::filesystem::remove(path, error); + } + } cleanup{fixture}; + std::filesystem::copy_file(own, fixture); + Require(RunProbe(own, name) == 0); + return 0; +} + +} + +int main(int argc, char** argv) { + if (argc == 3 && std::strcmp(argv[1], "--probe") == 0) return Probe(argv[2]); + Require(argc == 1); + return Test(); +} diff --git a/core/libs/tests/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 f82ec5ab46..2b211538ed 100644 --- a/core/libs/tests/GuestTcp.cpp +++ b/core/libs/tests/GuestTcp.cpp @@ -1,8 +1,12 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include +#include #include +#include #include #include +#include extern "C" { int APS5_VABI socket_nid_postfix(int, int, int); @@ -14,6 +18,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 getpeername_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI shutdown_nid_postfix(int, int); +std::int64_t APS5_VABI recvfrom_nid_postfix(int, void*, std::uint64_t, int, void*, std::uint32_t*); +int APS5_VABI fcntl_nid_postfix(int, int, ...); int APS5_VABI close_nid_postfix(int); int* APS5_VABI __error_nid_postfix(); } @@ -22,7 +29,119 @@ static void Require(bool condition) { if (!condition) std::abort(); } +static void StreamReceiveReportsNoSender() { + 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); + const char text[] = "stream"; + Require(send_nid_postfix(client, text, sizeof(text), 0) == sizeof(text)); + std::array sender; + sender.fill(0xaa); + char received[sizeof(text)]{}; + std::uint32_t senderSize = sender.size(); + Require(recvfrom_nid_postfix(accepted, received, sizeof(received), 0, sender.data(), &senderSize) == sizeof(text)); + Require(std::strcmp(text, received) == 0); + Require(senderSize == 0 && sender[0] == 0xaa && sender[1] == 0xaa); + for (const int descriptor : {client, accepted, listener}) Require(close_nid_postfix(descriptor) == 0); +} + +static void ListenersShareAPortAcrossFamilies() { + const int inet = socket_nid_postfix(2, 1, 0); + Require(inet >= 0); + std::array any{16, 2}; + Require(bind_nid_postfix(inet, any.data(), any.size()) == 0 && listen_nid_postfix(inet, 4) == 0); + std::uint32_t size = any.size(); + Require(getsockname_nid_postfix(inet, any.data(), &size) == 0 && size == any.size()); + const int inet6 = socket_nid_postfix(28, 1, 0); + if (inet6 < 0) { + Require(*__error_nid_postfix() == 47); + Require(close_nid_postfix(inet) == 0); + return; + } + std::array any6{28, 28, any[2], any[3]}; + if (bind_nid_postfix(inet6, any6.data(), any6.size()) != 0) { + if (*__error_nid_postfix() != 49) { + std::fprintf(stderr, "bind of [::]:%d failed with errno %d while an IPv4 listener holds 0.0.0.0:%d\n", + (any[2] << 8) | any[3], *__error_nid_postfix(), (any[2] << 8) | any[3]); + std::abort(); + } + } else { + Require(listen_nid_postfix(inet6, 4) == 0); + } + Require(close_nid_postfix(inet6) == 0); + Require(close_nid_postfix(inet) == 0); +} + +constexpr int GetFlags = 3; +constexpr int SetFlags = 4; +constexpr int NonBlockingFlag = 4; + +static int AcceptWithin(int listener) { + for (int attempt = 0; attempt < 200; ++attempt) { + const int accepted = accept_nid_postfix(listener, nullptr, nullptr); + if (accepted >= 0) return accepted; + Require(*__error_nid_postfix() == 35); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + return -1; +} + +static void AcceptedSocketsFollowListenerBlocking() { + const int listener = socket_nid_postfix(2, 1, 0); + Require(listener >= 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(listener, address.data(), address.size()) == 0 && listen_nid_postfix(listener, 4) == 0); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(listener, address.data(), &size) == 0); + + const int firstClient = socket_nid_postfix(2, 1, 0); + Require(connect_nid_postfix(firstClient, address.data(), address.size()) == 0); + const int blocking = accept_nid_postfix(listener, nullptr, nullptr); + Require(blocking >= 0 && (fcntl_nid_postfix(blocking, GetFlags) & NonBlockingFlag) == 0); + + Require(fcntl_nid_postfix(listener, SetFlags, NonBlockingFlag) == 0); + const int secondClient = socket_nid_postfix(2, 1, 0); + Require(connect_nid_postfix(secondClient, address.data(), address.size()) == 0); + const int inherited = AcceptWithin(listener); + Require(inherited >= 0 && (fcntl_nid_postfix(inherited, GetFlags) & NonBlockingFlag) != 0); + char byte; + Require(recv_nid_postfix(inherited, &byte, 1, 0) == -1 && *__error_nid_postfix() == 35); + Require(fcntl_nid_postfix(inherited, SetFlags, 0) == 0 && (fcntl_nid_postfix(inherited, GetFlags) & NonBlockingFlag) == 0); + Require((fcntl_nid_postfix(listener, GetFlags) & NonBlockingFlag) != 0); + + for (const int descriptor : {firstClient, secondClient, blocking, inherited, listener}) Require(close_nid_postfix(descriptor) == 0); +} + +static void UnboundSocketNameIsZero() { + for (const int family : {2, 28}) { + for (const int type : {1, 2}) { + const int descriptor = socket_nid_postfix(family, type, 0); + Require(descriptor >= 0); + const std::uint32_t expected = family == 2 ? 16 : 28; + std::array name; + name.fill(0xaa); + std::uint32_t size = name.size(); + Require(getsockname_nid_postfix(descriptor, name.data(), &size) == 0); + Require(size == expected && name[0] == expected && name[1] == family); + for (std::uint32_t index = 2; index < expected; ++index) Require(name[index] == 0); + Require(name[expected] == 0xaa); + Require(close_nid_postfix(descriptor) == 0); + } + } +} + int main() { + StreamReceiveReportsNoSender(); + ListenersShareAPortAcrossFamilies(); + AcceptedSocketsFollowListenerBlocking(); + UnboundSocketNameIsZero(); const int listener = socket_nid_postfix(2, 1, 6); Require(listener >= 0); std::array address{16, 2, 0, 0, 127, 0, 0, 1}; @@ -47,12 +166,37 @@ int main() { Require(getpeername_nid_postfix(accepted, connected_peer.data(), &address_size) == 0); Require(address_size == 16 && connected_peer[1] == 2); + for (auto query : {getsockname_nid_postfix, getpeername_nid_postfix}) { + std::array expected{}; + address_size = expected.size(); + Require(query(accepted, expected.data(), &address_size) == 0 && address_size == expected.size()); + for (std::uint32_t capacity : {0u, 1u, 2u, 15u, 16u, 28u, UINT32_MAX}) { + std::array actual; + actual.fill(0xa5); + address_size = capacity; + const auto copied = std::min(capacity, std::uint32_t{16}); + Require(query(accepted, actual.data(), &address_size) == 0 && address_size == copied); + Require(std::memcmp(actual.data(), expected.data(), copied) == 0); + Require(std::all_of(actual.begin() + copied, actual.end(), [](auto byte) { return byte == 0xa5; })); + } + } + const int shortClient = socket_nid_postfix(2, 1, 0); + Require(shortClient >= 0 && connect_nid_postfix(shortClient, address.data(), address.size()) == 0); + peer.fill(0xa5); + address_size = 1; + const int shortAccepted = accept_nid_postfix(listener, peer.data(), &address_size); + Require(shortAccepted >= 0 && address_size == 1 && peer[0] == 16); + Require(std::all_of(peer.begin() + 1, peer.end(), [](auto byte) { return byte == 0xa5; })); + Require(close_nid_postfix(shortAccepted) == 0 && close_nid_postfix(shortClient) == 0); + const char request[] = "guest TCP loopback"; char received[sizeof(request)]{}; Require(send_nid_postfix(client, request, sizeof(request), 0x1) == -1 && *__error_nid_postfix() == 45); Require(send_nid_postfix(client, request, sizeof(request), 0x20000) == sizeof(request)); Require(recv_nid_postfix(accepted, received, sizeof(received), 0) == sizeof(received)); Require(std::strcmp(request, received) == 0); + Require(shutdown_nid_postfix(client, 1) == 0); + Require(send_nid_postfix(client, request, sizeof(request), 0x20000) == -1 && *__error_nid_postfix() == 32); Require(close_nid_postfix(accepted) == 0); #ifndef _WIN32 @@ -63,4 +207,4 @@ int main() { Require(close_nid_postfix(client) == 0); Require(close_nid_postfix(listener) == 0); Require(close_nid_postfix(listener) == -1 && *__error_nid_postfix() == 9); -} \ No newline at end of file +} diff --git a/core/libs/tests/GuestTerminal.cpp b/core/libs/tests/GuestTerminal.cpp index 19653acc33..ad27b96a30 100644 --- a/core/libs/tests/GuestTerminal.cpp +++ b/core/libs/tests/GuestTerminal.cpp @@ -5,14 +5,23 @@ #include #include #include +#include #include extern "C" { int APS5_VABI tcgetattr_nid_postfix(int, void*); int APS5_VABI tcsetattr_nid_postfix(int, int, const void*); +int APS5_VABI tcsendbreak_nid_postfix(int, int); +int APS5_VABI tcdrain_nid_postfix(int); +int APS5_VABI tcflush_nid_postfix(int, int); +int APS5_VABI tcflow_nid_postfix(int, int); +int APS5_VABI tcgetpgrp_nid_postfix(int); +int APS5_VABI tcsetpgrp_nid_postfix(int, int); int APS5_VABI open_nid_postfix(const char*, int, int); int APS5_VABI pipe_nid_postfix(int*); int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI socketpair_nid_postfix(int, int, int, int*); +int APS5_VABI mkdir_nid_postfix(const char*, unsigned short); int APS5_VABI close_nid_postfix(int); int* APS5_VABI __error_nid_postfix(); } @@ -34,9 +43,45 @@ constexpr int BadDescriptor = 9; constexpr int InvalidArgument = 22; constexpr int BadAddress = 14; constexpr int AddressNotAvailable = 49; +constexpr int OperationNotSupported = 45; static bool Fails(int result, int error) { return result == -1 && *__error_nid_postfix() == error; } +static void RequireControlFails(int descriptor, int error) { + *__error_nid_postfix() = 0; + Require(Fails(tcsendbreak_nid_postfix(descriptor, 0), error)); + *__error_nid_postfix() = 0; + Require(Fails(tcsendbreak_nid_postfix(descriptor, 5), error)); + *__error_nid_postfix() = 0; + Require(Fails(tcdrain_nid_postfix(descriptor), error)); + for (const int queue : {1, 2, 3}) { + *__error_nid_postfix() = 0; + Require(Fails(tcflush_nid_postfix(descriptor, queue), error)); + } + for (const int action : {1, 2, 3, 4}) { + *__error_nid_postfix() = 0; + Require(Fails(tcflow_nid_postfix(descriptor, action), error)); + } +} + +static void RequireProcessGroupFails(int descriptor, int error) { + *__error_nid_postfix() = 0; + Require(Fails(tcgetpgrp_nid_postfix(descriptor), error)); + for (const int group : {0, 1}) { + *__error_nid_postfix() = 0; + Require(Fails(tcsetpgrp_nid_postfix(descriptor, group), error)); + } +} + +template static bool Throws(TCall call) { + try { + call(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + static void RequireNotTerminal(int descriptor) { unsigned char attributes[44]; std::memset(attributes, 0xaa, sizeof(attributes)); @@ -49,6 +94,7 @@ static void RequireNotTerminal(int descriptor) { *__error_nid_postfix() = 0; Require(Fails(tcsetattr_nid_postfix(descriptor, action, attributes), NotTerminal)); } + RequireControlFails(descriptor, NotTerminal); } int main() { @@ -56,14 +102,19 @@ int main() { const int file = open_nid_postfix(name.c_str(), 0x202, 0644); Require(file >= 0); RequireNotTerminal(file); - RequireNotTerminal(0); - RequireNotTerminal(1); - RequireNotTerminal(2); + RequireProcessGroupFails(file, NotTerminal); + for (const int stream : {0, 1, 2}) { + RequireNotTerminal(stream); + RequireProcessGroupFails(stream, NotTerminal); + } int ends[2] = {-1, -1}; Require(pipe_nid_postfix(ends) == 0); - RequireNotTerminal(ends[0]); - RequireNotTerminal(ends[1]); + for (const int end : ends) { + RequireNotTerminal(end); + Require(Throws([end] { tcgetpgrp_nid_postfix(end); })); + Require(Throws([end] { tcsetpgrp_nid_postfix(end, 0); })); + } const int socket = socket_nid_postfix(2, 2, 0); Require(socket >= 0); @@ -74,6 +125,27 @@ int main() { Require(Fails(tcgetattr_nid_postfix(socket, probe), AddressNotAvailable)); Require(std::memcmp(probe, untouched, sizeof(probe)) == 0); Require(Fails(tcsetattr_nid_postfix(socket, Flush, probe), AddressNotAvailable)); + RequireControlFails(socket, AddressNotAvailable); + RequireProcessGroupFails(socket, AddressNotAvailable); + + int pair[2] = {-1, -1}; + Require(socketpair_nid_postfix(1, 1, 0, pair) == 0); + for (const int end : pair) { + Require(Fails(tcgetattr_nid_postfix(end, probe), OperationNotSupported)); + RequireControlFails(end, OperationNotSupported); + RequireProcessGroupFails(end, OperationNotSupported); + } + Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0); + + const auto directoryName = name + "-directory"; + Require(mkdir_nid_postfix(directoryName.c_str(), 0755) == 0); + const int directory = open_nid_postfix(directoryName.c_str(), 0x00020000, 0); + Require(directory >= 0); + RequireNotTerminal(directory); + RequireProcessGroupFails(directory, NotTerminal); + Require(close_nid_postfix(directory) == 0); + RequireControlFails(directory, BadDescriptor); + std::filesystem::remove(directoryName); unsigned char attributes[44]{}; Require(Fails(tcsetattr_nid_postfix(file, 3, attributes), InvalidArgument)); @@ -84,14 +156,32 @@ int main() { Require(Fails(tcsetattr_nid_postfix(-1, Now, nullptr), BadAddress)); Require(Fails(tcgetattr_nid_postfix(-1, attributes), BadDescriptor)); Require(Fails(tcsetattr_nid_postfix(-1, Now, attributes), BadDescriptor)); + for (const int descriptor : {file, -1}) { + for (const int queue : {0, 4, -1}) { + *__error_nid_postfix() = 0; + Require(Fails(tcflush_nid_postfix(descriptor, queue), InvalidArgument)); + } + for (const int action : {0, 5, -1}) { + *__error_nid_postfix() = 0; + Require(Fails(tcflow_nid_postfix(descriptor, action), InvalidArgument)); + } + } + RequireControlFails(-1, BadDescriptor); + RequireProcessGroupFails(-1, BadDescriptor); Require(close_nid_postfix(socket) == 0); Require(Fails(tcgetattr_nid_postfix(socket, attributes), BadDescriptor)); Require(Fails(tcsetattr_nid_postfix(socket, Now, attributes), BadDescriptor)); + RequireControlFails(socket, BadDescriptor); + RequireProcessGroupFails(socket, BadDescriptor); Require(close_nid_postfix(ends[0]) == 0 && close_nid_postfix(ends[1]) == 0); + RequireControlFails(ends[0], BadDescriptor); + RequireProcessGroupFails(ends[0], BadDescriptor); Require(close_nid_postfix(file) == 0); Require(Fails(tcgetattr_nid_postfix(file, attributes), BadDescriptor)); Require(Fails(tcsetattr_nid_postfix(file, Flush, attributes), BadDescriptor)); + RequireControlFails(file, BadDescriptor); + RequireProcessGroupFails(file, BadDescriptor); std::filesystem::remove(name); return 0; } diff --git a/core/libs/tests/GuestTextToSpeech2.cpp b/core/libs/tests/GuestTextToSpeech2.cpp index f3ee2c80e6..9bac0096e0 100644 --- a/core/libs/tests/GuestTextToSpeech2.cpp +++ b/core/libs/tests/GuestTextToSpeech2.cpp @@ -3,27 +3,30 @@ #include #include #include -#include extern "C" { std::int32_t APS5_VABI sceTextToSpeech2Initialize(const void* param); int APS5_VABI sceTextToSpeech2Open(); +int APS5_VABI sceTextToSpeech2Cancel(); +int APS5_VABI sceTextToSpeech2Close(); +int APS5_VABI sceTextToSpeech2GetSpeechStatus(); +int APS5_VABI sceTextToSpeech2Speak(); +int APS5_VABI sceTextToSpeech2Terminate(); +int APS5_VABI sceTextToSpeech2GetSystemStatus(); } int main() { + constexpr std::int32_t unsupported = static_cast(0x8002002D); const std::uint32_t param[12]{0x2000000, 0x26c}; - if (sceTextToSpeech2Initialize(param) != static_cast(0x8002002D)) { + if (sceTextToSpeech2Initialize(param) != unsupported) { std::puts("TextToSpeech2: initialization did not report the unsupported operation"); std::abort(); } - bool threw = false; - try { - sceTextToSpeech2Open(); - } catch (const std::runtime_error&) { - threw = true; - } - if (!threw) { - std::puts("TextToSpeech2: open after failed initialization did not throw"); + if (sceTextToSpeech2Open() != unsupported || sceTextToSpeech2Cancel() != unsupported || + sceTextToSpeech2Close() != unsupported || sceTextToSpeech2GetSpeechStatus() != unsupported || + sceTextToSpeech2Speak() != unsupported || sceTextToSpeech2Terminate() != unsupported || + sceTextToSpeech2GetSystemStatus() != unsupported) { + std::puts("TextToSpeech2: later calls did not report the unsupported operation"); std::abort(); } std::puts("TextToSpeech2 tests passed"); diff --git a/core/libs/tests/GuestThermalSensors.cpp b/core/libs/tests/GuestThermalSensors.cpp new file mode 100644 index 0000000000..3c051911a6 --- /dev/null +++ b/core/libs/tests/GuestThermalSensors.cpp @@ -0,0 +1,88 @@ +#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) { + if (!value) { + std::fprintf(stderr, "thermal sensors check failed\n"); + std::abort(); + } +} + +int main() { + const auto root = std::filesystem::path("anyps5-thermal-sensors-test"); + std::filesystem::remove_all(root); + std::filesystem::create_directories(root / "hwmon" / "hwmon0"); + std::filesystem::create_directories(root / "hwmon" / "hwmon1"); + std::filesystem::create_directories(root / "thermal" / "thermal_zone0"); + std::ofstream(root / "hwmon" / "hwmon0" / "name") << "k10temp\n"; + std::ofstream(root / "hwmon" / "hwmon0" / "temp1_input") << "45200\n"; + std::ofstream(root / "hwmon" / "hwmon1" / "name") << "nvme\n"; + std::ofstream(root / "hwmon" / "hwmon1" / "temp1_input") << "39800\n"; + std::ofstream(root / "thermal" / "thermal_zone0" / "type") << "acpitz\n"; + std::ofstream(root / "thermal" / "thermal_zone0" / "temp") << "28000\n"; + + int celsius[3] = {0, 0, 0}; + ReadThermalSensors_nid_no_patch(root.string().c_str(), celsius); + Require(celsius[0] == 45 && celsius[1] == 28 && celsius[2] == 39); + + int missing[3] = {0, 0, 0}; + ReadThermalSensors_nid_no_patch((root / "absent").string().c_str(), missing); + Require(missing[0] == -1 && missing[1] == -1 && missing[2] == -1); + ReadThermalSensors_nid_no_patch(nullptr, missing); + Require(missing[0] == -1); + + const auto skipped = std::filesystem::path("anyps5-thermal-sensors-skip"); + std::filesystem::remove_all(skipped); + std::filesystem::create_directories(skipped / "hwmon" / "hwmonB"); + std::filesystem::create_directories(skipped / "hwmon" / "hwmonA"); + std::filesystem::create_directories(skipped / "hwmon" / "hwmonC"); + std::ofstream(skipped / "hwmon" / "hwmonA" / "name") << "amdgpu\n"; + std::ofstream(skipped / "hwmon" / "hwmonA" / "temp1_input") << "55000\n"; + std::ofstream(skipped / "hwmon" / "hwmonB" / "name") << "BAT0\n"; + std::ofstream(skipped / "hwmon" / "hwmonB" / "temp1_input") << "31600\n"; + std::ofstream(skipped / "hwmon" / "hwmonC" / "name") << "iwlwifi_1\n"; + std::ofstream(skipped / "hwmon" / "hwmonC" / "temp1_input") << "57000\n"; + int fallback[3] = {0, 0, 0}; + ReadThermalSensors_nid_no_patch(skipped.string().c_str(), fallback); + Require(fallback[0] == 55 && fallback[1] == -1 && fallback[2] == -1); + std::filesystem::remove_all(skipped); + + int value = 0; + Require(sceKernelGetCpuTemperature(nullptr) == SCE_KERNEL_ERROR_EFAULT); + Require(sceKernelGetSocSensorTemperature(0, nullptr) == SCE_KERNEL_ERROR_EFAULT); + Require(sceKernelGetSocSensorTemperature(-1, &value) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelGetSocSensorTemperature(3, &value) == SCE_KERNEL_ERROR_EINVAL); + + int sentinel = -12345; + Require(sceKernelGetSocSensorTemperature(15, &sentinel) == SCE_KERNEL_ERROR_EINVAL && sentinel == -12345); + int probe = -12345; + const int result = sceKernelGetSocSensorTemperature(0, &probe); + Require(result == 0 ? (probe >= 0 && probe <= 130) : (result == SCE_KERNEL_ERROR_EINVAL && probe == -12345)); + 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/GuestThreadCallbackAbi.cpp b/core/libs/tests/GuestThreadCallbackAbi.cpp index a496f88512..4fe7aa04ca 100644 --- a/core/libs/tests/GuestThreadCallbackAbi.cpp +++ b/core/libs/tests/GuestThreadCallbackAbi.cpp @@ -14,6 +14,7 @@ using GuestDestructor = void(APS5_VABI *)(void*); extern "C" { int APS5_VABI __cxa_thread_atexit_impl_nid_postfix(GuestDestructor, void*, void*); +int APS5_VABI __cxa_thread_atexit_nid_postfix(GuestDestructor, void*, void*); int APS5_VABI LibcInternalExtCxaThreadAtexit_nid_postfix(GuestDestructor, void*, void*); } @@ -24,7 +25,7 @@ struct Call { void* object; }; -std::array calls{}; +std::array calls{}; std::atomic callCount{0}; std::atomic registrationFailed{false}; @@ -37,6 +38,7 @@ void Record(int id, void* object) { void APS5_VABI FirstDestructor(void* object) { Record(1, object); } void APS5_VABI SecondDestructor(void* object) { Record(2, object); } void APS5_VABI ThirdDestructor(void* object) { Record(3, object); } +void APS5_VABI FourthDestructor(void* object) { Record(4, object); } void Fail(const char* message) { std::fprintf(stderr, "%s\n", message); } @@ -55,13 +57,11 @@ int main() { } void* directDso = module; -#ifdef _WIN32 - directDso = nullptr; -#endif int firstObject = 1; int secondObject = 2; int thirdObject = 3; + int fourthObject = 4; std::thread worker([&] { if (__cxa_thread_atexit_impl_nid_postfix(FirstDestructor, &firstObject, directDso) != 0) registrationFailed.store(true); @@ -69,6 +69,8 @@ int main() { registrationFailed.store(true); if (LibcInternalExtCxaThreadAtexit_nid_postfix(ThirdDestructor, &thirdObject, module) != 0) registrationFailed.store(true); + if (__cxa_thread_atexit_nid_postfix(FourthDestructor, &fourthObject, directDso) != 0) + registrationFailed.store(true); }); worker.join(); @@ -84,7 +86,7 @@ int main() { Fail("thread exit did not run each registered destructor once"); return 1; } - const std::array expected = {{{3, &thirdObject}, {2, &secondObject}, {1, &firstObject}}}; + const std::array expected = {{{4, &fourthObject}, {3, &thirdObject}, {2, &secondObject}, {1, &firstObject}}}; for (std::size_t index = 0; index < expected.size(); ++index) { if (calls[index].id != expected[index].id || calls[index].object != expected[index].object) { std::fprintf(stderr, "thread destructor %d received object %p; expected %p\n", calls[index].id, diff --git a/core/libs/tests/GuestThreadCpuClock.cpp b/core/libs/tests/GuestThreadCpuClock.cpp index 058ed0969b..15946c4aec 100644 --- a/core/libs/tests/GuestThreadCpuClock.cpp +++ b/core/libs/tests/GuestThreadCpuClock.cpp @@ -12,11 +12,13 @@ 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 scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +int APS5_VABI sceKernelClockGettime(KernelClockid clockId, KernelTimespec* tp); int* APS5_VABI __error_nid_postfix(); } static constexpr int GUEST_EFAULT = 14; static constexpr int GUEST_EINVAL = 22; +static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); static constexpr int GUEST_CLOCK_MONOTONIC = 4; static constexpr int GUEST_CLOCK_THREAD_CPUTIME_ID = 14; static constexpr std::int64_t NANOS_PER_SECOND = 1000000000LL; @@ -100,6 +102,9 @@ static void OtherThreadClockReadsThatThread() { *__error_nid_postfix() = 0; Require(clock_gettime_nid_postfix(clockId, &time) == -1); Require(*__error_nid_postfix() == GUEST_EINVAL); + *__error_nid_postfix() = 0; + Require(sceKernelClockGettime(clockId, &time) == SCE_KERNEL_ERROR_EINVAL); + Require(*__error_nid_postfix() == GUEST_EINVAL); } int main() { diff --git a/core/libs/tests/GuestThreadSanitizer.cpp b/core/libs/tests/GuestThreadSanitizer.cpp new file mode 100644 index 0000000000..4403a31564 --- /dev/null +++ b/core/libs/tests/GuestThreadSanitizer.cpp @@ -0,0 +1,9 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include + +extern "C" int APS5_VABI sceKernelIsThreadSanitizerEnabled(void); + +int main() { + if (sceKernelIsThreadSanitizerEnabled() != 0) std::abort(); + return 0; +} diff --git a/core/libs/tests/GuestThreadSelf.cpp b/core/libs/tests/GuestThreadSelf.cpp index 1358c50459..87e38f6c0a 100644 --- a/core/libs/tests/GuestThreadSelf.cpp +++ b/core/libs/tests/GuestThreadSelf.cpp @@ -1,17 +1,30 @@ #include "SceTypes.hpp" #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); int APS5_VABI scePthreadJoin(Pthread thread, void** retval); int APS5_VABI scePthreadDetach(Pthread thread); +int APS5_VABI pthread_join_nid_postfix(Pthread thread, void** value); +int APS5_VABI pthread_detach_nid_postfix(Pthread thread); void APS5_VABI scePthreadExit(void* retval); Pthread APS5_VABI scePthreadSelf(); +int APS5_VABI scePthreadGetthreadid(void); +int APS5_VABI pthread_getthreadid_np_nid_postfix(void); void APS5_VABI scePthreadTestcancel(); void APS5_VABI pthread_testcancel_nid_postfix(void); int APS5_VABI scePthreadSetcancelstate(int state, int* old_state); int APS5_VABI scePthreadSetcanceltype(int type, int* old_type); +void APS5_VABI pthread_set_name_np_nid_postfix(Pthread thread, const char* name); +int APS5_VABI scePthreadGetname(Pthread thread, char* name); int APS5_VABI scePthreadMutexattrInit(PthreadMutexattr* attr); int APS5_VABI scePthreadMutexattrDestroy(PthreadMutexattr* attr); int APS5_VABI scePthreadMutexattrSettype(PthreadMutexattr* attr, int type); @@ -24,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) { @@ -41,6 +56,9 @@ static bool StackContains(Pthread thread, const void* address) { static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_EINVAL = 0x80020016; static constexpr int SCE_KERNEL_ERROR_EPERM = 0x80020001; +static constexpr int SCE_KERNEL_ERROR_EDEADLK = 0x8002000b; +static constexpr int GUEST_EDEADLK = 11; +static constexpr int GUEST_EINVAL = 22; static constexpr int MUTEX_TYPE_RECURSIVE = 2; static constexpr int PTHREAD_CANCEL_ENABLE = 0; static constexpr int PTHREAD_CANCEL_ASYNCHRONOUS = 2; @@ -48,21 +66,103 @@ 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)); + return tid; +} + struct WorkerContext { Pthread thread = nullptr; Pthread selfFromWorker = nullptr; + std::int64_t workerTid = 0; + int workerThreadId = 0; + int workerPosixThreadId = 0; bool workerStackReported = false; PthreadMutex* mutex = nullptr; int unlockResult = 0; bool testcancelReturned = false; + int sceJoinSelf = 0; + int posixJoinSelf = 0; }; static void* APS5_VABI Worker(void* arg) { auto& context = *static_cast(arg); context.selfFromWorker = scePthreadSelf(); + context.workerTid = ThreadIdField(context.selfFromWorker); + context.workerThreadId = scePthreadGetthreadid(); + context.workerPosixThreadId = pthread_getthreadid_np_nid_postfix(); int local = 0; context.workerStackReported = StackContains(context.selfFromWorker, &local); context.unlockResult = scePthreadMutexUnlock(context.mutex); + context.sceJoinSelf = scePthreadJoin(context.selfFromWorker, nullptr); + context.posixJoinSelf = pthread_join_nid_postfix(context.selfFromWorker, nullptr); int oldState = -1; int oldType = -1; context.testcancelReturned = scePthreadSetcancelstate(PTHREAD_CANCEL_ENABLE, &oldState) == SCE_OK && @@ -78,10 +178,24 @@ int main() { const Pthread mainSelf = scePthreadSelf(); Require(mainSelf != nullptr); Require(scePthreadSelf() == mainSelf); + const std::int64_t mainTid = ThreadIdField(mainSelf); + Require(mainTid != 0); + Require(scePthreadGetthreadid() == static_cast(mainTid)); + 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); + Require(pthread_join_nid_postfix(nullptr, nullptr) == GUEST_EINVAL); + Require(pthread_detach_nid_postfix(nullptr) == GUEST_EINVAL); PthreadMutexattr attr = nullptr; Require(scePthreadMutexattrInit(&attr) == SCE_OK); @@ -104,7 +218,12 @@ int main() { Require(context.selfFromWorker != nullptr); Require(context.selfFromWorker == context.thread); Require(context.selfFromWorker != mainSelf); + Require(context.workerTid != 0 && context.workerTid != mainTid); + Require(context.workerThreadId == static_cast(context.workerTid)); + Require(context.workerPosixThreadId == context.workerThreadId && context.workerPosixThreadId != static_cast(mainTid)); Require(context.unlockResult == SCE_KERNEL_ERROR_EPERM); + Require(context.sceJoinSelf == SCE_KERNEL_ERROR_EDEADLK); + Require(context.posixJoinSelf == GUEST_EDEADLK); Require(context.workerStackReported); Require(context.testcancelReturned); @@ -113,4 +232,12 @@ int main() { Require(scePthreadMutexUnlock(&mutex) == SCE_OK); Require(scePthreadMutexDestroy(&mutex) == SCE_OK); + + char name[32] = {'x'}; + pthread_set_name_np_nid_postfix(mainSelf, "q2-main"); + Require(scePthreadGetname(mainSelf, name) == SCE_OK && std::strcmp(name, "q2-main") == 0); + pthread_set_name_np_nid_postfix(mainSelf, nullptr); + Require(scePthreadGetname(mainSelf, name) == SCE_OK && name[0] == '\0'); + pthread_set_name_np_nid_postfix(nullptr, "ignored"); + } diff --git a/core/libs/tests/GuestTimeConversion.cpp b/core/libs/tests/GuestTimeConversion.cpp index 6cd199d707..7b11ebe2ef 100644 --- a/core/libs/tests/GuestTimeConversion.cpp +++ b/core/libs/tests/GuestTimeConversion.cpp @@ -8,7 +8,10 @@ #include #include #include +#include #include +#include +#include #include extern "C" { @@ -18,8 +21,18 @@ GuestTm* APS5_VABI libc_localtime_nid_postfix(const std::int64_t*); GuestTm* APS5_VABI localtime_nid_postfix(const std::int64_t*); GuestTm* APS5_VABI gmtime_s_nid_postfix(const std::int64_t*, GuestTm*); GuestTm* APS5_VABI localtime_s_nid_postfix(const std::int64_t*, GuestTm*); +char* APS5_VABI asctime_nid_postfix(const GuestTm*); std::int64_t APS5_VABI mktime_nid_postfix(GuestTm*); std::size_t APS5_VABI strftime_nid_postfix(char*, std::size_t, const char*, const GuestTm*); +void APS5_VABI tzset_nid_postfix(void); +extern char* tzname_nid_postfix[2]; +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); } using Converter = GuestTm* (APS5_VABI *)(const std::int64_t*); @@ -49,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; @@ -64,6 +77,17 @@ static void CheckConcurrent(Converter convert, BufferedConverter buffered, const Require(convert(&invalid) == nullptr, "Unrepresentable time was accepted"); } +static GuestTm UtcTime(std::int64_t timer) { + GuestTm utc{}; + Require(gmtime_s_nid_postfix(&timer, &utc) == &utc, "UTC conversion failed"); + return utc; +} + +static void CheckAsctime(const GuestTm& brokenDown, const char* expected) { + const char* text = asctime_nid_postfix(&brokenDown); + Require(text != nullptr && std::strcmp(text, expected) == 0, "asctime did not format the broken-down time"); +} + struct UtcCase { std::int64_t timer; int year; @@ -88,14 +112,321 @@ static void CheckUtc(const UtcCase& expected) { Require(utc.tm_zone != nullptr && std::strcmp(utc.tm_zone, "UTC") == 0, "UTC conversion did not set tm_zone"); } -int main() { +struct DifferenceCase { + std::int64_t time1; + std::int64_t time0; + double seconds; +}; + +static void CheckDifference(const DifferenceCase& expected) { + Require(libc_difftime_nid_postfix(expected.time1, expected.time0) == expected.seconds + && difftime_nid_postfix(expected.time1, expected.time0) == expected.seconds, "difftime did not return time1 - time0"); +} + +static void CheckFormat(const GuestTm& guest, const char* format, const char* expected) { + char buffer[96]{}; + const std::size_t size = strftime_nid_postfix(buffer, sizeof(buffer), format, &guest); + if (size != std::strlen(expected) || std::strcmp(buffer, expected) != 0) + std::fprintf(stderr, "Format '%s': expected '%s', got '%s' (%zu bytes)\n", format, expected, buffer, size); + Require(size == std::strlen(expected) && std::strcmp(buffer, expected) == 0, "strftime did not format a FreeBSD conversion"); +} + +static void CheckBufferSize(const GuestTm& guest, const char* format, std::size_t length) { + char buffer[96]; + std::memset(buffer, 'x', sizeof(buffer)); + Require(strftime_nid_postfix(buffer, length + 1, format, &guest) == length, "strftime rejected an exact-fit buffer"); + Require(std::strlen(buffer) == length, "strftime did not terminate an exact-fit buffer"); + Require(strftime_nid_postfix(buffer, length, format, &guest) == 0, "strftime accepted a buffer one byte short"); + Require(strftime_nid_postfix(buffer, 1, format, &guest) == 0, "strftime accepted a one byte buffer"); +} + +static const char* HostZoneName(int isdst) { #ifdef _WIN32 - _putenv_s("TZ", "UTC-2"); - _tzset(); + return _tzname[isdst != 0 ? 1 : 0]; #else - setenv("TZ", "UTC-2", 1); - tzset(); + return tzname[isdst != 0 ? 1 : 0]; #endif +} + +static void CheckFreeBsdConversions() { + const std::int64_t timer = 1700000000; + GuestTm guest{}; + Require(gmtime_s_nid_postfix(&timer, &guest) == &guest, "UTC conversion failed"); + CheckFormat(guest, "%k|%l", "22|10"); + CheckFormat(guest, "%v", "14-Nov-2023"); + CheckFormat(guest, "%+", "Tue Nov 14 22:13:20 UTC 2023"); + CheckFormat(guest, "%s", "1699992800"); + CheckFormat(guest, "%%k %%l %%s %%v %%+", "%k %l %s %v %+"); + CheckFormat(guest, "%%%k", "%22"); + CheckFormat(guest, "[%k][%l]", "[22][10]"); + CheckFormat(guest, "%Ek|%Ol|%Os|%Ev|%O+", "22|10|1699992800|14-Nov-2023|Tue Nov 14 22:13:20 UTC 2023"); + CheckFormat(guest, "%E%k", "%k"); + CheckFormat(guest, "%O%l", "%l"); + CheckFormat(guest, "%E%s", "%s"); + CheckFormat(guest, "%E%v", "%v"); + CheckFormat(guest, "%O%+", "%+"); + CheckFormat(guest, "%k|%E%l|%l", "22|%l|10"); + CheckFormat(guest, "%E%", "%"); + CheckFormat(guest, "%O%%k", "%22"); + CheckFormat(guest, "%E%k%k", "%k22"); + CheckBufferSize(guest, "%k", 2); + CheckBufferSize(guest, "%k:%l", 5); + CheckBufferSize(guest, "%+", 28); + CheckBufferSize(guest, "%Ek", 2); + + guest.tm_zone = "XYZ"; + CheckFormat(guest, "%+", "Tue Nov 14 22:13:20 XYZ 2023"); + CheckFormat(guest, "%O+", "Tue Nov 14 22:13:20 XYZ 2023"); + guest.tm_zone = "A%B"; + CheckFormat(guest, "%+", "Tue Nov 14 22:13:20 A%B 2023"); + guest.tm_zone = nullptr; + guest.tm_isdst = 0; + char expected[96]; + std::snprintf(expected, sizeof(expected), "Tue Nov 14 22:13:20 %s 2023", HostZoneName(0)); + CheckFormat(guest, "%+", expected); + guest.tm_isdst = 1; + std::snprintf(expected, sizeof(expected), "Tue Nov 14 22:13:20 %s 2023", HostZoneName(1)); + CheckFormat(guest, "%+", expected); + guest.tm_isdst = 0; + + GuestTm local{}; + Require(localtime_s_nid_postfix(&timer, &local) == &local, "Local conversion failed"); + GuestTm normalized = local; + const std::int64_t mktimeResult = mktime_nid_postfix(&normalized); + Require(mktimeResult == timer, "mktime did not invert localtime"); + std::snprintf(expected, sizeof(expected), "%lld", static_cast(mktimeResult)); + CheckFormat(local, "%s", expected); + CheckFormat(local, "%Os", expected); + + guest.tm_hour = 0; + CheckFormat(guest, "%k|%l", " 0|12"); + guest.tm_hour = 12; + CheckFormat(guest, "%k|%l", "12|12"); + guest.tm_hour = 5; + guest.tm_mday = 5; + CheckFormat(guest, "%k|%l|%v", " 5| 5| 5-Nov-2023"); + guest.tm_hour = 13; + CheckFormat(guest, "%k|%l", "13| 1"); + char tooSmall[4]{}; + Require(strftime_nid_postfix(tooSmall, sizeof(tooSmall), "%k:%l", &guest) == 0, "strftime accepted an undersized buffer"); +} + +static void CheckLiteralConversions() { + const std::int64_t timer = 1700000000; + GuestTm guest{}; + Require(gmtime_s_nid_postfix(&timer, &guest) == &guest, "UTC conversion failed"); + struct FormatCase { + const char* format; + const char* expected; + }; + constexpr FormatCase cases[]{ + {"%Q", "Q"}, + {"%P|%f|%N|%1|%#Y", "P|f|N|1|#Y"}, + {"[%Q] %Y-%m-%d", "[Q] 2023-11-14"}, + {"%EQ|%OQ|%_Q|%-Q|%0Q", "Q|Q|Q|Q|Q"}, + {"%E_Q|%_EQ|%O0Q|%-OQ", "Q|Q|Q|Q"}, + {"%EEY|%EOY|%OEY|%OOY", "EY|OY|EY|OY"}, + {"%EE%Y|%OO%k", "E2023|O22"}, + {"%--Y|%__Y|%00Y|%_0Y|%0-Y", "-Y|_Y|0Y|0Y|-Y"}, + {"%E_EY|%_EOY|%O0OY", "EY|OY|OY"}, + {"%", "%"}, + {"text%", "text%"}, + {"%E", "E"}, + {"%O", "O"}, + {"%_", "_"}, + {"%-", "-"}, + {"%0", "0"}, + {"%E_", "_"}, + {"%_E", "E"}, + {"%Q%", "Q%"}, + {"%k%", "22%"}, + {"%%Q|%%%Q|%%%%Q", "%Q|%Q|%%Q"}, + {"%E%Q|%O%Q|%_%Q|%0%Q|%-%Q", "%Q|%Q|%Q|%Q|%Q"}, + {"%E%%Q|%O%%%Q", "%Q|%%Q"}, + {"%Q|%Ek|%Ol|%Ev|%Y", "Q|22|10|14-Nov-2023|2023"}, + {"%\xc3\xa5", "\xc3\xa5"}, + {"%%", "%"}, + {"literal text", "literal text"}, + }; + for (const auto& test : cases) { + CheckFormat(guest, test.format, test.expected); + CheckBufferSize(guest, test.format, std::strlen(test.expected)); + } + CheckFormat(guest, "", ""); + char buffer[2]{'x', 'y'}; + Require(strftime_nid_postfix(buffer, 0, "%Q", &guest) == 0, "strftime accepted a zero-size buffer"); + Require(buffer[0] == 'x' && buffer[1] == 'y', "strftime wrote to a zero-size buffer"); +} + +static void CheckLocalRange() { + constexpr std::array timers{-1, -7200, -7201, -86400, -2208988800, -12622780801, -62135596800, 32535216000, 40000000000, 253402300799}; + for (const std::int64_t timer : timers) { + GuestTm local{}; + *__error_nid_postfix() = 0; + Require(localtime_s_nid_postfix(&timer, &local) == &local && local.tm_gmtoff == 7200, "Local conversion rejected a time outside 1970 to 3000"); + Require(Equal(local, UtcTime(timer + 7200)), "Local conversion outside 1970 to 3000 has the wrong calendar fields"); + Require(local.tm_zone != nullptr && std::strlen(local.tm_zone) > 0, "Local conversion outside 1970 to 3000 did not set tm_zone"); + GuestTm normalized = local; + normalized.tm_wday = -1; + normalized.tm_yday = -1; + Require(mktime_nid_postfix(&normalized) == timer && Equal(normalized, local), "mktime did not invert localtime outside 1970 to 3000"); + Require(*__error_nid_postfix() == 0, "A local conversion outside 1970 to 3000 set errno"); + const GuestTm* shared = localtime_nid_postfix(&timer); + Require(shared != nullptr && Equal(*shared, local), "localtime rejected a time outside 1970 to 3000"); + Require(ctime_nid_postfix(&timer)[0] != '?', "ctime rejected a time outside 1970 to 3000"); + } + GuestTm beforeEpoch{}; + beforeEpoch.tm_year = 70; + beforeEpoch.tm_mon = -1; + beforeEpoch.tm_mday = 1; + beforeEpoch.tm_isdst = -1; + Require(mktime_nid_postfix(&beforeEpoch) == -2685600 && beforeEpoch.tm_year == 69 && beforeEpoch.tm_mon == 11 && beforeEpoch.tm_mday == 1 + && beforeEpoch.tm_wday == 1 && beforeEpoch.tm_yday == 334, "mktime did not normalize a month before 1970"); + GuestTm afterLimit{}; + afterLimit.tm_year = 1200; + afterLimit.tm_mon = 17; + afterLimit.tm_mday = 15; + afterLimit.tm_hour = 12; + afterLimit.tm_isdst = -1; + const std::int64_t late = mktime_nid_postfix(&afterLimit); + Require(late == 35705181600 && afterLimit.tm_year == 1201 && afterLimit.tm_mon == 5 && afterLimit.tm_mday == 15 && afterLimit.tm_hour == 12 + && Equal(afterLimit, UtcTime(late + 7200)), "mktime did not normalize a month after 3000"); + GuestTm farDay{}; + farDay.tm_year = 70; + farDay.tm_mday = -36500; + farDay.tm_isdst = -1; + const std::int64_t early = mktime_nid_postfix(&farDay); + Require(early == -3153693600 && farDay.tm_mday >= 1 && Equal(farDay, UtcTime(early + 7200)), "mktime did not normalize a day before 1970"); + const std::int64_t overflow = std::numeric_limits::min(); + GuestTm scratch{}; + Require(localtime_s_nid_postfix(&overflow, &scratch) == nullptr, "Unrepresentable negative local time was accepted"); +} + +#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}, @@ -109,6 +440,26 @@ int main() { const std::int64_t underflow = std::numeric_limits::min(); GuestTm scratch{}; Require(gmtime_s_nid_postfix(&underflow, &scratch) == nullptr, "Unrepresentable negative time was accepted"); + const std::int64_t overflow = std::numeric_limits::max(); + for (const std::int64_t* timer : {&underflow, &overflow}) { + *__error_nid_postfix() = 1234; + Require(gmtime_s_nid_postfix(timer, &scratch) == nullptr && *__error_nid_postfix() == 1234, + "gmtime_s of an unrepresentable time changed errno"); + Require(gmtime_nid_postfix(timer) == nullptr && *__error_nid_postfix() == 1234, + "gmtime of an unrepresentable time changed errno"); + Require(localtime_s_nid_postfix(timer, &scratch) == nullptr && *__error_nid_postfix() == 1234, + "localtime_s of an unrepresentable time changed errno"); + Require(localtime_nid_postfix(timer) == nullptr && *__error_nid_postfix() == 1234, + "localtime of an unrepresentable time changed errno"); + } + + CheckAsctime(UtcTime(0), "Thu Jan 1 00:00:00 1970\n"); + CheckAsctime(UtcTime(-2208988801), "Sun Dec 31 23:59:59 1899\n"); + CheckAsctime(UtcTime(-30610224000), "Wed Jan 1 00:00:00 1000\n"); + GuestTm pastMonthEnd = UtcTime(983404800); + pastMonthEnd.tm_mon = 1; + pastMonthEnd.tm_mday = 29; + CheckAsctime(pastMonthEnd, "Thu Feb 29 00:00:00 2001\n"); const std::int64_t epoch = 0; GuestTm local{}; @@ -127,14 +478,59 @@ int main() { Require(roundTrip.tm_wday == 4 && roundTrip.tm_yday == 0, "mktime did not normalize the calendar fields"); Require(roundTrip.tm_gmtoff == 7200 && roundTrip.tm_zone != nullptr, "mktime did not set the zone fields"); + Require(std::strcmp(ctime_nid_postfix(&epoch), "Thu Jan 1 02:00:00 1970\n") == 0, "ctime did not format local time"); + const std::int64_t leap = 951782400 + 13 * 3600 + 4 * 60 + 5; + Require(std::strcmp(ctime_nid_postfix(&leap), "Tue Feb 29 15:04:05 2000\n") == 0, "ctime formatted the wrong date"); + *__error_nid_postfix() = 0; + Require(std::strcmp(ctime_nid_postfix(&underflow), "??? ??? ?? ??:??:?? ????\n") == 0 && *__error_nid_postfix() == 22, + "ctime of an unrepresentable time did not report it"); + + constexpr std::int64_t latest = std::numeric_limits::max(); + constexpr std::int64_t earliest = std::numeric_limits::min(); + constexpr std::array differenceCases{{ + {10, 3, 7.0}, + {3, 10, -7.0}, + {0, -1, 1.0}, + {-1, 0, -1.0}, + {-86400, -172800, 86400.0}, + {latest, latest - 1, 1.0}, + {earliest + 1, earliest, 1.0}, + {latest, earliest, 0x1p64}, + {earliest, latest, -0x1p64}, + }}; + for (const auto& differenceCase : differenceCases) CheckDifference(differenceCase); + GuestTm utc{}; Require(gmtime_s_nid_postfix(&epoch, &utc) == &utc, "UTC epoch conversion failed"); char formatted[64]{}; Require(strftime_nid_postfix(formatted, sizeof(formatted), "%Y-%m-%d %H:%M:%S", &utc) == 19 && std::strcmp(formatted, "1970-01-01 00:00:00") == 0, "strftime did not read the guest tm"); + 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 + 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"); + 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 new file mode 100644 index 0000000000..1e037fd9d8 --- /dev/null +++ b/core/libs/tests/GuestTruncate.cpp @@ -0,0 +1,129 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#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 + +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; +constexpr int GuestEinval = 22; +constexpr int SceKernelErrorBase = static_cast(0x80020000u); + +static void RequireFailure(int result, int error) { + Require(result == -1); + Require(*__error_nid_postfix() == error); +} + +static std::string Contents(const std::filesystem::path& file) { + std::ifstream stream(file, std::ios::binary); + 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); + Require(std::filesystem::create_directory(directory)); + 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); + Require(Contents(file) == "012345"); + + Require(truncate_nid_postfix(path.c_str(), 9) == 0); + Require(std::filesystem::file_size(file) == 9); + Require(Contents(file) == std::string("012345\0\0\0", 9)); + + Require(truncate_nid_postfix(path.c_str(), 0) == 0); + Require(std::filesystem::file_size(file) == 0); + + RequireFailure(truncate_nid_postfix(path.c_str(), -1), GuestEinval); + Require(std::filesystem::file_size(file) == 0); + RequireFailure(truncate_nid_postfix((directory / "missing").string().c_str(), 0), GuestEnoent); + RequireFailure(truncate_nid_postfix("", 0), GuestEnoent); + RequireFailure(truncate_nid_postfix(nullptr, 0), GuestEfault); + RequireFailure(truncate_nid_postfix(directory.string().c_str(), 0), GuestEisdir); + Require(std::filesystem::is_directory(directory)); + Require(sceKernelTruncate_nid_postfix(directory.string().c_str(), 0) == SceKernelErrorBase + GuestEisdir); + Require(sceKernelTruncate_nid_postfix(path.c_str(), -1) == SceKernelErrorBase + GuestEinval); + + { std::ofstream output(file, std::ios::binary); output << "locked"; } +#ifdef _WIN32 + Require(SetFileAttributesW(file.c_str(), FILE_ATTRIBUTE_READONLY)); + RequireFailure(truncate_nid_postfix(path.c_str(), 1), GuestEacces); + Require(sceKernelTruncate_nid_postfix(path.c_str(), 1) == SceKernelErrorBase + GuestEacces); + Require(SetFileAttributesW(file.c_str(), FILE_ATTRIBUTE_NORMAL)); +#else + std::filesystem::permissions(file, std::filesystem::perms::owner_read); + if (::geteuid() != 0) { + RequireFailure(truncate_nid_postfix(path.c_str(), 1), GuestEacces); + Require(sceKernelTruncate_nid_postfix(path.c_str(), 1) == SceKernelErrorBase + GuestEacces); + } else { + std::fprintf(stderr, "running as root, which ignores file permissions: the read-only cases not tested\n"); + } + std::filesystem::permissions(file, std::filesystem::perms::owner_read | std::filesystem::perms::owner_write); +#endif + Require(Contents(file) == "locked"); + + Require(std::filesystem::remove(file)); + Require(std::filesystem::remove(directory)); +} 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 ac9b76ea13..f9e50c0e20 100644 --- a/core/libs/tests/GuestUdp.cpp +++ b/core/libs/tests/GuestUdp.cpp @@ -1,18 +1,25 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include #include +#include #include #include extern "C" { int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t); int APS5_VABI fcntl_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 bind_nid_postfix(int, const void*, std::uint32_t); int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI getpeername_nid_postfix(int, void*, std::uint32_t*); int APS5_VABI ioctl_nid_postfix(int, std::uint64_t, void*); +int APS5_VABI _ioctl_nid_postfix(int, std::uint64_t, void*); +std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); +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); @@ -36,9 +43,99 @@ struct Msghdr { extern "C" { std::int64_t APS5_VABI sendmsg_nid_postfix(int, const Msghdr*, int); std::int64_t APS5_VABI recvmsg_nid_postfix(int, Msghdr*, int); +std::int64_t APS5_VABI sceKernelReadv(int, const Iovec*, int); +std::int64_t APS5_VABI sceKernelWritev(int, const Iovec*, int); } static void Require(bool value) { if (!value) std::abort(); } + +static void CheckAddressLengths(int family) { + const std::uint32_t addressSize = family == 2 ? 16 : 28; + const int receiver = socket_nid_postfix(family, 2, 0); + if (receiver < 0 && family == 28 && *__error_nid_postfix() == 47) return; + Require(receiver >= 0); + const int sender = socket_nid_postfix(family, 2, 0); + Require(sender >= 0); + std::array destination{}; + destination[0] = static_cast(addressSize); + destination[1] = static_cast(family); + if (family == 2) { destination[4] = 127; destination[7] = 1; } + else destination[23] = 1; + const int bound = bind_nid_postfix(receiver, destination.data(), addressSize); + if (bound < 0 && family == 28 && *__error_nid_postfix() == 49) { + Require(close_nid_postfix(sender) == 0 && close_nid_postfix(receiver) == 0); + return; + } + Require(bound == 0); + std::uint32_t size = destination.size(); + Require(getsockname_nid_postfix(receiver, destination.data(), &size) == 0 && size == addressSize); + Require(connect_nid_postfix(sender, destination.data(), addressSize) == 0); + std::array expected{}; + size = expected.size(); + Require(getsockname_nid_postfix(sender, expected.data(), &size) == 0 && size == addressSize); + for (std::uint32_t capacity : {0u, 1u, 2u, addressSize - 1, addressSize, 32u, UINT32_MAX}) { + const auto copied = std::min(capacity, addressSize); + std::array address; + address.fill(0xa5); + size = capacity; + Require(getsockname_nid_postfix(receiver, address.data(), &size) == 0 && size == copied); + Require(std::memcmp(address.data(), destination.data(), copied) == 0); + Require(std::all_of(address.begin() + copied, address.end(), [](auto byte) { return byte == 0xa5; })); + const char payload[] = "address length"; + char received[sizeof(payload)]{}; + Require(sendto_nid_postfix(sender, payload, sizeof(payload), 0, nullptr, 0) == sizeof(payload)); + address.fill(0xa5); + size = capacity; + Require(recvfrom_nid_postfix(receiver, received, sizeof(received), 2, address.data(), &size) == sizeof(payload)); + Require(size == copied && std::memcmp(address.data(), expected.data(), copied) == 0); + Require(std::all_of(address.begin() + copied, address.end(), [](auto byte) { return byte == 0xa5; })); + address.fill(0xa5); + Iovec vector{received, sizeof(received)}; + Msghdr message{address.data(), capacity, &vector, 1, nullptr, 0, -1}; + Require(recvmsg_nid_postfix(receiver, &message, 0) == sizeof(payload)); + Require(message.nameLength == copied && std::memcmp(received, payload, sizeof(payload)) == 0); + Require(std::memcmp(address.data(), expected.data(), copied) == 0); + Require(std::all_of(address.begin() + copied, address.end(), [](auto byte) { return byte == 0xa5; })); + } + Require(close_nid_postfix(sender) == 0 && close_nid_postfix(receiver) == 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(); 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); @@ -47,13 +144,36 @@ int main() { std::uint32_t size = destination.size(); Require(getsockname_nid_postfix(receiver, destination.data(), &size) == 0 && size == 16); Require(destination[2] || destination[3]); + Require(connect_nid_postfix(sender, destination.data(), destination.size()) == 0); + char vectorHead[] = "atomic "; + char vectorTail[] = "datagram"; + Iovec vectorOutput[]{{vectorHead, sizeof(vectorHead) - 1}, {vectorTail, sizeof(vectorTail)}}; + Require(sceKernelWritev(sender, vectorOutput, 2) == sizeof(vectorHead) + sizeof(vectorTail) - 1); + char vectorFirst[sizeof(vectorHead) - 1]{}; + char vectorSecond[sizeof(vectorTail)]{}; + Iovec vectorInput[]{{vectorFirst, sizeof(vectorFirst)}, {vectorSecond, sizeof(vectorSecond)}}; + Require(sceKernelReadv(receiver, vectorInput, 2) == sizeof(vectorHead) + sizeof(vectorTail) - 1); + Require(std::memcmp(vectorFirst, "atomic ", sizeof(vectorFirst)) == 0 && std::strcmp(vectorSecond, "datagram") == 0); int enabled = 1; Require(setsockopt_nid_postfix(sender, 0xffff, 0x20, &enabled, sizeof(enabled)) == 0); int option = 0; std::uint32_t optionSize = sizeof(option); Require(getsockopt_nid_postfix(sender, 0xffff, 0x20, &option, &optionSize) == 0 && option != 0); + for (const std::int64_t wide : {0, 1}) { + Require(setsockopt_nid_postfix(sender, 0xffff, 0x20, &wide, sizeof(wide)) == 0); + optionSize = sizeof(option); + Require(getsockopt_nid_postfix(sender, 0xffff, 0x20, &option, &optionSize) == 0 && (option != 0) == (wide != 0)); + } + Require(setsockopt_nid_postfix(sender, 0xffff, 0x20, &enabled, 2) == -1 && *__error_nid_postfix() == 22); Require(setsockopt_nid_postfix(sender, 0xffff, 12345, &enabled, sizeof(enabled)) == -1); Require(*__error_nid_postfix() == 42); + const int unconnected = socket_nid_postfix(2, 2, 0); + Require(unconnected >= 0); + Require(send_nid_postfix(unconnected, "x", 1, 0) == -1); + Require(*__error_nid_postfix() == 39); + Require(sendto_nid_postfix(unconnected, "x", 1, 0, nullptr, 0) == -1); + Require(*__error_nid_postfix() == 39); + Require(close_nid_postfix(unconnected) == 0); const int unsized = socket_nid_postfix(2, 2, 0); std::array unsizedAddress{0, 2, 0, 0, 127, 0, 0, 1}; Require(unsized >= 0 && bind_nid_postfix(unsized, unsizedAddress.data(), unsizedAddress.size()) == 0); @@ -86,6 +206,9 @@ int main() { Require(std::strcmp(buffer, message) == 0 && source[1] == 2 && source[4] == 127); Require(ioctl_nid_postfix(receiver, 0x4004667f, &queued) == 0 && queued == 0); Require(ioctl_nid_postfix(receiver, 123, &queued) == -1 && *__error_nid_postfix() == 25); + queued = -1; + Require(_ioctl_nid_postfix(receiver, 0x4004667f, &queued) == 0 && queued == 0); + Require(_ioctl_nid_postfix(receiver, 123, &queued) == -1 && *__error_nid_postfix() == 25); char head[] = "scatter "; char tail[] = "gather"; Iovec parts[2]{{head, 8}, {tail, 6}}; @@ -122,7 +245,47 @@ int main() { Require(close_nid_postfix(receiver) == 0); Require(close_nid_postfix(receiver) == -1 && *__error_nid_postfix() == 9); Require(ioctl_nid_postfix(receiver, 0x4004667f, &queued) == -1); + Require(_ioctl_nid_postfix(receiver, 0x4004667f, &queued) == -1 && *__error_nid_postfix() == 9); Require(close_nid_postfix(sender) == 0); + const int peer = socket_nid_postfix(2, 2, 0); + const int other = socket_nid_postfix(2, 2, 0); + const int dissolved = socket_nid_postfix(2, 2, 0); + Require(peer >= 0 && other >= 0 && dissolved >= 0); + std::array peerAddress{16, 2, 0, 0, 127, 0, 0, 1}; + std::array dissolvedAddress{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(peer, peerAddress.data(), peerAddress.size()) == 0); + Require(bind_nid_postfix(dissolved, dissolvedAddress.data(), dissolvedAddress.size()) == 0); + size = peerAddress.size(); + Require(getsockname_nid_postfix(peer, peerAddress.data(), &size) == 0); + size = dissolvedAddress.size(); + Require(getsockname_nid_postfix(dissolved, dissolvedAddress.data(), &size) == 0); + Require(connect_nid_postfix(dissolved, peerAddress.data(), peerAddress.size()) == 0); + std::array peerName{}; + size = peerName.size(); + Require(getpeername_nid_postfix(dissolved, peerName.data(), &size) == 0 && peerName[2] == peerAddress[2] && peerName[3] == peerAddress[3]); + const std::array unspecified{16, 0}; + Require(connect_nid_postfix(dissolved, unspecified.data(), unspecified.size()) == -1 && *__error_nid_postfix() == 47); + size = peerName.size(); + Require(getpeername_nid_postfix(dissolved, peerName.data(), &size) == -1 && *__error_nid_postfix() == 57); + std::array localName{}; + size = localName.size(); + Require(getsockname_nid_postfix(dissolved, localName.data(), &size) == 0); + Require(localName[2] == dissolvedAddress[2] && localName[3] == dissolvedAddress[3]); +#ifndef _WIN32 + Require(localName[4] == 0 && localName[5] == 0 && localName[6] == 0 && localName[7] == 0); +#endif + Require(send_nid_postfix(dissolved, "x", 1, 0) == -1 && *__error_nid_postfix() == 39); + const char unrelated[] = "unrelated sender"; + Require(sendto_nid_postfix(other, unrelated, sizeof(unrelated), 0, dissolvedAddress.data(), dissolvedAddress.size()) == sizeof(unrelated)); + queued = 0; + for (int i = 0; i < 1000 && queued == 0; ++i) { + Require(ioctl_nid_postfix(dissolved, 0x4004667f, &queued) == 0); + if (!queued) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + Require(queued == sizeof(unrelated)); + Require(recvfrom_nid_postfix(dissolved, buffer, sizeof(buffer), 0, nullptr, nullptr) == sizeof(unrelated)); + Require(std::strcmp(buffer, unrelated) == 0); + for (const int descriptor : {peer, other, dissolved}) Require(close_nid_postfix(descriptor) == 0); unsigned char parsed[16]{}; Require(inet_pton_nid_postfix(2, "127.0.0.1", parsed) == 1); Require(parsed[0] == 127 && parsed[1] == 0 && parsed[2] == 0 && parsed[3] == 1); @@ -138,4 +301,14 @@ int main() { const int stream = socket_nid_postfix(2, 1, 0); Require(stream >= 0); Require(close_nid_postfix(stream) == 0); + const int nonblocking = socket_nid_postfix(2, 2 | 0x20000000, 0); + Require(nonblocking >= 0 && fcntl_nid_postfix(nonblocking, 3) == 6); + std::array nonblockingAddress{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(nonblocking, nonblockingAddress.data(), nonblockingAddress.size()) == 0); + Require(recvfrom_nid_postfix(nonblocking, buffer, sizeof(buffer), 0, nullptr, nullptr) == -1 && *__error_nid_postfix() == 35); + Require(close_nid_postfix(nonblocking) == 0); + const int closeOnExec = socket_nid_postfix(2, 1 | 0x10000000, 6); + Require(closeOnExec >= 0 && fcntl_nid_postfix(closeOnExec, 3) == 2); + Require(close_nid_postfix(closeOnExec) == 0); + Require(socket_nid_postfix(2, 0x20000000, 0) == -1 && *__error_nid_postfix() == 43); } diff --git a/core/libs/tests/GuestUltCondition.cpp b/core/libs/tests/GuestUltCondition.cpp new file mode 100644 index 0000000000..51741e4aa2 --- /dev/null +++ b/core/libs/tests/GuestUltCondition.cpp @@ -0,0 +1,238 @@ +#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 sceUltConditionVariableWait(void*); +int APS5_VABI sceUltConditionVariableSignal(void*); +int APS5_VABI sceUltConditionVariableDestroy(void*); +} + +constexpr int Null = -2139029503; +constexpr int Invalid = -2139029500; +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; + + Fixture() { + CHECK(sceUltMutexCreate(&mutex, "mutex", nullptr, nullptr, 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; + 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(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) == State); + 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) == Busy); + CHECK(sceUltMutexCreate(&f.mutex, nullptr, nullptr, nullptr, 0) == State); + auto other = std::async(std::launch::async, [&] { + CHECK(sceUltMutexUnlock(&f.mutex) == State); + 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) == Busy); + f.Unlock(); + CHECK(sceUltMutexDestroy(&f.mutex) == Busy); + 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) == Busy); + f.Unlock(); + }); + Await(entered, 1); + f.Lock(); + CHECK(sceUltConditionVariableDestroy(&f.condition) == Busy); + CHECK(sceUltMutexDestroy(&f.mutex) == Busy); + 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 1ab7d5e978..2a93f78c0c 100644 --- a/core/libs/tests/GuestUltQueue.cpp +++ b/core/libs/tests/GuestUltQueue.cpp @@ -98,14 +98,6 @@ static void Validation() { try { sceUltQueueDataResourcePoolGetWorkAreaSize(1, UINT64_MAX, 1); } catch (const std::out_of_range&) { overflow = true; } CHECK(overflow); - bool rejected = false; - try { sceUltQueueCreate(&extra, nullptr, 8, nullptr, &fixture.pool, &extra, 0); } - catch (const std::runtime_error&) { rejected = true; } - CHECK(rejected); - rejected = false; - try { sceUltQueueDataResourcePoolCreate(&extra, nullptr, 1, 8, 1, nullptr, nullptr, &extra, 0); } - catch (const std::runtime_error&) { rejected = true; } - CHECK(rejected); overflow = false; try { sceUltQueueDataResourcePoolGetWorkAreaSize(UINT32_MAX, UINT64_MAX / 2, 1); } catch (const std::out_of_range&) { overflow = true; } @@ -116,6 +108,14 @@ static void Validation() { CHECK(sceUltQueueDataResourcePoolDestroy(&fixture.pool) == State); fixture.Create(1); fixture.Destroy(); + std::array optParam{}; + optParam.fill(0x5A); + CHECK(sceUltQueueDataResourcePoolCreate(&fixture.pool, "pool", 1, 8, 1, nullptr, nullptr, optParam.data(), 0) == 0); + CHECK(sceUltQueueCreate(&fixture.first, "first", 8, nullptr, &fixture.pool, optParam.data(), 0) == 0); + std::uint64_t output = 0; + CHECK(sceUltQueuePush(&fixture.first, &value) == 0); + CHECK(sceUltQueuePop(&fixture.first, &output) == 0 && output == value); + fixture.Destroy(); } static void Fifo() { diff --git a/core/libs/tests/GuestUltSync.cpp b/core/libs/tests/GuestUltSync.cpp new file mode 100644 index 0000000000..9695ac0263 --- /dev/null +++ b/core/libs/tests/GuestUltSync.cpp @@ -0,0 +1,318 @@ +#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 void*, 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*, std::uint32_t); +int APS5_VABI sceUltWaitingQueueResourcePoolDestroy(void*); +int APS5_VABI sceUltReaderWriterLockOptParamInitialize(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 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); + CHECK(_sceUltMutexCreate(&mutex, "trylock", nullptr, nullptr, 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) == State); + std::thread([&] { + other = sceUltMutexTryLock(&mutex); + if (other == Ok) other = sceUltMutexUnlock(&mutex); + }).join(); + CHECK(other == Ok); + CHECK(sceUltMutexLock(&mutex) == Ok); + CHECK(sceUltMutexDestroy(&mutex) == Busy); + 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}) { + 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, 0) == 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 = Busy; + while (joined == Busy && std::chrono::steady_clock::now() < deadline) { + joined = sceUltUlthreadTryJoin(&second, &status); + if (joined == Busy) 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 13ac01a590..0d074c1d20 100644 --- a/core/libs/tests/GuestUltThread.cpp +++ b/core/libs/tests/GuestUltThread.cpp @@ -4,7 +4,9 @@ #include #include #include +#include #include +#include extern "C" { int APS5_VABI sceUltInitialize(); @@ -18,6 +20,25 @@ 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 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 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 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 sceUltGetConditionVariableInfo(); } static constexpr int Ok = 0; @@ -88,6 +109,14 @@ static void Validation() { CHECK(sceUltUlthreadJoin(&thread, &status) == Ok && status == 0x27); CHECK(sceUltFinalize() == Ok); CHECK(sceUltUlthreadRuntimeGetWorkAreaSize(1, 1) == 256u + 16u * 1024u); + CHECK(sceUltConditionVariableDestroy(nullptr) == -2139029503); + bool threwInfo = false; + try { + sceUltGetConditionVariableInfo(); + } catch (const std::runtime_error&) { + threwInfo = true; + } + CHECK(threwInfo); } static void TryJoin() { @@ -133,6 +162,68 @@ static void Destroy() { CHECK(sceUltFinalize() == Ok); } +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(sceUltSemaphoreAcquire(semaphore, 2) == Ok); + CHECK(sceUltSemaphoreRelease(semaphore, 2) == Ok); + alignas(8) static std::uint8_t mutexOptParam[128]; + alignas(8) static std::uint8_t mutex[256]; + std::memset(mutexOptParam, 0xFF, sizeof(mutexOptParam)); + CHECK(_sceUltMutexOptParamInitialize(nullptr, 0) == Null); + 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(sceUltMutexLock(mutex) == Ok); + 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); + 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) == Busy); + 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) == Busy); + CHECK(sceUltWaitingQueueResourcePoolDestroy(pool) == Busy); + 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) { CHECK(argc == 2); CHECK(sceUltInitialize() == Ok); @@ -140,5 +231,6 @@ int main(int argc, char** argv) { else if (std::strcmp(argv[1], "validation") == 0) Validation(); else if (std::strcmp(argv[1], "tryjoin") == 0) TryJoin(); else if (std::strcmp(argv[1], "destroy") == 0) Destroy(); + else if (std::strcmp(argv[1], "underscoredcreate") == 0) UnderscoredCreate(); else CHECK(false); } diff --git a/core/libs/tests/GuestUmtxOp.cpp b/core/libs/tests/GuestUmtxOp.cpp new file mode 100644 index 0000000000..97eef1d837 --- /dev/null +++ b/core/libs/tests/GuestUmtxOp.cpp @@ -0,0 +1,373 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI _umtx_op_nid_postfix(void* object, int operation, std::uint64_t value, void* sizeArgument, void* timeArgument); +int* APS5_VABI __error_nid_postfix(); +int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp); +} + +static constexpr int UMTX_OP_WAIT = 2; +static constexpr int UMTX_OP_WAKE = 3; +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; +static constexpr std::uint32_t UMTX_ABSTIME = 1; +static constexpr int CLOCK_MONOTONIC_ID = 4; + +static constexpr int GUEST_EFAULT = 14; +static constexpr int GUEST_EINVAL = 22; +static constexpr int GUEST_EOPNOTSUPP = 45; +static constexpr int GUEST_ETIMEDOUT = 60; + +static constexpr auto SETTLE = std::chrono::milliseconds(100); + +struct UmtxTime { + KernelTimespec timeout; + std::uint32_t flags; + std::uint32_t clockId; +}; + +static void Require(bool value) { if (!value) std::abort(); } + +static KernelTimespec FailsafeTimeout() { return {10, 0}; } + +static int Umtx(void* object, int operation, std::uint64_t value, void* sizeArgument = nullptr, void* timeArgument = nullptr) { + *__error_nid_postfix() = 0; + return _umtx_op_nid_postfix(object, operation, value, sizeArgument, timeArgument); +} + +static bool FailsWith(int result, int error) { + return result == -1 && *__error_nid_postfix() == error; +} + +static void* SizeOf(std::size_t size) { return reinterpret_cast(size); } + +static void AwaitCount(const std::atomic& counter, int value) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (counter.load() != value) { + Require(std::chrono::steady_clock::now() < deadline); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +} + +static void ReturnsWhenTheValueDiffers() { + std::uint64_t value = 2; + Require(Umtx(&value, UMTX_OP_WAIT, 1) == 0); + KernelTimespec none{0, 0}; + Require(Umtx(&value, UMTX_OP_WAIT, 1, nullptr, &none) == 0); + + std::uint32_t word = 2; + Require(Umtx(&word, UMTX_OP_WAIT_UINT, 1) == 0); + Require(Umtx(&word, UMTX_OP_WAIT_UINT_PRIVATE, 1, nullptr, &none) == 0); +} + +static void WaitComparesALongAndWaitUintAnInt() { + std::uint64_t value = 0x100000001ULL; + KernelTimespec none{0, 0}; + Require(Umtx(&value, UMTX_OP_WAIT, 1, nullptr, &none) == 0); + Require(FailsWith(Umtx(&value, UMTX_OP_WAIT, 0x100000001ULL, nullptr, &none), GUEST_ETIMEDOUT)); + + auto* low = reinterpret_cast(&value); + Require(FailsWith(Umtx(low, UMTX_OP_WAIT_UINT, 1, nullptr, &none), GUEST_ETIMEDOUT)); + Require(Umtx(low, UMTX_OP_WAIT_UINT_PRIVATE, 0x700000001ULL, nullptr, &none) == 0); + Require(Umtx(low, UMTX_OP_WAIT_UINT, 0x700000001ULL, nullptr, &none) == 0); + std::uint32_t all = 0xffffffffu; + Require(Umtx(&all, UMTX_OP_WAIT_UINT, 0xffffffffffffffffULL, nullptr, &none) == 0); + Require(FailsWith(Umtx(&all, UMTX_OP_WAIT_UINT, 0xffffffffULL, nullptr, &none), GUEST_ETIMEDOUT)); +} + +static void TimesOutWhileTheValueMatches() { + std::uint32_t word = 1; + KernelTimespec none{0, 0}; + Require(FailsWith(Umtx(&word, UMTX_OP_WAIT_UINT, 1, nullptr, &none), GUEST_ETIMEDOUT)); + + KernelTimespec relative{0, 20000000}; + auto start = std::chrono::steady_clock::now(); + Require(FailsWith(Umtx(&word, UMTX_OP_WAIT_UINT, 1, SizeOf(sizeof(relative)), &relative), GUEST_ETIMEDOUT)); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(15)); + + UmtxTime time{{0, 20000000}, 0, CLOCK_MONOTONIC_ID}; + start = std::chrono::steady_clock::now(); + Require(FailsWith(Umtx(&word, UMTX_OP_WAIT_UINT, 1, SizeOf(sizeof(time)), &time), GUEST_ETIMEDOUT)); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(15)); + + KernelTimespec now{}; + Require(clock_gettime_nid_postfix(CLOCK_MONOTONIC_ID, &now) == 0); + UmtxTime absolute{{now.tv_sec, now.tv_nsec + 20000000}, UMTX_ABSTIME, CLOCK_MONOTONIC_ID}; + if (absolute.timeout.tv_nsec >= 1000000000) { + absolute.timeout.tv_sec += 1; + absolute.timeout.tv_nsec -= 1000000000; + } + start = std::chrono::steady_clock::now(); + Require(FailsWith(Umtx(&word, UMTX_OP_WAIT_UINT, 1, SizeOf(sizeof(absolute)), &absolute), GUEST_ETIMEDOUT)); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(15)); + + UmtxTime past{{now.tv_sec, now.tv_nsec}, UMTX_ABSTIME, CLOCK_MONOTONIC_ID}; + start = std::chrono::steady_clock::now(); + Require(FailsWith(Umtx(&word, UMTX_OP_WAIT_UINT, 1, SizeOf(sizeof(past)), &past), GUEST_ETIMEDOUT)); + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(1)); +} + +static void RejectsAnInvalidTimeout() { + std::uint32_t word = 1; + KernelTimespec negative{-1, 0}; + KernelTimespec tooManyNanos{0, 1000000000}; + for (const int operation : {UMTX_OP_WAIT, UMTX_OP_WAIT_UINT}) { + 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)); +} + +static void WakeReleasesAWaiterWithoutAValueChange() { + std::uint64_t value = 1; + std::atomic finished{0}; + int result = -2; + std::thread waiter([&] { + KernelTimespec failsafe = FailsafeTimeout(); + result = Umtx(&value, UMTX_OP_WAIT, 1, SizeOf(sizeof(failsafe)), &failsafe); + ++finished; + }); + while (finished.load() == 0) { + Require(Umtx(&value, UMTX_OP_WAKE, 1) == 0); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + waiter.join(); + Require(result == 0); +} + +static void WakeHonoursTheCountAndTheAddress() { + std::uint32_t word = 1; + std::uint32_t other = 1; + std::atomic started{0}; + std::atomic finished{0}; + std::atomic otherFinished{0}; + std::vector results(4, -2); + int otherResult = -2; + + std::vector waiters; + for (int i = 0; i < 4; ++i) { + waiters.emplace_back([&, i] { + KernelTimespec failsafe = FailsafeTimeout(); + ++started; + 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, SizeOf(sizeof(failsafe)), &failsafe); + ++otherFinished; + }); + AwaitCount(started, 5); + std::this_thread::sleep_for(SETTLE); + + Require(Umtx(&word, UMTX_OP_WAKE_PRIVATE, 1) == 0); + AwaitCount(finished, 1); + std::this_thread::sleep_for(SETTLE); + Require(finished.load() == 1); + + Require(Umtx(&word, UMTX_OP_WAKE, 0) == 0); + AwaitCount(finished, 2); + std::this_thread::sleep_for(SETTLE); + Require(finished.load() == 2); + + Require(Umtx(&word, UMTX_OP_WAKE, INT_MAX) == 0); + AwaitCount(finished, 4); + std::this_thread::sleep_for(SETTLE); + Require(otherFinished.load() == 0); + + Require(Umtx(&other, UMTX_OP_WAKE_PRIVATE, 1) == 0); + for (auto& waiter : waiters) waiter.join(); + otherWaiter.join(); + for (const int result : results) Require(result == 0); + Require(otherResult == 0); +} + +static void WakeWithoutWaitersSucceeds() { + std::uint32_t word = 0; + Require(Umtx(&word, UMTX_OP_WAKE, 1) == 0); + Require(Umtx(&word, UMTX_OP_WAKE_PRIVATE, INT_MAX) == 0); +} + +static void NwakeWakesEveryWaiterOnEachListedAddress() { + std::uint32_t first = 1; + std::uint64_t second = 1; + std::uint32_t unlisted = 1; + std::atomic started{0}; + std::atomic finished{0}; + std::atomic unlistedFinished{0}; + std::vector results(4, -2); + int unlistedResult = -2; + + std::vector waiters; + for (int i = 0; i < 3; ++i) { + waiters.emplace_back([&, i] { + KernelTimespec failsafe = FailsafeTimeout(); + ++started; + 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, SizeOf(sizeof(failsafe)), &failsafe); + ++finished; + }); + std::thread unlistedWaiter([&] { + KernelTimespec failsafe = FailsafeTimeout(); + ++started; + unlistedResult = Umtx(&unlisted, UMTX_OP_WAIT_UINT, 1, SizeOf(sizeof(failsafe)), &failsafe); + ++unlistedFinished; + }); + AwaitCount(started, 5); + std::this_thread::sleep_for(SETTLE); + + void* addresses[] = {&first, nullptr, &second}; + Require(Umtx(addresses, UMTX_OP_NWAKE_PRIVATE, 3) == 0); + AwaitCount(finished, 4); + std::this_thread::sleep_for(SETTLE); + Require(unlistedFinished.load() == 0); + + Require(Umtx(&unlisted, UMTX_OP_WAKE_PRIVATE, 1) == 0); + for (auto& waiter : waiters) waiter.join(); + unlistedWaiter.join(); + for (const int result : results) Require(result == 0); + Require(unlistedResult == 0); +} + +static void NwakeReadsOnlyTheCountedEntries() { + Require(Umtx(nullptr, UMTX_OP_NWAKE_PRIVATE, 0) == 0); + Require(Umtx(nullptr, UMTX_OP_NWAKE_PRIVATE, 0xffffffff00000000ULL) == 0); + Require(Umtx(nullptr, UMTX_OP_NWAKE_PRIVATE, 0xffffffffULL) == 0); + Require(FailsWith(Umtx(nullptr, UMTX_OP_NWAKE_PRIVATE, 1), GUEST_EFAULT)); + std::uint32_t word = 0; + void* addresses[] = {&word}; + 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)); + Require(FailsWith(Umtx(nullptr, UMTX_OP_WAKE, 1), GUEST_EFAULT)); + Require(Umtx(nullptr, UMTX_OP_WAKE_PRIVATE, 1) == 0); + + std::uint64_t value = 0; + Require(FailsWith(Umtx(&value, 0, 0), GUEST_EOPNOTSUPP)); + 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, UMTX_OP_MUTEX_LOCK, 0); } + catch (const std::runtime_error&) { threw = true; } + Require(threw); + threw = false; + try { Umtx(&value, UMTX_OP_SEM2_WAIT, 0); } + catch (const std::runtime_error&) { threw = true; } + Require(threw); +} + +int main() { + ReturnsWhenTheValueDiffers(); + WaitComparesALongAndWaitUintAnInt(); + TimesOutWhileTheValueMatches(); + RejectsAnInvalidTimeout(); + WakeReleasesAWaiterWithoutAValueChange(); + WakeHonoursTheCountAndTheAddress(); + WakeWithoutWaitersSucceeds(); + NwakeWakesEveryWaiterOnEachListedAddress(); + NwakeReadsOnlyTheCountedEntries(); + SemWaitReturnsAtOnceWhileTheCountIsNonzero(); + SemWaitTimesOutWhileTheCountIsZero(); + SemWakeReleasesTheWaiterAndClearsTheFlag(); + SemWakeWithoutWaitersSucceeds(); + RejectsBadObjectsAndOperations(); +} diff --git a/core/libs/tests/GuestUnwindModuleInfo.cpp b/core/libs/tests/GuestUnwindModuleInfo.cpp index b5af722f49..d0519e368b 100644 --- a/core/libs/tests/GuestUnwindModuleInfo.cpp +++ b/core/libs/tests/GuestUnwindModuleInfo.cpp @@ -2,10 +2,17 @@ #include "tests/GuestUnwindModuleInfoFixture.hpp" #include #include +#include +#include #include +#include #include +#include extern "C" int APS5_VABI sceKernelGetModuleInfoForUnwind(uint64_t addr, int flags, ModuleInfoForUnwind* info); +extern "C" void* APS5_VABI dlopen_nid_postfix(const char*, int); +extern "C" void* APS5_VABI dlsym_nid_postfix(void*, const char*); +extern "C" int APS5_VABI dlclose_nid_postfix(void*); static void Require(bool value) { if (!value) std::abort(); } @@ -19,6 +26,54 @@ static bool Throws(std::uint64_t address) { return false; } +struct FixtureCopy { + void* handle; + UnwindFixture* fixture; + std::uint64_t address; +}; + +static std::string FixtureCopyPath() { + HMODULE linked = nullptr; + Require(GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(GetUnwindFixture()), &linked)); + wchar_t original[MAX_PATH] = {}; + Require(GetModuleFileNameW(linked, original, MAX_PATH) != 0); + const auto copy = std::filesystem::path(original).replace_filename("unwind_fixture_copy.dll"); + std::filesystem::copy_file(original, copy, std::filesystem::copy_options::overwrite_existing); + return copy.string(); +} + +static FixtureCopy LoadFixtureCopy(const std::string& path) { + void* handle = dlopen_nid_postfix(path.c_str(), 2); + Require(handle != nullptr); + const auto get = reinterpret_cast(dlsym_nid_postfix(handle, "GetUnwindFixture_nid_no_patch")); + Require(get != nullptr); + auto* fixture = get(); + return {handle, fixture, reinterpret_cast(fixture)}; +} + +template +static void LookupOfEditedCopy(const std::string& path, Edit edit, bool throws) { + const auto copy = LoadFixtureCopy(path); + edit(*copy.fixture); + Require(Throws(copy.address) == throws); + Require(dlclose_nid_postfix(copy.handle) == 0); +} + +static void ReloadedModuleIsRead(const std::string& path) { + for (int round = 0; round < 2; ++round) { + const auto copy = LoadFixtureCopy(path); + if (round == 0) { + copy.fixture->frames.terminator = 12; + copy.fixture->frames.spare[0] = 1; + } + ModuleInfoForUnwind info{}; + Require(sceKernelGetModuleInfoForUnwind(copy.address, 0, &info) == 0); + Require(info.eh_frame_size == (round == 0 ? 4 + 12 + 4 + 12 : 4 + 12)); + Require(dlclose_nid_postfix(copy.handle) == 0); + } +} + int main() { auto* fixture = GetUnwindFixture(); Require(fixture != nullptr); @@ -38,21 +93,34 @@ int main() { const auto* dos = reinterpret_cast(base); const auto* nt = reinterpret_cast(base + dos->e_lfanew); Require(info.seg0_size == nt->OptionalHeader.SizeOfImage); - - fixture->header.version = 2; - Require(Throws(address)); - fixture->header.version = 1; - for (const std::uint8_t encoding : {0x3b, 0x9b, 0x0f, 0xff}) { - fixture->header.framePointerEncoding = encoding; - Require(Throws(address)); + char mapped[4096] = {}; + Require(GetMappedFileNameA(GetCurrentProcess(), memory.BaseAddress, mapped, sizeof(mapped) - 1) != 0); + Require(std::strncmp(info.name, mapped, sizeof(info.name) - 1) == 0); + for (const auto* inside : {static_cast(&fixture->header), static_cast(&fixture->frames)}) { + ModuleInfoForUnwind other{}; + Require(sceKernelGetModuleInfoForUnwind(reinterpret_cast(inside), 0, &other) == 0); + Require(std::memcmp(&other, &info, sizeof(info)) == 0); } - fixture->header.framePointerEncoding = 0x1b; - fixture->frames.cieLength = 0xffffffffu; - Require(Throws(address)); - fixture->frames.cieLength = 12; - Require(!Throws(address)); + for (int repeat = 0; repeat < 3; ++repeat) { + ModuleInfoForUnwind again{}; + Require(sceKernelGetModuleInfoForUnwind(address, 0, &again) == 0); + Require(std::memcmp(&again, &info, sizeof(info)) == 0); + } + + const auto path = FixtureCopyPath(); + LookupOfEditedCopy(path, [](UnwindFixture& edited) { edited.header.version = 2; }, true); + for (const std::uint8_t encoding : {0x3b, 0x9b, 0x0f, 0xff}) { + LookupOfEditedCopy(path, [encoding](UnwindFixture& edited) { edited.header.framePointerEncoding = encoding; }, true); + } + LookupOfEditedCopy(path, [](UnwindFixture& edited) { edited.frames.cieLength = 0xffffffffu; }, true); + LookupOfEditedCopy(path, [](UnwindFixture&) {}, false); + ReloadedModuleIsRead(path); + std::filesystem::remove(path); ModuleInfoForUnwind host{}; Require(sceKernelGetModuleInfoForUnwind(reinterpret_cast(&GetModuleHandleW), 0, &host) == 0); Require(host.eh_frame_hdr_addr == 0 && host.eh_frame_addr == 0 && host.eh_frame_size == 0); + ModuleInfoForUnwind hostAgain{}; + Require(sceKernelGetModuleInfoForUnwind(reinterpret_cast(&GetModuleHandleW), 0, &hostAgain) == 0); + Require(std::memcmp(&hostAgain, &host, sizeof(host)) == 0); } diff --git a/core/libs/tests/GuestUnwindModuleInfoFixture.cpp b/core/libs/tests/GuestUnwindModuleInfoFixture.cpp index 2e473b4bff..a5abebb36c 100644 --- a/core/libs/tests/GuestUnwindModuleInfoFixture.cpp +++ b/core/libs/tests/GuestUnwindModuleInfoFixture.cpp @@ -21,3 +21,5 @@ extern "C" UnwindFixture* GetUnwindFixture() { UnwindFixtureMeta = static_cast(reinterpret_cast(&fixture.header) - base); return &fixture; } + +extern "C" UnwindFixture* GetUnwindFixture_nid_no_patch() { return GetUnwindFixture(); } diff --git a/core/libs/tests/GuestUnwindModuleInfoFixture.hpp b/core/libs/tests/GuestUnwindModuleInfoFixture.hpp index d03be890d2..08b2e941d6 100644 --- a/core/libs/tests/GuestUnwindModuleInfoFixture.hpp +++ b/core/libs/tests/GuestUnwindModuleInfoFixture.hpp @@ -16,6 +16,8 @@ struct alignas(4) UnwindFixtureFrames { std::uint32_t cieId = 0; std::uint8_t cie[8] = {1, 0, 1, 0x78, 16, 0, 0, 0}; std::uint32_t terminator = 0; + std::uint32_t spare[3] = {}; + std::uint32_t spareTerminator = 0; }; struct UnwindFixture { diff --git a/core/libs/tests/GuestUsbStorageDialog.cpp b/core/libs/tests/GuestUsbStorageDialog.cpp new file mode 100644 index 0000000000..5217f66be0 --- /dev/null +++ b/core/libs/tests/GuestUsbStorageDialog.cpp @@ -0,0 +1,79 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI sceUsbStorageDialogInitialize(void); +int APS5_VABI sceUsbStorageDialogOpen(const void* param); +int APS5_VABI sceUsbStorageDialogUpdateStatus(void); +int APS5_VABI sceUsbStorageDialogGetStatus(void); +int APS5_VABI sceUsbStorageDialogGetResult(void* 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() { + std::int64_t param = 0; + std::int32_t result = 0x5a5a5a5a; + + 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); + + 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); + Require(sceUsbStorageDialogGetResult(&result) == 0); + Require(result == kResultUserCanceled); + + 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 = 0; + Require(sceUsbStorageDialogGetResult(&result) == 0); + Require(result == kResultUserCanceled); + Require(sceUsbStorageDialogTerminate() == 0); + return 0; +} diff --git a/core/libs/tests/GuestUsbd.cpp b/core/libs/tests/GuestUsbd.cpp index 910810ec4c..59e59542b0 100644 --- a/core/libs/tests/GuestUsbd.cpp +++ b/core/libs/tests/GuestUsbd.cpp @@ -3,7 +3,11 @@ #include #include #include +#include #include +#include +#include +#include #include namespace { @@ -21,7 +25,31 @@ void APS5_VABI sceUsbdExit(); std::int64_t APS5_VABI sceUsbdGetDeviceList(void*** list); void APS5_VABI sceUsbdFreeDeviceList(void** list, std::int32_t unrefDevices); std::int32_t APS5_VABI sceUsbdHandleEventsTimeout(const UsbdTimeval* timeout); -int APS5_VABI sceUsbdOpen(); +std::int32_t APS5_VABI sceUsbdOpen(void* device, void** deviceHandle); +void APS5_VABI sceUsbdClose(void* deviceHandle); +void* APS5_VABI sceUsbdRefDevice(void* device); +void APS5_VABI sceUsbdUnrefDevice(void* device); +std::uint8_t APS5_VABI sceUsbdGetBusNumber(void* device); +std::uint8_t APS5_VABI sceUsbdGetDeviceAddress(void* device); +std::int32_t APS5_VABI sceUsbdGetDeviceDescriptor(void* device, void* descriptor); +std::int32_t APS5_VABI sceUsbdGetConfigDescriptor(void* device, std::uint8_t configIndex, void** config); +std::int32_t APS5_VABI sceUsbdGetActiveConfigDescriptor(void* device, void** config); +std::int32_t APS5_VABI sceUsbdClaimInterface(void* deviceHandle, std::int32_t interfaceNumber); +std::int32_t APS5_VABI sceUsbdReleaseInterface(void* deviceHandle, std::int32_t interfaceNumber); +std::int32_t APS5_VABI sceUsbdSetConfiguration(void* deviceHandle, std::int32_t configuration); +std::int32_t APS5_VABI sceUsbdResetDevice(void* deviceHandle); +std::int32_t APS5_VABI sceUsbdCheckConnected(void* deviceHandle); +std::int32_t APS5_VABI sceUsbdKernelDriverActive(void* deviceHandle, std::int32_t interfaceNumber); +std::int32_t APS5_VABI sceUsbdAttachKernelDriver(void* deviceHandle, std::int32_t interfaceNumber); +std::int32_t APS5_VABI sceUsbdControlTransfer(void* deviceHandle, std::uint8_t requestType, std::uint8_t request, std::uint16_t value, std::uint16_t index, std::uint8_t* data, std::uint16_t length, std::uint32_t timeout); +std::int32_t APS5_VABI sceUsbdGetStringDescriptor(void* deviceHandle, std::uint8_t descriptorIndex, std::uint16_t languageId, std::uint8_t* data, std::int32_t length); +std::int32_t APS5_VABI sceUsbdSubmitTransfer(void* transfer); +void* APS5_VABI sceUsbdAllocTransfer(std::int32_t isoPackets); +void APS5_VABI sceUsbdFreeTransfer(void* transfer); +void APS5_VABI sceUsbdFillInterruptTransfer(void* transfer, void* deviceHandle, std::uint8_t endpoint, std::uint8_t* buffer, std::int32_t length, void* callback, void* userData, std::uint32_t timeout); +std::int32_t APS5_VABI sceUsbdCancelTransfer(void* transfer); +std::int32_t APS5_VABI sceUsbdEventHandlingOk(); +void APS5_VABI sceUsbdFreeConfigDescriptor(void* config); } namespace { @@ -35,14 +63,178 @@ void Require(bool condition, const char* message) { } } +template +bool Throws(TCall call) { + try { + call(); + } catch (const std::runtime_error&) { + return true; + } + return false; } -int main() { +struct HostTransfer { + void* deviceHandle; + std::uint8_t flags; + std::uint8_t endpoint; + std::uint8_t type; + std::uint32_t timeout; + std::int32_t status; + std::int32_t length; + std::int32_t actualLength; + void (APS5_VABI* callback)(HostTransfer*); + void* userData; + std::uint8_t* buffer; + std::int32_t numIsoPackets; +}; + +void APS5_VABI MarkDone(HostTransfer* transfer) { + *static_cast(transfer->userData) += 1; +} + +void APS5_VABI MarkDoneThenSignal(HostTransfer* transfer) { + auto* notified = static_cast*>(transfer->userData); + notified->store(true, std::memory_order_release); +} + +void Pump(const int& done) { + const UsbdTimeval slice{0, 100000}; + for (int round = 0; round < 50 && done == 0; ++round) Require(sceUsbdHandleEventsTimeout(&slice) == 0, "event handling failed while a transfer was in flight"); +} + +void Device() { + const char* wanted = std::getenv("APS5_USBD_DEVICE"); + Require(wanted != nullptr, "APS5_USBD_DEVICE is not set to vendor:product in hex"); + const unsigned long vendor = std::strtoul(wanted, nullptr, 16); + const char* separator = std::strchr(wanted, ':'); + Require(separator != nullptr, "APS5_USBD_DEVICE has no ':'"); + const unsigned long product = std::strtoul(separator + 1, nullptr, 16); + void** list = nullptr; + const std::int64_t count = sceUsbdGetDeviceList(&list); + void* device = nullptr; + std::uint8_t descriptor[18] = {}; + for (std::int64_t index = 0; index < count && device == nullptr; ++index) { + Require(sceUsbdGetDeviceDescriptor(list[index], descriptor) == 0, "device descriptor failed"); + if ((descriptor[8] | descriptor[9] << 8) == vendor && (descriptor[10] | descriptor[11] << 8) == product) device = sceUsbdRefDevice(list[index]); + } + sceUsbdFreeDeviceList(list, 1); + Require(device != nullptr, "the APS5_USBD_DEVICE device was not listed"); + std::printf("device bus %u address %u\n", sceUsbdGetBusNumber(device), sceUsbdGetDeviceAddress(device)); + void* config = nullptr; + Require(sceUsbdGetActiveConfigDescriptor(device, &config) == 0 && config != nullptr, "active config descriptor failed"); + const auto* raw = static_cast(config); + Require(raw[1] == 2 && raw[4] > 0, "active config descriptor is not a configuration with interfaces"); + const std::uint8_t interfaces = raw[4]; + sceUsbdFreeConfigDescriptor(config); + Require(Throws([&] { sceUsbdFreeConfigDescriptor(config); }), "a config descriptor was freed twice"); + Require(sceUsbdGetConfigDescriptor(device, 0, &config) == 0, "config descriptor 0 failed"); + sceUsbdFreeConfigDescriptor(config); + void* handle = nullptr; + Require(sceUsbdOpen(device, &handle) == 0 && handle != nullptr, "opening the device failed"); + Require(sceUsbdCheckConnected(handle) == 0, "an open device was reported disconnected"); + std::uint8_t text[256] = {}; + if (descriptor[15] != 0) { + const std::int32_t length = sceUsbdGetStringDescriptor(handle, descriptor[15], 0x409, text, sizeof(text)); + Require(length >= 2 && text[1] == 3, "product string descriptor failed"); + std::string product_name; + for (std::int32_t index = 2; index + 1 < length; index += 2) product_name += static_cast(text[index]); + std::printf("product \"%s\"\n", product_name.c_str()); + } + std::uint8_t again[18] = {}; + Require(sceUsbdControlTransfer(handle, 0x80, 6, 0x0100, 0, again, sizeof(again), 1000) == 18, "synchronous device descriptor request failed"); + Require(std::memcmp(again, descriptor, sizeof(again)) == 0, "device descriptor read over the wire differs from the cached one"); + for (std::int32_t interfaceNumber = 0; interfaceNumber < interfaces; ++interfaceNumber) { + Require(sceUsbdClaimInterface(handle, interfaceNumber) == 0, "claiming an interface failed"); + Require(sceUsbdReleaseInterface(handle, interfaceNumber) == 0, "releasing an interface failed"); + } + Require(sceUsbdReleaseInterface(handle, 0) == static_cast(0x80240005), "releasing an unclaimed interface did not report not found"); + Require(sceUsbdGetConfigDescriptor(device, 0, &config) == 0, "config descriptor 0 for the endpoint scan failed"); + const std::uint8_t* config0 = static_cast(config); + const std::uint8_t* interfaceArray = *reinterpret_cast(config0 + 16); + std::uint8_t interruptAddress = 0; + std::uint16_t interruptMax = 0; + if (interfaceArray == nullptr) { + Require(false, "the active config descriptor has no interface array"); + } + for (std::uint8_t index = 0; index < interfaces && interruptAddress == 0; ++index) { + const std::uint8_t* alternate = *reinterpret_cast(interfaceArray + index * 16); + if (alternate == nullptr) continue; + const std::uint8_t* endpoints = *reinterpret_cast(alternate + 16); + if (endpoints == nullptr) continue; + for (std::uint8_t endpoint = 0; endpoint < alternate[4]; ++endpoint) { + const std::uint8_t* item = endpoints + endpoint * 32; + if ((item[2] & 0x80) != 0 && (item[3] & 3) == 3) { + interruptAddress = item[2]; + interruptMax = static_cast(item[4] | item[5] << 8); + } + } + } + Require(interruptAddress != 0 && interruptMax != 0, "the device has no interrupt IN endpoint"); + sceUsbdFreeConfigDescriptor(config); + + auto* transfer = static_cast(sceUsbdAllocTransfer(0)); + std::uint8_t setup[8 + 18] = {0x80, 6, 0x00, 0x01, 0x00, 0x00, 18, 0}; + int done = 0; + transfer->deviceHandle = handle; + transfer->type = 0; + transfer->timeout = 1000; + transfer->buffer = setup; + transfer->length = sizeof(setup); + transfer->callback = MarkDone; + transfer->userData = &done; + Require(sceUsbdSubmitTransfer(transfer) == 0, "submitting an asynchronous control transfer failed"); + Require(Throws([&] { sceUsbdFreeTransfer(transfer); }), "an in-flight transfer was freed"); + Pump(done); + Require(done == 1 && transfer->status == 0 && transfer->actualLength == 18, "asynchronous control transfer did not complete"); + Require(std::memcmp(setup + 8, descriptor, 18) == 0, "asynchronous device descriptor differs from the cached one"); + bool broken = false; + { + const UsbdTimeval slice{0, 200}; + for (int round = 0; round < 300 && !broken; ++round) { + std::atomic notified{false}; + std::uint8_t blink[64] = {}; + sceUsbdFillInterruptTransfer(transfer, handle, interruptAddress, blink, interruptMax, reinterpret_cast(&MarkDoneThenSignal), ¬ified, 0); + const int submitResult = sceUsbdSubmitTransfer(transfer); + if (submitResult != 0) { + broken = true; + break; + } + std::thread canceller([&] { + const int outcome = sceUsbdCancelTransfer(transfer); + if (outcome != 0 && outcome != static_cast(0x80240005)) broken = true; + }); + while (!notified.load(std::memory_order_acquire)) { + if (sceUsbdHandleEventsTimeout(&slice) != 0) broken = true; + } + canceller.join(); + if (transfer->status != 0 && transfer->status != 2 && transfer->status != 3) broken = true; + } + } + Require(!broken, "the cancel race failed"); + + Require(sceUsbdCancelTransfer(transfer) == static_cast(0x80240005), "cancelling a completed transfer did not report not found"); + sceUsbdFreeTransfer(transfer); + sceUsbdClose(handle); + Require(Throws([&] { sceUsbdClose(handle); }), "a handle was closed twice"); + sceUsbdUnrefDevice(device); + std::puts("USBD device tests passed"); +} + +} + +int main(int argc, char** argv) { Require(sceUsbdInit() == 0, "initialization failed"); void** list = nullptr; - Require(sceUsbdGetDeviceList(&list) == 0, "a device was listed"); - Require(list != nullptr && list[0] == nullptr, "device list is not an empty null-terminated array"); + const std::int64_t count = sceUsbdGetDeviceList(&list); + Require(count >= 0 && list != nullptr && list[count] == nullptr, "device list is not a null-terminated array of the reported length"); + for (std::int64_t index = 0; index < count; ++index) { + std::uint8_t deviceDescriptor[18] = {}; + Require(sceUsbdGetDeviceDescriptor(list[index], deviceDescriptor) == 0 && deviceDescriptor[0] == 18 && deviceDescriptor[1] == 1, "listed device has no device descriptor"); + Require(sceUsbdRefDevice(list[index]) == list[index], "listed device was not referenced"); + sceUsbdUnrefDevice(list[index]); + } sceUsbdFreeDeviceList(list, 1); + Require(Throws([&] { sceUsbdFreeDeviceList(list, 1); }), "a device list was freed twice"); Require(sceUsbdGetDeviceList(nullptr) == invalidArgument, "null list accepted"); Require(sceUsbdHandleEventsTimeout(nullptr) == invalidArgument, "null timeout accepted"); const UsbdTimeval invalid{0, 1000000}; @@ -51,13 +243,57 @@ int main() { const auto start = std::chrono::steady_clock::now(); Require(sceUsbdHandleEventsTimeout(&timeout) == 0, "event handling failed"); Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(45), "event handling returned before the timeout"); - bool threw = false; - try { - sceUsbdOpen(); - } catch (const std::runtime_error&) { - threw = true; - } - Require(threw, "opening a device did not throw"); + int foreign = 0; + void* handle = &foreign; + Require(sceUsbdOpen(&foreign, nullptr) == invalidArgument, "null handle output accepted"); + Require(Throws([&] { sceUsbdOpen(nullptr, &handle); }), "opening a null device did not throw"); + Require(Throws([&] { sceUsbdOpen(&foreign, &handle); }), "opening a device that was never listed did not throw"); + sceUsbdClose(nullptr); + Require(Throws([&] { sceUsbdClose(&foreign); }), "closing a handle that was never opened did not throw"); + Require(sceUsbdRefDevice(nullptr) == nullptr, "null device was referenced"); + sceUsbdUnrefDevice(nullptr); + Require(Throws([&] { sceUsbdRefDevice(&foreign); }), "referencing a device that was never listed did not throw"); + Require(Throws([&] { sceUsbdUnrefDevice(&foreign); }), "unreferencing a device that was never listed did not throw"); + Require(sceUsbdGetBusNumber(nullptr) == 0 && sceUsbdGetDeviceAddress(nullptr) == 0, "null device reported a bus or address"); + Require(Throws([&] { sceUsbdGetBusNumber(&foreign); }), "bus number of a device that was never listed did not throw"); + std::uint8_t descriptor[18] = {}; + void* config = &foreign; + Require(sceUsbdGetDeviceDescriptor(nullptr, descriptor) == invalidArgument, "null device descriptor accepted"); + Require(sceUsbdGetDeviceDescriptor(&foreign, nullptr) == invalidArgument, "null descriptor output accepted"); + Require(sceUsbdGetConfigDescriptor(nullptr, 0, &config) == invalidArgument, "null config device accepted"); + Require(sceUsbdGetConfigDescriptor(&foreign, 0, nullptr) == invalidArgument, "null config output accepted"); + Require(Throws([&] { sceUsbdGetConfigDescriptor(&foreign, 0, &config); }), "config of a device that was never listed did not throw"); + Require(Throws([&] { sceUsbdGetActiveConfigDescriptor(nullptr, &config); }), "active config of a null device did not throw"); + Require(sceUsbdClaimInterface(nullptr, 0) == invalidArgument && sceUsbdReleaseInterface(nullptr, 0) == invalidArgument, "null handle interface call accepted"); + Require(sceUsbdSetConfiguration(nullptr, 1) == invalidArgument && sceUsbdResetDevice(nullptr) == invalidArgument, "null handle configuration or reset accepted"); + Require(sceUsbdCheckConnected(nullptr) == invalidArgument, "null handle connection check accepted"); + Require(sceUsbdKernelDriverActive(nullptr, 0) == invalidArgument && sceUsbdAttachKernelDriver(nullptr, 0) == invalidArgument, "null handle kernel driver call accepted"); + Require(sceUsbdControlTransfer(nullptr, 0x80, 6, 0x100, 0, descriptor, 18, 1000) == invalidArgument, "null handle control transfer accepted"); + Require(sceUsbdControlTransfer(&foreign, 0x80, 6, 0x100, 0, nullptr, 18, 1000) == invalidArgument, "control transfer without data accepted"); + Require(Throws([&] { sceUsbdControlTransfer(&foreign, 0x80, 6, 0x100, 0, descriptor, 18, 1000); }), "control transfer on a handle that was never opened did not throw"); + Require(sceUsbdGetStringDescriptor(nullptr, 1, 0x409, descriptor, 18) == invalidArgument, "null handle string descriptor accepted"); + Require(sceUsbdGetStringDescriptor(&foreign, 1, 0x409, descriptor, 0) == invalidArgument, "empty string descriptor buffer accepted"); + Require(Throws([&] { sceUsbdClaimInterface(&foreign, 0); }), "claiming on a handle that was never opened did not throw"); + auto* transfer = static_cast(sceUsbdAllocTransfer(2)); + Require(transfer != nullptr, "transfer allocation failed"); + Require(*reinterpret_cast(transfer + 56) == 2, "iso packet count not stored"); + Require(sceUsbdAllocTransfer(-1) == nullptr, "negative iso packet count accepted"); + std::uint8_t buffer[8] = {}; + int user = 0; + sceUsbdFillInterruptTransfer(transfer, nullptr, 0x81, buffer, 8, reinterpret_cast(&Require), &user, 250); + Require(transfer[9] == 0x81 && transfer[10] == 3, "endpoint or interrupt type not filled"); + Require(*reinterpret_cast(transfer + 12) == 250 && *reinterpret_cast(transfer + 20) == 8, "timeout or length not filled"); + Require(*reinterpret_cast(transfer + 40) == &user && *reinterpret_cast(transfer + 48) == buffer, "user data or buffer not filled"); + Require(sceUsbdCancelTransfer(transfer) == static_cast(0x80240005), "cancelling an unsubmitted transfer did not report not found"); + Require(sceUsbdSubmitTransfer(nullptr) == invalidArgument, "null transfer submitted"); + Require(sceUsbdSubmitTransfer(transfer) == invalidArgument, "transfer without a handle submitted"); + sceUsbdFillInterruptTransfer(transfer, &foreign, 0x81, buffer, 8, reinterpret_cast(&Require), &user, 250); + Require(Throws([&] { sceUsbdSubmitTransfer(transfer); }), "submitting on a handle that was never opened did not throw"); + sceUsbdFreeTransfer(transfer); + sceUsbdFreeTransfer(nullptr); + Require(sceUsbdEventHandlingOk() == 1, "event handling not allowed"); + sceUsbdFreeConfigDescriptor(nullptr); + if (argc == 2 && std::strcmp(argv[1], "device") == 0) Device(); sceUsbdExit(); std::puts("USBD tests passed"); return 0; diff --git a/core/libs/tests/GuestUserContext.cpp b/core/libs/tests/GuestUserContext.cpp new file mode 100644 index 0000000000..c616e2260d --- /dev/null +++ b/core/libs/tests/GuestUserContext.cpp @@ -0,0 +1,509 @@ +#include "prx/libc/include/general/AsmMacros.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/System/include/UserContext.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#endif + +using ContextEntry = void (APS5_VABI*)(); + +extern "C" { +[[gnu::returns_twice]] int APS5_VABI getcontext_nid_postfix(UserUcontext* context); +int APS5_VABI setcontext_nid_postfix(const UserUcontext* context); +void APS5_VABI makecontext_nid_postfix(UserUcontext* context, ContextEntry entry, int count, ...); +[[gnu::returns_twice]] int APS5_VABI setjmp_nid_postfix(std::uint64_t* buffer); +[[noreturn]] void APS5_VABI longjmp_nid_postfix(std::uint64_t* buffer, int value); +[[gnu::returns_twice]] int APS5_VABI sigsetjmp_nid_postfix(std::uint64_t* buffer, int saveMask); +[[noreturn]] void APS5_VABI siglongjmp_nid_postfix(std::uint64_t* buffer, int value); +int APS5_VABI sigprocmask_nid_postfix(int how, const void* set, void* previousSet); +int* APS5_VABI __error_nid_postfix(); + +int APS5_VABI CaptureKnownRegisters(UserUcontext* context); +void CaptureReturnAddress(); +void RecordRegisters(); +void APS5_VABI EntryThunk(); + +std::uint64_t CapturedStackPointer = 0; +std::uint64_t EntryStackPointer = 0; +std::uint64_t RecordedRegisters[18] = {}; +std::uint32_t RecordedMxcsr = 0; +std::uint16_t RecordedControlWord = 0; +alignas(16) std::uint8_t RecordedXmm[16 * 16] = {}; +UserUcontext ReturnContext = {}; +} + +asm(".text\n" + APS5_ASM_FUNCTION("CaptureKnownRegisters") + " push %rbx\n" + " push %rbp\n" + " push %r12\n" + " push %r13\n" + " push %r14\n" + " push %r15\n" + " sub $8, %rsp\n" + " movabsq $0x1111111111111111, %rbx\n" + " movabsq $0x2222222222222222, %rbp\n" + " movabsq $0x3333333333333333, %r12\n" + " movabsq $0x4444444444444444, %r13\n" + " movabsq $0x5555555555555555, %r14\n" + " movabsq $0x6666666666666666, %r15\n" + " movabsq $0x7777777777777777, %rcx\n" + " movabsq $0x8888888888888888, %rsi\n" + " movabsq $0x9999999999999999, %r8\n" + " movabsq $0xaaaaaaaaaaaaaaaa, %r9\n" + " movabsq $0xbbbbbbbbbbbbbbbb, %r10\n" + " movabsq $0xcccccccccccccccc, %r11\n" + " movabsq $0xdddddddddddddddd, %rdx\n" + " movabsq $0xeeeeeeeeeeeeeeee, %rax\n" + " mov %rsp, " APS5_ASM_SYMBOL("CapturedStackPointer") "(%rip)\n" + " stc\n" + " call " APS5_ASM_SYMBOL("getcontext_nid_postfix") "\n" + APS5_ASM_FUNCTION("CaptureReturnAddress") + " add $8, %rsp\n" + " pop %r15\n" + " pop %r14\n" + " pop %r13\n" + " pop %r12\n" + " pop %rbp\n" + " pop %rbx\n" + " ret\n" + APS5_ASM_FUNCTION("RecordRegisters") + " pushfq\n" + " mov %rax, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x00(%rip)\n" + " pop %rax\n" + " mov %rax, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x88(%rip)\n" + " mov %rbx, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x08(%rip)\n" + " mov %rcx, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x10(%rip)\n" + " mov %rdx, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x18(%rip)\n" + " mov %rsi, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x20(%rip)\n" + " mov %rdi, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x28(%rip)\n" + " mov %rbp, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x30(%rip)\n" + " mov %rsp, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x38(%rip)\n" + " mov %r8, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x40(%rip)\n" + " mov %r9, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x48(%rip)\n" + " mov %r10, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x50(%rip)\n" + " mov %r11, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x58(%rip)\n" + " mov %r12, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x60(%rip)\n" + " mov %r13, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x68(%rip)\n" + " mov %r14, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x70(%rip)\n" + " mov %r15, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x78(%rip)\n" + " stmxcsr " APS5_ASM_SYMBOL("RecordedMxcsr") "(%rip)\n" + " fnstcw " APS5_ASM_SYMBOL("RecordedControlWord") "(%rip)\n" + " movdqu %xmm0, " APS5_ASM_SYMBOL("RecordedXmm") "+0x00(%rip)\n" + " movdqu %xmm1, " APS5_ASM_SYMBOL("RecordedXmm") "+0x10(%rip)\n" + " movdqu %xmm2, " APS5_ASM_SYMBOL("RecordedXmm") "+0x20(%rip)\n" + " movdqu %xmm3, " APS5_ASM_SYMBOL("RecordedXmm") "+0x30(%rip)\n" + " movdqu %xmm4, " APS5_ASM_SYMBOL("RecordedXmm") "+0x40(%rip)\n" + " movdqu %xmm5, " APS5_ASM_SYMBOL("RecordedXmm") "+0x50(%rip)\n" + " movdqu %xmm6, " APS5_ASM_SYMBOL("RecordedXmm") "+0x60(%rip)\n" + " movdqu %xmm7, " APS5_ASM_SYMBOL("RecordedXmm") "+0x70(%rip)\n" + " movdqu %xmm8, " APS5_ASM_SYMBOL("RecordedXmm") "+0x80(%rip)\n" + " movdqu %xmm9, " APS5_ASM_SYMBOL("RecordedXmm") "+0x90(%rip)\n" + " movdqu %xmm10, " APS5_ASM_SYMBOL("RecordedXmm") "+0xa0(%rip)\n" + " movdqu %xmm11, " APS5_ASM_SYMBOL("RecordedXmm") "+0xb0(%rip)\n" + " movdqu %xmm12, " APS5_ASM_SYMBOL("RecordedXmm") "+0xc0(%rip)\n" + " movdqu %xmm13, " APS5_ASM_SYMBOL("RecordedXmm") "+0xd0(%rip)\n" + " movdqu %xmm14, " APS5_ASM_SYMBOL("RecordedXmm") "+0xe0(%rip)\n" + " movdqu %xmm15, " APS5_ASM_SYMBOL("RecordedXmm") "+0xf0(%rip)\n" + " cld\n" + " lea " APS5_ASM_SYMBOL("ReturnContext") "(%rip), %rdi\n" + " and $-16, %rsp\n" + " call " APS5_ASM_SYMBOL("setcontext_nid_postfix") "\n" + " ud2\n" + APS5_ASM_FUNCTION("EntryThunk") + " mov %rsp, " APS5_ASM_SYMBOL("EntryStackPointer") "(%rip)\n" + " jmp " APS5_ASM_SYMBOL("ContextBody") "\n"); + +namespace { + +constexpr int GuestEinval = 22; +constexpr int GuestSigBlock = 1; +constexpr int GuestSigSetmask = 3; +constexpr std::uint64_t UserChangeableFlags = 0x254dd5; +constexpr std::uint64_t InterruptFlag = 0x200; +constexpr std::size_t FxsaveMxcsr = 24; +constexpr std::size_t FxsaveXmm = 160; +constexpr std::size_t StackBytes = 256 * 1024; + +struct alignas(16) Stack { + unsigned char bytes[StackBytes]; +}; + +struct Fibre { + UserUcontext context; + std::uint64_t jump[12]; + int started; +}; + +Stack g_restoreStack; +Stack g_contextStack; +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; +bool g_signalJumps = false; + +void Require(bool value, const char* what) { + if (!value) { + std::fprintf(stderr, "User context check failed: %s\n", what); + std::abort(); + } +} + +std::uint16_t ControlWord() { + std::uint16_t control = 0; + asm volatile("fnstcw %0" : "=m"(control)); + return control; +} + +std::uint64_t Pattern(unsigned index) { + return 0x0101010101010101ull * (index + 1) + 0x0f00000000000000ull; +} + +void CheckStackBounds(const Stack* stack, const char* what) { +#ifdef _WIN32 + ULONG_PTR low = 0; + ULONG_PTR high = 0; + GetCurrentThreadStackLimits(&low, &high); + Require(low == reinterpret_cast(stack->bytes) && high == reinterpret_cast(stack->bytes + StackBytes), what); +#else + (void)stack; + (void)what; +#endif +} + +void ThrowAndCatch() { + bool caught = false; + try { + throw std::runtime_error("host exception on a context stack"); + } catch (const std::runtime_error&) { + caught = true; + } + 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); + page[sizeof(page) - 1] = static_cast(depth); + if (depth > 0) GrowStack(depth - 1); + Require(page[0] == static_cast(depth), "a deep host frame keeps its contents"); +} + +void CheckErrors() { + *__error_nid_postfix() = 0; + Require(getcontext_nid_postfix(nullptr) == -1 && *__error_nid_postfix() == GuestEinval, "getcontext rejects a null context"); + *__error_nid_postfix() = 0; + Require(setcontext_nid_postfix(nullptr) == -1 && *__error_nid_postfix() == GuestEinval, "setcontext rejects a null context"); + UserUcontext context{}; + Require(getcontext_nid_postfix(&context) == 0, "capture a context to corrupt"); + context.mcontext.len = 0; + *__error_nid_postfix() = 0; + Require(setcontext_nid_postfix(&context) == -1 && *__error_nid_postfix() == GuestEinval, "setcontext rejects a wrong mc_len"); + context.mcontext.len = sizeof(UserMcontext); + context.mcontext.flags = 0x8; + *__error_nid_postfix() = 0; + Require(setcontext_nid_postfix(&context) == -1 && *__error_nid_postfix() == GuestEinval, "setcontext rejects unknown mc_flags"); + context.mcontext.flags = 0; + UserUcontext invalid = context; + invalid.stackPointer = g_contextStack.bytes; + invalid.stackSize = StackBytes; + makecontext_nid_postfix(&invalid, &EntryThunk, 7); + Require(invalid.mcontext.len == 0, "makecontext invalidates a context with more than six arguments"); + invalid = context; + invalid.stackPointer = g_contextStack.bytes; + invalid.stackSize = 2047; + makecontext_nid_postfix(&invalid, &EntryThunk, 0); + Require(invalid.mcontext.len == 0, "makecontext invalidates a context with a stack below MINSIGSTKSZ"); + invalid = context; + invalid.stackPointer = nullptr; + invalid.stackSize = StackBytes; + makecontext_nid_postfix(&invalid, &EntryThunk, 0); + Require(invalid.mcontext.len == 0, "makecontext invalidates a context without a stack"); + invalid = context; + invalid.stackPointer = g_contextStack.bytes; + invalid.stackSize = StackBytes; + invalid.mcontext.len = 0; + makecontext_nid_postfix(&invalid, &EntryThunk, 0); + Require(invalid.mcontext.len == 0 && invalid.mcontext.rip == context.mcontext.rip, "makecontext leaves a context without a valid mc_len untouched"); + makecontext_nid_postfix(nullptr, &EntryThunk, 0); +} + +void CheckCapture() { + UserUcontext context; + std::memset(&context, 0xa5, sizeof(context)); + UserUcontext untouched; + std::memset(&untouched, 0xa5, sizeof(untouched)); + std::uint32_t previous[4] = {}; + const std::uint32_t blocked[4] = {0x24, 0, 0, 0}; + Require(sigprocmask_nid_postfix(GuestSigSetmask, blocked, previous) == 0, "block signals before the capture"); + Require(CaptureKnownRegisters(&context) == 0, "getcontext returns 0"); + Require(sigprocmask_nid_postfix(GuestSigSetmask, previous, nullptr) == 0, "restore the signal mask"); + const auto& machine = context.mcontext; + Require(machine.rbx == 0x1111111111111111ull && machine.rbp == 0x2222222222222222ull, "getcontext saves rbx and rbp"); + Require(machine.r12 == 0x3333333333333333ull && machine.r13 == 0x4444444444444444ull, "getcontext saves r12 and r13"); + Require(machine.r14 == 0x5555555555555555ull && machine.r15 == 0x6666666666666666ull, "getcontext saves r14 and r15"); + Require(machine.rcx == 0x7777777777777777ull && machine.rsi == 0x8888888888888888ull, "getcontext saves rcx and rsi"); + Require(machine.r8 == 0x9999999999999999ull && machine.r9 == 0xaaaaaaaaaaaaaaaaull, "getcontext saves r8 and r9"); + Require(machine.r10 == 0xbbbbbbbbbbbbbbbbull && machine.r11 == 0xccccccccccccccccull, "getcontext saves r10 and r11"); + Require(machine.rdi == reinterpret_cast(&context), "getcontext saves rdi"); + Require(machine.rax == 0 && machine.rdx == 0, "getcontext stores rax and rdx as 0"); + Require((machine.rflags & 1) == 0, "getcontext clears the carry flag"); + Require(machine.rip == reinterpret_cast(&CaptureReturnAddress), "getcontext saves its return address"); + Require(machine.rsp == CapturedStackPointer, "getcontext saves the caller's stack pointer"); + Require(machine.len == sizeof(UserMcontext) && machine.fpformat == 0x10002 && machine.ownedfp == 0x20001, "getcontext sets mc_len, mc_fpformat and mc_ownedfp"); + Require(machine.onstack == 0 && machine.trapno == 0 && machine.addr == 0 && machine.flags == 0 && machine.err == 0, "getcontext zeroes the trap fields"); + Require(machine.fsbase == 0 && machine.gsbase == 0 && machine.xfpustate == 0 && machine.xfpustateLength == 0, "getcontext zeroes the bases and the extended FPU state"); + for (const auto word : machine.spare) Require(word == 0, "getcontext zeroes mc_spare"); + Require(context.sigmask[0] == 0x24 && context.sigmask[1] == 0 && context.sigmask[2] == 0 && context.sigmask[3] == 0, "getcontext saves the signal mask"); + std::uint32_t mxcsr = 0; + std::memcpy(&mxcsr, reinterpret_cast(machine.fpstate) + FxsaveMxcsr, sizeof(mxcsr)); + std::uint16_t control = 0; + std::memcpy(&control, machine.fpstate, sizeof(control)); + Require(mxcsr == _mm_getcsr() && control == ControlWord(), "getcontext saves MXCSR and the x87 control word"); + Require(std::memcmp(&context.link, &untouched.link, sizeof(UserUcontext) - offsetof(UserUcontext, link)) == 0, "getcontext keeps uc_link, uc_stack and uc_flags"); +} + +void CheckRestore() { + const auto mxcsrBefore = _mm_getcsr(); + const auto controlBefore = ControlWord(); + UserUcontext target{}; + volatile int phase = 0; + Require(getcontext_nid_postfix(&ReturnContext) == 0, "capture the return context"); + if (phase == 0) { + phase = 1; + Require(getcontext_nid_postfix(&target) == 0, "capture the target template"); + auto& machine = target.mcontext; + machine.rax = Pattern(0); + machine.rbx = Pattern(1); + machine.rcx = Pattern(2); + machine.rdx = Pattern(3); + machine.rsi = Pattern(4); + machine.rdi = Pattern(5); + machine.rbp = Pattern(6); + machine.r8 = Pattern(8); + machine.r9 = Pattern(9); + machine.r10 = Pattern(10); + machine.r11 = Pattern(11); + machine.r12 = Pattern(12); + machine.r13 = Pattern(13); + machine.r14 = Pattern(14); + machine.r15 = Pattern(15); + machine.rsp = reinterpret_cast(g_restoreStack.bytes + StackBytes - 256); + machine.rip = reinterpret_cast(&RecordRegisters); + machine.rflags = (machine.rflags & ~UserChangeableFlags) | 0x8c1; + auto* image = reinterpret_cast(machine.fpstate); + const std::uint16_t control = 0x027f; + std::memcpy(image, &control, sizeof(control)); + const std::uint32_t mxcsr = 0x80007f80u; + std::memcpy(image + FxsaveMxcsr, &mxcsr, sizeof(mxcsr)); + for (unsigned index = 0; index < 32; ++index) { + const auto value = Pattern(index + 16); + std::memcpy(image + FxsaveXmm + index * 8, &value, sizeof(value)); + } + setcontext_nid_postfix(&target); + Require(false, "setcontext returned from a valid context"); + } + const auto& machine = target.mcontext; + Require(RecordedRegisters[0] == Pattern(0) && RecordedRegisters[1] == Pattern(1) && RecordedRegisters[2] == Pattern(2) && RecordedRegisters[3] == Pattern(3), "setcontext restores rax, rbx, rcx and rdx"); + Require(RecordedRegisters[4] == Pattern(4) && RecordedRegisters[5] == Pattern(5) && RecordedRegisters[6] == Pattern(6), "setcontext restores rsi, rdi and rbp"); + for (unsigned index = 8; index < 16; ++index) Require(RecordedRegisters[index] == Pattern(index), "setcontext restores r8 to r15"); + Require(RecordedRegisters[7] == machine.rsp, "setcontext restores rsp without touching the target stack"); + Require((RecordedRegisters[17] & UserChangeableFlags) == 0x8c1 && (RecordedRegisters[17] & InterruptFlag) != 0, "setcontext restores the user-changeable flags only"); + Require(RecordedMxcsr == 0x7f80u, "setcontext restores MXCSR without its reserved bits"); + Require(RecordedControlWord == 0x027f, "setcontext restores the x87 control word"); + for (unsigned index = 0; index < 32; ++index) { + std::uint64_t value = 0; + std::memcpy(&value, RecordedXmm + index * 8, sizeof(value)); + Require(value == Pattern(index + 16), "setcontext restores XMM0 to XMM15"); + } + Require(_mm_getcsr() == mxcsrBefore && ControlWord() == controlBefore, "the return context restores MXCSR and the x87 control word"); +} + +} + +extern "C" void APS5_VABI ContextBody(std::uint64_t first, std::uint64_t second, std::uint64_t third, std::uint64_t fourth, std::uint64_t fifth, std::uint64_t sixth) { + g_arguments[0] = first; + g_arguments[1] = second; + g_arguments[2] = third; + g_arguments[3] = fourth; + g_arguments[4] = fifth; + g_arguments[5] = sixth; + CheckStackBounds(&g_contextStack, "the stack bounds follow setcontext onto the context stack"); + WalkContextChain(); + ThrowAndCatch(); + ++g_entries; +} + +extern "C" void APS5_VABI ReturnWithoutLink() {} + +namespace { + +[[gnu::noinline]] void Switch(Fibre* from, Fibre* to) { + from->started = 1; + if (g_signalJumps) { + if (sigsetjmp_nid_postfix(from->jump, 1) == 0) { + if (to->started) siglongjmp_nid_postfix(to->jump, 1); + setcontext_nid_postfix(&to->context); + Require(false, "setcontext into the job returned"); + } + return; + } + if (setjmp_nid_postfix(from->jump) == 0) { + if (to->started) longjmp_nid_postfix(to->jump, 1); + setcontext_nid_postfix(&to->context); + Require(false, "setcontext into the job returned"); + } +} + +} + +extern "C" void APS5_VABI JobEntry() { + for (;;) { + ++g_jobRuns; + CheckStackBounds(&g_jobStack, g_signalJumps ? "the stack bounds follow siglongjmp onto the job stack" : "the stack bounds follow longjmp onto the job stack"); + ThrowAndCatch(); + Switch(&g_job, &g_dispatcher); + } +} + +namespace { + +void CheckMakecontext() { +#ifdef _WIN32 + ULONG_PTR threadLow = 0; + ULONG_PTR threadHigh = 0; + GetCurrentThreadStackLimits(&threadLow, &threadHigh); +#endif + UserUcontext caller{}; + UserUcontext context{}; + volatile int phase = 0; + Require(getcontext_nid_postfix(&caller) == 0, "capture the caller"); + if (phase == 0) { + phase = 1; + Require(getcontext_nid_postfix(&context) == 0, "capture the context template"); + context.stackPointer = g_contextStack.bytes; + context.stackSize = StackBytes; + context.link = &caller; + makecontext_nid_postfix(&context, &EntryThunk, 6, 0x8000000000000001ull, 0x8000000000000002ull, 0x8000000000000003ull, 0x8000000000000004ull, 0x8000000000000005ull, 0x8000000000000006ull); + Require(context.mcontext.len == sizeof(UserMcontext), "makecontext keeps the context valid"); + setcontext_nid_postfix(&context); + 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"); + Require((EntryStackPointer + 8) % 16 == 0, "the made context's function starts with an aligned stack"); +#ifdef _WIN32 + ULONG_PTR low = 0; + ULONG_PTR high = 0; + GetCurrentThreadStackLimits(&low, &high); + Require(low == threadLow && high == threadHigh, "the stack bounds follow setcontext back to the thread stack"); +#endif +} + +void CheckJumpBuffers(bool signalJumps) { + g_signalJumps = signalJumps; + g_jobRuns = 0; + g_dispatcher = {}; + g_job = {}; +#ifdef _WIN32 + ULONG_PTR threadLow = 0; + ULONG_PTR threadHigh = 0; + GetCurrentThreadStackLimits(&threadLow, &threadHigh); +#endif + Require(getcontext_nid_postfix(&g_job.context) == 0, "capture the job template"); + g_job.context.stackPointer = g_jobStack.bytes; + g_job.context.stackSize = StackBytes; + g_job.context.link = nullptr; + makecontext_nid_postfix(&g_job.context, &JobEntry, 0); + for (int round = 1; round <= 4; ++round) { + Switch(&g_dispatcher, &g_job); + Require(g_jobRuns == round, "the job runs once per switch"); +#ifdef _WIN32 + ULONG_PTR low = 0; + ULONG_PTR high = 0; + GetCurrentThreadStackLimits(&low, &high); + Require(low == threadLow && high == threadHigh, signalJumps ? "the stack bounds follow siglongjmp back to the thread stack" : "the stack bounds follow longjmp back to the thread stack"); +#endif + } +} + +void CheckSignalMask() { + std::uint32_t original[4] = {}; + Require(sigprocmask_nid_postfix(GuestSigBlock, nullptr, original) == 0, "read the signal mask"); + const std::uint32_t first[4] = {0x10, 0, 0, 0}; + const std::uint32_t second[4] = {0x20, 0, 0, 0}; + UserUcontext context{}; + volatile int phase = 0; + Require(sigprocmask_nid_postfix(GuestSigSetmask, first, nullptr) == 0, "set the first mask"); + Require(getcontext_nid_postfix(&context) == 0, "capture the masked context"); + if (phase == 0) { + phase = 1; + Require(sigprocmask_nid_postfix(GuestSigSetmask, second, nullptr) == 0, "set the second mask"); + setcontext_nid_postfix(&context); + Require(false, "setcontext returned from a valid context"); + } + std::uint32_t current[4] = {}; + Require(sigprocmask_nid_postfix(GuestSigBlock, nullptr, current) == 0 && current[0] == 0x10, "setcontext restores the context's signal mask"); + Require(sigprocmask_nid_postfix(GuestSigSetmask, original, nullptr) == 0, "restore the signal mask"); +} + +int ExitFromContext() { + UserUcontext context{}; + Require(getcontext_nid_postfix(&context) == 0, "capture the exiting template"); + context.stackPointer = g_exitStack.bytes; + context.stackSize = StackBytes; + context.link = nullptr; + makecontext_nid_postfix(&context, &ReturnWithoutLink, 0); + setcontext_nid_postfix(&context); + return 1; +} + +} + +int main(int argc, char** argv) { + if (argc > 1 && std::strcmp(argv[1], "exit-on-return") == 0) return ExitFromContext(); + CheckErrors(); + CheckCapture(); + CheckRestore(); + CheckSignalMask(); + std::thread([] { + CheckMakecontext(); + CheckJumpBuffers(false); + CheckJumpBuffers(true); + GrowStack(32); + }).join(); + return 0; +} diff --git a/core/libs/tests/GuestUserService.cpp b/core/libs/tests/GuestUserService.cpp index 823c0536ac..4e6e1809a4 100644 --- a/core/libs/tests/GuestUserService.cpp +++ b/core/libs/tests/GuestUserService.cpp @@ -2,12 +2,22 @@ #include "prx/libSceUserService/UserService.hpp" #include #include +#include extern "C" { 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); } namespace { @@ -41,4 +51,54 @@ int main() { Require(sceUserServiceGetNpAccountId(123, &account_id) == USER_SERVICE_ERROR_INVALID_ARGUMENT); Require(sceUserServiceGetNpAccountId(USER_SERVICE_INITIAL_USER_ID, &account_id) == USER_SERVICE_OK); Require(account_id == 0); + + std::uint32_t age_level = 0x5a5a5a5au; + Require(sceUserServiceGetAgeLevel(USER_SERVICE_INITIAL_USER_ID, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + const int absent_users[] = {0, USER_SERVICE_USER_ID_INVALID, 0xff, 0x12345678}; + for (int user_id : absent_users) { + Require(sceUserServiceGetAgeLevel(user_id, &age_level) == USER_SERVICE_ERROR_NOT_LOGGED_IN); + Require(age_level == 0x5a5a5a5au); + Require(sceUserServiceGetAgeLevel(user_id, nullptr) == USER_SERVICE_ERROR_NOT_LOGGED_IN); + } + Require(sceUserServiceGetAgeLevel(USER_SERVICE_INITIAL_USER_ID, &age_level) == USER_SERVICE_OK); + Require(age_level == 0xffffffffu); + + struct Setting { + int (APS5_VABI *get)(int, std::int32_t*); + std::int32_t value; + }; + const Setting settings[] = { + {sceUserServiceGetAccessibilityChatTranscription, 0}, + {sceUserServiceGetAccessibilityPressAndHoldDelay, 0}, + {sceUserServiceGetAccessibilityTriggerEffect, 1}, + {sceUserServiceGetAccessibilityVibration, 1}, + {sceUserServiceGetAccessibilityZoomEnabled, 0}, + {sceUserServiceGetAccessibilityZoomFollowFocus, 1}, + }; + for (const Setting& setting : settings) { + std::int32_t value = 0x5a5a5a5a; + Require(setting.get(USER_SERVICE_INITIAL_USER_ID, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + for (int user_id : absent_users) { + Require(setting.get(user_id, &value) == USER_SERVICE_ERROR_NOT_LOGGED_IN); + Require(value == 0x5a5a5a5a); + Require(setting.get(user_id, nullptr) == USER_SERVICE_ERROR_NOT_LOGGED_IN); + } + 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..c169e42d26 --- /dev/null +++ b/core/libs/tests/GuestUserServiceLifecycle.cpp @@ -0,0 +1,87 @@ +#include "SceTypes.hpp" +#include "prx/libSceUserService/UserService.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI sceUserServiceInitialize(const void* params); +int APS5_VABI sceUserServiceInitialize2(int priority); +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) { + return sceUserServiceInitialize(&priority); +} + +void TestThreadPriority() { + for (int priority : {0, 0xFF, 0x300, -1}) { + Require(InitializeWithPriority(priority) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceInitialize2(priority) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + } + RequireNotInitialized(); + + Require(sceUserServiceInitialize2(0x100) == USER_SERVICE_OK); + RequireLoginOnce(); + for (int priority : {0x100, 700, 0x2FF}) { + Require(InitializeWithPriority(priority) == USER_SERVICE_ERROR_ALREADY_INITIALIZED); + Require(sceUserServiceInitialize2(priority) == USER_SERVICE_ERROR_ALREADY_INITIALIZED); + } + for (int priority : {0, 0xFF, 0x300, -1}) { + Require(InitializeWithPriority(priority) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceInitialize2(priority) == 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/GuestUserServiceUserName.cpp b/core/libs/tests/GuestUserServiceUserName.cpp new file mode 100644 index 0000000000..f755162970 --- /dev/null +++ b/core/libs/tests/GuestUserServiceUserName.cpp @@ -0,0 +1,49 @@ +#include "SceTypes.hpp" +#include "prx/libSceUserService/UserService.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceUserServiceGetUserName(int user_id, char* name, std::size_t size); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +bool Filled(const char* buffer, std::size_t from, std::size_t to, char value) { + for (std::size_t i = from; i < to; ++i) { + if (buffer[i] != value) return false; + } + return true; +} + +} + +int main() { + char name[32]; + const std::size_t length = std::strlen(USER_SERVICE_INITIAL_USER_NAME); + + for (std::size_t size = 0; size <= USER_SERVICE_MAX_USER_NAME_LENGTH; ++size) { + std::memset(name, 'A', sizeof(name)); + Require(sceUserServiceGetUserName(USER_SERVICE_INITIAL_USER_ID, name, size) == USER_SERVICE_ERROR_BUFFER_TOO_SHORT); + Require(Filled(name, 0, size, 0) && Filled(name, size, sizeof(name), 'A')); + } + for (std::size_t size : {USER_SERVICE_MAX_USER_NAME_LENGTH + 1, sizeof(name)}) { + std::memset(name, 'A', sizeof(name)); + Require(sceUserServiceGetUserName(USER_SERVICE_INITIAL_USER_ID, name, size) == USER_SERVICE_OK); + Require(std::strcmp(name, USER_SERVICE_INITIAL_USER_NAME) == 0); + Require(Filled(name, length, size, 0) && Filled(name, size, sizeof(name), 'A')); + } + + for (int user : {0, USER_SERVICE_USER_ID_INVALID, 1, 0xff, 0x12345678}) { + std::memset(name, 'A', sizeof(name)); + Require(sceUserServiceGetUserName(user, name, sizeof(name)) == USER_SERVICE_ERROR_UNKNOWN_USER); + Require(sceUserServiceGetUserName(user, name, 1) == USER_SERVICE_ERROR_UNKNOWN_USER); + Require(Filled(name, 0, sizeof(name), 'A')); + Require(sceUserServiceGetUserName(user, nullptr, sizeof(name)) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + } + Require(sceUserServiceGetUserName(USER_SERVICE_INITIAL_USER_ID, nullptr, sizeof(name)) == USER_SERVICE_ERROR_INVALID_ARGUMENT); +} diff --git a/core/libs/tests/GuestUtime.cpp b/core/libs/tests/GuestUtime.cpp new file mode 100644 index 0000000000..884cff4e47 --- /dev/null +++ b/core/libs/tests/GuestUtime.cpp @@ -0,0 +1,95 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif + +struct LibcUtimbuf { + std::int64_t actime; + std::int64_t modtime; +}; +static_assert(sizeof(LibcUtimbuf) == 16); + +extern "C" { +int APS5_VABI utime_nid_postfix(const char*, const LibcUtimbuf*); +int* APS5_VABI __error_nid_postfix(); +} + +static void Require(bool condition, int line) { + if (!condition) { + std::fprintf(stderr, "Utime check failed at line %d\n", line); + std::abort(); + } +} +#define Check(value) Require((value), __LINE__) + +static LibcUtimbuf ReadTimes(const std::filesystem::path& path, bool wholeSeconds = false) { +#ifdef _WIN32 + const HANDLE file = CreateFileW(path.c_str(), FILE_READ_ATTRIBUTES, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, 0, nullptr); + Check(file != INVALID_HANDLE_VALUE); + FILETIME access{}, modified{}; + Check(GetFileTime(file, nullptr, &access, &modified)); + Check(CloseHandle(file)); + const auto decode = [wholeSeconds](FILETIME time) { + const auto ticks = (static_cast(time.dwHighDateTime) << 32) | time.dwLowDateTime; + if (wholeSeconds) Check(ticks % 10000000 == 0); + return static_cast(ticks / 10000000) - 11644473600LL; + }; + return {decode(access), decode(modified)}; +#else + struct stat info{}; + Check(::stat(path.c_str(), &info) == 0); + if (wholeSeconds) { +#ifdef __APPLE__ + Check(info.st_atimespec.tv_nsec == 0 && info.st_mtimespec.tv_nsec == 0); +#else + Check(info.st_atim.tv_nsec == 0 && info.st_mtim.tv_nsec == 0); +#endif + } + return {info.st_atime, info.st_mtime}; +#endif +} + +int main() { + const auto root = std::filesystem::path("anyps5-utime-test-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Check(std::filesystem::create_directory(root)); + const auto file = root / "data.bin"; + { std::ofstream stream(file); stream << "timestamps"; Check(stream.good()); } + const auto path = file.string(); + const LibcUtimbuf times{1000000000LL, 1000000123LL}; + *__error_nid_postfix() = 71; + Check(utime_nid_postfix(path.c_str(), ×) == 0); + Check(*__error_nid_postfix() == 71); + const auto stored = ReadTimes(file, true); + Check(stored.actime == times.actime && stored.modtime == times.modtime); + const auto missing = (root / "missing.bin").string(); + *__error_nid_postfix() = 71; + Check(utime_nid_postfix(missing.c_str(), ×) == -1 && *__error_nid_postfix() == 2); + *__error_nid_postfix() = 71; + Check(utime_nid_postfix("", ×) == -1 && *__error_nid_postfix() == 2); + *__error_nid_postfix() = 71; + Check(utime_nid_postfix(nullptr, ×) == -1 && *__error_nid_postfix() == 14); + *__error_nid_postfix() = 71; + Check(utime_nid_postfix(nullptr, nullptr) == -1 && *__error_nid_postfix() == 14); + const auto unchanged = ReadTimes(file, true); + Check(unchanged.actime == times.actime && unchanged.modtime == times.modtime); + const auto before = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); + *__error_nid_postfix() = 71; + Check(utime_nid_postfix(path.c_str(), nullptr) == 0); + Check(*__error_nid_postfix() == 71); + const auto after = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); + const auto current = ReadTimes(file); + Check(current.actime >= before - 1 && current.actime <= after + 1); + Check(current.modtime >= before - 1 && current.modtime <= after + 1); + Check(std::filesystem::remove_all(root) == 2); +} diff --git a/core/libs/tests/GuestUtimeForwarding.cpp b/core/libs/tests/GuestUtimeForwarding.cpp new file mode 100644 index 0000000000..870d83d404 --- /dev/null +++ b/core/libs/tests/GuestUtimeForwarding.cpp @@ -0,0 +1,65 @@ +#include "SceTypes.hpp" +#include +#include +#include + +struct LibcUtimbuf { + std::int64_t actime; + std::int64_t modtime; +}; + +extern "C" int APS5_VABI utime_nid_postfix(const char*, const LibcUtimbuf*); + +namespace { +int guestError; +int backendResult; +int backendError; +int calls; +const char* expectedPath; +const LibcUtimbuf* expectedTimes; + +void Require(bool condition, int line) { + if (!condition) { + std::fprintf(stderr, "Utime forwarding check failed at line %d\n", line); + std::abort(); + } +} +} + +#define Check(value) Require((value), __LINE__) + +extern "C" int* APS5_VABI __error_nid_postfix() { + return &guestError; +} + +extern "C" int APS5_VABI utimes_nid_postfix(const char* path, const KernelTimeval* times) { + ++calls; + Check(path == expectedPath); + Check((times == nullptr) == (expectedTimes == nullptr)); + if (expectedTimes) { + Check(times[0].tv_sec == expectedTimes->actime && times[0].tv_usec == 0); + Check(times[1].tv_sec == expectedTimes->modtime && times[1].tv_usec == 0); + } + guestError = backendError; + return backendResult; +} + +int main() { + const char path[] = "/app0/timestamps"; + const LibcUtimbuf times{0x100000001LL, -123}; + for (const LibcUtimbuf* value : {×, static_cast(nullptr)}) { + for (const char* name : {path, static_cast(nullptr)}) { + expectedPath = name; + expectedTimes = value; + for (int result : {0, -1}) { + backendResult = result; + backendError = result == 0 ? 22 : 14; + guestError = 71; + const int previousCalls = calls; + Check(utime_nid_postfix(name, value) == result); + Check(calls == previousCalls + 1); + Check(guestError == (result == 0 ? 71 : 14)); + } + } + } +} diff --git a/core/libs/tests/GuestVdecsw.cpp b/core/libs/tests/GuestVdecsw.cpp index 99a11fe967..72f76ec270 100644 --- a/core/libs/tests/GuestVdecsw.cpp +++ b/core/libs/tests/GuestVdecsw.cpp @@ -120,9 +120,12 @@ int APS5_VABI sceVdecswCreateDecoder(const DecoderConfigInfo*, const DecoderMemo int APS5_VABI sceVdecswDeleteDecoder(std::uint64_t); int APS5_VABI sceVdecswSetDecodeInput(std::uint64_t, const InputData*); int APS5_VABI sceVdecswTrySyncDecodeInput(std::uint64_t, InputResult*); +int APS5_VABI sceVdecswSyncDecodeInput(std::uint64_t, InputResult*); int APS5_VABI sceVdecswSetDecodeOutput(std::uint64_t, const FrameBuffer*); int APS5_VABI sceVdecswTrySyncDecodeOutput(std::uint64_t, OutputInfo*); +int APS5_VABI sceVdecswSyncDecodeOutput(std::uint64_t, OutputInfo*); int APS5_VABI sceVdecswGetAvcPictureInfo(const OutputInfo*, AvcPictureInfo*, AvcPictureInfo*); +int APS5_VABI sceVdecswGetHevcPictureInfo(const OutputInfo*, void*); int APS5_VABI sceVdecswFinalizeDecodeSequence(std::uint64_t); int APS5_VABI sceVdecswResetDecoder(std::uint64_t); } @@ -188,13 +191,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; } }; @@ -233,12 +236,37 @@ void testWithoutOutput() { check(sceVdecswTrySyncDecodeInput(session.decoder, &result) == InputQueueEmpty, "a reset decoder kept its inputs"); } +void testSyncWrappers() { + Session session; + InputResult idle{sizeof(InputResult)}; + check(sceVdecswSyncDecodeInput(session.decoder, &idle) == InputQueueEmpty, "sync input on an idle decoder did not report an empty queue"); + const auto units = accessUnits(false); + const InputData input{sizeof(InputData), units[0].data(), units[0].size(), 1000, 0, 0xa0}; + check(sceVdecswSetDecodeInput(session.decoder, &input) == 0, "decode input was rejected"); + InputResult result{sizeof(InputResult)}; + check(sceVdecswSyncDecodeInput(session.decoder, &result) == 0 && result.decodedAu == units[0].data(), "sync input did not consume the access unit"); + std::vector buffer(session.memory.maxFrameBufferSize); + const FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size()}; + check(sceVdecswSetDecodeOutput(session.decoder, &frame) == 0, "output buffer was rejected"); + check(sceVdecswFinalizeDecodeSequence(session.decoder) == 0, "finalize failed"); + OutputInfo output{sizeof(OutputInfo)}; + check(sceVdecswSyncDecodeOutput(session.decoder, &output) == 0 && output.isValid && output.isLastFrame, "sync output did not return the finalized picture"); + bool hevcThrows = false; + try { + sceVdecswGetHevcPictureInfo(&output, &result); + } catch (const std::exception&) { + hevcThrows = true; + } + check(hevcThrows, "GetHevcPictureInfo must still throw"); +} + } int main() { try { testDecode(); testWithoutOutput(); + testSyncWrappers(); std::puts("Vdecsw tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/tests/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/GuestVideoOutHighFrameRate.cpp b/core/libs/tests/GuestVideoOutHighFrameRate.cpp new file mode 100644 index 0000000000..3e4f4c4437 --- /dev/null +++ b/core/libs/tests/GuestVideoOutHighFrameRate.cpp @@ -0,0 +1,74 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include "tests/VideoOutTestEnvironment.hpp" +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceVideoOutOpen(int, int, int, const void*); +int APS5_VABI sceVideoOutClose(int); +int APS5_VABI sceVideoOutIsOutputSupported(int, std::uint64_t, const VideoOutOutputOptions*, void*, std::uint64_t); +int APS5_VABI sceVideoOutConfigureOutput(int, std::uint64_t, const VideoOutOutputOptions*, void*, std::uint64_t); +int APS5_VABI sceVideoOutWaitVblank(int); +int APS5_VABI sceVideoOutGetOutputStatus(int, VideoOutOutputStatus*); +int APS5_VABI sceVideoOutGetResolutionStatus(int, VideoOutResolutionStatus*); +} + +static constexpr int Unsupported = static_cast(0x80290016u); + +static void Require(bool value, const char* message) { + if (!value) throw std::runtime_error(message); +} + +static double Measure(int handle) { + Require(sceVideoOutWaitVblank(handle) == 0, "initial vblank failed"); + const auto start = std::chrono::steady_clock::now(); + for (int frame = 0; frame < 60; ++frame) Require(sceVideoOutWaitVblank(handle) == 0, "vblank failed"); + return std::chrono::duration(std::chrono::steady_clock::now() - start).count(); +} + +int main(int argc, char** argv) { + const bool requireDisplay = argc > 1 && std::strcmp(argv[1], "--require-high-frame-rate") == 0; + VideoOutTestEnvironment environment; + { + std::ofstream param("app0/sce_sys/param.json", std::ios::binary); + param << R"({"titleId":"PPSA99007","localizedParameters":{"en-US":{"titleName":"High frame rate test"}},"attribute3":64})"; + Require(static_cast(param), "cannot write high-frame-rate fixture"); + } + int handle; + try { + handle = sceVideoOutOpen(255, 0, 0, nullptr); + } catch (const std::runtime_error& error) { + if (requireDisplay) throw; + std::printf("skipped, no display or Vulkan device: %s\n", error.what()); + return 77; + } + const int supported = sceVideoOutIsOutputSupported(handle, 15, nullptr, nullptr, 0); + if (supported == Unsupported && !requireDisplay) { + Require(sceVideoOutConfigureOutput(handle, 15, nullptr, nullptr, 0) == Unsupported, "unsupported display accepted high-frame-rate output"); + Require(sceVideoOutClose(handle) == 0, "close failed"); + LibcRunShutdown_nid_postfix(); + std::puts("skipped, current display does not provide 120 Hz"); + return 77; + } + Require(supported == 1, "120 Hz output not reported as supported"); + Require(sceVideoOutConfigureOutput(handle, 1, nullptr, nullptr, 0) == 0, "default mode failed"); + const double normal = Measure(handle); + Require(sceVideoOutConfigureOutput(handle, 15, nullptr, nullptr, 0) == 0, "permitted high-frame-rate mode failed"); + VideoOutOutputStatus output{}; + VideoOutResolutionStatus resolution{}; + Require(sceVideoOutGetOutputStatus(handle, &output) == 0 && output.refreshRate == 13, "wrong output refresh rate"); + Require(sceVideoOutGetResolutionStatus(handle, &resolution) == 0 && resolution.refreshRate == 13, "wrong resolution refresh rate"); + const double fast = Measure(handle); + Require(sceVideoOutConfigureOutput(handle, 1, nullptr, nullptr, 0) == 0, "default mode restore failed"); + Require(sceVideoOutGetOutputStatus(handle, &output) == 0 && output.refreshRate == 3, "default output refresh rate not restored"); + const double restored = Measure(handle); + std::printf("60 vblanks: default=%.3f s high=%.3f s restored=%.3f s\n", normal, fast, restored); + Require(normal > 0.8 && normal < 1.3 && fast > 0.4 && fast < 0.7 && restored > 0.8 && restored < 1.3, "incorrect vblank periods"); + Require(normal / fast > 1.7 && restored / fast > 1.7, "high-frame-rate mode did not double vblank frequency"); + Require(sceVideoOutClose(handle) == 0, "close failed"); + LibcRunShutdown_nid_postfix(); +} diff --git a/core/libs/tests/GuestVideoOutOutputMode.cpp b/core/libs/tests/GuestVideoOutOutputMode.cpp index 6762cd4462..82ce4bc2fa 100644 --- a/core/libs/tests/GuestVideoOutOutputMode.cpp +++ b/core/libs/tests/GuestVideoOutOutputMode.cpp @@ -1,7 +1,9 @@ #include "SceTypes.hpp" #include "prx/libc/include/Shutdown.hpp" +#include "tests/VideoOutTestEnvironment.hpp" #include #include +#include #include #include #include @@ -16,15 +18,18 @@ 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); +static constexpr int RESOURCE_BUSY = static_cast(0x80290009); static constexpr int UNSUPPORTED_OUTPUT_MODE = static_cast(0x80290016); static constexpr int UNKNOWN_OUTPUT_MODE = static_cast(0x8029001E); static void Require(bool value) { if (!value) std::abort(); } int main() { + VideoOutTestEnvironment environment; std::array raw{}; raw.fill(0xAA); VideoOutOutputOptions options{}; @@ -34,22 +39,41 @@ int main() { for (std::size_t index = 0; index < raw.size(); ++index) Require(raw[index] == (index == 2 ? 0xFF : 0)); Require(sceVideoOutInitializeOutputOptions(nullptr) == INVALID_ADDRESS); - for (int handle : {-1, 0, 1, 0x7FFFFFFF}) { - Require(sceVideoOutIsOutputSupported(handle, 1, &options, nullptr, 0) == INVALID_HANDLE); - Require(sceVideoOutIsOutputSupported(handle, 2, nullptr, &options, 1) == INVALID_HANDLE); - } + 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); - const int handle = sceVideoOutOpen(SYSTEM_USER, MAIN_BUS, 0, nullptr); + int handle = 0; + try { + for (int unopened : {-1, 0, 1, 0x7FFFFFFF}) { + Require(sceVideoOutIsOutputSupported(unopened, 1, &options, nullptr, 0) == INVALID_HANDLE); + Require(sceVideoOutIsOutputSupported(unopened, 2, nullptr, &options, 1) == INVALID_HANDLE); + } + 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(sceVideoOutOpen(SYSTEM_USER, MAIN_BUS, 0, nullptr) == RESOURCE_BUSY); Require(sceVideoOutIsOutputSupported(handle, 1, &options, nullptr, 0) == 1); Require(sceVideoOutIsOutputSupported(handle, 1, nullptr, nullptr, 0) == 1); - for (std::uint64_t mode : {0x4ull, 0x7ull, 0x8ull, 0xCull, 0xDull, 0xEull, 0xFull, 0x10ull, 0x11ull, 0x13ull}) { + const int highFrameRate = sceVideoOutIsOutputSupported(handle, 15, &options, nullptr, 0); + Require(highFrameRate == 1 || highFrameRate == UNSUPPORTED_OUTPUT_MODE); + Require(sceVideoOutConfigureOutput(handle, 15, &options, nullptr, 0) == UNSUPPORTED_OUTPUT_MODE); + for (std::uint64_t mode : {0x4ull, 0x7ull, 0x8ull, 0xCull, 0xDull, 0xEull, 0x10ull, 0x11ull, 0x13ull}) { Require(sceVideoOutIsOutputSupported(handle, mode, &options, nullptr, 0) == UNSUPPORTED_OUTPUT_MODE); Require(sceVideoOutConfigureOutput(handle, mode, &options, nullptr, 0) == UNSUPPORTED_OUTPUT_MODE); } 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/GuestVideoOutResolutionStatus.cpp b/core/libs/tests/GuestVideoOutResolutionStatus.cpp index 0e81b766ca..e401ad3023 100644 --- a/core/libs/tests/GuestVideoOutResolutionStatus.cpp +++ b/core/libs/tests/GuestVideoOutResolutionStatus.cpp @@ -1,5 +1,8 @@ #include "SceTypes.hpp" #include "prx/libc/include/Shutdown.hpp" +#include "tests/VideoOutTestEnvironment.hpp" +#include +#include #include #include #include @@ -9,6 +12,7 @@ extern "C" { int APS5_VABI sceVideoOutOpen(int userId, int busType, int index, const void* param); int APS5_VABI sceVideoOutClose(int handle); int APS5_VABI sceVideoOutGetOutputStatus(int handle, VideoOutOutputStatus* status); +int APS5_VABI sceVideoOutGetOutputStatusWithVrr(int handle, void* status); int APS5_VABI sceVideoOutGetResolutionStatus(int handle, VideoOutResolutionStatus* status); } @@ -28,6 +32,7 @@ static bool Rejects(int handle, VideoOutResolutionStatus* status) { } int main() { + VideoOutTestEnvironment environment; int handle = 0; try { handle = sceVideoOutOpen(SYSTEM_USER, MAIN_BUS, 0, nullptr); @@ -50,6 +55,18 @@ int main() { Require(sceVideoOutGetOutputStatus(handle, &output) == 0); Require(status.refreshRate == output.refreshRate); + std::array vrr; + vrr.fill(std::byte{0xff}); + Require(sceVideoOutGetOutputStatusWithVrr(handle, vrr.data()) == 0); + Require(std::memcmp(vrr.data(), &output, sizeof(output)) == 0); + for (std::size_t offset = sizeof(output); offset < 80; ++offset) Require(vrr[offset] == std::byte{0}); + for (std::size_t offset = 80; offset < vrr.size(); ++offset) Require(vrr[offset] == std::byte{0xff}); + vrr.fill(std::byte{0xff}); + Require(sceVideoOutGetOutputStatusWithVrr(handle, nullptr) == static_cast(0x80290002u)); + Require(sceVideoOutGetOutputStatusWithVrr(NEVER_OPENED_HANDLE, vrr.data()) == static_cast(0x8029000Bu)); + Require(sceVideoOutGetOutputStatusWithVrr(-1, vrr.data()) == static_cast(0x8029000Bu)); + for (const auto byte : vrr) Require(byte == std::byte{0xff}); + Require(Rejects(handle, nullptr)); Require(Rejects(0, &status)); Require(Rejects(-1, &status)); @@ -57,5 +74,6 @@ int main() { Require(sceVideoOutClose(handle) == 0); Require(Rejects(handle, &status)); + Require(sceVideoOutGetOutputStatusWithVrr(handle, vrr.data()) == static_cast(0x8029000Bu)); LibcRunShutdown_nid_postfix(); } diff --git a/core/libs/tests/GuestVideoOutWqhdDetection.cpp b/core/libs/tests/GuestVideoOutWqhdDetection.cpp index 0cb8a54ecf..c0e68b0c49 100644 --- a/core/libs/tests/GuestVideoOutWqhdDetection.cpp +++ b/core/libs/tests/GuestVideoOutWqhdDetection.cpp @@ -1,10 +1,9 @@ #include "SceTypes.hpp" #include "prx/libc/include/Shutdown.hpp" +#include "tests/VideoOutTestEnvironment.hpp" #include #include #include -#include -#include #include extern "C" { @@ -34,12 +33,7 @@ static bool SameStatus(const VideoOutOutputStatus& a, const VideoOutOutputStatus } int main() { - std::filesystem::create_directories("app0/sce_sys"); - { - std::ofstream param("app0/sce_sys/param.json", std::ios::binary); - param << R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":"Example"}},"downloadDataSize":0})"; - Require(static_cast(param)); - } + VideoOutTestEnvironment environment; int handle = 0; try { handle = sceVideoOutOpen(SYSTEM_USER, MAIN_BUS, 0, nullptr); diff --git a/core/libs/tests/GuestVideodec.cpp b/core/libs/tests/GuestVideodec.cpp new file mode 100644 index 0000000000..f4ad8e987e --- /dev/null +++ b/core/libs/tests/GuestVideodec.cpp @@ -0,0 +1,247 @@ +#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; +}; + +static_assert(sizeof(PictureInfo) == 112 && sizeof(AvcInfo) == 36); +static_assert(sizeof(ConfigInfo) == 40 && sizeof(ResourceInfo) == 56 && sizeof(Ctrl) == 24); + +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*); +} + +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 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"); +} + +} + +int main() { + try { + testDecode(); + testDecodeCropped(); + testErrors(); + testReset(); + 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 29649d183c..101240eb6a 100644 --- a/core/libs/tests/GuestVideodec2.cpp +++ b/core/libs/tests/GuestVideodec2.cpp @@ -1,9 +1,13 @@ #include #include +#include #include #include +#include #include #include +#include +#include #include #include "prx/libc/include/General.hpp" #include "H264Fixture.hpp" @@ -76,6 +80,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*); @@ -84,6 +111,7 @@ 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*); } @@ -355,7 +383,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; }; @@ -377,8 +419,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"); @@ -389,16 +513,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"); @@ -410,7 +547,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]; @@ -418,13 +555,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}; @@ -444,6 +602,304 @@ 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)}; + 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"); + return handle; +} + +AvcPictureInfo query(std::vector& buffer) { + OutputInfo output{sizeof(OutputInfo)}; + output.frameBuffer = buffer.data(); + AvcPictureInfo info{sizeof(AvcPictureInfo)}; + check(sceVideodec2GetAvcPictureInfo_nid_postfix(&output, &info, nullptr) == 0, "picture info query failed"); + return info; +} + +bool owned(std::vector& buffer, std::uint64_t pts) { + const auto info = query(buffer); + return info.isValid && info.ptsData == pts; +} + +std::uint64_t publishAvc(std::uint64_t handle, std::uint64_t base, std::vector& buffer) { + const auto units = accessUnits(false); + std::size_t pictures = 0; + const auto take = [&](const OutputInfo& output) { + if (!output.isValid) return false; + check(owned(buffer, base + DisplayOrderUnits[pictures]), "picture info does not follow the latest publication"); + ++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 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"); + return base + DisplayOrderUnits[pictures - 1]; +} + +void publishClip(std::uint64_t handle, const std::vector>& units, std::vector& buffer) { + for (std::size_t unit = 0; unit < units.size(); ++unit) { + const InputData input{sizeof(InputData), units[unit].data(), units[unit].size(), 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"); + } + 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 (!output.isValid) break; + } +} + +std::uint64_t frameBufferBytes() { + const DecoderConfigInfo config{sizeof(DecoderConfigInfo), 1, 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"); + return memory.maxFrameBufferSize; +} + +void testDecodeAfterFlush() { + std::vector buffer(frameBufferBytes()); + const auto handle = createDecoder(1, 100); + { + FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; + OutputInfo output{sizeof(OutputInfo)}; + check(sceVideodec2Flush_nid_postfix(handle, &frame, &output) == 0 && !output.isValid && !frame.isAccepted, "flushing a decoder that decoded nothing gave a picture"); + } + const auto units = accessUnits(false); + for (const std::uint64_t base : {1000u, 5000u}) { + std::size_t pictures = 0; + const auto take = [&](const OutputInfo& output) { + if (!output.isValid) return false; + check(pictures < PictureHashes.size() && owned(buffer, base + DisplayOrderUnits[pictures]), "a picture decoded after a flush is out of display order"); + check(hashNv12(buffer.data(), output.framePitch, output.frameHeight) == PictureHashes[pictures], "picture " + std::to_string(pictures) + " decoded after a flush differs from the reference"); + ++pictures; + return true; + }; + for (std::size_t unit = 0; unit < units.size(); ++unit) { + const InputData input{sizeof(InputData), units[unit].data(), units[unit].size(), base + unit, unit, base + unit}; + FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; + OutputInfo output{sizeof(OutputInfo)}; + check(sceVideodec2Decode_nid_postfix(handle, &input, &frame, &output) == 0, "decode after a flush failed"); + take(output); + } + for (;;) { + FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; + OutputInfo output{sizeof(OutputInfo)}; + check(sceVideodec2Flush_nid_postfix(handle, &frame, &output) == 0, "flush failed"); + if (!take(output)) break; + } + check(pictures == PictureHashes.size(), "missing pictures of a stream decoded after a flush"); + } + check(sceVideodec2DeleteDecoder_nid_postfix(handle) == 0, "delete failed"); +} + +void testPictureOwnership() { + std::vector shared(frameBufferBytes()), other(frameBufferBytes()); + { + const auto older = createDecoder(1, 100); + const auto newer = createDecoder(1, 100); + publishAvc(older, 1000, shared); + const auto latest = publishAvc(newer, 5000, shared); + check(owned(shared, latest), "an older decoder answered for a buffer a newer decoder wrote last"); + check(sceVideodec2Reset_nid_postfix(newer) == 0, "reset failed"); + check(!query(shared).isValid, "a stale picture survived the reset of the decoder that last wrote the buffer"); + check(sceVideodec2DeleteDecoder_nid_postfix(older) == 0 && sceVideodec2DeleteDecoder_nid_postfix(newer) == 0, "delete failed"); + } + { + const auto older = createDecoder(1, 100); + const auto newer = createDecoder(1, 100); + publishAvc(older, 1000, shared); + publishAvc(newer, 5000, shared); + check(sceVideodec2DeleteDecoder_nid_postfix(newer) == 0, "delete failed"); + check(!query(shared).isValid, "a stale picture survived the deletion of the decoder that last wrote the buffer"); + check(sceVideodec2DeleteDecoder_nid_postfix(older) == 0, "delete failed"); + } + { + const auto older = createDecoder(1, 100); + const auto newer = createDecoder(1, 100); + publishAvc(older, 1000, shared); + const auto latest = publishAvc(newer, 5000, shared); + check(sceVideodec2Reset_nid_postfix(older) == 0, "reset failed"); + check(owned(shared, latest), "resetting a decoder that no longer owns the buffer dropped its picture"); + check(sceVideodec2DeleteDecoder_nid_postfix(older) == 0, "delete failed"); + check(owned(shared, latest), "deleting a decoder that no longer owns the buffer dropped its picture"); + check(sceVideodec2DeleteDecoder_nid_postfix(newer) == 0, "delete failed"); + check(!query(shared).isValid, "a deleted decoder still owns its picture"); + } + { + const auto older = createDecoder(1, 100); + const auto newer = createDecoder(1, 100); + publishAvc(newer, 5000, shared); + const auto latest = publishAvc(older, 1000, shared); + const auto third = createDecoder(1, 100); + const auto separate = publishAvc(third, 9000, other); + check(owned(shared, latest) && owned(other, separate), "distinct buffers are not independent"); + check(sceVideodec2Reset_nid_postfix(older) == 0, "reset failed"); + check(!query(shared).isValid && owned(other, separate), "resetting one buffer's owner changed another buffer"); + check(sceVideodec2DeleteDecoder_nid_postfix(older) == 0 && sceVideodec2DeleteDecoder_nid_postfix(newer) == 0 && sceVideodec2DeleteDecoder_nid_postfix(third) == 0, "delete failed"); + } + for (const auto codec : {CodecHevc, CodecVp9}) { + const auto units = codec == CodecHevc ? hevcUnits(HevcStream.data(), HevcStream.size()) : ivfFrames(); + const auto avc = createDecoder(1, 100); + const auto foreign = createDecoder(codec, codec == CodecHevc ? 1 : 0); + publishAvc(avc, 1000, shared); + publishClip(foreign, units, shared); + checkThrows([&] { query(shared); }); + check(sceVideodec2DeleteDecoder_nid_postfix(avc) == 0, "delete failed"); + const auto newer = createDecoder(1, 100); + const auto latest = publishAvc(newer, 5000, shared); + check(owned(shared, latest), "a decoder of another codec answered for an AVC picture"); + check(sceVideodec2DeleteDecoder_nid_postfix(foreign) == 0 && sceVideodec2DeleteDecoder_nid_postfix(newer) == 0, "delete failed"); + } +} + +void testConcurrentOwnership() { + std::vector shared(frameBufferBytes()); + const auto keeper = createDecoder(1, 100); + const auto kept = publishAvc(keeper, 1000, shared); + std::atomic done = false; + std::string failure; + std::thread reader([&] { + try { + while (!done.load()) { + const auto info = query(shared); + check(!info.isValid || info.ptsData == kept || (info.ptsData >= 5000 && info.ptsData < 5000 + PictureHashes.size()), "a query returned metadata no decoder published"); + } + } catch (const std::exception& error) { + failure = error.what(); + } + }); + try { + for (int round = 0; round < 8; ++round) { + const auto writer = createDecoder(1, 100); + publishAvc(writer, 5000, shared); + if (round % 2 == 0) check(sceVideodec2Reset_nid_postfix(writer) == 0, "reset failed"); + check(sceVideodec2DeleteDecoder_nid_postfix(writer) == 0, "delete failed"); + check(!query(shared).isValid, "a removed writer left its picture behind"); + } + } catch (...) { + done = true; + reader.join(); + throw; + } + done = true; + reader.join(); + check(failure.empty(), failure); + check(sceVideodec2DeleteDecoder_nid_postfix(keeper) == 0, "delete failed"); +} + } int main() { @@ -451,6 +907,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; @@ -474,7 +933,11 @@ int main() { } testClip(CodecHevc, 1, hevcLengthPrefixed, HevcHashes); testClip(CodecVp9, 0, ivfFrames(), Vp9Hashes); + testVp9PictureMetadata(); testTenBit(); + testDecodeAfterFlush(); + testPictureOwnership(); + testConcurrentOwnership(); std::puts("Videodec2 tests passed"); return 0; } catch (const std::exception& error) { 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..62d682e6ad --- /dev/null +++ b/core/libs/tests/GuestVoice.cpp @@ -0,0 +1,142 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceVoiceInit(VoiceInitParam*, std::int32_t); +int APS5_VABI sceVoiceStart(const VoiceStartParam*); +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, bool); +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 sceVoiceGetResourceInfo(VoiceResourceInfo*); +int APS5_VABI sceVoiceEnableChat(void); +int APS5_VABI sceVoiceDisableChat(void); +} + +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); +} + +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; + std::uint32_t id = 0; + Require(sceVoiceCreatePort(&id, ¶m) == 0); + return id; +} + +} // namespace + +int main() { + TestNotInitialized(); + RequireThrows([] { sceVoiceResetPort(0); }); + RequireThrows([] { sceVoicePausePort(0); }); + RequireThrows([] { sceVoiceResumePort(0); }); + RequireThrows([] { VoiceResourceInfo info{}; sceVoiceGetResourceInfo(&info); }); + RequireThrows([] { sceVoiceEnableChat(); }); + RequireThrows([] { sceVoiceDisableChat(); }); + VoiceInitParam init{}; + VoiceStartParam start{}; + Require(sceVoiceInit(&init, 0) == 0); + Require(sceVoiceInit(&init, 0) == AlreadyInitialized); + Require(sceVoiceStart(&start) == 0); + RequireThrows([] { sceVoiceGetResourceInfo(nullptr); }); + VoiceResourceInfo resourceInfo{}; + Require(sceVoiceGetResourceInfo(&resourceInfo) == 0); + Require(resourceInfo.max_voice_input_ports == 7); + Require(resourceInfo.max_voice_output_ports == 7); + Require(resourceInfo.max_device_input_ports == 1); + Require(resourceInfo.max_device_output_ports == 1); + Require(resourceInfo.max_ports == 64); + Require(sceVoiceEnableChat() == 0); + Require(sceVoiceDisableChat() == 0); + const auto input = CreatePort(2, 0.5f); + const auto output = CreatePort(3, 0.25f); + Require(sceVoiceConnectIPortToOPort(input, output) == 0); + Require(sceVoiceSetMuteFlag(output, true) == 0); + VoicePortInfo before{}; + Require(sceVoiceGetPortInfo(input, &before) == 0); + + Require(sceVoiceResetPort(input) == 0); + Require(sceVoiceResetPort(output) == 0); + RequireThrows([&] { sceVoiceResetPort(output + 1); }); + + 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(output) == 0); + RequireThrows([&] { sceVoicePausePort(output + 1); }); + RequireThrows([&] { sceVoiceResumePort(output + 1); }); + + 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(sceVoiceDeletePort(input) == 0); + RequireThrows([&] { sceVoiceResetPort(input); }); + RequireThrows([&] { sceVoicePausePort(input); }); + Require(sceVoiceResetPort(output) == 0); + Require(sceVoiceEnd_nid_postfix() == 0); + TestNotInitialized(); + RequireThrows([&] { sceVoiceResetPort(output); }); + RequireThrows([&] { sceVoiceResumePort(output); }); + RequireThrows([] { VoiceResourceInfo info{}; sceVoiceGetResourceInfo(&info); }); + RequireThrows([] { sceVoiceEnableChat(); }); + RequireThrows([] { sceVoiceDisableChat(); }); +} 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..3264ec11c4 --- /dev/null +++ b/core/libs/tests/GuestVoicePortAttr.cpp @@ -0,0 +1,59 @@ +#include "SceTypes.hpp" + +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceVoiceInit(VoiceInitParam*, int32_t); +int APS5_VABI sceVoiceEnd_nid_postfix(void); +int APS5_VABI sceVoiceCreatePort(uint32_t*, const VoicePortParam*); +int APS5_VABI sceVoiceDeletePort(uint32_t); +int APS5_VABI sceVoiceGetPortAttr(uint32_t, int32_t, void*, int32_t); +} + +static void Require(bool value) { if (!value) std::abort(); } + +template +static bool Throws(Call call, std::string_view message) { + try { + call(); + } catch (const std::logic_error& error) { + return std::string_view(error.what()).find(message) != std::string_view::npos; + } + return false; +} + +int main() { + constexpr std::int32_t voiceActivity = 1001; + + Require(Throws([] { sceVoiceGetPortAttr(0, voiceActivity, nullptr, 1); }, "library not initialized")); + + VoiceInitParam init{}; + Require(sceVoiceInit(&init, 0) == 0); + + VoicePortParam param{}; + param.port_type = 2; + param.volume = 1.0f; + param.voice.bitrate = 16000; + std::uint32_t port = 0; + Require(sceVoiceCreatePort(&port, ¶m) == 0); + + std::array buffer{0xa5, 0xa5, 0xa5}; + Require(sceVoiceGetPortAttr(port, voiceActivity, &buffer[1], 1) == 0); + Require(buffer[0] == 0xa5 && buffer[1] == 0 && buffer[2] == 0xa5); + + Require(Throws([port, &buffer] { sceVoiceGetPortAttr(port, 1, &buffer[1], 1); }, "attribute 1 not implemented")); + + Require(Throws([&buffer] { sceVoiceGetPortAttr(0x1234, voiceActivity, &buffer[1], 1); }, "unknown port")); + + Require(sceVoiceDeletePort(port) == 0); + Require(Throws([port, &buffer] { sceVoiceGetPortAttr(port, voiceActivity, &buffer[1], 1); }, "unknown port")); + + Require(sceVoiceEnd_nid_postfix() == 0); + return 0; +} diff --git a/core/libs/tests/GuestVoiceQoS.cpp b/core/libs/tests/GuestVoiceQoS.cpp index bf8b3467c6..49d2671371 100644 --- a/core/libs/tests/GuestVoiceQoS.cpp +++ b/core/libs/tests/GuestVoiceQoS.cpp @@ -1,20 +1,60 @@ #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, const 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(); +int APS5_VABI sceVoiceQoSInitHQ(); +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); constexpr int initialized = static_cast(0x804E0905); + constexpr int notInitialized = static_cast(0x804E0901); 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); @@ -25,4 +65,100 @@ int main(int argc, char** argv) { Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == 0); Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == initialized); Require(sceVoiceQoSInit(nullptr, 0, 0) == initialized); + + std::int32_t local = -1; + std::int32_t remote = -1; + std::int32_t connection = -1; + Require(sceVoiceQoSCreateLocalEndpoint(nullptr, 1, 0, 0) == argumentInvalid); + Require(sceVoiceQoSCreateLocalEndpoint(&local, 1, 0, 0) == 0 && local > 0); + Require(sceVoiceQoSCreateRemoteEndpoint(&remote) == 0 && remote > 0 && remote != local); + Require(sceVoiceQoSConnect(&connection, remote, local) == argumentInvalid); + Require(sceVoiceQoSConnect(&connection, local, remote) == 0 && connection > 0); + std::int32_t 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); + + std::int32_t otherLocal = -1; + std::int32_t otherConnection = -1; + Require(sceVoiceQoSCreateLocalEndpoint(&otherLocal, 1, 0, 0) == 0); + Require(sceVoiceQoSConnect(&otherConnection, otherLocal, remote) == 0); + Require(sceVoiceQoSDeleteLocalEndpoint(otherLocal) == 0); + Require(sceVoiceQoSDeleteLocalEndpoint(otherLocal) == argumentInvalid); + Require(sceVoiceQoSDisconnect(otherConnection) == 0); + + const std::uint32_t level = 0x1234; + std::uint32_t readLevel = 0; + Require(sceVoiceQoSGetLocalEndpointAttribute(local, 3, &readLevel, sizeof(readLevel)) == 0 && readLevel == 0); + Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, &level, sizeof(level)) == 0); + Require(sceVoiceQoSGetLocalEndpointAttribute(local, 3, &readLevel, sizeof(readLevel)) == 0 && readLevel == level); + Require(sceVoiceQoSGetLocalEndpointAttribute(local, 3, nullptr, sizeof(readLevel)) == argumentInvalid); + Require(sceVoiceQoSGetLocalEndpointAttribute(local, 3, &readLevel, 0) == argumentInvalid); + Require(sceVoiceQoSGetLocalEndpointAttribute(local, 3, &readLevel, -1) == argumentInvalid); + Require(sceVoiceQoSGetLocalEndpointAttribute(local + 100, 3, &readLevel, sizeof(readLevel)) == argumentInvalid); + Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, nullptr, sizeof(level)) == argumentInvalid); + Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, &level, 0) == 0); + Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, &level, -1) == 0); + Require(sceVoiceQoSSetLocalEndpointAttribute(local + 100, 3, &level, sizeof(level)) == argumentInvalid); + + Require(sceVoiceQoSSetRemoteEndpointAttribute(remote, 3, &level, sizeof(level)) == 0); + Require(sceVoiceQoSSetRemoteEndpointAttribute(remote, 3, nullptr, sizeof(level)) == argumentInvalid); + Require(sceVoiceQoSSetRemoteEndpointAttribute(remote, 3, &level, 0) == 0); + Require(sceVoiceQoSSetRemoteEndpointAttribute(remote, 3, &level, -1) == 0); + Require(sceVoiceQoSSetRemoteEndpointAttribute(remote + 100, 3, &level, sizeof(level)) == argumentInvalid); + + 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(sceVoiceQoSDisconnect(connection) == 0); + Require(sceVoiceQoSDisconnect(connection) == argumentInvalid); + Require(sceVoiceQoSGetLocalEndpoint(connection, &queriedLocal) == argumentInvalid); + Require(sceVoiceQoSGetRemoteEndpoint(connection, &queriedRemote) == argumentInvalid); + Require(sceVoiceQoSDeleteLocalEndpoint(local) == 0); + Require(sceVoiceQoSDeleteLocalEndpoint(local) == argumentInvalid); + Require(sceVoiceQoSDeleteRemoteEndpoint(remote) == 0); + + Require(sceVoiceQoSCreateRemoteEndpoint(&remote) == 0); + 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(sceVoiceQoSGetLocalEndpointAttribute(local, 3, &readLevel, sizeof(readLevel)) == 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(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(); })); + Require(NotImplemented([] { sceVoiceQoSInitHQ(); })); + Require(NotImplemented([] { sceVoiceQoSSetMode(); })); + Require(NotImplemented([] { sceVoiceQoSTerminate(); })); } diff --git a/core/libs/tests/GuestVrSetupDialog.cpp b/core/libs/tests/GuestVrSetupDialog.cpp new file mode 100644 index 0000000000..56a48ccdd0 --- /dev/null +++ b/core/libs/tests/GuestVrSetupDialog.cpp @@ -0,0 +1,72 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI sceVrSetupDialogInitialize(void); +int APS5_VABI sceVrSetupDialogOpen(const void* param); +int APS5_VABI sceVrSetupDialogUpdateStatus(void); +int APS5_VABI sceVrSetupDialogGetStatus(void); +int APS5_VABI sceVrSetupDialogGetResult(void* result); +int APS5_VABI sceVrSetupDialogClose(void); +int APS5_VABI sceVrSetupDialogTerminate(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(sceVrSetupDialogTerminate() == kErrNotInitialized); + Require(sceVrSetupDialogOpen(¶m) == kErrNotInitialized); + Require(sceVrSetupDialogClose() == kErrNotInitialized); + Require(sceVrSetupDialogGetResult(result) == kErrNotInitialized); + Require(sceVrSetupDialogGetStatus() == kStatusNone); + Require(sceVrSetupDialogUpdateStatus() == kStatusNone); + + Require(sceVrSetupDialogInitialize() == 0); + Require(sceVrSetupDialogInitialize() == kErrAlreadyInitialized); + Require(sceVrSetupDialogGetStatus() == kStatusInitialized); + Require(sceVrSetupDialogUpdateStatus() == kStatusInitialized); + + Require(sceVrSetupDialogOpen(nullptr) == kErrArgNull); + Require(sceVrSetupDialogGetResult(result) == kErrNotFinished); + + Require(sceVrSetupDialogOpen(¶m) == 0); + Require(sceVrSetupDialogGetStatus() == kStatusFinished); + Require(sceVrSetupDialogUpdateStatus() == kStatusFinished); + + Require(sceVrSetupDialogGetResult(nullptr) == kErrArgNull); + Require(sceVrSetupDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(result[1] == -1); + + 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() == kStatusFinished); + result[0] = -1; + Require(sceVrSetupDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(sceVrSetupDialogTerminate() == 0); +} diff --git a/core/libs/tests/GuestVrTracker2.cpp b/core/libs/tests/GuestVrTracker2.cpp new file mode 100644 index 0000000000..f8d5899a66 --- /dev/null +++ b/core/libs/tests/GuestVrTracker2.cpp @@ -0,0 +1,115 @@ +#include "prx/libc/include/general/VabiMacros.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); +std::int32_t APS5_VABI sceVrTracker2Initialize(); +} + +namespace { + +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"); + } + parameter[0] = 0x21; + Require(sceVrTracker2QueryMemory(parameter.data(), requirement.data()) == InvalidArgument, "query size"); + + Require(Throws([] { sceVrTracker2Initialize(); }), "unknown export"); + return 0; +} diff --git a/core/libs/tests/GuestWaitpid.cpp b/core/libs/tests/GuestWaitpid.cpp new file mode 100644 index 0000000000..8ef90e7b19 --- /dev/null +++ b/core/libs/tests/GuestWaitpid.cpp @@ -0,0 +1,40 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +extern "C" { +int APS5_VABI wait_nid_postfix(int*); +int APS5_VABI waitpid_nid_postfix(int, int*, int); +int APS5_VABI getpid_nid_postfix(void); +int* APS5_VABI __error_nid_postfix(); +} +constexpr int noChild = 10; +constexpr int invalidArgument = 22; +constexpr int untouched = 0x5a5a5a5a; +static void Require(bool value) { if (!value) std::abort(); } +static void RequireFailure(int pid, int options, int error) { + int status = untouched; + *__error_nid_postfix() = 0; + Require(waitpid_nid_postfix(pid, &status, options) == -1); + Require(*__error_nid_postfix() == error); + Require(status == untouched); + *__error_nid_postfix() = 0; + Require(waitpid_nid_postfix(pid, nullptr, options) == -1); + Require(*__error_nid_postfix() == error); +} +int main() { + const int self = getpid_nid_postfix(); + for (int pid : {-1, 0, 1, self, self + 1, -self}) { + for (int options : {0, 1, 2, 3, 4, 8, 16, 32, 0x2c, 0x3f, static_cast(0x80000000u), static_cast(0x80000001u), static_cast(0x8000003fu)}) + RequireFailure(pid, options, noChild); + for (int options : {0x40, 0x41, 0x80, 0x100, 0x40000000, static_cast(0x80000040u), static_cast(0xffffffffu)}) + RequireFailure(pid, options, invalidArgument); + } + int status = untouched; + *__error_nid_postfix() = invalidArgument; + Require(wait_nid_postfix(&status) == -1); + Require(*__error_nid_postfix() == noChild); + Require(status == untouched); + *__error_nid_postfix() = invalidArgument; + Require(wait_nid_postfix(nullptr) == -1); + Require(*__error_nid_postfix() == noChild); +} 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 9e9f02fe00..ea5d58c2cc 100644 --- a/core/libs/tests/GuestWebBrowserDialog.cpp +++ b/core/libs/tests/GuestWebBrowserDialog.cpp @@ -6,8 +6,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); +int APS5_VABI sceWebBrowserDialogClose(void); int APS5_VABI sceWebBrowserDialogGetStatus(void); int APS5_VABI sceWebBrowserDialogUpdateStatus(void); +int APS5_VABI sceWebBrowserDialogGetResult(void* result); } namespace { @@ -15,20 +18,77 @@ namespace { constexpr int COMMON_DIALOG_STATUS_NONE = 0; constexpr int COMMON_DIALOG_STATUS_INITIALIZED = 1; constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; +constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; +constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); +constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004u); +constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); +constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); void Require(bool value) { if (!value) std::abort(); } } int main() { - Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); - Require(sceWebBrowserDialogInitialize() == 0); - Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_INITIALIZED); - Require(sceWebBrowserDialogTerminate() == 0); - Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); - Require(sceWebBrowserDialogInitialize() == 0); std::uint8_t param[64] = {}; + std::int32_t 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(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(sceWebBrowserDialogTerminate() == 0); + Require(sceWebBrowserDialogTerminate() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); + + Require(sceWebBrowserDialogInitialize() == 0); + Require(sceWebBrowserDialogOpen(nullptr) == COMMON_DIALOG_ERROR_ARG_NULL); Require(sceWebBrowserDialogOpen(param) == 0); Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_FINISHED); Require(sceWebBrowserDialogUpdateStatus() == COMMON_DIALOG_STATUS_FINISHED); + + Require(sceWebBrowserDialogGetResult(nullptr) == COMMON_DIALOG_ERROR_ARG_NULL); + Require(sceWebBrowserDialogGetResult(&result) == 0); + Require(result == COMMON_DIALOG_RESULT_USER_CANCELED); + + Require(sceWebBrowserDialogClose() == 0); + Require(sceWebBrowserDialogTerminate() == 0); + Require(sceWebBrowserDialogTerminate() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); + + Require(sceWebBrowserDialogOpenForPredeterminedContent(param) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(sceWebBrowserDialogInitialize() == 0); + Require(sceWebBrowserDialogOpenForPredeterminedContent(nullptr) == COMMON_DIALOG_ERROR_ARG_NULL); + Require(sceWebBrowserDialogOpenForPredeterminedContent(param) == 0); + Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_FINISHED); + Require(sceWebBrowserDialogUpdateStatus() == COMMON_DIALOG_STATUS_FINISHED); + + result = 0x5a5a5a5a; + Require(sceWebBrowserDialogGetResult(&result) == 0); + Require(result == COMMON_DIALOG_RESULT_USER_CANCELED); + + Require(sceWebBrowserDialogClose() == 0); + Require(sceWebBrowserDialogTerminate() == 0); + Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); + + Require(sceWebBrowserDialogInitialize() == 0); + Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_INITIALIZED); + Require(sceWebBrowserDialogClose() == 0); + Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_FINISHED); + result = 0x5a5a5a5a; + Require(sceWebBrowserDialogGetResult(&result) == 0); + Require(result == COMMON_DIALOG_RESULT_USER_CANCELED); + Require(sceWebBrowserDialogTerminate() == 0); + Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); + + return 0; } diff --git a/core/libs/tests/GuestWideConversion.cpp b/core/libs/tests/GuestWideConversion.cpp new file mode 100644 index 0000000000..335e193708 --- /dev/null +++ b/core/libs/tests/GuestWideConversion.cpp @@ -0,0 +1,232 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +std::uint64_t APS5_VABI _WStoul_nid_postfix(const char16_t*, char16_t**, int); +int APS5_VABI _Iswctype_nid_postfix(std::uint32_t, std::uint64_t); +int APS5_VABI swscanf_nid_postfix(const char16_t*, const char16_t*, ...); +int APS5_VABI swscanf_s_nid_postfix(const char16_t*, const char16_t*, ...); +int APS5_VABI iswalnum_nid_postfix(std::uint32_t); +int APS5_VABI iswalpha_nid_postfix(std::uint32_t); +int APS5_VABI iswcntrl_nid_postfix(std::uint32_t); +int APS5_VABI iswdigit_nid_postfix(std::uint32_t); +int APS5_VABI iswgraph_nid_postfix(std::uint32_t); +int APS5_VABI iswlower_nid_postfix(std::uint32_t); +int APS5_VABI iswprint_nid_postfix(std::uint32_t); +int APS5_VABI iswpunct_nid_postfix(std::uint32_t); +int APS5_VABI iswspace_nid_postfix(std::uint32_t); +int APS5_VABI iswupper_nid_postfix(std::uint32_t); +int APS5_VABI iswxdigit_nid_postfix(std::uint32_t); +int APS5_VABI iswblank_nid_postfix(std::uint32_t); +} + +static int failures = 0; + +static void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "%s\n", message); + ++failures; + } +} + +template static bool Throws(TCall call) { + try { + call(); + } catch (const std::exception&) { + return true; + } + return false; +} + +static void CheckStoul(const char16_t* text, int base, std::uint64_t value, std::size_t consumed, int error, const char* message) { + char16_t* end = nullptr; + errno = 0; + const std::uint64_t result = _WStoul_nid_postfix(text, &end, base); + const int saved = errno; + if (result != value || end != text + consumed || saved != error) { + std::fprintf(stderr, "_WStoul %s: value %llu, consumed %lld, errno %d\n", message, + static_cast(result), static_cast(end - text), saved); + ++failures; + } +} + +static void CheckStoul() { + constexpr std::uint64_t max = std::numeric_limits::max(); + CheckStoul(u" 4294967296", 10, 4294967296ULL, 12, 0, "keeps 64 bits"); + CheckStoul(u"18446744073709551615", 10, max, 20, 0, "maximum"); + CheckStoul(u"18446744073709551616", 10, max, 20, ERANGE, "overflow"); + CheckStoul(u"-1", 10, max, 2, 0, "negation"); + CheckStoul(u"\t+7xyz", 10, 7, 3, 0, "stops at a non-digit"); + CheckStoul(u"0x1F", 16, 31, 4, 0, "hex prefix with base 16"); + CheckStoul(u"0x1f", 0, 31, 4, 0, "hex prefix with base 0"); + 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"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"); + Require(_WStoul_nid_postfix(u"9", nullptr, 10) == 9, "_WStoul accepts a null end pointer"); + Require(Throws([] { _WStoul_nid_postfix(u"1", nullptr, 1); }), "_WStoul throws for base 1"); + Require(Throws([] { _WStoul_nid_postfix(u"1", nullptr, 37); }), "_WStoul throws for base 37"); +} + +static void CheckClassification() { + int (APS5_VABI* const classes[])(std::uint32_t) = { + iswalnum_nid_postfix, iswalpha_nid_postfix, iswcntrl_nid_postfix, iswdigit_nid_postfix, + iswgraph_nid_postfix, iswlower_nid_postfix, iswprint_nid_postfix, iswpunct_nid_postfix, + iswspace_nid_postfix, iswupper_nid_postfix, iswxdigit_nid_postfix, iswblank_nid_postfix, + }; + for (std::uint64_t type = 1; type <= 12; ++type) { + for (std::uint32_t c = 0; c < 0x300; ++c) { + if ((_Iswctype_nid_postfix(c, type) != 0) != (classes[type - 1](c) != 0)) { + std::fprintf(stderr, "_Iswctype(%#x, %llu) differs from its isw function\n", c, static_cast(type)); + ++failures; + } + } + Require(_Iswctype_nid_postfix(0xffffffffu, type) == 0, "_Iswctype is false for WEOF"); + } + Require(_Iswctype_nid_postfix(u'A', 2) != 0 && _Iswctype_nid_postfix(u'1', 2) == 0, "_Iswctype alpha"); + Require(_Iswctype_nid_postfix(u'7', 4) != 0 && _Iswctype_nid_postfix(u'f', 11) != 0, "_Iswctype digit and xdigit"); + Require(_Iswctype_nid_postfix(u'\t', 12) != 0 && _Iswctype_nid_postfix(u'\n', 12) == 0, "_Iswctype blank"); + Require(_Iswctype_nid_postfix(u'\n', 9) != 0 && _Iswctype_nid_postfix(u' ', 5) == 0, "_Iswctype space and graph"); + Require(_Iswctype_nid_postfix(u'a', 6) != 0 && _Iswctype_nid_postfix(u'a', 10) == 0, "_Iswctype lower and upper"); + Require(_Iswctype_nid_postfix(0x7f, 3) != 0 && _Iswctype_nid_postfix(u'!', 8) != 0, "_Iswctype cntrl and punct"); + Require(_Iswctype_nid_postfix(0xe9, 2) == 0 && _Iswctype_nid_postfix(0x3042, 1) == 0, "_Iswctype C locale has no non-ASCII letters"); + Require(Throws([] { _Iswctype_nid_postfix(u'a', 0); }), "_Iswctype throws for class 0"); + Require(Throws([] { _Iswctype_nid_postfix(u'a', 13); }), "_Iswctype throws for class 13"); + Require(Throws([] { _Iswctype_nid_postfix(u'a', 0x100); }), "_Iswctype throws for a rune mask"); +} + +static void CheckScan() { + int first = 0; + int second = 0; + Require(swscanf_s_nid_postfix(u" 42 -7", u"%d%d", &first, &second) == 2 && first == 42 && second == -7, "%d"); + Require(swscanf_s_nid_postfix(u"x=0x1f", u"x=%i", &first) == 1 && first == 31, "%i with a hex prefix"); + Require(swscanf_s_nid_postfix(u"1 2", u"%*d%d", &first) == 1 && first == 2, "suppressed %d"); + Require(swscanf_s_nid_postfix(u"12345", u"%3d%d", &first, &second) == 2 && first == 123 && second == 45, "%d width"); + std::int64_t wide = 0; + Require(swscanf_s_nid_postfix(u"5000000000", u"%ld", &wide) == 1 && wide == 5000000000LL, "%ld is 64-bit"); + short half = 0; + 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"); + char16_t small[5] = {u'?', u'?', u'?', u'?', u'?'}; + Require(swscanf_s_nid_postfix(u"hello", u"%ls", small, std::size_t{5}) == 0 && small[0] == 0, "%ls too small"); + Require(swscanf_s_nid_postfix(u"7 hello", u"%d%ls", &first, small, std::size_t{5}) == 1 && first == 7, "%ls too small after a conversion"); + char narrow[4] = {}; + Require(swscanf_s_nid_postfix(u"abc def", u"%s", narrow, std::size_t{4}) == 1 && std::strcmp(narrow, "abc") == 0, "%s"); + char16_t pair[3] = {u'?', u'?', u'?'}; + Require(swscanf_s_nid_postfix(u"xyz", u"%2lc", pair, std::size_t{2}) == 1 && pair[0] == u'x' && pair[1] == u'y' && pair[2] == u'?', "%2lc"); + Require(swscanf_s_nid_postfix(u"xyz", u"%2lc", pair, std::size_t{1}) == 0, "%2lc too small"); + Require(swscanf_s_nid_postfix(u"x", u"%2lc", pair, std::size_t{2}) == 0, "%2lc short input"); + Require(swscanf_s_nid_postfix(u"cab12", u"%l[abc]%d", text, std::size_t{6}, &first) == 2 && std::u16string(text) == u"cab" && first == 12, "%l["); + Require(swscanf_s_nid_postfix(u"été!", u"%l[^!]", text, std::size_t{6}) == 1 && std::u16string(text) == u"été", "%l[^"); + std::int64_t count = -1; + Require(swscanf_s_nid_postfix(u" 12 ab", u"%d %ln", &first, &count) == 1 && count == 4, "%ln"); + + 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"); + 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"%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"); + Require(swscanf_s_nid_postfix(u"5", u"%d", static_cast(nullptr)) == -1, "null %d argument"); + Require(swscanf_s_nid_postfix(u"5 abc", u"%d%ls", &first, static_cast(nullptr), std::size_t{4}) == -1, "null %ls argument"); + Require(swscanf_s_nid_postfix(nullptr, u"%d", &first) == -1, "null buffer"); + Require(swscanf_s_nid_postfix(u"5", nullptr) == -1, "null format"); + Require(Throws([&] { swscanf_s_nid_postfix(u"abc", u"%l[a-c]", text, std::size_t{6}); }), "scanset range throws"); + Require(Throws([&] { swscanf_s_nid_postfix(u"5", u"%Ld", &wide); }), "%Ld throws"); + Require(Throws([&] { swscanf_s_nid_postfix(u"Ā", u"%s", narrow, std::size_t{4}); }), "unconvertible narrow %s throws"); +} + +static void CheckUnsizedScan() { + int first = 0; + int second = 0; + Require(swscanf_nid_postfix(u" 42 -7", u"%d%d", &first, &second) == 2 && first == 42 && second == -7, "swscanf %d"); + Require(swscanf_nid_postfix(u"x=0x1f", u"x=%i", &first) == 1 && first == 31, "swscanf %i with a hex prefix"); + Require(swscanf_nid_postfix(u"1 2", u"%*d%d", &first) == 1 && first == 2, "swscanf suppressed %d"); + Require(swscanf_nid_postfix(u"12345", u"%3d%d", &first, &second) == 2 && first == 123 && second == 45, "swscanf %d width"); + std::int64_t wide = 0; + Require(swscanf_nid_postfix(u"5000000000", u"%ld", &wide) == 1 && wide == 5000000000LL, "swscanf %ld is 64-bit"); + short half = 0; + Require(swscanf_nid_postfix(u"-2", u"%hd", &half) == 1 && half == -2, "swscanf %hd"); + unsigned value = 0; + Require(swscanf_nid_postfix(u"0xg", u"%x", &value) == 0, "swscanf %x prefix without digits is a matching failure"); + 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"); + Require(swscanf_nid_postfix(u"7 a-long-word", u"%d%ls", &first, text) == 2 && first == 7 && std::u16string(text) == u"a-long-word", "swscanf %ls takes no size"); + Require(swscanf_nid_postfix(u"hello world", u"%3ls", text) == 1 && std::u16string(text) == u"hel", "swscanf %ls width"); + char narrow[8] = {}; + Require(swscanf_nid_postfix(u"abc def", u"%s%d", narrow, &first) == 1 && std::strcmp(narrow, "abc") == 0, "swscanf %s"); + char16_t pair[3] = {u'?', u'?', u'?'}; + Require(swscanf_nid_postfix(u"xyz", u"%2lc", pair) == 1 && pair[0] == u'x' && pair[1] == u'y' && pair[2] == u'?', "swscanf %2lc"); + Require(swscanf_nid_postfix(u"x", u"%2lc", pair) == 0, "swscanf %2lc short input"); + char letter = 0; + Require(swscanf_nid_postfix(u" q7", u"%c%d", &letter, &first) == 1 && letter == ' ', "swscanf %c does not skip white space"); + Require(swscanf_nid_postfix(u"cab12", u"%l[abc]%d", text, &first) == 2 && std::u16string(text) == u"cab" && first == 12, "swscanf %l["); + Require(swscanf_nid_postfix(u"été!", u"%l[^!]", text) == 1 && std::u16string(text) == u"été", "swscanf %l[^"); + Require(swscanf_nid_postfix(u"key=val", u"%[^=]=%s", narrow, narrow + 4) == 2 && std::strcmp(narrow, "key") == 0 && std::strcmp(narrow + 4, "val") == 0, "swscanf %[^"); + std::int64_t count = -1; + Require(swscanf_nid_postfix(u" 12 ab", u"%d %ln", &first, &count) == 1 && count == 4, "swscanf %ln"); + int shortCount = -1; + Require(swscanf_nid_postfix(u"abc", u"%*ls%n", &shortCount) == 0 && shortCount == 3, "swscanf %n after a suppressed conversion"); + Require(swscanf_nid_postfix(u"50% off", u"%d%% off", &first) == 1 && first == 50, "swscanf %%"); + Require(swscanf_nid_postfix(u"50 off", u"%d%%", &first) == 1, "swscanf %% mismatch is a matching failure"); + + double real = 0; + Require(swscanf_nid_postfix(u"1.5e3", u"%lf", &real) == 1 && real == 1500.0, "swscanf %lf"); + Require(swscanf_nid_postfix(u"1e+x", u"%lf", &real) == 0, "swscanf %lf incomplete exponent is a matching failure"); + long double extended = 0; + Require(swscanf_nid_postfix(u"2.25", u"%Lf", &extended) == 1 && extended == 2.25L, "swscanf %Lf"); + float single = 0; + Require(swscanf_nid_postfix(u"-inf", u"%f", &single) == 1 && single == -std::numeric_limits::infinity(), "swscanf %f infinity"); + + Require(swscanf_nid_postfix(u"a", u"b%d", &first) == 0, "swscanf literal mismatch is a matching failure"); + Require(swscanf_nid_postfix(u"3 x", u"%d%d", &first, &second) == 1 && first == 3, "swscanf partial match"); + Require(swscanf_nid_postfix(u"", u"%d", &first) == -1, "swscanf empty input is an input failure"); + Require(swscanf_nid_postfix(u"", u"%ls", text) == -1, "swscanf empty input before %ls"); + Require(swscanf_nid_postfix(u" ", u"%ls", text) == -1, "swscanf white space only is an input failure"); + Require(swscanf_nid_postfix(u"5", u"%d%d", &first, &second) == 1, "swscanf input failure after a conversion"); + Require(swscanf_nid_postfix(nullptr, u"%d", &first) == -1, "swscanf null buffer"); + Require(swscanf_nid_postfix(u"5", nullptr) == -1, "swscanf null format"); + Require(Throws([&] { swscanf_nid_postfix(u"abc", u"%l[a-c]", text); }), "swscanf scanset range throws"); + Require(Throws([&] { swscanf_nid_postfix(u"5", u"%Ld", &wide); }), "swscanf %Ld throws"); + Require(Throws([&] { swscanf_nid_postfix(u"1", u"%lp", &pointer); }), "swscanf %lp throws"); + Require(Throws([&] { swscanf_nid_postfix(u"Ā", u"%s", narrow); }), "swscanf unconvertible narrow %s throws"); +} + +int main() { + CheckStoul(); + CheckClassification(); + CheckScan(); + CheckUnsizedScan(); + return failures == 0 ? 0 : 1; +} diff --git a/core/libs/tests/GuestWideFormatting.cpp b/core/libs/tests/GuestWideFormatting.cpp index c758ba10b9..8ebfa2753e 100644 --- a/core/libs/tests/GuestWideFormatting.cpp +++ b/core/libs/tests/GuestWideFormatting.cpp @@ -1,13 +1,22 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/GuestTime.hpp" #include #include #include #include +#include #include extern "C" int APS5_VABI vswprintf_nid_postfix(char16_t*, std::size_t, const char16_t*, VaList*); extern "C" int APS5_VABI snwprintf_s_nid_postfix(char16_t*, std::size_t, const char16_t*, ...); +extern "C" int APS5_VABI vswprintf_s_nid_postfix(char16_t*, std::size_t, const char16_t*, VaList*); +extern "C" int APS5_VABI swprintf_s_nid_postfix(char16_t*, std::size_t, const char16_t*, ...); +extern "C" int APS5_VABI vsnwprintf_s_nid_postfix(char16_t*, std::size_t, const char16_t*, VaList*); +extern "C" int APS5_VABI swprintf_nid_postfix(char16_t*, std::size_t, const char16_t*, ...); +extern "C" std::size_t APS5_VABI strftime_nid_postfix(char*, std::size_t, const char*, const GuestTm*); +extern "C" std::size_t APS5_VABI wcsftime_nid_postfix(char16_t*, std::size_t, const char16_t*, const GuestTm*); +extern "C" int* APS5_VABI __error_nid_postfix(); static int APS5_VABI Format(char16_t* buffer, std::size_t size, const char16_t* format, ...) { #ifdef _WIN32 @@ -88,6 +97,32 @@ static void CheckBounded() { Require(Format(buffer, 16, u"%s", nullNarrow) == 6 && buffer == std::u16string(u"(null)"), "vswprintf null %s argument"); } +static void CheckPositional() { + char16_t buffer[32]; + Require(swprintf_nid_postfix(buffer, 32, u"%2$d-%1$d", 1, 2) == 3 && buffer == std::u16string(u"2-1"), + "swprintf positional integers"); + Require(swprintf_nid_postfix(buffer, 32, u"%2$ls %1$ls", u"world", u"hello") == 11 && buffer == std::u16string(u"hello world"), + "swprintf positional wide strings"); + Require(swprintf_nid_postfix(buffer, 32, u"[%1$*2$d|%3$.*2$f]", 7, 2, 2.5) == 9 && buffer == std::u16string(u"[ 7|2.50]"), + "swprintf positional width and precision"); + Require(swprintf_nid_postfix(buffer, 32, u"%1$Lg %2$d %d", 0.5L, 3, 9) == 7 && buffer == std::u16string(u"0.5 3 9"), + "swprintf positional long double and the next argument"); + Require(snwprintf_s_nid_postfix(buffer, 32, u"%2$s%1$lc", 0x20ac, "\xc3\xa9") == 2 && buffer == std::u16string(u"\u00e9\u20ac"), + "snwprintf_s positional arguments"); +} + +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)) == 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() { char16_t buffer[16]; int count = -7; @@ -111,9 +146,170 @@ static void CheckCount() { Require(Format(buffer, 16, u"a%n", nullCount) < 0, "vswprintf null %n argument"); } +static void CheckGroupingFlag() { + char16_t buffer[48]; + Require(swprintf_nid_postfix(buffer, 48, u"%'d|%'-6u|%'.2f|%'+05d|%'g", 1234567, 42u, 1234.5, 12, 1e6) == 34 && + buffer == std::u16string(u"1234567|42 |1234.50|+0012|1e+06"), "swprintf ' flag"); + Require(snwprintf_s_nid_postfix(buffer, 48, u"%'lld %ls", 9876543210ll, u"x") == 12 && buffer == std::u16string(u"9876543210 x"), + "snwprintf_s ' flag"); +} + +static void CheckSecureWideBoundary() { + constexpr std::size_t wideRsizeMax = (SIZE_MAX >> 1) / sizeof(char16_t); + char16_t buffer[4]; + for (auto& unit : buffer) unit = u'x'; + + buffer[0] = u'x'; + buffer[1] = u'y'; + Require(swprintf_s_nid_postfix(buffer, wideRsizeMax, u"%n") < 0 && buffer[0] == 0, "swprintf_s accepted-boundary failure clears"); + Require(buffer[1] == u'y', "swprintf_s does not clear beyond the first unit"); + + buffer[0] = u'x'; + buffer[1] = u'y'; + Require(swprintf_s_nid_postfix(buffer, wideRsizeMax + 1, u"%n") < 0 && buffer[0] == u'x' && buffer[1] == u'y', + "swprintf_s oversized size leaves the buffer untouched"); + + buffer[0] = u'x'; + buffer[1] = u'y'; + Require(vswprintf_s_nid_postfix(buffer, wideRsizeMax, u"%n", nullptr) < 0 && buffer[0] == 0, "vswprintf_s accepted-boundary failure clears"); + buffer[0] = u'x'; + buffer[1] = u'y'; + Require(vswprintf_s_nid_postfix(buffer, wideRsizeMax + 1, u"%n", nullptr) < 0 && buffer[0] == u'x' && buffer[1] == u'y', + "vswprintf_s oversized size leaves the buffer untouched"); + + buffer[0] = u'x'; + buffer[1] = u'y'; + Require(vsnwprintf_s_nid_postfix(buffer, wideRsizeMax, u"%n", nullptr) < 0 && buffer[0] == 0, "vsnwprintf_s accepted-boundary failure clears"); + buffer[0] = u'x'; + buffer[1] = u'y'; + Require(vsnwprintf_s_nid_postfix(buffer, wideRsizeMax + 1, u"%n", nullptr) < 0 && buffer[0] == u'x' && buffer[1] == u'y', + "vsnwprintf_s oversized size leaves the buffer untouched"); +} + +static void CheckSwprintf() { + char16_t buffer[8]; + for (auto& unit : buffer) unit = u'x'; + Require(swprintf_nid_postfix(buffer, 8, u"%d-%ls", 42, u"ab") == 5, "swprintf length"); + Require(buffer == std::u16string(u"42-ab") && buffer[6] == u'x', "swprintf output"); + Require(swprintf_nid_postfix(buffer, 8, u"中%ls%s", u"文", "\xc3\xa9") == 3, "swprintf non-ASCII length"); + Require(buffer == std::u16string(u"中文é"), "swprintf non-ASCII output"); + Require(swprintf_nid_postfix(buffer, 8, u"%s", "\xf0\x9f\x98\x80") == 2, "swprintf surrogate pair length"); + Require(buffer == std::u16string(u"\U0001f600"), "swprintf surrogate pair output"); + Require(swprintf_nid_postfix(buffer, 8, u"") == 0 && buffer[0] == 0, "swprintf empty format"); + for (auto& unit : buffer) unit = u'x'; + Require(swprintf_nid_postfix(buffer, 3, u"%ls", u"abcd") < 0, "swprintf truncation result"); + Require(buffer == std::u16string(u"ab") && buffer[3] == u'x', "swprintf truncation output"); + Require(swprintf_nid_postfix(buffer, 3, u"abc") < 0, "swprintf terminator does not fit"); + buffer[0] = u'x'; + Require(swprintf_nid_postfix(buffer, 0, u"a") < 0 && buffer[0] == u'x', "swprintf zero size"); + Require(swprintf_nid_postfix(nullptr, 8, u"a") < 0, "swprintf null buffer"); + Require(swprintf_nid_postfix(buffer, 5, u"%ls", u"abcd") == 4 && buffer == std::u16string(u"abcd"), "swprintf exact fit"); + char16_t wide[16]; + Require(swprintf_nid_postfix(wide, 16, u"%5ls|%-3c|%lc", u"wide", 'z', 0x20ac) == 11, "swprintf padded length"); + Require(wide == std::u16string(u" wide|z |€"), "swprintf padded output"); + Require(swprintf_nid_postfix(wide, 16, u"%q") < 0 && wide[0] == 0, "swprintf invalid conversion"); +} + +static bool WcsftimeThrows(char16_t* buffer, const char16_t* format, const GuestTm* time) { + try { + wcsftime_nid_postfix(buffer, 16, format, time); + } catch (const std::invalid_argument&) { + return true; + } + return false; +} + +static void CheckWcsftime() { + GuestTm time{}; + time.tm_sec = 9; + time.tm_min = 5; + time.tm_hour = 13; + time.tm_mday = 29; + time.tm_mon = 1; + time.tm_year = 124; + time.tm_wday = 4; + time.tm_yday = 59; + time.tm_zone = "UTC"; + struct Case { + const char16_t* wide; + const char* narrow; + }; + const Case cases[] = { + {u"%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M:%S"}, + {u"%a %b %j %p %Z", "%a %b %j %p %Z"}, + {u"%%", "%%"}, + {u"%k|%l|%v|%+", "%k|%l|%v|%+"}, + {u"\u00e9t\u00e9 %Y", "\xe9t\xe9 %Y"}, + }; + for (const auto& item : cases) { + char narrow[128]; + char16_t wide[128]; + const std::size_t length = strftime_nid_postfix(narrow, sizeof(narrow), item.narrow, &time); + Require(length != 0, "wcsftime reference strftime"); + Require(wcsftime_nid_postfix(wide, 128, item.wide, &time) == length, "wcsftime length matches strftime"); + for (std::size_t index = 0; index <= length; ++index) + Require(wide[index] == static_cast(narrow[index]), "wcsftime output matches strftime"); + } + char16_t buffer[32]; + Require(wcsftime_nid_postfix(buffer, 32, u"%Y-%m-%d %H:%M:%S", &time) == 19 + && buffer == std::u16string(u"2024-02-29 13:05:09"), "wcsftime date and time"); + Require(wcsftime_nid_postfix(buffer, 32, u"%a %b %j %p", &time) == 14 + && buffer == std::u16string(u"Thu Feb 060 PM"), "wcsftime names"); + Require(wcsftime_nid_postfix(buffer, 32, u"%%", &time) == 1 && buffer == std::u16string(u"%"), "wcsftime percent"); + Require(wcsftime_nid_postfix(buffer, 20, u"%Y-%m-%d %H:%M:%S", &time) == 19, "wcsftime exact fit"); + + int& error = *__error_nid_postfix(); + for (auto& unit : buffer) unit = u'x'; + error = 5; + Require(wcsftime_nid_postfix(buffer, 19, u"%Y-%m-%d %H:%M:%S", &time) == 0 && error == 5 && buffer[0] == u'x', + "wcsftime overflow"); + Require(wcsftime_nid_postfix(buffer, 1, u"%Y", &time) == 0 && error == 5 && buffer[0] == u'x', "wcsftime one unit"); + Require(wcsftime_nid_postfix(buffer, 0, u"%Y", &time) == 0 && error == 5 && buffer[0] == u'x', "wcsftime zero size"); + Require(wcsftime_nid_postfix(buffer, 32, u"", &time) == 0 && error == 5 && buffer[0] == u'x', "wcsftime empty format"); + Require(wcsftime_nid_postfix(nullptr, 32, u"%Y", &time) == 0 && error == 5, "wcsftime null buffer"); + Require(wcsftime_nid_postfix(buffer, 32, u"%Y \u20ac", &time) == 0 && error == 86 && buffer[0] == u'x', + "wcsftime unconvertible format"); + error = 5; + Require(wcsftime_nid_postfix(buffer, SIZE_MAX, u"%Y", &time) == 0 && error == 22 && buffer[0] == u'x', + "wcsftime oversized buffer"); + Require(WcsftimeThrows(buffer, nullptr, &time), "wcsftime null format"); + Require(WcsftimeThrows(buffer, u"%Y", nullptr), "wcsftime null time"); +} + +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(); CheckCount(); + CheckSwprintf(); + CheckSecureWideBoundary(); + CheckWcsftime(); Check(u"%.2s", "\xc3\xa9\xc3\xa8", u"\u00e9\u00e8"); Check(u"%.1s", "\xc3\xa9\xc3\xa8", u"\u00e9"); Check(u"%.0s", "\xc3\xa9", u""); @@ -125,4 +321,5 @@ int main() { Check(u"%.2s", "\xf0\x9f\x98\x80x", u"\U0001f600"); Check(u"%.1s", "\xf0\x9f\x98\x80x", u""); Check(u"%.3s", "\xf0\x9f\x98\x80x", u"\U0001f600x"); + CheckGroupingFlag(); } diff --git a/core/libs/tests/GuestWideStreamScan.cpp b/core/libs/tests/GuestWideStreamScan.cpp new file mode 100644 index 0000000000..0da21861b0 --- /dev/null +++ b/core/libs/tests/GuestWideStreamScan.cpp @@ -0,0 +1,164 @@ +#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("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); +} + +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 2e01b4d336..809d47fe3e 100644 --- a/core/libs/tests/GuestWideStrings.cpp +++ b/core/libs/tests/GuestWideStrings.cpp @@ -1,30 +1,58 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include #include +#include +#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); +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); 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); @@ -47,11 +75,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); @@ -68,6 +151,14 @@ int main() { require(wcscpy_nid_postfix(buffer, u"wide") == buffer); require(same(buffer, u"wide", 5) && buffer[5] == 0xaaaa); + char16_t concatenated[8]; + wmemset_nid_postfix(concatenated, 0xaaaa, 8); + concatenated[0] = 0; + require(wcscat_nid_postfix(concatenated, u"ab") == concatenated); + require(wcscat_nid_postfix(concatenated, u"世") == concatenated); + const char16_t expectedConcatenated[] = {u'a', u'b', u'世', 0, 0xaaaa}; + require(same(concatenated, expectedConcatenated, 5)); + wmemset_nid_postfix(buffer, 0xaaaa, 8); require(wcsncpy_nid_postfix(buffer, u"ab", 5) == buffer); const char16_t padded[] = {u'a', u'b', 0, 0, 0, 0xaaaa}; @@ -77,6 +168,63 @@ 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 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, 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); @@ -95,6 +243,38 @@ 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; + char16_t* token = wcstok_nid_postfix(tokens, u",", &last); + require(token == tokens + 2 && same(token, u"one", 4) && last == tokens + 6); + token = wcstok_nid_postfix(nullptr, u",;", &last); + require(token == tokens + 7 && same(token, u"two", 4) && last == tokens + 11); + token = wcstok_nid_postfix(nullptr, u",", &last); + require(token == tokens + 11 && same(token, u"three", 6) && last == tokens + 17); + require(wcstok_nid_postfix(nullptr, u",", &last) == nullptr && last == nullptr); + require(wcstok_nid_postfix(nullptr, u",", &last) == nullptr && last == nullptr); + + char16_t tail[] = u"a b"; + require(wcstok_nid_postfix(tail, u" ", &last) == tail && last == tail + 2); + require(wcstok_nid_postfix(nullptr, u" ", &last) == tail + 2 && same(tail, u"a\0b", 4) && last == nullptr); + + char16_t onlyDelimiters[] = u";;"; + last = onlyDelimiters; + require(wcstok_nid_postfix(onlyDelimiters, u";", &last) == nullptr && last == nullptr && same(onlyDelimiters, u";;", 3)); + + char16_t empty[] = u""; + last = empty; + require(wcstok_nid_postfix(empty, u";", &last) == nullptr && last == nullptr); + + char16_t whole[] = u"word"; + require(wcstok_nid_postfix(whole, u"", &last) == whole && last == nullptr && same(whole, u"word", 5)); + + char16_t wide[] = u"\u4e00\u3001\xd83d\xde00\u3001"; + require(wcstok_nid_postfix(wide, u"\u3001", &last) == wide && same(wide, u"\u4e00\0", 2) && last == wide + 2); + require(wcstok_nid_postfix(nullptr, u"\u3001", &last) == wide + 2 && wide[2] == 0xd83d && wide[3] == 0xde00 && wide[4] == 0 && last == wide + 5); + require(wcstok_nid_postfix(nullptr, u"\u3001", &last) == nullptr && last == nullptr); const char16_t units[] = {u'x', 0, u'￿', u'y'}; require(wmemchr_nid_postfix(units, u'￿', 4) == units + 2); @@ -141,6 +321,13 @@ int main() { require(wcstof_nid_postfix(u"0.25", nullptr) == 0.25f); const char16_t* half = u"-1.5é"; require(wcstold_nid_postfix(half, &end) == -1.5L && end == half + 4); + const char16_t* wideNan = u" -nan(0x123)é"; + require(std::bit_cast(wcstod_nid_postfix(wideNan, &end)) == 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(wcscoll_nid_postfix(u"a", u"b") < 0); require(wcscoll_nid_postfix(u"￿", u"a") > 0); @@ -149,7 +336,49 @@ int main() { char16_t transformed[8]; wmemset_nid_postfix(transformed, 0xaaaa, 8); require(wcsxfrm_nid_postfix(transformed, u"wide", 8) == 4 && same(transformed, u"wide", 5) && transformed[5] == 0xaaaa); + wmemset_nid_postfix(transformed, 0xaaaa, 8); require(wcsxfrm_nid_postfix(transformed, u"much too long", 4) == 13); + if (!same(transformed, u"muc", 4)) std::fputs("wcsxfrm did not copy and terminate its truncated result\n", stderr); + require(same(transformed, u"muc", 4) && transformed[4] == 0xaaaa); require(wcsxfrm_nid_postfix(nullptr, u"abc", 0) == 3); + struct TransformCase { + const char16_t* source; + std::size_t count; + std::size_t length; + const char16_t* expected; + std::size_t written; + }; + for (const TransformCase test : { + TransformCase{u"wide", 0, 4, u"", 0}, + TransformCase{u"wide", 1, 4, u"", 1}, + TransformCase{u"wide", 4, 4, u"wid", 4}, + TransformCase{u"wide", 5, 4, u"wide", 5}, + TransformCase{u"", 0, 0, u"", 0}, + TransformCase{u"", 1, 0, u"", 1}, + TransformCase{u"", 8, 0, u"", 1}, + TransformCase{u"\x7fff\x8000\xffff\xd83d\xde00", 4, 5, u"\x7fff\x8000\xffff", 4}, + TransformCase{u"\x7fff\x8000\xffff\xd83d\xde00", 6, 5, u"\x7fff\x8000\xffff\xd83d\xde00", 6} + }) { + wmemset_nid_postfix(transformed, 0xaaaa, 8); + require(wcsxfrm_nid_postfix(transformed, test.source, test.count) == test.length); + require(same(transformed, test.expected, test.written)); + for (std::size_t index = test.written; index < 8; ++index) require(transformed[index] == 0xaaaa); + } + { + 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 28090a8a1f..de3ea75fc1 100644 --- a/core/libs/tests/GuestWorkingDirectory.cpp +++ b/core/libs/tests/GuestWorkingDirectory.cpp @@ -1,16 +1,28 @@ +#include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/General.hpp" #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); int* APS5_VABI __error_nid_postfix(); } static void Require(bool value) { if (!value) std::abort(); } +static int allocations = 0; +static int releases = 0; +static void* APS5_VABI Allocate(std::size_t bytes) { ++allocations; return GuestHeap::GuestHeapAllocate_nid_postfix(bytes); } +static void APS5_VABI Release(void* pointer) { ++releases; GuestHeap::GuestHeapFree_nid_postfix(pointer); } +static void* APS5_VABI Unused() { std::abort(); } int main() { + const std::array api{reinterpret_cast(&Allocate), reinterpret_cast(&Release), reinterpret_cast(&Unused), + reinterpret_cast(&Unused), reinterpret_cast(&Unused), reinterpret_cast(&Unused), reinterpret_cast(&Unused)}; + ApplicationHeapRegister_nid_no_patch(api.data()); const auto host = std::filesystem::canonical(std::filesystem::current_path()); char path[1024]; Require(getcwd_nid_postfix(path, sizeof(path)) == path && std::strcmp(path, "/") == 0); @@ -28,8 +40,17 @@ int main() { Require(std::strcmp(tiny, "xyz") == 0); Require(getcwd_nid_postfix(path, 0) == nullptr && *__error_nid_postfix() == 22); char* allocated = getcwd_nid_postfix(nullptr, 0); - Require(allocated && std::strcmp(allocated, path) == 0); - GuestHeap::GuestHeapFree_nid_postfix(allocated); + 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..50a9688420 100644 --- a/core/libs/tests/GuestZlib.cpp +++ b/core/libs/tests/GuestZlib.cpp @@ -68,6 +68,8 @@ int main() { corrupt.back() ^= 1; decode(corrupt.data(), corrupt.size(), expectedLength, static_cast(0x811200FF)); decode(compressed.data(), compressed.size() - 1, 65536, static_cast(0x811200FF)); + decode(compressed.data(), compressed.size() - 4, expectedLength, static_cast(0x811200FF)); + decode(compressed.data(), compressed.size() - 1, expectedLength, static_cast(0x811200FF)); Require(sceZlibFinalize() == 0); Require(sceZlibGetResult(id, &produced, &status) == static_cast(0x81120032)); std::array scratch{}; 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/HostCpu.cpp b/core/libs/tests/HostCpu.cpp new file mode 100644 index 0000000000..7c5d3aea88 --- /dev/null +++ b/core/libs/tests/HostCpu.cpp @@ -0,0 +1,139 @@ +#include "prx/libkernel/System/include/HostCpu.hpp" +#include +#include +#include +#include +#include + +static void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +static constexpr std::uint32_t Bit(unsigned bit) { + return std::uint32_t{1} << bit; +} + +static HostCpuRegisters Zen2() { + HostCpuRegisters registers; + registers.Leaf1Ecx = Bit(0) | Bit(1) | Bit(9) | Bit(12) | Bit(13) | Bit(19) | Bit(20) | Bit(22) | Bit(23) | Bit(25) | Bit(26) | Bit(27) | Bit(28) | Bit(29) | Bit(30); + registers.Leaf1Edx = Bit(25) | Bit(26); + registers.Leaf7Ebx = Bit(3) | Bit(5) | Bit(8) | Bit(18) | Bit(19) | Bit(23) | Bit(24) | Bit(29); + registers.Leaf7Ecx = Bit(22); + registers.LeafDSub1Eax = Bit(0) | Bit(1); + registers.Leaf80000001Ecx = Bit(0) | Bit(5) | Bit(6) | Bit(8); + registers.Leaf80000008Ebx = Bit(0); + registers.Xcr0 = 0x7; + return registers; +} + +static std::string Text(const std::vector& names) { + std::string text; + for (const auto& name : names) text += (text.empty() ? "" : ",") + name; + return text; +} + +static void CheckMissing(const HostCpuRegisters& registers, const std::vector& expected, const std::string& label) { + const auto missing = MissingHostCpuFeatures(registers); + Require(missing == expected, label + ": expected [" + Text(expected) + "], got [" + Text(missing) + "]"); +} + +static void CheckZen2() { + CheckMissing(Zen2(), {}, "Zen 2"); + for (const auto& feature : HostCpuFeatures(Zen2())) + Require(feature.Present, std::string("Zen 2 lacks ") + feature.Name); +} + +static void CheckIntelWithoutAmdOnlyExtensions() { + auto registers = Zen2(); + registers.Leaf7Ebx &= ~(Bit(18) | Bit(19) | Bit(23) | Bit(24) | Bit(29)); + registers.Leaf7Ecx = 0; + registers.LeafDSub1Eax = Bit(0); + registers.Leaf80000001Ecx &= ~(Bit(6) | Bit(8)); + registers.Leaf80000008Ebx = 0; + CheckMissing(registers, {}, "AVX2 host without SSE4A, CLZERO, SHA, ADX, RDSEED, CLFLUSHOPT, CLWB, RDPID"); +} + +static void CheckSingleMissingExtension() { + struct Case { + const char* Name; + std::uint32_t HostCpuRegisters::* Register; + unsigned Bit; + }; + const Case cases[] = { + {"BMI2", &HostCpuRegisters::Leaf7Ebx, 8}, + {"BMI1", &HostCpuRegisters::Leaf7Ebx, 3}, + {"AVX2", &HostCpuRegisters::Leaf7Ebx, 5}, + {"MOVBE", &HostCpuRegisters::Leaf1Ecx, 22}, + {"FMA", &HostCpuRegisters::Leaf1Ecx, 12}, + {"F16C", &HostCpuRegisters::Leaf1Ecx, 29}, + {"POPCNT", &HostCpuRegisters::Leaf1Ecx, 23}, + {"SSE4.2", &HostCpuRegisters::Leaf1Ecx, 20}, + {"CMPXCHG16B", &HostCpuRegisters::Leaf1Ecx, 13}, + {"LZCNT", &HostCpuRegisters::Leaf80000001Ecx, 5}, + {"LAHF/SAHF", &HostCpuRegisters::Leaf80000001Ecx, 0}, + }; + for (const auto& entry : cases) { + auto registers = Zen2(); + registers.*entry.Register &= ~Bit(entry.Bit); + CheckMissing(registers, {entry.Name}, std::string("without ") + entry.Name); + } +} + +static void CheckAvxWithoutOsState() { + auto registers = Zen2(); + registers.Xcr0 = 0x3; + CheckMissing(registers, {"OS AVX state (XCR0)"}, "XCR0 without YMM state"); + registers = Zen2(); + registers.Leaf1Ecx &= ~Bit(27); + CheckMissing(registers, {"OS AVX state (XCR0)"}, "OSXSAVE clear"); +} + +static void CheckPreHaswell() { + auto registers = Zen2(); + registers.Leaf1Ecx &= ~(Bit(12) | Bit(22)); + registers.Leaf7Ebx = 0; + registers.Leaf80000001Ecx &= ~Bit(5); + CheckMissing(registers, {"LZCNT", "BMI1", "BMI2", "MOVBE", "AVX2", "FMA"}, "AVX host without AVX2"); +} + +static void CheckError() { + auto registers = Zen2(); + registers.Leaf7Ebx &= ~Bit(8); + CheckHostCpu(Zen2(), "AMD Ryzen 7 3700X 8-Core Processor"); + try { + CheckHostCpu(registers, "Example CPU"); + throw std::logic_error("missing BMI2 did not throw"); + } catch (const std::runtime_error& error) { + const std::string message = error.what(); + Require(message.find("(Example CPU) lacks BMI2.") != std::string::npos, "unexpected message: " + message); + } + try { + CheckHostCpu(registers, "AMD Example "); + throw std::logic_error("missing BMI2 did not throw"); + } catch (const std::runtime_error& error) { + const std::string message = error.what(); + Require(message.find("(AMD Example) lacks BMI2.") != std::string::npos, "unexpected message: " + message); + } + try { + CheckHostCpu(registers, ""); + throw std::logic_error("missing BMI2 did not throw"); + } catch (const std::runtime_error& error) { + const std::string message = error.what(); + Require(message.find("(unknown model) lacks BMI2.") != std::string::npos, "unexpected message: " + message); + } +} + +int main() { + try { + CheckZen2(); + CheckIntelWithoutAmdOnlyExtensions(); + CheckSingleMissingExtension(); + CheckAvxWithoutOsState(); + CheckPreHaswell(); + CheckError(); + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } + return 0; +} diff --git a/core/libs/tests/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 5332d836ac..3d3b40d608 100644 --- a/core/libs/tests/ImageSampleGather.cpp +++ b/core/libs/tests/ImageSampleGather.cpp @@ -2,6 +2,7 @@ #include "RdnaDecoder/RdnaImageOpDecoder.hpp" #include #include +#include using namespace ShaderRecompiler; static void Require(bool value) { if (!value) throw std::runtime_error("image sample/gather regression"); } @@ -27,6 +28,30 @@ static void CheckPck(std::uint32_t encoding, RdnaOpcode opcode, std::uint32_t dm Require(instruction.dataDwordCount == dataComponents); Require(IsImageOpcode(instruction.op)); } +static void CheckSamplerVccTuple() { + const std::array code{0xf0800f08u, 26u << 21u}; + const RdnaInstruction instruction = DecodeRdnaMimg(0u, code, 0u); + Require(instruction.op == RdnaOpcode::ImageSample); + Require(instruction.source1.reg == 0u); + Require(instruction.source2.kind == RdnaOperandKind::ScalarRegister && instruction.source2.reg == 104u); + IrProgram program; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + TranslationContext context(program, block, 256); + context.TranslateInstruction(instruction); + Require(program.Resources().memoryInfo.size() == 1u); + Require(program.Resources().memoryInfo.back().sampler == 26u); +} +static void CheckSamplerVccTupleOverflow() { + const std::array code{0xf0800f08u, 27u << 21u}; + bool rejected = false; + try { + DecodeRdnaMimg(0u, code, 0u); + } catch (const std::runtime_error&) { + rejected = true; + } + Require(rejected); +} static void CheckPckRejectsZeroMask(std::uint32_t encoding) { const std::array code{(0x3cu << 26u) | (encoding << 18u) | (1u << 3u), 0u}; bool rejected = false; @@ -37,7 +62,36 @@ static void CheckPckRejectsZeroMask(std::uint32_t encoding) { } Require(rejected); } +static void CheckUnsupportedOpcodeDiagnostic(std::uint32_t encoding, std::string_view expected) { + const std::array code{(0x3cu << 26u) | ((encoding & 0x7fu) << 18u) | (encoding >> 7u), 0u}; + try { + DecodeRdnaMimg(0u, code, 0u); + } catch (const std::runtime_error& error) { + Require(std::string_view(error.what()).find(expected) != std::string_view::npos); + return; + } + 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); Check(0x35u, RdnaOpcode::ImageSampleBO, 0xfu); @@ -88,4 +142,6 @@ int main() { CheckPck(0x63u, RdnaOpcode::ImageGather8hPck, 0xFu, 4u); CheckPckRejectsZeroMask(0x62u); CheckPckRejectsZeroMask(0x63u); + CheckUnsupportedOpcodeDiagnostic(0x06u, "MIMG opcode 0x06"); + CheckUnsupportedOpcodeDiagnostic(0xffu, "MIMG opcode 0xff"); } diff --git a/core/libs/tests/InterpolationReferenceRange.cpp b/core/libs/tests/InterpolationReferenceRange.cpp new file mode 100644 index 0000000000..7bf9c80ad3 --- /dev/null +++ b/core/libs/tests/InterpolationReferenceRange.cpp @@ -0,0 +1,53 @@ +#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 is out of range (attribute 5 component 1 mode 0, 2 inputs)") != std::string::npos, "an attribute past the pixel inputs", attribute); + 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..0fa28bad98 --- /dev/null +++ b/core/libs/tests/JsonCopyFailure.cpp @@ -0,0 +1,220 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +struct Value { void* node; }; +struct String { void* text; }; +struct Array { void* items; }; +struct Object { void* items; }; +struct Pair { String key; std::uint64_t reserved; Value value; }; + +extern "C" { +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"); + } + _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]), 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/LinearDisplayBuffer.cpp b/core/libs/tests/LinearDisplayBuffer.cpp index ad10b3f421..ff5e5da9b8 100644 --- a/core/libs/tests/LinearDisplayBuffer.cpp +++ b/core/libs/tests/LinearDisplayBuffer.cpp @@ -1,7 +1,16 @@ #include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp" +#include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" +#include "prx/libSceAgcDriver/Execution/include/PqToSdr.hpp" +#include "prx/libSceAgcDriver/Graphics/include/HalfFloatScanout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp" +#include #include +#include +#include +#include #include #include +#include static void Require(bool condition) { if (!condition) std::abort(); } template static void Reject(TAction action) { @@ -10,6 +19,157 @@ template static void Reject(TAction action) { Require(rejected); } +static std::size_t EquationOffset(std::uint32_t elementBytes, std::uint32_t blocksPerRow, std::uint32_t blockHeight, std::uint32_t x, std::uint32_t y) { + const auto* equation = AgcDriver::Graphics::FindTextureSwizzleEquation(27u, elementBytes); + Require(equation != nullptr); + std::size_t offset = 0; + for (std::uint32_t bit = 0; bit < 16u; ++bit) { + const auto mask = equation->bits[bit]; + offset |= static_cast(std::popcount((x & mask & 0xfffu) ^ ((y << 12u) & mask & 0xfff000u)) & 1) << bit; + } + const auto blockWidth = 65536u / elementBytes / blockHeight; + return (static_cast(y / blockHeight) * blocksPerRow + x / blockWidth) * 65536u + offset; +} + +static void HalfFloatCodes() { + for (std::uint32_t bits = 0; bits <= 0xffffu; ++bits) { + const auto exponent = static_cast((bits >> 10u) & 0x1fu); + const auto mantissa = static_cast(bits & 0x3ffu); + const double magnitude = exponent == 0 ? std::ldexp(mantissa, -24) : exponent == 31 ? (mantissa == 0 ? INFINITY : NAN) : std::ldexp(1024.0 + mantissa, exponent - 25); + const double value = (bits & 0x8000u) != 0u ? -magnitude : magnitude; + unsigned color = 0; + unsigned alpha = 0; + if (value > 0) { + const double clamped = std::min(value, 1.0); + const double encoded = clamped <= 0.0031308 ? 12.92 * clamped : 1.055 * std::pow(clamped, 1.0 / 2.4) - 0.055; + color = static_cast(std::floor(encoded * 255.0 + 0.5)); + alpha = static_cast(std::floor(clamped * 255.0 + 0.5)); + } + Require(AgcDriver::Graphics::HalfFloatScanoutCode(static_cast(bits), false) == color); + Require(AgcDriver::Graphics::HalfFloatScanoutCode(static_cast(bits), true) == alpha); + } +} + +static void HalfFloatDisplay() { + constexpr std::uint64_t rgba16Float = 0xc001000622000000ull; + const std::array halves{0x3c00, 0x3800, 0x0000, 0x3c00, 0xbc00, 0x4000, 0x7e00, 0x3800, 0x1234, 0x1234, 0x1234, 0x1234, + 0x7c00, 0x3400, 0x1419, 0x0000, 0x0001, 0x3555, 0x3c00, 0x3c00, 0x1234, 0x1234, 0x1234, 0x1234}; + std::array input{}; + std::memcpy(input.data(), halves.data(), input.size()); + AgcDriver::DisplayBuffer buffer{65536, rgba16Float, 2, 2, 1, 3}; + Require(AgcDriver::DisplayBufferSize(buffer) == input.size()); + const auto pixels = AgcDriver::DecodeDisplayBuffer(buffer, input); + const std::array expected{0, 188, 255, 255, 0, 255, 0, 128, 3, 137, 255, 0, 255, 156, 0, 255}; + Require(pixels.size() == expected.size()); + for (unsigned i = 0; i < pixels.size(); ++i) Require(std::to_integer(pixels[i]) == expected[i]); + Require(AgcDriver::DisplayBufferOffset(buffer, 1, 1) == 32); + buffer.pixelFormat = 0xc001000600000000ull; + const auto swapped = AgcDriver::DecodeDisplayBuffer(buffer, input); + const std::array blueFirst{255, 188, 0, 255, 0, 255, 0, 128, 255, 137, 3, 0, 0, 156, 255, 255}; + for (unsigned i = 0; i < swapped.size(); ++i) Require(std::to_integer(swapped[i]) == blueFirst[i]); + Require(AgcDriver::DisplayHalfFloat(buffer.pixelFormat) && !AgcDriver::DisplayRedLow(buffer.pixelFormat) && AgcDriver::DisplayTexelFormat(buffer.pixelFormat) == VK_FORMAT_R16G16B16A16_SFLOAT); + Require(AgcDriver::DisplayTexelFormat(rgba16Float) == VK_FORMAT_R16G16B16A16_SFLOAT && AgcDriver::DisplayTexelBytes(rgba16Float) == 8 && AgcDriver::DisplayRedLow(rgba16Float) && !AgcDriver::DisplayTenBit(rgba16Float)); + Require(AgcDriver::ResidentPresentPath(VK_FORMAT_R16G16B16A16_SFLOAT, rgba16Float, true) == AgcDriver::ResidentPresent::Convert); + Require(AgcDriver::ResidentPresentPath(VK_FORMAT_R8G8B8A8_UNORM, rgba16Float, true) == AgcDriver::ResidentPresent::None); + Require(AgcDriver::ResidentPresentPath(VK_FORMAT_R16G16B16A16_SFLOAT, 0x8000000022000000ull, true) == AgcDriver::ResidentPresent::None); + buffer = {65536, rgba16Float, 3840, 2160}; + Require(AgcDriver::DisplayBufferSize(buffer) == 30u * 34u * 65536u); + buffer.dccAddress = 0x20000; + Reject([&] { AgcDriver::DisplayBufferSize(buffer); }); + buffer.dccAddress = 0; + buffer.pixelFormat = 0xc001000700000000ull; + Reject([&] { AgcDriver::DisplayBufferSize(buffer); }); + for (const auto [pixelFormat, elementBytes, blockHeight] : {std::tuple{rgba16Float, 8u, 64u}, std::tuple{std::uint64_t{0x8000000000000000ull}, 4u, 128u}}) { + buffer = {65536, pixelFormat, 300, 200}; + for (std::uint32_t y = 0; y < buffer.height; ++y) { + for (std::uint32_t x = 0; x < buffer.width; ++x) Require(AgcDriver::DisplayBufferOffset(buffer, x, y) == EquationOffset(elementBytes, 3, blockHeight, x, y)); + } + } +} + +static double ReferencePqNits(std::uint32_t code) { + const double m1 = 2610.0 / 16384.0, m2 = 2523.0 / 4096.0 * 128.0, c1 = 3424.0 / 4096.0, c2 = 2413.0 / 4096.0 * 32.0, c3 = 2392.0 / 4096.0 * 32.0; + const double e = std::pow(code / 1023.0, 1.0 / m2); + return 10000.0 * std::pow(std::max(e - c1, 0.0) / (c2 - c3 * e), 1.0 / m1); +} + +static std::array ReferenceSdr(std::uint32_t red, std::uint32_t green, std::uint32_t blue, double whiteNits) { + const double referenceWhite = std::min(203.0, whiteNits); + const double r = ReferencePqNits(red) / referenceWhite, g = ReferencePqNits(green) / referenceWhite, b = ReferencePqNits(blue) / referenceWhite; + std::array rgb{std::max(1.6604910 * r - 0.5876411 * g - 0.0728499 * b, 0.0), std::max(-0.1245505 * r + 1.1328999 * g - 0.0083494 * b, 0.0), std::max(-0.0181508 * r - 0.1005789 * g + 1.1187297 * b, 0.0)}; + const double luminance = 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]; + const double knee = 0.75; + if (luminance > knee) { + const double t = (luminance - knee) / (1.0 - knee), w = (whiteNits / referenceWhite - knee) / (1.0 - knee); + const double mapped = knee + (1.0 - knee) * t * (1.0 + t / (w * w)) / (1.0 + t); + for (auto& channel : rgb) channel *= mapped / luminance; + } + for (auto& channel : rgb) { + const double v = std::clamp(channel, 0.0, 1.0); + channel = 255.0 * (v <= 0.0031308 ? 12.92 * v : 1.055 * std::pow(v, 1.0 / 2.4) - 0.055); + } + return rgb; +} + +static std::uint32_t PqWord(std::uint32_t red, std::uint32_t green, std::uint32_t blue, bool redLow) { + return (redLow ? red : blue) | (green << 10u) | ((redLow ? blue : red) << 20u) | (3u << 30u); +} + +static void PqDisplay() { + constexpr std::uint64_t rgbPq = 0x8100070422000000ull; + constexpr std::uint64_t bgrPq = 0x8100070400000000ull; + for (const auto pixelFormat : {rgbPq, bgrPq}) { + const bool redLow = pixelFormat == rgbPq; + Require(AgcDriver::DisplayPq(pixelFormat) && AgcDriver::DisplayTenBit(pixelFormat) && !AgcDriver::DisplayHalfFloat(pixelFormat) && AgcDriver::DisplayRedLow(pixelFormat) == redLow); + Require(AgcDriver::DisplayTexelBytes(pixelFormat) == 4 && AgcDriver::DisplayTexelFormat(pixelFormat) == (redLow ? VK_FORMAT_A2B10G10R10_UNORM_PACK32 : VK_FORMAT_A2R10G10B10_UNORM_PACK32)); + for (const auto storage : {VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_FORMAT_A2R10G10B10_UNORM_PACK32}) Require(AgcDriver::ResidentPresentPath(storage, pixelFormat, true) == AgcDriver::ResidentPresent::Convert); + Require(AgcDriver::ResidentPresentPath(VK_FORMAT_B8G8R8A8_UNORM, pixelFormat, true) == AgcDriver::ResidentPresent::None && AgcDriver::ResidentPresentPath(VK_FORMAT_R16G16B16A16_SFLOAT, pixelFormat, true) == AgcDriver::ResidentPresent::None); + Require(AgcDriver::DisplayBufferSize({65536, pixelFormat, 3840, 2160}) == 30u * 17u * 65536u); + struct Anchor { + std::uint32_t red, green, blue; + unsigned r, g, b; + }; + const std::array anchors{{{0, 0, 0, 0, 0, 0}, {1023, 1023, 1023, 255, 255, 255}, {520, 520, 520, 255, 255, 255}, {450, 450, 450, 187, 187, 187}, + {600, 500, 300, 255, 213, 0}, {480, 470, 350, 225, 204, 95}, {350, 420, 500, 16, 167, 241}, {1023, 0, 0, 255, 0, 0}}}; + std::array input{}; + for (std::size_t i = 0; i < anchors.size(); ++i) { + const auto word = PqWord(anchors[i].red, anchors[i].green, anchors[i].blue, redLow); + std::memcpy(input.data() + i * 4, &word, 4); + } + const AgcDriver::DisplayBuffer buffer{65536, pixelFormat, 4, 2, 1, 0}; + Require(AgcDriver::DisplayBufferSize(buffer) == input.size()); + const auto pixels = AgcDriver::DecodeDisplayBuffer(buffer, input); + for (std::size_t i = 0; i < anchors.size(); ++i) { + Require(std::to_integer(pixels[i * 4]) == anchors[i].b && std::to_integer(pixels[i * 4 + 1]) == anchors[i].g && std::to_integer(pixels[i * 4 + 2]) == anchors[i].r && std::to_integer(pixels[i * 4 + 3]) == 255); + } + } + struct WhiteAnchor { + std::uint32_t red, green, blue; + unsigned r, g, b; + }; + const std::array shoulder{{{770, 770, 770, 255, 255, 255}, {700, 700, 700, 252, 252, 252}, {650, 650, 650, 249, 249, 249}, {560, 560, 560, 221, 221, 221}, {450, 450, 450, 136, 136, 136}, {600, 500, 300, 255, 155, 0}}}; + for (const auto& anchor : shoulder) { + const auto pixel = AgcDriver::PqToSdrPixel(PqWord(anchor.red, anchor.green, anchor.blue, true), true, 1000.0f); + Require(pixel[2] == anchor.r && pixel[1] == anchor.g && pixel[0] == anchor.b && pixel[3] == 255); + } + for (const auto pixelFormat : {0x8100070000000000ull, 0x8100000400000000ull, 0x8000070400000000ull, 0x8100070400000001ull, 0x8100070600000000ull, 0xc001070700000000ull}) { + Require(!AgcDriver::DisplayPq(pixelFormat)); + Reject([&] { AgcDriver::DisplayBufferSize({65536, pixelFormat, 64, 64}); }); + } + for (const float white : {100.0f, 150.0f, 1000.0f}) { + for (std::uint32_t red = 0; red < 1024; red += 31) { + for (std::uint32_t green = 0; green < 1024; green += 31) { + for (std::uint32_t blue = 0; blue < 1024; blue += 31) { + const auto expected = ReferenceSdr(red, green, blue, white); + const auto pixel = AgcDriver::PqToSdrPixel(PqWord(red, green, blue, true), true, white); + Require(std::fabs(pixel[2] - expected[0]) <= 1.0 && std::fabs(pixel[1] - expected[1]) <= 1.0 && std::fabs(pixel[0] - expected[2]) <= 1.0 && pixel[3] == 255); + Require(AgcDriver::PqToSdrPixel(PqWord(red, green, blue, false), false, white) == pixel); + } + } + } + } +} + int main() { AgcDriver::DisplayBuffer buffer{65536, 0x8000000000000000ull, 2, 2, 1, 3}; const std::array input{1,2,3,4, 5,6,7,8, 99,99,99,99, @@ -32,4 +192,7 @@ int main() { Reject([&] { AgcDriver::DisplayBufferSize(buffer); }); buffer.tilingMode = 0; Require(AgcDriver::DisplayBufferSize(buffer) == 65536); + HalfFloatCodes(); + HalfFloatDisplay(); + PqDisplay(); } diff --git a/core/libs/tests/LogMacros.cpp b/core/libs/tests/LogMacros.cpp new file mode 100644 index 0000000000..9ff2d5c98f --- /dev/null +++ b/core/libs/tests/LogMacros.cpp @@ -0,0 +1,109 @@ +#include "prx/libc/include/general/LogMacros.hpp" +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#include +#include +#else +#include +#include +#endif + +namespace { + +void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "log macros test failed: %s\n", what); + std::exit(1); +} + +template +std::vector CaptureWrites(TAction action) { + std::vector messages; +#ifdef _WIN32 + const std::string name = R"(\\.\pipe\anyps5-log-macros-)" + std::to_string(GetCurrentProcessId()); + HANDLE server = CreateNamedPipeA(name.c_str(), PIPE_ACCESS_INBOUND, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT | PIPE_REJECT_REMOTE_CLIENTS, 1, 0, 1 << 20, 0, nullptr); + Require(server != INVALID_HANDLE_VALUE, "message pipe"); + HANDLE client = CreateFileA(name.c_str(), GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr); + Require(client != INVALID_HANDLE_VALUE, "message pipe client"); + std::FILE* stream = _fdopen(_open_osfhandle(reinterpret_cast(client), _O_WRONLY | _O_BINARY), "wb"); + std::thread reader([&] { + std::string buffer(1 << 20, '\0'); + DWORD read = 0; + while (ReadFile(server, buffer.data(), static_cast(buffer.size()), &read, nullptr)) messages.emplace_back(buffer.data(), read); + }); +#else + int sockets[2]; + Require(socketpair(AF_UNIX, SOCK_SEQPACKET, 0, sockets) == 0, "message socket pair"); + std::FILE* stream = fdopen(sockets[0], "w"); + std::thread reader([&] { + std::string buffer(1 << 20, '\0'); + ssize_t read = 0; + while ((read = recv(sockets[1], buffer.data(), buffer.size(), 0)) > 0) messages.emplace_back(buffer.data(), static_cast(read)); + }); +#endif + Require(stream != nullptr, "message stream"); + Require(std::setvbuf(stream, nullptr, _IONBF, 0) == 0, "unbuffered message stream"); + action(stream); + std::fclose(stream); + reader.join(); +#ifdef _WIN32 + CloseHandle(server); +#else + close(sockets[1]); +#endif + return messages; +} + +std::string Join(const std::vector& messages) { + std::string text; + for (const auto& message : messages) text += message; + return text; +} + +bool HasLine(const std::string& text, const std::string& tail, bool timestamped) { + const std::string head = "[tests/LogMacros.cpp:"; + 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 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", 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", 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", 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/MemoryPoolTest.cpp b/core/libs/tests/MemoryPoolTest.cpp new file mode 100644 index 0000000000..4134c904a7 --- /dev/null +++ b/core/libs/tests/MemoryPoolTest.cpp @@ -0,0 +1,341 @@ +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" +#include "prx/libkernel/DirectMemory/MemoryPool.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include +#include +#include +#include +#include +#include + +bool DirectMemoryCheckedFree(int64_t start, size_t len); +void DirectMemoryRetype(int64_t start, size_t len, int memoryType); +size_t DirectMemoryFreeRun(uint64_t offset, uint64_t limit); + +namespace { + +constexpr uint64_t kPage = static_cast(PS5_PAGE_SIZE); +constexpr uint64_t kPool = static_cast(DIRECT_MEMORY_SIZE); + +enum class Fault { None, BackingBadAlloc, BackingRuntimeError, ForgetRuntimeError }; +Fault g_fault = Fault::None; +long g_newBudget = -1; + +int g_failures = 0; + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + std::printf(" FAILED line %d: %s\n", __LINE__, #cond); \ + ++g_failures; \ + } \ + } while (0) + +struct NewBudget { + explicit NewBudget(long n) { g_newBudget = n; } + ~NewBudget() { g_newBudget = -1; } +}; + +bool BlockAt(uint64_t offset, int64_t& s, int64_t& e, int& t) { + return DirectMemoryFind(static_cast(offset), false, &s, &e, &t); +} + +bool BlockIs(uint64_t offset, uint64_t start, uint64_t end, int type) { + int64_t s = 0, e = 0; + int t = 0; + return BlockAt(offset, s, e, t) && s == static_cast(start) && e == static_cast(end) && t == type; +} + +bool PoolEmpty() { + int64_t s, e; + int t; + return !DirectMemoryFind(0, true, &s, &e, &t) && DirectMemoryFreeRun(0, kPool) == kPool; +} + +void alloc_unplaceable_requests() { + int64_t out = -1; + CHECK(DirectMemoryAlloc(0, kPool, 0, 0, 1, &out) == SCE_KERNEL_ERROR_EINVAL); + CHECK(DirectMemoryAlloc(-1, kPool, kPage, 0, 1, &out) == SCE_KERNEL_ERROR_EAGAIN); + CHECK(DirectMemoryAlloc(0, kPool, SIZE_MAX, 0, 1, &out) == SCE_KERNEL_ERROR_EAGAIN); + CHECK(DirectMemoryAlloc(0, kPool, kPool + kPage, 0, 1, &out) == SCE_KERNEL_ERROR_EAGAIN); + CHECK(DirectMemoryAlloc(kPage, kPage, kPage, 0, 1, &out) == SCE_KERNEL_ERROR_EAGAIN); + CHECK(DirectMemoryAlloc(2 * kPage, kPage, kPage, 0, 1, &out) == SCE_KERNEL_ERROR_EAGAIN); + CHECK(DirectMemoryAlloc(kPool, kPool + kPage, kPage, 0, 1, &out) == SCE_KERNEL_ERROR_EAGAIN); + CHECK(out == -1); + CHECK(PoolEmpty()); +} + +void alloc_null_out() { + CHECK(DirectMemoryAlloc(0, kPool, kPage, 0, 1, nullptr) == SCE_KERNEL_ERROR_EFAULT); + CHECK(PoolEmpty()); +} + +void alloc_rounding_alignment_and_clamping() { + int64_t a = -1, b = -1; + CHECK(DirectMemoryAlloc(0, kPool, 1, 1, 1, &a) == 0 && a == 0); + CHECK(DirectMemoryAlloc(0, kPool, kPage, 0, 1, &b) == 0 && b == static_cast(kPage)); + DirectMemoryFree(0, kPool); + + CHECK(DirectMemoryAlloc(0, -1, kPage, 0, 1, &a) == 0 && a == 0); + DirectMemoryFree(0, kPage); + CHECK(DirectMemoryAlloc(0, INT64_MAX, kPage, 0, 1, &a) == 0 && a == 0); + DirectMemoryFree(0, kPage); + + CHECK(DirectMemoryAlloc(1, kPool, kPage, 0, 1, &a) == 0 && a == static_cast(kPage)); + DirectMemoryFree(0, kPool); + + CHECK(DirectMemoryAlloc(0, kPool, kPage, static_cast(1) << 62, 1, &a) == 0 && a == 0); + CHECK(DirectMemoryAlloc(0, kPool, kPage, static_cast(1) << 62, 1, &b) == SCE_KERNEL_ERROR_EAGAIN); + DirectMemoryFree(0, kPool); + CHECK(PoolEmpty()); +} + +void alloc_non_power_of_two_alignment() { + int64_t a = -1, b = -1, c = -1; + CHECK(DirectMemoryAlloc(kPage, kPool, kPage, 3 * kPage, 1, &a) == 0 && a == static_cast(3 * kPage)); + CHECK(DirectMemoryAlloc(kPage, kPool, kPage, 3 * kPage, 1, &b) == 0 && b == static_cast(6 * kPage)); + CHECK(DirectMemoryAlloc(0, kPool, kPage, 3 * kPage, 1, &c) == 0 && c == 0); + CHECK(BlockIs(3 * kPage, 3 * kPage, 4 * kPage, 1)); + DirectMemoryFree(0, kPool); + CHECK(PoolEmpty()); +} + +void alloc_exhaustion_and_gap_reuse() { + int64_t a, b, c; + CHECK(DirectMemoryAlloc(0, 3 * kPage, kPage, 0, 1, &a) == 0 && a == 0); + CHECK(DirectMemoryAlloc(0, 3 * kPage, kPage, 0, 1, &b) == 0 && b == static_cast(kPage)); + CHECK(DirectMemoryAlloc(0, 3 * kPage, kPage, 0, 1, &c) == 0 && c == static_cast(2 * kPage)); + CHECK(DirectMemoryAlloc(0, 3 * kPage, kPage, 0, 1, &c) == SCE_KERNEL_ERROR_EAGAIN); + DirectMemoryFree(static_cast(kPage), kPage); + CHECK(DirectMemoryAlloc(0, 3 * kPage, kPage, 0, 1, &c) == 0 && c == static_cast(kPage)); + DirectMemoryFree(0, kPool); +} + +void free_out_of_range_is_ignored() { + int64_t a; + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 7, &a) == 0 && a == 0); + DirectMemoryFree(0, SIZE_MAX); + DirectMemoryFree(static_cast(kPool), kPage); + DirectMemoryFree(static_cast(kPool - kPage), 2 * kPage); + DirectMemoryFree(INT64_MAX, kPage); + CHECK(!DirectMemoryCheckedFree(0, SIZE_MAX)); + CHECK(!DirectMemoryCheckedFree(static_cast(kPool), kPage)); + CHECK(BlockIs(0, 0, 4 * kPage, 7)); + DirectMemoryFree(0, kPool); + CHECK(PoolEmpty()); +} + +void retype_out_of_range_is_ignored() { + int64_t a; + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 7, &a) == 0 && a == 0); + DirectMemoryRetype(0, SIZE_MAX, 9); + DirectMemoryRetype(static_cast(kPool), kPage, 9); + DirectMemoryRetype(static_cast(kPool - kPage), 2 * kPage, 9); + CHECK(BlockIs(0, 0, 4 * kPage, 7)); + DirectMemoryFree(0, kPool); +} + +void free_partial_and_spanning() { + int64_t a, b; + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 1, &a) == 0 && a == 0); + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 2, &b) == 0 && b == static_cast(4 * kPage)); + DirectMemoryFree(static_cast(2 * kPage), 4 * kPage); + CHECK(BlockIs(0, 0, 2 * kPage, 1)); + CHECK(BlockIs(6 * kPage, 6 * kPage, 8 * kPage, 2)); + CHECK(DirectMemoryFreeRun(2 * kPage, kPool) == 4 * kPage); + CHECK(!DirectMemoryCheckedFree(0, 4 * kPage)); + CHECK(DirectMemoryCheckedFree(0, 2 * kPage)); + DirectMemoryFree(0, kPool); + CHECK(PoolEmpty()); +} + +void retype_splits_and_skips_gaps() { + int64_t a; + CHECK(DirectMemoryAlloc(2 * kPage, kPool, 4 * kPage, 0, 1, &a) == 0 && a == static_cast(2 * kPage)); + DirectMemoryRetype(static_cast(3 * kPage), kPage, 9); + CHECK(BlockIs(2 * kPage, 2 * kPage, 3 * kPage, 1)); + CHECK(BlockIs(3 * kPage, 3 * kPage, 4 * kPage, 9)); + CHECK(BlockIs(4 * kPage, 4 * kPage, 6 * kPage, 1)); + DirectMemoryRetype(0, 8 * kPage, 5); + CHECK(BlockIs(2 * kPage, 2 * kPage, 3 * kPage, 5)); + CHECK(BlockIs(5 * kPage, 4 * kPage, 6 * kPage, 5)); + int64_t s, e; + int t; + CHECK(!BlockAt(0, s, e, t) && !BlockAt(6 * kPage, s, e, t)); + DirectMemoryFree(0, kPool); +} + +void alloc_backing_bad_alloc_rolls_back() { + int64_t out = -1; + g_fault = Fault::BackingBadAlloc; + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 1, &out) == SCE_KERNEL_ERROR_ENOMEM); + g_fault = Fault::None; + CHECK(out == -1); + CHECK(PoolEmpty()); + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 1, &out) == 0 && out == 0); + DirectMemoryFree(0, kPool); +} + +void alloc_node_bad_alloc_rolls_back() { + int64_t out = -1; + { + NewBudget budget(0); + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 1, &out) == SCE_KERNEL_ERROR_ENOMEM); + } + CHECK(out == -1); + CHECK(PoolEmpty()); + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 1, &out) == 0 && out == 0); + DirectMemoryFree(0, kPool); +} + +void alloc_unexpected_exception_propagates() { + int64_t out = -1; + g_fault = Fault::BackingRuntimeError; + bool threw = false; + try { + (void)DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 1, &out); + } catch (const std::runtime_error&) { + threw = true; + } + g_fault = Fault::None; + CHECK(threw); + CHECK(PoolEmpty()); +} + +void free_node_bad_alloc_rolls_back() { + int64_t a; + CHECK(DirectMemoryAlloc(0, kPool, 8 * kPage, 0, 3, &a) == 0 && a == 0); + for (long budget = 0; budget < 2; ++budget) { + bool threw = false; + { + NewBudget guard(budget); + try { + DirectMemoryFree(static_cast(2 * kPage), 2 * kPage); + } catch (const std::bad_alloc&) { + threw = true; + } + } + CHECK(threw); + CHECK(BlockIs(0, 0, 8 * kPage, 3)); + CHECK(DirectMemoryFreeRun(2 * kPage, 4 * kPage) == 0); + } + DirectMemoryFree(static_cast(2 * kPage), 2 * kPage); + CHECK(BlockIs(0, 0, 2 * kPage, 3)); + CHECK(BlockIs(4 * kPage, 4 * kPage, 8 * kPage, 3)); + CHECK(DirectMemoryFreeRun(2 * kPage, 4 * kPage) == 2 * kPage); + DirectMemoryFree(0, kPool); +} + +void retype_node_bad_alloc_rolls_back() { + int64_t a; + CHECK(DirectMemoryAlloc(0, kPool, 8 * kPage, 0, 3, &a) == 0 && a == 0); + for (long budget = 0; budget < 2; ++budget) { + bool threw = false; + { + NewBudget guard(budget); + try { + DirectMemoryRetype(static_cast(2 * kPage), 2 * kPage, 9); + } catch (const std::bad_alloc&) { + threw = true; + } + } + CHECK(threw); + CHECK(BlockIs(0, 0, 8 * kPage, 3)); + } + DirectMemoryRetype(static_cast(2 * kPage), 2 * kPage, 9); + CHECK(BlockIs(2 * kPage, 2 * kPage, 4 * kPage, 9)); + DirectMemoryFree(0, kPool); +} + +void forget_exception_propagates_and_rolls_back() { + int64_t a; + CHECK(DirectMemoryAlloc(0, kPool, 8 * kPage, 0, 3, &a) == 0 && a == 0); + g_fault = Fault::ForgetRuntimeError; + bool threwFree = false, threwChecked = false; + try { + DirectMemoryFree(static_cast(2 * kPage), 2 * kPage); + } catch (const std::runtime_error&) { + threwFree = true; + } + try { + (void)DirectMemoryCheckedFree(static_cast(2 * kPage), 2 * kPage); + } catch (const std::runtime_error&) { + threwChecked = true; + } + g_fault = Fault::None; + CHECK(threwFree); + CHECK(threwChecked); + CHECK(BlockIs(0, 0, 8 * kPage, 3)); + DirectMemoryFree(0, kPool); + CHECK(PoolEmpty()); +} + +struct Case { + const char* name; + void (*fn)(); +}; + +const Case kCases[] = { + {"alloc_unplaceable_requests", alloc_unplaceable_requests}, + {"alloc_null_out", alloc_null_out}, + {"alloc_rounding_alignment_and_clamping", alloc_rounding_alignment_and_clamping}, + {"alloc_non_power_of_two_alignment", alloc_non_power_of_two_alignment}, + {"alloc_exhaustion_and_gap_reuse", alloc_exhaustion_and_gap_reuse}, + {"free_out_of_range_is_ignored", free_out_of_range_is_ignored}, + {"retype_out_of_range_is_ignored", retype_out_of_range_is_ignored}, + {"free_partial_and_spanning", free_partial_and_spanning}, + {"retype_splits_and_skips_gaps", retype_splits_and_skips_gaps}, + {"alloc_backing_bad_alloc_rolls_back", alloc_backing_bad_alloc_rolls_back}, + {"alloc_node_bad_alloc_rolls_back", alloc_node_bad_alloc_rolls_back}, + {"alloc_unexpected_exception_propagates", alloc_unexpected_exception_propagates}, + {"free_node_bad_alloc_rolls_back", free_node_bad_alloc_rolls_back}, + {"retype_node_bad_alloc_rolls_back", retype_node_bad_alloc_rolls_back}, + {"forget_exception_propagates_and_rolls_back", forget_exception_propagates_and_rolls_back}, +}; + +} + +void CreateDirectMemoryBacking(int64_t, size_t, int) { + if (g_fault == Fault::BackingBadAlloc) throw std::bad_alloc(); + if (g_fault == Fault::BackingRuntimeError) throw std::runtime_error("backing"); +} + +void ForgetDirectMemory(int64_t, size_t) { + if (g_fault == Fault::ForgetRuntimeError) throw std::runtime_error("forget"); +} + +void* operator new(size_t n) { + if (g_newBudget == 0) throw std::bad_alloc(); + if (g_newBudget > 0) --g_newBudget; + void* p = std::malloc(n ? n : 1); + if (!p) throw std::bad_alloc(); + return p; +} +void operator delete(void* p) noexcept { std::free(p); } +void operator delete(void* p, size_t) noexcept { std::free(p); } + +int main(int argc, char** argv) { + if (argc == 2 && std::strcmp(argv[1], "--list") == 0) { + for (const Case& c : kCases) std::printf("%s\n", c.name); + return 0; + } + int ran = 0; + for (const Case& c : kCases) { + if (argc == 2 && std::strcmp(argv[1], c.name) != 0) continue; + std::printf("[ RUN ] %s\n", c.name); + const int before = g_failures; + try { + c.fn(); + } catch (...) { + std::printf(" FAILED: unexpected exception escaped\n"); + ++g_failures; + } + std::printf("[ %s ] %s\n", g_failures == before ? " OK " : "FAIL", c.name); + ++ran; + } + if (ran == 0) { + std::printf("no such case\n"); + return 2; + } + return g_failures == 0 ? 0 : 1; +} diff --git a/core/libs/tests/Mspace.cpp b/core/libs/tests/Mspace.cpp index 2d61f1c56a..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" { @@ -18,9 +19,21 @@ void APS5_VABI sceLibcMspaceFree_nid_postfix(void*, void*); int APS5_VABI sceLibcMspacePosixMemalign_nid_postfix(void*, void**, std::size_t, std::size_t); std::size_t APS5_VABI sceLibcMspaceMallocUsableSize_nid_postfix(const void*); void* APS5_VABI sceLibcMspaceMemalign_nid_postfix(void*, std::size_t, std::size_t); +void* APS5_VABI sceLibcMspaceAlignedAlloc_nid_postfix(void*, std::size_t, std::size_t); void* APS5_VABI sceLibcMspaceReallocalign_nid_postfix(void*, void*, std::size_t, std::size_t); int APS5_VABI sceLibcMspaceMallocStats_nid_postfix(void*, void*); int APS5_VABI sceLibcMspaceMallocStatsFast_nid_postfix(void*, void*); +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; @@ -34,7 +47,125 @@ 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); + Require(arena != nullptr); + const auto aligned = [](const void* pointer) { return pointer && (reinterpret_cast(pointer) & 31u) == 0; }; + void* blocks[64] = {}; + for (std::size_t index = 0; index < 64; ++index) { + blocks[index] = sceLibcMspaceMalloc_nid_postfix(arena, index + 1); + Require(aligned(blocks[index])); + std::memset(blocks[index], 0xa5, index + 1); + } + void* zeroed = sceLibcMspaceCalloc_nid_postfix(arena, 3, 13); + Require(aligned(zeroed)); + blocks[0] = sceLibcMspaceRealloc_nid_postfix(arena, blocks[0], 600); + Require(aligned(blocks[0])); + void* small = sceLibcMspaceMemalign_nid_postfix(arena, 16, 40); + Require(aligned(small)); + 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)); + for (void* block : blocks) sceLibcMspaceFree_nid_postfix(arena, block); + for (void* block : {zeroed, small, moved, posix}) sceLibcMspaceFree_nid_postfix(arena, block); + Require(sceLibcMspaceDestroy_nid_postfix(arena) == 0); +} +static void CheckAdjacentBlocks() { + alignas(32) static std::array storage{}; + void* arena = sceLibcMspaceCreate_nid_postfix("adjacent", storage.data(), storage.size(), 0); + Require(arena != nullptr); + const auto end = [](void* block) { return static_cast(block) + sceLibcMspaceMallocUsableSize_nid_postfix(block); }; + std::array blocks{}; + for (std::size_t index = 0; index < blocks.size(); ++index) { + blocks[index] = sceLibcMspaceMalloc_nid_postfix(arena, index % 48 + 1); + Require(blocks[index] != nullptr); + if (index > 0) Require(blocks[index] == end(blocks[index - 1])); + } + void* last = blocks.back(); + Require(sceLibcMspaceRealloc_nid_postfix(arena, last, 80) == last); + void* after = sceLibcMspaceMalloc_nid_postfix(arena, 16); + Require(after == end(last)); + MallocManagedSize stats{sizeof(MallocManagedSize), 1, 0, 0, 0, 0, 0}; + Require(sceLibcMspaceMallocStats_nid_postfix(arena, &stats) == 0); + Require(stats.currentInuseSize == static_cast(end(after) - static_cast(blocks[0]))); + Require(sceLibcMspaceDestroy_nid_postfix(arena) == 0); +} +static void CheckMovedBlockKeepsUsableBytes() { + alignas(32) static std::array storage{}; + void* arena = sceLibcMspaceCreate_nid_postfix("moved", storage.data(), storage.size(), 0); + Require(arena != nullptr); + const auto fill = [](void* block) { + const auto usable = sceLibcMspaceMallocUsableSize_nid_postfix(block); + Require(usable > 1); + for (std::size_t index = 0; index < usable; ++index) static_cast(block)[index] = static_cast(index + 1); + return usable; + }; + const auto kept = [](const void* block, std::size_t usable) { + for (std::size_t index = 0; index < usable; ++index) if (static_cast(block)[index] != static_cast(index + 1)) return false; + return true; + }; + void* block = sceLibcMspaceMalloc_nid_postfix(arena, 1); + const auto usable = fill(block); + Require(sceLibcMspaceMalloc_nid_postfix(arena, 1) != nullptr); + void* moved = sceLibcMspaceRealloc_nid_postfix(arena, block, 4096); + Require(moved != nullptr && moved != block && kept(moved, usable)); + void* other = sceLibcMspaceMalloc_nid_postfix(arena, 1); + const auto otherUsable = fill(other); + Require(sceLibcMspaceMalloc_nid_postfix(arena, 1) != nullptr); + void* realigned = sceLibcMspaceReallocalign_nid_postfix(arena, other, 256, 4096); + Require(realigned != nullptr && realigned != other && kept(realigned, otherUsable)); + Require(sceLibcMspaceDestroy_nid_postfix(arena) == 0); +} int main() { + CheckPaf(); + CheckAlignment(); + CheckAdjacentBlocks(); + CheckMovedBlockKeepsUsableBytes(); alignas(16) std::array storage{}; void* arena = sceLibcMspaceCreate_nid_postfix("test", storage.data(), storage.size(), 0); Require(arena != nullptr); @@ -51,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); @@ -113,15 +244,23 @@ int main() { void* memaligned = sceLibcMspaceMemalign_nid_postfix(arena, 256, 100); Require(memaligned && (reinterpret_cast(memaligned) & 255) == 0); Require(sceLibcMspaceMemalign_nid_postfix(arena, 24, 100) == nullptr); + void* alignedAlloc = sceLibcMspaceAlignedAlloc_nid_postfix(arena, 128, 256); + Require(alignedAlloc && (reinterpret_cast(alignedAlloc) & 127) == 0); + Require(sceLibcMspaceAlignedAlloc_nid_postfix(arena, 24, 100) == nullptr); unsigned char* realigned = static_cast(sceLibcMspaceReallocalign_nid_postfix(arena, nullptr, 64, 64)); Require(realigned && (reinterpret_cast(realigned) & 63) == 0); for (int i = 0; i < 64; ++i) realigned[i] = static_cast(i + 1); - 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..fb64d834ba --- /dev/null +++ b/core/libs/tests/NetBlockingSend.cpp @@ -0,0 +1,146 @@ +#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; + 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 < 16; ++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/ParamJsonParser.cpp b/core/libs/tests/ParamJsonParser.cpp index 653fb6745d..8ec7173a5a 100644 --- a/core/libs/tests/ParamJsonParser.cpp +++ b/core/libs/tests/ParamJsonParser.cpp @@ -1,5 +1,6 @@ #include "prx/libkernel/AppMetadata/include/ParamJsonParser.hpp" #include +#include #include #include #include @@ -33,6 +34,37 @@ static ParsedParamJson ParseText(const std::filesystem::path& path, const std::s return parseParamJson(path); } +static void CheckNumericLocale(const std::filesystem::path& path) { + struct RestoreLocale { + std::string previous = std::setlocale(LC_NUMERIC, nullptr); + ~RestoreLocale() { std::setlocale(LC_NUMERIC, previous.c_str()); } + } restore; + bool commaLocale = false; + for (const auto* name : {"French", "French_France.1252", "fr_FR.UTF-8", "de_DE.UTF-8"}) { + if (std::setlocale(LC_NUMERIC, name) != nullptr && std::string(std::localeconv()->decimal_point) == ",") { + commaLocale = true; + break; + } + } + if (!commaLocale) { +#ifdef _WIN32 + throw std::runtime_error("No Windows comma-decimal locale available for regression"); +#else + std::cout << "Comma-decimal locale regression unavailable on this host\n"; + return; +#endif + } + for (const auto& [number, expected] : {std::pair{"1.25e3", std::uint64_t{1250}}, + std::pair{"0.5e3", std::uint64_t{500}}, std::pair{"12.75e2", std::uint64_t{1275}}, + std::pair{"1.25", std::uint64_t{1}}}) { + Require(ParseSize(path, number).downloadDataSizeMiB == expected, + std::string("JSON number depends on host locale: ") + number); + } + bool rejected = false; + try { ParseSize(path, "-0.5"); } catch (const std::exception&) { rejected = true; } + Require(rejected, "Negative fractional size was accepted under comma locale"); +} + static void CheckUserDefinedParams(const std::filesystem::path& path) { const auto parsed = ParseText(path, R"({"titleId":"PPSA23566","localizedParameters":{"en-US":{"titleName":"Example"}},)" R"("userDefinedParam1":23566,"userDefinedParam3":-7,"userDefinedParam4":2147483647})"); @@ -49,6 +81,52 @@ 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) { + return prefix + std::string(arrays, '[') + std::string(arrays, ']') + "}"; + }; + Require(ParseText(path, nested(511)).title == "Example", "Nesting within the limit rejected"); + for (const std::size_t arrays : {std::size_t{512}, std::size_t{513}, std::size_t{100000}}) { + bool rejected = false; + try { + ParseText(path, nested(arrays)); + } catch (const std::runtime_error& error) { + rejected = std::string(error.what()).find("nested too deeply") != std::string::npos; + } + Require(rejected, "Excessive nesting not rejected: " + std::to_string(arrays)); + } +} + +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", "-16384", "1", "16385", "838860800.0", "8e8", "true", "null", "\"838860800\"", "18446744073709551616"}) { + bool rejected = false; + try { ParseText(path, prefix + R"(,"kernel":{"flexibleMemorySize":)" + value + "}}"); } + catch (const std::runtime_error&) { rejected = true; } + Require(rejected, std::string("Accepted invalid flexible memory size: ") + value); + } + for (const auto* value : {"null", "true", "[]", "1", "\"kernel\""}) { + bool rejected = false; + try { ParseText(path, prefix + R"(,"kernel":)" + value + "}"); } + catch (const std::runtime_error&) { rejected = true; } + Require(rejected, "Accepted a non-object kernel"); + } +} + static void CheckLanguages(const std::filesystem::path& path) { const auto nested = ParseText(path, "{\r\n \"localizedParameters\": {\r\n \"defaultLanguage\": \"en-GB\",\r\n" " \"en-GB\": { \"titleName\": \"British\" }\r\n },\r\n \"titleId\": \"PPSA00001\"\r\n}\r\n"); @@ -71,6 +149,19 @@ static void CheckLanguages(const std::filesystem::path& path) { Require(rejected, "Accepted localizedParameters without a language entry"); } +static void CheckOutputPermission(const std::filesystem::path& path) { + const std::string prefix = R"({"titleId":"PPSA99007","localizedParameters":{"en-US":{"titleName":"Example"}})"; + Require(ParseText(path, prefix + "}").attribute3 == 0, "Absent output permission is not zero"); + for (const auto& [text, expected] : {std::pair{"0", 0u}, {"524352", 0x80040u}, {"4294967295", 0xFFFFFFFFu}}) { + Require(ParseText(path, prefix + ",\"attribute3\":" + text + "}").attribute3 == expected, "Incorrect output permission"); + } + for (const auto* text : {"-1", "0.5", "1e3", "4294967296", "true", "null", "\"524352\""}) { + bool rejected = false; + try { ParseText(path, prefix + ",\"attribute3\":" + text + "}"); } catch (const std::exception&) { rejected = true; } + Require(rejected, "Accepted invalid attribute3"); + } +} + static void CheckNumberSyntax(const std::filesystem::path& path) { const std::string prefix = R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":"Example"}})"; const std::pair fields[] = { @@ -90,11 +181,36 @@ static void CheckNumberSyntax(const std::filesystem::path& path) { } } +static void CheckUtf8(const std::filesystem::path& path) { + const std::string prefix = R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":")"; + const std::string suffix = R"("}}})"; + for (const auto& title : {std::string("ASCII"), std::string("\xC2\x80"), std::string("\xE3\x82\xB2\xE3\x83\xBC\xE3\x83\xA0"), + std::string("\xF0\x9F\x8E\xAE"), std::string("\xF4\x8F\xBF\xBF")}) { + Require(ParseText(path, prefix + title + suffix).title == title, "Valid UTF-8 title changed"); + } + Require(ParseText(path, prefix + R"(\uD83C\uDFAE)" + suffix).title == "\xF0\x9F\x8E\xAE", "Escaped title changed"); + for (const auto& title : {std::string("\x80"), std::string("\xC0\xAF"), std::string("\xC1\xBF"), std::string("\xC2"), + std::string("\xE0\x80\xAF"), std::string("\xED\xA0\x80"), std::string("\xE3\x82"), + std::string("\xF0\x80\x80\xAF"), std::string("\xF4\x90\x80\x80"), std::string("\xF5\x80\x80\x80"), + std::string("\xFE"), std::string("\xFF"), std::string("\xC2x"), std::string("\xE3\x82x")}) { + bool rejected = false; + try { ParseText(path, prefix + title + suffix); } catch (const std::exception&) { rejected = true; } + Require(rejected, "Accepted malformed UTF-8 title"); + } + bool rejected = false; + try { ParseText(path, std::string("{\"") + "\x80" + R"(":0,)" + prefix.substr(1) + "Valid" + suffix); } + catch (const std::exception&) { rejected = true; } + Require(rejected, "Accepted malformed UTF-8 object key"); +} + int main() { const auto path = std::filesystem::temp_directory_path() / ("anyps5-param-json-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".json"); int result = 0; try { + CheckNumericLocale(path); + CheckUtf8(path); + CheckFlexibleMemorySize(path); CheckNumberSyntax(path); const std::pair sizes[] = { {"", 0}, {"0", 0}, {"-0", 0}, {"0.25", 0}, {"0e3", 0}, {"0E+3", 0}, {"-0e-3", 0}, {"4096", 4096}, @@ -116,6 +232,9 @@ int main() { } CheckLanguages(path); CheckUserDefinedParams(path); + CheckContentVersion(path); + CheckOutputPermission(path); + CheckNesting(path); } catch (const std::exception& error) { std::cerr << error.what() << '\n'; result = 1; 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 8815d9225f..c4c02e81fa 100644 --- a/core/libs/tests/PixelAncillary.cpp +++ b/core/libs/tests/PixelAncillary.cpp @@ -1,7 +1,9 @@ #include "IntermediateRepresentation/IrBuilder.hpp" #include "Optimization/ConstantFolder.hpp" #include "Optimization/DeadCodeEliminator.hpp" +#include "Optimization/MaskedSelectEliminator.hpp" #include "Optimization/ShaderInfoCollector.hpp" +#include "Recompiler.hpp" #include #include #include @@ -12,16 +14,35 @@ 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; std::uint32_t layer; bool frontFacing; + RasterPrimitive primitive = RasterPrimitive::Triangle; + bool layerExported = false; }; -constexpr std::array Rows {{{0u, 0u, false}, {0u, 0u, true}, {15u, 0x1FFFu, false}, {15u, 0x1FFFu, true}, {0u, 0x1FFFu, true}, {15u, 0u, false}}}; +constexpr std::array Rows {{{0u, 0u, false}, {0u, 0u, true}, {15u, 0x1FFFu, false}, {15u, 0x1FFFu, true}, {0u, 0x1FFFu, true}, {15u, 0u, false}, + {0u, 0x1FFFu, false, RasterPrimitive::Triangle, true}, {15u, 0x1FFFu, true, RasterPrimitive::Triangle, true}, {15u, 3u, false, RasterPrimitive::Triangle, true}, {0u, 0u, true, RasterPrimitive::Triangle, true}}}; + +static std::uint32_t Evaluate(const IrValue* value, const Row& row); +static std::uint32_t BitField(std::uint32_t word, std::uint32_t offset, std::uint32_t count, bool isSigned); +static void RequireField(const IrValue& user, StageInputKind kind, std::uint32_t fieldOffset, std::uint32_t count) { + if (kind == StageInputKind::Layer) { + for (const Row& row : Rows) Require(Evaluate(&user, row) == (row.layerExported ? BitField(row.layer, fieldOffset, count, user.Opcode() == IrOpcode::BitFieldSExtract) : 0u)); + return; + } + const IrValue* field = user.Argument(0)->Resolve(); + Require(field->Opcode() == IrOpcode::GetBuiltin); + Require(static_cast(field->Argument(0)->Resolve()->ImmediateU32()) == kind); + Require(user.Argument(1)->Resolve()->ImmediateU32() == fieldOffset); + Require(user.Argument(2)->Resolve()->ImmediateU32() == count); +} static IrBlock& Begin(IrProgram& program) { program.Resources().stage = IrShaderStage::Pixel; + program.Metadata().pixelIteratesSamples = Iterated; program.Resources().resourceTrackingComplete = true; auto& block = program.CreateBlock(); program.SetEntryBlock(block); @@ -59,15 +80,58 @@ static void Lower(IrProgram& program) { const ShaderPixelInputInfo pixel {}; ShaderInfoCollector().Collect(program, ShaderStageInputInfo {nullptr, &pixel, nullptr}); } +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); + program.BlockOrder().push_back(&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& maskedOffset = builder.BitwiseAnd(builder.Constant(offset), builder.Constant(31u)); + auto& maskedCount = builder.BitwiseAnd(builder.Constant(count), builder.Constant(31u)); + auto& available = builder.ISub(builder.Constant(32u), maskedOffset); + auto& clampedCount = builder.Emit(IrOpcode::UMin32, IrType::U32, {&maskedCount, &available}); + auto& user = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&ancillary, &maskedOffset, &clampedCount}); + static_cast(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&user})); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + Lower(program); + RequireField(user, kind, fieldOffset, count); +} + +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); + program.BlockOrder().push_back(&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& maskedOffset = builder.BitwiseAnd(builder.Constant(offset), builder.Constant(31u)); + auto& maskedCount = builder.BitwiseAnd(builder.Constant(count), builder.Constant(31u)); + auto& available = builder.ISub(builder.Constant(32u), maskedOffset); + auto& clampedCount = builder.Emit(IrOpcode::UMin32, IrType::U32, {&maskedCount, &available}); + auto& user = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&ancillary, &maskedOffset, &clampedCount}); + static_cast(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&user})); + auto& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + auto& active = builder.Emit(IrOpcode::ULessThan32, IrType::Bool, {&lane, &builder.Constant(16u)}); + auto& copy = builder.Emit(IrOpcode::SelectU32, IrType::U32, {&active, &ancillary, &builder.Constant(0u)}); + static_cast(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {©})); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + ConstantFolder().Fold(program); + RequireField(user, kind, fieldOffset, count); +} static void Extract(IrOpcode opcode, std::uint32_t offset, std::uint32_t count, StageInputKind kind, std::uint32_t fieldOffset) { IrProgram program; auto& user = Build(program, opcode, offset, count); Lower(program); - const IrValue* field = user.Argument(0)->Resolve(); - Require(field->Opcode() == IrOpcode::GetBuiltin); - Require(static_cast(field->Argument(0)->Resolve()->ImmediateU32()) == kind); - Require(user.Argument(1)->Resolve()->ImmediateU32() == fieldOffset); - Require(user.Argument(2)->Resolve()->ImmediateU32() == count); + RequireField(user, kind, fieldOffset, count); } static void Refused(IrOpcode opcode, std::uint32_t offset, std::uint32_t count, bool throughSelect = false) { IrProgram program; @@ -99,21 +163,25 @@ static std::uint32_t Evaluate(const IrValue* value, const Row& row) { case StageInputKind::SampleId: return row.sample; case StageInputKind::Layer: return row.layer; case StageInputKind::FrontFacing: return row.frontFacing ? 1u : 0u; + case StageInputKind::RasterPrimitive: return static_cast(row.primitive); + case StageInputKind::LayerExported: return row.layerExported ? 1u : 0u; default: break; } break; case IrOpcode::ShiftLeftLogical32: return Evaluate(value->Argument(0), row) << Evaluate(value->Argument(1), row); case IrOpcode::BitwiseOr32: return Evaluate(value->Argument(0), row) | Evaluate(value->Argument(1), row); + case IrOpcode::IMul32: return Evaluate(value->Argument(0), row) * Evaluate(value->Argument(1), row); case IrOpcode::BitFieldUExtract: case IrOpcode::BitFieldSExtract: return BitField(Evaluate(value->Argument(0), row), Evaluate(value->Argument(1), row), Evaluate(value->Argument(2), row), value->Opcode() == IrOpcode::BitFieldSExtract); case IrOpcode::SelectU32: return Evaluate(value->Argument(0), row) != 0u ? Evaluate(value->Argument(1), row) : Evaluate(value->Argument(2), row); + case IrOpcode::Phi: return Evaluate(value->Argument(row.frontFacing ? 0u : 1u), row); default: break; } throw std::runtime_error("packed pixel ancillary evaluation reached an unsupported value"); } static std::uint32_t PackedWord(const Row& row) { - return UnmodelledBits | (row.sample << 8u) | (row.layer << 16u); + return UnmodelledBits | ((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; @@ -135,15 +203,378 @@ static void LateFold() { IrValue& user = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&ancillary, &offset, &count}); End(builder, user); Lower(program); - const IrValue* field = user.Argument(0)->Resolve(); - Require(field->Opcode() == IrOpcode::GetBuiltin); - Require(static_cast(field->Argument(0)->Resolve()->ImmediateU32()) == StageInputKind::Layer); - Require(user.Argument(1)->Resolve()->ImmediateU32() == 0u); - Require(user.Argument(2)->Resolve()->ImmediateU32() == 11u); + RequireField(user, StageInputKind::Layer, 0u, 11u); for (const Row& row : Rows) { Require(Evaluate(&user, row) == BitField(PackedWord(row), 16u, 11u, false)); } } +static IrValue& Merged(IrProgram& program, IrOpcode opcode, std::uint32_t offset, std::uint32_t count, bool loop) { + auto& entry = Begin(program); + auto& carry = program.CreateBlock(); + auto& merge = program.CreateBlock(); + program.BlockOrder().push_back(&carry); + program.BlockOrder().push_back(&merge); + IrBuilder builder(program); + builder.SetInsertionPoint(entry); + IrValue& ancillary = AncillaryBuiltin(builder); + IrValue& frontFacing = FrontFacingBuiltin(builder); + IrValue& phi = program.CreateValue(IrOpcode::Phi, IrType::U32); + IrValue* other = &builder.Constant(SelectOther); + if (loop) { + entry.AddBranch(&carry); + carry.AddBranch(&carry); + carry.AddBranch(&merge); + carry.AppendInstruction(&phi); + builder.SetInsertionPoint(carry); + other = &builder.Emit(IrOpcode::SelectU32, IrType::U32, {&frontFacing, &phi, &builder.Constant(SelectOther)}); + phi.AddPhiOperand(&entry, &ancillary); + phi.AddPhiOperand(&carry, other); + } else { + entry.AddBranch(&carry); + entry.AddBranch(&merge); + carry.AddBranch(&merge); + merge.AppendInstruction(&phi); + phi.AddPhiOperand(&entry, &ancillary); + phi.AddPhiOperand(&carry, other); + } + builder.SetInsertionPoint(merge); + IrValue& user = opcode == IrOpcode::BitwiseOr32 ? builder.Emit(opcode, IrType::U32, {&phi, &builder.Constant(offset)}) + : builder.Emit(opcode, IrType::U32, {&phi, &builder.Constant(offset), &builder.Constant(count)}); + End(builder, user); + return user; +} +static void MergedValues(IrOpcode opcode, std::uint32_t offset, std::uint32_t count, bool loop) { + IrProgram program; + auto& user = Merged(program, opcode, offset, count, loop); + Lower(program); + for (const Row& row : Rows) { + const std::uint32_t word = row.frontFacing ? PackedWord(row) : SelectOther; + Require(Evaluate(&user, row) == BitField(word, offset, count, opcode == IrOpcode::BitFieldSExtract)); + } +} +static void MergedRefused(IrOpcode opcode, std::uint32_t offset, std::uint32_t count, bool loop) { + IrProgram program; + static_cast(Merged(program, opcode, offset, count, loop)); + try { + Lower(program); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find("unsupported live use") != std::string::npos); + return; + } + Require(false); +} +enum class HelperRead { Export, FullWaveExport, DataTestExport, Ballot }; + +struct HelperProgram { + IrValue* written = nullptr; + IrValue* layer = nullptr; + IrValue* exported = nullptr; +}; + +static HelperProgram HelperOnly(IrProgram& program, HelperRead read, std::uint32_t chain = 0u) { + auto& entry = Begin(program); + auto& body = program.CreateBlock(); + auto& merge = program.CreateBlock(); + program.BlockOrder().push_back(&body); + program.BlockOrder().push_back(&merge); + entry.AddBranch(&body); + entry.AddBranch(&merge); + body.AddBranch(&merge); + IrBuilder builder(program); + builder.SetInsertionPoint(entry); + IrValue& helper = builder.Emit(IrOpcode::GetBuiltin, IrType::U32, {&builder.Constant(static_cast(StageInputKind::HelperInvocation)), &builder.Constant(0u)}); + IrValue& live = builder.IEqual(helper, builder.Constant(0u)); + IrValue& ancillary = AncillaryBuiltin(builder); + IrValue& layer = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&ancillary, &builder.Constant(16u), &builder.Constant(11u)}); + IrValue& target = builder.Select(live, layer, builder.Constant(0u)); + IrValue& tested = builder.LogicalAnd(live, FrontFacingBuiltin(builder)); + IrValue& written = builder.Select(read == HelperRead::DataTestExport ? tested : live, builder.Constant(SelectOther), ancillary); + builder.SetInsertionPoint(body); + IrValue& squared = builder.Emit(IrOpcode::FPMul32, IrType::F32, {&builder.BitCastF32(written), &builder.BitCastF32(written)}); + IrValue& killed = builder.Select(tested, builder.BitCastU32(squared), written); + IrValue& exec = program.CreateValue(IrOpcode::Phi, IrType::Bool); + IrValue& value = program.CreateValue(IrOpcode::Phi, IrType::U32); + merge.AppendInstruction(&exec); + merge.AppendInstruction(&value); + exec.AddPhiOperand(&entry, &live); + exec.AddPhiOperand(&body, &tested); + value.AddPhiOperand(&entry, &written); + value.AddPhiOperand(&body, &killed); + builder.SetInsertionPoint(merge); + IrValue* shaded = &value; + for (std::uint32_t step = 0; step < chain; ++step) shaded = &builder.IAdd(*shaded, helper); + IrValue& data = builder.Emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {shaded, &target, &builder.Constant(0u), &builder.Constant(0u)}); + program.Metadata().exportInfo.push_back(ExportInfo {.kind = ExportTargetKind::Mrt, .index = 0u, .en = 0xFu, .done = true, .vm = true}); + IrValue& guard = read == HelperRead::FullWaveExport ? builder.ConstantBool(true) : exec; + static_cast(builder.Emit(IrOpcode::SetAttribute, IrType::Void, {&data, &guard}, ExportFlags {0u, 0u})); + if (read == HelperRead::Ballot) { + IrValue& ballot = builder.Emit(IrOpcode::Ballot, IrType::U32x4, {&builder.INotEqual(value, builder.Constant(0u))}); + static_cast(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&builder.CompositeExtract(ballot, 0u)})); + } + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + return HelperProgram {&written, &layer, &value}; +} +static void LowerMasked(IrProgram& program, bool compose = false) { + ConstantFolder().Fold(program); + DeadCodeEliminator().RemoveIdentities(program); + DeadCodeEliminator().Eliminate(program); + static_cast(MaskedSelectEliminator().Eliminate(program)); + DeadCodeEliminator().Eliminate(program); + if (compose && ConstantFolder().ComposePackedAncillary(program)) DeadCodeEliminator().Eliminate(program); + const ShaderPixelInputInfo pixel {}; + ShaderInfoCollector().Collect(program, ShaderStageInputInfo {nullptr, &pixel, nullptr}); +} +static void HelperLanesOnly(std::uint32_t chain = 0u) { + IrProgram program; + const HelperProgram built = HelperOnly(program, HelperRead::Export, chain); + LowerMasked(program, true); + Require(!program.Info().rasterPrimitive); + for (const IrBlock* block : program.BlockOrder()) { + for (const IrValue* inst : block->Instructions()) { + Require(inst->Opcode() != IrOpcode::GetBuiltin || static_cast(inst->Argument(0)->Resolve()->ImmediateU32()) != StageInputKind::PackedAncillary); + } + } + Require(built.written->Parent() == nullptr); + RequireField(*built.layer, StageInputKind::Layer, 0u, 11u); + const IrValue* entryValue = built.exported->Argument(0)->Resolve(); + Require(entryValue->HasImmediate() && entryValue->ImmediateU32() == SelectOther); +} +static void HelperLanesRefused(HelperRead read, std::uint32_t chain = 0u) { + IrProgram program; + static_cast(HelperOnly(program, read, chain)); + try { + LowerMasked(program); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find("unsupported live use") != std::string::npos); + return; + } + Require(false); +} +enum class LoopRead { ExportAfter, ExportInside, UniformLoop, RevivedExportInside }; + +static IrValue& LaneBit(IrBuilder& builder, IrValue& low, IrValue& high) { + IrValue& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + IrValue& word = builder.Select(builder.Emit(IrOpcode::ULessThan32, IrType::Bool, {&lane, &builder.Constant(32u)}), low, high); + IrValue& shifted = builder.Emit(IrOpcode::ShiftRightLogical32, IrType::U32, {&word, &builder.BitwiseAnd(lane, builder.Constant(31u))}); + return builder.INotEqual(builder.BitwiseAnd(shifted, builder.Constant(1u)), builder.Constant(0u)); +} +static HelperProgram HelperLoop(IrProgram& program, LoopRead read) { + auto& entry = Begin(program); + auto& header = program.CreateBlock(); + auto& exit = program.CreateBlock(); + program.BlockOrder().push_back(&header); + program.BlockOrder().push_back(&exit); + entry.AddBranch(&header); + header.AddBranch(&header); + header.AddBranch(&exit); + IrBuilder builder(program); + builder.SetInsertionPoint(entry); + IrValue& helper = builder.Emit(IrOpcode::GetBuiltin, IrType::U32, {&builder.Constant(static_cast(StageInputKind::HelperInvocation)), &builder.Constant(0u)}); + IrValue& live = builder.IEqual(helper, builder.Constant(0u)); + IrValue& ancillary = AncillaryBuiltin(builder); + IrValue& layer = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&ancillary, &builder.Constant(16u), &builder.Constant(11u)}); + IrValue& target = builder.Select(live, layer, builder.Constant(0u)); + IrValue& written = builder.Select(live, builder.Constant(SelectOther), ancillary); + IrValue& frontFacing = FrontFacingBuiltin(builder); + IrValue& saved = builder.Emit(IrOpcode::Ballot, IrType::U32x4, {&live}); + IrValue& savedLow = builder.CompositeExtract(saved, 0u); + IrValue& savedHigh = builder.CompositeExtract(saved, 1u); + IrValue& low = program.CreateValue(IrOpcode::Phi, IrType::U32); + IrValue& high = program.CreateValue(IrOpcode::Phi, IrType::U32); + IrValue& value = program.CreateValue(IrOpcode::Phi, IrType::U32); + header.AppendInstruction(&low); + header.AppendInstruction(&high); + header.AppendInstruction(&value); + low.AddPhiOperand(&entry, &savedLow); + high.AddPhiOperand(&entry, &savedHigh); + value.AddPhiOperand(&entry, &written); + builder.SetInsertionPoint(header); + IrValue& loopExec = read == LoopRead::UniformLoop ? live : LaneBit(builder, low, high); + IrValue& squared = builder.Emit(IrOpcode::FPMul32, IrType::F32, {&builder.BitCastF32(value), &builder.BitCastF32(value)}); + IrValue& next = builder.Select(loopExec, builder.BitCastU32(squared), value); + IrValue& stay = builder.Emit(IrOpcode::Ballot, IrType::U32x4, {&frontFacing}); + IrValue& stayLow = builder.CompositeExtract(stay, 0u); + IrValue& stayHigh = builder.CompositeExtract(stay, 1u); + const bool revived = read == LoopRead::RevivedExportInside; + low.AddPhiOperand(&header, revived ? &stayLow : &builder.BitwiseAnd(low, stayLow)); + high.AddPhiOperand(&header, revived ? &stayHigh : &builder.BitwiseAnd(high, stayHigh)); + value.AddPhiOperand(&header, &next); + program.Metadata().exportInfo.push_back(ExportInfo {.kind = ExportTargetKind::Mrt, .index = 0u, .en = 0xFu, .done = true, .vm = true}); + const auto exportValue = [&](IrValue& guard) { + IrValue& data = builder.Emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&value, &target, &builder.Constant(0u), &builder.Constant(0u)}); + static_cast(builder.Emit(IrOpcode::SetAttribute, IrType::Void, {&data, &guard}, ExportFlags {0u, 0u})); + }; + if (read == LoopRead::ExportInside || revived) exportValue(loopExec); + builder.SetInsertionPoint(exit); + if (read == LoopRead::ExportAfter) exportValue(LaneBit(builder, savedLow, savedHigh)); + if (read == LoopRead::UniformLoop) exportValue(live); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + return HelperProgram {&written, &layer, &value}; +} +static void HelperLoopOnly(LoopRead read) { + IrProgram program; + const HelperProgram built = HelperLoop(program, read); + LowerMasked(program); + for (const IrBlock* block : program.BlockOrder()) { + for (const IrValue* inst : block->Instructions()) { + Require(inst->Opcode() != IrOpcode::GetBuiltin || static_cast(inst->Argument(0)->Resolve()->ImmediateU32()) != StageInputKind::PackedAncillary); + } + } + Require(built.written->Parent() == nullptr); + RequireField(*built.layer, StageInputKind::Layer, 0u, 11u); + const IrValue* entryValue = built.exported->Argument(0)->Resolve(); + Require(entryValue->HasImmediate() && entryValue->ImmediateU32() == SelectOther); +} +static void HelperLoopRefused(LoopRead read) { + IrProgram program; + static_cast(HelperLoop(program, read)); + try { + LowerMasked(program); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find("unsupported live use") != std::string::npos); + return; + } + Require(false); +} +static bool BuiltinRemains(const IrProgram& program, StageInputKind kind) { + for (const IrBlock* block : program.BlockOrder()) { + for (const IrValue* inst : block->Instructions()) { + if (inst->Opcode() == IrOpcode::GetBuiltin && static_cast(inst->Argument(0)->Resolve()->ImmediateU32()) == kind) return true; + } + } + return false; +} +static bool AncillaryRemains(const IrProgram& program) { + return BuiltinRemains(program, StageInputKind::PackedAncillary); +} +static void 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); + auto& body = program.CreateBlock(); + auto& merge = program.CreateBlock(); + program.BlockOrder().push_back(&body); + program.BlockOrder().push_back(&merge); + entry.AddBranch(&body); + entry.AddBranch(&merge); + body.AddBranch(&merge); + IrBuilder builder(program); + builder.SetInsertionPoint(entry); + IrValue& helper = builder.Emit(IrOpcode::GetBuiltin, IrType::U32, {&builder.Constant(static_cast(StageInputKind::HelperInvocation)), &builder.Constant(0u)}); + IrValue& live = builder.IEqual(helper, builder.Constant(0u)); + IrValue& ancillary = AncillaryBuiltin(builder); + IrValue& tested = builder.INotEqual(builder.Select(FrontFacingBuiltin(builder), builder.Constant(7u), builder.Constant(0u)), builder.Constant(0u)); + builder.SetInsertionPoint(body); + IrValue& written = builder.Select(live, builder.Constant(SelectOther), ancillary); + IrValue& carried = program.CreateValue(IrOpcode::Phi, IrType::U32); + merge.AppendInstruction(&carried); + carried.AddPhiOperand(&entry, &ancillary); + carried.AddPhiOperand(&body, &written); + builder.SetInsertionPoint(merge); + const auto channel = [&](float value) -> IrValue& { + IrValue& half = builder.ConstantF32(value); + return builder.Select(tested, builder.Emit(IrOpcode::PackFloat2x16Rtz, IrType::U32, {&half, &half}), carried); + }; + IrValue& data = builder.Emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&channel(0.25f), &channel(0.75f), &builder.Constant(0u), &builder.Constant(0u)}); + program.Metadata().exportInfo.push_back(ExportInfo {.kind = ExportTargetKind::Mrt, .index = 0u, .en = 0xFu, .done = true, .vm = true}); + static_cast(builder.Emit(IrOpcode::SetAttribute, IrType::Void, {&data, &live}, ExportFlags {0u, 0u})); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + LowerMasked(program, true); + Require(!AncillaryRemains(program) && program.Info().rasterPrimitive); + Require(BuiltinRemains(program, StageInputKind::LayerExported) && 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) | ((Iterated ? row.sample : 0u) << 8u) | (row.layerExported ? row.layer << 16u : 0u))); + } +} +static void WholeWordReplacesRefusal(HelperRead read) { + IrProgram program; + const HelperProgram built = HelperOnly(program, read); + LowerMasked(program, true); + Require(!AncillaryRemains(program) && program.Info().rasterPrimitive); + Require(built.written->Parent() != nullptr); + const Row row {9u, 0x123u, false, RasterPrimitive::Rectangle}; + Require(Evaluate(built.written->Argument(2), row) == (3u | (9u << 8u))); +} +enum class StartingQuad { Kept, BallotAfterBranch, BallotOfOtherLanes, ComputeStage }; + +static IrValue& StartingQuadMasked(IrProgram& program, StartingQuad shape) { + auto& entry = Begin(program); + if (shape == StartingQuad::ComputeStage) program.Resources().stage = IrShaderStage::Compute; + IrBlock* tail = &entry; + if (shape == StartingQuad::BallotAfterBranch) { + auto& body = program.CreateBlock(); + auto& merge = program.CreateBlock(); + program.BlockOrder().push_back(&body); + program.BlockOrder().push_back(&merge); + entry.AddBranch(&body); + entry.AddBranch(&merge); + body.AddBranch(&merge); + tail = &merge; + } + IrBuilder builder(program); + builder.SetInsertionPoint(entry); + IrValue& ancillary = AncillaryBuiltin(builder); + IrValue& helper = builder.Emit(IrOpcode::GetBuiltin, IrType::U32, {&builder.Constant(static_cast(StageInputKind::HelperInvocation)), &builder.Constant(0u)}); + IrValue& started = shape == StartingQuad::BallotOfOtherLanes ? FrontFacingBuiltin(builder) : builder.IEqual(helper, builder.Constant(0u)); + IrValue& live = builder.Emit(IrOpcode::Ballot, IrType::Vec4U32, {&started}); + builder.SetInsertionPoint(*tail); + IrValue& active = builder.Emit(IrOpcode::Ballot, IrType::Vec4U32, {&builder.ConstantBool(true)}); + IrValue& low = builder.BitwiseAnd(builder.CompositeExtract(live, 0u), builder.CompositeExtract(active, 0u)); + IrValue& high = builder.BitwiseAnd(builder.CompositeExtract(live, 1u), builder.CompositeExtract(active, 1u)); + IrValue& quads = builder.Emit(IrOpcode::WqmU64, IrType::U64, {&builder.Emit(IrOpcode::CompositeConstructU64, IrType::U64, {&low, &high})}); + IrValue& lane = LaneBit(builder, builder.Emit(IrOpcode::CompositeExtractU64, IrType::U32, {&quads, &builder.Constant(0u)}), builder.Emit(IrOpcode::CompositeExtractU64, IrType::U32, {&quads, &builder.Constant(1u)})); + IrValue& kept = builder.Select(lane, builder.Constant(SelectOther), ancillary); + End(builder, kept); + return kept; +} +static const IrValue* StartingQuadExported(IrProgram& program, StartingQuad shape) { + static_cast(StartingQuadMasked(program, shape)); + if (shape == StartingQuad::ComputeStage) { + ConstantFolder().Fold(program); + DeadCodeEliminator().Eliminate(program); + } else { + Lower(program); + } + for (const IrBlock* block : program.BlockOrder()) { + for (const IrValue* inst : block->Instructions()) { + if (inst->Opcode() == IrOpcode::ReferenceU32) return inst->Argument(0)->Resolve(); + } + } + return nullptr; +} +static void StartingQuadOnly() { + IrProgram program; + const IrValue* value = StartingQuadExported(program, StartingQuad::Kept); + Require(value != nullptr && value->HasImmediate() && value->ImmediateU32() == SelectOther); +} +static void StartingQuadOutsidePixel() { + IrProgram program; + const IrValue* value = StartingQuadExported(program, StartingQuad::ComputeStage); + Require(value != nullptr && value->Opcode() == IrOpcode::SelectU32); +} +static void StartingQuadRefused(StartingQuad shape) { + IrProgram program; + static_cast(StartingQuadMasked(program, shape)); + try { + Lower(program); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find("unsupported live use") != std::string::npos); + return; + } + Require(false); +} int main() { Extract(IrOpcode::BitFieldUExtract, 8u, 4u, StageInputKind::SampleId, 0u); Extract(IrOpcode::BitFieldUExtract, 9u, 2u, StageInputKind::SampleId, 1u); @@ -159,6 +590,10 @@ int main() { Values(IrOpcode::BitFieldUExtract, 8u, 4u, true); Values(IrOpcode::BitFieldUExtract, 16u, 13u, true); Values(IrOpcode::BitFieldSExtract, 20u, 9u, true); + ExtractVector(16u, 11u, StageInputKind::Layer, 0u); + ExtractVectorMaskedCopy(16u, 11u, StageInputKind::Layer, 0u); + ExtractVectorMaskedCopy(8u, 4u, StageInputKind::SampleId, 0u); + ExtractVector(8u, 4u, StageInputKind::SampleId, 0u); Refused(IrOpcode::BitwiseOr32, 1u, 0u); Refused(IrOpcode::BitwiseOr32, 1u, 0u, true); Refused(IrOpcode::BitFieldUExtract, 2u, 4u); @@ -172,4 +607,37 @@ int main() { Refused(IrOpcode::BitFieldUExtract, 8u, 0u); Refused(IrOpcode::BitFieldUExtract, 0u, 2u, true); Refused(IrOpcode::BitFieldSExtract, 12u, 4u, true); + MergedValues(IrOpcode::BitFieldUExtract, 8u, 4u, false); + MergedValues(IrOpcode::BitFieldUExtract, 16u, 13u, false); + MergedValues(IrOpcode::BitFieldSExtract, 20u, 9u, false); + MergedValues(IrOpcode::BitFieldUExtract, 16u, 11u, true); + MergedRefused(IrOpcode::BitwiseOr32, 1u, 0u, false); + MergedRefused(IrOpcode::BitFieldUExtract, 12u, 4u, false); + MergedRefused(IrOpcode::BitFieldUExtract, 0u, 2u, true); + HelperLanesOnly(); + HelperLanesOnly(6000u); + HelperLanesRefused(HelperRead::FullWaveExport); + HelperLanesRefused(HelperRead::DataTestExport); + HelperLanesRefused(HelperRead::Ballot); + HelperLanesRefused(HelperRead::FullWaveExport, 6000u); + HelperLoopOnly(LoopRead::ExportAfter); + HelperLoopOnly(LoopRead::ExportInside); + HelperLoopOnly(LoopRead::UniformLoop); + HelperLoopRefused(LoopRead::RevivedExportInside); + ExportedWholeWord(); + WholeWordReplacesRefusal(HelperRead::FullWaveExport); + WholeWordReplacesRefusal(HelperRead::DataTestExport); + WholeWordReplacesRefusal(HelperRead::Ballot); + StartingQuadOnly(); + StartingQuadRefused(StartingQuad::BallotAfterBranch); + StartingQuadRefused(StartingQuad::BallotOfOtherLanes); + 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 new file mode 100644 index 0000000000..d9120b173c --- /dev/null +++ b/core/libs/tests/PreciseWait.cpp @@ -0,0 +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() { + 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/PthreadStart.cpp b/core/libs/tests/PthreadStart.cpp index 6aa3877a1f..8750d5eb5e 100644 --- a/core/libs/tests/PthreadStart.cpp +++ b/core/libs/tests/PthreadStart.cpp @@ -41,8 +41,7 @@ static void* APS5_VABI CheckThread(void* arg) { if (context.thread->threadId != std::this_thread::get_id() || !context.thread->nativeHandle) throw std::runtime_error("Native thread was not initialized"); #else - if (context.thread->_thr.get_id() != std::this_thread::get_id()) throw std::runtime_error("Thread object was not initialized"); - if (!context.thread->_thr.joinable()) throw std::runtime_error("Thread object is not joinable"); + if (!pthread_equal(context.thread->hostThread, pthread_self())) throw std::runtime_error("Thread object was not initialized"); #endif PthreadAttr attr = nullptr; if (scePthreadAttrInit(&attr) != 0) throw std::runtime_error("Attribute initialization failed"); diff --git a/core/libs/tests/RegisterContext.cpp b/core/libs/tests/RegisterContext.cpp index 28c22057c6..5c45536bd4 100644 --- a/core/libs/tests/RegisterContext.cpp +++ b/core/libs/tests/RegisterContext.cpp @@ -12,7 +12,7 @@ #endif static_assert(sizeof(_Unwind_Context::registers) == 17 * sizeof(std::uintptr_t)); -static_assert(offsetof(_Unwind_Context, cfa) == 17 * sizeof(std::uintptr_t)); +static_assert(offsetof(_Unwind_Context, cfa) == offsetof(_Unwind_Context, registers) + 17 * sizeof(std::uintptr_t)); extern "C" void CaptureProbe(std::uintptr_t*); extern "C" void RestoreProbe(std::uintptr_t*, std::uintptr_t*); diff --git a/core/libs/tests/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/SaveDataMemoryGrowth.cpp b/core/libs/tests/SaveDataMemoryGrowth.cpp index 95d5225a37..87d06e2a33 100644 --- a/core/libs/tests/SaveDataMemoryGrowth.cpp +++ b/core/libs/tests/SaveDataMemoryGrowth.cpp @@ -82,10 +82,10 @@ int main(int argc, char**) { std::fprintf(stderr, "Read denial unavailable for this user\n"); exitCode = 77; } else { - Require(Read(path) == original); Require(status == static_cast(0x809F000Bu)); + Require(Read(path) == original); Require(result.existed_memory_size == 123); - Require(!std::filesystem::exists("_sd_mem/u7531/slot0.param")); + Require(std::filesystem::is_regular_file("_sd_mem/u7531/slot0.param")); Require(!std::filesystem::exists(path.string() + ".tmp")); } } else { 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 70724bd18b..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,7 +28,15 @@ 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) { @@ -35,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())); @@ -51,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{}; @@ -61,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))); @@ -85,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); @@ -115,7 +166,186 @@ 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"); + Require(std::filesystem::create_directories(setupParamPath.parent_path())); + SaveDataParam setupParam{}; + setupParam.user_param = 100; + SaveDataMemorySetup2 setup2{}; + setup2.user_id = 42; + setup2.slot_id = 0; + setup2.memory_size = 128; + setup2.option = 1; + setup2.init_param = &setupParam; + SaveDataMemorySetupResult setupResult{}; + setupResult.existed_memory_size = 555; + 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(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == 0); + Require(setupResult.existed_memory_size == 0); + Require(Read(setupBinPath) == std::vector(128, 0)); + const auto* setupBytes = reinterpret_cast(&setupParam); + Require(Read(setupParamPath) == std::vector(setupBytes, setupBytes + sizeof(setupParam))); + std::vector seededBin(128); + for (std::size_t i = 0; i < seededBin.size(); ++i) { + seededBin[i] = static_cast(static_cast(i * 3 + 7)); + } + { + std::ofstream seededFile(setupBinPath, std::ios::binary); + seededFile.write(seededBin.data(), static_cast(seededBin.size())); + Require(static_cast(seededFile)); + } + SaveDataParam previousParam = setupParam; + setupParam.user_param = 200; + setup2.memory_size = 256; + setupResult.existed_memory_size = 999; + 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(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == 0); + Require(setupResult.existed_memory_size == 128); + std::vector expectedGrownBin = seededBin; + expectedGrownBin.resize(256, 0); + Require(Read(setupBinPath) == expectedGrownBin); + const auto* updatedBytes = reinterpret_cast(&setupParam); + Require(Read(setupParamPath) == std::vector(updatedBytes, updatedBytes + sizeof(setupParam))); + setupParam.user_param = 300; + constexpr std::size_t GrownMemorySize = 2 * sizeof(SaveDataParam); + setup2.memory_size = GrownMemorySize; + setupResult.existed_memory_size = 888; + 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(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == 0); + Require(setupResult.existed_memory_size == 256); + 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())); + SaveDataParam freshFailParam{}; + freshFailParam.user_param = 55; + SaveDataMemorySetup2 freshFailSetup{}; + freshFailSetup.user_id = 55; + freshFailSetup.slot_id = 0; + freshFailSetup.memory_size = 64; + freshFailSetup.option = 1; + freshFailSetup.init_param = &freshFailParam; + SaveDataMemorySetupResult freshFailResult{}; + freshFailResult.existed_memory_size = 777; + 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(sceSaveDataSetupSaveDataMemory2(&freshFailSetup, &freshFailResult) == 0); + Require(freshFailResult.existed_memory_size == 0); + Require(Read(freshFailBinPath) == std::vector(64, 0)); + Require(Read(freshFailParamPath) == std::vector(freshFailBytes, freshFailBytes + sizeof(freshFailParam))); + SaveDataParam noParam{}; + noParam.user_param = 5; + SaveDataMemorySetup2 noParamSetup{}; + SaveDataMemorySetupResult noParamResult{}; + noParamSetup.user_id = 56; + noParamSetup.slot_id = 0; + noParamSetup.memory_size = 16; + noParamSetup.option = 1; + noParamSetup.init_param = nullptr; + Require(sceSaveDataSetupSaveDataMemory2(&noParamSetup, &noParamResult) == 0); + Require(Read("_sd_mem/u56/slot0.bin") == std::vector(16, 0)); + Require(!std::filesystem::exists("_sd_mem/u56/slot0.param")); + noParamSetup.user_id = 57; + noParamSetup.option = 0; + noParamSetup.init_param = &noParam; + Require(sceSaveDataSetupSaveDataMemory2(&noParamSetup, &noParamResult) == 0); + Require(Read("_sd_mem/u57/slot0.bin") == std::vector(16, 0)); + Require(!std::filesystem::exists("_sd_mem/u57/slot0.param")); + SaveDataParam userA{}; + userA.user_param = 1; + SaveDataParam userB{}; + userB.user_param = 2; + SaveDataMemorySetup2 isoSetup{}; + SaveDataMemorySetupResult isoResult{}; + isoSetup.user_id = 60; + isoSetup.slot_id = 0; + isoSetup.memory_size = 8; + isoSetup.option = 1; + isoSetup.init_param = &userA; + Require(sceSaveDataSetupSaveDataMemory2(&isoSetup, &isoResult) == 0); + isoSetup.slot_id = 1; + isoSetup.init_param = &userB; + Require(sceSaveDataSetupSaveDataMemory2(&isoSetup, &isoResult) == 0); + const auto* userABytes = reinterpret_cast(&userA); + const auto* userBBytes = reinterpret_cast(&userB); + Require(Read("_sd_mem/u60/slot0.param") == std::vector(userABytes, userABytes + sizeof(userA))); + Require(Read("_sd_mem/u60/slot1.param") == std::vector(userBBytes, userBBytes + sizeof(userB))); + isoSetup.user_id = 61; + isoSetup.slot_id = 0; + Require(sceSaveDataSetupSaveDataMemory2(&isoSetup, &isoResult) == 0); + Require(Read("_sd_mem/u60/slot0.param") == std::vector(userABytes, userABytes + sizeof(userA))); + Require(Read("_sd_mem/u61/slot0.param") == std::vector(userBBytes, userBBytes + sizeof(userB))); + const auto orphanParamPath = std::filesystem::path("_sd_mem/u99/slot0.param"); + const auto orphanBinPath = std::filesystem::path("_sd_mem/u99/slot0.bin"); + Require(std::filesystem::create_directories(orphanBinPath.parent_path())); + const std::vector orphanBin(64, 'k'); + { + std::ofstream orphanFile(orphanBinPath, std::ios::binary); + orphanFile.write(orphanBin.data(), static_cast(orphanBin.size())); + Require(static_cast(orphanFile)); + } + SaveDataParam orphanParam{}; + orphanParam.user_param = 42; + SaveDataMemorySetup2 orphanSetup{}; + orphanSetup.user_id = 99; + orphanSetup.slot_id = 0; + orphanSetup.memory_size = 64; + orphanSetup.option = 1; + orphanSetup.init_param = &orphanParam; + SaveDataMemorySetupResult orphanResult{}; + orphanResult.existed_memory_size = 333; + Require(sceSaveDataSetupSaveDataMemory2(&orphanSetup, &orphanResult) == 0); + Require(orphanResult.existed_memory_size == 64); + Require(Read(orphanBinPath) == orphanBin); + const auto* orphanBytes = reinterpret_cast(&orphanParam); + Require(Read(orphanParamPath) == std::vector(orphanBytes, orphanBytes + sizeof(orphanParam))); Require(sceSaveDataTerminate() == 0); + 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 == 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 3058f65772..c4d38b87c4 100644 --- a/core/libs/tests/ScalarCall.cpp +++ b/core/libs/tests/ScalarCall.cpp @@ -6,10 +6,13 @@ #include "ControlFlow/GraphBuilder.hpp" #include #include +#include #include +#include #include #include #include +#include using namespace ShaderRecompiler; @@ -96,6 +99,276 @@ static void CheckCallAfterEnd() { static_cast(InstructionTranslator{}.Translate(decoded, cfg, options)); } +static std::vector UserDataCaller() { + return {0x7e020280u, 0xbe940383u, 0xbe8e0300u, 0xbe8f0301u, 0xbe8e210eu, 0xbe8e0302u, 0xbe8f0303u, 0xbe8e210eu, + 0x80948114u, 0xbf078014u, 0xbf85fff7u, 0xbf810000u, 0x30306c73u}; +} + +static std::vector UserDataCalls(const std::vector& code, std::uint32_t userDataCount = 8u) { + const SwappcInfo swappc{false, 0u, userDataCount}; + return FindUserDataCalls(RdnaInstructionDecoder{}.Decode(code), swappc); +} + +static void CheckUserDataCalls() { + const SwappcInfo swappc{false, 0u, 8u}; + const auto caller = UserDataCaller(); + const auto program = RdnaInstructionDecoder{}.Decode(caller); + const auto calls = FindUserDataCalls(program, swappc); + Require(calls.size() == 2u); + Require(calls[0].programCounter == 0x10u && calls[0].linkRegister == 14u && calls[0].userDataRegister == 0u); + Require(calls[1].programCounter == 0x1cu && calls[1].linkRegister == 14u && calls[1].userDataRegister == 2u); + ExpectThrow("not statically resolvable", [&] { static_cast(GraphBuilder{}.Build(program, &swappc)); }); + + auto overwritten = caller; + overwritten[1] = 0xbe800380u; + const auto second = UserDataCalls(overwritten); + Require(second.size() == 1u && second[0].programCounter == 0x1cu); + auto entered = caller; + entered[10] = 0xbf85fff8u; + const auto afterLabel = UserDataCalls(entered); + Require(afterLabel.size() == 1u && afterLabel[0].programCounter == 0x1cu); + auto looped = caller; + looped[8] = 0xbe820380u; + const auto beforeWrite = UserDataCalls(looped); + Require(beforeWrite.size() == 1u && beforeWrite[0].programCounter == 0x10u); + Require(UserDataCalls({0xbe842104u, 0xbf810000u}, 6u).empty()); + const auto direct = UserDataCalls({0xbe8e2104u, 0xbf810000u}, 6u); + Require(direct.size() == 1u && direct[0].linkRegister == 14u && direct[0].userDataRegister == 4u); + + const std::vector addFive{0x4a020285u, 0xbe80200eu}; + const std::vector addSixtyFour{0x4a0202c0u, 0xbe80200eu}; + Require(UserDataCalleeLength(addFive, 14u) == 2u); + Require(!UserDataCalleeLength(std::vector{0x4a020285u}, 14u).has_value()); + ExpectThrow("reads its program counter", [] { static_cast(UserDataCalleeLength(std::vector{0xbe901f00u, 0xbe80200eu}, 14u)); }); + ExpectThrow("makes a call", [] { static_cast(UserDataCalleeLength(std::vector{0xbe92210eu, 0xbe80200eu}, 14u)); }); + ExpectThrow("ends the program", [] { static_cast(UserDataCalleeLength(std::vector{0xbf810000u}, 14u)); }); + ExpectThrow("jumps through another register pair", [] { static_cast(UserDataCalleeLength(std::vector{0xbe802010u}, 14u)); }); + ExpectThrow("before code it branches to", [] { static_cast(UserDataCalleeLength(std::vector{0xbf840001u, 0xbe80200eu, 0x4a020285u, 0xbe80200eu}, 14u)); }); + + const std::vector> callees{addFive, addSixtyFour}; + const auto linked = LinkUserDataCalls(program, calls, callees); + Require(linked.size() == 17u && linked[4] == 0xbb0e0007u && linked[7] == 0xbb0e0006u && linked[16] == 0xbf9f0000u); + 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; + 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); + const auto block = [&](std::uint32_t programCounter) { return cfg.FindBlockByProgramCounter(programCounter).id; }; + Require(cfg.FindBlockByProgramCounter(0x08u).terminator.trueBlock == block(0x30u)); + Require(cfg.FindBlockByProgramCounter(0x30u).terminator.trueBlock == block(0x14u)); + Require(cfg.FindBlockByProgramCounter(0x14u).terminator.trueBlock == block(0x38u)); + Require(cfg.FindBlockByProgramCounter(0x38u).terminator.trueBlock == block(0x20u)); + ShaderComputeInputInfo computeInfo{}; + TranslateOptions options{}; + options.stage = ShaderStageKind::Compute; + options.inputInfo.compute = &computeInfo; + options.userDataCount = 8u; + static_cast(InstructionTranslator{}.Translate(linkedProgram, cfg, options)); + + auto readsLink = linked; + readsLink[8] = 0xbe94030eu; + ExpectThrow("escapes the constant-offset call/return model", [&] { static_cast(GraphBuilder{}.Build(RdnaInstructionDecoder{}.Decode(readsLink), &swappc)); }); + auto writesInside = linked; + writesInside[12] = 0xbe8e0300u; + ExpectThrow("escapes the constant-offset call/return model", [&] { static_cast(GraphBuilder{}.Build(RdnaInstructionDecoder{}.Decode(writesInside), &swappc)); }); + auto sameCopy = linked; + sameCopy[7] = 0xbb0e0004u; + 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)}); @@ -194,6 +467,9 @@ int main(int argc, char** argv) { CheckLink(); 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 new file mode 100644 index 0000000000..0c86103e10 --- /dev/null +++ b/core/libs/tests/SslCertificateFixture.hpp @@ -0,0 +1,131 @@ +#pragma once +#include +#include +inline constexpr char RootPem[] = R"FIXTURE(-----BEGIN CERTIFICATE----- +MIIByDCCAW6gAwIBAgIDAIChMAoGCCqGSM49BAMCMEIxCzAJBgNVBAYTAkdCMRUw +EwYDVQQKDAxBbnlQUzUgVGVzdHMxHDAaBgNVBAMME0FueVBTNSBmaXh0dXJlIHJv +b3QwHhcNMjYxMDA4MjIxNDAyWhcNMzYxMDA1MjIxNDAyWjBCMQswCQYDVQQGEwJH +QjEVMBMGA1UECgwMQW55UFM1IFRlc3RzMRwwGgYDVQQDDBNBbnlQUzUgZml4dHVy +ZSByb290MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEGBewuI37COE1s3xt1y52 +kxoDW1vLl0IzSIlIjKyLrj80kjZWqXOR6X4Th8cyBPVeQkHh0CE+TjZtKpI3CL/H +6aNTMFEwHQYDVR0OBBYEFFZolbTcGqYVMtT6BD37g9yqaAzcMB8GA1UdIwQYMBaA +FFZolbTcGqYVMtT6BD37g9yqaAzcMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0E +AwIDSAAwRQIgUQ4NhPYRVB5YgwemEZ5vm76nEY97hJYlq/GymdmmsLgCIQDKMTQC +eNbT9zlbdI+uo50utM9rDZ8kSBx5RsuAVj8M3w== +-----END CERTIFICATE----- +)FIXTURE"; +inline constexpr char ClientPem[] = R"FIXTURE(-----BEGIN CERTIFICATE----- +MIIB3TCCAYOgAwIBAgIDEjRWMAoGCCqGSM49BAMCMEIxCzAJBgNVBAYTAkdCMRUw +EwYDVQQKDAxBbnlQUzUgVGVzdHMxHDAaBgNVBAMME0FueVBTNSBmaXh0dXJlIHJv +b3QwHhcNMjYxMDA4MjIxNDAyWhcNMzYxMDA1MjIxNDAyWjBoMQswCQYDVQQGEwJH +QjEVMBMGA1UECgwMQW55UFM1IFRlc3RzMSIwDgYDVQQLDAdQcmltYXJ5MBAGA1UE +CwwJU2Vjb25kYXJ5MR4wHAYDVQQDDBVBbnlQUzUgZml4dHVyZSBjbGllbnQwWTAT +BgcqhkjOPQIBBggqhkjOPQMBBwNCAAR1Njvxhr/lzV/EnLlOEyir7zimMPNEDnWt +ZYBape38P+jLkWNUgklWh0rIaqQq7rzISu0+mK+ee16bJC3qdWsXo0IwQDAdBgNV +HQ4EFgQUwVSZJzELeQ6acITiH929Z4kUuEUwHwYDVR0jBBgwFoAUVmiVtNwaphUy +1PoEPfuD3KpoDNwwCgYIKoZIzj0EAwIDSAAwRQIhAIJ3dzC+ddOvUbBXg4sCYTG3 +xXS9+jF9gxro9MqK1ZenAiA+l/Y8EMrvzjXx7kN5ZV+cVSnH1Ngn3kqaE8w7gIRK +NQ== +-----END CERTIFICATE----- +)FIXTURE"; +inline constexpr char RootKey[] = R"FIXTURE(-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgHIXi+x3Z7ozqaKSg +w4mBVS67OC+pzhTpjT6EhQ9/V32hRANCAAQYF7C4jfsI4TWzfG3XLnaTGgNbW8uX +QjNIiUiMrIuuPzSSNlapc5HpfhOHxzIE9V5CQeHQIT5ONm0qkjcIv8fp +-----END PRIVATE KEY----- +)FIXTURE"; +inline constexpr char ClientKey[] = R"FIXTURE(-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgHoANtFzCHilXfZPd +GmO1G+NKG5sSUnUWmDrU29+IaQahRANCAAR1Njvxhr/lzV/EnLlOEyir7zimMPNE +DnWtZYBape38P+jLkWNUgklWh0rIaqQq7rzISu0+mK+ee16bJC3qdWsX +-----END PRIVATE KEY----- +)FIXTURE"; +inline constexpr std::array RootDer{ + 0x30, 0x82, 0x01, 0xc8, 0x30, 0x82, 0x01, 0x6e, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x03, 0x00, + 0x80, 0xa1, 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x30, 0x42, + 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x47, 0x42, 0x31, 0x15, 0x30, + 0x13, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0c, 0x41, 0x6e, 0x79, 0x50, 0x53, 0x35, 0x20, 0x54, + 0x65, 0x73, 0x74, 0x73, 0x31, 0x1c, 0x30, 0x1a, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, 0x13, 0x41, + 0x6e, 0x79, 0x50, 0x53, 0x35, 0x20, 0x66, 0x69, 0x78, 0x74, 0x75, 0x72, 0x65, 0x20, 0x72, 0x6f, + 0x6f, 0x74, 0x30, 0x1e, 0x17, 0x0d, 0x32, 0x36, 0x31, 0x30, 0x30, 0x38, 0x32, 0x32, 0x31, 0x34, + 0x30, 0x32, 0x5a, 0x17, 0x0d, 0x33, 0x36, 0x31, 0x30, 0x30, 0x35, 0x32, 0x32, 0x31, 0x34, 0x30, + 0x32, 0x5a, 0x30, 0x42, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x47, + 0x42, 0x31, 0x15, 0x30, 0x13, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0c, 0x41, 0x6e, 0x79, 0x50, + 0x53, 0x35, 0x20, 0x54, 0x65, 0x73, 0x74, 0x73, 0x31, 0x1c, 0x30, 0x1a, 0x06, 0x03, 0x55, 0x04, + 0x03, 0x0c, 0x13, 0x41, 0x6e, 0x79, 0x50, 0x53, 0x35, 0x20, 0x66, 0x69, 0x78, 0x74, 0x75, 0x72, + 0x65, 0x20, 0x72, 0x6f, 0x6f, 0x74, 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, + 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, + 0x04, 0x18, 0x17, 0xb0, 0xb8, 0x8d, 0xfb, 0x08, 0xe1, 0x35, 0xb3, 0x7c, 0x6d, 0xd7, 0x2e, 0x76, + 0x93, 0x1a, 0x03, 0x5b, 0x5b, 0xcb, 0x97, 0x42, 0x33, 0x48, 0x89, 0x48, 0x8c, 0xac, 0x8b, 0xae, + 0x3f, 0x34, 0x92, 0x36, 0x56, 0xa9, 0x73, 0x91, 0xe9, 0x7e, 0x13, 0x87, 0xc7, 0x32, 0x04, 0xf5, + 0x5e, 0x42, 0x41, 0xe1, 0xd0, 0x21, 0x3e, 0x4e, 0x36, 0x6d, 0x2a, 0x92, 0x37, 0x08, 0xbf, 0xc7, + 0xe9, 0xa3, 0x53, 0x30, 0x51, 0x30, 0x1d, 0x06, 0x03, 0x55, 0x1d, 0x0e, 0x04, 0x16, 0x04, 0x14, + 0x56, 0x68, 0x95, 0xb4, 0xdc, 0x1a, 0xa6, 0x15, 0x32, 0xd4, 0xfa, 0x04, 0x3d, 0xfb, 0x83, 0xdc, + 0xaa, 0x68, 0x0c, 0xdc, 0x30, 0x1f, 0x06, 0x03, 0x55, 0x1d, 0x23, 0x04, 0x18, 0x30, 0x16, 0x80, + 0x14, 0x56, 0x68, 0x95, 0xb4, 0xdc, 0x1a, 0xa6, 0x15, 0x32, 0xd4, 0xfa, 0x04, 0x3d, 0xfb, 0x83, + 0xdc, 0xaa, 0x68, 0x0c, 0xdc, 0x30, 0x0f, 0x06, 0x03, 0x55, 0x1d, 0x13, 0x01, 0x01, 0xff, 0x04, + 0x05, 0x30, 0x03, 0x01, 0x01, 0xff, 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, + 0x03, 0x02, 0x03, 0x48, 0x00, 0x30, 0x45, 0x02, 0x20, 0x51, 0x0e, 0x0d, 0x84, 0xf6, 0x11, 0x54, + 0x1e, 0x58, 0x83, 0x07, 0xa6, 0x11, 0x9e, 0x6f, 0x9b, 0xbe, 0xa7, 0x11, 0x8f, 0x7b, 0x84, 0x96, + 0x25, 0xab, 0xf1, 0xb2, 0x99, 0xd9, 0xa6, 0xb0, 0xb8, 0x02, 0x21, 0x00, 0xca, 0x31, 0x34, 0x02, + 0x78, 0xd6, 0xd3, 0xf7, 0x39, 0x5b, 0x74, 0x8f, 0xae, 0xa3, 0x9d, 0x2e, 0xb4, 0xcf, 0x6b, 0x0d, + 0x9f, 0x24, 0x48, 0x1c, 0x79, 0x46, 0xcb, 0x80, 0x56, 0x3f, 0x0c, 0xdf, +}; +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/ThreadLocalDestructors.cmake b/core/libs/tests/ThreadLocalDestructors.cmake new file mode 100644 index 0000000000..d58f04de55 --- /dev/null +++ b/core/libs/tests/ThreadLocalDestructors.cmake @@ -0,0 +1,38 @@ +file(GLOB modules "${MODULES}/*.prx") +if(NOT modules) + message(FATAL_ERROR "no modules in ${MODULES}") +endif() +set(explicit "__cxa_thread_atexit_nid_postfix|__cxa_thread_atexit_impl_nid_postfix|LibcInternalExtCxaThreadAtexit_nid_postfix") +set(failures "") +foreach(module IN LISTS modules) + get_filename_component(name "${module}" NAME) + execute_process(COMMAND "${NM}" --defined-only "${module}" OUTPUT_VARIABLE symbols RESULT_VARIABLE result ERROR_QUIET) + if(NOT result EQUAL 0 OR symbols STREQUAL "") + list(APPEND failures "${name}: no symbol table to inspect") + continue() + endif() + if(NOT symbols MATCHES " T __mingw_cxa_thread_atexit\n") + continue() + endif() + execute_process(COMMAND "${OBJDUMP}" -d --no-show-raw-insn "${module}" OUTPUT_VARIABLE code RESULT_VARIABLE result ERROR_QUIET) + if(NOT result EQUAL 0) + list(APPEND failures "${name}: cannot disassemble") + continue() + endif() + string(REGEX MATCHALL "\n[0-9a-f]+ <[^>\n]+>:|(call|jmp)[^\n]*<__cxa_thread_atexit>" lines "${code}") + set(function "") + foreach(line IN LISTS lines) + if(line MATCHES "^\n[0-9a-f]+ <([^>\n]+)>:$") + set(function "${CMAKE_MATCH_1}") + elseif(NOT function MATCHES "^(${explicit})$") + list(APPEND failures "${name}: ${function} registers a thread_local destructor") + endif() + endforeach() +endforeach() +if(failures) + list(REMOVE_DUPLICATES failures) + list(JOIN failures "\n " text) + message(FATAL_ERROR "On Windows a thread_local destructor in a module runs after emulated TLS freed the object; keep such objects in HostThreadLocal:\n ${text}") +endif() +list(LENGTH modules count) +message(STATUS "${count} modules register no thread_local destructors") 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/UndeliverableHostException.cpp b/core/libs/tests/UndeliverableHostException.cpp new file mode 100644 index 0000000000..6356af4e7b --- /dev/null +++ b/core/libs/tests/UndeliverableHostException.cpp @@ -0,0 +1,29 @@ +#include +#include +#include +#include + +namespace { + +[[gnu::noinline]] void* AllocateWithoutUnwindData(std::size_t size) { + void* const block = ::operator new(size); + std::printf("allocated %p\n", block); + return block; +} + +} + +int main(int argc, char** argv) { + if (argc != 2) { + std::fprintf(stderr, "usage: %s \n", argv[0]); + return 2; + } + SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); + if (LoadLibraryExA(argv[1], nullptr, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS) == nullptr) { + std::fprintf(stderr, "%s: LoadLibraryEx failed with error %lu\n", argv[1], GetLastError()); + return 1; + } + AllocateWithoutUnwindData(std::size_t{1} << 46); + std::fputs("operator new returned instead of throwing\n", stderr); + return 1; +} diff --git a/core/libs/tests/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/VectorCompareConst.cpp b/core/libs/tests/VectorCompareConst.cpp index 51ee5102ff..8891fca645 100644 --- a/core/libs/tests/VectorCompareConst.cpp +++ b/core/libs/tests/VectorCompareConst.cpp @@ -18,6 +18,37 @@ static void Check(std::uint32_t encoding, RdnaOpcode opcode, bool exec) { TranslationContext context(program, block, 256); context.TranslateInstruction(instruction); } + +static void CheckUnorderedInfinity(RdnaOpcode opcode, std::uint32_t constant, bool constantIsLhs) { + RdnaInstruction instruction{}; + instruction.op = opcode; + instruction.family = RdnaInstructionFamily::VOPC; + instruction.sourceCount = 2u; + instruction.destination.kind = RdnaOperandKind::VccLo; + RdnaOperand& variable = constantIsLhs ? instruction.source1 : instruction.source0; + variable.kind = RdnaOperandKind::VectorRegister; + variable.reg = 1u; + RdnaOperand& infinity = constantIsLhs ? instruction.source0 : instruction.source1; + infinity.kind = RdnaOperandKind::FloatInlineConstant; + infinity.value = constant; + IrProgram program; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + TranslationContext context(program, block, 256u); + context.TranslateInstruction(instruction); + const IrValue* vccValue = nullptr; + bool comparedFloats = false; + for (const auto* value : block.Instructions()) { + if (value->Opcode() == IrOpcode::SetVcc) vccValue = value->Argument(0u); + comparedFloats = comparedFloats || value->Opcode() == IrOpcode::FPUnordLessThanEqual32 || + value->Opcode() == IrOpcode::FPUnordGreaterThanEqual32; + } + Require(vccValue != nullptr && !comparedFloats); + Require(vccValue->Opcode() == IrOpcode::LogicalAnd); + const IrValue* result = vccValue->Argument(1u)->Resolve(); + Require(result->HasImmediate() && result->ImmediateU32() == 1u); +} + int main() { Check(0xa0u, RdnaOpcode::VCmpFI64, false); Check(0xa7u, RdnaOpcode::VCmpTI64, false); @@ -43,4 +74,8 @@ int main() { Check(0xffu, RdnaOpcode::VCmpxTruF16, true); Check(0xdfu, RdnaOpcode::VCmpxOF16, true); Check(0xf8u, RdnaOpcode::VCmpxUF16, true); + CheckUnorderedInfinity(RdnaOpcode::VCmpNgtF32, 0x7f800000u, false); + CheckUnorderedInfinity(RdnaOpcode::VCmpNltF32, 0xff800000u, false); + CheckUnorderedInfinity(RdnaOpcode::VCmpNltF32, 0x7f800000u, true); + CheckUnorderedInfinity(RdnaOpcode::VCmpNgtF32, 0xff800000u, true); } diff --git a/core/libs/tests/VectorConstantFactorTininess.cpp b/core/libs/tests/VectorConstantFactorTininess.cpp new file mode 100644 index 0000000000..f75cf27774 --- /dev/null +++ b/core/libs/tests/VectorConstantFactorTininess.cpp @@ -0,0 +1,138 @@ +#include "Translation/TranslationContext.hpp" +#include "RdnaDecoder/RdnaVectorOpDecoder.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +namespace { + +enum class Tininess { + None, + Rounded, + ExponentsOnly, + Full, +}; + +void Require(bool value, const std::string& message) { + if (!value) throw std::runtime_error("f32 constant factor: " + message); +} + +const IrBlock& Translate(IrProgram& program, const std::string& name, const std::vector& words, RdnaOpcode opcode, std::uint32_t floatMode) { + const RdnaInstruction instruction = DecodeRdnaVectorOp(std::span(words), 0u); + Require(instruction.op == opcode, name + " decodes to another opcode"); + Require(instruction.wordCount == words.size(), name + " has the wrong length"); + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + TranslationContext context(program, block, 256); + context.SetFloatMode(ShaderFloatMode{floatMode, true, false, false}); + context.TranslateInstruction(instruction); + return block; +} + +void Check(const std::string& name, const std::vector& words, RdnaOpcode opcode, Tininess expected, std::uint32_t floatMode = 0xc0u) { + IrProgram program; + const auto& block = Translate(program, name, words, opcode, floatMode); + std::uint32_t multiplyHigh = 0u; + std::uint32_t exponentCompares = 0u; + std::uint32_t roundedCompares = 0u; + for (const auto* value : block.Instructions()) { + if (value->Opcode() == IrOpcode::UMulHi) ++multiplyHigh; + if (value->Opcode() != IrOpcode::ULessThan32 || !value->Argument(1)->HasImmediate()) continue; + if (value->Argument(1)->ImmediateU32() == 128u) ++exponentCompares; + if (value->Argument(1)->ImmediateU32() == 0x207fffffu) ++roundedCompares; + } + const std::uint32_t expectedExponentCompares = expected == Tininess::ExponentsOnly || expected == Tininess::Full ? 1u : 0u; + const std::uint32_t expectedRoundedCompares = expected == Tininess::Rounded ? 1u : 0u; + const std::uint32_t expectedMultiplyHigh = expected == Tininess::Full ? 1u : 0u; + Require(exponentCompares == expectedExponentCompares, name + " emits " + std::to_string(exponentCompares) + " exponent tiny product checks, expected " + std::to_string(expectedExponentCompares)); + Require(roundedCompares == expectedRoundedCompares, name + " emits " + std::to_string(roundedCompares) + " rounded tiny product checks, expected " + std::to_string(expectedRoundedCompares)); + Require(multiplyHigh == expectedMultiplyHigh, name + " emits " + std::to_string(multiplyHigh) + " significand products, expected " + std::to_string(expectedMultiplyHigh)); +} + +std::unordered_set Cone(const IrValue* root, const std::unordered_set& instructions) { + std::unordered_set cone; + std::vector pending{root}; + while (!pending.empty()) { + const IrValue* value = pending.back(); + pending.pop_back(); + if (!instructions.contains(value) || !cone.insert(value).second) continue; + for (const IrValue* argument : value->Arguments()) pending.push_back(argument); + } + return cone; +} + +void CheckFlushSize(const std::string& name, const std::vector& words, RdnaOpcode opcode, std::uint32_t floatMode, std::size_t expected) { + IrProgram program; + const auto& block = Translate(program, name, words, opcode, floatMode); + const std::unordered_set instructions(block.Instructions().begin(), block.Instructions().end()); + for (const auto* value : block.Instructions()) { + if (value->Opcode() != IrOpcode::SelectF32) continue; + const IrOpcode product = value->Argument(2)->Opcode(); + if (product != IrOpcode::FPMul32 && product != IrOpcode::FPFma32) continue; + const auto flush = Cone(value, instructions); + const auto kept = Cone(value->Argument(2), instructions); + const auto size = static_cast(std::count_if(flush.begin(), flush.end(), [&](const IrValue* instruction) { return !kept.contains(instruction); })); + Require(size == expected, name + " flushes a tiny product with " + std::to_string(size) + " instructions, expected " + std::to_string(expected)); + return; + } + Require(false, name + " emits no tiny product flush"); +} + +void CheckFoldedSource(const std::string& name, const std::vector& words, RdnaOpcode opcode, IrOpcode product, std::uint32_t index, std::uint32_t expected) { + IrProgram program; + const auto& block = Translate(program, name, words, opcode, 0xc0u); + for (const auto* value : block.Instructions()) { + if (value->Opcode() != product) continue; + const IrValue* source = value->Argument(index); + Require(source->Opcode() == IrOpcode::BitCastF32U32 && source->Argument(0)->HasImmediate(), name + " source " + std::to_string(index) + " is not a constant"); + Require(source->Argument(0)->ImmediateU32() == expected, name + " source " + std::to_string(index) + " is folded to the wrong constant"); + return; + } + Require(false, name + " emits no product"); +} + +} + +int main() { + try { + Check("v_mul_f32 v5, v6, v7", {0x100a0f06u}, RdnaOpcode::VMulF32, Tininess::Rounded); + Check("v_mul_f32 v5, 2.0, v7", {0x100a0ef4u}, RdnaOpcode::VMulF32, Tininess::None); + Check("v_mul_f32 v5, -4.0, v7", {0x100a0ef7u}, RdnaOpcode::VMulF32, Tininess::None); + Check("v_mul_f32 v5, 1.0, v7", {0x100a0ef2u}, RdnaOpcode::VMulF32, Tininess::None); + Check("v_mul_f32 v5, 0x7f800000, v7", {0x100a0effu, 0x7f800000u}, RdnaOpcode::VMulF32, Tininess::None); + Check("v_mul_f32 v5, 0x00400000, v7", {0x100a0effu, 0x00400000u}, RdnaOpcode::VMulF32, Tininess::None); + Check("v_mul_f32 v5, 1, v7", {0x100a0e81u}, RdnaOpcode::VMulF32, Tininess::None); + Check("v_mul_f32 v5, 0.5, v7", {0x100a0ef0u}, RdnaOpcode::VMulF32, Tininess::Rounded); + Check("v_mul_f32 v5, 0x3e800000, v7", {0x100a0effu, 0x3e800000u}, RdnaOpcode::VMulF32, Tininess::Rounded); + Check("v_mul_f32 v5, 0x3f7fffff, v7", {0x100a0effu, 0x3f7fffffu}, RdnaOpcode::VMulF32, Tininess::Rounded); + Check("v_mul_f32 v5, 0.15915494, v7", {0x100a0ef8u}, RdnaOpcode::VMulF32, Tininess::Rounded); + Check("v_mul_legacy_f32 v5, 4.0, v7", {0x0e0a0ef6u}, RdnaOpcode::VMulLegacyF32, Tininess::None); + Check("v_fmamk_f32 v5, v6, 0x40400000, v7", {0x580a0f06u, 0x40400000u}, RdnaOpcode::VMadmkF32, Tininess::None); + Check("v_fmaak_f32 v5, v6, v7, 0x40400000", {0x5a0a0f06u, 0x40400000u}, RdnaOpcode::VMadakF32, Tininess::Rounded); + Check("v_fma_f32 v5, v6, -|2.0|, v7", {0xd54b0205u, 0x441de906u}, RdnaOpcode::VFmaF32, Tininess::None); + Check("v_fma_f32 v5, v6, -|0.5|, v7", {0xd54b0205u, 0x441de106u}, RdnaOpcode::VFmaF32, Tininess::Rounded); + Check("v_mul_f32 v5, v6, v7 rounding toward +infinity", {0x100a0f06u}, RdnaOpcode::VMulF32, Tininess::Full, 0xc1u); + Check("v_mul_f32 v5, 0.5, v7 rounding toward zero", {0x100a0ef0u}, RdnaOpcode::VMulF32, Tininess::ExponentsOnly, 0xc3u); + Check("v_fma_f32 v5, v6, -|0.5|, v7 rounding toward -infinity", {0xd54b0205u, 0x441de106u}, RdnaOpcode::VFmaF32, Tininess::ExponentsOnly, 0xc2u); + Check("v_mad_legacy_f32 v5, v6, v7, v8", {0xd5400005u, 0x04220f06u}, RdnaOpcode::VMadLegacyF32, Tininess::Full); + Check("v_mul_f32 v5, 2.0, v7 with f32 denormals kept", {0x100a0ef4u}, RdnaOpcode::VMulF32, Tininess::None, 0xf0u); + Check("v_mul_f32 v5, v6, v7 with f32 denormals kept", {0x100a0f06u}, RdnaOpcode::VMulF32, Tininess::None, 0xf0u); + CheckFlushSize("v_mul_f32 v5, v6, v7", {0x100a0f06u}, RdnaOpcode::VMulF32, 0xc0u, 12u); + CheckFlushSize("v_mul_f32 v5, v6, v7 rounding toward +infinity", {0x100a0f06u}, RdnaOpcode::VMulF32, 0xc1u, 36u); + CheckFoldedSource("v_fma_f32 v5, v6, -|2.0|, v7", {0xd54b0205u, 0x441de906u}, RdnaOpcode::VFmaF32, IrOpcode::FPFma32, 1u, 0xc0000000u); + CheckFoldedSource("v_mul_f32 v5, 0x00400000, v7", {0x100a0effu, 0x00400000u}, RdnaOpcode::VMulF32, IrOpcode::FPMul32, 0u, 0x00000000u); + CheckFoldedSource("v_mul_f32 v5, 0x80400000, v7", {0x100a0effu, 0x80400000u}, RdnaOpcode::VMulF32, IrOpcode::FPMul32, 0u, 0x80000000u); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/tests/VectorFusedMultiplyAdd.cpp b/core/libs/tests/VectorFusedMultiplyAdd.cpp index 3315e155a7..3f2926517e 100644 --- a/core/libs/tests/VectorFusedMultiplyAdd.cpp +++ b/core/libs/tests/VectorFusedMultiplyAdd.cpp @@ -90,6 +90,45 @@ void CheckMix(const std::string& name, const std::array& words Require(fused == 1u, name + " emits " + std::to_string(fused) + " fused multiply-adds"); } +void CheckMixHalf(const std::string& name, const std::array& words, RdnaOpcode opcode, std::uint32_t floatMode) { + const RdnaInstruction instruction = DecodeRdnaVectorOp(std::span(words), 0u); + Require(instruction.family == RdnaInstructionFamily::VOP3P, name + " is not decoded as VOP3P"); + Require(instruction.op == opcode, name + " decodes to another opcode"); + + IrProgram program; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + TranslationContext context(program, block, 256); + context.SetFloatMode(ShaderFloatMode{floatMode, false, false, false}); + context.TranslateInstruction(instruction); + + std::uint32_t exact = 0u; + for (const auto* value : block.Instructions()) { + Require(value->Opcode() != IrOpcode::FPFma32 && value->Opcode() != IrOpcode::FPMad32, name + " rounds to f32 before rounding to f16"); + if (value->Opcode() != IrOpcode::FPInterpolateF16) continue; + ++exact; + const std::uint32_t rounding = (value->Argument(3)->ImmediateU32() >> InterpolationRound16Shift) & 3u; + Require(rounding == ((floatMode >> 2u) & 3u), name + " rounds in f16 mode " + std::to_string(rounding)); + } + Require(exact == 1u, name + " emits " + std::to_string(exact) + " product-sums rounded once to f16"); +} + +void RejectMixHalf(const std::string& name, const std::array& words, std::uint32_t floatMode) { + const RdnaInstruction instruction = DecodeRdnaVectorOp(std::span(words), 0u); + IrProgram program; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + TranslationContext context(program, block, 256); + context.SetFloatMode(ShaderFloatMode{floatMode, false, false, false}); + try { + context.TranslateInstruction(instruction); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find("FLOAT_MODE " + std::to_string(floatMode)) != std::string::npos, name + " fails with another error: " + error.what()); + return; + } + Require(false, name + " is translated in FLOAT_MODE " + std::to_string(floatMode)); +} + } int main() { @@ -105,6 +144,13 @@ int main() { Check("v_fmaak_f32", 0x2du, 2u, RdnaOpcode::VMadakF32, IrOpcode::FPFma32, addLiteral); CheckMix("v_fma_mix_f32", {0xcc200005u, 0x04220f06u}); CheckMix("v_fma_mix_f32 op_sel_hi:[1,0,1]", {0xcc204005u, 0x0c220f06u}); + CheckMixHalf("v_fma_mixlo_f16", {0xcc210005u, 0x04220f06u}, RdnaOpcode::VMadMixloF16, 0xc0u); + CheckMixHalf("v_fma_mixhi_f16", {0xcc220005u, 0x04220f06u}, RdnaOpcode::VMadMixhiF16, 0xc0u); + CheckMixHalf("v_fma_mixlo_f16 op_sel_hi:[1,1,1]", {0xcc214005u, 0x1c220f06u}, RdnaOpcode::VMadMixloF16, 0xc0u); + CheckMixHalf("v_fma_mixlo_f16 with f32 round toward zero", {0xcc210005u, 0x04220f06u}, RdnaOpcode::VMadMixloF16, 0xf3u); + RejectMixHalf("v_fma_mixlo_f16 with f16 denormals flushed", {0xcc210005u, 0x04220f06u}, 0x30u); + CheckMixHalf("v_fma_mixhi_f16 with f16 round toward -inf", {0xcc220005u, 0x04220f06u}, RdnaOpcode::VMadMixhiF16, 0xf8u); + CheckMixHalf("v_fma_mixlo_f16 with f16 round toward zero", {0xcc210005u, 0x04220f06u}, RdnaOpcode::VMadMixloF16, 0xccu); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/libs/tests/VectorMadLegacy.cpp b/core/libs/tests/VectorMadLegacy.cpp index cf2677161b..aaf379bc7b 100644 --- a/core/libs/tests/VectorMadLegacy.cpp +++ b/core/libs/tests/VectorMadLegacy.cpp @@ -21,6 +21,6 @@ static void Check(std::uint32_t encoding, RdnaOpcode opcode, IrOpcode expected) Require(found); } int main() { - Check(0x140u, RdnaOpcode::VMadLegacyF32, IrOpcode::FPFma32); + Check(0x140u, RdnaOpcode::VMadLegacyF32, IrOpcode::FPMul32); Check(0x150u, RdnaOpcode::VMullitF32, IrOpcode::FPMul32); } diff --git a/core/libs/tests/VideoOutTestEnvironment.hpp b/core/libs/tests/VideoOutTestEnvironment.hpp new file mode 100644 index 0000000000..786cf5e32a --- /dev/null +++ b/core/libs/tests/VideoOutTestEnvironment.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +class VideoOutTestEnvironment final { + std::filesystem::path previous; + std::filesystem::path root; + +public: + VideoOutTestEnvironment() : previous(std::filesystem::current_path()) { + const auto seed = static_cast(std::chrono::steady_clock::now().time_since_epoch().count()) ^ + (static_cast(std::random_device{}()) << 32u) ^ std::random_device{}(); + for (std::uint32_t attempt = 0; attempt < 64; ++attempt) { + const auto candidate = std::filesystem::temp_directory_path() / + ("anyps5-video-out-" + std::to_string(seed + attempt)); + std::error_code error; + if (std::filesystem::create_directory(candidate, error)) { + root = candidate; + break; + } + if (error) throw std::runtime_error("Cannot create VideoOut test directory: " + error.message()); + } + if (root.empty()) throw std::runtime_error("Cannot allocate a unique VideoOut test directory"); + try { + std::filesystem::current_path(root); + std::filesystem::create_directories("app0/sce_sys"); + std::ofstream param("app0/sce_sys/param.json", std::ios::binary); + param << R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":"Example"}},"downloadDataSize":0})"; + if (!param) throw std::runtime_error("Cannot write VideoOut param.json fixture"); + } catch (...) { + std::error_code error; + std::filesystem::current_path(previous, error); + std::filesystem::remove_all(root, error); + throw; + } + } + + ~VideoOutTestEnvironment() { + std::error_code error; + std::filesystem::current_path(previous, error); + std::filesystem::remove_all(root, error); + } + + VideoOutTestEnvironment(const VideoOutTestEnvironment&) = delete; + VideoOutTestEnvironment& operator=(const VideoOutTestEnvironment&) = delete; +}; 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 new file mode 100644 index 0000000000..d16340e2b3 --- /dev/null +++ b/core/libs/tests/WindowsBacktraceReport.cpp @@ -0,0 +1,54 @@ +#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(); + +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 2e13adb432..c83acd49c2 100644 --- a/core/libs/tests/WindowsExceptions.cpp +++ b/core/libs/tests/WindowsExceptions.cpp @@ -1,12 +1,19 @@ #include +#include #include #include +#include #include #include +#include +#include #include +#include +#include #include #include #include +#include #include "../prx/libc/include/general/VabiMacros.hpp" extern "C" { @@ -51,7 +58,118 @@ 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"); +} + +[[gnu::noinline, noreturn]] static void ThrowUncaughtProbe() { + throw std::runtime_error("unhandled site probe"); +} + +static int RunThrowChild() { + std::fprintf(stderr, "probe %p\n", reinterpret_cast(&ThrowUncaughtProbe)); + std::fflush(stderr); + ThrowUncaughtProbe(); + return 100; +} + +static std::string RunChild(const wchar_t* argument, DWORD& exitCode) { + SECURITY_ATTRIBUTES attributes{}; + attributes.nLength = sizeof(attributes); + attributes.bInheritHandle = TRUE; + HANDLE readPipe = nullptr; + HANDLE writePipe = nullptr; + RequireHandle(CreatePipe(&readPipe, &writePipe, &attributes, 0) != 0); + RequireHandle(SetHandleInformation(readPipe, HANDLE_FLAG_INHERIT, 0) != 0); + std::array image{}; + RequireHandle(GetModuleFileNameW(nullptr, image.data(), static_cast(image.size())) != 0); + std::array command{}; + const std::size_t imageLength = std::wcslen(image.data()); + const std::size_t argumentLength = std::wcslen(argument); + RequireHandle(1 + imageLength + argumentLength + 1 < command.size()); + command[0] = L'"'; + std::memcpy(command.data() + 1, image.data(), imageLength * sizeof(wchar_t)); + std::memcpy(command.data() + 1 + imageLength, argument, (argumentLength + 1) * sizeof(wchar_t)); + STARTUPINFOW startup{}; + startup.cb = sizeof(startup); + startup.dwFlags = STARTF_USESTDHANDLES; + startup.hStdInput = GetStdHandle(STD_INPUT_HANDLE); + startup.hStdOutput = writePipe; + startup.hStdError = writePipe; + PROCESS_INFORMATION child{}; + RequireHandle(CreateProcessW(image.data(), command.data(), nullptr, nullptr, TRUE, CREATE_NO_WINDOW, nullptr, nullptr, &startup, &child) != 0); + CloseHandle(writePipe); + CloseHandle(child.hThread); + std::array output{}; + std::size_t outputLength = 0; + DWORD chunk = 0; + while (outputLength < output.size() && ReadFile(readPipe, output.data() + outputLength, static_cast(output.size() - outputLength), &chunk, nullptr) != 0 && chunk != 0) { + outputLength += chunk; + } + CloseHandle(readPipe); + RequireHandle(WaitForSingleObject(child.hProcess, 20000) == WAIT_OBJECT_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.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.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; } }; @@ -105,9 +223,115 @@ static void testExceptionPointer() { if (std::current_exception()) throw std::runtime_error("stale current exception"); } -int main() { +static void testFutureException() { + std::atomic count{0}; + { + std::promise promise; + auto future = promise.get_future(); + try { + throw TrackedError(&count); + } catch (const TrackedError&) { + promise.set_exception(std::current_exception()); + } + try { + future.get(); + throw std::runtime_error("future did not rethrow"); + } catch (const TrackedError& error) { + if (std::strcmp(error.what(), "retained error")) throw std::runtime_error("future rethrew another error"); + } + } + if (count != 1) throw std::runtime_error("future result destroyed its exception an incorrect number of times"); +} + +static void testMadeExceptionPointer() { + std::atomic count{0}; + { + auto made = std::make_exception_ptr(TrackedError(&count)); + try { + std::rethrow_exception(made); + } catch (const TrackedError& error) { + if (std::strcmp(error.what(), "retained error")) throw std::runtime_error("made exception lost its message"); + } + } + if (count != 2) throw std::runtime_error("made exception destroyed an incorrect number of times"); +} + +struct UnwindProbe { + int& seen; + ~UnwindProbe() { seen = std::uncaught_exceptions(); } +}; + +static void testUncaughtExceptions() { + if (std::uncaught_exceptions() != 0) throw std::runtime_error("uncaught exception outside unwinding"); + int seen = -1; + try { + UnwindProbe probe{seen}; + throw std::runtime_error("probe"); + } catch (const std::runtime_error&) { + } + if (seen != 1) throw std::runtime_error("unwinding did not count its exception"); +} + +static int guestFrameDestroyed; +struct GuestFrameGuard { ~GuestFrameGuard() { ++guestFrameDestroyed; } }; +static void ThrowThroughGuestFrames(int depth) { + GuestFrameGuard guard; + if (depth) { ThrowThroughGuestFrames(depth - 1); return; } + throw std::runtime_error("through guest frames"); +} + +static void testUnwindThroughGuestFrames() { + for (int depth : {0, 1, 7, 64}) { + guestFrameDestroyed = 0; + try { + ThrowThroughGuestFrames(depth); + throw std::logic_error("guest frame did not propagate"); + } catch (const std::runtime_error& error) { + if (std::strcmp(error.what(), "through guest frames")) throw; + } + if (guestFrameDestroyed != depth + 1) throw std::runtime_error("unwinding through guest frames skipped a destructor"); + } +} + +static int onceDestroyed; +struct OnceGuard { ~OnceGuard() { ++onceDestroyed; } }; +static void ThrowThroughOnce(int depth) { + OnceGuard guard; + if (depth) { ThrowThroughOnce(depth - 1); return; } + std::once_flag flag; + std::call_once(flag, [] { throw std::runtime_error("through runtime frames"); }); +} + +static void testUnwindThroughRuntimeModules() { + for (int depth : {0, 1, 7, 64}) { + onceDestroyed = 0; + try { + ThrowThroughOnce(depth); + throw std::logic_error("call_once did not propagate"); + } catch (const std::runtime_error& error) { + if (std::strcmp(error.what(), "through runtime frames")) throw; + } + if (onceDestroyed != depth + 1) throw std::runtime_error("unwinding through runtime DLL frames skipped a destructor"); + } +} + +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(); + testUncaughtExceptions(); + testUnwindThroughGuestFrames(); + testUnwindThroughRuntimeModules(); try { Rethrow(); return 1; 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/WindowsWriteWatch.cpp b/core/libs/tests/WindowsWriteWatch.cpp new file mode 100644 index 0000000000..b44c48af61 --- /dev/null +++ b/core/libs/tests/WindowsWriteWatch.cpp @@ -0,0 +1,239 @@ +#define NOMINMAX +#include "prx/libc/include/WindowsMappings.hpp" +#include +#include +#include +#include +#include + +namespace { +using Mappings = GuestArena::WindowsMappings; +using Collection = Mappings::Collection; +constexpr std::size_t Small = 4096; +constexpr std::size_t Bytes = 65536; +std::atomic faults{0}; + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +LONG CALLBACK HandleWrite(EXCEPTION_POINTERS* exception) { + const auto* record = exception->ExceptionRecord; + if (record->ExceptionCode != EXCEPTION_ACCESS_VIOLATION || record->NumberParameters < 2 || record->ExceptionInformation[0] != 1) return EXCEPTION_CONTINUE_SEARCH; + if (!Mappings::Get().HandleWrite(record->ExceptionInformation[1])) return EXCEPTION_CONTINUE_SEARCH; + ++faults; + return EXCEPTION_CONTINUE_EXECUTION; +} + +struct Region { + HANDLE section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, Bytes, nullptr); + unsigned char* first = static_cast(Mappings::Get().Reserve(nullptr, Bytes)); + unsigned char* alias = static_cast(Mappings::Get().Reserve(nullptr, Bytes)); + + Region() { + Require(section && first && alias, "cannot allocate shared test memory"); + Mappings::Get().Map(first, Bytes, section, 0, PAGE_READWRITE); + Mappings::Get().Map(alias, Bytes, section, 0, PAGE_READWRITE); + } + ~Region() { + Mappings::Get().Reset(first, Bytes); + Mappings::Get().Reset(alias, Bytes); + VirtualFree(first, 0, MEM_RELEASE); + VirtualFree(alias, 0, MEM_RELEASE); + CloseHandle(section); + } + void Write(std::size_t page, unsigned char value) { + static_cast(first)[page * Small] = value; + } + std::set Collect(Collection mode, std::size_t begin = 0, std::size_t count = 16, bool clear = true, bool useAlias = false, std::size_t capacity = 32) { + auto* base = useAlias ? alias : first; + const auto address = reinterpret_cast(base); + auto cursor = address + begin * Small; + const auto end = cursor + count * Small; + std::set result; + while (cursor < end) { + std::array pages{}; + auto available = capacity; + Require(Mappings::Get().Collect(cursor, end - cursor, pages.data(), &available, clear, mode), "shared collection failed"); + for (std::size_t i = 0; i < available; ++i) result.insert((reinterpret_cast(pages[i]) - address) / Small); + if (available < capacity) break; + cursor = reinterpret_cast(pages[available - 1]) + Small; + } + return result; + } + DWORD Protection(std::size_t page, bool useAlias = false) { + MEMORY_BASIC_INFORMATION info{}; + Require(VirtualQuery((useAlias ? alias : first) + page * Small, &info, sizeof(info)) == sizeof(info), "protection query failed"); + return info.Protect; + } +}; + +std::set Pages(std::size_t begin, std::size_t end) { + std::set result; + for (auto page = begin; page < end; ++page) result.insert(page); + return result; +} + +void CheckArmed() { + Region region; + Require(region.Collect(Collection::Armed).empty(), "import collected fresh memory"); + const auto before = faults.load(); + region.Write(0, 1); + region.Write(4, 2); + Require(faults == before, "import armed fresh memory"); + Require(region.Collect(Collection::All, 0, 1) == Pages(0, 1), "first partial collect failed"); + Require(region.Protection(0) == PAGE_READONLY && region.Protection(3) == PAGE_READWRITE, "partial collect did not protect only its page"); + region.Write(0, 3); + region.Write(4, 4); + Require(region.Collect(Collection::Armed) == Pages(0, 1), "armed collect lost a write or consumed fresh pages"); + Require(region.Protection(4) == PAGE_READWRITE, "armed collect protected another fresh view"); + Require(region.Collect(Collection::All) == Pages(1, 16), "ordinary collect lost fresh pages"); + Require(region.Collect(Collection::All).empty(), "cleared memory reports writes"); +} + +void CheckFresh() { + Region region; + Require(region.Collect(Collection::All, 0, 1) == Pages(0, 1), "cannot collect initial page"); + region.Write(0, 7); + Require(region.Collect(Collection::Fresh) == Pages(1, 16), "fresh collect consumed an existing dirty page"); + Require(region.Collect(Collection::Fresh).empty(), "fresh collect repeated initial writes"); + Require(region.Collect(Collection::Armed) == Pages(0, 1), "fresh collect cleared an existing write"); + region.Write(9, 9); + Require(region.Collect(Collection::Armed) == Pages(9, 10), "write after activation was missed"); +} + +void CheckAliasesAndCapacity() { + Region region; + Require(region.Collect(Collection::All, 0, 1) == Pages(0, 1), "initial collect failed"); + Require(region.Protection(0, true) == PAGE_READONLY, "alias was not protected"); + Require(region.Collect(Collection::Armed, 0, 16, true, true).empty(), "alias protection was mistaken for collection history"); + Require(region.Collect(Collection::Fresh, 0, 16, true, true, 1) == Pages(0, 16), "small output buffers lost fresh pages"); + Require(region.Collect(Collection::Fresh, 0, 16, true, true).empty(), "small output buffers did not finish fresh collection"); + static_cast(region.alias)[0] = 8; + Require(region.Collect(Collection::Armed) == Pages(0, 1), "alias write was missed or fresh neighbors were consumed"); + Require(region.Collect(Collection::Fresh) == Pages(1, 16), "alias collection changed another view's history"); + Mappings::Get().Reset(region.first, Bytes); + Mappings::Get().Map(region.first, Bytes, region.section, 0, PAGE_READWRITE); + Require(region.Collect(Collection::Armed).empty(), "remapped address kept old collection history"); + Require(region.Collect(Collection::Fresh) == Pages(0, 16), "remapped memory was not fresh"); +} + +void CheckPinsAndHostWrites() { + Region region; + const auto address = reinterpret_cast(region.first); + Mappings::Get().Pin(address, 16384); + Require(region.Collect(Collection::Fresh) == Pages(0, 16), "pinned fresh collection failed"); + Require(region.Protection(0) == PAGE_READWRITE, "pinned page was protected"); + Require(region.Collect(Collection::Armed) == Pages(0, 4), "pinned writes were trusted as clean"); + Mappings::Get().Unpin(address, 16384); + Require(region.Collect(Collection::Armed) == Pages(0, 4), "unpin was not invalidated"); + Require(region.Collect(Collection::Armed).empty(), "unpin did not restore tracking"); + Require(Mappings::Get().BeginHostWrite(address, 16384), "host write refused"); + region.Write(0, 3); + region.Collect(Collection::Armed); + region.Write(0, 4); + Mappings::Get().EndHostWrite(address, 16384); + Require(region.Collect(Collection::Armed) == Pages(0, 4), "host write completion was missed"); + Require(region.Collect(Collection::Armed).empty(), "completed host write stayed dirty"); +} + +void CheckFailureAndHoles() { + Region region; + const auto address = reinterpret_cast(region.first); + DWORD previous; + Mappings::Get().SetProtection(address + 16384, 16384, PAGE_NOACCESS); + Require(VirtualProtect(region.first + 16384, 16384, PAGE_NOACCESS, &previous), "cannot protect test page"); + std::array pages{}; + auto count = pages.size(); + Require(!Mappings::Get().Collect(address, Bytes, pages.data(), &count, true, Collection::Fresh), "fresh inaccessible page accepted"); + Require(region.Protection(0) == PAGE_READWRITE, "failed collect changed an earlier page"); + Mappings::Get().SetProtection(address + 16384, 16384, PAGE_READWRITE); + Require(VirtualProtect(region.first + 16384, 16384, PAGE_READWRITE, &previous), "cannot restore test page"); + Mappings::Get().Reset(region.first + 16384, 16384); + Require(region.Collect(Collection::Fresh) == std::set({0, 1, 2, 3, 8, 9, 10, 11, 12, 13, 14, 15}), "fresh collection did not tolerate an unmapped block neighbor"); + Require(region.Collect(Collection::Fresh).empty(), "holes caused fresh pages to repeat"); +} + +void CheckPrivate() { + auto& mappings = Mappings::Get(); + auto* memory = static_cast(mappings.Reserve(nullptr, Bytes)); + Require(memory != nullptr, "private reservation failed"); + mappings.Commit(memory, Bytes, PAGE_READWRITE, Bytes, true); + const auto address = reinterpret_cast(memory); + std::array pages{}; + std::size_t count = pages.size(); + mappings.Collect(address, Bytes, pages.data(), &count, true); + static_cast(memory)[Small] = 3; + count = pages.size(); + Require(mappings.Collect(address, Bytes, pages.data(), &count, true, Collection::Fresh) && count == 0, "fresh collect touched private memory"); + count = pages.size(); + Require(mappings.Collect(address, Bytes, pages.data(), &count, true, Collection::Armed) && count == 1 && pages[0] == memory + Small, "private write watch changed"); + mappings.Reset(memory, Bytes); + VirtualFree(memory, 0, MEM_RELEASE); +} + +void CheckForgottenSectionKeepsNoPages() { + auto& mappings = Mappings::Get(); + HANDLE section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, Bytes, nullptr); + auto* first = static_cast(mappings.Reserve(nullptr, Bytes)); + auto* second = static_cast(mappings.Reserve(nullptr, Bytes)); + Require(section && first && second, "cannot allocate forgotten section test memory"); + const auto resident = [&](unsigned char* address) { + MEMORY_BASIC_INFORMATION info{}; + Require(VirtualQuery(address, &info, sizeof(info)) == sizeof(info), "resident query failed"); + return info.State == MEM_COMMIT && info.Type == MEM_PRIVATE; + }; + mappings.Map(first, Bytes, section, 0, PAGE_READWRITE, true, true); + mappings.Map(second, Bytes, section, 0, PAGE_READWRITE, true, true); + Require(!resident(first) && !resident(second), "a second mapping kept the first resident"); + mappings.ForgetSection(section); + mappings.Reset(first, Bytes); + mappings.Reset(second, Bytes); + mappings.Map(first, Bytes, section, 0, PAGE_READWRITE, true, true); + Require(resident(first), "a forgotten section's handle value kept its pages in the section"); + first[0] = 7; + mappings.ForgetSection(section); + mappings.Reset(first, Bytes); + mappings.Map(first, Bytes, section, 0, PAGE_READWRITE, true, true); + Require(resident(first) && first[0] == 0, "a forgotten section's handle value kept its unmapped bytes"); + mappings.Reset(first, Bytes); + VirtualFree(first, 0, MEM_RELEASE); + VirtualFree(second, 0, MEM_RELEASE); + CloseHandle(section); +} + +void CheckConcurrentWrites() { + Region region; + std::atomic start{false}; + std::thread writer([&] { + while (!start.load(std::memory_order_acquire)) std::this_thread::yield(); + for (std::size_t page = 0; page < 16; ++page) region.Write(page, static_cast(page + 1)); + }); + start.store(true, std::memory_order_release); + region.Collect(Collection::Fresh); + writer.join(); + for (std::size_t page = 0; page < 16; ++page) Require(region.first[page * Small] == page + 1, "activation lost a concurrent write"); + region.Collect(Collection::Armed); + Require(region.Collect(Collection::Armed).empty(), "concurrent writes did not settle"); +} +} + +int main() { + auto* handler = AddVectoredExceptionHandler(1, HandleWrite); + try { + Require(handler != nullptr, "cannot install test handler"); + CheckArmed(); + CheckFresh(); + CheckAliasesAndCapacity(); + CheckPinsAndHostWrites(); + CheckFailureAndHoles(); + CheckPrivate(); + CheckConcurrentWrites(); + CheckForgottenSectionKeepsNoPages(); + std::cout << "Windows deferred write-watch tests passed\n"; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } + RemoveVectoredExceptionHandler(handler); +} diff --git a/core/libs/tests/WriteWatch.cpp b/core/libs/tests/WriteWatch.cpp new file mode 100644 index 0000000000..0588e469d0 --- /dev/null +++ b/core/libs/tests/WriteWatch.cpp @@ -0,0 +1,306 @@ +#include "prx/libc/include/GuestWriteWatch.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#if !defined(__APPLE__) +#error The page protection write watch is the macOS backend +#endif + +using namespace GuestWriteWatch; + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +struct Region { + explicit Region(std::size_t pageCount) : page(static_cast(::getpagesize())), bytes(pageCount * page) { + base = static_cast(::mmap(nullptr, bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0)); + Require(base != MAP_FAILED, "cannot map the test region"); + } + ~Region() { + GuestWriteWatchUnregister_nid_postfix(base, bytes); + ::munmap(base, bytes); + } + std::uintptr_t Address(std::size_t index) const { + return reinterpret_cast(base) + index * page; + } + using Collector = bool (*)(std::uintptr_t, std::size_t, void (*)(void*, std::uintptr_t, std::uintptr_t), void*); + std::set Collect(Collector collector = &GuestWriteWatchCollect_nid_postfix, std::size_t first = 0, std::size_t count = 0) { + std::set pages; + struct Sink { + Region* region; + std::set* pages; + } sink{this, &pages}; + const bool complete = collector(Address(first), count == 0 ? bytes - first * page : count * page, [](void* context, std::uintptr_t begin, std::uintptr_t end) { + auto& target = *static_cast(context); + for (auto at = begin; at < end; at += target.region->page) target.pages->insert((at - target.region->Address(0)) / target.region->page); + }, &sink); + Require(complete, "collection of a watched range is incomplete"); + return pages; + } + std::size_t page; + std::size_t bytes; + std::uint8_t* base = nullptr; +}; + +std::set All(std::size_t count) { + std::set pages; + for (std::size_t index = 0; index < count; ++index) pages.insert(index); + return pages; +} + +void CheckCollect() { + Region region(8); + Require(GuestWriteWatchAvailable_nid_postfix(), "write watch is unavailable"); + Require(!GuestWriteWatchCovers_nid_postfix(region.Address(0), region.bytes), "unregistered range is covered"); + GuestWriteWatchRegister_nid_postfix(region.base, region.bytes); + Require(GuestWriteWatchCovers_nid_postfix(region.Address(0), region.bytes), "registered range is not covered"); + Require(region.Collect() == All(8), "a new range is not reported in full"); + Require(region.Collect().empty(), "an untouched range reports writes"); + region.base[3 * region.page + 17] = 1; + region.base[5 * region.page] = 2; + region.base[5 * region.page + 9] = 3; + Require(region.Collect() == std::set({3, 5}), "written pages are not reported exactly"); + Require(region.Collect().empty(), "collected writes are reported again"); + Require(region.base[3 * region.page + 17] == 1 && region.base[5 * region.page + 9] == 3, "watched writes were lost"); + std::memset(region.base + region.page - 4, 7, 8); + Require(region.Collect() == std::set({0, 1}), "a write across a page boundary is not reported on both pages"); +} + +void CheckFresh() { + Region region(8); + GuestWriteWatchRegister_nid_postfix(region.base, region.bytes); + Require(region.Collect(&GuestWriteWatchCollect_nid_postfix, 0, 4) == All(4), "the collected half is not reported in full"); + region.base[region.page] = 1; + Require(region.Collect(&GuestWriteWatchCollectFresh_nid_postfix) == std::set({4, 5, 6, 7}), "a fresh collection does not report exactly the never collected pages"); + Require(region.Collect(&GuestWriteWatchCollectFresh_nid_postfix).empty(), "a fresh collection reports pages again"); + region.base[6 * region.page] = 1; + Require(region.Collect() == std::set({1, 6}), "a fresh collection does not arm the pages it reports"); + Require(region.Collect().empty(), "collected writes are reported again"); +} + +void CheckArmed() { + Region region(8); + GuestWriteWatchRegister_nid_postfix(region.base, region.bytes); + Require(region.Collect(&GuestWriteWatchCollect_nid_postfix, 0, 4) == All(4), "the collected half is not reported in full"); + region.base[region.page] = 1; + region.base[6 * region.page] = 1; + Require(region.Collect(&GuestWriteWatchCollectArmed_nid_postfix) == std::set({1}), "an armed collection does not report exactly the written armed pages"); + Require(region.Collect() == std::set({4, 5, 6, 7}), "an armed collection takes never collected pages"); + Require(region.Collect().empty(), "collected writes are reported again"); +} + +void CheckRegisterAgain() { + Region region(4); + GuestWriteWatchRegister_nid_postfix(region.base, region.bytes); + Require(region.Collect() == All(4), "a new range is not reported in full"); + GuestWriteWatchRegister_nid_postfix(region.base, region.bytes); + Require(region.Collect() == All(4), "a range registered again is not reported in full"); + region.base[2 * region.page] = 1; + Require(region.Collect() == std::set({2}), "a range registered while protected stops reporting writes"); + GuestWriteWatchRegister_nid_postfix(region.base + region.page, region.page); + Require(region.Collect() == std::set({1}), "registering part of a range does not report that part alone"); + region.base[0] = 1; + region.base[3 * region.page] = 1; + Require(region.Collect() == std::set({0, 3}), "the rest of a split range stops reporting writes"); +} + +int KernelProtection(const void* page) { + mach_vm_address_t address = reinterpret_cast(page); + 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; + Require(mach_vm_region(mach_task_self(), &address, &size, VM_REGION_BASIC_INFO_64, reinterpret_cast(&info), &count, &object) == KERN_SUCCESS && address == reinterpret_cast(page), "cannot read the kernel protection of a page"); + return ((info.protection & VM_PROT_READ) ? PROT_READ : 0) | ((info.protection & VM_PROT_WRITE) ? PROT_WRITE : 0) | ((info.protection & VM_PROT_EXECUTE) ? PROT_EXEC : 0); +} + +void CheckRegisterMapped() { + Region region(4); + std::uintptr_t runEnd = 0; + Require(::mprotect(region.base, region.bytes, PROT_NONE) == 0, "cannot reserve the test region"); + GuestWriteWatchRegister_nid_postfix(region.base, region.bytes); + Require(::mmap(region.base, region.bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON | MAP_FIXED, -1, 0) == region.base, "cannot map over the reservation"); + GuestWriteWatchRegister_nid_postfix(region.base, region.bytes); + Require(GuestWriteWatchProtection_nid_postfix(region.Address(0), region.Address(4), &runEnd) == (PROT_READ | PROT_WRITE) && runEnd == region.Address(4), "a range mapped over its reservation keeps the reservation's protection"); + Require(region.Collect() == All(4), "a mapped range is not reported in full"); + region.base[2 * region.page] = 1; + Require(region.Collect() == std::set({2}), "a range mapped over its reservation stops reporting writes"); + Require(::mmap(region.base + region.page, region.page, PROT_NONE, MAP_PRIVATE | MAP_ANON | MAP_FIXED, -1, 0) == region.base + region.page, "cannot reserve a watched page again"); + GuestWriteWatchRegister_nid_postfix(region.base + region.page, region.page); + Require(GuestWriteWatchProtection_nid_postfix(region.Address(1), region.Address(2), &runEnd) == PROT_NONE, "a protected page reserved again reports its old protection"); + Require(KernelProtection(region.base + region.page) == PROT_NONE, "registering a protected page reserved again restored its old protection"); +} + +void CheckHostWrite() { + Region region(4); + GuestWriteWatchRegister_nid_postfix(region.base, region.bytes); + Require(region.Collect() == All(4), "a new range is not reported in full"); + int descriptors[2]; + Require(::pipe(descriptors) == 0, "cannot create a pipe"); + const std::array payload{'w', 'a', 't', 'c', 'h', 'e', 'd', '!'}; + Require(::write(descriptors[1], payload.data(), payload.size()) == static_cast(payload.size()), "cannot fill the pipe"); + errno = 0; + Require(::read(descriptors[0], region.base + region.page, payload.size()) == -1 && errno == EFAULT, "a system call wrote a protected page without a host write"); + GuestWriteWatchBeginHostWrite_nid_postfix(region.base + region.page, payload.size()); + Require(region.Collect() == std::set({1}), "a page under a host write is not reported"); + Require(::read(descriptors[0], region.base + region.page, payload.size()) == static_cast(payload.size()), "a collection protected a page during a host write"); + GuestWriteWatchEndHostWrite_nid_postfix(region.base + region.page, payload.size()); + Require(std::memcmp(region.base + region.page, payload.data(), payload.size()) == 0, "the host write did not reach the page"); + Require(region.Collect() == std::set({1}), "a host write is not reported after it ends"); + Require(region.Collect().empty(), "a finished host write is reported again"); + ::close(descriptors[0]); + ::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; + Require(GuestWriteWatchProtection_nid_postfix(region.Address(0), region.Address(6), &runEnd) == -1 && runEnd == region.Address(6), "an unwatched range reports a protection"); + GuestWriteWatchRegister_nid_postfix(region.base + 2 * region.page, 3 * region.page); + Require(GuestWriteWatchProtection_nid_postfix(region.Address(0), region.Address(6), &runEnd) == -1 && runEnd == region.Address(2), "the run before a watched range does not end at it"); + static_cast(GuestWriteWatchCollect_nid_postfix(region.Address(2), 3 * region.page, [](void*, std::uintptr_t, std::uintptr_t) {}, nullptr)); + Require(GuestWriteWatchProtection_nid_postfix(region.Address(2), region.Address(6), &runEnd) == (PROT_READ | PROT_WRITE) && runEnd == region.Address(5), "a protected page does not report the protection it was given"); + Require(::mprotect(region.base + 3 * region.page, region.page, PROT_READ) == 0, "cannot change the protection"); + GuestWriteWatchProtectionChanged_nid_postfix(region.base + 3 * region.page, region.page, PROT_READ); + Require(GuestWriteWatchProtection_nid_postfix(region.Address(2), region.Address(6), &runEnd) == (PROT_READ | PROT_WRITE) && runEnd == region.Address(3), "a run does not end where the protection changes"); + Require(GuestWriteWatchProtection_nid_postfix(region.Address(3), region.Address(6), &runEnd) == PROT_READ && runEnd == region.Address(4), "a changed protection is not reported"); + std::set pages; + static_cast(GuestWriteWatchCollect_nid_postfix(region.Address(2), 3 * region.page, [](void* context, std::uintptr_t begin, std::uintptr_t) { static_cast*>(context)->insert(begin); }, &pages)); + Require(pages == std::set({region.Address(3)}), "a protection change is not reported as a write"); + Require(::mprotect(region.base + 3 * region.page, region.page, PROT_READ | PROT_WRITE) == 0, "cannot restore the protection"); + GuestWriteWatchProtectionChanged_nid_postfix(region.base + 3 * region.page, region.page, PROT_READ | PROT_WRITE); + region.base[3 * region.page] = 1; + GuestWriteWatchUnregister_nid_postfix(region.base + 2 * region.page, 3 * region.page); + region.base[2 * region.page] = 1; + region.base[4 * region.page] = 1; + 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; + constexpr int Rounds = 50; + Region region(Threads * PagesPerThread); + GuestWriteWatchRegister_nid_postfix(region.base, region.bytes); + Require(region.Collect() == All(Threads * PagesPerThread), "a new range is not reported in full"); + std::atomic start{false}; + std::atomic finished{0}; + std::array writers; + for (std::size_t thread = 0; thread < Threads; ++thread) { + writers[thread] = std::thread([&, thread] { + while (!start.load()) std::this_thread::yield(); + for (int round = 1; round <= Rounds; ++round) + for (std::size_t index = 0; index < PagesPerThread; ++index) region.base[(thread * PagesPerThread + index) * region.page] = static_cast(round); + finished.fetch_add(1); + }); + } + std::vector seen(Threads * PagesPerThread, 0); + const auto take = [&] { + for (const auto index : region.Collect()) seen[index] = region.base[index * region.page]; + }; + start.store(true); + while (finished.load() != Threads) { + take(); + std::this_thread::yield(); + } + for (auto& writer : writers) writer.join(); + take(); + for (std::size_t index = 0; index < seen.size(); ++index) + Require(seen[index] == static_cast(Rounds), "a write that raced a collection was not reported"); + Require(region.Collect().empty(), "writes are reported after the writers stopped"); +} + +} + +int main(int argc, char** argv) { + try { + 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(); + else if (mode == "armed") CheckArmed(); + 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]); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 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/shared_memory/CMakeLists.txt b/core/libs/tests/shared_memory/CMakeLists.txt new file mode 100644 index 0000000000..f9e85f8dec --- /dev/null +++ b/core/libs/tests/shared_memory/CMakeLists.txt @@ -0,0 +1,18 @@ +cmake_minimum_required(VERSION 3.22) +project(SharedMemoryTests LANGUAGES CXX) +enable_testing() +if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux") + message(FATAL_ERROR "Standalone shared-memory tests require Linux") +endif() +set(LIBS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../..") +add_executable(guest_named_shm_open_tests + ../GuestNamedShmOpen.cpp + Support.cpp + ${LIBS_DIR}/prx/libkernel/File/src/SharedMemory.cpp + ${LIBS_DIR}/prx/libc/src/CreationMask.cpp + ${LIBS_DIR}/nid/src/Sha1.cpp) +target_include_directories(guest_named_shm_open_tests PRIVATE ${LIBS_DIR} ${LIBS_DIR}/nid/include) +target_compile_features(guest_named_shm_open_tests PRIVATE cxx_std_20) +target_link_libraries(guest_named_shm_open_tests PRIVATE rt) +add_test(NAME guest_named_shm_open COMMAND guest_named_shm_open_tests) +set_tests_properties(guest_named_shm_open PROPERTIES TIMEOUT 20) diff --git a/core/libs/tests/shared_memory/Support.cpp b/core/libs/tests/shared_memory/Support.cpp new file mode 100644 index 0000000000..e9df457817 --- /dev/null +++ b/core/libs/tests/shared_memory/Support.cpp @@ -0,0 +1,11 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include + +extern "C" int* APS5_VABI __error_nid_postfix() { + static thread_local int error; + return &error; +} + +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 new file mode 100644 index 0000000000..ba692f336c --- /dev/null +++ b/core/libs/tests/write_watch/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.20) + +if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + project(WriteWatchTests LANGUAGES CXX) + enable_testing() + set(ANYPS5_TEST_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tests") +endif() + +find_package(Threads REQUIRED) +add_executable(write_watch_tests ../WriteWatch.cpp ../../prx/libc/src/GuestWriteWatch.cpp) +target_include_directories(write_watch_tests PRIVATE ../..) +target_compile_features(write_watch_tests PRIVATE cxx_std_20) +target_link_libraries(write_watch_tests PRIVATE Threads::Threads) +set_target_properties(write_watch_tests PROPERTIES + CXX_EXTENSIONS OFF + RUNTIME_OUTPUT_DIRECTORY "$<1:${ANYPS5_TEST_OUTPUT_DIRECTORY}>") + +foreach(mode collect fresh armed register mapped host empty protection 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 7c66d2088d..fa78fbaa42 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -1,7 +1,6 @@ -add_executable(relinker - ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/CliArgs.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/Autorun.cpp +option(ANYPS5_RELINKER_CLI "Build the native relinker command line" ON) + +add_library(relinker_core STATIC ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/linux/LinuxElfPatcher.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp @@ -31,8 +30,8 @@ add_executable(relinker ${CMAKE_CURRENT_SOURCE_DIR}/io/src/ByteWriter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/io/src/ByteReader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/io/src/BufferUtils.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/io/src/FileReader.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/io/src/FileWriter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/NativePath.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/FileIdentity.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/Amd64OnlyConverter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/InstructionScanner.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp @@ -50,10 +49,16 @@ add_executable(relinker ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ClzeroLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ReciprocalOperands.cpp ${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 @@ -66,10 +71,14 @@ add_executable(relinker ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/ValidationPolicy.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/output/SysVDynamicSectionBuilder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/output/CallRegistryWriter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/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 PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/cli/include +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 ${CMAKE_CURRENT_SOURCE_DIR}/io/include @@ -77,6 +86,64 @@ target_include_directories(relinker PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include ) +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) + target_compile_options(relinker_core PRIVATE -fexceptions) + add_subdirectory(wasm) +endif() + +if(NOT ANYPS5_RELINKER_CLI) + return() +endif() + +add_executable(relinker + ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/CliArgs.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/Autorun.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/linux/LinuxDesktopEntryWriter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/FileReader.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/FileWriter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/NativeModules.cpp) +target_link_libraries(relinker PRIVATE relinker_core) +target_include_directories(relinker PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/cli/include ${CMAKE_CURRENT_SOURCE_DIR}/../libs) +set_target_properties(relinker PROPERTIES CXX_EXTENSIONS OFF) + +if(BUILD_TESTING AND WIN32) + add_test_executable(windows_guest_export_alias_tests ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/WindowsExportAliasTests.cpp) + target_link_libraries(windows_guest_export_alias_tests PRIVATE relinker_core) + add_test(NAME windows_guest_export_aliases COMMAND windows_guest_export_alias_tests) +endif() + +if(WIN32) + add_library(relinker_resources OBJECT ${CMAKE_CURRENT_SOURCE_DIR}/relinker.rc) + set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/relinker.rc PROPERTIES OBJECT_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/relinker.manifest) + target_sources(relinker PRIVATE $) + target_compile_options(relinker PRIVATE -municode) + target_link_options(relinker PRIVATE -municode) +endif() + if(WIN32) add_test_executable(windows_dependency_diagnostics_tests EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp @@ -118,6 +185,10 @@ if(WIN32) ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ClzeroLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ReciprocalOperands.cpp ${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) @@ -128,17 +199,98 @@ endif() if(BUILD_TESTING) + add_test_executable(elf_reader_bounds_tests + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/ElfReaderBoundsTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/parsing/ElfReader.cpp) + target_include_directories(elf_reader_bounds_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include) + add_test(NAME elf_reader_bounds COMMAND elf_reader_bounds_tests) + set_tests_properties(elf_reader_bounds PROPERTIES TIMEOUT 20) + + add_test_executable(call_site_index_tests + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/CallSiteIndexTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/CallSiteResolver.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/InstructionScanner.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp + ) + target_include_directories(call_site_index_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include + ${CMAKE_CURRENT_SOURCE_DIR}/io/include + ) + add_test(NAME call_site_index COMMAND call_site_index_tests) + set_tests_properties(call_site_index PROPERTIES TIMEOUT 20) + + add_test_executable(sdk_mman_syscall_tests + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/SdkMmanSyscallTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/SyscallScanner.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/InstructionScanner.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/DecodedInstruction.cpp) + target_include_directories(sdk_mman_syscall_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include) + 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) - target_include_directories(autorun_exit_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/cli/include) + ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/Autorun.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/NativePath.cpp) + target_include_directories(autorun_exit_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/cli/include ${CMAKE_CURRENT_SOURCE_DIR}/io/include ${CMAKE_CURRENT_SOURCE_DIR}/domain/include) add_test(NAME autorun_exit_status COMMAND autorun_exit_tests) set_tests_properties(autorun_exit_status PROPERTIES TIMEOUT 20) + add_test_executable(rewrite_boundaries_tests + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/tests/RewriteBoundaryTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionRewriter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/InstructionScanner.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp + ) + target_include_directories(rewrite_boundaries_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include + ${CMAKE_CURRENT_SOURCE_DIR}/io/include + ) + add_test(NAME rewrite_boundaries COMMAND rewrite_boundaries_tests) + set_tests_properties(rewrite_boundaries PROPERTIES TIMEOUT 20) + add_test_executable(cli_args_tests ${CMAKE_CURRENT_SOURCE_DIR}/cli/tests/CliArgsTests.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/CliArgs.cpp) - target_include_directories(cli_args_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/cli/include) + ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/CliArgs.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/NativePath.cpp) + target_include_directories(cli_args_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/cli/include + ${CMAKE_CURRENT_SOURCE_DIR}/io/include + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ) add_test(NAME cli_args COMMAND cli_args_tests) + add_test_executable(program_layout_ranges_tests + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/ProgramLayoutRangeTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/ByteWriter.cpp + ) + target_include_directories(program_layout_ranges_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include + ${CMAKE_CURRENT_SOURCE_DIR}/io/include + ) + add_test(NAME program_layout_ranges COMMAND program_layout_ranges_tests) + set_tests_properties(program_layout_ranges PROPERTIES TIMEOUT 20) + add_test_executable(buffer_bounds_tests ${CMAKE_CURRENT_SOURCE_DIR}/io/tests/BufferBoundsTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/io/src/ByteReader.cpp @@ -148,10 +300,26 @@ 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) + target_include_directories(param_json_utf8_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../libs) + add_test(NAME param_json_utf8 COMMAND param_json_utf8_tests) + set_tests_properties(param_json_utf8 PROPERTIES TIMEOUT 20) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") add_test_executable(file_writer_tests ${CMAKE_CURRENT_SOURCE_DIR}/io/tests/FileWriterTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/io/src/FileWriter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/NativePath.cpp ) target_include_directories(file_writer_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/domain/include @@ -159,8 +327,58 @@ if(BUILD_TESTING) ) add_test(NAME file_writer_binary COMMAND file_writer_tests binary) add_test(NAME file_writer_text COMMAND file_writer_tests text) + + add_test_executable(linux_desktop_entry_tests + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/LinuxDesktopEntryTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/linux/LinuxDesktopEntryWriter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/NativePath.cpp + ) + target_include_directories(linux_desktop_entry_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include + ${CMAKE_CURRENT_SOURCE_DIR}/io/include + ${CMAKE_CURRENT_SOURCE_DIR}/../libs + ) + add_test(NAME linux_desktop_entry COMMAND linux_desktop_entry_tests) endif() + add_test_executable(section_header_ranges_tests + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/SectionRangeTests.cpp + ${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 + ) + target_include_directories(section_header_ranges_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include + ${CMAKE_CURRENT_SOURCE_DIR}/io/include + ) + 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 + ) + 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 + ) + 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 @@ -180,6 +398,10 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ClzeroLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ReciprocalOperands.cpp ${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 @@ -194,60 +416,185 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include ${CMAKE_CURRENT_SOURCE_DIR}/io/include ) + if(APPLE AND (CMAKE_OSX_ARCHITECTURES STREQUAL "x86_64" OR (NOT CMAKE_OSX_ARCHITECTURES AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64"))) + target_link_options(amd64_only_converter_tests PRIVATE "LINKER:-pagezero_size,0x10000") + endif() add_test(NAME amd64_only_converter COMMAND amd64_only_converter_tests) + add_test_executable(call_site_resolver_tests + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/CallSiteResolverTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/CallSiteResolver.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/InstructionScanner.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp + ) + target_include_directories(call_site_resolver_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include + ) + add_test(NAME call_site_resolver COMMAND call_site_resolver_tests) + add_test_executable(decoder_length_tests + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/tests/DecoderLengthTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp + ) + target_include_directories(decoder_length_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include + ) + add_test(NAME decoder_length COMMAND decoder_length_tests) find_package(Python3 QUIET COMPONENTS Interpreter) if(Python3_Interpreter_FOUND) + add_test(NAME cli_help COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/cli/tests/test_help.py $) + add_test(NAME windows_fixed_base COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_fixed_base.py $) + set_tests_properties(windows_fixed_base PROPERTIES TIMEOUT 20) + add_test(NAME 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} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_conversion_diagnostics.py $) + set_tests_properties(conversion_diagnostics PROPERTIES TIMEOUT 20) add_test(NAME dynamic_segment COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_dynamic_segment.py $) add_test(NAME rela_size COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_rela_size.py $) + add_test(NAME none_relocations COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_none_relocations.py $) + 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 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 tls_function_coverage COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_tls_function_coverage.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}) + endforeach() + 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}) + set_tests_properties(tls_function_coverage_${group} PROPERTIES TIMEOUT 30 LABELS tls_function_coverage) + 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} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_eh_frame_header.py $) add_test(NAME linux_dynamic_strings COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_dynamic_strings.py $) + add_test(NAME guest_runpath COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_runpath.py $) add_test(NAME executable_segments COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_executable_segments.py $) + add_test(NAME linux_executable_exports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_executable_exports.py $) + add_test(NAME executable_tls_relocations COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_executable_tls_relocations.py $) + add_test(NAME 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} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_fixed_base.py $) add_test(NAME linux_program_header_order COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_program_header_order.py $) + add_test(NAME linux_input_program_headers COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_input_program_headers.py $) add_test(NAME windows_gui COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_gui.py $) + add_test(NAME windows_working_directory COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_working_directory.py $) + add_test(NAME windows_runpath COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_runpath.py $) + set_tests_properties(windows_runpath PROPERTIES TIMEOUT 30) + add_test(NAME windows_trace_imports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_trace_imports.py $) + add_test(NAME 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} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_desktop_entry_output.py $) add_test(NAME guest_intel_trampolines COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_intel_trampolines.py $) + add_test(NAME guest_syscall_trap COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_syscall_trap.py $) add_test(NAME guest_tls COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_tls.py $) add_test(NAME guest_needed_modules COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_needed_modules.py $) + add_test(NAME guest_module_dir COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_module_dir.py $) add_test(NAME guest_module_directories COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_module_directories.py $) + add_test(NAME relinker_executables COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_executables.py $) + add_test(NAME 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} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_guest_stack.py $) add_test(NAME guest_symbol_names COMMAND ${Python3_EXECUTABLE} ${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_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 @@ -255,25 +602,147 @@ 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) + add_library(windows_stdio_host_ucrt SHARED elfpatcher/tests/WindowsStdioHost.cpp) + target_compile_options(windows_stdio_host_ucrt PRIVATE -mcrtdll=ucrt) + target_link_options(windows_stdio_host_ucrt PRIVATE -mcrtdll=ucrt -static-libgcc -static-libstdc++) + set_target_properties(windows_stdio_host_ucrt PROPERTIES PREFIX "" WINDOWS_EXPORT_ALL_SYMBOLS ON) + list(APPEND windows_stdio_host_arguments $) + + get_filename_component(testCompilerDirectory "${CMAKE_CXX_COMPILER}" DIRECTORY) + set(mingwDlltoolExecutable "${testCompilerDirectory}/dlltool.exe") + if(NOT EXISTS "${mingwDlltoolExecutable}") + message(FATAL_ERROR "Cannot locate MinGW DLL tool: ${mingwDlltoolExecutable}") + endif() + set(msvcrtImportLibrary "${CMAKE_CURRENT_BINARY_DIR}/windows_stdio_msvcrt_import.a") + add_custom_command(OUTPUT "${msvcrtImportLibrary}" + COMMAND "${mingwDlltoolExecutable}" -d "${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/WindowsMsvcrt.def" + -l "${msvcrtImportLibrary}" -D msvcrt.dll + DEPENDS elfpatcher/tests/WindowsMsvcrt.def + VERBATIM) + add_custom_target(windows_stdio_msvcrt_import DEPENDS "${msvcrtImportLibrary}") + add_library(windows_stdio_host_msvcrt SHARED elfpatcher/tests/WindowsMsvcrtStdioHost.cpp) + add_dependencies(windows_stdio_host_msvcrt windows_stdio_msvcrt_import) + target_link_libraries(windows_stdio_host_msvcrt PRIVATE "${msvcrtImportLibrary}") + target_compile_options(windows_stdio_host_msvcrt PRIVATE -fno-builtin) + target_link_options(windows_stdio_host_msvcrt PRIVATE -nostdlib -Wl,--no-undefined -Wl,--entry,0) + set_target_properties(windows_stdio_host_msvcrt PROPERTIES PREFIX "" WINDOWS_EXPORT_ALL_SYMBOLS ON) + list(APPEND windows_stdio_host_arguments $) + else() + add_library(windows_stdio_host SHARED elfpatcher/tests/WindowsStdioHost.cpp) + set_target_properties(windows_stdio_host PROPERTIES PREFIX "" WINDOWS_EXPORT_ALL_SYMBOLS ON) + list(APPEND windows_stdio_host_arguments $) + endif() + endif() add_test(NAME windows_import_modules COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_import_modules.py $) + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_import_modules.py + $ ${windows_stdio_host_arguments}) add_test(NAME elf_header COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_elf_header.py $) + add_test(NAME weak_imports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_weak_imports.py $) + add_test(NAME 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} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_null_symbol_relocations.py $) + add_test(NAME input_nonascii COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_nonascii.py $) + add_test(NAME long_paths COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_long_paths.py $) + set_tests_properties(long_paths PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME string_table_bounds COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_string_table_bounds.py $) + add_test(NAME sce_code_extent COMMAND ${Python3_EXECUTABLE} + ${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} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_jmprel_table_bounds.py $) + if(WIN32) + add_test(NAME windows_diagnostic_fixture_isolation COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/test_windows_diagnostic_fixture_isolation.py $) + set_tests_properties(windows_diagnostic_fixture_isolation PROPERTIES TIMEOUT 30) + endif() + add_test(NAME entry_point_bounds COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_entry_point_bounds.py $) + add_test(NAME init_array_roots COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_init_array_roots.py $) + add_test(NAME gnu_hash_analysis COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_gnu_hash_analysis.py $) + add_test(NAME guest_gnu_hash COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_gnu_hash.py $) + set_tests_properties(guest_gnu_hash PROPERTIES TIMEOUT 20) + add_test(NAME to_rosetta COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_to_rosetta.py $) + add_test(NAME syscall_number COMMAND ${Python3_EXECUTABLE} + ${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() + add_test_executable(unwind_record_bounds_tests + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/UnwindRecordBoundsTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/BufferUtils.cpp + ) + target_include_directories(unwind_record_bounds_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include + ${CMAKE_CURRENT_SOURCE_DIR}/io/include + ) + add_test(NAME unwind_record_bounds COMMAND unwind_record_bounds_tests) + set_tests_properties(unwind_record_bounds PROPERTIES TIMEOUT 20) + add_test_executable(windows_icon_resource_tests ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/WindowsIconResourceTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp @@ -287,6 +756,29 @@ if(BUILD_TESTING) ) add_test(NAME windows_icon_resource COMMAND windows_icon_resource_tests) + 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 + ) + target_include_directories(control_flow_graph_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include + ) + add_test(NAME control_flow_graph COMMAND control_flow_graph_tests) + set_tests_properties(control_flow_graph PROPERTIES TIMEOUT 20) + add_test_executable(strict_nid_filter_tests ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/StrictNidFilterTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp @@ -322,6 +814,11 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/RelaBoundsTests.cpp ) add_test(NAME rela_bounds COMMAND rela_bounds_tests $) + + add_test_executable(syscall_scanner_tests + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/SyscallScannerTests.cpp + ) + add_test(NAME syscall_scanner COMMAND syscall_scanner_tests $) add_test_executable(guest_intel_trampolines_tests ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/GuestIntelTrampolinesTests.cpp ) @@ -337,4 +834,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 b80e4de9c4..cb379408f0 100644 --- a/core/relinker/cli/include/Cli.hpp +++ b/core/relinker/cli/include/Cli.hpp @@ -3,14 +3,19 @@ #include #include +#include #include +#include namespace Cli { struct Args { + bool showHelp = false; bool skipSyscallCheck = false; + bool trapSdkSyscallStubs = false; bool skipSceModule = false; bool toIntel = false; + bool toRosetta = false; bool writeRegistry = false; bool toWindows = false; bool toMacos = false; @@ -18,15 +23,28 @@ struct Args { bool autorun = false; bool windowsDiagnostics = false; bool windowsGui = false; + 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; + std::vector executables; }; Args ParseArgs(int argc, char* argv[]); +const char* Usage(); + int Autorun(const std::string& absPath, bool toWindows); } diff --git a/core/relinker/cli/include/ConsoleProgress.hpp b/core/relinker/cli/include/ConsoleProgress.hpp new file mode 100644 index 0000000000..a05bf67795 --- /dev/null +++ b/core/relinker/cli/include/ConsoleProgress.hpp @@ -0,0 +1,112 @@ +#ifndef CLI_CONSOLEPROGRESS_HPP +#define CLI_CONSOLEPROGRESS_HPP + +#include +#include +#include +#include +#include +#include +#include + +namespace Cli { + +class ConsoleProgress final : public Domain::IProgress { +public: + explicit ConsoleProgress(std::ostream& stream) : stream(stream) {} + + void IncludeFile(std::string_view path) { + const auto separator = path.find_last_of("/\\"); + const auto name = path.substr(separator == std::string_view::npos ? 0 : separator + 1); + nameWidth = std::max(nameWidth, textWidth(name)); + } + + void Report(const Domain::ProgressEvent& event) override { + if (event.Completed > event.Total) throw std::logic_error("Invalid progress counters"); + const char* label = nullptr; + switch (event.Stage) { + case Domain::ProgressStage::Executable: label = "Executable"; break; + case Domain::ProgressStage::Modules: label = "PRX conversion"; break; + case Domain::ProgressStage::Syscalls: label = "Syscall scan"; break; + case Domain::ProgressStage::Decode: label = "Instruction scan"; break; + case Domain::ProgressStage::Lowering: label = "Instruction conversion"; break; + case Domain::ProgressStage::Imports: label = "Import analysis"; break; + case Domain::ProgressStage::Image: label = "Output image"; break; + case Domain::ProgressStage::Write: label = "Write output"; break; + default: throw std::logic_error("Unknown progress stage"); + } + const auto separator = event.Input.find_last_of("/\\"); + const auto name = event.Input.substr(separator == std::string::npos ? 0 : separator + 1); + if (event.Stage == Domain::ProgressStage::Executable) { + if (event.Completed == 0) Message("Converting " + name + "\n"); + else Finish(); + return; + } + if (event.Stage == Domain::ProgressStage::Imports) { + if (event.Completed == 0) { + Finish(); + stream << '\n'; + Message("Import analysis\n"); + } + else Finish(); + return; + } + const auto percent = event.Total == 0 ? 100u : static_cast(100.0L * event.Completed / event.Total); + const bool changed = lastStage != event.Stage || lastInput != event.Input; + const bool complete = event.Completed == event.Total; + const auto now = std::chrono::steady_clock::now(); + if (!changed && lastPercent == percent && event.Completed != 0) return; + if (!changed && !complete && now - lastUpdate < std::chrono::milliseconds(80)) return; + if (changed || messages) Finish(); + std::ostringstream line; + line << std::left << std::setw(22) << label; + line << " | " << name << std::string(nameWidth > textWidth(name) ? nameWidth - textWidth(name) : 0, ' '); + line << " | " << std::right << std::setw(3) << percent << '%'; + if (event.Stage == Domain::ProgressStage::Modules) line << " (" << event.Completed << '/' << event.Total << ')'; + const auto text = line.str(); + stream << '\r' << text; + if (lineWidth > text.size()) stream << std::string(lineWidth - text.size(), ' ') << '\r' << text; + stream << std::flush; + lineWidth = text.size(); + active = true; + lastStage = event.Stage; + lastInput = event.Input; + lastPercent = percent; + lastUpdate = now; + if (complete) Finish(); + } + + void Message(std::string_view message) override { + if (active) Finish(); + stream << message; + if (!message.empty() && message.back() != '\n') stream << '\n'; + stream << std::flush; + messages = true; + } + + void Finish() { + if (active || messages) stream << '\n' << std::flush; + active = false; + messages = false; + lineWidth = 0; + } + +private: + static std::size_t textWidth(std::string_view text) { + return static_cast(std::count_if(text.begin(), text.end(), [](unsigned char character) { return (character & 0xc0) != 0x80; })); + } + + std::size_t nameWidth = 0; + std::ostream& stream; + Domain::ProgressStage lastStage = Domain::ProgressStage::Write; + std::string lastInput; + unsigned lastPercent = 101; + std::size_t lineWidth = 0; + bool active = false; + bool messages = false; + std::chrono::steady_clock::time_point lastUpdate{}; +}; + +} + +#endif diff --git a/core/relinker/cli/include/NativeModules.hpp b/core/relinker/cli/include/NativeModules.hpp new file mode 100644 index 0000000000..66139266ca --- /dev/null +++ b/core/relinker/cli/include/NativeModules.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include + +namespace Cli { + +struct NativeModules { + std::filesystem::path Root; + std::vector RuntimeDirectories; + 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/include/RuntimeLayout.hpp b/core/relinker/cli/include/RuntimeLayout.hpp new file mode 100644 index 0000000000..6ba34c3932 --- /dev/null +++ b/core/relinker/cli/include/RuntimeLayout.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include +#include + +namespace Cli { + +class RuntimeLayout { +public: + static void Print(std::ostream& stream, const std::string& executable, const std::vector& modules, const std::vector& extraExecutables = {}) { + Node root; + for (const auto& module : modules) { + auto* node = &root; + std::size_t start = 0; + for (;;) { + const auto end = module.find('/', start); + const auto name = module.substr(start, end == std::string::npos ? end : end - start); + node = &node->children[name]; + if (end == std::string::npos) break; + start = end + 1; + } + } + stream << "Arrange the files like this:\n" + << "folder/\n" + << " |-- " << executable << "\n"; + for (const auto& extra : extraExecutables) stream << " |-- " << extra << '\n'; + stream << " |-- libs/\n" + << " |-- *.prx (system libraries)\n" + << " |-- app0/\n" + << " |-- \n"; + printChildren(stream, root, " "); + stream << '\n'; + } + +private: + struct Node { + std::map children; + }; + + static void printChildren(std::ostream& stream, const Node& node, const std::string& prefix) { + for (const auto& [name, child] : node.children) { + stream << prefix << "|-- " << name << (child.children.empty() ? "\n" : "/\n"); + printChildren(stream, child, prefix + " "); + } + } +}; + +} diff --git a/core/relinker/cli/src/Autorun.cpp b/core/relinker/cli/src/Autorun.cpp index 0660289e4b..aa54150b47 100644 --- a/core/relinker/cli/src/Autorun.cpp +++ b/core/relinker/cli/src/Autorun.cpp @@ -1,23 +1,56 @@ #include +#include +#include #include #include #include #include #include +#include + +#ifdef _WIN32 +#include +#endif namespace Cli { int Autorun(const std::string& absPath, bool toWindows) { if (!toWindows) { - std::filesystem::permissions(absPath, + std::filesystem::permissions(Io::NativePath(absPath), std::filesystem::perms::owner_exec | std::filesystem::perms::group_exec | std::filesystem::perms::others_exec, std::filesystem::perm_options::add); } + int rawCode = 0; + int exitCode = 0; + #ifdef _WIN32 - const std::string cmd = "\"" + absPath + "\""; + std::wstring command = L"\"" + Io::NativePath(absPath).wstring() + L"\""; + STARTUPINFOW startup{}; + startup.cb = sizeof(startup); + PROCESS_INFORMATION process{}; + if (!CreateProcessW(nullptr, command.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &startup, &process)) + throw Domain::RelinkerException("Cannot run the output executable: " + absPath); + if (WaitForSingleObject(process.hProcess, INFINITE) != WAIT_OBJECT_0) { + const unsigned long waitError = GetLastError(); + CloseHandle(process.hThread); + CloseHandle(process.hProcess); + throw Domain::RelinkerException("Cannot wait for the output executable: " + absPath + " (error " + std::to_string(waitError) + ")"); + } + unsigned long rawExitCode = 0; + if (!GetExitCodeProcess(process.hProcess, &rawExitCode)) { + const unsigned long exitError = GetLastError(); + CloseHandle(process.hThread); + CloseHandle(process.hProcess); + throw Domain::RelinkerException("Cannot read the output executable exit code: " + absPath + " (error " + std::to_string(exitError) + ")"); + } + CloseHandle(process.hThread); + CloseHandle(process.hProcess); + rawCode = static_cast(rawExitCode); + exitCode = rawCode; + std::cout << "\nExit code: " << rawCode << '\n'; #else std::string cmd = "'"; for (const char character : absPath) { @@ -25,17 +58,14 @@ int Autorun(const std::string& absPath, bool toWindows) { else cmd += character; } cmd += '\''; -#endif - const int rawCode = std::system(cmd.c_str()); + rawCode = std::system(cmd.c_str()); - int exitCode = rawCode; -#ifdef _WIN32 - std::cout << "\nExit code: " << rawCode << '\n'; -#else - 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 f354274b9e..3fa915c6d2 100644 --- a/core/relinker/cli/src/CliArgs.cpp +++ b/core/relinker/cli/src/CliArgs.cpp @@ -1,32 +1,131 @@ #include +#include +#include +#include #include #include #include +#include +#include +#include +#include namespace Cli { +const char* Usage() { + return "Usage: relinker [--help] [--windows | --macos] [--windows-diagnostics] [--windows-gui] [--trace-imports] [--windows-fixed-base
] [--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] [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"; +} + +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"); + if (hexadecimal) value.remove_prefix(2); + std::uint64_t address = 0; + const auto parsed = std::from_chars(value.data(), value.data() + value.size(), address, hexadecimal ? 16 : 10); + if (value.empty() || parsed.ec != std::errc{} || parsed.ptr != value.data() + value.size() || address < lowest || address > highest || address % alignment != 0) + throw std::runtime_error(error); + return address; +} + 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 == "--skip-syscall-check") { + 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") { + if (args.windowsFixedBase) throw std::runtime_error("--windows-fixed-base cannot be repeated"); + if (i + 1 >= argc || std::string(argv[i + 1]).empty() || std::string(argv[i + 1]).starts_with("--")) + throw std::runtime_error("--windows-fixed-base requires an address"); + std::string_view value(argv[++i]); + const bool hexadecimal = value.starts_with("0x") || value.starts_with("0X"); + if (hexadecimal) value.remove_prefix(2); + std::uint64_t address = 0; + const auto parsed = std::from_chars(value.data(), value.data() + value.size(), address, hexadecimal ? 16 : 10); + if (parsed.ec != std::errc{} || parsed.ptr != value.data() + value.size() || address < 0x20000 || address > 0x7fffffff0000 || (address & 0xffff) != 0) + throw std::runtime_error("--windows-fixed-base requires a 64 KiB-aligned address from 0x20000 through 0x7fffffff0000"); + args.windowsFixedBase = address; + } else if (arg == "--linux-fixed-base") { + if (args.linuxFixedBase) throw std::runtime_error("--linux-fixed-base cannot be repeated"); + if (i + 1 >= argc || std::string_view(argv[i + 1]).empty() || std::string_view(argv[i + 1]).starts_with("--")) + throw std::runtime_error("--linux-fixed-base requires an address"); + args.linuxFixedBase = 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") { + args.trapSdkSyscallStubs = true; } else if (arg == "--skip-sce-module") { args.skipSceModule = true; + } else if (arg == "--sce-module-path") { + if (sceModulePathSpecified) throw std::runtime_error("--sce-module-path must be specified once"); + if (i + 1 >= argc || std::string(argv[i + 1]).empty() || std::string(argv[i + 1]).starts_with("--")) throw std::runtime_error("--sce-module-path requires a nonempty path"); + args.sceModulePath = argv[++i]; + sceModulePathSpecified = true; } else if (arg == "--exclude-sce-module") { if (i + 1 >= argc) throw std::runtime_error("--exclude-sce-module requires a file name"); args.excludedSceModules.insert(argv[++i]); + } else if (arg == "--module-dir") { + if (i + 1 >= argc) + throw std::runtime_error("--module-dir requires a directory"); + args.moduleDirectories.push_back(argv[++i]); + } else if (arg == "--executable") { + if (i + 1 >= argc || std::string(argv[i + 1]).empty() || std::string(argv[i + 1]).starts_with("--")) throw std::runtime_error("--executable requires a nonempty path"); + args.executables.push_back(argv[++i]); } else if (arg == "--to-intel") { args.toIntel = true; + } else if (arg == "--to-rosetta") { + args.toRosetta = true; + args.toIntel = true; } else if (arg.rfind("unused-filter=", 0) == 0) { const std::string value = arg.substr(14); if (unusedFilterSpecified || value.size() != 1 || value[0] < '0' || value[0] > '2') 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") { @@ -45,8 +144,12 @@ Args ParseArgs(int argc, char* argv[]) { args.windowsDiagnostics = true; } else if (arg == "--windows-gui") { args.windowsGui = true; + } else if (arg == "--trace-imports") { + args.traceImports = true; } else if (arg.rfind("--", 0) == 0 || arg == "unused-filter") { throw std::runtime_error("unknown option: " + arg); + } else if (arg.empty()) { + throw std::runtime_error("input and output paths must be nonempty"); } else if (args.inputPath.empty()) { args.inputPath = arg; } else if (args.outputPath.empty()) { @@ -56,22 +159,60 @@ 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"); + if (args.skipSceModule && !args.excludedSceModules.empty()) throw std::runtime_error("--exclude-sce-module conflicts with --skip-sce-module"); + if (args.skipSceModule && !args.moduleDirectories.empty()) + throw std::runtime_error("--module-dir conflicts with --skip-sce-module"); + + if (args.skipSceModule && !args.executables.empty()) + throw std::runtime_error("--executable conflicts with --skip-sce-module"); + if (args.toWindows && args.toMacos) throw std::runtime_error("--windows conflicts with --macos"); + if (args.toRosetta && args.toWindows) + throw std::runtime_error("--to-rosetta conflicts with --windows"); if (args.windowsDiagnostics && !args.toWindows) throw std::runtime_error("--windows-diagnostics requires --windows"); if (args.windowsGui && !args.toWindows) throw std::runtime_error("--windows-gui requires --windows"); - if (args.inputPath.empty() || args.outputPath.empty()) - throw std::runtime_error( - "Usage: relinker [--windows | --macos] [--windows-diagnostics] [--windows-gui] [--skip-syscall-check] [--skip-sce-module] [--exclude-sce-module ]... [--to-intel] [unused-filter=0|1|2] [--registry] [--rpath ] [--lazy-binding] [--autorun] \n" - "Example: relinker input.elf output.elf" - ); + if (args.windowsFixedBase && !args.toWindows) + throw std::runtime_error("--windows-fixed-base 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() || (!verifying && args.outputPath.empty()))) + throw std::runtime_error(Usage()); + + + 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 new file mode 100644 index 0000000000..f01ac374d4 --- /dev/null +++ b/core/relinker/cli/src/NativeModules.cpp @@ -0,0 +1,110 @@ +#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(); + if (!std::filesystem::exists(root)) throw Domain::RelinkerException("Guest module parent directory does not exist: " + Io::Utf8Path(root)); + if (!std::filesystem::is_directory(root)) throw Domain::RelinkerException("Guest module parent path is not a directory: " + Io::Utf8Path(root)); + const auto missingModuleMessage = std::string(GuestModulePattern) + " was not found in: " + Io::Utf8Path(root) + ". Use --skip-sce-module to disable guest module processing."; + const auto singular = root / "sce_module"; + const auto plural = root / "sce_modules"; + const auto prx = root / "prx"; + const bool hasSingular = std::filesystem::exists(singular); + const bool hasPlural = std::filesystem::exists(plural); + const bool hasPrx = std::filesystem::exists(prx); + if (hasSingular && hasPlural) throw Domain::RelinkerException("Both sce_module and sce_modules exist in the guest module parent directory"); + if (!hasSingular && !hasPlural && !hasPrx && moduleDirectories.empty()) throw Domain::RelinkerException(missingModuleMessage); + std::vector directories; + if (hasSingular || hasPlural) directories.push_back(hasSingular ? singular : plural); + if (hasPrx) directories.push_back(prx); + std::vector runtimeDirectories; + for (const auto& name : moduleDirectories) { + const auto relative = Io::NativePath(name).lexically_normal(); + if (relative.empty() || relative.has_root_path() || *relative.begin() == "..") + throw Domain::RelinkerException("--module-dir must be a directory inside the guest module parent directory: " + name); + auto directory = (root / relative).lexically_normal(); + if (!directory.has_filename()) directory = directory.parent_path(); + if (!std::filesystem::is_directory(directory)) throw Domain::RelinkerException("Guest module path is not a directory: " + Io::Utf8Path(directory)); + if (std::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); + } + std::vector paths; + std::set unmatchedExclusions = excludedModules; + const auto isElf = [](const std::filesystem::path& path, bool& self) { + std::ifstream stream(path, std::ios::binary); + if (!stream) throw Domain::RelinkerException("Cannot read guest candidate: " + Io::Utf8Path(path)); + char magic[4]{}; + stream.read(magic, 4); + if (stream.bad()) throw Domain::RelinkerException("Cannot read guest candidate magic: " + Io::Utf8Path(path)); + if (stream.gcount() != 4) return false; + const auto byte = [&](const std::size_t index) { return static_cast(magic[index]); }; + self = (byte(0) == 0x4f && byte(1) == 0x15 && byte(2) == 0x3d && byte(3) == 0x1d) || (byte(0) == 0x54 && byte(1) == 0x14 && byte(2) == 0xf5 && byte(3) == 0xee); + return byte(0) == 0x7f && magic[1] == 'E' && magic[2] == 'L' && magic[3] == 'F'; + }; + for (const auto& directory : directories) { + if (!std::filesystem::is_directory(directory)) throw Domain::RelinkerException("Guest module path is not a directory: " + Io::Utf8Path(directory)); + for (const auto& entry : std::filesystem::directory_iterator(directory)) { + if (Io::Utf8Path(entry.path().filename()).ends_with(GuestModuleSuffix)) continue; + if (excludedModules.contains(Io::Utf8Path(entry.path().filename()))) { + unmatchedExclusions.erase(Io::Utf8Path(entry.path().filename())); + continue; + } + if (!entry.is_regular_file()) continue; + if (Io::SameFile(entry.path(), inputPath)) continue; + bool self = false; + if (isElf(entry.path(), self)) paths.push_back(entry.path()); + else if (self) + std::cout << "Skipping SELF container: " << entry.path().lexically_relative(root).generic_string() + << " (unwrap it before relinking)\n"; + } + } + if (!unmatchedExclusions.empty()) throw Domain::RelinkerException("Excluded guest module file not found: " + *unmatchedExclusions.begin()); + std::sort(paths.begin(), paths.end()); + NativeModules result; + result.Root = root; + result.RuntimeDirectories = std::move(runtimeDirectories); + for (const auto& path : paths) result.Modules.push_back({path, Io::FileReader().Read(Io::Utf8Path(path))}); + 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 98f3b29974..8f18c4c963 100644 --- a/core/relinker/cli/tests/CliArgsTests.cpp +++ b/core/relinker/cli/tests/CliArgsTests.cpp @@ -52,7 +52,7 @@ void defaults() { const auto args = parse({"input.elf", "output.elf"}); require(args.inputPath == "input.elf", "input path was not captured"); require(args.outputPath == "output.elf", "output path was not captured"); - require(!args.skipSyscallCheck && !args.skipSceModule && !args.toIntel && !args.writeRegistry && + require(!args.skipSyscallCheck && !args.trapSdkSyscallStubs && !args.skipSceModule && !args.toIntel && !args.writeRegistry && !args.toWindows && !args.lazyBinding && !args.autorun && !args.windowsDiagnostics && !args.windowsGui, "a flag defaulted to true"); require(args.unusedFilterLevel == 0u, "unused filter level did not default to 0"); @@ -76,6 +76,13 @@ void booleanFlags() { require(args.inputPath == "input.elf" && args.outputPath == "output.elf", "flags consumed the positional paths"); } +void trapSdkSyscallStubs() { + const auto args = parse({"--trap-sdk-syscall-stubs", "input.elf", "output.elf"}); + require(args.trapSdkSyscallStubs, "--trap-sdk-syscall-stubs was ignored"); + expectThrows({"--skip-syscall-check", "--trap-sdk-syscall-stubs", "input.elf", "output.elf"}, + "--skip-syscall-check conflicts with --trap-sdk-syscall-stubs"); +} + void positionalsInterleaveWithFlags() { const auto args = parse({"--windows", "input.elf", "--registry", "output.elf", "--autorun"}); require(args.toWindows && args.writeRegistry && args.autorun, "a flag after a positional was ignored"); @@ -126,6 +133,15 @@ void excludeConflictsWithSkip() { expectThrows({"--exclude-sce-module", "m", "--skip-sce-module", "input.elf", "output.elf"}, message); } +void executables() { + const auto args = parse({"--executable", "picker/picker.bin", "input.elf", "--executable", "es-de/es-de.bin", "output.elf"}); + require(args.executables == std::vector{"picker/picker.bin", "es-de/es-de.bin"}, "--executable paths were not recorded in order"); + expectThrows({"--executable"}, "--executable requires a nonempty path"); + expectThrows({"--executable", "", "input.elf", "output.elf"}, "--executable requires a nonempty path"); + expectThrows({"--executable", "--registry", "input.elf", "output.elf"}, "--executable requires a nonempty path"); + expectThrows({"--skip-sce-module", "--executable", "picker.bin", "input.elf", "output.elf"}, "--executable conflicts with --skip-sce-module"); +} + void unusedFilter() { for (const auto& value : {std::string("0"), std::string("1"), std::string("2")}) { const auto args = parse({"unused-filter=" + value, "input.elf", "output.elf"}); @@ -156,6 +172,13 @@ void unknownOption() { expectThrows({"--", "input.elf", "output.elf"}, "unknown option: --"); } +void emptyPositionals() { + for (const auto& arguments : std::vector>{ + {"", "input.elf", "output.elf"}, {"input.elf", "", "output.elf"}, + {"", "output.elf"}, {"input.elf", ""}}) + expectThrows(arguments, "input and output paths must be nonempty"); +} + void tooManyPositionals() { expectThrows({"input.elf", "output.elf", "extra.elf"}, "unexpected argument: extra.elf"); } @@ -183,12 +206,139 @@ void missingPositionals() { expectUsage({"--rpath", "/libs"}); } +void windowsFixedBase() { + require(!parse({"--windows", "input.elf", "output.exe"}).windowsFixedBase, "fixed base was enabled by default"); + for (const auto& value : {"0x400000", "0X400000", "4194304"}) { + const auto args = parse({"--windows-fixed-base", value, "--windows", "input.elf", "output.exe"}); + require(args.windowsFixedBase == 0x400000, "fixed base was parsed incorrectly"); + } + require(parse({"--windows", "--windows-fixed-base", "0x400000000", "input.elf", "output.exe"}).windowsFixedBase == 0x400000000, "fixed base was truncated to 32 bits"); + for (const auto& value : {"0", "0x10000", "0x400001", "0x", "-4194304", "+4194304", " 4194304", "0x400000junk", "18446744073709551616", "0x800000000000"}) + expectThrows({"--windows", "--windows-fixed-base", value, "input.elf", "output.exe"}, "--windows-fixed-base requires a 64 KiB-aligned address from 0x20000 through 0x7fffffff0000"); + expectThrows({"--windows-fixed-base"}, "--windows-fixed-base requires an address"); + expectThrows({"--windows-fixed-base", "--windows"}, "--windows-fixed-base requires an address"); + expectThrows({"--windows-fixed-base", "0x400000", "input.elf", "output.exe"}, "--windows-fixed-base requires --windows"); + expectThrows({"--macos", "--windows-fixed-base", "0x400000", "input.elf", "output.exe"}, "--windows-fixed-base requires --windows"); + expectThrows({"--windows", "--windows-fixed-base", "0x400000", "--windows-fixed-base", "0x800000", "input.elf", "output.exe"}, "--windows-fixed-base cannot be repeated"); +} + +void linuxFixedBase() { + require(!parse({"input.elf", "output.elf"}).linuxFixedBase, "fixed base was enabled by default"); + for (const auto& value : {"0x400000", "0X400000", "4194304"}) + require(parse({"--linux-fixed-base", value, "input.elf", "output.elf"}).linuxFixedBase == 0x400000, "fixed base was parsed incorrectly"); + require(parse({"input.elf", "--linux-fixed-base", "0x10000", "output.elf"}).linuxFixedBase == 0x10000, "lowest fixed base was rejected"); + require(parse({"--linux-fixed-base", "0x7fffffff0000", "input.elf", "output.elf"}).linuxFixedBase == 0x7fffffff0000, "highest fixed base was rejected"); + require(parse({"--linux-fixed-base", "0x400000000", "input.elf", "output.elf"}).linuxFixedBase == 0x400000000, "fixed base was truncated to 32 bits"); + const std::string range = "--linux-fixed-base requires a 64 KiB-aligned address from 0x10000 through 0x7fffffff0000"; + for (const auto& value : {"0", "0x0", "0xf000", "0x400001", "0x408000", "0x", "0X", "-4194304", "+4194304", " 4194304", "4194304 ", "0x400000junk", "0x-400000", "18446744073709551616", "0x800000000000", "0x7fffffff10000"}) + expectThrows({"--linux-fixed-base", value, "input.elf", "output.elf"}, range); + expectThrows({"--linux-fixed-base"}, "--linux-fixed-base requires an address"); + expectThrows({"--linux-fixed-base", "", "input.elf", "output.elf"}, "--linux-fixed-base requires an address"); + expectThrows({"--linux-fixed-base", "--windows", "input.elf", "output.elf"}, "--linux-fixed-base requires an address"); + expectThrows({"--linux-fixed-base", "0x400000", "--linux-fixed-base", "0x400000", "input.elf", "output.elf"}, "--linux-fixed-base cannot be repeated"); + expectThrows({"--windows", "--linux-fixed-base", "0x400000", "input.elf", "output.exe"}, "--linux-fixed-base cannot be used with --windows or --macos"); + expectThrows({"--linux-fixed-base", "0x400000", "--macos", "input.elf", "output"}, "--linux-fixed-base cannot be used with --windows or --macos"); + expectThrows({"--lazy-binding", "--linux-fixed-base", "0x400000", "input.elf", "output.elf"}, "--linux-fixed-base conflicts with --lazy-binding"); +} + +void 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() { + const auto args = parse({"--module-dir", "Media/Plugins", "input.elf", "--module-dir", "extra", "output.elf"}); + require(args.moduleDirectories == std::vector{"Media/Plugins", "extra"}, "--module-dir directories were not recorded in order"); + require(args.inputPath == "input.elf" && args.outputPath == "output.elf", "--module-dir consumed a positional path"); + expectThrows({"--module-dir"}, "--module-dir requires a directory"); + expectThrows({"--skip-sce-module", "--module-dir", "Media/Plugins", "input.elf", "output.elf"}, "--module-dir conflicts with --skip-sce-module"); + expectThrows({"--module-dir", "Media/Plugins", "--skip-sce-module", "input.elf", "output.elf"}, "--module-dir conflicts with --skip-sce-module"); +} + +void sceModulePath() { + const auto args = parse({"--sce-module-path", "modules", "input.elf", "output.elf"}); + require(args.sceModulePath == "modules", "--sce-module-path value was not captured"); + expectThrows({"--sce-module-path"}, "--sce-module-path requires a nonempty path"); + expectThrows({"--sce-module-path", "", "input.elf", "output.elf"}, "--sce-module-path requires a nonempty path"); + expectThrows({"--sce-module-path", "--registry", "input.elf", "output.elf"}, "--sce-module-path requires a nonempty path"); + expectThrows({"--sce-module-path", "a", "--sce-module-path", "b", "input.elf", "output.elf"}, "--sce-module-path must be specified once"); + expectThrows({"--skip-sce-module", "--sce-module-path", "modules", "input.elf", "output.elf"}, "--sce-module-path conflicts with --skip-sce-module"); + expectThrows({"--sce-module-path", "modules", "--skip-sce-module", "input.elf", "output.elf"}, "--sce-module-path conflicts with --skip-sce-module"); +} + +void targetSelection() { + const auto macos = parse({"--macos", "input.elf", "output.elf"}); + require(macos.toMacos && !macos.toWindows, "--macos did not select only the macOS target"); + const auto rosetta = parse({"--to-rosetta", "input.elf", "output.elf"}); + require(rosetta.toRosetta && rosetta.toIntel, "--to-rosetta did not imply --to-intel"); + const auto rosettaOnMacos = parse({"--macos", "--to-rosetta", "input.elf", "output.elf"}); + require(rosettaOnMacos.toMacos && rosettaOnMacos.toRosetta && rosettaOnMacos.toIntel, "--to-rosetta was rejected with --macos"); + const std::string targetConflict = "--windows conflicts with --macos"; + expectThrows({"--windows", "--macos", "input.elf", "output.elf"}, targetConflict); + expectThrows({"--macos", "--windows", "input.elf", "output.elf"}, targetConflict); + const std::string rosettaConflict = "--to-rosetta conflicts with --windows"; + expectThrows({"--windows", "--to-rosetta", "input.elf", "output.elf"}, rosettaConflict); + expectThrows({"--to-rosetta", "--windows", "input.elf", "output.elf"}, rosettaConflict); +} + +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(); booleanFlags(); + trapSdkSyscallStubs(); positionalsInterleaveWithFlags(); singleDashIsPositional(); rpath(); @@ -198,15 +348,25 @@ int main() { excludeSceModuleConsumesNextTokenVerbatim(); excludeSceModuleWithoutValue(); excludeConflictsWithSkip(); + moduleDirectories(); + sceModulePath(); + targetSelection(); + executables(); unusedFilter(); unusedFilterRejectsBadValues(); unusedFilterRejectsDuplicates(); unusedFilterBareWordIsUnknown(); unknownOption(); + emptyPositionals(); tooManyPositionals(); windowsDiagnosticsRequiresWindows(); windowsGuiRequiresWindows(); 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/cli/tests/test_help.py b/core/relinker/cli/tests/test_help.py new file mode 100644 index 0000000000..8c9659affc --- /dev/null +++ b/core/relinker/cli/tests/test_help.py @@ -0,0 +1,44 @@ +from pathlib import Path +import subprocess +import sys +import tempfile + + +def main(): + relinker = Path(sys.argv[1]).resolve() + with tempfile.TemporaryDirectory(prefix="anyps5-help-") as directory: + work = Path(directory) + source = work / "missing.elf" + output = work / "existing.exe" + output.write_bytes(b"preserve this output") + + def run(*args): + return subprocess.run([str(relinker), *args], cwd=work, + capture_output=True, text=True, timeout=10) + + for args in [("--help",), ("--help", "--registry", "--autorun", str(source), str(output)), + (str(source), str(output), "--help")]: + result = run(*args) + assert result.returncode == 0, result.stderr + assert result.stdout.startswith("Usage: relinker "), result.stdout + assert "--help" in result.stdout and "Example:" in result.stdout + assert result.stderr == "", result.stderr + assert output.read_bytes() == b"preserve this output" + assert sorted(path.name for path in work.iterdir()) == [output.name] + + result = run() + assert result.returncode == 1 and "Usage: relinker" in result.stderr + for args in [("--help", "--unknown"), ("--unknown", "--help")]: + result = run(*args) + assert result.returncode == 1 and "unknown option" in result.stderr + result = run("--help", "--windows-gui") + assert result.returncode == 1 and "requires --windows" in result.stderr + for flag in ["--rpath", "--exclude-sce-module"]: + result = run(flag, "--help", str(source), str(output)) + assert result.returncode == 2 and "Cannot open file" in result.stderr + assert result.stdout == "", result.stdout + assert output.read_bytes() == b"preserve this output" + + +if __name__ == "__main__": + main() diff --git a/core/relinker/codegen/include/codegen/CodegenTypes.hpp b/core/relinker/codegen/include/codegen/CodegenTypes.hpp index e41f6cd538..021f2bf059 100644 --- a/core/relinker/codegen/include/codegen/CodegenTypes.hpp +++ b/core/relinker/codegen/include/codegen/CodegenTypes.hpp @@ -65,11 +65,14 @@ struct DecodedInstructionInfo { std::uint8_t RexPrefix; ControlFlowKind FlowKind; bool HasRipRelativeDisp; + 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/IAmd64OnlyConverter.hpp b/core/relinker/codegen/include/codegen/IAmd64OnlyConverter.hpp index 1bc42caf8b..ef17189275 100644 --- a/core/relinker/codegen/include/codegen/IAmd64OnlyConverter.hpp +++ b/core/relinker/codegen/include/codegen/IAmd64OnlyConverter.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include namespace Codegen { @@ -12,13 +13,10 @@ class IAmd64OnlyConverter { public: virtual ~IAmd64OnlyConverter() = default; - [[nodiscard]] virtual ConvertResult Convert( - std::vector fileBytes, - const std::vector& codeSegments - ) const = 0; + [[nodiscard]] virtual ConvertResult Convert(std::vector fileBytes, const std::vector& segments) const = 0; }; -std::unique_ptr MakeAmd64OnlyConverter(); +std::unique_ptr MakeAmd64OnlyConverter(Amd64OnlyTarget target = Amd64OnlyTarget::Intel, std::function progress = {}); } diff --git a/core/relinker/codegen/include/codegen/IInstructionScanner.hpp b/core/relinker/codegen/include/codegen/IInstructionScanner.hpp index 1f639525f5..a09771a326 100644 --- a/core/relinker/codegen/include/codegen/IInstructionScanner.hpp +++ b/core/relinker/codegen/include/codegen/IInstructionScanner.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -12,14 +13,13 @@ class IInstructionScanner { public: virtual ~IInstructionScanner() = default; - [[nodiscard]] virtual std::vector ScanCodeSection( - const std::vector& codeSection, - Domain::FileByteOffset codeSectionOffset, - Domain::FileByteOffset codeSectionSize - ) const = 0; + [[nodiscard]] virtual std::vector ScanCodeSection(const std::vector& codeSection, Domain::FileByteOffset codeSectionOffset, Domain::FileByteOffset codeSectionSize) const = 0; + virtual void VisitCodeSection(const std::vector& codeSection, Domain::FileByteOffset codeSectionOffset, Domain::FileByteOffset codeSectionSize, const std::function& visit) const = 0; }; -std::unique_ptr MakeInstructionScanner(); +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 8a89186717..c3df3388cc 100644 --- a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp +++ b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp @@ -38,6 +38,12 @@ inline constexpr Entry kMwaitx = {"MWAITX", nullptr, 0}; inline constexpr Entry kClzero = {"CLZERO", nullptr, 0}; 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}; @@ -46,6 +52,8 @@ inline constexpr Entry kMovntss = {"MOVNTSS", nullptr, 0}; inline constexpr Entry kMovntsd = {"MOVNTSD", nullptr, 0}; inline constexpr Entry kVrcpps = {"VRCPPS", nullptr, 0}; inline constexpr Entry kVrsqrtps = {"VRSQRTPS", nullptr, 0}; +inline constexpr Entry kRcpps = {"RCPPS", nullptr, 0}; +inline constexpr Entry kRsqrtps = {"RSQRTPS", nullptr, 0}; inline constexpr Entry kSha256rnds2 = {"SHA256RNDS2", nullptr, 0}; inline constexpr Entry kSha256msg1 = {"SHA256MSG1", nullptr, 0}; inline constexpr Entry kSha256msg2 = {"SHA256MSG2", nullptr, 0}; @@ -57,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/Amd64OnlySubstitutionTypes.hpp b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTypes.hpp index 2b835fd940..e2f5946424 100644 --- a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTypes.hpp +++ b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTypes.hpp @@ -8,6 +8,11 @@ namespace Codegen { +enum class Amd64OnlyTarget : std::uint8_t { + Intel, + Rosetta +}; + enum class Amd64OnlyLowering : std::uint8_t { InPlace, Trampoline, @@ -30,6 +35,7 @@ struct Amd64OnlyMatch { std::size_t ReturnBranchOffset; bool Optional = false; std::vector Relocations = {}; + bool RelocatableWindow = false; }; struct Amd64OnlySubstitutionReport { 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 dc85cbcd44..4176cc4cff 100644 --- a/core/relinker/codegen/include/codegen/x86/DecodedInstruction.hpp +++ b/core/relinker/codegen/include/codegen/x86/DecodedInstruction.hpp @@ -23,6 +23,10 @@ struct DecodedInstruction { [[nodiscard]] bool IsMcommit() const; [[nodiscard]] bool IsMovntss() const; [[nodiscard]] bool IsMovntsd() const; + [[nodiscard]] bool IsRdseed() const; + [[nodiscard]] bool IsRdpid() const; + [[nodiscard]] bool IsRdtscp() const; + [[nodiscard]] bool IsClwb() const; private: [[nodiscard]] std::size_t _skipPrefixesAndRex(bool* outHasOperandSizePrefix, bool* outHasRepnePrefix, bool* outHasRepPrefix) const; diff --git a/core/relinker/codegen/include/codegen/x86/IAmd64OnlyInstructionMatcher.hpp b/core/relinker/codegen/include/codegen/x86/IAmd64OnlyInstructionMatcher.hpp index 65d6f04c1e..9986e79286 100644 --- a/core/relinker/codegen/include/codegen/x86/IAmd64OnlyInstructionMatcher.hpp +++ b/core/relinker/codegen/include/codegen/x86/IAmd64OnlyInstructionMatcher.hpp @@ -2,6 +2,7 @@ #define CODEGEN_X86_IAMD64ONLYINSTRUCTIONMATCHER_HPP #include +#include #include #include #include @@ -23,9 +24,14 @@ public: std::span> instructions, std::span trailing ) const = 0; + + virtual void EmitOutOfLine( + StubBodyBuilder& body, + std::span instruction + ) const = 0; }; -std::unique_ptr MakeAmd64OnlyInstructionMatcher(); +std::unique_ptr MakeAmd64OnlyInstructionMatcher(Amd64OnlyTarget target = Amd64OnlyTarget::Intel); } diff --git a/core/relinker/codegen/include/codegen/x86/ReciprocalOperands.hpp b/core/relinker/codegen/include/codegen/x86/ReciprocalOperands.hpp index 31102844ec..ca38206236 100644 --- a/core/relinker/codegen/include/codegen/x86/ReciprocalOperands.hpp +++ b/core/relinker/codegen/include/codegen/x86/ReciprocalOperands.hpp @@ -16,9 +16,10 @@ struct ReciprocalOperands { ReciprocalOperation Operation; std::uint8_t Destination; std::uint8_t Source; + bool Vex; }; -[[nodiscard]] std::optional DecodeVexReciprocal(const std::uint8_t* data, std::size_t length); +[[nodiscard]] std::optional DecodeReciprocal(const std::uint8_t* data, std::size_t length); } 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 212eeea546..72fb24c1c9 100644 --- a/core/relinker/codegen/include/codegen/x86/StubBodyBuilder.hpp +++ b/core/relinker/codegen/include/codegen/x86/StubBodyBuilder.hpp @@ -1,6 +1,7 @@ #ifndef CODEGEN_X86_STUBBODYBUILDER_HPP #define CODEGEN_X86_STUBBODYBUILDER_HPP +#include #include #include #include @@ -37,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 { @@ -53,6 +53,10 @@ public: void Restore(std::uint8_t reg); void Raw(std::span bytes); void Move(std::span instruction, std::optional ripDisplacementOffset); + void RipRelative(std::span instruction, std::size_t displacementOffset, std::int64_t target); + void MoveControlFlow(std::span instruction, const DecodedInstructionInfo& info); + void JumpTo(std::int64_t siteTarget); + void Seek(std::int64_t siteOffset); void Advance(std::size_t length); [[nodiscard]] LoweredBody Finish(); @@ -63,12 +67,15 @@ private: std::size_t ConstantIndex; }; + void _rel32(std::initializer_list opcode, std::int64_t siteTarget); + void _pushReturnAddress(std::int64_t siteTarget); + std::vector _bytes; std::vector _fixups; std::vector _constants; std::vector _relocations; std::size_t _stackDepth = 0; - std::size_t _siteOffset = 0; + std::int64_t _siteOffset = 0; }; } diff --git a/core/relinker/codegen/include/codegen/x86/WideStoreLowering.hpp b/core/relinker/codegen/include/codegen/x86/WideStoreLowering.hpp new file mode 100644 index 0000000000..7e27e27966 --- /dev/null +++ b/core/relinker/codegen/include/codegen/x86/WideStoreLowering.hpp @@ -0,0 +1,16 @@ +#ifndef CODEGEN_X86_WIDESTORELOWERING_HPP +#define CODEGEN_X86_WIDESTORELOWERING_HPP + +#include +#include + +namespace Codegen { + +class WideStoreLowering { +public: + void EmitOutOfLine(StubBodyBuilder& body, const WideStoreOperands& operands) const; +}; + +} + +#endif diff --git a/core/relinker/codegen/include/codegen/x86/WideStoreOperands.hpp b/core/relinker/codegen/include/codegen/x86/WideStoreOperands.hpp new file mode 100644 index 0000000000..1716af37be --- /dev/null +++ b/core/relinker/codegen/include/codegen/x86/WideStoreOperands.hpp @@ -0,0 +1,26 @@ +#ifndef CODEGEN_X86_WIDESTOREOPERANDS_HPP +#define CODEGEN_X86_WIDESTOREOPERANDS_HPP + +#include +#include +#include +#include + +namespace Codegen { + +struct WideStoreHalf { + std::vector Bytes; + std::optional RipDisplacementOffset; + std::int64_t RipTarget = 0; +}; + +struct WideStoreOperands { + WideStoreHalf Low; + WideStoreHalf High; +}; + +[[nodiscard]] std::optional DecodeVexWideStore(const std::uint8_t* data, std::size_t length); + +} + +#endif diff --git a/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp b/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp index 969a1fd300..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; @@ -170,6 +171,7 @@ inline constexpr std::uint8_t TwoByteGrp6 = 0x00; inline constexpr std::uint8_t TwoByteLar = 0x02; inline constexpr std::uint8_t TwoByteLsl = 0x03; inline constexpr std::uint8_t TwoBytePrefetchw = 0x0D; +inline constexpr std::uint8_t TwoByte3DNow = 0x0F; inline constexpr std::uint8_t TwoByteMovCrDrMin = 0x20; inline constexpr std::uint8_t TwoByteMovCrDrMax = 0x23; inline constexpr std::uint8_t TwoBytePopcnt = 0xB8; @@ -220,6 +222,10 @@ inline constexpr std::size_t Vex2PrefixLength = 2; inline constexpr std::size_t Vex3PrefixLength = 3; inline constexpr std::uint8_t Vex3MapMask = 0x1F; inline constexpr std::uint8_t Vex3Map0F3A = 0x03; +inline constexpr std::uint8_t XopPrefix = 0x8F; +inline constexpr std::uint8_t XopMapMin = 0x08; +inline constexpr std::uint8_t XopMap8 = 0x08; +inline constexpr std::uint8_t XopMapA = 0x0A; } diff --git a/core/relinker/codegen/src/Amd64OnlyConverter.cpp b/core/relinker/codegen/src/Amd64OnlyConverter.cpp index 409b62e878..8f312031fe 100644 --- a/core/relinker/codegen/src/Amd64OnlyConverter.cpp +++ b/core/relinker/codegen/src/Amd64OnlyConverter.cpp @@ -2,10 +2,16 @@ #include #include #include +#include #include +#include #include +#include #include +#include +#include #include +#include #include #include #include @@ -24,11 +30,195 @@ auto _atFileOffset(const Domain::FileByteOffset base, const TOperation& operatio } } +constexpr std::int64_t kShortJumpSize = 2; +constexpr std::uint32_t kLoadSegment = 1; +constexpr std::uint32_t kExecutableSegment = 1; +constexpr std::uint8_t kLea = 0x8D; +constexpr std::uint64_t kIslandSearchBytes = 0x100; + +struct Placement { + std::vector Sites; + std::vector Claimed; + std::vector Consumed; + std::size_t ReplacementLength = 0; +}; + +class WindowPlacer { +public: + WindowPlacer( + std::vector& seg, + const std::vector& matches, + const Domain::ProgramHeader& ph, + const std::set& branchTargets, + const std::set& pending, + const std::set& claimed, + const IAmd64OnlyInstructionMatcher& matcher + ) : _seg(seg), _matches(matches), _ph(ph), _branchTargets(branchTargets), _pending(pending), _claimed(claimed), _matcher(matcher) {} + + [[nodiscard]] std::optional Window(const std::size_t index) const { + 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; + } + } + return std::nullopt; + } + + [[nodiscard]] std::optional Island(const std::size_t index) { + const auto store = _address(index); + for (std::size_t distance = 1;; ++distance) { + bool inReach = false; + for (const bool before : {true, false}) { + if (before ? distance > index : index + distance >= _matches.size()) + continue; + const auto start = before ? index - distance : index + distance; + if (_address(start) + kIslandSearchBytes < store || _address(start) > store + kIslandSearchBytes) + continue; + inReach = true; + if (auto placement = _islandAt(index, start)) + return placement; + } + if (!inReach) + return std::nullopt; + } + } + +private: + std::vector& _seg; + const std::vector& _matches; + const Domain::ProgramHeader& _ph; + const std::set& _branchTargets; + const std::set& _pending; + const std::set& _claimed; + const IAmd64OnlyInstructionMatcher& _matcher; + X64InstructionDecoder _decoder; + + [[nodiscard]] std::uint64_t _address(const std::size_t index) const { return _ph.MappedAddress + _matches[index].Offset; } + [[nodiscard]] std::uint64_t _end(const std::size_t index) const { return _address(index) + _matches[index].Length; } + [[nodiscard]] std::span _bytes(const std::size_t index) const { return {_seg.data() + _matches[index].Offset, _matches[index].Length}; } + [[nodiscard]] DecodedInstructionInfo _info(const std::size_t index) const { return _decoder.DecodeInstruction(_seg.data() + _matches[index].Offset, _matches[index].Length); } + + [[nodiscard]] bool _entered(const std::uint64_t start, const std::uint64_t end) const { + const auto hit = _branchTargets.upper_bound(start); + return hit != _branchTargets.end() && *hit < end; + } + + [[nodiscard]] bool _blocksFollowing(const std::size_t index) const { + const auto kind = _info(index).FlowKind; + return kind != ControlFlowKind::Sequential && kind != ControlFlowKind::ConditionalBranch; + } + + [[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 { + const auto offset = static_cast(start - _ph.MappedAddress); + return { + static_cast(_ph.Offset + offset), + static_cast(start), + length, + std::vector(_seg.begin() + static_cast(offset), _seg.begin() + static_cast(offset + length)), + std::move(body.Bytes), + body.ReturnBranchOffset, + std::move(body.Relocations) + }; + } + + [[nodiscard]] std::optional _relocate(const std::size_t index, const std::size_t first, const std::size_t last) const { + if (_entered(_address(first), _end(last))) + return std::nullopt; + StubBodyBuilder body; + Placement placement; + try { + for (auto k = first; k <= last; ++k) { + if (_pending.contains(k)) { + _matcher.EmitOutOfLine(body, _bytes(k)); + if (k != index) + placement.Consumed.push_back(k); + } else { + body.MoveControlFlow(_bytes(k), _info(k)); + } + placement.Claimed.push_back(k); + } + } catch (const CodegenException&) { + return std::nullopt; + } + placement.Sites.push_back(_site(_address(first), static_cast(_end(last) - _address(first)), body.Finish())); + placement.ReplacementLength = placement.Sites.back().Body.size(); + return placement; + } + + [[nodiscard]] std::optional _islandAt(const std::size_t index, const std::size_t start) { + using Amd64OnlySubstitutionTable::kJmpRel32; + std::size_t head = 0; + for (auto last = start; last < _matches.size(); ++last) { + if (!_movable(last, true) || _entered(_address(start), _end(last))) + return std::nullopt; + if (head == 0 && _end(last) - _address(start) >= kJmpRel32.Size) + head = static_cast(_end(last) - _address(start)); + const auto island = _address(start) + head; + const auto reach = static_cast(island) - static_cast(_address(index) + kShortJumpSize); + if (head != 0 && (reach < std::numeric_limits::min() || reach > std::numeric_limits::max())) + return std::nullopt; + if (head != 0 && _end(last) >= island + kJmpRel32.Size) { + try { + return _buildIsland(index, start, last, head); + } catch (const CodegenException&) { + return std::nullopt; + } + } + if (_blocksFollowing(last)) + return std::nullopt; + } + return std::nullopt; + } + + [[nodiscard]] Placement _buildIsland(const std::size_t index, const std::size_t start, const std::size_t last, const std::size_t head) { + using Amd64OnlySubstitutionTable::kJmpRel32; + const auto windowStart = _address(start); + const auto windowEnd = _end(last); + const auto island = windowStart + head; + const auto store = _address(index); + Placement placement; + + StubBodyBuilder moved; + for (auto k = start; k <= last; ++k) { + moved.MoveControlFlow(_bytes(k), _info(k)); + placement.Claimed.push_back(k); + } + if (!_blocksFollowing(last)) + moved.JumpTo(static_cast(windowEnd - windowStart)); + placement.Sites.push_back(_site(windowStart, head, moved.Finish())); + + StubBodyBuilder lowered; + lowered.Seek(static_cast(store) - static_cast(island)); + _matcher.EmitOutOfLine(lowered, _bytes(index)); + lowered.JumpTo(static_cast(_end(index)) - static_cast(island)); + placement.Sites.push_back(_site(island, kJmpRel32.Size, lowered.Finish())); + placement.ReplacementLength = placement.Sites.back().Body.size(); + placement.Claimed.push_back(index); + + const auto offset = _matches[index].Offset; + std::fill_n(_seg.begin() + static_cast(offset), _matches[index].Length, Amd64OnlySubstitutionTable::kTrapFill); + _seg[offset] = X64OpcodeConstants::OneByteJmpRel8; + _seg[offset + 1] = static_cast(static_cast(static_cast(island) - static_cast(store + kShortJumpSize))); + return placement; + } +}; + class Amd64OnlyConverter : public IAmd64OnlyConverter { public: + Amd64OnlyConverter(const Amd64OnlyTarget target, std::function progress) : _matcher(MakeAmd64OnlyInstructionMatcher(target)), progress(std::move(progress)) {} + [[nodiscard]] ConvertResult Convert( std::vector fileBytes, - const std::vector& codeSegments + const std::vector& segments ) const override; private: @@ -38,36 +228,76 @@ private: Amd64OnlyMatch Substitution; }; - std::unique_ptr _matcher = MakeAmd64OnlyInstructionMatcher(); - std::unique_ptr _scanner = MakeInstructionScanner(); + std::unique_ptr _matcher; + std::function progress; void _convertSegment( std::vector& fileBytes, const Domain::ProgramHeader& ph, - ConvertResult& result + const std::vector& segments, + ConvertResult& result, + std::uint64_t completed, + std::uint64_t total ) const; [[nodiscard]] static std::set _collectBranchTargets( const std::vector& seg, const std::vector& matches, - const Domain::ProgramHeader& ph + const Domain::ProgramHeader& ph, + const std::vector& fileBytes, + const std::vector& segments ); }; std::set Amd64OnlyConverter::_collectBranchTargets( const std::vector& seg, const std::vector& matches, - const Domain::ProgramHeader& ph + const Domain::ProgramHeader& ph, + const std::vector& fileBytes, + const std::vector& segments ) { + const auto startsInstruction = [&](const std::uint64_t address) { + const auto offset = address - ph.MappedAddress; + 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) { const auto info = decoder.DecodeInstruction(seg.data() + match.Offset, match.Length); - if (!info.HasBranchTarget || info.HasRipRelativeDisp) - continue; - const auto target = static_cast(ph.MappedAddress + match.Offset + info.Length) + info.BranchDisp; - if (target >= 0) - targets.insert(static_cast(target)); + 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 && !pastPrefixes(static_cast(target))) + targets.insert(static_cast(target)); + } else if (info.HasRipRelativeDisp && info.Opcode == kLea) { + std::int32_t displacement; + std::memcpy(&displacement, seg.data() + match.Offset + info.RipRelativeDispOffset, sizeof(displacement)); + const auto table = static_cast(next + displacement); + for (const auto& data : segments) { + if (data.Type != kLoadSegment || table < data.MappedAddress || table - data.MappedAddress >= data.FileSize) + continue; + if (data.Offset > fileBytes.size() || data.FileSize > fileBytes.size() - data.Offset) + throw CodegenException("Segment exceeds the file", data.Offset); + for (auto entry = table; entry - data.MappedAddress + sizeof(std::int32_t) <= data.FileSize; entry += sizeof(std::int32_t)) { + std::int32_t relative; + std::memcpy(&relative, fileBytes.data() + data.Offset + (entry - data.MappedAddress), sizeof(relative)); + const auto target = table + static_cast(static_cast(relative)); + if (!startsInstruction(target)) + break; + targets.insert(target); + } + } + } } return targets; } @@ -75,37 +305,54 @@ std::set Amd64OnlyConverter::_collectBranchTargets( void Amd64OnlyConverter::_convertSegment( std::vector& fileBytes, const Domain::ProgramHeader& ph, - ConvertResult& result + const std::vector& segments, + ConvertResult& result, + std::uint64_t completed, + std::uint64_t total ) const { + if (ph.Offset > fileBytes.size() || ph.FileSize > fileBytes.size() - ph.Offset) + throw CodegenException("Code segment exceeds the file", ph.Offset); const auto segOffset = static_cast(ph.Offset); const auto segSize = static_cast(ph.FileSize); - if (segOffset > fileBytes.size() || segSize > fileBytes.size() - segOffset) - throw CodegenException("Code segment exceeds the file", ph.Offset); std::vector seg( fileBytes.begin() + static_cast(segOffset), fileBytes.begin() + static_cast(segOffset + segSize) ); - const auto matches = _atFileOffset(ph.Offset, [&] { return _scanner->ScanCodeSection(seg, 0, seg.size()); }); + const auto scanner = MakeInstructionScanner([&](auto offset, auto) { if (progress) progress(completed + offset, total); }); + const auto matches = _atFileOffset(ph.Offset, [&] { return scanner->ScanCodeSection(seg, 0, seg.size()); }); + if (progress) progress(completed + segSize, total); std::vector pending; for (std::size_t index = 0; index < matches.size(); ++index) { + if (progress && index != 0 && index % 4096 == 0) progress(completed + segSize + matches[index].Offset, total); const auto& match = matches[index]; auto substitution = _atFileOffset(ph.Offset + match.Offset, [&] { return _matcher->Match(seg.data() + match.Offset, match.Length); }); if (substitution.has_value()) pending.push_back({index, match, std::move(*substitution)}); } - if (pending.empty()) + if (progress) progress(completed + std::uint64_t{2} * segSize, total); + if (pending.empty()) { + if (progress) progress(completed + std::uint64_t{3} * segSize, total); return; + } const bool needsBranchTargets = std::any_of(pending.begin(), pending.end(), [](const Pending& item) { return item.Substitution.Lowering == Amd64OnlyLowering::Trampoline; }); - const auto branchTargets = needsBranchTargets ? _collectBranchTargets(seg, matches, ph) : std::set{}; + const auto branchTargets = needsBranchTargets ? _collectBranchTargets(seg, matches, ph, fileBytes, segments) : std::set{}; + std::set pendingIndices; + for (const auto& item : pending) + pendingIndices.insert(item.Index); + std::set claimed; std::set consumed; + + std::size_t processed = 0; for (const auto& item : pending) { + if (progress && processed != 0 && processed % 256 == 0) progress(completed + std::uint64_t{2} * segSize + item.Instruction.Offset, total); + ++processed; if (consumed.contains(item.Index)) continue; const auto& match = item.Instruction; @@ -124,6 +371,20 @@ void Amd64OnlyConverter::_convertSegment( break; } case Amd64OnlyLowering::Trampoline: { + if (substitution.RelocatableWindow && match.Length < Amd64OnlySubstitutionTable::kJmpRel32.Size) { + WindowPlacer placer(seg, matches, ph, branchTargets, pendingIndices, claimed, *_matcher); + auto placement = placer.Window(item.Index); + if (!placement) + placement = placer.Island(item.Index); + if (!placement) + throw CodegenException("No room for a jump to the stub of " + substitution.InstructionName + " within its neighbours or a nearby island", fileOffset); + for (auto& site : placement->Sites) + result.Trampolines.push_back(std::move(site)); + claimed.insert(placement->Claimed.begin(), placement->Claimed.end()); + consumed.insert(placement->Consumed.begin(), placement->Consumed.end()); + replacementLength = placement->ReplacementLength; + break; + } std::vector taken; try { std::size_t siteLength = match.Length; @@ -135,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()); @@ -170,6 +433,8 @@ void Amd64OnlyConverter::_convertSegment( }); replacementLength = stub.StubBody.size(); consumed.insert(taken.begin(), taken.end()); + for (auto k = item.Index; k < matches.size() && matches[k].Offset < match.Offset + siteLength; ++k) + claimed.insert(k); } catch (const CodegenException&) { if (!substitution.Optional) throw; @@ -190,23 +455,36 @@ void Amd64OnlyConverter::_convertSegment( if (seg.size() != segSize) throw CodegenException("Code segment size changed during Intel conversion", ph.Offset); std::copy(seg.begin(), seg.end(), fileBytes.begin() + static_cast(segOffset)); + if (progress) progress(completed + std::uint64_t{3} * segSize, total); } ConvertResult Amd64OnlyConverter::Convert( std::vector fileBytes, - const std::vector& codeSegments + const std::vector& segments ) const { ConvertResult result{}; - for (const auto& ph : codeSegments) - _convertSegment(fileBytes, ph, result); + std::uint64_t total = 0; + for (const auto& ph : segments) { + if (ph.Type != kLoadSegment || (ph.Flags & kExecutableSegment) == 0) continue; + if (ph.FileSize > (std::numeric_limits::max() - total) / 3) throw CodegenException("Instruction conversion progress size overflow"); + total += 3 * ph.FileSize; + } + if (progress) progress(0, total); + std::uint64_t completed = 0; + for (const auto& ph : segments) { + if (ph.Type != kLoadSegment || (ph.Flags & kExecutableSegment) == 0) continue; + _convertSegment(fileBytes, ph, segments, result, completed, total); + completed += 3 * ph.FileSize; + } + if (progress) progress(total, total); result.Bytes = std::move(fileBytes); return result; } } -std::unique_ptr MakeAmd64OnlyConverter() { - return std::make_unique(); +std::unique_ptr MakeAmd64OnlyConverter(const Amd64OnlyTarget target, std::function progress) { + return std::make_unique(target, std::move(progress)); } } diff --git a/core/relinker/codegen/src/InstructionScanner.cpp b/core/relinker/codegen/src/InstructionScanner.cpp index ccd03f9834..28f8544994 100644 --- a/core/relinker/codegen/src/InstructionScanner.cpp +++ b/core/relinker/codegen/src/InstructionScanner.cpp @@ -3,31 +3,51 @@ #include #include #include +#include #include 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: - [[nodiscard]] std::vector ScanCodeSection( - const std::vector& codeSection, - Domain::FileByteOffset codeSectionOffset, - Domain::FileByteOffset codeSectionSize - ) const override; + 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 { - const std::size_t limit = std::min(codeSection.size(), static_cast(codeSectionSize)); +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; @@ -36,19 +56,41 @@ std::vector InstructionScanner::ScanCodeSection( 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) { + progress(i, limit); + lastReport = i; + } } + 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() { - return std::make_unique(); +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 caff05f46d..739a3aea7f 100644 --- a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp +++ b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp @@ -12,10 +12,15 @@ #include #include #include +#include +#include +#include +#include #include #include #include #include +#include #include #include #include @@ -41,6 +46,8 @@ const Entry& _sse4aEntry(const Sse4aOperands& operands) { } const Entry& _reciprocalEntry(const ReciprocalOperands& operands) { + if (!operands.Vex) + return operands.Operation == ReciprocalOperation::ReciprocalSquareRoot ? kRsqrtps : kRcpps; return operands.Operation == ReciprocalOperation::ReciprocalSquareRoot ? kVrsqrtps : kVrcpps; } @@ -70,8 +77,8 @@ const Entry& _sha1Entry(const Sha1Operands& operands) { return kSha1rnds4; } -Amd64OnlyMatch _trampoline(std::string name, const std::size_t length, LoweredBody body, const bool optional = false) { - return Amd64OnlyMatch{std::move(name), length, Amd64OnlyLowering::Trampoline, {}, std::move(body.Bytes), body.ReturnBranchOffset, optional, std::move(body.Relocations)}; +Amd64OnlyMatch _trampoline(std::string name, const std::size_t length, LoweredBody body, const bool optional = false, const bool window = false) { + return Amd64OnlyMatch{std::move(name), length, Amd64OnlyLowering::Trampoline, {}, std::move(body.Bytes), body.ReturnBranchOffset, optional, std::move(body.Relocations), window}; } void _moveTrailing(StubBodyBuilder& body, std::span trailing) { @@ -102,11 +109,47 @@ 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; } +std::vector _rdpidReplacement(const DecodedInstruction& instr) { + using namespace X64OpcodeConstants; + const auto opcode = instr.OpcodeOffset(); + const auto rex = opcode > 0 && instr.Data[opcode - 1] >= RexMin && instr.Data[opcode - 1] <= RexMax ? instr.Data[opcode - 1] : 0; + const auto reg = static_cast((instr.Data[opcode + 2] & ModRmRmMask) | ((rex & 1) << 3)); + std::vector bytes; + if (reg >= 8) + bytes.push_back(0x41); + bytes.push_back(static_cast(0xB8 + (reg & 7))); + bytes.insert(bytes.end(), 4, 0); + 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; @@ -114,6 +157,8 @@ bool _validWait(const DecodedInstruction& instr) { class Amd64OnlyInstructionMatcher : public IAmd64OnlyInstructionMatcher { public: + explicit Amd64OnlyInstructionMatcher(const Amd64OnlyTarget target) : _target(target) {} + [[nodiscard]] std::optional Match( const std::uint8_t* data, std::size_t length, @@ -125,18 +170,30 @@ public: std::span trailing ) const override; + void EmitOutOfLine( + StubBodyBuilder& body, + std::span instruction + ) const override; + private: + Amd64OnlyTarget _target; Sse4aLowering _lowering; Sha256Lowering _sha256Lowering; Sha1Lowering _sha1Lowering; ClzeroLowering _clzeroLowering; ReciprocalLowering _reciprocalLowering; + WideStoreLowering _wideStoreLowering; [[nodiscard]] Amd64OnlyMatch _matchMovnts(const DecodedInstruction& instr, const Entry& entry) const; [[nodiscard]] Amd64OnlyMatch _matchSse4a(const DecodedInstruction& instr, const Entry& entry, const Entry& registerFormEntry, std::span trailing) const; [[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 { @@ -177,6 +234,133 @@ 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)); + return Amd64OnlyMatch{kRdseed.Name, instr.Length, Amd64OnlyLowering::InPlace, std::move(replacement), {}, 0}; + } + if (instr.IsClwb()) + 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; +} + +const char* Amd64OnlyInstructionMatcher::_emitOutOfLine(StubBodyBuilder& body, const DecodedInstruction& instr) const { + if (instr.IsExtrq() || instr.IsInsertq()) { + const auto operands = DecodeSse4a(instr.Data, instr.Length); + _lowering.EmitOutOfLine(body, operands); + return _sse4aEntry(operands).Name; + } + if (instr.IsSha256()) { + const auto operands = DecodeSha256(instr.Data, instr.Length); + _sha256Lowering.EmitOutOfLine(body, operands); + return _sha256Entry(operands).Name; + } + if (instr.IsSha1()) { + const auto operands = DecodeSha1(instr.Data, instr.Length); + _sha1Lowering.EmitOutOfLine(body, operands); + return _sha1Entry(operands).Name; + } + if (instr.IsClzero()) { + _clzeroLowering.EmitOutOfLine(body, DecodeClzero(instr.Data, instr.Length)); + return kClzero.Name; + } + if (const auto reciprocal = DecodeReciprocal(instr.Data, instr.Length)) { + _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; +} + +void Amd64OnlyInstructionMatcher::EmitOutOfLine(StubBodyBuilder& body, const std::span instruction) const { + if (_emitOutOfLine(body, DecodedInstruction{instruction.data(), instruction.size()}) == nullptr) + throw CodegenException("AMD-only instruction has no out-of-line lowering"); + body.Advance(instruction.size()); +} + std::optional Amd64OnlyInstructionMatcher::MatchSequence( std::span> instructions, std::span trailing @@ -187,37 +371,18 @@ std::optional Amd64OnlyInstructionMatcher::MatchSequence( const char* name = nullptr; for (const auto& instruction : instructions) { const DecodedInstruction instr{instruction.data(), instruction.size()}; - if (instr.IsExtrq() || instr.IsInsertq()) { - const auto operands = DecodeSse4a(instr.Data, instr.Length); - if (name == nullptr) - name = _sse4aEntry(operands).Name; - _lowering.EmitOutOfLine(body, operands); - } else if (instr.IsSha256()) { - const auto operands = DecodeSha256(instr.Data, instr.Length); - if (name == nullptr) - name = _sha256Entry(operands).Name; - _sha256Lowering.EmitOutOfLine(body, operands); - } else if (instr.IsSha1()) { - const auto operands = DecodeSha1(instr.Data, instr.Length); - if (name == nullptr) - name = _sha1Entry(operands).Name; - _sha1Lowering.EmitOutOfLine(body, operands); - } else if (instr.IsClzero()) { - const auto operands = DecodeClzero(instr.Data, instr.Length); - if (name == nullptr) - name = kClzero.Name; - _clzeroLowering.EmitOutOfLine(body, operands); - } else if (const auto reciprocal = DecodeVexReciprocal(instr.Data, instr.Length)) { - if (name == nullptr) - name = _reciprocalEntry(*reciprocal).Name; - _reciprocalLowering.EmitOutOfLine(body, *reciprocal); - } else { + auto emitted = _emitOutOfLine(body, instr); + if (emitted == nullptr && name != nullptr) + emitted = _emitInPlace(body, instr); + if (emitted == nullptr) return std::nullopt; - } + if (name == nullptr) + name = emitted; body.Advance(instr.Length); } _moveTrailing(body, trailing); - const bool optional = DecodeVexReciprocal(instructions.front().data(), instructions.front().size()).has_value(); + const DecodedInstruction first{instructions.front().data(), instructions.front().size()}; + 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); } @@ -252,7 +417,7 @@ std::optional Amd64OnlyInstructionMatcher::Match( if (instr.IsMwaitx()) return _validWait(instr) ? _inPlace(kMwaitx, length, std::vector(kPause.Bytes, kPause.Bytes + kPause.Size)) : _unsupported(kMwaitx, length); - if (const auto reciprocal = DecodeVexReciprocal(data, length)) { + if (const auto reciprocal = DecodeReciprocal(data, length)) { return _trampoline(_reciprocalEntry(*reciprocal).Name, length, _outOfLine(length, trailing, [&](StubBodyBuilder& body) { _reciprocalLowering.EmitOutOfLine(body, *reciprocal); }), true); } @@ -263,18 +428,18 @@ 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 std::nullopt; + return _matchRosetta(instr, trailing); } } -std::unique_ptr MakeAmd64OnlyInstructionMatcher() { - return std::make_unique(); +std::unique_ptr MakeAmd64OnlyInstructionMatcher(const Amd64OnlyTarget target) { + return std::make_unique(target); } } 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 0c91e49acb..d54ce541bc 100644 --- a/core/relinker/codegen/src/x86/DecodedInstruction.cpp +++ b/core/relinker/codegen/src/x86/DecodedInstruction.cpp @@ -262,6 +262,68 @@ bool DecodedInstruction::IsMcommit() const { return Data[pos] == TwoByteOpcodeEscape && Data[pos + 1] == TwoByteGrp7 && Data[pos + 2] == 0xFA; } +bool DecodedInstruction::IsRdseed() const { + bool operandSizeOverride = false; + bool repnePrefix = false; + bool repPrefix = false; + const std::size_t pos = _skipPrefixesAndRex(&operandSizeOverride, &repnePrefix, &repPrefix); + + if (repnePrefix || repPrefix || pos + 2 >= Length) { + return false; + } + + const auto modrm = Data[pos + 2]; + return Data[pos] == TwoByteOpcodeEscape && Data[pos + 1] == TwoByteGrp9 && + ((modrm >> ModRmModShift) & ModRmModMask) == ModRmModRegister && ((modrm >> ModRmRegShift) & ModRmRegMask) == 7; +} + +bool DecodedInstruction::IsRdpid() const { + bool operandSizeOverride = false; + bool repnePrefix = false; + bool repPrefix = false; + const std::size_t pos = _skipPrefixesAndRex(&operandSizeOverride, &repnePrefix, &repPrefix); + + if (operandSizeOverride || repnePrefix || !repPrefix || pos + 2 >= Length) { + return false; + } + + const auto modrm = Data[pos + 2]; + return Data[pos] == TwoByteOpcodeEscape && Data[pos + 1] == TwoByteGrp9 && + ((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; + bool repPrefix = false; + const std::size_t pos = _skipPrefixesAndRex(&operandSizeOverride, &repnePrefix, &repPrefix); + + if (!operandSizeOverride || repnePrefix || repPrefix || pos + 2 >= Length) { + return false; + } + + const auto modrm = Data[pos + 2]; + return Data[pos] == TwoByteOpcodeEscape && Data[pos + 1] == TwoByteGrp15 && + ((modrm >> ModRmModShift) & ModRmModMask) != ModRmModRegister && ((modrm >> ModRmRegShift) & ModRmRegMask) == 6; +} + bool DecodedInstruction::IsMovntss() const { bool operandSizeOverride = false; bool repnePrefix = false; diff --git a/core/relinker/codegen/src/x86/ReciprocalLowering.cpp b/core/relinker/codegen/src/x86/ReciprocalLowering.cpp index 8f6d794b46..d01588ae86 100644 --- a/core/relinker/codegen/src/x86/ReciprocalLowering.cpp +++ b/core/relinker/codegen/src/x86/ReciprocalLowering.cpp @@ -46,11 +46,15 @@ void ReciprocalLowering::EmitOutOfLine(StubBodyBuilder& body, const ReciprocalOp body.SsePlain({0x0F, kSqrtps}, scratch, operands.Source); body.RipOperand({0x0F, kMovaps}, dst, _ones()); body.SsePlain({0x0F, kDivps}, dst, scratch); - _vexMovaps(body, dst, dst); + if (operands.Vex) + _vexMovaps(body, dst, dst); } else { body.RipOperand({0x0F, kMovaps}, scratch, _ones()); body.SsePlain({0x0F, kDivps}, scratch, operands.Source); - _vexMovaps(body, dst, scratch); + if (operands.Vex) + _vexMovaps(body, dst, scratch); + else + body.SsePlain({0x0F, kMovaps}, dst, scratch); } body.Restore(scratch); } diff --git a/core/relinker/codegen/src/x86/ReciprocalOperands.cpp b/core/relinker/codegen/src/x86/ReciprocalOperands.cpp index c9e2037684..a985003b5a 100644 --- a/core/relinker/codegen/src/x86/ReciprocalOperands.cpp +++ b/core/relinker/codegen/src/x86/ReciprocalOperands.cpp @@ -12,14 +12,15 @@ constexpr std::uint8_t kOpcodeRcpps = 0x53; } -std::optional DecodeVexReciprocal(const std::uint8_t* data, const std::size_t length) { +std::optional DecodeReciprocal(const std::uint8_t* data, const std::size_t length) { using namespace X64OpcodeConstants; - if (length < 4) + if (length < 3) return std::nullopt; bool registerExtension = false; bool rmExtension = false; std::uint8_t payload = 0; std::size_t opcodeOffset = 0; + bool vex = true; if (data[0] == OneByteVex2 && length == 4) { registerExtension = (data[1] & 0x80) == 0; payload = data[1]; @@ -31,13 +32,21 @@ std::optional DecodeVexReciprocal(const std::uint8_t* data, rmExtension = (data[1] & 0x20) == 0; payload = data[2]; opcodeOffset = 3; + } else if (data[0] == TwoByteOpcodeEscape && length == 3) { + opcodeOffset = 1; + vex = false; + } else if (data[0] >= RexMin && data[0] <= RexMax && length == 4 && data[1] == TwoByteOpcodeEscape) { + registerExtension = (data[0] & 0x04) != 0; + rmExtension = (data[0] & 0x01) != 0; + opcodeOffset = 2; + vex = false; } else { return std::nullopt; } const auto vvvv = static_cast((payload >> 3) & 0x0F); const bool wide = (payload & 0x04) != 0; const auto pp = static_cast(payload & 0x03); - if (vvvv != kVexUnusedRegister || wide || pp != 0) + if (vex && (vvvv != kVexUnusedRegister || wide || pp != 0)) return std::nullopt; const auto opcode = data[opcodeOffset]; if (opcode != kOpcodeRsqrtps && opcode != kOpcodeRcpps) @@ -49,6 +58,7 @@ std::optional DecodeVexReciprocal(const std::uint8_t* data, operands.Operation = opcode == kOpcodeRsqrtps ? ReciprocalOperation::ReciprocalSquareRoot : ReciprocalOperation::Reciprocal; operands.Destination = static_cast(((modrm >> ModRmRegShift) & ModRmRegMask) | (registerExtension ? 8 : 0)); operands.Source = static_cast((modrm & ModRmRmMask) | (rmExtension ? 8 : 0)); + operands.Vex = vex; return operands; } 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 a1b6e44b61..3210fc1e7c 100644 --- a/core/relinker/codegen/src/x86/StubBodyBuilder.cpp +++ b/core/relinker/codegen/src/x86/StubBodyBuilder.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include namespace Codegen { @@ -99,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); } @@ -150,7 +147,7 @@ void StubBodyBuilder::Load(const std::uint8_t reg, const MemoryOperand& operand) if (operand.Rm == ModRmRmSibPresent) _bytes.push_back(operand.Sib); if (operand.RipRelative) { - _relocations.push_back({_bytes.size(), _bytes.size() + Disp32Size, static_cast(_siteOffset + operand.InstructionLength) + operand.Displacement}); + _relocations.push_back({_bytes.size(), _bytes.size() + Disp32Size, _siteOffset + static_cast(operand.InstructionLength) + operand.Displacement}); _bytes.insert(_bytes.end(), Disp32Size, 0); return; } @@ -189,13 +186,84 @@ void StubBodyBuilder::Move(const std::span instruction, cons value |= static_cast(instruction[*ripDisplacementOffset + index]) << (index * 8); _bytes[start + *ripDisplacementOffset + index] = 0; } - _relocations.push_back({start + *ripDisplacementOffset, start + instruction.size(), static_cast(_siteOffset + instruction.size()) + static_cast(value)}); + _relocations.push_back({start + *ripDisplacementOffset, start + instruction.size(), _siteOffset + static_cast(instruction.size()) + static_cast(value)}); } Advance(instruction.size()); } +void StubBodyBuilder::RipRelative(const std::span instruction, const std::size_t displacementOffset, const std::int64_t target) { + using namespace X64OpcodeConstants; + if (displacementOffset > instruction.size() || instruction.size() - displacementOffset < Disp32Size) + throw CodegenException("RIP-relative displacement lies outside its instruction"); + const auto start = _bytes.size(); + Raw(instruction); + std::fill_n(_bytes.begin() + static_cast(start + displacementOffset), Disp32Size, std::uint8_t{0}); + _relocations.push_back({start + displacementOffset, start + instruction.size(), _siteOffset + target}); +} + +void StubBodyBuilder::MoveControlFlow(const std::span instruction, const DecodedInstructionInfo& info) { + using namespace X64OpcodeConstants; + 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()); + auto& modrm = jump[info.RipRelativeDispOffset - 1]; + modrm = static_cast((modrm & ~(ModRmRegMask << ModRmRegShift)) | (4u << ModRmRegShift)); + Move(jump, info.RipRelativeDispOffset); + return; + } + if (!info.HasBranchTarget || info.HasRipRelativeDisp) { + Move(instruction, info.HasRipRelativeDisp ? std::optional(info.RipRelativeDispOffset) : std::nullopt); + return; + } + for (std::size_t index = 0; index < info.OpcodeOffset; ++index) { + const auto prefix = instruction[index]; + if (prefix != PrefixSegCs && prefix != PrefixSegDs && prefix != PrefixRepne && prefix != PrefixAddressSize && (prefix & 0xF0) != kRexBase) + throw CodegenException("A relative branch with this prefix cannot move into a stub"); + } + const auto target = end + info.BranchDisp; + if (info.IsTwoByteOpcode) { + _rel32({TwoByteOpcodeEscape, info.Opcode}, target); + } else if (info.Opcode == OneByteCallRel32) { + _pushReturnAddress(end); + _rel32({OneByteJmpRel32}, target); + } else if (info.Opcode == OneByteJmpRel32 || info.Opcode == OneByteJmpRel8) { + _rel32({OneByteJmpRel32}, target); + } else if (info.Opcode >= OneByteJccRel8Min && info.Opcode <= OneByteJccRel8Max) { + _rel32({TwoByteOpcodeEscape, static_cast(TwoByteJccRel32Min | (info.Opcode & 0x0F))}, target); + } else { + throw CodegenException("A LOOP or JRCXZ branch cannot move into a stub"); + } + Advance(instruction.size()); +} + +void StubBodyBuilder::JumpTo(const std::int64_t siteTarget) { + _rel32({X64OpcodeConstants::OneByteJmpRel32}, siteTarget); +} + +void StubBodyBuilder::Seek(const std::int64_t siteOffset) { + _siteOffset = siteOffset; +} + void StubBodyBuilder::Advance(const std::size_t length) { - _siteOffset += length; + _siteOffset += static_cast(length); +} + +void StubBodyBuilder::_rel32(const std::initializer_list opcode, const std::int64_t siteTarget) { + _bytes.insert(_bytes.end(), opcode); + const auto displacement = _bytes.size(); + _bytes.insert(_bytes.end(), X64OpcodeConstants::Disp32Size, std::uint8_t{0}); + _relocations.push_back({displacement, _bytes.size(), siteTarget}); +} + +void StubBodyBuilder::_pushReturnAddress(const std::int64_t siteTarget) { + _bytes.insert(_bytes.end(), {0x48, 0x8D, 0x64, 0x24, 0xF8, 0x50}); + _rel32({0x48, 0x8D, 0x05}, siteTarget); + _bytes.insert(_bytes.end(), {0x48, 0x89, 0x44, 0x24, 0x08, 0x58}); } LoweredBody StubBodyBuilder::Finish() { diff --git a/core/relinker/codegen/src/x86/WideStoreLowering.cpp b/core/relinker/codegen/src/x86/WideStoreLowering.cpp new file mode 100644 index 0000000000..40e22c9a37 --- /dev/null +++ b/core/relinker/codegen/src/x86/WideStoreLowering.cpp @@ -0,0 +1,21 @@ +#include + +namespace Codegen { + +namespace { + +void _emitHalf(StubBodyBuilder& body, const WideStoreHalf& half) { + if (half.RipDisplacementOffset) + body.RipRelative(half.Bytes, *half.RipDisplacementOffset, half.RipTarget); + else + body.Raw(half.Bytes); +} + +} + +void WideStoreLowering::EmitOutOfLine(StubBodyBuilder& body, const WideStoreOperands& operands) const { + _emitHalf(body, operands.Low); + _emitHalf(body, operands.High); +} + +} diff --git a/core/relinker/codegen/src/x86/WideStoreOperands.cpp b/core/relinker/codegen/src/x86/WideStoreOperands.cpp new file mode 100644 index 0000000000..478c4108ed --- /dev/null +++ b/core/relinker/codegen/src/x86/WideStoreOperands.cpp @@ -0,0 +1,133 @@ +#include +#include +#include +#include + +namespace Codegen { + +namespace { + +constexpr std::uint8_t kVexMap0F = 0x01; +constexpr std::uint8_t kVexLength256 = 0x04; +constexpr std::uint8_t kVexUnusedRegister = 0x0F; +constexpr std::uint8_t kPpNone = 0x00; +constexpr std::uint8_t kPp66 = 0x01; +constexpr std::uint8_t kPpF3 = 0x02; +constexpr std::uint8_t kVextractf128 = 0x19; +constexpr std::uint8_t kVextractf128Payload = 0x7D; +constexpr std::uint8_t kUpperHalf = 0x01; +constexpr std::int64_t kHalfBytes = 16; + +bool _unalignedStore(const std::uint8_t pp, const std::uint8_t opcode) { + return ((pp == kPpNone || pp == kPp66) && opcode == 0x11) || (pp == kPpF3 && opcode == 0x7F); +} + +bool _legacyPrefixAllowed(const std::uint8_t prefix) { + using namespace X64OpcodeConstants; + return prefix == PrefixSegCs || prefix == PrefixSegSs || prefix == PrefixSegDs || prefix == PrefixSegEs || prefix == PrefixSegFs || prefix == PrefixSegGs || prefix == PrefixAddressSize; +} + +void _appendU32(std::vector& bytes, const std::uint32_t value) { + for (std::size_t index = 0; index < X64OpcodeConstants::Disp32Size; ++index) + bytes.push_back(static_cast(value >> (index * 8))); +} + +} + +std::optional DecodeVexWideStore(const std::uint8_t* data, const std::size_t length) { + using namespace X64OpcodeConstants; + std::size_t pos = 0; + while (pos < length && _legacyPrefixAllowed(data[pos])) + ++pos; + const auto vex = pos; + if (vex + 2 > length) + return std::nullopt; + std::uint8_t inverted = 0; + std::uint8_t payload = 0; + if (data[vex] == OneByteVex2) { + inverted = static_cast((data[vex + 1] & 0x80) | 0x60); + payload = data[vex + 1]; + pos = vex + Vex2PrefixLength; + } else if (data[vex] == OneByteVex3) { + if (vex + Vex3PrefixLength > length || (data[vex + 1] & Vex3MapMask) != kVexMap0F) + return std::nullopt; + inverted = static_cast(data[vex + 1] & 0xE0); + payload = data[vex + 2]; + pos = vex + Vex3PrefixLength; + } else { + return std::nullopt; + } + if ((payload & kVexLength256) == 0 || ((payload >> 3) & 0x0F) != kVexUnusedRegister) + return std::nullopt; + if (pos + 2 > length || !_unalignedStore(static_cast(payload & 0x03), data[pos])) + return std::nullopt; + const auto modRmOffset = pos + 1; + const auto modrm = data[modRmOffset]; + const auto mod = static_cast((modrm >> ModRmModShift) & ModRmModMask); + const auto rm = static_cast(modrm & ModRmRmMask); + if (mod == ModRmModRegister) + return std::nullopt; + auto cursor = modRmOffset + 1; + std::optional sib; + if (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 != length) + return std::nullopt; + std::int64_t displacement = 0; + if (displacementSize == Disp8Size) { + displacement = static_cast(data[cursor]); + } else if (displacementSize == Disp32Size) { + std::uint32_t value = 0; + for (std::size_t index = 0; index < Disp32Size; ++index) + value |= static_cast(data[cursor + index]) << (index * 8); + displacement = static_cast(value); + } + + WideStoreOperands operands; + operands.Low.Bytes.assign(data, data + length); + operands.Low.Bytes[data[vex] == OneByteVex2 ? vex + 1 : vex + 2] &= static_cast(~kVexLength256); + if (ripRelative) { + operands.Low.RipDisplacementOffset = cursor; + operands.Low.RipTarget = static_cast(length) + displacement; + } + + auto& high = operands.High.Bytes; + high.assign(data, data + vex); + high.insert(high.end(), {OneByteVex3, static_cast(inverted | Vex3Map0F3A), kVextractf128Payload, kVextractf128}); + const auto reg = static_cast(modrm & (ModRmRegMask << ModRmRegShift)); + std::int64_t highDisplacement = displacement + kHalfBytes; + std::uint8_t highMod = mod; + std::size_t highDisplacementSize = displacementSize; + if (!ripRelative && !absolute) { + if (mod == ModRmModIndirect || (mod == ModRmModDisp8 && highDisplacement > std::numeric_limits::max())) { + highMod = highDisplacement > std::numeric_limits::max() ? ModRmModDisp32 : ModRmModDisp8; + highDisplacementSize = highMod == ModRmModDisp8 ? Disp8Size : Disp32Size; + } + } + if (!ripRelative && highDisplacement > std::numeric_limits::max()) + throw CodegenException("256-bit store displacement leaves no room for its upper half"); + high.push_back(static_cast((highMod << ModRmModShift) | reg | rm)); + if (sib) + high.push_back(*sib); + const auto highDisplacementOffset = high.size(); + if (highDisplacementSize == Disp8Size) + high.push_back(static_cast(static_cast(highDisplacement))); + else if (ripRelative) + _appendU32(high, 0); + else if (highDisplacementSize == Disp32Size) + _appendU32(high, static_cast(static_cast(highDisplacement))); + high.push_back(kUpperHalf); + if (ripRelative) { + operands.High.RipDisplacementOffset = highDisplacementOffset; + operands.High.RipTarget = static_cast(length) + displacement + kHalfBytes; + } + return operands; +} + +} diff --git a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp index 7a0b2a0014..b22c3f8726 100644 --- a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp +++ b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp @@ -8,6 +8,7 @@ namespace Codegen { using namespace X64OpcodeConstants; std::size_t X64InstructionDecoder::Decode(const std::uint8_t* data, std::size_t available) const { + constexpr std::size_t maxInstructionLength = 15; std::size_t pos = 0; bool rexPresent = false; std::uint8_t rex = 0; @@ -16,6 +17,7 @@ std::size_t X64InstructionDecoder::Decode(const std::uint8_t* data, std::size_t bool repnePrefix = false; while (pos < available) { + if (pos == maxInstructionLength) throw CodegenException("Instruction exceeds 15 bytes"); const std::uint8_t b = data[pos]; if (b >= RexMin && b <= RexMax) { @@ -55,6 +57,7 @@ std::size_t X64InstructionDecoder::Decode(const std::uint8_t* data, std::size_t bool vexPresent = false; bool evexPresent = false; std::uint8_t vexMap = 0; + std::uint8_t xopMap = 0; std::uint8_t threeByteMap = 0; bool threeByteEscape = false; @@ -76,6 +79,12 @@ std::size_t X64InstructionDecoder::Decode(const std::uint8_t* data, std::size_t throw CodegenException("Instruction truncated after VEX3 prefix"); } pos += 1; + } else if (opcode == XopPrefix && pos < available && (data[pos] & Vex3MapMask) >= XopMapMin) { + if (pos + 1 >= available) { + throw CodegenException("Instruction truncated after XOP prefix"); + } + xopMap = static_cast(data[pos] & Vex3MapMask); + pos += 2; } else if (opcode == EvexPrefix) { if (pos + 2 >= available) { throw CodegenException("Instruction truncated after EVEX prefix"); @@ -101,7 +110,7 @@ std::size_t X64InstructionDecoder::Decode(const std::uint8_t* data, std::size_t } } - if (vexPresent || evexPresent) { + if (vexPresent || evexPresent || xopMap != 0) { if (pos >= available) { throw CodegenException("Instruction truncated after VEX prefix"); } @@ -114,7 +123,14 @@ std::size_t X64InstructionDecoder::Decode(const std::uint8_t* data, std::size_t const bool vectorImmediate = vexMap == 1 && ((opcode >= 0x70 && opcode <= 0x73) || opcode == 0xC2 || opcode == 0xC4 || opcode == 0xC5 || opcode == 0xC6); - if (vexPresent) { + if (xopMap != 0) { + hasModRm = true; + if (xopMap == XopMap8) { + immediateSize = ImmSize8; + } else if (xopMap == XopMapA) { + immediateSize = ImmSize32; + } + } else if (vexPresent) { if (vexMap == 1 && opcode >= VexNoModRmMin && opcode <= VexNoModRmMax) { hasModRm = false; } else { @@ -191,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 || @@ -225,8 +241,8 @@ std::size_t X64InstructionDecoder::Decode(const std::uint8_t* data, std::size_t } hasModRm = true; } else if (opcode >= TwoByteJccRel32Min && opcode <= TwoByteJccRel32Max) { - immediateSize = ImmSize32; - } else if (opcode == TwoByteMovImm8ModRm) { + immediateSize = operandImmediateSize; + } else if (opcode == TwoByteMovImm8ModRm || opcode == TwoByte3DNow) { hasModRm = true; immediateSize = ImmSize8; } else if (opcode >= TwoByteShiftImm8Min && opcode <= TwoByteShiftImm8Max) { @@ -267,6 +283,7 @@ std::size_t X64InstructionDecoder::Decode(const std::uint8_t* data, std::size_t } if (!hasModRm) { + if (pos + immediateSize > maxInstructionLength) throw CodegenException("Instruction exceeds 15 bytes"); if (pos + immediateSize > available) { throw CodegenException("Instruction truncated in immediate operand"); } @@ -316,6 +333,7 @@ std::size_t X64InstructionDecoder::Decode(const std::uint8_t* data, std::size_t pos += immediateSize; + if (pos > maxInstructionLength) throw CodegenException("Instruction exceeds 15 bytes"); if (pos > available) { throw CodegenException("Instruction truncated in displacement or immediate operand"); } @@ -337,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]; @@ -351,6 +371,12 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( b == PrefixOperandSize || b == PrefixAddressSize) { if (b >= PrefixSegFs && b <= PrefixSegGs) info.SegmentPrefix = b; + if (b == PrefixAddressSize) + info.AddressSize32 = true; + if (b == PrefixOperandSize) + operandSizeOverride = true; + if (b == PrefixRep) + repPrefix = true; info.RexPrefix = 0; ++pos; continue; @@ -370,6 +396,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; @@ -379,10 +407,19 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( std::uint8_t op = data[pos++]; bool twoByteOpcode = false; - if (op == OneByteVex2 || op == OneByteVex3 || op == EvexPrefix) { - pos += (op == OneByteVex2 ? 1 : (op == OneByteVex3 ? 2 : EvexPrefixLength - 1)) + 1; - if (pos < info.Length) + const bool xop = op == XopPrefix && pos < info.Length && (data[pos] & Vex3MapMask) >= XopMapMin; + if (op == OneByteVex2 || op == OneByteVex3 || op == EvexPrefix || xop) { + 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; } @@ -396,6 +433,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; @@ -443,6 +493,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; @@ -467,12 +519,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; } } @@ -537,7 +589,7 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( op == TwoByteImulRmModRm || op == TwoByteGrp7 || op == TwoByteGrp15 || op == TwoByteXadd || op == TwoByteGrp9 || op == TwoByteNopModRm || - op == TwoByteEndbr || op == TwoByteMovImm8ModRm || + op == TwoByteEndbr || op == TwoByteMovImm8ModRm || op == TwoByte3DNow || op == ThreeByteEscape38 || op == ThreeByteEscape3A || (op >= TwoByteShiftImm8Min && op <= TwoByteShiftImm8Max) || op == TwoByteShldImm8 || op == TwoByteShrdImm8 || @@ -555,6 +607,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/src/x86/X64InstructionRewriter.cpp b/core/relinker/codegen/src/x86/X64InstructionRewriter.cpp index a3899c3dd9..212bdaee22 100644 --- a/core/relinker/codegen/src/x86/X64InstructionRewriter.cpp +++ b/core/relinker/codegen/src/x86/X64InstructionRewriter.cpp @@ -74,6 +74,18 @@ RewriteResult X64InstructionRewriter::Rewrite( throw CodegenException("Rewrite offset is out of bounds", request.Offset); } + const auto scanner = MakeInstructionScanner(); + const auto matches = scanner->ScanCodeSection(codeSection, 0, codeSection.size()); + bool instructionStart = false; + for (const auto& match : matches) { + if (match.Offset == request.Offset) { + instructionStart = true; + break; + } + } + if (!instructionStart) + throw CodegenException("Rewrite offset is not an instruction boundary", request.Offset); + const X64InstructionDecoder decoder; const std::size_t decodedLength = decoder.Decode(codeSection.data() + offset, codeSection.size() - offset); @@ -90,9 +102,6 @@ RewriteResult X64InstructionRewriter::Rewrite( rewritten.insert(rewritten.end(), request.NewBytes.begin(), request.NewBytes.end()); rewritten.insert(rewritten.end(), codeSection.begin() + offset + decodedLength, codeSection.end()); - const auto scanner = MakeInstructionScanner(); - const auto matches = scanner->ScanCodeSection(codeSection, 0, codeSection.size()); - std::vector adjustments; for (const auto& match : matches) { diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index c3e4a1da25..08d527462d 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -8,12 +8,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -23,7 +25,7 @@ #include #include #include -#ifdef __linux__ +#if defined(__linux__) || defined(__APPLE__) #include #endif #include @@ -135,7 +137,10 @@ void decoderLengths() { {0x66, 0x48, 0x69, 0xC0, 1, 2, 3, 4}, {0x66, 0x48, 0x68, 1, 2, 3, 4}, {0x66, 0x48, 0xA9, 1, 2, 3, 4}, {0x66, 0x48, 0xF7, 0xC0, 1, 2, 3, 4}, {0x66, 0x81, 0xC0, 0x34, 0x12}, {0x48, 0x66, 0x81, 0xC0, 0x34, 0x12}, {0x66, 0x41, 0x81, 0xC0, 0x34, 0x12}, {0x66, 0xF7, 0xC0, 0x34, 0x12}, - {0x48, 0x64, 0x8B, 0x00}, {0x48, 0xF3, 0x0F, 0x2B, 0x00}, {0x48, 0x67, 0x0F, 0x01, 0xFC}, {0x48, 0x48, 0x0F, 0x01, 0xFC}}; + {0x48, 0x64, 0x8B, 0x00}, {0x48, 0xF3, 0x0F, 0x2B, 0x00}, {0x48, 0x67, 0x0F, 0x01, 0xFC}, {0x48, 0x48, 0x0F, 0x01, 0xFC}, + {0x8F, 0xE8, 0x40, 0xA3, 0xC1, 0x70}, {0x8F, 0xE8, 0xF8, 0xA3, 0x4A, 0x10, 0x60}, {0x8F, 0xE8, 0xC0, 0xA3, 0xBA, 0x70, 0xFF, 0xFF, 0xFF, 0x30}, + {0x8F, 0xE9, 0x78, 0x90, 0xC1}, {0x8F, 0xEA, 0x78, 0x10, 0xC0, 1, 2, 3, 4}, + {0x8F, 0xC0}, {0x41, 0x8F, 0x07}, {0x8F, 0x05, 1, 2, 3, 4}}; Bytes padded; for (const auto& instruction : instructions) { padded = instruction; @@ -156,7 +161,8 @@ const std::vector kRipRelativeVectorLoads = { {0xC4, 0xE2, 0x79, 0x00, 0x05, 0x10, 0x00, 0x00, 0x00}, {0x62, 0xF1, 0xFD, 0x08, 0x6F, 0x05, 0x10, 0x00, 0x00, 0x00}, {0x66, 0x0F, 0x38, 0x00, 0x05, 0x10, 0x00, 0x00, 0x00}, - {0x66, 0x0F, 0x3A, 0x0F, 0x05, 0x10, 0x00, 0x00, 0x00, 0x08}}; + {0x66, 0x0F, 0x3A, 0x0F, 0x05, 0x10, 0x00, 0x00, 0x00, 0x08}, + {0x8F, 0xE8, 0x78, 0xA3, 0x05, 0x10, 0x00, 0x00, 0x00, 0x30}}; const std::vector kRegisterVectorOperations = { {0xC5, 0xF9, 0x6F, 0xC1}, {0x66, 0x0F, 0x38, 0x00, 0xC1}, {0xC5, 0xF8, 0x77}, {0xC4, 0xE2, 0x79, 0x00, 0x00}}; @@ -164,7 +170,7 @@ void decoderRipRelative() { const Codegen::X64InstructionDecoder decoder; for (const auto& instruction : kRipRelativeVectorLoads) { const auto info = decoder.DecodeInstruction(instruction.data(), instruction.size()); - require(info.Length == instruction.size() && info.HasRipRelativeDisp && read(instruction, info.RipRelativeDispOffset) == 0x10, "VEX, EVEX or three-byte RIP-relative operand was not reported"); + require(info.Length == instruction.size() && info.HasRipRelativeDisp && read(instruction, info.RipRelativeDispOffset) == 0x10, "VEX, EVEX, XOP or three-byte RIP-relative operand was not reported"); } for (const auto& instruction : kRegisterVectorOperations) require(!decoder.DecodeInstruction(instruction.data(), instruction.size()).HasRipRelativeDisp, "Vector instruction without a RIP-relative operand was reported as RIP-relative"); @@ -344,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}); @@ -484,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); @@ -606,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() { @@ -645,18 +684,480 @@ void converterClzero() { require(failureOffset([&] { (void)converter->Convert(segmentRelative, {segmentHeader(text.size())}); }, "FS-relative CLZERO was accepted") == 0x20A, "CLZERO operand failure does not carry the file offset"); } +void rosettaSubstitutions() { + using Codegen::Amd64OnlyLowering; + 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}, 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}); + require(rdseed16 && rdseed16->ReplacementBytes == Bytes{0x66, 0x41, 0x0F, 0xC7, 0xF2}, "16-bit RDSEED was not rewritten to RDRAND"); + require(!match(rosetta, {0x0F, 0xC7, 0x38}).has_value(), "VMPTRST (the memory form of 0F C7 /7) was taken for RDSEED"); + const auto clwb = match(rosetta, {0x66, 0x0F, 0xAE, 0x74, 0x24, 0x08}); + require(clwb && clwb->Lowering == Amd64OnlyLowering::InPlace && clwb->ReplacementBytes == Bytes{0x66, 0x0F, 0x1F, 0x44, 0x00, 0x00} && clwb->InstructionName == "CLWB", "CLWB was not replaced by a NOP"); + require(!match(rosetta, {0x66, 0x0F, 0xAE, 0xF1}).has_value(), "TPAUSE (the register form of 66 0F AE /6) was taken for CLWB"); + require(!match(rosetta, {0x66, 0x0F, 0xAE, 0x38}).has_value(), "CLFLUSHOPT was taken for CLWB"); + 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() { + struct Case { + Bytes Original; + Bytes Low; + Bytes High; + const char* Name; + }; + const std::vector cases = { + {{0xC5, 0xFC, 0x11, 0x07}, {0xC5, 0xF8, 0x11, 0x07}, {0xC4, 0xE3, 0x7D, 0x19, 0x47, 0x10, 0x01}, "vmovups (%rdi)"}, + {{0xC4, 0x41, 0x7E, 0x7F, 0x4C, 0x87, 0x70}, {0xC4, 0x41, 0x7A, 0x7F, 0x4C, 0x87, 0x70}, {0xC4, 0x43, 0x7D, 0x19, 0x8C, 0x87, 0x80, 0x00, 0x00, 0x00, 0x01}, "vmovdqu 0x70(%r15,%rax,4)"}, + {{0xC5, 0xFC, 0x11, 0x4C, 0x24, 0xF8}, {0xC5, 0xF8, 0x11, 0x4C, 0x24, 0xF8}, {0xC4, 0xE3, 0x7D, 0x19, 0x4C, 0x24, 0x08, 0x01}, "vmovups -8(%rsp)"}, + {{0xC5, 0xFE, 0x7F, 0x93, 0x78, 0x56, 0x34, 0x12}, {0xC5, 0xFA, 0x7F, 0x93, 0x78, 0x56, 0x34, 0x12}, {0xC4, 0xE3, 0x7D, 0x19, 0x93, 0x88, 0x56, 0x34, 0x12, 0x01}, "vmovdqu 0x12345678(%rbx)"}, + {{0x64, 0xC5, 0xFC, 0x11, 0x04, 0x25, 0x40, 0x00, 0x00, 0x00}, {0x64, 0xC5, 0xF8, 0x11, 0x04, 0x25, 0x40, 0x00, 0x00, 0x00}, {0x64, 0xC4, 0xE3, 0x7D, 0x19, 0x04, 0x25, 0x50, 0x00, 0x00, 0x00, 0x01}, "vmovups %fs:0x40"}, + {{0xC4, 0x41, 0x7C, 0x11, 0x65, 0x00}, {0xC4, 0x41, 0x78, 0x11, 0x65, 0x00}, {0xC4, 0x43, 0x7D, 0x19, 0x65, 0x10, 0x01}, "vmovups (%r13)"}, + {{0xC4, 0xC1, 0x7C, 0x11, 0x47, 0x68}, {0xC4, 0xC1, 0x78, 0x11, 0x47, 0x68}, {0xC4, 0xC3, 0x7D, 0x19, 0x47, 0x78, 0x01}, "vmovups 0x68(%r15)"}}; + for (const auto& item : cases) { + const auto split = Codegen::DecodeVexWideStore(item.Original.data(), item.Original.size()); + require(split && split->Low.Bytes == item.Low && split->High.Bytes == item.High && !split->Low.RipDisplacementOffset && !split->High.RipDisplacementOffset, (std::string("256-bit store was not split into its 128-bit halves: ") + item.Name).c_str()); + } + const Bytes rip = {0xC5, 0xFD, 0x11, 0x1D, 0x00, 0x01, 0x00, 0x00}; + const auto ripSplit = Codegen::DecodeVexWideStore(rip.data(), rip.size()); + require(ripSplit && ripSplit->Low.Bytes == Bytes{0xC5, 0xF9, 0x11, 0x1D, 0x00, 0x01, 0x00, 0x00} && ripSplit->High.Bytes == Bytes{0xC4, 0xE3, 0x7D, 0x19, 0x1D, 0x00, 0x00, 0x00, 0x00, 0x01}, "RIP-relative 256-bit store was not split"); + require(ripSplit->Low.RipDisplacementOffset == 4 && ripSplit->Low.RipTarget == 8 + 0x100 && ripSplit->High.RipDisplacementOffset == 5 && ripSplit->High.RipTarget == 8 + 0x110, "RIP-relative halves do not address the original operand and the one 16 bytes above it"); + for (const auto& other : {Bytes{0xC5, 0xF8, 0x11, 0x07}, Bytes{0xC5, 0xFC, 0x11, 0xC1}, Bytes{0xC5, 0xFC, 0x10, 0x07}, Bytes{0xC4, 0xE2, 0x75, 0x2E, 0x07}, Bytes{0xC5, 0xF4, 0x11, 0x07}, Bytes{0xC4, 0xE3, 0x7D, 0x19, 0x07, 0x01}, Bytes{0xC5, 0xFC, 0x11, 0x04}, Bytes{0xC5, 0xFC, 0x11, 0x47}}) + require(!Codegen::DecodeVexWideStore(other.data(), other.size()).has_value(), "An instruction other than a plain 256-bit store was split"); + for (const auto& aligned : {Bytes{0xC5, 0xFC, 0x29, 0x4C, 0x24, 0xF8}, Bytes{0xC5, 0xFD, 0xE7, 0x93, 0x78, 0x56, 0x34, 0x12}, Bytes{0xC5, 0xFD, 0x7F, 0x1F}, Bytes{0xC5, 0xFC, 0x2B, 0x27}}) + require(!Codegen::DecodeVexWideStore(aligned.data(), aligned.size()).has_value(), "An aligned 256-bit store, which cannot cross a page, was split"); + const Bytes farRip = {0xC5, 0xFC, 0x11, 0x05, 0xF8, 0xFF, 0xFF, 0x7F}; + const auto farRipSplit = Codegen::DecodeVexWideStore(farRip.data(), farRip.size()); + require(farRipSplit && farRipSplit->High.RipTarget == std::int64_t{8} + 0x7FFFFFF8 + 0x10, "A RIP-relative 256-bit store near the end of its displacement range was not split"); + const Bytes farBase = {0xC5, 0xFC, 0x11, 0x80, 0xF8, 0xFF, 0xFF, 0x7F}; + requireFailure([&] { (void)Codegen::DecodeVexWideStore(farBase.data(), farBase.size()); }, "A 256-bit store whose upper half has no 32-bit displacement was kept"); + requireFailure([] { Codegen::StubBodyBuilder body; body.RipRelative(Bytes{0x90}, 0, 0); }, "A RIP-relative displacement outside its instruction was relocated"); + const auto rosetta = Codegen::MakeAmd64OnlyInstructionMatcher(Codegen::Amd64OnlyTarget::Rosetta); + const Bytes store = {0xC4, 0xC1, 0x7C, 0x11, 0x47, 0x68}; + const auto lowered = rosetta->Match(store.data(), store.size()); + require(lowered && lowered->Lowering == Codegen::Amd64OnlyLowering::Trampoline && !lowered->Optional && lowered->RelocatableWindow && lowered->InstructionName == "256-bit store", "A 256-bit store was not lowered through a required stub that may move its neighbours for Rosetta"); + Bytes expectedBody = cases.back().Low; + expectedBody.insert(expectedBody.end(), cases.back().High.begin(), cases.back().High.end()); + require(Bytes(lowered->StubBody.begin(), lowered->StubBody.begin() + static_cast(lowered->ReturnBranchOffset)) == expectedBody, "The 256-bit store stub is not the two halves"); + const std::vector> pair = {std::span(cases[0].Original), std::span(rip)}; + const auto sequence = rosetta->MatchSequence(pair, {}); + require(sequence && sequence->Relocations.size() == 2 && sequence->Relocations[0].SiteTarget == 4 + 8 + 0x100 && sequence->Relocations[1].SiteTarget == 4 + 8 + 0x110, "A RIP-relative 256-bit store after another is not relocated from its place in the site"); + require(!Codegen::MakeAmd64OnlyInstructionMatcher()->Match(store.data(), store.size()).has_value(), "A 256-bit store was split for an Intel host"); + const std::vector> single = {std::span(store)}; + require(!Codegen::MakeAmd64OnlyInstructionMatcher()->MatchSequence(single, {}).has_value(), "A 256-bit store was split in a sequence for an Intel host"); +} + +void converterWideStoreWindows() { + const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta); + const Bytes store = {0xC5, 0xFC, 0x11, 0x07}; + Bytes halves = {0xC5, 0xF8, 0x11, 0x07, 0xC4, 0xE3, 0x7D, 0x19, 0x47, 0x10, 0x01}; + const auto convert = [&](const Bytes& text) { + Bytes file(0x300 + text.size(), 0xCC); + std::copy(text.begin(), text.end(), file.begin() + 0x200); + return converter->Convert(file, {segmentHeader(text.size())}); + }; + const auto body = [](const Codegen::TrampolineSite& site) { + return Bytes(site.Body.begin(), site.Body.begin() + static_cast(site.ReturnBranchOffset)); + }; + const auto withStore = [&](Bytes before, const Bytes& after) { + before.insert(before.end(), store.begin(), store.end()); + before.insert(before.end(), after.begin(), after.end()); + return before; + }; + + const auto returned = convert(withStore({}, {0xC3})); + Bytes expected = halves; + expected.push_back(0xC3); + require(returned.Trampolines.size() == 1 && returned.KeptCount == 0 && returned.Reports.size() == 1 && returned.Reports[0].InstructionName == "256-bit store", "A 256-bit store followed by a return was not moved"); + require(returned.Trampolines[0].Offset == 0x200 && returned.Trampolines[0].Length == 5 && body(returned.Trampolines[0]) == expected, "A 256-bit store followed by a return did not end its stub with the return"); + + const auto called = convert(withStore({}, {0xE8, 0x10, 0x00, 0x00, 0x00})); + require(called.Trampolines.size() == 1 && called.Trampolines[0].Offset == 0x200 && called.Trampolines[0].Length == 9, "A 256-bit store followed by a call did not take the call"); + const auto& call = called.Trampolines[0]; + expected = halves; + expected.insert(expected.end(), {0x48, 0x8D, 0x64, 0x24, 0xF8, 0x50, 0x48, 0x8D, 0x05, 0, 0, 0, 0, 0x48, 0x89, 0x44, 0x24, 0x08, 0x58, 0xE9, 0, 0, 0, 0}); + require(body(call) == expected, "A moved call does not push its original return address and jump"); + require(call.Relocations.size() == 2 && call.Relocations[0].DisplacementOffset == halves.size() + 9 && call.Relocations[0].SiteTarget == 9 && call.Relocations[1].DisplacementOffset == halves.size() + 20 && call.Relocations[1].SiteTarget == 9 + 0x10, "A moved call returns to or calls the wrong address"); + + const auto branched = convert(withStore({}, {0x75, 0x10, 0x90})); + require(branched.Trampolines.size() == 1 && branched.Trampolines[0].Length == 6, "A 256-bit store followed by a short conditional branch did not take the branch"); + const auto& jcc = branched.Trampolines[0]; + expected = halves; + expected.insert(expected.end(), {0x0F, 0x85, 0, 0, 0, 0}); + require(body(jcc) == expected && jcc.Relocations.size() == 1 && jcc.Relocations[0].SiteTarget == 6 + 0x10, "A moved short conditional branch was not re-encoded with a 32-bit displacement to its target"); + + const auto jumped = convert(withStore({}, {0xEB, 0x10, 0x90})); + expected = halves; + expected.insert(expected.end(), {0xE9, 0, 0, 0, 0}); + require(jumped.Trampolines.size() == 1 && body(jumped.Trampolines[0]) == expected && jumped.Trampolines[0].Relocations.size() == 1 && jumped.Trampolines[0].Relocations[0].SiteTarget == 6 + 0x10, "A moved short jump was not re-encoded with a 32-bit displacement to its target"); + + const auto longBranched = convert(withStore({}, {0x0F, 0x85, 0x10, 0x00, 0x00, 0x00})); + expected = halves; + expected.insert(expected.end(), {0x0F, 0x85, 0, 0, 0, 0}); + require(longBranched.Trampolines.size() == 1 && longBranched.Trampolines[0].Length == 10 && body(longBranched.Trampolines[0]) == expected && longBranched.Trampolines[0].Relocations.size() == 1 && longBranched.Trampolines[0].Relocations[0].SiteTarget == 10 + 0x10, "A moved conditional branch with a 32-bit displacement does not reach its target"); + + const auto entered = convert(withStore({0x48, 0x83, 0xC1, 0x01}, {0xC3, 0xEB, 0xFD})); + expected = {0x48, 0x83, 0xC1, 0x01}; + expected.insert(expected.end(), halves.begin(), halves.end()); + require(entered.Trampolines.size() == 1 && entered.Trampolines[0].Offset == 0x200 && entered.Trampolines[0].Length == 8 && body(entered.Trampolines[0]) == expected, "A 256-bit store before a branch target did not take room from the instruction before it"); + + Bytes adds; + for (int index = 0; index < 4; ++index) + adds.insert(adds.end(), {0x48, 0x83, 0xC1, 0x01}); + Bytes after = {0xC3, 0xEB, 0xFD}; + after.insert(after.end(), adds.begin(), adds.end()); + const auto island = convert(withStore({0xE8, 0x00, 0x00, 0x00, 0x00}, after)); + require(island.Trampolines.size() == 2 && island.KeptCount == 0, "A 256-bit store with no movable neighbours was not lowered through an island"); + const auto& moved = island.Trampolines[0]; + const auto& lowered = island.Trampolines[1]; + expected = adds; + expected.insert(expected.end(), {0xE9, 0, 0, 0, 0}); + require(moved.Offset == 0x20C && moved.Length == 8 && body(moved) == expected && moved.Relocations.size() == 1 && moved.Relocations[0].SiteTarget == 16, "The instructions under the island were not moved and continued after it"); + expected = halves; + expected.insert(expected.end(), {0xE9, 0, 0, 0, 0}); + require(lowered.Offset == 0x214 && lowered.Length == 5 && body(lowered) == expected && lowered.Relocations.size() == 1 && lowered.Relocations[0].SiteTarget == -11, "The island does not store both halves and return after the original store"); + require(Bytes(island.Bytes.begin() + 0x205, island.Bytes.begin() + 0x209) == Bytes{0xEB, 0x0D, 0xCC, 0xCC}, "The 256-bit store was not replaced by a short jump to its island"); + + requireFailure([&] { (void)convert(withStore({0xE8, 0x00, 0x00, 0x00, 0x00}, {0xC3, 0xEB, 0xFD})); }, "A 256-bit store with no room for a stub jump was kept"); + + const auto paired = convert(withStore({}, {0xC5, 0xFC, 0x11, 0x0E, 0xC3})); + expected = halves; + expected.insert(expected.end(), {0xC5, 0xF8, 0x11, 0x0E, 0xC4, 0xE3, 0x7D, 0x19, 0x4E, 0x10, 0x01}); + require(paired.Trampolines.size() == 1 && paired.Trampolines[0].Length == 8 && body(paired.Trampolines[0]) == expected && paired.Reports.size() == 1, "A 256-bit store followed by another was not lowered with it in one stub"); + + const Bytes push = {0x48, 0x8D, 0x64, 0x24, 0xF8, 0x50, 0x48, 0x8D, 0x05, 0, 0, 0, 0, 0x48, 0x89, 0x44, 0x24, 0x08, 0x58}; + const auto ripCalled = convert(withStore({}, {0xFF, 0x15, 0x10, 0x00, 0x00, 0x00})); + expected = halves; + expected.insert(expected.end(), push.begin(), push.end()); + expected.insert(expected.end(), {0xFF, 0x25, 0, 0, 0, 0}); + const auto& ripCall = ripCalled.Trampolines[0]; + require(ripCalled.Trampolines.size() == 1 && ripCall.Length == 10 && body(ripCall) == expected, "A moved call through a RIP-relative pointer does not push its original return address and jump through the pointer"); + require(ripCall.Relocations.size() == 2 && ripCall.Relocations[0].SiteTarget == 10 && ripCall.Relocations[1].DisplacementOffset == halves.size() + 21 && ripCall.Relocations[1].SiteTarget == 10 + 0x10, "A moved call through a RIP-relative pointer returns to or reads the wrong address"); + + const auto ripJumped = convert(withStore({}, {0xFF, 0x25, 0x10, 0x00, 0x00, 0x00})); + expected = halves; + expected.insert(expected.end(), {0xFF, 0x25, 0, 0, 0, 0}); + require(ripJumped.Trampolines.size() == 1 && body(ripJumped.Trampolines[0]) == expected && ripJumped.Trampolines[0].Relocations.size() == 1 && ripJumped.Trampolines[0].Relocations[0].SiteTarget == 10 + 0x10, "A moved jump through a RIP-relative pointer reads the wrong address"); + + const auto relaxed = convert(withStore({}, {0x67, 0xE8, 0x10, 0x00, 0x00, 0x00})); + expected = halves; + expected.insert(expected.end(), push.begin(), push.end()); + expected.insert(expected.end(), {0xE9, 0, 0, 0, 0}); + require(relaxed.Trampolines.size() == 1 && relaxed.Trampolines[0].Length == 10 && body(relaxed.Trampolines[0]) == expected && relaxed.Trampolines[0].Relocations.size() == 2 && relaxed.Trampolines[0].Relocations[1].SiteTarget == 10 + 0x10, "A call with an address-size prefix was not moved like a call"); + + const Bytes add = {0x48, 0x83, 0xC1, 0x01}; + for (const auto& follower : {Bytes{0xFF, 0xD0}, Bytes{0xE3, 0x10}, Bytes{0x66, 0x75, 0x10}, Bytes{0x66, 0x0F, 0xAE, 0x37}}) { + const auto preceded = convert(withStore(add, follower)); + expected = add; + expected.insert(expected.end(), halves.begin(), halves.end()); + 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()); + throwing.insert(throwing.end(), {0xFF, 0xD0}); + throwing.insert(throwing.end(), adds.begin(), adds.end()); + const auto farther = convert(withStore({0xE8, 0x00, 0x00, 0x00, 0x00}, throwing)); + require(farther.Trampolines.size() == 2 && farther.Trampolines[0].Offset == 0x21A && farther.Trampolines[1].Offset == 0x222, "An island that would move a register call was not passed over for the next one"); + require(Bytes(farther.Bytes.begin() + 0x205, farther.Bytes.begin() + 0x207) == Bytes{0xEB, 0x1B}, "The 256-bit store does not jump to the island after the register call"); + + Bytes reentered = {0xFF, 0xD0, 0xEB, 0xF8}; + reentered.insert(reentered.end(), adds.begin(), adds.end()); + const auto beyondStore = convert(withStore(Bytes(adds.begin(), adds.begin() + 8), reentered)); + require(beyondStore.Trampolines.size() == 2 && beyondStore.Trampolines[0].Offset == 0x210 && beyondStore.Trampolines[1].Offset == 0x218, "An island before a 256-bit store that is a branch target took the store with it"); + require(Bytes(beyondStore.Bytes.begin() + 0x208, beyondStore.Bytes.begin() + 0x20A) == Bytes{0xEB, 0x0E}, "The 256-bit store that is a branch target does not jump to the island after it"); + + const Bytes farCall = {0xE8, 0x40, 0x00, 0x00, 0x00}; + Bytes claimedFirst = {0xFF, 0xD0}; + claimedFirst.insert(claimedFirst.end(), adds.begin(), adds.end()); + const auto afterSequence = convert(withStore({0xC5, 0xF8, 0x53, 0xC1, 0x48, 0x83, 0xC1, 0x01}, claimedFirst)); + require(afterSequence.Trampolines.size() == 3 && afterSequence.Trampolines[0].Offset == 0x200 && afterSequence.Trampolines[0].Length == 8 && afterSequence.Trampolines[1].Offset == 0x20E && afterSequence.Trampolines[2].Offset == 0x216, "A 256-bit store moved an instruction already moved into the stub of the instruction before it"); + + Bytes pendingRun = {0xFF, 0xD0, 0x48, 0x83, 0xC1, 0x01, 0xC5, 0xFC, 0x11, 0x47, 0x10}; + pendingRun.insert(pendingRun.end(), adds.begin(), adds.end()); + const auto pendingIsland = convert(withStore(farCall, pendingRun)); + require(pendingIsland.Trampolines.size() == 3 && pendingIsland.Trampolines[0].Offset == 0x214 && pendingIsland.Trampolines[1].Offset == 0x21C && pendingIsland.Trampolines[2].Offset == 0x20F && pendingIsland.Trampolines[2].Length == 5, "An island moved the call before a 256-bit store or another store that still needs its own stub"); + + Bytes enteredRun = {0xFF, 0xD0}; + for (int index = 0; index < 6; ++index) + enteredRun.insert(enteredRun.end(), add.begin(), add.end()); + enteredRun.insert(enteredRun.end(), {0xEB, 0xEE}); + const auto enteredIsland = convert(withStore(farCall, enteredRun)); + require(enteredIsland.Trampolines.size() == 2 && enteredIsland.Trampolines[0].Offset == 0x213 && enteredIsland.Trampolines[1].Offset == 0x21B, "An island was placed over a branch target"); + + Bytes returning = {0xFF, 0xD0}; + for (int index = 0; index < 3; ++index) + returning.insert(returning.end(), add.begin(), add.end()); + returning.push_back(0xC3); + const auto returnIsland = convert(withStore(farCall, returning)); + expected = Bytes(adds.begin(), adds.begin() + 12); + 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()); + require(afterBranch.Trampolines.size() == 1 && afterBranch.Trampolines[0].Offset == 0x200 && afterBranch.Trampolines[0].Length == 6 && body(afterBranch.Trampolines[0]) == expected && afterBranch.Trampolines[0].Relocations[0].SiteTarget == 2 + 0x10, "A 256-bit store after a conditional branch did not take the branch"); + + Bytes far = adds; + far.resize(8); + for (int index = 0; index < 60; ++index) + far.insert(far.end(), {0xFF, 0xD0}); + far.insert(far.end(), adds.begin(), adds.end()); + for (int index = 0; index < 70; ++index) + far.insert(far.end(), {0xFF, 0xD0}); + Bytes tail; + for (int index = 0; index < 11; ++index) + tail.insert(tail.end(), {0xFF, 0xD0}); + tail.insert(tail.end(), {0x90, 0x90}); + 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}; + const Bytes dispatch = {0x48, 0x63, 0x04, 0x81, 0x48, 0x01, 0xC8, 0xFF, 0xE0, 0x48, 0x83, 0xC1, 0x01, 0xC5, 0xFC, 0x11, 0x07, 0x48, 0x83, 0xC2, 0x01, 0xC3}; + const Bytes store = {0xC5, 0xFC, 0x11, 0x07}; + const Domain::ProgramHeader code{1, 5, 0x200, 0x1000, 0, 29, 29, 16}; + const Domain::ProgramHeader data{1, 4, 0x300, 0x3000, 0, 0x40, 0x40, 16}; + const auto table = [](const std::uint64_t base, const std::vector& targets) { + Bytes bytes; + for (const auto target : targets) { + const auto relative = static_cast(target - base); + for (std::size_t byte = 0; byte < 4; ++byte) + bytes.push_back(static_cast(relative >> (byte * 8))); + } + return bytes; + }; + const auto convert = [&](const Bytes& first, const std::vector>& placed, const std::vector& headers) { + Bytes file(0x400, 0xCC); + std::copy(first.begin(), first.end(), file.begin() + 0x200); + std::copy(dispatch.begin(), dispatch.end(), file.begin() + 0x207); + for (const auto& [offset, bytes] : placed) + std::copy(bytes.begin(), bytes.end(), file.begin() + static_cast(offset)); + return converter->Convert(file, headers); + }; + const auto placedAt = [](const Codegen::ConvertResult& result, const Domain::FileByteOffset offset) { + return result.Trampolines.size() == 1 && result.Trampolines[0].Offset == offset && result.Trampolines[0].Length == 8; + }; + + require(placedAt(convert(leaTable, {{0x300, table(0x3000, {0x1010, 0x1018})}, {0x330, store}}, {code, data}), 0x210), "A 256-bit store moved the instruction after it although a jump table enters that instruction"); + require(placedAt(convert(leaTable, {{0x300, table(0x3000, {0x1010, 0x1019, 0x1018})}}, {code, data}), 0x214), "A jump table was read past an entry that does not start an instruction"); + require(placedAt(convert({0x48, 0x8B, 0x0D, 0xF9, 0x1F, 0x00, 0x00}, {{0x300, table(0x3000, {0x1010, 0x1018})}}, {code, data}), 0x214), "A jump table was read at an address loaded rather than formed by LEA"); + require(placedAt(convert({0x48, 0x8D, 0x0D, 0xF7, 0x1F, 0x00, 0x00}, {{0x2FE, table(0x2FFE, {0x1018})}}, {code, data}), 0x214), "A jump table was read at an address outside every segment"); + require(placedAt(convert(leaTable, {{0x300, table(0x3000, {0x1010})}, {0x380, table(0x3000, {0x1018})}}, {code, data, {0x61000000, 4, 0x380, 0x3000, 0, 0x10, 0x10, 16}}), 0x214), "A jump table was read from a segment that is not loaded"); + require(placedAt(convert(leaTable, {{0x300, table(0x3000, {0x1010, 0x1018})}, {0x3A0, store}}, {code, data, {2, 5, 0x3A0, 0x2000, 0, 4, 4, 16}}), 0x210), "A segment that is not loaded was converted"); + requireFailure([&] { (void)convert(leaTable, {{0x300, table(0x3000, {0x1010, 0x1018})}}, {code, {1, 4, 0x300, 0x3000, 0, 0x1000, 0x1000, 16}}); }, "A jump table was read from a segment that exceeds the file"); +} + +void converterRosetta() { + const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta); + Bytes file(0x300, 0xCC); + const Bytes text = { + 0x0F, 0xC7, 0xF8, + 0xF3, 0x0F, 0xC7, 0xF9, + 0x48, 0x83, 0xC1, 0x01, + 0x66, 0x0F, 0xAE, 0x37, + 0xC3}; + std::copy(text.begin(), text.end(), file.begin() + 0x200); + const auto result = converter->Convert(file, {segmentHeader(text.size())}); + require(result.ReplacedCount == 2 && result.Trampolines.size() == 1 && result.KeptCount == 0 && result.Reports.size() == 3, "RDSEED, RDPID and CLWB were not converted for Rosetta"); + const auto& site = result.Trampolines[0]; + require(site.Offset == 0x203 && site.Length == 8 && result.Reports[1].InstructionName == "RDPID", "RDPID did not absorb the following instruction"); + const Bytes add = {0x48, 0x83, 0xC1, 0x01}; + require(Bytes(site.Body.begin(), site.Body.begin() + 5) == Bytes{0xB9, 0, 0, 0, 0} && Bytes(site.Body.begin() + static_cast(site.ReturnBranchOffset - add.size()), site.Body.begin() + static_cast(site.ReturnBranchOffset)) == add, "RDPID stub does not load 0 and then run the absorbed instruction"); + auto expected = file; + expected[0x202] = 0xF0; + const Bytes nop = {0x0F, 0x1F, 0x40, 0x00}; + std::copy(nop.begin(), nop.end(), expected.begin() + 0x20B); + require(result.Bytes == expected, "RDSEED or CLWB was replaced with the wrong bytes"); + auto beforeReturn = file; + beforeReturn[0x207] = 0xC3; + const auto kept = converter->Convert(beforeReturn, {segmentHeader(text.size())}); + require(kept.KeptCount == 1 && kept.Trampolines.empty() && kept.ReplacedCount == 2, "RDPID without room for a jump was not kept"); + const auto intel = Codegen::MakeAmd64OnlyConverter()->Convert(file, {segmentHeader(text.size())}); + 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::DecodeVexReciprocal(Bytes{0xC5, 0xF8, 0x52, 0xD5}.data(), 4); + 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"); - const auto vex2High = Codegen::DecodeVexReciprocal(Bytes{0xC5, 0x78, 0x53, 0xC1}.data(), 4); + const auto vex2High = Codegen::DecodeReciprocal(Bytes{0xC5, 0x78, 0x53, 0xC1}.data(), 4); require(vex2High && vex2High->Operation == Codegen::ReciprocalOperation::Reciprocal && vex2High->Destination == 8 && vex2High->Source == 1, "VEX2 VRCPPS with a high destination was not decoded"); - const auto vex3 = Codegen::DecodeVexReciprocal(Bytes{0xC4, 0x41, 0x78, 0x52, 0xC9}.data(), 5); + const auto vex3 = Codegen::DecodeReciprocal(Bytes{0xC4, 0x41, 0x78, 0x52, 0xC9}.data(), 5); require(vex3 && vex3->Destination == 9 && vex3->Source == 9, "VEX3 VRSQRTPS with high registers was not decoded"); - require(!Codegen::DecodeVexReciprocal(Bytes{0xC5, 0xFC, 0x52, 0xD5}.data(), 4), "256-bit VRSQRTPS must stay native"); - require(!Codegen::DecodeVexReciprocal(Bytes{0xC5, 0xFA, 0x52, 0xD5}.data(), 4), "VRSQRTSS must stay native"); - require(!Codegen::DecodeVexReciprocal(Bytes{0xC5, 0xF8, 0x52, 0x10}.data(), 4), "Memory form must stay native"); - require(!Codegen::DecodeVexReciprocal(Bytes{0xC5, 0xF8, 0x51, 0xD5}.data(), 4), "VSQRTPS is not a reciprocal"); - require(!Codegen::DecodeVexReciprocal(Bytes{0x0F, 0x52, 0xD5}.data(), 3), "Legacy RSQRTPS must stay native"); + require(!Codegen::DecodeReciprocal(Bytes{0xC5, 0xFC, 0x52, 0xD5}.data(), 4), "256-bit VRSQRTPS must stay native"); + require(!Codegen::DecodeReciprocal(Bytes{0xC5, 0xFA, 0x52, 0xD5}.data(), 4), "VRSQRTSS must stay native"); + require(!Codegen::DecodeReciprocal(Bytes{0xC5, 0xF8, 0x52, 0x10}.data(), 4), "Memory form must stay native"); + require(!Codegen::DecodeReciprocal(Bytes{0xC5, 0xF8, 0x51, 0xD5}.data(), 4), "VSQRTPS is not a reciprocal"); + require(vex2->Vex && vex2High->Vex && vex3->Vex, "VEX reciprocals must clear the upper vector bits"); + const auto legacy = Codegen::DecodeReciprocal(Bytes{0x0F, 0x52, 0xD5}.data(), 3); + require(legacy && !legacy->Vex && legacy->Operation == Codegen::ReciprocalOperation::ReciprocalSquareRoot && legacy->Destination == 2 && legacy->Source == 5, "Legacy RSQRTPS was not decoded"); + const auto legacyHigh = Codegen::DecodeReciprocal(Bytes{0x45, 0x0F, 0x53, 0xC9}.data(), 4); + require(legacyHigh && !legacyHigh->Vex && legacyHigh->Operation == Codegen::ReciprocalOperation::Reciprocal && legacyHigh->Destination == 9 && legacyHigh->Source == 9, "Legacy RCPPS with high registers was not decoded"); + for (const Bytes& unsupported : {Bytes{0xF3, 0x0F, 0x52, 0xD5}, Bytes{0xF3, 0x0F, 0x53, 0xD5}, Bytes{0x0F, 0x52, 0x10}, Bytes{0x0F, 0x53, 0x10}, Bytes{0x0F, 0x51, 0xD5}, Bytes{0x0F, 0x52}, Bytes{0x45, 0x0F, 0x53}}) + require(!Codegen::DecodeReciprocal(unsupported.data(), unsupported.size()), "An unsupported legacy reciprocal encoding was accepted"); } void converterReciprocal() { @@ -690,12 +1191,33 @@ void converterReciprocal() { require(moved.Trampolines.size() == 1 && moved.KeptCount == 0 && moved.Bytes == ripRelative && moved.Trampolines[0].Length == 10, "VRSQRTPS did not absorb the following RIP-relative load"); const auto& movedSite = moved.Trampolines[0]; require(movedSite.Relocations.size() == 1 && movedSite.Relocations[0].DisplacementOffset == movedSite.ReturnBranchOffset - 4 && movedSite.Relocations[0].InstructionEnd == movedSite.ReturnBranchOffset && movedSite.Relocations[0].SiteTarget == 10 + 0x10, "Absorbed RIP-relative load has the wrong relocation"); + for (const std::uint8_t opcode : {std::uint8_t{0x52}, std::uint8_t{0x53}}) { + const std::string name = opcode == 0x52 ? "RSQRTPS" : "RCPPS"; + for (const Bytes& instruction : {Bytes{0x0F, opcode, 0xD5}, Bytes{0x45, 0x0F, opcode, 0xC9}}) { + auto legacyFile = file; + Bytes legacyText = instruction; + legacyText.insert(legacyText.end(), {0x90, 0x90, 0xC3}); + std::copy(legacyText.begin(), legacyText.end(), legacyFile.begin() + 0x200); + const auto legacy = converter->Convert(legacyFile, {segmentHeader(legacyText.size())}); + require(legacy.Trampolines.size() == 1 && legacy.KeptCount == 0 && legacy.Reports[0].InstructionName == name && legacy.Trampolines[0].Length == 5, "Legacy reciprocal did not absorb enough instructions for a jump"); + legacyText = instruction; + legacyText.push_back(0xC3); + std::copy(legacyText.begin(), legacyText.end(), legacyFile.begin() + 0x200); + const auto kept = converter->Convert(legacyFile, {segmentHeader(legacyText.size())}); + require(kept.Trampolines.empty() && kept.KeptCount == 1 && kept.Bytes == legacyFile && kept.Reports[0].InstructionName == name, "Legacy reciprocal before a return must stay native"); + } + } + auto sequenceFile = file; + const Bytes sequenceText = {0x0F, 0x52, 0xD5, 0xC5, 0xF8, 0x53, 0xC1, 0xC3}; + std::copy(sequenceText.begin(), sequenceText.end(), sequenceFile.begin() + 0x200); + const auto sequence = converter->Convert(sequenceFile, {segmentHeader(sequenceText.size())}); + require(sequence.Trampolines.size() == 1 && sequence.KeptCount == 0 && sequence.Trampolines[0].Length == 7 && sequence.Reports[0].InstructionName == "RSQRTPS", "Mixed legacy and VEX reciprocals were not lowered together"); } void converterStrayRex() { const auto converter = Codegen::MakeAmd64OnlyConverter(); const auto convert = [&](const Bytes& text) { - Bytes file(0x300, 0xCC); + Bytes file(0x300 + text.size(), 0xCC); std::copy(text.begin(), text.end(), file.begin() + 0x200); return std::pair{file, converter->Convert(file, {segmentHeader(text.size())})}; }; @@ -752,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) { @@ -772,7 +1294,7 @@ Bytes elfFixture(const Bytes& text) { write(bytes, 24, 0x1000); write(bytes, 32, 64); write(bytes, 54, 56); - write(bytes, 56, 6); + write(bytes, 56, 7); write(bytes, 64, 1); write(bytes, 68, 5); write(bytes, 72, 0x200); @@ -806,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); @@ -856,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); @@ -983,10 +1558,10 @@ void reciprocalExecution() { for (const auto& lanes : inputs) { const std::uint64_t source[2] = {packLanes(lanes, 0), packLanes(lanes, 2)}; const std::uint64_t destination[2] = {0x1111111111111111ull, 0x2222222222222222ull}; - for (const bool squareRoot : {true, false}) { + for (const bool squareRoot : {true, false}) for (const bool vex : {false, true}) { const std::uint8_t opcode = squareRoot ? 0x52 : 0x53; - const Bytes distinct = {0xC5, 0xF8, opcode, 0xD5}; - const Bytes same = {0xC5, 0xF8, opcode, 0xD2}; + const Bytes distinct = vex ? Bytes{0xC5, 0xF8, opcode, 0xD5} : Bytes{0x0F, opcode, 0xD5}; + const Bytes same = vex ? Bytes{0xC5, 0xF8, opcode, 0xD2} : Bytes{0x0F, opcode, 0xD2}; const auto distinctOut = runRegisterFormStub(distinct, destination, source); const auto sameOut = runRegisterFormStub(same, source, source); float distinctLanes[4]; @@ -995,8 +1570,8 @@ void reciprocalExecution() { std::memcpy(sameLanes, sameOut.data(), sizeof(sameLanes)); for (std::size_t lane = 0; lane < 4; ++lane) { const float expected = squareRoot ? 1.0f / std::sqrt(lanes[lane]) : 1.0f / lanes[lane]; - require(sameLane(expected, distinctLanes[lane]), "VRSQRTPS/VRCPPS stub is not correctly rounded"); - require(sameLane(expected, sameLanes[lane]), "VRSQRTPS/VRCPPS stub with equal operands is not correctly rounded"); + require(sameLane(expected, distinctLanes[lane]), "Packed reciprocal stub is not correctly rounded"); + require(sameLane(expected, sameLanes[lane]), "Packed reciprocal stub with equal operands is not correctly rounded"); } } } @@ -1291,6 +1866,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() {} @@ -1299,6 +1960,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() { @@ -1309,6 +2006,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(); @@ -1330,12 +2039,26 @@ 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(); rewriterStrayRex(); @@ -1343,7 +2066,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 new file mode 100644 index 0000000000..1de81e75ab --- /dev/null +++ b/core/relinker/codegen/tests/DecoderLengthCases.hpp @@ -0,0 +1,974 @@ +#ifndef CODEGEN_TESTS_DECODERLENGTHCASES_HPP +#define CODEGEN_TESTS_DECODERLENGTHCASES_HPP + +#include + +struct DecoderLengthCase { + const char* Bytes; + std::size_t Length; +}; + +inline constexpr DecoderLengthCase kDecoderLengthCases[] = { + {"00846511223344", 7}, + {"01846511223344", 7}, + {"02846511223344", 7}, + {"03846511223344", 7}, + {"0484", 2}, + {"0584651122", 5}, + {"08846511223344", 7}, + {"09846511223344", 7}, + {"0a846511223344", 7}, + {"0b846511223344", 7}, + {"0c84", 2}, + {"0d84651122", 5}, + {"0f00846511223344", 8}, + {"0f01846511223344", 8}, + {"0f02846511223344", 8}, + {"0f03846511223344", 8}, + {"0f05", 2}, + {"0f06", 2}, + {"0f07", 2}, + {"0f08", 2}, + {"0f09", 2}, + {"0f0b", 2}, + {"0f0d846511223344", 8}, + {"0f0e", 2}, + {"0f0fc196", 4}, + {"0f0f08b7", 4}, + {"0f0f4f80b4", 5}, + {"0f0f846511223344b4", 9}, + {"0f0f0d11223344a6", 8}, + {"410f0f1c24bf", 6}, + {"0f10846511223344", 8}, + {"0f11846511223344", 8}, + {"0f12846511223344", 8}, + {"0f13846511223344", 8}, + {"0f14846511223344", 8}, + {"0f15846511223344", 8}, + {"0f16846511223344", 8}, + {"0f17846511223344", 8}, + {"0f18846511223344", 8}, + {"0f1c846511223344", 8}, + {"0f1f846511223344", 8}, + {"0f20c1", 3}, + {"0f21c1", 3}, + {"0f22c1", 3}, + {"0f23c1", 3}, + {"0f28846511223344", 8}, + {"0f29846511223344", 8}, + {"0f2a846511223344", 8}, + {"0f2b846511223344", 8}, + {"0f2c846511223344", 8}, + {"0f2d846511223344", 8}, + {"0f2e846511223344", 8}, + {"0f2f846511223344", 8}, + {"0f30", 2}, + {"0f31", 2}, + {"0f32", 2}, + {"0f33", 2}, + {"0f34", 2}, + {"0f35", 2}, + {"0f37", 2}, + {"0f3800846511223344", 9}, + {"0f3801846511223344", 9}, + {"0f3802846511223344", 9}, + {"0f3803846511223344", 9}, + {"0f3804846511223344", 9}, + {"0f3805846511223344", 9}, + {"0f3806846511223344", 9}, + {"0f3807846511223344", 9}, + {"0f3808846511223344", 9}, + {"0f3809846511223344", 9}, + {"0f380a846511223344", 9}, + {"0f380b846511223344", 9}, + {"0f381c846511223344", 9}, + {"0f381d846511223344", 9}, + {"0f381e846511223344", 9}, + {"0f388a846511223344", 9}, + {"0f388b846511223344", 9}, + {"0f38c8846511223344", 9}, + {"0f38c9846511223344", 9}, + {"0f38ca846511223344", 9}, + {"0f38cb846511223344", 9}, + {"0f38cc846511223344", 9}, + {"0f38cd846511223344", 9}, + {"0f38f0846511223344", 9}, + {"0f38f1846511223344", 9}, + {"0f38f6846511223344", 9}, + {"0f38f9846511223344", 9}, + {"0f38fc846511223344", 9}, + {"0f3a0f84651122334455", 10}, + {"0f3acc84651122334455", 10}, + {"0f40846511223344", 8}, + {"0f41846511223344", 8}, + {"0f42846511223344", 8}, + {"0f43846511223344", 8}, + {"0f44846511223344", 8}, + {"0f45846511223344", 8}, + {"0f46846511223344", 8}, + {"0f47846511223344", 8}, + {"0f48846511223344", 8}, + {"0f49846511223344", 8}, + {"0f4a846511223344", 8}, + {"0f4b846511223344", 8}, + {"0f4c846511223344", 8}, + {"0f4d846511223344", 8}, + {"0f4e846511223344", 8}, + {"0f4f846511223344", 8}, + {"0f50c1", 3}, + {"0f51846511223344", 8}, + {"0f52846511223344", 8}, + {"0f53846511223344", 8}, + {"0f54846511223344", 8}, + {"0f55846511223344", 8}, + {"0f56846511223344", 8}, + {"0f57846511223344", 8}, + {"0f58846511223344", 8}, + {"0f59846511223344", 8}, + {"0f5a846511223344", 8}, + {"0f5b846511223344", 8}, + {"0f5c846511223344", 8}, + {"0f5d846511223344", 8}, + {"0f5e846511223344", 8}, + {"0f5f846511223344", 8}, + {"0f60846511223344", 8}, + {"0f61846511223344", 8}, + {"0f62846511223344", 8}, + {"0f63846511223344", 8}, + {"0f64846511223344", 8}, + {"0f65846511223344", 8}, + {"0f66846511223344", 8}, + {"0f67846511223344", 8}, + {"0f68846511223344", 8}, + {"0f69846511223344", 8}, + {"0f6a846511223344", 8}, + {"0f6b846511223344", 8}, + {"0f6e846511223344", 8}, + {"0f6f846511223344", 8}, + {"0f7084651122334455", 9}, + {"0f74846511223344", 8}, + {"0f75846511223344", 8}, + {"0f76846511223344", 8}, + {"0f77", 2}, + {"0f7e846511223344", 8}, + {"0f7f846511223344", 8}, + {"0f8084651122", 6}, + {"0f8184651122", 6}, + {"0f8284651122", 6}, + {"0f8384651122", 6}, + {"0f8484651122", 6}, + {"0f8584651122", 6}, + {"0f8684651122", 6}, + {"0f8784651122", 6}, + {"0f8884651122", 6}, + {"0f8984651122", 6}, + {"0f8a84651122", 6}, + {"0f8b84651122", 6}, + {"0f8c84651122", 6}, + {"0f8d84651122", 6}, + {"0f8e84651122", 6}, + {"0f8f84651122", 6}, + {"0f90846511223344", 8}, + {"0f91846511223344", 8}, + {"0f92846511223344", 8}, + {"0f93846511223344", 8}, + {"0f94846511223344", 8}, + {"0f95846511223344", 8}, + {"0f96846511223344", 8}, + {"0f97846511223344", 8}, + {"0f98846511223344", 8}, + {"0f99846511223344", 8}, + {"0f9a846511223344", 8}, + {"0f9b846511223344", 8}, + {"0f9c846511223344", 8}, + {"0f9d846511223344", 8}, + {"0f9e846511223344", 8}, + {"0f9f846511223344", 8}, + {"0fa0", 2}, + {"0fa1", 2}, + {"0fa2", 2}, + {"0fa3846511223344", 8}, + {"0fa484651122334455", 9}, + {"0fa5846511223344", 8}, + {"0fa8", 2}, + {"0fa9", 2}, + {"0faa", 2}, + {"0fab846511223344", 8}, + {"0fac84651122334455", 9}, + {"0fad846511223344", 8}, + {"0fae846511223344", 8}, + {"0faf846511223344", 8}, + {"0fb0846511223344", 8}, + {"0fb1846511223344", 8}, + {"0fb2846511223344", 8}, + {"0fb3846511223344", 8}, + {"0fb4846511223344", 8}, + {"0fb5846511223344", 8}, + {"0fb6846511223344", 8}, + {"0fb7846511223344", 8}, + {"0fb9846511223344", 8}, + {"0fbb846511223344", 8}, + {"0fbc846511223344", 8}, + {"0fbd846511223344", 8}, + {"0fbe846511223344", 8}, + {"0fbf846511223344", 8}, + {"0fc0846511223344", 8}, + {"0fc1846511223344", 8}, + {"0fc284651122334455", 9}, + {"0fc3846511223344", 8}, + {"0fc484651122334455", 9}, + {"0fc5c111", 4}, + {"0fc684651122334455", 9}, + {"0fc8", 2}, + {"0fc9", 2}, + {"0fca", 2}, + {"0fcb", 2}, + {"0fcc", 2}, + {"0fcd", 2}, + {"0fce", 2}, + {"0fcf", 2}, + {"0fd1846511223344", 8}, + {"0fd2846511223344", 8}, + {"0fd3846511223344", 8}, + {"0fd4846511223344", 8}, + {"0fd5846511223344", 8}, + {"0fd7c1", 3}, + {"0fd8846511223344", 8}, + {"0fd9846511223344", 8}, + {"0fda846511223344", 8}, + {"0fdb846511223344", 8}, + {"0fdc846511223344", 8}, + {"0fdd846511223344", 8}, + {"0fde846511223344", 8}, + {"0fdf846511223344", 8}, + {"0fe0846511223344", 8}, + {"0fe1846511223344", 8}, + {"0fe2846511223344", 8}, + {"0fe3846511223344", 8}, + {"0fe4846511223344", 8}, + {"0fe5846511223344", 8}, + {"0fe7846511223344", 8}, + {"0fe8846511223344", 8}, + {"0fe9846511223344", 8}, + {"0fea846511223344", 8}, + {"0feb846511223344", 8}, + {"0fec846511223344", 8}, + {"0fed846511223344", 8}, + {"0fee846511223344", 8}, + {"0fef846511223344", 8}, + {"0ff1846511223344", 8}, + {"0ff2846511223344", 8}, + {"0ff3846511223344", 8}, + {"0ff4846511223344", 8}, + {"0ff5846511223344", 8}, + {"0ff6846511223344", 8}, + {"0ff7c1", 3}, + {"0ff8846511223344", 8}, + {"0ff9846511223344", 8}, + {"0ffa846511223344", 8}, + {"0ffb846511223344", 8}, + {"0ffc846511223344", 8}, + {"0ffd846511223344", 8}, + {"0ffe846511223344", 8}, + {"10846511223344", 7}, + {"11846511223344", 7}, + {"12846511223344", 7}, + {"13846511223344", 7}, + {"1484", 2}, + {"1584651122", 5}, + {"18846511223344", 7}, + {"19846511223344", 7}, + {"1a846511223344", 7}, + {"1b846511223344", 7}, + {"1c84", 2}, + {"1d84651122", 5}, + {"20846511223344", 7}, + {"21846511223344", 7}, + {"22846511223344", 7}, + {"23846511223344", 7}, + {"2484", 2}, + {"2584651122", 5}, + {"28846511223344", 7}, + {"29846511223344", 7}, + {"2a846511223344", 7}, + {"2b846511223344", 7}, + {"2c84", 2}, + {"2d84651122", 5}, + {"30846511223344", 7}, + {"31846511223344", 7}, + {"32846511223344", 7}, + {"33846511223344", 7}, + {"3484", 2}, + {"3584651122", 5}, + {"38846511223344", 7}, + {"39846511223344", 7}, + {"3a846511223344", 7}, + {"3b846511223344", 7}, + {"3c84", 2}, + {"3d84651122", 5}, + {"480584651122", 6}, + {"480d84651122", 6}, + {"481584651122", 6}, + {"481d84651122", 6}, + {"482584651122", 6}, + {"482d84651122", 6}, + {"483584651122", 6}, + {"483d84651122", 6}, + {"486884651122", 6}, + {"486984651122334455667708", 12}, + {"488184651122334455667708", 12}, + {"48a984651122", 6}, + {"48b88465112233445566", 10}, + {"48b98465112233445566", 10}, + {"48ba8465112233445566", 10}, + {"48bb8465112233445566", 10}, + {"48bc8465112233445566", 10}, + {"48bd8465112233445566", 10}, + {"48be8465112233445566", 10}, + {"48bf8465112233445566", 10}, + {"48c784651122334455667708", 12}, + {"48f784651122334455667708", 12}, + {"50", 1}, + {"51", 1}, + {"52", 1}, + {"53", 1}, + {"54", 1}, + {"55", 1}, + {"56", 1}, + {"57", 1}, + {"58", 1}, + {"59", 1}, + {"5a", 1}, + {"5b", 1}, + {"5c", 1}, + {"5d", 1}, + {"5e", 1}, + {"5f", 1}, + {"63846511223344", 7}, + {"66058465", 4}, + {"660d8465", 4}, + {"660f3810846511223344", 10}, + {"660f3814846511223344", 10}, + {"660f3815846511223344", 10}, + {"660f3817846511223344", 10}, + {"660f3820846511223344", 10}, + {"660f3821846511223344", 10}, + {"660f3822846511223344", 10}, + {"660f3823846511223344", 10}, + {"660f3824846511223344", 10}, + {"660f3825846511223344", 10}, + {"660f3828846511223344", 10}, + {"660f3829846511223344", 10}, + {"660f382a846511223344", 10}, + {"660f382b846511223344", 10}, + {"660f3830846511223344", 10}, + {"660f3831846511223344", 10}, + {"660f3832846511223344", 10}, + {"660f3833846511223344", 10}, + {"660f3834846511223344", 10}, + {"660f3835846511223344", 10}, + {"660f3837846511223344", 10}, + {"660f3838846511223344", 10}, + {"660f3839846511223344", 10}, + {"660f383a846511223344", 10}, + {"660f383b846511223344", 10}, + {"660f383c846511223344", 10}, + {"660f383d846511223344", 10}, + {"660f383e846511223344", 10}, + {"660f383f846511223344", 10}, + {"660f3840846511223344", 10}, + {"660f3841846511223344", 10}, + {"660f3880846511223344", 10}, + {"660f3881846511223344", 10}, + {"660f3882846511223344", 10}, + {"660f38cf846511223344", 10}, + {"660f38db846511223344", 10}, + {"660f38dc846511223344", 10}, + {"660f38dd846511223344", 10}, + {"660f38de846511223344", 10}, + {"660f38df846511223344", 10}, + {"660f38f5846511223344", 10}, + {"660f38f8846511223344", 10}, + {"660f3a0884651122334455", 11}, + {"660f3a0984651122334455", 11}, + {"660f3a0a84651122334455", 11}, + {"660f3a0b84651122334455", 11}, + {"660f3a0c84651122334455", 11}, + {"660f3a0d84651122334455", 11}, + {"660f3a0e84651122334455", 11}, + {"660f3a1484651122334455", 11}, + {"660f3a1584651122334455", 11}, + {"660f3a1684651122334455", 11}, + {"660f3a1784651122334455", 11}, + {"660f3a2084651122334455", 11}, + {"660f3a2184651122334455", 11}, + {"660f3a2284651122334455", 11}, + {"660f3a4084651122334455", 11}, + {"660f3a4184651122334455", 11}, + {"660f3a4284651122334455", 11}, + {"660f3a4484651122334455", 11}, + {"660f3a6084651122334455", 11}, + {"660f3a6184651122334455", 11}, + {"660f3a6284651122334455", 11}, + {"660f3a6384651122334455", 11}, + {"660f3ace84651122334455", 11}, + {"660f3acf84651122334455", 11}, + {"660f3adf84651122334455", 11}, + {"660f6c846511223344", 9}, + {"660f6d846511223344", 9}, + {"660f78c11122", 6}, + {"660f79c1", 4}, + {"660f7c846511223344", 9}, + {"660f7d846511223344", 9}, + {"660f801122", 5}, + {"660f841122", 5}, + {"660f8f1122", 5}, + {"660fd0846511223344", 9}, + {"660fd6846511223344", 9}, + {"660fe6846511223344", 9}, + {"66158465", 4}, + {"661d8465", 4}, + {"66258465", 4}, + {"662d8465", 4}, + {"66358465", 4}, + {"663d8465", 4}, + {"66480584651122", 7}, + {"66480f8411223344", 8}, + {"66480d84651122", 7}, + {"66481584651122", 7}, + {"66481d84651122", 7}, + {"66482584651122", 7}, + {"66482d84651122", 7}, + {"66483584651122", 7}, + {"66483d84651122", 7}, + {"66486884651122", 7}, + {"66486984651122334455667708", 13}, + {"66488184651122334455667708", 13}, + {"6648a984651122", 7}, + {"6648b88465112233445566", 11}, + {"6648b98465112233445566", 11}, + {"6648ba8465112233445566", 11}, + {"6648bb8465112233445566", 11}, + {"6648bc8465112233445566", 11}, + {"6648bd8465112233445566", 11}, + {"6648be8465112233445566", 11}, + {"6648bf8465112233445566", 11}, + {"6648c784651122334455667708", 13}, + {"6648e811223344", 7}, + {"6648e911223344", 7}, + {"6648f784651122334455667708", 13}, + {"66688465", 4}, + {"66698465112233445566", 10}, + {"66818465112233445566", 10}, + {"66a98465", 4}, + {"66b88465", 4}, + {"66b98465", 4}, + {"66ba8465", 4}, + {"66bb8465", 4}, + {"66bc8465", 4}, + {"66bd8465", 4}, + {"66be8465", 4}, + {"66bf8465", 4}, + {"66c78465112233445566", 10}, + {"66e81122", 4}, + {"66e91122", 4}, + {"66f78465112233445566", 10}, + {"676984651122334455667708", 12}, + {"678184651122334455667708", 12}, + {"67a084651122", 6}, + {"67a184651122", 6}, + {"67a284651122", 6}, + {"67a384651122", 6}, + {"67f784651122334455667708", 12}, + {"6884651122", 5}, + {"6984651122334455667708", 11}, + {"6a84", 2}, + {"6b84651122334455", 8}, + {"6c", 1}, + {"6d", 1}, + {"6e", 1}, + {"6f", 1}, + {"7084", 2}, + {"7184", 2}, + {"7284", 2}, + {"7384", 2}, + {"7484", 2}, + {"7584", 2}, + {"7684", 2}, + {"7784", 2}, + {"7884", 2}, + {"7984", 2}, + {"7a84", 2}, + {"7b84", 2}, + {"7c84", 2}, + {"7d84", 2}, + {"7e84", 2}, + {"7f84", 2}, + {"8084651122334455", 8}, + {"8184651122334455667708", 11}, + {"8384651122334455", 8}, + {"84846511223344", 7}, + {"85846511223344", 7}, + {"86846511223344", 7}, + {"87846511223344", 7}, + {"88846511223344", 7}, + {"89846511223344", 7}, + {"8a846511223344", 7}, + {"8b846511223344", 7}, + {"8c846511223344", 7}, + {"8d846511223344", 7}, + {"8e846511223344", 7}, + {"90", 1}, + {"91", 1}, + {"92", 1}, + {"93", 1}, + {"94", 1}, + {"95", 1}, + {"96", 1}, + {"97", 1}, + {"98", 1}, + {"99", 1}, + {"9b", 1}, + {"9c", 1}, + {"9d", 1}, + {"9e", 1}, + {"9f", 1}, + {"a08465112233445566", 9}, + {"a18465112233445566", 9}, + {"a28465112233445566", 9}, + {"a38465112233445566", 9}, + {"a4", 1}, + {"a5", 1}, + {"a6", 1}, + {"a7", 1}, + {"a884", 2}, + {"a984651122", 5}, + {"aa", 1}, + {"ab", 1}, + {"ac", 1}, + {"ad", 1}, + {"ae", 1}, + {"af", 1}, + {"b084", 2}, + {"b184", 2}, + {"b284", 2}, + {"b384", 2}, + {"b484", 2}, + {"b584", 2}, + {"b684", 2}, + {"b784", 2}, + {"b884651122", 5}, + {"b984651122", 5}, + {"ba84651122", 5}, + {"bb84651122", 5}, + {"bc84651122", 5}, + {"bd84651122", 5}, + {"be84651122", 5}, + {"bf84651122", 5}, + {"c084651122334455", 8}, + {"c184651122334455", 8}, + {"c28465", 3}, + {"c3", 1}, + {"c4e17d4bc1", 5}, + {"c4e1f913846511223344", 10}, + {"c4e1f917846511223344", 10}, + {"c4e1f944c1", 5}, + {"c4e1f96e846511223344", 10}, + {"c4e1f990846511223344", 10}, + {"c4e1f991846511223344", 10}, + {"c4e1f998c1", 5}, + {"c4e1f999c1", 5}, + {"c4e1f9c484651122334455", 11}, + {"c4e1f9c5c111", 6}, + {"c4e1f9d6846511223344", 10}, + {"c4e1f9f7c1", 5}, + {"c4e1fa7e846511223344", 10}, + {"c4e1fb92c1", 5}, + {"c4e1fb93c1", 5}, + {"c4e1fc77", 4}, + {"c4e1fd14846511223344", 10}, + {"c4e1fd15846511223344", 10}, + {"c4e1fd28846511223344", 10}, + {"c4e1fd29846511223344", 10}, + {"c4e1fd2b846511223344", 10}, + {"c4e1fd2e846511223344", 10}, + {"c4e1fd2f846511223344", 10}, + {"c4e1fd41c1", 5}, + {"c4e1fd42c1", 5}, + {"c4e1fd45c1", 5}, + {"c4e1fd46c1", 5}, + {"c4e1fd47c1", 5}, + {"c4e1fd4ac1", 5}, + {"c4e1fd50c1", 5}, + {"c4e1fd54846511223344", 10}, + {"c4e1fd55846511223344", 10}, + {"c4e1fd56846511223344", 10}, + {"c4e1fd57846511223344", 10}, + {"c4e1fd60846511223344", 10}, + {"c4e1fd61846511223344", 10}, + {"c4e1fd62846511223344", 10}, + {"c4e1fd63846511223344", 10}, + {"c4e1fd64846511223344", 10}, + {"c4e1fd65846511223344", 10}, + {"c4e1fd66846511223344", 10}, + {"c4e1fd67846511223344", 10}, + {"c4e1fd68846511223344", 10}, + {"c4e1fd69846511223344", 10}, + {"c4e1fd6a846511223344", 10}, + {"c4e1fd6b846511223344", 10}, + {"c4e1fd6c846511223344", 10}, + {"c4e1fd6d846511223344", 10}, + {"c4e1fd74846511223344", 10}, + {"c4e1fd75846511223344", 10}, + {"c4e1fd76846511223344", 10}, + {"c4e1fdc684651122334455", 11}, + {"c4e1fdd1846511223344", 10}, + {"c4e1fdd2846511223344", 10}, + {"c4e1fdd3846511223344", 10}, + {"c4e1fdd4846511223344", 10}, + {"c4e1fdd5846511223344", 10}, + {"c4e1fdd7c1", 5}, + {"c4e1fdd8846511223344", 10}, + {"c4e1fdd9846511223344", 10}, + {"c4e1fdda846511223344", 10}, + {"c4e1fddb846511223344", 10}, + {"c4e1fddc846511223344", 10}, + {"c4e1fddd846511223344", 10}, + {"c4e1fdde846511223344", 10}, + {"c4e1fddf846511223344", 10}, + {"c4e1fde0846511223344", 10}, + {"c4e1fde1846511223344", 10}, + {"c4e1fde2846511223344", 10}, + {"c4e1fde3846511223344", 10}, + {"c4e1fde4846511223344", 10}, + {"c4e1fde5846511223344", 10}, + {"c4e1fde7846511223344", 10}, + {"c4e1fde8846511223344", 10}, + {"c4e1fde9846511223344", 10}, + {"c4e1fdea846511223344", 10}, + {"c4e1fdeb846511223344", 10}, + {"c4e1fdec846511223344", 10}, + {"c4e1fded846511223344", 10}, + {"c4e1fdee846511223344", 10}, + {"c4e1fdef846511223344", 10}, + {"c4e1fdf1846511223344", 10}, + {"c4e1fdf2846511223344", 10}, + {"c4e1fdf3846511223344", 10}, + {"c4e1fdf4846511223344", 10}, + {"c4e1fdf5846511223344", 10}, + {"c4e1fdf6846511223344", 10}, + {"c4e1fdf8846511223344", 10}, + {"c4e1fdf9846511223344", 10}, + {"c4e1fdfa846511223344", 10}, + {"c4e1fdfb846511223344", 10}, + {"c4e1fdfc846511223344", 10}, + {"c4e1fdfd846511223344", 10}, + {"c4e1fdfe846511223344", 10}, + {"c4e1fe16846511223344", 10}, + {"c4e1fe52846511223344", 10}, + {"c4e1fe53846511223344", 10}, + {"c4e1fe5b846511223344", 10}, + {"c4e1fe6f846511223344", 10}, + {"c4e1fe7f846511223344", 10}, + {"c4e1ff10846511223344", 10}, + {"c4e1ff11846511223344", 10}, + {"c4e1ff12846511223344", 10}, + {"c4e1ff2a846511223344", 10}, + {"c4e1ff2c846511223344", 10}, + {"c4e1ff2d846511223344", 10}, + {"c4e1ff51846511223344", 10}, + {"c4e1ff58846511223344", 10}, + {"c4e1ff59846511223344", 10}, + {"c4e1ff5a846511223344", 10}, + {"c4e1ff5c846511223344", 10}, + {"c4e1ff5d846511223344", 10}, + {"c4e1ff5e846511223344", 10}, + {"c4e1ff5f846511223344", 10}, + {"c4e1ff7084651122334455", 11}, + {"c4e1ff7c846511223344", 10}, + {"c4e1ff7d846511223344", 10}, + {"c4e1ffc284651122334455", 11}, + {"c4e1ffd0846511223344", 10}, + {"c4e1ffe6846511223344", 10}, + {"c4e1fff0846511223344", 10}, + {"c4e27948c1", 5}, + {"c4e27949846511223344", 10}, + {"c4e2796e846511223344", 10}, + {"c4e2796f846511223344", 10}, + {"c4e27a5fc1", 5}, + {"c4e27b4a846511223344", 10}, + {"c4e27b4b846511223344", 10}, + {"c4e27b5cc1", 5}, + {"c4e27b5ec1", 5}, + {"c4e27b6bc1", 5}, + {"c4e27b6cc1", 5}, + {"c4e27d0c846511223344", 10}, + {"c4e27d0d846511223344", 10}, + {"c4e27d0e846511223344", 10}, + {"c4e27d0f846511223344", 10}, + {"c4e27d13846511223344", 10}, + {"c4e27d16846511223344", 10}, + {"c4e27d18846511223344", 10}, + {"c4e27d19846511223344", 10}, + {"c4e27d1a846511223344", 10}, + {"c4e27d2c846511223344", 10}, + {"c4e27d2d846511223344", 10}, + {"c4e27d2e846511223344", 10}, + {"c4e27d2f846511223344", 10}, + {"c4e27d36846511223344", 10}, + {"c4e27d46846511223344", 10}, + {"c4e27d52846511223344", 10}, + {"c4e27d53846511223344", 10}, + {"c4e27d58846511223344", 10}, + {"c4e27d59846511223344", 10}, + {"c4e27d5a846511223344", 10}, + {"c4e27d78846511223344", 10}, + {"c4e27d79846511223344", 10}, + {"c4e27dcf846511223344", 10}, + {"c4e27e72846511223344", 10}, + {"c4e27eb1846511223344", 10}, + {"c4e27ed2846511223344", 10}, + {"c4e27ed3846511223344", 10}, + {"c4e27f50846511223344", 10}, + {"c4e27f51846511223344", 10}, + {"c4e27fb0846511223344", 10}, + {"c4e27fcbc1", 5}, + {"c4e27fccc1", 5}, + {"c4e27fcdc1", 5}, + {"c4e27fda846511223344", 10}, + {"c4e2f8f2846511223344", 10}, + {"c4e2f941846511223344", 10}, + {"c4e2f9db846511223344", 10}, + {"c4e2f9e0846511223344", 10}, + {"c4e2f9e1846511223344", 10}, + {"c4e2f9e2846511223344", 10}, + {"c4e2f9e3846511223344", 10}, + {"c4e2f9e4846511223344", 10}, + {"c4e2f9e5846511223344", 10}, + {"c4e2f9e6846511223344", 10}, + {"c4e2f9e7846511223344", 10}, + {"c4e2f9e8846511223344", 10}, + {"c4e2f9e9846511223344", 10}, + {"c4e2f9ea846511223344", 10}, + {"c4e2f9eb846511223344", 10}, + {"c4e2f9ec846511223344", 10}, + {"c4e2f9ed846511223344", 10}, + {"c4e2f9ee846511223344", 10}, + {"c4e2f9ef846511223344", 10}, + {"c4e2fbf5846511223344", 10}, + {"c4e2fbf6846511223344", 10}, + {"c4e2fbf7846511223344", 10}, + {"c4e2fd00846511223344", 10}, + {"c4e2fd01846511223344", 10}, + {"c4e2fd02846511223344", 10}, + {"c4e2fd03846511223344", 10}, + {"c4e2fd04846511223344", 10}, + {"c4e2fd05846511223344", 10}, + {"c4e2fd06846511223344", 10}, + {"c4e2fd07846511223344", 10}, + {"c4e2fd08846511223344", 10}, + {"c4e2fd09846511223344", 10}, + {"c4e2fd0a846511223344", 10}, + {"c4e2fd0b846511223344", 10}, + {"c4e2fd17846511223344", 10}, + {"c4e2fd1c846511223344", 10}, + {"c4e2fd1d846511223344", 10}, + {"c4e2fd1e846511223344", 10}, + {"c4e2fd20846511223344", 10}, + {"c4e2fd21846511223344", 10}, + {"c4e2fd22846511223344", 10}, + {"c4e2fd23846511223344", 10}, + {"c4e2fd24846511223344", 10}, + {"c4e2fd25846511223344", 10}, + {"c4e2fd28846511223344", 10}, + {"c4e2fd29846511223344", 10}, + {"c4e2fd2a846511223344", 10}, + {"c4e2fd2b846511223344", 10}, + {"c4e2fd30846511223344", 10}, + {"c4e2fd31846511223344", 10}, + {"c4e2fd32846511223344", 10}, + {"c4e2fd33846511223344", 10}, + {"c4e2fd34846511223344", 10}, + {"c4e2fd35846511223344", 10}, + {"c4e2fd37846511223344", 10}, + {"c4e2fd38846511223344", 10}, + {"c4e2fd39846511223344", 10}, + {"c4e2fd3a846511223344", 10}, + {"c4e2fd3b846511223344", 10}, + {"c4e2fd3c846511223344", 10}, + {"c4e2fd3d846511223344", 10}, + {"c4e2fd3e846511223344", 10}, + {"c4e2fd3f846511223344", 10}, + {"c4e2fd40846511223344", 10}, + {"c4e2fd45846511223344", 10}, + {"c4e2fd47846511223344", 10}, + {"c4e2fd8c846511223344", 10}, + {"c4e2fd8e846511223344", 10}, + {"c4e2fd90846511223344", 10}, + {"c4e2fd91846511223344", 10}, + {"c4e2fd92846511223344", 10}, + {"c4e2fd93846511223344", 10}, + {"c4e2fd96846511223344", 10}, + {"c4e2fd97846511223344", 10}, + {"c4e2fd98846511223344", 10}, + {"c4e2fd99846511223344", 10}, + {"c4e2fd9a846511223344", 10}, + {"c4e2fd9b846511223344", 10}, + {"c4e2fd9c846511223344", 10}, + {"c4e2fd9d846511223344", 10}, + {"c4e2fd9e846511223344", 10}, + {"c4e2fd9f846511223344", 10}, + {"c4e2fda6846511223344", 10}, + {"c4e2fda7846511223344", 10}, + {"c4e2fda8846511223344", 10}, + {"c4e2fda9846511223344", 10}, + {"c4e2fdaa846511223344", 10}, + {"c4e2fdab846511223344", 10}, + {"c4e2fdac846511223344", 10}, + {"c4e2fdad846511223344", 10}, + {"c4e2fdae846511223344", 10}, + {"c4e2fdaf846511223344", 10}, + {"c4e2fdb4846511223344", 10}, + {"c4e2fdb5846511223344", 10}, + {"c4e2fdb6846511223344", 10}, + {"c4e2fdb7846511223344", 10}, + {"c4e2fdb8846511223344", 10}, + {"c4e2fdb9846511223344", 10}, + {"c4e2fdba846511223344", 10}, + {"c4e2fdbb846511223344", 10}, + {"c4e2fdbc846511223344", 10}, + {"c4e2fdbd846511223344", 10}, + {"c4e2fdbe846511223344", 10}, + {"c4e2fdbf846511223344", 10}, + {"c4e2fddc846511223344", 10}, + {"c4e2fddd846511223344", 10}, + {"c4e2fdde846511223344", 10}, + {"c4e2fddf846511223344", 10}, + {"c4e379de84651122334455", 11}, + {"c4e37d0284651122334455", 11}, + {"c4e37d0484651122334455", 11}, + {"c4e37d0584651122334455", 11}, + {"c4e37d0684651122334455", 11}, + {"c4e37d1884651122334455", 11}, + {"c4e37d1984651122334455", 11}, + {"c4e37d1d84651122334455", 11}, + {"c4e37d3884651122334455", 11}, + {"c4e37d3984651122334455", 11}, + {"c4e37d4684651122334455", 11}, + {"c4e37d4a84651122334455", 11}, + {"c4e37d4b84651122334455", 11}, + {"c4e37d4c84651122334455", 11}, + {"c4e3f91484651122334455", 11}, + {"c4e3f91584651122334455", 11}, + {"c4e3f91684651122334455", 11}, + {"c4e3f91784651122334455", 11}, + {"c4e3f92084651122334455", 11}, + {"c4e3f92184651122334455", 11}, + {"c4e3f92284651122334455", 11}, + {"c4e3f930c111", 6}, + {"c4e3f931c111", 6}, + {"c4e3f932c111", 6}, + {"c4e3f933c111", 6}, + {"c4e3f94184651122334455", 11}, + {"c4e3f96084651122334455", 11}, + {"c4e3f96184651122334455", 11}, + {"c4e3f96284651122334455", 11}, + {"c4e3f96384651122334455", 11}, + {"c4e3f9df84651122334455", 11}, + {"c4e3fbf084651122334455", 11}, + {"c4e3fd0084651122334455", 11}, + {"c4e3fd0184651122334455", 11}, + {"c4e3fd0884651122334455", 11}, + {"c4e3fd0984651122334455", 11}, + {"c4e3fd0a84651122334455", 11}, + {"c4e3fd0b84651122334455", 11}, + {"c4e3fd0c84651122334455", 11}, + {"c4e3fd0d84651122334455", 11}, + {"c4e3fd0e84651122334455", 11}, + {"c4e3fd0f84651122334455", 11}, + {"c4e3fd4084651122334455", 11}, + {"c4e3fd4284651122334455", 11}, + {"c4e3fd4484651122334455", 11}, + {"c4e3fd4884651122334455", 11}, + {"c4e3fd4984651122334455", 11}, + {"c4e3fd5c84651122334455", 11}, + {"c4e3fd5d84651122334455", 11}, + {"c4e3fd5e84651122334455", 11}, + {"c4e3fd5f84651122334455", 11}, + {"c4e3fd6884651122334455", 11}, + {"c4e3fd6984651122334455", 11}, + {"c4e3fd6a84651122334455", 11}, + {"c4e3fd6b84651122334455", 11}, + {"c4e3fd6c84651122334455", 11}, + {"c4e3fd6d84651122334455", 11}, + {"c4e3fd6e84651122334455", 11}, + {"c4e3fd6f84651122334455", 11}, + {"c4e3fd7884651122334455", 11}, + {"c4e3fd7984651122334455", 11}, + {"c4e3fd7a84651122334455", 11}, + {"c4e3fd7b84651122334455", 11}, + {"c4e3fd7c84651122334455", 11}, + {"c4e3fd7d84651122334455", 11}, + {"c4e3fd7e84651122334455", 11}, + {"c4e3fd7f84651122334455", 11}, + {"c4e3fdce84651122334455", 11}, + {"c4e3fdcf84651122334455", 11}, + {"c684651122334455", 8}, + {"c784651122334455667708", 11}, + {"c8846511", 4}, + {"c9", 1}, + {"ca8465", 3}, + {"cb", 1}, + {"cc", 1}, + {"cd84", 2}, + {"cf", 1}, + {"d0846511223344", 7}, + {"d1846511223344", 7}, + {"d2846511223344", 7}, + {"d3846511223344", 7}, + {"d7", 1}, + {"d8846511223344", 7}, + {"d9846511223344", 7}, + {"da846511223344", 7}, + {"db846511223344", 7}, + {"dc846511223344", 7}, + {"dd846511223344", 7}, + {"de846511223344", 7}, + {"df846511223344", 7}, + {"e084", 2}, + {"e184", 2}, + {"e284", 2}, + {"e384", 2}, + {"e484", 2}, + {"e584", 2}, + {"e684", 2}, + {"e784", 2}, + {"e884651122", 5}, + {"e984651122", 5}, + {"eb84", 2}, + {"ec", 1}, + {"ed", 1}, + {"ee", 1}, + {"ef", 1}, + {"f20fd6c1", 4}, + {"f20ff0846511223344", 9}, + {"f30f38d8846511223344", 10}, + {"f30f38fac1", 5}, + {"f30f38fbc1", 5}, + {"f30fb8846511223344", 9}, + {"f3c784651122334455667708", 12}, + {"f4", 1}, + {"f5", 1}, + {"f684651122334455", 8}, + {"f784651122334455667708", 11}, + {"f8", 1}, + {"f9", 1}, + {"fa", 1}, + {"fb", 1}, + {"fc", 1}, + {"fd", 1}, + {"fe846511223344", 7}, + {"ff846511223344", 7}, +}; + +#endif diff --git a/core/relinker/codegen/tests/DecoderLengthTests.cpp b/core/relinker/codegen/tests/DecoderLengthTests.cpp new file mode 100644 index 0000000000..3ad1299b0d --- /dev/null +++ b/core/relinker/codegen/tests/DecoderLengthTests.cpp @@ -0,0 +1,152 @@ +#include "DecoderLengthCases.hpp" +#include "DecoderMetadataCases.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using Bytes = std::vector; + +void require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +Bytes parse(std::string_view hex) { + Bytes bytes; + for (std::size_t index = 0; index + 1 < hex.size(); index += 2) { + bytes.push_back(static_cast(std::stoul(std::string(hex.substr(index, 2)), nullptr, 16))); + } + return bytes; +} + +std::size_t decodedLength(const Codegen::X64InstructionDecoder& decoder, const Bytes& bytes, const std::string& name) { + try { + return decoder.Decode(bytes.data(), bytes.size()); + } catch (const Codegen::CodegenException& error) { + throw std::runtime_error("Cannot decode " + name + ": " + error.what()); + } +} + +bool truncationRejected(const Codegen::X64InstructionDecoder& decoder, const Bytes& bytes, std::size_t available) { + try { + (void)decoder.Decode(bytes.data(), available); + } catch (const Codegen::CodegenException&) { + return true; + } + return false; +} + +} + +int main() { + try { + const Codegen::X64InstructionDecoder decoder; + for (const auto& testCase : kDecoderLengthCases) { + const std::string name = testCase.Bytes; + auto bytes = parse(testCase.Bytes); + require(bytes.size() == testCase.Length, "Decoder length case " + name + " does not hold its own length"); + require(decodedLength(decoder, bytes, name) == testCase.Length, "Wrong length for " + name); + require(truncationRejected(decoder, bytes, bytes.size() - 1), "A truncated " + name + " was accepted"); + bytes.insert(bytes.end(), 15, 0x90); + require(decodedLength(decoder, bytes, name + " followed by NOPs") == testCase.Length, "Wrong length for " + name + " followed by NOPs"); + } + for (const auto& testCase : std::vector>{ + {14, {0x90}}, + {12, {0x68, 0x00, 0x00}}, + {8, {0x8b, 0x84, 0x25, 0x00, 0x00, 0x00, 0x00}} + }) { + Bytes longest(testCase.first, 0x66); + longest.insert(longest.end(), testCase.second.begin(), testCase.second.end()); + require(longest.size() == 15, "Maximum-length instruction fixture is not 15 bytes"); + require(decoder.Decode(longest.data(), longest.size()) == 15, "Valid 15-byte instruction was rejected"); + require(decoder.DecodeInstruction(longest.data(), longest.size()).Length == 15, "Valid 15-byte instruction metadata was rejected"); + Bytes withTrailingNops = longest; + withTrailingNops.insert(withTrailingNops.end(), 15, 0x90); + require(decoder.Decode(withTrailingNops.data(), withTrailingNops.size()) == 15, "Valid maximum-length instruction consumed trailing bytes"); + longest.insert(longest.begin(), 0x66); + require(truncationRejected(decoder, longest, longest.size()), "Instruction longer than 15 bytes was accepted"); + try { + (void)decoder.DecodeInstruction(longest.data(), longest.size()); + throw std::runtime_error("Instruction metadata accepted a length above 15 bytes"); + } 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'; + return 1; + } + return 0; +} 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/codegen/tests/RewriteBoundaryTests.cpp b/core/relinker/codegen/tests/RewriteBoundaryTests.cpp new file mode 100644 index 0000000000..fa9ee00553 --- /dev/null +++ b/core/relinker/codegen/tests/RewriteBoundaryTests.cpp @@ -0,0 +1,27 @@ +#include +#include +#include + +static void Require(bool ok) { if (!ok) throw std::runtime_error("Invalid rewrite boundary accepted"); } + +int main() { + try { + const Codegen::X64InstructionRewriter rewriter; + for (const auto& code : {std::vector{0xB8, 0x90, 0x90, 0x90, 0x90, 0xC3}, + std::vector{0x48, 0x8B, 0x05, 0x90, 0x90, 0x90, 0x90, 0xC3}, + std::vector{0xE8, 0x90, 0x90, 0x90, 0x90, 0xC3}}) { + const auto original = code; + for (std::size_t offset = 1; offset + 1 < code.size(); ++offset) { + bool rejected = false; + try { (void)rewriter.Rewrite(code, {offset, {0x90}}); } + catch (const std::exception&) { rejected = true; } + Require(rejected && code == original); + } + const auto result = rewriter.Rewrite(code, {0, {0x90}}); + Require(result.Bytes == std::vector{0x90, 0xC3}); + } + const auto result = rewriter.Rewrite({0x90, 0x90, 0xC3}, {1, {0x90, 0x90}}); + Require(result.Bytes == std::vector{0x90, 0x90, 0x90, 0xC3}); + std::cout << "18 rewrite boundary cases passed\n"; + } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } +} diff --git a/core/relinker/domain/include/domain/GuestRuntime.hpp b/core/relinker/domain/include/domain/GuestRuntime.hpp index 2c45abb92c..d4828544a6 100644 --- a/core/relinker/domain/include/domain/GuestRuntime.hpp +++ b/core/relinker/domain/include/domain/GuestRuntime.hpp @@ -2,6 +2,7 @@ #define DOMAIN_GUESTRUNTIME_HPP #include +#include #include #include @@ -13,6 +14,8 @@ struct GuestImport { std::uint64_t Addend; std::uint32_t RelocationType = 1; std::string Library; + bool Function = false; + bool Weak = false; }; struct GuestRuntime { @@ -24,6 +27,9 @@ struct GuestRuntime { std::uint32_t FiniRva = 0; bool UsePlatformTlsResolver = true; std::vector Names; + std::vector UnmatchedImportModules; + std::set ZeroReturnExports; + bool DeferInitialization = false; }; } diff --git a/core/relinker/domain/include/domain/IProgress.hpp b/core/relinker/domain/include/domain/IProgress.hpp new file mode 100644 index 0000000000..27f40568da --- /dev/null +++ b/core/relinker/domain/include/domain/IProgress.hpp @@ -0,0 +1,28 @@ +#ifndef DOMAIN_IPROGRESS_HPP +#define DOMAIN_IPROGRESS_HPP + +#include +#include +#include + +namespace Domain { + +enum class ProgressStage { Executable, Modules, Syscalls, Decode, Lowering, Imports, Image, Write }; + +struct ProgressEvent { + ProgressStage Stage; + std::string Input; + std::uint64_t Completed; + std::uint64_t Total; +}; + +class IProgress { +public: + virtual ~IProgress() = default; + virtual void Report(const ProgressEvent& event) = 0; + virtual void Message(std::string_view message) = 0; +}; + +} + +#endif diff --git a/core/relinker/domain/include/domain/ImportModule.hpp b/core/relinker/domain/include/domain/ImportModule.hpp index a2bd98e33d..18b018f770 100644 --- a/core/relinker/domain/include/domain/ImportModule.hpp +++ b/core/relinker/domain/include/domain/ImportModule.hpp @@ -3,22 +3,33 @@ #include #include +#include #include #include namespace Domain { +inline constexpr std::array ModuleSuffixes = {".prx", ".sprx", ".suprx", ".debug_prx"}; + +inline std::string_view ModuleSuffix(const std::string_view name) { + const auto suffix = std::find_if(ModuleSuffixes.begin(), ModuleSuffixes.end(), [&](const auto candidate) { return name.ends_with(candidate); }); + 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) { - if (name.ends_with(".prx")) name.resize(name.size() - 4); - else if (name.ends_with(".sprx")) name.resize(name.size() - 5); - else if (name.ends_with(".suprx")) name.resize(name.size() - 6); + name.resize(name.size() - ModuleSuffix(name).size()); if (name.ends_with("-module")) name.resize(name.size() - 7); std::replace(name.begin(), name.end(), '.', '_'); return name; } -inline std::string ImportModule(const std::string& symbol, const std::map& modules, - std::span dependencies = {}) { +inline std::string DeclaredImportModule(const std::string& symbol, const std::map& modules) { if (modules.empty()) return {}; const auto first = symbol.find('#'); if (first == std::string::npos) return {}; @@ -38,7 +49,14 @@ inline std::string ImportModule(const std::string& symbol, const std::mapsecond; if (name.empty() || name.find_first_of("/\\:$\r\n") != std::string::npos) throw RelinkerException("Invalid import module name: " + name); - if (!name.ends_with(".prx") && !name.ends_with(".sprx") && !name.ends_with(".suprx")) name += ".prx"; + return name; +} + +inline std::string ImportModule(const std::string& symbol, const std::map& modules, + std::span dependencies = {}) { + auto name = DeclaredImportModule(symbol, modules); + if (name.empty()) return {}; + if (ModuleSuffix(name).empty()) name += ".prx"; if (std::find(dependencies.begin(), dependencies.end(), name) != dependencies.end()) return name; std::string matched; for (const auto& dependency : dependencies) { diff --git a/core/relinker/domain/include/domain/InputProgress.hpp b/core/relinker/domain/include/domain/InputProgress.hpp new file mode 100644 index 0000000000..185274552c --- /dev/null +++ b/core/relinker/domain/include/domain/InputProgress.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +namespace Domain { + +class InputProgress final : public IProgress { +public: + InputProgress(IProgress& progress, std::string input) : progress(progress), input(std::move(input)) {} + + void Report(const ProgressEvent& event) override { + auto named = event; + if (named.Input.empty()) named.Input = input; + progress.Report(named); + } + + void Message(std::string_view message) override { progress.Message(message); } + +private: + IProgress& progress; + std::string input; +}; + +} diff --git a/core/relinker/domain/include/domain/Types.hpp b/core/relinker/domain/include/domain/Types.hpp index e49cc03472..dd181853e0 100644 --- a/core/relinker/domain/include/domain/Types.hpp +++ b/core/relinker/domain/include/domain/Types.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -15,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; @@ -60,6 +67,8 @@ struct NidReference { FileByteOffset RelocationTableOffset; VirtualAddress RelocationAddress; std::int64_t Addend; + bool Weak = false; + bool Function = false; }; struct CallSiteInfo { @@ -96,6 +105,9 @@ struct SysVDynamicSection { std::vector RelaPltData; std::vector GuestModules; std::map ImportModules; + std::set FunctionImports; + std::set UnmatchedImportModules; + std::vector NeededLibraries; }; struct CallRegistryEntry { @@ -107,6 +119,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 029c72c3b2..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 { @@ -51,23 +52,27 @@ inline constexpr std::size_t kRelaPltAlignment = 8; inline constexpr std::size_t kGotAlignment = 8; inline constexpr std::size_t kGotReservedSlots = 3; +inline constexpr std::uint64_t kElfHeaderSize = 64; inline constexpr std::uint64_t kDefaultLoadAlignment = 0x1000; inline constexpr std::uint64_t kPhdrHeaderAlignment = 8; inline constexpr std::uint64_t kDynamicHeaderAlignment = 8; inline constexpr std::uint64_t kInterpHeaderAlignment = 1; -inline constexpr std::uint16_t kSyntheticProgramHeaderCount = 4; +inline constexpr std::uint16_t kSyntheticProgramHeaderCount = 5; +inline constexpr std::uint32_t PT_NULL = 0; inline constexpr std::uint32_t PT_LOAD = 1; inline constexpr std::uint32_t PT_DYNAMIC = 2; inline constexpr std::uint32_t PT_INTERP = 3; inline constexpr std::uint32_t PT_NOTE = 4; inline constexpr std::uint32_t PT_PHDR = 6; +inline constexpr std::uint32_t PT_TLS = 7; inline constexpr std::uint32_t PT_GNU_EH_FRAME = 0x6474e550; inline constexpr std::uint32_t PT_GNU_STACK = 0x6474e551; inline constexpr std::uint32_t PT_GNU_RELRO = 0x6474e552; inline constexpr std::uint32_t PF_X = 0x1; inline constexpr std::uint32_t PF_W = 0x2; inline constexpr std::uint32_t PF_R = 0x4; +inline constexpr std::uint32_t ET_EXEC = 2; inline constexpr std::uint32_t ET_DYN = 3; inline constexpr std::uint32_t PT_SCE_DYNLIBDATA = 0x61000000; inline constexpr std::uint32_t PT_OS_PROCPARAM = 0x61000001; @@ -76,10 +81,12 @@ inline constexpr std::uint32_t PT_OS_RELRO = 0x61000010; inline constexpr std::uint32_t PT_LOOS = 0x61000000; inline constexpr std::uint32_t PT_HIOS = 0x6fffffff; +inline constexpr std::int64_t DT_DEBUG = 21; inline constexpr std::int64_t DT_NULL = 0; inline constexpr std::int64_t DT_NEEDED = 1; inline constexpr std::int64_t DT_PLTRELSZ = 2; inline constexpr std::int64_t DT_PLTGOT = 3; +inline constexpr std::int64_t DT_HASH = 4; inline constexpr std::int64_t DT_STRTAB = 5; inline constexpr std::int64_t DT_SYMTAB = 6; inline constexpr std::int64_t DT_RELA = 7; @@ -87,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; @@ -97,7 +107,13 @@ inline constexpr std::int64_t DT_INIT_ARRAYSZ = 27; inline constexpr std::int64_t DT_FINI_ARRAYSZ = 28; 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; @@ -105,6 +121,8 @@ inline constexpr std::int64_t DT_OS_INIT_ARRAY = 0x60000019; inline constexpr std::int64_t DT_OS_FINI_ARRAY = 0x6000001a; inline constexpr std::int64_t DT_OS_INIT_ARRAYSZ = 0x6000001b; inline constexpr std::int64_t DT_OS_FINI_ARRAYSZ = 0x6000001c; +inline constexpr std::int64_t DT_OS_PREINIT_ARRAY = 0x60000020; +inline constexpr std::int64_t DT_OS_PREINIT_ARRAYSZ = 0x60000021; inline constexpr std::int64_t DT_OS_PLTGOT = 0x61000027; inline constexpr std::int64_t DT_OS_JMPREL = 0x61000029; inline constexpr std::int64_t DT_OS_PLTREL = 0x6100002b; @@ -130,14 +148,22 @@ inline constexpr std::uint32_t R_X86_64_RELATIVE = 8; inline constexpr std::uint32_t R_X86_64_GOTPCREL = 9; inline constexpr std::uint32_t R_X86_64_32 = 10; inline constexpr std::uint32_t R_X86_64_32S = 11; +inline constexpr std::uint32_t R_X86_64_DTPMOD64 = 16; +inline constexpr std::uint32_t R_X86_64_DTPOFF64 = 17; +inline constexpr std::uint32_t R_X86_64_TPOFF64 = 18; inline constexpr std::uint32_t R_X86_64_GOTPCRELX = 41; inline constexpr std::uint32_t R_X86_64_REX_GOTPCRELX = 42; +inline constexpr std::uint32_t R_X86_64_IRELATIVE = 37; inline constexpr std::uint8_t STB_GLOBAL = 1; inline constexpr std::uint8_t STB_WEAK = 2; +inline constexpr std::uint8_t STT_NOTYPE = 0; inline constexpr std::uint8_t STT_FUNC = 2; +inline constexpr std::uint8_t STT_TLS = 6; inline constexpr std::uint8_t STV_DEFAULT = 0; inline constexpr std::uint16_t SHN_UNDEF = 0; +inline constexpr std::uint16_t SHN_LORESERVE = 0xff00; +inline constexpr std::uint16_t SHN_ABS = 0xfff1; inline constexpr std::uint32_t SHT_NULL = 0; inline constexpr std::uint32_t SHT_PROGBITS = 1; @@ -167,6 +193,8 @@ inline constexpr std::uint32_t kDynamicLinkToStrTab = 2; inline constexpr std::uint8_t kStubOpAndRsp0xf0[] = {0x48, 0x83, 0xe4, 0xf0}; inline constexpr std::uint8_t kStubOpMovRdiRsp[] = {0x48, 0x89, 0xe7}; inline constexpr std::uint8_t kStubOpXorRsiRsi[] = {0x48, 0x31, 0xf6}; +inline constexpr std::uint8_t kStubOpPushZero[] = {0x6a, 0x00}; +inline constexpr std::uint8_t kStubOpMovRbpRsp[] = {0x48, 0x89, 0xe5}; inline constexpr std::uint8_t kStubOpCallRel32 = 0xe8; inline constexpr std::size_t kStubCallInstructionSize = 5; inline constexpr std::uint8_t kStubOpUd2[] = {0x0f, 0x0b}; 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/IProgramHeaderLayoutBuilder.hpp b/core/relinker/elfpatcher/include/elfpatcher/general/IProgramHeaderLayoutBuilder.hpp index 75b2ae1e17..fcdb2eca02 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/general/IProgramHeaderLayoutBuilder.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/general/IProgramHeaderLayoutBuilder.hpp @@ -13,6 +13,8 @@ namespace Elfpatcher { virtual std::uint64_t ComputeExtraBlockVaddr(const std::vector& originalHeaders, std::uint64_t extraBlockOffset) const = 0; + virtual std::size_t RequiredProgramHeaderCount(const std::vector& originalHeaders) const = 0; + virtual std::uint16_t WriteLayout(std::vector& buf, const ProgramHeaderLayoutRequest& request) const = 0; }; diff --git a/core/relinker/elfpatcher/include/elfpatcher/general/ProgramHeaderLayoutBuilder.hpp b/core/relinker/elfpatcher/include/elfpatcher/general/ProgramHeaderLayoutBuilder.hpp index 6c86cf4cf5..4d89ad7698 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/general/ProgramHeaderLayoutBuilder.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/general/ProgramHeaderLayoutBuilder.hpp @@ -17,6 +17,10 @@ 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( std::vector& buf, const ProgramHeaderLayoutRequest& request @@ -26,12 +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/LinuxDesktopEntryWriter.hpp b/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxDesktopEntryWriter.hpp new file mode 100644 index 0000000000..b4b95f3eef --- /dev/null +++ b/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxDesktopEntryWriter.hpp @@ -0,0 +1,27 @@ +#ifndef ELFPATCHER_LINUX_LINUXDESKTOPENTRYWRITER_HPP +#define ELFPATCHER_LINUX_LINUXDESKTOPENTRYWRITER_HPP + +#include +#include +#include +#include +#include + +namespace Elfpatcher::Linux { + +struct LinuxDesktopEntry { + std::filesystem::path IconPath; + std::vector Icon; + std::filesystem::path EntryPath; + std::string Text; +}; + +class LinuxDesktopEntryWriter { +public: + std::optional Prepare(const std::filesystem::path& sceSysDirectory, const std::filesystem::path& executablePath) const; + std::filesystem::path Write(const LinuxDesktopEntry& entry) const; +}; + +} + +#endif diff --git a/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp b/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp index ae5782f330..c24b3114f0 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp @@ -6,8 +6,10 @@ #include #include #include +#include #include #include +#include #include namespace Elfpatcher::Linux { @@ -18,11 +20,13 @@ public: std::shared_ptr entryStubBuilder, std::shared_ptr programHeaderLayoutBuilder, std::shared_ptr sectionHeaderTableBuilder, - std::shared_ptr byteWriter + std::shared_ptr byteWriter, + std::optional fixedBase = std::nullopt, + 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, @@ -37,6 +41,8 @@ private: std::shared_ptr _programHeaderLayoutBuilder; 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 800fc04835..07db6f888a 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp @@ -2,19 +2,30 @@ #define ELFPATCHER_WINDOWS_WINDOWSELFPATCHER_HPP #include +#include +#include #include +#include +#include namespace Elfpatcher::Windows { class WindowsPePatcher : public IElfPatcher { public: - explicit WindowsPePatcher(bool windowsGui = false, std::filesystem::path iconPath = {}); + explicit WindowsPePatcher(bool windowsGui = false, std::optional> icon = std::nullopt, std::filesystem::path iconName = "icon0.png", std::function progress = {}, std::optional fixedBase = std::nullopt, bool traceImports = false, 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: - bool _windowsGui; - std::filesystem::path _iconPath; + std::function progress; + bool windowsGui; + std::optional> icon; + std::string diagnostics; + std::filesystem::path iconName; + std::optional fixedBase; + bool traceImports; + std::function message; }; } diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsEntryStubBuilder.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsEntryStubBuilder.hpp index a7a1225c18..7a1968e9e1 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsEntryStubBuilder.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsEntryStubBuilder.hpp @@ -2,6 +2,7 @@ #define ELFPATCHER_WINDOWS_ENTRYSTUBBUILDER_HPP #include +#include namespace Elfpatcher::Windows { @@ -19,7 +20,7 @@ struct WindowsEntryStub { class WindowsEntryStubBuilder { public: - WindowsEntryStub Build(std::uint32_t dataRva, std::uint32_t entryRva, const WindowsImports& nativeImports, const std::vector& libraries, const std::vector& imports, const std::string& runPath, bool lazyBinding, bool dependencyDiagnostics, const std::vector& guestModules = {}) const; + WindowsEntryStub Build(std::uint32_t dataRva, std::uint32_t entryRva, const WindowsImports& nativeImports, const std::vector& libraries, const std::vector& imports, const std::string& runPath, bool lazyBinding, bool dependencyDiagnostics, const std::vector& guestModules = {}, const std::set& unmatchedImportModules = {}, bool traceImports = false) const; }; } diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsIconResourceBuilder.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsIconResourceBuilder.hpp index 978458f4e4..cdc42b7c2d 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsIconResourceBuilder.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsIconResourceBuilder.hpp @@ -8,7 +8,7 @@ namespace Elfpatcher::Windows { class WindowsIconResourceBuilder { public: - PeDirectory Build(const std::filesystem::path& iconPath, std::vector& sections, std::uint32_t nextRva) const; + PeDirectory Build(const std::vector& png, const std::filesystem::path& iconPath, std::vector& sections, std::uint32_t nextRva) const; }; } diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsLoadImage.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsLoadImage.hpp index 8bd9ff6ec7..fa2ef59297 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsLoadImage.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsLoadImage.hpp @@ -8,20 +8,30 @@ namespace Elfpatcher::Windows { class WindowsLoadImage { public: - WindowsLoadImage(const std::vector& source, const std::vector& headers, bool requireEntry = true); + WindowsLoadImage(const std::vector& source, const std::vector& headers, bool requireEntry = true, std::uint64_t imageBase = ImageBase); std::uint32_t GetRva(std::uint64_t address, std::uint64_t size = 1) const; std::uint32_t GetEndRva() const; std::uint32_t GetEntryRva() const; std::uint64_t GetRelocatedAddress(std::uint64_t address) const; + std::uint64_t GetImageBase() const; void WritePointer(std::uint64_t address, std::uint64_t value); void RequireWritable(std::uint64_t address, std::uint64_t size) const; std::vector BuildSections() const; 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; std::uint32_t entryRva = 0; }; diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp index bef348f4a9..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 { @@ -35,6 +51,8 @@ struct PeImport { std::int32_t TargetModule = -1; std::uint32_t RelocationType = 1; std::string Library; + bool Weak = false; + bool Function = false; }; struct PeRelocations { diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeWriter.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeWriter.hpp index 9692b150bc..db44e19058 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeWriter.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeWriter.hpp @@ -2,12 +2,13 @@ #define ELFPATCHER_WINDOWS_PEWRITER_HPP #include +#include namespace Elfpatcher::Windows { class WindowsPeWriter { public: - std::vector Write(const std::vector& sections, std::uint32_t entryRva, const std::array& directories, bool windowsGui = false) const; + std::vector Write(const std::vector& sections, std::uint32_t entryRva, const std::array& directories, bool windowsGui = false, std::optional fixedImageBase = std::nullopt, bool dynamicBase = true) const; }; } diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsRelocationBuilder.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsRelocationBuilder.hpp index 0d641e8fed..c9d0c656e3 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsRelocationBuilder.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsRelocationBuilder.hpp @@ -7,7 +7,8 @@ namespace Elfpatcher::Windows { class WindowsRelocationBuilder { public: - PeRelocations Apply(WindowsLoadImage& image, const Domain::SysVDynamicSection& dynamicSection) const; + PeRelocations Apply(WindowsLoadImage& image, const Domain::SysVDynamicSection& dynamicSection, + const std::vector& headers) const; std::vector BuildBaseRelocations(const std::vector& targets) const; }; diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsTlsBuilder.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsTlsBuilder.hpp index e7dc1ed7dd..39b4a54f87 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsTlsBuilder.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsTlsBuilder.hpp @@ -2,12 +2,14 @@ #define ELFPATCHER_WINDOWS_WINDOWSTLSBUILDER_HPP #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; + 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/EntryStubBuilder.cpp b/core/relinker/elfpatcher/src/general/EntryStubBuilder.cpp index 21bc763d80..72168e18b3 100644 --- a/core/relinker/elfpatcher/src/general/EntryStubBuilder.cpp +++ b/core/relinker/elfpatcher/src/general/EntryStubBuilder.cpp @@ -1,5 +1,7 @@ #include #include +#include +#include namespace Elfpatcher { @@ -17,10 +19,16 @@ std::vector EntryStubBuilder::BuildEntryStub( std::vector s; _appendBytes(s, kStubOpMovRdiRsp, sizeof(kStubOpMovRdiRsp)); _appendBytes(s, kStubOpAndRsp0xf0, sizeof(kStubOpAndRsp0xf0)); + _appendBytes(s, kStubOpPushZero, sizeof(kStubOpPushZero)); + _appendBytes(s, kStubOpPushZero, sizeof(kStubOpPushZero)); + _appendBytes(s, kStubOpMovRbpRsp, sizeof(kStubOpMovRbpRsp)); _appendBytes(s, kStubOpXorRsiRsi, sizeof(kStubOpXorRsiRsi)); const std::uint64_t callInsnVaddr = stubVaddr + s.size(); const std::uint64_t callNextVaddr = callInsnVaddr + kStubCallInstructionSize; - const auto rel32 = static_cast(realEntryVaddr - callNextVaddr); + const auto displacement = static_cast(realEntryVaddr - callNextVaddr); + if (displacement < std::numeric_limits::min() || displacement > std::numeric_limits::max()) + throw Domain::RelinkerException("Entry stub call exceeds rel32 range"); + const auto rel32 = static_cast(displacement); s.push_back(kStubOpCallRel32); s.push_back(static_cast(rel32 & 0xff)); s.push_back(static_cast((rel32 >> 8) & 0xff)); diff --git a/core/relinker/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp b/core/relinker/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp index ec3acaaadf..fc23e28dea 100644 --- a/core/relinker/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp +++ b/core/relinker/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp @@ -3,12 +3,52 @@ #include #include #include -#include + +#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 @@ -29,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; } @@ -102,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; @@ -111,83 +170,99 @@ 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; if (vaddrEnd > highestVaddrEnd) highestVaddrEnd = vaddrEnd; } 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; + return (highestVaddrEnd + kDefaultLoadAlignment - 1) & ~(kDefaultLoadAlignment - 1); } std::uint16_t ProgramHeaderLayoutBuilder::WriteLayout( std::vector& buf, const ProgramHeaderLayoutRequest& request ) const { - std::vector kept; - std::uint16_t frameCount = 0; - for (const auto& ph : request.OriginalHeaders) { - if (_segmentFilter->ShouldSkip(ph)) - continue; - kept.push_back(ph); - if (ph.Type == PT_GNU_EH_FRAME) - frameCount++; - } + auto kept = _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; + const bool keepFrames = kept.size() + kSyntheticProgramHeaderCount <= request.PhNum; if (!keepFrames && frameCount > 0) { - std::erase_if(kept, [](const Domain::ProgramHeader& ph) { return ph.Type == PT_GNU_EH_FRAME; }); - std::cerr << "WARNING: No free program header slot for PT_GNU_EH_FRAME; C++ exceptions thrown in the executable cannot be caught.\n"; + throw Domain::RelinkerException("No free program header slot for PT_GNU_EH_FRAME"); } _sortLoadHeaders(kept); - const auto keptCount = static_cast(kept.size()); - const std::uint16_t neededPh = keptCount + kSyntheticProgramHeaderCount; + const std::size_t neededPh = kept.size() + kSyntheticProgramHeaderCount; if (neededPh > request.PhNum) throw Domain::RelinkerException( "Not enough program header slots: need " + std::to_string(neededPh) + ", available " + std::to_string(request.PhNum)); - const std::uint64_t headerBlockSize = request.PhOff + static_cast(neededPh) * request.PhEntSize; + const auto checkedEnd = [](std::uint64_t start, std::uint64_t size) { + if (size > std::numeric_limits::max() - start) + throw Domain::RelinkerException("Program header layout range overflowed"); + return start + size; + }; + if (request.PhEntSize < 56) + throw Domain::RelinkerException("Program header entry is too small"); + const std::uint64_t tableEnd = checkedEnd(request.PhOff, static_cast(neededPh) * request.PhEntSize); + if (tableEnd > buf.size()) + throw Domain::RelinkerException("Program header table exceeds the output file"); + const bool tableInExtraBlock = request.PhOff >= request.ExtraBlockOffset; + if (tableInExtraBlock) { + 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"); std::uint64_t headerBlockAlign = kDefaultLoadAlignment; for (const auto& ph : request.OriginalHeaders) { 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 = - (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) throw Domain::RelinkerException("Dynamic segment offset lies before the extra block"); - if (request.DynamicSegmentOffset + request.DynamicSegmentSize > request.ExtraBlockOffset + request.ExtraBlockSize) + if (request.DynamicSegmentOffset - request.ExtraBlockOffset > request.ExtraBlockSize || + request.DynamicSegmentSize > request.ExtraBlockSize - (request.DynamicSegmentOffset - request.ExtraBlockOffset)) throw Domain::RelinkerException("Dynamic segment does not fit within the extra block"); if (request.InterpOffset < request.ExtraBlockOffset) throw Domain::RelinkerException("Interp offset lies before the extra block"); - if (request.InterpOffset + request.InterpSize > request.ExtraBlockOffset + request.ExtraBlockSize) + if (request.InterpOffset - request.ExtraBlockOffset > request.ExtraBlockSize || + 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; - layout.push_back(_makePhdrHeader(request.PhOff, headerBlockVaddr + request.PhOff, static_cast(neededPh) * request.PhEntSize)); + 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) { if (ph.Type == PT_LOAD) 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 c403a058e1..e07068514f 100644 --- a/core/relinker/elfpatcher/src/general/SectionHeaderTableBuilder.cpp +++ b/core/relinker/elfpatcher/src/general/SectionHeaderTableBuilder.cpp @@ -1,5 +1,10 @@ #include #include +#include +#include +#include +#include +#include namespace Elfpatcher { @@ -38,6 +43,61 @@ 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 || (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; + const auto start = Io::ReadU64(buf, header + 8); + const auto size = Io::ReadU64(buf, header + 32); + if (offset >= start && offset - start <= size && sectionSize <= size - (offset - start)) { + const auto relative = offset - start; + const auto address = Io::ReadU64(buf, header + 16); + if (relative > std::numeric_limits::max() - address || + sectionSize > std::numeric_limits::max() - (address + relative)) + throw std::runtime_error("Allocated output section address overflowed"); + return address + relative; + } + } + throw std::runtime_error("Allocated output section is outside load segments"); + }; + + const auto dynStrAddress = mappedAddress(request.DynStrOffset, request.DynStrSize); + const auto dynSymAddress = mappedAddress(request.DynSymOffset, request.DynSymSize); + const auto dynamicAddress = mappedAddress(request.DynamicSegmentOffset, request.DynamicSegmentSize); + const auto stubAddress = mappedAddress(request.StubOffset, request.StubSize); + const auto shStrTabOff = static_cast(buf.size()); constexpr std::uint64_t shStrTabSize = sizeof(kShStrTabBlob); for (const char i : kShStrTabBlob) @@ -47,11 +107,11 @@ void SectionHeaderTableBuilder::WriteTable( buf.resize(buf.size() + kShdrEntrySize * kSectionHeaderCount, 0); _writeSectionHeader(buf, shOff + kShdrEntrySize * 0, 0, SHT_NULL, 0, 0, 0, 0, 0, 0, 0, 0); - _writeSectionHeader(buf, shOff + kShdrEntrySize * 1, kShStrTabNameOffset, SHT_STRTAB, 0, shStrTabOff, shStrTabOff, shStrTabSize, 0, 0, kNoSpecialAlignment, 0); - _writeSectionHeader(buf, shOff + kShdrEntrySize * 2, kDynStrNameOffset, SHT_STRTAB, SHF_ALLOC, request.DynStrOffset, request.DynStrOffset, request.DynStrSize, 0, 0, kNoSpecialAlignment, 0); - _writeSectionHeader(buf, shOff + kShdrEntrySize * 3, kDynSymNameOffset, SHT_DYNSYM, SHF_ALLOC, request.DynSymOffset, request.DynSymOffset, request.DynSymSize, kDynSymLinkToStrTab, kDynSymInfoFirstGlobal, kDynSymAlign, kSymEntrySize); - _writeSectionHeader(buf, shOff + kShdrEntrySize * 4, kDynamicNameOffset, SHT_DYNAMIC, SHF_ALLOC | SHF_WRITE, request.DynamicSegmentOffset, request.DynamicSegmentOffset, request.DynamicSegmentSize, kDynamicLinkToStrTab, 0, kDynSymAlign, kDynEntrySize); - _writeSectionHeader(buf, shOff + kShdrEntrySize * 5, kTextNameOffset, SHT_PROGBITS, SHF_ALLOC | SHF_EXECINSTR, request.StubOffset, request.StubOffset, request.StubSize, 0, 0, kNoSpecialAlignment, 0); + _writeSectionHeader(buf, shOff + kShdrEntrySize * 1, kShStrTabNameOffset, SHT_STRTAB, 0, 0, shStrTabOff, shStrTabSize, 0, 0, kNoSpecialAlignment, 0); + _writeSectionHeader(buf, shOff + kShdrEntrySize * 2, kDynStrNameOffset, SHT_STRTAB, SHF_ALLOC, dynStrAddress, request.DynStrOffset, request.DynStrSize, 0, 0, kNoSpecialAlignment, 0); + _writeSectionHeader(buf, shOff + kShdrEntrySize * 3, kDynSymNameOffset, SHT_DYNSYM, SHF_ALLOC, dynSymAddress, request.DynSymOffset, request.DynSymSize, kDynSymLinkToStrTab, kDynSymInfoFirstGlobal, kDynSymAlign, kSymEntrySize); + _writeSectionHeader(buf, shOff + kShdrEntrySize * 4, kDynamicNameOffset, SHT_DYNAMIC, SHF_ALLOC | SHF_WRITE, dynamicAddress, request.DynamicSegmentOffset, request.DynamicSegmentSize, kDynamicLinkToStrTab, 0, kDynSymAlign, kDynEntrySize); + _writeSectionHeader(buf, shOff + kShdrEntrySize * 5, kTextNameOffset, SHT_PROGBITS, SHF_ALLOC | SHF_EXECINSTR, stubAddress, request.StubOffset, request.StubSize, 0, 0, kNoSpecialAlignment, 0); _byteWriter->WriteU64(buf, kEhdrShOffOffset, shOff); _byteWriter->WriteU16(buf, kEhdrShEntSizeOffset, static_cast(kShdrEntrySize)); diff --git a/core/relinker/elfpatcher/src/general/SegmentFilter.cpp b/core/relinker/elfpatcher/src/general/SegmentFilter.cpp index ff8f24e7f8..114c7e1a3c 100644 --- a/core/relinker/elfpatcher/src/general/SegmentFilter.cpp +++ b/core/relinker/elfpatcher/src/general/SegmentFilter.cpp @@ -13,8 +13,10 @@ 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) 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; return false; } diff --git a/core/relinker/elfpatcher/src/linux/LinuxDesktopEntryWriter.cpp b/core/relinker/elfpatcher/src/linux/LinuxDesktopEntryWriter.cpp new file mode 100644 index 0000000000..04f3d5f91f --- /dev/null +++ b/core/relinker/elfpatcher/src/linux/LinuxDesktopEntryWriter.cpp @@ -0,0 +1,132 @@ +#include +#include +#include +#include "prx/libkernel/AppMetadata/include/ParamJsonParser.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Elfpatcher::Linux { + +namespace { + +bool fileExists(const std::filesystem::path& path) { + std::error_code error; + const bool result = std::filesystem::exists(path, error); + if (error) throw Domain::RelinkerException("Cannot inspect '" + Io::Utf8Path(path) + "': " + error.message()); + return result; +} + +std::vector readPng(const std::filesystem::path& path) { + std::ifstream stream(path, std::ios::binary); + if (!stream) throw Domain::RelinkerException("Cannot open icon '" + Io::Utf8Path(path) + "'"); + std::vector bytes{std::istreambuf_iterator(stream), std::istreambuf_iterator()}; + constexpr std::array header{0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52}; + if (bytes.size() < 33 || !std::equal(header.begin(), header.end(), bytes.begin())) throw Domain::RelinkerException("Invalid icon PNG '" + Io::Utf8Path(path) + "': missing PNG signature or IHDR"); + return bytes; +} + +std::string cleanTitle(const std::string& title) { + std::string withoutMarks; + for (std::size_t i = 0; i < title.size(); ++i) { + const std::string_view rest(title.data() + i, title.size() - i); + if (rest.starts_with("\xe2\x84\xa2")) { i += 2; continue; } + if (rest.starts_with("\xc2\xae") || rest.starts_with("\xc2\xa9")) { i += 1; continue; } + const auto c = static_cast(title[i]); + withoutMarks += c < 0x20 || c == 0x7f ? ' ' : title[i]; + } + std::string result; + for (const char c : withoutMarks) { + if (c == ' ' && (result.empty() || result.back() == ' ')) continue; + result += c; + } + if (!result.empty() && result.back() == ' ') result.pop_back(); + return result; +} + +std::string title(const std::filesystem::path& paramJsonPath, const std::filesystem::path& executablePath) { + if (!fileExists(paramJsonPath)) return Io::Utf8Path(executablePath.stem()); + std::string parsed; + try { + parsed = cleanTitle(parseParamJson(paramJsonPath).title); + } catch (const std::exception& e) { + throw Domain::RelinkerException("Invalid '" + Io::Utf8Path(paramJsonPath) + "': " + e.what()); + } + return parsed.empty() ? Io::Utf8Path(executablePath.stem()) : parsed; +} + +std::string escapeValue(const std::string& value) { + std::string result; + for (const char c : value) { + if (c == '\\') result += "\\\\"; + else result += c; + } + return result; +} + +void requirePrintable(const std::filesystem::path& path) { + const auto text = Io::Utf8Path(path); + const bool control = std::any_of(text.begin(), text.end(), [](const char c) { + const auto byte = static_cast(c); + return byte < 0x20 || byte == 0x7f; + }); + if (control) throw Domain::RelinkerException("Cannot write a desktop entry for '" + text + "': the path contains a control character"); +} + +std::string quoteExec(const std::string& path) { + std::string result = "\""; + for (const char c : path) { + if (c == '"' || c == '`' || c == '$' || c == '\\') result += '\\'; + if (c == '%') result += '%'; + result += c; + } + return escapeValue(result + "\""); +} + +} + +std::optional LinuxDesktopEntryWriter::Prepare(const std::filesystem::path& sceSysDirectory, const std::filesystem::path& executablePath) const { + const auto iconSource = sceSysDirectory / "icon0.png"; + if (!fileExists(iconSource)) return std::nullopt; + requirePrintable(executablePath); + LinuxDesktopEntry entry; + entry.Icon = readPng(iconSource); + const auto name = title(sceSysDirectory / "param.json", executablePath); + entry.IconPath = executablePath; + entry.IconPath += ".png"; + entry.EntryPath = executablePath; + entry.EntryPath += ".desktop"; + entry.Text = "[Desktop Entry]\n" + "Type=Application\n" + "Name=" + escapeValue(name) + "\n" + "Exec=" + quoteExec(Io::Utf8Path(executablePath)) + "\n" + "Path=" + escapeValue(Io::Utf8Path(executablePath.parent_path())) + "\n" + "Icon=" + escapeValue(Io::Utf8Path(entry.IconPath)) + "\n" + "Terminal=false\n" + "Categories=Game;\n"; + return entry; +} + +std::filesystem::path LinuxDesktopEntryWriter::Write(const LinuxDesktopEntry& entry) const { + std::ofstream icon(entry.IconPath, std::ios::binary); + icon.write(reinterpret_cast(entry.Icon.data()), static_cast(entry.Icon.size())); + icon.close(); + if (!icon) throw Domain::RelinkerException("Failed to write icon '" + Io::Utf8Path(entry.IconPath) + "'"); + + std::ofstream text(entry.EntryPath, std::ios::binary); + text << entry.Text; + text.close(); + if (!text) throw Domain::RelinkerException("Failed to write desktop entry '" + Io::Utf8Path(entry.EntryPath) + "'"); + + std::error_code error; + std::filesystem::permissions(entry.EntryPath, 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 mark desktop entry '" + Io::Utf8Path(entry.EntryPath) + "' executable: " + error.message()); + return entry.EntryPath; +} + +} diff --git a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp index 24746a4a18..df116a307c 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp @@ -4,23 +4,252 @@ #include #include #include +#include #include #include +#include #include +#include #include +#include +#include namespace Elfpatcher::Linux { +namespace { + +constexpr std::uint64_t kLinuxUserAddressEnd = 0x800000000000; + +std::string Hex(const std::uint64_t value) { + std::ostringstream text; + text << "0x" << std::hex << value; + return text.str(); +} + +std::uint64_t Rebased(const std::uint64_t base, const std::uint64_t address, const std::string& what) { + if (base > kLinuxUserAddressEnd || address > kLinuxUserAddressEnd - base) + throw Domain::RelinkerException(what + " " + Hex(address) + " lies outside the Linux user address space at fixed base " + Hex(base)); + return base + address; +} + +void RequireWholeEntries(const std::vector& table, const std::uint64_t entrySize, const std::string& what) { + if (table.size() % entrySize != 0) + throw Domain::RelinkerException(what + " is not a whole number of entries"); +} + +void RequireOnlyDependencies(const std::vector& entries) { + RequireWholeEntries(entries, kDynEntrySize, "Executable dependency table"); + for (std::size_t entry = 0; entry < entries.size(); entry += kDynEntrySize) { + const auto tag = static_cast(Io::ReadU64(entries, entry)); + if (tag != DT_NEEDED) + throw Domain::RelinkerException("Dynamic tag " + Hex(static_cast(tag)) + " cannot be placed at a fixed Linux base"); + } +} + +std::vector RebasedSymbols(const std::vector& table, const std::uint64_t base) { + RequireWholeEntries(table, kSymEntrySize, "Dynamic symbol table"); + auto rebased = table; + for (std::size_t entry = 0; entry < rebased.size(); entry += kSymEntrySize) { + const auto section = Io::ReadU16(rebased, entry + 6); + if (section == SHN_UNDEF || section == SHN_ABS || (rebased[entry + 4] & 0xf) == STT_TLS) + continue; + if (section >= SHN_LORESERVE) + throw Domain::RelinkerException("Symbol section index " + Hex(section) + " cannot be placed at a fixed Linux base"); + Io::WriteU64(rebased, entry + 8, Rebased(base, Io::ReadU64(rebased, entry + 8), "Symbol")); + } + return rebased; +} + +std::vector RebasedRelocations(const std::vector& table, const std::uint64_t base) { + RequireWholeEntries(table, kRelaEntrySize, "Relocation table"); + auto rebased = table; + for (std::size_t entry = 0; entry < rebased.size(); entry += kRelaEntrySize) { + const auto type = static_cast(Io::ReadU64(rebased, entry + 8)); + switch (type) { + case R_X86_64_NONE: + continue; + case R_X86_64_RELATIVE: + case R_X86_64_IRELATIVE: { + const auto addend = static_cast(Io::ReadU64(rebased, entry + 16)); + if (addend < 0) + throw Domain::RelinkerException("Relocation target " + std::to_string(addend) + " lies below the image at a fixed Linux base"); + Io::WriteU64(rebased, entry + 16, Rebased(base, static_cast(addend), "Relocation target")); + break; + } + case R_X86_64_64: + case R_X86_64_PC32: + case R_X86_64_COPY: + case R_X86_64_GLOB_DAT: + case R_X86_64_JUMP_SLOT: + case R_X86_64_32: + case R_X86_64_32S: + case R_X86_64_DTPMOD64: + case R_X86_64_DTPOFF64: + case R_X86_64_TPOFF64: + break; + default: + throw Domain::RelinkerException("Relocation type " + std::to_string(type) + " cannot be placed at a fixed Linux base"); + } + Io::WriteU64(rebased, entry, Rebased(base, Io::ReadU64(rebased, entry), "Relocation offset")); + } + return rebased; +} + +void 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; +} + +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); + const auto type = Io::ReadU32(buf, header + kPhdrTypeOffset); + if (type == PT_NULL || type == PT_GNU_STACK) + continue; + const auto address = Rebased(base, Io::ReadU64(buf, header + kPhdrVaddrOffset), "Program header address"); + const auto physical = Rebased(base, Io::ReadU64(buf, header + kPhdrPaddrOffset), "Program header physical address"); + if (type == PT_LOAD) { + const auto alignment = Io::ReadU64(buf, header + kPhdrAlignOffset); + if (alignment > 1 && base % alignment != 0) + throw Domain::RelinkerException("Fixed Linux base " + Hex(base) + " is not a multiple of the PT_LOAD alignment " + Hex(alignment)); + if (Io::ReadU64(buf, header + kPhdrMemSizeOffset) > kLinuxUserAddressEnd - address) + throw Domain::RelinkerException("PT_LOAD at " + Hex(address) + " ends outside the Linux user address space"); + } + Io::WriteU64(buf, header + kPhdrVaddrOffset, address); + Io::WriteU64(buf, header + kPhdrPaddrOffset, physical); + } +} + +} + LinuxElfPatcher::LinuxElfPatcher( std::shared_ptr entryStubBuilder, std::shared_ptr programHeaderLayoutBuilder, std::shared_ptr sectionHeaderTableBuilder, - std::shared_ptr byteWriter + std::shared_ptr byteWriter, + std::optional fixedBase, + bool traceImports ) : _entryStubBuilder(std::move(entryStubBuilder)) , _programHeaderLayoutBuilder(std::move(programHeaderLayoutBuilder)) , _sectionHeaderTableBuilder(std::move(sectionHeaderTableBuilder)) , _byteWriter(std::move(byteWriter)) + , _fixedBase(fixedBase) + , _traceImports(traceImports) { } @@ -66,7 +295,7 @@ void LinuxElfPatcher::_appendTrampoline( } std::vector LinuxElfPatcher::Patch( - const std::vector& sourceElf, + std::vector sourceElf, const std::vector& originalHeaders, const Domain::SysVDynamicSection& dynSection, const std::uint64_t originalPltGotVaddr, @@ -77,13 +306,31 @@ std::vector LinuxElfPatcher::Patch( { if (dependencyDiagnostics) throw Domain::RelinkerException("Linux target does not support --windows-diagnostics"); + if (_fixedBase && lazyBinding) + throw Domain::RelinkerException("A fixed Linux base requires eager binding; --lazy-binding is incompatible"); + if (_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; + }; + const auto table = [this, base](const std::vector& data, const bool relocations) { + if (!_fixedBase) return data; + return relocations ? RebasedRelocations(data, base) : RebasedSymbols(data, base); + }; - std::vector buf = sourceElf; + std::vector buf = std::move(sourceElf); + const std::uint32_t type = _fixedBase ? ET_EXEC : ET_DYN; buf[kEhdrOsAbiOffset] = 0; buf[kEhdrAbiVersionOffset] = 0; - buf[kEhdrTypeOffset] = static_cast(ET_DYN & 0xFF); - buf[kEhdrTypeOffset + 1] = static_cast((ET_DYN >> 8) & 0xFF); + buf[kEhdrTypeOffset] = static_cast(type & 0xFF); + buf[kEhdrTypeOffset + 1] = static_cast((type >> 8) & 0xFF); _byteWriter->WriteU64(buf, kEhdrShOffOffset, 0); _byteWriter->WriteU16(buf, kEhdrShNumOffset, 0); _byteWriter->WriteU16(buf, kEhdrShStrNdxOffset, 0); @@ -104,21 +351,54 @@ 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()); - for (std::uint8_t b : dynSection.DynSymData) + 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(symbols.size() / kSymEntrySize); + Io::AppendU32(buf, 1); + Io::AppendU32(buf, symbolCount); + Io::AppendU32(buf, symbolCount > 1 ? 1 : 0); + for (std::uint32_t index = 0; index < symbolCount; ++index) + Io::AppendU32(buf, index != 0 && index + 1 < symbolCount ? index + 1 : 0); + alignBuf(buf, kDynSymAlignment); + const auto relaOff = static_cast(buf.size()); - for (std::uint8_t b : dynSection.RelaData) + 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); const auto relaPltOff = static_cast(buf.size()); - for (std::uint8_t b : dynSection.RelaPltData) + const auto pltRelocations = table(dynSection.RelaPltData, true); + for (std::uint8_t b : pltRelocations) buf.push_back(b); alignBuf(buf, kRelaPltAlignment); @@ -134,20 +414,22 @@ std::vector LinuxElfPatcher::Patch( std::vector dynSegBuf; for (std::uint8_t b : dynSection.DynamicSegmentData) dynSegBuf.push_back(b); - _appendDynEntry(dynSegBuf, DT_STRTAB, vaddrOfExtraBlockOffset(dynStrOff)); + _appendDynEntry(dynSegBuf, DT_STRTAB, address(vaddrOfExtraBlockOffset(dynStrOff))); _appendDynEntry(dynSegBuf, DT_STRSZ, dynStrSize); - _appendDynEntry(dynSegBuf, DT_SYMTAB, vaddrOfExtraBlockOffset(dynSymOff)); + _appendDynEntry(dynSegBuf, DT_SYMTAB, address(vaddrOfExtraBlockOffset(dynSymOff))); _appendDynEntry(dynSegBuf, DT_SYMENT, kSymEntrySize); - if (!dynSection.RelaData.empty()) { - _appendDynEntry(dynSegBuf, DT_RELA, vaddrOfExtraBlockOffset(relaOff)); - _appendDynEntry(dynSegBuf, DT_RELASZ, dynSection.RelaData.size()); + _appendDynEntry(dynSegBuf, DT_HASH, address(vaddrOfExtraBlockOffset(hashOff))); + _appendDynEntry(dynSegBuf, DT_DEBUG, 0); + if (!relocations.empty()) { + _appendDynEntry(dynSegBuf, DT_RELA, address(vaddrOfExtraBlockOffset(relaOff))); + _appendDynEntry(dynSegBuf, DT_RELASZ, relocations.size()); _appendDynEntry(dynSegBuf, DT_RELAENT, kRelaEntrySize); } if (!dynSection.RelaPltData.empty()) { - _appendDynEntry(dynSegBuf, DT_JMPREL, vaddrOfExtraBlockOffset(relaPltOff)); + _appendDynEntry(dynSegBuf, DT_JMPREL, address(vaddrOfExtraBlockOffset(relaPltOff))); _appendDynEntry(dynSegBuf, DT_PLTRELSZ, dynSection.RelaPltData.size()); _appendDynEntry(dynSegBuf, DT_PLTREL, static_cast(DT_RELA)); - _appendDynEntry(dynSegBuf, DT_PLTGOT, originalPltGotVaddr); + _appendDynEntry(dynSegBuf, DT_PLTGOT, address(originalPltGotVaddr)); } if (!lazyBinding) _appendDynEntry(dynSegBuf, DT_FLAGS, DF_BIND_NOW); @@ -158,13 +440,53 @@ 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(stubVaddr, 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); - _byteWriter->WriteU64(buf, kEhdrEntryOffset, stubVaddr); + const auto stubSize = static_cast(buf.size()) - stubOff; + _byteWriter->WriteU64(buf, kEhdrEntryOffset, address(stubVaddr)); for (const auto& site : trampolines) _appendTrampoline(buf, site, extraBlockOff, vaddrOfExtraBlockOffset); @@ -175,13 +497,26 @@ std::vector LinuxElfPatcher::Patch( for (char i : kInterp) buf.push_back(static_cast(i)); + std::uint64_t tableOff = phOff; + std::uint16_t tableSlots = phNum; + if (const auto required = _programHeaderLayoutBuilder->RequiredProgramHeaderCount(originalHeaders); required > phNum && required <= std::numeric_limits::max()) { + alignBuf(buf, kPhdrHeaderAlignment); + tableOff = static_cast(buf.size()); + tableSlots = static_cast(required); + buf.resize(buf.size() + required * phEntSize); + _byteWriter->WriteU64(buf, kEhdrPhOffOffset, tableOff); + } + const std::uint64_t extraBlockSize = static_cast(buf.size()) - extraBlockOff; + auto headers = originalHeaders; + SeparateSharedLoadPages(buf, headers); + ProgramHeaderLayoutRequest layoutRequest{}; - layoutRequest.PhOff = phOff; + layoutRequest.PhOff = tableOff; layoutRequest.PhEntSize = phEntSize; - layoutRequest.PhNum = phNum; - layoutRequest.OriginalHeaders = originalHeaders; + layoutRequest.PhNum = tableSlots; + layoutRequest.OriginalHeaders = headers; layoutRequest.ExtraBlockOffset = extraBlockOff; layoutRequest.ExtraBlockVaddr = extraBlockVaddr; layoutRequest.ExtraBlockSize = extraBlockSize; @@ -192,16 +527,18 @@ std::vector LinuxElfPatcher::Patch( const std::uint16_t writtenPh = _programHeaderLayoutBuilder->WriteLayout(buf, layoutRequest); _byteWriter->WriteU16(buf, kEhdrPhNumOffset, writtenPh); + if (_fixedBase) + 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 6e1912dd9b..7f8032b78a 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -20,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; @@ -29,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; @@ -73,7 +75,8 @@ std::vector GuestModuleWriter::WriteMacOs(const Relinker::GuestIma image.WritePointer(target, symbol.Value + addend - GuestTlsBlockSize(*tls)); } else if (type == 1 || type == 6 || type == 7) { if (symbolIndex == 0 || (type != 1 && addend != 0) || tlsSymbol) throw Domain::RelinkerException("Invalid guest symbol relocation", target); - if (!imported) { + if (symbol.Section == Relinker::AbsoluteSection) image.WritePointer(target, symbol.Value + addend); + else if (!imported) { if (symbol.Section >= 0xff00) throw Domain::RelinkerException("Unsupported special guest symbol section", target); image.WritePointer(target, image.GetRelocatedAddress(symbol.Value) + addend); input.Rebases.push_back(rva); @@ -81,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); } @@ -91,7 +94,7 @@ std::vector GuestModuleWriter::WriteMacOs(const Relinker::GuestIma for (std::size_t index = 1; index < guest.Symbols.size(); ++index) { const auto& symbol = guest.Symbols[index]; - if (symbol.Section == 0 || (symbol.Info >> 4) == 0 || symbol.Visibility == 1 || symbol.Visibility == 2) continue; + if (symbol.Section == 0 || symbol.Section == Relinker::AbsoluteSection || (symbol.Info >> 4) == 0 || symbol.Visibility == 1 || symbol.Visibility == 2) continue; if (symbol.Section >= 0xff00) throw Domain::RelinkerException("Unsupported guest export section: " + symbol.Name); const auto name = "_" + DynamicName(guest, index); if ((symbol.Info & 15) == 6) { @@ -104,16 +107,16 @@ 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 { return WriteMacOsImage(input); } catch (Domain::RelinkerException& error) { - error.InputPath = guest.SourcePath.string(); + if (error.InputPath.empty()) error.InputPath = Io::Utf8Path(guest.SourcePath); throw; } } diff --git a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp index 4f50948bcf..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,7 +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 MhPie = 0x200000; +constexpr std::uint32_t MhWeakDefines = 0x8000; constexpr std::uint32_t LcSegment64 = 0x19; constexpr std::uint32_t LcSymtab = 0x2; @@ -60,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; @@ -153,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]; @@ -165,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); } @@ -193,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; @@ -200,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; @@ -239,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; @@ -291,7 +342,7 @@ public: private: struct Node { std::vector>> Children; - std::optional Rva; + std::optional Export; std::uint64_t Offset = 0; }; @@ -323,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) { @@ -334,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; } @@ -360,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"); @@ -370,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) { @@ -381,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; @@ -388,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)); @@ -398,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) { @@ -425,16 +495,25 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { metaEnd += 8; input.Binds.push_back({symbol, metaRva + static_cast(slot)}); } + std::uint64_t moduleArgsSlot = 0; + std::uint64_t moduleResultSlot = 0; + if (input.InitRva != 0) { + moduleArgsSlot = metaEnd; + moduleResultSlot = metaEnd + 8; + metaEnd += 16; + input.Binds.push_back({"___aps5_get_pending_module_args_nid_no_patch", metaRva + static_cast(moduleArgsSlot)}); + input.Binds.push_back({"___aps5_set_module_init_result_nid_no_patch", metaRva + static_cast(moduleResultSlot)}); + } meta.resize(metaEnd, 0); for (std::size_t index = 0; index < input.TlsExports.size(); ++index) { const auto& variable = input.TlsExports[index]; 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); @@ -451,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); @@ -461,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); } @@ -472,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}); @@ -488,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)); @@ -546,8 +641,11 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { code.Rip({0x48, 0x89, 0x05}, import.Rva); } if (input.InitRva != 0) { - callZeroArgs(); + code.Rip({0xff, 0x15}, metaRva + static_cast(moduleArgsSlot)); + code.Emit({0x48, 0x8b, 0x38, 0x48, 0x8b, 0x70, 0x08, 0x31, 0xd2}); code.Rip({0xe8}, input.InitRva); + code.Emit({0x89, 0xc7}); + code.Rip({0xff, 0x15}, metaRva + static_cast(moduleResultSlot)); } for (const auto slot : input.InitArrayRvas) callSlot(slot); if (hasFini) { @@ -557,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}); } @@ -598,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); @@ -748,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); @@ -756,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)); @@ -792,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 | (input.Executable ? MhPie : 0)); + 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 f64e0bdb59..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"); @@ -25,10 +39,9 @@ std::vector ReadNeededLibraries(const Domain::SysVDynamicSection& d 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 = Windows::ReadString(dynamicSection.DynStrData, static_cast(nameOffset)); - if (name.empty() || !unique.insert(name).second) throw Domain::RelinkerException("Invalid or duplicate DT_NEEDED library: " + name); - result.push_back(std::move(name)); + 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,29 +51,61 @@ 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); + const auto relocations = Windows::WindowsRelocationBuilder().Apply(image, dynamicSection, originalHeaders); MacOsImageInput input; input.Executable = true; input.Source = &sourceElf; input.Headers = &originalHeaders; input.Image = ℑ + input.Message = message; + for (std::size_t offset = 24; offset < dynamicSection.DynSymData.size(); offset += 24) { + const auto& symbols = dynamicSection.DynSymData; + const auto section = Io::ReadU16(symbols, offset + 6); + const auto info = symbols[offset + 4]; + const auto visibility = symbols[offset + 5] & 3; + if (section == SHN_UNDEF || (info >> 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 a6e4c952d8..3affd7853e 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsDependencyStubBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsDependencyStubBuilder.cpp @@ -23,11 +23,15 @@ WindowsDependencyStubBuilder::WindowsDependencyStubBuilder(PeSection& data) { storage.emplace(name, CheckedRva(data.Rva + data.Data.size())); data.Data.resize(data.Data.size() + 32); } + Io::AlignBuffer(data.Data, 2); + storage.emplace("wide", CheckedRva(data.Rva + data.Data.size())); + data.Data.resize(data.Data.size() + PathCapacity * 2); const std::map messages = { {"invalid", "FAIL: invalid or unsupported PE dependency metadata\n"}, {"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: "}, @@ -105,6 +109,37 @@ WindowsDependencyStub WindowsDependencyStubBuilder::Build(WindowsStubEmitter& co a.Jump("io", 0x84); a.End(); + a.Begin("wide"); + a.Mov(R8, Cx); + a.Value(Cx, 65001); + a.Value(Dx, 0); + a.Value(R9, 0xffffffffffffffffu); + a.Data(Ax, "wide"); + a.Store(Sp, 32, Ax); + a.Value(Ax, PathCapacity); + a.Store(Sp, 40, Ax); + a.Api("MultiByteToWideChar"); + a.Test(Ax); + a.Jump("limit", 0x84); + a.Data(Ax, "wide"); + a.End(); + + a.Begin("narrow"); + a.Store(Sp, 32, Cx); + a.Value(Cx, 65001); + a.Value(Dx, 0); + a.Data(R8, "wide"); + a.Value(R9, 0xffffffffffffffffu); + a.Value(Ax, PathCapacity); + a.Store(Sp, 40, Ax); + a.Value(Ax, 0); + a.Store(Sp, 48, Ax); + a.Store(Sp, 56, Ax); + a.Api("WideCharToMultiByte"); + a.Test(Ax); + a.Jump("limit", 0x84); + a.End(); + a.Begin("directory"); a.Mov(Bx, Cx); a.Call("copy"); @@ -178,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"); @@ -201,16 +237,47 @@ WindowsDependencyStub WindowsDependencyStubBuilder::Build(WindowsStubEmitter& co a.Mov(Dx, Si); a.Call("copy"); a.Address(Cx, Bx, 64); - a.Api("GetModuleHandleA"); + a.Call("wide"); + a.Mov(Cx, Ax); + a.Api("GetModuleHandleW"); a.Store(Bx, 8, Ax); a.Test(Ax); a.Jump("registerMapped", 0x85); a.Address(Cx, Bx, 64); + a.Call("wide"); + a.Mov(Cx, Ax); a.Value(Dx, 0); a.Value(R8, 0x20); - a.Api("LoadLibraryExA"); + 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); @@ -263,7 +330,9 @@ WindowsDependencyStub WindowsDependencyStubBuilder::Build(WindowsStubEmitter& co a.Mov(R8, Bx); a.Call("join"); a.Data(Cx, "scratch"); - a.Api("GetFileAttributesA"); + a.Call("wide"); + a.Mov(Cx, Ax); + a.Api("GetFileAttributesW"); a.Value(Dx, 0xffffffffu); a.Compare(Ax, Dx); a.Jump(std::string("resolveNext") + directory, 0x84); @@ -272,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"); @@ -292,18 +362,25 @@ 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, "scratch"); + a.Data(Dx, "wide"); a.Value(R8, PathCapacity); - a.Api("GetModuleFileNameA"); + a.Api("GetModuleFileNameW"); a.Test(Ax); a.Jump("io", 0x84); a.CompareValue(Ax, PathCapacity); a.Jump("limit", 0x83); + a.Data(Cx, "scratch"); + a.Call("narrow"); a.Mark("resolvePath"); a.Data(Cx, "scratch"); + a.Mov(Dx, Si); a.Call("register"); a.End(); @@ -623,13 +700,19 @@ WindowsDependencyStub WindowsDependencyStubBuilder::Build(WindowsStubEmitter& co a.Data(Cx, "executableDirectory"); a.Mov(Dx, Si); a.Call("directory"); - a.Data(Cx, "systemDirectory"); + a.Data(Cx, "wide"); a.Value(Dx, PathCapacity - 1); - a.Api("GetSystemDirectoryA"); + a.Api("GetSystemDirectoryW"); a.Test(Ax); a.Jump("io", 0x84); a.CompareValue(Ax, PathCapacity - 1); a.Jump("limit", 0x83); + a.Data(Cx, "systemDirectory"); + a.Call("narrow"); + a.Data(Cx, "systemDirectory"); + a.Api("lstrlenA"); + a.CompareValue(Ax, PathCapacity - 1); + a.Jump("limit", 0x83); a.Data(Dx, "systemDirectory"); a.Add(Dx, Ax); a.Value(Ax, '\\'); @@ -646,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 068954f288..175a42f755 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp @@ -2,8 +2,13 @@ #include #include #include +#include #include +#include #include +#include +#include +#include #include #include @@ -24,6 +29,8 @@ std::string normalizeRunPath(std::string path) { path = "."; if (path.starts_with("$ORIGIN\\")) path.erase(0, 8); + if (path.empty()) + path = "."; if (path.find('$') != std::string::npos) throw Domain::RelinkerException("Unsupported variable in Windows run path"); if (path.back() != '\\') @@ -31,9 +38,38 @@ std::string normalizeRunPath(std::string path) { return path; } +std::u16string toUtf16(const std::string& text) { + static constexpr std::uint32_t minimum[] = {0, 0, 0x80, 0x800, 0x10000}; + std::u16string result; + for (std::size_t index = 0; index < text.size();) { + const auto lead = static_cast(text[index]); + const std::size_t length = lead < 0x80 ? 1 : (lead & 0xe0) == 0xc0 ? 2 : (lead & 0xf0) == 0xe0 ? 3 : (lead & 0xf8) == 0xf0 ? 4 : 0; + if (length == 0 || text.size() - index < length) + throw Domain::RelinkerException("Windows library path is not valid UTF-8: " + text); + std::uint32_t code = length == 1 ? lead : lead & (0x7fu >> length); + for (std::size_t next = 1; next < length; ++next) { + const auto continuation = static_cast(text[index + next]); + if ((continuation & 0xc0) != 0x80) + throw Domain::RelinkerException("Windows library path is not valid UTF-8: " + text); + code = code << 6 | (continuation & 0x3f); + } + if (code < minimum[length] || code > 0x10ffff || (code >= 0xd800 && code < 0xe000)) + throw Domain::RelinkerException("Windows library path is not valid UTF-8: " + text); + if (code >= 0x10000) { + code -= 0x10000; + result.push_back(static_cast(0xd800 + (code >> 10))); + result.push_back(static_cast(0xdc00 + (code & 0x3ff))); + } else { + result.push_back(static_cast(code)); + } + index += length; + } + return result; } -WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, const std::uint32_t entryRva, const WindowsImports& nativeImports, const std::vector& libraries, const std::vector& imports, const std::string& runPath, const bool lazyBinding, const bool dependencyDiagnostics, const std::vector& guestModules) const { +} + +WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, const std::uint32_t entryRva, const WindowsImports& nativeImports, const std::vector& libraries, const std::vector& imports, const std::string& runPath, const bool lazyBinding, const bool dependencyDiagnostics, const std::vector& guestModules, const std::set& unmatchedImportModules, const bool traceImports) const { if (!imports.empty() && libraries.empty()) throw Domain::RelinkerException("ELF imports have no DT_NEEDED libraries"); const auto path = normalizeRunPath(runPath); @@ -54,13 +90,28 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con Io::AppendString(data, value); return rva; }; + const auto addWideString = [&](const std::u16string& value) { + Io::AlignBuffer(data, 2); + const auto rva = CheckedRva(dataRva + data.size()); + for (const auto unit : value) + Io::AppendU16(data, unit); + Io::AppendU16(data, 0); + return rva; + }; - const auto programPath = reserve(PathCapacity); - const auto modulePath = reserve(PathCapacity); + 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); const auto argumentCount = reserve(4); const auto wideArguments = reserve(8); const auto shellHandle = reserve(8); + const auto logHandle = reserve(8); + const std::u16string logFileName = u"anyps5.log"; + const auto logName = addWideString(logFileName); const auto shellLibrary = addString("shell32.dll"); const auto parseArguments = addString("CommandLineToArgvW"); const auto handles = reserve(libraries.size() * 8); @@ -71,44 +122,63 @@ 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 libraryNames; + std::vector libraryUnits; for (std::size_t index = 0; index < libraries.size(); ++index) { auto name = index < guestModules.size() ? libraries[index] : path + libraries[index]; std::replace(name.begin(), name.end(), '/', '\\'); - if (name.size() + 1 >= PathCapacity) + const auto wideName = toUtf16(name); + if (wideName.size() + 1 >= PathCapacity) throw Domain::RelinkerException("Windows library path exceeds the startup buffer: " + name); - libraryPaths.push_back(addString(name)); - libraryNames.push_back(std::move(name)); + libraryPaths.push_back(addWideString(wideName)); + libraryUnits.push_back(wideName.size()); } 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; std::vector platformTlsResolvers; + 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 (libraries[index] == "libc.prx" && library != "libc.prx") order.push_back(CheckedRva(index)); } - if (import.Library == "libSceLibcInternal.prx") { + 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()) + if (std::find(names.begin(), names.end(), "libc.prx") != names.end() && !guestModules[index].ZeroReturnExports.contains(import.Name)) order.push_back(CheckedRva(index)); } } @@ -119,17 +189,56 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con })); searchTables.push_back(CheckedRva(dataRva + data.size())); searchCounts.push_back(CheckedRva(order.size())); - for (const auto index : order) Io::AppendU32(data, index); + for (const auto index : order) { + Io::AppendU32(data, index); + if (!import.Library.empty()) imported[index] = true; + } + } + + const auto stem = [](const std::string& name) { + auto result = std::filesystem::path(name).filename().string(); + result.resize(std::min(result.size(), result.find('.'))); + std::transform(result.begin(), result.end(), result.begin(), [](unsigned char character) { return static_cast(std::tolower(character)); }); + return result; + }; + std::set unmatched; + for (const auto& module : unmatchedImportModules) unmatched.insert(stem(module)); + for (const auto& guest : guestModules) { + for (const auto& module : guest.UnmatchedImportModules) unmatched.insert(stem(module)); + } + for (std::size_t index = guestModules.size(); index < libraries.size(); ++index) { + if (unmatched.contains(stem(libraries[index]))) imported[index] = true; + } + + std::optional traceProvider; + std::map traceLibraries; + std::uint32_t traceSymbol = 0; + std::uint32_t traceFunction = 0; + if (traceImports) { + for (std::size_t index = guestModules.size(); index < libraries.size(); ++index) { + if (libraries[index] == "libc.prx") traceProvider = index; + } + if (!traceProvider.has_value()) + throw Domain::RelinkerException("--trace-imports requires libc.prx among the libraries"); + imported[*traceProvider] = true; + traceSymbol = addString("TraceImport_nid_no_patch"); + traceFunction = reserve(8); + for (const auto& import : imports) { + if (!traceLibraries.contains(import.Library)) traceLibraries.emplace(import.Library, addString(import.Library)); + } } const auto lastError = reserve(4); + const auto unresolvedCount = reserve(4); const auto errorDigits = reserve(11); const auto errorMessage = reserve(ErrorMessageCapacity); const auto loading = addString("Loading PRX: "); const auto loaded = addString(" -> OK\n"); const auto loadFailed = addString(" -> FAILED\n"); const auto failedModule = addString("Failed to load module: "); + const auto skippedModule = addString(" -> not found; nothing is imported from it, continuing without it\n"); const auto errorPrefix = addString("GetLastError: "); + const auto moduleNotFoundHint = addString("hint: the module or one of its dependent DLLs was not found; a self-built libs/ also needs libgcc_s_seh-1.dll, libstdc++-6.dll and libwinpthread-1.dll from the toolchain's mingw64/bin next to it\n"); const auto messageSeparator = addString(" - "); const auto newline = addString("\n"); const auto searched = addString("Searched libraries:\n"); @@ -138,6 +247,7 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con std::vector resolvedPaths; for (std::size_t index = 0; index < libraries.size(); ++index) resolvedPaths.push_back(reserve(PathCapacity)); + const std::uint32_t programText = dependencyDiagnostics ? reserve(PathCapacity) : 0; const auto diagnosticsOffset = data.size(); std::vector errors = {"FAIL: cannot obtain executable path\n", "FAIL: executable or library path is too long\n", "FAIL: executable path has no directory\n"}; @@ -146,6 +256,12 @@ 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(); + errors.push_back("FAIL: libc.prx does not export TraceImport_nid_no_patch\n"); std::vector errorRvas; for (const auto& error : errors) errorRvas.push_back(addString(error)); @@ -246,12 +362,112 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con code.PatchBranch(success, code.GetRva()); }; + const auto writeUtf8 = [&](const std::uint32_t destination) { + code.Emit({0xb9}); + code.U32(65001); + code.Emit({0x31, 0xd2, 0x41, 0xb9, 0xff, 0xff, 0xff, 0xff}); + code.Rip({0x48, 0x8d, 0x05}, destination); + code.Emit({0x48, 0x89, 0x44, 0x24, 0x20, 0x48, 0xc7, 0x44, 0x24, 0x28}); + code.U32(PathCapacity); + code.Emit({0x48, 0xc7, 0x44, 0x24, 0x30, 0, 0, 0, 0, 0x48, 0xc7, 0x44, 0x24, 0x38, 0, 0, 0, 0}); + call("WideCharToMultiByte"); + code.Emit({0x85, 0xc0}); + const auto converted = code.Branch({0x0f, 0x85}); + fail(1, 0xc0000106u); + code.PatchBranch(converted, code.GetRva()); + }; + + const auto logMissingHandle = [&](const std::uint32_t standardHandle) { + const auto units = CheckedRva(logFileName.size() + 1); + code.Emit({0xb9}); + code.U32(standardHandle); + call("GetStdHandle"); + code.Emit({0x48, 0x85, 0xc0}); + const auto missing = code.Branch({0x0f, 0x84}); + code.Emit({0x48, 0x83, 0xf8, 0xff}); + const auto present = code.Branch({0x0f, 0x85}); + code.PatchBranch(missing, code.GetRva()); + code.Rip({0x48, 0x8b, 0x05}, logHandle); + code.Emit({0x48, 0x85, 0xc0}); + const auto opened = code.Branch({0x0f, 0x85}); + code.Emit({0x4c, 0x89, 0xe0, 0x48, 0x29, 0xd8, 0x48, 0x05}); + code.U32(units * 2); + code.Emit({0x48, 0x3d}); + code.U32(PathCapacity * 2); + const auto tooLong = code.Branch({0x0f, 0x87}); + code.Emit({0x4c, 0x89, 0xe7}); + code.Rip({0x48, 0x8d, 0x35}, logName); + code.Emit({0xb9}); + code.U32(units); + code.Emit({0x66, 0xf3, 0xa5}); + code.Rip({0x48, 0x8d, 0x0d}, modulePath); + code.Emit({0xba, 0, 0, 0, 0x40, 0x41, 0xb8, 3, 0, 0, 0, 0x45, 0x31, 0xc9}); + code.Emit({0x48, 0xc7, 0x44, 0x24, 0x20, 2, 0, 0, 0, 0x48, 0xc7, 0x44, 0x24, 0x28, 0x80, 0, 0, 0, 0x48, 0xc7, 0x44, 0x24, 0x30, 0, 0, 0, 0}); + call("CreateFileW"); + code.Emit({0x66, 0x41, 0xc7, 0x04, 0x24, 0x00, 0x00}); + code.Emit({0x48, 0x83, 0xf8, 0xff}); + const auto failed = code.Branch({0x0f, 0x84}); + code.Rip({0x48, 0x89, 0x05}, logHandle); + code.PatchBranch(opened, code.GetRva()); + code.Emit({0xb9}); + code.U32(standardHandle); + code.Emit({0x48, 0x89, 0xc2}); + call("SetStdHandle"); + const auto done = code.GetRva(); + code.PatchBranch(present, done); + code.PatchBranch(tooLong, done); + code.PatchBranch(failed, done); + }; + code.Emit({0x53, 0x55, 0x56, 0x57, 0x41, 0x54, 0x48, 0x83, 0xec, 0x60}); + code.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}); code.U32(PathCapacity); - call("GetModuleFileNameA"); + call("GetModuleFileNameW"); requireNonzero(0, 0xc000000du); code.Emit({0x3d}); code.U32(PathCapacity); @@ -261,60 +477,121 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con code.Emit({0x89, 0xc1}); code.Rip({0x48, 0x8d, 0x35}, programPath); code.Rip({0x48, 0x8d, 0x3d}, modulePath); - code.Emit({0xfc, 0xf3, 0xa4, 0x49, 0x89, 0xfc}); + code.Emit({0xfc, 0x66, 0xf3, 0xa5, 0x49, 0x89, 0xfc}); code.Rip({0x48, 0x8d, 0x1d}, modulePath); const auto findSeparator = code.GetRva(); code.Emit({0x49, 0x39, 0xdc}); const auto hasDirectory = code.Branch({0x0f, 0x85}); fail(2, 0xc000000du); code.PatchBranch(hasDirectory, code.GetRva()); - code.Emit({0x49, 0xff, 0xcc, 0x41, 0x80, 0x3c, 0x24, 0x5c}); + code.Emit({0x49, 0x83, 0xec, 0x02, 0x66, 0x41, 0x83, 0x3c, 0x24, 0x5c}); code.Rip({0x0f, 0x85}, findSeparator); - code.Emit({0x49, 0xff, 0xc4}); + code.Emit({0x49, 0x83, 0xc4, 0x02}); + code.Emit({0x66, 0x41, 0xc7, 0x04, 0x24, 0x00, 0x00}); + logMissingHandle(0xfffffff5u); + logMissingHandle(0xfffffff4u); + code.Rip({0x48, 0x8d, 0x0d}, modulePath); + call("SetCurrentDirectoryW"); + requireNonzero(directoryError, 0xc000000du); + if (dependencyDiagnostics) { + code.Rip({0x4c, 0x8d, 0x05}, programPath); + 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()) { code.Rip({0x48, 0x8d, 0x0d}, libraryPaths[index]); } else { + const auto units = CheckedRva(libraryUnits[index] + 1); code.Emit({0x4c, 0x89, 0xe0, 0x48, 0x29, 0xd8, 0x48, 0x05}); - code.U32(CheckedRva(libraryNames[index].size() + 1)); + code.U32(units * 2); code.Emit({0x48, 0x3d}); - code.U32(PathCapacity); + code.U32(PathCapacity * 2); const auto fits = code.Branch({0x0f, 0x86}); fail(1, 0xc0000106u); code.PatchBranch(fits, code.GetRva()); code.Emit({0x4c, 0x89, 0xe7}); code.Rip({0x48, 0x8d, 0x35}, libraryPaths[index]); code.Emit({0xb9}); - code.U32(CheckedRva(libraryNames[index].size() + 1)); - code.Emit({0xf3, 0xa4}); + code.U32(units); + code.Emit({0x66, 0xf3, 0xa5}); code.Rip({0x48, 0x8d, 0x0d}, modulePath); } - code.Emit({0x48, 0x89, 0xce}); - call("lstrlenA"); - code.Emit({0x8d, 0x48, 1}); - code.Rip({0x48, 0x8d, 0x3d}, resolvedPaths[index]); - code.Emit({0xf3, 0xa4}); + code.Emit({0x48, 0x89, 0xce, 0x49, 0x89, 0xf0}); + writeUtf8(resolvedPaths[index]); if (dependencyDiagnostics) { writeString(loading); writeString(resolvedPaths[index]); } - code.Rip({0x48, 0x8d, 0x0d}, resolvedPaths[index]); - code.Emit({0x31, 0xd2, 0x41, 0xb8, 0, 0x11, 0, 0}); - call("LoadLibraryExA"); + std::vector missingOptional; + std::optional probeFailed; + if (index >= guestModules.size() && !imported[index]) { + code.Emit({0x48, 0x89, 0xf1}); + call("GetFileAttributesW"); + code.Emit({0x83, 0xf8, 0xff}); + const auto exists = code.Branch({0x0f, 0x85}); + captureLastError(); + code.Emit({0x83, 0xf8, 2}); + missingOptional.push_back(code.Branch({0x0f, 0x84})); + code.Emit({0x83, 0xf8, 3}); + missingOptional.push_back(code.Branch({0x0f, 0x84})); + probeFailed = code.Branch({0xe9}); + code.PatchBranch(exists, code.GetRva()); + } + + code.Emit({0x48, 0x89, 0xf1, 0x31, 0xd2, 0x41, 0xb8, 0, 0x11, 0, 0}); + call("LoadLibraryExW"); code.Emit({0x48, 0x85, 0xc0}); const auto loadSucceeded = code.Branch({0x0f, 0x85}); captureLastError(); - writeString(loadFailed, true); + if (dependencyDiagnostics) { + writeString(loadFailed, true); + } + if (probeFailed) code.PatchBranch(*probeFailed, code.GetRva()); writeString(failedModule, true); writeString(resolvedPaths[index], true); writeString(newline, true); writeLastError(); + code.Rip({0x8b, 0x05}, lastError); + code.Emit({0x83, 0xf8, 0x7e}); + const auto notModuleNotFound = code.Branch({0x0f, 0x85}); + writeString(moduleNotFoundHint, true); + code.PatchBranch(notModuleNotFound, code.GetRva()); if (dependencyDiagnostics) { code.Rip({0x48, 0x8d, 0x0d}, resolvedPaths[index]); - code.Rip({0x48, 0x8d, 0x15}, programPath); + code.Rip({0x48, 0x8d, 0x15}, programText); dependencyCalls.push_back(code.Branch({0xe8})); } raise(0xc0000135u); @@ -324,9 +601,31 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con if (dependencyDiagnostics) { writeString(loaded); } + if (!missingOptional.empty()) { + const auto next = code.Branch({0xe9}); + for (const auto branch : missingOptional) code.PatchBranch(branch, code.GetRva()); + if (dependencyDiagnostics) writeString(skippedModule, true); + code.PatchBranch(next, code.GetRva()); + } } + if (traceImports) { + code.Rip({0x48, 0x8b, 0x0d}, CheckedRva(handles + *traceProvider * 8)); + code.Rip({0x48, 0x8d, 0x15}, traceSymbol); + call("GetProcAddress"); + requireNonzero(traceError, 0xc0000139u); + code.Rip({0x48, 0x89, 0x05}, traceFunction); + } + const auto trace = [&](const PeImport& import, const std::uint32_t symbolName) { + if (!traceImports || !import.Function || import.Addend != 0) return; + code.Emit({0x48, 0x89, 0xc1}); + code.Rip({0x48, 0x8d, 0x15}, symbolName); + code.Rip({0x4c, 0x8d, 0x05}, traceLibraries.at(import.Library)); + code.Rip({0xff, 0x15}, traceFunction); + }; + std::vector unresolvedBranches; + bool anyUnresolvable = false; std::vector lazyUnresolvedImports; std::vector tlsResolverAddresses; for (std::size_t index = 0; index < imports.size(); ++index) { @@ -345,13 +644,42 @@ 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(); + if (imports[index].Addend != 0) { + code.Emit({0x48, 0xba}); + code.U64(imports[index].Addend); + code.Emit({0x48, 0x01, 0xd0}); + } + guestStartup.WriteImport(code, imports[index], handles); + const auto weakDone = code.Branch({0xe9}); + patchResolved(); + code.Rip({0xe9}, writeWeak); + code.PatchBranch(weakDone, code.GetRva()); + continue; + } 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); @@ -363,8 +691,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) { @@ -372,9 +701,17 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con code.U64(imports[index].Addend); code.Emit({0x48, 0x01, 0xd0}); } + trace(imports[index], symbolNames[index]); guestStartup.WriteImport(code, imports[index], handles); + 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); @@ -394,6 +731,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); @@ -426,6 +770,7 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con call("FreeLibrary"); code.Rip({0x48, 0x8b, 0x3d}, argumentBlock); const auto exitCallback = code.Branch({0x48, 0x8d, 0x35}); + code.Emit({0x48, 0xc7, 0x44, 0x24, 0x40, 0, 0, 0, 0, 0x48, 0xc7, 0x44, 0x24, 0x48, 0, 0, 0, 0, 0x48, 0x8d, 0x6c, 0x24, 0x40}); code.Rip({0xe8}, entryRva); code.Emit({0x89, 0x44, 0x24, 0x58}); guestStartup.Finalize(code, guestModules, handles, guestFinished); @@ -438,9 +783,10 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con for (const auto branch : unresolvedBranches) code.PatchBranch(branch, code.GetRva()); writeString(searched, true); - for (const auto resolvedPath : resolvedPaths) { + for (std::size_t index = 0; index < resolvedPaths.size(); ++index) { + if (index >= guestModules.size() && !imported[index] && !unscopedImports) continue; writeString(indent, true); - writeString(resolvedPath, true); + writeString(resolvedPaths[index], true); writeString(newline, true); } writeLastError(); diff --git a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp index 2dc27774d1..0b284a4bcd 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp @@ -6,14 +6,16 @@ #include #include #include +#include #include #include #include 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; @@ -58,8 +60,8 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI } else if (type == 18) { const auto tls = std::find_if(guest.Headers.begin(), guest.Headers.end(), [](const auto& header) { return header.Type == 7; }); if (tls == guest.Headers.end() || (symbolIndex != 0 && (symbol.Section == 0 || (symbol.Info & 15) != 6))) throw Domain::RelinkerException("Unsupported external static TLS relocation", target); - const auto size = Io::AlignUp64(tls->MemorySize, std::max(tls->Alignment, 16)); - image.WritePointer(target, symbol.Value + addend - size); + const auto threadPointerOffset = Io::AlignUp64(tls->MemorySize, std::max(tls->Alignment, 1)); + image.WritePointer(target, symbol.Value + addend - threadPointerOffset); } else if (type == 1 || type == 6 || type == 7) { if (symbolIndex == 0 || (type != 1 && addend != 0) || (symbol.Info & 15) == 6) throw Domain::RelinkerException("Invalid guest symbol relocation", target); if (symbol.Section == Relinker::AbsoluteSection) image.WritePointer(target, symbol.Value + addend); @@ -70,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}); + 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); } @@ -82,9 +84,9 @@ 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) { - error.InputPath = guest.SourcePath.string(); + if (error.InputPath.empty()) error.InputPath = Io::Utf8Path(guest.SourcePath); throw; } WindowsTrampolineBuilder().Build(guest.Trampolines, image, sections, nextRva); @@ -92,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; @@ -107,10 +109,29 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI sections.push_back({".ehmeta", nextRva, SectionRead | 0x40u, std::move(metadata)}); nextRva = AlignRva(nextRva + sections.back().Data.size()); } + const auto returnsZero = [&](const std::uint64_t address) { + for (const auto& header : guest.Headers) { + if (header.Type != 1 || (header.Flags & 1) == 0 || address < header.MappedAddress || address - header.MappedAddress >= header.FileSize) continue; + const auto start = guest.Bytes.begin() + static_cast(header.Offset + (address - header.MappedAddress)); + const auto available = header.FileSize - (address - header.MappedAddress); + const auto matches = [&](const std::vector& code) { + return code.size() <= available && std::equal(code.begin(), code.end(), start); + }; + return matches({0x31, 0xc0, 0xc3}) || matches({0x66, 0x0f, 0xef, 0xc0, 0x31, 0xc0, 0xc3}); + } + return false; + }; std::map exports; + std::map exportSymbols; PeSection tlsExports{".tlsrefs", nextRva, SectionRead | 0x40u, {}}; for (const auto& symbol : guest.Symbols) { - if (symbol.Section == 0 || symbol.Section == Relinker::AbsoluteSection || (symbol.Info >> 4) == 0 || symbol.Visibility == 1 || symbol.Visibility == 2) continue; + if (symbol.Section == 0 || (symbol.Info >> 4) == 0 || symbol.Visibility == 1 || symbol.Visibility == 2) continue; + const auto [previous, inserted] = exportSymbols.emplace(symbol.Name, &symbol); + if (!inserted) { + if (!Relinker::IdenticalOrdinaryFunctionExport(*previous->second, symbol)) throw Domain::RelinkerException("Duplicate guest export: " + symbol.Name); + continue; + } + if (symbol.Section == Relinker::AbsoluteSection) continue; if (symbol.Section >= 0xff00) throw Domain::RelinkerException("Unsupported guest export section: " + symbol.Name); std::uint32_t rva = 0; if ((symbol.Info & 15) == 6) { @@ -121,11 +142,21 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI relocations.push_back(rva); } else rva = image.GetRva(symbol.Value, std::max(symbol.Size, 1)); if (!exports.emplace(symbol.Name, rva).second) throw Domain::RelinkerException("Duplicate guest export: " + symbol.Name); + if ((symbol.Info & 15) == 2 && returnsZero(symbol.Value)) runtime.ZeroReturnExports.insert(symbol.Name); } if (!tlsExports.Data.empty()) { nextRva = AlignRva(nextRva + tlsExports.Data.size()); sections.push_back(std::move(tlsExports)); } + if (runtime.DeferInitialization) { + PeSection lifecycle{".ginit", nextRva, SectionRead | 0x40u, {}}; + 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)); + nextRva = AlignRva(nextRva + lifecycle.Data.size()); + sections.push_back(std::move(lifecycle)); + } if (exports.size() > 65535) throw Domain::RelinkerException("Too many guest PE exports"); PeSection exportSection{".edata", nextRva, SectionRead | 0x40u, std::vector(40)}; auto& data = exportSection.Data; @@ -172,7 +203,8 @@ 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); const auto characteristics = Io::ReadU16(result, 0x80 + 22); diff --git a/core/relinker/elfpatcher/src/windows/WindowsGuestStartup.cpp b/core/relinker/elfpatcher/src/windows/WindowsGuestStartup.cpp index 0d60683062..8d241798f9 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsGuestStartup.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsGuestStartup.cpp @@ -29,6 +29,7 @@ void WindowsGuestStartup::callLifecycle(WindowsStubEmitter& code, const std::uin void WindowsGuestStartup::Initialize(WindowsStubEmitter& code, const std::vector& modules, const std::uint32_t handles) const { for (std::size_t index = 0; index < modules.size(); ++index) { + if (modules[index].DeferInitialization) continue; const auto handle = CheckedRva(handles + index * 8); callLifecycle(code, handle, modules[index].InitRva); for (const auto slot : modules[index].InitArrayRvas) callLifecycle(code, handle, slot, true); @@ -43,6 +44,7 @@ void WindowsGuestStartup::Finalize(WindowsStubEmitter& code, const std::vector 0; --index) { const auto handle = CheckedRva(handles + (index - 1) * 8); const auto& module = modules[index - 1]; + if (module.DeferInitialization) continue; for (auto slot = module.FiniArrayRvas.rbegin(); slot != module.FiniArrayRvas.rend(); ++slot) callLifecycle(code, handle, *slot, true); callLifecycle(code, handle, module.FiniRva); } diff --git a/core/relinker/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp index 6d59474ec5..4a547373bd 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp @@ -3,7 +3,7 @@ #include #include #include -#include + #include namespace Elfpatcher::Windows { @@ -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"); @@ -30,11 +33,17 @@ void validatePng(const std::vector& bytes, const std::filesystem:: ((color == 2 || color == 4 || color == 6) && (depth == 8 || depth == 16)); if (!validDepth || bytes[26] != 0 || bytes[27] != 0 || bytes[28] > 1) fail("invalid IHDR encoding"); bool hasData = false; + bool dataStarted = false; + bool dataEnded = false; + bool hasPalette = false; for (std::size_t offset = 8; offset < bytes.size();) { if (bytes.size() - offset < 12) fail("truncated chunk"); const auto length = readBigEndian(bytes, offset); 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]; @@ -42,7 +51,20 @@ void validatePng(const std::vector& bytes, const std::filesystem:: } if ((crc ^ 0xffffffffu) != readBigEndian(bytes, offset + 8 + length)) fail("invalid chunk CRC"); if (type == 0x49484452 && offset != 8) fail("duplicate IHDR"); - if (type == 0x49444154 && length != 0) hasData = true; + if (type == 0x504c5445) { + if (hasPalette || dataStarted || !length || length % 3 || length > 768 || color == 0 || color == 4 || + (color == 3 && length / 3 > (1u << depth))) fail("invalid PLTE"); + hasPalette = true; + } + if (type == 0x49444154) { + if (dataEnded || (color == 3 && !hasPalette)) fail("invalid IDAT order or missing PLTE"); + dataStarted = true; + if (length != 0) hasData = true; + } else { + if (dataStarted) dataEnded = true; + if (!(type & 0x20000000u) && type != 0x49484452 && type != 0x504c5445 && type != 0x49454e44) + fail("unknown critical chunk"); + } offset += length + 12; if (type == 0x49454e44) { if (length != 0 || offset != bytes.size() || !hasData) fail("invalid IEND or missing IDAT"); @@ -54,19 +76,14 @@ void validatePng(const std::vector& bytes, const std::filesystem:: } -PeDirectory WindowsIconResourceBuilder::Build(const std::filesystem::path& iconPath, std::vector& sections, const std::uint32_t nextRva) const { - if (iconPath.empty()) return {}; - std::error_code error; - const bool exists = std::filesystem::exists(iconPath, error); - if (error) throw Domain::RelinkerException("Cannot inspect Windows icon '" + iconPath.string() + "': " + error.message()); - if (!exists) return {}; - std::ifstream stream(iconPath, std::ios::binary | std::ios::ate); - if (!stream) throw Domain::RelinkerException("Cannot open Windows icon '" + iconPath.string() + "'"); - const auto size = stream.tellg(); - if (size < 0 || static_cast(size) > 0x7fffffffu - 184) throw Domain::RelinkerException("Invalid Windows icon file size: " + iconPath.string()); - std::vector png(static_cast(size)); - stream.seekg(0); - if (!stream.read(reinterpret_cast(png.data()), size)) throw Domain::RelinkerException("Cannot read Windows icon '" + iconPath.string() + "'"); +PeDirectory WindowsIconResourceBuilder::Build(const std::vector& png, const std::filesystem::path& iconPath, std::vector& sections, const std::uint32_t nextRva) const { + if (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()); CheckedRva(static_cast(nextRva) + data.size()); diff --git a/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp index 26a775b872..e7153da217 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 = {"ExitProcess", "FormatMessageA", "FreeLibrary", "GetCommandLineW", "GetFileAttributesA", "GetLastError", "GetModuleFileNameA", "GetModuleHandleA", "GetProcAddress", "GetStdHandle", "GetSystemDirectoryA", "LoadLibraryExA", "LocalFree", "RaiseException", "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,16 +44,19 @@ 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)); - if (name.empty() || name.find_first_of("/\\:") != std::string::npos || !unique.insert(name).second) - throw Domain::RelinkerException("Invalid or duplicate DT_NEEDED library: " + name); - result.push_back(std::move(name)); + 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) + result.push_back(std::move(name)); } // AnyPS5 implements the C runtime in libc.prx. Windows GetProcAddress // does not search a module's dependencies as ELF symbol lookup does. // Keep the requested module first, then make its shared runtime visible. - if (unique.contains("libSceLibcInternal.prx") && !unique.contains("libc.prx")) + if ((unique.contains("libSceLibcInternal.prx") || unique.contains("libkernel.prx")) && !unique.contains("libc.prx")) result.push_back("libc.prx"); + if (unique.contains("libkernel_web.prx") && !unique.contains("libkernel.prx")) + result.push_back("libkernel.prx"); return result; } diff --git a/core/relinker/elfpatcher/src/windows/WindowsLoadImage.cpp b/core/relinker/elfpatcher/src/windows/WindowsLoadImage.cpp index f82ee3d930..9f100e655c 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsLoadImage.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsLoadImage.cpp @@ -5,7 +5,7 @@ namespace Elfpatcher::Windows { -WindowsLoadImage::WindowsLoadImage(const std::vector& source, const std::vector& headers, const bool requireEntry) { +WindowsLoadImage::WindowsLoadImage(const std::vector& source, const std::vector& headers, const bool requireEntry, const std::uint64_t imageBase) : imageBase(imageBase) { if (source.size() < 64 || Io::ReadU32(source, 0) != 0x464c457f || source[4] != 2 || source[5] != 1 || source[6] != 1 || Io::ReadU16(source, 18) != 62) throw Domain::RelinkerException("Windows output requires a little-endian ELF64 x86-64 image"); const auto type = Io::ReadU16(source, 16); @@ -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,13 +79,40 @@ 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; + return imageBase + LoadRva + address - firstAddress; +} + +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 { @@ -95,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 322fbd46c3..063af7924b 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp @@ -8,32 +8,19 @@ #include #include #include -#include -#include + + #include namespace Elfpatcher::Windows { namespace { -void writeDiagnosticsImports(const std::vector& imports) { - const std::filesystem::path path = std::filesystem::absolute("windows-diagnostics-imports.txt"); - std::ofstream stream(path, std::ios::trunc); - if (!stream) - throw Domain::RelinkerException("Cannot open windows-diagnostics-imports.txt for writing"); - for (const auto& import : imports) - stream << import.Name << '\n'; - if (!stream) - throw Domain::RelinkerException("Cannot write windows-diagnostics-imports.txt"); - stream.close(); - std::cout << "Wrote Windows import diagnostics to " << path.string() << '\n'; -} - -void writeGotStub(std::vector& sections, const std::uint32_t targetRva, const std::uint32_t stubRva) { +void writeGotStub(std::vector& sections, const std::uint32_t targetRva, const std::uint32_t stubRva, const std::uint64_t imageBase) { for (auto& section : sections) { - if (section.Data.size() < 8 || targetRva < section.Rva || targetRva - section.Rva > section.Data.size() - 8) + if (!section.Writable(targetRva, 8)) continue; - Io::WriteU64(section.Data, targetRva - section.Rva, ImageBase + stubRva); + Io::WriteU64(section.Data, targetRva - section.Rva, imageBase + stubRva); return; } throw Domain::RelinkerException("Lazy import GOT slot is not contained in any section", targetRva); @@ -41,16 +28,26 @@ void writeGotStub(std::vector& sections, const std::uint32_t targetRv } -WindowsPePatcher::WindowsPePatcher(const bool windowsGui, std::filesystem::path iconPath) : _windowsGui(windowsGui), _iconPath(std::move(iconPath)) { +WindowsPePatcher::WindowsPePatcher(const bool windowsGui, std::optional> icon, std::filesystem::path iconName, std::function progress, const std::optional fixedBase, const bool traceImports, 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"); } -std::vector WindowsPePatcher::Patch(const std::vector& sourceElf, const std::vector& originalHeaders, const Domain::SysVDynamicSection& dynamicSection, const std::uint64_t originalPltGotVaddr, const std::string& runPath, const bool lazyBinding, const bool dependencyDiagnostics, const std::vector& trampolines) { - WindowsLoadImage image(sourceElf, originalHeaders); +const std::string& WindowsPePatcher::GetDiagnostics() const { return diagnostics; } + +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(); + WindowsLoadImage image(sourceElf, originalHeaders, true, fixedBase ? *fixedBase - LoadRva : ImageBase); if (originalPltGotVaddr != 0) image.GetRva(originalPltGotVaddr, 8); + report(500); const WindowsRelocationBuilder relocationBuilder; - auto relocations = relocationBuilder.Apply(image, dynamicSection); + auto relocations = relocationBuilder.Apply(image, dynamicSection, originalHeaders); + report(1000); auto sections = image.BuildSections(); + report(1500); std::array directories{}; auto nextRva = image.GetEndRva(); bool hasProcessParameters = false; @@ -71,7 +68,8 @@ std::vector WindowsPePatcher::Patch(const std::vector WindowsPePatcher::Patch(const std::vector(index), import.RelocationType, import.Library}); + for (const auto& import : module.Imports) { + relocations.Imports.push_back({import.Name, import.TargetRva, import.Addend, static_cast(index), import.RelocationType, import.Library}); + relocations.Imports.back().Function = import.Function; + relocations.Imports.back().Weak = import.Weak; + } } libraries.insert(libraries.begin(), guestPaths.begin(), guestPaths.end()); if (dependencyDiagnostics) - writeDiagnosticsImports(relocations.Imports); - auto entry = WindowsEntryStubBuilder().Build(nextRva, image.GetEntryRva(), nativeImports, libraries, relocations.Imports, runPath, lazyBinding, dependencyDiagnostics, dynamicSection.GuestModules); + for (const auto& import : relocations.Imports) diagnostics += import.Name + '\n'; + report(8500); + auto entry = WindowsEntryStubBuilder().Build(nextRva, image.GetEntryRva(), nativeImports, libraries, relocations.Imports, runPath, lazyBinding, dependencyDiagnostics, dynamicSection.GuestModules, dynamicSection.UnmatchedImportModules, traceImports); + report(9000); directories[3] = entry.ExceptionDirectory; const auto entryRva = entry.Code.Rva; nextRva = AlignRva(entry.Code.Rva + entry.Code.Data.size()); @@ -96,17 +100,20 @@ std::vector WindowsPePatcher::Patch(const std::vector{} : relocationBuilder.BuildBaseRelocations(relocations.BaseRelocations); if (!relocationData.empty()) { directories[5] = {nextRva, CheckedRva(relocationData.size())}; sections.push_back({".reloc", nextRva, SectionRead | 0x02000040u, std::move(relocationData)}); nextRva = AlignRva(nextRva + sections.back().Data.size()); } - directories[2] = WindowsIconResourceBuilder().Build(_iconPath, sections, nextRva); - return WindowsPeWriter().Write(sections, entryRva, directories, _windowsGui); + directories[2] = icon ? WindowsIconResourceBuilder().Build(*icon, iconName, sections, nextRva) : PeDirectory{}; + report(9500); + auto result = WindowsPeWriter().Write(sections, entryRva, directories, windowsGui, fixedBase ? std::optional(image.GetImageBase()) : std::nullopt, false); + report(10000); + return result; } } diff --git a/core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp b/core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp index 3c4e9ac34d..48d867f0f5 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp @@ -5,7 +5,7 @@ namespace Elfpatcher::Windows { -std::vector WindowsPeWriter::Write(const std::vector& sections, const std::uint32_t entryRva, const std::array& directories, const bool windowsGui) const { +std::vector WindowsPeWriter::Write(const std::vector& sections, const std::uint32_t entryRva, const std::array& directories, const bool windowsGui, const std::optional fixedImageBase, const bool dynamicBase) const { constexpr std::size_t peOffset = 0x80; constexpr std::size_t optionalOffset = peOffset + 24; constexpr std::size_t sectionTable = optionalOffset + 240; @@ -18,10 +18,12 @@ std::vector WindowsPeWriter::Write(const std::vector& s Io::WriteU16(result, peOffset + 4, 0x8664); Io::WriteU16(result, peOffset + 6, static_cast(sections.size())); Io::WriteU16(result, peOffset + 20, 240); - Io::WriteU16(result, peOffset + 22, directories[5].Size == 0 ? 0x23 : 0x22); + if (fixedImageBase && (directories[5].Size != 0 || *fixedImageBase < 0x10000 || (*fixedImageBase & 0xffff) != 0)) + throw Domain::RelinkerException("Invalid fixed Windows image base or base relocations"); + Io::WriteU16(result, peOffset + 22, fixedImageBase ? 0x23 : 0x22); Io::WriteU16(result, optionalOffset, 0x20b); Io::WriteU32(result, optionalOffset + 16, entryRva); - Io::WriteU64(result, optionalOffset + 24, ImageBase); + Io::WriteU64(result, optionalOffset + 24, fixedImageBase.value_or(ImageBase)); Io::WriteU32(result, optionalOffset + 32, SectionAlignment); Io::WriteU32(result, optionalOffset + 36, FileAlignment); Io::WriteU16(result, optionalOffset + 40, 6); @@ -30,7 +32,7 @@ std::vector WindowsPeWriter::Write(const std::vector& s Io::WriteU16(result, optionalOffset + 50, 2); Io::WriteU32(result, optionalOffset + 60, LoadRva); Io::WriteU16(result, optionalOffset + 68, windowsGui ? 2 : 3); - Io::WriteU16(result, optionalOffset + 70, directories[5].Size == 0 ? 0x100 : 0x140); + Io::WriteU16(result, optionalOffset + 70, dynamicBase && directories[5].Size != 0 ? 0x140 : 0x100); Io::WriteU64(result, optionalOffset + 72, 0x210000); Io::WriteU64(result, optionalOffset + 80, 0x200000); Io::WriteU64(result, optionalOffset + 88, 0x100000); @@ -42,13 +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 rawSize = Io::AlignUp(CheckedRva(section.Data.size()), FileAlignment); - const auto rawOffset = CheckedRva(result.size()); + 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); @@ -61,12 +64,16 @@ std::vector WindowsPeWriter::Write(const std::vector& s } else { dataSize = CheckedRva(static_cast(dataSize) + rawSize); } - result.insert(result.end(), section.Data.begin(), section.Data.end()); - result.resize(static_cast(rawOffset) + rawSize); - endRva = AlignRva(section.Rva + static_cast(section.Data.size())); + if (rawSize != 0) { + result.insert(result.end(), section.Data.begin(), section.Data.begin() + static_cast(initialized)); + result.resize(static_cast(rawOffset) + rawSize); + } + endRva = AlignRva(section.Rva + section.MemorySize()); } if (!foundEntry) throw Domain::RelinkerException("PE entry point is not inside executable section data", entryRva); + if (fixedImageBase && *fixedImageBase > 0x7fffffffffff - static_cast(endRva)) + throw Domain::RelinkerException("Fixed Windows image exceeds the user address space"); Io::WriteU32(result, optionalOffset + 4, codeSize); Io::WriteU32(result, optionalOffset + 8, dataSize); Io::WriteU32(result, optionalOffset + 56, endRva); diff --git a/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp index b1f71e3f3d..71b149d37f 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp @@ -1,10 +1,16 @@ #include +#include #include +#include +#include +#include #include +#include namespace Elfpatcher::Windows { -PeRelocations WindowsRelocationBuilder::Apply(WindowsLoadImage& image, const Domain::SysVDynamicSection& dynamicSection) const { +PeRelocations WindowsRelocationBuilder::Apply(WindowsLoadImage& image, const Domain::SysVDynamicSection& dynamicSection, + const std::vector& headers) const { if (dynamicSection.DynSymData.empty() || dynamicSection.DynSymData.size() % 24 != 0) throw Domain::RelinkerException("Invalid ELF dynamic symbol table size"); PeRelocations result; @@ -18,8 +24,10 @@ PeRelocations WindowsRelocationBuilder::Apply(WindowsLoadImage& image, const Dom const auto addend = Io::ReadU64(table, offset + 16); const auto type = static_cast(info); const auto symbol = info >> 32; - if ((plt && type != 7) || (type != 1 && type != 6 && type != 7 && type != 8)) + if ((plt && type != 7) || (type != 1 && type != 6 && type != 7 && type != 8 && type != 16 && type != 17 && type != 18)) throw Domain::RelinkerException("Unsupported Windows relocation type " + std::to_string(type), target); + 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)) @@ -32,19 +40,40 @@ PeRelocations WindowsRelocationBuilder::Apply(WindowsLoadImage& image, const Dom result.BaseRelocations.push_back(rva); continue; } + if (type == 16 || type == 17 || type == 18) { + if (symbol != 0) + throw Domain::RelinkerException("Imported TLS relocation " + std::to_string(type) + " is not supported for this target", target); + if (type == 16) + throw Domain::RelinkerException("Executable DTPMOD64 relocation is not supported for this target", target); + const auto tls = std::find_if(headers.begin(), headers.end(), [](const auto& header) { return header.Type == 7; }); + if (tls == headers.end() || tls->MemorySize == 0) + throw Domain::RelinkerException("TLS relocation without a non-empty PT_TLS segment", target); + const auto alignment = std::max(tls->Alignment, 1); + if (!std::has_single_bit(alignment) || tls->MemorySize > 0x7fff0000u) + throw Domain::RelinkerException("Invalid or unsupported ELF TLS layout", tls->Offset); + image.RequireWritable(target, 8); + image.WritePointer(target, type == 17 ? addend : addend - Io::AlignUp64(tls->MemorySize, alignment)); + continue; + } if (symbol == 0 || symbol >= dynamicSection.DynSymData.size() / 24) throw Domain::RelinkerException("Invalid import symbol index", target); const auto symbolOffset = static_cast(symbol) * 24; if (Io::ReadU16(dynamicSection.DynSymData, symbolOffset + 6) != 0) throw Domain::RelinkerException("Defined symbols require explicit relocation support", target); const auto name = ReadString(dynamicSection.DynStrData, Io::ReadU32(dynamicSection.DynSymData, symbolOffset)); - if (name.empty()) + if (name.empty() && dynamicSection.DynSymData[symbolOffset + 4] >> 4 != STB_WEAK) throw Domain::RelinkerException("Empty import symbol name", target); if (type != 1 && addend != 0) throw Domain::RelinkerException("GLOB_DAT/JUMP_SLOT relocation has a nonzero addend", target); image.RequireWritable(target, 8); + if (name.empty()) { + image.WritePointer(target, addend); + continue; + } image.WritePointer(target, 0); result.Imports.push_back({name, rva, addend}); + result.Imports.back().Weak = (dynamicSection.DynSymData[symbolOffset + 4] >> 4) == STB_WEAK; + result.Imports.back().Function = type == 7 || (type == 6 && dynamicSection.FunctionImports.contains(target)); if (const auto module = dynamicSection.ImportModules.find(target); module != dynamicSection.ImportModules.end()) result.Imports.back().Library = module->second; } @@ -56,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 3fb5379dc3..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 @@ -23,6 +24,8 @@ struct MovedInstruction { std::uint32_t Rva; std::vector Bytes; std::optional RipDisplacement; + std::optional BranchTarget; + std::size_t BranchDisplacementOffset = 0; }; struct TlsAccess { @@ -88,15 +91,33 @@ 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 { +} + +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); + 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) { if (header.Type == 7) { @@ -108,19 +129,31 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co continue; std::uint64_t decodedEnd = 0; for (auto instruction = instructions.lower_bound(header.MappedAddress); instruction != instructions.end() && *instruction - header.MappedAddress < header.FileSize; ++instruction) { + if (scanned % 4096 == 0) report(7000 + 2000 * scanned / std::max(instructions.size(), 1)); + ++scanned; const auto offset = *instruction - header.MappedAddress; const auto* bytes = source.data() + header.Offset + offset; const auto info = decoder.DecodeInstruction(bytes, header.FileSize - offset); 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; @@ -135,8 +168,9 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co 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; const bool aluRead = supportedPrefixes && (info.RexPrefix == 0x48 || info.RexPrefix == 0x4c) && loadRegister != 4 && info.Length - position == 7 && isAluReadOpcode(bytes[position]) && (bytes[position + 1] & 0xc7) == 0x04 && bytes[position + 2] == 0x25; const bool wide = (info.RexPrefix & 8) != 0; - const auto address = supportedPrefixes && !loadValue && !aluRead && bytes[position] == 0x8b && (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 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 = 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; @@ -144,12 +178,32 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co if (length >= header.FileSize - offset) throw Domain::RelinkerException("Short guest TLS instruction ends its segment", header.Offset + offset); const auto next = decoder.DecodeInstruction(bytes + length, header.FileSize - offset - length); - if (next.FlowKind != Codegen::ControlFlowKind::Sequential || next.HasBranchTarget || next.SegmentPrefix != 0) + const bool relativeBranch = + (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); - moved.push_back({CheckedRva(rva + length), std::vector(bytes + length, bytes + length + next.Length), next.HasRipRelativeDisp ? std::optional(next.RipRelativeDispOffset) : std::nullopt}); + std::vector movedBytes(bytes + length, bytes + length + next.Length); + std::optional branchTarget; + std::size_t branchDisplacementOffset = 0; + if (relativeBranch) { + const auto target = static_cast(rva) + static_cast(length + next.Length) + next.BranchDisp; + if (target < 0 || target > std::numeric_limits::max()) + throw Domain::RelinkerException("Short guest TLS instruction is followed by a branch beyond the address space", header.Offset + offset + length); + branchTarget = static_cast(target); + 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.resize(movedBytes.size() + 4); + } else { + branchDisplacementOffset = next.Length - 4; + } + } + moved.push_back({CheckedRva(rva + length), std::move(movedBytes), next.HasRipRelativeDisp ? std::optional(next.RipRelativeDispOffset) : std::nullopt, branchTarget, branchDisplacementOffset}); length += next.Length; } - accesses.push_back({rva, header.Offset + offset, length, false, 0, addressRegister, 0, 0, *address, wide, std::move(moved)}); + 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; } if (!loadValue && !storeImmediate && !aluRead) { @@ -160,11 +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"); @@ -175,35 +233,34 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co throw Domain::RelinkerException("Invalid or unsupported ELF TLS layout", tls->Offset); // Native homebrew linkers can emit an empty PT_TLS placeholder. // It needs no Windows TLS directory unless guest code accesses TLS. - if (tls->MemorySize == 0) { - if (!accesses.empty()) - throw Domain::RelinkerException("Guest TLS access with empty PT_TLS", tls->Offset); + if (tls->MemorySize == 0 && accesses.empty()) return {}; - } const auto alignment = std::max(tls->Alignment, 16); const auto blockSize = CheckedRva((tls->MemorySize + alignment - 1) & ~(alignment - 1)); + const auto threadPointerAlignment = std::max(tls->Alignment, 1); + const auto threadPointerOffset = CheckedRva((tls->MemorySize + threadPointerAlignment - 1) & ~(threadPointerAlignment - 1)); const auto templateOffset = CheckedRva((64 + alignment - 1) & ~(alignment - 1)); constexpr std::uint32_t threadControlBlockSize = 0x30; constexpr std::int64_t stackGuardDisplacement = 0x28; for (const auto& access : accesses) { if (access.StoreImmediate || access.Displacement == 0) continue; const auto displacement = static_cast(std::bit_cast(access.Displacement)); - if (displacement != stackGuardDisplacement && (displacement > 0 || displacement < -static_cast(blockSize))) + 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 = [&] { code.Rip({0x8b, 0x0d}, indexRva); code.Emit({0x65, 0x48, 0x8b, 0x04, 0x25, 0x58, 0, 0, 0, 0x48, 0x8b, 0x04, 0xc8, 0x48, 0x8d, 0x80}); - code.U32(blockSize); + code.U32(threadPointerOffset); }; code.Emit({0x83, 0xfa, 1}); @@ -217,33 +274,97 @@ 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) { - const auto target = branchTargets.upper_bound(access.Rva); + if (patched % 256 == 0) report(9000 + 900 * patched / accesses.size()); + ++patched; + 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()); code.Emit({0x48, 0x8d, 0x64, 0x24, 0x80}); if (!access.Address.empty()) { - code.Emit({0x51}); - code.Emit({0x50}); - for (const auto byte : access.Address) - code.Emit({byte}); - code.Emit({0x50}); - loadPointer(); - code.Emit({0x59}); - const auto rex = static_cast((access.Wide ? 0x48 : 0x40) | ((access.Register >> 3) << 2)); - if (rex != 0x40) - code.Emit({rex}); - code.Emit({0x8b, static_cast(0x04 | ((access.Register & 7) << 3)), 0x08}); - if (access.Register == 0) { - code.Emit({0x48, 0x8d, 0x64, 0x24, 0x08}); + if (access.AluOpcode != 0 && access.Register == 0) { + code.Emit({0x51}); + code.Emit({0x52}); + code.Emit({0x50}); + for (const auto byte : access.Address) + code.Emit({byte}); + code.Emit({0x50}); + loadPointer(); code.Emit({0x59}); - } else if (access.Register == 1) { + const auto rex = static_cast((access.Wide ? 0x48 : 0x40) | 0x00); + if (rex != 0x40) + code.Emit({rex}); + code.Emit({0x8b, 0x14, 0x08}); + code.Emit({0x58}); + if (access.Wide) + code.Emit({0x48, access.AluOpcode, 0xc2}); + else + code.Emit({access.AluOpcode, 0xc2}); + code.Emit({0x5a}); + code.Emit({0x59}); + } else if (access.AluOpcode != 0 && access.Register == 1) { + code.Emit({0x51}); + code.Emit({0x50}); + for (const auto byte : access.Address) + code.Emit({byte}); + code.Emit({0x50}); + loadPointer(); + code.Emit({0x59}); + const auto rex = static_cast((access.Wide ? 0x48 : 0x40) | 0x00); + if (rex != 0x40) + code.Emit({rex}); + code.Emit({0x8b, 0x04, 0x08}); + if (access.Wide) + code.Emit({0x48, 0x8b, 0x4c, 0x24, 0x08, 0x48, access.AluOpcode, 0xc8}); + else + code.Emit({0x48, 0x8b, 0x4c, 0x24, 0x08, access.AluOpcode, 0xc8}); code.Emit({0x58}); code.Emit({0x48, 0x8d, 0x64, 0x24, 0x08}); + } else if (access.AluOpcode != 0) { + code.Emit({0x51}); + code.Emit({0x50}); + for (const auto byte : access.Address) + code.Emit({byte}); + code.Emit({0x50}); + loadPointer(); + code.Emit({0x59}); + const auto rexLoad = static_cast((access.Wide ? 0x48 : 0x40) | 0x00); + if (rexLoad != 0x40) + code.Emit({rexLoad}); + code.Emit({0x8b, 0x04, 0x08}); + const auto rexAlu = static_cast((access.Wide ? 0x48 : 0x40) | ((access.Register >> 3) << 2)); + if (rexAlu != 0x40) + code.Emit({rexAlu}); + code.Emit({access.AluOpcode, static_cast(0xc0 | ((access.Register & 7) << 3))}); + code.Emit({0x58}); + code.Emit({0x59}); } else { - code.Emit({0x58}); + code.Emit({0x51}); + code.Emit({0x50}); + for (const auto byte : access.Address) + code.Emit({byte}); + code.Emit({0x50}); + loadPointer(); code.Emit({0x59}); + const auto rex = static_cast((access.Wide ? 0x48 : 0x40) | ((access.Register >> 3) << 2)); + if (rex != 0x40) + code.Emit({rex}); + code.Emit({0x8b, static_cast(0x04 | ((access.Register & 7) << 3)), 0x08}); + if (access.Register == 0) { + code.Emit({0x48, 0x8d, 0x64, 0x24, 0x08}); + code.Emit({0x59}); + } else if (access.Register == 1) { + code.Emit({0x58}); + code.Emit({0x48, 0x8d, 0x64, 0x24, 0x08}); + } else { + code.Emit({0x58}); + code.Emit({0x59}); + } } } else if (access.AluOpcode != 0 && access.Register == 0) { code.Emit({0x51}); @@ -299,16 +420,23 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co throw Domain::RelinkerException("Moved RIP-relative instruction exceeds rel32 range from its TLS stub", access.FileOffset); Io::WriteU32(bytes, *instruction.RipDisplacement, static_cast(static_cast(displacement))); } + if (instruction.BranchTarget) { + const auto displacement = static_cast(*instruction.BranchTarget) - (static_cast(code.GetRva()) + static_cast(bytes.size())); + if (displacement < std::numeric_limits::min() || displacement > std::numeric_limits::max()) + throw Domain::RelinkerException("Moved branch exceeds rel32 range from its TLS stub", access.FileOffset); + Io::WriteU32(bytes, instruction.BranchDisplacementOffset, static_cast(static_cast(displacement))); + } for (const auto byte : bytes) code.Emit({byte}); } code.Rip({0xe9}, CheckedRva(access.Rva + access.Length)); } + report(9900); const auto templateBytes = WindowsTlsTemplateBuilder().Build(*tls, image, sections, templateRva, relocations); std::copy(templateBytes.begin(), templateBytes.end(), data.Data.begin() + templateOffset); const auto writeAddress = [&](const std::size_t offset, const std::uint32_t rva) { - Io::WriteU64(data.Data, offset, ImageBase + rva); + Io::WriteU64(data.Data, offset, image.GetImageBase() + rva); relocations.push_back(CheckedRva(data.Rva + offset)); }; diff --git a/core/relinker/elfpatcher/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/Amd64OnlyWindowsTests.cpp b/core/relinker/elfpatcher/tests/Amd64OnlyWindowsTests.cpp index 749aa19231..c726b29582 100644 --- a/core/relinker/elfpatcher/tests/Amd64OnlyWindowsTests.cpp +++ b/core/relinker/elfpatcher/tests/Amd64OnlyWindowsTests.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -59,6 +60,7 @@ struct alignas(16) State { std::uint64_t RspBefore; std::uint64_t RspAfter; std::uint8_t RedZone[128]; + std::uint8_t Ymm[16][32]; }; constexpr std::uint64_t kCanary = 0xA5C3E17B9D24F608ull; @@ -94,6 +96,11 @@ void movdquXmmRsp(Bytes& code, const std::uint8_t reg, const bool store, const s emitU32(code, offset); } +void vmovdquYmmRbx(Bytes& code, const std::uint8_t reg, const bool store) { + emit(code, {0xC4, static_cast(reg >= 8 ? 0x61 : 0xE1), 0x7E, static_cast(store ? 0x7F : 0x6F), static_cast(0x83 | ((reg & 7) << 3))}); + emitU32(code, static_cast(offsetof(State, Ymm)) + reg * 32); +} + class Harness { public: Harness() : _memory(VirtualAlloc(nullptr, kSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)) { @@ -104,7 +111,7 @@ public: VirtualFree(_memory, 0, MEM_RELEASE); } - void Run(const Bytes& body, const std::size_t returnBranchOffset, State& state) const { + void Run(const Bytes& body, const std::size_t returnBranchOffset, State& state, const bool wide = false) const { Bytes code; emit(code, {0x53, 0x55, 0x56, 0x57, 0x41, 0x54, 0x41, 0x55, 0x41, 0x56, 0x41, 0x57, 0x48, 0x81, 0xEC, 0xA8, 0x00, 0x00, 0x00}); for (std::uint8_t reg = 6; reg < 16; ++reg) movdquXmmRsp(code, reg, true, (reg - 6) * 16); @@ -115,7 +122,12 @@ public: for (int slot = 0; slot < 16; ++slot) emit(code, {0x48, 0x89, 0x44, 0x24, static_cast(-128 + slot * 8)}); emit(code, {0x48, 0x89, 0xA3}); emitU32(code, kRspBeforeOffset); - for (std::uint8_t reg = 0; reg < 16; ++reg) movdquXmmRbx(code, reg, false); + for (std::uint8_t reg = 0; reg < 16; ++reg) { + if (wide) + vmovdquYmmRbx(code, reg, false); + else + movdquXmmRbx(code, reg, false); + } code.push_back(0xE9); const auto jumpOffset = code.size(); emitU32(code, 0); @@ -129,6 +141,8 @@ public: emit(code, {0x9C, 0x8F, 0x83}); emitU32(code, kFlagsOutOffset); for (std::uint8_t reg = 0; reg < 16; ++reg) movdquXmmRbx(code, reg, true); + if (wide) + for (std::uint8_t reg = 0; reg < 16; ++reg) vmovdquYmmRbx(code, reg, true); for (std::uint8_t reg = 6; reg < 16; ++reg) movdquXmmRsp(code, reg, false, (reg - 6) * 16); emit(code, {0x48, 0x81, 0xC4, 0xA8, 0x00, 0x00, 0x00, 0x41, 0x5F, 0x41, 0x5E, 0x41, 0x5D, 0x41, 0x5C, 0x5F, 0x5E, 0x5D, 0x5B, 0xC3}); code.resize(Io::AlignUp(code.size(), std::size_t{16}), 0xCC); @@ -219,6 +233,69 @@ void requireEnvironment(const State& input, const State& state, const std::uint1 } } +void reciprocalExecution() { + const Harness harness; + const auto matcher = Codegen::MakeAmd64OnlyInstructionMatcher(); + const bool wide = __builtin_cpu_supports("avx"); + const float inputs[][4] = { + {1.0f, 4.0f, 0.25f, 2.0f}, + {0.0f, -0.0f, INFINITY, -1.0f}, + {1.00000012f, 0.99999994f, 3.0e-38f, 1.0e30f}, + {NAN, -INFINITY, -4.0f, 16.0f}}; + for (const bool vex : {false, true}) { + if (vex && !wide) + continue; + for (const std::uint8_t opcode : {std::uint8_t{0x52}, std::uint8_t{0x53}}) { + for (const std::uint8_t destination : {std::uint8_t{0}, std::uint8_t{2}, std::uint8_t{9}, std::uint8_t{15}}) { + for (const std::uint8_t source : {destination, std::uint8_t{0}, std::uint8_t{5}, std::uint8_t{14}}) { + Bytes site; + if (vex) { + emit(site, {0xC4, static_cast((destination >= 8 ? 0 : 0x80) | 0x40 | (source >= 8 ? 0 : 0x20) | 1), 0x78}); + } else { + const auto rex = static_cast(0x40 | (destination >= 8 ? 4 : 0) | (source >= 8 ? 1 : 0)); + if (rex != 0x40) + site.push_back(rex); + site.push_back(0x0F); + } + emit(site, {opcode, static_cast(0xC0 | ((destination & 7) << 3) | (source & 7))}); + const auto match = matcher->Match(site.data(), site.size()); + require(match && match->Lowering == Codegen::Amd64OnlyLowering::Trampoline, "Packed reciprocal was not lowered"); + for (const auto& lanes : inputs) { + State input{}; + for (std::size_t reg = 0; reg < 16; ++reg) { + for (std::size_t byte = 0; byte < 16; ++byte) { + input.Xmm[reg][byte] = static_cast(reg * 16 + byte); + input.Ymm[reg][byte + 16] = static_cast(reg * 16 + byte + 1); + } + } + std::memcpy(input.Xmm[source], lanes, sizeof(lanes)); + for (std::size_t reg = 0; reg < 16; ++reg) + std::memcpy(input.Ymm[reg], input.Xmm[reg], 16); + input.FlagsIn = 0xAC7; + State state = input; + harness.Run(match->StubBody, match->ReturnBranchOffset, state, wide); + ++g_executions; + float actual[4]; + std::memcpy(actual, state.Xmm[destination], sizeof(actual)); + for (std::size_t lane = 0; lane < 4; ++lane) { + const float expected = opcode == 0x52 ? 1.0f / std::sqrt(lanes[lane]) : 1.0f / lanes[lane]; + require(std::isnan(expected) ? std::isnan(actual[lane]) : std::memcmp(&expected, &actual[lane], sizeof(float)) == 0, "Packed reciprocal result differs from division and square root"); + } + requireEnvironment(input, state, static_cast(1u << destination), "packed reciprocal"); + if (wide) { + const std::uint8_t zero[16] = {}; + for (std::size_t reg = 0; reg < 16; ++reg) { + const auto* expected = vex && reg == destination ? zero : input.Ymm[reg] + 16; + require(std::memcmp(state.Ymm[reg] + 16, expected, 16) == 0, "Packed reciprocal changed the wrong upper YMM bits"); + } + } + } + } + } + } + } +} + void executeCase(const Harness& harness, const Codegen::IAmd64OnlyInstructionMatcher& matcher, const Case& item, std::mt19937_64& random, const int images) { const auto site = encode(item); const auto match = matcher.Match(site.data(), site.size()); @@ -557,7 +634,7 @@ void cpuExecution() { for (std::uint8_t src = 0; src < 16; ++src) executeSha256(harness, *matcher, {{opcode, dst, src}}, random, 2); for (int sequence = 0; sequence < 256; ++sequence) { - std::vector steps(2 + random() % 2); + std::vector steps(2 + random() % 2, Sha256Step{}); for (auto& step : steps) step = {static_cast(0xCB + random() % 3), static_cast(random() % 16), static_cast(random() % 16)}; executeSha256(harness, *matcher, steps, random, 2); } @@ -616,6 +693,7 @@ void peBuilder() { std::array directories{}; const auto file = WindowsPeWriter().Write(sections, LoadRva, directories); require(file.size() > 0x400, "PE writer rejected the stub section"); + require(read(file, 0x96) == 0x22, "PE writer marked a fixed image as relocation-stripped"); auto altered = converted.Bytes; altered[0x205] = 0xDC; requireFailure([&] { @@ -642,6 +720,7 @@ void peBuilder() { int main() { try { cpuExecution(); + reciprocalExecution(); peBuilder(); std::cout << "AMD64-only Windows tests passed (" << g_executions << " lowered sequences executed)\n"; } catch (const std::exception& error) { diff --git a/core/relinker/elfpatcher/tests/EntryStubTests.cpp b/core/relinker/elfpatcher/tests/EntryStubTests.cpp new file mode 100644 index 0000000000..5b3b8a78b1 --- /dev/null +++ b/core/relinker/elfpatcher/tests/EntryStubTests.cpp @@ -0,0 +1,134 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void require(const bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +bool bytesAt(const std::vector& stub, std::size_t& offset, const std::vector& expected) { + if (offset + expected.size() > stub.size() || std::memcmp(stub.data() + offset, expected.data(), expected.size()) != 0) return false; + offset += expected.size(); + return true; +} + +void stubTerminatesTheFrameChain() { + constexpr std::uint64_t stubVaddr = 0xb32db98; + constexpr std::uint64_t entryVaddr = 0x80; + const auto stub = Elfpatcher::EntryStubBuilder().BuildEntryStub(stubVaddr, entryVaddr); + std::size_t offset = 0; + require(bytesAt(stub, offset, {0x48, 0x89, 0xe7}), "the stub does not pass the initial stack pointer in rdi"); + require(bytesAt(stub, offset, {0x48, 0x83, 0xe4, 0xf0}), "the stub does not align the stack to 16 bytes"); + require(bytesAt(stub, offset, {0x6a, 0x00, 0x6a, 0x00}), "the stub does not push a zero frame record"); + require(bytesAt(stub, offset, {0x48, 0x89, 0xe5}), "the stub does not point rbp at the zero frame record"); + require(bytesAt(stub, offset, {0x48, 0x31, 0xf6}), "the stub does not clear rsi"); + require(offset < stub.size() && stub[offset] == 0xe8, "the stub does not call the entry point"); + std::int32_t rel32 = 0; + std::memcpy(&rel32, stub.data() + offset + 1, sizeof(rel32)); + offset += 5; + require(stubVaddr + offset + static_cast(rel32) == entryVaddr, "the stub calls the wrong address"); + 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"); +} + +} + +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(); + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } + return 0; +} diff --git a/core/relinker/elfpatcher/tests/GuestModuleArgsHost.cpp b/core/relinker/elfpatcher/tests/GuestModuleArgsHost.cpp index 708d39dc7b..b99a198578 100644 --- a/core/relinker/elfpatcher/tests/GuestModuleArgsHost.cpp +++ b/core/relinker/elfpatcher/tests/GuestModuleArgsHost.cpp @@ -45,6 +45,13 @@ extern "C" void SetPendingArgs(std::size_t args, const void* argp) { pending.argp = argp; } +extern "C" void APS5_VABI RecordFini(std::size_t args, const void* argp, const void* callback) { + char line[112]; + std::snprintf(line, sizeof(line), "fini=%016zx argp=%016zx callback=%016zx\n", args, + reinterpret_cast(argp), reinterpret_cast(callback)); + Write(line); +} + extern "C" const void* __aps5_get_pending_module_args_nid_no_patch() { return &pending; } diff --git a/core/relinker/elfpatcher/tests/LinuxDesktopEntryTests.cpp b/core/relinker/elfpatcher/tests/LinuxDesktopEntryTests.cpp new file mode 100644 index 0000000000..0c473271cd --- /dev/null +++ b/core/relinker/elfpatcher/tests/LinuxDesktopEntryTests.cpp @@ -0,0 +1,147 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +const std::vector Png{ + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4, 0x89, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x44, 0x41, + 0x54, 0x78, 0x9c, 0x63, 0xf8, 0xcf, 0xc0, 0xf0, 0x1f, 0x00, 0x05, 0x00, 0x01, 0xff, 0x89, 0x99, 0x3d, 0x1d, 0x00, 0x00, + 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82}; + +void require(const bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +void writeBytes(const std::filesystem::path& path, const std::vector& bytes) { + std::filesystem::create_directories(path.parent_path()); + std::ofstream stream(path, std::ios::binary); + stream.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); +} + +void writeText(const std::filesystem::path& path, const std::string& text) { + writeBytes(path, std::vector(text.begin(), text.end())); +} + +std::vector readBytes(const std::filesystem::path& path) { + std::ifstream stream(path, std::ios::binary); + return {std::istreambuf_iterator(stream), std::istreambuf_iterator()}; +} + +std::string param(const std::string& title) { + return R"({"titleId": "PPSA00000", "defaultLanguage": "en-US", "localizedParameters": {"en-US": {"titleName": ")" + title + R"("}}})"; +} + +std::map readEntry(const std::filesystem::path& path) { + std::ifstream stream(path); + std::string line; + std::getline(stream, line); + require(line == "[Desktop Entry]", "Missing [Desktop Entry] group in " + path.string()); + std::map fields; + while (std::getline(stream, line)) { + const auto separator = line.find('='); + require(separator != std::string::npos, "Malformed desktop entry line: " + line); + fields[line.substr(0, separator)] = line.substr(separator + 1); + } + return fields; +} + +void requireField(const std::map& fields, const std::string& key, const std::string& expected) { + const auto found = fields.find(key); + require(found != fields.end() && found->second == expected, key + " is '" + (found == fields.end() ? "" : found->second) + "', expected '" + expected + "'"); +} + +std::filesystem::path write(const std::filesystem::path& sceSys, const std::filesystem::path& executable) { + const Elfpatcher::Linux::LinuxDesktopEntryWriter writer; + const auto entry = writer.Prepare(sceSys, executable); + return entry ? writer.Write(*entry) : std::filesystem::path{}; +} + +void requireFailure(const std::filesystem::path& sceSys, const std::filesystem::path& executable, const std::string& reason) { + try { + Elfpatcher::Linux::LinuxDesktopEntryWriter().Prepare(sceSys, executable); + } catch (const Domain::RelinkerException& e) { + require(std::string(e.what()).find(reason) != std::string::npos, "Failure does not name " + reason + ": " + e.what()); + require(!std::filesystem::exists(executable.string() + ".desktop") && !std::filesystem::exists(executable.string() + ".png"), "Files written despite the failure"); + return; + } + throw std::runtime_error("Prepare succeeded despite " + reason); +} + +void run(const std::filesystem::path& root) { + require(write(root / "none" / "sce_sys", root / "none" / "out" / "game").empty(), "Desktop entry written without icon0.png"); + require(!std::filesystem::exists(root / "none" / "out"), "Files written without icon0.png"); + + const auto titledSceSys = root / "titled" / "game" / "sce_sys"; + const auto titled = root / "titled" / "out dir" / "game"; + writeBytes(titledSceSys / "icon0.png", Png); + writeText(titledSceSys / "param.json", param("Some™ Game®:\\n Edition© ")); + std::filesystem::create_directories(titled.parent_path()); + require(write(titledSceSys, titled) == titled.string() + ".desktop", "Unexpected desktop entry path"); + const auto fields = readEntry(titled.string() + ".desktop"); + requireField(fields, "Type", "Application"); + requireField(fields, "Name", "Some Game: Edition"); + requireField(fields, "Exec", "\"" + titled.string() + "\""); + requireField(fields, "Path", titled.parent_path().string()); + requireField(fields, "Icon", titled.string() + ".png"); + requireField(fields, "Terminal", "false"); + require(readBytes(titled.string() + ".png") == Png, "Icon bytes changed"); + const auto permissions = std::filesystem::status(titled.string() + ".desktop").permissions(); + require((permissions & std::filesystem::perms::owner_exec) != std::filesystem::perms::none, "Desktop entry is not executable"); + + const auto untitledSceSys = root / "untitled" / "game" / "sce_sys"; + const auto untitled = root / "untitled" / "out" / "100% $HOME"; + writeBytes(untitledSceSys / "icon0.png", Png); + std::filesystem::create_directories(untitled.parent_path()); + write(untitledSceSys, untitled); + const auto untitledFields = readEntry(untitled.string() + ".desktop"); + requireField(untitledFields, "Name", "100% $HOME"); + requireField(untitledFields, "Exec", "\"" + (root / "untitled" / "out").string() + "/100%% \\\\$HOME\""); + + const auto badPngSceSys = root / "badpng" / "game" / "sce_sys"; + writeText(badPngSceSys / "icon0.png", "GIF89a" + std::string(40, '\0')); + std::filesystem::create_directories(root / "badpng" / "out"); + requireFailure(badPngSceSys, root / "badpng" / "out" / "game", "icon0.png"); + + const auto badParamSceSys = root / "badparam" / "game" / "sce_sys"; + writeBytes(badParamSceSys / "icon0.png", Png); + writeText(badParamSceSys / "param.json", "{"); + std::filesystem::create_directories(root / "badparam" / "out"); + requireFailure(badParamSceSys, root / "badparam" / "out" / "game", "param.json"); + + const auto controlSceSys = root / "control" / "game" / "sce_sys"; + writeBytes(controlSceSys / "icon0.png", Png); + for (const char control : {'\n', '\t', '\r', '\x7f'}) { + const auto directory = root / "control" / (std::string("out") + control + "dir"); + std::filesystem::create_directories(directory); + requireFailure(controlSceSys, directory / "game", "control character"); + } +} + +} + +int main() { + 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); + } catch (const std::exception& e) { + std::cerr << e.what() << '\n'; + std::filesystem::remove_all(root); + return 1; + } + std::filesystem::remove_all(root); + return 0; +} diff --git a/core/relinker/elfpatcher/tests/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 new file mode 100644 index 0000000000..18748830de --- /dev/null +++ b/core/relinker/elfpatcher/tests/ProgramLayoutRangeTests.cpp @@ -0,0 +1,154 @@ +#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, 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 { + 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, "overflowing load end accepted"); + load.MappedAddress = 0x1000; load.MemorySize = 0x1000; + 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) == 7, "header count for a valid request"); + for (int kind = 0; kind < 11; ++kind) { + auto request = valid; + switch (kind) { + case 0: request.PhEntSize = 55; break; + case 1: request.PhOff = maximum - 10; break; + case 2: request.PhOff = 0x10F0; break; + case 3: request.ExtraBlockVaddr = maximum - 10; break; + case 4: request.DynamicSegmentSize = maximum; break; + case 5: request.InterpSize = maximum; break; + case 6: request.DynamicSegmentOffset = maximum; break; + case 7: request.InterpOffset = maximum; break; + case 8: request.ExtraBlockOffset = maximum; break; + case 9: request.ExtraBlockSize = 0x1000; 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, "invalid request accepted or output changed"); + } + 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 new file mode 100644 index 0000000000..c75c4e9773 --- /dev/null +++ b/core/relinker/elfpatcher/tests/SectionRangeTests.cpp @@ -0,0 +1,54 @@ +#include +#include +#include +#include +#include +#include + +static void Require(bool ok) { if (!ok) throw std::runtime_error("Section range validation failed"); } + +static std::vector Fixture() { + std::vector bytes(512, 0); + Io::WriteU64(bytes, 32, 64); Io::WriteU16(bytes, 54, 56); Io::WriteU16(bytes, 56, 1); + Io::WriteU32(bytes, 64, 1); Io::WriteU64(bytes, 72, 128); + Io::WriteU64(bytes, 80, 0x1000); Io::WriteU64(bytes, 96, 256); + return bytes; +} + +int main() { + try { + Elfpatcher::SectionHeaderTableBuilder builder(std::make_shared()); + const Elfpatcher::SectionHeaderTableRequest valid{128, 8, 136, 24, 160, 16, 176, 16}; + auto bytes = Fixture(); builder.WriteTable(bytes, valid); + const auto table = Io::ReadU64(bytes, 40); + Require(Io::ReadU64(bytes, table + 64 + 8) == 0); + Require(Io::ReadU64(bytes, table + 64 + 16) == 0); + Require(Io::ReadU64(bytes, table + 64 + 24) == 512); + Require(Io::ReadU64(bytes, table + 64 + 32) != 0); + Require(Io::ReadU64(bytes, table + 2 * 64 + 16) == 0x1000); + auto empty = valid; empty.StubOffset = 384; empty.StubSize = 0; + bytes = Fixture(); builder.WriteTable(bytes, empty); + Require(Io::ReadU64(bytes, Io::ReadU64(bytes, 40) + 5 * 64 + 16) == 0x1100); + for (int kind = 0; kind < 11; ++kind) { + bytes = Fixture(); auto request = valid; + switch (kind) { + case 0: request.DynStrSize = 257; break; + case 1: request.DynSymSize = std::numeric_limits::max(); break; + case 2: request.DynamicSegmentOffset = 383; request.DynamicSegmentSize = 2; break; + case 3: request.StubOffset = 384; request.StubSize = 1; break; + case 4: Io::WriteU16(bytes, 54, 1); break; + 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::runtime_error&) { rejected = true; } + Require(rejected && bytes == before); + } + 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 9866f1f075..fa6c1d4085 100644 --- a/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp +++ b/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp @@ -5,9 +5,13 @@ #include #include #include +#include +#include +#include #include #include #include +#include #include namespace { @@ -15,6 +19,18 @@ namespace { namespace Fs = std::filesystem; using namespace Elfpatcher::Windows; +Fs::path createFixtureDirectory(const Fs::path& parent) { + std::random_device random; + for (int attempt = 0; attempt < 100; ++attempt) { + auto name = Fs::path(u8"windows-diagnostic-fixtures-ü日-"); + 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 diagnostic fixture directory"); +} + void checkRuntimeDependencies() { const auto check = [](const std::vector& input, const std::vector& expected) { Domain::SysVDynamicSection dynamic; @@ -31,17 +47,62 @@ 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", "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) { - std::ofstream stream(path, std::ios::binary); + const auto fail = [&](const char* phase) { + const auto error = errno; + const auto nativeError = _doserrno; + throw std::runtime_error("Cannot write diagnostic fixture: phase=" + std::string(phase) + + " path=" + path.string() + " errno=" + std::to_string(error) + + " native_error=" + std::to_string(nativeError) + + " bytes=" + std::to_string(bytes.size())); + }; + 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()))) - throw std::runtime_error("Cannot write diagnostic fixture"); + fail("write"); + stream.close(); + if (!stream) + fail("close"); } void createImage(const Fs::path& path, const std::vector>& imports, const std::string& exported, const std::string& forwarded = {}) { @@ -140,18 +201,18 @@ void expectDiagnostic(const Fs::path& runner, const std::vector& ex HANDLE output = nullptr; if (!CreatePipe(&input, &output, &security, 0) || !SetHandleInformation(input, HANDLE_FLAG_INHERIT, 0)) throw std::runtime_error("Cannot create diagnostic test pipe"); - STARTUPINFOA startup{}; + STARTUPINFOW startup{}; startup.cb = sizeof(startup); startup.dwFlags = STARTF_USESTDHANDLES; startup.hStdOutput = output; startup.hStdError = output; startup.hStdInput = GetStdHandle(STD_INPUT_HANDLE); PROCESS_INFORMATION process{}; - const auto filename = runner.string(); - if (!CreateProcessA(filename.c_str(), nullptr, nullptr, nullptr, TRUE, CREATE_NO_WINDOW, nullptr, runner.parent_path().string().c_str(), &startup, &process)) + const auto filename = runner.wstring(); + 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"); } @@ -183,8 +244,7 @@ int main() { const auto size = GetModuleFileNameA(nullptr, filename.data(), static_cast(filename.size())); if (size == 0 || size >= filename.size()) throw std::runtime_error("Cannot locate diagnostic tests"); - const auto directory = Fs::path(filename.data()).parent_path() / "windows-diagnostic-fixtures"; - Fs::create_directories(directory); + const auto directory = createFixtureDirectory(Fs::path(filename.data()).parent_path()); const auto root = directory / "diagnostic-root.prx"; const auto middle = directory / "diagnostic-middle.dll"; const auto leaf = directory / "diagnostic-leaf.dll"; @@ -211,12 +271,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())) @@ -224,7 +287,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(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 f91bf45f6d..a3813723aa 100644 --- a/core/relinker/elfpatcher/tests/WindowsIconResourceTests.cpp +++ b/core/relinker/elfpatcher/tests/WindowsIconResourceTests.cpp @@ -4,9 +4,7 @@ #include #include #include -#include #include -#include #include #include #include @@ -29,31 +27,85 @@ std::uint32_t readU32(const std::vector& data, const std::size_t o static_cast(data[offset + 2]) << 16 | static_cast(data[offset + 3]) << 24; } -void writeBytes(const std::filesystem::path& path, const std::vector& bytes) { - std::filesystem::create_directories(path.parent_path()); - std::ofstream stream(path, std::ios::binary); - stream.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); - require(static_cast(stream), "Cannot write " + path.string()); +std::vector chunk(const std::string& type, const std::vector& payload) { + std::vector result; + const auto append = [&result](std::uint32_t value) { + for (const auto shift : {24, 16, 8, 0}) result.push_back(static_cast(value >> shift)); + }; + append(static_cast(payload.size())); + result.insert(result.end(), type.begin(), type.end()); + result.insert(result.end(), payload.begin(), payload.end()); + std::uint32_t crc = 0xffffffffu; + for (std::size_t index = 4; index < result.size(); ++index) { + crc ^= result[index]; + for (int bit = 0; bit < 8; ++bit) crc = (crc >> 1) ^ ((crc & 1) ? 0xedb88320u : 0u); + } + append(crc ^ 0xffffffffu); + return result; } -void requireEmpty(const Elfpatcher::Windows::WindowsIconResourceBuilder& builder, const std::filesystem::path& path, const std::string& name) { - std::vector sections; - const auto directory = builder.Build(path, sections, 0x10000); - require(sections.empty(), "Section added for " + name); - require(directory.Rva == 0 && directory.Size == 0, "Resource directory set for " + name); +void checkChunks() { + const Elfpatcher::Windows::WindowsIconResourceBuilder builder; + const auto check = [&](const std::string& name, const std::vector>& chunks, bool valid, const std::vector& header = {}) { + std::vector image(Png.begin(), Png.begin() + (header.empty() ? 33 : 8)); + if (!header.empty()) { + const auto ihdr = chunk("IHDR", header); + image.insert(image.end(), ihdr.begin(), ihdr.end()); + } + for (const auto& value : chunks) image.insert(image.end(), value.begin(), value.end()); + image.insert(image.end(), Png.end() - 12, Png.end()); + const std::filesystem::path path = std::filesystem::path(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(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); + }; + const std::vector data(Png.begin() + 41, Png.begin() + 54); + const auto idat = chunk("IDAT", data); + const auto note = chunk("tEXt", {'n', 0, 'v'}); + check("unknown-critical", {chunk("ABCD", {}), idat}, false); + check("ancillary", {note, idat}, true); + 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); + check("nonconsecutive", {first, note, last}, false); + const auto palette = chunk("PLTE", {255, 0, 0}); + check("late-palette", {idat, palette}, false); + check("duplicate-palette", {palette, palette, idat}, false); + check("palette", {palette, idat}, true); + check("empty-palette", {chunk("PLTE", {}), idat}, false); + check("partial-palette", {chunk("PLTE", {0, 1, 2, 3}), idat}, false); + check("large-palette", {chunk("PLTE", std::vector(771)), idat}, false); + std::vector header(Png.begin() + 16, Png.begin() + 29); + header[8] = 1; + header[9] = 3; + const auto indexed = chunk("IDAT", {0x78, 0x9c, 0x63, 0x60, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01}); + check("indexed", {palette, indexed}, true, header); + check("indexed-missing-palette", {indexed}, false, header); + check("indexed-large-palette", {chunk("PLTE", std::vector(9)), indexed}, false, header); + header[9] = 0; + check("grayscale-palette", {palette, indexed}, false, header); } -void run(const std::filesystem::path& root) { +void run() { const Elfpatcher::Windows::WindowsIconResourceBuilder builder; - requireEmpty(builder, {}, "an empty path"); - requireEmpty(builder, root / "missing" / "icon0.png", "a missing icon"); + checkChunks(); - const auto iconPath = root / "valid" / "icon0.png"; - writeBytes(iconPath, Png); + const std::filesystem::path iconPath = "valid/icon0.png"; const std::uint32_t nextRva = 0x50000; std::vector sections; - const auto directory = builder.Build(iconPath, sections, nextRva); + const auto directory = builder.Build(Png, iconPath, sections, nextRva); require(sections.size() == 1, "Expected one section"); require(sections[0].Name == ".rsrc", "Unexpected section name: " + sections[0].Name); require(sections[0].Rva == nextRva, "Unexpected section RVA"); @@ -68,33 +120,69 @@ void run(const std::filesystem::path& root) { require(readU32(data, 174) == Png.size(), "Unexpected icon size in group entry"); require(std::equal(Png.begin(), Png.end(), data.begin() + 184), "Icon bytes changed"); - const auto badPath = root / "bad" / "icon0.png"; + const std::filesystem::path badPath(u8"\u65e5\u672c/bad/icon0.png"); + const auto badUtf8 = badPath.u8string(); auto truncated = Png; truncated.resize(40); - writeBytes(badPath, truncated); std::vector badSections; + bool caughtTruncated = false; try { - builder.Build(badPath, badSections, nextRva); + builder.Build(truncated, badPath, badSections, nextRva); } catch (const Domain::RelinkerException& e) { - require(std::string(e.what()).find(badPath.string()) != std::string::npos, std::string("Failure does not name the icon: ") + e.what()); + require(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"); } } int main() { - const auto root = std::filesystem::temp_directory_path() / ("anyps5-icon-resource-tests-" + std::to_string(std::random_device{}())); - std::filesystem::remove_all(root); try { - run(root); + run(); } catch (const std::exception& e) { std::cerr << e.what() << '\n'; - std::filesystem::remove_all(root); return 1; } - std::filesystem::remove_all(root); return 0; } diff --git a/core/relinker/elfpatcher/tests/WindowsMsvcrt.def b/core/relinker/elfpatcher/tests/WindowsMsvcrt.def new file mode 100644 index 0000000000..c163db8f44 --- /dev/null +++ b/core/relinker/elfpatcher/tests/WindowsMsvcrt.def @@ -0,0 +1,4 @@ +LIBRARY msvcrt.dll +EXPORTS + __iob_func + fputs diff --git a/core/relinker/elfpatcher/tests/WindowsMsvcrtStdioHost.cpp b/core/relinker/elfpatcher/tests/WindowsMsvcrtStdioHost.cpp new file mode 100644 index 0000000000..4c370e0924 --- /dev/null +++ b/core/relinker/elfpatcher/tests/WindowsMsvcrtStdioHost.cpp @@ -0,0 +1,22 @@ +struct _iobuf { + char* ptr; + int count; + char* base; + int flags; + int file; + int charbuf; + int bufferSize; + char* tempName; +}; + +static_assert(sizeof(_iobuf) == 48); + +extern "C" _iobuf* __cdecl __iob_func(); +extern "C" int __cdecl fputs(const char*, _iobuf*); + +extern "C" int shared() { + auto* streams = __iob_func(); + fputs("Windows DLL stdout probe\n", streams + 1); + fputs("Windows DLL stderr probe\n", streams + 2); + return 11; +} diff --git a/core/relinker/elfpatcher/tests/WindowsStdioHost.cpp b/core/relinker/elfpatcher/tests/WindowsStdioHost.cpp new file mode 100644 index 0000000000..72a29373f4 --- /dev/null +++ b/core/relinker/elfpatcher/tests/WindowsStdioHost.cpp @@ -0,0 +1,7 @@ +#include + +extern "C" int shared() { + std::fputs("Windows DLL stdout probe\n", stdout); + std::fputs("Windows DLL stderr probe\n", stderr); + return 11; +} diff --git a/core/relinker/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/elfpatcher/tests/test_windows_diagnostic_fixture_isolation.py b/core/relinker/elfpatcher/tests/test_windows_diagnostic_fixture_isolation.py new file mode 100644 index 0000000000..3d7f58285c --- /dev/null +++ b/core/relinker/elfpatcher/tests/test_windows_diagnostic_fixture_isolation.py @@ -0,0 +1,43 @@ +import concurrent.futures +import ctypes +import mmap +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile + + +def run(executable): + result = subprocess.run([str(executable)], capture_output=True, text=True, timeout=25) + assert result.returncode == 0, (result.returncode, result.stdout, result.stderr) + assert "Windows dependency machine-code tests passed" in result.stdout, result.stdout + + +def main(): + ctypes.windll.kernel32.SetErrorMode(0x0001 | 0x0002 | 0x8000) + original = Path(sys.argv[1]).resolve() + with tempfile.TemporaryDirectory(prefix="anyps5-diagnostic-isolation-") as temporary: + directory = Path(temporary) + executable = directory / original.name + shutil.copy2(original, executable) + legacy = directory / "windows-diagnostic-fixtures" + legacy.mkdir() + fixture = legacy / "diagnostic-runner.exe" + expected = b"previous diagnostic fixture" * 256 + fixture.write_bytes(expected) + with fixture.open("rb") as stream, mmap.mmap(stream.fileno(), 0, access=mmap.ACCESS_READ) as mapping: + run(executable) + with concurrent.futures.ThreadPoolExecutor(max_workers=2) as pool: + futures = [pool.submit(run, executable) for _ in range(2)] + for future in futures: + future.result() + assert mapping[:] == expected, "Mapped previous fixture changed" + assert fixture.read_bytes() == expected, "Previous fixture was overwritten" + assert {path.name for path in legacy.iterdir()} == {fixture.name}, "Previous fixture directory was reused" + assert {path.name for path in directory.iterdir()} == {executable.name, legacy.name}, "Successful diagnostic runs left fixture directories" + print("Windows diagnostic fixture isolation passed") + + +if __name__ == "__main__": + main() 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/NativePath.hpp b/core/relinker/io/include/io/NativePath.hpp new file mode 100644 index 0000000000..77ac098a13 --- /dev/null +++ b/core/relinker/io/include/io/NativePath.hpp @@ -0,0 +1,14 @@ +#ifndef IO_NATIVEPATH_HPP +#define IO_NATIVEPATH_HPP + +#include +#include + +namespace Io { + +std::filesystem::path NativePath(const std::string& path); +std::string Utf8Path(const std::filesystem::path& path); + +} + +#endif 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 65bff45a76..228903ab48 100644 --- a/core/relinker/io/src/FileReader.cpp +++ b/core/relinker/io/src/FileReader.cpp @@ -1,14 +1,22 @@ #include +#include #include +#include #include namespace Io { std::vector FileReader::Read(const std::string& path) { - std::ifstream f(path, std::ios::binary | std::ios::ate); + 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 | std::ios::ate); if (!f) throw Domain::RelinkerException("Cannot open file: " + path); const std::streamsize size = f.tellg(); + if (size < 0) + throw Domain::RelinkerException("Cannot read file: " + path); f.seekg(0); std::vector buf(static_cast(size)); if (!f.read(reinterpret_cast(buf.data()), size)) diff --git a/core/relinker/io/src/FileWriter.cpp b/core/relinker/io/src/FileWriter.cpp index a8c65fd637..49df133411 100644 --- a/core/relinker/io/src/FileWriter.cpp +++ b/core/relinker/io/src/FileWriter.cpp @@ -1,27 +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(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(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/src/NativePath.cpp b/core/relinker/io/src/NativePath.cpp new file mode 100644 index 0000000000..13b11fa505 --- /dev/null +++ b/core/relinker/io/src/NativePath.cpp @@ -0,0 +1,15 @@ +#include + +namespace Io { + +std::filesystem::path NativePath(const std::string& path) { + const std::u8string utf8(path.begin(), path.end()); + return std::filesystem::path(utf8); +} + +std::string Utf8Path(const std::filesystem::path& path) { + const auto utf8 = path.u8string(); + return std::string(utf8.begin(), utf8.end()); +} + +} 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 f90bcffcd5..33240459b0 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -1,34 +1,250 @@ +#include +#include +#include +#include #include #include #include +#include +#include +#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include -#include -#include -#include #include +#include +#include #include #include #include +#include #include #include -int main(const int argc, char* argv[]) { +#ifdef _WIN32 +#include +#include +#endif + +namespace { + +struct ConvertedExecutable { + std::string InputPath; + std::string OutputPath; + Relinker::RelinkResult Result; + std::vector GuestArtifacts; + std::vector Bytes; + std::string Diagnostics; + std::vector AppliedPatches; +}; + +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; + 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(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); + } + const auto iconPath = Io::NativePath(args.inputPath).parent_path() / "sce_sys" / "icon0.png"; + if (args.toWindows && std::filesystem::exists(iconPath)) input.Icon = fileReader.Read(Io::Utf8Path(iconPath)); + Relinker::ConversionOptions options; + options.Target = args.toWindows ? Relinker::TargetSystem::Windows : args.toMacos ? Relinker::TargetSystem::MacOs : Relinker::TargetSystem::Linux; + options.ToIntel = args.toIntel; + options.ToRosetta = args.toRosetta; + options.SkipSyscallCheck = args.skipSyscallCheck; + options.TrapSdkSyscallStubs = args.trapSdkSyscallStubs; + options.UnusedFilterLevel = args.unusedFilterLevel; + options.LazyBinding = args.lazyBinding; + options.RunPath = args.runPath; + options.WindowsGui = args.windowsGui; + options.TraceImports = args.traceImports; + options.WindowsFixedBase = mainExecutable ? args.windowsFixedBase : std::nullopt; + options.WindowsDiagnostics = args.windowsDiagnostics && mainExecutable; + options.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), std::move(converted.AppliedPatches)}; +} + +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[]) { Cli::Args args; try { args = Cli::ParseArgs(argc, argv); @@ -37,133 +253,267 @@ int main(const int argc, char* argv[]) { return 1; } + if (args.showHelp) { + std::cout << Cli::Usage() << '\n'; + return 0; + } + + Cli::ConsoleProgress progress(std::cout); try { - auto extension = std::filesystem::path(args.outputPath).extension().string(); + 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::FileReader fileReader; Io::FileWriter fileWriter; - auto sourceBytes = fileReader.Read(args.inputPath); - const std::string absPath = std::filesystem::absolute(args.outputPath).string(); + 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); + const std::string absPath = Io::Utf8Path(std::filesystem::absolute(Io::NativePath(args.outputPath))); - std::vector trampolines; - if (args.toIntel) { - const auto codeSegments = Relinker::ElfReader(sourceBytes).ReadCodeSegments(); - auto converted = Codegen::MakeAmd64OnlyConverter()->Convert(std::move(sourceBytes), codeSegments); - sourceBytes = std::move(converted.Bytes); - trampolines = std::move(converted.Trampolines); - std::map stubsByName; - for (const auto& report : converted.Reports) { - if (report.Lowering == Codegen::Amd64OnlyLowering::Kept) - std::cout << "Intel substitution: " << report.InstructionName << " at 0x" << std::hex << report.Offset << std::dec << " (" << report.OriginalLength << " bytes) kept: no room for a jump\n"; - else if (report.InstructionName == "VRSQRTPS" || report.InstructionName == "VRCPPS") - ++stubsByName[report.InstructionName]; - else - std::cout << "Intel substitution: " << report.InstructionName << " at 0x" << std::hex << report.Offset << std::dec << " (" << report.OriginalLength << " bytes) -> " << (report.Lowering == Codegen::Amd64OnlyLowering::InPlace ? "in place " : "stub ") << report.ReplacementLength << " bytes\n"; + struct ExtraExecutable { + std::string Argument; + std::filesystem::path Relative; + std::filesystem::path Input; + std::filesystem::path Output; + }; + std::vector extraExecutables; + const auto outputDirectory = Io::NativePath(absPath).parent_path(); + const auto moduleRoot = args.executables.empty() ? std::filesystem::path() : std::filesystem::absolute(Io::NativePath(args.sceModulePath)).lexically_normal(); + 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); + 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 (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)); } - for (const auto& [name, count] : stubsByName) - std::cout << "Intel substitution: " << name << " -> stub at " << count << " sites\n"; - std::cout << "Intel conversion: " << converted.ReplacedCount << " in place, " << trampolines.size() << " stubs, " << converted.KeptCount << " kept\n"; + extraExecutables.push_back({executable, relative, input, output}); } - auto elfReader = std::make_shared(sourceBytes); - const std::shared_ptr syscallScanner = args.skipSyscallCheck ? Relinker::MakeNullSyscallScanner() : Relinker::MakeSyscallScanner(); - - const auto pipeline = std::make_shared( - elfReader, - syscallScanner, - Relinker::MakeCallSiteResolver(), - std::make_shared(), - std::make_shared(), - args.unusedFilterLevel == 2 ? Relinker::MakeStrictUnusedNidFilter() : Relinker::MakeUnusedNidFilter(), - args.unusedFilterLevel - ); - - std::cout << "System: " << (args.toWindows ? "Windows" : args.toMacos ? "macOS" : "Linux") << "; unused-filter=" << args.unusedFilterLevel << "\n"; - std::cout << "sce_module/sce_modules/prx processing: " << (args.skipSceModule ? "disabled (--skip-sce-module)" : "enabled") << '\n'; - for (const auto& name : args.excludedSceModules) std::cout << "Guest module excluded: " << name << '\n'; - auto result = pipeline->Relink(sourceBytes); - for (const auto& patch : result.Patches) { - if (patch.Offset > sourceBytes.size() || patch.Bytes.size() > sourceBytes.size() - patch.Offset) - throw Domain::RelinkerException("Relinker patch exceeds source image", patch.Offset); - for (std::size_t index = 0; index < patch.Bytes.size(); ++index) sourceBytes[patch.Offset + index] = patch.Bytes[index]; + 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) { + progress.IncludeFile(Io::Utf8Path(extra.Input)); + progress.IncludeFile(Io::Utf8Path(extra.Output)); } - - std::vector guestArtifacts; - if (!args.skipSceModule) { - guestArtifacts = Relinker::GuestModuleBuilder().Build(args.inputPath, absPath, result.DynamicSection, args.toWindows, args.toMacos, args.toIntel, *syscallScanner, args.lazyBinding, args.runPath, args.excludedSceModules); + std::vector executables; + 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, 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; + }); + if (!shared) throw Domain::RelinkerException("Guest modules convert differently for " + executable + ", so it cannot share them with the input executable"); + executables.push_back(std::move(converted)); } + const auto& guestArtifacts = executables.front().GuestArtifacts; + 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)); + const Elfpatcher::Linux::LinuxDesktopEntryWriter desktopEntryWriter; + const auto desktopEntry = args.toWindows || args.toMacos ? std::nullopt : desktopEntryWriter.Prepare(Io::NativePath(args.inputPath).parent_path() / "sce_sys", Io::NativePath(absPath)); + + const auto checkOutputInputs = [&](const std::filesystem::path& path, const char* kind) { + if (!std::filesystem::exists(path)) return; + for (const auto& executable : executables) + 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 (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"); + if (desktopEntry) { + checkOutputInputs(desktopEntry->IconPath, "Icon"); + checkOutputInputs(desktopEntry->EntryPath, "Desktop entry"); + } + if (args.windowsDiagnostics) + checkOutputInputs(std::filesystem::absolute("windows-diagnostics-imports.txt"), "Windows import diagnostics"); + + std::vector registryPaths; + std::vector neededPaths; if (args.writeRegistry) { - const std::filesystem::path outFsPath(absPath); - const std::string registryPath = (outFsPath.parent_path() / (outFsPath.stem().string() + ".registry.json")).string(); - const auto registryWriter = std::make_shared(); - fileWriter.Write(registryPath, registryWriter->WriteCallRegistry(result.RegistryEntries)); - for (const auto& artifact : guestArtifacts) { - const auto modulePath = outFsPath.parent_path() / (outFsPath.stem().string() + "." + artifact.Path.filename().string() + ".registry.json"); - fileWriter.Write(modulePath.string(), registryWriter->WriteModuleImports(artifact.Imports)); + for (const auto& executable : executables) { + const std::filesystem::path outFsPath = Io::NativePath(executable.OutputPath); + registryPaths.push_back(outFsPath.parent_path() / Io::NativePath(Io::Utf8Path(outFsPath.stem()) + ".registry.json")); + } + const std::filesystem::path outFsPath = Io::NativePath(absPath); + 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)); } } - auto byteWriter = std::make_shared(); - - std::shared_ptr patcher; - if (args.toMacos) { - patcher = std::make_shared(); - } else if (args.toWindows) { - patcher = std::make_shared(args.windowsGui, std::filesystem::path(args.inputPath).parent_path() / "sce_sys" / "icon0.png"); - } else { - patcher = std::make_shared( - std::make_shared(), - std::make_shared( - std::make_shared(), - byteWriter - ), - std::make_shared(byteWriter), - byteWriter - ); - } - - std::vector executableBytes; - try { - executableBytes = patcher->Patch(sourceBytes, result.OriginalHeaders, result.DynamicSection, result.OriginalPltGotVaddr, args.runPath, args.lazyBinding, args.windowsDiagnostics, trampolines); - } catch (Domain::RelinkerException& error) { - error.InputPath = args.inputPath; - throw; - } + if (args.windowsDiagnostics) fileWriter.Write("windows-diagnostics-imports.txt", executables.front().Diagnostics); + const auto outputCount = guestArtifacts.size() + executables.size(); + progress.Report({Domain::ProgressStage::Write, absPath, 0, outputCount}); + std::size_t written = 0; for (const auto& artifact : guestArtifacts) { std::filesystem::create_directories(artifact.Path.parent_path()); - fileWriter.Write(artifact.Path.string(), artifact.Bytes); - std::cout << "Guest module: " << artifact.Path.string() << '\n'; + fileWriter.Write(Io::Utf8Path(artifact.Path), artifact.Bytes); + progress.Report({Domain::ProgressStage::Write, absPath, ++written, outputCount}); } - fileWriter.Write(absPath, executableBytes); - std::cout << "External prx references: " << result.RegistryEntries.size() << "\nOutput file: " << absPath << '\n'; - std::cout << "Expected runtime layout (relative to the output executable):\n" - << std::filesystem::path(absPath).filename().string() << "\n" - << "libs/\n *.prx\napp0/\n \n"; - for (const auto& artifact : guestArtifacts) - std::cout << " " << artifact.Path.lexically_relative(std::filesystem::path(absPath).parent_path() / "app0").generic_string() << '\n'; - std::cout << "Game resources and system libraries must be placed in this layout separately.\n"; + 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) { + fileWriter.Write(Io::Utf8Path(registryPaths[index]), registryWriter->WriteCallRegistry(executables[index].Result.RegistryEntries)); + 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' + << "External prx references: " << executables.front().Result.RegistryEntries.size() << '\n' + << "Converted modules: " << guestArtifacts.size() << "\n\n"; + std::vector extraPaths; + for (std::size_t index = 1; index < executables.size(); ++index) { + std::cout << "Executable file: " << executables[index].OutputPath << '\n'; + extraPaths.push_back(Io::Utf8Path(Io::NativePath(executables[index].OutputPath).filename())); + } + std::vector modulePaths; + for (const auto& artifact : guestArtifacts) { + std::string relative = Io::Utf8Path(artifact.Path.lexically_relative(Io::NativePath(absPath).parent_path().lexically_normal() / "app0")); + std::replace(relative.begin(), relative.end(), '\\', '/'); + modulePaths.push_back(std::move(relative)); + } + Cli::RuntimeLayout::Print(std::cout, Io::Utf8Path(Io::NativePath(absPath).filename()), modulePaths, extraPaths); if (args.runPath != "$ORIGIN/libs") std::cout << "Custom library search path (--rpath): " << args.runPath << '\n'; if (args.autorun) return Cli::Autorun(absPath, args.toWindows); } catch (const Domain::RelinkerException& e) { + progress.Finish(); std::cerr << "FAIL: " << e.what(); if (e.FailureOffset != 0) std::cerr << " (offset 0x" << std::hex << e.FailureOffset << ")"; std::cerr << "\n"; if (!e.InputPath.empty()) std::cerr << "Input: " << e.InputPath << '\n'; return 2; } catch (const Codegen::CodegenException& e) { + progress.Finish(); std::cerr << "FAIL: " << e.what(); if (e.FailureOffset != 0) std::cerr << " (offset 0x" << std::hex << e.FailureOffset << ")"; std::cerr << "\n"; return 2; } catch (const std::exception& e) { + progress.Finish(); std::cerr << "FAIL: " << e.what() << "\n"; return 2; } return 0; } + +#ifdef _WIN32 + +std::string Utf8Argument(const wchar_t* argument) { + const std::size_t argumentSize = std::wcslen(argument); + if (argumentSize == 0) return {}; + const int size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, argument, static_cast(argumentSize), nullptr, 0, nullptr, nullptr); + if (size <= 0) throw std::runtime_error("Cannot encode the command line as UTF-8"); + std::string utf8(static_cast(size), '\0'); + if (WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, argument, static_cast(argumentSize), utf8.data(), size, nullptr, nullptr) != size) + throw std::runtime_error("Cannot encode the command line as UTF-8"); + return utf8; +} + +} + +int wmain(const int argc, wchar_t* argv[]) { + try { + std::vector arguments; + arguments.reserve(static_cast(argc)); + arguments.emplace_back(); + for (int index = 1; index < argc; ++index) arguments.push_back(Utf8Argument(argv[index])); + std::vector pointers; + pointers.reserve(arguments.size() + 1); + for (auto& argument : arguments) pointers.push_back(argument.data()); + pointers.push_back(nullptr); + return Run(static_cast(arguments.size()), pointers.data()); + } catch (const std::exception& error) { + std::cerr << "FAIL: " << error.what() << "\n"; + return 1; + } +} + +#else + +} + +int main(const int argc, char* argv[]) { + return Run(argc, argv); +} + +#endif diff --git a/core/relinker/relinker.manifest b/core/relinker/relinker.manifest new file mode 100644 index 0000000000..8f006058f1 --- /dev/null +++ b/core/relinker/relinker.manifest @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + true + + + diff --git a/core/relinker/relinker.rc b/core/relinker/relinker.rc new file mode 100644 index 0000000000..a543e8bc25 --- /dev/null +++ b/core/relinker/relinker.rc @@ -0,0 +1,3 @@ +#include + +CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "relinker.manifest" diff --git a/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp b/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp index 2146ab9cd6..cdd140af64 100644 --- a/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp +++ b/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp @@ -3,12 +3,14 @@ #include #include +#include +#include namespace Relinker { class CodeInstructionCollector { public: - std::set Collect(const std::vector& bytes, const std::vector& headers) const; + std::set Collect(const std::vector& bytes, const std::vector& headers, const std::function& progress = {}, 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 new file mode 100644 index 0000000000..4e897a7a0b --- /dev/null +++ b/core/relinker/relinker/include/relinker/analysis/SceCodeExtent.hpp @@ -0,0 +1,15 @@ +#ifndef RELINKER_ANALYSIS_SCECODEEXTENT_HPP +#define RELINKER_ANALYSIS_SCECODEEXTENT_HPP + +#include +#include +#include + +namespace Relinker { + +std::optional FindSceCodeEnd(std::span segment, VirtualAddress segmentVAddr, const std::vector& functions, VirtualAddress pltGot, ByteCount pltRelocationsSize); +std::vector SceUnwindRanges(std::span segment, VirtualAddress segmentVAddr, const std::vector& functions); + +} + +#endif diff --git a/core/relinker/relinker/include/relinker/analysis/SyscallScanner.hpp b/core/relinker/relinker/include/relinker/analysis/SyscallScanner.hpp index 2e4551de7b..8b9c2d6c25 100644 --- a/core/relinker/relinker/include/relinker/analysis/SyscallScanner.hpp +++ b/core/relinker/relinker/include/relinker/analysis/SyscallScanner.hpp @@ -3,10 +3,11 @@ #include #include +#include namespace Relinker { -std::unique_ptr MakeSyscallScanner(); +std::unique_ptr MakeSyscallScanner(Domain::IProgress& progress, bool trapSdkStubs = false); std::unique_ptr MakeNullSyscallScanner(); } diff --git a/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter.hpp b/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter.hpp index 91ca7f3065..acd0fc633f 100644 --- a/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter.hpp +++ b/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter.hpp @@ -3,11 +3,12 @@ #include #include +#include namespace Relinker { std::shared_ptr MakeUnusedNidFilter(); -std::shared_ptr MakeStrictUnusedNidFilter(); +std::shared_ptr MakeStrictUnusedNidFilter(Domain::IProgress& progress); } diff --git a/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter/IControlFlowGraph.hpp b/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter/IControlFlowGraph.hpp index 421029d69b..2bd06b8cfd 100644 --- a/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter/IControlFlowGraph.hpp +++ b/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter/IControlFlowGraph.hpp @@ -3,9 +3,12 @@ #include #include +#include #include #include +#include #include +#include namespace Relinker::UnusedNidFilter { @@ -16,12 +19,33 @@ public: virtual bool IsReachable(VirtualAddress vaddr) const = 0; }; +std::unique_ptr BuildControlFlowGraph( + std::span text, + VirtualAddress textVaddr, + VirtualAddress entryVaddr, + const std::vector& extraEntries, + const IRelativeRelocationIndex& relativeRelocations, + 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 = {}, const std::function& isData = {}); + std::unique_ptr BuildControlFlowGraph( const std::vector& text, VirtualAddress textVaddr, VirtualAddress entryVaddr, const std::vector& extraEntries, - const IRelativeRelocationIndex& relativeRelocations + const IRelativeRelocationIndex& relativeRelocations, + bool followCodeAddresses = false +); + +std::unique_ptr BuildAddressTakenControlFlowGraph( + const std::vector& text, + VirtualAddress textVaddr, + VirtualAddress entryVaddr, + const std::vector& extraEntries, + const IRelativeRelocationIndex& relativeRelocations, + const std::vector& data ); } diff --git a/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter/IEntryPointCollector.hpp b/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter/IEntryPointCollector.hpp index 0241329ee2..fc9127d18f 100644 --- a/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter/IEntryPointCollector.hpp +++ b/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter/IEntryPointCollector.hpp @@ -2,6 +2,7 @@ #define RELINKER_ANALYSIS_UNUSED_NID_FILTER_IENTRYPOINTCOLLECTOR_HPP #include +#include #include #include @@ -13,7 +14,8 @@ public: virtual std::vector Collect( const std::vector& elfBytes, VirtualAddress textVaddr, - std::size_t textSize + std::size_t textSize, + const IRelativeRelocationIndex& relativeRelocations ) const = 0; }; diff --git a/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter/IRelativeRelocationIndex.hpp b/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter/IRelativeRelocationIndex.hpp index 5a3a447426..3c5023559e 100644 --- a/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter/IRelativeRelocationIndex.hpp +++ b/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter/IRelativeRelocationIndex.hpp @@ -13,6 +13,7 @@ class IRelativeRelocationIndex { public: virtual ~IRelativeRelocationIndex() = default; virtual std::optional TargetOfSlot(VirtualAddress slotVaddr) const = 0; + virtual std::vector Targets() const = 0; }; std::unique_ptr BuildRelativeRelocationIndex( diff --git a/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter/StrictReachability.hpp b/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter/StrictReachability.hpp index 5c0d663c7a..2261cfee8c 100644 --- a/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter/StrictReachability.hpp +++ b/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter/StrictReachability.hpp @@ -39,6 +39,8 @@ struct StrictReachabilityResult { StrictReachabilityResult AnalyzeStrictReachability(const StrictReachabilityInput& input); +std::vector ReadRelativeTableTargets(const std::vector& data, VirtualAddress base, VirtualAddress textVaddr, std::size_t textSize); + } #endif 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 90ce34de30..14eca6cbe1 100644 --- a/core/relinker/relinker/include/relinker/domain/IElfReader.hpp +++ b/core/relinker/relinker/include/relinker/domain/IElfReader.hpp @@ -16,9 +16,7 @@ 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::vector ReadCodeSegments() 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 3986e965e3..a4adf4a77c 100644 --- a/core/relinker/relinker/include/relinker/domain/ISyscallScanner.hpp +++ b/core/relinker/relinker/include/relinker/domain/ISyscallScanner.hpp @@ -2,7 +2,9 @@ #define RELINKER_DOMAIN_ISYSCALLSCANNER_HPP #include +#include #include +#include namespace Relinker { @@ -10,11 +12,7 @@ class ISyscallScanner { public: virtual ~ISyscallScanner() = default; - virtual void ScanCodeSectionForSyscalls( - const std::vector& codeSection, - FileByteOffset codeSectionOffset, - FileByteOffset codeSectionSize - ) = 0; + virtual std::vector ScanCodeSectionForSyscalls(const std::vector& codeSection, FileByteOffset codeSectionOffset, FileByteOffset codeSectionSize, std::string_view input = {}, const std::vector& unwindRanges = {}) = 0; }; } diff --git a/core/relinker/relinker/include/relinker/domain/IUnusedNidFilter.hpp b/core/relinker/relinker/include/relinker/domain/IUnusedNidFilter.hpp index 98bb86c9de..f2c5ea47a7 100644 --- a/core/relinker/relinker/include/relinker/domain/IUnusedNidFilter.hpp +++ b/core/relinker/relinker/include/relinker/domain/IUnusedNidFilter.hpp @@ -14,7 +14,8 @@ public: const std::vector& nidRefs, const std::vector& elfBytes, const std::vector& textSection, - VirtualAddress textVAddr + VirtualAddress textVAddr, + const std::vector& extraEntries = {} ) = 0; }; diff --git a/core/relinker/relinker/include/relinker/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 6a861e1bda..6be45157af 100644 --- a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp +++ b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp @@ -4,15 +4,20 @@ #include #include #include +#include #include +#include #include #include +#include namespace Relinker { inline constexpr char GuestSymbolSuffix[] = "#guest"; inline constexpr char GuestModuleSuffix[] = ".guest.prx"; +inline constexpr char GuestModulePattern[] = "sce_module/*.prx"; inline constexpr std::uint16_t AbsoluteSection = 0xfff1; +inline constexpr char GuestInitializeExport[] = "__aps5_guest_initialize"; struct GuestSymbol { std::string Name; @@ -22,8 +27,16 @@ struct GuestSymbol { std::uint64_t Value; std::uint64_t Size; std::string Library; + std::string QualifiedName; }; +inline bool IdenticalOrdinaryFunctionExport(const GuestSymbol& left, const GuestSymbol& right) { + return left.Section != 0 && left.Section < 0xff00 && (left.Info >> 4) != 0 && + left.Visibility != 1 && left.Visibility != 2 && (left.Info & 15) == 2 && + left.Info == right.Info && left.Visibility == right.Visibility && + left.Section == right.Section && left.Value == right.Value && left.Size == right.Size; +} + struct GuestImage { std::filesystem::path SourcePath; std::string OutputName; @@ -31,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; @@ -38,26 +52,35 @@ 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) const; + GuestImage Read(const std::filesystem::path& path, std::vector bytes, bool windows = false) const; }; struct GuestArtifact { std::filesystem::path Path; 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 { public: - std::vector Build(const std::filesystem::path& inputPath, const std::filesystem::path& outputPath, Domain::SysVDynamicSection& dynamic, bool windows, bool macos, bool toIntel, ISyscallScanner& syscallScanner, bool lazyBinding, const std::string& runPath, const std::set& excludedModules) const; + std::vector Build(const std::filesystem::path& inputPath, const std::filesystem::path& outputPath, Domain::SysVDynamicSection& dynamic, bool windows, bool macos, bool toIntel, bool toRosetta, ISyscallScanner& syscallScanner, bool lazyBinding, const std::string& runPath, const std::set& excludedModules, const std::filesystem::path& root, const std::vector& runtimeDirectories, std::vector modules, Domain::IProgress& progress) const; }; } diff --git a/core/relinker/relinker/include/relinker/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 1ed4ca8895..692f30c43d 100644 --- a/core/relinker/relinker/include/relinker/parsing/ElfReader.hpp +++ b/core/relinker/relinker/include/relinker/parsing/ElfReader.hpp @@ -15,11 +15,10 @@ public: 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::vector ReadCodeSegments() 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..d893a7015e --- /dev/null +++ b/core/relinker/relinker/include/relinker/patching/GamePatch.hpp @@ -0,0 +1,131 @@ +#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; +}; + +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::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 new file mode 100644 index 0000000000..194262b78e --- /dev/null +++ b/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace Relinker { + +enum class TargetSystem { Linux, Windows, MacOs }; + +struct ConversionOptions { + TargetSystem Target = TargetSystem::Linux; + std::uint32_t UnusedFilterLevel = 0; + bool ToIntel = false; + bool ToRosetta = false; + bool SkipSyscallCheck = false; + bool TrapSdkSyscallStubs = false; + bool LazyBinding = false; + bool WindowsGui = false; + bool TraceImports = false; + std::optional WindowsFixedBase; + bool WindowsDiagnostics = false; + std::optional LinuxFixedBase; + std::optional MacosFixedBase; + std::string RunPath = "$ORIGIN/libs"; + std::set ExcludedModules; + std::vector Patches; + std::string TargetName; +}; + +struct ConversionInput { + std::vector Executable; + std::filesystem::path InputPath; + std::filesystem::path OutputPath; + std::filesystem::path ModuleRoot; + std::vector Modules; + std::vector RuntimeDirectories; + std::optional> Icon; +}; + +struct ConversionOutput { + std::vector Executable; + std::vector Modules; + RelinkResult Analysis; + std::string WindowsDiagnostics; + std::vector AppliedPatches; +}; + +class Conversion { +public: + explicit Conversion(Domain::IProgress& progress); + ConversionOutput Convert(ConversionInput input, const ConversionOptions& options) const; + +private: + Domain::IProgress& progress; +}; + +} diff --git a/core/relinker/relinker/include/relinker/pipeline/RelinkerPipeline.hpp b/core/relinker/relinker/include/relinker/pipeline/RelinkerPipeline.hpp index 78554168b1..26e20d2420 100644 --- a/core/relinker/relinker/include/relinker/pipeline/RelinkerPipeline.hpp +++ b/core/relinker/relinker/include/relinker/pipeline/RelinkerPipeline.hpp @@ -9,25 +9,27 @@ #include #include #include +#include namespace Relinker { class RelinkerPipeline : public IRelinkerPipeline { public: - RelinkerPipeline(std::shared_ptr elfReader, std::shared_ptr syscallScanner, std::shared_ptr callSiteResolver, std::shared_ptr validationPolicy, std::shared_ptr dynamicSectionBuilder, std::shared_ptr unusedNidFilter, std::uint32_t unusedFilterLevel); + RelinkerPipeline(std::shared_ptr elfReader, std::shared_ptr syscallScanner, std::shared_ptr callSiteResolver, std::shared_ptr validationPolicy, std::shared_ptr dynamicSectionBuilder, std::shared_ptr unusedNidFilter, std::uint32_t unusedFilterLevel, Domain::IProgress& progress); - RelinkResult Relink(const std::vector& sourceElf) override; + RelinkResult Relink() override; private: - std::shared_ptr _elfReader; - std::shared_ptr _syscallScanner; - std::shared_ptr _callSiteResolver; - std::shared_ptr _validationPolicy; - std::shared_ptr _dynamicSectionBuilder; - std::shared_ptr _unusedNidFilter; + std::shared_ptr elfReader; + std::shared_ptr syscallScanner; + std::shared_ptr callSiteResolver; + std::shared_ptr validationPolicy; + std::shared_ptr dynamicSectionBuilder; + std::shared_ptr unusedNidFilter; std::uint32_t unusedFilterLevel; + Domain::IProgress& progress; - static std::string _relocationTypeName(std::uint32_t type); + static std::string relocationTypeName(std::uint32_t type); }; } diff --git a/core/relinker/relinker/src/analysis/CallSiteResolver.cpp b/core/relinker/relinker/src/analysis/CallSiteResolver.cpp index 8628e151cc..91fe531f9c 100644 --- a/core/relinker/relinker/src/analysis/CallSiteResolver.cpp +++ b/core/relinker/relinker/src/analysis/CallSiteResolver.cpp @@ -2,8 +2,11 @@ #include #include #include +#include #include -#include +#include +#include +#include namespace Relinker { @@ -16,90 +19,81 @@ 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; - return mod == ModRmModIndirect && rm == ModRmRmRipRelative; - } - 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::multimap _cachedTargetToInstr; + 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(); - 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); + const auto candidateCount = std::count_if(textSection.begin(), textSection.end(), [](std::uint8_t b) { + return b == OneByteGrp5Rm || b == OneByteModRmRangeJMax; + }); + 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 = static_cast(static_cast(instrEnd) + disp); - _cachedTargetToInstr.emplace(target, m.Offset); - } + 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; - auto lower = _cachedTargetToInstr.lower_bound(targetGotOrPltAddress); - auto upper = _cachedTargetToInstr.lower_bound(targetGotOrPltAddress + targetGotOrPltSize); + const auto findTarget = [&](VirtualAddress target) { + return std::lower_bound(_cachedTargetToInstr.begin(), _cachedTargetToInstr.end(), + std::pair{target, 0}); + }; + auto lower = findTarget(targetGotOrPltAddress); + auto upper = findTarget(targetGotOrPltAddress + targetGotOrPltSize); for (auto it = lower; it != upper; ++it) sites.push_back(textSectionVAddr + it->second); return sites; diff --git a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp index b346b598c1..645dd46f3a 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -1,14 +1,23 @@ #include #include #include +#include +#include +#include #include #include #include #include +#include #include #include #include +#include +#include #include +#include +#include +#include namespace Relinker { @@ -22,11 +31,22 @@ public: if (found == Values.end()) return std::nullopt; return found->second; } + std::vector Targets() const override { + std::vector result; + for (const auto& [slot, target] : Values) result.push_back(target); + return result; + } }; } -std::set CodeInstructionCollector::Collect(const std::vector& bytes, const std::vector& headers) const { +std::set CodeInstructionCollector::Collect(const std::vector& bytes, const std::vector& headers, const std::function& progress, std::set* entryRoots, const std::function& message) const { + std::uint64_t lastProgress = 0; + const auto report = [&](std::uint64_t completed) { + if (completed <= lastProgress) return; + lastProgress = completed; + if (progress) progress(completed, 10000); + }; const auto range = [&](std::uint64_t offset, std::uint64_t size) { if (offset > bytes.size() || size > bytes.size() - offset) throw Domain::RelinkerException("Code analysis: file range exceeds image", offset); }; @@ -39,8 +59,9 @@ std::set CodeInstructionCollector::Collect(const std::ve } throw Domain::RelinkerException("Code analysis: unmapped address", address); }; + auto codeHeaders = headers; const auto isCode = [&](std::uint64_t address) { - for (const auto& header : headers) if (header.Type == 1 && (header.Flags & 1) != 0 && address >= header.MappedAddress && address - header.MappedAddress < header.FileSize) return true; + for (const auto& header : codeHeaders) if (header.Type == 1 && (header.Flags & 1) != 0 && address >= header.MappedAddress && address - header.MappedAddress < header.FileSize) return true; return false; }; std::map tags; @@ -57,7 +78,7 @@ std::set CodeInstructionCollector::Collect(const std::ve const auto tag = Io::ReadU64(bytes, offset); if (tag == 0) { terminated = true; break; } const auto value = Io::ReadU64(bytes, offset + 8); - if (tag == 1 || (tag >= 0x61000000 && tag != 0x6100003f && (tag < 0x61000027 || tag > 0x6100003b))) continue; + if (tag == 1 || (tag >= 0x61000000 && tag != 0x6ffffef5 && tag != 0x6100003f && (tag < 0x61000027 || tag > 0x6100003b))) continue; if (!tags.emplace(tag, value).second) throw Domain::RelinkerException("Code analysis: duplicate dynamic tag", offset); } if (!terminated) throw Domain::RelinkerException("Code analysis: unterminated dynamic segment"); @@ -77,9 +98,113 @@ 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); + 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)); + addPointerRoot(address + info.Length + static_cast(displacement)); + }; const auto addFunction = [&](std::uint64_t begin, std::uint64_t size, bool symbolAlias) { if (size == 0) return; if (!isCode(begin) || size > std::numeric_limits::max() - begin) @@ -102,13 +227,45 @@ 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) { if (*symbolBytes % 24 != 0) throw Domain::RelinkerException("Code analysis: invalid symbol table size"); symbolCount = *symbolBytes / 24; } else if (tags.contains(4)) symbolCount = Io::ReadU32(bytes, fileOffset(tags.at(4), 8) + 4); + else if (tags.contains(0x6ffffef5)) { + const auto address = tags.at(0x6ffffef5); + const auto header = fileOffset(address, 16); + const auto bucketCount = Io::ReadU32(bytes, header); + const auto firstSymbol = Io::ReadU32(bytes, header + 4); + const auto bloomCount = Io::ReadU32(bytes, header + 8); + if (bucketCount == 0 || bloomCount == 0 || firstSymbol == 0) + throw Domain::RelinkerException("Code analysis: invalid GNU hash header", address); + const std::uint64_t bucketOffset = 16 + static_cast(bloomCount) * 8; + const auto chainOffset = bucketOffset + static_cast(bucketCount) * 4; + const auto start = fileOffset(address, chainOffset); + std::uint64_t lastBucket = 0; + for (std::uint64_t index = 0; index < bucketCount; ++index) { + const auto bucket = Io::ReadU32(bytes, start + bucketOffset + index * 4); + if (bucket != 0 && bucket < firstSymbol) + throw Domain::RelinkerException("Code analysis: invalid GNU hash bucket", address); + lastBucket = std::max(lastBucket, static_cast(bucket)); + } + symbolCount = firstSymbol; + if (lastBucket != 0) { + symbolCount = lastBucket; + for (;;) { + const auto offset = chainOffset + (symbolCount - firstSymbol) * 4; + if (offset > std::numeric_limits::max() - address) + throw Domain::RelinkerException("Code analysis: invalid GNU hash chain", address); + const auto chain = Io::ReadU32(bytes, fileOffset(address + offset, 4)); + ++symbolCount; + if (chain & 1) break; + } + } + } std::uint64_t symbols = 0; if (symbolCount != 0) { if (value(11, 0x6100003b) != 24) throw Domain::RelinkerException("Code analysis: invalid symbol entry size"); @@ -124,39 +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) { - if (index != 0 || !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); @@ -173,11 +344,28 @@ 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; std::vector> skipped; std::set staticTargets; + std::uint64_t functionBytes = 0; + for (const auto& [begin, end] : functions) functionBytes += end - begin; + std::uint64_t decodedBytes = 0; + report(1000); for (const auto& [begin, end] : functions) { const auto offset = fileOffset(begin, end - begin); for (auto address = begin; address < end;) { @@ -200,15 +388,22 @@ 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); staticTargets.insert(target); addRoot(target); } address += info.Length; + decodedBytes += info.Length; + if (progress && (decodedBytes & 65535) < info.Length) report(1000 + 2500 * decodedBytes / functionBytes); } } + report(3500); + std::uint64_t scanBytes = 0; + for (const auto& header : headers) if (header.Type == 1 && (header.Flags & 1) != 0) scanBytes += header.FileSize; + std::uint64_t scannedBytes = 0; for (const auto& header : headers) { if (header.Type != 1 || (header.Flags & 1) == 0 || header.FileSize < 16) continue; range(header.Offset, header.FileSize); @@ -217,10 +412,22 @@ std::set CodeInstructionCollector::Collect(const std::ve const auto paddedStart = [&](std::size_t address) { return address % 16 == 0 && address >= 2 && text[address - 1] == 0xcc && text[address - 2] == 0xcc; }; const auto nextPaddedStart = [&](std::size_t address) { address = (address + 16) & ~std::size_t{15}; - while (address < size && !paddedStart(address)) address += 16; + while (address < size && !paddedStart(address)) { + if (progress && address % 65536 == 0) report(3500 + 2500 * (scannedBytes + address) / scanBytes); + address += 16; + } return address; }; + bool afterTransfer = false; for (std::size_t position = nextPaddedStart(0); position < size;) { + if (progress && position % 65536 < 16) report(3500 + 2500 * (scannedBytes + position) / scanBytes); + const auto aligned = (position + 15) & ~std::size_t{15}; + const bool zeroPadding = afterTransfer && aligned != position && aligned <= size && std::all_of(text + position, text + aligned, [](const std::uint8_t value) { return value == 0; }); + afterTransfer = false; + if (zeroPadding) { + position = aligned; + continue; + } if (position + 1 < size && text[position] == 0 && text[position + 1] == 0) { position = nextPaddedStart(position); continue; @@ -242,11 +449,13 @@ 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; } + scannedBytes += header.FileSize; } + report(6000); const auto decodesInSegment = [&](std::uint64_t address) { for (const auto& header : headers) { if (header.Type != 1 || (header.Flags & 1) == 0 || address < header.MappedAddress || address - header.MappedAddress >= header.FileSize) continue; @@ -266,60 +475,75 @@ std::set CodeInstructionCollector::Collect(const std::ve } } std::size_t previousRoots = 0; + std::unordered_set decodedInstructions; + std::vector> sectionReachable(codeHeaders.size()); do { previousRoots = roots.size(); - for (const auto& header : headers) { + for (std::size_t i = 0; i < codeHeaders.size(); ++i) { + const auto& header = codeHeaders[i]; if (header.Type != 1 || (header.Flags & 1) == 0 || header.FileSize == 0) continue; range(header.Offset, header.FileSize); std::vector entries; for (const auto root : roots) if (root >= header.MappedAddress && root - header.MappedAddress < header.FileSize) entries.push_back(root); if (entries.empty()) continue; - const std::vector text(bytes.begin() + header.Offset, bytes.begin() + header.Offset + header.FileSize); - const auto graph = UnusedNidFilter::BuildControlFlowGraph(text, header.MappedAddress, entries.front(), entries, pointers); + const std::span text(bytes.data() + header.Offset, header.FileSize); + 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); const auto offset = address - header.MappedAddress; const auto info = decoder.DecodeInstruction(text.data() + offset, text.size() - offset); + addAddressTakenRoot(address, text.data() + offset, info); if (info.HasBranchTarget && !info.HasRipRelativeDisp) addRoot(static_cast(static_cast(address + info.Length) + info.BranchDisp)); } } } while (roots.size() != previousRoots); + 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; } } } @@ -327,13 +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 new file mode 100644 index 0000000000..3de6710b89 --- /dev/null +++ b/core/relinker/relinker/src/analysis/SceCodeExtent.cpp @@ -0,0 +1,58 @@ +#include +#include +#include + +namespace Relinker { + +std::optional FindSceCodeEnd(std::span segment, VirtualAddress segmentVAddr, const std::vector& functions, VirtualAddress pltGot, ByteCount pltRelocationsSize) { + VirtualAddress codeEnd = 0; + for (const auto& function : functions) + if (function.Begin >= segmentVAddr && function.Begin - segmentVAddr < segment.size()) + codeEnd = std::max(codeEnd, function.End); + if (codeEnd == 0) + return std::nullopt; + if (codeEnd - segmentVAddr > segment.size()) + throw RelinkerException("Unwind function exceeds executable segment", codeEnd); + if (pltRelocationsSize == 0) + return codeEnd; + const auto target = [&](const std::size_t at) { + std::int32_t displacement = 0; + std::memcpy(&displacement, segment.data() + at + 2, sizeof(displacement)); + return segmentVAddr + at + 6 + static_cast(static_cast(displacement)); + }; + const 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; + const ByteCount pltSize = 16 * (1 + pltRelocationsSize / 24); + 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 f57cede700..6003cda8e4 100644 --- a/core/relinker/relinker/src/analysis/SyscallScanner.cpp +++ b/core/relinker/relinker/src/analysis/SyscallScanner.cpp @@ -1,7 +1,18 @@ #include #include +#include +#include +#include +#include +#include +#include +#include + #include +#include +#include #include +#include namespace Relinker { @@ -13,57 +24,320 @@ 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; +static constexpr std::size_t SYSCALL_NUMBER_LOOKBACK = 8; + +static constexpr std::array SDK_GENERIC_SYSCALL_STUB{ + 0x48, 0x89, 0xF8, 0x48, 0x89, 0xF7, 0x48, 0x89, 0xD6, 0x48, 0x89, 0xCA, + 0x4D, 0x89, 0xC2, 0x4D, 0x89, 0xC8, 0x4C, 0x8B, 0x4C, 0x24, 0x08, +}; + +static bool IsSdkSyscallStub(const std::vector& code, const std::size_t syscallOffset) { + constexpr std::array numberLoad{0x48, 0xC7, 0xC0}; + constexpr std::array argumentShuffle{0x49, 0x89, 0xCA}; + if (syscallOffset <= code.size() && syscallOffset >= SDK_GENERIC_SYSCALL_STUB.size() && code.size() - syscallOffset >= 3u) { + const auto begin = code.begin() + static_cast(syscallOffset - SDK_GENERIC_SYSCALL_STUB.size()); + if (std::equal(SDK_GENERIC_SYSCALL_STUB.begin(), SDK_GENERIC_SYSCALL_STUB.end(), begin) + && code[syscallOffset] == 0x0F && code[syscallOffset + 1] == 0x05 && code[syscallOffset + 2] == 0xC3) + return true; + } + if (syscallOffset > code.size() || syscallOffset < 10u || code.size() - syscallOffset < 3u) + return false; + const auto begin = code.begin() + static_cast(syscallOffset - 10u); + return std::equal(numberLoad.begin(), numberLoad.end(), begin) + && std::equal(argumentShuffle.begin(), argumentShuffle.end(), begin + 7) + && code[syscallOffset] == 0x0F && code[syscallOffset + 1] == 0x05 && code[syscallOffset + 2] == 0xC3; +} + +enum class ForbiddenInstruction { + None, + Invocation, + Return, +}; + +static constexpr std::array SDK_MPROTECT_INLINE{ + 0x55, 0x48, 0x89, 0xE5, 0x53, 0x50, 0x89, 0xD1, 0x49, 0x89, 0xF0, 0x48, 0x89, 0xFE, + 0xF6, 0xC1, 0x04, 0x75, 0x31, 0x48, 0x63, 0xD1, 0xB8, 0x4A, 0x00, 0x00, 0x00, + 0x48, 0x89, 0xF7, 0x4C, 0x89, 0xC6, 0x45, 0x31, 0xD2, 0x45, 0x31, 0xC0, 0x45, 0x31, 0xC9, + 0x0F, 0x05, 0x0F, 0x93, 0xC1, 0x48, 0x89, 0xC3, 0x48, 0xF7, 0xDB, 0x84, 0xC9, + 0x48, 0x0F, 0x45, 0xD8, 0x48, 0x85, 0xDB, +}; + +static constexpr std::array SDK_MMAP_INLINE{ + 0x49, 0x63, 0xD6, 0x4C, 0x63, 0xD1, 0x4D, 0x63, 0xC0, 0xB8, 0xDD, 0x01, 0x00, 0x00, + 0x48, 0x89, 0xDE, 0x0F, 0x05, 0x48, 0x89, 0xC1, 0x0F, 0x93, 0xC2, 0x48, 0xF7, 0xD8, + 0x84, 0xD2, 0x48, 0x0F, 0x45, 0xC1, 0x48, 0x85, 0xC0, +}; + +static constexpr std::array SDK_MMAP_EXEC_INLINE{ + 0x44, 0x89, 0xF0, 0x83, 0xE0, 0xFB, 0x48, 0x63, 0xD0, 0x4C, 0x63, 0xD1, 0x4D, 0x63, 0xC0, + 0xB8, 0xDD, 0x01, 0x00, 0x00, 0x48, 0x89, 0xDE, 0x0F, 0x05, 0x0F, 0x93, 0xC1, + 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 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; + return recent.Contains(codeOffset + start); +} + +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) { + if (opcodeOffset + 1 >= limit) + return ForbiddenInstruction::None; + + const std::uint8_t byte0 = codeSection[opcodeOffset]; + const std::uint8_t byte1 = codeSection[opcodeOffset + 1]; + + const bool isSyscall = (byte0 == SYSCALL_BYTE0 && byte1 == SYSCALL_BYTE1); + const bool isInt80 = (byte0 == INT80_BYTE0 && byte1 == INT80_BYTE1); + const bool isSysenter = (byte0 == SYSENTER_BYTE0 && byte1 == SYSENTER_BYTE1); + const bool isSysret = (byte0 == SYSRET_BYTE0 && byte1 == SYSRET_BYTE1); + + if (isSyscall || isInt80 || isSysenter) + return ForbiddenInstruction::Invocation; + + if (isSysret) + return ForbiddenInstruction::Return; + + return ForbiddenInstruction::None; +} + +static std::optional ConstantRaxLoad(const std::uint8_t* bytes, const std::size_t length) { + const std::size_t opcode = Codegen::DecodedInstruction{bytes, length}.OpcodeOffset(); + const bool hasRex = opcode > 0 && (bytes[opcode - 1] & 0xF0) == 0x40; + const std::uint8_t rex = hasRex ? bytes[opcode - 1] : 0; + + if (hasRex && rex == 0x48 && length - opcode == 6 && bytes[opcode] == 0xC7 && bytes[opcode + 1] == 0xC0) { + std::uint32_t value = 0; + std::memcpy(&value, bytes + opcode + 2, sizeof(value)); + return value; + } + + if (!hasRex && length - opcode == 5 && bytes[opcode] == 0xB8) { + std::uint32_t value = 0; + std::memcpy(&value, bytes + opcode + 1, sizeof(value)); + return value; + } + + return std::nullopt; +} + +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) + return std::nullopt; + + const std::uint8_t* bytes = codeSection.data() + start; + + if (const auto value = ConstantRaxLoad(bytes, match.Length)) + return value; + + const std::size_t opcode = Codegen::DecodedInstruction{bytes, match.Length}.OpcodeOffset(); + const bool hasRex = opcode > 0 && (bytes[opcode - 1] & 0xF0) == 0x40; + const bool movesFirstSyscallArgument = hasRex && bytes[opcode - 1] == 0x49 + && match.Length - opcode == 2 && bytes[opcode] == 0x89 && bytes[opcode + 1] == 0xCA; + + if (!movesFirstSyscallArgument) + return std::nullopt; + } + + return std::nullopt; +} + +static std::optional 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: - void ScanCodeSectionForSyscalls( - const std::vector& codeSection, - FileByteOffset codeSectionOffset, - FileByteOffset codeSectionSize) override; + 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, const std::vector& unwindRanges) override; + +private: + Domain::IProgress& progress; + bool trapSdkStubs; }; -void SyscallScanner::ScanCodeSectionForSyscalls( - const std::vector& codeSection, - const FileByteOffset codeSectionOffset, - const FileByteOffset codeSectionSize) { - const std::size_t limit = std::min(codeSection.size(), static_cast(codeSectionSize)); +std::vector SyscallScanner::ScanCodeSectionForSyscalls(const std::vector& codeSection, const FileByteOffset codeSectionOffset, const FileByteOffset codeSectionSize, std::string_view input, 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(); - const auto matches = scanner->ScanCodeSection(codeSection, codeSectionOffset, codeSectionSize); + 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; + 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)); - for (const auto& [Offset, Length] : matches) { - const std::size_t i = Offset - codeSectionOffset; + const auto kind = ClassifyForbiddenInstruction(codeSection, opcode, spanLimit); - if (i + 1 < limit) { - const std::uint8_t b0 = codeSection[i]; - const std::uint8_t b1 = codeSection[i + 1]; + if (kind == ForbiddenInstruction::None) + return; - const bool isSyscall = (b0 == SYSCALL_BYTE0 && b1 == SYSCALL_BYTE1); - const bool isInt80 = (b0 == INT80_BYTE0 && b1 == INT80_BYTE1); - const bool isSysenter = (b0 == SYSENTER_BYTE0 && b1 == SYSENTER_BYTE1); - const bool isSysret = (b0 == SYSRET_BYTE0 && b1 == SYSRET_BYTE1); + if (trapSdkStubs && kind == ForbiddenInstruction::Invocation && + (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()); + return; + } - if (isSyscall || isInt80 || isSysenter || isSysret) { - const FileByteOffset instrOffset = codeSectionOffset + i; - std::ostringstream msg; - msg << "Forbidden syscall instruction at code offset 0x" << std::hex << instrOffset; - throw RelinkerException(msg.str(), instrOffset); + 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 (number) + msg << ": syscall number " << std::dec << *number; + + 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, source, limit, limit}); + return trapped; } -std::unique_ptr MakeSyscallScanner() { - return std::make_unique(); +std::unique_ptr MakeSyscallScanner(Domain::IProgress& progress, const bool trapSdkStubs) { + return std::make_unique(progress, trapSdkStubs); } class NullSyscallScanner : public ISyscallScanner { public: - void ScanCodeSectionForSyscalls( - const std::vector&, - FileByteOffset, - FileByteOffset - ) override {} + 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 e6b7963488..3bcedf3e05 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include namespace Relinker { @@ -12,25 +14,35 @@ public: const std::vector& nidRefs, const std::vector& elfBytes, const std::vector& textSection, - VirtualAddress textVAddr + VirtualAddress textVAddr, + const std::vector& extraEntries ) override { if (textSection.empty()) throw RelinkerException("Cannot filter NIDs: text section is empty"); + auto relativeRelocations = UnusedNidFilter::BuildRelativeRelocationIndex(elfBytes); auto collector = UnusedNidFilter::MakeEntryPointCollector(); - auto entries = collector->Collect(elfBytes, textVAddr, textSection.size()); + auto entries = collector->Collect(elfBytes, textVAddr, textSection.size(), *relativeRelocations); + for (const auto entry : extraEntries) + if (entry >= textVAddr && entry - textVAddr < textSection.size()) entries.push_back(entry); if (entries.empty()) throw RelinkerException("Cannot filter NIDs: no entry points found"); VirtualAddress primary = entries[0]; std::vector extra(entries.begin() + 1, entries.end()); - auto relativeRelocations = UnusedNidFilter::BuildRelativeRelocationIndex(elfBytes); - auto cfg = UnusedNidFilter::BuildControlFlowGraph(textSection, textVAddr, primary, extra, *relativeRelocations); + const ElfReader reader(elfBytes); + std::vector data; + for (const auto& segment : reader.ReadProgramHeaders()) { + if (segment.Type != 1 || (segment.Flags & 1) != 0 || (segment.Flags & 6) == 0 || segment.FileSize == 0) continue; + data.push_back({segment.MappedAddress, reader.ReadSegment(segment)}); + } + + auto cfg = UnusedNidFilter::BuildAddressTakenControlFlowGraph(textSection, textVAddr, primary, extra, *relativeRelocations, data); auto index = UnusedNidFilter::BuildGotAccessIndex(*cfg, textSection, textVAddr); 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 bb98c8277d..da812ba58e 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp @@ -1,108 +1,210 @@ #include #include +#include +#include #include #include #include +#include namespace Relinker::UnusedNidFilter { class ControlFlowGraph : public IControlFlowGraph { public: explicit ControlFlowGraph(std::unordered_set reachable) - : _reachable(std::move(reachable)) {} + : _owned(std::move(reachable)), _borrowed(nullptr) {} - const std::unordered_set& ReachableVaddrs() const override { return _reachable; } - bool IsReachable(VirtualAddress vaddr) const override { return _reachable.count(vaddr) > 0; } + explicit ControlFlowGraph(const std::unordered_set* reachable) + : _borrowed(reachable) {} + + const std::unordered_set& ReachableVaddrs() const override { + return _borrowed ? *_borrowed : _owned; + } + bool IsReachable(VirtualAddress vaddr) const override { + return (_borrowed ? *_borrowed : _owned).count(vaddr) > 0; + } private: - std::unordered_set _reachable; + std::unordered_set _owned; + const std::unordered_set* _borrowed = nullptr; }; +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, 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; + std::vector addressTaken; + bool speculative = false; + + auto enqueue = [&](VirtualAddress va) { + if (reachable.count(va) > 0) 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) + enqueue(va); + else + addressTaken.push_back(va); + }; + + enqueue(entryVaddr); + for (VirtualAddress va : extraEntries) enqueue(va); + for (VirtualAddress va : relativeRelocations.Targets()) takeAddress(va); + + while (!worklist.empty() || !speculative) { + if (worklist.empty()) { + speculative = true; + for (VirtualAddress va : addressTaken) enqueue(va); + continue; + } + + VirtualAddress va = worklist.front(); + worklist.pop(); + const VirtualAddress blockEntry = 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); + + Codegen::DecodedInstructionInfo info; + if (speculative) { + try { + info = decoder.DecodeInstruction(text.data() + bufOff, available); + } catch (const Codegen::CodegenException&) { + break; + } + } else { + info = decoder.DecodeInstruction(text.data() + bufOff, available); + } + reachable.insert(va); + if (progress && reachable.size() % 4096 == 0) progress(reachable.size()); + visited.erase(va); + + VirtualAddress nextVaddr = va + static_cast(info.Length); + + 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(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 = nextVaddr + static_cast(static_cast(displacement)); + takeAddress(base); + for (VirtualAddress target : ReadRelativeTableTargets(*data, base, textVaddr, text.size())) takeAddress(target); + } + + bool fallThrough = true; + switch (info.FlowKind) { + using enum Codegen::ControlFlowKind; + case Sequential: + break; + case ConditionalBranch: + if (info.HasBranchTarget) + 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 = nextVaddr + static_cast(static_cast(disp)); + auto resolved = relativeRelocations.TargetOfSlot(slotVaddr); + if (resolved.has_value()) + enqueue(*resolved); + } else if (info.HasBranchTarget) { + 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 = nextVaddr + static_cast(static_cast(disp)); + auto resolved = relativeRelocations.TargetOfSlot(slotVaddr); + if (resolved.has_value()) + enqueue(*resolved); + } else if (info.HasBranchTarget) { + enqueue(nextVaddr + static_cast(info.BranchDisp)); + } + fallThrough = !zeroPaddingFollows(nextVaddr); + break; + case IndirectCall: + fallThrough = !zeroPaddingFollows(nextVaddr); + break; + case Return: + case IndirectJump: + case Trap: + fallThrough = false; + break; + } + if (!fallThrough) break; + va = nextVaddr; + } + } +} + +} + +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); +} + +std::unique_ptr BuildControlFlowGraph( + std::span text, + VirtualAddress textVaddr, + VirtualAddress entryVaddr, + const std::vector& extraEntries, + const IRelativeRelocationIndex& relativeRelocations, + bool followCodeAddresses +) { + std::unordered_set reachable; + BuildControlFlowGraph(text, textVaddr, entryVaddr, extraEntries, relativeRelocations, followCodeAddresses, reachable); + return std::make_unique(std::move(reachable)); +} + std::unique_ptr BuildControlFlowGraph( const std::vector& text, VirtualAddress textVaddr, VirtualAddress entryVaddr, const std::vector& extraEntries, - const IRelativeRelocationIndex& relativeRelocations + const IRelativeRelocationIndex& relativeRelocations, + bool followCodeAddresses +) { + return BuildControlFlowGraph(std::span(text.data(), text.size()), textVaddr, entryVaddr, extraEntries, relativeRelocations, followCodeAddresses); +} + +std::unique_ptr BuildAddressTakenControlFlowGraph( + const std::vector& text, + VirtualAddress textVaddr, + VirtualAddress entryVaddr, + const std::vector& extraEntries, + const IRelativeRelocationIndex& relativeRelocations, + const std::vector& data ) { - const Codegen::X64InstructionDecoder decoder; std::unordered_set reachable; - std::queue worklist; - - auto enqueue = [&](VirtualAddress va) { - if (reachable.count(va) > 0) return; - if (va < textVaddr || va >= textVaddr + static_cast(text.size())) return; - reachable.insert(va); - worklist.push(va); - }; - - enqueue(entryVaddr); - for (VirtualAddress va : extraEntries) enqueue(va); - - while (!worklist.empty()) { - VirtualAddress va = worklist.front(); - worklist.pop(); - - if (va < textVaddr || va >= textVaddr + static_cast(text.size())) - throw RelinkerException("CFG: jump target outside text segment", 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); - - Codegen::DecodedInstructionInfo info = decoder.DecodeInstruction(text.data() + bufOff, available); - - VirtualAddress nextVaddr = va + static_cast(info.Length); - - switch (info.FlowKind) { - using enum Codegen::ControlFlowKind; - case Sequential: - enqueue(nextVaddr); - break; - case ConditionalBranch: - enqueue(nextVaddr); - if (info.HasBranchTarget) - enqueue(static_cast(static_cast(nextVaddr) + info.BranchDisp)); - break; - case UnconditionalJump: - 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 - ); - auto resolved = relativeRelocations.TargetOfSlot(slotVaddr); - if (resolved.has_value()) - enqueue(*resolved); - } else if (info.HasBranchTarget) { - enqueue(static_cast(static_cast(nextVaddr) + 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 - ); - 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); - break; - case IndirectCall: - enqueue(nextVaddr); - break; - case Return: - case IndirectJump: - case Trap: - break; - } - } - + 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 50cbf3ee22..5f1b754c86 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp @@ -1,6 +1,7 @@ #include #include #include +#include namespace Relinker::UnusedNidFilter { @@ -21,6 +22,7 @@ public: std::vector result; for (const auto& segment : headers) { if (segment.Type != 0x6474E550 || segment.FileSize == 0) continue; + if (segment.FileSize < 4) throw RelinkerException("Strict filter: EH frame header exceeds segment", segment.MappedAddress); auto position = segment.MappedAddress; const auto version = read(position); const auto frameEncoding = read(position); @@ -30,12 +32,15 @@ public: throw RelinkerException("Strict filter: unsupported EH frame header", segment.MappedAddress); encoded(position, frameEncoding, segment.MappedAddress); const auto count = encoded(position, countEncoding, segment.MappedAddress); + if (position < segment.MappedAddress || position - segment.MappedAddress > segment.FileSize) + throw RelinkerException("Strict filter: EH frame header exceeds segment", position); if (count > segment.FileSize / 2) throw RelinkerException("Strict filter: invalid EH frame count", position); for (std::uint64_t index = 0; index < count; ++index) { const auto begin = encoded(position, tableEncoding, segment.MappedAddress); const auto fde = encoded(position, tableEncoding, segment.MappedAddress); - if (position > segment.MappedAddress + segment.FileSize) throw RelinkerException("Strict filter: EH frame table exceeds segment", position); - result.push_back(readFde(begin, fde)); + if (position < segment.MappedAddress || position - segment.MappedAddress > segment.FileSize) throw RelinkerException("Strict filter: EH frame table exceeds segment", position); + auto region = readFde(begin, fde); + if (region.End != region.Begin) result.push_back(std::move(region)); } } return result; @@ -48,11 +53,38 @@ private: const std::set& importSlots; std::map cies; + VirtualAddress checkedRangeEnd(VirtualAddress address, std::uint64_t size) const { + if (size > std::numeric_limits::max() - address) + throw RelinkerException("Strict filter: unwind range overflowed", address); + const auto end = address + size; + while (address < end) { + bool found = false; + for (const auto& segment : headers) { + if (segment.Type != 1 || address < segment.MappedAddress) continue; + const auto relative = address - segment.MappedAddress; + if (relative >= segment.FileSize) continue; + const auto chunk = std::min(end - address, segment.FileSize - relative); + if (segment.Offset > bytes.size() || relative > bytes.size() - segment.Offset || + chunk > bytes.size() - segment.Offset - relative) + throw RelinkerException("Strict filter: unwind data exceeds file", address); + address += chunk; + found = true; + break; + } + if (!found) throw RelinkerException("Strict filter: unwind range is not file-backed", address); + } + return end; + } + template TValue read(VirtualAddress& address) const { for (const auto& segment : headers) { if (segment.Type != 1 || address < segment.MappedAddress || address - segment.MappedAddress > segment.FileSize || sizeof(TValue) > segment.FileSize - (address - segment.MappedAddress)) continue; - const auto offset = segment.Offset + address - segment.MappedAddress; + const auto relative = address - segment.MappedAddress; + if (segment.Offset > bytes.size() || relative > bytes.size() - segment.Offset || + sizeof(TValue) > std::numeric_limits::max() - address) + throw RelinkerException("Strict filter: unwind data range overflowed", address); + const auto offset = segment.Offset + relative; if (offset > bytes.size() || sizeof(TValue) > bytes.size() - offset) throw RelinkerException("Strict filter: unwind data exceeds file", address); TValue value; std::memcpy(&value, bytes.data() + offset, sizeof(value)); @@ -67,7 +99,7 @@ private: unsigned shift = 0; for (;;) { const auto byte = read(position); - if (shift == 63 && ((byte & 0x7F) != 0 && (byte & 0x7F) != 1 && (!signedValue || (byte & 0x7F) != 0x7F))) + if (shift == 63 && (byte & 0x7F) != 0 && (byte & 0x7F) != (signedValue ? 0x7F : 1)) throw RelinkerException("Strict filter: overflowing LEB128 value", position); value |= static_cast(byte & 0x7F) << shift; shift += 7; @@ -95,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; @@ -115,7 +156,7 @@ private: auto position = address; const auto length = read(position); if (length == 0xFFFFFFFF || length < 8) throw RelinkerException("Strict filter: unsupported CIE record size", address); - const auto end = position + length; + const auto end = checkedRangeEnd(position, length); if (read(position) != 0) throw RelinkerException("Strict filter: invalid CIE identifier", address); const auto version = read(position); if (version != 1 && version != 3 && version != 4) throw RelinkerException("Strict filter: unsupported CIE version", address); @@ -137,8 +178,9 @@ private: if (augmentation.front() != 'z') throw RelinkerException("Strict filter: unsupported CIE augmentation", address); cie.Augmented = true; const auto size = leb(position); + if (position > end || size > end - position) + throw RelinkerException("Strict filter: CIE augmentation exceeds record", address); const auto augmentationEnd = position + size; - if (augmentationEnd > end) throw RelinkerException("Strict filter: CIE augmentation exceeds record", address); for (std::size_t index = 1; index < augmentation.size(); ++index) { switch (augmentation[index]) { case 'L': cie.LsdaEncoding = read(position); break; @@ -163,24 +205,26 @@ private: auto position = address; const auto length = read(position); if (length == 0xFFFFFFFF || length < 8) throw RelinkerException("Strict filter: unsupported FDE record size", address); - const auto end = position + length; + 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); if (cie.Augmented) { const auto augmentationSize = leb(position); + if (position > end || augmentationSize > end - position) + throw RelinkerException("Strict filter: FDE augmentation exceeds record", address); const auto augmentationEnd = position + augmentationSize; if (augmentationEnd > end) throw RelinkerException("Strict filter: FDE augmentation exceeds record", address); if (cie.LsdaEncoding != 0xFF) { const auto lsda = encoded(position, cie.LsdaEncoding, 0, begin); - if (lsda != 0) readLsda(lsda, region); + if (lsda != 0 && cie.Personality != 0) readLsda(lsda, region); } if (position > augmentationEnd) throw RelinkerException("Strict filter: FDE augmentation size mismatch", address); } @@ -198,13 +242,18 @@ private: if ((callEncoding & 0xF0) != 0) throw RelinkerException("Strict filter: unsupported LSDA call-site encoding", address); const auto tableSize = leb(position); if (tableSize > bytes.size()) throw RelinkerException("Strict filter: invalid LSDA table size", address); - const auto tableEnd = position + tableSize; + const auto tableEnd = checkedRangeEnd(position, tableSize); while (position < tableEnd) { encoded(position, callEncoding); encoded(position, callEncoding); const auto landing = encoded(position, callEncoding); leb(position); - if (landing != 0) region.ExtraTargets.push_back(landingBase + landing); + if (landing != 0) { + const bool negative = (callEncoding & 0x08) != 0 && (landing >> 63) != 0; + if (negative ? landingBase < std::uint64_t{0} - landing : landing > std::numeric_limits::max() - landingBase) + throw RelinkerException("Strict filter: LSDA landing pad address overflowed", address); + region.ExtraTargets.push_back(landingBase + landing); + } } if (position != tableEnd) throw RelinkerException("Strict filter: LSDA call-site table size mismatch", address); } diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp index d24bcaf3da..947521c429 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp @@ -1,6 +1,8 @@ #include +#include #include #include +#include namespace Relinker::UnusedNidFilter { @@ -26,8 +28,18 @@ std::uint64_t read64(const std::vector& b, std::size_t off) { return v; } +bool fitsAt(std::uint64_t base, std::uint64_t offset, std::uint64_t length, std::size_t size) { + return base <= size && offset <= size - base && length <= size - base - offset; +} + +struct LoadSegment { + VirtualAddress vaddr; + std::uint64_t fileOffset; + std::uint64_t fileSize; +}; + 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); } } @@ -37,8 +49,12 @@ public: std::vector Collect( const std::vector& elfBytes, VirtualAddress textVaddr, - std::size_t textSize + 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') { @@ -62,9 +78,16 @@ public: if (phEntSize < 56) throw RelinkerException("ELF program header entry too small"); + std::vector loads; + for (const auto& header : ElfReader(elfBytes).ReadProgramHeaders()) { + if (header.Type != PT_LOAD) continue; + loads.push_back({header.MappedAddress, header.Offset, header.FileSize}); + } + for (std::uint16_t i = 0; i < phCount; ++i) { - std::size_t phPos = static_cast(phOff) + i * phEntSize; - if (phPos + 56 > elfBytes.size()) throw RelinkerException("Program header out of bounds"); + const std::uint64_t phIndexOff = static_cast(i) * phEntSize; + if (!fitsAt(phOff, phIndexOff, 56, elfBytes.size())) throw RelinkerException("Program header out of bounds"); + std::size_t phPos = static_cast(phOff + phIndexOff); std::uint32_t type = read32(elfBytes, phPos); @@ -73,12 +96,14 @@ public: std::uint64_t segOff = read64(elfBytes, phPos + 8); std::uint64_t segSz = read64(elfBytes, phPos + 32); - if (segOff + segSz > elfBytes.size()) throw RelinkerException("PT_DYNAMIC segment out of bounds"); + if (!fitsAt(segOff, 0, segSz, elfBytes.size())) throw RelinkerException("PT_DYNAMIC segment out of bounds"); VirtualAddress initArrayVa = 0; std::uint64_t initArraySz = 0; VirtualAddress finiArrayVa = 0; std::uint64_t finiArraySz = 0; + VirtualAddress preinitArrayVa = 0; + std::uint64_t preinitArraySz = 0; for (std::uint64_t off = 0; off + 16 <= segSz; off += 16) { std::size_t pos = static_cast(segOff + off); @@ -92,24 +117,37 @@ public: if (tag == DT_INIT_ARRAYSZ || tag == DT_OS_INIT_ARRAYSZ) initArraySz = val; if (tag == DT_FINI_ARRAY || tag == DT_OS_FINI_ARRAY) finiArrayVa = val; if (tag == DT_FINI_ARRAYSZ || tag == DT_OS_FINI_ARRAYSZ) finiArraySz = val; + if (tag == DT_PREINIT_ARRAY || tag == DT_OS_PREINIT_ARRAY) preinitArrayVa = val; + if (tag == DT_PREINIT_ARRAYSZ || tag == DT_OS_PREINIT_ARRAYSZ) preinitArraySz = val; } auto collectArray = [&](VirtualAddress arrayVa, std::uint64_t arraySz) { if (arrayVa == 0 || arraySz == 0) return; - std::uint64_t phFileOff = read64(elfBytes, phPos + 8); - std::uint64_t phVaddr = read64(elfBytes, phPos + 16); - if (arrayVa < phVaddr) return; - std::uint64_t arrayFileOff = phFileOff + (arrayVa - phVaddr); - std::uint64_t count = arraySz / 8; - for (std::uint64_t k = 0; k < count; ++k) { - std::size_t entPos = static_cast(arrayFileOff + k * 8); - if (entPos + 8 > elfBytes.size()) break; - addIfInText(read64(elfBytes, entPos)); + if (arraySz % 8 != 0) throw RelinkerException("Init/fini array size is not a multiple of 8", arrayVa); + const LoadSegment* container = nullptr; + for (const auto& load : loads) { + if (arrayVa >= load.vaddr && arrayVa - load.vaddr <= load.fileSize && + arraySz <= load.fileSize - (arrayVa - load.vaddr)) { + container = &load; + break; + } + } + if (!container) throw RelinkerException("Init/fini array is not file-backed", arrayVa); + std::uint64_t arrayFileOff = container->fileOffset + (arrayVa - container->vaddr); + if (arrayFileOff > elfBytes.size() || arraySz > elfBytes.size() - arrayFileOff) + throw RelinkerException("Init/fini array is out of bounds", arrayVa); + for (std::uint64_t k = 0; k < arraySz / 8; ++k) { + VirtualAddress slot = arrayVa + k * 8; + auto relocated = relativeRelocations.TargetOfSlot(slot); + std::uint64_t target = relocated ? *relocated : read64(elfBytes, static_cast(arrayFileOff + k * 8)); + if (target == 0 || target == std::numeric_limits::max()) continue; + addIfInText(target); } }; collectArray(initArrayVa, initArraySz); collectArray(finiArrayVa, finiArraySz); + collectArray(preinitArrayVa, preinitArraySz); break; } @@ -120,14 +158,16 @@ public: std::uint16_t shStrIdx = read16(elfBytes, 62); if (shEntSize >= 64 && shOff != 0 && shCount > 0 && shStrIdx < shCount) { - std::size_t shStrPos = static_cast(shOff) + shStrIdx * shEntSize; - if (shStrPos + 64 <= elfBytes.size()) { + const std::uint64_t shStrIndexOff = static_cast(shStrIdx) * shEntSize; + if (fitsAt(shOff, shStrIndexOff, 64, elfBytes.size())) { + std::size_t shStrPos = static_cast(shOff + shStrIndexOff); std::uint64_t strTabOff = read64(elfBytes, shStrPos + 24); std::uint64_t strTabSz = read64(elfBytes, shStrPos + 32); for (std::uint16_t si = 0; si < shCount; ++si) { - std::size_t shPos = static_cast(shOff) + si * shEntSize; - if (shPos + 64 > elfBytes.size()) break; + const std::uint64_t shIndexOff = static_cast(si) * shEntSize; + if (!fitsAt(shOff, shIndexOff, 64, elfBytes.size())) break; + std::size_t shPos = static_cast(shOff + shIndexOff); std::uint32_t shType = read32(elfBytes, shPos + 4); if (shType != SHT_DYNSYM && shType != SHT_SYMTAB) continue; @@ -140,9 +180,9 @@ public: std::uint64_t strOff = 0; if (symLink < shCount) { - std::size_t strShPos = static_cast(shOff) + symLink * shEntSize; - if (strShPos + 64 <= elfBytes.size()) - strOff = read64(elfBytes, strShPos + 24); + const std::uint64_t strShIndexOff = static_cast(symLink) * shEntSize; + if (fitsAt(shOff, strShIndexOff, 64, elfBytes.size())) + strOff = read64(elfBytes, static_cast(shOff + strShIndexOff) + 24); } else { strOff = strTabOff; (void)strTabSz; @@ -151,8 +191,8 @@ public: std::uint64_t symCount = symSz / entSz; for (std::uint64_t k = 0; k < symCount; ++k) { + if (!fitsAt(symOff, k * entSz, 24, elfBytes.size())) break; std::size_t sPos = static_cast(symOff + k * entSz); - if (sPos + 24 > elfBytes.size()) break; std::uint8_t info = elfBytes[sPos + 4]; std::uint8_t stBind = info >> 4; std::uint8_t stType = info & 0xF; diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/GotAccessIndex.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/GotAccessIndex.cpp index 9ca3e74e7d..9d1e5a3c2e 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/GotAccessIndex.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/GotAccessIndex.cpp @@ -1,28 +1,11 @@ #include #include -#include #include #include +#include namespace Relinker::UnusedNidFilter { -namespace { - -constexpr std::uint8_t OneByteMovLoadRm8 = 0x8A; -constexpr std::uint8_t OneByteMovLoadRm = 0x8B; -constexpr std::uint8_t Grp5RegCallIndirect = 2; -constexpr std::uint8_t Grp5RegJmpIndirect = 4; - -bool ReadsMemoryOperandAsPointer(const Codegen::DecodedInstructionInfo& info) { - if (info.IsTwoByteOpcode) return false; - if (info.Opcode == OneByteMovLoadRm8 || info.Opcode == OneByteMovLoadRm) return true; - if (info.Opcode == Codegen::X64OpcodeConstants::OneByteGrp5Rm) - return info.ModRmRegField == Grp5RegCallIndirect || info.ModRmRegField == Grp5RegJmpIndirect; - return false; -} - -} - class GotAccessIndex : public IGotAccessIndex { public: explicit GotAccessIndex(std::unordered_set accessed) @@ -41,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; @@ -53,14 +39,14 @@ std::unique_ptr BuildGotAccessIndex( Codegen::DecodedInstructionInfo info = decoder.DecodeInstruction(text.data() + bufOff, available); if (!info.HasRipRelativeDisp) continue; - if (!ReadsMemoryOperandAsPointer(info)) continue; 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 832de8d4fd..6632f2e0ae 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/PltCompactor.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/PltCompactor.cpp @@ -1,15 +1,15 @@ #include #include #include -#include -#include +#include +#include namespace Relinker::UnusedNidFilter { CompactedPlt CompactPlt(const std::vector& originalReferences, const std::vector& keptReferences, const std::vector& text, VirtualAddress textVaddr, FileByteOffset textOffset, FileByteOffset tableOffset) { CompactedPlt result; result.References = keptReferences; - std::map originalSlots; + std::unordered_map originalSlots; for (const auto& reference : originalReferences) { if (reference.RelocationTypeValue != 7) continue; if (reference.RelocationTableOffset < tableOffset || (reference.RelocationTableOffset - tableOffset) % 24 != 0 || (reference.RelocationTableOffset - tableOffset) / 24 > std::numeric_limits::max()) @@ -17,26 +17,34 @@ CompactedPlt CompactPlt(const std::vector& originalReferences, con if (!originalSlots.emplace(reference.RelocationAddress, static_cast((reference.RelocationTableOffset - tableOffset) / 24)).second) throw RelinkerException("Strict filter: duplicate original PLT slot", reference.RelocationAddress); } - std::map newSlots; + std::unordered_map newSlots; for (auto& reference : result.References) { 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::set found; + std::unordered_set found; for (std::size_t offset = 0; offset + 16 <= text.size(); ++offset) { - if (text[offset] != 0xFF || text[offset + 1] != 0x25) continue; + 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; - if (text[offset + 6] != 0x68 || text[offset + 11] != 0xE9) 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 fd508b987f..d6a5eb9ffe 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp @@ -1,7 +1,10 @@ #include +#include #include #include #include +#include +#include namespace Relinker::UnusedNidFilter { @@ -33,10 +36,22 @@ struct LoadSegment { std::uint64_t fileSize; }; -std::uint64_t vaddrToFileOffset(const std::vector& loads, VirtualAddress va) { +std::uint64_t vaddrToFileOffset( + const std::vector& loads, + VirtualAddress va, + std::uint64_t size +) { for (const auto& seg : loads) { - if (va >= seg.vaddr && va < seg.vaddr + seg.fileSize) - return seg.fileOffset + (va - seg.vaddr); + if (va >= seg.vaddr) { + std::uint64_t relative = va - seg.vaddr; + if (relative < seg.fileSize) { + if (size > seg.fileSize - relative) + throw RelinkerException("Relocation table out of bounds", va); + if (relative > std::numeric_limits::max() - seg.fileOffset) + throw RelinkerException("Relocation table file offset overflow", va); + return seg.fileOffset + relative; + } + } } throw RelinkerException("Cannot translate virtual address to file offset", va); } @@ -46,16 +61,27 @@ constexpr std::size_t RelaEntSize = 24; void collectRelativeEntries( const std::vector& elfBytes, const std::vector& loads, - VirtualAddress tableVaddr, + std::uint64_t tableAddress, std::uint64_t tableSize, + bool sceTable, + std::uint64_t sceTableBase, std::unordered_map& out ) { - if (tableVaddr == 0 || tableSize == 0) return; - std::uint64_t tableFileOff = vaddrToFileOffset(loads, tableVaddr); + if (tableSize == 0 && !sceTable) return; + if (tableSize % RelaEntSize != 0) + throw RelinkerException("Invalid relocation table size", tableSize); - for (std::uint64_t off = 0; off + RelaEntSize <= tableSize; off += RelaEntSize) { + if (sceTable && tableAddress > std::numeric_limits::max() - sceTableBase) + throw RelinkerException("Relocation table file offset overflow", tableAddress); + std::uint64_t tableFileOff = sceTable ? sceTableBase + tableAddress : vaddrToFileOffset(loads, tableAddress, tableSize); + if (tableFileOff > elfBytes.size()) + throw RelinkerException("Relocation table out of bounds", tableFileOff); + if (tableSize > elfBytes.size() - tableFileOff) + throw RelinkerException("Relocation table out of bounds", tableFileOff); + + for (std::uint64_t off = 0; off < tableSize; off += RelaEntSize) { std::size_t pos = static_cast(tableFileOff + off); - if (pos + RelaEntSize > elfBytes.size()) + if (pos > elfBytes.size() || RelaEntSize > elfBytes.size() - pos) throw RelinkerException("Relocation entry out of bounds", pos); std::uint64_t rOffset = read64(elfBytes, pos); @@ -65,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); } } @@ -82,6 +113,13 @@ public: return it->second; } + std::vector Targets() const override { + std::vector result; + result.reserve(_targets.size()); + for (const auto& entry : _targets) result.push_back(entry.second); + return result; + } + private: std::unordered_map _targets; }; @@ -101,21 +139,29 @@ std::unique_ptr BuildRelativeRelocationIndex( std::uint16_t phCount = read16(elfBytes, 56); if (phEntSize < 56) throw RelinkerException("ELF program header entry too small"); + if (phOff > elfBytes.size() || phCount > (elfBytes.size() - phOff) / phEntSize) + throw RelinkerException("Program header out of bounds"); std::vector loads; + std::uint64_t sceTableBase = 0; + bool hasSceDynlibData = false; for (std::uint16_t i = 0; i < phCount; ++i) { std::size_t phPos = static_cast(phOff) + i * phEntSize; if (phPos + 56 > elfBytes.size()) throw RelinkerException("Program header out of bounds"); std::uint32_t type = read32(elfBytes, phPos); - if (type != PT_LOAD) continue; - LoadSegment seg; - seg.fileOffset = read64(elfBytes, phPos + 8); - seg.vaddr = read64(elfBytes, phPos + 16); - seg.fileSize = read64(elfBytes, phPos + 32); - loads.push_back(seg); + if (type == PT_SCE_DYNLIBDATA) { + if (hasSceDynlibData) throw RelinkerException("Multiple PT_SCE_DYNLIBDATA segments", phPos); + hasSceDynlibData = true; + sceTableBase = read64(elfBytes, phPos + 8); + } + } + for (const auto& header : ElfReader(elfBytes).ReadProgramHeaders()) { + if (header.Type != PT_LOAD) continue; + loads.push_back({header.MappedAddress, header.Offset, header.FileSize}); } std::unordered_map targets; + bool hasDynamic = false; for (std::uint16_t i = 0; i < phCount; ++i) { std::size_t phPos = static_cast(phOff) + i * phEntSize; @@ -123,35 +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 + segSz > elfBytes.size()) throw RelinkerException("PT_DYNAMIC segment out of bounds"); + 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; + 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 + 16 <= segSz; off += 16) { + 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) relaVa = val; - if (tag == DT_RELASZ || tag == DT_OS_RELASZ) relaSz = val; - if (tag == DT_JMPREL || tag == DT_OS_JMPREL) jmprelVa = val; - if (tag == DT_PLTRELSZ || tag == DT_OS_PLTRELSZ) pltrelsz = val; + 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; + } + 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; + } + 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, targets); - collectRelativeEntries(elfBytes, loads, jmprelVa, pltrelsz, 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 9561c3dc98..92b5f148e1 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp @@ -3,7 +3,7 @@ #include #include #include -#include +#include #include #include @@ -25,6 +25,11 @@ public: requireRange(segment.Offset, segment.FileSize); if (segment.MemorySize > std::numeric_limits::max() - segment.MappedAddress) throw RelinkerException("Strict filter: segment address overflow", segment.MappedAddress); + if (segment.Type == 0x61000000) { + if (hasSceDynlibData) throw RelinkerException("Strict filter: multiple PT_SCE_DYNLIBDATA segments", segment.Offset); + hasSceDynlibData = true; + sceTableBase = segment.Offset; + } if (segment.Type == 1 && segment.FileSize > segment.MemorySize) throw RelinkerException("Strict filter: invalid load segment size", segment.Offset); if (segment.Type != 2) continue; @@ -74,7 +79,12 @@ public: const auto slot = read(*rela + offset); const auto info = read(*rela + offset + 8); if (static_cast(info) != 8) continue; - if (info >> 32) throw RelinkerException("Strict filter: relative relocation has a symbol", slot); + if (const auto symbol = info >> 32; symbol != 0) { + const auto symbols = table(6, 0x61000039); + const auto size = tags.find(0x6100003F); + if (!symbols || size == tags.end() || symbol >= size->second / 24 || (read(*symbols + symbol * 24 + 4) & 15) != 3 || read(*symbols + symbol * 24 + 8) != 0) + throw RelinkerException("Strict filter: relative relocation has a symbol", slot); + } const auto target = read(*rela + offset + 16); if (input.ImportSlots.contains(slot) || !input.Pointers.emplace(slot, target).second) throw RelinkerException("Strict filter: overlapping pointer relocations", slot); @@ -88,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; @@ -136,6 +146,8 @@ private: ElfReader reader; std::vector headers; std::map tags; + std::uint64_t sceTableBase = 0; + bool hasSceDynlibData = false; void requireRange(std::uint64_t offset, std::uint64_t size) const { if (offset > bytes.size() || size > bytes.size() - offset) throw RelinkerException("Strict filter: file range out of bounds", offset); @@ -168,13 +180,19 @@ private: std::optional table(std::int64_t standardTag, std::int64_t osTag) const { const auto address = value(standardTag, osTag); if (!address) return std::nullopt; - return tags.contains(osTag) ? *address : fileOffset(*address, 0); + if (!tags.contains(osTag)) return fileOffset(*address, 0); + if (*address > std::numeric_limits::max() - sceTableBase) + throw RelinkerException("Strict filter: SCE table file offset overflow", *address); + return sceTableBase + *address; } }; class StrictNidFilter : public IUnusedNidFilter { + Domain::IProgress& progress; + public: - std::vector Filter(const std::vector& nidRefs, const std::vector& elfBytes, const std::vector& textSection, VirtualAddress textVAddr) override { + explicit StrictNidFilter(Domain::IProgress& progress) : progress(progress) {} + std::vector Filter(const std::vector& nidRefs, const std::vector& elfBytes, const std::vector& textSection, VirtualAddress textVAddr, const std::vector&) override { const StrictImage image(elfBytes); const auto input = image.Build(nidRefs, textSection, textVAddr); const auto analysis = UnusedNidFilter::AnalyzeStrictReachability(input); @@ -182,16 +200,16 @@ public: for (const auto& reference : nidRefs) { if (analysis.ImportSlots.contains(reference.RelocationAddress)) result.push_back(reference); } - std::cout << "Strict reachability: " << analysis.Instructions.size() << " instructions; conservative indirect transfers=" << analysis.IndirectTransfers << "\n"; - std::cout << "Function graph: " << analysis.LiveRegions << "/" << analysis.TotalRegions << " live regions; address-taken roots=" << analysis.AddressTakenRoots << "; unwind functions=" << input.Functions.size() << "\n"; + { std::ostringstream message; message << "Strict reachability: " << analysis.Instructions.size() << " instructions; conservative indirect transfers=" << analysis.IndirectTransfers << "\n"; progress.Message(message.str()); } + { std::ostringstream message; message << "Function graph: " << analysis.LiveRegions << "/" << analysis.TotalRegions << " live regions; address-taken roots=" << analysis.AddressTakenRoots << "; unwind functions=" << input.Functions.size() << "\n"; progress.Message(message.str()); } return result; } }; } -std::shared_ptr MakeStrictUnusedNidFilter() { - return std::make_shared(); +std::shared_ptr MakeStrictUnusedNidFilter(Domain::IProgress& progress) { + return std::make_shared(progress); } } diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp index 60dcf67b86..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() { @@ -163,7 +168,8 @@ private: if (info.HasRipRelativeDisp) { std::int32_t displacement; std::memcpy(&displacement, input.Text.data() + offset + info.RipRelativeDispOffset, sizeof(displacement)); - const auto target = next + static_cast(static_cast(displacement)); + auto target = next + static_cast(static_cast(displacement)); + if (info.AddressSize32) target = static_cast(target); if (input.ImportSlots.contains(target)) region.Imports.insert(target); if (!info.IsTwoByteOpcode && info.Opcode == 0x8D) { addAddressTaken(target); @@ -201,16 +207,8 @@ private: void collectRelativeTables() { for (const auto base : tableBases) { - for (const auto& data : input.Data) { - if (base < data.Address || base - data.Address >= data.Bytes.size()) continue; - for (auto offset = static_cast(base - data.Address); offset + 4 <= data.Bytes.size(); offset += 4) { - std::int32_t displacement; - std::memcpy(&displacement, data.Bytes.data() + offset, sizeof(displacement)); - const auto target = base + static_cast(static_cast(displacement)); - if (!isCode(target)) break; - addAddressTaken(target); - } - } + 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); } } }; @@ -221,4 +219,19 @@ StrictReachabilityResult AnalyzeStrictReachability(const StrictReachabilityInput return Analyzer(input).Run(); } +std::vector ReadRelativeTableTargets(const std::vector& data, VirtualAddress base, VirtualAddress textVaddr, std::size_t textSize) { + std::vector targets; + for (const auto& region : data) { + if (base < region.Address || base - region.Address >= region.Bytes.size()) continue; + for (auto offset = static_cast(base - region.Address); offset + 4 <= region.Bytes.size(); offset += 4) { + std::int32_t displacement; + std::memcpy(&displacement, region.Bytes.data() + offset, sizeof(displacement)); + const auto target = base + static_cast(static_cast(displacement)); + if (target < textVaddr || target - textVaddr >= textSize) break; + targets.push_back(target); + } + } + return targets; +} + } diff --git a/core/relinker/relinker/src/analysis/ValidationPolicy.cpp b/core/relinker/relinker/src/analysis/ValidationPolicy.cpp index 2f2f4f91bc..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, @@ -29,21 +20,14 @@ void ValidationPolicy::_initializeSupportedRelocationTypes() { R_X86_64_GOTPCREL, R_X86_64_32, R_X86_64_32S, + R_X86_64_DTPMOD64, + R_X86_64_DTPOFF64, + R_X86_64_TPOFF64, R_X86_64_GOTPCRELX, R_X86_64_REX_GOTPCRELX, }; } -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(); @@ -56,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 3e9d18c32e..a630010692 100644 --- a/core/relinker/relinker/src/guest/GuestImageReader.cpp +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -1,94 +1,20 @@ #include #include #include +#include #include #include #include #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 { - const auto fail = [&](const std::string& message) { throw Domain::RelinkerException(path.string() + ": " + message); }; +GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector bytes, const bool windows) const try { + const auto fail = [&](const std::string& message) { throw Domain::RelinkerException(Io::Utf8Path(path) + ": " + message); }; const auto range = [&](std::uint64_t offset, std::uint64_t size) { if (offset > bytes.size() || size > bytes.size() - offset) { std::ostringstream message; @@ -139,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) { @@ -147,8 +74,9 @@ 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 == 0x6100003f || (tag >= 0x61000027 && tag <= 0x6100003b)) { + else if (tag < 0x60000000 || tag == 0x6ffffef5 || tag == 0x6100003f || (tag >= 0x61000027 && tag <= 0x6100003b)) { if (!tags.emplace(tag, value).second) fail("Duplicate dynamic tag " + std::to_string(tag)); } } @@ -156,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; @@ -194,34 +123,69 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector std::uint64_t symSize = 0; if (tags.contains(0x6100003f)) symSize = tags.at(0x6100003f); else if (tags.contains(4)) symSize = static_cast(Io::ReadU32(bytes, translate(tags.at(4), 8) + 4)) * 24; - else fail("Missing dynamic symbol count"); + else if (tags.contains(0x6ffffef5)) { + const auto address = tags.at(0x6ffffef5); + const auto header = translate(address, 16); + const auto bucketCount = Io::ReadU32(bytes, header); + const auto firstSymbol = Io::ReadU32(bytes, header + 4); + const auto bloomCount = Io::ReadU32(bytes, header + 8); + if (bucketCount == 0 || bloomCount == 0 || firstSymbol == 0) fail("Invalid GNU hash header"); + const std::uint64_t bucketOffset = 16 + static_cast(bloomCount) * 8; + const auto chainOffset = bucketOffset + static_cast(bucketCount) * 4; + const auto start = translate(address, chainOffset); + std::uint64_t lastBucket = 0; + for (std::uint64_t index = 0; index < bucketCount; ++index) { + const auto bucket = Io::ReadU32(bytes, start + bucketOffset + index * 4); + if (bucket != 0 && bucket < firstSymbol) fail("Invalid GNU hash bucket"); + lastBucket = std::max(lastBucket, static_cast(bucket)); + } + std::uint64_t symbolCount = firstSymbol; + if (lastBucket != 0) { + symbolCount = lastBucket; + for (;;) { + const auto offset = chainOffset + (symbolCount - firstSymbol) * 4; + if (offset > std::numeric_limits::max() - address) fail("Invalid GNU hash chain"); + const auto chain = Io::ReadU32(bytes, translate(address + offset, 4)); + ++symbolCount; + if (chain & 1) break; + } + } + symSize = symbolCount * 24; + } else fail("Missing dynamic symbol count"); if (symSize == 0 || symSize % 24 != 0 || symSize / 24 > std::numeric_limits::max()) fail("Invalid dynamic symbol count"); const auto symOffset = table(6, 0x61000039, symSize); image.Dynamic.DynSymData.assign(bytes.begin() + symOffset, bytes.begin() + symOffset + symSize); 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)); - std::set exports; + std::map exports; for (std::uint64_t offset = 0; offset < symSize; offset += 24) { auto name = string(Io::ReadU32(bytes, symOffset + offset)); const auto library = Io::ReadU16(bytes, symOffset + offset + 6) == 0 ? Domain::ImportModule(name, importModules, neededLibraries) : std::string{}; + if (Io::ReadU16(bytes, symOffset + offset + 6) == 0 && library.empty()) { + if (auto declared = Domain::DeclaredImportModule(name, importModules); !declared.empty()) image.Dynamic.UnmatchedImportModules.insert(std::move(declared)); + } + const auto qualifiedName = name; name = name.substr(0, name.find('#')); const auto info = bytes[symOffset + offset + 4]; const auto visibility = bytes[symOffset + offset + 5]; const auto section = Io::ReadU16(bytes, symOffset + offset + 6); const auto value = Io::ReadU64(bytes, symOffset + offset + 8); const auto size = Io::ReadU64(bytes, symOffset + offset + 16); + const GuestSymbol symbol{name, info, visibility, section, value, size, library, qualifiedName}; if (offset == 0 && (info != 0 || visibility != 0 || section != 0 || value != 0 || size != 0 || !name.empty())) fail("Invalid null symbol"); if (offset == 0) { 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) { - if (name.empty() || !exports.insert(name).second) fail("Duplicate or empty export after stripping #: " + name); + 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); } if ((info & 15) == 6 && section != 0 && (tls == nullptr || value > tls->MemorySize || size > tls->MemorySize - value)) fail("TLS symbol exceeds TLS segment: " + name); if (section != 0 && section != AbsoluteSection && (info & 15) != 6) { @@ -231,7 +195,7 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector if (image.Dynamic.DynStrData.size() > std::numeric_limits::max()) fail("String table too large"); Io::WriteU32(image.Dynamic.DynSymData, offset, static_cast(image.Dynamic.DynStrData.size())); Io::AppendString(image.Dynamic.DynStrData, name); - image.Symbols.push_back({name, info, visibility, section, value, size, library}); + image.Symbols.push_back(symbol); } if (tags.contains(14)) { image.Soname = string(tags.at(14)); @@ -247,28 +211,52 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector std::set dependencies; for (const auto offset : needed) { const auto name = string(offset); - if (name.empty() || name.find_first_of("\\:$\r\n") != std::string::npos || !dependencies.insert(name).second) fail("Invalid or duplicate dependency: " + name); - image.Dependencies.push_back(name); + 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); - output.assign(bytes.begin() + offset, bytes.begin() + offset + size); + 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) { - const auto info = Io::ReadU64(output, index + 8); + 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; if ((info >> 32) >= image.Symbols.size() || (plt && relocation != 7) || (relocation != 1 && relocation != 6 && relocation != 7 && relocation != 8 && relocation != 16 && relocation != 17 && relocation != 18)) fail("Unsupported relocation or symbol index"); - const auto target = Io::ReadU64(output, index); - const auto addend = Io::ReadU64(output, index + 16); + const auto target = Io::ReadU64(bytes, offset + index); + const auto addend = Io::ReadU64(bytes, offset + index + 16); mapped(target, 8, 2); 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); 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]; @@ -277,18 +265,18 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector 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"); } + output.insert(output.end(), bytes.begin() + offset + index, bytes.begin() + offset + index + 24); } }; - 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); - if (!image.Dynamic.RelaPltData.empty() && get(20, 0x6100002b) != 7) fail("Unsupported PLT relocation format"); + 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; @@ -322,9 +310,12 @@ 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) { + if (error.InputPath.empty()) error.InputPath = Io::Utf8Path(path); + throw; } } diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index ba1f29a03a..1f9eed1e66 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -1,19 +1,32 @@ #include +#include +#include #include #include -#include +#include +#include #include #include -#include #include -#include #include +#include #include +#include +#include +#include +#include +#include +#include namespace Relinker { namespace { +struct GuestExport { + std::size_t Provider; + std::uint8_t Type; +}; + std::vector ReadNeededNames(const Domain::SysVDynamicSection& dynamic) { if (dynamic.DynamicSegmentData.size() % 16 != 0) throw Domain::RelinkerException("Invalid executable dependency table"); std::vector names; @@ -29,160 +42,110 @@ std::vector ReadNeededNames(const Domain::SysVDynamicSection& dynam return names; } +std::string FoldFilename(std::string name) { + for (auto& character : name) if (character >= 'A' && character <= 'Z') character = static_cast(character + ('a' - 'A')); + return name; } -std::vector GuestModuleBuilder::Build(const std::filesystem::path& inputPath, const std::filesystem::path& outputPath, Domain::SysVDynamicSection& dynamic, const bool windows, const bool macos, const bool toIntel, ISyscallScanner& syscallScanner, const bool lazyBinding, const std::string& runPath, const std::set& excludedModules) const { - const auto root = std::filesystem::absolute(inputPath).parent_path(); - const auto singular = root / "sce_module"; - const auto plural = root / "sce_modules"; - const auto prx = root / "prx"; - const bool hasSingular = std::filesystem::exists(singular); - const bool hasPlural = std::filesystem::exists(plural); - const bool hasPrx = std::filesystem::exists(prx); - if (hasSingular && hasPlural) throw Domain::RelinkerException("Both sce_module and sce_modules exist beside the input executable"); - if (!hasSingular && !hasPlural && !hasPrx) throw Domain::RelinkerException("sce_module/sce_modules/prx was not found beside the input executable: " + root.string() + ". Use --skip-sce-module only if this game can run without these modules."); - std::vector directories; - if (hasSingular || hasPlural) directories.push_back(hasSingular ? singular : plural); - if (hasPrx) directories.push_back(prx); +std::string ModuleStem(std::string name, const bool windows) { + if (windows) name = FoldFilename(std::move(name)); + for (const std::string_view suffix : {".debug_prx", ".suprx", ".sprx", ".prx"}) { + if (name.size() > suffix.size() && name.ends_with(suffix)) return name.substr(0, name.size() - suffix.size()); + } + return {}; +} + +} + +std::vector GuestModuleBuilder::Build(const std::filesystem::path& inputPath, const std::filesystem::path& outputPath, Domain::SysVDynamicSection& dynamic, const bool windows, const bool macos, const bool toIntel, const bool toRosetta, ISyscallScanner& syscallScanner, const bool lazyBinding, const std::string& runPath, const std::set& excludedModules, const std::filesystem::path& root, const std::vector& runtimeDirectories, std::vector modules, Domain::IProgress& progress) const { std::vector paths; - std::set unmatchedExclusions = excludedModules; - const auto isElf = [](const std::filesystem::path& path) { - std::ifstream stream(path, std::ios::binary); - if (!stream) throw Domain::RelinkerException("Cannot read guest candidate: " + path.string()); - char magic[4]{}; - stream.read(magic, 4); - if (stream.bad()) throw Domain::RelinkerException("Cannot read guest candidate magic: " + path.string()); - return stream.gcount() == 4 && static_cast(magic[0]) == 0x7f && magic[1] == 'E' && magic[2] == 'L' && magic[3] == 'F'; - }; - for (const auto& directory : directories) { - if (!std::filesystem::is_directory(directory)) throw Domain::RelinkerException("Guest module path is not a directory: " + directory.string()); - for (const auto& entry : std::filesystem::directory_iterator(directory)) { - if (entry.path().filename().string().ends_with(GuestModuleSuffix)) continue; - if (excludedModules.contains(entry.path().filename().string())) { - unmatchedExclusions.erase(entry.path().filename().string()); - continue; - } - if (!entry.is_regular_file()) continue; - if (isElf(entry.path())) paths.push_back(entry.path()); - } + std::map discovered; + progress.Report({Domain::ProgressStage::Modules, {}, 0, modules.size()}); + for (auto& module : modules) { + const auto path = module.Path.lexically_normal(); + if (path == inputPath.lexically_normal()) throw Domain::RelinkerException("Module path collides with the input executable: " + Io::Utf8Path(path)); + if (path == outputPath.lexically_normal()) throw Domain::RelinkerException("Executable output would overwrite an input module: " + Io::Utf8Path(path)); + const auto relative = path.lexically_relative(root); + if (relative.empty() || relative.has_root_path() || *relative.begin() == "..") throw Domain::RelinkerException("Module is outside module root: " + Io::Utf8Path(path)); + if (excludedModules.contains(Io::Utf8Path(path.filename()))) throw Domain::RelinkerException("Excluded module supplied to converter: " + Io::Utf8Path(path)); + if (discovered.contains(path)) throw Domain::RelinkerException("Duplicate module path: " + Io::Utf8Path(path)); + paths.push_back(path); + 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; + std::map neededAliases; for (const auto& name : neededNames) { if (excludedModules.contains(name)) continue; - if (std::none_of(paths.begin(), paths.end(), [&](const auto& path) { return path.filename().string() == name; })) missingNeeded.insert(name); + if (std::any_of(paths.begin(), paths.end(), [&](const auto& path) { return Io::Utf8Path(path.filename()) == name || (windows && FoldFilename(Io::Utf8Path(path.filename())) == FoldFilename(name)); })) continue; + const auto stem = ModuleStem(name, windows); + const auto alias = stem.empty() ? paths.end() : std::find_if(paths.begin(), paths.end(), [&](const auto& path) { return ModuleStem(Io::Utf8Path(path.filename()), windows) == stem; }); + if (alias != paths.end()) neededAliases.emplace(name, *alias); + else missingNeeded.insert(name); } - if (!missingNeeded.empty()) { - std::map found; - for (auto it = std::filesystem::recursive_directory_iterator(root); it != std::filesystem::recursive_directory_iterator(); ++it) { - if (it->is_directory() && std::find(directories.begin(), directories.end(), it->path()) != directories.end()) { - it.disable_recursion_pending(); - continue; - } - const auto name = it->path().filename().string(); - if (!it->is_regular_file() || !missingNeeded.contains(name) || !isElf(it->path())) continue; - if (!found.emplace(name, it->path()).second) throw Domain::RelinkerException("Ambiguous needed module: " + found.at(name).string() + " and " + it->path().string()); - } - for (const auto& [name, path] : found) paths.push_back(path); - } - Io::FileReader reader; - std::map discovered; const auto matchesIdentity = [](const std::string& name, const std::vector& identities) { - return std::any_of(identities.begin(), identities.end(), [&](const auto& identity) { - return name == identity || name == identity + ".prx" || name == identity + ".sprx" || name == identity + ".suprx"; - }); + const auto stem = std::string_view(name).substr(0, name.size() - Domain::ModuleSuffix(name).size()); + return std::any_of(identities.begin(), identities.end(), [&](const auto& identity) { return name == identity || stem == identity; }); }; - const auto foldFilename = [](std::string name) { - for (auto& character : name) if (character >= 'A' && character <= 'Z') character = static_cast(character + ('a' - 'A')); - return name; - }; - for (const auto& path : paths) discovered.emplace(path, GuestImageReader().Read(path, reader.Read(path.string()))); - std::set unresolved; - for (const auto& name : missingNeeded) { - if (std::none_of(discovered.begin(), discovered.end(), [&](const auto& entry) { - const auto& [path, image] = entry; - return path.filename().string() == name || image.Soname == name || - (windows && foldFilename(path.filename().string()) == foldFilename(name)) || matchesIdentity(name, image.ModuleNames); - })) unresolved.insert(name); - } - if (!unresolved.empty()) { - std::map identities; - for (auto it = std::filesystem::recursive_directory_iterator(root); it != std::filesystem::recursive_directory_iterator(); ++it) { - if (it->is_directory() && std::find(directories.begin(), directories.end(), it->path()) != directories.end()) { - it.disable_recursion_pending(); - continue; - } - const auto& path = it->path(); - const auto extension = path.extension().string(); - if (!it->is_regular_file() || (extension != ".prx" && extension != ".sprx" && extension != ".suprx") || - path.filename().string().ends_with(GuestModuleSuffix) || excludedModules.contains(path.filename().string()) || discovered.contains(path) || !isElf(path)) continue; - const auto names = GuestImageReader().ReadModuleNames(reader.Read(path.string())); - for (const auto& name : unresolved) { - if (!matchesIdentity(name, names)) continue; - if (!identities.emplace(name, path).second) throw Domain::RelinkerException("Ambiguous needed module identity: " + name); - } - } - for (const auto& [name, path] : identities) { - if (discovered.contains(path)) continue; - discovered.emplace(path, GuestImageReader().Read(path, reader.Read(path.string()))); - paths.push_back(path); - } - } - if (!unmatchedExclusions.empty()) throw Domain::RelinkerException("Excluded guest module file not found: " + *unmatchedExclusions.begin()); std::sort(paths.begin(), paths.end()); paths.erase(std::unique(paths.begin(), paths.end()), paths.end()); if (paths.empty()) return {}; if (lazyBinding) throw Domain::RelinkerException("Guest modules require eager binding; --lazy-binding is incompatible"); std::vector images; - std::map> exports; + std::unordered_map> exports; std::map> sharedExports; 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 = std::find(providers.begin(), providers.end(), images.size()) != providers.end(); - if (!windows && repeated) throw Domain::RelinkerException("Duplicate guest export after stripping #: " + symbol.Name + " in " + path.string() + " and " + path.string()); - if (!repeated) providers.push_back(images.size()); - if (!windows && providers.size() > 1) sharedExports[symbol.Name].insert(providers.begin(), providers.end()); - } - std::vector codeHeaders; - for (const auto& header : image.Headers) if (header.Type == 1 && (header.Flags & 1) != 0) codeHeaders.push_back(header); - if (toIntel) { - auto converted = Codegen::MakeAmd64OnlyConverter()->Convert(std::move(image.Bytes), codeHeaders); - image.Trampolines = std::move(converted.Trampolines); - image.Bytes = std::move(converted.Bytes); - } - for (const auto& header : codeHeaders) { - const std::vector code(image.Bytes.begin() + header.Offset, image.Bytes.begin() + header.Offset + header.FileSize); - syscallScanner.ScanCodeSectionForSyscalls(code, header.MappedAddress, header.FileSize); + const bool repeated = !providers.empty() && providers.back().Provider == images.size(); + 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); } images.push_back(std::move(image)); } std::map guestNames; std::map windowsGuestFiles; for (std::size_t index = 0; index < images.size(); ++index) { - for (const auto& name : {images[index].SourcePath.filename().string(), images[index].Soname}) { + for (const auto& name : {Io::Utf8Path(images[index].SourcePath.filename()), images[index].Soname}) { if (name.empty()) continue; const auto [found, inserted] = guestNames.emplace(name, index); if (!inserted && found->second != index) throw Domain::RelinkerException("Ambiguous guest dependency name: " + name); } - if (windows) windowsGuestFiles.emplace(foldFilename(images[index].SourcePath.filename().string()), index); + if (windows) windowsGuestFiles.emplace(FoldFilename(Io::Utf8Path(images[index].SourcePath.filename())), index); + } + for (const auto& [name, path] : neededAliases) { + if (guestNames.contains(name)) continue; + const auto image = std::find_if(images.begin(), images.end(), [&](const auto& candidate) { return candidate.SourcePath == path; }); + if (image == images.end()) throw Domain::RelinkerException("Needed module alias has no guest image: " + name); + const auto index = static_cast(image - images.begin()); + const auto [found, inserted] = guestNames.emplace(name, index); + if (!inserted && found->second != index) throw Domain::RelinkerException("Ambiguous guest dependency name: " + name); } const auto findGuest = [&](const std::string& name) { const auto exact = guestNames.find(name); if (exact != guestNames.end() || !windows) return exact; - const auto file = windowsGuestFiles.find(foldFilename(name)); + const auto file = windowsGuestFiles.find(FoldFilename(name)); return file == windowsGuestFiles.end() ? guestNames.end() : guestNames.emplace(name, file->second).first; }; const auto resolveIdentity = [&](const std::string& name) { @@ -202,7 +165,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path const auto shared = sharedExports.find(name); if (shared == sharedExports.end()) return; std::string providers; - for (const auto provider : shared->second) providers += " " + images[provider].SourcePath.string(); + for (const auto provider : shared->second) providers += " " + Io::Utf8Path(images[provider].SourcePath); throw Domain::RelinkerException("Ambiguous guest import " + name + " in " + importer + ": exported by" + providers); }; const auto rename = [](std::vector& symbols, std::vector& strings, std::size_t index, const std::string& name) { @@ -210,7 +173,21 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path Io::WriteU32(symbols, index * 24, static_cast(strings.size())); Io::AppendString(strings, name + GuestSymbolSuffix); }; + 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 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; + } + } for (std::size_t offset = 0; offset < dynamic.DynSymData.size(); offset += 24) { if (Io::ReadU16(dynamic.DynSymData, offset + 6) != 0) continue; const auto nameOffset = Io::ReadU32(dynamic.DynSymData, offset); @@ -219,8 +196,25 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path const auto end = std::find(start, dynamic.DynStrData.end(), 0); if (end == dynamic.DynStrData.end()) throw Domain::RelinkerException("Unterminated executable symbol name"); const std::string name(start, end); - rejectSharedImport(name.substr(0, name.find('#')), inputPath.string()); - if (!windows && exports.contains(name)) rename(dynamic.DynSymData, dynamic.DynStrData, offset / 24, name); + if (declaresHost(executableModules[offset / 24])) continue; + const std::string base = name.substr(0, name.find('#')); + const auto shared = sharedExports.find(base); + std::size_t needed = 0; + if (shared != sharedExports.end()) { + for (const auto provider : shared->second) { + const auto& file = images[provider].SourcePath.filename().string(); + const auto& soname = images[provider].Soname; + if (std::find(neededNames.begin(), neededNames.end(), file) == neededNames.end() && + (soname.empty() || std::find(neededNames.begin(), neededNames.end(), soname) == neededNames.end())) continue; + if (++needed > 1) break; + } + } + if (needed != 1) rejectSharedImport(base, Io::Utf8Path(inputPath)); + 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()); @@ -234,41 +228,78 @@ 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()) continue; - rejectSharedImport(symbol.Name, images[index].SourcePath.string()); + 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)) + rejectSharedImport(symbol.Name, Io::Utf8Path(images[index].SourcePath)); const auto found = exports.find(symbol.Name); - std::vector providers; + const GuestExport* provider = nullptr; if (found != exports.end()) { - for (const auto provider : found->second) { - const auto declared = findGuest(symbol.Library); - if (!windows || symbol.Library.empty() || (declared != guestNames.end() && declared->second == provider)) providers.push_back(provider); + for (const auto& candidate : found->second) { + if (!symbol.Library.empty() && (declared == guestNames.end() || declared->second != candidate.Provider)) continue; + if (provider != nullptr) throw Domain::RelinkerException("Ambiguous guest import after stripping #: " + symbol.Name); + provider = &candidate; } } - if (providers.size() > 1) throw Domain::RelinkerException("Ambiguous guest import after stripping #: " + symbol.Name); - if (!providers.empty()) { - const auto& provider = images[providers.front()]; - const auto exported = std::find_if(provider.Symbols.begin(), provider.Symbols.end(), [&](const auto& candidate) { return candidate.Section != 0 && candidate.Section != AbsoluteSection && candidate.Name == symbol.Name && (candidate.Info >> 4) != 0 && candidate.Visibility != 1 && candidate.Visibility != 2; }); - if (exported == provider.Symbols.end() || ((symbol.Info & 15) != 0 && (symbol.Info & 15) != (exported->Info & 15))) throw Domain::RelinkerException("Guest import/export type mismatch: " + symbol.Name); - if (providers.front() != index) dependencies[index].insert(providers.front()); + 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 && 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 (providers.empty()) systemImports[index].insert(symbolIndex); + 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(); + }; + for (const auto& name : neededNames) { + const auto found = findGuest(name); + if (found != guestNames.end()) 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); + } + const auto deferred = [&](const std::size_t index) { + return !required[index] && isRuntime(index); + }; + const auto appDirectory = [&](const GuestImage& image) { + const auto directory = image.SourcePath.parent_path().lexically_relative(root).generic_string(); + return directory == "." ? std::string("app0") : "app0/" + directory; + }; if (!windows) { for (auto& image : images) { for (std::size_t index = 1; index < image.Symbols.size(); ++index) { const auto& symbol = image.Symbols[index]; const bool exported = symbol.Section != 0 && (symbol.Info >> 4) != 0 && symbol.Visibility != 1 && symbol.Visibility != 2; - if ((symbol.Section == 0 || exported) && exports.contains(symbol.Name)) rename(image.Dynamic.DynSymData, image.Dynamic.DynStrData, index, symbol.Name); + if (((symbol.Section == 0 && !declaresHost(symbol.Library)) || exported) && exports.contains(symbol.Name)) rename(image.Dynamic.DynSymData, image.Dynamic.DynStrData, index, symbol.Name); } } } std::vector order; std::vector states(images.size()); const std::function visit = [&](std::size_t index) { - if (states[index] == 1) throw Domain::RelinkerException("Cyclic guest initialization dependency: " + images[index].SourcePath.string()); + if (states[index] == 1) throw Domain::RelinkerException("Cyclic guest initialization dependency: " + Io::Utf8Path(images[index].SourcePath)); if (states[index] == 2) return; states[index] = 1; for (const auto dependency : dependencies[index]) visit(dependency); @@ -277,52 +308,99 @@ 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); Io::AppendU64(dynamic.DynamicSegmentData, dynamic.DynStrData.size()); Io::AppendString(dynamic.DynStrData, name); }; - for (const auto index : order) if (!windows) addNeeded("$ORIGIN/app0/" + images[index].SourcePath.parent_path().lexically_relative(root).generic_string() + "/" + images[index].OutputName); + for (const auto index : order) if (!windows && !deferred(index)) addNeeded("$ORIGIN/" + appDirectory(images[index]) + "/" + images[index].OutputName); for (const auto& name : hostLibraries) addNeeded(name); - std::string guestRunPath = runPath; - if (!windows) { - if (guestRunPath == "$ORIGIN") guestRunPath = "$ORIGIN/../.."; - else if (guestRunPath.starts_with("$ORIGIN/")) guestRunPath.insert(8, "../../"); - else if (!std::filesystem::path(guestRunPath).is_absolute()) throw Domain::RelinkerException("Guest Linux run path must be absolute or begin with $ORIGIN"); - } + if (!windows && runPath != "$ORIGIN" && !runPath.starts_with("$ORIGIN/") && !runPath.starts_with("$ORIGIN:") && !runPath.starts_with('/')) throw Domain::RelinkerException("Guest Linux run path must be absolute or begin with $ORIGIN"); + const auto outputDirectory = outputPath.parent_path().lexically_normal(); std::vector artifacts; for (const auto index : order) { - const auto& image = images[index]; - const auto relativeDirectory = "app0/" + image.SourcePath.parent_path().lexically_relative(root).generic_string(); - const auto destination = std::filesystem::absolute(outputPath).parent_path() / relativeDirectory; + auto& image = images[index]; + progress.Report({Domain::ProgressStage::Modules, Io::Utf8Path(image.SourcePath), artifacts.size(), order.size()}); + std::vector codeHeaders; + for (const auto& header : image.Headers) if (header.Type == 1 && (header.Flags & 1) != 0) codeHeaders.push_back(header); + if (toIntel) { + auto converted = Codegen::MakeAmd64OnlyConverter(toRosetta ? Codegen::Amd64OnlyTarget::Rosetta : Codegen::Amd64OnlyTarget::Intel, [&](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) { + 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; + } + } + const auto relativeDirectory = appDirectory(image); + const auto destination = outputDirectory / relativeDirectory; const auto target = destination / image.OutputName; - if (target.lexically_normal() == std::filesystem::absolute(outputPath).lexically_normal()) throw Domain::RelinkerException("Guest output collides with the executable output"); - for (const auto& source : paths) if (std::filesystem::exists(target) && std::filesystem::equivalent(source, target)) throw Domain::RelinkerException("Guest output would overwrite an input module: " + target.string()); - if (std::filesystem::exists(target) && std::filesystem::equivalent(inputPath, target)) throw Domain::RelinkerException("Guest output would overwrite the input executable"); + if (target.lexically_normal() == outputPath.lexically_normal()) throw Domain::RelinkerException("Guest output collides with the executable output"); Domain::GuestRuntime runtime; runtime.UsePlatformTlsResolver = image.UsePlatformTlsResolver; runtime.Path = relativeDirectory + "/" + image.OutputName; - runtime.Names = {image.SourcePath.filename().string(), image.Soname}; + 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(); + for (std::size_t position = 0; (position = guestRunPath.find("$ORIGIN", position)) != std::string::npos;) { + position += 7; + if (position != guestRunPath.size() && guestRunPath[position] != '/' && guestRunPath[position] != ':') continue; + guestRunPath.insert(position, "/" + relativeRoot); + position += relativeRoot.size() + 1; + } std::vector needed; for (const auto dependency : dependencies[index]) { const auto dependencyPath = images[dependency].SourcePath.parent_path() / images[dependency].OutputName; needed.push_back("$ORIGIN/" + dependencyPath.lexically_relative(image.SourcePath.parent_path()).generic_string()); } - needed.insert(needed.end(), hostLibraries.begin(), hostLibraries.end()); - 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) { @@ -332,14 +410,20 @@ 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)}); + 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 d3ad5fee49..0d8bdd763f 100644 --- a/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp +++ b/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp @@ -68,6 +68,7 @@ SysVDynamicSection SysVDynamicSectionBuilder::BuildDynamicSection( std::uint32_t originalJmprelCount) { SysVDynamicSection result; + result.NeededLibraries = neededLibraries; result.DynStrData.push_back(0); std::vector neededOffsets; @@ -82,6 +83,13 @@ SysVDynamicSection SysVDynamicSectionBuilder::BuildDynamicSection( return value.substr(0, hashPos); }; + auto symbolInfo = [](const NidReference& ref) -> std::uint8_t { + if (ref.Nid.empty()) return static_cast((STB_WEAK << 4) | STT_NOTYPE); + const bool tls = ref.RelocationTypeValue == R_X86_64_DTPMOD64 || ref.RelocationTypeValue == R_X86_64_DTPOFF64 + || ref.RelocationTypeValue == R_X86_64_TPOFF64; + return static_cast(((ref.Weak ? STB_WEAK : STB_GLOBAL) << 4) | (tls ? STT_TLS : STT_FUNC)); + }; + static constexpr std::size_t kRelaEntSize = 24; std::vector pltSlots(originalJmprelCount, nullptr); @@ -89,6 +97,7 @@ SysVDynamicSection SysVDynamicSectionBuilder::BuildDynamicSection( for (const auto& ref : nidReferences) { if (!ref.Library.empty()) result.ImportModules.emplace(ref.RelocationAddress, ref.Library); + if (ref.Function) result.FunctionImports.insert(ref.RelocationAddress); std::uint32_t relType = ref.RelocationTypeValue; if (relType == 0) relType = R_X86_64_JUMP_SLOT; @@ -132,8 +141,7 @@ SysVDynamicSection SysVDynamicSectionBuilder::BuildDynamicSection( const NidReference& ref = *slot; const std::uint32_t nameOff = _appendStr(result.DynStrData, stripHashSuffix(ref.Nid)); - const auto info = static_cast((STB_GLOBAL << 4) | STT_FUNC); - _appendElfSym(result.DynSymData, nameOff, info, STV_DEFAULT, 0, 0, 0); + _appendElfSym(result.DynSymData, nameOff, symbolInfo(ref), STV_DEFAULT, 0, 0, 0); const std::uint64_t relaInfo = (static_cast(symIdx) << 32) | R_X86_64_JUMP_SLOT; _appendRela(result.RelaPltData, ref.RelocationAddress, relaInfo, ref.Addend); @@ -143,8 +151,7 @@ SysVDynamicSection SysVDynamicSectionBuilder::BuildDynamicSection( for (const NidReference* slotPtr : nonPltRefs) { const NidReference& ref = *slotPtr; const std::uint32_t nameOff = _appendStr(result.DynStrData, stripHashSuffix(ref.Nid)); - const auto info = static_cast((STB_GLOBAL << 4) | STT_FUNC); - _appendElfSym(result.DynSymData, nameOff, info, STV_DEFAULT, 0, 0, 0); + _appendElfSym(result.DynSymData, nameOff, symbolInfo(ref), STV_DEFAULT, 0, 0, 0); const std::uint64_t relaInfo = (static_cast(symIdx) << 32) | ref.RelocationTypeValue; _appendRela(result.RelaData, ref.RelocationAddress, relaInfo, ref.Addend); diff --git a/core/relinker/relinker/src/parsing/ElfReader.cpp b/core/relinker/relinker/src/parsing/ElfReader.cpp index 84b845c19a..0fc55afebd 100644 --- a/core/relinker/relinker/src/parsing/ElfReader.cpp +++ b/core/relinker/relinker/src/parsing/ElfReader.cpp @@ -1,7 +1,10 @@ #include #include #include +#include +#include #include +#include namespace Relinker { @@ -24,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; @@ -83,6 +90,14 @@ ElfHeader ElfReader::ReadHeader() const { _fileBuffer[2] != 'L' || _fileBuffer[3] != 'F') { throw RelinkerException("Invalid ELF magic number: " + formatMagic()); } + if (_fileBuffer[4] != 2 || _fileBuffer[5] != 1 || _fileBuffer[6] != 1) + throw RelinkerException("Expected little-endian ELF64 version 1"); + if (_readU16At(0x12) != 62) + throw RelinkerException("Expected an x86-64 ELF"); + if (_readU32At(0x14) != 1) + throw RelinkerException("Unsupported ELF object version: expected 1", 0x14); + if (_readU16At(0x34) != 64) + throw RelinkerException("Invalid ELF header size: expected 64 bytes", 0x34); ElfHeader header{}; header.Machine = _readU16At(0x12); @@ -121,15 +136,70 @@ std::vector ElfReader::ReadProgramHeaders() const { ph.MemorySize = _readU64At(offset + 0x28); ph.Alignment = _readU64At(offset + 0x30); + if (ph.Type == PT_LOAD && ph.FileSize > ph.MemorySize) + throw RelinkerException("PT_LOAD file size exceeds memory size", ph.Offset); + 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; } + for (auto& relro : headers) { + if (relro.Type != PT_OS_RELRO || relro.MemorySize == 0) continue; + bool covered = false; + for (const auto& load : headers) { + if (load.Type != PT_LOAD || load.MemorySize == 0) continue; + const bool overlaps = relro.MappedAddress < load.MappedAddress ? load.MappedAddress - relro.MappedAddress < relro.MemorySize : relro.MappedAddress - load.MappedAddress < load.MemorySize; + if (!overlaps) continue; + if (relro.MappedAddress < load.MappedAddress || relro.MemorySize > load.MemorySize - (relro.MappedAddress - load.MappedAddress)) + throw RelinkerException("PT_SCE_RELRO partially overlaps a PT_LOAD segment", relro.MappedAddress); + 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; } std::vector ElfReader::ReadSectionHeaders() const { const ElfHeader header = ReadHeader(); + if (header.SectionHeaderCount != 0 && header.SectionHeaderEntrySize != kShdrEntrySize) { + throw RelinkerException("Invalid ELF section header entry size: expected 64 bytes", 0x3a); + } + const ByteCount tableSize = static_cast(header.SectionHeaderCount) * header.SectionHeaderEntrySize; + if (!_rangeFits(header.SectionHeaderOffset, tableSize, _fileBuffer.size())) { + throw RelinkerException("Section header table out of bounds", header.SectionHeaderOffset); + } + if (header.SectionHeaderStringIndex != 0 && header.SectionHeaderStringIndex >= header.SectionHeaderCount) { + throw RelinkerException("Section header string table index out of bounds", 0x3e); + } std::vector headers; FileByteOffset offset = header.SectionHeaderOffset; @@ -144,7 +214,7 @@ std::vector ElfReader::ReadSectionHeaders() const { sh.SectionSize = _readU64At(offset + 0x20); sh.Link = _readU32At(offset + 0x28); sh.Info = _readU32At(offset + 0x2c); - sh.EntrySize = _readU64At(offset + 0x30); + sh.EntrySize = _readU64At(offset + kShdrEntSizeOffset); sh.Name = _resolveShdrName(nameOffset, header); @@ -163,17 +233,27 @@ std::string ElfReader::_resolveShdrName(std::uint32_t nameOffset, const ElfHeade FileByteOffset shstrOffset = header.SectionHeaderOffset + (header.SectionHeaderStringIndex * header.SectionHeaderEntrySize); - const FileByteOffset strTableOffset = _readU64At(shstrOffset + 0x18); - - std::string name; - FileByteOffset currentPos = strTableOffset + nameOffset; - - while (currentPos < _fileBuffer.size() && _fileBuffer[currentPos] != '\0') { - name += static_cast(_fileBuffer[currentPos]); - currentPos++; + if (_readU32At(shstrOffset + kShdrTypeOffset) != SHT_STRTAB) { + throw RelinkerException("Section names do not reference a string table", shstrOffset); } - - return name; + const FileByteOffset strTableOffset = _readU64At(shstrOffset + 0x18); + const ByteCount strTableSize = _readU64At(shstrOffset + kShdrSizeOffset); + if (!_rangeFits(strTableOffset, strTableSize, _fileBuffer.size())) { + throw RelinkerException("Section name string table out of bounds", strTableOffset); + } + if (strTableSize == 0 && nameOffset == 0) { + return ""; + } + if (nameOffset >= strTableSize) { + throw RelinkerException("Section name offset out of bounds", nameOffset); + } + const auto first = _fileBuffer.begin() + strTableOffset + nameOffset; + const auto end = _fileBuffer.begin() + strTableOffset + strTableSize; + const auto terminator = std::find(first, end, 0); + if (terminator == end) { + throw RelinkerException("Unterminated section name", strTableOffset + nameOffset); + } + return std::string(first, terminator); } std::vector ElfReader::ReadDynamicTags(const ProgramHeader& dynamicHeader) const { @@ -205,36 +285,15 @@ std::vector ElfReader::ReadDynamicTags(const ProgramHeader& dynamicH } FileByteOffset ElfReader::TranslateVirtualAddress(VirtualAddress address) const { - const ElfHeader header = ReadHeader(); - - FileByteOffset offset = header.ProgramHeaderOffset; - - for (std::uint16_t i = 0; i < header.ProgramHeaderCount; ++i) { - const std::uint32_t type = _readU32At(offset); - const FileByteOffset segOffset = _readU64At(offset + 0x08); - const VirtualAddress segVAddr = _readU64At(offset + 0x10); - const ByteCount segFileSize = _readU64At(offset + 0x20); - - if (type == PT_LOAD && address >= segVAddr && address < segVAddr + segFileSize) { - return segOffset + (address - segVAddr); - } - - offset += header.ProgramHeaderEntrySize; + for (const auto& segment : ReadProgramHeaders()) { + if (segment.Type == PT_LOAD && address >= segment.MappedAddress && + 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); @@ -245,18 +304,6 @@ std::vector ElfReader::ReadSegment(const ProgramHeader& header) co _fileBuffer.begin() + header.Offset + header.FileSize); } -std::vector ElfReader::ReadCodeSegments() const { - static constexpr std::uint32_t kPtLoad = 1; - static constexpr std::uint32_t kPfX = 0x1; - std::vector result; - for (const auto& ph : ReadProgramHeaders()) { - if (ph.Type == kPtLoad && (ph.Flags & kPfX)) { - result.push_back(ph); - } - } - return result; -} - std::uint64_t ElfReader::GetFileSize() const { return _fileBuffer.size(); } diff --git a/core/relinker/relinker/src/patching/GamePatch.cpp b/core/relinker/relinker/src/patching/GamePatch.cpp new file mode 100644 index 0000000000..05d334868a --- /dev/null +++ b/core/relinker/relinker/src/patching/GamePatch.cpp @@ -0,0 +1,608 @@ +#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", "settings", "settings_sha256"}; +constexpr std::array kReservedNames{"format", "relinker_version", "relinker_commit", "config", "config_sha256", "main_target", "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 IsElfDyn(const std::vector& bytes) { + return bytes.size() >= 18 && bytes[0] == 0x7f && bytes[1] == 'E' && bytes[2] == 'L' && bytes[3] == 'F' && bytes[16] == Elfpatcher::ET_DYN && bytes[17] == 0; +} + +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; + } + 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 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 (!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) + if (!IsElfDyn(executable.Bytes)) throw Domain::RelinkerException(executable.Name + " is not an ET_DYN Linux ELF output (ET_EXEC from --linux-fixed-base, PE and Mach-O outputs are not supported): verify the input instead"); + } 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; + } + std::vector own; + for (const auto other : indices) own.push_back(file.Entries[other]); + 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); + 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 new file mode 100644 index 0000000000..ad008fa002 --- /dev/null +++ b/core/relinker/relinker/src/pipeline/Conversion.cpp @@ -0,0 +1,166 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Relinker { + +Conversion::Conversion(Domain::IProgress& progress) : progress(progress) {} + +ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOptions& options) const try { + Domain::InputProgress inputProgress(progress, Io::Utf8Path(input.InputPath)); + static_assert(std::endian::native == std::endian::little); + if (options.Target != TargetSystem::Linux && options.Target != TargetSystem::Windows && options.Target != TargetSystem::MacOs) throw Domain::RelinkerException("Unknown target system"); + if (options.ToRosetta && (!options.ToIntel || options.Target == TargetSystem::Windows)) throw Domain::RelinkerException("Rosetta conversion requires Intel lowering and a non-Windows target"); + if (options.UnusedFilterLevel > 2) throw Domain::RelinkerException("Unused NID filter level must be 0, 1 or 2"); + if (input.InputPath.empty() || input.OutputPath.empty()) throw Domain::RelinkerException("Input and output names are required"); + if (input.InputPath.lexically_normal() == input.OutputPath.lexically_normal()) throw Domain::RelinkerException("Executable output would overwrite the input executable"); + if (!input.Modules.empty() && input.ModuleRoot.empty()) throw Domain::RelinkerException("Module root is required"); + if (options.WindowsDiagnostics && options.Target != TargetSystem::Windows) throw Domain::RelinkerException("Windows diagnostics require the Windows target"); + 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" || 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()); } + else if (counted(report.InstructionName)) + ++stubsByName[report.InstructionName]; + else + { std::ostringstream message; message << "Intel substitution: " << report.InstructionName << " at 0x" << std::hex << report.Offset << std::dec << " (" << report.OriginalLength << " bytes) -> " << (report.Lowering == Codegen::Amd64OnlyLowering::InPlace ? "in place " : "stub ") << report.ReplacementLength << " bytes\n"; inputProgress.Message(message.str()); } + } + for (const auto& [name, count] : stubsByName) + { std::ostringstream message; message << "Intel substitution: " << name << " -> stub at " << count << " sites\n"; inputProgress.Message(message.str()); } + { std::ostringstream message; message << "Intel conversion: " << converted.ReplacedCount << " in place, " << trampolines.size() << " stubs, " << converted.KeptCount << " kept\n"; inputProgress.Message(message.str()); } + } + + auto elfReader = std::make_shared(std::move(sourceBytes)); + const std::shared_ptr syscallScanner = options.SkipSyscallCheck ? Relinker::MakeNullSyscallScanner() : Relinker::MakeSyscallScanner(inputProgress, options.TrapSdkSyscallStubs); + + const auto pipeline = std::make_shared( + elfReader, + syscallScanner, + Relinker::MakeCallSiteResolver(), + std::make_shared(), + std::make_shared(), + options.UnusedFilterLevel == 2 ? Relinker::MakeStrictUnusedNidFilter(inputProgress) : Relinker::MakeUnusedNidFilter(), + options.UnusedFilterLevel, inputProgress + ); + + { std::ostringstream message; message << "System: " << (windows ? "Windows" : macos ? "macOS" : "Linux") << "; unused-filter=" << options.UnusedFilterLevel << "\n"; inputProgress.Message(message.str()); } + for (const auto& name : options.ExcludedModules) { std::ostringstream message; message << "Guest module excluded: " << name << '\n'; inputProgress.Message(message.str()); } + auto result = pipeline->Relink(); + sourceBytes = 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); + for (std::size_t index = 0; index < patch.Bytes.size(); ++index) sourceBytes[patch.Offset + index] = patch.Bytes[index]; + } + 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()) { + guestArtifacts = Relinker::GuestModuleBuilder().Build(input.InputPath, input.OutputPath, result.DynamicSection, windows, macos, options.ToIntel, options.ToRosetta, *syscallScanner, options.LazyBinding, options.RunPath, options.ExcludedModules, input.ModuleRoot, input.RuntimeDirectories, std::move(input.Modules), inputProgress); + } + + auto byteWriter = std::make_shared(); + + std::shared_ptr patcher; + if (macos) { + patcher = std::make_shared(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, [&](std::string_view text) { inputProgress.Message(text); }); + } else { + patcher = std::make_shared( + std::make_shared(), + std::make_shared( + std::make_shared(options.LazyBinding), + byteWriter + ), + std::make_shared(byteWriter), + byteWriter, + options.LinuxFixedBase, + options.TraceImports + ); + } + + if (!windows) inputProgress.Report({Domain::ProgressStage::Image, Io::Utf8Path(input.InputPath), 0, 1}); + std::vector executableBytes; + try { + 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; + } + + if (!windows) inputProgress.Report({Domain::ProgressStage::Image, Io::Utf8Path(input.InputPath), 1, 1}); + std::string diagnostics; + if (windows) diagnostics = std::static_pointer_cast(patcher)->GetDiagnostics(); + inputProgress.Report({Domain::ProgressStage::Executable, Io::Utf8Path(input.InputPath), 1, 1}); + return {std::move(executableBytes), std::move(guestArtifacts), std::move(result), std::move(diagnostics), std::move(appliedPatches)}; +} 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 d631eea28f..78ce7cd35a 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -1,34 +1,53 @@ #include #include -#include #include +#include +#include +#include +#include #include -#include + #include +#include +#include +#include #include namespace Relinker { using namespace Elfpatcher; -RelinkerPipeline::RelinkerPipeline(std::shared_ptr elfReader, std::shared_ptr syscallScanner, std::shared_ptr callSiteResolver, std::shared_ptr validationPolicy, std::shared_ptr dynamicSectionBuilder, std::shared_ptr unusedNidFilter, std::uint32_t unusedFilterLevel) - : _elfReader(std::move(elfReader)) - , _syscallScanner(std::move(syscallScanner)) - , _callSiteResolver(std::move(callSiteResolver)) - , _validationPolicy(std::move(validationPolicy)) - , _dynamicSectionBuilder(std::move(dynamicSectionBuilder)) - , _unusedNidFilter(std::move(unusedNidFilter)) +namespace { + +constexpr std::uint16_t kEtNone = 0; +constexpr std::uint16_t kEtRel = 1; +constexpr std::uint16_t kEtCore = 4; + +} + +RelinkerPipeline::RelinkerPipeline(std::shared_ptr elfReader, std::shared_ptr syscallScanner, std::shared_ptr callSiteResolver, std::shared_ptr validationPolicy, std::shared_ptr dynamicSectionBuilder, std::shared_ptr unusedNidFilter, std::uint32_t unusedFilterLevel, Domain::IProgress& progress) + : elfReader(std::move(elfReader)) + , syscallScanner(std::move(syscallScanner)) + , callSiteResolver(std::move(callSiteResolver)) + , validationPolicy(std::move(validationPolicy)) + , dynamicSectionBuilder(std::move(dynamicSectionBuilder)) + , unusedNidFilter(std::move(unusedNidFilter)) , unusedFilterLevel(unusedFilterLevel) + , progress(progress) { + if (!this->elfReader || !this->syscallScanner || !this->callSiteResolver || !this->validationPolicy || !this->dynamicSectionBuilder || !this->unusedNidFilter) throw RelinkerException("Missing relinker pipeline dependency"); if (unusedFilterLevel > 2) throw RelinkerException("Unused NID filter level must be 0, 1 or 2"); } -std::string RelinkerPipeline::_relocationTypeName(std::uint32_t type) { +std::string RelinkerPipeline::relocationTypeName(std::uint32_t type) { switch (type) { case 1: return "R_X86_64_64"; case 6: return "R_X86_64_GLOB_DAT"; case 7: return "R_X86_64_JUMP_SLOT"; case 10: return "R_X86_64_32"; + case 16: return "R_X86_64_DTPMOD64"; + case 17: return "R_X86_64_DTPOFF64"; + case 18: return "R_X86_64_TPOFF64"; default: { std::ostringstream oss; oss << "UNKNOWN(" << type << ")"; @@ -37,28 +56,47 @@ std::string RelinkerPipeline::_relocationTypeName(std::uint32_t type) { } } -RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf) { - auto programHeaders = _elfReader->ReadProgramHeaders(); +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(); std::vector textSection; VirtualAddress textVAddr = 0; - std::vector, VirtualAddress>> executableSegments; + std::vector, VirtualAddress, FileByteOffset>> executableSegments; VirtualAddress gotVAddr = 0; ByteCount gotSize = 0; for (const auto& ph : programHeaders) { if (ph.Type == PT_LOAD && (ph.Flags & PF_X) != 0) { - auto segment = _elfReader->ReadSegment(ph); + auto segment = elfReader->ReadSegment(ph); if (!segment.empty()) { - if (textSection.empty()) { - textSection = segment; + if (executableSegments.empty()) { + if (unusedFilterLevel != 0) textSection = segment; textVAddr = ph.MappedAddress; } - executableSegments.emplace_back(std::move(segment), ph.MappedAddress); + executableSegments.emplace_back(std::move(segment), ph.MappedAddress, ph.Offset); } } } + FileByteOffset sceTableBase = 0; + bool hasSceDynlibData = false; + for (const auto& ph : programHeaders) { + if (ph.Type != PT_SCE_DYNLIBDATA) + continue; + if (hasSceDynlibData) + throw RelinkerException("Multiple PT_SCE_DYNLIBDATA segments", ph.Offset); + hasSceDynlibData = true; + sceTableBase = ph.Offset; + } + std::vector dynTags; bool hasDynamicSegment = false; @@ -67,7 +105,7 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf continue; hasDynamicSegment = true; - dynTags = _elfReader->ReadDynamicTags(ph); + dynTags = elfReader->ReadDynamicTags(ph); break; } @@ -85,7 +123,35 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf for (const auto& t : dynTags) if (t.Tag == tag) return t.Value; - throw RelinkerException("DT tag not found"); + const char* name = "unknown"; + switch (tag) { + case DT_OS_STRTAB: name = "DT_OS_STRTAB"; break; + case DT_STRTAB: name = "DT_STRTAB"; break; + case DT_OS_STRSZ: name = "DT_OS_STRSZ"; break; + case DT_STRSZ: name = "DT_STRSZ"; break; + case DT_OS_SYMTAB: name = "DT_OS_SYMTAB"; break; + case DT_SYMTAB: name = "DT_SYMTAB"; break; + case DT_OS_SYMENT: name = "DT_OS_SYMENT"; break; + case DT_SYMENT: name = "DT_SYMENT"; break; + case DT_OS_PLTGOT: name = "DT_OS_PLTGOT"; break; + case DT_PLTGOT: name = "DT_PLTGOT"; break; + case DT_OS_PLTREL: name = "DT_OS_PLTREL"; break; + case DT_PLTREL: name = "DT_PLTREL"; break; + case DT_OS_PLTRELSZ: name = "DT_OS_PLTRELSZ"; break; + case DT_PLTRELSZ: name = "DT_PLTRELSZ"; break; + case DT_OS_JMPREL: name = "DT_OS_JMPREL"; break; + case DT_JMPREL: name = "DT_JMPREL"; break; + case DT_OS_RELA: name = "DT_OS_RELA"; break; + case DT_RELA: name = "DT_RELA"; break; + case DT_OS_RELASZ: name = "DT_OS_RELASZ"; break; + case DT_RELASZ: name = "DT_RELASZ"; break; + case DT_OS_RELAENT: name = "DT_OS_RELAENT"; break; + case DT_RELAENT: name = "DT_RELAENT"; break; + case DT_OS_SYMTABSZ: name = "DT_OS_SYMTABSZ"; break; + } + std::ostringstream message; + message << "Required dynamic tag " << name << " (0x" << std::hex << tag << ") not found"; + throw RelinkerException(message.str()); }; auto requireExactlyOneOf = [&](const std::int64_t osTag, const std::int64_t sysvTag, const char* name) { @@ -93,15 +159,22 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf const bool hasSysv = hasTag(sysvTag); if (hasOs && hasSysv) throw RelinkerException(std::string("Both DT_OS_ and DT_ variants present for ") + name); - if (!hasOs && !hasSysv) - throw RelinkerException(std::string("Neither DT_OS_ nor DT_ variant present for ") + name); + if (!hasOs && !hasSysv) { + std::ostringstream message; + message << "Neither DT_OS_ nor DT_ variant present for " << name << " (tags 0x" << std::hex << osTag << " and 0x" << sysvTag << ")"; + throw RelinkerException(message.str()); + } return hasOs; }; auto readAsOffset = [&](const std::int64_t osTag, const std::int64_t sysvTag, const char* name) -> FileByteOffset { - if (requireExactlyOneOf(osTag, sysvTag, name)) - return getTagValue(osTag); - return _elfReader->TranslateVirtualAddress(getTagValue(sysvTag)); + if (requireExactlyOneOf(osTag, sysvTag, name)) { + const std::uint64_t value = getTagValue(osTag); + if (value > std::numeric_limits::max() - sceTableBase) + throw RelinkerException(std::string("Adding PT_SCE_DYNLIBDATA base to ") + name + " overflows", value); + return sceTableBase + value; + } + return elfReader->TranslateVirtualAddress(getTagValue(sysvTag)); }; auto readAsSize = [&](const std::int64_t osTag, const std::int64_t sysvTag, const char* name) -> ByteCount { @@ -123,8 +196,9 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf const FileByteOffset dynSymTabOffset = readAsOffset(DT_OS_SYMTAB, DT_SYMTAB, "DT_SYMTAB"); constexpr std::size_t symEntSize = 24; - if (readAsSize(DT_OS_SYMENT, DT_SYMENT, "DT_SYMENT") != symEntSize) - throw RelinkerException("Unsupported DT_SYMENT value"); + const auto symbolEntrySize = readAsSize(DT_OS_SYMENT, DT_SYMENT, "DT_SYMENT"); + if (symbolEntrySize != symEntSize) + throw RelinkerException("Unsupported DT_SYMENT value: " + std::to_string(symbolEntrySize) + "; expected 24 bytes"); FileByteOffset dynJmpRelOffset = 0; if (hasPltRelocations) { @@ -133,20 +207,34 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf ? getTagValue(DT_OS_PLTREL) : getTagValue(DT_PLTREL); if (jmprelType != DT_RELA) - throw RelinkerException("Unsupported DT_PLTREL type"); + throw RelinkerException("Unsupported DT_PLTREL type: " + std::to_string(jmprelType) + "; expected DT_RELA (7)"); dynJmpRelOffset = readAsOffset(DT_OS_JMPREL, DT_JMPREL, "DT_JMPREL"); if (gotSize % 24 != 0) - throw RelinkerException("Invalid DT_PLTRELSZ value"); + throw RelinkerException("Invalid DT_PLTRELSZ value: " + std::to_string(gotSize) + " bytes; expected a multiple of 24 bytes"); } const ByteCount dynJmpRelSize = gotSize; - const FileByteOffset dynRelaOffset = readAsOffset(DT_OS_RELA, DT_RELA, "DT_RELA"); - const ByteCount dynRelaSize = readAsSize(DT_OS_RELASZ, DT_RELASZ, "DT_RELASZ"); + 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; - if (readAsSize(DT_OS_RELAENT, DT_RELAENT, "DT_RELAENT") != relaEntSize) - throw RelinkerException("Unsupported DT_RELAENT value"); + 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"); + throw RelinkerException("Invalid DT_RELASZ value: " + std::to_string(dynRelaSize) + " bytes; expected a multiple of 24 bytes"); std::vector> neededLibraryNamesByStrOffset; for (const auto& tag : dynTags) @@ -155,9 +243,10 @@ 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::vector& raw = elfReader->GetRawBytes(); + const std::optional dynSymTabSize = hasTag(DT_OS_SYMTABSZ) + ? std::optional(getTagValue(DT_OS_SYMTABSZ)) : std::nullopt; if (dynStrTabOffset > raw.size() || dynStrTabSize > raw.size() - dynStrTabOffset) throw RelinkerException("Dynamic string table is out of bounds", dynStrTabOffset); @@ -168,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); @@ -184,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; @@ -199,9 +292,50 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf return relaOff + off; }; - auto extractRela = [&](const FileByteOffset relaOff, const ByteCount relaSize) { + auto symbolEntryPos = [&](const std::uint32_t symIdx) -> FileByteOffset { + const ByteCount off = static_cast(symIdx) * symEntSize; + const FileByteOffset symOff = dynSymTabOffset + off; + if (symOff < dynSymTabOffset || symOff > raw.size() || raw.size() - symOff < symEntSize + || (dynSymTabSize && (off > *dynSymTabSize || symEntSize > *dynSymTabSize - off))) + throw RelinkerException("Symbol table entry out of bounds", symOff); + return symOff; + }; + + auto isZeroSectionSymbol = [&](const std::uint32_t symIdx) { + const FileByteOffset symOff = symbolEntryPos(symIdx); + std::uint64_t value = 0; + std::memcpy(&value, raw.data() + symOff + 8, sizeof(value)); + return (raw[symOff + 4] & 0xf) == 3 && value == 0; + }; + + const auto isTlsRelocation = [](const std::uint32_t type) { + return type == R_X86_64_DTPMOD64 || type == R_X86_64_DTPOFF64 || type == R_X86_64_TPOFF64; + }; + + const bool hasTlsSegment = std::any_of(programHeaders.begin(), programHeaders.end(), + [](const ProgramHeader& ph) { return ph.Type == PT_TLS; }); + + const auto definedTlsSymbolValue = [&](const std::uint32_t symIdx, const FileByteOffset pos) -> std::optional { + const FileByteOffset symOff = symbolEntryPos(symIdx); + std::uint16_t section = 0; + std::memcpy(§ion, raw.data() + symOff + 6, sizeof(section)); + if (section == SHN_UNDEF) + return std::nullopt; + if ((raw[symOff + 4] & 15) != STT_TLS) + throw RelinkerException("TLS relocation names a defined symbol that is not STT_TLS", pos); + std::uint64_t value = 0; + std::memcpy(&value, raw.data() + symOff + 8, sizeof(value)); + return value; + }; + const auto definedTlsSymbol = [&](const std::uint32_t symIdx, const FileByteOffset pos) { + return definedTlsSymbolValue(symIdx, pos).has_value(); + }; + + auto extractRela = [&](const FileByteOffset relaOff, const ByteCount relaSize, 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; @@ -212,45 +346,113 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf const std::uint32_t symIdx = static_cast(rInfo >> 32); const std::uint32_t relType = static_cast(rInfo & 0xffffffff); + if (relType == R_X86_64_NONE) + continue; + if (relType == 8) { - if (symIdx != 0) + if (symIdx != 0 && !isZeroSectionSymbol(symIdx)) throw RelinkerException("RELATIVE relocation has a nonzero symbol index", pos); continue; } - const FileByteOffset symOff = dynSymTabOffset + static_cast(symIdx) * symEntSize; - if (symOff < dynSymTabOffset || symOff > raw.size() || raw.size() - symOff < 4) - throw RelinkerException("Symbol table entry out of bounds", symOff); + if (isTlsRelocation(relType) && (symIdx == 0 || definedTlsSymbol(symIdx, pos))) + continue; + + const FileByteOffset symOff = symbolEntryPos(symIdx); std::uint32_t nameOff = 0; std::memcpy(&nameOff, raw.data() + symOff, 4); const auto name = readCStr(nameOff); - nidRefs.push_back({name, Domain::ImportModule(name, importModules, neededLibraries), relType, pos, rOffset, rAddend}); + if (name.empty() && (raw.size() - symOff < symEntSize + || std::any_of(raw.begin() + static_cast(symOff), raw.begin() + static_cast(symOff + symEntSize), + [](const std::uint8_t byte) { return byte != 0; }))) + throw RelinkerException("Unnamed import symbol is not a null symbol entry", symOff); + const bool weak = (raw[symOff + 4] >> 4) == STB_WEAK; + const bool function = (raw[symOff + 4] & 15) == STT_FUNC; + nidRefs.push_back({name, Domain::ImportModule(name, importModules, neededLibraries), relType, pos, rOffset, rAddend, weak, function}); } }; - 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); + validationPolicy->ValidateRelocationTypeSupported(ref.RelocationTypeValue, ref.RelocationTableOffset); - for (const auto& [segment, segmentVAddr] : executableSegments) - _syscallScanner->ScanCodeSectionForSyscalls(segment, segmentVAddr, segment.size()); + 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 (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); + } + } - _validationPolicy->ValidateSyscallAbsence(); + 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 (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] = scanSegment[offset] = 0x0F; + segment[offset + 1] = scanSegment[offset + 1] = 0x0B; + syscallPatches.push_back({segmentFileOffset + offset, {0x0F, 0x0B}}); + } + } + + + std::vector> exportedSymbols; + std::vector exportedFunctions; + if (hasTag(DT_OS_SYMTABSZ)) { + const auto symbolBytes = getTagValue(DT_OS_SYMTABSZ); + if (symbolBytes % symEntSize != 0 || dynSymTabOffset > raw.size() || symbolBytes > raw.size() - dynSymTabOffset) + throw RelinkerException("Invalid executable dynamic symbol table size"); + for (ByteCount off = symEntSize; off < symbolBytes; off += symEntSize) { + const auto* symbol = raw.data() + dynSymTabOffset + off; + std::uint16_t section = 0; + std::memcpy(§ion, symbol + 6, sizeof(section)); + if (section == 0 || (symbol[4] >> 4) == 0 || (symbol[5] & 3) == 1 || (symbol[5] & 3) == 2) continue; + std::uint32_t sourceName = 0; + std::memcpy(&sourceName, symbol, sizeof(sourceName)); + auto name = readCStr(sourceName); + name.resize(name.find('#') == std::string::npos ? name.size() : name.find('#')); + exportedSymbols.emplace_back(off, std::move(name)); + const auto binding = symbol[4] >> 4; + if ((binding == STB_GLOBAL || binding == STB_WEAK) && (symbol[4] & 15) == STT_FUNC) { + VirtualAddress address = 0; + std::memcpy(&address, symbol + 8, sizeof(address)); + exportedFunctions.push_back(address); + } + } + } + + progress.Report({Domain::ProgressStage::Imports, {}, 0, 1}); const std::size_t originalNidCount = nidRefs.size(); const auto originalNidRefs = nidRefs; - std::cout << "NID input: " << originalNidCount << " references\n"; + { std::ostringstream message; message << "NID input: " << originalNidCount << " references\n"; progress.Message(message.str()); } if (unusedFilterLevel == 2) { if (executableSegments.size() > 1) throw RelinkerException("Strict NID filtering does not support multiple executable segments"); - nidRefs = _unusedNidFilter->Filter(nidRefs, raw, textSection, textVAddr); + nidRefs = unusedNidFilter->Filter(nidRefs, raw, textSection, textVAddr); if (nidRefs.size() > originalNidCount) throw RelinkerException("Strict NID filter increased the reference count"); - std::cout << "Strict filtering total: " << originalNidCount << " -> " << nidRefs.size() << "; filtered=" << originalNidCount - nidRefs.size() << "\n"; + { std::ostringstream message; message << "Strict filtering total: " << originalNidCount << " -> " << nidRefs.size() << "; filtered=" << originalNidCount - nidRefs.size() << "\n"; progress.Message(message.str()); } } else if (unusedFilterLevel == 1) { std::vector pltRefs; std::vector nonPltRefs; @@ -261,15 +463,15 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf nonPltRefs.push_back(ref); } - std::cout << "PLT preservation: " << pltRefs.size() << " -> " << pltRefs.size() << "; filtered=0\n"; + { std::ostringstream message; message << "PLT preservation: " << pltRefs.size() << " -> " << pltRefs.size() << "; filtered=0\n"; progress.Message(message.str()); } const std::size_t nonPltCount = nonPltRefs.size(); if (executableSegments.size() > 1) { - std::cout << "CFG/GOT filtering skipped: multiple executable segments are not modeled; " - << nonPltCount << " -> " << nonPltCount << "; filtered=0\n"; + { std::ostringstream message; message << "CFG/GOT filtering skipped: multiple executable segments are not modeled; " + << nonPltCount << " -> " << nonPltCount << "; filtered=0\n"; progress.Message(message.str()); } } else { - nonPltRefs = _unusedNidFilter->Filter(nonPltRefs, raw, textSection, textVAddr); + nonPltRefs = unusedNidFilter->Filter(nonPltRefs, raw, textSection, textVAddr, exportedFunctions); if (nonPltRefs.size() > nonPltCount) throw RelinkerException("Unused NID filter increased the reference count"); - std::cout << "CFG/GOT filtering: " << nonPltCount << " -> " << nonPltRefs.size() << "; filtered=" << nonPltCount - nonPltRefs.size() << "\n"; + { std::ostringstream message; message << "CFG/GOT filtering: " << nonPltCount << " -> " << nonPltRefs.size() << "; filtered=" << nonPltCount - nonPltRefs.size() << "\n"; progress.Message(message.str()); } } nidRefs.clear(); @@ -277,22 +479,36 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf for (auto& ref : pltRefs) nidRefs.push_back(std::move(ref)); for (auto& ref : nonPltRefs) nidRefs.push_back(std::move(ref)); } else { - std::cout << "Unused NID filtering: disabled; filtered=0\n"; + { std::ostringstream message; message << "Unused NID filtering: disabled; filtered=0\n"; progress.Message(message.str()); } } - std::cout << "NID total: " << originalNidCount << " -> " << nidRefs.size() << "; filtered=" << originalNidCount - nidRefs.size() << "\n"; + { std::ostringstream message; message << "NID total: " << originalNidCount << " -> " << nidRefs.size() << "; filtered=" << originalNidCount - nidRefs.size() << "\n"; progress.Message(message.str()); } auto dynamicRefs = nidRefs; std::vector patches; auto pltCount = static_cast(dynJmpRelSize / relaEntSize); if (unusedFilterLevel == 2) { - auto compacted = UnusedNidFilter::CompactPlt(originalNidRefs, nidRefs, textSection, textVAddr, _elfReader->TranslateVirtualAddress(textVAddr), dynJmpRelOffset); + auto compacted = UnusedNidFilter::CompactPlt(originalNidRefs, nidRefs, textSection, textVAddr, elfReader->TranslateVirtualAddress(textVAddr), dynJmpRelOffset); dynamicRefs = std::move(compacted.References); patches = std::move(compacted.Patches); - std::cout << "PLT compaction: " << pltCount << " -> " << compacted.SlotCount << "\n"; + { std::ostringstream message; message << "PLT compaction: " << pltCount << " -> " << compacted.SlotCount << "\n"; progress.Message(message.str()); } pltCount = compacted.SlotCount; } - auto dynSection = _dynamicSectionBuilder->BuildDynamicSection(dynamicRefs, neededLibraries, dynJmpRelOffset, pltCount); + auto dynSection = dynamicSectionBuilder->BuildDynamicSection(dynamicRefs, neededLibraries, dynJmpRelOffset, pltCount); + for (const auto& ref : dynamicRefs) { + if (!ref.Library.empty()) continue; + if (auto declared = Domain::DeclaredImportModule(ref.Nid, importModules); !declared.empty()) dynSection.UnmatchedImportModules.insert(std::move(declared)); + } + + for (const auto& [off, name] : exportedSymbols) { + const auto* symbol = raw.data() + dynSymTabOffset + off; + const auto nameOffset = static_cast(dynSection.DynStrData.size()); + dynSection.DynStrData.insert(dynSection.DynStrData.end(), name.begin(), name.end()); + dynSection.DynStrData.push_back(0); + const auto position = dynSection.DynSymData.size(); + dynSection.DynSymData.insert(dynSection.DynSymData.end(), symbol, symbol + symEntSize); + std::memcpy(dynSection.DynSymData.data() + position, &nameOffset, sizeof(nameOffset)); + } auto appendRela = [&](std::vector& buf, std::uint64_t offset, std::uint64_t info, std::int64_t addend) { std::size_t pos = buf.size(); @@ -302,9 +518,11 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf std::memcpy(buf.data() + pos + 16, &addend, 8); }; - auto extractRelative = [&](const FileByteOffset relaOff, const ByteCount relaSize) { + auto extractLocal = [&](const FileByteOffset relaOff, const ByteCount relaSize, 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); @@ -312,36 +530,58 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf std::memcpy(&rAddend, raw.data() + pos + 16, 8); const std::uint32_t symIdx = static_cast(rInfo >> 32); const std::uint32_t relType = static_cast(rInfo & 0xffffffff); - if (symIdx == 0 && relType == R_X86_64_RELATIVE) + if (relType == R_X86_64_RELATIVE) { appendRela(dynSection.RelaData, rOffset, static_cast(R_X86_64_RELATIVE), rAddend); + continue; + } + if (!isTlsRelocation(relType)) + continue; + std::uint64_t offset = 0; + if (symIdx != 0) { + const auto value = definedTlsSymbolValue(symIdx, pos); + if (!value) + continue; + offset = *value; + } + if (!hasTlsSegment) + throw RelinkerException("TLS relocation in an executable without a PT_TLS segment", pos); + if (relType == R_X86_64_DTPMOD64 && rAddend != 0) + throw RelinkerException("DTPMOD64 relocation has a nonzero addend", pos); + const auto addend = relType == R_X86_64_DTPMOD64 ? 0 : static_cast(offset + static_cast(rAddend)); + appendRela(dynSection.RelaData, rOffset, static_cast(relType), addend); } }; - extractRelative(dynRelaOffset, dynRelaSize); - extractRelative(dynJmpRelOffset, dynJmpRelSize); + extractLocal(dynRelaOffset, dynRelaSize, false); + extractLocal(dynJmpRelOffset, dynJmpRelSize, true); std::vector entries; entries.reserve(nidRefs.size()); for (const auto& ref : nidRefs) { + if (ref.Nid.empty()) continue; CallRegistryEntry entry; entry.Nid = ref.Nid; entry.Library = ref.Library; - entry.RelocationTypeString = _relocationTypeName(ref.RelocationTypeValue); + entry.RelocationTypeString = relocationTypeName(ref.RelocationTypeValue); entry.RelocationOffset = ref.RelocationTableOffset; entry.TargetSection = ".got"; entry.TargetOffset = ref.RelocationAddress; entry.CallSitesResolved = false; + entry.Weak = ref.Weak; entries.push_back(std::move(entry)); } - for (const auto& [segment, segmentVAddr] : executableSegments) { + 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(); } } + patches.insert(patches.end(), std::make_move_iterator(syscallPatches.begin()), std::make_move_iterator(syscallPatches.end())); + progress.Report({Domain::ProgressStage::Imports, {}, 1, 1}); return RelinkResult{std::move(entries), std::move(programHeaders), std::move(dynSection), gotVAddr, std::move(patches)}; } diff --git a/core/relinker/relinker/tests/CallSiteIndexTests.cpp b/core/relinker/relinker/tests/CallSiteIndexTests.cpp new file mode 100644 index 0000000000..c286f1c224 --- /dev/null +++ b/core/relinker/relinker/tests/CallSiteIndexTests.cpp @@ -0,0 +1,44 @@ +#include +#include +#include +#include +#include + +static void Require(bool ok) { if (!ok) throw std::runtime_error("Incorrect call-site index"); } + +int main() { + try { + std::vector code; + const std::vector targets{0x2000, 0x1000, 0x2000, 0x3000, 0x1008}; + for (const auto target : targets) { + const auto offset = code.size(); + code.insert(code.end(), {0xFF, 0x15, 0, 0, 0, 0}); + const auto disp = static_cast(target - (0x100 + offset + 6)); + std::memcpy(code.data() + offset + 2, &disp, 4); + } + auto resolver = Relinker::MakeCallSiteResolver(); + Require(resolver->ResolveCallSites(code, 0x100, 0x2000, 8) == std::vector{0x100, 0x10C}); + Require(resolver->ResolveCallSites(code, 0x100, 0x1000, 8) == std::vector{0x106}); + Require(resolver->ResolveCallSites(code, 0x100, 0x1000, 9) == std::vector{0x106, 0x118}); + Require(resolver->ResolveCallSites(code, 0x100, 0x2000, 0).empty()); + Require(resolver->ResolveCallSites(code, 0x100, 0x4000, 8).empty()); + auto copy = code; + Require(resolver->ResolveCallSites(copy, 0x100, 0x2000, 8) == std::vector{0x100, 0x10C}); + Require(resolver->ResolveCallSites(copy, 0x200, 0x2100, 8) == std::vector{0x200, 0x20C}); + std::vector mov{0x48, 0x8B, 0x05, 0xF9, 0x0F, 0, 0}; + Require(resolver->ResolveCallSites(mov, 0x1000, 0x2000, 8) == std::vector{0x1000}); + Require(resolver->ResolveCallSites({}, 0, 0, 1).empty()); + bool rejected = false; + try { resolver->ResolveCallSites(code, 0, std::numeric_limits::max(), 1); } + catch (const std::exception&) { rejected = true; } + Require(rejected); + rejected = false; + try { resolver->ResolveCallSites(code, std::numeric_limits::max(), 0x1000, 8); } + catch (const std::exception&) { rejected = true; } + Require(rejected); + constexpr std::uint64_t highBase = 0x8000000000000000ULL; + std::vector highCode{0xFF, 0x15, 0xFA, 0xFF, 0xFF, 0xFF}; + Require(resolver->ResolveCallSites(highCode, highBase + 0x100, highBase + 0x100, 8) == std::vector{highBase + 0x100}); + std::cout << "12 call-site index cases passed\n"; + } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } +} diff --git a/core/relinker/relinker/tests/CallSiteResolverTests.cpp b/core/relinker/relinker/tests/CallSiteResolverTests.cpp new file mode 100644 index 0000000000..f06fab07fa --- /dev/null +++ b/core/relinker/relinker/tests/CallSiteResolverTests.cpp @@ -0,0 +1,45 @@ +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void require(bool condition, const std::string& name) { + if (!condition) throw std::runtime_error(name); +} + +void check(const std::string& name, std::initializer_list instruction, bool expected) { + auto resolver = Relinker::MakeCallSiteResolver(); + const std::vector text(instruction); + const auto sites = resolver->ResolveCallSites(text, 0x1000, 0x1100, 8); + require(sites == (expected ? std::vector{0x1000} : std::vector{}), name); + require(resolver->ResolveCallSites(text, 0x1000, 0x1200, 8).empty(), name + " unexpected address"); +} + +} + +int main() { + try { + check("indirect call", {0xff, 0x15, 0xfa, 0, 0, 0}, true); + check("indirect tail jump", {0xff, 0x25, 0xfa, 0, 0, 0}, true); + 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); + std::cout << "Call-site resolver tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << "call_site_resolver_tests: " << error.what() << '\n'; + return 1; + } +} diff --git a/core/relinker/relinker/tests/ControlFlowGraphTests.cpp b/core/relinker/relinker/tests/ControlFlowGraphTests.cpp new file mode 100644 index 0000000000..94a34990c1 --- /dev/null +++ b/core/relinker/relinker/tests/ControlFlowGraphTests.cpp @@ -0,0 +1,370 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace Relinker::UnusedNidFilter; +using Relinker::VirtualAddress; +using Relinker::RelinkerException; +using Bytes = std::vector; +constexpr VirtualAddress TextAddress = 0xec00001000; + +class Pointers : public IRelativeRelocationIndex { +public: + std::map Slots; + + std::optional TargetOfSlot(VirtualAddress slot) const override { + const auto position = Slots.find(slot); + if (position == Slots.end()) return std::nullopt; + return position->second; + } + + std::vector Targets() const override { + std::vector targets; + for (const auto& [slot, target] : Slots) targets.push_back(target); + return targets; + } +}; + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +void RequireOffsets(const IControlFlowGraph& graph, std::initializer_list offsets) { + std::unordered_set expected; + for (const auto offset : offsets) expected.insert(TextAddress + offset); + Require(graph.ReachableVaddrs() == expected, "Unexpected reachable instruction set"); + for (const auto address : expected) Require(graph.IsReachable(address), "Missing reachable instruction"); + Require(!graph.IsReachable(TextAddress - 1), "Address before code is reachable"); +} + +void SetDisplacement(Bytes& text, std::size_t offset, std::size_t next, VirtualAddress target) { + const auto displacement = static_cast(target - (TextAddress + next)); + std::memcpy(text.data() + offset, &displacement, sizeof(displacement)); +} + +void CheckFallthrough() { + const Pointers pointers; + const Bytes text{0x90, 0x48, 0x89, 0xc0, 0xc3, 0x90}; + const auto graph = BuildControlFlowGraph(text, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*graph, {0, 1, 4}); + const auto end = BuildControlFlowGraph(Bytes{0x90, 0x90}, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*end, {0, 1}); + const auto empty = BuildControlFlowGraph(Bytes{}, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*empty, {}); + const auto outside = BuildControlFlowGraph(text, TextAddress, TextAddress - 1, {TextAddress + text.size()}, pointers); + RequireOffsets(*outside, {}); +} + +void CheckBranches() { + const Pointers pointers; + const Bytes text{0x75, 0x03, 0x90, 0xeb, 0x03, 0x90, 0xeb, 0xfa, 0xc3, 0x90}; + const auto graph = BuildControlFlowGraph(text, TextAddress, TextAddress, {TextAddress, TextAddress + 2}, pointers); + RequireOffsets(*graph, {0, 2, 3, 5, 6, 8}); + const auto self = BuildControlFlowGraph(Bytes{0xeb, 0xfe, 0x90}, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*self, {0}); + const auto outside = BuildControlFlowGraph(Bytes{0xeb, 0x7f, 0x90}, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*outside, {0}); + const auto trap = BuildControlFlowGraph(Bytes{0x0f, 0x0b, 0x90}, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*trap, {0}); +} + +void CheckCalls() { + const Pointers pointers; + const Bytes text{0xe8, 0x03, 0, 0, 0, 0x90, 0xc3, 0x90, 0xe8, 0xf3, 0xff, 0xff, 0xff, 0xc3}; + const auto graph = BuildControlFlowGraph(text, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*graph, {0, 5, 6, 8, 13}); + const auto indirect = BuildControlFlowGraph(Bytes{0xff, 0xd0, 0x90, 0xff, 0xe0, 0xc3}, TextAddress, TextAddress, {}, pointers); + 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}; + SetDisplacement(text, 2, 6, slot); + SetDisplacement(text, 10, 14, slot + 8); + Pointers pointers; + pointers.Slots = {{slot, TextAddress + 8}, {slot + 8, TextAddress + 16}}; + const auto graph = BuildControlFlowGraph(text, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*graph, {0, 6, 7, 8, 16}); + const Pointers missing; + const auto unresolved = BuildControlFlowGraph(text, TextAddress, TextAddress, {TextAddress + 8}, missing); + RequireOffsets(*unresolved, {0, 6, 7, 8}); +} + +void CheckCodeAddresses() { + Bytes text(17, 0x90); + text[0] = 0x48; + text[1] = 0x8d; + text[2] = 0x05; + SetDisplacement(text, 3, 7, TextAddress + 16); + text[7] = text[16] = 0xc3; + const Pointers pointers; + const auto ordinary = BuildControlFlowGraph(text, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*ordinary, {0, 7}); + const auto addresses = BuildControlFlowGraph(text, TextAddress, TextAddress, {}, pointers, true); + RequireOffsets(*addresses, {0, 7, 16}); +} + +void CheckSpeculativeRoots() { + constexpr auto table = TextAddress + 0x1000; + Bytes text(32, 0xc3); + text[0] = 0x48; + text[1] = 0x8d; + text[2] = 0x05; + SetDisplacement(text, 3, 7, table); + text[16] = 0x90; + text[31] = 0x0f; + Bytes entries(12); + const std::int32_t targets[]{static_cast(TextAddress + 16 - table), + static_cast(TextAddress + 31 - table), 0}; + std::memcpy(entries.data(), targets, sizeof(targets)); + Pointers pointers; + pointers.Slots = {{table + 16, TextAddress + 24}, {table + 24, TextAddress + 31}}; + const std::vector data{{table, entries}}; + const auto graph = BuildAddressTakenControlFlowGraph(text, TextAddress, TextAddress, {}, pointers, data); + RequireOffsets(*graph, {0, 7, 16, 17, 24}); + bool threw = false; + try { + BuildAddressTakenControlFlowGraph(text, TextAddress, TextAddress, {TextAddress + 31}, pointers, data); + } catch (const Codegen::CodegenException&) { + threw = true; + } + 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}; + std::unordered_set reachable{TextAddress + 1}; + const auto first = BuildControlFlowGraph(std::span(text), TextAddress, TextAddress, {}, pointers, false, reachable); + RequireOffsets(*first, {0, 1}); + Require(&first->ReachableVaddrs() == &reachable, "Borrowed graph copied the result set"); + const auto second = BuildControlFlowGraph(std::span(text), TextAddress, TextAddress + 3, + {TextAddress + 3, TextAddress + 4}, pointers, false, reachable); + RequireOffsets(*second, {0, 1, 3, 4}); + 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() { + try { + 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) { + std::cerr << "control_flow_graph_tests: " << error.what() << '\n'; + return 1; + } +} diff --git a/core/relinker/relinker/tests/ConversionTests.cpp b/core/relinker/relinker/tests/ConversionTests.cpp new file mode 100644 index 0000000000..0b6d9d62c9 --- /dev/null +++ b/core/relinker/relinker/tests/ConversionTests.cpp @@ -0,0 +1,263 @@ +#include "TestProgress.hpp" +#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); +} + +template +void reject(TAction action) { + try { action(); } catch (const Domain::RelinkerException&) { return; } + throw std::runtime_error("Invalid conversion was accepted"); +} + +std::vector fixture() { + std::vector bytes(0x3000); + const std::array magic{0x7f, 'E', 'L', 'F', 2, 1, 1}; + std::copy(magic.begin(), magic.end(), bytes.begin()); + Io::WriteU16(bytes, 16, 3); + Io::WriteU16(bytes, 18, 62); + Io::WriteU32(bytes, 20, 1); + Io::WriteU64(bytes, 24, 0x1000); + Io::WriteU64(bytes, 32, 64); + Io::WriteU16(bytes, 52, 64); + Io::WriteU16(bytes, 54, 56); + Io::WriteU16(bytes, 56, 8); + Io::WriteU16(bytes, 58, 64); + const auto header = [&](std::size_t index, unsigned type, unsigned flags, unsigned offset, unsigned size) { + const auto at = 64 + index * 56; + Io::WriteU32(bytes, at, type); + Io::WriteU32(bytes, at + 4, flags); + Io::WriteU64(bytes, at + 8, offset); + Io::WriteU64(bytes, at + 16, offset); + Io::WriteU64(bytes, at + 24, offset); + Io::WriteU64(bytes, at + 32, size); + Io::WriteU64(bytes, at + 40, size); + Io::WriteU64(bytes, at + 48, 8); + }; + header(0, 1, 5, 0x1000, 8); + header(1, 1, 6, 0x2000, 0x1000); + for (std::size_t index = 3; index < 8; ++index) header(index, 0x6fffff01, 0, 0, 0); + bytes[0x1000] = 0xc3; + const std::array, 9> tags{{{5, 0x2600}, {10, 1}, {6, 0x2620}, {11, 24}, {7, 0x2700}, {8, 0}, {9, 24}, {4, 0x2680}, {0, 0}}}; + header(2, 2, 6, 0x2000, tags.size() * 16); + for (std::size_t index = 0; index < tags.size(); ++index) { + Io::WriteU64(bytes, 0x2000 + index * 16, tags[index].first); + Io::WriteU64(bytes, 0x2008 + index * 16, tags[index].second); + } + Io::WriteU32(bytes, 0x2680, 1); + Io::WriteU32(bytes, 0x2684, 1); + return bytes; +} + +Relinker::ConversionInput input() { + Relinker::ConversionInput result; + result.Executable = fixture(); + result.InputPath = "virtual/title/custom-name.bin"; + result.OutputPath = "virtual/output/title"; + result.ModuleRoot = "virtual/title"; + return result; +} + +void convert() { + TestProgress progress; + const Relinker::Conversion converter(progress); + for (const auto target : {Relinker::TargetSystem::Windows, Relinker::TargetSystem::Linux}) { + Relinker::ConversionOptions options; + options.Target = target; + options.ToIntel = true; + auto source = input(); + source.Modules.push_back({"virtual/title/sce_module/plugin.prx", fixture()}); + const auto output = converter.Convert(std::move(source), options); + require(output.Executable.size() > 64, "No executable returned"); + require(output.Executable.front() == (target == Relinker::TargetSystem::Windows ? 'M' : 0x7f), "Wrong target format"); + require(output.Modules.size() == 1, "Module was not converted"); + require(output.Modules.front().Path.generic_string() == "virtual/output/app0/sce_module/plugin.prx.guest.prx", "Wrong module layout"); + require(output.Modules.front().Bytes.front() == output.Executable.front(), "Wrong module format"); + require(progress.Events.back().Stage == Domain::ProgressStage::Executable && progress.Events.back().Completed == 1, "Conversion did not finish"); + for (const auto& event : progress.Events) require(event.Completed <= event.Total, "Invalid counters"); + if (target == Relinker::TargetSystem::Windows) { + std::uint64_t previous = 0; + bool intermediate = false; + for (const auto& event : progress.Events) { + if (event.Stage != Domain::ProgressStage::Image) continue; + require(event.Input == "virtual/title/custom-name.bin", "Lost image input identity"); + require(event.Total == 10000 && event.Completed >= previous, "Image progress moved backwards"); + intermediate |= event.Completed > 0 && event.Completed < event.Total; + previous = event.Completed; + } + require(intermediate && previous == 10000, "Missing image progress"); + } + progress.Events.clear(); + } + auto source = input(); + source.Modules.push_back({"virtual/escaped.prx", fixture()}); + reject([&] { converter.Convert(source, {}); }); + source = input(); + source.Modules.push_back({"virtual/title/sce_module/plugin.prx", fixture()}); + source.Modules.push_back(source.Modules.front()); + reject([&] { converter.Convert(source, {}); }); + source = input(); + source.Icon = std::vector{}; + Relinker::ConversionOptions windows; + windows.Target = Relinker::TargetSystem::Windows; + reject([&] { converter.Convert(source, windows); }); + source = input(); + source.Executable = {}; + reject([&] { converter.Convert(source, {}); }); + require(progress.Events.back().Completed != 1 || progress.Events.back().Stage != Domain::ProgressStage::Executable, "Failed conversion reported completion"); +} + +void fixedBase() { + TestProgress progress; + const Relinker::Conversion converter(progress); + auto source = input(); + source.Modules.push_back({"virtual/title/sce_module/plugin.prx", fixture()}); + const auto plain = converter.Convert(source, {}); + Relinker::ConversionOptions options; + options.LinuxFixedBase = 0x400000000; + const auto fixed = converter.Convert(source, options); + require(Io::ReadU16(plain.Executable, 16) == 3 && Io::ReadU16(fixed.Executable, 16) == 2, "Fixed base did not select ET_EXEC"); + require(Io::ReadU64(fixed.Executable, 24) == Io::ReadU64(plain.Executable, 24) + *options.LinuxFixedBase, "Fixed entry was not rebased above 4 GiB"); + require(fixed.Modules.front().Bytes == plain.Modules.front().Bytes, "Executable fixed base changed a bundled module"); + for (const auto target : {Relinker::TargetSystem::Windows, Relinker::TargetSystem::MacOs}) { + options.Target = target; + reject([&] { converter.Convert(source, options); }); + } +} + +void scanning() { + TestProgress progress; + const auto scanner = Relinker::MakeSyscallScanner(progress); + std::vector code(150000, 0x90); + require(scanner->ScanCodeSectionForSyscalls(code, 0, code.size(), "sample.prx").empty(), "Unexpected syscall trap"); + require(std::any_of(progress.Events.begin(), progress.Events.end(), [](const auto& event) { return event.Completed > 0 && event.Completed < event.Total; }), "Only start and finish were reported"); + for (const auto& event : progress.Events) require(event.Input == "sample.prx", "Lost scanner input identity"); + progress.Events.clear(); + Domain::ProgramHeader header{}; + header.Type = 1; + header.Flags = 1; + header.FileSize = code.size(); + header.MemorySize = code.size(); + const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Intel, [&](auto completed, auto total) { progress.Report({Domain::ProgressStage::Lowering, "sample.prx", completed, total}); }); + require(converter->Convert(code, {header}).Bytes == code, "Progress changed instruction conversion"); + std::uint64_t previous = 0; + for (const auto& event : progress.Events) { + require(event.Completed >= previous && event.Completed <= event.Total, "Instruction conversion progress moved backwards"); + previous = event.Completed; + } + require(previous == progress.Events.back().Total, "Instruction conversion progress did not finish"); + reject([&] { scanner->ScanCodeSectionForSyscalls(code, 0, (std::uint64_t{1} << 32) + code.size()); }); + auto image = code; + image.resize(code.size() + 64, 0x90); + image.back() = 0xc3; + Io::WriteU64(image, 24, 64); + header.Offset = 64; + header.MappedAddress = 64; + previous = 0; + std::size_t updates = 0; + const auto instructions = Relinker::CodeInstructionCollector().Collect(image, {header}, [&](auto completed, auto total) { + require(completed >= previous && completed <= total, "Image analysis progress moved backwards"); + previous = completed; + ++updates; + }); + require(instructions.size() == code.size(), "Progress changed image code analysis"); + require(updates > 10 && previous == 10000, "Missing intermediate image analysis progress"); + code[70000] = 0x0f; + code[70001] = 0x05; + progress.Events.clear(); + reject([&] { scanner->ScanCodeSectionForSyscalls(code, 0, code.size()); }); + require(progress.Events.back().Completed < progress.Events.back().Total, "Failed scan reported completion"); +} + +class FailingProgress final : public Domain::IProgress { +public: + std::string InputPath; + void Report(const Domain::ProgressEvent&) override { + Domain::RelinkerException error("diagnostic context", 0x1234); + error.InputPath = InputPath; + throw error; + } + void Message(std::string_view) override {} +}; + +void diagnosticContext() { + for (const bool specific : {false, true}) { + FailingProgress progress; + if (specific) progress.InputPath = "virtual/title/sce_module/plugin.prx"; + const auto source = input(); + try { Relinker::Conversion(progress).Convert(source, {}); } + catch (const Domain::RelinkerException& error) { + require(error.InputPath == (specific ? progress.InputPath : Io::Utf8Path(source.InputPath)), "Wrong diagnostic input path"); + require(error.FailureOffset == 0x1234, "Diagnostic offset changed"); + require(std::string(error.what()) == "diagnostic context", "Diagnostic message changed"); + continue; + } + throw std::runtime_error("Diagnostic failure was swallowed"); + } +} + +class CancellingProgress final : public Domain::IProgress { +public: + Domain::ProgressStage Stage = Domain::ProgressStage::Decode; + void Report(const Domain::ProgressEvent& event) override { + if (event.Stage == Stage && event.Completed > 0) throw std::runtime_error("cancelled"); + } + void Message(std::string_view) override {} +}; + +void cancellation() { + for (const auto stage : {Domain::ProgressStage::Decode, Domain::ProgressStage::Image}) { + CancellingProgress progress; + progress.Stage = stage; + Relinker::ConversionOptions options; + options.Target = Relinker::TargetSystem::Windows; + try { Relinker::Conversion(progress).Convert(input(), options); } + catch (const std::runtime_error& error) { + require(std::string(error.what()) == "cancelled", "Callback exception was replaced"); + continue; + } + throw std::runtime_error("Callback exception was swallowed"); + } +} + +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}); }); +} + +} + +int main() { + try { + convert(); + fixedBase(); + scanning(); + diagnosticContext(); + cancellation(); + baseRelocations(); + } 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 8c44f11795..daa1f322cb 100644 --- a/core/relinker/relinker/tests/ElfFixture.hpp +++ b/core/relinker/relinker/tests/ElfFixture.hpp @@ -17,7 +17,7 @@ using Bytes = std::vector; inline constexpr std::uint64_t kCodeOffset = 0x4000; inline constexpr std::uint64_t kCodeSize = 0x1000; inline constexpr std::uint64_t kDynamicVaddr = 0x600; -inline constexpr std::uint16_t kSpareProgramHeaders = 3; +inline constexpr std::uint16_t kSpareProgramHeaders = 4; inline constexpr std::uint64_t kModuleDataOffset = 0x5000; inline constexpr std::uint64_t kModuleDataVaddr = 0x2000; inline constexpr std::uint64_t kModuleDataSize = 0x400; diff --git a/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp b/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp new file mode 100644 index 0000000000..154238155d --- /dev/null +++ b/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp @@ -0,0 +1,339 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using Bytes = std::vector; + +template +void Write(Bytes& bytes, const std::size_t offset, const TValue value) { + if (offset > bytes.size() || sizeof(value) > bytes.size() - offset) throw std::runtime_error("Invalid fixture offset"); + std::memcpy(bytes.data() + offset, &value, sizeof(value)); +} + +Bytes Fixture() { + Bytes bytes(0x300); + const std::uint8_t magic[] = {0x7f, 'E', 'L', 'F', 2, 1, 1}; + std::memcpy(bytes.data(), magic, sizeof(magic)); + Write(bytes, 16, 3); + Write(bytes, 18, 62); + Write(bytes, 20, 1); + Write(bytes, 32, 64); + Write(bytes, 40, 128); + Write(bytes, 52, 64); + Write(bytes, 54, 56); + Write(bytes, 56, 1); + Write(bytes, 58, 64); + Write(bytes, 60, 3); + Write(bytes, 62, 2); + Write(bytes, 64, 1); + Write(bytes, 72, 0x240); + Write(bytes, 80, 0x1000); + Write(bytes, 96, 32); + Write(bytes, 104, 64); + Write(bytes, 192, 1); + Write(bytes, 196, 1); + Write(bytes, 216, 0x240); + Write(bytes, 224, 32); + Write(bytes, 240, 8); + Write(bytes, 248, 24); + Write(bytes, 256, 7); + Write(bytes, 260, 3); + Write(bytes, 280, 0x200); + const char strings[] = "\0.text\0.shstrtab"; + Write(bytes, 288, sizeof(strings)); + std::memcpy(bytes.data() + 0x200, strings, sizeof(strings)); + 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"); +} + +void RejectSections(Bytes bytes) { + try { + Relinker::ElfReader(std::move(bytes)).ReadSectionHeaders(); + } catch (const Relinker::RelinkerException&) { + return; + } + throw std::runtime_error("Invalid section metadata was accepted"); +} + +void RejectAddress(Bytes bytes, const std::uint64_t address) { + try { + Relinker::ElfReader(std::move(bytes)).TranslateVirtualAddress(address); + } catch (const Relinker::RelinkerException&) { + return; + } + 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); + }}, + {"section header stride", [] { + for (const std::uint16_t stride : {0, 56, 65}) { + auto bytes = Fixture(); + Write(bytes, 58, stride); + RejectSections(std::move(bytes)); + } + }}, + {"section name table index", [] { + auto bytes = Fixture(); + Write(bytes, 62, 3); + RejectSections(std::move(bytes)); + }}, + {"section name table type", [] { + auto bytes = Fixture(); + Write(bytes, 260, 1); + RejectSections(std::move(bytes)); + }}, + {"section name table range", [] { + auto bytes = Fixture(); + Write(bytes, 288, 0x200); + RejectSections(std::move(bytes)); + }}, + {"section name offset", [] { + auto bytes = Fixture(); + Write(bytes, 192, 64); + bytes[0x240] = 'X'; + RejectSections(std::move(bytes)); + }}, + {"section name terminator", [] { + auto bytes = Fixture(); + Write(bytes, 288, 5); + RejectSections(std::move(bytes)); + }}, + {"section header table truncation", [] { + auto bytes = Fixture(); + Write(bytes, 62, 0); + bytes.resize(128 + 3 * 64 - 1); + RejectSections(std::move(bytes)); + }}, + {"translation program header stride", [] { + for (const std::uint16_t stride : {0, 48, 64}) { + auto bytes = Fixture(); + Write(bytes, 54, stride); + RejectAddress(std::move(bytes), 0x1008); + } + }}, + {"translation virtual boundary", [] { + auto bytes = Fixture(); + const auto start = std::numeric_limits::max() - 31; + Write(bytes, 80, start); + Require(Relinker::ElfReader(std::move(bytes)).TranslateVirtualAddress(start + 16) == 0x250); + }}, + {"translation overflowing file offset", [] { + auto bytes = Fixture(); + Write(bytes, 72, std::numeric_limits::max() - 3); + RejectAddress(std::move(bytes), 0x1008); + }}, + {"translation file mapping range", [] { + auto bytes = Fixture(); + Write(bytes, 72, 0x2f0); + RejectAddress(std::move(bytes), 0x1008); + }}, + {"empty and absent name tables", [] { + auto bytes = Fixture(); + Write(bytes, 62, 0); + const auto unnamed = Relinker::ElfReader(bytes).ReadSectionHeaders(); + Require(unnamed.size() == 3 && unnamed[1].Name.empty()); + Write(bytes, 62, 2); + Write(bytes, 288, 0); + Write(bytes, 192, 0); + Write(bytes, 256, 0); + const auto empty = Relinker::ElfReader(std::move(bytes)).ReadSectionHeaders(); + Require(empty.size() == 3 && empty[1].Name.empty()); + }}, + {"section name at table boundary", [] { + auto bytes = Fixture(); + Write(bytes, 192, 17); + RejectSections(std::move(bytes)); + }}, + {"section name table offset overflow", [] { + auto bytes = Fixture(); + Write(bytes, 280, std::numeric_limits::max() - 3); + RejectSections(std::move(bytes)); + }}, + {"section table offset overflow", [] { + auto bytes = Fixture(); + Write(bytes, 62, 0); + Write(bytes, 40, std::numeric_limits::max() - 31); + RejectSections(std::move(bytes)); + }}, + {"valid string table suffix and final null", [] { + auto bytes = Fixture(); + Write(bytes, 192, 2); + Write(bytes, 256, 16); + const auto sections = Relinker::ElfReader(std::move(bytes)).ReadSectionHeaders(); + Require(sections[1].Name == "text" && sections[2].Name.empty()); + }}, + {"ELF without section or program tables", [] { + auto bytes = Fixture(); + Write(bytes, 32, 0); + Write(bytes, 40, 0); + Write(bytes, 54, 0); + Write(bytes, 56, 0); + Write(bytes, 58, 0); + Write(bytes, 60, 0); + Write(bytes, 62, 0); + const Relinker::ElfReader reader(bytes); + Require(reader.ReadProgramHeaders().empty() && reader.ReadSectionHeaders().empty()); + RejectAddress(std::move(bytes), 0); + }}, + {"translation truncated program header table", [] { + auto bytes = Fixture(); + std::memcpy(bytes.data() + 0x2c8, bytes.data() + 64, 56); + Write(bytes, 32, 0x2c8); + Write(bytes, 56, 2); + RejectAddress(std::move(bytes), 0x1008); + }}, + {"translation below segment and at virtual maximum", [] { + RejectAddress(Fixture(), 0x0fff); + auto bytes = Fixture(); + const auto maximum = std::numeric_limits::max(); + Write(bytes, 80, maximum - 31); + Require(Relinker::ElfReader(std::move(bytes)).TranslateVirtualAddress(maximum) == 0x25f); + }}, + {"ordinary translations and zero fill", [] { + const Relinker::ElfReader reader(Fixture()); + Require(reader.TranslateVirtualAddress(0x1000) == 0x240); + Require(reader.TranslateVirtualAddress(0x101f) == 0x25f); + RejectAddress(Fixture(), 0x1020); + }}, + }; + std::size_t failures = 0; + for (const auto& [name, test] : cases) { + try { + test(); + std::cout << "PASS: " << name << '\n'; + } catch (const std::exception& error) { + ++failures; + std::cerr << "FAIL: " << name << ": " << error.what() << '\n'; + } + } + return failures == 0 ? 0 : 1; +} diff --git a/core/relinker/relinker/tests/GamePatchTests.cpp b/core/relinker/relinker/tests/GamePatchTests.cpp new file mode 100644 index 0000000000..f81bc1f2fe --- /dev/null +++ b/core/relinker/relinker/tests/GamePatchTests.cpp @@ -0,0 +1,525 @@ +#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); }, "verify the input instead"); + 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"); +} + +void reservedNames() { + for (const char* reserved : {"format", "relinker_version", "relinker_commit", "config", "config_sha256", "main_target", "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 new file mode 100644 index 0000000000..f929c7837b --- /dev/null +++ b/core/relinker/relinker/tests/SdkMmanSyscallTests.cpp @@ -0,0 +1,103 @@ +#include "TestProgress.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using Bytes = std::vector; +struct Fixture { Bytes code; std::size_t syscall; }; + +const std::vector Fixtures = { + {{0x55, 0x48, 0x89, 0xE5, 0x53, 0x50, 0x89, 0xD1, 0x49, 0x89, 0xF0, 0x48, 0x89, 0xFE, + 0xF6, 0xC1, 0x04, 0x75, 0x31, 0x48, 0x63, 0xD1, 0xB8, 0x4A, 0x00, 0x00, 0x00, + 0x48, 0x89, 0xF7, 0x4C, 0x89, 0xC6, 0x45, 0x31, 0xD2, 0x45, 0x31, 0xC0, 0x45, 0x31, 0xC9, + 0x0F, 0x05, 0x0F, 0x93, 0xC1, 0x48, 0x89, 0xC3, 0x48, 0xF7, 0xDB, 0x84, 0xC9, + 0x48, 0x0F, 0x45, 0xD8, 0x48, 0x85, 0xDB}, 42}, + {{0x49, 0x63, 0xD6, 0x4C, 0x63, 0xD1, 0x4D, 0x63, 0xC0, 0xB8, 0xDD, 0x01, 0x00, 0x00, + 0x48, 0x89, 0xDE, 0x0F, 0x05, 0x48, 0x89, 0xC1, 0x0F, 0x93, 0xC2, 0x48, 0xF7, 0xD8, + 0x84, 0xD2, 0x48, 0x0F, 0x45, 0xC1, 0x48, 0x85, 0xC0}, 17}, + {{0x44, 0x89, 0xF0, 0x83, 0xE0, 0xFB, 0x48, 0x63, 0xD0, 0x4C, 0x63, 0xD1, 0x4D, 0x63, 0xC0, + 0xB8, 0xDD, 0x01, 0x00, 0x00, 0x48, 0x89, 0xDE, 0x0F, 0x05, 0x0F, 0x93, 0xC1, + 0x49, 0x89, 0xC7, 0x49, 0xF7, 0xDF, 0x84, 0xC9, 0x4C, 0x0F, 0x45, 0xF8, 0x4D, 0x85, 0xFF}, 23}, +}; + +void Require(bool value, const std::string& message) { + if (!value) throw std::runtime_error(message); +} + +bool Rejected(const Bytes& code, bool trap, std::size_t size) { + try { + Relinker::MakeSyscallScanner(testProgress, trap)->ScanCodeSectionForSyscalls(code, 0, size); + return false; + } catch (const std::runtime_error&) { + return true; + } +} + +void CheckFixtures() { + std::size_t rejectedMutations = 0; + for (const auto& fixture : Fixtures) { + Require(Rejected(fixture.code, false, fixture.code.size()), "default mode accepted an SDK inline syscall"); + for (const auto padding : {std::size_t{0}, std::size_t{1}, std::size_t{7}, std::size_t{64}}) { + Bytes code(padding, 0x90); + code.insert(code.end(), fixture.code.begin(), fixture.code.end()); + const auto trapped = Relinker::MakeSyscallScanner(testProgress, true)->ScanCodeSectionForSyscalls(code, 0x123400, code.size()); + Require(trapped == std::vector{padding + fixture.syscall}, "SDK inline syscall returned the wrong patch offset"); + } + for (std::size_t length = fixture.syscall + 2; length < fixture.code.size(); ++length) + Require(Rejected(fixture.code, true, length), "SDK matching read its suffix beyond the code section"); + for (std::size_t removed = 1; removed <= fixture.syscall; ++removed) { + const Bytes fragment(fixture.code.begin() + static_cast(removed), fixture.code.end()); + if (Rejected(fragment, false, fragment.size())) + Require(Rejected(fragment, true, fragment.size()), "SDK matching accepted a truncated prefix"); + } + for (std::size_t offset = 0; offset < fixture.code.size(); ++offset) { + if (offset == fixture.syscall || offset == fixture.syscall + 1) continue; + for (unsigned bit = 0; bit < 8; ++bit) { + auto code = fixture.code; + code[offset] ^= static_cast(1u << bit); + if (!Rejected(code, false, code.size())) continue; + Require(Rejected(code, true, code.size()), "SDK matching accepted a changed instruction sequence"); + ++rejectedMutations; + } + } + } + Require(rejectedMutations > 1000, "too few near-match rejection cases ran"); + Bytes embedded = {0x48, 0xB8}; + embedded.insert(embedded.end(), Fixtures.front().code.begin(), Fixtures.front().code.end()); + Require(Rejected(embedded, false, embedded.size()), "embedded fixture lost its syscall instruction"); + 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"; +} + +} + +int main() { + try { + CheckFixtures(); + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/relinker/relinker/tests/StrictNidFilterTests.cpp b/core/relinker/relinker/tests/StrictNidFilterTests.cpp index dfef7a5ad0..349da2d7a0 100644 --- a/core/relinker/relinker/tests/StrictNidFilterTests.cpp +++ b/core/relinker/relinker/tests/StrictNidFilterTests.cpp @@ -1,14 +1,18 @@ +#include "TestProgress.hpp" #include #include #include #include #include +#include #include #include #include #include #include +#include #include +#include namespace { @@ -95,6 +99,22 @@ void fallthroughAfterZeroPadding() { require(AnalyzeStrictReachability(input).ImportSlots.contains(0x2000), "Zero padding after a region's last jump dropped its fall-through edge"); } +void addressSizeOverrideImport() { + auto input = fixture(); + input.Text.assign(32, 0); + input.TextVaddr = 0x100001000ull; + input.Entries = {input.TextVaddr}; + input.ImportSlots = {0x2000, 0x100002000ull}; + emit(input, 0, {0x67, 0xFF, 0x15, 0xF9, 0x0F, 0x00, 0x00, 0xC3}); + require(AnalyzeStrictReachability(input).ImportSlots == std::set{0x2000}, "EIP-relative import was not resolved with 32-bit address wrapping"); + input.Text.assign(32, 0); + emit(input, 0, {0x67, 0x48, 0x8B, 0x05, 0xF8, 0x0F, 0x00, 0x00, 0xC3}); + require(AnalyzeStrictReachability(input).ImportSlots == std::set{0x2000}, "REX.W EIP-relative load did not resolve its import"); + input.Text.assign(32, 0); + emit(input, 0, {0xFF, 0x15, 0xFA, 0x0F, 0x00, 0x00, 0xC3}); + require(AnalyzeStrictReachability(input).ImportSlots == std::set{0x100002000ull}, "RIP-relative import lost its full 64-bit address"); +} + void callbackAndRelocationRoots() { auto input = fixture(); ripOperand(input, 0, {0x48, 0x8D, 0x3D}, 0x1020); @@ -167,6 +187,53 @@ 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); + std::vector cie{1, 0}; + cie.insert(cie.end(), codeAlignment.begin(), codeAlignment.end()); + cie.insert(cie.end(), dataAlignment.begin(), dataAlignment.end()); + cie.push_back(16); + write(bytes, 0x200, static_cast(4 + cie.size())); + std::copy(cie.begin(), cie.end(), bytes.begin() + 0x208); + write(bytes, 0x240, 20); + write(bytes, 0x244, 0x44); + write(bytes, 0x248, 0x1000); + write(bytes, 0x250, 0x20); + write(bytes, 0x300, 1); + write(bytes, 0x302, 3); + write(bytes, 0x304, 0x200); + write(bytes, 0x30C, 1); + write(bytes, 0x310, 0x1000); + write(bytes, 0x318, 0x240); + const std::vector headers = {{1, 4, 0, 0, 0, bytes.size(), bytes.size(), 8}, {0x6474E550, 4, 0x300, 0x300, 0, 32, 32, 4}}; + try { + const auto functions = Relinker::UnusedNidFilter::ReadExceptionFunctions(bytes, headers, {}, {}); + require(!overflowing, "Overflowing CIE LEB128 value was accepted"); + require(functions.size() == 1 && functions[0].Begin == 0x1000 && functions[0].End == 0x1020 && functions[0].ExtraTargets.empty(), "Valid CIE LEB128 changed the function range"); + } catch (const Relinker::RelinkerException& error) { + require(overflowing && std::string(error.what()).find("overflowing LEB128 value") != std::string::npos, "CIE LEB128 was rejected for an unexpected reason"); + } + }; + check({1}, {0x78}, false); + for (const std::uint8_t prefix : {0x80, 0xFF}) { + for (unsigned terminal = 0; terminal < 0x80; ++terminal) { + std::vector encoded(10, prefix); + encoded.back() = static_cast(terminal); + check({1}, encoded, terminal != 0 && terminal != 0x7F); + check(encoded, {0x78}, terminal != 0 && terminal != 1); + } + } +} + void vectorInstructionLengths() { const Codegen::X64InstructionDecoder decoder; const std::vector> instructions = { @@ -183,10 +250,13 @@ void vectorInstructionLengths() { void exceptionLandingPads() { std::vector bytes(0x400); - write(bytes, 0x200, 15); + write(bytes, 0x200, 25); write(bytes, 0x208, 1); - const std::vector augmentation = {'z', 'L', 'R', 0, 1, 0x78, 16, 2, 0, 0}; + const std::vector augmentation = {'z', 'P', 'L', 'R', 0, 1, 0x78, 16, 11, 0}; std::copy(augmentation.begin(), augmentation.end(), bytes.begin() + 0x209); + write(bytes, 0x213, 0x1000); + write(bytes, 0x21B, 0); + write(bytes, 0x21C, 0); write(bytes, 0x220, 29); write(bytes, 0x224, 0x24); write(bytes, 0x228, 0x1000); @@ -204,15 +274,79 @@ void exceptionLandingPads() { const std::vector headers = {{1, 4, 0, 0, 0, bytes.size(), bytes.size(), 8}, {0x6474E550, 4, 0x300, 0x300, 0, 32, 32, 4}}; auto input = fixture(); input.Functions = Relinker::UnusedNidFilter::ReadExceptionFunctions(bytes, headers, {}, {}); - require(input.Functions.size() == 1 && input.Functions[0].ExtraTargets == std::vector{0x1040}, "LSDA landing pad outside the function was not recovered"); + require(input.Functions.size() == 1 && input.Functions[0].ExtraTargets == std::vector{0x1000, 0x1040}, "LSDA landing pad outside the function was not recovered"); emit(input, 0, {0x0F, 0x0B}); emit(input, 31, {0xC3}); importThunk(input, 64, 0x2000); require(AnalyzeStrictReachability(input).ImportSlots.contains(0x2000), "Exception-only import was removed"); + const auto checkLanding = [&](std::uint64_t base, std::uint64_t offset, bool signedOffset, bool rejected) { + auto data = bytes; + write(data, 0x280, 0); + write(data, 0x281, base); + write(data, 0x289, 0xFF); + write(data, 0x28A, signedOffset ? 12 : 4); + write(data, 0x28B, 25); + write(data, 0x28C, 0); + write(data, 0x294, 1); + write(data, 0x29C, offset); + write(data, 0x2A4, 0); + try { + const auto functions = Relinker::UnusedNidFilter::ReadExceptionFunctions(data, headers, {}, {}); + require(!rejected, "Overflowing LSDA landing pad address was accepted"); + const auto expected = offset == 0 ? std::vector{0x1000} : + std::vector{0x1000, base + offset}; + require(functions.size() == 1 && functions[0].ExtraTargets == expected, + "Valid LSDA landing pad address changed"); + } catch (const Relinker::RelinkerException& error) { + require(rejected && std::string(error.what()).find("LSDA landing pad address overflowed") != std::string::npos, + "LSDA landing pad rejected for an unexpected reason"); + } + }; + const auto maximum = std::numeric_limits::max(); + for (const std::uint64_t base : {std::uint64_t{0x1000}, maximum - 16}) { + checkLanding(base, 0, false, false); + checkLanding(base, maximum - base, false, false); + checkLanding(base, maximum - base + 1, false, true); + checkLanding(base, maximum, false, true); + } + checkLanding(0x1000, maximum, true, false); + checkLanding(0x1000, std::uint64_t{0} - 0x1000, true, false); + checkLanding(0x1000, std::uint64_t{0} - 0x1001, true, true); + checkLanding(maximum - 16, 16, true, false); + checkLanding(maximum - 16, 17, true, true); + checkLanding(0, 0, true, false); + checkLanding(0, maximum, false, false); + checkLanding(std::uint64_t{1} << 63, std::uint64_t{1} << 63, true, false); + checkLanding((std::uint64_t{1} << 63) - 1, std::uint64_t{1} << 63, true, true); write(bytes, 0x282, 0xFF); requireFailure([&] { Relinker::UnusedNidFilter::ReadExceptionFunctions(bytes, headers, {}, {}); }, "Malformed LSDA was accepted"); } +void exceptionDataWithoutPersonality() { + std::vector bytes(0x400); + write(bytes, 0x200, 15); + write(bytes, 0x208, 1); + const std::vector augmentation = {'z', 'L', 'R', 0, 1, 0x78, 16, 2, 0, 0}; + std::copy(augmentation.begin(), augmentation.end(), bytes.begin() + 0x209); + write(bytes, 0x220, 29); + write(bytes, 0x224, 0x24); + write(bytes, 0x228, 0x1000); + write(bytes, 0x230, 0x20); + write(bytes, 0x238, 8); + write(bytes, 0x239, 0x280); + const std::vector lsda = {0xFF, 0xFF, 1, 3, 0, 1, 0x40, 0}; + std::copy(lsda.begin(), lsda.end(), bytes.begin() + 0x280); + write(bytes, 0x300, 1); + write(bytes, 0x302, 3); + write(bytes, 0x304, 0x200); + write(bytes, 0x30C, 1); + write(bytes, 0x310, 0x1000); + write(bytes, 0x318, 0x220); + const std::vector headers = {{1, 4, 0, 0, 0, bytes.size(), bytes.size(), 8}, {0x6474E550, 4, 0x300, 0x300, 0, 32, 32, 4}}; + const auto functions = Relinker::UnusedNidFilter::ReadExceptionFunctions(bytes, headers, {}, {}); + require(functions.size() == 1 && functions[0].Begin == 0x1000 && functions[0].End == 0x1020 && functions[0].ExtraTargets.empty(), "LSDA without a personality routine was read as a C++ call-site table"); +} + std::vector elfFixture(const StrictReachabilityInput& input) { std::vector bytes(0x400); bytes[0] = 0x7F; @@ -224,8 +358,10 @@ std::vector elfFixture(const StrictReachabilityInput& input) { bytes[6] = 1; write(bytes, 16, 3); write(bytes, 18, 62); + write(bytes, 20, 1); write(bytes, 24, input.Entries.at(0)); write(bytes, 32, 64); + write(bytes, 52, 64); write(bytes, 54, 56); write(bytes, 56, 2); write(bytes, 64, 1); @@ -255,7 +391,7 @@ void filterCallbackDataImports() { const auto bytes = elfFixture(input); for (const std::uint32_t relocationType : {1, 6}) { const std::vector references = {{"callbackData", {}, relocationType, 0x300, 0x2000, 0}, {"deadData", {}, relocationType, 0x318, 0x2008, 0}}; - const auto filtered = Relinker::MakeStrictUnusedNidFilter()->Filter(references, bytes, input.Text, input.TextVaddr); + const auto filtered = Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(references, bytes, input.Text, input.TextVaddr); require(filtered.size() == 1 && filtered[0].Nid == references[0].Nid, "Strict ELF filter lost callback data or retained unreachable data"); } } @@ -266,7 +402,7 @@ void filterAndPltCompaction() { importThunk(input, 16, 0x2008); const auto bytes = elfFixture(input); const std::vector references = {{"live", {}, 7, 0x300, 0x2000, 0}, {"dead", {}, 7, 0x318, 0x2008, 0}}; - const auto filtered = Relinker::MakeStrictUnusedNidFilter()->Filter(references, bytes, input.Text, input.TextVaddr); + const auto filtered = Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(references, bytes, input.Text, input.TextVaddr); require(filtered.size() == 1 && filtered[0].Nid == references[0].Nid, "Strict ELF filter did not remove only the dead PLT import"); for (std::size_t index = 0; index < references.size(); ++index) { const auto offset = index * 16; @@ -289,13 +425,251 @@ void filterAndPltCompaction() { require(section.RelaPltData.size() == 24 && section.DynSymData.size() == 48, "Compacted PLT retained a dead dynamic symbol"); auto truncated = bytes; truncated.resize(70); - requireFailure([&] { Relinker::MakeStrictUnusedNidFilter()->Filter(references, truncated, input.Text, input.TextVaddr); }, "Truncated ELF was accepted"); + requireFailure([&] { Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(references, truncated, input.Text, input.TextVaddr); }, "Truncated ELF was accepted"); auto exceptional = bytes; write(exceptional, 56, 3); write(exceptional, 176, 0x6474E550); write(exceptional, 184, 0x380); write(exceptional, 208, 8); - requireFailure([&] { Relinker::MakeStrictUnusedNidFilter()->Filter(references, exceptional, input.Text, input.TextVaddr); }, "Exception metadata was ignored"); + requireFailure([&] { Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(references, exceptional, input.Text, input.TextVaddr); }, "Exception metadata was ignored"); +} + +void relativeRelocationTableBounds() { + std::vector bytes(0x400, 0); + 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, 20, 1); + write(bytes, 32, 64); + write(bytes, 52, 64); + write(bytes, 54, 56); + write(bytes, 56, 2); + + write(bytes, 64, 1); + write(bytes, 68, 7); + write(bytes, 72, 0x200); + write(bytes, 80, 0x1000); + write(bytes, 96, 0x100); + write(bytes, 104, 0x100); + + write(bytes, 120, 2); + write(bytes, 124, 6); + write(bytes, 128, 0x100); + write(bytes, 136, 0x2000); + write(bytes, 152, 0x80); + write(bytes, 160, 0x80); + + write(bytes, 0x100, 7); + write(bytes, 0x108, 0x10E8); + write(bytes, 0x110, 8); + write(bytes, 0x118, 24); + write(bytes, 0x120, 0); + write(bytes, 0x128, 0); + + write(bytes, 0x2E8, 0x5000); + write(bytes, 0x2F0, 8); + write(bytes, 0x2F8, 0x6000); + + write(bytes, 0x300, 0x7000); + write(bytes, 0x308, 8); + write(bytes, 0x310, 0x8000); + + auto valid = Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(bytes); + require(valid->TargetOfSlot(0x5000) == 0x6000, "Valid relative relocation was not indexed"); + require(!valid->TargetOfSlot(0x7000).has_value(), "Out of segment relocation was indexed in valid table"); + + auto emptyRelocationTable = bytes; + write(emptyRelocationTable, 0x108, ~std::uint64_t{0}); + write(emptyRelocationTable, 0x118, 0); + auto emptyIndex = Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(emptyRelocationTable); + require(!emptyIndex->TargetOfSlot(0x5000).has_value(), + "Empty relocation table was unexpectedly parsed"); + + auto overextended = bytes; + write(overextended, 0x118, 48); + requireFailure([&] { Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(overextended); }, "Relocation table extending beyond PT_LOAD was accepted"); + + auto badSize = bytes; + write(badSize, 0x118, 25); + requireFailure([&] { Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(badSize); }, "Unaligned relocation table size was accepted"); + + auto pltOverextended = bytes; + write(pltOverextended, 0x100, 23); + write(pltOverextended, 0x108, 0x10E8); + write(pltOverextended, 0x110, 2); + write(pltOverextended, 0x118, 48); + requireFailure([&] { Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(pltOverextended); }, "PLT relocation table extending beyond PT_LOAD was accepted"); + + auto pltBadSize = bytes; + write(pltBadSize, 0x100, 23); + write(pltBadSize, 0x108, 0x10E8); + write(pltBadSize, 0x110, 2); + write(pltBadSize, 0x118, 1); + requireFailure([&] { Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(pltBadSize); }, "Unaligned PLT relocation table size was accepted"); + + auto overflowSize = bytes; + write(overflowSize, 0x118, (~std::uint64_t{0} / 24) * 24); + requireFailure([&] { Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(overflowSize); }, "Relocation table size near UINT64_MAX was accepted"); + + auto overflowRelaVa = bytes; + write(overflowRelaVa, 0x108, ~std::uint64_t{0} - 15); + requireFailure([&] { Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(overflowRelaVa); }, "Relocation table vaddr near UINT64_MAX was accepted"); + + auto overflowLoadOffset = bytes; + write(overflowLoadOffset, 72, ~std::uint64_t{0} - 16); + requireFailure([&] { Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(overflowLoadOffset); }, "PT_LOAD file offset near UINT64_MAX was accepted"); + + auto overflowLoadSize = bytes; + write(overflowLoadSize, 96, ~std::uint64_t{0} - 16); + write(overflowLoadSize, 0x118, 0x300); + requireFailure([&] { Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(overflowLoadSize); }, "Relocation table beyond file with fileSize near UINT64_MAX was accepted"); + + auto overflowDynamicOffset = bytes; + write(overflowDynamicOffset, 128, ~std::uint64_t{0} - 16); + requireFailure([&] { Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(overflowDynamicOffset); }, "PT_DYNAMIC offset near UINT64_MAX was accepted"); +} + +void sceDynlibDataOffsets() { + auto input = fixture(); + emit(input, 0, {0xC3}); + importThunk(input, 16, 0x2000); + auto bytes = elfFixture(input); + bytes.resize(0xA00); + write(bytes, 56, 4); + write(bytes, 176, 2); + write(bytes, 180, 6); + write(bytes, 184, 0x300); + write(bytes, 192, 0x2000); + write(bytes, 208, 0x40); + write(bytes, 216, 0x40); + write(bytes, 224, 8); + write(bytes, 232, 0x61000000); + write(bytes, 236, 4); + write(bytes, 240, 0x500); + write(bytes, 264, 0x500); + write(bytes, 272, 0x500); + write(bytes, 280, 0x10); + write(bytes, 0x300, 0x6100002F); + write(bytes, 0x308, 0x400); + write(bytes, 0x310, 0x61000031); + write(bytes, 0x318, 24); + write(bytes, 0x320, 0x61000033); + write(bytes, 0x328, 24); + write(bytes, 0x900, 0x2040); + write(bytes, 0x908, 8); + write(bytes, 0x910, 0x1010); + + std::string failures; + try { + const auto index = Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(bytes); + if (index->TargetOfSlot(0x2040) != 0x1010) failures += "SCE relative relocation was not indexed; "; + } catch (const std::exception& error) { + failures += std::string("SCE relative relocation index: ") + error.what() + "; "; + } + try { + const std::vector references = {{"live", {}, 6, 0x900, 0x2000, 0}}; + const auto filtered = Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(references, bytes, input.Text, input.TextVaddr); + if (filtered.size() != 1) failures += "SCE relocation target did not keep its import"; + } catch (const std::exception& error) { + failures += std::string("SCE strict filter: ") + error.what(); + } + require(failures.empty(), failures.c_str()); + + auto noSegment = bytes; + write(noSegment, 232, 0x6FFFFF01); + write(noSegment, 0x308, 0x900); + require(Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(noSegment)->TargetOfSlot(0x2040) == 0x1010, + "SCE relocation without PT_SCE_DYNLIBDATA was not read at its absolute file offset"); + const std::vector references = {{"live", {}, 6, 0x900, 0x2000, 0}}; + require(Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(references, noSegment, input.Text, input.TextVaddr).size() == 1, + "Strict filter lost an SCE relocation without PT_SCE_DYNLIBDATA"); + + auto jmprel = bytes; + write(jmprel, 208, 0x60); + write(jmprel, 216, 0x60); + write(jmprel, 0x318, 0); + write(jmprel, 0x330, 0x61000029); + write(jmprel, 0x338, 0x430); + write(jmprel, 0x340, 0x6100002D); + write(jmprel, 0x348, 24); + write(jmprel, 0x930, 0x2050); + write(jmprel, 0x938, 8); + write(jmprel, 0x940, 0x1010); + require(Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(jmprel)->TargetOfSlot(0x2050) == 0x1010, + "SCE JMPREL table was not read at its segment-relative file offset"); + write(jmprel, 0x338, 0x4F0); + requireFailure([&] { (void)Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(jmprel); }, + "SCE JMPREL table past the end of the file was accepted"); + + auto emptyJmprel = jmprel; + write(emptyJmprel, 0x338, 0x430); + write(emptyJmprel, 0x348, 0); + require(!Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(emptyJmprel)->TargetOfSlot(0x2050), + "Valid empty SCE JMPREL table was rejected or parsed"); + write(emptyJmprel, 0x338, 0x501); + requireFailure([&] { (void)Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(emptyJmprel); }, + "Empty SCE JMPREL table past the file was accepted"); + write(emptyJmprel, 0x338, std::numeric_limits::max()); + requireFailure([&] { (void)Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(emptyJmprel); }, + "Empty SCE JMPREL table with overflowing offset was accepted"); + + auto emptyRela = bytes; + write(emptyRela, 0x318, 0); + require(!Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(emptyRela)->TargetOfSlot(0x2040), + "Valid empty SCE RELA table was rejected or parsed"); + write(emptyRela, 0x308, 0x501); + requireFailure([&] { (void)Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(emptyRela); }, + "Empty SCE RELA table past the file was accepted"); + write(emptyRela, 0x308, std::numeric_limits::max()); + requireFailure([&] { (void)Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(emptyRela); }, + "Empty SCE RELA table with overflowing offset was accepted"); + + auto overflow = bytes; + write(overflow, 0x308, std::numeric_limits::max()); + requireFailure([&] { (void)Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(overflow); }, + "SCE relocation file offset overflow was accepted"); + requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(references, overflow, input.Text, input.TextVaddr); }, + "Strict filter accepted an overflowing SCE table offset"); + + auto duplicate = bytes; + write(duplicate, 56, 5); + write(duplicate, 288, 0x61000000); + requireFailure([&] { (void)Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(duplicate); }, + "Duplicate PT_SCE_DYNLIBDATA segments were accepted"); + requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(references, duplicate, input.Text, input.TextVaddr); }, + "Strict filter accepted duplicate PT_SCE_DYNLIBDATA segments"); + + auto sectionRelative = bytes; + write(sectionRelative, 208, 0x70); + write(sectionRelative, 216, 0x70); + write(sectionRelative, 0x330, 0x61000039); + write(sectionRelative, 0x338, 0x100); + write(sectionRelative, 0x340, 0x6100003B); + write(sectionRelative, 0x348, 24); + write(sectionRelative, 0x350, 0x6100003F); + write(sectionRelative, 0x358, 48); + write(sectionRelative, 0x61C, 3); + write(sectionRelative, 0x908, (std::uint64_t{1} << 32) | 8); + require(Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(references, sectionRelative, input.Text, input.TextVaddr).size() == 1, + "Strict filter rejected a RELATIVE relocation against a zero-value section symbol"); + auto valuedSection = sectionRelative; + write(valuedSection, 0x620, 0x10); + requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(references, valuedSection, input.Text, input.TextVaddr); }, + "Strict filter accepted a RELATIVE relocation against a section symbol with a value"); + auto untyped = sectionRelative; + write(untyped, 0x61C, 0); + requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(references, untyped, input.Text, input.TextVaddr); }, + "Strict filter accepted a RELATIVE relocation against a non-section symbol"); + auto outside = sectionRelative; + write(outside, 0x908, (std::uint64_t{2} << 32) | 8); + requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(references, outside, input.Text, input.TextVaddr); }, + "Strict filter accepted a RELATIVE relocation symbol outside the symbol table"); } } @@ -306,16 +680,22 @@ int main() { ud1DeadTail(); conditionalTailCall(); fallthroughAfterZeroPadding(); + addressSizeOverrideImport(); callbackAndRelocationRoots(); registerImportCall(); mergedJumpTable(); conservativeIndirectTargets(); isolatedFunctionCycle(); relativeTableAndWholeFunction(); + indexedTableImports(); + exceptionLeb128Bounds(); vectorInstructionLengths(); exceptionLandingPads(); + exceptionDataWithoutPersonality(); filterCallbackDataImports(); filterAndPltCompaction(); + relativeRelocationTableBounds(); + sceDynlibDataOffsets(); std::cout << "Strict NID filter tests passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/relinker/relinker/tests/SyscallScannerTests.cpp b/core/relinker/relinker/tests/SyscallScannerTests.cpp new file mode 100644 index 0000000000..65901a6f33 --- /dev/null +++ b/core/relinker/relinker/tests/SyscallScannerTests.cpp @@ -0,0 +1,162 @@ +#include "ElfFixture.hpp" +#include "RelinkerProcess.hpp" +#include +#include +#include +#include + +namespace { + +using namespace RelinkerTests; + +void require(const bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +const Bytes kConstantStub = { + 0x48, 0xC7, 0xC0, 0x09, 0x00, 0x00, 0x00, + 0x49, 0x89, 0xCA, + 0x0F, 0x05, + 0xC3, +}; + +const Bytes kUnsignedStub = { + 0xB8, 0x39, 0x00, 0x00, 0x00, + 0x0F, 0x05, + 0xC3, +}; + +const Bytes kRegisterStub = { + 0x48, 0x89, 0xF8, + 0x0F, 0x05, + 0xC3, +}; + +const Bytes kInt80Stub = { + 0xB8, 0x0B, 0x00, 0x00, 0x00, + 0xCD, 0x80, + 0xC3, +}; + +const Bytes kSysenterStub = { + 0x0F, 0x34, + 0xC3, +}; + +const Bytes kSysretStub = { + 0x48, 0xC7, 0xC0, 0x09, 0x00, 0x00, 0x00, + 0x0F, 0x07, + 0xC3, +}; + +const Bytes kClobberedStub = { + 0x48, 0xC7, 0xC0, 0x09, 0x00, 0x00, 0x00, + 0x48, 0x83, 0xC0, 0x01, + 0x0F, 0x05, + 0xC3, +}; + +const Bytes kPrefixedConstantStub = { + 0x66, 0x48, 0xC7, 0xC0, 0x09, 0x00, 0x00, 0x00, + 0xF2, 0x49, 0x89, 0xCA, + 0xF3, 0x0F, 0x05, + 0xC3, +}; + +const Bytes kPrefixedClobberedStub = { + 0x66, 0x48, 0xC7, 0xC0, 0x09, 0x00, 0x00, 0x00, + 0x66, 0x48, 0x83, 0xC0, 0x01, + 0xF3, 0x0F, 0x05, + 0xC3, +}; + +const Bytes kSdkGenericStub = { + 0x48, 0x89, 0xF8, 0x48, 0x89, 0xF7, 0x48, 0x89, 0xD6, 0x48, 0x89, 0xCA, + 0x4D, 0x89, 0xC2, 0x4D, 0x89, 0xC8, 0x4C, 0x8B, 0x4C, 0x24, 0x08, + 0x0F, 0x05, 0xC3, +}; + +std::string RunScanner(const std::string& binary, const Bytes& code) { + const TempDirectory directory; + const auto input = directory.Path() / "input.elf"; + const auto output = directory.Path() / "output.elf"; + WriteFile(input, MakeExecutable(code)); + const auto run = RunRelinker(binary, {"--skip-sce-module", input.string(), output.string()}, directory.Path() / "relinker.log"); + require(run.ExitCode != 0, "Relinker accepted a forbidden syscall instruction:\n" + run.Output); + return run.Output; +} + +void RequireNumber(const std::string& binary, const Bytes& code, const std::string& expected, const std::string& description) { + const auto output = RunScanner(binary, code); + require(output.find("Forbidden syscall instruction at code offset 0x") != std::string::npos, + "Relinker did not report the " + description + " as forbidden:\n" + output); + require(output.find(expected) != std::string::npos, + "Relinker did not report " + expected + " for the " + description + ":\n" + output); +} + +void RequireNoNumber(const std::string& binary, const Bytes& code, const std::string& description) { + const auto output = RunScanner(binary, code); + require(output.find("Forbidden syscall instruction at code offset 0x") != std::string::npos, + "Relinker did not report the " + description + " as forbidden:\n" + output); + require(output.find("syscall number") == std::string::npos, + "Relinker reported a syscall number for the " + description + ":\n" + output); +} + +void RequireSdkStubsAreTrapped(const std::string& binary) { + Bytes code = kConstantStub; + const auto numberedOffset = code.size() - 3u; + code.insert(code.end(), kSdkGenericStub.begin(), kSdkGenericStub.end()); + const auto genericOffset = kConstantStub.size() + kSdkGenericStub.size() - 3u; + const TempDirectory directory; + const auto input = directory.Path() / "input.elf"; + const auto output = directory.Path() / "output.elf"; + WriteFile(input, MakeExecutable(code)); + const auto run = RunRelinker(binary, {"--skip-sce-module", "--trap-sdk-syscall-stubs", input.string(), output.string()}, directory.Path() / "relinker.log"); + require(run.ExitCode == 0, "Relinker did not trap known SDK syscall stubs:\n" + run.Output); + const auto relinked = ReadFile(output); + require(Read(relinked, kCodeOffset + numberedOffset) == 0x0B0Fu, + "Relinker did not replace the numbered SDK syscall with UD2"); + require(Read(relinked, kCodeOffset + genericOffset) == 0x0B0Fu, + "Relinker did not replace the generic SDK syscall with UD2"); +} + +void RequireUnknownSyscallsStillFail(const std::string& binary) { + const TempDirectory directory; + const auto input = directory.Path() / "input.elf"; + const auto output = directory.Path() / "output.elf"; + WriteFile(input, MakeExecutable(kRegisterStub)); + const auto run = RunRelinker(binary, {"--skip-sce-module", "--trap-sdk-syscall-stubs", input.string(), output.string()}, directory.Path() / "relinker.log"); + require(run.ExitCode != 0, "Relinker accepted an unknown syscall in SDK-stub trap mode:\n" + run.Output); + require(run.Output.find("Forbidden syscall instruction at code offset 0x") != std::string::npos, + "Relinker did not report an unknown syscall in SDK-stub trap mode:\n" + run.Output); +} + +} + +int main(const int argc, char** argv) { + try { + require(argc == 2, "usage: syscall_scanner_tests "); + const std::string binary = argv[1]; + + RequireNumber(binary, kConstantStub, "syscall number 9", "64-bit constant stub"); + RequireNumber(binary, kUnsignedStub, "syscall number 57", "32-bit constant stub"); + RequireNumber(binary, kInt80Stub, "syscall number 11", "int 0x80 stub"); + + RequireNoNumber(binary, kRegisterStub, "stub that loads the number from a register"); + RequireNoNumber(binary, kSysenterStub, "sysenter stub"); + RequireNoNumber(binary, kSysretStub, "sysret stub with a constant load in front of it"); + RequireNoNumber(binary, kClobberedStub, "stub whose intervening instruction writes rax"); + + RequireNumber(binary, kPrefixedConstantStub, "syscall number 9", "prefixed constant stub"); + RequireNoNumber(binary, kPrefixedClobberedStub, "prefixed stub whose intervening instruction writes rax"); + + RequireSdkStubsAreTrapped(binary); + RequireUnknownSyscallsStillFail(binary); + + std::cout << "syscall scanner tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << "syscall_scanner_tests: " << error.what() << '\n'; + return 1; + } +} diff --git a/core/relinker/relinker/tests/TestProgress.hpp b/core/relinker/relinker/tests/TestProgress.hpp new file mode 100644 index 0000000000..2c8c9d120d --- /dev/null +++ b/core/relinker/relinker/tests/TestProgress.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +class TestProgress final : public Domain::IProgress { +public: + std::vector Events; + std::string Messages; + + void Report(const Domain::ProgressEvent& event) override { Events.push_back(event); } + void Message(std::string_view message) override { Messages += message; } +}; + +inline TestProgress testProgress; diff --git a/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp b/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp new file mode 100644 index 0000000000..31f110f1a8 --- /dev/null +++ b/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp @@ -0,0 +1,72 @@ +#include +#include +#include +#include +#include + +static void Require(bool ok) { if (!ok) throw std::runtime_error("Unwind record validation failed"); } + +static std::vector Fixture() { + std::vector bytes(128, 0); + bytes[0] = 1; bytes[1] = 0; bytes[2] = 3; bytes[3] = 0; + Io::WriteU64(bytes, 4, 0x1040); Io::WriteU32(bytes, 12, 1); + Io::WriteU64(bytes, 16, 0x2000); Io::WriteU64(bytes, 24, 0x1060); + Io::WriteU32(bytes, 64, 9); bytes[72] = 1; bytes[73] = 0; bytes[74] = 1; bytes[75] = 0x78; bytes[76] = 16; + Io::WriteU32(bytes, 96, 20); Io::WriteU32(bytes, 100, 36); + Io::WriteU64(bytes, 104, 0x2000); Io::WriteU64(bytes, 112, 0x10); + return bytes; +} + +int main() { + try { + Domain::ProgramHeader load{}; load.Type = 1; load.MappedAddress = 0x1000; load.FileSize = 128; + auto eh = load; eh.Type = 0x6474E550; eh.FileSize = 32; + const auto regions = Relinker::UnusedNidFilter::ReadExceptionFunctions(Fixture(), {load, eh}, {}, {}); + Require(regions.size() == 1 && regions[0].Begin == 0x2000 && regions[0].End == 0x2010); + 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); + 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; + case 1: Io::WriteU32(bytes, 96, 0x1000); break; + case 2: Io::WriteU32(bytes, 64, 100); break; + case 3: Io::WriteU32(bytes, 96, 32); bytes.resize(120); break; + 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; + 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 << "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 new file mode 100644 index 0000000000..8bfc0400e6 --- /dev/null +++ b/core/relinker/relinker/tests/WindowsExportAliasTests.cpp @@ -0,0 +1,76 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using Bytes = std::vector; +static void Check(bool ok, const char* message) { if (!ok) throw std::runtime_error(message); } +static void Ph(Bytes& b, int i, unsigned type, unsigned flags, unsigned off, unsigned va, unsigned size) { + const auto p=64+i*56; Io::WriteU32(b,p,type); Io::WriteU32(b,p+4,flags); + Io::WriteU64(b,p+8,off);Io::WriteU64(b,p+16,va);Io::WriteU64(b,p+24,va); + Io::WriteU64(b,p+32,size);Io::WriteU64(b,p+40,size);Io::WriteU64(b,p+48,8); +} +static Bytes Fixture(bool reverse=false) { + Bytes b(0x4000);std::memcpy(b.data(),"\x7f" "ELF\x02\x01\x01",7); + Io::WriteU16(b,16,3);Io::WriteU16(b,18,62);Io::WriteU32(b,20,1); + Io::WriteU64(b,32,64);Io::WriteU16(b,52,64);Io::WriteU16(b,54,56);Io::WriteU16(b,56,3); + Ph(b,0,1,5,0x1000,0,0x1000);Ph(b,1,1,6,0x2000,0x2000,0x1000); + b[0x1020]=0xc3;b[0x1028]=0xc3; + std::string strings(1,'\0');auto str=[&](const char* s){auto o=strings.size();strings+=s;strings+='\0';return o;}; + const auto a=str(reverse?"alias#B#A":"alias#A#A"),c=str(reverse?"alias#A#A":"alias#B#A"); + const auto imp=str("imported#A#B"),host=str("fixtureHost.prx"),module=str("fixtureModule"); + std::copy(strings.begin(),strings.end(),b.begin()+0x2200); + const std::pair tags[]={{5,0x2200},{10,strings.size()},{6,0x2400},{11,24},{4,0x2600},{7,0x2700},{8,48},{9,24},{1,host},{0x61000043,module},{0x61000045,(1ull<<48)|host},{0,0}}; + Ph(b,2,2,6,0x2000,0x2000,sizeof(tags)); + for(unsigned i=0;i=start&&rva-start static void Reject(F&& f,const char* expected) { + bool rejected=false;try{f();}catch(const Domain::RelinkerException& e){rejected=std::string(e.what()).find(expected)!=std::string::npos;} + Check(rejected,expected); +} +static void Positive(bool reverse) { + const auto bytes=Fixture(reverse);const auto g=Relinker::GuestImageReader().Read("generic.prx",bytes,true); + Check(g.Symbols.size()==4&&g.Bytes==bytes,"input symbols/bytes changed"); + Check(g.Symbols[1].Name=="alias"&&g.Symbols[2].Name=="alias","stripped lookup names"); + Check(g.Symbols[1].QualifiedName==(reverse?"alias#B#A":"alias#A#A")&&g.Symbols[2].QualifiedName==(reverse?"alias#A#A":"alias#B#A"),"qualified names lost"); + Check(g.Symbols[1].Info==18&&g.Symbols[2].Info==18&&g.Symbols[1].Visibility==3&&g.Symbols[2].Visibility==3&&g.Symbols[1].Section==1&&g.Symbols[2].Section==1&&g.Symbols[1].Value==0x20&&g.Symbols[2].Value==0x20&&g.Symbols[1].Size==1&&g.Symbols[2].Size==1,"raw tuple changed"); + Check((Io::ReadU64(g.Dynamic.RelaData,8)>>32)==1&&(Io::ReadU64(g.Dynamic.RelaData,32)>>32)==2,"alias relocation indices changed"); + Check(g.Symbols[3].QualifiedName=="imported#A#B"&&g.Symbols[3].Library=="fixtureHost.prx","qualified import changed"); + Domain::GuestRuntime runtime;const auto pe=Elfpatcher::GuestModuleWriter().WriteWindows(g,runtime); + const auto directory=Offset(pe,Io::ReadU32(pe,Io::ReadU32(pe,0x3c)+24+112)); + Check(Io::ReadU32(pe,directory+20)==1&&Io::ReadU32(pe,directory+24)==1,"multiple PE exports"); + const auto function=Io::ReadU32(pe,Offset(pe,Io::ReadU32(pe,directory+28))); + const auto nameRva=Io::ReadU32(pe,Offset(pe,Io::ReadU32(pe,directory+32))); + Check(std::strcmp(reinterpret_cast(pe.data()+Offset(pe,nameRva)),"alias")==0,"wrong export name"); + Elfpatcher::Windows::WindowsLoadImage image(g.Bytes,g.Headers,false); + Check(function==image.GetRva(0x20,1),"wrong export RVA"); + for(auto target:{0x2800,0x2808})Check(Io::ReadU64(pe,Offset(pe,image.GetRva(target,8)))==Elfpatcher::Windows::ImageBase+function,"wrong alias relocation value"); +} +int main() { + try { + Positive(false);Positive(true); + Reject([]{Relinker::GuestImageReader().Read("generic.prx",Fixture());},"Duplicate or empty export"); + 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"; + } catch(const std::exception& e){std::cerr<= 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 1c56670c4c..5b9a61ac30 100644 --- a/core/relinker/relinker/tests/test_call_registry.py +++ b/core/relinker/relinker/tests/test_call_registry.py @@ -8,34 +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) @@ -57,6 +60,43 @@ def main(): result, output = run_case(relinker, work, "syscall-filter", ["unused-filter=1"], b"\x0f\x05\xc3") assert result.returncode == 2 and "Forbidden syscall instruction" in result.stderr and not output.exists(), result + forbidden = ( + ("rex-syscall", b"\x48\x0f\x05"), + ("operand-syscall", b"\x66\x0f\x05"), + ("address-sysenter", b"\x67\x0f\x34"), + ("segment-sysenter", b"\x2e\x0f\x34"), + ("rex-sysret", b"\x48\x0f\x07"), + ("mixed-sysret", b"\x66\x67\x48\x0f\x07"), + ("operand-int80", b"\x66\xcd\x80"), + ("segment-int80", b"\x64\xcd\x80"), + ) + for name, instruction in forbidden: + result, output = run_case(relinker, work, name, [], b"\x90" + instruction + b"\xc3") + 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) + + result, output = run_case(relinker, work, "skip-prefixed", ["--skip-syscall-check"], b"\x48\x0f\x05\xc3") + assert result.returncode == 0 and output.exists(), (result.stdout, result.stderr) + result, output = run_case(relinker, work, "skip", ["--skip-syscall-check"], b"\x0f\x05\xc3") assert result.returncode == 0 and output.exists(), (result.stdout, result.stderr) second_code = b"\x90" * 16 + b"\xb8\x2a\x00\x00\x00\xc3" diff --git a/core/relinker/relinker/tests/test_executable_tls_relocations.py b/core/relinker/relinker/tests/test_executable_tls_relocations.py new file mode 100644 index 0000000000..fd6ceff424 --- /dev/null +++ b/core/relinker/relinker/tests/test_executable_tls_relocations.py @@ -0,0 +1,154 @@ +"""Initial-exec TLS relocations in the executable (R_X86_64_TPOFF64, DTPOFF64, DTPMOD64), as a PS5 eboot +has them in its GOT. The Linux ELF keeps them dynamic and symbolless, with the variable's offset in the +TLS block as the addend, for the dynamic loader to resolve against where it puts the block; a named one +against a defined STT_TLS symbol is folded the same way, and one against an import stays named, typed +STT_TLS. The Windows and macOS images resolve TPOFF64 and DTPOFF64 statically against their block, +AlignUp(p_memsz, p_align) below the thread pointer, and reject what they cannot express.""" + +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +from test_linux_load_alignment import fixture +from test_macos_output import MachO +from test_tls_function_coverage import pe_bytes_at + +PT_LOAD = 1 +PT_TLS = 7 +DTPMOD64, DTPOFF64, TPOFF64, TPOFF32 = 16, 17, 18, 23 +TLS_MEMSZ, TLS_ALIGN = 0x24, 16 +TLS_BLOCK = 0x30 +STRINGS = b"\0Tls\0Data\0ImportedTls\0" +TLS_NAME, DATA_NAME, IMPORT_NAME = 1, 5, 10 + +NULL, TLS_SYMBOL, DATA_SYMBOL, IMPORTED = 0, 1, 2, 3 + + +def image(relocations, tls=True, symbol_bytes=None): + """The load-alignment fixture with a writable segment (vaddr 0x1000) holding a GOT and an 8-byte + TLS template, a symbol table and the given (offset, type, symbol, addend) relocations.""" + data = fixture() + struct.pack_into("> 32) * 24) + end = elf.index(b"\0", strings + name) + result.append((target, info & 0xffffffff, elf[strings + name:end].decode(), symbol_info & 15, addend)) + return result + + +def macho_bytes_at(data, address, size): + for segment in MachO(data).segments: + if segment["vmaddr"] <= address and address + size <= segment["vmaddr"] + segment["filesize"]: + offset = segment["fileoff"] + address - segment["vmaddr"] + return data[offset:offset + size] + raise AssertionError(f"unmapped address {address:#x}") + + +def main(): + relinker = Path(sys.argv[1]).resolve() + with tempfile.TemporaryDirectory(prefix="anyps5-exec-tls-") as directory: + local = [(0x1000, TPOFF64, NULL, 4), (0x1008, DTPOFF64, NULL, 8), (0x1010, TPOFF64, TLS_SYMBOL, 2)] + result, output = relink(relinker, directory, "linux", image(local + [(0x1018, DTPMOD64, NULL, 0), + (0x1020, TPOFF64, IMPORTED, 0)])) + assert result.returncode == 0, (result.stdout, result.stderr) + elf = output.read_bytes() + relocations = dynamic_relocations(elf) + tls = sorted(relocation for relocation in relocations if relocation[1] in (DTPMOD64, DTPOFF64, TPOFF64)) + assert tls == [(0x1000, TPOFF64, "", 0, 4), (0x1008, DTPOFF64, "", 0, 8), (0x1010, TPOFF64, "", 0, 0x12), + (0x1018, DTPMOD64, "", 0, 0), (0x1020, TPOFF64, "ImportedTls", 6, 0)], relocations + assert any(struct.unpack_from(" {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 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("> 5) % 64)) + if chains: + chains[-1] |= 1 + struct.pack_into("> 5) % 64)) + if chains: + chains[-1] |= 1 + 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' + 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) + recorded = events.read_text() + assert f'args={ARGS:016x} argp={ARGP:016x}' in recorded, recorded + assert f'result={RETURNED}' in recorded, recorded + + def main(): if sys.argv[1] == '--load': import ctypes - host = ctypes.CDLL(sys.argv[2]) + host = ctypes.CDLL(sys.argv[2], mode=ctypes.RTLD_GLOBAL if sys.platform == 'darwin' else ctypes.DEFAULT_MODE) host.SetPendingArgs.argtypes = [ctypes.c_size_t, ctypes.c_void_p] host.SetPendingArgs(ARGS, ARGP) ctypes.CDLL(sys.argv[3]) return relinker = Path(sys.argv[1]).resolve() host = Path(sys.argv[2]).resolve() - native = sys.platform == 'linux' and os.uname().machine == 'x86_64' + linux = sys.platform == 'linux' and os.uname().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' @@ -95,20 +200,34 @@ def main(): assert GETTER in bare_imports, bare_imports assert SETTER in bare_imports, bare_imports assert dynamic_tags(bare_image).get(8) == 48, dynamic_tags(bare_image) - if not native: + mach = work / 'macos' + mach_modules = mach / 'sce_module' + mach_modules.mkdir(parents=True) + (mach_modules / name).write_bytes(args_module()) + mach_source = mach / 'input.elf' + mach_source.write_bytes(executable(name)) + mach_result = subprocess.run([str(relinker), '--macos', '--skip-syscall-check', str(mach_source), str(mach / 'output')], + capture_output=True, text=True, timeout=30) + assert mach_result.returncode == 0, (mach_result.stdout, mach_result.stderr) + mach_converted = mach / 'app0' / 'sce_module' / (name + '.guest.prx') + binds = macho_binds(mach_converted.read_bytes()) + assert ('_' + GETTER).encode() in binds, f'the macOS module does not bind {GETTER}' + assert ('_' + SETTER).encode() in binds, f'the macOS module does not bind {SETTER}' + 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: + libraries = mach / 'libs' + libraries.mkdir() + shutil.copyfile(host, libraries / 'libkernel.prx') + (libraries / 'libc.prx').symlink_to('libkernel.prx') + load_and_record(mach, libraries / 'libkernel.prx', mach_converted, macos_loader(work)) + else: print('Guest module argument tests passed: native execution skipped') return - libraries = work / 'libs' - libraries.mkdir() - shutil.copyfile(host, libraries / 'libc.prx') - events = work / 'events.txt' - run = subprocess.run([sys.executable, __file__, '--load', str(libraries / 'libc.prx'), str(converted)], - 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) - recorded = events.read_text() - assert f'args={ARGS:016x} argp={ARGP:016x}' in recorded, recorded - assert f'result={RETURNED}' in recorded, recorded print('Guest module argument tests passed') diff --git a/core/relinker/relinker/tests/test_guest_module_dir.py b/core/relinker/relinker/tests/test_guest_module_dir.py new file mode 100644 index 0000000000..c80b9ce2b1 --- /dev/null +++ b/core/relinker/relinker/tests/test_guest_module_dir.py @@ -0,0 +1,314 @@ +from pathlib import Path +import json +import os +import struct +import subprocess +import sys +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, module_with_needed +from test_macos_output import MachO + + +def pe_export_names(data): + header, = 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": + assert data.startswith(b"\x7fELF") + tags = dynamic_tags(data) + for address, size, expected in ((7, 8, expected_rela), (23, 2, expected_plt)): + actual = [] + if tags.get(size, 0): + start = file_offset(data, tags[address]) + actual = [struct.unpack_from("= 0, (tags, strings) + actual = strings[offset:end].decode() + configured = run_path if run_path is not None else "$ORIGIN/libs" + path_list = label.startswith("list") or label == "absolute-list" + configured_paths = configured.split(":") if path_list else [configured] + actual_paths = actual.split(":") if path_list else [actual] + expected = [Path(path.replace("$ORIGIN", str(output.parent))).resolve() for path in configured_paths] + resolved = [Path(path.replace("$ORIGIN", str(artifact.parent))).resolve() for path in actual_paths] + if resolved != expected or (label.startswith("absolute") and actual != run_path): + failures.append((str(relative), label, actual, str(resolved), str(expected))) + checked += 1 + for index, run_path in enumerate(("relative/libs", "C:/host/libs", "C:\\host\\libs", "\\\\server\\share")): + output = work / f"invalid-{index}.elf" + result = subprocess.run([str(relinker), "--rpath", run_path, str(source), str(output)], + capture_output=True, text=True, timeout=20) + assert result.returncode == 2 and "run path must be absolute or begin with $ORIGIN" in result.stderr, ( + run_path, result.returncode, result.stdout, result.stderr) + assert not output.exists() + assert not failures, failures + print(f"Guest run path tests passed: {checked} module/path combinations") + + +if __name__ == "__main__": + main() diff --git a/core/relinker/relinker/tests/test_guest_symbol_names.py b/core/relinker/relinker/tests/test_guest_symbol_names.py index a991808308..3580b83e32 100644 --- a/core/relinker/relinker/tests/test_guest_symbol_names.py +++ b/core/relinker/relinker/tests/test_guest_symbol_names.py @@ -4,7 +4,10 @@ import subprocess import sys import tempfile +from guest_import_fixtures import dynamic from test_guest_intel_trampolines import PLAIN_SITE, elf_loads, guest_fixture, main_fixture +from test_guest_module_directories import needed_libraries +from test_windows_import_modules import consumer, provider DYNAMIC = 0x4800 STRINGS = 0x4A00 @@ -36,15 +39,33 @@ def guest_module(symbols): return image -def importing_executable(names): +def needed_module_import(exported, imported, owner, needed_module_tags=(0x6100000F,)): + image = guest_fixture(PLAIN_SITE) + strings, offsets = string_table([exported, imported, owner, owner[:-len(".prx")]]) + assert len(strings) <= 0x40 + image[0x800:0x800 + len(strings)] = strings + 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: + work = Path(directory) + cases = [ + ("no-array", image(), DROPPED), + ("init-before-dynamic", image(DT_INIT_ARRAY, DT_INIT_ARRAYSZ, BEFORE_DYNAMIC, [FUNCTION]), KEPT), + ("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), + ("init-relative-overrides-word", image(DT_INIT_ARRAY, DT_INIT_ARRAYSZ, BEFORE_DYNAMIC, [0x200], + [(BEFORE_DYNAMIC, FUNCTION)]), KEPT), + ("init-relative-terminator", image(DT_INIT_ARRAY, DT_INIT_ARRAYSZ, BEFORE_DYNAMIC, [FUNCTION], + [(BEFORE_DYNAMIC, 0)]), DROPPED), + ("fini-before-dynamic", image(DT_FINI_ARRAY, DT_FINI_ARRAYSZ, BEFORE_DYNAMIC, [FUNCTION]), KEPT), + ("fini-relative-addend", image(DT_FINI_ARRAY, DT_FINI_ARRAYSZ, AFTER_DYNAMIC, [0], + [(AFTER_DYNAMIC, FUNCTION)]), KEPT), + ("preinit-before-dynamic", image(DT_PREINIT_ARRAY, DT_PREINIT_ARRAYSZ, BEFORE_DYNAMIC, [FUNCTION]), KEPT), + ("preinit-os-tags", image(DT_OS_PREINIT_ARRAY, DT_OS_PREINIT_ARRAYSZ, BEFORE_DYNAMIC, [FUNCTION]), KEPT), + ] + for name, elf, expected in cases: + line = filtered(relinker, work, name, elf) + assert line == expected, (name, line, expected) + + result, output = run(relinker, work, "unmapped-array", + 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") + + +if __name__ == "__main__": + main() diff --git a/core/relinker/relinker/tests/test_input_file.py b/core/relinker/relinker/tests/test_input_file.py new file mode 100644 index 0000000000..6146103953 --- /dev/null +++ b/core/relinker/relinker/tests/test_input_file.py @@ -0,0 +1,41 @@ +"""Reject directories and other non-files before allocating an input buffer.""" + +import os +from pathlib import Path +import subprocess +import sys +import tempfile + + +def main(): + relinker = Path(sys.argv[1]).resolve() + with tempfile.TemporaryDirectory(prefix="anyps5-input-file-") as directory: + work = Path(directory) + source_dir = work / "game" + source_dir.mkdir() + missing = work / "missing.elf" + short = work / "short.elf" + short.write_bytes(b"\x7fELF" + bytes(6)) + + def convert(name, source, error, flags): + output = work / (name + ".out") + registry = work / (name + ".registry.json") + result = subprocess.run([str(relinker), "--skip-sce-module", "--registry", *flags, str(source), str(output)], + capture_output=True, text=True, timeout=20) + assert result.returncode == 2 and error in result.stderr and "bad_alloc" not in result.stderr, (name, flags, result) + assert not output.exists() and not registry.exists(), (name, flags, result) + + for flags in ([], ["--windows"], ["--to-intel"], ["--windows", "--to-intel"]): + convert("directory" + "-".join(flags), source_dir, "Cannot open file: " + str(source_dir), flags) + convert("missing", missing, "Cannot open file: " + str(missing), []) + convert("short", short, "File too small for ELF header", []) + if os.name == "posix": + convert("null", Path("/dev/null"), "Cannot open file: /dev/null", []) + fifo = work / "fifo.elf" + os.mkfifo(fifo) + convert("fifo", fifo, "Cannot open file: " + str(fifo), []) + print("Input file tests passed") + + +if __name__ == "__main__": + main() diff --git a/core/relinker/relinker/tests/test_input_nonascii.py b/core/relinker/relinker/tests/test_input_nonascii.py new file mode 100644 index 0000000000..6d9598878b --- /dev/null +++ b/core/relinker/relinker/tests/test_input_nonascii.py @@ -0,0 +1,33 @@ +"""Convert inputs whose directory and file names hold characters outside the local code page.""" + +from pathlib import Path +import subprocess +import sys +import tempfile + +from test_optional_plt import fixture + + +def main(): + relinker = Path(sys.argv[1]).resolve() + names = ["проба", "ゲーム", "próba m²"] + with tempfile.TemporaryDirectory(prefix="anyps5-nonascii-") as directory: + work = Path(directory) / "игры" + work.mkdir() + for name in names: + source = work / (name + ".elf") + source.write_bytes(bytes(fixture())) + output = work / (name + ".exe") + registry = work / (name + ".registry.json") + result = subprocess.run([str(relinker), "--skip-sce-module", "--registry", "--windows", str(source), str(output)], + capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=20) + assert result.returncode == 0, (name, result) + assert output.read_bytes()[:2] == b"MZ" and registry.exists(), (name, result) + output.unlink() + registry.unlink() + source.unlink() + print("Non-ASCII input path tests passed") + + +if __name__ == "__main__": + main() diff --git a/core/relinker/relinker/tests/test_input_preservation.py b/core/relinker/relinker/tests/test_input_preservation.py new file mode 100644 index 0000000000..241dd1f4bc --- /dev/null +++ b/core/relinker/relinker/tests/test_input_preservation.py @@ -0,0 +1,383 @@ +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): + filename = "windows-diagnostics-imports.txt" + for bundled in (False, True): + for alias in ("direct", "hardlink", "symlink"): + for registry in (False, True): + case = work / f"diagnostics-{bundled}-{alias}-{registry}" + modules = case / "sce_module" + modules.mkdir(parents=True) + diagnostics = (modules if bundled and alias == "direct" else case) / filename + source = diagnostics if not bundled and alias == "direct" else case / "input.elf" + original = bytes(main_fixture()) + source.write_bytes(original) + module = diagnostics if bundled and alias == "direct" else modules / "sample.prx" + module_original = bytes(guest_fixture(PLAIN_SITE)) + module.write_bytes(module_original) + protected = module if bundled else source + if alias == "hardlink": + os.link(protected, diagnostics) + elif alias == "symlink": + try: + diagnostics.symlink_to(protected) + except OSError as error: + if os.name != "nt" or error.winerror != 1314: + raise + print("Diagnostic symbolic-link case skipped: Windows symlink privilege unavailable") + continue + output = case / "output.exe" + previous_output = b"previous executable" + output.write_bytes(previous_output) + registry_path = case / "output.registry.json" + previous_registry = b"previous registry" + registry_path.write_bytes(previous_registry) + options = [] if bundled else ["--skip-sce-module"] + if registry: + options.append("--registry") + result = subprocess.run([str(relinker), "--windows", "--windows-diagnostics", *options, + str(source), str(output)], cwd=diagnostics.parent, + capture_output=True, text=True, timeout=20) + owner = "an input module" if bundled else "the input executable" + assert result.returncode == 2 and f"Windows import diagnostics output would overwrite {owner}" in result.stderr, (result.stdout, result.stderr) + assert str(diagnostics) in result.stderr, result.stderr + assert source.read_bytes() == original and module.read_bytes() == module_original + assert diagnostics.read_bytes() == (module_original if bundled else original) + assert output.read_bytes() == previous_output and registry_path.read_bytes() == previous_registry + assert not (case / "app0").exists() + + for enabled in (False, True): + for existing in (False, True): + case = work / f"diagnostics-valid-{enabled}-{existing}" + case.mkdir() + diagnostics = case / filename + source = case / "input.elf" if enabled else diagnostics + original = bytes(main_fixture()) + source.write_bytes(original) + if enabled and existing: + diagnostics.write_bytes(b"previous diagnostics") + output = case / "output.exe" + options = ["--windows-diagnostics"] if enabled else [] + result = subprocess.run([str(relinker), "--windows", "--skip-sce-module", *options, + str(source), str(output)], cwd=case, + capture_output=True, text=True, timeout=20) + assert result.returncode == 0 and output.read_bytes().startswith(b"MZ"), (result.stdout, result.stderr) + assert source.read_bytes() == original + assert diagnostics.read_bytes() == (b"" if enabled else original) + + +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}-{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") + 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()) + source.write_bytes(original) + protected = module if bundled else source + if alias == "hardlink": + os.link(protected, registry) + elif alias == "symlink": + try: + registry.symlink_to(protected) + except OSError as error: + if os.name != "nt" or error.winerror != 1314: + raise + print("Registry symbolic-link case skipped: Windows symlink privilege unavailable") + continue + previous_registry = b"previous registry" + untouched = registries[int(not module_registry)] + untouched.write_bytes(previous_registry) + previous_output = b"previous executable" + output.write_bytes(previous_output) + result = subprocess.run([str(relinker), *options, "--registry", str(source), str(output)], + capture_output=True, text=True, timeout=20) + 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 + assert registry.read_bytes() == (module_original if bundled else original) + assert not (case / "app0").exists() + + +def check_registry_after_failed_patch(relinker, work): + case = work / "registry-failed-patch" + (case / "sce_module").mkdir(parents=True) + (case / "sce_module" / "sample.prx").write_bytes(guest_fixture(PLAIN_SITE)) + (case / "sce_sys").mkdir() + (case / "sce_sys" / "icon0.png").write_bytes(b"\x89PNG\r\n\x1a\n" + bytes(40)) + source = case / "input.elf" + source.write_bytes(main_fixture()) + 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", + 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) + result = subprocess.run([str(relinker), "--windows", "--registry", str(source), str(output)], + capture_output=True, text=True, timeout=20) + assert result.returncode == 2 and "Invalid Windows icon PNG" in result.stderr, (result.stdout, result.stderr) + assert output.read_bytes() == previous_output + assert all(registry.read_bytes() == previous_registry for registry in registries) + assert not (case / "app0").exists() + + +def check_output_directory(relinker, work): + for windows in (False, True): + for existing in (False, True): + case = work / f"output-directory-{windows}-{existing}" + (case / "sce_module").mkdir(parents=True) + (case / "sce_module" / "sample.prx").write_bytes(guest_fixture(PLAIN_SITE)) + source = case / "input.elf" + source.write_bytes(main_fixture()) + target = case / "output" + if existing: + target.mkdir() + output = str(target) if existing else str(target) + os.sep + options = ["--windows"] if windows else [] + result = subprocess.run([str(relinker), *options, str(source), output], cwd=case, + capture_output=True, text=True, timeout=20) + assert result.returncode == 2 and "Executable output is a directory" in result.stderr, (result.stdout, result.stderr) + assert not (case / "app0").exists() and not (target / "app0").exists() + assert target.exists() == existing + + +def check_desktop_inputs(relinker, work): + for desktop in (False, True): + for bundled in (False, True): + for alias in ("direct", "hardlink", "symlink"): + for registry in (False, True): + case = work / f"desktop-{desktop}-{bundled}-{alias}-{registry}" + modules = case / "sce_module" + modules.mkdir(parents=True) + sce_sys = case / "sce_sys" + sce_sys.mkdir() + image = png(1, 1) + (sce_sys / "icon0.png").write_bytes(image) + output = (modules if bundled and alias == "direct" else case) / "output.elf" + paths = [Path(f"{output}.png"), Path(f"{output}.desktop")] + target = paths[int(desktop)] + source = target if not bundled and alias == "direct" else case / "input.elf" + original = bytes(main_fixture()) + source.write_bytes(original) + module = target if bundled and alias == "direct" else modules / "sample.prx" + module_original = bytes(guest_fixture(PLAIN_SITE)) + module.write_bytes(module_original) + protected = module if bundled else source + if alias == "hardlink": + os.link(protected, target) + elif alias == "symlink": + try: + target.symlink_to(protected) + except OSError as error: + if os.name != "nt" or error.winerror != 1314: + raise + print("Desktop symbolic-link case skipped: Windows symlink privilege unavailable") + continue + previous_output = b"previous executable" + output.write_bytes(previous_output) + untouched = paths[int(not desktop)] + previous_auxiliary = b"previous auxiliary output" + untouched.write_bytes(previous_auxiliary) + registry_path = output.with_suffix(".registry.json") + previous_registry = b"previous registry" + registry_path.write_bytes(previous_registry) + options = ["--registry"] if registry else [] + result = subprocess.run([str(relinker), *options, str(source), str(output)], + capture_output=True, text=True, timeout=20) + kind = "Desktop entry" if desktop else "Icon" + owner = "an input module" if bundled else "the input executable" + assert result.returncode == 2 and f"{kind} output would overwrite {owner}" in result.stderr, (result.stdout, result.stderr) + assert str(target) in result.stderr, result.stderr + assert source.read_bytes() == original and module.read_bytes() == module_original + assert output.read_bytes() == previous_output and untouched.read_bytes() == previous_auxiliary + assert registry_path.read_bytes() == previous_registry + assert target.read_bytes() == (module_original if bundled else original) + assert (sce_sys / "icon0.png").read_bytes() == image + assert not (output.parent / "app0").exists() + + +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) + check_output_directory(relinker, work) + 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}" + case.mkdir() + source = case / "input.elf" + source.write_bytes(main_fixture()) + protected = source + if bundled: + modules = case / "sce_module" + modules.mkdir() + protected = modules / "sample.prx" + protected.write_bytes(guest_fixture(PLAIN_SITE)) + output = protected + if alias == "normalized": + (protected.parent / "unused").mkdir() + output = protected.parent / "unused" / ".." / protected.name + elif alias == "hardlink": + output = case / "alias.elf" + os.link(protected, output) + elif alias == "symlink": + output = case / "alias.elf" + try: + output.symlink_to(protected) + except OSError as error: + if os.name != "nt" or error.winerror != 1314: + raise + print("Symbolic-link case skipped: Windows symlink privilege unavailable") + continue + original = source.read_bytes() + module_original = protected.read_bytes() + arguments = [] if bundled else ["--skip-sce-module"] + result = subprocess.run([str(relinker), *options, *arguments, "--registry", + str(source), str(output)], + capture_output=True, text=True, timeout=20) + expected = "Executable output would overwrite an input module" if bundled else "Executable output would overwrite the input executable" + if (result.returncode != 2 or expected not in result.stderr + or source.read_bytes() != original + or protected.read_bytes() != module_original + or output.read_bytes() != module_original + or output.with_suffix(".registry.json").exists() + or (output.parent / "app0").exists()): + raise AssertionError((windows, bundled, alias, result.returncode, + result.stdout, result.stderr)) + + for existing in (False, True): + case = work / f"{windows}-valid-{existing}" + case.mkdir() + source = case / "input.elf" + original = main_fixture() + source.write_bytes(original) + modules = case / "sce_module" + modules.mkdir() + module = modules / "sample.prx" + module_original = guest_fixture(PLAIN_SITE) + module.write_bytes(module_original) + output = case / ("output.exe" if windows else "output.elf") + if not windows: + (case / "sce_sys").mkdir() + image = png(1, 1) + (case / "sce_sys" / "icon0.png").write_bytes(image) + if existing: + Path(f"{output}.png").write_bytes(b"previous icon") + Path(f"{output}.desktop").write_bytes(b"previous desktop entry") + if existing: + output.write_bytes(b"previous output") + result = subprocess.run([str(relinker), *options, "--registry", str(source), str(output)], + capture_output=True, text=True, timeout=20) + converted = case / "app0" / "sce_module" / "sample.prx.guest.prx" + magic = b"MZ" if windows else b"\x7fELF" + if (result.returncode != 0 or not output.read_bytes().startswith(magic) + or not converted.read_bytes().startswith(magic) + or not output.with_suffix(".registry.json").exists() + or source.read_bytes() != original or module.read_bytes() != module_original): + raise AssertionError((windows, existing, result.returncode, result.stdout, result.stderr)) + if not windows: + assert Path(f"{output}.png").read_bytes() == image + assert Path(f"{output}.desktop").read_text().startswith("[Desktop Entry]\n") + print("Input preservation tests passed") + + +if __name__ == "__main__": + main() 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("I", len(data)) + kind + data + struct.pack(">I", zlib.crc32(kind + data)) + + +def png(width, height): + rows = b"".join(b"\0" + bytes((15, 65, 200, 255)) * width for _ in range(height)) + return (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0)) + + chunk(b"IDAT", zlib.compress(rows)) + chunk(b"IEND", b"")) + + +def unescape(value): + result, index = "", 0 + while index < len(value): + if value[index] == "\\" and index + 1 < len(value): + result += {"s": " ", "n": "\n", "t": "\t", "r": "\r", "\\": "\\"}[value[index + 1]] + index += 2 + else: + result += value[index] + index += 1 + return result + + +def unquote(value): + assert value.startswith('"') and value.endswith('"'), value + result, index = "", 1 + while index < len(value) - 1: + if value[index] == "\\": + assert value[index + 1] in '"`$\\', value + index += 1 + elif value[index] == "%": + assert value[index + 1] == "%", value + index += 1 + result += value[index] + index += 1 + return result + + +def entry(path): + lines = path.read_text(encoding="utf-8").splitlines() + assert lines[0] == "[Desktop Entry]", lines + return dict(line.split("=", 1) for line in lines[1:]) + + +def main(): + relinker = Path(sys.argv[1]).resolve() + with tempfile.TemporaryDirectory(prefix="anyps5-desktop-") as directory: + work = Path(directory) + app = work / "app with spaces" + (app / "sce_sys").mkdir(parents=True) + source = app / "eboot.bin" + source.write_bytes(fixture()) + + def relink(name, *options): + output = work / name + result = subprocess.run([str(relinker), "--skip-sce-module", *options, str(source), str(output)], + cwd=work, capture_output=True, text=True, timeout=20) + desktop, icon = Path(f"{output}.desktop"), Path(f"{output}.png") + return result, output, desktop, icon + + result, output, desktop, icon = relink("no-icon") + assert result.returncode == 0, (result.stdout, result.stderr) + assert output.exists() and not desktop.exists() and not icon.exists() + + image = png(64, 64) + (app / "sce_sys" / "icon0.png").write_bytes(image) + (app / "sce_sys" / "param.json").write_text(json.dumps( + {"titleId": "PPSA00000", "defaultLanguage": "en-US", + "localizedParameters": {"en-US": {"titleName": "Sample Title"}}})) + + result, output, desktop, icon = relink("linux") + assert result.returncode == 0, (result.stdout, result.stderr) + assert icon.read_bytes() == image + fields = entry(desktop) + assert fields["Name"] == "Sample Title", fields + assert unquote(unescape(fields["Exec"])) == str(output), fields + assert unescape(fields["Path"]) == str(work) and unescape(fields["Icon"]) == str(icon), fields + assert os.name == "nt" or desktop.stat().st_mode & 0o111 + validator = shutil.which("desktop-file-validate") + if validator: + checked = subprocess.run([validator, str(desktop)], capture_output=True, text=True) + assert checked.returncode == 0 and not checked.stdout.strip(), checked.stdout + + for name, option in (("macos", "--macos"), ("windows.exe", "--windows")): + result, output, desktop, icon = relink(name, option) + assert result.returncode == 0, (name, result.stdout, result.stderr) + assert output.exists() and not desktop.exists() and not icon.exists(), name + + (app / "sce_sys" / "icon0.png").write_bytes(b"not a PNG") + result, output, desktop, icon = relink("bad-icon") + assert result.returncode != 0, (result.stdout, result.stderr) + assert not output.exists() and not desktop.exists() and not icon.exists() + assert "Invalid icon PNG" in result.stderr, result.stderr + + +if __name__ == "__main__": + main() diff --git a/core/relinker/relinker/tests/test_linux_eh_frame_header.py b/core/relinker/relinker/tests/test_linux_eh_frame_header.py index 047a3c56b5..aa6e08da8a 100644 --- a/core/relinker/relinker/tests/test_linux_eh_frame_header.py +++ b/core/relinker/relinker/tests/test_linux_eh_frame_header.py @@ -7,11 +7,12 @@ import tempfile PT_LOAD = 1 PT_DYNAMIC = 2 PT_GNU_EH_FRAME = 0x6474E550 +PT_GNU_STACK = 0x6474E551 PT_SCE_VERSION = 0x6FFFFF01 EH_FRAME_HEADER = (PT_GNU_EH_FRAME, 4, 0x4800, 0x800, 0x800, 16, 16, 4) -def fixture(header_count): +def fixture(header_count, with_frame=True): image = bytearray(0x8000) 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" + + image = fixture(6, with_frame=False) + table_end = 64 + 6 * 56 + 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) == 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" + + for with_frame in (False, True): + image = fixture(5, with_frame=with_frame) + 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) == (8 if with_frame else 7), headers + assert [header for header in headers if header[0] == PT_GNU_EH_FRAME] == ([EH_FRAME_HEADER] if with_frame else []), headers + assert struct.unpack_from("= len(image), "The grown program header table must be moved past the input" + assert elf[64 + 5 * 56:64 + 6 * 56] == image[64 + 5 * 56:64 + 6 * 56], "Bytes after the input program header table changed" + + image = fixture(5, with_frame=False) + headers = image[64:64 + 4 * 56] + headers += struct.pack("= len(image), "The program header table was not moved" if sys.platform.startswith("linux") and platform.machine() in ("x86_64", "AMD64"): - output.chmod(0o755) - for arguments, expected in ((["Z"], -signal.SIGTRAP), (["Z", "extra"], -signal.SIGILL)): - executed = subprocess.run([str(output), *arguments], capture_output=True, timeout=20) - assert executed.returncode == expected, (arguments, executed.returncode) + for executable in (output, relocated): + executable.chmod(0o755) + for arguments, expected in ((["Z"], -signal.SIGTRAP), (["Z", "extra"], -signal.SIGILL)): + executed = 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)) + far = Path(directory) / ("far-%x.elf" % address) + result = subprocess.run([str(relinker), "--skip-sce-module", str(source), str(far)], + 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) + 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_executable_exports.py b/core/relinker/relinker/tests/test_linux_executable_exports.py new file mode 100644 index 0000000000..bc27b67838 --- /dev/null +++ b/core/relinker/relinker/tests/test_linux_executable_exports.py @@ -0,0 +1,54 @@ +import os +from pathlib import Path +import platform +import struct +import subprocess +import sys +import tempfile +from test_linux_load_alignment import fixture + + +def main(): + if sys.platform != "linux" or platform.machine() != "x86_64": + return + relinker = Path(sys.argv[1]).resolve() + with tempfile.TemporaryDirectory(prefix="anyps5-main-export-") as directory: + work = Path(directory) + probe = work / "probe.c" + probe.write_text( + "#include \n" + "#include \n" + "void check(void) {\n" + " int (*function)(void) = dlsym(RTLD_DEFAULT, \"FixtureExport\");\n" + " _exit(function && function() == 42 ? 42 : 1);\n" + "}\n") + library = work / "probe.so" + subprocess.run([os.environ.get("CC", "cc"), "-shared", "-fPIC", str(probe), "-o", str(library), "-ldl", "-Wl,-init,check"], check=True) + image = fixture() + strings = b"\0FixtureExport#A#B\0".ljust(32, b"\0") + str(library).encode() + b"\0" + image[0x4600:0x4600 + len(strings)] = strings + image[0x4210:0x4216] = bytes.fromhex("b82a000000c3") + struct.pack_into(" struct.unpack_from(" 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_long_paths.py b/core/relinker/relinker/tests/test_long_paths.py new file mode 100644 index 0000000000..e5c5bb857f --- /dev/null +++ b/core/relinker/relinker/tests/test_long_paths.py @@ -0,0 +1,43 @@ +"""Convert an input and write outputs whose paths exceed the 260 characters of the Windows MAX_PATH limit.""" + +import os +from pathlib import Path +import subprocess +import sys +import tempfile + +from test_guest_intel_trampolines import main_fixture + + +def long_paths_enabled(): + if os.name != "nt": + return True + import winreg + try: + with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SYSTEM\CurrentControlSet\Control\FileSystem") as key: + return winreg.QueryValueEx(key, "LongPathsEnabled")[0] == 1 + except OSError: + return False + + +def main(): + relinker = Path(sys.argv[1]).resolve() + if not long_paths_enabled(): + print("skipped, Windows long paths are disabled: LongPathsEnabled is not 1") + sys.exit(77) + with tempfile.TemporaryDirectory(prefix="anyps5-long-path-") as directory: + work = Path(directory).joinpath(*["long-path-component-" + "x" * 40] * 5) + work.mkdir(parents=True) + source = work / "input.elf" + source.write_bytes(main_fixture()) + for options, name in (([], "output.elf"), (["--windows", "--registry"], "output.exe")): + output = work / name + assert len(str(output)) > 260, output + result = subprocess.run([str(relinker), "--skip-sce-module", *options, str(source), str(output)], + capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=20) + assert result.returncode == 0 and output.is_file(), (options, result) + print("Long path tests passed") + + +if __name__ == "__main__": + main() diff --git a/core/relinker/relinker/tests/test_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 87b742b245..694ed2e2eb 100644 --- a/core/relinker/relinker/tests/test_macos_output.py +++ b/core/relinker/relinker/tests/test_macos_output.py @@ -14,10 +14,12 @@ 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 MH_EXECUTE = 2 +MH_PIE = 0x200000 LC_SEGMENT_64 = 0x19 LC_LOAD_DYLIB = 0xC LC_RPATH = 0x8000001C @@ -34,8 +36,8 @@ GS_LOAD = bytes.fromhex("65488b04c5" "00000000") class MachO: def __init__(self, data): self.data = data - magic, cputype, _, filetype, ncmds, sizeofcmds, _, _ = struct.unpack_from("> 32) * 24) + imports.append((target, info & 0xffffffff, addend, strings[name:strings.index(b"\0", name)], bind_type, section, value, size)) + return sorted(imports) + + +def main(): + relinker = Path(sys.argv[1]).resolve() + with tempfile.TemporaryDirectory(prefix="anyps5-null-symbol-") as directory: + work = Path(directory) + for index in (1, 0): + source = work / f"null-entry-{index}.elf" + source.write_bytes(image_with(index=index)) + for target, options, magic in TARGETS: + for level in (0, 1): + output = work / f"null-entry-{index}-{target}-{level}.out" + result = subprocess.run([str(relinker), "--skip-sce-module", "--registry", f"unused-filter={level}", + *options, str(source), str(output)], capture_output=True, text=True, timeout=20) + assert result.returncode == 0, (index, target, level, result.stdout, result.stderr) + data = output.read_bytes() + assert data.startswith(magic), (index, target) + registry = json.loads(output.with_suffix(".registry.json").read_text(encoding="utf-8")) + assert registry == [], (index, target, level, registry) + if target == "linux": + assert linux_imports(data) == [(GLOB_DAT_SLOT, 6, 0, b"", 0x20, 0, 0, 0), + (ABSOLUTE_SLOT, 1, ADDEND, b"", 0x20, 0, 0, 0)], (index, level) + if target == "windows" and os.name == "nt": + run = subprocess.run([str(output)], capture_output=True, timeout=20) + assert run.returncode == 42, (index, level, run.returncode, run.stderr) + source = work / "named-unnamed.elf" + source.write_bytes(image_with(struct.pack("> 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) + header_count = 4 + interpreter_pointer + tls + struct.pack_into("> 3), 0xb8 | (target & 7)]) + struct.pack("= 8 else b"") + bytes([0x50 | (target & 7)])) + emit(bytes.fromhex("48 8d a4 24 00 ff ff ff")) + emit(fs_load(0, 0)) + for displacement, value in REGISTER_VALUES.items(): + immediate(1, value) + emit(bytes([0x48, 0x89, 0x48, displacement & 255])) + markers = [0x1020304050607000 + target * 0x101 for target in range(16)] + expected = list(markers) + for target in range(16): + if target != 4: + immediate(target, markers[target]) + for target, value in address.items(): + immediate(target, value) + expected[target] = value & mask + value = REGISTER_VALUES[offset] + base_val = expected[register] if wide else expected[register] & 0xffffffff + operand_val = value if wide else value & 0xffffffff + if opcode == 0x33: + res = base_val ^ operand_val + elif opcode == 0x23: + res = base_val & operand_val + elif opcode == 0x0b: + res = base_val | operand_val + elif opcode == 0x03: + res = (base_val + operand_val) & (mask if wide else 0xffffffff) + elif opcode == 0x2b: + res = (base_val - operand_val) & (mask if wide else 0xffffffff) + elif opcode == 0x3b: + res = base_val + elif opcode == 0x13: + res = (base_val + operand_val + (flags & 1)) & (mask if wide else 0xffffffff) + elif opcode == 0x1b: + res = (base_val - operand_val - (flags & 1)) & (mask if wide else 0xffffffff) + else: + raise ValueError(f"Unknown opcode: {opcode}") + if opcode != 0x3b: + expected[register] = res if wide else res & 0xffffffff + emit(b"\x68" + struct.pack("> 3) << 2, 0x89, 0x84 | (target & 7) << 3, 0x24]) + struct.pack("= 8 else b"") + bytes([0x58 | (target & 7)])) + emit(b"\xc3") + for position in failures: + struct.pack_into("> 3) << 2]) if wide or register >= 8 else b"") + alu = rex_alu + bytes([opcode, 0xc0 | (register & 7) << 3]) + return bytes.fromhex("48 8d 64 24 80 51 50") + address + b"\x50", b"\x59" + load + alu + bytes.fromhex("58 59 48 8d a4 24 80 00 00 00") + + + def make_image(transfer, metadata, extent=None, body=None): image = fixture() image.extend(b"\x90" * 0x1000) @@ -370,6 +491,25 @@ def make_image(transfer, metadata, extent=None, body=None): return image +def make_eof_segment_prefix_image(): + image = bytearray(0x1000) + image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9) + struct.pack_into("> 3) << 2), opcode, 0xc0 | ((register & 7) << 3)]) + assert expected in stub, (case, expected.hex(), stub.hex()) + if args.group in ("all", "alu_execution"): + for name, image in alu_execution_cases(): + convert(name, image) + if args.group in ("all", "register_address_loads"): + for name, image, address, instruction, register, wide, moved in register_load_cases(): + convert(name, image, tls_address=address) + pe = (work / (name + ".exe")).read_bytes() + patched = pe_bytes_at(pe, 0x10000 + address, 5) + stub = pe_bytes_at(pe, 0x10000 + address + 5 + struct.unpack_from("= 0, (name, stub.hex()) + displacement = struct.unpack_from("> 3) << 2), opcode, 0xc0 | ((register & 7) << 3)]) - assert expected in stub, (case, expected.hex(), stub.hex()) - for name, image in alu_execution_cases(): - convert(name, image) - for name, image, address, instruction, register, wide, moved in register_load_cases(): - convert(name, image, tls_address=address) - pe = (work / (name + ".exe")).read_bytes() - patched = pe_bytes_at(pe, 0x10000 + address, 5) - stub = pe_bytes_at(pe, 0x10000 + address + 5 + struct.unpack_from(" 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 new file mode 100644 index 0000000000..eda91aa3a0 --- /dev/null +++ b/core/relinker/relinker/tests/test_unused_filter_indirect.py @@ -0,0 +1,153 @@ +import json +import struct +import subprocess +import sys +import tempfile +from pathlib import Path + +CODE = 0x1000 +SLOT = 0x800 +POINTER = 0x810 +TABLE = 0x820 +FUNCTION = CODE + 0x40 +SECOND_FUNCTION = CODE + 0x60 + + +def rel(next_address, target): + return struct.pack(" {kept};" in result.stdout, (name, result.stdout) + return kept == 1 + + +def exported_fixture(binding, defined, sections): + image = build(at_offset([(0, b"\xc3"), (0x40, load_slot(FUNCTION))])) + strings = b"\0foo\0lib.so\0exported\0" + image[0x600:0x600 + len(strings)] = strings + struct.pack_into("> 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): + image = executable(owner, symbol) + image[0x4898 + 4] = (STB_WEAK << 4) | 2 + code = (b'\x48\x8b\x05' + struct.pack('> 4 + 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) + + for name, image, symbol, expected in (('linux-weak', weak_executable('a.prx', 'absent#A#B'), 'absent', STB_WEAK), + ('linux-global', executable('a.prx', 'absent#A#B'), 'absent', STB_GLOBAL)): + source = work / (name + '.elf') + source.write_bytes(image) + output = work / (name + '.out') + result = subprocess.run([str(relinker), '--skip-sce-module', str(source), str(output)], + capture_output=True, text=True, timeout=30) + assert result.returncode == 0, (name, result.stdout, result.stderr) + bindings = import_bindings(output.read_bytes()) + assert bindings.get(symbol) == expected, (name, bindings) + + for name, symbol, expected in (('windows-weak-missing', 'absent#A#B', 33), + ('windows-weak-present', 'shared#A#B', 11)): + case = work / name + (case / 'prx').mkdir(parents=True) + (case / 'prx' / 'a.prx').write_bytes(provider(11)) + source = case / 'input.elf' + source.write_bytes(weak_executable('a.prx', symbol)) + output = case / 'output.exe' + result = subprocess.run([str(relinker), '--windows', str(source), str(output)], + capture_output=True, text=True, timeout=30) + assert result.returncode == 0, (name, result.stdout, result.stderr) + if os.name == 'nt': + run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30) + assert run.returncode == expected, (name, run.returncode, run.stdout, run.stderr) + + 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') + + +if __name__ == '__main__': + main() diff --git a/core/relinker/relinker/tests/test_windows_address_space.py b/core/relinker/relinker/tests/test_windows_address_space.py index d2720daf6b..8ad7e7f792 100644 --- a/core/relinker/relinker/tests/test_windows_address_space.py +++ b/core/relinker/relinker/tests/test_windows_address_space.py @@ -1,4 +1,4 @@ -"""Keep the main thread stack that Windows creates before libc reserves the guest arena below it.""" +"""Keep converted Windows images inside the console's 40-bit address window: the executable loads at its fixed low base, guest modules keep relocating, and the main thread stack that Windows creates before libc reserves the guest arena stays below it.""" import os from pathlib import Path @@ -7,6 +7,7 @@ import subprocess import sys import tempfile +from test_guest_module_directories import module_with_symbol from test_optional_plt import fixture GUEST_ARENA_START = 0x200000000 @@ -14,9 +15,28 @@ DYNAMIC_BASE = 0x40 NX_COMPAT = 0x100 -def dll_characteristics(pe): +def optional_header(pe): pe_offset = struct.unpack_from(" 0, "executable fixture has no base relocations" + assert dll_characteristics(executable) == NX_COMPAT, f"DllCharacteristics {dll_characteristics(executable):#x}" + assert image_base(executable) < GUEST_ARENA_START, f"ImageBase {image_base(executable):#x}" if os.name == "nt": for _ in range(8): executed = subprocess.run([str(output)], capture_output=True, text=True, timeout=20) assert executed.returncode < GUEST_ARENA_START >> 32, \ f"stack not below {GUEST_ARENA_START:#x}: exit code {executed.returncode:#x}" + module_case = work / "module" + (module_case / "prx").mkdir(parents=True) + (module_case / "prx" / "provider.prx").write_bytes(module_with_relative_relocation()) + module_source = module_case / "input.elf" + module_source.write_bytes(fixture()) + module_output = module_case / "output.exe" + result = subprocess.run([str(relinker), "--windows", str(module_source), str(module_output)], + capture_output=True, text=True, timeout=30) + assert result.returncode == 0, (result.stdout, result.stderr) + artifact = module_case / "app0" / "prx" / "provider.prx.guest.prx" + assert artifact.is_file(), list((module_case / "app0").rglob("*")) + module = artifact.read_bytes() + assert relocation_directory_size(module) > 0, "module fixture has no base relocations" + assert dll_characteristics(module) == DYNAMIC_BASE | NX_COMPAT, \ + f"DllCharacteristics {dll_characteristics(module):#x}" print("Windows address space integration tests passed") diff --git a/core/relinker/relinker/tests/test_windows_bss.py b/core/relinker/relinker/tests/test_windows_bss.py new file mode 100644 index 0000000000..088ebc5a33 --- /dev/null +++ b/core/relinker/relinker/tests/test_windows_bss.py @@ -0,0 +1,129 @@ +"""Leave the zero-filled tail of PT_LOAD memory out of the Windows file and let the loader zero-fill it.""" + +import os +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +from test_optional_plt import fixture + +BSS = 64 << 20 +FILE_ALIGNMENT = 0x200 +SECTION_ALIGNMENT = 0x1000 +LOAD_RVA = 0x10000 + + +def sections(pe): + header = struct.unpack_from("= memory_size and rva + virtual_size <= image_size, first + assert 0 < raw_size < SECTION_ALIGNMENT * 2, first + for name, virtual_size, rva, raw_size, raw in table: + assert raw_size % FILE_ALIGNMENT == 0 and raw % FILE_ALIGNMENT == 0, name + assert raw_size <= -(-virtual_size // FILE_ALIGNMENT) * FILE_ALIGNMENT, name + assert (raw_size == 0) == (raw == 0) and raw + raw_size <= len(pe), name + mapped = bytearray(image_size) + for name, virtual_size, rva, raw_size, raw in table: + size = min(raw_size, virtual_size) + mapped[rva:rva + size] = pe[raw:raw + size] + loaded = mapped[LOAD_RVA:LOAD_RVA + memory_size] + assert loaded[0x210:0x21a] == image[0x210:0x21a] + assert not any(loaded[0x1000:]) + 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(" +#include +#include +#include + +namespace { + +class BrowserProgress final : public Domain::IProgress { +public: + explicit BrowserProgress(emscripten::val callback) : callback(std::move(callback)) {} + + void Report(const Domain::ProgressEvent& event) override { + auto report = emscripten::val::object(); + report.set("stage", static_cast(event.Stage)); + report.set("input", event.Input); + report.set("completed", static_cast(event.Completed)); + report.set("total", static_cast(event.Total)); + callback(report); + } + + void Message(std::string_view message) override { + auto report = emscripten::val::object(); + report.set("message", std::string(message)); + callback(report); + } + +private: + emscripten::val callback; +}; + +std::vector ReadBytes(const emscripten::val& bytes, std::size_t& remaining) { + if (!bytes.instanceof(emscripten::val::global("Uint8Array"))) throw std::invalid_argument("Expected Uint8Array input"); + const auto length = bytes["length"].as(); + if (length > remaining) throw std::invalid_argument("Executable, modules and icon exceed the 256 MiB conversion limit"); + remaining -= length; + std::vector result(length); + if (length != 0) emscripten::val(emscripten::typed_memory_view(length, result.data())).call("set", bytes); + return result; +} + +emscripten::val CopyBytes(const std::vector& bytes) { + return emscripten::val::global("Uint8Array").new_(emscripten::val(emscripten::typed_memory_view(bytes.size(), bytes.data()))); +} + +emscripten::val Convert(const emscripten::val& request, const emscripten::val& callback) { + auto result = emscripten::val::object(); + try { + const auto target = request["target"].as(); + if (target != "windows" && target != "linux") throw std::invalid_argument("Unknown target system"); + const auto name = request["name"].as(); + if (name.empty() || name.find_first_of("/\\") != std::string::npos || name == "." || name == "..") throw std::invalid_argument("Invalid executable name"); + std::size_t remaining = 256 * 1024 * 1024; + Relinker::ConversionInput input; + input.InputPath = Io::NativePath("/input/" + name); + input.OutputPath = Io::NativePath("/output/" + request["outputName"].as()); + if (input.OutputPath.parent_path() != "/output") throw std::invalid_argument("Invalid output name"); + input.ModuleRoot = "/input"; + input.Executable = ReadBytes(request["executable"], remaining); + const auto modules = request["modules"]; + const auto count = modules["length"].as(); + if (count > 4096) throw std::invalid_argument("Too many modules"); + for (unsigned index = 0; index < count; ++index) { + const auto module = modules[index]; + const auto path = Io::NativePath(module["name"].as()); + if (path.is_absolute() || path.lexically_normal() != path || path.begin() == path.end() || *path.begin() != "sce_module") throw std::invalid_argument("Invalid module path"); + input.Modules.push_back({Io::NativePath("/input") / path, ReadBytes(module["bytes"], remaining)}); + } + input.RuntimeDirectories.push_back("/input/sce_module"); + if (!request["icon"].isNull()) input.Icon = ReadBytes(request["icon"], remaining); + Relinker::ConversionOptions options; + options.Target = target == "windows" ? Relinker::TargetSystem::Windows : Relinker::TargetSystem::Linux; + options.WindowsGui = false; + options.WindowsDiagnostics = target == "windows"; + options.ToIntel = request["toIntel"].as(); + BrowserProgress progress(callback); + auto output = Relinker::Conversion(progress).Convert(std::move(input), options); + result.set("executable", CopyBytes(output.Executable)); + result.set("diagnostics", output.WindowsDiagnostics); + auto artifacts = emscripten::val::array(); + for (const auto& module : output.Modules) { + auto artifact = emscripten::val::object(); + artifact.set("name", Io::Utf8Path(module.Path.lexically_relative("/output"))); + artifact.set("bytes", CopyBytes(module.Bytes)); + artifacts.call("push", artifact); + } + result.set("modules", artifacts); + result.set("ok", true); + } catch (const std::exception& error) { + result.set("ok", false); + result.set("error", std::string(error.what())); + } + return result; +} + +} + +EMSCRIPTEN_BINDINGS(relinker) { + emscripten::function("Convert", &Convert); +} diff --git a/core/relinker/wasm/CMakeLists.txt b/core/relinker/wasm/CMakeLists.txt new file mode 100644 index 0000000000..b92335d011 --- /dev/null +++ b/core/relinker/wasm/CMakeLists.txt @@ -0,0 +1,28 @@ +add_executable(relinker_wasm Bindings.cpp) +target_link_libraries(relinker_wasm PRIVATE relinker_core) +target_compile_options(relinker_wasm PRIVATE -fexceptions) +target_link_options(relinker_wasm PRIVATE + --bind + --no-entry + -fexceptions + -sMODULARIZE=1 + -sEXPORT_ES6=1 + -sEXPORT_NAME=createRelinker + -sALLOW_MEMORY_GROWTH=1 + -sMAXIMUM_MEMORY=2147483648 + -sSTACK_SIZE=1048576 + -sFILESYSTEM=0 + -sENVIRONMENT=web,worker,node +) +set_target_properties(relinker_wasm PROPERTIES + SUFFIX ".mjs" + CXX_EXTENSIONS OFF + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/web/native" +) + +find_package(Python3 REQUIRED COMPONENTS Interpreter) +add_custom_target(relinker_web + COMMAND "${Python3_EXECUTABLE}" "${PROJECT_SOURCE_DIR}/tools/build_web.py" --build "${CMAKE_BINARY_DIR}" + DEPENDS relinker_wasm + VERBATIM +) diff --git a/core/shader/recompiler/BdaAbi.hpp b/core/shader/recompiler/BdaAbi.hpp index aa31e43e9d..0c84230b80 100644 --- a/core/shader/recompiler/BdaAbi.hpp +++ b/core/shader/recompiler/BdaAbi.hpp @@ -1,6 +1,7 @@ #ifndef CORE_SHADER_RECOMPILER_BDAABI_HPP #define CORE_SHADER_RECOMPILER_BDAABI_HPP +#include #include #include #include @@ -26,8 +27,15 @@ struct Range { std::uint32_t reserved; }; +inline bool RangesOrdered(const Range* ranges, std::size_t count) { + for (std::size_t index = 1; index < count; ++index) { + if (ranges[index].begin < ranges[index - 1].begin) return false; + } + return true; +} + enum class FaultState : std::uint32_t { Empty, Writing, Ready }; -enum class FaultReason : std::uint32_t { Unmapped = 1, Permission, Overflow, InvalidTable, InvalidRectangle, LoopLimit, Unaligned, InvalidDescriptor }; +enum class FaultReason : std::uint32_t { Unmapped = 1, Permission, Overflow, InvalidTable, InvalidRectangle, LoopLimit, Unaligned, InvalidDescriptor, ImageTable, UnlinkedCall }; struct Fault { FaultState state; @@ -36,22 +44,57 @@ struct Fault { std::uint32_t bytes; std::uint32_t stage; std::uint32_t instruction; - std::uint32_t reserved; + std::uint32_t otherFaults; }; static_assert(std::is_standard_layout_v
&& 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 WrittenPageSlots = 4096; +inline constexpr std::uint32_t WrittenChunkShift = 17; +inline constexpr std::uint32_t WrittenChunkPages = 1u << (WrittenChunkShift - WrittenPageShift); +inline constexpr std::uint32_t WrittenPageSlots = 2048; inline constexpr std::uint32_t WrittenPageProbes = 8; +inline constexpr std::uint64_t WrittenAddressLimit = std::uint64_t{0xffffffffu} << WrittenChunkShift; inline constexpr std::uint32_t WrittenOverflowWord = sizeof(Fault) / sizeof(std::uint32_t); -inline constexpr std::uint32_t WrittenSlotsWord = WrittenOverflowWord + 1; -inline constexpr std::size_t FaultBufferBytes = (WrittenSlotsWord + WrittenPageSlots) * sizeof(std::uint32_t); +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::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); + +inline constexpr std::uint32_t WrittenChunkKey(std::uint64_t address) { + return static_cast(address >> WrittenChunkShift) + 1u; +} + +template +bool ForEachWrittenRange(const std::uint32_t* words, TMark&& mark) { + const auto count = words[WrittenCountWord]; + if (count > WrittenPageSlots) return false; + for (std::uint32_t claim = 0; claim < count; ++claim) { + const auto slot = words[WrittenListWord + claim]; + if (slot >= WrittenPageSlots || words[WrittenSlotsWord + 2 * slot] == 0) return false; + } + for (std::uint32_t claim = 0; claim < count; ++claim) { + const auto slot = words[WrittenListWord + claim]; + const auto key = words[WrittenSlotsWord + 2 * slot]; + auto mask = words[WrittenSlotsWord + 2 * slot + 1]; + const auto base = static_cast(key - 1u) << WrittenChunkShift; + while (mask != 0) { + const auto first = static_cast(std::countr_zero(mask)); + const auto pages = static_cast(std::countr_one(mask >> first)); + mark(base + (static_cast(first) << WrittenPageShift), static_cast(pages) << WrittenPageShift); + mask = pages + first >= 32u ? 0u : mask & ~((1u << (first + pages)) - 1u); + } + } + return true; +} } diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index 324eca0f0d..ad13332d35 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -5,13 +5,14 @@ function(add_shader_recompiler target runtimeTarget) endif() if(ANYPS5_ENABLE_SPIRV_TOOLS AND NOT TARGET SPIRV-Tools-opt) - set(SPIRV-Headers_SOURCE_DIR "${PROJECT_SOURCE_DIR}/3rdparty/SPIRV-Headers") + set(SPIRV-Headers_SOURCE_DIR "${ANYPS5_SPIRV_HEADERS_SOURCE_DIR}") set(SPIRV_SKIP_EXECUTABLES ON) set(SPIRV_SKIP_TESTS ON) set(SPIRV_TOOLS_BUILD_STATIC ON) set(SPIRV_WERROR OFF) set(BUILD_SHARED_LIBS OFF) add_subdirectory("${PROJECT_SOURCE_DIR}/3rdparty/SPIRV-Tools" "${CMAKE_BINARY_DIR}/3rdparty/SPIRV-Tools" EXCLUDE_FROM_ALL) + include("${CMAKE_CURRENT_FUNCTION_LIST_DIR}/PatchSpirvTools.cmake") endif() if(NOT TARGET glslang) @@ -34,8 +35,11 @@ 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 @@ -71,6 +75,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 @@ -95,8 +100,10 @@ function(add_shader_recompiler target runtimeTarget) ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/SsaBuilder/SsaRegisterStateCleanup.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/ConstantFolder.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/DeadCodeEliminator.cpp + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/HostInterpolationChecker.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/ReadLaneEliminator.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/MaskedSelectEliminator.cpp + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/TessellationLowering.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/ResourceTracker.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/ResourceMaterializer.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/BindingAllocator.cpp @@ -104,6 +111,9 @@ function(add_shader_recompiler target runtimeTarget) ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/DescriptorBindingBuilder.cpp ${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 ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/SrtWalker/SrtDiagnostics.cpp @@ -112,6 +122,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 @@ -153,21 +164,30 @@ function(add_shader_recompiler target runtimeTarget) ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Translation/include ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/include ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/SpirvBackend/include - ${PROJECT_SOURCE_DIR}/3rdparty/SPIRV-Headers/include - ${PROJECT_SOURCE_DIR}/3rdparty/glslang + ${ANYPS5_SPIRV_HEADERS_INCLUDE_DIR} + ${ANYPS5_GLSLANG_INCLUDE_DIRS} ) 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}") @@ -183,35 +203,53 @@ function(add_shader_recompiler target runtimeTarget) list(REMOVE_ITEM shaderCacheGuardSources ${shaderCacheRecompilerSources}) find_package(Git QUIET) - set(shaderCacheSubmodules "${PROJECT_SOURCE_DIR}/3rdparty/SPIRV-Headers" "${PROJECT_SOURCE_DIR}/3rdparty/glslang") - if(ANYPS5_ENABLE_SPIRV_TOOLS) - list(APPEND shaderCacheSubmodules "${PROJECT_SOURCE_DIR}/3rdparty/SPIRV-Tools") - endif() set(shaderCacheSubmoduleStamps) - foreach(submodule IN LISTS shaderCacheSubmodules) - list(APPEND shaderCacheSubmoduleStamps "${submodule}/CMakeLists.txt") - if(NOT GIT_FOUND) - continue() + if(NOT ANYPS5_USE_SYSTEM_DEPS) + set(shaderCacheSubmodules "${ANYPS5_SPIRV_HEADERS_SOURCE_DIR}" "${ANYPS5_GLSLANG_SOURCE_DIR}") + if(ANYPS5_ENABLE_SPIRV_TOOLS) + list(APPEND shaderCacheSubmodules "${PROJECT_SOURCE_DIR}/3rdparty/SPIRV-Tools") endif() - foreach(ref IN ITEMS HEAD refs/heads) - execute_process(COMMAND "${GIT_EXECUTABLE}" -C "${submodule}" rev-parse --path-format=absolute --git-path ${ref} - OUTPUT_VARIABLE stamp OUTPUT_STRIP_TRAILING_WHITESPACE RESULT_VARIABLE status ERROR_QUIET) - if(status EQUAL 0 AND EXISTS "${stamp}" AND NOT IS_DIRECTORY "${stamp}") - list(APPEND shaderCacheSubmoduleStamps "${stamp}") + foreach(submodule IN LISTS shaderCacheSubmodules) + list(APPEND shaderCacheSubmoduleStamps "${submodule}/CMakeLists.txt") + if(NOT GIT_FOUND) + continue() endif() + foreach(ref IN ITEMS HEAD refs/heads) + execute_process(COMMAND "${GIT_EXECUTABLE}" -C "${submodule}" rev-parse --path-format=absolute --git-path ${ref} + OUTPUT_VARIABLE stamp OUTPUT_STRIP_TRAILING_WHITESPACE RESULT_VARIABLE status ERROR_QUIET) + if(status EQUAL 0 AND EXISTS "${stamp}" AND NOT IS_DIRECTORY "${stamp}") + list(APPEND shaderCacheSubmoduleStamps "${stamp}") + endif() + endforeach() endforeach() - endforeach() + endif() get_target_property(shaderCacheIncludeDirectories ${target} INCLUDE_DIRECTORIES) string(TOUPPER "${CMAKE_BUILD_TYPE}" shaderCacheBuildType) set(shaderCacheExtra "${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION} ${CMAKE_BUILD_TYPE} spirv-tools=${ANYPS5_ENABLE_SPIRV_TOOLS} ${CMAKE_SYSTEM_PROCESSOR} flags=${CMAKE_CXX_FLAGS} ${CMAKE_CXX_FLAGS_${shaderCacheBuildType}}") + if(ANYPS5_USE_SYSTEM_DEPS) + 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 @@ -226,15 +264,39 @@ function(add_shader_recompiler target runtimeTarget) MatchesPreparedShader GetPreparedArtifact MaterializeShader + RegistrationResult ResolveSource + ResourceMaterializer::EvaluateNanoseconds + ResourceMaterializer::MaterializeNanoseconds + ReleaseUndrawnResourcePlan ResourceMaterializer::SpecializationNanoseconds RuntimeAbi::RequireVersion 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) @@ -257,21 +319,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 @@ -285,8 +346,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 4207ad6ab2..d95621f9a1 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -52,6 +52,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 +86,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 +96,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,6 +131,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, TessellationInstancePatches(request)); } template @@ -165,6 +195,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); @@ -176,7 +207,17 @@ private: append(key, value.executeOnNoop); append(key, value.conservativeZExport); 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) { @@ -190,6 +231,8 @@ private: append(key, value.fetchAttribReg); append(key, value.fetchBufferReg); append(key, value.fetchEmbedded); + append(key, value.paClVsOutCntl); + append(key, value.depthRemapMode); 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) { @@ -238,6 +281,17 @@ private: append(key, value.nonConstantImageOffsets); 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 bca0da8c96..1f333297f7 100644 --- a/core/shader/recompiler/ControlFlow/include/ControlFlow/GraphBuilder.hpp +++ b/core/shader/recompiler/ControlFlow/include/ControlFlow/GraphBuilder.hpp @@ -4,14 +4,27 @@ #include "ControlFlow/ControlFlowGraph.hpp" #include "RdnaDecoder/RdnaProgram.hpp" #include +#include +#include #include 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); +[[nodiscard]] std::vector LinkUserDataTail(const RdnaProgram& program, const UserDataTail& tail, const RdnaProgram& continuation); + 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 { @@ -24,6 +37,41 @@ struct SwappcCall { std::uint32_t returnTargetProgramCounter = 0; }; +struct UserDataCall { + std::uint32_t programCounter = 0; + std::uint32_t linkRegister = 0; + 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: [[nodiscard]] ControlFlowGraph Build(const RdnaProgram& program, const SwappcInfo* swappc = nullptr) const; 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/include/ControlFlow/Structurizer.hpp b/core/shader/recompiler/ControlFlow/include/ControlFlow/Structurizer.hpp index d643041acf..ac92180cb2 100644 --- a/core/shader/recompiler/ControlFlow/include/ControlFlow/Structurizer.hpp +++ b/core/shader/recompiler/ControlFlow/include/ControlFlow/Structurizer.hpp @@ -10,18 +10,23 @@ public: void Structurize(ControlFlowGraph& graph) const; private: + void structurizeAttempts(ControlFlowGraph& graph, bool relaxReturnJoins) const; + void structurizeWithFlags(ControlFlowGraph& graph) const; + void structurize(ControlFlowGraph& graph, bool privatizeReturns, bool routeLeavingExits, bool routeSharedExits, bool relaxReturnJoins) const; + void privatizeSharedReturns(ControlFlowGraph& graph) const; void computeDominatorTree(ControlFlowGraph& graph) const; void detectNaturalLoops(ControlFlowGraph& graph) const; void computePostDominators(ControlFlowGraph& graph) const; void computeBackEdges(ControlFlowGraph& graph) const; void computeStronglyConnectedComponents(ControlFlowGraph& graph) const; void recomputeAnalyses(ControlFlowGraph& graph) const; - void canonicalizeNaturalLoops(ControlFlowGraph& graph) const; - void splitSharedMergeBlocks(ControlFlowGraph& graph) const; + void canonicalizeNaturalLoops(ControlFlowGraph& graph, bool routeLeavingExits, bool routeSharedExits) const; + void splitSharedMergeBlocks(ControlFlowGraph& graph, bool routeLeavingExits, bool routeSharedExits, bool relaxReturnJoins) const; void isolateSemanticLoopHeaders(ControlFlowGraph& graph) const; void orderByDominance(ControlFlowGraph& graph) const; void clearStructuredTerminators(ControlFlowGraph& graph) const; void verifyReducibility(const ControlFlowGraph& graph) const; + bool hasStructuredConstructs(const ControlFlowGraph& graph) const; }; } diff --git a/core/shader/recompiler/ControlFlow/src/ControlFlowGraph.cpp b/core/shader/recompiler/ControlFlow/src/ControlFlowGraph.cpp index a0894ce95d..3f331f1ece 100644 --- a/core/shader/recompiler/ControlFlow/src/ControlFlowGraph.cpp +++ b/core/shader/recompiler/ControlFlow/src/ControlFlowGraph.cpp @@ -106,11 +106,13 @@ BasicBlock& ControlFlowGraph::FindBlockByProgramCounter(std::uint32_t programCou } bool ControlFlowGraph::Dominates(std::uint32_t dominator, std::uint32_t blockId) const { - return contains(FindBlock(blockId).dominators, dominator); + const auto& dominators = FindBlock(blockId).dominators; + return std::binary_search(dominators.begin(), dominators.end(), dominator); } bool ControlFlowGraph::PostDominates(std::uint32_t postDominator, std::uint32_t blockId) const { - return contains(FindBlock(blockId).postDominators, postDominator); + const auto& postDominators = FindBlock(blockId).postDominators; + return std::binary_search(postDominators.begin(), postDominators.end(), postDominator); } std::uint32_t ControlFlowGraph::FindNearestCommonPostDominator(std::uint32_t firstBlock, std::uint32_t secondBlock) const { diff --git a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp index 55067d748c..5a2459f5af 100644 --- a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp +++ b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp @@ -1,10 +1,13 @@ #include "ControlFlow/GraphBuilder.hpp" #include "ControlFlow/ControlFlowHelpers.hpp" +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" #include +#include #include #include #include #include +#include #include #include #include @@ -193,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]; @@ -221,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) || @@ -256,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: @@ -272,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: @@ -292,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; } @@ -370,7 +381,7 @@ bool pairOverlapsRegister(const RdnaOperand& operand, std::uint32_t linkRegister return slot != NoScalarRegister && slot <= linkRegister + 1u && linkRegister < slot + count; } -bool touchesLinkRegister(const RdnaInstruction& instruction, std::uint32_t linkRegister) { +bool readsLinkRegister(const RdnaInstruction& instruction, std::uint32_t linkRegister) { if (instruction.op == RdnaOpcode::SMovrelsB32 || instruction.op == RdnaOpcode::SMovrelsB64) return true; std::uint32_t source0Count = 1u, source1Count = 1u, source2Count = 1u; switch (instruction.family) { @@ -397,10 +408,36 @@ bool touchesLinkRegister(const RdnaInstruction& instruction, std::uint32_t linkR break; default: break; } + return pairOverlapsRegister(instruction.source0, linkRegister, source0Count) || pairOverlapsRegister(instruction.source1, linkRegister, source1Count) || + pairOverlapsRegister(instruction.source2, linkRegister, source2Count) || pairOverlapsRegister(instruction.source3, linkRegister); +} + +bool touchesLinkRegister(const RdnaInstruction& instruction, std::uint32_t linkRegister) { return writesScalar(instruction, linkRegister) || writesScalar(instruction, linkRegister + 1u) || pairOverlapsRegister(instruction.destination, linkRegister) || pairOverlapsRegister(instruction.destination2, linkRegister) || - pairOverlapsRegister(instruction.source0, linkRegister, source0Count) || pairOverlapsRegister(instruction.source1, linkRegister, source1Count) || - pairOverlapsRegister(instruction.source2, linkRegister, source2Count) || pairOverlapsRegister(instruction.source3, linkRegister); + readsLinkRegister(instruction, linkRegister); +} + +bool isScalarLoad(RdnaOpcode op) { + switch (op) { + 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 true; + default: return false; + } +} + +bool onlyWritesLinkRegister(const RdnaInstruction& instruction, std::uint32_t linkRegister) { + const bool overwrites = instruction.family == RdnaInstructionFamily::SOP2 || instruction.op == RdnaOpcode::SMovB32 || instruction.op == RdnaOpcode::SMovB64 || + (instruction.family == RdnaInstructionFamily::SMEM && isScalarLoad(instruction.op)); + return overwrites && !readsLinkRegister(instruction, linkRegister); } bool isFetchCallPrefixOpcode(const RdnaInstruction& instruction) { @@ -436,7 +473,328 @@ const SwappcCall* findReturnAt(const std::vector& calls, std::uint32 return nullptr; } -std::vector analyzeSwappcCalls(const RdnaProgram& program, const SwappcInfo* swappc) { +bool isFetchCall(const RdnaProgram& program, std::uint32_t index, const SwappcInfo* swappc, bool outsideProgram) { + const auto& instruction = program.instructions[index]; + const bool positional = std::all_of(program.instructions.begin(), program.instructions.begin() + index, isFetchCallPrefixOpcode); + 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) { + return writesScalar(other, targetRegister) || writesScalar(other, targetRegister + 1u); + }); + return swappc != nullptr && swappc->fetchCallAllowed && positional && (userDataPair || outsideProgram); +} + +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) { + std::set links; + for (const auto& call : calls) { + if (!call.fetch) 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 && 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]; @@ -450,40 +808,80 @@ std::vector analyzeSwappcCalls(const RdnaProgram& program, const Swa SwappcCall call; call.callIndex = index; call.linkRegister = linkRegister; - for (const auto& other : calls) { - if (call.linkRegister <= other.linkRegister + 1u && other.linkRegister <= call.linkRegister + 1u) { - 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"); - } - } std::uint32_t target = instruction.branchTarget; const bool staticTarget = instruction.op == RdnaOpcode::SCallB64 || resolveSetpcTarget(program, index, target); call.targetIndex = staticTarget ? instructionIndexOfProgramCounter(program, target) : InvalidControlFlowId; if (instruction.op == RdnaOpcode::SCallB64 && call.targetIndex == InvalidControlFlowId) { throw std::invalid_argument("s_call_b64 at program counter " + toHexString(instruction.programCounter) + " targets invalid instruction boundary " + toHexString(target)); } - const bool positional = std::all_of(program.instructions.begin(), program.instructions.begin() + index, isFetchCallPrefixOpcode); - 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) { - return writesScalar(other, targetRegister) || writesScalar(other, targetRegister + 1u); - }); - const bool outsideProgram = staticTarget && call.targetIndex == InvalidControlFlowId; - if (instruction.op == RdnaOpcode::SSwappcB64 && swappc != nullptr && swappc->fetchCallAllowed && positional && (userDataPair || outsideProgram)) { + if (instruction.op == RdnaOpcode::SSwappcB64 && isFetchCall(program, index, swappc, staticTarget && call.targetIndex == InvalidControlFlowId)) { call.fetch = true; call.returnTargetProgramCounter = instructionEndProgramCounter(instruction); calls.push_back(call); 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"); + } throw std::invalid_argument("computed/data-dependent s_swappc_b64 call target at program counter " + toHexString(instruction.programCounter) + " is not statically resolvable"); } call.targetProgramCounter = target; calls.push_back(call); } + const auto sharedLinkError = [&](const SwappcCall& call) { + return std::invalid_argument("unclosable scalar call/return pairing at program counter " + toHexString(program.instructions[call.callIndex].programCounter) + + ": link register s[" + std::to_string(call.linkRegister) + "] is shared with another call"); + }; + const auto escapeError = [&](const SwappcCall& call, const RdnaInstruction& instruction) { + return std::invalid_argument("scalar call link register s[" + std::to_string(call.linkRegister) + "] written at program counter " + + toHexString(program.instructions[call.callIndex].programCounter) + " escapes the constant-offset call/return model (used at program counter " + + toHexString(instruction.programCounter) + ")"); + }; + const auto sharesLink = [&](const SwappcCall& call) { + return std::ranges::count_if(calls, [&](const SwappcCall& other) { return other.linkRegister == call.linkRegister; }) > 1; + }; + for (std::size_t first = 0; first < calls.size(); ++first) { + for (std::size_t second = first + 1u; second < calls.size(); ++second) { + if (calls[first].linkRegister == calls[second].linkRegister && (calls[first].fetch || calls[second].fetch)) throw sharedLinkError(calls[second]); + } + } + for (auto& call : calls) { + if (sharesLink(call)) { + call.returnIndex = InvalidControlFlowId; + for (auto index = call.targetIndex; index < program.instructions.size(); ++index) { + const auto& instruction = program.instructions[index]; + if (instruction.op == RdnaOpcode::SSetpcB64 && isScalar(instruction.source0, call.linkRegister)) { + call.returnIndex = index; + break; + } + } + if (call.returnIndex == InvalidControlFlowId) { + throw std::invalid_argument("unclosable scalar call/return pairing at program counter " + toHexString(program.instructions[call.callIndex].programCounter) + + ": no paired s_setpc_b64 return reads link register s[" + std::to_string(call.linkRegister) + "]"); + } + call.returnTargetProgramCounter = instructionEndProgramCounter(program.instructions[call.callIndex]); + continue; + } + std::uint32_t regionEnd = InvalidControlFlowId; + if (!call.fetch) { + std::uint32_t returns = 0; + for (std::uint32_t index = 0; index < program.instructions.size(); ++index) { + const auto& instruction = program.instructions[index]; + if (index != call.callIndex && instruction.op == RdnaOpcode::SSetpcB64 && isScalar(instruction.source0, call.linkRegister)) { + ++returns; + regionEnd = index; + } + } + if (returns != 1u || regionEnd < call.targetIndex) regionEnd = InvalidControlFlowId; + } std::uint32_t returnIndex = InvalidControlFlowId; for (std::uint32_t index = 0; index < program.instructions.size(); ++index) { if (index == call.callIndex) { @@ -498,10 +896,10 @@ std::vector analyzeSwappcCalls(const RdnaProgram& program, const Swa returnIndex = index; continue; } + const bool outsideRegion = regionEnd != InvalidControlFlowId && (index < call.targetIndex || index > regionEnd); + if (outsideRegion) continue; if (touchesLinkRegister(instruction, call.linkRegister)) { - throw std::invalid_argument("scalar call link register s[" + std::to_string(call.linkRegister) + "] written at program counter " + - toHexString(program.instructions[call.callIndex].programCounter) + " escapes the constant-offset call/return model (used at program counter " + - toHexString(instruction.programCounter) + ")"); + throw escapeError(call, instruction); } } if (call.fetch) { @@ -523,6 +921,24 @@ std::vector analyzeSwappcCalls(const RdnaProgram& program, const Swa call.returnTargetProgramCounter = instructionEndProgramCounter(program.instructions[call.callIndex]); } + for (const auto& call : calls) { + if (!sharesLink(call)) continue; + for (const auto& other : calls) { + if (&other == &call || other.linkRegister != call.linkRegister) continue; + const bool callInside = other.callIndex >= call.targetIndex && other.callIndex <= call.returnIndex; + const bool regionsMeet = other.targetIndex <= call.returnIndex && call.targetIndex <= other.returnIndex; + if (callInside || regionsMeet) throw sharedLinkError(other); + } + for (std::uint32_t index = 0; index < program.instructions.size(); ++index) { + 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() && 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) + @@ -570,7 +986,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}); @@ -848,9 +1264,377 @@ void GraphBuilder::linkBlocks(std::vector& blocks, const RdnaProgram } } +std::optional UnresolvableSwappcTarget(const RdnaProgram& program, const SwappcInfo* swappc) { + 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 target = instruction.branchTarget; + const bool staticTarget = resolveSetpcTarget(program, index, target); + const bool inProgram = staticTarget && instructionIndexOfProgramCounter(program, target) != InvalidControlFlowId; + if (!inProgram && !isFetchCall(program, index, swappc, staticTarget)) { + return instruction.programCounter; + } + } + return std::nullopt; +} + +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); + } + 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) { + if (!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; +} + +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]; + if (instruction.op != RdnaOpcode::SSwappcB64) continue; + std::uint32_t target = 0; + if (resolveSetpcTarget(program, index, target)) continue; + const auto link = scalarIndex(instruction.destination); + 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) && unchanged(source, index) && unchanged(source + 1u, index)) { + pair = source; + } else { + std::uint32_t low = 0, high = 0; + if (!findLastWriter(program, index, {source}, low) || !findLastWriter(program, index, {source + 1u}, high)) continue; + const auto first = std::min(low, high); + const bool sameBlock = std::none_of(labels.begin(), labels.end(), [&](std::uint32_t label) { + return label > program.instructions[first].programCounter && label <= instruction.programCounter; + }); + const auto& lowWriter = program.instructions[low]; + const auto& highWriter = program.instructions[high]; + std::uint32_t from = NoScalarRegister; + if (low == high && lowWriter.op == RdnaOpcode::SMovB64 && isScalar(lowWriter.destination, source)) { + from = scalarIndex(lowWriter.source0); + } else if (lowWriter.op == RdnaOpcode::SMovB32 && highWriter.op == RdnaOpcode::SMovB32 && isScalar(lowWriter.destination, source) && isScalar(highWriter.destination, source + 1u)) { + 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) || !unchanged(from, low) || !unchanged(from + 1u, high)) continue; + pair = from; + } + calls.push_back({instruction.programCounter, link, pair}); + } + return calls; +} + +std::optional FindUserDataTail(const RdnaProgram& program, std::uint32_t userDataBase, std::uint32_t userDataCount) { + if (program.instructions.size() < 2u || program.instructions.back().op != RdnaOpcode::SCodeEnd) return std::nullopt; + const auto& tail = program.instructions[program.instructions.size() - 2u]; + if (tail.op != RdnaOpcode::SSetpcB64 || tail.source0.kind != RdnaOperandKind::ScalarRegister) return std::nullopt; + const auto reg = tail.source0.reg; + if (reg % 2u != 0u || reg < userDataBase || 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)) 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(); + } + } + 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 || 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"); + } + const auto words = tail.programCounter / 4u; + std::vector linked(program.code.begin(), program.code.begin() + words); + linked.push_back(0xbf820000u); + linked.insert(linked.end(), continuation.code.begin(), continuation.code.end()); + return linked; +} + +std::optional UserDataCalleeLength(std::span callee, std::uint32_t linkRegister) { + std::uint32_t furthest = 0; + for (std::uint32_t word = 0; word < callee.size();) { + const auto programCounter = word * 4u; + RdnaInstruction instruction; + try { + instruction = DecodeRdnaInstruction(programCounter, callee, word); + } catch (const std::out_of_range&) { + if (callee.size() - word < 16u) return std::nullopt; + throw; + } + word += instruction.wordCount; + const auto at = " at callee offset " + toHexString(programCounter); + if (instruction.op == RdnaOpcode::SGetpcB64) throw std::invalid_argument("user-data callee reads its program counter" + at + ", which a linked copy does not keep"); + if (instruction.op == RdnaOpcode::SSwappcB64 || instruction.op == RdnaOpcode::SCallB64) throw std::invalid_argument("user-data callee makes a call" + at); + if (instruction.op == RdnaOpcode::SEndpgm || instruction.op == RdnaOpcode::SCodeEnd) throw std::invalid_argument("user-data callee ends the program" + at + " before it returns"); + if (IsDirectBranchOpcode(instruction.op)) { + if (instruction.branchTarget >= 0x80000000u) throw std::invalid_argument("user-data callee branches before its start" + at); + furthest = std::max(furthest, instruction.branchTarget / 4u); + } + if (instruction.op != RdnaOpcode::SSetpcB64) continue; + if (!isScalar(instruction.source0, linkRegister)) throw std::invalid_argument("user-data callee jumps through another register pair than its link s[" + std::to_string(linkRegister) + "]" + at); + if (furthest >= word) throw std::invalid_argument("user-data callee returns" + at + " before code it branches to"); + return word; + } + return std::nullopt; +} + +std::vector LinkUserDataCalls(const RdnaProgram& program, std::span calls, std::span> callees) { + 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); + 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(std::string(kind) + " call does not name an s_swappc_b64"); + } + 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(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 {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 48386cd206..11a9d92195 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -153,6 +153,8 @@ public: bool ReadBool() { return ReadU8() != 0u; } + std::uint64_t Remaining() const { return m_data.size() - m_offset; } + std::uint32_t ReadU32() { std::uint32_t value = 0u; for (std::uint32_t i = 0; i < 4u; i++) { @@ -235,6 +237,30 @@ ConservativeZExport readConservativeZExport(Reader& reader) { return static_cast(value); } +ColorExportPacking readColorExportPacking(Reader& reader) { + const auto value = reader.ReadU8(); + if (value > static_cast(ColorExportPacking::Float11_11_10)) { + throw std::runtime_error("RequestSerializer: invalid ColorExportPacking value"); + } + return static_cast(value); +} + +RasterPrimitive readRasterPrimitive(Reader& reader) { + const auto value = reader.ReadU8(); + if (value > static_cast(RasterPrimitive::Rectangle)) { + throw std::runtime_error("RequestSerializer: invalid RasterPrimitive value"); + } + return static_cast(value); +} + +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); @@ -312,6 +338,24 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) { for (const std::uint8_t value : info.targetExportMapping) { writer.WriteU8(value); } + for (const ColorExportPacking value : info.targetExportPacking) { + writer.WriteU8(static_cast(value)); + } + writer.WriteBool(info.dualSourceBlend); + writer.WriteBool(info.alphaToCoverage); + writer.WriteBool(info.stencilRefExportEnable); + 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) { @@ -353,6 +397,36 @@ ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { } else { info.targetExportMapping.fill(0u); } + if (version >= 14u) { + for (ColorExportPacking& value : info.targetExportPacking) { + value = readColorExportPacking(reader); + } + } + if (version >= 15u) info.dualSourceBlend = reader.ReadBool(); + if (version >= 16u) info.alphaToCoverage = reader.ReadBool(); + if (version >= 17u) info.stencilRefExportEnable = reader.ReadBool(); + if (version >= 19u) info.rasterPrimitive = readRasterPrimitive(reader); + if (version >= 23u) info.layerExported = reader.ReadBool(); + if (version >= 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) | @@ -380,9 +454,10 @@ void writeVertexInfo(Writer& writer, const ShaderVertexStageInfo& info) { writer.WriteU32(info.fetchAttribReg); writer.WriteU32(info.fetchBufferReg); writer.WriteBool(info.fetchEmbedded); + writer.WriteU32(info.paClVsOutCntl); } -ShaderVertexStageInfo readVertexInfo(Reader& reader) { +ShaderVertexStageInfo readVertexInfo(Reader& reader, std::uint32_t version) { ShaderVertexStageInfo info{}; for (auto& resource : info.resources) { for (std::uint32_t& value : resource.fields) { @@ -399,6 +474,7 @@ ShaderVertexStageInfo readVertexInfo(Reader& reader) { info.fetchAttribReg = reader.ReadU32(); info.fetchBufferReg = reader.ReadU32(); info.fetchEmbedded = reader.ReadBool(); + if (version >= 20u) info.paClVsOutCntl = reader.ReadU32(); return info; } @@ -464,6 +540,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) { @@ -477,6 +554,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; } @@ -486,15 +564,25 @@ void writeTessellationConfiguration(Writer& writer, const TessellationConfigurat writer.WriteU32(configuration.domain); writer.WriteU32(configuration.partitioning); writer.WriteU32(configuration.outputTopology); + writer.WriteU32(configuration.patchesPerGroup); + writer.WriteU32(configuration.primitiveGroupSize); + writer.WriteBool(configuration.breakAtInstance); + writer.WriteBool(configuration.mergedHull); } -TessellationConfiguration readTessellationConfiguration(Reader& reader) { +TessellationConfiguration readTessellationConfiguration(Reader& reader, std::uint32_t version) { TessellationConfiguration configuration{}; configuration.inputControlPoints = reader.ReadU32(); configuration.outputControlPoints = reader.ReadU32(); configuration.domain = reader.ReadU32(); configuration.partitioning = reader.ReadU32(); configuration.outputTopology = reader.ReadU32(); + if (version >= 22u) configuration.patchesPerGroup = reader.ReadU32(); + if (version >= 24u) { + configuration.primitiveGroupSize = reader.ReadU32(); + configuration.breakAtInstance = reader.ReadBool(); + } + if (version >= 39u) configuration.mergedHull = reader.ReadBool(); return configuration; } @@ -543,7 +631,7 @@ GuestContext readGuestContext(Reader& reader, DeserializedRequest& result, std:: context.pixel = result.pixel; } if (reader.ReadBool()) { - result.vertex = readVertexInfo(reader); + result.vertex = readVertexInfo(reader, version); context.vertex = result.vertex; } const auto regionCount = reader.ReadU64(); @@ -667,6 +755,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) { @@ -697,7 +798,7 @@ GraphicsCompileContext readGraphicsCompileContext(Reader& reader, DeserializedGr graphics.mesh = storage.mesh; } if (reader.ReadBool()) { - storage.tessellation = readTessellationConfiguration(reader); + storage.tessellation = readTessellationConfiguration(reader, version); graphics.tessellation = storage.tessellation; } storage.draw.indexAddress = reader.ReadU64(); @@ -705,6 +806,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; } @@ -714,7 +831,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(13u); + writer.WriteU32(42u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -733,6 +850,19 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const writer.WriteU32(request.target.srgbDecodeFormats); if (request.context.compute.has_value()) writer.WriteU32(request.context.compute->scratchDwords); writer.WriteBool(request.target.narrowSubgroupClock); + writer.WriteBool(request.target.float64DenormPreserve); + writer.WriteBool(request.target.registrationModules); + 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); return base64Encode(buffer); } @@ -741,7 +871,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 > 13u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 42u) 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); @@ -761,6 +891,33 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const if (version >= 9u) result.request.target.srgbDecodeFormats = reader.ReadU32(); if (version >= 10u && result.request.context.compute.has_value()) result.request.context.compute->scratchDwords = reader.ReadU32(); if (version >= 12u) result.request.target.narrowSubgroupClock = reader.ReadBool(); + if (version >= 16u) result.request.target.float64DenormPreserve = reader.ReadBool(); + if (version >= 17u) result.request.target.registrationModules = reader.ReadBool(); + if (version >= 17u) result.request.target.registrationWordLimit = reader.ReadU32(); + if (version >= 18u) result.request.target.vertexSubgroups = reader.ReadBool(); + if (version >= 21u) { + if (version >= 22u) reader.Require(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; + } + } + 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 >= 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 adc9332392..3267461138 100644 --- a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp +++ b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp @@ -1,6 +1,10 @@ #include "ControlFlow/Structurizer.hpp" #include "ControlFlow/ControlFlowHelpers.hpp" #include +#include +#include +#include +#include #include #include #include @@ -20,6 +24,142 @@ std::vector allBlockIds(std::uint32_t count) { return ids; } +std::vector reversePostOrder(const ControlFlowGraph& graph); + +std::vector analysisOrder(const ControlFlowGraph& graph, bool successorsFirst) { + const auto count = static_cast(graph.blocks.size()); + std::vector order; + std::vector placed(count, false); + if (graph.entryBlock < count) { + order = reversePostOrder(graph); + if (successorsFirst) { + std::reverse(order.begin(), order.end()); + } + for (const auto id : order) { + placed[id] = true; + } + } + for (std::uint32_t id = 0; id < count; ++id) { + if (!placed[id]) { + order.push_back(id); + } + } + return order; +} + +bool inPlaceIntersections() { + static const bool enabled = std::getenv("APS5_NO_PERF_DOM_INTERSECTION") == nullptr; + return enabled; +} + +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); + return; + } + std::size_t left = 0; + std::size_t right = 0; + std::size_t count = 0; + while (left < values.size() && right < other.size()) { + if (values[left] < other[right]) { + ++left; + } else if (other[right] < values[left]) { + ++right; + } else { + values[count++] = values[left++]; + ++right; + } + } + values.resize(count); +} + +void replaceAnalysisSet(std::vector& destination, std::vector&& values) { + if (inPlaceIntersections() && values.capacity() - values.size() > values.size()) { + std::vector(values).swap(values); + } + destination = std::move(values); +} + +void addSortedUnique(std::vector& values, std::uint32_t value) { + const auto position = std::lower_bound(values.begin(), values.end(), value); + if (position == values.end() || *position != value) { + values.insert(position, value); + } +} + bool replaceValue(std::vector& values, std::uint32_t oldValue, std::uint32_t newValue) { bool changed = false; for (auto& value : values) { @@ -109,18 +249,23 @@ std::uint32_t moveBlockBefore(ControlFlowGraph& graph, std::uint32_t blockId, st std::vector dominatedBlocks(const ControlFlowGraph& graph, std::uint32_t headerBlock, std::uint32_t stopBlock = InvalidControlFlowId) { std::vector blocks; std::vector stack = {headerBlock}; + std::vector visited(graph.blocks.size()); blocks.reserve(graph.blocks.size()); stack.reserve(graph.blocks.size()); while (!stack.empty()) { const auto blockId = stack.back(); stack.pop_back(); - if (blockId == stopBlock || contains(blocks, blockId) || !graph.Dominates(headerBlock, blockId)) { + if (blockId >= visited.size()) { + visited.resize(blockId + 1); + } + if (blockId == stopBlock || visited[blockId] || !graph.Dominates(headerBlock, blockId)) { continue; } const auto& block = graph.FindBlock(blockId); - addUnique(blocks, blockId); + visited[blockId] = true; + blocks.push_back(blockId); for (const auto successor : block.successors) { if (successor != stopBlock && graph.Dominates(headerBlock, successor)) { stack.push_back(successor); @@ -268,6 +413,79 @@ bool reachesWithinIteration(const ControlFlowGraph& graph, const NaturalLoop& lo return false; } +std::uint32_t joinWithinIteration(const ControlFlowGraph& graph, const NaturalLoop& loop, std::uint32_t trueTarget, std::uint32_t falseTarget, std::uint32_t globalMerge) { + std::vector reached(graph.blocks.size(), false); + std::vector pending = {trueTarget, falseTarget}; + while (!pending.empty()) { + const auto blockId = pending.back(); + pending.pop_back(); + if (blockId == globalMerge || reached[blockId] || !isInsideLoopConstruct(graph, loop, blockId)) { + continue; + } + reached[blockId] = true; + const auto& successors = graph.FindBlock(blockId).successors; + pending.insert(pending.end(), successors.begin(), successors.end()); + } + std::vector joins(graph.blocks.size(), false); + std::vector joining = {globalMerge}; + joins[globalMerge] = true; + for (std::size_t index = 0; index < joining.size(); ++index) { + for (const auto predecessor : graph.FindBlock(joining[index]).predecessors) { + if (reached[predecessor] && !joins[predecessor]) { + joins[predecessor] = true; + joining.push_back(predecessor); + } + } + } + sortUnique(joining); + if (!joins[trueTarget] || !joins[falseTarget]) { + return globalMerge; + } + const auto count = static_cast(graph.blocks.size()); + std::vector> postDominators(count); + const auto order = analysisOrder(graph, true); + 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; + } + } + 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; + } + } + } + } + const auto common = intersectSorted(postDominators[trueTarget], postDominators[falseTarget]); + const auto merge = std::find_if(common.begin(), common.end(), [&](std::uint32_t candidate) { + return std::all_of(common.begin(), common.end(), [&](std::uint32_t other) { return contains(postDominators[candidate], other); }); + }); + return merge == common.end() ? globalMerge : *merge; +} + std::vector linearTail(const ControlFlowGraph& graph, std::uint32_t start) { std::vector tail; for (auto blockId = start; !contains(tail, blockId);) { @@ -312,40 +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); } } - for (bool changed = true; changed;) { - changed = false; + const auto order = analysisOrder(graph, true); + std::vector> postDominators; + if (inPlaceIntersections()) { + std::vector sets(count, BlockSet::all(count)); for (const auto& block : graph.blocks) { - if (kept[block.id].empty()) { - continue; + 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) { - next = intersectSorted(next, postDominators[kept[block.id][i]]); - } - addUnique(next, block.id); - sortUnique(next); - if (next != postDominators[block.id]) { - 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; + } } } } @@ -380,7 +628,102 @@ std::uint32_t mergeBesideReturns(const ControlFlowGraph& graph, const BasicBlock return *merge; } -std::uint32_t findSelectionMerge(const ControlFlowGraph& graph, const BasicBlock& block) { +std::uint32_t iterationSelectionMerge(const ControlFlowGraph& graph, const NaturalLoop& loop, const BasicBlock& header, std::uint32_t join) { + const auto count = static_cast(graph.blocks.size()); + static const bool reuse = std::getenv("APS5_NO_PERF_ITERATION_SELECTION") == nullptr; + const auto escapes = [&](std::uint32_t blockId) { return blockId == loop.continueBlock || blockId == loop.mergeBlock; }; + std::vector joins(count, false); + std::vector pending = {join}; + joins[join] = true; + while (!pending.empty()) { + const auto blockId = pending.back(); + pending.pop_back(); + for (const auto predecessor : graph.FindBlock(blockId).predecessors) { + if (!joins[predecessor] && !escapes(predecessor)) { + joins[predecessor] = true; + pending.push_back(predecessor); + } + } + } + const auto trueTarget = header.terminator.trueBlock; + const auto falseTarget = header.terminator.falseBlock; + if (!joins[trueTarget] || !joins[falseTarget]) { + return InvalidControlFlowId; + } + const auto order = analysisOrder(graph, true); + 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 = sets[successor]; + seeded = true; + } else { + next.intersect(sets[successor]); + } + } + if (!seeded) next = BlockSet::all(count); + next.add(block.id); + changed = replaceSet(sets[block.id], std::move(next)) || changed; + } + } + 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; + } + } + } + } + const auto common = intersectSorted(postDominators[trueTarget], postDominators[falseTarget]); + const auto merge = std::find_if(common.begin(), common.end(), [&](std::uint32_t candidate) { + return std::all_of(common.begin(), common.end(), [&](std::uint32_t other) { return contains(postDominators[candidate], other); }); + }); + if (merge == common.end() || *merge == join || *merge == header.id || !graph.Dominates(header.id, *merge) || !isInsideLoopConstruct(graph, loop, *merge)) { + return InvalidControlFlowId; + } + return *merge; +} + +std::uint32_t findSelectionMerge(const ControlFlowGraph& graph, const BasicBlock& block, bool routeLeavingExits = false, bool routeSharedExits = false) { const auto globalMerge = graph.FindNearestCommonPostDominator(block.terminator.trueBlock, block.terminator.falseBlock); const auto* loop = findInnermostContainingLoop(graph, block.id); const auto trueTarget = block.terminator.trueBlock; @@ -412,6 +755,9 @@ std::uint32_t findSelectionMerge(const ControlFlowGraph& graph, const BasicBlock return globalMerge; } + if (routeSharedExits && reachesWithinIteration(graph, *loop, trueTarget, loop->continueBlock) && reachesWithinIteration(graph, *loop, falseTarget, loop->continueBlock)) { + return joinWithinIteration(graph, *loop, trueTarget, falseTarget, loop->continueBlock); + } if (isLoopControlGateway(graph, *loop, trueTarget) && graph.Dominates(block.id, trueTarget) && isInsideLoopConstruct(graph, *loop, falseTarget)) { return trueTarget; } @@ -437,6 +783,14 @@ std::uint32_t findSelectionMerge(const ControlFlowGraph& graph, const BasicBlock if (trueJoins != falseJoins) { return trueJoins ? trueTarget : falseTarget; } + const auto escapesAround = [&](std::uint32_t arm) { + return canReachBefore(graph, arm, loop->continueBlock, globalMerge) || canReachBefore(graph, arm, loop->mergeBlock, globalMerge); + }; + if (trueJoins && !graph.Dominates(block.id, globalMerge) && (escapesAround(trueTarget) || escapesAround(falseTarget))) { + if (const auto merge = iterationSelectionMerge(graph, *loop, block, globalMerge); merge != InvalidControlFlowId) { + return merge; + } + } } return globalMerge; } @@ -468,7 +822,7 @@ bool isInnermostLoopControlConditional(const ControlFlowGraph& graph, const Basi return (isControlTarget(trueTarget) && (isControlTarget(falseTarget) || isInsideLoopConstruct(graph, *loop, falseTarget))) || (isControlTarget(falseTarget) && isInsideLoopConstruct(graph, *loop, trueTarget)); } -std::uint32_t exitTailSelectionMerge(const ControlFlowGraph& graph, const BasicBlock& block) { +std::uint32_t exitTailSelectionMerge(const ControlFlowGraph& graph, const BasicBlock& block, bool privateTail) { if (block.terminator.kind != TerminatorKind::ConditionalBranch) { return InvalidControlFlowId; } @@ -484,7 +838,7 @@ std::uint32_t exitTailSelectionMerge(const ControlFlowGraph& graph, const BasicB } const auto body = trueInBody ? trueTarget : falseTarget; const auto tail = trueInBody ? falseTarget : trueTarget; - if (tail == loop->mergeBlock || tail == loop->continueBlock || body == loop->continueBlock || !isInsideLoopConstruct(graph, *loop, tail) || !graph.Dominates(block.id, body) || !graph.Dominates(block.id, tail)) { + if (tail == loop->mergeBlock || tail == loop->continueBlock || body == loop->continueBlock || !isInsideLoopConstruct(graph, *loop, tail) || !graph.Dominates(block.id, body) || !graph.Dominates(block.id, tail) || (privateTail && reachesWithinIteration(graph, *loop, body, tail))) { return InvalidControlFlowId; } std::vector pending{tail}; @@ -509,6 +863,20 @@ std::uint32_t exitTailSelectionMerge(const ControlFlowGraph& graph, const BasicB return body; } +bool isStructuredLoopExit(const ControlFlowGraph& graph, const BasicBlock& block, bool privateTail) { + if (!isInnermostLoopControlConditional(graph, block)) { + return false; + } + if (!privateTail) { + return true; + } + const auto* loop = findInnermostContainingLoop(graph, block.id); + const auto isControlTarget = [&](std::uint32_t target) { + return target == loop->mergeBlock || target == loop->continueBlock || (block.id == loop->continueBlock && target == loop->headerBlock); + }; + return isControlTarget(block.terminator.trueBlock) || isControlTarget(block.terminator.falseBlock) || exitTailSelectionMerge(graph, block, true) != InvalidControlFlowId; +} + bool mergeLeavesContainingLoop(const ControlFlowGraph& graph, std::uint32_t header, std::uint32_t merge) { for (const auto& loop : graph.naturalLoops) { if (loop.headerBlock != header && isInsideLoopConstruct(graph, loop, header) && !isInsideLoopConstruct(graph, loop, merge)) { @@ -560,7 +928,7 @@ bool splitSharedMergeBlock(ControlFlowGraph& graph, std::uint32_t merge, const s return true; } -bool splitOneReturnJoin(ControlFlowGraph& graph) { +bool splitOneReturnJoin(ControlFlowGraph& graph, bool relaxReturnJoins) { for (const auto& block : graph.blocks) { if (block.terminator.kind != TerminatorKind::ConditionalBranch || findInnermostContainingLoop(graph, block.id) != nullptr || findSelectionMerge(graph, block) != InvalidControlFlowId) { continue; @@ -568,9 +936,9 @@ bool splitOneReturnJoin(ControlFlowGraph& graph) { const auto returns = [&](std::uint32_t target) { return graph.Dominates(block.id, target) && hasLinearPathToTerminal(graph, target); }; const auto joinsEnclosing = [&](std::uint32_t target) { const auto& predecessors = graph.FindBlock(target).predecessors; - return !graph.Dominates(block.id, target) && std::all_of(predecessors.begin(), predecessors.end(), [&](std::uint32_t predecessor) { + return !graph.Dominates(block.id, target) && (relaxReturnJoins || std::all_of(predecessors.begin(), predecessors.end(), [&](std::uint32_t predecessor) { return graph.Dominates(block.id, predecessor) || graph.Dominates(predecessor, block.id); - }); + })); }; const auto trueTarget = block.terminator.trueBlock; const auto falseTarget = block.terminator.falseBlock; @@ -615,6 +983,8 @@ constexpr std::uint32_t CloneWordLimit = 1024u; constexpr std::uint32_t CloneWordFloor = 256u; constexpr std::uint32_t CloneShareDivisor = 8u; constexpr std::uint32_t CloneBudgetDivisor = 4u; +constexpr std::uint32_t SplitGrowthFactor = 16u; +constexpr std::uint32_t SplitGrowthFloor = 64u; struct SplitBudget { std::uint32_t programWords = 0; @@ -791,6 +1161,24 @@ void cloneBlocks(ControlFlowGraph& graph, const std::vector& bloc applyBlockOrder(graph, std::move(ordered)); } +bool privatizeOneSharedReturn(ControlFlowGraph& graph) { + for (const auto& block : graph.blocks) { + if (!block.successors.empty() || block.terminator.kind != TerminatorKind::Return || block.predecessors.size() < 2u || block.estimatedSpirvWords > CloneWordFloor) { + continue; + } + for (const auto predecessor : block.predecessors) { + const auto& from = graph.FindBlock(predecessor); + if (from.terminator.kind != TerminatorKind::ConditionalBranch || findInnermostContainingLoop(graph, predecessor) != nullptr) { + continue; + } + const auto shared = block.id; + cloneBlocks(graph, {shared}, {{predecessor, shared}}); + return true; + } + } + return false; +} + std::optional privatizeMergeTail(const ControlFlowGraph& graph, std::uint32_t header, std::uint32_t merge, std::uint32_t& cost, const std::function& recompute) { const auto& headerBlock = graph.FindBlock(header); if ((headerBlock.terminator.trueBlock != merge && headerBlock.terminator.falseBlock != merge) || graph.FindBlock(merge).predecessors.size() < 2u || !hasLinearPathToTerminal(graph, merge)) { @@ -957,6 +1345,41 @@ std::uint32_t nextGotoVariable(const ControlFlowGraph& graph) { return next; } +bool hasSharedExitSelection(const ControlFlowGraph& graph, const NaturalLoop& loop) { + for (const auto id : loop.blocks) { + const auto& block = graph.FindBlock(id); + if (block.terminator.kind != TerminatorKind::ConditionalBranch) continue; + const auto globalMerge = graph.FindNearestCommonPostDominator(block.terminator.trueBlock, block.terminator.falseBlock); + if (globalMerge != InvalidControlFlowId && isInsideLoopConstruct(graph, loop, globalMerge)) continue; + for (const auto exit : loop.exitBlocks) { + const auto& forwarder = graph.FindBlock(exit); + if (forwarder.instructionBegin == forwarder.instructionEnd && forwarder.terminator.kind == TerminatorKind::Branch && forwarder.terminator.gotoVariable != InvalidControlFlowId && forwarder.terminator.trueBlock == loop.mergeBlock) continue; + if (exit != loop.mergeBlock && graph.Dominates(block.id, exit) && reachesWithinIteration(graph, loop, block.terminator.trueBlock, exit) && reachesWithinIteration(graph, loop, block.terminator.falseBlock, exit)) return true; + } + } + return false; +} + +bool exitsLeaveConstruct(const ControlFlowGraph& graph, const NaturalLoop& loop) { + std::vector visited(graph.blocks.size(), false); + std::vector pending; + pending.insert(pending.end(), loop.exitBlocks.begin(), loop.exitBlocks.end()); + while (!pending.empty()) { + const auto blockId = pending.back(); + pending.pop_back(); + if (blockId == loop.mergeBlock || visited[blockId]) { + continue; + } + if (!graph.Dominates(loop.headerBlock, blockId)) { + return true; + } + visited[blockId] = true; + const auto& successors = graph.FindBlock(blockId).successors; + pending.insert(pending.end(), successors.begin(), successors.end()); + } + return false; +} + std::optional routeLoopExits(const ControlFlowGraph& graph, const NaturalLoop& loop, const std::function& recompute, std::string& refusal) { std::vector exits; for (const auto member : loop.blocks) { @@ -1067,7 +1490,7 @@ bool structurizeEnteredRegion(ControlFlowGraph& graph, std::uint32_t header, std throw std::runtime_error("selection header block " + std::to_string(header) + " has externally entered region block " + std::to_string(entryEdges.front().second) + " (from block " + std::to_string(entryEdges.front().first) + "): " + cloneReason + "; " + junctionReason); } -bool splitOneSelectionMerge(ControlFlowGraph& graph, SplitBudget& budget, const std::function& recompute) { +bool splitOneSelectionMerge(ControlFlowGraph& graph, SplitBudget& budget, const std::function& recompute, bool routeLeavingExits, bool routeSharedExits) { std::vector loopHeaders; loopHeaders.reserve(graph.naturalLoops.size()); for (const auto& loop : graph.naturalLoops) { @@ -1088,11 +1511,11 @@ bool splitOneSelectionMerge(ControlFlowGraph& graph, SplitBudget& budget, const for (const auto blockId : selectionHeaders) { const auto& block = graph.FindBlock(blockId); - if (isInnermostLoopControlConditional(graph, block)) { + if (isStructuredLoopExit(graph, block, routeLeavingExits)) { continue; } - const auto merge = findSelectionMerge(graph, block); + const auto merge = findSelectionMerge(graph, block, routeLeavingExits, routeSharedExits); if (merge == InvalidControlFlowId) { continue; } @@ -1202,13 +1625,467 @@ void tarjanVisit(TarjanState& state, std::uint32_t blockId) { state.components.push_back(std::move(component)); } +bool forcedFlagStructurization() { + static const bool forced = std::getenv("APS5_FORCE_STRUCTURIZER_FALLBACK") != nullptr; + return forced; +} + +std::vector terminatorTargets(const Terminator& terminator) { + if (terminator.kind == TerminatorKind::Branch) { + return {terminator.trueBlock}; + } + if (terminator.kind == TerminatorKind::ConditionalBranch) { + return {terminator.trueBlock, terminator.falseBlock}; + } + return {}; +} + +bool supportsFlagStructurization(const ControlFlowGraph& graph) { + const auto count = graph.blocks.size(); + if (graph.unsupported || graph.entryBlock >= count) { + return false; + } + for (std::uint32_t id = 0; id < count; ++id) { + const auto& block = graph.blocks[id]; + const auto kind = block.terminator.kind; + if (block.id != id || (kind != TerminatorKind::Branch && kind != TerminatorKind::ConditionalBranch && kind != TerminatorKind::Return)) { + return false; + } + const auto targets = terminatorTargets(block.terminator); + if (std::any_of(targets.begin(), targets.end(), [&](std::uint32_t target) { return target >= count; })) { + return false; + } + } + return true; +} + +struct FlagEmission { + ControlFlowGraph* graph = nullptr; + std::uint32_t entryBlock = InvalidControlFlowId; + std::uint32_t firstVariable = 0; + std::uint32_t nextVariable = 0; + std::vector> targets; + std::vector> successors; + std::vector> predecessors; + std::vector> openLoops; + std::vector order; + std::vector fallthroughs; +}; + +std::vector> orderedComponents(const FlagEmission& emission, const std::vector& members) { + const auto count = emission.successors.size(); + std::vector inside(count, false); + for (const auto member : members) { + inside[member] = true; + } + std::vector index(count, InvalidControlFlowId); + std::vector lowlink(count, InvalidControlFlowId); + std::vector componentOf(count, InvalidControlFlowId); + std::vector onStack(count, false); + std::vector stack; + std::vector> work; + std::vector> components; + std::uint32_t nextIndex = 0; + const auto visit = [&](std::uint32_t blockId) { + index[blockId] = nextIndex; + lowlink[blockId] = nextIndex; + ++nextIndex; + stack.push_back(blockId); + onStack[blockId] = true; + work.emplace_back(blockId, 0u); + }; + for (const auto root : members) { + if (index[root] != InvalidControlFlowId) { + continue; + } + visit(root); + while (!work.empty()) { + const auto blockId = work.back().first; + const auto& successors = emission.successors[blockId]; + if (work.back().second < successors.size()) { + const auto successor = successors[work.back().second++]; + if (!inside[successor]) { + continue; + } + if (index[successor] == InvalidControlFlowId) { + visit(successor); + } else if (onStack[successor]) { + lowlink[blockId] = std::min(lowlink[blockId], index[successor]); + } + continue; + } + work.pop_back(); + if (!work.empty()) { + auto& parent = lowlink[work.back().first]; + parent = std::min(parent, lowlink[blockId]); + } + if (lowlink[blockId] != index[blockId]) { + continue; + } + std::vector component; + for (;;) { + const auto member = stack.back(); + stack.pop_back(); + onStack[member] = false; + componentOf[member] = static_cast(components.size()); + component.push_back(member); + if (member == blockId) { + break; + } + } + sortUnique(component); + components.push_back(std::move(component)); + } + } + + std::vector incoming(components.size(), 0u); + std::vector> following(components.size()); + for (const auto member : members) { + for (const auto successor : emission.successors[member]) { + if (inside[successor] && componentOf[successor] != componentOf[member]) { + following[componentOf[member]].push_back(componentOf[successor]); + } + } + } + for (auto& next : following) { + sortUnique(next); + for (const auto component : next) { + ++incoming[component]; + } + } + std::map ready; + for (std::uint32_t component = 0; component < components.size(); ++component) { + if (incoming[component] == 0u) { + ready.emplace(components[component].front(), component); + } + } + std::vector> ordered; + ordered.reserve(components.size()); + while (!ready.empty()) { + const auto component = ready.begin()->second; + ready.erase(ready.begin()); + for (const auto next : following[component]) { + if (--incoming[next] == 0u) { + ready.emplace(components[next].front(), next); + } + } + ordered.push_back(std::move(components[component])); + } + return ordered; +} + +std::uint32_t flagLoopHead(const FlagEmission& emission, const std::vector& component) { + for (const auto member : component) { + const auto& predecessors = emission.predecessors[member]; + if (member == emission.entryBlock || std::any_of(predecessors.begin(), predecessors.end(), [&](std::uint32_t predecessor) { return !std::binary_search(component.begin(), component.end(), predecessor); })) { + return member; + } + } + return component.front(); +} + +std::uint32_t appendFlagBlock(FlagEmission& emission, std::uint32_t positionBlock) { + const auto block = appendRouteBlock(*emission.graph, positionBlock); + emission.order.push_back(block); + return block; +} + +std::uint32_t appendFlagAssignment(FlagEmission& emission, std::uint32_t positionBlock, std::uint32_t variable, std::int32_t value) { + const auto assignment = appendAssignmentBlock(*emission.graph, positionBlock, positionBlock, variable, value); + emission.order.push_back(assignment); + return assignment; +} + +void setDispatch(BasicBlock& block, std::uint32_t variable, std::uint32_t trueTarget, std::uint32_t falseTarget) { + block.successors = {trueTarget, falseTarget}; + block.terminator.kind = TerminatorKind::ConditionalBranch; + block.terminator.condition = BranchCondition::GotoVariable; + block.terminator.trueBlock = trueTarget; + block.terminator.falseBlock = falseTarget; + block.terminator.gotoVariable = variable; +} + +std::vector alwaysNextComponents(const FlagEmission& emission, const std::vector>& components, const std::vector& firsts) { + const auto count = static_cast(components.size()); + std::vector componentOf(emission.successors.size(), InvalidControlFlowId); + for (std::uint32_t component = 0; component < count; ++component) { + for (const auto member : components[component]) { + componentOf[member] = component; + } + } + std::vector next(count, true); + std::vector skipped(count + 1u, 0); + for (std::uint32_t component = 0; component < count; ++component) { + for (const auto member : components[component]) { + for (const auto successor : emission.successors[member]) { + const auto target = componentOf[successor]; + if (target <= component) { + continue; + } + ++skipped[component + 1u]; + --skipped[target]; + if (successor != firsts[target]) { + next[target] = false; + } + } + } + } + const auto entryComponent = componentOf[emission.entryBlock]; + std::int64_t crossing = 0; + for (std::uint32_t component = 0; component < count; ++component) { + crossing += skipped[component]; + if (crossing > 0 || component < entryComponent || (component == entryComponent && firsts[component] != emission.entryBlock)) { + next[component] = false; + } + } + return next; +} + +void emitFlagBlock(FlagEmission& emission, std::uint32_t original, bool guarded) { + auto& graph = *emission.graph; + const auto reach = emission.firstVariable + original; + const auto guard = guarded ? appendFlagBlock(emission, original) : InvalidControlFlowId; + const auto clear = appendFlagAssignment(emission, original, reach, 0); + emission.fallthroughs.push_back(clear); + emission.order.push_back(original); + + std::vector edges; + std::vector ends; + for (const auto target : emission.targets[original]) { + const auto edge = appendFlagAssignment(emission, original, emission.firstVariable + target, 1); + edges.push_back(edge); + const auto loop = std::find_if(emission.openLoops.begin(), emission.openLoops.end(), [&](const std::pair& open) { return open.first == target; }); + if (loop == emission.openLoops.end()) { + ends.push_back(edge); + continue; + } + emission.fallthroughs.push_back(edge); + ends.push_back(appendFlagAssignment(emission, original, loop->second, 1)); + } + + const bool conditional = graph.blocks[original].terminator.kind == TerminatorKind::ConditionalBranch; + const auto join = conditional ? appendFlagBlock(emission, original) : InvalidControlFlowId; + const auto merge = guarded ? appendFlagBlock(emission, original) : InvalidControlFlowId; + const auto exit = conditional ? join : merge; + for (const auto end : ends) { + if (exit == InvalidControlFlowId) { + emission.fallthroughs.push_back(end); + } else { + setBranch(graph.blocks[end], exit); + } + } + if (conditional) { + emission.fallthroughs.push_back(join); + } + if (guarded) { + emission.fallthroughs.push_back(merge); + setDispatch(graph.blocks[guard], reach, clear, merge); + graph.blocks[guard].terminator.mergeBlock = merge; + } + + auto& block = graph.blocks[original]; + block.successors = edges; + block.terminator.mergeBlock = join; + block.terminator.continueBlock = InvalidControlFlowId; + block.terminator.loopHeader = false; + if (!edges.empty()) { + block.terminator.trueBlock = edges.front(); + } + if (conditional) { + block.terminator.falseBlock = edges.back(); + } +} + +void emitFlagRegion(FlagEmission& emission, const std::vector& members); + +void emitFlagLoop(FlagEmission& emission, std::vector component, std::uint32_t head, bool guarded) { + auto& graph = *emission.graph; + const auto again = emission.nextVariable++; + const auto header = appendFlagBlock(emission, head); + emission.fallthroughs.push_back(header); + emission.fallthroughs.push_back(appendFlagAssignment(emission, head, again, 0)); + emission.openLoops.emplace_back(head, again); + emitFlagBlock(emission, head, guarded); + component.erase(std::find(component.begin(), component.end(), head)); + emitFlagRegion(emission, component); + emission.openLoops.pop_back(); + const auto latch = appendFlagBlock(emission, head); + const auto merge = appendFlagBlock(emission, head); + emission.fallthroughs.push_back(merge); + setDispatch(graph.blocks[latch], again, header, merge); + auto& terminator = graph.blocks[header].terminator; + terminator.loopHeader = true; + terminator.mergeBlock = merge; + terminator.continueBlock = latch; +} + +void emitFlagRegion(FlagEmission& emission, const std::vector& members) { + const auto components = orderedComponents(emission, members); + std::vector loops; + std::vector firsts; + for (const auto& component : components) { + const auto first = component.front(); + loops.push_back(component.size() != 1u || contains(emission.successors[first], first)); + firsts.push_back(loops.back() ? flagLoopHead(emission, component) : first); + } + const auto next = emission.openLoops.empty() ? alwaysNextComponents(emission, components, firsts) : std::vector(components.size(), false); + for (std::size_t index = 0; index < components.size(); ++index) { + if (loops[index]) { + emitFlagLoop(emission, components[index], firsts[index], !next[index]); + } else { + emitFlagBlock(emission, firsts[index], !next[index] || emission.graph->blocks[firsts[index]].terminator.kind == TerminatorKind::Return); + } + } +} + } void Structurizer::Structurize(ControlFlowGraph& graph) const { + const auto original = graph; + const bool flagsSupported = supportsFlagStructurization(original); + if (flagsSupported && forcedFlagStructurization()) { + structurizeWithFlags(graph); + return; + } + try { + structurizeAttempts(graph, false); + } catch (const std::runtime_error&) { + const auto error = std::current_exception(); + graph = original; + try { + structurizeAttempts(graph, true); + } catch (const std::runtime_error&) { + if (!flagsSupported) { + std::rethrow_exception(error); + } + graph = original; + try { + structurizeWithFlags(graph); + } catch (const std::runtime_error&) { + std::rethrow_exception(error); + } + } + } +} + +void Structurizer::structurizeWithFlags(ControlFlowGraph& graph) const { + if (!supportsFlagStructurization(graph)) { + throw std::runtime_error("CFG flag structurization supports only branch, conditional branch and return blocks"); + } + const auto count = static_cast(graph.blocks.size()); + const auto entry = graph.entryBlock; + FlagEmission emission; + emission.graph = &graph; + emission.entryBlock = entry; + emission.firstVariable = nextGotoVariable(graph); + emission.nextVariable = emission.firstVariable + count; + emission.targets.resize(count); + emission.successors.resize(count); + emission.predecessors.resize(count); + for (const auto& block : graph.blocks) { + emission.targets[block.id] = terminatorTargets(block.terminator); + emission.successors[block.id] = emission.targets[block.id]; + sortUnique(emission.successors[block.id]); + for (const auto successor : emission.successors[block.id]) { + emission.predecessors[successor].push_back(block.id); + } + } + + emitFlagRegion(emission, allBlockIds(count)); + const auto body = std::move(emission.order); + emission.order.clear(); + for (auto variable = emission.firstVariable; variable < emission.nextVariable; ++variable) { + emission.fallthroughs.push_back(appendFlagAssignment(emission, entry, variable, variable == emission.firstVariable + entry ? 1 : 0)); + } + emission.order.insert(emission.order.end(), body.begin(), body.end()); + appendFlagBlock(emission, entry); + + const auto& order = emission.order; + std::vector placed(graph.blocks.size(), false); + for (const auto blockId : order) { + if (blockId >= placed.size() || placed[blockId]) { + throw std::runtime_error("CFG flag structurization placed a block twice"); + } + placed[blockId] = true; + } + if (order.size() != graph.blocks.size()) { + throw std::runtime_error("CFG flag structurization did not place every block"); + } + std::vector fallthrough(graph.blocks.size(), false); + for (const auto blockId : emission.fallthroughs) { + fallthrough[blockId] = true; + } + for (std::size_t position = 0; position + 1u < order.size(); ++position) { + if (fallthrough[order[position]]) { + setBranch(graph.blocks[order[position]], order[position + 1u]); + } + } + + graph.entryBlock = order.front(); + std::vector blocks; + blocks.reserve(order.size()); + for (const auto blockId : order) { + blocks.push_back(std::move(graph.blocks[blockId])); + } + applyBlockOrder(graph, std::move(blocks)); + rebuildPredecessors(graph); + recomputeAnalyses(graph); + orderByDominance(graph); + if (graph.irreducible || !hasStructuredConstructs(graph)) { + throw std::runtime_error("CFG flag structurization did not produce structured constructs"); + } +} + +void Structurizer::structurizeAttempts(ControlFlowGraph& graph, bool relaxReturnJoins) const { + const auto original = graph; + try { + structurize(graph, false, false, false, relaxReturnJoins); + if (!hasStructuredConstructs(graph)) throw std::runtime_error("CFG does not have structured constructs"); + } catch (const std::runtime_error&) { + graph = original; + try { + structurize(graph, true, false, false, relaxReturnJoins); + if (!hasStructuredConstructs(graph)) throw std::runtime_error("CFG does not have structured constructs"); + } catch (const std::runtime_error&) { + const auto error = std::current_exception(); + graph = original; + try { + structurize(graph, false, true, false, relaxReturnJoins); + if (hasStructuredConstructs(graph)) return; + } catch (const std::runtime_error&) { + } + graph = original; + try { + structurize(graph, false, true, true, relaxReturnJoins); + } catch (const std::runtime_error&) { + std::rethrow_exception(error); + } + if (!hasStructuredConstructs(graph)) { + std::rethrow_exception(error); + } + } + } +} + +void Structurizer::privatizeSharedReturns(ControlFlowGraph& graph) const { + const auto budget = std::max(16u, graph.blocks.size() * 4u); + for (std::size_t clones = 0; privatizeOneSharedReturn(graph); ++clones) { + if (clones == budget) { + throw std::runtime_error("CFG shared return privatization exceeded budget"); + } + rebuildPredecessors(graph); + recomputeAnalyses(graph); + } +} + +void Structurizer::structurize(ControlFlowGraph& graph, bool privatizeReturns, bool routeLeavingExits, bool routeSharedExits, bool relaxReturnJoins) const { recomputeAnalyses(graph); verifyReducibility(graph); - canonicalizeNaturalLoops(graph); - splitSharedMergeBlocks(graph); + if (privatizeReturns) { + privatizeSharedReturns(graph); + } + canonicalizeNaturalLoops(graph, routeLeavingExits, routeSharedExits); + splitSharedMergeBlocks(graph, routeLeavingExits, routeSharedExits, relaxReturnJoins); isolateSemanticLoopHeaders(graph); orderByDominance(graph); clearStructuredTerminators(graph); @@ -1240,14 +2117,14 @@ void Structurizer::Structurize(ControlFlowGraph& graph) const { if (block.terminator.kind != TerminatorKind::ConditionalBranch || block.terminator.loopHeader) { continue; } - if (isInnermostLoopControlConditional(graph, block)) { - if (const auto merge = exitTailSelectionMerge(graph, block); merge != InvalidControlFlowId) { + if (isStructuredLoopExit(graph, block, routeLeavingExits)) { + if (const auto merge = exitTailSelectionMerge(graph, block, routeLeavingExits); merge != InvalidControlFlowId) { exitTails.emplace_back(block.id, merge); } continue; } - const auto merge = findSelectionMerge(graph, block); + const auto merge = findSelectionMerge(graph, block, routeLeavingExits, routeSharedExits); if (merge == InvalidControlFlowId) { throw std::runtime_error("conditional block " + std::to_string(block.id) + " has no structured merge"); } @@ -1266,34 +2143,47 @@ void Structurizer::Structurize(ControlFlowGraph& graph) const { 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 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 (auto& block : graph.blocks) { - if (block.id == graph.entryBlock) { - continue; - } - + for (const auto id : order) { + auto& block = graph.blocks[id]; + 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) { - next = intersectSorted(next, graph.blocks[block.predecessors[i]].dominators); - } - addUnique(next, block.id); - sortUnique(next); + 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) { - block.dominators = std::move(next); + replaceAnalysisSet(block.dominators, std::move(next)); changed = true; } } @@ -1337,30 +2227,45 @@ 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 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; - for (auto& block : graph.blocks) { + for (const auto id : order) { + auto& block = graph.blocks[id]; std::vector next; if (block.successors.empty()) { next = {block.id}; } else { next = graph.blocks[block.successors.front()].postDominators; - for (std::size_t i = 1; i < block.successors.size(); ++i) { - next = intersectSorted(next, graph.blocks[block.successors[i]].postDominators); - } - addUnique(next, block.id); - sortUnique(next); + 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) { - block.postDominators = std::move(next); + replaceAnalysisSet(block.postDominators, std::move(next)); changed = true; } } @@ -1414,7 +2319,7 @@ void Structurizer::recomputeAnalyses(ControlFlowGraph& graph) const { computeStronglyConnectedComponents(graph); } -void Structurizer::canonicalizeNaturalLoops(ControlFlowGraph& graph) const { +void Structurizer::canonicalizeNaturalLoops(ControlFlowGraph& graph, bool routeLeavingExits, bool routeSharedExits) const { const auto rewriteBudget = graph.blocks.size() * 2u + 16u; for (std::size_t rewrite = 0; rewrite < rewriteBudget; ++rewrite) { bool changed = false; @@ -1425,7 +2330,12 @@ void Structurizer::canonicalizeNaturalLoops(ControlFlowGraph& graph) const { addUnique(latches, edge.sourceBlock); } } - if (latches.size() <= 1u) { + const bool latchInInnerLoop = routeSharedExits && latches.size() == 1u && std::any_of(graph.naturalLoops.begin(), graph.naturalLoops.end(), [&](const NaturalLoop& inner) { + return inner.headerBlock != loop.headerBlock && contains(loop.blocks, inner.headerBlock) && contains(inner.blocks, latches.front()); + }); + const auto& header = graph.FindBlock(loop.headerBlock); + const bool semanticSelfLoop = latches.size() == 1u && latches.front() == header.id && header.instructionBegin != header.instructionEnd; + if (latches.size() <= 1u && !latchInInnerLoop && !semanticSelfLoop) { continue; } @@ -1465,7 +2375,7 @@ void Structurizer::canonicalizeNaturalLoops(ControlFlowGraph& graph) const { const NaturalLoop* unmerged = nullptr; for (const auto& loop : graph.naturalLoops) { - if (loop.exitBlocks.size() > 1u && loop.mergeBlock == InvalidControlFlowId && (unmerged == nullptr || loop.blocks.size() < unmerged->blocks.size())) { + if (loop.exitBlocks.size() > 1u && (loop.mergeBlock == InvalidControlFlowId || (routeLeavingExits && (exitsLeaveConstruct(graph, loop) || (routeSharedExits && hasSharedExitSelection(graph, loop))))) && (unmerged == nullptr || loop.blocks.size() < unmerged->blocks.size())) { unmerged = &loop; } } @@ -1475,7 +2385,8 @@ void Structurizer::canonicalizeNaturalLoops(ControlFlowGraph& graph) const { std::string refusal; auto routed = routeLoopExits(graph, *unmerged, [this](ControlFlowGraph& candidate) { recomputeAnalyses(candidate); }, refusal); if (!routed) { - throw std::runtime_error("loop at block " + std::to_string(unmerged->headerBlock) + " has exits with no common post-dominator: " + refusal); + const auto exits = unmerged->mergeBlock == InvalidControlFlowId ? std::string("with no common post-dominator") : "that leave the loop before its merge block " + std::to_string(unmerged->mergeBlock); + throw std::runtime_error("loop at block " + std::to_string(unmerged->headerBlock) + " has exits " + exits + ": " + refusal); } graph = std::move(*routed); } @@ -1483,15 +2394,19 @@ void Structurizer::canonicalizeNaturalLoops(ControlFlowGraph& graph) const { throw std::runtime_error("CFG loop canonicalization exceeded rewrite budget"); } -void Structurizer::splitSharedMergeBlocks(ControlFlowGraph& graph) const { +void Structurizer::splitSharedMergeBlocks(ControlFlowGraph& graph, bool routeLeavingExits, bool routeSharedExits, bool relaxReturnJoins) const { const auto originalBlockCount = static_cast(graph.blocks.size()); const auto splitBudget = std::max(16u, originalBlockCount * 4u); + const auto growthBudget = std::max(SplitGrowthFloor, originalBlockCount * SplitGrowthFactor); const auto programWords = estimatedSpirvWords(graph, allBlockIds(originalBlockCount)); SplitBudget budget{programWords, std::max(CloneWordLimit, programWords / CloneBudgetDivisor), std::max(CloneWordLimit, programWords), nextGotoVariable(graph)}; for (std::uint32_t splits = 0; splits < splitBudget; ++splits) { - if (!splitOneLoopMerge(graph) && !splitOneSelectionMerge(graph, budget, [this](ControlFlowGraph& candidate) { recomputeAnalyses(candidate); }) && !splitOneReturnJoin(graph)) { + if (!splitOneLoopMerge(graph) && !splitOneSelectionMerge(graph, budget, [this](ControlFlowGraph& candidate) { recomputeAnalyses(candidate); }, routeLeavingExits, routeSharedExits) && !splitOneReturnJoin(graph, relaxReturnJoins)) { return; } + if (graph.blocks.size() > growthBudget) { + throw std::runtime_error("CFG shared merge splitting exceeded growth budget: originalBlocks=" + std::to_string(originalBlockCount) + " currentBlocks=" + std::to_string(graph.blocks.size()) + " growthBudget=" + std::to_string(growthBudget)); + } rebuildPredecessors(graph); recomputeAnalyses(graph); verifyReducibility(graph); @@ -1553,4 +2468,98 @@ void Structurizer::verifyReducibility(const ControlFlowGraph& graph) const { } } +bool Structurizer::hasStructuredConstructs(const ControlFlowGraph& graph) const { + for (const auto& block : graph.blocks) { + const auto& terminator = block.terminator; + if (terminator.kind != TerminatorKind::ConditionalBranch || terminator.loopHeader || terminator.mergeBlock != InvalidControlFlowId || terminator.trueBlock == terminator.falseBlock) continue; + const BasicBlock* loop = nullptr; + for (const auto& header : graph.blocks) { + if (header.terminator.loopHeader && graph.Dominates(header.id, block.id) && !graph.Dominates(header.terminator.mergeBlock, block.id) && (loop == nullptr || graph.Dominates(loop->id, header.id))) loop = &header; + } + if (loop == nullptr || (terminator.trueBlock != loop->terminator.mergeBlock && terminator.trueBlock != loop->terminator.continueBlock && terminator.falseBlock != loop->terminator.mergeBlock && terminator.falseBlock != loop->terminator.continueBlock)) return false; + } + auto structural = graph; + for (auto& block : structural.blocks) { + if (block.terminator.mergeBlock != InvalidControlFlowId) { + block.successors.push_back(block.terminator.mergeBlock); + } + if (block.terminator.loopHeader) { + block.successors.push_back(block.terminator.continueBlock); + } + } + rebuildPredecessors(structural); + computeDominatorTree(structural); + computePostDominators(structural); + const auto inRegion = [&](const BasicBlock& header, std::uint32_t blockId) { + return structural.Dominates(header.id, blockId) && !structural.Dominates(header.terminator.mergeBlock, blockId); + }; + const auto inContinue = [&](const BasicBlock& header, std::uint32_t blockId) { + return structural.Dominates(header.terminator.continueBlock, blockId) && structural.PostDominates(header.terminator.continueBlock, blockId); + }; + const auto inConstruct = [&](const BasicBlock& header, std::uint32_t blockId) { + return inRegion(header, blockId) && !(header.terminator.loopHeader && inContinue(header, blockId)); + }; + const auto innermostLoop = [&](std::uint32_t blockId) { + const BasicBlock* innermost = nullptr; + for (const auto& header : graph.blocks) { + if (header.terminator.loopHeader && inRegion(header, blockId) && (innermost == nullptr || structural.Dominates(innermost->id, header.id))) { + innermost = &header; + } + } + return innermost; + }; + const auto exitsLoop = [](const BasicBlock* loop, std::uint32_t target) { + return loop != nullptr && (target == loop->terminator.mergeBlock || target == loop->terminator.continueBlock); + }; + for (const auto& block : graph.blocks) { + const auto& terminator = block.terminator; + for (const auto successor : block.successors) { + const auto& target = graph.FindBlock(successor); + if (structural.Dominates(successor, block.id) && (!target.terminator.loopHeader || target.terminator.continueBlock != block.id)) { + return false; + } + } + if (terminator.kind == TerminatorKind::ConditionalBranch && !terminator.loopHeader && terminator.mergeBlock == InvalidControlFlowId && terminator.trueBlock != terminator.falseBlock) { + const auto* loop = innermostLoop(block.id); + if (!exitsLoop(loop, terminator.trueBlock) && !exitsLoop(loop, terminator.falseBlock)) { + return false; + } + } + if (terminator.mergeBlock == InvalidControlFlowId) { + continue; + } + if (terminator.mergeBlock == block.id || !structural.Dominates(block.id, terminator.mergeBlock)) { + return false; + } + if (terminator.loopHeader && (terminator.continueBlock == terminator.mergeBlock || !structural.Dominates(block.id, terminator.continueBlock))) { + return false; + } + const auto* loop = terminator.loopHeader ? &block : innermostLoop(block.id); + for (const auto& member : graph.blocks) { + if (inConstruct(block, member.id)) { + if (member.id != block.id && member.terminator.mergeBlock != InvalidControlFlowId && !inConstruct(block, member.terminator.mergeBlock)) { + return false; + } + if (member.id != block.id && !std::all_of(member.predecessors.begin(), member.predecessors.end(), [&](std::uint32_t predecessor) { return inConstruct(block, predecessor); })) { + return false; + } + if (!std::all_of(member.successors.begin(), member.successors.end(), [&](std::uint32_t successor) { return inConstruct(block, successor) || successor == terminator.mergeBlock || exitsLoop(loop, successor); })) { + return false; + } + } else if (terminator.loopHeader && inContinue(block, member.id)) { + if (!std::all_of(member.successors.begin(), member.successors.end(), [&](std::uint32_t successor) { return inContinue(block, successor) || successor == block.id || successor == terminator.mergeBlock; })) { + return false; + } + } + } + if (terminator.loopHeader) { + const auto& predecessors = graph.FindBlock(terminator.continueBlock).predecessors; + if (!std::all_of(predecessors.begin(), predecessors.end(), [&](std::uint32_t predecessor) { return inConstruct(block, predecessor) || inContinue(block, predecessor); })) { + return false; + } + } + } + return true; +} + } diff --git a/core/shader/recompiler/Deserialize.cpp b/core/shader/recompiler/Deserialize.cpp index c7379f1a7a..6f1b3a0ca9 100644 --- a/core/shader/recompiler/Deserialize.cpp +++ b/core/shader/recompiler/Deserialize.cpp @@ -167,6 +167,7 @@ int main(int argc, char** argv) { translateOptions.userDataBaseRegister = request.context.userDataBaseRegister; 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; translateOptions.inputInfo = inputInfo; InstructionTranslator translator; @@ -225,7 +226,7 @@ int main(int argc, char** argv) { std::cout << "ShaderInfoCollector::Collect OK\n"; BindingAllocator bindingAllocator; - const auto bindings = bindingAllocator.Allocate(program, request.layout); + const auto bindings = bindingAllocator.Allocate(program, request.layout, request.target.registrationModules); std::cout << "BindingAllocator::Allocate OK, bindings=" << bindings.bindings.size() << "\n"; std::cout << "ALL STAGES OK\n"; diff --git a/core/shader/recompiler/FragmentOutputSpecialization.hpp b/core/shader/recompiler/FragmentOutputSpecialization.hpp new file mode 100644 index 0000000000..00894c6af1 --- /dev/null +++ b/core/shader/recompiler/FragmentOutputSpecialization.hpp @@ -0,0 +1,224 @@ +#ifndef CORE_SHADER_RECOMPILER_FRAGMENTOUTPUTSPECIALIZATION_HPP +#define CORE_SHADER_RECOMPILER_FRAGMENTOUTPUTSPECIALIZATION_HPP + +#include "PipelineSpecialization.hpp" +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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::Float11_11_10)) throw std::runtime_error("invalid prepared fragment export packing"); + if (constant.value != static_cast(ColorExportPacking::None)) packed.insert(constant.id - PipelineSpecialization::ExportPackingBase); + } + if (packed.empty() && !dualSource && reversed == 0u) return words; + if (words.size() < 5u || words[0] != spv::MagicNumber) throw std::runtime_error("invalid fragment output specialization module"); + std::map locations; + std::set indexed; + std::map> typeKeys; + std::map, std::uint32_t> types; + std::map outputs; + 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 fragment output specialization instruction"); + if (op == spv::OpDecorate && count == 4u && words[cursor + 2u] == spv::DecorationLocation) locations.emplace(words[cursor + 1u], words[cursor + 3u]); + if (op == spv::OpDecorate && count == 4u && words[cursor + 2u] == spv::DecorationIndex) indexed.insert(words[cursor + 1u]); + if ((op == spv::OpTypeInt || op == spv::OpTypeFloat || op == spv::OpTypeVector || op == spv::OpTypePointer) && count >= 3u) { + std::vector key{static_cast(op)}; + key.insert(key.end(), words.begin() + cursor + 2u, words.begin() + cursor + count); + typeKeys.emplace(words[cursor + 1u], key); + types.emplace(std::move(key), words[cursor + 1u]); + } + if (op == spv::OpVariable && count >= 4u && words[cursor + 3u] == spv::StorageClassOutput) outputs.emplace(words[cursor + 2u], words[cursor + 1u]); + cursor += count; + } + const auto key = [&](std::uint32_t id) { + const auto found = typeKeys.find(id); + return found != typeKeys.end() ? found->second : std::vector{}; + }; + const auto floatVectorPointer = [&](std::uint32_t pointer) { + const auto outer = key(pointer); + if (outer.size() != 3u || outer[0] != spv::OpTypePointer || outer[1] != spv::StorageClassOutput) return false; + const auto vector = key(outer[2]); + return vector.size() == 3u && vector[0] == spv::OpTypeVector && vector[2] == 4u && key(vector[1]) == std::vector{spv::OpTypeFloat, 32u}; + }; + const auto output = [&](std::uint32_t location) { + std::uint32_t variable = 0; + for (const auto& [id, pointer] : outputs) { + const auto found = locations.find(id); + if (found == locations.end() || found->second != location || indexed.contains(id)) continue; + if (variable != 0u) throw std::runtime_error("fragment outputs share location " + std::to_string(location)); + variable = id; + } + return variable; + }; + std::set retyped; + for (const auto location : packed) { + const auto variable = output(location); + if (variable == 0u) continue; + if (!specializedIds.contains(PipelineSpecialization::ExportPackingBase + location)) throw std::runtime_error("fragment output " + std::to_string(location) + " has no color export packing path"); + if (!floatVectorPointer(outputs.at(variable))) throw std::runtime_error("packed fragment output " + std::to_string(location) + " is not a float4"); + retyped.insert(variable); + } + const auto secondSource = dualSource ? output(1u) : 0u; + if (retyped.contains(secondSource)) throw std::runtime_error("the second dual-source blend color cannot be packed"); + std::uint32_t blended = 0; + if (reversed != 0u) { + if (dualSource) throw std::runtime_error("reversed-order blending cannot be combined with dual-source blending"); + blended = output(0u); + if (blended == 0u || retyped.contains(blended) || !floatVectorPointer(outputs.at(blended))) throw std::runtime_error("reversed-order blending needs a float4 MRT0 output"); + for (const auto& [id, pointer] : outputs) { + if (id != blended && locations.contains(id)) throw std::runtime_error("reversed-order blending needs MRT0 to be the only color output"); + } + } + if (retyped.empty() && secondSource == 0u && blended == 0u) return words; + auto bound = words[3]; + const auto allocate = [&] { + if (bound == std::numeric_limits::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); + if (found != types.end()) return found->second; + if (bound == std::numeric_limits::max()) throw std::runtime_error("fragment output specialization module ID overflow"); + const auto id = bound++; + std::vector instruction{(static_cast(declaration.size() + 1u) << 16u) | declaration.front(), id}; + instruction.insert(instruction.end(), declaration.begin() + 1, declaration.end()); + types.emplace(std::move(declaration), id); + declarations.push_back(std::move(instruction)); + return id; + }; + const auto wordType = retyped.empty() ? 0u : declare({spv::OpTypeInt, 32u, 0u}); + const auto vectorType = retyped.empty() ? 0u : declare({spv::OpTypeVector, wordType, 4u}); + const auto pointerType = retyped.empty() ? 0u : declare({spv::OpTypePointer, spv::StorageClassOutput, vectorType}); + const auto colorType = blended == 0u ? 0u : key(outputs.at(blended))[2]; + const auto floatType = blended == 0u ? 0u : key(colorType)[1]; + const auto zero = blended == 0u ? 0u : allocate(); + const auto one = blended == 0u ? 0u : allocate(); + const auto secondColor = blended == 0u ? 0u : allocate(); + 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; + const auto op = static_cast(words[cursor] & 0xffffu); + const auto operand = [&](std::size_t index) { return index < count ? words[cursor + index] : 0u; }; + if (op == spv::OpVariable && retyped.contains(operand(2u))) { + if (count != 4u) throw std::runtime_error("a packed fragment output has an initializer"); + std::vector variable(words.begin() + cursor, words.begin() + cursor + count); + variable[1] = pointerType; + variables.push_back(std::move(variable)); + cursor += count; + continue; + } + if (op == spv::OpDecorate && count == 4u && secondSource != 0u && operand(1u) == secondSource && operand(2u) == spv::DecorationLocation) { + result.insert(result.end(), {(4u << 16u) | spv::OpDecorate, secondSource, spv::DecorationLocation, 0u, (4u << 16u) | spv::OpDecorate, secondSource, spv::DecorationIndex, 1u}); + cursor += count; + continue; + } + if (op == spv::OpDecorate && count == 4u && blended != 0u && operand(1u) == blended && operand(2u) == spv::DecorationLocation) { + result.insert(result.end(), words.begin() + cursor, words.begin() + cursor + count); + result.insert(result.end(), {(4u << 16u) | spv::OpDecorate, secondColor, spv::DecorationLocation, 0u, (4u << 16u) | spv::OpDecorate, secondColor, spv::DecorationIndex, 1u}); + cursor += count; + continue; + } + if (op == spv::OpEntryPoint && secondColor != 0u) { + result.push_back(((count + 1u) << 16u) | spv::OpEntryPoint); + result.insert(result.end(), words.begin() + cursor + 1u, words.begin() + cursor + count); + result.push_back(secondColor); + cursor += count; + continue; + } + if (op == spv::OpFunction && !inserted) { + for (const auto& declaration : declarations) result.insert(result.end(), declaration.begin(), declaration.end()); + for (const auto& variable : variables) result.insert(result.end(), variable.begin(), variable.end()); + inserted = true; + } + if (op == spv::OpStore && count >= 3u && retyped.contains(operand(1u))) { + if (bound == std::numeric_limits::max()) throw std::runtime_error("fragment output specialization module ID overflow"); + const auto bits = bound++; + result.insert(result.end(), {(4u << 16u) | spv::OpBitcast, vectorType, bits, operand(2u)}); + result.insert(result.end(), words.begin() + cursor, words.begin() + cursor + count); + result[result.size() - count + 2u] = bits; + cursor += count; + continue; + } + if (op == spv::OpStore && count >= 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; + } + if (!inserted) throw std::runtime_error("fragment output specialization module has no function"); + result[3] = bound; + return result; +} + +} + +#endif 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/IrBlock.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrBlock.hpp index adb325a3ed..435a5c0b1a 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrBlock.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrBlock.hpp @@ -5,10 +5,37 @@ #include #include #include +#include #include namespace ShaderRecompiler { +template +class IrSsaValues { +public: + IrValue*& operator[](std::size_t index) { + if (storage == nullptr) storage = std::make_unique>(); + return (*storage)[index]; + } + + IrValue* operator[](std::size_t index) const { return values()[index]; } + bool HasStorage() const { return storage != nullptr; } + void fill(IrValue* value) { + if (value == nullptr) { storage.reset(); return; } + if (storage == nullptr) storage = std::make_unique>(); + storage->fill(value); + } + auto begin() const { return values().begin(); } + auto end() const { return values().end(); } + +private: + const std::array& values() const { + static const std::array empty{}; + return storage != nullptr ? *storage : empty; + } + std::unique_ptr> storage; +}; + class IrBlock { public: explicit IrBlock(std::uint32_t id); @@ -29,10 +56,10 @@ public: void AddPredecessor(IrBlock* block); void AddSuccessor(IrBlock* block); - std::array ssaScalarValues{}; - std::array ssaThreadBitScalarValues{}; - std::array ssaScalarMaskTags{}; - std::array ssaVectorValues{}; + IrSsaValues ssaScalarValues{}; + IrSsaValues ssaThreadBitScalarValues{}; + IrSsaValues ssaScalarMaskTags{}; + IrSsaValues ssaVectorValues{}; [[nodiscard]] const std::vector& Predecessors() const; [[nodiscard]] const std::vector& Successors() const; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrBuilder.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrBuilder.hpp index 99b5ef6faa..39c7c07c37 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrBuilder.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrBuilder.hpp @@ -88,6 +88,7 @@ public: } private: + IrValue* vectorWindowIndex(VectorReg reg); IrProgram& program; IrBlock* insertionPoint; }; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp index eed5b95514..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; }; @@ -62,6 +87,8 @@ struct ResourceBlock { IrValue* condition = nullptr; 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 91952766ec..809c3e0932 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp @@ -39,10 +39,12 @@ 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; std::uint32_t runtimeImageCount = 0; + std::uint32_t runtimeStateDwords = 0; std::vector userDataRegisters; std::vector descriptors; @@ -52,12 +54,18 @@ struct IrBindingLayout { [[nodiscard]] std::uint32_t ImageMetadataDword() const { return DispatchThreadLimitDword() + (dispatchThreadLimit ? 3u : 0u); } - [[nodiscard]] std::uint32_t ShaderDataDwords() const { + [[nodiscard]] std::uint32_t RuntimeStateDword() const { return ImageMetadataDword() + runtimeImageCount * (sizeof(RuntimeAbi::ResourceMetadata) / sizeof(std::uint32_t)); } + [[nodiscard]] std::uint32_t ShaderDataDwords() const { + return RuntimeStateDword() + runtimeStateDwords; + } [[nodiscard]] bool UsesPushData() const { return pushDataStartDword != PushData::NoStart; } + [[nodiscard]] std::uint32_t PushSlotDword() const { + return UsesPushData() ? pushDataStartDword / PushData::DwordCount * PushData::DwordCount : 0u; + } void AdvancePushData(std::uint32_t& cursor) const { if (UsesPushData()) { cursor = pushDataStartDword + ShaderDataDwords(); diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp index 99cf118a7b..ae85d35ec0 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp @@ -19,10 +19,21 @@ 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; bool shaderInfoComplete = false; IrBindingLayout bindings; bool bindingLayoutComplete = false; + std::uint32_t tessellationLocalStride = 0; + std::uint32_t vectorWindowBase = 0; + std::uint32_t vectorWindowSize = 0; + bool vectorWindowIndexedWrites = false; + std::uint32_t tessellationControlStride = 0; + std::uint32_t tessellationPatchStride = 0; + 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 a8216cc163..e340873ac7 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourceInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourceInfo.hpp @@ -50,6 +50,8 @@ struct MemoryInfo { bool planningOnly = false; 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 953dacc163..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; @@ -42,6 +132,8 @@ struct ResourceSnapshot { std::vector userData; UniformFill uniformFill; std::vector srtPoison; + std::uint32_t nullRootReads = 0; + ImageTableSnapshot tables; }; struct UniformFillPlan { @@ -49,7 +141,85 @@ struct UniformFillPlan { std::array values{}; }; -inline constexpr std::uint32_t NativePushConstantSize = sizeof(PushData); +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; @@ -57,7 +227,7 @@ struct IrResourcePlan { 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; @@ -70,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 9edaac96b7..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; @@ -50,15 +52,18 @@ inline constexpr std::uint32_t AddressBorder = 2u; inline constexpr std::uint32_t ReferenceFloat = 0u; inline constexpr std::uint32_t ReferenceUnorm = 1u; inline constexpr std::uint32_t ReferenceSnorm = 2u; +inline constexpr std::uint32_t ReferenceZero = 3u; [[nodiscard]] inline std::uint32_t Function(std::uint32_t state) { return (state >> FunctionShift) & 0x7u; } [[nodiscard]] inline std::uint32_t AddressX(std::uint32_t state) { return (state >> ClampXShift) & 0x3u; } [[nodiscard]] inline std::uint32_t AddressY(std::uint32_t state) { return (state >> ClampYShift) & 0x3u; } [[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; @@ -85,19 +90,26 @@ 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; }; -enum SamplerUse : std::uint8_t { +enum SamplerUse : std::uint16_t { SamplerUseExplicitLod = 1u << 0u, SamplerUseImplicitLod = 1u << 1u, SamplerUseGradient = 1u << 2u, @@ -106,6 +118,7 @@ enum SamplerUse : std::uint8_t { SamplerUseGather = 1u << 5u, SamplerUseQueryLod = 1u << 6u, SamplerUseAdjust = 1u << 7u, + SamplerUseNonZeroLod = 1u << 8u, }; inline bool ImageSampleExplicitLod(std::uint32_t flags, IrShaderStage stage) { @@ -120,11 +133,21 @@ struct SamplerResource { std::uint32_t copyOf = NoCopy; bool forcePointFiltering = false; bool depthCompare = false; - std::uint8_t uses = 0; + 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.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 | SamplerUseOffset)) == 0u && !sampler.depthCompare; +} + struct SampledResourcePair { std::uint32_t image = 0; std::uint32_t sampler = 0; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp index ca81c0fec2..1440effcb2 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp @@ -35,10 +35,16 @@ struct ShaderInfo { bool bdaWrites = false; bool usesFaultBuffer = false; bool dispatchThreadLimit = false; + bool rasterPrimitive = false; + bool layerExported = false; bool operator==(const ShaderInfo& other) const = default; }; +[[nodiscard]] inline std::uint32_t WorkgroupMemoryStrideDwords(const ShaderInfo& info) { + return info.sharedMemoryBytes == 0u ? 0u : (info.sharedMemoryBytes / 4u + 2u) & ~1u; +} + } #endif diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp index ca9e8fd91e..bfa9785b83 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp @@ -13,7 +13,18 @@ enum class TessellationAttribute { ControlOutput, EvaluationInput, PatchOutput, - Factor + Factor, + PatchInput, + InstanceOutput, + InstanceInput, + ControlShared +}; + +enum class TessellationBaseKind { + OffChip, + Factor, + RelativeIndex, + PassthroughPrimitive }; enum class StageInputKind { @@ -36,14 +47,41 @@ enum class StageInputKind { DispatchThreadLimit, Parameter, 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, Mrt, Depth, SampleMask, + StencilRef, PointSize, ClipDistance, CullDistance, diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp index d223ca6960..c1ed5e72ed 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp @@ -73,12 +73,14 @@ enum class IrOpcode : std::uint16_t { Void, Reference, ReferenceU32, + UnlinkedCall, GetUserData, GetShaderBase, ShaderClock, RealtimeClock, MeshDrawParameter, MeshArgument, + MeshRestartStart, MeshAllocate, TessellationBase, GetTessellationAttribute, @@ -92,6 +94,8 @@ enum class IrOpcode : std::uint16_t { SetScalarRegister, GetVectorRegister, SetVectorRegister, + LoadVectorWindow, + StoreVectorWindow, GetGotoVariable, SetGotoVariable, GetScc, @@ -140,6 +144,8 @@ enum class IrOpcode : std::uint16_t { CompositeExtractU32x4, PackHalf2x16, PackFloat2x16Rtz, + FPAdd16x2, + FPMul16x2, FPAbs32, FPNeg32, FPSaturate32, @@ -293,6 +299,9 @@ enum class IrOpcode : std::uint16_t { StoreAddressU8, StoreAddressU16, StoreAddressU32, + StoreAddressU32x2, + StoreAddressU32x3, + StoreAddressU32x4, AddressAtomicSwap32, AddressAtomicCmpSwap32, AddressAtomicIAdd32, @@ -423,6 +432,8 @@ enum class IrOpcode : std::uint16_t { SharedAtomicMskor64, DataAppend, DataConsume, + DataOrderedCount, + DataOrderedSwap, SwizzleU32, ImageQueryDimensions, ImageQueryLod, @@ -460,6 +471,8 @@ enum class IrOpcode : std::uint16_t { GetAttribute, GetInterpolationParameter, GetInterpolationParameterF16, + InterpolateHostP1, + InterpolateHostP2, SetAttribute, ControlNop, Waitcnt, diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp index 26739b94f6..8d051b4d14 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp @@ -3,6 +3,7 @@ #include "IntermediateRepresentation/IrBlock.hpp" #include "IntermediateRepresentation/IrMetadata.hpp" +#include #include #include #include @@ -26,9 +27,11 @@ public: const IrResourcePlan& Resources() const; IrProgramMetadata& Metadata(); const IrProgramMetadata& Metadata() const; + [[nodiscard]] CompiledShaderInfo CompiledInfo() const&; CompiledShaderInfo TakeCompiledInfo() &&; IrValue& CreateValue(IrOpcode opcode, IrType type, std::uint64_t flags); void SetEntryBlock(IrBlock& block); + std::size_t ReclaimUnusedValues(); [[nodiscard]] std::vector& BlockOrder(); [[nodiscard]] const std::vector& BlockOrder() const; 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/IrBuilder/IrBuilderRegisters.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrBuilder/IrBuilderRegisters.cpp index 2a81dd39b5..3698a7afc1 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrBuilder/IrBuilderRegisters.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrBuilder/IrBuilderRegisters.cpp @@ -1,6 +1,7 @@ #include "IntermediateRepresentation/IrBuilder.hpp" #include "IntermediateRepresentation/IrBuilderInternal.hpp" #include +#include namespace ShaderRecompiler { @@ -106,12 +107,26 @@ void IrBuilder::SetScalarMaskTag(ScalarReg reg, IrValue& value) { (void)Emit(IrOpcode::SetScalarMaskTag, IrType::Void, {&operand, &value}); } +IrValue* IrBuilder::vectorWindowIndex(VectorReg reg) { + const auto& metadata = program.Metadata(); + const auto index = static_cast(RegIndex(reg)); + if (metadata.vectorWindowSize == 0u || index < metadata.vectorWindowBase) return nullptr; + if (index - metadata.vectorWindowBase >= metadata.vectorWindowSize) throw std::runtime_error("vector register " + std::to_string(index) + " lies past the relative-addressing window"); + return &Constant(index - metadata.vectorWindowBase); +} + IrValue& IrBuilder::GetVectorReg(VectorReg reg) { + IrValue* index = vectorWindowIndex(reg); + if (index != nullptr && program.Metadata().vectorWindowIndexedWrites) return Emit(IrOpcode::LoadVectorWindow, IrType::U32, {index}); IrValue& operand = createRegisterOperand(program, RegisterBank::Vector, RegIndex(reg), IrType::VectorReg); return Emit(IrOpcode::GetVectorRegister, IrType::U32, {&operand}); } void IrBuilder::SetVectorReg(VectorReg reg, IrValue& value) { + if (IrValue* index = vectorWindowIndex(reg)) { + (void)Emit(IrOpcode::StoreVectorWindow, IrType::Void, {index, &value}); + if (program.Metadata().vectorWindowIndexedWrites) return; + } IrValue& operand = createRegisterOperand(program, RegisterBank::Vector, RegIndex(reg), IrType::VectorReg); (void)Emit(IrOpcode::SetVectorRegister, IrType::Void, {&operand, &value}); } diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp index a06c8e9f44..b3622f4c6a 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp @@ -114,12 +114,14 @@ 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), makeMeta("RealtimeClock", U64), makeMeta("MeshDrawParameter", U32, U32), makeMeta("MeshArgument", U32, U32), + makeMeta("MeshRestartStart", U32, U32, U1), makeMeta("MeshAllocate", Void, U32), makeMeta("TessellationBase", U32, U32), makeMeta("GetTessellationAttribute", U32, U32, U32, U1), @@ -133,6 +135,8 @@ constexpr std::array(IrOpcode::Count)> Meta makeMeta("SetScalarRegister", Void, ScalarReg, U32), makeMeta("GetVectorRegister", U32, VectorReg), makeMeta("SetVectorRegister", Void, VectorReg, U32), + makeMeta("LoadVectorWindow", U32, U32), + makeMeta("StoreVectorWindow", Void, U32, U32), makeMeta("GetGotoVariable", U1, U32), makeMeta("SetGotoVariable", Void, U32, U1), makeMeta("GetScc", U1), @@ -181,6 +185,8 @@ constexpr std::array(IrOpcode::Count)> Meta makeMeta("CompositeExtractU32x4", U32, U32x4, U32), makeMeta("PackHalf2x16", U32, F32x2), makeMeta("PackFloat2x16Rtz", U32, F32, F32), + makeMeta("FPAdd16x2", U32, U32, U32), + makeMeta("FPMul16x2", U32, U32, U32), makeMeta("FPAbs32", F32, F32), makeMeta("FPNeg32", F32, F32), makeMeta("FPSaturate32", F32, F32), @@ -334,6 +340,9 @@ constexpr std::array(IrOpcode::Count)> Meta makeMeta("StoreAddressU8", Void, AddressResource, U32, U32, U8, U1), makeMeta("StoreAddressU16", Void, AddressResource, U32, U32, U16, U1), makeMeta("StoreAddressU32", Void, AddressResource, U32, U32, U32, U1), + makeMeta("StoreAddressU32x2", Void, AddressResource, U32, U32, U32x2, U1), + makeMeta("StoreAddressU32x3", Void, AddressResource, U32, U32, U32x3, U1), + makeMeta("StoreAddressU32x4", Void, AddressResource, U32, U32, U32x4, U1), makeMeta("AddressAtomicSwap32", U32, AddressResource, U32, U32, U32, U1), makeMeta("AddressAtomicCmpSwap32", U32, AddressResource, U32, U32, U32, U32, U1), makeMeta("AddressAtomicIAdd32", U32, AddressResource, U32, U32, U32, U1), @@ -464,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), @@ -501,6 +512,8 @@ constexpr std::array(IrOpcode::Count)> Meta makeMeta("GetAttribute", U32, U32, U32), makeMeta("GetInterpolationParameter", U32, U32, U32, U32), makeMeta("GetInterpolationParameterF16", F32, U32, U32, U32, U32), + makeMeta("InterpolateHostP1", F32, U32, U32, F32), + makeMeta("InterpolateHostP2", Void, U32, U32, F32, F32, U1), makeMeta("SetAttribute", Void, U32x4, U1), makeMeta("ControlNop", Void), makeMeta("Waitcnt", Void), @@ -685,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; @@ -747,6 +762,12 @@ AddressOpcodeInfo AddressOpcodeInfoOf(IrOpcode opcode) { return {AddressAccess::Write, 16u}; case IrOpcode::StoreAddressU32: return {AddressAccess::Write, 32u}; + case IrOpcode::StoreAddressU32x2: + return {AddressAccess::Write, 32u, 2u}; + case IrOpcode::StoreAddressU32x3: + return {AddressAccess::Write, 32u, 3u}; + case IrOpcode::StoreAddressU32x4: + return {AddressAccess::Write, 32u, 4u}; case IrOpcode::AddressAtomicSwap32: case IrOpcode::AddressAtomicCmpSwap32: case IrOpcode::AddressAtomicIAdd32: @@ -872,11 +893,14 @@ 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: case IrOpcode::SetScalarMaskTag: case IrOpcode::SetScalarRegister: case IrOpcode::SetVectorRegister: + case IrOpcode::StoreVectorWindow: case IrOpcode::SetGotoVariable: case IrOpcode::SetScc: case IrOpcode::SetExec: diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp index 6a9d16ec0f..08dba6b1d9 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp @@ -207,6 +207,58 @@ std::string valueToString(const std::map& ids, cons } +std::size_t IrProgram::ReclaimUnusedValues() { + 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) 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); + for (IrValue* value : block->ssaScalarValues) keep(value); + for (IrValue* value : block->ssaVectorValues) keep(value); + for (IrValue* value : block->ssaThreadBitScalarValues) keep(value); + for (IrValue* value : block->ssaScalarMaskTags) keep(value); + } + }; + keepBlocks(blocks); + keepBlocks(resourcePlan.blockStorage); + for (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); + for (IrValue* value : resourcePlan.uniformFill.values) keep(value); + for (const auto& block : metadata.blockInfo) { keep(block.condition); keep(block.indirectTarget); } + for (IrValue* value : metadata.dynamicReads) keep(value); + while (!pending.empty()) { + IrValue* value = pending.back(); + pending.pop_back(); + for (std::size_t index = 0; index < value->ArgumentCount(); ++index) keep(value->Argument(index)); + } + for (const auto& value : values) { + if (isLive(*value)) continue; + for (std::size_t index = 0; index < value->ArgumentCount(); ++index) value->ReplaceArgument(index, nullptr); + } + for (const auto& value : values) { + 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 !isLive(*value); }); + values.shrink_to_fit(); + return removed; +} + std::vector>& IrProgram::Blocks() { return blocks; } @@ -270,16 +322,18 @@ const IrProgramMetadata& IrProgram::Metadata() const { return metadata; } -CompiledShaderInfo IrProgram::TakeCompiledInfo() && { +namespace { + +CompiledShaderInfo compiledInfo(const IrResourcePlan& resourcePlan, std::uint32_t waveSize, ShaderInfo info, IrBindingLayout bindings) { CompiledShaderInfo result; result.stage = resourcePlan.stage; result.shaderHash = resourcePlan.shaderHash; result.waveSize = waveSize; result.userDataBase = resourcePlan.userDataBase; result.userDataCount = resourcePlan.userDataCount; - result.scratchDwords = resourcePlan.info.scratchDwords; - result.info = std::move(resourcePlan.info); - result.bindings = std::move(metadata.bindings); + result.scratchDwords = info.scratchDwords; + result.info = std::move(info); + result.bindings = std::move(bindings); for (const auto& output : result.info.outputs) { if (output.kind == StageOutputKind::Parameter && output.index < 32u) { result.paramExportMask |= 1u << output.index; @@ -288,6 +342,16 @@ CompiledShaderInfo IrProgram::TakeCompiledInfo() && { return result; } +} + +CompiledShaderInfo IrProgram::CompiledInfo() const& { + return compiledInfo(resourcePlan, waveSize, resourcePlan.info, metadata.bindings); +} + +CompiledShaderInfo IrProgram::TakeCompiledInfo() && { + return compiledInfo(resourcePlan, waveSize, std::move(resourcePlan.info), std::move(metadata.bindings)); +} + IrValue& IrProgram::CreateValue(IrOpcode opcode, IrType type, std::uint64_t flags) { IrValue& created = CreateValue(opcode, type); created.SetFlags(flags); diff --git a/core/shader/recompiler/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 120de5358a..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) 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 6be55fb231..758920681c 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ConstantFolder.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ConstantFolder.hpp @@ -14,8 +14,9 @@ public: void Fold(IrProgram& program, std::span blocks) const; -private: - [[nodiscard]] bool tryFoldValue(IrProgram& program, IrValue& value) const; + [[nodiscard]] bool FoldVertexLaneBounds(IrProgram& program, std::uint32_t hostSubgroupSize, std::uint32_t spirvVersion) const; + + [[nodiscard]] bool ComposePackedAncillary(IrProgram& program) const; }; } diff --git a/core/shader/recompiler/Optimization/include/Optimization/DenormalFlushEliminator.hpp b/core/shader/recompiler/Optimization/include/Optimization/DenormalFlushEliminator.hpp new file mode 100644 index 0000000000..a75f0be768 --- /dev/null +++ b/core/shader/recompiler/Optimization/include/Optimization/DenormalFlushEliminator.hpp @@ -0,0 +1,20 @@ +#ifndef CORE_SHADER_RECOMPILIER_OPTIMIZATION_INCLUDE_OPTIMIZATION_DENORMALFLUSHELIMINATOR_HPP +#define CORE_SHADER_RECOMPILIER_OPTIMIZATION_INCLUDE_OPTIMIZATION_DENORMALFLUSHELIMINATOR_HPP + +#include "IntermediateRepresentation/IrProgram.hpp" +#include + +namespace ShaderRecompiler { + +struct DenormalFlushEliminationStats { + std::uint32_t removedFlushes = 0; +}; + +class DenormalFlushEliminator { +public: + [[nodiscard]] DenormalFlushEliminationStats Eliminate(IrProgram& program) const; +}; + +} + +#endif diff --git a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp index 01ece57e4a..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 @@ -14,6 +15,8 @@ struct PreparedDescriptorBinding { std::vector resources; std::vector identityImageSwizzle; std::vector samplerFilterElements; + std::vector inactive; + std::vector tableSamplerMasks; std::size_t writtenBuffers = 0; std::size_t readOnlyBuffers = 0; }; @@ -28,14 +31,15 @@ struct DescriptorBindingPlan { std::vector bindings; std::vector imageMetadata; std::vector specialization; + std::vector runtimeState; bool storageData = false; }; class DescriptorBindingBuilder { public: - DescriptorBindingPlan Prepare(const IrBindingLayout& layout, const ShaderInfo& info, IrShaderStage stage, const ResourceSnapshot& snapshot, std::span exportMappings = {}) const; + 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/HostInterpolationChecker.hpp b/core/shader/recompiler/Optimization/include/Optimization/HostInterpolationChecker.hpp new file mode 100644 index 0000000000..a909f6440d --- /dev/null +++ b/core/shader/recompiler/Optimization/include/Optimization/HostInterpolationChecker.hpp @@ -0,0 +1,16 @@ +#ifndef CORE_SHADER_RECOMPILIER_OPTIMIZATION_INCLUDE_OPTIMIZATION_HOSTINTERPOLATIONCHECKER_HPP +#define CORE_SHADER_RECOMPILIER_OPTIMIZATION_INCLUDE_OPTIMIZATION_HOSTINTERPOLATIONCHECKER_HPP + +#include "IntermediateRepresentation/IrProgram.hpp" +#include "Optimization/ShaderStageInputInfo.hpp" + +namespace ShaderRecompiler { + +class HostInterpolationChecker { +public: + [[nodiscard]] bool Lower(IrProgram& program, const ShaderPixelInputInfo& pixel) const; +}; + +} + +#endif diff --git a/core/shader/recompiler/Optimization/include/Optimization/MaskedSelectEliminator.hpp b/core/shader/recompiler/Optimization/include/Optimization/MaskedSelectEliminator.hpp index fad7eed9c1..e8b873610f 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/MaskedSelectEliminator.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/MaskedSelectEliminator.hpp @@ -3,11 +3,13 @@ #include "IntermediateRepresentation/IrProgram.hpp" #include +#include namespace ShaderRecompiler { struct MaskedSelectEliminationStats { std::uint32_t removedSelects = 0; + std::uint64_t steps = 0; }; class MaskedSelectEliminator { @@ -15,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/RedundantValueEliminator.hpp b/core/shader/recompiler/Optimization/include/Optimization/RedundantValueEliminator.hpp new file mode 100644 index 0000000000..910c3499db --- /dev/null +++ b/core/shader/recompiler/Optimization/include/Optimization/RedundantValueEliminator.hpp @@ -0,0 +1,20 @@ +#ifndef CORE_SHADER_RECOMPILIER_OPTIMIZATION_INCLUDE_OPTIMIZATION_REDUNDANTVALUEELIMINATOR_HPP +#define CORE_SHADER_RECOMPILIER_OPTIMIZATION_INCLUDE_OPTIMIZATION_REDUNDANTVALUEELIMINATOR_HPP + +#include "IntermediateRepresentation/IrProgram.hpp" +#include + +namespace ShaderRecompiler { + +struct RedundantValueEliminationStats { + std::uint32_t replacedValues = 0; +}; + +class RedundantValueEliminator { +public: + [[nodiscard]] RedundantValueEliminationStats Eliminate(IrProgram& program) const; +}; + +} + +#endif diff --git a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp index 5bd14939ee..1425a5ec58 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp @@ -3,27 +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 a0c470a7bf..7067a1fc6c 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp @@ -9,10 +9,27 @@ namespace ShaderRecompiler { +struct PreparedControlFlow; +struct SourceEntry; +class ShaderPreparationContext { +public: + [[nodiscard]] std::shared_ptr AcquireFrontend(const RecompileRequest& request); + [[nodiscard]] std::uint32_t ResourcePrograms() const; + +private: + friend IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparationContext* preparation); + friend struct PreparedInterface; + std::shared_ptr frontend; + std::shared_ptr interfaceSource; + std::unique_ptr interfaceProgram; + std::uint32_t resourcePrograms = 0; +}; + [[nodiscard]] IrProgram PrepareResourceProgram(const RecompileRequest& request); +[[nodiscard]] IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparationContext* preparation); +[[nodiscard]] std::uint64_t FrontEndBuilds(); [[nodiscard]] std::shared_ptr GetResourcePlan(const RecompileRequest& request); -struct SourceEntry; struct ResourceCapture { std::shared_ptr plan; ResourceSnapshot snapshot; @@ -21,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; @@ -55,12 +73,15 @@ private: }; [[nodiscard]] std::span GetPreparedCode(const SourceHandle& handle); [[nodiscard]] std::shared_ptr PrepareShader(const RecompileRequest& request); +[[nodiscard]] std::shared_ptr PrepareShader(const RecompileRequest& request, ShaderPreparationContext* preparation); [[nodiscard]] bool MatchesPreparedShader(const RecompileRequest& request, const SourceHandle& handle); void BuildPreparedShaderKey(const RecompileRequest& request, std::vector& key); [[nodiscard]] bool MatchesPreparedShader(const RecompileRequest& request, const SourceHandle& handle, std::span key); [[nodiscard]] const CompiledShaderArtifact& GetPreparedArtifact(const SourceHandle& handle); +[[nodiscard]] std::shared_ptr 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 996730b67e..84034d4306 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; @@ -109,11 +115,15 @@ struct ShaderMeshInputInfo: ShaderWorkgroupInputInfo { struct ShaderTessellationInputInfo { std::uint32_t inputControlPoints = 0; std::uint32_t outputControlPoints = 0; - std::uint32_t lsStride = 0; - std::uint32_t hsStride = 0; std::uint32_t domain = 0; std::uint32_t partitioning = 0; std::uint32_t outputTopology = 0; + 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 { @@ -129,8 +139,10 @@ struct ShaderVertexInputInfo { int fetchBufferReg = 0; int buffersNum = 0; std::uint32_t scratchSizeDwords = 0; + std::uint32_t hostSubgroupSize = 64; std::uint32_t paClVsOutCntl = 0; ShaderClipSpaceTransform clipSpace; + ShaderDepthRemap depthRemap; ShaderMeshInputInfo mesh; ShaderTessellationInputInfo tess; bool fetchExternal = false; @@ -163,22 +175,56 @@ 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; ConservativeZExport psConservativeZExport = ConservativeZExport::AnyZ; 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; } @@ -191,6 +237,10 @@ struct ShaderPixelInputInfo { return input < 32u && ((customInterpolationMask & (1u << input)) != 0u || InputIsPassthrough(input)); } + [[nodiscard]] bool InputIsFlat(std::uint32_t input) const { + return input < inputNum && input < 32u && (interpolatorSettings[input] & 0x400u) != 0u && !InputIsCustom(input); + } + [[nodiscard]] std::uint32_t InputSlot(std::uint32_t input) const { return input < 32u ? interpolatorSettings[input] & 0x1fu : input; } diff --git a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker.hpp b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker.hpp index 16cdedac5c..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 { @@ -46,7 +101,7 @@ public: void EvaluateUniformValues(const IrResourcePlan& program, std::span values, const SrtRuntime& runtime, std::span results) const; void EvaluateDescriptorSource(const IrResourcePlan& program, std::uint32_t source, const SrtRuntime& runtime, DescriptorValue& result) const; void EvaluateDescriptorSources(const IrResourcePlan& program, std::span sources, const SrtRuntime& runtime, std::vector& results) const; - void EvaluateRuntimeSources(const IrResourcePlan& program, std::span sources, const SrtRuntime& runtime, std::vector& results, std::vector& flat, std::span cleanFlatSlots, std::vector& activeSources, std::vector* poison = nullptr) const; + void EvaluateRuntimeSources(const IrResourcePlan& program, std::span sources, const SrtRuntime& runtime, std::vector& results, std::vector& flat, std::span cleanFlatSlots, std::vector& activeSources, std::vector* poison = nullptr, std::uint32_t* nullRootReads = nullptr) const; void Walk(const IrResourcePlan& program, const SrtRuntime& runtime, std::vector& flat) const; }; diff --git a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtDescriptorEvaluation.hpp b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtDescriptorEvaluation.hpp index eb9c568ade..2b881a4164 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtDescriptorEvaluation.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtDescriptorEvaluation.hpp @@ -15,7 +15,7 @@ namespace ShaderRecompiler::Detail { // Why the last EvaluateRuntimeSourcesImpl call on this thread returned false. const std::string& RuntimeSourceFailureReason(); -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 = nullptr); +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 = nullptr, std::uint32_t* nullRootReads = nullptr); } diff --git a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp index 269bfceb42..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,34 +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) {} - - bool Evaluate(IrValue* value, std::uint32_t& result); - bool EvaluateWide(IrValue* raw, std::uint64_t& result); - void ReportInaccessibleReads(InaccessibleRead* sink) { _inaccessible = sink; } + 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; } 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 new file mode 100644 index 0000000000..7032578c3b --- /dev/null +++ b/core/shader/recompiler/Optimization/include/Optimization/TessellationLowering.hpp @@ -0,0 +1,17 @@ +#ifndef CORE_SHADER_RECOMPILIER_OPTIMIZATION_INCLUDE_OPTIMIZATION_TESSELLATIONLOWERING_HPP +#define CORE_SHADER_RECOMPILIER_OPTIMIZATION_INCLUDE_OPTIMIZATION_TESSELLATIONLOWERING_HPP + +#include "IntermediateRepresentation/IrProgram.hpp" +#include "Optimization/ShaderStageInputInfo.hpp" +#include "Recompiler.hpp" + +namespace ShaderRecompiler { + +class TessellationLowering { +public: + void Lower(IrProgram& program, const ShaderTessellationInputInfo& tessellation, const TessellationTargetLimits* limits = nullptr) const; +}; + +} + +#endif 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 518ac228e9..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) 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" @@ -89,6 +89,9 @@ BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const Bin if (layout.pushConstantOffsetBytes + layout.pushConstantSizeBytes > NativePushConstantSize) { fail("shader binding layout failed: push constant range exceeds the native push constant size"); } + if (layout.pushConstantSizeBytes != 0u && layout.pushConstantOffsetBytes / NativePushSlotSize != (layout.pushConstantOffsetBytes + layout.pushConstantSizeBytes - 1u) / NativePushSlotSize) { + fail("shader binding layout failed: push constant range crosses a stage slot"); + } const ShaderInfo& info = program.Resources().info; @@ -99,9 +102,24 @@ 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 (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; + for (const auto& block : program.Blocks()) { + for (const IrValue* inst : block->Instructions()) { + if (inst->Opcode() == IrOpcode::GetAttribute && inst->Flags() == 1u) attributes = std::max(attributes, inst->Argument(0)->Resolve()->ImmediateU32() + 1u); + } + } + next.runtimeStateDwords = (attributes + RuntimeAbi::VertexSelectorsPerDword - 1u) / RuntimeAbi::VertexSelectorsPerDword; + } 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()); @@ -114,7 +132,7 @@ BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const Bin std::array, ImageBindingCount> imageGroups; const auto place = [&](std::uint32_t i) { const bool dynamic = info.images[i].mipMode == ImageMipMode::DynamicStorage; - const std::uint32_t count = dynamic ? RuntimeAbi::StorageHeapCapacity : 1u; + const std::uint32_t count = dynamic ? RuntimeAbi::StorageMipSlots : 1u; std::array placed{}; for (const auto& mode : ResourceMaterializer::RuntimeImageModes(info.images[i])) { const auto group = ImageBindingIndex(DescriptorBindingForImage(mode)); @@ -123,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++) { @@ -148,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); @@ -166,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); @@ -187,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 ea498bb8ec..1eb0dd3650 100644 --- a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp +++ b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp @@ -1,11 +1,14 @@ #include "Optimization/ConstantFolder.hpp" #include "IntermediateRepresentation/IrBuilder.hpp" +#include "IntermediateRepresentation/IrMetadata/StageIO.hpp" #include #include #include #include #include +#include +#include #include #include @@ -54,6 +57,251 @@ template bool foldU64Shift(IrBuilder& builder, IrValue& ins return true; } +bool foldConstantBitTest(IrBuilder& builder, IrValue& inst) { + auto& left = resolveArg(inst, 0); + auto& right = resolveArg(inst, 1); + if (!isImmediate(right, IrType::U32) || right.ImmediateU32() != 0u || left.Opcode() != IrOpcode::BitwiseAnd32) return false; + auto& first = resolveArg(left, 0); + auto& second = resolveArg(left, 1); + IrValue* shifted = nullptr; + if (isImmediate(second, IrType::U32) && second.ImmediateU32() == 1u) shifted = &first; + else if (isImmediate(first, IrType::U32) && first.ImmediateU32() == 1u) shifted = &second; + if (shifted == nullptr || shifted->Opcode() != IrOpcode::ShiftRightLogical32) return false; + auto& word = resolveArg(*shifted, 0); + auto& amount = resolveArg(*shifted, 1); + if (!isImmediate(word, IrType::U32) || (word.ImmediateU32() != 0u && word.ImmediateU32() != 0xffffffffu)) return false; + if (amount.Opcode() != IrOpcode::BitwiseAnd32) return false; + auto& amountLeft = resolveArg(amount, 0); + auto& amountRight = resolveArg(amount, 1); + const bool masked = (isImmediate(amountLeft, IrType::U32) && amountLeft.ImmediateU32() == 31u) || (isImmediate(amountRight, IrType::U32) && amountRight.ImmediateU32() == 31u); + if (!masked) return false; + replaceWith(inst, builder.ConstantBool(word.ImmediateU32() != 0u)); + return true; +} + +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; +} + +bool runsAllStartingLanes(const IrProgram& program, const IrBlock* target) { + if (program.BlockOrder().empty()) return false; + const IrBlock* block = program.BlockOrder().front(); + for (std::size_t steps = 0; block != nullptr && steps < program.BlockOrder().size(); ++steps) { + if (block == target) return true; + if (block->Successors().size() != 1u) return false; + block = block->Successors().front(); + if (block->Predecessors().size() != 1u) return false; + } + return false; +} + +bool holdsStartingLanes(const IrProgram& program, const IrValue& word, std::uint32_t index, std::uint32_t depth = 0) { + if (depth > 8u) return false; + if (word.Opcode() == IrOpcode::BitwiseAnd32) { + return holdsStartingLanes(program, resolveArg(word, 0), index, depth + 1u) && holdsStartingLanes(program, resolveArg(word, 1), index, depth + 1u); + } + if (word.Opcode() == IrOpcode::BitwiseOr32) { + return holdsStartingLanes(program, resolveArg(word, 0), index, depth + 1u) || holdsStartingLanes(program, resolveArg(word, 1), index, depth + 1u); + } + if (word.Opcode() != IrOpcode::CompositeExtractU32x4 || !isImmediateU32(resolveArg(word, 1), index)) return false; + const auto& ballot = resolveArg(word, 0); + if (ballot.Opcode() != IrOpcode::Ballot || !runsAllStartingLanes(program, ballot.Parent())) return false; + const auto& condition = resolveArg(ballot, 0); + if (isImmediate(condition, IrType::U1)) return condition.ImmediateBool(); + if (condition.Opcode() != IrOpcode::IEqual32) return false; + const auto helper = [](const IrValue& value) { + if (value.Opcode() != IrOpcode::GetBuiltin) return false; + const auto& kind = resolveArg(value, 0); + return isImmediate(kind, IrType::U32) && static_cast(kind.ImmediateU32()) == StageInputKind::HelperInvocation; + }; + const auto& lhs = resolveArg(condition, 0); + const auto& rhs = resolveArg(condition, 1); + return (helper(lhs) && isImmediateU32(rhs, 0u)) || (helper(rhs) && isImmediateU32(lhs, 0u)); +} + +bool foldStartingQuadBitTest(const IrProgram& program, IrBuilder& builder, IrValue& inst) { + if (program.Resources().stage != IrShaderStage::Pixel) return false; + auto& left = resolveArg(inst, 0); + auto& right = resolveArg(inst, 1); + if (!isImmediateU32(right, 0u) || left.Opcode() != IrOpcode::BitwiseAnd32) return false; + IrValue* shifted = isImmediateU32(resolveArg(left, 1), 1u) ? &resolveArg(left, 0) : isImmediateU32(resolveArg(left, 0), 1u) ? &resolveArg(left, 1) : nullptr; + if (shifted == nullptr || shifted->Opcode() != IrOpcode::ShiftRightLogical32) return false; + const auto& amount = resolveArg(*shifted, 1); + if (amount.Opcode() != IrOpcode::BitwiseAnd32) return false; + const bool laneAmount = (isLaneIndex(resolveArg(amount, 0)) && isImmediateU32(resolveArg(amount, 1), 31u)) || (isLaneIndex(resolveArg(amount, 1)) && isImmediateU32(resolveArg(amount, 0), 31u)); + if (!laneAmount) return false; + const auto& word = resolveArg(*shifted, 0); + const IrValue* low = &word; + const IrValue* high = nullptr; + if (word.Opcode() == IrOpcode::SelectU32) { + const auto& half = resolveArg(word, 0); + if (half.Opcode() != IrOpcode::ULessThan32 || !isLaneIndex(resolveArg(half, 0)) || !isImmediateU32(resolveArg(half, 1), 32u)) return false; + low = &resolveArg(word, 1); + high = &resolveArg(word, 2); + } else if (program.WaveSize() != 32u) { + return false; + } + if (low->Opcode() != IrOpcode::CompositeExtractU64 || !isImmediateU32(resolveArg(*low, 1), 0u)) return false; + const auto& wqm = resolveArg(*low, 0); + if (high != nullptr && (high->Opcode() != IrOpcode::CompositeExtractU64 || &resolveArg(*high, 0) != &wqm || !isImmediateU32(resolveArg(*high, 1), 1u))) return false; + if (wqm.Opcode() != IrOpcode::WqmU64) return false; + const auto& lanes = resolveArg(wqm, 0); + if (lanes.Opcode() != IrOpcode::CompositeConstructU64 || !holdsStartingLanes(program, resolveArg(lanes, 0), 0u)) return false; + if (high != nullptr && !holdsStartingLanes(program, resolveArg(lanes, 1), 1u)) return false; + replaceWith(inst, builder.ConstantBool(true)); + return true; +} + template bool foldU32Compare(IrBuilder& builder, IrValue& inst, TFunction function) { auto& lhs = resolveArg(inst, 0); auto& rhs = resolveArg(inst, 1); @@ -64,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); @@ -100,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) { @@ -122,7 +667,7 @@ bool foldPhi(IrValue& inst) { } if (same == nullptr) { same = value; - } else if (!(*same == *value)) { + } else if (same != value && !(same->HasImmediate() && value->HasImmediate() && *same == *value)) { return false; } } @@ -186,16 +731,24 @@ bool foldCompositeExtract(IrBuilder& builder, IrValue& inst, IrOpcode construct, } bool forwardsWord(const IrUse& use) { - return use.user->Opcode() == IrOpcode::Identity || (use.user->Opcode() == IrOpcode::SelectU32 && use.operand != 0u); + 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) { - const std::vector uses = value.OperandUses(); - for (const IrUse& use : uses) { - if (forwardsWord(use)) { - collectReaders(*use.user, readers); - } else { + 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); } } } @@ -212,9 +765,29 @@ std::optional packedField(std::uint32_t offset, std::uint32_t count return static_cast(field - packedFields.begin()); } +bool isPackedAncillary(const IrValue& builtin) { + auto& kind = resolveArg(builtin, 0); + return isImmediate(kind, IrType::U32) && static_cast(kind.ImmediateU32()) == StageInputKind::PackedAncillary; +} + +IrValue& insertBefore(IrProgram& program, IrValue& next, IrOpcode opcode, IrValue& lhs, IrValue& rhs) { + IrValue& created = program.CreateValue(opcode, IrType::U32); + created.AddArgument(&lhs); + created.AddArgument(&rhs); + next.Parent()->InsertInstructionBefore(&next, &created); + return created; +} + +IrValue& insertBuiltin(IrProgram& program, IrBuilder& builder, IrValue& next, StageInputKind kind) { + return insertBefore(program, next, IrOpcode::GetBuiltin, builder.Constant(static_cast(kind)), builder.Constant(0u)); +} + +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) { - auto& kind = resolveArg(ancillary, 0); - if (!isImmediate(kind, IrType::U32) || static_cast(kind.ImmediateU32()) != StageInputKind::PackedAncillary) { + if (!isPackedAncillary(ancillary)) { return; } const std::vector uses = ancillary.OperandUses(); @@ -231,36 +804,42 @@ void lowerPackedAncillary(IrProgram& program, IrBuilder& builder, IrValue& ancil for (const IrUse& use : forwarded) { collectReaders(*use.user, readers); } - for (const IrUse& use : readers) { + const auto extractsField = [](const IrUse& use) { const IrValue& user = *use.user; if ((user.Opcode() != IrOpcode::BitFieldUExtract && user.Opcode() != IrOpcode::BitFieldSExtract) || use.operand != 0u) { - return; + return false; } auto& offset = resolveArg(user, 1); auto& count = resolveArg(user, 2); - if (!isImmediate(offset, IrType::U32) || !isImmediate(count, IrType::U32) || count.ImmediateU32() == 0u || - !packedField(offset.ImmediateU32(), count.ImmediateU32())) { - return; - } + return isImmediate(offset, IrType::U32) && isImmediate(count, IrType::U32) && count.ImmediateU32() != 0u && + packedField(offset.ImmediateU32(), count.ImmediateU32()).has_value(); + }; + const bool whole = std::ranges::all_of(readers, extractsField); + if (!whole) { + std::vector extracts; + const auto collectExtracts = [&](const auto& self, IrValue& value) -> void { + for (const IrUse& use : value.OperandUses()) { + if (use.user->Opcode() == IrOpcode::Identity) { + self(self, *use.user); + } else if (extractsField(use)) { + extracts.push_back(use); + } + } + }; + collectExtracts(collectExtracts, ancillary); + direct = std::move(extracts); + forwarded.clear(); } std::array fields {}; const auto field = [&](std::size_t index) -> IrValue& { + if (fields[index] == nullptr && index == 1u) fields[index] = &insertBefore(program, ancillary, IrOpcode::IMul32, insertBuiltin(program, builder, ancillary, StageInputKind::Layer), insertBuiltin(program, builder, ancillary, StageInputKind::LayerExported)); if (fields[index] == nullptr) { - const auto builtin = index == 0u ? StageInputKind::SampleId : StageInputKind::Layer; - IrValue& created = program.CreateValue(IrOpcode::GetBuiltin, IrType::U32); - created.AddArgument(&builder.Constant(static_cast(builtin))); - created.AddArgument(&builder.Constant(0u)); - ancillary.Parent()->InsertInstructionBefore(&ancillary, &created); - fields[index] = &created; + fields[index] = &sampleNumber(program, builder, ancillary); } return *fields[index]; }; const auto emit = [&](IrOpcode opcode, IrValue& lhs, IrValue& rhs) -> IrValue& { - IrValue& created = program.CreateValue(opcode, IrType::U32); - created.AddArgument(&lhs); - created.AddArgument(&rhs); - ancillary.Parent()->InsertInstructionBefore(&ancillary, &created); - return created; + return insertBefore(program, ancillary, opcode, lhs, rhs); }; if (!forwarded.empty()) { IrValue& sample = emit(IrOpcode::ShiftLeftLogical32, field(0u), builder.Constant(8u)); @@ -280,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); @@ -307,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); @@ -317,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; } @@ -339,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; @@ -352,10 +948,6 @@ bool ConstantFolder::tryFoldValue(IrProgram& program, IrValue& value) const { replaceWith(value, builder.Constant(static_cast(operand.ImmediateU16()))); return true; } - if (operand.Opcode() == IrOpcode::ConvertU16U32) { - replaceWith(value, *operand.Argument(0)); - return true; - } return false; } case IrOpcode::ConvertU8U32: { @@ -372,10 +964,6 @@ bool ConstantFolder::tryFoldValue(IrProgram& program, IrValue& value) const { replaceWith(value, builder.Constant(static_cast(operand.ImmediateU8()))); return true; } - if (operand.Opcode() == IrOpcode::ConvertU8U32) { - replaceWith(value, *operand.Argument(0)); - return true; - } return false; } case IrOpcode::CompositeExtractU64: return foldCompositeExtract(builder, value, IrOpcode::CompositeConstructU64, 2); @@ -484,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)); @@ -500,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: { @@ -534,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; })) { @@ -548,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); @@ -578,6 +1192,19 @@ bool ConstantFolder::tryFoldValue(IrProgram& program, IrValue& value) const { replaceWith(value, builder.Constant(static_cast(std::popcount(operand.ImmediateU64())))); return true; } + case IrOpcode::BitReverse32: { + auto& operand = resolveArg(value, 0); + if (!isImmediate(operand, IrType::U32)) { + return false; + } + const std::uint32_t word = operand.ImmediateU32(); + std::uint32_t reversed = 0; + for (std::uint32_t bit = 0; bit < 32; ++bit) { + reversed |= ((word >> bit) & 1u) << (31u - bit); + } + replaceWith(value, builder.Constant(reversed)); + return true; + } case IrOpcode::SMin32: case IrOpcode::SMax32: { const auto opcode = value.Opcode(); @@ -593,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 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; }); @@ -686,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; @@ -710,4 +1357,41 @@ 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()) { + for (IrValue* inst : block->Instructions()) { + if (inst->Opcode() == IrOpcode::GetBuiltin && inst->HasUses() && isPackedAncillary(*inst)) { + words.push_back(inst); + } + } + } + IrBuilder builder(program); + for (IrValue* word : words) { + IrValue& primitive = insertBuiltin(program, builder, *word, StageInputKind::RasterPrimitive); + IrValue& sample = insertBefore(program, *word, IrOpcode::ShiftLeftLogical32, 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); + word->ReplaceAllUsesWith(&insertBefore(program, *word, IrOpcode::BitwiseOr32, primitive, fields)); + } + return !words.empty(); +} + } diff --git a/core/shader/recompiler/Optimization/src/DenormalFlushEliminator.cpp b/core/shader/recompiler/Optimization/src/DenormalFlushEliminator.cpp new file mode 100644 index 0000000000..f8a013e4b7 --- /dev/null +++ b/core/shader/recompiler/Optimization/src/DenormalFlushEliminator.cpp @@ -0,0 +1,163 @@ +#include "Optimization/DenormalFlushEliminator.hpp" +#include +#include + +namespace ShaderRecompiler { + +namespace { + +bool isImmediate(const IrValue* value, std::uint32_t bits) { + return value->HasImmediate() && value->ImmediateU32() == bits; +} + +const IrValue* maskedOperand(const IrValue* value, IrOpcode opcode, std::uint32_t bits) { + if (value->Opcode() != opcode || value->ArgumentCount() != 2u) return nullptr; + const IrValue* lhs = value->Argument(0)->Resolve(); + const IrValue* rhs = value->Argument(1)->Resolve(); + if (isImmediate(rhs, bits)) return lhs; + if (isImmediate(lhs, bits)) return rhs; + 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(); + const IrValue* exponent = maskedOperand(value.Argument(0)->Resolve(), IrOpcode::IEqual32, 0u); + if (exponent == nullptr || maskedOperand(exponent, IrOpcode::BitwiseAnd32, 0x7f800000u) != source) return nullptr; + return maskedOperand(value.Argument(1)->Resolve(), IrOpcode::BitwiseAnd32, 0x80000000u) == source ? source : nullptr; +} + +class DenormalFreeValues { +public: + bool Contains(const IrValue* value) { + value = value->Resolve(); + if (const auto found = known.find(value); found != known.end()) return found->second; + known.emplace(value, false); + const bool result = compute(*value); + known[value] = result; + return result; + } + +private: + bool compute(const IrValue& value) { + if (value.HasImmediate()) { + const std::uint32_t bits = value.ImmediateU32(); + return (bits & 0x7f800000u) != 0u || (bits & 0x7fffffffu) == 0u; + } + if (flushedSource(value) != nullptr) return true; + switch (value.Opcode()) { + case IrOpcode::ConvertIToF32: + case IrOpcode::ConvertUToF32: + case IrOpcode::ConvertF32S32: + case IrOpcode::ConvertF32U32: + case IrOpcode::ConvertF32F16: + case IrOpcode::FPFloor32: + case IrOpcode::FPCeil32: + return true; + case IrOpcode::BitCastU32F32: + case IrOpcode::BitCastF32U32: + case IrOpcode::FPAbs32: + case IrOpcode::FPNeg32: + case IrOpcode::FPSaturate32: + return Contains(value.Argument(0)); + case IrOpcode::BitwiseXor32: + if (const IrValue* operand = maskedOperand(&value, IrOpcode::BitwiseXor32, 0x80000000u)) return Contains(operand); + return false; + case IrOpcode::BitwiseAnd32: + if (const IrValue* operand = maskedOperand(&value, IrOpcode::BitwiseAnd32, 0x7fffffffu)) return Contains(operand); + return value.ArgumentCount() == 2u && clearsSignificand(value); + case IrOpcode::SelectU32: + 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: + case IrOpcode::FPMaxTri32: + case IrOpcode::FPMedTri32: + case IrOpcode::Phi: + for (std::size_t index = 0; index < value.ArgumentCount(); ++index) { + if (!Contains(value.Argument(index))) return false; + } + return true; + default: + return false; + } + } + + std::unordered_map known; +}; + +} + +DenormalFlushEliminationStats DenormalFlushEliminator::Eliminate(IrProgram& program) const { + DenormalFlushEliminationStats stats; + DenormalFreeValues denormalFree; + for (auto* block : program.BlockOrder()) { + for (auto* inst : block->Instructions()) { + if (!inst->HasUses()) continue; + IrValue* source = flushedSource(*inst); + if (source == nullptr || !denormalFree.Contains(source)) continue; + inst->ReplaceAllUsesWith(source); + ++stats.removedFlushes; + } + } + return stats; +} + +} diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index 5a7a24e54d..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; @@ -150,7 +170,6 @@ const char* UnnormalizedUseReason(std::uint32_t uses) { switch (uses & (~uses + 1u)) { case SamplerUseImplicitLod: return "is used by an implicit-LOD sample"; case SamplerUseGradient: return "is used by a sample with derivatives"; - case SamplerUseOffset: return "is used with a texel offset"; case SamplerUseCompare: return "is used with depth comparison"; case SamplerUseGather: return "is used by a gather"; case SamplerUseQueryLod: return "is used by image_get_lod"; @@ -161,52 +180,82 @@ const char* UnnormalizedUseReason(std::uint32_t uses) { 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())}; + 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"); + } continue; } - const auto& sampler = info.samplers[r]; - const std::uint32_t unsupported = sampler.uses & ~static_cast(SamplerUseExplicitLod); - if (unsupported != 0u) { + const std::uint32_t unsupported = sampler.uses & ~static_cast(SamplerUseExplicitLod | SamplerUseOffset | SamplerUseNonZeroLod); + if (unsupported != 0u && !SamplesBaseLevelIfUnnormalized(sampler)) { failUnnormalized(UnnormalizedUseReason(unsupported)); } 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); - const auto& image = info.runtimeImageModes.at(pair.image).at(ResourceMaterializer::RuntimeImageMode(base, snapshot.images.at(pair.image), info.runtimeImageModes.at(pair.image))); - if (image.indirectRoot != ImageResource::NoIndirectImage) { + if (base.table != NoTable) { failUnnormalized("samples an image selected at run time"); } - if (image.constantSwizzle) { + const auto& image = info.runtimeImageModes.at(pair.image).at(ResourceMaterializer::RuntimeImageMode(base, snapshot.images.at(pair.image), info.runtimeImageModes.at(pair.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 ((image.dimension != RdnaImageDimension::Dim1D && image.dimension != RdnaImageDimension::Dim2D) || image.cube) { - failUnnormalized("samples a 1D-array, 2D-array, 3D, cube or multisampled image"); - } - if (image.depthCompare) { - failUnnormalized("is used with depth comparison"); - } - if (image.conversionFormat != IrBufferFormat::Invalid || image.packed) { - failUnnormalized("samples an image that needs a format conversion or packed access"); + 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++) { + bool exact = proof.images[resource]; + for (const auto& pair : info.sampledPairs) { + if (!exact) break; + if (pair.image != resource) continue; + const auto& words = snapshot.samplers.at(pair.sampler).dwords; + 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; + } return proof; } -std::vector ShaderDataDwordsFor(const IrBindingLayout& layout, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads, const std::vector& imageMetadata) { +std::vector ShaderDataDwordsFor(const IrBindingLayout& layout, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads, const std::vector& imageMetadata, std::span runtimeState) { std::vector result(layout.ShaderDataDwords(), 0u); for (std::size_t i = 0; i < layout.userDataRegisters.size(); ++i) { const auto reg = layout.userDataRegisters[i]; @@ -224,14 +273,19 @@ std::vector ShaderDataDwordsFor(const IrBindingLayout& layout, st std::copy_n(descriptor.dwords.begin(), metadata.descriptor.size(), metadata.descriptor.begin()); std::memcpy(result.data() + entry.offset, &metadata, sizeof(metadata)); } + if (runtimeState.size() != layout.runtimeStateDwords) fail("runtime state does not match its shader data layout"); + std::copy(runtimeState.begin(), runtimeState.end(), result.begin() + layout.RuntimeStateDword()); return result; } } +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; @@ -244,25 +298,33 @@ 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); } -DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& layout, const ShaderInfo& info, IrShaderStage stage, const ResourceSnapshot& snapshot, std::span exportMappings) const { +DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& layout, const ShaderInfo& info, IrShaderStage stage, const ResourceSnapshot& snapshot, std::span exportMappings, bool registrationModules, bool fragmentDescriptorSet) const { if (stage == IrShaderStage::Pixel && exportMappings.size() != 8u) fail("fragment runtime export mappings are missing"); if (info.images.size() > ShaderInfo::MaxImages) fail("runtime image count exceeds the static capacity"); DescriptorBindingPlan plan; const auto unnormalized = ProveUnnormalized(info, snapshot); + for (std::uint32_t index = 0; index < info.samplers.size(); ++index) { + if (FoldsTexelOffsets(info.samplers[index])) plan.specialization.push_back({PipelineSpecialization::FoldOffsetBase + index, unnormalized.samplers[index] ? 1u : 0u}); + if (SamplesBaseLevelIfUnnormalized(info.samplers[index])) plan.specialization.push_back({PipelineSpecialization::UnnormalizedBaseLevelBase + index, unnormalized.samplers[index] ? 1u : 0u}); + } std::vector compareStates(info.images.size()); for (std::uint32_t index = 0; index < info.images.size(); ++index) { - 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(); ++word) plan.specialization.push_back({first + word, values[word]}); + 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"); @@ -271,15 +333,18 @@ 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"); - const auto first = PipelineSpecialization::BufferBase + index * PipelineSpecialization::BufferWords; - plan.specialization.push_back({first, descriptor.dwords[1] & 0xffff0000u}); - plan.specialization.push_back({first + 1u, descriptor.dwords[3]}); - plan.specialization.push_back({first + 2u, present ? 1u : 0u}); - plan.specialization.push_back({first + 3u, misalignment}); + if (registrationModules && buffer.descriptorFormatted) plan.specialization.push_back({PipelineSpecialization::BufferBase + index * PipelineSpecialization::BufferWords + 1u, descriptor.dwords[3]}); + if (registrationModules) plan.specialization.push_back({PipelineSpecialization::BufferBase + index * PipelineSpecialization::BufferWords + 3u, misalignment}); + if (!registrationModules) { + const auto first = PipelineSpecialization::BufferBase + index * PipelineSpecialization::BufferWords; + plan.specialization.push_back({first, descriptor.dwords[1] & 0xffff0000u}); + plan.specialization.push_back({first + 1u, descriptor.dwords[3]}); + plan.specialization.push_back({first + 2u, present ? 1u : 0u}); + plan.specialization.push_back({first + 3u, misalignment}); + } if (buffer.formattedReadMask != 0u) { for (std::uint32_t component = 0; component < 4u; ++component) { if ((buffer.formattedReadMask & (1u << component)) == 0u) continue; @@ -288,23 +353,49 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l } } } - for (std::uint32_t index = 0; index < info.images.size(); ++index) { - if (info.images[index].indirectRoot != ImageResource::NoIndirectImage) continue; + for (std::uint32_t index = 0; index < info.images.size() && !registrationModules; ++index) { + 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(); ++target) { + 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}); } std::vector samplerModes(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 samplerIntegerModes(info.samplers.size(), 0u); + std::vector samplerFloatModes(info.samplers.size(), 0u); + 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; + (image.numericClass == IrTextureNumericClass::Uint || image.numericClass == IrTextureNumericClass::Sint ? samplerIntegerModes : samplerFloatModes).at(sampler) |= mode; + }; for (const auto& pair : info.sampledPairs) { const auto& image = info.images.at(pair.image); - if (image.indirectRoot == ImageResource::NoIndirectImage) samplerModes.at(pair.sampler) |= samplerMode(info.runtimeImageModes.at(pair.image).at(imageModes[pair.image])); - else { - const auto& root = info.images.at(image.indirectRoot); - for (const auto slot : root.indirectResources) samplerModes.at(pair.sampler) |= samplerMode(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) { @@ -312,21 +403,8 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l const auto word = snapshot.images.at(index).dwords[3]; const auto first = (word >> 12u) & 0xfu; const auto last = (word >> 16u) & 0xfu; - if (last < first || last - first >= RuntimeAbi::StorageHeapCapacity) fail("invalid dynamic storage mip range"); - 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(); ++mode) plan.specialization.push_back({PipelineSpecialization::ImageModeBase + resource * PipelineSpecialization::ImageModeStride + mode, active[mode] ? 1u : 0u}); + 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}); } plan.bindings.reserve(layout.descriptors.size()); for (const auto& logical : layout.descriptors) { @@ -334,6 +412,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l const bool samplerHeap = logical.kind == DescriptorBindingKind::Samplers; IrDescriptorBinding compact{logical.kind, {}}; std::vector originals; + std::vector inactive; std::uint32_t previous = UINT32_MAX; std::uint32_t mip = 0; for (std::uint32_t element = 0; element < logical.resources.size(); ++element) { @@ -341,27 +420,29 @@ 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; } - if (imageHeap || samplerHeap) plan.specialization.push_back({PipelineSpecialization::DescriptorIndex(static_cast(logical.kind), element), active ? static_cast(compact.resources.size()) : 0u}); - if (active) { + if ((imageHeap || samplerHeap) && !registrationModules) plan.specialization.push_back({PipelineSpecialization::DescriptorIndex(static_cast(logical.kind), element), active ? static_cast(compact.resources.size()) : 0u}); + if (active || ((imageHeap || samplerHeap) && registrationModules)) { compact.resources.push_back(resource); originals.push_back(element); + inactive.push_back(!active); } } - if (imageHeap || samplerHeap) plan.specialization.push_back({PipelineSpecialization::HeapCountBase + static_cast(logical.kind), std::max(1u, static_cast(compact.resources.size()))}); + if ((imageHeap || samplerHeap) && !registrationModules) plan.specialization.push_back({PipelineSpecialization::HeapCountBase + static_cast(logical.kind), std::max(1u, static_cast(compact.resources.size()))}); if ((imageHeap || samplerHeap) && compact.resources.empty()) continue; PreparedDescriptorBinding entry{}; auto& physical = entry.descriptor; - physical.descriptorSet = RuntimeAbi::DescriptorSet; + physical.descriptorSet = fragmentDescriptorSet && stage == IrShaderStage::Pixel ? RuntimeAbi::FragmentDescriptorSet : RuntimeAbi::DescriptorSet; physical.binding = NativeBinding(stage, compact.kind); physical.count = compact.resources.empty() ? 1u : static_cast(compact.resources.size()); physical.kind = PhysicalKindFor(compact.kind); @@ -371,6 +452,8 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l const auto& buffer = info.buffers.at(resource); 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; } @@ -388,20 +471,9 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l physical.imageDepthCompare.push_back(mode.depthCompare); physical.imageAtomic.push_back(image.atomic); physical.imageUnnormalized.push_back(unnormalized.images.at(resource)); + 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) 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; - 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) { @@ -410,30 +482,57 @@ 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)); - if ((originals[element] & 1u) != 0u) entry.samplerFilterElements.push_back(element); + 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); + 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"); } plan.storageData |= physical.role == DescriptorRole::ShaderData; entry.resources = std::move(compact.resources); + if (registrationModules && (imageHeap || samplerHeap)) entry.inactive = std::move(inactive); 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].table != NoTable) 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.elementCount = 1u; + if (imageModes[resource] > RuntimeAbi::ImageModeMask) fail("runtime image mode exceeds its metadata field"); + 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; + plan.imageMetadata.push_back({resource, layout.ImageMetadataDword() + resource * static_cast(sizeof(metadata) / sizeof(std::uint32_t)), metadata}); + } + if (layout.runtimeStateDwords != 0u) { + plan.runtimeState.assign(layout.runtimeStateDwords, 0u); + for (std::uint32_t target = 0; target < exportMappings.size(); ++target) plan.runtimeState.at(RuntimeAbi::StateExportMappings + target / 4u) |= static_cast(exportMappings[target]) << (target % 4u * 8u); + } std::vector samplerMasks(info.samplers.size()); for (const auto& binding : plan.bindings) { if (binding.descriptor.role != DescriptorRole::GuestSamplers) continue; if (binding.resources.size() > ShaderInfo::MaxSamplers) fail("prepared sampler mask capacity exceeded"); - for (std::uint32_t element = 0; element < binding.resources.size(); ++element) samplerMasks.at(binding.resources[element]) |= 1u << element; + for (std::uint32_t element = 0; element < binding.resources.size(); ++element) { + if (binding.inactive.empty() || !binding.inactive[element]) samplerMasks.at(binding.resources[element]) |= 1u << element; + } } for (auto& binding : plan.bindings) { if (binding.descriptor.role != DescriptorRole::GuestImages) continue; 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; } @@ -451,16 +550,22 @@ 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; + selected.runtimeState = plan.runtimeState; 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) : std::vector{}; + 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; @@ -478,6 +583,10 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con for (std::size_t element = 0; element < entry.resources.size(); ++element) { const auto& descriptor = snapshot.images.at(entry.resources[element]); if (descriptor.dwordCount != 8u) fail("invalid runtime image descriptor width"); + if (!entry.inactive.empty() && entry.inactive[element]) { + physical.guestDescriptor.insert(physical.guestDescriptor.end(), 8u, 0u); + continue; + } const auto offset = physical.guestDescriptor.size(); physical.guestDescriptor.insert(physical.guestDescriptor.end(), descriptor.dwords.begin(), descriptor.dwords.begin() + 8); if (entry.identityImageSwizzle[element]) physical.guestDescriptor[offset + 3u] = (physical.guestDescriptor[offset + 3u] & ~0xfffu) | ShaderImageIdentitySwizzle; @@ -490,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 new file mode 100644 index 0000000000..db4de9830a --- /dev/null +++ b/core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp @@ -0,0 +1,141 @@ +#include "Optimization/HostInterpolationChecker.hpp" +#include "IntermediateRepresentation/IrBlock.hpp" +#include "IntermediateRepresentation/IrMetadata/StageIO.hpp" +#include "Optimization/DeadCodeEliminator.hpp" +#include "Optimization/MaskedSelectEliminator.hpp" +#include +#include + +namespace ShaderRecompiler { +namespace { + +bool isCopy(const IrValue& value) { + return value.IsIdentity() || value.Opcode() == IrOpcode::BitCastF32U32 || value.Opcode() == IrOpcode::BitCastU32F32; +} + +const IrValue* lookThrough(const IrValue* value) { + value = value->Resolve(); + while (value->Opcode() == IrOpcode::BitCastF32U32 || value->Opcode() == IrOpcode::BitCastU32F32) value = value->Argument(0)->Resolve(); + return value; +} + +const IrValue* partialOf(const IrValue& interpolation) { + const auto* exec = interpolation.Argument(4)->Resolve(); + const auto* value = lookThrough(interpolation.Argument(3)); + while (value->Opcode() == IrOpcode::SelectU32) { + if (value->Argument(0)->Resolve() != exec) return value; + value = lookThrough(value->Argument(1)); + } + return value; +} + +bool isBarycentric(const IrValue& value) { + if (value.Opcode() != IrOpcode::GetBuiltin) return false; + return IsBarycentricInput(static_cast(value.Argument(0)->Resolve()->ImmediateU32())); +} + +bool isBarycentric(const IrValue& value, StageInputKind kind, std::uint32_t component) { + return isBarycentric(value) && static_cast(value.Argument(0)->Resolve()->ImmediateU32()) == kind && value.Argument(1)->Resolve()->ImmediateU32() == component; +} + +template +bool usesAll(const IrValue& value, const TAccept& accept) { + for (const auto& use : value.OperandUses()) { + if (isCopy(*use.user)) { + if (!usesAll(*use.user, accept)) return false; + } else if (!accept(*use.user, use.operand)) { + return false; + } + } + return true; +} + +bool sameCondition(const IrValue& left, const IrValue& right) { + return left.Argument(0)->Resolve() == right.Argument(0)->Resolve(); +} + +bool partialSelect(const IrValue& select) { + return usesAll(select, [&](const IrValue& user, std::size_t operand) { + return (user.Opcode() == IrOpcode::InterpolateHostP2 && operand == 3u) || (user.Opcode() == IrOpcode::SelectU32 && operand == 2u && sameCondition(user, select)); + }); +} + +bool sameParameter(const IrValue& left, const IrValue& right) { + return left.Argument(0)->Resolve()->ImmediateU32() == right.Argument(0)->Resolve()->ImmediateU32() && left.Argument(1)->Resolve()->ImmediateU32() == right.Argument(1)->Resolve()->ImmediateU32(); +} + +bool exact(const IrValue& inst, const IrProgram& program, const ShaderPixelInputInfo& pixel, std::vector& selects) { + if (inst.Opcode() == IrOpcode::InterpolateHostP1) { + return usesAll(inst, [&](const IrValue& user, std::size_t operand) { + if (user.Opcode() == IrOpcode::InterpolateHostP2 && operand == 3u) return true; + if (user.Opcode() != IrOpcode::SelectU32 || operand != 1u || !partialSelect(user)) return false; + selects.push_back(const_cast(&user)); + return true; + }); + } + if (inst.Opcode() == IrOpcode::GetInterpolationParameter) { + const auto input = inst.Argument(0)->Resolve()->ImmediateU32(); + return pixel.InputIsDefault(input) || (inst.Argument(2)->Resolve()->ImmediateU32() == 2u && pixel.InputIsFlat(input)); + } + if (inst.Opcode() != IrOpcode::InterpolateHostP2) return true; + const auto* partial = partialOf(inst); + if (partial->Opcode() != IrOpcode::InterpolateHostP1 || !sameParameter(inst, *partial)) return false; + const auto& metadata = program.Metadata(); + const auto kind = pixel.InputIsLinear(inst.Argument(0)->Resolve()->ImmediateU32(), metadata.pixelLinearInputs, metadata.pixelPerspectiveInputs) ? StageInputKind::BaryCoordNoPerspective : StageInputKind::BaryCoordSmooth; + return isBarycentric(*lookThrough(partial->Argument(2)), kind, 0u) && isBarycentric(*lookThrough(inst.Argument(2)), kind, 1u); +} + +bool sharesSlots(const IrProgram& program, const ShaderPixelInputInfo& pixel) { + constexpr std::uint32_t unassigned = 3u; + std::array interpolation {}; + interpolation.fill(unassigned); + const auto& metadata = program.Metadata(); + for (const auto& block : program.Blocks()) { + for (const IrValue* inst : block->Instructions()) { + if (inst->Opcode() != IrOpcode::GetAttribute && inst->Opcode() != IrOpcode::GetInterpolationParameter) continue; + const auto input = inst->Argument(0)->Resolve()->ImmediateU32(); + if (input >= pixel.inputNum || pixel.InputIsDefault(input)) continue; + const auto mode = pixel.InputIsFlat(input) ? 2u : pixel.InputIsLinear(input, metadata.pixelLinearInputs, metadata.pixelPerspectiveInputs) ? 1u : 0u; + auto& slot = interpolation[pixel.InputSlot(input)]; + if (slot != unassigned && slot != mode) return true; + slot = mode; + } + } + return false; +} + +} + +bool HostInterpolationChecker::Lower(IrProgram& program, const ShaderPixelInputInfo& pixel) const { + std::vector selects; + for (const auto& block : program.Blocks()) { + for (const IrValue* inst : block->Instructions()) { + if (!exact(*inst, program, pixel, selects)) return false; + } + } + if (sharesSlots(program, pixel)) return false; + for (auto* select : selects) select->ReplaceAllUsesWith(select->Argument(2)); + for (const auto& block : program.Blocks()) { + auto& instructions = block->Instructions(); + for (auto it = instructions.begin(); it != instructions.end();) { + IrValue* inst = *it; + if (inst->Opcode() != IrOpcode::InterpolateHostP2) { + ++it; + continue; + } + inst->Invalidate(); + inst->SetParent(nullptr); + it = instructions.erase(it); + } + } + DeadCodeEliminator{}.Eliminate(program); + if (MaskedSelectEliminator{}.Eliminate(program).removedSelects != 0u) DeadCodeEliminator{}.Eliminate(program); + for (const auto& block : program.Blocks()) { + for (const IrValue* inst : block->Instructions()) { + if (isBarycentric(*inst) || inst->Opcode() == IrOpcode::InterpolateHostP1) return false; + } + } + return true; +} + +} diff --git a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp index 74f4cda6f7..a88fa3e288 100644 --- a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp +++ b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp @@ -1,6 +1,13 @@ #include "Optimization/MaskedSelectEliminator.hpp" #include "Optimization/DeadCodeEliminator.hpp" #include "RdnaDecoder/RdnaInstruction.hpp" +#include +#include +#include +#include +#include +#include +#include #include #include #include @@ -10,7 +17,6 @@ namespace ShaderRecompiler { namespace { -constexpr std::size_t VisitLimit = 4096; constexpr std::uint32_t DepthLimit = 8; bool isSelect(IrOpcode opcode) { @@ -58,6 +64,8 @@ bool isLaneLocal(IrOpcode opcode) { case IrOpcode::CompositeExtractU32x4: case IrOpcode::PackHalf2x16: case IrOpcode::PackFloat2x16Rtz: + case IrOpcode::FPAdd16x2: + case IrOpcode::FPMul16x2: case IrOpcode::BitFieldInsert: case IrOpcode::BitFieldUExtract: case IrOpcode::BitFieldSExtract: @@ -169,6 +177,7 @@ bool isLaneLocal(IrOpcode opcode) { case IrOpcode::FPFrexpMant64: case IrOpcode::FPFrexpExp64: case IrOpcode::MakeImageAddress: + case IrOpcode::ImageQueryDimensions: return true; default: return false; @@ -202,6 +211,8 @@ bool readsOnlyWhereActive(IrOpcode opcode) { case IrOpcode::WriteSharedU32x3: case IrOpcode::WriteSharedU32x4: case IrOpcode::SetAttribute: + case IrOpcode::ImageRead: + case IrOpcode::ImageWrite: return true; default: return false; @@ -209,7 +220,7 @@ bool readsOnlyWhereActive(IrOpcode opcode) { } bool isExplicitLodSample(const IrProgram& program, const IrValue& value) { - if (value.Opcode() != IrOpcode::ImageSampleRaw) return false; + if (value.Opcode() != IrOpcode::ImageSampleRaw && value.Opcode() != IrOpcode::ImageGatherRaw) return false; if (program.Resources().stage != IrShaderStage::Pixel) return true; const auto index = value.Flags().index; if (index >= program.Resources().memoryInfo.size()) return false; @@ -232,6 +243,7 @@ const IrValue* otherArgument(const IrValue* value, std::uint32_t immediate) { struct ThreadBit { const IrValue* low = nullptr; const IrValue* high = nullptr; + const IrValue* word = nullptr; }; bool isLane(const IrValue* value) { @@ -250,11 +262,11 @@ std::optional threadBit(const IrValue* value, std::uint32_t waveSize) const IrValue* lane = otherArgument(amount, 31u); if (lane == nullptr || !isLane(lane)) return std::nullopt; const IrValue* word = shifted->Argument(0)->Resolve(); - if (waveSize == 32u || word->HasImmediate()) return ThreadBit{word, word}; + if (waveSize == 32u || word->HasImmediate()) return ThreadBit{word, word, word}; if (word->Opcode() != IrOpcode::SelectU32) return std::nullopt; const IrValue* half = word->Argument(0)->Resolve(); if (half->Opcode() != IrOpcode::ULessThan32 || !isLane(half->Argument(0)) || !isImmediate(half->Argument(1)->Resolve(), 32u)) return std::nullopt; - return ThreadBit{word->Argument(1)->Resolve(), word->Argument(2)->Resolve()}; + return ThreadBit{word->Argument(1)->Resolve(), word->Argument(2)->Resolve(), word}; } bool alwaysTrue(const IrValue* value, std::uint32_t waveSize, std::uint32_t depth = 0) { @@ -266,38 +278,71 @@ bool alwaysTrue(const IrValue* value, std::uint32_t waveSize, std::uint32_t dept return bit && isImmediate(bit->low, 0xffffffffu) && isImmediate(bit->high, 0xffffffffu); } -bool implies(const IrValue* condition, const IrValue* mask, std::uint32_t waveSize, std::uint32_t depth = 0); +using BlockSet = std::unordered_set; -bool wordsImply(const IrValue* low, const IrValue* high, const IrValue* mask, std::uint32_t waveSize, std::uint32_t depth) { +BlockSet uniformEntryBlocks(const IrProgram& program) { + BlockSet result; + if (program.BlockOrder().empty()) return result; + const IrBlock* block = program.BlockOrder().front(); + while (block->Predecessors().empty() || (block->Predecessors().size() == 1u && result.contains(block->Predecessors().front()))) { + if (!result.insert(block).second || block->Successors().size() != 1u) break; + block = block->Successors().front(); + } + return result; +} + +bool sameCondition(const IrValue* condition, const IrValue* predicate, std::uint32_t depth) { + condition = condition->Resolve(); + predicate = predicate->Resolve(); + if (condition == predicate || (predicate->HasImmediate() && predicate->Type() == IrType::Bool && predicate->ImmediateBool())) return true; if (depth > DepthLimit) return false; - if (isImmediate(low, 0u) && (waveSize == 32u || isImmediate(high, 0u))) return true; - if (low->Opcode() == IrOpcode::CompositeExtractU32x4 && isImmediate(low->Argument(1)->Resolve(), 0u)) { - const IrValue* ballot = low->Argument(0)->Resolve(); - const bool sameBallot = waveSize == 32u || (high->Opcode() == IrOpcode::CompositeExtractU32x4 && high->Argument(0)->Resolve() == ballot && isImmediate(high->Argument(1)->Resolve(), 1u)); - if (ballot->Opcode() == IrOpcode::Ballot && sameBallot && implies(ballot->Argument(0), mask, waveSize, depth + 1u)) return true; + return condition->Opcode() == IrOpcode::LogicalAnd && (sameCondition(condition->Argument(0), predicate, depth + 1u) || sameCondition(condition->Argument(1), predicate, depth + 1u)); +} + +bool withinBallotWord(const IrValue* condition, const IrValue* word, std::uint32_t half, const BlockSet& uniform, std::uint32_t depth) { + if (word->Opcode() != IrOpcode::CompositeExtractU32x4 || !isImmediate(word->Argument(1)->Resolve(), half)) return false; + const IrValue* ballot = word->Argument(0)->Resolve(); + return ballot->Opcode() == IrOpcode::Ballot && uniform.contains(ballot->Parent()) && sameCondition(condition, ballot->Argument(0), depth + 1u); +} + +bool withinWords(const IrValue* condition, const IrValue* low, const IrValue* high, std::uint32_t waveSize, const BlockSet& uniform, std::uint32_t depth) { + if (depth > DepthLimit) return false; + if (low->Opcode() == IrOpcode::CompositeExtractU32x4) { + return withinBallotWord(condition, low, 0u, uniform, depth) && (waveSize == 32u || withinBallotWord(condition, high, 1u, uniform, depth)); } if (low->Opcode() != IrOpcode::BitwiseAnd32) return false; - if (waveSize == 32u) return wordsImply(low->Argument(0)->Resolve(), high, mask, waveSize, depth + 1u) || wordsImply(low->Argument(1)->Resolve(), high, mask, waveSize, depth + 1u); + const IrValue* lowLeft = low->Argument(0)->Resolve(); + const IrValue* lowRight = low->Argument(1)->Resolve(); + if (waveSize == 32u) return withinWords(condition, lowLeft, high, waveSize, uniform, depth + 1u) && withinWords(condition, lowRight, high, waveSize, uniform, depth + 1u); if (high->Opcode() != IrOpcode::BitwiseAnd32) return false; - for (std::size_t lowIndex = 0; lowIndex < 2u; ++lowIndex) { - for (std::size_t highIndex = 0; highIndex < 2u; ++highIndex) { - if (wordsImply(low->Argument(lowIndex)->Resolve(), high->Argument(highIndex)->Resolve(), mask, waveSize, depth + 1u)) return true; - } - } - return false; + const IrValue* highLeft = high->Argument(0)->Resolve(); + const IrValue* highRight = high->Argument(1)->Resolve(); + return (withinWords(condition, lowLeft, highLeft, waveSize, uniform, depth + 1u) && withinWords(condition, lowRight, highRight, waveSize, uniform, depth + 1u)) || + (withinWords(condition, lowLeft, highRight, waveSize, uniform, depth + 1u) && withinWords(condition, lowRight, highLeft, waveSize, uniform, depth + 1u)); } -bool implies(const IrValue* condition, const IrValue* mask, std::uint32_t waveSize, std::uint32_t depth) { - condition = condition->Resolve(); - if (condition == mask || alwaysTrue(mask, waveSize)) return true; - if (condition->HasImmediate()) return condition->Type() == IrType::Bool && !condition->ImmediateBool(); - if (depth > DepthLimit) return false; - if (condition->Opcode() == IrOpcode::LogicalAnd) return implies(condition->Argument(0), mask, waveSize, depth + 1u) || implies(condition->Argument(1), mask, waveSize, depth + 1u); - const auto bit = threadBit(condition, waveSize); - return bit && wordsImply(bit->low, bit->high, mask, waveSize, depth + 1u); +const IrValue* wholeQuadSource(const IrValue* mask, std::uint32_t waveSize) { + const auto bit = threadBit(mask, waveSize); + if (!bit || bit->low->Opcode() != IrOpcode::CompositeExtractU64 || !isImmediate(bit->low->Argument(1)->Resolve(), 0u)) return nullptr; + const IrValue* wqm = bit->low->Argument(0)->Resolve(); + if (waveSize != 32u && (bit->high->Opcode() != IrOpcode::CompositeExtractU64 || bit->high->Argument(0)->Resolve() != wqm || !isImmediate(bit->high->Argument(1)->Resolve(), 1u))) return nullptr; + if (wqm->Opcode() != IrOpcode::WqmU64) return nullptr; + const IrValue* source = wqm->Argument(0)->Resolve(); + return source->Opcode() == IrOpcode::CompositeConstructU64 ? source : nullptr; } -std::unordered_set blocksOutsideLoops(const IrProgram& program) { +enum class Region : std::uint8_t { Unreached, Once, Loop }; + +Region regionOf(const std::vector& regions, const IrBlock* block) { + return block != nullptr && block->Id() < regions.size() ? regions[block->Id()] : Region::Unreached; +} + +bool followable(const std::vector& regions, const IrBlock* block, bool acrossLoops) { + const Region region = regionOf(regions, block); + return region == Region::Once || (acrossLoops && region == Region::Loop); +} + +std::vector blockRegions(const IrProgram& program) { const auto& order = program.BlockOrder(); if (order.empty()) return {}; std::unordered_map state; @@ -330,65 +375,893 @@ std::unordered_set blocksOutsideLoops(const IrProgram& program) looping.insert(body.begin(), body.end()); } } - std::unordered_set result; + std::vector result; for (const auto& [block, visited] : state) { - if (visited == 2 && !looping.contains(block)) result.insert(block); + if (visited != 2) continue; + if (block->Id() >= result.size()) result.resize(block->Id() + 1u, Region::Unreached); + result[block->Id()] = looping.contains(block) ? Region::Loop : Region::Once; } return result; } -bool unobservedWhereMasked(const IrProgram& program, const std::unordered_set& once, const IrValue& select, const IrValue* mask) { - const auto waveSize = program.WaveSize(); - std::vector pending{&select}; - std::unordered_set visited{&select}; - while (!pending.empty()) { - const IrValue* value = pending.back(); - pending.pop_back(); - for (const IrUse& use : value->OperandUses()) { - const IrValue* user = use.user; - if (user->Parent() == nullptr || !once.contains(user->Parent())) return false; - if (isSelect(user->Opcode()) && use.operand == 1u && implies(user->Argument(0), mask, waveSize)) continue; - if (user->Opcode() == IrOpcode::LogicalAnd && use.operand < 2u && implies(user->Argument(1u - use.operand), mask, waveSize)) continue; - if (readsOnlyWhereActive(user->Opcode()) && use.operand + 1u < user->ArgumentCount() && implies(user->Argument(user->ArgumentCount() - 1u), mask, waveSize)) continue; - if (!user->IsPhi() && !isLaneLocal(user->Opcode()) && !isExplicitLodSample(program, *user)) return false; - if (visited.insert(user).second) { - if (visited.size() > VisitLimit) return false; - pending.push_back(user); +using MaskSet = std::uint32_t; + +class MaskSets { +public: + static constexpr MaskSet None = 0; + static constexpr MaskSet All = 1; + + explicit MaskSets(std::uint64_t& steps) : steps(steps), sets(2) {} + + MaskSet Single(std::uint32_t mask) { + sets.push_back({mask}); + return static_cast(sets.size() - 1u); + } + + MaskSet Of(std::vector&& masks) { + if (masks.empty()) return None; + std::sort(masks.begin(), masks.end()); + sets.push_back(std::move(masks)); + return static_cast(sets.size() - 1u); + } + + MaskSet Union(MaskSet left, MaskSet right) { + if (left == right || right == None) return left; + if (left == None) return right; + if (left == All || right == All) return All; + std::vector merged; + merged.reserve(sets[left].size() + sets[right].size()); + std::set_union(sets[left].begin(), sets[left].end(), sets[right].begin(), sets[right].end(), std::back_inserter(merged)); + return keep(left, right, std::move(merged)); + } + + MaskSet Intersection(MaskSet left, MaskSet right) { + if (left == right || right == All) return left; + if (left == All) return right; + if (left == None || right == None) return None; + std::vector common; + std::set_intersection(sets[left].begin(), sets[left].end(), sets[right].begin(), sets[right].end(), std::back_inserter(common)); + return keep(left, right, std::move(common)); + } + + [[nodiscard]] bool Contains(MaskSet set, std::uint32_t mask) const { + return set == All || (set != None && std::binary_search(sets[set].begin(), sets[set].end(), mask)); + } + + [[nodiscard]] bool Same(MaskSet left, MaskSet right) const { + return left == right || (left > All && right > All && sets[left] == sets[right]); + } + +private: + MaskSet keep(MaskSet left, MaskSet right, std::vector&& result) { + steps += sets[left].size() + sets[right].size(); + if (result.empty()) return None; + if (result.size() == sets[left].size()) return left; + if (result.size() == sets[right].size()) return right; + sets.push_back(std::move(result)); + return static_cast(sets.size() - 1u); + } + + std::uint64_t& steps; + std::vector> sets; +}; + +class Components { +public: + template + void From(std::uint32_t root, TSuccessors&& successors, TVisit&& visit) { + if (seen(root)) return; + open(root, successors); + while (!frames.empty()) { + Frame& frame = frames.back(); + if (frame.next != frame.end) { + const std::uint32_t successor = edges[frame.next++]; + if (successor == frame.node) frame.cyclic = true; + if (!seen(successor)) { + open(successor, successors); + } else if (low[successor] != Closed) { + low[frame.node] = std::min(low[frame.node], order[successor]); + } + continue; + } + const std::uint32_t node = frame.node; + const bool cyclic = frame.cyclic; + edges.resize(frame.begin); + frames.pop_back(); + if (!frames.empty()) low[frames.back().node] = std::min(low[frames.back().node], low[node]); + if (low[node] != order[node]) continue; + members.clear(); + do { + members.push_back(pending.back()); + pending.pop_back(); + low[members.back()] = Closed; + } while (members.back() != node); + visit(members, cyclic || members.size() != 1u); + } + } + +private: + static constexpr std::uint32_t Closed = 0xffffffffu; + + struct Frame { + std::uint32_t node = 0; + std::size_t begin = 0; + std::size_t next = 0; + std::size_t end = 0; + bool cyclic = false; + }; + + [[nodiscard]] bool seen(std::uint32_t node) const { + return node < order.size() && order[node] != 0u; + } + + template + void open(std::uint32_t node, TSuccessors& successors) { + if (node >= order.size()) { + order.resize(node + 1u, 0u); + low.resize(node + 1u, 0u); + } + order[node] = low[node] = ++opened; + pending.push_back(node); + const std::size_t begin = edges.size(); + successors(node, edges); + frames.push_back(Frame{node, begin, begin, edges.size(), false}); + } + + std::vector order; + std::vector low; + std::vector pending; + std::vector edges; + std::vector members; + std::vector frames; + std::uint32_t opened = 0; +}; + +struct MaskFamily { + bool acrossLoops = false; + bool wholeQuads = false; + + bool operator==(const MaskFamily&) const = default; +}; + +enum class Implication : std::uint8_t { Condition, Words, SelectedTrue, SelectedFalse }; + +struct ImplicationKey { + const IrValue* first = nullptr; + const IrValue* second = nullptr; + Implication kind = Implication::Condition; + + bool operator==(const ImplicationKey&) const = default; +}; + +struct ImplicationKeyHash { + std::size_t operator()(const ImplicationKey& key) const { + return std::hash{}(key.first) * 31u ^ std::hash{}(key.second); + } +}; + +class ImpliedMasks { +public: + ImpliedMasks(const std::vector& regions, const BlockSet& uniform, std::uint32_t waveSize, MaskFamily family, MaskSets& sets, std::uint64_t& steps) : regions(regions), uniform(uniform), waveSize(waveSize), family(family), sets(sets), steps(steps) {} + + void AddMask(const IrValue* mask) { + if (!mark(masks, mask)) return; + const auto bit = threadBit(mask, waveSize); + if (bit) { + mark(followed, bit->word); + sameWords[wordsKey(bit->low, bit->high)].masks.push_back(mask->Id()); + } + if (!family.wholeQuads) return; + mark(followed, mask); + const IrValue* source = wholeQuadSource(mask, waveSize); + const auto known = std::find_if(quads.begin(), quads.end(), [&](const Quads& entry) { return entry.source == source; }); + if (known == quads.end()) { + quads.push_back(Quads{source, sets.Single(mask->Id())}); + } else { + known->masks = sets.Union(known->masks, sets.Single(mask->Id())); + } + } + + MaskSet Of(const IrValue* guard) { + const std::uint32_t root = condition(guard); + components.From(root, [this](std::uint32_t node, std::vector& out) { expand(node, out); }, [this](const std::vector& members, bool cyclic) { settle(members, cyclic); }); + return nodes[root].implied; + } + + [[nodiscard]] bool Follows(const IrValue& value) const { + return value.Id() < followed.size() && followed[value.Id()] != 0u; + } + +private: + struct Node { + ImplicationKey key; + MaskSet own = MaskSets::None; + MaskSet implied = MaskSets::All; + bool every = false; + bool queued = false; + std::uint32_t group = 0; + std::vector inputs; + std::vector outputs; + }; + + struct Quads { + const IrValue* source = nullptr; + MaskSet masks = MaskSets::None; + }; + + struct SameWords { + std::vector masks; + MaskSet set = MaskSets::None; + }; + + static bool mark(std::vector& marks, const IrValue* value) { + if (value->Id() >= marks.size()) marks.resize(value->Id() + 1u, 0u); + return std::exchange(marks[value->Id()], std::uint8_t{1}) == 0u; + } + + std::uint32_t add(const ImplicationKey& key) { + nodes.push_back(Node{key}); + mark(followed, key.first); + if (key.second != nullptr) mark(followed, key.second); + return static_cast(nodes.size() - 1u); + } + + [[nodiscard]] ImplicationKey wordsKey(const IrValue* low, const IrValue* high) const { + return ImplicationKey{low, waveSize == 32u ? low : high, Implication::Words}; + } + + std::uint32_t words(const IrValue* low, const IrValue* high) { + const ImplicationKey key = wordsKey(low, high); + const auto known = wordNodes.find(key); + return known != wordNodes.end() ? known->second : wordNodes.emplace(key, add(key)).first->second; + } + + std::uint32_t condition(const IrValue* value) { + value = value->Resolve(); + if (value->Id() >= conditionNodes.size()) conditionNodes.resize(value->Id() + 1u, Unknown); + if (conditionNodes[value->Id()] == Unknown) conditionNodes[value->Id()] = add(ImplicationKey{value}); + return conditionNodes[value->Id()]; + } + + bool merges(const IrValue& phi) const { + if (!followable(regions, phi.Parent(), family.acrossLoops) || phi.ArgumentCount() != phi.PhiBlockCount()) return false; + for (std::size_t index = 0; index < phi.ArgumentCount(); ++index) { + const IrValue* incoming = phi.Argument(index)->Resolve(); + if (!incoming->HasImmediate() && !followable(regions, incoming->Parent(), family.acrossLoops)) return false; + } + return true; + } + + 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; + } + + void expand(std::uint32_t node, std::vector& out) { + const ImplicationKey key = nodes[node].key; + Node shape{key}; + if (key.kind == Implication::Condition) { + expandCondition(key.first, shape); + } else if (key.kind == Implication::Words) { + expandWords(key.first, key.second, shape); + } else { + expandSelected(key.first, key.kind == Implication::SelectedTrue, shape); + } + out.insert(out.end(), shape.inputs.begin(), shape.inputs.end()); + for (const std::uint32_t input : shape.inputs) nodes[input].outputs.push_back(node); + shape.outputs = std::move(nodes[node].outputs); + nodes[node] = std::move(shape); + } + + void expandCondition(const IrValue* value, Node& shape) { + if (value->Id() < masks.size() && masks[value->Id()] != 0u && !threadBit(value, waveSize)) shape.own = sets.Single(value->Id()); + if (value->HasImmediate()) { + if (value->Type() == IrType::Bool && !value->ImmediateBool()) shape.own = MaskSets::All; + return; + } + for (const Quads& entry : quads) { + ++steps; + if (withinWords(value, entry.source->Argument(0)->Resolve(), entry.source->Argument(1)->Resolve(), waveSize, uniform, 0u)) shape.own = sets.Union(shape.own, entry.masks); + } + if (value->Opcode() == IrOpcode::LogicalAnd) { + shape.inputs = {condition(value->Argument(0)), condition(value->Argument(1))}; + } else if (value->Opcode() == IrOpcode::SelectU1) { + shape.every = true; + shape.inputs = {add(ImplicationKey{value, nullptr, Implication::SelectedTrue}), add(ImplicationKey{value, nullptr, Implication::SelectedFalse})}; + } else if (value->IsPhi()) { + if (!merges(*value)) return; + shape.every = true; + for (std::size_t index = 0; index < value->ArgumentCount(); ++index) shape.inputs.push_back(condition(value->Argument(index))); + } else if (const auto bit = threadBit(value, waveSize)) { + mark(followed, bit->word); + shape.inputs = {words(bit->low, bit->high)}; + } + } + + void expandSelected(const IrValue* select, bool whenTrue, Node& shape) { + const IrValue* selector = select->Argument(0)->Resolve(); + shape.inputs = {condition(select->Argument(whenTrue ? 1u : 2u))}; + if (whenTrue) { + shape.inputs.push_back(condition(selector)); + } else if (selector->Opcode() == IrOpcode::LogicalNot) { + shape.inputs.push_back(condition(selector->Argument(0))); + } + } + + void expandWords(const IrValue* low, const IrValue* high, Node& shape) { + if (isImmediate(low, 0u) && (waveSize == 32u || isImmediate(high, 0u))) { + shape.own = MaskSets::All; + return; + } + if (const auto same = sameWords.find(wordsKey(low, high)); same != sameWords.end()) { + if (same->second.set == MaskSets::None) same->second.set = sets.Of(std::move(same->second.masks)); + shape.own = same->second.set; + } + if (low->Opcode() == IrOpcode::CompositeExtractU32x4 && isImmediate(low->Argument(1)->Resolve(), 0u)) { + const IrValue* ballot = low->Argument(0)->Resolve(); + const bool sameBallot = waveSize == 32u || (high->Opcode() == IrOpcode::CompositeExtractU32x4 && high->Argument(0)->Resolve() == ballot && isImmediate(high->Argument(1)->Resolve(), 1u)); + if (ballot->Opcode() == IrOpcode::Ballot && sameBallot) shape.inputs = {condition(ballot->Argument(0))}; + } else if (low->Opcode() == IrOpcode::BitwiseAnd32) { + if (waveSize == 32u) { + shape.inputs = {words(low->Argument(0)->Resolve(), high), words(low->Argument(1)->Resolve(), high)}; + } else if (high->Opcode() == IrOpcode::BitwiseAnd32) { + for (std::size_t lowIndex = 0; lowIndex < 2u; ++lowIndex) { + for (std::size_t highIndex = 0; highIndex < 2u; ++highIndex) shape.inputs.push_back(words(low->Argument(lowIndex)->Resolve(), high->Argument(highIndex)->Resolve())); + } + } + } else if (low->IsPhi() && merges(*low) && (waveSize == 32u || (high->IsPhi() && high->Parent() == low->Parent() && merges(*high)))) { + for (std::size_t index = 0; index < low->PhiBlockCount(); ++index) { + const IrValue* highIncoming = waveSize == 32u ? high : incomingFrom(*high, low->PhiBlock(index)); + if (highIncoming == nullptr) { + shape.inputs.clear(); + return; + } + shape.inputs.push_back(words(low->Argument(index)->Resolve(), highIncoming)); + } + shape.every = true; + } + } + + MaskSet evaluate(const Node& node) { + MaskSet result = node.every ? MaskSets::All : MaskSets::None; + for (const std::uint32_t input : node.inputs) { + ++steps; + result = node.every ? sets.Intersection(result, nodes[input].implied) : sets.Union(result, nodes[input].implied); + } + return sets.Union(result, node.own); + } + + void settle(const std::vector& members, bool cyclic) { + if (!cyclic) { + nodes[members.front()].implied = evaluate(nodes[members.front()]); + return; + } + ++groups; + queue.assign(members.rbegin(), members.rend()); + for (const std::uint32_t member : members) { + nodes[member].queued = true; + nodes[member].group = groups; + } + while (!queue.empty()) { + const std::uint32_t member = queue.back(); + queue.pop_back(); + nodes[member].queued = false; + const MaskSet result = evaluate(nodes[member]); + if (sets.Same(result, nodes[member].implied)) continue; + nodes[member].implied = result; + for (const std::uint32_t output : nodes[member].outputs) { + if (nodes[output].group == groups && !std::exchange(nodes[output].queued, true)) queue.push_back(output); } } } - return true; + + static constexpr std::uint32_t Unknown = 0xffffffffu; + + const std::vector& regions; + const BlockSet& uniform; + std::uint32_t waveSize; + MaskFamily family; + MaskSets& sets; + std::uint64_t& steps; + std::vector masks; + std::vector followed; + std::vector quads; + std::vector nodes; + std::vector conditionNodes; + std::unordered_map wordNodes; + std::unordered_map sameWords; + std::vector queue; + std::uint32_t groups = 0; + Components components; +}; + +MaskFamily familyOf(const std::vector& regions, const IrValue* mask, std::uint32_t waveSize) { + 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>; + +class UnreadLanes { +public: + UnreadLanes(IrProgram& program, const std::vector& regions, const BlockSet& uniform, MaskFamily family, std::uint64_t& steps, FirstLaneReads* shared = nullptr) + : program(program), regions(regions), uniform(uniform), family(family), sampledByQuad(family.wholeQuads && program.Resources().stage == IrShaderStage::Pixel), firstLane(shared != nullptr), sameBlock(firstLane && !family.acrossLoops), steps(steps), sets(steps), implied(regions, uniform, program.WaveSize(), family, sets, steps), firstLanes(firstLane ? *shared : owned) { + if (!firstLane) return; + for (const IrBlock* block : program.BlockOrder()) { + for (const IrValue* inst : block->Instructions()) { + if (inst->Opcode() != IrOpcode::ReadFirstLane) continue; + const IrValue* mask = inst->Argument(1)->Resolve(); + if (familyOf(regions, mask, program.WaveSize()) == family) implied.AddMask(mask); + } + } + } + + bool UnreadOutsideItsMask(IrValue& read) { + if (solving) return false; + solving = true; + static_cast(readerOf(read)); + components.From(read.Id(), [this](std::uint32_t node, std::vector& out) { expand(node, out); }, [this](const std::vector& members, bool cyclic) { settle(members, cyclic); }); + solving = false; + return sets.Contains(unread[read.Id()], read.Argument(1)->Resolve()->Id()); + } + + [[nodiscard]] bool Follows(const IrValue& value) const { + return implied.Follows(value) || (value.Id() < readers.size() && readers[value.Id()] != Reader::Unknown); + } + + std::uint32_t Remove() { + std::vector order; + for (auto blockIt = program.BlockOrder().rbegin(); blockIt != program.BlockOrder().rend(); ++blockIt) { + const bool single = regionOf(regions, *blockIt) == Region::Once; + const auto& instructions = (*blockIt)->Instructions(); + for (auto it = instructions.rbegin(); it != instructions.rend(); ++it) { + IrValue* inst = *it; + order.push_back(inst); + if (!single || !isSelect(inst->Opcode()) || inst->ArgumentCount() != 3u || !inst->HasUses()) continue; + const IrValue* mask = inst->Argument(0)->Resolve(); + if (familyOf(regions, mask, program.WaveSize()) != family) continue; + implied.AddMask(mask); + slot(writes, inst->Id(), static_cast(nullptr)) = mask; + found = true; + } + } + if (!found) return 0; + for (IrValue* inst : order) { + if (halted) break; + static_cast(readerOf(*inst)); + components.From(inst->Id(), [this](std::uint32_t node, std::vector& out) { expand(node, out); }, [this](const std::vector& members, bool cyclic) { settle(members, cyclic); }); + } + if (removed != 0u) { + for (IrBlock* block : program.BlockOrder()) std::erase_if(block->Instructions(), [](const IrValue* inst) { return inst->Parent() == nullptr; }); + } + return removed; + } + +private: + static constexpr std::uint32_t Queued = 0xffffffffu; + + template + static TValue& slot(std::vector& table, std::uint32_t id, TValue fill) { + if (id >= table.size()) table.resize(id + 1u, fill); + return table[id]; + } + + enum class Reader : std::uint8_t { Unknown, Outside, Reads, Passes }; + + Reader readerOf(IrValue& user) { + Reader& known = slot(readers, user.Id(), Reader::Unknown); + if (known != Reader::Unknown) return known; + slot(values, user.Id(), static_cast(nullptr)) = &user; + slot(unread, user.Id(), MaskSets::All); + slot(groups, user.Id(), std::uint32_t{0}); + if (sameBlock ? user.IsPhi() : !followable(regions, user.Parent(), family.acrossLoops)) return known = Reader::Outside; + const bool passes = user.IsPhi() || isLaneLocal(user.Opcode()) || isExplicitLodSample(program, user) || (sampledByQuad && user.Opcode() == IrOpcode::ImageSampleRaw); + return known = passes ? Reader::Passes : Reader::Reads; + } + + [[nodiscard]] bool leaves(const IrValue& value, const IrValue& user) const { + return sameBlock && user.Parent() != value.Parent(); + } + + [[nodiscard]] bool readsFirstLane(const IrUse& use) const { + return use.user->Opcode() == IrOpcode::ReadFirstLane && use.operand == 0u; + } + + [[nodiscard]] bool solvedHere(const IrValue& read) const { + return firstLane && familyOf(regions, read.Argument(1)->Resolve(), program.WaveSize()) == family; + } + + bool unreadOutsideItsMask(IrValue& read) { + const IrValue* mask = read.Argument(1)->Resolve(); + if (solvedHere(read)) return sets.Contains(unread[read.Id()], mask->Id()); + const MaskFamily readFamily = familyOf(regions, mask, program.WaveSize()); + auto& reads = firstLanes[(readFamily.acrossLoops ? 2u : 0u) + (readFamily.wholeQuads ? 1u : 0u)]; + if (!reads) reads = std::make_unique(program, regions, uniform, readFamily, steps, &firstLanes); + return reads->UnreadOutsideItsMask(read); + } + + void expand(std::uint32_t node, std::vector& out) { + for (const IrUse& use : values[node]->OperandUses()) { + const Reader reader = readerOf(*use.user); + if (reader == Reader::Outside || leaves(*values[node], *use.user)) continue; + if (reader == Reader::Passes || (readsFirstLane(use) && solvedHere(*use.user))) out.push_back(use.user->Id()); + } + } + + MaskSet unreadBy(const IrValue& value, const IrUse& use) { + IrValue* user = use.user; + const Reader reader = readerOf(*user); + if (reader == Reader::Outside || leaves(value, *user)) return MaskSets::None; + MaskSet guarded = MaskSets::None; + if (isSelect(user->Opcode()) && use.operand == 1u) { + guarded = implied.Of(user->Argument(0)); + } else if (user->Opcode() == IrOpcode::LogicalAnd && use.operand < 2u) { + guarded = implied.Of(user->Argument(1u - use.operand)); + } else if (readsOnlyWhereActive(user->Opcode()) && use.operand + 1u < user->ArgumentCount()) { + guarded = implied.Of(user->Argument(user->ArgumentCount() - 1u)); + } else if (readsFirstLane(use)) { + guarded = implied.Of(user->Argument(1)); + if (guarded != MaskSets::None && !unreadOutsideItsMask(*user)) guarded = MaskSets::None; + } + if (guarded == MaskSets::All || reader != Reader::Passes) return guarded; + return sets.Union(guarded, unread[user->Id()]); + } + + MaskSet evaluate(std::uint32_t node) { + MaskSet result = MaskSets::All; + for (const IrUse& use : values[node]->OperandUses()) { + if (result == MaskSets::None) break; + ++steps; + result = sets.Intersection(result, unreadBy(*values[node], use)); + } + return result; + } + + void settle(const std::vector& members, bool cyclic) { + if (halted) return; + if (!cyclic) { + unread[members.front()] = evaluate(members.front()); + remove(members.front()); + return; + } + ++group; + queue.assign(members.rbegin(), members.rend()); + for (const std::uint32_t member : members) groups[member] = Queued; + while (!queue.empty()) { + const std::uint32_t member = queue.back(); + queue.pop_back(); + groups[member] = group; + const MaskSet result = evaluate(member); + if (sets.Same(result, unread[member])) continue; + unread[member] = result; + for (const IrValue* argument : values[member]->Arguments()) { + if (argument == nullptr || argument->Id() >= groups.size() || groups[argument->Id()] != group) continue; + groups[argument->Id()] = Queued; + queue.push_back(argument->Id()); + } + } + } + + void remove(std::uint32_t node) { + IrValue* select = values[node]; + const IrValue* mask = node < writes.size() ? writes[node] : nullptr; + if (mask == nullptr || !select->HasUses() || select->Argument(0)->Resolve() != mask || !sets.Contains(unread[node], mask->Id())) return; + halted = implied.Follows(*select) || std::any_of(owned.begin(), owned.end(), [&](const auto& reads) { return reads && reads->Follows(*select); }); + select->ReplaceAllUsesWith(select->Argument(1)); + select->Invalidate(); + select->SetParent(nullptr); + ++removed; + } + + IrProgram& program; + const std::vector& regions; + const BlockSet& uniform; + MaskFamily family; + bool sampledByQuad; + bool firstLane; + bool sameBlock; + std::uint64_t& steps; + MaskSets sets; + ImpliedMasks implied; + FirstLaneReads owned; + FirstLaneReads& firstLanes; + Components components; + std::vector values; + std::vector writes; + std::vector unread; + std::vector readers; + std::vector groups; + std::vector queue; + std::uint32_t group = 0; + std::uint32_t removed = 0; + bool found = false; + bool halted = false; + bool solving = false; +}; + +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) 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); + it = instructions.erase(it); + ++removed; + } + } + 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 once = blocksOutsideLoops(program); - bool changed = true; - while (changed) { - changed = false; - for (auto blockIt = program.BlockOrder().rbegin(); blockIt != program.BlockOrder().rend(); ++blockIt) { - const bool single = once.contains(*blockIt); - 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()) continue; - const IrValue* mask = inst->Argument(0)->Resolve(); - if (!alwaysTrue(mask, program.WaveSize()) && (!single || !unobservedWhereMasked(program, once, *inst, mask))) continue; - inst->ReplaceAllUsesWith(inst->Argument(1)); - inst->Invalidate(); - inst->SetParent(nullptr); - it = instructions.erase(it); - ++stats.removedSelects; - changed = true; - } + const auto regions = blockRegions(program); + const BlockSet uniform = uniformEntryBlocks(program); + std::vector selects; + std::uint32_t removed = 1; + while (removed != 0u) { + removed = removeFullWaveSelects(program, regions, selects); + for (const MaskFamily family : {MaskFamily{true, false}, MaskFamily{true, true}, MaskFamily{false, false}, MaskFamily{false, true}}) { + if (hasUnreadCandidate(selects, regions, family, program.WaveSize())) removed += UnreadLanes(program, regions, uniform, family, stats.steps).Remove(); } - if (changed) DeadCodeEliminator{}.Eliminate(program); + stats.removedSelects += removed; + if (removed != 0u) DeadCodeEliminator{}.Eliminate(program); } 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 new file mode 100644 index 0000000000..6e8c4e269d --- /dev/null +++ b/core/shader/recompiler/Optimization/src/RedundantValueEliminator.cpp @@ -0,0 +1,283 @@ +#include "Optimization/RedundantValueEliminator.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ShaderRecompiler { + +namespace { + +bool Recomputable(IrOpcode opcode) { + switch (opcode) { + case IrOpcode::BitCastU16F16: + case IrOpcode::BitCastF16U16: + case IrOpcode::BitCastU32F32: + case IrOpcode::BitCastF32U32: + case IrOpcode::ConvertU16U32: + case IrOpcode::ConvertU32U16: + case IrOpcode::ConvertU8U32: + case IrOpcode::ConvertU32U8: + case IrOpcode::ConvertF32F16: + case IrOpcode::ConvertF16F32: + case IrOpcode::ConvertS32F32: + case IrOpcode::ConvertU32F32: + case IrOpcode::ConvertF32S32: + case IrOpcode::ConvertF32U32: + case IrOpcode::CompositeConstructU64: + case IrOpcode::CompositeConstructU32x2: + case IrOpcode::CompositeConstructU32x3: + case IrOpcode::CompositeConstructF32x2: + case IrOpcode::CompositeConstructU32x4: + case IrOpcode::CompositeExtractU64: + case IrOpcode::CompositeExtractU32x2: + case IrOpcode::CompositeExtractU32x3: + case IrOpcode::CompositeExtractU32x4: + case IrOpcode::FPAbs32: + case IrOpcode::FPNeg32: + case IrOpcode::FPSaturate32: + case IrOpcode::BitFieldInsert: + case IrOpcode::BitFieldUExtract: + case IrOpcode::BitFieldSExtract: + case IrOpcode::SelectU1: + case IrOpcode::SelectF32: + case IrOpcode::SelectU32: + case IrOpcode::IAdd32: + case IrOpcode::ISub32: + case IrOpcode::IMul32: + case IrOpcode::IAdd64: + case IrOpcode::ISub64: + case IrOpcode::IMul64: + case IrOpcode::SMulHi: + case IrOpcode::UMulHi: + case IrOpcode::IAbs32: + case IrOpcode::ShiftLeftLogical32: + case IrOpcode::ShiftLeftLogical64: + case IrOpcode::ShiftRightLogical32: + case IrOpcode::ShiftRightLogical64: + case IrOpcode::ShiftRightArithmetic32: + case IrOpcode::ShiftRightArithmetic64: + case IrOpcode::BitwiseAnd32: + case IrOpcode::BitwiseAnd64: + case IrOpcode::BitwiseOr32: + case IrOpcode::BitwiseXor32: + case IrOpcode::BitwiseNot32: + case IrOpcode::BitReverse32: + case IrOpcode::BitCount32: + case IrOpcode::BitCount64: + case IrOpcode::FindUMsb32: + case IrOpcode::FindUMsb64: + case IrOpcode::FindILsb32: + case IrOpcode::SMin32: + case IrOpcode::UMin32: + case IrOpcode::SMax32: + case IrOpcode::UMax32: + case IrOpcode::SLessThan32: + case IrOpcode::SLessThan64: + case IrOpcode::ULessThan32: + case IrOpcode::ULessThan64: + case IrOpcode::IEqual32: + case IrOpcode::IEqual64: + case IrOpcode::SLessThanEqual32: + case IrOpcode::ULessThanEqual32: + case IrOpcode::SGreaterThan32: + case IrOpcode::UGreaterThan32: + case IrOpcode::UGreaterThan64: + case IrOpcode::INotEqual32: + case IrOpcode::INotEqual64: + case IrOpcode::SGreaterThanEqual32: + case IrOpcode::UGreaterThanEqual32: + case IrOpcode::LogicalOr: + case IrOpcode::LogicalAnd: + case IrOpcode::LogicalXor: + case IrOpcode::LogicalNot: + case IrOpcode::FPOrdEqual32: + case IrOpcode::FPUnordEqual32: + case IrOpcode::FPOrdNotEqual32: + case IrOpcode::FPUnordNotEqual32: + case IrOpcode::FPOrdLessThan32: + case IrOpcode::FPUnordLessThan32: + case IrOpcode::FPOrdGreaterThan32: + case IrOpcode::FPUnordGreaterThan32: + case IrOpcode::FPOrdLessThanEqual32: + case IrOpcode::FPUnordLessThanEqual32: + case IrOpcode::FPOrdGreaterThanEqual32: + case IrOpcode::FPUnordGreaterThanEqual32: + case IrOpcode::FPIsNan32: + case IrOpcode::FPAdd32: + case IrOpcode::FPSub32: + case IrOpcode::FPFma32: + case IrOpcode::FPMad32: + case IrOpcode::FPMul32: + case IrOpcode::FPMin32: + case IrOpcode::FPMax32: + case IrOpcode::FPFloor32: + case IrOpcode::FPCeil32: + case IrOpcode::FPTrunc32: + case IrOpcode::FPFract32: + case IrOpcode::FPRoundEven32: + return true; + default: + return false; + } +} + +struct Operand { + const IrValue* value; + IrType type; + std::uint64_t immediate; + + bool operator==(const Operand& other) const = default; +}; + +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::array operands{}; + std::size_t operandCount = 0; + + 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* 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; + } +}; + +std::vector ReversePostOrder(IrBlock& entry) { + std::vector order; + std::unordered_map visited; + std::vector> stack{{&entry, 0u}}; + visited[&entry] = true; + while (!stack.empty()) { + auto& [block, next] = stack.back(); + if (next < block->Successors().size()) { + IrBlock* successor = block->Successors()[next++]; + if (visited.try_emplace(successor, true).second) stack.emplace_back(successor, 0u); + continue; + } + order.push_back(block); + stack.pop_back(); + } + return {order.rbegin(), order.rend()}; +} + +std::vector> DominatorChildren(const std::vector& order, std::unordered_map& index) { + for (std::size_t position = 0; position < order.size(); ++position) index[order[position]] = position; + constexpr std::size_t none = static_cast(-1); + std::vector dominator(order.size(), none); + dominator[0] = 0; + const auto intersect = [&](std::size_t left, std::size_t right) { + while (left != right) { + while (left > right) left = dominator[left]; + while (right > left) right = dominator[right]; + } + return left; + }; + for (bool changed = true; changed;) { + changed = false; + for (std::size_t position = 1; position < order.size(); ++position) { + std::size_t candidate = none; + for (const IrBlock* predecessor : order[position]->Predecessors()) { + const auto found = index.find(predecessor); + if (found == index.end() || dominator[found->second] == none) continue; + candidate = candidate == none ? found->second : intersect(candidate, found->second); + } + if (candidate != dominator[position]) { + dominator[position] = candidate; + changed = true; + } + } + } + std::vector> children(order.size()); + for (std::size_t position = 1; position < order.size(); ++position) children[dominator[position]].push_back(order[position]); + return children; +} + +} + +RedundantValueEliminationStats RedundantValueEliminator::Eliminate(IrProgram& program) const { + RedundantValueEliminationStats stats; + std::unordered_map available; + std::vector added; + std::unordered_map replacements; + const auto visit = [&](IrBlock& block) { + for (auto* inst : block.Instructions()) { + if (!Recomputable(inst->Opcode()) || inst->HasImmediate() || !inst->HasUses()) continue; + 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); + continue; + } + inst->ReplaceAllUsesWith(found->second); + replacements.emplace(inst, found->second); + ++stats.replacedValues; + } + }; + if (program.BlockOrder().empty()) return stats; + const auto order = ReversePostOrder(*program.BlockOrder().front()); + std::unordered_map index; + const auto children = DominatorChildren(order, index); + for (const auto* block : program.BlockOrder()) { + if (!index.contains(block)) throw std::runtime_error("redundant value elimination: block " + std::to_string(block->Id()) + " is unreachable from the entry block"); + } + std::vector> stack{{0u, 0u}}; + std::vector scopes{added.size()}; + visit(*order[0]); + while (!stack.empty()) { + auto& [position, next] = stack.back(); + if (next < children[position].size()) { + const auto child = index.at(children[position][next++]); + scopes.push_back(added.size()); + visit(*order[child]); + stack.emplace_back(child, 0u); + continue; + } + while (added.size() > scopes.back()) { + available.erase(available.find(*added.back())); + added.pop_back(); + } + scopes.pop_back(); + stack.pop_back(); + } + const auto replaced = [&](IrValue*& value) { + if (value == nullptr) return; + if (const auto found = replacements.find(value->Resolve()); found != replacements.end()) value = found->second; + }; + for (auto& info : program.Metadata().blockInfo) { + replaced(info.condition); + replaced(info.indirectTarget); + } + for (auto*& read : program.Metadata().dynamicReads) replaced(read); + return stats; +} + +} diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index dea1dba133..2ccd9a4039 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -1,9 +1,13 @@ #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 "SpirvBackend/SpirvBufferFormat.hpp" +#include "ThreadOwned.hpp" +#include "ImageTableAbi.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include #include #include @@ -12,8 +16,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -27,7 +33,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 +77,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); } @@ -91,7 +106,7 @@ RdnaImageDimension descriptorDimension(const DescriptorValue& descriptor, RdnaIm case ImageType::Cube: return RdnaImageDimension::Dim2DArray; case ImageType::Color2DArray: - return wantArray ? RdnaImageDimension::Dim2DArray : RdnaImageDimension::Dim2D; + return wantArray || requested == RdnaImageDimension::Dim3D ? RdnaImageDimension::Dim2DArray : RdnaImageDimension::Dim2D; case ImageType::Color2DMsaaArray: return wantArray ? RdnaImageDimension::Dim2DMsaaArray : RdnaImageDimension::Dim2DMsaa; case ImageType::Color2D: @@ -122,7 +137,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 +145,81 @@ 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; + } + } 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] & (3u << 20u)) != 0u) return rejectImage(PoisonReason::Fmask, "FMASK CMASK compression is not implemented"); + 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 +231,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 && (srgbDecodeFormats & SrgbDecodeBit(format)) != 0u; + 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 +262,117 @@ 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"); + return rejectImage(PoisonReason::NumericClass, "sampled image descriptor uses an unsupported format"); } - 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 +380,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,9 +1273,12 @@ 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; - walker.EvaluateRuntimeSources(plan, plan.materializationSources, runtime, values, snapshot.flattenedSrt, plan.cleanFlatSlots, activeSources, &snapshot.srtPoison); + 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()) { @@ -454,29 +1288,13 @@ void materializeSnapshot(const IrResourcePlan& plan, const SrtRuntime& runtime, 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,24 +1305,63 @@ void materializeSnapshot(const IrResourcePlan& plan, const SrtRuntime& runtime, if (descriptor.dwordCount != 8u) { throw std::runtime_error("image descriptor has an invalid width"); } - if (!validImageDescriptor(descriptor, image.r128) && !nullImageDescriptor(descriptor)) { + 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(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::Format8UNorm: case IrBufferFormat::Format8_8UNorm: case IrBufferFormat::Format16_16UNorm: + case IrBufferFormat::Format16_16UNorm: case IrBufferFormat::Format11_11_10UNorm: case IrBufferFormat::Format10_11_11UNorm: case IrBufferFormat::Format2_10_10_10UNorm: - case IrBufferFormat::Format10_10_10_2UNorm: case IrBufferFormat::Format8_8_8_8UNorm: case IrBufferFormat::Format16_16_16_16UNorm: + case IrBufferFormat::Format10_10_10_2UNorm: case IrBufferFormat::Format16_16_16_16UNorm: return EmulatedCompare::ReferenceUnorm; + case IrBufferFormat::Format8UNorm: case IrBufferFormat::Format8_8UNorm: case IrBufferFormat::Format8_8_8_8UNorm: + case IrBufferFormat::Format8Srgb: case IrBufferFormat::Format8_8Srgb: case IrBufferFormat::Format8_8_8_8Srgb: + return EmulatedCompare::ReferenceZero; case IrBufferFormat::Format8SNorm: case IrBufferFormat::Format16SNorm: case IrBufferFormat::Format8_8SNorm: case IrBufferFormat::Format16_16SNorm: 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: @@ -517,21 +1374,65 @@ std::uint32_t colorCompareReference(IrBufferFormat format) { } } -std::uint32_t emulatedCompareState(const ShaderInfo& info, const ResourceSnapshot& snapshot, std::uint32_t index) { +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 (!image.depthCompare || descriptor.dwordCount != 8u || nullImageDescriptor(descriptor)) return 0u; + 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.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); - if (type != ImageType::Color2D && type != ImageType::Color2DArray) throw std::runtime_error("comparison sampling of a color texture is implemented only for 2D and 2D array views"); - if ((descriptorImageSwizzle(descriptor) & 0x7u) != 4u) throw std::runtime_error("comparison sampling of a color texture is implemented only when the view's X channel is red"); + 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) 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.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); + if (type != ImageType::Color2D && type != ImageType::Color2DArray && type != ImageType::Cube) throw std::runtime_error("comparison sampling of a color texture is implemented only for 2D, 2D array and cube views"); + bool redChannel = false; + bool compareChannel = false; + for (std::uint32_t channel = 0u; channel < 4u; ++channel) { + const auto select = (descriptorImageSwizzle(descriptor) >> (channel * 3u)) & 0x7u; + if (select == 2u || select == 3u) throw std::runtime_error("comparison sampling of a color texture is implemented only when every view channel is a constant or a color channel"); + redChannel = redChannel || select == 4u; + compareChannel = compareChannel || select >= 4u; + } + if (compareChannel && !redChannel) throw std::runtime_error("comparison sampling of a color texture is implemented only when a view channel selects red"); + 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; @@ -541,6 +1442,10 @@ std::uint32_t emulatedCompareState(const ShaderInfo& info, const ResourceSnapsho if (((words[0] >> 29u) & 0x3u) != 0u) throw std::runtime_error("comparison sampling of a color texture through a min or max reduction sampler is not implemented"); const auto magFilter = (words[2] >> 20u) & 0x3u; const auto minFilter = (words[2] >> 22u) & 0x3u; + const bool constantZero = reference == EmulatedCompare::ReferenceZero; + if (type == ImageType::Cube && magFilter == 1u && ((words[0] >> 28u) & 1u) == 0u && !constantZero) { + throw std::runtime_error("bilinear cube comparison requires DISABLE_CUBE_WRAP to avoid seamless face filtering"); + } const auto addressMode = [](std::uint32_t clamp) { if (clamp == 0u) return EmulatedCompare::AddressWrap; if (clamp == 2u) return EmulatedCompare::AddressEdge; @@ -552,7 +1457,7 @@ std::uint32_t emulatedCompareState(const ShaderInfo& info, const ResourceSnapsho const auto borderType = (words[3] >> 30u) & 0x3u; const bool border = addressX == EmulatedCompare::AddressBorder || addressY == EmulatedCompare::AddressBorder; if (border && borderType == 3u) throw std::runtime_error("comparison sampling of a color texture with a border color table is not implemented"); - if (magFilter != minFilter || magFilter > 1u) throw std::runtime_error("comparison sampling of a color texture is implemented only with equal point or bilinear minification and magnification filters"); + if (!constantZero && (magFilter != minFilter || magFilter > 1u)) throw std::runtime_error("comparison sampling of a color texture is implemented only with equal point or bilinear minification and magnification filters"); if (unnormalized) throw std::runtime_error("comparison sampling of a color texture does not implement unnormalized coordinates"); const auto state = EmulatedCompare::Enabled | (function << EmulatedCompare::FunctionShift) | (magFilter == 1u ? EmulatedCompare::Linear : 0u) | (addressX << EmulatedCompare::ClampXShift) | (addressY << EmulatedCompare::ClampYShift) | (border && borderType == 2u ? EmulatedCompare::BorderWhite : 0u); @@ -564,32 +1469,33 @@ 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 + && std::none_of(plan.memoryInfo.begin(), plan.memoryInfo.end(), [i](const MemoryInfo& memory) { return memory.kind == ResourceKind::Buffer && memory.resource == i; }); + if (scalarOnly) { + snapshot.buffers.at(i).dwords[1] &= ~0x40000000u; + snapshot.buffers.at(i).dwords[3] = 0u; + } const auto decoded = decodeBufferDescriptor(snapshot.buffers.at(i)); - if (decoded.Type() != 0u) throw std::runtime_error("buffer descriptor uses an unsupported type"); + if (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))); } } @@ -606,9 +1512,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) { @@ -619,21 +1850,64 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe mode.depthBits = depth; mode.depthUnorm16 = unorm16; mode.cube = false; - mode.mipCount = mode.mipMode == ImageMipMode::DynamicStorage ? RuntimeAbi::StorageHeapCapacity : 1u; + mode.mipCount = mode.mipMode == ImageMipMode::DynamicStorage ? RuntimeAbi::StorageMipSlots : 1u; mode.shaderSwizzle = ShaderImageIdentitySwizzle; if (conversion == IrBufferFormat::Format11_11_10UNorm || conversion == IrBufferFormat::Format10_11_11Float) mode.shaderSwizzle = 0x2acu; modes.push_back(mode); - if (image.dimension == RdnaImageDimension::Dim2D && image.fmaskCompatible && !depth && packed == IrBufferFormat::Invalid && image.byElements == 0u) { + const bool volumeLoad = (image.dimension == RdnaImageDimension::Dim2D || image.dimension == RdnaImageDimension::Dim2DArray) && image.fmaskCompatible; + const bool volumeStore = image.dimension == RdnaImageDimension::Dim2DArray && storage && image.written && !image.read && !image.atomic && image.mipMode != ImageMipMode::DynamicStorage; + 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); } + 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; + plane.dimension = dimension; + 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); + } + 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); + } + if (image.dimension == RdnaImageDimension::Dim1D && !storage && !depth && conversion == IrBufferFormat::Invalid && packed == IrBufferFormat::Invalid) { + auto plane = mode; + plane.dimension = RdnaImageDimension::Dim2D; + modes.push_back(plane); + } if (image.dimension == RdnaImageDimension::Dim1DArray || image.dimension == RdnaImageDimension::Dim2DArray || image.dimension == RdnaImageDimension::Dim2DMsaaArray) { auto plain = mode; 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); } @@ -662,27 +1936,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; @@ -692,8 +1963,15 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe mode.packedFormat = IrBufferFormat::Invalid; mode.shaderSwizzle = ShaderImageIdentitySwizzle; modes.push_back(mode); + if (image.dimension == RdnaImageDimension::Dim2DArray && !image.remappedDepth1D) { + auto plain = mode; + plain.dimension = RdnaImageDimension::Dim2D; + modes.push_back(plain); + mode.cube = true; + modes.push_back(mode); + } } - if (image.srgbDecodeFormats != 0u && image.srgbDecodeCompatible && !storage && !image.depthCompare && !image.packed) { + if (image.srgbDecodeFormats != 0u && image.srgbDecodeCompatible && !image.depthCompare && !storage && !image.packed) { const auto count = modes.size(); for (std::size_t index = 0; index < count; ++index) { const auto& base = modes[index]; @@ -703,7 +1981,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; @@ -718,46 +1996,101 @@ 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; } -std::uint32_t ResourceMaterializer::RuntimeImageMode(const ImageResource& image, const DescriptorValue& descriptor, std::span modes) { +bool ResourceMaterializer::NullImageDescriptor(const ImageResource& image, const DescriptorValue& descriptor) { + return nullImageDescriptor(image, descriptor); +} + +const char* ResourceMaterializer::UnnormalizedSampleMismatch(const ImageResource& image) { + 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"; + if (image.conversionFormat != IrBufferFormat::Invalid || image.packed) return "samples an image that needs a format conversion or packed access"; + return nullptr; +} + +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::StorageHeapCapacity : 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"); - throw std::runtime_error("image descriptor is incompatible with the static runtime image interface"); + 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) + "}"; + }; + std::string message = "image descriptor is incompatible with the static runtime image interface: descriptor format " + std::to_string(static_cast(format)) + " decodes to " + describe(decoded.numericClass, decoded.dimension, decoded.conversionFormat, decoded.packedFormat, decoded.cube, decoded.depthBits, decoded.depthUnorm16, decoded.srgbDecode) + (emulated ? " (emulated compare)" : "") + "; modes:"; + 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]); + 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) { @@ -773,49 +2106,41 @@ void ResourceMaterializer::ApplyStaticInterface(IrProgram& program, bool nativeS for (const auto* inst : block->Instructions()) { if (inst->Opcode() != IrOpcode::ImageSampleRaw) continue; const auto& memory = resources.memoryInfo.at(inst->Flags().index); - if ((memory.imageSampleFlags & (RdnaImageSampleFlagCompare | RdnaImageSampleFlagOffset)) != (RdnaImageSampleFlagCompare | RdnaImageSampleFlagOffset)) continue; + if ((memory.imageSampleFlags & RdnaImageSampleFlagOffset) == 0u) continue; const auto* address = inst->Argument(2)->Resolve(); const auto component = GetRdnaImageAddressComponentLayout(memory.imageSampleFlags, 0u); const auto argument = component.bitOffset / 32u; if (component.bitWidth == 32u && argument < address->ArgumentCount() && address->Argument(argument)->Resolve()->HasImmediate()) continue; - images.at(memory.resource).emulatedCompare |= EmulatedCompare::NativeOffsetUnsupported; + if ((memory.imageSampleFlags & RdnaImageSampleFlagCompare) != 0u) { + images.at(memory.resource).emulatedCompare |= EmulatedCompare::NativeOffsetUnsupported; + } 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::StorageHeapCapacity : 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); + image.mipCount = image.mipMode == ImageMipMode::DynamicStorage ? RuntimeAbi::StorageMipSlots : 1u; + 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); } @@ -851,13 +2176,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) { @@ -869,20 +2194,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); } } @@ -909,6 +2287,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()) { @@ -921,53 +2302,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::SampledHeapCapacity; +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 124eb27474..6f52b4489a 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 @@ -14,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -81,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(); } @@ -112,13 +118,25 @@ 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())); + } + m_blockSources.assign(blockIndices.size(), {}); + m_blockImages.assign(blockIndices.size(), {}); for (auto& block : m_program.Blocks()) { + const auto found = blockIndices.find(block.get()); + m_currentBlock = found == blockIndices.end() ? std::numeric_limits::max() : found->second; for (IrValue* inst : block->Instructions()) { Collect(*inst); } } + m_currentBlock = std::numeric_limits::max(); + PlanResourceControlFlow(); if (m_bdaTraces > bdaTraceLimit) std::fprintf(stderr, "[bda] %u more address accesses in this program not shown\n", m_bdaTraces - bdaTraceLimit); + CheckTableStores(); + AssignTables(); LinkImageAliases(); for (const auto& patch : m_handlePatches) { patch.handle->SetFlags(patch.resource); @@ -130,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); @@ -166,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) { @@ -195,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; } @@ -202,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) { @@ -232,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; @@ -240,13 +264,117 @@ private: same = same && EquivalentValue(m_program.Resources(), current.dwords[i], descriptor.dwords[i]); } if (same) { + NoteBlockSource(candidate); return candidate; } } m_sources.push_back(descriptor); + NoteBlockSource(static_cast(m_sources.size() - 1)); return static_cast(m_sources.size() - 1); } + void NoteBlockImage(std::uint32_t image) { + if (m_currentBlock >= m_blockImages.size()) { + return; + } + auto& images = m_blockImages[m_currentBlock]; + if (std::ranges::find(images, image) == images.end()) { + images.push_back(image); + } + } + + void NoteBlockSource(std::uint32_t source) { + if (m_currentBlock >= m_blockSources.size()) { + return; + } + auto& sources = m_blockSources[m_currentBlock]; + if (std::ranges::find(sources, source) == sources.end()) { + sources.push_back(source); + } + } + + void PlanResourceControlFlow() { + const auto& order = m_program.BlockOrder(); + const auto& info = m_program.Metadata().blockInfo; + if (order.empty() || order.size() != info.size()) { + return; + } + std::unordered_map indexOf; + std::unordered_map idToIndex; + for (std::uint32_t index = 0; index < order.size(); index++) { + indexOf.emplace(order[index], index); + idToIndex.emplace(info[index].id, index); + } + std::vector controlFlow(order.size()); + for (std::uint32_t index = 0; index < order.size(); index++) { + auto& block = controlFlow[index]; + const auto& terminator = info[index].terminator; + const auto target = [&](std::uint32_t id) { return idToIndex.find(id); }; + const auto trueEdge = target(terminator.trueBlock); + const auto falseEdge = target(terminator.falseBlock); + if (terminator.kind == TerminatorKind::ConditionalBranch && !terminator.loopHeader && info[index].condition != nullptr && trueEdge != idToIndex.end() && falseEdge != idToIndex.end()) { + block.condition = info[index].condition; + block.successors = {trueEdge->second, falseEdge->second}; + } else if (terminator.kind != TerminatorKind::Return) { + for (const IrBlock* successor : order[index]->Successors()) { + const auto found = indexOf.find(successor); + if (found == indexOf.end()) { + return; + } + block.successors.push_back(found->second); + } + } + block.sources = m_blockSources[index]; + block.images = m_blockImages[index]; + 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;) { + unconditional[index] = 1u; + if (controlFlow[index].successors.size() != 1u) { + break; + } + index = controlFlow[index].successors.front(); + } + std::vector reached(m_sources.size()); + std::vector reachedImages(m_info.images.size()); + std::vector reachedFlat(m_program.Resources().srtReads.size()); + for (std::uint32_t index = 0; index < order.size(); index++) { + if (unconditional[index] == 0u) { + continue; + } + for (const auto source : controlFlow[index].sources) { + reached[source] = 1u; + } + for (const auto image : controlFlow[index].images) { + reachedImages[image] = 1u; + } + for (const auto slot : controlFlow[index].flatSlots) { + reachedFlat[slot] = 1u; + } + } + bool guarded = false; + for (const auto& block : controlFlow) { + for (const auto source : block.sources) { + guarded = guarded || reached[source] == 0u; + } + for (const auto image : block.images) { + guarded = guarded || reachedImages[image] == 0u; + } + for (const auto slot : block.flatSlots) { + guarded = guarded || reachedFlat[slot] == 0u; + } + } + if (guarded) { + m_program.Resources().controlFlow = std::move(controlFlow); + } + } + static bool immediateU32(IrValue* value, std::uint32_t& result) { value = value->Resolve(); if (!value->HasImmediate() || value->Type() != IrType::U32) { @@ -256,12 +384,6 @@ private: return true; } - static bool usesOnly(const IrValue& value, std::span users) { - return !value.Uses().empty() && std::ranges::all_of(value.Uses(), [&](const IrValue* user) { - return std::ranges::find(users, user) != users.end(); - }); - } - const MemoryInfo* ScalarReadMemory(const IrValue& read, std::uint32_t& index) const { if (read.Opcode() != IrOpcode::ReadConstBuffer || read.ArgumentCount() != 2u) { return nullptr; @@ -274,11 +396,44 @@ private: return memory.kind == ResourceKind::ScalarBuffer && memory.dataBits == 32u && memory.dataDwords == 1u ? &memory : nullptr; } - bool MemoryIndexBelongsTo(std::uint32_t index, const IrValue& owner) const { + 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; + } + + void IndexMemoryUsers() { for (const auto& block : m_program.Blocks()) { for (const IrValue* inst : block->Instructions()) { - const auto op = inst->Opcode(); - if ((BufferAccessOf(op) == BufferAccess::None && AddressOpcodeInfoOf(op).access == AddressAccess::None && ImageOpcodeInfoOf(op).access == ImageAccess::None) || inst == &owner) { + if (AccessesMemory(inst->Opcode())) { + m_memoryUsers[inst->Flags().index].push_back(inst); + } + } + } + m_memoryUsersIndexed = true; + } + + bool MemoryIndexBelongsTo(std::uint32_t index, const IrValue& owner) const { + if (m_memoryUsersIndexed) { + const auto users = m_memoryUsers.find(index); + return users == m_memoryUsers.end() || std::ranges::all_of(users->second, [&](const IrValue* user) { return user == &owner; }); + } + for (const auto& block : m_program.Blocks()) { + for (const IrValue* inst : block->Instructions()) { + if (!AccessesMemory(inst->Opcode()) || inst == &owner) { continue; } if (inst->Flags().index == index) { @@ -289,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; @@ -302,178 +457,366 @@ 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; - - const std::array imageUsers {&handle}; - for (const auto* read : heapReads) { - if (!usesOnly(*read, imageUsers)) { - 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; + } + + 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; + 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); + if (dword == 0u) { + first = memory->offset; + address = addressRead; + base = readBase; + baseAddend = readAddend; + } 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 (!EquivalentValue(m_program.Resources(), base, readBase)) { + 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; @@ -500,6 +843,10 @@ private: } return; } + if (depth != 0u && value->Opcode() == IrOpcode::ReadFirstLane && value->ArgumentCount() == 2u) { + CollectDescriptorPhis(value->Argument(0), phis, depth - 1u); + return; + } if (depth == 0u || !Detail::IsRuntimeUniformOp(value->Opcode())) { return; } @@ -765,7 +1112,9 @@ private: std::vector> arms; std::map, IrValue*> selectors; std::vector> phis; + std::vector> lanes; std::unordered_map resolved; + std::unordered_map order; }; IrValue* WebSelector(std::vector tuple, DescriptorWeb& web, IrValue& position) { @@ -785,6 +1134,10 @@ private: return known->second; } const auto phi = std::ranges::find_if(tuple, [](const IrValue* value) { return value->IsPhi(); }); + const auto lane = std::ranges::find_if(tuple, [](const IrValue* value) { return value->Opcode() == IrOpcode::SelectU32 || value->Opcode() == IrOpcode::ReadFirstLane; }); + if (phi == tuple.end() && lane != tuple.end()) { + return LaneSelector(tuple, **lane, web, position); + } if (phi == tuple.end()) { for (IrValue* value : tuple) { if (HoldsProgramCounter(value, phiSearchDepth) || Rematerialize(value, position, false, phiSearchDepth) == nullptr) { @@ -803,7 +1156,27 @@ private: web.arms.push_back(std::move(tuple)); return &m_builder.Constant(static_cast(web.arms.size() - 1u)); } - IrBlock* block = (*phi)->Parent(); + if (web.order.empty()) { + const auto& order = m_program.BlockOrder(); + for (std::size_t index = 0; index < order.size(); index++) { + web.order.emplace(order[index], index); + } + } + IrBlock* block = nullptr; + std::size_t latest = 0; + for (const IrValue* value : tuple) { + if (!value->IsPhi()) { + continue; + } + const auto found = web.order.find(value->Parent()); + if (found == web.order.end()) { + return nullptr; + } + if (block == nullptr || found->second > latest) { + block = value->Parent(); + latest = found->second; + } + } if (block == nullptr || web.selectors.size() == DescriptorWeb::maxStates) { return nullptr; } @@ -812,7 +1185,7 @@ private: return nullptr; } for (const IrValue* value : tuple) { - if (value->IsPhi() && (value->Parent() != block || value->PhiBlockCount() != predecessors.size())) { + if (value->IsPhi() && value->Parent() == block && value->PhiBlockCount() != predecessors.size()) { return nullptr; } } @@ -822,7 +1195,7 @@ private: for (IrBlock* predecessor : predecessors) { std::vector incoming; for (IrValue* value : tuple) { - IrValue* edge = value->IsPhi() ? PhiIncoming(*value, predecessor) : value; + IrValue* edge = value->IsPhi() && value->Parent() == block ? PhiIncoming(*value, predecessor) : value; if (edge == nullptr) { return nullptr; } @@ -837,6 +1210,64 @@ private: return &selector; } + IrValue* LaneSelector(const std::vector& tuple, IrValue& first, DescriptorWeb& web, IrValue& position) { + IrBlock* block = first.Parent(); + const auto op = first.Opcode(); + const bool select = op == IrOpcode::SelectU32; + if (block == nullptr || first.ArgumentCount() != (select ? 3u : 2u) || web.selectors.size() == DescriptorWeb::maxStates) { + return nullptr; + } + IrValue* shared = first.Argument(select ? 0u : 1u); + std::vector left; + std::vector right; + for (IrValue* value : tuple) { + if (value->Opcode() == op && value->Parent() == block && value->ArgumentCount() == first.ArgumentCount() && EquivalentValue(m_program.Resources(), value->Argument(select ? 0u : 1u), shared)) { + left.push_back(value->Argument(select ? 1u : 0u)); + if (select) { + right.push_back(value->Argument(2u)); + } + } else if (value->HasImmediate() || (select && !value->IsPhi() && value->Opcode() != IrOpcode::ReadFirstLane)) { + left.push_back(value); + if (select) { + right.push_back(value); + } + } else { + return nullptr; + } + } + auto& instructions = block->Instructions(); + auto latest = instructions.end(); + for (auto it = instructions.begin(); it != instructions.end(); ++it) { + if (std::ranges::find(tuple, *it) != tuple.end()) { + latest = it; + } + } + if (latest == instructions.end()) { + return nullptr; + } + IrValue* before = std::next(latest) == instructions.end() ? nullptr : *std::next(latest); + IrValue* leftSelector = WebSelector(std::move(left), web, position); + if (leftSelector == nullptr) { + return nullptr; + } + IrValue& node = m_program.CreateValue(op, IrType::U32, first.Flags()); + if (select) { + IrValue* rightSelector = WebSelector(std::move(right), web, position); + if (rightSelector == nullptr) { + return nullptr; + } + node.AddArgument(shared); + node.AddArgument(leftSelector); + node.AddArgument(rightSelector); + } else { + node.AddArgument(leftSelector); + node.AddArgument(shared); + } + web.selectors.emplace(tuple, &node); + web.lanes.emplace_back(block, before, &node); + return &node; + } + void SplitDescriptorWeb(IrValue& inst, const std::array& handles, const std::array& split, std::uint32_t handleCount) { std::vector tuple; std::array offsets {}; @@ -853,15 +1284,30 @@ private: for (const auto& [block, phi] : web.phis) { block->InsertInstructionBefore(nullptr, phi); } + for (const auto& [block, before, node] : web.lanes) { + if (before == nullptr) { + block->AppendInstruction(node); + } else { + block->InsertInstructionBefore(before, node); + } + } } else { for (const auto& [block, phi] : web.phis) { for (std::size_t index = 0; index < phi->ArgumentCount(); index++) { phi->ReplaceArgument(index, nullptr); } } + for (const auto& [block, before, node] : web.lanes) { + for (std::size_t index = 0; index < node->ArgumentCount(); index++) { + node->ReplaceArgument(index, nullptr); + } + } for (const auto& [block, phi] : web.phis) { phi->Invalidate(); } + for (const auto& [block, before, node] : web.lanes) { + node->Invalidate(); + } } if (selector == nullptr || web.arms.empty()) { return; @@ -891,35 +1337,342 @@ 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; + } + } + } + } + m_memoryUsersIndexed = false; + 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) { @@ -930,15 +1683,6 @@ private: DescriptorSource descriptor; MakeSource(*handle, width, sampler, sampleAdjust, descriptor); std::uint32_t badDword = 0; - if (expected == IrOpcode::GetImageResource) { - for (; badDword < descriptor.dwordCount; badDword++) { - const IrValue* value2 = descriptor.dwords[badDword]->Resolve(); - if (value2->Opcode() == IrOpcode::ReadConstBuffer) { - fail(std::string(IrOpcodeName(expected)) + " dword " + std::to_string(badDword) + " is not a valid runtime value; chain: " + describeValueChain(descriptor.dwords[badDword], 8u)); - } - } - badDword = 0; - } if (!ValidateSource(descriptor, badDword)) { fail(std::string(IrOpcodeName(expected)) + " dword " + std::to_string(badDword) + " is not a valid runtime value; chain: " + describeValueChain(descriptor.dwords[badDword], 8u)); } @@ -961,7 +1705,7 @@ private: if (resource == std::numeric_limits::max()) fail("buffer resource limit exceeded"); AddMemoryPatch(memoryIndex, resource, 0u, false); memory.gpuDescriptor = false; - m_info.usesDma = m_info.usesDma || access == BufferAccess::Atomic; + m_info.usesFaultBuffer = m_info.usesFaultBuffer || access == BufferAccess::Atomic; return true; } memory.gpuDescriptor = true; @@ -1015,13 +1759,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; } @@ -1029,17 +1792,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); @@ -1060,12 +1816,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 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; } } @@ -1145,7 +1922,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; @@ -1191,13 +1968,14 @@ 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); } resource = AddImage(source, memory, op, flags.pc); + NoteBlockImage(resource); if (resource == std::numeric_limits::max()) { fail("image resource limit exceeded"); } @@ -1207,10 +1985,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"); @@ -1233,7 +2016,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; @@ -1252,11 +2035,19 @@ private: ShaderInfo m_info; IrBuilder m_builder; std::vector m_sources; + std::vector> m_blockSources; + std::vector> m_blockImages; + std::uint32_t m_currentBlock = std::numeric_limits::max(); std::vector m_handlePatches; std::vector m_memoryPatches; // APS5_TRACE_BDA: address accesses seen by Collect (the first bdaTraceLimit are printed). 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 672f01ef43..135e1cd331 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -102,7 +102,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,12 +143,16 @@ 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) { return Fail("typed pixel scalar input is invalid"); } break; case StageInputKind::VertexIndex: case StageInputKind::InstanceIndex: + case StageInputKind::BaseInstance: case StageInputKind::InvocationId: case StageInputKind::PrimitiveId: case StageInputKind::FrontFacing: @@ -169,6 +173,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: @@ -234,7 +246,7 @@ bool IsPixelParameterFlat(const ShaderPixelInputInfo& pixel, std::uint32_t input } void CollectPixelInputs(const IrProgram& program, const ShaderPixelInputInfo* pixel, ShaderInfo& info) { - if (pixel->HasPositionInput()) { + if (pixel->HasPositionInput() || pixel->psDitheredAlphaToCoverage || pixel->QuadMasked() || pixel->SampleLocationsVary() || pixel->ReordersSamples()) { AddInput(info, StageInputKind::FragCoord, 0, 4, "gl_FragCoord"); } if (pixel->psFrontFace) { @@ -257,7 +269,7 @@ 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 < 2u || !IsPixelParameterFlat(*pixel, input); + perVertex[input] = perVertex[input] || mode == 1u || !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; @@ -324,10 +336,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) { @@ -341,6 +359,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; @@ -365,11 +386,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"); @@ -386,6 +416,12 @@ void CollectBuiltinInputs(const IrProgram& program, ShaderInfo& info) { case StageInputKind::DispatchThreadLimit: info.dispatchThreadLimit = true; break; + case StageInputKind::RasterPrimitive: + info.rasterPrimitive = true; + break; + case StageInputKind::LayerExported: + info.layerExported = true; + break; case StageInputKind::PackedAncillary: case StageInputKind::Parameter: break; @@ -408,6 +444,9 @@ void CollectOutputs(const IrProgram& program, ShaderStageInputInfo inputInfo, Sh if (program.Resources().stage == IrShaderStage::Pixel && (exportInfo.en & 0x4u) != 0u && inputInfo.pixel->psSampleMaskExportEnable) { AddOutput(info, StageOutputKind::SampleMask, 0, 0, "gl_SampleMask"); } + if (program.Resources().stage == IrShaderStage::Pixel && (exportInfo.en & 0x2u) != 0u && inputInfo.pixel->psStencilRefExportEnable) { + AddOutput(info, StageOutputKind::StencilRef, 0, 0, "gl_FragStencilRefARB"); + } continue; } if (exportInfo.en == 0u) { @@ -457,7 +496,7 @@ void CollectOutputs(const IrProgram& program, ShaderStageInputInfo inputInfo, Sh } } -std::uint8_t SamplerUseOf(IrOpcode opcode, std::uint32_t flags, IrShaderStage stage) { +std::uint16_t SamplerUseOf(IrOpcode opcode, std::uint32_t flags, IrShaderStage stage) { if (opcode == IrOpcode::ImageGatherRaw) { return SamplerUseGather; } @@ -465,6 +504,9 @@ std::uint8_t SamplerUseOf(IrOpcode opcode, std::uint32_t flags, IrShaderStage st return SamplerUseQueryLod; } std::uint32_t use = (flags & RdnaImageSampleFlagDerivative) != 0u ? SamplerUseGradient : ImageSampleExplicitLod(flags, stage) ? SamplerUseExplicitLod : SamplerUseImplicitLod; + if (use == SamplerUseExplicitLod && (flags & RdnaImageSampleFlagLevelZero) == 0u) { + use |= SamplerUseNonZeroLod; + } if ((flags & RdnaImageSampleFlagOffset) != 0u) { use |= SamplerUseOffset; } @@ -474,7 +516,7 @@ std::uint8_t SamplerUseOf(IrOpcode opcode, std::uint32_t flags, IrShaderStage st if ((flags & RdnaImageSampleFlagAdjust) != 0u) { use |= SamplerUseAdjust; } - return static_cast(use); + return static_cast(use); } void CollectSamplerUses(const IrProgram& program, ShaderInfo& info) { @@ -496,7 +538,7 @@ void CollectSamplerUses(const IrProgram& program, ShaderInfo& info) { return Fail("image instruction references an unknown sampler"); } auto& sampler = info.samplers[memory.sampler]; - sampler.uses = static_cast(sampler.uses | SamplerUseOf(inst->Opcode(), memory.imageSampleFlags, program.Resources().stage)); + sampler.uses = static_cast(sampler.uses | SamplerUseOf(inst->Opcode(), memory.imageSampleFlags, program.Resources().stage)); } } } @@ -517,6 +559,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: diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp index 752c49c96d..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) { +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,20 +62,30 @@ 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(), 1u); + 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) { for (const auto source : block.sources) { active.at(source) = 0u; } + for (const auto image : block.images) { + active.at(program.descriptorSources.size() + image) = 0u; + } + for (const auto slot : block.flatSlots) activeFlat.at(slot) = 0u; } std::vector visited(program.controlFlow.size()); std::vector pending {0}; + InaccessibleRead conditionRead; + std::uint32_t conditionNullRoots = 0; + cleanEvaluator.ReportInaccessibleReads(&conditionRead); + cleanEvaluator.ReportNullRootReads(&conditionNullRoots); while (!pending.empty()) { const auto index = pending.back(); pending.pop_back(); @@ -84,19 +97,32 @@ bool EvaluateRuntimeSourcesImpl(const IrResourcePlan& program, std::span> zeroedSources; std::vector evaluated; @@ -111,7 +137,7 @@ 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); @@ -134,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 @@ -143,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); @@ -160,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); @@ -176,9 +204,148 @@ 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 396b0cad6a..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) { @@ -143,7 +226,8 @@ bool Evaluator::EvaluateRawRead(IrValue& inst, std::uint64_t& result) { const auto stride = (static_cast(high) >> 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)) { - return false; + result = 0; + return true; } address = ((base & ~std::uint64_t {3}) + byteOffset) & ~std::uint64_t {3}; } else { @@ -152,12 +236,18 @@ bool Evaluator::EvaluateRawRead(IrValue& inst, std::uint64_t& result) { return false; } } - if (_inaccessible != nullptr && _runtime.accessible != nullptr && !_runtime.accessible(_runtime.userContext, address, sizeof(std::uint32_t)) && LoadedFromMemory(*handle)) { - *_inaccessible = {&inst, address}; + if (_nullRoots != nullptr && base == 0 && !LoadedFromMemory(*handle.Value())) { + ++*_nullRoots; + result = 0; + return true; + } + if (_runtime.userDataUse != nullptr) _runtime.userDataUse->reads.emplace_back(address, origin); + if (_inaccessible != nullptr && _runtime.accessible != nullptr && !_runtime.accessible(_runtime.userContext, address, sizeof(std::uint32_t)) && LoadedFromMemory(*handle.Value())) { + *_inaccessible = {inst.Value(), address}; return false; } 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; @@ -172,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; @@ -180,18 +456,26 @@ 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); } case IrOpcode::BitCastU32F32: @@ -205,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; @@ -256,6 +541,12 @@ bool Evaluator::EvaluateInst(IrValue& inst, std::uint64_t& result) { return true; } return false; + case IrOpcode::SMin32: + if (binary()) { + result = static_cast(std::min(std::bit_cast(static_cast(a)), std::bit_cast(static_cast(b)))); + return true; + } + return false; case IrOpcode::UMin32: if (binary()) { result = std::min(static_cast(a), static_cast(b)); @@ -338,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); @@ -444,6 +755,12 @@ bool Evaluator::EvaluateInst(IrValue& inst, std::uint64_t& result) { return true; } return false; + case IrOpcode::SLessThan32: + if (binary()) { + result = std::bit_cast(static_cast(a)) < std::bit_cast(static_cast(b)) ? 1u : 0u; + return true; + } + return false; case IrOpcode::ULessThan32: if (binary()) { result = static_cast(a) < static_cast(b) ? 1u : 0u; @@ -490,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 274524be75..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: @@ -70,6 +83,7 @@ bool IsRuntimeUniformOp(IrOpcode opcode) { case IrOpcode::ISub64: case IrOpcode::IMul32: case IrOpcode::IMul64: + case IrOpcode::SMin32: case IrOpcode::UMin32: case IrOpcode::ShiftLeftLogical32: case IrOpcode::ShiftLeftLogical64: @@ -82,9 +96,13 @@ 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: + case IrOpcode::SLessThan32: case IrOpcode::ULessThan32: case IrOpcode::IEqual32: case IrOpcode::UGreaterThan32: diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtPlanBuilder.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtPlanBuilder.cpp index e3e0af4223..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,16 +117,14 @@ 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); - for (const auto& use : std::vector(patch.inst->OperandUses())) { - use.user->ReplaceArgument(use.operand, &flat); - } + patch.inst->ReplaceAllUsesWith(&flat); for (auto& info : _program.Metadata().blockInfo) { if (info.condition != nullptr && info.condition->Resolve() == patch.inst) { info.condition = ♭ 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 4e8a85d891..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 sources, const SrtRuntime& runtime, std::vector& results, std::vector& flat, std::span cleanFlatSlots, std::vector& activeSources, std::vector* poison) const { - if (!Detail::EvaluateRuntimeSourcesImpl(program, sources, runtime, results, flat, true, cleanFlatSlots, activeSources, poison)) { +void SrtWalker::EvaluateRuntimeSources(const IrResourcePlan& program, std::span sources, const SrtRuntime& runtime, std::vector& results, std::vector& flat, std::span cleanFlatSlots, std::vector& activeSources, std::vector* poison, std::uint32_t* nullRootReads) const { + if (!Detail::EvaluateRuntimeSourcesImpl(program, sources, runtime, results, flat, true, cleanFlatSlots, activeSources, poison, nullRootReads)) { throw std::runtime_error("SrtWalker::EvaluateRuntimeSources failed to evaluate runtime sources: " + Detail::RuntimeSourceFailureReason()); } } diff --git a/core/shader/recompiler/Optimization/src/SsaBuilder/SsaPass.cpp b/core/shader/recompiler/Optimization/src/SsaBuilder/SsaPass.cpp index f3ed83cbed..74181182b1 100644 --- a/core/shader/recompiler/Optimization/src/SsaBuilder/SsaPass.cpp +++ b/core/shader/recompiler/Optimization/src/SsaBuilder/SsaPass.cpp @@ -115,7 +115,8 @@ IrValue* Pass::TryRemoveTrivialPhi(IrValue& phi, Variable variable) { if (same == nullptr) { same = MakeInitialValue(variable); } - std::vector 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 new file mode 100644 index 0000000000..79a616db94 --- /dev/null +++ b/core/shader/recompiler/Optimization/src/TessellationLowering.cpp @@ -0,0 +1,1092 @@ +#include "Optimization/TessellationLowering.hpp" +#include "Optimization/DeadCodeEliminator.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ShaderRecompiler { +namespace { + +constexpr std::int64_t LocalVertexIndexCount = 8192; +constexpr std::int64_t TriangleFactorBytes = 16; +constexpr std::int64_t QuadFactorBytes = 24; +constexpr std::int64_t AddressLimit = 0xffffffffll; + +struct Affine { + std::int64_t vertex = 0; + std::int64_t relative = 0; + std::int64_t constant = 0; + bool dynamic = false; + std::int64_t dynamicScale = 0; + const IrValue* dynamicRoot = nullptr; + + [[nodiscard]] bool Scalar() const { return vertex == 0 && relative == 0 && !dynamic; } + bool operator==(const Affine& other) const = default; +}; + +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); + throw std::runtime_error(text); +} + +bool IsImmediate(const IrValue* raw, std::uint32_t expected) { + const IrValue* value = raw->Resolve(); + return value->HasImmediate() && value->Type() == IrType::U32 && value->ImmediateU32() == expected; +} + +bool IsBase(const IrValue* raw, TessellationBaseKind kind) { + const IrValue* value = raw->Resolve(); + 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; +} + +IrValue* VariableOperand(IrValue* value, std::uint32_t constant) { + if (IsImmediate(value->Argument(1), constant)) return value->Argument(0)->Resolve(); + if (IsImmediate(value->Argument(0), constant)) return value->Argument(1)->Resolve(); + return nullptr; +} + +bool IsLaneId(const IrValue* value) { + return value != nullptr && value->Resolve()->Opcode() == IrOpcode::LaneId; +} + +bool IsTrue(const IrValue* value) { + return value != nullptr && value->HasImmediate() && value->Type() == IrType::Bool && value->ImmediateBool(); +} + +using LaneWords = std::pair; + +IrValue* BallotPredicate(IrValue* word, std::uint32_t half) { + IrValue* ballot = word->Argument(0)->Resolve(); + return ballot->Opcode() == IrOpcode::Ballot && IsImmediate(word->Argument(1), half) ? ballot->Argument(0)->Resolve() : nullptr; +} + +bool SamePredicate(const IrValue* lhs, const IrValue* rhs) { + if (lhs == nullptr || rhs == nullptr) return false; + if (lhs == rhs) return true; + return lhs->HasImmediate() && rhs->HasImmediate() && lhs->Type() == IrType::Bool && rhs->Type() == IrType::Bool && lhs->ImmediateBool() == rhs->ImmediateBool(); +} + +std::optional LaneBitWords(IrValue* value, std::uint32_t waveSize) { + if (value->Opcode() != IrOpcode::INotEqual32) return std::nullopt; + IrValue* masked = VariableOperand(value, 0u); + if (masked == nullptr || masked->Opcode() != IrOpcode::BitwiseAnd32) return std::nullopt; + IrValue* shifted = VariableOperand(masked, 1u); + if (shifted == nullptr || shifted->Opcode() != IrOpcode::ShiftRightLogical32) return std::nullopt; + IrValue* bit = shifted->Argument(1)->Resolve(); + if (bit->Opcode() != IrOpcode::BitwiseAnd32 || !IsLaneId(VariableOperand(bit, 31u))) return std::nullopt; + IrValue* word = shifted->Argument(0)->Resolve(); + if (waveSize != 64u) return LaneWords{word, nullptr}; + if (word->Opcode() != IrOpcode::SelectU32) return LaneWords{word, word}; + IrValue* low = word->Argument(0)->Resolve(); + if (low->Opcode() != IrOpcode::ULessThan32 || !IsLaneId(low->Argument(0)) || !IsImmediate(low->Argument(1), 32u)) return LaneWords{word, word}; + return LaneWords{word->Argument(1)->Resolve(), word->Argument(2)->Resolve()}; +} + +class LaneBits { +public: + explicit LaneBits(IrProgram& program) : program(program) {} + + IrValue* Of(const LaneWords& words) { + std::vector visited; + if (matches(words, visited)) return &build(words); + for (const auto& pair : visited) { + if (known.at(pair)) known.erase(pair); + } + return nullptr; + } + +private: + static LaneWords argument(const LaneWords& words, std::size_t index) { + return {words.first->Argument(index)->Resolve(), words.second == nullptr ? nullptr : words.second->Argument(index)->Resolve()}; + } + + bool matches(const LaneWords& words, std::vector& visited) { + if (const auto found = known.find(words); found != known.end()) return found->second; + known.emplace(words, true); + visited.push_back(words); + const bool result = check(words, visited); + if (!result) known[words] = false; + return result; + } + + bool check(const LaneWords& words, std::vector& visited) { + const auto [low, high] = words; + if (low->HasImmediate()) { + if (low->Type() != IrType::U32 || (low->ImmediateU32() != 0u && low->ImmediateU32() != 0xffffffffu)) return false; + return high == nullptr || (high->HasImmediate() && high->Type() == IrType::U32 && high->ImmediateU32() == low->ImmediateU32()); + } + if (high != nullptr && (high->HasImmediate() || high->Opcode() != low->Opcode())) return false; + switch (low->Opcode()) { + case IrOpcode::CompositeExtractU32x4: { + if (BallotPredicate(low, 0u) == nullptr) return false; + return high == nullptr || SamePredicate(BallotPredicate(low, 0u), BallotPredicate(high, 1u)); + } + case IrOpcode::BitwiseAnd32: + case IrOpcode::BitwiseOr32: + case IrOpcode::BitwiseXor32: + return matches(argument(words, 0), visited) && matches(argument(words, 1), visited); + case IrOpcode::BitwiseNot32: + return matches(argument(words, 0), visited); + case IrOpcode::SelectU32: + if (high != nullptr && high->Argument(0)->Resolve() != low->Argument(0)->Resolve()) return false; + return matches(argument(words, 1), visited) && matches(argument(words, 2), visited); + case IrOpcode::Phi: + if (low->ArgumentCount() == 0 || low->ArgumentCount() != low->PhiBlockCount()) return false; + if (high != nullptr && (high->Parent() != low->Parent() || high->ArgumentCount() != low->ArgumentCount() || high->PhiBlockCount() != low->PhiBlockCount())) return false; + for (std::size_t index = 0; index < low->ArgumentCount(); ++index) { + if (high != nullptr && high->PhiBlock(index) != low->PhiBlock(index)) return false; + if (!matches(argument(words, index), visited)) return false; + } + return true; + default: + return false; + } + } + + IrValue& boolean(bool value) { + IrValue& result = program.CreateValue(IrOpcode::Void, IrType::Bool); + result.SetImmediateBool(value); + return result; + } + + IrValue& emit(IrValue* position, IrOpcode opcode, std::initializer_list arguments) { + IrValue& value = program.CreateValue(opcode, IrType::Bool); + for (IrValue* argument : arguments) value.AddArgument(argument); + position->Parent()->InsertInstructionBefore(position, &value); + return value; + } + + IrValue& combine(IrValue* position, IrOpcode opcode, IrValue& lhs, IrValue& rhs) { + for (const auto& [fixed, other] : {std::pair{&lhs, &rhs}, std::pair{&rhs, &lhs}}) { + if (!fixed->HasImmediate()) continue; + if (opcode == IrOpcode::LogicalAnd) return fixed->ImmediateBool() ? *other : *fixed; + if (opcode == IrOpcode::LogicalOr) return fixed->ImmediateBool() ? *fixed : *other; + if (!fixed->ImmediateBool()) return *other; + } + return emit(position, opcode, {&lhs, &rhs}); + } + + IrValue& build(const LaneWords& words) { + IrValue* low = words.first; + if (low->HasImmediate()) return boolean(low->ImmediateU32() != 0u); + if (const auto found = built.find(words); found != built.end()) return *found->second; + IrValue* result = nullptr; + switch (low->Opcode()) { + case IrOpcode::CompositeExtractU32x4: + result = BallotPredicate(low, 0u); + break; + case IrOpcode::BitwiseAnd32: + case IrOpcode::BitwiseOr32: + case IrOpcode::BitwiseXor32: { + const auto opcode = low->Opcode() == IrOpcode::BitwiseAnd32 ? IrOpcode::LogicalAnd : low->Opcode() == IrOpcode::BitwiseOr32 ? IrOpcode::LogicalOr : IrOpcode::LogicalXor; + IrValue& lhs = build(argument(words, 0)); + IrValue& rhs = build(argument(words, 1)); + result = &combine(low, opcode, lhs, rhs); + break; + } + case IrOpcode::BitwiseNot32: { + IrValue& operand = build(argument(words, 0)); + result = operand.HasImmediate() ? &boolean(!operand.ImmediateBool()) : &emit(low, IrOpcode::LogicalNot, {&operand}); + break; + } + case IrOpcode::SelectU32: { + IrValue& chosen = build(argument(words, 1)); + IrValue& other = build(argument(words, 2)); + result = &emit(low, IrOpcode::SelectU1, {low->Argument(0)->Resolve(), &chosen, &other}); + break; + } + default: { + IrValue& phi = program.CreateValue(IrOpcode::Phi, IrType::Bool); + low->Parent()->InsertInstructionBefore(low, &phi); + built.emplace(words, &phi); + for (std::size_t index = 0; index < low->ArgumentCount(); ++index) phi.AddPhiOperand(low->PhiBlock(index), &build(argument(words, index))); + return phi; + } + } + built.emplace(words, result); + return *result; + } + + IrProgram& program; + std::map known; + std::map built; +}; + +bool Implies(IrValue* rawPredicate, IrValue* rawCondition) { + IrValue* predicate = rawPredicate->Resolve(); + IrValue* condition = rawCondition->Resolve(); + if (predicate == condition) return true; + if (IsTrue(condition)) return true; + return predicate->Opcode() == IrOpcode::LogicalAnd && (Implies(predicate->Argument(0), condition) || Implies(predicate->Argument(1), condition)); +} + +class AddressEvaluator { +public: + AddressEvaluator(std::uint32_t vertexCount, std::int64_t relativeCount) : vertexCount(vertexCount), relativeCount(relativeCount) {} + + std::optional Evaluate(IrValue* value, IrValue* predicate, bool allowDynamic = false) { + strictMemo.clear(); + dynamicMemo.clear(); + independence.clear(); + active = predicate; + return evaluate(value, allowDynamic); + } + + bool IsInvocation(IrValue* value) { + const auto address = Evaluate(value, nullptr); + return address && *address == Affine{1, 0, 0}; + } + + [[nodiscard]] std::int64_t Lowest(const Affine& value) const { + return value.constant + std::min(0, value.vertex * lastVertex()) + std::min(0, value.relative * (relativeCount - 1)); + } + + [[nodiscard]] std::int64_t Highest(const Affine& value) const { + return value.constant + std::max(0, value.vertex * lastVertex()) + std::max(0, value.relative * (relativeCount - 1)); + } + +private: + [[nodiscard]] std::int64_t lastVertex() const { return vertexCount == 0 ? 0 : static_cast(vertexCount) - 1; } + + [[nodiscard]] std::optional bounded(const Affine& value) const { + if (value.dynamic) return value; + if (Lowest(value) < 0 || Highest(value) > AddressLimit) return std::nullopt; + return value; + } + + [[nodiscard]] std::optional scale(const Affine& value, std::int64_t factor) const { + if (factor < 0 || factor >= (1ll << 31) || std::abs(value.dynamicScale) * factor > AddressLimit) return std::nullopt; + if (value.dynamic && factor == 0) return Affine{}; + return bounded({value.vertex * factor, value.relative * factor, value.constant * factor, value.dynamic, value.dynamicScale * factor, value.dynamicRoot}); + } + + [[nodiscard]] std::optional> split(const Affine& value, std::int64_t divisor) const { + if (value.dynamic) return std::nullopt; + Affine high; + Affine low; + (value.vertex % divisor == 0 ? high.vertex : low.vertex) = value.vertex; + (value.relative % divisor == 0 ? high.relative : low.relative) = value.relative; + high.constant = FloorDiv(value.constant, divisor) * divisor; + low.constant = value.constant - high.constant; + if (Lowest(low) < 0 || Highest(low) >= divisor) return std::nullopt; + return std::pair{high, low}; + } + + [[nodiscard]] std::optional shiftRight(const Affine& value, std::int64_t shift) const { + if (shift < 0 || shift >= 32) return std::nullopt; + const auto divisor = 1ll << shift; + const auto parts = split(value, divisor); + if (!parts) return std::nullopt; + return bounded({parts->first.vertex / divisor, parts->first.relative / divisor, parts->first.constant / divisor}); + } + + [[nodiscard]] std::optional mask(const Affine& value, std::int64_t bits) const { + if (value.dynamic) return std::nullopt; + if (value.Scalar()) return Affine{0, 0, value.constant & bits}; + const auto width = std::bit_width(static_cast(bits)); + if (width >= 32) return bits == AddressLimit ? std::optional(value) : std::nullopt; + const auto divisor = 1ll << width; + const auto parts = split(value, divisor); + if (!parts) return std::nullopt; + if (parts->second.Scalar()) return Affine{0, 0, parts->second.constant & bits}; + return bits == divisor - 1 ? std::optional(parts->second) : std::nullopt; + } + + [[nodiscard]] std::optional disjoint(const Affine& high, const Affine& low) const { + if (high.dynamic || low.dynamic || Lowest(low) < 0) return std::nullopt; + const auto divisor = 1ll << std::bit_width(static_cast(Highest(low))); + if (high.vertex % divisor != 0 || high.relative % divisor != 0 || high.constant % divisor != 0) return std::nullopt; + return bounded({high.vertex + low.vertex, high.relative + low.relative, high.constant + low.constant}); + } + + bool independentOfRelative(IrValue* raw) { + IrValue* start = raw->Resolve(); + if (const auto found = independence.find(start); found != independence.end()) return found->second; + std::vector pending{start}; + std::unordered_set visited; + bool independent = true; + while (independent && !pending.empty()) { + IrValue* value = pending.back(); + pending.pop_back(); + if (value == nullptr || value->HasImmediate() || !visited.insert(value).second) continue; + if (IsBase(value, TessellationBaseKind::RelativeIndex)) { + independent = false; + break; + } + if (value != start) { + if (const auto known = evaluate(value, false)) { + independent = known->relative == 0; + continue; + } + } + if (active != nullptr && value->Opcode() == IrOpcode::SelectU32 && Implies(active, value->Argument(0))) { + pending.push_back(value->Argument(1)->Resolve()); + continue; + } + 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()); + } + } + independence[start] = independent; + return independent; + } + + std::optional evaluate(IrValue* raw, bool dynamic) { + IrValue* value = raw->Resolve(); + if (value->HasImmediate()) { + if (value->Type() != IrType::U32) return std::nullopt; + return Affine{0, 0, value->ImmediateU32()}; + } + auto& memo = dynamic ? dynamicMemo : strictMemo; + if (const auto found = memo.find(value); found != memo.end()) return found->second; + memo.emplace(value, std::nullopt); + auto result = compute(value, dynamic); + if (!result && dynamic && independentOfRelative(value)) result = Affine{0, 0, 0, true, 1, value}; + memo[value] = result; + return result; + } + + std::optional compute(IrValue* value, bool dynamic) { + const auto argument = [&](std::size_t index) { return evaluate(value->Argument(index), dynamic); }; + const auto scalar = [&](std::size_t index) -> std::optional { + const auto operand = argument(index); + if (!operand || !operand->Scalar()) return std::nullopt; + return operand->constant; + }; + switch (value->Opcode()) { + case IrOpcode::TessellationBase: + return IsBase(value, TessellationBaseKind::RelativeIndex) ? std::optional(Affine{0, 1, 0}) : std::nullopt; + case IrOpcode::GetBuiltin: + return vertexCount != 0 && IsImmediate(value->Argument(0), static_cast(StageInputKind::InvocationId)) ? std::optional(Affine{1, 0, 0}) : std::nullopt; + case IrOpcode::IAdd32: + case IrOpcode::ISub32: { + const auto lhs = argument(0); + const auto rhs = argument(1); + if (!lhs || !rhs) return std::nullopt; + const std::int64_t sign = value->Opcode() == IrOpcode::IAdd32 ? 1 : -1; + Affine sum{lhs->vertex + sign * rhs->vertex, lhs->relative + sign * rhs->relative, lhs->constant + sign * rhs->constant, lhs->dynamic || rhs->dynamic, lhs->dynamicScale, lhs->dynamicRoot}; + if (rhs->dynamic && !lhs->dynamic) { + sum.dynamicScale = sign * rhs->dynamicScale; + sum.dynamicRoot = rhs->dynamicRoot; + } else if (rhs->dynamic && lhs->dynamicRoot != nullptr && lhs->dynamicRoot == rhs->dynamicRoot) { + sum.dynamicScale = lhs->dynamicScale + sign * rhs->dynamicScale; + if (sum.dynamicScale == 0) { + sum.dynamic = false; + sum.dynamicRoot = nullptr; + } + } else if (rhs->dynamic) { + sum.dynamicScale = std::gcd(lhs->dynamicScale, rhs->dynamicScale); + sum.dynamicRoot = nullptr; + } + return bounded(sum); + } + case IrOpcode::IMul32: { + const auto lhs = argument(0); + const auto rhs = argument(1); + if (!lhs || !rhs) return std::nullopt; + if (rhs->Scalar()) return scale(*lhs, rhs->constant); + if (lhs->Scalar()) return scale(*rhs, lhs->constant); + return std::nullopt; + } + case IrOpcode::ShiftLeftLogical32: { + const auto lhs = argument(0); + const auto shift = scalar(1); + if (!lhs || !shift || *shift < 0 || *shift >= 31) return std::nullopt; + return scale(*lhs, 1ll << *shift); + } + case IrOpcode::ShiftRightLogical32: { + const auto lhs = argument(0); + const auto shift = scalar(1); + if (!lhs || !shift) return std::nullopt; + return shiftRight(*lhs, *shift); + } + case IrOpcode::BitwiseAnd32: { + const auto lhs = argument(0); + const auto rhs = argument(1); + if (!lhs || !rhs) return std::nullopt; + if (rhs->Scalar()) return mask(*lhs, rhs->constant); + if (lhs->Scalar()) return mask(*rhs, lhs->constant); + return std::nullopt; + } + case IrOpcode::BitwiseOr32: { + const auto lhs = argument(0); + const auto rhs = argument(1); + if (!lhs || !rhs) return std::nullopt; + if (const auto result = disjoint(*lhs, *rhs)) return result; + return disjoint(*rhs, *lhs); + } + case IrOpcode::BitFieldUExtract: { + const auto source = argument(0); + const auto offset = scalar(1); + const auto count = scalar(2); + 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; + return mask(*shifted, (1ll << *count) - 1); + } + case IrOpcode::SelectU32: { + if (active != nullptr && Implies(active, value->Argument(0))) return argument(1); + const auto lhs = argument(1); + const auto rhs = argument(2); + 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; + auto result = argument(0); + for (std::size_t index = 1; result && index < value->ArgumentCount(); ++index) { + const auto other = argument(index); + if (!other) return std::nullopt; + if (Same(*result, *other)) continue; + result = dynamic ? CommonMultiple(*result, *other) : std::nullopt; + } + return result && (dynamic || !result->dynamic) ? result : std::nullopt; + } + + std::uint32_t vertexCount; + std::int64_t relativeCount; + IrValue* active = nullptr; + std::unordered_map> strictMemo; + std::unordered_map> dynamicMemo; + std::unordered_map independence; +}; + +bool FirstInvocationOnly(IrValue* raw, AddressEvaluator& evaluator, std::unordered_set& path) { + IrValue* value = raw->Resolve(); + if (value->HasImmediate()) return value->Type() == IrType::Bool && !value->ImmediateBool(); + if (!path.insert(value).second) return true; + const auto invocation = [&](std::size_t index) { return evaluator.IsInvocation(value->Argument(index)); }; + const auto immediate = [&](std::size_t index, std::uint32_t expected) { return IsImmediate(value->Argument(index), expected); }; + const bool result = [&] { + switch (value->Opcode()) { + case IrOpcode::LogicalAnd: + return FirstInvocationOnly(value->Argument(0), evaluator, path) || FirstInvocationOnly(value->Argument(1), evaluator, path); + case IrOpcode::ULessThan32: + return invocation(0) && immediate(1, 1u); + case IrOpcode::UGreaterThan32: + return immediate(0, 1u) && invocation(1); + case IrOpcode::ULessThanEqual32: + return invocation(0) && immediate(1, 0u); + case IrOpcode::UGreaterThanEqual32: + return immediate(0, 0u) && invocation(1); + case IrOpcode::IEqual32: + return (invocation(0) && immediate(1, 0u)) || (immediate(0, 0u) && invocation(1)); + default: + break; + } + if (!value->IsPhi() || value->ArgumentCount() == 0) return false; + for (std::size_t index = 0; index < value->ArgumentCount(); ++index) { + if (!FirstInvocationOnly(value->Argument(index), evaluator, path)) return false; + } + return true; + }(); + path.erase(value); + return result; +} + +struct Access { + IrBlock* block = nullptr; + IrValue* inst = nullptr; + TessellationAttribute kind = TessellationAttribute::LocalOutput; + bool write = false; + std::uint32_t components = 1; + Affine address; + IrValue* predicate = nullptr; + std::uint32_t pc = 0; + IrValue* addressValue = nullptr; + std::uint32_t memoryOffset = 0; +}; + +std::uint32_t ComponentCount(IrOpcode opcode) { + switch (opcode) { + case IrOpcode::LoadSharedU32x2: + case IrOpcode::WriteSharedU32x2: + case IrOpcode::LoadBufferU32x2: + case IrOpcode::StoreBufferU32x2: + return 2u; + case IrOpcode::LoadSharedU32x3: + case IrOpcode::WriteSharedU32x3: + case IrOpcode::LoadBufferU32x3: + case IrOpcode::StoreBufferU32x3: + return 3u; + case IrOpcode::LoadSharedU32x4: + case IrOpcode::WriteSharedU32x4: + case IrOpcode::LoadBufferU32x4: + case IrOpcode::StoreBufferU32x4: + return 4u; + case IrOpcode::LoadSharedU32: + case IrOpcode::WriteSharedU32: + case IrOpcode::LoadBufferU32: + case IrOpcode::StoreBufferU32: + return 1u; + default: + return 0u; + } +} + +class Lowering { +public: + 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(); + for (const auto& access : accesses) rewrite(access); + if (stage == IrShaderStage::TessellationEvaluation) removePrimitiveExports(); + constexpr DeadCodeEliminator deadCodeEliminator; + deadCodeEliminator.Eliminate(program); + if (stage == IrShaderStage::TessellationControl) removeUnobservedCycles(); + std::vector relative; + for (IrBlock* block : program.BlockOrder()) { + for (IrValue* inst : block->Instructions()) { + if (stage == IrShaderStage::TessellationControl && inst->Opcode() == IrOpcode::Ballot && inst->HasUses()) Fail("the hull program uses a lane mask other than its own lane's bit", 0u); + if (inst->Opcode() != IrOpcode::TessellationBase || !inst->HasUses()) continue; + if (IsBase(inst, TessellationBaseKind::RelativeIndex)) { + relative.push_back(inst); + continue; + } + if (IsBase(inst, TessellationBaseKind::PassthroughPrimitive)) Fail("the primitive VGPR is used other than as the passthrough primitive export", 0u); + Fail("a tessellation ring base is used outside a lowered ring access", 0u); + } + } + 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: + void lowerOwnLaneBits() { + LaneBits laneBits(program); + for (IrBlock* block : program.BlockOrder()) { + for (IrValue* inst : block->Instructions()) { + const auto words = LaneBitWords(inst, program.WaveSize()); + if (!words) continue; + if (IrValue* bit = laneBits.Of(*words)) inst->ReplaceUsesWith(bit, true); + } + } + ResolveControlFlowIdentities(program); + constexpr DeadCodeEliminator identities; + identities.RemoveIdentities(program); + } + + void collect() { + const auto& memoryInfo = program.Resources().memoryInfo; + for (IrBlock* block : program.BlockOrder()) { + for (IrValue* inst : block->Instructions()) { + const auto opcode = inst->Opcode(); + const auto shared = SharedAccessOf(opcode); + const auto buffer = BufferAccessOf(opcode); + if (shared == SharedAccess::None && buffer == BufferAccess::None) continue; + const auto flags = inst->Flags(); + const auto& memory = memoryInfo.at(flags.index); + Access access{block, inst, TessellationAttribute::LocalOutput, false, ComponentCount(opcode), {}, inst->Argument(inst->ArgumentCount() - 1u), flags.pc}; + IrValue* address = nullptr; + if (shared != SharedAccess::None) { + if (memory.kind != ResourceKind::Lds) continue; + access.write = shared == SharedAccess::Write; + 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 { + Fail("unsupported LDS access in a tessellation stage", flags.pc); + } + address = inst->Argument(0); + } else { + if (memory.kind != ResourceKind::Buffer || (!IsBase(inst->Argument(3), TessellationBaseKind::OffChip) && !IsBase(inst->Argument(3), TessellationBaseKind::Factor))) continue; + if (buffer == BufferAccess::Atomic || access.components == 0u || memory.dataBits != 32u || memory.formatted || memory.typed || memory.d16 || memory.idxen) Fail("only untyped 32-bit tessellation ring accesses are supported", flags.pc); + access.write = buffer == BufferAccess::Write; + const bool factor = IsBase(inst->Argument(3), TessellationBaseKind::Factor); + if (stage == IrShaderStage::TessellationControl && access.write) { + access.kind = factor ? TessellationAttribute::Factor : TessellationAttribute::ControlOutput; + } else if (stage == IrShaderStage::TessellationEvaluation && !access.write && !factor) { + access.kind = TessellationAttribute::EvaluationInput; + } else { + Fail("unsupported tessellation ring access", flags.pc); + } + address = memory.offen ? inst->Argument(2) : nullptr; + } + if (address != nullptr) { + 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; + } + access.memoryOffset = memory.offset; + access.address.constant += memory.offset; + if ((access.address.constant & 3) != 0) Fail("unaligned tessellation ring or LDS address", flags.pc); + accesses.push_back(access); + } + } + } + + void agree(std::uint32_t& stride, std::int64_t value, std::uint32_t pc) const { + if (value <= 0 || (value & 3) != 0 || value > 0xffff || (stride != 0u && stride != value)) Fail("inconsistent tessellation stride", pc); + stride = static_cast(value); + } + + void derive() { + const auto inputs = static_cast(tessellation.inputControlPoints); + const auto outputs = static_cast(tessellation.outputControlPoints); + const auto soleControlPoint = [&](const Access& access) { + const auto end = access.address.constant + 4 * static_cast(access.components); + return outputs == 1 && access.address.vertex == 0 && access.address.relative > 0 && access.address.constant >= 0 && end <= access.address.relative; + }; + for (auto& access : accesses) { + if (access.kind == TessellationAttribute::LocalOutput) agree(localStride, access.address.relative, access.pc); + if (access.kind == TessellationAttribute::ControlInput && access.address.vertex != 0) agree(localStride, access.address.vertex, access.pc); + if (access.kind == TessellationAttribute::ControlInput && (access.address.dynamic || access.address.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; + } + } + } + std::optional recordStride; + std::int64_t patchLowest = AddressLimit; + std::int64_t patchHighest = 0; + const auto record = [&](const Access& access, std::int64_t end) { + if (recordStride && *recordStride != access.address.relative) Fail("inconsistent patch-constant stride", access.pc); + recordStride = access.address.relative; + patchLowest = std::min(patchLowest, access.address.constant); + patchHighest = std::max(patchHighest, end); + }; + for (auto& access : accesses) { + const auto& address = access.address; + const auto end = address.constant + 4 * static_cast(access.components); + switch (access.kind) { + case TessellationAttribute::LocalOutput: + if (address.vertex != 0 || address.constant < 0 || end > localStride) Fail("the local stage writes outside its vertex's LDS slot", access.pc); + break; + case TessellationAttribute::ControlInput: + if (localStride == 0u) Fail("the hull shader reads no LDS per control point, so the local stride is unknown", access.pc); + if ((address.vertex != 0 && address.vertex != localStride) || address.relative != inputs * localStride || address.constant < 0 || end + address.vertex * (outputs - 1) > inputs * localStride) Fail("the hull shader reads LDS outside its patch", access.pc); + if (address.dynamic && (address.dynamicScale == 0 || address.dynamicScale % localStride != 0)) Fail("the hull shader reads LDS at a computed offset that is not a whole number of control points", access.pc); + break; + case TessellationAttribute::ControlOutput: + if (address.vertex == 0 && !soleControlPoint(access)) { + if (controlStride == 0u || evaluator.Lowest(address) < static_cast(tessellation.patchesPerGroup) * outputs * controlStride) Fail("the hull shader writes off-chip memory outside its control points and patch constants", access.pc); + record(access, end); + access.kind = TessellationAttribute::PatchOutput; + requireFirstInvocation(access); + } else if (address.relative != outputs * controlStride || address.constant < 0 || end > controlStride) { + Fail("the hull shader writes off-chip memory outside its control point", access.pc); + } + 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; + const auto factorBytes = quads ? QuadFactorBytes : TriangleFactorBytes; + if ((tessellation.domain != 1u && !quads) || address.vertex != 0 || address.relative != factorBytes || address.constant < 0 || end > factorBytes) Fail(quads ? "the hull shader writes tessellation factors outside its patch's quad factors" : "the hull shader writes tessellation factors outside its patch's triangle factors", access.pc); + requireFirstInvocation(access); + break; + } + case TessellationAttribute::PatchInput: + if (controlStride == 0u) Fail("the domain shader reads patch constants but no control point, so their locations are unknown", access.pc); + if (address.relative == 0) Fail("the domain shader reads off-chip memory that is not indexed by its relative patch", access.pc); + if (evaluator.Lowest(address) < static_cast(tessellation.patchesPerGroup) * outputs * controlStride) Fail("the domain shader reads off-chip memory that is neither its patch's control points nor a patch-constant record", access.pc); + record(access, end); + break; + case TessellationAttribute::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; + } + } + if (!recordStride) return; + if (*recordStride == 0 || std::abs(*recordStride) < patchHighest - patchLowest) Fail(stage == IrShaderStage::TessellationEvaluation ? "the domain shader reads more than one patch-constant record" : "patch-constant records of neighbouring patches overlap", 0u); + patchStride = static_cast(std::abs(*recordStride)); + } + + bool dynamicInputAddress(IrValue* user, IrValue* value) { + if (user->Opcode() != IrOpcode::GetTessellationAttribute || user->Argument(1)->Resolve() != value->Resolve()) return false; + 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); + 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) { + std::vector pending(bases.begin(), bases.end()); + std::unordered_set visited(bases.begin(), bases.end()); + while (!pending.empty()) { + IrValue* value = pending.back(); + pending.pop_back(); + for (IrValue* user : value->Uses()) { + switch (user->Opcode()) { + case IrOpcode::Identity: + case IrOpcode::IAdd32: + case IrOpcode::ISub32: + case IrOpcode::IMul32: + case IrOpcode::ShiftLeftLogical32: + case IrOpcode::ShiftRightLogical32: + case IrOpcode::BitwiseAnd32: + case IrOpcode::BitwiseOr32: + case IrOpcode::BitFieldUExtract: + case IrOpcode::SelectU32: + case IrOpcode::Phi: + if (visited.insert(user).second) pending.push_back(user); + continue; + default: + break; + } + if (dynamicInputAddress(user, value)) continue; + const auto address = evaluator.Evaluate(value, nullptr); + if (!address || address->relative != 0) return true; + } + } + return false; + } + + void requireGroupedPatches() const { + if (stage == IrShaderStage::Local) Fail("the relative patch or vertex index is used outside a tessellation ring or LDS address", 0u); + if (tessellation.primitiveGroupSize == 0u) Fail(stage == IrShaderStage::TessellationControl ? "the hull program uses its relative patch index as data, but GE_CNTL.PRIM_GRP_SIZE is 0 or not programmed" : "the domain program uses its relative patch index as data, but GE_CNTL.PRIM_GRP_SIZE is 0 or not programmed", 0u); + if (tessellation.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) { + const auto emit = [&](IrOpcode opcode, std::initializer_list arguments) -> IrValue& { + IrValue& value = program.CreateValue(opcode, IrType::U32); + for (IrValue* argument : arguments) value.AddArgument(argument); + base.Parent()->InsertInstructionBefore(&base, &value); + return value; + }; + const auto remainder = [&](IrValue& value, std::uint32_t divisor) -> IrValue& { + IrValue& count = constant(divisor); + return emit(IrOpcode::ISub32, {&value, &emit(IrOpcode::IMul32, {&emit(IrOpcode::UDiv32, {&value, &count}), &count})}); + }; + IrValue* primitive = &emit(IrOpcode::GetBuiltin, {&constant(static_cast(StageInputKind::PrimitiveId)), &constant(0u)}); + 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() { + std::unordered_set observed; + std::vector pending; + for (IrBlock* block : program.BlockOrder()) { + for (IrValue* inst : block->Instructions()) { + if (inst->MayHaveSideEffects() && observed.insert(inst).second) pending.push_back(inst); + } + } + while (!pending.empty()) { + IrValue* value = pending.back(); + pending.pop_back(); + for (IrValue* argument : value->Arguments()) { + if (argument != nullptr && observed.insert(argument).second) pending.push_back(argument); + } + } + std::vector unobserved; + for (IrBlock* block : program.BlockOrder()) { + for (IrValue* inst : block->Instructions()) { + if (!observed.contains(inst)) unobserved.push_back(inst); + } + } + for (IrValue* inst : unobserved) { + for (std::size_t index = 0; index < inst->ArgumentCount(); ++index) inst->ReplaceArgument(index, nullptr); + } + for (IrValue* inst : unobserved) { + inst->Parent()->RemoveInstruction(inst); + inst->Invalidate(); + } + } + + void 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; + if (!FirstInvocationOnly(access.predicate, evaluator, path)) Fail("per-patch tessellation output is not written by control point 0 alone", access.pc); + } + + IrValue& constant(std::uint32_t value) { + IrValue& result = program.CreateValue(IrOpcode::Void, IrType::U32); + result.SetImmediateU32(value); + 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); + access.block->InsertInstructionBefore(access.inst, &value); + return value; + } + + IrValue& componentAddress(const Access& access, std::uint32_t 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)); + 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; + if (access.write) { + for (std::uint32_t component = 0; component < access.components; ++component) { + IrValue* data = nullptr; + if (shared) { + data = access.inst->Argument(1u + component); + } else if (access.components == 1u) { + data = access.inst->Argument(4u); + } else { + const auto extract = access.components == 2u ? IrOpcode::CompositeExtractU32x2 : access.components == 3u ? IrOpcode::CompositeExtractU32x3 : IrOpcode::CompositeExtractU32x4; + data = &insert(access, extract, IrType::U32, {access.inst->Argument(4u), &constant(component)}); + } + static_cast(insert(access, IrOpcode::SetTessellationAttribute, IrType::Void, {&kind, &componentAddress(access, component), data, access.predicate})); + } + } else { + std::vector values; + for (std::uint32_t component = 0; component < access.components; ++component) { + values.push_back(&insert(access, IrOpcode::GetTessellationAttribute, IrType::U32, {&kind, &componentAddress(access, component), access.predicate})); + } + IrValue* replacement = values.front(); + if (access.components == 2u) replacement = &insert(access, IrOpcode::CompositeConstructU32x2, IrType::U32x2, {values[0], values[1]}); + if (access.components == 3u) replacement = &insert(access, IrOpcode::CompositeConstructU32x3, IrType::U32x3, {values[0], values[1], values[2]}); + if (access.components == 4u) replacement = &insert(access, IrOpcode::CompositeConstructU32x4, IrType::U32x4, {values[0], values[1], values[2], values[3]}); + access.inst->ReplaceAllUsesWith(replacement); + } + access.block->RemoveInstruction(access.inst); + access.inst->Invalidate(); + } + + void removePrimitiveExports() { + const auto& exports = program.Metadata().exportInfo; + for (IrBlock* block : program.BlockOrder()) { + auto& instructions = block->Instructions(); + for (auto it = instructions.begin(); it != instructions.end();) { + IrValue* inst = *it; + const auto flags = inst->Flags(); + if (inst->Opcode() != IrOpcode::SetAttribute || exports.at(flags.index).kind != ExportTargetKind::Primitive) { + ++it; + continue; + } + const IrValue* data = inst->Argument(0)->Resolve(); + if ((exports.at(flags.index).en & 1u) == 0u || data->Opcode() != IrOpcode::CompositeConstructU32x4 || !IsBase(data->Argument(0), TessellationBaseKind::PassthroughPrimitive)) Fail("the domain shader exports a primitive other than the passthrough primitive", flags.pc); + it = instructions.erase(it); + inst->SetParent(nullptr); + inst->Invalidate(); + } + } + } + + IrProgram& program; + const ShaderTessellationInputInfo& tessellation; + const TessellationTargetLimits* limits; + IrShaderStage stage; + bool merged; + AddressEvaluator evaluator; + std::vector accesses; + 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 TessellationTargetLimits* limits) const { + const auto stage = program.Resources().stage; + if (stage != IrShaderStage::Local && stage != IrShaderStage::TessellationControl && stage != IrShaderStage::TessellationEvaluation) return; + 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 new file mode 100644 index 0000000000..4acc117b3b --- /dev/null +++ b/core/shader/recompiler/PatchSpirvTools.cmake @@ -0,0 +1,168 @@ +get_target_property(spirvToolsSourceDir SPIRV-Tools-opt SOURCE_DIR) +set(spirvToolsRedundancySource "${spirvToolsSourceDir}/redundancy_elimination.cpp") +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${spirvToolsRedundancySource}") +file(READ "${spirvToolsRedundancySource}" spirvToolsRedundancy) + +set(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 [=[ + 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; + } + }); + } + 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}") + +set(spirvToolsPatchedDir "${CMAKE_BINARY_DIR}/3rdparty/SPIRV-Tools/patched") +set(spirvToolsPatchedSource "${spirvToolsPatchedDir}/redundancy_elimination.cpp") +file(MAKE_DIRECTORY "${spirvToolsPatchedDir}") +file(CONFIGURE OUTPUT "${spirvToolsPatchedSource}" CONTENT "${spirvToolsRedundancy}" @ONLY) + +get_target_property(spirvToolsSources SPIRV-Tools-opt SOURCES) +set(spirvToolsPatchedSources) +set(spirvToolsReplacements 0) +foreach(source IN LISTS spirvToolsSources) + get_filename_component(sourcePath "${source}" ABSOLUTE BASE_DIR "${spirvToolsSourceDir}") + if(sourcePath STREQUAL spirvToolsRedundancySource) + list(APPEND spirvToolsPatchedSources "${spirvToolsPatchedSource}") + math(EXPR spirvToolsReplacements "${spirvToolsReplacements} + 1") + else() + list(APPEND spirvToolsPatchedSources "${source}") + endif() +endforeach() +if(NOT spirvToolsReplacements EQUAL 1) + message(FATAL_ERROR "SPIRV-Tools optimizer sources have changed; review the local memory fix") +endif() +set_property(TARGET SPIRV-Tools-opt PROPERTY SOURCES "${spirvToolsPatchedSources}") + +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 4b7fea929a..ae9ec178cf 100644 --- a/core/shader/recompiler/PipelineSpecialization.hpp +++ b/core/shader/recompiler/PipelineSpecialization.hpp @@ -24,17 +24,33 @@ inline constexpr std::uint32_t VertexBase = 4096u; inline constexpr std::uint32_t VertexWords = 6u; inline constexpr std::uint32_t PushDataOffset = 8188u; inline constexpr std::uint32_t ExportBase = 8192u; +inline constexpr std::uint32_t ExportPackingBase = 8224u; +inline constexpr std::uint32_t DualSourceBlend = 8232u; +inline constexpr std::uint32_t RasterPrimitive = 8233u; +inline constexpr std::uint32_t LayerExported = 8234u; +inline constexpr std::uint32_t 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; inline constexpr std::uint32_t DescriptorIndex(std::uint32_t binding, std::uint32_t element) { 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/RdnaInstruction.hpp b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaInstruction.hpp index 1c75988153..85dc6da282 100644 --- a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaInstruction.hpp +++ b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaInstruction.hpp @@ -4,10 +4,16 @@ #include "RdnaDecoder/RdnaOpcode.hpp" #include #include +#include #include namespace ShaderRecompiler { +class UnsupportedInstructionError : public std::runtime_error { +public: + using std::runtime_error::runtime_error; +}; + enum class RdnaInstructionFamily { Unknown, SOP1, @@ -47,7 +53,11 @@ enum class RdnaOperandKind { VccZ, ExecZ, M0, - PopsExitingWaveId + PopsExitingWaveId, + SrcSharedBase, + SrcSharedLimit, + SrcPrivateBase, + SrcPrivateLimit }; enum class RdnaImageDimension : std::uint32_t { diff --git a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaInstructionDecoder.hpp b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaInstructionDecoder.hpp index a03a46841e..9456f38cd1 100644 --- a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaInstructionDecoder.hpp +++ b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaInstructionDecoder.hpp @@ -8,6 +8,9 @@ namespace ShaderRecompiler { +inline constexpr std::uint32_t SharedApertureHigh = 0x80000000u; +inline constexpr std::uint32_t PrivateApertureHigh = 0x70000000u; + class RdnaInstructionDecoder { public: [[nodiscard]] RdnaProgram Decode(std::span code) const; @@ -23,6 +26,7 @@ void DecodeRdnaProgram(std::span code, RdnaProgram& program RdnaOperand DecodeRdnaScalarSource(std::uint32_t code, std::uint32_t programCounter); RdnaOperand DecodeRdnaScalarDestination(std::uint32_t code, std::uint32_t programCounter); RdnaOperand DecodeRdnaVectorGpr(std::uint32_t reg); +std::uint64_t RdnaApertureSourceValue(const RdnaOperand& operand); void ReadRdnaLiteralOperands(std::span code, std::uint32_t wordIndex, RdnaInstruction& instruction); void SetRdnaRawWords(RdnaInstruction& instruction, std::span code, std::uint32_t wordIndex, std::uint32_t wordCount); void SetRdnaUnsupported(RdnaInstruction& instruction, RdnaInstructionFamily family, std::uint32_t opcodeId, const char* reason); diff --git a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp index bdffac7f0b..0b83c3ae6d 100644 --- a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp +++ b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp @@ -70,9 +70,6 @@ enum class RdnaOpcode : std::uint16_t { VAndB32, VOrB32, VXorB32, - VLshlB32, - VLshrB32, - VAshrI32, VCmpEqF32, VCmpLtF32, VCmpGtF32, diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp index 7cefea341a..c3d1a68701 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp @@ -13,9 +13,9 @@ struct FormatInfo { bool sint32; }; -constexpr std::array kFormatInfoTable {{ +constexpr std::array kFormatInfoTable {{ {IrBufferFormat::Format8UNorm, true, false, false}, - {IrBufferFormat::Format8SNorm, false, false, false}, + {IrBufferFormat::Format8SNorm, true, false, false}, {IrBufferFormat::Format8UInt, true, true, false}, {IrBufferFormat::Format8SInt, true, false, true}, {IrBufferFormat::Format16UNorm, true, false, false}, @@ -40,7 +40,9 @@ constexpr std::array kFormatInfoTable {{ {IrBufferFormat::Format11_11_10Float, true, false, false}, {IrBufferFormat::Format10_11_11Float, true, true, false}, {IrBufferFormat::Format10_10_10_2UNorm, true, false, false}, + {IrBufferFormat::Format10_10_10_2SNorm, true, false, false}, {IrBufferFormat::Format10_10_10_2UInt, true, true, false}, + {IrBufferFormat::Format10_10_10_2SInt, true, false, true}, {IrBufferFormat::Format8_8_8_8UNorm, true, false, false}, {IrBufferFormat::Format8_8_8_8SNorm, true, false, false}, {IrBufferFormat::Format8_8_8_8UInt, true, true, false}, @@ -144,6 +146,7 @@ IrTextureNumericClass VertexInputNumericClass(IrBufferFormat format) { case IrBufferFormat::Format8_8UInt: case IrBufferFormat::Format32UInt: case IrBufferFormat::Format16_16UInt: + case IrBufferFormat::Format10_10_10_2UInt: case IrBufferFormat::Format8_8_8_8UInt: case IrBufferFormat::Format32_32UInt: case IrBufferFormat::Format16_16_16_16UInt: @@ -154,6 +157,7 @@ IrTextureNumericClass VertexInputNumericClass(IrBufferFormat format) { case IrBufferFormat::Format8_8SInt: case IrBufferFormat::Format32SInt: case IrBufferFormat::Format16_16SInt: + case IrBufferFormat::Format10_10_10_2SInt: case IrBufferFormat::Format8_8_8_8SInt: case IrBufferFormat::Format32_32SInt: case IrBufferFormat::Format16_16_16_16SInt: diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaExportOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaExportOpDecoder.cpp index 89e5645f70..24e45182ba 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaExportOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaExportOpDecoder.cpp @@ -14,7 +14,7 @@ RdnaInstruction DecodeRdnaExportOp(std::span code, std::uin RdnaInstruction DecodeRdnaExportOp(std::uint32_t programCounter, std::span code, std::uint32_t wordIndex) { const std::size_t index = wordIndex; if (index >= code.size() || code.size() - index < 2u) { - throw std::runtime_error("truncated export instruction"); + throw std::out_of_range("truncated export instruction"); } if (programCounter % 4u != 0u || programCounter > std::numeric_limits::max() - 7u) { throw std::runtime_error("invalid export program counter"); diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp index 640be3b59a..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}, @@ -162,7 +163,9 @@ const ImageOpcodeInfo& lookupOpcode(std::uint32_t opcode) { return entry; } } - throw std::runtime_error("unsupported MIMG opcode"); + char message[48]; + std::snprintf(message, sizeof(message), "unsupported MIMG opcode 0x%02x", opcode); + throw UnsupportedInstructionError(message); } void validateFlags(std::uint32_t flags) { @@ -323,7 +326,7 @@ RdnaInstruction DecodeRdnaImageOp(std::span code, std::uint RdnaInstruction DecodeRdnaMimg(std::uint32_t programCounter, std::span code, std::uint32_t wordIndex) { const std::size_t index = wordIndex; if (index >= code.size() || code.size() - index < 2u) { - throw std::runtime_error("truncated MIMG instruction"); + throw std::out_of_range("truncated MIMG instruction"); } const auto word0 = code[index]; const auto word1 = code[index + 1u]; @@ -343,7 +346,7 @@ RdnaInstruction DecodeRdnaMimg(std::uint32_t programCounter, std::span> 1u) & 3u; const auto wordCount = 2u + nsa; if (code.size() - index < wordCount) { - throw std::runtime_error("truncated MIMG NSA payload"); + throw std::out_of_range("truncated MIMG NSA payload"); } if (programCounter % 4u != 0u || programCounter > std::numeric_limits::max() - (wordCount * 4u - 1u)) { throw std::runtime_error("invalid MIMG program counter"); @@ -415,7 +418,7 @@ RdnaInstruction DecodeRdnaMimg(std::uint32_t programCounter, std::span> 16u) & 31u) * 4u; const auto sampler = ((word1 >> 21u) & 31u) * 4u; const bool r128 = (word0 & 0x8000u) != 0u; - if (resource + (r128 ? 4u : 8u) > 106u || ((info.sample || info.gather || opcode == 0x60u) && sampler + 4u > 106u)) { + if (resource + (r128 ? 4u : 8u) > 106u || ((info.sample || info.gather || opcode == 0x60u) && sampler > 104u)) { throw std::runtime_error("MIMG descriptor register range overflow"); } RdnaInstruction instruction{}; diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp index f039446736..abdb151c23 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp @@ -217,6 +217,10 @@ RdnaOperand DecodeRdnaScalarSource(std::uint32_t code, std::uint32_t programCoun case 125u: operand.kind = RdnaOperandKind::Null; return operand; case 126u: operand.kind = RdnaOperandKind::ExecLo; return operand; case 127u: operand.kind = RdnaOperandKind::ExecHi; return operand; + case 235u: operand.kind = RdnaOperandKind::SrcSharedBase; return operand; + case 236u: operand.kind = RdnaOperandKind::SrcSharedLimit; return operand; + case 237u: operand.kind = RdnaOperandKind::SrcPrivateBase; return operand; + case 238u: operand.kind = RdnaOperandKind::SrcPrivateLimit; return operand; case 239u: operand.kind = RdnaOperandKind::PopsExitingWaveId; return operand; case 248u: operand.kind = RdnaOperandKind::FloatInlineConstant; @@ -296,6 +300,16 @@ void SetRdnaUnsupported(RdnaInstruction& instruction, RdnaInstructionFamily fami throw std::invalid_argument("unsupported RDNA instruction, family=" + std::to_string(static_cast(family)) + " opcode=" + toHexString(opcodeId) + " reason=" + reason + " pc=" + toHexString(instruction.programCounter)); } +std::uint64_t RdnaApertureSourceValue(const RdnaOperand& operand) { + switch (operand.kind) { + case RdnaOperandKind::SrcSharedBase: return std::uint64_t{SharedApertureHigh} << 32u; + case RdnaOperandKind::SrcSharedLimit: return (std::uint64_t{SharedApertureHigh} << 32u) | 0xffffffffu; + case RdnaOperandKind::SrcPrivateBase: return std::uint64_t{PrivateApertureHigh} << 32u; + case RdnaOperandKind::SrcPrivateLimit: return (std::uint64_t{PrivateApertureHigh} << 32u) | 0xffffffffu; + default: throw std::invalid_argument("RdnaApertureSourceValue: " + RdnaOperandToString(operand) + " is not a scalar aperture source"); + } +} + std::string RdnaOperandToString(const RdnaOperand& operand) { switch (operand.kind) { case RdnaOperandKind::None: return "none"; @@ -315,6 +329,10 @@ std::string RdnaOperandToString(const RdnaOperand& operand) { case RdnaOperandKind::ExecZ: return "execz"; case RdnaOperandKind::M0: return "m0"; case RdnaOperandKind::PopsExitingWaveId: return "pops_exiting_wave_id"; + case RdnaOperandKind::SrcSharedBase: return "src_shared_base"; + case RdnaOperandKind::SrcSharedLimit: return "src_shared_limit"; + case RdnaOperandKind::SrcPrivateBase: return "src_private_base"; + case RdnaOperandKind::SrcPrivateLimit: return "src_private_limit"; } throw std::invalid_argument("unsupported operand kind for string conversion"); diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp index e501c2529e..98950f77cb 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp @@ -436,6 +436,10 @@ RdnaOperand scalarSource(std::uint32_t code) { case 125u: operand.kind = RdnaOperandKind::Null; return operand; case 126u: operand.kind = RdnaOperandKind::ExecLo; return operand; case 127u: operand.kind = RdnaOperandKind::ExecHi; return operand; + case 235u: operand.kind = RdnaOperandKind::SrcSharedBase; return operand; + case 236u: operand.kind = RdnaOperandKind::SrcSharedLimit; return operand; + case 237u: operand.kind = RdnaOperandKind::SrcPrivateBase; return operand; + case 238u: operand.kind = RdnaOperandKind::SrcPrivateLimit; return operand; case 239u: operand.kind = RdnaOperandKind::PopsExitingWaveId; return operand; case 248u: operand.kind = RdnaOperandKind::FloatInlineConstant; operand.value = floatBits(0.15915494309189535f); return operand; case 251u: operand.kind = RdnaOperandKind::VccZ; return operand; @@ -465,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; } @@ -696,7 +703,7 @@ void setRawWords(RdnaInstruction& instruction, std::span co void requireTwoWords(std::span code, std::uint32_t wordIndex, std::uint32_t programCounter, const char* reason) { const std::size_t index = wordIndex; if (index >= code.size() || code.size() - index < 2u) { - throw std::runtime_error(reason); + throw std::out_of_range(reason); } if (programCounter % 4u != 0u || programCounter > std::numeric_limits::max() - 7u) { throw std::runtime_error("invalid memory instruction program counter"); @@ -1039,7 +1046,7 @@ RdnaInstruction DecodeRdnaDs(std::uint32_t programCounter, std::span code, std::uint32_t wordIndex) { const auto programCounter = toProgramCounter(wordIndex); if (static_cast(wordIndex) >= code.size()) { - throw std::runtime_error("truncated memory instruction"); + throw std::out_of_range("truncated memory instruction"); } switch (code[wordIndex] >> 26u) { case 0x36u: return DecodeRdnaDs(programCounter, code, wordIndex); diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaOpcode.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaOpcode.cpp index 79affa1cd2..374315cb1a 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaOpcode.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaOpcode.cpp @@ -183,9 +183,6 @@ bool IsVectorAluOpcode(RdnaOpcode opcode) { case RdnaOpcode::VAndB32: case RdnaOpcode::VOrB32: case RdnaOpcode::VXorB32: - case RdnaOpcode::VLshlB32: - case RdnaOpcode::VLshrB32: - case RdnaOpcode::VAshrI32: case RdnaOpcode::VCmpEqF32: case RdnaOpcode::VCmpLtF32: case RdnaOpcode::VCmpGtF32: diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp index 02abfe5a4b..c85b8d5636 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp @@ -74,7 +74,7 @@ RdnaOpcode decodeSop1Opcode(std::uint32_t opcode) { case 0x46u: return RdnaOpcode::SAndn1WrexecB32; case 0x47u: return RdnaOpcode::SAndn2WrexecB32; case 0x49u: return RdnaOpcode::SMovrelsd2B32; - default: throw std::invalid_argument("unsupported SOP1 opcode " + std::to_string(opcode)); + default: throw UnsupportedInstructionError("unsupported SOP1 opcode " + std::to_string(opcode)); } } @@ -131,7 +131,7 @@ RdnaOpcode decodeSop2Opcode(std::uint32_t opcode) { case 0x34u: return RdnaOpcode::SPackHhB32B16; case 0x35u: return RdnaOpcode::SMulHiU32; case 0x36u: return RdnaOpcode::SMulHiI32; - default: throw std::invalid_argument("unsupported SOP2 opcode " + std::to_string(opcode)); + default: throw UnsupportedInstructionError("unsupported SOP2 opcode " + std::to_string(opcode)); } } @@ -155,7 +155,7 @@ RdnaOpcode decodeSopcOpcode(std::uint32_t opcode) { case 0x0fu: return RdnaOpcode::SBitcmp1B64; case 0x12u: return RdnaOpcode::SCmpEqU64; case 0x13u: return RdnaOpcode::SCmpLgU64; - default: throw std::invalid_argument("unsupported SOPC opcode " + std::to_string(opcode)); + default: throw UnsupportedInstructionError("unsupported SOPC opcode " + std::to_string(opcode)); } } @@ -188,7 +188,7 @@ RdnaOpcode decodeSopkOpcode(std::uint32_t opcode) { case 0x1au: return RdnaOpcode::SWaitcnt; case 0x1bu: return RdnaOpcode::SSubvectorLoopBegin; case 0x1cu: return RdnaOpcode::SSubvectorLoopEnd; - default: throw std::invalid_argument("unsupported SOPK opcode " + std::to_string(opcode)); + default: throw UnsupportedInstructionError("unsupported SOPK opcode " + std::to_string(opcode)); } } @@ -231,7 +231,7 @@ RdnaOpcode decodeSoppOpcode(std::uint32_t opcode) { case 0x24u: return RdnaOpcode::SRoundMode; case 0x25u: return RdnaOpcode::SDenormMode; case 0x28u: return RdnaOpcode::STtracedata; - default: throw std::invalid_argument("unsupported SOPP opcode " + std::to_string(opcode)); + default: throw UnsupportedInstructionError("unsupported SOPP opcode " + std::to_string(opcode)); } } diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp index 326343f038..02ccb674b8 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp @@ -58,11 +58,8 @@ constexpr Vop2OpcodeInfo vop2Opcodes[] = { {0x12u, RdnaOpcode::VMaxI32, Vop2SdwaProfile::Integer}, {0x13u, RdnaOpcode::VMinU32, Vop2SdwaProfile::Integer}, {0x14u, RdnaOpcode::VMaxU32, Vop2SdwaProfile::Integer}, - {0x15u, RdnaOpcode::VLshrB32}, {0x16u, RdnaOpcode::VLshrrevB32, Vop2SdwaProfile::Integer}, - {0x17u, RdnaOpcode::VAshrI32}, {0x18u, RdnaOpcode::VAshrrevI32, Vop2SdwaProfile::Integer}, - {0x19u, RdnaOpcode::VLshlB32}, {0x1au, RdnaOpcode::VLshlrevB32, Vop2SdwaProfile::Integer}, {0x1bu, RdnaOpcode::VAndB32, Vop2SdwaProfile::Integer}, {0x1cu, RdnaOpcode::VOrB32, Vop2SdwaProfile::Integer}, @@ -71,9 +68,6 @@ constexpr Vop2OpcodeInfo vop2Opcodes[] = { {0x1fu, RdnaOpcode::VMacF32}, {0x20u, RdnaOpcode::VMadmkF32}, {0x21u, RdnaOpcode::VMadakF32}, - {0x22u, RdnaOpcode::VBcntU32B32}, - {0x23u, RdnaOpcode::VMbcntLoU32B32}, - {0x24u, RdnaOpcode::VMbcntHiU32B32}, {0x25u, RdnaOpcode::VAddNcU32, Vop2SdwaProfile::Integer}, {0x28u, RdnaOpcode::VAddcU32, Vop2SdwaProfile::Integer}, {0x29u, RdnaOpcode::VSubCoCiU32, Vop2SdwaProfile::Integer}, @@ -535,6 +529,9 @@ constexpr VectorOpcodeInfo vop3Opcodes[] = { {0x311u, RdnaOpcode::VPackB32F16}, {0x314u, RdnaOpcode::VLshlrevB16}, {0x319u, RdnaOpcode::VSubrevI32}, + {0x200u, RdnaOpcode::VInterpP1F32}, + {0x201u, RdnaOpcode::VInterpP2F32}, + {0x202u, RdnaOpcode::VInterpMovF32}, {0x342u, RdnaOpcode::VInterpP1llF16}, {0x343u, RdnaOpcode::VInterpP1lvF16}, {0x344u, RdnaOpcode::VPermB32}, @@ -630,11 +627,11 @@ RdnaOpcode lookupVectorOpcode(const TEntry (&table)[Size], std::uint32_t encodin } bool isVop2LiteralMadOpcode(std::uint32_t opcode) { - return opcode == 0x20u || opcode == 0x21u || opcode == 0x2cu || opcode == 0x2du; + return opcode == 0x20u || opcode == 0x21u || opcode == 0x2cu || opcode == 0x2du || opcode == 0x37u || opcode == 0x38u; } bool isUnsupportedVop3EncodedVop2Alias(std::uint32_t opcode) { - return isVop2LiteralMadOpcode(opcode) || opcode == 0x02u || opcode == 0x0du; + return isVop2LiteralMadOpcode(opcode) || opcode == 0x02u || opcode == 0x0du || opcode == 0x3cu; } bool isVop3EncodedVopc(std::uint32_t opcode) { @@ -729,7 +726,6 @@ bool hasDppForm(RdnaOpcode opcode) { bool acceptsDpp8(RdnaOpcode opcode) { switch (opcode) { - case RdnaOpcode::VPkFmacF16: case RdnaOpcode::VMovrelsB32: case RdnaOpcode::VMovrelsdB32: case RdnaOpcode::VMovrelsd2B32: @@ -744,6 +740,10 @@ bool isNativeVop3F16TernaryOpcode(RdnaOpcode opcode) { opcode == RdnaOpcode::VFmaF16; } +bool isNativeVop3F16PackOpcode(RdnaOpcode opcode) { + return opcode == RdnaOpcode::VPackB32F16 || opcode == RdnaOpcode::VCvtPknormI16F16 || opcode == RdnaOpcode::VCvtPknormU16F16; +} + bool isNativeVop3I16TernaryOpcode(RdnaOpcode opcode) { return opcode == RdnaOpcode::VMed3I16 || opcode == RdnaOpcode::VMed3U16 || opcode == RdnaOpcode::VMax3I16 || opcode == RdnaOpcode::VMax3U16 || opcode == RdnaOpcode::VMin3I16 || opcode == RdnaOpcode::VMin3U16 || @@ -757,6 +757,9 @@ bool isNativeVop3AlignOpcode(RdnaOpcode opcode) { bool usesIntegerSaturateClamp(RdnaOpcode opcode) { switch (opcode) { + case RdnaOpcode::VAddNcU32: + case RdnaOpcode::VSubNcU32: + case RdnaOpcode::VSubrevNcU32: case RdnaOpcode::VAddNcI32: case RdnaOpcode::VSubNcI32: case RdnaOpcode::VMadU32U24: @@ -782,6 +785,10 @@ bool isCubeOpcode(RdnaOpcode opcode) { opcode == RdnaOpcode::VCubemaF32; } +bool isVop3Integer16AddSubOpcode(RdnaOpcode opcode) { + return opcode == RdnaOpcode::VAddNcU16 || opcode == RdnaOpcode::VSubNcU16 || opcode == RdnaOpcode::VAddNcI16 || opcode == RdnaOpcode::VSubNcI16; +} + bool isNativeVop3B16BinaryOpcode(RdnaOpcode opcode) { switch (opcode) { case RdnaOpcode::VAddNcU16: @@ -1119,15 +1126,18 @@ constexpr Vop1SdwaRule vop1SdwaRules[] = { {RdnaOpcode::VCvtF32U32, sdwaSelAll(), 0, 0, false}, {RdnaOpcode::VCvtF32I32, sdwaSelAll(), 0, 0, false}, {RdnaOpcode::VCvtF32Ubyte0, sdwaSelAll(), 0, 0, false}, - {RdnaOpcode::VNotB32, sdwaSelAll(), 0, 0, false}, + {RdnaOpcode::VCvtF32Ubyte1, sdwaSelAll(), 0, 0, false}, + {RdnaOpcode::VCvtF32Ubyte2, sdwaSelAll(), 0, 0, false}, + {RdnaOpcode::VCvtF32Ubyte3, sdwaSelAll(), 0, 0, false}, + {RdnaOpcode::VNotB32, sdwaSelAll(), sdwaSelBytes() | sdwaSelWords(), sdwaSelAll(), false}, {RdnaOpcode::VFfblB32, sdwaSelAll(), 0, 0, false}, {RdnaOpcode::VFfbhU32, sdwaSelAll(), 0, 0, false}, {RdnaOpcode::VBfrevB32, sdwaSelAll(), 0, 0, false}, {RdnaOpcode::VCvtF32F16, sdwaSelWords() | sdwaSelFull(), 0, 0, true}, {RdnaOpcode::VCvtF16F32, sdwaSelAll(), sdwaSelWords(), sdwaSelAll(), true}, - {RdnaOpcode::VCvtF16U16, sdwaSelWords() | sdwaSelFull(), sdwaSelWords(), sdwaSelWords() | sdwaSelFull(), false}, + {RdnaOpcode::VCvtF16U16, sdwaSelAll(), sdwaSelWords(), sdwaSelAll(), false}, {RdnaOpcode::VCvtU16F16, sdwaSelWords() | sdwaSelFull(), sdwaSelWords(), sdwaSelWords() | sdwaSelFull(), true}, - {RdnaOpcode::VCvtF16I16, sdwaSelWords() | sdwaSelFull(), sdwaSelWords(), sdwaSelWords() | sdwaSelFull(), false}, + {RdnaOpcode::VCvtF16I16, sdwaSelAll(), sdwaSelWords(), sdwaSelAll(), false}, {RdnaOpcode::VCvtI16F16, sdwaSelWords() | sdwaSelFull(), sdwaSelWords(), sdwaSelWords() | sdwaSelFull(), true}, {RdnaOpcode::VRcpF16, sdwaSelWords() | sdwaSelFull(), sdwaSelWords(), sdwaSelWords() | sdwaSelFull(), true}, {RdnaOpcode::VSqrtF16, sdwaSelWords() | sdwaSelFull(), sdwaSelWords(), sdwaSelWords() | sdwaSelFull(), true}, @@ -1861,7 +1871,7 @@ void applyNativeVop3B16BinaryModifiers(RdnaInstruction& instruction, std::uint32 instruction.destination.sdwaSel = (opSel & 0x8u) != 0u ? 5u : 4u; } -void applyNativeVop3PackB32F16Modifiers(RdnaInstruction& instruction, std::uint32_t opSel, std::uint32_t abs, std::uint32_t neg) { +void applyNativeVop3F16PackModifiers(RdnaInstruction& instruction, std::uint32_t opSel, std::uint32_t abs, std::uint32_t neg) { instruction.source0.opSel = (opSel & 0x1u) != 0u; instruction.source1.opSel = (opSel & 0x2u) != 0u; instruction.source0.negate = (neg & 0x1u) != 0u; @@ -1925,7 +1935,7 @@ bool supportsNativeVop3SourceModifiers(RdnaOpcode opcode) { } bool supportsNativeVop3ResultModifiers(RdnaOpcode opcode) { - if (isVop1FloatResultOpcode(opcode)) { + if (isVop1FloatResultOpcode(opcode) || isNativeVop3F16TernaryOpcode(opcode) || isCubeOpcode(opcode)) { return true; } switch (opcode) { @@ -1939,7 +1949,6 @@ bool supportsNativeVop3ResultModifiers(RdnaOpcode opcode) { case RdnaOpcode::VMacF32: case RdnaOpcode::VMadF32: case RdnaOpcode::VFmaF32: - case RdnaOpcode::VFmaF16: case RdnaOpcode::VAddF16: case RdnaOpcode::VSubF16: case RdnaOpcode::VSubrevF16: @@ -1981,7 +1990,7 @@ bool supportsNativeVop3Clamp(RdnaOpcode opcode) { return true; } return supportsNativeVop3ResultModifiers(opcode) || usesInexactClampControl(opcode) || usesIntegerSaturateClamp(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) { @@ -2007,7 +2016,7 @@ void checkNativeVop3Modifiers(RdnaOpcode opcode, bool permlane, bool carryInOut, return; } if (isNativeVop3B16BinaryOpcode(opcode)) { - if (abs != 0u || clamp != 0u || omod != 0u || neg != 0u) { + if (abs != 0u || (clamp != 0u && !isVop3Integer16AddSubOpcode(opcode)) || omod != 0u || neg != 0u) { throw std::invalid_argument("VOP3 source modifiers are not implemented"); } return; @@ -2015,7 +2024,7 @@ void checkNativeVop3Modifiers(RdnaOpcode opcode, bool permlane, bool carryInOut, if (carryInOut || scalarDst) { const bool floatCompare = scalarDst && isVopcFloatCompareOpcode(opcode); const bool divScale = opcode == RdnaOpcode::VDivScaleF32 || opcode == RdnaOpcode::VDivScaleF64; - if ((clamp != 0u && !isVop3BMadU64Opcode(opcode)) || (omod != 0u && !divScale) || (neg != 0u && !floatCompare && !divScale)) { + if ((clamp != 0u && !isVop3BMadU64Opcode(opcode) && !floatCompare) || (omod != 0u && !divScale) || (neg != 0u && !floatCompare && !divScale)) { throw std::invalid_argument("VOP3 source modifiers are not implemented"); } return; @@ -2050,7 +2059,7 @@ void checkNativeVop3Modifiers(RdnaOpcode opcode, bool permlane, bool carryInOut, } return; } - if (opcode == RdnaOpcode::VPackB32F16) { + if (isNativeVop3F16PackOpcode(opcode)) { if ((abs & ~0x3u) != 0u || (opSel & ~0x3u) != 0u || clamp != 0u || omod != 0u || (neg & ~0x3u) != 0u) { throw std::invalid_argument("VOP3 source modifiers are not implemented"); } @@ -2059,8 +2068,9 @@ void checkNativeVop3Modifiers(RdnaOpcode opcode, bool permlane, bool carryInOut, const bool sourceModifiers = supportsNativeVop3SourceModifiers(opcode); const bool resultModifiers = supportsNativeVop3ResultModifiers(opcode); const bool clampModifier = supportsNativeVop3Clamp(opcode); + const bool ignoresOutputModifiers = opcode == RdnaOpcode::VCvtI16F16; if (sourceModifiers) { - if (opSel != 0u || (omod != 0u && !resultModifiers) || (clamp != 0u && !clampModifier)) { + if (opSel != 0u || (omod != 0u && !resultModifiers && !ignoresOutputModifiers) || (clamp != 0u && !clampModifier && !ignoresOutputModifiers)) { throw std::invalid_argument("VOP3 source modifiers are not implemented"); } return; @@ -2083,7 +2093,8 @@ bool isVop3F16InterpolationOpcode(RdnaOpcode opcode) { void decodeVop3F16Interpolation(RdnaInstruction& instruction, std::uint32_t vdst, std::uint32_t src0, std::uint32_t src1, std::uint32_t src2, std::uint32_t abs, std::uint32_t opSel, std::uint32_t clamp, std::uint32_t omod, std::uint32_t neg) { const bool hasSource2 = instruction.op != RdnaOpcode::VInterpP1llF16; const bool f16Result = instruction.op == RdnaOpcode::VInterpP2F16; - if ((abs & 0x1u) != 0u || (neg & 0x1u) != 0u || opSel != 0u || (f16Result && omod != 0u) || (!hasSource2 && ((abs | neg) & 0x4u) != 0u)) { + const std::uint32_t resultHalf = f16Result ? 0x8u : 0x0u; + if ((abs & 0x1u) != 0u || (neg & 0x1u) != 0u || (opSel & ~resultHalf) != 0u || (f16Result && omod != 0u) || (!hasSource2 && ((abs | neg) & 0x4u) != 0u)) { throw std::invalid_argument("VOP3 interpolation modifiers are not implemented"); } if (src1 < 256u || (hasSource2 && src2 < 256u)) { @@ -2093,6 +2104,7 @@ void decodeVop3F16Interpolation(RdnaInstruction& instruction, std::uint32_t vdst instruction.destination = DecodeRdnaVectorGpr(vdst); instruction.destination.clamp = clamp != 0u; instruction.destination.omod = omod; + if ((opSel & resultHalf) != 0u) instruction.destination.sdwaSel = 5u; instruction.source0 = DecodeRdnaVectorGpr(src1 - 256u); instruction.source0.absolute = (abs & 0x2u) != 0u; instruction.source0.negate = (neg & 0x2u) != 0u; @@ -2113,6 +2125,47 @@ void decodeVop3F16Interpolation(RdnaInstruction& instruction, std::uint32_t vdst } } +bool isVop3F32InterpolationOpcode(RdnaOpcode opcode) { + return opcode == RdnaOpcode::VInterpP1F32 || opcode == RdnaOpcode::VInterpP2F32 || opcode == RdnaOpcode::VInterpMovF32; +} + +void decodeVop3F32Interpolation(RdnaInstruction& instruction, std::uint32_t vdst, std::uint32_t src0, std::uint32_t src1, std::uint32_t src2, std::uint32_t abs, std::uint32_t opSel, std::uint32_t clamp, std::uint32_t omod, std::uint32_t neg) { + const bool moves = instruction.op == RdnaOpcode::VInterpMovF32; + const RdnaOperand thirdSource = DecodeRdnaScalarSource(src2, instruction.programCounter); + if (thirdSource.kind != RdnaOperandKind::IntegerInlineConstant || thirdSource.value != 0u) { + throw std::invalid_argument("VOP3 interpolation third source is not the inline constant 0"); + } + if ((abs & 0x5u) != 0u || (neg & 0x5u) != 0u || opSel != 0u || (src0 >> 8u) != 0u || (moves && (abs | neg) != 0u)) { + throw std::invalid_argument("VOP3 interpolation modifiers are not implemented"); + } + if (moves) { + if (src1 >= 3u) { + throw std::invalid_argument("VOP3 interpolation source is not an interpolation parameter"); + } + } else if (src1 < 256u) { + throw std::invalid_argument("VOP3 interpolation source is not a vector register"); + } + instruction.destination = DecodeRdnaVectorGpr(vdst); + instruction.destination.clamp = clamp != 0u; + instruction.destination.omod = omod; + if (moves) { + instruction.source0.kind = RdnaOperandKind::IntegerInlineConstant; + instruction.source0.value = src1; + instruction.source0.signedVal = static_cast(src1); + } else { + instruction.source0 = DecodeRdnaVectorGpr(src1 - 256u); + instruction.source0.absolute = (abs & 0x2u) != 0u; + instruction.source0.negate = (neg & 0x2u) != 0u; + } + instruction.source1.kind = RdnaOperandKind::IntegerInlineConstant; + instruction.source1.value = src0 & 0x3fu; + instruction.source1.signedVal = static_cast(instruction.source1.value); + instruction.source2.kind = RdnaOperandKind::IntegerInlineConstant; + instruction.source2.value = (src0 >> 6u) & 0x3u; + instruction.source2.signedVal = static_cast(instruction.source2.value); + instruction.sourceCount = 3; +} + void applyNativeVop3SourceModifiers(RdnaInstruction& instruction, std::uint32_t abs, std::uint32_t neg) { RdnaOperand* sources[] = {&instruction.source0, &instruction.source1, &instruction.source2}; for (std::uint32_t i = 0; i < instruction.sourceCount && i < 3u; ++i) { @@ -2277,6 +2330,10 @@ RdnaInstruction DecodeRdnaVop3(std::uint32_t programCounter, std::span +#include +#include #include #include #include @@ -7,11 +10,14 @@ #include "CacheKey.hpp" #include "CompiledVariant.hpp" #include "VertexInputSpecialization.hpp" +#include "FragmentOutputSpecialization.hpp" #include "SpirvBackend/SpirvSpecialization.hpp" #include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include "ShaderDiskCache.hpp" #include +#include #include +#include #include #include #include @@ -24,16 +30,21 @@ #include "Optimization/include/Optimization/BindingAllocator.hpp" #include "Optimization/include/Optimization/ConstantFolder.hpp" #include "Optimization/include/Optimization/DeadCodeEliminator.hpp" +#include "Optimization/include/Optimization/DenormalFlushEliminator.hpp" #include "Optimization/include/Optimization/DescriptorBindingBuilder.hpp" +#include "Optimization/include/Optimization/HostInterpolationChecker.hpp" #include "Optimization/include/Optimization/MaskedSelectEliminator.hpp" #include "Optimization/include/Optimization/ReadLaneEliminator.hpp" +#include "Optimization/include/Optimization/RedundantValueEliminator.hpp" #include "Optimization/include/Optimization/RequestMemoryView.hpp" #include "Optimization/include/Optimization/ResourceMaterializer.hpp" #include "Optimization/ResourceProgram.hpp" #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" +#include "Optimization/include/Optimization/TessellationLowering.hpp" #include "SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp" #if ANYPS5_ENABLE_SPIRV_TOOLS #include "SpirvBackend/SpirvOptimizer.hpp" @@ -76,9 +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; @@ -89,30 +108,123 @@ std::uint32_t HostSubgroupSize(const RecompileRequest& request) { return request.target.subgroupSize; } +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); + 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; + return bytes + 4u > request.target.maxWorkgroupSharedMemoryBytes ? static_cast(bytes) : 0u; +} + +std::atomic frontEndBuilds{0}; + +} + +struct PreparedControlFlow { + PreparedControlFlow(const RecompileRequest& request, const SwappcInfo& swappcInfo) + : 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); + } + + PreparedControlFlow(const PreparedControlFlow&) = delete; + PreparedControlFlow& operator=(const PreparedControlFlow&) = delete; + + bool Matches(const RecompileRequest& request, const SwappcInfo& swappcInfo) const { + return stage == request.shader.stage && mergedHull == MergedHullStage(request) && swappc.fetchCallAllowed == swappcInfo.fetchCallAllowed && + swappc.userDataBaseRegister == swappcInfo.userDataBaseRegister && swappc.userDataCount == swappcInfo.userDataCount && + 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; + ControlFlowGraph cfg; +}; + +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()), 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; + } + auto prepared = std::make_shared(request, swappc); + if (reuse) { + frontend = prepared; + } + return prepared; +} + +std::uint64_t FrontEndBuilds() { + return frontEndBuilds.load(std::memory_order_relaxed); +} + +std::uint32_t ShaderPreparationContext::ResourcePrograms() const { + return resourcePrograms; } IrProgram PrepareResourceProgram(const RecompileRequest& request) { + return PrepareResourceProgram(request, nullptr); +} + +IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparationContext* preparation) { + frontEndBuilds.fetch_add(1, std::memory_order_relaxed); + ShaderPreparationContext local; + auto& context = preparation != nullptr ? *preparation : local; + ++context.resourcePrograms; + context.interfaceSource.reset(); + context.interfaceProgram.reset(); + const auto frontend = context.AcquireFrontend(request); const auto stageKind = toShaderStageKind(request.shader.stage); const auto inputInfo = RequestInputInfo(request); - - constexpr RdnaInstructionDecoder decoder; - const auto decoded = decoder.Decode(request.shader.code); - - constexpr GraphBuilder graphBuilder; - SwappcInfo swappcInfo; - swappcInfo.fetchCallAllowed = inputInfo.vertex != nullptr; - swappcInfo.userDataBaseRegister = request.context.userDataBaseRegister; - swappcInfo.userDataCount = static_cast(request.context.userData.size()); - auto cfg = graphBuilder.Build(decoded, &swappcInfo); - - constexpr Structurizer structurizer; - structurizer.Structurize(cfg); + const auto& decoded = frontend->decoded; + const auto& cfg = frontend->cfg; TranslateOptions translateOptions {}; translateOptions.stage = stageKind; @@ -121,14 +233,20 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request) { translateOptions.userDataBaseRegister = request.context.userDataBaseRegister; 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; } @@ -142,35 +260,71 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request) { if (list != "all" && list.find(address) == std::string::npos) return; std::fprintf(stderr, "==== IR 0x%s after %s\n%s\n", address, pass, ProgramToString(program).c_str()); }; - dumpIr("translate"); constexpr SsaBuilder ssaBuilder; - ssaBuilder.Rewrite(program); - dumpIr("ssa"); - constexpr ConstantFolder constantFolder; constexpr DeadCodeEliminator deadCodeEliminator; - - constantFolder.Fold(program); - ResolveControlFlowIdentities(program); - deadCodeEliminator.RemoveIdentities(program); - deadCodeEliminator.Eliminate(program); - dumpIr("fold"); - constexpr ReadLaneEliminator readLaneEliminator; - const auto readLaneStats = readLaneEliminator.Eliminate(program, translateOptions.waveSize); - if (readLaneStats.rewrittenReads != 0u) { + constexpr MaskedSelectEliminator maskedSelectEliminator; + constexpr VectorWindowStoreEliminator vectorWindowStoreEliminator; + const auto simplify = [&] { + dumpIr("translate"); + ssaBuilder.Rewrite(program); + dumpIr("ssa"); + constantFolder.Fold(program); ResolveControlFlowIdentities(program); deadCodeEliminator.RemoveIdentities(program); deadCodeEliminator.Eliminate(program); + dumpIr("fold"); + + const auto readLaneStats = readLaneEliminator.Eliminate(program, translateOptions.waveSize); + if (readLaneStats.rewrittenReads != 0u) { + constantFolder.Fold(program); + ResolveControlFlowIdentities(program); + deadCodeEliminator.RemoveIdentities(program); + 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) { + return value->Opcode() == IrOpcode::GetBuiltin && value->HasUses() && value->Argument(0)->Resolve()->ImmediateU32() == static_cast(StageInputKind::PackedAncillary); + }); + }); + if (liveAncillary) { + constantFolder.Fold(program); + ResolveControlFlowIdentities(program); + deadCodeEliminator.RemoveIdentities(program); + } + deadCodeEliminator.Eliminate(program); + } + if (constantFolder.ComposePackedAncillary(program)) { + deadCodeEliminator.Eliminate(program); + } + }; + simplify(); + if (stageKind == ShaderStageKind::Pixel && !translateOptions.fragmentShaderBarycentricEnabled && !HostInterpolationChecker{}.Lower(program, *inputInfo.pixel)) { + translateOptions.fragmentShaderBarycentricEnabled = true; + program = translator.Translate(decoded, cfg, translateOptions); + program.Metadata().barycentricEmulation = true; + simplify(); } - constexpr MaskedSelectEliminator maskedSelectEliminator; - if (maskedSelectEliminator.Eliminate(program).removedSelects != 0u) { + constexpr DenormalFlushEliminator denormalFlushEliminator; + if (denormalFlushEliminator.Eliminate(program).removedFlushes != 0u) { deadCodeEliminator.Eliminate(program); } + if (inputInfo.vertex != nullptr) { + constexpr TessellationLowering tessellationLowering; + tessellationLowering.Lower(program, inputInfo.vertex->tess, request.target.tessellation ? &*request.target.tessellation : nullptr); + } + constexpr SrtWalker srtWalker; srtWalker.BuildPlan(program); deadCodeEliminator.Eliminate(program); @@ -181,6 +335,9 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request) { 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; } @@ -189,9 +346,10 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request) { // 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 { @@ -200,13 +358,19 @@ struct EmissionFailure { std::exception_ptr failure; }; -struct SourceEntry { +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. @@ -214,22 +378,42 @@ struct SourceEntry { 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 { +class PrimedSharedMutex : public std::shared_mutex { +public: + PrimedSharedMutex() { + lock(); + unlock(); + } +}; + struct ResourceProgram { - explicit ResourceProgram(const RecompileRequest& request) : program(std::make_unique(PrepareResourceProgram(request))), plan(std::make_shared(ResourceMaterializer{}.ExtractPlan(*program))) {} + explicit ResourceProgram(const RecompileRequest& request, ShaderPreparationContext* preparation = nullptr, bool reclaim = true) : program(std::make_unique(PrepareResourceProgram(request, preparation))) { + const auto clearRegisterState = [](const auto& storage) { + for (const auto& block : storage) { + block->ssaScalarValues.fill(nullptr); + block->ssaVectorValues.fill(nullptr); + block->ssaThreadBitScalarValues.fill(nullptr); + block->ssaScalarMaskTags.fill(nullptr); + } + }; + clearRegisterState(program->Blocks()); + clearRegisterState(program->Resources().blockStorage); + if (reclaim) program->ReclaimUnusedValues(); + plan = std::make_shared(ResourceMaterializer{}.ExtractPlan(*program)); + } std::unique_ptr program; std::shared_ptr plan; }; std::shared_ptr makeResourcePlan(const RecompileRequest& request) { - return ResourceProgram(request).plan; + return ResourceProgram(request, nullptr, false).plan; } struct SourceKeyHash { @@ -248,8 +432,18 @@ bool FailureMemo() { return memo; } -std::shared_ptr getSource(const RecompileRequest& request) { - static std::shared_mutex mutex; +bool diskPlans() { + static const bool enabled = [] { + const char* dumpIr = std::getenv("APS5_DUMP_IR"); + return std::getenv("APS5_NO_PERF_DISK_PLANS") == nullptr && (dumpIr == nullptr || *dumpIr == '\0'); + }(); + return enabled && ShaderDiskCache::Enabled() && !DebugProbeActive(); +} + +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; struct SourceKeyStorage {}; @@ -275,6 +469,7 @@ std::shared_ptr getSource(const RecompileRequest& request) { 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 @@ -290,20 +485,56 @@ std::shared_ptr getSource(const RecompileRequest& request) { { 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); - try { - ResourceProgram resource(request); - source->plan = std::move(resource.plan); - source->program = std::move(resource.program); - } catch (...) { - if (FailureMemo()) source->planFailure = std::current_exception(); - throw; + const bool disk = diskPlans(); + std::vector planKey; + if (disk) { + ShaderDiskCache::BuildPlanKey(request, HostSubgroupSize(request), planKey); + IrResourcePlan loaded; + if (ShaderDiskCache::LoadPlan(planKey, loaded)) { + loaded.shaderHash = request.shader.codeAddress; + source->plan = std::make_shared(std::move(loaded)); + } + } + if (source->plan == nullptr) { + try { + ResourceProgram resource(request, preparation, reclaim); + source->plan = std::move(resource.plan); + 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; + } + if (disk) ShaderDiskCache::StorePlan(std::move(planKey), source->plan); } } } 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{}; } @@ -313,46 +544,134 @@ std::uint64_t nextVariantId() { return variants.fetch_add(1, std::memory_order_relaxed) + 1; } -CompiledVariant compileVariant(const RecompileRequest& request, IrProgram program, std::exception_ptr* emissionFailure = nullptr) try { +template +auto recordEmissionFailure(std::exception_ptr* emissionFailure, TStep&& step) -> decltype(step()) { + try { + return step(); + } catch (const std::bad_alloc&) { + throw; + } catch (...) { + if (emissionFailure != nullptr) *emissionFailure = std::current_exception(); + throw; + } +} + +void prepareInterface(const RecompileRequest& request, IrProgram& program) { const auto inputInfo = RequestInputInfo(request); constexpr DeadCodeEliminator deadCodeEliminator; constexpr ResourceMaterializer resourceMaterializer; 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)); deadCodeEliminator.Eliminate(program); constexpr ShaderInfoCollector shaderInfoCollector; shaderInfoCollector.Collect(program, inputInfo); +} +CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) { + const auto inputInfo = RequestInputInfo(request); constexpr BindingAllocator bindingAllocator; - auto bindings = bindingAllocator.Allocate(program, request.layout); + bool registration = request.target.registrationModules && (request.shader.stage == ShaderStage::Vertex || request.shader.stage == ShaderStage::Fragment); + program.Metadata().bindingLayoutComplete = false; + 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; targetOptions.spirvVersion = request.target.spirvVersion; - targetOptions.subgroupSize = request.target.subgroupSize; + targetOptions.subgroupSize = HostSubgroupSize(request) == 1u ? 1u : request.target.subgroupSize; targetOptions.bdaAbiVersion = request.target.bdaAbiVersion; targetOptions.supportedCapabilities = request.target.supportedCapabilities; targetOptions.supportedExtensions = request.target.supportedExtensions; 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, 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; @@ -362,7 +681,12 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra for (const auto& output : program.Info().outputs) { if (output.kind == StageOutputKind::Parameter) result.parameterExports.push_back(output.location); } - if (request.shader.stage == ShaderStage::Fragment) result.fragmentParameters = DescribeFragmentParameters(program, inputInfo); + if (request.shader.stage == ShaderStage::Fragment) { + result.fragmentParameters = DescribeFragmentParameters(program, inputInfo); + const auto& inputs = program.Info().inputs; + const auto reads = [&](StageInputKind kind) { return std::any_of(inputs.begin(), inputs.end(), [&](const auto& input) { return input.kind == kind; }); }; + if (program.Metadata().barycentricEmulation) result.barycentricEmulation = {true, reads(StageInputKind::BaryCoordSmooth), reads(StageInputKind::BaryCoordNoPerspective)}; + } if (request.shader.stage == ShaderStage::Vertex || request.shader.stage == ShaderStage::Local) { if (inputInfo.vertex == nullptr) throw std::runtime_error("vertex input metadata is missing"); for (const auto& input : program.Info().inputs) { @@ -393,12 +717,16 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra } #endif - return {request.layout, std::move(program).TakeCompiledInfo(), std::move(static_cast(bindings)), std::move(result)}; -} catch (const std::bad_alloc&) { - throw; -} catch (...) { - if (emissionFailure != nullptr) *emissionFailure = std::current_exception(); - throw; + return {request.layout, {}, std::move(static_cast(bindings)), std::move(result)}; +} + +CompiledVariant compileVariant(const RecompileRequest& request, IrProgram program, std::exception_ptr* emissionFailure = nullptr) { + return recordEmissionFailure(emissionFailure, [&] { + prepareInterface(request, program); + auto variant = emitVariant(request, program); + variant.info = std::move(program).TakeCompiledInfo(); + return variant; + }); } struct SpecializedModule { @@ -415,15 +743,33 @@ 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; } @@ -431,15 +777,23 @@ 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); } cursor += count; } - materialized = SpecializeSpirv(materialized); + materialized = SpecializeFragmentOutputs(SpecializeSpirv(materialized), constants, specializedIds); #if ANYPS5_ENABLE_SPIRV_TOOLS materialized = ValidateAndOptimizeSpirv(materialized, target.vulkanVersion, target.spirvVersion, target.nonConstantImageOffsets, true, true); #endif @@ -495,11 +849,13 @@ std::shared_ptr buildSpecializedModule(const CompiledSh struct SpecializedModuleEntry { std::once_flag ready; + std::exception_ptr failure; std::shared_ptr module; }; struct PreparedModuleEntry { std::once_flag ready; + std::exception_ptr failure; std::shared_ptr module; DescriptorBindingPlan bindings; }; @@ -508,11 +864,43 @@ struct PreparedBindingPlan { std::once_flag ready; std::exception_ptr failure; DescriptorBindingPlan bindings; - std::shared_mutex mutex; + PrimedSharedMutex mutex; std::map, std::shared_ptr> modules; }; -std::shared_ptr preparedBindingPlan(const CompiledVariant& variant, const ResourceSnapshot& snapshot, std::span exports) { +struct RegistrationModuleEntry { + std::once_flag ready; + std::exception_ptr failure; + std::shared_ptr module; +}; + +std::shared_ptr registrationModule(std::uint64_t variantId, std::span moduleKey, const std::function()>& specialize) { + std::vector key{variantId}; + key.insert(key.end(), moduleKey.begin(), moduleKey.end()); + static PrimedSharedMutex mutex; + static std::map, std::shared_ptr> modules; + std::shared_ptr entry; + { + std::shared_lock lock(mutex); + if (const auto found = modules.find(key); found != modules.end()) entry = found->second; + } + if (entry == nullptr) { + std::unique_lock lock(mutex); + const auto found = modules.find(key); + entry = found != modules.end() ? found->second : modules.emplace(key, std::make_shared()).first->second; + } + std::call_once(entry->ready, [&] { + try { + entry->module = specialize(); + } catch (...) { + entry->failure = std::current_exception(); + } + }); + if (entry->failure) std::rethrow_exception(entry->failure); + return entry->module; +} + +std::shared_ptr preparedBindingPlan(const CompiledVariant& variant, const ResourceSnapshot& snapshot, std::span exports, bool fragmentDescriptorSet) { struct BindingPlanKeyStorage {}; auto& key = HostThreadLocal, BindingPlanKeyStorage>(); key.clear(); @@ -539,7 +927,7 @@ std::shared_ptr preparedBindingPlan(const CompiledVariant& } key.push_back(exports.size()); for (const auto mapping : exports) key.push_back(mapping); - static std::shared_mutex mutex; + static PrimedSharedMutex mutex; static std::map, std::shared_ptr> plans; std::shared_ptr plan; { @@ -553,7 +941,7 @@ std::shared_ptr preparedBindingPlan(const CompiledVariant& } std::call_once(plan->ready, [&] { try { - plan->bindings = DescriptorBindingBuilder{}.Prepare(variant.bindings.layout, variant.info.info, variant.info.stage, snapshot, exports); + plan->bindings = DescriptorBindingBuilder{}.Prepare(variant.bindings.layout, variant.info.info, variant.info.stage, snapshot, exports, variant.artifact.registrationModule, fragmentDescriptorSet); } catch (...) { plan->failure = std::current_exception(); } @@ -568,7 +956,7 @@ std::shared_ptr specializeModule(const CompiledShaderAr key.clear(); key.push_back(artifact.variantId); for (const auto& constant : constants) key.push_back((static_cast(constant.id) << 32u) | constant.value); - static std::shared_mutex mutex; + static PrimedSharedMutex mutex; static std::map, std::shared_ptr> modules; std::shared_ptr entry; { @@ -580,11 +968,18 @@ std::shared_ptr specializeModule(const CompiledShaderAr const auto found = modules.find(key); entry = found != modules.end() ? found->second : modules.emplace(key, std::make_shared()).first->second; } - std::call_once(entry->ready, [&] { entry->module = buildSpecializedModule(artifact, classes, constants, target); }); + std::call_once(entry->ready, [&] { + try { + entry->module = buildSpecializedModule(artifact, classes, constants, target); + } catch (...) { + entry->failure = std::current_exception(); + } + }); + if (entry->failure) std::rethrow_exception(entry->failure); return entry->module; } -RecompileResult materializeResult(const CompiledVariant& variant, const RecompileRequest& request, const ResourceSnapshot& snapshot) { +RecompileResult materializeResult(const CompiledVariant& variant, const RecompileRequest& request, const ResourceSnapshot& snapshot, bool registrationDefaults = false) { RecompileResult result; const auto& artifact = variant.artifact; static_cast(result) = { @@ -593,6 +988,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil .imageMetadataDword = artifact.imageMetadataDword, .runtimeImageCount = artifact.runtimeImageCount, .runtimeImageResources = artifact.runtimeImageResources, + .registrationModule = artifact.registrationModule, .bdaAbiVersion = artifact.bdaAbiVersion, .runtimeAbiVersion = artifact.runtimeAbiVersion, .vertexInputs = artifact.vertexInputs, @@ -603,19 +999,45 @@ 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 }; - const auto plan = preparedBindingPlan(variant, snapshot, request.context.pixel ? std::span(request.context.pixel->targetExportMapping) : std::span{}); + 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; struct LocalModuleKeyStorage {}; 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{}; for (const auto& input : result.vertexInputs) { if (!request.context.vertex || input.location >= request.context.vertex->resourcesNum || input.location >= request.context.vertex->resources.size()) throw std::runtime_error("Shader cache: invalid vertex attribute metadata"); const auto& vertex = *request.context.vertex; @@ -623,7 +1045,8 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil const auto fetchIndex = vertex.fetchEmbedded ? vertex.resourcesDst[input.location].fetchIndex : input.fetchIndex; result.vertexAttributes.push_back({input.location, input.components, resource, fetchIndex}); if (!artifact.vertexInputPatches.empty()) { - const auto numeric = VertexInputNumericClass(static_cast((resource.fields[3] >> 12u) & 0x7fu)); + const auto format = static_cast((resource.fields[3] >> 12u) & 0x7fu); + const auto numeric = registrationDefaults && format == IrBufferFormat::Invalid ? IrTextureNumericClass::Float : VertexInputNumericClass(format); if (numeric == IrTextureNumericClass::Unsupported) throw std::runtime_error("unsupported prepared vertex format"); const auto kind = numeric == IrTextureNumericClass::Float ? 0u : numeric == IrTextureNumericClass::Sint ? 1u : 2u; vertexClasses.at(input.location) = kind; @@ -636,10 +1059,11 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil if (selector >= 4u) formatComponents = std::max(formatComponents, selector - 3u); selectors |= selector << (component * 3u); } - moduleKey.push_back(selectors); + moduleKey.push_back(artifact.registrationModule ? selectors & ~0xfffu : selectors); + 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()) { + 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; @@ -649,10 +1073,21 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil const auto found = plan->modules.find(moduleKey); entry = found != plan->modules.end() ? found->second : plan->modules.emplace(moduleKey, std::make_shared()).first->second; } - std::call_once(entry->ready, [&] { + const auto prepare = [&] { 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++]; @@ -663,12 +1098,20 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil constants.push_back({first + 5u, kind}); } } - const auto module = specializeModule(variant.artifact, vertexClasses, constants, request.target); + const auto module = artifact.registrationModule && plan->bindings.specialization.empty() ? registrationModule(artifact.variantId, moduleKey, [&] { return specializeModule(variant.artifact, vertexClasses, constants, request.target); }) : specializeModule(variant.artifact, vertexClasses, constants, request.target); if (variant.bindings.layout.UsesPushData() && !module->pushData) throw std::runtime_error("specialization removed the prepared push constant interface"); auto selected = DescriptorBindingBuilder{}.Select(plan->bindings, module->bindings); entry->bindings = std::move(selected); entry->module = module; + }; + std::call_once(entry->ready, [&] { + try { + prepare(); + } catch (...) { + entry->failure = std::current_exception(); + } }); + if (entry->failure) std::rethrow_exception(entry->failure); const auto& module = entry->module; result.specializationId = module->specializationId; result.spirv = module->spirv; @@ -676,11 +1119,45 @@ 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.poisonedSrtReads = static_cast(snapshot.srtPoison.size()); + 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; + if (layout.runtimeStateDwords > runtimeSelectors.size()) throw std::runtime_error("vertex selector state exceeds its capacity"); + if (layout.UsesPushData()) { + if ((first + layout.runtimeStateDwords) * sizeof(std::uint32_t) > result.pushConstants.size()) throw std::runtime_error("vertex selector state lies outside the push data"); + std::memcpy(result.pushConstants.data() + first * sizeof(std::uint32_t), runtimeSelectors.data(), layout.runtimeStateDwords * sizeof(std::uint32_t)); + } else { + const auto data = std::ranges::find_if(result.bindings, [](const DescriptorBinding& binding) { return binding.role == DescriptorRole::ShaderData; }); + if (data == result.bindings.end() || first + layout.runtimeStateDwords > data->guestDescriptor.size()) throw std::runtime_error("vertex selector state lies outside the shader data"); + 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; } @@ -688,7 +1165,50 @@ bool sameLayout(const BindingLayout& left, const BindingLayout& right) { return left.descriptorSet == right.descriptorSet && left.firstBinding == right.firstBinding && left.pushConstantOffsetBytes == right.pushConstantOffsetBytes && left.pushConstantSizeBytes == right.pushConstantSizeBytes; } -std::shared_ptr findOrCompileVariant(SourceEntry& source, const RecompileRequest& request, bool& cacheHit) { +} + +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) { + preparation.interfaceSource.reset(); + preparation.interfaceProgram.reset(); + auto program = source.program != nullptr ? std::move(source.program) : std::make_unique(PrepareResourceProgram(request, &preparation)); + recordEmissionFailure(emissionFailure, [&] { prepareInterface(request, *program); }); + preparation.interfaceSource = source.shared_from_this(); + preparation.interfaceProgram = std::move(program); + } + auto& program = *preparation.interfaceProgram; + return recordEmissionFailure(emissionFailure, [&] { + auto variant = emitVariant(request, program); + variant.info = program.CompiledInfo(); + return variant; + }); + } +}; + +namespace { + +std::shared_ptr findOrCompileVariant(SourceEntry& source, const RecompileRequest& request, bool& cacheHit, ShaderPreparationContext* preparation = nullptr) { for (const auto& candidate : source.variants) { if (sameLayout(candidate->layout, request.layout)) { cacheHit = true; @@ -712,16 +1232,24 @@ std::shared_ptr findOrCompileVariant(SourceEntry& source, } } if (variant == nullptr) { - auto program = source.program != nullptr ? std::move(*source.program) : PrepareResourceProgram(request); - source.program.reset(); + static const bool layoutPair = std::getenv("APS5_NO_PERF_LAYOUT_PAIR") == nullptr; std::exception_ptr emissionFailure; try { - variant = std::make_shared(compileVariant(request, std::move(program), &emissionFailure)); + if (layoutPair && preparation != nullptr) { + variant = std::make_shared(PreparedInterface::Compile(source, request, *preparation, &emissionFailure)); + } else { + auto program = source.program != nullptr ? std::move(*source.program) : PrepareResourceProgram(request, preparation); + source.program.reset(); + variant = std::make_shared(compileVariant(request, std::move(program), &emissionFailure)); + } } catch (...) { if (emissionFailure != nullptr && FailureMemo()) source.emissionFailures.push_back({request.shader.codeAddress, request.layout, emissionFailure}); throw; } - if (disk) ShaderDiskCache::Store(std::move(diskKey), variant); + if (disk) { + if (auto persisted = ShaderDiskCache::Persist(diskKey, *variant)) variant = std::move(persisted); + else ShaderDiskCache::Store(std::move(diskKey), variant); + } } source.program.reset(); source.variants.push_back(variant); @@ -740,7 +1268,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); @@ -754,17 +1282,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}; @@ -811,6 +1353,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]); } }; @@ -824,10 +1367,34 @@ 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); } if (request.context.vertex) { const auto& vertex = *request.context.vertex; @@ -854,13 +1421,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{}; @@ -871,25 +1441,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(); @@ -935,21 +1503,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; @@ -967,7 +1543,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); } @@ -983,15 +1559,19 @@ std::shared_ptr ResolveSource(const RecompileRequest& reques } std::shared_ptr PrepareShader(const RecompileRequest& request) { + return PrepareShader(request, nullptr); +} + +std::shared_ptr PrepareShader(const RecompileRequest& request, ShaderPreparationContext* preparation) { return recompileReporting(request, [&]() -> std::shared_ptr { static_cast(RequestInputInfo(request)); auto handle = std::make_shared(); - handle->source = getSource(request); + handle->source = getSource(request, preparation, false); RecompileCacheKey::BuildInterface(request, handle->staticKey); handle->staticKey.push_back(HostSubgroupSize(request)); bool cacheHit = false; std::lock_guard lock(handle->source->mutex); - handle->artifact = findOrCompileVariant(*handle->source, request, cacheHit); + handle->artifact = findOrCompileVariant(*handle->source, request, cacheHit, preparation); return handle; }); } @@ -1017,6 +1597,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); } @@ -1026,7 +1607,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 {}; @@ -1037,20 +1620,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; @@ -1062,9 +1645,23 @@ const CompiledShaderArtifact& GetPreparedArtifact(const SourceHandle& handle) { return handle.artifact->artifact; } +std::shared_ptr RegistrationResult(const RecompileRequest& request, const SourceHandle& handle) { + if (handle.artifact == nullptr || !MatchesPreparedShader(request, handle)) throw std::runtime_error("ShaderRecompiler: prepared artifact does not match the static ABI"); + if (!request.target.registrationModules || !handle.artifact->artifact.registrationModule) return nullptr; + const auto& info = handle.artifact->info.info; + if (std::ranges::any_of(info.buffers, [](const BufferResource& buffer) { return buffer.descriptorFormatted; }) || std::ranges::any_of(info.samplers, [](const SamplerResource& sampler) { return FoldsTexelOffsets(sampler); })) return nullptr; + ResourceSnapshot snapshot; + snapshot.buffers.assign(info.buffers.size(), {{}, 4u}); + snapshot.images.assign(info.images.size(), {{}, 8u}); + snapshot.samplers.assign(info.samplers.size(), {{}, 4u}); + snapshot.userData.assign(RuntimeAbi::UserDataCapacity, 0u); + snapshot.flattenedSrt.assign(1u, 0u); + return std::make_shared(materializeResult(*handle.artifact, request, snapshot, true)); +} + 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; @@ -1077,10 +1674,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 603d7b9375..cceee1ae33 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -3,13 +3,16 @@ #include "RuntimeAbi.hpp" #include "PipelineSpecialization.hpp" +#include #include +#include #include #include #include #include #include #include +#include #include #include @@ -107,6 +110,46 @@ enum class ConservativeZExport : std::uint8_t { GreaterThanZ }; +enum class ColorExportPacking : std::uint8_t { + None, + Unorm10_11_11, + Float11_11_10 +}; + +enum class RasterPrimitive : std::uint8_t { + Point, + Line, + Triangle, + 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; @@ -119,6 +162,7 @@ struct ShaderPixelStageInfo { bool posZ; bool posW; bool frontFace; + bool frontFaceAllBits; bool ancillary; bool sampleShading; bool noPerspective; @@ -132,6 +176,21 @@ struct ShaderPixelStageInfo { bool orderedPixelShader; std::array targetOutputMode; std::array targetExportMapping; + std::array targetExportPacking; + 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 { @@ -153,6 +212,8 @@ struct ShaderVertexStageInfo { std::uint32_t fetchAttribReg; std::uint32_t fetchBufferReg; bool fetchEmbedded; + std::uint32_t paClVsOutCntl = 0; + std::uint32_t depthRemapMode = 0; }; struct ShaderFloatMode { @@ -167,6 +228,7 @@ struct ShaderFloatMode { inline constexpr std::uint32_t InterpolationQuiet = 1u; inline constexpr std::uint32_t InterpolationFlush32 = 2u; inline constexpr std::uint32_t InterpolationFlush16 = 4u; +inline constexpr std::uint32_t InterpolationRound16Shift = 3u; struct GuestContext { std::uint32_t waveSize; @@ -177,6 +239,8 @@ struct GuestContext { std::optional vertex; std::span memory; std::optional floatMode; + std::span linkedCallFallbacks; + bool convertedSamples = false; }; struct MeshTargetLimits { @@ -217,6 +281,18 @@ struct SpirvTarget { bool nonConstantImageOffsets = false; std::uint32_t srgbDecodeFormats = 0; bool narrowSubgroupClock = false; + bool fixedPushSlots = false; + bool float64DenormPreserve = false; + bool registrationModules = false; + 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 { @@ -253,6 +329,7 @@ struct MeshConfiguration { std::uint32_t ldsSizeDwords; std::uint32_t provokingVertex; std::uint32_t esgsItemSize = 0; + std::uint32_t outputPrimitiveSize = 3; }; struct TessellationConfiguration { @@ -261,15 +338,29 @@ struct TessellationConfiguration { std::uint32_t domain; std::uint32_t partitioning; std::uint32_t outputTopology; + 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; inline constexpr std::uint32_t MeshArgumentBytes = 20; +inline constexpr std::uint32_t MeshIndexRestartTable = 0x100; +inline constexpr std::uint32_t MeshRestartLengthDword = MeshArgumentBytes / 4; +inline constexpr std::uint32_t MeshRestartTableDword = MeshRestartLengthDword + 1; inline constexpr std::uint32_t MeshIndexBufferUserWord = 4; +inline constexpr std::uint32_t WorkgroupMemoryDescriptorSet = 1; struct GraphicsDrawParameters { std::uint64_t indexAddress; @@ -278,12 +369,37 @@ 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 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 { @@ -295,6 +411,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, @@ -311,7 +432,9 @@ enum class DescriptorImageShape { Image2DArray, ImageCube, Image3D, - Image1DArray + Image1DArray, + Image2DMultisample, + Image2DMultisampleArray }; enum class DescriptorRole { @@ -322,7 +445,11 @@ enum class DescriptorRole { BdaPagetable, FaultBuffer, FlattenedSrt, - ShaderData + ShaderData, + ImageTable, + SamplerTable, + ImageTableMap, + ImageStorageTable }; struct DescriptorBinding { @@ -340,6 +467,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. @@ -351,8 +479,20 @@ struct DescriptorBinding { // (a producer that does not fill it) must be treated as written. 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 { @@ -363,57 +503,127 @@ struct VertexAttribute { std::uint32_t formatComponents = 0; }; -struct FragmentParameter { - std::uint32_t location; - std::uint32_t sourceLocation; - bool flat; - bool perVertex; - bool custom = 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, 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 // holder writes to them (tests and tools patch modules in place). Reads look like a vector. // The non-const data(), operator[], begin() and end() count as writes: on a shared holder (every // cached result) they clone the module, so a consumer that only reads takes the result by const // reference, or the per-dispatch copy this class removes comes back without a compiler hint. +// ContentHash is computed once per shared holder, on first use, and dropped by a write. class SharedSpirv { public: SharedSpirv() = default; - SharedSpirv(std::vector words) : words(std::make_shared>(std::move(words))) {} + SharedSpirv(std::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)); + holder = std::make_shared(std::move(other), nullptr, std::span{}); return *this; } - [[nodiscard]] const std::vector& Words() const { return words ? *words : Empty(); } - operator const std::vector&() const { return Words(); } + [[nodiscard]] std::span Words() const { return 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(); } [[nodiscard]] const std::uint32_t* data() const { return Words().data(); } [[nodiscard]] std::uint32_t* data() { return Mutable().data(); } [[nodiscard]] const std::uint32_t& operator[](std::size_t index) const { return Words()[index]; } [[nodiscard]] std::uint32_t& operator[](std::size_t index) { return Mutable()[index]; } - [[nodiscard]] std::vector::const_iterator begin() const { return Words().begin(); } - [[nodiscard]] std::vector::const_iterator end() const { return Words().end(); } + [[nodiscard]] std::span::iterator begin() const { return Words().begin(); } + [[nodiscard]] std::span::iterator end() const { return Words().end(); } [[nodiscard]] std::vector::iterator begin() { return Mutable().begin(); } [[nodiscard]] std::vector::iterator end() { return Mutable().end(); } void resize(std::size_t count) { Mutable().resize(count); } std::vector::iterator insert(std::vector::const_iterator where, std::initializer_list values) { return Mutable().insert(where, values); } - friend bool operator==(const SharedSpirv& left, const SharedSpirv& right) { return left.words == right.words || left.Words() == right.Words(); } + friend bool operator==(const SharedSpirv& left, const SharedSpirv& right) { return std::ranges::equal(left.Words(), right.Words()); } private: - static const std::vector& Empty() { - static const std::vector empty; - return empty; - } - // Copy on write: a holder whose words are shared gets its own copy before the first write. + 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>(Words()); - return *words; + 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 holder->words; } - std::shared_ptr> words; + std::shared_ptr holder; }; struct VertexInput { @@ -441,6 +651,7 @@ struct CompiledShaderArtifact { std::uint32_t imageMetadataDword = 0; std::uint32_t runtimeImageCount = 0; std::vector runtimeImageResources; + bool registrationModule = false; std::uint32_t bdaAbiVersion = 0; std::uint32_t runtimeAbiVersion = RuntimeAbi::Version; std::vector vertexInputs; @@ -451,8 +662,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; }; @@ -461,15 +678,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; @@ -486,10 +764,28 @@ void SetDebugProbeActive(bool active); struct RectListShaders { RecompileResult control; RecompileResult evaluation; + RecompileResult lastVertexControl; }; [[nodiscard]] RectListShaders BuildRectListShaders(const RecompileResult& vertex, const RecompileResult& fragment, const SpirvTarget& target); +struct GeometryStageLimits { + std::uint32_t maxGeometryInputComponents; + std::uint32_t maxGeometryOutputComponents; + std::uint32_t maxGeometryOutputVertices; + std::uint32_t maxGeometryTotalOutputComponents; + std::uint32_t maxFragmentInputComponents; +}; + +[[nodiscard]] RecompileResult BuildBarycentricGeometryShader(const RecompileResult& vertex, const RecompileResult& fragment, const SpirvTarget& target, const std::optional& limits); + +struct BarycentricTessellationShaders { + RecompileResult control; + RecompileResult evaluation; +}; + +[[nodiscard]] BarycentricTessellationShaders BuildBarycentricTessellationShaders(const RecompileResult& vertex, const RecompileResult& fragment, const SpirvTarget& target, std::uint32_t maxFragmentInputComponents); + } #endif diff --git a/core/shader/recompiler/RuntimeAbi.hpp b/core/shader/recompiler/RuntimeAbi.hpp index 66f7f8a2f0..52d25ba439 100644 --- a/core/shader/recompiler/RuntimeAbi.hpp +++ b/core/shader/recompiler/RuntimeAbi.hpp @@ -9,8 +9,9 @@ namespace ShaderRecompiler::RuntimeAbi { -inline constexpr std::uint32_t Version = 10u; +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; inline constexpr std::uint32_t PushConstantDwords = 32u; inline constexpr std::uint32_t FirstImageBinding = 1u; @@ -26,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 }; @@ -34,9 +39,18 @@ enum class Stage : std::uint32_t { Main, Fragment, TessellationControl, Tessella inline constexpr std::uint32_t UserDataCapacity = 128u; inline constexpr std::uint32_t BufferCapacity = 128u; inline constexpr std::uint32_t ImageCapacity = 256u; -inline constexpr std::uint32_t SampledHeapCapacity = 16u; -inline constexpr std::uint32_t StorageHeapCapacity = 4u; -inline constexpr std::uint32_t SamplerHeapCapacity = 16u; +inline constexpr std::uint32_t SampledHeapCapacity = 64u; +inline constexpr std::uint32_t BindlessTableSlots = 16u; +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; +inline constexpr std::uint32_t VertexSelectorsPerDword = 2u; +inline constexpr std::uint32_t ImageModeMask = 0xffu; +inline constexpr std::uint32_t CompareStateShift = 9u; +inline constexpr std::uint32_t CompareStateMask = 0x1fffu; struct ResourceMetadata { std::uint32_t binding; @@ -74,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) == 13760u); +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 == 3432u); +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); @@ -92,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 2295064824..9b3893ce2a 100644 --- a/core/shader/recompiler/ShaderCacheDirectory.hpp +++ b/core/shader/recompiler/ShaderCacheDirectory.hpp @@ -1,36 +1,92 @@ #ifndef CORE_SHADER_RECOMPILER_SHADERCACHEDIRECTORY_HPP #define CORE_SHADER_RECOMPILER_SHADERCACHEDIRECTORY_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 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#endif 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)); + 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()); + constexpr DWORD replaceExisting = 0x00000001, posixSemantics = 0x00000002; + rename->flags = replaceExisting | posixSemantics; + rename->nameBytes = static_cast(nameBytes); + std::memcpy(rename->name, destination.c_str(), nameBytes); +#endif bool written = false; { std::ofstream file(temporary, std::ios::binary | std::ios::trunc); if (!file) return false; - file.write(reinterpret_cast(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(); } @@ -38,7 +94,26 @@ inline bool WriteFileAtomically(const std::filesystem::path& path, std::span(22); + if (!SetFileInformationByHandle(handle, renameInfoEx, rename, static_cast(renameBuffer.size()))) renameError = GetLastError(); + } + if (renameError == ERROR_SUCCESS) return true; + if (renameError != ERROR_INVALID_PARAMETER && renameError != ERROR_INVALID_FUNCTION && renameError != ERROR_NOT_SUPPORTED) { + std::filesystem::remove(temporary, error); + return false; + } +#endif + std::filesystem::rename(temporary, destination, error); if (error) { std::filesystem::remove(temporary, error); return false; @@ -48,17 +123,54 @@ inline bool WriteFileAtomically(const std::filesystem::path& path, std::span& bytes) { bytes.clear(); - std::ifstream file(path, std::ios::binary | std::ios::ate); - if (!file) return false; - const auto size = static_cast(file.tellg()); - if (size < 0) return false; - file.seekg(0, std::ios::beg); - bytes.resize(static_cast(size)); - if (!file.read(reinterpret_cast(bytes.data()), static_cast(bytes.size()))) { + try { +#ifdef _WIN32 + std::error_code error; + const auto native = NativeCachePath_nid_no_patch(path, error); + if (error) return false; + const auto file = CreateFileW(native.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (file == INVALID_HANDLE_VALUE) return false; + struct Close { HANDLE file; ~Close() { CloseHandle(file); } } close{file}; + LARGE_INTEGER fileSize{}; + if (!GetFileSizeEx(file, &fileSize)) return false; + const auto size = fileSize.QuadPart; +#else + std::ifstream file(path, std::ios::binary | std::ios::ate); + if (!file) return false; + const auto size = static_cast(file.tellg()); +#endif + if (size < 0 || static_cast(size) > bytes.max_size() || + static_cast(size) > static_cast(std::numeric_limits::max())) + return false; +#ifdef _WIN32 + bytes.resize(static_cast(size)); + for (std::size_t offset = 0; offset < bytes.size();) { + const auto left = bytes.size() - offset; + const auto chunk = static_cast(left > MAXDWORD ? MAXDWORD : left); + DWORD count = 0; + if (!ReadFile(file, bytes.data() + offset, chunk, &count, nullptr) || count == 0) { + bytes.clear(); + return false; + } + offset += count; + } +#else + if (!file.seekg(0, std::ios::beg)) return false; + bytes.resize(static_cast(size)); + if (!file.read(reinterpret_cast(bytes.data()), static_cast(bytes.size()))) { + bytes.clear(); + return false; + } +#endif + return true; + } catch (const std::bad_alloc&) { + bytes.clear(); + return false; + } catch (const std::length_error&) { bytes.clear(); return false; } - return true; } inline std::uint64_t HashBytes(std::span bytes, std::uint64_t seed = 0) { diff --git a/core/shader/recompiler/ShaderCachePack.cpp b/core/shader/recompiler/ShaderCachePack.cpp new file mode 100644 index 0000000000..6b15865823 --- /dev/null +++ b/core/shader/recompiler/ShaderCachePack.cpp @@ -0,0 +1,118 @@ +#include "ShaderCachePack.hpp" +#include "ShaderCacheDirectory.hpp" +#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 +#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 1f570b0ba1..bcda9f0c40 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -1,5 +1,6 @@ #include "ShaderDiskCache.hpp" #include "CacheKey.hpp" +#include "ShaderCachePack.hpp" #include "ShaderCacheDirectory.hpp" #include "ThreadOwned.hpp" #include "Optimization/ResourceMaterializer.hpp" @@ -11,8 +12,16 @@ #define WIN32_LEAN_AND_MEAN #endif #include +#elif defined(__APPLE__) +#include #endif -#include "ShaderCacheVersion.hpp" +#ifndef _WIN32 +#include +#include +#include +#include +#endif +#include "ShaderCacheSourceVersion.hpp" #include #include #include @@ -22,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +39,8 @@ #include #include #include +#include +#include namespace ShaderRecompiler { @@ -53,7 +65,15 @@ std::filesystem::path ShaderCacheDirectory() { const char* directory = std::getenv("ANYPS5_SHADER_CACHE_DIR"); if (directory != nullptr && *directory != '\0') return std::filesystem::path(directory); std::error_code error; +#ifdef __APPLE__ + std::uint32_t size = 0; + _NSGetExecutablePath(nullptr, &size); + std::vector buffer(size); + if (_NSGetExecutablePath(buffer.data(), &size) != 0) return {}; + const auto executable = std::filesystem::canonical(buffer.data(), error); +#else const auto executable = std::filesystem::read_symlink("/proc/self/exe", error); +#endif if (error) return {}; return executable.parent_path() / "shader_cache"; #endif @@ -64,30 +84,78 @@ std::filesystem::path ShaderCacheDirectory() { namespace ShaderRecompiler::ShaderDiskCache { #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) -static_assert(sizeof(CompiledShaderArtifact) == 184, "CompiledShaderArtifact changed: update the artifact encoder"); -static_assert(sizeof(ShaderInvocation) == 112, "ShaderInvocation changed: update the invocation encoder"); -static_assert(sizeof(RecompileResult) == 296, "RecompileResult changed: update EncodeResult and DecodeResult"); -static_assert(sizeof(DescriptorBinding) == 448, "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) == 112, "ImageResource changed: update the info encoder"); -static_assert(sizeof(SamplerResource) == 16, "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) == 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) == 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"); #endif namespace { +struct AnyField { + template + operator TField() const; +}; + +template +constexpr bool bracedWith(std::index_sequence) { + return requires { TStruct{(static_cast(Index), AnyField{})...}; }; +} + +template +constexpr std::size_t fieldCount() { + if constexpr (bracedWith(std::make_index_sequence{})) { + return fieldCount(); + } else { + return Count; + } +} + +static_assert(fieldCount() == 23, "ShaderInfo changed: update the info encoder"); +static_assert(fieldCount() == 12, "BufferResource changed: update the info encoder"); +static_assert(fieldCount() == 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() == 24, "IrResourcePlan changed: update EncodePlan and DecodePlan"); +static_assert(fieldCount() == 2, "GuestRegister changed: update the plan value encoder"); +static_assert(fieldCount() == 31, "MemoryInfo changed: update the plan encoder"); +static_assert(fieldCount() == 3, "DescriptorSource changed: update the plan encoder"); +static_assert(fieldCount() == 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"); + constexpr std::uint32_t FileMagic = 0x43535041u; struct FileHeader { @@ -99,6 +167,72 @@ struct FileHeader { std::uint64_t keyHash; std::uint64_t payloadHash; }; + +std::size_t PayloadOffset(std::size_t keyBytes) { + constexpr std::size_t alignment = 8; + return (sizeof(FileHeader) + keyBytes + alignment - 1) / alignment * alignment; +} + +class MappedFile { +public: + MappedFile(const void* data, std::size_t size) : data(data), size(size) {} + MappedFile(const MappedFile&) = delete; + MappedFile& operator=(const MappedFile&) = delete; + ~MappedFile() { +#ifdef _WIN32 + UnmapViewOfFile(data); +#else + munmap(const_cast(data), size); +#endif + } + + [[nodiscard]] std::span Bytes() const { return {static_cast(data), size}; } + +private: + const void* data; + std::size_t size; +}; + +std::shared_ptr MapFile(const std::filesystem::path& path) { +#ifdef _WIN32 + 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()) { + CloseHandle(file); + return nullptr; + } + const HANDLE mapping = CreateFileMappingW(file, nullptr, PAGE_READONLY, 0, 0, nullptr); + CloseHandle(file); + if (mapping == nullptr) return nullptr; + const void* data = MapViewOfFile(mapping, FILE_MAP_READ, 0, 0, 0); + CloseHandle(mapping); + if (data == nullptr) return nullptr; + try { + return std::make_shared(data, static_cast(length.QuadPart)); + } catch (...) { + UnmapViewOfFile(data); + throw; + } +#else + const int descriptor = open(path.c_str(), O_RDONLY | O_CLOEXEC); + if (descriptor < 0) return nullptr; + struct stat status{}; + void* data = MAP_FAILED; + if (fstat(descriptor, &status) == 0 && status.st_size > 0) data = mmap(nullptr, static_cast(status.st_size), PROT_READ, MAP_PRIVATE, descriptor, 0); + close(descriptor); + if (data == MAP_FAILED) return nullptr; + try { + return std::make_shared(data, static_cast(status.st_size)); + } catch (...) { + munmap(data, static_cast(status.st_size)); + throw; + } +#endif +} static_assert(sizeof(FileHeader) == 48 && std::is_trivially_copyable_v); class Writer { @@ -128,6 +262,14 @@ public: for (const bool flag : flags) Value(flag); } + void Compact(std::uint64_t value) { + while (value >= 0x80u) { + Raw(static_cast(value | 0x80u)); + value >>= 7u; + } + Raw(static_cast(value)); + } + void Text(const std::string& text) { Value(text.size()); const auto bytes = std::as_bytes(std::span(text.data(), text.size())); @@ -153,10 +295,26 @@ private: class Reader { public: - explicit Reader(std::span bytes) : bytes(bytes) {} + explicit Reader(std::span bytes, std::shared_ptr owner = nullptr) : bytes(bytes), owner(std::move(owner)) {} [[nodiscard]] bool Ok() const { return ok; } [[nodiscard]] bool Done() const { return ok && position == bytes.size(); } + [[nodiscard]] std::size_t Remaining() const { return ok ? bytes.size() - position : 0; } + void Fail() { ok = false; } + + [[nodiscard]] std::uint64_t Compact() { + std::uint64_t value = 0; + for (std::uint32_t shift = 0; shift < 64u; shift += 7u) { + const auto byte = Raw(); + if (!ok) return 0; + value |= static_cast(byte & 0x7fu) << shift; + if ((byte & 0x80u) != 0u) continue; + if ((byte == 0u && shift != 0u) || (shift == 63u && byte > 1u)) ok = false; + return ok ? value : 0; + } + ok = false; + return 0; + } template void Value(TValue& value) requires (std::is_integral_v || std::is_enum_v) { @@ -188,6 +346,18 @@ public: } } + SharedSpirv Spirv() { + const auto count = Count(sizeof(std::uint32_t)); + const auto* first = bytes.data() + position; + position += count * sizeof(std::uint32_t); + if (owner != nullptr && reinterpret_cast(first) % alignof(std::uint32_t) == 0) { + return SharedSpirv(owner, std::span(reinterpret_cast(first), count)); + } + std::vector words(count); + if (count != 0) std::memcpy(words.data(), first, count * sizeof(std::uint32_t)); + return SharedSpirv(std::move(words)); + } + void Flags(std::vector& flags) { const auto count = Count(1); flags.assign(count, false); @@ -233,6 +403,7 @@ private: } std::span bytes; + std::shared_ptr owner; std::size_t position = 0; bool ok = true; }; @@ -252,11 +423,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) { @@ -275,11 +452,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) { @@ -291,6 +474,7 @@ void encodeArtifact(Writer& writer, const CompiledShaderArtifact& result) { writer.Value(result.imageMetadataDword); writer.Value(result.runtimeImageCount); writer.Values(std::span(result.runtimeImageResources)); + writer.Value(result.registrationModule); writer.List(result.vertexInputPatches, [](Writer& out, const VertexInputPatch& patch) { out.Value(patch.location); out.Value(patch.word); @@ -309,6 +493,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); @@ -316,20 +501,38 @@ void encodeArtifact(Writer& writer, const CompiledShaderArtifact& result) { out.Value(parameter.flat); out.Value(parameter.perVertex); out.Value(parameter.custom); + out.Value(parameter.flatP10); + out.Value(parameter.vertexOrder); }); + 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) { reader.Value(result.runtimeAbiVersion); - std::vector words; - reader.Values(words); - result.spirv = std::move(words); + result.spirv = reader.Spirv(); reader.Value(result.bdaAbiVersion); reader.Value(result.memoryOffsetDword); reader.Value(result.shaderDataDwords); reader.Value(result.imageMetadataDword); reader.Value(result.runtimeImageCount); reader.Values(result.runtimeImageResources); + reader.Value(result.registrationModule); reader.List(result.vertexInputPatches, 20, [](Reader& in, VertexInputPatch& patch) { in.Value(patch.location); in.Value(patch.word); @@ -348,6 +551,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); @@ -355,7 +559,30 @@ void decodeArtifact(Reader& reader, CompiledShaderArtifact& result) { in.Value(parameter.flat); in.Value(parameter.perVertex); in.Value(parameter.custom); + in.Value(parameter.flatP10); + in.Value(parameter.vertexOrder); }); + 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; } @@ -367,6 +594,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); @@ -374,7 +602,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) { @@ -388,6 +635,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); @@ -395,15 +643,36 @@ 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); writer.Value(layout.runtimeImageCount); + writer.Value(layout.runtimeStateDwords); writer.Values(std::span(layout.userDataRegisters)); writer.List(layout.descriptors, [](Writer& out, const IrDescriptorBinding& descriptor) { out.Value(descriptor.kind); @@ -413,10 +682,12 @@ 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); reader.Value(layout.runtimeImageCount); + reader.Value(layout.runtimeStateDwords); reader.Values(layout.userDataRegisters); reader.List(layout.descriptors, 12, [](Reader& in, IrDescriptorBinding& descriptor) { in.Value(descriptor.kind); @@ -424,15 +695,7 @@ void decodeLayout(Reader& reader, IrBindingLayout& layout) { }); } -void encodeInfo(Writer& writer, const CompiledShaderInfo& compiled) { - writer.Value(compiled.stage); - writer.Value(compiled.shaderHash); - writer.Value(compiled.waveSize); - writer.Value(compiled.userDataBase); - writer.Value(compiled.userDataCount); - writer.Value(compiled.scratchDwords); - writer.Value(compiled.paramExportMask); - const auto& info = compiled.info; +void encodeShaderInfo(Writer& writer, const ShaderInfo& info) { writer.Value(info.scratchDwords); writer.Value(info.sharedMemoryBytes); writer.List(info.buffers, [](Writer& out, const BufferResource& buffer) { @@ -476,14 +739,21 @@ void encodeInfo(Writer& writer, const CompiledShaderInfo& compiled) { 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) { @@ -493,6 +763,8 @@ void encodeInfo(Writer& writer, const CompiledShaderInfo& compiled) { out.Value(sampler.forcePointFiltering); 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); @@ -524,18 +796,11 @@ void encodeInfo(Writer& writer, const CompiledShaderInfo& compiled) { writer.Value(info.bdaWrites); writer.Value(info.usesFaultBuffer); writer.Value(info.dispatchThreadLimit); - encodeLayout(writer, compiled.bindings); + writer.Value(info.rasterPrimitive); + writer.Value(info.layerExported); } -void decodeInfo(Reader& reader, CompiledShaderInfo& compiled) { - reader.Value(compiled.stage); - reader.Value(compiled.shaderHash); - reader.Value(compiled.waveSize); - reader.Value(compiled.userDataBase); - reader.Value(compiled.userDataCount); - reader.Value(compiled.scratchDwords); - reader.Value(compiled.paramExportMask); - auto& info = compiled.info; +void decodeShaderInfo(Reader& reader, ShaderInfo& info) { reader.Value(info.scratchDwords); reader.Value(info.sharedMemoryBytes); reader.List(info.buffers, 27, [](Reader& in, BufferResource& buffer) { @@ -552,7 +817,7 @@ void decodeInfo(Reader& reader, CompiledShaderInfo& compiled) { in.Value(buffer.scalar); in.Value(buffer.typedAlignment); }); - reader.List(info.images, 74, [](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); @@ -579,25 +844,34 @@ void decodeInfo(Reader& reader, CompiledShaderInfo& compiled) { 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, 15, [](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); in.Value(sampler.forcePointFiltering); 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); @@ -629,6 +903,31 @@ void decodeInfo(Reader& reader, CompiledShaderInfo& compiled) { reader.Value(info.bdaWrites); reader.Value(info.usesFaultBuffer); reader.Value(info.dispatchThreadLimit); + reader.Value(info.rasterPrimitive); + reader.Value(info.layerExported); +} + +void encodeInfo(Writer& writer, const CompiledShaderInfo& compiled) { + writer.Value(compiled.stage); + writer.Value(compiled.shaderHash); + writer.Value(compiled.waveSize); + writer.Value(compiled.userDataBase); + writer.Value(compiled.userDataCount); + writer.Value(compiled.scratchDwords); + writer.Value(compiled.paramExportMask); + encodeShaderInfo(writer, compiled.info); + encodeLayout(writer, compiled.bindings); +} + +void decodeInfo(Reader& reader, CompiledShaderInfo& compiled) { + reader.Value(compiled.stage); + reader.Value(compiled.shaderHash); + reader.Value(compiled.waveSize); + reader.Value(compiled.userDataBase); + reader.Value(compiled.userDataCount); + reader.Value(compiled.scratchDwords); + reader.Value(compiled.paramExportMask); + decodeShaderInfo(reader, compiled.info); decodeLayout(reader, compiled.bindings); } @@ -644,12 +943,383 @@ void decodeAllocation(Reader& reader, CompiledBindingLayout& allocation) { reader.Value(allocation.pushConstantSizeBytes); } +constexpr std::uint32_t PlanEntryKind = 0x4e414c50u; +constexpr std::uint32_t NoPlanIndex = std::numeric_limits::max(); + +void encodeMemoryInfo(Writer& out, const MemoryInfo& memory) { + out.Value(memory.kind); + out.Value(memory.resource); + out.Value(memory.sampler); + out.Value(memory.offset); + out.Value(memory.secondaryOffset); + out.Value(memory.dmask); + out.Value(memory.dataDwords); + out.Value(memory.dataBits); + out.Value(memory.componentIndex); + out.Value(memory.componentCount); + out.Value(memory.dataFormat); + out.Value(memory.numberFormat); + out.Value(memory.imageSampleFlags); + out.Value(memory.imageDimension); + out.Value(memory.imageAddressComponents); + out.Value(memory.addressIsFull); + out.Value(memory.dataSigned); + out.Value(memory.typed); + out.Value(memory.formatted); + out.Value(memory.d16); + out.Value(memory.imageHasMip); + out.Value(memory.imageR128); + out.Value(memory.imagePacked); + out.Value(memory.imageByElements); + out.Value(memory.idxen); + out.Value(memory.offen); + out.Value(memory.planningOnly); + out.Value(memory.coherent); + out.Value(memory.gpuDescriptor); + out.Value(memory.flushDenormals); + out.Value(memory.flushDenormalResults); +} + +void decodeMemoryInfo(Reader& in, MemoryInfo& memory) { + in.Value(memory.kind); + in.Value(memory.resource); + in.Value(memory.sampler); + in.Value(memory.offset); + in.Value(memory.secondaryOffset); + in.Value(memory.dmask); + in.Value(memory.dataDwords); + in.Value(memory.dataBits); + in.Value(memory.componentIndex); + in.Value(memory.componentCount); + in.Value(memory.dataFormat); + in.Value(memory.numberFormat); + in.Value(memory.imageSampleFlags); + in.Value(memory.imageDimension); + in.Value(memory.imageAddressComponents); + in.Value(memory.addressIsFull); + in.Value(memory.dataSigned); + in.Value(memory.typed); + in.Value(memory.formatted); + in.Value(memory.d16); + in.Value(memory.imageHasMip); + in.Value(memory.imageR128); + in.Value(memory.imagePacked); + in.Value(memory.imageByElements); + in.Value(memory.idxen); + in.Value(memory.offen); + in.Value(memory.planningOnly); + in.Value(memory.coherent); + in.Value(memory.gpuDescriptor); + in.Value(memory.flushDenormals); + in.Value(memory.flushDenormalResults); +} + +bool encodePlan(Writer& writer, const IrResourcePlan& plan) { + 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()); + blocks.reserve(plan.blockStorage.size()); + 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(); + std::uint32_t current = 0; + const auto valueIndex = [&](const IrValue* value) { + if (value == nullptr) return NoPlanIndex; + const auto found = values.find(value); + if (found == values.end()) { + owned = false; + return NoPlanIndex; + } + return found->second; + }; + writer.Value(plan.stage); + writer.Value(plan.shaderHash); + writer.Value(plan.userDataBase); + writer.Value(plan.userDataCount); + writer.Value(plan.srgbDecodeFormats); + writer.List(plan.blockStorage, [](Writer& out, const std::unique_ptr& block) { out.Value(block->Id()); }); + writer.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; + 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); + 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; + 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 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) { + out.Value(valueIndex(read.value)); + out.Value(read.flatOffset); + }); + writer.Values(std::span(plan.guardedSrtSlots)); + writer.Value(plan.srtGuardOffset); + writer.Values(std::span(plan.cleanFlatSlots)); + writer.Values(std::span(plan.pureFlatSlots)); + writer.Value(plan.denseValueIds); + writer.Value(plan.requiresSpecializationMemory); + writer.Value(plan.srtPlanComplete); + writer.Value(plan.resourceTrackingComplete); + encodeShaderInfo(writer, plan.info); + const auto& fill = plan.uniformFill.fill; + writer.Value(fill.kind); + writer.Value(fill.resource); + for (const auto stride : fill.groupStride) writer.Value(stride); + writer.Value(fill.words); + writer.Value(fill.value); + for (const auto* value : plan.uniformFill.values) writer.Value(valueIndex(value)); + return owned; +} + +struct PlanValueRecord { + IrOpcode opcode = IrOpcode::Identity; + IrType type = IrType::Void; + std::uint32_t id = 0; + std::uint64_t flags = 0; + bool hasImmediate = false; + std::uint64_t immediate = 0; + GuestRegister registerValue{}; + std::vector arguments; + std::vector phiBlocks; +}; + +bool decodePlan(Reader& reader, IrResourcePlan& plan) { + reader.Value(plan.stage); + reader.Value(plan.shaderHash); + reader.Value(plan.userDataBase); + reader.Value(plan.userDataCount); + reader.Value(plan.srgbDecodeFormats); + std::vector blockIds; + reader.Values(blockIds); + std::vector records; + std::uint32_t current = 0; + reader.List(records, 6, [&](Reader& in, PlanValueRecord& record) { + constexpr std::uint64_t Word = std::numeric_limits::max(); + const auto opcode = in.Compact(); + const auto type = in.Compact(); + const auto id = in.Compact(); + record.flags = in.Compact(); + const auto registerValue = in.Compact(); + const auto shape = in.Compact(); + const auto arguments = shape >> 1u; + if (opcode >= static_cast(IrOpcode::Count) || type > Word || id > Word || (registerValue >> 4u) > Word || arguments > in.Remaining()) { + in.Fail(); + return; + } + record.opcode = static_cast(opcode); + record.type = static_cast(type); + record.id = static_cast(id); + record.registerValue = {static_cast(registerValue & 0xfu), static_cast(registerValue >> 4u)}; + record.hasImmediate = (shape & 1u) != 0u; + if (record.hasImmediate) record.immediate = in.Compact(); + record.arguments.resize(static_cast(arguments)); + for (auto& argument : record.arguments) { + const auto code = in.Compact(); + if (code == 0u) { + argument = NoPlanIndex; + continue; + } + const auto zigzag = code - 1u; + const auto delta = static_cast(zigzag >> 1u) ^ -static_cast(zigzag & 1u); + const auto index = static_cast(current) + delta; + if (index < 0 || index >= static_cast(records.size())) { + in.Fail(); + return; + } + argument = static_cast(index); + } + if (record.opcode == IrOpcode::Phi) { + record.phiBlocks.resize(record.arguments.size()); + for (auto& block : record.phiBlocks) { + const auto index = in.Compact(); + block = index < blockIds.size() ? static_cast(index) : NoPlanIndex; + if (block == NoPlanIndex) in.Fail(); + } + } + ++current; + }); + if (!reader.Ok()) return false; + plan.blockStorage.reserve(blockIds.size()); + for (const auto id : blockIds) plan.blockStorage.push_back(std::make_unique(id)); + plan.valueStorage.Reserve(records.size()); + for (const auto& record : records) { + auto* value = plan.valueStorage.Create(record.opcode, record.type, record.id); + value->SetFlags(record.flags); + if (record.hasImmediate) value->SetImmediateU64(record.immediate); + value->SetRegister(record.registerValue); + } + bool valid = true; + const auto valueAt = [&](std::uint32_t index) -> IrValue* { + if (index == NoPlanIndex) return nullptr; + if (index >= plan.valueStorage.Size()) { + valid = false; + return nullptr; + } + return &plan.valueStorage[index]; + }; + for (std::size_t index = 0; index < records.size(); ++index) { + const auto& record = records[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) { + 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, 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 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, 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) { + read.value = valueAt(in.Get()); + in.Value(read.flatOffset); + }); + reader.Values(plan.guardedSrtSlots); + reader.Value(plan.srtGuardOffset); + reader.Values(plan.cleanFlatSlots); + reader.Values(plan.pureFlatSlots); + reader.Value(plan.denseValueIds); + reader.Value(plan.requiresSpecializationMemory); + reader.Value(plan.srtPlanComplete); + reader.Value(plan.resourceTrackingComplete); + decodeShaderInfo(reader, plan.info); + auto& fill = plan.uniformFill.fill; + reader.Value(fill.kind); + reader.Value(fill.resource); + for (auto& stride : fill.groupStride) reader.Value(stride); + reader.Value(fill.words); + reader.Value(fill.value); + for (auto& value : plan.uniformFill.values) value = valueAt(reader.Get()); + if (!valid || !reader.Done() || fill.words > plan.uniformFill.values.size()) return false; + const auto knownSource = [&](std::uint32_t source) { return source < plan.descriptorSources.size(); }; + if (!std::ranges::all_of(plan.materializationSources, knownSource)) return false; + if (!std::ranges::all_of(plan.descriptorSources, [&](const DescriptorSource& source) { return source.dwordCount <= source.dwords.size() && (!source.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; + 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.flatSlots, [&](std::uint32_t slot) { return slot < plan.srtReads.size(); }); + }); + if (!blocksValid) return false; + ResourceMaterializer::CompactPlan(plan); + return true; +} + constexpr std::string_view NeutralSwitches[] = { "APS5_PROFILE_DRAW", "APS5_DUMP_IR", "APS5_NO_CODE_HASH_KEY", "APS5_NO_FAILURE_MEMO", + "APS5_NO_PERF_FRONTEND_PAIR", + "APS5_NO_PERF_DISK_PLANS", + "APS5_NO_PERF_DOM_INTERSECTION", + "APS5_NO_PERF_ITERATION_SELECTION", + "APS5_NO_PERF_LAYOUT_PAIR", "APS5_NO_RESULT_MEMO", + "APS5_NO_TABLE_RESOLUTION_REUSE", + "APS5_NO_TABLE_SNAPSHOT_REUSE", }; const std::vector& switchKey() { @@ -674,57 +1344,85 @@ std::string hex(std::uint64_t value) { return text; } +std::vector wrapEntry(std::span key, std::span payload) { + const FileHeader header{FileMagic, FormatVersion, SourceVersion(), key.size(), payload.size(), HashBytes(key), HashBytes(payload)}; + const auto payloadOffset = PayloadOffset(key.size()); + std::vector file(payloadOffset + payload.size()); + std::memcpy(file.data(), &header, sizeof(header)); + std::memcpy(file.data() + sizeof(header), key.data(), key.size()); + if (!payload.empty()) std::memcpy(file.data() + payloadOffset, payload.data(), payload.size()); + return file; +} + +LoadStatus unwrapEntry(std::span file, std::span key, std::span& payload) { + if (file.size() < sizeof(FileHeader)) return LoadStatus::Rejected; + FileHeader header; + std::memcpy(&header, file.data(), sizeof(header)); + if (header.magic != FileMagic || header.format != FormatVersion || header.sourceVersion != SourceVersion()) return LoadStatus::Rejected; + const auto body = file.size() - sizeof(header); + if (header.keyBytes > body) return LoadStatus::Rejected; + const auto payloadOffset = PayloadOffset(static_cast(header.keyBytes)); + if (payloadOffset > file.size() || header.payloadBytes != file.size() - payloadOffset) return LoadStatus::Rejected; + const auto storedKey = file.subspan(sizeof(header), static_cast(header.keyBytes)); + const auto padding = file.subspan(sizeof(header) + storedKey.size(), payloadOffset - sizeof(header) - storedKey.size()); + if (std::ranges::any_of(padding, [](std::byte value) { return value != std::byte{0}; })) return LoadStatus::Rejected; + payload = file.subspan(payloadOffset); + if (HashBytes(storedKey) != header.keyHash) return LoadStatus::Rejected; + if (storedKey.size() != key.size() || !std::equal(storedKey.begin(), storedKey.end(), key.begin())) return LoadStatus::KeyMismatch; + if (HashBytes(payload) != header.payloadHash) return LoadStatus::Rejected; + return LoadStatus::Loaded; +} + +std::atomic 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) { - const auto started = std::chrono::steady_clock::now(); - const auto name = EntryName(key); - thread_local std::vector* fileSlot = nullptr; - auto& file = ThreadOwned(fileSlot); - bool loaded = false; - if (!ReadWholeFile(directory / name, file)) { - misses.fetch_add(1, std::memory_order_relaxed); - } else { - switch (DecodeEntry(file, key, variant)) { - case LoadStatus::Loaded: - hits.fetch_add(1, std::memory_order_relaxed); - bytesRead.fetch_add(file.size(), std::memory_order_relaxed); - loaded = true; - break; - case LoadStatus::Absent: - case LoadStatus::KeyMismatch: - misses.fetch_add(1, std::memory_order_relaxed); - break; - case LoadStatus::Rejected: { - const auto failures = loadFailures.fetch_add(1, std::memory_order_relaxed); - if (failures < 8) std::fprintf(stderr, "[shader-disk-cache] rejected %s (%zu bytes): truncated, corrupt or another format; recompiling\n", name.c_str(), file.size()); - break; - } - } - } - if (file.capacity() > (4u << 20u)) std::vector().swap(file); - loadNanoseconds.fetch_add(static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - started).count()), std::memory_order_relaxed); - report(false); - return loaded; + 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); + } + + bool LoadPlan(std::span key, IrResourcePlan& plan) { + return load(key, [&](std::span file, std::shared_ptr) { return DecodePlanEntry(file, key, plan); }, planHits, planMisses, planLoadFailures); } void Store(std::vector key, std::shared_ptr variant) { - { - std::lock_guard lock(mutex); - jobs.push_back({std::move(key), std::move(variant)}); - } - wake.notify_one(); + enqueue({std::move(key), std::move(variant), nullptr}); + } + + void StorePlan(std::vector key, std::shared_ptr plan) { + enqueue({std::move(key), nullptr, std::move(plan)}); + } + + std::shared_ptr Persist(std::span key, const CompiledVariant& variant) { + const auto path = directory / EntryName(key); + const auto file = EncodeEntry(key, variant); + if (!WriteFileAtomically(path, file)) return nullptr; + writes.fetch_add(1, std::memory_order_relaxed); + bytesWritten.fetch_add(file.size(), std::memory_order_relaxed); + auto mapped = MapFile(path); + if (mapped == nullptr) return nullptr; + auto persisted = std::make_shared(); + const auto bytes = mapped->Bytes(); + if (DecodeEntry(bytes, key, *persisted, std::move(mapped)) != LoadStatus::Loaded) return nullptr; + persisted->layout = variant.layout; + persisted->artifact.variantId = variant.artifact.variantId; + return persisted; } void Flush() { @@ -732,6 +1430,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); @@ -741,21 +1452,187 @@ public: counters.writeFailures = writeFailures.load(std::memory_order_relaxed); counters.bytesRead = bytesRead.load(std::memory_order_relaxed); counters.bytesWritten = bytesWritten.load(std::memory_order_relaxed); + counters.planHits = planHits.load(std::memory_order_relaxed); + counters.planMisses = planMisses.load(std::memory_order_relaxed); + counters.planWrites = planWrites.load(std::memory_order_relaxed); + counters.planLoadFailures = planLoadFailures.load(std::memory_order_relaxed); + 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(); + const auto name = EntryName(key); + thread_local std::vector* fileSlot = nullptr; + auto& file = ThreadOwned(fileSlot); + bool loaded = false; + std::span bytes; + std::shared_ptr owner; + if (auto mapped = MapFile(directory / name)) { + bytes = mapped->Bytes(); + owner = std::move(mapped); + } + const bool present = owner != nullptr || ReadWholeFile(directory / name, file); + if (owner == nullptr) bytes = file; + if (!present) { + missCount.fetch_add(1, std::memory_order_relaxed); + } else { + switch (decode(bytes, std::move(owner))) { + case LoadStatus::Loaded: + hitCount.fetch_add(1, std::memory_order_relaxed); + bytesRead.fetch_add(bytes.size(), std::memory_order_relaxed); + loaded = true; + break; + case LoadStatus::Absent: + case LoadStatus::KeyMismatch: + missCount.fetch_add(1, std::memory_order_relaxed); + break; + case LoadStatus::Rejected: { + const auto failures = failureCount.fetch_add(1, std::memory_order_relaxed); + if (failures < 8) std::fprintf(stderr, "[shader-disk-cache] rejected %s (%zu bytes): truncated, corrupt or another format; recompiling\n", name.c_str(), bytes.size()); + break; + } + } + } + if (file.capacity() > (4u << 20u)) std::vector().swap(file); + loadNanoseconds.fetch_add(static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - started).count()), std::memory_order_relaxed); + report(false); + return loaded; + } + + void enqueue(Job job) { + { + std::unique_lock lock(mutex); + if (stopping) { + lock.unlock(); + write(job); + return; + } + jobs.push_back(std::move(job)); + } + wake.notify_one(); + } + void run() { std::fprintf(stderr, "[shader-disk-cache] %s (source version %s, format %u)\n", directory.string().c_str(), hex(SourceVersion()).c_str(), FormatVersion); housekeeping(); 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(); @@ -767,16 +1644,18 @@ 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) { - const auto file = EncodeEntry(job.key, *job.variant); - if (WriteFileAtomically(directory / EntryName(job.key), file)) { - writes.fetch_add(1, std::memory_order_relaxed); + const auto file = job.plan != nullptr ? EncodePlanEntry(job.key, *job.plan) : EncodeEntry(job.key, *job.variant); + if (!file.empty() && WriteFileAtomically(directory / EntryName(job.key), file)) { + (job.plan != nullptr ? planWrites : writes).fetch_add(1, std::memory_order_relaxed); bytesWritten.fetch_add(file.size(), std::memory_order_relaxed); } else { const auto failures = writeFailures.fetch_add(1, std::memory_order_relaxed); @@ -790,6 +1669,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(); @@ -826,31 +1710,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) return; - std::fprintf(stderr, "[shader-disk-cache] (10 s): %llu hits, %llu misses, %llu writes (%.1f MiB), %llu load failures, %llu write failures; %.1f MiB read in %.1f ms (totals: %llu hits, %llu misses, %llu writes)\n", + 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), static_cast(totals.bytesRead - reported.bytesRead) / (1024.0 * 1024.0), static_cast(loadNs - reportedLoadNs) / 1e6, - static_cast(totals.hits), static_cast(totals.misses), static_cast(totals.writes)); + delta(totals.loadFailures, reported.loadFailures), delta(totals.writeFailures, reported.writeFailures), + delta(totals.planHits, reported.planHits), delta(totals.planMisses, reported.planMisses), delta(totals.planWrites, reported.planWrites), delta(totals.planLoadFailures, reported.planLoadFailures), + static_cast(totals.bytesRead - reported.bytesRead) / (1024.0 * 1024.0), static_cast(loadNs - reportedLoadNs) / 1e6, + static_cast(totals.hits), static_cast(totals.misses), static_cast(totals.writes), static_cast(totals.planHits), static_cast(totals.planMisses)); + 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; } @@ -880,6 +1794,23 @@ void BuildKey(const RecompileRequest& request, std::uint32_t hostSubgroupSize, s key.insert(key.end(), switches.begin(), switches.end()); } +void BuildPlanKey(const RecompileRequest& request, std::uint32_t hostSubgroupSize, std::vector& key) { + key.clear(); + Writer writer(key); + writer.Value(FileMagic); + writer.Value(FormatVersion); + writer.Value(SourceVersion()); + writer.Value(PlanEntryKind); + thread_local std::vector* memoryKeySlot = nullptr; + auto& memoryKey = ThreadOwned(memoryKeySlot); + RecompileCacheKey::Build(request, memoryKey); + writer.Values(std::span(memoryKey)); + writer.Values(request.shader.code); + writer.Value(hostSubgroupSize); + const auto& switches = switchKey(); + key.insert(key.end(), switches.begin(), switches.end()); +} + std::string EntryName(std::span key) { return hex(HashBytes(key, 0x5eed0001ull)) + hex(HashBytes(key, 0x5eed0002ull)) + ".bin"; } @@ -888,12 +1819,16 @@ void EncodeResult(const RecompileResult& result, std::vector& out) { Writer writer(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; } @@ -905,27 +1840,14 @@ std::vector EncodeEntry(std::span key, const Compile encodeArtifact(writer, variant.artifact); encodeInfo(writer, variant.info); encodeAllocation(writer, variant.bindings); - const FileHeader header{FileMagic, FormatVersion, SourceVersion(), key.size(), payload.size(), HashBytes(key), HashBytes(payload)}; - std::vector file(sizeof(header) + key.size() + payload.size()); - std::memcpy(file.data(), &header, sizeof(header)); - std::memcpy(file.data() + sizeof(header), key.data(), key.size()); - std::memcpy(file.data() + sizeof(header) + key.size(), payload.data(), payload.size()); - return file; + return wrapEntry(key, payload); } -LoadStatus DecodeEntry(std::span file, std::span key, CompiledVariant& variant) { - if (file.size() < sizeof(FileHeader)) return LoadStatus::Rejected; - FileHeader header; - std::memcpy(&header, file.data(), sizeof(header)); - if (header.magic != FileMagic || header.format != FormatVersion || header.sourceVersion != SourceVersion()) return LoadStatus::Rejected; - const auto body = file.size() - sizeof(header); - if (header.keyBytes > body || header.payloadBytes != body - header.keyBytes) return LoadStatus::Rejected; - const auto storedKey = file.subspan(sizeof(header), static_cast(header.keyBytes)); - const auto payload = file.subspan(sizeof(header) + static_cast(header.keyBytes)); - if (HashBytes(storedKey) != header.keyHash) return LoadStatus::Rejected; - if (storedKey.size() != key.size() || !std::equal(storedKey.begin(), storedKey.end(), key.begin())) return LoadStatus::KeyMismatch; - if (HashBytes(payload) != header.payloadHash) return LoadStatus::Rejected; - Reader reader(payload); +LoadStatus DecodeEntry(std::span file, std::span key, CompiledVariant& variant, std::shared_ptr owner) { + std::span payload; + const auto status = unwrapEntry(file, key, payload); + if (status != LoadStatus::Loaded) return status; + Reader reader(payload, std::move(owner)); CompiledVariant decoded; decodeArtifact(reader, decoded.artifact); decodeInfo(reader, decoded.info); @@ -937,6 +1859,32 @@ LoadStatus DecodeEntry(std::span file, std::span& out) { + Writer writer(out); + return encodePlan(writer, plan); +} + +bool DecodePlan(std::span bytes, IrResourcePlan& plan) { + Reader reader(bytes); + IrResourcePlan decoded; + if (!decodePlan(reader, decoded)) return false; + plan = std::move(decoded); + return true; +} + +std::vector EncodePlanEntry(std::span key, const IrResourcePlan& plan) { + std::vector payload; + if (!EncodePlan(plan, payload)) return {}; + return wrapEntry(key, payload); +} + +LoadStatus DecodePlanEntry(std::span file, std::span key, IrResourcePlan& plan) { + std::span payload; + const auto status = unwrapEntry(file, key, payload); + if (status != LoadStatus::Loaded) return status; + return DecodePlan(payload, plan) ? LoadStatus::Loaded : LoadStatus::Rejected; +} + bool Enabled() { return store().Enabled(); } @@ -955,11 +1903,41 @@ void Store(std::vector key, std::shared_ptr va if (instance.Enabled()) instance.Store(std::move(key), std::move(variant)); } +bool LoadPlan(std::span key, IrResourcePlan& plan) { + auto& instance = store(); + return instance.Enabled() && instance.LoadPlan(key, plan); +} + +void StorePlan(std::vector key, std::shared_ptr plan) { + auto& instance = store(); + if (instance.Enabled() && plan != nullptr) instance.StorePlan(std::move(key), std::move(plan)); +} + +std::shared_ptr Persist(std::span key, const CompiledVariant& variant) { + auto& instance = store(); + return instance.Enabled() ? instance.Persist(key, variant) : nullptr; +} + void Flush() { auto& instance = store(); if (instance.Enabled()) instance.Flush(); } +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 f8254462b9..e5acfa71f9 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -2,6 +2,7 @@ #define CORE_SHADER_RECOMPILER_SHADERDISKCACHE_HPP #include "CompiledVariant.hpp" +#include "IntermediateRepresentation/IrMetadata/ResourcePlan.hpp" #include #include #include @@ -12,7 +13,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 22; +inline constexpr std::uint32_t FormatVersion = 44; enum class LoadStatus { Loaded, @@ -29,6 +30,20 @@ struct Counters { std::uint64_t writeFailures = 0; std::uint64_t bytesRead = 0; std::uint64_t bytesWritten = 0; + std::uint64_t planHits = 0; + std::uint64_t planMisses = 0; + std::uint64_t planWrites = 0; + std::uint64_t planLoadFailures = 0; + 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(); @@ -38,17 +53,28 @@ 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); +[[nodiscard]] LoadStatus DecodeEntry(std::span file, std::span key, CompiledVariant& variant, std::shared_ptr owner = nullptr); + +void BuildPlanKey(const RecompileRequest& request, std::uint32_t hostSubgroupSize, std::vector& key); +[[nodiscard]] bool EncodePlan(const IrResourcePlan& plan, std::vector& out); +[[nodiscard]] bool DecodePlan(std::span bytes, IrResourcePlan& plan); +[[nodiscard]] std::vector EncodePlanEntry(std::span key, const IrResourcePlan& plan); +[[nodiscard]] LoadStatus DecodePlanEntry(std::span file, std::span key, IrResourcePlan& plan); [[nodiscard]] bool Enabled(); [[nodiscard]] std::filesystem::path EntryDirectory(); [[nodiscard]] bool Load(std::span key, CompiledVariant& variant); void Store(std::vector key, std::shared_ptr variant); +[[nodiscard]] std::shared_ptr Persist(std::span key, const CompiledVariant& variant); +[[nodiscard]] bool LoadPlan(std::span key, IrResourcePlan& plan); +void StorePlan(std::vector key, std::shared_ptr plan); void Flush(); +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 f032957af5..ee435ffc67 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp @@ -15,18 +15,24 @@ 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; std::vector extensions; }; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp index 32b263cd15..bce467f22c 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp @@ -17,6 +17,7 @@ void DefineBdaFaultFunction(SpirvEmitterState& state); void DefineBdaDwordReadFunctions(SpirvEmitterState& state); void DefineBdaSpanReadFunctions(SpirvEmitterState& state); void RecordBdaFault(SpirvEmitterState& state, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction, BdaAbi::FaultReason reason); +void RecordBdaFaultReason(SpirvEmitterState& state, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction, std::uint32_t reason); void RecordBdaFaultWords(SpirvEmitterState& state, std::uint32_t addressLow, std::uint32_t addressHigh, std::uint32_t bytes, std::uint32_t instruction, BdaAbi::FaultReason reason); void ReturnBdaFailureIf(SpirvEmitterState& state, std::uint32_t condition, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction, BdaAbi::FaultReason reason); void ValidateBdaTarget(const IrProgram& program, const SpirvTargetOptions& target); @@ -24,17 +25,29 @@ void ValidateBdaTarget(const IrProgram& program, const SpirvTargetOptions& targe // every invocation running, so their faulting reads return zero instead. 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); +void EmitBdaDwordWrites(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, std::uint32_t dwords, std::uint32_t value); std::uint32_t EmitBdaAtomic(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t bytes, const std::function& operation); // Reads the dwords of a 1-4 dword load at address + offset that the program extracts, with one // table lookup for the whole span; the per-byte lookups of EmitBdaRead remain the fallback (and // the only path under APS5_BDA_BYTE_READS=1). Dwords the program never extracts are neither read -std::array EmitBdaDwordReads(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, std::uint32_t dwords, bool everyDword = false); +std::array EmitBdaDwordReads(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, std::uint32_t dwords, bool everyDword = false, std::uint32_t usedDwords = 0u); bool BdaByteReadsForced(); std::uint32_t AddBdaAddress(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, bool subtract); +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 d221ffe2ec..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 @@ -20,12 +21,37 @@ struct SpirvTargetOptions { std::span supportedExtensions; 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 07af37ad01..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,10 +13,12 @@ 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); std::uint32_t TypeBoolVector(SpirvEmitterState& state, std::uint32_t components); +std::uint32_t TypeU8(SpirvEmitterState& state); std::uint32_t TypeU32(SpirvEmitterState& state); std::uint32_t TypeU64(SpirvEmitterState& state); std::uint32_t TypeScalarU64(SpirvEmitterState& state); @@ -33,6 +36,9 @@ std::uint32_t TypeStorageBufferPointer(SpirvEmitterState& state); std::uint32_t TypeStorageBufferElementPointer(SpirvEmitterState& state); std::uint32_t TypeStorageBufferU64Pointer(SpirvEmitterState& state); std::uint32_t TypeStorageBufferU64ElementPointer(SpirvEmitterState& state); +std::uint32_t TypeStorageBufferU8Block(SpirvEmitterState& state); +std::uint32_t TypeStorageBufferU8Pointer(SpirvEmitterState& state); +std::uint32_t TypeStorageBufferU8ElementPointer(SpirvEmitterState& state); std::uint32_t TypePhysicalU32Pointer(SpirvEmitterState& state); std::uint32_t TypePushConstantElementPointer(SpirvEmitterState& state); std::uint32_t TypeU32ArrayPointer(SpirvEmitterState& state, std::uint32_t storageClass, std::uint32_t dwords); @@ -57,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); @@ -101,11 +110,20 @@ std::uint32_t EmitBinaryU32(SpirvEmitterState& state, std::uint32_t opcode, std: std::uint32_t EmitShaderDataDwordLoad(SpirvEmitterState& state, std::uint32_t dwordIndex); void EmitMemoryOffsets(SpirvEmitterState& state); std::uint32_t LdsDwordCount(const SpirvEmitterState& state); +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); @@ -128,11 +146,14 @@ F32Class EmitClassifyF32(SpirvEmitterState& state, std::uint32_t value); std::uint32_t EmitClassMaskBitMatch(SpirvEmitterState& state, std::uint32_t mask, std::uint32_t bit, std::uint32_t classMatch); std::uint32_t EmitClassMaskF32(SpirvEmitterState& state, std::uint32_t value, std::uint32_t mask); std::uint32_t EmitMinMaxF32Value(SpirvEmitterState& state, std::uint32_t lhs, std::uint32_t rhs, bool maxValue); -std::uint32_t EmitFlushF32DenormToSignedZero(SpirvEmitterState& state, std::uint32_t value); std::uint32_t EmitTrigCycleF32(SpirvEmitterState& state, std::uint32_t src, bool preserveSignedZero); +void EmitF16DenormPreserve(SpirvEmitterState& state); std::uint32_t EmitF16BitsToF32(SpirvEmitterState& state, std::uint32_t bits); +std::uint32_t EmitF16BitsToF32AnyNan(SpirvEmitterState& state, std::uint32_t bits); +std::uint32_t EmitF16BitsToF32Decoded(SpirvEmitterState& state, std::uint32_t bits); void EmitProgram(SpirvEmitterState& state); void DefineGetBdaPointer(SpirvEmitterState& state); +void DefineDot2F32F16Function(SpirvEmitterState& state); void EmitLabel(SpirvEmitterState& state, std::uint32_t label); std::uint32_t Unary(SpirvEmitterState& state, std::uint32_t opcode, std::uint32_t type, std::uint32_t value); std::uint32_t Binary(SpirvEmitterState& state, std::uint32_t opcode, std::uint32_t type, std::uint32_t lhs, std::uint32_t rhs); @@ -223,10 +244,10 @@ std::uint32_t EmitValueIfElse(SpirvEmitterState& state, std::uint32_t condition, template std::uint32_t AtomicUpdateTyped(SpirvEmitterState& state, std::uint32_t pointer, ResourceKind kind, std::uint32_t type, TFunction&& function) { const auto scope = kind == ResourceKind::Lds ? spv::ScopeWorkgroup : spv::ScopeDevice; - const auto memory = [&]() { + const auto memory = [&]() -> std::uint32_t { switch (kind) { case ResourceKind::Lds: - return spv::MemorySemanticsWorkgroupMemoryMask; + return LdsMemorySemantics(state); case ResourceKind::Image: return spv::MemorySemanticsImageMemoryMask; default: @@ -255,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 a2f5aa8d5c..a8f03e192a 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp @@ -13,6 +13,8 @@ std::uint32_t EmitConvertU32F32(SpirvEmitterState& state, std::uint32_t arg0); std::uint32_t EmitConvertF32S32(SpirvEmitterState& state, std::uint32_t arg0); std::uint32_t EmitCompositeExtractU64(SpirvEmitterState& state, std::uint32_t arg0, const IrValue* arg1); std::uint32_t EmitPackFloat2x16Rtz(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1); +std::uint32_t EmitFPAdd16x2(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1); +std::uint32_t EmitFPMul16x2(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1); std::uint32_t EmitFPSaturate32(SpirvEmitterState& state, std::uint32_t arg0); std::uint32_t EmitIAdd64(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1); std::uint32_t EmitISub64(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1); @@ -59,9 +61,10 @@ std::uint32_t EmitFPSin(SpirvEmitterState& state, std::uint32_t arg0); std::uint32_t EmitFPCos(SpirvEmitterState& state, std::uint32_t arg0); std::uint32_t EmitIdentity(SpirvValueEmitContext& ctx, std::uint32_t value); void EmitVoid(SpirvValueEmitContext& context); -void EmitBarrier(SpirvEmitterState& state); +void EmitBarrier(SpirvValueEmitContext& ctx, const IrValue& inst); std::uint32_t EmitMeshDrawParameter(SpirvValueEmitContext& ctx, const IrValue& inst); std::uint32_t EmitMeshArgument(SpirvValueEmitContext& ctx, const IrValue& inst); +std::uint32_t EmitMeshRestartStart(SpirvValueEmitContext& ctx, const IrValue& inst); std::uint32_t EmitGetTessellationAttribute(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitSetTessellationAttribute(SpirvValueEmitContext& ctx, const IrValue& inst); std::uint32_t EmitGetUserData(SpirvEmitterState& state, ScalarReg reg); @@ -165,7 +168,6 @@ std::uint32_t EmitAndU32(SpirvEmitterState& state, std::uint32_t arg0, std::uint std::uint32_t EmitLogicalAndBool(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1); std::uint32_t EmitLogicalOrBool(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1); std::uint32_t EmitLogicalNotBool(SpirvEmitterState& state, std::uint32_t arg0); -std::uint32_t EmitTruncF32Value(SpirvEmitterState& state, std::uint32_t arg0); std::uint32_t EmitFNegateValue(SpirvEmitterState& state, std::uint32_t arg0); std::uint32_t EmitFAbsValue(SpirvEmitterState& state, std::uint32_t arg0); std::uint32_t EmitPackHalf2x16(SpirvEmitterState& state, std::uint32_t arg0); @@ -209,7 +211,7 @@ std::uint32_t EmitBitCastF16U16(SpirvEmitterState& state, std::uint32_t arg0); std::uint32_t EmitConvertU32U16(SpirvEmitterState& state, std::uint32_t arg0); std::uint32_t EmitConvertU32U8(SpirvEmitterState& state, std::uint32_t arg0); std::uint32_t EmitBitCastU32F32(SpirvEmitterState& state, std::uint32_t arg0); -std::uint32_t EmitConvertF32F16(SpirvEmitterState& state, std::uint32_t bits); +std::uint32_t EmitConvertF32F16(SpirvEmitterState& state, const IrValue& inst, std::uint32_t bits); std::uint32_t EmitCompositeExtractU32x2(SpirvEmitterState& state, std::uint32_t arg0, const IrValue* arg1); std::uint32_t EmitCompositeExtractU32x3(SpirvEmitterState& state, std::uint32_t arg0, const IrValue* arg1); std::uint32_t EmitCompositeExtractU32x4(SpirvEmitterState& state, std::uint32_t arg0, const IrValue* arg1); @@ -218,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); @@ -243,6 +246,9 @@ void EmitLoadAddressU32x4(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitStoreAddressU8(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitStoreAddressU16(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitStoreAddressU32(SpirvValueEmitContext& ctx, const IrValue& inst); +void EmitStoreAddressU32x2(SpirvValueEmitContext& ctx, const IrValue& inst); +void EmitStoreAddressU32x3(SpirvValueEmitContext& ctx, const IrValue& inst); +void EmitStoreAddressU32x4(SpirvValueEmitContext& ctx, const IrValue& inst); std::uint32_t EmitAddressAtomic(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitLoadBufferU8(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitLoadBufferU16(SpirvValueEmitContext& ctx, const IrValue& inst); @@ -341,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); @@ -385,6 +393,8 @@ void EmitGetScalarRegister(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitSetScalarRegister(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitGetVectorRegister(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitSetVectorRegister(SpirvValueEmitContext& ctx, const IrValue& inst); +void EmitLoadVectorWindow(SpirvValueEmitContext& ctx, const IrValue& inst); +void EmitStoreVectorWindow(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitGetGotoVariable(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitSetGotoVariable(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitGetScc(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 7a4988803e..dc3a268ee3 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include #include #include @@ -71,37 +73,76 @@ 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; - std::uint32_t tessPatchBase = 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; + std::uint32_t storageBufferCoherentVariable = 0; + std::uint32_t storageBufferU8Variable = 0; std::vector memoryByteOffsets; + std::map, std::uint32_t> formattedBufferFunctions; + std::map, std::uint32_t> formattedGpuBufferFunctions; + std::uint32_t bufferFormatTable = 0; std::uint32_t bdaPagetableVariable = 0; std::uint32_t faultBufferVariable = 0; std::uint32_t bdaPointerFunction = 0; @@ -109,19 +150,37 @@ struct SpirvEmitterState { // every read takes the byte path. std::uint32_t bdaProbeFunction = 0; std::uint32_t bdaWritePointerFunction = 0; + std::uint32_t bdaWriteProbeFunction = 0; std::uint32_t bdaAtomicPointerFunction = 0; std::uint32_t bdaNoteWriteFunction = 0; std::array bdaByteWriteFunctions {}; std::uint32_t bdaFaultFunction = 0; 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). bool bdaStopsInvocations = true; + bool continueTarget = false; // 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; @@ -134,11 +193,24 @@ struct SpirvEmitterState { std::uint32_t shaderDataStorageVariable = 0; 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; @@ -147,15 +219,23 @@ 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; + std::optional zeroPositionClipIndex; std::uint32_t cullDistanceVariable = 0; std::uint32_t layerVariable = 0; std::uint32_t viewportIndexVariable = 0; 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; @@ -171,6 +251,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 ee30b65067..53a851625b 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp @@ -11,10 +11,15 @@ std::uint32_t EmitBinaryU32(SpirvEmitterState& state, std::uint32_t opcode, std: std::uint32_t EmitShaderDataDwordLoad(SpirvEmitterState& state, std::uint32_t dwordIndex); void EmitMemoryOffsets(SpirvEmitterState& state); std::uint32_t LdsDwordCount(const SpirvEmitterState& state); +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 EmitStorageBufferElementPointer(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index, std::uint32_t pointerType); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp index 1650714e30..b38633af2e 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp @@ -7,15 +7,19 @@ namespace ShaderRecompiler { +std::uint32_t RuntimeImageDword(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t member); std::uint32_t ResourceForDescriptor(const SpirvEmitterState& state, DescriptorBindingKind kind, std::uint32_t resource); std::uint32_t DescriptorElementPointer(SpirvEmitterState& state, std::uint32_t resultPtrType, std::uint32_t variableId, std::uint32_t arrayIndex, DescriptorBindingKind kind, std::uint32_t resource, const char* variableName); std::uint32_t ImageScalarType(SpirvEmitterState& state, IrTextureNumericClass numericClass); 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 c7e4746b6a..a24284d37c 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvSubgroup.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvSubgroup.hpp @@ -6,6 +6,13 @@ 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); std::uint32_t EmitWaveBallot(SpirvEmitterState& state, std::uint32_t ballot); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvTypes.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvTypes.hpp index c1fb1ae9c0..bf9ea5769c 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvTypes.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvTypes.hpp @@ -9,6 +9,7 @@ namespace ShaderRecompiler { std::uint32_t TypeVoid(SpirvEmitterState& state); std::uint32_t TypeBool(SpirvEmitterState& state); std::uint32_t TypeBoolVector(SpirvEmitterState& state, std::uint32_t components); +std::uint32_t TypeU8(SpirvEmitterState& state); std::uint32_t TypeU32(SpirvEmitterState& state); std::uint32_t TypeU64(SpirvEmitterState& state); std::uint32_t TypeScalarU64(SpirvEmitterState& state); @@ -26,6 +27,9 @@ std::uint32_t TypeStorageBufferPointer(SpirvEmitterState& state); std::uint32_t TypeStorageBufferElementPointer(SpirvEmitterState& state); std::uint32_t TypeStorageBufferU64Pointer(SpirvEmitterState& state); std::uint32_t TypeStorageBufferU64ElementPointer(SpirvEmitterState& state); +std::uint32_t TypeStorageBufferU8Block(SpirvEmitterState& state); +std::uint32_t TypeStorageBufferU8Pointer(SpirvEmitterState& state); +std::uint32_t TypeStorageBufferU8ElementPointer(SpirvEmitterState& state); std::uint32_t TypePhysicalU32Pointer(SpirvEmitterState& state); std::uint32_t TypePushConstantElementPointer(SpirvEmitterState& state); std::uint32_t TypeU32ArrayPointer(SpirvEmitterState& state, std::uint32_t storageClass, std::uint32_t dwords); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp index 15e5f762c6..a6a4b13296 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp @@ -52,9 +52,13 @@ 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); void PatchDeferredPhi(SpirvDeferredPhi phi, std::size_t incoming, std::uint32_t value, std::uint32_t parent); @@ -138,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 { @@ -159,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())) { @@ -253,6 +259,14 @@ private: std::vector typeDeclarations; 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 new file mode 100644 index 0000000000..7e1ba593b4 --- /dev/null +++ b/core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp @@ -0,0 +1,448 @@ +#include "Recompiler.hpp" +#include "SpirvBackend/SpirvClipDistances.hpp" +#include "SpirvBackend/SpirvModule.hpp" +#if ANYPS5_ENABLE_SPIRV_TOOLS +#include "SpirvBackend/SpirvOptimizer.hpp" +#endif +#include +#include +#include +#include +#include +#include + +namespace ShaderRecompiler { +namespace { + +constexpr std::uint32_t ParameterLocations = 32u; + +void require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error("Barycentric geometry SPIR-V: " + message); +} + +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: + explicit BarycentricGeometryEmitter(std::uint32_t version) : builder(version) { + builder.AddMemoryModel(spv::AddressingModelLogical, spv::MemoryModelGLSL450); + voidType = builder.Type(spv::OpTypeVoid); + 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); + perVertexType = builder.DecoratedType(spv::OpTypeStruct, {{spv::OpMemberDecorate, {0u, spv::DecorationBuiltIn, spv::BuiltInPosition}}, {spv::OpDecorate, {spv::DecorationBlock}}}, vec4FloatType); + } + + std::vector Emit(const std::vector& passed, const std::vector& perVertex, const BarycentricEmulationLayout& layout) { + builder.EmitCapability(spv::CapabilityShader); + builder.EmitCapability(spv::CapabilityGeometry); + main = result(spv::OpFunction, voidType, spv::FunctionControlMaskNone, functionType); + builder.AddExecutionMode(main, spv::ExecutionModeTriangles); + builder.AddExecutionMode(main, spv::ExecutionModeInvocations, 1u); + builder.AddExecutionMode(main, spv::ExecutionModeOutputTriangleStrip); + builder.AddExecutionMode(main, spv::ExecutionModeOutputVertices, 3u); + + const auto glIn = addInterface(spv::StorageClassInput, builder.Type(spv::OpTypeArray, perVertexType, constant(uintType, 3u))); + std::set sources(passed.begin(), passed.end()); + for (const auto& parameter : perVertex) sources.insert(parameter.sourceLocation); + std::array inputs {}; + for (const auto location : sources) { + inputs[location] = addInterface(spv::StorageClassInput, vertexArrayType); + decorate(inputs[location], spv::DecorationLocation, location); + } + const auto position = addInterface(spv::StorageClassOutput, vec4FloatType); + decorate(position, spv::DecorationBuiltIn, spv::BuiltInPosition); + 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::ExecutionModelGeometry, main, "main", interfaces); + builder.AddFunction(spv::OpLabel, builder.AllocateId()); + + const auto ptrInputVec4 = builder.Type(spv::OpTypePointer, spv::StorageClassInput, vec4FloatType); + const auto ptrOutputVec4 = builder.Type(spv::OpTypePointer, spv::StorageClassOutput, vec4FloatType); + const auto zero = constant(floatType, 0u); + const auto one = constant(floatType, std::bit_cast(1.0f)); + std::array, ParameterLocations> values {}; + for (const auto location : sources) { + for (std::uint32_t vertex = 0; vertex < 3u; ++vertex) values[location][vertex] = load(vec4FloatType, result(spv::OpAccessChain, ptrInputVec4, inputs[location], constant(intType, vertex))); + } + for (std::uint32_t vertex = 0; vertex < 3u; ++vertex) { + store(position, load(vec4FloatType, result(spv::OpAccessChain, ptrInputVec4, glIn, constant(intType, vertex), constant(intType, 0u)))); + for (std::size_t i = 0; i < passed.size(); ++i) store(passedOutputs[i], values[passed[i]][vertex]); + const auto weights = result(spv::OpCompositeConstruct, vec3FloatType, vertex == 0u ? one : zero, vertex == 1u ? one : zero, vertex == 2u ? one : zero); + 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)), values[perVertex[i].sourceLocation][source]); + } + builder.AddFunction(spv::OpEmitVertex); + } + builder.AddFunction(spv::OpEndPrimitive); + builder.AddFunction(spv::OpReturn); + builder.AddFunction(spv::OpFunctionEnd); + return builder.Finalize(); + } + +private: + SpirvModule builder; + std::vector interfaces; + std::uint32_t main = 0; + std::uint32_t voidType = 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 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; + } +}; + +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) { + 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, 0u) : index))); + if (smooth != 0u) store(smooth, weights); + if (linear != 0u) store(linear, weights); + for (std::size_t i = 0; i < perVertex.size(); ++i) { + for (std::uint32_t source = 0; source < 3u; ++source) store(result(spv::OpAccessChain, ptrOutputVec4, perVertexOutputs[i], constant(intType, source)), load(vec4FloatType, result(spv::OpAccessChain, ptrInputVec4, inputs[perVertex[i].sourceLocation], constant(intType, source)))); + } + return finish(); + } + +private: + SpirvModule builder; + std::uint32_t clipDistances; + std::vector interfaces; + std::uint32_t main = 0; + std::uint32_t voidType = 0; + std::uint32_t boolType = 0; + std::uint32_t uintType = 0; + std::uint32_t intType = 0; + std::uint32_t floatType = 0; + std::uint32_t vec3FloatType = 0; + std::uint32_t vec4FloatType = 0; + std::uint32_t vertexArrayType = 0; + std::uint32_t functionType = 0; + std::uint32_t perVertexType = 0; + std::uint32_t perVertexArrayType = 0; + std::uint32_t clipArrayType = 0; + std::uint32_t controlInputClipType = 0; + std::uint32_t ptrInputVec4 = 0; + std::uint32_t ptrOutputVec4 = 0; + std::uint32_t ptrControlInputClip = 0; + std::uint32_t ptrControlOutputClip = 0; + std::uint32_t ptrEvaluationInputClip = 0; + std::uint32_t ptrEvaluationOutputClip = 0; + std::uint32_t 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, bool meshPositions) { + BarycentricEmulationLayout layout; + if (!emulation.active) return layout; + std::array used {}; + for (const auto& parameter : parameters) { + require(parameter.location < ParameterLocations, "fragment parameter location " + std::to_string(parameter.location) + " is out of range"); + if (!parameter.perVertex) used[parameter.location] = true; + } + const auto take = [&](std::uint32_t count) { + for (std::uint32_t first = 0; first + count <= ParameterLocations; ++first) { + if (std::any_of(used.begin() + first, used.begin() + first + count, [](bool taken) { return taken; })) continue; + std::fill(used.begin() + first, used.begin() + first + count, true); + return first; + } + throw std::runtime_error("Barycentric geometry SPIR-V: the emulated barycentric inputs exceed " + std::to_string(ParameterLocations) + " locations"); + }; + if (emulation.smooth) layout.smoothLocation = take(1u); + if (emulation.linear) layout.linearLocation = take(1u); + if (meshPositions) layout.positionsLocation = take(3u); + for (const auto& parameter : parameters) { + if (parameter.perVertex) layout.perVertexLocations.emplace_back(parameter.location, take(3u)); + } + return layout; +} + +EmulatedInterface DescribeEmulatedInterface(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 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(emulated.passed, emulated.perVertex, emulated.layout); +#if ANYPS5_ENABLE_SPIRV_TOOLS + result.spirv = ValidateAndOptimizeSpirv(result.spirv, target.vulkanVersion, target.spirvVersion); +#endif + return result; +} + +BarycentricTessellationShaders BuildBarycentricTessellationShaders(const RecompileResult& vertex, const RecompileResult& fragment, const SpirvTarget& target, std::uint32_t maxFragmentInputComponents) { + require(fragment.barycentricEmulation.active, "the fragment shader does not emulate barycentrics"); + require(target.tessellation.has_value(), "tessellation shaders are unavailable"); + require(target.spirvVersion >= 0x00010300u && target.spirvVersion <= 0x00010500u, "unsupported SPIR-V target version"); + require(std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), spv::CapabilityTessellation) != target.supportedCapabilities.end(), "tessellation capability is unavailable"); + const auto emulated = CollectEmulatedInterface(vertex, fragment); + const auto passed = static_cast(emulated.passed.size()); + const auto perVertex = static_cast(emulated.perVertex.size()); + const auto clipDistances = VertexClipDistanceCount(vertex.spirv.Words()); + require(clipDistances == 0u || std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), spv::CapabilityClipDistance) != target.supportedCapabilities.end(), "the vertex clip distances need shaderClipDistance"); + const auto controlComponents = 4u + clipDistances + 4u * emulated.sources; + const auto evaluationOutputComponents = 4u + clipDistances + 4u * passed + 3u * emulated.barycentrics + 12u * perVertex; + const auto fragmentComponents = 4u * (passed + emulated.barycentrics + 3u * perVertex); + const auto& limits = *target.tessellation; + require(limits.maxPatchSize >= 3u && controlComponents <= limits.maxControlPerVertexInputComponents && controlComponents <= limits.maxControlPerVertexOutputComponents && 3u * controlComponents + 4u <= limits.maxControlTotalOutputComponents && limits.maxControlPerPatchOutputComponents >= 4u && controlComponents <= limits.maxEvaluationInputComponents && evaluationOutputComponents <= limits.maxEvaluationOutputComponents && fragmentComponents <= maxFragmentInputComponents, "the emulated barycentric interface exceeds device limits"); + std::set sourceSet(emulated.passed.begin(), emulated.passed.end()); + for (const auto& parameter : emulated.perVertex) sourceSet.insert(parameter.sourceLocation); + const std::vector sources(sourceSet.begin(), sourceSet.end()); + BarycentricTessellationShaders shaders; + shaders.control.spirv = BarycentricTessellationEmitter(target.spirvVersion, clipDistances).EmitControl(sources); + shaders.evaluation.spirv = BarycentricTessellationEmitter(target.spirvVersion, clipDistances).EmitEvaluation(sources, emulated.passed, emulated.passedFlat, emulated.perVertex, emulated.layout); +#if ANYPS5_ENABLE_SPIRV_TOOLS + shaders.control.spirv = ValidateAndOptimizeSpirv(shaders.control.spirv, target.vulkanVersion, target.spirvVersion); + shaders.evaluation.spirv = ValidateAndOptimizeSpirv(shaders.evaluation.spirv, target.vulkanVersion, target.spirvVersion); +#endif + return shaders; +} + +} diff --git a/core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp b/core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp index ed7a17055e..bbf6dcc0f6 100644 --- a/core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp +++ b/core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp @@ -1,5 +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" @@ -30,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); @@ -40,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::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); @@ -55,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)); @@ -115,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) { @@ -122,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); @@ -144,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))); } @@ -263,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); } @@ -270,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); @@ -295,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 {}; @@ -315,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); @@ -323,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()); @@ -342,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; @@ -359,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; @@ -393,17 +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"); - std::uint32_t faultBinding = 0; - for (const auto* shader : {&vertex, &fragment}) { - for (const auto& binding : shader->bindings) { - require(binding.binding < std::numeric_limits::max(), "descriptor binding overflow"); - faultBinding = std::max(faultBinding, binding.binding + 1); - } - } - RectListEmitter control(parameters, spv::ExecutionModelTessellationControl, target.spirvVersion, faultBinding); - RectListEmitter evaluation(parameters, spv::ExecutionModelTessellationEvaluation, target.spirvVersion, faultBinding); + 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, 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(); @@ -411,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 1fb8126384..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 { @@ -223,6 +225,23 @@ std::uint32_t RoundToPrecision(SpirvEmitterState& state, std::uint32_t value, st return ToF64(state, Join(state, {rounded.low, Binary(state, spv::OpBitwiseOr, u32, Binary(state, spv::OpBitwiseAnd, u32, rounded.high, ConstantU32(state, 0x7fffffffu)), sign)})); } +std::uint32_t RoundToPrecisionDirected(SpirvEmitterState& state, std::uint32_t value, std::uint32_t fractionBits, std::uint32_t smallestStep, std::uint32_t rounding) { + const auto bits = Split(state, FromF64(state, value)); + const auto exponent = Unary(state, spv::OpBitcast, TypeI32(state), ExponentField(state, bits.high)); + const auto step = Glsl(state, TypeI32(state), GLSLstd450SMax, Binary(state, spv::OpISub, TypeI32(state), exponent, ConstantI32(state, static_cast(fractionBits))), ConstantI32(state, static_cast(smallestStep))); + const auto unbiased = Binary(state, spv::OpISub, TypeI32(state), step, ConstantI32(state, 1023)); + const auto scaled = Exact(state, spv::OpFMul, value, PowerOfTwo(state, Unary(state, spv::OpSNegate, TypeI32(state), unbiased))); + std::uint32_t integral = 0; + if (rounding == 3u) { + integral = Trunc(state, FromF64(state, scaled)); + } else if (rounding == 2u) { + integral = Floor(state, FromF64(state, scaled)); + } else { + integral = Unary(state, spv::OpFNegate, TypeF64(state), Floor(state, FromF64(state, Unary(state, spv::OpFNegate, TypeF64(state), scaled)))); + } + return Exact(state, spv::OpFMul, integral, PowerOfTwo(state, unbiased)); +} + std::uint32_t F32BitsOfRounded(SpirvEmitterState& state, std::uint32_t value) { const auto u32 = TypeU32(state); const auto bits = Split(state, FromF64(state, value)); @@ -265,12 +284,32 @@ std::uint32_t Interpolate(SpirvEmitterState& state, std::uint32_t delta, std::ui base = halfResult ? FlushDenormal32(state, base, flush32, 0x00800000u) : FlushDenormal32(state, base, flush16, 0x38800000u); const auto product = Exact(state, spv::OpFMul, FromF32Exact(state, delta), FromF32Exact(state, coordinate)); const auto sum = AddRoundToOdd(state, product, FromF32Exact(state, base)); - auto rounded = halfResult ? RoundToPrecision(state, sum, 10u, 999u) : RoundToPrecision(state, sum, 23u, 874u); + const std::uint32_t rounding16 = halfResult ? (mode >> InterpolationRound16Shift) & 3u : 0u; + auto rounded = halfResult ? (rounding16 == 0u ? RoundToPrecision(state, sum, 10u, 999u) : RoundToPrecisionDirected(state, sum, 10u, 999u, rounding16)) : RoundToPrecision(state, sum, 23u, 874u); if (halfResult) { const auto bits = Split(state, FromF64(state, rounded)); const auto sign = Binary(state, spv::OpBitwiseAnd, u32, bits.high, ConstantU32(state, 0x80000000u)); - const auto overflow = Binary(state, spv::OpUGreaterThan, TypeBool(state), Binary(state, spv::OpBitwiseAnd, u32, bits.high, ConstantU32(state, 0x7fffffffu)), ConstantU32(state, 0x40f00000u)); - rounded = ToF64(state, Join(state, SelectBits(state, overflow, {ConstantU32(state, 0u), Binary(state, spv::OpBitwiseOr, u32, sign, ConstantU32(state, 0x40f00000u))}, bits))); + const auto magnitude = Binary(state, spv::OpBitwiseAnd, u32, bits.high, ConstantU32(state, 0x7fffffffu)); + if (rounding16 == 0u) { + const auto overflow = Binary(state, spv::OpUGreaterThan, TypeBool(state), magnitude, ConstantU32(state, 0x40f00000u)); + rounded = ToF64(state, Join(state, SelectBits(state, overflow, {ConstantU32(state, 0u), Binary(state, spv::OpBitwiseOr, u32, sign, ConstantU32(state, 0x40f00000u))}, bits))); + } else { + const auto overflow = Binary(state, spv::OpUGreaterThan, TypeBool(state), magnitude, ConstantU32(state, 0x40effc00u)); + const auto negative = Binary(state, spv::OpINotEqual, TypeBool(state), sign, ConstantU32(state, 0u)); + auto limit = ConstantU32(state, 0x40effc00u); + if (rounding16 == 1u) limit = Select(state, u32, negative, ConstantU32(state, 0x40effc00u), ConstantU32(state, 0x40f00000u)); + if (rounding16 == 2u) limit = Select(state, u32, negative, ConstantU32(state, 0x40f00000u), ConstantU32(state, 0x40effc00u)); + rounded = ToF64(state, Join(state, SelectBits(state, overflow, {ConstantU32(state, 0u), Binary(state, spv::OpBitwiseOr, u32, sign, limit)}, bits))); + if (rounding16 == 2u) { + const auto boolean = TypeBool(state); + const auto zeroBits = [&](F64Bits value, std::uint32_t highMask) { + return Binary(state, spv::OpLogicalAnd, boolean, Binary(state, spv::OpIEqual, boolean, Binary(state, spv::OpBitwiseAnd, u32, value.high, ConstantU32(state, highMask)), ConstantU32(state, 0u)), Binary(state, spv::OpIEqual, boolean, value.low, ConstantU32(state, 0u))); + }; + const auto positiveZeros = Binary(state, spv::OpLogicalAnd, boolean, zeroBits(Split(state, FromF64(state, product)), 0xffffffffu), zeroBits(Split(state, FromF64(state, FromF32Exact(state, base))), 0xffffffffu)); + const auto negativeZero = Binary(state, spv::OpLogicalAnd, boolean, zeroBits(Split(state, FromF64(state, sum)), 0x7fffffffu), Unary(state, spv::OpLogicalNot, boolean, positiveZeros)); + rounded = ToF64(state, Join(state, SelectBits(state, negativeZero, {ConstantU32(state, 0u), ConstantU32(state, 0x80000000u)}, Split(state, FromF64(state, rounded))))); + } + } } const auto finite = F32BitsOfRounded(state, rounded); const auto inputs = std::array{Unary(state, spv::OpBitcast, u32, delta), Unary(state, spv::OpBitcast, u32, coordinate), Unary(state, spv::OpBitcast, u32, base)}; @@ -289,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); @@ -591,7 +766,9 @@ std::uint32_t EmitFPTrigPreop64(SpirvEmitterState& state, std::uint32_t arg0, st return FromF64(state, Exact(state, spv::OpFMul, Exact(state, spv::OpFMul, mantissa, scale), ConstantF64(state, 0x1a70000000000000ull))); } -std::uint32_t EmitFPDot2F32F16(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1, std::uint32_t arg2) { +namespace { + +std::uint32_t Dot2F32F16Exact(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1, std::uint32_t arg2) { const auto u32 = TypeU32(state); const auto op = [&](spv::Op opcode, std::uint32_t lhs, std::uint32_t value) { return Binary(state, opcode, u32, lhs, ConstantU32(state, value)); }; const auto test = [&](spv::Op opcode, std::uint32_t lhs, std::uint32_t value) { return Binary(state, opcode, TypeBool(state), lhs, ConstantU32(state, value)); }; @@ -681,12 +858,138 @@ std::uint32_t EmitFPDot2F32F16(SpirvEmitterState& state, std::uint32_t arg0, std return pick(aLow.nan, quietHalf(aLow), result); } + +std::uint32_t ExactF32(SpirvEmitterState& state, std::uint32_t opcode, std::uint32_t lhs, std::uint32_t rhs) { + const auto result = Binary(state, opcode, TypeF32(state), lhs, rhs); + state.module.AddAnnotation(spv::OpDecorate, result, spv::DecorationNoContraction); + return result; +} + +std::array TwoSumF32(SpirvEmitterState& state, std::uint32_t lhs, std::uint32_t rhs) { + const auto sum = ExactF32(state, spv::OpFAdd, lhs, rhs); + const auto rhsPart = ExactF32(state, spv::OpFSub, sum, lhs); + const auto lhsPart = ExactF32(state, spv::OpFSub, sum, rhsPart); + return {sum, ExactF32(state, spv::OpFAdd, ExactF32(state, spv::OpFSub, lhs, lhsPart), ExactF32(state, spv::OpFSub, rhs, rhsPart))}; +} + +std::uint32_t Dot2F32F16Fast(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1, std::uint32_t arg2, std::uint32_t& fast) { + const auto u32 = TypeU32(state); + const auto f32 = TypeF32(state); + const auto boolean = TypeBool(state); + const auto op = [&](spv::Op opcode, std::uint32_t lhs, std::uint32_t value) { return Binary(state, opcode, u32, lhs, ConstantU32(state, value)); }; + const auto test = [&](spv::Op opcode, std::uint32_t lhs, std::uint32_t value) { return Binary(state, opcode, boolean, lhs, ConstantU32(state, value)); }; + const auto all = [&](std::uint32_t lhs, std::uint32_t rhs) { return Binary(state, spv::OpLogicalAnd, boolean, lhs, rhs); }; + const auto half = [&](std::uint32_t packed, std::uint32_t shift, std::uint32_t& finite) { + const auto bits = op(spv::OpBitwiseAnd, op(spv::OpShiftRightLogical, packed, shift), 0xffffu); + const auto magnitude = op(spv::OpBitwiseAnd, bits, 0x7fffu); + const auto sign = op(spv::OpShiftLeftLogical, op(spv::OpBitwiseAnd, bits, 0x8000u), 16u); + const auto normal = op(spv::OpIAdd, op(spv::OpShiftLeftLogical, magnitude, 13u), 112u << 23u); + const auto subnormal = Unary(state, spv::OpBitcast, u32, ExactF32(state, spv::OpFMul, Unary(state, spv::OpConvertUToF, f32, magnitude), ConstantF32(state, 0x33800000u))); + const auto unsignedBits = Select(state, u32, test(spv::OpULessThan, magnitude, 0x400u), subnormal, normal); + finite = all(finite, test(spv::OpULessThan, magnitude, 0x7c00u)); + return Unary(state, spv::OpBitcast, f32, Binary(state, spv::OpBitwiseOr, u32, unsignedBits, sign)); + }; + const auto magnitudeC = op(spv::OpBitwiseAnd, arg2, 0x7fffffffu); + const auto flushedC = test(spv::OpULessThan, magnitudeC, 0x00800000u); + fast = all(test(spv::OpULessThan, magnitudeC, 0x7f800000u), Binary(state, spv::OpLogicalOr, boolean, flushedC, test(spv::OpUGreaterThanEqual, magnitudeC, 0x0d800000u))); + const auto valueC = Unary(state, spv::OpBitcast, f32, Select(state, u32, flushedC, op(spv::OpBitwiseAnd, arg2, 0x80000000u), arg2)); + const auto productLow = ExactF32(state, spv::OpFMul, half(arg0, 0u, fast), half(arg1, 0u, fast)); + const auto productHigh = ExactF32(state, spv::OpFMul, half(arg0, 16u, fast), half(arg1, 16u, fast)); + const auto upper = TwoSumF32(state, productHigh, valueC); + const auto total = TwoSumF32(state, productLow, upper[0]); + const auto errors = TwoSumF32(state, total[1], upper[1]); + const auto nearestBits = Unary(state, spv::OpBitcast, u32, errors[0]); + const auto errorBits = Unary(state, spv::OpBitcast, u32, errors[1]); + const auto inexact = test(spv::OpINotEqual, op(spv::OpBitwiseAnd, errorBits, 0x7fffffffu), 0u); + const auto even = test(spv::OpIEqual, op(spv::OpBitwiseAnd, nearestBits, 1u), 0u); + const auto sameSign = test(spv::OpIEqual, op(spv::OpBitwiseAnd, Binary(state, spv::OpBitwiseXor, u32, nearestBits, errorBits), 0x80000000u), 0u); + const auto odd = Binary(state, spv::OpIAdd, u32, nearestBits, Select(state, u32, sameSign, ConstantU32(state, 1u), ConstantU32(state, 0xffffffffu))); + const auto roundedToOdd = Unary(state, spv::OpBitcast, f32, Select(state, u32, all(inexact, even), odd, nearestBits)); + const auto result = Unary(state, spv::OpBitcast, u32, ExactF32(state, spv::OpFAdd, total[0], roundedToOdd)); + const auto magnitude = op(spv::OpBitwiseAnd, result, 0x7fffffffu); + fast = all(fast, all(test(spv::OpUGreaterThanEqual, magnitude, 0x0d800000u), test(spv::OpULessThan, magnitude, 0x7f000000u))); + return result; +} + +} + +void DefineDot2F32F16Function(SpirvEmitterState& state) { + bool used = false; + for (const IrBlock* block : state.program.BlockOrder()) { + for (const IrValue* inst : block->Instructions()) used = used || inst->Opcode() == IrOpcode::FPDot2F32F16; + } + if (!used) return; + const auto u32 = TypeU32(state); + state.dot2F32F16Function = state.module.AllocateId(); + state.module.AddName(state.dot2F32F16Function, "dot2_f32_f16_exact"); + state.module.AddFunction(spv::OpFunction, u32, state.dot2F32F16Function, spv::FunctionControlDontInlineMask, state.module.Type(spv::OpTypeFunction, u32, u32, u32, u32)); + std::array parameters{}; + for (auto& parameter : parameters) { + parameter = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionParameter, u32, parameter); + } + EmitLabel(state, state.module.AllocateId()); + state.module.AddFunction(spv::OpReturnValue, Dot2F32F16Exact(state, parameters[0], parameters[1], parameters[2])); + state.module.AddFunction(spv::OpFunctionEnd); +} + +std::uint32_t EmitFPDot2F32F16(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1, std::uint32_t arg2) { + if (state.dot2F32F16Function == 0u) FailEmit("v_dot2_f32_f16 function was not defined before function emission"); + std::uint32_t fast = 0; + const auto fastResult = Dot2F32F16Fast(state, arg0, arg1, arg2, fast); + return EmitValueOrDefaultIfCondition(state, Unary(state, spv::OpLogicalNot, TypeBool(state), fast), TypeU32(state), fastResult, [&] { + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionCall, TypeU32(state), result, state.dot2F32F16Function, arg0, arg1, arg2); + return result; + }); +} + +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 08a6ca99b3..d609c27286 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp @@ -1,5 +1,6 @@ #include "SpirvBackend/SpirvEmitterInstructions.hpp" #include "SpirvBackend/SpirvMemory/SpirvSubgroup.hpp" +#include "IntermediateRepresentation/IrBlock.hpp" #include "IntermediateRepresentation/IrValue.hpp" #include #include @@ -53,19 +54,17 @@ std::uint32_t CompareOrdered64(SpirvEmitterState& state, std::uint32_t lhsValue, } std::uint32_t EmitMulHigh(SpirvEmitterState& state, std::uint32_t lhs, std::uint32_t rhs, bool signedValue) { - const auto operandType = signedValue ? TypeI32(state) : TypeU32(state); - const auto pairType = signedValue ? TypeI32Pair(state) : TypeU32Pair(state); - auto lhsOperand = lhs; - auto rhsOperand = rhs; - if (signedValue) { - lhsOperand = Unary(state, spv::OpBitcast, TypeI32(state), lhs); - rhsOperand = Unary(state, spv::OpBitcast, TypeI32(state), rhs); - } + const auto pairType = TypeU32Pair(state); const auto extended = state.module.AllocateId(); - state.module.AddFunction(signedValue ? spv::OpSMulExtended : spv::OpUMulExtended, pairType, extended, lhsOperand, rhsOperand); + state.module.AddFunction(spv::OpUMulExtended, pairType, extended, lhs, rhs); const auto high = state.module.AllocateId(); - state.module.AddFunction(spv::OpCompositeExtract, operandType, high, extended, 1u); - return signedValue ? Unary(state, spv::OpBitcast, TypeU32(state), high) : high; + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), high, extended, 1u); + if (!signedValue) return high; + const auto aNeg = Binary(state, spv::OpSLessThan, TypeBool(state), lhs, ConstantU32(state, 0u)); + const auto bNeg = Binary(state, spv::OpSLessThan, TypeBool(state), rhs, ConstantU32(state, 0u)); + const auto corrA = Select(state, TypeU32(state), aNeg, rhs, ConstantU32(state, 0u)); + const auto corrB = Select(state, TypeU32(state), bNeg, lhs, ConstantU32(state, 0u)); + return Binary(state, spv::OpISub, TypeU32(state), high, Binary(state, spv::OpIAdd, TypeU32(state), corrA, corrB)); } std::uint32_t EmitShift64(SpirvEmitterState& state, spv::Op opcode, std::uint32_t value, std::uint32_t shift) { @@ -140,31 +139,6 @@ std::uint32_t EmitExt(SpirvEmitterState& state, std::uint32_t type, std::uint32_ return result; } -std::uint32_t EmitF32ToU32(SpirvEmitterState& state, std::uint32_t src, bool signedValue) { - const auto truncated = EmitTruncF32Value(state, src); - const auto convertedRaw = state.module.AllocateId(); - if (signedValue) { - const auto convertedSigned = state.module.AllocateId(); - state.module.AddFunction(spv::OpConvertFToS, TypeI32(state), convertedSigned, truncated); - state.module.AddFunction(spv::OpBitcast, TypeU32(state), convertedRaw, convertedSigned); - } else { - state.module.AddFunction(spv::OpConvertFToU, TypeU32(state), convertedRaw, truncated); - } - const auto nan = EmitClassifyF32(state, src).nan; - if (signedValue) { - const auto below = Binary(state, spv::OpFOrdLessThanEqual, TypeBool(state), src, ConstantF32(state, 0xcf000000u)); - const auto above = Binary(state, spv::OpFOrdGreaterThanEqual, TypeBool(state), src, ConstantF32(state, 0x4f000000u)); - const auto high = Select(state, TypeU32(state), above, ConstantU32(state, 0x7fffffffu), convertedRaw); - const auto low = Select(state, TypeU32(state), below, ConstantU32(state, 0x80000000u), high); - return Select(state, TypeU32(state), nan, ConstantU32(state, 0u), low); - } - const auto below = Binary(state, spv::OpFOrdLessThanEqual, TypeBool(state), src, ConstantF32(state, 0u)); - const auto above = Binary(state, spv::OpFOrdGreaterThanEqual, TypeBool(state), src, ConstantF32(state, 0x4f800000u)); - const auto zero = Binary(state, spv::OpLogicalOr, TypeBool(state), nan, below); - const auto high = Select(state, TypeU32(state), above, ConstantU32(state, 0xffffffffu), convertedRaw); - return Select(state, TypeU32(state), zero, ConstantU32(state, 0u), high); -} - std::uint32_t EmitDppWriteCondition(SpirvValueEmitContext& ctx, const IrValue& inst) { auto& state = ctx.state; const auto flags = inst.Flags(); @@ -212,11 +186,10 @@ std::uint32_t EmitDsMaskedLaneRead(SpirvEmitterState& state, std::uint32_t sourc if (state.laneCount == 2) { lane = Binary(state, spv::OpBitwiseAnd, TypeU32(state), lane, ConstantU32(state, 31u)); } - const auto physicalLane = EmitHostSubgroupLane(state, lane); const auto shuffled = state.module.AllocateId(); - state.module.AddFunction(spv::OpGroupNonUniformShuffle, TypeU32(state), shuffled, ConstantU32(state, spv::ScopeSubgroup), source, physicalLane); + EmitSubgroupShuffleInHalf(state, TypeU32(state), shuffled, source, lane); const auto sourceExec = state.module.AllocateId(); - state.module.AddFunction(spv::OpGroupNonUniformShuffle, TypeBool(state), sourceExec, ConstantU32(state, spv::ScopeSubgroup), 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)); } @@ -234,9 +207,10 @@ struct WaveReduction { IrOpcode opcode; std::uint32_t identity; spv::Op reduce; + bool floating = false; }; -constexpr std::array WaveReductions{{ +constexpr std::array WaveReductions{{ {IrOpcode::UMin32, 0xffffffffu, spv::OpGroupNonUniformUMin}, {IrOpcode::UMax32, 0u, spv::OpGroupNonUniformUMax}, {IrOpcode::SMin32, 0x7fffffffu, spv::OpGroupNonUniformSMin}, @@ -244,6 +218,8 @@ constexpr std::array WaveReductions{{ {IrOpcode::IAdd32, 0u, spv::OpGroupNonUniformIAdd}, {IrOpcode::BitwiseAnd32, 0xffffffffu, spv::OpGroupNonUniformBitwiseAnd}, {IrOpcode::BitwiseOr32, 0u, spv::OpGroupNonUniformBitwiseOr}, + {IrOpcode::FPMin32, 0x7f800000u, spv::OpNop, true}, + {IrOpcode::FPMax32, 0xff800000u, spv::OpNop, true}, }}; struct HalfWaveScan { @@ -251,8 +227,31 @@ struct HalfWaveScan { const IrValue* source = nullptr; }; +const IrValue* StoredWindowValue(const IrValue* load) { + const auto* slot = load->Argument(0)->Resolve(); + const auto* block = load->Parent(); + if (slot == nullptr || !slot->HasImmediate() || block == nullptr) return nullptr; + const auto& instructions = block->Instructions(); + auto at = std::find(instructions.begin(), instructions.end(), load); + if (at == instructions.end()) return nullptr; + while (at != instructions.begin()) { + const auto* previous = *--at; + if (previous->HasImmediate() || previous->Opcode() != IrOpcode::StoreVectorWindow) continue; + const auto* stored = previous->Argument(0)->Resolve(); + if (stored == nullptr || !stored->HasImmediate()) return nullptr; + if (stored->ImmediateU32() == slot->ImmediateU32()) return previous->Argument(1)->Resolve(); + } + return nullptr; +} + const IrValue* Resolved(const IrValue* value) { - return value != nullptr ? value->Resolve() : nullptr; + value = value != nullptr ? value->Resolve() : nullptr; + while (value != nullptr && !value->HasImmediate() && value->Opcode() == IrOpcode::LoadVectorWindow) { + const auto* stored = StoredWindowValue(value); + if (stored == nullptr) break; + value = stored; + } + return value; } bool Is(const IrValue* value, IrOpcode opcode) { @@ -269,6 +268,11 @@ bool EitherOrder(const IrValue* value, TMatch&& match) { return match(Resolved(value->Argument(0)), Resolved(value->Argument(1))) || match(Resolved(value->Argument(1)), Resolved(value->Argument(0))); } +template +bool EitherOrder(const std::array& operands, TMatch&& match) { + return match(operands[0], operands[1]) || match(operands[1], operands[0]); +} + std::optional> LaneBitWords(const IrValue* bit) { const IrValue* masked = nullptr; const IrValue* shifted = nullptr; @@ -291,16 +295,40 @@ 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]); } +bool HelperTest(const IrValue* predicate) { + return Is(predicate, IrOpcode::IEqual32) && EitherOrder(predicate, [](const IrValue* builtin, const IrValue* zero) { return IsU32(zero, 0u) && Is(builtin, IrOpcode::GetBuiltin) && IsU32(builtin->Argument(0), static_cast(StageInputKind::HelperInvocation)) && IsU32(builtin->Argument(1), 0u); }); +} + +bool HelperOrFalseConjunct(const IrValue* bit) { + bit = Resolved(bit); + if (bit == nullptr) return false; + if (bit->HasImmediate()) return bit->Type() == IrType::Bool && !bit->ImmediateBool(); + if (Is(bit, IrOpcode::LogicalAnd)) return HelperOrFalseConjunct(bit->Argument(0)) || HelperOrFalseConjunct(bit->Argument(1)); + return HelperTest(bit); +} + +bool EveryInvocationEnters(IrShaderStage stage) { + return stage == IrShaderStage::Vertex || stage == IrShaderStage::Local || stage == IrShaderStage::TessellationControl || stage == IrShaderStage::TessellationEvaluation; +} + class EntryLaneWalk { public: + explicit EntryLaneWalk(IrShaderStage stage) : everyInvocationEnters(EveryInvocationEnters(stage)) {} + bool ZeroBit(const IrValue* bit) { bit = Resolved(bit); if (bit == nullptr || !Spend()) return false; - if (Is(bit, IrOpcode::LogicalAnd)) return ZeroBit(bit->Argument(0)) || ZeroBit(bit->Argument(1)); + 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); } @@ -314,10 +342,11 @@ private: bool NotHelper(const IrValue* predicate) { predicate = Resolved(predicate); - if (predicate == nullptr || predicate->HasImmediate() || !Spend()) return false; + 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 EitherOrder(predicate, [](const IrValue* builtin, const IrValue* zero) { return IsU32(zero, 0u) && Is(builtin, IrOpcode::GetBuiltin) && IsU32(builtin->Argument(0), static_cast(StageInputKind::HelperInvocation)) && IsU32(builtin->Argument(1), 0u); }); + case IrOpcode::IEqual32: return HelperTest(predicate); default: return ZeroBit(predicate); } } @@ -359,59 +388,202 @@ private: return result; } + bool everyInvocationEnters; std::unordered_set phis; - std::uint32_t budget = 512u; + std::uint32_t budget = 2048u; }; -bool RowShiftFlags(const IrValue* value, std::uint32_t control) { +bool EveryLaneBit(const IrValue* bit, bool constantTrue) { + const auto* resolved = Resolved(bit); + if (constantTrue && resolved != nullptr && resolved->HasImmediate()) return resolved->Type() == IrType::Bool && resolved->ImmediateBool(); + return AllLanesBit(resolved); +} + +bool WritesEveryLane(const IrValue& update) { + const auto flags = update.Flags(); + return flags.boundControl && flags.rowMask == 0xfu && flags.bankMask == 0xfu && EveryLaneBit(update.Argument(2), true); +} + +bool KeepsNoOldValue(const IrValue& permlane) { + const auto flags = permlane.Flags(); + return flags.fetchInactive || flags.boundControl || EveryLaneBit(permlane.Argument(3), true); +} + +bool UpperLaneTest(const IrValue* bit) { + const auto* compare = Resolved(bit); + return Is(compare, IrOpcode::UGreaterThanEqual32) && Is(Resolved(compare->Argument(0)), IrOpcode::LaneId) && IsU32(compare->Argument(1), 32u); +} + +class UpperLaneWalk { +public: + explicit UpperLaneWalk(IrShaderStage stage) : enters(EveryInvocationEnters(stage)), laneIndexed(enters && stage != IrShaderStage::TessellationControl) {} + + bool ZeroValue(const IrValue* value) { + value = Resolved(value); + if (!enters || value == nullptr || !spend()) return false; + if (value->HasImmediate()) return IsU32(value, 0u); + if (zeroValues.contains(value)) return true; + bool zero = false; + switch (value->Opcode()) { + case IrOpcode::SelectU32: zero = ZeroValue(value->Argument(2)) && (zeroBit(value->Argument(0)) || ZeroValue(value->Argument(1))); break; + case IrOpcode::BitwiseOr32: zero = ZeroValue(value->Argument(0)) && ZeroValue(value->Argument(1)); break; + case IrOpcode::DppMoveU32: zero = ZeroValue(value->Argument(0)); break; + case IrOpcode::DppUpdateU32: zero = ZeroValue(value->Argument(0)) && (WritesEveryLane(*value) || ZeroValue(value->Argument(1))); break; + case IrOpcode::Permlane16U32: zero = ZeroValue(value->Argument(0)) && (KeepsNoOldValue(*value) || ZeroValue(value->Argument(4))); break; + case IrOpcode::Phi: zero = incoming(value, [this](const IrValue* source) { return ZeroValue(source); }); break; + default: break; + } + if (zero && phis.empty()) zeroValues.insert(value); + return zero; + } + +private: + bool spend() { + if (budget == 0u) return false; + --budget; + return true; + } + + bool zeroBit(const IrValue* bit) { + bit = Resolved(bit); + if (bit == nullptr || !spend()) return false; + if (bit->HasImmediate()) return bit->Type() == IrType::Bool && !bit->ImmediateBool(); + switch (bit->Opcode()) { + case IrOpcode::LogicalAnd: return zeroBit(bit->Argument(0)) || zeroBit(bit->Argument(1)); + case IrOpcode::SelectU1: return zeroBit(bit->Argument(2)) && (zeroBit(bit->Argument(0)) || zeroBit(bit->Argument(1))); + case IrOpcode::Phi: return incoming(bit, [this](const IrValue* source) { return zeroBit(source); }); + case IrOpcode::INotEqual32: return EitherOrder(bit, [this](const IrValue* value, const IrValue* zero) { return IsU32(zero, 0u) && ZeroValue(value); }); + case IrOpcode::ULessThan32: return belowUpperLanes(bit->Argument(0), bit->Argument(1)); + case IrOpcode::UGreaterThan32: return belowUpperLanes(bit->Argument(1), bit->Argument(0)); + default: return false; + } + } + + bool belowUpperLanes(const IrValue* index, const IrValue* bound) { + const auto* limit = Resolved(bound); + return limit != nullptr && limit->HasImmediate() && limit->Type() == IrType::U32 && limit->ImmediateU32() <= 32u && upperLaneIndex(index); + } + + bool upperLaneIndex(const IrValue* index) { + index = Resolved(index); + if (!laneIndexed || index == nullptr || !spend()) return false; + if (Is(index, IrOpcode::LaneId)) return true; + if (Is(index, IrOpcode::BitCount32)) { + const auto* mask = Resolved(index->Argument(0)); + return Is(mask, IrOpcode::SelectU32) && IsU32(mask->Argument(1), 0xffffffffu) && UpperLaneTest(mask->Argument(0)); + } + return Is(index, IrOpcode::IAdd32) && EitherOrder(index, [this](const IrValue* counted, const IrValue* added) { return Is(added, IrOpcode::BitCount32) && upperLaneIndex(counted); }); + } + + template + bool incoming(const IrValue* phi, TCheck&& check) { + if (!phis.insert(phi).second) return true; + 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 enters; + bool laneIndexed; + std::unordered_set zeroValues; + std::unordered_set phis; + std::uint32_t budget = 1024u; +}; + +bool RowShiftFlags(const IrValue* value, std::uint32_t control, bool zeroFill) { const auto flags = value->Flags(); - return flags.control == control && flags.rowMask == 0xfu && flags.bankMask == 0xfu && !flags.boundControl && !flags.fetchInactive; + return flags.control == control && flags.rowMask == 0xfu && flags.bankMask == 0xfu && (!flags.boundControl || zeroFill) && !flags.fetchInactive; } -const IrValue* RowScanStepInput(const IrValue* step, const WaveReduction& reduction, std::uint32_t control) { - if (!Is(step, IrOpcode::DppUpdateU32) || !RowShiftFlags(step, control) || !AllLanesBit(step->Argument(2))) return nullptr; - const auto* previous = Resolved(step->Argument(1)); - const auto* combined = Resolved(step->Argument(0)); - const bool matched = Is(combined, reduction.opcode) && EitherOrder(combined, [&](const IrValue* moved, const IrValue* own) { - return own == previous && Is(moved, IrOpcode::DppMoveU32) && RowShiftFlags(moved, control) && Resolved(moved->Argument(0)) == previous && AllLanesBit(moved->Argument(1)); +const IrValue* Unflushed(const IrValue* value) { + if (!Is(value, IrOpcode::SelectU32)) return value; + const auto* kept = Resolved(value->Argument(2)); + const auto masked = [kept](const IrValue* candidate, std::uint32_t mask) { + return Is(candidate, IrOpcode::BitwiseAnd32) && EitherOrder(candidate, [&](const IrValue* source, const IrValue* constant) { return source == kept && IsU32(constant, mask); }); + }; + const auto* condition = Resolved(value->Argument(0)); + const bool exponentZero = Is(condition, IrOpcode::IEqual32) && EitherOrder(condition, [&](const IrValue* exponent, const IrValue* zero) { return IsU32(zero, 0u) && masked(exponent, 0x7f800000u); }); + return exponentZero && masked(Resolved(value->Argument(1)), 0x80000000u) ? kept : value; +} + +std::optional> ReductionOperands(const IrValue* combined, const WaveReduction& reduction) { + if (reduction.floating) { + combined = Unflushed(combined); + if (!Is(combined, IrOpcode::BitCastU32F32)) return std::nullopt; + combined = Resolved(combined->Argument(0)); + } + if (!Is(combined, reduction.opcode)) return std::nullopt; + std::array operands{Resolved(combined->Argument(0)), Resolved(combined->Argument(1))}; + if (!reduction.floating) return operands; + for (auto& operand : operands) { + if (!Is(operand, IrOpcode::BitCastF32U32)) return std::nullopt; + operand = Unflushed(Resolved(operand->Argument(0))); + } + return operands; +} + +const IrValue* RowScanStepInput(const IrValue* step, const WaveReduction& reduction, std::uint32_t control, bool narrow) { + const bool zeroFill = narrow && reduction.identity == 0u && !reduction.floating; + if (!Is(step, IrOpcode::DppUpdateU32) || !RowShiftFlags(step, control, zeroFill) || !EveryLaneBit(step->Argument(2), narrow)) return nullptr; + const auto* previous = zeroFill && WritesEveryLane(*step) ? nullptr : Resolved(step->Argument(1)); + const IrValue* input = nullptr; + const auto operands = ReductionOperands(Resolved(step->Argument(0)), reduction); + const bool matched = operands && EitherOrder(*operands, [&](const IrValue* moved, const IrValue* own) { + input = own; + return (previous == nullptr || own == previous) && Is(moved, IrOpcode::DppMoveU32) && RowShiftFlags(moved, control, zeroFill) && Resolved(moved->Argument(0)) == own && EveryLaneBit(moved->Argument(1), narrow); }); - return matched ? previous : nullptr; + return matched ? input : nullptr; } -const IrValue* Unmasked(const IrValue* value) { - while (Is(value, IrOpcode::SelectU32) && AllLanesBit(value->Argument(0))) value = Resolved(value->Argument(1)); +const IrValue* Unmasked(const IrValue* value, bool narrow) { + while (Is(value, IrOpcode::SelectU32) && EveryLaneBit(value->Argument(0), narrow)) value = Resolved(value->Argument(1)); return value; } -bool CrossesRowsOf(const IrValue* value, const IrValue* scan) { - const auto* permlane = Unmasked(value); +bool CrossesRowsOf(const IrValue* value, const IrValue* scan, bool narrow) { + const auto* permlane = Unmasked(value, narrow); if (!Is(permlane, IrOpcode::Permlane16U32)) return false; const auto flags = permlane->Flags(); - return flags.x16 && !flags.fetchInactive && Resolved(permlane->Argument(0)) == scan && IsU32(permlane->Argument(1), 0xffffffffu) && IsU32(permlane->Argument(2), 0xffffffffu) && AllLanesBit(permlane->Argument(3)); + return flags.x16 && !flags.fetchInactive && Resolved(permlane->Argument(0)) == scan && IsU32(permlane->Argument(1), 0xffffffffu) && IsU32(permlane->Argument(2), 0xffffffffu) && EveryLaneBit(permlane->Argument(3), narrow); } -std::optional MatchHalfWaveScan(const IrValue* value) { - const auto* combined = Unmasked(Resolved(value)); - const auto reduction = std::find_if(WaveReductions.begin(), WaveReductions.end(), [&](const WaveReduction& candidate) { return Is(combined, candidate.opcode); }); +std::optional MatchHalfWaveScan(const IrValue* value, IrShaderStage stage, bool narrow) { + const auto* combined = Unmasked(Resolved(value), narrow); + std::optional> operands; + const auto reduction = std::find_if(WaveReductions.begin(), WaveReductions.end(), [&](const WaveReduction& candidate) { return (operands = ReductionOperands(combined, candidate)).has_value(); }); const IrValue* scan = nullptr; - if (reduction == WaveReductions.end() || !EitherOrder(combined, [&](const IrValue* own, const IrValue* crossed) { scan = own; return CrossesRowsOf(crossed, own); })) return std::nullopt; + if (reduction == WaveReductions.end() || !EitherOrder(*operands, [&](const IrValue* own, const IrValue* crossed) { scan = own; return CrossesRowsOf(crossed, own, narrow); })) return std::nullopt; for (const auto control : {0x118u, 0x114u, 0x112u, 0x111u}) { - scan = RowScanStepInput(scan, *reduction, control); + scan = RowScanStepInput(scan, *reduction, control, narrow); if (scan == nullptr) return std::nullopt; } - const auto* source = Unmasked(scan); - if (!Is(source, IrOpcode::SelectU32) || !IsU32(source->Argument(2), reduction->identity) || !EntryLaneWalk{}.ZeroBit(source->Argument(0))) return std::nullopt; + const auto* source = Unmasked(scan, narrow); + const bool entryKeys = Is(source, IrOpcode::SelectU32) && IsU32(source->Argument(2), reduction->identity) && EntryLaneWalk{stage}.ZeroBit(source->Argument(0)); + const bool upperKeys = narrow && reduction->identity == 0u && !reduction->floating && UpperLaneWalk{stage}.ZeroValue(source); + if (!entryKeys && !upperKeys) return std::nullopt; return HalfWaveScan{&*reduction, source}; } 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; - const auto scan = MatchHalfWaveScan(inst.Argument(0)); + 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; + if (narrow) scan = MatchHalfWaveScan(inst.Argument(0), stage, true); if (!scan) return std::nullopt; + if (scan->reduction->floating) { + if (lane < 32u || state.hostSubgroupSize > 32u) return std::nullopt; + return ConstantU32(state, scan->reduction->identity); + } + if (HostSubgroupSingleLane(state)) return lane >= 32u ? ConstantU32(state, scan->reduction->identity) : ctx.Def(scan->source); const auto read = "v_readlane_b32 of lane " + std::to_string(lane) + " of a wave64 half-wave reduction scan"; const auto& capabilities = state.supportedCapabilities; - if (std::find(capabilities.begin(), capabilities.end(), static_cast(spv::CapabilityGroupNonUniformArithmetic)) == capabilities.end()) ctx.Fail(inst, (read + " needs subgroup arithmetic, which the device lacks").c_str()); + if (std::find(capabilities.begin(), capabilities.end(), static_cast(spv::CapabilityGroupNonUniformArithmetic)) == capabilities.end()) { + if (narrow) return lane >= 32u ? std::optional(ConstantU32(state, scan->reduction->identity)) : std::nullopt; + ctx.Fail(inst, (read + " needs subgroup arithmetic, which the device lacks").c_str()); + } if (state.hostSubgroupSize > 64u) ctx.Fail(inst, (read + " on a " + std::to_string(state.hostSubgroupSize) + "-lane host subgroup, which is wider than the wave").c_str()); const auto identity = ConstantU32(state, scan->reduction->identity); if (lane >= 32u && state.hostSubgroupSize <= 32u) return identity; @@ -426,6 +598,69 @@ std::optional EmitHalfWaveReduction(SpirvValueEmitContext& ctx, c return result; } +bool ActiveMaskedWord(const IrValue* word, std::uint32_t depth = 0u) { + word = Resolved(word); + if (word == nullptr || depth > 8u) return false; + if (Is(word, IrOpcode::CompositeExtractU32x4)) { + const auto* ballot = Resolved(word->Argument(0)); + const auto* active = ballot != nullptr && Is(ballot, IrOpcode::Ballot) ? Resolved(ballot->Argument(0)) : nullptr; + return active != nullptr && active->HasImmediate() && active->Type() == IrType::Bool && active->ImmediateBool() && IsU32(word->Argument(1), 0u); + } + return Is(word, IrOpcode::BitwiseAnd32) && (ActiveMaskedWord(word->Argument(0), depth + 1u) || ActiveMaskedWord(word->Argument(1), depth + 1u)); +} + +bool ActiveMaskedBit(const IrValue* bit) { + const auto words = LaneBitWords(Resolved(bit)); + return words && ActiveMaskedWord((*words)[0]) && ActiveMaskedWord((*words)[1]); +} + +std::optional MatchRowScan(const IrValue* value, IrShaderStage stage, bool narrow) { + const auto* top = Unmasked(Resolved(value), narrow); + for (const auto& reduction : WaveReductions) { + if (reduction.floating) continue; + const IrValue* scan = top; + for (const auto control : {0x118u, 0x114u, 0x112u, 0x111u}) { + scan = RowScanStepInput(scan, reduction, control, narrow); + if (scan == nullptr) break; + } + if (scan == nullptr) continue; + const auto* source = Unmasked(scan, narrow); + if (Is(source, IrOpcode::SelectU32) && IsU32(source->Argument(2), reduction.identity) && (ActiveMaskedBit(source->Argument(0)) || EntryLaneWalk{stage}.ZeroBit(source->Argument(0)))) return HalfWaveScan{&reduction, source}; + } + return std::nullopt; +} + +std::optional EmitRowReduction(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t lane) { + auto& state = ctx.state; + const bool split = state.splitSubgroup && state.hostSubgroupSize == 64u && state.program.WaveSize() == 32u; + if ((lane & 15u) != 15u || state.laneCount != 1u || HostSubgroupSingleLane(state) || (!split && (state.splitSubgroup || state.hostSubgroupSize != state.program.WaveSize()))) return std::nullopt; + const auto stage = state.program.Resources().stage; + auto scan = MatchRowScan(inst.Argument(0), stage, false); + if (!scan) scan = MatchRowScan(inst.Argument(0), stage, true); + if (!scan) return std::nullopt; + const auto& capabilities = state.supportedCapabilities; + if (std::find(capabilities.begin(), capabilities.end(), static_cast(spv::CapabilityGroupNonUniformArithmetic)) == capabilities.end()) return std::nullopt; + const auto u32 = TypeU32(state); + const auto boolean = TypeBool(state); + const auto identity = ConstantU32(state, scan->reduction->identity); + const auto row = Binary(state, spv::OpShiftRightLogical, u32, EmitSubgroupLocalInvocationId(state), ConstantU32(state, 4u)); + const auto inRow = Binary(state, spv::OpIEqual, boolean, row, ConstantU32(state, lane >> 4u)); + const auto source = ctx.Def(scan->source); + state.module.EmitCapability(spv::CapabilityGroupNonUniformArithmetic); + const auto reduce = [&](std::uint32_t include) { + const auto keys = Select(state, u32, include, source, identity); + const auto result = state.module.AllocateId(); + state.module.AddFunction(scan->reduction->reduce, u32, result, ConstantU32(state, spv::ScopeSubgroup), static_cast(spv::GroupOperationReduce), keys); + return result; + }; + if (!split) return reduce(inRow); + const auto upper = Binary(state, spv::OpUGreaterThanEqual, boolean, EmitHostSubgroupLane(state, ConstantU32(state, 0u)), ConstantU32(state, 32u)); + const auto lower = Unary(state, spv::OpLogicalNot, boolean, upper); + const auto lowerTotal = reduce(Binary(state, spv::OpLogicalAnd, boolean, inRow, lower)); + const auto upperTotal = reduce(Binary(state, spv::OpLogicalAnd, boolean, inRow, upper)); + return Select(state, u32, upper, upperTotal, lowerTotal); +} + } std::uint32_t EmitConvertU16U32(SpirvEmitterState& state, std::uint32_t arg0) { @@ -437,17 +672,16 @@ 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) { - return EmitF32ToU32(state, arg0, true); + const auto converted = Unary(state, spv::OpConvertFToS, TypeI32(state), arg0); + return Unary(state, spv::OpBitcast, TypeU32(state), converted); } std::uint32_t EmitConvertU32F32(SpirvEmitterState& state, std::uint32_t arg0) { - return EmitF32ToU32(state, arg0, false); + return Unary(state, spv::OpConvertFToU, TypeU32(state), arg0); } std::uint32_t EmitConvertF32S32(SpirvEmitterState& state, std::uint32_t arg0) { @@ -459,9 +693,23 @@ std::uint32_t EmitCompositeExtractU64(SpirvEmitterState& state, std::uint32_t ar return EmitNative(state, arg0, arg1->ImmediateU32()); } +static bool NativeF16Rte(const SpirvEmitterState& state); + +static std::uint32_t EmitF32ToF16BitsTowardZero(SpirvEmitterState& state, std::uint32_t value) { + if (!NativeF16Rte(state)) return EmitF32ToF16RtzBits(state, value); + const auto u32 = TypeU32(state); + const auto f16 = state.module.Type(spv::OpTypeFloat, 16u); + const auto nearest = EmitF32ToF16BitsRte(state, value); + const auto half = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, f16, half, Unary(state, spv::OpBitcast, state.module.Type(spv::OpTypeVector, f16, 2u), nearest), 0u); + const auto widened = Unary(state, spv::OpFConvert, TypeF32(state), half); + const auto away = Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), EmitFAbsValue(state, widened), EmitFAbsValue(state, value)); + return Select(state, u32, away, Binary(state, spv::OpISub, u32, nearest, ConstantU32(state, 1u)), nearest); +} + std::uint32_t EmitPackFloat2x16Rtz(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { - const auto low = EmitF32ToF16RtzBits(state, arg0); - const auto high = Binary(state, spv::OpShiftLeftLogical, TypeU32(state), EmitF32ToF16RtzBits(state, arg1), ConstantU32(state, 16u)); + const auto low = EmitF32ToF16BitsTowardZero(state, arg0); + const auto high = Binary(state, spv::OpShiftLeftLogical, TypeU32(state), EmitF32ToF16BitsTowardZero(state, arg1), ConstantU32(state, 16u)); return Binary(state, spv::OpBitwiseOr, TypeU32(state), low, high); } @@ -643,19 +891,33 @@ std::uint32_t EmitFPMaxTri32(SpirvEmitterState& state, std::uint32_t arg0, std:: } std::uint32_t EmitFPMedTri32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1, std::uint32_t arg2) { - const auto minAb = EmitMinMaxF32Value(state, arg0, arg1, false); - const auto min3 = EmitMinMaxF32Value(state, minAb, arg2, false); - const auto maxAb = EmitMinMaxF32Value(state, arg0, arg1, true); - const auto highMin = EmitMinMaxF32Value(state, maxAb, arg2, false); - const auto median = EmitMinMaxF32Value(state, minAb, highMin, true); - const auto nanAb = Binary(state, spv::OpLogicalOr, TypeBool(state), EmitClassifyF32(state, arg0).nan, EmitClassifyF32(state, arg1).nan); - const auto anyNan = Binary(state, spv::OpLogicalOr, TypeBool(state), nanAb, EmitClassifyF32(state, arg2).nan); - return Select(state, TypeF32(state), anyNan, min3, median); + const auto u32 = TypeU32(state); + const auto boolean = TypeBool(state); + const auto bits = [&](std::uint32_t value) { return Unary(state, spv::OpBitcast, u32, value); }; + const auto value = [&](std::uint32_t valueBits) { return Unary(state, spv::OpBitcast, TypeF32(state), valueBits); }; + const auto isNan = [&](std::uint32_t valueBits) { return EmitCompareU32Constant(state, spv::OpUGreaterThan, EmitAndConstant(state, valueBits, 0x7fffffffu), 0x7f800000u); }; + const auto numeric = [&](std::uint32_t lhs, std::uint32_t rhs, bool maxValue) { + const auto either = Binary(state, spv::OpBitwiseOr, u32, lhs, rhs); + const auto bothZero = EmitCompareU32Constant(state, spv::OpIEqual, EmitAndConstant(state, either, 0x7fffffffu), 0u); + const auto zeroBits = maxValue ? Binary(state, spv::OpBitwiseAnd, u32, lhs, rhs) : either; + const auto ordered = Select(state, u32, Binary(state, maxValue ? spv::OpFOrdGreaterThanEqual : spv::OpFOrdLessThan, boolean, value(lhs), value(rhs)), lhs, rhs); + return Select(state, u32, bothZero, zeroBits, ordered); + }; + const auto a = bits(arg0); + const auto b = bits(arg1); + const auto c = bits(arg2); + const auto nanA = isNan(a); + const auto nanB = isNan(b); + const auto nanC = isNan(c); + const auto minAb = Select(state, u32, nanB, a, numeric(a, b, false)); + const auto min3 = Select(state, u32, nanC, minAb, numeric(minAb, c, false)); + const auto median = numeric(minAb, numeric(numeric(a, b, true), c, false), true); + const auto anyNan = Binary(state, spv::OpLogicalOr, boolean, Binary(state, spv::OpLogicalOr, boolean, nanA, nanB), nanC); + return value(Select(state, u32, anyNan, min3, median)); } std::uint32_t EmitFPRecip32(SpirvEmitterState& state, std::uint32_t arg0) { - const auto source = EmitFlushF32DenormToSignedZero(state, arg0); - return Binary(state, spv::OpFDiv, TypeF32(state), ConstantF32(state, 0x3f800000u), source); + return Binary(state, spv::OpFDiv, TypeF32(state), ConstantF32(state, 0x3f800000u), arg0); } std::uint32_t EmitFPRecipIFlag32(SpirvEmitterState& state, std::uint32_t arg0) { @@ -663,31 +925,96 @@ std::uint32_t EmitFPRecipIFlag32(SpirvEmitterState& state, std::uint32_t arg0) { } std::uint32_t EmitFPRecipSqrt32(SpirvEmitterState& state, std::uint32_t arg0) { - return EmitExt(state, TypeF32(state), GLSLstd450InverseSqrt, {EmitFlushF32DenormToSignedZero(state, arg0)}); + return EmitExt(state, TypeF32(state), GLSLstd450InverseSqrt, {arg0}); } std::uint32_t EmitFPSqrt(SpirvEmitterState& state, std::uint32_t arg0) { - return EmitExt(state, TypeF32(state), GLSLstd450Sqrt, {EmitFlushF32DenormToSignedZero(state, arg0)}); + return EmitExt(state, TypeF32(state), GLSLstd450Sqrt, {arg0}); } std::uint32_t EmitFPExp2(SpirvEmitterState& state, std::uint32_t arg0) { - return EmitExt(state, TypeF32(state), GLSLstd450Exp2, {EmitFlushF32DenormToSignedZero(state, arg0)}); + return EmitExt(state, TypeF32(state), GLSLstd450Exp2, {arg0}); +} + +std::uint32_t Log2SeriesNearOne(SpirvEmitterState& state, std::uint32_t difference) { + auto polynomial = ConstantF32(state, 0xbe000000u); + for (const std::uint32_t coefficient : {0x3e124925u, 0xbe2aaaabu, 0x3e4ccccdu, 0xbe800000u, 0x3eaaaaabu, 0xbf000000u, 0x3f800000u}) { + polynomial = Exact(state, EmitExt(state, TypeF32(state), GLSLstd450Fma, {polynomial, difference, ConstantF32(state, coefficient)})); + } + const auto product = Exact(state, Binary(state, spv::OpFMul, TypeF32(state), difference, polynomial)); + return Exact(state, Binary(state, spv::OpFMul, TypeF32(state), product, ConstantF32(state, 0x3fb8aa3bu))); } std::uint32_t EmitFPLog2(SpirvEmitterState& state, std::uint32_t arg0) { - return EmitExt(state, TypeF32(state), GLSLstd450Log2, {EmitFlushF32DenormToSignedZero(state, arg0)}); + const auto value = EmitExt(state, TypeF32(state), GLSLstd450Log2, {arg0}); + const auto difference = Exact(state, Binary(state, spv::OpFSub, TypeF32(state), arg0, ConstantF32(state, 0x3f800000u))); + const auto series = Log2SeriesNearOne(state, difference); + const auto valueBits = Unary(state, spv::OpBitcast, TypeU32(state), value); + const auto seriesBits = Unary(state, spv::OpBitcast, TypeU32(state), series); + const auto distance = EmitExt(state, TypeU32(state), GLSLstd450UMin, {Binary(state, spv::OpISub, TypeU32(state), valueBits, seriesBits), Binary(state, spv::OpISub, TypeU32(state), seriesBits, valueBits)}); + const auto near = Binary(state, spv::OpFOrdLessThanEqual, TypeBool(state), EmitExt(state, TypeF32(state), GLSLstd450FAbs, {difference}), ConstantF32(state, 0x3e000000u)); + const auto far = Binary(state, spv::OpUGreaterThan, TypeBool(state), distance, ConstantU32(state, 4u)); + return Select(state, TypeF32(state), Binary(state, spv::OpLogicalAnd, TypeBool(state), near, far), series, value); +} + +std::uint32_t SinTurnSeries(SpirvEmitterState& state, std::uint32_t turns) { + const auto square = Exact(state, Binary(state, spv::OpFMul, TypeF32(state), turns, turns)); + auto polynomial = ConstantF32(state, 0xc17183a8u); + for (const std::uint32_t coefficient : {0x42283c1au, 0xc2996966u, 0x42a335e3u, 0xc2255de7u, 0x40c90fdbu}) { + polynomial = Exact(state, EmitExt(state, TypeF32(state), GLSLstd450Fma, {polynomial, square, ConstantF32(state, coefficient)})); + } + return Exact(state, Binary(state, spv::OpFMul, TypeF32(state), turns, polynomial)); +} + +std::uint32_t CosTurnSeries(SpirvEmitterState& state, std::uint32_t turns) { + const auto square = Exact(state, Binary(state, spv::OpFMul, TypeF32(state), turns, turns)); + auto polynomial = ConstantF32(state, 0x40fce9c5u); + for (const std::uint32_t coefficient : {0xc1d368f9u, 0x4270fa83u, 0xc2aae9e4u, 0x4281e0f8u, 0xc19de9e6u, 0x3f800000u}) { + polynomial = Exact(state, EmitExt(state, TypeF32(state), GLSLstd450Fma, {polynomial, square, ConstantF32(state, coefficient)})); + } + return polynomial; +} + +std::uint32_t SeriesWhenFar(SpirvEmitterState& state, std::uint32_t value, std::uint32_t series) { + const auto valueBits = Unary(state, spv::OpBitcast, TypeU32(state), value); + const auto seriesBits = Unary(state, spv::OpBitcast, TypeU32(state), series); + const auto distance = EmitExt(state, TypeU32(state), GLSLstd450UMin, {Binary(state, spv::OpISub, TypeU32(state), valueBits, seriesBits), Binary(state, spv::OpISub, TypeU32(state), seriesBits, valueBits)}); + return Select(state, TypeF32(state), Binary(state, spv::OpUGreaterThan, TypeBool(state), distance, ConstantU32(state, 4u)), series, value); +} + +std::uint32_t TrigLinearNearZero(SpirvEmitterState& state, std::uint32_t value, std::uint32_t distance, std::uint32_t linearConstant) { + const auto linear = Binary(state, spv::OpFMul, TypeF32(state), distance, linearConstant); + const auto magnitude = Binary(state, spv::OpBitwiseAnd, TypeU32(state), Unary(state, spv::OpBitcast, TypeU32(state), distance), ConstantU32(state, 0x7fffffffu)); + const auto near = Binary(state, spv::OpULessThan, TypeBool(state), magnitude, ConstantU32(state, 0x39800000u)); + return Select(state, TypeF32(state), near, linear, value); } std::uint32_t EmitFPSin(SpirvEmitterState& state, std::uint32_t arg0) { const auto cycle = EmitTrigCycleF32(state, arg0, true); - const auto source = Binary(state, spv::OpFMul, TypeF32(state), cycle, ConstantF32(state, 0x40c90fdbu)); - return EmitExt(state, TypeF32(state), GLSLstd450Sin, {source}); + const auto absolute = EmitExt(state, TypeF32(state), GLSLstd450FAbs, {cycle}); + const auto signBits = Binary(state, spv::OpBitwiseAnd, TypeU32(state), Unary(state, spv::OpBitcast, TypeU32(state), cycle), ConstantU32(state, 0x80000000u)); + const auto nearHalf = Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), absolute, ConstantF32(state, 0x3e800000u)); + const auto distance = Select(state, TypeF32(state), nearHalf, Exact(state, Binary(state, spv::OpFSub, TypeF32(state), ConstantF32(state, 0x3f000000u), absolute)), absolute); + const auto pastEighth = Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), distance, ConstantF32(state, 0x3e000000u)); + const auto series = Select(state, TypeF32(state), pastEighth, CosTurnSeries(state, Exact(state, Binary(state, spv::OpFSub, TypeF32(state), ConstantF32(state, 0x3e800000u), distance))), SinTurnSeries(state, distance)); + const auto value = SeriesWhenFar(state, EmitExt(state, TypeF32(state), GLSLstd450Sin, {Binary(state, spv::OpFMul, TypeF32(state), absolute, ConstantF32(state, 0x40c90fdbu))}), series); + const auto linearConstant = Select(state, TypeF32(state), nearHalf, ConstantF32(state, 0x40c90fdbu), ConstantF32(state, 0x40c90fd5u)); + const auto result = TrigLinearNearZero(state, value, distance, linearConstant); + return Unary(state, spv::OpBitcast, TypeF32(state), Binary(state, spv::OpBitwiseOr, TypeU32(state), Unary(state, spv::OpBitcast, TypeU32(state), result), signBits)); } std::uint32_t EmitFPCos(SpirvEmitterState& state, std::uint32_t arg0) { const auto cycle = EmitTrigCycleF32(state, arg0, false); - const auto source = Binary(state, spv::OpFMul, TypeF32(state), cycle, ConstantF32(state, 0x40c90fdbu)); - return EmitExt(state, TypeF32(state), GLSLstd450Cos, {source}); + const auto absolute = EmitExt(state, TypeF32(state), GLSLstd450FAbs, {cycle}); + const auto distance = Exact(state, Binary(state, spv::OpFSub, TypeF32(state), ConstantF32(state, 0x3e800000u), absolute)); + const auto low = CosTurnSeries(state, absolute); + const auto middle = SinTurnSeries(state, distance); + const auto high = Unary(state, spv::OpFNegate, TypeF32(state), CosTurnSeries(state, Exact(state, Binary(state, spv::OpFSub, TypeF32(state), ConstantF32(state, 0x3f000000u), absolute)))); + const auto pastEighth = Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), absolute, ConstantF32(state, 0x3e000000u)); + const auto pastThreeEighths = Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), absolute, ConstantF32(state, 0x3ec00000u)); + const auto series = Select(state, TypeF32(state), pastThreeEighths, high, Select(state, TypeF32(state), pastEighth, middle, low)); + const auto value = SeriesWhenFar(state, EmitExt(state, TypeF32(state), GLSLstd450Cos, {Binary(state, spv::OpFMul, TypeF32(state), absolute, ConstantF32(state, 0x40c90fdbu))}), series); + return TrigLinearNearZero(state, value, distance, ConstantF32(state, 0x40c90fdbu)); } std::uint32_t EmitIdentity(SpirvValueEmitContext&, std::uint32_t value) { @@ -704,7 +1031,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)); } @@ -737,7 +1064,14 @@ std::uint32_t EmitWqmU64(SpirvEmitterState& state, std::uint32_t value) { } std::uint32_t EmitLaneId(SpirvEmitterState& state) { - return state.program.Resources().stage == IrShaderStage::TessellationControl ? EmitInputComponentU32(state, StageInputKind::InvocationId, 0) : EmitSubgroupLocalInvocationId(state); + if (state.program.Resources().stage != IrShaderStage::TessellationControl) return EmitSubgroupLocalInvocationId(state); + const auto variable = InputVariableForKind(state, StageInputKind::InvocationId); + if (variable == 0) FailEmit("stage input InvocationId was not declared"); + 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 bits; } std::uint32_t EmitBallot(SpirvValueEmitContext& ctx, const IrValue* predicate) { @@ -761,7 +1095,11 @@ 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 (HostSubgroupNarrowerThanWave(state) && index >= state.hostSubgroupSize) FailOutsideHostSubgroup(ctx, inst, "v_readlane_b32 of lane " + std::to_string(index)); + 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)); + } } const auto lane = Binary(state, spv::OpBitwiseAnd, TypeU32(state), ctx.Arg(inst, 1), ConstantU32(state, state.program.WaveSize() - 1u)); return ctx.Shuffle(inst, 0, lane); @@ -804,7 +1142,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; } @@ -832,9 +1170,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); @@ -1027,13 +1365,16 @@ std::uint32_t EmitFPUnordEqual32(SpirvEmitterState& state, std::uint32_t arg0, s return EmitNative(state, arg0, arg1); } -std::uint32_t EmitFPOrdNotEqual32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { +static std::uint32_t BothOrderedBits32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { const auto ordered = [&](std::uint32_t value) { const auto magnitude = EmitNative(state, EmitNative(state, value), ConstantU32(state, 0x7fffffffu)); return EmitNative(state, magnitude, ConstantU32(state, 0x7f800000u)); }; - const auto bothOrdered = EmitNative(state, ordered(arg0), ordered(arg1)); - return EmitNative(state, EmitNative(state, arg0, arg1), bothOrdered); + return EmitNative(state, ordered(arg0), ordered(arg1)); +} + +std::uint32_t EmitFPOrdNotEqual32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { + return EmitNative(state, EmitNative(state, arg0, arg1), BothOrderedBits32(state, arg0, arg1)); } std::uint32_t EmitFPUnordNotEqual32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { @@ -1041,19 +1382,19 @@ std::uint32_t EmitFPUnordNotEqual32(SpirvEmitterState& state, std::uint32_t arg0 } std::uint32_t EmitFPOrdLessThan32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { - return EmitNative(state, arg0, arg1); + return EmitNative(state, EmitNative(state, arg0, arg1), BothOrderedBits32(state, arg0, arg1)); } std::uint32_t EmitFPUnordLessThan32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { - return EmitNative(state, arg0, arg1); + return EmitNative(state, EmitNative(state, arg0, arg1)); } std::uint32_t EmitFPOrdGreaterThan32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { - return EmitNative(state, arg0, arg1); + return EmitNative(state, EmitNative(state, arg0, arg1), BothOrderedBits32(state, arg0, arg1)); } std::uint32_t EmitFPUnordGreaterThan32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { - return EmitNative(state, arg0, arg1); + return EmitNative(state, EmitNative(state, arg0, arg1)); } std::uint32_t EmitFPOrdLessThanEqual32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { @@ -1061,7 +1402,7 @@ std::uint32_t EmitFPOrdLessThanEqual32(SpirvEmitterState& state, std::uint32_t a } std::uint32_t EmitFPUnordLessThanEqual32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { - return EmitNative(state, arg0, arg1); + return EmitNative(state, EmitNative(state, arg0, arg1)); } std::uint32_t EmitFPOrdGreaterThanEqual32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { @@ -1069,7 +1410,7 @@ std::uint32_t EmitFPOrdGreaterThanEqual32(SpirvEmitterState& state, std::uint32_ } std::uint32_t EmitFPUnordGreaterThanEqual32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { - return EmitNative(state, arg0, arg1); + return EmitNative(state, EmitNative(state, arg0, arg1)); } std::uint32_t EmitFPAdd32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { @@ -1124,10 +1465,6 @@ std::uint32_t EmitLogicalNotBool(SpirvEmitterState& state, std::uint32_t arg0) { return EmitNative(state, arg0); } -std::uint32_t EmitTruncF32Value(SpirvEmitterState& state, std::uint32_t arg0) { - return EmitGlsl(state, arg0); -} - std::uint32_t EmitFNegateValue(SpirvEmitterState& state, std::uint32_t arg0) { return EmitNative(state, arg0); } @@ -1143,16 +1480,20 @@ static bool NativeF16Rte(const SpirvEmitterState& state) { return supported(spv::CapabilityFloat16) && supported(spv::CapabilityRoundingModeRTE) && supported(spv::CapabilityDenormPreserve); } -static std::uint32_t EmitNativeF32ToF16BitsRte(SpirvEmitterState& state, std::uint32_t value) { +static void EmitNativeF16Modes(SpirvEmitterState& state) { state.module.EmitCapability(spv::CapabilityFloat16); state.module.EmitCapability(spv::CapabilityRoundingModeRTE); state.module.EmitCapability(spv::CapabilityDenormPreserve); if (!state.nativeF16ModesEmitted) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeRoundingModeRTE, 16u); - state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDenormPreserve, 16u); + EmitF16DenormPreserve(state); state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeSignedZeroInfNanPreserve, 16u); state.nativeF16ModesEmitted = true; } +} + +static std::uint32_t EmitNativeF32ToF16BitsRte(SpirvEmitterState& state, std::uint32_t value) { + EmitNativeF16Modes(state); const auto u32 = TypeU32(state); const auto f16 = state.module.Type(spv::OpTypeFloat, 16u); const auto half = Unary(state, spv::OpFConvert, f16, value); @@ -1163,8 +1504,27 @@ static std::uint32_t EmitNativeF32ToF16BitsRte(SpirvEmitterState& state, std::ui const auto magnitude = Binary(state, spv::OpBitwiseAnd, u32, source, ConstantU32(state, 0x7fffffffu)); const auto sign = Binary(state, spv::OpBitwiseAnd, u32, Binary(state, spv::OpShiftRightLogical, u32, source, ConstantU32(state, 16u)), ConstantU32(state, 0x8000u)); const auto payload = Binary(state, spv::OpShiftRightLogical, u32, Binary(state, spv::OpBitwiseAnd, u32, magnitude, ConstantU32(state, 0x7fffffu)), ConstantU32(state, 13u)); - const auto nan = Binary(state, spv::OpBitwiseOr, u32, sign, Binary(state, spv::OpBitwiseOr, u32, ConstantU32(state, 0x7e00u), payload)); - return Select(state, u32, Binary(state, spv::OpUGreaterThan, TypeBool(state), magnitude, ConstantU32(state, 0x7f800000u)), nan, bits); + const auto nan = Binary(state, spv::OpBitwiseOr, u32, ConstantU32(state, 0x7e00u), payload); + const auto result = Select(state, u32, Binary(state, spv::OpUGreaterThan, TypeBool(state), magnitude, ConstantU32(state, 0x7f800000u)), nan, Binary(state, spv::OpBitwiseAnd, u32, bits, ConstantU32(state, 0x7fffu))); + return Binary(state, spv::OpBitwiseOr, u32, sign, result); +} + +static std::uint32_t TypeF16x2(SpirvEmitterState& state) { + if (!NativeF16Rte(state)) FailEmit("packed f16 math needs Float16, RoundingModeRTE and DenormPreserve"); + EmitNativeF16Modes(state); + return state.module.Type(spv::OpTypeVector, state.module.Type(spv::OpTypeFloat, 16u), 2u); +} + +std::uint32_t EmitFPAdd16x2(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { + const auto f16x2 = TypeF16x2(state); + const auto sum = Binary(state, spv::OpFAdd, f16x2, Unary(state, spv::OpBitcast, f16x2, arg0), Unary(state, spv::OpBitcast, f16x2, arg1)); + return Unary(state, spv::OpBitcast, TypeU32(state), Exact(state, sum)); +} + +std::uint32_t EmitFPMul16x2(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { + const auto f16x2 = TypeF16x2(state); + const auto product = Binary(state, spv::OpFMul, f16x2, Unary(state, spv::OpBitcast, f16x2, arg0), Unary(state, spv::OpBitcast, f16x2, arg1)); + return Unary(state, spv::OpBitcast, TypeU32(state), Exact(state, product)); } std::uint32_t EmitF32ToF16BitsRte(SpirvEmitterState& state, std::uint32_t value) { @@ -1233,7 +1593,13 @@ std::uint32_t EmitFPTrunc32(SpirvEmitterState& state, std::uint32_t arg0) { } std::uint32_t EmitFPFract32(SpirvEmitterState& state, std::uint32_t arg0) { - return EmitGlsl(state, arg0); + const auto magnitude = EmitGlsl(state, arg0); + const auto fraction = EmitGlsl(state, magnitude); + const auto complement = EmitFPSub32(state, ConstantF32(state, 0x3f800000u), fraction); + const auto negative = Binary(state, spv::OpFOrdLessThan, TypeBool(state), arg0, ConstantF32(state, 0u)); + const auto partial = Binary(state, spv::OpFOrdNotEqual, TypeBool(state), fraction, ConstantF32(state, 0u)); + const auto below = Binary(state, spv::OpLogicalAnd, TypeBool(state), negative, partial); + return Select(state, TypeF32(state), below, complement, fraction); } std::uint32_t EmitBitCastF16U16(SpirvEmitterState& state, std::uint32_t arg0) { @@ -1252,8 +1618,44 @@ std::uint32_t EmitBitCastU32F32(SpirvEmitterState& state, std::uint32_t arg0) { return EmitBitCastU16F16(state, arg0); } -std::uint32_t EmitConvertF32F16(SpirvEmitterState& state, std::uint32_t bits) { - return EmitF16BitsToF32(state, bits); +static bool NarrowF16Field(const IrValue& half) { + const IrValue* value = half.Resolve(); + while (value->Opcode() == IrOpcode::BitCastF16U16 || value->Opcode() == IrOpcode::ConvertU16U32) value = value->Argument(0)->Resolve(); + if (value->Opcode() != IrOpcode::BitFieldUExtract && value->Opcode() != IrOpcode::BitFieldSExtract) return false; + const IrValue* count = value->Argument(2)->Resolve(); + return !count->HasImmediate() || count->ImmediateU32() < 16u; +} + +static bool OnlyCompared(const IrValue& value) { + return std::ranges::all_of(value.Uses(), [](const IrValue* user) { + switch (user->Opcode()) { + case IrOpcode::FPOrdEqual32: + case IrOpcode::FPUnordEqual32: + case IrOpcode::FPOrdNotEqual32: + case IrOpcode::FPUnordNotEqual32: + case IrOpcode::FPOrdLessThan32: + case IrOpcode::FPUnordLessThan32: + case IrOpcode::FPOrdGreaterThan32: + case IrOpcode::FPUnordGreaterThan32: + case IrOpcode::FPOrdLessThanEqual32: + case IrOpcode::FPUnordLessThanEqual32: + case IrOpcode::FPOrdGreaterThanEqual32: + case IrOpcode::FPUnordGreaterThanEqual32: + case IrOpcode::FPIsNan32: + return true; + case IrOpcode::FPAbs32: + case IrOpcode::FPNeg32: + case IrOpcode::Identity: + return OnlyCompared(*user); + default: + return false; + } + }); +} + +std::uint32_t EmitConvertF32F16(SpirvEmitterState& state, const IrValue& inst, std::uint32_t bits) { + if (NarrowF16Field(*inst.Argument(0))) return EmitF16BitsToF32Decoded(state, bits); + return OnlyCompared(inst) ? EmitF16BitsToF32AnyNan(state, bits) : EmitF16BitsToF32(state, bits); } std::uint32_t EmitCompositeExtractU32x2(SpirvEmitterState& state, std::uint32_t arg0, const IrValue* arg1) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterRegisters.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterRegisters.cpp index d46db44610..f38c0fe047 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterRegisters.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterRegisters.cpp @@ -1,6 +1,10 @@ #include "SpirvBackend/SpirvAluEmitter.hpp" +#include "SpirvBackend/SpirvEmitterHelpers.hpp" #include "SpirvBackend/SpirvEmitterInstructions.hpp" +#include +#include #include +#include namespace ShaderRecompiler { @@ -44,6 +48,91 @@ void EmitSetVectorRegister(SpirvValueEmitContext& ctx, const IrValue& inst) { EmitUnreachable(ctx, inst); } +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), 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) { + 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) { + 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) { EmitUnreachable(ctx, inst); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp index 91b74f5ece..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: @@ -48,6 +73,9 @@ bool LaneSource(const IrProgram& program, const IrValue& value) { case IrOpcode::WriteLane: 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(); @@ -223,11 +251,17 @@ std::uint32_t FunctionLdsDwords(const IrProgram& program) { std::uint64_t needed = 0; for (const IrBlock* block : program.BlockOrder()) { for (const IrValue* inst : block->Instructions()) { - if (SharedAccessOf(inst->Opcode()) == SharedAccess::None) continue; + const auto access = SharedAccessOf(inst->Opcode()); + if (access == SharedAccess::None) continue; const auto index = inst->Flags().index; if (index >= program.Resources().memoryInfo.size()) return FunctionLdsDwordLimit; const auto& memory = program.Resources().memoryInfo[index]; if (memory.kind != ResourceKind::Lds) continue; + if (access == SharedAccess::Append || access == SharedAccess::Consume) { + needed = std::max(needed, (std::uint64_t{memory.offset} >> 2u) + 1u); + if (needed >= FunctionLdsDwordLimit) return FunctionLdsDwordLimit; + continue; + } const auto dwords = SharedAccessDwords(inst->Opcode()); if (dwords == 0u || inst->ArgumentCount() == 0) return FunctionLdsDwordLimit; const auto address = bound.Of(inst->Argument(0)); @@ -311,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) { @@ -325,6 +361,13 @@ SpirvRequirements AnalyzeProgramRequirements(const IrProgram& program) { } requirements.functionScratch = true; } + if (program.Resources().memoryInfo.at(memoryIndex).kind == ResourceKind::Flat && program.Resources().stage == IrShaderStage::Compute && program.Info().scratchDwords != 0u) { + requirements.functionScratch = true; + } + if (program.Resources().memoryInfo.at(memoryIndex).kind == ResourceKind::Flat && program.Resources().stage == IrShaderStage::Compute && addressAccess == AddressAccess::Atomic && inst->Type() == IrType::U64) { + requirements.ldsLock = true; + requirements.subgroupBallot = true; + } } if (BufferAccessOf(inst->Opcode()) != BufferAccess::None) { const auto memoryIndex = inst->Flags().index; @@ -333,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; } @@ -362,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; @@ -437,6 +483,7 @@ SpirvRequirements AnalyzeProgramRequirements(const IrProgram& program) { requirements.functionLdsDwords = static_cast(std::max((needed + 63u) & ~63ull, 64u)); } } + requirements.coherentBufferAlias = requirements.coherentBuffers && !program.Metadata().invalidatesCaches; return requirements; } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp index ca8c1a2678..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))); }); @@ -75,6 +80,11 @@ void RecordBdaFault(SpirvEmitterState& state, std::uint32_t address, std::uint32 state.module.AddFunction(spv::OpFunctionCall, TypeVoid(state), state.module.AllocateId(), state.bdaFaultFunction, ConstantU32(state, static_cast(reason)), address, bytes, instruction); } +void RecordBdaFaultReason(SpirvEmitterState& state, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction, std::uint32_t reason) { + if (state.bdaFaultFunction == 0 || !state.program.Info().usesDma) throw std::runtime_error("BDA fault function is missing"); + state.module.AddFunction(spv::OpFunctionCall, TypeVoid(state), state.module.AllocateId(), state.bdaFaultFunction, reason, address, bytes, instruction); +} + void RecordBdaFaultWords(SpirvEmitterState& state, std::uint32_t addressLow, std::uint32_t addressHigh, std::uint32_t bytes, std::uint32_t instruction, BdaAbi::FaultReason reason) { if (state.program.Info().usesDma) { const auto u64 = TypeScalarU64(state); @@ -110,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 7410a42385..d1889a85fe 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp @@ -2,36 +2,28 @@ #include "SpirvBackend/SpirvMemory/SpirvTypes.hpp" #include "SpirvBackend/SpirvMemory/SpirvConstants.hpp" #include +#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); const auto constant = [&](std::uint32_t value) { return ConstantU32(state, value); }; const auto binary = [&](std::uint32_t op, std::uint32_t type, std::uint32_t left, std::uint32_t right) { return Binary(state, op, type, left, right); }; - const auto load = [&](std::uint32_t pointer) { - const auto value = state.module.AllocateId(); - state.module.AddFunction(spv::OpLoad, u32, value, pointer); - return value; - }; const auto function = state.module.AllocateId(); state.module.AddName(function, name); - state.module.AddFunction(spv::OpFunction, u64, function, spv::FunctionControlDontInlineMask, state.module.Type(spv::OpTypeFunction, u64, u64, u32, u32)); + 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(); @@ -39,41 +31,67 @@ std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool r state.module.AddFunction(spv::OpFunctionParameter, u32, bytes); state.module.AddFunction(spv::OpFunctionParameter, u32, instruction); EmitLabel(state, state.module.AllocateId()); - const auto low = state.module.AllocateId(); - const auto high = state.module.AllocateId(); - const auto pointer = TypePointer(state, spv::StorageClassFunction, u32); - state.module.AddFunction(spv::OpVariable, pointer, low, spv::StorageClassFunction); - state.module.AddFunction(spv::OpVariable, pointer, high, spv::StorageClassFunction); - const auto fail = [&](std::uint32_t condition, BdaAbi::FaultReason reason) { - if (recordFaults) { - ReturnBdaFailureIf(state, condition, address, bytes, instruction, reason); - } 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); } + 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); fail(binary(spv::OpULessThan, boolean, length, constant(4)), BdaAbi::FaultReason::InvalidTable); - fail(binary(spv::OpINotEqual, boolean, BdaLoadWord(state, constant(0)), constant(BdaAbi::Version)), BdaAbi::FaultReason::InvalidTable); - fail(binary(spv::OpINotEqual, boolean, BdaLoadWord(state, constant(2)), constant(sizeof(BdaAbi::Range))), BdaAbi::FaultReason::InvalidTable); - fail(binary(spv::OpINotEqual, boolean, BdaLoadWord(state, constant(3)), constant(0)), BdaAbi::FaultReason::InvalidTable); const auto count = BdaLoadWord(state, constant(1)); const auto available = binary(spv::OpISub, u32, length, constant(4)); - fail(binary(spv::OpINotEqual, boolean, binary(spv::OpBitwiseAnd, u32, available, constant(7)), constant(0)), BdaAbi::FaultReason::InvalidTable); - fail(binary(spv::OpINotEqual, boolean, count, binary(spv::OpShiftRightLogical, u32, available, constant(3))), BdaAbi::FaultReason::InvalidTable); - fail(binary(spv::OpIEqual, boolean, bytes, constant(0)), BdaAbi::FaultReason::Overflow); const auto end = binary(spv::OpIAdd, u64, address, Unary(state, spv::OpUConvert, u64, bytes)); - fail(binary(spv::OpULessThanEqual, boolean, end, address), BdaAbi::FaultReason::Overflow); - state.module.AddFunction(spv::OpStore, low, constant(0)); - state.module.AddFunction(spv::OpStore, high, count); + failFirst({ + {binary(spv::OpINotEqual, boolean, BdaLoadWord(state, constant(0)), constant(BdaAbi::Version)), BdaAbi::FaultReason::InvalidTable}, + {binary(spv::OpINotEqual, boolean, BdaLoadWord(state, constant(2)), constant(sizeof(BdaAbi::Range))), BdaAbi::FaultReason::InvalidTable}, + {binary(spv::OpINotEqual, boolean, BdaLoadWord(state, constant(3)), constant(0)), BdaAbi::FaultReason::InvalidTable}, + {binary(spv::OpINotEqual, boolean, binary(spv::OpBitwiseAnd, u32, available, constant(7)), constant(0)), BdaAbi::FaultReason::InvalidTable}, + {binary(spv::OpINotEqual, boolean, count, binary(spv::OpShiftRightLogical, u32, available, constant(3))), BdaAbi::FaultReason::InvalidTable}, + {binary(spv::OpIEqual, boolean, bytes, constant(0)), BdaAbi::FaultReason::Overflow}, + {binary(spv::OpULessThanEqual, boolean, end, address), BdaAbi::FaultReason::Overflow}, + }); const auto header = state.module.AllocateId(); const auto body = state.module.AllocateId(); const auto continuation = state.module.AllocateId(); const auto merge = state.module.AllocateId(); + const auto entryLabel = state.currentLabel; state.module.AddFunction(spv::OpBranch, header); EmitLabel(state, header); - const auto lower = load(low); - const auto upper = load(high); + const auto lower = state.module.AllocateId(); + const auto upper = state.module.AllocateId(); + const auto newLow = state.module.AllocateId(); + const auto newHigh = state.module.AllocateId(); + state.module.AddFunction(spv::OpPhi, u32, lower, constant(0), entryLabel, newLow, continuation); + state.module.AddFunction(spv::OpPhi, u32, upper, count, entryLabel, newHigh, continuation); const auto search = binary(spv::OpULessThan, boolean, lower, upper); state.module.AddFunction(spv::OpLoopMerge, merge, continuation, spv::LoopControlMaskNone); state.module.AddFunction(spv::OpBranchConditional, search, body, merge); @@ -81,17 +99,13 @@ std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool r const auto midpoint = binary(spv::OpIAdd, u32, lower, binary(spv::OpShiftRightLogical, u32, binary(spv::OpISub, u32, upper, lower), constant(1))); const auto entry = binary(spv::OpIAdd, u32, constant(4), binary(spv::OpIMul, u32, midpoint, constant(8))); const auto before = binary(spv::OpULessThan, boolean, address, BdaLoadAddress(state, entry)); - const auto newLow = state.module.AllocateId(); - const auto newHigh = state.module.AllocateId(); state.module.AddFunction(spv::OpSelect, u32, newLow, before, lower, binary(spv::OpIAdd, u32, midpoint, constant(1))); state.module.AddFunction(spv::OpSelect, u32, newHigh, before, midpoint, upper); - state.module.AddFunction(spv::OpStore, low, newLow); - state.module.AddFunction(spv::OpStore, high, newHigh); state.module.AddFunction(spv::OpBranch, continuation); EmitLabel(state, continuation); state.module.AddFunction(spv::OpBranch, header); EmitLabel(state, merge); - const auto index = load(low); + const auto index = lower; fail(binary(spv::OpIEqual, boolean, index, constant(0)), BdaAbi::FaultReason::Unmapped); const auto selected = binary(spv::OpIAdd, u32, constant(4), binary(spv::OpIMul, u32, binary(spv::OpISub, u32, index, constant(1)), constant(8))); const auto at = [&](std::uint32_t offset) { return binary(spv::OpIAdd, u32, selected, constant(offset)); }; @@ -99,17 +113,33 @@ std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool r const auto finish = BdaLoadAddress(state, at(2)); const auto base = BdaLoadAddress(state, at(4)); const auto permissions = BdaLoadWord(state, at(6)); - fail(binary(spv::OpINotEqual, boolean, BdaLoadWord(state, at(7)), constant(0)), BdaAbi::FaultReason::InvalidTable); - fail(binary(spv::OpUGreaterThanEqual, boolean, begin, finish), BdaAbi::FaultReason::InvalidTable); - fail(binary(spv::OpUGreaterThan, boolean, end, finish), BdaAbi::FaultReason::Unmapped); - fail(binary(spv::OpINotEqual, boolean, binary(spv::OpBitwiseAnd, u32, permissions, constant(permission)), constant(permission)), BdaAbi::FaultReason::Permission); - fail(binary(spv::OpIEqual, boolean, base, BdaConstant(state, 0)), BdaAbi::FaultReason::InvalidTable); const auto offset = binary(spv::OpISub, u64, address, begin); const auto result = binary(spv::OpIAdd, u64, base, offset); - fail(binary(spv::OpULessThan, boolean, result, base), BdaAbi::FaultReason::Overflow); const auto deviceEnd = binary(spv::OpIAdd, u64, result, Unary(state, spv::OpUConvert, u64, bytes)); - fail(binary(spv::OpULessThanEqual, boolean, deviceEnd, result), BdaAbi::FaultReason::Overflow); - state.module.AddFunction(spv::OpReturnValue, result); + failFirst({ + {binary(spv::OpINotEqual, boolean, BdaLoadWord(state, at(7)), constant(0)), BdaAbi::FaultReason::InvalidTable}, + {binary(spv::OpUGreaterThanEqual, boolean, begin, finish), BdaAbi::FaultReason::InvalidTable}, + {binary(spv::OpUGreaterThan, boolean, end, finish), BdaAbi::FaultReason::Unmapped}, + {binary(spv::OpINotEqual, boolean, binary(spv::OpBitwiseAnd, u32, permissions, constant(permission)), constant(permission)), BdaAbi::FaultReason::Permission}, + {binary(spv::OpIEqual, boolean, base, BdaConstant(state, 0)), BdaAbi::FaultReason::InvalidTable}, + {binary(spv::OpULessThan, boolean, result, base), BdaAbi::FaultReason::Overflow}, + {binary(spv::OpULessThanEqual, boolean, deviceEnd, result), BdaAbi::FaultReason::Overflow}, + }); + 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; } @@ -134,8 +164,11 @@ static std::uint32_t DefineBdaNoteWrite(SpirvEmitterState& state) { state.module.AddFunction(spv::OpVariable, TypePointer(state, spv::StorageClassFunction, u32), counter, spv::StorageClassFunction); state.module.AddFunction(spv::OpStore, done, ConstantBool(state, false)); state.module.AddFunction(spv::OpStore, counter, constant(0u)); - const auto page = binary(spv::OpIAdd, u32, Unary(state, spv::OpUConvert, u32, binary(spv::OpShiftRightLogical, TypeScalarU64(state), address, BdaConstant(state, BdaAbi::WrittenPageShift))), constant(1)); - const auto hash = binary(spv::OpShiftRightLogical, u32, binary(spv::OpIMul, u32, page, constant(0x9e3779b1u)), constant(32u - static_cast(std::countr_zero(BdaAbi::WrittenPageSlots)))); + const auto u64 = TypeScalarU64(state); + const auto key = binary(spv::OpIAdd, u32, Unary(state, spv::OpUConvert, u32, binary(spv::OpShiftRightLogical, u64, address, BdaConstant(state, BdaAbi::WrittenChunkShift))), constant(1)); + const auto page = binary(spv::OpBitwiseAnd, u32, Unary(state, spv::OpUConvert, u32, binary(spv::OpShiftRightLogical, u64, address, BdaConstant(state, BdaAbi::WrittenPageShift))), constant(BdaAbi::WrittenChunkPages - 1u)); + const auto bit = binary(spv::OpShiftLeftLogical, u32, constant(1u), page); + const auto hash = binary(spv::OpShiftRightLogical, u32, binary(spv::OpIMul, u32, key, constant(0x9e3779b1u)), constant(32u - static_cast(std::countr_zero(BdaAbi::WrittenPageSlots)))); const auto scope = constant(spv::ScopeDevice); const auto relaxed = constant(spv::MemorySemanticsMaskNone); const auto header = state.module.AllocateId(); @@ -152,16 +185,32 @@ static std::uint32_t DefineBdaNoteWrite(SpirvEmitterState& state) { state.module.AddFunction(spv::OpLoopMerge, merge, continuation, spv::LoopControlMaskNone); state.module.AddFunction(spv::OpBranchConditional, searching, body, merge); EmitLabel(state, body); - const auto slot = binary(spv::OpIAdd, u32, constant(BdaAbi::WrittenSlotsWord), binary(spv::OpBitwiseAnd, u32, binary(spv::OpIAdd, u32, hash, probe), constant(BdaAbi::WrittenPageSlots - 1u))); + const auto slot = binary(spv::OpBitwiseAnd, u32, binary(spv::OpIAdd, u32, hash, probe), constant(BdaAbi::WrittenPageSlots - 1u)); + const auto keyWord = binary(spv::OpIAdd, u32, constant(BdaAbi::WrittenSlotsWord), binary(spv::OpShiftLeftLogical, u32, slot, constant(1u))); const auto current = state.module.AllocateId(); - state.module.AddFunction(spv::OpAtomicLoad, u32, current, BdaWord(state, state.faultBufferVariable, slot), scope, relaxed); - const auto held = binary(spv::OpIEqual, boolean, current, page); - state.module.AddFunction(spv::OpStore, done, held); - EmitIfCondition(state, Unary(state, spv::OpLogicalNot, boolean, held), [&] { + state.module.AddFunction(spv::OpAtomicLoad, u32, current, BdaWord(state, state.faultBufferVariable, keyWord), scope, relaxed); + const auto same = binary(spv::OpIEqual, boolean, current, key); + state.module.AddFunction(spv::OpStore, done, same); + EmitIfCondition(state, Unary(state, spv::OpLogicalNot, boolean, same), [&] { const auto previous = state.module.AllocateId(); - state.module.AddFunction(spv::OpAtomicCompareExchange, u32, previous, BdaWord(state, state.faultBufferVariable, slot), scope, relaxed, relaxed, page, constant(0u)); - const auto taken = binary(spv::OpLogicalOr, boolean, binary(spv::OpIEqual, boolean, previous, constant(0u)), binary(spv::OpIEqual, boolean, previous, page)); - state.module.AddFunction(spv::OpStore, done, taken); + state.module.AddFunction(spv::OpAtomicCompareExchange, u32, previous, BdaWord(state, state.faultBufferVariable, keyWord), scope, relaxed, relaxed, key, constant(0u)); + const auto claimed = binary(spv::OpIEqual, boolean, previous, constant(0u)); + state.module.AddFunction(spv::OpStore, done, binary(spv::OpLogicalOr, boolean, claimed, binary(spv::OpIEqual, boolean, previous, key))); + EmitIfCondition(state, claimed, [&] { + const auto index = state.module.AllocateId(); + state.module.AddFunction(spv::OpAtomicIAdd, u32, index, BdaWord(state, state.faultBufferVariable, constant(BdaAbi::WrittenCountWord)), scope, relaxed, constant(1u)); + state.module.AddFunction(spv::OpAtomicStore, BdaWord(state, state.faultBufferVariable, binary(spv::OpIAdd, u32, constant(BdaAbi::WrittenListWord), index)), scope, relaxed, slot); + }); + }); + const auto holds = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, boolean, holds, done); + EmitIfCondition(state, holds, [&] { + const auto maskWord = binary(spv::OpIAdd, u32, keyWord, constant(1u)); + const auto mask = state.module.AllocateId(); + state.module.AddFunction(spv::OpAtomicLoad, u32, mask, BdaWord(state, state.faultBufferVariable, maskWord), scope, relaxed); + EmitIfCondition(state, binary(spv::OpIEqual, boolean, binary(spv::OpBitwiseAnd, u32, mask, bit), constant(0u)), [&] { + state.module.AddFunction(spv::OpAtomicOr, u32, state.module.AllocateId(), BdaWord(state, state.faultBufferVariable, maskWord), scope, relaxed, bit); + }); }); state.module.AddFunction(spv::OpBranch, continuation); EmitLabel(state, continuation); @@ -178,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; @@ -185,8 +238,11 @@ 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); state.bdaAtomicPointerFunction = DefineBdaLookup(state, "get_bda_atomic_pointer", true, BdaAbi::Read | BdaAbi::Write); state.bdaNoteWriteFunction = DefineBdaNoteWrite(state); DefineBdaByteWriteFunctions(state); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp index 0e827c94cb..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 { @@ -66,31 +70,39 @@ std::uint32_t EmitBdaByte(SpirvEmitterState& state, std::uint32_t guest, std::ui return value; } -std::uint32_t EmitBdaBytes(SpirvEmitterState& state, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction, std::uint32_t accessMask) { - auto result = ConstantU32(state, 0u); - for (std::uint32_t byte = 0; byte < bytes; ++byte) { - const auto guest = Binary(state, spv::OpIAdd, TypeScalarU64(state), address, BdaConstant(state, byte)); - const auto value = EmitBdaByte(state, guest, instruction, accessMask); - result = Binary(state, spv::OpBitwiseOr, TypeU32(state), result, Binary(state, spv::OpShiftLeftLogical, TypeU32(state), value, ConstantU32(state, byte * 8u))); - } +std::uint32_t EmitBdaByteLoop(SpirvEmitterState& state, std::uint32_t address, std::uint32_t count, std::uint32_t instruction, std::uint32_t accessMask) { + const auto u32 = TypeU32(state); + const auto entry = state.currentLabel; + const auto header = state.module.AllocateId(); + const auto body = state.module.AllocateId(); + const auto continuation = state.module.AllocateId(); + const auto merge = state.module.AllocateId(); + const auto byte = state.module.AllocateId(); + const auto result = state.module.AllocateId(); + const auto nextByte = state.module.AllocateId(); + const auto nextResult = state.module.AllocateId(); + state.module.AddFunction(spv::OpBranch, header); + EmitLabel(state, header); + state.module.AddFunction(spv::OpPhi, u32, byte, ConstantU32(state, 0u), entry, nextByte, continuation); + state.module.AddFunction(spv::OpPhi, u32, result, ConstantU32(state, 0u), entry, nextResult, continuation); + const auto more = Binary(state, spv::OpULessThan, TypeBool(state), byte, count); + state.module.AddFunction(spv::OpLoopMerge, merge, continuation, spv::LoopControlMaskNone); + state.module.AddFunction(spv::OpBranchConditional, more, body, merge); + EmitLabel(state, body); + const auto guest = Binary(state, spv::OpIAdd, TypeScalarU64(state), address, Unary(state, spv::OpUConvert, TypeScalarU64(state), byte)); + const auto value = EmitBdaByte(state, guest, instruction, accessMask); + state.module.AddFunction(spv::OpBitwiseOr, u32, nextResult, result, Binary(state, spv::OpShiftLeftLogical, u32, value, Binary(state, spv::OpShiftLeftLogical, u32, byte, ConstantU32(state, 3u)))); + state.module.AddFunction(spv::OpBranch, continuation); + EmitLabel(state, continuation); + state.module.AddFunction(spv::OpIAdd, u32, nextByte, byte, ConstantU32(state, 1u)); + state.module.AddFunction(spv::OpBranch, header); + EmitLabel(state, merge); return result; } -// 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; +std::uint32_t EmitBdaBytes(SpirvEmitterState& state, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction, std::uint32_t accessMask) { + if (bytes == 1u) return EmitBdaByte(state, address, instruction, accessMask); + return EmitBdaByteLoop(state, address, ConstantU32(state, bytes), instruction, accessMask); } 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) { @@ -127,7 +139,41 @@ std::uint32_t BdaSpanReadResultType(SpirvEmitterState& state) { return state.bdaStopsInvocations ? state.module.Type(spv::OpTypeStruct, values, TypeU32(state)) : values; } -std::uint32_t DefineBdaSpanReadFunction(SpirvEmitterState& state, bool coherent) { +std::uint32_t DefineBdaSpanLoadFunction(SpirvEmitterState& state, bool coherent) { + const auto u32 = TypeU32(state); + const auto u64 = TypeScalarU64(state); + const auto values = TypeU32Vector(state, 4); + const auto function = state.module.AllocateId(); + state.module.AddName(function, std::string("load_bda_span") + (coherent ? "_coherent" : "")); + state.module.AddFunction(spv::OpFunction, values, function, spv::FunctionControlDontInlineMask, state.module.Type(spv::OpTypeFunction, values, u64, u32, u32)); + const auto physical = state.module.AllocateId(); + const auto firstBytes = state.module.AllocateId(); + const auto used = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionParameter, u64, physical); + state.module.AddFunction(spv::OpFunctionParameter, u32, firstBytes); + state.module.AddFunction(spv::OpFunctionParameter, u32, used); + EmitLabel(state, state.module.AllocateId()); + std::array loaded{}; + for (std::uint32_t dword = 0; dword < 4u; ++dword) { + const auto isUsed = Binary(state, spv::OpINotEqual, TypeBool(state), Binary(state, spv::OpBitwiseAnd, u32, used, ConstantU32(state, 1u << dword)), ConstantU32(state, 0u)); + loaded[dword] = EmitValueOrZeroIfCondition(state, isUsed, [&] { + const auto delta = Unary(state, spv::OpUConvert, u64, Binary(state, spv::OpISub, u32, ConstantU32(state, dword * 4u), firstBytes)); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpConvertUToPtr, TypePhysicalU32Pointer(state), pointer, Binary(state, spv::OpIAdd, u64, physical, delta)); + const auto value = state.module.AllocateId(); + if (coherent) state.module.AddFunction(spv::OpAtomicLoad, u32, value, pointer, ConstantU32(state, spv::ScopeDevice), ConstantU32(state, 0u)); + else state.module.AddFunction(spv::OpLoad, u32, value, pointer, BdaAccessMask(coherent), 4u); + return value; + }); + } + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, values, result, loaded[0], loaded[1], loaded[2], loaded[3]); + state.module.AddFunction(spv::OpReturnValue, result); + state.module.AddFunction(spv::OpFunctionEnd); + return function; +} + +std::uint32_t DefineBdaSpanReadFunction(SpirvEmitterState& state, bool coherent, std::uint32_t loader) { const auto u32 = TypeU32(state); const auto u64 = TypeScalarU64(state); const auto boolean = TypeBool(state); @@ -166,39 +212,52 @@ std::uint32_t DefineBdaSpanReadFunction(SpirvEmitterState& state, bool coherent) return value; }; const auto read = EmitValueIfElse(state, wide, values, [&] { - std::array loaded{}; - for (std::uint32_t dword = 0; dword < 4u; ++dword) { - loaded[dword] = EmitValueOrZeroIfCondition(state, isUsed(dword), [&] { - const auto delta = Unary(state, spv::OpUConvert, u64, Binary(state, spv::OpISub, u32, ConstantU32(state, dword * 4u), firstBytes)); - const auto pointer = state.module.AllocateId(); - state.module.AddFunction(spv::OpConvertUToPtr, TypePhysicalU32Pointer(state), pointer, Binary(state, spv::OpIAdd, u64, physical, delta)); - const auto value = state.module.AllocateId(); - state.module.AddFunction(spv::OpLoad, u32, value, pointer, BdaAccessMask(coherent), 4u); - return value; - }); - } - return composite(loaded); + const auto loaded = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionCall, values, loaded, loader, physical, firstBytes, used); + return loaded; }, [&] { - std::array bytes{}; - for (std::uint32_t dword = 0; dword < 4u; ++dword) { - bytes[dword] = EmitValueOrZeroIfCondition(state, isUsed(dword), [&] { - const auto isFirst = Binary(state, spv::OpIEqual, boolean, first, ConstantU32(state, dword)); - const auto dwordAddress = EmitValueIfElse(state, isFirst, u64, [&] { return firstAddress; }, [&] { - return AddBdaSignedOffset(state, address, Binary(state, spv::OpIAdd, u32, offset, ConstantU32(state, dword * 4u)), instruction); - }); - const auto pair = state.module.AllocateId(); - state.module.AddFunction(spv::OpFunctionCall, TypeU32Vector(state, 2), pair, reader, dwordAddress, instruction); - if (stops) { - const auto stopped = state.module.AllocateId(); - state.module.AddFunction(spv::OpCompositeExtract, u32, stopped, pair, 1u); - StopBdaInvocationIf(state, Binary(state, spv::OpINotEqual, boolean, stopped, zero)); - } - const auto value = state.module.AllocateId(); - state.module.AddFunction(spv::OpCompositeExtract, u32, value, pair, 0u); - return value; + const auto entry = state.currentLabel; + const auto header = state.module.AllocateId(); + const auto body = state.module.AllocateId(); + const auto continuation = state.module.AllocateId(); + const auto merge = state.module.AllocateId(); + const auto dword = state.module.AllocateId(); + const auto gathered = state.module.AllocateId(); + const auto nextDword = state.module.AllocateId(); + const auto nextGathered = state.module.AllocateId(); + const auto zeros = state.module.Constant(spv::OpConstantComposite, values, zero, zero, zero, zero); + state.module.AddFunction(spv::OpBranch, header); + EmitLabel(state, header); + state.module.AddFunction(spv::OpPhi, u32, dword, zero, entry, nextDword, continuation); + state.module.AddFunction(spv::OpPhi, values, gathered, zeros, entry, nextGathered, continuation); + const auto more = Binary(state, spv::OpULessThan, boolean, dword, ConstantU32(state, 4u)); + state.module.AddFunction(spv::OpLoopMerge, merge, continuation, spv::LoopControlMaskNone); + state.module.AddFunction(spv::OpBranchConditional, more, body, merge); + EmitLabel(state, body); + const auto used32 = Binary(state, spv::OpINotEqual, boolean, Binary(state, spv::OpBitwiseAnd, u32, used, Binary(state, spv::OpShiftLeftLogical, u32, ConstantU32(state, 1u), dword)), zero); + const auto value = EmitValueOrZeroIfCondition(state, used32, [&] { + const auto isFirst = Binary(state, spv::OpIEqual, boolean, first, dword); + const auto dwordAddress = EmitValueIfElse(state, isFirst, u64, [&] { return firstAddress; }, [&] { + return AddBdaSignedOffset(state, address, Binary(state, spv::OpIAdd, u32, offset, Binary(state, spv::OpShiftLeftLogical, u32, dword, ConstantU32(state, 2u))), instruction); }); - } - return composite(bytes); + const auto pair = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionCall, TypeU32Vector(state, 2), pair, reader, dwordAddress, instruction); + if (stops) { + const auto stopped = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, u32, stopped, pair, 1u); + StopBdaInvocationIf(state, Binary(state, spv::OpINotEqual, boolean, stopped, zero)); + } + const auto loaded = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, u32, loaded, pair, 0u); + return loaded; + }); + state.module.AddFunction(spv::OpVectorInsertDynamic, values, nextGathered, gathered, value, dword); + state.module.AddFunction(spv::OpBranch, continuation); + EmitLabel(state, continuation); + state.module.AddFunction(spv::OpIAdd, u32, nextDword, dword, ConstantU32(state, 1u)); + state.module.AddFunction(spv::OpBranch, header); + EmitLabel(state, merge); + return gathered; }); auto returned = read; if (stops) { @@ -234,7 +293,35 @@ void DefineBdaDwordReadFunctions(SpirvEmitterState& state) { EmitLabel(state, state.module.AllocateId()); state.bdaStopsInvocations = stop; EmitBdaOverflowCheck(state, address, 4u, instruction); - const auto value = EmitBdaBytes(state, address, 4u, instruction, BdaAccessMask(coherent)); + std::uint32_t value = 0; + if (coherent && state.bdaProbeFunction != 0) { + const auto u64t = TypeScalarU64(state); + const auto physical = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionCall, u64t, physical, state.bdaProbeFunction, address, ConstantU32(state, 4u), instruction); + const auto usable = Binary(state, spv::OpLogicalAnd, TypeBool(state), Binary(state, spv::OpINotEqual, TypeBool(state), physical, BdaConstant(state, 0u)), Binary(state, spv::OpIEqual, TypeBool(state), Binary(state, spv::OpBitwiseAnd, u64t, physical, BdaConstant(state, 3u)), BdaConstant(state, 0u))); + const auto atomicLabel = state.module.AllocateId(); + const auto byteLabel = state.module.AllocateId(); + const auto byteExit = state.module.AllocateId(); + const auto merge = state.module.AllocateId(); + state.module.AddFunction(spv::OpSelectionMerge, merge, spv::SelectionControlMaskNone); + state.module.AddFunction(spv::OpBranchConditional, usable, atomicLabel, byteLabel); + EmitLabel(state, atomicLabel); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpConvertUToPtr, TypePhysicalU32Pointer(state), pointer, physical); + const auto atomicValue = state.module.AllocateId(); + state.module.AddFunction(spv::OpAtomicLoad, u32, atomicValue, pointer, ConstantU32(state, spv::ScopeDevice), ConstantU32(state, 0u)); + state.module.AddFunction(spv::OpBranch, merge); + EmitLabel(state, byteLabel); + const auto byteValue = EmitBdaBytes(state, address, 4u, instruction, BdaAccessMask(coherent)); + state.module.AddFunction(spv::OpBranch, byteExit); + EmitLabel(state, byteExit); + state.module.AddFunction(spv::OpBranch, merge); + EmitLabel(state, merge); + value = state.module.AllocateId(); + state.module.AddFunction(spv::OpPhi, u32, value, atomicValue, atomicLabel, byteValue, byteExit); + } else { + value = EmitBdaBytes(state, address, 4u, instruction, BdaAccessMask(coherent)); + } const auto result = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeConstruct, pair, result, value, ConstantU32(state, 0u)); state.module.AddFunction(spv::OpReturnValue, result); @@ -252,13 +339,17 @@ void DefineBdaSpanReadFunctions(SpirvEmitterState& state) { const bool coherentAccesses = std::any_of(memory.begin(), memory.end(), [](const MemoryInfo& info) { return info.coherent; }); const bool stops = state.bdaStopsInvocations; const auto zero = ConstantU32(state, 0u); + std::array loaders{}; + for (const bool coherent : {false, true}) { + if (!coherent || coherentAccesses) loaders[coherent] = DefineBdaSpanLoadFunction(state, coherent); + } for (const bool stop : {false, true}) { if (stop && !stops) continue; state.bdaStopsInvocations = stop; state.bdaStopValue = stop ? state.module.Constant(spv::OpConstantComposite, BdaSpanReadResultType(state), state.module.Constant(spv::OpConstantComposite, TypeU32Vector(state, 4), zero, zero, zero, zero), ConstantU32(state, 1u)) : 0u; for (const bool coherent : {false, true}) { if (coherent && !coherentAccesses) continue; - state.bdaSpanReadFunctions[stop][coherent] = DefineBdaSpanReadFunction(state, coherent); + state.bdaSpanReadFunctions[stop][coherent] = DefineBdaSpanReadFunction(state, coherent, loaders[coherent]); } } state.bdaStopsInvocations = stops; @@ -271,11 +362,33 @@ 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); +} + std::uint32_t AddBdaAddress(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, bool subtract) { auto& state = ctx.state; const auto result = Binary(state, subtract ? spv::OpISub : spv::OpIAdd, TypeScalarU64(state), address, offset); const auto overflow = subtract ? Binary(state, spv::OpUGreaterThan, TypeBool(state), offset, address) : Binary(state, spv::OpULessThan, TypeBool(state), result, address); - EmitIfCondition(state, overflow, [&] { RecordBdaFault(state, address, ConstantU32(state, 0u), ConstantU32(state, inst.Flags().pc), BdaAbi::FaultReason::Overflow); }); + EmitIfCondition(state, overflow, [&] { RecordBdaFault(state, address, ConstantU32(state, 0u), BdaInstructionPc(state, inst), BdaAbi::FaultReason::Overflow); }); StopBdaInvocationIf(state, overflow); return result; } @@ -298,7 +411,6 @@ void ValidateBdaTarget(const IrProgram& program, const SpirvTargetOptions& targe for (const auto extension : {"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}) { if (std::find(target.supportedExtensions.begin(), target.supportedExtensions.end(), extension) == target.supportedExtensions.end()) throw std::runtime_error(std::string("BDA requires unsupported extension ") + extension); } - if (program.Resources().stage == IrShaderStage::TessellationControl) throw std::runtime_error("BDA fault termination requires a barrier-safe tessellation-control execution protocol"); } bool BdaInvocationsMayStop(const IrProgram& program) { @@ -310,40 +422,253 @@ 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"); if (state.bdaPointerFunction == 0) ctx.Fail(inst, "BDA lookup function is missing"); if (bits == 32u) return EmitBdaDwordReads(ctx, inst, address, 0u, 1u)[0]; - const auto instruction = ConstantU32(state, inst.Flags().pc); + const auto instruction = BdaInstructionPc(state, inst); EmitBdaOverflowCheck(state, address, bits / 8u, instruction); 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"); - const auto instruction = ConstantU32(state, inst.Flags().pc); + const auto instruction = BdaInstructionPc(state, inst); if (bits != 8u && bits != 16u && bits != 32u) ctx.Fail(inst, "unsupported BDA write width"); - if (bits != 32u) { + 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(); return; } const auto unaligned = BdaAddressUnaligned(state, address); - EmitIfCondition(state, unaligned, [&] { RecordBdaFault(state, address, ConstantU32(state, 4u), instruction, BdaAbi::FaultReason::Unaligned); }); + EmitIfCondition(state, unaligned, storeBytes); EmitIfCondition(state, Unary(state, spv::OpLogicalNot, TypeBool(state), unaligned), [&] { EmitBdaStoreAt(state, address, 4u, TypePhysicalU32Pointer(state), value, instruction, BdaAccessMask(state, inst)); }); @@ -405,7 +730,7 @@ void EmitBdaStore(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t auto& state = ctx.state; if (bits != 8u && bits != 16u && bits != 32u) ctx.Fail(inst, "unsupported BDA store width"); if (state.bdaWritePointerFunction == 0 || state.bdaNoteWriteFunction == 0) ctx.Fail(inst, "BDA write functions are missing"); - const auto instruction = ConstantU32(state, inst.Flags().pc); + const auto instruction = BdaInstructionPc(state, inst); const auto storeBytes = [&] { const auto function = state.bdaByteWriteFunctions[BdaCoherent(state, inst)]; if (function == 0) ctx.Fail(inst, "BDA byte write function is missing"); @@ -422,11 +747,83 @@ void EmitBdaStore(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t }); } +void EmitBdaDwordWrites(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, std::uint32_t dwords, std::uint32_t value) { + auto& state = ctx.state; + if (dwords < 2u || dwords > 4u) ctx.Fail(inst, "unsupported BDA dword store count"); + if (state.bdaWritePointerFunction == 0 || state.bdaNoteWriteFunction == 0) ctx.Fail(inst, "BDA write functions are missing"); + const auto u32 = TypeU32(state); + const auto u64 = TypeScalarU64(state); + const auto boolean = TypeBool(state); + std::array values{}; + std::array addresses{}; + for (std::uint32_t dword = 0; dword < dwords; ++dword) { + values[dword] = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, u32, values[dword], value, dword); + addresses[dword] = AddBdaImmediate(ctx, inst, address, static_cast(offset + dword * 4u)); + } + const auto storeDwords = [&] { + for (std::uint32_t dword = 0; dword < dwords; ++dword) EmitBdaStore(ctx, inst, addresses[dword], values[dword], 32u); + }; + if (state.bdaWriteProbeFunction == 0) { + storeDwords(); + return; + } + const auto instruction = BdaInstructionPc(state, inst); + const auto accessMask = BdaAccessMask(state, inst); + const auto physical = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionCall, u64, physical, state.bdaWriteProbeFunction, addresses[0], ConstantU32(state, dwords * 4u), instruction); + const auto mapped = Binary(state, spv::OpINotEqual, boolean, physical, BdaConstant(state, 0u)); + const auto aligned = Binary(state, spv::OpLogicalAnd, boolean, Unary(state, spv::OpLogicalNot, boolean, BdaAddressUnaligned(state, addresses[0])), Unary(state, spv::OpLogicalNot, boolean, BdaAddressUnaligned(state, physical))); + const auto wide = Binary(state, spv::OpLogicalAnd, boolean, mapped, aligned); + const auto wideLabel = state.module.AllocateId(); + const auto dwordLabel = state.module.AllocateId(); + const auto merge = state.module.AllocateId(); + state.module.AddFunction(spv::OpSelectionMerge, merge, spv::SelectionControlMaskNone); + state.module.AddFunction(spv::OpBranchConditional, wide, wideLabel, dwordLabel); + EmitLabel(state, wideLabel); + const auto vectorBytes = dwords == 3u ? 4u : dwords * 4u; + const auto separate = [&] { + for (std::uint32_t dword = 0; dword < dwords; ++dword) { + const auto element = dword == 0u ? physical : Binary(state, spv::OpIAdd, u64, physical, BdaConstant(state, dword * 4u)); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpConvertUToPtr, TypePhysicalU32Pointer(state), pointer, element); + state.module.AddFunction(spv::OpStore, pointer, values[dword], accessMask, 4u); + } + }; + if (dwords == 3u) { + separate(); + } else { + const auto vectorAligned = Binary(state, spv::OpIEqual, boolean, Binary(state, spv::OpBitwiseAnd, u64, physical, BdaConstant(state, vectorBytes - 1u)), BdaConstant(state, 0u)); + const auto vectorType = TypeU32Vector(state, dwords); + const auto vector = state.module.AllocateId(); + if (dwords == 2u) state.module.AddFunction(spv::OpCompositeConstruct, vectorType, vector, values[0], values[1]); + else state.module.AddFunction(spv::OpCompositeConstruct, vectorType, vector, values[0], values[1], values[2], values[3]); + EmitIfCondition(state, vectorAligned, [&] { + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpConvertUToPtr, TypePointer(state, spv::StorageClassPhysicalStorageBuffer, vectorType), pointer, physical); + state.module.AddFunction(spv::OpStore, pointer, vector, accessMask, vectorBytes); + }); + EmitIfCondition(state, Unary(state, spv::OpLogicalNot, boolean, vectorAligned), separate); + } + const auto last = addresses[dwords - 1u]; + state.module.AddFunction(spv::OpFunctionCall, state.module.Type(spv::OpTypeVoid), state.module.AllocateId(), state.bdaNoteWriteFunction, addresses[0]); + const auto pageShift = BdaConstant(state, BdaAbi::WrittenPageShift); + const auto crosses = Binary(state, spv::OpINotEqual, boolean, Binary(state, spv::OpShiftRightLogical, u64, addresses[0], pageShift), Binary(state, spv::OpShiftRightLogical, u64, last, pageShift)); + EmitIfCondition(state, crosses, [&] { + state.module.AddFunction(spv::OpFunctionCall, state.module.Type(spv::OpTypeVoid), state.module.AllocateId(), state.bdaNoteWriteFunction, last); + }); + state.module.AddFunction(spv::OpBranch, merge); + EmitLabel(state, dwordLabel); + storeDwords(); + state.module.AddFunction(spv::OpBranch, merge); + EmitLabel(state, merge); +} + std::uint32_t EmitBdaAtomic(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t bytes, const std::function& operation) { auto& state = ctx.state; if (bytes != 4u && bytes != 8u) ctx.Fail(inst, "unsupported BDA atomic width"); if (state.bdaWritePointerFunction == 0 || state.bdaNoteWriteFunction == 0) ctx.Fail(inst, "BDA write functions are missing"); - const auto instruction = ConstantU32(state, inst.Flags().pc); + const auto instruction = BdaInstructionPc(state, inst); const auto type = bytes == 8u ? TypeScalarU64(state) : TypeU32(state); const auto zero = bytes == 8u ? BdaConstant(state, 0u) : ConstantU32(state, 0u); const auto unaligned = Binary(state, spv::OpINotEqual, TypeBool(state), Binary(state, spv::OpBitwiseAnd, TypeScalarU64(state), address, BdaConstant(state, bytes - 1u)), BdaConstant(state, 0u)); @@ -451,12 +848,12 @@ std::uint32_t EmitBdaAtomic(SpirvValueEmitContext& ctx, const IrValue& inst, std // unaligned mirror or a wrapping span falls back to the byte path. That path repeats, per extracted // dword in order, the address, overflow check and byte lookups of a separate dword load, so every // fault is recorded exactly as one; the probe records none, and a span it accepts cannot fault. -std::array EmitBdaDwordReads(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, std::uint32_t dwords, bool everyDword) { +std::array EmitBdaDwordReads(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, std::uint32_t dwords, bool everyDword, std::uint32_t usedDwords) { auto& state = ctx.state; if (dwords == 0u || dwords > 4u) ctx.Fail(inst, "unsupported BDA read width"); if (state.bdaPointerFunction == 0) ctx.Fail(inst, "BDA lookup function is missing"); - const auto instruction = ConstantU32(state, inst.Flags().pc); - const auto used = everyDword ? (1u << dwords) - 1u : UsedBdaDwords(inst, dwords); + const auto instruction = BdaInstructionPc(state, inst); + const auto used = usedDwords != 0u ? usedDwords : everyDword ? (1u << dwords) - 1u : UsedBdaDwords(inst, dwords); const auto isUsed = [&](std::uint32_t dword) { return (used & (1u << dword)) != 0u; }; std::array values{}; std::uint32_t first = dwords; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index b663800a2f..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) { @@ -118,7 +125,7 @@ std::uint32_t SpirvValueEmitContext::Ballot(const IrValue* predicate) { const auto ballotType = TypeU32Vector(state, 4u); const auto scope = ConstantU32(state, spv::ScopeSubgroup); const auto low = state.module.AllocateId(); - state.module.AddFunction(spv::OpGroupNonUniformBallot, ballotType, low, scope, otherHalf == nullptr || half == 0u ? Def(predicate) : otherHalf->Def(predicate)); + EmitSubgroupBallot(state, low, otherHalf == nullptr || half == 0u ? Def(predicate) : otherHalf->Def(predicate)); if (otherHalf == nullptr) { return EmitWaveBallot(state, low); } @@ -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)); @@ -136,7 +143,7 @@ std::uint32_t SpirvValueEmitContext::Ballot(const IrValue* predicate) { std::uint32_t SpirvValueEmitContext::FirstLane(std::uint32_t ballot) { if (otherHalf == nullptr) { const auto result = state.module.AllocateId(); - state.module.AddFunction(spv::OpGroupNonUniformBallotFindLSB, TypeU32(state), result, ConstantU32(state, spv::ScopeSubgroup), ballot); + EmitSubgroupBallotFindLsb(state, result, ballot); return result; } const auto low = state.module.AllocateId(); @@ -159,7 +166,7 @@ std::uint32_t SpirvValueEmitContext::Shuffle(const IrValue& inst, std::size_t in const auto scope = ConstantU32(state, spv::ScopeSubgroup); const auto low = state.module.AllocateId(); if (otherHalf == nullptr) { - state.module.AddFunction(spv::OpGroupNonUniformShuffle, type, low, scope, Arg(inst, index), EmitHostSubgroupLane(state, lane)); + EmitSubgroupShuffle(state, type, low, Arg(inst, index), EmitHostSubgroupLane(state, lane)); return low; } const auto physicalLane = EmitBinaryU32(state, spv::OpBitwiseAnd, lane, ConstantU32(state, 31u)); @@ -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"); } @@ -249,27 +263,62 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh state.supportedExtensions = target.supportedExtensions; 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; + state.requirements.subgroupLocalInvocationId = false; + } 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 8dc88285fd..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); } @@ -132,7 +146,7 @@ bool IsContinueTarget(const IrProgram& program, std::uint32_t block) { std::uint32_t EmitWaveAny(SpirvEmitterState& state, std::uint32_t predicate) { const auto ballot = state.module.AllocateId(); - state.module.AddFunction(spv::OpGroupNonUniformBallot, TypeU32Vector(state, 4u), ballot, ConstantU32(state, spv::ScopeSubgroup), predicate); + EmitSubgroupBallot(state, ballot, predicate); const auto wave = EmitWaveBallot(state, ballot); const auto low = state.module.AllocateId(); const auto high = state.module.AllocateId(); @@ -217,6 +231,11 @@ void EmitStructuredTerminator(SpirvValueEmitContext& ctx, const IrProgram& progr const std::string message = "branch condition " + std::string(IrOpcodeName(resolved->Opcode())) + " of block " + std::to_string(info.id) + " (defined in " + where + ") was not emitted before the branch"; ctx.Fail(message.c_str()); } + if (ctx.Label(trueBlock) == ctx.Label(falseBlock)) { + if (term.loopHeader) emitMerge(); + state.module.AddFunction(spv::OpBranch, ctx.Label(trueBlock)); + return; + } auto condition = EmitBranchCondition(ctx, info); if (state.loopGuardLimit != 0 && term.trueBlock != term.falseBlock) { if (IsLoopMerge(program, term.trueBlock)) condition = GuardLoopExit(state, info, condition, true); @@ -288,12 +307,14 @@ 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); case IrOpcode::RealtimeClock: return Invoke(EmitRealtimeClock, ctx, inst); case IrOpcode::MeshDrawParameter: return Invoke(EmitMeshDrawParameter, ctx, inst); case IrOpcode::MeshArgument: return Invoke(EmitMeshArgument, ctx, inst); + case IrOpcode::MeshRestartStart: return Invoke(EmitMeshRestartStart, ctx, inst); case IrOpcode::MeshAllocate: return Invoke(EmitMeshAllocate, ctx, inst); case IrOpcode::TessellationBase: return Invoke(EmitTessellationBase, ctx, inst); case IrOpcode::GetTessellationAttribute: return Invoke(EmitGetTessellationAttribute, ctx, inst); @@ -307,6 +328,8 @@ void EmitDirectInstruction(SpirvValueEmitContext& ctx, const IrValue& inst) { case IrOpcode::SetScalarRegister: return Invoke(EmitSetScalarRegister, ctx, inst); case IrOpcode::GetVectorRegister: return Invoke(EmitGetVectorRegister, ctx, inst); case IrOpcode::SetVectorRegister: return Invoke(EmitSetVectorRegister, ctx, inst); + case IrOpcode::LoadVectorWindow: return Invoke(EmitLoadVectorWindow, ctx, inst); + case IrOpcode::StoreVectorWindow: return Invoke(EmitStoreVectorWindow, ctx, inst); case IrOpcode::GetGotoVariable: return Invoke(EmitGetGotoVariable, ctx, inst); case IrOpcode::SetGotoVariable: return Invoke(EmitSetGotoVariable, ctx, inst); case IrOpcode::GetScc: return Invoke(EmitGetScc, ctx, inst); @@ -355,6 +378,8 @@ void EmitDirectInstruction(SpirvValueEmitContext& ctx, const IrValue& inst) { case IrOpcode::CompositeExtractU32x4: return Invoke(EmitCompositeExtractU32x4, ctx, inst); case IrOpcode::PackHalf2x16: return Invoke(EmitPackHalf2x16, ctx, inst); case IrOpcode::PackFloat2x16Rtz: return Invoke(EmitPackFloat2x16Rtz, ctx, inst); + case IrOpcode::FPAdd16x2: return Invoke(EmitFPAdd16x2, ctx, inst); + case IrOpcode::FPMul16x2: return Invoke(EmitFPMul16x2, ctx, inst); case IrOpcode::FPAbs32: return Invoke(EmitFPAbs32, ctx, inst); case IrOpcode::FPNeg32: return Invoke(EmitFPNeg32, ctx, inst); case IrOpcode::FPSaturate32: return Invoke(EmitFPSaturate32, ctx, inst); @@ -508,6 +533,9 @@ void EmitDirectInstruction(SpirvValueEmitContext& ctx, const IrValue& inst) { case IrOpcode::StoreAddressU8: return Invoke(EmitStoreAddressU8, ctx, inst); case IrOpcode::StoreAddressU16: return Invoke(EmitStoreAddressU16, ctx, inst); case IrOpcode::StoreAddressU32: return Invoke(EmitStoreAddressU32, ctx, inst); + case IrOpcode::StoreAddressU32x2: return Invoke(EmitStoreAddressU32x2, ctx, inst); + case IrOpcode::StoreAddressU32x3: return Invoke(EmitStoreAddressU32x3, ctx, inst); + case IrOpcode::StoreAddressU32x4: return Invoke(EmitStoreAddressU32x4, ctx, inst); case IrOpcode::AddressAtomicSwap32: return Invoke(EmitAddressAtomic, ctx, inst); case IrOpcode::AddressAtomicCmpSwap32: return Invoke(EmitAddressAtomic, ctx, inst); case IrOpcode::AddressAtomicIAdd32: return Invoke(EmitAddressAtomic, ctx, inst); @@ -638,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); @@ -730,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 | spv::MemorySemanticsWorkgroupMemoryMask)); + 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; } @@ -773,6 +805,32 @@ void PatchStructuredPhis(SpirvValueEmitContext& ctx, StructuredFunctionState& fu } } +bool CommunicatesThroughMemory(IrOpcode opcode) { + const auto buffer = BufferAccessOf(opcode); + const auto address = AddressOpcodeInfoOf(opcode).access; + const auto image = ImageOpcodeInfoOf(opcode).access; + 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) { @@ -812,11 +870,13 @@ void EmitControlFlow(SpirvValueEmitContext& context, StructuredFunctionState& fu context.Fail("structured control flow block has no terminator metadata"); } const bool stops = state.bdaStopsInvocations; - state.bdaStopsInvocations = stops && !IsContinueTarget(program, info->id); + state.continueTarget = IsContinueTarget(program, info->id); + state.bdaStopsInvocations = stops && !state.continueTarget; EmitStructuredBlock(context, functionState, block); functionState.blockExitLabels.emplace(block, state.currentLabel); EmitStructuredTerminator(context, program, *info); state.bdaStopsInvocations = stops; + state.continueTarget = false; } PatchStructuredPhis(context, functionState); } @@ -824,10 +884,20 @@ void EmitControlFlow(SpirvValueEmitContext& context, StructuredFunctionState& fu void EmitVoid(SpirvValueEmitContext&) { } -void EmitBarrier(SpirvEmitterState& state) { +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"); + } + } + return; + } const bool tessellation = state.program.Resources().stage == IrShaderStage::TessellationControl; const auto memoryScope = tessellation ? spv::ScopeInvocation : spv::ScopeWorkgroup; - const auto semantics = tessellation ? spv::MemorySemanticsMaskNone : spv::MemorySemanticsAcquireReleaseMask | spv::MemorySemanticsWorkgroupMemoryMask; + const std::uint32_t semantics = tessellation ? static_cast(spv::MemorySemanticsMaskNone) : spv::MemorySemanticsAcquireReleaseMask | LdsMemorySemantics(state); state.module.AddFunction(spv::OpControlBarrier, ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, memoryScope), ConstantU32(state, semantics)); } @@ -843,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 bcef5cda37..772f4f8f4c 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -4,8 +4,10 @@ #include "SpirvBackend/SpirvBda.hpp" #include "SpirvBackend/SpirvBufferFormat.hpp" #include "SpirvBackend/SpirvEmitterInstructions.hpp" +#include "SpirvBackend/SpirvMemory/SpirvDescriptors.hpp" #include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include "RdnaDecoder/RdnaImageOpDecoder.hpp" +#include "ImageTableAbi.hpp" #include #include #include @@ -15,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -22,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 { @@ -46,19 +40,16 @@ struct SampleSetup { std::uint32_t coord; }; -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 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); - const auto value = state.module.AllocateId(); - state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer); - return value; +std::uint32_t RuntimeImageMode(SpirvEmitterState& state, std::uint32_t resource) { + const auto mode = Binary(state, spv::OpShiftRightLogical, TypeU32(state), RuntimeImageDword(state, resource, offsetof(RuntimeAbi::ResourceMetadata, flags) / sizeof(std::uint32_t)), ConstantU32(state, 1u)); + return state.registrationModules ? Binary(state, spv::OpBitwiseAnd, TypeU32(state), mode, ConstantU32(state, RuntimeAbi::ImageModeMask)) : mode; } std::uint32_t RuntimeImageSwizzle(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t component) { - if (state.program.Info().images.at(resource).indirectRoot == ImageResource::NoIndirectImage) 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,23 +176,29 @@ std::uint32_t CubeLayer(SpirvEmitterState& state, std::uint32_t value) { return result; } -std::uint32_t CoordF32(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, std::uint32_t first, std::uint32_t components, std::uint32_t encoded) { - if (first == NoImageComponent || encoded < components || access.mem.imageAddressComponents < first + encoded) { +std::uint32_t CoordF32(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, std::uint32_t first, std::uint32_t components, std::uint32_t encoded, bool gradient = false) { + const bool cube = access.image.cube; + const auto present = std::min(encoded, components); + 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 bool cube = access.image.cube; - auto x = AddressF32(ctx, access, first); + const auto component = [&](std::uint32_t index) { return index < present ? AddressF32(ctx, access, first + index) : ZeroF32(ctx.state); }; + auto x = component(0u); if (components == 1u) { return x; } - auto y = AddressF32(ctx, access, first + 1u); - if (cube) { + auto y = component(1u); + if (cube && !gradient) { 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 = AddressF32(ctx, access, first + 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); } @@ -245,6 +242,24 @@ std::uint32_t LodU32(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) return AddressU32(ctx, access, component); } +bool VolumeSliceAddress(const ImageEmitAccess& access) { + return access.mem.imageDimension == RdnaImageDimension::Dim2DArray && access.image.dimension == RdnaImageDimension::Dim3D; +} + +std::uint32_t VolumeSliceInRange(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, std::uint32_t image, std::optional lod) { + auto& state = ctx.state; + state.module.EmitCapability(spv::CapabilityImageQuery); + const auto size = state.module.AllocateId(); + if (lod.has_value()) { + state.module.AddFunction(spv::OpImageQuerySizeLod, TypeU32Vector(state, 3), size, image, *lod); + } else { + state.module.AddFunction(spv::OpImageQuerySize, TypeU32Vector(state, 3), size, image); + } + const auto depth = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), depth, size, 2u); + return Binary(state, spv::OpULessThan, TypeBool(state), AddressU32(ctx, access, 2u), depth); +} + std::uint32_t DrefValueF32(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, const ImageSampleLayout& layout) { if (layout.dref == NoImageComponent || access.mem.imageAddressComponents <= layout.dref) { ctx.Fail(access.inst, "has no depth reference in the image address"); @@ -273,15 +288,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; @@ -331,7 +337,7 @@ std::uint32_t ResultVector(SpirvValueEmitContext& ctx, const ImageEmitAccess& ac std::uint32_t component[4] = {}; for (std::uint32_t index = 0; index < 4u; index++) { if (dref) { - component[index] = index == 0u ? F32BitsToU32(ctx, value) : ConstantU32(state, 0); + component[index] = F32BitsToU32(ctx, value); continue; } const auto scalar = state.module.AllocateId(); @@ -388,13 +394,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(); @@ -403,7 +415,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); @@ -417,7 +429,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); @@ -434,6 +446,9 @@ std::uint32_t QueryDimensions(SpirvValueEmitContext& ctx, const ImageEmitAccess& state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), result[index], size, index); } } + if (access.mem.imageDimension == RdnaImageDimension::Dim3D && dimension == RdnaImageDimension::Dim2D) { + result[2] = ConstantU32(state, 1); + } if (info.multisampled == 0u) { result[3] = state.module.AllocateId(); state.module.AddFunction(spv::OpImageQueryLevels, TypeU32(state), result[3], image); @@ -455,6 +470,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]; } @@ -467,6 +485,15 @@ std::uint32_t PackedOffset(SpirvValueEmitContext& ctx, const ImageEmitAccess& ac return result; } +std::uint32_t FoldedOffsetCoord(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, const SampleSetup& setup) { + auto& state = ctx.state; + const auto components = setup.dimensionInfo.spatialComponents; + const auto type = components == 1u ? TypeF32(state) : TypeF32Vector(state, components); + const auto sum = Binary(state, spv::OpFAdd, type, setup.coord, Unary(state, spv::OpConvertSToF, type, PackedOffset(ctx, access, setup.layout))); + state.module.AddAnnotation(spv::OpDecorate, sum, spv::DecorationNoContraction); + return sum; +} + std::uint32_t HorizontalOffsets(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { auto& state = ctx.state; const auto components = RdnaImageDimensionInfoFor(access.image.dimension).spatialComponents; @@ -551,7 +578,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); @@ -636,7 +663,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; @@ -657,7 +684,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; @@ -726,9 +753,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++) { @@ -751,7 +775,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(); @@ -844,7 +868,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) { @@ -856,7 +880,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); @@ -868,7 +892,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) { @@ -882,7 +906,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); @@ -909,7 +933,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)); @@ -938,7 +962,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; })); } @@ -950,9 +974,15 @@ void EmitReadOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { auto& state = ctx.state; const auto& dimensionInfo = RdnaImageDimensionInfoFor(access.image.dimension); const auto numericClass = access.image.numericClass; - const auto condition = ctx.Arg(access.inst, 2); + auto condition = ctx.Arg(access.inst, 2); + 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), 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) { @@ -967,7 +997,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); })); } @@ -978,25 +1008,88 @@ 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; - EmitIfCondition(state, ctx.Arg(access.inst, 3), [&]() { + auto condition = ctx.Arg(access.inst, 3); + if (VolumeSliceAddress(access)) { + if (access.image.mipMode == ImageMipMode::DynamicStorage) ctx.Fail(access.inst, "stores a 2D-array address to a 3D image with dynamic mips, which is not implemented"); + const auto image = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, ImageType(state, access.image), image, StorageImageDescriptorPointer(state, access.mem.resource)); + condition = Binary(state, spv::OpLogicalAnd, TypeBool(state), condition, VolumeSliceInRange(ctx, access, image, std::nullopt)); + } + EmitIfCondition(state, condition, [&]() { const auto mipLod = access.image.mipMode == ImageMipMode::DynamicStorage ? LodU32(ctx, access) : 0u; const auto coord = CoordU32(ctx, access); - const auto texel = access.mem.imagePacked ? PackedStoreTexel(ctx, access, ctx.Arg(access.inst, 2)) : StoreTexel(ctx, access, ctx.Arg(access.inst, 2), uintImage); + 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)) { @@ -1056,7 +1149,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) { @@ -1089,7 +1183,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) { @@ -1158,7 +1252,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) { @@ -1171,15 +1265,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) { @@ -1215,53 +1309,23 @@ 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; - const auto parameter = [&](std::uint32_t word) { return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::CompareBase + mem.resource * PipelineSpecialization::CompareWords + word, 0u); }; + const auto compare = state.registrationModules ? EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), RuntimeImageDword(state, mem.resource, offsetof(RuntimeAbi::ResourceMetadata, flags) / sizeof(std::uint32_t)), ConstantU32(state, RuntimeAbi::CompareStateShift)), RuntimeAbi::CompareStateMask) : 0u; + const auto parameter = [&](std::uint32_t word) { + if (!state.registrationModules) return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::CompareBase + mem.resource * PipelineSpecialization::CompareWords + word, 0u); + constexpr std::array, 6> fields{{{EmulatedCompare::FunctionShift, 7u}, {4u, 1u}, {EmulatedCompare::ClampXShift, 3u}, {EmulatedCompare::ClampYShift, 3u}, {EmulatedCompare::ReferenceShift, 3u}, {12u, 1u}}}; + 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); @@ -1278,7 +1342,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)); @@ -1290,11 +1354,18 @@ 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) { - return Select(state, i32, equal(mode, EmulatedCompare::AddressWrap), Binary(state, spv::OpSMod, i32, index, extent), ext(i32, GLSLstd450SClamp, {index, ConstantI32(state, 0), Binary(state, spv::OpISub, i32, extent, ConstantI32(state, 1))})); + const auto remainder = Binary(state, spv::OpSRem, i32, index, extent); + const auto negative = Binary(state, spv::OpSLessThan, TypeBool(state), remainder, ConstantI32(state, 0)); + const auto correction = Select(state, i32, negative, extent, ConstantI32(state, 0)); + const auto wrapped = Binary(state, spv::OpIAdd, i32, remainder, correction); + const auto clamped = ext(i32, GLSLstd450SClamp, {index, ConstantI32(state, 0), Binary(state, spv::OpISub, i32, extent, ConstantI32(state, 1))}); + return Select(state, i32, equal(mode, EmulatedCompare::AddressWrap), wrapped, clamped); }; const auto inside = [&](std::uint32_t index, std::uint32_t extent) { const auto notBelow = Binary(state, spv::OpSGreaterThanEqual, TypeBool(state), index, ConstantI32(state, 0)); @@ -1304,7 +1375,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)); @@ -1312,9 +1383,18 @@ 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, 0u); + auto red = extract(f32, texel, 3u); + for (std::uint32_t channel = 3u; channel-- > 0u;) red = Select(state, f32, equal(RuntimeImageSwizzle(state, mem.resource, channel), 4u), extract(f32, texel, channel), red); red = Select(state, f32, equal(modeX, EmulatedCompare::AddressBorder), Select(state, f32, inside(x, width), red, borderRed), red); red = Select(state, f32, equal(modeY, EmulatedCompare::AddressBorder), Select(state, f32, inside(y, height), red, borderRed), red); constexpr std::array operations{spv::OpFOrdLessThan, spv::OpFOrdEqual, spv::OpFOrdLessThanEqual, spv::OpFOrdGreaterThan, spv::OpFOrdNotEqual, spv::OpFOrdGreaterThanEqual}; @@ -1323,7 +1403,8 @@ void EmitEmulatedCompareSample(SpirvValueEmitContext& ctx, const ImageEmitAccess const auto compared = Select(state, f32, Binary(state, operations[index], TypeBool(state), reference, red), one, zero); result = Select(state, f32, equal(parameter(0u), index + 1u), compared, result); } - return result; + result = Select(state, f32, equal(parameter(4u), EmulatedCompare::ReferenceZero), zero, 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)); @@ -1337,25 +1418,58 @@ 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); }); - ctx.Define(access.inst, TableResult(ctx, access, ResultVector(ctx, access, result, setup.numericClass, true, false))); + 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)); + }; + std::uint32_t channels[4] = {}; + 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, ResultVector(ctx, access, value, IrTextureNumericClass::Float, false, false)); } void EmitSampleOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, const SampleSetup& setup) { @@ -1363,7 +1477,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); @@ -1383,8 +1501,8 @@ void EmitSampleOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con std::vector operands; if (HasFlag(mem, RdnaImageSampleFlagDerivative)) { operandMask |= spv::ImageOperandsGradMask; - operands.push_back(CoordF32(ctx, access, setup.layout.gradX, setup.dimensionInfo.spatialComponents, AddressDimension(access).spatialComponents)); - operands.push_back(CoordF32(ctx, access, setup.layout.gradY, setup.dimensionInfo.spatialComponents, AddressDimension(access).spatialComponents)); + operands.push_back(CoordF32(ctx, access, setup.layout.gradX, setup.dimensionInfo.spatialComponents, AddressDimension(access).spatialComponents, true)); + operands.push_back(CoordF32(ctx, access, setup.layout.gradY, setup.dimensionInfo.spatialComponents, AddressDimension(access).spatialComponents, true)); } else if (explicitLod) { operandMask |= spv::ImageOperandsLodMask; operands.push_back(HasFlag(mem, RdnaImageSampleFlagLod) ? AddressF32(ctx, access, setup.layout.lod) : ZeroF32(state)); @@ -1413,21 +1531,29 @@ void EmitSampleOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con const auto* value = access.address.Argument(argument)->Resolve(); return value->HasImmediate() ? value : nullptr; }; - if (setup.layout.offset != NoImageComponent && constantOffset() == nullptr) { - const bool gatherExtended = std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityImageGatherExtended)) != state.supportedCapabilities.end(); - if (!state.nonConstantImageOffsets || !gatherExtended) { - ctx.Fail(access.inst, "has a texel offset that is not a constant, which image sampling takes only with VK_KHR_maintenance8 and shaderImageGatherExtended"); - } + const bool hasOffset = setup.layout.offset != NoImageComponent; + const bool foldOffset = hasOffset && FoldsTexelOffsets(state.program.Info().samplers.at(mem.sampler)) && ResourceMaterializer::UnnormalizedSampleMismatch(image) == nullptr; + const auto foldedMask = operandMask; + const auto foldedOperands = operands; + const bool gatherExtended = std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityImageGatherExtended)) != state.supportedCapabilities.end(); + const bool offsetOperand = hasOffset && (constantOffset() != nullptr || (state.nonConstantImageOffsets && gatherExtended)); + if (hasOffset && !offsetOperand && !foldOffset) { + ctx.Fail(access.inst, "has a texel offset that is not a constant, which image sampling takes only with VK_KHR_maintenance8 and shaderImageGatherExtended"); + } + if (offsetOperand && constantOffset() == nullptr) { state.module.EmitCapability(spv::CapabilityImageGatherExtended); operandMask |= spv::ImageOperandsOffsetMask; operands.insert(operands.end() - ((operandMask & spv::ImageOperandsMinLodMask) != 0u ? 1 : 0), PackedOffset(ctx, access, setup.layout)); - } else if (setup.layout.offset != NoImageComponent) { + } else if (offsetOperand) { const auto bits = constantOffset()->ImmediateU32(); std::array values{}; for (std::uint32_t index = 0; index < setup.dimensionInfo.spatialComponents; index++) { 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]) @@ -1436,38 +1562,56 @@ void EmitSampleOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con operands.insert(operands.end() - ((operandMask & spv::ImageOperandsMinLodMask) != 0u ? 1 : 0), offset); } - const auto sampled = MakeSampledImage(state, mem.resource, mem.sampler, access.slot); - const auto sample = state.module.AllocateId(); - std::vector words = {opcode, resultType, sample, sampled, setup.coord}; - if (setup.dref) { - words.push_back(drefValue); + 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); + const auto sample = state.module.AllocateId(); + std::vector words = {sampleOpcode, resultType, sample, sampled, coord}; + if (setup.dref) { + words.push_back(drefValue); + } + if (mask != 0u) { + words.push_back(mask); + words.insert(words.end(), arguments.begin(), arguments.end()); + } + state.module.AddFunction(words); + return sample; + }; + std::uint32_t sample = 0; + const bool baseLevelIfUnnormalized = !setup.dref && SamplesBaseLevelIfUnnormalized(state.program.Info().samplers.at(mem.sampler)); + if (baseLevelIfUnnormalized) { + const auto selector = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::UnnormalizedBaseLevelBase + mem.sampler, 0u); + const auto unnormalized = Binary(state, spv::OpINotEqual, TypeBool(state), selector, ConstantU32(state, 0u)); + sample = EmitValueIfElse(state, unnormalized, resultType, [&] { + return emitSample(hasOffset ? FoldedOffsetCoord(ctx, access, setup) : setup.coord, spv::ImageOperandsLodMask, {ZeroF32(state)}, spv::OpImageSampleExplicitLod); + }, [&] { + return emitSample(setup.coord, operandMask, operands, opcode); + }); + } else if (!foldOffset) { + sample = emitSample(setup.coord, operandMask, operands, opcode); + } else if (!offsetOperand) { + sample = emitSample(FoldedOffsetCoord(ctx, access, setup), foldedMask, foldedOperands, opcode); + } else { + const auto selector = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::FoldOffsetBase + mem.sampler, 0u); + const auto folded = Binary(state, spv::OpINotEqual, TypeBool(state), selector, ConstantU32(state, 0u)); + const auto foldLabel = state.module.AllocateId(); + const auto operandLabel = state.module.AllocateId(); + const auto merge = state.module.AllocateId(); + state.module.AddFunction(spv::OpSelectionMerge, merge, spv::SelectionControlMaskNone); + state.module.AddFunction(spv::OpBranchConditional, folded, foldLabel, operandLabel); + EmitLabel(state, foldLabel); + const auto foldSample = emitSample(FoldedOffsetCoord(ctx, access, setup), foldedMask, foldedOperands, opcode); + const auto foldEnd = state.currentLabel; + state.module.AddFunction(spv::OpBranch, merge); + EmitLabel(state, operandLabel); + const auto operandSample = emitSample(setup.coord, operandMask, operands, opcode); + const auto operandEnd = state.currentLabel; + state.module.AddFunction(spv::OpBranch, merge); + EmitLabel(state, merge); + sample = state.module.AllocateId(); + state.module.AddFunction(spv::OpPhi, resultType, sample, foldSample, foldEnd, operandSample, operandEnd); } - if (operandMask != 0u) { - words.push_back(operandMask); - words.insert(words.end(), operands.begin(), operands.end()); - } - state.module.AddFunction(words); 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) { @@ -1507,6 +1651,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) { @@ -1526,7 +1869,7 @@ void EmitImageMode(SpirvValueEmitContext& ctx, const IrValue& inst, const ImageR if (address == nullptr) { ctx.Fail(inst, "has no image address"); } - const ImageEmitAccess access{inst, mem, image, *address, TableSlot(ctx, inst, mem, image)}; + const ImageEmitAccess access{inst, mem, image, *address}; if (imageInfo.access == ImageAccess::Atomic) { EmitAtomicOp(ctx, access); return; @@ -1557,84 +1900,103 @@ 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)); - ctx.Define(inst, Select(state, TypeU32Vector(state, 4u), ctx.Arg(inst, 2u), 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); } }; - 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.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ImageBase + memory.resource * PipelineSpecialization::ImageWords, 0u) : Binary(state, spv::OpShiftRightLogical, TypeU32(state), RuntimeImageDword(state, memory.resource, offsetof(RuntimeAbi::ResourceMetadata, flags) / sizeof(std::uint32_t)), ConstantU32(state, 1u)); + 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 = state.module.AllocateId(); + 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]}); } + std::vector incoming; + if (undefined != 0u) { + state.module.AddFunction(spv::OpUndef, TypeId(state, inst.Type()), undefined); + incoming.insert(incoming.end(), {undefined, state.currentLabel}); + } state.module.AddFunction(spv::OpSelectionMerge, merge, spv::SelectionControlMaskNone); state.module.AddFunction(words); - std::vector incoming; 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) { - 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(); - const auto inactiveLabel = state.module.AllocateId(); - modeMerge = state.module.AllocateId(); - state.module.AddFunction(spv::OpSelectionMerge, modeMerge, spv::SelectionControlMaskNone); - state.module.AddFunction(spv::OpBranchConditional, active, activeLabel, inactiveLabel); - EmitLabel(state, inactiveLabel); - state.module.AddFunction(spv::OpUnreachable); - EmitLabel(state, activeLabel); - } emitMode(modes[index]); - if (modeMerge != 0u) { - state.module.AddFunction(spv::OpBranch, modeMerge); - EmitLabel(state, modeMerge); - } 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; - EmitLabel(state, invalid); - state.module.AddFunction(spv::OpUnreachable); + 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); + } EmitLabel(state, merge); if (returnsValue) { 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 7416ff3832..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), base, 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)); @@ -65,21 +65,74 @@ std::uint32_t LdsDwordCount(const SpirvEmitterState& state) { return state.requirements.functionLdsDwords != 0u ? state.requirements.functionLdsDwords : FunctionLdsDwordLimit; } +bool LdsInDeviceMemory(const SpirvEmitterState& state) { + return state.program.Info().sharedMemoryBytes != 0u; +} + +std::uint32_t LdsMemorySemantics(const SpirvEmitterState& state) { + return spv::MemorySemanticsWorkgroupMemoryMask | (LdsInDeviceMemory(state) ? spv::MemorySemanticsUniformMemoryMask : 0u); +} + +void EmitLdsBufferBase(SpirvEmitterState& state) { + if (!LdsInDeviceMemory(state)) { + return; + } + if (state.ldsBufferVariable == 0u || state.numWorkgroupsVariable == 0u) { + FailEmit("device memory LDS was not declared before function emission"); + } + if (ShaderWorkgroupInput(state) == nullptr || state.program.Resources().stage != IrShaderStage::Compute || state.program.Info().sharedMemoryBytes != LdsDwordCount(state) * 4u) { + FailEmit("device memory LDS does not match the compute program's LDS size"); + } + const auto groups = [&](std::uint32_t component) { + const auto pointer = state.module.AllocateId(); + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassInput, TypeU32(state)), pointer, state.numWorkgroupsVariable, ConstantU32(state, component)); + state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer); + return value; + }; + const auto group = [&](std::uint32_t component) { return EmitInputComponentU32(state, StageInputKind::WorkgroupId, component); }; + const auto plane = EmitAddU32(state, EmitBinaryU32(state, spv::OpIMul, group(2u), groups(1u)), group(1u)); + const auto linear = EmitAddU32(state, EmitBinaryU32(state, spv::OpIMul, plane, groups(0u)), group(0u)); + state.ldsBufferBase = EmitBinaryU32(state, spv::OpIMul, linear, ConstantU32(state, WorkgroupMemoryStrideDwords(state.program.Info()))); +} + std::uint32_t EmitLdsLockPointer(SpirvEmitterState& state) { if (!state.requirements.ldsLock) { FailEmit("LDS lock was not requested by the program analysis"); } + if (LdsInDeviceMemory(state)) { + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypeStorageBufferElementPointer(state), pointer, state.ldsBufferVariable, ConstantU32(state, 0u), EmitAddU32(state, state.ldsBufferBase, ConstantU32(state, LdsDwordCount(state)))); + return pointer; + } EnsureLdsStorage(state); const auto pointer = state.module.AllocateId(); state.module.AddFunction(spv::OpAccessChain, TypeU32ElementPointer(state, spv::StorageClassWorkgroup), pointer, state.ldsVariable, ConstantU32(state, LdsDwordCount(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; switch (mem.kind) { case ResourceKind::Lds: + if (LdsInDeviceMemory(state)) { + if (state.ldsBufferVariable == 0u || state.ldsBufferBase == 0u) { + FailEmit("device memory LDS was not prepared at function entry"); + } + access.objectPointer = state.ldsBufferVariable; + access.length = ConstantU32(state, LdsDwordCount(state)); + return access; + } EnsureLdsStorage(state); access.objectPointer = state.ldsVariable; access.length = ConstantU32(state, LdsDwordCount(state)); @@ -124,10 +177,38 @@ std::uint32_t EmitMemoryElementInBounds(SpirvEmitterState& state, const MemoryRe return inBounds; } +bool InFunctionStorage(const SpirvEmitterState& state, ResourceKind kind) { + if (kind == ResourceKind::Scratch) return true; + return kind == ResourceKind::Lds && !LdsInDeviceMemory(state) && ShaderWorkgroupInput(state) == nullptr; +} + +std::uint32_t 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)); + } if (access.kind == ResourceKind::Lds || access.kind == ResourceKind::Scratch) { const auto pointer = state.module.AllocateId(); - const std::uint32_t storageClass = access.kind == ResourceKind::Scratch ? spv::StorageClassFunction : ShaderWorkgroupInput(state) != nullptr ? spv::StorageClassWorkgroup : spv::StorageClassFunction; + const std::uint32_t storageClass = InFunctionStorage(state, access.kind) ? spv::StorageClassFunction : spv::StorageClassWorkgroup; state.module.AddFunction(spv::OpAccessChain, TypeU32ElementPointer(state, storageClass), pointer, access.objectPointer, index); return pointer; } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp index b1a37bb579..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 @@ -50,45 +49,51 @@ std::uint32_t DescriptorElementPointer(SpirvEmitterState& state, std::uint32_t r } const auto pointer = state.module.AllocateId(); const bool heap = kind == DescriptorBindingKind::Samplers || ImageBindingResourceClass(kind) != ImageResourceClass::None; - const auto index = heap ? state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::DescriptorIndex(static_cast(kind), arrayIndex), arrayIndex) : ConstantU32(state, arrayIndex); + const auto index = heap && !state.registrationModules ? state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::DescriptorIndex(static_cast(kind), arrayIndex), arrayIndex) : ConstantU32(state, arrayIndex); state.module.AddFunction(spv::OpAccessChain, resultPtrType, pointer, variableId, index); return pointer; } -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"); - } - 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; +std::uint32_t RuntimeImageDword(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t member) { + 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); + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer); + return value; } -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 RuntimeMipCount(SpirvEmitterState& state, std::uint32_t resource) { + const auto word = RuntimeImageDword(state, resource, offsetof(RuntimeAbi::ResourceMetadata, descriptor) / sizeof(std::uint32_t) + 3u); + const auto first = EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), word, ConstantU32(state, 12u)), 0xfu); + const auto last = EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), word, ConstantU32(state, 16u)), 0xfu); + return Binary(state, spv::OpIAdd, TypeU32(state), Binary(state, spv::OpISub, TypeU32(state), last, first), ConstantU32(state, 1u)); +} + +void DecorateTableNonUniform(SpirvEmitterState& state, std::uint32_t id) { + if (!state.tableIndexNonUniform) return; + state.module.EmitCapability(spv::CapabilityShaderNonUniform); + state.module.EmitCapability(spv::CapabilitySampledImageArrayNonUniformIndexing); + if (std::any_of(state.tableImageVariables.begin(), state.tableImageVariables.end(), [](const SpirvTableImageVariable& variable) { return ImageBindingResourceClass(variable.kind) == ImageResourceClass::Storage; })) state.module.EmitCapability(spv::CapabilityStorageImageArrayNonUniformIndexing); + state.module.AddAnnotation(spv::OpDecorate, id, spv::DecorationNonUniform); +} + +std::uint32_t TableStorageImagePointer(SpirvEmitterState& state, DescriptorBindingKind kind, std::uint32_t resource, std::uint32_t pointerType) { + const auto found = std::find_if(state.tableImageVariables.begin(), state.tableImageVariables.end(), [&](const SpirvTableImageVariable& variable) { return variable.kind == kind; }); + if (found == state.tableImageVariables.end() || !state.tableAccess.active) ExitDescriptorBindingFailure(state, kind, resource, "image storage table array was not emitted for the mode"); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, pointerType, pointer, found->variable, state.tableAccess.element); + DecorateTableNonUniform(state, pointer); + return pointer; +} + +bool TableModeEmitted(const ImageResource& mode) { + return RdnaImageDimensionInfoFor(mode.dimension).multisampled == 0u; +} + +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) { @@ -148,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; } @@ -203,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"); } @@ -218,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"); @@ -240,6 +272,7 @@ void EmitStorageImageWrite(SpirvEmitterState& state, std::uint32_t resource, std if (image.mipCount == 0u) { ExitDescriptorBindingFailure(state, kind, resource, "dynamic storage image has no mip descriptors"); } + const auto count = state.registrationModules ? RuntimeMipCount(state, resource) : state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::MipCountBase + resource, image.mipCount); const auto mergeLabel = state.module.AllocateId(); std::vector labels(image.mipCount); std::vector words{spv::OpSwitch, mipLod, mergeLabel}; @@ -252,7 +285,6 @@ void EmitStorageImageWrite(SpirvEmitterState& state, std::uint32_t resource, std state.module.AddFunction(std::span(words)); for (std::uint32_t mip = 0; mip < image.mipCount; mip++) { EmitLabel(state, labels[mip]); - const auto count = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::MipCountBase + resource, image.mipCount); const auto active = Binary(state, spv::OpULessThan, TypeBool(state), ConstantU32(state, mip), count); const auto writeLabel = state.module.AllocateId(); const auto nextLabel = state.module.AllocateId(); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index efb0d8e3f7..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; @@ -82,6 +106,7 @@ std::uint32_t BuiltInForInput(StageInputKind kind) { std::uint32_t PerVertexType(SpirvEmitterState& state) { return state.module.DecoratedType(spv::OpTypeStruct, {{spv::OpMemberDecorate, {0u, spv::DecorationBuiltIn, spv::BuiltInPosition}}, + {spv::OpMemberDecorate, {0u, spv::DecorationInvariant}}, {spv::OpDecorate, {spv::DecorationBlock}}}, TypeF32Vector(state, 4u)); } @@ -119,7 +144,7 @@ std::uint32_t PushConstantArrayType(SpirvEmitterState& state) { std::uint32_t PushConstantBlockType(SpirvEmitterState& state) { return state.module.DecoratedType(spv::OpTypeStruct, - {{spv::OpMemberDecorate, {0u, spv::DecorationOffset, 0u}}, + {{spv::OpMemberDecorate, {0u, spv::DecorationOffset, state.program.Metadata().bindings.PushSlotDword() * 4u}}, {spv::OpDecorate, {spv::DecorationBlock}}}, PushConstantArrayType(state)); } @@ -156,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; @@ -171,7 +196,37 @@ 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; + })) { + state.inputs.push_back(SpirvInputBinding {{StageInputKind::WorkgroupId, 0u, 3u, "gl_WorkGroupID", false}}); + } + state.numWorkgroupsVariable = DefineInterfaceVariable(state, TypeU32Vector(state, 3u), spv::StorageClassInput, "gl_NumWorkGroups"); + state.module.AddAnnotation(spv::OpDecorate, state.numWorkgroupsVariable, spv::DecorationBuiltIn, spv::BuiltInNumWorkgroups); + } const bool pixelStage = state.program.Resources().stage == IrShaderStage::Pixel; + const bool emulated = pixelStage && state.program.Metadata().barycentricEmulation; + BarycentricEmulationLayout emulation; + if (emulated) { + const auto parameters = DescribeFragmentParameters(state.program, state.inputInfo); + 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)}, 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) { const auto location = PixelParameterLocation(state, input.location); @@ -192,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); @@ -213,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)); @@ -230,12 +289,20 @@ 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) { const auto flat = PixelParameterIsFlat(state, input.location); - if (input.perVertex) { + auto location = PixelParameterLocation(state, input.location); + if (input.perVertex && emulated) { + const auto relocated = std::find_if(emulation.perVertexLocations.begin(), emulation.perVertexLocations.end(), [&](const auto& entry) { return entry.first == location; }); + if (relocated == emulation.perVertexLocations.end()) { + 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, state.meshBarycentricPixel ? spv::DecorationPerPrimitiveEXT : spv::DecorationFlat); + } else if (input.perVertex) { state.module.AddAnnotation(spv::OpDecorate, input.variableId, spv::DecorationPerVertexKHR); } else if (flat) { state.module.AddAnnotation(spv::OpDecorate, input.variableId, spv::DecorationFlat); @@ -243,7 +310,13 @@ void DefineInputs(SpirvEmitterState& state) { if (!flat && !input.perVertex && PixelParameterIsLinear(state, input.location)) { state.module.AddAnnotation(spv::OpDecorate, input.variableId, spv::DecorationNoPerspective); } - state.module.AddAnnotation(spv::OpDecorate, input.variableId, spv::DecorationLocation, PixelParameterLocation(state, input.location)); + state.module.AddAnnotation(spv::OpDecorate, input.variableId, spv::DecorationLocation, location); + } else if (emulated && (input.kind == StageInputKind::BaryCoordSmooth || input.kind == StageInputKind::BaryCoordNoPerspective)) { + const bool linear = input.kind == StageInputKind::BaryCoordNoPerspective; + if (linear) { + state.module.AddAnnotation(spv::OpDecorate, input.variableId, spv::DecorationNoPerspective); + } + state.module.AddAnnotation(spv::OpDecorate, input.variableId, spv::DecorationLocation, linear ? emulation.linearLocation : emulation.smoothLocation); } else if (const auto builtin = BuiltInForInput(input.kind); builtin != NoBuiltIn) { state.module.AddAnnotation(spv::OpDecorate, input.variableId, spv::DecorationBuiltIn, builtin); } @@ -251,13 +324,29 @@ 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; + void DefineOutputs(SpirvEmitterState& state) { state.outputs.reserve(state.program.Info().outputs.size()); std::uint32_t clipDistanceCount = 0u; @@ -274,6 +363,20 @@ 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) && 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; + } const auto BuiltIn = [&](std::uint32_t& variable, std::uint32_t type, const char* name, std::uint32_t builtin) { if (variable == 0u) { variable = DefineInterfaceVariable(state, type, spv::StorageClassOutput, name); @@ -281,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: @@ -312,6 +416,20 @@ void DefineOutputs(SpirvEmitterState& state) { case StageOutputKind::SampleMask: binding.variableId = BuiltIn(state.sampleMaskVariable, SampleMaskArrayType(state), "gl_SampleMask", spv::BuiltInSampleMask); break; + case StageOutputKind::StencilRef: + 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: { const bool uintOutput = binding.kind == StageOutputKind::Mrt && state.program.Resources().stage == IrShaderStage::Pixel && binding.index < std::size(PixelInfo(state).targetOutputMode) && PixelInfo(state).targetOutputMode[binding.index] == 7u; @@ -322,12 +440,22 @@ void DefineOutputs(SpirvEmitterState& state) { } } } + if (state.zeroPositionClipIndex) { + BuiltIn(state.clipDistanceVariable, F32ArrayType(state, clipDistanceCount), "gl_ClipDistance", spv::BuiltInClipDistance); + } + if (state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->psAlphaToCoverage) { + 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) { + 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"); @@ -337,31 +465,34 @@ void DefineDescriptors(SpirvEmitterState& state) { const auto Define = [&](std::uint32_t type, const char* name, std::uint32_t storage = spv::StorageClassStorageBuffer) { const auto variable = state.module.DefineGlobalVariable(TypePointer(state, storage, type), storage); state.module.AddName(variable, name); - state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationDescriptorSet, 0u); + state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationDescriptorSet, state.fragmentDescriptorSet && stage == IrShaderStage::Pixel ? RuntimeAbi::FragmentDescriptorSet : RuntimeAbi::DescriptorSet); state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationBinding, NativeBinding(stage, binding.kind)); return variable; }; const auto ArrayType = [&](std::uint32_t type) { const bool heap = binding.kind == DescriptorBindingKind::Samplers || ImageBindingResourceClass(binding.kind) != ImageResourceClass::None; const auto count = static_cast(binding.resources.size()); - const auto length = heap ? state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::HeapCountBase + static_cast(binding.kind), count) : ConstantU32(state, count); + const auto length = heap && !state.registrationModules ? state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::HeapCountBase + static_cast(binding.kind), count) : ConstantU32(state, count); return state.module.Type(spv::OpTypeArray, type, length); }; switch (binding.kind) { case DescriptorBindingKind::Buffers: state.storageBufferVariable = Define(ArrayType(StorageBufferBlockType(state)), "buffers"); - if (state.requirements.bufferInt64Atomics) { - state.storageBufferU64Variable = Define(ArrayType(StorageBufferU64BlockType(state)), "buffers_u64"); - state.module.AddAnnotation(spv::OpDecorate, state.storageBufferVariable, spv::DecorationAliased); - state.module.AddAnnotation(spv::OpDecorate, state.storageBufferU64Variable, spv::DecorationAliased); + if (state.requirements.coherentBufferAlias) state.storageBufferCoherentVariable = Define(ArrayType(StorageBufferBlockType(state)), "buffers_coherent"); + 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); + } } if (state.requirements.coherentBuffers) { - state.module.AddAnnotation(spv::OpDecorate, state.storageBufferVariable, spv::DecorationCoherent); + state.module.AddAnnotation(spv::OpDecorate, state.storageBufferCoherentVariable != 0u ? state.storageBufferCoherentVariable : state.storageBufferVariable, spv::DecorationCoherent); if (state.storageBufferU64Variable != 0u) state.module.AddAnnotation(spv::OpDecorate, state.storageBufferU64Variable, spv::DecorationCoherent); } 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"); @@ -371,12 +502,40 @@ 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); break; case DescriptorBindingKind::Gds: 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) { @@ -395,6 +554,21 @@ void DefineDescriptors(SpirvEmitterState& state) { } } } + if (LdsInDeviceMemory(state)) { + state.ldsBufferVariable = state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassStorageBuffer, StorageBufferBlockType(state)), spv::StorageClassStorageBuffer); + state.module.AddName(state.ldsBufferVariable, "workgroup_memory"); + 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 2c3eb07f54..4c05ffe094 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp @@ -48,11 +48,27 @@ namespace ShaderRecompiler if (inputInfo.pixel == nullptr) FailEmit("pixel input info is missing"); const auto& pixel = *inputInfo.pixel; std::vector result; + std::array readsP10{}; + for (const auto& block : program.Blocks()) { + for (const auto* instruction : block->Instructions()) { + if (instruction->Opcode() != IrOpcode::GetInterpolationParameter || instruction->Argument(2)->Resolve()->ImmediateU32() != 0u) continue; + const auto attribute = instruction->Argument(0)->Resolve()->ImmediateU32(); + if (attribute < readsP10.size()) readsP10[attribute] = true; + } + } for (const auto& input : program.Info().inputs) { if (input.kind != StageInputKind::Parameter) continue; const auto location = PixelInputLocation(pixel, input.location); - if (std::any_of(result.begin(), result.end(), [&](const auto& parameter) { return parameter.location == location; })) continue; - result.push_back({location, location, PixelInputIsFlat(pixel, input.location), input.perVertex, PixelInputIsCustom(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 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->vertexOrder = existing->vertexOrder || vertexOrder; + continue; + } + result.push_back({location, location, flat, input.perVertex, PixelInputIsCustom(pixel, input.location), flatP10, vertexOrder}); } return result; } @@ -180,6 +196,13 @@ namespace ShaderRecompiler } std::uint32_t EmitLocalInvocationIndex(SpirvEmitterState& state) { + if (state.spannedWave) { + const auto subgroup = state.module.AllocateId(); + const auto invocation = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), subgroup, state.subgroupIdVariable); + state.module.AddFunction(spv::OpLoad, TypeU32(state), invocation, state.subgroupLocalInvocationIdVariable); + return EmitAddU32(state, EmitBinaryU32(state, spv::OpShiftLeftLogical, subgroup, ConstantU32(state, 5u)), invocation); + } const auto variable = InputVariableForKind(state, StageInputKind::LocalInvocationIndex); if (variable == 0) { FailEmit("LocalInvocationIndex was not declared"); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index a0ab538176..d997a174eb 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -3,6 +3,7 @@ #include "SpirvBackend/SpirvEmitterInstructions.hpp" #include "SpirvBackend/SpirvMemory/SpirvSubgroup.hpp" #include "SpirvBackend/SpirvBufferFormat.hpp" +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" #include #include #include @@ -12,6 +13,7 @@ #include #include #include +#include #include namespace ShaderRecompiler { @@ -133,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) { @@ -202,9 +203,110 @@ 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; + if (left->HasImmediate() || right->HasImmediate()) return left->HasImmediate() && right->HasImmediate() && left->ArgumentCount() == 0u && left->ImmediateU32() == right->ImmediateU32(); + if (left->Opcode() != IrOpcode::GetAddressResource && left->Opcode() != IrOpcode::SelectU32) return false; + for (std::size_t index = 0; index < left->ArgumentCount(); ++index) { + const IrValue* first = left->Argument(index); + const IrValue* second = right->Argument(index); + if (first == nullptr || second == nullptr || !SameAddressValue(first->Resolve(), second->Resolve(), depth + 1u)) return false; + } + return true; +} + +bool WritesMemory(IrOpcode opcode) { + const auto buffer = BufferAccessOf(opcode); + const auto shared = SharedAccessOf(opcode); + const auto address = AddressOpcodeInfoOf(opcode).access; + const auto image = ImageOpcodeInfoOf(opcode).access; + return buffer == BufferAccess::Write || buffer == BufferAccess::Atomic || (shared != SharedAccess::None && shared != SharedAccess::Read) || address == AddressAccess::Write || address == AddressAccess::Atomic || + image == ImageAccess::Write || image == ImageAccess::Atomic || opcode == IrOpcode::Barrier || opcode == IrOpcode::StoreVectorWindow; +} + +std::vector> AdjacentBdaLoads(const SpirvEmitterState& state, const IrValue& inst, const MemoryInfo& mem) { + std::vector> members; + const IrBlock* block = inst.Parent(); + 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; + std::uint32_t taken = 1u; + for (++next; next != instructions.end(); ++next) { + const IrValue* other = *next; + if (other->Opcode() != inst.Opcode()) { + if (WritesMemory(other->Opcode())) break; + continue; + } + const auto& otherMemory = 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) { + const IrValue* left = inst.Argument(index); + const IrValue* right = other->Argument(index); + if (left == nullptr || right == nullptr || !SameAddressValue(left->Resolve(), right->Resolve())) sameArguments = false; + } + const auto delta = otherMemory.offset - mem.offset; + if (!sameArguments || delta == 0u || delta > 12u || (delta & 3u) != 0u || (taken & (1u << (delta / 4u))) != 0u) continue; + taken |= 1u << (delta / 4u); + members.emplace_back(other, delta / 4u); + } + return members; +} + std::uint32_t LoadBda(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, std::uint32_t bits) { + auto& state = ctx.state; + if (bits == 32u) { + if (const auto grouped = state.bdaGroupedLoads.find(&inst); grouped != state.bdaGroupedLoads.end()) { + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), value, grouped->second.first, grouped->second.second); + return value; + } + const auto members = AdjacentBdaLoads(state, inst, mem); + if (!members.empty()) { + std::uint32_t used = 1u; + for (const auto& member : members) used |= 1u << member.second; + const auto vector = TypeU32Vector(state, 4); + const auto zero = ConstantU32(state, 0u); + const auto none = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, vector, none, zero, zero, zero, zero); + const auto all = EmitValueOrDefaultIfCondition(state, ActiveArgument(ctx, inst), vector, none, [&]() { + const auto values = 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; + }); + for (const auto& member : members) state.bdaGroupedLoads[member.first] = {all, member.second}; + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), value, all, 0u); + return value; + } + } return EmitValueOrZeroIfCondition(ctx.state, ActiveArgument(ctx, inst), [&]() { - return EmitBdaRead(ctx, inst, GuestAddress(ctx, inst, mem), bits); + return ReadBda(ctx, inst, mem, bits); }); } @@ -231,7 +333,7 @@ void StoreSubwordInBounds(SpirvValueEmitContext& ctx, const MemoryInfo& mem, con const auto merge = [&](std::uint32_t old) { return Binary(state, spv::OpBitwiseOr, TypeU32(state), Binary(state, spv::OpBitwiseAnd, TypeU32(state), old, Unary(state, spv::OpNot, TypeU32(state), mask)), value); }; - if (mem.kind == ResourceKind::Scratch) { + if (InFunctionStorage(state, mem.kind)) { const auto old = state.module.AllocateId(); if (resource.memoryAccess != 0u) { state.module.AddFunction(spv::OpLoad, TypeU32(state), old, pointer, resource.memoryAccess); @@ -288,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) { @@ -336,11 +437,175 @@ const MemoryInfo& SharedMemory(SpirvValueEmitContext& ctx, const IrValue& inst) return mem; } + +} + +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; + std::uint32_t priv = 0; +}; + +ApertureAddress SplitAperture(SpirvEmitterState& state, std::uint32_t address) { + const auto u32 = TypeU32(state); + const auto high = Unary(state, spv::OpUConvert, u32, Binary(state, spv::OpShiftRightLogical, TypeScalarU64(state), address, BdaConstant(state, 32u))); + return {Unary(state, spv::OpUConvert, u32, address), Binary(state, spv::OpIEqual, TypeBool(state), high, ConstantU32(state, SharedApertureHigh)), Binary(state, spv::OpIEqual, TypeBool(state), high, ConstantU32(state, PrivateApertureHigh))}; +} + +std::uint32_t ApertureByteInBounds(SpirvEmitterState& state, const MemoryResourceAccess& resource, std::uint32_t byteOffset, std::uint32_t offset, std::uint32_t index) { + const auto noWrap = Binary(state, spv::OpULessThanEqual, TypeBool(state), byteOffset, ConstantU32(state, 0xffffffffu - offset)); + return AndCondition(state, noWrap, EmitMemoryElementInBounds(state, resource, index)); +} + +std::uint32_t ApertureContained(SpirvEmitterState& state, std::uint32_t byteOffset, std::uint32_t bits) { + const auto inDword = Binary(state, spv::OpBitwiseAnd, TypeU32(state), byteOffset, ConstantU32(state, 3u)); + return Binary(state, spv::OpULessThanEqual, TypeBool(state), EmitAddU32(state, inDword, ConstantU32(state, bits / 8u)), ConstantU32(state, 4u)); +} + +std::uint32_t LoadApertureDword(SpirvValueEmitContext& ctx, ResourceKind kind, std::uint32_t byteOffset, std::uint32_t bits) { + auto& state = ctx.state; + if (kind == ResourceKind::Scratch && state.program.Info().scratchDwords == 0u) return ConstantU32(state, 0u); + MemoryInfo storage{}; + storage.kind = kind; + const auto resource = PrepareMemoryResourceAccess(state, storage); + const auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), byteOffset, ConstantU32(state, 2u)); + return EmitValueOrZeroIfCondition(state, EmitMemoryElementInBounds(state, resource, index), [&] { + return bits == 32u ? LoadWordInBounds(ctx, resource, index) : LoadSubwordInBounds(ctx, resource, byteOffset, index, bits, false); + }); +} + +std::uint32_t LoadApertureByte(SpirvValueEmitContext& ctx, ResourceKind kind, std::uint32_t byteOffset, std::uint32_t offset) { + auto& state = ctx.state; + if (kind == ResourceKind::Scratch && state.program.Info().scratchDwords == 0u) return ConstantU32(state, 0u); + MemoryInfo storage{}; + storage.kind = kind; + const auto resource = PrepareMemoryResourceAccess(state, storage); + const auto address = EmitAddU32(state, byteOffset, ConstantU32(state, offset)); + const auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), address, ConstantU32(state, 2u)); + return EmitValueOrZeroIfCondition(state, ApertureByteInBounds(state, resource, byteOffset, offset, index), [&] { + return LoadSubwordInBounds(ctx, resource, address, index, 8u, false); + }); +} + +std::uint32_t ApertureElementInBounds(SpirvValueEmitContext& ctx, ResourceKind kind, std::uint32_t byteOffset, std::uint32_t bits) { + auto& state = ctx.state; + MemoryInfo storage{}; + storage.kind = kind; + const auto resource = PrepareMemoryResourceAccess(state, storage); + const auto last = bits / 8u - 1u; + const auto address = EmitAddU32(state, byteOffset, ConstantU32(state, last)); + const auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), address, ConstantU32(state, 2u)); + return ApertureByteInBounds(state, resource, byteOffset, last, index); +} + +std::uint32_t LoadApertureElement(SpirvValueEmitContext& ctx, ResourceKind kind, std::uint32_t byteOffset, std::uint32_t bits) { + auto& state = ctx.state; + if (bits == 8u) return LoadApertureByte(ctx, kind, byteOffset, 0u); + if (kind == ResourceKind::Scratch && state.program.Info().scratchDwords == 0u) return ConstantU32(state, 0u); + return EmitValueOrZeroIfCondition(state, ApertureElementInBounds(ctx, kind, byteOffset, bits), [&] { + return EmitValueIfElse(state, ApertureContained(state, byteOffset, bits), TypeU32(state), [&] { + return LoadApertureDword(ctx, kind, byteOffset, bits); + }, [&] { + std::uint32_t value = ConstantU32(state, 0u); + for (std::uint32_t byte = 0; byte < bits / 8u; ++byte) { + const auto part = Binary(state, spv::OpShiftLeftLogical, TypeU32(state), LoadApertureByte(ctx, kind, byteOffset, byte), ConstantU32(state, byte * 8u)); + value = Binary(state, spv::OpBitwiseOr, TypeU32(state), value, part); + } + return value; + }); + }); +} + +void StoreApertureByte(SpirvValueEmitContext& ctx, ResourceKind kind, std::uint32_t byteOffset, std::uint32_t offset, std::uint32_t data) { + auto& state = ctx.state; + if (kind == ResourceKind::Scratch && state.program.Info().scratchDwords == 0u) return; + MemoryInfo storage{}; + storage.kind = kind; + const auto resource = PrepareMemoryResourceAccess(state, storage); + const auto address = EmitAddU32(state, byteOffset, ConstantU32(state, offset)); + const auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), address, ConstantU32(state, 2u)); + EmitIfCondition(state, ApertureByteInBounds(state, resource, byteOffset, offset, index), [&] { StoreSubwordInBounds(ctx, storage, resource, address, index, 8u, data); }); +} + +void StoreApertureDword(SpirvValueEmitContext& ctx, ResourceKind kind, std::uint32_t byteOffset, std::uint32_t bits, std::uint32_t data) { + auto& state = ctx.state; + if (kind == ResourceKind::Scratch && state.program.Info().scratchDwords == 0u) return; + MemoryInfo storage{}; + storage.kind = kind; + const auto resource = PrepareMemoryResourceAccess(state, storage); + const auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), byteOffset, ConstantU32(state, 2u)); + EmitIfCondition(state, EmitMemoryElementInBounds(state, resource, index), [&] { + if (bits == 32u) { + StoreWordInBounds(ctx, resource, index, data); + } else { + StoreSubwordInBounds(ctx, storage, resource, byteOffset, index, bits, data); + } + }); +} + +void StoreApertureElement(SpirvValueEmitContext& ctx, ResourceKind kind, std::uint32_t byteOffset, std::uint32_t bits, std::uint32_t data) { + auto& state = ctx.state; + if (bits == 8u) { + StoreApertureByte(ctx, kind, byteOffset, 0u, data); + return; + } + if (kind == ResourceKind::Scratch && state.program.Info().scratchDwords == 0u) return; + EmitIfCondition(state, ApertureElementInBounds(ctx, kind, byteOffset, bits), [&] { + const auto contained = ApertureContained(state, byteOffset, bits); + EmitIfCondition(state, contained, [&] { StoreApertureDword(ctx, kind, byteOffset, bits, data); }); + EmitIfCondition(state, Unary(state, spv::OpLogicalNot, TypeBool(state), contained), [&] { + for (std::uint32_t byte = 0; byte < bits / 8u; ++byte) { + StoreApertureByte(ctx, kind, byteOffset, byte, Binary(state, spv::OpShiftRightLogical, TypeU32(state), data, ConstantU32(state, byte * 8u))); + } + }); + }); +} + +template +std::uint32_t RouteFlatAccess(SpirvEmitterState& state, std::uint32_t address, std::uint32_t type, TAperture&& aperture, TGlobal&& global) { + const auto split = SplitAperture(state, address); + return EmitValueIfElse(state, split.shared, type, [&] { return aperture(ResourceKind::Lds, split.low); }, [&] { + return EmitValueIfElse(state, split.priv, type, [&] { return aperture(ResourceKind::Scratch, split.low); }, global); + }); +} + void LoadAddress(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t bits) { const auto& mem = ctx.Memory(inst); if (bits == 32u && mem.planningOnly) { return; } + if (RoutesApertures(ctx.state, mem)) { + ctx.Define(inst, EmitValueOrZeroIfCondition(ctx.state, ActiveArgument(ctx, inst), [&] { + const auto address = GuestAddress(ctx, inst, mem); + return RouteFlatAccess(ctx.state, address, TypeU32(ctx.state), [&](ResourceKind kind, std::uint32_t low) { return LoadApertureElement(ctx, kind, low, bits); }, [&] { return EmitBdaRead(ctx, inst, address, bits); }); + })); + return; + } switch (mem.kind) { case ResourceKind::Scratch: ctx.Define(inst, bits == 32u ? LoadWord(ctx, inst, mem) : LoadSubword(ctx, inst, mem, bits)); @@ -358,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)); }); } @@ -367,6 +632,22 @@ void LoadAddressWide(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint3 if (mem.planningOnly) { return; } + if (RoutesApertures(ctx.state, mem)) { + auto& state = ctx.state; + const auto type = TypeU32Composite(state, components); + ctx.Define(inst, EmitValueOrDefaultIfCondition(state, ActiveArgument(ctx, inst), type, ConstantU32CompositeZero(state, components), [&] { + const auto base = GuestAddressBase(ctx, inst, mem); + const auto address = AddBdaImmediate(ctx, inst, base, static_cast(mem.offset)); + return RouteFlatAccess(state, address, type, [&](ResourceKind kind, std::uint32_t low) { + std::array values{}; + for (std::uint32_t component = 0; component < components; component++) { + values[component] = LoadApertureElement(ctx, kind, Binary(state, spv::OpIAdd, TypeU32(state), low, ConstantU32(state, component * 4u)), 32u); + } + return ConstructU32Composite(state, components, values); + }, [&] { return ConstructU32Composite(state, components, EmitBdaDwordReads(ctx, inst, base, mem.offset, components)); }); + })); + return; + } switch (mem.kind) { case ResourceKind::Flat: case ResourceKind::Global: @@ -390,7 +671,18 @@ void StoreAddress(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t case ResourceKind::Flat: case ResourceKind::Global: EmitIfCondition(ctx.state, ActiveArgument(ctx, inst), [&]() { - EmitBdaStore(ctx, inst, GuestAddress(ctx, inst, mem), ctx.Arg(inst, inst.ArgumentCount() - 2u), bits); + auto& state = ctx.state; + const auto address = GuestAddress(ctx, inst, mem); + const auto data = ctx.Arg(inst, inst.ArgumentCount() - 2u); + if (!RoutesApertures(state, mem)) { + EmitBdaStore(ctx, inst, address, data, bits); + return; + } + const auto split = SplitAperture(state, address); + EmitIfCondition(state, split.shared, [&] { StoreApertureElement(ctx, ResourceKind::Lds, split.low, bits, data); }); + EmitIfCondition(state, split.priv, [&] { StoreApertureElement(ctx, ResourceKind::Scratch, split.low, bits, data); }); + const auto global = Unary(state, spv::OpLogicalNot, TypeBool(state), Binary(state, spv::OpLogicalOr, TypeBool(state), split.shared, split.priv)); + EmitIfCondition(state, global, [&] { EmitBdaStore(ctx, inst, address, data, bits); }); }); return; default: @@ -472,6 +764,61 @@ std::uint32_t EmitAtomicOperation(SpirvValueEmitContext& ctx, const IrValue& ins return old; } +std::uint32_t AtomicNext32(SpirvEmitterState& state, std::uint32_t opcode, std::uint32_t old, std::uint32_t value, std::uint32_t comparator) { + const auto u32 = TypeU32(state); + const auto winner = [&](spv::Op compare) { + return Select(state, u32, Binary(state, compare, TypeBool(state), old, value), old, value); + }; + switch (opcode) { + case spv::OpAtomicExchange: + return value; + case spv::OpAtomicCompareExchange: + return Select(state, u32, Binary(state, spv::OpIEqual, TypeBool(state), old, comparator), value, old); + case spv::OpAtomicIAdd: + return Binary(state, spv::OpIAdd, u32, old, value); + case spv::OpAtomicISub: + return Binary(state, spv::OpISub, u32, old, value); + case spv::OpAtomicSMin: + return winner(spv::OpSLessThan); + case spv::OpAtomicUMin: + return winner(spv::OpULessThan); + case spv::OpAtomicSMax: + return winner(spv::OpSGreaterThan); + case spv::OpAtomicUMax: + return winner(spv::OpUGreaterThan); + case spv::OpAtomicAnd: + return Binary(state, spv::OpBitwiseAnd, u32, old, value); + case spv::OpAtomicOr: + return Binary(state, spv::OpBitwiseOr, u32, old, value); + case spv::OpAtomicXor: + return Binary(state, spv::OpBitwiseXor, u32, old, value); + default: + throw std::runtime_error("AtomicNext32: opcode has no 32-bit replacement"); + } +} + +template +std::uint32_t UpdateFunctionElement(SpirvEmitterState& state, std::uint32_t pointer, TReplacement&& replacement) { + const auto old = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), old, pointer); + state.module.AddFunction(spv::OpStore, pointer, replacement(old)); + return old; +} + +template +std::uint32_t LockedLdsUpdate(SpirvEmitterState& state, std::uint32_t type, std::uint32_t zero, TUpdate&& update); + +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(); + return LockedLdsUpdate(state, TypeU32(state), ConstantU32(state, 0u), operation); +} + template std::uint32_t EmitAtomicAccess(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, TOperation&& operation) { if (mem.kind == ResourceKind::Buffer) return EmitRuntimeBufferAtomic(ctx, inst, 32u, ActiveArgument(ctx, inst), operation); @@ -479,7 +826,7 @@ std::uint32_t EmitAtomicAccess(SpirvValueEmitContext& ctx, const IrValue& inst, return EmitValueOrZeroIfCondition(state, ActiveArgument(ctx, inst), [&]() { const auto access = PrepareMemoryElement(ctx, mem, DwordIndex(ctx, inst, mem)); return EmitValueOrZeroIfCondition(state, EmitMemoryElementInBounds(state, access.resource, access.index), [&]() { - return operation(EmitMemoryElementPointer(state, access.resource, access.index)); + return EmitLdsAtomic32(state, mem, [&]() { return operation(EmitMemoryElementPointer(state, access.resource, access.index)); }); }); }); } @@ -488,9 +835,11 @@ template std::uint32_t EmitAtomicUpdate(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, TReplacement&& replacement) { const auto value = ctx.Arg(inst, inst.ArgumentCount() - 2u); return EmitAtomicAccess(ctx, inst, mem, [&](std::uint32_t pointer) { - return AtomicUpdate(ctx.state, pointer, mem.kind, [&](std::uint32_t old) { + const auto next = [&](std::uint32_t old) { return replacement(ctx.state, old, value); - }); + }; + if (InFunctionStorage(ctx.state, mem.kind)) return UpdateFunctionElement(ctx.state, pointer, next); + return AtomicUpdate(ctx.state, pointer, mem.kind, next); }); } @@ -559,11 +908,15 @@ std::uint32_t Atomic32(SpirvValueEmitContext& ctx, const IrValue& inst, const Me } if (mem.kind == ResourceKind::Buffer) NoteBufferAtomicSite(zeroSkip, zeroIdentity && !zeroSkip); const auto apply = [&](std::uint32_t pointer) { + if (InFunctionStorage(state, mem.kind)) { + const auto value = ctx.Arg(inst, inst.ArgumentCount() - 2u); + return UpdateFunctionElement(state, pointer, [&](std::uint32_t old) { return AtomicNext32(state, SpirvAtomicOpcode(inst.Opcode()), old, value, 0u); }); + } const auto operation = [&]() { const auto old = EmitAtomicOperation(ctx, inst, pointer, scope); if (lds) { - const std::uint32_t semantics = spv::MemorySemanticsAcquireReleaseMask | spv::MemorySemanticsWorkgroupMemoryMask; - state.module.AddFunction(spv::OpMemoryBarrier, ConstantU32(state, scope), ConstantU32(state, semantics)); + const std::uint32_t semantics = spv::MemorySemanticsAcquireReleaseMask | LdsMemorySemantics(state); + if (!state.insideLdsLock) state.module.AddFunction(spv::OpMemoryBarrier, ConstantU32(state, scope), ConstantU32(state, semantics)); } else { EmitDeviceAtomicMemoryBarrier(state); } @@ -580,7 +933,7 @@ std::uint32_t Atomic32(SpirvValueEmitContext& ctx, const IrValue& inst, const Me if (mem.kind == ResourceKind::Buffer) return EmitRuntimeBufferAtomic(ctx, inst, 32u, active, apply); return EmitValueOrZeroIfCondition(state, active, [&] { const auto access = PrepareMemoryElement(ctx, mem, DwordIndex(ctx, inst, mem)); - return EmitValueOrZeroIfCondition(state, EmitMemoryElementInBounds(state, access.resource, access.index), [&] { return apply(EmitMemoryElementPointer(state, access.resource, access.index)); }); + return EmitValueOrZeroIfCondition(state, EmitMemoryElementInBounds(state, access.resource, access.index), [&] { return EmitLdsAtomic32(state, mem, [&] { return apply(EmitMemoryElementPointer(state, access.resource, access.index)); }); }); }); } @@ -611,11 +964,15 @@ struct AtomicFloatBits { std::uint64_t sign() const { return wide ? 0x8000000000000000ull : 0x80000000ull; } std::uint64_t quiet() const { return wide ? 0x0008000000000000ull : 0x00400000ull; } std::uint64_t infinity() const { return wide ? 0x7ff0000000000000ull : 0x7f800000ull; } + std::uint64_t minNormal() const { return wide ? 0x0010000000000000ull : 0x00800000ull; } std::uint32_t op(spv::Op opcode, std::uint32_t lhs, std::uint32_t rhs) const { return Binary(state, opcode, type(), lhs, rhs); } std::uint32_t test(spv::Op opcode, std::uint32_t lhs, std::uint32_t rhs) const { return Binary(state, opcode, TypeBool(state), lhs, rhs); } std::uint32_t select(std::uint32_t condition, std::uint32_t lhs, std::uint32_t rhs) const { return Select(state, type(), condition, lhs, rhs); } std::uint32_t magnitude(std::uint32_t value) const { return op(spv::OpBitwiseAnd, value, constant(sign() - 1u)); } std::uint32_t nan(std::uint32_t value) const { return test(spv::OpUGreaterThan, magnitude(value), constant(infinity())); } + std::uint32_t flush(std::uint32_t value, bool enabled) const { + return enabled ? select(test(spv::OpULessThan, magnitude(value), constant(minNormal())), op(spv::OpBitwiseAnd, value, constant(sign())), value) : value; + } std::uint32_t signalingNan(std::uint32_t value) const { return Binary(state, spv::OpLogicalAnd, TypeBool(state), nan(value), test(spv::OpIEqual, op(spv::OpBitwiseAnd, value, constant(quiet())), constant(0u))); } @@ -674,28 +1031,21 @@ std::uint32_t SharedAtomicUpdate(SpirvValueEmitContext& ctx, const IrValue& inst auto& state = ctx.state; const auto& mem = SharedMemory(ctx, inst); return EmitAtomicAccess(ctx, inst, mem, [&](std::uint32_t pointer) { - const auto old = AtomicUpdate(state, pointer, mem.kind, [&](std::uint32_t current) { return replacement(state, current); }); - const std::uint32_t semantics = spv::MemorySemanticsAcquireReleaseMask | spv::MemorySemanticsWorkgroupMemoryMask; - state.module.AddFunction(spv::OpMemoryBarrier, ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, semantics)); + const auto next = [&](std::uint32_t current) { return replacement(state, current); }; + if (InFunctionStorage(state, mem.kind)) return UpdateFunctionElement(state, pointer, next); + const auto old = AtomicUpdate(state, pointer, mem.kind, next); + const std::uint32_t semantics = spv::MemorySemanticsAcquireReleaseMask | LdsMemorySemantics(state); + if (!state.insideLdsLock) state.module.AddFunction(spv::OpMemoryBarrier, ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, semantics)); return old; }); } std::uint32_t SharedFloatMinMax(SpirvValueEmitContext& ctx, const IrValue& inst, bool maxValue) { const auto data = ctx.Arg(inst, 1); + const bool flush = SharedMemory(ctx, inst).flushDenormals; return SharedAtomicUpdate(ctx, inst, [&](SpirvEmitterState& state, std::uint32_t old) { - const auto oldFloat = Unary(state, spv::OpBitcast, TypeF32(state), old); - const auto dataFloat = Unary(state, spv::OpBitcast, TypeF32(state), data); - const auto oldNan = Unary(state, spv::OpIsNan, TypeBool(state), oldFloat); - const auto dataNan = Unary(state, spv::OpIsNan, TypeBool(state), dataFloat); - const auto better = Binary(state, maxValue ? spv::OpFOrdGreaterThan : spv::OpFOrdLessThan, TypeBool(state), dataFloat, oldFloat); - const auto magnitudes = Binary(state, spv::OpBitwiseAnd, TypeU32(state), Binary(state, spv::OpBitwiseOr, TypeU32(state), old, data), ConstantU32(state, 0x7fffffffu)); - const auto zeros = Binary(state, spv::OpIEqual, TypeBool(state), magnitudes, ConstantU32(state, 0u)); - const auto zero = Binary(state, maxValue ? spv::OpBitwiseAnd : spv::OpBitwiseOr, TypeU32(state), old, data); - auto result = Select(state, TypeU32(state), better, data, old); - result = Select(state, TypeU32(state), zeros, zero, result); - result = Select(state, TypeU32(state), dataNan, old, result); - return Select(state, TypeU32(state), oldNan, data, result); + const AtomicFloatBits bits{state, false}; + return bits.minMax(bits.flush(old, flush), bits.flush(data, flush), maxValue); }); } @@ -739,6 +1089,148 @@ std::uint32_t AddressAtomicOpcode(IrOpcode opcode) { } } +std::uint32_t FlatAtomicNext32(SpirvEmitterState& state, IrOpcode opcode, std::uint32_t old, std::uint32_t value, std::uint32_t comparator) { + switch (opcode) { + case IrOpcode::AddressAtomicSwap32: + case IrOpcode::AddressAtomicCmpSwap32: + case IrOpcode::AddressAtomicIAdd32: + case IrOpcode::AddressAtomicISub32: + case IrOpcode::AddressAtomicSMin32: + case IrOpcode::AddressAtomicUMin32: + case IrOpcode::AddressAtomicSMax32: + case IrOpcode::AddressAtomicUMax32: + case IrOpcode::AddressAtomicAnd32: + case IrOpcode::AddressAtomicOr32: + case IrOpcode::AddressAtomicXor32: + return AtomicNext32(state, AddressAtomicOpcode(opcode), old, value, comparator); + case IrOpcode::AddressAtomicInc32: + return AtomicIncrement(state, old, value); + case IrOpcode::AddressAtomicDec32: + return AtomicDecrement(state, old, value); + case IrOpcode::AddressAtomicUSubSat32: + return AtomicUSubSat(state, old, value); + case IrOpcode::AddressAtomicFMin32: + return AtomicFloatBits{state, false}.minMax(old, value, false); + case IrOpcode::AddressAtomicFMax32: + return AtomicFloatBits{state, false}.minMax(old, value, true); + case IrOpcode::AddressAtomicFCmpSwap32: + return AtomicFloatBits{state, false}.compareSwap(old, comparator, value); + default: + throw std::runtime_error("FlatAtomicNext32: opcode has no aperture lowering"); + } +} + +std::uint32_t ApertureAtomic32(SpirvValueEmitContext& ctx, const IrValue& inst, ResourceKind kind, std::uint32_t address, std::uint32_t byteOffset, std::uint32_t value, std::uint32_t comparator) { + auto& state = ctx.state; + const auto instruction = ConstantU32(state, inst.Flags().pc); + const auto unaligned = Binary(state, spv::OpINotEqual, TypeBool(state), Binary(state, spv::OpBitwiseAnd, TypeU32(state), byteOffset, ConstantU32(state, 3u)), ConstantU32(state, 0u)); + EmitIfCondition(state, unaligned, [&] { RecordBdaFault(state, address, ConstantU32(state, 4u), instruction, BdaAbi::FaultReason::Unaligned); }); + if (kind == ResourceKind::Scratch && state.program.Info().scratchDwords == 0u) return ConstantU32(state, 0u); + MemoryInfo storage{}; + storage.kind = kind; + const auto resource = PrepareMemoryResourceAccess(state, storage); + const auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), byteOffset, ConstantU32(state, 2u)); + const auto aligned = Unary(state, spv::OpLogicalNot, TypeBool(state), unaligned); + const auto opcode = inst.Opcode(); + return EmitValueOrZeroIfCondition(state, aligned, [&] { + return EmitValueOrZeroIfCondition(state, EmitMemoryElementInBounds(state, resource, index), [&] { + const auto pointer = EmitMemoryElementPointer(state, resource, index); + if (kind == ResourceKind::Lds) { + const auto update = [&]() { return AtomicUpdate(state, pointer, kind, [&](std::uint32_t current) { return FlatAtomicNext32(state, opcode, current, value, comparator); }); }; + return state.requirements.ldsLock ? LockedLdsUpdate(state, TypeU32(state), ConstantU32(state, 0u), update) : update(); + } + const auto old = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), old, pointer); + state.module.AddFunction(spv::OpStore, pointer, FlatAtomicNext32(state, opcode, old, value, comparator)); + return old; + }); + }); +} + +std::uint32_t FlatAtomicNext64(SpirvEmitterState& state, IrOpcode opcode, std::uint32_t old, std::uint32_t value, std::uint32_t comparator) { + const auto u64 = TypeScalarU64(state); + const AtomicFloatBits bits{state, true}; + const auto winner = [&](spv::Op compare) { + return Select(state, u64, Binary(state, compare, TypeBool(state), old, value), old, value); + }; + switch (opcode) { + case IrOpcode::AddressAtomicSwap64: + return value; + case IrOpcode::AddressAtomicCmpSwap64: + return Select(state, u64, Binary(state, spv::OpIEqual, TypeBool(state), old, comparator), value, old); + case IrOpcode::AddressAtomicIAdd64: + return Binary(state, spv::OpIAdd, u64, old, value); + case IrOpcode::AddressAtomicISub64: + return Binary(state, spv::OpISub, u64, old, value); + case IrOpcode::AddressAtomicSMin64: + return winner(spv::OpSLessThan); + case IrOpcode::AddressAtomicUMin64: + return winner(spv::OpULessThan); + case IrOpcode::AddressAtomicSMax64: + return winner(spv::OpSGreaterThan); + case IrOpcode::AddressAtomicUMax64: + return winner(spv::OpUGreaterThan); + case IrOpcode::AddressAtomicAnd64: + return Binary(state, spv::OpBitwiseAnd, u64, old, value); + case IrOpcode::AddressAtomicOr64: + return Binary(state, spv::OpBitwiseOr, u64, old, value); + case IrOpcode::AddressAtomicXor64: + return Binary(state, spv::OpBitwiseXor, u64, old, value); + case IrOpcode::AddressAtomicInc64: + return bits.increment(old, value); + case IrOpcode::AddressAtomicDec64: + return bits.decrement(old, value); + case IrOpcode::AddressAtomicFMin64: + return bits.minMax(old, value, false); + case IrOpcode::AddressAtomicFMax64: + return bits.minMax(old, value, true); + case IrOpcode::AddressAtomicFCmpSwap64: + return bits.compareSwap(old, comparator, value); + default: + throw std::runtime_error("FlatAtomicNext64: opcode has no aperture lowering"); + } +} + +std::uint32_t ApertureAtomic64(SpirvValueEmitContext& ctx, const IrValue& inst, ResourceKind kind, std::uint32_t address, std::uint32_t byteOffset, std::uint32_t value, std::uint32_t comparator) { + auto& state = ctx.state; + const auto instruction = ConstantU32(state, inst.Flags().pc); + 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 && !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); + const auto low = Binary(state, spv::OpShiftRightLogical, TypeU32(state), byteOffset, ConstantU32(state, 2u)); + const auto high = EmitAddU32(state, low, ConstantU32(state, 1u)); + const auto aligned = Unary(state, spv::OpLogicalNot, TypeBool(state), unaligned); + 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); + const auto lowValue = state.module.AllocateId(); + const auto highValue = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), lowValue, lowPointer); + state.module.AddFunction(spv::OpLoad, TypeU32(state), highValue, highPointer); + const auto old = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, TypeU64(state), old, lowValue, highValue); + const auto next = Unary(state, spv::OpBitcast, TypeU64(state), FlatAtomicNext64(state, opcode, Unary(state, spv::OpBitcast, TypeScalarU64(state), old), value, comparator)); + const auto nextLow = state.module.AllocateId(); + const auto nextHigh = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), nextLow, next, 0u); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), nextHigh, next, 1u); + state.module.AddFunction(spv::OpStore, lowPointer, nextLow); + state.module.AddFunction(spv::OpStore, highPointer, nextHigh); + return old; + }; + return kind == ResourceKind::Lds ? LockedLdsUpdate(state, TypeU64(state), ConstantU64(state, 0u), update) : update(); + }); + }); +} + std::uint32_t AddressAtomic(SpirvValueEmitContext& ctx, const IrValue& inst) { auto& state = ctx.state; const auto& mem = ctx.Memory(inst); @@ -752,7 +1244,7 @@ std::uint32_t AddressAtomic(SpirvValueEmitContext& ctx, const IrValue& inst) { const auto scalarType = wide ? TypeScalarU64(state) : TypeU32(state); const auto scalar = [&](std::uint32_t value) { return wide ? Unary(state, spv::OpBitcast, scalarType, value) : value; }; const auto value = scalar(ctx.Arg(inst, 3)); - const auto old = EmitBdaAtomic(ctx, inst, GuestAddress(ctx, inst, mem), wide ? 8u : 4u, [&](std::uint32_t pointer) { + const auto operation = [&](std::uint32_t pointer) { if (inst.Opcode() == IrOpcode::AddressAtomicInc32 || inst.Opcode() == IrOpcode::AddressAtomicDec32) { const bool increment = inst.Opcode() == IrOpcode::AddressAtomicInc32; return AtomicUpdate(state, pointer, mem.kind, [&](std::uint32_t current) { @@ -794,12 +1286,22 @@ std::uint32_t AddressAtomic(SpirvValueEmitContext& ctx, const IrValue& inst) { } EmitDeviceAtomicMemoryBarrier(state); return result; - }); + }; + if (RoutesApertures(state, mem)) { + const auto address = GuestAddress(ctx, inst, mem); + const bool hasComparator = inst.Opcode() == IrOpcode::AddressAtomicCmpSwap32 || inst.Opcode() == IrOpcode::AddressAtomicFCmpSwap32 || inst.Opcode() == IrOpcode::AddressAtomicCmpSwap64 || inst.Opcode() == IrOpcode::AddressAtomicFCmpSwap64; + const auto comparator = hasComparator ? scalar(ctx.Arg(inst, 4)) : 0u; + if (wide) { + return RouteFlatAccess(state, address, type, [&](ResourceKind kind, std::uint32_t low) { return ApertureAtomic64(ctx, inst, kind, address, low, value, comparator); }, [&] { return Unary(state, spv::OpBitcast, type, EmitBdaAtomic(ctx, inst, address, 8u, operation)); }); + } + return RouteFlatAccess(state, address, TypeU32(state), [&](ResourceKind kind, std::uint32_t low) { return ApertureAtomic32(ctx, inst, kind, address, low, value, comparator); }, [&] { return EmitBdaAtomic(ctx, inst, address, 4u, operation); }); + } + const auto old = EmitBdaAtomic(ctx, inst, GuestAddress(ctx, inst, mem), wide ? 8u : 4u, operation); return wide ? Unary(state, spv::OpBitcast, type, old) : old; }); } -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) { @@ -810,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)); @@ -831,31 +1334,34 @@ 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 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(); - state.module.AddFunction(spv::OpGroupNonUniformShuffle, TypeU32(state), result, ConstantU32(state, spv::ScopeSubgroup), atomic, EmitHostSubgroupLane(state, sourceLane)); + EmitSubgroupShuffle(state, TypeU32(state), result, atomic, EmitHostSubgroupLane(state, sourceLane)); return result; } template -std::uint32_t LockedLdsUpdate(SpirvEmitterState& state, TUpdate&& update) { +std::uint32_t LockedLdsUpdate(SpirvEmitterState& state, std::uint32_t type, std::uint32_t zero, TUpdate&& update) { const auto lock = EmitLdsLockPointer(state); const auto entry = state.currentLabel; const auto header = state.module.AllocateId(); const auto body = state.module.AllocateId(); const auto pending = state.module.AllocateId(); - const auto spinHeader = state.module.AllocateId(); - const auto spinBody = state.module.AllocateId(); - const auto spinCont = state.module.AllocateId(); + const auto attempt = state.module.AllocateId(); + const auto attemptMerge = state.module.AllocateId(); const auto critical = state.module.AllocateId(); const auto servedMerge = state.module.AllocateId(); const auto pendingMerge = state.module.AllocateId(); @@ -868,7 +1374,7 @@ std::uint32_t LockedLdsUpdate(SpirvEmitterState& state, TUpdate&& update) { state.module.AddFunction(spv::OpBranch, header); EmitLabel(state, header); state.module.AddFunction(spv::OpPhi, TypeBool(state), done, ConstantBool(state, false), entry, doneNext, cont); - state.module.AddFunction(spv::OpPhi, TypeU64(state), result, ConstantU64(state, 0u), entry, resultNext, cont); + state.module.AddFunction(spv::OpPhi, type, result, zero, entry, resultNext, cont); state.module.AddFunction(spv::OpLoopMerge, merge, cont, spv::LoopControlMaskNone); state.module.AddFunction(spv::OpBranch, body); EmitLabel(state, body); @@ -876,35 +1382,40 @@ std::uint32_t LockedLdsUpdate(SpirvEmitterState& state, TUpdate&& update) { state.module.AddFunction(spv::OpBranchConditional, done, pendingMerge, pending); EmitLabel(state, pending); const auto elected = state.module.AllocateId(); - state.module.AddFunction(spv::OpGroupNonUniformElect, TypeBool(state), elected, ConstantU32(state, spv::ScopeSubgroup)); + EmitSubgroupElect(state, elected); state.module.AddFunction(spv::OpSelectionMerge, servedMerge, spv::SelectionControlMaskNone); - state.module.AddFunction(spv::OpBranchConditional, elected, spinHeader, servedMerge); - EmitLabel(state, spinHeader); - state.module.AddFunction(spv::OpLoopMerge, critical, spinCont, spv::LoopControlMaskNone); - state.module.AddFunction(spv::OpBranch, spinBody); - EmitLabel(state, spinBody); + state.module.AddFunction(spv::OpBranchConditional, elected, attempt, servedMerge); + EmitLabel(state, attempt); const auto previous = state.module.AllocateId(); - state.module.AddFunction(spv::OpAtomicCompareExchange, TypeU32(state), previous, lock, ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsAcquireMask | spv::MemorySemanticsWorkgroupMemoryMask), ConstantU32(state, spv::MemorySemanticsMaskNone), ConstantU32(state, 1u), ConstantU32(state, 0u)); + state.module.AddFunction(spv::OpAtomicCompareExchange, TypeU32(state), previous, lock, ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsAcquireMask | LdsMemorySemantics(state)), ConstantU32(state, spv::MemorySemanticsMaskNone), ConstantU32(state, 1u), ConstantU32(state, 0u)); const auto acquired = Binary(state, spv::OpIEqual, TypeBool(state), previous, ConstantU32(state, 0u)); - state.module.AddFunction(spv::OpBranchConditional, acquired, critical, spinCont); - EmitLabel(state, spinCont); - state.module.AddFunction(spv::OpBranch, spinHeader); + 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 | spv::MemorySemanticsWorkgroupMemoryMask), 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); + const auto attempted = state.module.AllocateId(); + const auto attemptedResult = state.module.AllocateId(); + state.module.AddFunction(spv::OpPhi, TypeBool(state), attempted, ConstantBool(state, true), criticalExit, ConstantBool(state, false), attempt); + state.module.AddFunction(spv::OpPhi, type, attemptedResult, old, criticalExit, result, attempt); state.module.AddFunction(spv::OpBranch, servedMerge); EmitLabel(state, servedMerge); const auto served = state.module.AllocateId(); const auto servedResult = state.module.AllocateId(); - state.module.AddFunction(spv::OpPhi, TypeBool(state), served, ConstantBool(state, true), criticalExit, ConstantBool(state, false), pending); - state.module.AddFunction(spv::OpPhi, TypeU64(state), servedResult, old, criticalExit, result, pending); + state.module.AddFunction(spv::OpPhi, TypeBool(state), served, attempted, attemptMerge, ConstantBool(state, false), pending); + state.module.AddFunction(spv::OpPhi, type, servedResult, attemptedResult, attemptMerge, result, pending); state.module.AddFunction(spv::OpBranch, pendingMerge); EmitLabel(state, pendingMerge); state.module.AddFunction(spv::OpPhi, TypeBool(state), doneNext, served, servedMerge, done, body); - state.module.AddFunction(spv::OpPhi, TypeU64(state), resultNext, servedResult, servedMerge, result, body); + state.module.AddFunction(spv::OpPhi, type, resultNext, servedResult, servedMerge, result, body); const auto ballot = state.module.AllocateId(); - state.module.AddFunction(spv::OpGroupNonUniformBallot, TypeU32Vector(state, 4u), ballot, ConstantU32(state, spv::ScopeSubgroup), 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(); @@ -928,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); @@ -946,7 +1463,7 @@ std::uint32_t SharedAtomic64(SpirvValueEmitContext& ctx, const IrValue& inst, TR state.module.AddFunction(spv::OpStore, highPointer, nextHigh); return old; }; - return state.requirements.ldsLock ? LockedLdsUpdate(state, update) : update(); + return state.requirements.ldsLock ? LockedLdsUpdate(state, TypeU64(state), ConstantU64(state, 0u), update) : update(); }); }); } @@ -972,11 +1489,10 @@ std::uint32_t SharedAtomic64Select(SpirvValueEmitContext& ctx, const IrValue& in } std::uint32_t SharedFloatMinMax64(SpirvValueEmitContext& ctx, const IrValue& inst, bool maxValue) { + const bool flush = SharedMemory(ctx, inst).flushDenormals; return SharedAtomic64Binary(ctx, inst, [&](SpirvEmitterState& state, std::uint32_t old, std::uint32_t data) { const AtomicFloatBits bits{state, true}; - auto result = bits.select(bits.test(maxValue ? spv::OpUGreaterThan : spv::OpULessThan, bits.orderKey(data), bits.orderKey(old)), data, old); - result = bits.select(bits.nan(data), old, result); - return bits.select(bits.nan(old), data, result); + return bits.minMax(bits.flush(old, flush), bits.flush(data, flush), maxValue); }); } @@ -1043,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); } @@ -1121,6 +1637,44 @@ void EmitStoreAddressU32(SpirvValueEmitContext& ctx, const IrValue& inst) { StoreAddress(ctx, inst, 32u); } +void StoreAddressWide(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t components) { + const auto& mem = ctx.Memory(inst); + if (mem.kind != ResourceKind::Flat && mem.kind != ResourceKind::Global) ctx.Fail(inst, "must write a physical address resource"); + EmitIfCondition(ctx.state, ActiveArgument(ctx, inst), [&]() { + auto& state = ctx.state; + const auto base = GuestAddressBase(ctx, inst, mem); + const auto data = ctx.Arg(inst, inst.ArgumentCount() - 2u); + if (!RoutesApertures(state, mem)) { + EmitBdaDwordWrites(ctx, inst, base, mem.offset, components, data); + return; + } + const auto split = SplitAperture(state, AddBdaImmediate(ctx, inst, base, static_cast(mem.offset))); + const auto storeAperture = [&](ResourceKind kind) { + for (std::uint32_t component = 0; component < components; component++) { + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), value, data, component); + StoreApertureElement(ctx, kind, Binary(state, spv::OpIAdd, TypeU32(state), split.low, ConstantU32(state, component * 4u)), 32u, value); + } + }; + EmitIfCondition(state, split.shared, [&] { storeAperture(ResourceKind::Lds); }); + EmitIfCondition(state, split.priv, [&] { storeAperture(ResourceKind::Scratch); }); + const auto global = Unary(state, spv::OpLogicalNot, TypeBool(state), Binary(state, spv::OpLogicalOr, TypeBool(state), split.shared, split.priv)); + EmitIfCondition(state, global, [&] { EmitBdaDwordWrites(ctx, inst, base, mem.offset, components, data); }); + }); +} + +void EmitStoreAddressU32x2(SpirvValueEmitContext& ctx, const IrValue& inst) { + StoreAddressWide(ctx, inst, 2u); +} + +void EmitStoreAddressU32x3(SpirvValueEmitContext& ctx, const IrValue& inst) { + StoreAddressWide(ctx, inst, 3u); +} + +void EmitStoreAddressU32x4(SpirvValueEmitContext& ctx, const IrValue& inst) { + StoreAddressWide(ctx, inst, 4u); +} + std::uint32_t EmitAddressAtomic(SpirvValueEmitContext& ctx, const IrValue& inst) { return AddressAtomic(ctx, inst); } @@ -1345,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); @@ -1355,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); }); } @@ -1373,9 +1946,10 @@ std::uint32_t EmitSharedAtomicCmpst32(SpirvValueEmitContext& ctx, const IrValue& std::uint32_t EmitSharedAtomicCmpstF32(SpirvValueEmitContext& ctx, const IrValue& inst) { const auto compare = ctx.Arg(inst, 1); const auto source = ctx.Arg(inst, 2); + const bool flush = SharedMemory(ctx, inst).flushDenormals; return SharedAtomicUpdate(ctx, inst, [&](SpirvEmitterState& state, std::uint32_t old) { - const auto equal = Binary(state, spv::OpFOrdEqual, TypeBool(state), Unary(state, spv::OpBitcast, TypeF32(state), old), Unary(state, spv::OpBitcast, TypeF32(state), compare)); - return Select(state, TypeU32(state), equal, source, old); + const AtomicFloatBits bits{state, false}; + return bits.select(bits.equal(bits.flush(old, flush), bits.flush(compare, flush)), source, old); }); } @@ -1475,9 +2049,10 @@ std::uint32_t EmitSharedAtomicCmpst64(SpirvValueEmitContext& ctx, const IrValue& std::uint32_t EmitSharedAtomicCmpstF64(SpirvValueEmitContext& ctx, const IrValue& inst) { const auto compare = ScalarU64Argument(ctx, inst, 3u); const auto source = ScalarU64Argument(ctx, inst, 2u); + const bool flush = SharedMemory(ctx, inst).flushDenormals; return SharedAtomic64(ctx, inst, [&](SpirvEmitterState& state, std::uint32_t old) { const AtomicFloatBits bits{state, true}; - return bits.select(bits.equal(old, compare), source, old); + return bits.select(bits.equal(bits.flush(old, flush), bits.flush(compare, flush)), source, old); }); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 2689e24de5..5fad9ba460 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,13 +224,23 @@ void DefineModule(SpirvEmitterState& state) { state.scratchVariable.at(half) = state.module.AllocateId(); } } + 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(); if (StageOf(state) == IrShaderStage::Mesh) { const auto& mesh = VertexInfo(state).mesh; state.meshGuestFunc = state.module.AllocateId(); state.module.EmitCapability(spv::CapabilityMeshShadingEXT); state.module.EmitExtension("SPV_EXT_mesh_shader"); - state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeOutputTrianglesEXT); + state.module.AddExecutionMode(state.mainFunc, mesh.outputPrimitiveSize == 2u ? spv::ExecutionModeOutputLinesEXT : spv::ExecutionModeOutputTrianglesEXT); state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeOutputVertices, mesh.maxVertices); state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeOutputPrimitivesEXT, mesh.maxPrimitives); } @@ -80,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"); @@ -101,12 +286,24 @@ void DefineModule(SpirvEmitterState& state) { if (state.cullDistanceVariable != 0) { state.module.EmitCapability(spv::CapabilityCullDistance); } - if (state.layerVariable != 0) { + const bool layerExtension = std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityShaderViewportIndexLayerEXT)) != state.supportedCapabilities.end(); + if (state.layerVariable != 0 && layerExtension) { + state.module.EmitCapability(spv::CapabilityShaderViewportIndexLayerEXT); + state.module.EmitExtension("SPV_EXT_shader_viewport_index_layer"); + } else if (state.layerVariable != 0) { state.module.RequireVersion(0x00010500u); state.module.EmitCapability(spv::CapabilityShaderLayer); } 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); @@ -128,41 +325,46 @@ 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; }); - if (fragmentBarycentric) { + if (fragmentBarycentric && !state.program.Metadata().barycentricEmulation) { state.module.EmitCapability(spv::CapabilityFragmentBarycentricKHR); state.module.EmitExtension("SPV_KHR_fragment_shader_barycentric"); } 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); state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeSignedZeroInfNanPreserve, 64u); + if (state.float64DenormPreserve) { + state.module.EmitCapability(spv::CapabilityDenormPreserve); + state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDenormPreserve, 64u); + } } 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); @@ -176,7 +378,15 @@ void DefineModule(SpirvEmitterState& state) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDepthGreater); } } - if (pixel.psEarlyZ && !pixel.psPixelKillEnable && !pixel.psDepthExportEnable && !pixel.psSampleMaskExportEnable) { + 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"); + } + state.module.EmitCapability(spv::CapabilityStencilExportEXT); + state.module.EmitExtension("SPV_EXT_shader_stencil_export"); + state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeStencilRefReplacingEXT); + } + if (pixel.psEarlyZ && !pixel.psAlphaToCoverage && !pixel.QuadMasked()) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeEarlyFragmentTests); } if (pixel.psOrderedPixelShader) { @@ -189,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) { @@ -200,6 +410,11 @@ void DefineModule(SpirvEmitterState& state) { state.module.AddName(state.scratchVariable.at(half), "scratch_dwords"); } } + if (state.program.Metadata().vectorWindowSize != 0u) { + for (std::uint32_t half = 0; half < state.laneCount; half++) { + state.module.AddName(state.vectorWindowVariable.at(half), "vector_window"); + } + } } } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp index cde24c0baa..4c0c787f07 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp @@ -1,5 +1,7 @@ #include "SpirvBackend/SpirvEmitter.hpp" #include "SpirvBackend/SpirvEmitterHelpers.hpp" +#include "SpirvBackend/SpirvMemory/SpirvSubgroup.hpp" +#include #include #include #include @@ -15,6 +17,7 @@ namespace ShaderRecompiler namespace { std::uint32_t HostInvocationId(SpirvEmitterState& state) { + if (HostSubgroupSingleLane(state)) return ConstantU32(state, 0u); if (state.subgroupLocalInvocationIdVariable == 0) { FailEmit("SubgroupLocalInvocationId was not declared before function emission"); } @@ -23,15 +26,143 @@ 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) { + return state.hostSubgroupSize == 1u; + } + + 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.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); + state.module.AddFunction(spv::OpExtInst, TypeU32(state), result, GlslStd450(state), GLSLstd450FindILsb, own); + return; + } + state.module.AddFunction(spv::OpGroupNonUniformBallotFindLSB, TypeU32(state), result, ConstantU32(state, spv::ScopeSubgroup), ballot); + } + + 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; + } + 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)); + return; + } + state.module.AddFunction(spv::OpGroupNonUniformElect, TypeBool(state), result, ConstantU32(state, spv::ScopeSubgroup)); } 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); @@ -184,7 +315,7 @@ namespace ShaderRecompiler std::uint32_t EmitSubgroupLaneActiveBool(SpirvEmitterState& state, std::uint32_t lane) { const auto activeBallot = state.module.AllocateId(); - state.module.AddFunction(spv::OpGroupNonUniformBallot, TypeU32Vector(state, 4), activeBallot, ConstantU32(state, spv::ScopeSubgroup), ConstantBool(state, true)); + EmitSubgroupBallot(state, activeBallot, ConstantBool(state, true)); return EmitBallotLaneActiveBool(state, EmitWaveBallot(state, activeBallot), lane); } } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvTypes.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvTypes.cpp index bdc7ced720..1265fd1b02 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvTypes.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvTypes.cpp @@ -25,6 +25,14 @@ std::uint32_t StorageBufferU64Type(SpirvEmitterState& state) { array); } +std::uint32_t StorageBufferU8Type(SpirvEmitterState& state) { + const auto array = state.module.DecoratedType(spv::OpTypeRuntimeArray, {{spv::OpDecorate, {spv::DecorationArrayStride, 1u}}}, TypeU8(state)); + return state.module.DecoratedType(spv::OpTypeStruct, + {{spv::OpDecorate, {spv::DecorationBlock}}, + {spv::OpMemberDecorate, {0, spv::DecorationOffset, 0}}}, + array); +} + } @@ -40,6 +48,10 @@ std::uint32_t TypeBoolVector(SpirvEmitterState& state, std::uint32_t components) return state.module.Type(spv::OpTypeVector, TypeBool(state), components); } +std::uint32_t TypeU8(SpirvEmitterState& state) { + return state.module.Type(spv::OpTypeInt, 8u, 0u); +} + std::uint32_t TypeU32(SpirvEmitterState& state) { return state.module.Type(spv::OpTypeInt, 32u, 0u); } @@ -113,6 +125,18 @@ std::uint32_t TypeStorageBufferU64ElementPointer(SpirvEmitterState& state) { return TypePointer(state, spv::StorageClassStorageBuffer, TypeScalarU64(state)); } +std::uint32_t TypeStorageBufferU8Block(SpirvEmitterState& state) { + return StorageBufferU8Type(state); +} + +std::uint32_t TypeStorageBufferU8Pointer(SpirvEmitterState& state) { + return TypePointer(state, spv::StorageClassStorageBuffer, StorageBufferU8Type(state)); +} + +std::uint32_t TypeStorageBufferU8ElementPointer(SpirvEmitterState& state) { + return TypePointer(state, spv::StorageClassStorageBuffer, TypeU8(state)); +} + std::uint32_t TypePhysicalU32Pointer(SpirvEmitterState& state) { return TypePointer(state, spv::StorageClassPhysicalStorageBuffer, TypeU32(state)); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModule.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModule.cpp index 9b6d701cc1..f10a1cdd9e 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModule.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModule.cpp @@ -87,8 +87,9 @@ std::vector SpirvModule::Finalize() const { if (unpatchedPhiIncomings != 0) { throw std::runtime_error("SpirvModule::Finalize called with unpatched OpPhi incoming pairs"); } + if (inHelperFunction) throw std::runtime_error("SpirvModule::Finalize called inside a helper function"); std::vector module; - module.reserve(5u + capabilities.size() + extensions.size() + extInstImports.size() + memoryModel.size() + entryPoints.size() + executionModes.size() + debug.size() + annotations.size() + typeDeclarations.size() + declarations.size() + globalVariables.size() + functionInstructions.size()); + module.reserve(5u + capabilities.size() + extensions.size() + extInstImports.size() + memoryModel.size() + entryPoints.size() + executionModes.size() + debug.size() + annotations.size() + typeDeclarations.size() + declarations.size() + globalVariables.size() + functionInstructions.size() + helperFunctionInstructions.size()); module.push_back(spv::MagicNumber); module.push_back(version); module.push_back(0u); @@ -106,9 +107,24 @@ std::vector SpirvModule::Finalize() const { module.insert(module.end(), declarations.begin(), declarations.end()); module.insert(module.end(), globalVariables.begin(), globalVariables.end()); module.insert(module.end(), functionInstructions.begin(), functionInstructions.end()); + module.insert(module.end(), helperFunctionInstructions.begin(), helperFunctionInstructions.end()); return module; } +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; +} + void SpirvModule::RequireVersion(std::uint32_t version) { this->version = std::max(this->version, version); } @@ -187,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); } @@ -201,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 4fb0e39aaf..8e8d4fe56e 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -1,4 +1,5 @@ #include "SpirvBackend/SpirvModuleEmitter.hpp" +#include "SpirvBackend/SpirvBufferFormat.hpp" #include "SpirvBackend/SpirvEmitterHelpers.hpp" #include "SpirvBackend/SpirvEmitterInstructions.hpp" #include "SpirvBackend/SpirvFlowEmitter.hpp" @@ -26,6 +27,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: @@ -37,14 +80,101 @@ 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); } if (kind == StageInputKind::DispatchThreadLimit) { return EmitShaderDataDwordLoad(state, state.program.Metadata().bindings.DispatchThreadLimitDword() + component); } - if (state.laneCount == 2 && (kind == StageInputKind::LocalInvocationId || kind == StageInputKind::GlobalInvocationId)) { + if (kind == StageInputKind::RasterPrimitive) { + return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::RasterPrimitive, static_cast(RasterPrimitive::Triangle)); + } + if (kind == StageInputKind::LayerExported) { + return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::LayerExported, 0u); + } + if ((state.laneCount == 2 || 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"); @@ -59,9 +189,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); @@ -74,17 +213,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) { @@ -96,6 +249,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(); @@ -142,12 +302,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; @@ -242,6 +407,101 @@ std::uint32_t MrtOutputMode(const SpirvEmitterState& state, const ExportInfo& ex return state.inputInfo.pixel->targetOutputMode[exp.index]; } +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(); + state.module.AddFunction(spv::OpCompositeExtract, TypeF32(state), value, color, component); + const auto bits = state.module.AllocateId(); + state.module.AddFunction(spv::OpBitcast, TypeU32(state), bits, value); + const auto field = EmitFormatStoreComponent(state, info, component, bits); + packed = EmitOrU32(state, packed, info.componentBitOffset[component] == 0u ? field : EmitBinaryU32(state, spv::OpShiftLeftLogical, field, ConstantU32(state, info.componentBitOffset[component]))); + } + return packed; +} + +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::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(); + state.module.AddFunction(spv::OpCompositeExtract, TypeF32(state), unpacked, color, component); + components[component] = Select(state, TypeF32(state), packed, component == 0u ? Unary(state, spv::OpBitcast, TypeF32(state), word) : ConstantF32(state, 0u), unpacked); + } + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, TypeF32Vector(state, 4u), result, components[0], components[1], components[2], components[3]); + return result; +} + +bool AlphaToCoverage(const SpirvEmitterState& state) { + return state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->psAlphaToCoverage; +} + +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); +} + +void StoreDitheredAlphaCoverage(SpirvEmitterState& state, std::uint32_t color) { + constexpr std::array masks{{ + 0x00000000u, 0x00000010u, 0x00000004u, 0x10000004u, 0x10000002u, 0x10100002u, 0x04100002u, 0x04101002u, + 0x04101001u, 0x04101011u, 0x04041011u, 0x14041011u, 0x12041011u, 0x12141011u, 0x12140411u, 0x12141411u, + 0x12141411u, 0x12141431u, 0x12141415u, 0x32141415u, 0x32121415u, 0x32321415u, 0x16321415u, 0x16323415u, + 0x15323415u, 0x15323435u, 0x15163435u, 0x35163435u, 0x35163235u, 0x35363235u, 0x35361635u, 0x35363635u, + 0x35363635u, 0x35363675u, 0x3536363du, 0x7536363du, 0x75363637u, 0x75763637u, 0x3d763637u, 0x3d767637u, + 0x3d757637u, 0x3d757677u, 0x3d3d7677u, 0x7d3d7677u, 0x773d7677u, 0x777d7677u, 0x777d3e77u, 0x777d7e77u, + 0x777d7e77u, 0x777d7ef7u, 0x777d7e7fu, 0xf77d7e7fu, 0xf7777e7fu, 0xf7f77e7fu, 0x7ff77e7fu, 0x7ff7fe7fu, + 0x7ff7fd7fu, 0x7ff7fdffu, 0x7f7ffdffu, 0xff7ffdffu, 0xff7ff7ffu, 0xfffff7ffu, 0xffff7fffu, 0xffffffffu, + }}; + const auto alpha = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeF32(state), alpha, color, 3u); + const auto positive = Select(state, TypeF32(state), Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), alpha, ConstantF32Value(state, 0.0f)), alpha, ConstantF32Value(state, 0.0f)); + const auto bounded = Select(state, TypeF32(state), Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), positive, ConstantF32Value(state, 63.0f / 64.0f)), ConstantF32Value(state, 63.0f / 64.0f), positive); + const auto index = Unary(state, spv::OpConvertFToU, TypeU32(state), Binary(state, spv::OpFMul, TypeF32(state), bounded, ConstantF32Value(state, 64.0f))); + std::array words{}; + for (std::size_t i = 0; i < words.size(); ++i) words[i] = ConstantU32(state, masks[i]); + const auto array = state.module.Type(spv::OpTypeArray, TypeU32(state), ConstantU32(state, static_cast(words.size()))); + const auto table = state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassPrivate, array), spv::StorageClassPrivate); + state.module.AddFunction(spv::OpStore, table, state.module.Constant(spv::OpConstantComposite, array, std::span(words))); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassPrivate, TypeU32(state)), pointer, table, index); + const auto packed = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), packed, pointer); + const auto coord = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeF32Vector(state, 4u), coord, InputVariableForKind(state, StageInputKind::FragCoord)); + std::array parity{}; + for (std::uint32_t i = 0; i < 2; ++i) { + const auto component = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeF32(state), component, coord, i); + parity[i] = Binary(state, spv::OpBitwiseAnd, TypeU32(state), Unary(state, spv::OpConvertFToU, TypeU32(state), component), ConstantU32(state, 1u)); + } + const auto pixel = Binary(state, spv::OpBitwiseOr, TypeU32(state), Binary(state, spv::OpShiftLeftLogical, TypeU32(state), parity[0], ConstantU32(state, 1u)), parity[1]); + const auto shift = Binary(state, spv::OpShiftLeftLogical, TypeU32(state), pixel, ConstantU32(state, 3u)); + const auto mask = Binary(state, spv::OpBitwiseAnd, TypeU32(state), Binary(state, spv::OpShiftRightLogical, TypeU32(state), packed, shift), ConstantU32(state, 255u)); + StoreCoverage(state, Unary(state, spv::OpBitcast, TypeI32(state), mask)); +} + +void StoreAlphaCoverage(SpirvEmitterState& state, std::uint32_t color) { + if (state.inputInfo.pixel->psDitheredAlphaToCoverage) { + StoreDitheredAlphaCoverage(state, color); + return; + } + const auto alpha = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeF32(state), alpha, color, 3u); + const auto below = state.module.AllocateId(); + state.module.AddFunction(spv::OpFUnordLessThan, TypeBool(state), below, alpha, ConstantF32(state, 0x3f000000u)); + const auto mask = state.module.AllocateId(); + state.module.AddFunction(spv::OpSelect, TypeI32(state), mask, below, ConstantI32(state, 0), ConstantI32(state, -1)); + StoreCoverage(state, mask); +} + std::uint32_t ExportRawComponent(SpirvValueEmitContext& ctx, std::uint32_t vector, std::uint32_t component) { const auto value = ctx.state.module.AllocateId(); ctx.state.module.AddFunction(spv::OpCompositeExtract, TypeU32(ctx.state), value, vector, component); @@ -281,7 +541,7 @@ std::uint32_t ExportVector(SpirvValueEmitContext& ctx, std::uint32_t data, const ConstantU32(state, uintOutput ? 1u : 0x3f800000u), }; const auto mode = MrtOutputMode(state, exp); - const auto exported = exp.en & (mode == 1u ? 0x1u : mode == 2u ? 0x3u : mode == 3u ? 0x9u : 0xfu); + const auto exported = exp.en & (mode == 1u ? 0x1u : mode == 2u || mode == 3u ? 0x3u : 0xfu); if (exp.compr) { for (std::uint32_t pair = 0; pair < 2u; pair++) { if ((exp.en & (3u << (pair * 2u))) == 0u) { @@ -300,7 +560,7 @@ std::uint32_t ExportVector(SpirvValueEmitContext& ctx, std::uint32_t data, const } else { for (std::uint32_t component = 0; component < 4u; component++) { if (((exported >> component) & 1u) != 0u) { - raw.at(component) = ExportRawComponent(ctx, data, component); + raw.at(mode == 3u && component == 1u ? 3u : component) = ExportRawComponent(ctx, data, component); } } } @@ -354,19 +614,49 @@ void EmitAuxPositionExport(SpirvValueEmitContext& ctx, std::uint32_t data, const state.module.AddFunction(spv::OpStore, state.pointSizeVariable, f32); continue; } - const auto storeDistance = [&](std::uint32_t variable, std::uint32_t index) { + const auto storeDistance = [&](StageOutputKind kind, std::uint32_t variable, std::uint32_t index) { if (index == std::numeric_limits::max()) { return; } + if (state.program.Resources().stage == IrShaderStage::Mesh) { + state.module.AddFunction(spv::OpStore, MeshOutputPointer(state, kind, index), f32); + return; + } const auto pointer = state.module.AllocateId(); state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassOutput, TypeF32(state)), pointer, variable, ConstantU32(state, index)); state.module.AddFunction(spv::OpStore, pointer, f32); }; - storeDistance(state.clipDistanceVariable, output.clipDistance); - storeDistance(state.cullDistanceVariable, output.cullDistance); + storeDistance(StageOutputKind::ClipDistance, state.clipDistanceVariable, output.clipDistance); + storeDistance(StageOutputKind::CullDistance, state.cullDistanceVariable, output.cullDistance); } } +void StoreZeroPositionClipDistance(SpirvEmitterState& state, std::uint32_t position) { + const auto bits = state.module.AllocateId(); + state.module.AddFunction(spv::OpBitcast, TypeU32Vector(state, 4u), bits, position); + std::uint32_t combined = 0u; + for (std::uint32_t component = 0; component < 4u; component++) { + const auto raw = state.module.AllocateId(); + const auto magnitude = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), raw, bits, component); + state.module.AddFunction(spv::OpBitwiseAnd, TypeU32(state), magnitude, raw, ConstantU32(state, 0x7fffffffu)); + if (component == 0u) { + combined = magnitude; + continue; + } + const auto merged = state.module.AllocateId(); + state.module.AddFunction(spv::OpBitwiseOr, TypeU32(state), merged, combined, magnitude); + combined = merged; + } + const auto allZero = state.module.AllocateId(); + const auto distance = state.module.AllocateId(); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpIEqual, TypeBool(state), allZero, combined, ConstantU32(state, 0u)); + state.module.AddFunction(spv::OpSelect, TypeF32(state), distance, allZero, ConstantF32Value(state, -1.0f), ConstantF32Value(state, 0.0f)); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassOutput, TypeF32(state)), pointer, state.clipDistanceVariable, ConstantU32(state, *state.zeroPositionClipIndex)); + state.module.AddFunction(spv::OpStore, pointer, distance); +} + std::uint32_t ConvertClipCoordinate(SpirvEmitterState& state, std::uint32_t coordinate, float scale, float offset, float halfExtent) { const auto window = state.module.AllocateId(); const auto biased = state.module.AllocateId(); @@ -379,6 +669,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"); @@ -418,51 +761,90 @@ std::uint32_t MeshLoad(SpirvEmitterState& state, std::uint32_t variable, std::ui return value; } +bool IsMeshDistance(StageOutputKind kind) { + return kind == StageOutputKind::ClipDistance || kind == StageOutputKind::CullDistance; +} + std::uint32_t MeshOutputType(SpirvEmitterState& state, StageOutputKind kind) { - return kind == StageOutputKind::Layer ? TypeU32(state) : TypeF32Vector(state, 4u); + if (kind == StageOutputKind::Layer) { + return TypeU32(state); + } + return IsMeshDistance(kind) ? TypeF32(state) : TypeF32Vector(state, 4u); +} + +std::uint32_t MeshDistancePointer(SpirvEmitterState& state, const SpirvOutputBinding& output, std::uint32_t vertex) { + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassOutput, TypeF32(state)), pointer, output.variableId, vertex, ConstantU32(state, output.index)); + return pointer; } } namespace { +std::uint32_t TessellationOuterFactors(const SpirvEmitterState& state) { + return state.inputInfo.vertex->tess.domain == 2u ? 4u : 3u; +} + std::uint32_t TessellationPointer(SpirvValueEmitContext& ctx, const IrValue& inst) { auto& state = ctx.state; const auto kind = static_cast(inst.Argument(0)->ImmediateU32()); - const auto& tess = state.inputInfo.vertex->tess; const auto variable = state.tessVariables.at(static_cast(kind)); if (variable == 0u) { throw std::runtime_error("tessellation attribute has no interface variable"); } - const bool input = kind == TessellationAttribute::ControlInput || kind == TessellationAttribute::EvaluationInput; + const bool input = kind == TessellationAttribute::ControlInput || kind == TessellationAttribute::EvaluationInput || kind == TessellationAttribute::PatchInput || 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"); } const auto index = inst.Argument(1)->ImmediateU32() / 4u; - const bool outer = index < 3u; - if (index >= 4u) { + const auto outerFactors = TessellationOuterFactors(state); + const bool outer = index < outerFactors; + if (index >= outerFactors + (outerFactors == 4u ? 2u : 1u)) { throw std::runtime_error("tessellation factor index out of range"); } - state.module.AddFunction(spv::OpAccessChain, TypePointer(state, storage, TypeF32(state)), pointer, outer ? variable : state.tessInnerVariable, ConstantU32(state, outer ? index : index - 3u)); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, storage, TypeF32(state)), pointer, outer ? variable : state.tessInnerVariable, ConstantU32(state, outer ? index : index - outerFactors)); return pointer; } - auto address = ctx.Arg(inst, 1); - if (kind == TessellationAttribute::PatchOutput) { - address = EmitBinaryU32(state, spv::OpISub, address, ConstantU32(state, state.tessPatchBase)); - } + const auto address = ctx.Arg(inst, 1); + const bool patch = kind == TessellationAttribute::PatchOutput || kind == TessellationAttribute::PatchInput; const bool local = kind == TessellationAttribute::LocalOutput || kind == TessellationAttribute::ControlInput; - const auto stride = local ? tess.lsStride : tess.hsStride; - const auto offset = kind == TessellationAttribute::PatchOutput ? address : EmitBinaryU32(state, spv::OpUMod, address, ConstantU32(state, stride)); + 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 && 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, (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 || 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; + } + 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)); - const auto type = TypePointer(state, storage, TypeU32(state)); - if (kind == TessellationAttribute::LocalOutput || kind == TessellationAttribute::PatchOutput) { + if (kind == TessellationAttribute::LocalOutput || patch) { state.module.AddFunction(spv::OpAccessChain, type, pointer, variable, attribute, component); } else { - const auto vertex = kind == TessellationAttribute::ControlOutput ? EmitLaneId(state) : EmitBinaryU32(state, spv::OpUDiv, address, ConstantU32(state, stride)); + const 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; @@ -471,23 +853,13 @@ std::uint32_t TessellationPointer(SpirvValueEmitContext& ctx, const IrValue& ins } void DefineTessellationInterfaces(SpirvEmitterState& state) { - std::array used {}; - std::uint32_t patchBegin = std::numeric_limits::max(); - std::uint32_t patchEnd = 0u; + std::array used {}; for (const IrBlock* block : state.program.BlockOrder()) { for (const IrValue* inst : block->Instructions()) { if (inst->Opcode() != IrOpcode::GetTessellationAttribute && inst->Opcode() != IrOpcode::SetTessellationAttribute) { continue; } - const auto kind = inst->Argument(0)->ImmediateU32(); - used.at(kind) = true; - if (kind == static_cast(TessellationAttribute::PatchOutput)) { - if (!inst->Argument(1)->HasImmediate()) { - throw std::runtime_error("tessellation patch output offset must be immediate"); - } - patchBegin = std::min(patchBegin, inst->Argument(1)->ImmediateU32()); - patchEnd = std::max(patchEnd, inst->Argument(1)->ImmediateU32() + 4u); - } + used.at(inst->Argument(0)->ImmediateU32()) = true; } } if (std::none_of(used.begin(), used.end(), [](bool value) { return value; })) { @@ -502,33 +874,42 @@ void DefineTessellationInterfaces(SpirvEmitterState& state) { continue; } const auto kind = static_cast(index); - const bool input = kind == TessellationAttribute::ControlInput || kind == TessellationAttribute::EvaluationInput; + const bool input = kind == TessellationAttribute::ControlInput || kind == TessellationAttribute::EvaluationInput || kind == TessellationAttribute::PatchInput || 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 (kind == TessellationAttribute::PatchOutput) { - state.tessPatchBase = patchBegin; - type = array(TypeU32Vector(state, 4u), (patchEnd - patchBegin + 15u) / 16u); + } 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 ? tess.lsStride : tess.hsStride; + 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, kind == TessellationAttribute::PatchOutput ? (tess.hsStride + 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 || kind == TessellationAttribute::PatchOutput) { + if (kind == TessellationAttribute::Factor || patch) { state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationPatch); } } @@ -536,16 +917,16 @@ void DefineTessellationInterfaces(SpirvEmitterState& state) { void DefineTessellationExecutionModes(SpirvEmitterState& state) { const auto& tess = state.inputInfo.vertex->tess; - if (tess.domain != 1u || tess.partitioning != 2u || tess.outputTopology != 2u) { + if ((tess.domain != 1u && tess.domain != 2u) || (tess.partitioning != 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); if (state.program.Resources().stage == IrShaderStage::TessellationControl) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeOutputVertices, tess.outputControlPoints); } else { - state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeTriangles); - state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeSpacingFractionalOdd); - state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeVertexOrderCw); + state.module.AddExecutionMode(state.mainFunc, tess.domain == 2u ? spv::ExecutionModeQuads : spv::ExecutionModeTriangles); + 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); } } @@ -554,34 +935,130 @@ void DefineMeshOutputs(SpirvEmitterState& state) { throw std::runtime_error("vertex input info is missing for mesh output definition"); } const auto& mesh = state.inputInfo.vertex->mesh; + std::uint32_t clipDistanceCount = 0u; + std::uint32_t cullDistanceCount = 0u; + for (const auto& output : state.outputs) { + if (output.kind == StageOutputKind::ClipDistance) { + clipDistanceCount = std::max(clipDistanceCount, output.index + 1u); + } else if (output.kind == StageOutputKind::CullDistance) { + cullDistanceCount = std::max(cullDistanceCount, output.index + 1u); + } + } for (auto& output : state.outputs) { + if (IsMeshDistance(output.kind)) { + const bool clip = output.kind == StageOutputKind::ClipDistance; + auto& variable = clip ? state.clipDistanceVariable : state.cullDistanceVariable; + if (variable == 0u) { + const auto distances = state.module.Type(spv::OpTypeArray, TypeF32(state), ConstantU32(state, clip ? clipDistanceCount : cullDistanceCount)); + variable = MeshArray(state, spv::StorageClassOutput, distances, mesh.maxVertices); + state.interfaceVariables.push_back(variable); + state.module.AddName(variable, clip ? "gl_ClipDistance" : "gl_CullDistance"); + state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationBuiltIn, clip ? spv::BuiltInClipDistance : spv::BuiltInCullDistance); + } + output.variableId = variable; + output.meshDataVariable = MeshArray(state, spv::StorageClassPrivate, TypeF32(state), state.laneCount); + continue; + } if (output.kind != StageOutputKind::Position && output.kind != StageOutputKind::Parameter && output.kind != StageOutputKind::Layer) { throw std::runtime_error("unsupported mesh output kind"); } 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); + } else if (output.kind != StageOutputKind::Layer) { + state.module.AddAnnotation(spv::OpDecorate, output.variableId, spv::DecorationBuiltIn, spv::BuiltInPosition); } - if (output.kind == StageOutputKind::Layer) { - state.module.AddAnnotation(spv::OpDecorate, output.variableId, spv::DecorationPerPrimitiveEXT); + 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, TypeU32Vector(state, mesh.outputPrimitiveSize), mesh.maxPrimitives); state.meshCull = MeshArray(state, spv::StorageClassOutput, TypeBool(state), mesh.maxPrimitives); state.interfaceVariables.push_back(state.meshPrimitives); state.interfaceVariables.push_back(state.meshCull); - state.module.AddAnnotation(spv::OpDecorate, state.meshPrimitives, spv::DecorationBuiltIn, spv::BuiltInPrimitiveTriangleIndicesEXT); + state.module.AddAnnotation(spv::OpDecorate, state.meshPrimitives, spv::DecorationBuiltIn, mesh.outputPrimitiveSize == 2u ? spv::BuiltInPrimitiveLineIndicesEXT : spv::BuiltInPrimitiveTriangleIndicesEXT); state.module.AddAnnotation(spv::OpDecorate, state.meshCull, spv::DecorationBuiltIn, spv::BuiltInCullPrimitiveEXT); state.module.AddAnnotation(spv::OpDecorate, state.meshCull, spv::DecorationPerPrimitiveEXT); + if (state.numWorkgroupsVariable == 0u) { + state.numWorkgroupsVariable = DefineInterfaceVariable(state, TypeU32Vector(state, 3u), spv::StorageClassInput, "gl_NumWorkGroups"); + state.module.AddAnnotation(spv::OpDecorate, state.numWorkgroupsVariable, spv::DecorationBuiltIn, spv::BuiltInNumWorkgroups); + } +} + +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) { @@ -606,43 +1083,73 @@ void EmitMeshEntryPoint(SpirvEmitterState& state) { state.module.AddFunction(spv::OpExtInst, TypeU32(state), result, GlslStd450(state), GLSLstd450UMin, value, ConstantU32(state, limit)); return result; }; - const auto vertices = clamp(MeshLoad(state, state.meshAllocation, spv::StorageClassWorkgroup, TypeU32(state), ConstantU32(state, 0u)), mesh.maxVertices); + const auto groupsX = state.module.AllocateId(); + const auto groupsXPointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassInput, TypeU32(state)), groupsXPointer, state.numWorkgroupsVariable, ConstantU32(state, 0u)); + state.module.AddFunction(spv::OpLoad, TypeU32(state), groupsX, groupsXPointer); + const auto noGroups = state.module.AllocateId(); + state.module.AddFunction(spv::OpIEqual, TypeBool(state), noGroups, groupsX, ConstantU32(state, 0u)); + const auto allocatedVertices = clamp(MeshLoad(state, state.meshAllocation, spv::StorageClassWorkgroup, TypeU32(state), ConstantU32(state, 0u)), mesh.maxVertices); + const auto vertices = state.module.AllocateId(); + state.module.AddFunction(spv::OpSelect, TypeU32(state), vertices, noGroups, ConstantU32(state, 0u), allocatedVertices); const auto primitives = clamp(MeshLoad(state, state.meshAllocation, spv::StorageClassWorkgroup, TypeU32(state), ConstantU32(state, 1u)), mesh.maxPrimitives); state.module.AddFunction(spv::OpSetMeshOutputsEXT, vertices, primitives); 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); const auto value = MeshLoad(state, output.meshDataVariable, spv::StorageClassPrivate, type, ConstantU32(state, half)); - const auto pointer = MeshElement(state, output.variableId, spv::StorageClassOutput, type, index); + const auto pointer = IsMeshDistance(output.kind) ? MeshDistancePointer(state, output, index) : MeshElement(state, output.variableId, spv::StorageClassOutput, type, index); state.module.AddFunction(spv::OpStore, pointer, value); } + 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++) { + for (std::uint32_t component = 0; component < mesh.outputPrimitiveSize; component++) { const auto field = state.module.AllocateId(); state.module.AddFunction(spv::OpBitFieldUExtract, TypeU32(state), field, packed, ConstantU32(state, component * 10u), ConstantU32(state, 9u)); vertex.at(component) = state.module.AllocateId(); state.module.AddFunction(spv::OpExtInst, TypeU32(state), vertex.at(component), GlslStd450(state), GLSLstd450UMin, field, lastVertex); } - const auto triangle = state.module.AllocateId(); - state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 3u), triangle, vertex[0], vertex[1], vertex[2]); - const auto trianglePointer = MeshElement(state, state.meshPrimitives, spv::StorageClassOutput, TypeU32Vector(state, 3u), index); - state.module.AddFunction(spv::OpStore, trianglePointer, triangle); + const auto indices = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, mesh.outputPrimitiveSize), indices, std::span(vertex.data(), mesh.outputPrimitiveSize)); + const auto indicesPointer = MeshElement(state, state.meshPrimitives, spv::StorageClassOutput, TypeU32Vector(state, mesh.outputPrimitiveSize), index); + state.module.AddFunction(spv::OpStore, indicesPointer, indices); const auto nullBit = EmitBinaryU32(state, spv::OpBitwiseAnd, packed, ConstantU32(state, 0x80000000u)); const auto culled = state.module.AllocateId(); state.module.AddFunction(spv::OpINotEqual, TypeBool(state), culled, nullBit, ConstantU32(state, 0u)); @@ -652,10 +1159,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); @@ -775,11 +1317,7 @@ F32Class EmitClassifyF32Bits(SpirvEmitterState& state, std::uint32_t bits) { F32Class cls; cls.bits = bits; const auto absBits = EmitAndConstant(state, cls.bits, 0x7fffffffu); - const auto exponentBits = EmitAndConstant(state, absBits, 0x7f800000u); - const auto mantissaBits = EmitAndConstant(state, absBits, 0x007fffffu); - const auto exponentMax = EmitCompareU32Constant(state, spv::OpIEqual, exponentBits, 0x7f800000u); - const auto mantissaNonzero = EmitCompareU32Constant(state, spv::OpINotEqual, mantissaBits, 0u); - cls.nan = Binary(state, spv::OpLogicalAnd, TypeBool(state), exponentMax, mantissaNonzero); + cls.nan = EmitCompareU32Constant(state, spv::OpUGreaterThan, absBits, 0x7f800000u); cls.zero = EmitCompareU32Constant(state, spv::OpIEqual, absBits, 0u); return cls; } @@ -842,62 +1380,74 @@ std::uint32_t EmitMinMaxF32Value(SpirvEmitterState& state, std::uint32_t lhs, st const auto zeroBits = Binary(state, maxValue ? spv::OpBitwiseAnd : spv::OpBitwiseOr, TypeU32(state), lhsClass.bits, rhsClass.bits); const auto numericBits = Select(state, TypeU32(state), bothZero, zeroBits, orderedBits); - const auto lhsNanBits = Select(state, TypeU32(state), lhsClass.nan, rhsClass.bits, numericBits); - const auto resultBits = Select(state, TypeU32(state), rhsClass.nan, lhsClass.bits, lhsNanBits); + const auto resultBits = Select(state, TypeU32(state), rhsClass.nan, lhsClass.bits, numericBits); return Unary(state, spv::OpBitcast, TypeF32(state), resultBits); } -std::uint32_t EmitFlushF32DenormToSignedZero(SpirvEmitterState& state, std::uint32_t value) { - const auto bits = state.module.AllocateId(); - const auto absBits = state.module.AllocateId(); - const auto signBits = state.module.AllocateId(); - const auto nonZero = state.module.AllocateId(); - const auto subnormal = state.module.AllocateId(); - const auto flush = state.module.AllocateId(); - const auto selected = state.module.AllocateId(); - const auto result = state.module.AllocateId(); - state.module.AddFunction(spv::OpBitcast, TypeU32(state), bits, value); - state.module.AddFunction(spv::OpBitwiseAnd, TypeU32(state), absBits, bits, ConstantU32(state, 0x7fffffffu)); - state.module.AddFunction(spv::OpBitwiseAnd, TypeU32(state), signBits, bits, ConstantU32(state, 0x80000000u)); - state.module.AddFunction(spv::OpINotEqual, TypeBool(state), nonZero, absBits, ConstantU32(state, 0u)); - state.module.AddFunction(spv::OpULessThan, TypeBool(state), subnormal, absBits, ConstantU32(state, 0x00800000u)); - state.module.AddFunction(spv::OpLogicalAnd, TypeBool(state), flush, nonZero, subnormal); - state.module.AddFunction(spv::OpSelect, TypeU32(state), selected, flush, signBits, bits); - state.module.AddFunction(spv::OpBitcast, TypeF32(state), result, selected); - return result; -} - std::uint32_t EmitTrigCycleF32(SpirvEmitterState& state, std::uint32_t src, bool preserveSignedZero) { - const auto fract = state.module.AllocateId(); - const auto bits = state.module.AllocateId(); - const auto absBits = state.module.AllocateId(); - const auto large = state.module.AllocateId(); - const auto finite = state.module.AllocateId(); - const auto largeFinite = state.module.AllocateId(); + const auto rounded = state.module.AllocateId(); const auto reduced = state.module.AllocateId(); - state.module.AddFunction(spv::OpExtInst, TypeF32(state), fract, GlslStd450(state), GLSLstd450Fract, src); - state.module.AddFunction(spv::OpBitcast, TypeU32(state), bits, src); - state.module.AddFunction(spv::OpBitwiseAnd, TypeU32(state), absBits, bits, ConstantU32(state, 0x7fffffffu)); - state.module.AddFunction(spv::OpUGreaterThanEqual, TypeBool(state), large, absBits, ConstantU32(state, 0x4b000000u)); - state.module.AddFunction(spv::OpULessThan, TypeBool(state), finite, absBits, ConstantU32(state, 0x7f800000u)); - state.module.AddFunction(spv::OpLogicalAnd, TypeBool(state), largeFinite, large, finite); - state.module.AddFunction(spv::OpSelect, TypeF32(state), reduced, largeFinite, ConstantF32Value(state, 0.0f), fract); + state.module.AddFunction(spv::OpExtInst, TypeF32(state), rounded, GlslStd450(state), GLSLstd450RoundEven, src); + state.module.AddFunction(spv::OpFSub, TypeF32(state), reduced, src, rounded); if (!preserveSignedZero) { return reduced; } + const auto bits = state.module.AllocateId(); + const auto absBits = state.module.AllocateId(); const auto zero = state.module.AllocateId(); const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpBitcast, TypeU32(state), bits, src); + state.module.AddFunction(spv::OpBitwiseAnd, TypeU32(state), absBits, bits, ConstantU32(state, 0x7fffffffu)); state.module.AddFunction(spv::OpIEqual, TypeBool(state), zero, absBits, ConstantU32(state, 0u)); state.module.AddFunction(spv::OpSelect, TypeF32(state), result, zero, src, reduced); return result; } -std::uint32_t EmitF16BitsToF32(SpirvEmitterState& state, std::uint32_t bits) { +void EmitF16DenormPreserve(SpirvEmitterState& state) { + if (state.f16DenormPreserveEmitted) return; + state.module.EmitCapability(spv::CapabilityDenormPreserve); + state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDenormPreserve, 16u); + state.f16DenormPreserveEmitted = true; +} + +static std::uint32_t UnpackF16BitsToF32(SpirvEmitterState& state, std::uint32_t bits, bool nanBits) { + if (!state.float16DenormPreserve) return EmitF16BitsToF32Decoded(state, bits); + EmitF16DenormPreserve(state); + const auto u32 = TypeU32(state); + const auto f32 = TypeF32(state); const auto unpacked = state.module.AllocateId(); - const auto result = state.module.AllocateId(); + const auto value = state.module.AllocateId(); state.module.AddFunction(spv::OpExtInst, TypeF32Vector(state, 2), unpacked, GlslStd450(state), GLSLstd450UnpackHalf2x16, bits); - state.module.AddFunction(spv::OpCompositeExtract, TypeF32(state), result, unpacked, 0u); - return result; + state.module.AddFunction(spv::OpCompositeExtract, f32, value, unpacked, 0u); + if (!nanBits) return value; + const auto signAndPayload = Binary(state, spv::OpShiftRightArithmetic, u32, Binary(state, spv::OpShiftLeftLogical, u32, bits, ConstantU32(state, 16u)), ConstantU32(state, 3u)); + const auto nan = Unary(state, spv::OpBitcast, f32, Binary(state, spv::OpBitwiseOr, u32, signAndPayload, ConstantU32(state, 0x7f800000u))); + return Select(state, f32, Unary(state, spv::OpIsNan, TypeBool(state), value), nan, value); +} + +std::uint32_t EmitF16BitsToF32(SpirvEmitterState& state, std::uint32_t bits) { + return UnpackF16BitsToF32(state, bits, true); +} + +std::uint32_t EmitF16BitsToF32AnyNan(SpirvEmitterState& state, std::uint32_t bits) { + return UnpackF16BitsToF32(state, bits, false); +} + +std::uint32_t EmitF16BitsToF32Decoded(SpirvEmitterState& state, std::uint32_t bits) { + const auto u32 = TypeU32(state); + const auto f32 = TypeF32(state); + const auto op = [&](spv::Op opcode, std::uint32_t lhs, std::uint32_t rhs) { return Binary(state, opcode, u32, lhs, rhs); }; + const auto sign = op(spv::OpShiftLeftLogical, op(spv::OpBitwiseAnd, bits, ConstantU32(state, 0x8000u)), ConstantU32(state, 16u)); + const auto exponent = op(spv::OpBitwiseAnd, op(spv::OpShiftRightLogical, bits, ConstantU32(state, 10u)), ConstantU32(state, 0x1fu)); + const auto mantissa = op(spv::OpBitwiseAnd, bits, ConstantU32(state, 0x3ffu)); + const auto mantissa32 = op(spv::OpShiftLeftLogical, mantissa, ConstantU32(state, 13u)); + const auto normal = op(spv::OpBitwiseOr, op(spv::OpShiftLeftLogical, op(spv::OpIAdd, exponent, ConstantU32(state, 127u - 15u)), ConstantU32(state, 23u)), mantissa32); + const auto special = op(spv::OpBitwiseOr, ConstantU32(state, 0x7f800000u), mantissa32); + const auto subnormal = Unary(state, spv::OpBitcast, u32, Binary(state, spv::OpFMul, f32, Unary(state, spv::OpConvertUToF, f32, mantissa), ConstantF32Value(state, 0x1p-24f))); + const auto boolean = TypeBool(state); + const auto finite = Select(state, u32, Binary(state, spv::OpIEqual, boolean, exponent, ConstantU32(state, 0u)), subnormal, normal); + const auto magnitude = Select(state, u32, Binary(state, spv::OpIEqual, boolean, exponent, ConstantU32(state, 0x1fu)), special, finite); + return Unary(state, spv::OpBitcast, f32, op(spv::OpBitwiseOr, sign, magnitude)); } void EmitProgram(SpirvEmitterState& state) { @@ -918,6 +1468,7 @@ void EmitProgram(SpirvEmitterState& state) { state.labels.emplace(block, label); } DefineGetBdaPointer(state); + DefineDot2F32F16Function(state); for (const IrBlock* block : program.BlockOrder()) { const bool needsScratch = std::any_of(block->Instructions().begin(), block->Instructions().end(), [](const IrValue* inst) { return inst->Opcode() == IrOpcode::SwizzleU32; @@ -930,16 +1481,33 @@ 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++) { state.module.AddFunction(spv::OpVariable, TypeU32ArrayPointer(state, spv::StorageClassFunction, state.program.Info().scratchDwords), state.scratchVariable.at(half), spv::StorageClassFunction); } } + if (const auto window = state.program.Metadata().vectorWindowSize; window != 0u) { + 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) { state.module.AddFunction(spv::OpVariable, TypePointer(state, spv::StorageClassFunction, TypeU32(state)), state.pixelValidMaskVariable, spv::StorageClassFunction); } @@ -949,9 +1517,14 @@ void EmitProgram(SpirvEmitterState& state) { state.module.AddFunction(spv::OpVariable, TypePointer(state, spv::StorageClassFunction, TypeU32(state)), lane.scratchU32Variable, spv::StorageClassFunction); } } + EmitLdsBufferBase(state); if (state.requirements.ldsLock) { - 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 | spv::MemorySemanticsWorkgroupMemoryMask)); + 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) { state.gdsLength = state.module.AllocateId(); @@ -960,10 +1533,71 @@ 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 (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); @@ -989,6 +1623,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); } @@ -1030,12 +1665,9 @@ std::uint32_t EmitMeshDrawParameter(SpirvValueEmitContext& ctx, const IrValue& i return result; } -std::uint32_t EmitMeshArgument(SpirvValueEmitContext& ctx, const IrValue& inst) { - auto& state = ctx.state; - const auto index = inst.Argument(0)->ImmediateU32(); - if (state.program.Resources().stage != IrShaderStage::Mesh || index >= MeshArgumentBytes / 4u) { - ctx.Fail(inst, "invalid mesh argument"); - } +namespace { + +std::uint32_t MeshArgumentAddress(SpirvEmitterState& state) { const auto push = [&](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, MeshDrawPushOffsetBytes / 4u + dword)); @@ -1045,7 +1677,42 @@ std::uint32_t EmitMeshArgument(SpirvValueEmitContext& ctx, const IrValue& inst) }; const auto low = push(MeshArgumentAddressDword); const auto high = push(MeshArgumentAddressDword + 1u); - const auto address = Binary(state, spv::OpBitwiseOr, TypeScalarU64(state), low, Binary(state, spv::OpShiftLeftLogical, TypeScalarU64(state), high, ConstantU32(state, 32u))); + return Binary(state, spv::OpBitwiseOr, TypeScalarU64(state), low, Binary(state, spv::OpShiftLeftLogical, TypeScalarU64(state), high, ConstantU32(state, 32u))); +} + +std::uint32_t LoadMeshArgumentWord(SpirvEmitterState& state, std::uint32_t address, std::uint32_t dword) { + const auto element = Binary(state, spv::OpIAdd, TypeScalarU64(state), address, Binary(state, spv::OpShiftLeftLogical, TypeScalarU64(state), Unary(state, spv::OpUConvert, TypeScalarU64(state), dword), ConstantU32(state, 2u))); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpConvertUToPtr, TypePointer(state, spv::StorageClassPhysicalStorageBuffer, TypeU32(state)), pointer, element); + const auto loaded = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), loaded, pointer, spv::MemoryAccessAlignedMask, 4u); + return loaded; +} + +} + +std::uint32_t EmitMeshRestartStart(SpirvValueEmitContext& ctx, const IrValue& inst) { + auto& state = ctx.state; + if (state.program.Resources().stage != IrShaderStage::Mesh) { + ctx.Fail(inst, "mesh restart table read outside a mesh shader"); + } + return EmitValueOrZeroIfCondition(state, ctx.Arg(inst, 1), [&]() { + const auto address = MeshArgumentAddress(state); + const auto length = LoadMeshArgumentWord(state, address, ConstantU32(state, MeshRestartLengthDword)); + const auto last = Binary(state, spv::OpISub, TypeU32(state), length, ConstantU32(state, 1u)); + const auto position = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, TypeU32(state), position, GlslStd450(state), GLSLstd450UMin, ctx.Arg(inst, 0), last); + return LoadMeshArgumentWord(state, address, Binary(state, spv::OpIAdd, TypeU32(state), position, ConstantU32(state, MeshRestartTableDword))); + }); +} + +std::uint32_t EmitMeshArgument(SpirvValueEmitContext& ctx, const IrValue& inst) { + auto& state = ctx.state; + const auto index = inst.Argument(0)->ImmediateU32(); + if (state.program.Resources().stage != IrShaderStage::Mesh || index >= MeshArgumentBytes / 4u) { + ctx.Fail(inst, "invalid mesh argument"); + } + const auto address = MeshArgumentAddress(state); const auto present = Binary(state, spv::OpINotEqual, TypeBool(state), address, BdaConstant(state, 0u)); const auto before = state.currentLabel; const auto loadLabel = state.module.AllocateId(); @@ -1073,13 +1740,45 @@ std::uint32_t EmitGetTessellationAttribute(SpirvValueEmitContext& ctx, const IrV }); } +namespace { + +std::uint32_t TessellationLevel(SpirvEmitterState& state, std::uint32_t index) { + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePushConstantElementPointer(state), pointer, state.pushConstantVariable, ConstantU32(state, 0u), ConstantU32(state, TessellationLevelPushOffsetBytes / 4u + index)); + const auto word = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), word, pointer); + const auto level = state.module.AllocateId(); + state.module.AddFunction(spv::OpBitcast, TypeF32(state), level, word); + return level; +} + +std::uint32_t TessellationLevelBound(SpirvEmitterState& state, std::uint32_t level, std::uint32_t operation, float limit) { + const auto bounded = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, TypeF32(state), bounded, GlslStd450(state), operation, level, ConstantF32Value(state, limit)); + return bounded; +} + +} + void EmitSetTessellationAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) { EmitIfCondition(ctx.state, ctx.Arg(inst, 3), [&]() { auto value = ctx.Arg(inst, 2); if (inst.Argument(0)->ImmediateU32() == static_cast(TessellationAttribute::Factor)) { - const auto floating = ctx.state.module.AllocateId(); - ctx.state.module.AddFunction(spv::OpBitcast, TypeF32(ctx.state), floating, value); - value = floating; + auto& state = ctx.state; + if (state.program.Resources().stage != IrShaderStage::TessellationControl || state.pushConstantVariable == 0u) ctx.Fail(inst, "writes tessellation factors outside a hull program"); + const bool outer = inst.Argument(1)->HasImmediate() && inst.Argument(1)->ImmediateU32() / 4u < TessellationOuterFactors(state); + const auto minimum = TessellationLevel(state, 0u); + const auto lowest = outer ? minimum : TessellationLevelBound(state, minimum, GLSLstd450FMax, 1.0f); + const auto floating = state.module.AllocateId(); + state.module.AddFunction(spv::OpBitcast, TypeF32(state), floating, value); + const auto inside = state.module.AllocateId(); + state.module.AddFunction(spv::OpFOrdGreaterThan, TypeBool(state), inside, floating, ConstantF32Value(state, outer ? 0.0f : 1.0f)); + const auto raised = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, TypeF32(state), raised, GlslStd450(state), GLSLstd450FMax, floating, lowest); + const auto clamped = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, TypeF32(state), clamped, GlslStd450(state), GLSLstd450FMin, 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); } ctx.state.module.AddFunction(spv::OpStore, TessellationPointer(ctx, inst), value); }); @@ -1104,7 +1803,7 @@ std::uint32_t EmitGetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) const auto component = inst.Argument(1)->ImmediateU32(); if (state.inputInfo.vertex == nullptr || !state.inputInfo.vertex->fetchEmbedded || component >= 4u) ctx.Fail(inst, "invalid prepared vertex attribute"); const auto first = PipelineSpecialization::VertexBase + attribute * PipelineSpecialization::VertexWords; - const auto selector = state.module.SpecializationConstant(TypeU32(state), first + component, component + 4u); + const auto selector = state.registrationModules ? EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), EmitShaderDataDwordLoad(state, state.program.Metadata().bindings.RuntimeStateDword() + RuntimeAbi::StateVertexSelectors + attribute / RuntimeAbi::VertexSelectorsPerDword), ConstantU32(state, attribute % RuntimeAbi::VertexSelectorsPerDword * 16u + component * 3u)), 7u) : state.module.SpecializationConstant(TypeU32(state), first + component, component + 4u); const auto one = state.module.SpecializationConstant(TypeU32(state), first + 4u, 0x3f800000u); auto value = Select(state, TypeU32(state), Binary(state, spv::OpIEqual, TypeBool(state), selector, ConstantU32(state, 1u)), one, ConstantU32(state, 0u)); for (std::uint32_t channel = 0; channel < 4u; ++channel) value = Select(state, TypeU32(state), Binary(state, spv::OpIEqual, TypeBool(state), selector, ConstantU32(state, channel + 4u)), EmitAttributeValue(state, attribute, channel), value); @@ -1152,13 +1851,18 @@ void EmitSetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) { state.module.AddFunction(spv::OpBitcast, TypeF32(state), f32, raw); state.module.AddFunction(spv::OpStore, state.depthVariable, f32); } + if ((exp.en & 2u) != 0u && state.stencilRefVariable != 0u) { + 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); + } 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; } @@ -1173,13 +1877,16 @@ void EmitSetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) { if (exp.index >= 8u) throw std::runtime_error("fragment export target exceeds the runtime mapping table"); std::array components{}; for (std::uint32_t component = 0; component < components.size(); ++component) { - const auto index = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ExportBase + exp.index * 4u + component, component); + const auto index = state.registrationModules ? EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), EmitShaderDataDwordLoad(state, state.program.Metadata().bindings.RuntimeStateDword() + RuntimeAbi::StateExportMappings + exp.index / 4u), ConstantU32(state, exp.index % 4u * 8u + component * 2u)), 3u) : state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ExportBase + exp.index * 4u + component, component); components[component] = state.module.AllocateId(); state.module.AddFunction(spv::OpVectorExtractDynamic, uintOutput ? TypeU32(state) : TypeF32(state), components[component], value, index); } const auto mapped = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeConstruct, vectorType, mapped, components[0], components[1], components[2], components[3]); value = mapped; + if (exp.index == 0u && !uintOutput && AlphaToCoverage(state)) StoreAlphaCoverage(state, value); + const auto mode = MrtOutputMode(state, exp); + if (!uintOutput && mode != 5u && mode != 6u) value = SelectExportPacking(state, exp.index, value); } if (exp.kind == ExportTargetKind::Position) { if (state.inputInfo.vertex == nullptr) { @@ -1188,6 +1895,12 @@ 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); } if (state.program.Resources().stage == IrShaderStage::Mesh) { const auto kind = exp.kind == ExportTargetKind::Position ? StageOutputKind::Position : StageOutputKind::Parameter; 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 namespace ShaderRecompiler { @@ -20,6 +22,8 @@ struct RuntimeDescriptor { std::uint32_t swizzle; std::uint32_t index; std::uint32_t valid; + std::uint32_t offset = 0; + std::uint32_t soffset = 0; }; struct RuntimeAddress { @@ -45,7 +49,7 @@ std::uint32_t nonzero(SpirvEmitterState& state, std::uint32_t value) { 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), ConstantU32(state, instruction.Flags().pc), reason); }); + EmitIfCondition(state, condition, [&] { RecordBdaFault(state, address, ConstantU32(state, bytes), BdaInstructionPc(state, instruction), reason); }); StopBdaInvocationIf(state, condition); } @@ -54,9 +58,11 @@ RuntimeDescriptor descriptor(SpirvValueEmitContext& context, const IrValue& inst const auto* handle = instruction.Argument(0)->Resolve(); if (handle->Opcode() != IrOpcode::GetBufferResource || handle->ArgumentCount() != 4u) context.Fail(instruction, "requires a runtime four-dword V#"); 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 = memory.gpuDescriptor ? context.Arg(*handle, 1u) : state.module.SpecializationConstant(TypeU32(state), first, 0u); - const auto word3 = memory.gpuDescriptor ? context.Arg(*handle, 3u) : state.module.SpecializationConstant(TypeU32(state), first + 1u, 0u); + const auto word1 = runtime ? context.Arg(*handle, 1u) : state.module.SpecializationConstant(TypeU32(state), first, 0u); + 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); const auto u32 = TypeU32(state); auto base = ConstantU32(state, 0u); if (memory.gpuDescriptor || BufferAccessOf(instruction.Opcode()) == BufferAccess::Atomic) { @@ -79,11 +85,12 @@ RuntimeDescriptor descriptor(SpirvValueEmitContext& context, const IrValue& inst } if (memory.gpuDescriptor) faultIf(context, instruction, invalid, base, 0u, BdaAbi::FaultReason::InvalidDescriptor); const auto stride = field(state, word1, 16u, 14u); - const auto valid = memory.gpuDescriptor ? both(state, Unary(state, spv::OpLogicalNot, TypeBool(state), invalid), Binary(state, spv::OpINotEqual, TypeBool(state), base, BdaConstant(state, 0u))) : nonzero(state, state.module.SpecializationConstant(u32, first + 2u, 0u)); + const auto present = runtime ? both(state, nonzero(state, context.Arg(*handle, 2u)), Binary(state, spv::OpLogicalOr, TypeBool(state), nonzero(state, context.Arg(*handle, 0u)), nonzero(state, field(state, word1, 0u, 16u)))) : nonzero(state, state.module.SpecializationConstant(u32, first + 2u, 0u)); + const auto valid = memory.gpuDescriptor ? both(state, Unary(state, spv::OpLogicalNot, TypeBool(state), invalid), Binary(state, spv::OpINotEqual, TypeBool(state), base, BdaConstant(state, 0u))) : present; 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); @@ -92,10 +99,9 @@ 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 = add(context.Arg(instruction, 2u), constant(context.Memory(instruction).offset + componentOffset)); - const auto soffset = context.Arg(instruction, 3u); + 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); const auto indexMsb = Binary(state, spv::OpShiftRightLogical, u32, resource.index, indexShift); @@ -113,14 +119,21 @@ 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}; } -MemoryResourceAccess directAccess(SpirvValueEmitContext& context, const IrValue& instruction, bool wide = false) { +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) { auto& state = context.state; const auto& memory = context.Memory(instruction); - const auto variable = wide ? state.storageBufferU64Variable : state.storageBufferVariable; + const auto variable = wide ? state.storageBufferU64Variable : plainLoad || state.storageBufferCoherentVariable == 0u ? state.storageBufferVariable : state.storageBufferCoherentVariable; if (variable == 0u) context.Fail(instruction, "direct buffer descriptor array is missing"); const auto element = ResourceForDescriptor(state, DescriptorBindingKind::Buffers, memory.resource); MemoryResourceAccess access; @@ -137,8 +150,8 @@ MemoryResourceAccess directAccess(SpirvValueEmitContext& context, const IrValue& std::uint32_t baseMisalignment(SpirvValueEmitContext& context, const IrValue& instruction) { const auto& memory = context.Memory(instruction); if (memory.kind == ResourceKind::ScalarBuffer) return ConstantU32(context.state, 0u); - const auto first = PipelineSpecialization::BufferBase + memory.resource * PipelineSpecialization::BufferWords; - return context.state.module.SpecializationConstant(TypeU32(context.state), first + 3u, 0u); + auto& state = context.state; + return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::BufferBase + memory.resource * PipelineSpecialization::BufferWords + 3u, 0u); } std::uint32_t directWordIndex(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t address) { @@ -147,35 +160,68 @@ 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); + const auto access = directAccess(context, instruction, false, !context.Memory(instruction).coherent); const auto index = directWordIndex(state, access, address); const auto misalignment = baseMisalignment(context, instruction); 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) { + if (state.storageBufferU8Variable != 0u) return state.storageBufferU8Variable; + if (state.program.Resources().stage != IrShaderStage::Compute) return 0u; + if (std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityStorageBuffer8BitAccess)) == state.supportedCapabilities.end()) return 0u; + const IrBindingLayout& layout = state.program.Metadata().bindings; + const auto binding = std::find_if(layout.descriptors.begin(), layout.descriptors.end(), [](const IrDescriptorBinding& descriptor) { return descriptor.kind == DescriptorBindingKind::Buffers; }); + if (binding == layout.descriptors.end() || state.storageBufferVariable == 0u) return 0u; + state.module.EmitCapability(spv::CapabilityStorageBuffer8BitAccess); + state.module.EmitExtension("SPV_KHR_8bit_storage"); + const auto count = ConstantU32(state, static_cast(binding->resources.size())); + const auto type = state.module.Type(spv::OpTypeArray, TypeStorageBufferU8Block(state), count); + const auto variable = state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassStorageBuffer, type), spv::StorageClassStorageBuffer); + state.module.AddName(variable, "buffers_u8"); + state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationDescriptorSet, 0u); + state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationBinding, NativeBinding(state.program.Resources().stage, DescriptorBindingKind::Buffers)); + state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationAliased); + if (state.storageBufferU64Variable == 0u && state.storageBufferCoherentVariable == 0u) state.module.AddAnnotation(spv::OpDecorate, state.storageBufferVariable, spv::DecorationAliased); + if (state.requirements.coherentBuffers) state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationCoherent); + state.storageBufferU8Variable = variable; + return variable; } void writeBuffer(SpirvValueEmitContext& context, const IrValue& instruction, std::uint32_t address, std::uint32_t value, std::uint32_t bits = 32u) { @@ -191,6 +237,24 @@ void writeBuffer(SpirvValueEmitContext& context, const IrValue& instruction, std const auto unaligned = nonzero(state, misalignment); const auto next = EmitAddU32(state, index, ConstantU32(state, 1u)); const auto last = Select(state, u32, unaligned, next, index); + const auto bytes = bits != 32u ? byteBuffers(state) : 0u; + if (bytes != 0u) { + const auto element = ResourceForDescriptor(state, DescriptorBindingKind::Buffers, context.Memory(instruction).resource); + const auto block = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypeStorageBufferU8Pointer(state), block, bytes, ConstantU32(state, element)); + EmitIfCondition(state, EmitMemoryElementInBounds(state, access, last), [&] { + const auto first = EmitAddU32(state, EmitAddU32(state, Binary(state, spv::OpShiftLeftLogical, u32, index, ConstantU32(state, 2u)), misalignment), EmitAndConstant(state, address, 3u)); + for (std::uint32_t byte = 0; byte < bits / 8u; ++byte) { + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypeStorageBufferU8ElementPointer(state), pointer, block, ConstantU32(state, 0u), EmitAddU32(state, first, ConstantU32(state, byte))); + const auto shifted = byte == 0u ? value : Binary(state, spv::OpShiftRightLogical, u32, value, ConstantU32(state, byte * 8u)); + const auto stored = Unary(state, spv::OpUConvert, TypeU8(state), shifted); + if (access.memoryAccess != 0u) state.module.AddFunction(spv::OpStore, pointer, stored, access.memoryAccess); + else state.module.AddFunction(spv::OpStore, pointer, stored); + } + }); + return; + } EmitIfCondition(state, EmitMemoryElementInBounds(state, access, last), [&] { auto mask = ConstantU32(state, bits == 32u ? 0xffffffffu : (1u << bits) - 1u); auto masked = Binary(state, spv::OpBitwiseAnd, u32, value, mask); @@ -231,10 +295,12 @@ std::uint32_t composite(SpirvEmitterState& state, std::uint32_t components, cons return result; } -std::array storeValues(SpirvValueEmitContext& context, const IrValue& instruction, std::uint32_t components) { - auto& state = context.state; +std::uint32_t storeData(SpirvValueEmitContext& context, const IrValue& instruction) { + return context.Arg(instruction, instruction.ArgumentCount() - 2u); +} + +std::array storeValues(SpirvEmitterState& state, std::uint32_t data, std::uint32_t components) { std::array result{}; - const auto data = context.Arg(instruction, instruction.ArgumentCount() - 2u); for (std::uint32_t component = 0; component < components; ++component) { result[component] = data; if (components != 1u) { @@ -245,7 +311,7 @@ std::array storeValues(SpirvValueEmitContext& context, const I return result; } -std::uint32_t formatted(SpirvValueEmitContext& context, const IrValue& instruction, const RuntimeDescriptor& resource, const SpirvBufferFormatInfo& info, std::uint32_t components, bool store) { +std::uint32_t formatted(SpirvValueEmitContext& context, const IrValue& instruction, const RuntimeDescriptor& resource, const SpirvBufferFormatInfo& info, std::uint32_t components, bool store, std::uint32_t data) { auto& state = context.state; const auto& memory = context.Memory(instruction); const auto u32 = TypeU32(state); @@ -271,11 +337,11 @@ std::uint32_t formatted(SpirvValueEmitContext& context, const IrValue& instructi inBounds = both(state, inBounds, Binary(state, spv::OpLogicalOr, TypeBool(state), Unary(state, spv::OpLogicalNot, TypeBool(state), required[component]), addresses[component].inBounds)); } if (store) { - const auto data = storeValues(context, instruction, components); + const auto values = storeValues(state, data, components); EmitIfCondition(state, inBounds, [&] { auto packed = ConstantU32(state, 0u); for (std::uint32_t component = 0; component < info.componentCount; ++component) { - const auto encoded = component >= components ? ConstantU32(state, 0u) : memory.d16 ? EmitD16StoreComponent(state, info, component, data[component]) : EmitFormatStoreComponent(state, info, component, data[component]); + const auto encoded = component >= components ? ConstantU32(state, 0u) : memory.d16 ? EmitD16StoreComponent(state, info, component, values[component]) : EmitFormatStoreComponent(state, info, component, values[component]); if (info.packedBitfield) { packed = EmitOrU32(state, packed, Binary(state, spv::OpShiftLeftLogical, u32, EmitAndConstant(state, encoded, (1u << info.componentBits[component]) - 1u), ConstantU32(state, info.componentBitOffset[component]))); } else { @@ -313,13 +379,142 @@ std::uint32_t formatted(SpirvValueEmitContext& context, const IrValue& instructi return composite(state, components, outputs); } -std::uint32_t formattedDispatch(SpirvValueEmitContext& context, const IrValue& instruction, const RuntimeDescriptor& resource, std::uint32_t components, bool store) { +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); + +std::uint32_t formattedDispatch(SpirvValueEmitContext& context, const IrValue& instruction, const RuntimeDescriptor& resource, std::uint32_t components, bool store, std::uint32_t data) { auto& state = context.state; const auto& memory = context.Memory(instruction); if (memory.typed) { const auto info = GetFormatInfo(DecodeTBufferFormat(memory.dataFormat, memory.numberFormat)); if (info.type == SpirvFormatComponentType::Unknown) context.Fail(instruction, "typed buffer instruction has an invalid format"); - return formatted(context, instruction, resource, info, components, store); + return formatted(context, instruction, resource, info, components, store, data); } if (!store && memory.gpuDescriptor) { auto invalid = ConstantBool(state, false); @@ -333,12 +528,30 @@ std::uint32_t formattedDispatch(SpirvValueEmitContext& context, const IrValue& i if (memory.gpuDescriptor && word->HasImmediate()) { const auto format = static_cast((word->ImmediateU32() >> 12u) & 0x7fu); if (format == IrBufferFormat::Invalid) context.Fail(instruction, "formatted buffer instruction has an invalid constant format"); - return formatted(context, instruction, resource, GetFormatInfo(format), components, store); + return formatted(context, instruction, resource, GetFormatInfo(format), components, store, data); } + if (!memory.gpuDescriptor) return formattedCall(context, instruction, resource, components, store, data); + return formattedGpuCall(context, instruction, resource, components, store, data); +} + +std::uint32_t formatSwitch(SpirvValueEmitContext& context, const IrValue& instruction, const RuntimeDescriptor& resource, std::uint32_t components, bool store, std::uint32_t data) { + 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) { @@ -352,7 +565,7 @@ std::uint32_t formattedDispatch(SpirvValueEmitContext& context, const IrValue& i state.module.AddFunction(branch); for (std::uint32_t index = 0; index < count; ++index) { EmitLabel(state, labels[index]); - const auto value = formatted(context, instruction, resource, GetFormatInfo(static_cast(index + 1u)), components, store); + const auto value = formatted(context, instruction, resource, GetFormatInfo(static_cast(index + 1u)), components, store, data); const auto exit = state.module.AllocateId(); state.module.AddFunction(spv::OpBranch, exit); EmitLabel(state, exit); @@ -360,7 +573,7 @@ std::uint32_t formattedDispatch(SpirvValueEmitContext& context, const IrValue& i phi.insert(phi.end(), {value, exit}); } EmitLabel(state, invalid); - if (memory.gpuDescriptor) RecordBdaFault(state, resource.base, ConstantU32(state, 0u), ConstantU32(state, instruction.Flags().pc), BdaAbi::FaultReason::InvalidDescriptor); + if (memory.gpuDescriptor) RecordBdaFault(state, resource.base, ConstantU32(state, 0u), BdaInstructionPc(state, instruction), BdaAbi::FaultReason::InvalidDescriptor); const auto zero = store || components == 1u ? ConstantU32(state, 0u) : ConstantU32CompositeZero(state, components); const auto invalidExit = state.module.AllocateId(); state.module.AddFunction(spv::OpBranch, invalidExit); @@ -374,6 +587,163 @@ std::uint32_t formattedDispatch(SpirvValueEmitContext& context, const IrValue& i return result; } +std::uint32_t formattedCall(SpirvValueEmitContext& context, const IrValue& instruction, const RuntimeDescriptor& resource, std::uint32_t components, bool store, std::uint32_t data) { + auto& state = context.state; + const auto& memory = context.Memory(instruction); + const auto u32 = TypeU32(state); + const auto valueType = components == 1u ? u32 : TypeU32Composite(state, components); + const auto resultType = store ? state.module.Type(spv::OpTypeVoid) : valueType; + const auto element = ResourceForDescriptor(state, DescriptorBindingKind::Buffers, memory.resource); + const std::array key{memory.resource, components, store ? 1u : 0u, memory.d16 ? 1u : 0u, memory.coherent ? 1u : 0u, static_cast(memory.kind)}; + auto function = state.formattedBufferFunctions.find(key); + if (function == state.formattedBufferFunctions.end()) { + const auto id = state.module.AllocateId(); + const auto functionType = store ? state.module.Type(spv::OpTypeFunction, resultType, u32, u32, u32, u32, u32, u32, u32, u32, u32, valueType) : state.module.Type(spv::OpTypeFunction, resultType, u32, u32, u32, u32, u32, u32, u32, u32, u32); + const auto label = state.currentLabel; + const auto byteOffset = state.memoryByteOffsets.at(element); + struct Restore { + SpirvEmitterState& state; + std::uint32_t element; + std::uint32_t label; + std::uint32_t byteOffset; + ~Restore() { + state.module.EndHelperFunction(); + state.memoryByteOffsets.at(element) = byteOffset; + state.currentLabel = label; + } + }; + state.module.BeginHelperFunction(); + const Restore restore{state, element, label, byteOffset}; + state.module.AddFunction(spv::OpFunction, resultType, id, spv::FunctionControlDontInlineMask, functionType); + std::array parameters{}; + for (std::uint32_t index = 0; index < (store ? 10u : 9u); ++index) { + parameters[index] = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionParameter, index == 9u ? valueType : u32, parameters[index]); + } + EmitLabel(state, state.module.AllocateId()); + state.memoryByteOffsets.at(element) = parameters[4]; + const auto first = PipelineSpecialization::BufferBase + memory.resource * PipelineSpecialization::BufferWords; + RuntimeDescriptor shared{}; + if (state.registrationModules) { + const auto word3 = state.program.Info().buffers.at(memory.resource).descriptorFormatted ? state.module.SpecializationConstant(u32, first + 1u, 0u) : parameters[6]; + shared = {ConstantU32(state, 0u), parameters[3], parameters[5], word3, nonzero(state, parameters[7]), parameters[0], nonzero(state, parameters[8]), parameters[1], parameters[2]}; + } else { + const auto word1 = state.module.SpecializationConstant(u32, first, 0u); + const auto stride = field(state, word1, 16u, 14u); + const auto swizzle = both(state, nonzero(state, stride), nonzero(state, field(state, word1, 31u, 1u))); + const auto valid = nonzero(state, state.module.SpecializationConstant(u32, first + 2u, 0u)); + shared = {ConstantU32(state, 0u), parameters[3], stride, state.module.SpecializationConstant(u32, first + 1u, 0u), swizzle, parameters[0], valid, parameters[1], parameters[2]}; + } + const auto value = formatSwitch(context, instruction, shared, components, store, store ? parameters[9] : 0u); + if (store) state.module.AddFunction(spv::OpReturn); + else state.module.AddFunction(spv::OpReturnValue, value); + state.module.AddFunction(spv::OpFunctionEnd); + function = state.formattedBufferFunctions.emplace(key, id).first; + } + const auto offset = Binary(state, spv::OpIAdd, u32, context.Arg(instruction, 2u), ConstantU32(state, memory.offset)); + const auto result = state.module.AllocateId(); + const auto flag = [&](std::uint32_t condition) { return Select(state, u32, condition, ConstantU32(state, 1u), ConstantU32(state, 0u)); }; + const auto swizzle = flag(resource.swizzle); + const auto valid = flag(resource.valid); + if (store) state.module.AddFunction(spv::OpFunctionCall, resultType, result, function->second, resource.index, offset, context.Arg(instruction, 3u), resource.records, state.memoryByteOffsets.at(element), resource.stride, resource.word3, swizzle, valid, data); + else state.module.AddFunction(spv::OpFunctionCall, resultType, result, function->second, resource.index, offset, context.Arg(instruction, 3u), resource.records, state.memoryByteOffsets.at(element), resource.stride, resource.word3, swizzle, valid); + return store ? ConstantU32(state, 0u) : result; +} + +std::uint32_t formattedGpuCall(SpirvValueEmitContext& context, const IrValue& instruction, const RuntimeDescriptor& resource, std::uint32_t components, bool store, std::uint32_t data) { + auto& state = context.state; + const auto& memory = context.Memory(instruction); + const auto u32 = TypeU32(state); + const auto u64 = TypeScalarU64(state); + const auto boolean = TypeBool(state); + const bool stops = state.bdaStopsInvocations; + const auto valueType = components == 1u ? u32 : TypeU32Composite(state, components); + const auto resultType = stops ? (store ? u32 : state.module.Type(spv::OpTypeStruct, valueType, u32)) : (store ? state.module.Type(spv::OpTypeVoid) : valueType); + const std::array key{components, store ? 1u : 0u, memory.d16 ? 1u : 0u, memory.coherent ? 1u : 0u, static_cast(memory.kind), stops ? 1u : 0u}; + auto function = state.formattedGpuBufferFunctions.find(key); + if (function == state.formattedGpuBufferFunctions.end()) { + const auto id = state.module.AllocateId(); + const auto functionType = store ? state.module.Type(spv::OpTypeFunction, resultType, u64, u32, u32, u32, boolean, u32, boolean, u32, u32, u32, valueType) + : state.module.Type(spv::OpTypeFunction, resultType, u64, u32, u32, u32, boolean, u32, boolean, u32, u32, u32); + struct Restore { + SpirvEmitterState& state; + std::uint32_t label; + std::uint32_t stopValue; + ~Restore() { + state.module.EndHelperFunction(); + state.currentLabel = label; + state.bdaStopValue = stopValue; + state.bdaPcOverride = 0u; + } + }; + state.module.BeginHelperFunction(); + const Restore restore{state, state.currentLabel, state.bdaStopValue}; + state.module.AddName(id, std::string(store ? "store" : "load") + "_formatted_gpu_buffer_x" + std::to_string(components)); + state.module.AddFunction(spv::OpFunction, resultType, id, spv::FunctionControlDontInlineMask, functionType); + const std::array types{u64, u32, u32, u32, boolean, u32, boolean, u32, u32, u32, valueType}; + std::array parameters{}; + for (std::uint32_t index = 0; index < (store ? 11u : 10u); ++index) { + parameters[index] = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionParameter, types[index], parameters[index]); + } + EmitLabel(state, state.module.AllocateId()); + const auto zero = components == 1u ? ConstantU32(state, 0u) : ConstantU32CompositeZero(state, components); + if (stops) state.bdaStopValue = store ? ConstantU32(state, 1u) : state.module.Constant(spv::OpConstantComposite, resultType, zero, ConstantU32(state, 1u)); + state.bdaPcOverride = parameters[9]; + const RuntimeDescriptor shared{parameters[0], parameters[1], parameters[2], parameters[3], parameters[4], parameters[5], parameters[6], parameters[7], parameters[8]}; + const auto value = formatSwitch(context, instruction, shared, components, store, store ? parameters[10] : 0u); + if (!stops && store) { + state.module.AddFunction(spv::OpReturn); + } else { + auto returned = stops && store ? ConstantU32(state, 0u) : value; + if (stops && !store) { + returned = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, resultType, returned, value, ConstantU32(state, 0u)); + } + state.module.AddFunction(spv::OpReturnValue, returned); + } + state.module.AddFunction(spv::OpFunctionEnd); + function = state.formattedGpuBufferFunctions.emplace(key, id).first; + } + const auto offset = Binary(state, spv::OpIAdd, u32, context.Arg(instruction, 2u), ConstantU32(state, memory.offset)); + std::vector call{spv::OpFunctionCall, resultType, state.module.AllocateId(), function->second, resource.base, resource.records, resource.stride, resource.word3, resource.swizzle, resource.index, resource.valid, offset, context.Arg(instruction, 3u), BdaInstructionPc(state, instruction)}; + if (store) call.push_back(data); + state.module.AddFunction(call); + const auto result = call[2]; + if (!stops) return store ? ConstantU32(state, 0u) : result; + if (store) { + StopBdaInvocationIf(state, Binary(state, spv::OpINotEqual, boolean, result, ConstantU32(state, 0u))); + return ConstantU32(state, 0u); + } + const auto stopped = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, u32, stopped, result, 1u); + StopBdaInvocationIf(state, Binary(state, spv::OpINotEqual, boolean, stopped, ConstantU32(state, 0u))); + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, valueType, value, result, 0u); + 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) { @@ -383,24 +753,20 @@ std::uint32_t EmitRuntimeBufferLoad(SpirvValueEmitContext& context, const IrValu const auto zero = components == 1u ? ConstantU32(state, 0u) : ConstantU32CompositeZero(state, components); return EmitValueOrDefaultIfCondition(state, context.Arg(instruction, instruction.ArgumentCount() - 1u), type, zero, [&] { const auto resource = descriptor(context, instruction); - if (memory.formatted) return EmitValueOrDefaultIfCondition(state, resource.valid, type, zero, [&] { return formattedDispatch(context, instruction, resource, components, false); }); + if (memory.formatted) return EmitValueOrDefaultIfCondition(state, resource.valid, type, zero, [&] { return formattedDispatch(context, instruction, resource, components, false, 0u); }); const auto readComponents = [&] { 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)); @@ -415,14 +781,29 @@ void EmitRuntimeBufferStore(SpirvValueEmitContext& context, const IrValue& instr EmitIfCondition(state, context.Arg(instruction, instruction.ArgumentCount() - 1u), [&] { const auto resource = descriptor(context, instruction); if (memory.formatted) { - EmitIfCondition(state, resource.valid, [&] { static_cast(formattedDispatch(context, instruction, resource, components, true)); }); + EmitIfCondition(state, resource.valid, [&] { static_cast(formattedDispatch(context, instruction, resource, components, true, storeData(context, instruction))); }); return; } - const auto values = storeValues(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 values = storeValues(state, storeData(context, instruction), components); + 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); }); } @@ -438,7 +819,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); }); } @@ -446,6 +827,11 @@ std::uint32_t EmitRuntimeBufferAtomic(SpirvValueEmitContext& context, const IrVa auto& state = context.state; if (bits != 32u && bits != 64u) context.Fail(instruction, "unsupported runtime buffer atomic width"); if (context.Memory(instruction).gpuDescriptor && (state.bdaAtomicPointerFunction == 0u || state.bdaNoteWriteFunction == 0u)) context.Fail(instruction, "BDA atomic functions are missing"); + if (!context.Memory(instruction).gpuDescriptor) { + if (state.program.Resources().stage == IrShaderStage::TessellationControl) context.Fail(instruction, "BDA fault termination requires a barrier-safe tessellation-control execution protocol"); + if (std::ranges::find(state.supportedCapabilities, spv::CapabilityInt64) == state.supportedCapabilities.end()) context.Fail(instruction, "buffer atomic fault addresses require shaderInt64"); + state.module.EmitCapability(spv::CapabilityInt64); + } const auto type = bits == 64u ? TypeU64(state) : TypeU32(state); const auto scalar = bits == 64u ? TypeScalarU64(state) : TypeU32(state); const auto zero = bits == 64u ? ConstantU64(state, 0u) : ConstantU32(state, 0u); @@ -456,29 +842,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)); - faultIf(context, instruction, unaligned, guest, bits / 8u, BdaAbi::FaultReason::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), ConstantU32(state, instruction.Flags().pc)); + 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 2a62cbcde3..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); @@ -220,20 +272,318 @@ bool VisitInputs(const Instruction& instruction, const TVisitor& visit) { return true; } +template +void VisitIdOperands(const Instruction& instruction, std::uint32_t switchLiteralWords, const TVisitor& visit) { + const auto size = instruction.size(); + const auto from = [&](std::size_t first) { + for (auto index = first; index < size; ++index) visit(index); + }; + const auto only = [&](std::initializer_list indices) { + for (const auto index : indices) { + if (index < size) visit(index); + } + }; + constexpr std::uint32_t knownMemoryAccess = static_cast(spv::MemoryAccessVolatileMask) | static_cast(spv::MemoryAccessAlignedMask) | + static_cast(spv::MemoryAccessNontemporalMask) | static_cast(spv::MemoryAccessMakePointerAvailableMask) | + static_cast(spv::MemoryAccessMakePointerVisibleMask) | static_cast(spv::MemoryAccessNonPrivatePointerMask); + const auto memoryOperands = [&](std::size_t index) { + while (index < size) { + const auto mask = instruction[index++]; + if ((mask & ~knownMemoryAccess) != 0u) { + from(index); + return; + } + if ((mask & static_cast(spv::MemoryAccessAlignedMask)) != 0u) ++index; + if ((mask & static_cast(spv::MemoryAccessMakePointerAvailableMask)) != 0u && index < size) visit(index++); + if ((mask & static_cast(spv::MemoryAccessMakePointerVisibleMask)) != 0u && index < size) visit(index++); + } + }; + switch (Opcode(instruction)) { + case spv::OpNop: + case spv::OpCapability: + case spv::OpExtension: + case spv::OpExtInstImport: + case spv::OpMemoryModel: + case spv::OpSource: + case spv::OpSourceContinued: + case spv::OpSourceExtension: + case spv::OpString: + case spv::OpName: + case spv::OpMemberName: + case spv::OpModuleProcessed: + case spv::OpDecorate: + case spv::OpMemberDecorate: + case spv::OpDecorateString: + case spv::OpMemberDecorateString: + case spv::OpTypeVoid: + case spv::OpTypeBool: + case spv::OpTypeInt: + case spv::OpTypeFloat: + case spv::OpTypeSampler: + case spv::OpConstantTrue: + case spv::OpConstantFalse: + case spv::OpConstant: + case spv::OpConstantSampler: + case spv::OpConstantNull: + case spv::OpSpecConstantTrue: + case spv::OpSpecConstantFalse: + case spv::OpSpecConstant: + case spv::OpLabel: + case spv::OpNoLine: + case spv::OpReturn: + case spv::OpUnreachable: + case spv::OpKill: + case spv::OpFunctionEnd: + return; + case spv::OpTypeVector: + case spv::OpTypeMatrix: + case spv::OpTypeImage: + only({2}); + return; + case spv::OpTypePointer: + only({3}); + return; + case spv::OpTypeForwardPointer: + case spv::OpExecutionMode: + case spv::OpLine: + case spv::OpSelectionMerge: + only({1}); + return; + case spv::OpSpecConstantOp: + only({1}); + from(4); + return; + case spv::OpVariable: + case spv::OpFunction: + only({1, 4}); + return; + case spv::OpLoad: + only({1, 3}); + memoryOperands(4); + return; + case spv::OpStore: + case spv::OpCopyMemory: + only({1, 2}); + memoryOperands(3); + return; + case spv::OpCopyMemorySized: + only({1, 2, 3}); + memoryOperands(4); + return; + case spv::OpArrayLength: + case spv::OpCompositeExtract: + only({1, 3}); + return; + case spv::OpCompositeInsert: + case spv::OpVectorShuffle: + only({1, 3, 4}); + return; + case spv::OpExtInst: + only({1, 3}); + from(5); + return; + case spv::OpEntryPoint: { + only({2}); + std::size_t index = 3; + while (index < size) { + const auto word = instruction[index++]; + if ((word & 0xffu) == 0u || (word & 0xff00u) == 0u || (word & 0xff0000u) == 0u || (word & 0xff000000u) == 0u) break; + } + from(index); + return; + } + case spv::OpExecutionModeId: + case spv::OpDecorateId: + only({1}); + from(3); + return; + case spv::OpLoopMerge: + only({1, 2}); + return; + case spv::OpBranchConditional: + only({1, 2, 3}); + return; + case spv::OpSwitch: + only({1, 2}); + for (std::size_t index = 3; index + switchLiteralWords < size; index += switchLiteralWords + 1u) visit(index + switchLiteralWords); + return; + case spv::OpImageSampleImplicitLod: + case spv::OpImageSampleExplicitLod: + case spv::OpImageSampleProjImplicitLod: + case spv::OpImageSampleProjExplicitLod: + case spv::OpImageFetch: + case spv::OpImageRead: + case spv::OpImageSparseSampleImplicitLod: + case spv::OpImageSparseSampleExplicitLod: + case spv::OpImageSparseSampleProjImplicitLod: + case spv::OpImageSparseSampleProjExplicitLod: + case spv::OpImageSparseFetch: + case spv::OpImageSparseRead: + only({1, 3, 4}); + from(6); + return; + case spv::OpImageSampleDrefImplicitLod: + case spv::OpImageSampleDrefExplicitLod: + case spv::OpImageSampleProjDrefImplicitLod: + case spv::OpImageSampleProjDrefExplicitLod: + case spv::OpImageGather: + case spv::OpImageDrefGather: + case spv::OpImageSparseSampleDrefImplicitLod: + case spv::OpImageSparseSampleDrefExplicitLod: + case spv::OpImageSparseSampleProjDrefImplicitLod: + case spv::OpImageSparseSampleProjDrefExplicitLod: + case spv::OpImageSparseGather: + case spv::OpImageSparseDrefGather: + only({1, 3, 4, 5}); + from(7); + return; + case spv::OpImageWrite: + only({1, 2, 3}); + from(5); + return; + case spv::OpGroupIAdd: + case spv::OpGroupFAdd: + case spv::OpGroupFMin: + case spv::OpGroupUMin: + case spv::OpGroupSMin: + case spv::OpGroupFMax: + case spv::OpGroupUMax: + case spv::OpGroupSMax: + case spv::OpGroupNonUniformBallotBitCount: + case spv::OpGroupNonUniformIAdd: + case spv::OpGroupNonUniformFAdd: + case spv::OpGroupNonUniformIMul: + case spv::OpGroupNonUniformFMul: + case spv::OpGroupNonUniformSMin: + case spv::OpGroupNonUniformUMin: + case spv::OpGroupNonUniformFMin: + case spv::OpGroupNonUniformSMax: + case spv::OpGroupNonUniformUMax: + case spv::OpGroupNonUniformFMax: + case spv::OpGroupNonUniformBitwiseAnd: + case spv::OpGroupNonUniformBitwiseOr: + case spv::OpGroupNonUniformBitwiseXor: + case spv::OpGroupNonUniformLogicalAnd: + case spv::OpGroupNonUniformLogicalOr: + case spv::OpGroupNonUniformLogicalXor: + only({1, 3}); + from(5); + return; + default: + from(1); + return; + } +} + struct ScalarType { std::uint32_t width; bool boolean; }; +template +class IdTable { +public: + void Reset(std::size_t bound) { + items.assign(bound, T{}); + present.assign(bound, 0u); + count = 0; + } + bool contains(std::uint32_t id) const { return id < present.size() && present[id] != 0u; } + const T* find(std::uint32_t id) const { return contains(id) ? &items[id] : nullptr; } + const T& at(std::uint32_t id) const { + if (!contains(id)) throw std::out_of_range("prepared SPIR-V id has no entry"); + return items[id]; + } + T& at(std::uint32_t id) { + if (!contains(id)) throw std::out_of_range("prepared SPIR-V id has no entry"); + return items[id]; + } + bool emplace(std::uint32_t id, T value) { + check(id); + if (present[id] != 0u) return false; + items[id] = std::move(value); + present[id] = 1u; + ++count; + return true; + } + T& operator[](std::uint32_t id) { + check(id); + if (present[id] == 0u) { + items[id] = T{}; + present[id] = 1u; + ++count; + } + return items[id]; + } + void erase(std::uint32_t id) { + if (!contains(id)) return; + present[id] = 0u; + items[id] = T{}; + --count; + } + std::size_t size() const { return count; } + bool empty() const { return count == 0; } + class Iterator { + public: + Iterator(IdTable& table, std::uint32_t id) : table(table), id(id) { skip(); } + std::pair operator*() const { return {id, table.items[id]}; } + Iterator& operator++() { + ++id; + skip(); + return *this; + } + bool operator!=(const Iterator& other) const { return id != other.id; } + + private: + void skip() { + while (id < table.present.size() && table.present[id] == 0u) ++id; + } + IdTable& table; + std::uint32_t id; + }; + Iterator begin() { return Iterator(*this, 0u); } + Iterator end() { return Iterator(*this, static_cast(present.size())); } + +private: + void check(std::uint32_t id) const { + if (id >= present.size()) throw std::runtime_error("prepared SPIR-V id exceeds the module bound"); + } + std::vector items; + std::vector present; + std::size_t count = 0; +}; + +class IdSet { +public: + void Reset(std::size_t bound) { present.assign(bound, 0u); } + bool contains(std::uint32_t id) const { return id < present.size() && present[id] != 0u; } + bool insert(std::uint32_t id) { + if (id >= present.size()) throw std::runtime_error("prepared SPIR-V id exceeds the module bound"); + if (present[id] != 0u) return false; + present[id] = 1u; + return true; + } + +private: + std::vector present; +}; + class Specialization { public: - explicit Specialization(std::span words) { + 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]; + types.Reset(bound); + values.Reset(bound); + 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; @@ -242,8 +592,10 @@ public: if (hasResult && hasType) resultTypes.emplace(instruction.at(2), instruction.at(1)); if (op == spv::OpTypeBool) types.emplace(instruction.at(1), ScalarType{1u, true}); if (op == spv::OpTypeInt) types.emplace(instruction.at(1), ScalarType{instruction.at(2), false}); + 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; } } @@ -255,7 +607,10 @@ public: changed |= prune(); changed |= propagateCopies(); } + shareConstants(); removeDeadComputations(); + removeUnreachableBlocks(); + removeUnusedConstants(); orderPhis(); std::vector words = header; bool inserted = false; @@ -273,15 +628,20 @@ public: private: std::optional value(std::uint32_t id) const { - const auto found = values.find(id); - return found != values.end() ? std::optional(found->second) : std::nullopt; + const auto* found = values.find(id); + return found != nullptr ? std::optional(*found) : std::nullopt; + } + + 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; - const auto type = types.find(instruction[1]); - if (type == types.end() || type->second.width > 32u) return std::nullopt; + const auto* type = types.find(instruction[1]); + if (type == nullptr || type->width > 32u) return std::nullopt; const auto left = value(instruction[3]); if (!left) return std::nullopt; if (op == spv::OpCopyObject) return left; @@ -299,8 +659,8 @@ private: case spv::OpBitwiseAnd: return *left & *right; case spv::OpBitwiseOr: return *left | *right; case spv::OpBitwiseXor: return *left ^ *right; - case spv::OpShiftRightLogical: return *right < type->second.width ? std::optional(*left >> *right) : std::nullopt; - case spv::OpShiftLeftLogical: return *right < type->second.width ? std::optional(*left << *right) : std::nullopt; + case spv::OpShiftRightLogical: return *right < type->width ? std::optional(*left >> *right) : std::nullopt; + case spv::OpShiftLeftLogical: return *right < type->width ? std::optional(*left << *right) : std::nullopt; case spv::OpIEqual: return *left == *right; case spv::OpINotEqual: return *left != *right; case spv::OpULessThan: return *left < *right; @@ -326,7 +686,10 @@ private: bool fold() { bool changed = false; bool function = false; - std::map> extracts; + IdTable> extracts; + extracts.Reset(bound); + IdTable bitcasts; + bitcasts.Reset(bound); for (auto& instruction : instructions) { if (instruction.empty()) continue; auto op = Opcode(instruction); @@ -345,17 +708,27 @@ 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) { - const auto first = extracts.find(instruction[3]); - bool shuffle = first != extracts.end(); + const auto* first = extracts.find(instruction[3]); + bool shuffle = first != nullptr && vectorTypes.contains(instruction[1]) && resultTypes.contains(first->first) && vectorTypes.contains(resultTypes.at(first->first)); for (std::size_t index = 4; shuffle && index < instruction.size(); ++index) { - const auto found = extracts.find(instruction[index]); - shuffle = found != extracts.end() && found->second.first == first->second.first; + const auto* found = extracts.find(instruction[index]); + shuffle = found != nullptr && found->first == first->first; } if (shuffle) { - const auto source = first->second.first; + const auto source = first->first; bool identity = resultTypes.contains(source) && resultTypes.at(source) == instruction[1]; Instruction replacement = Make(spv::OpVectorShuffle, {instruction[1], instruction[2], source, source}); for (std::size_t index = 3; index < instruction.size(); ++index) { @@ -368,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; @@ -381,7 +757,8 @@ private: } bool prune() { - std::map> blocks; + IdTable> blocks; + blocks.Reset(bound); std::vector entries; std::uint32_t block = 0; bool entry = false; @@ -416,7 +793,18 @@ private: } return true; }; - std::map> edges; + IdSet loopHeaders; + loopHeaders.Reset(bound); + std::map continueHeaders; + for (const auto& [label, indices] : blocks) { + for (const auto index : indices) { + if (Opcode(instructions[index]) != spv::OpLoopMerge) continue; + loopHeaders.insert(label); + continueHeaders.emplace(instructions[index].at(2), label); + } + } + IdTable> edges; + edges.Reset(bound); for (const auto& [label, indices] : blocks) { auto& terminal = instructions[indices.back()]; auto op = Opcode(terminal); @@ -424,7 +812,7 @@ private: bool loop = false; for (const auto index : indices) loop |= Opcode(instructions[index]) == spv::OpLoopMerge; if (!loop && op == spv::OpBranchConditional) { - if (const auto condition = value(terminal.at(1))) target = terminal.at(*condition != 0u ? 2u : 3u); + if (const auto condition = value(terminal.at(1)); condition.has_value() && !loopHeaders.contains(terminal.at(*condition != 0u ? 3u : 2u))) target = terminal.at(*condition != 0u ? 2u : 3u); } else if (!loop && op == spv::OpSwitch) { if (const auto selector = value(terminal.at(1))) { target = terminal.at(2); @@ -440,8 +828,8 @@ private: retainMerge = !linearSelection(target, instruction.at(1)); if (!retainMerge) instruction.clear(); } - const auto replacement = !retainMerge ? Make(spv::OpBranch, {target}) : op == spv::OpSwitch ? - Make(spv::OpSwitch, {terminal.at(1), target}) : Make(spv::OpBranchConditional, {terminal.at(1), target, target}); + const auto replacement = !retainMerge ? Make(spv::OpBranch, {target}) : op == spv::OpSwitch ? Make(spv::OpSwitch, {terminal.at(1), target}) : + header.at(1) >= 0x00010600u ? terminal : Make(spv::OpBranchConditional, {terminal.at(1), target, target}); if (terminal != replacement) { terminal = replacement; changed = true; @@ -453,22 +841,39 @@ private: else if (op == spv::OpBranchConditional) successors = {terminal.at(2), terminal.at(3)}; else if (op == spv::OpSwitch) { successors.push_back(terminal.at(2)); - const auto selectorType = resultTypes.find(terminal.at(1)); - const auto stride = selectorType != resultTypes.end() && types.contains(selectorType->second) && types.at(selectorType->second).width == 64u ? 3u : 2u; + const auto* selectorType = resultTypes.find(terminal.at(1)); + const auto stride = selectorType != nullptr && types.contains(*selectorType) && types.at(*selectorType).width == 64u ? 3u : 2u; for (std::size_t index = 3; index + stride <= terminal.size(); index += stride) successors.push_back(terminal[index + stride - 1u]); } } - std::set live; + IdSet live; + live.Reset(bound); auto pending = entries; while (!pending.empty()) { const auto label = pending.back(); pending.pop_back(); if (!blocks.contains(label)) throw std::runtime_error("prepared branch references a missing block"); - if (!live.insert(label).second) continue; + if (!live.insert(label)) continue; const auto& successors = edges.at(label); pending.insert(pending.end(), successors.begin(), successors.end()); } + const auto backEdge = [&](std::uint32_t label) { + const auto header = continueHeaders.find(label); + return !live.contains(label) && header != continueHeaders.end() && live.contains(header->second) ? header->second : 0u; + }; for (const auto& [label, indices] : blocks) { + if (const auto header = backEdge(label)) { + const auto branch = Make(spv::OpBranch, {header}); + if (indices.size() == 2u && instructions[indices.back()] == branch) continue; + for (std::size_t index = 1; index < indices.size(); ++index) { + auto& instruction = instructions[indices[index]]; + if (const auto result = Result(instruction)) removed.insert(result); + instruction.clear(); + } + instructions[indices.back()] = branch; + changed = true; + continue; + } if (!live.contains(label)) { if (indices.size() == 2u && Opcode(instructions[indices.back()]) == spv::OpUnreachable) continue; for (std::size_t index = 1; index < indices.size(); ++index) { @@ -487,9 +892,10 @@ private: for (std::size_t operand = 3; operand + 1u < instruction.size(); operand += 2u) { const auto parent = instruction[operand + 1u]; if (live.contains(parent) && std::ranges::find(edges.at(parent), label) != edges.at(parent).end()) phi.insert(phi.end(), {instruction[operand], parent}); + else if (backEdge(parent) == label) phi.insert(phi.end(), {instruction[2], parent}); } if (phi.size() == 3u) throw std::runtime_error("reachable prepared phi has no predecessor"); - if (phi.size() == instruction.size() && phi.size() != 5u) continue; + if (phi == instruction && phi.size() != 5u) continue; phi[0] = (static_cast(phi.size()) << 16u) | spv::OpPhi; instruction = phi.size() == 5u ? Make(spv::OpCopyObject, {phi[1], phi[2], phi[3]}) : std::move(phi); changed = true; @@ -498,6 +904,54 @@ private: return changed; } + IdSet referencedIds() const { + IdSet referenced; + referenced.Reset(bound); + for (const auto& instruction : instructions) { + if (instruction.empty()) continue; + std::uint32_t switchLiteralWords = 1u; + if (Opcode(instruction) == spv::OpSwitch && instruction.size() > 1u) { + const auto* selectorType = resultTypes.find(instruction[1]); + if (selectorType != nullptr && types.contains(*selectorType) && types.at(*selectorType).width == 64u) switchLiteralWords = 2u; + } + VisitIdOperands(instruction, switchLiteralWords, [&](std::size_t index) { referenced.insert(instruction[index]); }); + } + return referenced; + } + + void removeUnreachableBlocks() { + const auto referenced = referencedIds(); + const auto next = [&](std::size_t index) { + while (++index < instructions.size() && instructions[index].empty()) {} + return index; + }; + std::size_t previous = instructions.size(); + for (std::size_t index = 0; index < instructions.size(); ++index) { + if (instructions[index].empty()) continue; + const auto op = Opcode(instructions[index]); + const auto following = next(index); + const bool entry = previous < instructions.size() && (Opcode(instructions[previous]) == spv::OpFunction || Opcode(instructions[previous]) == spv::OpFunctionParameter); + if (op == spv::OpLabel && !entry && following < instructions.size() && Opcode(instructions[following]) == spv::OpUnreachable && !referenced.contains(instructions[index].at(1))) { + removed.insert(instructions[index].at(1)); + instructions[index].clear(); + instructions[following].clear(); + index = following; + continue; + } + previous = index; + } + } + + void removeUnusedConstants() { + const auto referenced = referencedIds(); + std::erase_if(constants, [&](const Instruction& constant) { + const auto id = Result(constant); + if (referenced.contains(id)) return false; + removed.insert(id); + return true; + }); + } + void orderPhis() { std::vector ordered; std::vector copies; @@ -520,8 +974,25 @@ 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() { - std::map copies; + IdTable copies; + copies.Reset(bound); for (const auto& instruction : instructions) { if (Opcode(instruction) != spv::OpCopyObject) continue; if (instruction.size() != 4u) throw std::runtime_error("invalid prepared copy instruction"); @@ -566,7 +1037,8 @@ private: } void removeDeadComputations() { - std::map definitions; + IdTable definitions; + definitions.Reset(bound); std::vector pending; bool function = false; for (std::size_t index = 0; index < instructions.size(); ++index) { @@ -592,13 +1064,14 @@ private: const auto resultIndex = hasResult ? (hasType ? 2u : 1u) : 0u; for (std::size_t operand = 1; operand < instruction.size(); ++operand) if (operand != resultIndex) pending.push_back(instruction[operand]); } - std::set live; + IdSet live; + live.Reset(bound); while (!pending.empty()) { const auto id = pending.back(); pending.pop_back(); - const auto found = definitions.find(id); - if (found == definitions.end() || !live.insert(id).second) continue; - const auto& instruction = instructions[found->second]; + const auto* found = definitions.find(id); + if (found == nullptr || !live.insert(id)) continue; + const auto& instruction = instructions[*found]; if (!VisitInputs(instruction, [&](std::size_t operand) { pending.push_back(instruction[operand]); })) throw std::runtime_error("missing prepared pure instruction operands"); } for (const auto& [id, index] : definitions) { @@ -609,12 +1082,17 @@ private: } std::vector header; + WordArena arena; std::vector instructions; std::vector constants; - std::map types; - std::map values; - std::map resultTypes; - std::set removed; + std::uint32_t bound = 0; + IdTable types; + IdTable values; + 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/EmbeddedVertexFetch.hpp b/core/shader/recompiler/Translation/include/Translation/EmbeddedVertexFetch.hpp index 238056cd61..9729baeeb4 100644 --- a/core/shader/recompiler/Translation/include/Translation/EmbeddedVertexFetch.hpp +++ b/core/shader/recompiler/Translation/include/Translation/EmbeddedVertexFetch.hpp @@ -24,6 +24,8 @@ struct EmbeddedFetchPlan { bool instanceOffsetShared = false; bool vertexOffsetConflict = false; bool instanceOffsetConflict = false; + bool vertexIndexObserved = false; + bool instanceIndexObserved = false; }; class EmbeddedVertexFetchAnalyzer { diff --git a/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp b/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp index 3085542e0c..e54dfdec75 100644 --- a/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp +++ b/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp @@ -30,8 +30,12 @@ struct TranslateOptions { std::uint32_t userDataBaseRegister = 0; std::uint32_t userDataCount = 64; std::uint32_t scratchDwords = 0; + std::uint32_t sharedMemoryBytes = 0; std::uint64_t shaderHash = 0; bool fragmentShaderBarycentricEnabled = false; + bool interpolationFunctions = false; + bool nativeFloat16 = false; + bool float32FlushToZero = 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 bf5a58d69c..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,9 +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 { @@ -48,25 +58,42 @@ 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); + 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); - IrF32 flushTinyProduct(IrValue* lhs, IrValue* rhs, IrValue* product, IrValue* addend = nullptr); + IrF32 flushTinyProduct(IrValue* lhs, IrValue* rhs, IrValue* product, IrValue* addend = nullptr, bool afterRounding = true); IrU32 quietNan32(IrU32 bits); 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); @@ -81,6 +108,7 @@ private: IrU32 conditionBit(const RdnaOperand& operand); IrU1 readMask(const RdnaOperand& operand); IrU1 readMaskValid(const RdnaOperand& operand); + static bool maskLanesExact(const RdnaOperand& operand); std::array writeMask(const RdnaOperand& operand, IrU1 value, bool write64 = false); void writeCompareResult(const RdnaOperand& operand, IrU1 value); MemoryFlags addMemoryInfo(const MemoryInfo& memory, std::uint32_t pc); @@ -129,24 +157,33 @@ private: bool dsWrite2(const RdnaInstruction& inst); bool dsSrc2(const RdnaInstruction& inst, IrOpcode opcode); bool dsAtomic2(const RdnaInstruction& inst, IrOpcode opcode, bool returnsValue); + bool sharedFloatFlush(IrOpcode opcode) const; 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); bool dsBpermuteB32(const RdnaInstruction& inst); bool dsPermuteB32(const RdnaInstruction& inst); IrF32 selectF32(IrU1 condition, IrF32 trueValue, IrF32 falseValue); + IrU32 minMaxF16Bits(bool maxValue, IrU32 lhs, IrU32 rhs); IrU32 convertF32ToU32Saturated(IrF32 value, float upperBound, float safeUpper, std::uint32_t highResult); IrU32 convertF32ToI32Saturated(IrF32 value, float lowerBound, float upperBound, float safeUpper, std::uint32_t lowerResult, std::uint32_t upperResult); IrU32 convertFlooredF32ToI32(IrF32 source, IrF32 floored); + bool directedF32Rounding() const; + IrF32 convertMagnitudeToF32Directed(IrU32 magnitude, IrU32 signBits, std::uint32_t rounding, IrF32 native); + IrF32 convertU32ToF32(IrU32 source); + IrF32 convertS32ToF32(IrU32 source); IrU32 packU16Lanes(IrU32 low, IrU32 high); void emitCompareResult(const RdnaInstruction& inst, IrU1 value, bool scalar, bool cmpx); void emitCompareConstant(const RdnaInstruction& inst, bool value, bool scalar, bool cmpx); void emitIntegerCompare(const RdnaInstruction& inst, IrOpcode opcode, IrType type, bool scalar, bool cmpx); void emitInteger16Compare(const RdnaInstruction& inst, IrOpcode opcode, bool signedValue, bool cmpx); void emitFloatCompare(const RdnaInstruction& inst, IrOpcode opcode, bool half, bool cmpx, bool swap = false); + IrU1 floatCompareWithConstant(IrOpcode opcode, IrU32 variableBits, std::uint32_t constant); + IrU1 floatCompareKeepingDenormals(IrOpcode opcode, IrU32 lhsBits, IrU32 rhsBits); void emitInteger64Order(const RdnaInstruction& inst, bool signedValue, bool swap, bool negate, bool cmpx); void emitFloatOrderedCompare(const RdnaInstruction& inst, bool ordered, bool half, bool cmpx); void emitFloatClassCompare(const RdnaInstruction& inst, bool cmpx); @@ -177,15 +214,18 @@ 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); bool floatTernary(const RdnaInstruction& inst, IrOpcode opcode, bool accumulator, bool mix); bool ieeeMinMaxF32(const RdnaInstruction& inst, IrOpcode opcode); + bool minMaxF16(const RdnaInstruction& inst, IrOpcode opcode); bool vDivScaleF32(const RdnaInstruction& inst); bool vDivFmasF32(const RdnaInstruction& inst); bool vDivFixupF32(const RdnaInstruction& inst); bool vFrexpMantF32(const RdnaInstruction& inst); - bool vFmaLegacyF32(const RdnaInstruction& inst); + bool vFmaLegacyF32(const RdnaInstruction& inst, bool accumulate); bool vLdexpF32(const RdnaInstruction& inst); IrU32 readF16Bits(const RdnaOperand& operand); IrU32 normF16(IrU32 bits, bool signedValue); @@ -195,10 +235,13 @@ private: bool vCvtNormF16(const RdnaInstruction& inst, bool signedValue); bool vCvtPknormF16(const RdnaInstruction& inst, bool signedValue); bool vSatPkU8I16(const RdnaInstruction& inst); - bool vMulLegacyF32(const RdnaInstruction& inst, bool accumulate); + bool vMulLegacyF32(const RdnaInstruction& inst); bool vMullitF32(const RdnaInstruction& inst); void emitFloat16ClassCompare(const RdnaInstruction& inst, bool cmpx); bool float64Operation(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); bool vDivScaleF64(const RdnaInstruction& inst); bool vDivFmasF64(const RdnaInstruction& inst); @@ -213,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); @@ -226,6 +272,8 @@ private: bool packedInteger16MinMax(const RdnaInstruction& inst, IrOpcode opcode, bool sign); bool sU64Mask(const RdnaInstruction& inst, IrOpcode logicalOpcode, IrOpcode bitOpcode, bool negateRhs, bool negateResult, bool unary); IrU1 u64MaskBinary(const RdnaInstruction& inst, IrOpcode opcode, bool negateRhs, bool negateResult); + template + bool sU32Mask(const RdnaInstruction& inst, IrOpcode logicalOpcode, bool binary, bool negateRhs, bool negateResult, TWord&& word); bool simpleInteger(const RdnaInstruction& inst, IrOpcode opcode, IrType type, bool reverse, bool maskShiftCount, bool updateScc); bool composedIntegerBinary(const RdnaInstruction& inst, IrOpcode opcode, bool negateRhs, bool negateResult, bool updateScc); bool vAndOrB32(const RdnaInstruction& inst); @@ -293,6 +341,7 @@ private: void sSwappcB64(const RdnaInstruction& inst); void sCselectB32(const RdnaInstruction& inst); void scalarSelect64(const RdnaInstruction& inst, const RdnaOperand& falseSource); + void scalarSelectMask64(const RdnaInstruction& inst); void movB32(const RdnaInstruction& inst, bool applyFloatModifiers); void sMovB64(const RdnaInstruction& inst); void sWqm(const RdnaInstruction& inst, bool wide); @@ -301,7 +350,7 @@ private: void vMovrelsdB32(const RdnaInstruction& inst, bool split, bool swap); void vSwapB32(const RdnaInstruction& inst); IrU32 readRelativeVector(const RdnaOperand& base, IrValue& offset, const char* name); - void writeRelativeVector(const RdnaOperand& base, IrValue& offset, IrU32 value, const char* name); + void writeRelativeVector(const RdnaOperand& base, IrValue& offset, IrU32 value, const char* name, IrValue* enable = nullptr); void vReadfirstlaneB32(const RdnaInstruction& inst); void vReadlaneB32(const RdnaInstruction& inst); void vWritelaneB32(const RdnaInstruction& inst); @@ -315,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); @@ -324,12 +377,16 @@ private: const ShaderPixelInputInfo* pixelInput = nullptr; bool fragmentShaderBarycentricEnabled = false; std::optional floatMode; + std::optional declaredFloatMode; + RoundingState rounding; bool ieeeMode = false; + bool nativeFloat16 = false; IrBuilder ir; IrBlock& block; 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 aebe411e06..f06b151231 100644 --- a/core/shader/recompiler/Translation/src/AttributeInstructions.cpp +++ b/core/shader/recompiler/Translation/src/AttributeInstructions.cpp @@ -53,7 +53,10 @@ ExportFlags TranslationContext::addExportInfo(const RdnaInstruction& inst) { } void TranslationContext::vInterpP1F32(const RdnaInstruction& inst) { - if (!fragmentShaderBarycentricEnabled) return; + if (!fragmentShaderBarycentricEnabled) { + writeOperand(inst.destination, &ir.Emit(IrOpcode::InterpolateHostP1, IrType::F32, {&ir.Constant(inst.source1.value), &ir.Constant(inst.source2.value), readOperand(inst.source0, IrType::F32)})); + return; + } auto& delta = ir.Emit(IrOpcode::GetInterpolationParameter, IrType::U32, {&ir.Constant(inst.source1.value), &ir.Constant(inst.source2.value), &ir.Constant(0u)}); auto& origin = ir.Emit(IrOpcode::GetInterpolationParameter, IrType::U32, {&ir.Constant(inst.source1.value), &ir.Constant(inst.source2.value), &ir.Constant(2u)}); auto& product = ir.Emit(IrOpcode::FPMul32, IrType::F32, {&ir.BitCastF32(delta), readOperand(inst.source0, IrType::F32)}); @@ -75,12 +78,13 @@ void TranslationContext::vInterpP2F32(const RdnaInstruction& inst) { return base != ShaderPixelInputInfo::NoPixelInputVgpr && inst.source0.reg == base + 1u; }; const auto bit = 1u << inst.source1.value; - if (readsPair(PixelInput::LinearCenter) || readsPair(PixelInput::LinearCentroid)) { + if (readsPair(PixelInput::LinearSample) || readsPair(PixelInput::LinearCenter) || readsPair(PixelInput::LinearCentroid)) { program.Metadata().pixelLinearInputs |= bit; - } else if (readsPair(PixelInput::PerspectiveCenter) || readsPair(PixelInput::PerspectiveCentroid)) { + } else if (readsPair(PixelInput::PerspectiveSample) || readsPair(PixelInput::PerspectiveCenter) || readsPair(PixelInput::PerspectiveCentroid)) { program.Metadata().pixelPerspectiveInputs |= bit; } } + (void)ir.Emit(IrOpcode::InterpolateHostP2, IrType::Void, {&ir.Constant(inst.source1.value), &ir.Constant(inst.source2.value), readOperand(inst.source0, IrType::F32), readOperand(inst.destination, IrType::F32), &ir.GetExec()}); IrValue& value = ir.Emit(IrOpcode::GetAttribute, IrType::U32, {&ir.Constant(inst.source1.value), &ir.Constant(inst.source2.value)}); writeOperand(inst.destination, &value); } @@ -89,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)})); } @@ -124,7 +127,9 @@ void TranslationContext::vInterpP1F16(const RdnaInstruction& inst) { void TranslationContext::vInterpP2F16(const RdnaInstruction& inst) { const auto mode = interpolationModeF16(inst); const IrF32 delta = interpolationParameterF16(inst, 1u); - writeF16(inst.destination, IrF32(ir.Emit(IrOpcode::FPInterpolateF16, IrType::F32, {&delta.Value(), readOperand(inst.source0, IrType::F32), readOperand(inst.source3, IrType::F32), &ir.Constant(mode)}))); + IrValue* coordinate = readOperand(inst.source0, IrType::F32); + IrValue* partial = readOperand(inst.source3, IrType::F32); + writeF16(inst.destination, IrF32(ir.Emit(IrOpcode::FPInterpolateF16, IrType::F32, {&delta.Value(), coordinate, partial, &ir.Constant(mode)})), {&delta.Value(), coordinate, partial}); } void TranslationContext::eXP(const RdnaInstruction& inst) { diff --git a/core/shader/recompiler/Translation/src/CompareInstructions.cpp b/core/shader/recompiler/Translation/src/CompareInstructions.cpp index 9a87667291..a562ccc2d0 100644 --- a/core/shader/recompiler/Translation/src/CompareInstructions.cpp +++ b/core/shader/recompiler/Translation/src/CompareInstructions.cpp @@ -1,11 +1,80 @@ #include "Translation/CompareInstructions.hpp" #include "Translation/TranslationContext.hpp" #include +#include +#include #include #include namespace ShaderRecompiler { +namespace { + +std::optional constantBits(const IrValue& value) { + if (value.HasImmediate()) return value.ImmediateU32(); + const IrOpcode opcode = value.Opcode(); + if (opcode != IrOpcode::BitwiseAnd32 && opcode != IrOpcode::BitwiseOr32 && opcode != IrOpcode::BitwiseXor32) return std::nullopt; + const auto lhs = constantBits(*value.Argument(0)); + const auto rhs = constantBits(*value.Argument(1)); + if (!lhs.has_value() || !rhs.has_value()) return std::nullopt; + if (opcode == IrOpcode::BitwiseAnd32) return *lhs & *rhs; + if (opcode == IrOpcode::BitwiseOr32) return *lhs | *rhs; + return *lhs ^ *rhs; +} + +IrOpcode mirroredFloatCompare(IrOpcode opcode) { + switch (opcode) { + case IrOpcode::FPOrdLessThan32: return IrOpcode::FPOrdGreaterThan32; + case IrOpcode::FPUnordLessThan32: return IrOpcode::FPUnordGreaterThan32; + case IrOpcode::FPOrdGreaterThan32: return IrOpcode::FPOrdLessThan32; + case IrOpcode::FPUnordGreaterThan32: return IrOpcode::FPUnordLessThan32; + case IrOpcode::FPOrdLessThanEqual32: return IrOpcode::FPOrdGreaterThanEqual32; + case IrOpcode::FPUnordLessThanEqual32: return IrOpcode::FPUnordGreaterThanEqual32; + case IrOpcode::FPOrdGreaterThanEqual32: return IrOpcode::FPOrdLessThanEqual32; + case IrOpcode::FPUnordGreaterThanEqual32: return IrOpcode::FPUnordLessThanEqual32; + case IrOpcode::FPOrdEqual32: + case IrOpcode::FPUnordEqual32: + case IrOpcode::FPOrdNotEqual32: + case IrOpcode::FPUnordNotEqual32: return opcode; + default: throw std::runtime_error("f32 compare against a constant: unexpected compare opcode"); + } +} + +bool unorderedInfinityCompareIsTrue(IrOpcode opcode, bool constantIsLhs, std::uint32_t constant) { + if (constant == 0x7f800000u) { + return constantIsLhs ? opcode == IrOpcode::FPUnordGreaterThanEqual32 : opcode == IrOpcode::FPUnordLessThanEqual32; + } + if (constant == 0xff800000u) { + return constantIsLhs ? opcode == IrOpcode::FPUnordLessThanEqual32 : opcode == IrOpcode::FPUnordGreaterThanEqual32; + } + return false; +} + +struct OrderedRelation { + IrOpcode integerOpcode; + bool unordered; +}; + +OrderedRelation orderedRelationOf(IrOpcode opcode) { + switch (opcode) { + case IrOpcode::FPOrdEqual32: return {IrOpcode::IEqual32, false}; + case IrOpcode::FPUnordEqual32: return {IrOpcode::IEqual32, true}; + case IrOpcode::FPOrdNotEqual32: return {IrOpcode::INotEqual32, false}; + case IrOpcode::FPUnordNotEqual32: return {IrOpcode::INotEqual32, true}; + case IrOpcode::FPOrdLessThan32: return {IrOpcode::SLessThan32, false}; + case IrOpcode::FPUnordLessThan32: return {IrOpcode::SLessThan32, true}; + case IrOpcode::FPOrdLessThanEqual32: return {IrOpcode::SLessThanEqual32, false}; + case IrOpcode::FPUnordLessThanEqual32: return {IrOpcode::SLessThanEqual32, true}; + case IrOpcode::FPOrdGreaterThan32: return {IrOpcode::SGreaterThan32, false}; + case IrOpcode::FPUnordGreaterThan32: return {IrOpcode::SGreaterThan32, true}; + case IrOpcode::FPOrdGreaterThanEqual32: return {IrOpcode::SGreaterThanEqual32, false}; + case IrOpcode::FPUnordGreaterThanEqual32: return {IrOpcode::SGreaterThanEqual32, true}; + default: throw std::runtime_error("f32 compare keeping denormals: unexpected compare opcode"); + } +} + +} + void TranslateCompareInstruction(IrBuilder& builder, const RdnaInstruction& instruction) { throw std::runtime_error("TranslateCompareInstruction not implemented"); } @@ -46,16 +115,78 @@ 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 { - lhs = readOperand(sourceAt(inst, 0u), IrType::F32); - rhs = readOperand(sourceAt(inst, 1u), IrType::F32); + IrValue* lhsBits = readOperand(sourceAt(inst, 0u), IrType::U32); + IrValue* rhsBits = readOperand(sourceAt(inst, 1u), IrType::U32); + if (swap) std::swap(lhsBits, rhsBits); + const auto lhsConstant = constantBits(*lhsBits); + const auto rhsConstant = constantBits(*rhsBits); + if (lhsConstant.has_value() && !rhsConstant.has_value() && unorderedInfinityCompareIsTrue(opcode, true, *lhsConstant)) { + emitCompareConstant(inst, true, false, cmpx); + return; + } + if (!lhsConstant.has_value() && rhsConstant.has_value() && unorderedInfinityCompareIsTrue(opcode, false, *rhsConstant)) { + emitCompareConstant(inst, true, false, cmpx); + return; + } + if ((f32DenormalFlush & 1u) != 0u && lhsConstant.has_value() != rhsConstant.has_value()) { + const IrOpcode oriented = lhsConstant.has_value() ? mirroredFloatCompare(opcode) : opcode; + emitCompareResult(inst, floatCompareWithConstant(oriented, IrU32(lhsConstant.has_value() ? *rhsBits : *lhsBits), lhsConstant.has_value() ? *lhsConstant : *rhsConstant), false, cmpx); + return; + } + if ((f32DenormalFlush & 1u) == 0u) { + emitCompareResult(inst, floatCompareKeepingDenormals(opcode, IrU32(*lhsBits), IrU32(*rhsBits)), false, cmpx); + return; + } + lhs = &ir.BitCastF32(flushF32Denormal(IrU32(*lhsBits)).Value()); + rhs = &ir.BitCastF32(flushF32Denormal(IrU32(*rhsBits)).Value()); + emitCompareResult(inst, IrU1(ir.Emit(opcode, IrType::U1, {lhs, rhs})), false, cmpx); + return; } if (swap) std::swap(lhs, rhs); emitCompareResult(inst, IrU1(ir.Emit(opcode, IrType::U1, {lhs, rhs})), false, cmpx); } +IrU1 TranslationContext::floatCompareKeepingDenormals(IrOpcode opcode, IrU32 lhsBits, IrU32 rhsBits) { + const auto magnitude = [&](IrU32 bits) -> IrValue& { return ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffffffu)); }; + const auto order = [&](IrU32 bits) -> IrValue& { + IrValue& absolute = magnitude(bits); + IrValue& negative = ir.INotEqual(ir.BitwiseAnd(bits.Value(), ir.Constant(0x80000000u)), ir.Constant(0u)); + return ir.Select(negative, ir.ISub(ir.Constant(0u), absolute), absolute); + }; + const auto isNan = [&](IrU32 bits) -> IrValue& { return ir.UGreaterThan(magnitude(bits), ir.Constant(0x7f800000u)); }; + const OrderedRelation relation = orderedRelationOf(opcode); + IrValue& ordering = ir.Emit(relation.integerOpcode, IrType::U1, {&order(lhsBits), &order(rhsBits)}); + IrValue& eitherNan = ir.LogicalOr(isNan(lhsBits), isNan(rhsBits)); + return IrU1(relation.unordered ? ir.LogicalOr(eitherNan, ordering) : ir.LogicalAnd(ir.LogicalNot(eitherNan), ordering)); +} + +IrU1 TranslationContext::floatCompareWithConstant(IrOpcode opcode, IrU32 variableBits, std::uint32_t constant) { + IrValue& value = ir.BitCastF32(variableBits.Value()); + const auto compare = [&](IrOpcode compareOpcode, IrValue& lhs, std::uint32_t rhs) { return IrU1(ir.Emit(compareOpcode, IrType::U1, {&lhs, &ir.BitCastF32(ir.Constant(rhs))})); }; + if ((constant & 0x7f800000u) != 0u) return compare(opcode, value, constant); + IrValue& magnitude = ir.BitCastF32(ir.BitwiseAnd(variableBits.Value(), ir.Constant(0x7fffffffu))); + constexpr std::uint32_t MinNormal = 0x00800000u; + constexpr std::uint32_t NegativeMinNormal = 0x80800000u; + switch (opcode) { + case IrOpcode::FPOrdLessThan32: return compare(IrOpcode::FPOrdLessThanEqual32, value, NegativeMinNormal); + case IrOpcode::FPUnordLessThan32: return compare(IrOpcode::FPUnordLessThanEqual32, value, NegativeMinNormal); + case IrOpcode::FPOrdLessThanEqual32: return compare(IrOpcode::FPOrdLessThan32, value, MinNormal); + case IrOpcode::FPUnordLessThanEqual32: return compare(IrOpcode::FPUnordLessThan32, value, MinNormal); + case IrOpcode::FPOrdGreaterThan32: return compare(IrOpcode::FPOrdGreaterThanEqual32, value, MinNormal); + case IrOpcode::FPUnordGreaterThan32: return compare(IrOpcode::FPUnordGreaterThanEqual32, value, MinNormal); + case IrOpcode::FPOrdGreaterThanEqual32: return compare(IrOpcode::FPOrdGreaterThan32, value, NegativeMinNormal); + case IrOpcode::FPUnordGreaterThanEqual32: return compare(IrOpcode::FPUnordGreaterThan32, value, NegativeMinNormal); + case IrOpcode::FPOrdEqual32: return compare(IrOpcode::FPOrdLessThan32, magnitude, MinNormal); + case IrOpcode::FPUnordEqual32: return compare(IrOpcode::FPUnordLessThan32, magnitude, MinNormal); + case IrOpcode::FPOrdNotEqual32: return compare(IrOpcode::FPOrdGreaterThanEqual32, magnitude, MinNormal); + case IrOpcode::FPUnordNotEqual32: return compare(IrOpcode::FPUnordGreaterThanEqual32, magnitude, MinNormal); + default: throw std::runtime_error("f32 compare against a constant: unexpected compare opcode"); + } +} + void TranslationContext::emitInteger64Order(const RdnaInstruction& inst, bool signedValue, bool swap, bool negate, bool cmpx) { IrValue* lhs = readOperand(sourceAt(inst, swap ? 1u : 0u), IrType::U64); IrValue* rhs = readOperand(sourceAt(inst, swap ? 0u : 1u), IrType::U64); @@ -94,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 798eaf34c2..bb8094339a 100644 --- a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp +++ b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp @@ -13,7 +13,39 @@ void TranslateControlFlowInstruction(IrBuilder& builder, const RdnaInstruction& } void TranslationContext::sSubvectorLoop(const RdnaInstruction& inst, bool begin) { - throw std::runtime_error("GNM subvector loop instructions are not supported by this translator"); + if (program.WaveSize() != 64u) { + throw std::runtime_error(std::string(begin ? "s_subvector_loop_begin" : "s_subvector_loop_end") + " in a wave" + std::to_string(program.WaveSize()) + " shader is not measured, only wave64 subvector loops are supported"); + } + if (inst.destination.kind != RdnaOperandKind::ScalarRegister) { + throw std::runtime_error(std::string(begin ? "s_subvector_loop_begin" : "s_subvector_loop_end") + " with a destination that is not an SGPR is not measured"); + } + const IrU32 execLo(ir.GetExecLo()); + const IrU32 execHi(ir.GetExecHi()); + const IrU32 counter = readRawU32(inst.destination); + const IrU32 zero(ir.Constant(0u)); + IrU32 newExecLo = execLo; + IrU32 newExecHi = execHi; + IrU32 newCounter = counter; + if (begin) { + const IrU1 lowZero(ir.IEqual(execLo.Value(), zero.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())); + instructionBranchCondition = execZero; + } else { + const IrU1 highSet(ir.INotEqual(execHi.Value(), zero.Value())); + const IrU1 counterZero(ir.IEqual(counter.Value(), zero.Value())); + const IrU1 stay(ir.LogicalOr(highSet.Value(), counterZero.Value())); + newExecLo = IrU32(ir.Select(highSet.Value(), counter.Value(), ir.Select(counterZero.Value(), execLo.Value(), zero.Value()))); + newExecHi = IrU32(ir.Select(stay.Value(), execHi.Value(), counter.Value())); + newCounter = IrU32(ir.Select(stay.Value(), counter.Value(), execLo.Value())); + instructionBranchCondition = IrU1(ir.LogicalNot(stay.Value())); + } + writeRawU32(inst.destination, newCounter); + ir.SetExecLo(newExecLo.Value()); + ir.SetExecHi(newExecHi.Value()); + ir.SetExec(threadBit({newExecLo, newExecHi}).Value()); } // s_*_saveexec reads its source before it writes the old exec into the destination, so @@ -27,6 +59,7 @@ bool SaveexecWritesDestinationFirst() { void TranslationContext::sSaveexec(const RdnaInstruction& inst, IrOpcode operation, bool negateExec, bool negateSource, bool write64, bool negateResult, bool writeResult) { // Callers name the lane-wise operation; the exec mask words themselves combine bitwise. + const IrOpcode laneOperation = operation; if (operation == IrOpcode::LogicalAnd) operation = IrOpcode::BitwiseAnd32; else if (operation == IrOpcode::LogicalOr) operation = IrOpcode::BitwiseOr32; else if (operation == IrOpcode::LogicalXor) operation = IrOpcode::BitwiseXor32; @@ -55,19 +88,40 @@ void TranslationContext::sSaveexec(const RdnaInstruction& inst, IrOpcode operati return; } const IrU32 oldExec(ir.GetExecLo()); + const IrU1 oldLane(ir.GetExec()); const IrU32 savedExec = hostExecWord(0u); - if (writeFirst) writeRawU32(inst.destination, savedExec); + const bool lanes = program.WaveSize() == 32u && (execSource || maskLanesExact(sourceAt(inst, 0u))); + const auto writeSaved = [&](IrU32 word, IrU1 lane) { + writeRawU32(inst.destination, word); + if (lanes && inst.destination.kind == RdnaOperandKind::ScalarRegister) { + const auto reg = static_cast(inst.destination.reg); + ir.SetThreadBitScalarReg(reg, lane.Value()); + ir.SetScalarMaskTag(reg, ir.ConstantBool(true)); + } + }; + if (writeFirst) writeSaved(savedExec, oldLane); const IrU32 source = execSource ? oldExec : readU32(sourceAt(inst, 0u)); - if (!writeFirst && !writeResult) writeRawU32(inst.destination, savedExec); + IrU1 sourceLane; + if (lanes) sourceLane = execSource ? oldLane : readMask(sourceAt(inst, 0u)); + if (!writeFirst && !writeResult) writeSaved(savedExec, oldLane); IrValue& execOperand = negateExec ? ir.BitwiseNot(oldExec.Value()) : oldExec.Value(); IrValue& sourceOperand = negateSource ? ir.BitwiseNot(source.Value()) : source.Value(); IrValue& combined = ir.Emit(operation, IrType::U32, {&execOperand, &sourceOperand}); const IrU32 newExec(negateResult ? ir.BitwiseNot(combined) : combined); ir.SetExecLo(newExec.Value()); - if (writeResult) writeRawU32(inst.destination, hostExecWord(0u)); + IrU1 newLane; + if (lanes) { + IrValue& execLane = negateExec ? ir.LogicalNot(oldLane.Value()) : oldLane.Value(); + IrValue& sourceLaneOperand = negateSource ? ir.LogicalNot(sourceLane.Value()) : sourceLane.Value(); + IrValue& combinedLane = ir.Emit(laneOperation, IrType::U1, {&execLane, &sourceLaneOperand}); + newLane = IrU1(negateResult ? ir.LogicalNot(combinedLane) : combinedLane); + } else { + newLane = threadBit({newExec, IrU32(ir.GetExecHi())}); + } + if (writeResult) writeSaved(hostExecWord(0u), newLane); const IrU1 nonZero(ir.INotEqual(newExec.Value(), ir.Constant(0u))); ir.SetScc(nonZero.Value()); - ir.SetExec(threadBit({newExec, IrU32(ir.GetExecHi())}).Value()); + ir.SetExec(newLane.Value()); } void TranslationContext::addU32(const RdnaInstruction& inst, bool vector, bool useCarryIn) { @@ -228,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)); @@ -244,6 +305,21 @@ void TranslationContext::scalarSelect64(const RdnaInstruction& inst, const RdnaO writeU32Pair(inst.destination, {low, high}); } +void TranslationContext::scalarSelectMask64(const RdnaInstruction& inst) { + if (inst.destination.kind != RdnaOperandKind::ScalarRegister) { + scalarSelect64(inst, sourceAt(inst, 1u)); + return; + } + const RdnaOperand& trueSource = sourceAt(inst, 0u); + const RdnaOperand& falseSource = sourceAt(inst, 1u); + IrValue& valid = ir.LogicalAnd(readMaskValid(trueSource).Value(), readMaskValid(falseSource).Value()); + IrValue& bit = ir.Emit(IrOpcode::SelectU1, IrType::U1, {&ir.GetScc(), &readMask(trueSource).Value(), &readMask(falseSource).Value()}); + scalarSelect64(inst, falseSource); + const auto dst = static_cast(inst.destination.reg); + ir.SetThreadBitScalarReg(dst, bit); + ir.SetScalarMaskTag(dst, valid); +} + void TranslationContext::movB32(const RdnaInstruction& inst, bool applyFloatModifiers) { if (applyFloatModifiers) { IrF32 value(*readOperand(sourceAt(inst, 0u), IrType::F32)); @@ -278,31 +354,71 @@ void TranslationContext::sWqm(const RdnaInstruction& inst, bool wide) { ir.SetScc(ir.INotEqual(low.Value(), ir.Constant(0u))); } -// M0 holds a uniform register offset. Vector registers are SSA values with fixed indices, so an indexed -// access becomes a select over every register the program references at or past the base. +struct RelativeWindowAccess { + IrValue* index = nullptr; + IrValue* inRange = nullptr; +}; + +RelativeWindowAccess relativeWindowAccess(IrBuilder& ir, const IrProgram& program, std::uint32_t reg, IrValue& offset, const char* name, std::uint32_t vectorLimit) { + const auto& metadata = program.Metadata(); + if (metadata.vectorWindowSize == 0u) { + if (reg >= vectorLimit) throw std::runtime_error(std::string(name) + " addresses a register outside the relative-addressing span"); + return {nullptr, &ir.ULessThan(offset, ir.Constant(vectorLimit - reg))}; + } + if (metadata.vectorWindowSize == 0u || reg < metadata.vectorWindowBase) throw std::runtime_error(std::string(name) + " addresses a register outside the relative-addressing window"); + const auto first = reg - metadata.vectorWindowBase; + IrValue& index = ir.Emit(IrOpcode::UMin32, IrType::U32, {&ir.IAdd(ir.Constant(first), offset), &ir.Constant(metadata.vectorWindowSize - 1u)}); + return {&index, &ir.ULessThan(offset, ir.Constant(metadata.vectorWindowSize - first))}; +} + IrU32 TranslationContext::readRelativeVector(const RdnaOperand& base, IrValue& offset, const char* name) { if (base.kind != RdnaOperandKind::VectorRegister) { throw std::runtime_error(std::string(name) + " source is not a vector register"); } - IrU32 result(ir.GetVectorReg(static_cast(base.reg))); - for (std::uint32_t reg = base.reg + 1u; reg < currentVectorLimit; ++reg) { - IrValue& hit = ir.IEqual(offset, ir.Constant(reg - base.reg)); - result = IrU32(ir.Select(hit, ir.GetVectorReg(static_cast(reg)), result.Value())); + const auto access = relativeWindowAccess(ir, program, base.reg, offset, name, currentVectorLimit); + if (access.index == nullptr) { + IrU32 result(ir.GetVectorReg(static_cast(0))); + for (std::uint32_t reg = base.reg; reg < currentVectorLimit; ++reg) { + IrValue& hit = ir.IEqual(offset, ir.Constant(reg - base.reg)); + result = IrU32(ir.Select(hit, ir.GetVectorReg(static_cast(reg)), result.Value())); + } + return applyBitSourceModifiers(base, result); } - return applyBitSourceModifiers(base, result); + IrValue& loaded = ir.Emit(IrOpcode::LoadVectorWindow, IrType::U32, {access.index}); + return applyBitSourceModifiers(base, IrU32(ir.Select(*access.inRange, loaded, ir.GetVectorReg(static_cast(0))))); } -void TranslationContext::writeRelativeVector(const RdnaOperand& base, IrValue& offset, IrU32 value, const char* name) { +void TranslationContext::writeRelativeVector(const RdnaOperand& base, IrValue& offset, IrU32 value, const char* name, IrValue* enable) { const RdnaOperand& destination = base; if (destination.kind != RdnaOperandKind::VectorRegister) { throw std::runtime_error(std::string(name) + " destination is not a vector register"); } - for (std::uint32_t reg = destination.reg; reg < currentVectorLimit; ++reg) { - RdnaOperand target = destination; - target.reg = reg; - IrValue& hit = ir.IEqual(offset, ir.Constant(reg - destination.reg)); - writeRawU32(target, IrU32(ir.Select(hit, value.Value(), ir.GetVectorReg(static_cast(reg))))); + const auto access = relativeWindowAccess(ir, program, destination.reg, offset, name, currentVectorLimit); + if (access.index == nullptr) { + for (std::uint32_t reg = destination.reg; reg < currentVectorLimit; ++reg) { + IrValue& previous = ir.GetVectorReg(static_cast(reg)); + IrValue* merged = nullptr; + if (destination.dpp) { + const DppMoveFlags flags{destination.dpp8 ? DppMoveFlags::Lanes8 | destination.dppCtrl : destination.dppCtrl, static_cast(destination.dppRowMask), static_cast(destination.dppBankMask), destination.dppFetchInactive, destination.dppBoundCtrl}; + merged = &ir.Emit(IrOpcode::DppUpdateU32, IrType::U32, {&value.Value(), &previous, &ir.GetExec()}, flags); + } else merged = &ir.Select(ir.GetExec(), value.Value(), previous); + IrValue* written = &ir.IEqual(offset, ir.Constant(reg - destination.reg)); + if (enable != nullptr) written = &ir.LogicalAnd(*written, *enable); + ir.SetVectorReg(static_cast(reg), ir.Select(*written, *merged, previous)); + } + return; } + IrValue& previous = ir.Emit(IrOpcode::LoadVectorWindow, IrType::U32, {access.index}); + IrValue* merged = nullptr; + if (destination.dpp) { + const DppMoveFlags flags{destination.dpp8 ? DppMoveFlags::Lanes8 | destination.dppCtrl : destination.dppCtrl, static_cast(destination.dppRowMask), static_cast(destination.dppBankMask), destination.dppFetchInactive, destination.dppBoundCtrl}; + merged = &ir.Emit(IrOpcode::DppUpdateU32, IrType::U32, {&value.Value(), &previous, &ir.GetExec()}, flags); + } else { + merged = &ir.Select(ir.GetExec(), value.Value(), previous); + } + IrValue* written = access.inRange; + if (enable != nullptr) written = &ir.LogicalAnd(*written, *enable); + (void)ir.Emit(IrOpcode::StoreVectorWindow, IrType::Void, {access.index, &ir.Select(*written, *merged, previous)}); } void TranslationContext::vMovrelsB32(const RdnaInstruction& inst) { @@ -325,8 +441,9 @@ void TranslationContext::vMovrelsdB32(const RdnaInstruction& inst, bool split, b return; } const IrU32 previous = readRelativeVector(plainOperand(inst.destination), destinationOffset, name); - writeRelativeVector(inst.destination, destinationOffset, value, name); - writeRelativeVector(sourceAt(inst, 0u), sourceOffset, previous, name); + IrValue& bothInRange = ir.LogicalAnd(*relativeWindowAccess(ir, program, sourceAt(inst, 0u).reg, sourceOffset, name, currentVectorLimit).inRange, *relativeWindowAccess(ir, program, inst.destination.reg, destinationOffset, name, currentVectorLimit).inRange); + writeRelativeVector(inst.destination, destinationOffset, value, name, &bothInRange); + writeRelativeVector(sourceAt(inst, 0u), sourceOffset, previous, name, &bothInRange); } void TranslationContext::vSwapB32(const RdnaInstruction& inst) { diff --git a/core/shader/recompiler/Translation/src/ConvertInstructions.cpp b/core/shader/recompiler/Translation/src/ConvertInstructions.cpp index b0a367c0dc..d14b698c77 100644 --- a/core/shader/recompiler/Translation/src/ConvertInstructions.cpp +++ b/core/shader/recompiler/Translation/src/ConvertInstructions.cpp @@ -18,10 +18,10 @@ IrU32 TranslationContext::convertF32ToU32Saturated(IrF32 value, float upperBound const IrU1 nan(ir.Emit(IrOpcode::FPIsNan32, IrType::U1, {&value.Value()})); const IrU1 low(ir.Emit(IrOpcode::FPOrdLessThanEqual32, IrType::U1, {&value.Value(), &zero.Value()})); const IrU1 high(ir.Emit(IrOpcode::FPOrdGreaterThanEqual32, IrType::U1, {&value.Value(), &ir.ConstantF32(upperBound)})); - const IrF32 truncated(ir.Emit(IrOpcode::FPTrunc32, IrType::F32, {&value.Value()})); - const IrF32 safeLow = selectF32(IrU1(ir.LogicalOr(nan.Value(), low.Value())), zero, truncated); + const IrF32 safeLow = selectF32(IrU1(ir.LogicalOr(nan.Value(), low.Value())), zero, value); const IrF32 safe = selectF32(high, IrF32(ir.ConstantF32(safeUpper)), safeLow); const IrU32 converted(ir.Emit(IrOpcode::ConvertU32F32, IrType::U32, {&safe.Value()})); + if (static_cast(safeUpper) == highResult) return converted; return IrU32(ir.Select(high.Value(), ir.Constant(highResult), converted.Value())); } @@ -29,14 +29,13 @@ IrU32 TranslationContext::convertF32ToI32Saturated(IrF32 value, float lowerBound const IrU1 nan(ir.Emit(IrOpcode::FPIsNan32, IrType::U1, {&value.Value()})); const IrU1 low(ir.Emit(IrOpcode::FPOrdLessThanEqual32, IrType::U1, {&value.Value(), &ir.ConstantF32(lowerBound)})); const IrU1 high(ir.Emit(IrOpcode::FPOrdGreaterThanEqual32, IrType::U1, {&value.Value(), &ir.ConstantF32(upperBound)})); - const IrF32 truncated(ir.Emit(IrOpcode::FPTrunc32, IrType::F32, {&value.Value()})); - const IrF32 safeLow = selectF32(low, IrF32(ir.ConstantF32(lowerBound)), truncated); + const IrF32 safeLow = selectF32(low, IrF32(ir.ConstantF32(lowerBound)), value); const IrF32 safeHigh = selectF32(high, IrF32(ir.ConstantF32(safeUpper)), safeLow); const IrF32 safe = selectF32(nan, IrF32(ir.ConstantF32(0.0f)), safeHigh); - const IrU32 converted(ir.Emit(IrOpcode::ConvertS32F32, IrType::U32, {&safe.Value()})); - const IrU32 clampedHigh(ir.Select(high.Value(), ir.Constant(upperResult), converted.Value())); - const IrU32 clamped(ir.Select(low.Value(), ir.Constant(lowerResult), clampedHigh.Value())); - return IrU32(ir.Select(nan.Value(), ir.Constant(0u), clamped.Value())); + IrU32 converted(ir.Emit(IrOpcode::ConvertS32F32, IrType::U32, {&safe.Value()})); + if (static_cast(static_cast(lowerBound)) != lowerResult) converted = IrU32(ir.Select(low.Value(), ir.Constant(lowerResult), converted.Value())); + if (static_cast(static_cast(safeUpper)) != upperResult) converted = IrU32(ir.Select(high.Value(), ir.Constant(upperResult), converted.Value())); + return converted; } IrU32 TranslationContext::packU16Lanes(IrU32 low, IrU32 high) { @@ -53,37 +52,78 @@ void TranslationContext::vCvtF32Ubyte(const RdnaInstruction& inst, std::uint32_t writeOperand(inst.destination, &result.Value()); } +bool TranslationContext::directedF32Rounding() const { + return floatMode.has_value() && (floatMode->floatMode & 3u) != 0u; +} + +IrF32 TranslationContext::convertMagnitudeToF32Directed(IrU32 magnitude, IrU32 signBits, std::uint32_t rounding, IrF32 native) { + const IrU32 msb(ir.Emit(IrOpcode::FindUMsb32, IrType::U32, {&magnitude.Value()})); + const IrU1 inexact(ir.UGreaterThan(msb.Value(), ir.Constant(23u))); + const IrU32 shift(ir.ISub(msb.Value(), ir.Constant(23u))); + const IrU32 top24(ir.ShiftRightLogical(magnitude.Value(), shift.Value())); + const IrU32 discarded(ir.BitwiseAnd(magnitude.Value(), ir.ISub(ir.ShiftLeftLogical(ir.Constant(1u), shift.Value()), ir.Constant(1u)))); + const IrU1 positive(ir.IEqual(signBits.Value(), ir.Constant(0u))); + const IrU1 away(rounding == 1u ? positive : rounding == 2u ? IrU1(ir.LogicalNot(positive.Value())) : IrU1(ir.ConstantBool(false))); + const IrU1 increment(ir.LogicalAnd(away.Value(), ir.INotEqual(discarded.Value(), ir.Constant(0u)))); + const IrU32 rounded(ir.IAdd(top24.Value(), ir.Select(increment.Value(), ir.Constant(1u), ir.Constant(0u)))); + const IrU1 overflow(ir.Emit(IrOpcode::UGreaterThanEqual32, IrType::U1, {&rounded.Value(), &ir.Constant(0x1000000u)})); + const IrU32 finalMsb(ir.Select(overflow.Value(), ir.IAdd(msb.Value(), ir.Constant(1u)), msb.Value())); + const IrU32 finalTop(ir.Select(overflow.Value(), ir.Constant(0x800000u), rounded.Value())); + const IrU32 bits(ir.BitwiseOr(signBits.Value(), ir.BitwiseOr(ir.ShiftLeftLogical(ir.IAdd(finalMsb.Value(), ir.Constant(127u)), ir.Constant(23u)), ir.BitwiseAnd(finalTop.Value(), ir.Constant(0x7fffffu))))); + const IrF32 directed(ir.BitCastF32(bits.Value())); + const IrU1 useNative(ir.LogicalOr(ir.IEqual(magnitude.Value(), ir.Constant(0u)), ir.LogicalNot(inexact.Value()))); + return selectF32(useNative, native, directed); +} + +IrF32 TranslationContext::convertU32ToF32(IrU32 source) { + const IrF32 native(ir.Emit(IrOpcode::ConvertF32U32, IrType::F32, {&source.Value()})); + if (!directedF32Rounding()) { + return native; + } + return convertMagnitudeToF32Directed(source, IrU32(ir.Constant(0u)), floatMode->floatMode & 3u, native); +} + +IrF32 TranslationContext::convertS32ToF32(IrU32 source) { + const IrF32 native(ir.Emit(IrOpcode::ConvertF32S32, IrType::F32, {&source.Value()})); + if (!directedF32Rounding()) { + return native; + } + const IrU32 signBits(ir.BitwiseAnd(source.Value(), ir.Constant(0x80000000u))); + const IrU32 magnitude(ir.Emit(IrOpcode::IAbs32, IrType::U32, {&source.Value()})); + return convertMagnitudeToF32Directed(magnitude, signBits, floatMode->floatMode & 3u, native); +} + void TranslationContext::vCvtF32U32(const RdnaInstruction& inst) { const IrU32 source = readU32(sourceAt(inst, 0u)); - const IrF32 result(ir.Emit(IrOpcode::ConvertF32U32, IrType::F32, {&source.Value()})); + const IrF32 result = convertU32ToF32(source); writeOperand(inst.destination, &result.Value()); } void TranslationContext::vCvtF32I32(const RdnaInstruction& inst) { const IrU32 source = readU32(sourceAt(inst, 0u)); - const IrF32 result(ir.Emit(IrOpcode::ConvertF32S32, IrType::F32, {&source.Value()})); + const IrF32 result = convertS32ToF32(source); writeOperand(inst.destination, &result.Value()); } void TranslationContext::vCvtU32F32(const RdnaInstruction& inst) { - const IrF32 value(*readOperand(sourceAt(inst, 0u), IrType::F32)); + const IrF32 value(ir.BitCastF32(*readOperand(sourceAt(inst, 0u), IrType::U32))); const IrU32 result = convertF32ToU32Saturated(value, 4294967296.0f, 4294967040.0f, 0xffffffffu); writeOperand(inst.destination, &result.Value()); } void TranslationContext::vCvtI32F32(const RdnaInstruction& inst) { - const IrF32 value(*readOperand(sourceAt(inst, 0u), IrType::F32)); + const IrF32 value(ir.BitCastF32(*readOperand(sourceAt(inst, 0u), IrType::U32))); const IrU32 result = convertF32ToI32Saturated(value, -2147483648.0f, 2147483648.0f, 2147483520.0f, 0x80000000u, 0x7fffffffu); writeOperand(inst.destination, &result.Value()); } void TranslationContext::vCvtF16F32(const RdnaInstruction& inst) { const IrF32 value(*readOperand(sourceAt(inst, 0u), IrType::F32)); - writeF16(inst.destination, value); + writeF16Conversion(inst.destination, value, {&value.Value()}); } void TranslationContext::vCvtF32F16(const RdnaInstruction& inst) { - const IrF32 value = readF16AsF32(sourceAt(inst, 0u)); + const IrF32 value = flushF16InputValue(readF16AsF32(sourceAt(inst, 0u))); writeOperand(inst.destination, &value.Value()); } @@ -91,7 +131,7 @@ void TranslationContext::vCvtF1616(const RdnaInstruction& inst, bool signedValue const IrU32 source = readU16AsU32(sourceAt(inst, 0u), signedValue); const IrOpcode opcode = signedValue ? IrOpcode::ConvertF32S32 : IrOpcode::ConvertF32U32; const IrF32 value(ir.Emit(opcode, IrType::F32, {&source.Value()})); - writeF16(inst.destination, value); + writeF16Conversion(inst.destination, value, {}); } void TranslationContext::vCvt16F16(const RdnaInstruction& inst, bool signedValue) { @@ -158,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()); } @@ -170,15 +215,15 @@ void TranslationContext::vCvtPknormF32(const RdnaInstruction& inst, bool signedV } void TranslationContext::vCvtPkU8F32(const RdnaInstruction& inst) { - const IrF32 source(*readOperand(sourceAt(inst, 0u), IrType::F32)); - const IrU32 truncated = convertF32ToU32Saturated(source, 255.0f, 255.0f, 255u); - IrValue& whole = ir.Emit(IrOpcode::ConvertF32U32, IrType::F32, {&truncated.Value()}); - IrValue& fraction = ir.Emit(IrOpcode::FPSub32, IrType::F32, {&source.Value(), &whole}); - const IrU1 odd(ir.INotEqual(ir.BitwiseAnd(truncated.Value(), ir.Constant(1u)), ir.Constant(0u))); - const IrU1 aboveHalf(ir.Emit(IrOpcode::FPOrdGreaterThan32, IrType::U1, {&fraction, &ir.ConstantF32(0.5f)})); - const IrU1 tie(ir.LogicalAnd(ir.Emit(IrOpcode::FPOrdEqual32, IrType::U1, {&fraction, &ir.ConstantF32(0.5f)}), odd.Value())); - const IrU1 roundUp(ir.LogicalAnd(ir.LogicalOr(aboveHalf.Value(), tie.Value()), ir.ULessThan(truncated.Value(), ir.Constant(255u)))); - const IrU32 byteValue(ir.Select(roundUp.Value(), ir.IAdd(truncated.Value(), ir.Constant(1u)), truncated.Value())); + const IrF32 source(ir.BitCastF32(*readOperand(sourceAt(inst, 0u), IrType::U32))); + const IrF32 zero(ir.ConstantF32(0.0f)); + const IrU1 nan(ir.Emit(IrOpcode::FPIsNan32, IrType::U1, {&source.Value()})); + const IrU1 low(ir.Emit(IrOpcode::FPOrdLessThanEqual32, IrType::U1, {&source.Value(), &zero.Value()})); + const IrU1 high(ir.Emit(IrOpcode::FPOrdGreaterThanEqual32, IrType::U1, {&source.Value(), &ir.ConstantF32(255.0f)})); + const IrF32 rounded(ir.Emit(IrOpcode::FPRoundEven32, IrType::F32, {&source.Value()})); + const IrF32 bounded = selectF32(high, IrF32(ir.ConstantF32(255.0f)), rounded); + const IrF32 safe = selectF32(IrU1(ir.LogicalOr(nan.Value(), low.Value())), zero, bounded); + const IrU32 byteValue(ir.Emit(IrOpcode::ConvertU32F32, IrType::U32, {&safe.Value()})); const IrU32 index(ir.BitwiseAnd(readU32(sourceAt(inst, 1u)).Value(), ir.Constant(3u))); const IrU32 shift(ir.ShiftLeftLogical(index.Value(), ir.Constant(3u))); const IrU32 mask(ir.ShiftLeftLogical(ir.Constant(0xffu), shift.Value())); @@ -188,8 +233,13 @@ void TranslationContext::vCvtPkU8F32(const RdnaInstruction& inst) { } void TranslationContext::vPackB32F16(const RdnaInstruction& inst) { - const IrU32 low = readF16Bits(sourceAt(inst, 0u)); - const IrU32 high(ir.ShiftLeftLogical(readF16Bits(sourceAt(inst, 1u)).Value(), ir.Constant(16u))); + const auto half = [&](std::uint32_t 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())); + }; + const IrU32 low = half(0u); + const IrU32 high(ir.ShiftLeftLogical(half(1u).Value(), ir.Constant(16u))); const IrU32 result(ir.BitwiseOr(low.Value(), high.Value())); writeOperand(inst.destination, &result.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 4fda5df13d..966973b0f8 100644 --- a/core/shader/recompiler/Translation/src/EmbeddedVertexFetch.cpp +++ b/core/shader/recompiler/Translation/src/EmbeddedVertexFetch.cpp @@ -149,6 +149,15 @@ bool isAttributePropagationAlu(RdnaOpcode opcode) { } } +std::uint32_t scalarBfeU32(std::uint32_t source, std::uint32_t field) { + const std::uint32_t count = std::min((field >> 16u) & 0x7fu, 32u); + if (count == 0u) { + return 0u; + } + const std::uint32_t shifted = source >> (field & 31u); + return count == 32u ? shifted : shifted & ((1u << count) - 1u); +} + std::int32_t bufferAttributeFromOffset(std::uint32_t rawOffset, std::uint32_t dword) { return static_cast((rawOffset + dword * 4u) / 16u); } @@ -177,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; @@ -208,6 +227,202 @@ 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: + case RdnaInstructionFamily::VOP2: + case RdnaInstructionFamily::VOP3: + case RdnaInstructionFamily::VOP3P: + case RdnaInstructionFamily::VOPC: + return true; + default: + return false; + } +} + +bool isSequentialProgramControl(RdnaOpcode opcode) { + switch (opcode) { + case RdnaOpcode::SNop: + case RdnaOpcode::SWaitcnt: + case RdnaOpcode::SWaitcntDepctr: + case RdnaOpcode::SSetprio: + case RdnaOpcode::SSendmsg: + case RdnaOpcode::SBarrier: + case RdnaOpcode::SSleep: + case RdnaOpcode::SClause: + case RdnaOpcode::SInstPrefetch: + case RdnaOpcode::SRoundMode: + case RdnaOpcode::SDenormMode: + case RdnaOpcode::SIcacheInv: + case RdnaOpcode::STtracedata: + case RdnaOpcode::SIncperflevel: + case RdnaOpcode::SDecperflevel: + return true; + default: + return false; + } +} + +bool leavesStraightLine(const RdnaInstruction& inst) { + if (inst.op == RdnaOpcode::Unknown || inst.op == RdnaOpcode::SSetpcB64 || inst.op == RdnaOpcode::SRfeB64) return true; + return inst.family == RdnaInstructionFamily::SOPP && !isSequentialProgramControl(inst.op); +} + +bool readsSgprRange(const RdnaInstruction& inst, std::uint32_t first, std::uint32_t count) { + const std::array sources{&inst.source0, &inst.source1, &inst.source2, &inst.source3}; + for (std::uint32_t i = 0u; i < sources.size(); i++) { + if (!isScalarOperand(*sources[i])) continue; + const auto slot = scalarSlot(*sources[i]); + if (slot < first + count && first < slot + scalarOperandWidth(inst, i)) return true; + } + return false; +} + +bool readsVccImplicitly(const RdnaInstruction& inst) { + switch (inst.op) { + case RdnaOpcode::VCndmaskB32: + case RdnaOpcode::VAddcU32: + case RdnaOpcode::VSubCoCiU32: + case RdnaOpcode::VSubrevCoCiU32: + return !isScalarOperand(inst.source2); + case RdnaOpcode::VDivFmasF32: + case RdnaOpcode::VDivFmasF64: + return true; + default: + return false; + } +} + +bool mayReachVgpr(const RdnaInstruction& inst, std::uint32_t reg) { + switch (inst.family) { + case RdnaInstructionFamily::SOP1: + case RdnaInstructionFamily::SOP2: + case RdnaInstructionFamily::SOPK: + case RdnaInstructionFamily::SOPC: + case RdnaInstructionFamily::SMEM: + case RdnaInstructionFamily::SOPP: + return leavesStraightLine(inst); + default: + break; + } + if (!isVectorAlu(inst)) return true; + switch (inst.op) { + case RdnaOpcode::VMovrelsB32: + case RdnaOpcode::VMovreldB32: + case RdnaOpcode::VMovrelsdB32: + case RdnaOpcode::VMovrelsd2B32: + case RdnaOpcode::VSwaprelB32: + return true; + default: + break; + } + const std::array sources{&inst.source0, &inst.source1, &inst.source2, &inst.source3}; + for (std::uint32_t i = 0u; i < sources.size(); i++) { + const auto width = inst.op == RdnaOpcode::VMqsadU32U8 && i == 2u ? 4u : 2u; + if (isVectorOperand(*sources[i]) && reg >= sources[i]->reg && reg < sources[i]->reg + width) return true; + } + return isVectorOperand(inst.destination) && reg >= inst.destination.reg && reg < inst.destination.reg + std::max(embeddedFetchDstSize(inst), 2u); +} + +bool writesExec(const RdnaInstruction& inst) { + const auto exec = [](const RdnaOperand& operand) { return operand.kind == RdnaOperandKind::ExecLo || operand.kind == RdnaOperandKind::ExecHi; }; + if (exec(inst.destination) || exec(inst.destination2)) return true; + switch (inst.op) { + case RdnaOpcode::SAndSaveexecB32: + case RdnaOpcode::SOrSaveexecB32: + case RdnaOpcode::SXorSaveexecB32: + case RdnaOpcode::SAndn2SaveexecB32: + case RdnaOpcode::SOrn2SaveexecB32: + case RdnaOpcode::SNandSaveexecB32: + case RdnaOpcode::SNorSaveexecB32: + case RdnaOpcode::SXnorSaveexecB32: + case RdnaOpcode::SAndn1SaveexecB32: + case RdnaOpcode::SOrn1SaveexecB32: + case RdnaOpcode::SAndn1WrexecB32: + case RdnaOpcode::SAndn2WrexecB32: + case RdnaOpcode::SAndSaveexecB64: + case RdnaOpcode::SOrSaveexecB64: + case RdnaOpcode::SXorSaveexecB64: + case RdnaOpcode::SAndn2SaveexecB64: + case RdnaOpcode::SOrn2SaveexecB64: + case RdnaOpcode::SNandSaveexecB64: + case RdnaOpcode::SNorSaveexecB64: + case RdnaOpcode::SXnorSaveexecB64: + case RdnaOpcode::SAndn1SaveexecB64: + case RdnaOpcode::SOrn1SaveexecB64: + case RdnaOpcode::SAndn1WrexecB64: + case RdnaOpcode::SAndn2WrexecB64: + case RdnaOpcode::SSubvectorLoopBegin: + case RdnaOpcode::SSubvectorLoopEnd: + return true; + default: + return false; + } +} + +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)); +} + +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); + const std::uint32_t count = waveSize == 64u ? 2u : 1u; + for (std::size_t index = addIndex + 1u; index < program.instructions.size(); index++) { + const auto& inst = program.instructions[index]; + if (inst.op == RdnaOpcode::SEndpgm) return false; + if (leavesStraightLine(inst) || readsSgprRange(inst, first, count) || (first == kVccLoSlot && readsVccImplicitly(inst))) return true; + const bool laneMask = isVectorAlu(inst) && inst.op != RdnaOpcode::VReadlaneB32 && inst.op != RdnaOpcode::VReadfirstlaneB32; + const auto rewrites = [&](const RdnaOperand& operand) { return isScalarOperand(operand) && scalarSlot(operand) == first && (count == 1u || laneMask); }; + if (rewrites(inst.destination) || rewrites(inst.destination2)) return false; + } + return false; +} + } EmbeddedFetchPlan EmbeddedVertexFetchAnalyzer::Analyze(const RdnaProgram& program, std::uint32_t attributeTableRegister, std::uint32_t bufferTableRegister, std::uint32_t userDataBaseRegister, std::uint32_t userDataCount, std::uint32_t waveSize) const { @@ -244,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)); @@ -354,12 +570,13 @@ EmbeddedFetchPlan EmbeddedVertexFetchAnalyzer::Analyze(const RdnaProgram& progra dst.loadPcs.push_back(inst.programCounter); } } else if (isScalarOperand(inst.source1) && scalarSlot(inst.source1) < sgprs.size() && sgprs[scalarSlot(inst.source1)].kind == SgprValueKind::Attribute && (inst.memoryOffset & 0x3u) == 0u) { + const auto attribute = sgprs[scalarSlot(inst.source1)]; for (std::uint32_t i = 0u; i < decodedDstSize(inst) && slot + i < sgprs.size(); i++) { auto& dst = sgprs[slot + i]; dst = SgprValue{}; dst.kind = SgprValueKind::Buffer; - dst.attributeId = sgprs[scalarSlot(inst.source1)].attributeId; - dst.loadPcs = sgprs[scalarSlot(inst.source1)].loadPcs; + dst.attributeId = attribute.attributeId; + dst.loadPcs = attribute.loadPcs; dst.loadPcs.push_back(inst.programCounter); } } else { @@ -393,7 +610,7 @@ EmbeddedFetchPlan EmbeddedVertexFetchAnalyzer::Analyze(const RdnaProgram& progra dst.constant = src0 << (src1 & 31u); break; case RdnaOpcode::SBfeU32: - dst.constant = src0 >> (src1 & 31u); + dst.constant = scalarBfeU32(src0, src1); break; default: dst.constant = src0 + src1; @@ -439,14 +656,61 @@ 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; }; plan.vertexOffsetShared = shared(plan.vertexOffsetSgpr, vertexAddPcs); plan.instanceOffsetShared = shared(plan.instanceOffsetSgpr, instanceAddPcs); + const bool relativeScalarAccess = std::any_of(program.instructions.begin(), program.instructions.end(), [](const RdnaInstruction& inst) { + return inst.op == RdnaOpcode::SMovrelsB32 || inst.op == RdnaOpcode::SMovrelsB64 || inst.op == RdnaOpcode::SMovrelsd2B32 || inst.op == RdnaOpcode::SMovreldB32 || inst.op == RdnaOpcode::SMovreldB64; + }); + const auto observed = [&](std::int32_t sgpr, const std::vector& addPcs, std::uint32_t indexVgpr) { + if (sgpr < 0) return false; + if (addPcs.size() != 1u || relativeScalarAccess) return true; + const auto add = std::find_if(program.instructions.begin(), program.instructions.end(), [&](const RdnaInstruction& inst) { return inst.programCounter == addPcs.front(); }); + 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) && !setsExecAllOnes(inst)) || mayReachVgpr(inst, reg); })) return true; + return carryObserved(program, static_cast(add - program.instructions.begin()), waveSize); + }; + plan.vertexIndexObserved = observed(plan.vertexOffsetSgpr, vertexAddPcs, kVertexIndexVgpr); + plan.instanceIndexObserved = observed(plan.instanceOffsetSgpr, instanceAddPcs, kInstanceIndexVgpr); return plan; } diff --git a/core/shader/recompiler/Translation/src/Float64Instructions.cpp b/core/shader/recompiler/Translation/src/Float64Instructions.cpp index 47e19cf033..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,6 +39,73 @@ bool TranslationContext::float64Operation(const RdnaInstruction& inst, IrOpcode return true; } +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{}; + for (std::uint32_t index = 0u; index < 2u; ++index) { + IrValue& lhsChecked = ir.Select(*lhsNan, bits[1][index].Value(), ir.CompositeExtract(plain, index)); + words[index] = &ir.Select(*rhsNan, bits[0][index].Value(), lhsChecked); + } + writeF64Result(inst.destination, ir.ConstructU64(*words[0], *words[1])); + return true; +} + +bool TranslationContext::float64Unary(const RdnaInstruction& inst, IrOpcode opcode) { + 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(), words[0].Value()), ir.Select(nan.Value(), quiet[1].Value(), words[1].Value()))); + return true; +} + +bool TranslationContext::vCvtF64F32(const RdnaInstruction& inst) { + IrValue* value = readOperand(sourceAt(inst, 0u), IrType::F32); + IrValue& result = ir.Emit(IrOpcode::ConvertF64F32, IrType::U64, {value}); + const IrU32 bits(ir.BitCastU32(*value)); + const IrU1 nan(ir.UGreaterThan(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffffffu)), ir.Constant(0x7f800000u))); + const IrU32 mantissa(ir.BitwiseAnd(bits.Value(), ir.Constant(0x007fffffu))); + const IrU32 high(ir.BitwiseOr(ir.BitwiseOr(ir.BitwiseAnd(bits.Value(), ir.Constant(0x80000000u)), ir.Constant(0x7ff00000u)), ir.ShiftRightLogical(mantissa.Value(), ir.Constant(3u)))); + const auto quiet = quietNan64({IrU32(ir.ShiftLeftLogical(mantissa.Value(), ir.Constant(29u))), high}); + 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)))); + return true; +} + void TranslationContext::writeF64Result(const RdnaOperand& operand, IrValue& value) { rejectHalfOrDoubleOutputModifier(operand); RdnaOperand destination = operand; @@ -107,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) { @@ -129,9 +207,6 @@ bool TranslationContext::vDivFixupF64(const RdnaInstruction& inst) { const std::array zero{IrU32(ir.Constant(0u)), sign}; const std::array infinity{IrU32(ir.Constant(0u)), IrU32(ir.BitwiseOr(sign.Value(), ir.Constant(0x7ff00000u)))}; const std::array defaultNan{IrU32(ir.Constant(0u)), IrU32(ir.Constant(0xfff80000u))}; - const auto quiet = [&](const std::array& word) { - return std::array{word[0], IrU32(ir.BitwiseOr(word[1].Value(), ir.Constant(0x00080000u)))}; - }; const IrU32 difference(ir.ISub(exponent(numerator[1]).Value(), exponent(denominator[1]).Value())); const IrU1 underflow(ir.Emit(IrOpcode::SLessThan32, IrType::U1, {&difference.Value(), &ir.Constant(static_cast(-1075))})); std::array result{quotient[0], IrU32(ir.BitwiseOr(sign.Value(), magnitude(quotient[1]).Value()))}; @@ -143,8 +218,8 @@ bool TranslationContext::vDivFixupF64(const RdnaInstruction& inst) { choose(IrU1(ir.LogicalOr(isInf(denominator).Value(), isZero(numerator).Value())), zero); choose(IrU1(ir.LogicalOr(isZero(denominator).Value(), isInf(numerator).Value())), infinity); choose(IrU1(ir.LogicalOr(ir.LogicalAnd(isZero(denominator).Value(), isZero(numerator).Value()), ir.LogicalAnd(isInf(denominator).Value(), isInf(numerator).Value()))), defaultNan); - choose(isNan(denominator), quiet(denominator)); - choose(isNan(numerator), quiet(numerator)); + choose(isNan(denominator), quietNan64(denominator)); + choose(isNan(numerator), quietNan64(numerator)); writeF64Result(inst.destination, ir.ConstructU64(result[0].Value(), result[1].Value())); return true; } diff --git a/core/shader/recompiler/Translation/src/FloatInstructions.cpp b/core/shader/recompiler/Translation/src/FloatInstructions.cpp index a375ba70d8..0632342acb 100644 --- a/core/shader/recompiler/Translation/src/FloatInstructions.cpp +++ b/core/shader/recompiler/Translation/src/FloatInstructions.cpp @@ -11,32 +11,62 @@ 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 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); - return applyF32ResultModifiers(inst.destination, IrF32(ir.Emit(opcode, IrType::F32, {&lhs.Value(), &rhs.Value(), &acc.Value()}))); + 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); - return applyF32ResultModifiers(inst.destination, IrF32(ir.Emit(opcode, IrType::F32, {&lhs.Value(), &rhs.Value(), &third.Value()}))); + 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, IrF32(ir.Emit(opcode, IrType::F32, {&lhs.Value(), &rhs.Value()}))); + return applyF32ResultModifiers(inst.destination, clampF16Overflow(flushTinyResult(IrF32(ir.Emit(opcode, IrType::F32, {&lhs.Value(), &rhs.Value()}))), {&lhs.Value(), &rhs.Value()})); }; - IrU32 result = packHalf2x16(translateLane(false), translateLane(true)); - if (!inst.destination.clamp || !dx10Clamp()) { - const auto laneBits = [&](const RdnaOperand& operand, bool high) { - const IrU32 source = readF16SourceBits(operand); - const bool selectHigh = high ? operand.opSelHi : operand.opSel; - 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; - }; + const auto laneBits = [&](const RdnaOperand& operand, bool high) { + const IrU32 source = readF16SourceBits(operand); + const bool selectHigh = high ? operand.opSelHi : operand.opSel; + 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 flushF16Input(bits); + }; + 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 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)); + if (native || !inst.destination.clamp || !dx10Clamp() || quietSnan) { const auto isNan = [&](const IrU32& bits) { return IrU1(ir.UGreaterThan(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffu)), ir.Constant(0x7c00u))); }; @@ -55,9 +85,17 @@ 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; + if (native && fp16Overflow()) { + IrU1 overflow(ir.IEqual(magnitude(lane).Value(), ir.Constant(0x7c00u))); + for (std::uint32_t index = 0u; index < count; ++index) { + overflow = IrU1(ir.LogicalAnd(overflow.Value(), ir.INotEqual(magnitude(bits[index]).Value(), ir.Constant(0x7c00u)))); + } + lane = IrU32(ir.Select(overflow.Value(), ir.BitwiseOr(ir.BitwiseAnd(lane.Value(), ir.Constant(0x8000u)), ir.Constant(0x7bffu)), lane.Value())); + } if (quietSnan) { lane = IrU32(ir.Select(isNan(bits[0]).Value(), bits[1].Value(), lane.Value())); lane = IrU32(ir.Select(isNan(bits[1]).Value(), bits[0].Value(), lane.Value())); @@ -84,12 +122,14 @@ bool TranslationContext::packedFloat16(const RdnaInstruction& inst, IrOpcode opc bool TranslationContext::float16Unary(const RdnaInstruction& inst, IrOpcode opcode) { const RdnaOperand& operand = sourceAt(inst, 0u); - const IrF32 argument = readF16AsF32(operand); + 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, result), 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())); } + bits = flushF16Result(bits); const IrU32 half = readF16Bits(operand); const IrU1 nan(ir.UGreaterThan(ir.BitwiseAnd(half.Value(), ir.Constant(0x7fffu)), ir.Constant(0x7c00u))); const IrU32 special(ir.Select(nan.Value(), quietNan16(half).Value(), ir.Constant(0xfe00u))); @@ -182,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))); }; @@ -197,27 +240,94 @@ bool TranslationContext::vDivFixupF16(const RdnaInstruction& inst) { result = IrU32(ir.Select(ir.LogicalOr(bothZero.Value(), bothInf.Value()), ir.Constant(0xfe00u), result.Value())); result = IrU32(ir.Select(isNan(1u).Value(), quietNan16(bits[1]).Value(), result.Value())); result = IrU32(ir.Select(isNan(2u).Value(), quietNan16(bits[2]).Value(), result.Value())); + if (fp16Overflow()) { + const IrU1 keep(ir.LogicalOr(isZero(1u).Value(), isInf(2u).Value())); + const IrU1 overflow(ir.LogicalAnd(ir.IEqual(ir.BitwiseAnd(result.Value(), ir.Constant(0x7fffu)), ir.Constant(0x7c00u)), ir.LogicalNot(keep.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; } bool TranslationContext::float16Binary(const RdnaInstruction& inst, IrOpcode opcode, bool reverse) { - const IrF32 lhs = readF16AsF32(sourceAt(inst, reverse ? 1u : 0u)); - const IrF32 rhs = readF16AsF32(sourceAt(inst, reverse ? 0u : 1u)); - writeF16(inst.destination, IrF32(ir.Emit(opcode, IrType::F32, {&lhs.Value(), &rhs.Value()}))); + 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 read = [&](const RdnaOperand& operand) { + const IrF32 value = readF16AsF32(operand); + 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 = flushTinyF16Value(result, opcode == IrOpcode::FPMul32 ? 0x387ff000u : 0x38800000u); + writeF16(inst.destination, result, {&lhs.Value(), &rhs.Value()}); return true; } bool TranslationContext::float16Ternary(const RdnaInstruction& inst, IrOpcode opcode, bool accumulator, bool mix) { + 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(); } - writeF16(inst.destination, IrF32(ir.Emit(opcode, IrType::F32, {args[0], args[1], args[2]}))); + if (opcode == IrOpcode::FPFma32 && !mix) { + 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) { + const std::uint32_t rounding = floatMode.has_value() ? ((floatMode->floatMode >> 2u) & 3u) << InterpolationRound16Shift : 0u; + writeF16(inst.destination, IrF32(ir.Emit(IrOpcode::FPInterpolateF16, IrType::F32, {args[0], args[1], args[2], &ir.Constant(rounding)})), {args[0], args[1], args[2]}); + return true; + } + writeF16(inst.destination, IrF32(ir.Emit(opcode, IrType::F32, {args[0], args[1], args[2]})), {args[0], args[1], args[2]}); 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()}); + IrValue& addendPart = ir.Emit(IrOpcode::FPSub32, IrType::F32, {&sum, &product}); + IrValue& productPart = ir.Emit(IrOpcode::FPSub32, IrType::F32, {&sum, &addendPart}); + IrValue& productError = ir.Emit(IrOpcode::FPSub32, IrType::F32, {&product, &productPart}); + IrValue& addendError = ir.Emit(IrOpcode::FPSub32, IrType::F32, {&addend.Value(), &addendPart}); + IrValue& error = ir.Emit(IrOpcode::FPAdd32, IrType::F32, {&productError, &addendError}); + IrValue& inexact = ir.Emit(IrOpcode::FPOrdNotEqual32, IrType::U1, {&error, &ir.ConstantF32(0.0f)}); + IrValue& sumBits = ir.BitCastU32(sum); + IrValue& sameSign = ir.IEqual(ir.BitwiseAnd(ir.BitwiseXor(sumBits, ir.BitCastU32(error)), ir.Constant(0x80000000u)), ir.Constant(0u)); + IrValue& towardZero = ir.Select(sameSign, sumBits, ir.ISub(sumBits, ir.Constant(1u))); + return IrF32(ir.BitCastF32(ir.Select(inexact, ir.BitwiseOr(towardZero, ir.Constant(1u)), sumBits))); +} + IrU32 TranslationContext::readF16Bits(const RdnaOperand& operand) { const IrU32 source = readF16SourceBits(operand); IrU32 bits(ir.BitwiseAnd((operand.opSel ? ir.ShiftRightLogical(source.Value(), ir.Constant(16u)) : source.Value()), ir.Constant(0xffffu))); @@ -257,17 +367,19 @@ IrU32 TranslationContext::normF32(IrU32 bits, bool signedValue) { const IrU1 nan(ir.UGreaterThan(magnitude.Value(), ir.Constant(0x7f800000u))); const IrU1 saturated(ir.Emit(IrOpcode::UGreaterThanEqual32, IrType::U1, {&magnitude.Value(), &ir.Constant(0x3f800000u)})); const std::uint32_t scale = signedValue ? 32767u : 65535u; - const IrU32 exponent(ir.ShiftRightLogical(magnitude.Value(), ir.Constant(23u))); - const IrU32 mantissa(ir.BitwiseOr(ir.BitwiseAnd(magnitude.Value(), ir.Constant(0x7fffffu)), ir.Constant(0x800000u))); - const IrU32 shift(ir.Emit(IrOpcode::UMin32, IrType::U32, {&ir.ISub(ir.Constant(149u), exponent.Value()), &ir.Constant(63u)})); - const IrU64 product(ir.Emit(IrOpcode::IMul64, IrType::U64, {&ir.ConstructU64(mantissa.Value(), ir.Constant(0u)), &ir.ConstantU64(scale)})); - const IrU64 halves(ir.Emit(IrOpcode::ShiftRightLogical64, IrType::U64, {&product.Value(), &shift.Value()})); - const IrU1 sticky(ir.Emit(IrOpcode::INotEqual64, IrType::U1, {&ir.Emit(IrOpcode::ShiftLeftLogical64, IrType::U64, {&halves.Value(), &shift.Value()}), &product.Value()})); - const IrU32 doubled = extractU64(halves)[0]; - const IrU32 truncated(ir.ShiftRightLogical(doubled.Value(), ir.Constant(1u))); - const IrU1 roundUp(ir.LogicalAnd(ir.INotEqual(ir.BitwiseAnd(doubled.Value(), ir.Constant(1u)), ir.Constant(0u)), - ir.LogicalOr(sticky.Value(), ir.INotEqual(ir.BitwiseAnd(truncated.Value(), ir.Constant(1u)), ir.Constant(0u))))); - IrU32 value(ir.Select(saturated.Value(), ir.Constant(scale), ir.IAdd(truncated.Value(), ir.Select(roundUp.Value(), ir.Constant(1u), ir.Constant(0u))))); + IrValue& absolute = ir.BitCastF32(ir.Select(ir.LogicalOr(saturated.Value(), nan.Value()), ir.Constant(0u), magnitude.Value())); + IrValue& scaled = ir.Emit(IrOpcode::FPMul32, IrType::F32, {&absolute, &ir.ConstantF32(static_cast(scale + 1u))}); + IrValue& product = ir.Emit(IrOpcode::FPSub32, IrType::F32, {&scaled, &absolute}); + IrValue& error = ir.Emit(IrOpcode::FPSub32, IrType::F32, {&ir.Emit(IrOpcode::FPSub32, IrType::F32, {&scaled, &product}), &absolute}); + IrValue& floored = ir.Emit(IrOpcode::FPFloor32, IrType::F32, {&product}); + IrValue& tie = ir.Emit(IrOpcode::FPOrdEqual32, IrType::U1, {&ir.Emit(IrOpcode::FPSub32, IrType::F32, {&product, &floored}), &ir.ConstantF32(0.5f)}); + IrValue& zero = ir.ConstantF32(0.0f); + IrValue& tieDown = ir.LogicalAnd(tie, ir.Emit(IrOpcode::FPOrdLessThan32, IrType::U1, {&error, &zero})); + IrValue& tieUp = ir.LogicalAnd(tie, ir.Emit(IrOpcode::FPOrdGreaterThan32, IrType::U1, {&error, &zero})); + IrValue& nearest = ir.Emit(IrOpcode::FPRoundEven32, IrType::F32, {&product}); + IrValue& roundedDown = ir.Emit(IrOpcode::SelectF32, IrType::F32, {&tieDown, &floored, &nearest}); + IrValue& rounded = ir.Emit(IrOpcode::SelectF32, IrType::F32, {&tieUp, &ir.Emit(IrOpcode::FPAdd32, IrType::F32, {&floored, &ir.ConstantF32(1.0f)}), &roundedDown}); + IrU32 value(ir.Select(saturated.Value(), ir.Constant(scale), ir.Emit(IrOpcode::ConvertU32F32, IrType::U32, {&rounded}))); if (signedValue) { value = IrU32(ir.Select(negative.Value(), ir.ISub(ir.Constant(0u), value.Value()), value.Value())); } else { @@ -277,22 +389,29 @@ IrU32 TranslationContext::normF32(IrU32 bits, bool signedValue) { } bool TranslationContext::vLdexpF16(const RdnaInstruction& inst) { - const IrU32 bits = readF16Bits(sourceAt(inst, 0u)); + const IrU32 source = readF16Bits(sourceAt(inst, 0u)); + const IrU32 bits = flushF16Input(source); const IrF16 half(ir.Emit(IrOpcode::BitCastF16U16, IrType::F16, {&ir.Emit(IrOpcode::ConvertU16U32, IrType::U16, {&bits.Value()})})); const IrF32 value(ir.Emit(IrOpcode::ConvertF32F16, IrType::F32, {&half.Value()})); - const IrU32 exponent(ir.Emit(IrOpcode::BitFieldSExtract, IrType::U32, {&readU32(sourceAt(inst, 1u)).Value(), &ir.Constant(0u), &ir.Constant(16u)})); + const RdnaOperand& exponentSource = sourceAt(inst, 1u); + const IrU32 exponentBits = inst.family == RdnaInstructionFamily::VOP3 ? readF16SourceBits(exponentSource) : readU32(exponentSource); + const IrU32 exponent(ir.Emit(IrOpcode::BitFieldSExtract, IrType::U32, {&exponentBits.Value(), &ir.Constant(0u), &ir.Constant(16u)})); const IrU32 clamped(ir.Emit(IrOpcode::SMax32, IrType::U32, {&ir.Emit(IrOpcode::SMin32, IrType::U32, {&exponent.Value(), &ir.Constant(64u)}), &ir.Constant(static_cast(-64))})); IrValue& power = ir.BitCastF32(ir.ShiftLeftLogical(ir.IAdd(clamped.Value(), ir.Constant(127u)), ir.Constant(23u))); - const IrF16 scaled(ir.Emit(IrOpcode::ConvertF16F32, IrType::F16, {&ir.Emit(IrOpcode::FPMul32, IrType::F32, {&value.Value(), &power})})); - const IrU32 result(ir.Emit(IrOpcode::ConvertU32U16, IrType::U32, {&ir.Emit(IrOpcode::BitCastU16F16, IrType::U16, {&scaled.Value()})})); - const IrU1 nan(ir.UGreaterThan(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffu)), ir.Constant(0x7c00u))); - write16Bits(inst.destination, clampF16Bits(inst.destination, IrU32(ir.Select(nan.Value(), quietNan16(bits).Value(), result.Value())))); + const IrF32 product(ir.Emit(IrOpcode::FPMul32, IrType::F32, {&value.Value(), &power})); + IrU32 result = convertF32ToF16Bits(product, {&value.Value()}); + if (floatMode.has_value() && (floatMode->floatMode & 0x80u) == 0u) { + const IrU1 tiny(ir.ULessThan(ir.BitwiseAnd(ir.BitCastU32(product.Value()), ir.Constant(0x7fffffffu)), ir.Constant(0x38800000u))); + result = IrU32(ir.Select(tiny.Value(), ir.BitwiseAnd(bits.Value(), ir.Constant(0x8000u)), result.Value())); + } + const IrU1 nan(ir.UGreaterThan(ir.BitwiseAnd(source.Value(), ir.Constant(0x7fffu)), ir.Constant(0x7c00u))); + write16Bits(inst.destination, clampF16Bits(inst.destination, IrU32(ir.Select(nan.Value(), quietNan16(source).Value(), result.Value())))); return true; } bool TranslationContext::vFrexpF16(const RdnaInstruction& inst, bool exponent) { - const IrU32 bits = readF16Bits(sourceAt(inst, 0u)); - const IrF32 value = readF16AsF32(sourceAt(inst, 0u)); + const IrU32 bits = flushF16Input(readF16Bits(sourceAt(inst, 0u))); + const IrF32 value = flushF16InputValue(readF16AsF32(sourceAt(inst, 0u))); const IrU32 word(ir.BitCastU32(value.Value())); const IrU1 special(ir.LogicalOr(ir.IEqual(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffu)), ir.Constant(0u)), ir.Emit(IrOpcode::UGreaterThanEqual32, IrType::U1, {&ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffu)), &ir.Constant(0x7c00u)}))); @@ -311,13 +430,13 @@ bool TranslationContext::vFrexpF16(const RdnaInstruction& inst, bool exponent) { } bool TranslationContext::vCvtNormF16(const RdnaInstruction& inst, bool signedValue) { - write16Bits(inst.destination, normF16(readF16Bits(sourceAt(inst, 0u)), signedValue)); + write16Bits(inst.destination, normF16(flushF16Input(readF16Bits(sourceAt(inst, 0u))), signedValue)); return true; } 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; @@ -334,24 +453,19 @@ bool TranslationContext::vSatPkU8I16(const RdnaInstruction& inst) { return true; } -bool TranslationContext::vMulLegacyF32(const RdnaInstruction& inst, bool accumulate) { +bool TranslationContext::vMulLegacyF32(const RdnaInstruction& inst) { IrValue* lhs = readOperand(sourceAt(inst, 0u), IrType::F32); IrValue* rhs = readOperand(sourceAt(inst, 1u), IrType::F32); const auto isZero = [&](IrValue* value) { return IrU1(ir.IEqual(ir.BitwiseAnd(ir.BitCastU32(*value), ir.Constant(0x7fffffffu)), ir.Constant(0u))); }; const IrU1 zero(ir.LogicalOr(isZero(lhs).Value(), isZero(rhs).Value())); - IrValue* result = &ir.Emit(IrOpcode::SelectF32, IrType::F32, {&zero.Value(), &ir.ConstantF32(0.0f), nanResultF32({lhs, rhs}, &flushTinyProduct(lhs, rhs, &ir.Emit(IrOpcode::FPMul32, IrType::F32, {lhs, rhs})).Value())}); - if (accumulate) { - IrValue* addend = readOperand(accumulatorOperand(inst), IrType::F32); - result = &ir.Emit(IrOpcode::FPAdd32, IrType::F32, {result, addend}); - } - writeOperand(inst.destination, result); + writeOperand(inst.destination, &ir.Emit(IrOpcode::SelectF32, IrType::F32, {&zero.Value(), &ir.ConstantF32(0.0f), nanResultF32({lhs, rhs}, &flushTinyProduct(lhs, rhs, &ir.Emit(IrOpcode::FPMul32, IrType::F32, {lhs, rhs})).Value())})); return true; } bool TranslationContext::vMullitF32(const RdnaInstruction& inst) { - const IrU32 lhs = readU32(sourceAt(inst, 0u)); - const IrU32 rhs = readU32(sourceAt(inst, 1u)); - const IrU32 limit = readU32(sourceAt(inst, 2u)); + const IrU32 lhs = flushF32Denormal(readU32(sourceAt(inst, 0u))); + const IrU32 rhs = flushF32Denormal(readU32(sourceAt(inst, 1u))); + const IrU32 limit = flushF32Denormal(readU32(sourceAt(inst, 2u))); const auto magnitude = [&](const IrU32& bits) { return IrU32(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffffffu))); }; const auto isNan = [&](const IrU32& bits) { return IrU1(ir.UGreaterThan(magnitude(bits).Value(), ir.Constant(0x7f800000u))); }; const auto isZero = [&](const IrU32& bits) { return IrU1(ir.IEqual(magnitude(bits).Value(), ir.Constant(0u))); }; @@ -359,7 +473,9 @@ bool TranslationContext::vMullitF32(const RdnaInstruction& inst) { const IrU1 rhsNegativeMax(ir.UGreaterThan(rhs.Value(), ir.Constant(0xff7ffffeu))); IrU1 lowest(ir.LogicalOr(rhsNegativeMax.Value(), ir.LogicalOr(isNan(limit).Value(), limitNotPositive.Value()))); lowest = IrU1(ir.LogicalOr(lowest.Value(), isNan(rhs).Value())); - const IrU32 product(ir.BitCastU32(ir.Emit(IrOpcode::FPMul32, IrType::F32, {&ir.BitCastF32(lhs.Value()), &ir.BitCastF32(rhs.Value())}))); + IrValue& lhsF32 = ir.BitCastF32(lhs.Value()); + IrValue& rhsF32 = ir.BitCastF32(rhs.Value()); + const IrU32 product(ir.BitCastU32(flushTinyProduct(&lhsF32, &rhsF32, &ir.Emit(IrOpcode::FPMul32, IrType::F32, {&lhsF32, &rhsF32})).Value())); IrU32 result(ir.Select(isNan(lhs).Value(), quietNan32(lhs).Value(), product.Value())); result = IrU32(ir.Select(ir.LogicalOr(isZero(lhs).Value(), isZero(rhs).Value()), ir.Constant(0u), result.Value())); result = IrU32(ir.Select(lowest.Value(), ir.Constant(0xff7fffffu), result.Value())); @@ -440,34 +556,26 @@ bool TranslationContext::vDivFmasF32(const RdnaInstruction& inst) { const IrU1 up(ir.Emit(IrOpcode::UGreaterThanEqual32, IrType::U1, {&exponent(*addend).Value(), &ir.Constant(128u)})); IrValue& power = ir.Emit(IrOpcode::SelectF32, IrType::F32, {&up.Value(), &ir.ConstantF32(18446744073709551616.0f), &ir.ConstantF32(5.42101086242752217e-20f)}); IrValue& plain = ir.Emit(IrOpcode::FPFma32, IrType::F32, {lhs, rhs, addend}); - const IrU32 plainExponent = exponent(plain); const IrU1 plainInfinite(ir.IEqual(ir.BitwiseAnd(ir.BitCastU32(plain), ir.Constant(0x7fffffffu)), ir.Constant(0x7f800000u))); - const IrU1 rescale(ir.LogicalOr(ir.LogicalAnd(up.Value(), ir.IEqual(plainExponent.Value(), ir.Constant(0u))), ir.LogicalAnd(ir.LogicalNot(up.Value()), plainInfinite.Value()))); + const IrU1 rescale(ir.LogicalAnd(ir.LogicalNot(up.Value()), plainInfinite.Value())); IrValue& lhsMagnitude = ir.BitwiseAnd(ir.BitCastU32(*lhs), ir.Constant(0x7fffffffu)); IrValue& rhsMagnitude = ir.BitwiseAnd(ir.BitCastU32(*rhs), ir.Constant(0x7fffffffu)); const IrU1 lhsLarger(ir.UGreaterThan(lhsMagnitude, rhsMagnitude)); - const IrU1 scaleLhs(ir.LogicalOr(ir.LogicalAnd(up.Value(), ir.LogicalNot(lhsLarger.Value())), ir.LogicalAnd(ir.LogicalNot(up.Value()), lhsLarger.Value()))); - IrValue& scaled = ir.Emit(IrOpcode::SelectF32, IrType::F32, {&scaleLhs.Value(), lhs, rhs}); - IrValue& other = ir.Emit(IrOpcode::SelectF32, IrType::F32, {&scaleLhs.Value(), rhs, lhs}); + IrValue& scaled = ir.Emit(IrOpcode::SelectF32, IrType::F32, {&lhsLarger.Value(), lhs, rhs}); + IrValue& other = ir.Emit(IrOpcode::SelectF32, IrType::F32, {&lhsLarger.Value(), rhs, lhs}); IrValue& rescaled = ir.Emit(IrOpcode::FPFma32, IrType::F32, {&ir.Emit(IrOpcode::FPMul32, IrType::F32, {&scaled, &power}), &other, &ir.Emit(IrOpcode::FPMul32, IrType::F32, {addend, &power})}); IrValue& afterwards = ir.Emit(IrOpcode::FPMul32, IrType::F32, {&plain, &power}); IrValue& scaledResult = ir.Emit(IrOpcode::SelectF32, IrType::F32, {&rescale.Value(), &rescaled, &afterwards}); - const auto isNan = [&](IrValue& value) { return IrU1(ir.UGreaterThan(ir.BitwiseAnd(ir.BitCastU32(value), ir.Constant(0x7fffffffu)), ir.Constant(0x7f800000u))); }; - const auto quiet = [&](IrValue& value) { return IrU32(ir.BitwiseOr(ir.BitCastU32(value), ir.Constant(0x400000u))); }; IrValue& computed = ir.Emit(IrOpcode::SelectF32, IrType::F32, {&scale.Value(), &scaledResult, &plain}); - IrU32 result(ir.Select(isNan(computed).Value(), ir.Constant(0xffc00000u), ir.BitCastU32(computed))); - result = IrU32(ir.Select(isNan(*addend).Value(), quiet(*addend).Value(), result.Value())); - result = IrU32(ir.Select(isNan(*rhs).Value(), quiet(*rhs).Value(), result.Value())); - result = IrU32(ir.Select(isNan(*lhs).Value(), quiet(*lhs).Value(), result.Value())); - writeOperand(inst.destination, &ir.BitCastF32(result.Value())); + writeOperand(inst.destination, nanResultF32({lhs, rhs, addend}, &computed, &invalidProductF32(lhs, rhs))); return true; } bool TranslationContext::vDivFixupF32(const RdnaInstruction& inst) { - const IrU32 quotient(ir.BitCastU32(*readOperand(sourceAt(inst, 0u), IrType::F32))); - const IrU32 denominator(ir.BitCastU32(*readOperand(sourceAt(inst, 1u), IrType::F32))); - const IrU32 numerator(ir.BitCastU32(*readOperand(sourceAt(inst, 2u), IrType::F32))); + const IrU32 quotient = readU32(sourceAt(inst, 0u)); + const IrU32 denominator = flushF32Denormal(readU32(sourceAt(inst, 1u))); + const IrU32 numerator = flushF32Denormal(readU32(sourceAt(inst, 2u))); const auto magnitude = [&](IrU32 word) { return IrU32(ir.BitwiseAnd(word.Value(), ir.Constant(0x7fffffffu))); }; const auto isNan = [&](IrU32 word) { return IrU1(ir.UGreaterThan(magnitude(word).Value(), ir.Constant(0x7f800000u))); }; const auto isInf = [&](IrU32 word) { return IrU1(ir.IEqual(magnitude(word).Value(), ir.Constant(0x7f800000u))); }; @@ -531,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; } @@ -566,16 +680,70 @@ bool TranslationContext::ieeeMinMaxF32(const RdnaInstruction& inst, IrOpcode opc return true; } -bool TranslationContext::vFmaLegacyF32(const RdnaInstruction& inst) { +IrU32 TranslationContext::minMaxF16Bits(bool maxValue, IrU32 lhs, IrU32 rhs) { + const auto order = [&](const IrU32& bits) -> IrValue& { + IrValue& value = ir.Emit(IrOpcode::BitFieldSExtract, IrType::U32, {&bits.Value(), &ir.Constant(0u), &ir.Constant(16u)}); + 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)})); + return IrU32(ir.Select(ordered.Value(), lhs.Value(), rhs.Value())); +} + +bool TranslationContext::minMaxF16(const RdnaInstruction& inst, IrOpcode opcode) { + const bool ternary = opcode == IrOpcode::FPMinTri32 || opcode == IrOpcode::FPMaxTri32 || opcode == IrOpcode::FPMedTri32; + const auto isNan = [&](const IrU32& bits) { + return IrU1(ir.UGreaterThan(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffu)), ir.Constant(0x7c00u))); + }; + const auto isSnan = [&](const IrU32& bits) { + return IrU1(ir.LogicalAnd(isNan(bits).Value(), ir.IEqual(ir.BitwiseAnd(bits.Value(), ir.Constant(0x0200u)), ir.Constant(0u)))); + }; + std::array args{}; + for (std::uint32_t index = 0u; index < (ternary ? 3u : 2u); ++index) { + args[index] = readF16Bits(sourceAt(inst, index)); + if (floatMode.has_value() && (floatMode->floatMode & 0x40u) == 0u) { + const IrU1 tiny(ir.ULessThan(ir.BitwiseAnd(args[index].Value(), ir.Constant(0x7fffu)), ir.Constant(0x0400u))); + args[index] = IrU32(ir.Select(tiny.Value(), ir.BitwiseAnd(args[index].Value(), ir.Constant(0x8000u)), args[index].Value())); + } + } + const auto pair = [&](IrOpcode pairOpcode, const IrU32& lhs, const IrU32& rhs) { + IrU32 bits(ir.Select(isNan(lhs).Value(), rhs.Value(), minMaxF16Bits(pairOpcode == IrOpcode::FPMax32, lhs, rhs).Value())); + bits = IrU32(ir.Select(isNan(rhs).Value(), lhs.Value(), bits.Value())); + if (ieeeMode) { + bits = IrU32(ir.Select(isSnan(rhs).Value(), quietNan16(rhs).Value(), bits.Value())); + bits = IrU32(ir.Select(isSnan(lhs).Value(), quietNan16(lhs).Value(), bits.Value())); + } + return bits; + }; + IrU32 result; + if (opcode == IrOpcode::FPMin32 || opcode == IrOpcode::FPMax32) { + result = pair(opcode, args[0], args[1]); + } else if (opcode == IrOpcode::FPMinTri32 || opcode == IrOpcode::FPMaxTri32) { + const auto pairOpcode = opcode == IrOpcode::FPMinTri32 ? IrOpcode::FPMin32 : IrOpcode::FPMax32; + result = pair(pairOpcode, pair(pairOpcode, args[0], args[1]), args[2]); + } else { + const IrU1 anyNan(ir.LogicalOr(ir.LogicalOr(isNan(args[0]).Value(), isNan(args[1]).Value()), isNan(args[2]).Value())); + const IrU32 minimum = pair(IrOpcode::FPMin32, pair(IrOpcode::FPMin32, args[0], args[1]), args[2]); + const IrU32 median = minMaxF16Bits(true, minMaxF16Bits(false, args[0], args[1]), minMaxF16Bits(false, minMaxF16Bits(true, args[0], args[1]), args[2])); + result = IrU32(ir.Select(anyNan.Value(), minimum.Value(), median.Value())); + } + write16Bits(inst.destination, clampF16Bits(inst.destination, result)); + return true; +} + +bool TranslationContext::vFmaLegacyF32(const RdnaInstruction& inst, bool accumulate) { IrValue* lhs = readOperand(sourceAt(inst, 0u), IrType::F32); IrValue* rhs = readOperand(sourceAt(inst, 1u), IrType::F32); - IrValue* addend = readOperand(sourceAt(inst, 2u), IrType::F32); + IrValue* addend = readOperand(accumulate ? accumulatorOperand(inst) : sourceAt(inst, 2u), IrType::F32); const auto isZero = [&](IrValue* value) { return IrU1(ir.IEqual(ir.BitwiseAnd(ir.BitCastU32(*value), ir.Constant(0x7fffffffu)), ir.Constant(0u))); }; const IrU1 zero(ir.LogicalOr(isZero(lhs).Value(), isZero(rhs).Value())); const auto factor = [&](IrValue* value) { return &ir.Emit(IrOpcode::SelectF32, IrType::F32, {&zero.Value(), &ir.ConstantF32(0.0f), value}); }; IrValue* lhsFactor = factor(lhs); IrValue* rhsFactor = factor(rhs); - writeOperand(inst.destination, nanResultF32({lhsFactor, rhsFactor, addend}, &ir.Emit(IrOpcode::FPFma32, IrType::F32, {lhsFactor, rhsFactor, addend}), &invalidProductF32(lhsFactor, rhsFactor))); + IrValue* product = &flushTinyProduct(lhsFactor, rhsFactor, &ir.Emit(IrOpcode::FPMul32, IrType::F32, {lhsFactor, rhsFactor}), nullptr, false).Value(); + const IrU1 zeroSum(ir.LogicalAnd(isZero(product).Value(), isZero(addend).Value())); + IrValue& zeroSumBits = ir.BitwiseAnd(ir.BitwiseAnd(ir.BitCastU32(*product), ir.BitCastU32(*addend)), ir.Constant(0x80000000u)); + IrValue* sum = &ir.Emit(IrOpcode::SelectF32, IrType::F32, {&zeroSum.Value(), &ir.BitCastF32(zeroSumBits), &ir.Emit(IrOpcode::FPAdd32, IrType::F32, {product, addend})}); + writeOperand(inst.destination, nanResultF32({lhsFactor, rhsFactor, addend}, sum, &invalidProductF32(lhsFactor, rhsFactor))); return true; } @@ -607,10 +775,50 @@ bool TranslationContext::vLdexpF32(const RdnaInstruction& inst) { const IrU32 sign(ir.BitwiseAnd(bits.Value(), ir.Constant(0x80000000u))); const IrU32 exponent(ir.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&bits.Value(), &ir.Constant(23u), &ir.Constant(8u)})); const IrU32 clamped(ir.Emit(IrOpcode::SMax32, IrType::U32, {&ir.Emit(IrOpcode::SMin32, IrType::U32, {&offset.Value(), &ir.Constant(512u)}), &ir.Constant(static_cast(-512))})); + const IrU32 overflow(ir.BitwiseOr(sign.Value(), ir.Constant(0x7f800000u))); + const std::uint32_t denorm32 = floatMode.has_value() ? (floatMode->floatMode >> 4u) & 3u : 0u; + const std::uint32_t roundMode = floatMode.has_value() ? floatMode->floatMode & 3u : 0u; + IrValue* positive = roundMode == 1u || roundMode == 2u ? &ir.IEqual(sign.Value(), ir.Constant(0u)) : nullptr; + IrValue* overflowBits = &overflow.Value(); + 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 != 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)))); + const IrU32 msb(ir.Emit(IrOpcode::FindUMsb32, IrType::U32, {&sig.Value()})); + const IrU32 normShift(ir.ISub(ir.Constant(23u), msb.Value())); + const IrU32 sigNorm(ir.ShiftLeftLogical(sig.Value(), normShift.Value())); + const IrU32 effExp(ir.Select(isSubnormal.Value(), ir.Constant(1u), exponent.Value())); + const IrU32 expNorm(ir.ISub(ir.IAdd(effExp.Value(), clamped.Value()), normShift.Value())); + const IrU32 normResult(ir.BitwiseOr(sign.Value(), ir.BitwiseOr(ir.ShiftLeftLogical(expNorm.Value(), ir.Constant(23u)), ir.BitwiseAnd(sigNorm.Value(), ir.Constant(0x007fffffu))))); + const IrU32 underShiftRaw(ir.ISub(ir.Constant(1u), expNorm.Value())); + const IrU32 underShift(ir.Emit(IrOpcode::SMin32, IrType::U32, {&ir.Emit(IrOpcode::SMax32, IrType::U32, {&underShiftRaw.Value(), &ir.Constant(1u)}), &ir.Constant(25u)})); + const IrU32 unrounded(ir.ShiftRightLogical(sigNorm.Value(), underShift.Value())); + const IrU32 half(ir.ShiftLeftLogical(ir.Constant(1u), ir.ISub(underShift.Value(), ir.Constant(1u)))); + const IrU32 remMask(ir.BitwiseOr(ir.ISub(half.Value(), ir.Constant(1u)), half.Value())); + const IrU32 rem(ir.BitwiseAnd(sigNorm.Value(), remMask.Value())); + const IrU1 remGreater(ir.UGreaterThan(rem.Value(), half.Value())); + const IrU1 remEqual(ir.IEqual(rem.Value(), half.Value())); + const IrU1 lsbOdd(ir.INotEqual(ir.BitwiseAnd(unrounded.Value(), ir.Constant(1u)), ir.Constant(0u))); + IrValue* roundUp = &ir.LogicalOr(remGreater.Value(), ir.LogicalAnd(remEqual.Value(), lsbOdd.Value())); + if (roundMode == 1u) roundUp = &ir.LogicalAnd(ir.INotEqual(rem.Value(), ir.Constant(0u)), *positive); + else if (roundMode == 2u) roundUp = &ir.LogicalAnd(ir.INotEqual(rem.Value(), ir.Constant(0u)), ir.LogicalNot(*positive)); + 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)}), (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())); + return true; + } const IrU32 scaled(ir.IAdd(exponent.Value(), clamped.Value())); const IrU32 normal(ir.BitwiseOr(ir.BitwiseAnd(bits.Value(), ir.Constant(0x807fffffu)), ir.ShiftLeftLogical(scaled.Value(), ir.Constant(23u)))); - const IrU32 overflow(ir.BitwiseOr(sign.Value(), ir.Constant(0x7f800000u))); - IrU32 result(ir.Select(ir.Emit(IrOpcode::SGreaterThanEqual32, IrType::U1, {&scaled.Value(), &ir.Constant(255u)}), overflow.Value(), normal.Value())); + IrU32 result(ir.Select(ir.Emit(IrOpcode::SGreaterThanEqual32, IrType::U1, {&scaled.Value(), &ir.Constant(255u)}), *overflowBits, normal.Value())); result = IrU32(ir.Select(ir.Emit(IrOpcode::SLessThanEqual32, IrType::U1, {&scaled.Value(), &ir.Constant(0u)}), 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())); @@ -731,7 +939,15 @@ bool TranslationContext::floatCube(const RdnaInstruction& inst, std::uint32_t re default: throw std::runtime_error("invalid cube result kind"); } - writeOperand(inst.destination, &result.Value()); + const IrU32 bits(ir.BitCastU32(result.Value())); + const IrU1 nan(ir.UGreaterThan(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffffffu)), ir.Constant(0x7f800000u))); + 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 2f5a7d6c5b..cbd1075512 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -86,11 +86,16 @@ 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(); } -void translateExternalFetch(TranslationContext& context, const RdnaInstruction& call, const ShaderVertexInputInfo* input) { +template +void forEachExternalFetchLoad(const RdnaInstruction& call, const ShaderVertexInputInfo* input, TAction action) { if (input == nullptr || input->resourcesNum <= 0) { throw std::runtime_error("s_swappc_b64 fetch-shader call at program counter " + std::to_string(call.programCounter) + " has no parsed fetch-shader plan to inline"); } @@ -108,8 +113,9 @@ void translateExternalFetch(TranslationContext& context, const RdnaInstruction& fetch.programCounter = call.programCounter; fetch.destination.kind = RdnaOperandKind::VectorRegister; fetch.destination.reg = static_cast(destination.registerStart); + fetch.dataDwordCount = static_cast(destination.registersNum); fetch.formatted = true; - context.TranslateEmbeddedFetch(fetch, static_cast(index), static_cast(destination.registersNum)); + action(static_cast(index), fetch); } } @@ -161,9 +167,31 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO IrBuilder entryIr(program); entryIr.SetInsertionPoint(entryBlock); + const auto setVector = [&entryIr, &program](VectorReg reg, IrValue& value) { + auto& metadata = program.Metadata(); + const auto index = RegIndex(reg); + if (metadata.vectorWindowSize != 0u && index >= metadata.vectorWindowBase) { + metadata.vectorWindowSize = std::max(metadata.vectorWindowSize, index - metadata.vectorWindowBase + 1u); + } + entryIr.SetVectorReg(reg, value); + }; + const auto builtin = [&entryIr](StageInputKind kind, std::uint32_t component = 0u) -> IrValue& { return entryIr.Emit(IrOpcode::GetBuiltin, IrOpcodeType(IrOpcode::GetBuiltin), {&entryIr.Constant(static_cast(kind)), &entryIr.Constant(component)}); }; + const auto drawIndex = [&entryIr, &options, &builtin](StageInputKind kind) -> IrValue& { + const auto* fetch = options.embeddedFetch; + const std::int32_t folded = fetch == nullptr ? -1 : kind == StageInputKind::VertexIndex ? fetch->vertexOffsetSgpr : fetch->instanceOffsetSgpr; + IrValue& index = builtin(kind); + return folded < 0 ? index : entryIr.ISub(index, entryIr.GetUserData(static_cast(folded))); + }; + 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); @@ -198,7 +226,7 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO const auto* cs = options.inputInfo.compute; const std::uint32_t threadIds = cs->threadIdsNum > 0 ? std::min(static_cast(cs->threadIdsNum), 3u) : 0u; for (std::uint32_t index = 0; index < threadIds; index++) { - entryIr.SetVectorReg(static_cast(index), builtin(StageInputKind::LocalInvocationId, index)); + setVector(static_cast(index), builtin(StageInputKind::LocalInvocationId, index)); } std::uint32_t regOffset = 0; for (std::uint32_t index = 0; index < 3u; index++) { @@ -211,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)); } @@ -219,7 +247,7 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO const auto& mesh = options.inputInfo.vertex->mesh; const std::uint32_t size = mesh.InputPrimitiveSize(); const std::uint32_t stepCount = mesh.InputPrimitiveStep(); - if (options.waveSize != 64u || mesh.primitivesPerGroup == 0u || mesh.verticesPerGroup != mesh.InputVertexCount(mesh.primitivesPerGroup) || mesh.verticesPerGroup > totalThreads || mesh.primitivesPerGroup > totalThreads || totalThreads % 64u != 0u || totalThreads > 15u * 64u || mesh.esgsItemSize == 0u || mesh.esgsItemSize * mesh.verticesPerGroup > 0xffffu) { + if (mesh.primitivesPerGroup == 0u || mesh.verticesPerGroup != mesh.InputVertexCount(mesh.primitivesPerGroup) || mesh.verticesPerGroup > totalThreads || mesh.primitivesPerGroup > totalThreads || totalThreads % options.waveSize != 0u || totalThreads / options.waveSize > 15u || mesh.esgsItemSize == 0u || mesh.esgsItemSize * mesh.verticesPerGroup > 0xffffu) { throw std::runtime_error("mesh shader translation configuration is not supported (wave " + std::to_string(options.waveSize) + ", primitives per group " + std::to_string(mesh.primitivesPerGroup) + ", vertices per group " + std::to_string(mesh.verticesPerGroup) + ", threads " + std::to_string(totalThreads) + ", ESGS item size " + std::to_string(mesh.esgsItemSize) + ")"); } constexpr std::uint32_t kTriFanPrimitiveType = 5u; @@ -250,28 +278,36 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO IrValue& vertices = minimum(subtractSaturate(indexCount, firstVertex), u32(mesh.verticesPerGroup)); IrValue& primitives = entryIr.Select(entryIr.ULessThan(vertices, u32(size)), u32(0u), entryIr.IAdd(entryIr.Emit(IrOpcode::UDiv32, IrOpcodeType(IrOpcode::UDiv32), {&subtractSaturate(vertices, u32(size)), &step}), u32(1u))); entryIr.SetScalarReg(static_cast(2), entryIr.BitwiseOr(entryIr.ShiftLeftLogical(vertices, u32(12u)), entryIr.ShiftLeftLogical(primitives, u32(22u)))); - IrValue& wave = entryIr.ShiftRightLogical(local, u32(6u)); - IrValue& waveBase = entryIr.BitwiseAnd(local, u32(~63u)); - IrValue& vertexCount = minimum(subtractSaturate(vertices, waveBase), u32(64u)); - IrValue& primitiveCount = minimum(subtractSaturate(primitives, waveBase), u32(64u)); - IrValue& waveInfo = entryIr.BitwiseOr(entryIr.ShiftLeftLogical(wave, u32(24u)), u32((totalThreads / 64u) << 28u)); + IrValue& wave = entryIr.ShiftRightLogical(local, u32(options.waveSize == 32u ? 5u : 6u)); + IrValue& waveBase = entryIr.BitwiseAnd(local, u32(~(options.waveSize - 1u))); + IrValue& vertexCount = minimum(subtractSaturate(vertices, waveBase), u32(options.waveSize)); + IrValue& primitiveCount = minimum(subtractSaturate(primitives, waveBase), u32(options.waveSize)); + IrValue& waveInfo = entryIr.BitwiseOr(entryIr.ShiftLeftLogical(wave, u32(24u)), u32((totalThreads / options.waveSize) << 28u)); entryIr.SetScalarReg(static_cast(3), entryIr.BitwiseOr(waveInfo, entryIr.BitwiseOr(entryIr.ShiftLeftLogical(primitiveCount, u32(8u)), vertexCount))); - IrValue& parity = mesh.inputPrimitive == kTriStripPrimitiveType ? entryIr.BitwiseAnd(entryIr.IAdd(firstPrimitive, local), u32(1u)) : u32(0u); + const bool strip = mesh.inputPrimitive == kTriStripPrimitiveType; + IrValue& indexWord = draw(3u); + IrValue& stripPosition = entryIr.IAdd(entryIr.IAdd(firstPrimitive, local), firstIndex); + IrValue& restartTable = entryIr.INotEqual(entryIr.BitwiseAnd(indexWord, u32(MeshIndexRestartTable)), u32(0u)); + IrValue& stripStart = strip ? entryIr.Emit(IrOpcode::MeshRestartStart, IrOpcodeType(IrOpcode::MeshRestartStart), {&entryIr.IAdd(stripPosition, u32(2u)), &restartTable}) : u32(0u); + IrValue& parity = strip ? entryIr.BitwiseAnd(entryIr.ISub(stripPosition, entryIr.Emit(IrOpcode::UMax32, IrOpcodeType(IrOpcode::UMax32), {&stripStart, &firstIndex})), u32(1u)) : u32(0u); IrValue& vertex = entryIr.IMul(local, step); IrValue& item = u32(mesh.esgsItemSize); - IrValue& first = fan ? entryIr.IMul(entryIr.IAdd(vertex, u32(1u)), item) : entryIr.IMul(entryIr.IAdd(vertex, parity), item); - IrValue& second = fan ? entryIr.IMul(entryIr.IAdd(vertex, u32(2u)), item) : size >= 2u ? entryIr.IMul(entryIr.ISub(entryIr.IAdd(vertex, u32(1u)), parity), item) : u32(0u); - IrValue& third = fan ? u32(0u) : size == 3u ? entryIr.IMul(entryIr.IAdd(vertex, u32(2u)), item) : u32(0u); - entryIr.SetVectorReg(static_cast(0), entryIr.BitwiseOr(entryIr.BitwiseAnd(first, u32(0xffffu)), entryIr.ShiftLeftLogical(second, u32(16u)))); - entryIr.SetVectorReg(static_cast(1), entryIr.BitwiseAnd(third, u32(0xffffu))); - entryIr.SetVectorReg(static_cast(2), entryIr.IAdd(firstPrimitive, local)); - entryIr.SetVectorReg(static_cast(3), u32(0u)); - entryIr.SetVectorReg(static_cast(4), u32(0u)); + const auto unlessRestarted = [&](IrValue& offset) -> IrValue& { + return strip ? entryIr.Select(entryIr.ULessThan(stripPosition, stripStart), entryIr.IMul(vertex, item), offset) : offset; + }; + IrValue& first = fan ? entryIr.IMul(entryIr.IAdd(vertex, u32(1u)), item) : unlessRestarted(entryIr.IMul(entryIr.IAdd(vertex, parity), item)); + IrValue& second = fan ? entryIr.IMul(entryIr.IAdd(vertex, u32(2u)), item) : size >= 2u ? unlessRestarted(entryIr.IMul(entryIr.ISub(entryIr.IAdd(vertex, u32(1u)), parity), item)) : u32(0u); + IrValue& third = fan ? u32(0u) : size == 3u ? unlessRestarted(entryIr.IMul(entryIr.IAdd(vertex, u32(2u)), item)) : u32(0u); + setVector(static_cast(0), entryIr.BitwiseOr(entryIr.BitwiseAnd(first, u32(0xffffu)), entryIr.ShiftLeftLogical(second, u32(16u)))); + setVector(static_cast(1), entryIr.BitwiseAnd(third, u32(0xffffu))); + setVector(static_cast(2), entryIr.IAdd(firstPrimitive, local)); + setVector(static_cast(3), u32(0u)); + setVector(static_cast(4), u32(0u)); if (options.userDataBaseRegister != 0u || options.userDataCount < 8u) { throw std::runtime_error("mesh shader translation requires the merged program's eight hidden user words"); } IrValue& inputVertex = fan ? entryIr.Select(entryIr.IEqual(local, u32(0u)), u32(0u), entryIr.IAdd(firstVertex, local)) : entryIr.IAdd(firstVertex, local); - IrValue& indexBytes = draw(3u); + IrValue& indexBytes = entryIr.BitwiseAnd(indexWord, u32(0xffu)); IrValue& indexed = entryIr.INotEqual(indexBytes, u32(0u)); IrValue& byteOffset = entryIr.IMul(entryIr.IAdd(inputVertex, firstIndex), indexBytes); IrValue& indexResource = entryIr.Emit(IrOpcode::GetBufferResource, IrOpcodeType(IrOpcode::GetBufferResource), {&entryIr.GetUserData(static_cast(4)), &entryIr.GetUserData(static_cast(5)), &entryIr.GetUserData(static_cast(6)), &entryIr.GetUserData(static_cast(7))}); @@ -282,67 +318,136 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO for (std::uint32_t reg = 4u; reg < 8u; reg++) { entryIr.SetScalarReg(static_cast(reg), u32(0u)); } - entryIr.SetVectorReg(static_cast(5), entryIr.IAdd(draw(1u), entryIr.Select(indexed, index, inputVertex))); - entryIr.SetVectorReg(static_cast(6), u32(0u)); - entryIr.SetVectorReg(static_cast(7), u32(0u)); - entryIr.SetVectorReg(static_cast(8), entryIr.IAdd(draw(2u), builtin(StageInputKind::WorkgroupId, 1u))); + IrValue& restartIndex = entryIr.Select(entryIr.IEqual(indexBytes, u32(2u)), u32(0xffffu), u32(0xffffffffu)); + IrValue& fetchedIndex = entryIr.Select(entryIr.LogicalAnd(restartTable, entryIr.IEqual(index, restartIndex)), u32(0u), index); + setVector(static_cast(5), entryIr.IAdd(draw(1u), entryIr.Select(indexed, fetchedIndex, inputVertex))); + setVector(static_cast(6), u32(0u)); + setVector(static_cast(7), u32(0u)); + setVector(static_cast(8), entryIr.IAdd(draw(2u), builtin(StageInputKind::WorkgroupId, 1u))); } else if (options.stage == ShaderStageKind::Local) { entryIr.SetScalarReg(static_cast(3), entryIr.Constant(64u)); - entryIr.SetVectorReg(static_cast(2), builtin(StageInputKind::VertexIndex)); - entryIr.SetVectorReg(static_cast(3), entryIr.Constant(0u)); - entryIr.SetVectorReg(static_cast(5), builtin(StageInputKind::InstanceIndex)); + 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), entryIr.Emit(IrOpcode::TessellationBase, IrOpcodeType(IrOpcode::TessellationBase), {&entryIr.Constant(0u)})); - entryIr.SetScalarReg(static_cast(4), entryIr.Emit(IrOpcode::TessellationBase, IrOpcodeType(IrOpcode::TessellationBase), {&entryIr.Constant(1u)})); + entryIr.SetScalarReg(static_cast(2), tessellationBase(TessellationBaseKind::OffChip)); + entryIr.SetScalarReg(static_cast(4), tessellationBase(TessellationBaseKind::Factor)); entryIr.SetScalarReg(static_cast(3), entryIr.Constant(0x81010000u | tess.inputControlPoints | (tess.outputControlPoints << 8u))); - entryIr.SetVectorReg(static_cast(0), builtin(StageInputKind::PrimitiveId)); - entryIr.SetVectorReg(static_cast(1), entryIr.ShiftLeftLogical(builtin(StageInputKind::InvocationId), entryIr.Constant(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), entryIr.Emit(IrOpcode::TessellationBase, IrOpcodeType(IrOpcode::TessellationBase), {&entryIr.Constant(0u)})); - entryIr.SetVectorReg(static_cast(5), builtin(StageInputKind::TessCoord, 0u)); - entryIr.SetVectorReg(static_cast(6), builtin(StageInputKind::TessCoord, 1u)); - entryIr.SetVectorReg(static_cast(7), entryIr.Constant(0u)); - entryIr.SetVectorReg(static_cast(8), builtin(StageInputKind::PrimitiveId)); + entryIr.SetScalarReg(static_cast(4), tessellationBase(TessellationBaseKind::OffChip)); + setVector(static_cast(0), tessellationBase(TessellationBaseKind::PassthroughPrimitive)); + setVector(static_cast(5), builtin(StageInputKind::TessCoord, 0u)); + setVector(static_cast(6), builtin(StageInputKind::TessCoord, 1u)); + setVector(static_cast(7), tessellationBase(TessellationBaseKind::RelativeIndex)); + setVector(static_cast(8), builtin(StageInputKind::PrimitiveId)); } else if (options.stage == ShaderStageKind::Pixel) { 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; }; - if (options.fragmentShaderBarycentricEnabled) { - for (const auto [input, kind] : {std::pair{PixelInput::PerspectiveCenter, StageInputKind::BaryCoordSmooth}, std::pair{PixelInput::PerspectiveCentroid, StageInputKind::BaryCoordSmooth}, - std::pair{PixelInput::LinearCenter, StageInputKind::BaryCoordNoPerspective}, std::pair{PixelInput::LinearCentroid, StageInputKind::BaryCoordNoPerspective}}) { - if (!loaded(input)) continue; - entryIr.SetVectorReg(static_cast(vgpr(input)), builtin(kind, 0u)); - entryIr.SetVectorReg(static_cast(vgpr(input) + 1u), builtin(kind, 1u)); - } + 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)) { - entryIr.SetVectorReg(static_cast(vgpr(PixelInput::PositionX)), builtin(StageInputKind::FragCoord, 0u)); + setVector(static_cast(vgpr(PixelInput::PositionX)), position(0u)); } if (loaded(PixelInput::PositionY)) { - entryIr.SetVectorReg(static_cast(vgpr(PixelInput::PositionY)), builtin(StageInputKind::FragCoord, 1u)); + setVector(static_cast(vgpr(PixelInput::PositionY)), position(1u)); } if (loaded(PixelInput::PositionZ)) { - entryIr.SetVectorReg(static_cast(vgpr(PixelInput::PositionZ)), builtin(StageInputKind::FragCoord, 2u)); + setVector(static_cast(vgpr(PixelInput::PositionZ)), builtin(StageInputKind::FragCoord, 2u)); } if (loaded(PixelInput::PositionW)) { - IrValue& reciprocalW = entryIr.BitCastF32(builtin(StageInputKind::FragCoord, 3u)); - IrValue& w = entryIr.Emit(IrOpcode::FPRecip32, IrOpcodeType(IrOpcode::FPRecip32), {&reciprocalW}); - entryIr.SetVectorReg(static_cast(vgpr(PixelInput::PositionW)), entryIr.BitCastU32(w)); + IrValue& reciprocalBits = builtin(StageInputKind::FragCoord, 3u); + IrValue& flushed = entryIr.Select(entryIr.IEqual(entryIr.BitwiseAnd(reciprocalBits, entryIr.Constant(0x7f800000u)), entryIr.Constant(0u)), entryIr.BitwiseAnd(reciprocalBits, entryIr.Constant(0x80000000u)), reciprocalBits); + IrValue& w = entryIr.Emit(IrOpcode::FPRecip32, IrOpcodeType(IrOpcode::FPRecip32), {&entryIr.BitCastF32(flushed)}); + setVector(static_cast(vgpr(PixelInput::PositionW)), entryIr.BitCastU32(w)); } if (loaded(PixelInput::FrontFace)) { - entryIr.SetVectorReg(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)) { - entryIr.SetVectorReg(static_cast(vgpr(PixelInput::Ancillary)), builtin(StageInputKind::PackedAncillary)); + 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)); + } + if (loaded(PixelInput::PositionFixedPoint)) { + 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))}); + setVector(static_cast(vgpr(PixelInput::PositionFixedPoint)), entryIr.BitwiseOr(entryIr.BitwiseAnd(x, entryIr.Constant(0xffffu)), entryIr.ShiftLeftLogical(y, entryIr.Constant(16u)))); } } else if (options.stage == ShaderStageKind::Vertex) { if (options.userDataBaseRegister >= 8u) { - entryIr.SetScalarReg(static_cast(3), entryIr.Constant(options.waveSize | (options.waveSize << 8u))); + const auto hostLanes = options.inputInfo.vertex->hostSubgroupSize; + const std::uint32_t lanes = hostLanes == 1u ? 1u : options.waveSize == 64u && hostLanes == 32u ? 32u : options.waveSize; + entryIr.SetScalarReg(static_cast(2), entryIr.Constant((lanes << 12u) | (lanes << 22u))); + entryIr.SetScalarReg(static_cast(3), entryIr.Constant(lanes | (lanes << 8u))); } - entryIr.SetVectorReg(static_cast(5), builtin(StageInputKind::VertexIndex)); - entryIr.SetVectorReg(static_cast(8), builtin(StageInputKind::InstanceIndex)); + setVector(static_cast(5), drawIndex(StageInputKind::VertexIndex)); + setVector(static_cast(8), drawIndex(StageInputKind::InstanceIndex)); } } @@ -350,6 +455,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"); } @@ -365,21 +473,58 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con continue; } includeInstructionVectorRegisters(instruction, vectorLimit); + if (cfg.hasFetchCall && instruction.programCounter == cfg.fetchCallProgramCounter) { + forEachExternalFetchLoad(instruction, options.inputInfo.vertex, [&](std::uint32_t, const RdnaInstruction& fetch) { + vectorLimit = std::min(NumVectorRegs, std::max(vectorLimit, fetch.destination.reg + fetch.dataDwordCount)); + }); + } } } IrProgram program; program.SetWaveSize(options.waveSize); + std::uint32_t windowBase = NumVectorRegs; + bool indexedWrites = false; + for (const auto& cfgBlock : cfg.blocks) { + for (std::uint32_t index = cfgBlock.instructionBegin; index < cfgBlock.instructionEnd; index++) { + const auto& instruction = decoded.instructions[index]; + if (isCodeTableLoad(cfg, instruction.programCounter)) continue; + switch (instruction.op) { + case RdnaOpcode::VMovrelsB32: + windowBase = std::min(windowBase, instruction.source0.reg); + break; + case RdnaOpcode::VMovreldB32: + windowBase = std::min(windowBase, instruction.destination.reg); + indexedWrites = true; + break; + case RdnaOpcode::VMovrelsdB32: + case RdnaOpcode::VMovrelsd2B32: + case RdnaOpcode::VSwaprelB32: + windowBase = std::min({windowBase, instruction.source0.reg, instruction.destination.reg}); + indexedWrites = true; + break; + default: + break; + } + } + } + const bool ancillaryWindow = options.stage == ShaderStageKind::Pixel && options.inputInfo.pixel != nullptr && options.inputInfo.pixel->psAncillary && options.inputInfo.pixel->psInputVgpr[static_cast(PixelInput::Ancillary)] >= windowBase; + if (windowBase < vectorLimit && !ancillaryWindow) { + program.Metadata().vectorWindowBase = windowBase; + program.Metadata().vectorWindowSize = vectorLimit - windowBase; + program.Metadata().vectorWindowIndexedWrites = indexedWrites; + } program.Resources().stage = toIrShaderStage(options.stage); program.Resources().shaderHash = options.shaderHash; program.Resources().userDataBase = options.userDataBaseRegister; program.Resources().userDataCount = options.userDataCount; program.Info().scratchDwords = options.scratchDwords; + program.Info().sharedMemoryBytes = options.sharedMemoryBytes; if (options.embeddedFetch != nullptr) { program.Info().vertexOffsetSgpr = options.embeddedFetch->vertexOffsetSgpr; program.Info().instanceOffsetSgpr = options.embeddedFetch->instanceOffsetSgpr; - program.Info().vertexOffsetShared = options.embeddedFetch->vertexOffsetShared; - program.Info().instanceOffsetShared = options.embeddedFetch->instanceOffsetShared; + program.Info().vertexOffsetShared = options.embeddedFetch->vertexOffsetShared || options.embeddedFetch->vertexIndexObserved; + program.Info().instanceOffsetShared = options.embeddedFetch->instanceOffsetShared || options.embeddedFetch->instanceIndexObserved; program.Info().vertexOffsetConflict = options.embeddedFetch->vertexOffsetConflict; program.Info().instanceOffsetConflict = options.embeddedFetch->instanceOffsetConflict; } @@ -428,12 +573,26 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con blocks.front()->AddBranch(blocks[blockIndices.at(cfg.blocks.front().id)]); emitEntryPrologue(program, *blocks.front(), options); + if (program.Metadata().vectorWindowSize != 0u) { + vectorLimit = program.Metadata().vectorWindowBase + program.Metadata().vectorWindowSize; + } + if (ancillaryWindow) { + for (const IrValue* instruction : blocks.front()->Instructions()) { + if (instruction->Opcode() == IrOpcode::SetVectorRegister) { + vectorLimit = std::max(vectorLimit, instruction->Argument(0)->Register().index + 1u); + } + } + } + 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)) { @@ -442,6 +601,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()) { @@ -456,7 +619,9 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con } context.TranslateInstruction(instruction); if (cfg.hasFetchCall && instruction.programCounter == cfg.fetchCallProgramCounter) { - translateExternalFetch(context, instruction, options.inputInfo.vertex); + forEachExternalFetchLoad(instruction, options.inputInfo.vertex, [&](std::uint32_t attribute, const RdnaInstruction& fetch) { + context.TranslateEmbeddedFetch(fetch, attribute, fetch.dataDwordCount); + }); } } context.AddBranchCondition(cfgBlock, blockInfos[typedIndex]); diff --git a/core/shader/recompiler/Translation/src/IntegerInstructions.cpp b/core/shader/recompiler/Translation/src/IntegerInstructions.cpp index 4a9400e1a7..353a6b2fc3 100644 --- a/core/shader/recompiler/Translation/src/IntegerInstructions.cpp +++ b/core/shader/recompiler/Translation/src/IntegerInstructions.cpp @@ -20,11 +20,41 @@ bool TranslationContext::integer16Shift(const RdnaInstruction& inst, IrOpcode op bool TranslationContext::integer16Binary(const RdnaInstruction& inst, IrOpcode opcode, bool sign) { const IrU32 lhs = readU16AsU32(sourceAt(inst, 0u), sign); const IrU32 rhs = readU16AsU32(sourceAt(inst, 1u), sign); - const IrU32 result(ir.Emit(opcode, IrType::U32, {&lhs.Value(), &rhs.Value()})); + const IrU32 result = saturateInteger16(inst.destination, IrU32(ir.Emit(opcode, IrType::U32, {&lhs.Value(), &rhs.Value()})), sign); write16Bits(inst.destination, IrU32(ir.BitwiseAnd(result.Value(), ir.Constant(0xffffu)))); 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; @@ -625,7 +655,8 @@ bool TranslationContext::sQuadmask(const RdnaInstruction& inst, bool wide) { bool TranslationContext::bfmB32(const RdnaInstruction& inst) { const IrU32 count(ir.BitwiseAnd(readU32(sourceAt(inst, 0u)).Value(), ir.Constant(31u))); const IrU32 offset(ir.BitwiseAnd(readU32(sourceAt(inst, 1u)).Value(), ir.Constant(31u))); - const IrU32 result(ir.Emit(IrOpcode::BitFieldInsert, IrType::U32, {&ir.Constant(0u), &ir.Constant(0xffffffffu), &offset.Value(), &count.Value()})); + const IrU32 mask = rightMask32(count); + const IrU32 result(ir.ShiftLeftLogical(mask.Value(), offset.Value())); writeOperand(inst.destination, &result.Value()); return true; } diff --git a/core/shader/recompiler/Translation/src/MemoryInstructions/FlatInstructions.cpp b/core/shader/recompiler/Translation/src/MemoryInstructions/FlatInstructions.cpp index 9fbd72b3ec..862c65f29b 100644 --- a/core/shader/recompiler/Translation/src/MemoryInstructions/FlatInstructions.cpp +++ b/core/shader/recompiler/Translation/src/MemoryInstructions/FlatInstructions.cpp @@ -1,6 +1,7 @@ #include "Translation/MemoryInstructions.hpp" #include "Translation/TranslationContext.hpp" #include +#include #include #include @@ -15,6 +16,22 @@ bool wideAddressLoadsEnabled() { return !byteReads; } +bool wideAddressStoresEnabled() { + static const bool byteWrites = std::getenv("APS5_BDA_DWORD_STORES") != nullptr; + return !byteWrites; +} + +IrOpcode wideAddressStoreOpcode(std::uint32_t dwords) { + switch (dwords) { + case 2u: + return IrOpcode::StoreAddressU32x2; + case 3u: + return IrOpcode::StoreAddressU32x3; + default: + return IrOpcode::StoreAddressU32x4; + } +} + IrOpcode wideAddressLoadOpcode(std::uint32_t dwords) { switch (dwords) { case 2u: @@ -136,6 +153,19 @@ bool TranslationContext::flatStore(const RdnaInstruction& inst) { const AddressOperands address = readAddressOperands(inst, 0u); IrValue& active = ir.GetExec(); const std::uint32_t count = memory.dataBits == 32u ? memory.dataDwords : 1u; + if (count > 1u && count <= 4u && memory.kind != ResourceKind::Scratch && wideAddressStoresEnabled()) { + MemoryInfo group = memory; + group.dataDwords = count; + group.componentCount = count; + group.componentIndex = 0u; + std::array parts{}; + for (std::uint32_t index = 0u; index < count; ++index) parts[index] = &readU32(offsetOperand(inst.destination, index)).Value(); + IrValue& data = count == 2u ? ir.Emit(IrOpcode::CompositeConstructU32x2, IrOpcodeType(IrOpcode::CompositeConstructU32x2), {parts[0], parts[1]}) + : count == 3u ? ir.Emit(IrOpcode::CompositeConstructU32x3, IrOpcodeType(IrOpcode::CompositeConstructU32x3), {parts[0], parts[1], parts[2]}) + : ir.Emit(IrOpcode::CompositeConstructU32x4, IrOpcodeType(IrOpcode::CompositeConstructU32x4), {parts[0], parts[1], parts[2], parts[3]}); + (void)ir.Emit(wideAddressStoreOpcode(count), IrType::Void, {address.resource, address.low, address.high, &data, &active}, addMemoryInfo(group, inst.programCounter)); + return true; + } for (std::uint32_t index = 0u; index < count; ++index) { MemoryInfo component = memory; component.offset += index * 4u; diff --git a/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp b/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp index 4b5b9a0faa..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,16 +203,8 @@ 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)); - const bool dref = (memory.imageSampleFlags & RdnaImageSampleFlagCompare) != 0u; - if (dref && memory.dataBits != 16u) { - IrValue& component = ir.CompositeExtract(result, 0u); - for (std::uint32_t index = 0; index < memory.dataDwords; ++index) { - writeOperand(offsetOperand(inst.destination, index), &component); - } - } else { - writeImageComponents(inst.destination, &result, memory, 4u); - } + 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; } @@ -224,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 8e4e86c872..b0680c202a 100644 --- a/core/shader/recompiler/Translation/src/MemoryInstructions/MemoryInstructionDispatch.cpp +++ b/core/shader/recompiler/Translation/src/MemoryInstructions/MemoryInstructionDispatch.cpp @@ -29,13 +29,16 @@ bool TranslationContext::emitMemory(const RdnaInstruction& inst) { switch (inst.op) { case RdnaOpcode::SGl1Inv: case RdnaOpcode::SDcacheInv: + case RdnaOpcode::BufferGl0Inv: + case RdnaOpcode::BufferGl1Inv: + program.Metadata().invalidatesCaches = true; + emitControlNop(); + return true; case RdnaOpcode::SDcacheWb: case RdnaOpcode::SAtcProbe: case RdnaOpcode::SAtcProbeBuffer: case RdnaOpcode::SDcacheDiscard: case RdnaOpcode::SDcacheDiscardX2: - case RdnaOpcode::BufferGl0Inv: - case RdnaOpcode::BufferGl1Inv: emitControlNop(); return true; case RdnaOpcode::SMemtime: @@ -499,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 be58eaf478..a781713ddc 100644 --- a/core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp +++ b/core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp @@ -31,7 +31,9 @@ MemoryInfo sharedMemoryInfoFromInstruction(const RdnaInstruction& inst) { } bool TranslationContext::dsAtomic(const RdnaInstruction& inst, IrOpcode opcode, bool returnsValue) { - const MemoryInfo memory = sharedMemoryInfoFromInstruction(inst); + 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(); @@ -211,8 +213,25 @@ bool TranslationContext::dsSrc2(const RdnaInstruction& inst, IrOpcode opcode) { return true; } +bool TranslationContext::sharedFloatFlush(IrOpcode opcode) const { + switch (opcode) { + 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: + case IrOpcode::SharedAtomicCmpstF64: + return floatMode.has_value() && ((floatMode->floatMode >> 6u) & 1u) == 0u; + default: + return false; + } +} + bool TranslationContext::dsAtomic2(const RdnaInstruction& inst, IrOpcode opcode, bool returnsValue) { - const MemoryInfo memory = sharedMemoryInfoFromInstruction(inst); + MemoryInfo memory = sharedMemoryInfoFromInstruction(inst); + memory.flushDenormals = sharedFloatFlush(opcode); const IrU32 address = readU32(inst.source0); const IrU32 data0 = readU32(inst.source1); const IrU32 data1 = readU32(inst.source2); @@ -226,7 +245,8 @@ bool TranslationContext::dsAtomic64(const RdnaInstruction& inst, IrOpcode opcode if (inst.gds) { throw std::runtime_error("64-bit GDS atomics are not supported"); } - const MemoryInfo memory = sharedMemoryInfoFromInstruction(inst); + MemoryInfo memory = sharedMemoryInfoFromInstruction(inst); + memory.flushDenormals = sharedFloatFlush(opcode); const IrU32 address = readU32(inst.source0); const IrU64 data0 = readU64(inst.source1); IrValue& active = ir.GetExec(); @@ -275,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 7136560e73..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 @@ -9,9 +10,39 @@ void TranslateScalarInstruction(IrBuilder& builder, const RdnaInstruction& instr throw std::runtime_error("TranslateScalarInstruction not implemented"); } +template +bool TranslationContext::sU32Mask(const RdnaInstruction& inst, IrOpcode logicalOpcode, bool binary, bool negateRhs, bool negateResult, TWord&& word) { + const auto& destination = inst.destination; + const auto execOrVcc = [](const RdnaOperand& operand) { return operand.kind == RdnaOperandKind::ExecLo || operand.kind == RdnaOperandKind::VccLo; }; + const bool involved = execOrVcc(destination) || execOrVcc(sourceAt(inst, 0u)) || (binary && execOrVcc(sourceAt(inst, 1u))); + const bool exact = maskLanesExact(sourceAt(inst, 0u)) && (!binary || maskLanesExact(sourceAt(inst, 1u))); + if (program.WaveSize() != 32u || !involved || !exact || (destination.kind != RdnaOperandKind::ScalarRegister && !execOrVcc(destination))) return word(); + IrU1 lane = binary ? u64MaskBinary(inst, logicalOpcode, negateRhs, negateResult) : readMask(sourceAt(inst, 0u)); + if (!binary && negateResult) lane = IrU1(ir.LogicalNot(lane.Value())); + IrU1 valid = readMaskValid(sourceAt(inst, 0u)); + if (binary) valid = IrU1(ir.LogicalAnd(valid.Value(), readMaskValid(sourceAt(inst, 1u)).Value())); + const bool result = word(); + switch (destination.kind) { + case RdnaOperandKind::ExecLo: + ir.SetExec(lane.Value()); + break; + case RdnaOperandKind::VccLo: + ir.SetVcc(lane.Value()); + break; + default: { + const auto reg = static_cast(destination.reg); + ir.SetThreadBitScalarReg(reg, lane.Value()); + ir.SetScalarMaskTag(reg, valid.Value()); + break; + } + } + return result; +} + bool TranslationContext::emitScalar(const RdnaInstruction& inst) { switch (inst.op) { case RdnaOpcode::SMovB32: + return sU32Mask(inst, IrOpcode::LogicalAnd, false, false, false, [&] { movB32(inst, false); return true; }); case RdnaOpcode::SMovkI32: movB32(inst, false); return true; @@ -43,7 +74,7 @@ bool TranslationContext::emitScalar(const RdnaInstruction& inst) { sCselectB32(inst); return true; case RdnaOpcode::SCselectB64: - scalarSelect64(inst, sourceAt(inst, 1u)); + scalarSelectMask64(inst); return true; case RdnaOpcode::SCmovB32: { const IrU32 source = readU32(sourceAt(inst, 0u)); @@ -56,17 +87,16 @@ bool TranslationContext::emitScalar(const RdnaInstruction& inst) { scalarSelect64(inst, inst.destination); return true; case RdnaOpcode::SSetregB32: + if ((inst.source1.value & 0x3fu) == 1u) { + writeRounding(inst); + } + emitControlNop(); + return true; case RdnaOpcode::SVersion: 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 || written != 0u) { - throw std::runtime_error("s_setreg_imm32_b32 at pc " + std::to_string(inst.programCounter) + " writes a hardware register field other than round to nearest even in MODE"); - } + writeRounding(inst); emitControlNop(); return true; } @@ -77,7 +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"); } - writeRawU32(inst.destination, IrU32(ir.Constant(0u))); + 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: { @@ -271,13 +305,13 @@ bool TranslationContext::emitScalar(const RdnaInstruction& inst) { case RdnaOpcode::SMulHiI32: return simpleInteger(inst, IrOpcode::SMulHi, IrType::U32, false, false, false); case RdnaOpcode::SAndB32: - return simpleInteger(inst, IrOpcode::BitwiseAnd32, IrType::U32, false, false, true); + return sU32Mask(inst, IrOpcode::LogicalAnd, true, false, false, [&] { return simpleInteger(inst, IrOpcode::BitwiseAnd32, IrType::U32, false, false, true); }); case RdnaOpcode::SOrB32: - return simpleInteger(inst, IrOpcode::BitwiseOr32, IrType::U32, false, false, true); + return sU32Mask(inst, IrOpcode::LogicalOr, true, false, false, [&] { return simpleInteger(inst, IrOpcode::BitwiseOr32, IrType::U32, false, false, true); }); case RdnaOpcode::SXorB32: - return simpleInteger(inst, IrOpcode::BitwiseXor32, IrType::U32, false, false, true); + return sU32Mask(inst, IrOpcode::LogicalXor, true, false, false, [&] { return simpleInteger(inst, IrOpcode::BitwiseXor32, IrType::U32, false, false, true); }); case RdnaOpcode::SNotB32: - return simpleInteger(inst, IrOpcode::BitwiseNot32, IrType::U32, false, false, true); + return sU32Mask(inst, IrOpcode::LogicalAnd, false, false, true, [&] { return simpleInteger(inst, IrOpcode::BitwiseNot32, IrType::U32, false, false, true); }); case RdnaOpcode::SBrevB32: return simpleInteger(inst, IrOpcode::BitReverse32, IrType::U32, false, false, false); case RdnaOpcode::SBrevB64: { @@ -335,15 +369,15 @@ bool TranslationContext::emitScalar(const RdnaInstruction& inst) { case RdnaOpcode::SLshrB64: return simpleInteger(inst, IrOpcode::ShiftRightLogical64, IrType::U64, false, false, true); case RdnaOpcode::SAndn2B32: - return composedIntegerBinary(inst, IrOpcode::BitwiseAnd32, true, false, true); + return sU32Mask(inst, IrOpcode::LogicalAnd, true, true, false, [&] { return composedIntegerBinary(inst, IrOpcode::BitwiseAnd32, true, false, true); }); case RdnaOpcode::SOrn2B32: - return composedIntegerBinary(inst, IrOpcode::BitwiseOr32, true, false, true); + return sU32Mask(inst, IrOpcode::LogicalOr, true, true, false, [&] { return composedIntegerBinary(inst, IrOpcode::BitwiseOr32, true, false, true); }); case RdnaOpcode::SNandB32: - return composedIntegerBinary(inst, IrOpcode::BitwiseAnd32, false, true, true); + return sU32Mask(inst, IrOpcode::LogicalAnd, true, false, true, [&] { return composedIntegerBinary(inst, IrOpcode::BitwiseAnd32, false, true, true); }); case RdnaOpcode::SNorB32: - return composedIntegerBinary(inst, IrOpcode::BitwiseOr32, false, true, true); + return sU32Mask(inst, IrOpcode::LogicalOr, true, false, true, [&] { return composedIntegerBinary(inst, IrOpcode::BitwiseOr32, false, true, true); }); case RdnaOpcode::SXnorB32: - return composedIntegerBinary(inst, IrOpcode::BitwiseXor32, false, true, true); + return sU32Mask(inst, IrOpcode::LogicalXor, true, false, true, [&] { return composedIntegerBinary(inst, IrOpcode::BitwiseXor32, false, true, true); }); case RdnaOpcode::SFf0I32B64: return sFfI32B64(inst, true); case RdnaOpcode::SFf1I32B64: @@ -414,9 +448,7 @@ bool TranslationContext::emitScalar(const RdnaInstruction& inst) { emitControlNop(); return true; case RdnaOpcode::SRoundMode: - if (inst.source0.value != 0u) { - throw std::runtime_error("s_round_mode " + std::to_string(inst.source0.value) + " at pc " + std::to_string(inst.programCounter) + " selects a rounding mode other than round to nearest even"); - } + writeRounding(inst); emitControlNop(); return true; case RdnaOpcode::SDenormMode: diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index c75a92eec6..88f4b0fad4 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 != 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) { @@ -111,8 +123,10 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues } pixelStorage.psInputVgpr[static_cast(input)] = PixelInputVgpr(pixel.inputAddr, input); }; + place(PixelInput::PerspectiveSample, (pixel.inputAddr & PixelInputBit(PixelInput::PerspectiveSample)) != 0u); place(PixelInput::PerspectiveCenter, pixel.hasPerspectiveCenterVgpr); place(PixelInput::PerspectiveCentroid, pixel.perspectiveCentroid); + place(PixelInput::LinearSample, (pixel.inputAddr & PixelInputBit(PixelInput::LinearSample)) != 0u); place(PixelInput::LinearCenter, pixel.noPerspective); place(PixelInput::LinearCentroid, pixel.linearCentroid); place(PixelInput::PositionX, pixel.posX); @@ -121,6 +135,9 @@ 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) { pixelStorage.targetOutputMode[i] = pixel.targetOutputMode[i]; } @@ -129,16 +146,28 @@ 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; pixelStorage.psConservativeZExport = pixel.conservativeZExport; pixelStorage.psOrderedPixelShader = pixel.orderedPixelShader; + if (pixel.ditheredAlphaToCoverage && !pixel.alphaToCoverage) throw std::runtime_error("dithered alpha-to-coverage requires enabled coverage"); + pixelStorage.psAlphaToCoverage = pixel.alphaToCoverage; + pixelStorage.psDitheredAlphaToCoverage = pixel.ditheredAlphaToCoverage; + 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; @@ -157,7 +186,10 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues auto& vertexStorage = HostThreadLocal(); vertexStorage = ShaderVertexInputInfo{}; vertexStorage.logicalStage = _toIrShaderStage(stage); + vertexStorage.hostSubgroupSize = hostSubgroupSize; vertexStorage.fetchEmbedded = vertex.fetchEmbedded; + vertexStorage.paClVsOutCntl = vertex.paClVsOutCntl; + vertexStorage.depthRemap = {vertex.depthRemapMode}; vertexStorage.fetchExternal = false; vertexStorage.fetchAttribReg = static_cast(vertex.fetchAttribReg); vertexStorage.fetchBufferReg = static_cast(vertex.fetchBufferReg); @@ -172,18 +204,22 @@ 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->partitioning != 2u || tessellation->outputTopology != 2u) throw std::runtime_error("ShaderInputInfoBuilder: unsupported tessellation configuration"); + _validateTessellation(tessellation); auto& target = vertexStorage.tess; target.inputControlPoints = tessellation->inputControlPoints; target.outputControlPoints = tessellation->outputControlPoints; target.domain = tessellation->domain; target.partitioning = tessellation->partitioning; target.outputTopology = tessellation->outputTopology; + target.patchesPerGroup = tessellation->patchesPerGroup; + 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; @@ -198,7 +234,7 @@ 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; } ShaderStageInputInfo result; result.vertex = &vertexStorage; @@ -211,4 +247,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 66b55249dc..5a87cd02e4 100644 --- a/core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp +++ b/core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp @@ -15,10 +15,22 @@ 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) { + const bool wide = program.WaveSize() == 64u; + if (mask[0].Value().HasImmediate() && (!wide || mask[1].Value().HasImmediate())) { + const std::uint32_t low = mask[0].Value().ImmediateU32(); + const std::uint32_t high = wide ? mask[1].Value().ImmediateU32() : low; + if (low == 0u && high == 0u) return IrU1(ir.ConstantBool(false)); + if (low == 0xffffffffu && high == 0xffffffffu) return IrU1(ir.ConstantBool(true)); + } const IrU32 lane(ir.Emit(IrOpcode::LaneId, IrType::U32, {})); const IrU32 word = program.WaveSize() == 64u ? IrU32(ir.Select(ir.ULessThan(lane.Value(), ir.Constant(32u)), mask[0].Value(), mask[1].Value())) : mask[0]; const IrU32 bit(ir.BitwiseAnd(lane.Value(), ir.Constant(31u))); @@ -41,15 +53,32 @@ IrU1 TranslationContext::readMask(const RdnaOperand& operand) { } case RdnaOperandKind::ExecLo: case RdnaOperandKind::ExecHi: return IrU1(ir.GetExec()); - case RdnaOperandKind::VccLo: + case RdnaOperandKind::VccLo: return IrU1(ir.GetVcc()); case RdnaOperandKind::VccHi: return program.WaveSize() == 32u ? threadBit({readRawU32(operand), IrU32(ir.Constant(0u))}) : IrU1(ir.GetVcc()); case RdnaOperandKind::Scc: return IrU1(ir.GetScc()); case RdnaOperandKind::VccZ: return IrU1(ir.LogicalNot(ir.GetVcc())); case RdnaOperandKind::ExecZ: return IrU1(ir.LogicalNot(ir.GetExec())); + case RdnaOperandKind::SrcSharedBase: + case RdnaOperandKind::SrcSharedLimit: + case RdnaOperandKind::SrcPrivateBase: + case RdnaOperandKind::SrcPrivateLimit: return threadBit(readU32Pair(operand)); default: return IrU1(ir.INotEqual(readRawU32(operand).Value(), ir.Constant(0u))); } } +bool TranslationContext::maskLanesExact(const RdnaOperand& operand) { + switch (operand.kind) { + case RdnaOperandKind::ScalarRegister: + case RdnaOperandKind::ExecLo: + case RdnaOperandKind::VccLo: + case RdnaOperandKind::VccHi: + case RdnaOperandKind::LiteralConstant: + case RdnaOperandKind::IntegerInlineConstant: + case RdnaOperandKind::FloatInlineConstant: return true; + default: return false; + } +} + IrU1 TranslationContext::readMaskValid(const RdnaOperand& operand) { if (operand.kind == RdnaOperandKind::LiteralConstant || operand.kind == RdnaOperandKind::IntegerInlineConstant || operand.kind == RdnaOperandKind::FloatInlineConstant) { const bool zero = operand.value == 0u; @@ -99,7 +128,12 @@ std::array TranslationContext::writeMask(const RdnaOperand& operand, I case RdnaOperandKind::VccLo: case RdnaOperandKind::VccHi: if (!write64 && program.WaveSize() == 32u) { - writeRawU32(operand, mask[0]); + if (operand.kind != RdnaOperandKind::VccLo) { + writeRawU32(operand, mask[0]); + return mask; + } + ir.SetVccLo(mask[0].Value()); + ir.SetVcc(value.Value()); return mask; } ir.SetVcc(value.Value()); diff --git a/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp b/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp index 3136b2f114..d54715a372 100644 --- a/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp +++ b/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp @@ -1,7 +1,9 @@ +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" #include "Translation/TranslationContext.hpp" #include #include #include +#include namespace ShaderRecompiler { @@ -12,6 +14,24 @@ DppMoveFlags dppFlags(const RdnaOperand& operand) { return {control, static_cast(operand.dppRowMask), static_cast(operand.dppBankMask), operand.dppFetchInactive, operand.dppBoundCtrl}; } +const IrValue* constantF32Bits(const IrValue& value) { + if (value.Opcode() != IrOpcode::BitCastF32U32) return nullptr; + const IrValue* bits = value.Argument(0); + return bits->HasImmediate() ? bits : nullptr; +} + +bool keepsProductOutOfTinyRange(const IrValue& factor) { + const IrValue* bits = constantF32Bits(factor); + if (bits == nullptr) return false; + const std::uint32_t exponent = (bits->ImmediateU32() >> 23u) & 0xffu; + return exponent == 0u || exponent >= 127u; +} + +bool hasUnitSignificand(const IrValue& factor) { + const IrValue* bits = constantF32Bits(factor); + return bits != nullptr && (bits->ImmediateU32() & 0x007fffffu) == 0u; +} + } const RdnaOperand& TranslationContext::sourceAt(const RdnaInstruction& inst, std::uint32_t index) { @@ -127,24 +147,53 @@ RdnaOperand TranslationContext::plainOperand(const RdnaOperand& operand) { IrU32 TranslationContext::flushF32Denormal(IrU32 bits) { if ((f32DenormalFlush & 1u) == 0u) return bits; + if (bits.Value().HasImmediate()) { + const std::uint32_t value = bits.Value().ImmediateU32(); + return (value & 0x7f800000u) == 0u ? IrU32(ir.Constant(value & 0x80000000u)) : bits; + } const IrU1 denormal(ir.IEqual(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7f800000u)), ir.Constant(0u))); return IrU32(ir.Select(denormal.Value(), ir.BitwiseAnd(bits.Value(), ir.Constant(0x80000000u)), bits.Value())); } -IrF32 TranslationContext::flushTinyProduct(IrValue* lhs, IrValue* rhs, IrValue* product, IrValue* addend) { - if ((f32DenormalFlush & 2u) == 0u) return IrF32(*product); +IrF32 TranslationContext::flushTinyProduct(IrValue* lhs, IrValue* rhs, IrValue* product, IrValue* addend, bool afterRounding) { + if ((f32DenormalFlush & 2u) == 0u || keepsProductOutOfTinyRange(*lhs) || keepsProductOutOfTinyRange(*rhs)) return IrF32(*product); IrValue& lhsBits = ir.BitCastU32(*lhs); IrValue& rhsBits = ir.BitCastU32(*rhs); + const std::uint32_t roundMode = floatMode.has_value() ? floatMode->floatMode & 3u : 0u; + const auto zeroAddend = [&](IrValue* tiny) -> IrValue* { + if (addend == nullptr) return tiny; + return &ir.LogicalAnd(*tiny, ir.IEqual(ir.BitwiseAnd(ir.BitCastU32(*addend), ir.Constant(0x7fffffffu)), ir.Constant(0u))); + }; + if (afterRounding && roundMode == 0u) { + IrValue& scaled = ir.BitwiseAnd(ir.BitCastU32(ir.Emit(IrOpcode::FPMul32, IrType::F32, {&ir.Emit(IrOpcode::FPMul32, IrType::F32, {lhs, &ir.ConstantF32(0x1p64f)}), rhs})), ir.Constant(0x7fffffffu)); + IrValue* tiny = zeroAddend(&ir.ULessThan(ir.ISub(scaled, ir.Constant(1u)), ir.Constant(0x207fffffu))); + IrValue& sign = ir.BitwiseAnd(ir.BitwiseXor(lhsBits, rhsBits), ir.Constant(0x80000000u)); + return IrF32(ir.Emit(IrOpcode::SelectF32, IrType::F32, {tiny, &ir.BitCastF32(sign), product})); + } const auto exponent = [&](IrValue& bits) -> IrValue& { return ir.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&bits, &ir.Constant(23u), &ir.Constant(8u)}); }; const auto significand = [&](IrValue& bits) -> IrValue& { return ir.BitwiseOr(ir.BitwiseAnd(bits, ir.Constant(0x007fffffu)), ir.Constant(0x00800000u)); }; IrValue& lhsExponent = exponent(lhsBits); IrValue& rhsExponent = exponent(rhsBits); const auto finiteNonZero = [&](IrValue& value) -> IrValue& { return ir.LogicalAnd(ir.INotEqual(value, ir.Constant(0u)), ir.INotEqual(value, ir.Constant(0xffu))); }; - IrValue& carry = ir.Select(ir.UGreaterThan(ir.Emit(IrOpcode::UMulHi, IrType::U32, {&significand(lhsBits), &significand(rhsBits)}), ir.Constant(0x7fffu)), ir.Constant(1u), ir.Constant(0u)); - IrValue* tiny = &ir.LogicalAnd(ir.LogicalAnd(finiteNonZero(lhsExponent), finiteNonZero(rhsExponent)), ir.ULessThan(ir.IAdd(ir.IAdd(lhsExponent, rhsExponent), carry), ir.Constant(128u))); - if (addend != nullptr) tiny = &ir.LogicalAnd(*tiny, ir.IEqual(ir.BitwiseAnd(ir.BitCastU32(*addend), ir.Constant(0x7fffffffu)), ir.Constant(0u))); + IrValue* carry = &ir.Constant(0u); + IrValue* roundsUp = nullptr; + if (!hasUnitSignificand(*lhs) && !hasUnitSignificand(*rhs)) { + IrValue& lhsSignificand = significand(lhsBits); + IrValue& rhsSignificand = significand(rhsBits); + IrValue& high = ir.Emit(IrOpcode::UMulHi, IrType::U32, {&lhsSignificand, &rhsSignificand}); + carry = &ir.Select(ir.UGreaterThan(high, ir.Constant(0x7fffu)), ir.Constant(1u), ir.Constant(0u)); + if (afterRounding && roundMode != 3u) roundsUp = &ir.LogicalAnd(ir.IEqual(high, ir.Constant(0x7fffu)), ir.UGreaterThan(ir.IMul(lhsSignificand, rhsSignificand), ir.Constant(0xff800000u))); + } + IrValue& exponentSum = ir.IAdd(ir.IAdd(lhsExponent, rhsExponent), *carry); + IrValue* tiny = zeroAddend(&ir.LogicalAnd(ir.LogicalAnd(finiteNonZero(lhsExponent), finiteNonZero(rhsExponent)), ir.ULessThan(exponentSum, ir.Constant(128u)))); IrValue& sign = ir.BitwiseAnd(ir.BitwiseXor(lhsBits, rhsBits), ir.Constant(0x80000000u)); - return IrF32(ir.Emit(IrOpcode::SelectF32, IrType::F32, {tiny, &ir.BitCastF32(sign), product})); + IrValue* rounded = &sign; + if (roundsUp != nullptr) { + roundsUp = &ir.LogicalAnd(ir.IEqual(exponentSum, ir.Constant(127u)), *roundsUp); + roundsUp = &ir.LogicalAnd(*roundsUp, ir.IEqual(sign, ir.Constant(roundMode == 1u ? 0u : 0x80000000u))); + rounded = &ir.Select(*roundsUp, ir.BitwiseOr(sign, ir.Constant(0x00800000u)), sign); + } + return IrF32(ir.Emit(IrOpcode::SelectF32, IrType::F32, {tiny, &ir.BitCastF32(*rounded), product})); } IrValue* TranslationContext::readOperand(const RdnaOperand& operand, IrType type) { @@ -173,11 +222,17 @@ IrValue* TranslationContext::readOperand(const RdnaOperand& operand, IrType type return &ir.ConstructU64(pair[0].Value(), pair[1].Value()); } IrU32 bits = applyBitSourceModifiers(operand, readRawU32(operand)); - if (operand.absolute) { - bits = IrU32(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffffffu))); - } - if (operand.negate) { - bits = IrU32(ir.BitwiseXor(bits.Value(), ir.Constant(0x80000000u))); + if (bits.Value().HasImmediate()) { + const std::uint32_t value = bits.Value().ImmediateU32(); + const std::uint32_t magnitude = operand.absolute ? value & 0x7fffffffu : value; + bits = IrU32(ir.Constant(operand.negate ? magnitude ^ 0x80000000u : magnitude)); + } else { + if (operand.absolute) { + bits = IrU32(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffffffu))); + } + if (operand.negate) { + bits = IrU32(ir.BitwiseXor(bits.Value(), ir.Constant(0x80000000u))); + } } if (TypesOverlap(type, IrType::F32) && !TypesOverlap(type, IrType::U32)) { return &ir.BitCastF32(flushF32Denormal(bits).Value()); @@ -307,25 +362,42 @@ 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; } +std::array TranslationContext::quietNan64(const std::array& bits) { + 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()})); @@ -346,6 +418,21 @@ IrF32 TranslationContext::applyF16ResultModifiers(const RdnaOperand& operand, Ir return dx10Clamp() ? clamped : selectF32(IrU1(ir.Emit(IrOpcode::FPIsNan32, IrType::U1, {&value.Value()})), value, clamped); } +IrF32 TranslationContext::clampF16Overflow(IrF32 value, std::initializer_list sources) { + if (!fp16Overflow()) { + return value; + } + const IrU32 bits(ir.BitCastU32(value.Value())); + const IrU32 magnitude(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffffffu))); + IrU1 overflow(ir.LogicalAnd(ir.LogicalNot(ir.ULessThan(magnitude.Value(), ir.Constant(0x477ff000u))), ir.ULessThan(magnitude.Value(), ir.Constant(0x7f800001u)))); + for (IrValue* source : sources) { + const IrU1 infinite(ir.IEqual(ir.BitwiseAnd(ir.BitCastU32(*source), ir.Constant(0x7fffffffu)), ir.Constant(0x7f800000u))); + overflow = IrU1(ir.LogicalAnd(overflow.Value(), ir.LogicalNot(infinite.Value()))); + } + const IrU32 largest(ir.BitwiseOr(ir.BitwiseAnd(bits.Value(), ir.Constant(0x80000000u)), ir.Constant(0x477fe000u))); + return IrF32(ir.BitCastF32(ir.Select(overflow.Value(), largest.Value(), bits.Value()))); +} + IrU32 TranslationContext::clampF16Bits(const RdnaOperand& operand, IrU32 bits) { rejectHalfOrDoubleOutputModifier(operand); if (!operand.clamp) { @@ -371,6 +458,10 @@ IrU32 TranslationContext::readScalarCode(std::uint32_t code) { const std::array mask = ballotMask(IrU1(ir.GetExec())); return mask[code - 126u]; } + case 235u: + case 236u: + case 237u: + case 238u: return readRawU32(DecodeRdnaScalarSource(code, 0u)); default: return IrU32(ir.Constant(0u)); } } @@ -400,6 +491,10 @@ IrU32 TranslationContext::readRawU32(const RdnaOperand& operand) { const IrU32 zero(ir.Constant(0u)); return IrU32(ir.Select(ir.IEqual(mask.Value(), zero.Value()), ir.Constant(1u), zero.Value())); } + case RdnaOperandKind::SrcSharedBase: + case RdnaOperandKind::SrcSharedLimit: + case RdnaOperandKind::SrcPrivateBase: + case RdnaOperandKind::SrcPrivateLimit: return IrU32(ir.Constant(static_cast(RdnaApertureSourceValue(operand)))); default: throw std::runtime_error("TranslationContext::readRawU32 invalid decoded operand used as a raw U32 source"); } } diff --git a/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp b/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp index fc3a74a66f..3ba6021ff2 100644 --- a/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp +++ b/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp @@ -1,4 +1,5 @@ #include "Translation/TranslationContext.hpp" +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" #include #include #include @@ -19,6 +20,10 @@ std::array TranslationContext::readU32Pair(const RdnaOperand& operand) const auto bits = std::bit_cast(static_cast(std::bit_cast(operand.value))); return {IrU32(ir.Constant(static_cast(bits))), IrU32(ir.Constant(static_cast(bits >> 32u)))}; } + case RdnaOperandKind::SrcSharedBase: + case RdnaOperandKind::SrcSharedLimit: + case RdnaOperandKind::SrcPrivateBase: + case RdnaOperandKind::SrcPrivateLimit: return {readU32(operand), IrU32(ir.Constant(static_cast(RdnaApertureSourceValue(operand) >> 32u)))}; default: return {readU32(operand), readU32(offsetOperand(operand, 1u))}; } } @@ -47,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))}; } @@ -112,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) { @@ -125,7 +136,10 @@ IrU32 TranslationContext::readU16LaneRaw(const RdnaOperand& operand, bool highLa } IrU32 TranslationContext::readU16LaneAsU32(const RdnaOperand& operand, bool highLane, bool signExtend) { - const IrU32 raw = readU16LaneRaw(operand, highLane ? operand.opSelHi : operand.opSel); + IrU32 raw = readU16LaneRaw(operand, highLane ? operand.opSelHi : operand.opSel); + if (highLane ? operand.negateHi : operand.negate) { + raw = IrU32(ir.BitwiseXor(raw.Value(), ir.Constant(0x8000u))); + } if (!signExtend) { return raw; } @@ -156,10 +170,99 @@ void TranslationContext::write16Bits(const RdnaOperand& operand, IrU32 value) { writeRawU32(operand, IrU32(ir.BitwiseOr(cleared.Value(), masked.Value()))); } -void TranslationContext::writeF16(const RdnaOperand& operand, IrF32 value) { - const IrF16 half(ir.Emit(IrOpcode::ConvertF16F32, IrType::F16, {&applyF16ResultModifiers(operand, value).Value()})); +bool TranslationContext::directedF16Rounding() const { + return floatMode.has_value() && ((floatMode->floatMode >> 2u) & 3u) != 0u; +} + +IrU32 TranslationContext::flushF16Bits(IrU32 bits, std::uint32_t keepBit) { + if (!floatMode.has_value() || (floatMode->floatMode & keepBit) != 0u) return bits; + const IrU1 tiny(ir.ULessThan(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffu)), ir.Constant(0x0400u))); + return IrU32(ir.Select(tiny.Value(), ir.BitwiseAnd(bits.Value(), ir.Constant(0x8000u)), bits.Value())); +} + +IrU32 TranslationContext::flushF16Input(IrU32 bits) { + return flushF16Bits(bits, 0x40u); +} + +IrU32 TranslationContext::flushF16Result(IrU32 bits) { + return flushF16Bits(bits, 0x80u); +} + +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(threshold))); + return IrF32(ir.BitCastF32(ir.Select(tiny.Value(), ir.BitwiseAnd(bits.Value(), ir.Constant(0x80000000u)), bits.Value()))); +} + +IrU32 TranslationContext::convertF32ToF16Bits(IrF32 value, std::initializer_list sources) { + const IrF16 half(ir.Emit(IrOpcode::ConvertF16F32, IrType::F16, {&clampF16Overflow(value, sources).Value()})); + const IrU32 native(ir.Emit(IrOpcode::ConvertU32U16, IrType::U32, {&ir.Emit(IrOpcode::BitCastU16F16, IrType::U16, {&half.Value()})})); + if (!directedF16Rounding()) { + return native; + } + const std::uint32_t rounding = (floatMode->floatMode >> 2u) & 3u; + const IrU32 bits(ir.BitCastU32(value.Value())); + const IrU32 sign(ir.BitwiseAnd(ir.ShiftRightLogical(bits.Value(), ir.Constant(16u)), ir.Constant(0x8000u))); + const IrU32 magnitude(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffffffu))); + const IrU32 exponent(ir.ShiftRightLogical(magnitude.Value(), ir.Constant(23u))); + const IrU1 normal(ir.UGreaterThan(exponent.Value(), ir.Constant(112u))); + const IrU32 significand(ir.BitwiseOr(ir.BitwiseAnd(magnitude.Value(), ir.Constant(0x7fffffu)), ir.Constant(0x800000u))); + const IrU32 shift(ir.Emit(IrOpcode::UMin32, IrType::U32, {&ir.ISub(ir.Constant(126u), exponent.Value()), &ir.Constant(24u)})); + const IrU32 subnormal(ir.ShiftRightLogical(significand.Value(), shift.Value())); + const IrU32 truncated(ir.Select(normal.Value(), ir.ISub(ir.ShiftRightLogical(magnitude.Value(), ir.Constant(13u)), ir.Constant(0x1c000u)), subnormal.Value())); + const IrU1 tiny(ir.ULessThan(exponent.Value(), ir.Constant(103u))); + const IrU32 discarded(ir.Select(normal.Value(), ir.BitwiseAnd(magnitude.Value(), ir.Constant(0x1fffu)), + ir.Select(tiny.Value(), magnitude.Value(), ir.ISub(significand.Value(), ir.ShiftLeftLogical(subnormal.Value(), shift.Value()))))); + const IrU1 away(rounding == 1u ? ir.IEqual(sign.Value(), ir.Constant(0u)) : rounding == 2u ? ir.INotEqual(sign.Value(), ir.Constant(0u)) : ir.ConstantBool(false)); + const IrU1 increment(ir.LogicalAnd(away.Value(), ir.INotEqual(discarded.Value(), ir.Constant(0u)))); + const IrU32 rounded(ir.IAdd(truncated.Value(), ir.Select(increment.Value(), ir.Constant(1u), ir.Constant(0u)))); + const IrU32 overflow(fp16Overflow() ? ir.Constant(0x7bffu) : ir.Select(away.Value(), ir.Constant(0x7c00u), ir.Constant(0x7bffu))); + IrU32 result(ir.Select(ir.UGreaterThan(exponent.Value(), ir.Constant(142u)), overflow.Value(), rounded.Value())); + if (fp16Overflow()) { + result = IrU32(ir.Select(ir.IEqual(result.Value(), ir.Constant(0x7c00u)), ir.Constant(0x7bffu), result.Value())); + } + result = IrU32(ir.BitwiseOr(sign.Value(), result.Value())); + 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() && (!floatMode.has_value() || (floatMode->floatMode & 0x80u) != 0u)) { + writeF16(operand, value, sources); + return; + } + 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, 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 9b5fd45802..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) { @@ -31,6 +49,7 @@ std::uint32_t TranslationContext::f32DenormalFlushFor(const RdnaInstruction& ins case RdnaOpcode::VSubrevF32: case RdnaOpcode::VMulF32: case RdnaOpcode::VMulLegacyF32: + case RdnaOpcode::VMullitF32: case RdnaOpcode::VMinF32: case RdnaOpcode::VMaxF32: case RdnaOpcode::VMin3F32: @@ -74,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: @@ -85,8 +105,15 @@ std::uint32_t TranslationContext::f32DenormalFlushFor(const RdnaInstruction& ins case RdnaOpcode::VCvtPkU8F32: case RdnaOpcode::VCvtPkrtzF16F32: case RdnaOpcode::VCvtF16F32: + case RdnaOpcode::VDivScaleF32: + case RdnaOpcode::VDivFixupF32: + case RdnaOpcode::VMadMixloF16: + case RdnaOpcode::VMadMixhiF16: flush = 1u; break; + case RdnaOpcode::VMadLegacyF32: + case RdnaOpcode::VMacLegacyF32: + return 3u; default: return 0u; } @@ -94,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) { @@ -616,9 +644,9 @@ bool TranslationContext::emitVector(const RdnaInstruction& inst) { case RdnaOpcode::VSatPkU8I16: return vSatPkU8I16(inst); case RdnaOpcode::VMulLegacyF32: - return vMulLegacyF32(inst, false); + return vMulLegacyF32(inst); case RdnaOpcode::VMacLegacyF32: - return vMulLegacyF32(inst, true); + return vFmaLegacyF32(inst, true); case RdnaOpcode::VMullitF32: return vMullitF32(inst); case RdnaOpcode::VCmpClassF32: @@ -809,11 +837,13 @@ bool TranslationContext::emitVector(const RdnaInstruction& inst) { case RdnaOpcode::VAshrrevI16: return integer16Shift(inst, IrOpcode::ShiftRightArithmetic32, true); case RdnaOpcode::VAddNcU16: - case RdnaOpcode::VAddNcI16: return integer16Binary(inst, IrOpcode::IAdd32, false); + case RdnaOpcode::VAddNcI16: + return integer16Binary(inst, IrOpcode::IAdd32, true); case RdnaOpcode::VSubNcU16: - case RdnaOpcode::VSubNcI16: return integer16Binary(inst, IrOpcode::ISub32, false); + case RdnaOpcode::VSubNcI16: + return integer16Binary(inst, IrOpcode::ISub32, true); case RdnaOpcode::VMed3I16: return vMed3I16(inst); case RdnaOpcode::VMin3I16: @@ -845,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: @@ -883,9 +913,9 @@ bool TranslationContext::emitVector(const RdnaInstruction& inst) { case RdnaOpcode::VMulF16: return float16Binary(inst, IrOpcode::FPMul32, false); case RdnaOpcode::VMinF16: - return float16Binary(inst, IrOpcode::FPMin32, false); + return minMaxF16(inst, IrOpcode::FPMin32); case RdnaOpcode::VMaxF16: - return float16Binary(inst, IrOpcode::FPMax32, false); + return minMaxF16(inst, IrOpcode::FPMax32); case RdnaOpcode::VFmacF16: return float16Ternary(inst, IrOpcode::FPFma32, true, false); case RdnaOpcode::VFmamkF16: @@ -920,11 +950,11 @@ bool TranslationContext::emitVector(const RdnaInstruction& inst) { case RdnaOpcode::VCosF16: return float16Unary(inst, IrOpcode::FPCos); case RdnaOpcode::VMin3F16: - return float16Ternary(inst, IrOpcode::FPMinTri32, false, false); + return minMaxF16(inst, IrOpcode::FPMinTri32); case RdnaOpcode::VMax3F16: - return float16Ternary(inst, IrOpcode::FPMaxTri32, false, false); + return minMaxF16(inst, IrOpcode::FPMaxTri32); case RdnaOpcode::VMed3F16: - return float16Ternary(inst, IrOpcode::FPMedTri32, false, false); + return minMaxF16(inst, IrOpcode::FPMedTri32); case RdnaOpcode::VDivFixupF16: return vDivFixupF16(inst); case RdnaOpcode::VFrexpMantF32: @@ -936,37 +966,37 @@ bool TranslationContext::emitVector(const RdnaInstruction& inst) { case RdnaOpcode::VFmaF64: return float64Operation(inst, IrOpcode::FPFma64); case RdnaOpcode::VMinF64: - return float64Operation(inst, IrOpcode::FPMin64); + return minMaxF64(inst, IrOpcode::FPMin64); case RdnaOpcode::VMaxF64: - return float64Operation(inst, IrOpcode::FPMax64); + return minMaxF64(inst, IrOpcode::FPMax64); case RdnaOpcode::VLdexpF64: - return float64Operation(inst, IrOpcode::FPLdexp64); + return float64Unary(inst, IrOpcode::FPLdexp64); case RdnaOpcode::VTruncF64: - return float64Operation(inst, IrOpcode::FPTrunc64); + return float64Unary(inst, IrOpcode::FPTrunc64); case RdnaOpcode::VCeilF64: - return float64Operation(inst, IrOpcode::FPCeil64); + return float64Unary(inst, IrOpcode::FPCeil64); case RdnaOpcode::VRndneF64: - return float64Operation(inst, IrOpcode::FPRoundEven64); + return float64Unary(inst, IrOpcode::FPRoundEven64); case RdnaOpcode::VFloorF64: - return float64Operation(inst, IrOpcode::FPFloor64); + return float64Unary(inst, IrOpcode::FPFloor64); case RdnaOpcode::VFractF64: - return float64Operation(inst, IrOpcode::FPFract64); + return float64Unary(inst, IrOpcode::FPFract64); case RdnaOpcode::VRcpF64: - return float64Operation(inst, IrOpcode::FPRcp64); + return float64Unary(inst, IrOpcode::FPRcp64); case RdnaOpcode::VRsqF64: - return float64Operation(inst, IrOpcode::FPRsq64); + return float64Unary(inst, IrOpcode::FPRsq64); case RdnaOpcode::VSqrtF64: - return float64Operation(inst, IrOpcode::FPSqrt64); + return float64Unary(inst, IrOpcode::FPSqrt64); case RdnaOpcode::VTrigPreopF64: return float64Operation(inst, IrOpcode::FPTrigPreop64); case RdnaOpcode::VFrexpMantF64: - return float64Operation(inst, IrOpcode::FPFrexpMant64); + return float64Unary(inst, IrOpcode::FPFrexpMant64); case RdnaOpcode::VFrexpExpI32F64: return float64Operation(inst, IrOpcode::FPFrexpExp64); case RdnaOpcode::VCvtF32F64: return float64Operation(inst, IrOpcode::ConvertF32F64); case RdnaOpcode::VCvtF64F32: - return float64Operation(inst, IrOpcode::ConvertF64F32); + return vCvtF64F32(inst); case RdnaOpcode::VCvtF64I32: return float64Operation(inst, IrOpcode::ConvertF64S32); case RdnaOpcode::VCvtF64U32: @@ -1035,7 +1065,7 @@ bool TranslationContext::emitVector(const RdnaInstruction& inst) { case RdnaOpcode::VFmaF32: return floatTernary(inst, roundsProductSeparately(inst) ? IrOpcode::FPMad32 : IrOpcode::FPFma32, false, true); case RdnaOpcode::VMadLegacyF32: - return vFmaLegacyF32(inst); + return vFmaLegacyF32(inst, false); case RdnaOpcode::VMin3F32: return ieeeMode ? ieeeMinMaxF32(inst, IrOpcode::FPMinTri32) : floatTernary(inst, IrOpcode::FPMinTri32, false, false); case RdnaOpcode::VMax3F32: @@ -1113,16 +1143,10 @@ bool TranslationContext::emitVector(const RdnaInstruction& inst) { return simpleInteger(inst, IrOpcode::BitReverse32, IrType::U32, false, false, false); case RdnaOpcode::VFfblB32: return simpleInteger(inst, IrOpcode::FindILsb32, IrType::U32, false, false, false); - case RdnaOpcode::VLshlB32: - return simpleInteger(inst, IrOpcode::ShiftLeftLogical32, IrType::U32, false, true, false); case RdnaOpcode::VLshlrevB32: return simpleInteger(inst, IrOpcode::ShiftLeftLogical32, IrType::U32, true, true, false); - case RdnaOpcode::VLshrB32: - return simpleInteger(inst, IrOpcode::ShiftRightLogical32, IrType::U32, false, true, false); case RdnaOpcode::VLshrrevB32: return simpleInteger(inst, IrOpcode::ShiftRightLogical32, IrType::U32, true, true, false); - case RdnaOpcode::VAshrI32: - return simpleInteger(inst, IrOpcode::ShiftRightArithmetic32, IrType::U32, false, true, false); case RdnaOpcode::VAshrrevI32: return simpleInteger(inst, IrOpcode::ShiftRightArithmetic32, IrType::U32, true, true, false); case RdnaOpcode::VLshlrevB64: @@ -1136,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/VertexInputSpecialization.hpp b/core/shader/recompiler/VertexInputSpecialization.hpp index 2b2722eed6..12053a78a5 100644 --- a/core/shader/recompiler/VertexInputSpecialization.hpp +++ b/core/shader/recompiler/VertexInputSpecialization.hpp @@ -16,7 +16,7 @@ namespace ShaderRecompiler { inline SharedSpirv SpecializeVertexInputTypes(const CompiledShaderArtifact& artifact, std::span classes) { if (artifact.vertexInputPatches.empty()) return artifact.spirv; - auto words = artifact.spirv.Words(); + auto words = artifact.spirv.Copy(); for (const auto& patch : artifact.vertexInputPatches) { if (patch.location >= classes.size() || classes[patch.location] >= patch.values.size() || patch.word >= words.size()) throw std::runtime_error("invalid prepared vertex type patch"); words[patch.word] = patch.values[classes[patch.location]]; diff --git a/core/shader/recompiler/tests/CacheFileReadTests.cpp b/core/shader/recompiler/tests/CacheFileReadTests.cpp new file mode 100644 index 0000000000..f0e936b798 --- /dev/null +++ b/core/shader/recompiler/tests/CacheFileReadTests.cpp @@ -0,0 +1,128 @@ +#include "ShaderCacheDirectory.hpp" +#include +#include +#include + +static bool FailLargeAllocation = false; + +void* operator new(std::size_t size) { + if (FailLargeAllocation && size > 1024 * 1024) throw std::bad_alloc(); + if (auto* 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); } +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, 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}}; + Require(!ShaderRecompiler::ReadWholeFile(path, bytes) && bytes.empty()); + { std::ofstream file(path, std::ios::binary); } + Require(ShaderRecompiler::ReadWholeFile(path, bytes) && bytes.empty()); + const std::vector payload(2 * 1024 * 1024, 'S'); + { std::ofstream file(path, std::ios::binary | std::ios::trunc); file.write(payload.data(), payload.size()); Require(static_cast(file)); } + Require(ShaderRecompiler::ReadWholeFile(path, bytes) && bytes.size() == payload.size()); + Require(bytes.front() == std::byte{'S'} && bytes.back() == std::byte{'S'}); + std::vector failed{std::byte{1}}; + FailLargeAllocation = true; + const auto read = ShaderRecompiler::ReadWholeFile(path, failed); + 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 new file mode 100644 index 0000000000..2696063735 --- /dev/null +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -0,0 +1,531 @@ +#include "ControlFlow/RequestSerializer.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr char kHistoricalVersion21[] = + "NVNQQRUAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t3gABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEAAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAAAAF4VjQSAQEBOTAAAAA="; +constexpr char kVersion21WithF16Tail[] = + "NVNQQRUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAQA0DAAEB"; + +constexpr char kVersion22WithoutF16Tail[] = + "NVNQQRYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAQA0DAAE="; + +constexpr char kLayerVersion23[] = + "NVNQQRcAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t3gAB" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEDAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAkAAAANAAAAAQUKBQABAAAAAAAAAAAABQAAAAAABAAAAAAAAAABAwUHAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAACwAAABAAAABAAAAAAQYAAAAAAAAAAAAAAAAAADTvzasAAAA5" + "AAAABAAAAAkAAAAAAXhWNBIBAQE5MAAAAAA="; + +void Require(bool condition) { + if (!condition) std::abort(); +} + +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); +} + +std::vector DecodeBase64(std::string_view text) { + constexpr std::string_view alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + std::vector bytes; + std::uint32_t chunk = 0; + std::size_t bits = 0; + for (const char symbol : text) { + if (symbol == '=') break; + const auto value = alphabet.find(symbol); + Require(value != std::string_view::npos); + chunk = ((chunk << 6u) | static_cast(value)) & 0xffffffu; + bits += 6u; + if (bits >= 8u) { + bits -= 8u; + bytes.push_back(static_cast((chunk >> bits) & 0xffu)); + } + } + return bytes; +} + +std::string EncodeBase64(const std::vector& bytes) { + constexpr std::string_view alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + std::string text; + for (std::size_t index = 0; index < bytes.size(); index += 3u) { + const std::size_t count = std::min(3u, bytes.size() - index); + std::uint32_t chunk = static_cast(bytes[index]) << 16u; + if (count > 1u) chunk |= static_cast(bytes[index + 1u]) << 8u; + if (count > 2u) chunk |= bytes[index + 2u]; + for (std::size_t symbol = 0; symbol < 4u; ++symbol) { + text.push_back(symbol <= count ? alphabet[(chunk >> (18u - 6u * symbol)) & 0x3fu] : '='); + } + } + return text; +} + +} + +int main() { + const ShaderRecompiler::RequestSerializer serializer; + + const auto historical = serializer.Deserialize(kHistoricalVersion21); + Require(!historical.request.target.float16DenormPreserve); + + const auto version21WithTail = serializer.Deserialize(kVersion21WithF16Tail); + Require(version21WithTail.request.target.float16DenormPreserve); + + bool rejectedTruncatedVersion22 = false; + try { + static_cast(serializer.Deserialize(kVersion22WithoutF16Tail)); + } catch (const std::runtime_error& error) { + rejectedTruncatedVersion22 = std::string_view(error.what()) == "RequestSerializer: truncated data"; + } + Require(rejectedTruncatedVersion22); + + struct CanonicalMain22 { + bool pixel; + bool tessellation; + std::string_view encoded; + }; + constexpr std::array canonicalMain22{{ + {true, false, + "NVNQQRYAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t3gAB" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEDAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAACQAAAA0AAAABBQoFAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAAAEDBQcAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAABBgAAAAAAAAAAAAAAAAAANO/NqwAAADkA" + "AAAEAAAACQAAAAABeFY0EgEBATkwAAAAAA==" + }, + {true, true, + "NVNQQRYAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t3gAB" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEDAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAACQAAAA0AAAABBQoFAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAAAEDBQcAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAABBgAAAAAAAAAAAAAAAAEEAAAABAAAAAIA" + "AAACAAAAAwAAAD8AAAAANO/NqwAAADkAAAAEAAAACQAAAAABeFY0EgEBATkwAAAAAA==" + }, + {false, false, + "NVNQQRYAAAABAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t3gAA" + "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQAAAA0AAAABBQoFAAEAAAAAAAAA" + "AAAFAAAAAAAEAAAAAAAAAAEDBQcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAIAAAALAAAAEAAAAEAAAAABBgAAAAAAAAAAAAAAAAAANO/NqwAAADkAAAAEAAAACQAAAAABeFY0EgEBATkwAAAAAA==" + }, + {false, true, + "NVNQQRYAAAABAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t3gAA" + "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQAAAA0AAAABBQoFAAEAAAAAAAAA" + "AAAFAAAAAAAEAAAAAAAAAAEDBQcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAIAAAALAAAAEAAAAEAAAAABBgAAAAAAAAAAAAAAAAEEAAAABAAAAAIAAAACAAAAAwAAAD8AAAAANO/NqwAAADkAAAAEAAAACQAA" + "AAABeFY0EgEBATkwAAAAAA==" + }, + }}; + for (const auto& fixture : canonicalMain22) { + for (const bool preserve : {false, true}) { + auto bytes = DecodeBase64(fixture.encoded); + bytes.back() = static_cast(preserve); + const auto replay = serializer.Deserialize(EncodeBase64(bytes)); + const auto& request = replay.request; + Require(request.context.pixel.has_value() == fixture.pixel && request.context.vertex.has_value()); + Require(request.context.vertex->fetchAttribReg == 9u && request.context.vertex->fetchBufferReg == 13u && request.context.vertex->fetchEmbedded && request.context.vertex->paClVsOutCntl == 0x00050a05u); + Require(!fixture.pixel || (request.context.pixel->rasterPrimitive == ShaderRecompiler::RasterPrimitive::Rectangle && request.context.pixel->alphaToCoverage && request.context.pixel->stencilRefExportEnable && request.context.pixel->targetOutputMode[0] == 9u && request.context.pixel->targetExportMapping[0] == 0xe4u && !request.context.pixel->layerExported && !request.context.pixel->ditheredAlphaToCoverage && 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] = 42u; + 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); + upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u); + Require(serializer.Serialize(request) == EncodeBase64(upgraded)); + auto truncated = bytes; + truncated.pop_back(); + bool rejected = false; + try { + static_cast(serializer.Deserialize(EncodeBase64(truncated))); + } catch (const std::runtime_error& error) { + rejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; + } + Require(rejected); + if (!fixture.pixel) { + auto vertex23 = bytes; + vertex23[4] = 23u; + const auto vertexReplay = serializer.Deserialize(EncodeBase64(vertex23)); + Require(serializer.Serialize(vertexReplay.request) == EncodeBase64(upgraded)); + continue; + } + for (const bool layer : {false, true}) { + auto layered = bytes; + layered[4] = 23u; + layered.insert(layered.begin() + 258u, static_cast(layer)); + const auto layerReplay = serializer.Deserialize(EncodeBase64(layered)); + Require(layerReplay.request.context.pixel->layerExported == layer && !layerReplay.request.context.pixel->ditheredAlphaToCoverage && 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; + truncatedLayer.pop_back(); + bool layerRejected = false; + try { + static_cast(serializer.Deserialize(EncodeBase64(truncatedLayer))); + } catch (const std::runtime_error& error) { + layerRejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; + } + Require(layerRejected); + layered[4] = 42u; + InsertMeshBarycentricFlag(layered); + layered.insert(layered.begin() + 259u, {0u, 0u}); + InsertPixelDefaultsAfterFrontFace(layered); + if (fixture.tessellation) layered.insert(layered.end() - 35u, 6u, 0u); + layered.insert(layered.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u); + Require(serializer.Serialize(layerReplay.request) == EncodeBase64(layered)); + } + } + } + + const auto layer23 = DecodeBase64(kLayerVersion23); + Require(layer23.size() == 1451u); + for (const bool layer : {false, true}) { + for (const bool preserve : {false, true}) { + auto bytes = layer23; + bytes[258] = static_cast(layer); + bytes.back() = static_cast(preserve); + const auto encoded23 = EncodeBase64(bytes); + const auto replay = serializer.Deserialize(encoded23); + Require(replay.request.context.pixel.has_value() && replay.request.context.vertex.has_value()); + Require(replay.request.context.pixel->layerExported == layer); + Require(!replay.request.context.pixel->ditheredAlphaToCoverage && 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] = 42u; + InsertMeshBarycentricFlag(bytes); + bytes.insert(bytes.begin() + 259u, {0u, 0u}); + InsertPixelDefaultsAfterFrontFace(bytes); + bytes.insert(bytes.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u); + Require(serializer.Serialize(replay.request) == EncodeBase64(bytes)); + + auto truncated23 = DecodeBase64(encoded23); + truncated23.pop_back(); + bool rejected = false; + try { + static_cast(serializer.Deserialize(EncodeBase64(truncated23))); + } catch (const std::runtime_error& error) { + rejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; + } + Require(rejected); + } + } + + struct Main24Fixture { + bool pixel; + bool tessellation; + std::string_view encoded; + }; + static constexpr std::array canonicalMain24{{ + {true, false, + "NVNQQRgAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t3gAB" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEDAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAkAAAANAAAAAQUKBQABAAAAAAAAAAAABQAAAAAABAAAAAAAAAABAwUHAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAACwAAABAAAABAAAAAAQYAAAAAAAAAAAAAAAAAADTvzasAAAA5" + "AAAABAAAAAkAAAAAAXhWNBIBAQE5MAAAAAE=" + }, + {true, true, + "NVNQQRgAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t3gAB" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEDAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAkAAAANAAAAAQUKBQABAAAAAAAAAAAABQAAAAAABAAAAAAAAAABAwUHAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAACwAAABAAAABAAAAAAQYAAAAAAAAAAAAAAAABBAAAAAQAAAAC" + "AAAAAgAAAAMAAAA/AAAAJQAAAAEANO/NqwAAADkAAAAEAAAACQAAAAABeFY0EgEBATkwAAAAAQ==" + }, + {false, false, + "NVNQQRgAAAABAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t3gAA" + "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQAAAA0AAAABBQoFAAEAAAAAAAAA" + "AAAFAAAAAAAEAAAAAAAAAAEDBQcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAIAAAALAAAAEAAAAEAAAAABBgAAAAAAAAAAAAAAAAAANO/NqwAAADkAAAAEAAAACQAAAAABeFY0EgEBATkwAAAAAQ==" + }, + {false, true, + "NVNQQRgAAAABAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t3gAA" + "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQAAAA0AAAABBQoFAAEAAAAAAAAA" + "AAAFAAAAAAAEAAAAAAAAAAEDBQcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAIAAAALAAAAEAAAAEAAAAABBgAAAAAAAAAAAAAAAAEEAAAABAAAAAIAAAACAAAAAwAAAD8AAAAlAAAAAQA0782rAAAAOQAAAAQA" + "AAAJAAAAAAF4VjQSAQEBOTAAAAAB" + }, + }}; + for (const auto& fixture : canonicalMain24) { + for (const bool preserve : {false, true}) { + for (const bool layer : {false, true}) { + if (!fixture.pixel && layer) continue; + auto bytes = DecodeBase64(fixture.encoded); + bytes.back() = static_cast(preserve); + if (fixture.pixel) bytes[258] = static_cast(layer); + const auto replay = serializer.Deserialize(EncodeBase64(bytes)); + const auto& request = replay.request; + Require(request.context.pixel.has_value() == fixture.pixel && request.context.vertex.has_value()); + Require(!fixture.pixel || (request.context.pixel->layerExported == layer && !request.context.pixel->ditheredAlphaToCoverage && 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 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] = 42u; + 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); + upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u); + 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)); + 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] = 42u; + InsertMeshBarycentricFlag(main25); + if (fixture.pixel) { + main25.insert(main25.begin() + 260u, 0u); + InsertPixelDefaultsAfterFrontFace(main25); + } + if (fixture.tessellation) main25.insert(main25.end() - 35u, 1u, 0u); + main25.insert(main25.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u); + Require(serializer.Serialize(replay25.request) == EncodeBase64(main25)); + } + } + } + } + + 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 && replay.request.target.float32FlushToZero == !preserve && replay.request.target.float32DenormPreserve == preserve); + auto bytes = DecodeBase64(encoded); + Require(bytes.size() >= 8u && bytes[4] == 42u && 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] = 43u; + bool futureRejected = false; + try { + static_cast(serializer.Deserialize(EncodeBase64(future))); + } catch (const std::runtime_error& error) { + futureRejected = std::string_view(error.what()) == "unsupported recompile request serialization version"; + } + Require(futureRejected); + bytes.pop_back(); + bool rejected = false; + try { + static_cast(serializer.Deserialize(EncodeBase64(bytes))); + } catch (const std::runtime_error& error) { + rejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; + } + Require(rejected); + } + + 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); + } + + 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 new file mode 100644 index 0000000000..b7c45cab14 --- /dev/null +++ b/demo/web/app.css @@ -0,0 +1,10 @@ +:root{color-scheme:light;--bg:#f9fafb;--surface:#fff;--sunken:#f2f3f5;--line:#e1e3e6;--text:#151619;--muted:#67696c;--accent:#245fd4;--on-accent:#fff;--warning:#875410;--warning-bg:#fff8e8;--error:#a52222;--error-bg:#fff0f0;--good:#218740} +@media(prefers-color-scheme:dark){:root:not([data-theme=light]){color-scheme:dark;--bg:#0d0e10;--surface:#161719;--sunken:#131416;--line:#2a2b2e;--text:#eff0f2;--muted:#a0a2a6;--accent:#7baaff;--on-accent:#0b0f18;--warning:#e4bb74;--warning-bg:#262117;--error:#ffa6a6;--error-bg:#2d191b;--good:#4fb768}} +:root[data-theme=dark]{color-scheme:dark;--bg:#0d0e10;--surface:#161719;--sunken:#131416;--line:#2a2b2e;--text:#eff0f2;--muted:#a0a2a6;--accent:#7baaff;--on-accent:#0b0f18;--warning:#e4bb74;--warning-bg:#262117;--error:#ffa6a6;--error-bg:#2d191b;--good:#4fb768} +*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif}button,input,select{font:inherit}button,a,select,input{touch-action:manipulation}button{cursor:pointer}button:disabled{cursor:not-allowed;opacity:.45}a{color:var(--accent);text-underline-offset:3px}button:focus-visible,a:focus-visible,select:focus-visible,input:focus-visible{outline:2px solid var(--accent);outline-offset:4px}code,pre,.eyebrow,.fine{font-family:ui-monospace,"Cascadia Mono",Consolas,monospace}code{font-size:.9em}h1,h2,h3,p{margin-top:0}[hidden]{display:none!important}header{border-bottom:1px solid var(--line);background:var(--surface)}.bar{max-width:1120px;margin:auto;min-height:68px;padding:12px 24px;display:flex;align-items:center;justify-content:space-between;gap:24px}.brand{display:flex;align-items:center;gap:10px;text-decoration:none;color:var(--text);font-weight:700;font-size:17px;white-space:nowrap}.brand>span:last-child{font-weight:400;color:var(--muted);font-size:14px}.mark{display:flex;gap:3px;align-items:center;margin-right:3px}.mark i{display:block;width:4px;height:20px;background:var(--text)}.mark i:last-child{opacity:.3}nav{display:flex;align-items:center;gap:24px;font-size:13px}nav a{color:var(--muted);text-decoration:none}nav a:hover{color:var(--text)}main{max-width:1024px;margin:auto;padding:58px 24px 36px}.intro{max-width:660px;margin-bottom:30px}.eyebrow{color:var(--muted);letter-spacing:.12em;font-size:11px;margin-bottom:12px}h1{font-size:clamp(29px,4vw,43px);line-height:1.15;letter-spacing:-.035em;font-weight:650;margin-bottom:18px}.converter{border:1px solid var(--line);border-radius:10px;background:var(--surface);overflow:hidden}.panel-heading{display:flex;justify-content:space-between;align-items:center;gap:16px;border-bottom:1px solid var(--line);padding:19px 26px}.panel-heading h2{font-size:15px;margin:0;font-weight:600}.private{font-size:12px;color:var(--muted)}.private b{color:var(--good);font-size:9px;margin-right:5px}.drop{margin:24px;border:1px dashed var(--line);border-radius:6px;text-align:center;padding:28px 16px 17px;background:var(--bg)}.drop.dragging{border-color:var(--accent);background:var(--sunken)}.archive-icon{display:inline-flex;align-items:center;justify-content:center;width:40px;height:45px;border:1px solid var(--line);border-radius:4px;font-size:25px;color:var(--muted);margin-bottom:15px}.drop h3{font-size:19px;margin-bottom:8px;font-weight:600}.drop>p{color:var(--muted);font-size:13px;margin-bottom:20px}.button{display:inline-flex;align-items:center;justify-content:center;gap:18px;border:1px solid transparent;border-radius:6px;background:var(--accent);color:var(--on-accent);text-decoration:none;font-weight:600;font-size:13px;padding:11px 20px;white-space:nowrap}.button:hover:not(:disabled){filter:brightness(.94)}.drop .fine{margin:15px 0 0;font-size:10px}.fine{font-size:11px;line-height:1.6;color:var(--muted);margin-bottom:0}.plain{border:0;background:none;color:var(--muted);padding:8px;font-size:13px}.plain:hover{color:var(--text)}#theme{font-size:19px;padding:3px}.settings{display:grid;grid-template-columns:1fr 1fr;gap:36px;padding:0 26px 20px}fieldset{border:0;margin:0;padding:0;min-width:0}legend,.save-settings>label{display:block;font-size:12px;font-weight:600;margin-bottom:10px}.choices{display:flex;gap:10px}.choices label{flex:1;display:flex;gap:9px;align-items:center;border:1px solid var(--line);border-radius:6px;padding:10px;font-size:13px;cursor:pointer}.choices label:has(input:checked){border-color:var(--accent)}.choices span{font-size:11px;color:var(--muted);margin-left:auto}input{accent-color:var(--accent)}input[type=radio],input[type=checkbox]{margin:0;width:15px;height:15px;flex-shrink:0}select{width:100%;padding:10px;border:1px solid var(--line);border-radius:6px;background:var(--surface);color:var(--text);font-size:13px}.save-settings .fine{margin-top:8px}.options{padding:0 26px 23px;font-size:12px;color:var(--muted)}.options label{display:flex;gap:9px;align-items:center}.action-row{border-top:1px solid var(--line);padding:17px 26px;display:flex;gap:12px;align-items:center}.action-row>p{margin:0 auto 0 0;font-size:12px}.muted{color:var(--muted)}.selection{display:flex;justify-content:space-between;align-items:center;padding:25px 26px;gap:16px}.selection h3{font-size:18px;margin-bottom:6px;overflow-wrap:anywhere}.selection p{margin-bottom:0;font-size:12px}.selection .eyebrow{font-size:10px;margin-bottom:6px}.warnings,.notice{padding:16px 20px;background:var(--warning-bg);color:var(--warning);font-size:13px;border:1px solid var(--line);border-radius:6px}.warnings{margin:0 26px 24px}.warnings ul{padding-left:18px;margin:0 0 12px}.warnings label{display:flex;gap:8px;align-items:center}.notice{margin-bottom:20px}.status{border-top:1px solid var(--line);padding:22px 26px}.status-line{display:flex;justify-content:space-between;font-size:13px;gap:16px}progress{appearance:none;width:100%;height:5px;margin:14px 0 8px;border:0;border-radius:3px;overflow:hidden;background:var(--line)}progress::-webkit-progress-bar{background:var(--line)}progress::-webkit-progress-value{background:var(--accent)}progress::-moz-progress-bar{background:var(--accent)}#current-file{overflow-wrap:anywhere;min-height:18px}details{margin-top:18px;font-size:11px;color:var(--muted)}summary{cursor:pointer}pre{max-height:180px;overflow:auto;white-space:pre-wrap;word-break:break-word;font-size:11px}.error{margin:0 26px 24px;padding:15px;background:var(--error-bg);color:var(--error);border-radius:6px;white-space:pre-wrap;overflow-wrap:anywhere}.done{padding:24px 26px;border-top:1px solid var(--line)}.done h3{font-size:20px;color:var(--good);margin-bottom:8px}.done p{font-size:13px;color:var(--muted)}.done .fine{margin:15px 0 0}.compatibility{margin:12px 0 0;color:var(--muted);font-size:11px;line-height:1.8}.limits{border-top:1px solid var(--line);padding-top:22px;margin:30px 0 0;color:var(--muted);font-size:11px;line-height:1.8}footer{max-width:1024px;margin:auto;border-top:1px solid var(--line);padding:22px 24px;display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}footer div{display:flex;gap:20px}footer a{color:var(--muted);text-decoration:none}@media(max-width:680px){.bar{flex-wrap:wrap;gap:12px;padding:14px 20px}nav{gap:17px;font-size:12px}main{padding:34px 16px 26px}.panel-heading{padding:16px}.private{font-size:10px}.drop{margin:16px}.settings{grid-template-columns:1fr;padding:0 16px 18px;gap:22px}.options{padding:0 16px 20px}.action-row{padding:16px;flex-wrap:wrap}.action-row>p{width:100%}.selection{padding:20px 16px}.warnings{margin:0 16px 20px}.status,.done{padding:20px 16px}.error{margin:0 16px 20px}footer{flex-wrap:wrap;gap:12px}footer div{flex-wrap:wrap;gap:14px}} + +.input-actions{display:flex;justify-content:center;flex-wrap:wrap;gap:12px} +.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 new file mode 100644 index 0000000000..4e0b534145 --- /dev/null +++ b/demo/web/app.mjs @@ -0,0 +1,336 @@ +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 selectedInputs = []; +let summaries = []; +let inspectionComplete = false; +let working = false; +let failing = false; +let sink; +let outputDirectory; +let outputTransaction; +let downloadUrl; +let logs = []; +let release; +const canSaveZip = typeof window.showSaveFilePicker === "function" && window.isSecureContext; +const canStream = typeof window.showDirectoryPicker === "function" && window.isSecureContext; +const supported = window.isSecureContext && window.Worker && window.WebAssembly && window.ReadableStream && window.WritableStream && window.crypto?.subtle; +$("save").querySelector('[value="disk"]').disabled = !canStream; +$("save").querySelector('[value="zip"]').disabled = !canSaveZip; +$("save").value = canSaveZip ? "zip" : "memory"; + +function bytes(value) { + if (value < 1024) return `${value} B`; + const unit = value >= 1024 ** 3 ? 3 : value >= 1024 ** 2 ? 2 : 1; + return `${(value / 1024 ** unit).toFixed(1)} ${["B", "KiB", "MiB", "GiB"][unit]}`; +} + +function 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 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" || 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 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; + 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) { + for (const id of ["choose", "choose-folder", "change", "target", "save", "intel"]) $(id).disabled = value || !supported || (id === "choose-folder" && !("webkitdirectory" in $("folder"))); +} + +async function fail(message) { + if (failing) return; + failing = true; + worker?.terminate(); + worker = null; + working = true; + selectedInputs = []; + summaries = []; + inspectionComplete = false; + const transaction = outputTransaction; + outputTransaction = null; + outputDirectory = null; + if (transaction) { + $("status").hidden = false; + $("phase").textContent = "Removing incomplete output folder"; + } + 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(); +} + +async function onMessage({ data }) { + try { + if (data.type === "progress") { + if (data.message) { + logs.push(`${data.appName ? `${data.appName}: ` : ""}${data.message.trim()}`); + logs = logs.slice(-150); + $("log").textContent = logs.join("\n"); + return; + } + 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; + $("percent").textContent = `${percent}%`; + } else { + $("progress").removeAttribute("value"); + $("percent").textContent = ""; + } + } else if (data.type === "inspected") { + summaries[data.index] = data.summary; + } else if (data.type === "inspection-complete") { + inspectionComplete = true; + working = false; + $("selection").hidden = false; + $("drop").hidden = true; + $("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); + update(); + } else if (data.type === "directory" || data.type === "open-file") { + if (!outputDirectory) throw new Error("No output folder is open"); + const parts = data.name.replace(/\/$/u, "").split("/"); + if (parts.some(part => !part || part === "." || part === ".." || part.includes("\\"))) throw new Error("Invalid output path"); + const filename = data.type === "open-file" ? parts.pop() : null; + let parent = outputDirectory; + for (const part of parts) parent = await parent.getDirectoryHandle(part, { create: true }); + if (filename) { + try { + const handle = await parent.getFileHandle(filename, { create: true }); + sink = await handle.createWritable(); + } catch (error) { + throw new Error(`Cannot write ${data.name}: ${error.message}. Choose ZIP output if the browser blocks this file type.`); + } + } + worker?.postMessage({ type: "written" }); + } else if (data.type === "close-file") { + if (!sink) throw new Error("No output file is open"); + await sink.close(); + sink = null; + worker?.postMessage({ type: "written" }); + } else if (data.type === "write") { + if (!sink) throw new Error("No output file is open"); + await sink.write(data.bytes); + worker?.postMessage({ type: "written" }); + } else if (data.type === "complete") { + if (sink) { await sink.close(); sink = null; } + working = false; + $("done").hidden = false; + $("status").hidden = true; + $("cancel").hidden = true; + $("result-info").textContent = `${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; + $("download").download = `${data.outputName}.zip`; + $("download").hidden = false; + } else if (data.mode === "zip") { + $("result-info").textContent += " · ZIP saved to disk"; + } else { + $("result-info").textContent += ` · Saved in ${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; + outputTransaction?.commit(); + outputTransaction = null; + selectedInputs = []; + summaries = []; + inspectionComplete = false; + worker.terminate(); + worker = null; + lock(false); + update(); + } else if (data.type === "error") await fail(data.message); + } catch (error) { await fail(error.message); } +} + +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; } + selectedInputs = inputs; + summaries = []; + inspectionComplete = false; + working = true; + logs = []; + $("log").textContent = ""; + for (const id of ["error", "done", "download", "warnings", "selection"]) $(id).hidden = true; + $("acknowledge").checked = false; + $("status").hidden = false; + $("cancel").hidden = false; + $("phase").textContent = "Inspecting files"; + $("current-file").textContent = inputs.length > 1 ? `Inspecting ${inputs.length} ZIP archives` : inputs[0].name; + $("percent").textContent = ""; + $("progress").removeAttribute("value"); + lock(true); + update(); + worker = new Worker(new URL("./worker.mjs", import.meta.url), { type: "module" }); + worker.onmessage = onMessage; + worker.onerror = event => fail(event.message || "The converter worker stopped. Try a smaller app or use the native relinker."); + worker.postMessage({ type: "inspect", inputs }); +} + +$("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) selectInputs([{ name: files[0].path.split("/")[0], files }]); +}; +$("file").onchange = () => { + const inputs = [...$("file").files].map(file => ({ name: file.name, file })); + $("file").value = ""; + selectInputs(inputs); +}; +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; + 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 () => { + if ($("convert").disabled) return; + working = true; + lock(true); + update(); + try { + const target = document.querySelector('input[name="target"]:checked').value; + if ($("save").value === "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 = 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, toIntel: $("intel").checked, mode: $("save").value }); + } catch (error) { + if (error.name === "AbortError") { working = false; lock(false); update(); } + else await fail(error.message); + } +}; +$("theme").onclick = () => { + const dark = document.documentElement.dataset.theme ? document.documentElement.dataset.theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches; + document.documentElement.dataset.theme = dark ? "light" : "dark"; +}; +update(); +lock(false); +if (supported) fetch("./release.json", { cache: "no-cache" }).then(response => { if (!response.ok) throw new Error(`Cannot load release information (${response.status}).${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(); +}).catch(error => fail(error.message)); + +if (!("webkitdirectory" in $("folder"))) { + $("choose-folder").disabled = true; + $("choose-folder").title = "Folder selection is unavailable in this browser. Choose a ZIP instead."; +} diff --git a/demo/web/archive.mjs b/demo/web/archive.mjs new file mode 100644 index 0000000000..7021114f0e --- /dev/null +++ b/demo/web/archive.mjs @@ -0,0 +1,391 @@ +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 { batchMemoryEstimate, batchOutputDirectory, memoryEstimate, MEMORY_LIMIT }; +export const CONVERSION_LIMIT = 256 * 1024 * 1024; +export const ICON_LIMIT = 16 * 1024 * 1024; +const MAX_ENTRIES = 100000; + +class BoundedBlobReader extends BlobReader { + async readUint8Array(offset, length) { + if (length > 16 * 1024 * 1024) throw new Error("ZIP directory metadata exceeds 16 MiB"); + return super.readUint8Array(offset, length); + } +} + + +export function validPath(name) { + if (!name || name.includes("\\") || /[\x00-\x1f<>:"|?*]/u.test(name)) throw new Error(`Unsafe ZIP path: ${name}`); + const parts = name.replace(/\/$/u, "").split("/"); + if (parts.some(part => !part || part === "." || part === ".." || /[. ]$/u.test(part) || /^(con|prn|aux|nul|com[1-9\u00b9\u00b2\u00b3]|lpt[1-9\u00b9\u00b2\u00b3])(?:\.|$)/iu.test(part))) throw new Error(`Unsafe ZIP path: ${name}`); + return name; +} + +function safeSize(value) { + if (!Number.isSafeInteger(value) || value < 0) throw new Error("ZIP contains an unsupported file size"); + return value; +} + +async function headerBytes(entry, size = 4) { + if (entry.uncompressedSize < size) return null; + const stop = new AbortController(); + const header = new Uint8Array(size); + let filled = 0; + try { + await entry.getData(new WritableStream({ + write(chunk) { + const length = Math.min(size - filled, chunk.length); + header.set(chunk.subarray(0, length), filled); + filled += length; + if (filled === size) stop.abort(); + } + }), { signal: stop.signal, checkSignature: false }); + } catch (error) { + if (!stop.signal.aborted || filled !== size) throw error; + } + 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) { + if (entry.uncompressedSize > limit) throw new Error(`${entry.filename} exceeds the conversion memory limit`); + let size = 0; + const chunks = []; + await entry.getData(new WritableStream({ + write(chunk) { + size += chunk.byteLength; + if (size > limit || size > entry.uncompressedSize) throw new Error(`Unexpected expanded size: ${entry.filename}`); + chunks.push(chunk); + } + }), { checkSignature: true }); + if (size !== entry.uncompressedSize) throw new Error(`Incorrect expanded size: ${entry.filename}`); + const bytes = new Uint8Array(size); + let offset = 0; + for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.length; } + return bytes; +} + +class FolderReader { + constructor(files) { + this.files = files; + } + + async *getEntriesGenerator() { + for (const { path, file } of this.files) { + if (!(file instanceof Blob)) throw new Error(`Invalid folder file: ${path}`); + yield { + filename: path, uncompressedSize: file.size, directory: false, compressionMethod: 0, + externalFileAttributes: (0o100644 << 16) >>> 0, + getData(destination, options = {}) { + return file.stream().pipeTo(destination, { signal: options.signal }); + } + }; + } + } + + async close() {} +} + +export class Archive { + constructor(file) { + this.reader = Array.isArray(file) ? new FolderReader(file) : new ZipReader(new BoundedBlobReader(file)); + this.entries = []; + } + + async inspect(report) { + const paths = new Map(); + let total = 0; + for await (const entry of this.reader.getEntriesGenerator()) { + if (this.entries.length >= MAX_ENTRIES) throw new Error("Input contains more than 100,000 entries"); + 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}`); + if (entry.encrypted) throw new Error(`Encrypted ZIP entries are not supported: ${entry.filename}`); + if (!entry.directory && entry.compressionMethod !== 0 && entry.compressionMethod !== 8) throw new Error(`Unsupported ZIP compression: ${entry.filename}`); + const mode = (entry.externalFileAttributes >>> 16) & 0xf000; + if (mode && mode !== 0x8000 && mode !== 0x4000) throw new Error(`Links and special files are not supported: ${entry.filename}`); + paths.set(key, entry.directory); + total = safeSize(total + safeSize(entry.uncompressedSize)); + this.entries.push(entry); + } + if (!this.entries.some(entry => !entry.directory)) throw new Error("No files were selected"); + const directories = new Set(); + for (const entry of this.entries) { + const parts = entry.filename.split("/"); + for (let index = 1; index < parts.length; index++) { + 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.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 header = await headerBytes(item.entry); + if (item.name.startsWith("sce_module/")) { + 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}`); + } + 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); + 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, 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); + remaining -= bytes.length; + return bytes; + }; + const name = this.executable.name; + const stem = name.includes(".") ? name.slice(0, name.lastIndexOf(".")) : name; + const outputName = (stem || "app") + (target === "windows" ? ".exe" : ".elf"); + const input = { name, outputName, target, toIntel, executable: await read(this.executable), modules: [], icon: null }; + for (const item of this.modules) input.modules.push({ name: item.name, bytes: await read(item) }); + if (this.icon && target === "windows") input.icon = await read(this.icon); + return input; + } + + async close() { await this.reader.close(); } +} + +export function 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) { + const stream = new TransformStream(); + let produced = 0; + const limit = new TransformStream({ transform(chunk, controller) { + produced += chunk.length; + if (produced > entry.uncompressedSize) throw new Error(`Unexpected expanded size: ${name}`); + controller.enqueue(chunk); + }, flush() { + if (produced !== entry.uncompressedSize) throw new Error(`Incorrect expanded size: ${name}`); + } }); + await Promise.all([ + entry.getData(stream.writable, { checkSignature: true }), + writer.add(name, stream.readable.pipeThrough(limit), { + versionMadeBy: 0x31e, externalFileAttributes: entry.externalFileAttributes >>> 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 }) + }) + ]); +} + +export class DirectoryWriter { + constructor(destination) { + this.destination = destination; + } + + async add(name, source, options = {}) { + if (options.directory) { + await this.destination.directory(name); + return; + } + const output = await this.destination.file(name); + if (source instanceof ReadableStream) { + let completed = 0; + const reader = source.getReader(); + const writer = output.getWriter(); + try { + for (;;) { + const { value, done } = await reader.read(); + if (done) break; + for (let offset = 0; offset < value.length; offset += 65536) { + const chunk = value.subarray(offset, offset + 65536); + await writer.write(chunk); + completed += chunk.length; + options.onprogress?.(completed); + } + } + await writer.close(); + } catch (error) { + await Promise.allSettled([reader.cancel(error), writer.abort(error)]); + throw error; + } finally { + reader.releaseLock(); + writer.releaseLock(); + } + } else { + await source.init(); + const writer = output.getWriter(); + try { + for (let offset = 0; offset < source.size; offset += 65536) { + await writer.write(await source.readUint8Array(offset, Math.min(65536, source.size - offset))); + } + await writer.close(); + } catch (error) { + await Promise.allSettled([writer.abort(error)]); + throw error; + } finally { + writer.releaseLock(); + } + } + } + + async close() {} +} + +export async function packageArchive(archive, converted, outputName, libraryFile, destination, report, directory = false, options = {}) { + const libraries = new ZipReader(new BoundedBlobReader(libraryFile)); + 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 => { + const path = outputPath(name); + const key = path.replace(/\/$/u, "").toLowerCase(); + if (names.has(key)) throw new Error(`Output path collision: ${name}`); + names.add(key); + }; + try { + const libraryEntries = await libraries.getEntries(); + if (!libraryEntries.length || libraryEntries.length > 4096) throw new Error("Invalid library archive"); + for (const entry of libraryEntries) { + if (!entry.filename.startsWith("libs/") || entry.encrypted || ((entry.externalFileAttributes >>> 16) & 0xf000) === 0xa000) throw new Error("Invalid library archive layout"); + claim(entry.filename); + } + const replaced = new Set(converted.modules.map(module => module.name)); + const originalFiles = archive.files.filter(item => !replaced.has("app0/" + item.name)); + for (const item of originalFiles) claim("app0/" + item.name); + claim(outputName); + if (converted.diagnostics) claim("windows-diagnostics-imports.txt"); + for (const module of converted.modules) { + if (!module.name.startsWith("app0/sce_module/")) throw new Error("Invalid converted module path"); + claim(module.name); + } + const 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(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(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(outputPath("app0/" + name), undefined, { directory: true }); + else await appendEntry(writer, outputPath("app0/" + name), entry, report); + } + 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 new file mode 100644 index 0000000000..e0d468febc --- /dev/null +++ b/demo/web/index.html @@ -0,0 +1,29 @@ + + + + + +AnyPS5 · Converter + + +
+
+

LOCAL CONVERSION

PS5 apps to Windows/Linux converter

+ +
+

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.

+ + + +
+

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 new file mode 100644 index 0000000000..d0c2ca7c77 --- /dev/null +++ b/demo/web/tests/archive.test.mjs @@ -0,0 +1,277 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import { pathToFileURL } from "node:url"; +import path from "node:path"; +import { register } from "node:module"; + +register("./cdn-loader.mjs", import.meta.url, { data: { url: process.env.ANYPS5_TEST_ZIP_URL, path: process.env.ANYPS5_TEST_ZIP_PATH } }); + +const site = name => pathToFileURL(path.join(process.argv[2], name)).href; +const { Archive, memoryEstimate, MEMORY_LIMIT, 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"))); +const module = new Uint8Array(await readFile(path.join(process.argv[3], "module.elf"))); +const resource = new Uint8Array(2 * 1024 * 1024).fill(65); + +async function zip(entries) { + const output = new BlobWriter("application/zip"); + const writer = new ZipWriter(output, { zip64: true, level: 9, useWebWorkers: false, useCompressionStream: false }); + for (const [name, bytes, options] of entries) await writer.add(name, bytes ? new Uint8ArrayReader(bytes) : undefined, options); + return writer.close(); +} + +const source = await zip([["title/", null, { directory: true }], ["title/program.dat", executable], ["title/sce_module/library.prx.esbak", module], ["title/sce_module/library.prx", new Uint8Array([79, 21, 61, 29])], ["title/sce_module/library.prx.esbak.guest.prx", new Uint8Array([77, 90, 0, 0])], ["title/sce_sys/param.json", new TextEncoder().encode("{}")], ["title/shaders/example.frag", executable], ["title/content/resource.bin", resource]]); +const folder = [ + { path: "title/program.dat", file: new Blob([executable]) }, + { path: "title/sce_module/library.prx.esbak", file: new Blob([module]) }, + { path: "title/sce_module/library.prx", file: new Blob([new Uint8Array([79, 21, 61, 29])]) }, + { path: "title/sce_module/library.prx.esbak.guest.prx", file: new Blob([new Uint8Array([77, 90, 0, 0])]) }, + { path: "title/sce_sys/param.json", file: new Blob(["{}"]) }, + { path: "title/shaders/example.frag", file: new Blob([executable]) }, + { path: "title/content/resource.bin", file: new Blob([resource]) } +]; +for (const [kind, selected] of [["ZIP", source], ["folder", folder]]) { +for (const target of ["windows", "linux"]) { + const archive = new Archive(selected); + const summary = await archive.inspect(() => {}); + assert.equal(summary.name, "program.dat"); + assert.equal(summary.root, "title/"); + assert.equal(summary.modules, 1); + assert.equal(summary.selectedBytes, executable.length + module.length); + const input = await archive.conversionInput(target, false, () => {}); + assert.equal(input.modules.length, 1); + assert.deepEqual(input.executable, executable); + assert.equal(input.modules[0].name, "sce_module/library.prx.esbak"); + const events = []; + const converted = native.Convert(input, event => events.push(event)); + assert.equal(converted.ok, true, converted.error); + assert.equal(converted.executable[0], target === "windows" ? 77 : 127); + if (target === "windows") { + const image = new DataView(converted.executable.buffer, converted.executable.byteOffset, converted.executable.byteLength); + assert.equal(image.getUint16(image.getUint32(60, true) + 24 + 68, true), 3); + } + assert(events.some(event => event.stage === 6 && event.completed > 0)); + const libraries = await zip([["libs/system.prx", new Uint8Array([1, 2, 3])]]); + const chunks = []; + let maxChunk = 0; + const destination = new WritableStream({ async write(chunk) { + maxChunk = Math.max(maxChunk, chunk.length); + chunks.push(chunk.slice()); + await new Promise(resolve => setTimeout(resolve, 1)); + } }); + await packageArchive(archive, converted, input.outputName, libraries, destination, () => {}); + await destination.close(); + const reader = new ZipReader(new BlobReader(new Blob(chunks))); + const entries = await reader.getEntries(); + const byName = new Map(entries.map(entry => [entry.filename, entry])); + assert(byName.has(input.outputName)); + assert(byName.has("libs/system.prx")); + assert.deepEqual(await byName.get("app0/shaders/example.frag").getData(new Uint8ArrayWriter(), { checkSignature: true }), executable); + assert.deepEqual(await byName.get("app0/sce_module/library.prx.esbak.guest.prx").getData(new Uint8ArrayWriter(), { checkSignature: true }), converted.modules[0].bytes); + assert.deepEqual(await byName.get("app0/sce_module/library.prx").getData(new Uint8ArrayWriter(), { checkSignature: true }), new Uint8Array([79, 21, 61, 29])); + assert.deepEqual(await byName.get("app0/program.dat").getData(new Uint8ArrayWriter(), { checkSignature: true }), executable); + assert.deepEqual(await byName.get("app0/content/resource.bin").getData(new Uint8ArrayWriter(), { checkSignature: true }), resource); + assert.equal((byName.get(input.outputName).externalFileAttributes >>> 16) & 0o777, 0o755); + assert(maxChunk < resource.length); + const saved = new Map(); + let active = false; + let directoryChunk = 0; + await packageArchive(archive, converted, input.outputName, libraries, { + async directory(name) { saved.set(name, null); }, + async file(name) { + assert.equal(active, false); + active = true; + const pieces = []; + return new WritableStream({ + async write(chunk) { + directoryChunk = Math.max(directoryChunk, chunk.length); + pieces.push(chunk.slice()); + await new Promise(resolve => setTimeout(resolve, 1)); + }, + async close() { + saved.set(name, new Uint8Array(await new Blob(pieces).arrayBuffer())); + active = false; + } + }); + } + }, () => {}, true); + assert.equal(active, false); + assert(directoryChunk < resource.length); + assert.equal(saved.size, entries.length); + for (const entry of entries) { + assert(saved.has(entry.filename)); + if (!entry.directory) assert.deepEqual(saved.get(entry.filename), await entry.getData(new Uint8ArrayWriter(), { checkSignature: true })); + } + await reader.close(); + await archive.close(); + console.log(`${kind}, ${target}: conversion, original files, modules, permissions, streamed ZIP and directory output verified`); +} +} + +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], + [[["app", executable], ["link", resource, { versionMadeBy: 0x31e, externalFileAttributes: (0o120777 << 16) >>> 0 }]], /Links and special/u] +]) { + const archive = new Archive(await zip(entries)); + await assert.rejects(archive.inspect(() => {}), error); + await archive.close(); +} +const missing = new Archive(await zip([["any-name", executable]])); +assert.equal((await missing.inspect(() => {})).warnings.length, 2); +await missing.close(); +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) }; +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/cdn-loader.mjs b/demo/web/tests/cdn-loader.mjs new file mode 100644 index 0000000000..13bc480b2f --- /dev/null +++ b/demo/web/tests/cdn-loader.mjs @@ -0,0 +1,9 @@ +let library; + +export function initialize(data) { + library = data; +} + +export function resolve(specifier, context, nextResolve) { + return nextResolve(specifier === library.url ? library.path : specifier, context); +} diff --git a/demo/web/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 new file mode 100644 index 0000000000..51d63e03e0 --- /dev/null +++ b/demo/web/worker.mjs @@ -0,0 +1,201 @@ +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; +let lastPhase = ""; + +function report(event) { + const phase = event.phase ?? String(event.stage); + const now = performance.now(); + if (!event.message && phase === lastPhase && event.completed !== event.total && now - lastReport < 80) return; + lastReport = now; + lastPhase = phase; + self.postMessage({ type: "progress", ...event }); +} + +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 || !Number.isSafeInteger(asset.size) || asset.size < 0 || asset.size > 512 * 1024 * 1024 || !/^libraries\/(windows|linux)-[a-f0-9]+\.zip$/u.test(asset.path) || !/^[a-f0-9]{64}$/u.test(asset.sha256)) throw new Error("Invalid release manifest"); + const latestResponse = await fetch("https://api.github.com/repos/boykopovar/AnyPS5/releases/latest", { headers: { Accept: "application/vnd.github+json" } }); + if (!latestResponse.ok) { + const rateLimited = latestResponse.status === 429 || (latestResponse.status === 403 && latestResponse.headers.get("x-ratelimit-remaining") === "0"); + 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 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; + const resolve = pendingWrite; + pendingWrite = null; + resolve(); + return; + } + if (busy) return; + busy = true; + try { + if (data.type === "inspect") { + if (archive) await archive.close(); + 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 (!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); + 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(); + const chunks = []; + let size = 0; + let outputName = "anyps5-apps"; + const send = (event, transfer = []) => new Promise(resolve => { + pendingWrite = resolve; + self.postMessage(event, transfer); + }); + if (data.mode === "disk") { + const destination = { + directory: name => send({ type: "directory", name }), + async file(name) { + await send({ type: "open-file", name }); + return new WritableStream({ + async write(chunk) { + size += chunk.byteLength; + const bytes = chunk.slice(); + await send({ type: "write", bytes }, [bytes.buffer]); + }, + close: () => send({ type: "close-file" }) + }); + } + }; + outputName = await convertInput(inputs[0], 0, native, libraries, data, destination, true); + } else { + const destination = new WritableStream({ + async write(chunk) { + size += chunk.byteLength; + if (data.mode === "zip") { + const bytes = chunk.slice(); + await send({ type: "write", bytes }, [bytes.buffer]); + return; + } + if (size > MEMORY_LIMIT) throw new Error("The compressed ZIP exceeded 256 MiB. Use Save ZIP to disk."); + chunks.push(chunk.slice()); + } + }); + 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(); + } + 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) }); + } finally { + busy = false; + } +}; diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 1a617546a1..574244a6da 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -43,7 +43,52 @@ 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. + +## 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 + +Runtime-loaded Windows modules export `__aps5_guest_initialize`: the entry RVA, the initializer count and the RVAs of initializer pointer slots. [DynamicLoader.cpp](../../core/libs/prx/libkernel/Module/src/DynamicLoader.cpp) consumes the first two words in the mapped image after initialization, so another reference to that image does not run it again. Unloading the image discards this state; a later load reads the original table from the file, including when Windows reuses the same base address. Native references outside the guest handle table retain the same initialized image. + +While initialization runs, the count is `UINT32_MAX` and the entry word holds the initializing Windows thread ID. A different thread waits on the loader's condition variable; the owning thread can recursively open its module. Completion clears both words, restores the table's original page protection and wakes waiters. An exception leaves the count at `UINT32_MAX` with thread ID 0, so another load rejects that image until it is unloaded. The table's file format is unchanged and its initializer slots remain intact. + +[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. + +[`Cancel.hpp`](../../core/libs/prx/libkernel/Pthread/include/Cancel.hpp) connects condition, semaphore and join waits to the request. A canceled condition waiter reacquires its guest mutex before cleanup. Semaphore and join waits release their internal locks before exiting. A request pending on entry to a semaphore wait is acted on before consuming an available token; a canceled join leaves its target joinable and its result output untouched. + +Cancellation exits through `scePthreadExit` with result pointer `1`. It runs cleanup handlers in reverse push order before thread-specific destructors; cancellation points inside cleanup must not start another exit. The host thread then terminates through the existing Windows or Linux lifecycle, and join waits for that termination. Host thread termination is not used to inject cancellation into a running guest frame. + +The reference contract and constants come from FreeBSD libthr, pinned at [`5ed7eb0d`](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_cancel.c), with cleanup and exit ordering in [`thr_exit.c`](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_exit.c#L196-L259). These are reference semantics, not console measurements. The supported cancellation points and incomplete asynchronous behavior are listed in [TechnicalDebt](TechnicalDebt.md). + +[`GuestPthreadCancelRecovery.cpp`](../../core/libs/tests/GuestPthreadCancelRecovery.cpp) checks pending requests before ordinary and timed semaphore acquisition, cleanup reentrancy and destructor ordering, enabled and disabled timed condition waits, canceled join output, and subsequent resource and thread reuse. Host atomic gates establish pending requests; acquiring the condition mutex after the worker releases it establishes wait entry without a scheduling sleep. Both `guest_pthread_cancel_recovery` and `guest_pthread_cancel_recovery_coarse` retain checks in Release and have a 20-second timeout, shorter than the 60-second guest waits. The latter sets `APS5_COARSE_TIMED_WAITS=1` to exercise the Windows fallback; Linux uses that condition-variable path by default. + +## APR and AMM command buffers + +[`Apr.cpp`](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) executes APR and AMM command buffers. A buffer runs on the submitting thread until a `WaitOnAddress` or `WaitOnCounter` that is not yet satisfied; the rest of it, and every buffer submitted after it, is copied and runs on a worker thread, so a submitter that satisfies the wait after the submit returns does not hang. Buffers without such a wait still complete before the submit returns. + +APR and AMM each have their own queue and worker thread. Order is kept inside a queue; a wait in one does not hold up the other, because AMM `WriteAddress` commands advance the counters that APR buffers wait on. Submission ids are per queue, and `sceKernelAprWaitCommandBuffer` and `sceAmprAmmWaitCommandBufferCompletion` block until the queue has completed that id. A command that throws on a worker aborts the process. + +[`GuestAmpr.cpp`](../../core/libs/tests/GuestAmpr.cpp) checks both cases: a submit whose wait is satisfied afterwards returns and keeps buffer order, and an AMM buffer submitted after a waiting APR buffer still completes. ## Graphics @@ -66,4 +111,90 @@ 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 over part of a plain host-pointer import (one registered range, no alias, dma-buf, chunked or joined import, no pending settle) trims the import instead of retiring it (`retireOrTrim` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp)): its buffer is destroyed and it waits in `HostImports::trimmed` with the side of its range the change left, the larger side for a change strictly inside the range, while its `VkDeviceMemory` keeps the whole range it was made for pinned (`HostImport::memoryBase` and `pinnedBytes`, which `liveBytes` and the eviction count), also after the guest protects, unmaps or maps again the pages that were cut off. The next `refreshImports` binds a new `VkBuffer` to the same memory at the offset of the largest registered range inside the kept side (`reattachTrimmed`) when that range still has the import's backing serial, overlaps no other import, is what a new import would cover and the buffer accepts the offset; otherwise, or when an import of a range overlapping the kept side is made first, the trimmed import is released and held through the recorder (`KeepThroughHold`) as a retired import is. `GuestAllocations::Range::backingSerial` tells whether a range's pages are still the ones that were imported: a new range gets a fresh serial, the untouched pieces of a split keep theirs, and every span a mutation passes to `RequireUnpinned` (a protect or unmap, a fixed mapping over registered memory, the other mappings of remapped direct memory, and for an unmap or remap the whole 2 MiB resident runs it touches, which it may demote to shared pages) gets a fresh one 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, 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. + +An immediate MODE write confined to the rounding fields is accepted only when its selected replacement bits equal the initial state. Source bits above the selected width are ignored. `s_round_mode` similarly compares its low four immediate bits with the initial rounding fields. A state-changing write or an SGPR-sourced MODE write throws with its instruction and program counter. Runtime transitions require control-flow-aware mode tracking and matching arithmetic lowering. Writes to other hardware registers retain their existing behavior and remain technical debt. + +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 e23d12180b..4cebf79a20 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 @@ -10,9 +10,11 @@ cmake --build build-relinker --parallel ctest --test-dir build-relinker --output-on-failure ``` -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). +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. -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. +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 WinLibs requirement below applies to the full build. The executable is `build-relinker/core/relinker/relinker` (`relinker.exe` on Windows with Ninja). + +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. @@ -25,11 +27,21 @@ git submodule update --init --recursive ## Requirements - x86-64, Git, CMake 3.22.1 or newer, Ninja, C++20. -- Linux: GCC, G++, binutils. SDL's X11 backend requires X11 and Xext development headers (`libx11-dev` and `libxext-dev` on Debian/Ubuntu). +- 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: only MinGW-w64 GCC 15.2.0 (WinLibs `x86_64-ucrt-posix-seh`, release [`15.2.0posix-14.0.0-ucrt-r7`](https://github.com/brechtsanders/winlibs_mingw/releases/tag/15.2.0posix-14.0.0-ucrt-r7), archive `winlibs-x86_64-posix-seh-gcc-15.2.0-mingw-w64ucrt-14.0.0-r7.zip`) is currently supported. Add its `mingw64/bin` directory to `PATH` before configuring. - Windows antivirus software can quarantine the executables that the relinker tests create in the temp directory, which fails `optional_plt`, `empty_tls`, `windows_address_space` and `windows_icon`. Set `TEMP` and `TMP` to a directory the scanner excludes before running `ctest`. -- 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 @@ -40,12 +52,43 @@ cmake --build build --target libs --parallel ``` `libs` is a custom target: every library under [core/libs/prx](../../core/libs/prx) is built with -`EXCLUDE_FROM_ALL`, so the first command alone does not produce them. Titles load the patched `.prx` -files from `build/core/libs/libs`, which only that second step refreshes. Running a title after a -library change without it therefore tests the previous binaries and can show no effect at all. +`EXCLUDE_FROM_ALL`. With the default `BUILD_TESTING=OFF`, the default build does not produce them, +so the explicit `--target libs` step is required. With `BUILD_TESTING=ON`, the default full build +also builds `libs` through the `patched_libraries_tests` dependency. Titles load the patched `.prx` +files from `build/core/libs/libs`; running a title after a library change without rebuilding `libs` +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. + +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`: @@ -54,8 +97,12 @@ 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. | | `-DAPS5_ENABLE_TIMING_LOG=ON` | `OFF` | Compile frame timing logging. | +| `-DAPS5_ENABLE_TRACY=ON` | `OFF` | Build the [Tracy](#tracy) profiler client into the agc driver. | | `-DAPS5_AGC_CREATE_LOG=OFF` | `ON` | Disable successful `sceAgcCreateShader` logging. | | `-DAGC_BUILD_VISUAL_TEST=ON` | `OFF` | Build the standalone AGC SPIR-V visual test. | @@ -72,10 +119,169 @@ 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`. + +## 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 + +The macOS write watch can be built and tested separately, without submodules or the prx libraries: + +```sh +cmake -S core/libs/tests/write_watch -B build/write-watch -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=x86_64 +cmake --build build/write-watch +ctest --test-dir build/write-watch --output-on-failure +``` + +With `CMAKE_OSX_ARCHITECTURES=x86_64` Apple silicon runs the tests through Rosetta, which is how relinked titles run; without it they run natively. The full `BUILD_TESTING` build on macOS also registers them. + +## Tracy + +`-DAPS5_ENABLE_TRACY=ON` builds the [Tracy](https://github.com/wolfpld/tracy) 0.14.1 client from `3rdparty/tracy` as a shared library next to the patched `.prx` files (`libTracyClient.dll`, `libTracyClient.so.0.14.1` on Linux); copy it with them. Every `PerformanceTimer` scope of libSceAgcDriver and libSceVideoOut is a zone, every presented flip ends a frame, and the queue workers and the presenter are named threads. Connect the Tracy 0.14.1 profiler to `127.0.0.1`: the client listens only on localhost and records only while a profiler is connected. Sampling, system tracing, call stacks, code transfer and Tracy's crash handler are disabled. Without the flag nothing of Tracy is built. + +On Windows MinGW frees `thread_local` objects before their destructors run, so the Tracy client is built without `thread_local` destructors and never destroys its event queue: a thread that emitted a zone and ended keeps its event blocks (2 MiB each) until the process exits. + +## Compute pipeline replay + +Build `agc_compute_pipeline_replay` in the full build with `-DANYPS5_ENABLE_SPIRV_TOOLS=ON`; replay validates the captured module before calling Vulkan. Set `APS5_DUMP_COMPUTE_PIPELINES` to a dedicated empty directory before starting a title to save each uncached compute pipeline's final SPIR-V, descriptor layouts, push constant ranges, specialization values, subgroup size and creation flags before compilation. Files are named `compute-/capture.aps5cp`; identical records are deduplicated. Capture stops with an error at 128 records, 256 MiB total or 36 MiB per record (at most 8,388,608 SPIR-V words, or 32 MiB of shader code). Capture files contain shader code and device metadata, but no guest buffers or textures; review them before sharing. + +Set `APS5_DUMP_COMPUTE_MIN_WORDS` to capture only modules at or above that SPIR-V word count, for example `100000` when investigating large NVIDIA compilations. The default is `0` (all modules); values outside `0` through `8388608` fail. The filter applies only when capture is enabled. + +```sh +cmake --build build --target agc_compute_pipeline_replay +build/core/libs/prx/libSceAgcDriver/agc_compute_pipeline_replay capture.aps5cp --cache-out pipeline.cache --output fresh.json +build/core/libs/prx/libSceAgcDriver/agc_compute_pipeline_replay capture.aps5cp --cache-in pipeline.cache --output imported.json +``` + +On Windows use `agc_compute_pipeline_replay.exe` and prepend `build/core/libs/libs/unpatched` to the process `PATH`. `ANYPS5_GPU` selects a device by name as for the driver. The tool uses the driver's compute pipeline creation path, without recording commands, dispatching work or importing guest memory. Compile-only devices also enable supported partially-bound, variable-count and runtime-array descriptor features to replay captured layouts; ordinary runtime devices are unchanged. Binding flags are preserved independently of update-after-bind. Replay accepts the unconditional Vulkan 1.0 capabilities listed in the pinned [Vulkan registry](https://github.com/KhronosGroup/Vulkan-Headers/blob/2fa203425eb4af9dfc6b03f97ef72b0b5bcb8350/registry/vk.xml); other capabilities must be enabled by the device target. Non-Vulkan capabilities such as Kernel and unknown capabilities fail before pipeline creation. Unsupported features, non-highest variable bindings and counts exceeding layout support fail explicitly. Unsupported inputs and mismatched cache vendor, device or UUID fail explicitly. Output files must not already exist. + +Each invocation creates one pipeline. With no `--cache-in`, its application pipeline cache starts empty; `--cache-in` imports a raw cache previously written by `--cache-out` (at most 64 MiB), and `--cache-out` exports the resulting cache. The title's wrapped `vk-pipelines-*.bin` format is not accepted. It does not load or save the title's usual pipeline cache. NVIDIA and other driver caches remain uncontrolled: a fresh application cache is not a cold driver cache. JSON reports the capture identity, GPU/driver identifiers, word count, cache policy and separate wall times for shader module, layouts, pipeline creation and cache work. Device initialization is outside these timings. Pipeline statistics captures and `APS5_PIPELINE_STATS` are refused because they change compilation cost. + +`--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_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. + +Set `APS5_TRACE_EVENTS=` to change the queue-worker and presenter buffers for a frame or seconds window. The count is a whole number from 1 through 4,194,304; the default is 262,144. An invalid value fails when one of these threads first binds to the trace, before recording. Each buffer uses `count * sizeof(TraceThread::Slice)` bytes (currently 32 bytes per event): 8 MiB at the default, 96 MiB at `3145728`, and 128 MiB at the maximum, per thread. The buffers are allocated even while waiting for the window to open, and formatting the trace uses additional memory. Shader preparation keeps its explicitly sized 32,768-event buffers, as does a title thread named `title` that binds one the first time it registers a shader or resolves a shader ABI. The GPU range limit stays 262,144. Summary-only sessions allocate no event buffers and do not read this setting. A larger buffer can still fill; use a shorter window or increase the count within the bound. + +Inside the window the queue workers and the presenter record every `PerformanceTimer` scope and stage as a complete event named after its frame timing key: `Driver.WorkerWait`, `Driver.Packet`, `Driver.Packet.draw`, `Graphics.Draw`, `Driver.Present`, `Driver.Present.inflight_wait`, `Vulkan.Retire` and so on. A slice shorter than 20 us is only counted: its time is in the thread's counter ` folded` (milliseconds per present) and in the summary. A thread writes a present's counter sample at its first slice after the next present, so the last present of the window has no sample; its time is still in the summary. By default, each queue worker and presenter allocates a buffer for 262,144 events when it starts; a full buffer stops that thread's recording, sets `trace_dropped` and puts an instant event `trace buffer full` on the thread's track at the start of the first lost slice (slices are appended when their outer scope ends, so that can precede the last recorded slice by up to one outer-scope length), with the number of lost slices in `args.events_lost`. When the window closes, the presenter formats the trace once and a `ProfileOutput` writer thread writes `aps5-trace--.json.gz` (Chrome trace-event JSON, opened by ui.perfetto.dev or chrome://tracing) and appends one line to `aps5-summary.jsonl`, both in `APS5_TRACE_DIR` or the working directory. A window that is still open when the title exits writes nothing. + +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). + +The `gpu` track holds one `batch` slice per recorder batch, from the batch's timestamp pair. With `APS5_PROFILE_GPU=1` it also holds a slice per command class range, under the class names of the `[gputime]` line (`draw`, `copy`, `fill`, `present-blit`, ...); program ranges are left out. The track keeps up to 262,144 ranges; more set `trace_dropped`. The GPU clock is mapped onto the CPU clock with calibrated timestamps (`VK_KHR_calibrated_timestamps` or the EXT version, enabled only when a trace switch is set) taken when the window opens and when it closes, so the two clocks are matched at both ends of the window. A batch is added when the driver reads its timestamps, after its fence signals, so the batches of the last frames of the window, which complete after it closes, are missing. Without calibrated timestamps the trace has no `gpu` track, and the `[trace]` line says so. + +With 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. + ## Shader recompiler The shader recompilation logic in [core/shader/recompiler](../../core/shader/recompiler) is isolated from the rest of the project and is a pure function of its input data, designed for integration into any other project. The current CMake target also includes cache support and links a supplied runtime target, glslang, and optionally SPIRV-Tools. 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..7d6f4d537b --- /dev/null +++ b/docs/dev/ENVIRONMENT.md @@ -0,0 +1,569 @@ +# 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_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..73d6271fc2 --- /dev/null +++ b/docs/dev/GAME_TESTING.md @@ -0,0 +1,111 @@ +# 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. +- 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. + +## 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. + +## 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 storage-image lines 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 9e6475c5c0..e48f86ed1a 100644 --- a/docs/dev/HW_ORACLE.md +++ b/docs/dev/HW_ORACLE.md @@ -42,7 +42,7 @@ use hyphens; Python keywords use underscores. Record these settings alongside th | `dx10_clamp` | 0 or 1: DX10 clamp disabled or enabled | | `denorm32`, `denorm16` | 0: flush input/output; 1: preserve input, flush output; 2: flush input, preserve output; 3: preserve both | | `round32`, `round16` | 0: nearest even; 1: toward +infinity; 2: toward -infinity; 3: toward zero | -| `fp16_overflow` | 0: overflow to infinity; 1: clamp computed overflow to the largest finite value (infinite inputs and division by zero still produce infinity) | +| `fp16_overflow` | 0: overflow to infinity; 1: clamp computed overflow to the largest finite value (an infinite source operand still produces infinity, as does `v_div_fixup_f16` for a zero denominator or an infinite numerator, but not for an infinite quotient; `v_rcp_f16`, `v_rsq_f16` and `v_log_f16` of zero are clamped) | `denorm16` and `round16` control both f16 and f64. These values follow the [LLVM AMDGPU kernel descriptor documentation](https://llvm.org/docs/AMDGPUUsage.html#amdhsa-kernel-descriptor). diff --git a/docs/dev/PROGRESS.md b/docs/dev/PROGRESS.md index 36d9a1e088..a1257f85a9 100644 --- a/docs/dev/PROGRESS.md +++ b/docs/dev/PROGRESS.md @@ -1,26 +1,41 @@ # 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 -The scanner reads `APS5_VABI` function definitions under `core/libs/prx//`, excluding `tests` subdirectories and their descendants. Test callbacks do not contribute to library counts. A definition is pending when its body calls `NotImplemented_nid_no_patch`, directly or through a recognized local stub wrapper. Other definitions are counted as implemented. These counts describe declared functions, not every firmware export, verified ABI compatibility or playable games. Silent behavior and unverified assumptions remain in [Technical debt](TechnicalDebt.md). +The scanner reads `APS5_VABI` function definitions under `core/libs/prx//`, excluding `tests` subdirectories and their descendants. Test callbacks do not contribute to library counts. A definition is pending when its body calls `NotImplemented_nid_no_patch`, directly or through a recognized local stub wrapper. Call matching uses complete identifiers and accepts whitespace or comments before `(`. Other definitions are counted as implemented. Braces inside string literals, character literals and comments do not delimit bodies. These counts describe declared functions, not every firmware export, verified ABI compatibility or playable games. Silent behavior and unverified assumptions remain in [Technical debt](TechnicalDebt.md). -A library with no scanned definitions can include a sibling library's source builder. The scanner recognizes literal `include(${CMAKE_CURRENT_SOURCE_DIR}/..//.cmake)` references, including quoted paths. It does not evaluate arbitrary CMake expressions. +A library with no scanned definitions can include a sibling library's source builder. The scanner recognizes literal `include(${CMAKE_CURRENT_SOURCE_DIR}/..//.cmake)` references, including quoted paths. It does not evaluate arbitrary CMake expressions. It also recognizes a top-level `.cpp` that includes another library's source with `#include "prx//.cpp"`. For these wrappers, the JSON group adds `shared_sources` with the source library's directory name. Its own counts and name lists remain empty. The HTML table links to the source library instead of showing `0/0`; the source library's map tooltip lists its wrappers. Global totals count the definitions at the source library once. +A library with no scanned definitions and no shared sources, such as one whose `Export.cpp` only has `APS5_DUMMY_FUN` because its functions are implemented in another library, is listed as having no exports instead of `0/0`. It is left out of the map. + +## History + +[`tools/progress_history.py`](../../tools/progress_history.py) measures the first-parent history of `main` with the same rules as `progress.py` and writes `history.json`, `history.svg` 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 Comparisons identify functions by library and name. When a head group becomes a shared-source wrapper, its previous definitions are compared under the source library's identity. Removing a duplicate implementation is reported as sharing sources, without reporting the retained functions as removed. Real removals and transitions back to stubs remain visible. Percentages retain the original snapshot totals, so eliminating duplicate accounting can change a percentage without changing available functionality. Older JSON snapshots without `shared_sources` are accepted. +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 ``` The regressions check that adding test callbacks leaves counts and comparisons unchanged, including when a callback shares a pending function's name or a wrapper uses shared sources. They also create a source library and a copied wrapper, move the wrapper to a shared builder, check output labels and totals, then verify real regressions and removals are still reported. With `BUILD_TESTING=ON` and Python available, CTest registers them as `progress_report`. diff --git a/docs/dev/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 29cfa2b3a5..ee2a2dfd75 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -1,5 +1,7 @@ # 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. 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 - Building on Windows requires a specific version of mingw - MinGW-w64 GCC 15.2.0 (`winlibs-gcc15`, `x86_64-ucrt-posix-seh`) @@ -11,387 +13,19 @@ ### Silent stubs Throughout the project, every function at every stage either **does exactly what it's supposed to or throws an exception**. Everywhere... except: -- [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. -- [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 -- [libSceCommonDialog](../../core/libs/prx/libSceCommonDialog/Export.cpp) -- [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 -- [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 -- [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. -- [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 -- [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 -- [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. -- [libSceNpCommerce](../../core/libs/prx/libSceNpCommerce/Export.cpp) - the PS Store icon show/hide calls and `sceNpCommerceSetPsStoreIconLayout` do nothing -- [libSceUsbd](../../core/libs/prx/libSceUsbd/Export.cpp) implements only the no-device path: `sceUsbdInit` and `sceUsbdExit` succeed without a host USB context, the device list is always empty and event handling waits for its timeout; calls that need a device throw -- [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 -- [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 -- [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) -- Without `VK_KHR_fragment_shader_barycentric` the shader recompiler [does not load the barycentric VGPRs](../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp): [`v_interp_p1_f32` is dropped and `v_interp_p2_f32` reads the attribute Vulkan interpolated](../../core/shader/recompiler/Translation/src/AttributeInstructions.cpp), so pixel shaders that use the barycentrics for anything other than `v_interp` get wrong values. -- [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 -- [libSceTextToSpeech2](../../core/libs/prx/libSceTextToSpeech2/Export.cpp) has no speech engine: `sceTextToSpeech2Initialize` fails and the other exports throw. PPSA01317 skips its narration set-up when initialization fails -- [libSceAvPlayer](../../core/libs/prx/libSceAvPlayer/Export.cpp): `sceAvPlayerSetLogCallback` accepts a callback that is never called, as the player produces no log messages, and `sceAvPlayerSetAvailableBandwidth` has no effect, as it governs HLS sources, which `sceAvPlayerAddSource` does not implement. -- [libSceHttp2](../../core/libs/prx/libSceHttp2/Export.cpp) - no request reaches the network, so `sceHttp2SetMinSslVersion` has no TLS connection to restrict and returns success for any `id`, like `sceHttp2SslEnableOption` (prosper returns the invalid-id error for an unknown one) -- [libSceNpTrophy2](../../core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp): `sceNpTrophy2RegisterUnlockCallback` accepts a callback that is never called, as no trophy is ever unlocked, and `sceNpTrophy2UnregisterUnlockCallback` only returns success. -- [sceAvPlayerSetTrickSpeed](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) with a negative speed runs the clock backwards but delivers no frames; when a forward speed is set again, playback resumes from the rewound time. -- [ulobjmgr](../../core/libs/prx/ulobjmgr/Export.cpp) registers no object: `_sceUlobjmgrRegisterObject` always hands out id 0 and `_sceUlobjmgrUnregisterObject` releases nothing, as shadPS4 does -- [libSceHttp](../../core/libs/prx/libSceHttp/Export.cpp) - no request reaches the network, so `sceHttpSetResponseHeaderMaxSize` has no response header to limit 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 -- [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 -- [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 -- [Packed pixel ancillary word](../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) (shader recompiler) - the sample index at bits 8-11 and the render target index at bits 16-28 follow #295, and their equality with the SampleId and Layer builtins is inferred, not measured. A value passed through a select or move is rebuilt from those two fields with bits 0-7, 12-15 and 29-31 zero. That is exact only because every read of the word must be an extract inside one modelled field; any other read throws. The other bits are unmodelled and their RDNA 2 layout is unknown: the RDNA 3.5 ISA is reported to give prim type at bits 0-1, VRS rates at bits 2-5 and eye id at bit 7, which was not checked against RDNA 2 -- [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}EntitlementInfoList](../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) - signatures unknown; no reference implements them, so they are declared without parameters. 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 -- [sceRudpGetStatus](../../core/libs/prx/libSceRudp/Export.cpp) - PPSA01521 passes an output pointer and byte count `0xF8`; the status layout and size rules are unknown, so the buffer is zeroed like `sceRudpGetContextStatus` (state 0 = idle, no connections) and the size is not validated. It returns `RUDP_ERROR_NOT_INITIALIZED` before initialization and 0 otherwise, as `sceRudpGetContextStatus` does. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp) declares it without arguments and returns success without writing status -- [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 - -- [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 -- [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 -- [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 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. -- PPSA01341 imports declared without parameters, signatures unknown: [sceAgcSetSemaphoreMemory](../../core/libs/prx/libSceAgc/Unimplemented.cpp), [sceAgcDriverRegisterMultipleResources](../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp). Names from the shadPS4 aerolib NID list -- [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 -- [sceNpEAAccessInitialize, sceNpEAAccessTerminate](../../core/libs/prx/libSceNpPartner001/Export.cpp) (libSceNpPartner001) - names and the no-argument signatures follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_partner.cpp); imported by PPSA23566, not confirmed on a PS5 title -- [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 -- [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 -- [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 -- [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 -- [scePadOpenExt](../../core/libs/prx/libScePad/Export.cpp) (libScePad) - only special ports (type 2) are handled, and they return `DEVICE_NOT_CONNECTED` (0x80920007) because the host has no special controller; whether the console opens a handle and reports the device as disconnected later is unknown. 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 -- PPSA23566 imports declared without parameters, signatures unknown: [sceAudioOut2UserGetSupportedAttributes](../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp), [sceConvertKeycodeGetCharacterFromKeyboardData](../../core/libs/prx/libSceConvertKeycode/Export.cpp), [sce::Json::MemAllocator::notifyError](../../core/libs/prx/libSceJson2/Export.cpp), [sceNetResolverAbort](../../core/libs/prx/libSceNet/Export.cpp), [sceNpCommerceDialogOpen2](../../core/libs/prx/libSceNpCommerce/Export.cpp), [sceNpEntitlementAccessGetPftFlag](../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp), [sceProprietaryVoiceChatHelper{Initialize,Terminate,SetVoiceChatState,GetVoiceChatUsageState}](../../core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp), [sceVoice{GetResourceInfo,ResetPort,EnableChat,DisableChat}](../../core/libs/prx/libSceVoice/Export.cpp). Names from the shadPS4 aerolib NID list -- PPSA01325 imports declared without parameters, signature unknown: [sceRudpActivate](../../core/libs/prx/libSceRudp/Export.cpp). Name from the shadPS4 aerolib NID list -- [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 -- [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 -- [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 -- [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 -- [sceAgcDcbClearState](../../core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp) (libSceAgc) - the (buffer, command) signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Agc/SceAgc.cs) and [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c), and the 2-dword CLEAR_STATE (0x12) packet is OpenAGC's; OpenAGC masks the command to 4 bits, here a command above 0xf throws, as the driver rejects that payload -- [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 -- [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 -- [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 -- [sceVideoOutRegisterBuffers2](../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - DCC buffers (category 1): `dcc_control` is taken as the CB_DCC_CONTROL block layout, only bits 0x10026c are accepted; `dcc_cb_register_clear_color` as the register-clear texel, only 32-bit values are presented -- [sceVideoOutIsOutputSupported](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - only the default mode (1) is reported as supported. The console answers per display and per title: with a 120 Hz TV it also answers 1 for mode 15 (119.88 Hz), and [PS5_Vulkan](https://github.com/mihawk-99/PS5_Vulkan/blob/5b5e4fc2d80fdb67a4f61ba9e6a8a424026bb8a5/docs/HARDWARE_FINDINGS.md) measured that `sceVideoOutConfigureOutput` accepts 15 only from a title whose param.json declares high-frame-rate output (attribute3 0x80040). Modes 4, 7, 8, 12-17 and 19 return `VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE` here, as they did on the measured display. Mode 18 throws, because one console answered 0x80290016 and another 0x8029001E. The name `VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE` for 0x8029001E is ours. Word 3 of the output options is accepted with any value, as on the console, and its meaning is unknown. PPSA12544 probes `0xd000000a`, which the console answers with 0x8029001E, so the title selects 3840 pixels -- [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 -- [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 -- [sceRtcParseDateTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - the console's zone name table is only known from the names measured on a PS5 (firmware 13.60); 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 -- [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 -- [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 -- [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 -- [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 -- [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 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 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 in the guest arena on first use, not the console's; maps outside them throw. Addresses, sizes and direct offsets must be 16 KiB aligned. The memory type and the GPU mask are ignored, AMPR and ACP access maps as CPU access, unmapped pages go back to the pool and stay reserved. Submission runs the buffer before returning, the submit result is always 0 and a failing command throws; the AMM measures return errors sign-extended, as ampr_emu does. `sceAmprAmmGetVirtualAddressRanges` throws for a null output -- [AMM 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 -- [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) -- [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 -- [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 -- [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 meaning of the 64-bit writing attribute is unknown -- [sceFontWritingLineGetOrderingSpace](../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - signature taken from 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/Font/SceFont.Engine.cs) bindings, not confirmed on a title. The modelled line applies no spacing, so the head, inline, tail and advance spaces are 0; what spacing the console applies is unknown. A null output throws -- [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 -- [sceCoredumpWriteUserString](../../core/libs/prx/libkernel/System/src/Coredump.cpp) and [sceSaveDataConvert, sceSaveDataGetConvertProgress](../../core/libs/prx/libSceSaveData.native/Export.cpp) - imported by PPSA16064, not called during its start-up and added without an implementation; unknown signatures -- [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 -- [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` -- [sceUserServiceGetUserColor](../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the default profile colour is assumed to be blue (0) -- [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 -- [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 -- [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 -- [sceAgcSetSubmitMode](../../core/libs/prx/libSceAgc/Misc/src/Suspend.cpp) (libSceAgc) - mode values unknown; only 0 is accepted -- [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 -- [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 -- [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). 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. -- [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. -- [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 -- [zARR5aCmkoY](../../core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp) (libSceAgc) - unknown name, signature -- [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 -- [sceAgcBranchPatchSetThenTarget_0300, sceAgcBranchPatchSetElseTarget_0300](../../core/libs/prx/libSceAgc/Unimplemented.cpp) (libSceAgc) - names from their NIDs (`7Wa3aeJgeVU`, `rP5xLdOf26k`), signatures unknown -- [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 -- [sceAgcGetDataPacketPayloadRange](../../core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp) (libSceAgc) - signature, the `{base, size in bytes}` output and the type 0 range (header + 1, one dword longer than the type 1 payload) from KytyPS5 only, matching `sceAgcGetDataPacketPayloadAddress_0090`; prosper returns header + 2 for both types from that address function -- [sceAgcAsyncCondExecPatchSetCommandAddress, sceAgcAsyncCondExecPatchSetEnd](../../core/libs/prx/libSceAgc/Patch/src/CondExec.cpp) (libSceAgc) - no reference has a body for them; they are assumed to patch the COND_EXEC packet like `sceAgcCondExecPatchSetCommandAddress` and `sceAgcCondExecPatchSetEnd`, since `sceAgcAcbCondExec` writes the same packet as `sceAgcDcbCondExec`, as `sceAgcAsyncRewindPatchSetRewindState` does for REWIND -- [sceAgcAcbAtomicGds_0900](../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - name from its NID (`gQkqkLttcpw`), signature unknown -- [sceAgcDcbPrimeUtcl2](../../core/libs/prx/libSceAgc/DcbState/src/Display.cpp) (libSceAgc) - assumed to write the packet `sceAgcAcbPrimeUtcl2` writes: PRIME_UTCL2 has the same 5 dwords on the graphics and compute rings. Not confirmed on a title. Both write a NOP of that size, as there is no TLB to prime -- [sceAgcDcbQueueEndOfShaderActionGetSize](../../core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp) (libSceAgc) - the builder is inline in the SDK and not exported; the size is assumed to be that of the RELEASE_MEM packet `sceAgcCbReleaseMem` writes (32 bytes), the same as `sceAgcAcbQueueEndOfShaderActionGetSize`, as in [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) and [REmu](https://github.com/xnetcat/REmu/blob/main/crates/remu-runtime/src/agc_dcb.rs) -- [sceKernelInternalMemoryGetModuleSegmentInfo](../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - unknown signature -- [sceKernelGetModuleInfoForUnwind](../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - on Linux the tables come from `sceKernelGetModuleInfoFromAddr`, whose `eh_frame_size` includes the zero terminator, as measured on firmware 13.60 (reported in [#2419](https://github.com/boykopovar/AnyPS5/issues/2419), not reproduced here); on Windows it is assumed to stop before the terminator. LLVM libunwind's `CFI_Parser::findFDE` stops at the terminator or the length, whichever comes first, so it finds the same FDEs either way. Only the main executable and `.guest.prx` images report tables; other host images report none, as on Windows -- [sceKernelGetModuleInfoFromAddr](../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x1A8-byte info layout follows the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation; a title's `libc.prx` reads only `id` (PPSA14632). The meaning of `flags` is unknown; only 2 is accepted, as is only the full `st_size`. The id is the `sceKernelLoadStartModule` handle of an image opened through the loader and otherwise a stable id from the same range. The name is the host file name without `.guest.prx`. An image with more than 4 load segments (the converted executable has 7) reports its first 4. `tls_offset` is 0 and `ref_count` is 1, as the host loader exposes neither. On Windows the module is the PE image holding the address: a relinked image reports its `.elf` sections as segments and the eh_frame_hdr recorded in `.ehmeta`, any other image its first 4 sections with memory access and the `.ehfram` section without an eh_frame_hdr; the TLS fields and `init_proc_addr`/`fini_proc_addr` are 0 -- [sceKernelGetModuleList](../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - lists the main program and every loaded `.prx` image in load order; host runtime libraries are left out. The handles are the ids `sceKernelGetModuleInfoFromAddr` reports, not the console's. The `(int32_t* handles, size_t count, size_t* actual)` signature and returning `ENOMEM` without writing `actual` after filling the first `count` handles when more images are loaded follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/kernel/process.cpp#L244-L264); whether the console writes any handles in that case is unknown -- [sceKernelGetModuleInfo](../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x160-byte layout (`st_size`, `name[256]`, 4 segments, `segment_count`, `fingerprint[20]`) follows [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_types.pas#L319-L337) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/module.h#L15-L31). Only that `st_size` is accepted, as in shadPS4; other sizes return `EINVAL`, a null pointer `EFAULT` and a handle `sceKernelGetModuleList` does not list `ESRCH`. Name and segments are the ones `sceKernelGetModuleInfoFromAddr` reports, on Windows from the same PE sections, so `prot` uses the read/write/execute bits 1/2/4, while fpPS4 and shadPS4 store the ELF `p_flags`. The fingerprint is left zero, as the host image does not carry it -- [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 -- [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 -- [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 -- [scePthreadCondWait / scePthreadCondTimedwait](../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) - 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`); unverified on hardware -- [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 -- [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 -- [sceKernelMapNamedFlexibleMemoryInternal](../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - flag 0x8000 is accepted and ignored; its meaning is not established. KytyPS5 (`GUEST_MAP_UNKNOWN_8000`) and sharpemu accept it without reading it. Every known caller is the SDK 9 `libc.prx` heap (Astro Bot PPSA21564, PPSA22520), which passes it with read/write protection and uses the mapping as an older `libc.prx` uses `sceKernelMapNamedFlexibleMemory` with flags 0 -- [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 -- [Guest arena](../../core/libs/prx/libc/src/GuestArena.cpp) (libc) - the application map area is assumed to end at 0xFC_0000_0000, as `SCE_KERNEL_APP_MAP_AREA_END_ADDR` does on the PS4. On Windows, host allocations made in the arena before libc loads stay, and a fixed guest mapping over one throws -- [sceLibcInternalBacktraceForGame](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - unknown signature -- [sceLibcInternalHeapErrorReportForGame](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - unknown signature -- [_sceLibcInternalThreadAtexit, _sceLibcInternalThreadDtors](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - FreeBSD `__cxa_thread_atexit` semantics assumed; a null or non-image destructor and an allocation failure throw -- [_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 -- [sceImeGetPanelPositionAndForm](../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the output is taken to be the `PositionAndForm` that `sceImeDialogGetPanelPositionAndForm` fills; it is never written, since no panel can be open. That a closed panel is reported before a null output is assumed by analogy with `sceImeSetCaret`, not confirmed on a PS5 title -- [_sceLibcInternalForceTlsDestructor](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - behaviour follows `sceLibcForceTlsDestructor(int)` in a title's `libc.prx` (disassembly): the calling thread's destructors whose dso belongs to the module run newest first, only if the dso's module is loaded and the dso points to itself, in up to 4 passes. The return type is unknown (that function returns nothing), 0 is returned. The module of each dso is looked up when the destructors are forced, not when they are registered as `libc.prx` does, and the lookup is Linux only, so on Windows the call throws when the thread has destructors -- [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 -- [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 -- [__progname](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - unknown data export -- [seteuid, setegid](../../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` and `sys_setegid`, id 0 is then accepted without a privilege check and changes no credential, so 0 is returned; any other id would change the effective id, which is not modelled, and throws. 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 -- [sceSslClose](../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature -- [sceSslGetSerialNumber](../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature -- [X+4jdIS75P0](../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown name, signature -- [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 -- [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 -- [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 -- [scePsmlMfsrGetContextBufferRequirement1100, scePsmlMfsrCreateContext1100, scePsmlMfsrGetDispatchMfsrPacket1100](../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_debug) - argument layouts unknown; only 0x8A810001 (not initialized) is returned -- [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 -- [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 -- [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 - AudioIn and NpSessionSignaling exports added without an implementation have assumed signatures -- [sceNpSessionSignalingGetConnectionFromPeerAddress](../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - imported by PPSA01289 and added without an implementation; unknown signature -- [libSceCdlgG2P](../../core/libs/prx/libSceCdlgG2P/Export.cpp) - `sceG2PDialogInitialize`, `Terminate`, `Open`, `GetStatus`, `UpdateStatus` and `GetResult`, imported by PPSA01289, are added without an implementation and declared without parameters -- [vieBRwlh1Lw](../../core/libs/prx/libSceAgc/Unimplemented.cpp) (libSceAgc) - unknown name, signature -- [scePadIsRemoteController](../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the name hashes to NID `fCWdlnmB1Ks`; the signature `(int handle, bool* remote)` comes from PPSA16064, which zeroes a byte, passes its address after `scePadGetControllerInformation` and ignores the result. The error codes follow the other pad calls. Controllers are local devices, so the answer is always false -- [scePadSetAngularVelocityBiasCorrectionState](../../core/libs/prx/libScePad/Export.cpp) (libScePad) - imported by PPSA16064 and not called during its start-up; unknown signature -- [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; the values are unknown and 0 is reported for both, as in the [prosper](https://github.com/mattias800/prosper/pull/4153) reimplementation. Null outputs throw +- `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 -- [sceHttp2GetMemoryPoolStats, sceSslGetMemoryPoolStats, sceNetGetMemoryPoolStats](../../core/libs/prx/libSceNet/Export.cpp) (libSceHttp2, libSceSsl, libSceNet) - signatures `(int id, stats*)` and the statistics layouts (pool size, peak and current use, and for Http2 and Ssl a 32-bit reserved field) follow the PS4 SDK's `SceHttp2MemoryPoolStats`, `SceSslMemoryPoolStats` and `SceNetMemoryPoolStats`, not confirmed for the PS5. The pool size is the one given at init or pool creation; nothing is allocated from these pools, so the peak and current use are 0. Net returns `EBADF` for an unknown pool and `EINVAL` for a null output, as its other pool calls do; for Http2 and Ssl the error codes are unknown, so both cases throw -- [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 -- [libSceVrSetupDialog](../../core/libs/prx/libSceVrSetupDialog/Export.cpp), and the Share, NpTrophy2, NpEntitlementAccess and WebBrowserDialog exports imported by PPSA12544's Unity plugins and added without an implementation, have unknown signatures -- [sceAjmDecWVorbisCreateHeaderPacket, sceAjmDecWVorbisCreateSetupPacket](../../core/libs/prx/libSceAjm.native/Export.cpp) (libSceAjmWVorbis, implemented in libSceAjm.native) - unknown signatures -- Acm, GameLiveStreaming, Net, NpEntitlementAccess, NpManager, NpWebApi2, VoiceQoS, WebBrowserDialog and libkernel exports imported by PPSA14632 and added without an implementation are declared without parameters -- [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 -- [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 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 -- [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 -- [SceAvPlayerVideoEx](../../core/libs/SceTypes.hpp) (libSceAvPlayer) - frame rate field and encoding unknown; it is left zero in frame and stream 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 -- [sceAjmBatchJobGetInfo](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - writes the result and format sideband of a run job without buffers with the `SIDEBAND_FORMAT` flag (0x400000000000), as [KytyPS5](https://github.com/KytyPS5/KytyPS5) does, not confirmed on a PS5 title; the format reports bitrate 0 for ATRAC9 as in KytyPS5, while [shadPS4](https://github.com/shadps4-emu/shadPS4) derives it from the superframe size; before the instance is initialized it returns the not-initialized result (1) and no format, like the other run jobs here, while KytyPS5 returns result 0 with the format of the uninitialized decoder -- [sceAjmDecMp3ParseFrame](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - signature, result layout and rate tables taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title: the layer field is not checked and MPEG-2.5 stops at 64 kbps; the original file length (`parse_ofl`) layout (Xing/Info + LAME tag, VBRI, Fraunhofer `0xB4` block with its CRC, encoder delay = samples per frame + LAME delay + 529) also comes from shadPS4 only, and the Fraunhofer block has no other public description -- [sceAjmBatchJobGetCodecInfo, sceAjmBatchJobGetGaplessDecode](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `RUN_GET_CODEC_INFO` flag (0x800), the ATRAC9 codec info layout and its next frame size are taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [RPCSX](https://github.com/RPCSX/rpcsx) reimplementations, not confirmed on a PS5 title; a run job without buffers returns result 0, as in shadPS4. The gapless sideband reports the total and skip counts the title set and does not count them down: RPCSX does the same, shadPS4 counts them down and BryKytyPS5 returns its current counts -- [AJM Opus decoder](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 24) - the initialize parameter layout (`u32 channels, u32 sample rate, u32`, seen as `2, 48000, 0`) and the little-endian `u16` byte count before each packet come from one title (The Smurfs Dreams); only 48000 Hz and a zero third word are accepted -- [sceAjmBatchJobControl](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - RESET (bit 13), INITIALIZE (bit 14) and the sideband input order (gapless decode, then initialize parameters) come from [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4); other flags throw -- [sceAjmBatchJobControl](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - RESET is assumed to clear the context as sceAjmBatchJobClearContext does -- [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) - 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 -- [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 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 -- [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 -- [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 -- [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 -- [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 -- [sceHttpSetRequestStatusCallback](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - no reference implements it (shadPS4 only logs and returns 0); when the callback runs and what status it gets is unknown -- [sceNpSessionSignalingGetMemoryInfo](../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - unknown signature -- [sceNpSessionSignalingGetConnectionStatistics](../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - unknown signature -- [sceUserServiceGetAgeLevel](../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the meaning of the value is unknown; 0 is reported, as for the unset game presets. PPSA12544 (Unity) only stores it for its user profile -- [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 -- [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 -- [sceNgs2SystemSetSampleRate, sceNgs2SystemSetUserData, sceNgs2SystemGetUserData, sceNgs2SystemLock, sceNgs2SystemUnlock](../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - shadPS4 only checks the system handle (`INVALID_SYSTEM_HANDLE`) and SharpProspero names what each call does; neither gives the other limits. A sample rate of 0 and a null user data pointer throw, any other rate is accepted, and changing the rate while a sampler filter is enabled throws because the filter coefficients were computed for the old rate. Lock and unlock take the same lock as `sceNgs2RackLock` -- [sceNgs2RackGetInfo](../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - field order from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `OrbisNgs2RackInfo` with the 64-byte name of the other info structures (216 bytes); `type` is assumed to be the rack id divided by 0x1000, `min_grain_samples` 64 (the system minimum) and `state_flags` 1; `uid` comes from a per-process rack counter, `render_count` is the owning system's render count and `active_voice_count` counts voices that are not empty (paused and stopped voices included). The last process ratio and tick and `active_channel_work_count` are left zero -- [sceNgs2VoiceQueryInfo](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - only info id 0x4001 is known: PPSA22520 passes an 8-byte buffer and branches on the first word being 1 or 2, so it is assumed to be the voice channel count (0 before the voice is set up); the second word is zeroed and other ids throw -- [sceNgs2VoiceGetPortInfo](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - layout from shadPS4; the delay and destination input are zero because ports have neither yet -- [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 -- [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 -- [sceVoiceSetMuteFlag](../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature -- [sceTextToSpeech2GetSystemStatus](../../core/libs/prx/libSceTextToSpeech2/Export.cpp) (libSceTextToSpeech2) - unknown signature -- [sceCoredumpAttachUserMemoryFile](../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - unknown signature -- [sceCoredumpAttachMemoryRegion](../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - unknown signature -- [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 -- [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) -- [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` -- [libSceUlt ulthreads](../../core/libs/prx/libSceUlt/Export.cpp) - `_sceUltUlthreadRuntimeOptParamInitialize`, `_sceUltUlthreadRuntimeCreate` and `_sceUltUlthreadCreate` are assumed to take the same arguments as the existing exports without the underscore, including the trailing build version. `sceUltUlthreadTryJoin` is assumed to return `ULT_ERROR_BUSY` while the ulthread runs, and `sceUltUlthreadRuntimeDestroy` to return `ULT_ERROR_BUSY` while an ulthread created on the runtime is not joined. Neither error has been confirmed on PS5 hardware -- [libSceUlt queues](../../core/libs/prx/libSceUlt/Export.cpp) - the added `sceUltQueuePop`, `sceUltQueueTryPush`, `sceUltQueueDestroy` and `sceUltQueueDataResourcePoolDestroy` signatures are inferred from the existing push/pop and object APIs. Queue destruction is assumed to reject blocked callers with `ULT_ERROR_BUSY` and otherwise discard queued data; pool exhaustion returns `ULT_ERROR_AGAIN`. These rules, alignment checks and shared slot limits have not been confirmed on PS5 hardware or a title. Non-null queue and data-pool option parameters throw because their layouts are unknown. -- [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 -- [_sceUlobjmgrRegisterObject](../../core/libs/prx/ulobjmgr/Export.cpp) (ulobjmgr) - parameters taken from shadPS4 (PS4): a nonzero 64-bit object, a nonzero 32-bit kind and a 32-bit id output; the meaning of the first two and the PS5 signature are unverified. The id bound of `_sceUlobjmgrUnregisterObject` (below 0x4000) and the raw `EINVAL` (22) return also come from shadPS4 -- [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 -- [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) -- [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 -- [Shader registration](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a shader header is accepted at any address: PPSA26344 passes headers 4 and 1 bytes past an 8-byte boundary; whether the console refuses an alignment is unknown -- [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 -- [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 -- [sce::Json::InitParameter2](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - layout inferred, not known: assumed to start with the allocator, its user data and the file buffer size, in that order, and the whole struct is assumed to fit in 40 bytes; `Initializer::initialize(InitParameter2)` ignores them as `initialize(InitParameter)` does -- [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 -- [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 -- [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 the call waits for the timeout and returns 0 events, and a wait without limit throws - -- [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 +- The Share, NpTrophy2, NpEntitlementAccess and WebBrowserDialog exports imported by PPSA12544's Unity plugins and added without an implementation have unknown signatures +- Acm, GameLiveStreaming, Net, NpEntitlementAccess, VoiceQoS, WebBrowserDialog and libkernel exports imported by PPSA14632 and added without an implementation are declared without parameters; of the VoiceQoS ones only those still throwing remain so ### 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. -- [mkstemp and isatty](../../core/libs/prx/libScePosixForWebKit/Filesystem.cpp) (libScePosixForWebKit) follow FreeBSD's [temporary-file API](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/stdio/mktemp.c) and [terminal check](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/gen/isatty.c), without console verification. `mkstemp` replaces all trailing `X` characters, accepts a fixed name, and creates files through the guest path resolver with exclusive read/write access and mode 0600. Windows file permissions follow the existing kernel file implementation. `isatty` tests Windows console handles rather than all character devices; files, pipes and guest sockets return `ENOTTY`, closed descriptors return `EBADF`. -- [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. -- [Windows file timestamps](../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes` and `futimes` reject timestamps outside nonnegative signed 64-bit Windows file-time ticks with `EINVAL`, including exactly 1601-01-01 00:00:00 UTC, whose zero value means "leave unchanged" to `SetFileTime`. Guest directory descriptors still resolve through their recorded path. Filesystems with coarser timestamp resolution cannot retain every guest microsecond -- [libSceHttp2 asynchronous requests](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) never reach the network: `sceHttp2SendRequestAsync` accepts the request, queues a completion whose result is the library's network error and triggers the user event named by the kqueue option (equeue at offset 0, event id at 8, user data at 16, layout inferred from PPSA17221); `sceHttp2WaitAsync` returns that completion. The completion's `event_type` value is unknown and left 0. PPSA17221 completes its request object only after `sceHttp2SendRequestAsync` returns, and its handler for a synchronous failure calls `shared_from_this` before that, so a send must not fail synchronously. A kqueue option or user event that is missing, an extra option, and a wait with no completed operation throw -- [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 -- [Wide-character conversions](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - [`swprintf`](../../core/libs/prx/libc/src/Formatting.cpp) still passes guest strings to the host's `wchar_t` functions. The guest's `wchar_t` is 16 bits, so on Linux, where the host's is 32 bits, it misreads and miswrites guest strings. 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 -- [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. -- Windows host-pointer imports of the guest mapping itself are compared instead of write-watched: a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. This preserves CPU edits through byte comparisons but disables snapshot reuse and unit shadows for excluded blocks; restoring tracking on import retirement without replacing the backing and measuring the comparison cost remain unimplemented. -- [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 -- [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 -- [sceKernelPread, sceKernelPwrite, sceKernelPreadv, sceKernelPwritev](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on Windows transfer at the given offset with an overlapped `ReadFile`/`WriteFile` and restore the descriptor's file pointer afterwards, so a concurrent `read`/`write` on the same descriptor can see the moved pointer in between, unlike on the console. Handles that are not disk files return `ESPIPE`. `readv` on a pipe or console handle stops after the first iovec read, a short read the console is assumed to allow; each iovec is transferred separately, so a failure after the first transfer returns the bytes already done -- [sceKernelAioSubmitReadCommands, sceKernelAioSubmitWriteCommands and their Multiple variants](../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) on Windows transfer through the same positioned path as `sceKernelPread`/`sceKernelPwrite` and restore the descriptor's file pointer afterwards, so a concurrent `read`/`write` on the same descriptor can see the moved pointer in between, unlike on the console -- [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 -- [sceKernelSync](../../core/libs/prx/libc/src/General.cpp) (libkernel) on Windows 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. Such a file's data may not reach the disk before a host crash; the guest reads the same data either way -- [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) delivers on Windows only and throws on Linux -- [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 -- [sceKernelRaiseException](../../core/libs/prx/libkernel/System/src/Exception.cpp) reaches a thread that waits in an alertable `TimedWait` wait through an APC. A thread blocked anywhere else, including all waits under `APS5_COARSE_TIMED_WAITS` and host waits inside the emulator, is redirected and runs the handler only when that wait returns -- [sceKernelRaiseException](../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows runs the handler wherever the target thread was interrupted. It waits only while the thread is inside winpthreads or restoring a context through `NtContinue`, and throws if that lasts 1 s, so a handler that calls into other host code the thread was interrupted in, such as the C runtime's heap or a lock of the emulator, can deadlock on it or reenter it -- [sigaltstack](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records each thread's alternate signal stack and reports it back, following FreeBSD's `kern_sigaltstack` (`EINVAL` for flags other than `SS_DISABLE`, `ENOMEM` below `MINSIGSTKSZ` 2048), but handlers never run on it, so `SS_ONSTACK` and `EPERM` are never reported -- [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`. -- [Thread cancellation](../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) is not modelled: `scePthreadCancel` throws, so no cancel request is ever pending and `scePthreadTestcancel` and `pthread_testcancel` return without acting; the cancel state and type set by `scePthreadSetcancelstate` and `scePthreadSetcanceltype` are stored and not used -- [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. -- [Packed horizontal gathers](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_gather4h_pck`, MIMG opcode 98): the four window texels are read raw and concatenated into a little-endian bitstream, with DMASK bit i writing stream word i to the next destination register; measured on RDNA2 (gfx1031) at every edge: the anchor is floor(x-0.5) with elements at -1, 0, +1 and +2 clamped horizontally into the width, the row is floor(y), negative rows clamp to row 0, rows at or past the height return zeros, sampler addressing modes do not affect the window, and formats wider than 32 bits per texel write zeros in every register. The emitter fetches each window texel at LOD 0 and packs its bits like `image_load_pck`, so the sampler's filter, addressing and LOD state are not used; formats whose bits are not recoverable from the view and D16 data throw. The anchor and row multiply the normalized coordinate by the image size without contraction. The stream rule was measured for R8_UNORM, RG8_UNORM and RGBA8; only RGBA8 has an execution test. `image_gather8h_pck` (opcode 99) never returns texel data on this hardware - across dmasks 0x1 to 0xFF including the upper nibble, RGBA8, RGBA16 and RGBA32 formats, 1D, 2D and 3D descriptors and normalized and unnormalized sampling it only zero-fills popcount(dmask) registers (sentinel-prefilled destinations) - so it decodes but throws at translation; its eight-texel semantics remain unmeasured. -- [MIMG BY2/BY4 loads](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_load[_mip]_by2/by4`, opcodes 0x42, 0x43, 0x4a and 0x4b) were measured on RDNA2 (gfx1035) with the [hardware oracle](HW_ORACLE.md) for 2D images with identity swizzle. They read N texels (BY2: 2, BY4: 4) from `x & ~(N - 1)` on row y, texel by texel and component by component within each texel, with each texel converted like `image_load`. Every register 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 the N texels fit in one dword: R8 and R16 (DMASK 0x3) or RG8 (0xf) for BY2, and R8 (0xf) for BY4. With 32-bit texels (R32, RG32, RG16, RGBA8) the hardware returns zeros. Other formats, masks, dimensions, swizzles, D16/A16 and indirect descriptors throw. The stores (0x52, 0x53, 0x5a and 0x5b) decode but throw at translation. Measured on hardware, they write from the unaligned x up to the end of its dword, which is not implemented. -- [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). -- [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. -- [Unnormalized S#s](../../core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp) are accepted only when direct and used only by explicit-LOD `image_sample` without offset, 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#. -- 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. -- [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 -- [`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. -- [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. -- [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. -- [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. -- The shader recompiler [ignores `s_setreg_b32`](../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp): writes to MODE (float rounding and denormal controls) and the other hardware registers have no effect, and `s_getreg_b32` reads of the MODE round fields report round to nearest even even after `s_setreg_b32` wrote another mode. The initial round mode from the shader's program registers is not read either. -- A [flat SRT read](../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) through a null or unmapped pointer loaded from memory raises a [BDA fault](../../core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp) only if the program executes it, then stops or reads zero like a BDA read. -- Repeated `s_load`s of one [flat SRT](../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) dword through a null or unmapped pointer report one of their pcs, and an `s_load` whose inaccessible dwords are all unused, or only address repeated loads, reports no fault of its own. -- A value read through a null or unmapped pointer still throws at record time if a [branch condition, uniform fill or table base](../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) needs it, or if the pointer comes from user data, even if the console skips the read. -- 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. -- 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. -- [`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); other such reads throw. -- [Shader recompilation](../../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../../core/shader/recompiler) was written completely independently from [libs/prx](../../core/libs/prx). -- [v_interp_p1ll_f16, v_interp_p1lv_f16 and v_interp_p2_f16](../../core/shader/recompiler/Translation/src/AttributeInstructions.cpp) read the 16-bit halves of each vertex's parameter through `VK_KHR_fragment_shader_barycentric` and throw without it, as fixed-function interpolation cannot split a 32-bit attribute. P10 and P20 are the vertex differences rounded to f16 to nearest even, which is not confirmed on hardware. v_interp_p1ll_f16 and v_interp_p1lv_f16 round the exact `P10 * I + P0` once to f32 and v_interp_p2_f16 rounds the exact `P20 * J + S2` once to f16, as measured on RDNA2 (#697); the exact sums are computed in f64, so these instructions need the Float64 capability. They follow the shader's float mode, measured on RDNA2 (gfx1035) in all eight combinations of `IEEE_MODE` and kept/flushed f32 and f16 denormals over 768 special and 1,024 random rows. With IEEE=1 a NaN operand is returned quieted; with IEEE=0 it is returned unquieted, except that v_interp_p2_f16's f16 result is always quiet. Flushed f32 denormals flush I/J, v_interp_p2_f16's f32 S2 and the f32 result; flushed f16 denormals flush P0, P10/P20, v_interp_p1lv_f16's f16 S2 and the f16 result. Denormal modes 1 and 2 (flush only inputs or only outputs) and rounding other than to nearest even are not measured and throw. An unknown mode follows IEEE=0 with denormals kept. Lavapipe has no barycentrics, so the draw test is skipped in CI; `agc_driver_interpolation_modes` checks the arithmetic in every mode against the hardware rows on any device with Float64. -- [v_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. -- [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. -- [`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. -- [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. -- [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`, `IEEE_MODE` and the `FLOAT_MODE` output denormal bits by the output modifier, `DX10_CLAMP` by the clamp modifier, 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 lowering, which matches IEEE=0. The f16 and f64 min/max/med3, NaN quieting in f16 arithmetic (the unary instructions and the f32 arithmetic follow it, see below), `FP16_OVFL` 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. -- [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): other denormal settings are not modelled. -- [v_fma_legacy_f32](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (VOP3 0x140) is assumed to be fused, which has not been measured on hardware. LLVM decodes this encoding as `v_mad_legacy_f32` for gfx1013, and `v_mac_legacy_f32` (VOP2 0x06) was measured to round its product before the add, so 0x140 may do the same. -- [v_fma_f16, v_pk_fma_f16 and v_pk_fmac_f16](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) are an f32 FMA rounded to f16, which rounds twice: 0x62e9 * 0xd159 + 0x23ec gives 0xf89e, the hardware 0xf89f. -- [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; shaders without a known float mode follow IEEE=0. `v_add/sub/subrev/mul/fma/fmac/fmamk/fmaak/mul_legacy/fma_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 of `v_fma_legacy_f32` still wins). The f16 arithmetic, `v_fma_mix_f32` with f16 sources, `v_cubema`, `v_mac_legacy_f32` 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` and `v_dot2_f32_f16` always quiet and `v_div_scale_f32` never does. -- [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/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; 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. Tininess is detected before rounding: a product whose exact value is below 2^-126 gives zero even when it rounds to 2^-126, computed exactly from the significands for `v_mul_f32`, `v_mul_legacy_f32`, and for `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. `v_fma_mix_f32` was measured with f32 sources only; its f16 sources are not flushed. Denormal modes 1 and 2 throw. 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_mac_legacy_f32`, `v_dot2_f32_f16`, the division steps (on hardware `v_div_fixup_f32` flushes only src1 and src2, `v_div_fmas_f32` only its result), f32 interpolation and LDS float atomics keep the previous lowering. With f32 denormals kept (3) the results depend on the driver's default denormal behaviour, as no `DenormPreserve` execution mode is requested, and `v_ldexp_f32` flushes in every mode. -- The executable file that [relinker](../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens the console when launched, which is inconvenient for playability. -- [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. -- [Per-stage descriptor count](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a pixel shader's colour attachments are not counted against `maxPerStageResources`, so its buffers and images can fit the limit while the pipeline exceeds it -- [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. v_sin_f32/v_cos_f32 with inputs beyond ±256 cycles were not compared with the console. -- `--to-intel` does not lower RDPRU/MCOMMIT; the [matcher](../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) fails the relink instead. -- [Ordered not-equal compares](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) (shader recompiler) - `FPOrdNotEqual32` is emitted as `OpFUnordNotEqual` with both operands checked for NaN on their bits instead of `OpFOrdNotEqual`; the two agree 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 the compare feeds a ballot on a 32-wide subgroup (#825) -- 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. -- [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.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. -- [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. -- 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. -- 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. -- [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) - only a CMASK whose nibbles are all fast-cleared (0) or all expanded (0xF) is modeled, checked over the target's whole CMASK, 4 KiB per 1024x512-pixel metablock (the size PS5 titles clear, for every color format seen); the addressing within a metablock is not decoded. The first draw or fast-clear eliminate over a fast-cleared CMASK fills the whole target with `CB_COLOR_CLEAR_WORD` and stores those bytes as expanded, so a draw that covers only part of the target reads the clear value in the rest as the eliminate would leave it. On a DCC target the clear is in the DCC keys, so its CMASK must be all expanded. A mixed CMASK throws, as does a DCC target's CMASK that is not all expanded and a fast clear of a mipmapped, 3D or array color target -- [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. -- [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. Seamless cube filtering and out-of-range cube-array sampling remain unimplemented. PS5 equivalence has not been measured on console hardware. -- [DCC display buffers](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) are presented only when all keys of the console's DCC extent agree on uncompressed or a fast-clear code, padding keys included; other keys, and register-clear keys over a pending image, throw. -- [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). -- [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. -- [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. -- [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 +- 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. -- `--to-intel` replaces the approximate VEX.128 register forms of VRSQRTPS and 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). The 256-bit, scalar, legacy and memory forms keep the native instruction, as does a 4-byte site 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). -- [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. -- [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. -- [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. -- [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. -- [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. -- [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. -- The [output modifier](../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) (`mul:2`, `mul:4`, `div:2`) follows the shader's float mode as measured on RDNA2 (gfx1035): the hardware applies it only with IEEE mode off and output denormals of the result's precision flushed (bit 1 of the `FLOAT_MODE` denormal field clear), and ignores it otherwise. On f32 results it is applied as the hardware does with f32 denormals flushed; a shader without a known float mode is translated with IEEE mode off and f32 denormals flushed, the mode titles use. On f16 and f64 results it is ignored, and it throws in the modes where the hardware applies it. -- The [clamp modifier](../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follows `DX10_CLAMP`, measured on RDNA2 (gfx1035) for f32, f16, packed f16 and f64 results at IEEE=0 and IEEE=1: with `DX10_CLAMP`=1 (and in shaders without a known float mode) a NaN result clamps to +0; 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. -- [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 and 2D array views, float/unorm/snorm formats, wrap, clamp-to-edge and clamp-to-border addressing (border red 0 for the black borders, 1 for opaque white) and point or bilinear filtering at one level are implemented, with texel offsets added to the texel indices; LOD biases and clamps are accepted, as they cannot select another level of a single-level sample. Half-border addressing, the border color table, gradients, gathers, other dimensions, anisotropy and mip filtering throw. Under bilinear filtering, taps past a clamp-to-border edge compare the border value and blend with the texel taps, so results near the edge depend on the filter weights. AMD documentation does not say how a color format's reference is handled: it is clamped to [0, 1] for unorm and [-1, 1] for snorm, as Vulkan does for unorm depth, and not clamped for float. Bilinear weights are full float, not the hardware's fixed-point subtexel weights -- [libScePlayerInvitationDialog](../../core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp) simulates dialog completion without displaying UI or sending invitations, so `scePlayerInvitationDialogGetResult` reports `USER_CANCELED`; its parameter ABI remains unverified. The 40-byte result layout (error code, result, 32 reserved bytes) is taken from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Dialog/PlayerInvitationDialog.cs) and the error codes from the other common dialogs, not confirmed on a title. -- [Converted unorm images](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (10_11_11_UNORM, read and written through an R32_UINT view): samples, gathers, LOD queries, 16-bit data and a DST_SEL of the missing W channel throw. -- [Converted unorm image loads](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) divide by 2^n - 1 with the buffer format path's `OpFDiv`, which Vulkan allows to be up to 2.5 ULP off, so some drivers are not bit-exact. -- [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. They are atomic with respect to each other only: 32-bit accesses to the same dwords issued concurrently are not ordered by the lock. 64-bit GDS atomics throw. `ds_min/max(_rtn)_f64` and `ds_cmpst(_rtn)_f64` follow the NaN and signed-zero rules measured for the f32 forms; the f64 forms were 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. -- [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. -- [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). -- [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. -- [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`. -- [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). -- [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) passes `%ls` and `%S` to glibc on Linux, which reads them as strings of 32-bit `wchar_t`; the guest's `wchar_t` is 16 bits (UTF-16), so these conversions fail with -1. The Windows path converts them to UTF-8, and `guest_formatting` checks wide conversions on Windows only -- [collate::_Getcat](../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) builds the facet for the C locale only and throws for any other locale name. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size, and for results of 4095 characters or more, whose string storage uses the guest's over-aligned large allocation. -- [The scanf family](../../core/libs/prx/libc/src/Formatting.cpp) (libc) hands `%i` to glibc on Linux, which since 2.38 reads the C23 `0b`/`0B` binary prefix when the library is built as GNU C++: `sscanf("0b101", "%i", &value)` stores 5 where C17 stores 0 and stops at the `b`. The strtol family stops at the prefix as C17 does -- [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` -- [poll](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits only on guest sockets, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A descriptor below the socket range (files, pipes, stdio) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), `POLLPRI` on Windows, which `WSAPoll` rejects, and an infinite timeout with no socket to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked -- [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 -- [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 -- [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. -- [Font writing lines](../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - `sceFontCreateWritingLine`, `sceFontWritingLineWritesOrder`, `sceFontWritingLineRefersRenderStep`, `sceFontWritingLineGetRenderMetrics` and `sceFontWritingLineClear` follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libFont.cpp): runs are placed left to right, each at the line's advance so far, with no spacing or adjustment. Only horizontal and left-to-right lines, writing attribute 0 and non-null run metrics are modelled; other values, the metrics of an empty line and unknown or null handles throw, since no reference gives their result or error code. `sceFontWritingLineGetOrderingSpace` still throws (KytyPS5's values are placeholders). 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. -- [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, or a second `s_swappc_b64` sharing it) throws; constant-offset folding over the link is not modeled. The special encoding `s_swappc_b64` with SDST 125 still decodes as `s_setpc_b64`, keeping the existing decode quirk. -- 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. -- [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 -- [Player review dialog](../../core/libs/prx/libSceCdlgPlayerReview/Export.cpp) (libSceCdlgPlayerReview) shows nothing: `scePlayerReviewDialogOpen` finishes at once and the result reports that the user canceled, as `sceWebBrowserDialogOpen` does -- [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. -- [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. -- [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 -- [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. -- [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. -- [localtime, mktime](../../core/libs/prx/libc/src/Time.cpp) (libc) on Windows convert through the CRT's `localtime_s` and `mktime`, which accept about 1970 to January 3001 UTC and apply the CRT's daylight-saving rule; outside that range `localtime` returns null and `mktime` returns -1, where the console converts. `gmtime` converts every time whose year fits an `int` on both hosts. -- [Static scalar calls](../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) (`s_call_b64` and `s_swappc_b64`) require one call per ordinary even-aligned SGPR link pair (s0:s1 through s104:s105) and one later `s_setpc_b64` return reading that exact pair. Regions must nest or be disjoint; recursion, 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. -- [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. Native RDNA2/PS5 execution and title compatibility for these bounded scalar calls remain unverified; `s_rfe_b64` still throws in translation. -- [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 -- [syslog](../../core/libs/prx/libc/src/Syslog.cpp) (libc) writes each message to `stderr` as `[syslog:] `, with a newline added when missing, instead of sending it to the console's system log. Priority and facility handling follow FreeBSD's `vsyslog`: `%m` expands to the guest errno's message, unknown bits are reported (as priority 0x23, since FreeBSD passes `LOG_ERR|LOG_CONS|LOG_PERROR|LOG_PID` as the priority) and masked, a missing facility becomes `LOG_USER`, and errno is preserved. `openlog`, `setlogmask` and `closelog` are not implemented, so the identity, options and mask they set are not applied -- [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 and other signals (`SIGBUS`, `SIGHUP`, ...) are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`; `raise` of an `SA_SIGINFO` handler on a signal `signal` delivers throws instead of terminating, and `raise` of the other signals still returns `EINVAL` + +- [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/libSceAcm.md b/docs/dev/debt/libSceAcm.md new file mode 100644 index 0000000000..59682c3221 --- /dev/null +++ b/docs/dev/debt/libSceAcm.md @@ -0,0 +1,13 @@ +# libSceAcm technical debt + +### Silent stubs + +- [libSceAcm](../../../core/libs/prx/libSceAcm/Export.cpp) batches execute as silent no-ops, so `sceAcm_ConvReverb_SharedInput`, `sceAcm_FFT` and `sceAcm_Panner` return success without encoding audio commands. + +### Unknown function info + +- Context validation error codes (0x81940002 for bad context and 0x81940006 for bad pointer) for [libSceAcm](../../../core/libs/prx/libSceAcm/Export.cpp) follow prosper's `hle_audio_spatial.cpp`. KytyPS5 (`acm.cpp`) and prosper do not track individual batch identifiers, so `sceAcmBatchWait` returns success for valid contexts without inspecting batch IDs. Hardware ACM DSP coprocessor scheduling and console error verification remain unmeasured. +- [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 +- 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). + +- [`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`. diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md new file mode 100644 index 0000000000..38c82421d4 --- /dev/null +++ b/docs/dev/debt/libSceAgc.md @@ -0,0 +1,50 @@ +# libSceAgc technical debt + +### Unknown function info + +- [GS shader-half fusion](../../../core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp) (libSceAgc) - `SPI_SHADER_PGM_RSRC2_GS.OC_LDS_EN` is preserved if either half requests it, while user SGPR allocation comes from the front half and other state stays with the back half. Bit 18 is `OC_LDS_EN` in AMD's [GFX10 register definitions](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/include/asic_reg/gc/gc_10_1_0_sh_mask.h). Mesa's radeonsi sets it on a merged ES+GS program exactly when the ES stage is a tessellation evaluation shader ([`si_shader_gs_legacy`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/45ea3ed8ce649d6b72d1843307a29d9cc3b7704e/src/gallium/drivers/radeonsi/gfx/si_state_shaders.cpp#L631), [`gfx10_shader_ngg`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/45ea3ed8ce649d6b72d1843307a29d9cc3b7704e/src/gallium/drivers/radeonsi/gfx/si_state_shaders.cpp#L1144)), so it belongs to the front half; console fusion output has not been measured. Cocoon (PPSA08766) requests front `0x70004` and back `0xab0020`. Other unmatched hardware state still throws. + +- 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 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) - 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 +- [sceAgcCreateInterpolantMapping_10000](../../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) - name from its NID (`dbOlWdppb4o`); same arguments as `sceAgcCreateInterpolantMapping_0100`; PPSA26344 passes three pointers at each of nine observed call sites (`0x9F914E`, `0x9F91E2`, `0x9F9216`, `0x1604BBD`, `0x16122D7`, `0x1618285`, `0x161838C`, `0x16184EE`, `0x1618607`), consistent with that argument assumption, but does not read the return value there, so its return type is not established. The `SPI_PS_INPUT_CNTL` bits for `is_f16` 2 inputs follow KytyPS5, not confirmed on hardware +- [sceAgcGetDataPacketPayloadRange](../../../core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp) (libSceAgc) - signature, the `{base, size in bytes}` output and the type 0 range (header + 1, one dword longer than the type 1 payload) from KytyPS5 only, matching `sceAgcGetDataPacketPayloadAddress_0090`; prosper returns header + 2 for both types from that address function +- [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) +- [sceAgcUpdateInterpolantMapping_10000](../../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) - name from its NID (`vieBRwlh1Lw`), not in the aerolib NID list; the `(regs, gs, ps)` arguments of `sceAgcCreateInterpolantMapping_0100` come from OpenAGC only +- [sceAgcDcbSetZPassPredicationEnableGetSize, sceAgcDcbSetPredicationDisableGetSize, sceAgcDcbSetBoolPredicationEnableGetSize](../../../core/libs/prx/libSceAgc/DcbState/src/Predication.cpp) (libSceAgc) - the builders are inline in the SDK and not exported; the size is assumed to be that of the SET_PREDICATION packet `sceAgcDcbSetPredication` writes (16 bytes) +- [sceAgcUpdateInterpolantMapping_10000](../../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) follows [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c) only, not confirmed on hardware: the `sceAgcCreateInterpolantMapping_10000` transform without the identity tail, so it writes one register per pixel shader input and none for a pixel shader without inputs, and a null pixel shader throws. For an `is_f16` 1 or 3 input whose `default_value` and `default_value_hi` differ it throws: OpenAGC takes `DEFAULT_VAL` from `default_value_hi`, while KytyPS5, sharpemu and `sceAgcCreateInterpolantMapping_10000` here take it from `default_value` +- [sceAgcAcbWaitUntilSafeForRendering](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - `(buffer, videoOutHandle, displayBufferIndex)`, the declared arguments, which PPSA12544 (01.000.010) passes inside `waitUntilSafeForRendering` and `waitUntilSafeForRendering VR` markers on an asynchronous compute command buffer, ignoring the result. It writes the same rendering-wait packet as `sceAgcDcbWaitUntilSafeForRendering`, which the driver now accepts on compute queues too; the wait blocks only that queue's worker. Whether the console encodes the compute variant differently is not known +- [sceAgcGetDefaultCxStateFlat](../../../core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp) (libSceAgc) - copies up to `size` bytes (clamped to 2048 bytes, the 512-dword `AgcContextState`) from the default context state initialized from the version 8 public register default groups (CX, SH, UC), indexed by register offset, following OpenAGC `src/context_state.c`; returns `0x80890001` on null destination; version selection without `sceAgcInit` is unverified + + +- [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. +- [sceAgcDcbSetWorkloadsActive and sceAgcDcbSetWorkloadComplete](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) (libSceAgc) and [sceAgcDriverRegisterWorkloadStream](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - the driver call records only the stream id (1 to 31; it returns 0 for any argument and keeps no stream record), and the DCB calls write the packets [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/agc.cpp) writes: an 18-dword (active) or 12-dword (complete) `IT_NOP` packet with the stream id, and for active the 64-bit mask of the workload ids, for complete the workload id and the 64-bit clear mask of that id. Both return null for a null command buffer, an unregistered or out-of-range stream, a workload id above 63, and for active a duplicate id or a count of 0 or above 63. The layouts and checks come from KytyPS5 alone; the console oracle in [prosper](https://github.com/mattias800/prosper/blob/main/prosper/tests/data/console_oracle/agcbuild.golden.tsv) measures the null return only for unregistered streams, not the packet contents of a registered one. The error codes the console driver returns for an invalid or repeated registration are unknown, so registration never fails here. PPSA29866 calls sceAgcDcbSetWorkloadsActive while building its command buffers (sceAgcDcbSetWorkloadComplete was not observed); nothing else was observed. +- [sceAgcDcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) and [sceAgcAcbSetWorkloadsActive, sceAgcAcbSetWorkloadComplete, sceAgcAcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - return null without writing to the buffer for a null command buffer or a stream that is not registered (out-of-range ids included), and throw for a registered stream. The console oracle in prosper measured only that null return, with no stream registered, three argument patterns each and nothing written ([ACB](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L48-L56), [DCB](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L196-L204)); the packet a registered stream gets is not measured. KytyPS5 has no body for these four, and its `IT_NOP` layout for the DCB active and complete calls differs from the SET_UCONFIG_REG, WRITE_DATA and SET_WORKLOAD sequence [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/agc_workload_packet.c) derives from the driver, so neither is taken. The `(buf, streamId)`, `(buf, streamId, workloadId)` and `(buf, streamId, workloadIds, workloadCount)` signatures are those of the DCB calls in [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L2317-L2384), which says the ACB calls use the same driver builders; no ACB declaration was found. `sceAgcDcbSetWorkloadsActive` checks the stream before it reads the workload ids, as OpenAGC does: the console returned null for stream 1 with the ids at address 2 ([measured](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L203)) without faulting +- [sceAgcCreatePrimState](../../../core/libs/prx/libSceAgc/Shader/src/PrimState.cpp) (libSceAgc) - the `ge_user_vgpr_en` special is passed to the user configuration registers at whatever offset the shader header stores, as the SPRX does according to [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1181-L1252), which copies all four specials verbatim (KytyPS5 stops on any other offset). PPSA99039, PPSA99505, PPSA99515 and PPSA99997 build their headers by hand with it at 0x25c (`GE_USER_VGPR1`) instead of 0x262 (`GE_USER_VGPR_EN`), value 0, and run on the console; it is not moved to 0x262, and the driver reads neither register. `ge_cntl`, `vgt_shader_stages_en` and a used `vgt_gs_out_prim_type` must still be at their own offsets, since the driver reads those registers there, so a header that stores one elsewhere throws naming the offset, although by OpenAGC the SPRX would copy it as well +- [sceAgcDcbEventWrite](../../../core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp) (libSceAgc) - non-sample-dump event types (any event other than 0x38 and 0x39) emit a 2-dword PM4 packet 0x46 without address dwords; the address argument is ignored for these events. Void Terrarium (PPSA03061, #5055) passes a non-null address buffer (0x00000002d2a10000) with event type 0x16 (cache flush). diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md new file mode 100644 index 0000000000..05682afa75 --- /dev/null +++ b/docs/dev/debt/libSceAgcDriver.md @@ -0,0 +1,225 @@ +# 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 + +- [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 + +- [sceAgcDriverAcquireHDRScopesACQ, sceAgcDriverGetCoredumpAddress, sceAgcDriverHp3dLockEnd, sceAgcDriverHp3dLockStart, sceAgcDriverIsEsvmProcess, sceAgcDriverNopDataWritePacket, sceAgcDriverReleaseHDRScopesACQ, sceAgcDriverSetTargetRingForDiag, 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`. + +- [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. + + +- [Optional BMP frame dumps](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) use checked host stdio. An open, header, pixel or close failure prints one diagnostic and disables subsequent BMP dumps for the process; queued frames drain without more file I/O. Pixel validation and allocation failures retain the existing fatal path. The CPU fixture checks identical BMP bytes and real pipe failures. VideoOut flips on an RTX 3060 continue after a first dump that cannot be opened; header, pixel and close failures during presentation are unmeasured. + +- [Inactive vertex point size](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) - `PA_CL_VS_OUT_CNTL.USE_VTX_POINT_SIZE` is accepted for lines, triangles and rectangles with `GS_EN` and `HS_EN` clear, when polygon mode is disabled or both faces are filled. Points, patches and absent routing or rasterization registers still throw. Bit 16 selects the misc export's X component as PointSize ([AMD CIK register reference](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/programmer-references/CIK_3D_registers_v2.pdf), PA_CL_VS_OUT_CNTL); ignoring it for non-point rasterization follows [Vulkan PointSize](https://docs.vulkan.org/refpages/latest/refpages/source/PointSize.html). This is inferred, not measured on console hardware. XashPS5 (PPSA19111 v1.3) sets `0x01210000` on a filled triangle draw. +- [Native multisampled depth clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) accept the recognized HTILE mask kernel's direct full-range dispatch after its GPU writes complete. Every metadata byte needs tracked driver-store coverage since notification, no intervening CPU or depth write, and HTILE words proving the current clear value of zero or one. Partial, overlapping, indirect and unproven producers remain unsupported. Existing sampled views retain the native image; guest depth publication and metadata-fill transitions remain unimplemented. Windows shared imports retain write tracking through separate aliases; private host imports that lose tracking and copied or mirrored HTILE uploads remain unsupported. Clear-word encoding and console equivalence remain unmeasured on PS5 hardware. +- [Native depth allocation retirement](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) covers sampling-eligible multisampled owners only. Successful release notifications must cumulatively cover both complete depth and HTILE ranges; protection changes, failed releases and partial coverage keep the original guards. Retired owners leave lookup and metadata routing, while held views and recorded work retain their images. Retirement of single-sample or sampling-ineligible depth attachments and console equivalence remain unimplemented or unmeasured. The regression uses real allocation-registry operations with a no-op host-release callback, so it does not validate the host unmap backend. +- [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. Guest publication, 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. +- [Line-strip geometry output](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a geometry program with `VGT_GS_OUT_PRIM_TYPE = 1` (line strip) runs as a mesh shader with `OutputLinesEXT`: each primitive export is one line whose two vertex indices are packed like the three of a triangle, as Mesa's NGG lowering exports line strips (`ac_nir_lower_ngg_gs.c`), and each input primitive gets `VGT_GS_MAX_VERT_OUT - 1` lines where a triangle strip gets `- 2` triangles. A subgroup gets at least `GE_MAX_OUTPUT_PER_SUBGROUP` line slots, because Mesa's lowering exports one primitive, possibly null, per emitted vertex; whether the console's compiler does the same is not known, and triangle strips keep `- 2` per input. A fast-launch subgroup takes its line count from `GE_NGG_SUBGRP_CNTL.PRIM_AMP_FACTOR` as for triangles. Line output counts as lines for the polygon offset decode, as on the vertex path. The lines are drawn 1 pixel wide with Vulkan's default line rasterization, like lines on the vertex path: `PA_SU_LINE_CNTL` is not read, and neither its width nor the console's line rules were measured. Points, rectangle lists, per-stream types and line strips from a vertex program on the mesh path still throw. +- [Alpha-to-coverage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - only `DB_ALPHA_TO_MASK = 0xaa01` is accepted: the enable bit with the four quad offsets at 2 and no `OFFSET_ROUND`, the undithered setting [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/gallium/drivers/radeonsi/si_state.c#L371-L381) programs. Draws are single-sampled, so the pixel shader [writes](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) `SampleMask` 0 when its MRT0 alpha is below one half or NaN and all ones otherwise, and runs with late fragment tests. That rule was measured on a Radeon 660M (gfx1035, RADV with `RADV_DEBUG=noatocdithering`, which programs the same register) for 0, -0, 0.25, the two floats below one half, one half, the two above, 0.75, 1, 2, -1, both infinities and both NaNs, and is pinned in [AlphaToCoverage.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp); it is not measured on gfx1013. Vulkan's `alphaToCoverageEnable` is not used: the NVIDIA driver 615.78.08 dithers single-sample coverage (an alpha of 0.25 writes half the pixels of an RTX 3050 Laptop target). Other offsets, `OFFSET_ROUND`, a sample-mask export, and an MRT0 that is missing, swizzled, packed or exported without a floating-point alpha throw. A draw without a color target on MRT0 keeps alpha-to-coverage from its MRT0 export, as [radeonsi exports MRT0 as `32_AR` for alpha-to-coverage without a color buffer](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state_shaders.cpp#L2425-L2431). A draw that enables alpha-to-coverage with no MRT0 export still throws: what the console covers then is not measured. Default dithered `DB_ALPHA_TO_MASK = 0x18701` additionally uses a per-quad sample-mask table [measured on gfx1036](https://github.com/boykopovar/AnyPS5/pull/4451#issuecomment-6124848921) for eight standard samples with FP16_ABGR or 32_ABGR exports; other sample counts, custom locations, EQAA combinations and dithered UNORM16/SNORM16 exports throw. The dithered rule is not measured on gfx1013. +- [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. `PS_ITER_SAMPLE`, an `ANCILLARY` or `SAMPLE_COVERAGE` pixel input and a sample-mask export throw in a pixel program that runs, 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. Points, lines and alpha-to-coverage throw as for a multisampled target. With a one-sample depth target, `DB_EQAA` Z interpolation (`INTERPOLATE_COMP_Z`, `INTERPOLATE_SRC_Z`) or overrasterization (`OVERRASTERIZATION_AMOUNT`, `ENABLE_POSTZ_OVERRASTERIZATION`) also throws. 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) - `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` > 0 with `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear is drawn with the target's sample count and every sample at the pixel center, as a single-sample scan into a multisampled target already is. On GFX10 and later the bit is what makes the rasterizer use the programmed sample locations ([RADV 061bc615](https://gitlab.freedesktop.org/mesa/mesa/-/commit/061bc6151a11597f720535f00e79a5752e24aff6)), and with multisampling disabled GL rasterizes at the pixel center and gives the fragment full coverage, which radeonsi programs by clearing the bit; that the hardware does the same with `MSAA_NUM_SAMPLES` > 0 is inferred from these and is not measured on hardware. A depth target with the same sample count also has every sample at the pixel center, so each depth and stencil sample is tested and written with the depth at the pixel center, as GL allows when multisampling is disabled ([L185-L190](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L185-L190), [L422-L425](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L422-L425)). 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, and how its occlusion queries count such a draw, is not measured on hardware. `DB_EQAA` Z interpolation or overrasterization and a sample-mask export with a depth target throw. Points, lines and alpha-to-coverage throw. SONIC SUPERSTARS (PPSA06888) draws such a 2x target, and METAL GEAR SOLID: Peace Walker (PPSA18216) a 4x depth and color pair. +- [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 +- [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) - for a stride-0 V# with OOB_SELECT 2 the whole fetch reads memory unless NUM_RECORDS is 0; with OOB_SELECT 3 it reads zero when NUM_RECORDS is 0 and throws when the fetch only partly fits. Rules taken from the GFX10 OOB_SELECT table in Mesa [ac_descriptors.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/ac_descriptors.c), not measured on hardware. Whether OOB_SELECT 3 checks each dword of a multi-dword fetch separately is unknown. OOB_SELECT 0 and 1 keep the structured record checks. +- [Resource registration](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - registration always fails with `RESOURCE_REGISTRATION_UNAVAILABLE`, so the owner and resource queries, `sceAgcDriverSetResourceUserData`, `sceAgcDriverFindResourcesPublic` and `sceAgcDriverUnregisterAllResourcesForOwner` return the same error without reading their arguments. Their parameter lists follow the libSceGnmDriver counterparts and are unverified. `sceAgcDriverGetResourceRegistrationMaxNameLength` writes 0xfc and returns 0, as [prosper](https://github.com/mattias800/prosper) does from a title's registration configuration; the `uint32_t*` parameter matches the SDK wrapper `sce::Agc::ResourceRegistration::getMaxNameLength(unsigned int*)`. sharpemu writes 256 and KytyPS5 returns `RESOURCE_REGISTRATION_UNAVAILABLE`, so the value is unverified; a null output throws +- [sceAgcDriverRegisterMultipleResources](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) uses the existing unavailable-registration state and leaves caller memory untouched. Its four arguments (uint32 handle output, uint32 owner, descriptor pointer, uint32 count) follow the original PPSA34502 / 01.010.000 codzm call. The caller builds 40-byte descriptors and explicitly accepts 0x8A6C9018 with an invalid output handle. Enabled registration and console argument-validation precedence are unverified. +- [Shader headers](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - registration and ABI resolution read fixed header fields through aligned copies, so the header pointer itself may have any alignment. PPSA26344 passes headers 4 and 1 bytes past an 8-byte boundary, and PPSA01317 passes 4-byte-aligned headers to ABI resolution; console alignment requirements are unknown +- [Registered shader types](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - types 0, 1, 2, 4, 6 and 7 are prepared and type 5 is skipped. Any other type, such as type 8, is logged (`unsupported registered shader type 8; it is not prepared at registration`) and left unprepared at registration, and throws when it is used later. Hello Neighbor 2 (PPSA07426), Ad Infinitum (PPSA10600) and Ghostrunner (PPSA03685) register type 8 headers and run on past them; what a type 8 header describes is unknown, #1892 found no context registers in it. +- [NGG vertex workgroups](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - type-2 PRIMGEN programs using DS instructions or `s_barrier` are prepared with the draw's routing and primitive registers on the existing mesh path. The workgroup requirement follows [Mesa's NGG lowering](https://github.com/mihawk-99/PS5_Mesa/blob/b3588f78dd7fd46fbadc7ecd487e5f14d4213e4b/src/amd/common/nir/ac_nir_lower_ngg.c#L1135); the mesh path supplies LDS and two guest lanes per host invocation for wave64 on a 32-lane host subgroup. Console routing has not been measured. Passthrough and tessellation keep their existing paths; unsupported mesh assembly and subgroup budgets throw. +- [Shader header copies](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a header bound for a draw or dispatch at another address than the registered one is accepted when its fixed fields match the registered header, 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 + +- [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. + +- [Initial stencil state](../../../core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp) uses zero for `DB_STENCIL_CONTROL`, `DB_STENCILREFMASK` and `DB_STENCILREFMASK_BF` (context `0x10b`-`0x10d`), including after `CLEAR_STATE`. The public V0, V4, V5 and V7-V11 [register-default tables](../../../core/libs/prx/libSceAgc/Command/include/RegisterDefaultsData.hpp) supply these values. Linux's [GFX10 clear-state table](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/amdgpu/clearstate_gfx10.h) agrees on the control register but uses `0x01000000` for the reference masks; the console reset values have not been measured. Planet Coaster (PPSA01736 v01.017) enables stencil tests without writing the control register. +- [Type-0 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. +- 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, when a buffer upload, a `GuestBuffers` V#, an image lookup or a pending-store flush overlaps it; CPU reads and reads through address-based heaps are not caught. Guest writes are ordered when collected (bind, flush, collect epoch), not when made, at 64 KiB granularity: a collected write reloads the whole plane 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, 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). 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) 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. 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 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. 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. 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) - `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. +- [11_11_10 float color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R32_UINT attachments: the pixel shader packs each export toward zero like format-43 image stores (overflow to the largest finite value, negatives to zero, NaN kept), which is assumed to match the CB's conversion; only the reversed component order (COMP_SWAP 2) is accepted. +- [PA_SU_SC_MODE_CNTL.PROVOKING_VTX_LAST](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver) uses Vulkan's last-vertex mode for native primitives. Bit 19 follows [opengnm's `flatshade_first` mapping](https://github.com/blackbearreloaded/ps5-opengl/blob/db752da4c89380af228497b5b6852163a4e61069/src/gallium/ps5/ps5_screen.c#L2244); vertex selection follows [Vulkan](https://docs.vulkan.org/spec/latest/chapters/drawing.html#drawing-primitive-topologies), without console measurements. A [rect-list](../../../core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp) draw in last mode gives all four corners of each rectangle the flat inputs of its third vertex through the generated control stage, as first mode gives them those of its first, so Vulkan's choice of provoking vertex for the tessellated triangles does not matter; which vertex the console takes for a rectangle is not measured either. A geometry program's primitives keep the vertex indices of its own primitive export on the [mesh path](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp), so last mode takes their flat inputs (from [Vulkan](https://docs.vulkan.org/spec/latest/chapters/vertexpostproc.html#vertexpostproc-flatshading)) and their [layer](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) from the last index of each primitive, while their per-vertex inputs keep the order of first mode ([Vulkan](https://docs.vulkan.org/spec/latest/chapters/primsrast.html#primsrast-barycentric)); that the console does the same for an exported primitive is not measured. Last mode throws without `VK_EXT_provoking_vertex`, for tessellation primitives, for triangle strips or fans assembled for a geometry program (whose odd-triangle order on the console is not measured), explicit per-vertex flat inputs, and triangle strips whose pixel shader reads a vertex slot itself (`v_interp_mov_f32`, or 16-bit interpolation, which is not checked yet), since the vertex order of odd strip triangles [depends on the device](https://docs.vulkan.org/refpages/latest/refpages/source/VkPhysicalDeviceFragmentShaderBarycentricPropertiesKHR.html). `v_interp_p1_f32` and `v_interp_p2_f32` read P10 and P20 against the guest's I and J, which come from `gl_BaryCoordKHR` in the same vertex order as the per-vertex values, so the order cancels out and strips with only those reads draw. A FLAT_SHADE input without pass-through that is read through `v_interp_mov_f32` P10 and not P20 [returns the provoking vertex's value](../../../core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp), like P0. ps5-opengl reads every flat input that way, and its driver passes the GL 4.6 CTS on a PS5 (#1413). The lane itself was not measured on the console, so with the first provoking vertex such a shader throws. +- [Dual-source blending](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) takes the MRT1 export as the second source of the MRT0 blend, as RADV and radeonsi program it; what the CB does with other export or target layouts is unknown, so they throw. +- [Blending into reversed-order color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - 8_8_8_8 and 16_16_16_16 targets with `COMP_SWAP` 2 or 3 keep the exported alpha in their first channel and red or blue in their fourth, which Vulkan would blend with the color and the alpha equation respectively. The color block is taken to blend in shader export order and apply `COMP_SWAP` afterwards: the alpha equation and every source-alpha factor use the exported alpha, and the destination alpha is the first channel. This follows the `CB_TARGET_MASK` text of the AMD [SI](https://xorg.freedesktop.org/docs/AMD/old/SI_3D_registers.pdf) (p. 229) and [CIK](https://xorg.freedesktop.org/docs/AMD/old/CIK_3D_registers_v2.pdf) (p. 238) register references ("the order of components after blending and before COMP_SWAP is applied"). RADV builds `CB_BLENDn_CONTROL` without regard to the swap ([radv_pipeline_graphics.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/vulkan/radv_pipeline_graphics.c#L650-L709)) and renders R4G4B4A4, B4G4R4A4, R5G5B5A1 and B5G5R5A1 with `SWAP_STD_REV` and `SWAP_ALT_REV` ([vk_format.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/vulkan/util/vk_format.c#L43-L48), [ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c#L609-L624)). Mesa CI's Navi10 (gfx1010) CTS run ([job 112381553](https://gitlab.freedesktop.org/mesa/mesa/-/jobs/112381553), [112381554](https://gitlab.freedesktop.org/mesa/mesa/-/jobs/112381554)) passes all 400 `blend.format..states` cases of each of those formats, including separate alpha and destination-alpha factors. It is not measured on gfx1013 or on 8_8_8_8 and 16_16_16_16 targets. Factor pairs Vulkan applies per channel are used directly. Otherwise the pixel shader only copies values: it writes the exported alpha, each channel's own export or the constants 0 and 1 into a second color at location 0 index 1 (and 1 into a channel of the first color whose source factor is ONE, moving its export into the factor), and the blend forms every factor, `1 -` and product from those copies through `SRC1_COLOR`/`SRC1_ALPHA` factors. The blend unit therefore rounds them as it rounds a plain Vulkan blend of the same export; Vulkan does not require a second-source color to be converted like the first, but on an RTX 5080 and lavapipe every reversed-order case of the execution test is bit-identical to the same draw into a `SWAP_STD` target, which the test checks. Factor combinations that copies cannot express (for example `ONE_MINUS_SRC_ALPHA` in the alpha equation with `SRC_COLOR` in the color equation), sRGB, SNORM and integer targets, constant and second-source factors, destination-alpha and `SRC_ALPHA_SATURATE` factors in the color equation, different color and alpha operations, MIN or MAX with factors other than ONE, other color exports when the blend needs the second color, and devices without `dualSrcBlend` throw. Gran Turismo 7 (PPSA01317) blends `CB_BLEND0_CONTROL` 0x65010504 into such a target (#4782). +- [DB_SHADER_CONTROL.CONSERVATIVE_Z_EXPORT](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver) becomes `DepthLess`/`DepthGreater`. For a pixel shader that breaks the promise, the host driver decides whether its interpolated Z is depth-tested before shading; what the console does then was not measured. +- [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. 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 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. +- [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. +- [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 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. +- [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, a sample-mask export and a single-sample color target next to the multisampled depth target throw +- [DCC display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) are presented only when all keys of the console's DCC extent agree on uncompressed or a fast-clear code, padding keys included; other keys, and register-clear keys over a pending image, throw. +- [Storage image write-back](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) stores the GPU results over a CPU-stamped range whose guest bytes equal the copy taken when the image's layers got their generation (images up to 1 MiB whose guest bytes were settled at that point, with no unit shadow, pending image results or recorded GPU store over them; other images keep the previous rule: the CPU's bytes win). A CPU store that rewrites the baseline value after the GPU changed it is not told apart from no store, and the GPU result wins there. The 1 MiB limit is a choice, not a console value. 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. 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. +- [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; 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). 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) - 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 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. +- 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, and the other five slots stay zero. +- [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. +- [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. + +- [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. + +- [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. diff --git a/docs/dev/debt/libSceAjm.native.md b/docs/dev/debt/libSceAjm.native.md new file mode 100644 index 0000000000..f8982027f0 --- /dev/null +++ b/docs/dev/debt/libSceAjm.native.md @@ -0,0 +1,30 @@ +# libSceAjm.native technical debt + +### Unknown function info + +- [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. +- [sceAjmDecWVorbisCreateHeaderPacket, sceAjmDecWVorbisCreateSetupPacket](../../../core/libs/prx/libSceAjm.native/Export.cpp) (libSceAjmWVorbis, implemented in libSceAjm.native) - unknown signatures +- [sceAjmBatchJobGetInfo](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - writes the result and format sideband of a run job without buffers with the `SIDEBAND_FORMAT` flag (0x400000000000), as [KytyPS5](https://github.com/KytyPS5/KytyPS5) does, not confirmed on a PS5 title; the format reports bitrate 0 for ATRAC9 as in KytyPS5, while [shadPS4](https://github.com/shadps4-emu/shadPS4) derives it from the superframe size; before the instance is initialized it returns the not-initialized result (1) and no format, like the other run jobs here, while KytyPS5 returns result 0 with the format of the uninitialized decoder +- [sceAjmDecMp3ParseFrame](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - signature, result layout and rate tables taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title: the layer field is not checked and MPEG-2.5 stops at 64 kbps; the original file length (`parse_ofl`) layout (Xing/Info + LAME tag, VBRI, Fraunhofer `0xB4` block with its CRC, encoder delay = samples per frame + LAME delay + 529) also comes from shadPS4 only, and the Fraunhofer block has no other public description +- [sceAjmBatchJobGetCodecInfo, sceAjmBatchJobGetGaplessDecode](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `RUN_GET_CODEC_INFO` flag (0x800), the ATRAC9 codec info layout and its next frame size are taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [RPCSX](https://github.com/RPCSX/rpcsx) reimplementations, not confirmed on a PS5 title; a run job without buffers returns result 0, as in shadPS4. The gapless sideband reports the total and skip counts the title set and does not count them down: RPCSX does the same, shadPS4 counts them down and BryKytyPS5 returns its current counts +- [AJM Opus decoder](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 24) - the initialize parameter layout and little-endian `u16` packet byte count come from The Smurfs Dreams (`2, 48000, 0`) and Lost Soul Aside (`4, 48000, 1`). The third word is interpreted as the mapping family, as in [KytyPS5's initialize structure](https://github.com/KytyPS5/KytyPS5/blob/984cfcebd141b211aa9e0a8cde63b0ebe652d07a/src/libs/ajm/opus_decoder.h#L11-L15). Family 1 uses the conventional stream counts, coupled counts and channel map from [libopus 1.5.2's surround layout](https://github.com/xiph/opus/blob/v1.5.2/src/opus_multistream_encoder.c#L47-L55), passed to FFmpeg through an [RFC 7845 section 5.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1) header with zero pre-skip and gain; gapless skipping remains controlled by AJM sideband parameters. FFmpeg returns PCM in its native speaker order. The implicit stream layout, PCM channel order and invalid family-0 channel count result are not verified on a console. NHL 27 (PPSA34063) passes `1, 48000, 255`. Family 255 with one channel is decoded as one mono stream, the same as family 0; this is assumed, since the stream count, coupled stream count and channel mapping that a family-255 Opus header carries ([RFC 7845 section 5.1.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1.1)) are not passed. Only 48000 Hz, mapping families 0 and 1, and family 255 with one channel are implemented; other rates and families, and family 255 with more channels, throw +- [AJM 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`; with a zero change per sample it sets the ratio as `sceAjmBatchJobSetResampleParameters` does (Watch Dogs: Legion, PPSA01500, calls it so). How a non-zero change ramps the ratio is not confirmed on a console (no source: KytyPS5 ignores it, prosper stubs it, the PS5 firmware does not export the batch builders to a payload): it is assumed to be added to the ratio after each emitted output frame, clamped at zero, and kept across batches. RIDE 4 (PPSA01599) passes one call with a change of 4.6e-7, so the exact rule matters little there. +- [sceAjmBatchJobControl](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - RESET (bit 13), INITIALIZE (bit 14) and the sideband input order (gapless decode, then initialize parameters) come from [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4); other flags throw +- [sceAjmBatchJobControl](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - RESET is assumed to clear the context as sceAjmBatchJobClearContext does +- [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 + +- [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. + +- [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. +- [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 new file mode 100644 index 0000000000..764ea7ec20 --- /dev/null +++ b/docs/dev/debt/libSceAmpr.md @@ -0,0 +1,17 @@ +# libSceAmpr technical debt + +### 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_KERNEL_ERROR_EBUSY` or a shifted offset. The at-start flag of the `_04_00` writes is ignored; commands run in order, each after the previous one completes +- [APR map begin and end](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - as in ampr_emu, `MapBegin`/`MapDirectBegin` map when they run and `MapEnd` only closes the region; while a region is open a second begin and every completion write return `EPERM`, the at-start `_04_00` writes are accepted +- [AMPR markers](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - signatures taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, not confirmed on a title: `sceAmprCommandBufferSetMarkerWithColor` takes the color by pointer, the push and measure variants by value. Colors are not recorded +- [__cxa_call_unexpected](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - takes the in-flight exception's unwind header and never returns, as [libcxxabi 18.1.8](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_personality.cpp#L1171-L1211) does. No `std::set_unexpected` is exported and the exception header's unexpected handler is never set, so the handler is always the default one, which calls `std::terminate` ([cxa_default_handlers.cpp](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_default_handlers.cpp#L88-L99)); it therefore forwards to libc's `__cxa_call_terminate`, which begins the catch and runs the current terminate handler. That matches libcxxabi except for which terminate handler runs: there `std::terminate` after `__cxa_begin_catch` ([cxa_exception.cpp L470-L477](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_exception.cpp#L470-L477)) runs the handler saved in the caught exception's header ([cxa_handlers.cpp L72-L93](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_handlers.cpp#L72-L93)), which `__cxa_throw` and `__cxa_init_primary_exception` set to `std::get_terminate()` when the exception is thrown ([cxa_exception.cpp L209-L214](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_exception.cpp#L209-L214), [L279](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_exception.cpp#L279)), while here the current global handler runs, as AnyPS5's own `std::terminate` does. The two differ only if `std::set_terminate` was called between the throw and this call. A null header terminates because this emulator's libc `__cxa_begin_catch` terminates on null (libcxxabi does not define that case). Dynamic exception specifications that would let a replaced unexpected handler throw a permitted type are not modelled + +### Functional + +- [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. 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 new file mode 100644 index 0000000000..62bdd6066f --- /dev/null +++ b/docs/dev/debt/libSceAppContent.md @@ -0,0 +1,13 @@ +# libSceAppContent technical debt + +### 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 +- [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) - 17 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. `sceAppContentGetAddcontDownloadProgress` answers `PARAMETER` for a null label or progress and `NOT_FOUND` otherwise, as [dlc_emu](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.cpp#L2328-L2341) does for a label that is not installed additional content, and never writes the progress. Its 16-byte layout (`DataSize`, then `DownloadedSize`, both 64-bit) follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/AppContent/AppContent.cs#L45-L54) and is unused here. The signatures of `sceAppContentAddcontEnqueueDownload` and `sceAppContentGetAddcontDownloadProgress` follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/AppContent/AppContent.cs#L140-L148), and the signature of `sceAppContentAddcontDelete` follows [dlc_emu](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.h#L91-L92); dlc_emu is not a console measurement and no console source gives the error codes, so the signatures and the error codes are unverified +- [Temporary app-content storage](../../../core/libs/prx/libSceAppContent/Export.cpp) tracks one `/temp0` mount, preserves its files on unmount, and rejects new path resolutions until remount. Mount, format, space-query and unmount errors follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_path.pas); the pointer ABI is also declared by [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/AppContent/AppContent.cs). Existing open host descriptors remain usable after unmount; console behaviour with open files is unverified. Host filesystem failures still throw. diff --git a/docs/dev/debt/libSceAppInstUtil.md b/docs/dev/debt/libSceAppInstUtil.md new file mode 100644 index 0000000000..ecf5fb2a82 --- /dev/null +++ b/docs/dev/debt/libSceAppInstUtil.md @@ -0,0 +1,5 @@ +# libSceAppInstUtil technical debt + +### Unknown function info + +- [sceAppInstUtilAppIsInUpdating, sceAppInstUtilAppGetSize, sceAppInstUtilAppInstallPkg, sceAppInstUtilAppPrepareOverwritePkg, sceAppInstUtilAppRecoverApp, sceAppInstUtilAppUnInstallAddcont, sceAppInstUtilAppUnInstallPat, sceAppInstUtilAppUnInstallTheme, sceAppInstUtilAppUnInstallTypes, sceAppInstUtilGetInstallProgressInfo, sceAppInstUtilGetInstallProgress and sceAppInstUtilGetPrimaryAppSlot](../../../core/libs/prx/libSceAppInstUtil/Export.cpp) (libSceAppInstUtil) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAppInstUtil.c), but no credible prototype was found. They are declared without parameters only to resolve the known imports; this is not an ABI claim and callers must not call them until a prototype is verified. The other seven declarations follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/AppInstUtil/AppInstUtil.cs) 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 new file mode 100644 index 0000000000..c7d94441a4 --- /dev/null +++ b/docs/dev/debt/libSceAudio3d.md @@ -0,0 +1,32 @@ +# libSceAudio3d technical debt + +### Silent stubs + +- [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) (libSceAudio3d) - the supported list (late reverb level, downmix spread radius, downmix spread height aware) is inferred from the attributes `sceAudio3dPortSetAttribute` accepts; its order and the count written when the caller capacity is smaller are unverified. [shadPS4](https://github.com/shadps4-emu/shadPS4) returns object attributes (PCM, gain...) instead, prosper and KytyPS5 have no body +- [sceAudio3dObjectReserve](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - object ids, the `max_objects` limit and the checks of `sceAudio3dObjectReserve` and `sceAudio3dObjectUnreserve` follow [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4), the only reference with a body: ids count up from 1 per port, skipping 0, 0xFFFFFFFF and ids in use, and a failed reserve writes 0xFFFFFFFF +- [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 +- [sceAudio3dGetSpeakerArrayMemorySize](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dCreateSpeakerArray](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dDeleteSpeakerArray](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dGetSpeakerArrayMixCoefficients](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dGetSpeakerArrayMixCoefficients2](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [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 new file mode 100644 index 0000000000..04f555333b --- /dev/null +++ b/docs/dev/debt/libSceAudioIn.md @@ -0,0 +1,46 @@ +# libSceAudioIn technical debt + +### Silent stubs + +- [sceAudioInInit](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - returns 0 without library-wide setup, as `sceAudioOutInit` does; each `sceAudioInOpen` opens its own SDL capture device + +### 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 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 +- [sceAudioInOpenEx](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInHqOpenEx](../../../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 +- [sceAudioInSetNoiseGateThreshold](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [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, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/c86c8b4d9333a766cc8c4495f89f893bf35e6199/src/libs/audio.cpp) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Audio/AudioIn.cs) describe the higher-quality path: a longer grain returns the invalid-size error and a lower rate the invalid-frequency error. AsyncOpen also accepts the 384-sample grain, the largest [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Audio/AudioIn.cs) documents for the low-latency path, and returns the invalid-size error for every other size; the console's shorter average return interval of an async port's blocks is not modelled, so like a synchronous port `sceAudioInInput` paces one grain at a time. Param 0x10 opens a float mono port like 0x11, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/c86c8b4d9333a766cc8c4495f89f893bf35e6199/src/libs/audio.cpp) accepts both, while SharpProspero documents only 0x11. Host capture pacing and console behavior remain unverified. +- [sceAudioInGetGain, sceAudioInGetRerouteCount](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - signatures and semantics follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Audio/AudioIn.cs#L92-L110). `sceAudioInGetGain` writes the unattenuated linear gain `1.0f` into `*gain` before validating `handle`, returning `0` for an open port and `0x80260101` (`SCE_AUDIO_IN_ERROR_INVALID_HANDLE`) otherwise; a null `gain` pointer throws because the console result is unverified, and host or system input attenuation is not modelled. `sceAudioInGetRerouteCount` returns `0` for an open port and `0x80260101` otherwise, as each port opens its SDL capture device once and capture-device hot-swapping is not modelled. 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 new file mode 100644 index 0000000000..f6e6901f1f --- /dev/null +++ b/docs/dev/debt/libSceAudioOut.md @@ -0,0 +1,119 @@ +# libSceAudioOut technical debt + +### Unknown function info + +- [sceAudioOut2ContextAdvance and sceAudioOut2ContextPush](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) - with an SDL device, Advance snapshots each port's PCM and gain; Push consumes completed grains in order and a blocking Push waits for Advance when none is ready. Inferred from PPSA26358, which rewrites the same 512-frame, 12-channel float buffer on a producer thread and pushes on another thread. Console buffer ownership, snapshot timing and empty-queue errors are unverified. Completed grains remain queued until Push consumes them, so memory use grows with an unconsumed PCM backlog. The console backlog limit is unverified. A nonblocking Push without a completed grain throws; this is a runtime restriction, not a console measurement. Context destruction concurrent with active calls remains unsupported. Without an SDL device the existing wall-clock path remains. + +- [sceAudioOut2UserGetSupportedAttributes](../../../core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp) (libSceAudioOut) - the user handle and two uint32 output pointers follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp), not a console measurement. The masks describe this runtime: no context attributes and port attributes PCM (bit 0) and gain (bit 1). Null outputs and unknown or destroyed user handles throw; console validation order and error values are unknown. Users are tracked for this query, but port ownership and busy-user destruction are not implemented. The other PPSA23566 import, [sceConvertKeycodeGetCharacterFromKeyboardData](../../../core/libs/prx/libSceConvertKeycode/Export.cpp), remains declared without parameters, with an unknown signature. Names from the shadPS4 aerolib NID list +- [sceAudioOutExOpen, sceAudioOutExConfigureOutput, sceAudioOutExClose](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - the HDMI bitstream calls follow [PS5-Audio-Passthrough-Research](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/cf3c921d79bf00db4847f31b68a7bd95a8037cd7/docs/FINDINGS.md#L31-L80), which disassembles firmware 12.70's libSceAudioOut and logs the calls on a console. `sceAudioOutExOpen(user, mode)` returns `0x80260015` for a mode above 10 or outside mask `0x61F`; otherwise it opens a port of internal type 6 in S16 stereo, 256 frames at 48000 Hz for modes 0, 1, 2 and 9 and 1024 frames at 192000 Hz for modes 3, 4 and 10. `sceAudioOutExConfigureOutput(control, flags, mode, target, option)` returns `0x80268009` when `control` is not 0, takes modes 0 to 10 and 0xFF and targets 1 to 3 and 0xFF, and returned 0 on the console; `sceAudioOutExClose` returned 0. The argument registers match [Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L26-L28) and PPSA99051's [bitstream_out.cpp](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/cf3c921d79bf00db4847f31b68a7bd95a8037cd7/sample-app/src/platform/ps5/bitstream_out.cpp#L29-L32). Audio reaches the host through SDL as PCM and the host has no HDMI bitstream output, so an Ex port has no host device: `sceAudioOutOutput` paces it at its own rate and drops its IEC 61937 bursts, and `sceAudioOutExConfigureOutput` changes nothing. Nonzero `flags` or `option`, other modes or targets, `sceAudioOutExClose` of a handle that is not an Ex port, and `sceAudioOutGetPortState` on an Ex port throw, since the console's answers are unknown +- [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, output)`: [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp) reads `(user_id, output, latency_us)`, but the known calls set just edi and esi. The validation follows KytyPS5: an output above 2 returns `0x80268001` and every user id is accepted, where before only `(0xFF, 2)` (PPSA26344) and `(0xFF, 1)` (PPSA14632) were; the latency value of the three-argument reading is never received, and there is no 3D rendering stage to apply anything to +- [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 +- [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. The debug state ids are unknown, so every id is accepted, and a null param returns `0x80260502` like the other AudioOut2 calls. 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) - effect of a negative mix level unknown; only 0 to 32768 (0 dB) is accepted +- [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 +- [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 +- [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) - 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 +- [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 +- [sceAudioOut2LoTerminate](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) throws. It takes no arguments and returns an int, and stops the low-latency service ([SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs#L497-L498)), but what it does to open contexts and ports and what it returns when the service is not started are unknown, and its counterpart `sceAudioOut2Terminate` is unimplemented. +- [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/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [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/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [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. SetParam accepts the parameter set, and SetSystemDebugState records the debug state id, without anything to apply them to. +- [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) (libSceAudioOut) reports an HDMI sink without a monitor name or audio descriptors: it zeroes the 0x180 bytes and returns 0, since audio reaches the host through SDL as PCM and there is no EDID to describe. The arguments `(type, info, size)`, the required size 0x180 and the fields titles read (monitor name at +0x0B, 13 bytes; descriptor count as a 32-bit value at +0x94; 8-byte descriptors `[coding type, channels, rate mask, 0, extra, 0, 0, 0]` from +0x98) come from [PS5-Audio-Passthrough-Research](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/c3be9c2afcb8f7838784ead0fe1c12a2ceea965a/docs/FINDINGS.md) (firmware 12.70, one monitor). What a console without an HDMI audio sink returns, an error or no descriptors, is unknown. The other bytes, among them the header at +0x90 (`01 09 4f 00` on that monitor), have no known meaning and are written as 0. No LPCM descriptor is reported for the host device, as the rate mask and extra byte of one are not known. A null buffer returns `0x80260004` like the other AudioOut calls; the console's code is not measured. Monitor type 0x10, which the console also accepts, other types and sizes other than 0x180 throw: their results and error codes are unknown. diff --git a/docs/dev/debt/libSceAudioPropagation.md b/docs/dev/debt/libSceAudioPropagation.md new file mode 100644 index 0000000000..30802ce4c7 --- /dev/null +++ b/docs/dev/debt/libSceAudioPropagation.md @@ -0,0 +1,11 @@ +# libSceAudioPropagation technical debt + +### 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` preserves each destination mix because there are no path contributions, following the additive-output contract in [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioPropagation.cs); console behaviour has not been measured. It adds no 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. + +### 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 +- [libSceAudioPropagation firmware exports](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) (libSceAudioPropagation) - `sceAudioPropagationPathGetNumPoints`, `sceAudioPropagationReportApi`, `sceAudioPropagationResetAttributes`, `sceAudioPropagationSourceQueryInfo`, `sceAudioPropagationSystemLock`, `sceAudioPropagationSystemQueryInfo`, `sceAudioPropagationAudioPathInit`, `sceAudioPropagationAudioPathPointInit`, `sceAudioPropagationMaterialInit`, `sceAudioPropagationPortalSettingsInit`, `sceAudioPropagationRayInit`, `sceAudioPropagationSourceRenderInfoInit`, `sceAudioPropagationSourceSetAudioPathsParamInit`, `sceAudioPropagationSystem`, `sceAudioPropagationSystemMemoryInit`, `sceAudioPropagationSystemOptionInit`, `sceAudioPropagationReportRegisterHandler`, `sceAudioPropagationReportUnregisterHandler`, `sceAudioPropagationSystemEnumHandles`, `sceAudioPropagationSystemUnlock` are declared without parameters from the firmware export database (aerolib.csv); their guest signatures are unverified on hardware and every call throws diff --git a/docs/dev/debt/libSceAudiodec.md b/docs/dev/debt/libSceAudiodec.md new file mode 100644 index 0000000000..0e37f5e79f --- /dev/null +++ b/docs/dev/debt/libSceAudiodec.md @@ -0,0 +1,11 @@ +# libSceAudiodec technical debt + +### Unknown function info + +- [sceAudiodecDecode](../../../core/libs/prx/libSceAudiodec/src/Codecs.cpp) (libSceAudiodec) - a raw M4AAC stream carries no channel configuration, so the decoder tries the mono/stereo configuration and then configurations 3 to 7 within `ui_max_channels` until one decodes the first access unit, and keeps that one. No reference documents how the library picks the layout (shadPS4 maps the channel count straight to the configuration; SharpProspero only describes the maximum). With the stereo configuration a mono raw stream decodes as 2 channels, so 2 is reported, not 1 +- [sceAudiodecDecode](../../../core/libs/prx/libSceAudiodec/src/Codecs.cpp) (libSceAudiodec) - the HE-AAC handling follows one console measurement of an HE-AAC v2 ADTS stream: SBR output at 48 kHz in one channel with `ui_enable_heaac` 1, the 24 kHz mono core with 0. Not measured: whether the console, like FFmpeg, decodes a stream without SBR data in its first access unit as AAC-LC with `ui_enable_heaac` 1, and what it reports in `ui_sampling_freq` and `ui_heaac` with `ui_enable_heaac` 0 (here the rate of the returned PCM and 0) + +### Functional + +- [sceAudiodecDecode](../../../core/libs/prx/libSceAudiodec/src/Codecs.cpp) (libSceAudiodec) - an ADTS stream with channel configuration 0 (layout in a program config element) is still parsed by FFmpeg, so `ui_enable_heaac` 0 does not remove its SBR and a mono one with Parametric Stereo comes out as stereo. +- [libSceAudiodec](../../../core/libs/prx/libSceAudiodec/Export.cpp) throws for the 24-bit PCM word size (`iBwPcm` 0): its sample layout is unknown. ATRAC9 decoding is covered only by configuration and error tests, as no ATRAC9 encoder is available for a fixture. diff --git a/docs/dev/debt/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..b7200004e3 --- /dev/null +++ b/docs/dev/debt/libSceAutoMounterClient.md @@ -0,0 +1,5 @@ +# 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. diff --git a/docs/dev/debt/libSceAvPlayer.md b/docs/dev/debt/libSceAvPlayer.md new file mode 100644 index 0000000000..2874d437d3 --- /dev/null +++ b/docs/dev/debt/libSceAvPlayer.md @@ -0,0 +1,22 @@ +# libSceAvPlayer technical debt + +### Silent stubs + +- [libSceAvPlayer](../../../core/libs/prx/libSceAvPlayer/Export.cpp): `sceAvPlayerSetLogCallback` accepts a callback that is never called, as the player produces no log messages, and `sceAvPlayerSetAvailableBandwidth` has no effect, as it governs HLS sources, which `sceAvPlayerAddSource` does not implement. +- [sceAvPlayerSetTrickSpeed](../../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) with a negative speed runs the clock backwards but delivers no frames; when a forward speed is set again, playback resumes from the rewound time. + +### 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](../../../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/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..8d8e3ccbe7 --- /dev/null +++ b/docs/dev/debt/libSceBgft.md @@ -0,0 +1,5 @@ +# libSceBgft technical debt + +### Unknown function info + +- [sceBgftServiceDownloadPauseTaskAll, sceBgftServiceDownloadResumeTaskAll, sceBgftServiceDownloadStartTaskAll, sceBgftServiceDownloadStopTaskAll, sceBgftServiceDownloadFindActivePatchTask, sceBgftServiceDownloadFindActivePupTask, sceBgftServiceIntDownloadFindActiveGameAndGameAcTask, sceBgftServiceIntDownloadFindActiveTask, sceBgftServiceIntDownloadGetGameAndGameAcProgress, sceBgftServiceIntDownloadGetPatchProgress, sceBgftServiceIntDownloadGetPlayGoProgress, sceBgftServiceIntDownloadGetProgress, sceBgftServiceIntDownloadGetTaskInfo, sceBgftServiceIntDownloadPauseTask, sceBgftServiceIntDownloadRegisterTaskByStorageEx, sceBgftServiceIntDownloadResumeTask, sceBgftServiceIntDownloadStopTask, sceBgftServiceIntDownloadStartTask, sceBgftServiceIntDownloadUnregisterTask and sceBgftServiceIntInit](../../../core/libs/prx/libSceBgft/Export.cpp) (libSceBgft) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceBgft.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. The eight task-control declarations not listed here follow [SharpProspero's DownloadService bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Platform/DownloadService.cs) diff --git a/docs/dev/debt/libSceBluetoothHid.md b/docs/dev/debt/libSceBluetoothHid.md new file mode 100644 index 0000000000..b8ec7bf2ab --- /dev/null +++ b/docs/dev/debt/libSceBluetoothHid.md @@ -0,0 +1,10 @@ +# libSceBluetoothHid technical debt + +### 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 + +### 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 +- [sceBluetoothHidDebugGetVersion, sceBluetoothHidDisconnectDevice, sceBluetoothHidGetDeviceInfo, sceBluetoothHidGetFeatureReport, sceBluetoothHidGetReportDescriptor, sceBluetoothHidInterruptOutput, sceBluetoothHidParamInitialize, sceBluetoothHidSetFeatureReport, sceBluetoothHidThreadParamInitialize](../../../core/libs/prx/libSceBluetoothHid/Export.cpp) (libSceBluetoothHid) - throwing stubs declared without parameters from the firmware export database (aerolib.csv); their guest signatures are unverified on hardware diff --git a/docs/dev/debt/libSceCdlgG2P.md b/docs/dev/debt/libSceCdlgG2P.md new file mode 100644 index 0000000000..2549e5ceee --- /dev/null +++ b/docs/dev/debt/libSceCdlgG2P.md @@ -0,0 +1,5 @@ +# libSceCdlgG2P technical debt + +### 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 diff --git a/docs/dev/debt/libSceCdlgPlayerReview.md b/docs/dev/debt/libSceCdlgPlayerReview.md new file mode 100644 index 0000000000..98ff1e48e6 --- /dev/null +++ b/docs/dev/debt/libSceCdlgPlayerReview.md @@ -0,0 +1,10 @@ +# libSceCdlgPlayerReview technical debt + +### 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 +- [scePlayerReviewDialogClose](../../../core/libs/prx/libSceCdlgPlayerReview/Export.cpp) (libSceCdlgPlayerReview) - `scePlayerReviewDialogClose` moves an initialized dialog straight to `COMMON_DIALOG_STATUS_FINISHED` even when called before Open, as Signin, NetCtlAp and PlayerSelection do; MsgDialog.native, SaveDataDialog.native and ErrorDialog return an error there instead. Unverified against real hardware + +### Functional + +- [Player review dialog](../../../core/libs/prx/libSceCdlgPlayerReview/Export.cpp) (libSceCdlgPlayerReview) shows nothing: `scePlayerReviewDialogOpen` finishes at once and the result reports that the user canceled, as `sceWebBrowserDialogOpen` does diff --git a/docs/dev/debt/libSceCesCs.md b/docs/dev/debt/libSceCesCs.md new file mode 100644 index 0000000000..df97d9e096 --- /dev/null +++ b/docs/dev/debt/libSceCesCs.md @@ -0,0 +1,14 @@ +# libSceCesCs technical debt + +### Silent stubs + +- [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`) +- [libSceCesCs firmware 3.20 pending exports](../../../core/libs/prx/libSceCesCs/PendingExports.cpp) - 249 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`. diff --git a/docs/dev/debt/libSceCommonDialog.md b/docs/dev/debt/libSceCommonDialog.md new file mode 100644 index 0000000000..0e7ac021bf --- /dev/null +++ b/docs/dev/debt/libSceCommonDialog.md @@ -0,0 +1,5 @@ +# libSceCommonDialog technical debt + +### Silent stubs + +- [libSceCommonDialog](../../../core/libs/prx/libSceCommonDialog/Export.cpp) 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/libSceContentDelete.md b/docs/dev/debt/libSceContentDelete.md new file mode 100644 index 0000000000..a0dacf89d0 --- /dev/null +++ b/docs/dev/debt/libSceContentDelete.md @@ -0,0 +1,9 @@ +# libSceContentDelete technical debt + +### Functional + +- [sceContentDeleteById and sceContentDeleteByPath](../../../core/libs/prx/libSceContentDelete/Export.cpp) (libSceContentDelete) delete from a content library that is empty on the host, as nothing is exported to it, so both throw for every content id and path once the arguments and the initialization are checked: no reference gives the console's error for content that is not in the library ([SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Content/ContentDelete.cs#L25-L42) names only `0x809D5006`, no permission). The signatures, a 64-bit content id and a UTF-8 path, follow [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json#L541-L542) and SharpProspero, which agree. A null path and a call outside `sceContentDeleteInitialize` and `sceContentDeleteTerminate` throw, as the console's errors for them are unknown, and the order of the two checks is unverified. + +### Unknown function info + +- [sceContentDeleteByIdListForSf, sceContentDeleteGlobalMstat, sceContentDeleteMemoryInitialize, sceContentDeleteMemoryTerminate](../../../core/libs/prx/libSceContentDelete/Export.cpp) (libSceContentDelete) - exported symbols from decrypted PS5 firmware 1.00–3.20 libSceContentDelete.sprx; parameter lists and return values are unknown/unverified and no reference (KytyPS5, shadPS4, fpPS4, SharpProspero) documents their signatures. Declared as stubs that throw via `NotImplemented_nid_no_patch` diff --git a/docs/dev/debt/libSceContentExport.md b/docs/dev/debt/libSceContentExport.md new file mode 100644 index 0000000000..18355bf87b --- /dev/null +++ b/docs/dev/debt/libSceContentExport.md @@ -0,0 +1,10 @@ +# libSceContentExport technical debt + +### 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 + +### Unknown function info + +- PlayStation 5 firmware exports declared without parameters, signatures unknown: [sceContentExportAddToFavorites, sceContentExportDataProvideFunction, sceContentExportFree, sceContentExportFrom, sceContentExportFromFileForSf, sceContentExportFromFileWithContentIdList, sceContentExportFromFileWithTitleIdList, sceContentExportInit, sceContentExportMalloc, sceContentExportRemoveFromFavorites, sceContentExportRenameContent, sceContentExportValidateContents](../../../core/libs/prx/libSceContentExport/Export.cpp) (libSceContentExport). Names from the firmware NID database; unimplemented and throw `NotImplemented_nid_no_patch`. + diff --git a/docs/dev/debt/libSceContentSearch.md b/docs/dev/debt/libSceContentSearch.md new file mode 100644 index 0000000000..56c59f62ad --- /dev/null +++ b/docs/dev/debt/libSceContentSearch.md @@ -0,0 +1,9 @@ +# 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 diff --git a/docs/dev/debt/libSceConvertKeycode.md b/docs/dev/debt/libSceConvertKeycode.md new file mode 100644 index 0000000000..706e31a66e --- /dev/null +++ b/docs/dev/debt/libSceConvertKeycode.md @@ -0,0 +1,9 @@ +# libSceConvertKeycode technical debt + +### Unknown function info + +- [sceConvertKeycodeGetCharacter](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) (libSceConvertKeycode) - the signature `(uint16_t keycode, uint32_t, uint32_t keyboardType, char16_t* character)` is inferred from PPSA12361, which passes a 16-bit keycode, 0, the type from `sceConvertKeycodeGetImeKeyboardType` and a pointer it reads one UTF-16 unit from when the result is 0. The meaning of the second argument is unknown. PPSA02530 (#5726) and PPSA27836 pass `0x4000000` as a constant: PPSA27836 calls it for every keycode from 0 to 0x8F to build a table of key names, upper-cases `a` to `z` itself and skips keycodes that fail or return 0, so that bit is ignored and the unshifted character is returned as for 0. Any other bit throws, and so does a keyboard type other than 0. The returned character for HID usage `0x64` has not been measured on a console; `\` is an unverified US-layout guess based on the Windows VK mapping, which does not establish the console result. + +### Functional + +- [sceConvertKeycodeGetCharacter](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) (libSceConvertKeycode) takes the keycode as a USB HID keyboard usage and returns the unshifted character of the US layout for letters, digits, punctuation, space and the keypad. A keycode without a character returns 0 with the character 0; whether the console returns an error instead is not measured. diff --git a/docs/dev/debt/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/libSceDbgThreadSanitizer.md b/docs/dev/debt/libSceDbgThreadSanitizer.md new file mode 100644 index 0000000000..5dc8905e10 --- /dev/null +++ b/docs/dev/debt/libSceDbgThreadSanitizer.md @@ -0,0 +1,5 @@ +# libSceDbgThreadSanitizer technical debt + +### Unknown function info + +- [__tsan_acquire, __tsan_release](../../../core/libs/prx/libSceDbgThreadSanitizer/Export.cpp) (libSceDbgThreadSanitizer, NIDs `XyX1104ACcU` and `IkoI44e0OXc`) - declared as throwing stubs: AnyPS5 runs no race detector. The `void (void* addr)` signatures are those of LLVM compiler-rt's [tsan_interface.h](https://github.com/llvm/llvm-project/blob/87f0227cb60147a26a1eeb4fb06e3b505e9c7261/compiler-rt/include/sanitizer/tsan_interface.h#L24-L25), whose names hash to the imported NIDs; that the console library has the same signatures is assumed. PPSA23226's bundled `libSceJobManager_nosubmission.prx` imports both and calls them only after [sceKernelIsThreadSanitizerEnabled](../../../core/libs/prx/libkernel/System/src/Sanitizer.cpp) reports the sanitizer enabled, which it never does here diff --git a/docs/dev/debt/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/libSceDiscId.md b/docs/dev/debt/libSceDiscId.md new file mode 100644 index 0000000000..fb26a7a344 --- /dev/null +++ b/docs/dev/debt/libSceDiscId.md @@ -0,0 +1,5 @@ +# libSceDiscId technical debt + +### Unknown function info + +- [sceDiscIdGet and sceDiscIdGet2](../../../core/libs/prx/libSceDiscId/Export.cpp) (libSceDiscId) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceDiscId.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/libSceDiscMap.md b/docs/dev/debt/libSceDiscMap.md new file mode 100644 index 0000000000..4fcd7700ec --- /dev/null +++ b/docs/dev/debt/libSceDiscMap.md @@ -0,0 +1,10 @@ +# libSceDiscMap technical debt + +### Silent stubs + +- [libSceDiscMap](../../../core/libs/prx/libSceDiscMap/Export.cpp) implements only the state with no disc bitmap: `sceDiscMapIsRequestOnHDD` and `ioKMruft1ek` return `SCE_DISC_MAP_ERROR_NO_BITMAP_INFO` (0x81100004) for every request and write nothing. CUSA02636's bundled `libSceFios2.prx` imports both; what it does with the error is not known (shadPS4 returns the same, and [shadps4-game-compatibility #617](https://github.com/shadps4-compatibility/shadps4-game-compatibility/issues/617) lists CUSA02636 as in-game) + +### 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 +- [sceDiscMapIsRequestOnHDD, sceDiscMapUnknown_ioKMruft1ek](../../../core/libs/prx/libSceDiscMap/Export.cpp) (libSceDiscMap, NIDs `lbQKqsERhtE` and `ioKMruft1ek`) - `ioKMruft1ek` has no known name. The `(path, offset, size, result)` and `(path, offset, size, flags, out1, out2)` signatures follow [shadPS4 disc_map.cpp](https://github.com/shadps4-emu/shadPS4/blob/b2d7ec7d31f9ad9426db93afe079db8679b3e1e0/src/core/libraries/disc_map/disc_map.cpp); the widths of the `ioKMruft1ek` outputs are unknown (shadPS4 `int*`, [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/src/ps4_libscediscmap.pas) `PInt64`), so they are `void*`. The error code is the one both return; [shadPS4 #2686](https://github.com/shadps4-emu/shadPS4/pull/2686) reads the console library as checking an initialisation global first in each call and returning it while the global is clear. Not measured on a console diff --git a/docs/dev/debt/libSceErrorDialog.md b/docs/dev/debt/libSceErrorDialog.md new file mode 100644 index 0000000000..1166c74424 --- /dev/null +++ b/docs/dev/debt/libSceErrorDialog.md @@ -0,0 +1,9 @@ +# libSceErrorDialog technical debt + +### 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/libSceFiber.md b/docs/dev/debt/libSceFiber.md new file mode 100644 index 0000000000..9ad010851f --- /dev/null +++ b/docs/dev/debt/libSceFiber.md @@ -0,0 +1,12 @@ +# libSceFiber technical debt + +### Functional + +- [sceFiberGetInfo](../../../core/libs/prx/libSceFiber/Export.cpp) reports the requested-size window at the top of the host stack used for execution, rather than the original caller buffer, which is retained and pinned. The additional 1 MiB host-call margin below that window is not included in `size_context`. Planet Coaster (PPSA01736 v01.017) uses this range for ChakraCore garbage-collector stack scans; reporting the unused caller buffer makes the scan cross unmapped memory. The range is covered by a regression test while running, switching and migrating a fiber between threads. Console behavior for substituted host stacks has no direct equivalent and is not measured. + +### Unknown function info + +- [_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/libSceFont.md b/docs/dev/debt/libSceFont.md new file mode 100644 index 0000000000..5c3d5ad062 --- /dev/null +++ b/docs/dev/debt/libSceFont.md @@ -0,0 +1,24 @@ +# libSceFont technical debt + +### 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 +- [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 +- [sceFontWritingRefersRenderStepCharacter](../../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - the second argument is an out pointer to the writing's letter step, as in [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1882-L1896) and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings. Measured on PS4 firmware 11.00 `libSceFont.sprx` (not on a PS5): exactly 8 bytes are written, the address of a step stored inside the writing, and a null writing writes null; shadPS4 and prosper treat the argument as a struct, which the measurement contradicts. The letter step lives in the writing, so the pointer stays valid as long as the writing does, and its contents are rebuilt by each call of this function. On the PS4 library repeated calls on one step walk the characters it covers and then return null; here every call returns the step's one character, so the second call differs. Assumed: each step covers one character and one glyph. PPSA06699 (Hot Shots Golf 2) dereferences the returned pointer for its character counts. +- [sceFontWritingLineGetOrderingSpace](../../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - the four outputs are written in the order the PS4 11.00 `libSceFont.sprx` writes them (line advance, leading space, space between spaced runs, trailing space); the parameter names in the [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) IDA type database and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings order them differently (head, inline, tail, advance), and the PS5 library was not measured. PPSA01325's only call uses them in the PS4 order: it computes first - second - fourth - 0.25 * third and compares that, divided by a scale, with its width limit. The PS4 library writes the trailing space through its pointer without a null check whenever the leading-space pointer is set; that call throws here +- [sceFontCharacterGetSyllableStringState](../../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - the signature `(textCharacter, int* syllableStringState)` follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs). Measured on PS4 firmware 11.00 `libSceFont.sprx` (not on a PS5): the state is 0 for Latin, CJK, Hangul, Arabic, Hebrew, Devanagari and the other scripts tried, and the library sets 1, 3 or 4 (never 2 on real text) for Thai while it creates the string; a null character or a null result returns `SCE_FONT_ERROR_INVALID_PARAMETER`, and a null character writes 0 to a non-null result. This returns 0 for every character outside the Thai block (U+0E00 to U+0E7F), as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1805-L1815) does, and throws for Thai characters, whose cluster states are not modelled. SharpProspero's `SceFontSyllableString` names 0 NonDetection, 1 Starting, 2 Following, 3 Depending and 4 Connecting. PPSA06699 (Hot Shots Golf 2) calls it. +- [sceFontCharactersRefersTextCodes, sceFontTextCodesStepNext and sceFontTextCodesStepBack](../../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - the signatures follow the [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json#L679) IDA type database and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Font/SceFont.Engine.cs#L395-L406), and the 64-byte `FontTextCodes` with the order pointer at 0 and the code at 8 follows SharpProspero's [SceFontTextCodes](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Font/SceFontStructs.cs#L216-L228). The traversal follows the PS4 11.00 `libSceFont.sprx` disassembly posted in [#4050](https://github.com/boykopovar/AnyPS5/pull/4050#issuecomment-6114504202), not a PS5 measurement: the cursor keeps the current, start and term characters at +0x28, +0x30 and +0x38; `Refers` never compares the character with the term and returns null for a clustered or synthetic character; `StepNext` returns null at the term after visiting it; `StepBack` returns null when the current character is the start; both steps skip synthetic characters. Assumed and not covered by the report, which says `StepNext` returns null only at the term: `StepNext` also returns null at the end of the chain (for example with a null term), and `StepBack` at the head of the chain. A null cursor or output returns null as in [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1696-L1740); a null text character throws, as its console result is unknown + +- [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) - 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. diff --git a/docs/dev/debt/libSceFontFt.md b/docs/dev/debt/libSceFontFt.md new file mode 100644 index 0000000000..cd35391b21 --- /dev/null +++ b/docs/dev/debt/libSceFontFt.md @@ -0,0 +1,5 @@ +# libSceFontFt technical debt + +### Unknown function info + +- [sceFontFtSupportBdf, sceFontFtSupportCid, sceFontFtSupportOpenType, sceFontFtSupportOpenTypeOtf, sceFontFtSupportOpenTypeTtf, sceFontFtSupportPcf, sceFontFtSupportPfr, sceFontFtSupportTrueType, sceFontFtSupportType1, sceFontFtSupportType42 and sceFontFtSupportWinFonts](../../../core/libs/prx/libSceFontFt/Export.cpp) (libSceFontFt) are declared without parameters, signatures unknown; [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/7da7a8e1f98eca9d8f01f84d20681f8faa0bd8fd/src/core/libraries/font/fontft.cpp), another emulator, declares them the same way and only logs. They are assumed to enable one font format in the FontFt library, which here always registers FreeType's default modules, so each call returns 0 when the host FreeType has the module (`bdf`, `t1cid`, `truetype`, `cff`, `pcf`, `pfr`, `type1`, `type42`, `winfonts`; OpenType needs `cff` and `truetype`) and throws when it does not. The mapping of Sony's format names to FreeType modules, the return value and the error codes of the console are unverified, and no title is known to call them. diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md new file mode 100644 index 0000000000..fa594755cb --- /dev/null +++ b/docs/dev/debt/libSceFreeType.md @@ -0,0 +1,11 @@ +# libSceFreeType technical debt + +### Unknown function info + +- [FT_Select_Charmap, FT_Set_Charmap, FT_Get_First_Char and FT_Get_Next_Char](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console signatures and errors are unverified; the LP64 guest adapter follows the bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Character iteration uses the low 32 bits of the input code, as that version does internally, and updates the guest's selected charmap pointer without refreshing unrelated glyph or outline data. Guest charmap fields are treated as read-only copies of the native records. + +- [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 + +### Functional + +- [libSceFreeType](../../../core/libs/prx/libSceFreeType/src/GuestObjects.cpp) (libSceFreeType) - the guest reads copies of the bundled FreeType's face, size and glyph slot, updated after each call that changes them. Their private fields (`driver`, `memory`, `stream`, `sizes_list`, `extensions`) are null, and `subglyphs` is null whatever `num_subglyphs` reports. The outline points are copied as 64-bit coordinates; changes the guest makes to them and to the outline flags are written back before `FT_Render_Glyph`, while replacing the outline arrays, or a coordinate the host `FT_Pos` cannot hold, throws. `FT_New_Face` resolves the guest path and reads the whole file into memory; on Windows, where FreeType's `long` is 32 bits, a font of 2 GiB or more throws. Libraries and faces are never freed, since `FT_Done_Face` and `FT_Done_FreeType` are not exported. The bundled FreeType's own functions are kept out of the exports with `--exclude-libs` on Windows and Linux only; on macOS they stay exported, so a guest import of another FreeType function could resolve to them instead of failing at start-up diff --git a/docs/dev/debt/libSceGameLiveStreaming.md b/docs/dev/debt/libSceGameLiveStreaming.md new file mode 100644 index 0000000000..24dfaa238f --- /dev/null +++ b/docs/dev/debt/libSceGameLiveStreaming.md @@ -0,0 +1,6 @@ +# libSceGameLiveStreaming technical debt + +### Unknown function info + +- [sceGameLiveStreamingEnableLiveStreaming](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - imported by PS4 titles, which do not load without it, and added without an implementation: declared without parameters, every call throws +- [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 diff --git a/docs/dev/debt/libSceGameUpdate.md b/docs/dev/debt/libSceGameUpdate.md new file mode 100644 index 0000000000..c67a4a14a3 --- /dev/null +++ b/docs/dev/debt/libSceGameUpdate.md @@ -0,0 +1,6 @@ +# libSceGameUpdate technical debt + +### Unknown function info + +- [sceGameUpdateGetAddcontLatestVersion](../../../core/libs/prx/libSceGameUpdate/Export.cpp) (libSceGameUpdate) - a null `entitlement_label` returns `0x80412803` as in prosper; KytyPS5 accepts it instead, and neither is confirmed on a console. Bytes after `found` are zeroed as in KytyPS5; prosper leaves them untouched +- [sceGameUpdateCreateRequest, sceGameUpdateCheck, sceGameUpdateAbortRequest, sceGameUpdateDeleteRequest](../../../core/libs/prx/libSceGameUpdate/Export.cpp) (libSceGameUpdate) - an initialized `sceGameUpdateCreateRequest` returns `0x80D50001` and creates no request, so `sceGameUpdateCheck`, `sceGameUpdateAbortRequest` and `sceGameUpdateDeleteRequest` answer `0x80412805` for every id that passes their argument checks. The PS5 3.20 module, as read by prosper, keeps 8 requests with ids from `0x20000000`, and answers "no update" when its patch-check service reports `0x80412883`; what that service returns on an offline console is unknown. Closes when: measured on a PS5 diff --git a/docs/dev/debt/libSceGnmDriverForNeoMode.md b/docs/dev/debt/libSceGnmDriverForNeoMode.md new file mode 100644 index 0000000000..726f106af0 --- /dev/null +++ b/docs/dev/debt/libSceGnmDriverForNeoMode.md @@ -0,0 +1,7 @@ +# libSceGnmDriverForNeoMode technical debt + +### Unknown function info + +- [libSceGnmDriverForNeoMode pending exports](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) - 118 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`. + +- [libSceGnmDriverForNeoMode pending exports](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) - 45 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/libSceGnmDriverForNeoMode.c). diff --git a/docs/dev/debt/libSceHmd.md b/docs/dev/debt/libSceHmd.md new file mode 100644 index 0000000000..54a6bf362d --- /dev/null +++ b/docs/dev/debt/libSceHmd.md @@ -0,0 +1,167 @@ +# libSceHmd technical debt + +### 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 + +- [sceHmdInternalGetPUVersion](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalEventInitialize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetVirtualDisplaySize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSocialScreenSetOutput](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetVirtualDisplayDepth](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetEyeStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalAnotherScreenOpen](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionUnsetCallback](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetCrashReportCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetDebugTextMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdDistortionGetWorkMemoryAlign](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalRegisterEventCallback](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdDistortionGetWorkMemorySize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionStartWideNear](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetPUSerialNumber](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetDebugSocialScreenMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetM2LedBrightness](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetIPD](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalMirroringModeSetAspect](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalDfuGetStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalAnotherScreenSendVideo](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetRequiredPUPVersion](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionSetCallback](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalDfuCheckAfterPvt](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalResetInertialSensor](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionSetUserEventStart](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdDistortionSetOutputMinColor](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetVirtualDisplayDepth](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdGetDistortionWorkMemoryAlign](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionStartMultilayer](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalPushVr2dData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionClearUserEventEnd](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetIPD](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetVRMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalCrashReportReadDataSize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetVr2dData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalAnotherScreenSendAudio](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetPuPositionParam](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionInitializeCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdGet2DEyeOffset](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetDebugSocialScreenMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetHmuPowerStatusForDebug](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdGetDistortionWorkMemorySize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionSetDisplayBuffers](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalAnotherScreenSetFadeAndSwitch](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalCreateSharedMemory](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalMirroringModeSetAspectDebug](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetPuBuildNumber](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdDistortionGetCompoundEyeCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetS3dPassMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionStopCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalIsCommonDlgMiniAppVr2d](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalMmapGetSensorCalibrationData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdDistortionGetCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionQueryOnionBuffAlign](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetDebugMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalIsMiniAppVr2d](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalIsCommonDlgVr2d](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionSetOutputMinColor](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetPortConnection](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionDebugGetLastInfo](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetM2LedOn](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdGetFieldOfView](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionAddDisplayBuffer](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetIpdSettingEnableForSystemService](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetLedOn](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternal3dAudioSendData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionInitialize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetDeviceInformationByHandle](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionStopLiveCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalAnotherScreenClose](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalCrashReportReadData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetDeviceInformation](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionStartCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetDefaultLedData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalDfuClose](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionQueryGarlicBuffAlign](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetHmuPowerControl](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalBindDeviceWithUserId](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetHmuSerialNumber](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdDistortionGetWideNearCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalCheckS3dPassModeAvailable](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalAnotherScreenGetAudioStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSeparateModeGetAudioStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionStartLiveCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalIsGameVr2d](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdGetAssyError](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionFinalize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSocialScreenSetFadeAndSwitch](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalDfuStart](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetVirtualDisplayHeight](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdDistortionInitialize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalDfuSetMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetForcedCrash](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalDfuCheckPartialUpdateAvailable](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSeparateModeClose](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetDebugTextMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalCrashReportClose](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetHmuPowerControlForDebug](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetVirtualDisplayHeight](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionStart](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalResetLedForVsh](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalDfuOpen](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetDebugMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalCrashReportCancel](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSeparateModeGetVideoStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalCheckDeviceModelMk3](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdSetVr2dEnable](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetSidetone](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSeparateModeOpen](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalMmapGetModeId](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdDistortionGet2dVrCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalDfuSendSize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdGetWideNearDistortionCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionStartMultilayer2](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdGetDistortionCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetDeviceConnection](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetVirtualDisplaySize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionUnsetDisplayBuffers](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetHmuOpticalParam](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSeparateModeSendVideo](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalResetLedForVrTracker](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionQueryOnionBuffSize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionStartWithOverlay](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionSetUserEventEnd](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalCrashReportOpen](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetDemoMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionStartWideNearWithOverlay](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdGetDistortionParams](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalDfuSend](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionClearUserEventStart](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetUserType](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetDemoMode](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalMapSharedMemory](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetCrashDumpInfo](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetTv4kCapability](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalDfuReset](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionStart2dVr](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalMmapGetCount](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetDefaultLedData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdGetInertialSensorData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternal3dAudioClose](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetBrightness](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSocialScreenGetFadeState](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSeparateModeSendAudio](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetStatusReport](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetBrightness](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionFinalizeCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetPortStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionStop](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalAnotherScreenGetVideoStatus](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternal3dAudioOpen](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalAnotherScreenGetFadeState](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalSetDebugGpo](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetIpdSettingEnableForSystemService](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionDebugGetLastInfoMultilayer](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalGetPuRevision](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionQueryGarlicBuffSize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdGet2dVrCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature diff --git a/docs/dev/debt/libSceHmd2.md b/docs/dev/debt/libSceHmd2.md new file mode 100644 index 0000000000..1dacc21ad2 --- /dev/null +++ b/docs/dev/debt/libSceHmd2.md @@ -0,0 +1,85 @@ +# libSceHmd2 technical debt + +### Silent stubs + +- [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) has no PS VR2 runtime: `sceHmd2Initialize` returns libSceHmd's `UNSUPPORTED_FEATURE` (0x81110016) for any param, so the other calls answer as before initialization and `sceHmd2Open` returns `NotInitialized` (0x8a720002). These results are restored from the behaviour before #7323 and are not measured on hardware: firmware 13.20's libSceHmd2, with the `/dev/hmd2_cmd` state measured on a retail PS5 with no headset connected (connected flag 0, status 2, user -1), initializes, returns a handle from `sceHmd2Open` and reports `NotDetected`, which the code past the initialization check models but no title reaches. PPSA01317 passes a pointer to 16 zeroed bytes, only tests the result for non-zero and then skips its VR set-up; with initialization succeeding it stopped on `sceHmd2ReprojectionQueryBufferSizeAlign` (#6350). That export, `sceHmd2ReprojectionQueryDisplayBufferSizeAlign` and `sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign` stay unimplemented and throw pending console measurements of their sizes and alignments. The other reprojection and gaze exports throw, and [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) never initializes + +### Unknown function info + +- PPSA09030 imports from its `Media/Plugins/psvr2.prx` declared without parameters, signatures unknown, added without an implementation because the title fails to load the plugin without them: [sceHmd2ReprojectionGetStatus](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) +- [sceHmd2Open](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the handle is the value the console's driver returned to the first open; titles built with an SDK before 7.00 may open for any user, and the firmware's `sceMbusAddHandleByUserId` result for a user other than the initial one is unknown, so that case throws. 0x8a720006 for a second open is named `AlreadyOpened` from its use +- [sceHmd2SetVibration](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the second parameter is checked against 25 only while a headset is connected; its name and unit are unknown. The field of view member names are assumed: of the firmware's default tangents the first is the widest and the last two are equal +- PPSA01317 imports declared without parameters, signatures unknown: the [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) exports that throw and the remaining [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) exports. Names from the aerolib NID list +- PPSA15878 imports declared without parameters, signatures unknown: the [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) reprojection and gaze exports, `sceVrTracker2Finalize`, `sceVrTracker2GetPlayAreaOrientedBoundingBox` and `sceVrTracker2LocateCoordinateSystem` in [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp), and the `scePadVrController` trigger effect exports in [libScePad](../../../core/libs/prx/libScePad/Export.cpp). Names from the aerolib NID list +- [libSceHmd2 gaze and reprojection exports](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - `sceHmd2ReprojectionSeeThroughDraw`, `sceHmd2ReprojectionSeeThroughIC`, `sceHmd2ReprojectionVideoOutEq`, `sceHmd2ReprojectionWaitShader`, `sceHmd2SeeThroughCallbackGetPoseDataFunc`, `sceHmd2ReprojectionClear`, `sceHmd2ReprojectionSet`, `sceHmd2ReprojectionSetEventGpuEnd`, `sceHmd2ReprojectionSetEventGpuStart`, `sceHmd2RpeorjectionSetParam` are declared without parameters from the firmware export database (aerolib.csv); their guest signatures are unverified on hardware and every call throws +- [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 + +- [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 +- [sceHmd2ReprojectionQueryDisplayBufferSizeAlign, sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign, sceHmd2ReprojectionGetMirroringDisplayBufferSizeAlign and sceHmd2ReprojectionGetMirroringWorkMemorySizeAlign](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - each returns a `{uint64 size, uint64 alignment}` pair in RAX:RDX; the values were measured on a retail PS5 (firmware 13.60) with no PS VR2 headset from a homebrew payload that loaded `libSceHmd2.sprx` without initializing it, and stayed the same for every argument tried; `sceHmd2ReprojectionQueryBufferSizeAlign` measured there returns 0x100828 and 0x100 for every argument, including the 2, 0xAF and 1 that PPSA05666 passes. They are assumed not to depend on initialization or on a connected headset, and their parameters are unknown +- [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 +- [libSceHmd2 pre-initialization sweep](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the names in the entries above are the aerolib names of the NIDs where one exists; the error names `Error0004`, `Error002C`, `Error1002` and `Error1003` are placeholders for codes whose meaning is unknown, and `ReprojectionNotInitialized` is ours because every export answering `0x8a72000b` is a reprojection one. Exports that answered differently to null and to zeroed arguments, wrote output, returned a pointer-like value, answered `0x8a72000a` (which `sceHmd2Initialize` also returns for some applications) or crashed the probe are left as they were +- [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 +- [libSceHmd2 null-argument sweep](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - `Error1010` and `Error800d0203` are placeholder names. Left as they were: the exports that write output (`omimgG5itSs`, `FBUEhH9tQ1A`, `6PeHcAaKYg8`, `yur+xPdWPAg`, `RnieDDp5ge4`, `jM3NEJA11yw`, `b0H-n-tUk0U`) and those whose answer with a single null argument is neither `0x8a720008` nor their all-present answer, which take a handle or slot number rather than a pointer first (`MXhloJ6Iw3w`, `19wrIIiqFgM`, `Z389vAm47Ys`, `cDcrvEG8j8k`, `tuqJO6O6cb4`, `fd4fu1cziaI`, `UxMg7iL4nRs`, `K1GELwlaaGc`, `RFn39VCHjGY`, `FRoSjceDv10`, `wiv8+7ogrq0`, `wQ44gBwBw44`) +- [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 diff --git a/docs/dev/debt/libSceHttp.md b/docs/dev/debt/libSceHttp.md new file mode 100644 index 0000000000..bc45f2c73b --- /dev/null +++ b/docs/dev/debt/libSceHttp.md @@ -0,0 +1,79 @@ +# libSceHttp technical debt + +### 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 + +### 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. +- [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); 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) - 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); decoding and error codes follow a payload process on a PS5 (firmware 13.60). A buffer one byte short of the decoded size got the decoded bytes without a terminator and left `require` alone; a shorter buffer is assumed to get the first `prepare` bytes the same way +- [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) - read from the disassembly of libSceHttp.sprx on firmware 13.60, not measured on a console and not compared with other emulators. The arguments are (out, src, pool, prepare, require), `prepare` before `require`, the reverse of `sceHttpUriParse`. A null source returns `ERROR_INVALID_URL`; without an output or a pool, a null `require` returns `ERROR_INVALID_VALUE`, otherwise the size is only reported. Only the scheme, hostname, path, username and password are copied, in that order, each with its terminator (a missing one becomes an empty string and takes one byte), and the port; the query, the fragment and the opaque flag stay 0. A pool that is too small clears the output and the pool and returns `ERROR_OUT_OF_MEMORY` (0x80431022, the console's out-of-size error) +- [sceHttpAuthCacheImport](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpAuthCacheExport](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpDbgEnableProfile](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpDbgSetPrintf](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpDbgShowStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpGetMemoryPoolStats](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpDbgShowConnectionStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpDbgShowRequestStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpDbgShowMemoryPoolStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpDbgGetConnectionStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpDbgGetRequestStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpAbortRequestForce](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpAddRequestHeaderRaw](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpRemoveRequestHeader](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpGetAuthEnabled](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpGetAutoRedirect](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpTrySetNonblock](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpGetNonblock](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpTryGetNonblock](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpSetEpollId](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpGetEpollId](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpGetEpoll](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpGetCookie](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpGetCookieEnabled](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpAddCookie](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpSetCookieSendCallback](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpCookieExport](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpCookieImport](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpSetCookieTotalMaxSize](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpSetCookieMaxSize](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpSetCookieMaxNum](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpSetCookieMaxNumPerDomain](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpGetCookieStats](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpsEnableOptionPrivate](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpsDisableOptionPrivate](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpSetPriorityOption](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpSetPolicyOption](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpRequestGetAllHeaders](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpSetAcceptEncodingGZIPEnabled](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpGetAcceptEncodingGZIPEnabled](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpSetDefaultAcceptEncodingGZIPEnabled](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpSetProxy](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpSetSocketCreationCallback](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpSetHttp09Enabled](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpGetRegisteredCtxIds](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpGetConnectionStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpAddQuery](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpSetDelayBuildRequestEnabled](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpCacheRedirectedConnectionEnabled](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature + +### Functional + +- [sceHttpUriParse, sceHttpUriBuild, sceHttpUriMerge](../../../core/libs/prx/libSceHttp/src/Uri.cpp) - console behavior for non-ASCII scheme and hostname bytes remains unmeasured. ASCII character classification and scheme case folding follow [RFC 3986 sections 3.1 and 3.2](https://www.rfc-editor.org/rfc/rfc3986#section-3.1), independently of the host locale; parsing retains the existing C-locale rejection of those bytes. + +- [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 new file mode 100644 index 0000000000..2fa52cfbc5 --- /dev/null +++ b/docs/dev/debt/libSceHttp2.md @@ -0,0 +1,20 @@ +# libSceHttp2 technical debt + +### 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) + +### 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. +- [sceHttp2WebSocketSetPingTimeout](../../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2, NID `-MU9L6S3Fj4`) - imported by PPSA27836 01.000.001, whose loader stopped on the missing export; not called during its start-up. The parameters 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 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 diff --git a/docs/dev/debt/libSceHttpCache.md b/docs/dev/debt/libSceHttpCache.md new file mode 100644 index 0000000000..9ee1af9ece --- /dev/null +++ b/docs/dev/debt/libSceHttpCache.md @@ -0,0 +1,7 @@ +# libSceHttpCache technical debt + +### Unknown function info + +- [sceHttpCacheClear, sceHttpCacheClearAll, sceHttpCacheCompleteRequest, sceHttpCacheCreateRequest, sceHttpCacheCreateRequestWithTag, sceHttpCacheDeleteRequest, sceHttpCacheInit, sceHttpCacheReadData, sceHttpCacheRetrieve, sceHttpCacheRetrieveWithMemoryPool, sceHttpCacheRevalidate, sceHttpCacheRevalidateWithMemoryPool, sceHttpCacheSetCacheSharing, sceHttpCacheSetData, sceHttpCacheSetQuota, sceHttpCacheSetResponseHeader, sceHttpCacheSystemClearAll, sceHttpCacheSystemInit, sceHttpCacheSystemSendStatistics, sceHttpCacheSystemShutdown, sceHttpCacheSystemTerm and sceHttpCacheTerm](../../../core/libs/prx/libSceHttpCache/Export.cpp) (libSceHttpCache) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceHttpCache.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 + +- 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/libSceIme.md b/docs/dev/debt/libSceIme.md new file mode 100644 index 0000000000..465e86b085 --- /dev/null +++ b/docs/dev/debt/libSceIme.md @@ -0,0 +1,64 @@ +# libSceIme technical debt + +### Silent stubs + +- [sceImeOpen](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) shows no panel: the text is typed on the host keyboard, which [ImeTextInput](../../../core/libs/prx/libSceVideoOut/src/ImeTextInput.cpp) forwards from the game window (SDL text input, Backspace, Delete, Left, Right, Home, End, Enter and Escape), and the controller cannot type. While a panel is open, key presses do not reach the keyboard-to-pad bindings of [PadInput](../../../core/libs/prx/libSceVideoOut/src/PadInput.cpp), as the console's panel takes the controller; key releases still do. Minecraft Preview (PPSA19639, #1277) opens it when entering a world. The external keyboard filter is accepted and never called because no keycode events are produced + +### Unknown function info + +- [sceImeSetConsecutiveClauseConversionEnabled](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - name from its NID (`row9sfnYNx4`) in [sce_symbols](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv); neither [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0ebea15c24253b60f514fc37a348dabc8d0bcb90/src/core/libraries/ime/ime.cpp) nor [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/e5f0ee7fc2fb4ce88c6abb2fa02296404e2fe2a6/src/libs/ime.cpp) implements it, so it throws. PPSA24191 calls it with 1 as its only argument before each `sceImeOpen` and opens the panel only when it returns 0; the argument type, the result while a panel is open and the error codes are unknown +- [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 +- [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 +- [sceImeVshOpen](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeKeyboardUpdate](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshUpdate](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshUpdateContext](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshClearPreedit](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshNotifySetText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshSetCaretGeometry](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshConfirmPreedit](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshSetCaretIndexInPreedit](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshSetPreeditGeometry](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshSetSelectGeometry](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshClose](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshGetPanelPositionAndForm](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeSetCandidateIndex](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeConfirmCandidate](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeDisableController](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeSetVoiceDictationEnabled](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeSetEditEventWithRemotePlayEnabled](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeForTestFunction](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshInformConfirmdString2](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshSetSelectionText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeCheckUpdateTextInfo](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeCheckRemoteEventParam](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeKeyboardOpenInternal](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshSetParam](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshUpdateContext2](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshSendTextInfo](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshInformConfirmdString](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshNotifyCurrentTextInfo](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshNotifyCurrentTextInfoForReconvertText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshNotifyDeleteText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshNotifyUpdateCaretText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshCheckAutoReConvertingPreeditText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshSetPanelPosition](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeVshNotifyPreeditMetaInfo](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeConfigSet](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeConfigGet](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeDicGetWords](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeDicAddWord](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeDicDeleteWord](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeDicReplaceWord](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeDicDeleteLearnDics](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeDicDeleteUserDics](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature + +- [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 diff --git a/docs/dev/debt/libSceImeDialog.md b/docs/dev/debt/libSceImeDialog.md new file mode 100644 index 0000000000..58f32f8d68 --- /dev/null +++ b/docs/dev/debt/libSceImeDialog.md @@ -0,0 +1,13 @@ +# 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, sceImeDialogGetCurrentStarState, sceImeDialogInitInternal, sceImeDialogSetPanelPosition](../../../core/libs/prx/libSceImeDialog/Export.cpp) (libSceImeDialog) - their signatures follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/ime/ime_dialog.h#L34-L51), not measured on a console. They are declared solely to resolve known imports, `sceImeDialogForTestFunction` without parameters and the others with shadPS4's parameters; this is not an ABI claim, and every call throws. +- [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/libSceJobManager.md b/docs/dev/debt/libSceJobManager.md new file mode 100644 index 0000000000..123977e935 --- /dev/null +++ b/docs/dev/debt/libSceJobManager.md @@ -0,0 +1,5 @@ +# libSceJobManager technical debt + +### 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. 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..e5b7252fa0 --- /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 +- [libSceJpegDec](../../../core/libs/prx/libSceJpegDec/Export.cpp) (libSceJpegDec) - structure layouts follow [OpenOrbis `jpeg.h`](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4a92695538bdeb09fb056d06dd11725/include/orbis/_types/jpeg.h#L13-L72) and [SharpProspero `JpegDec.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Image/JpegDec.cs#L20-L117); attributes 1 and 2 are the values [SharpProspero's `JpegImage.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Graphics/JpegImage.cs#L64-L74) passes. The error codes are libScePngDec's: no reference lists the JpegDec ones, and SharpProspero raises 0x80650020 on its JpegDec path where it raises libScePngDec's 0x80690020 on its PngDec path. Image info always reports color space 3, suitable CSC attribute 1 and zero component ids and sampling factors; the console's values, including which images it reports attribute 2 for, are unknown. The coefficient memory size is 0 because the host decoder needs no coefficient buffer; the size the console reports for progressive images is unknown. `max_image_width` is not checked, and the packed `width << 16 | height` decode result is libScePngDec's + +### Functional + +- [libSceJpegDec](../../../core/libs/prx/libSceJpegDec/Export.cpp) (libSceJpegDec) - decode modes other than 0 and down scales other than 1, 2, 4 and 8 throw, as does a down scale of 2, 4 or 8 on a CMYK or YCCK image, which libjpeg-turbo cannot decode to RGB. The down scale factors are the ones [SharpProspero `JpegDec.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Image/JpegDec.cs#L57-L58) documents; the output size is the source size divided by the factor and rounded up, and the pixels come from libjpeg-turbo's scaled inverse DCT. The console's rounding, pixel values and the packed decode result for a down-scaled image (the output size here) are unverified. In `sceJpegDecDecode`, rejecting an output `image_mem_addr` not aligned to 4 bytes and rejecting an `alpha_value` above 0xFF follow libScePngDec validation and are not verified from a libSceJpegDec binary disassembly diff --git a/docs/dev/debt/libSceJpegEnc.md b/docs/dev/debt/libSceJpegEnc.md new file mode 100644 index 0000000000..2bff53d167 --- /dev/null +++ b/docs/dev/debt/libSceJpegEnc.md @@ -0,0 +1,5 @@ +# libSceJpegEnc technical debt + +### Unknown function info + +- [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 diff --git a/docs/dev/debt/libSceJson2.md b/docs/dev/debt/libSceJson2.md new file mode 100644 index 0000000000..3a89284e01 --- /dev/null +++ b/docs/dev/debt/libSceJson2.md @@ -0,0 +1,17 @@ +# libSceJson2 technical debt + +### 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 null text pointer or zero size returns `JsonErrorParse` (0x80920101) without modifying the destination Value; whether the console distinguishes a null pointer from a syntax error with a separate parameter error code is unknown. 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 +- [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. Default-constructed iterators are singular +- [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, compare and operator==(const String&), append(const char*, size_t), operator+=; Value::swap; Array::iterator and Object::iterator copy constructor, advance and operator==](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the signatures follow the mangled names and the behaviour is that of the matching `std::string` or container operation, with no cited source in KytyPS5, shadPS4, SharpProspero or disassembly. `append(text, length)` throws on a null text with a non-zero length; `operator+=` returns the String; `Value::swap` exchanges the node pointers; `advance` moves the iterator in place, returns it, and does not check the distance against the end. Whether the console returns the iterator from `advance` and how it bounds it is unverified +- [sce::Json::Value::operator bool](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - member of `Value` returning `bool` as a conversion operator, with no cited source in KytyPS5, shadPS4, SharpProspero, or disassembly: evaluates to `false` for `TypeNull` and boolean `false`, and `true` for boolean `true`. For all other value types it currently returns `true`; whether the console converts them, throws, or checks container emptiness is unverified +- [sce::Json::Object::insert and sce::Json::Object::erase](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - signatures and behaviors have no cited source in KytyPS5, shadPS4, SharpProspero, or disassembly: `insert` returns an iterator through a result pointer before `this` as `Object::begin` does, and takes a const Pair reference. If the key exists, it updates the existing value and returns an iterator to it; otherwise it appends a deep copy of the pair and returns an iterator to the new element; whether the console updates existing keys, appends duplicates, or ignores duplicate insertion is unverified. `erase` removes the item with matching key and returns 1 if found and removed, or 0 if not; whether the console returns size_t, bool, or an iterator is unverified + +### Functional + +- [sce::Json::InitParameter2 base-object constructor](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - forwards to the complete-object constructor and uses its inferred layout and default field values; constructor equivalence and initialization bytes remain unverified on the console diff --git a/docs/dev/debt/libSceJxr.md b/docs/dev/debt/libSceJxr.md new file mode 100644 index 0000000000..20faabeefb --- /dev/null +++ b/docs/dev/debt/libSceJxr.md @@ -0,0 +1,5 @@ +# 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. 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/libSceKeyboard.md b/docs/dev/debt/libSceKeyboard.md new file mode 100644 index 0000000000..a0a894689e --- /dev/null +++ b/docs/dev/debt/libSceKeyboard.md @@ -0,0 +1,10 @@ +# libSceKeyboard technical debt + +### 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. + +### 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. +- [sceKeyboardConnectPort, sceKeyboardDebugGetDeviceId, sceKeyboardDeviceOpen, sceKeyboardDisconnectAll, sceKeyboardDisconnectDevice, sceKeyboardDisconnectPort, sceKeyboardGetConnection, sceKeyboardGetDeviceInfo, sceKeyboardGetHandle, sceKeyboardMbusInit, sceKeyboardPadEmulateClose, sceKeyboardPadEmulateOpen, sceKeyboardSetProcessFocus, sceKeyboardSetProcessPrivilege](../../../core/libs/prx/libSceKeyboard/Export.cpp) (libSceKeyboard) - throwing stubs declared without parameters from the firmware export database (aerolib.csv); their guest signatures are unverified on hardware diff --git a/docs/dev/debt/libSceLibcInternal.md b/docs/dev/debt/libSceLibcInternal.md new file mode 100644 index 0000000000..ff8c271f1f --- /dev/null +++ b/docs/dev/debt/libSceLibcInternal.md @@ -0,0 +1,19 @@ +# libSceLibcInternal technical debt + +### Unknown function info + +- [_sceLibcInternalThreadAtexit, _sceLibcInternalThreadDtors](../../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - FreeBSD `__cxa_thread_atexit` semantics assumed; a null or non-image destructor and an allocation failure throw +- [_sceLibcInternalForceTlsDestructor](../../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - behaviour follows `sceLibcForceTlsDestructor(int)` in a title's `libc.prx` (disassembly): the calling thread's destructors whose dso belongs to the module run newest first, only if the dso's module is loaded and the dso points to itself, in up to 4 passes. The return type is unknown (that function returns nothing), 0 is returned. The module of each dso is looked up when the destructors are forced, not when they are registered as `libc.prx` does + +### Functional + +- [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) 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) 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..b8330484a5 --- /dev/null +++ b/docs/dev/debt/libSceLibreSsl.md @@ -0,0 +1,5 @@ +# libSceLibreSsl technical debt + +### Unknown function info + +- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 399 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`. diff --git a/docs/dev/debt/libSceLibreSsl3.md b/docs/dev/debt/libSceLibreSsl3.md new file mode 100644 index 0000000000..3fbd00e901 --- /dev/null +++ b/docs/dev/debt/libSceLibreSsl3.md @@ -0,0 +1,5 @@ +# libSceLibreSsl3 technical debt + +### Unknown function info + +- [libSceLibreSsl3 pending exports](../../../core/libs/prx/libSceLibreSsl3/Export.cpp) - 87 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl3.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/libSceLoginDialog.md b/docs/dev/debt/libSceLoginDialog.md new file mode 100644 index 0000000000..07a38ff414 --- /dev/null +++ b/docs/dev/debt/libSceLoginDialog.md @@ -0,0 +1,10 @@ +# libSceLoginDialog technical debt + +### 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 0x8134xxxx codes, the Open param checks (size 0x40, word 1 below 2, words 0x2C to 0x3C zero) and the `{1, 0x81340007}` result words are the ones [prosper](https://github.com/mattias800/prosper/blob/ed42e499888d5a86792c3e1b2df3aebd8ca1a681/prosper/src/hle/service/hle_service.cpp#L805-L874) reads from the shipped module. The order of the param and state checks follows prosper's handlers and is not stated separately, the meaning of word 1 and of the second result word is unknown, words 2 to 10 are not checked, and Open's busy result is never reached because the dialog never runs. Closes when: the module's check order is read or measured on a PS5 diff --git a/docs/dev/debt/libSceLoginService.md b/docs/dev/debt/libSceLoginService.md new file mode 100644 index 0000000000..f33713e90d --- /dev/null +++ b/docs/dev/debt/libSceLoginService.md @@ -0,0 +1,9 @@ +# libSceLoginService technical debt + +### Silent stubs + +- [sceLoginServiceInitialize, sceLoginServiceTerminate](../../../core/libs/prx/libSceLoginService/Export.cpp) (libSceLoginService) - return success and keep no login-service state: AnyPS5 has no PSN login service. EA SPORTS FC 27 (PPSA34015) loads the module with `sceSysmoduleLoadModule(0xe3)` and calls `sceLoginServiceInitialize` with no arguments; a missing `libSceLoginService.prx` stops Windows start-up with `Failed to load module` / `GetLastError: 126` + +### Unknown function info + +- [sceLoginServiceRequestDevices](../../../core/libs/prx/libSceLoginService/Export.cpp) (libSceLoginService) - PPSA34015 passes one pointer to a stack buffer (`lea rdi, [rbp-0x80]`) then calls `sceLoginServiceTerminate`. The buffer layout, other arguments and console errors are unknown, so every call throws without reading or writing caller memory diff --git a/docs/dev/debt/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 new file mode 100644 index 0000000000..8c7cb7dd7e --- /dev/null +++ b/docs/dev/debt/libSceMbus.md @@ -0,0 +1,11 @@ +# libSceMbus technical debt + +### 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 + +### 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 + +- [libSceMbus firmware 3.20 pending exports](../../../core/libs/prx/libSceMbus/PendingExports.cpp) - 115 symbol names are present in [the PS5 3.20 libSceMbus export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMbus.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`. diff --git a/docs/dev/debt/libSceMouse.md b/docs/dev/debt/libSceMouse.md new file mode 100644 index 0000000000..19fe9ab3b5 --- /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) - signature taken from a call site (a handle, then x and y as 16-bit values: PPSA30601 passes the centre of its display size); behaviour not measured on hardware. Validates initialization and the handle like `sceMouseRead` and returns `MOUSE_OK`; the position is not stored because the library reports relative motion only diff --git a/docs/dev/debt/libSceMove.md b/docs/dev/debt/libSceMove.md new file mode 100644 index 0000000000..d1660178ce --- /dev/null +++ b/docs/dev/debt/libSceMove.md @@ -0,0 +1,10 @@ +# libSceMove technical debt + +### Silent stubs + +- [libSceMove](../../../core/libs/prx/libSceMove/Export.cpp) implements only the no-controller path: initialization and termination succeed, `sceMoveOpen` returns a new handle, `sceMoveClose` and `sceMoveResetLightSphere` succeed for an open handle, and the device info, state, extension port, vibration and `sceMoveSetLightSphere` calls return 1 (no controller connected) and write nothing + +### Unknown function info + +- [libSceMove](../../../core/libs/prx/libSceMove/Export.cpp) (libSceMove) - behaviour, error codes and the handle values (`0x30B0100`, then every `0x100`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/move/move.cpp) (PS4), which checks no handle or open arguments; none is measured on a PS5. Signatures follow [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json), except `sceMoveGetExtensionPortInfo`, which follows shadPS4. Before `sceMoveInit` and after `sceMoveTerm` every other call returns `NOT_INIT` (`0x80EE0001`); a null output or a negative timestamp returns `INVALID_ARG` (`0x80EE0003`). Closes when: measured on a PS5 +- [libSceMove](../../../core/libs/prx/libSceMove/Export.cpp) (libSceMove) - a handle that was never opened, is closed or was opened before `sceMoveTerm` returns `INVALID_HANDLE` (`0x80EE0004`), checked before `INVALID_ARG`, and `sceMoveOpen` returns `ALREADY_OPENED` (`0x80EE0007`) for a type and index that are already open; both codes are named in [shadPS4's move_error.h](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/move/move_error.h), which never returns them, so the results and their order are not measured. `sceMoveOpen` throws for a user other than the initial user, whose console result is not known, and accepts any type and index. Closes when: measured on a PS5 diff --git a/docs/dev/debt/libSceMoveTracker.md b/docs/dev/debt/libSceMoveTracker.md new file mode 100644 index 0000000000..156c1dfb0a --- /dev/null +++ b/docs/dev/debt/libSceMoveTracker.md @@ -0,0 +1,5 @@ +# libSceMoveTracker technical debt + +### Unknown function info + +- [sceMoveTrackerCalibrateReset, sceMoveTrackerCalibrateYaw, sceMoveTrackerCameraUpdate, sceMoveTrackerCaptureInit, sceMoveTrackerCaptureTerm, sceMoveTrackerControllersUpdate, sceMoveTrackerGetState, sceMoveTrackerGetVersion, sceMoveTrackerGetWorkingMemorySize, sceMoveTrackerInit, sceMoveTrackerPlayGetImages, sceMoveTrackerPlayGetStates, sceMoveTrackerPlayStart, sceMoveTrackerPlayStep, sceMoveTrackerRecordSave, sceMoveTrackerRecordStart, sceMoveTrackerRecordStop and sceMoveTrackerTerm](../../../core/libs/prx/libSceMoveTracker/Export.cpp) (libSceMoveTracker) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMoveTracker.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/libSceMsgDialog.native.md b/docs/dev/debt/libSceMsgDialog.native.md new file mode 100644 index 0000000000..bcd6bc6be9 --- /dev/null +++ b/docs/dev/debt/libSceMsgDialog.native.md @@ -0,0 +1,5 @@ +# libSceMsgDialog.native technical debt + +### Silent stubs + +- [libSceMsgDialog.native](../../../core/libs/prx/libSceMsgDialog.native/Export.cpp) - dialogs finish at `sceMsgDialogOpen` without UI and answer with button 1, so `sceMsgDialogClose` returns `NOT_RUNNING`; its result before `sceMsgDialogInitialize` follows libSceMsgDialog. `sceMsgDialogTerminate` resets running or finished dialogs and returns OK, rejecting uninitialized calls with `COMMON_DIALOG_ERROR_NOT_INITIALIZED` following shadPS4; null pointer arguments return `COMMON_DIALOG_ERROR_ARG_NULL`. diff --git a/docs/dev/debt/libSceNKWeb.md b/docs/dev/debt/libSceNKWeb.md new file mode 100644 index 0000000000..4191fc13a3 --- /dev/null +++ b/docs/dev/debt/libSceNKWeb.md @@ -0,0 +1,5 @@ +# libSceNKWeb technical debt + +### Unknown function info + +- [sceNKWebInitialize and sceNKWebTerminate](../../../core/libs/prx/libSceNKWeb/Export.cpp) (libSceNKWeb) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNKWeb.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/libSceNet.md b/docs/dev/debt/libSceNet.md new file mode 100644 index 0000000000..cdb38de437 --- /dev/null +++ b/docs/dev/debt/libSceNet.md @@ -0,0 +1,197 @@ +# libSceNet technical debt + +### Silent stubs + +- [sceNetShowIfconfig, sceNetShowNetstat, sceNetShowRoute](../../../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 + +- [sceNetEtherStrton](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - its signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs#L737-L913) and its [Net bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs#L19), not measured on a console; the throwing declaration only resolves the import. +- [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 +- [sceNetClearDnsCache](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) returns 0 without doing anything: every `sceNetResolverStart*` lookup goes to the host resolver, so the library keeps no DNS cache to drop, and the host's own cache is left alone. The `(int flags)` signature and its meaning come from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs#L928-L930); nonzero flags throw because their meaning is unknown +- [sceNetGetStatisticsInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetGetInterfaceStats](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetSetDnsInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetShowIfconfigWithMemory](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetShowPolicy](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetShowPolicyWithMemory](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetShowRouteWithMemory](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetResolverConnectAbort](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetResolverConnectCreate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetResolverConnect](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetResolverConnectDestroy](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetGetSockInfo6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetSetDns6Info](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetShowRoute6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetShowRoute6WithMemory](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetAllocateAllRouteInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetAllocateAllRouteInfo6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetControl](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetDbgInit](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetEventCallbackCreate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetEventCallbackDestroy](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetEventCallbackGetError](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetEventCallbackWaitCB](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetFreeAllRouteInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetGetArpInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetGetAppMemInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [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 +- [sceNetGetIfList](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetGetIfListEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetGetIfListOnce](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetGetIfName](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(uint32_t index)` signature and the table of ten interface names (`lo0`, `eth0`, `eth1`, `dbg0`, `wlan0`, `wlan1`, `gbe0`, `bt0`, `phone0`, `pppoe0`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/d5ba5fd06c49af29b8ec9200cad70bca676a9cd4/src/core/libraries/net/net.cpp); an index past the table sets `EINVAL` and returns an empty string. Whether the console has more names, or returns null, is unverified on hardware +- [sceNetGetIfnameNumList](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetGetNameToIndex](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetGetRandom](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(uint32_t* output)` signature and the `EINVAL` failure for a null output follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/d5ba5fd06c49af29b8ec9200cad70bca676a9cd4/src/core/libraries/net/net.cpp); the value comes from the host's `std::random_device`, as `kern.rng_pseudo` in libkernel does. Unverified on hardware +- [sceNetGetRouteInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetGetsockoptEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetGetSystemTime](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unverified on PS5 hardware: the `void(uint64_t*)` signature, process-relative microseconds and ignored null output follow [shadPS4 net.cpp](https://github.com/shadps4-emu/shadPS4/blob/d615b3887ba29025a59242e1a26f5b68bb176158/src/core/libraries/net/net.cpp#L1654-L1659). The clock is the same as `sceKernelGetProcessTime`, including `APS5_TIME_SCALE` +- [sceNetGetSystemMemInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetInitParam](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [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. +- [sceNetIoctl](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetMemoryAllocate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(int64_t size, int flags)` signature, flag 2 as zero fill and the null result for a size of 0 or less follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/d5ba5fd06c49af29b8ec9200cad70bca676a9cd4/src/core/libraries/net/net.cpp). The block comes from the guest heap, and an allocation failure returns null without setting an errno. Any other flag bit throws, since its meaning is not known. Unverified on hardware +- [sceNetMemoryFree](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(void* memory)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/d5ba5fd06c49af29b8ec9200cad70bca676a9cd4/src/core/libraries/net/net.cpp); the block returns to the guest heap and a null pointer is ignored. Unverified on hardware +- [sceNetSetDnsInfoToKernel](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetSetsockoptEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetSocketInternal](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetSyncCreate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetSyncDestroy](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetSyncGet](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetSyncSignal](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetSyncWait](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetSysctl](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetThreadCreate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetThreadExit](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetThreadJoin](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetUsleep](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(int microseconds)` signature and the `EINVAL` failure for a negative count follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp); other counts sleep through libkernel's `sceKernelUsleep`, a cancellation point, and return 0. Whether the console takes the count as signed, and what it returns, is unverified on hardware +- [sceNetShowIfconfigForBuffer](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetShowRouteForBuffer](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetShowNetstatForBuffer](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetShowNetstatEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetShowNetstatExForBuffer](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetInetNtopWithScopeId](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetInetPtonWithScopeId](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetGetDns6Info](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetSetDns6InfoToKernel](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetShowRoute6ForBuffer](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigGetIfFlags](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigSetIfFlags](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigUnsetIfFlags](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigDelIfaddr](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigDownInterface](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigSetDefaultRoute](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigSetDefaultRouteDev](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigSetIfaddr](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigSetIfLinkLocalAddr6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigUpInterface](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigUpInterfaceWithFlags](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigCleanUpAllInterfaces](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigFlushRoute](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigFlushRoute6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigAddIfaddr](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigGetIfaddr](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigSetIfmtu](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigAddArp](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigAddArpWithInterface](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigAddMRoute](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigAddRoute](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigAddRouteWithInterface](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigDelArp](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigDelArpWithInterface](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigDelDefaultRoute](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigDelDefaultRouteDev](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigDelMRoute](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigDelRoute](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigGetDefaultRoute](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetDhcpGetAutoipInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetDhcpGetInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetDhcpGetInfoEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetDhcpStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetDhcpStop](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigAddIfaddr6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigDelIfaddr6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigDelDefaultRoute6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigSetIfaddr6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigSetIfaddr6WithFlags](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigSetDefaultRoute6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigGetIfaddr6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigGetDefaultRoute6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigDelRoute6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigAddRoute6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetAddrConfig6Start](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetAddrConfig6Stop](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetAddrConfig6GetInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetInfoDumpStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetInfoDumpStop](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetDhcpdStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetDhcpdStop](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetDuplicateIpStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetDuplicateIpStop](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigWlanAdhocClearWakeOnWlan](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigWlanAdhocCreate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigWlanAdhocGetWakeOnWlanInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigWlanAdhocJoin](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigWlanAdhocLeave](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigWlanAdhocPspEmuClearWakeOnWlan](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigWlanAdhocPspEmuGetWakeOnWlanInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigWlanAdhocPspEmuSetWakeOnWlan](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigWlanAdhocScanJoin](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigWlanAdhocSetExtInfoElement](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigWlanAdhocSetWakeOnWlan](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigWlanApStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigWlanApStop](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigWlanBackgroundScanQuery](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigWlanBackgroundScanStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigWlanBackgroundScanStop](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigWlanDiagGetDeviceInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigWlanDiagSetAntenna](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigWlanDiagSetTxFixedRate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigWlanGetDeviceConfig](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigWlanInfraGetRssiInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigWlanInfraLeave](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigWlanInfraScanJoin](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigWlanScan](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigWlanSetDeviceConfig](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigEtherGetLinkMode](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigEtherPostPlugInOutEvent](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigEtherSetLinkMode](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetPppoeStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetPppoeStop](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetBandwidthControlSetDefaultParam](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetBandwidthControlGetDefaultParam](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetBandwidthControlGetDataTraffic](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigSetDefaultScope](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigRoutingStart](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigRoutingStop](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigRoutingShowRoutingConfig](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetConfigRoutingShowtCtlVar](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetGetStatisticsInfoInternal](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetDumpAbort](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetDumpCreate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetDumpSetFilterParam](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetDumpDestroy](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetDumpRead](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetEmulationDebugSettingsSet](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetEmulationDebugSettingsGet](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetEmulationSet](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetEmulationGet](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature + +### Functional + +- [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. Windows sockets stay non-blocking and the library waits for readiness until the stored timeout. A stream receive that times out still reports `ETIMEDOUT` rather than FreeBSD's `EAGAIN`; a datagram receive reports `EAGAIN`. 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. +- [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) - 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. diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md new file mode 100644 index 0000000000..f9fb92ef5c --- /dev/null +++ b/docs/dev/debt/libSceNetCtl.md @@ -0,0 +1,87 @@ +# libSceNetCtl technical debt + +### Silent stubs + +- [sceNetCtlGetNatInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - `sceNetCtlGetNatInfo` validates null pointers and returns `SCE_NET_CTL_ERROR_NOT_CONNECTED` (`0x80412108`) without performing NAT discovery. + +### Unknown function info + +- [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 +- [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 +- [sceNetCtlRegisterCallbackForNpToolkit](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlRegisterCallbackIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlRegisterCallbackForLibIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlUnregisterCallbackForNpToolkit](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlUnregisterCallbackIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlUnregisterCallbackForLibIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlCheckCallbackForNpToolkit](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlCheckCallbackForLibIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlClearEventIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlClearEventForNpToolkit](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlClearEventForLibIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetResultIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetStateIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetState2IpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetInfoIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetEtherLinkMode](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetWifiType](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlConnectIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlConnectWithRetryIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlConnectConfIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlDisconnectIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlScanIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetScanInfoBssidIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetScanInfoByBssidIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetScanInfoForSsidScanIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetScanInfoBssidForSsidListScanIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetScanInfoForSsidListScanIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlSetErrorNotificationEnabledIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlRegisterCallbackV6IpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlUnregisterCallbackV6IpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [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 +- [sceNetCtlGetResultV6IpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetStateV6IpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetInfoV6IpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetNatInfoIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlSetStunWithPaddingFlagIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlUnsetStunWithPaddingFlagIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlIsBandwidthManagementEnabledIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlDisableBandwidthManagementIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlEnableBandwidthManagementIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetBandwidthInfoIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetNetEvConfigInfoIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetConnectionElapsedTimeIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApRpCheckCallback](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApRpClearEvent](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApRpRegisterCallback](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApRpUnregisterCallback](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApRpGetResult](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApRpGetState](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApRpGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApRpStart](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApCpStart](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApAppStartWithRetryPid](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApAppStartWithRetry](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApRpStartWithRetry](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApRpStartConf](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApRpStop](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApCpStop](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApAppInitWpaKey](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApAppInitWpaKeyForQa](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlApRestart](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetBweCheckCallbackIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetBweClearEventIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetBweRegisterCallbackIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetBweUnregisterCallbackIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetBweGetInfoIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetBweStartInternetConnectionTestIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetBweFinishInternetConnectionTestIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetBweStartInternetConnectionTestBandwidthTestIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetBweSetInternetConnectionTestResultIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature +- [sceNetCtlGetWifiInfoIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature + +### Functional + +- [sceNetCtlCheckCallback](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - the state comes from the host interfaces, as in `sceNetCtlGetState`: `IPOBTAINED` (3) while the host has a non-loopback IPv4 or IPv6 address, `DISCONNECTED` (1) otherwise; the event values follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/network/net_ctl_codes.h#L27-L29), and no other event is delivered. Each callback registered with `sceNetCtlRegisterCallback` is called on the checking thread with its registration argument when the state differs from the last one it received, so a new registration receives the current state at the next check. This edge-triggered model follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L3648-L3661); [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/network/net_ctl_obj.cpp#L47-L57) delivers the state on every check instead, and neither is measured on hardware. KytyPS5 calls the callbacks under its lock; here they are called after the lock is released, from a copy of their slots, so a `sceNetCtlUnregisterCallback` or `sceNetCtlTerm` on another thread that returns while a check is in flight does not stop that check from calling the callback once more with its old argument. Callbacks registered with `sceNetCtlRegisterCallbackV6` keep their argument but receive no event, because no source says which call delivers them +- [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 diff --git a/docs/dev/debt/libSceNetCtlAp.md b/docs/dev/debt/libSceNetCtlAp.md new file mode 100644 index 0000000000..012a82c476 --- /dev/null +++ b/docs/dev/debt/libSceNetCtlAp.md @@ -0,0 +1,12 @@ +# 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) (libSceNetCtlAp) - checks run in this order, which is not verified: a null structure returns `INVALID_ADDR` (`0x80412991`), a size field other than 152 or 184 `INVALID_SIZE` (`0x80412998`), then `NOT_STARTED`. The option string is not read, since the accepted strings (`INVALID_OPT_STR`, `0x80412901`) are unknown. Whether `sceNetCtlApStop` of a stopped access point returns `NOT_STARTED` or 0 is unverified +- [sceNetCtlApClearEvent, sceNetCtlApGetResult](../../../core/libs/prx/libSceNetCtlAp/Export.cpp) (libSceNetCtlAp) - event types 1 to 4 are accepted and others return `INVALID_TYPE` (`0x80412990`), checked before the null `errorCode` (`INVALID_ADDR`). With no event ever delivered, `sceNetCtlApGetResult` writes 0; what the console writes before the first event is unverified +- [sceNetCtlApRegisterCallback, sceNetCtlApUnregisterCallback](../../../core/libs/prx/libSceNetCtlAp/Export.cpp) (libSceNetCtlAp) - a null function or id pointer returns `INVALID_ADDR`, a ninth registration `CALLBACK_MAX` (`0x80412995`), an id outside 0 to 7 `INVALID_ID` (`0x80412996`) and an unregistered id `ID_NOT_FOUND` (`0x80412997`). Ids are the lowest free slot, as in libSceNetCtl; the console's id assignment is unverified diff --git a/docs/dev/debt/libSceNetCtlApDialog.md b/docs/dev/debt/libSceNetCtlApDialog.md new file mode 100644 index 0000000000..aefc800699 --- /dev/null +++ b/docs/dev/debt/libSceNetCtlApDialog.md @@ -0,0 +1,5 @@ +# libSceNetCtlApDialog technical debt + +### Silent stubs + +- [libSceNetCtlApDialog](../../../core/libs/prx/libSceNetCtlApDialog/Export.cpp) - the access-point dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as no access-point connection is brought up. `Open` takes the 104-byte parameter block and `GetResult` writes a 40-byte result (int32 result and 36 zeroed reserved bytes), following [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/NetCtlApDialog.cs); the parameter block is not validated and the 0x80B8xxxx error codes follow the other common dialogs, neither verified on hardware. Deathloop imports it diff --git a/docs/dev/debt/libSceNgs2.md b/docs/dev/debt/libSceNgs2.md new file mode 100644 index 0000000000..7b134da79b --- /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, sceNgs2GetWaveformFrameInfo, sceNgs2ParseWaveformUser, sceNgs2SystemEnumHandles, sceNgs2SystemEnumRackHandles, sceNgs2VoiceGetMatrixInfo, sceNgs2VoiceGetOwner](../../../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 new file mode 100644 index 0000000000..09f0ba458a --- /dev/null +++ b/docs/dev/debt/libSceNgs2.native.md @@ -0,0 +1,43 @@ +# libSceNgs2.native technical debt + +### 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, 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 and 0x1d for UserFx, voice parameter ids `0x40001f00 | module index` and `0x40001d00 | module index`, the process flags (1 on the first process call after setup, 2 after a parameter change, 0 otherwise) and the call order (setup per voice at rack creation, process after the inputs are mixed, cleanup at destruction) come only from [KytyPS5](https://github.com/KytyPS5/KytyPS5) and shadPS4. Only UserFx and UserFx2 modules on a single buffer are accepted: other module ids, more buffers, a module control handler, different input and output channel counts, setup flags and default rack options throw. Each module's state is a separate buffer, not the `state_offset` slice of the voice state, and `sceNgs2VoiceGetState` still throws for this rack. As in KytyPS5, UserFx2 process runs only on grains where an input produced samples, so an effect tail stops with the input and a pending flag 2 waits for the next audible grain +- [NGS2 custom module ids](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native) - a custom rack with a module id other than 0x13 (sampler rack), 0x1d and 0x1f (submixer rack) throws. Trails in the Sky 2nd Chapter (PPSA35169) creates a rack with module id 0x20 (`NGS2: custom module id 0x20 is not implemented`, #4246); which module it is, and its structure, are unknown. +- [NGS2 reverb rack](../../../core/libs/prx/libSceNgs2.native/src/Reverb.cpp) (libSceNgs2.native, rack 0x2001) - follows the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output at 11.025 to 48 kHz, including parameter changes during playback (differences at float rounding level); not yet compared with the PS5 library. Structure layouts come from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_reverb.h` and the param ids 0x20010000 (setup) and 0x20010001 (I3DL2) from PPSA22520. Only the default `reverb_size` of 1 is handled, with its 14400-sample reflection buffer; other sizes and the library's half-rate mode throw. System sample rates below about 7.5 kHz, where an allpass delay would have no samples, throw. Setup only accepts equal input and output channel counts +- [sceNgs2ReportRegisterHandler, sceNgs2ReportUnregisterHandler](../../../core/libs/prx/libSceNgs2.native/src/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 +- [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 +- [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` 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. Unknown flags, 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 + +### 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 new file mode 100644 index 0000000000..bb10e4b7d8 --- /dev/null +++ b/docs/dev/debt/libSceNpAuth.md @@ -0,0 +1,9 @@ +# libSceNpAuth technical debt + +### Unknown function info + +- [sceNpAuthGetAuthorizationCodeA](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpAuth.c); its signature follows [shadPS4 `np_auth.cpp`](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L155-L190), is not measured on a console, and the throwing declaration only resolves the import +- [sceNpAuthGetIdTokenA](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpAuth.c); its prototype follows [shadPS4 `np_auth.cpp`](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L340-L375), is not measured on a console, and the throwing declaration only resolves the import +- [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, sceNpAuthGetAuthorizationCode, sceNpAuthGetAuthorizationCodeV3, sceNpAuthGetIdToken, 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/main/src/core/libraries/np/np_auth.cpp) (PS4); 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 +- [sceNpAuthSetTimeout](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - imported by PPSA01317, which fails to load without it. The signature `(reqId, resolveRetry, resolveTimeout, connTimeout, sendTimeout, recvTimeout)` follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_auth.cpp) (PS4), which only stubs it. An unknown request ID returns `ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND` like the other request calls; otherwise it returns OK and keeps nothing, as no request reaches the network the timeouts would bound. Argument checks of the console are unverified diff --git a/docs/dev/debt/libSceNpAuthAuthorizedAppDialog.md b/docs/dev/debt/libSceNpAuthAuthorizedAppDialog.md new file mode 100644 index 0000000000..e6b5fa19a6 --- /dev/null +++ b/docs/dev/debt/libSceNpAuthAuthorizedAppDialog.md @@ -0,0 +1,6 @@ +# libSceNpAuthAuthorizedAppDialog technical debt + +### 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. +- [sceNpAuthAuthorizedAppDialogClose](../../../core/libs/prx/libSceNpAuthAuthorizedAppDialog/Export.cpp) - `sceNpAuthAuthorizedAppDialogClose` moves an initialized dialog straight to `COMMON_DIALOG_STATUS_FINISHED`, also when called before `Open`, following the Signin / NetCtlAp dialogs; MsgDialog / SaveDataDialog / ErrorDialog return NOT_RUNNING or INVALID_STATE for that case, and which one the real library does is unverified. diff --git a/docs/dev/debt/libSceNpCommerce.md b/docs/dev/debt/libSceNpCommerce.md new file mode 100644 index 0000000000..d9ac7be185 --- /dev/null +++ b/docs/dev/debt/libSceNpCommerce.md @@ -0,0 +1,10 @@ +# libSceNpCommerce technical debt + +### 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) (libSceNpCommerce) - declared without parameters; follows [prosper](https://github.com/mattias800/prosper/blob/73413cb445b0364bb49c4d2478e9ee1614b9f7d9/prosper/src/hle/np/np.cpp#L500-L504), which serves it with its `sceNpCommerceDialogOpen` handler: every argument is ignored, nothing is validated, it returns 0 and the dialog finishes at once, so `sceNpCommerceDialogGetResult` reports it cancelled. The parameters and the console's error results are unknown. PPSA34501 calls it at Zombies > Online 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..be7f650ac6 --- /dev/null +++ b/docs/dev/debt/libSceNpCppWebApi.md @@ -0,0 +1,17 @@ +# libSceNpCppWebApi technical debt + +### Silent stubs + +- [sce::Np::CppWebApi::Common](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - the `finish` overrides of `TransactionBase<...>` (8 instantiations), `DownStreamTransaction<...>` (2 instantiations) and `Vector>::pushBack` return success without doing work; a failed request never produces a transaction to finish +- [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 + +### 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 +- [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 new file mode 100644 index 0000000000..2a9d2f3089 --- /dev/null +++ b/docs/dev/debt/libSceNpEntitlementAccess.md @@ -0,0 +1,11 @@ +# libSceNpEntitlementAccess technical debt + +### 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 + +### Functional + +- [sceNpEntitlementAccessGetGameTrialsFlag, sceNpEntitlementAccessGetPftFlag](../../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) (libSceNpEntitlementAccess) - reports a full game, not a game trial or Play First Trial: writes 0 to the `uint32_t` output and returns 0, and a null pointer returns `0x817D0002`. The single-pointer signature, the value, the one-word write and the error of `sceNpEntitlementAccessGetGameTrialsFlag` follow [sharpemu](https://github.com/sharpemu/sharpemu/blob/a2983692930564815a77f81e74c151268f5b935f/src/SharpEmu.Libs/Np/NpEntitlementAccessExports.cs#L98-L116), which has no `sceNpEntitlementAccessGetPftFlag`; those of `sceNpEntitlementAccessGetPftFlag` follow [dlc_emu](https://github.com/drakmor/dlc_emu/blob/main/src/dlc_modules/dlc_content.cpp), which takes the same `SceNpEntitlementAccessGameTrialsFlag*` as the trials flag, writes the OFF value 0 and returns the parameter error for null; `0x817D0002` is the parameter error of KytyPS5, prosper and `sceNpEntitlementAccessGetEntitlementKey`, while `sceNpEntitlementAccessGetSkuFlag` here returns `0x80558003`. None of this is a console measurement, and PRAGMATA (PPSA02530) shows four live registers at the call, so further arguments are not ruled out diff --git a/docs/dev/debt/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 new file mode 100644 index 0000000000..b45e64e5f6 --- /dev/null +++ b/docs/dev/debt/libSceNpManager.md @@ -0,0 +1,18 @@ +# libSceNpManager technical debt + +### Unknown function info + +- [sceNpGetSandboxInfo](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `ZVZ++68nNI8`) - the name hashes to the imported NID (found with nidhunt, confirmed with `tools/nid_names.py`). STAR WARS: Galactic Racer (PPSA28416) imports it; its OnlineSubsystemPS5 calls it with one pointer to a 0x50-byte stack buffer and reads the buffer as a string (discussions/3086). The returned string, the exact parameter struct and console errors are unknown, so every call throws without reading or writing caller memory. A working implementation belongs with that title's bring-up. +- [sceNpUnknown_8weiQyIdI3A](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `8weiQyIdI3A`) - the name is not in aerolib. EA SPORTS FC 27 (PPSA34015) fails to load `libSceNpManager.prx` without it (`unresolved ELF import` / `GetLastError: 127`). Its only call is `mov edi, r14d` (a user id) and `lea rsi, [rbp-0x40]` after a 17-byte copy into that buffer (`mov edx, 0x11`); `test eax, eax` follows. The C name, the type of the 17-byte object and the console return are unknown, so every call throws without reading or writing caller memory. +- [sceNpUnknown_WxLcN76q19o](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `WxLcN76q19o`) - the name is not in aerolib. PPSA34015 imports it from `libSceNpManager.prx` as a strong `JUMP_SLOT` with no call found in the eboot; the loader still requires the export. Arguments, result and console errors are unknown, so every call throws. +- [sceNpSetTimeout](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the signature follows [orbital](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json), [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/np/np_manager.cpp) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Np/NpAccount.cs); the rejected values (all five 0, negative retries, a resolve timeout under 1 s or another timeout under 10 s, 0 meaning the default) are shadPS4's, not checked on a console. An id that was never created or was deleted returns `REQUEST_NOT_FOUND`, as in shadPS4; there is no network, so the timeouts are not stored +- [sceNpGetUserIdByAccountId](../../../core/libs/prx/libSceNpManager/Export.cpp) - the `(account id, user id output)` signature and its errors follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_manager.cpp): account id 0 or a null output returns `SCE_NP_ERROR_INVALID_ARGUMENT`, and a signed-out NP returns `SCE_NP_ERROR_SIGNED_OUT` without writing the output. NP is always signed out here, so no account id resolves to a user + +### Functional + +- [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 +- [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 + +- The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`sceNpCheckCallbackForLib`](../../../core/libs/prx/libSceNpManager/Export.cpp). diff --git a/docs/dev/debt/libSceNpPartner001.md b/docs/dev/debt/libSceNpPartner001.md new file mode 100644 index 0000000000..27edb961fb --- /dev/null +++ b/docs/dev/debt/libSceNpPartner001.md @@ -0,0 +1,6 @@ +# libSceNpPartner001 technical debt + +### 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 +- [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..45d68abf16 --- /dev/null +++ b/docs/dev/debt/libSceNpParty.md @@ -0,0 +1,5 @@ +# libSceNpParty technical debt + +### Unknown function info + +- [sceNpPartyGetState](../../../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 diff --git a/docs/dev/debt/libSceNpSessionSignaling.md b/docs/dev/debt/libSceNpSessionSignaling.md new file mode 100644 index 0000000000..f5f1396a51 --- /dev/null +++ b/docs/dev/debt/libSceNpSessionSignaling.md @@ -0,0 +1,10 @@ +# libSceNpSessionSignaling technical debt + +### 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 +- PPSA31327 imports declared without parameters, signatures unknown: [sceNpSessionSignalingGetGroupInfo, sceNpSessionSignalingGetConnectionFromNetAddress2](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling), imported by Yakuza 0 Director's Cut and not called before the main menu. +- [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/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/libSceNpTrophy2.md b/docs/dev/debt/libSceNpTrophy2.md new file mode 100644 index 0000000000..4576f12de6 --- /dev/null +++ b/docs/dev/debt/libSceNpTrophy2.md @@ -0,0 +1,21 @@ +# libSceNpTrophy2 technical debt + +### Silent stubs + +- [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) report the title's [trophy data](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp) (`sce_sys/trophy2/trophyNN.ucp`) and the unlocks in the [trophy store](../../../core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp). Without the file, or when it is malformed, they report the old placeholders and `sceNpTrophy2GetTrophyInfoArray` lists only trophy 0. Icons are not read. Hidden trophies report their names and descriptions; whether the console blanks them before the unlock is not known. `progress_percentage` weighs gold 6, silver 2 and bronze 1 and leaves the platinum out, as KytyPS5 does (the ratio of the published PSN trophy points); the console's formula is not known. A trophy or group that is not in the file throws. A context that was not created or was destroyed reports placeholders and no unlocks and returns success; the console's errors for it are unknown. +- [Trophy data limits](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp): a `trophyNN.ucp` or `udsNN.ucp` larger than 256 MiB or listing more than 4096 files, and a JSON file in it that nests deeper than 64 levels or holds more than 1048576 values, are not used, and the title runs as without that file. The limits keep the memory the reader takes to the file and about 140 MiB of parsed JSON; the console's limits are not known. +- [Text language](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp): texts come from `tropmeta_.json` for the console language, then `en-US`, then the file's `defaultLanguage`. The tags for languages 0 to 29 are KytyPS5's; `uk-UA` for 30 (Ukrainian) is an assumption. +- [Platinum](../../../core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp): an unlock through any path unlocks the platinum that the trophy names in `platinumTrophyId` once every trophy naming that platinum is unlocked, as KytyPS5 does; trophies without `platinumTrophyId` are not required. With the title's trophy data loaded, a posted `_UnlockTrophy` of a trophy that is not in the data, of a platinum or of a progressive trophy is not stored and prints a `[trophy] ignored` line, as KytyPS5 refuses them, so a progressive trophy unlocks only through its statistic; without trophy data any id of 0 or more is stored, including ids the data would make the platinum, a progressive trophy or no trophy, and the platinum is not unlocked when the trophies it needs are. The console's behaviour is not known. +- [Damaged records](../../../core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp): a line in `_trophy/_.txt` or `.stats.txt` that is not a new ` ` pair (a blank line, a carriage return, a repeated or negative id, a value that is not a number) makes `readRecord` throw a `std::runtime_error` that names the file and the line. Every call that reads that record then throws it instead of returning, which stops the title as any throwing export does, so the title gets no return value; main returned 0 from these calls. The failed read is not kept, so each later call reads the file again and throws again until the file is fixed or deleted. The unlock record is read by `sceNpTrophy2GetTrophyInfo`, `sceNpTrophy2GetTrophyInfoArray` and `sceNpTrophy2GetGameInfo` for a created context, by `sceNpTrophy2GetGroupInfo` and `sceNpTrophy2GetGroupInfoArray` when the title's trophy data is loaded, by a `sceNpUniversalDataSystemPostEvent` of an `_UnlockTrophy` that the trophy data does not refuse, and by a statistic change when the trophy data is loaded; the stat record by a `sceNpUniversalDataSystemPostEvent` of an event that a statistics rule copies and by the trophy queries of a progressive trophy with a statistic. What the console does with a damaged record is not known. diff --git a/docs/dev/debt/libSceNpTus.md b/docs/dev/debt/libSceNpTus.md new file mode 100644 index 0000000000..141e50a1cc --- /dev/null +++ b/docs/dev/debt/libSceNpTus.md @@ -0,0 +1,116 @@ +# 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. +- [sceNpTusSetThreadParam](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTssCreateNpTitleCtx](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTssCreateNpTitleCtxA](../../../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 +- [sceNpTusGetMultiUserVariable](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsVariable](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsVariableAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariable](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariable](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotData](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotDataVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotDataAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotDataVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusSetDataAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusSetDataVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatus](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatus](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsDataStatus](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsDataStatusAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTssGetSmallStorage](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTssGetSmallStorageAsync](../../../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 +- [sceNpTusAddAndGetVariableA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataForCrossSaveAsync](../../../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 +- [sceNpTusGetMultiUserVariableA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync](../../../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 +- [sceNpTusSetDataA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusSetDataAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusSetDataAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusSetDataAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotDataA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotDataAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableForCrossSaveVUserAsync](../../../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. +- [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/libSceNpWebApi2.md b/docs/dev/debt/libSceNpWebApi2.md new file mode 100644 index 0000000000..14eab1d91c --- /dev/null +++ b/docs/dev/debt/libSceNpWebApi2.md @@ -0,0 +1,15 @@ +# libSceNpWebApi2 technical debt + +### Silent stubs + +- [sceNpWebApi2SetRequestTimeout](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - the `(request id, timeout)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_web_api2/np_web_api2.cpp). Pinball FX (PPSA08755) calls it while its main menu loads. It succeeds for any request id and stores nothing: no request reaches a service, as `sceNpWebApi2SendRequest` fails with `UNAVAILABLE` at once, so no timeout can expire. shadPS4 returns `LIB_CONTEXT_NOT_FOUND`, `USER_CONTEXT_NOT_FOUND` or `REQUEST_NOT_FOUND` for an id it does not know; this library keeps no context or request table (`sceNpWebApi2CreateRequest` hands out counter values, and `sceNpWebApi2AbortRequest`, `sceNpWebApi2AddHttpRequestHeader` and `sceNpWebApi2DeleteRequest` also accept any id), so those errors are not reported until the request functions track their ids together + +### Unknown function info + +- [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. +- [sceNpWebApi2PushEventAbortHandle](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2, NID `1OLgvahaSco`) - the two signed 32-bit handle arguments and return type follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/np/np_web_api2/np_web_api2.cpp) (PS4); the PS5 signature and abort behavior are unverified. PPSA01736 imports it. An unknown library context throws, as in `sceNpWebApi2PushEventSetHandleTimeout`. The handle is not checked and nothing is aborted, as push event handles are not tracked and nothing waits on a handle offline +- [sceNpWebApi2GetMemoryPoolStats](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - imported by PPSA21703's Unity PSN plugin. The signature `(int libCtxId, stats*)` and the layout of the Http2 statistics above (measured on a PS5 for Http2, Ssl and Net) are assumed for this library, not measured. The pool size is the one given to the initializer that created the context, and the peak and current use are 0. The error codes are unknown, so a null output or an unknown context throws +- [sceNpWebApi2PushEventSetHandleTimeout](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - imported by PPSA01317, which fails to load without it. The signature `(int libCtxId, int handleId, uint32_t timeout)` follows `sceNpWebApiSetHandleTimeout` of the PS4 libSceNpWebApi in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_web_api.cpp) and is unverified on the PS5. An unknown library context throws, as in `sceNpWebApi2GetMemoryPoolStats`, since its error code is unknown. The handle is not checked, as push event handles are not tracked (`sceNpWebApi2PushEventDeleteHandle` accepts any handle), and the timeout is not kept, as nothing waits on a handle offline diff --git a/docs/dev/debt/libSceOpusCeltDec.md b/docs/dev/debt/libSceOpusCeltDec.md new file mode 100644 index 0000000000..66de7596b2 --- /dev/null +++ b/docs/dev/debt/libSceOpusCeltDec.md @@ -0,0 +1,10 @@ +# libSceOpusCeltDec technical debt + +### Unknown function info + +- [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. +- [sceOpusCeltDecCreateEx, sceOpusCeltDecDecode, sceOpusCeltDecDestroy, sceOpusCeltDecGetSize, sceOpusCeltDecInitialize, sceOpusCeltDecTerminate](../../../core/libs/prx/libSceOpusCeltDec/Export.cpp) (libSceOpusCeltDec) - imported by PPSA99001. Their signatures are assumed to match libSceOpusDec's in [ProsperoRadio](https://github.com/blackbearreloaded/ProsperoRadio/blob/2c4029c3a8f43e9e1dfef98374d94d99b14d745d/src/opus_decoder.cpp), and the 640-byte state size, context handling and byte-count return are taken from [libSceOpusDec](../../../core/libs/prx/libSceOpusDec/Export.cpp); none of them was measured on a console. PPSA99001 opens this decoder only as a fallback when libSceOpusDec reports a retryable error for a CELT packet, which libSceOpusDec here never returns, so the fallback is not reached + +### Functional + +- [sceOpusCeltDecDecode](../../../core/libs/prx/libSceOpusCeltDec/Export.cpp) (libSceOpusCeltDec) decodes with the FFmpeg decoder of [libSceOpusDec](../../../core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp) and shares its limits. Packets whose TOC configuration is below 16 (SILK and hybrid) throw: whether the console decodes them or returns an error code is unknown. Contexts and decoder states are separate from libSceOpusDec's diff --git a/docs/dev/debt/libSceOpusCeltEnc.md b/docs/dev/debt/libSceOpusCeltEnc.md new file mode 100644 index 0000000000..6f232c9cee --- /dev/null +++ b/docs/dev/debt/libSceOpusCeltEnc.md @@ -0,0 +1,5 @@ +# libSceOpusCeltEnc technical debt + +### Unknown function info + +- [sceOpusCeltEncCreate, sceOpusCeltEncCreateEx, sceOpusCeltEncCtl, sceOpusCeltEncDestroy, sceOpusCeltEncEncode, sceOpusCeltEncEncodeFloat, sceOpusCeltEncEncodeFloatWithPriority, sceOpusCeltEncEncodeWithPriority, sceOpusCeltEncGetSize, sceOpusCeltEncInitialize and sceOpusCeltEncTerminate](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) (libSceOpusCeltEnc) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceOpusCeltEnc.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/libSceOpusDec.md b/docs/dev/debt/libSceOpusDec.md new file mode 100644 index 0000000000..fa6aa1dfd1 --- /dev/null +++ b/docs/dev/debt/libSceOpusDec.md @@ -0,0 +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 mono/stereo signed-16 decoding at the creation rates the research lists: 8000, 12000, 16000, 24000 and 48000 Hz. The six signatures, 640-byte opaque state allocation and byte-count return follow [public PS5 decoder research](https://github.com/blackbearreloaded/ps5-hardware-audio-decoding/blob/26be5a12f92b2cdc67f8a9e082fd69ae0e9a35a2/docs/DECODING.md#direct-opus-abi) and [ProsperoRadio](https://github.com/blackbearreloaded/ProsperoRadio/blob/2c4029c3a8f43e9e1dfef98374d94d99b14d745d/src/opus_decoder.cpp). Console PCM equivalence and error codes are unverified. Other sample rates, packet loss concealment, invalid inputs and termination with live decoders throw. Context identifiers and host decoder state are private registries; the firmware's opaque state layout and error recovery are unknown. +- [sceOpusDecDecode](../../../core/libs/prx/libSceOpusDec/Export.cpp) (libSceOpusDec) returns exactly the packet's duration, as libopus does. FFmpeg resamples narrowband SILK from 8 kHz and holds back 24 samples of the first narrowband packet after the decoder starts or after a packet of another bandwidth, releasing them with the next packet that is not narrowband. The decoder keeps FFmpeg's output in order and pads the start with silence, so such a packet begins with 24 samples (0.5 ms) of silence and the output after it is 24 samples late; libopus has no such gap +- [sceOpusDecDecode](../../../core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp) (libSceOpusDec, libSceOpusCeltDec) decodes at 48000 Hz and converts to a lower creation rate with FFmpeg's resampler, where libopus decodes at that rate directly. The resampler's filter delay is padded with silence at the start of the stream like the narrowband delay above, so the output is late by that delay and is not sample-identical to libopus; which of the two the console matches is not measured diff --git a/docs/dev/debt/libSceOpusSilkEnc.md b/docs/dev/debt/libSceOpusSilkEnc.md new file mode 100644 index 0000000000..3e129c1e1f --- /dev/null +++ b/docs/dev/debt/libSceOpusSilkEnc.md @@ -0,0 +1,5 @@ +# libSceOpusSilkEnc technical debt + +### Unknown function info + +- [sceOpusSilkEncCancel, sceOpusSilkEncCreate, sceOpusSilkEncCreateEx, sceOpusSilkEncCtl, sceOpusSilkEncDestroy, sceOpusSilkEncEncode, sceOpusSilkEncEncodeFloat, sceOpusSilkEncEncodeFloatWithPriority, sceOpusSilkEncEncodeFloatWithPriorityAndTimeout, sceOpusSilkEncEncodeWithPriority, sceOpusSilkEncEncodeWithPriorityAndTimeout, sceOpusSilkEncGetSize, sceOpusSilkEncInitialize and sceOpusSilkEncTerminate](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) (libSceOpusSilkEnc) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceOpusSilkEnc.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/libScePad.md b/docs/dev/debt/libScePad.md new file mode 100644 index 0000000000..c52607d8ee --- /dev/null +++ b/docs/dev/debt/libScePad.md @@ -0,0 +1,166 @@ +# libScePad technical debt + +### Unknown function info + +- [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 +- PPSA07411 PS VR2 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [scePadVrControllerSetVibration, scePadVrControllerSetVibrationMode and scePadVrControllerReadCombinedState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) and [sceVrTracker2ResetLocalCoordinate](../../../core/libs/prx/libSceVrTracker2/Export.cpp) (libSceVrTracker2) +- [scePadSetVibrationMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - modes 1 to 3 and 5 are accepted and stored. On a PS5 (firmware 13.60) the call returns `0x80920005` before `scePadInit`, then rejects a mode of 0 or below with `INVALID_ARG` before it checks the handle, and returns `INVALID_HANDLE` for a bad handle with any higher mode; whether an open handle accepts modes above 3 was not measured, since the payload could not open a pad; 5 is accepted because PPSA25286 passes it on its open handle at start-up; the meanings of modes 3 and 5 are unknown (PPSA21480 calls it with 3 right after `scePadOpen` and aborts on failure; [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Pad/Pad.cs) names only 1 and 2). The stored mode has no other effect +- [scePadIsRemoteController](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the name hashes to NID `fCWdlnmB1Ks`; the signature `(int handle, bool* remote)` comes from PPSA16064, which zeroes a byte, passes its address after `scePadGetControllerInformation` and ignores the result. The error codes follow the other pad calls. Controllers are local devices, so the answer is always false +- [scePadSetAngularVelocityBiasCorrectionState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - inferred signature `(int handle, bool enabled)` follows `scePadSetTiltCorrectionState` and `scePadSetAngularVelocityDeadbandState`; an invalid handle returns `PAD_ERROR_INVALID_HANDLE`, disabling is a no-op, and enabling throws because bias correction is not modelled +- [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 +- [scePadSetTriggerEffectExt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadLastLotusButtonData](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadReadStateExt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetExtensionReport](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadGetExtensionUnitInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadEnableExtensionPort](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadEnableSpecificDeviceClass](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadOpenExt2](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadDeviceOpen](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadGetDataInternal](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetUserColor](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadDisableVibration](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetVibrationStrength](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetLightBarBaseBrightness](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetLightBarBlinking](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadGetIdleCount](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadGetControllerIdleCount](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetPowerSavingState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetAutoPowerOffCount](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadEnableUsbConnection](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadEnableAutoDetect](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadEnablePlayPauseEmulation](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadDisableTrackingLedBatterySaver](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetLoginUserNumber](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadGetCapability](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadShareOutputData](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadShareOutputDataWithFunctionType](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetVibrationForce](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadGetVersionInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadGetControllerType](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadDisableDataConversion](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetButtonRemappingInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetProcessPrivilegeOfButtonRemapping](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetForceIntercepted](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadIsLightBarBaseBrightnessControllable](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSwitchConnection](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadDisableTriggerEffect](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetTriggerEffectStrength](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadGetTriggerEffectInternalState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetUserNumber](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadDisableTransportButtons](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetLotusIrCodeSetStart](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetLotusIrCodeSetData](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetLotusIrCodeSetEnd](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadGetLotusIrCodeSetResult](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetLotusEnableIrKey](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetLotusDisableIrKey](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadGetUsbDetectionState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadUiReadState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadUiReadStateForTuning](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetStickTriggerTuningMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetStickTuningParam](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetTriggerTuningParam](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadGetProfileHunt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetProfileHunt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadResetHunt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadGetProfileInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadGetActiveProfileInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadDisableActiveProfileChange](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadDisableActiveProfileChangeByDeviceId](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetActiveProfileSlot](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadGetSavedSettingInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetSavedSettingInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadVrControllerGetPhysicalUsbConnectionStatus](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadParseUniqueDataLeonardo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadGetControllerTypeLeonardo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetControllerTypeLeonardo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadGetProfileLeonardo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetProfileLeonardo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadResetLeonardo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetProfileEditMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadGetProfileEditMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadGetProfileEditModeLeonardo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetSelectButtonCustomizeMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadGetSelectButtonCustomizeMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetToggleAllReleaseLeonardo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadEnableControllerIndicator](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadEnableProfileIndicator](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetProfileIndicatorAnimation](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadIsCoPilot](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetIsCoPilot](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadRegisterCollaboInputMgrProcess](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadGetTelemetryInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadGetActiveTime](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadGetDeviceId](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadGetInfoByPortType](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadResetLightBarAll](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadResetLightBarAllByPortType](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetLightBarForTracker](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadReadForTracker](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadReadBlasterForTracker](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadResetOrientationForTracker](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadGetMotionTimerUnit](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetVrTrackingMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadGetMotionSensorPosition](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadGetSphereRadius](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadIsValidHandle](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadIsDS4Connected](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadIsBlasterConnected](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadIsMoveConnected](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadGetBluetoothAddress](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadIsMoveReproductionModel](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadReadAstonForTracker](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadGetAstonModel](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadMbusInit](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadMbusTerm](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadConnectPort](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadDisconnectPort](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadDisconnectDevice](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadGetDeviceInfo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetProcessFocus](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadVertualDeviceAddDevice](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadVirtualDeviceAddDevice](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadVirtualDeviceDeleteDevice](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadVirtualDeviceInsertData](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadVirtualDeviceInsertTriggerInternalState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadVirtualDeviceGetRemoteSetting](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadVirtualDeviceSetTriggerEffectSetting](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadVirtualDeviceDisableButtonRemapping](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetShareButtonMaskForRemotePlay](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetPS4BcVibrationMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetVirtualDeviceComponentInformation](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadEnablePs4CompatibleMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadGetLicenseControllerInformation](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadReadHistory](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadStartRecording](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadStopRecording](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadRemoteControlInsertData](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadRemoteControlInsertDataForAston](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadGetFeatureReport2](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetFeatureReport2](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadSetMuteLed](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadBondUpdateInit](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadBondUpdateIsReady](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadBondUpdateStart](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadBondUpdateGetStatus](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadBondUpdateSendData](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadBondUpdateResetBond](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadBondUpdateGetReconnectStatus](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadBondUpdateTerm](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadControllerUpdateInit](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadControllerUpdateIsUpdateNeeded2](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadControllerUpdateIsReady](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadControllerUpdateStart](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadControllerUpdateGetStatus](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadControllerUpdateSendData](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadControllerUpdateResetController](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadControllerUpdateGetReconnectStatus](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature +- [scePadControllerUpdateTerm](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature + +### 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. The console's values are not measured diff --git a/docs/dev/debt/libScePlayGo.md b/docs/dev/debt/libScePlayGo.md new file mode 100644 index 0000000000..e1834da340 --- /dev/null +++ b/docs/dev/debt/libScePlayGo.md @@ -0,0 +1,8 @@ +# libScePlayGo technical debt + +### Unknown function info + +- [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) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePlayGo.c); the `(uint32_t, uint64_t)` prototype follows [ChonkyStation4 `ScePlayGo.cpp`](https://github.com/liuk7071/ChonkyStation4/blob/c101a23ecbc15e56c6e30d5dd24d3ed8880a957a/ChonkyStation4/OS/Libraries/ScePlayGo/ScePlayGo.cpp#L240), is not measured on a console, and every call throws +- [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 any `type`, i.e. every optional chunk (language pack) supported and installed, like `scePlayGoGetLanguageMask`. The bit layout and the values of `type` are not known; PPSA01748 calls both with type 0 and hides every text language listed in `playgo-scenario.json` `chunkSupportedLanguages` when the bitmask is 0. The console's answer for a package that ships no optional chunks is unverified. diff --git a/docs/dev/debt/libScePlayGoDialog.md b/docs/dev/debt/libScePlayGoDialog.md new file mode 100644 index 0000000000..7e06e1309c --- /dev/null +++ b/docs/dev/debt/libScePlayGoDialog.md @@ -0,0 +1,5 @@ +# libScePlayGoDialog technical debt + +### Unknown function info + +- [scePlayGoDialogGetResult](../../../core/libs/prx/libScePlayGoDialog/Export.cpp) (libScePlayGoDialog) - checks that the dialog has reached STATUS_FINISHED and returns 0x80B80005 (COMMON_DIALOG_ERROR_NOT_FINISHED) when called before the dialog completes, following the other common dialog modules in the tree (MsgDialog, SigninDialog, PlayerSelectionDialog, SaveDataDialog). shadPS4's scePlayGoDialogGetResult is a dummy that always returns OK and provides no evidence for either value; the exact return code and intermediate state check have not been verified against a real hardware trace or title measurement. diff --git a/docs/dev/debt/libScePlayerInvitationDialog.md b/docs/dev/debt/libScePlayerInvitationDialog.md new file mode 100644 index 0000000000..4e249f6289 --- /dev/null +++ b/docs/dev/debt/libScePlayerInvitationDialog.md @@ -0,0 +1,5 @@ +# libScePlayerInvitationDialog technical debt + +### Functional + +- [libScePlayerInvitationDialog](../../../core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp) simulates dialog completion without displaying UI or sending invitations, so `scePlayerInvitationDialogGetResult` reports `USER_CANCELED`; its parameter ABI remains unverified. The 40-byte result layout (error code, result, 32 reserved bytes) is taken from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Dialog/PlayerInvitationDialog.cs) and the error codes from the other common dialogs, not confirmed on a title. diff --git a/docs/dev/debt/libScePlayerSelectionDialog.md b/docs/dev/debt/libScePlayerSelectionDialog.md new file mode 100644 index 0000000000..c4936f8740 --- /dev/null +++ b/docs/dev/debt/libScePlayerSelectionDialog.md @@ -0,0 +1,9 @@ +# libScePlayerSelectionDialog technical debt + +### Silent stubs + +- [libScePlayerSelectionDialog](../../../core/libs/prx/libScePlayerSelectionDialog/Export.cpp) - the player-selection dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled. `GetResult` writes only the first `int32` (`USER_CANCELED`); the rest of `ScePlayerSelectionDialogResult` (player list pointer and length) is not filled. EA SPORTS FC 27 (PPSA34015) fails to load `libScePlayerSelectionDialog.prx` without `Initialize`, `Open`, `Close`, `GetStatus`, `UpdateStatus` and `GetResult` (`unresolved ELF import` / `GetLastError: 127`) + +### 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..2eca53adb5 --- /dev/null +++ b/docs/dev/debt/libScePngDec.md @@ -0,0 +1,6 @@ +# libScePngDec technical debt + +### Unknown function info + +- [scePngDecQueryMemorySize, scePngDecCreate](../../../core/libs/prx/libScePngDec/Export.cpp) (libScePngDec) - the work memory is a fixed 0x20 bytes, because decoding uses host memory; the PS5 module asks for `((max_image_width + 0x47) & ~7) * (8 << attribute)` plus a constant, as read from the 3.20 module by prosper. A null parameter returns `0x80690003`; prosper returns `0x80690001` when it cannot read the parameter, which was not measured. Closes when: measured on a PS5 +- [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 new file mode 100644 index 0000000000..0ba7edc2bf --- /dev/null +++ b/docs/dev/debt/libScePngEnc.md @@ -0,0 +1,5 @@ +# libScePngEnc technical debt + +### Unknown function info + +- [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 diff --git a/docs/dev/debt/libScePosixForWebKit.md b/docs/dev/debt/libScePosixForWebKit.md new file mode 100644 index 0000000000..c57da143e9 --- /dev/null +++ b/docs/dev/debt/libScePosixForWebKit.md @@ -0,0 +1,12 @@ +# libScePosixForWebKit technical debt + +### 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 + +### Functional + +- [mkstemp and isatty](../../../core/libs/prx/libScePosixForWebKit/Filesystem.cpp) (libScePosixForWebKit) follow FreeBSD's [temporary-file API](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/stdio/mktemp.c) and [terminal check](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/gen/isatty.c), without console verification. `mkstemp` replaces all trailing `X` characters, accepts a fixed name, and creates files through the guest path resolver with exclusive read/write access and mode 0600. Windows file permissions follow the existing kernel file implementation. `isatty` tests Windows console handles rather than all character devices; files, pipes and guest sockets return `ENOTTY`, closed descriptors return `EBADF`. +- [arc4random](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) returns four bytes of host OS entropy (`getentropy`, `rand_s` on Windows), as `arc4random_buf` does, uniform over the 32-bit range like FreeBSD 11.4's RC4 keystream in [arc4random.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/arc4random.c). The console's generator and its reseeding are unknown; `arc4random_uniform`, `arc4random_stir` and `arc4random_addrandom` are not in the module's export set and are absent +- [fnmatch](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) follows FreeBSD 11 [fnmatch.c](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/gen/fnmatch.c) in the C locale: each byte is one character, `FNM_CASEFOLD` folds ASCII letters only, and bracket ranges compare byte values. `[:class:]` names are not character classes: as in FreeBSD 11 through 14, `[[:digit:]]` is the set of `[`, `:`, `d`, `i`, `g`, `t` followed by a literal `]` (classes were added to `fnmatch.c` after 14). Multibyte characters and locale collation are not modelled. A null pattern or string throws +- [getservbyname](../../../core/libs/prx/libScePosixForWebKit/Resolver.cpp) (libScePosixForWebKit) searches a built-in copy of 37 lines of FreeBSD 11.4's [etc/services](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/etc/services) (ftp-data, ftp, ssh, telnet, smtp, domain, tftp, http, pop3, ntp, imap, https, rtsp, submission, imaps, pop3s with their aliases and protocols, in file order) the way [getservent.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/net/getservent.c) searches the file: case-sensitive name or alias, a null protocol takes the first match, the port in network byte order, the result in per-thread storage. The console's `/etc/services` is unknown, so any other service returns null without setting `errno`, as for a name the file lacks. `getaddrinfo` still resolves service names through the host's service database diff --git a/docs/dev/debt/libSceProprietaryVoiceChatHelperPrx.md b/docs/dev/debt/libSceProprietaryVoiceChatHelperPrx.md new file mode 100644 index 0000000000..cf1e21965c --- /dev/null +++ b/docs/dev/debt/libSceProprietaryVoiceChatHelperPrx.md @@ -0,0 +1,5 @@ +# libSceProprietaryVoiceChatHelperPrx technical debt + +### Silent stubs + +- [libSceProprietaryVoiceChatHelperPrx](../../../core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp) implements only the no-system-voice-chat path: the host has no system voice chat for a title's own voice chat to announce itself to, so `sceProprietaryVoiceChatHelperInitialize` and `sceProprietaryVoiceChatHelperTerminate` only track whether the library is initialized, and `sceProprietaryVoiceChatHelperSetVoiceChatState` returns success and does nothing: there is nothing to notify, and its state argument is unknown, so no state is recorded. `sceProprietaryVoiceChatHelperGetVoiceChat` and `sceProprietaryVoiceChatHelperGetVoiceChatUsageState` throw, because nothing they could report is known: with the state argument unreadable there is no recorded usage state, and whether the console returns the state or writes it through an output pointer, and how it encodes it, is unknown. Initializing twice, and any call before `sceProprietaryVoiceChatHelperInitialize` or after `sceProprietaryVoiceChatHelperTerminate`, throws, as the console's error codes for this library are unknown. Whether the console checks initialization order at all is unverified diff --git a/docs/dev/debt/libScePsml_debug.md b/docs/dev/debt/libScePsml_debug.md new file mode 100644 index 0000000000..d30a44b812 --- /dev/null +++ b/docs/dev/debt/libScePsml_debug.md @@ -0,0 +1,14 @@ +# libScePsml_debug technical debt + +### Unknown function info + +- [scePsmlMfsrGetSharedResourcesInitRequirement, scePsmlMfsrCreateSharedResources, scePsmlMfsrReleaseSharedResources, scePsmlMfsrGetContextBufferRequirement800M3_2, scePsmlMfsrCreateContext800M3_2, scePsmlMfsrReleaseContext](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - signatures from the calls in PPSA08330 (0x5724b6-0x5727e8, 0xc8697) and PPSA03016 (0x43dcf4d-0x43dd69a): a `{block bytes, alignment, block count}` requirement, a 32-byte shared-resources parameter with the type at 0 and the `{address, bytes}` block list at 8 and 16, a context parameter with the block list at 8 and 16 and the shared-resources handle at 0x38, and 8-byte handles written through the first argument and passed back by value. The other context parameter fields (a 16-bit size pair at 0x18, two descriptor pointers at 0x20 and 0x28) are not read. The requirement of four 2 MiB blocks, 2 MiB aligned, is what PPSA08330 v06.006 allocates regardless of the returned count; Sony's values are unknown ([KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/libPsml.cpp) derives 52 blocks for type 0; its layout differs from these calls: it writes a 0x30-byte object through the first argument, where PPSA08330 passes one of two adjacent 8-byte handle globals, and reads the shared resources at context parameter offset 8, where both titles pass the block list). Only type 0 is accepted. The error codes for null pointers, short block lists and foreign handles are unknown, so those calls throw +- [scePsmlMfsr2CreateContext, scePsmlMfsr2CreateSharedResources, scePsmlMfsr2GetContextInitRequirement, scePsmlMfsr2GetDispatchPackets, scePsmlMfsr2GetDispatchPacketsSizeInDwords, scePsmlMfsr2GetSharedResourcesInitRequirement, scePsmlMfsr2Init, scePsmlMfsr2ReleaseContext, scePsmlMfsr2ReleaseSharedResources](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_mfsr2) - unknown signatures, imported by PPSA23566; declared without parameters +- [scePsmlMfsrGetContextBufferRequirement1100, scePsmlMfsrCreateContext1100, scePsmlMfsrGetDispatchMfsrPacket1100](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_debug) - argument layouts unknown; PPSA26344 imports but does not call the first two and at `0x87EFA` passes `(rdi, rsi=&stack, rdx)` as three pointers to `GetDispatchMfsrPacket1100`; before `scePsmlMfsrInit` they return 0x8A810001 (not initialized) and after it they throw. `scePsmlMfsrCreateContext1300` and `scePsmlMfsrGetDispatchMfsrPacket900`, `1000` and `1300` take the same shapes (context slot and parameter; context, command buffer and parameter) and behave the same way. PPSA08330 and PPSA03016 pass `GetDispatchMfsrPacket900`/`1000` the context handle, a `{cursor, size in dwords}` command buffer and the dispatch parameters, and advance their cursor by the size `scePsmlMfsrGetDispatchMfsrPacketSizeInDwords` returned. Whether the 1100 and 1300 versions take these arguments, and the console's error, are unverified +- [scePsmlMfsrGetDispatchMfsrPacket1000](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - imported by PPSA25344. libScePsml and libScePsml_debug are built from the same source +- [scePsmlMfsrCreateContext1300, scePsmlMfsrGetDispatchMfsrPacket1300](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - imported by PPSA28416 next to `scePsmlMfsrGetContextBufferRequirement1100`. The names are not in the aerolib NID list; they hash to the imported NIDs `kLF7WtQ1-Dg` and `DDVUyPyYHTA` (`tools/nid_names.py`) +- [scePsmlMfsrEnableSpecularAA, scePsmlMfsrEnableQuality8K](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_specularAA and libScePsml_quality8K) - imported by PPSA01317, which fails to load without them; signatures unknown, so they throw. The names are not in the aerolib NID list; they hash to the imported NIDs `xNZM9Y5lf-M` and `lcZD9tJraAw` (`tools/nid_names.py`) + +### Functional + +- [scePsmlMfsrInit](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - succeeds and only sets the initialized state; the shared resources and contexts are bookkeeping objects that use none of the guest blocks. Whether Sony's library fails on hardware without PSSR is unknown. PPSA08330 calls the setup unconditionally and only uses a context when its cached `sceKernelIsTrinityMode` result is set (0x3c643d, 0x567f97; otherwise it stores a -0.25 mip bias itself at 0x567fe2), which AnyPS5 reports as 0, so the dispatch, mip bias and capture calls still throw diff --git a/docs/dev/debt/libSceRazorCpu.md b/docs/dev/debt/libSceRazorCpu.md new file mode 100644 index 0000000000..3a4d1b15d4 --- /dev/null +++ b/docs/dev/debt/libSceRazorCpu.md @@ -0,0 +1,16 @@ +# libSceRazorCpu technical debt + +### 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 +- [sceRazorCpuDisableFiberUserMarkers, sceRazorCpuBeginLogicalFileAccess and sceRazorCpuEndLogicalFileAccess](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signatures and meanings from SharpProspero ([fiber markers](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Debug/RazorCpu.cs#L73-L79), [logical file access](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Debug/RazorCpu.cs#L189-L209)): the first makes later markers carry the thread instead of the fiber, the other two attribute the thread's file reads and writes to a named logical file. Both only change what a capture records, so without one they do nothing, and the two file-access calls return 0 for any arguments. Whether the console checks the operation (0 read, 1 write) or an unmatched end without a capture is unverified +- [sceRazorCpuInitDataTags, sceRazorCpuShutdownDataTags, sceRazorCpuTagBuffer, sceRazorCpuTagArray, sceRazorCpuResizeTaggedBuffer and sceRazorCpuUnTagBuffer](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signatures from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Debug/RazorCpu.cs#L130-L187), which documents them as the data sampler's buffer-tag table and the tags in it. Without a capture there is no data sampler, so they neither read the caller's storage nor keep the tags, and return 0 for any arguments; the console's errors for a tag before `sceRazorCpuInitDataTags`, an unknown buffer or a full table are unverified + +### 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 +- [sceRazorCpuNamedSync](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - NID `XCuZoBSVFG8` is named by [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/razor_cpu/razor_cpu.cpp); the NID and a stub returning 0 come from there, but its stub declares no arguments; the `const char* label` parameter (NUL-terminated, up to 16384 bytes) comes from [SharpProspero](https://github.com/SvenGDK/SharpProspero), which documents it as the frame-boundary label. Without a capture it does nothing and returns 0; the label is not read, and the capture behavior and the real return values are unverified +- [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..e599cc296d --- /dev/null +++ b/docs/dev/debt/libSceRegMgr.md @@ -0,0 +1,6 @@ +# libSceRegMgr technical debt + +### Unknown function info + +- [sceRegMgrNonSysGetInt](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr, NID `dKeshzt29G4`) - observed as an import needed for module loading (issue 4720); its `(int64_t key, int32_t unk, int32_t* data)` signature follows [ChonkyStation4](https://github.com/liuk7071/ChonkyStation4/blob/c101a23ecbc15e56c6e30d5dd24d3ed8880a957a/ChonkyStation4/OS/Libraries/SceRegMgr/SceRegMgr.cpp#L38-L47), not measured on a console, so every call throws +- [sceRegMgrGetBin and sceRegMgrSetBin](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceRegMgr.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. The string and integer prototypes follow [SharpProspero's SystemSettings bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Platform/SystemSettings.cs) diff --git a/docs/dev/debt/libSceRtc.md b/docs/dev/debt/libSceRtc.md new file mode 100644 index 0000000000..f0955cd1a3 --- /dev/null +++ b/docs/dev/debt/libSceRtc.md @@ -0,0 +1,21 @@ +# 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 zone name table matches every name of 1 to 3 uppercase letters as a payload process on PS5 firmware 13.60 parsed them in the RFC 2822 form; of the longer names only real ones were tried (ACDT, AEST, CEST, NZDT, MESZ and others). Any other name of 4 or more letters is assumed to take the military letter offset of its first character, so a longer 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 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. +- [sceRtcFormatRFC2822, sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - only shadPS4 (PS4) has bodies. A null tick returns `INVALID_POINTER` like the existing `sceRtcFormatRFC3339`, where shadPS4 formats the current time instead; offsets outside ±1439 minutes return `INVALID_VALUE` like `sceRtcFormatRFC3339`; the local-time variants use the host's offset at the given instant (shadPS4 uses `sceKernelGettimezone`, the current offset) +- [sceRtcGetDosTime, sceRtcSetDosTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - bit layout from shadPS4 (PS4, the standard FAT date/time). A year outside 1980-2107 makes `GetDosTime` return `INVALID_YEAR` after writing 0 for an earlier one and `0xff9fbf7d` (2107-12-31 23:59:58, the largest encodable time) for a later one, and `SetDosTime` decodes the year as the unsigned 7-bit FAT field (1980-2107) without validating the fields, as the disassembly-derived [fpPS4](https://github.com/red-prig/fpPS4) reimplementation of the PS4 library does; not confirmed on a PS5 title, and shadPS4 instead masks out-of-range years into the packed value and sign-extends the 16-bit date +- [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 new file mode 100644 index 0000000000..19757221c0 --- /dev/null +++ b/docs/dev/debt/libSceRudp.md @@ -0,0 +1,16 @@ +# libSceRudp technical debt + +### Silent stubs + +- [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; whether the real calls take parameters is unknown +- [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) (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 + +### 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 +- [sceRudpGetOption](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) takes `(int ctx_id, int option, void* value, uint32_t len)`, taken from the bundled `Media/Plugins/SceRudp.prx` of PPSA06888: its two exported wrappers pass their first three arguments through unchanged and set `len` to 4, the same shape as its `sceRudpSetOption` call, which passes a 4-byte value. The option codes, the defaults and the error results are unknown, so it still throws +- PPSA06888 imports with unknown signatures: [sceRudpGetRemoteInfo](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp), imported by its bundled `Media/Plugins/SceRudp.prx` and not called during start-up. The third argument is a pointer to a 4-byte buffer seeded from the byte its wrapper reads through the second argument, which does not show what the arguments mean. The managed `SceRudp.Context.GetRemoteInfo()` in the title's Unity metadata takes no parameters and returns a `SceRudp.AddrAndVport`; its native arguments are not visible there. 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 +- [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. The signatures, the 8-byte `SceRudpPollEvent`, 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..c941424735 --- /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) (libSceS3da) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceS3da.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/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 new file mode 100644 index 0000000000..db7aeb7660 --- /dev/null +++ b/docs/dev/debt/libSceSaveData.native.md @@ -0,0 +1,24 @@ +# libSceSaveData.native technical debt + +### Unknown function info + +- [sceSaveDataCreateTransactionResource, sceSaveDataDeleteTransactionResource](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - transaction resources return synthetic positive identifiers and validate initialization state; resource parameter validation and transactional rollback semantics on host filesystem storage are unknown +- [sceSaveDataPrepareForTransferring](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native, NID `RvID8BVeg+g`) - declared without parameters as a throwing stub; Minecraft (PPSA17221) imports it beside `sceSaveDataTransferringMountPs4` and `sceSaveDataDirNameSearchPs4` and fails to load without it. The name hashes to the NID but is not in aerolib; it was found by combining words from the libSceSaveData names in aerolib. Its arguments and behaviour are unknown. +- [sceSaveDataTransferringMountPs4, sceSaveDataDirNameSearchPs4](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - arguments assumed to be those of `sceSaveDataTransferringMount` and `sceSaveDataDirNameSearch`: KytyPS5 binds both NIDs to the PS5 functions, and [prosper](https://github.com/mattias800/prosper) captured the same result layout on a title for the search. There is no PS4 save area, so the mount returns `NOT_FOUND` and the search reports zero hits +- [sceSaveDataConvert](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the parameter block (user id at `+0x00`, `SceSaveDataDirName` pointer at `+0x08`, 48 opaque bytes after it) is read from PPSA28416's single call, not confirmed by KytyPS5, shadPS4, fpPS4 or SharpProspero, which have no body for it. Host saves have no size limit, so an existing save returns `SCE_OK` without touching its files and no event is queued; what the console returns for a save that is mounted, already converted or needs no conversion is unknown. The directory name is validated as `sceSaveDataDelete` does +- [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. 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 new file mode 100644 index 0000000000..92ab4e7f4c --- /dev/null +++ b/docs/dev/debt/libSceSaveDataDialog.native.md @@ -0,0 +1,5 @@ +# libSceSaveDataDialog.native technical debt + +### 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. 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..568cb899b6 --- /dev/null +++ b/docs/dev/debt/libSceScreenShot.md @@ -0,0 +1,6 @@ +# libSceScreenShot technical debt + +### Unknown function info + +- [sceScreenShotCapture, sceScreenShotDisable, sceScreenShotDisableNotification, sceScreenShotEnable, sceScreenShotEnableNotification, sceScreenShotGetAppInfo, sceScreenShotGetDrcParam, sceScreenShotIsDisabled, 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. +- [sceScreenShotSetOverlayImage and sceScreenShotSetOverlayImageWithOrigin](../../../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). The PS4 compatibility implementation also exposes the names but declares no parameters; no PS5 prototype was found. The declarations have no parameters solely to resolve known imports and make no ABI claim 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 new file mode 100644 index 0000000000..8ad04d2333 --- /dev/null +++ b/docs/dev/debt/libSceShare.md @@ -0,0 +1,11 @@ +# libSceShare technical debt + +### 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 + +### Functional + +- [sceShareCaptureVideoClipExtended](../../../core/libs/prx/libSceShare/Export.cpp) (libSceShare) - imported by PPSA12544's Share plugin; the signature (extended parameter, request id) and the answer follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/147188b143f67c27d8da396e759c19336f64c105/src/libs/libShare.cpp), not confirmed on a PS5 title. Nothing is captured, so, like `sceShareCaptureScreenshotExtended`, it stores the invalid request id -1 when the pointer is not null and returns `NOT_SUPPORTED` without reading the extended parameter, whose layout is unknown diff --git a/docs/dev/debt/libSceSharePlay.md b/docs/dev/debt/libSceSharePlay.md new file mode 100644 index 0000000000..e6c2086083 --- /dev/null +++ b/docs/dev/debt/libSceSharePlay.md @@ -0,0 +1,10 @@ +# libSceSharePlay technical debt + +### 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 + +### Functional + +- [sceSharePlayGetCurrentConnectionInfoA](../../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. +- [sceSharePlayProhibitController](../../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - accepts 0 (allow) and 1 (prohibit) and returns 0, following [sharpemu](https://github.com/sharpemu/sharpemu/blob/a2983692930564815a77f81e74c151268f5b935f/src/SharpEmu.Libs/Share/SharePlayExports.cs#L9-L31). The setting is not stored, as no Share Play session or visitor controller exists and no export reads it back. sharpemu rejects any other value with its synthetic `0x80020003`, not a Share Play code, so other values throw. Like the other exports here it does not require `sceSharePlayInitialize`. The `int` argument type and the console's error for other values are unverified diff --git a/docs/dev/debt/libSceShellUIUtil.md b/docs/dev/debt/libSceShellUIUtil.md new file mode 100644 index 0000000000..f07f584e14 --- /dev/null +++ b/docs/dev/debt/libSceShellUIUtil.md @@ -0,0 +1,5 @@ +# libSceShellUIUtil technical debt + +### Unknown function info + +- [sceShellUIUtilCreateTaskToken, sceShellUIUtilGetAppUrl, sceShellUIUtilInitialize, sceShellUIUtilLaunchByUri, sceShellUIUtilLaunchTask and sceShellUIUtilTerminate](../../../core/libs/prx/libSceShellUIUtil/Export.cpp) (libSceShellUIUtil) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceShellUIUtil.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/libSceSigninDialog.md b/docs/dev/debt/libSceSigninDialog.md new file mode 100644 index 0000000000..5353db25a6 --- /dev/null +++ b/docs/dev/debt/libSceSigninDialog.md @@ -0,0 +1,5 @@ +# libSceSigninDialog technical debt + +### 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 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/libSceSsl.md b/docs/dev/debt/libSceSsl.md new file mode 100644 index 0000000000..597a2246f7 --- /dev/null +++ b/docs/dev/debt/libSceSsl.md @@ -0,0 +1,10 @@ +# libSceSsl technical debt + +### Unknown function info + +- [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 any other flags (`MsgFlagPartialRead` 1 measured, 2 behaved the same) returns what the first record delivers; 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 +- [sceSslGetSerialNumber, sceSslGetSubjectName, sceSslGetIssuerName, sceSslGetNameEntryCount, sceSslGetNameEntryInfo, sceSslFreeSslCertName, sceSslGetNotBefore, sceSslGetNotAfter, sceSslGetFingerprint](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and context ownership follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L293-L350), with hash types matching its `SceSslFingerprintHashAlgorithm` enum. Operations take a context handle as their first argument and operate on certificate and name handles scoped to that context. `sceSslGetSerialNumber` copies into a caller-allocated output buffer or queries the required length when the buffer is null or capacity is zero; a buffer smaller than the serial number returns `SSL_ERROR_INVALID_ARG` (0x8095177a). Mbed TLS parses the certificate's serial number, validity period (converted to microseconds since Gregorian year 0001 epoch), fingerprint digests and X.509 subject and issuer distinguished names, writing the attribute long names the console writes (`commonName`, `organizationName`, `countryName`, measured on 13.40 through the verify callback's chain handles; numeric strings for attributes Mbed TLS cannot name) and value strings with `valueLen` set to the value's length. An unknown context, unknown or expired certificate handle or invalid certificate name handle throws; null output pointers, unsupported hash types or out-of-range entry indices return `SSL_ERROR_INVALID_ARG` (0x8095177a). `sceSslGetSerialNumber` with a buffer large enough leaves `*sboLen` as the caller set it, as measured on 13.40 with a 32-byte buffer. The error codes, the zero-capacity length query, the NUL-terminated truncation of name entry strings, and the lifetime of name handles (released only by `sceSslFreeSslCertName` or when the context is terminated) have not been measured on a PS5 +- [sceSslLoadCert, sceSslGetCaList, sceSslFreeCaList, sceSslGetCaCerts, sceSslFreeCaCerts, sceSslUnloadCert](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and layouts follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs). PPSA15508/01.002.000 calls `sceSslLoadCert` with one 1,408-byte PEM CA and null client certificate/key arguments; this confirms that call shape, not console error behavior. Mbed TLS 3.6.7 parses DER/PEM chains and checks an optional client certificate/key pair. Imports are retained per context and committed together; a failed import preserves existing credentials. CA queries return independently owned snapshots; unload clears active credentials while outstanding snapshots remain valid. Appending repeated CA imports, snapshot lifetime across unload, memory overhead and error precedence have not been measured on a PS5. Invalid loads, malformed material, exhausted pools and unsupported keys throw rather than inventing console error codes. No root bundle is shipped; an empty context still returns `NOT_FOUND`. The existing null-list error, unknown-context behavior and legacy clearing of foreign raw CA lists remain unchanged. Certificate reader queries reflect active certificates or outstanding snapshots. Loading credentials does not start a TLS connection; `sceSslConnect` verifies the server against the loaded CAs and offers the loaded client certificate +- [sceSslEnableVerifyOption, sceSslDisableVerifyOption, sceSslSetMinSslVersion, sceSslSetAlpn, sceSslGetAlpnSelected](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - 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 throws (not measured). `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 throw from `sceSslGetAlpnSelected`, whose console return was not measured +- [sceSslGetPem](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - static inspection of PPSA28002 confirms the context-first four-argument call with a caller-owned buffer and an in/out 64-bit length. Mbed TLS serializes PEM including its trailing NUL. Buffers smaller than the required encoding throw; an unknown context or foreign or expired certificate handle throws. The exact console PEM length policy, error precedence and truncation behavior have not been measured on a PS5. Existing serial/name, validity and fingerprint readers and their documented uncertainties remain unchanged diff --git a/docs/dev/debt/libSceSysCore.md b/docs/dev/debt/libSceSysCore.md new file mode 100644 index 0000000000..0ebd352ebd --- /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) (libSceSysCore) - the name and NID are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSysCore.c), but no credible prototype was found. The declaration has no parameters solely to resolve a known import and makes no ABI claim 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 new file mode 100644 index 0000000000..e0bc2e09c2 --- /dev/null +++ b/docs/dev/debt/libSceSystemGesture.md @@ -0,0 +1,10 @@ +# libSceSystemGesture technical debt + +### Silent stubs + +- [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. Error codes follow the 0x80D1 values main already uses in this file; KytyPS5 uses 0x8089 (INVALID_ARGUMENT 0x80890001, INVALID_HANDLE 0x80890006), and which one the console returns is not verified. `sceSystemGestureGetTouchRecognizerInformation` returns the gesture type and rectangle given to `sceSystemGestureCreateTouchRecognizer` or `UpdateTouchRecognizerRectangle`, stored in the title's recognizer buffer in a layout private to this file, and always reports `updated_time` 0 because no recognizer is ever updated with input. It throws for a recognizer that was not created by `sceSystemGestureCreateTouchRecognizer` or has no rectangle, since the console's result in those cases is not known + +### Unknown function info + +- [Primitive touch events](../../../core/libs/prx/libSceSystemGesture/Export.cpp) (libSceSystemGesture) - `sceSystemGestureUpdatePrimitiveTouchRecognizer` reads the pad reports the title passes in, `{int32 padHandle, int32 reportNumber, const ScePadData* padDataBuffer, 8 reserved bytes}`, as [fpPS4](https://github.com/red-prig/fpPS4/blob/585d06fa7c04698c41272b95a696a14c4180e13e/src/ps4_libscesystemgesture.pas) records `SceSystemGestureTouchPadData`; `padHandle` is not checked and a report of a pad other than the one the title opened is not told apart. The touch states 0 inactive, 1 begin, 2 active and 3 end are the Vita ones in [RPCS3](https://github.com/RPCS3/rpcs3/blob/38c444cfa1dbe152daa63b1703c12172078cd056/rpcs3/Emu/PSP2/Modules/sceSystemGesture.h), whose recognizer, rectangle and primitive event layouts are the ones main already uses, and no public source states the PS5 values. A primitive keeps the touch id the pad reports, its positions are the pad's touchpad coordinates unscaled, `delta_time` and `elapsed_time` come from the reports' own `timestamp`, and `is_updated` is 1 for a primitive whose state or position changed in the update; the console's units, scaling and `is_updated` rule are unverified. An ended touch stays readable as one `end` event until the next report sampled drops it. A null `pInputData`, a null `padDataBuffer` or a `reportNumber` of 0 or less leaves the events unchanged and returns success, an event buffer shorter than the event count reports as many events as it holds, and `sceSystemGestureGetPrimitiveTouchEventByPrimitiveID` throws for an id that no tracked touch has; what the console answers in those cases is not known +- [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/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md new file mode 100644 index 0000000000..2e396200ca --- /dev/null +++ b/docs/dev/debt/libSceSystemService.md @@ -0,0 +1,107 @@ +# libSceSystemService technical debt + +### Silent stubs + +- [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. 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, time format and enter button stay fixed +- [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the time zone (parameter 4) and summer time (parameter 5) follow the host's local time zone at the moment of the call. Titles compute the local time as UTC plus the time zone in minutes plus 60 minutes when summer time is 1 ([Apollo](https://github.com/bucanero/apollo-ps4/blob/252a9d5a187c463994263009851a179f4f910c49/source/exec_cmd.c#L70-L92), [GoldHEN's frame logger](https://github.com/GoldHEN/GoldHEN_Plugins_Repository/blob/6c3a06f53d9caebadfb0aeed9bb94119012010a7/plugin_src/frame_logger/source/main.c#L60-L82), and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/413d2e3518fb435f40971d238fe0ecae7ee551da/src/SharpProspero/Platform/SystemParameters.cs#L75-L79) calls them the offset from UTC in minutes and whether summer time is in effect), so the time zone is the host's current offset east of UTC less the summer time shift while summer time is in effect: 60 minutes on Linux, and on Windows the C runtime's own summer time bias (`_get_dstbias`), which is also what shifted its local time. For a host zone whose summer time is not one hour, Linux keeps the sum right and the standard offset wrong, and Windows the opposite; how the console reports such a zone is unknown. libkernel's `sceKernelConvertUtcToLocaltime`, `sceKernelConvertLocaltimeToUtc` and `sceKernelGettimezone` still use UTC +- [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 firmware 3.20 pending exports](../../../core/libs/prx/libSceSystemService/PendingExports.cpp) - 475 symbol names are present in [the PS5 3.20 libSceSystemService export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSystemService.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`. +- [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 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 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/libSceTextToSpeech2.md b/docs/dev/debt/libSceTextToSpeech2.md new file mode 100644 index 0000000000..fb8dc582dc --- /dev/null +++ b/docs/dev/debt/libSceTextToSpeech2.md @@ -0,0 +1,10 @@ +# libSceTextToSpeech2 technical debt + +### Silent stubs + +- [libSceTextToSpeech2](../../../core/libs/prx/libSceTextToSpeech2/Export.cpp) has no speech engine: `sceTextToSpeech2Initialize` and the other exports return `EOPNOTSUPP`. PPSA01317 skips its narration set-up when initialization fails + +### 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` diff --git a/docs/dev/debt/libSceUlt.md b/docs/dev/debt/libSceUlt.md new file mode 100644 index 0000000000..b55da9ee7f --- /dev/null +++ b/docs/dev/debt/libSceUlt.md @@ -0,0 +1,19 @@ +# libSceUlt technical debt + +### Unknown function info + +- [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) - `_sceUltUlthreadRuntimeOptParamInitialize`, `_sceUltUlthreadRuntimeCreate` and `_sceUltUlthreadCreate` are assumed to take the same arguments as the existing exports without the underscore, including the trailing build version. `sceUltUlthreadTryJoin` is assumed to return `ULT_ERROR_BUSY` while the ulthread runs, and `sceUltUlthreadRuntimeDestroy` to return `ULT_ERROR_BUSY` while an ulthread created on the runtime is not joined. Neither error has been confirmed on PS5 hardware +- [libSceUlt queues](../../../core/libs/prx/libSceUlt/Export.cpp) - the added `sceUltQueuePop`, `sceUltQueueTryPush`, `sceUltQueueDestroy` and `sceUltQueueDataResourcePoolDestroy` signatures are inferred from the existing push/pop and object APIs. Queue destruction is assumed to reject blocked callers with `ULT_ERROR_BUSY` and otherwise discard queued data; pool exhaustion returns `ULT_ERROR_AGAIN`. These rules, alignment checks and shared slot limits have not been confirmed on PS5 hardware or a title. +- [sceUltQueueCreate, sceUltQueueDataResourcePoolCreate](../../../core/libs/prx/libSceUlt/Export.cpp) - a non-null option parameter is accepted and ignored, as the other Ult create calls already do. [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/00d087b2323bf377fd39b7163a256977383cb232/include/orbis/Ult.h) declares `sceUltQueueOptParamInitialize` and `sceUltQueueDataResourcePoolOptParamInitialize` without their structures; the PS Vita libult in [RPCS3](https://github.com/RPCS3/rpcs3/blob/13aa88c3c4d66ea8799e987f7d762d90943ab17b/rpcs3/Emu/PSP2/Modules/sceUlt.h#L25-L78) defines both as a 16-byte reserved header followed by reserved fields, 128 bytes, with no settings, and [fpPS4](https://github.com/red-prig/fpPS4/blob/5ffbdbbd76b4da6e4b8721743ea491f494b28913/src/ps4_libsceult.pas#L142-L172) keeps them as 128 opaque bytes. That the PS5 structures carry no settings either is assumed from these, not verified +- [_sceUltWaitingQueueResourcePoolCreate, _sceUltSemaphoreCreate, _sceUltMutexOptParamInitialize, _sceUltMutexCreate, _sceUltQueueDataResourcePoolCreate and _sceUltQueueCreate](../../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) forward to the exports without the underscore; that their argument lists match is inferred from the other underscore Ult exports, not measured +- [sceUltConditionVariableSignalAll, sceUltGetConditionVariableInfo, sceUltGetMutexInfo, sceUltGetQueueDataResourcePoolInfo, sceUltGetQueueInfo, sceUltGetReaderWriterLockInfo, sceUltGetSemaphoreInfo, sceUltGetUlthreadInfo, sceUltGetUlthreadRuntimeInfo, sceUltGetWaitingQueueResourcePoolInfo, sceUltAbstractThreadFactoryCreateThread, sceUltAbstractThreadFactoryGetNumMaxThreads, sceUltAbstractThreadFactoryJoinThread, sceUltKernelThreadFactoryFinalize, sceUltKernelThreadFactoryGetFactoryPointer, sceUltKernelThreadFactoryInitialize, sceUltModuleFinalize, sceUltModuleInitialize, sceUltUlthreadFactoryFinalize, sceUltUlthreadFactoryGetFactoryPointer, sceUltUlthreadFactoryGetWorkAreaSize, sceUltUlthreadFactoryInitialize, sceUltConditionVariableCreate, sceUltConditionVariableOptParamInitialize](../../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) - throwing stubs declared without parameters from the firmware export database (aerolib.csv); their guest signatures are unverified on hardware +- [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`. +- [sceUltWaitingQueueResourcePoolDestroy](../../../core/libs/prx/libSceUlt/Export.cpp) refuses with `ULT_ERROR_BUSY` 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. +- [sceUltMutexDestroy](../../../core/libs/prx/libSceUlt/Export.cpp) returns `ULT_ERROR_BUSY` when the mutex is held or has waiting threads, tracked via owner and waiter counts in `UltMutexState`. Prosper (`hle_ult.cpp`) logs a destroy of a held mutex and returns `ULT_OK`; AnyPS5 returns `ULT_ERROR_BUSY` to prevent destroying active mutexes. + +- The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`_sceUltConditionVariableOptParamInitialize`](../../../core/libs/prx/libSceUlt/Export.cpp). diff --git a/docs/dev/debt/libSceUrlConfigResolver.md b/docs/dev/debt/libSceUrlConfigResolver.md new file mode 100644 index 0000000000..035bdc8e88 --- /dev/null +++ b/docs/dev/debt/libSceUrlConfigResolver.md @@ -0,0 +1,5 @@ +# 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). 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..93445b0d4a --- /dev/null +++ b/docs/dev/debt/libSceUsbStorageDialog.md @@ -0,0 +1,10 @@ +# 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 + +- [sceUsbStorageDialogGetResult](../../../core/libs/prx/libSceUsbStorageDialog/Export.cpp) (libSceUsbStorageDialog) - `SceUsbStorageDialogResult` full layout, size and additional fields are unverified across open references (KytyPS5, shadPS4, fpPS4, SharpProspero); only the 4-byte `result` field (`COMMON_DIALOG_RESULT_USER_CANCELED = 1`) is written to avoid guest memory corruption +- [sceUsbStorageDialogOpen, sceUsbStorageDialogClose](../../../core/libs/prx/libSceUsbStorageDialog/Export.cpp) (libSceUsbStorageDialog) - the relative validation precedence between uninitialized state, busy/running state, and null parameter checks is inferred from standard common dialog conventions and unverified against real console firmware diff --git a/docs/dev/debt/libSceUsbd.md b/docs/dev/debt/libSceUsbd.md new file mode 100644 index 0000000000..85e8ca7ba8 --- /dev/null +++ b/docs/dev/debt/libSceUsbd.md @@ -0,0 +1,5 @@ +# libSceUsbd technical debt + +### 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 diff --git a/docs/dev/debt/libSceUserService.md b/docs/dev/debt/libSceUserService.md new file mode 100644 index 0000000000..4fca560df4 --- /dev/null +++ b/docs/dev/debt/libSceUserService.md @@ -0,0 +1,11 @@ +# libSceUserService technical debt + +### 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, sceUserServiceTerminate](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - not measured: whether an invalid thread priority leaves an uninitialized service uninitialized (it does here), the parameters after their first `int`, and any second argument of `sceUserServiceInitialize2`. Measured on a PS5: before initialization and after `sceUserServiceTerminate`, `GetLoginUserIdList`, `GetEvent` and `Terminate` return `0x80960002`; a second initialization returns `0x80960003` and a new one reports the initial user's login again; a priority outside 0x100 to 0x2FF, from the first `int` of the parameters or the `sceUserServiceInitialize2` argument, returns `0x80960005` before the initialized check. The other calls do not check initialization +- [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 +- [sceUserServiceGetAccessibilityChatTranscription, PressAndHoldDelay, TriggerEffect, Vibration, ZoomEnabled, ZoomFollowFocus](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - report 0, 0, 1, 1, 0 and 1, what a PS5 (firmware 13.60) reports for a user whose accessibility settings are all at their defaults; the console reports the user's own settings (a changed vibration intensity read 3), which are not modelled. What each number means is unknown. A user who is not logged in returns `0x80960009` with or without an output +- [sceUserServiceGetAccessibilityKeyremapEnable, sceUserServiceGetAccessibilityZoom](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - declared without parameters as throwing stubs calling `NotImplemented_nid_no_patch`. Their signatures and values remain unverified on hardware diff --git a/docs/dev/debt/libSceVdecsw.md b/docs/dev/debt/libSceVdecsw.md new file mode 100644 index 0000000000..bffe3f23ab --- /dev/null +++ b/docs/dev/debt/libSceVdecsw.md @@ -0,0 +1,9 @@ +# libSceVdecsw technical debt + +### 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. `sceVdecswSyncDecodeInput` / `sceVdecswSyncDecodeOutput` retry the TrySync variants until they stop returning DECODE_PENDING / OUTPUT_PENDING; they do not sleep beyond a yield, so another thread must drain outputs when the picture queue is full. `sceVdecswGetHevcPictureInfo` still throws: the 0xb8-byte HEVC picture layout is in shadPS4's `vdecsw.h`, but this wrapper only keeps AVC picture info from Videodec2 + +### Functional + +- [sceVdecswGetHevcPictureInfo](../../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - throws until HEVC picture info is filled from the decoder diff --git a/docs/dev/debt/libSceVideoCoreInterface.md b/docs/dev/debt/libSceVideoCoreInterface.md new file mode 100644 index 0000000000..f5053cfc00 --- /dev/null +++ b/docs/dev/debt/libSceVideoCoreInterface.md @@ -0,0 +1,5 @@ +# 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). diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md new file mode 100644 index 0000000000..8b62870d99 --- /dev/null +++ b/docs/dev/debt/libSceVideoOut.md @@ -0,0 +1,39 @@ +# libSceVideoOut technical debt + +### 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 + +- PPSA29866 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceVideoOutSetFlipMaster and sceVideoOutClearFlipMaster](../../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut) +- [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 +- [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`) - `(int handle, void* status)`, the 80-byte layout and the return codes follow the clean-room description in #2746. Bytes 0-47 are `sceVideoOutGetOutputStatus`'s; bytes 48-53, which the console copies from an internal query whose fields are not established, are written as zero, as are the rest, so the 16-bit value at byte 52 never adjusts `refreshRate`. A null `status` returns `0x80290002` and an unknown handle `0x8029000B`. The console's earlier global-flag check (`0x8029001C`) and its object-field check (`0x8029001B`) test states that are not modelled and are never reported. +- [sceVideoOutRegisterBuffers2](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - buffer option 8 (STRICT_COLORIMETRY) is presented as option 0, with the stored values unchanged; what the console changes for it is unknown. Only options 0 and 8 are accepted +- [sceVideoOutAddBufferAttributeOption](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut, NID `MBlv8YWoyOk`) - the signature `(VideoOutBufferAttribute2*, uint64_t option)` comes from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/VideoOut/VideoOut.cs#L174-L176) and the call site in PPSA25380, which passes 0x20 and ignores the result. The option is ORed into `attribute->option`; whether the console validates the bits, keeps them elsewhere or fails on an unknown option is unknown. A null attribute throws. Option 0x20 makes `sceVideoOutRegisterBuffers2` throw, as every option other than 0 and 8; what it selects for an overlay buffer is unknown +- [sceVideoOutRegisterBuffers2](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - DCC buffers (category 1): `dcc_control` is taken as the CB_DCC_CONTROL block layout, only bits 0x10026c are accepted; `dcc_cb_register_clear_color` as the register-clear texel, only 32-bit values are presented +- [sceVideoOutIsOutputSupported](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - mode 1 is supported; mode 15 is supported when the current host display runs at 120 Hz or above. Configuring mode 15 requires attribute3 bit 0x40; this single-bit mask is inferred from the measured full value 0x80040, not isolated by a console measurement. Vblanks run at 119.88 Hz until mode 1 restores 59.94 Hz. Display modes are read through SDL; a move to a slower display 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 another 0x8029001E. The name `VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE` for 0x8029001E is ours. Word 3 of the output options is accepted with any value, as on the console, and its meaning is unknown. PPSA12544 probes `0xd000000a`, which the console answers with 0x8029001E, so the title selects 3840 pixels; PPSA26344 at `0xCD0946` passes `(handle, 0xf, nullptr, nullptr, 0)` and branches on the result equalling 1 +- [sceVideoOutGetResolutionStatus](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the 48-byte layout follows the declarations in ps5-opengl and EVO-PLAYER-PS5 (#1578), which agree; not confirmed on a console. `screenSizeInInch` is 0, the value EDID uses for an undefined size, since the host display's size is not read; `flags` is 0, its bits are unknown. The pane equals the full size because `sceVideoOutSetWindowModeMargins` applies no margins +- [sceVideoOutOpen](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the open param's first word is unknown (PPSA21564 passes 16; it is not the byte size, since the affinity mask is at offset 16); only 16 is accepted. 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 +- [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. +- [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..79bb5888e1 --- /dev/null +++ b/docs/dev/debt/libSceVideoRecordingP.md @@ -0,0 +1,14 @@ +# libSceVideoRecordingP technical debt + +### Unknown function info + +- [_sceVideoRecordingQueryParam_nid_postfix](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) (libSceVideoRecordingP) - declared with `void(void* param)` signature following [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/VideoRecording.cs#L22); unimplemented and throws `NotImplemented_nid_no_patch` +- [sceVideoRecordingCloseByHandle, sceVideoRecordingGetInfoByHandle](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) (libSceVideoRecordingP) - inferred handle-based recording control signatures taking an integer recording handle; unimplemented and throw `NotImplemented_nid_no_patch` +- [sceVideoRecordingCopyBGRAtoNV12](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) (libSceVideoRecordingP) - inferred pixel buffer conversion signature `(void* dst, const void* src, uint32_t width, uint32_t height)`; unimplemented and throws `NotImplemented_nid_no_patch` +- [sceVideoRecordingGetSupportInfo, sceVideoRecordingGetVideoOutMode](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) (libSceVideoRecordingP) - query signatures taking an output pointer; exact structure layouts and return values are unverified and throw `NotImplemented_nid_no_patch` +- [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) (libSceVideoRecordingP) - initialization stub taking an optional parameter structure pointer; unverified and throws `NotImplemented_nid_no_patch` +- [sceVideoRecordingOpen2](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) (libSceVideoRecordingP) - extended recording open signature; parameters inferred from `sceVideoRecordingOpen` and [fpPS4](https://github.com/red-prig/fpPS4); unimplemented and throws `NotImplemented_nid_no_patch` +- [sceVideoRecordingQueryHddSize2, sceVideoRecordingQueryMemSize2](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) (libSceVideoRecordingP) - memory and disk size query signatures taking parameter pointer; unverified and throw `NotImplemented_nid_no_patch` +- [sceVideoRecordingRestModeProcessGetStatus, sceVideoRecordingRestModeProcessStart](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) (libSceVideoRecordingP) - rest mode recording interfaces; unverified and throw `NotImplemented_nid_no_patch` +- [sceVideoRecordingSendCommand](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) (libSceVideoRecordingP) - command dispatch signature taking integer command ID and parameter pointer; unverified and throws `NotImplemented_nid_no_patch` diff --git a/docs/dev/debt/libSceVideodec.md b/docs/dev/debt/libSceVideodec.md new file mode 100644 index 0000000000..c24087ab78 --- /dev/null +++ b/docs/dev/debt/libSceVideodec.md @@ -0,0 +1,15 @@ +# libSceVideodec technical debt + +### Unknown function info + +- [sceVideodecCreateDecoder, sceVideodecDecode, sceVideodecDeleteDecoder, sceVideodecFlush 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 + +- [libSceVideodec](../../../core/libs/prx/libSceVideodec/Export.cpp) (libSceVideodec) - the module's other two exports, `sceVideodecMapMemory` and `sceVideodecQueryResourceInfo`, are not provided, so a title that imports them fails to start + +### 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 + +- [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 and sceVideodecQueryResourceInfo](../../../core/libs/prx/libSceVideodec/Export.cpp) (libSceVideodec) - their names are listed in the PS5 3.20 export table and cross-checked against PS5 13.60. Their signatures and behaviour are unverified; these no-parameter declarations only resolve imports and throw through NotImplemented_nid_no_patch. diff --git a/docs/dev/debt/libSceVideodec2.md b/docs/dev/debt/libSceVideodec2.md new file mode 100644 index 0000000000..3706171e00 --- /dev/null +++ b/docs/dev/debt/libSceVideodec2.md @@ -0,0 +1,17 @@ +# libSceVideodec2 technical debt + +### Unknown function info + +- [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 + +- [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. What the console returns for them, and for a stream that continues across the flush without a new IDR picture, is not measured. +- [sceVideodec2Decode](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) and `sceVideodec2Flush` output the coded picture, with the chroma plane after its luma rows, as the console does for 1080p H.264 and HEVC streams coded at 1920x1088. An HEVC or VP9 picture whose size is not a multiple of 16, such as HEVC coded at 1920x1080, is output at that size; whether the console pads it is unverified. `sceVideodec2GetPictureInfo` reports no frame cropping diff --git a/docs/dev/debt/libSceVoice.md b/docs/dev/debt/libSceVoice.md new file mode 100644 index 0000000000..5c4f80575c --- /dev/null +++ b/docs/dev/debt/libSceVoice.md @@ -0,0 +1,25 @@ +# libSceVoice technical debt + +### Silent stubs + +- [sceVoiceGetPortAttr](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - attribute 1001 writes one zero byte and returns 0 for an existing port; the backend processes no voice audio. PPSA34501 requests size 1 and reads one byte on success, and this fallback avoids its startup exception. The name follows attribute 1001 (`CELLVOICE_ATTR_VAD`) (Voice Activity Detection) in [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellVoice.h), not confirmed for the PS5. The correct result, eligible port types and behaviour are unknown. + +### Unknown function info + +- [sceVoiceResetPort](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - the `(uint32_t portId)` signature is the PS Vita one in [VitaSDK](https://github.com/vitasdk/vita-headers/blob/ae5808f3105c42fa3373ba4ab64ab54e2e3ca848/include/psp2/voice.h#L503-L514) and the PS3 `cellVoiceResetPort` in [RPCS3](https://github.com/RPCS3/rpcs3/blob/be69c054eeac57e2e671697a463473d37fe5a7b2/rpcs3/Emu/Cell/Modules/cellVoice.cpp#L493-L510); that the PS5 library takes the same argument is assumed. A reset clears a port's buffered data, and ports here buffer nothing (`sceVoiceReadFromOPort` returns 0 bytes and `sceVoiceWriteToIPort` throws), so it only checks that the library is initialized and the port exists, as RPCS3 does, and keeps the port's connections, volume and mute flag. An uninitialized library or an unknown port throws like the other port calls; the console's error codes are not measured. PPSA23566 imports it +- [sceVoiceGetResourceInfo](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - the `(VoiceResourceInfo* info)` signature and port limits (7 voice inputs, 7 voice outputs, 1 device input, 1 device output, 64 total ports) are the PS Vita ones in [VitaSDK](https://github.com/vitasdk/vita-headers/blob/ae5808f3105c42fa3373ba4ab64ab54e2e3ca848/include/psp2/voice.h#L150-L157); that the PS5 library uses the same layout and limits is assumed. An uninitialized library or a null output pointer throws like the other Voice calls; console limits and error codes are not measured. PPSA23566 imports it +- [sceVoiceEnableChat, sceVoiceDisableChat](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) take no arguments and return 0, following [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs#L460-L464) and the `sceAudioOut2EnableChat` model. The host mixes no chat audio, so the calls only record whether chat audio is enabled and fail when the library is not initialized. Console error codes and chat session requirements are unverified. PPSA23566 imports them +- [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 +- [sceVoicePausePort, sceVoiceResumePort](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - not measured on a PS5: the `(uint32_t portId)` signature follows [VitaSDK](https://github.com/vitasdk/vita-headers/blob/ae5808f3105c42fa3373ba4ab64ab54e2e3ca848/include/psp2/voice.h#L516-L538) and the PS3 calls in [RPCS3](https://github.com/RPCS3/rpcs3/blob/be69c054eeac57e2e671697a463473d37fe5a7b2/rpcs3/Emu/Cell/Modules/cellVoice.cpp#L443-L529), and the console's error codes and its result for pausing a paused port or resuming a running one are unknown (0 here, as in RPCS3). Both only check that the library is initialized and the port exists: VitaSDK documents that a paused port keeps its data and reported state, and nothing here processes port data. PPSA24191 imports them +- [sceVoiceInitHQ](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [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 +- [sceVoiceVADSetVersion](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceSetMuteFlagAll](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceSetBitRate](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceUpdatePort](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoicePausePortAll](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceResumePortAll](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature diff --git a/docs/dev/debt/libSceVoiceChat.md b/docs/dev/debt/libSceVoiceChat.md new file mode 100644 index 0000000000..60ad4dc3bb --- /dev/null +++ b/docs/dev/debt/libSceVoiceChat.md @@ -0,0 +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 new file mode 100644 index 0000000000..fa6732187f --- /dev/null +++ b/docs/dev/debt/libSceVoiceQoS.md @@ -0,0 +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. PPSA26344's two call sites (`0xE307BF`, `0x151C45D`) pass an allocated pointer, a 0x40000-byte block and app type 0x10000000; the app type and block are not used. `sceVoiceQoSCreateLocalEndpoint` takes four arguments (`int* id, userId, deviceInId, deviceOutId`) as declared in fpPS4 (`src/ps4_libscevoice.pas`, `ps4_sceVoiceQoSCreateLocalEndpoint`); signatures for `DeleteLocalEndpoint(id)`, `CreateRemoteEndpoint(int* id)`, `DeleteRemoteEndpoint(id)`, `Connect(int* id, local, remote)`, `Disconnect(connection)`, `SetLocalEndpointAttribute(id, attribute, value, size)`, `SetRemoteEndpointAttribute(id, attribute, value, size)`, and `GetLocalEndpointAttribute(id, attribute, value, size)` were verified against call sites in Aliens: Fireteam Elite (PPSA03393, e.g. `0x76D132`, `0x76D16E`, `0x76CECB`). Endpoints and connections are tracked by allocated id; duplicate connections and deleting an endpoint while referenced in a connection are permitted as the console is not known to reject them; endpoint attributes are stored as given (including empty or non-positive sizes), as their ids and defaults are unknown. The not-initialized error 0x804E0901 is assumed from the 0x804E09xx numbering, and unknown ids return `ARGUMENT_INVALID`. Aliens: Fireteam Elite (PPSA03393) uses all implemented calls on its load path; it stops with `not implemented` when any unimplemented call throws +- [libSceVoiceQoS firmware exports](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `sceVoiceQoSGetRemoteEndpointAttribute`, `sceVoiceQoSGetConnectionAttribute` and `sceVoiceQoSGetStatus` follow public PS4 SDK prototypes in [Orbital](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json#L2436-L2441) and remain throwing stubs as endpoint and connection attribute storage is unverified on hardware. `sceVoiceQoSEnableChat`, `sceVoiceQoSDisableChat`, `sceVoiceQoSDebugGetStatus`, `sceVoiceQoSInitHQ`, `sceVoiceQoSSetMode`, and `sceVoiceQoSTerminate` are declared without parameters from the firmware export database (aerolib.csv); their guest signatures are unverified on hardware and every call throws + +### Functional + +- [sceVoiceQoSEnd](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0901` (not initialized) before any initialization was measured on a PS5 (firmware 13.60, #1407); ending an initialized library was not, because a valid `sceVoiceQoSInit` never returns inside a homebrew payload. PPSA14632 calls it at shutdown and PPSA26344 after a nonzero `sceVoiceQoSInit`. After an initialization it returns 0 and clears the init flag, so the library can be initialized again. Whether the console also ends the libSceVoice instance that its `sceVoiceQoSInit` starts ([prosper](https://github.com/mattias800/prosper)'s reading of the module), and what it answers while endpoints exist, is unknown; here it drops every endpoint and connection +- [sceVoiceQoSGetLocalEndpoint, sceVoiceQoSGetRemoteEndpoint](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - signatures follow [Orbital](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json#L2437-L2439) (`GetLocalEndpoint(connectionId, int* localId)` and `GetRemoteEndpoint(connectionId, int* remoteId)`). Operations look up the connection by allocated id and return the connected local or remote endpoint id; an unknown connection id or null output pointer returns `ARGUMENT_INVALID` (0x804E0902), and calling before initialization returns `0x804E0901` diff --git a/docs/dev/debt/libSceVrSetupDialog.md b/docs/dev/debt/libSceVrSetupDialog.md new file mode 100644 index 0000000000..336e3361d1 --- /dev/null +++ b/docs/dev/debt/libSceVrSetupDialog.md @@ -0,0 +1,5 @@ +# libSceVrSetupDialog technical debt + +### 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 the library. `sceVrSetupDialogGetStatus` (NID `qRUACYyUNFA`) is present in the firmware export database (aerolib), but direct import by PPSA12544 has not been explicitly confirmed. diff --git a/docs/dev/debt/libSceVrTracker.md b/docs/dev/debt/libSceVrTracker.md new file mode 100644 index 0000000000..918f6cc2d4 --- /dev/null +++ b/docs/dev/debt/libSceVrTracker.md @@ -0,0 +1,5 @@ +# libSceVrTracker technical debt + +### Unknown function info + +- [sceVrTrackerCpuProcess, sceVrTrackerGetResult, sceVrTrackerGetTime, sceVrTrackerGpuSubmit, sceVrTrackerGpuWait and sceVrTrackerInit](../../../core/libs/prx/libSceVrTracker/Export.cpp) (libSceVrTracker) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVrTracker.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/libSceVrTracker2.md b/docs/dev/debt/libSceVrTracker2.md new file mode 100644 index 0000000000..42938b9e61 --- /dev/null +++ b/docs/dev/debt/libSceVrTracker2.md @@ -0,0 +1,11 @@ +# libSceVrTracker2 technical debt + +### Silent stubs + +- [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) never initializes, so its exports answer as the console's module does before initialization, measured on a retail PS5 (firmware 13.60) with no PS VR2 headset by calling each of the 115 NIDs with null arguments, zeroed buffers and buffers filled with 0xAA and 0x01: 99 return 0x8A740001 and write nothing; `XknY0IDcOwU` and `ark13GkiskA` return 0x8A740006 for a null first argument and otherwise clear its first 24 and 16 bytes and return 0x8A740001; `-YvjOUph8B0` returns 0x8A740006 for a null second argument and otherwise 0x8A740001; `QyZvFyf-QEk` and `fTYZOZepkqE` return 0 and write nothing; `TZIUIk9id3c` writes 0xC000, 0, 0x40 and 0x40 as `uint32`, `uint32`, `uint16` and `uint16` at offsets 4 to 15 of its first argument and returns 0; `IB0qcDVu8vU` clears the `uint32` at its first argument and returns 0. The last two crash on the console for a null first argument and throw here. Signatures and what the tracker returns once initialized are unknown; the twelve exports with aerolib names keep them and the others carry this library's own names + +### Unknown function info + +- [sceVrTracker2QueryMemory](../../../core/libs/prx/libSceVrTracker2/Export.cpp) (libSceVrTracker2, NID `TwqZnaIjWv4`) - accepts only a parameter whose first `uint32` is 0x20 and whose bytes 4 to 31 are zero, found by searching sizes 8 to 0x400 and setting each field and byte; it then writes 32 bytes, `uint32` 0x20, 32 MiB and 64 KiB followed by zeros, and returns 0, and otherwise returns 0x8A740006 without writing, as it does for a null argument. The meaning of the parameter's reserved bytes and of the written fields is assumed from their values +- [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) - initialization (`6Jy73SRfG-o`) takes one parameter of 0x48 bytes. PPSA05666 passes the size 0x48 at offset 0, `uint32` 0x100 and 0x2BC at 0x8 and 0xC, `uint64` 0x1FFF at 0x10 and 0x18, a pointer to its work memory at 0x20, and the memory query's 32 MiB and 64 KiB as `uint32` at 0x28 and 0x2C, with the other bytes zero; the priority and CPU mask meanings of 0x8 to 0x1F are assumed from the values. Called with that parameter and 32 MiB of anonymous memory from a homebrew payload on a PS5 (firmware 13.60) with no PS VR2 headset, it did not return within a minute, so what it returns without a headset is unknown and it throws +- [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) - eight exports throw. `6Jy73SRfG-o` (initialization) and `9R6W33Au4B4` return 0x8A740006 on the console for null arguments, for zeroed, 0xAA- and 0x01-filled buffers and for a size field from 8 to 0x400 in any of the first three arguments; initialization also returned it for the memory query's parameter with 32 MiB aligned to 64 KiB passed inside the parameter or as further arguments, so the parameter layout it accepts is unknown. `NBhWy+Gl0tY` returns 0x8A740006 for those buffers but crashes for null arguments; `jRmuXjsknsI` returns 0x8A740006 for null arguments and crashes for buffers, also when every 8-byte slot points at readable memory; `O8ZkTFtvUL4`, `940WPg+WwIk`, `oZDm1bynCaA` and `U1VYczwqukY` crash for both diff --git a/docs/dev/debt/libSceWebBrowserDialog.md b/docs/dev/debt/libSceWebBrowserDialog.md new file mode 100644 index 0000000000..3d44927711 --- /dev/null +++ b/docs/dev/debt/libSceWebBrowserDialog.md @@ -0,0 +1,10 @@ +# libSceWebBrowserDialog technical debt + +### Silent stubs + +- [libSceWebBrowserDialog](../../../core/libs/prx/libSceWebBrowserDialog/Export.cpp) - `sceWebBrowserDialogOpen` and `sceWebBrowserDialogOpenForPredeterminedContent` run the common-dialog state machine without displaying an interactive browser, and `sceWebBrowserDialogGetResult` reports that the user cancelled. Minecraft Bedrock (PPSA17221) imports `sceWebBrowserDialogOpenForPredeterminedContent` (#5744). The real export takes a second argument, the allow-list of up to twenty addresses (SharpProspero `docs/dialogs.md`); it is not read here, so no address filtering is modelled, and the exact validation order and codes with a null second argument are unverified. +- [libSceWebBrowserDialog](../../../core/libs/prx/libSceWebBrowserDialog/Export.cpp) - `sceWebBrowserDialogClose` 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 + +- [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 new file mode 100644 index 0000000000..5d289d575b --- /dev/null +++ b/docs/dev/debt/libSceZlib.md @@ -0,0 +1,5 @@ +# libSceZlib technical debt + +### 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. diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md new file mode 100644 index 0000000000..e3c4207769 --- /dev/null +++ b/docs/dev/debt/libc.md @@ -0,0 +1,135 @@ +# libc technical debt + +### 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. +- [__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, 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: `set_constraint_handler_s` is not exported and the console's default handler is unknown. An invalid multibyte `%s` argument is copied byte by byte instead of being reported as an encoding error, as in `vswprintf` +- [swprintf_s and vswprintf_s](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follow C11 K.3.9.1.4 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`: the complete output, including the terminating null, must fit in the buffer, otherwise it is a runtime-constraint violation that sets `buffer[0]` to zero and returns a negative value, and the buffer is not partially written. Argument and encoding checks match `snwprintf_s` +- [strerror_s and strerrorlen_s](../../../core/libs/prx/libc/src/WideConversion.cpp) (libc) follow [C11 K.3.7.4.2 and K.3.7.4.3](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf), using the existing FreeBSD error messages and unknown-error formatting. Truncation writes an ellipsis when capacity exceeds 3; constraint failures leave the destination untouched and invoke the registered guest-ABI handler. The console's `RSIZE_MAX` (`SIZE_MAX >> 1` here), nonzero returns (`EINVAL` 22 for constraints, `ERANGE` 34 for truncation), handler arguments and default abort behavior are unverified. Messages remain in the C locale, and guest errno is preserved. +- [getw and putw](../../../core/libs/prx/libc/src/BinaryWordIo.cpp) (libc) follow FreeBSD 11.4 [getw.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/getw.c) and [putw.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/putw.c): one 32-bit little-endian word, `getw` returns -1 for EOF or an error, and `putw` returns 0 on success or -1 on error while holding the stream lock. They use the existing host-backed streams, so buffering and error timing follow the host CRT. Null streams throw and closed streams return -1 with `EBADF`, as existing byte I/O does. Console behavior is unverified. +- [vsnwprintf_s](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - `va_list` counterpart of `snwprintf_s` (C11 K.3.9.1.3): truncates to the buffer and returns the full length, with the same constraints and argument checks +- [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. 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 + +### Functional + +- [_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. +- 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) - `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) +- [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. +- [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), 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` and `freopen`](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows fail to open a directory in a read mode with `EACCES`, where FreeBSD opens it; directory streams are not emulated +- [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 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 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. +- [wcsrtombs_s](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached +- [c16rtomb, c32rtomb, mbrtoc16 and mbrtoc32](../../../core/libs/prx/libc/src/Multibyte.cpp) (libSceLibcInternal, implemented in libc) follow the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x7D480 to 0x7D920), which converts to and from UTF-8 whatever the locale, with the original sequences of up to six bytes and no check for surrogates or for values above 0x10FFFF when encoding. The conversion state keeps the value in its first eight bytes and the count of bytes still expected (or 0x10 for a pending low surrogate, 1 for a stored high surrogate) as 16 bits at offset 0xA; a null state, which the console dereferences, throws. As on the console, `mbrtoc16` does not store the complete code point when it returns a high surrogate, so the low surrogate of the next call is built from the value the state last held, which is the partial value of an earlier incomplete call or whatever the caller initialised it to, `c16rtomb` returns -1 without setting `errno` when a high surrogate is not followed by a low one, and `mbrtoc16` and `mbrtoc32` report `EILSEQ` as 86. Measured on a PS5 (firmware 13.60) from a homebrew payload, the system `libSceLibcInternal`'s default locale also converts UTF-8 in `wcrtomb`, `wctomb`, `mbrtowc` and `mblen` (`wctomb` of U+263A gives `E2 98 BA`, `mblen` of a lone 0xE9 gives -1); this libc keeps its single-byte C locale, so [wctomb and mblen](../../../core/libs/prx/libc/src/Multibyte.cpp), which call `wcrtomb` and `mbtowc` here as they call the same internal routines on the console, share that difference. Titles bundle their own `libc.prx`, whose locale was not measured +- [The narrow printf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not use the guest's single-byte `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments, `%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, on Linux throws for the 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`): glibc would store a 32-bit `wchar_t` per character into the guest's 16-bit buffer. Windows hands `%ls`, `%lc` and `%l[` to the host, whose `wchar_t` is 16 bits +- [The scanf family](../../../core/libs/prx/libc/include/WindowsScanning.hpp) (libc) on Windows returns 0, as C17 and FreeBSD do, when input fails after only suppressed conversions have completed (`sscanf("12", "%*d%d", &value)`). Linux hands the format to glibc, which returns `EOF` there. On Windows the host stops a `%x`, `%X`, `%i` or `%p` after the `x` of a `0x` that no hex digit follows (or that the width cuts off), and a float conversion (`%e`, `%f`, `%g`, `%a`) after the `e` of an exponent with no digits (`1e`), where C17 and FreeBSD stop before the `x` or the `e` +- [Guest path resolution](../../../core/libs/prx/libc/src/General.cpp) falls back to ASCII case-insensitive component lookup, following [shadPS4's host filesystem](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/file_sys/backends/host_fs.cpp). PPSA03747 requests `Skyrim.INI`, `Data/Video` and lowercase archive/plugin names that differ from the dump's filenames. Exact host names take precedence; multiple fallback matches throw instead of selecting an arbitrary file. Existing parent names are resolved when creating a new file, and guest mount aliases use the same ASCII comparison. Results are not cached, so renames and newly created files remain visible. Unicode case folding and PS5 behaviour for case-colliding host entries are unverified; host roots are used as supplied. Explicit native Windows drive and UNC paths bypass guest parent-component validation.. A relinked module's `.guest.prx` is matched the same way when the original module is not beside it: Borderlands 4 (PPSA01494) loads `/app0/prx/libRenoirCore.PS5.prx`, which it ships as `librenoircore.ps5.prx`. 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. 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)) +- [_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`. `%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) (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 diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md new file mode 100644 index 0000000000..5d642253a4 --- /dev/null +++ b/docs/dev/debt/libkernel.md @@ -0,0 +1,239 @@ +# libkernel technical debt + +### 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_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 +- [sceKernelGetMainSocId and sceKernelGetCpumode](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) both return 0, and [sceKernelGetCpuFrequency](../../../core/libs/prx/libkernel/Time/Time.cpp) returns the host TSC frequency measured for `sceKernelGetTscFrequency`, not the console CPU clock. The PS5's SoC id and CPU mode are unknown; shadPS4's PS4 values are [0x710f10/0x740f30 and 0, 2 or 5 from the param.sfo CPU-mode flags](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1a/src/core/libraries/kernel/process.cpp). PPSA99203 only logs them (`[console] main SoC id: result=%#x out=%#x`, `CPU: %llu Hz, cpumode %d`), passing a pointer that `sceKernelGetMainSocId` leaves untouched +- [sceKernelEnableDmemAliasing](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) returns 0 and changes nothing: direct memory can always be mapped more than once here, as `DoMapDirect` in [DirectMemory.cpp](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) only checks that the physical pages are allocated, so there is no aliasing to turn on. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L39-L43) (PS4) sets a flag there that lifts the `EBUSY` its [`sceKernelMapDirectMemory2`](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L299-L301), and [`sceKernelMapDirectMemory` for SDK 2.50 and later](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L237-L242), return for direct memory that is [already mapped](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/memory.cpp#L548-L553); that check is not applied here. The no-argument `int` signature follows shadPS4 and ProsperoEden (PPSA99008), which calls it before mapping the same direct memory again. Whether the PS5 refuses such a mapping without the call is unverified +- [sceKernelInternalMemoryGetAvailableSize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) reports 0 bytes and returns 0: what the console's internal memory pool holds and how large it is are unknown. The `int (size_t*)` signature is from [xUtilty](https://github.com/Backporter/xUtilty/blob/328a856af1ef/source/SystemWrapper.cpp#L85) (PS4). PPSA99203 calls it once beside `sceKernelAvailableFlexibleMemorySize` while starting its GS renderer and never reads the result ([GS.cpp](https://github.com/Swordpdf/PS5SX2/blob/b4cf55e87c34/pcsx2/GS/GS.cpp#L233-L247)) +- [sceKernelIsThreadSanitizerEnabled](../../../core/libs/prx/libkernel/System/src/Sanitizer.cpp) (libkernel, NID `LzoM-wVLJDE`) returns 0, as `sceKernelIsAddressSanitizerEnabled` does: a converted title never runs under the thread sanitizer, so a title that checks it skips the [libSceDbgThreadSanitizer](../../../core/libs/prx/libSceDbgThreadSanitizer/Export.cpp) annotation calls, which throw. PPSA23226's bundled `libSceJobManager_nosubmission.prx` makes those calls only when it reports nonzero + +### Unknown function info + +- [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`. +- [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` 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 +- [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 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 +- [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 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) (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 +- [sceKernelGetModuleInfoFromAddr](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x1A8-byte info layout follows the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation; a title's `libc.prx` reads only `id` (PPSA14632). The meaning of `flags` is unknown; only 2 is accepted. The input `st_size` is ignored, because PPSA21703's `libc.prx` passes an uninitialized 0x1A8-byte buffer; the full size is written back. The id is the `sceKernelLoadStartModule` handle of an image opened through the loader and otherwise a stable id from the same range. The name is the host file name without `.guest.prx`. An image with more than 4 load segments (the converted executable has 7) reports its first 4. `tls_offset` is 0 and `ref_count` is 1, as the host loader exposes neither. On Windows the module is the PE image holding the address: a relinked image reports its `.elf` sections as segments and the eh_frame_hdr recorded in `.ehmeta`, any other image its first 4 sections with memory access and the `.ehfram` section without an eh_frame_hdr; the TLS fields and `init_proc_addr`/`fini_proc_addr` are 0 +- [sceKernelGetModuleList](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - lists the main program and every loaded `.prx` image in load order; host runtime libraries are left out. The handles are the ids `sceKernelGetModuleInfoFromAddr` reports, not the console's. The `(int32_t* handles, size_t count, size_t* actual)` signature and returning `ENOMEM` without writing `actual` after filling the first `count` handles when more images are loaded follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/kernel/process.cpp#L244-L264); whether the console writes any handles in that case is unknown +- [sceKernelGetModuleInfo](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x160-byte layout (`st_size`, `name[256]`, 4 segments, `segment_count`, `fingerprint[20]`) follows [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_types.pas#L319-L337) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/module.h#L15-L31). Only that `st_size` is accepted, as in shadPS4; other sizes return `EINVAL`, a null pointer `EFAULT` and a handle `sceKernelGetModuleList` does not list `ESRCH`. Name and segments are the ones `sceKernelGetModuleInfoFromAddr` reports, on Windows from the same PE sections, so `prot` uses the read/write/execute bits 1/2/4, while fpPS4 and shadPS4 store the ELF `p_flags`. The fingerprint is left zero, as the host image does not carry it +- [sceKernelGetModuleList2 and sceKernelGetModuleInfo2](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the title-module variants follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/process.cpp): `sceKernelGetModuleList2` lists what `sceKernelGetModuleList` lists without system libraries, and `sceKernelGetModuleInfo2` answers `EPERM` for a system library after the `EFAULT`, `EINVAL` and `ESRCH` checks of `sceKernelGetModuleInfo`. A system library here is a loaded `.prx` image that is not a relinked title module (`.guest.prx`); the main program and the title's own modules are listed. Not verified on the console. PPSA34063 imports both and fails to load without them +- [sceKernelGetLibkernelTextLocation](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the `(void** address, std::size_t* size)` signature and the choice of the main program's first segment follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_libkernel.pas#L338-L348); whether the console reports that segment, the text of libkernel itself or the calling module's is unverified. The segment is the first one `sceKernelGetModuleInfo` reports for the main program, and 0 is returned. A null argument throws. +- [sceKernelReleaseFlexibleMemory](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - unmaps the range like sceKernelMunmap, as fpPS4 and prosper do; whether the console keeps the virtual range reserved after releasing its pages is unverified +- [sceKernelAvailableFlexibleMemorySize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - the segments of the title's executable and of its `.guest.prx` modules count against the 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) - 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. 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. `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. +- [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 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` 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, setgid, 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_setgid`, `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_getname_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - a thread that has already been joined returns `ESRCH` (3) and its output buffer is left untouched, as measured on a PS5 (firmware 13.60) by the author of this entry; since [#4481](https://github.com/boykopovar/AnyPS5/pull/4481) the joined thread's object is retained, so the posix wrapper answers `ESRCH` for it instead of reading the retained name. A null name buffer is answered with `EFAULT` before that, an order the console table does not measure. The SCE sibling `scePthreadGetname` has no measured joined-thread behaviour on the console and still reads the retained object's name, so it is unchanged, as is the pre-existing dangling case of a thread that ended detached (its object is released at exit; see the scePthreadJoin entry) +- [sceKernelGetOperationMode](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the signature `(int* mode, int* submode)` comes from PPSA12544, which logs both values after the call and ignores the result. A homebrew payload on a PS5 (firmware 13.60) reads mode 2 and submode 0, nothing else is written, and the result register holds no status, so the function is taken to return nothing. Whether a title process reads the same values is unverified. Null outputs throw +- [sceCoredumpGetStopInfoCpu](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) takes `(void* info, size_t size)` as in the [Orbital type database](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json); the 16-byte, 8-byte-aligned block follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs), which places coredump reads inside the handler. The handler is never invoked here, so a well-formed request returns `NOT_IN_COREDUMP_HANDLER` without reading or writing the block. Its value `0x81180003` follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/coredump/coredump_error.h) and the NHL 27 (PPSA34063) call site reported in [#4275](https://github.com/boykopovar/AnyPS5/discussions/4275), not a PS5 measurement. The console validation order is unverified: null, unaligned or wrapping buffers and sizes other than 16 throw instead of guessing an argument error. Information from an active coredump handler remains unsupported +- [sceCoredumpGetThreadContextInfo](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `ShChva57wIM`) takes `(void* threadContextInfo, size_t threadContextInfoSize)` as in the [Orbital type database](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json); the 144-byte block of 18 64-bit registers follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs#L120-L141). It answers as `sceCoredumpGetStopInfoCpu`: the handler is never invoked here, so an aligned 144-byte request returns `NOT_IN_COREDUMP_HANDLER` without reading or writing the block, and null, unaligned or wrapping buffers and other sizes throw, as the console validation order is unverified. Planet Coaster (PPSA01736) imports it +- [sceCoredumpAttachMemoryRegion, sceCoredumpAttachUserMemoryFile](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - both take `(uint32_t userValue, const void* mem, size_t size)`: for the first, the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs) bindings agree; the second has only SharpProspero (the PS4 5.05 firmware does not export it, so Orbital has no entry) and is described there as attaching the region as an unnamed user file. Not confirmed on a title; the RED engine's PS4 crash handler ([Vangaurd](https://github.com/vurimad/Vangaurd/blob/main/source/imported/common/redSystem/src/errorHandlerImplErrorHooksOrbis.cpp)) calls the sibling `sceCoredumpAttachMemoryRegionAsUserFile(999, buffer, bytes, name)` from inside its handler. Attaching belongs inside the core dump handler, which the system runs on its own thread after the process stops (SharpProspero's remark, the RED engine's usage and `SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER`, "function was not called from registered core dump handler", in the Vita's SceCoredump error table, [RivetDB](https://github.com/CreepNT/RivetDB/blob/master/rivetdb.json)). The handler is never invoked here, so every call is outside it and returns that error; nothing is recorded, as there is no dump to attach to. Its value `0x81180003` comes from shadPS4 (PS4), not confirmed on a PS5: [fpPS4](https://github.com/red-prig/fpPS4/blob/master/kernel/ps4_libkernel.pas) has `NOT_REGISTERED` 0x81180001 and `ALREADY_REGISTERED` 0x81180002, shadPS4's `coredump_error.h` has `NOT_IN_COREDUMP_HANDLER` = 0x81180003, and the Vita table numbers `PARAM` 0, `NOT_REGISTERED` 1, `ALREADY_REGISTERED` 2 and `NOT_IN_COREDUMP_HANDLER` 3. Whether the console checks the region before the calling context is unknown, so a null, empty or wrapping region throws. `userValue` is not interpreted; SharpProspero names its bits 31 and 30 as truncation flags for the user file variants +- [sceCoredumpWriteUserData, sceCoredumpDebugTextOut](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - the signatures `ssize_t (const void* data, size_t size)` and `void (const char* str, int len)` follow the [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json) IDA type database and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs) bindings ("Repeated calls append", "The bytes written, or a negative error code"). User data is written from inside the core dump handler: the Vita's [`sceCoredumpWriteUserData`](https://github.com/vitasdk/vita-headers/blob/e66ebe90b73d1fa4cce005a5b2072cec27322544/include/psp2/coredump_nounlink.h) returns `0x800A0003` ("No user-data capture is active") outside it, and the RED engine's PS4 crash handler ([Vangaurd](https://github.com/vurimad/Vangaurd/blob/4f86d2b0e1a6c0d50526152c517a0292b1a68af4/source/imported/common/redSystem/src/errorHandlerImplErrorHooksOrbis.cpp)) makes all its coredump calls, `sceCoredumpDebugTextOut` included, from its handler. The handler is never invoked here, so `sceCoredumpWriteUserData` returns `SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER` (0x81180003 in shadPS4's [`coredump_error.h`](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/coredump/coredump_error.h), PS4 numbering) sign-extended to 64 bits and keeps nothing; whether the console checks the buffer before the calling context is unknown, so a null, empty or wrapping buffer throws. `sceCoredumpDebugTextOut` returns nothing; its text, which goes into the report on the console, is printed to stderr with the `[coredump]` prefix of `sceKernelDebugWriteCppExceptionInfo`, and a negative length or a null text with a nonzero length throws. No title is seen calling either: PPSA11386 imports `sceCoredumpWriteUserData` and does not call it before its stop (#2523) +- [sceCoredumpAttachUserFile, sceCoredumpAttachMemoryRegionAsUserFile](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - the signatures `int (uint32_t userValue, const char* path)` and `int (uint32_t userValue, const void* mem, size_t size, const char* name)` follow the [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json) IDA type database and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs) bindings. The crash handler calls both from inside its core dump handler, `sceCoredumpAttachUserFile(priority, absolutePath)` and `sceCoredumpAttachMemoryRegionAsUserFile(999, buffer, bytes, "LoggerQueueCrashDump.txt")`. The handler is never invoked here, so both return `SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER` (0x81180003, PS4 numbering from shadPS4) and attach nothing; whether the console checks the arguments before the calling context is unknown, so a null path or name and a null, empty or wrapping region throw. An empty path or name is passed through to that error, and `userValue` is not interpreted. No title is seen calling either +- [sceKernelAddTimerEvent](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) - `usec` is taken as the period in microseconds; re-adding an existing timer keeps its pending expiration count and restarts the period from the time of the call, where FreeBSD 12+ `filt_timertouch` clears pending expirations on `EV_ADD`; `usec == 0` fires on every wait. None of this is confirmed on a PS5 title +- [sceKernelWaitEqueue](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) - a level-triggered user event (`sceKernelAddUserEvent`) is reported at most once per call, as in FreeBSD `kqueue_scan` (`sys/kern/kern_event.c`), which re-queues a reported knote without `EV_CLEAR` or `EV_DISPATCH` behind the scan marker, and kevent(2), where several triggers do not produce several kevents. Not measured on a console. Events are scanned in list order and the list is not rotated, so when `num` is smaller than the number of ready level events the later ones can starve, where FreeBSD moves a reported level knote to the tail. Several completions of one AMPR or VideoOut event within one call are returned as separate entries, where FreeBSD would aggregate them; the Orbis behaviour is unconfirmed +- [AC1FtjqMCL0](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature +- [0iEKNAvT600](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature +- [2cELEBPdYQ0](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature +- [5xYxiOtOccA](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature +- [6W+shIH315Q](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature +- [7ilObq815O4](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature +- [7iq7Hfs8HBk](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature +- [AMJZhpC+siY](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature +- [E9XkHowt+dY](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature +- [GADjreszOgM](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature +- [Giyg2xGrXDU](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature +- [KC3y21wrqzc](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature +- [Qb8wS4GZUsw](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature +- [aAQZSjb2fho](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature +- [pnz21I6VjDs](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature +- [qk043QPsrZU](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature +- [uDGXwUKqi8s](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature +- [sceKernelGetCpuTemperature, 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 +- [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) - 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 + +- [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, 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), `EVFILT_READ` on another kqueue, a `NOTE_NSECONDS` period above one second with a fraction (11.0 converts the fraction with the microsecond macro) and any change to an existing timer (11.0 keeps the armed expiry and applies the new period after it, 11.4's `filt_timertouch` restarts it) throw. `EV_DISABLE`, `EV_DISPATCH` and `EV_FORCEONESHOT` follow kern_event.c: a disabled knote stays registered, stops polling or firing and is not reported, a trigger raised while it is disabled is kept and reported after `EV_ENABLE`, where `knote` would drop it, `EV_DISPATCH` reports once and then disables the knote, and `EV_FORCEONESHOT` is ignored on a new knote while on an existing one it sets `EV_ONESHOT` and fires the knote; a negative `nevents` fails with `EINVAL`. A new user event keeps its `fflags` unmasked and is not triggered by `NOTE_TRIGGER`, because `filt_userattach` reads the already cleared `kn_fflags`. The timer limit `kq_calloutmax` (`ENOMEM`) is not enforced, and closing a kqueue wakes a thread waiting on it in `kevent` with `EBADF`, where `kqueue_drain` makes the close wait for that thread. A level-triggered event is reported once per call and moved behind the other events, as in `kqueue_scan`; the `sceKernel*Equeue` calls keep their own collection loop. `NOTE_LOWAT` on a socket sets the low-water mark as FreeBSD 11's [`filt_soread` and `filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/uipc_socket.c) do, and a later change of the knote replaces it as `kqueue_register` replaces `kn_sdata`: a read event fires once the buffered bytes reach the mark, or at end of file or on a listening socket. A read mark above 1 is taken on stream sockets only: on a datagram socket Linux's `FIONREAD` reports the next datagram where FreeBSD's `sbavail` counts every queued byte, so it throws. A write event 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`. 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 +- [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. A mounted volume, whose substitute name is `\??\Volume{GUID}\`, and any other reparse point throw, as for `readlink`. `open` with `O_NOFOLLOW` returns `EMLINK` for that junction and does not open the target +- [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; 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 +- [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. 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. 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, 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. 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`), 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. 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. +- [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: 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; 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. +- [nmount](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throws: mounting file systems is not supported. Prospero Themes (PPSA90210) imports it and fails to load without it; it opens its theme library without calling it, and its strings (`/dev/ssd0.system_ex`, `/system_ex`, `exfatfs`, `fstype`, `fspath`, `from`, `ignoreacl`) point to remounting the console's `/system_ex` partition to write theme files there. +- [socketpair](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - the pair is the host's `AF_UNIX` pair on Linux and, on Windows, a loopback TCP connection for `SOCK_STREAM` or two connected UDP sockets for `SOCK_DGRAM`; `SOCK_SEQPACKET` throws on Windows. A datagram sent to a full receiver gets `ENOBUFS` on the console, waits or gets `EAGAIN` on Linux and is dropped on Windows, and on Windows a datagram `recv` after the peer closed reports `ECONNRESET` instead of the console's `ENOTCONN`, and a stream `send` after the peer closed reports `ECONNABORTED` instead of the console's `EPIPE`. `read` and `write` on a pair end go to the host like on every guest socket. `getpeername` on a pair end succeeds after the peer closed, where the console returns `ENOTCONN`, and `connect`, `bind` and `sendto` with an address on a pair end report an address-family error instead of the console's `EISCONN`. On Windows `poll` on a stream end reports `POLLHUP` without `POLLIN` after the peer's `shutdown(SHUT_WR)` or `close`, where the console reports `POLLIN`, with `POLLHUP` only after `close` +- [UDP recv/recvfrom](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) and [sceNetRecv/sceNetRecvfrom](../../../core/libs/prx/libSceNet/Export.cpp) translate Windows WSAEMSGSIZE into the copied byte count for datagram sockets, including MSG_PEEK, and retain the sender address. This follows FreeBSD 11.4 recv(2) and Winsock's documented truncated-datagram behavior; console behavior and existing title impact are unverified. +- [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 +- [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). +- [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. A libSceNet socket joins the same host wait through the native socket libSceNet lends to libkernel. `WSAPoll` rejects `POLLPRI`, so on Windows a socket's `POLLPRI` is asked as `POLLRDBAND` and `POLLIN` as `POLLRDNORM`; Winsock reports urgent data until it is read with `MSG_OOB`, where FreeBSD 11.4's `sopoll_generic` stops once the normal stream passes the mark. Guest `send` and `recv` refuse `MSG_OOB` with `EOPNOTSUPP`, so urgent data can only come from an outside peer and Windows keeps reporting `POLLPRI` after the stream passes the mark. A pipe follows FreeBSD's `pipe_poll`: the read end is readable when data is buffered, either end reports `POLLHUP` once the other end is closed, with the requested `POLLIN` and `POLLRDNORM` bits and without `POLLOUT`, which `pollscan` clears, and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. `POLLOUT` on a read end throws: FreeBSD opens both ends for reading and writing and reports it writable, and host pipes are one-way. Windows pipes cannot be waited on, so while a pipe is polled `poll` checks it with `NtQueryInformationFile` and waits on the sockets in slices of 1 ms. A regular file, a directory or a file read from app0.zar is always ready for `POLLIN`, `POLLRDNORM` and `POLLOUT` and never for `POLLPRI`, `POLLRDBAND` or `POLLWRBAND`, as FreeBSD 11.4's [`vop_stdpoll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_default.c#L563-L574) and `vop_nopoll` answer every event `poll` accepts; which of the two the console's filesystems use is not known, and they agree on these events. `/dev/random` and `/dev/urandom` follow FreeBSD 11.4's [`randomdev_poll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/dev/random/randomdev.c#L274-L285) for a seeded device: a request with `POLLIN` or `POLLRDNORM` gets only those bits back, and any other request gets every requested bit. Open descriptors 0 to 2 follow the console model `select` uses: descriptor 0 never reports an event and descriptors 1 and 2 report `POLLOUT`, whatever the host attaches to them. A closed descriptor reports `POLLNVAL`, and an open descriptor that is neither a guest socket, a libSceNet socket, a pipe, a file, a directory, the random device nor descriptor 0 to 2 (other devices, a kqueue) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), an infinite timeout with no socket or pipe to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked +- [kevent 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`. +- [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, `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 are sent, where `udp_output()` fails with 49 on the unbound path but FreeBSD can send to port 0 from a socket bound to a specific address and port. 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 +- [mincore](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `sys_mincore` (`sys/vm/vm_mmap.c`), not measured on the console: the address is rounded down to the page, length 0 returns 0, a null vector over a mapped range returns `EFAULT`, an overflowing range or any unmapped page in the range (as reported by `sceKernelVirtualQuery`) returns `ENOMEM`. For valid ranges, a page reports `MINCORE_INCORE` (0x1) when `sceKernelVirtualQuery` reports it committed and 0 when it is only reserved; actual residency, referenced and modified bits are not modelled. Length 0 returns 0 when the start page is mapped and `ENOMEM` otherwise, and the range and mapping checks come before the null vector `EFAULT`, as in `kern_mincore` Java 25 LAN / DNS (PPSA99869) imports it +- [mlockall, munlockall](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `mlockall` returns `EINVAL` for zero or unknown flags. Valid `MCL_CURRENT` throws because it would lock host stacks, heaps, modules and driver mappings along with guest memory; `MCL_FUTURE` throws because later mappings are not tracked for locking. FreeBSD 11.4's `sys_munlockall` (`sys/vm/vm_mmap.c`:1133-1165) checks `priv_check(td, PRIV_VM_MUNLOCK)` first (returning `EPERM` without it) and unwires the user map (`vm_map_unwire` over `vm_map_min`...`vm_map_max` with `VM_MAP_WIRE_HOLESOK`). Returning 0 unconditionally assumes the calling process is privileged. `munlockall` unlocks memory locked by `mlock` (`VirtualUnlock` on Windows, tolerating `ERROR_NOT_LOCKED` for pages never locked, and `munlockall` on Linux). On main, XPSemu (PPSA97358) fails to load at these imports (mlockall #4386, munlockall); their calls in QEMU `os-posix.c` execute only when `-overcommit mem-lock=on` is enabled. The call has not been observed in a run with this implementation. +- [socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) creates only `SOCK_STREAM` and `SOCK_DGRAM` sockets in `AF_INET` and `AF_INET6`. A type that FreeBSD 11.4's inet domain has no protocol for in any configuration ([`in_proto.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/netinet/in_proto.c) has no `SOCK_RDM` entry and no unknown type) fails with `EPROTOTYPE` when the protocol is 0 and `EPROTONOSUPPORT` otherwise, as [`socreate`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c) does. `SOCK_RAW`, which `in_proto.c` always has (`socreate` creates it for a privileged process), and `SOCK_SEQPACKET`, which it has only in a kernel built with `SCTP`, fail with `EPROTONOSUPPORT`; whether the console allows either is not known. `SOCK_DGRAM` with `IPPROTO_UDPLITE` and `AF_UNIX` (`socketpair` supports it) fail with `EPROTONOSUPPORT` and `EAFNOSUPPORT` where FreeBSD creates the socket +- [sigtimedwait](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) waits on a process-wide pending set instead of throwing: a signal raised while blocked stays pending and is taken by `sigwait`, `sigwaitinfo` or `sigtimedwait`, delivered to the handler when `_sigprocmask` unblocks it, or delivered by `pause`. The set covers all 128 signals and the lowest number that is pending and in the set is selected (FreeBSD specifies the lowest number only among real-time signals). Selection, `EAGAIN` on timeout, `EINVAL` for a `tv_nsec` outside 0 to 1000000000 while nothing is pending and `NULL` timeout blocking indefinitely follow FreeBSD's [sigtimedwait(2)](https://man.freebsd.org/cgi/man.cgi?query=sigtimedwait). `sigwait` returns 0 with the number in its output, as POSIX specifies, and `sigwaitinfo` and `sigtimedwait` return the number, as FreeBSD does. The guest `siginfo_t` follows FreeBSD 11's `sys/sys/siginfo.h`: 80 bytes, `si_signo` at 0, `si_code` at 8, `si_pid` at 12, `si_uid` at 16, `si_addr` at 24, `si_value` at 32 and `si_band` at 40; only `si_signo`, `si_code` (`SI_USER`) and `si_pid` (the guest pid) are written, `si_uid` stays 0 because no uid is modelled, and the console's layout is not verified. PPSA85300 imports `sigtimedwait` and fails to load without it; its only call site is VLC's `vlc_writev` ([filesystem.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/posix/filesystem.c#L243-L278)), which polls with a zero timeout to drain a `SIGPIPE` only after `writev` fails with `EPIPE`. The waits are not cancellation points and an unblocked caught signal does not interrupt them with `EINTR`, where FreeBSD's `sigwaitinfo` and `sigtimedwait` may return it. `SIGPIPE` has no host equivalent and is never raised by a host write here (see `read, write and vector I/O`), so VLC's drain finds nothing +- [pause, sigsuspend, sigwait, sigwaitinfo](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) take the pending set of `sigtimedwait` instead of throwing. `pause` waits for a pending signal that is not blocked and whose disposition can be delivered, runs its handler on the calling thread and returns -1 with `EINTR`; an ignored signal is discarded and the wait continues, and it never returns for a signal whose handler cannot run. A signal whose registered handler has `SA_SIGINFO` still throws, and one whose disposition is the default action and that has no host equivalent (every signal except 2, 4, 6, 8, 11 and 15) is neither delivered nor discarded: it stays pending until a wait takes it, where FreeBSD runs the default action, which terminates the process for most of them. A pending `SIGALRM` with the default disposition exits the process with status 142 once it is unblocked, as FreeBSD's default action does. PPSA68091, PPSA99169, PPSA99420 and PPSA97358 import them and fail to load without them. PPSA68091 calls `pause` at one site (0x8F7266), in a loop that waits while a global holds another thread's `errno` address. PPSA99169 only stores the address of `pause` in a table of symbol addresses (0x30DB1). PPSA99420 calls `pause` at 0x3BD80F6 and 0x3C9E220, `sigwait` at 0x3C9E4C7 and `sigwaitinfo` at 0x3C9E588. PPSA97358 calls `sigwait` from QEMU's `sigwait_compat` thread ([compatfd.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/compatfd.c#L28-L53)), which `qemu_signal_init` ([main-loop.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/main-loop.c#L110-L147)) starts for `SIGIO`, `SIGALRM` and `SIGBUS` when the main loop is created, so it reaches the wait at start-up, and from the vCPU thread of the dummy accelerator ([dummy-cpus.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/accel/dummy-cpus.c#L48-L56)). `sigsuspend` swaps the process-wide mask of `_sigprocmask` for its argument, waits as `pause` does through the same helper, then restores the mask through `_sigprocmask`, and returns -1 with `EINTR`; a null set throws, as the console faults in libthr instead of returning `EFAULT`. All four words of the argument replace the mask and are restored afterwards, as in `_sigprocmask`. Java 25 LAN / DNS (PPSA99869, #5501) imports it and fails to load without it, after `sysctlnametomib`, `fegetenv` and `mincore`. None of the five waits is a cancellation point +- [getsid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) returns the calling process ID for pid 0 and the calling process, and `ESRCH` for other process IDs, assuming the single guest process is its own session leader. PPSA74126 imports it and fails to load without it +- [sigaction](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do; their mask and flags are not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` and `kill` accept every number from 1 to 128 now: a blocked one stays pending on the set of `sigtimedwait`, an unblocked one whose handler is registered runs it on the calling thread, and an unblocked one that has no host equivalent and no handler still fails with `EINVAL`, where FreeBSD runs its default action. `raise` of an `SA_SIGINFO` handler still throws instead of terminating +- [kill, killpg](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follow FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128 and for negative `killpg` process groups, signal 0 only checks that the target exists, and its own pid, 0 and minus its own pid (the guest is taken to lead its own process group) signal it through `raise`, so they share `raise`'s limits above. Any other pid, and -1, which skips the caller, report `ESRCH`, so `EPERM` is never reported +- [sceKernelFcntl](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) runs the POSIX `fcntl` below and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use. The return values and error codes are unverified on a console +- [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_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`. +- [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. 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. +- [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) 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_lock](../../../core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp) (libkernel) - a lock taken again by the thread that holds it throws here. On a PS5 (firmware 12.70) that call had not returned after 2 s, as FreeBSD 11.4 libthr [`thr_pspinlock.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_pspinlock.c) spins forever +- [pthread_rwlockattr_getpshared, pthread_rwlockattr_setpshared](../../../core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp) (libkernel) - `pthread_rwlockattr_destroy` clears the attribute here, so a later get, or a set of 0, throws. On a PS5 (firmware 12.70) destroy leaves the attribute set; a get afterwards returned 0 with a stale value and a set of 0 returned 0 +- [sceKernelGetEventError](../../../core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp) (libkernel) returns the event's `data` as an `int` when `EV_ERROR` (0x4000) is set in `flags` and 0 otherwise, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/947627d10d4ef4b68ba3c0189aab6c368e70a8d2/src/kernel/eventQueue.cpp#L663) does and as FreeBSD 11's [kevent](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/kern_event.c#L1005) reports a failed change (`flags = EV_ERROR`, errno in `data`). [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_queue.pas#L643) instead returns `fflags` plus `0x80020000` when `fflags` is nonzero. Whether the console reads `data` or `fflags` and returns the raw errno or an SCE error code is unverified. No event delivered by the equeue here carries `EV_ERROR`, so the call returns 0 for every event `sceKernelWaitEqueue` reports +- [pthread_cond_reltimedwait_np](../../../core/libs/prx/libkernel/Pthread/Posix/Cond.cpp) (libkernel) takes `(cond, mutex, usec)`, a relative timeout in microseconds, and is `scePthreadCondTimedwait` with POSIX error numbers. FreeBSD 11.4 has no such function; the signature comes only from shadPS4, which registers one function for this NID and for `scePthreadCondTimedwait` ([`condvar.cpp`](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/libraries/kernel/threads/condvar.cpp#L229-L234)). shadPS4 reads a 64-bit count; here it is the 32-bit `SceKernelUseconds` of the SCE twin, as for `sem_reltimedwait_np`. Unverified on the console +- [sysctl kern.arandom](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) serves FreeBSD 11's MIB {1, 37} (CTL_KERN, KERN_ARANDOM) and the "kern.arandom" sysctlbyname lookup through std::random_device, returning min(*oldLength, 256) random bytes. Following FreeBSD's sysctl_kern_arnd, a null buffer reports min(*oldLength, 256) without writing, a null length returns 0 without writing, and requests with a new value return EPERM (1) as the node is read-only. Cryptographic quality depends on the host's random device implementation. +- [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 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 +- [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 diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md new file mode 100644 index 0000000000..25499ae3d1 --- /dev/null +++ b/docs/dev/debt/libkernel_sys.md @@ -0,0 +1,20 @@ +# libkernel_sys technical debt + +### Silent stubs +- [fchmodat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - `AT_SYMLINK_NOFOLLOW` on a symbolic link (on Windows and Linux) + +### Functional + +- [fchdir](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) changes the guest working directory to the descriptor's directory: on Windows through the path recorded when the descriptor was opened, on Linux through the `/proc/self/fd` link target. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. 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 returns success silently on Windows and Linux, since the host cannot change a link's own mode, while a regular file is changed either way. The mode maps to the host file mode, which on Windows is only the CRT's read-only bit. PPSA99621 calls it through `std::filesystem::permissions`. Console behaviour is unverified +- [pathconf](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) looks the path up as FreeBSD 11's `kern_pathconf` does (links followed, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255) and returns the constants of FreeBSD 11's UFS `ufs_pathconf` for files and directories alike: `_PC_LINK_MAX` 32767, `_PC_NAME_MAX` 255, `_PC_PATH_MAX` and `_PC_SYMLINK_MAX` 1024, `_PC_PIPE_BUF` 512, `_PC_CHOWN_RESTRICTED` and `_PC_NO_TRUNC` 1, `_PC_FILESIZEBITS` 64, `_PC_REC_MAX_XFER_SIZE` -1, `_PC_PRIO_IO` and `_PC_SYNC_IO` 0. Whether the console's filesystems report the same is not verified. It throws for the names whose UFS value depends on the mount (`_PC_ALLOC_SIZE_MIN`, `_PC_REC_INCR_XFER_SIZE`, `_PC_REC_MIN_XFER_SIZE`, `_PC_MIN_HOLE_SIZE`), the kernel options (`_PC_ACL_EXTENDED`, `_PC_ACL_PATH_MAX`, `_PC_ACL_NFS4`, `_PC_MAC_PRESENT`) or the page size (`_PC_REC_XFER_ALIGN`), and on which UFS and `vop_stdpathconf` disagree (`_PC_MAX_CANON`, `_PC_MAX_INPUT`, `_PC_VDISABLE`, `_PC_ASYNC_IO`); other names fail with `EINVAL` +- [mkdirat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `mkdir`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. On Windows, `mode` is ignored. Console behaviour is unverified +- [readlink](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows reads a symbolic link's target from its reparse data and returns it with `/` separators, only when the target is relative, as `symlink` creates them. A symbolic link with an absolute target 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, whose substitute name is `\??\Volume{GUID}\`, stays a throw, because the name left after the prefix is not a drive path. Any other reparse point throws too, where FreeBSD reports `EINVAL` for a file that is not a link +- [readlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `readlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Console behaviour is unverified +- [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 diff --git a/docs/dev/debt/libs.md b/docs/dev/debt/libs.md new file mode 100644 index 0000000000..1e07f7ef14 --- /dev/null +++ b/docs/dev/debt/libs.md @@ -0,0 +1,9 @@ +# libs technical debt + +### 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 new file mode 100644 index 0000000000..549e2ad959 --- /dev/null +++ b/docs/dev/debt/relinker.md @@ -0,0 +1,32 @@ +# relinker technical debt + +### 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; 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. +- The [instruction decoder](../../../core/relinker/codegen/src/x86/X64InstructionDecoder.cpp) reads a 16-bit displacement for `66 E8`, `66 E9` and `66 0F 80`-`8F` without REX.W, as AMD processors, the console included, do (binutils `objdump -M amd64`; REX.W selects the 32-bit form). AMD also truncates the target of every near relative branch with that prefix, rel8, LOOP and JRCXZ forms included, to 16 bits. Instruction metadata marks these branches as truncated and reports no branch target for them, and stubs refuse to move them. Compilers do not emit these forms. +- `--to-intel` 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. Linux and macOS resolve these imports through the host loader by name and are unchanged. +- 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. +- 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)). 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` ([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 new file mode 100644 index 0000000000..0f25a6a034 --- /dev/null +++ b/docs/dev/debt/shader.md @@ -0,0 +1,178 @@ +# shader technical debt + +### Build + +- winpthreads initializes a statically initialized rwlock on its first lock and fails a concurrent first lock with `EINVAL`, which `std::shared_mutex` and `std::shared_timed_mutex` ignore, so that lock does not exclude. The host `std::shared_mutex` objects in [Recompiler.cpp](../../../core/shader/recompiler/Recompiler.cpp) are locked once when created. A new host `std::shared_mutex` used on Windows needs the same. + +- 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 + +- [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): emulated color comparison supports UNORM, SNORM and float formats for 2D, 2D-array and cube views. 8-bit sRGB and UNORM (`Format8Srgb`, `Format8_8Srgb`, `Format8_8_8_8Srgb`, `Format8UNorm`, `Format8_8UNorm`, `Format8_8_8_8UNorm`) return 0 for every compare function, measured with `image_sample_c_lz` on gfx1035 over references -0.5..1.5 and texel codes 0..255. Anisotropic filters are refused, except on those 8-bit formats, where every texel comparison is 0 (not measured with an anisotropic S#). Bilinear cube comparisons of float, snorm and the other unorm formats without `DISABLE_CUBE_WRAP` are refused; on that hardware a 4x4 R32 float cube blends across faces (46 of 90 edge rows differ from a per-face clamp; face 0, ref 0.1, u=3.9 v=2.5 is 0.6016 and u=1.5 v=0.1 is 0.3984), and with `DISABLE_CUBE_WRAP` the per-face clamp matches. An 8-bit sRGB or UNORM bilinear cube without that bit returns 0, since every texel comparison is already 0 for every compare function and the selected face cannot change the result. Cube-edge sampling behavior and console coverage on PS5's gfx1013 remain unverified. +- [s_setkill](../../../core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp) (core/shader/recompiler) - decoded as `s_endpgm` for any immediate: measured on RDNA2 hardware (gfx1035), a store before it is visible and nothing after it runs ([#771](https://github.com/boykopovar/AnyPS5/pull/771)); whether the KILL status bit it sets is observable differently from `s_endpgm` is unverified +- [s_sethalt](../../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) (core/shader/recompiler) - with SIMM16[0] clear it is a no-op: the RDNA 2 ISA guide describes that value as resuming a halted wave, and a wave that executes it is not halted; measured on a gfx1035 (RDNA 2) wave: `s_sethalt 0` followed by a VALU op completes and gives the normal result. Only SIMM16[0] is read; with it set the instruction throws +- [s_subvector_loop_begin and s_subvector_loop_end](../../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (core/shader/recompiler) - follow AMD's RDNA 2 instruction set reference and a gfx1036 (wave64) measurement, pinned in [SubvectorLoop.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp); `S0` in the end instruction's `elif(S0 == 0)` is read as its SGPR operand. A wave32 shader and a destination other than an SGPR throw because neither is measured. Measured on PC RDNA2 (Radeon 610M), not on a PS5. The [specialization pass](../../../core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp) keeps a constant-condition branch whose untaken target is a loop header, since folding it removes the loop's back edge, which SPIR-V requires exactly once per loop header (SPIRV-Tools rejects the module and the NVIDIA driver 615.78.08 crashes compiling it); a loop whose back-edge block becomes unreachable in another way is not handled +- [Vertex entry merged wave info](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) - `s3` of every vertex-path shader is set to `waveSize | waveSize << 8` (vertex and primitive count of a full wave, wave index 0), or to `32 | 32 << 8` for a wave64 program on a 32-lane host subgroup, which runs it at one lane per invocation and holds no lanes 32-63. On a device whose `subgroupSupportedStages` lacks the vertex stage (lavapipe, MoltenVK), vertex and local programs run as one-lane waves: `s3` is `1 | 1 << 8`, the lane id is 0, a ballot is the invocation's own bit, a shuffle or broadcast returns the invocation's value, an elect is true and a subgroup reduction is the value itself, so no `GroupNonUniform` capability is declared. Layout taken from the prologue Mesa's ACO emits for NGG vertex shaders (`s_bfe_u64 exec` from `s3`), not measured on hardware. The driver gives every graphics program user data from `s8`, so the condition does not tell NGG from legacy vertex shaders: they just never read `s3`. A draw whose last wave is partial still reports a full wave, and so does a host wave with idle invocations (an RTX 5070 ran every vertex warp of a non-indexed triangle list with 30 of its 32 invocations). +- [s_barrier in vertex-path programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp) (recompiler) - emits no instruction when the program has no LDS or GDS access and no memory write or atomic, so no wave of the subgroup can observe another, 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) + +### Functional + +- [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. 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. +- 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. +- [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. +- [Packed horizontal gathers](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_gather4h_pck`, MIMG opcode 98): the four window texels are read raw and concatenated into a little-endian bitstream, with DMASK bit i writing stream word i to the next destination register; measured on RDNA2 (gfx1031) at every edge: the anchor is floor(x-0.5) with elements at -1, 0, +1 and +2 clamped horizontally into the width, the row is floor(y), negative rows clamp to row 0, rows at or past the height return zeros, sampler addressing modes do not affect the window, and formats wider than 32 bits per texel write zeros in every register. The emitter fetches each window texel at LOD 0 and packs its bits like `image_load_pck`, so the sampler's filter, addressing and LOD state are not used; formats whose bits are not recoverable from the view and D16 data throw. The anchor and row multiply the normalized coordinate by the image size without contraction. The stream rule was measured for R8_UNORM, RG8_UNORM and RGBA8; only RGBA8 has an execution test. `image_gather8h_pck` (opcode 99) never returns texel data on this hardware - across dmasks 0x1 to 0xFF including the upper nibble, RGBA8, RGBA16 and RGBA32 formats, 1D, 2D and 3D descriptors and normalized and unnormalized sampling it only zero-fills popcount(dmask) registers (sentinel-prefilled destinations) - so it decodes but throws at translation; its eight-texel semantics remain unmeasured. +- [MIMG BY2/BY4 loads](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_load[_mip]_by2/by4`, opcodes 0x42, 0x43, 0x4a and 0x4b) were measured on RDNA2 (gfx1035) with the [hardware oracle](../HW_ORACLE.md) for 2D images with identity swizzle. They read N texels (BY2: 2, BY4: 4) from `x & ~(N - 1)` on row y, texel by texel and component by component within each texel, with each texel converted like `image_load`. Every register 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 the N texels fit in one dword: R8 and R16 (DMASK 0x3) or RG8 (0xf) for BY2, and R8 (0xf) for BY4. With 32-bit texels (R32, RG32, RG16, RGBA8) the hardware returns zeros. Other formats, masks, dimensions, swizzles, D16/A16 and indirect descriptors throw. The stores (0x52, 0x53, 0x5a and 0x5b) decode but throw at translation. Measured on hardware, they write from the unaligned x up to the end of its dword, which is not implemented. +- [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. 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 about 20 times smaller than on hardware. The families are matched by the name RADV reports ([`NarrowSubgroupClock`](../../../core/libs/prx/libSceAgcDriver/Execution/src/SubgroupClock.cpp)); a family added to Mesa later reads the subgroup clock until it is listed. AMD's proprietary Windows driver also exposes a 20-bit subgroup clock on device 0x1114 (Radeon 860M, measured in #1615), device 0x744c (Radeon RX 7900 XT, measured in #1632), and device 0x747e (Radeon RX 7800 XT, measured in #1632); other AMD proprietary devices remain unverified. +- [`s_memtime` and `s_memrealtime`](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) on llvmpipe read only the low 32 bits of the host's nanosecond clock at either scope: Mesa's [`emit_clock`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/gallium/auxiliary/gallivm/lp_bld_nir_soa.c) builds the high dword with a left shift by 32, so it is always 0 and both counters wrap every 2^32 ns (about 4.3 s). The [shader clock test](../../../core/libs/prx/libSceAgcDriver/tests/execution/ShaderClock.cpp) compares them modulo 2^32 there +- [`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. 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/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; `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 PPSA04263's traversal addresses, not measured on a console or read from SH_MEM_BASES. The scalar aperture sources read the same two values +- [Flat accesses outside the aperture routing](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (core/shader/recompiler) - only compute stages route flat accesses, so graphics-stage flat accesses to the shared or private aperture still go to BDA. In compute, a 64-bit flat atomic whose address is in the shared aperture takes the workgroup lock (`ldsLock`) for its two-dword update, and every such atomic requests that lock, so a shader with one pays for it even on global addresses (the path is exercised by `agc_driver_global_atomics_float`). Inferred, not measured on LDS or scratch: an aperture access that is not inside one dword is done byte by byte, an unaligned aperture atomic records the BDA unaligned fault, and LDS or scratch accesses past their end are dropped or read zero. +- On a host subgroup narrower than 32 lanes, [`threadBit`](../../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) reads an EXEC written as a value (`s_mov_b32`/`s_mov_b64`) at bit (subgroup invocation id) of EXEC_LO, so lanes at or above the subgroup size read a lower lane's bit. +- [EXEC read as a value](../../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) (`s_mov_b64 sN, exec`, the `s_*_saveexec` copy, ALU sources) 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`, 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 first vertex's value there, as the geometry stage's strip gives it; the last provoking vertex throws. The patch keeps the triangle's facing with `VertexOrderCw`, checked with face culling on MoltenVK 1.4.2 (Apple M2) only. Tessellation, mesh and rect-list pipelines, primitives other than triangles, triangle strips and fans without `geometryShader`, more than 32 fragment input locations and interfaces beyond the device's geometry or tessellation limits throw. +- [v_fma_f64 and v_div_fmas_f64](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp) with a subnormal operand or result round once, as IEEE 754 fusedMultiplyAdd does with f64 denormals kept, measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1, `v_div_fmas_f64` with VCC clear and set. The f16/f64 denormal field of `FLOAT_MODE` (bits 7:6) is not read: when it flushes, the hardware flushes the operands and the result of `v_fma_f64` (tininess before rounding) and only the result of `v_div_fmas_f64`, while these keep them. Shaders with f64 instructions declare `DenormPreserve` for 64-bit floats only when `shaderDenormPreserveFloat64` is true and Vulkan reports `denormBehaviorIndependence` as `ALL`; other independence modes are not handled by the current backend because they require coupled execution modes. See [VkShaderFloatControlsIndependence](https://docs.vulkan.org/refpages/latest/refpages/source/VkShaderFloatControlsIndependence.html). Elsewhere (including lavapipe), the driver may flush f64 subnormals, and `agc_driver_f64_fma_subnormals` is skipped. +- [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_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, 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) 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 in every f16 denormal mode, measured on RDNA2 (gfx1036 with f16 denormals preserved, gfx1030 in all sixteen combinations of f16 rounding and denormal mode). When bit 7 flushes f16 results, `v_cvt_f16_f32`, `v_cvt_f16_i16` and `v_cvt_f16_u16` write a zero of the result's sign for a value that, rounded in that mode with an unbounded exponent, is below 2^-14, so a value just below 2^-14 that rounds up to it stays 0x0400; `v_ldexp_f16` keeps its rule above. The `clamp` modifier is applied before the rounding. On gfx1030 every row matches: 8,996 f32 inputs around the half denormal, normal and overflow boundaries with and without `clamp`, every 16-bit integer, and 2,470 `v_ldexp_f16` operands. +- [v_cvt_pkrtz_f16_f32 and v_pack_b32_f16 denormals](../../../core/shader/recompiler/Translation/src/ConvertInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field: `v_cvt_pkrtz_f16_f32` flushes each denormal half result to its signed zero when bit 1 is clear (0 and 1), and `v_pack_b32_f16` flushes each denormal half input to its signed zero when bit 0 is clear (0 and 2). Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with denorm16 0..3 and f32 denormals kept, over 256 pairs of 16 f32 values around the f16 denormal range for `v_cvt_pkrtz_f16_f32` and 256 pairs of 16 halves (zeros, denormals, minimum normals, ±1, the largest finite value, infinities and quiet and signaling NaNs) for `v_pack_b32_f16`. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. +- [Comparison sampling of a color texture](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_sample_c`/`image_sample_c_lz` on a texture that is not R32 float or R16 unorm) is emulated in the shader, since Vulkan compares only depth formats: the red channel is fetched, compared and, for bilinear filtering, blended (compare, then filter). Only 2D, 2D array and cube views, float/unorm/snorm formats, wrap, clamp-to-edge and clamp-to-border addressing (border red 0 for the black borders, 1 for opaque white) and point or bilinear filtering at one level are implemented, with texel offsets added to the texel indices; LOD biases and clamps are accepted, as they cannot select another level of a single-level sample. Half-border addressing, the border color table, gradients, gathers, other dimensions, anisotropy (except on the 8-bit formats below) and mip filtering throw. Under bilinear filtering, taps past a clamp-to-border edge compare the border value and blend with the texel taps, so results near the edge depend on the filter weights. AMD documentation does not say how a color format's reference is handled: it is clamped to [0, 1] for unorm and [-1, 1] for snorm, as Vulkan does for unorm depth, and not clamped for float. 8-bit sRGB and UNORM (`Format8Srgb`, `Format8_8Srgb`, `Format8_8_8_8Srgb`, `Format8UNorm`, `Format8_8UNorm`, `Format8_8_8_8UNorm`) return 0 for every compare function, measured with `image_sample_c_lz` on gfx1035 over references -0.5..1.5 and texel codes 0..255. A bilinear cube of those 8-bit formats without `DISABLE_CUBE_WRAP`, and an anisotropic S# on them, return 0 for the same reason; float, snorm and the other unorm bilinear cubes without that bit still throw. Bilinear weights are full float, not the hardware's fixed-point subtexel weights +- [Negative color-texture comparison wrap indices](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (libSceAgcDriver) use explicit `OpSRem` normalization to implement modulo; the negative-offset point-wrap regression fails with direct `OpSMod` and passes with normalization on Mesa 22.3.6 llvmpipe (LLVM 15.0.6), but negative-index wrap has not been measured on PS5 hardware. +- [Converted unorm images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (10_11_11_UNORM, read and written through an R32_UINT view) 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. 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 throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) with `buffer_atomic_add`, `buffer_atomic_swap` and `buffer_atomic_cmpswap` and bases 0 to 3 bytes past a DWORD boundary with offsets 0 to 7, an address on a DWORD boundary updates that DWORD and returns its old value, and any other address stops the dispatch, which the recompiler reports as an unaligned access fault. +- [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 two LSBs of the byte-address are ignored, thus forcing Dword alignment"). The mode comes from `SH_MEM_CONFIG.alignment_mode`, and the console's mode is unknown. MXGP 26 (PPSA30839) sets bits with `buffer_atomic_or` at odd byte offsets, so the console is not in a strict mode, where section 3.11 reports a memory violation for a misaligned atomic. In mode UNALIGNED, which Linux KFD sets, a misaligned atomic stops the dispatch (measured on gfx1031 and gfx1036), so the console is not in that mode either. 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). +- [`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 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. 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. AMD's proprietary driver does not get `robustBufferAccess2` ([RobustBufferAccess.hpp](../../../core/libs/prx/libSceAgcDriver/Execution/include/RobustBufferAccess.hpp)), so its loads keep their selections. On an RX 6700 XT with driver 32.0.21045.5002, enabling the feature broke `agc_driver_layered_volume_draw` and `agc_driver_layered_volume_vertex`: the vertex draw left the volume untouched even with the selections kept. `agc_driver_subvector_loop` failed too, with the selected loads: the driver compiled the begin instruction's `exec_lo == 0` as a test of the whole 64-bit mask, so the high half of a wave64 skipped its loop. The begin instruction now tests for an empty mask as `(exec_lo | exec_hi) == 0` rather than `exec_lo == 0 && exec_hi == 0`, and that driver compiles it correctly. The cause of the layered-volume failures was not found. +- [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/dev/debt/ulobjmgr.md b/docs/dev/debt/ulobjmgr.md new file mode 100644 index 0000000000..9bf26b6ea4 --- /dev/null +++ b/docs/dev/debt/ulobjmgr.md @@ -0,0 +1,9 @@ +# ulobjmgr technical debt + +### Silent stubs + +- [ulobjmgr](../../../core/libs/prx/ulobjmgr/Export.cpp) registers no object: `_sceUlobjmgrRegisterObject` always hands out id 0 and `_sceUlobjmgrUnregisterObject` releases nothing, as shadPS4 does + +### Unknown function info + +- [_sceUlobjmgrRegisterObject](../../../core/libs/prx/ulobjmgr/Export.cpp) (ulobjmgr) - parameters taken from shadPS4 (PS4): a nonzero 64-bit object, a nonzero 32-bit kind and a 32-bit id output; the meaning of the first two and the PS5 signature are unverified. The id bound of `_sceUlobjmgrUnregisterObject` (below 0x4000) and the raw `EINVAL` (22) return also come from shadPS4 diff --git a/docs/user/COMPATIBILITY.md b/docs/user/COMPATIBILITY.md deleted file mode 100644 index 4b62cc2934..0000000000 --- a/docs/user/COMPATIBILITY.md +++ /dev/null @@ -1,5 +0,0 @@ -# Tested compatibility - -| Game | ID | Windows | Linux | GTX 1050 Ti / i5-7500 3.4GHz | Intel(R) HD Graphic 620 / i5-7200 2.5GHz | -|----------------|-----------|-------------------|-------|------------------------------|------------------------------------------| -| Dreaming Sarah | PPSA02929 | In game, playable | ? | 60 FPS | 36 FPS | diff --git a/docs/user/INPUT_MAPPING.md b/docs/user/INPUT_MAPPING.md index 56509963d9..7ab80c3d4f 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. @@ -12,9 +12,37 @@ Supported input sources are: - `MOUSE:Left`, `MOUSE:Middle`, `MOUSE:Right`, `MOUSE:X1`, or `MOUSE:X2`. - `WHEEL:Up` or `WHEEL:Down` for pad buttons. +While a title has a mouse open (`sceMouseOpen`) and the window is focused, AnyPS5 hides and captures the host cursor, so the title's own cursor is the only one; the capture ends when the window loses focus or when the mouse-to-stick toggle (`ToggleMouse`) is on. Mouse movement reaches the title scaled from the window's presented image to the title's display resolution, so the title's cursor moves as far as the host cursor would. + Game-controller bindings are enabled automatically and are not affected by keyboard or mouse overrides in this file. -Supported actions are `Cross`, `Circle`, `Triangle`, `Square`, `L1`, `R1`, `L2`, `R2`, `L3`, `R3`, `Options`, `Up`, `Right`, `Down`, `Left`, `LeftStickLeft`, `LeftStickRight`, `LeftStickUp`, `LeftStickDown`, `RightStickLeft`, `RightStickRight`, `RightStickUp`, `RightStickDown`, `TouchLeft`, `TouchRight`, `ToggleMouse`, and `ToggleFullscreen`. +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 | +| `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: @@ -30,4 +58,75 @@ R2 = MOUSE:Right ToggleMouse = MOUSE:Middle ``` +The same file also accepts these settings, each at most once, in the form `Setting = Value`: + +| Setting | Value | Default | +|---------|-------|---------| +| `LeftStickDeadzone`, `RightStickDeadzone` | Whole percentage from 0 to 90 of the controller stick's travel that reads as centered. Outside it the travel is rescaled, so the stick still reaches its full range. Keyboard and mouse stick bindings are not affected. | `0` | +| `MouseSensitivity` | Number greater than 0 and at most 10. At 1, moving the mouse 3000 counts per second tilts the stick fully, about 5 cm per second at 1600 DPI; a larger number needs less movement. | `1` | +| `MouseVerticalScale` | Number from 0.1 to 10 that multiplies vertical mouse speed, for games that pitch slower or faster than they turn. | `1` | +| `MouseCurve` | Number from 0.2 to 5, the response exponent. Above 1 gives finer control at low speed; below 1 reaches large tilts sooner. | `1` | +| `MouseDeadzoneOffset` | Number from 0 to 0.9, the smallest tilt any movement produces as a fraction of full tilt. Set it just above the game's own deadzone: raise it if slow movement doesn't move the camera, lower it if small movements jump. | `0.2` | +| `MouseMaxSpeed` | Number from 0.1 to 10, the highest tracked speed as a multiple of the full-tilt speed. Above 1, a fast flick keeps the stick at full tilt a little longer after the mouse stops. | `1` | +| `MouseSmoothingMs` | Number from 0 to 500, the smoothing time constant in milliseconds. 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 +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 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. + +For example, for a game with a small deadzone that needs fast turns: + +```ini +MouseSensitivity = 1.6 +MouseDeadzoneOffset = 0.12 +MouseCurve = 1.3 +``` + +Earlier versions sampled the mouse every 33 ms and tilted the stick at least halfway for any movement. `MouseDeadzoneOffset = 0.5`, `MouseSmoothingMs = 0` and `MouseDecayMs = 33` come close to that behavior. + An invalid line reports the file and line number and stops input initialization. If the configured file does not exist or cannot be read, AnyPS5 reports an error. With no `anyps5-input.ini` and no `ANYPS5_INPUT_CONFIG`, the built-in mapping is used. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index ba4f6d3522..42270f520c 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -1,8 +1,18 @@ # Relinker usage +[**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. `prx/` can coexist with either `sce_module/` or `sce_modules/`. Both `sce_module/` and `sce_modules/` present, or all three absent, is an error. +Use a clean ELF executable. Place its bundled ELF modules in `sce_module/`, `sce_modules/`, or `prx/` beside the input executable, or under the parent directory selected with `--sce-module-path `. The option accepts absolute paths and paths relative to the current working directory; do not include the `sce_module` directory itself. Only files directly inside the module directories are processed. SELF/fSELF containers are skipped with a warning; unwrap them before relinking if they need to be included. `tools/unwrap_self.py ` does that for a decrypted container: it places the segments at the offsets of the SELF segment records and restores `p_offset % p_align == p_vaddr % p_align`, and it refuses a container whose load segments all look encrypted. The command-line relinker skips an ELF module as a backup copy, with a message, when its file name extends the name of another ELF module in the same directory and both have the same SONAME, such as a dumper's `libX.prx.esbak` or `libX.prxbak` beside an ELF `libX.prx`; a single backup beside a SELF container is still relinked. `prx/` can coexist with either `sce_module/` or `sce_modules/`. Both `sce_module/` and `sce_modules/` present is an error. All three absent is an error unless an explicit `--module-dir` is supplied. ```text source/ @@ -15,6 +25,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 @@ -33,36 +45,114 @@ macOS output (x86-64 Mach-O, which runs under Rosetta on Apple silicon): relinker --macos source/input.elf eboot ``` -Add `--to-intel` for Intel hosts. The output format defaults to Linux ELF regardless of the filename; `.exe` alone does not select Windows. +Add `--to-intel` for Intel hosts, or `--to-rosetta` for Rosetta; a title that runs under Rosetta on Apple silicon is relinked with `--macos --to-rosetta`. `--to-intel` rewrites AMD-specific instructions found in the executable and bundled modules; it cannot translate AMD-specific instructions generated by a title at runtime, so such titles require an AMD host. The output format defaults to Linux ELF regardless of the filename; `.exe` alone does not select Windows. + +Titles run natively, so the host CPU needs the extensions of the PS5's Zen 2 CPU that compiled code uses without checking, among them AVX2, FMA, F16C, BMI1, BMI2, LZCNT and MOVBE. On Linux and Windows a converted title logs the host CPU's extensions at start-up and stops before running any title code, naming the CPU and the missing extensions, if one is absent ([list](../dev/debt/libkernel.md)). + +The executable, registry, Windows import diagnostics, icon and desktop entry output files must not refer to the input executable or a bundled module being converted, including through a hard link or symbolic link. These collisions fail before any output is written. An existing output file can be replaced if it is separate from those inputs. ## Options All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` defaults to `$ORIGIN/libs`. -| Option | Effect | -|-------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `--windows` | Produce a Windows PE executable. | -| `--macos` | Produce an x86-64 Mach-O executable, with bundled modules as Mach-O libraries in `app0/sce_module`. Conflicts with `--windows`. | -| `--windows-diagnostics` | Include startup dependency diagnostics. Requires `--windows`. | -| `--windows-gui` | Select the Windows GUI subsystem instead of the console subsystem. Requires `--windows`. | -| `--to-intel` | Convert supported AMD-only instructions in the executable and bundled modules. Unsupported instructions or unreachable conversion stubs cause an error. | -| `unused-filter=0` | Keep all imported NID references. | -| `unused-filter=1` | Filter unused non-PLT imports using control-flow and GOT access analysis; preserve PLT imports. | -| `unused-filter=2` | Apply strict unused-import analysis and compact the PLT. Unsupported analysis cases cause an error. | -| `--registry` | Write `.registry.json` beside the output executable, and `..registry.json` for each converted bundled module. | -| `--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. | -| `--skip-sce-module` | Deprecated. Skip all bundled module processing. | -| `--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. | -| `--lazy-binding` | Deprecated. Enable lazy symbol binding instead of eager binding. Incompatible with bundled ELF modules. | +| Option | Effect | +|-------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `--help` | Print usage to stdout and exit with code `0`; input and output paths are not required. | +| `--windows` | Produce a Windows PE executable. | +| `--macos` | Produce an x86-64 Mach-O executable, with bundled modules as Mach-O libraries in `app0/sce_module`. Conflicts with `--windows`. | +| `--windows-diagnostics` | Include startup dependency diagnostics. Requires `--windows`. | +| `--windows-gui` | Select the Windows GUI subsystem instead of the console subsystem. Requires `--windows`. | +| `--windows-fixed-base
` | Map the lowest ELF load page at this fixed Windows address, given in decimal or hexadecimal (`0x`). Requires `--windows`, a 64 KiB-aligned address from `0x20000` through `0x7fffffff0000`, and a complete image fitting in the user address space. The main executable has no ASLR or base relocations; an occupied address fails to load. Bundled modules keep their normal placement. | +| `--trace-imports` | Pass each function import through the libc.prx call tracer, see [tracing system library calls](#tracing-system-library-calls). Windows and Linux output only; 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`. | +| `--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: 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. | +| `--registry` | Write `.registry.json` beside the output executable, and `..registry.json` for each converted bundled module. | +| `--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`. | +| `--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. | +| `--lazy-binding` | Deprecated. Enable lazy symbol binding instead of eager binding. Incompatible with bundled ELF modules. | -Specify `unused-filter=0|1|2` without `--`, at most once. Unknown options and extra positional arguments are errors. There is no `--help` flag; invoking `relinker` without arguments prints the usage syntax and exits with an error. +Specify `unused-filter=0|1|2` without `--`, at most once. Unknown options and extra positional arguments are errors. Use `relinker --help` to print the usage syntax to stdout and exit with code `0` without converting files. Invoking `relinker` without arguments prints the usage syntax to stderr and exits with code `1`. The `--skip-sce-module`, `--exclude-sce-module `, `--skip-syscall-check`, and `--lazy-binding` flags are deprecated. If the application runs with these flags enabled, it will be extremely unstable and unsuitable for general use. These flags are only for debugging. +`--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`, `settings` and `settings_sha256` are accepted in any file; only `main_target` is 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 seven 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` 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 (`ET_DYN`); `ET_EXEC` output from `--linux-fixed-base`, PE and Mach-O outputs are refused. The file must have `main_target`, so verify the generated manifest, not a hand-written file. 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, or `relinker --macos --macos-fixed-base 0x400000 source/input.elf app` on macOS. Use these options only when the executable requires a fixed address. Without them, Linux output is a position-independent executable as before, and macOS output places the lowest ELF load page at `0x140010000`. + +The same base is applied to executables selected with `--executable`; bundled PRX modules are not rebased. + Paths are relative to the output executable: ```text @@ -71,42 +161,206 @@ 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, `app0/` must not contain symbolic links: a directory listing gives the title the link's own type, not the type of the file or directory it points to, so a title that walks its directories skips a linked directory. 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). On Windows, a self-built `libs/` also needs `libgcc_s_seh-1.dll`, `libstdc++-6.dll` and `libwinpthread-1.dll` from the `mingw64/bin` directory of the toolchain that built the libraries; the release archives already contain them. The libraries are loaded without searching `PATH`, so a copy elsewhere on the system is not used, and a library that needs one of them fails with error 126 (the specified module could not be found) although its `.prx` file is present. -On Windows, direct memory (`sceKernelAllocateDirectMemory`, up to 13824 MiB per title) is committed in full when the title allocates it, not when its pages are first used. The system commit limit (installed memory plus page file size, the second value of Committed in Task Manager) must cover it together with all other committed memory. Otherwise the allocation throws `create direct memory backing of 0x<n> bytes (<m> MiB)` with the Windows error; enlarge the page file or close other applications. +On Windows, direct memory (`sceKernelAllocateDirectMemory`, up to 13824 MiB per title) is committed when the title maps it, not when the title allocates it or first uses its pages. The system commit limit (installed memory plus page file size, the second value of Committed in Task Manager) must cover the mapped direct memory together with all other committed memory. Otherwise `sceKernelMapDirectMemory` throws `replace guest placeholder with resident direct memory` with the Windows error (`commit direct memory pages` when a page is mapped at a second address, `keep unmapped direct memory` when it is unmapped); enlarge the page file or close other applications. + +On Linux, when the Vulkan driver imports dma-buf memory (not the NVIDIA proprietary driver), shared direct memory is imported through `/dev/udmabuf`, and the user who runs the game needs read-write access to it. Many distributions create it as `root:kvm` with mode `0660`: add the user to the `kvm` group and log in again (an ACL such as `setfacl -m u:$USER:rw /dev/udmabuf` lasts until the next reboot). Without access, startup prints `[gpu] open /dev/udmabuf: Permission denied`, these ranges are copied instead of imported, and GPU stores to them through FLAT/GLOBAL addresses fail with `BDA access failed`. 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 .\app.exe ``` +A Windows executable started without a standard output or error handle, such as a `--windows-gui` build started from Explorer, writes that output to `anyps5.log` beside it, replacing the previous run's file. Without write access to that directory it runs without the log. + +### 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: + +```sh +APS5_PAD_INPUT_FILE=/tmp/anyps5-pad ./app.elf +python3 tools/pad_input.py /tmp/anyps5-pad cross +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 +``` + +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. 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. +The window presents with vertical sync (`fifo`). Set `ANYPS5_PRESENT_MODE` to `fifo_latest_ready`, `mailbox` or `immediate` to keep the game from waiting for the display: `fifo_latest_ready` and `mailbox` still show whole frames at the display's refresh and drop the ones in between, `immediate` presents at once and can tear. The start fails on any other value and when the surface does not offer the mode (`fifo_latest_ready` needs `VK_KHR_present_mode_fifo_latest_ready` or its EXT form). Flips stay paced at 59.94 Hz in every mode. A Wayland compositor can stop delivering frames to a window it does not show, for example one on another workspace: on Hyprland with the NVIDIA driver a `fifo` swapchain then presents about once per second, while `fifo_latest_ready` and `immediate` keep the game at its normal rate. + +On Windows, the OpenCL, OpenGL, and Vulkan Compatibility Pack can install Mesa Dozen, which exposes D3D12 devices through Vulkan. Some D3D12 drivers can crash while the Vulkan loader enumerates adapters, even when AnyPS5 would select a native Vulkan device. If this happens and the system has another Vulkan driver, disable Dozen for the current PowerShell session before launching AnyPS5: + +```powershell +$env:VK_LOADER_DRIVERS_DISABLE = '*dzn*' +.\app.exe +``` + +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 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. `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 @@ -124,9 +378,17 @@ python3 tools/import_audit.py app.registry.json app.libc.prx.guest.prx.registry. | `implemented` | A built library exports the NID and its function is not a throwing stub. | | `stub` | Exported, but the whole function calls `NotImplemented_nid_no_patch`: the game loads and throws when it calls it. | | `absent` | No built library exports the NID: the loader fails. | -| `module` | The import names a file found in `--modules` (the title's own library); its exports are not checked. | +| `module` | The import names a file found in `--modules` (the title's own library), which is trusted to provide it. | -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. +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. + +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 | |--------|--------| @@ -136,6 +398,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 new file mode 100644 index 0000000000..66798820d2 --- /dev/null +++ b/tools/build_web.py @@ -0,0 +1,185 @@ +import argparse +import hashlib +import io +import json +import os +from pathlib import Path +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): + source = ROOT / "demo/web" + for file in source.iterdir(): + if file.is_file(): + shutil.copy2(file, output / file.name) + (output / ".nojekyll").touch() + + +def request(url): + headers = {"User-Agent": "AnyPS5-Pages"} + token = os.environ.get("GITHUB_TOKEN") + if token and url.startswith("https://api.github.com/"): + headers["Authorization"] = f"Bearer {token}" + return urllib.request.urlopen(urllib.request.Request(url, headers=headers), timeout=60) + + +def publish_libraries(output, 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") + manifest = {"tag": tag, "url": release["html_url"], "platforms": {}} + destination = output / "libraries" + destination.mkdir(exist_ok=True) + for platform in ("windows", "linux"): + name = f"prx-{platform}-{tag}.zip" + matches = [asset for asset in release["assets"] if asset["name"] == name] + if len(matches) != 1: + raise ValueError(f"Missing or ambiguous release asset: {name}") + asset = matches[0] + temporary = destination / f"{platform}.download" + try: + digest = hashlib.sha256() + size = 0 + with request(asset["browser_download_url"]) as response, temporary.open("wb") as target: + while chunk := response.read(1024 * 1024): + size += len(chunk) + if size > asset["size"] or size > 512 * 1024 * 1024: + raise ValueError("Release archive exceeds expected size") + digest.update(chunk) + target.write(chunk) + if size != asset["size"]: + raise ValueError("Incomplete release archive") + checksum = digest.hexdigest() + if asset.get("digest") and asset["digest"] != "sha256:" + checksum: + raise ValueError("Release archive checksum mismatch") + with zipfile.ZipFile(temporary) as archive: + files = archive.infolist() + if not files or len(files) > 4096: + raise ValueError("Invalid library archive") + 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}") + path = f"libraries/{platform}-{checksum}.zip" + temporary.replace(output / path) + manifest["platforms"][platform] = {"path": path, "size": size, "expandedBytes": expanded, "sha256": checksum} + finally: + temporary.unlink(missing_ok=True) + (output / "release.json").write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8") + print(f"Published library assets from {tag}") + + +def 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")) + 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(): + parser = argparse.ArgumentParser() + parser.add_argument("--build", type=Path, required=True) + 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() + output = args.build.resolve() / "web" + if args.release: + publish_release(output) + print(output) + return + 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) + print(output) + + +if __name__ == "__main__": + main() diff --git a/tools/check_compile.py b/tools/check_compile.py new file mode 100644 index 0000000000..a251673abf --- /dev/null +++ b/tools/check_compile.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +import argparse +import json +import os +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", "-MQ", "-MJ") + + +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: + raise ValueError(result.stderr.strip() or "not a git checkout") + return Path(result.stdout.strip()) + + +def entries(build): + if not build.is_dir(): + raise ValueError(f"{build} is not a directory; configure the build first") + manifest = build / "compile_commands.json" + if manifest.is_file(): + return json.loads(manifest.read_text(encoding="utf-8")) + ninja = shutil.which("ninja") + if ninja is None: + raise ValueError(f"no {manifest} and ninja is not on PATH") + result = capture([ninja, "-C", str(build), "-t", "compdb", "-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, root): + directory = Path(entry["directory"]).resolve() + source = Path(entry["file"]) + if not source.is_absolute(): + source = directory / source + source = source.resolve() + try: + return source.relative_to(Path(root).resolve()).as_posix() + except ValueError: + return source.as_posix() + + +def index(build, root): + commands = {} + for entry in entries(build): + commands.setdefault(entry_key(entry, root), []).append(entry) + return commands + + +def changed(root, 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"]) + if untracked.returncode != 0: + raise ValueError(f"git ls-files: {untracked.stderr.strip()}") + names = dict.fromkeys(result.stdout.split("\0") + untracked.stdout.split("\0")) + return [name for name in names if name.endswith(CPP)] + + +def 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 = 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 + while index < len(arguments): + argument = arguments[index] + if argument in DROP_WITH_VALUE: + index += 2 + continue + if argument in DROP or argument.startswith(("-MT", "-MF", "-MQ", "-MJ")): + index += 1 + continue + if not argument.startswith("-"): + candidate = Path(argument) + if not candidate.is_absolute(): + candidate = directory / candidate + if candidate.resolve() == wanted: + index += 1 + continue + trimmed.append(argument) + index += 1 + trimmed += ["-fsyntax-only", str(wanted)] + return trimmed + + +def check_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 = True + output = (result.stdout or "") + (result.stderr or "") + 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") + args = parser.parse_args(argv) + try: + root = repository() + files = [Path(path) for path in args.paths] or [root / path for path in changed(root, args.base)] + if not files: + print(f"no changed translation unit against {args.base}") + return 0 + checked, failed = check(root, args.build, files, args.jobs) + except (json.JSONDecodeError, OSError, ValueError) as error: + parser.error(str(error)) + if failed: + print(f"{failed} of {checked} translation units did not compile", file=sys.stderr) + return 1 + if not checked: + print(f"no translation unit to check in {args.build}", file=sys.stderr) + return 1 + print(f"{checked} translation units compile") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/check_conventions.py b/tools/check_conventions.py index eb53a431d8..132f275710 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -1,51 +1,95 @@ 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 PurePosixPath +from pathlib import Path, PurePosixPath +from urllib.parse import unquote + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from nid_names import CPP_TOKEN, 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 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 in docs/dev/TechnicalDebt.md#silent-stubs, by their file or by name with a path in their library"), + "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>; 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|pkg_search_module|find_library)\b") +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]+)") -SECTION = re.compile(r"^###\s*(.+?)\s*$", re.M) +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", *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 @@ -54,23 +98,40 @@ class Check: self.added = defaultdict(list) self.status = {} self.numstat = {} - self.tree = set(git("ls-tree", "-r", "--name-only", head).splitlines()) + 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} - for line in git("diff", "--name-status", "--no-renames", self.base, head).splitlines(): - status, path = line.split("\t", 1) - self.status[path] = status - for line in git("diff", "--numstat", "--no-renames", self.base, head).splitlines(): + 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) - path = None - for line in git("diff", "-U0", "--no-color", "--no-renames", "--no-prefix", self.base, head).splitlines(): - if line.startswith("+++ "): - path = None if line == "+++ /dev/null" else line[4:] + 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", *diff).split("\n"): + if line.startswith("diff --git "): + path, number = next(paths), None elif line.startswith("@@"): number = int(re.search(r"\+(\d+)", line).group(1)) - elif line.startswith("+") and path: + 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)) @@ -78,25 +139,50 @@ 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 = [(text, links("docs/dev/TechnicalDebt.md", text)) for text in self.show("docs/dev/TechnicalDebt.md").splitlines()] if "docs/dev/TechnicalDebt.md" in self.tree else [] + 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 = None + source = self.show(path).split("\n") + masked = cpp_text("\n".join(source), preserve_comments=True).split("\n") for number, text in lines: - bare = STRINGS.sub('""', text) + bare = masked[number - 1] if ("//" in bare or "/*" in bare) and not ALLOWED_COMMENT.match(bare): self.report("comment", path, number) if EXTENSION.search(bare): 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)) match = EXPORT.search(bare) - if match and path.startswith("core/libs/prx/") and not match.group(1).endswith("_nid_no_patch"): + if match and core and path.startswith("core/libs/prx/") and not match.group(1).endswith(("_nid_no_patch", "_nid_no_patch_cut")): source = source or self.show(path).splitlines() body = function_body(source, number - 1) if body is None: @@ -105,27 +191,62 @@ 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 "docs/dev/TechnicalDebt.md" in self.status: + if any(is_debt(path) for path in self.status): self.findings = [finding for finding in self.findings if finding[0] != "comment"] def duplicates(self, exports): if not exports: return - pattern = r"\bAPS5_VABI\s+(" + "|".join(exports) + r")\s*\(" - result = subprocess.run(["git", "grep", "-nE", pattern, self.head, "--", "core/libs/prx"], capture_output=True, encoding="utf-8", errors="replace") - for line in result.stdout.splitlines(): - _, path, _, text = line.split(":", 3) - name = EXPORT.search(text).group(1) - own, number = exports[name] - if library(path) != library(own) and not text.rstrip().endswith(";"): - self.report("duplicate-export", own, number, f"{name} is also in {library(path)}") + result = subprocess.run(["git", "-c", "core.quotePath=false", "grep", "-nF", "--full-name", "-f", "-", self.head, "--", ":(top)core/libs/prx"], input="\n".join(exports) + "\n", capture_output=True, encoding="utf-8", errors="replace") + hits = [line.split(":", 3)[1:] for line in filter(None, result.stdout.split("\n"))] + self.read(path for path, _, _ in hits) + sources = {} + for path, number, text in hits: + if path not in sources: + sources[path] = cpp_text(self.show(path)).split("\n") + source = sources[path] + match = next((match for match in EXPORT.finditer(source[int(number) - 1]) if match.group(1) in exports), None) + if match is None: + continue + name = match.group(1) + own, own_number = exports[name] + if library(path) != library(own) and "tests" not in PurePosixPath(path).parts and function_body(source, int(number) - 1) is not None: + 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"): + if path == "cmake/SystemDeps.cmake": + continue for number, text in lines: if SYSTEM_DEPENDENCY.search(text.split("#", 1)[0]): self.report("system-dependency", path, number, text.strip()) @@ -134,29 +255,122 @@ 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") + values = {key: value for key, _, value in (entry.partition("\n") for entry in result.stdout.split("\0") if entry)} + return {path for key, path in values.items() if key.endswith(".path") and values.get(f"{key[:-5]}.url")} + def repository(self): + gitlinks = {path for path, mode in self.modes.items() if mode == "160000" and path.startswith("3rdparty/")} + checked = gitlinks if ".gitmodules" in self.status else gitlinks & self.status.keys() + if checked: + for path in sorted(checked - self.submodules()): + self.report("system-dependency", path, 0, "submodule without a url in .gitmodules") for path, (added, deleted) in self.numstat.items(): if path.startswith("3rdparty/"): continue + if self.modes.get(path) == "160000": + 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: + 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): + continue + source = self.show(path).splitlines() + lines = [(number, KEY.match(re.sub(r"(^|\s)#.*", "", text))) for number, text in enumerate(source, 1)] + lines = [(number, len(match[1]), match[2].strip("'\""), match[3] or "") for number, match in lines if match] + triggers = [entry for entry in lines if entry[1] == 0 and entry[2] == "on"] + if not triggers or pull_request_filtered(lines, triggers[0]): + continue + for number, name, needs, condition in jobs(lines, source): + if name != "eligibility" and ("eligibility" not in needs or "needs.eligibility.outputs." not in condition): + self.report("ci-gate", path, number, name) + def commits(self): for line in git("log", "--no-merges", "--format=%h %s", f"{self.base}..{self.head}").splitlines(): sha, subject = line.split(" ", 1) 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)) + documented = set(env_reference.documented_pattern(env_reference.PREFIXES).findall(self.show(doc))) if doc in self.tree else set() + 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) @@ -166,11 +380,11 @@ class Check: if not sections.get(name, "").strip(): self.report("pr-template", "", 0, f"section '{name}' is missing or empty") checklist = sections.get("Checklist", "") - for item in re.findall(r"^\s*-\s*\[ \]\s*(.+)$", checklist, re.M): + for item in re.findall(r"^\s*[-*+]\s*\[ \]\s*(.+)$", checklist, re.M): 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") @@ -182,13 +396,99 @@ 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-z]+:|#", target): + if re.match(r"[A-Za-z][A-Za-z0-9+.-]*:|//|#", target): continue target = target.split("#", 1)[0].split("?", 1)[0] - result.append((target, posixpath.normpath(target.lstrip("/") if target.startswith("/") else str(PurePosixPath(path).parent / target)))) + decoded = unquote(target) + result.append((target, posixpath.normpath(decoded.lstrip("/") if decoded.startswith("/") else str(PurePosixPath(path).parent / decoded)))) + return result + + +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: + return None + parts = match.group(1).split("/") + return parts[3] if parts[1:3] == ["libs", "prx"] and len(parts) > 3 else parts[1] + + +def silent_stub_debt(text, path=DEBT): + debt = [] + section_level = 0 + for line in text.splitlines(): + heading = re.match(r"^(#{1,6})\s+(.+?)\s*$", line) + if heading: + level = len(heading[1]) + if heading[2] == "Silent stubs": + section_level = level + elif level <= section_level: + section_level = 0 + if section_level: + debt.append((line, links(path, line))) + return debt + + +def block(lines, start): + _, indent, _, _ = lines[start] + end = start + 1 + while end < len(lines) and lines[end][1] > indent: + end += 1 + return lines[start + 1:end] + + +def pull_request_filtered(lines, trigger): + _, _, _, value = trigger + if value: + return "pull_request" not in re.findall(r"[A-Za-z_]+", value) + triggers = block(lines, lines.index(trigger)) + for index, (_, indent, name, value) in enumerate(triggers): + if name == "pull_request" and indent == triggers[0][1]: + return any(entry[2] in ("paths", "paths-ignore") for entry in block(triggers, index)) + return True + + +def field(source, entry): + number, indent, _, value = entry + for line in (line for line in source[number:] if line.strip()): + if len(line) - len(line.lstrip(" ")) <= indent: + break + value += " " + line.strip() + return value + + +def jobs(lines, source): + top = [index for index, entry in enumerate(lines) if entry[1] == 0 and entry[2] == "jobs"] + if not top: + return [] + entries = block(lines, top[0]) + result = [] + for index, (number, indent, name, _) in enumerate(entries): + if indent != entries[0][1]: + continue + 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 @@ -205,18 +505,65 @@ 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 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 in AGENT_FILES: - return False + if p.name.lower() in AGENT_FILES: + 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: + return match.group() + blank = re.sub(r"[^\n]", " ", match.group()) + return blank if match.group("comment") is not None else '""' + blank[2:] + + return CPP_TOKEN.sub(mask, text) + + def function_body(lines, start): - text = "\n".join(lines[start:start + 400]) - text = re.sub(r"//[^\r\n]*|/\*.*?\*/", "", STRINGS.sub('""', text), flags=re.S) + text = cpp_text("\n".join(lines[start:start + 400])) brace, semicolon = text.find("{"), text.find(";") if brace < 0 or 0 <= semicolon < brace: return None @@ -237,7 +584,8 @@ def output(check, repo): errors += level == "error" text = f"{message}" + (f": {detail}" if detail else "") if actions: - location = f" file={path},line={max(line, 1)}," if path else " " + escaped_path = path.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A").replace(":", "%3A").replace(",", "%2C") + location = f" file={escaped_path},line={max(line, 1)}," if path else " " print(f"::{level}{location}title={rule}::" + text.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A")) else: print(f"{path or '(pull request)'}{f':{line}' if line else ''}: {level}: [{rule}] {text}") @@ -271,12 +619,16 @@ 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"]) elif args.title is not None or args.body: - check.pull_request(args.title or "", open(args.body, encoding="utf-8").read() if args.body else "") + check.pull_request(args.title or "", open(args.body, encoding="utf-8-sig").read() if args.body else "") raise SystemExit(output(check, os.environ.get("GITHUB_REPOSITORY", "boykopovar/AnyPS5"))) diff --git a/tools/check_main_sync.py b/tools/check_main_sync.py new file mode 100644 index 0000000000..e1f41677d7 --- /dev/null +++ b/tools/check_main_sync.py @@ -0,0 +1,125 @@ +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) + + +if __name__ == "__main__": + 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() + 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)) + sys.stdout.reconfigure(errors="backslashreplace") + print(render(data)) + if data["conflicts"]: + raise SystemExit(1) + if args.strict and data["behind"]: + raise SystemExit(1) diff --git a/tools/check_skips.py b/tools/check_skips.py index f2f8fe1aa7..99628018e3 100644 --- a/tools/check_skips.py +++ b/tools/check_skips.py @@ -3,22 +3,41 @@ import re import xml.etree.ElementTree as ET from pathlib import Path -DEVICE_LIMITATION = re.compile(r"skipped,.*(subgroup|the device has no|the device reports|no display)", re.IGNORECASE) +DEVICE_LIMITATION = re.compile(r"skipped,.*(subgroup|the device (has no|reports|lacks|does not|imports no|flushes|binds fewer)|no display|no usable Vulkan device|host imports are unavailable)", re.IGNORECASE) 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" +def high_refresh_limitation(output): + reasons = [] + for line in output.splitlines(): + reason = line.strip() + if reason.lower().startswith("skipped,"): + reason = reason[len("skipped,"):].strip() + if reason: + reasons.append(reason) + return bool(reasons) and all(HIGH_REFRESH_LIMITATION.fullmatch(reason) for reason in reasons) + + def skips(path): for case in ET.parse(path).getroot().iter("testcase"): - if case.find("skipped") is None: + if case.get("status") == "disabled": + raise ValueError(f"{path}: testcase {case.get('name')!r} is disabled and was not run") + skipped = case.find("skipped") + if skipped is None: continue name = case.get("name") if not name: - continue - output = case.find("system-out") - yield name, (output.text or "") if output is not None else "" + raise ValueError(f"{path}: skipped testcase has no name") + 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__": @@ -27,18 +46,21 @@ if __name__ == "__main__": 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() - found = sorted(skips(args.junit)) + 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 if args.job == "windows": - unexpected = [name for name, output in found if not name.startswith(WINDOWS_PREFIX) and not NO_DISPLAY.search(output)] - expected = f"any {WINDOWS_PREFIX}* test, which the runner has no Vulkan driver for, or a test that reports no display or Vulkan device" + unexpected = [name for name, output in found if not name.startswith(WINDOWS_PREFIX) and not NO_DISPLAY.search(output) and not 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)] - expected = "any test that reports the device limitation it needs, such as narrow subgroups, a missing extension or no display for a Vulkan window" + unexpected = [name for name, output in found if not DEVICE_LIMITATION.search(output) and not high_refresh_limitation(output)] + expected = "any test that reports the device limitation it needs, such as narrow subgroups, a missing extension, no display for a Vulkan window or a display without a 120 Hz mode" for name in unexpected: print(f"error: {name} skipped") if unexpected: diff --git a/tools/check_test_registration.py b/tools/check_test_registration.py index 7e926d3561..fcce86cbd1 100644 --- a/tools/check_test_registration.py +++ b/tools/check_test_registration.py @@ -1,27 +1,49 @@ 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 = { - "agc_driver_recorder_tests", -} +KNOWN_UNRUN = set() +SCRIPT = re.compile(r"(?:[\w.-]+/)*test_\w+\.py") def built(build): - listed = subprocess.run(["ninja", "-C", str(build), "-t", "targets", "all"], - capture_output=True, text=True, check=True).stdout + program, generator = None, None + for line in (Path(build) / "CMakeCache.txt").read_text(encoding="utf-8").splitlines(): + if line.startswith("CMAKE_MAKE_PROGRAM:"): + program = line.partition("=")[2] + elif line.startswith("CMAKE_GENERATOR:"): + generator = line.partition("=")[2] + if generator not in ("Ninja", "Ninja Multi-Config"): + raise ValueError(f"test registration audit requires a Ninja generator, got {generator!r}") + if not program or program.endswith("-NOTFOUND"): + raise FileNotFoundError(f"CMAKE_MAKE_PROGRAM is not set in {Path(build) / 'CMakeCache.txt'}") + 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, encoding=encoding, check=True).stdout names = set() for line in listed.splitlines(): path, _, rule = line.partition(": ") if "EXECUTABLE" not in rule: continue - found = re.match(rf"^{TEST_OUTPUT}([A-Za-z0-9_.]+)$", path) + found = re.match(rf"^{TEST_OUTPUT}(?:.*[\\/])?([^\\/]+)$", path) if not found: continue - name = found.group(1).rstrip(".") + name = found.group(1) if name.endswith(".exe"): name = name[:-4] if name: @@ -29,15 +51,73 @@ def built(build): return names +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(code): + if command.start() < parsed_until: + continue + position, arguments = command.end(), [] + while position < len(content): + if content[position].isspace(): + position += 1 + continue + if content[position] == ")": + parsed_until = position + 1 + yield command.group(1).lower(), arguments + break + if content[position] == "#": + position = content.find("\n", position) + if position < 0: + break + continue + token = ARGUMENT.match(content, position) + if token is None: + raise ValueError(f"invalid CTest argument at position {position}") + argument = token.group(0) + if argument.startswith('"'): + argument = re.sub(r"\\(.)", lambda match: {"n": "\n", "r": "\r", "t": "\t"}.get(match[1], match[1]), argument[1:-1], flags=re.S) + elif token.group("equals") is not None: + width = len(token.group("equals")) + 2 + argument = argument[width:-width] + arguments.append(argument) + position = token.end() + + def registered(build): names = set() - for file in Path(build).rglob("CTestTestfile.cmake"): - for line in file.read_text(errors="replace").splitlines(): - found = re.match(r"\s*add_test\s*\((.*)\)\s*$", line) - if not found: + pending = [Path(build).resolve()] + visited = set() + while pending: + directory = pending.pop().resolve() + file = directory / "CTestTestfile.cmake" + if directory in visited or not file.is_file(): + continue + visited.add(directory) + 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: + 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]: + if name in latest: + latest[name][1] = value.upper() in ("1", "ON", "YES", "TRUE", "Y") + for arguments, disabled in tests: + if disabled: continue - for token in re.findall(r'"([^"]*)"|(\S+)', found.group(1)): - argument = token[0] or token[1] + for index, argument in enumerate(arguments): + if index and (argument.startswith("-") or re.match(r"^[A-Za-z_]\w*=", argument)): + continue base = re.split(r"[\\/]", argument)[-1] if base.endswith(".exe"): base = base[:-4] @@ -46,13 +126,57 @@ def registered(build): return names +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)] + + if __name__ == "__main__": 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") + 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() - every = sorted(built(args.build)) - run = registered(args.build) + 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") + raise SystemExit(1) + print(f"{len(scripts)} test script(s), each run by an add_test or a workflow") + raise SystemExit + 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}") 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..d1936ae21a --- /dev/null +++ b/tools/configure_seed.py @@ -0,0 +1,55 @@ +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(): + 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() + 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)} + 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") + 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 new file mode 100644 index 0000000000..1460f46bca --- /dev/null +++ b/tools/debt_split.py @@ -0,0 +1,244 @@ +import argparse +import difflib +import posixpath +import re +import subprocess +import sys +from pathlib import Path + +from check_conventions import DEBT as INDEX, debt_name + +DEBT_DIR = "docs/dev/debt" +HEADING = re.compile(r"^(#{1,6}) (.+?)\s*$") +LINK = re.compile(r"\]\(([^)\s]+)\)") + + +def parse(text): + items = [] + section = None + current = None + blanks = [] + for line in text.splitlines(): + heading = HEADING.match(line) + if heading: + section = heading[2] if len(heading[1]) == 3 else section + current = None + items.append(["heading", section, [line]]) + elif line.startswith("- "): + current = ["entry", section, [line]] + items.append(current) + blanks = [] + elif current and (line.startswith(" ") or line.startswith("\t")): + current[2] += blanks + [line] + blanks = [] + elif line.strip(): + current = None + items.append(["text", section, [line]]) + else: + blanks.append(line) + return [(kind, section, "\n".join(lines)) for kind, section, lines in items] + + +def key(kind, text): + return debt_name(text) if kind == "entry" else None + + +def relink(text, source, destination): + def replace(match): + target = match.group(1) + 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 + moved = posixpath.relpath(resolved, posixpath.dirname(destination)) + return "](" + moved + ("#" + anchor if anchor else "") + ")" + return LINK.sub(replace, text) + + +def debt_file(name): + return f"{DEBT_DIR}/{name}.md" + + +def render(title, sections, order): + out = [title, ""] + for section in sorted(sections, key=lambda section: order.index(section) if section in order else len(order)): + out += [f"### {section}", ""] + sections[section] + [""] + return "\n".join(out) + + +def read_debt(root, name): + path = root / debt_file(name) + sections = {} + if path.exists(): + section = None + 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, []) + elif kind != "heading" and section: + sections[section].append(text) + 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} + if not sections: + if path.exists(): + path.unlink() + return + path.parent.mkdir(parents=True, exist_ok=True) + write_lf(path, render(f"# {name} technical debt", sections, order)) + + +def sections_of(text): + return [section for kind, section, _ in parse(text) if kind == "heading" and section] + + +def split(text): + order = sections_of(text) + index, files = [], {} + for kind, section, item in parse(text): + name = key(kind, item) + if name is None: + index.append((kind, item)) + else: + files.setdefault(name, {}).setdefault(section, []).append(relink(item, INDEX, debt_file(name))) + return index, files, order + + +def render_index(index): + out = [] + for kind, item in index: + if kind in ("heading", "text") and out and out[-1] != "": + out.append("") + out.append(item) + if kind == "heading" or kind == "text": + out.append("") + return "\n".join(out).rstrip("\n") + "\n" + + +def index_note(): + return ("text", f"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.") + + +def command_split(root): + 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") + for name, sections in files.items(): + existing = read_debt(root, name) + for section, entries in sections.items(): + existing.setdefault(section, []).extend(entries) + write_debt(root, name, existing, order) + if index_note() not in index: + index.insert(1, index_note()) + 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", errors="replace", check=check).stdout + + +def apply_change(root, base, new, index_text): + order = sections_of(index_text) or sections_of(new) + before = [(kind, section, item, key(kind, item)) for kind, section, item in parse(base) if kind == "entry" and key(kind, item)] + after = [(kind, section, item, key(kind, item)) for kind, section, item in parse(new) if kind == "entry" and key(kind, item)] + files = {} + + def load(name): + if name not in files: + files[name] = read_debt(root, name) + return files[name] + + def locate(entry): + _, section, item, name = entry + entries = load(name).get(section, []) + moved = relink(item, INDEX, debt_file(name)) + if moved not in entries: + raise LookupError(f"{debt_file(name)}: entry not found under '{section}': {item.splitlines()[0][:100]}") + return entries, entries.index(moved) + + def insert(entry, anchor): + _, section, item, name = entry + entries = load(name).setdefault(section, []) + moved = relink(item, INDEX, debt_file(name)) + position = 0 if anchor is None else len(entries) + if anchor is not None and anchor[1] == section and anchor[3] == name: + anchored = relink(anchor[2], INDEX, debt_file(name)) + if anchored in entries: + position = entries.index(anchored) + 1 + entries.insert(position, moved) + + matcher = difflib.SequenceMatcher(None, [entry[1:3] for entry in before], [entry[1:3] for entry in after], autojunk=False) + for tag, i1, i2, j1, j2 in matcher.get_opcodes(): + if tag == "equal": + continue + removed, added = before[i1:i2], after[j1:j2] + kept_old, kept_new = set(), set() + for offset, (old, fresh) in enumerate(zip(removed, added)): + if old[1] == fresh[1] and old[3] == fresh[3]: + entries, position = locate(old) + entries[position] = relink(fresh[2], INDEX, debt_file(fresh[3])) + kept_old.add(offset) + kept_new.add(offset) + for offset, old in enumerate(removed): + if offset not in kept_old: + entries, position = locate(old) + del entries[position] + for offset, fresh in enumerate(added): + if offset in kept_new: + continue + 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) + return sorted(debt_file(name) for name in files) + + +def unkeyed(text): + return "\n".join(item for kind, _, item in parse(text) if key(kind, item) is None) + + +def command_resolve(root): + stages = {} + for line in git(root, "ls-files", "-u", "--", INDEX).splitlines(): + meta, _ = line.split("\t", 1) + _, sha, stage = meta.split() + stages[stage] = sha + if not {"1", "2", "3"} <= set(stages): + sys.exit(f"{INDEX} is not in a three-way conflict") + base, ours, theirs = (git(root, "cat-file", "-p", stages[stage]) for stage in "123") + if unkeyed(base) != unkeyed(theirs): + sys.exit(f"{INDEX}: the change also edits lines that stay in {INDEX}; resolve those by hand") + try: + touched = apply_change(root, base, theirs, ours) + except LookupError as error: + sys.exit(str(error)) + write_lf(root / INDEX, ours) + git(root, "add", "--", INDEX, *touched) + print("\n".join(touched)) + + +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(argv) + root = Path(git(Path.cwd(), "rev-parse", "--show-toplevel").strip()) + command_split(root) if args.command == "split" else command_resolve(root) + + +if __name__ == "__main__": + main() 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 6cb1c47381..ffac30f5c7 100644 --- a/tools/hw-oracle/hw_oracle.py +++ b/tools/hw-oracle/hw_oracle.py @@ -1,5 +1,9 @@ import argparse +import hashlib +import json import os +import operator +import shlex import shutil import struct import subprocess @@ -33,26 +37,48 @@ def tool(name): def oracle(): - binary = CACHE / "oracle" source = HERE / "oracle.c" - if binary.exists() and binary.stat().st_mtime >= source.stat().st_mtime: + compiler = shlex.split(os.environ.get("CC", "cc")) + if not compiler: + raise ValueError("CC must name a compiler command") + root = rocm_root() + if root is not None: + root = root.resolve() + 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" + if binary.is_file(): return binary - CACHE.mkdir(parents=True, exist_ok=True) - with tempfile.TemporaryDirectory(dir=CACHE) as tmp: + directory.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(dir=directory) as tmp: output = Path(tmp) / "oracle" - command = [os.environ.get("CC", "cc"), "-O1", str(source), "-o", str(output)] - root = rocm_root() + command = [*compiler, "-O1", str(source), "-o", str(output)] 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 + + @cache +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 subprocess.run([oracle(), "--target"], capture_output=True, text=True, check=True).stdout.strip() + return os.environ.get("HW_ORACLE_TARGET") or detected_target(oracle(), os.environ.get("ROCR_VISIBLE_DEVICES")) def assemble(body, work, wave64, *, ieee, denorm32, denorm16, dx10_clamp, round32, round16, fp16_overflow, lds=DEFAULT_LDS): @@ -71,13 +97,26 @@ 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"), "-shared", str(work / "k.o"), "-o", str(work / "k.co")], check=True) + 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 validated_rows(rows): + validated = [] + for index, row in enumerate(rows, 1): + try: + values = tuple(operator.index(value) for value in row) + except TypeError as error: + raise ValueError(f"row {index} needs 4 unsigned 32-bit integers") from error + if len(values) != 4 or any(value < 0 or value > 0xffffffff for value in values): + raise ValueError(f"row {index} needs 4 unsigned 32-bit integers") + validated.append(values) + 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 = [tuple(r) for r in rows] + rows = validated_rows(rows) wave = 64 if wave64 else 32 padded = rows + [(0, 0, 0, 0)] * (-len(rows) % wave) env = dict(os.environ) @@ -89,15 +128,27 @@ def run(body, rows, extra=b"", wave64=False, coarse=False, *, ieee, denorm32, de work = Path(tmp) code = assemble(body, work, wave64, ieee=ieee, denorm32=denorm32, denorm16=denorm16, dx10_clamp=dx10_clamp, round32=round32, round16=round16, fp16_overflow=fp16_overflow, lds=lds) - for start in range(0, len(padded), ROWS_PER_DISPATCH): - chunk = padded[start:start + ROWS_PER_DISPATCH] + 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) + (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)] +def parse_word(value): + try: + return int(value, 0) + except ValueError: + return int(value, 10) + + def main(): parser = argparse.ArgumentParser(description="Run an RDNA kernel body on the local AMD GPU, one input row per lane.") parser.add_argument("body", type=Path, help="assembly inserted into template.s") @@ -114,16 +165,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(int(v, 0) 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/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 0b8ad0ea9f..58f66cf38c 100644 --- a/tools/hw-oracle/test_hw_oracle.py +++ b/tools/hw-oracle/test_hw_oracle.py @@ -1,7 +1,12 @@ import contextlib +import hashlib import io +import json import os +import shlex +import shutil import subprocess +import sys import tempfile import unittest from pathlib import Path @@ -24,12 +29,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") @@ -38,24 +43,26 @@ class OracleCacheTests(unittest.TestCase): with self.assertRaises(subprocess.CalledProcessError): hw_oracle.oracle() self.assertEqual(binary.read_bytes() if binary.exists() else None, previous) - self.assertEqual(list(cache.iterdir()), [binary] if previous is not None else []) + self.assertEqual([file for file in cache.rglob("*") if file.is_file()], [binary] if previous is not None else []) compiler.side_effect = succeed - self.assertEqual(hw_oracle.oracle(), binary) - self.assertEqual(binary.read_bytes(), b"complete executable") - self.assertEqual(hw_oracle.oracle(), binary) + built = hw_oracle.oracle() + self.assertEqual(built.read_bytes(), b"complete executable") + self.assertEqual(hw_oracle.oracle(), built) self.assertEqual(compiler.call_count, 2) - self.assertEqual(list(cache.iterdir()), [binary]) + self.assertEqual(set(file for file in cache.rglob("*") if file.is_file()), {built, binary} if previous is not None else {built}) + if previous is not None: + self.assertEqual(binary.read_bytes(), previous) def test_overlapping_builds_do_not_expose_partial_output(self): with tempfile.TemporaryDirectory() as tmp: cache = Path(tmp) / "cache" - binary = cache / "oracle" outputs = [] - def compile(command, *, check): + def compile(command, *, check, stdout): self.assertTrue(check) output = Path(command[command.index("-o") + 1]) outputs.append(output) + binary = output.parent.parent / "oracle" output.write_bytes(b"partial executable") if len(outputs) == 1: self.assertFalse(binary.exists()) @@ -64,11 +71,255 @@ class OracleCacheTests(unittest.TestCase): with patch.object(hw_oracle, "CACHE", cache), patch.object(hw_oracle, "rocm_root", return_value=None), \ patch.object(hw_oracle.subprocess, "run", side_effect=compile) as compiler: - self.assertEqual(hw_oracle.oracle(), binary) + binary = hw_oracle.oracle() self.assertEqual(binary.read_bytes(), b"complete executable") self.assertEqual(compiler.call_count, 2) self.assertNotEqual(outputs[0], outputs[1]) - self.assertEqual(list(cache.iterdir()), [binary]) + self.assertEqual([file for file in cache.rglob("*") if file.is_file()], [binary]) + + +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)): + with self.subTest(row=row), patch.object(hw_oracle, "assemble", side_effect=AssertionError("assembler called")) as assemble: + with self.assertRaisesRegex(ValueError, "row 1"): + hw_oracle.run("s_nop 0", [row], **MODES) + assemble.assert_not_called() + + def test_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): + return 0xffffffff + + def dispatch(command, **kwargs): + data = Path(command[3]).read_bytes() + self.assertEqual(data[:16], hw_oracle.struct.pack("<4I", 0, 0xffffffff, 0x80000000, 1)) + self.assertEqual(data[16:32 * 16], bytes(31 * 16)) + self.assertEqual(data[32 * 16:], b"extra") + Path(command[4]).write_bytes(bytes(32 * 64)) + + with patch.object(hw_oracle, "assemble", return_value="kernel.co"), \ + patch.object(hw_oracle, "oracle", return_value="oracle"), \ + patch.object(hw_oracle.subprocess, "run", side_effect=dispatch): + results = hw_oracle.run("s_nop 0", [(0, Indexable(), 0x80000000, True)], b"extra", **MODES) + self.assertEqual(results, [tuple([0] * 16)]) + + def test_cli_accepts_zero_padded_decimal_rows(self): + with tempfile.TemporaryDirectory() as tmp: + body, rows = Path(tmp) / "body.s", Path(tmp) / "rows.txt" + body.write_text("s_nop 0") + rows.write_text("0001 08 0xFFFFFFFF 0b10\n") + argv = ["hw_oracle.py", str(body), str(rows)] + argv += [item for name, value in MODES.items() for item in ["--" + name.replace("_", "-"), str(value)]] + with patch("sys.argv", argv), patch.object(hw_oracle, "run", return_value=[]) as run: + hw_oracle.main() + self.assertEqual(run.call_args.args[1], [(1, 8, 0xffffffff, 2)]) + + 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): @@ -122,7 +373,220 @@ class FloatModeTests(unittest.TestCase): self.assertEqual(assemble.call_args.kwargs, MODES | {"lds": 4096}) +class TargetOverrideTests(unittest.TestCase): + def setUp(self): + detector = getattr(hw_oracle, "detected_target", hw_oracle.target) + detector.cache_clear() + + def tearDown(self): + detector = getattr(hw_oracle, "detected_target", hw_oracle.target) + detector.cache_clear() + + def test_override_is_read_for_each_call_without_a_gpu_probe(self): + with patch.object(hw_oracle, "oracle", side_effect=AssertionError("GPU probe")): + with patch.dict(os.environ, {"HW_ORACLE_TARGET": "gfx1030"}): + self.assertEqual(hw_oracle.target(), "gfx1030") + with patch.dict(os.environ, {"HW_ORACLE_TARGET": "gfx1100"}): + self.assertEqual(hw_oracle.target(), "gfx1100") + + def test_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")), \ + patch.object(hw_oracle.subprocess, "run", return_value=reply) as probe: + with patch.dict(os.environ, {"HW_ORACLE_TARGET": ""}): + self.assertEqual(hw_oracle.target(), "gfx1030") + self.assertEqual(hw_oracle.target(), "gfx1030") + with patch.dict(os.environ, {"HW_ORACLE_TARGET": "gfx1100"}): + self.assertEqual(hw_oracle.target(), "gfx1100") + with patch.dict(os.environ, {"HW_ORACLE_TARGET": ""}): + self.assertEqual(hw_oracle.target(), "gfx1030") + self.assertEqual(probe.call_count, 1) + with patch.object(hw_oracle, "oracle", return_value=Path("oracle-b")), \ + patch.dict(os.environ, {"HW_ORACLE_TARGET": ""}): + probe.return_value = subprocess.CompletedProcess([], 0, "gfx1200\n", "") + self.assertEqual(hw_oracle.target(), "gfx1200") + self.assertEqual(probe.call_count, 2) + + +class OracleConfigurationTests(unittest.TestCase): + @unittest.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") + if compiler is None: + self.skipTest("C compiler not found") + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + runtime = root / "runtime" + (runtime / "lib").mkdir(parents=True) + stub = root / "runtime.c" + stub.write_text("void runtime_stub(void) {}\n", encoding="utf-8") + subprocess.run([compiler, "-shared", "-fPIC", str(stub), "-o", str(runtime / "lib/libhsa-runtime64.so")], check=True) + source = root / "source" + source.mkdir() + source_file = source / "oracle.c" + source_file.write_text('#include <stdio.h>\nint main(void) { printf("%d", VALUE); }\n', encoding="utf-8") + os.utime(source_file, (0, 0)) + wrapper = root / "compiler wrapper" + wrapper.write_text("#!/bin/sh\nexec " + shlex.quote(compiler) + ' "$@"\n', encoding="utf-8") + wrapper.chmod(0o755) + with patch.object(hw_oracle, "HERE", source), patch.object(hw_oracle, "CACHE", root / "cache"), \ + patch.object(hw_oracle, "rocm_root", return_value=runtime): + with patch.dict(os.environ, {"CC": shlex.quote(str(wrapper)) + " -DVALUE=7"}): + first = hw_oracle.oracle() + self.assertEqual(subprocess.check_output([first]), b"7") + self.assertEqual(hw_oracle.oracle(), first) + with patch.dict(os.environ, {"CC": shlex.quote(str(wrapper)) + " -DVALUE=8"}): + second = hw_oracle.oracle() + self.assertNotEqual(second, first) + self.assertEqual(subprocess.check_output([second]), b"8") + source_file.write_text('#include <stdio.h>\nint main(void) { printf("%d", VALUE + 1); }\n', encoding="utf-8") + os.utime(source_file, (0, 0)) + third = hw_oracle.oracle() + self.assertNotEqual(third, second) + self.assertEqual(subprocess.check_output([third]), b"9") + + def test_compiler_command_supports_wrapper_and_flags(self): + with tempfile.TemporaryDirectory() as tmp: + 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") + + with patch.object(hw_oracle, "CACHE", Path(tmp)), patch.object(hw_oracle, "rocm_root", return_value=None), \ + patch.dict(os.environ, {"CC": "ccache cc -fno-omit-frame-pointer"}), \ + patch.object(hw_oracle.subprocess, "run", side_effect=compile): + self.assertEqual(hw_oracle.oracle().read_bytes(), b"executable") + + def test_source_change_with_preserved_timestamp_invalidates_cache(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + source = root / "source" + source.mkdir() + source_file = source / "oracle.c" + source_file.write_text("first", encoding="utf-8") + os.utime(source_file, (0, 0)) + + 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"), \ + patch.object(hw_oracle, "rocm_root", return_value=None), \ + patch.object(hw_oracle.subprocess, "run", side_effect=compile) as compiler: + self.assertEqual(hw_oracle.oracle().read_bytes(), b"first") + source_file.write_text("second", encoding="utf-8") + os.utime(source_file, (0, 0)) + self.assertEqual(hw_oracle.oracle().read_bytes(), b"second") + self.assertEqual(compiler.call_count, 2) + + def test_compiler_change_invalidates_cache(self): + with tempfile.TemporaryDirectory() as tmp: + 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), \ + patch.object(hw_oracle.subprocess, "run", side_effect=compile) as compiler: + with patch.dict(os.environ, {"CC": "cc"}): + self.assertEqual(hw_oracle.oracle().read_bytes(), b"cc") + with patch.dict(os.environ, {"CC": "clang"}): + self.assertEqual(hw_oracle.oracle().read_bytes(), b"clang") + self.assertEqual(compiler.call_count, 2) + + def test_runtime_path_change_invalidates_cache(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + + 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()) + + with patch.object(hw_oracle, "CACHE", root / "cache"), \ + patch.object(hw_oracle.subprocess, "run", side_effect=compile) as compiler: + with patch.object(hw_oracle, "rocm_root", return_value=root / "one"): + first = hw_oracle.oracle() + self.assertEqual(first.read_bytes(), ("-L" + str((root / "one").resolve() / "lib")).encode()) + with patch.object(hw_oracle, "rocm_root", return_value=root / "two"): + second = hw_oracle.oracle() + self.assertNotEqual(second, first) + self.assertEqual(second.read_bytes(), ("-L" + str((root / "two").resolve() / "lib")).encode()) + self.assertEqual(compiler.call_count, 2) + + class GroupSegmentTests(unittest.TestCase): + def test_wave64_dispatches_at_most_512_rows(self): + rows = [(index, 0, 0, 0) for index in range(1025)] + for wave64, expected in ((False, [1024, 32]), (True, [512, 512, 64])): + with self.subTest(wave64=wave64), tempfile.TemporaryDirectory() as tmp: + dispatch_sizes = [] + + 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)) + + with patch.object(hw_oracle, "assemble", return_value=Path(tmp) / "kernel.co"), \ + patch.object(hw_oracle, "oracle", return_value="oracle"), \ + patch.object(hw_oracle.subprocess, "run", side_effect=dispatch): + result = hw_oracle.run("s_nop 0", rows, wave64=wave64, **MODES) + + self.assertEqual(dispatch_sizes, expected) + self.assertEqual(len(result), len(rows)) + def test_cli_forwards_lds(self): with tempfile.TemporaryDirectory() as tmp: body = Path(tmp) / "body.s" diff --git a/tools/import_audit.py b/tools/import_audit.py index 25d6f611ba..6ee58ad673 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -1,9 +1,11 @@ import argparse +import hashlib import json -import re +import os import struct import subprocess import sys +import tempfile from collections import defaultdict from pathlib import Path @@ -12,9 +14,7 @@ import progress ROOT = Path(__file__).resolve().parent.parent PRX = ROOT / "core" / "libs" / "prx" -NID = re.compile(r"[A-Za-z0-9+\-]{11}") -EXPORT_ALIAS = re.compile(r'APS5_EXPORT\("([A-Za-z0-9+\-]{11})",\s*(\w+)\)') -NID_POSTFIX = "_nid_postfix" +MODULE_SUFFIX = "#" CLASSES = ("implemented", "stub", "absent", "module") PE_DIRECTORY_OFFSET = {0x20b: 112, 0x10b: 96} @@ -33,41 +33,192 @@ def unpack(layout, data, offset, path, what): return struct.unpack(layout, take(data, offset, struct.calcsize(layout), path, what)) -def cstring(data, offset, limit, path, what): +def cstring(data, offset, limit, path, what, encoding="utf-8", errors="surrogateescape"): end = data.find(b"\0", offset, limit) if offset < 0 or offset >= limit or end < 0: raise AuditError(f"{path}: {what} at 0x{offset:x} is not a NUL-terminated string inside 0x{limit:x} bytes") try: - return data[offset:end].decode("utf-8") + return data[offset:end].decode(encoding, errors=errors) except UnicodeDecodeError as error: - raise AuditError(f"{path}: {what} at 0x{offset:x} is not UTF-8: {error}") from error + raise AuditError(f"{path}: {what} at 0x{offset:x} is not {encoding}: {error}") from error -def elf_exports(path, data): +def elf_sections(path, data): header = unpack("<16sHHIQQQIHHHHHH", data, 0, path, "ELF header") ident, section_offset, section_size, section_count = header[0], header[6], header[11], header[12] if ident[4] != 2 or ident[5] != 1 or section_size != 64: raise AuditError(f"{path}: only little-endian ELF64 with 64-byte section headers is supported (class {ident[4]}, data {ident[5]}, shentsize {section_size})") - sections = [unpack("<IIQQQQIIQQ", data, section_offset + index * 64, path, f"section header {index}") for index in range(section_count)] + 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: + section_count = unpack("<IIQQQQIIQQ", data, section_offset, path, "extended section count")[5] + take(data, section_offset, section_count * 64, path, "section header table") + return [unpack("<IIQQQQIIQQ", data, section_offset + index * 64, path, f"section header {index}") for index in range(section_count)] + + +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") + segments = [unpack("<IIQQQQQQ", data, program_offset + index * program_size, path, f"program header {index}") for index in range(program_count)] + loads = [segment for segment in segments if segment[0] == 1] + + def file_offset(address, size, what): + for segment in loads: + _, _, offset, virtual, _, file_size, _, _ = segment + if address >= virtual and address - virtual <= file_size and size <= file_size - (address - virtual): + result = offset + address - virtual + take(data, result, size, path, what) + return result + raise AuditError(f"{path}: {what} address 0x{address:x} is outside file-backed load segments") + + dynamic_segments = [segment for segment in segments if segment[0] == 2] + if len(dynamic_segments) != 1: + raise AuditError(f"{path}: expected one dynamic segment, found {len(dynamic_segments)}") + _, _, dynamic_offset, _, _, dynamic_size, _, _ = dynamic_segments[0] + if dynamic_size % 16: + raise AuditError(f"{path}: dynamic segment size {dynamic_size} is not a multiple of 16") + dynamic = {} + needed_offsets = [] + for position in range(0, dynamic_size, 16): + tag, value = unpack("<qQ", data, dynamic_offset + position, path, "dynamic entry") + if tag == 0: + break + if tag == 1: + needed_offsets.append(value) + else: + dynamic[tag] = value + string_address = dynamic.get(5) + string_size = dynamic.get(10) + symbol_address = dynamic.get(6) + symbol_size = dynamic.get(11) + if None in (string_address, string_size, symbol_address, symbol_size) or symbol_size != 24 or string_size == 0: + raise AuditError(f"{path}: incomplete dynamic string or symbol table") + string_offset = file_offset(string_address, string_size, "dynamic string table") + + symbol_count = None + 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") + 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 + maximum = first_symbol - 1 + for index in struct.unpack(f"<{buckets}I", bucket_data) if buckets else (): + if index == 0: + continue + if index < first_symbol: + raise AuditError(f"{path}: GNU hash bucket index {index} precedes symbol offset {first_symbol}") + symbol_index = index + while True: + chain_address = dynamic[0x6ffffef5] + (chain_offset - hash_offset) + (symbol_index - first_symbol) * 4 + chain_file_offset = file_offset(chain_address, 4, "GNU hash chain") + chain_value = unpack("<I", data, chain_file_offset, path, "GNU hash chain entry")[0] + maximum = max(maximum, symbol_index) + if chain_value & 1: + break + symbol_index += 1 + symbol_count = maximum + 1 + if symbol_count is None: + raise AuditError(f"{path}: dynamic symbol table has no supported hash table") + symbol_offset = file_offset(symbol_address, symbol_count * symbol_size, "dynamic symbol table") + imports = set() + exports = set() + for index in range(symbol_count): + name, info, other, section_index, _, _ = unpack("<IBBHQQ", data, symbol_offset + index * symbol_size, path, "dynamic symbol") + if name == 0: + continue + symbol = cstring(data, string_offset + name, string_offset + string_size, path, "dynamic symbol name").split(MODULE_SUFFIX, 1)[0] + if section_index == 0: + imports.add(symbol) + elif info >> 4 in (1, 2, 10) and other & 3 in (0, 3): + exports.add(symbol) + needed = {cstring(data, string_offset + value, string_offset + string_size, path, "needed library name") for value in needed_offsets} + return imports, exports, needed + + +def elf_exports(path, data): + header = unpack("<16sHHIQQQIHHHHHH", data, 0, path, "ELF header") + if not header[6] or header[6] >= len(data): + return elf_program_dynamic_symbols(path, data)[1] + sections = elf_sections(path, data) names = set() - for section in sections: + for section_index, section in enumerate(sections): kind, offset, size, link, entry = section[1], section[4], section[5], section[6], section[9] if kind != 11: continue if entry != 24 or size % 24 or link >= len(sections) or sections[link][1] != 3: raise AuditError(f"{path}: invalid dynamic symbol table (entry size {entry}, size {size}, string table section {link})") + take(data, offset, size, path, "dynamic symbol table") strings_offset, strings_size = sections[link][4], sections[link][5] take(data, strings_offset, strings_size, path, "dynamic string table") + indexes = [candidate for candidate in sections if candidate[1] == 18 and candidate[6] == section_index] + if len(indexes) > 1: + raise AuditError(f"{path}: multiple extended section index tables for section {section_index}") for position in range(0, size, 24): name, info, other, index, _, _ = unpack("<IBBHQQ", data, offset + position, path, "dynamic symbol") + if index == 0xffff: + if not indexes or indexes[0][9] != 4 or indexes[0][5] != size // 24 * 4: + raise AuditError(f"{path}: invalid or missing extended section index table for section {section_index}") + index = unpack("<I", data, indexes[0][4] + position // 24 * 4, path, "extended symbol section index")[0] + if index >= len(sections): + raise AuditError(f"{path}: extended symbol section index {index} lies outside the section table") + elif 0 < index < 0xff00 and index >= len(sections): + raise AuditError(f"{path}: symbol section index {index} lies outside the section table") binding, visibility = info >> 4, other & 3 - if index == 0 or binding not in (1, 2, 10) or visibility not in (0, 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 pe_exports(path, data): +def elf_imports(path, data): + header = unpack("<16sHHIQQQIHHHHHH", data, 0, path, "ELF header") + 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) + symbols = set() + needed = set() + for section in sections: + kind, offset, size, link, entry = section[1], section[4], section[5], section[6], section[9] + if kind == 11: + if entry != 24 or size % 24 or link >= len(sections) or sections[link][1] != 3: + raise AuditError(f"{path}: invalid dynamic symbol table (entry size {entry}, size {size}, string table section {link})") + strings_offset, strings_size = sections[link][4], sections[link][5] + take(data, strings_offset, strings_size, path, "dynamic string table") + for position in range(0, size, 24): + name, _, _, index, _, _ = unpack("<IBBHQQ", data, offset + position, path, "dynamic symbol") + if name and index == 0: + symbol = cstring(data, strings_offset + name, strings_offset + strings_size, path, "import name") + symbols.add(symbol.split(MODULE_SUFFIX, 1)[0]) + elif kind == 6: + if entry != 16 or size % 16 or link >= len(sections) or sections[link][1] != 3: + raise AuditError(f"{path}: invalid dynamic section (entry size {entry}, size {size}, string table section {link})") + strings_offset, strings_size = sections[link][4], sections[link][5] + take(data, strings_offset, strings_size, path, "dynamic string table") + for position in range(0, size, 16): + tag, value = unpack("<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_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}") @@ -79,12 +230,17 @@ def pe_exports(path, data): directory = PE_DIRECTORY_OFFSET[magic] if optional_size < directory + 8: raise AuditError(f"{path}: PE optional header of {optional_size} bytes has no export directory") - 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") + take(data, optional, optional_size, path, "PE optional header") + 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] 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): + if address < headers_size and length <= headers_size - address: + take(data, address, length, path, f"header data at RVA 0x{address:x}") + return address, min(headers_size, len(data)) for section in sections: virtual, raw_size, raw_offset = section[2], section[3], section[4] if address < virtual: @@ -93,24 +249,83 @@ def pe_exports(path, data): if relative >= raw_size or length > raw_size - relative: continue take(data, raw_offset + relative, length, path, f"data at RVA 0x{address:x}") - return raw_offset + relative + return raw_offset + relative, min(raw_offset + raw_size, len(data)) raise AuditError(f"{path}: RVA 0x{address:x} is not inside any section") - table = unpack("<IIHHIIIIIII", data, to_offset(rva, 40), path, "export directory") - name_count, names_rva = table[7], table[9] + 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() if not name_count: return names - names_offset = to_offset(names_rva, name_count * 4) + if not functions_rva or not names_rva or not ordinals_rva: + raise AuditError(f"{path}: named PE exports require address, name and ordinal tables") + functions_offset = to_offset(functions_rva, function_count * 4)[0] + names_offset = to_offset(names_rva, name_count * 4)[0] + ordinals_offset = to_offset(ordinals_rva, name_count * 2)[0] + previous_name = None for index in range(name_count): name_rva = unpack("<I", data, names_offset + index * 4, path, "export name RVA")[0] - offset = to_offset(name_rva, 1) - names.add(cstring(data, offset, len(data), path, "export name")) + offset, limit = to_offset(name_rva, 1) + name = cstring(data, offset, limit, path, "export name", "ASCII", "strict") + if previous_name is not None and name < previous_name: + raise AuditError(f"{path}: PE export name pointer table is not sorted") + previous_name = name + ordinal = unpack("<H", data, ordinals_offset + index * 2, path, "export ordinal")[0] + if ordinal >= function_count: + raise AuditError(f"{path}: export ordinal {ordinal} lies outside the address table ({function_count} entries)") + function_rva = unpack("<I", data, functions_offset + ordinal * 4, path, "export address")[0] + if not function_rva: + continue + if rva <= function_rva < rva + size: + offset, limit = to_offset(function_rva, 1) + cstring(data, offset, min(limit, offset + rva + size - function_rva), path, "export forwarder", "ASCII", "strict") + names.add(name) return names -def read_exports(path): - data = path.read_bytes() +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() if data[:4] == b"\x7fELF": return elf_exports(path, data) if data[:2] == b"MZ": @@ -128,41 +343,153 @@ def directory_files(directory, pattern): def built_libraries(directories): exports = defaultdict(set) libraries = set() + images = {} for directory in directories: files = directory_files(directory, "*.prx") if not files: raise AuditError(f"{directory}: no .prx files; pass the directory the libs target builds (build/core/libs/libs)") for file in files: + 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) libraries.add(file.name) - for name in read_exports(file): + for name in read_exports(file, data): exports[name].add(file.name) return {name: sorted(files) for name, files in exports.items()}, libraries -def module_files(directories): - return {file.name for directory in directories for file in directory_files(directory, "*")} +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 nid_of(name, aliases): +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 module_files(directory): + 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 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 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): if not Path(source).is_dir(): raise AuditError(f"{source}: not a directory") - done, pending = set(), {} + stubs = {} + shared = {} for library in sorted(item for item in Path(source).iterdir() if item.is_dir()): aliases = {} - for file in library.rglob("*.cpp"): - for nid, name in EXPORT_ALIAS.findall(file.read_text(errors="ignore")): - aliases[name] = nid + for file in nid_names.source_files(library): + for nid, name in nid_names.source_aliases(file.read_text(errors="ignore")): + aliases.setdefault(name, set()).add(nid) group = progress.scan_library(library) - done.update(nid_of(name, aliases) for name in group["done_names"]) + if "shared_sources" in group: + shared[library.name] = group["shared_sources"] + done = {nid for name in group["done_names"] for nid in nids_of(name, aliases)} for name in group["todo_names"]: - pending.setdefault(nid_of(name, aliases), name) - return {nid: name for nid, name in pending.items() 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] + while owner in shared: + if owner in chain: + raise AuditError(f"{source}: shared source cycle: {' -> '.join(chain + [owner])}") + chain.append(owner) + owner = shared[owner] + for (provider, nid), name in list(stubs.items()): + if provider == owner + ".prx": + stubs[(library + ".prx", nid)] = name + return stubs def read_registry(path): @@ -177,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)) @@ -190,36 +534,95 @@ def check_conservation(references, records): raise AuditError(f"conservation failed: {references} references in, {classified} classified, unknown classes {unknown}") -def audit(imports, exports, stubs, libraries, modules): +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" + if not providers: + return "absent" + if all((provider, nid) in stubs for provider in providers): + return "stub" + return "implemented" + + +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, "weak": False}) + record["references"] += 1 for record in records.values(): nid, library = record["nid"], record["library"] providers = exports.get(nid, []) - if library and library in modules: - kind = "module" - elif not providers: - kind = "absent" - elif nid in stubs: - kind = "stub" - else: - kind = "implemented" + if not library: + providers = sorted(set(providers) | set((module_exports or {}).get(nid, []))) + 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"] = stubs.get(nid) - ordered = sorted(records.values(), key=lambda record: (record["class"], record["library"], record["nid"])) - check_conservation(len(imports), ordered) + 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: missing[library] += 1 + for _, needed in (module_imports or {}).values(): + for library in needed: + if library not in libraries and library not in modules: + missing[library] += 1 return ordered, dict(sorted(missing.items())) +def current_stop(imports, records): + """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, weak in imports: + if weak: + continue + record = classes.get((nid, library)) + if record is not None and record["class"] == "absent": + return record + return None + + def load_names(path): if not Path(path).is_file(): raise AuditError(f"{path}: names file not found; the audit never downloads one (see tools/nid_names.py for the source)") @@ -234,16 +637,36 @@ def attach_names(records, names): record["name_verified"] = nid_names.compute_nid(name) == record["nid"] -def git_stamp(): +def write_json(path, result): + if path.is_dir(): + raise AuditError(f"{path}: JSON output is a directory") + destination = path.resolve() + with tempfile.TemporaryDirectory(dir=destination.parent, prefix=destination.name + ".", suffix=".tmp") as directory: + temporary = Path(directory) / destination.name + try: + temporary.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8") + if destination.exists(): + temporary.chmod(destination.stat().st_mode & 0o7777) + temporary.replace(destination) + finally: + if temporary.exists(): + temporary.chmod(0o600) + temporary.unlink(missing_ok=True) + + +def git_stamp(source=ROOT): try: - commit = subprocess.run(["git", "-C", str(ROOT), "rev-parse", "HEAD"], capture_output=True, text=True, check=True).stdout.strip() - dirty = bool(subprocess.run(["git", "-C", str(ROOT), "status", "--porcelain"], capture_output=True, text=True, check=True).stdout.strip()) + commit = subprocess.run(["git", "-C", str(source), "rev-parse", "HEAD"], 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 { @@ -252,18 +675,39 @@ 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"]), } def label(record): if record["name"] is None: - return record["nid"] - mark = "" if record.get("name_verified", True) else " (name does not hash to this NID)" - return f"{record['nid']} {record['name']}{mark}" + text = record["nid"] + else: + mark = "" if record.get("name_verified", True) else " (name does not hash to this NID)" + text = f"{record['nid']} {record['name']}{mark}" + return f"{text} (imported by {record['module']})" if record.get("module") else text -def render(summary, records, missing): +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") @@ -272,16 +716,30 @@ def render(summary, records, missing): if selected: lines.append("") lines.append(f"{title} ({len(selected)})") - current = None + current = False for record in selected: if record["library"] != current: current = record["library"] lines.append(f" [{current or 'library not named'}]") lines.append(f" {label(record)}") + if stops: + lines.append("") + lines.append("Current stop (first unresolved import, in registry order)") + for entry in stops: + stop = entry["stop"] + lines.append(f" {entry['registry']}: none" if stop is None else f" {entry['registry']}: {label(stop)} [{stop['library']}]") if missing: lines.append("") lines.append(f"Needed libraries with no file in --libs or --modules ({len(missing)})") 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") @@ -292,34 +750,61 @@ def main(argv=None): parser = argparse.ArgumentParser(description="classify the imports of a converted game against the built system libraries") parser.add_argument("registries", nargs="+", type=Path, help="JSON written by 'relinker --registry'") parser.add_argument("--libs", action="append", required=True, help="directory of built .prx files; repeat for several") - parser.add_argument("--modules", action="append", default=[], help="directory of the title's own modules; libraries named there are not checked") + parser.add_argument("--modules", action="append", default=[], help="directory of the title's own modules; each module's own imports are audited") parser.add_argument("--source", default=str(PRX), help="core/libs/prx tree used to find throwing stubs (default: this repository's)") parser.add_argument("--names", help="aerolib.csv style file (NID, name per line) used to name absent imports") parser.add_argument("--json", type=Path, help="write the full result to this file") + 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: - imports = [entry for registry in args.registries for entry in read_registry(registry)] + 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 + 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")) + output = args.json.resolve() + if any(output == path.resolve() or (args.json.exists() and args.json.samefile(path)) for path in inputs): + raise AuditError(f"{args.json}: JSON output would overwrite an input file") + 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) - modules = module_files(args.modules) - records, missing = audit(imports, exports, source_stubs(args.source), libraries, modules) + host_dependencies, windows = built_host_dependencies(args.libs) + module_imports, module_exports = read_modules(args.modules) + 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, len(imports), 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() - result = {"source_commit": commit, "source_dirty": dirty, "registries": [str(path) for path in args.registries], **summary, "imports": records} - args.json.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8") + commit, dirty = git_stamp(Path(args.source)) + json_stops = [{"registry": entry["registry"], + "nid": entry["stop"]["nid"] if entry["stop"] else None, + "library": entry["stop"]["library"] if entry["stop"] else None, + "name": entry["stop"]["name"] if entry["stop"] else None} for entry in stops] + result = {"source_commit": commit, "source_dirty": dirty, "registries": [str(path) for path in args.registries], "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) return 2 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)) - 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 e4c0c98f04..1818f593c9 100644 --- a/tools/nid_names.py +++ b/tools/nid_names.py @@ -18,9 +18,15 @@ DEFAULT_CACHE = Path(tempfile.gettempdir()) / "anyps5-nid-db" / "aerolib.csv" CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-" SUFFIX = bytes([0x51, 0x8D, 0x64, 0xA6, 0x35, 0xDE, 0xD8, 0xC1, 0xE6, 0xB0, 0x39, 0xB1, 0xC3, 0xE5, 0x52, 0x30]) -ALIAS = re.compile(r'APS5_EXPORT\("([A-Za-z0-9+\-]{11})",\s*(\w*[Uu]nknown\w*)\)') +CPP_TOKEN = re.compile( + r'(?P<comment>//[^\n]*|/\*[\s\S]*?\*/)' + r'|(?P<raw>(?:u8|u|U|L)?R"(?P<delimiter>[^\s()\\]{0,16})\([\s\S]*?\)(?P=delimiter)")' + r'|(?P<string>(?:u8|u|U|L)?"(?:\\[\s\S]|[^"\\])*")' + r"|(?P<char>(?:u8|u|U|L)?'(?:\\[\s\S]|[^'\\])*')" + 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): @@ -37,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) @@ -49,61 +59,177 @@ def load_db(path): finally: temporary.unlink(missing_ok=True) db = {} - with open(path, errors="replace") as handle: + with open(path, encoding="utf-8-sig", errors="replace") as handle: for line in handle: parts = line.strip().split() - if len(parts) >= 2: - db.setdefault(parts[0], parts[1]) + if len(parts) >= 2 and not parts[0].startswith("#"): + nid, name = (part.split("#", 1)[0] for part in parts[:2]) + if nid and name: + db.setdefault(nid, name) return db +def source_files(root): + return (path for path in sorted(root.rglob("*.cpp")) + if not any(part.lower() in {"test", "tests"} + for part in path.relative_to(root).parts[:-1])) + + +def source_aliases(text): + tokens = [match.group() 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 != "APS5_EXPORT" or index + 5 >= len(tokens): + continue + opening, literal, comma, name, closing = tokens[index + 1:index + 6] + if opening != "(" or comma != "," or closing != ")": + continue + if re.fullmatch(r'"[A-Za-z0-9+\-]{11}"', literal) is None: + continue + if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", name): + yield literal[1:-1], name + + +def source_definitions(text): + def mask(match): + return match.group() if match.group("code") is not None else re.sub(r"[^\n]", " ", match.group()) + + 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 sorted(PRX.rglob("*.cpp")): - try: - text = path.read_text(errors="replace") - except OSError: - continue - for match in ALIAS.finditer(text): - found.setdefault(match.group(1), "%s:%s" % ( - path.relative_to(ROOT), match.group(2))) + for path in source_files(PRX): + text = path.read_text(encoding="utf-8") + for nid, name in source_aliases(text): + if "unknown" in name.lower(): + found.setdefault(nid, "%s:%s" % (path.relative_to(ROOT).as_posix(), name)) return found def collect_real_names(): names = set() - for path in sorted((ROOT / "core" / "libs").rglob("*.cpp")): - try: - text = path.read_text(errors="replace") - except OSError: - continue - for match in DEFINITION.finditer(text): + for path in source_files(ROOT / "core" / "libs"): + text = path.read_text(encoding="utf-8") + 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 -def main(): +def option_name(argument, options): + if not argument.startswith("-") or argument == "-": + return None + name = argument.split("=", 1)[0] + if name in options: + return name + if not name.startswith("--"): + return None + matches = [option for option in options if option.startswith(name)] + if len(matches) == 1: + return matches[0] + return name if matches else None + + +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 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(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)") - parser.add_argument("--nid", nargs="*", default=[], + 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") - args = parser.parse_args() + 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_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 -") db_path = Path(args.db) if args.db else DEFAULT_CACHE - db = load_db(db_path) - - if args.nid: - unknowns = {nid: "<cli>" for nid in args.nid} - else: - unknowns = collect_unknowns() - real_names = collect_real_names() + try: + db = load_db(db_path) + if requested_nids: + unknowns = {nid: "<cli>" for nid in (args.nid or [])} + else: + unknowns = collect_unknowns() + real_names = collect_real_names() if unknowns else set() + except (OSError, UnicodeError) as error: + parser.error(str(error)) rows = [] failures = 0 @@ -135,4 +261,7 @@ def main(): 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 53add73659..88558908a6 100644 --- a/tools/package_release.py +++ b/tools/package_release.py @@ -1,69 +1,158 @@ import argparse +import json +import os import re import shutil +import stat 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-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'}") + + +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): 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}") libraries = sorted((build / "core/libs/libs").glob("*.prx")) - expected = {f"{directory.name}.prx" for directory in Path("core/libs/prx").iterdir() if directory.is_dir()} + expected = {f"{directory.name}.prx" for directory in (ROOT / "core/libs/prx").iterdir() if directory.is_dir() and not directory.name.startswith(".")} missing = expected - {library.name for library in libraries} if missing: raise RuntimeError(f"Missing patched libraries: {', '.join(sorted(missing))}") executable = "relinker.exe" if platform == "windows" else "relinker" - files = list(libraries) + 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 platform == "windows": - runtime = Path("C:/winlibs/mingw64/bin") + runtime = windows_runtime(build) files.extend(runtime / name for name in ("libgcc_s_seh-1.dll", "libstdc++-6.dll", "libwinpthread-1.dll")) + 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}") - output.mkdir(parents=True, exist_ok=True) - with zipfile.ZipFile(output / 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}") - with tarfile.open(output / f"prx-{platform}-{version}.tar.gz", "w:gz", compresslevel=9) as archive: - for file in files: - archive.add(file, arcname=f"libs/{file.name}") asset = f"relinker-{version}.exe" if platform == "windows" else f"relinker-{version}" - shutil.copy2(binary, output / asset) + names = (f"prx-{platform}-{version}.zip", f"prx-{platform}-{version}.tar.gz", asset) + for name in names: + destination = output / name + if destination.exists() and not destination.is_file(): + raise RuntimeError(f"Release destination is not a file: {destination}") + 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): - documents = sorted(source.rglob("*.md")) + 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}") - names = set() + names = {file.name.casefold() for file in output.iterdir()} if output.exists() else set() for document in documents: - if document.name in names or (output / document.name).exists(): + if document.name.casefold() in names: raise RuntimeError(f"Duplicate release asset name: {document.name}") - names.add(document.name) + names.add(document.name.casefold()) output.mkdir(parents=True, exist_ok=True) for document in documents: shutil.copy2(document, output / document.name) +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(): 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") + 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") - package(args.platform, args.build, args.output, args.version) + 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) if __name__ == "__main__": diff --git a/tools/pad_input.py b/tools/pad_input.py new file mode 100644 index 0000000000..ab93227072 --- /dev/null +++ b/tools/pad_input.py @@ -0,0 +1,98 @@ +import argparse +import os +from pathlib import Path +import re +import stat +import tempfile +import time + + +BUTTONS = { + "l3": 0x2, "r3": 0x4, "options": 0x8, + "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, + "touch-left": 0x200000, "touch-right": 0x400000, "release": 0, +} + + +def previous_sequence(path): + try: + with path.open("rb") as command: + line = command.readline(129) + except FileNotFoundError: + return 0 + if os.name == "nt" and line.endswith(b"\r\n"): + line = line[:-2] + b"\n" + tokens = line.split() + 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 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 + return values[0] + + +def next_sequence(path): + sequence = max(time.monotonic_ns(), previous_sequence(path) + 1) + if sequence > 2**64 - 1: + raise ValueError("controller command sequence is exhausted") + return sequence + + +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("--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() + if not 1 <= args.hold_ms <= 5000: + parser.error("--hold-ms must be between 1 and 5000") + sticks = "" + if args.left_stick is not None or args.right_stick is not None: + axes = (args.left_stick or [128, 128]) + (args.right_stick or [128, 128]) + if not all(0 <= axis <= 255 for axis in axes): + parser.error("stick axes must be between 0 and 255") + sticks = "".join(f" {axis}" for axis in axes) + try: + 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) + 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} {BUTTONS[args.button]} {args.hold_ms}{sticks}\n") + os.replace(temporary, args.path) + finally: + if temporary is not None: + temporary.unlink(missing_ok=True) + print(f"Queued {args.button}{' with sticks' + sticks if sticks else ''} for {args.hold_ms} ms in {args.path}") + + +if __name__ == "__main__": + main() 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..4642a983ff --- /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") + 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..4ca5c70a9b --- /dev/null +++ b/tools/pr_labels.py @@ -0,0 +1,154 @@ +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(): + 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() + 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"))] + 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)}") + + +if __name__ == "__main__": + main() diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index 9b64188042..ca1bc134e5 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -3,19 +3,32 @@ import json import os import re import subprocess -from itertools import chain, combinations +from concurrent.futures import ThreadPoolExecutor +from itertools import combinations from pathlib import Path -from urllib.parse import urlencode +from urllib.parse import quote, urlencode PARENT = 'if .parent then "\\(.parent.owner.login)/\\(.parent.name)" else .nameWithOwner end' EXPORT = re.compile(r"^\+.*\bAPS5_VABI\s+(\w+)\s*\(") -DEPENDS = re.compile(r'Depends on:([^\r\n]*(?:\r?\n[ \t]*[-*][ \t]*#\d+[^\r\n]*)*)') +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) MARKER = "<!-- pr-overlap -->" +FETCH_REFS = 100 def git(*args): - return subprocess.run(["git", *args], capture_output=True, text=True, encoding="utf-8", 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): + 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): @@ -26,32 +39,56 @@ def upstream(): return os.environ.get("GITHUB_REPOSITORY") or gh("repo", "view", "--json", "nameWithOwner,parent", "--jq", PARENT).strip() +def depends_on(body): + body = re.sub(r"<!--.*?-->", "", body or "", flags=re.S) + 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", "--slurp", f"repos/{REPO}/pulls?{query}") + out = gh("api", "--paginate", f"repos/{REPO}/pulls?{query}") prs = {} - for pr in chain.from_iterable(json.loads(out)): - depends = DEPENDS.search(pr["body"] or "") + decoder = json.JSONDecoder() + offset = 0 + page_count = 0 + pull_requests = [] + while offset < len(out): + while offset < len(out) and out[offset].isspace(): + offset += 1 + if offset == len(out): + break + page, offset = decoder.raw_decode(out, offset) + if not isinstance(page, list): + raise ValueError("gh api --paginate returned a non-array page") + page_count += 1 + pull_requests.extend(page) + if page_count == 0: + raise ValueError("gh api --paginate returned no JSON pages") + + for pr in pull_requests: prs[pr["number"]] = { "ref": f"refs/pr/{pr['number']}", "sha": pr["head"]["sha"], "author": pr["user"]["login"], - "depends": {int(n) for n in re.findall(r"#(\d+)", depends.group(1))} if depends else set(), + "depends": depends_on(pr["body"]), } return prs def fetch(base, prs): - refspecs = [f"+refs/heads/{base}:refs/pr/base"] + [f"+refs/pull/{n}/head:{pr['ref']}" for n, pr in prs.items()] - git("fetch", "--no-tags", "-q", f"https://github.com/{REPO}.git", *refspecs) + refspecs = [f"+refs/heads/{base}:refs/pr/base"] + [f"+{pr['sha']}:{pr['ref']}" for pr in prs.values()] + for start in range(0, len(refspecs), FETCH_REFS): + git("fetch", "--no-tags", "-q", f"https://github.com/{REPO}.git", *refspecs[start:start + FETCH_REFS]) def merge(*args): - result = subprocess.run(["git", "merge-tree", "--write-tree", "--name-only", "--no-messages", *args], - capture_output=True, text=True, encoding="utf-8") - 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() @@ -62,10 +99,20 @@ def commit(tree): def exports(diff): found = set() - for line in diff.splitlines(): + lines = diff.splitlines() + for i, line in enumerate(lines): m = EXPORT.match(line) - if m and not m.group(1).endswith("_nid_no_patch") and not line.rstrip().endswith(";"): - found.add(m.group(1)) + if not m or m.group(1).endswith(("_nid_no_patch", "_nid_no_patch_cut")): + continue + text = line[1:] + for nxt in lines[i + 1:]: + if ";" in text or "{" in text or not nxt.startswith("+") or nxt.startswith("+++"): + break + text += nxt[1:] + semi, brace = text.find(";"), text.find("{") + if semi < 0 or 0 <= brace < semi: + name = m.group(1).removesuffix("_nid_postfix") + found.add(NID_DISAMBIG.sub("", name)) return found @@ -74,8 +121,8 @@ def scan(pr): pr["tree"] = None if conflicted else commit(tree) old, new = (git("merge-base", "refs/pr/base", pr["ref"]).strip(), pr["ref"]) if conflicted else ("refs/pr/base", pr["tree"]) pr["files"], pr["added"] = set(), set() - for line in git("diff", "--name-status", "--no-renames", old, new).splitlines(): - status, path = line.split("\t", 1) + changes = git("diff", "--name-status", "--no-renames", "-z", old, new).split("\0") + for status, path in zip(changes[::2], changes[1::2]): pr["files"].add(path) if status == "A": pr["added"].add(path) @@ -83,9 +130,18 @@ def scan(pr): pr["exports"] = exports(diff) +def scan_all(prs, jobs): + with ThreadPoolExecutor(max_workers=jobs) as executor: + list(executor.map(scan, prs.values())) + + def hunks(pr, path): diff = git("diff", "-U0", "--no-color", "refs/pr/base", pr["tree"], "--", path) - 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): @@ -103,18 +159,34 @@ def clashes(a, b, path): def conflicts(a, b): if not (a["tree"] and b["tree"] and a["files"] & b["files"]): return {} - files = merge("--merge-base=refs/pr/base", a["tree"], b["tree"])[1] + files = merge(a["tree"], b["tree"])[1] return {path: [] if path in a["added"] else clashes(a, b, path) for path in files} +def dependencies(prs, number): + found, pending = set(), [number] + while pending: + for n in prs[pending.pop()]["depends"]: + if n in prs and n not in found: + found.add(n) + pending.append(n) + return found + + def overlaps(prs, a, b): pa, pb = prs[a], prs[b] - if b in pa["depends"] or a in pb["depends"]: + da, db = dependencies(prs, a), dependencies(prs, b) + if b in da or a in db: return None - found = {"added": pa["added"] & pb["added"]} + shared = [prs[n] for n in da & db] + 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"] + found["exports"] = (pa["exports"] & pb["exports"]).difference(*(pr["exports"] for pr in 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 @@ -123,51 +195,88 @@ 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" +def write_reports(out, rows, blob): + out.mkdir(parents=True, exist_ok=True) + for number, found in rows.items(): + (out / f"{number}.md").write_text(report(found, blob), encoding="utf-8") + + +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}" + + if __name__ == "__main__": 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") + parser.add_argument("--jobs", type=int, default=4, help="number of pull request comparisons to run at once") args = parser.parse_args() if bool(args.out) == bool(args.branch): parser.error("pass exactly one of --out or --branch") - 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 = {n: pr for n, pr in prs.items() if pr["sha"] != 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") + try: + 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 - args.out.mkdir(parents=True, exist_ok=True) - for number, found in rows.items(): - (args.out / f"{number}.md").write_text(report(found, blob)) + write_reports(args.out, rows, blob) diff --git a/tools/prepare_release_notes.py b/tools/prepare_release_notes.py new file mode 100644 index 0000000000..0163999059 --- /dev/null +++ b/tools/prepare_release_notes.py @@ -0,0 +1,53 @@ +import argparse +import json +import os +import re +import stat +from pathlib import Path + +BODY_LIMIT = 125000 + + +def release_body(notes): + if len(notes) <= BODY_LIMIT: + return notes + return re.sub( + r"^## What's Changed[^\S\r\n]*\r?\n.*?(?=^#{1,2} |^\*\*Full Changelog\*\*:|\Z)", + "", + notes, + count=1, + flags=re.MULTILINE | re.DOTALL, + ) + + +def prepare_notes(source, body, full): + 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_bytes(notes.encode("utf-8")) + body.write_bytes(release_body(notes).encode("utf-8")) + + +def main(): + parser = argparse.ArgumentParser(description="Prepare size-safe GitHub release notes") + parser.add_argument("--input", type=Path, required=True) + parser.add_argument("--body", type=Path, required=True) + parser.add_argument("--full", type=Path, required=True) + args = parser.parse_args() + prepare_notes(args.input, args.body, args.full) + + +if __name__ == "__main__": + main() diff --git a/tools/progress.py b/tools/progress.py index 808f35a391..944ba4170b 100644 --- a/tools/progress.py +++ b/tools/progress.py @@ -1,4 +1,5 @@ import argparse +import importlib.util import json import os import re @@ -6,6 +7,11 @@ import sys from html import escape from pathlib import Path +tokenizer_spec = importlib.util.spec_from_file_location("anyps5_cpp_tokenizer", Path(__file__).with_name("nid_names.py")) +tokenizer = importlib.util.module_from_spec(tokenizer_spec) +tokenizer_spec.loader.exec_module(tokenizer) +CPP_TOKEN = tokenizer.CPP_TOKEN + ROOT = Path(__file__).resolve().parent.parent PRX = ROOT / "core" / "libs" / "prx" OPCODES = ROOT / "core" / "shader" / "recompiler" / "RdnaDecoder" / "include" / "RdnaDecoder" / "RdnaOpcode.hpp" @@ -13,7 +19,20 @@ ISA = Path(__file__).resolve().parent / "rdna_isa.txt" SOURCE = f'https://github.com/{os.environ.get("GITHUB_REPOSITORY", "boykopovar/AnyPS5")}/blob/main' DEFINITION = re.compile(r"\bAPS5_VABI\s+(\w+)\s*\([^;{]*\)\s*(?:noexcept\s*)?(?:try\s*)?\{") STUB = "NotImplemented_nid_no_patch" -STUB_WRAPPER = re.compile(r"\bstatic\s+(?:\[\[noreturn\]\]\s+)?void\s+(\w+)\s*\([^;{]*\)\s*\{") +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( + r"^[ \t]*(?:static\s+)?(?:\[\[noreturn\]\]\s+)?(?:(?!APS5_VABI)[\w:<>,\s*&])+?[\s*&]" + r"(?!(?:if|while|for|switch|catch|return|else)\b)(\w+)\s*\([^;{]*\)\s*" + r"(?:const\s*)?(?:noexcept\s*)?(?:->\s*[\w:<>*&\s]+?)?\{", re.M) +DISCARD = re.compile(r"\(void\)\s*\w+\s*;") +EMPTY_RETURN = re.compile(r"return\s*;") +CONST_RETURN = re.compile(r"return\s+(?:[+-]?\s*(?:0[xX][0-9a-fA-F]+|\d+)[uUlL]*|\w+|\"(?:\\.|[^\"\\])*\")\s*;") +WRAPPER_RETURN = re.compile(r"return\s+(\w+)\s*\(([^()]*)\)\s*;") +BARE_THROW = re.compile(r"throw\b[^;]*;") FLAT_SEGMENTS = ("GLOBAL_", "SCRATCH_") OPCODE_SENTINELS = {"Invalid", "Count", "Unknown", "Unsupported"} OPCODE_ALIASES = { @@ -60,6 +79,17 @@ PANEL_WIDTH, GAP, MAP_HEIGHT, HEADER = 495, 10, 280, 30 DONE_COLOR, TODO_COLOR, BORDER, TEXT = "#2ea043", "#6e7681", "#0d1117", "#ffffff" +def blank(match): + if match.group("code") is not None: + return match.group() + if match.group("comment") is None: + return "LITERAL" + return re.sub(r"[^\n]", " ", match.group()) + + +def code(text): + return CPP_TOKEN.sub(blank, text.replace("\\\n", "").replace("\\\r\n", "")) + def body_end(text, start): depth = 0 for i in range(start, len(text)): @@ -73,34 +103,94 @@ def body_end(text, start): def stub_calls(text): - calls = [STUB] + helpers = {} for match in STUB_WRAPPER.finditer(text): - if STUB in text[match.end() - 1:body_end(text, match.end() - 1)]: - calls.append(match.group(1) + "(") - return calls + body = text[match.end() - 1:body_end(text, match.end() - 1)] + 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): + depth, start, parts = 0, 1, [] + for i, character in enumerate(body[1:], 1): + if character == "{": + depth += 1 + elif character == "}": + depth -= 1 + elif character == ";" and not depth: + parts.append(body[start:i + 1]) + start = i + 1 + parts.append(body[start:]) + return parts + + +def inert_statement(statement, wrappers): + if EMPTY_RETURN.fullmatch(statement) or CONST_RETURN.fullmatch(statement) or BARE_THROW.fullmatch(statement): + return True + call = WRAPPER_RETURN.fullmatch(statement) + return bool(call) and call.group(1) in wrappers + + +def inert_body(body, wrappers): + terminals = 0 + for statement in statements(body): + statement = statement.strip() + if not statement: + continue + if DISCARD.fullmatch(statement): + continue + if terminals or not inert_statement(statement, wrappers): + return False + terminals += 1 + return True + + +def inert_wrappers(text): + bodies = {m.group(1): text[m.end() - 1:body_end(text, m.end() - 1)] for m in STUB_WRAPPER.finditer(text)} + names = set() + while True: + found = {name for name, body in bodies.items() if name not in names and inert_body(body, names)} + if not found: + return names + names |= found def scan_library(path): - done, todo = set(), set() + done, todo, inert, live = set(), set(), set(), set() for source in path.rglob("*.cpp"): if "tests" in source.relative_to(path).parts: continue - text = 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): name = match.group(1) - if name.endswith("_nid_no_patch"): + if name.endswith(("_nid_no_patch", "_nid_no_patch_cut")): continue body = text[match.end() - 1:body_end(text, match.end() - 1)] - (todo if any(call in body for call in calls) else done).add(name) + 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) + (inert if inert_body(body, wrappers) else live).add(name) todo -= done + inert -= live group = {"name": path.name, "label": path.name.removeprefix("libSce"), "done": len(done), "todo": len(todo), - "done_names": sorted(done), "todo_names": sorted(todo)} - cmake = path / "CMakeLists.txt" - if not (done or todo) and cmake.is_file(): - shared = re.search(r'^\s*include\(\s*"?\$\{CMAKE_CURRENT_SOURCE_DIR\}/\.\./([\w.-]+)/[\w.-]+\.cmake"?\s*\)', cmake.read_text(), re.M) - if shared and (path.parent / shared[1]).is_dir() and shared[1] != path.name: - group["shared_sources"] = shared[1] + "done_names": sorted(done), "todo_names": sorted(todo), "inert_names": sorted(inert)} + if not (done or todo): + cmake = path / "CMakeLists.txt" + texts = [(CMAKE_SHARED, cmake.read_text(encoding="utf-8", errors="ignore"))] if cmake.is_file() else [] + 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: + group["shared_sources"] = owners[0] return group @@ -114,18 +204,33 @@ 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("_")) def collect_shaders(): isa = {} - for line in ISA.read_text().splitlines(): + for line in ISA.read_text(encoding="utf-8", errors="ignore").splitlines(): if line and not line.startswith("#"): name, encoding = line.split() isa[name] = encoding by_camel = {camel(name): name for name in isa} - enum = re.search(r"enum class RdnaOpcode[^{]*\{(.*?)\};", OPCODES.read_text(), re.S).group(1) + enum = re.search(r"enum class RdnaOpcode[^{]*\{(.*?)\};", OPCODES.read_text(encoding="utf-8", errors="ignore"), re.S).group(1) opcodes = [o for o in re.findall(r"^\s*([A-Z]\w*)\s*[,=]", enum, re.M) if o not in OPCODE_SENTINELS] supported, extra = set(), [] for opcode in opcodes: @@ -184,7 +289,7 @@ def squarify(values, x, y, w, h): def cells(count, x, y, w, h): - if not count: + if not count or w <= 0 or h <= 0: return [] rows = max(1, min(count, round((count * h / w) ** 0.5))) result = [] @@ -216,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>") @@ -255,7 +361,10 @@ def table(heading, column, data): f'<a href="{SOURCE}/core/libs/prx/{owner}">{owner}</a></td></tr>') continue total = group["done"] + group["todo"] - percent = f'{100 * group["done"] / total:.0f}%' if total else "-" + if not total: + rows.append(f'<tr><td>{escape(group["name"])}</td><td colspan="3">No exports</td></tr>') + continue + percent = f'{100 * group["done"] / total:.0f}%' rows.append(f'<tr><td>{escape(group["name"])}</td><td>{group["done"]}</td><td>{total}</td><td>{percent}</td></tr>') rows.append(f'<tr><th>Total</th><th>{data["done"]}</th><th>{data["total"]}</th><th>{data["percent"]}%</th></tr>') rows.append("</table>") @@ -276,14 +385,17 @@ def summary(libraries, shaders): f'<p>Generated by <a href="{SOURCE}/tools/progress.py">tools/progress.py</a> on every push to <code>main</code>. ' 'Raw numbers: <a href="progress.json">progress.json</a>.</p>', table("System libraries", "Library", libraries), - "<p>A function is implemented when it no longer calls <code>NotImplemented_nid_no_patch</code>. " + "<p>A function is implemented when it no longer calls <code>NotImplemented_nid_no_patch</code>; " + "comparison reports separately mark implemented functions whose body only returns a constant or throws. " f'The total only includes functions already declared in <a href="{SOURCE}/core/libs/prx">core/libs/prx</a>, ' "not every function exported by the PS5 firmware. Shared-source libraries are counted only at their source library. " + "Libraries with no exports only provide an empty module for titles that load them. " f'<a href="{SOURCE}/docs/dev/PROGRESS.md">Counting rules</a>.</p>', table("GPU shader instructions", "Encoding", shaders), f'<p>The total is the AMD RDNA 1 + RDNA 2 instruction list (<a href="{SOURCE}/tools/rdna_isa.txt">tools/rdna_isa.txt</a>). ' f'An instruction is implemented when the <a href="{SOURCE}/core/shader/recompiler/RdnaDecoder">decoder</a> recognizes it. ' f"Decoded opcodes not present in AMD's public list are not counted: {extra}.</p>", + '<p><a href="history.html">Progress history</a> (one point per day).</p>', "</body></html>", ]) + "\n" @@ -311,6 +423,7 @@ def compare(title, column, unit, base, head): base_all, head_all = base_done | names(base, "todo", owners), head_done | names(head, "todo", owners) implemented, declared = head_done - base_done, head_all - base_all - head_done regressed, removed = base_done & (head_all - head_done), base_all - head_all + inert = implemented & names(head, "inert", owners) if not (implemented or declared or regressed or removed or shared): return [] delta = round(head["percent"] - base["percent"], 2) @@ -320,6 +433,7 @@ def compare(title, column, unit, base, head): changes = f', {", ".join(counts)} {unit}' if counts else "" lines = [f'{icon} **{title}**: {head["percent"]}% ({delta:+}%{changes})', ""] lines += details("✅", "implemented", column, implemented) + lines += details("⚪", "behaviourally empty (constant return or throw)", column, inert) lines += details("🆕", "declared as stubs", column, declared) lines += details("⚠️", "went back to stubs", column, regressed) lines += details("🗑️", "removed", column, removed) @@ -333,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 / "index.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 new file mode 100644 index 0000000000..ca8460652a --- /dev/null +++ b/tools/progress_history.py @@ -0,0 +1,245 @@ +import argparse +import hashlib +import io +import json +import os +import stat +import sys +import subprocess +import tarfile +import tempfile +from datetime import date, datetime, timezone +from html import escape +from pathlib import Path + +import progress + +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, MUTED = "#0d1117", "#30363d", "#c9d1d9", "#8b949e" + + +def git(*args): + return subprocess.run(["git", "-C", str(progress.ROOT), *args], check=True, capture_output=True).stdout + + +def daily_commits(ref): + seen = {} + for line in git("log", "--first-parent", "--format=%H %ct", ref).decode().splitlines(): + sha, timestamp = line.split() + seen.setdefault(datetime.fromtimestamp(int(timestamp), timezone.utc).date().isoformat(), sha) + return sorted(seen.items()) + + +def measure(sha): + paths = [progress.PRX.relative_to(progress.ROOT).as_posix(), progress.OPCODES.relative_to(progress.ROOT).as_posix()] + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + try: + 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 + progress.PRX, progress.OPCODES = root / paths[0], root / paths[1] + try: + libraries, shaders = progress.collect_libraries(), progress.collect_shaders() + except (OSError, AttributeError): + return None + finally: + progress.PRX, progress.OPCODES = saved + return {name: {key: data[key] for key in ("done", "total", "percent")} + for name, data in (("libraries", libraries), ("shaders", shaders))} + + +def 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): + 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 <= 14: + return f"{day:%b} {day.day}" + if span <= 150: + 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) + px = lambda d: LEFT + (d - first) / span * (WIDTH - LEFT - RIGHT) + 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'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {WIDTH} {height}" role="img" ' + 'aria-label="Implemented and declared system library functions and GPU shader instructions over time" ' + f'font-family="sans-serif" font-size="12" fill="{TEXT}">', + '<style>.hit{fill:#fff;fill-opacity:0}.hit:hover{fill-opacity:0.06}</style>', + f'<rect width="{WIDTH}" height="{height}" rx="6" fill="{BACKGROUND}"/>'] + 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'<text x="{LEFT}" y="{top - 12}" font-size="13" font-weight="bold">{title}</text>') + for tick in range(0, peak + 1, step): + 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" fill="{MUTED}">{tick:,}</text>') + for x, label in labels: + if label: + out.append(f'<line x1="{x:.1f}" x2="{x:.1f}" y1="{top}" y2="{base}" stroke="{GRID}" stroke-width="0.5"/>') + 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'<polygon points="{xs[0]:.1f},{base} {line(done)} {xs[-1]:.1f},{base}" fill="{color}" opacity="0.15"/>') + out.append(f'<polyline points="{line(total)}" fill="none" stroke="{MUTED}" stroke-width="2" stroke-dasharray="6 4"/>') + out.append(f'<polyline points="{line(done)}" fill="none" stroke="{color}" stroke-width="2"/>') + if len(points) <= MAX_DOTS: + out += [f'<circle cx="{x:.1f}" cy="{y:.1f}" r="3" fill="{color}"/>' for x, y in done] + last = points[-1][name] + declared_y, implemented_y = spread([total[-1][1], done[-1][1]]) + out.append(f'<text x="{WIDTH - RIGHT + 8}" y="{declared_y + 4:.1f}" fill="{MUTED}">Declared {last["total"]:,}</text>' + f'<text x="{WIDTH - RIGHT + 8}" y="{implemented_y + 4:.1f}" fill="{TEXT}" font-weight="bold">' + f'<tspan fill="{color}">\u25cf</tspan> Implemented {last["done"]:,} ({last["percent"]:.0f}%)</text>') + for left, right, p in zip(edges, edges[1:], points): + out.append(f'<rect class="hit" x="{left:.1f}" y="{top}" width="{right - left:.1f}" height="{PANEL}">' + f'<title>{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) + + +def page(points): + rows = "\n".join(f'
' + "".join( + f'' for name, _, _ in SERIES) + "" + for p in reversed(points)) + return "\n".join([ + '', + '', + 'AnyPS5 progress history', + '', + '', + '

Progress history

', + chart(points), + 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 day for the exact numbers. Raw numbers: history.json. ' + 'Back to the current progress.

', + '
{p["date"]}{p[name]["done"]}/{p[name]["total"]}{p[name]["percent"]}%
' + "".join(f'' for _, label, _ in SERIES) + '', + rows, + '
Date{escape(label)}%
', + '', + ]) + "\n" + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("output", type=Path) + parser.add_argument("--ref", default="HEAD", help="branch or commit whose first-parent history is measured") + parser.add_argument("--cache", type=Path, help="reuse per-commit counts while the scanner, ISA list and Python version match") + args = parser.parse_args() + 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)) + print(f"{len(points)} days, {points[0]['date']} .. {points[-1]['date']}") diff --git a/tools/select_tests.py b/tools/select_tests.py new file mode 100644 index 0000000000..efb972d0a1 --- /dev/null +++ b/tools/select_tests.py @@ -0,0 +1,288 @@ +import argparse +import collections +import json +import os +import re +import subprocess +import sys +from pathlib import Path + +FULL = re.compile(r"^CMakeLists\.txt$|^core/CMakeLists\.txt$|^core/libs/CMakeLists\.txt$|^\.github/workflows/build\.yml$" + r"|^3rdparty/|^cmake/|^\.gitmodules$|^tools/select_tests\.py$") +LOCAL = re.compile(r"(^|/)CMakeLists\.txt$|\.cmake$") +BATCH = 20000 + + +class Build: + def __init__(self, path): + cache = {} + for line in (Path(path) / "CMakeCache.txt").read_text(encoding="utf-8").splitlines(): + name, _, value = line.partition("=") + cache[name.partition(":")[0]] = value + self.path = os.path.normpath(cache["CMAKE_CACHEFILE_DIR"]) + self.source = os.path.normpath(cache["CMAKE_HOME_DIRECTORY"]) + self.program = cache.get("CMAKE_MAKE_PROGRAM") or "ninja" + self.encoding = "utf-8" + if sys.platform == "win32": + codepage = self.run("-t", "wincodepage").strip() + self.encoding = {"Build file encoding: UTF-8": "utf-8", "Build file encoding: ANSI": "mbcs"}[codepage] + + def run(self, *args): + return subprocess.run([self.program, "-C", self.path, *args], capture_output=True, text=True, + encoding=self.encoding, check=True).stdout + + def absolute(self, path): + return os.path.normpath(path if os.path.isabs(path) else os.path.join(self.path, path)) + + def key(self, path): + return os.path.normcase(self.absolute(path)) + + def repo(self, path): + absolute = self.absolute(path) + if inside(absolute, self.path) or not inside(absolute, self.source): + return None + return relative(absolute, self.source) + + def query(self, targets): + found = {} + for batch in chunks(targets): + current, section = None, None + for line in self.run("-t", "query", *batch).splitlines(): + if not line.startswith(" "): + current = line.rstrip(":") + found[current] = {"inputs": [], "phony": False} + elif line.startswith(" input: "): + section = "inputs" + found[current]["phony"] = line.strip() == "input: phony" + elif line.startswith(" outputs:"): + section = None + elif section and line.startswith(" "): + found[current]["inputs"].append(line.strip().removeprefix("|| ").removeprefix("| ")) + return found + + def walk(self, roots): + nodes, todo = {}, list(roots) + while todo: + batch = sorted({t for t in todo if t not in nodes}) + if not batch: + break + nodes.update(self.query(batch)) + todo = [i for t in batch for i in nodes.get(t, {"inputs": []})["inputs"]] + return nodes + + def leaves(self): + nodes = self.walk(["all"]) + leaves, todo, seen = set(), ["all"], set() + while todo: + name = todo.pop() + if name in seen or name not in nodes: + continue + seen.add(name) + if nodes[name]["phony"]: + todo.extend(nodes[name]["inputs"]) + else: + leaves.add(name) + return sorted(leaves) + + def tests(self, *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(): + 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() + if args.command == "ctest": + selected = Path(args.build, "select.tests") + extra = ["-I", selected.read_text(encoding="utf-8").strip()] if selected.exists() else [] + return subprocess.run(["ctest", "--test-dir", args.build, *args.arguments, *extra]).returncode + if args.command == "explain": + try: + graph = json.loads(args.graph.read_text(encoding="utf-8")) + changed = [line.strip() for line in args.changed.read_text(encoding="utf-8").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")) + 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 2dca532f4e..a701cb5472 100644 --- a/tools/stubgen.py +++ b/tools/stubgen.py @@ -1,7 +1,10 @@ #!/usr/bin/env python3 import argparse +import os import re +import stat import sys +import tempfile from pathlib import Path try: @@ -27,10 +30,12 @@ def parse_nids(path): nids = [] seen = set() with open(path, "r", encoding="utf-8-sig", errors="replace") as f: - for line in f: + for number, line in enumerate(f, 1): nid = strip_nid_token(line) if not nid: continue + if re.fullmatch(r"[A-Za-z0-9+\-]{11}", nid) is None: + raise ValueError(f"{path}:{number}: invalid NID {nid!r}") if nid in seen: continue seen.add(nid) @@ -104,13 +109,14 @@ def _default_td_path(module): def td_target(module, td_path, td_lib): if td_path is None: - td_path = _default_td_path(module) + td_path = "../../../" + _default_td_path(module) if td_lib is None: td_lib = module return (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() @@ -142,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)) @@ -167,44 +173,84 @@ def generate(module, nids, db, td_path=None, td_lib=None): return (code, td_text) +def same_file(left, right): + left, right = Path(left), Path(right) + return left.resolve() == right.resolve() or (left.exists() and right.exists() and left.samefile(right)) + + +def write_outputs(outputs): + staged = [] + umask = os.umask(0) + os.umask(umask) + try: + for filename, content in outputs: + path = Path(filename).resolve() + if path.is_dir(): + raise IsADirectoryError(str(path)) + with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", newline="\n", dir=path.parent, prefix=path.name + ".", suffix=".tmp", delete=False) as handle: + temporary = Path(handle.name) + staged.append((temporary, path)) + handle.write(content) + if path.exists(): + 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: + try: + temporary.unlink(missing_ok=True) + except PermissionError: + temporary.chmod(0o600) + temporary.unlink(missing_ok=True) + + def main(argv=None): 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") ap.add_argument("--db", required=False, default=None, help="path to NID name CSV, NID name per line like aerolib.csv") ap.add_argument("--out", required=False, default=None, help="output file for code, default stdout") - ap.add_argument("--td-path", required=False, default=None, help="override file path used inside TechnicalDebt lines, default derived from module") + ap.add_argument("--td-path", required=False, default=None, help="override file path used inside TechnicalDebt lines, default derived from module, relative to docs/dev/debt") ap.add_argument("--td-lib", required=False, default=None, help="override lib label used inside TechnicalDebt lines, default derived from module") - ap.add_argument("--td-out", required=False, default=None, help="output file for TechnicalDebt lines, default stdout") + 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) - nids = parse_nids(args.nids) - db = {} - if args.db: - db = parse_db(args.db) - code, td = generate(args.module, nids, db, args.td_path, args.td_lib) - if args.td_only: + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.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") + destinations = [args.td_out] if args.td_only else [args.out, args.td_out] + for destination in filter(None, destinations): + for source in filter(None, [args.nids, args.db]): + if same_file(destination, source): + ap.error(f"output {destination!r} would overwrite input {source!r}") + nids = parse_nids(args.nids) + db = parse_db(args.db) if args.db else {} + code, td = generate(args.module, nids, db, args.td_path, args.td_lib) + outputs = [] + if not args.td_only and args.out: + outputs.append((args.out, code)) if args.td_out: - with open(args.td_out, "w", encoding="utf-8", newline="\n") as f: - f.write(td) - else: + outputs.append((args.td_out, td)) + write_outputs(outputs) + except (OSError, ValueError) as error: + ap.error(str(error)) + if args.td_only: + if not args.td_out: sys.stdout.write(td) return 0 - if args.out: - with open(args.out, "w", encoding="utf-8", newline="\n") as f: - f.write(code) - else: + if not args.out: sys.stdout.write(code) - if args.td_out: - with open(args.td_out, "w", encoding="utf-8", newline="\n") as f: - f.write(td) - else: - if args.out: - sys.stdout.write(td) - else: - if td: - sys.stdout.write("\n") - sys.stdout.write(td) + if not args.td_out: + (sys.stdout if args.out else sys.stderr).write(td) return 0 diff --git a/tools/test_web.py b/tools/test_web.py new file mode 100644 index 0000000000..245e6b4eac --- /dev/null +++ b/tools/test_web.py @@ -0,0 +1,37 @@ +import argparse +import os +from pathlib import Path +import re +import subprocess +import sys +import tempfile +import urllib.request + +ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(ROOT / "core/relinker/relinker/tests")) +from test_guest_intel_trampolines import main_fixture +from test_guest_module_directories import module_with_symbol + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--build", type=Path, required=True) + args = parser.parse_args() + 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 = (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-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) + + +if __name__ == "__main__": + 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..3a56e49429 --- /dev/null +++ b/tools/tests/test_build_web.py @@ -0,0 +1,129 @@ +import contextlib +import hashlib +import io +import json +from pathlib import Path +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())) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_check_compile.py b/tools/tests/test_check_compile.py new file mode 100644 index 0000000000..3dcc4b1d43 --- /dev/null +++ b/tools/tests/test_check_compile.py @@ -0,0 +1,544 @@ +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 + +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) + self.root = Path(holder.name).resolve() + subprocess.run(["git", "init", "-q", str(self.root)], check=True) + subprocess.run(["git", "-C", str(self.root), "config", "user.email", "tests@example.com"], check=True) + subprocess.run(["git", "-C", str(self.root), "config", "user.name", "tests"], check=True) + self.build = self.root / "build" + self.build.mkdir() + + def tool(self, *arguments, env=None): + environment = dict(os.environ) + environment.update(env or {}) + return subprocess.run([sys.executable, "-I", str(TOOL), *arguments], cwd=self.root, + capture_output=True, text=True, encoding="utf-8", env=environment) + + def source(self, relative, text="int x;\n"): + path = self.root / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8") + return path + + def compiler(self, body, name="compiler.py"): + script = self.root / name + script.write_text(body, encoding="utf-8") + return script + + def entry(self, relative, command): + return {"directory": str(self.build), "command": command, "file": str(self.root / relative)} + + def manifest(self, entries): + (self.build / "compile_commands.json").write_text(json.dumps(entries), encoding="utf-8") + + def failing(self, relative, message=None): + source = self.source(relative) + message = message if message is not None else f"{relative}:1:1: error: no\n" + script = self.compiler("import sys\nsys.stderr.write(%r)\nsys.exit(1)\n" % message) + self.manifest([self.entry(relative, f'"{sys.executable}" "{script}" "{source}"')]) + return source + + def test_a_translation_unit_that_does_not_compile_fails_with_the_compiler_output(self): + self.failing("unit.cpp") + result = self.tool("unit.cpp") + self.assertEqual(result.returncode, 1) + self.assertIn("unit.cpp:1:1: error: no", result.stderr) + self.assertIn("1 of 1 translation units did not compile", result.stderr) + + def test_a_translation_unit_that_compiles_passes(self): + source = self.source("unit.cpp") + script = self.compiler("import sys\nsys.exit(0)\n") + self.manifest([self.entry("unit.cpp", f'"{sys.executable}" "{script}" "{source}"')]) + result = self.tool("unit.cpp") + self.assertEqual(result.returncode, 0) + self.assertIn("1 translation units compile", result.stdout) + + def test_the_command_is_rewritten_to_syntax_only_without_object_or_dependency_files(self): + source = self.source("unit.cpp") + script = self.compiler( + "import json\nimport sys\nfrom pathlib import Path\n" + "Path(sys.argv[1]).write_text(json.dumps(sys.argv[2:]))\n") + seen = self.root / "arguments.json" + self.manifest([self.entry("unit.cpp", f'"{sys.executable}" "{script}" "{seen}" -c -o unit.o ' + f'-MD -MT unit.o -MF unit.o.d "{source}"')]) + result = self.tool("unit.cpp") + self.assertEqual(result.returncode, 0) + self.assertEqual(json.loads(seen.read_text(encoding="utf-8")), ["-fsyntax-only", str(source)]) + + def test_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") + self.assertEqual(result.returncode, 1) + self.assertIn("orphan.cpp: no compile command in", result.stderr) + self.assertIn("no translation unit to check in", result.stderr) + + def test_a_translation_unit_outside_the_build_is_reported_but_the_others_decide(self): + source = self.source("unit.cpp") + script = self.compiler("import sys\nsys.exit(0)\n") + self.manifest([self.entry("unit.cpp", f'"{sys.executable}" "{script}" "{source}"')]) + result = self.tool("unit.cpp", "orphan.cpp") + self.assertEqual(result.returncode, 0) + self.assertIn("1 translation units compile", result.stdout) + self.assertIn("orphan.cpp: no compile command in", result.stderr) + + def test_an_absolute_path_names_the_same_translation_unit(self): + source = self.failing("unit.cpp") + result = self.tool(str(source)) + self.assertEqual(result.returncode, 1) + self.assertIn("unit.cpp:1:1: error: no", result.stderr) + + def test_a_relative_source_argument_in_the_command_is_replaced(self): + source = self.source("unit.cpp") + script = self.compiler( + "import json\nimport sys\nfrom pathlib import Path\n" + "Path(sys.argv[1]).write_text(json.dumps(sys.argv[2:]))\n") + seen = self.root / "arguments.json" + relative = os.path.relpath(source, self.build) + self.manifest([self.entry("unit.cpp", f'"{sys.executable}" "{script}" "{seen}" "{relative}"')]) + result = self.tool("unit.cpp") + self.assertEqual(result.returncode, 0) + self.assertEqual(json.loads(seen.read_text(encoding="utf-8")), ["-fsyntax-only", str(source)]) + + def test_a_build_directory_that_is_not_a_child_of_the_checkout_is_matched(self): + self.build = self.root / "out" / "build" + self.build.mkdir(parents=True) + source = self.source("unit.cpp") + script = self.compiler("import sys\nsys.exit(0)\n") + self.manifest([self.entry("unit.cpp", f'"{sys.executable}" "{script}" "{source}"')]) + result = self.tool("--build", str(self.build), "unit.cpp") + self.assertEqual(result.returncode, 0) + self.assertIn("1 translation units compile", result.stdout) + + def test_a_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) + subprocess.run(["git", "-C", str(self.root), "commit", "-qm", "add"], check=True) + self.source("caf\u00e9.cpp", "int y;\n") + result = self.tool("--base", "HEAD") + self.assertEqual(result.returncode, 1) + self.assertIn("error: no", result.stderr) + + def test_a_longer_path_does_not_shadow_the_translation_unit(self): + source = self.source("src/foo.cpp") + other = self.source("plugins/x/src/foo.cpp") + passes = self.compiler("import sys\nsys.exit(0)\n", name="passes.py") + fails = self.compiler( + "import sys\nsys.stderr.write('src/foo.cpp:1:1: error: no\\n')\nsys.exit(1)\n", name="fails.py") + self.manifest([ + self.entry("plugins/x/src/foo.cpp", f'"{sys.executable}" "{passes}" "{other}"'), + self.entry("src/foo.cpp", f'"{sys.executable}" "{fails}" "{source}"'), + ]) + result = self.tool("src/foo.cpp") + self.assertEqual(result.returncode, 1) + self.assertIn("src/foo.cpp:1:1: error: no", result.stderr) + + def test_a_new_untracked_translation_unit_is_checked(self): + self.source("base.cpp") + subprocess.run(["git", "-C", str(self.root), "add", "base.cpp"], check=True) + subprocess.run(["git", "-C", str(self.root), "commit", "-qm", "add"], check=True) + self.failing("brandnew.cpp") + result = self.tool("--base", "HEAD") + self.assertEqual(result.returncode, 1) + self.assertIn("brandnew.cpp:1:1: error: no", result.stderr) + + def test_a_missing_build_directory_is_rejected(self): + result = self.tool("--build", "nowhere", "unit.cpp") + self.assertEqual(result.returncode, 2) + self.assertIn("not a directory", result.stderr) + + def test_the_changed_translation_units_are_taken_from_the_base(self): + self.failing("changed.cpp") + subprocess.run(["git", "-C", str(self.root), "add", "changed.cpp"], check=True) + subprocess.run(["git", "-C", str(self.root), "commit", "-qm", "add"], check=True) + self.source("changed.cpp", "int y;\n") + result = self.tool("--base", "HEAD") + self.assertEqual(result.returncode, 1) + self.assertIn("changed.cpp:1:1: error: no", result.stderr) + + def test_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) + subprocess.run(["git", "-C", str(self.root), "commit", "-qm", "add"], check=True) + self.manifest([]) + result = self.tool("--base", "HEAD") + self.assertEqual(result.returncode, 0) + self.assertIn("no changed translation unit", result.stdout) + + def test_the_commands_come_from_ninja_when_the_manifest_is_absent(self): + source = self.source("unit.cpp") + script = self.compiler("import sys\nsys.exit(0)\n") + entry = self.entry("unit.cpp", f'"{sys.executable}" "{script}" "{source}"') + bin = self.root / "bin" + bin.mkdir() + stub = bin / "ninja_stub.py" + stub.write_text("import json\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('@"%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']}", + "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 new file mode 100644 index 0000000000..fcef15671a --- /dev/null +++ b/tools/tests/test_check_conventions_agent_files.py @@ -0,0 +1,34 @@ +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +from check_conventions import allowed_notes + + +class AgentFileTests(unittest.TestCase): + def test_agent_files_are_rejected_in_any_case(self): + for name in ( + "CLAUDE.md", "claude.md", "Claude.md", "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")) + self.assertIs(allowed_notes("core/x.cpp"), None) + + +if __name__ == "__main__": + unittest.main() 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_body_bom.py b/tools/tests/test_check_conventions_body_bom.py new file mode 100644 index 0000000000..2bc30ad43f --- /dev/null +++ b/tools/tests/test_check_conventions_body_bom.py @@ -0,0 +1,51 @@ +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +BODY = ( + "### What\nFix x.\n\n### Tested\nNot tested.\n\n### Checklist\n" + "- [x] Depends on: none\n- [x] AI-assisted: no\n" +) + + +class BodyBomTests(unittest.TestCase): + def run_checker(self, body): + checker = Path(__file__).resolve().parents[1] / "check_conventions.py" + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + + def git(*args): + return subprocess.run( + ["git", "-c", "user.name=Conventions Test", "-c", "user.email=test@example.com", + "-c", "commit.gpgsign=false", *args], + cwd=root, capture_output=True, text=True, check=True, + ) + + git("init", "-q") + (root / "README.md").write_text("# Example\n", encoding="utf-8") + git("add", ".") + git("commit", "-qm", "docs: add readme") + (root / "body.md").write_bytes(body) + return subprocess.run( + [sys.executable, "-I", str(checker), "--base", "HEAD", "--head", "HEAD", + "--title", "fix: x", "--body", "body.md"], + cwd=root, capture_output=True, text=True, encoding="utf-8", + env={key: value for key, value in os.environ.items() + if key not in ("GITHUB_EVENT_PATH", "GITHUB_STEP_SUMMARY", "GITHUB_ACTIONS")}, + ) + + def test_body_with_utf8_bom_keeps_its_first_section(self): + result = self.run_checker(b"\xef\xbb\xbf" + BODY.encode("utf-8")) + self.assertNotIn("section 'What' is missing", result.stdout) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_body_without_bom_passes(self): + result = self.run_checker(BODY.encode("utf-8")) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_check_conventions_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 new file mode 100644 index 0000000000..200d32f39b --- /dev/null +++ b/tools/tests/test_check_conventions_ci_gate.py @@ -0,0 +1,110 @@ +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +CHECKER = Path(__file__).resolve().parents[1] / "check_conventions.py" +WORKFLOWS = Path(__file__).resolve().parents[2] / ".github/workflows" + +GATE = """ eligibility: + runs-on: ubuntu-latest + outputs: + tests: ${{ steps.gate.outputs.tests }} + steps: + - run: echo +""" + + +def workflow(trigger, job): + return f"name: Test\n\non:\n{trigger}\njobs:\n{GATE}{job}" + + +PULL_REQUEST = " pull_request:\n branches: [main]\n" + + +class CiGateTests(unittest.TestCase): + def check(self, 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, + ) + + git("init", "-q") + (root / "README.md").write_text("readme\n", encoding="utf-8") + git("add", ".") + git("commit", "-qm", "docs: add readme") + for name, text in files.items(): + path = root / ".github/workflows" / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8") + git("add", ".") + git("commit", "-qm", "ci: add workflow") + 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 assertGated(self, text): + result = self.check({"build.yml": text}) + self.assertNotIn("ci-gate", result.stdout, result.stdout + result.stderr) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def assertUngated(self, text, job): + result = self.check({"build.yml": text}) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("[ci-gate]", result.stdout) + self.assertIn(job, result.stdout) + + def test_job_without_gate_fails(self): + self.assertUngated(workflow(PULL_REQUEST, " tests:\n runs-on: ubuntu-latest\n steps:\n - run: make\n"), "tests") + + def test_job_needing_the_gate_without_if_fails(self): + self.assertUngated(workflow(PULL_REQUEST, " tests:\n needs: eligibility\n runs-on: ubuntu-latest\n"), "tests") + + def test_if_on_another_condition_fails(self): + self.assertUngated(workflow(PULL_REQUEST, " tests:\n needs: eligibility\n if: github.event_name == 'pull_request'\n"), "tests") + + def test_gated_job_passes(self): + self.assertGated(workflow(PULL_REQUEST, " tests:\n needs: eligibility\n if: ${{ needs.eligibility.outputs.tests == 'true' }}\n")) + + def test_gate_written_over_several_lines_passes(self): + self.assertGated(workflow(PULL_REQUEST, " tests:\n needs:\n - other\n - eligibility\n if: >-\n github.event_name == 'push' ||\n needs.eligibility.outputs.tests == 'true'\n")) + + def test_paths_filter_passes(self): + self.assertGated(workflow(" pull_request:\n paths:\n - 'core/**'\n", " tests:\n runs-on: ubuntu-latest\n")) + + def test_paths_of_another_trigger_do_not_count(self): + self.assertUngated(workflow(" push:\n paths:\n - 'core/**'\n pull_request:\n branches: [main]\n", " tests:\n runs-on: ubuntu-latest\n"), "tests") + + def test_inline_trigger_list_fails(self): + self.assertUngated("name: Test\non: [push, pull_request]\njobs:\n tests:\n runs-on: ubuntu-latest\n", "tests") + + 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) + result = self.check(files) + self.assertNotIn("ci-gate", result.stdout, result.stdout + result.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_check_conventions_comments.py b/tools/tests/test_check_conventions_comments.py index 8f577a3702..f0d3d223fb 100644 --- a/tools/tests/test_check_conventions_comments.py +++ b/tools/tests/test_check_conventions_comments.py @@ -1,12 +1,48 @@ +import contextlib +import io import os 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])) -from check_conventions import STUB_BODY, function_body +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): @@ -37,6 +73,28 @@ class FunctionBodyTests(unittest.TestCase): source = 'int APS5_VABI sceExample() {\nuse("// }", "/* { */");\nreturn 0;\n}' self.assertEqual(function_body(source.splitlines(), 0), 'use("","");return0;') + def test_raw_literal_braces_and_comments_preserve_following_statements(self): + source = 'int APS5_VABI sceExample() {\nuse(R"json({"brace":"}","comment":"/* hidden */"})json");\nvalidate();\nreturn 0;\n}' + body = function_body(source.splitlines(), 0) + self.assertEqual(body, 'use("");validate();return0;') + self.assertIsNone(STUB_BODY.fullmatch(body)) + + def test_raw_literal_return_keeps_a_literal_sentinel(self): + source = 'const char* APS5_VABI sceExample() { return R"json({"url":"https://example.test"})json"; }' + body = function_body(source.splitlines(), 0) + self.assertEqual(body, 'return"";') + self.assertIsNone(STUB_BODY.fullmatch(body)) + + def test_apostrophes_in_comments_do_not_open_character_literals(self): + source = "int APS5_VABI sceExample() {\n// don't fail here\nvalidate();\n// it's fine\nreturn 0;\n}" + body = function_body(source.splitlines(), 0) + self.assertEqual(body, "validate();return0;") + self.assertIsNone(STUB_BODY.fullmatch(body)) + source = "int APS5_VABI sceExample() {\n/* don't */\nreturn 0;\n}\nstatic const char Separator = '/';" + self.assertEqual(function_body(source.splitlines(), 0), "return0;") + source = "int APS5_VABI sceExample() {\nuse(\"http://x\"); // isn't a stub\nreturn 0;\n}" + self.assertEqual(function_body(source.splitlines(), 0), 'use("");return0;') + def test_declaration_has_no_body(self): source = "int APS5_VABI sceExample();\nint APS5_VABI sceOther() { return 0; }" self.assertIsNone(function_body(source.splitlines(), 0)) @@ -46,6 +104,107 @@ class FunctionBodyTests(unittest.TestCase): self.assertIsNone(function_body(source.splitlines(), 0)) +class RawLiteralCommentTests(unittest.TestCase): + path = "core/example.cpp" + + def findings(self, source, added): + check = Check.__new__(Check) + check.tree = set() + check.status = {} + check.findings = [] + lines = source.split("\n") + check.added = {self.path: [(number, lines[number - 1]) for number in added]} + with patch.object(check, "show", return_value=source), patch.object(check, "duplicates"): + check.code() + return check.findings + + def test_isolated_checker_loads_its_trusted_sibling_from_another_directory(self): + checker = Path(__file__).resolve().parents[1] / "check_conventions.py" + with tempfile.TemporaryDirectory() as directory: + Path(directory, "nid_names.py").write_text('raise RuntimeError("untrusted current directory")\n', encoding="utf-8") + result = subprocess.run([sys.executable, "-I", str(checker), "--help"], cwd=directory, capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("check a branch", result.stdout) + + def test_raw_url_does_not_create_a_comment(self): + source = 'static const char* Metadata = R"json({"url":"https://example.test"})json";' + self.assertEqual(self.findings(source, [1]), []) + + def test_blank_added_cpp_lines_preserve_positions(self): + for source, added in (("\n", [1]), ("int value;\n\n", [2])): + with self.subTest(source=source): + self.assertEqual(self.findings(source, added), []) + + def test_unicode_separators_in_literals_keep_physical_lines(self): + for separator in ("\x85", "\u2028", "\u2029"): + with self.subTest(separator=separator): + source = f'static const char* Metadata = R"text(https://example.test/{separator}more)text";\nint value; // pending\n' + self.assertEqual(self.findings(source, [1, 2]), [("comment", self.path, 2, "")]) + + def test_added_line_inside_an_existing_multiline_raw_literal_is_not_a_comment(self): + source = 'static const char* Metadata = R"text(\nhttps://example.test/metadata\n)text";\n' + self.assertEqual(self.findings(source, [2]), []) + + def test_actual_comment_after_a_raw_literal_retains_its_line(self): + for comment in ("// pending implementation", "/* pending implementation */"): + with self.subTest(comment=comment): + source = f'static const char* Metadata = R"text(\nhttps://example.test/metadata\n)text"; {comment}\n' + self.assertEqual(self.findings(source, [2, 3]), [("comment", self.path, 3, "")]) + + def test_quotes_and_raw_looking_text_do_not_hide_actual_comments(self): + for comment in ('// " R"text(https://example.test)text"', '/* " R"text(https://example.test)text" */'): + with self.subTest(comment=comment): + self.assertEqual(self.findings(comment, [1]), [("comment", self.path, 1, "")]) + + +class 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" @@ -82,5 +241,577 @@ class SilentStubTests(unittest.TestCase): self.assertNotIn("[comment]", result.stdout) +class QuotedPathTests(unittest.TestCase): + def test_non_ascii_path_does_not_crash_the_checker(self): + 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") + git("config", "core.quotePath", "true") + readme = root / "README.md" + readme.write_text("# Example\n", encoding="utf-8") + git("add", ".") + git("commit", "-qm", "docs: add readme") + note = root / "docs/dev/caf\u00e9.md" + note.parent.mkdir(parents=True) + note.write_text("# Cafe\n\nA short note.\n", encoding="utf-8") + source = root / "core/x.cpp" + source.parent.mkdir(parents=True) + source.write_text("int x; // note\n", encoding="utf-8") + git("add", ".") + git("commit", "-qm", "feat: add cafe note") + result = subprocess.run( + [sys.executable, "-I", str(checker), "--base", "HEAD~1"], + cwd=root, capture_output=True, text=True, encoding="utf-8", + env={key: value for key, value in os.environ.items() + if key not in ("GITHUB_EVENT_PATH", "GITHUB_STEP_SUMMARY", "GITHUB_ACTIONS")}, + ) + self.assertNotIn("Traceback", result.stderr) + self.assertIn("[comment]", result.stdout) + self.assertIn("core/x.cpp", result.stdout) + + +class SilentStubSectionTests(unittest.TestCase): + path = "core/libs/prx/libSceExample/Export.cpp" + entry = f"- [Existing limitation](../../../{path}) - console layout is unknown.\n" + library_debt = "docs/dev/debt/libSceExample.md" + + def check_stub(self, debt_text, debt_path=library_debt): + checker = Path(__file__).resolve().parents[1] / "check_conventions.py" + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + + def git(*args): + return subprocess.run( + ["git", "-c", f"safe.directory={root}", "-c", "user.name=Conventions Test", + "-c", "user.email=test@example.com", "-c", "commit.gpgsign=false", *args], + cwd=root, capture_output=True, text=True, check=True, + ) + + git("init", "-q") + debt = root / "docs/dev/TechnicalDebt.md" + debt.parent.mkdir(parents=True) + debt.write_text("# Project technical debt\n", encoding="utf-8") + for library in ("libSceExample", "libSceOther"): + reference = root / "core/libs/prx" / library / "Reference.hpp" + reference.parent.mkdir(parents=True) + reference.write_text("", encoding="utf-8") + git("add", ".") + git("commit", "-qm", "docs: record technical debt") + source = root / self.path + source.write_text("int APS5_VABI sceExample() { return 0; }\n", encoding="utf-8") + (root / debt_path).parent.mkdir(parents=True, exist_ok=True) + (root / debt_path).write_text(debt_text, encoding="utf-8") + git("add", ".") + git("commit", "-qm", "feat: add example export") + 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, str(checker), "--base", "HEAD~1"], + cwd=root, capture_output=True, text=True, env=env, timeout=10, + ) + + def require_rejected(self, debt, debt_path=library_debt): + result = self.check_stub(debt, debt_path) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("[silent-stub]", result.stdout) + self.assertNotIn("[doc-link]", result.stdout) + self.assertNotIn("[debt-file]", result.stdout) + + def require_accepted(self, debt, debt_path=library_debt): + result = self.check_stub(debt, debt_path) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("All conventions checks passed", result.stdout) + + def test_debt_outside_silent_stubs_does_not_exempt_export(self): + for section in ("Build", "Unknown function info", "Functional"): + with self.subTest(section=section): + self.require_rejected(f"# libSceExample technical debt\n\n### {section}\n\n{self.entry}") + + def test_entries_after_silent_stubs_do_not_exempt_export(self): + for heading in ("# Other debt", "## Other debt", "### Functional"): + with self.subTest(heading=heading): + self.require_rejected(f"# libSceExample technical debt\n\n### Silent stubs\n\n{heading}\n\n{self.entry}") + + def test_file_entry_in_silent_stubs_exempts_export(self): + self.require_accepted(f"# libSceExample technical debt\n\n### Silent stubs\n\n{self.entry}") + + def test_named_entry_in_same_library_exempts_export(self): + entry = "- sceExample: [reference](../../../core/libs/prx/libSceExample/Reference.hpp)\n" + self.require_accepted(f"# libSceExample technical debt\n\n### Silent stubs\n\n{entry}") + + def test_named_entry_in_another_library_does_not_exempt_export(self): + entry = "- sceExample: [reference](../../../core/libs/prx/libSceOther/Reference.hpp)\n" + self.require_rejected(f"# libSceOther technical debt\n\n### Silent stubs\n\n{entry}", "docs/dev/debt/libSceOther.md") + + def test_different_function_in_same_library_does_not_exempt_export(self): + entry = "- sceExampleOther: [reference](../../../core/libs/prx/libSceExample/Reference.hpp)\n" + self.require_rejected(f"# libSceExample technical debt\n\n### Silent stubs\n\n{entry}") + + def test_subsection_in_silent_stubs_keeps_exemption(self): + self.require_accepted(f"# libSceExample technical debt\n\n### Silent stubs\n\n#### Dialogs\n\n{self.entry}") + + def test_entry_in_the_index_still_exempts_export(self): + entry = self.entry.replace("../../../", "../../") + result = self.check_stub(f"# Project technical debt\n\n### Silent stubs\n\n{entry}", "docs/dev/TechnicalDebt.md") + self.assertNotIn("[silent-stub]", result.stdout) + + def test_library_entry_in_the_index_is_rejected(self): + entry = self.entry.replace("../../../", "../../") + result = self.check_stub(f"# Project technical debt\n\n### Silent stubs\n\n{entry}", "docs/dev/TechnicalDebt.md") + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("docs/dev/TechnicalDebt.md:5: error: [debt-file]", result.stdout) + self.assertIn("docs/dev/debt/libSceExample.md", result.stdout) + + def test_library_entry_in_another_library_file_is_rejected(self): + result = self.check_stub(f"# libSceOther technical debt\n\n### Silent stubs\n\n{self.entry}", "docs/dev/debt/libSceOther.md") + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("docs/dev/debt/libSceOther.md:5: error: [debt-file]", result.stdout) + + def test_component_entry_goes_in_the_component_file(self): + result = self.check_stub(f"# shader technical debt\n\n### Functional\n\n- [Example](../../../{self.path}) - gap.\n".replace("libs/prx/libSceExample", "shader"), "docs/dev/debt/shader.md") + self.assertNotIn("[debt-file]", result.stdout + result.stderr) + + def test_entry_goes_with_its_first_core_link(self): + entry = f"- [Recompiler](../../../core/shader/Example.cpp) - needs [a driver feature](../../../{self.path}).\n" + result = self.check_stub(f"# libSceExample technical debt\n\n### Functional\n\n{entry}") + self.assertIn("docs/dev/debt/libSceExample.md:5: error: [debt-file]", result.stdout) + self.assertIn("docs/dev/debt/shader.md", result.stdout) + + def test_entry_without_a_core_link_stays_in_the_index(self): + 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=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) + + def git(*args): + return subprocess.run( + ["git", "-c", f"safe.directory={root}", "-c", "user.name=Conventions Test", + "-c", "user.email=test@example.com", "-c", "commit.gpgsign=false", *args], + cwd=root, capture_output=True, text=True, check=True, + ) + + def write(relative, content): + target = root / relative + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(content, encoding="utf-8") + + git("init", "-q") + write("core/libs/prx/libkernel/src/Time.cpp", "std::uint64_t APS5_VABI sceKernelGetProcessTime() { return now(); }\n") + write("core/libs/prx/libSceNgs2/tests/Render.cpp", "static int APS5_VABI Process(int value) { return value; }\n") + git("add", ".") + git("commit", "-qm", "feat: add libraries") + 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() + if key not in ("GITHUB_EVENT_PATH", "GITHUB_STEP_SUMMARY", "GITHUB_ACTIONS")} + env.update(GIT_CONFIG_COUNT="1", GIT_CONFIG_KEY_0="safe.directory", GIT_CONFIG_VALUE_0=str(root)) + return subprocess.run( + [sys.executable, "-I", str(checker), "--base", "HEAD~1"], + cwd=root, capture_output=True, text=True, env=env, timeout=20, + ) + + def test_mock_in_tests_directory_is_not_an_export(self): + result = self.check("core/libs/prx/libSceAudioOut/tests/Shutdown.cpp", + 'extern "C" std::uint64_t APS5_VABI sceKernelGetProcessTime() { return 0; }\n') + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("All conventions checks passed", result.stdout) + + def test_export_named_like_a_test_fixture_is_not_a_duplicate(self): + result = self.check("core/libs/prx/libSceAjm/src/Port.cpp", "int APS5_VABI Process(int value) { return value + 1; }\n") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("All conventions checks passed", result.stdout) + + def test_export_in_another_library_is_still_a_duplicate(self): + result = self.check("core/libs/prx/libSceAudioOut/src/Clock.cpp", "std::uint64_t APS5_VABI sceKernelGetProcessTime() { return now(); }\n") + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("core/libs/prx/libSceAudioOut/src/Clock.cpp:1: error: [duplicate-export]", result.stdout) + self.assertIn("sceKernelGetProcessTime is also in libkernel", result.stdout) + + 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): + check = Check.__new__(Check) + check.findings = [] + check.pull_request("fix: x", body) + return check.findings + + def findings(self, line): + return self.body_findings(f"### What\nx\n### Tested\nLinux\n### Checklist\n{line}\n- [x] Depends on: none\n- [x] AI-assisted: no\n") + + def test_unchecked_items_with_any_bullet_are_reported(self): + for bullet in ("-", "*", "+"): + with self.subTest(bullet=bullet): + self.assertEqual(self.findings(f"{bullet} [ ] all"), [("pr-template", "", 0, "unchecked: all")]) + + 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), []) + + def test_unchecked_item_after_a_checklist_subsection_is_reported(self): + body = "### What\nx\n### Tested\nLinux\n### Checklist\n- [x] Depends on: none\n- [x] AI-assisted: no\n#### Later\n- [ ] all\n" + self.assertEqual(self.body_findings(body), [("pr-template", "", 0, "unchecked: all")]) + + +class SpacedPathTests(unittest.TestCase): + def test_comment_in_path_with_space_is_reported(self): + 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.txt").write_text("x\n", encoding="utf-8") + git("add", ".") + git("commit", "-qm", "docs: add readme") + source = root / "core/a b.cpp" + source.parent.mkdir(parents=True) + source.write_text("int x; // note\n", encoding="utf-8") + git("add", ".") + git("commit", "-qm", "feat: add spaced file") + result = subprocess.run( + [sys.executable, "-I", str(checker), "--base", "HEAD~1"], + cwd=root, capture_output=True, text=True, + env={key: value for key, value in os.environ.items() + if key not in ("GITHUB_EVENT_PATH", "GITHUB_STEP_SUMMARY", "GITHUB_ACTIONS")}, + ) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("core/a b.cpp:1: error: [comment]", result.stdout) + + +class SystemDependencyTests(unittest.TestCase): + def test_flags_system_lookups(self): + for line in ("find_package( SDL2 REQUIRED)", "FIND_PACKAGE(SDL2)", "find_path(X x.h)", "find_program(P prog)"): + with self.subTest(line=line): + self.assertIsNotNone(SYSTEM_DEPENDENCY.search(line)) + + def test_allows_exempt_lines(self): + for line in ("find_package(Python3 COMPONENTS Interpreter)", "find_package( Threads REQUIRED )", "FIND_PACKAGE(Git)", "add_library(x)"): + with self.subTest(line=line): + self.assertIsNone(SYSTEM_DEPENDENCY.search(line)) + + +class RepositoryChecksTests(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + self.root = Path(self.directory.name) + self.original = Path.cwd() + os.chdir(self.root) + self.git("init", "-q") + (self.root / "README.md").write_text("Project\n", encoding="utf-8") + self.commit() + + def tearDown(self): + os.chdir(self.original) + self.directory.cleanup() + + def git(self, *args): + return subprocess.run(["git", "-c", "user.name=Conventions Test", "-c", + "user.email=test@example.com", "-c", "commit.gpgsign=false", *args], + capture_output=True, text=True, check=True).stdout.strip() + + def commit(self): + self.git("add", ".") + self.git("commit", "-qm", "test: add convention fixture") + + def add_thirdparty_submodule(self, gitmodules=None): + head = self.git("rev-parse", "HEAD") + if gitmodules is not None: + (self.root / ".gitmodules").write_text(gitmodules, encoding="utf-8") + self.git("add", ".gitmodules") + self.git("update-index", "--add", "--cacheinfo", "160000", head, "3rdparty/vendor") + self.git("commit", "-qm", "build: add submodule") + + def test_quoted_utf8_paths_are_checked(self): + name = "docs/dev/測試guide.md" if os.name == "nt" else "docs/dev/測試\tguide.md" + path = self.root / name + path.parent.mkdir(parents=True) + path.write_text("[missing](absent.md)\n", encoding="utf-8") + self.commit() + check = Check("HEAD~1", "HEAD") + check.repository() + check.docs() + self.assertEqual(check.findings, [("doc-link", name, 1, "absent.md")]) + + def test_mixed_patch_types_preserve_file_order(self): + name = "a b.cpp" if os.name == "nt" else "a\nb.cpp" + (self.root / "README.md").unlink() + (self.root / "a.bin").write_bytes(b"binary\x00data") + (self.root / name).write_text("// debt\n", encoding="utf-8") + self.commit() + check = Check("HEAD~1", "HEAD") + check.code() + check.repository() + self.assertIn(("comment", name, 1, ""), check.findings) + self.assertIn(("binary", "a.bin", 0, ""), check.findings) + self.assertNotIn("README.md", check.added) + + def test_increment_line_is_not_a_patch_file_header(self): + path = self.root / "core/example.cpp" + path.parent.mkdir() + path.write_text("void example() {\n++ counter;\n// debt\n}\n", encoding="utf-8") + self.commit() + check = Check("HEAD~1", "HEAD") + 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 = ( + ("core/example.cpp", f"static const char* Metadata = {literals};\n"), + ("core/libs/prx/libSceExample/Export.cpp", f"const char* APS5_VABI sceGetLabel() {{ return {literals}; }}\n"), + ) + for name, definition in fixtures: + with self.subTest(path=name): + path = self.root / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(definition + "int value; // pending\n", encoding="utf-8") + self.commit() + check = Check("HEAD~1", "HEAD") + self.assertEqual([number for number, _ in check.added[name]], [1, 2]) + check.code() + self.assertEqual(check.findings, [("comment", name, 2, "")]) + + def test_checks_from_a_subdirectory_see_the_whole_repository(self): + definition = "int APS5_VABI sceFooBar(int a, int b) { return a + b; }\n" + first = self.root / "core/libs/prx/libSceA/Export.cpp" + first.parent.mkdir(parents=True) + first.write_text(definition, encoding="utf-8") + (self.root / "tools").mkdir() + (self.root / "tools/tool.py").write_text("print()\n", encoding="utf-8") + self.commit() + second = self.root / "core/libs/prx/libSceB/Export.cpp" + second.parent.mkdir(parents=True) + second.write_text(definition, encoding="utf-8") + (self.root / "docs").mkdir() + (self.root / "docs/guide.md").write_text("[export](../core/libs/prx/libSceA/Export.cpp)\n", encoding="utf-8") + self.commit() + os.chdir(self.root / "tools") + check = Check("HEAD~1", "HEAD") + check.code() + check.docs() + 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") + self.git("commit", "-qm", "build: add invalid submodule") + check = Check("HEAD~1", "HEAD") + check.repository() + self.assertIn(("system-dependency", "vendor", 0, "submodule outside 3rdparty/"), check.findings) + + def test_thirdparty_submodule_remains_allowed(self): + self.add_thirdparty_submodule('[submodule "vendor"]\n\tpath = 3rdparty/vendor\n' + "\turl = https://example.com/vendor.git\n") + check = Check("HEAD~1", "HEAD") + check.repository() + self.assertEqual(check.findings, []) + + def test_thirdparty_submodule_without_gitmodules_entry_is_reported(self): + self.add_thirdparty_submodule() + check = Check("HEAD~1", "HEAD") + check.repository() + self.assertEqual(check.findings, + [("system-dependency", "3rdparty/vendor", 0, "submodule without a url in .gitmodules")]) + + def test_dropping_a_submodule_url_is_reported(self): + self.add_thirdparty_submodule('[submodule "vendor"]\n\tpath = 3rdparty/vendor\n' + "\turl = https://example.com/vendor.git\n") + (self.root / ".gitmodules").write_text('[submodule "vendor"]\n\tpath = 3rdparty/vendor\n', encoding="utf-8") + self.git("add", ".gitmodules") + self.git("commit", "-qm", "build: drop submodule url") + check = Check("HEAD~1", "HEAD") + check.repository() + self.assertEqual(check.findings, + [("system-dependency", "3rdparty/vendor", 0, "submodule without a url in .gitmodules")]) + + def test_relative_link_url_decoding(self): + self.assertEqual(links("docs/dev/index.md", "[guide](./space%20name.md#details)"), + [("./space%20name.md", "docs/dev/space name.md")]) + self.assertEqual(links("docs/dev/index.md", "[reference](HTTPS://example.com/x)"), []) + self.assertEqual(links("docs/dev/index.md", "[reference](//example.com/x)"), []) + + def test_percent_encoded_fragment_is_part_of_filename(self): + self.assertEqual(links("docs/dev/index.md", "[guide](./part%23one.md#details)"), + [("./part%23one.md", "docs/dev/part#one.md")]) + + def test_annotation_filename_properties_are_escaped(self): + check = type("Findings", (), {"findings": [("doc-link", "docs/name,with:colon%\n.md", 2, "missing") ]})() + original = os.environ.get("GITHUB_ACTIONS") + os.environ["GITHUB_ACTIONS"] = "true" + try: + with contextlib.redirect_stdout(io.StringIO()) as captured: + output(check, "owner/repo") + finally: + if original is None: + os.environ.pop("GITHUB_ACTIONS", None) + else: + os.environ["GITHUB_ACTIONS"] = original + annotation = captured.getvalue().splitlines()[0] + self.assertIn("file=docs/name%2Cwith%3Acolon%25%0A.md,line=2", annotation) + + 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..0ccd9bf93c --- /dev/null +++ b/tools/tests/test_check_conventions_env_reference.py @@ -0,0 +1,148 @@ +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): + 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") + apply(head) + git("add", "-A") + git("commit", "-qm", "feat: read a new name") + + +def findings_of(root, base, head): + previous = os.getcwd() + os.chdir(root) + try: + check = Check("base", "HEAD") + check.env_reference() + return check.findings + finally: + os.chdir(previous) + + +class EnvReferenceDiffTests(unittest.TestCase): + def run_case(self, base, head): + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = Path(directory.name) + repository(root, base, head) + return findings_of(root, base, head) + + 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")]) + + +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 new file mode 100644 index 0000000000..62c6f9642c --- /dev/null +++ b/tools/tests/test_check_conventions_wrapped_declarations.py @@ -0,0 +1,101 @@ +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +CHECKER = Path(__file__).resolve().parents[1] / "check_conventions.py" +DEFINITION = "int APS5_VABI sceFooBar(int a, int b) { return a + b; }\n" + + +class WrappedDeclarationTests(unittest.TestCase): + def check(self, first_file, first_text, second_text=DEFINITION): + 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, + ) + + git("init", "-q") + first = root / "core/libs/prx/libSceA" / first_file + first.parent.mkdir(parents=True) + first.write_text(first_text, encoding="utf-8") + other = root / "core/libs/prx/libSceA/Other.cpp" + other.write_text("int APS5_VABI sceOther() { return 1; }\n", encoding="utf-8") + git("add", ".") + git("commit", "-qm", "feat: add library a") + second = root / "core/libs/prx/libSceB/Export.cpp" + second.parent.mkdir(parents=True) + second.write_text(second_text, encoding="utf-8") + git("add", ".") + git("commit", "-qm", "feat: add library b") + env = {key: value for key, value in os.environ.items() + 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 test_wrapped_declaration_is_not_a_duplicate(self): + result = self.check("Export.h", "int APS5_VABI sceFooBar(int a,\n int b);\n") + 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) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertNotIn("[duplicate-export]", result.stdout) + + def test_wrapped_definition_is_a_duplicate(self): + result = self.check("Export.cpp", "int APS5_VABI sceFooBar(int a,\n int b) {\n return 0;\n}\n") + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + 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..a4590a60e5 --- /dev/null +++ b/tools/tests/test_check_main_sync.py @@ -0,0 +1,198 @@ +from pathlib import Path +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_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 new file mode 100644 index 0000000000..f4de797f05 --- /dev/null +++ b/tools/tests/test_check_skips.py @@ -0,0 +1,244 @@ +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import check_skips + +TOOL = Path(__file__).resolve().parents[1] / "check_skips.py" + +LIMITATIONS = [ + "skipped, the device lacks fragmentShaderPixelInterlock", + "skipped, the device lacks geometryShader", + "skipped, the device lacks shaderSubgroupClock", + "skipped, the device lacks VK_EXT_shader_stencil_export", + "skipped, the device does not import guest memory", + "skipped, the device imports no host memory", + "skipped, the device flushes f64 subnormals without DenormPreserve", + "skipped, the device binds fewer than 8 storage buffers per stage or set", + "skipped, no usable Vulkan device: ErrorInitializationFailed", + "skipped, unit shadows or host imports are unavailable", + "skipped, the device has no compute queue", + "skipped, the device reports 4 lanes", + "skipped, subgroup size 8", + "skipped, no display", +] + +OTHER = [ + "skipped, flaky", + "skipped, APS5_RAYTRACING replaces the node", + "skipped, interpolation modes require", +] + + +class CheckSkipsTests(unittest.TestCase): + def test_missing_high_refresh_display_is_expected_in_both_jobs(self): + reason = "current display does not provide 120 Hz" + reports = ( + f'skipped, {reason}', + f'', + f'{reason}', + f'skipped, {reason}', + f'\n\n{reason}\n' + f'\n\nskipped, {reason}\n\n' + f'\nskipped, {reason}\n', + f'\n\n{reason}\n\n{reason}\n', + ) + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + for report in reports: + junit.write_text(f'{report}', encoding="utf-8") + for job in ("linux", "windows"): + with self.subTest(job=job, report=report): + result = subprocess.run( + [sys.executable, "-I", str(TOOL), str(junit), "--job", job], + capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("1 test(s) skipped, all expected", result.stdout) + + def test_high_refresh_reason_cannot_hide_additional_diagnostics(self): + reason = "current display does not provide 120 Hz" + known = f"skipped, {reason}" + extra = "unexpected failure detail" + reports = ( + f'{known}\n{extra}', + f'{extra}\n{known}', + f'{known}\n{extra}', + f'{known}{extra}', + f'{extra}{known}', + f'{extra}', + f'{reason}{extra}', + f'{extra}', + f'{known}', + f'{reason}\n{extra}', + f'{known}\n{known}\n{extra}', + ) + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + for report in reports: + junit.write_text(f'{report}', encoding="utf-8") + for job in ("linux", "windows"): + with self.subTest(job=job, report=report): + result = subprocess.run( + [sys.executable, "-I", str(TOOL), str(junit), "--job", job], + capture_output=True, text=True) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("error: guest_video_out_high_frame_rate skipped", result.stdout) + + def test_unknown_display_reasons_stay_unexpected_in_both_jobs(self): + reasons = ( + "current display is broken", + "current display does not provide 120 Hz because the test is flaky", + ) + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + for reason in reasons: + junit.write_text(f'skipped, {reason}', encoding="utf-8") + for job in ("linux", "windows"): + with self.subTest(job=job, reason=reason): + result = subprocess.run( + [sys.executable, "-I", str(TOOL), str(junit), "--job", job], + capture_output=True, text=True) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("error: guest_video_out_high_frame_rate skipped", result.stdout) + + def test_disabled_tests_fail_in_both_jobs_including_agc(self): + xml = '' + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + junit.write_text(xml, 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.assertNotEqual(result.returncode, 0) + self.assertIn("agc_disabled", result.stderr) + self.assertIn("disabled", result.stderr) + + def test_device_limitations_match(self): + for message in LIMITATIONS: + with self.subTest(message=message): + self.assertTrue(check_skips.DEVICE_LIMITATION.search(message)) + + def test_other_reasons_do_not_match(self): + for message in OTHER: + with self.subTest(message=message): + self.assertFalse(check_skips.DEVICE_LIMITATION.search(message)) + + def test_only_unexpected_skip_is_reported(self): + xml = ( + '' + 'skipped, the device lacks geometryShader' + 'skipped, flaky' + '' + ) + 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), "--job", "linux"], capture_output=True, text=True) + self.assertEqual(result.returncode, 1) + 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) + + + + +class SkipReportTests(unittest.TestCase): + def skipped(self, content): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "ctest.xml" + path.write_text(content, encoding="utf-8") + return list(check_skips.skips(path)) + + def test_ctest_disabled_status_is_not_silently_ignored(self): + with self.assertRaisesRegex(ValueError, "disabled.*not run"): + self.skipped('') + + def test_device_message_does_not_excuse_a_disabled_test(self): + with self.assertRaisesRegex(ValueError, "disabled.*not run"): + self.skipped('') + + def test_running_test_without_skipped_marker_is_not_a_skip(self): + self.assertEqual(self.skipped(''), []) + + def test_skip_message_preserves_the_device_reason(self): + found = self.skipped('') + self.assertEqual(len(found), 1) + self.assertRegex(found[0][1], check_skips.DEVICE_LIMITATION) + + def test_stderr_preserves_the_device_reason(self): + found = self.skipped('skipped, no display') + self.assertEqual(len(found), 1) + self.assertRegex(found[0][1], check_skips.DEVICE_LIMITATION) + + def test_unnamed_skipped_test_is_an_error(self): + with self.assertRaises(ValueError): + self.skipped('') + + def test_unexpected_reason_stays_unexpected(self): + found = self.skipped('') + self.assertIsNone(check_skips.DEVICE_LIMITATION.search(found[0][1])) + + def test_ctest_stdout_is_retained(self): + found = self.skipped('skipped, the device reports too few descriptors') + self.assertRegex(found[0][1], check_skips.DEVICE_LIMITATION) + + def test_message_and_element_text_are_both_retained(self): + found = self.skipped('the device has no required extension') + self.assertRegex(found[0][1], check_skips.DEVICE_LIMITATION) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_configure_seed.py b/tools/tests/test_configure_seed.py new file mode 100644 index 0000000000..2cff72cd94 --- /dev/null +++ b/tools/tests/test_configure_seed.py @@ -0,0 +1,226 @@ +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_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 new file mode 100644 index 0000000000..1f2cf31775 --- /dev/null +++ b/tools/tests/test_debt_split.py @@ -0,0 +1,246 @@ +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 debt_split + +TOOL = Path(__file__).resolve().parents[1] / "debt_split.py" +INDEX = "docs/dev/TechnicalDebt.md" + +A_STUB = "- [libSceA](../../core/libs/prx/libSceA/Export.cpp) - `sceAOne` returns 0" +A_FUNCTIONAL = "- `sceATwo` in [libSceA](../../core/libs/prx/libSceA/src/Two.cpp) ignores its flags\n - flag 1 is unknown" +B_STUB = "- [libSceB](../../core/libs/prx/libSceB/Export.cpp) - `sceBOne` returns 0" +C_STUB = "- [libSceC](../../core/libs/prx/libSceC/Export.cpp) - `sceCOne` returns 0" +SHADER = "- [Recompiler](../../core/shader/recompiler/Lower.cpp) rounds once, pinned in [a test](../../core/libs/prx/libSceA/tests/Round.cpp)" +SHARED = "- A limitation of every library, see [architecture](ARCHITECTURE.md)" + + +def index(stubs, functional): + return ("# Project technical debt\n\n### Build\n\n- A toolchain limitation\n\n### Silent stubs\n\n" + "Everything throws, except:\n\n" + "\n".join(stubs) + "\n\n### Functional\n\n" + "\n".join(functional) + "\n") + + +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) + self.git("init", "-q", "-b", "main") + self.write(INDEX, ORIGINAL) + self.git("add", ".") + self.git("commit", "-qm", "docs: record technical debt") + + def tearDown(self): + self.directory.cleanup() + + def git(self, *args, check=True): + return subprocess.run( + ["git", "-c", "user.name=Debt Test", "-c", "user.email=test@example.com", "-c", "commit.gpgsign=false", *args], + cwd=self.root, capture_output=True, text=True, check=check, + env={**os.environ, "GIT_EDITOR": "true"}, + ) + + def write(self, path, text): + (self.root / path).parent.mkdir(parents=True, exist_ok=True) + (self.root / path).write_text(text, encoding="utf-8") + + def read(self, path): + return (self.root / path).read_text(encoding="utf-8") + + def tool(self, command): + return subprocess.run([sys.executable, str(TOOL), command], cwd=self.root, capture_output=True, text=True, timeout=20) + + def split(self): + result = self.tool("split") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + 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"), + "# libSceA technical debt\n\n### Silent stubs\n\n" + A_STUB.replace("../../", "../../../") + + "\n\n### Functional\n\n" + A_FUNCTIONAL.replace("../../", "../../../") + "\n") + self.assertIn(SHADER.replace("../../", "../../../"), self.read("docs/dev/debt/shader.md")) + text = self.read(INDEX) + for entry in (A_STUB, B_STUB, C_STUB, SHADER): + 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() + moved = self.read("docs/dev/debt/libSceA.md") + extra = "- [libSceA](../../core/libs/prx/libSceA/Three.cpp) - `sceAThree` returns 0" + self.write(INDEX, self.read(INDEX).replace("Everything throws, except:\n", "Everything throws, except:\n\n" + extra)) + result = self.tool("split") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual(result.stdout.split(), ["docs/dev/debt/libSceA.md"]) + stub = A_STUB.replace("../../", "../../../") + self.assertEqual(self.read("docs/dev/debt/libSceA.md"), moved.replace(stub, stub + "\n" + extra.replace("../../", "../../../"))) + self.assertEqual(self.read(INDEX).count("[debt](debt/)"), 1) + + def rebase(self, stubs, functional): + self.git("checkout", "-qb", "change") + self.write(INDEX, index(stubs, functional)) + self.git("commit", "-qam", "feat: change debt") + self.git("checkout", "-q", "main") + self.split() + self.git("checkout", "-q", "change") + rebase = self.git("rebase", "main", check=False) + self.assertNotEqual(rebase.returncode, 0, rebase.stdout + rebase.stderr) + return self.tool("resolve") + + def test_resolve_moves_added_edited_and_removed_entries(self): + added = "- [libSceA](../../core/libs/prx/libSceA/Export.cpp) - `sceAFour` returns 0" + edited = B_STUB + " and logs once" + result = self.rebase([A_STUB, added, edited], [A_FUNCTIONAL, SHADER, SHARED]) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + 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`")) + self.assertLess(a.index("`sceAFour`"), a.index("### Functional")) + self.assertIn(edited.replace("../../", "../../../"), self.read("docs/dev/debt/libSceB.md")) + self.assertFalse((self.root / "docs/dev/debt/libSceC.md").exists()) + self.assertEqual(self.read(INDEX), self.git("show", "main:" + INDEX).stdout) + + def test_resolve_moves_unchanged_entry_to_functional(self): + result = self.rebase([A_STUB, B_STUB], [C_STUB, A_FUNCTIONAL, SHADER, SHARED]) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual(self.git("diff", "--name-only", "--diff-filter=U").stdout, "") + self.git("rebase", "--continue") + self.assertEqual(self.read("docs/dev/debt/libSceC.md"), + "# libSceC technical debt\n\n### Functional\n\n" + C_STUB.replace("../../", "../../../") + "\n") + self.assertEqual(result.stdout.split(), ["docs/dev/debt/libSceC.md"]) + self.assertEqual(self.read(INDEX), self.git("show", "main:" + INDEX).stdout) + + def test_resolve_moves_unchanged_entry_to_silent_stubs(self): + result = self.rebase([A_STUB, B_STUB, C_STUB, A_FUNCTIONAL], [SHADER, SHARED]) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual(self.git("diff", "--name-only", "--diff-filter=U").stdout, "") + self.git("rebase", "--continue") + self.assertEqual(self.read("docs/dev/debt/libSceA.md"), + "# libSceA technical debt\n\n### Silent stubs\n\n" + A_STUB.replace("../../", "../../../") + + "\n" + A_FUNCTIONAL.replace("../../", "../../../") + "\n") + self.assertEqual(result.stdout.split(), ["docs/dev/debt/libSceA.md"]) + self.assertEqual(self.read(INDEX), self.git("show", "main:" + INDEX).stdout) + + def test_resolve_refuses_changes_to_lines_that_stay_in_the_index(self): + result = self.rebase([A_STUB, B_STUB, C_STUB], [A_FUNCTIONAL, SHADER, SHARED + " too"]) + self.assertNotEqual(result.returncode, 0) + self.assertIn("resolve those by hand", result.stderr) + self.assertIn("<<<<<<<", self.read(INDEX)) + + def test_resolve_refuses_an_entry_main_changed_too(self): + self.git("checkout", "-qb", "change") + self.write(INDEX, index([A_STUB, B_STUB + " here", C_STUB], [A_FUNCTIONAL, SHADER, SHARED])) + self.git("commit", "-qam", "feat: change b") + self.git("checkout", "-q", "main") + self.split() + path = "docs/dev/debt/libSceB.md" + self.write(path, self.read(path).replace("returns 0", "returns 1")) + self.git("commit", "-qam", "docs: change b") + self.git("checkout", "-q", "change") + self.assertNotEqual(self.git("rebase", "main", check=False).returncode, 0) + result = self.tool("resolve") + self.assertNotEqual(result.returncode, 0) + self.assertIn("entry not found", result.stderr) + + def test_resolve_needs_a_conflict(self): + result = self.tool("resolve") + self.assertNotEqual(result.returncode, 0) + self.assertIn("not in a three-way conflict", result.stderr) + + def test_resolve_preserves_entries_inserted_before_the_first_existing_entry(self): + first = "- [libSceA](../../core/libs/prx/libSceA/First.cpp) - `sceAFirst` returns 0" + second = "- [libSceA](../../core/libs/prx/libSceA/Second.cpp) - `sceASecond` returns 0" + result = self.rebase([first, second, A_STUB, B_STUB, C_STUB], [A_FUNCTIONAL, SHADER, SHARED]) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.git("rebase", "--continue") + self.assertEqual(self.read("docs/dev/debt/libSceA.md"), + "# libSceA technical debt\n\n### Silent stubs\n\n" + + "\n".join(entry.replace("../../", "../../../") for entry in (first, second, A_STUB)) + + "\n\n### Functional\n\n" + A_FUNCTIONAL.replace("../../", "../../../") + "\n") + self.assertEqual(self.read(INDEX), self.git("show", "main:" + INDEX).stdout) + + 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 2242cae3cd..a29775c13b 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -1,7 +1,10 @@ import contextlib import io import json +import os +import stat import struct +import subprocess import sys import tempfile import unittest @@ -20,23 +23,80 @@ MESSAGE_NID = "wTpfglkmv34" MESSAGE_NAME = "sceMsgDialogProgressBarSetValue" -def build_elf(symbols): +def build_elf(symbols, needed=()): strings = b"\0" entries = [struct.pack(" 2: + record["weak"] = entry[2] + records.append(record) + path.write_text(json.dumps(records), encoding=encoding) class Workspace: @@ -87,26 +198,289 @@ 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")) + registry = work.root / "game.json" + write_registry(registry, [("AAAAAAAAAAA", "libSceA.prx")]) + output = work.root / "audit.json" + previous = b'{"previous": "complete report"}\n' + + def disk_full(path, content, **kwargs): + with path.open("w", encoding="utf-8") as handle: + handle.write(content[:24]) + raise OSError(28, "No space left on device", str(path)) + + for exists in (True, False): + with self.subTest(existing_report=exists): + if exists: + output.write_bytes(previous) + else: + output.unlink(missing_ok=True) + files = set(work.root.iterdir()) + with patch.object(Path, "write_text", disk_full): + code, out, err = work.run(registry, "--json", str(output)) + self.assertEqual((code, out), (2, "")) + self.assertIn("No space left on device", err) + self.assertEqual(set(work.root.iterdir()), files) + if exists: + self.assertEqual(output.read_bytes(), previous) + else: + self.assertFalse(output.exists()) + + def test_failed_json_replacement_preserves_report_and_cleans_staging(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) + registry = work.root / "game.json" + write_registry(registry, [("AAAAAAAAAAA", "libSceA.prx")]) + output = work.root / "audit.json" + previous = b'{"previous": "complete report"}\n' + output.write_bytes(previous) + files = set(work.root.iterdir()) + with patch.object(Path, "replace", side_effect=PermissionError(13, "Permission denied", str(output))): + code, out, err = work.run(registry, "--json", str(output)) + self.assertEqual((code, out), (2, "")) + self.assertIn("Permission denied", err) + self.assertEqual(output.read_bytes(), previous) + self.assertEqual(set(work.root.iterdir()), files) + + def test_successful_json_write_replaces_complete_report_without_staging_files(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) + registry = work.root / "game.json" + write_registry(registry, [("AAAAAAAAAAA", "libSceA.prx")]) + output = work.root / "audit.json" + output.write_text("old report", encoding="utf-8") + files = set(work.root.iterdir()) + code, _, err = work.run(registry, "--json", str(output)) + self.assertEqual((code, err), (0, "")) + self.assertEqual(json.loads(output.read_text(encoding="utf-8"))["unique_by_class"]["implemented"], 1) + self.assertEqual(set(work.root.iterdir()), files) + + @unittest.skipUnless(sys.platform == "win32", "Windows read-only replacement behavior") + def test_read_only_json_report_stays_intact_without_a_read_only_temporary(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) + registry = work.root / "game.json" + write_registry(registry, [("AAAAAAAAAAA", "libSceA.prx")]) + output = work.root / "audit.json" + previous = b'{"previous": "complete report"}\n' + output.write_bytes(previous) + files = set(work.root.iterdir()) + output.chmod(0o444) + try: + code, out, err = work.run(registry, "--json", str(output)) + self.assertEqual((code, out), (2, "")) + self.assertIn("FAIL:", err) + self.assertEqual(output.read_bytes(), previous) + self.assertEqual(set(work.root.iterdir()), files) + finally: + output.chmod(0o666) + + def test_json_report_symlink_keeps_its_target(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) + registry = work.root / "game.json" + write_registry(registry, [("AAAAAAAAAAA", "libSceA.prx")]) + target = work.root / "actual-report.json" + target.write_text("old report", encoding="utf-8") + output = work.root / "audit.json" + try: + output.symlink_to(target.name) + except (OSError, NotImplementedError) as error: + self.skipTest(str(error)) + files = set(work.root.iterdir()) + code, _, err = work.run(registry, "--json", str(output)) + self.assertEqual((code, err), (0, "")) + self.assertTrue(output.is_symlink()) + self.assertEqual(json.loads(target.read_text(encoding="utf-8"))["unique_imports"], 1) + self.assertEqual(set(work.root.iterdir()), files) + + def test_json_directory_error_preserves_the_supplied_path(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) + registry = work.root / "game.json" + write_registry(registry, [("AAAAAAAAAAA", "libSceA.prx")]) + output = work.modules / ".." / work.modules.name + self.assertNotEqual(str(output), str(output.resolve())) + files = set(work.root.iterdir()) + code, out, err = work.run(registry, "--json", str(output)) + self.assertEqual((code, out), (2, "")) + self.assertEqual(err, f"FAIL: {output}: JSON output is a directory\n") + self.assertEqual(set(work.root.iterdir()), files) + + @unittest.skipUnless(os.name == "posix", "POSIX report permissions") + def test_new_json_report_permissions_follow_the_process_umask(self): + script = "\n".join([ + "import os, sys", + "from pathlib import Path", + "sys.path.insert(0, sys.argv[1])", + "import import_audit", + "os.umask(int(sys.argv[4], 8))", + "Path(sys.argv[3]).write_text('{}\\n', encoding='utf-8')", + "import_audit.write_json(Path(sys.argv[2]), {'complete': True})", + ]) + for mask, mode in ((0o022, 0o644), (0o002, 0o664), (0o077, 0o600)): + with self.subTest(umask=oct(mask)): + output = self.work.root / f"audit-{mask:o}.json" + reference = self.work.root / f"plain-{mask:o}.json" + files = set(self.work.root.iterdir()) + completed = subprocess.run([sys.executable, "-c", script, str(import_audit.ROOT / "tools"), str(output), str(reference), oct(mask)], capture_output=True, text=True) + self.assertEqual(completed.returncode, 0, completed.stderr) + self.assertEqual(stat.S_IMODE(output.stat().st_mode), mode) + self.assertEqual(stat.S_IMODE(output.stat().st_mode), stat.S_IMODE(reference.stat().st_mode)) + self.assertEqual(json.loads(output.read_text(encoding="utf-8")), {"complete": True}) + self.assertEqual(set(self.work.root.iterdir()), files | {output, reference}) + + @unittest.skipUnless(os.name == "posix", "POSIX report permissions") + def test_existing_json_report_permissions_are_preserved(self): + output = self.work.root / "audit.json" + for mode in (0o600, 0o640, 0o664): + with self.subTest(mode=oct(mode)): + output.write_text("old report", encoding="utf-8") + output.chmod(mode) + files = set(self.work.root.iterdir()) + import_audit.write_json(output, {"complete": True}) + self.assertEqual(stat.S_IMODE(output.stat().st_mode), mode) + self.assertEqual(json.loads(output.read_text(encoding="utf-8")), {"complete": True}) + self.assertEqual(set(self.work.root.iterdir()), files) + def test_classification_accounts_for_every_reference(self): work = self.work (work.libs / "libSceMsgDialog.prx").write_bytes(build_elf([ (MESSAGE_NID, 1, GLOBAL_FUNCTION), ("AAAAAAAAAAA", 1, GLOBAL_FUNCTION), ("BBBBBBBBBBB", 1, GLOBAL_FUNCTION), ("UNDEFINED11", 0, GLOBAL_FUNCTION), ("LOCALLOCAL1", 1, LOCAL_FUNCTION)])) (work.libs / "libc.prx").write_bytes(exporting("CCCCCCCCCCC")) - (work.modules / "libSceNpCppWebApi.prx").write_bytes(b"\x7fELF") + (work.modules / "libSceNpCppWebApi.prx").write_bytes(exporting()) write_source(work.source, "libSceMsgDialog", STUB_SOURCE.format(name=MESSAGE_NAME) + 'APS5_EXPORT("BBBBBBBBBBB", sceFooUnknown_B);\n' + STUB_SOURCE.format(name="sceFooUnknown_B")) registry = work.root / "game.registry.json" write_registry(registry, [ @@ -134,6 +508,468 @@ class ImportAuditTests(unittest.TestCase): self.assertIn(f"{MESSAGE_NID} {MESSAGE_NAME}", out) self.assertIn("DDDDDDDDDDD", out) + def test_module_imports_are_audited(self): + work = self.work + stub = nid_names.compute_nid("sceModuleStub") + (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) + (work.libs / "libSceB.prx").write_bytes(exporting(stub)) + write_source(work.source, "libSceB", STUB_SOURCE.format(name="sceModuleStub")) + (work.modules / "sibling.prx").write_bytes(exporting("BBBBBBBBBBB")) + (work.modules / "main.prx").write_bytes(build_elf([ + ("AAAAAAAAAAA", 0, GLOBAL_FUNCTION), ("BBBBBBBBBBB", 0, GLOBAL_FUNCTION), + (stub, 0, GLOBAL_FUNCTION), ("EEEEEEEEEEE", 0, GLOBAL_FUNCTION)], ["libSceA.prx", "sibling.prx"])) + 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["references"], 4) + self.assertEqual(data["unique_by_class"], {"implemented": 2, "stub": 1, "absent": 1, "module": 0}) + self.assertEqual(data["missing_libraries"], {}) + records = {(record["nid"], record["module"]): record for record in data["imports"]} + self.assertEqual(records[("AAAAAAAAAAA", "main.prx")]["class"], "implemented") + self.assertEqual(records[("AAAAAAAAAAA", "main.prx")]["providers"], ["libSceA.prx"]) + self.assertEqual(records[("BBBBBBBBBBB", "main.prx")]["class"], "implemented") + self.assertEqual(records[("BBBBBBBBBBB", "main.prx")]["providers"], ["sibling.prx"]) + self.assertEqual(records[(stub, "main.prx")]["class"], "stub") + self.assertEqual(records[(stub, "main.prx")]["name"], "sceModuleStub") + self.assertEqual(records[("EEEEEEEEEEE", "main.prx")]["class"], "absent") + 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()) + (work.modules / "main.prx").write_bytes(build_elf([], ["libSceMissing.prx"])) + 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["references"], data["imports"]), (0, [])) + 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()) + (work.modules / "guest.prx").write_bytes(build_pe(["alpha"])) + registry = work.root / "game.json" + write_registry(registry, []) + code, out, err = work.run(registry) + self.assertEqual((code, out), (2, "")) + self.assertIn("guest.prx", err) + self.assertIn("ELF", err) + + def test_sectionless_module_imports_and_exports_are_audited(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) + (work.modules / "stripped.prx").write_bytes(build_sectionless_elf([ + ("AAAAAAAAAAA", 0, GLOBAL_FUNCTION), ("BBBBBBBBBBB", 1, GLOBAL_FUNCTION)], ["libSceA.prx"])) + (work.modules / "main.prx").write_bytes(build_elf([("BBBBBBBBBBB", 0, GLOBAL_FUNCTION)], [])) + 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), (0, "")) + data = json.loads(result.read_text()) + self.assertEqual(data["references"], 2) + records = {(record["nid"], record["module"]): record for record in data["imports"]} + 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")) + section_offset = struct.unpack_from("\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.object(sys, "argv", ["pr_labels.py", "--all"]), patch.dict(pr_labels.os.environ, {"GITHUB_REPOSITORY": "o/r"}): + with self.assertRaises(SystemExit) as raised: + pr_labels.main() + self.assertEqual(edited, ["1", "2"]) + self.assertIn("#1", str(raised.exception.code)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_pr_overlap.py b/tools/tests/test_pr_overlap.py index 1670876dc2..520620c8a3 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 @@ -19,8 +25,7 @@ class OpenPullRequestsTests(unittest.TestCase): def response(*args): if args[0] == "api": - return json.dumps([pull_requests[start:start + 100] - for start in range(0, len(pull_requests), 100)]) + return json.dumps(pull_requests) limit = int(args[args.index("-L") + 1]) return json.dumps([ {"number": pr["number"], "author": pr["user"], "headRefOid": pr["head"]["sha"], @@ -38,23 +43,331 @@ class OpenPullRequestsTests(unittest.TestCase): def test_base_branch_is_encoded_in_the_paginated_request(self): with patch.object(pr_overlap, "REPO", "owner/project", create=True), \ - patch.object(pr_overlap, "gh", return_value="[[]]") as gh: + patch.object(pr_overlap, "gh", return_value="[]") as gh: self.assertEqual(pr_overlap.open_prs("topic/one&two"), {}) gh.assert_called_once_with( - "api", "--paginate", "--slurp", + "api", "--paginate", "repos/owner/project/pulls?state=open&base=topic%2Fone%26two&per_page=100", ) def test_single_page_preserves_author_head_and_dependencies(self): - pages = [[{"number": 7, "user": {"login": "contributor"}, "head": {"sha": "head-sha"}, - "body": "Depends on: none\nUnrelated issue #42"}]] + pull_requests = [{"number": 7, "user": {"login": "contributor"}, "head": {"sha": "head-sha"}, + "body": "Depends on: none\nUnrelated issue #42"}] with patch.object(pr_overlap, "REPO", "owner/project", create=True), \ - patch.object(pr_overlap, "gh", return_value=json.dumps(pages)): + patch.object(pr_overlap, "gh", return_value=json.dumps(pull_requests)): found = pr_overlap.open_prs("main") self.assertEqual(found, { 7: {"ref": "refs/pr/7", "sha": "head-sha", "author": "contributor", "depends": set()}, }) + def test_collects_concatenated_json_pages_from_paginated_gh_output(self): + pull_requests = [ + {"number": number, "user": {"login": f"author{number}"}, + "head": {"sha": f"sha{number}"}, "body": None} + for number in range(1, 4) + ] + pages = [pull_requests[:2], pull_requests[2:]] + output = "\n".join(json.dumps(page) for page in pages) + + with patch.object(pr_overlap, "REPO", "owner/project", create=True), \ + patch.object(pr_overlap, "gh", return_value=output): + found = pr_overlap.open_prs("main") + + self.assertEqual(set(found), {1, 2, 3}) + self.assertEqual(found[3]["sha"], "sha3") + + def test_rejects_empty_paginated_output(self): + with patch.object(pr_overlap, "REPO", "owner/project", create=True), \ + patch.object(pr_overlap, "gh", return_value=" \n"): + with self.assertRaisesRegex(ValueError, "returned no JSON pages"): + 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 = { + 7: {"ref": "refs/pr/7", "sha": "a" * 40, "author": "one", "depends": set()}, + 9: {"ref": "refs/pr/9", "sha": "b" * 40, "author": "two", "depends": set()}, + } + with patch.object(pr_overlap, "REPO", "owner/project", create=True), \ + patch.object(pr_overlap, "git", return_value="") as git: + pr_overlap.fetch("main", prs) + git.assert_called_once_with( + "fetch", "--no-tags", "-q", "https://github.com/owner/project.git", + "+refs/heads/main:refs/pr/base", f"+{'a' * 40}:refs/pr/7", f"+{'b' * 40}:refs/pr/9", + ) + + def test_batches_large_sets_of_pull_request_heads(self): + prs = { + number: { + "ref": f"refs/pr/{number}", "sha": f"{number:040x}", + "author": "contributor", "depends": set(), + } + for number in range(250) + } + with patch.object(pr_overlap, "REPO", "owner/project", create=True), \ + patch.object(pr_overlap, "git", return_value="") as git: + pr_overlap.fetch("main", prs) + self.assertEqual(git.call_count, 3) + self.assertEqual([len(call.args) - 4 for call in git.call_args_list], [100, 100, 51]) + self.assertEqual(git.call_args_list[0].args[4], "+refs/heads/main:refs/pr/base") + self.assertEqual(git.call_args_list[-1].args[-1], "+00000000000000000000000000000000000000f9:refs/pr/249") + + +class ConflictTests(unittest.TestCase): + def test_uses_the_shared_base_parent_without_newer_merge_tree_option(self): + first = {"tree": "first-tree", "files": {"shared.cpp"}, "added": set()} + second = {"tree": "second-tree", "files": {"shared.cpp"}, "added": set()} + with patch.object(pr_overlap, "merge", return_value=("merged-tree", set())) as merge: + 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 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 new file mode 100644 index 0000000000..11754c81b7 --- /dev/null +++ b/tools/tests/test_pr_overlap_dependencies.py @@ -0,0 +1,147 @@ +import json +from pathlib import Path +import sys +import unittest +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import pr_overlap + + +def pull_requests(body): + response = [{"number": number, "user": {"login": "contributor"}, "head": {"sha": str(number) * 40}, + "body": body if number == 1 else "Depends on: none"} for number in (1, 2)] + with patch.object(pr_overlap, "REPO", "owner/repo", create=True), \ + patch.object(pr_overlap, "gh", return_value=json.dumps(response)): + return pr_overlap.open_prs("main") + + +STACK = { + 1: (set(), {"one.cpp"}, {"sceFooInit"}), + 2: ({1, 999}, {"one.cpp", "two.cpp"}, {"sceFooInit"}), + 3: ({2}, {"one.cpp", "two.cpp", "three.cpp"}, {"sceFooInit"}), + 4: ({1}, {"one.cpp", "four.cpp"}, {"sceFooInit", "sceFooTerm"}), + 5: ({1}, {"one.cpp", "four.cpp"}, {"sceFooInit", "sceFooTerm"}), + 6: (set(), {"one.cpp"}, {"sceFooInit"}), + 7: ({8}, {"one.cpp", "seven.cpp"}, {"sceFooInit"}), + 8: ({7, 1}, {"one.cpp", "seven.cpp"}, {"sceFooInit"}), +} + + +def stacked_pull_requests(): + return {number: {"author": f"author{number}", "depends": depends, "added": added, "exports": exports, "tree": None} + for number, (depends, added, exports) in STACK.items()} + + +class DependencyTests(unittest.TestCase): + def test_documented_form_without_colon(self): + for body in ("Depends on #2.", "Depends on #2, #3.", "Depends on\t#2 and #3.", + "Depends on #2, and #3.", "**Depends on #2.**", "- [x] Depends on #2"): + with self.subTest(body=body): + expected = {2, 3} if "#3" in body else {2} + self.assertEqual(pull_requests(body)[1]["depends"], expected) + + def test_colon_forms_are_preserved(self): + for body in ("Depends on:#2, #3", "- [x] Depends on: #2, #3", "Depends on:\n- #2\n- #3", + "- [x] Depends on:\r\n * #2\r\n * #3"): + with self.subTest(body=body): + self.assertEqual(pull_requests(body)[1]["depends"], {2, 3}) + + def test_later_references_are_not_dependencies(self): + for newline in ("\n", "\r\n"): + for declaration in ("Depends on #2.", "Depends on: #2"): + body = newline.join((declaration, "- #3", "", "Related work: #4.", "- #5")) + with self.subTest(body=body): + expected = {2, 3} if ":" in declaration else {2} + self.assertEqual(pull_requests(body)[1]["depends"], expected) + + def test_no_dependencies(self): + for body in (None, "", "Depends on: none", "Depends on: ", + "Related to #2.", "Depends on compiler support; see #2.", + "Depends on\n#2", "Depends on #2suffix"): + with self.subTest(body=body): + self.assertEqual(pull_requests(body)[1]["depends"], set()) + + def test_plain_declaration_stops_before_unrelated_prose(self): + for body in ("Depends on #2. Related #3 covers something else.", + "Depends on #2; related #3 covers something else.", + "Depends on #2 and compiler support from #3.", + "Depends on #2, related work is in #3.", + "**Depends on #2**. Refs #3."): + with self.subTest(body=body): + self.assertEqual(pull_requests(body)[1]["depends"], {2}) + + def test_colon_declaration_stops_before_unrelated_prose(self): + for body in ("Depends on: #2. Related #3 covers something else.", + "Depends on: #2; related #3 covers something else.", + "Depends on: none; see issue #3.", + "Depends on: #2, related work is in #3."): + with self.subTest(body=body): + expected = set() if "none" in body else {2} + self.assertEqual(pull_requests(body)[1]["depends"], expected) + + def test_checklist_declaration_keeps_precedence(self): + for declaration, expected in (("#3", {3}), ("none", set())): + body = "Previously: Depends on #2.\n\n- [x] Depends on: " + declaration + with self.subTest(body=body): + self.assertEqual(pull_requests(body)[1]["depends"], expected) + + def test_template_placeholder_does_not_hide_prose(self): + placeholder = "- [ ] Depends on: " + for prose, expected in (("Adds X. Depends on #2.", {2}), ("Adds X.", set()), ("Depends on #2, #3.", {2, 3})): + for newline in ("\n", "\r\n"): + body = newline.join((prose, placeholder)) + 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()) + + def test_documented_dependency_suppresses_overlap(self): + prs = pull_requests("Depends on #2.") + for pr in prs.values(): + pr["added"] = {"shared.cpp"} + self.assertIsNone(pr_overlap.overlaps(prs, 1, 2)) + self.assertIsNone(pr_overlap.overlaps(prs, 2, 1)) + + def test_unrelated_reference_keeps_overlap(self): + prs = pull_requests("Related to #2.") + for pr in prs.values(): + pr["added"] = {"shared.cpp"} + self.assertEqual(pr_overlap.overlaps(prs, 1, 2), {"added": {"shared.cpp"}}) + + +class StackedDependencyTests(unittest.TestCase): + def test_transitive_dependency_suppresses_overlap(self): + prs = stacked_pull_requests() + self.assertIsNone(pr_overlap.overlaps(prs, 1, 3)) + self.assertIsNone(pr_overlap.overlaps(prs, 3, 1)) + + def test_changes_inherited_from_a_shared_dependency_are_not_overlap(self): + prs = stacked_pull_requests() + self.assertIsNone(pr_overlap.overlaps(prs, 3, 4)) + self.assertIsNone(pr_overlap.overlaps(prs, 2, 4)) + + def test_overlap_beyond_a_shared_dependency_is_kept(self): + self.assertEqual(pr_overlap.overlaps(stacked_pull_requests(), 4, 5), + {"added": {"four.cpp"}, "conflicts": {}, "exports": {"sceFooTerm"}}) + + def test_independent_pull_request_keeps_inherited_overlap(self): + self.assertEqual(pr_overlap.overlaps(stacked_pull_requests(), 3, 6), + {"added": {"one.cpp"}, "conflicts": {}, "exports": {"sceFooInit"}}) + + def test_dependency_cycle_terminates(self): + prs = stacked_pull_requests() + self.assertIsNone(pr_overlap.overlaps(prs, 7, 8)) + self.assertIsNone(pr_overlap.overlaps(prs, 8, 2)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_pr_overlap_encoding.py b/tools/tests/test_pr_overlap_encoding.py index 2272bcc13a..1dad26c746 100644 --- a/tools/tests/test_pr_overlap_encoding.py +++ b/tools/tests/test_pr_overlap_encoding.py @@ -1,8 +1,11 @@ from contextlib import contextmanager +import io import json +import os from pathlib import Path import subprocess import sys +from tempfile import TemporaryDirectory import unittest from unittest.mock import patch @@ -14,6 +17,23 @@ UNICODE = "Quoted \u201cmetadata\u201d, Y\u014dtei and \U0001f3ae" TREE = "a" * 40 +@contextmanager +def simulated_cp1252(): + if hasattr(subprocess, "_text_encoding"): + with patch.object(subprocess, "_text_encoding", return_value="cp1252"): + yield + else: + wrapper = io.TextIOWrapper + + def fake_wrapper(buffer, encoding=None, *args, **kwargs): + if encoding in (None, "locale"): + encoding = "cp1252" + return wrapper(buffer, encoding=encoding, *args, **kwargs) + + with patch.object(io, "TextIOWrapper", new=fake_wrapper): + yield + + @contextmanager def command_output(stdout, stderr="", returncode=0): code = ("import os, sys\n" @@ -24,7 +44,7 @@ def command_output(stdout, stderr="", returncode=0): def run(command, **kwargs): return RUN([sys.executable, "-c", code], **kwargs) - with patch.object(subprocess, "_text_encoding", return_value="cp1252"), \ + with simulated_cp1252(), \ patch.object(subprocess, "run", side_effect=run): yield @@ -61,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): @@ -73,6 +93,47 @@ class EncodingTests(unittest.TestCase): with self.assertRaisesRegex(RuntimeError, UNICODE): pr_overlap.merge("main", "topic") + def test_default_encoding_uses_simulated_cp1252(self): + code = "import os;os.write(1," + repr(UNICODE.encode("utf-8")) + ")" + with simulated_cp1252(): + default = RUN([sys.executable, "-c", code], capture_output=True, text=True, errors="replace") + self.assertEqual(default.stdout, UNICODE.encode("utf-8").decode("cp1252", errors="replace")) + explicit = RUN([sys.executable, "-c", code], capture_output=True, text=True, encoding="utf-8") + self.assertEqual(explicit.stdout, UNICODE) + + def test_git_diff_survives_invalid_utf8(self): + with TemporaryDirectory() as directory: + repo = Path(directory) + source = repo / "core" / "libs" / "prx" / "x" / "a.cpp" + source.parent.mkdir(parents=True) + ident = ["-c", "user.name=t", "-c", "user.email=t@example.test"] + for args in (["init", "-q"], ["commit", "-q", "--allow-empty", "-m", "base"]): + subprocess.run(["git", *ident, *args], cwd=repo, check=True, capture_output=True) + source.write_bytes(b"int APS5_VABI f() {} // caf\xe9\n") + for args in (["add", "."], ["commit", "-q", "-m", "add"]): + subprocess.run(["git", *ident, *args], cwd=repo, check=True, capture_output=True) + old = os.getcwd() + os.chdir(repo) + try: + diff = pr_overlap.git("diff", "-U0", "--no-color", "HEAD~1", "HEAD", "--", "core/libs/prx/*.cpp") + finally: + os.chdir(old) + self.assertEqual(pr_overlap.exports(diff), {"f"}) + + def test_report_is_written_as_utf8(self): + with TemporaryDirectory() as directory: + path = Path(directory) / "1.md" + write_text = Path.write_text + + def write_with_utf8(path, text, *args, **kwargs): + self.assertEqual(kwargs.get("encoding"), "utf-8") + return write_text(path, text, *args, **kwargs) + + with patch.object(Path, "write_text", new=write_with_utf8), \ + patch.object(pr_overlap, "report", return_value=UNICODE): + pr_overlap.write_reports(Path(directory), {1: {}}, "https://example.test") + self.assertEqual(path.read_bytes(), UNICODE.encode("utf-8")) + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_pr_overlap_errors.py b/tools/tests/test_pr_overlap_errors.py new file mode 100644 index 0000000000..c5bb5e0b8e --- /dev/null +++ b/tools/tests/test_pr_overlap_errors.py @@ -0,0 +1,41 @@ +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)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_pr_overlap_exports.py b/tools/tests/test_pr_overlap_exports.py index eb2e18db2f..d326656d8b 100644 --- a/tools/tests/test_pr_overlap_exports.py +++ b/tools/tests/test_pr_overlap_exports.py @@ -25,7 +25,20 @@ 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' + self.assertEqual(pr_overlap.exports(diff), pr_overlap.exports(DIFF)) def test_declarations_are_not_exports(self): self.assertTrue(TRAILING.endswith("; \n")) diff --git a/tools/tests/test_pr_overlap_own_branch.py b/tools/tests/test_pr_overlap_own_branch.py new file mode 100644 index 0000000000..802d57c74b --- /dev/null +++ b/tools/tests/test_pr_overlap_own_branch.py @@ -0,0 +1,51 @@ +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 pr_overlap + + +def run(*args): + return subprocess.run(["git", "-c", "user.name=t", "-c", "user.email=t@t", *args], + capture_output=True, text=True, check=True).stdout.strip() + + +class OwnBranchTests(unittest.TestCase): + def test_drops_pull_requests_already_contained_in_the_branch(self): + old = os.getcwd() + with tempfile.TemporaryDirectory() as tmp: + os.chdir(tmp) + try: + run("init", "-q") + run("commit", "-q", "--allow-empty", "-m", "a") + a = run("rev-parse", "HEAD") + run("commit", "-q", "--allow-empty", "-m", "b") + b = run("rev-parse", "HEAD") + run("checkout", "-q", a) + run("commit", "-q", "--allow-empty", "-m", "c") + c = run("rev-parse", "HEAD") + prs = {7: {"sha": a}, 8: {"sha": b}, 9: {"sha": c}} + self.assertEqual(set(pr_overlap.own(prs, b)), {9}) + 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_pr_overlap_wrapped_exports.py b/tools/tests/test_pr_overlap_wrapped_exports.py new file mode 100644 index 0000000000..96dbd22320 --- /dev/null +++ b/tools/tests/test_pr_overlap_wrapped_exports.py @@ -0,0 +1,28 @@ +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import pr_overlap + +DECL = "+int APS5_VABI sceFooBar(int a,\n+ int b);\n" + + +class WrappedExportTests(unittest.TestCase): + def test_wrapped_declaration_is_not_export(self): + self.assertEqual(pr_overlap.exports(DECL), set()) + + def test_wrapped_definition_is_export(self): + diff = "+int APS5_VABI sceFooBar(int a,\n+ int b) {\n+ return a + b;\n+}\n" + self.assertEqual(pr_overlap.exports(diff), {"sceFooBar"}) + + def test_declaration_then_definition_returns_only_definition(self): + diff = DECL + "+int APS5_VABI sceBaz(int a) { return a; }\n" + self.assertEqual(pr_overlap.exports(diff), {"sceBaz"}) + + def test_declaration_at_end_of_diff(self): + self.assertEqual(pr_overlap.exports("+int APS5_VABI sceFooBar(int a, int b);"), set()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_progress.py b/tools/tests/test_progress.py index 144a8d9987..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,7 +10,191 @@ 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' + source += 'int APS5_VABI Real() { return 7; }\n' + with tempfile.TemporaryDirectory() as directory: + library = Path(directory) / "libSceExample" + library.mkdir() + (library / "Export.cpp").write_text(source, encoding="utf-8") + group = progress.scan_library(library) + self.assertEqual(group["done_names"], ["Real"]) + self.assertEqual(group["todo_names"], []) + + def test_stub_marker_inside_raw_literals_does_not_mark_implementation_as_stub(self): + for prefix in ("", "u8", "u", "U", "L"): + source = ('int APS5_VABI Ready(int value) {\n' + 'auto message = ' + prefix + 'R"help(\n"quoted" NotImplemented_nid_no_patch(); } {\n)help";\n' + 'return value + 1;\n}\n') + with self.subTest(prefix=prefix), tempfile.TemporaryDirectory() as directory: + library = Path(directory) / "libSceExample" + library.mkdir() + (library / "Export.cpp").write_text(source, encoding="utf-8") + group = progress.scan_library(library) + self.assertEqual(group["done_names"], ["Ready"]) + self.assertEqual(group["todo_names"], []) + + def test_nonvoid_stub_wrappers_keep_callers_unimplemented(self): + with tempfile.TemporaryDirectory() as directory: + library = Path(directory) / "libSceExample" + library.mkdir() + (library / "Export.cpp").write_text( + "static int SupportFormat() { NotImplemented_nid_no_patch(__func__); return 0; }\n" + "int APS5_VABI sceExampleSupportFormat() { return SupportFormat(); }\n") + group = progress.scan_library(library) + self.assertEqual(group["done_names"], []) + self.assertEqual(group["todo_names"], ["sceExampleSupportFormat"]) + + def test_stub_wrapper_calls_allow_whitespace(self): + for separator in ("", " ", "\t", "\n", " /* separator */ "): + with self.subTest(separator=separator), tempfile.TemporaryDirectory() as directory: + library = Path(directory) / "libSceExample" + library.mkdir() + (library / "Export.cpp").write_text( + "static int Unsupported() { NotImplemented_nid_no_patch(__func__); return 0; }\n" + f"int APS5_VABI Pending() {{ return Unsupported{separator}(); }}\n") + group = progress.scan_library(library) + self.assertEqual(group["done_names"], []) + self.assertEqual(group["todo_names"], ["Pending"]) + + def test_stub_wrapper_names_match_whole_identifiers(self): + with tempfile.TemporaryDirectory() as directory: + library = Path(directory) / "libSceExample" + library.mkdir() + (library / "Export.cpp").write_text( + "static int Unsupported() { NotImplemented_nid_no_patch(__func__); return 0; }\n" + "static int NotUnsupported(int value) { return value * 2; }\n" + "int APS5_VABI Ready(int value) { return NotUnsupported(value); }\n") + group = progress.scan_library(library) + self.assertEqual(group["done_names"], ["Ready"]) + self.assertEqual(group["todo_names"], []) + + def test_braces_in_literals_and_comments_do_not_delimit_bodies(self): + with tempfile.TemporaryDirectory() as directory: + library = Path(directory) / "libSceExample" + library.mkdir() + (library / "Export.cpp").write_text( + "static int Helper() { const char* brace = \"}\"; NotImplemented_nid_no_patch(__func__); return 0; }\n" + "int APS5_VABI sceExampleOpen(char* out) { *out = '{'; return 0; }\n" + "int APS5_VABI sceExampleClose(char* out) { *out = '}'; NotImplemented_nid_no_patch(__func__); return 0; }\n" + "int APS5_VABI sceExampleQuote(std::string& out) { out += \"{\\\"\"; return 0; }\n" + "int APS5_VABI sceExampleNoted() { // }\n NotImplemented_nid_no_patch(__func__); return 0; }\n" + "int APS5_VABI sceExampleBlock() { /* { */ return 0; }\n" + "int APS5_VABI sceExampleWrapped() { return Helper(); }\n" + "int APS5_VABI sceExampleReady() { return 0; }\n") + group = progress.scan_library(library) + self.assertEqual(group["done_names"], ["sceExampleBlock", "sceExampleOpen", "sceExampleQuote", "sceExampleReady"]) + self.assertEqual(group["todo_names"], ["sceExampleClose", "sceExampleNoted", "sceExampleWrapped"]) + + def test_apostrophe_in_a_comment_does_not_pair_with_a_later_literal(self): + with tempfile.TemporaryDirectory() as directory: + library = Path(directory) / "libSceExample" + library.mkdir() + (library / "Export.cpp").write_text( + "int APS5_VABI sceExamplePending() {\n /* it's */ log('}');\n" + " NotImplemented_nid_no_patch(__func__);\n return 0;\n}\n" + "int APS5_VABI sceExampleReady() { return 0; }\n") + group = progress.scan_library(library) + self.assertEqual(group["done_names"], ["sceExampleReady"]) + self.assertEqual(group["todo_names"], ["sceExamplePending"]) + def test_test_sources_do_not_change_library_progress(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) / "tests" / "prx" @@ -81,6 +267,250 @@ class ProgressTests(unittest.TestCase): self.assertIn("-1 removed", report) self.assertIn("Pending", report) + def test_inert_bodies_are_marked_without_changing_counts(self): + with tempfile.TemporaryDirectory() as directory: + library = Path(directory) / "libSceExample" + library.mkdir() + (library / "Export.cpp").write_text( + "static int Support() { return 0; }\n" + "static int Compute(int x) { return x * 2; }\n" + "int APS5_VABI A() { return 0; }\n" + "int APS5_VABI B(const void* p) { (void)p; return SCE_KERNEL_ERROR_EOPNOTSUPP; }\n" + "int APS5_VABI C(void* p) { (void)p; return 0x8A810001; }\n" + 'const char* APS5_VABI I() { return "constant; { text }"; }\n' + 'int APS5_VABI D() { throw std::runtime_error("no"); }\n' + 'std::string APS5_VABI J() { return std::string("not constant"); }\n' + "int APS5_VABI E() { return Support(); }\n" + "int APS5_VABI F(int a, int b) { return a + b; }\n" + "int APS5_VABI G() { NotImplemented_nid_no_patch(__func__); return 0; }\n" + "int APS5_VABI H(int a) { return Compute(a); }\n") + group = progress.scan_library(library) + self.assertEqual(group["inert_names"], ["A", "B", "C", "D", "E", "I"]) + self.assertEqual(group["done_names"], ["A", "B", "C", "D", "E", "F", "H", "I", "J"]) + self.assertEqual(group["todo_names"], ["G"]) + + def test_compare_marks_behaviourally_empty_implementations(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + library = root / "libSceExample" + library.mkdir() + kept = ( + "int APS5_VABI Ready() { return 0; }\n" + "int APS5_VABI Stubbed() { NotImplemented_nid_no_patch(__func__); return 0; }\n" + "static int Compute(int x) { return x * 2; }\n" + "int APS5_VABI Wrapped(int a) { return Compute(a); }\n") + added = ( + "static int Support() { return 0; }\n" + "int APS5_VABI A() { return 0; }\n" + "int APS5_VABI B(const void* p) { (void)p; return SCE_KERNEL_ERROR_EOPNOTSUPP; }\n" + "int APS5_VABI C(void* p) { (void)p; return 0x8A810001; }\n" + 'int APS5_VABI D() { throw std::runtime_error("no"); }\n' + "int APS5_VABI E() { return Support(); }\n" + "int APS5_VABI F(int a, int b) { return a + b; }\n") + (library / "Export.cpp").write_text(kept) + with patch.object(progress, "PRX", root): + base = progress.collect_libraries() + for group in base["groups"]: + del group["inert_names"] + (library / "Export.cpp").write_text(kept + added) + with patch.object(progress, "PRX", root): + head = progress.collect_libraries() + report = "\n".join(progress.compare("Libraries", "Library", "functions", base, head)) + self.assertIn("+6 implemented", report) + self.assertIn("⚪ 5 behaviourally empty (constant return or throw)", report) + empty = report.split("behaviourally empty (constant return or throw)")[1] + self.assertEqual([line for line in empty.splitlines() if line.startswith("|")], + ["| Library | Name |", "| - | - |"] + + [f"| libSceExample | `{name}` |" for name in "ABCDE"]) + self.assertEqual(progress.compare("Libraries", "Library", "functions", head, head), []) + (library / "Export.cpp").write_text("int APS5_VABI Ready() { return 0; }\n") + with patch.object(progress, "PRX", root): + before = progress.collect_libraries() + (library / "Export.cpp").write_text( + "int APS5_VABI Ready() { return 0; }\nint APS5_VABI F(int a, int b) { return a + b; }\n") + with patch.object(progress, "PRX", root): + after = progress.collect_libraries() + report = "\n".join(progress.compare("Libraries", "Library", "functions", before, after)) + self.assertIn("+1 implemented", report) + self.assertNotIn("behaviourally empty", report) + (library / "Export.cpp").write_text( + "int APS5_VABI Ready() { return 0; }\nint APS5_VABI F(int a, int b) { return a + b; }\nint APS5_VABI A() { return 0; }\n") + with patch.object(progress, "PRX", root): + grown = progress.collect_libraries() + report = "\n".join(progress.compare("Libraries", "Library", "functions", after, grown)) + self.assertIn("+1 implemented", report) + self.assertIn("| libSceExample | `A` |", report) + self.assertIn("⚪ 1 behaviourally empty (constant return or throw)", report) + + def test_cut_helpers_are_not_system_exports(self): + with tempfile.TemporaryDirectory() as directory: + library = Path(directory) / "libSceExample" + library.mkdir() + (library / "Export.cpp").write_text( + "int APS5_VABI Ready_nid_no_patch_cut() { return 1; }\n" + "int APS5_VABI Pending_nid_no_patch_cut() { NotImplemented_nid_no_patch(__func__); }\n" + "int APS5_VABI Real() { return 2; }\n", encoding="utf-8") + group = progress.scan_library(library) + self.assertEqual(group["done_names"], ["Real"]) + self.assertEqual(group["todo_names"], []) + self.assertEqual(group["inert_names"], ["Real"]) + + def test_empty_libraries(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + owner = root / "libSceExample" + alias = root / "libSceExampleBackend" + dummy = root / "libSceDummy" + for path in (owner, alias, dummy): + path.mkdir() + (owner / "Export.cpp").write_text("int APS5_VABI Ready() { return 0; }\n") + (alias / "Export.cpp").write_text('#include "prx/libSceExample/Export.cpp"\n') + (dummy / "Export.cpp").write_text('#include "prx/libc/include/General.hpp"\nextern "C" {\nAPS5_DUMMY_FUN\n}\n') + with patch.object(progress, "PRX", root): + data = progress.collect_libraries() + groups = {group["name"]: group for group in data["groups"]} + self.assertEqual(groups[alias.name]["shared_sources"], owner.name) + self.assertNotIn("shared_sources", groups[dummy.name]) + self.assertEqual((data["done"], data["total"]), (1, 1)) + html = progress.table("Libraries", "Library", data) + self.assertIn(f'{alias.name}Shared sources:', html) + self.assertIn(f'{dummy.name}No exports', html) + self.assertNotIn("0", 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 new file mode 100644 index 0000000000..813b596990 --- /dev/null +++ b/tools/tests/test_progress_history.py @@ -0,0 +1,146 @@ +import os +import subprocess +import sys +import tempfile +import unittest +from datetime import date +from pathlib import Path +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import progress +import progress_history + +OPCODES = Path("core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp") + + +def commit(root, day, message, body, time="12:00:00Z"): + library = root / "core/libs/prx/libSceExample" + library.mkdir(parents=True, exist_ok=True) + (library / "Export.cpp").write_text(body) + (root / OPCODES).parent.mkdir(parents=True, exist_ok=True) + (root / OPCODES).write_text("enum class RdnaOpcode : int {\n SEndpgm,\n};\n") + env = {**os.environ, "GIT_AUTHOR_DATE": f"{day}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: + root = Path(directory) + subprocess.run(["git", "-C", directory, "init", "-q"], check=True) + commit(root, "2026-01-01", "first", "int APS5_VABI A() { Stub(); return 0; }\n" + stub) + commit(root, "2026-01-02", "second", "int APS5_VABI A() { return 0; }\nint APS5_VABI B() { return 0; }\n") + commit(root, "2026-01-02", "third", "int APS5_VABI A() { return 0; }\nint APS5_VABI B() { Stub(); return 0; }\n" + stub) + with patch.multiple(progress, ROOT=root, PRX=root / "core/libs/prx", OPCODES=root / OPCODES): + points = progress_history.collect("HEAD") + html = progress_history.page(points) + self.assertEqual([p["date"] for p in points], ["2026-01-01", "2026-01-02"]) + self.assertEqual([(p["libraries"]["done"], p["libraries"]["total"]) for p in points], [(0, 1), (1, 2)]) + self.assertIn("{label}<", svg) + + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_progress_read_encoding.py b/tools/tests/test_progress_read_encoding.py new file mode 100644 index 0000000000..40f3d7e02a --- /dev/null +++ b/tools/tests/test_progress_read_encoding.py @@ -0,0 +1,94 @@ +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +TOOLS = Path(__file__).resolve().parents[1] + +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")) +""" + +INCLUDE = 'include("${CMAKE_CURRENT_SOURCE_DIR}/../libSceOther/libSceOther.cmake")\n' + + +class ProgressReadEncodingTests(unittest.TestCase): + def test_utf8_cmake_with_ascii_default_codec(self): + with tempfile.TemporaryDirectory(prefix="progress read ") as directory: + root = Path(directory) + (root / "libSceOther").mkdir() + library = root / "libSceExample" + library.mkdir() + (library / "CMakeLists.txt").write_bytes(("# \u2010 \u2192 shared\n" + INCLUDE).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", 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") + + 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 new file mode 100644 index 0000000000..cf08a54344 --- /dev/null +++ b/tools/tests/test_progress_render_sliver.py @@ -0,0 +1,27 @@ +import importlib.util +import unittest +from pathlib import Path + +SPEC = importlib.util.spec_from_file_location("progress", Path(__file__).resolve().parents[1] / "progress.py") +progress = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(progress) + + +class ProgressRenderSliverTests(unittest.TestCase): + def test_render_with_sliver_group(self): + libraries = {"done": 301, "total": 301, "percent": 100, "groups": [ + {"name": "libA", "label": "libA", "done": 300, "todo": 0}, + {"name": "libB", "label": "libB", "done": 1, "todo": 0}]} + shaders = {"done": 0, "total": 0, "percent": 0, "groups": []} + svg = progress.render(libraries, shaders) + self.assertIn("libA", svg) + self.assertNotIn(">lib", svg) + + def test_cells_without_area(self): + self.assertEqual(progress.cells(5, 0, 0, 0, 10), []) + self.assertEqual(progress.cells(5, 0, 0, -1, 10), []) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_progress_wrappers.py b/tools/tests/test_progress_wrappers.py new file mode 100644 index 0000000000..3897bd125c --- /dev/null +++ b/tools/tests/test_progress_wrappers.py @@ -0,0 +1,82 @@ +import sys +import tempfile +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import progress + + +def scan(source): + with tempfile.TemporaryDirectory() as directory: + library = Path(directory) / "libSceExample" + library.mkdir() + (library / "Export.cpp").write_text(source) + return progress.scan_library(library) + + +STUB_BODY = "{ NotImplemented_nid_no_patch(__func__); return 0; }\n" +CALLER = "int APS5_VABI sceExampleCall() { return Helper(); }\n" + + +class WrapperTests(unittest.TestCase): + def assertTodo(self, source): + group = scan(source) + self.assertEqual(group["done_names"], []) + self.assertEqual(group["todo_names"], ["sceExampleCall"]) + + def test_non_static_helper_in_anonymous_namespace(self): + self.assertTodo("namespace {\nint Helper() " + STUB_BODY + "}\n" + CALLER) + + def test_noexcept_helper(self): + self.assertTodo("static int Helper() noexcept " + STUB_BODY + CALLER) + + def test_trailing_return_type_helper(self): + self.assertTodo("static auto Helper() -> int " + STUB_BODY + CALLER) + + def test_pointer_next_to_name_helper(self): + self.assertTodo("static int *Helper() { NotImplemented_nid_no_patch(__func__); return nullptr; }\n" + CALLER) + + def test_static_helper_still_works(self): + self.assertTodo("static int Helper() " + STUB_BODY + CALLER) + + def test_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") + self.assertEqual(group["done_names"], ["sceExampleCall"]) + self.assertEqual(group["todo_names"], []) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_select_tests.py b/tools/tests/test_select_tests.py new file mode 100644 index 0000000000..e86b16ccad --- /dev/null +++ b/tools/tests/test_select_tests.py @@ -0,0 +1,275 @@ +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) + + +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..0c462bb7ea --- /dev/null +++ b/tools/tests/test_shader_cache_runtime_sources.py @@ -0,0 +1,216 @@ +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 + + +SOURCE = Path(sys.argv.pop(1)) +CMAKE = sys.argv.pop(1) + + +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 ')} + + +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)) + + +unittest.main() diff --git a/tools/tests/test_stubgen.py b/tools/tests/test_stubgen.py new file mode 100644 index 0000000000..bc4bf37488 --- /dev/null +++ b/tools/tests/test_stubgen.py @@ -0,0 +1,204 @@ +import contextlib +import io +import os +import stat +import tempfile +from pathlib import Path +import sys +import unittest + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import check_conventions +import stubgen + + +class StubgenTests(unittest.TestCase): + def test_default_debt_link_resolves_from_the_library_debt_file(self): + _, td = stubgen.generate("libkernel", ["AC1FtjqMCL0"], {}) + self.assertEqual(check_conventions.debt_name(td), "libkernel") + found = check_conventions.links("docs/dev/debt/libkernel.md", td.strip()) + self.assertTrue(found) + for item in found: + self.assertEqual(item[1], "core/libs/prx/libkernel/Unimplemented.cpp") + + def test_explicit_td_path_is_verbatim(self): + _, td = stubgen.generate("libkernel", ["AC1FtjqMCL0"], {}, td_path="x/y.cpp") + self.assertIn("(x/y.cpp)", td) + + def test_code_still_generated(self): + code, _ = stubgen.generate("libkernel", ["AC1FtjqMCL0"], {}) + 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" + nids.write_text("AAAAAAAAAAA\n", encoding="utf-8") + output, debt = io.StringIO(), io.StringIO() + with contextlib.redirect_stdout(output), contextlib.redirect_stderr(debt): + self.assertEqual(stubgen.main(["--module", "libSceExample", "--nids", str(nids)]), 0) + self.assertTrue(output.getvalue().endswith("\n}\n")) + self.assertNotIn("unknown name, signature", output.getvalue()) + self.assertIn("unknown name, signature", debt.getvalue()) + + def test_output_cannot_overwrite_an_input_file(self): + with tempfile.TemporaryDirectory() as directory: + nids = Path(directory) / "nids.txt" + original = "AAAAAAAAAAA\n" + nids.write_text(original, encoding="utf-8") + with contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as error: + stubgen.main(["--module", "libSceExample", "--nids", str(nids), "--out", str(nids)]) + self.assertEqual(error.exception.code, 2) + self.assertEqual(nids.read_text(encoding="utf-8"), original) + + def test_debt_write_failure_preserves_existing_code(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") + output.write_text("existing source\n", encoding="utf-8") + arguments = ["--module", "libSceExample", "--nids", str(nids), "--out", str(output), "--td-out", str(root / "absent" / "debt.txt")] + with contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as error: + stubgen.main(arguments) + self.assertEqual(error.exception.code, 2) + self.assertEqual(output.read_text(encoding="utf-8"), "existing source\n") + self.assertEqual(sorted(p.name for p in root.iterdir()), ["Export.cpp", "nids.txt"]) + + def test_hard_linked_database_output_is_rejected(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + nids, database, output = root / "nids.txt", root / "names.txt", root / "Export.cpp" + nids.write_text("AAAAAAAAAAA\n", encoding="utf-8") + original = "AAAAAAAAAAA sceExample\n" + database.write_text(original, encoding="utf-8") + os.link(database, output) + arguments = ["--module", "libSceExample", "--nids", str(nids), "--db", str(database), "--out", str(output)] + with contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as error: + stubgen.main(arguments) + self.assertEqual(error.exception.code, 2) + self.assertEqual(database.read_text(encoding="utf-8"), original) + self.assertEqual(output.read_text(encoding="utf-8"), original) + + def test_malformed_nid_is_rejected_before_generating_cpp(self): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "nids.txt" + path.write_text('AAAAAAAAAAA\ninvalid"nid\n', encoding="utf-8") + with self.assertRaisesRegex(ValueError, "invalid NID"): + stubgen.parse_nids(path) + + def test_code_and_debt_outputs_cannot_overwrite_each_other(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + nids = root / "nids.txt" + nids.write_text("AAAAAAAAAAA\n", encoding="utf-8") + output = root / "Export.cpp" + arguments = ["--module", "libSceExample", "--nids", str(nids), "--out", str(output), "--td-out", str(output)] + with contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as error: + stubgen.main(arguments) + self.assertEqual(error.exception.code, 2) + self.assertFalse(output.exists()) + + def test_valid_annotations_comments_and_duplicates_are_preserved(self): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "nids.txt" + 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) + nids = root / "nids.txt" + nids.write_text("AAAAAAAAAAA\n", encoding="utf-8") + code, debt = root / "Export.cpp", root / "debt.txt" + arguments = ["--module", "libSceExample", "--nids", str(nids), "--out", str(code), "--td-out", str(debt)] + self.assertEqual(stubgen.main(arguments), 0) + self.assertIn('APS5_EXPORT("AAAAAAAAAAA",', code.read_text(encoding="utf-8")) + self.assertIn("unknown name, signature", debt.read_text(encoding="utf-8")) + if sys.platform != "win32": + mask = os.umask(0o022) + os.umask(mask) + 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"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_stubgen_stdout_newlines.py b/tools/tests/test_stubgen_stdout_newlines.py new file mode 100644 index 0000000000..427d45a14c --- /dev/null +++ b/tools/tests/test_stubgen_stdout_newlines.py @@ -0,0 +1,38 @@ +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + +tools = Path(__file__).resolve().parents[1] + + +class StubgenStdoutNewlineTests(unittest.TestCase): + def run_stubgen(self, *extra): + with tempfile.TemporaryDirectory() as d: + nids = Path(d) / "nids.txt" + nids.write_bytes(b"AC1FtjqMCL0\n") + return subprocess.run( + [sys.executable, str(tools / "stubgen.py"), "--module", "libkernel", "--nids", str(nids)] + list(extra), + capture_output=True, + check=True, + ) + + def test_code_on_stdout_uses_lf(self): + result = self.run_stubgen() + self.assertIn(b"\n", result.stdout) + self.assertNotIn(b"\r\n", result.stdout) + + def test_td_only_on_stdout_uses_lf(self): + result = self.run_stubgen("--td-only") + self.assertIn(b"\n", result.stdout) + self.assertNotIn(b"\r\n", result.stdout) + + 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 new file mode 100644 index 0000000000..106d3bc3ae --- /dev/null +++ b/tools/tests/test_test_registration.py @@ -0,0 +1,347 @@ +from pathlib import Path +import os +import shutil +import subprocess +import sys +import tempfile +import unittest +from types import SimpleNamespace +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +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) + + def test_direct_executable(self): + names = self.registered('add_test("bounds" "/build/tests/buffer_bounds_tests")\n') + self.assertIn("buffer_bounds_tests", names) + self.assertNotIn("bounds", names) + + def test_wrapped_executable(self): + names = self.registered('add_test("bounds" "/usr/bin/python3" "runner.py" "/build/tests/buffer_bounds_tests")\n') + self.assertIn("buffer_bounds_tests", names) + + def test_windows_executable_and_test_name(self): + names = self.registered('add_test("missing_tests.exe" "C:/Program Files/Python/python.exe" "runner.py" "C:/build/tests/buffer_bounds_tests.exe")\n') + self.assertIn("buffer_bounds_tests", names) + self.assertNotIn("missing_tests", names) + + def test_hyphenated_executable_is_discovered(self): + output = "tests/buffer-bounds_tests: CXX_EXECUTABLE_LINKER\n" + 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", 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") + + def test_nested_test_output_is_discovered(self): + output = "tests/gpu/buffer_bounds_tests: CXX_EXECUTABLE_LINKER\n" + 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", 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") + + def test_quoted_cmake_path_is_unescaped(self): + names = self.registered(r'add_test("bounds" "/build/tests/a\"b_tests")' + '\n') + self.assertIn('a"b_tests', names) + + def test_bracket_quoted_command_is_recognized(self): + names = self.registered('add_test([=[bounds]=] [=[/build/tests/buffer_bounds_tests]=])\n') + self.assertIn("buffer_bounds_tests", names) + + def test_multiline_test_command_is_recognized(self): + 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) + + def test_option_value_does_not_register_executable(self): + names = self.registered('add_test("bounds" "/usr/bin/python3" "runner.py" "--report=/build/tests/missing_tests")\n') + self.assertNotIn("missing_tests", names) + + def test_reenabled_test_is_counted_as_run(self): + names = self.registered('add_test("bounds" "/build/tests/buffer_bounds_tests")\nset_tests_properties("bounds" PROPERTIES DISABLED TRUE)\nset_tests_properties("bounds" PROPERTIES DISABLED OFF)\n') + self.assertIn("buffer_bounds_tests", names) + + def test_disabled_wrapper_does_not_register_its_argument(self): + names = self.registered('add_test("bounds" "/usr/bin/python3" "runner.py" "/build/tests/buffer_bounds_tests")\nset_tests_properties([=[bounds]=] PROPERTIES DISABLED "ON")\n') + self.assertNotIn("buffer_bounds_tests", names) + + def test_command_text_inside_an_argument_is_not_another_test(self): + names = self.registered('add_test("bounds" "/usr/bin/python3" [=[\nadd_test("fake" "/build/tests/missing_tests")\n]=])\n') + self.assertNotIn("missing_tests", names) + + def test_windows_escaped_path_is_decoded(self): + names = self.registered(r'add_test("bounds" "C:\\build\\tests\\buffer_bounds_tests.exe")' + '\n') + self.assertIn("buffer_bounds_tests", names) + + def test_unreferenced_registration_directory_does_not_count(self): + with tempfile.TemporaryDirectory() as directory: + build = Path(directory) + (build / "CTestTestfile.cmake").write_text("", encoding="utf-8") + stale = build / "removed" + stale.mkdir() + (stale / "CTestTestfile.cmake").write_text( + 'add_test("bounds" "/build/tests/buffer_bounds_tests")\n', encoding="utf-8") + self.assertNotIn("buffer_bounds_tests", check_test_registration.registered(build)) + (build / "CTestTestfile.cmake").unlink() + self.assertNotIn("buffer_bounds_tests", check_test_registration.registered(build)) + + def test_only_reachable_nested_directories_are_registered(self): + with tempfile.TemporaryDirectory() as directory: + build = Path(directory) + active = build / "active tests" + nested = active / "nested" + removed = active / "removed" + nested.mkdir(parents=True) + removed.mkdir() + (build / "CTestTestfile.cmake").write_text('subdirs("active tests")\n', encoding="utf-8") + (active / "CTestTestfile.cmake").write_text('subdirs([=[nested]=])\n', encoding="utf-8") + for path, name in ((nested, "active_tests"), (removed, "removed_tests")): + (path / "CTestTestfile.cmake").write_text( + f'add_test("check" "/build/tests/{name}")\n', encoding="utf-8") + self.assertEqual(check_test_registration.registered(build), {"active_tests"}) + + def test_external_registration_directory_is_followed(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + build, external = root / "build", root / "external" + build.mkdir() + external.mkdir() + (build / "CTestTestfile.cmake").write_text( + f'subdirs("{external.as_posix()}")\n', encoding="utf-8") + (external / "CTestTestfile.cmake").write_text( + 'add_test("bounds" "/build/tests/buffer_bounds_tests")\n', encoding="utf-8") + self.assertEqual(check_test_registration.registered(build), {"buffer_bounds_tests"}) + def test_unsupported_generator_never_runs_build_tool(self): + for generator in ("Unix Makefiles", "Visual Studio 17 2022", ""): + with self.subTest(generator=generator), tempfile.TemporaryDirectory() as directory: + build = Path(directory) + (build / "CMakeCache.txt").write_text("CMAKE_MAKE_PROGRAM:FILEPATH=make\nCMAKE_GENERATOR:INTERNAL=" + generator + "\n", encoding="utf-8") + with patch.object(check_test_registration.subprocess, "run") as run: + with self.assertRaisesRegex(ValueError, "requires a Ninja generator"): + check_test_registration.built(build) + run.assert_not_called() + + def test_missing_make_program_never_runs_build_tool(self): + for program in (None, "", "CMAKE_MAKE_PROGRAM-NOTFOUND"): + with self.subTest(program=program), tempfile.TemporaryDirectory() as directory: + build = Path(directory) + cache = "CMAKE_GENERATOR:INTERNAL=Ninja\n" + if program is not None: + cache += "CMAKE_MAKE_PROGRAM:FILEPATH=" + program + "\n" + (build / "CMakeCache.txt").write_text(cache, encoding="utf-8") + with patch.object(check_test_registration.subprocess, "run") as run: + with self.assertRaises(FileNotFoundError): + check_test_registration.built(build) + run.assert_not_called() + + @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"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_trace_digest.py b/tools/tests/test_trace_digest.py new file mode 100644 index 0000000000..a7fa3619db --- /dev/null +++ b/tools/tests/test_trace_digest.py @@ -0,0 +1,728 @@ +import contextlib +import gzip +import io +import json +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import trace_digest + + +def complete(tid, name, ts, dur): + return {"name": name, "ph": "X", "ts": ts, "dur": dur, "pid": 7, "tid": tid} + + +EVENTS = [ + {"name": "process_name", "ph": "M", "ts": 0, "pid": 7, "tid": 0, "args": {"name": "AnyPS5"}}, + {"name": "thread_name", "ph": "M", "ts": 0, "pid": 7, "tid": 1, "args": {"name": "queue worker 0x0"}}, + {"name": "thread_name", "ph": "M", "ts": 0, "pid": 7, "tid": 2, "args": {"name": "presenter"}}, + complete(1, "Driver.WorkerWait", 0, 4000), + complete(1, "Driver.Packet", 4000, 5000), + complete(1, "Driver.Packet.memory_wait", 5000, 2000), + complete(1, "Driver.Packet", 12000, 16000), + complete(1, "Driver.Packet.draw", 13000, 7000), + complete(2, "Driver.Present", 0, 2000), + complete(2, "Driver.Present.inflight_wait", 500, 1000), + complete(2, "Driver.Present", 10000, 16000), + complete(2, "Driver.Present.inflight_wait", 10500, 14500), + {"name": "queue worker 0x0 folded", "ph": "C", "ts": 0, "pid": 7, "tid": 1, "args": {"ms": 1.5}}, + {"name": "present", "ph": "i", "ts": 0, "pid": 7, "tid": 2, "s": "g", "args": {"present": 600}}, + {"name": "present", "ph": "i", "ts": 10000, "pid": 7, "tid": 2, "s": "g", "args": {"present": 601}}, + {"name": "present", "ph": "i", "ts": 30000, "pid": 7, "tid": 2, "s": "g", "args": {"present": 602}}, +] + +SUMMARY = {"commit": "abc", "dirty": False, "env": {"APS5_TRACE_FRAMES": "600:3"}, "gpu": "GPU", "driver": "Driver 1", "os": "OS 1", + "resolution": "1920x1080", "window": {"spec": "frames 600:3", "first_present": 600, "ms": 40.0}, "presents": 3, + "frame_ms": {"p50": 10.0, "p90": 20.0, "p99": 20.0, "max": 20.0}, "gpu_busy_ms_per_present": 5.0, + "worker_busy_pct": {"queue worker 0x0": 50.0}, "top_phases_ms_per_frame": {"Driver.Packet": 7.0}, "trace_dropped": False, + "trace": "aps5-trace-abc-7.json.gz"} + +STACKS = "# samples=5 depth=64 truncated=1\n# module a.prx 0x1000 0x1\nntdll.dll+0x1;a.prx+0x10;a.prx+0x20 3\nntdll.dll+0x1;a.prx+0x10;a.prx+0x30 1\n[truncated];a.prx+0x40;a.prx+0x30 1\n" + + +class TraceDigestTests(unittest.TestCase): + def 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) + self.trace = root / "aps5-trace-abc-7.json.gz" + with gzip.open(self.trace, "wt", encoding="utf-8") as handle: + json.dump({"traceEvents": EVENTS}, handle) + (root / "aps5-summary.jsonl").write_text(json.dumps(dict(SUMMARY, trace="other.json.gz")) + "\n" + json.dumps(SUMMARY) + "\n", encoding="utf-8") + self.stacks = root / "worker.collapsed.txt" + self.stacks.write_text(STACKS, encoding="utf-8") + + def tearDown(self): + self.directory.cleanup() + + def 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) + summary.write_text(json.dumps(SUMMARY) + "\n" + json.dumps(other) + "\n", encoding="utf-8") + selected = trace_digest.load_summary(summary, self.trace) + self.assertEqual(selected["trace"], self.trace.name) + self.assertEqual(selected["presents"], 3) + + def test_explicit_summary_without_matching_trace_is_not_used(self): + summary = Path(self.directory.name) / "unrelated-summary.jsonl" + summary.write_text(json.dumps(dict(SUMMARY, trace="other.json.gz")) + "\n", encoding="utf-8") + self.assertIsNone(trace_digest.load_summary(summary, self.trace)) + + def test_frames_end_at_the_window(self): + result = self.result() + self.assertEqual(result["frames"], 3) + self.assertAlmostEqual(result["window_ms"], 40.0) + + def test_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]) + self.assertAlmostEqual(slowest[0]["ms"], 20.0) + largest = {(entry["name"], entry["thread"]): entry["ms"] for entry in slowest[0]["slices"]} + self.assertAlmostEqual(largest[("Driver.Packet", "queue worker 0x0")], 16.0) + self.assertAlmostEqual(largest[("Driver.Present", "presenter")], 16.0) + self.assertAlmostEqual(largest[("Driver.Present.inflight_wait", "presenter")], 14.5) + + def test_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"] + self.assertAlmostEqual(worker["wait_ms_per_frame"], 6.0 / 3) + self.assertAlmostEqual(worker["busy_ms_per_frame"], 19.0 / 3) + self.assertAlmostEqual(worker["untraced_ms_per_frame"], 15.0 / 3) + self.assertAlmostEqual(worker["folded_ms_per_frame"], 1.5 / 3) + self.assertEqual(worker["top"][0]["name"], "Driver.Packet") + presenter = threads["presenter"] + self.assertAlmostEqual(presenter["wait_ms_per_frame"], 15.5 / 3) + self.assertAlmostEqual(presenter["busy_ms_per_frame"], 2.5 / 3) + + def test_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") + self.assertAlmostEqual(largest[0]["ms"], 21.0 / 3) + + def test_longest_waits(self): + waits = self.result()["longest_waits"] + self.assertEqual((waits[0]["name"], waits[0]["thread"], waits[0]["present"]), ("Driver.Present.inflight_wait", "presenter", 601)) + self.assertAlmostEqual(waits[0]["ms"], 14.5) + self.assertEqual(waits[1]["name"], "Driver.WorkerWait") + + def test_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) + self.assertAlmostEqual(stacks["truncated_pct"], 20.0) + self.assertEqual(stacks["self"][0], {"frame": "a.prx+0x20", "pct": 60.0}) + total = {entry["frame"]: entry["pct"] for entry in stacks["total"]} + self.assertEqual(stacks["common_frames"], 2) + self.assertNotIn("a.prx+0x10", total) + self.assertEqual(total, {"a.prx+0x20": 60.0, "a.prx+0x30": 40.0, "a.prx+0x40": 20.0}) + + def test_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): + self.assertEqual(trace_digest.main([str(self.trace), "--stacks", str(self.stacks)]), 0) + text = output.getvalue() + for heading in ("**Slowest frames**", "**Threads**", "**Largest slices**", "**Longest waits**", "**Worker stacks**"): + self.assertIn(heading, text) + self.assertLess(len(text.encode("utf-8")), 5000) + output = io.StringIO() + with contextlib.redirect_stdout(output): + trace_digest.main([str(self.trace), "--json"]) + self.assertEqual(json.loads(output.getvalue())["frames"], 3) + + 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, + presents=1800, frame_ms={"p50": 16.6, "p90": 18.0, "p99": p99, "max": p99 + 5.0}, gpu_busy_ms_per_present=gpu_ms, trace="") + line.update(changes) + return line + + +class IntervalDigestTests(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + self.path = Path(self.directory.name) / "aps5-summary.jsonl" + lines = [SUMMARY, interval_line(0.0, 30.0, 20.0, 5.0), interval_line(30.0, 60.5, 40.0, 10.0, trace_dropped=True), + interval_line(60.5, 90.5, 0.0, 0.0, worker_busy_pct={}, top_phases_ms_per_frame={}), interval_line(90.5, 120.5, 20.0, 5.0, gpu="Other GPU")] + self.path.write_text("\n".join(json.dumps(line) for line in lines) + "\n", encoding="utf-8") + + def tearDown(self): + self.directory.cleanup() + + 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"] + self.assertEqual((first["start_s"], first["end_s"], first["presents"]), (0.0, 30.0, 1800)) + self.assertEqual(first["busiest_worker"], {"thread": "queue worker 0x0", "pct": 50.0}) + self.assertEqual(first["top_phase"], {"name": "Driver.Packet", "ms_per_frame": 7.0}) + self.assertTrue(second["trace_dropped"]) + self.assertIsNone(third["busiest_worker"]) + 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): + self.assertEqual(trace_digest.main([str(self.path)]), 0) + text = output.getvalue() + self.assertIn("**4 intervals**, 0.0 to 120.5 s, 7200 presents", text) + self.assertIn("| 2 | 30-60 (dropped) | 1800 | 16.6 | 18.0 | 40.0 | 45.0 | 10.0 | 50 % | Driver.Packet 7.0 |", text) + self.assertIn("p99 ms +@.+ 0-40.0", text) + self.assertIn("changed at interval 4", text) + output = io.StringIO() + with contextlib.redirect_stdout(output): + trace_digest.main([str(self.path), "--json"]) + self.assertEqual(len(json.loads(output.getvalue())["intervals"]), 4) + + def test_file_without_intervals(self): + self.path.write_text(json.dumps(SUMMARY) + "\n", encoding="utf-8") + output = io.StringIO() + with contextlib.redirect_stdout(output): + 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..6ee6d2095e --- /dev/null +++ b/tools/tests/test_unwrap_self.py @@ -0,0 +1,668 @@ +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") + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/trace_digest.py b/tools/trace_digest.py new file mode 100644 index 0000000000..6d03d2200c --- /dev/null +++ b/tools/trace_digest.py @@ -0,0 +1,382 @@ +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, 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. + +A summary file written by APS5_TRACE_SUMMARY_EVERY (aps5-summary.jsonl, or any .jsonl) is digested as a table with +one row per interval and a text sparkline of the frame p99 and of the GPU busy time per present; the lines of +window captures in the same file are skipped.""" + +RAMP = "._:-=+*#%@" + + +def is_wait(name): + return "wait" in name.lower() + + +class Trace: + def __init__(self, document): + self.threads = {} + self.slices = collections.defaultdict(list) + self.presents = [] + self.folded = collections.defaultdict(float) + 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": + self.folded[event["tid"]] += sum(float(value) for value in event.get("args", {}).values()) + self.presents.sort() + for slices in self.slices.values(): + slices.sort(key=lambda entry: (entry[0], -entry[1])) + stack = [] + for entry in slices: + 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}") + + def frames(self, end): + starts = [ts for ts, _ in self.presents] + if end is None: + end = starts[-1] if starts else 0.0 + bounds = starts + ([end] if not starts or end > starts[-1] else []) + return [(index, bounds[index], bounds[index + 1], self.presents[index][1]) for index in range(len(bounds) - 1)] + + +def load_trace(path): + opener = gzip.open if str(path).endswith(".gz") else open + with opener(path, "rt", encoding="utf-8-sig") as handle: + return Trace(json.load(handle)) + + +def load_summary(path, trace_path): + candidate = Path(path) if path else Path(trace_path).with_name("aps5-summary.jsonl") + if not candidate.exists(): + return None + lines = [json.loads(line) for line in candidate.read_text(encoding="utf-8-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 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(" ") + 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 + + +def digest(trace, summary=None, stacks=None, worst=5, top=10): + window_ms = summary["window"]["ms"] if summary else None + frames = trace.frames(window_ms * 1000.0 if window_ms is not None else None) + span_us = frames[-1][2] - frames[0][1] if frames else 0.0 + count = max(len(frames), 1) + result = {"summary": summary, "frames": len(frames), "window_ms": span_us / 1000.0} + + slowest = [] + for index, begin, end, present in sorted(frames, key=lambda frame: frame[1] - frame[2])[:worst]: + inside = collections.Counter() + for tid, slices in trace.slices.items(): + for ts, dur, name, _, _ in slices: + overlap = min(ts + dur, end) - max(ts, begin) + if overlap > 0: + inside[(name, trace.name(tid))] += overlap + slowest.append({"present": present, "ms": (end - begin) / 1000.0, "at_ms": begin / 1000.0, + "slices": [{"name": name, "thread": thread, "ms": us / 1000.0} for (name, thread), us in inside.most_common(4)]}) + result["slowest_frames"] = slowest + + threads = [] + totals = collections.Counter() + waits = [] + for tid 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: + 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, + "folded_ms_per_frame": trace.folded.get(tid, 0.0) / count, + "busy_pct": 100.0 * (traced - waited) / span_us if span_us else 0.0, + "wait_pct": 100.0 * waited / span_us if span_us else 0.0, + "top": [{"name": name, "ms_per_frame": us / 1000.0 / count} for name, us in leaders.most_common(2)]}) + result["threads"] = threads + result["top_slices_ms_per_frame"] = [{"name": name, "ms": us / 1000.0 / count} for name, us in totals.most_common(top)] + starts = [ts for ts, _ in trace.presents] + result["longest_waits"] = [{"name": name, "thread": thread, "ms": dur / 1000.0, "at_ms": ts / 1000.0, + "present": trace.presents[bisect.bisect_right(starts, ts) - 1][1] if starts and ts >= starts[0] else None} + for dur, name, thread, ts in sorted(waits, reverse=True)[:top]] + + 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() + inclusive = collections.Counter() + truncated = 0 + common = None + for frames_of, number in stacks: + if frames_of[0] == "[truncated]": + truncated += number + frames_of = frames_of[1:] + elif frames_of: + common = set(frames_of) if common is None else common & set(frames_of) + if not frames_of: + continue + own[frames_of[-1]] += number + for frame in set(frames_of): + inclusive[frame] += number + common = common or set() + for frame in common: + del inclusive[frame] + share = lambda value: 100.0 * value / samples if samples else 0.0 + result["stacks"] = {"samples": samples, "truncated_pct": share(truncated), "common_frames": len(common), + "self": [{"frame": frame, "pct": share(value)} for frame, value in own.most_common(top)], + "total": [{"frame": frame, "pct": share(value)} for frame, value in inclusive.most_common(top)]} + return result + + +def 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"] + lines.append(f"**{summary['window']['spec']}** from present {summary['window']['first_present']}: {summary['presents']} presents, " + f"frame p50 {frame['p50']:.1f} / p90 {frame['p90']:.1f} / p99 {frame['p99']:.1f} / max {frame['max']:.1f} ms, " + f"GPU busy {summary['gpu_busy_ms_per_present']:.1f} ms per present") + lines.append(f"{summary['gpu']}, {summary['driver']}, {summary['os']}, {summary['resolution']}, commit {summary['commit']}" + f"{' (dirty)' if summary.get('dirty') else ''}, dropped: {'yes' if summary['trace_dropped'] else 'no'}") + lines.append("") + lines.append("```json") + lines.append(json.dumps(summary, separators=(",", ":"))) + lines.append("```") + else: + lines.append(f"{result['frames']} frames over {result['window_ms']:.1f} ms (no summary line found)") + lines.append("") + lines.append("**Slowest frames** (largest slices inside each, inclusive)") + lines.append("") + for frame in result["slowest_frames"]: + parts = ", ".join(f"{entry['name']} ({entry['thread']}) {entry['ms']:.1f}" for entry in frame["slices"]) + lines.append(f"- present {frame['present']}: {frame['ms']:.1f} ms at {frame['at_ms']:.0f} ms: {parts}") + lines.append("") + lines.append("**Threads** (ms per frame)") + lines.append("") + lines.append("| thread | busy | wait | untraced | in slices < 20 us | largest |") + lines.append("|---|---|---|---|---|---|") + for thread in result["threads"]: + largest = ", ".join(f"{entry['name']} {entry['ms_per_frame']:.1f}" for entry in thread["top"]) + lines.append(f"| {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("") + lines.append("**Longest waits**") + lines.append("") + for wait in result["longest_waits"]: + lines.append(f"- {wait['name']} ({wait['thread']}) {wait['ms']:.1f} ms at {wait['at_ms']:.0f} ms, present {wait['present']}") + if "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("") + lines.append(f"**Worker stacks** ({stacks['samples']} samples, {stacks['truncated_pct']:.1f} % truncated; total leaves out the {stacks['common_frames']} frames every complete stack shares)") + lines.append("") + lines.append("- self: " + ", ".join(f"{entry['frame']} {entry['pct']:.1f} %" for entry in stacks["self"])) + lines.append("- total: " + ", ".join(f"{entry['frame']} {entry['pct']:.1f} %" for entry in stacks["total"])) + return "\n".join(lines) + "\n" + + +def load_intervals(path): + 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] + + +def sparkline(values): + high = max(values, default=0.0) + if high <= 0: + return RAMP[0] * len(values) + return "".join(RAMP[min(len(RAMP) - 1, int(value / high * (len(RAMP) - 1) + 0.5))] for value in values) + + +def digest_intervals(lines): + rows = [] + for line in lines: + workers = line.get("worker_busy_pct", {}) + busiest = max(workers.items(), key=lambda entry: entry[1]) if workers else None + 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": 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 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} + + +def markdown_intervals(result): + rows = result["intervals"] + identity = result["identity"] + if not rows: + return "no interval lines (interval_start_s) found\n" + lines = [f"**{len(rows)} intervals**, {rows[0]['start_s']:.1f} to {rows[-1]['end_s']:.1f} s, {sum(row['presents'] for row in rows)} presents", + f"{identity['gpu']}, {identity['driver']}, {identity['os']}, {identity['resolution']}, commit {identity['commit']}" + f"{' (dirty)' if identity.get('dirty') else ''}"] + if result["identity_changed_at"]: + lines.append("commit, environment, GPU, driver or OS changed at interval " + ", ".join(str(index + 1) for index in result["identity_changed_at"])) + lines += ["", "| # | seconds | presents | p50 | p90 | p99 | max | GPU busy | busiest worker | largest phase |", "|---|---|---|---|---|---|---|---|---|---|"] + for index, row in enumerate(rows, 1): + frame = row["frame_ms"] + 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} | {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("--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 + 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__": + 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..1ce81f2e12 --- /dev/null +++ b/tools/unwrap_self.py @@ -0,0 +1,352 @@ +#!/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_EMPTY_LOAD_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_EMPTY_LOAD_PADDING: + raise Refused("empty load alignment requires more than %d bytes of output padding" % MAX_EMPTY_LOAD_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("